Browse Source

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.
Daniel Bertalan 4 years ago
parent
commit
be519022c3

+ 4 - 0
AK/Debug.h.in

@@ -396,6 +396,10 @@
 
 #ifndef WASM_BINPARSER_DEBUG
 #cmakedefine01 WASM_BINPARSER_DEBUG
+#endif 
+
+#ifndef ESCAPE_SEQUENCE_DEBUG
+#cmakedefine01 ESCAPE_SEQUENCE_DEBUG
 #endif
 
 #ifndef WINDOWMANAGER_DEBUG

+ 4 - 0
Kernel/CMakeLists.txt

@@ -278,9 +278,13 @@ set(ELF_SOURCES
     ../Userland/Libraries/LibELF/Validation.cpp
 )
 
+generate_state_machine(../Userland/Libraries/LibVT/StateMachine.txt ../Userland/Libraries/LibVT/EscapeSequenceStateMachine.h)
+
 set(VT_SOURCES
     ../Userland/Libraries/LibVT/Terminal.cpp
     ../Userland/Libraries/LibVT/Line.cpp
+    ../Userland/Libraries/LibVT/EscapeSequenceStateMachine.h
+    ../Userland/Libraries/LibVT/EscapeSequenceParser.cpp
 )
 
 set(KEYBOARD_SOURCES

+ 1 - 0
Meta/CMake/all_the_debug_macros.cmake

@@ -159,6 +159,7 @@ set(STORAGE_DEVICE_DEBUG ON)
 set(TCP_DEBUG ON)
 set(TERMCAP_DEBUG ON)
 set(TERMINAL_DEBUG ON)
+set(ESCAPE_SEQUENCE_DEBUG ON)
 set(UCI_DEBUG ON)
 set(UDP_DEBUG ON)
 set(UHCI_VERBOSE_DEBUG ON)

+ 12 - 0
Meta/CMake/utils.cmake

@@ -154,3 +154,15 @@ function(embed_resource target section file)
     )
     target_sources("${target}" PRIVATE "${asm_file}")
 endfunction()
+
+function(generate_state_machine source header)
+    set(source ${CMAKE_CURRENT_SOURCE_DIR}/${source})
+    add_custom_command(
+        OUTPUT ${header}
+	COMMAND ${write_if_different} ${header} ${CMAKE_BINARY_DIR}/Userland/DevTools/StateMachineGenerator/StateMachineGenerator ${source} > ${header}
+        VERBATIM
+        DEPENDS StateMachineGenerator
+        MAIN_DEPENDENCY ${source}
+    )
+    get_filename_component(output_name ${header} NAME)
+endfunction()

+ 73 - 116
Userland/DevTools/StateMachineGenerator/main.cpp

@@ -211,18 +211,15 @@ parse_state_machine(StringView input)
 }
 
 void output_header(const StateMachine&, SourceGenerator&);
-void output_cpp(const StateMachine&, SourceGenerator&);
 
 int main(int argc, char** argv)
 {
     Core::ArgsParser args_parser;
     const char* path = nullptr;
-    bool header_mode = false;
-    args_parser.add_option(header_mode, "Generate .h file", "header", 'H');
     args_parser.add_positional_argument(path, "Path to parser description", "input", Core::ArgsParser::Required::Yes);
     args_parser.parse(argc, argv);
 
-    auto file_or_error = Core::File::open(path, Core::IODevice::ReadOnly);
+    auto file_or_error = Core::File::open(path, Core::OpenMode::ReadOnly);
     if (file_or_error.is_error()) {
         fprintf(stderr, "Cannot open %s\n", path);
     }
@@ -232,10 +229,7 @@ int main(int argc, char** argv)
 
     StringBuilder builder;
     SourceGenerator generator { builder };
-    if (header_mode)
-        output_header(*state_machine, generator);
-    else
-        output_cpp(*state_machine, generator);
+    output_header(*state_machine, generator);
     outln("{}", generator.as_string_view());
     return 0;
 }
@@ -340,9 +334,66 @@ public:
 
     typedef Function<void(Action, u8)> Handler;
 
-    @class_name@(Handler);
+    @class_name@(Handler handler)
+    : m_handler(move(handler))
+    {
+    }
+
+    void advance(u8 byte)
+    {
+        auto next_state = lookup_state_transition(byte);
+        bool state_will_change = next_state.new_state != m_state && next_state.new_state != State::_Anywhere;
+
+        // only run exit directive if state is being changed
+        if (state_will_change) {
+            switch (m_state) {
+)~~~");
+    for (auto s : machine.states) {
+        auto state_generator = generator.fork();
+        if (s.exit_action.has_value()) {
+            state_generator.set("state_name", s.name);
+            state_generator.set("action", s.exit_action.value());
+            state_generator.append(R"~~~(
+            case State::@state_name@:
+                m_handler(Action::@action@, byte);
+                break;
+)~~~");
+        }
+    }
+    generator.append(R"~~~(
+            default:
+                break;
+            }
+        }
 
-    void advance(u8);
+        if (next_state.action != Action::_Ignore)
+            m_handler(next_state.action, byte);
+        m_state = next_state.new_state;
+
+        // only run entry directive if state is being changed
+        if (state_will_change)
+        {
+            switch (next_state.new_state)
+            {
+)~~~");
+    for (auto state : machine.states) {
+        auto state_generator = generator.fork();
+        if (state.entry_action.has_value()) {
+            state_generator.set("state_name", state.name);
+            state_generator.set("action", state.entry_action.value());
+            state_generator.append(R"~~~(
+            case State::@state_name@:
+                m_handler(Action::@action@, byte);
+                break;
+)~~~");
+        }
+    }
+    generator.append(R"~~~(
+            default:
+                break;
+            }
+        }
+    }
 
 private:
     enum class State : u8 {
@@ -370,121 +421,27 @@ private:
 
     Handler m_handler;
 
-    StateTransition lookup_state_transition(u8);
-)~~~");
-
-    auto table_generator = generator.fork();
-    generate_lookup_table(machine, table_generator);
-    generator.append(R"~~~(
-}; // end @class_name@
-)~~~");
-
-    if (machine.namespaces.has_value()) {
-        generator.append(R"~~~(
-} // end namespace
-)~~~");
-    }
-}
-
-void output_cpp(const StateMachine& machine, SourceGenerator& generator)
-{
-    VERIFY(!machine.name.is_empty());
-    generator.set("class_name", machine.name);
-    generator.set("state_count", String::number(machine.states.size() + 1));
-
-    generator.append(R"~~~(
-#include "@class_name@.h"
-#include <AK/Function.h>
-#include <AK/Types.h>
-)~~~");
-    if (machine.namespaces.has_value()) {
-        generator.set("namespace", machine.namespaces.value());
-        generator.append(R"~~~(
-namespace @namespace@ {
+    ALWAYS_INLINE StateTransition lookup_state_transition(u8 byte)
+    {
+        VERIFY((u8)m_state < @state_count@);
 )~~~");
-    }
-    generator.append(R"~~~(
-@class_name@::@class_name@(Handler handler)
-    : m_handler(move(handler))
-{
-}
-
-ALWAYS_INLINE @class_name@::StateTransition @class_name@::lookup_state_transition(u8 byte)
-{
-    VERIFY((u8)m_state < @state_count@);
-    )~~~");
     if (machine.anywhere.has_value()) {
         generator.append(R"~~~(
-    auto anywhere_state = STATE_TRANSITION_TABLE[0][byte];
-    if (anywhere_state.new_state != @class_name@::State::_Anywhere || anywhere_state.action != @class_name@::Action::_Ignore)
-        return anywhere_state;
-    else
-)~~~");
-    }
-    generator.append(R"~~~(
-        return STATE_TRANSITION_TABLE[(u8)m_state][byte];
-}
-)~~~");
-
-    generator.append(R"~~~(
-
-void @class_name@::advance(u8 byte)
-{
-    auto next_state = lookup_state_transition(byte);
-    bool state_will_change = next_state.new_state != m_state && next_state.new_state != @class_name@::State::_Anywhere;
-
-    // only run exit directive if state is being changed
-    if (state_will_change)
-    {
-        switch (m_state)
-        {
-)~~~");
-    for (auto s : machine.states) {
-        auto state_generator = generator.fork();
-        if (s.exit_action.has_value()) {
-            state_generator.set("state_name", s.name);
-            state_generator.set("action", s.exit_action.value());
-            state_generator.append(R"~~~(
-        case @class_name@::State::@state_name@:
-            m_handler(Action::@action@, byte);
-            break;
+        auto anywhere_state = STATE_TRANSITION_TABLE[0][byte];
+        if (anywhere_state.new_state != State::_Anywhere || anywhere_state.action != Action::_Ignore)
+            return anywhere_state;
+        else
 )~~~");
-        }
     }
     generator.append(R"~~~(
-        default:
-            break;
-        }
+            return STATE_TRANSITION_TABLE[(u8)m_state][byte];
     }
-
-    if (next_state.action != @class_name@::Action::_Ignore)
-        m_handler(next_state.action, byte);
-    m_state = next_state.new_state;
-
-    // only run entry directive if state is being changed
-    if (state_will_change)
-    {
-        switch (next_state.new_state)
-        {
 )~~~");
-    for (auto state : machine.states) {
-        auto state_generator = generator.fork();
-        if (state.entry_action.has_value()) {
-            state_generator.set("state_name", state.name);
-            state_generator.set("action", state.entry_action.value());
-            state_generator.append(R"~~~(
-        case @class_name@::State::@state_name@:
-            m_handler(Action::@action@, byte);
-            break;
-)~~~");
-        }
-    }
+
+    auto table_generator = generator.fork();
+    generate_lookup_table(machine, table_generator);
     generator.append(R"~~~(
-        default:
-            break;
-        }
-    }
-}
+}; // end @class_name@
 )~~~");
 
     if (machine.namespaces.has_value()) {

+ 4 - 0
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)

+ 162 - 0
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;
+    }
+}
+}

+ 77 - 0
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 };
+};
+
+}

+ 1 - 1
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
 

+ 315 - 390
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())

+ 48 - 58
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 };
 };