diff --git a/userspace/programs/Terminal/CMakeLists.txt b/userspace/programs/Terminal/CMakeLists.txt index 4ddc6892..f27b9e0a 100644 --- a/userspace/programs/Terminal/CMakeLists.txt +++ b/userspace/programs/Terminal/CMakeLists.txt @@ -6,6 +6,7 @@ set(SOURCES add_executable(Terminal ${SOURCES}) banan_link_library(Terminal ban) banan_link_library(Terminal libc) +banan_link_library(Terminal libclipboard) banan_link_library(Terminal libfont) banan_link_library(Terminal libgui) banan_link_library(Terminal libinput) diff --git a/userspace/programs/Terminal/Terminal.cpp b/userspace/programs/Terminal/Terminal.cpp index 6647c638..de424d64 100644 --- a/userspace/programs/Terminal/Terminal.cpp +++ b/userspace/programs/Terminal/Terminal.cpp @@ -3,6 +3,8 @@ #include #include +#include + #include #include #include @@ -127,6 +129,15 @@ void Terminal::run() m_window->set_min_size(m_font.width() * 8, m_font.height() * 2); + MUST(m_cells.resize(rows() * cols(), { + .codepoint = 0, + .fg_color = m_fg_color, + .bg_color = m_bg_color, + .bold = m_is_bold + })); + m_cells_rows = rows(); + m_cells_cols = cols(); + { winsize winsize { .ws_row = static_cast(rows()), @@ -148,8 +159,31 @@ void Terminal::run() MUST(m_cursor_buffer.resize(m_font.width() * m_font.height(), m_bg_color)); show_cursor(); - m_window->set_key_event_callback([&](LibGUI::EventPacket::KeyEvent::event_t event) { on_key_event(event); }); m_window->set_resize_window_event_callback([&] { + if (cols() != m_cells_cols || rows() != m_cells_rows) + { + BAN::Vector new_cells; + MUST(new_cells.resize(rows() * cols(), { + .codepoint = 0, + .fg_color = m_fg_color, + .bg_color = m_bg_color, + .bold = m_is_bold, + })); + + for (size_t y = 0; y < BAN::Math::min(rows(), m_cells_rows); y++) + for (size_t x = 0; x < BAN::Math::min(cols(), m_cells_cols); x++) + new_cells[y * cols() + x] = m_cells[y * m_cells_cols + x]; + m_cells = BAN::move(new_cells); + m_cells_rows = rows(); + m_cells_cols = cols(); + + if (m_selection_s_col != UINT32_MAX) + { + m_selection_s_col = m_selection_s_row = UINT32_MAX; + update_selection(); + } + } + if (const auto rem = m_window->height() % m_font.height()) { m_window->texture().fill_rect(0, m_window->height() - rem, m_window->width(), rem, m_bg_color); @@ -183,6 +217,113 @@ void Terminal::run() } }); + m_window->set_key_event_callback([&](LibGUI::EventPacket::KeyEvent::event_t event) { + if (event.released()) + return; + + if (event.shift() && event.ctrl()) + { + switch (event.key) + { + case LibInput::Key::C: + { + if (m_selection_s_col == UINT32_MAX) + break; + + const uint32_t m_selection_start = m_selection_s_row * cols() + m_selection_s_col; + const uint32_t m_selection_end = m_selection_e_row * cols() + m_selection_e_col; + + uint32_t selection_min = BAN::Math::min(m_selection_start, m_selection_end); + for (; selection_min < m_cells.size(); selection_min++) + if (m_cells[selection_min].codepoint != 0) + break; + + uint32_t selection_max = BAN::Math::max(m_selection_start, m_selection_end); + for (; selection_max >= selection_min; selection_max--) + if (m_cells[selection_max].codepoint != 0) + break; + + BAN::String buffer; + for (size_t i = selection_min; i <= selection_max; i++) + { + if (i != selection_min && i % cols() == 0) + MUST(buffer.push_back('\n')); + + const auto cell = m_cells[i]; + if (cell.codepoint == 0) + continue; + + char bytes[5]; + BAN::UTF8::from_codepoints(&cell.codepoint, 1, bytes); + MUST(buffer.append(bytes)); + } + + if (auto ret = LibClipboard::Clipboard::set_clipboard_text(buffer); ret.is_error()) + dwarnln("Failed to set clipboard: {}", ret.error()); + + break; + } + case LibInput::Key::V: + { + auto clipboard_or_error = LibClipboard::Clipboard::get_clipboard_text(); + if (clipboard_or_error.is_error()) + { + dwarnln("Failed to get clipboard: {}", clipboard_or_error.error()); + break; + } + + auto clipboard = clipboard_or_error.release_value(); + if (!clipboard.empty()) + write(m_shell_info.pts_master, clipboard.data(), clipboard.size()); + m_got_key_event = true; + + break; + } + default: + break; + } + + return; + } + + if (const char* text = LibInput::key_to_utf8_ansi(event.key, event.modifier)) + write(m_shell_info.pts_master, text, strlen(text)); + m_got_key_event = true; + }); + + m_window->set_mouse_move_event_callback([&](LibGUI::EventPacket::MouseMoveEvent::event_t event) { + if (!m_selecting) + return; + + m_selection_e_col = BAN::Math::clamp(event.x / static_cast(m_font.width()), 0, cols() - 1); + m_selection_e_row = BAN::Math::clamp(event.y / static_cast(m_font.height()), 0, rows() - 1); + + update_selection(); + }); + + m_window->set_mouse_button_event_callback([&](LibGUI::EventPacket::MouseButtonEvent::event_t event) { + switch (event.button) + { + case LibInput::MouseButton::Right: + if (!m_selecting) + m_selection_s_col = m_selection_s_row = UINT32_MAX; + m_selecting = false; + break; + case LibInput::MouseButton::Left: + if (event.pressed) + { + m_selection_e_col = m_selection_s_col = BAN::Math::clamp(event.x / static_cast(m_font.width()), 0, cols() - 1); + m_selection_e_row = m_selection_s_row = BAN::Math::clamp(event.y / static_cast(m_font.height()), 0, rows() - 1); + } + m_selecting = event.pressed; + break; + default: + break; + } + + update_selection(); + }); + const int max_fd = BAN::Math::max(m_shell_info.pts_master, m_window->server_fd()); while (!s_shell_exited) { @@ -200,7 +341,11 @@ void Terminal::run() timeval timeout; timeout.tv_sec = ms_until_blink / 1'000; timeout.tv_usec = ms_until_blink * 1'000; - if (select(max_fd + 1, &fds, nullptr, nullptr, &timeout) == 0) + + const int nselect = select(max_fd + 1, &fds, nullptr, nullptr, &timeout); + if (nselect < 0) + continue; + if (nselect == 0) { m_cursor_blink_shown = !m_cursor_blink_shown; m_cursor_blink_ms = current_ms + ms_until_blink; @@ -223,6 +368,77 @@ void Terminal::run() } } +void Terminal::update_selection(bool show) +{ + static uint32_t old_selection_s_col = UINT32_MAX; + static uint32_t old_selection_s_row = UINT32_MAX; + static uint32_t old_selection_e_col = 0; + static uint32_t old_selection_e_row = 0; + + if (old_selection_s_col != UINT32_MAX) + { + old_selection_s_col = BAN::Math::min(old_selection_s_col, cols() - 1); + old_selection_s_row = BAN::Math::min(old_selection_s_row, rows() - 1); + old_selection_e_col = BAN::Math::min(old_selection_e_col, cols() - 1); + old_selection_e_row = BAN::Math::min(old_selection_e_row, rows() - 1); + } + + Rectangle invalidate {}; + + const auto redraw_character = + [this, &invalidate](size_t index, bool selected) + { + const uint32_t col = index % cols(); + const uint32_t row = index / cols(); + const uint32_t x = col * m_font.width(); + const uint32_t y = row * m_font.height(); + const auto cell = m_cells[row * cols() + col]; + if (selected && cell.codepoint == 0) + return; + m_window->texture().fill_rect(x, y, m_font.width(), m_font.height(), selected ? s_default_fg_color : cell.bg_color); + m_window->texture().draw_character(cell.codepoint, m_font, x, y, selected ? s_default_bg_color : cell.fg_color); + invalidate = invalidate.get_bounding_box({ x, y, m_font.width(), m_font.height() }); + }; + + if (old_selection_s_col != UINT32_MAX) + { + const uint32_t old_selection_start = old_selection_s_row * cols() + old_selection_s_col; + const uint32_t old_selection_end = old_selection_e_row * cols() + old_selection_e_col; + const auto old_selection_min = BAN::Math::min(old_selection_start, old_selection_end); + const auto old_selection_max = BAN::Math::max(old_selection_start, old_selection_end); + for (size_t i = old_selection_min; i <= old_selection_max; i++) + redraw_character(i, false); + } + + if (show && m_selection_s_col != UINT32_MAX) + { + const uint32_t new_selection_start = m_selection_s_row * cols() + m_selection_s_col; + const uint32_t new_selection_end = m_selection_e_row * cols() + m_selection_e_col; + const auto new_selection_min = BAN::Math::min(new_selection_start, new_selection_end); + const auto new_selection_max = BAN::Math::max(new_selection_start, new_selection_end); + for (size_t i = new_selection_min; i <= new_selection_max; i++) + redraw_character(i, true); + } + + if (invalidate.width && invalidate.height) + m_window->invalidate(invalidate.x, invalidate.y, invalidate.width, invalidate.height); + + if (show) + { + old_selection_s_col = m_selection_s_col; + old_selection_s_row = m_selection_s_row; + old_selection_e_col = m_selection_e_col; + old_selection_e_row = m_selection_e_row; + } + else + { + old_selection_s_col = UINT32_MAX; + old_selection_s_row = UINT32_MAX; + old_selection_e_col = 0; + old_selection_e_row = 0; + } +} + void Terminal::hide_cursor() { if (m_cursor.x == cols()) @@ -297,13 +513,7 @@ bool Terminal::read_shell() // do possible scrolling already in here, so `putchar()` doesnt // have to scroll up to `rows()` times if (m_cursor.y + newline_count >= rows()) - { - const uint32_t scroll = m_cursor.y + newline_count - rows() + 1; - m_cursor.y -= scroll; - m_window->texture().shift_vertical(-scroll * (int32_t)m_font.height()); - m_window->texture().fill_rect(0, m_window->height() - scroll * m_font.height(), m_window->width(), scroll * m_font.height(), m_bg_color); - should_invalidate = { 0, 0, m_window->width(), m_window->height() }; - } + should_invalidate = scroll(m_cursor.y + newline_count - rows() + 1); i = start; for (i = start; i < non_ansi_end; i++) @@ -471,6 +681,16 @@ Rectangle Terminal::handle_csi(char ch) if (m_csi_info.fields[0] == -1 || m_csi_info.fields[0] == 0) { + for (size_t i = m_cursor.y * cols() + m_cursor.x; i < m_cells.size(); i++) + { + m_cells[i] = { + .codepoint = 0, + .fg_color = m_fg_color, + .bg_color = m_bg_color, + .bold = m_is_bold, + }; + } + rects[0].x = m_cursor.x * m_font.width(); rects[0].y = m_cursor.y * m_font.height(); rects[0].width = m_window->width() - rects[0].x; @@ -485,6 +705,16 @@ Rectangle Terminal::handle_csi(char ch) } else if (m_csi_info.fields[0] == 1) { + for (size_t i = 0; i < m_cursor.y * cols() + m_cursor.x; i++) + { + m_cells[i] = { + .codepoint = 0, + .fg_color = m_fg_color, + .bg_color = m_bg_color, + .bold = m_is_bold, + }; + } + rects[0].x = 0; rects[0].y = m_cursor.y * m_font.height(); rects[0].width = m_cursor.x * m_font.width(); @@ -499,6 +729,16 @@ Rectangle Terminal::handle_csi(char ch) } else { + for (size_t i = 0; i < m_cells.size(); i++) + { + m_cells[i] = { + .codepoint = 0, + .fg_color = m_fg_color, + .bg_color = m_bg_color, + .bold = m_is_bold, + }; + } + rects[0].x = 0; rects[0].y = 0; rects[0].width = m_window->width(); @@ -519,6 +759,16 @@ Rectangle Terminal::handle_csi(char ch) { m_csi_info.fields[0] = BAN::Math::max(m_csi_info.fields[0], 0); + const uint32_t s_col = (m_csi_info.fields[0] == 0) ? m_cursor.x : 0; + const uint32_t e_col = (m_csi_info.fields[0] == 1) ? m_cursor.x : cols() - 1; + for (size_t i = s_col; i <= e_col; i++) + m_cells[m_cursor.y * cols() + i] = { + .codepoint = ' ', + .fg_color = m_fg_color, + .bg_color = m_bg_color, + .bold = m_is_bold, + }; + Rectangle rect; rect.x = (m_csi_info.fields[0] == 0) ? m_cursor.x * m_font.width() : 0; rect.y = m_cursor.y * m_font.height(); @@ -536,6 +786,18 @@ Rectangle Terminal::handle_csi(char ch) const uint32_t src_y = m_cursor.y * m_font.height(); const uint32_t dst_y = src_y + count * m_font.height(); + for (size_t i = m_cursor.y; i + count < rows(); i++) + for (size_t col = 0; col < cols(); col++) + m_cells[(i + count) * cols() + col] = m_cells[i * cols() + col]; + for (size_t row = m_cursor.y; row < m_cursor.y + count; row++) + for (size_t col = 0; col < cols(); col++) + m_cells[row * cols() + col] = { + .codepoint = ' ', + .fg_color = m_fg_color, + .bg_color = m_bg_color, + .bold = m_is_bold, + }; + texture.copy_horizontal_slice(dst_y, src_y, m_window->height() - dst_y); texture.fill_rect(0, src_y, m_window->width(), count * m_font.height(), m_bg_color); should_invalidate = { @@ -553,6 +815,18 @@ Rectangle Terminal::handle_csi(char ch) const uint32_t dst_y = m_cursor.y * m_font.height(); const uint32_t src_y = dst_y + count * m_font.height(); + for (size_t i = m_cursor.y; i + count < rows(); i++) + for (size_t col = 0; col < cols(); col++) + m_cells[i * cols() + col] = m_cells[(i + count) * cols() + col]; + for (size_t row = m_cursor.y + count; row < rows(); row++) + for (size_t col = 0; col < cols(); col++) + m_cells[row * cols() + col] = { + .codepoint = ' ', + .fg_color = m_fg_color, + .bg_color = m_bg_color, + .bold = m_is_bold, + }; + texture.copy_horizontal_slice(dst_y, src_y, m_window->height() - dst_y); texture.fill_rect(0, m_window->height() - count * m_font.height(), m_window->width(), count * m_font.height(), m_bg_color); should_invalidate = { @@ -571,6 +845,16 @@ Rectangle Terminal::handle_csi(char ch) const uint32_t src_x = (m_cursor.x + count) * m_font.width(); const uint32_t y = m_cursor.y * m_font.height(); + for (size_t i = m_cursor.x; i + count < cols(); i++) + m_cells[m_cursor.y * cols() + i] = m_cells[m_cursor.y * cols() + i + count]; + for (size_t i = m_cursor.x + count; i < cols(); i++) + m_cells[m_cursor.y * cols() + i] = { + .codepoint = ' ', + .fg_color = m_fg_color, + .bg_color = m_bg_color, + .bold = m_is_bold, + }; + texture.copy_rect(dst_x, y, src_x, y, m_window->width() - src_x, m_font.height()); texture.fill_rect(m_window->width() - count * m_font.width(), y, count * m_font.width(), m_font.height(), m_bg_color); should_invalidate = { @@ -589,6 +873,16 @@ Rectangle Terminal::handle_csi(char ch) const uint32_t src_x = m_cursor.x * m_font.width(); const uint32_t y = m_cursor.y * m_font.height(); + for (size_t i = m_cursor.x; i + count < cols(); i++) + m_cells[m_cursor.y * cols() + i + count] = m_cells[m_cursor.y * cols() + i]; + for (size_t i = m_cursor.x; i < m_cursor.x + count; i++) + m_cells[m_cursor.y * cols() + i] = { + .codepoint = ' ', + .fg_color = m_fg_color, + .bg_color = m_bg_color, + .bold = m_is_bold, + }; + texture.copy_rect(dst_x, y, src_x, y, m_window->width() - dst_x, m_font.height()); texture.fill_rect(src_x, y, count * m_font.width(), m_font.height(), m_bg_color); should_invalidate = { @@ -662,6 +956,61 @@ Rectangle Terminal::handle_csi(char ch) return should_invalidate; } +Rectangle Terminal::scroll(uint32_t scroll) +{ + const bool has_selection = (m_selection_s_col != UINT32_MAX); + + if (has_selection) + update_selection(false); + + for (size_t i = 0; i < rows() - scroll; i++) + for (size_t x = 0; x < cols(); x++) + m_cells[i * cols() + x] = m_cells[(i + scroll) * cols() + x]; + for (size_t i = 0; i < scroll; i++) + for (size_t x = 0; x < cols(); x++) + m_cells[(rows() - scroll + i) * cols() + x] = { + .codepoint = 0, + .fg_color = m_fg_color, + .bg_color = m_bg_color, + .bold = false, + }; + + auto& texture = m_window->texture(); + + m_cursor.y -= scroll; + texture.shift_vertical(-scroll * (int32_t)m_font.height()); + + const size_t y_off = (rows() - scroll) * m_font.height(); + texture.fill_rect(0, y_off, m_window->width(), m_window->height() - y_off, m_bg_color); + + if (has_selection) + { + if (m_selection_s_row < scroll && m_selection_e_row < scroll) + m_selection_s_col = m_selection_s_row = UINT32_MAX; + else + { + if (m_selection_s_row >= scroll) + m_selection_s_row -= scroll; + else + { + m_selection_s_row = 0; + m_selection_s_col = 0; + } + + if (m_selection_e_row >= scroll) + m_selection_e_row -= scroll; + else + { + m_selection_e_row = 0; + m_selection_e_col = 0; + } + } + update_selection(); + } + + return { 0, 0, m_window->width(), m_window->height() }; +} + Rectangle Terminal::putcodepoint(uint32_t codepoint) { Rectangle should_invalidate; @@ -714,15 +1063,7 @@ Rectangle Terminal::putcodepoint(uint32_t codepoint) } if (m_cursor.y >= rows()) - { - const uint32_t scroll = m_cursor.y - rows() + 1; - m_cursor.y -= scroll; - texture.shift_vertical(-scroll * (int32_t)m_font.height()); - - const size_t y_off = (rows() - scroll) * m_font.height(); - texture.fill_rect(0, y_off, m_window->width(), m_window->height() - y_off, m_bg_color); - should_invalidate = { 0, 0, m_window->width(), m_window->height() }; - } + should_invalidate = scroll(m_cursor.y - rows() + 1); const uint32_t cell_w = m_font.width(); const uint32_t cell_h = m_font.height(); @@ -732,6 +1073,13 @@ Rectangle Terminal::putcodepoint(uint32_t codepoint) const auto fg_color = m_colors_inverted ? m_bg_color : m_fg_color; const auto bg_color = m_colors_inverted ? m_fg_color : m_bg_color; + m_cells[m_cursor.y * cols() + m_cursor.x] = { + .codepoint = codepoint, + .fg_color = fg_color, + .bg_color = bg_color, + .bold = m_is_bold, + }; + texture.fill_rect(cell_x, cell_y, cell_w, cell_h, bg_color); texture.draw_character(codepoint, m_font, cell_x, cell_y, fg_color); if (m_is_bold) @@ -744,15 +1092,7 @@ Rectangle Terminal::putcodepoint(uint32_t codepoint) } if (m_cursor.y >= rows()) - { - const uint32_t scroll = m_cursor.y - rows() + 1; - m_cursor.y -= scroll; - texture.shift_vertical(-scroll * (int32_t)m_font.height()); - - const size_t y_off = (rows() - scroll) * m_font.height(); - texture.fill_rect(0, y_off, m_window->width(), m_window->height() - y_off, m_bg_color); - should_invalidate = { 0, 0, m_window->width(), m_window->height() }; - } + should_invalidate = scroll(m_cursor.y - rows() + 1); return should_invalidate; } @@ -822,12 +1162,3 @@ Rectangle Terminal::putchar(uint8_t ch) return putcodepoint(codepoint); } - -void Terminal::on_key_event(LibGUI::EventPacket::KeyEvent::event_t event) -{ - if (event.released()) - return; - if (const char* text = LibInput::key_to_utf8_ansi(event.key, event.modifier)) - write(m_shell_info.pts_master, text, strlen(text)); - m_got_key_event = true; -} diff --git a/userspace/programs/Terminal/Terminal.h b/userspace/programs/Terminal/Terminal.h index 853c58ca..f6b70e0f 100644 --- a/userspace/programs/Terminal/Terminal.h +++ b/userspace/programs/Terminal/Terminal.h @@ -36,11 +36,14 @@ public: private: void handle_sgr(int32_t value); + Rectangle scroll(uint32_t scroll); Rectangle handle_csi(char ch); Rectangle putcodepoint(uint32_t codepoint); Rectangle putchar(uint8_t ch); bool read_shell(); + void update_selection(bool show = true); + BAN::Optional get_8bit_color(); BAN::Optional get_24bit_color(); @@ -79,6 +82,14 @@ private: bool question; }; + struct Cell + { + uint32_t codepoint; + uint32_t fg_color; + uint32_t bg_color; + bool bold; + }; + private: BAN::UniqPtr m_window; LibFont::Font m_font; @@ -86,6 +97,16 @@ private: State m_state { State::Normal }; CSIInfo m_csi_info; + BAN::Vector m_cells; + uint32_t m_cells_rows { 0 }; + uint32_t m_cells_cols { 0 }; + + uint32_t m_selection_s_col { UINT32_MAX }; + uint32_t m_selection_s_row { UINT32_MAX }; + uint32_t m_selection_e_col { UINT32_MAX }; + uint32_t m_selection_e_row { UINT32_MAX }; + bool m_selecting { false }; + bool m_cursor_shown { true }; bool m_cursor_blink_shown { true }; uint64_t m_cursor_blink_ms { 0 };