summaryrefslogtreecommitdiff
path: root/Userland/Libraries
diff options
context:
space:
mode:
authorDaniel Bertalan <dani@danielbertalan.dev>2021-05-08 20:37:43 +0200
committerAndreas Kling <kling@serenityos.org>2021-05-16 11:50:56 +0200
commitbe519022c3326f7d5cec964b6d3b72d419b0f522 (patch)
treee8d4698840276142443a0d0b1c99022bc6ccb2cf /Userland/Libraries
parent1b347298f118e4e78e7cef41096623913c9e640e (diff)
downloadserenity-be519022c3326f7d5cec964b6d3b72d419b0f522.zip
LibVT: Implement new ANSI escape sequence parser
This commit replaces the former, hand-written parser with a new one that can be generated automatically according to a state change diagram. The new `EscapeSequenceParser` class provides a more ergonomic interface to dealing with escape sequences. This interface has been inspired by Alacritty's [vte library](https://github.com/alacritty/vte/). I tried to avoid changing the application logic inside the `Terminal` class. While this code has not been thoroughly tested, I can't find regressions in the basic command line utilities or `vttest`. `Terminal` now displays nicer debug messages when it encounters an unknown escape sequence. Defensive programming and bounds checks have been added where we access parameters, and as a result, we can now endure 4-5 seconds of `cat /dev/urandom`. :D We generate EscapeSequenceStateMachine.h when building the in-kernel LibVT, and we assume that the file is already in place when the userland library is being built. This will probably cause problems later on, but I can't find a way to do it nicely.
Diffstat (limited to 'Userland/Libraries')
-rw-r--r--Userland/Libraries/LibVT/CMakeLists.txt4
-rw-r--r--Userland/Libraries/LibVT/EscapeSequenceParser.cpp162
-rw-r--r--Userland/Libraries/LibVT/EscapeSequenceParser.h77
-rw-r--r--Userland/Libraries/LibVT/StateMachine.txt2
-rw-r--r--Userland/Libraries/LibVT/Terminal.cpp705
-rw-r--r--Userland/Libraries/LibVT/Terminal.h106
6 files changed, 607 insertions, 449 deletions
diff --git a/Userland/Libraries/LibVT/CMakeLists.txt b/Userland/Libraries/LibVT/CMakeLists.txt
index eeca322d28..a5e3d6d729 100644
--- a/Userland/Libraries/LibVT/CMakeLists.txt
+++ b/Userland/Libraries/LibVT/CMakeLists.txt
@@ -1,7 +1,11 @@
+# FIXME: this assumes that EscapeSequenceStateMachine.h has been
+# already generated when the kernel was built. This will probably
+# mess builds up later on.
set(SOURCES
Line.cpp
Terminal.cpp
TerminalWidget.cpp
+ EscapeSequenceParser.cpp
)
serenity_lib(LibVT vt)
diff --git a/Userland/Libraries/LibVT/EscapeSequenceParser.cpp b/Userland/Libraries/LibVT/EscapeSequenceParser.cpp
new file mode 100644
index 0000000000..dc2fd1e411
--- /dev/null
+++ b/Userland/Libraries/LibVT/EscapeSequenceParser.cpp
@@ -0,0 +1,162 @@
+/*
+ * Copyright (c) 2021, the SerenityOS developers.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <AK/Format.h>
+#include <AK/Types.h>
+#include <LibVT/EscapeSequenceParser.h>
+#include <LibVT/EscapeSequenceStateMachine.h>
+
+namespace VT {
+EscapeSequenceParser::EscapeSequenceParser(EscapeSequenceExecutor& executor)
+ : m_executor(executor)
+ , m_state_machine([this](auto action, auto byte) { perform_action(action, byte); })
+{
+}
+
+EscapeSequenceParser::~EscapeSequenceParser()
+{
+}
+
+Vector<EscapeSequenceParser::OscParameter> EscapeSequenceParser::osc_parameters() const
+{
+ VERIFY(m_osc_raw.size() >= m_osc_parameter_indexes.last());
+ Vector<EscapeSequenceParser::OscParameter> params;
+ size_t prev_idx = 0;
+ for (auto end_idx : m_osc_parameter_indexes) {
+ // If the parameter is empty, we take an out of bounds index as the beginning of the Span.
+ // This should not be a problem as we won't dereference the 0-length Span that's created.
+ // Using &m_osc_raw[prev_idx] to get the start pointer checks whether we're out of bounds,
+ // so we would crash.
+ params.append({ m_osc_raw.data() + prev_idx, end_idx - prev_idx });
+ prev_idx = end_idx;
+ }
+ return params;
+}
+
+void EscapeSequenceParser::perform_action(EscapeSequenceStateMachine::Action action, u8 byte)
+{
+ auto advance_utf8 = [&](u8 byte) {
+ u32 new_codepoint = m_code_point;
+ new_codepoint <<= 6;
+ new_codepoint |= byte & 0x3f;
+ return new_codepoint;
+ };
+
+ switch (action) {
+ case EscapeSequenceStateMachine::Action::_Ignore:
+ break;
+ case EscapeSequenceStateMachine::Action::Print:
+ m_executor.emit_code_point((u32)byte);
+ break;
+ case EscapeSequenceStateMachine::Action::PrintUTF8:
+ m_executor.emit_code_point(advance_utf8(byte));
+ break;
+ case EscapeSequenceStateMachine::Action::Execute:
+ m_executor.execute_control_code(byte);
+ break;
+ case EscapeSequenceStateMachine::Action::Hook:
+ if (m_param_vector.size() == MAX_PARAMETERS)
+ m_ignoring = true;
+ else
+ m_param_vector.append(m_param);
+ m_executor.dcs_hook(m_param_vector, intermediates(), m_ignoring, byte);
+ break;
+ case EscapeSequenceStateMachine::Action::Put:
+ m_executor.receive_dcs_char(byte);
+ break;
+ case EscapeSequenceStateMachine::Action::BeginUTF8:
+ if ((byte & 0xe0) == 0xc0) {
+ m_code_point = byte & 0x1f;
+ } else if ((byte & 0xf0) == 0xe0) {
+ m_code_point = byte & 0x0f;
+ } else if ((byte & 0xf8) == 0xf0) {
+ m_code_point = byte & 0x07;
+ } else {
+ dbgln("Invalid character was parsed as UTF-8 initial byte {:02x}", byte);
+ VERIFY_NOT_REACHED();
+ }
+ break;
+ case EscapeSequenceStateMachine::Action::AdvanceUTF8:
+ VERIFY((byte & 0xc0) == 0x80);
+ m_code_point = advance_utf8(byte);
+ break;
+ case EscapeSequenceStateMachine::Action::FailUTF8:
+ m_executor.emit_code_point(U'�');
+ break;
+ case EscapeSequenceStateMachine::Action::OscStart:
+ m_osc_raw.clear();
+ m_osc_parameter_indexes.clear();
+ break;
+ case EscapeSequenceStateMachine::Action::OscPut:
+ if (byte == ';') {
+ if (m_osc_parameter_indexes.size() == MAX_OSC_PARAMETERS) {
+ dbgln("EscapeSequenceParser::perform_action: shenanigans! OSC sequence has too many parameters");
+ } else {
+ m_osc_parameter_indexes.append(m_osc_raw.size());
+ }
+ } else {
+ m_osc_raw.append(byte);
+ }
+ break;
+ case EscapeSequenceStateMachine::Action::OscEnd:
+ if (m_osc_parameter_indexes.size() == MAX_OSC_PARAMETERS) {
+ dbgln("EscapeSequenceParser::perform_action: shenanigans! OSC sequence has too many parameters");
+ } else {
+ m_osc_parameter_indexes.append(m_osc_raw.size());
+ }
+ m_executor.execute_osc_sequence(osc_parameters(), byte);
+ break;
+ case EscapeSequenceStateMachine::Action::Unhook:
+ m_executor.execute_dcs_sequence();
+ break;
+ case EscapeSequenceStateMachine::Action::CsiDispatch:
+ if (m_param_vector.size() > MAX_PARAMETERS) {
+ dbgln("EscapeSequenceParser::perform_action: shenanigans! CSI sequence has too many parameters");
+ m_ignoring = true;
+ } else {
+ m_param_vector.append(m_param);
+ }
+
+ m_executor.execute_csi_sequence(m_param_vector, intermediates(), m_ignoring, byte);
+ break;
+
+ case EscapeSequenceStateMachine::Action::EscDispatch:
+ m_executor.execute_escape_sequence(intermediates(), m_ignoring, byte);
+ break;
+ case EscapeSequenceStateMachine::Action::Collect:
+ if (m_intermediate_idx == MAX_INTERMEDIATES) {
+ dbgln("EscapeSequenceParser::perform_action: shenanigans! escape sequence has too many intermediates");
+ m_ignoring = true;
+ } else {
+ m_intermediates[m_intermediate_idx++] = byte;
+ }
+ break;
+ case EscapeSequenceStateMachine::Action::Param:
+ if (m_param_vector.size() == MAX_PARAMETERS) {
+ dbgln("EscapeSequenceParser::perform_action: shenanigans! escape sequence has too many parameters");
+ m_ignoring = true;
+ } else {
+ if (byte == ';') {
+ m_param_vector.append(m_param);
+ m_param = 0;
+ } else if (byte == ':') {
+ dbgln("EscapeSequenceParser::perform_action: subparameters are not yet implemented");
+ } else {
+ m_param *= 10;
+ m_param += (byte - '0');
+ }
+ }
+ break;
+ case EscapeSequenceStateMachine::Action::Clear:
+ m_intermediate_idx = 0;
+ m_ignoring = false;
+
+ m_param = 0;
+ m_param_vector.clear_with_capacity();
+ break;
+ }
+}
+}
diff --git a/Userland/Libraries/LibVT/EscapeSequenceParser.h b/Userland/Libraries/LibVT/EscapeSequenceParser.h
new file mode 100644
index 0000000000..14ec984698
--- /dev/null
+++ b/Userland/Libraries/LibVT/EscapeSequenceParser.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2021, the SerenityOS developers.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/Debug.h>
+#include <AK/Platform.h>
+#include <AK/Span.h>
+#include <AK/Types.h>
+#include <AK/Vector.h>
+#include <LibVT/EscapeSequenceStateMachine.h>
+
+namespace VT {
+class EscapeSequenceExecutor {
+public:
+ virtual ~EscapeSequenceExecutor() { }
+
+ using Parameters = Span<const unsigned>;
+ using Intermediates = Span<const u8>;
+ using OscParameter = Span<const u8>;
+ using OscParameters = Span<const OscParameter>;
+
+ virtual void emit_code_point(u32) = 0;
+ virtual void execute_control_code(u8) = 0;
+ virtual void execute_escape_sequence(Intermediates intermediates, bool ignore, u8 last_byte) = 0;
+ virtual void execute_csi_sequence(Parameters parameters, Intermediates intermediates, bool ignore, u8 last_byte) = 0;
+ virtual void execute_osc_sequence(OscParameters parameters, u8 last_byte) = 0;
+ virtual void dcs_hook(Parameters parameters, Intermediates intermediates, bool ignore, u8 last_byte) = 0;
+ virtual void receive_dcs_char(u8 byte) = 0;
+ virtual void execute_dcs_sequence() = 0;
+};
+
+class EscapeSequenceParser {
+public:
+ explicit EscapeSequenceParser(EscapeSequenceExecutor&);
+ ~EscapeSequenceParser();
+
+ ALWAYS_INLINE void on_input(u8 byte)
+ {
+ dbgln_if(ESCAPE_SEQUENCE_DEBUG, "on_input {:02x}", byte);
+ m_state_machine.advance(byte);
+ }
+
+private:
+ static constexpr size_t MAX_INTERMEDIATES = 2;
+ static constexpr size_t MAX_PARAMETERS = 16;
+ static constexpr size_t MAX_OSC_PARAMETERS = 16;
+
+ using Intermediates = EscapeSequenceExecutor::Intermediates;
+ using OscParameter = EscapeSequenceExecutor::OscParameter;
+
+ void perform_action(EscapeSequenceStateMachine::Action, u8);
+
+ EscapeSequenceExecutor& m_executor;
+ EscapeSequenceStateMachine m_state_machine;
+
+ u32 m_code_point { 0 };
+
+ u8 m_intermediates[MAX_INTERMEDIATES];
+ u8 m_intermediate_idx { 0 };
+
+ Intermediates intermediates() const { return { m_intermediates, m_intermediate_idx }; }
+ Vector<OscParameter> osc_parameters() const;
+
+ Vector<unsigned, 4> m_param_vector;
+ unsigned m_param { 0 };
+
+ Vector<u8> m_osc_parameter_indexes;
+ Vector<u8, 16> m_osc_raw;
+
+ bool m_ignoring { false };
+};
+
+}
diff --git a/Userland/Libraries/LibVT/StateMachine.txt b/Userland/Libraries/LibVT/StateMachine.txt
index ffa300ec01..1cd20ce8b4 100644
--- a/Userland/Libraries/LibVT/StateMachine.txt
+++ b/Userland/Libraries/LibVT/StateMachine.txt
@@ -3,7 +3,7 @@
// The description of the state machine is taken from https://vt100.net/emu/dec_ansi_parser
// with added support for UTF-8 parsing
-@name VTParserStateMachine
+@name EscapeSequenceStateMachine
@namespace VT
@begin Ground
diff --git a/Userland/Libraries/LibVT/Terminal.cpp b/Userland/Libraries/LibVT/Terminal.cpp
index 49c71a7edf..e06f57c1ed 100644
--- a/Userland/Libraries/LibVT/Terminal.cpp
+++ b/Userland/Libraries/LibVT/Terminal.cpp
@@ -4,6 +4,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
+#include "Terminal.h"
#include <AK/Debug.h>
#include <AK/StringBuilder.h>
#include <AK/StringView.h>
@@ -13,6 +14,7 @@ namespace VT {
Terminal::Terminal(TerminalClient& client)
: m_client(client)
+ , m_parser(*this)
{
}
@@ -37,22 +39,7 @@ void Terminal::clear_including_history()
m_client.terminal_history_changed();
}
-inline bool is_valid_parameter_character(u8 ch)
-{
- return ch >= 0x30 && ch <= 0x3f;
-}
-
-inline bool is_valid_intermediate_character(u8 ch)
-{
- return ch >= 0x20 && ch <= 0x2f;
-}
-
-inline bool is_valid_final_character(u8 ch)
-{
- return ch >= 0x40 && ch <= 0x7e;
-}
-
-void Terminal::alter_mode(bool should_set, bool question_param, const ParamVector& params)
+void Terminal::alter_mode(bool should_set, bool question_param, Parameters params)
{
int mode = 2;
if (params.size() > 0) {
@@ -60,9 +47,9 @@ void Terminal::alter_mode(bool should_set, bool question_param, const ParamVecto
}
if (!question_param) {
switch (mode) {
- // FIXME: implement *something* for this
+ // FIXME: implement *something* for this
default:
- unimplemented_escape();
+ dbgln("Terminal::alter_mode: Unimplemented mode {} (set={})", mode, should_set);
break;
}
} else {
@@ -70,7 +57,7 @@ void Terminal::alter_mode(bool should_set, bool question_param, const ParamVecto
case 3: {
// 80/132-column mode (DECCOLM)
unsigned new_columns = should_set ? 80 : 132;
- dbgln("Setting {}-column mode", new_columns);
+ dbgln_if(TERMINAL_DEBUG, "Setting {}-column mode", new_columns);
set_size(new_columns, rows());
clear();
break;
@@ -84,23 +71,33 @@ void Terminal::alter_mode(bool should_set, bool question_param, const ParamVecto
dbgln("Terminal: Show Cursor escapecode received. Not needed: ignored.");
break;
default:
- dbgln("Set Mode: Unimplemented mode {}", mode);
+ dbgln("Terminal::alter_mode: Unimplemented private mode {}", mode);
break;
}
}
}
-void Terminal::RM(bool question_param, const ParamVector& params)
+void Terminal::RM(Parameters params)
{
+ bool question_param = false;
+ if (params.size() > 0 && params[0] == '?') {
+ question_param = true;
+ params = params.slice(1);
+ }
alter_mode(true, question_param, params);
}
-void Terminal::SM(bool question_param, const ParamVector& params)
+void Terminal::SM(Parameters params)
{
+ bool question_param = false;
+ if (params.size() > 0 && params[0] == '?') {
+ question_param = true;
+ params = params.slice(1);
+ }
alter_mode(false, question_param, params);
}
-void Terminal::SGR(const ParamVector& params)
+void Terminal::SGR(Parameters params)
{
if (params.is_empty()) {
m_current_attribute.reset();
@@ -215,25 +212,26 @@ void Terminal::SGR(const ParamVector& params)
}
}
-void Terminal::SCOSC(const ParamVector&)
+void Terminal::SCOSC()
{
m_saved_cursor_row = m_cursor_row;
m_saved_cursor_column = m_cursor_column;
+ m_saved_attribute = m_current_attribute;
}
-void Terminal::SCORC(const ParamVector&)
+void Terminal::SCORC(Parameters)
{
set_cursor(m_saved_cursor_row, m_saved_cursor_column);
}
-void Terminal::XTERM_WM(const ParamVector& params)
+void Terminal::XTERM_WM(Parameters params)
{
if (params.size() < 1)
return;
dbgln("FIXME: XTERM_WM: Ps: {} (param count: {})", params[0], params.size());
}
-void Terminal::DECSTBM(const ParamVector& params)
+void Terminal::DECSTBM(Parameters params)
{
unsigned top = 1;
unsigned bottom = m_rows;
@@ -250,7 +248,7 @@ void Terminal::DECSTBM(const ParamVector& params)
set_cursor(0, 0);
}
-void Terminal::CUP(const ParamVector& params)
+void Terminal::CUP(Parameters params)
{
// CUP – Cursor Position
unsigned row = 1;
@@ -262,7 +260,7 @@ void Terminal::CUP(const ParamVector& params)
set_cursor(row - 1, col - 1);
}
-void Terminal::HVP(const ParamVector& params)
+void Terminal::HVP(Parameters params)
{
unsigned row = 1;
unsigned col = 1;
@@ -273,7 +271,7 @@ void Terminal::HVP(const ParamVector& params)
set_cursor(row - 1, col - 1);
}
-void Terminal::CUU(const ParamVector& params)
+void Terminal::CUU(Parameters params)
{
int num = 1;
if (params.size() >= 1)
@@ -286,7 +284,7 @@ void Terminal::CUU(const ParamVector& params)
set_cursor(new_row, m_cursor_column);
}
-void Terminal::CUD(const ParamVector& params)
+void Terminal::CUD(Parameters params)
{
int num = 1;
if (params.size() >= 1)
@@ -299,7 +297,7 @@ void Terminal::CUD(const ParamVector& params)
set_cursor(new_row, m_cursor_column);
}
-void Terminal::CUF(const ParamVector& params)
+void Terminal::CUF(Parameters params)
{
int num = 1;
if (params.size() >= 1)
@@ -312,7 +310,7 @@ void Terminal::CUF(const ParamVector& params)
set_cursor(m_cursor_row, new_column);
}
-void Terminal::CUB(const ParamVector& params)
+void Terminal::CUB(Parameters params)
{
int num = 1;
if (params.size() >= 1)
@@ -325,7 +323,7 @@ void Terminal::CUB(const ParamVector& params)
set_cursor(m_cursor_row, new_column);
}
-void Terminal::CHA(const ParamVector& params)
+void Terminal::CHA(Parameters params)
{
int new_column = 1;
if (params.size() >= 1)
@@ -335,7 +333,7 @@ void Terminal::CHA(const ParamVector& params)
set_cursor(m_cursor_row, new_column);
}
-void Terminal::REP(const ParamVector& params)
+void Terminal::REP(Parameters params)
{
if (params.size() < 1)
return;
@@ -344,7 +342,7 @@ void Terminal::REP(const ParamVector& params)
put_character_at(m_cursor_row, m_cursor_column++, m_last_code_point);
}
-void Terminal::VPA(const ParamVector& params)
+void Terminal::VPA(Parameters params)
{
int new_row = 1;
if (params.size() >= 1)
@@ -354,7 +352,7 @@ void Terminal::VPA(const ParamVector& params)
set_cursor(new_row, m_cursor_column);
}
-void Terminal::ECH(const ParamVector& params)
+void Terminal::ECH(Parameters params)
{
// Erase characters (without moving cursor)
int num = 1;
@@ -368,7 +366,7 @@ void Terminal::ECH(const ParamVector& params)
}
}
-void Terminal::EL(const ParamVector& params)
+void Terminal::EL(Parameters params)
{
int mode = 0;
if (params.size() >= 1)
@@ -393,12 +391,12 @@ void Terminal::EL(const ParamVector& params)
}
break;
default:
- unimplemented_escape();
+ unimplemented_csi_sequence(params, {}, 'K');
break;
}
}
-void Terminal::ED(const ParamVector& params)
+void Terminal::ED(Parameters params)
{
int mode = 0;
if (params.size() >= 1)
@@ -432,12 +430,12 @@ void Terminal::ED(const ParamVector& params)
clear();
break;
default:
- unimplemented_escape();
+ unimplemented_csi_sequence(params, {}, 'J');
break;
}
}
-void Terminal::SU(const ParamVector& params)
+void Terminal::SU(Parameters params)
{
int count = 1;
if (params.size() >= 1)
@@ -447,7 +445,7 @@ void Terminal::SU(const ParamVector& params)
scroll_up();
}
-void Terminal::SD(const ParamVector& params)
+void Terminal::SD(Parameters params)
{
int count = 1;
if (params.size() >= 1)
@@ -457,7 +455,7 @@ void Terminal::SD(const ParamVector& params)
scroll_down();
}
-void Terminal::IL(const ParamVector& params)
+void Terminal::IL(Parameters params)
{
int count = 1;
if (params.size() >= 1)
@@ -474,12 +472,12 @@ void Terminal::IL(const ParamVector& params)
m_need_full_flush = true;
}
-void Terminal::DA(const ParamVector&)
+void Terminal::DA(Parameters)
{
emit_string("\033[?1;0c");
}
-void Terminal::DL(const ParamVector& params)
+void Terminal::DL(Parameters params)
{
int count = 1;
if (params.size() >= 1)
@@ -502,7 +500,7 @@ void Terminal::DL(const ParamVector& params)
}
}
-void Terminal::DCH(const ParamVector& params)
+void Terminal::DCH(Parameters params)
{
int num = 1;
if (params.size() >= 1)
@@ -524,165 +522,6 @@ void Terminal::DCH(const ParamVector& params)
line.set_dirty(true);
}
-void Terminal::execute_xterm_command()
-{
- ParamVector numeric_params;
- auto param_string = String::copy(m_xterm_parameters);
- auto params = param_string.split(';', true);
- m_xterm_parameters.clear_with_capacity();
- for (auto& parampart : params)
- numeric_params.append(parampart.to_uint().value_or(0));
-
- while (params.size() < 3) {
- params.append(String::empty());
- numeric_params.append(0);
- }
-
- m_final = '@';
-
- if (numeric_params.is_empty()) {
- dbgln("Empty Xterm params?");
- return;
- }
-
- switch (numeric_params[0]) {
- case 0:
- case 1:
- case 2:
- m_client.set_window_title(params[1]);
- break;
- case 8:
- if (params[2].is_empty()) {
- m_current_attribute.href = String();
- m_current_attribute.href_id = String();
- } else {
- m_current_attribute.href = params[2];
- // FIXME: Respect the provided ID
- m_current_attribute.href_id = String::number(m_next_href_id++);
- }
- break;
- case 9:
- m_client.set_window_progress(numeric_params[1], numeric_params[2]);
- break;
- default:
- unimplemented_xterm_escape();
- break;
- }
-}
-
-void Terminal::execute_escape_sequence(u8 final)
-{
- bool question_param = false;
- m_final = final;
- ParamVector params;
-
- if (m_parameters.size() > 0 && m_parameters[0] == '?') {
- question_param = true;
- m_parameters.remove(0);
- }
- auto paramparts = String::copy(m_parameters).split(';');
- for (auto& parampart : paramparts) {
- auto value = parampart.to_uint();
- if (!value.has_value()) {
- // FIXME: Should we do something else?
- m_parameters.clear_with_capacity();
- m_intermediates.clear_with_capacity();
- return;
- }
- params.append(value.value());
- }
-
- switch (final) {
- case 'A':
- CUU(params);
- break;
- case 'B':
- CUD(params);
- break;
- case 'C':
- CUF(params);
- break;
- case 'D':
- CUB(params);
- break;
- case 'H':
- CUP(params);
- break;
- case 'J':
- ED(params);
- break;
- case 'K':
- EL(params);
- break;
- case 'M':
- DL(params);
- break;
- case 'P':
- DCH(params);
- break;
- case 'S':
- SU(params);
- break;
- case 'T':
- SD(params);
- break;
- case 'L':
- IL(params);
- break;
- case 'G':
- CHA(params);
- break;
- case 'X':
- ECH(params);
- break;
- case 'b':
- REP(params);
- break;
- case 'd':
- VPA(params);
- break;
- case 'm':
- SGR(params);
- break;
- case 's':
- SCOSC(params);
- break;
- case 'u':
- SCORC(params);
- break;
- case 't':
- XTERM_WM(params);
- break;
- case 'r':
- DECSTBM(params);
- break;
- case 'l':
- RM(question_param, params);
- break;
- case 'h':
- SM(question_param, params);
- break;
- case 'c':
- DA(params);
- break;
- case 'f':
- HVP(params);
- break;
- case 'n':
- DSR(params);
- break;
- case '@':
- ICH(params);
- break;
- default:
- dbgln("Terminal::execute_escape_sequence: Unhandled final '{:c}'", final);
- break;
- }
-
- m_parameters.clear_with_capacity();
- m_intermediates.clear_with_capacity();
-}
-
void Terminal::newline()
{
u16 new_row = m_cursor_row;
@@ -690,9 +529,13 @@ void Terminal::newline()
scroll_up();
} else {
++new_row;
- }
+ };
set_cursor(new_row, 0);
}
+void Terminal::carriage_return()
+{
+ set_cursor(m_cursor_row, 0);
+}
void Terminal::scroll_up()
{
@@ -748,6 +591,7 @@ void Terminal::put_character_at(unsigned row, unsigned column, u32 code_point)
void Terminal::NEL()
{
newline();
+ carriage_return();
}
void Terminal::IND()
@@ -760,7 +604,7 @@ void Terminal::RI()
CUU({});
}
-void Terminal::DSR(const ParamVector& params)
+void Terminal::DSR(Parameters params)
{
if (params.size() == 1 && params[0] == 5) {
// Device status
@@ -773,7 +617,7 @@ void Terminal::DSR(const ParamVector& params)
}
}
-void Terminal::ICH(const ParamVector& params)
+void Terminal::ICH(Parameters params)
{
int num = 0;
if (params.size() >= 1) {
@@ -795,153 +639,44 @@ void Terminal::ICH(const ParamVector& params)
line.set_dirty(true);
}
-void Terminal::on_input(u8 ch)
+void Terminal::on_input(u8 byte)
{
- dbgln_if(TERMINAL_DEBUG, "Terminal::on_input: {:#02x} ({:c}), fg={}, bg={}\n", ch, ch, m_current_attribute.foreground_color, m_current_attribute.background_color);
-
- auto fail_utf8_parse = [this] {
- m_parser_state = Normal;
- on_code_point(U'�');
- };
-
- auto advance_utf8_parse = [this, ch] {
- m_parser_code_point <<= 6;
- m_parser_code_point |= ch & 0x3f;
- if (m_parser_state == UTF8Needs1Byte) {
- on_code_point(m_parser_code_point);
- m_parser_state = Normal;
- } else {
- m_parser_state = (ParserState)(m_parser_state + 1);
- }
- };
-
- switch (m_parser_state) {
- case GotEscape:
- if (ch == '[') {
- m_parser_state = ExpectParameter;
- } else if (ch == '(') {
- m_swallow_current = true;
- m_parser_state = ExpectParameter;
- } else if (ch == ']') {
- m_parser_state = ExpectXtermParameter;
- m_xterm_parameters.clear_with_capacity();
- } else if (ch == '#') {
- m_parser_state = ExpectHashtagDigit;
- } else if (ch == 'D') {
- IND();
- m_parser_state = Normal;
- return;
- } else if (ch == 'M') {
- RI();
- m_parser_state = Normal;
- return;
- } else if (ch == 'E') {
- NEL();
- m_parser_state = Normal;
- return;
- } else {
- dbgln("Unexpected character in GotEscape '{}'", (char)ch);
- m_parser_state = Normal;
- }
- return;
- case ExpectHashtagDigit:
- if (ch >= '0' && ch <= '9') {
- execute_hashtag(ch);
- m_parser_state = Normal;
- }
- return;
- case ExpectXtermParameter:
- if (ch == 27) {
- m_parser_state = ExpectStringTerminator;
- return;
- }
- if (ch == 7) {
- execute_xterm_command();
- m_parser_state = Normal;
- return;
- }
- m_xterm_parameters.append(ch);
- return;
- case ExpectStringTerminator:
- if (ch == '\\')
- execute_xterm_command();
- else
- dbgln("Unexpected string terminator: {:#02x}", ch);
- m_parser_state = Normal;
- return;
- case ExpectParameter:
- if (is_valid_parameter_character(ch)) {
- m_parameters.append(ch);
- return;
- }
- m_parser_state = ExpectIntermediate;
- [[fallthrough]];
- case ExpectIntermediate:
- if (is_valid_intermediate_character(ch)) {
- m_intermediates.append(ch);
- return;
- }
- m_parser_state = ExpectFinal;
- [[fallthrough]];
- case ExpectFinal:
- if (is_valid_final_character(ch)) {
- m_parser_state = Normal;
- if (!m_swallow_current)
- execute_escape_sequence(ch);
- m_swallow_current = false;
- return;
- }
- m_parser_state = Normal;
- m_swallow_current = false;
- return;
- case UTF8Needs1Byte:
- case UTF8Needs2Bytes:
- case UTF8Needs3Bytes:
- if ((ch & 0xc0) != 0x80) {
- fail_utf8_parse();
- } else {
- advance_utf8_parse();
- }
- return;
+ m_parser.on_input(byte);
+}
- case Normal:
- if (!(ch & 0x80))
- break;
- if ((ch & 0xe0) == 0xc0) {
- m_parser_state = UTF8Needs1Byte;
- m_parser_code_point = ch & 0x1f;
- return;
- }
- if ((ch & 0xf0) == 0xe0) {
- m_parser_state = UTF8Needs2Bytes;
- m_parser_code_point = ch & 0x0f;
- return;
- }
- if ((ch & 0xf8) == 0xf0) {
- m_parser_state = UTF8Needs3Bytes;
- m_parser_code_point = ch & 0x07;
- return;
- }
- fail_utf8_parse();
+void Terminal::emit_code_point(u32 code_point)
+{
+ auto new_column = m_cursor_column + 1;
+ if (new_column < columns()) {
+ put_character_at(m_cursor_row, m_cursor_column, code_point);
+ set_cursor(m_cursor_row, new_column);
return;
}
+ if (m_stomp) {
+ m_stomp = false;
+ carriage_return();
+ newline();
+ put_character_at(m_cursor_row, m_cursor_column, code_point);
+ set_cursor(m_cursor_row, 1);
+ } else {
+ // Curious: We wait once on the right-hand side
+ m_stomp = true;
+ put_character_at(m_cursor_row, m_cursor_column, code_point);
+ }
+}
- switch (ch) {
- case '\0':
- return;
- case '\033':
- m_parser_state = GotEscape;
- m_swallow_current = false;
+void Terminal::execute_control_code(u8 code)
+{
+ switch (code) {
+ case '\a':
+ m_client.beep();
return;
- case 8: // Backspace
+ case '\b':
if (m_cursor_column) {
set_cursor(m_cursor_row, m_cursor_column - 1);
return;
}
return;
- case '\a':
- m_client.beep();
- return;
case '\t': {
for (unsigned i = m_cursor_column + 1; i < columns(); ++i) {
if (m_horizontal_tabs[i]) {
@@ -951,37 +686,211 @@ void Terminal::on_input(u8 ch)
}
return;
}
- case '\r':
- set_cursor(m_cursor_row, 0);
- return;
case '\n':
newline();
return;
+ case '\r':
+ carriage_return();
+ return;
+ default:
+ unimplemented_control_code(code);
}
+}
- on_code_point(ch);
+void Terminal::execute_escape_sequence(Intermediates intermediates, bool ignore, u8 last_byte)
+{
+ // FIXME: Handle it somehow?
+ if (ignore)
+ dbgln("Escape sequence has its ignore flag set.");
+
+ if (intermediates.size() == 0) {
+ switch (last_byte) {
+ case 'D':
+ IND();
+ return;
+ case 'E':
+ NEL();
+ return;
+ case 'M':
+ RI();
+ return;
+ }
+ } else if (intermediates[0] == '#') {
+ switch (last_byte) {
+ case '8':
+ // Confidence Test - Fill screen with E's
+ for (size_t row = 0; row < m_rows; ++row) {
+ for (size_t column = 0; column < m_columns; ++column) {
+ put_character_at(row, column, 'E');
+ }
+ }
+ return;
+ }
+ }
+ unimplemented_escape_sequence(intermediates, last_byte);
}
-void Terminal::on_code_point(u32 code_point)
+void Terminal::execute_csi_sequence(Parameters parameters, Intermediates intermediates, bool ignore, u8 last_byte)
{
- auto new_column = m_cursor_column + 1;
- if (new_column < columns()) {
- put_character_at(m_cursor_row, m_cursor_column, code_point);
- set_cursor(m_cursor_row, new_column);
- return;
+ // FIXME: Handle it somehow?
+ if (ignore)
+ dbgln("CSI sequence has its ignore flag set.");
+
+ switch (last_byte) {
+ case '@':
+ ICH(parameters);
+ break;
+ case 'A':
+ CUU(parameters);
+ break;
+ case 'B':
+ CUD(parameters);
+ break;
+ case 'C':
+ CUF(parameters);
+ break;
+ case 'D':
+ CUB(parameters);
+ break;
+ case 'G':
+ CHA(parameters);
+ break;
+ case 'H':
+ CUP(parameters);
+ break;
+ case 'J':
+ ED(parameters);
+ break;
+ case 'K':
+ EL(parameters);
+ break;
+ case 'L':
+ IL(parameters);
+ break;
+ case 'M':
+ DL(parameters);
+ break;
+ case 'P':
+ DCH(parameters);
+ break;
+ case 'S':
+ SU(parameters);
+ break;
+ case 'T':
+ SD(parameters);
+ break;
+ case 'X':
+ ECH(parameters);
+ break;
+ case 'b':
+ REP(parameters);
+ break;
+ case 'd':
+ VPA(parameters);
+ break;
+ case 'm':
+ SGR(parameters);
+ break;
+ case 's':
+ SCOSC();
+ break;
+ case 'u':
+ SCORC(parameters);
+ break;
+ case 't':
+ XTERM_WM(parameters);
+ break;
+ case 'r':
+ DECSTBM(parameters);
+ break;
+ case 'l':
+ RM(parameters);
+ break;
+ case 'h':
+ SM(parameters);
+ break;
+ case 'c':
+ DA(parameters);
+ break;
+ case 'f':
+ HVP(parameters);
+ break;
+ case 'n':
+ DSR(parameters);
+ break;
+ default:
+ unimplemented_csi_sequence(parameters, intermediates, last_byte);
}
- if (m_stomp) {
- m_stomp = false;
- newline();
- put_character_at(m_cursor_row, m_cursor_column, code_point);
- set_cursor(m_cursor_row, 1);
+}
+
+void Terminal::execute_osc_sequence(OscParameters parameters, u8 last_byte)
+{
+ auto stringview_ify = [&](size_t param_idx) {
+ return StringView((const char*)(&parameters[param_idx][0]), parameters[param_idx].size());
+ };
+
+ if (parameters.size() > 0 && !parameters[0].is_empty()) {
+ auto command_number = stringview_ify(0).to_uint();
+ if (command_number.has_value()) {
+ switch (command_number.value()) {
+ case 0:
+ case 1:
+ case 2:
+ if (parameters[1].is_empty())
+ dbgln("Attempted to set window title without any parameters");
+ else
+ m_client.set_window_title(stringview_ify(1));
+ // FIXME: the split breaks titles containing semicolons.
+ // Should we expose the raw OSC string from the parser? Or join by semicolon?
+ break;
+ case 8:
+ if (parameters.size() < 2) {
+ dbgln("Attempted to set href but gave too few parameters");
+ } else if (parameters[2].is_empty()) {
+ m_current_attribute.href = String();
+ m_current_attribute.href_id = String();
+ } else {
+ m_current_attribute.href = stringview_ify(2);
+ // FIXME: Respect the provided ID
+ m_current_attribute.href_id = String::number(m_next_href_id++);
+ }
+ break;
+ case 9:
+ if (parameters.size() < 2 || parameters[1].is_empty() || parameters[2].is_empty())
+ dbgln("Atttempted to set window progress but gave too few parameters");
+ else
+ m_client.set_window_progress(stringview_ify(1).to_int().value_or(0), stringview_ify(2).to_int().value_or(0));
+ break;
+ default:
+ unimplemented_osc_sequence(parameters, last_byte);
+ }
+ } else {
+ unimplemented_osc_sequence(parameters, last_byte);
+ }
} else {
- // Curious: We wait once on the right-hand side
- m_stomp = true;
- put_character_at(m_cursor_row, m_cursor_column, code_point);
+ unimplemented_osc_sequence(parameters, last_byte);
}
}
+void Terminal::dcs_hook(Parameters parameters, Intermediates intermediates, bool ignore, u8 last_byte)
+{
+ dbgln("Received DCS parameters, but we don't support it yet");
+ (void)parameters;
+ (void)last_byte;
+ (void)intermediates;
+ (void)ignore;
+}
+
+void Terminal::receive_dcs_char(u8 byte)
+{
+ dbgln_if(TERMINAL_DEBUG, "DCS string character {:c}", byte);
+ (void)byte;
+}
+
+void Terminal::execute_dcs_sequence()
+{
+}
+
void Terminal::inject_string(const StringView& str)
{
for (size_t i = 0; i < str.length(); ++i)
@@ -1078,26 +987,58 @@ void Terminal::handle_key_press(KeyCode key, u32 code_point, u8 flags)
emit_string(sb.to_string());
}
-void Terminal::unimplemented_escape()
+void Terminal::unimplemented_control_code(u8 code)
+{
+ dbgln("Unimplemented control code {:02x}", code);
+}
+
+void Terminal::unimplemented_escape_sequence(Intermediates intermediates, u8 last_byte)
{
StringBuilder builder;
- builder.appendff("Unimplemented escape: {:c}", m_final);
- if (!m_parameters.is_empty()) {
- builder.append(", parameters:");
- for (size_t i = 0; i < m_parameters.size(); ++i)
- builder.append((char)m_parameters[i]);
+ builder.appendff("Unimplemented escape sequence {:c}", last_byte);
+ if (!intermediates.is_empty()) {
+ builder.append(", intermediates: ");
+ for (size_t i = 0; i < intermediates.size(); ++i)
+ builder.append((char)intermediates[i]);
}
- if (!m_intermediates.is_empty()) {
+ dbgln("{}", builder.string_view());
+}
+
+void Terminal::unimplemented_csi_sequence(Parameters parameters, Intermediates intermediates, u8 last_byte)
+{
+ StringBuilder builder;
+ builder.appendff("Unimplemented CSI sequence: {:c}", last_byte);
+ if (!parameters.is_empty()) {
+ builder.append(", parameters: [");
+ for (size_t i = 0; i < parameters.size(); ++i)
+ builder.appendff("{}{}", (i == 0) ? "" : ", ", parameters[i]);
+ builder.append("]");
+ }
+ if (!intermediates.is_empty()) {
builder.append(", intermediates:");
- for (size_t i = 0; i < m_intermediates.size(); ++i)
- builder.append((char)m_intermediates[i]);
+ for (size_t i = 0; i < intermediates.size(); ++i)
+ builder.append((char)intermediates[i]);
}
dbgln("{}", builder.string_view());
}
-void Terminal::unimplemented_xterm_escape()
+void Terminal::unimplemented_osc_sequence(OscParameters parameters, u8 last_byte)
{
- dbgln("Unimplemented xterm escape: {:c}", m_final);
+ StringBuilder builder;
+ builder.appendff("Unimplemented OSC sequence parameters: (bel_terminated={}) [ ", last_byte == '\a');
+ bool first = true;
+ for (auto parameter : parameters) {
+ if (!first)
+ builder.append(", ");
+ builder.append("[");
+ for (auto character : parameter)
+ builder.append((char)character);
+ builder.append("]");
+ first = false;
+ }
+
+ builder.append(" ]");
+ dbgln("{}", builder.string_view());
}
void Terminal::set_size(u16 columns, u16 rows)
@@ -1145,22 +1086,6 @@ void Terminal::invalidate_cursor()
m_lines[m_cursor_row].set_dirty(true);
}
-void Terminal::execute_hashtag(u8 hashtag)
-{
- switch (hashtag) {
- case '8':
- // Confidence Test - Fill screen with E's
- for (size_t row = 0; row < m_rows; ++row) {
- for (size_t column = 0; column < m_columns; ++column) {
- put_character_at(row, column, 'E');
- }
- }
- break;
- default:
- dbgln("Unknown hashtag: '{}'", (char)hashtag);
- }
-}
-
Attribute Terminal::attribute_at(const Position& position) const
{
if (!position.is_valid())
diff --git a/Userland/Libraries/LibVT/Terminal.h b/Userland/Libraries/LibVT/Terminal.h
index 71984ab888..bf23e7cc9b 100644
--- a/Userland/Libraries/LibVT/Terminal.h
+++ b/Userland/Libraries/LibVT/Terminal.h
@@ -11,6 +11,7 @@
#include <AK/String.h>
#include <AK/Vector.h>
#include <Kernel/API/KeyCode.h>
+#include <LibVT/EscapeSequenceParser.h>
#include <LibVT/Line.h>
#include <LibVT/Position.h>
@@ -28,7 +29,7 @@ public:
virtual void emit(const u8*, size_t) = 0;
};
-class Terminal {
+class Terminal : public EscapeSequenceExecutor {
public:
explicit Terminal(TerminalClient&);
~Terminal();
@@ -106,68 +107,78 @@ public:
Attribute attribute_at(const Position&) const;
private:
- typedef Vector<unsigned, 4> ParamVector;
-
- void on_code_point(u32);
+ // ^EscapeSequenceExecutor
+ virtual void emit_code_point(u32) override;
+ virtual void execute_control_code(u8) override;
+ virtual void execute_escape_sequence(Intermediates intermediates, bool ignore, u8 last_byte) override;
+ virtual void execute_csi_sequence(Parameters parameters, Intermediates intermediates, bool ignore, u8 last_byte) override;
+ virtual void execute_osc_sequence(OscParameters parameters, u8 last_byte) override;
+ virtual void dcs_hook(Parameters parameters, Intermediates intermediates, bool ignore, u8 last_byte) override;
+ virtual void receive_dcs_char(u8 byte) override;
+ virtual void execute_dcs_sequence() override;
void scroll_up();
void scroll_down();
void newline();
+ void carriage_return();
+
void set_cursor(unsigned row, unsigned column);
void put_character_at(unsigned row, unsigned column, u32 ch);
void set_window_title(const String&);
- void unimplemented_escape();
- void unimplemented_xterm_escape();
+ void unimplemented_control_code(u8);
+ void unimplemented_escape_sequence(Intermediates, u8 last_byte);
+ void unimplemented_csi_sequence(Parameters, Intermediates, u8 last_byte);
+ void unimplemented_osc_sequence(OscParameters, u8 last_byte);
void emit_string(const StringView&);
- void alter_mode(bool should_set, bool question_param, const ParamVector&);
+ void alter_mode(bool should_set, bool question_param, Parameters);
// CUU – Cursor Up
- void CUU(const ParamVector&);
+ void CUU(Parameters);
// CUD – Cursor Down
- void CUD(const ParamVector&);
+ void CUD(Parameters);
// CUF – Cursor Forward
- void CUF(const ParamVector&);
+ void CUF(Parameters);
// CUB – Cursor Backward
- void CUB(const ParamVector&);
+ void CUB(Parameters);
// CUP - Cursor Position
- void CUP(const ParamVector&);
+ void CUP(Parameters);
// ED - Erase in Display
- void ED(const ParamVector&);
+ void ED(Parameters);
// EL - Erase in Line
- void EL(const ParamVector&);
+ void EL(Parameters);
// SGR – Select Graphic Rendition
- void SGR(const ParamVector&);
+ void SGR(Parameters);
// Save Current Cursor Position
- void SCOSC(const ParamVector&);
+ void SCOSC();
// Restore Saved Cursor Position
- void SCORC(const ParamVector&);
+ void SCORC(Parameters);
// DECSTBM – Set Top and Bottom Margins ("Scrolling Region")
- void DECSTBM(const ParamVector&);
+ void DECSTBM(Parameters);
// RM – Reset Mode
- void RM(bool question_param, const ParamVector&);
+ void RM(Parameters);
// SM – Set Mode
- void SM(bool question_param, const ParamVector&);
+ void SM(Parameters);
// DA - Device Attributes
- void DA(const ParamVector&);
+ void DA(Parameters);
// HVP – Horizontal and Vertical Position
- void HVP(const ParamVector&);
+ void HVP(Parameters);
// NEL - Next Line
void NEL();
@@ -179,43 +190,45 @@ private:
void RI();
// DSR - Device Status Reports
- void DSR(const ParamVector&);
+ void DSR(Parameters);
// ICH - Insert Character
- void ICH(const ParamVector&);
+ void ICH(Parameters);
// SU - Scroll Up (called "Pan Down" in VT510)
- void SU(const ParamVector&);
+ void SU(Parameters);
// SD - Scroll Down (called "Pan Up" in VT510)
- void SD(const ParamVector&);
+ void SD(Parameters);
// IL - Insert Line
- void IL(const ParamVector&);
+ void IL(Parameters);
// DCH - Delete Character
- void DCH(const ParamVector&);
+ void DCH(Parameters);
// DL - Delete Line
- void DL(const ParamVector&);
+ void DL(Parameters);
// CHA - Cursor Horizontal Absolute
- void CHA(const ParamVector&);
+ void CHA(Parameters);
// REP - Repeat
- void REP(const ParamVector&);
+ void REP(Parameters);
// VPA - Vertical Line Position Absolute
- void VPA(const ParamVector&);
+ void VPA(Parameters);
// ECH - Erase Character
- void ECH(const ParamVector&);
+ void ECH(Parameters);
// FIXME: Find the right names for these.
- void XTERM_WM(const ParamVector&);
+ void XTERM_WM(Parameters);
TerminalClient& m_client;
+ EscapeSequenceParser m_parser;
+
size_t m_history_start = 0;
NonnullOwnPtrVector<Line> m_history;
void add_line_to_history(NonnullOwnPtr<Line>&& line)
@@ -248,34 +261,11 @@ private:
bool m_stomp { false };
Attribute m_current_attribute;
+ Attribute m_saved_attribute;
u32 m_next_href_id { 0 };
- void execute_escape_sequence(u8 final);
- void execute_xterm_command();
- void execute_hashtag(u8);
-
- enum ParserState {
- Normal,
- GotEscape,
- ExpectParameter,
- ExpectIntermediate,
- ExpectFinal,
- ExpectHashtagDigit,
- ExpectXtermParameter,
- ExpectStringTerminator,
- UTF8Needs3Bytes,
- UTF8Needs2Bytes,
- UTF8Needs1Byte,
- };
-
- ParserState m_parser_state { Normal };
- u32 m_parser_code_point { 0 };
- Vector<u8> m_parameters;
- Vector<u8> m_intermediates;
- Vector<u8> m_xterm_parameters;
Vector<bool> m_horizontal_tabs;
- u8 m_final { 0 };
u32 m_last_code_point { 0 };
size_t m_max_history_lines { 1024 };
};