Serenity Operating System
at master 1376 lines 49 kB view raw
1/* 2 * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org> 3 * Copyright (c) 2022, the SerenityOS developers. 4 * 5 * SPDX-License-Identifier: BSD-2-Clause 6 */ 7 8#include "TerminalWidget.h" 9#include <AK/DeprecatedString.h> 10#include <AK/LexicalPath.h> 11#include <AK/StdLibExtras.h> 12#include <AK/StringBuilder.h> 13#include <AK/TemporaryChange.h> 14#include <AK/Utf32View.h> 15#include <AK/Utf8View.h> 16#include <LibConfig/Client.h> 17#include <LibCore/ConfigFile.h> 18#include <LibCore/MimeData.h> 19#include <LibDesktop/AppFile.h> 20#include <LibDesktop/Launcher.h> 21#include <LibGUI/Action.h> 22#include <LibGUI/Application.h> 23#include <LibGUI/Clipboard.h> 24#include <LibGUI/DragOperation.h> 25#include <LibGUI/Icon.h> 26#include <LibGUI/Menu.h> 27#include <LibGUI/Painter.h> 28#include <LibGUI/Scrollbar.h> 29#include <LibGUI/Window.h> 30#include <LibGfx/Font/Font.h> 31#include <LibGfx/Font/FontDatabase.h> 32#include <LibGfx/Palette.h> 33#include <LibGfx/StylePainter.h> 34#include <ctype.h> 35#include <errno.h> 36#include <stdio.h> 37#include <string.h> 38#include <sys/ioctl.h> 39#include <unistd.h> 40 41namespace VT { 42 43void TerminalWidget::set_pty_master_fd(int fd) 44{ 45 m_ptm_fd = fd; 46 if (m_ptm_fd == -1) { 47 m_notifier = nullptr; 48 return; 49 } 50 m_notifier = Core::Notifier::construct(m_ptm_fd, Core::Notifier::Read); 51 m_notifier->on_ready_to_read = [this] { 52 u8 buffer[BUFSIZ]; 53 ssize_t nread = read(m_ptm_fd, buffer, sizeof(buffer)); 54 if (nread < 0) { 55 dbgln("Terminal read error: {}", strerror(errno)); 56 perror("read(ptm)"); 57 GUI::Application::the()->quit(1); 58 return; 59 } 60 if (nread == 0) { 61 dbgln("TerminalWidget: EOF on master pty, firing on_command_exit hook."); 62 if (on_command_exit) 63 on_command_exit(); 64 int rc = close(m_ptm_fd); 65 if (rc < 0) { 66 perror("close"); 67 } 68 set_pty_master_fd(-1); 69 return; 70 } 71 for (ssize_t i = 0; i < nread; ++i) 72 m_terminal.on_input(buffer[i]); 73 flush_dirty_lines(); 74 }; 75} 76 77TerminalWidget::TerminalWidget(int ptm_fd, bool automatic_size_policy) 78 : m_terminal(*this) 79 , m_automatic_size_policy(automatic_size_policy) 80{ 81 static_assert(sizeof(m_colors) == sizeof(xterm_colors)); 82 memcpy(m_colors, xterm_colors, sizeof(m_colors)); 83 84 set_override_cursor(Gfx::StandardCursor::IBeam); 85 set_focus_policy(GUI::FocusPolicy::StrongFocus); 86 set_pty_master_fd(ptm_fd); 87 88 on_emoji_input = [this](auto emoji) { 89 inject_string(emoji); 90 }; 91 92 m_cursor_blink_timer = add<Core::Timer>(); 93 m_visual_beep_timer = add<Core::Timer>(); 94 m_auto_scroll_timer = add<Core::Timer>(); 95 96 m_scrollbar = add<GUI::Scrollbar>(Orientation::Vertical); 97 m_scrollbar->set_scroll_animation(GUI::Scrollbar::Animation::CoarseScroll); 98 m_scrollbar->set_relative_rect(0, 0, 16, 0); 99 m_scrollbar->on_change = [this](int) { 100 update(); 101 }; 102 103 m_cursor_blink_timer->set_interval(Config::read_i32("Terminal"sv, "Text"sv, "CursorBlinkInterval"sv, 500)); 104 m_cursor_blink_timer->on_timeout = [this] { 105 m_cursor_blink_state = !m_cursor_blink_state; 106 update_cursor(); 107 }; 108 109 m_auto_scroll_timer->set_interval(50); 110 m_auto_scroll_timer->on_timeout = [this] { 111 if (m_auto_scroll_direction != AutoScrollDirection::None) { 112 int scroll_amount = m_auto_scroll_direction == AutoScrollDirection::Up ? -1 : 1; 113 m_scrollbar->increase_slider_by(scroll_amount); 114 } 115 }; 116 m_auto_scroll_timer->start(); 117 118 auto font_entry = Config::read_string("Terminal"sv, "Text"sv, "Font"sv, "default"sv); 119 if (font_entry == "default") 120 set_font(Gfx::FontDatabase::default_fixed_width_font()); 121 else 122 set_font(Gfx::FontDatabase::the().get_by_name(font_entry)); 123 124 update_cached_font_metrics(); 125 126 m_terminal.set_size(Config::read_i32("Terminal"sv, "Window"sv, "Width"sv, 80), Config::read_i32("Terminal"sv, "Window"sv, "Height"sv, 25)); 127 128 m_copy_action = GUI::Action::create("&Copy", { Mod_Ctrl | Mod_Shift, Key_C }, Gfx::Bitmap::load_from_file("/res/icons/16x16/edit-copy.png"sv).release_value_but_fixme_should_propagate_errors(), [this](auto&) { 129 copy(); 130 }); 131 m_copy_action->set_swallow_key_event_when_disabled(true); 132 133 m_paste_action = GUI::Action::create("&Paste", { Mod_Ctrl | Mod_Shift, Key_V }, Gfx::Bitmap::load_from_file("/res/icons/16x16/paste.png"sv).release_value_but_fixme_should_propagate_errors(), [this](auto&) { 134 paste(); 135 }); 136 m_paste_action->set_swallow_key_event_when_disabled(true); 137 138 m_clear_including_history_action = GUI::Action::create("Clear Including &History", { Mod_Ctrl | Mod_Shift, Key_K }, [this](auto&) { 139 clear_including_history(); 140 }); 141 142 m_context_menu = GUI::Menu::construct(); 143 m_context_menu->add_action(copy_action()); 144 m_context_menu->add_action(paste_action()); 145 m_context_menu->add_separator(); 146 m_context_menu->add_action(clear_including_history_action()); 147 148 update_copy_action(); 149 update_paste_action(); 150 update_color_scheme(); 151} 152 153Gfx::IntRect TerminalWidget::glyph_rect(u16 row, u16 column) 154{ 155 int y = row * m_line_height; 156 int x = column * m_column_width; 157 return { x + frame_thickness() + m_inset, y + frame_thickness() + m_inset, m_column_width, m_cell_height }; 158} 159 160Gfx::IntRect TerminalWidget::row_rect(u16 row) 161{ 162 int y = row * m_line_height; 163 Gfx::IntRect rect = { frame_thickness() + m_inset, y + frame_thickness() + m_inset, m_column_width * m_terminal.columns(), m_cell_height }; 164 rect.inflate(0, m_line_spacing); 165 return rect; 166} 167 168void TerminalWidget::set_logical_focus(bool focus) 169{ 170 m_has_logical_focus = focus; 171 if (!m_has_logical_focus) { 172 m_cursor_blink_timer->stop(); 173 m_cursor_blink_state = true; 174 } else if (m_cursor_is_blinking_set) { 175 m_cursor_blink_timer->stop(); 176 m_cursor_blink_state = true; 177 m_cursor_blink_timer->start(); 178 } 179 set_auto_scroll_direction(AutoScrollDirection::None); 180 invalidate_cursor(); 181 update(); 182} 183 184void TerminalWidget::focusin_event(GUI::FocusEvent& event) 185{ 186 set_logical_focus(true); 187 return GUI::Frame::focusin_event(event); 188} 189 190void TerminalWidget::focusout_event(GUI::FocusEvent& event) 191{ 192 set_logical_focus(false); 193 return GUI::Frame::focusout_event(event); 194} 195 196void TerminalWidget::event(Core::Event& event) 197{ 198 if (event.type() == GUI::Event::ThemeChange) 199 update_color_scheme(); 200 else if (event.type() == GUI::Event::WindowBecameActive) 201 set_logical_focus(true); 202 else if (event.type() == GUI::Event::WindowBecameInactive) 203 set_logical_focus(false); 204 return GUI::Frame::event(event); 205} 206 207void TerminalWidget::keydown_event(GUI::KeyEvent& event) 208{ 209 // We specifically need to process shortcuts before input to the Terminal is done 210 // since otherwise escape sequences will eat all our shortcuts for dinner. 211 window()->propagate_shortcuts_up_to_application(event, this); 212 if (event.is_accepted()) 213 return; 214 215 if (m_ptm_fd == -1) { 216 return GUI::Frame::keydown_event(event); 217 } 218 219 // Reset timer so cursor doesn't blink while typing. 220 if (m_cursor_is_blinking_set) { 221 m_cursor_blink_timer->stop(); 222 m_cursor_blink_state = true; 223 m_cursor_blink_timer->start(); 224 } 225 226 if (event.key() == KeyCode::Key_PageUp && event.modifiers() == Mod_Shift) { 227 m_scrollbar->decrease_slider_by(m_terminal.rows()); 228 return; 229 } 230 if (event.key() == KeyCode::Key_PageDown && event.modifiers() == Mod_Shift) { 231 m_scrollbar->increase_slider_by(m_terminal.rows()); 232 return; 233 } 234 if (event.key() == KeyCode::Key_Alt) { 235 m_alt_key_held = true; 236 return; 237 } 238 239 // Clear the selection if we type in/behind it. 240 auto future_cursor_column = (event.key() == KeyCode::Key_Backspace) ? m_terminal.cursor_column() - 1 : m_terminal.cursor_column(); 241 auto min_selection_row = min(m_selection.start().row(), m_selection.end().row()); 242 auto max_selection_row = max(m_selection.start().row(), m_selection.end().row()); 243 244 if (future_cursor_column <= last_selection_column_on_row(m_terminal.cursor_row()) && m_terminal.cursor_row() >= min_selection_row && m_terminal.cursor_row() <= max_selection_row) { 245 m_selection.set_end({}); 246 update_copy_action(); 247 update(); 248 } 249 250 m_terminal.handle_key_press(event.key(), event.code_point(), event.modifiers()); 251 252 if (event.key() != Key_Control && event.key() != Key_Alt && event.key() != Key_LeftShift && event.key() != Key_RightShift && event.key() != Key_Super) 253 scroll_to_bottom(); 254} 255 256void TerminalWidget::keyup_event(GUI::KeyEvent& event) 257{ 258 switch (event.key()) { 259 case KeyCode::Key_Alt: 260 m_alt_key_held = false; 261 return; 262 default: 263 break; 264 } 265} 266 267void TerminalWidget::paint_event(GUI::PaintEvent& event) 268{ 269 GUI::Frame::paint_event(event); 270 271 GUI::Painter painter(*this); 272 273 auto visual_beep_active = m_visual_beep_timer->is_active(); 274 275 painter.add_clip_rect(event.rect()); 276 277 if (visual_beep_active) 278 painter.clear_rect(frame_inner_rect(), terminal_color_to_rgb(VT::Color::named(VT::Color::ANSIColor::Red))); 279 else 280 painter.clear_rect(frame_inner_rect(), terminal_color_to_rgb(VT::Color::named(VT::Color::ANSIColor::DefaultBackground)).with_alpha(m_opacity)); 281 invalidate_cursor(); 282 283 int rows_from_history = 0; 284 int first_row_from_history = m_terminal.history_size(); 285 int row_with_cursor = m_terminal.cursor_row(); 286 if (m_scrollbar->value() != m_scrollbar->max()) { 287 rows_from_history = min((int)m_terminal.rows(), m_scrollbar->max() - m_scrollbar->value()); 288 first_row_from_history = m_terminal.history_size() - (m_scrollbar->max() - m_scrollbar->value()); 289 row_with_cursor = m_terminal.cursor_row() + rows_from_history; 290 } 291 292 // Pass: Compute the rect(s) of the currently hovered link, if any. 293 Vector<Gfx::IntRect> hovered_href_rects; 294 if (!m_hovered_href_id.is_null()) { 295 for (u16 visual_row = 0; visual_row < m_terminal.rows(); ++visual_row) { 296 auto& line = m_terminal.line(first_row_from_history + visual_row); 297 for (size_t column = 0; column < line.length(); ++column) { 298 if (m_hovered_href_id == line.attribute_at(column).href_id) { 299 bool merged_with_existing_rect = false; 300 auto glyph_rect = this->glyph_rect(visual_row, column); 301 for (auto& rect : hovered_href_rects) { 302 if (rect.inflated(1, 1).intersects(glyph_rect)) { 303 rect = rect.united(glyph_rect); 304 merged_with_existing_rect = true; 305 break; 306 } 307 } 308 if (!merged_with_existing_rect) 309 hovered_href_rects.append(glyph_rect); 310 } 311 } 312 } 313 } 314 315 // Pass: Paint background & text decorations. 316 for (u16 visual_row = 0; visual_row < m_terminal.rows(); ++visual_row) { 317 auto row_rect = this->row_rect(visual_row); 318 if (!event.rect().contains(row_rect)) 319 continue; 320 auto& line = m_terminal.line(first_row_from_history + visual_row); 321 bool has_only_one_background_color = line.has_only_one_background_color(); 322 if (visual_beep_active) 323 painter.clear_rect(row_rect, terminal_color_to_rgb(VT::Color::named(VT::Color::ANSIColor::Red))); 324 else if (has_only_one_background_color) 325 painter.clear_rect(row_rect, terminal_color_to_rgb(line.attribute_at(0).effective_background_color()).with_alpha(m_opacity)); 326 327 for (size_t column = 0; column < line.length(); ++column) { 328 bool should_reverse_fill_for_cursor_or_selection = m_cursor_blink_state 329 && m_cursor_shape == VT::CursorShape::Block 330 && m_has_logical_focus 331 && visual_row == row_with_cursor 332 && column == m_terminal.cursor_column(); 333 should_reverse_fill_for_cursor_or_selection |= selection_contains({ first_row_from_history + visual_row, (int)column }); 334 auto attribute = line.attribute_at(column); 335 auto character_rect = glyph_rect(visual_row, column); 336 auto cell_rect = character_rect.inflated(0, m_line_spacing); 337 auto text_color_before_bold_change = should_reverse_fill_for_cursor_or_selection ? attribute.effective_background_color() : attribute.effective_foreground_color(); 338 auto text_color = terminal_color_to_rgb(m_show_bold_text_as_bright ? text_color_before_bold_change.to_bright() : text_color_before_bold_change); 339 if ((!visual_beep_active && !has_only_one_background_color) || should_reverse_fill_for_cursor_or_selection) 340 painter.clear_rect(cell_rect, terminal_color_to_rgb(should_reverse_fill_for_cursor_or_selection ? attribute.effective_foreground_color() : attribute.effective_background_color())); 341 342 if constexpr (TERMINAL_DEBUG) { 343 if (line.termination_column() == column) 344 painter.clear_rect(cell_rect, Gfx::Color::Magenta); 345 } 346 347 enum class UnderlineStyle { 348 None, 349 Dotted, 350 Solid, 351 }; 352 353 auto underline_style = UnderlineStyle::None; 354 auto underline_color = text_color; 355 356 if (has_flag(attribute.flags, VT::Attribute::Flags::Underline)) { 357 // Content has specified underline 358 underline_style = UnderlineStyle::Solid; 359 } else if (!attribute.href.is_empty()) { 360 // We're hovering a hyperlink 361 if (m_hovered_href_id == attribute.href_id || m_active_href_id == attribute.href_id) { 362 underline_style = UnderlineStyle::Solid; 363 underline_color = palette().active_link(); 364 } else { 365 underline_style = UnderlineStyle::Dotted; 366 underline_color = text_color.darkened(0.6f); 367 } 368 } 369 370 if (underline_style == UnderlineStyle::Solid) { 371 painter.draw_line(cell_rect.bottom_left(), cell_rect.bottom_right(), underline_color); 372 } else if (underline_style == UnderlineStyle::Dotted) { 373 int x1 = cell_rect.bottom_left().x(); 374 int x2 = cell_rect.bottom_right().x(); 375 int y = cell_rect.bottom_left().y(); 376 for (int x = x1; x <= x2; ++x) { 377 if ((x % 3) == 0) 378 painter.set_pixel({ x, y }, underline_color); 379 } 380 } 381 } 382 } 383 384 // Paint the hovered link rects, if any. 385 for (auto rect : hovered_href_rects) { 386 rect.inflate(6, 6); 387 painter.fill_rect(rect, palette().base()); 388 painter.draw_rect(rect.inflated(2, 2).intersected(frame_inner_rect()), palette().base_text()); 389 } 390 391 auto& font = this->font(); 392 auto& bold_font = font.bold_variant(); 393 394 // Pass: Paint foreground (text). 395 for (u16 visual_row = 0; visual_row < m_terminal.rows(); ++visual_row) { 396 auto row_rect = this->row_rect(visual_row); 397 if (!event.rect().contains(row_rect)) 398 continue; 399 auto& line = m_terminal.line(first_row_from_history + visual_row); 400 for (size_t column = 0; column < line.length(); ++column) { 401 auto attribute = line.attribute_at(column); 402 bool should_reverse_fill_for_cursor_or_selection = m_cursor_blink_state 403 && m_cursor_shape == VT::CursorShape::Block 404 && m_has_logical_focus 405 && visual_row == row_with_cursor 406 && column == m_terminal.cursor_column(); 407 should_reverse_fill_for_cursor_or_selection |= selection_contains({ first_row_from_history + visual_row, (int)column }); 408 auto text_color_before_bold_change = should_reverse_fill_for_cursor_or_selection ? attribute.effective_background_color() : attribute.effective_foreground_color(); 409 auto text_color = terminal_color_to_rgb(m_show_bold_text_as_bright ? text_color_before_bold_change.to_bright() : text_color_before_bold_change); 410 u32 code_point = line.code_point(column); 411 412 if (code_point == ' ') 413 continue; 414 415 auto character_rect = glyph_rect(visual_row, column); 416 417 if (!m_hovered_href_id.is_null() && attribute.href_id == m_hovered_href_id) { 418 text_color = palette().base_text(); 419 } 420 421 painter.draw_glyph_or_emoji( 422 character_rect.location(), 423 code_point, 424 has_flag(attribute.flags, VT::Attribute::Flags::Bold) ? bold_font : font, 425 text_color); 426 } 427 } 428 429 // Draw cursor. 430 if (m_cursor_blink_state && row_with_cursor < m_terminal.rows()) { 431 auto& cursor_line = m_terminal.line(first_row_from_history + row_with_cursor); 432 if (m_terminal.cursor_row() >= (m_terminal.rows() - rows_from_history)) 433 return; 434 435 if (m_has_logical_focus && m_cursor_shape == VT::CursorShape::Block) 436 return; // This has already been handled by inverting the cell colors 437 438 auto cursor_color = terminal_color_to_rgb(cursor_line.attribute_at(m_terminal.cursor_column()).effective_foreground_color()); 439 auto cell_rect = glyph_rect(row_with_cursor, m_terminal.cursor_column()).inflated(0, m_line_spacing); 440 if (m_cursor_shape == VT::CursorShape::Underline) { 441 auto x1 = cell_rect.bottom_left().x(); 442 auto x2 = cell_rect.bottom_right().x(); 443 auto y = cell_rect.bottom_left().y(); 444 for (auto x = x1; x <= x2; ++x) 445 painter.set_pixel({ x, y }, cursor_color); 446 } else if (m_cursor_shape == VT::CursorShape::Bar) { 447 auto x = cell_rect.bottom_left().x(); 448 auto y1 = cell_rect.top_left().y(); 449 auto y2 = cell_rect.bottom_left().y(); 450 for (auto y = y1; y <= y2; ++y) 451 painter.set_pixel({ x, y }, cursor_color); 452 } else { 453 // We fall back to a block if we don't support the selected cursor type. 454 painter.draw_rect(cell_rect, cursor_color); 455 } 456 } 457} 458 459void TerminalWidget::set_window_progress(int value, int max) 460{ 461 float float_value = value; 462 float float_max = max; 463 float progress = (float_value / float_max) * 100.0f; 464 window()->set_progress((int)roundf(progress)); 465} 466 467void TerminalWidget::set_window_title(StringView title) 468{ 469 if (!Utf8View(title).validate()) { 470 dbgln("TerminalWidget: Attempted to set window title to invalid UTF-8 string"); 471 return; 472 } 473 474 if (on_title_change) 475 on_title_change(title); 476} 477 478void TerminalWidget::invalidate_cursor() 479{ 480 m_terminal.invalidate_cursor(); 481} 482 483void TerminalWidget::flush_dirty_lines() 484{ 485 // FIXME: Update smarter when scrolled 486 if (m_terminal.m_need_full_flush || m_scrollbar->value() != m_scrollbar->max()) { 487 update(); 488 m_terminal.m_need_full_flush = false; 489 return; 490 } 491 Gfx::IntRect rect; 492 for (int i = 0; i < m_terminal.rows(); ++i) { 493 if (m_terminal.visible_line(i).is_dirty()) { 494 rect = rect.united(row_rect(i)); 495 m_terminal.visible_line(i).set_dirty(false); 496 } 497 } 498 update(rect); 499} 500 501void TerminalWidget::resize_event(GUI::ResizeEvent& event) 502{ 503 relayout(event.size()); 504} 505 506void TerminalWidget::relayout(Gfx::IntSize size) 507{ 508 if (!m_scrollbar) 509 return; 510 511 TemporaryChange change(m_in_relayout, true); 512 513 auto base_size = compute_base_size(); 514 int new_columns = (size.width() - base_size.width()) / m_column_width; 515 int new_rows = (size.height() - base_size.height()) / m_line_height; 516 m_terminal.set_size(new_columns, new_rows); 517 518 Gfx::IntRect scrollbar_rect = { 519 size.width() - m_scrollbar->width() - frame_thickness(), 520 frame_thickness(), 521 m_scrollbar->width(), 522 size.height() - frame_thickness() * 2, 523 }; 524 m_scrollbar->set_relative_rect(scrollbar_rect); 525 m_scrollbar->set_page_step(new_rows); 526} 527 528Gfx::IntSize TerminalWidget::compute_base_size() const 529{ 530 int base_width = frame_thickness() * 2 + m_inset * 2 + m_scrollbar->width(); 531 int base_height = frame_thickness() * 2 + m_inset * 2; 532 return { base_width, base_height }; 533} 534 535void TerminalWidget::apply_size_increments_to_window(GUI::Window& window) 536{ 537 window.set_size_increment({ m_column_width, m_line_height }); 538 window.set_base_size(compute_base_size()); 539} 540 541void TerminalWidget::update_cursor() 542{ 543 invalidate_cursor(); 544 flush_dirty_lines(); 545} 546 547void TerminalWidget::set_opacity(u8 new_opacity) 548{ 549 if (m_opacity == new_opacity) 550 return; 551 552 window()->set_has_alpha_channel(new_opacity < 255); 553 m_opacity = new_opacity; 554 update(); 555} 556 557void TerminalWidget::set_show_scrollbar(bool show_scrollbar) 558{ 559 m_scrollbar->set_visible(show_scrollbar); 560} 561 562bool TerminalWidget::has_selection() const 563{ 564 return m_selection.is_valid(); 565} 566 567void TerminalWidget::set_selection(const VT::Range& selection) 568{ 569 m_selection = selection; 570 update_copy_action(); 571 update(); 572} 573 574bool TerminalWidget::selection_contains(const VT::Position& position) const 575{ 576 if (!has_selection()) 577 return false; 578 579 if (m_rectangle_selection) { 580 auto m_selection_start = m_selection.start(); 581 auto m_selection_end = m_selection.end(); 582 auto min_selection_column = min(m_selection_start.column(), m_selection_end.column()); 583 auto max_selection_column = max(m_selection_start.column(), m_selection_end.column()); 584 auto min_selection_row = min(m_selection_start.row(), m_selection_end.row()); 585 auto max_selection_row = max(m_selection_start.row(), m_selection_end.row()); 586 587 return position.column() >= min_selection_column && position.column() <= max_selection_column && position.row() >= min_selection_row && position.row() <= max_selection_row; 588 } 589 590 auto normalized_selection = m_selection.normalized(); 591 return position >= normalized_selection.start() && position <= normalized_selection.end(); 592} 593 594VT::Position TerminalWidget::buffer_position_at(Gfx::IntPoint position) const 595{ 596 auto adjusted_position = position.translated(-(frame_thickness() + m_inset), -(frame_thickness() + m_inset)); 597 int row = adjusted_position.y() / m_line_height; 598 int column = adjusted_position.x() / m_column_width; 599 if (row < 0) 600 row = 0; 601 if (column < 0) 602 column = 0; 603 if (row >= m_terminal.rows()) 604 row = m_terminal.rows() - 1; 605 auto& line = m_terminal.line(row); 606 if (column >= (int)line.length()) 607 column = line.length() - 1; 608 row += m_scrollbar->value(); 609 return { row, column }; 610} 611 612u32 TerminalWidget::code_point_at(const VT::Position& position) const 613{ 614 VERIFY(position.is_valid()); 615 VERIFY(position.row() >= 0 && static_cast<size_t>(position.row()) < m_terminal.line_count()); 616 auto& line = m_terminal.line(position.row()); 617 if (static_cast<size_t>(position.column()) == line.length()) 618 return '\n'; 619 return line.code_point(position.column()); 620} 621 622VT::Position TerminalWidget::next_position_after(const VT::Position& position, bool should_wrap) const 623{ 624 VERIFY(position.is_valid()); 625 VERIFY(position.row() >= 0 && static_cast<size_t>(position.row()) < m_terminal.line_count()); 626 auto& line = m_terminal.line(position.row()); 627 if (static_cast<size_t>(position.column()) == line.length()) { 628 if (static_cast<size_t>(position.row()) == m_terminal.line_count() - 1) { 629 if (should_wrap) 630 return { 0, 0 }; 631 return {}; 632 } 633 return { position.row() + 1, 0 }; 634 } 635 return { position.row(), position.column() + 1 }; 636} 637 638VT::Position TerminalWidget::previous_position_before(const VT::Position& position, bool should_wrap) const 639{ 640 VERIFY(position.row() >= 0 && static_cast<size_t>(position.row()) < m_terminal.line_count()); 641 if (position.column() == 0) { 642 if (position.row() == 0) { 643 if (should_wrap) { 644 auto& last_line = m_terminal.line(m_terminal.line_count() - 1); 645 return { static_cast<int>(m_terminal.line_count() - 1), static_cast<int>(last_line.length()) }; 646 } 647 return {}; 648 } 649 auto& prev_line = m_terminal.line(position.row() - 1); 650 return { position.row() - 1, static_cast<int>(prev_line.length()) }; 651 } 652 return { position.row(), position.column() - 1 }; 653} 654 655static u32 to_lowercase_code_point(u32 code_point) 656{ 657 // FIXME: this only handles ascii characters, but handling unicode lowercasing seems like a mess 658 if (code_point < 128) 659 return tolower(code_point); 660 return code_point; 661} 662 663VT::Range TerminalWidget::find_next(StringView needle, const VT::Position& start, bool case_sensitivity, bool should_wrap) 664{ 665 if (needle.is_empty()) 666 return {}; 667 668 VT::Position position = start.is_valid() ? start : VT::Position(0, 0); 669 VT::Position original_position = position; 670 671 VT::Position start_of_potential_match; 672 size_t needle_index = 0; 673 674 Utf8View unicode_needle(needle); 675 Vector<u32> needle_code_points; 676 for (u32 code_point : unicode_needle) 677 needle_code_points.append(code_point); 678 679 do { 680 auto ch = code_point_at(position); 681 682 bool code_point_matches = false; 683 if (needle_index >= needle_code_points.size()) 684 code_point_matches = false; 685 else if (case_sensitivity) 686 code_point_matches = ch == needle_code_points[needle_index]; 687 else 688 code_point_matches = to_lowercase_code_point(ch) == to_lowercase_code_point(needle_code_points[needle_index]); 689 690 if (code_point_matches) { 691 if (needle_index == 0) 692 start_of_potential_match = position; 693 ++needle_index; 694 if (needle_index >= needle_code_points.size()) 695 return { start_of_potential_match, position }; 696 } else { 697 if (needle_index > 0) 698 position = start_of_potential_match; 699 needle_index = 0; 700 } 701 position = next_position_after(position, should_wrap); 702 } while (position.is_valid() && position != original_position); 703 704 return {}; 705} 706 707VT::Range TerminalWidget::find_previous(StringView needle, const VT::Position& start, bool case_sensitivity, bool should_wrap) 708{ 709 if (needle.is_empty()) 710 return {}; 711 712 VT::Position position = start.is_valid() ? start : VT::Position(m_terminal.line_count() - 1, m_terminal.line(m_terminal.line_count() - 1).length() - 1); 713 VT::Position original_position = position; 714 715 Utf8View unicode_needle(needle); 716 Vector<u32> needle_code_points; 717 for (u32 code_point : unicode_needle) 718 needle_code_points.append(code_point); 719 720 VT::Position end_of_potential_match; 721 size_t needle_index = needle_code_points.size() - 1; 722 723 do { 724 auto ch = code_point_at(position); 725 726 bool code_point_matches = false; 727 if (needle_index >= needle_code_points.size()) 728 code_point_matches = false; 729 else if (case_sensitivity) 730 code_point_matches = ch == needle_code_points[needle_index]; 731 else 732 code_point_matches = to_lowercase_code_point(ch) == to_lowercase_code_point(needle_code_points[needle_index]); 733 734 if (code_point_matches) { 735 if (needle_index == needle_code_points.size() - 1) 736 end_of_potential_match = position; 737 if (needle_index == 0) 738 return { position, end_of_potential_match }; 739 --needle_index; 740 } else { 741 if (needle_index < needle_code_points.size() - 1) 742 position = end_of_potential_match; 743 needle_index = needle_code_points.size() - 1; 744 } 745 position = previous_position_before(position, should_wrap); 746 } while (position.is_valid() && position != original_position); 747 748 return {}; 749} 750 751void TerminalWidget::doubleclick_event(GUI::MouseEvent& event) 752{ 753 if (event.button() == GUI::MouseButton::Primary) { 754 auto attribute = m_terminal.attribute_at(buffer_position_at(event.position())); 755 if (!attribute.href_id.is_null()) { 756 dbgln("Open hyperlinked URL: '{}'", attribute.href); 757 Desktop::Launcher::open(attribute.href); 758 return; 759 } 760 761 m_triple_click_timer.start(); 762 763 auto position = buffer_position_at(event.position()); 764 auto& line = m_terminal.line(position.row()); 765 bool want_whitespace = line.code_point(position.column()) == ' '; 766 767 int start_column = 0; 768 int end_column = 0; 769 770 for (int column = position.column(); column >= 0 && (line.code_point(column) == ' ') == want_whitespace; --column) { 771 start_column = column; 772 } 773 774 for (int column = position.column(); column < (int)line.length() && (line.code_point(column) == ' ') == want_whitespace; ++column) { 775 end_column = column; 776 } 777 778 m_selection.set({ position.row(), start_column }, { position.row(), end_column }); 779 update_copy_action(); 780 update(); 781 } 782 GUI::Frame::doubleclick_event(event); 783} 784 785void TerminalWidget::paste() 786{ 787 if (m_ptm_fd == -1) 788 return; 789 790 auto [data, mime_type, _] = GUI::Clipboard::the().fetch_data_and_type(); 791 if (!mime_type.starts_with("text/"sv)) 792 return; 793 if (data.is_empty()) 794 return; 795 send_non_user_input(data); 796} 797 798void TerminalWidget::copy() 799{ 800 if (has_selection()) 801 GUI::Clipboard::the().set_plain_text(selected_text()); 802} 803 804void TerminalWidget::mouseup_event(GUI::MouseEvent& event) 805{ 806 if (event.button() == GUI::MouseButton::Primary) { 807 if (!m_active_href_id.is_null()) { 808 m_active_href = {}; 809 m_active_href_id = {}; 810 update(); 811 } 812 813 if (m_triple_click_timer.is_valid()) 814 m_triple_click_timer.reset(); 815 816 set_auto_scroll_direction(AutoScrollDirection::None); 817 } 818} 819 820void TerminalWidget::mousedown_event(GUI::MouseEvent& event) 821{ 822 using namespace AK::TimeLiterals; 823 824 if (event.button() == GUI::MouseButton::Primary) { 825 m_left_mousedown_position = event.position(); 826 m_left_mousedown_position_buffer = buffer_position_at(m_left_mousedown_position); 827 828 auto attribute = m_terminal.attribute_at(m_left_mousedown_position_buffer); 829 if (!(event.modifiers() & Mod_Shift) && !attribute.href.is_empty()) { 830 m_active_href = attribute.href; 831 m_active_href_id = attribute.href_id; 832 update(); 833 return; 834 } 835 m_active_href = {}; 836 m_active_href_id = {}; 837 838 if (m_triple_click_timer.is_valid() && m_triple_click_timer.elapsed_time() < 250_ms) { 839 int start_column = 0; 840 int end_column = m_terminal.columns() - 1; 841 842 m_selection.set({ m_left_mousedown_position_buffer.row(), start_column }, { m_left_mousedown_position_buffer.row(), end_column }); 843 } else { 844 m_selection.set(m_left_mousedown_position_buffer, {}); 845 } 846 if (m_alt_key_held) 847 m_rectangle_selection = true; 848 else if (m_rectangle_selection) 849 m_rectangle_selection = false; 850 851 update_copy_action(); 852 update(); 853 } 854} 855 856void TerminalWidget::mousemove_event(GUI::MouseEvent& event) 857{ 858 auto position = buffer_position_at(event.position()); 859 860 auto attribute = m_terminal.attribute_at(position); 861 862 if (attribute.href_id != m_hovered_href_id) { 863 if (!attribute.href_id.is_null()) { 864 m_hovered_href_id = attribute.href_id; 865 m_hovered_href = attribute.href; 866 867 auto handlers = Desktop::Launcher::get_handlers_for_url(attribute.href); 868 if (!handlers.is_empty()) { 869 auto url = URL(attribute.href); 870 auto path = url.path(); 871 872 auto app_file = Desktop::AppFile::get_for_app(LexicalPath::basename(handlers[0])); 873 auto app_name = app_file->is_valid() ? app_file->name() : LexicalPath::basename(handlers[0]); 874 875 if (url.scheme() == "file") { 876 auto file_name = LexicalPath::basename(path); 877 878 if (path == handlers[0]) { 879 set_tooltip(DeprecatedString::formatted("Execute {}", app_name)); 880 } else { 881 set_tooltip(DeprecatedString::formatted("Open {} with {}", file_name, app_name)); 882 } 883 } else { 884 set_tooltip(DeprecatedString::formatted("Open {} with {}", attribute.href, app_name)); 885 } 886 } 887 } else { 888 m_hovered_href_id = {}; 889 m_hovered_href = {}; 890 set_tooltip({}); 891 } 892 show_or_hide_tooltip(); 893 if (!m_hovered_href.is_empty()) 894 set_override_cursor(Gfx::StandardCursor::Arrow); 895 else 896 set_override_cursor(Gfx::StandardCursor::IBeam); 897 update(); 898 } 899 900 if (!(event.buttons() & GUI::MouseButton::Primary)) 901 return; 902 903 if (!m_active_href_id.is_null()) { 904 auto diff = event.position() - m_left_mousedown_position; 905 auto distance_travelled_squared = diff.x() * diff.x() + diff.y() * diff.y(); 906 constexpr int drag_distance_threshold = 5; 907 908 if (distance_travelled_squared <= drag_distance_threshold) 909 return; 910 911 auto drag_operation = GUI::DragOperation::construct(); 912 drag_operation->set_text(m_active_href); 913 drag_operation->set_data("text/uri-list", m_active_href); 914 915 m_active_href = {}; 916 m_active_href_id = {}; 917 m_hovered_href = {}; 918 m_hovered_href_id = {}; 919 drag_operation->exec(); 920 update(); 921 return; 922 } 923 924 auto adjusted_position = event.position().translated(-(frame_thickness() + m_inset), -(frame_thickness() + m_inset)); 925 if (adjusted_position.y() < 0) 926 set_auto_scroll_direction(AutoScrollDirection::Up); 927 else if (adjusted_position.y() > m_terminal.rows() * m_line_height) 928 set_auto_scroll_direction(AutoScrollDirection::Down); 929 else 930 set_auto_scroll_direction(AutoScrollDirection::None); 931 932 VT::Position old_selection_end = m_selection.end(); 933 VT::Position old_selection_start = m_selection.start(); 934 935 if (m_triple_click_timer.is_valid()) { 936 int start_column = 0; 937 int end_column = m_terminal.columns() - 1; 938 939 if (position.row() < m_left_mousedown_position_buffer.row()) 940 m_selection.set({ position.row(), start_column }, { m_left_mousedown_position_buffer.row(), end_column }); 941 else 942 m_selection.set({ m_left_mousedown_position_buffer.row(), start_column }, { position.row(), end_column }); 943 } else { 944 m_selection.set_end(position); 945 } 946 947 if (old_selection_end != m_selection.end() || old_selection_start != m_selection.start()) { 948 update_copy_action(); 949 update(); 950 } 951} 952 953void TerminalWidget::leave_event(Core::Event&) 954{ 955 bool should_update = !m_hovered_href.is_empty(); 956 m_hovered_href = {}; 957 m_hovered_href_id = {}; 958 set_tooltip(m_hovered_href); 959 set_override_cursor(Gfx::StandardCursor::IBeam); 960 if (should_update) 961 update(); 962} 963 964void TerminalWidget::mousewheel_event(GUI::MouseEvent& event) 965{ 966 if (!is_scrollable()) 967 return; 968 set_auto_scroll_direction(AutoScrollDirection::None); 969 m_scrollbar->increase_slider_by(event.wheel_delta_y() * scroll_length()); 970 GUI::Frame::mousewheel_event(event); 971} 972 973bool TerminalWidget::is_scrollable() const 974{ 975 return m_scrollbar->is_scrollable(); 976} 977 978int TerminalWidget::scroll_length() const 979{ 980 return m_scrollbar->step(); 981} 982 983DeprecatedString TerminalWidget::selected_text() const 984{ 985 StringBuilder builder; 986 987 auto normalized_selection = m_selection.normalized(); 988 auto start = normalized_selection.start(); 989 auto end = normalized_selection.end(); 990 991 for (int row = start.row(); row <= end.row(); ++row) { 992 int first_column = first_selection_column_on_row(row); 993 int last_column = last_selection_column_on_row(row); 994 for (int column = first_column; column <= last_column; ++column) { 995 auto& line = m_terminal.line(row); 996 if (line.attribute_at(column).is_untouched()) { 997 builder.append('\n'); 998 break; 999 } 1000 // FIXME: This is a bit hackish. 1001 u32 code_point = line.code_point(column); 1002 builder.append(Utf32View(&code_point, 1)); 1003 if (column == static_cast<int>(line.length()) - 1 || (m_rectangle_selection && column == last_column)) { 1004 builder.append('\n'); 1005 } 1006 } 1007 } 1008 1009 return builder.to_deprecated_string(); 1010} 1011 1012int TerminalWidget::first_selection_column_on_row(int row) const 1013{ 1014 auto normalized_selection_start = m_selection.normalized().start(); 1015 return row == normalized_selection_start.row() || m_rectangle_selection ? normalized_selection_start.column() : 0; 1016} 1017 1018int TerminalWidget::last_selection_column_on_row(int row) const 1019{ 1020 auto normalized_selection_end = m_selection.normalized().end(); 1021 return row == normalized_selection_end.row() || m_rectangle_selection ? normalized_selection_end.column() : m_terminal.columns() - 1; 1022} 1023 1024void TerminalWidget::terminal_history_changed(int delta) 1025{ 1026 bool was_max = m_scrollbar->value() == m_scrollbar->max(); 1027 m_scrollbar->set_max(m_terminal.history_size()); 1028 if (was_max) 1029 m_scrollbar->set_value(m_scrollbar->max()); 1030 m_scrollbar->update(); 1031 // If the history buffer wrapped around, the selection needs to be offset accordingly. 1032 if (m_selection.is_valid() && delta < 0) 1033 m_selection.offset_row(delta); 1034} 1035 1036void TerminalWidget::terminal_did_resize(u16 columns, u16 rows) 1037{ 1038 auto pixel_size = widget_size_for_font(font()); 1039 m_pixel_width = pixel_size.width(); 1040 m_pixel_height = pixel_size.height(); 1041 1042 if (!m_in_relayout) { 1043 if (on_terminal_size_change) 1044 on_terminal_size_change(Gfx::IntSize { m_pixel_width, m_pixel_height }); 1045 } 1046 1047 if (m_automatic_size_policy) { 1048 set_fixed_size(m_pixel_width, m_pixel_height); 1049 } 1050 1051 update(); 1052 1053 winsize ws; 1054 ws.ws_row = rows; 1055 ws.ws_col = columns; 1056 if (m_ptm_fd != -1) { 1057 if (ioctl(m_ptm_fd, TIOCSWINSZ, &ws) < 0) { 1058 // This can happen if we resize just as the shell exits. 1059 dbgln("Notifying the pseudo-terminal about a size change failed."); 1060 } 1061 } 1062} 1063 1064void TerminalWidget::beep() 1065{ 1066 if (m_bell_mode == BellMode::Disabled) { 1067 return; 1068 } 1069 if (m_bell_mode == BellMode::AudibleBeep) { 1070 sysbeep(440); 1071 return; 1072 } 1073 m_visual_beep_timer->restart(200); 1074 m_visual_beep_timer->set_single_shot(true); 1075 m_visual_beep_timer->on_timeout = [this] { 1076 update(); 1077 }; 1078 update(); 1079} 1080 1081void TerminalWidget::emit(u8 const* data, size_t size) 1082{ 1083 if (write(m_ptm_fd, data, size) < 0) { 1084 perror("TerminalWidget::emit: write"); 1085 } 1086} 1087 1088void TerminalWidget::set_cursor_blinking(bool blinking) 1089{ 1090 if (blinking) { 1091 m_cursor_blink_timer->stop(); 1092 m_cursor_blink_state = true; 1093 m_cursor_blink_timer->start(); 1094 m_cursor_is_blinking_set = true; 1095 } else { 1096 m_cursor_blink_timer->stop(); 1097 m_cursor_blink_state = true; 1098 m_cursor_is_blinking_set = false; 1099 } 1100 invalidate_cursor(); 1101} 1102 1103void TerminalWidget::set_cursor_shape(CursorShape shape) 1104{ 1105 m_cursor_shape = shape; 1106 invalidate_cursor(); 1107 update(); 1108} 1109 1110void TerminalWidget::context_menu_event(GUI::ContextMenuEvent& event) 1111{ 1112 if (m_hovered_href_id.is_null()) { 1113 m_context_menu->popup(event.screen_position()); 1114 } else { 1115 m_context_menu_href = m_hovered_href; 1116 1117 // Ask LaunchServer for a list of programs that can handle the right-clicked URL. 1118 auto handlers = Desktop::Launcher::get_handlers_for_url(m_hovered_href); 1119 if (handlers.is_empty()) { 1120 m_context_menu->popup(event.screen_position()); 1121 return; 1122 } 1123 1124 m_context_menu_for_hyperlink = GUI::Menu::construct(); 1125 RefPtr<GUI::Action> context_menu_default_action; 1126 1127 // Go through the list of handlers and see if we can find a nice display name + icon for them. 1128 // Then add them to the context menu. 1129 // FIXME: Adapt this code when we actually support calling LaunchServer with a specific handler in mind. 1130 for (auto& handler : handlers) { 1131 auto af = Desktop::AppFile::get_for_app(LexicalPath::basename(handler)); 1132 if (!af->is_valid()) 1133 continue; 1134 auto action = GUI::Action::create(DeprecatedString::formatted("&Open in {}", af->name()), af->icon().bitmap_for_size(16), [this, handler](auto&) { 1135 Desktop::Launcher::open(m_context_menu_href, handler); 1136 }); 1137 1138 if (context_menu_default_action.is_null()) { 1139 context_menu_default_action = action; 1140 } 1141 1142 m_context_menu_for_hyperlink->add_action(action); 1143 } 1144 m_context_menu_for_hyperlink->add_action(GUI::Action::create("Copy &URL", [this](auto&) { 1145 GUI::Clipboard::the().set_plain_text(m_context_menu_href); 1146 })); 1147 m_context_menu_for_hyperlink->add_action(GUI::Action::create("Copy &Name", [&](auto&) { 1148 // file://courage/home/anon/something -> /home/anon/something 1149 auto path = URL(m_context_menu_href).path(); 1150 // /home/anon/something -> something 1151 auto name = LexicalPath::basename(path); 1152 GUI::Clipboard::the().set_plain_text(name); 1153 })); 1154 m_context_menu_for_hyperlink->add_separator(); 1155 m_context_menu_for_hyperlink->add_action(copy_action()); 1156 m_context_menu_for_hyperlink->add_action(paste_action()); 1157 1158 m_context_menu_for_hyperlink->popup(event.screen_position(), context_menu_default_action); 1159 } 1160} 1161 1162void TerminalWidget::drag_enter_event(GUI::DragEvent& event) 1163{ 1164 auto const& mime_types = event.mime_types(); 1165 if (mime_types.contains_slow("text/plain") || mime_types.contains_slow("text/uri-list")) 1166 event.accept(); 1167} 1168 1169void TerminalWidget::drop_event(GUI::DropEvent& event) 1170{ 1171 if (event.mime_data().has_urls()) { 1172 event.accept(); 1173 auto urls = event.mime_data().urls(); 1174 bool first = true; 1175 for (auto& url : event.mime_data().urls()) { 1176 if (!first) 1177 send_non_user_input(" "sv.bytes()); 1178 1179 if (url.scheme() == "file") 1180 send_non_user_input(url.path().bytes()); 1181 else 1182 send_non_user_input(url.to_deprecated_string().bytes()); 1183 1184 first = false; 1185 } 1186 } else if (event.mime_data().has_text()) { 1187 event.accept(); 1188 auto text = event.mime_data().text(); 1189 send_non_user_input(text.bytes()); 1190 } 1191} 1192 1193void TerminalWidget::did_change_font() 1194{ 1195 GUI::Frame::did_change_font(); 1196 update_cached_font_metrics(); 1197 if (!size().is_empty()) 1198 relayout(size()); 1199} 1200 1201static void collect_font_metrics(Gfx::Font const& font, int& column_width, int& cell_height, int& line_height, int& line_spacing) 1202{ 1203 line_spacing = 4; 1204 column_width = static_cast<int>(ceilf(font.glyph_width('x'))); 1205 cell_height = font.pixel_size_rounded_up(); 1206 line_height = cell_height + line_spacing; 1207} 1208 1209void TerminalWidget::update_cached_font_metrics() 1210{ 1211 collect_font_metrics(font(), m_column_width, m_cell_height, m_line_height, m_line_spacing); 1212} 1213 1214void TerminalWidget::clear_including_history() 1215{ 1216 m_terminal.clear_including_history(); 1217} 1218 1219void TerminalWidget::scroll_to_bottom() 1220{ 1221 m_scrollbar->set_value(m_scrollbar->max()); 1222} 1223 1224void TerminalWidget::scroll_to_row(int row) 1225{ 1226 m_scrollbar->set_value(row); 1227} 1228 1229void TerminalWidget::update_copy_action() 1230{ 1231 m_copy_action->set_enabled(has_selection()); 1232} 1233 1234void TerminalWidget::update_paste_action() 1235{ 1236 auto [data, mime_type, _] = GUI::Clipboard::the().fetch_data_and_type(); 1237 m_paste_action->set_enabled(mime_type.starts_with("text/"sv) && !data.is_empty()); 1238} 1239 1240void TerminalWidget::update_color_scheme() 1241{ 1242 auto const& palette = GUI::Widget::palette(); 1243 1244 m_show_bold_text_as_bright = palette.bold_text_as_bright(); 1245 1246 m_default_background_color = palette.background(); 1247 m_default_foreground_color = palette.foreground(); 1248 1249 m_colors[0] = palette.black(); 1250 m_colors[1] = palette.red(); 1251 m_colors[2] = palette.green(); 1252 m_colors[3] = palette.yellow(); 1253 m_colors[4] = palette.blue(); 1254 m_colors[5] = palette.magenta(); 1255 m_colors[6] = palette.cyan(); 1256 m_colors[7] = palette.white(); 1257 m_colors[8] = palette.bright_black(); 1258 m_colors[9] = palette.bright_red(); 1259 m_colors[10] = palette.bright_green(); 1260 m_colors[11] = palette.bright_yellow(); 1261 m_colors[12] = palette.bright_blue(); 1262 m_colors[13] = palette.bright_magenta(); 1263 m_colors[14] = palette.bright_cyan(); 1264 m_colors[15] = palette.bright_white(); 1265 1266 update(); 1267} 1268 1269Gfx::IntSize TerminalWidget::widget_size_for_font(Gfx::Font const& font) const 1270{ 1271 int column_width = 0; 1272 int line_height = 0; 1273 int cell_height = 0; 1274 int line_spacing = 0; 1275 collect_font_metrics(font, column_width, cell_height, line_height, line_spacing); 1276 auto base_size = compute_base_size(); 1277 return { 1278 base_size.width() + (m_terminal.columns() * column_width), 1279 base_size.height() + (m_terminal.rows() * line_height), 1280 }; 1281} 1282 1283constexpr Gfx::Color TerminalWidget::terminal_color_to_rgb(VT::Color color) const 1284{ 1285 switch (color.kind()) { 1286 case VT::Color::Kind::RGB: 1287 return Gfx::Color::from_rgb(color.as_rgb()); 1288 case VT::Color::Kind::Indexed: 1289 return m_colors[color.as_indexed()]; 1290 case VT::Color::Kind::Named: { 1291 auto ansi = color.as_named(); 1292 if ((u16)ansi < 256) 1293 return m_colors[(u16)ansi]; 1294 else if (ansi == VT::Color::ANSIColor::DefaultForeground) 1295 return m_default_foreground_color; 1296 else if (ansi == VT::Color::ANSIColor::DefaultBackground) 1297 return m_default_background_color; 1298 else 1299 VERIFY_NOT_REACHED(); 1300 } 1301 default: 1302 VERIFY_NOT_REACHED(); 1303 } 1304}; 1305 1306void TerminalWidget::set_font_and_resize_to_fit(Gfx::Font const& font) 1307{ 1308 resize(widget_size_for_font(font)); 1309 set_font(font); 1310} 1311 1312// Used for sending data that was not directly typed by the user. 1313// This basically wraps the code that handles sending the escape sequence in bracketed paste mode. 1314void TerminalWidget::send_non_user_input(ReadonlyBytes bytes) 1315{ 1316 constexpr StringView leading_control_sequence = "\e[200~"sv; 1317 constexpr StringView trailing_control_sequence = "\e[201~"sv; 1318 1319 int nwritten; 1320 if (m_terminal.needs_bracketed_paste()) { 1321 // We do not call write() separately for the control sequences and the data, 1322 // because that would present a race condition where another process could inject data 1323 // to prematurely terminate the escape. Could probably be solved by file locking. 1324 Vector<u8> output; 1325 output.ensure_capacity(leading_control_sequence.bytes().size() + bytes.size() + trailing_control_sequence.bytes().size()); 1326 1327 // HACK: We don't have a `Vector<T>::unchecked_append(Span<T> const&)` yet :^( 1328 output.append(leading_control_sequence.bytes().data(), leading_control_sequence.bytes().size()); 1329 output.append(bytes.data(), bytes.size()); 1330 output.append(trailing_control_sequence.bytes().data(), trailing_control_sequence.bytes().size()); 1331 nwritten = write(m_ptm_fd, output.data(), output.size()); 1332 } else { 1333 nwritten = write(m_ptm_fd, bytes.data(), bytes.size()); 1334 } 1335 if (nwritten < 0) { 1336 perror("write"); 1337 VERIFY_NOT_REACHED(); 1338 } 1339} 1340 1341void TerminalWidget::set_auto_scroll_direction(AutoScrollDirection direction) 1342{ 1343 m_auto_scroll_direction = direction; 1344 m_auto_scroll_timer->set_active(direction != AutoScrollDirection::None); 1345} 1346 1347Optional<VT::CursorShape> TerminalWidget::parse_cursor_shape(StringView cursor_shape_string) 1348{ 1349 if (cursor_shape_string == "Block"sv) 1350 return VT::CursorShape::Block; 1351 1352 if (cursor_shape_string == "Underline"sv) 1353 return VT::CursorShape::Underline; 1354 1355 if (cursor_shape_string == "Bar"sv) 1356 return VT::CursorShape::Bar; 1357 1358 return {}; 1359} 1360 1361DeprecatedString TerminalWidget::stringify_cursor_shape(VT::CursorShape cursor_shape) 1362{ 1363 switch (cursor_shape) { 1364 case VT::CursorShape::Block: 1365 return "Block"; 1366 case VT::CursorShape::Underline: 1367 return "Underline"; 1368 case VT::CursorShape::Bar: 1369 return "Bar"; 1370 case VT::CursorShape::None: 1371 return "None"; 1372 } 1373 VERIFY_NOT_REACHED(); 1374} 1375 1376}