1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-14 05:44:58 +00:00

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.
This commit is contained in:
Ali Mohammad Pur 2024-02-06 01:41:35 +03:30 committed by Ali Mohammad Pur
parent cde528fdd9
commit 54ab6fe5b9
12 changed files with 238 additions and 0 deletions

View file

@ -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++)

View file

@ -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;

View file

@ -284,6 +284,8 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
window->set_obey_widget_min_size(false);
auto terminal = window->set_main_widget<VT::TerminalWidget>(ptm_fd, true);
terminal->set_startup_process_id(shell_pid);
terminal->on_command_exit = [&] {
app->quit(0);
};
@ -309,6 +311,13 @@ ErrorOr<int> 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<int> 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();

View file

@ -40,10 +40,15 @@ ErrorOr<void> MainWidget::setup()
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& 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_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<void> 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<GUI::CheckBox>("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;

View file

@ -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 };
};
}

View file

@ -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"
}
}
}

View file

@ -42,6 +42,8 @@ ErrorOr<void> TerminalWrapper::run_command(ByteString const& command, Optional<B
m_pid = TRY(Core::System::fork());
if (m_pid > 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;

View file

@ -8,6 +8,7 @@
#pragma once
#include <AK/AnyOf.h>
#include <AK/DistinctNumeric.h>
#include <AK/Noncopyable.h>
#include <AK/Vector.h>
#include <LibVT/Attribute.h>
@ -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> 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; }
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<Cell> 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<u16> m_terminated_at;

View file

@ -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)

View file

@ -17,6 +17,7 @@
#ifndef KERNEL
# include <AK/ByteString.h>
# include <AK/HashTable.h>
# include <LibVT/Attribute.h>
# include <LibVT/Line.h>
#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<Mark> 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<ByteString> m_title_stack;
OrderedHashTable<Mark> m_valid_marks;
#endif
#ifndef KERNEL

View file

@ -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();
}
}
}

View file

@ -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<GUI::Action> m_copy_action;
RefPtr<GUI::Action> m_paste_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_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 };
};
}