Bläddra i källkod

LibVT+Everywhere: Introduce 'automarks' and 'clear previous command'

Automarks are similar to bookmarks placed by the terminal, allowing the
user to selectively remove a single command and its output from the
terminal scrollback.
This commit implements a single way to add marks: automatically placing
them when the shell becomes interactive.

To make sure the shell behaves correctly after its expected prompt
position changes, the terminal layer forces a resize event to be passed
to the shell on such (possibly) partial clears; this also has the nice
side effect of fixing the disappearing prompt on the preexisting "clear
including history" action: Fixes #4192.
Ali Mohammad Pur 1 år sedan
förälder
incheckning
54ab6fe5b9

+ 5 - 0
Kernel/Devices/TTY/VirtualConsole.cpp

@@ -377,6 +377,11 @@ void VirtualConsole::terminal_history_changed(int)
     // Do nothing, I guess?
     // Do nothing, I guess?
 }
 }
 
 
+void VirtualConsole::terminal_did_perform_possibly_partial_clear()
+{
+    // Do nothing, we're not going to hit this anyway.
+}
+
 void VirtualConsole::emit(u8 const* data, size_t size)
 void VirtualConsole::emit(u8 const* data, size_t size)
 {
 {
     for (size_t i = 0; i < size; i++)
     for (size_t i = 0; i < size; i++)

+ 1 - 0
Kernel/Devices/TTY/VirtualConsole.h

@@ -99,6 +99,7 @@ private:
     virtual void set_window_progress(int, int) override;
     virtual void set_window_progress(int, int) override;
     virtual void terminal_did_resize(u16 columns, u16 rows) override;
     virtual void terminal_did_resize(u16 columns, u16 rows) override;
     virtual void terminal_history_changed(int) override;
     virtual void terminal_history_changed(int) override;
+    virtual void terminal_did_perform_possibly_partial_clear() override;
     virtual void emit(u8 const*, size_t) override;
     virtual void emit(u8 const*, size_t) override;
     virtual void set_cursor_shape(VT::CursorShape) override;
     virtual void set_cursor_shape(VT::CursorShape) override;
     virtual void set_cursor_blinking(bool) override;
     virtual void set_cursor_blinking(bool) override;

+ 10 - 0
Userland/Applications/Terminal/main.cpp

@@ -284,6 +284,8 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
     window->set_obey_widget_min_size(false);
     window->set_obey_widget_min_size(false);
 
 
     auto terminal = window->set_main_widget<VT::TerminalWidget>(ptm_fd, true);
     auto terminal = window->set_main_widget<VT::TerminalWidget>(ptm_fd, true);
+    terminal->set_startup_process_id(shell_pid);
+
     terminal->on_command_exit = [&] {
     terminal->on_command_exit = [&] {
         app->quit(0);
         app->quit(0);
     };
     };
@@ -309,6 +311,13 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
         terminal->set_bell_mode(VT::TerminalWidget::BellMode::Visible);
         terminal->set_bell_mode(VT::TerminalWidget::BellMode::Visible);
     }
     }
 
 
+    auto automark = Config::read_string("Terminal"sv, "Terminal"sv, "AutoMark"sv, "MarkInteractiveShellPrompt"sv);
+    if (automark == "MarkNothing") {
+        terminal->set_auto_mark_mode(VT::TerminalWidget::AutoMarkMode::MarkNothing);
+    } else {
+        terminal->set_auto_mark_mode(VT::TerminalWidget::AutoMarkMode::MarkInteractiveShellPrompt);
+    }
+
     auto cursor_shape = VT::TerminalWidget::parse_cursor_shape(Config::read_string("Terminal"sv, "Cursor"sv, "Shape"sv, "Block"sv)).value_or(VT::CursorShape::Block);
     auto cursor_shape = VT::TerminalWidget::parse_cursor_shape(Config::read_string("Terminal"sv, "Cursor"sv, "Shape"sv, "Block"sv)).value_or(VT::CursorShape::Block);
     terminal->set_cursor_shape(cursor_shape);
     terminal->set_cursor_shape(cursor_shape);
 
 
@@ -399,6 +408,7 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
         window->set_fullscreen(!window->is_fullscreen());
         window->set_fullscreen(!window->is_fullscreen());
     }));
     }));
     view_menu->add_action(terminal->clear_including_history_action());
     view_menu->add_action(terminal->clear_including_history_action());
+    view_menu->add_action(terminal->clear_to_previous_mark_action());
 
 
     auto adjust_font_size = [&](float adjustment, Gfx::Font::AllowInexactSizeMatch preference) {
     auto adjust_font_size = [&](float adjustment, Gfx::Font::AllowInexactSizeMatch preference) {
         auto& font = terminal->font();
         auto& font = terminal->font();

+ 43 - 0
Userland/Applications/TerminalSettings/MainWidget.cpp

@@ -40,10 +40,15 @@ ErrorOr<void> MainWidget::setup()
     auto& beep_bell_radio = *find_descendant_of_type_named<GUI::RadioButton>("beep_bell_radio");
     auto& beep_bell_radio = *find_descendant_of_type_named<GUI::RadioButton>("beep_bell_radio");
     auto& visual_bell_radio = *find_descendant_of_type_named<GUI::RadioButton>("visual_bell_radio");
     auto& visual_bell_radio = *find_descendant_of_type_named<GUI::RadioButton>("visual_bell_radio");
     auto& no_bell_radio = *find_descendant_of_type_named<GUI::RadioButton>("no_bell_radio");
     auto& no_bell_radio = *find_descendant_of_type_named<GUI::RadioButton>("no_bell_radio");
+    auto& automark_off_radio = *find_descendant_of_type_named<GUI::RadioButton>("automark_of");
+    auto& automark_on_interactive_prompt_radio = *find_descendant_of_type_named<GUI::RadioButton>("automark_on_interactive_prompt");
 
 
     m_bell_mode = parse_bell(Config::read_string("Terminal"sv, "Window"sv, "Bell"sv));
     m_bell_mode = parse_bell(Config::read_string("Terminal"sv, "Window"sv, "Bell"sv));
     m_original_bell_mode = m_bell_mode;
     m_original_bell_mode = m_bell_mode;
 
 
+    m_automark_mode = parse_automark_mode(Config::read_string("Terminal"sv, "Terminal"sv, "AutoMark"sv));
+    m_original_automark_mode = m_automark_mode;
+
     switch (m_bell_mode) {
     switch (m_bell_mode) {
     case VT::TerminalWidget::BellMode::Visible:
     case VT::TerminalWidget::BellMode::Visible:
         visual_bell_radio.set_checked(true, GUI::AllowCallback::No);
         visual_bell_radio.set_checked(true, GUI::AllowCallback::No);
@@ -72,6 +77,26 @@ ErrorOr<void> MainWidget::setup()
         set_modified(true);
         set_modified(true);
     };
     };
 
 
+    switch (m_automark_mode) {
+    case VT::TerminalWidget::AutoMarkMode::MarkNothing:
+        automark_off_radio.set_checked(true, GUI::AllowCallback::No);
+        break;
+    case VT::TerminalWidget::AutoMarkMode::MarkInteractiveShellPrompt:
+        automark_on_interactive_prompt_radio.set_checked(true, GUI::AllowCallback::No);
+        break;
+    }
+
+    automark_off_radio.on_checked = [this](bool) {
+        m_automark_mode = VT::TerminalWidget::AutoMarkMode::MarkNothing;
+        Config::write_string("Terminal"sv, "Terminal"sv, "AutoMark"sv, stringify_automark_mode(m_automark_mode));
+        set_modified(true);
+    };
+    automark_on_interactive_prompt_radio.on_checked = [this](bool) {
+        m_automark_mode = VT::TerminalWidget::AutoMarkMode::MarkInteractiveShellPrompt;
+        Config::write_string("Terminal"sv, "Terminal"sv, "AutoMark"sv, stringify_automark_mode(m_automark_mode));
+        set_modified(true);
+    };
+
     m_confirm_close = Config::read_bool("Terminal"sv, "Terminal"sv, "ConfirmClose"sv, true);
     m_confirm_close = Config::read_bool("Terminal"sv, "Terminal"sv, "ConfirmClose"sv, true);
     m_orignal_confirm_close = m_confirm_close;
     m_orignal_confirm_close = m_confirm_close;
     auto& confirm_close_checkbox = *find_descendant_of_type_named<GUI::CheckBox>("terminal_confirm_close");
     auto& confirm_close_checkbox = *find_descendant_of_type_named<GUI::CheckBox>("terminal_confirm_close");
@@ -106,6 +131,24 @@ ByteString MainWidget::stringify_bell(VT::TerminalWidget::BellMode bell_mode)
     VERIFY_NOT_REACHED();
     VERIFY_NOT_REACHED();
 }
 }
 
 
+VT::TerminalWidget::AutoMarkMode MainWidget::parse_automark_mode(StringView automark_mode)
+{
+    if (automark_mode == "MarkNothing")
+        return VT::TerminalWidget::AutoMarkMode::MarkNothing;
+    if (automark_mode == "MarkInteractiveShellPrompt")
+        return VT::TerminalWidget::AutoMarkMode::MarkInteractiveShellPrompt;
+    VERIFY_NOT_REACHED();
+}
+
+ByteString MainWidget::stringify_automark_mode(VT::TerminalWidget::AutoMarkMode automark_mode)
+{
+    if (automark_mode == VT::TerminalWidget::AutoMarkMode::MarkNothing)
+        return "MarkNothing";
+    if (automark_mode == VT::TerminalWidget::AutoMarkMode::MarkInteractiveShellPrompt)
+        return "MarkInteractiveShellPrompt";
+    VERIFY_NOT_REACHED();
+}
+
 void MainWidget::apply_settings()
 void MainWidget::apply_settings()
 {
 {
     m_original_bell_mode = m_bell_mode;
     m_original_bell_mode = m_bell_mode;

+ 4 - 0
Userland/Applications/TerminalSettings/MainWidget.h

@@ -29,12 +29,16 @@ private:
     void write_back_settings() const;
     void write_back_settings() const;
 
 
     static VT::TerminalWidget::BellMode parse_bell(StringView bell_string);
     static VT::TerminalWidget::BellMode parse_bell(StringView bell_string);
+    static VT::TerminalWidget::AutoMarkMode parse_automark_mode(StringView automark_mode);
     static ByteString stringify_bell(VT::TerminalWidget::BellMode bell_mode);
     static ByteString stringify_bell(VT::TerminalWidget::BellMode bell_mode);
+    static ByteString stringify_automark_mode(VT::TerminalWidget::AutoMarkMode automark_mode);
 
 
     VT::TerminalWidget::BellMode m_bell_mode { VT::TerminalWidget::BellMode::Disabled };
     VT::TerminalWidget::BellMode m_bell_mode { VT::TerminalWidget::BellMode::Disabled };
+    VT::TerminalWidget::AutoMarkMode m_automark_mode { VT::TerminalWidget::AutoMarkMode::MarkInteractiveShellPrompt };
     bool m_confirm_close { true };
     bool m_confirm_close { true };
 
 
     VT::TerminalWidget::BellMode m_original_bell_mode;
     VT::TerminalWidget::BellMode m_original_bell_mode;
+    VT::TerminalWidget::AutoMarkMode m_original_automark_mode;
     bool m_orignal_confirm_close { true };
     bool m_orignal_confirm_close { true };
 };
 };
 }
 }

+ 18 - 0
Userland/Applications/TerminalSettings/TerminalSettingsMain.gml

@@ -48,4 +48,22 @@
             text: "Confirm exit when process is active"
             text: "Confirm exit when process is active"
         }
         }
     }
     }
+
+    @GUI::GroupBox {
+        title: "Auto-mark behavior"
+        preferred_height: "fit"
+        layout: @GUI::VerticalBoxLayout {
+            margins: [8]
+        }
+
+        @GUI::RadioButton {
+            name: "automark_off"
+            text: "Do not auto-mark"
+        }
+
+        @GUI::RadioButton {
+            name: "automark_on_interactive_prompt"
+            text: "Auto-mark on interactive shell prompts"
+        }
+    }
 }
 }

+ 2 - 0
Userland/DevTools/HackStudio/TerminalWrapper.cpp

@@ -42,6 +42,8 @@ ErrorOr<void> TerminalWrapper::run_command(ByteString const& command, Optional<B
     m_pid = TRY(Core::System::fork());
     m_pid = TRY(Core::System::fork());
 
 
     if (m_pid > 0) {
     if (m_pid > 0) {
+        m_terminal_widget->set_startup_process_id(m_pid);
+
         if (wait_for_exit == WaitForExit::Yes) {
         if (wait_for_exit == WaitForExit::Yes) {
             GUI::Application::the()->event_loop().spin_until([this]() {
             GUI::Application::the()->event_loop().spin_until([this]() {
                 return m_child_exited;
                 return m_child_exited;

+ 17 - 0
Userland/Libraries/LibVT/Line.h

@@ -8,6 +8,7 @@
 #pragma once
 #pragma once
 
 
 #include <AK/AnyOf.h>
 #include <AK/AnyOf.h>
+#include <AK/DistinctNumeric.h>
 #include <AK/Noncopyable.h>
 #include <AK/Noncopyable.h>
 #include <AK/Vector.h>
 #include <AK/Vector.h>
 #include <LibVT/Attribute.h>
 #include <LibVT/Attribute.h>
@@ -16,6 +17,10 @@
 
 
 namespace VT {
 namespace VT {
 
 
+AK_TYPEDEF_DISTINCT_ORDERED_ID(u32, Mark);
+
+inline static constexpr Mark Unmarked = 0;
+
 class Line {
 class Line {
     AK_MAKE_NONCOPYABLE(Line);
     AK_MAKE_NONCOPYABLE(Line);
     AK_MAKE_NONMOVABLE(Line);
     AK_MAKE_NONMOVABLE(Line);
@@ -40,6 +45,7 @@ public:
     void clear(Attribute const& attribute = Attribute())
     void clear(Attribute const& attribute = Attribute())
     {
     {
         m_terminated_at.clear();
         m_terminated_at.clear();
+        m_mark = Unmarked;
         clear_range(0, m_cells.size() - 1, attribute);
         clear_range(0, m_cells.size() - 1, attribute);
     }
     }
     void clear_range(size_t first_column, size_t last_column, Attribute const& attribute = Attribute());
     void clear_range(size_t first_column, size_t last_column, Attribute const& attribute = Attribute());
@@ -76,6 +82,16 @@ public:
     bool is_dirty() const { return m_dirty; }
     bool is_dirty() const { return m_dirty; }
     void set_dirty(bool b) { m_dirty = b; }
     void set_dirty(bool b) { m_dirty = b; }
 
 
+    Optional<Mark> mark() const
+    {
+        return m_mark == Unmarked ? OptionalNone {} : Optional<Mark>(m_mark);
+    }
+    void set_marked(Mark mark)
+    {
+        set_dirty(m_mark != mark);
+        m_mark = mark;
+    }
+
     Optional<u16> termination_column() const { return m_terminated_at; }
     Optional<u16> termination_column() const { return m_terminated_at; }
     void set_terminated(u16 column) { m_terminated_at = column; }
     void set_terminated(u16 column) { m_terminated_at = column; }
 
 
@@ -84,6 +100,7 @@ private:
     void push_cells_into_next_line(size_t new_length, Line* next_line, bool cursor_is_on_next_line, CursorPosition* cursor);
     void push_cells_into_next_line(size_t new_length, Line* next_line, bool cursor_is_on_next_line, CursorPosition* cursor);
 
 
     Vector<Cell> m_cells;
     Vector<Cell> m_cells;
+    Mark m_mark { Unmarked };
     bool m_dirty { false };
     bool m_dirty { false };
     // Note: The alignment is 8, so this member lives in the padding (that already existed before it was introduced)
     // Note: The alignment is 8, so this member lives in the padding (that already existed before it was introduced)
     [[no_unique_address]] Optional<u16> m_terminated_at;
     [[no_unique_address]] Optional<u16> m_terminated_at;

+ 63 - 0
Userland/Libraries/LibVT/Terminal.cpp

@@ -35,6 +35,7 @@ void Terminal::clear()
     for (size_t i = 0; i < rows(); ++i)
     for (size_t i = 0; i < rows(); ++i)
         active_buffer()[i]->clear();
         active_buffer()[i]->clear();
     set_cursor(0, 0);
     set_cursor(0, 0);
+    m_client.terminal_did_perform_possibly_partial_clear();
 }
 }
 
 
 void Terminal::clear_history()
 void Terminal::clear_history()
@@ -45,6 +46,68 @@ void Terminal::clear_history()
     m_history_start = 0;
     m_history_start = 0;
     m_client.terminal_history_changed(-previous_history_size);
     m_client.terminal_history_changed(-previous_history_size);
 }
 }
+
+void Terminal::clear_to_mark(Mark mark)
+{
+    auto cursor_row = this->cursor_row();
+    ScopeGuard send_sigwinch = [&] {
+        set_cursor(cursor_row, 1);
+        mark_cursor();
+        m_client.terminal_did_perform_possibly_partial_clear();
+    };
+    m_valid_marks.remove(mark);
+
+    {
+        auto it = active_buffer().rbegin();
+        size_t row = m_rows - 1;
+        // Skip to the cursor line.
+        for (size_t i = this->cursor_row() + 1; i < active_buffer().size(); ++i, row--)
+            ++it;
+        for (; it != active_buffer().rend(); ++it, row--) {
+            auto& line = *it;
+            auto line_mark = line->mark();
+            auto is_target_line = line_mark == mark;
+            if (line_mark.has_value())
+                m_valid_marks.remove(*line_mark);
+            line->clear();
+            if (is_target_line) {
+                cursor_row = row;
+                return;
+            }
+        }
+    }
+
+    // If the mark is not found, go through the history.
+    auto it = AK::find_if(
+        m_history.rbegin(),
+        m_history.rend(),
+        [mark](auto& line) {
+            return line->mark() == mark;
+        });
+    auto index = it == m_history.rend() ? 0 : m_history.size() - it.index();
+    m_client.terminal_history_changed(m_history.size() - index);
+    auto count = m_history.size() - index;
+    for (size_t i = 0; i < count; ++i) {
+        if (auto mark = m_history[index + i]->mark(); mark.has_value())
+            m_valid_marks.remove(*mark);
+    }
+    m_history.remove(index, count);
+    cursor_row = 0;
+}
+
+void Terminal::mark_cursor()
+{
+    static u32 next_mark_id { 0 };
+
+    auto& line = active_buffer()[cursor_row()];
+    if (line->mark().has_value()) {
+        return;
+    }
+
+    auto mark = Mark(next_mark_id++);
+    line->set_marked(mark);
+    m_valid_marks.set(mark);
+}
 #endif
 #endif
 
 
 void Terminal::alter_ansi_mode(bool should_set, Parameters params)
 void Terminal::alter_ansi_mode(bool should_set, Parameters params)

+ 7 - 0
Userland/Libraries/LibVT/Terminal.h

@@ -17,6 +17,7 @@
 
 
 #ifndef KERNEL
 #ifndef KERNEL
 #    include <AK/ByteString.h>
 #    include <AK/ByteString.h>
+#    include <AK/HashTable.h>
 #    include <LibVT/Attribute.h>
 #    include <LibVT/Attribute.h>
 #    include <LibVT/Line.h>
 #    include <LibVT/Line.h>
 #else
 #else
@@ -49,6 +50,7 @@ public:
     virtual void set_window_progress(int value, int max) = 0;
     virtual void set_window_progress(int value, int max) = 0;
     virtual void terminal_did_resize(u16 columns, u16 rows) = 0;
     virtual void terminal_did_resize(u16 columns, u16 rows) = 0;
     virtual void terminal_history_changed(int delta) = 0;
     virtual void terminal_history_changed(int delta) = 0;
+    virtual void terminal_did_perform_possibly_partial_clear() = 0;
     virtual void emit(u8 const*, size_t) = 0;
     virtual void emit(u8 const*, size_t) = 0;
     virtual void set_cursor_shape(CursorShape) = 0;
     virtual void set_cursor_shape(CursorShape) = 0;
     virtual void set_cursor_blinking(bool) = 0;
     virtual void set_cursor_blinking(bool) = 0;
@@ -85,8 +87,12 @@ public:
     }
     }
 
 
 #ifndef KERNEL
 #ifndef KERNEL
+    void mark_cursor();
+    OrderedHashTable<Mark> const& marks() const { return m_valid_marks; }
+
     void clear();
     void clear();
     void clear_history();
     void clear_history();
+    void clear_to_mark(Mark);
 #else
 #else
     virtual void clear() = 0;
     virtual void clear() = 0;
     virtual void clear_history() = 0;
     virtual void clear_history() = 0;
@@ -438,6 +444,7 @@ protected:
 #ifndef KERNEL
 #ifndef KERNEL
     ByteString m_current_window_title;
     ByteString m_current_window_title;
     Vector<ByteString> m_title_stack;
     Vector<ByteString> m_title_stack;
+    OrderedHashTable<Mark> m_valid_marks;
 #endif
 #endif
 
 
 #ifndef KERNEL
 #ifndef KERNEL

+ 47 - 0
Userland/Libraries/LibVT/TerminalWidget.cpp

@@ -69,8 +69,18 @@ void TerminalWidget::set_pty_master_fd(int fd)
             set_pty_master_fd(-1);
             set_pty_master_fd(-1);
             return;
             return;
         }
         }
+
         for (ssize_t i = 0; i < nread; ++i)
         for (ssize_t i = 0; i < nread; ++i)
             m_terminal.on_input(buffer[i]);
             m_terminal.on_input(buffer[i]);
+
+        auto owned_by_startup_process = m_startup_process_owns_pty;
+        auto pgrp = tcgetpgrp(m_ptm_fd);
+        m_startup_process_owns_pty = pgrp == m_startup_process_id;
+        if (m_startup_process_owns_pty != owned_by_startup_process) {
+            // pty owner state changed, handle it.
+            handle_pty_owner_change(pgrp);
+        }
+
         flush_dirty_lines();
         flush_dirty_lines();
     };
     };
 }
 }
@@ -140,11 +150,16 @@ TerminalWidget::TerminalWidget(int ptm_fd, bool automatic_size_policy)
         clear_including_history();
         clear_including_history();
     });
     });
 
 
+    m_clear_to_previous_mark_action = GUI::Action::create("Clear &Previous Command", { Mod_Ctrl | Mod_Shift, Key_U }, [this](auto&) {
+        clear_to_previous_mark();
+    });
+
     m_context_menu = GUI::Menu::construct();
     m_context_menu = GUI::Menu::construct();
     m_context_menu->add_action(copy_action());
     m_context_menu->add_action(copy_action());
     m_context_menu->add_action(paste_action());
     m_context_menu->add_action(paste_action());
     m_context_menu->add_separator();
     m_context_menu->add_separator();
     m_context_menu->add_action(clear_including_history_action());
     m_context_menu->add_action(clear_including_history_action());
+    m_context_menu->add_action(clear_to_previous_mark_action());
 
 
     update_copy_action();
     update_copy_action();
     update_paste_action();
     update_paste_action();
@@ -1035,6 +1050,22 @@ void TerminalWidget::terminal_history_changed(int delta)
         m_selection.offset_row(delta);
         m_selection.offset_row(delta);
 }
 }
 
 
+void TerminalWidget::terminal_did_perform_possibly_partial_clear()
+{
+    // Just pretend the whole terminal was cleared.
+    // Force an update by resizing slightly and then back to the original size.
+    winsize ws;
+    for (ssize_t offset = 1; offset >= 0; --offset) {
+        ws.ws_col = m_terminal.columns() - offset;
+        ws.ws_row = m_terminal.rows() - offset;
+        if (m_ptm_fd != -1) {
+            if (ioctl(m_ptm_fd, TIOCSWINSZ, &ws) < 0) {
+                perror("ioctl(TIOCSWINSZ)");
+            }
+        }
+    }
+}
+
 void TerminalWidget::terminal_did_resize(u16 columns, u16 rows)
 void TerminalWidget::terminal_did_resize(u16 columns, u16 rows)
 {
 {
     auto pixel_size = widget_size_for_font(font());
     auto pixel_size = widget_size_for_font(font());
@@ -1218,6 +1249,15 @@ void TerminalWidget::clear_including_history()
     m_terminal.clear_including_history();
     m_terminal.clear_including_history();
 }
 }
 
 
+void TerminalWidget::clear_to_previous_mark()
+{
+    auto marks = m_terminal.marks().values();
+    size_t offset = m_startup_process_owns_pty ? 2 : 1; // If the shell is the active process, we have an extra mark.
+    if (marks.size() < offset)
+        return;
+    m_terminal.clear_to_mark(marks[marks.size() - offset]);
+}
+
 void TerminalWidget::scroll_to_bottom()
 void TerminalWidget::scroll_to_bottom()
 {
 {
     m_scrollbar->set_value(m_scrollbar->max());
     m_scrollbar->set_value(m_scrollbar->max());
@@ -1375,4 +1415,11 @@ ByteString TerminalWidget::stringify_cursor_shape(VT::CursorShape cursor_shape)
     VERIFY_NOT_REACHED();
     VERIFY_NOT_REACHED();
 }
 }
 
 
+void TerminalWidget::handle_pty_owner_change(pid_t new_owner)
+{
+    if (m_auto_mark_mode == AutoMarkMode::MarkInteractiveShellPrompt && new_owner == m_startup_process_id) {
+        m_terminal.mark_cursor();
+    }
+}
+
 }
 }

+ 21 - 0
Userland/Libraries/LibVT/TerminalWidget.h

@@ -55,6 +55,14 @@ public:
     BellMode bell_mode() { return m_bell_mode; }
     BellMode bell_mode() { return m_bell_mode; }
     void set_bell_mode(BellMode bm) { m_bell_mode = bm; }
     void set_bell_mode(BellMode bm) { m_bell_mode = bm; }
 
 
+    enum class AutoMarkMode {
+        MarkNothing,
+        MarkInteractiveShellPrompt,
+    };
+
+    AutoMarkMode auto_mark_mode() { return m_auto_mark_mode; }
+    void set_auto_mark_mode(AutoMarkMode am) { m_auto_mark_mode = am; }
+
     bool has_selection() const;
     bool has_selection() const;
     bool selection_contains(const VT::Position&) const;
     bool selection_contains(const VT::Position&) const;
     ByteString selected_text() const;
     ByteString selected_text() const;
@@ -77,10 +85,14 @@ public:
     GUI::Action& copy_action() { return *m_copy_action; }
     GUI::Action& copy_action() { return *m_copy_action; }
     GUI::Action& paste_action() { return *m_paste_action; }
     GUI::Action& paste_action() { return *m_paste_action; }
     GUI::Action& clear_including_history_action() { return *m_clear_including_history_action; }
     GUI::Action& clear_including_history_action() { return *m_clear_including_history_action; }
+    GUI::Action& clear_to_previous_mark_action() { return *m_clear_to_previous_mark_action; }
 
 
     void copy();
     void copy();
     void paste();
     void paste();
     void clear_including_history();
     void clear_including_history();
+    void clear_to_previous_mark();
+
+    void set_startup_process_id(pid_t pid) { m_startup_process_id = pid; }
 
 
     const StringView color_scheme_name() const { return m_color_scheme_name; }
     const StringView color_scheme_name() const { return m_color_scheme_name; }
 
 
@@ -133,6 +145,7 @@ private:
     virtual void set_window_progress(int value, int max) override;
     virtual void set_window_progress(int value, int max) override;
     virtual void terminal_did_resize(u16 columns, u16 rows) override;
     virtual void terminal_did_resize(u16 columns, u16 rows) override;
     virtual void terminal_history_changed(int delta) override;
     virtual void terminal_history_changed(int delta) override;
+    virtual void terminal_did_perform_possibly_partial_clear() override;
     virtual void emit(u8 const*, size_t) override;
     virtual void emit(u8 const*, size_t) override;
 
 
     // ^GUI::Clipboard::ClipboardClient
     // ^GUI::Clipboard::ClipboardClient
@@ -163,6 +176,8 @@ private:
 
 
     void update_cached_font_metrics();
     void update_cached_font_metrics();
 
 
+    void handle_pty_owner_change(pid_t new_owner);
+
     VT::Terminal m_terminal;
     VT::Terminal m_terminal;
 
 
     VT::Range m_selection;
     VT::Range m_selection;
@@ -183,6 +198,8 @@ private:
 
 
     ByteString m_color_scheme_name;
     ByteString m_color_scheme_name;
 
 
+    AutoMarkMode m_auto_mark_mode { AutoMarkMode::MarkInteractiveShellPrompt };
+
     BellMode m_bell_mode { BellMode::Visible };
     BellMode m_bell_mode { BellMode::Visible };
     bool m_alt_key_held { false };
     bool m_alt_key_held { false };
     bool m_rectangle_selection { false };
     bool m_rectangle_selection { false };
@@ -229,6 +246,7 @@ private:
     RefPtr<GUI::Action> m_copy_action;
     RefPtr<GUI::Action> m_copy_action;
     RefPtr<GUI::Action> m_paste_action;
     RefPtr<GUI::Action> m_paste_action;
     RefPtr<GUI::Action> m_clear_including_history_action;
     RefPtr<GUI::Action> m_clear_including_history_action;
+    RefPtr<GUI::Action> m_clear_to_previous_mark_action;
 
 
     RefPtr<GUI::Menu> m_context_menu;
     RefPtr<GUI::Menu> m_context_menu;
     RefPtr<GUI::Menu> m_context_menu_for_hyperlink;
     RefPtr<GUI::Menu> m_context_menu_for_hyperlink;
@@ -237,6 +255,9 @@ private:
 
 
     Gfx::IntPoint m_left_mousedown_position;
     Gfx::IntPoint m_left_mousedown_position;
     VT::Position m_left_mousedown_position_buffer;
     VT::Position m_left_mousedown_position_buffer;
+
+    bool m_startup_process_owns_pty { false };
+    pid_t m_startup_process_id { -1 };
 };
 };
 
 
 }
 }