From 54ab6fe5b9ab1d95a2a58ba2ebd40aaeb8e3eeaa Mon Sep 17 00:00:00 2001 From: Ali Mohammad Pur Date: Tue, 6 Feb 2024 01:41:35 +0330 Subject: [PATCH] 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. --- Kernel/Devices/TTY/VirtualConsole.cpp | 5 ++ Kernel/Devices/TTY/VirtualConsole.h | 1 + Userland/Applications/Terminal/main.cpp | 10 +++ .../TerminalSettings/MainWidget.cpp | 43 +++++++++++++ .../TerminalSettings/MainWidget.h | 4 ++ .../TerminalSettings/TerminalSettingsMain.gml | 18 ++++++ .../DevTools/HackStudio/TerminalWrapper.cpp | 2 + Userland/Libraries/LibVT/Line.h | 17 +++++ Userland/Libraries/LibVT/Terminal.cpp | 63 +++++++++++++++++++ Userland/Libraries/LibVT/Terminal.h | 7 +++ Userland/Libraries/LibVT/TerminalWidget.cpp | 47 ++++++++++++++ Userland/Libraries/LibVT/TerminalWidget.h | 21 +++++++ 12 files changed, 238 insertions(+) diff --git a/Kernel/Devices/TTY/VirtualConsole.cpp b/Kernel/Devices/TTY/VirtualConsole.cpp index 98c6ebdb0c..7441976d9a 100644 --- a/Kernel/Devices/TTY/VirtualConsole.cpp +++ b/Kernel/Devices/TTY/VirtualConsole.cpp @@ -377,6 +377,11 @@ void VirtualConsole::terminal_history_changed(int) // 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) { for (size_t i = 0; i < size; i++) diff --git a/Kernel/Devices/TTY/VirtualConsole.h b/Kernel/Devices/TTY/VirtualConsole.h index ee9856c136..98e66664cf 100644 --- a/Kernel/Devices/TTY/VirtualConsole.h +++ b/Kernel/Devices/TTY/VirtualConsole.h @@ -99,6 +99,7 @@ private: virtual void set_window_progress(int, int) override; virtual void terminal_did_resize(u16 columns, u16 rows) 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 set_cursor_shape(VT::CursorShape) override; virtual void set_cursor_blinking(bool) override; diff --git a/Userland/Applications/Terminal/main.cpp b/Userland/Applications/Terminal/main.cpp index 58b7f947c7..11498e19a9 100644 --- a/Userland/Applications/Terminal/main.cpp +++ b/Userland/Applications/Terminal/main.cpp @@ -284,6 +284,8 @@ ErrorOr serenity_main(Main::Arguments arguments) window->set_obey_widget_min_size(false); auto terminal = window->set_main_widget(ptm_fd, true); + terminal->set_startup_process_id(shell_pid); + terminal->on_command_exit = [&] { app->quit(0); }; @@ -309,6 +311,13 @@ ErrorOr serenity_main(Main::Arguments arguments) 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); terminal->set_cursor_shape(cursor_shape); @@ -399,6 +408,7 @@ ErrorOr serenity_main(Main::Arguments arguments) window->set_fullscreen(!window->is_fullscreen()); })); 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& font = terminal->font(); diff --git a/Userland/Applications/TerminalSettings/MainWidget.cpp b/Userland/Applications/TerminalSettings/MainWidget.cpp index 6b4d96d173..21ff5389de 100644 --- a/Userland/Applications/TerminalSettings/MainWidget.cpp +++ b/Userland/Applications/TerminalSettings/MainWidget.cpp @@ -40,10 +40,15 @@ ErrorOr MainWidget::setup() auto& beep_bell_radio = *find_descendant_of_type_named("beep_bell_radio"); auto& visual_bell_radio = *find_descendant_of_type_named("visual_bell_radio"); auto& no_bell_radio = *find_descendant_of_type_named("no_bell_radio"); + auto& automark_off_radio = *find_descendant_of_type_named("automark_of"); + auto& automark_on_interactive_prompt_radio = *find_descendant_of_type_named("automark_on_interactive_prompt"); m_bell_mode = parse_bell(Config::read_string("Terminal"sv, "Window"sv, "Bell"sv)); 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) { case VT::TerminalWidget::BellMode::Visible: visual_bell_radio.set_checked(true, GUI::AllowCallback::No); @@ -72,6 +77,26 @@ ErrorOr MainWidget::setup() 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_orignal_confirm_close = m_confirm_close; auto& confirm_close_checkbox = *find_descendant_of_type_named("terminal_confirm_close"); @@ -106,6 +131,24 @@ ByteString MainWidget::stringify_bell(VT::TerminalWidget::BellMode bell_mode) 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() { m_original_bell_mode = m_bell_mode; diff --git a/Userland/Applications/TerminalSettings/MainWidget.h b/Userland/Applications/TerminalSettings/MainWidget.h index 416c67f9ba..259cc9bf93 100644 --- a/Userland/Applications/TerminalSettings/MainWidget.h +++ b/Userland/Applications/TerminalSettings/MainWidget.h @@ -29,12 +29,16 @@ private: void write_back_settings() const; 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_automark_mode(VT::TerminalWidget::AutoMarkMode automark_mode); 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 }; VT::TerminalWidget::BellMode m_original_bell_mode; + VT::TerminalWidget::AutoMarkMode m_original_automark_mode; bool m_orignal_confirm_close { true }; }; } diff --git a/Userland/Applications/TerminalSettings/TerminalSettingsMain.gml b/Userland/Applications/TerminalSettings/TerminalSettingsMain.gml index 3fb72e1e2b..a15c9b73ba 100644 --- a/Userland/Applications/TerminalSettings/TerminalSettingsMain.gml +++ b/Userland/Applications/TerminalSettings/TerminalSettingsMain.gml @@ -48,4 +48,22 @@ 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" + } + } } diff --git a/Userland/DevTools/HackStudio/TerminalWrapper.cpp b/Userland/DevTools/HackStudio/TerminalWrapper.cpp index a62db7d440..67f9c329be 100644 --- a/Userland/DevTools/HackStudio/TerminalWrapper.cpp +++ b/Userland/DevTools/HackStudio/TerminalWrapper.cpp @@ -42,6 +42,8 @@ ErrorOr TerminalWrapper::run_command(ByteString const& command, Optional 0) { + m_terminal_widget->set_startup_process_id(m_pid); + if (wait_for_exit == WaitForExit::Yes) { GUI::Application::the()->event_loop().spin_until([this]() { return m_child_exited; diff --git a/Userland/Libraries/LibVT/Line.h b/Userland/Libraries/LibVT/Line.h index fd920b593c..aadfdbe5d5 100644 --- a/Userland/Libraries/LibVT/Line.h +++ b/Userland/Libraries/LibVT/Line.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include #include @@ -16,6 +17,10 @@ namespace VT { +AK_TYPEDEF_DISTINCT_ORDERED_ID(u32, Mark); + +inline static constexpr Mark Unmarked = 0; + class Line { AK_MAKE_NONCOPYABLE(Line); AK_MAKE_NONMOVABLE(Line); @@ -40,6 +45,7 @@ public: void clear(Attribute const& attribute = Attribute()) { m_terminated_at.clear(); + m_mark = Unmarked; clear_range(0, m_cells.size() - 1, 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; } void set_dirty(bool b) { m_dirty = b; } + Optional mark() const + { + return m_mark == Unmarked ? OptionalNone {} : Optional(m_mark); + } + void set_marked(Mark mark) + { + set_dirty(m_mark != mark); + m_mark = mark; + } + Optional termination_column() const { return m_terminated_at; } 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); Vector m_cells; + Mark m_mark { Unmarked }; bool m_dirty { false }; // Note: The alignment is 8, so this member lives in the padding (that already existed before it was introduced) [[no_unique_address]] Optional m_terminated_at; diff --git a/Userland/Libraries/LibVT/Terminal.cpp b/Userland/Libraries/LibVT/Terminal.cpp index 96fbddd0dd..6b4fda3035 100644 --- a/Userland/Libraries/LibVT/Terminal.cpp +++ b/Userland/Libraries/LibVT/Terminal.cpp @@ -35,6 +35,7 @@ void Terminal::clear() for (size_t i = 0; i < rows(); ++i) active_buffer()[i]->clear(); set_cursor(0, 0); + m_client.terminal_did_perform_possibly_partial_clear(); } void Terminal::clear_history() @@ -45,6 +46,68 @@ void Terminal::clear_history() m_history_start = 0; 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 void Terminal::alter_ansi_mode(bool should_set, Parameters params) diff --git a/Userland/Libraries/LibVT/Terminal.h b/Userland/Libraries/LibVT/Terminal.h index 748c36907e..cf705626ae 100644 --- a/Userland/Libraries/LibVT/Terminal.h +++ b/Userland/Libraries/LibVT/Terminal.h @@ -17,6 +17,7 @@ #ifndef KERNEL # include +# include # include # include #else @@ -49,6 +50,7 @@ public: virtual void set_window_progress(int value, int max) = 0; virtual void terminal_did_resize(u16 columns, u16 rows) = 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 set_cursor_shape(CursorShape) = 0; virtual void set_cursor_blinking(bool) = 0; @@ -85,8 +87,12 @@ public: } #ifndef KERNEL + void mark_cursor(); + OrderedHashTable const& marks() const { return m_valid_marks; } + void clear(); void clear_history(); + void clear_to_mark(Mark); #else virtual void clear() = 0; virtual void clear_history() = 0; @@ -438,6 +444,7 @@ protected: #ifndef KERNEL ByteString m_current_window_title; Vector m_title_stack; + OrderedHashTable m_valid_marks; #endif #ifndef KERNEL diff --git a/Userland/Libraries/LibVT/TerminalWidget.cpp b/Userland/Libraries/LibVT/TerminalWidget.cpp index 607a7e5f5e..37533245b3 100644 --- a/Userland/Libraries/LibVT/TerminalWidget.cpp +++ b/Userland/Libraries/LibVT/TerminalWidget.cpp @@ -69,8 +69,18 @@ void TerminalWidget::set_pty_master_fd(int fd) set_pty_master_fd(-1); return; } + for (ssize_t i = 0; i < nread; ++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(); }; } @@ -140,11 +150,16 @@ TerminalWidget::TerminalWidget(int ptm_fd, bool automatic_size_policy) 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->add_action(copy_action()); m_context_menu->add_action(paste_action()); m_context_menu->add_separator(); m_context_menu->add_action(clear_including_history_action()); + m_context_menu->add_action(clear_to_previous_mark_action()); update_copy_action(); update_paste_action(); @@ -1035,6 +1050,22 @@ void TerminalWidget::terminal_history_changed(int 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) { auto pixel_size = widget_size_for_font(font()); @@ -1218,6 +1249,15 @@ void TerminalWidget::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() { m_scrollbar->set_value(m_scrollbar->max()); @@ -1375,4 +1415,11 @@ ByteString TerminalWidget::stringify_cursor_shape(VT::CursorShape cursor_shape) 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(); + } +} + } diff --git a/Userland/Libraries/LibVT/TerminalWidget.h b/Userland/Libraries/LibVT/TerminalWidget.h index 5293cff423..ca8e296628 100644 --- a/Userland/Libraries/LibVT/TerminalWidget.h +++ b/Userland/Libraries/LibVT/TerminalWidget.h @@ -55,6 +55,14 @@ public: BellMode bell_mode() { return m_bell_mode; } 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 selection_contains(const VT::Position&) const; ByteString selected_text() const; @@ -77,10 +85,14 @@ public: GUI::Action& copy_action() { return *m_copy_action; } GUI::Action& paste_action() { return *m_paste_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 paste(); 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; } @@ -133,6 +145,7 @@ private: virtual void set_window_progress(int value, int max) override; virtual void terminal_did_resize(u16 columns, u16 rows) 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; // ^GUI::Clipboard::ClipboardClient @@ -163,6 +176,8 @@ private: void update_cached_font_metrics(); + void handle_pty_owner_change(pid_t new_owner); + VT::Terminal m_terminal; VT::Range m_selection; @@ -183,6 +198,8 @@ private: ByteString m_color_scheme_name; + AutoMarkMode m_auto_mark_mode { AutoMarkMode::MarkInteractiveShellPrompt }; + BellMode m_bell_mode { BellMode::Visible }; bool m_alt_key_held { false }; bool m_rectangle_selection { false }; @@ -229,6 +246,7 @@ private: RefPtr m_copy_action; RefPtr m_paste_action; RefPtr m_clear_including_history_action; + RefPtr m_clear_to_previous_mark_action; RefPtr m_context_menu; RefPtr m_context_menu_for_hyperlink; @@ -237,6 +255,9 @@ private: Gfx::IntPoint m_left_mousedown_position; VT::Position m_left_mousedown_position_buffer; + + bool m_startup_process_owns_pty { false }; + pid_t m_startup_process_id { -1 }; }; }