Serenity Operating System
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}