TerminalWidget.cpp 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158
  1. /*
  2. * Copyright (c) 2018-2021, Andreas Kling <kling@serenityos.org>
  3. *
  4. * SPDX-License-Identifier: BSD-2-Clause
  5. */
  6. #include "TerminalWidget.h"
  7. #include <AK/LexicalPath.h>
  8. #include <AK/StdLibExtras.h>
  9. #include <AK/String.h>
  10. #include <AK/StringBuilder.h>
  11. #include <AK/TemporaryChange.h>
  12. #include <AK/Utf32View.h>
  13. #include <AK/Utf8View.h>
  14. #include <LibCore/ConfigFile.h>
  15. #include <LibCore/MimeData.h>
  16. #include <LibDesktop/AppFile.h>
  17. #include <LibDesktop/Launcher.h>
  18. #include <LibGUI/Action.h>
  19. #include <LibGUI/Application.h>
  20. #include <LibGUI/Clipboard.h>
  21. #include <LibGUI/DragOperation.h>
  22. #include <LibGUI/Icon.h>
  23. #include <LibGUI/Menu.h>
  24. #include <LibGUI/Painter.h>
  25. #include <LibGUI/Scrollbar.h>
  26. #include <LibGUI/Window.h>
  27. #include <LibGfx/Font.h>
  28. #include <LibGfx/FontDatabase.h>
  29. #include <LibGfx/Palette.h>
  30. #include <LibGfx/StylePainter.h>
  31. #include <ctype.h>
  32. #include <errno.h>
  33. #include <math.h>
  34. #include <stdio.h>
  35. #include <stdlib.h>
  36. #include <string.h>
  37. #include <sys/ioctl.h>
  38. #include <unistd.h>
  39. namespace VT {
  40. void TerminalWidget::set_pty_master_fd(int fd)
  41. {
  42. m_ptm_fd = fd;
  43. if (m_ptm_fd == -1) {
  44. m_notifier = nullptr;
  45. return;
  46. }
  47. m_notifier = Core::Notifier::construct(m_ptm_fd, Core::Notifier::Read);
  48. m_notifier->on_ready_to_read = [this] {
  49. u8 buffer[BUFSIZ];
  50. ssize_t nread = read(m_ptm_fd, buffer, sizeof(buffer));
  51. if (nread < 0) {
  52. dbgln("Terminal read error: {}", strerror(errno));
  53. perror("read(ptm)");
  54. GUI::Application::the()->quit(1);
  55. return;
  56. }
  57. if (nread == 0) {
  58. dbgln("TerminalWidget: EOF on master pty, firing on_command_exit hook.");
  59. if (on_command_exit)
  60. on_command_exit();
  61. int rc = close(m_ptm_fd);
  62. if (rc < 0) {
  63. perror("close");
  64. }
  65. set_pty_master_fd(-1);
  66. return;
  67. }
  68. for (ssize_t i = 0; i < nread; ++i)
  69. m_terminal.on_input(buffer[i]);
  70. flush_dirty_lines();
  71. };
  72. }
  73. TerminalWidget::TerminalWidget(int ptm_fd, bool automatic_size_policy, RefPtr<Core::ConfigFile> config)
  74. : m_terminal(*this)
  75. , m_automatic_size_policy(automatic_size_policy)
  76. , m_config(move(config))
  77. {
  78. set_override_cursor(Gfx::StandardCursor::IBeam);
  79. set_focus_policy(GUI::FocusPolicy::StrongFocus);
  80. set_accepts_emoji_input(true);
  81. set_pty_master_fd(ptm_fd);
  82. m_cursor_blink_timer = add<Core::Timer>();
  83. m_visual_beep_timer = add<Core::Timer>();
  84. m_auto_scroll_timer = add<Core::Timer>();
  85. m_scrollbar = add<GUI::Scrollbar>(Orientation::Vertical);
  86. m_scrollbar->set_relative_rect(0, 0, 16, 0);
  87. m_scrollbar->on_change = [this](int) {
  88. update();
  89. };
  90. dbgln("Load config file from {}", m_config->filename());
  91. m_cursor_blink_timer->set_interval(m_config->read_num_entry("Text",
  92. "CursorBlinkInterval",
  93. 500));
  94. m_cursor_blink_timer->on_timeout = [this] {
  95. m_cursor_blink_state = !m_cursor_blink_state;
  96. update_cursor();
  97. };
  98. m_auto_scroll_timer->set_interval(50);
  99. m_auto_scroll_timer->on_timeout = [this] {
  100. if (m_auto_scroll_direction != AutoScrollDirection::None) {
  101. int scroll_amount = m_auto_scroll_direction == AutoScrollDirection::Up ? -1 : 1;
  102. m_scrollbar->set_value(m_scrollbar->value() + scroll_amount);
  103. }
  104. };
  105. m_auto_scroll_timer->start();
  106. auto font_entry = m_config->read_entry("Text", "Font", "default");
  107. if (font_entry == "default")
  108. set_font(Gfx::FontDatabase::default_fixed_width_font());
  109. else
  110. set_font(Gfx::FontDatabase::the().get_by_name(font_entry));
  111. m_line_height = font().glyph_height() + m_line_spacing;
  112. m_terminal.set_size(m_config->read_num_entry("Window", "Width", 80), m_config->read_num_entry("Window", "Height", 25));
  113. m_copy_action = GUI::Action::create("&Copy", { Mod_Ctrl | Mod_Shift, Key_C }, Gfx::Bitmap::load_from_file("/res/icons/16x16/edit-copy.png"), [this](auto&) {
  114. copy();
  115. });
  116. m_copy_action->set_swallow_key_event_when_disabled(true);
  117. m_paste_action = GUI::Action::create("&Paste", { Mod_Ctrl | Mod_Shift, Key_V }, Gfx::Bitmap::load_from_file("/res/icons/16x16/paste.png"), [this](auto&) {
  118. paste();
  119. });
  120. m_paste_action->set_swallow_key_event_when_disabled(true);
  121. m_clear_including_history_action = GUI::Action::create("Clear Including &History", { Mod_Ctrl | Mod_Shift, Key_K }, [this](auto&) {
  122. clear_including_history();
  123. });
  124. m_context_menu = GUI::Menu::construct();
  125. m_context_menu->add_action(copy_action());
  126. m_context_menu->add_action(paste_action());
  127. m_context_menu->add_separator();
  128. m_context_menu->add_action(clear_including_history_action());
  129. GUI::Clipboard::the().on_change = [this](const String&) {
  130. update_paste_action();
  131. };
  132. update_copy_action();
  133. update_paste_action();
  134. }
  135. TerminalWidget::~TerminalWidget()
  136. {
  137. }
  138. static inline Color color_from_rgb(unsigned color)
  139. {
  140. return Color::from_rgb(color);
  141. }
  142. Gfx::IntRect TerminalWidget::glyph_rect(u16 row, u16 column)
  143. {
  144. int y = row * m_line_height;
  145. int x = column * font().glyph_width('x');
  146. return { x + frame_thickness() + m_inset, y + frame_thickness() + m_inset, font().glyph_width('x'), font().glyph_height() };
  147. }
  148. Gfx::IntRect TerminalWidget::row_rect(u16 row)
  149. {
  150. int y = row * m_line_height;
  151. Gfx::IntRect rect = { frame_thickness() + m_inset, y + frame_thickness() + m_inset, font().glyph_width('x') * m_terminal.columns(), font().glyph_height() };
  152. rect.inflate(0, m_line_spacing);
  153. return rect;
  154. }
  155. void TerminalWidget::set_logical_focus(bool focus)
  156. {
  157. m_has_logical_focus = focus;
  158. if (!m_has_logical_focus) {
  159. m_cursor_blink_timer->stop();
  160. } else {
  161. m_cursor_blink_state = true;
  162. m_cursor_blink_timer->start();
  163. }
  164. m_auto_scroll_direction = AutoScrollDirection::None;
  165. invalidate_cursor();
  166. update();
  167. }
  168. void TerminalWidget::focusin_event(GUI::FocusEvent& event)
  169. {
  170. set_logical_focus(true);
  171. return GUI::Frame::focusin_event(event);
  172. }
  173. void TerminalWidget::focusout_event(GUI::FocusEvent& event)
  174. {
  175. set_logical_focus(false);
  176. return GUI::Frame::focusout_event(event);
  177. }
  178. void TerminalWidget::event(Core::Event& event)
  179. {
  180. if (event.type() == GUI::Event::WindowBecameActive)
  181. set_logical_focus(true);
  182. else if (event.type() == GUI::Event::WindowBecameInactive)
  183. set_logical_focus(false);
  184. return GUI::Frame::event(event);
  185. }
  186. void TerminalWidget::keydown_event(GUI::KeyEvent& event)
  187. {
  188. if (m_ptm_fd == -1) {
  189. event.ignore();
  190. return GUI::Frame::keydown_event(event);
  191. }
  192. // Reset timer so cursor doesn't blink while typing.
  193. m_cursor_blink_timer->stop();
  194. m_cursor_blink_state = true;
  195. m_cursor_blink_timer->start();
  196. if (event.key() == KeyCode::Key_PageUp && event.modifiers() == Mod_Shift) {
  197. m_scrollbar->set_value(m_scrollbar->value() - m_terminal.rows());
  198. return;
  199. }
  200. if (event.key() == KeyCode::Key_PageDown && event.modifiers() == Mod_Shift) {
  201. m_scrollbar->set_value(m_scrollbar->value() + m_terminal.rows());
  202. return;
  203. }
  204. if (event.key() == KeyCode::Key_Alt) {
  205. m_alt_key_held = true;
  206. return;
  207. }
  208. // Clear the selection if we type in/behind it.
  209. auto future_cursor_column = (event.key() == KeyCode::Key_Backspace) ? m_terminal.cursor_column() - 1 : m_terminal.cursor_column();
  210. auto min_selection_row = min(m_selection.start().row(), m_selection.end().row());
  211. auto max_selection_row = max(m_selection.start().row(), m_selection.end().row());
  212. 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) {
  213. m_selection.set_end({});
  214. update_copy_action();
  215. update();
  216. }
  217. m_terminal.handle_key_press(event.key(), event.code_point(), event.modifiers());
  218. if (event.key() != Key_Control && event.key() != Key_Alt && event.key() != Key_LeftShift && event.key() != Key_RightShift && event.key() != Key_Super)
  219. scroll_to_bottom();
  220. }
  221. void TerminalWidget::keyup_event(GUI::KeyEvent& event)
  222. {
  223. switch (event.key()) {
  224. case KeyCode::Key_Alt:
  225. m_alt_key_held = false;
  226. return;
  227. default:
  228. break;
  229. }
  230. }
  231. void TerminalWidget::paint_event(GUI::PaintEvent& event)
  232. {
  233. GUI::Frame::paint_event(event);
  234. GUI::Painter painter(*this);
  235. auto visual_beep_active = m_visual_beep_timer->is_active();
  236. painter.add_clip_rect(event.rect());
  237. Gfx::IntRect terminal_buffer_rect(frame_inner_rect().top_left(), { frame_inner_rect().width() - m_scrollbar->width(), frame_inner_rect().height() });
  238. painter.add_clip_rect(terminal_buffer_rect);
  239. if (visual_beep_active)
  240. painter.clear_rect(frame_inner_rect(), Color::Red);
  241. else
  242. painter.clear_rect(frame_inner_rect(), Color(Color::Black).with_alpha(m_opacity));
  243. invalidate_cursor();
  244. int rows_from_history = 0;
  245. int first_row_from_history = m_terminal.history_size();
  246. int row_with_cursor = m_terminal.cursor_row();
  247. if (m_scrollbar->value() != m_scrollbar->max()) {
  248. rows_from_history = min((int)m_terminal.rows(), m_scrollbar->max() - m_scrollbar->value());
  249. first_row_from_history = m_terminal.history_size() - (m_scrollbar->max() - m_scrollbar->value());
  250. row_with_cursor = m_terminal.cursor_row() + rows_from_history;
  251. }
  252. // Pass: Compute the rect(s) of the currently hovered link, if any.
  253. Vector<Gfx::IntRect> hovered_href_rects;
  254. if (!m_hovered_href_id.is_null()) {
  255. for (u16 visual_row = 0; visual_row < m_terminal.rows(); ++visual_row) {
  256. auto& line = m_terminal.line(first_row_from_history + visual_row);
  257. for (size_t column = 0; column < line.length(); ++column) {
  258. if (m_hovered_href_id == line.attribute_at(column).href_id) {
  259. bool merged_with_existing_rect = false;
  260. auto glyph_rect = this->glyph_rect(visual_row, column);
  261. for (auto& rect : hovered_href_rects) {
  262. if (rect.inflated(1, 1).intersects(glyph_rect)) {
  263. rect = rect.united(glyph_rect);
  264. merged_with_existing_rect = true;
  265. break;
  266. }
  267. }
  268. if (!merged_with_existing_rect)
  269. hovered_href_rects.append(glyph_rect);
  270. }
  271. }
  272. }
  273. }
  274. // Pass: Paint background & text decorations.
  275. for (u16 visual_row = 0; visual_row < m_terminal.rows(); ++visual_row) {
  276. auto row_rect = this->row_rect(visual_row);
  277. if (!event.rect().contains(row_rect))
  278. continue;
  279. auto& line = m_terminal.line(first_row_from_history + visual_row);
  280. bool has_only_one_background_color = line.has_only_one_background_color();
  281. if (visual_beep_active)
  282. painter.clear_rect(row_rect, Color::Red);
  283. else if (has_only_one_background_color)
  284. painter.clear_rect(row_rect, color_from_rgb(line.attribute_at(0).effective_background_color()).with_alpha(m_opacity));
  285. for (size_t column = 0; column < line.length(); ++column) {
  286. bool should_reverse_fill_for_cursor_or_selection = m_cursor_blink_state
  287. && (m_cursor_style == VT::CursorStyle::SteadyBlock || m_cursor_style == VT::CursorStyle::BlinkingBlock)
  288. && m_has_logical_focus
  289. && visual_row == row_with_cursor
  290. && column == m_terminal.cursor_column();
  291. should_reverse_fill_for_cursor_or_selection |= selection_contains({ first_row_from_history + visual_row, (int)column });
  292. auto attribute = line.attribute_at(column);
  293. auto character_rect = glyph_rect(visual_row, column);
  294. auto cell_rect = character_rect.inflated(0, m_line_spacing);
  295. auto text_color = color_from_rgb(should_reverse_fill_for_cursor_or_selection ? attribute.effective_background_color() : attribute.effective_foreground_color());
  296. if ((!visual_beep_active && !has_only_one_background_color) || should_reverse_fill_for_cursor_or_selection)
  297. painter.clear_rect(cell_rect, color_from_rgb(should_reverse_fill_for_cursor_or_selection ? attribute.effective_foreground_color() : attribute.effective_background_color()));
  298. enum class UnderlineStyle {
  299. None,
  300. Dotted,
  301. Solid,
  302. };
  303. auto underline_style = UnderlineStyle::None;
  304. if (attribute.flags & VT::Attribute::Underline) {
  305. // Content has specified underline
  306. underline_style = UnderlineStyle::Solid;
  307. } else if (!attribute.href.is_empty()) {
  308. // We're hovering a hyperlink
  309. if (m_hovered_href_id == attribute.href_id || m_active_href_id == attribute.href_id)
  310. underline_style = UnderlineStyle::Solid;
  311. else
  312. underline_style = UnderlineStyle::Dotted;
  313. }
  314. if (underline_style == UnderlineStyle::Solid) {
  315. if (attribute.href_id == m_active_href_id && m_hovered_href_id == m_active_href_id)
  316. text_color = palette().active_link();
  317. painter.draw_line(cell_rect.bottom_left(), cell_rect.bottom_right(), text_color);
  318. } else if (underline_style == UnderlineStyle::Dotted) {
  319. auto dotted_line_color = text_color.darkened(0.6f);
  320. int x1 = cell_rect.bottom_left().x();
  321. int x2 = cell_rect.bottom_right().x();
  322. int y = cell_rect.bottom_left().y();
  323. for (int x = x1; x <= x2; ++x) {
  324. if ((x % 3) == 0)
  325. painter.set_pixel({ x, y }, dotted_line_color);
  326. }
  327. }
  328. }
  329. }
  330. // Paint the hovered link rects, if any.
  331. for (auto rect : hovered_href_rects) {
  332. rect.inflate(6, 6);
  333. painter.fill_rect(rect, palette().base());
  334. painter.draw_rect(rect.inflated(2, 2).intersected(frame_inner_rect()), palette().base_text());
  335. }
  336. auto& font = this->font();
  337. auto& bold_font = font.bold_variant();
  338. // Pass: Paint foreground (text).
  339. for (u16 visual_row = 0; visual_row < m_terminal.rows(); ++visual_row) {
  340. auto row_rect = this->row_rect(visual_row);
  341. if (!event.rect().contains(row_rect))
  342. continue;
  343. auto& line = m_terminal.line(first_row_from_history + visual_row);
  344. for (size_t column = 0; column < line.length(); ++column) {
  345. auto attribute = line.attribute_at(column);
  346. bool should_reverse_fill_for_cursor_or_selection = m_cursor_blink_state
  347. && (m_cursor_style == VT::CursorStyle::SteadyBlock || m_cursor_style == VT::CursorStyle::BlinkingBlock)
  348. && m_has_logical_focus
  349. && visual_row == row_with_cursor
  350. && column == m_terminal.cursor_column();
  351. should_reverse_fill_for_cursor_or_selection |= selection_contains({ first_row_from_history + visual_row, (int)column });
  352. auto text_color = color_from_rgb(should_reverse_fill_for_cursor_or_selection ? attribute.effective_background_color() : attribute.effective_foreground_color());
  353. u32 code_point = line.code_point(column);
  354. if (code_point == ' ')
  355. continue;
  356. auto character_rect = glyph_rect(visual_row, column);
  357. if (!m_hovered_href_id.is_null() && attribute.href_id == m_hovered_href_id) {
  358. text_color = palette().base_text();
  359. }
  360. painter.draw_glyph_or_emoji(
  361. character_rect.location(),
  362. code_point,
  363. attribute.flags & VT::Attribute::Bold ? bold_font : font,
  364. text_color);
  365. }
  366. }
  367. // Draw cursor.
  368. if (m_cursor_blink_state && row_with_cursor < m_terminal.rows()) {
  369. auto& cursor_line = m_terminal.line(first_row_from_history + row_with_cursor);
  370. if (m_terminal.cursor_row() >= (m_terminal.rows() - rows_from_history))
  371. return;
  372. if (m_has_logical_focus && (m_cursor_style == VT::CursorStyle::BlinkingBlock || m_cursor_style == VT::CursorStyle::SteadyBlock))
  373. return; // This has already been handled by inverting the cell colors
  374. auto cursor_color = color_from_rgb(cursor_line.attribute_at(m_terminal.cursor_column()).effective_foreground_color());
  375. auto cell_rect = glyph_rect(row_with_cursor, m_terminal.cursor_column()).inflated(0, m_line_spacing);
  376. if (m_cursor_style == VT::CursorStyle::BlinkingUnderline || m_cursor_style == VT::CursorStyle::SteadyUnderline) {
  377. auto x1 = cell_rect.bottom_left().x();
  378. auto x2 = cell_rect.bottom_right().x();
  379. auto y = cell_rect.bottom_left().y();
  380. for (auto x = x1; x <= x2; ++x)
  381. painter.set_pixel({ x, y }, cursor_color);
  382. } else if (m_cursor_style == VT::CursorStyle::BlinkingBar || m_cursor_style == VT::CursorStyle::SteadyBar) {
  383. auto x = cell_rect.bottom_left().x();
  384. auto y1 = cell_rect.top_left().y();
  385. auto y2 = cell_rect.bottom_left().y();
  386. for (auto y = y1; y <= y2; ++y)
  387. painter.set_pixel({ x, y }, cursor_color);
  388. } else {
  389. // We fall back to a block if we don't support the selected cursor type.
  390. painter.draw_rect(cell_rect, cursor_color);
  391. }
  392. }
  393. }
  394. void TerminalWidget::set_window_progress(int value, int max)
  395. {
  396. float float_value = value;
  397. float float_max = max;
  398. float progress = (float_value / float_max) * 100.0f;
  399. window()->set_progress((int)roundf(progress));
  400. }
  401. void TerminalWidget::set_window_title(const StringView& title)
  402. {
  403. if (!Utf8View(title).validate()) {
  404. dbgln("TerminalWidget: Attempted to set window title to invalid UTF-8 string");
  405. return;
  406. }
  407. if (on_title_change)
  408. on_title_change(title);
  409. }
  410. void TerminalWidget::invalidate_cursor()
  411. {
  412. m_terminal.invalidate_cursor();
  413. }
  414. void TerminalWidget::flush_dirty_lines()
  415. {
  416. // FIXME: Update smarter when scrolled
  417. if (m_terminal.m_need_full_flush || m_scrollbar->value() != m_scrollbar->max()) {
  418. update();
  419. m_terminal.m_need_full_flush = false;
  420. return;
  421. }
  422. Gfx::IntRect rect;
  423. for (int i = 0; i < m_terminal.rows(); ++i) {
  424. if (m_terminal.visible_line(i).is_dirty()) {
  425. rect = rect.united(row_rect(i));
  426. m_terminal.visible_line(i).set_dirty(false);
  427. }
  428. }
  429. update(rect);
  430. }
  431. void TerminalWidget::resize_event(GUI::ResizeEvent& event)
  432. {
  433. relayout(event.size());
  434. }
  435. void TerminalWidget::relayout(const Gfx::IntSize& size)
  436. {
  437. if (!m_scrollbar)
  438. return;
  439. TemporaryChange change(m_in_relayout, true);
  440. auto base_size = compute_base_size();
  441. int new_columns = (size.width() - base_size.width()) / font().glyph_width('x');
  442. int new_rows = (size.height() - base_size.height()) / m_line_height;
  443. m_terminal.set_size(new_columns, new_rows);
  444. Gfx::IntRect scrollbar_rect = {
  445. size.width() - m_scrollbar->width() - frame_thickness(),
  446. frame_thickness(),
  447. m_scrollbar->width(),
  448. size.height() - frame_thickness() * 2,
  449. };
  450. m_scrollbar->set_relative_rect(scrollbar_rect);
  451. m_scrollbar->set_page_step(new_rows);
  452. }
  453. Gfx::IntSize TerminalWidget::compute_base_size() const
  454. {
  455. int base_width = frame_thickness() * 2 + m_inset * 2 + m_scrollbar->width();
  456. int base_height = frame_thickness() * 2 + m_inset * 2;
  457. return { base_width, base_height };
  458. }
  459. void TerminalWidget::apply_size_increments_to_window(GUI::Window& window)
  460. {
  461. window.set_size_increment({ font().glyph_width('x'), m_line_height });
  462. window.set_base_size(compute_base_size());
  463. }
  464. void TerminalWidget::update_cursor()
  465. {
  466. invalidate_cursor();
  467. flush_dirty_lines();
  468. }
  469. void TerminalWidget::set_opacity(u8 new_opacity)
  470. {
  471. if (m_opacity == new_opacity)
  472. return;
  473. window()->set_has_alpha_channel(new_opacity < 255);
  474. m_opacity = new_opacity;
  475. update();
  476. }
  477. bool TerminalWidget::has_selection() const
  478. {
  479. return m_selection.is_valid();
  480. }
  481. void TerminalWidget::set_selection(const VT::Range& selection)
  482. {
  483. m_selection = selection;
  484. update_copy_action();
  485. update();
  486. }
  487. bool TerminalWidget::selection_contains(const VT::Position& position) const
  488. {
  489. if (!has_selection())
  490. return false;
  491. if (m_rectangle_selection) {
  492. auto m_selection_start = m_selection.start();
  493. auto m_selection_end = m_selection.end();
  494. auto min_selection_column = min(m_selection_start.column(), m_selection_end.column());
  495. auto max_selection_column = max(m_selection_start.column(), m_selection_end.column());
  496. auto min_selection_row = min(m_selection_start.row(), m_selection_end.row());
  497. auto max_selection_row = max(m_selection_start.row(), m_selection_end.row());
  498. return position.column() >= min_selection_column && position.column() <= max_selection_column && position.row() >= min_selection_row && position.row() <= max_selection_row;
  499. }
  500. auto normalized_selection = m_selection.normalized();
  501. return position >= normalized_selection.start() && position <= normalized_selection.end();
  502. }
  503. VT::Position TerminalWidget::buffer_position_at(const Gfx::IntPoint& position) const
  504. {
  505. auto adjusted_position = position.translated(-(frame_thickness() + m_inset), -(frame_thickness() + m_inset));
  506. int row = adjusted_position.y() / m_line_height;
  507. int column = adjusted_position.x() / font().glyph_width('x');
  508. if (row < 0)
  509. row = 0;
  510. if (column < 0)
  511. column = 0;
  512. if (row >= m_terminal.rows())
  513. row = m_terminal.rows() - 1;
  514. if (column >= m_terminal.columns())
  515. column = m_terminal.columns() - 1;
  516. row += m_scrollbar->value();
  517. return { row, column };
  518. }
  519. u32 TerminalWidget::code_point_at(const VT::Position& position) const
  520. {
  521. VERIFY(position.is_valid());
  522. VERIFY(position.row() >= 0 && static_cast<size_t>(position.row()) < m_terminal.line_count());
  523. auto& line = m_terminal.line(position.row());
  524. if (static_cast<size_t>(position.column()) == line.length())
  525. return '\n';
  526. return line.code_point(position.column());
  527. }
  528. VT::Position TerminalWidget::next_position_after(const VT::Position& position, bool should_wrap) const
  529. {
  530. VERIFY(position.is_valid());
  531. VERIFY(position.row() >= 0 && static_cast<size_t>(position.row()) < m_terminal.line_count());
  532. auto& line = m_terminal.line(position.row());
  533. if (static_cast<size_t>(position.column()) == line.length()) {
  534. if (static_cast<size_t>(position.row()) == m_terminal.line_count() - 1) {
  535. if (should_wrap)
  536. return { 0, 0 };
  537. return {};
  538. }
  539. return { position.row() + 1, 0 };
  540. }
  541. return { position.row(), position.column() + 1 };
  542. }
  543. VT::Position TerminalWidget::previous_position_before(const VT::Position& position, bool should_wrap) const
  544. {
  545. VERIFY(position.row() >= 0 && static_cast<size_t>(position.row()) < m_terminal.line_count());
  546. if (position.column() == 0) {
  547. if (position.row() == 0) {
  548. if (should_wrap) {
  549. auto& last_line = m_terminal.line(m_terminal.line_count() - 1);
  550. return { static_cast<int>(m_terminal.line_count() - 1), static_cast<int>(last_line.length()) };
  551. }
  552. return {};
  553. }
  554. auto& prev_line = m_terminal.line(position.row() - 1);
  555. return { position.row() - 1, static_cast<int>(prev_line.length()) };
  556. }
  557. return { position.row(), position.column() - 1 };
  558. }
  559. static u32 to_lowercase_code_point(u32 code_point)
  560. {
  561. // FIXME: this only handles ascii characters, but handling unicode lowercasing seems like a mess
  562. if (code_point < 128)
  563. return tolower(code_point);
  564. return code_point;
  565. }
  566. VT::Range TerminalWidget::find_next(const StringView& needle, const VT::Position& start, bool case_sensitivity, bool should_wrap)
  567. {
  568. if (needle.is_empty())
  569. return {};
  570. VT::Position position = start.is_valid() ? start : VT::Position(0, 0);
  571. VT::Position original_position = position;
  572. VT::Position start_of_potential_match;
  573. size_t needle_index = 0;
  574. do {
  575. auto ch = code_point_at(position);
  576. // FIXME: This is not the right way to use a Unicode needle!
  577. auto needle_ch = (u32)needle[needle_index];
  578. if (case_sensitivity ? ch == needle_ch : to_lowercase_code_point(ch) == to_lowercase_code_point(needle_ch)) {
  579. if (needle_index == 0)
  580. start_of_potential_match = position;
  581. ++needle_index;
  582. if (needle_index >= needle.length())
  583. return { start_of_potential_match, position };
  584. } else {
  585. if (needle_index > 0)
  586. position = start_of_potential_match;
  587. needle_index = 0;
  588. }
  589. position = next_position_after(position, should_wrap);
  590. } while (position.is_valid() && position != original_position);
  591. return {};
  592. }
  593. VT::Range TerminalWidget::find_previous(const StringView& needle, const VT::Position& start, bool case_sensitivity, bool should_wrap)
  594. {
  595. if (needle.is_empty())
  596. return {};
  597. VT::Position position = start.is_valid() ? start : VT::Position(m_terminal.line_count() - 1, m_terminal.line(m_terminal.line_count() - 1).length() - 1);
  598. VT::Position original_position = position;
  599. VT::Position end_of_potential_match;
  600. size_t needle_index = needle.length() - 1;
  601. do {
  602. auto ch = code_point_at(position);
  603. // FIXME: This is not the right way to use a Unicode needle!
  604. auto needle_ch = (u32)needle[needle_index];
  605. if (case_sensitivity ? ch == needle_ch : to_lowercase_code_point(ch) == to_lowercase_code_point(needle_ch)) {
  606. if (needle_index == needle.length() - 1)
  607. end_of_potential_match = position;
  608. if (needle_index == 0)
  609. return { position, end_of_potential_match };
  610. --needle_index;
  611. } else {
  612. if (needle_index < needle.length() - 1)
  613. position = end_of_potential_match;
  614. needle_index = needle.length() - 1;
  615. }
  616. position = previous_position_before(position, should_wrap);
  617. } while (position.is_valid() && position != original_position);
  618. return {};
  619. }
  620. void TerminalWidget::doubleclick_event(GUI::MouseEvent& event)
  621. {
  622. if (event.button() == GUI::MouseButton::Left) {
  623. auto attribute = m_terminal.attribute_at(buffer_position_at(event.position()));
  624. if (!attribute.href_id.is_null()) {
  625. dbgln("Open hyperlinked URL: '{}'", attribute.href);
  626. Desktop::Launcher::open(attribute.href);
  627. return;
  628. }
  629. m_triple_click_timer.start();
  630. auto position = buffer_position_at(event.position());
  631. auto& line = m_terminal.line(position.row());
  632. bool want_whitespace = line.code_point(position.column()) == ' ';
  633. int start_column = 0;
  634. int end_column = 0;
  635. for (int column = position.column(); column >= 0 && (line.code_point(column) == ' ') == want_whitespace; --column) {
  636. start_column = column;
  637. }
  638. for (int column = position.column(); column < m_terminal.columns() && (line.code_point(column) == ' ') == want_whitespace; ++column) {
  639. end_column = column;
  640. }
  641. m_selection.set({ position.row(), start_column }, { position.row(), end_column });
  642. update_copy_action();
  643. update();
  644. }
  645. GUI::Frame::doubleclick_event(event);
  646. }
  647. void TerminalWidget::paste()
  648. {
  649. if (m_ptm_fd == -1)
  650. return;
  651. auto mime_type = GUI::Clipboard::the().mime_type();
  652. if (!mime_type.starts_with("text/"))
  653. return;
  654. auto text = GUI::Clipboard::the().data();
  655. if (text.is_empty())
  656. return;
  657. int nwritten = write(m_ptm_fd, text.data(), text.size());
  658. if (nwritten < 0) {
  659. perror("write");
  660. VERIFY_NOT_REACHED();
  661. }
  662. }
  663. void TerminalWidget::copy()
  664. {
  665. if (has_selection())
  666. GUI::Clipboard::the().set_plain_text(selected_text());
  667. }
  668. void TerminalWidget::mouseup_event(GUI::MouseEvent& event)
  669. {
  670. if (event.button() == GUI::MouseButton::Left) {
  671. if (!m_active_href_id.is_null()) {
  672. m_active_href = {};
  673. m_active_href_id = {};
  674. update();
  675. }
  676. m_auto_scroll_direction = AutoScrollDirection::None;
  677. }
  678. }
  679. void TerminalWidget::mousedown_event(GUI::MouseEvent& event)
  680. {
  681. if (event.button() == GUI::MouseButton::Left) {
  682. m_left_mousedown_position = event.position();
  683. auto attribute = m_terminal.attribute_at(buffer_position_at(event.position()));
  684. if (!(event.modifiers() & Mod_Shift) && !attribute.href.is_empty()) {
  685. m_active_href = attribute.href;
  686. m_active_href_id = attribute.href_id;
  687. update();
  688. return;
  689. }
  690. m_active_href = {};
  691. m_active_href_id = {};
  692. if (m_triple_click_timer.is_valid() && m_triple_click_timer.elapsed() < 250) {
  693. int start_column = 0;
  694. int end_column = m_terminal.columns() - 1;
  695. auto position = buffer_position_at(event.position());
  696. m_selection.set({ position.row(), start_column }, { position.row(), end_column });
  697. } else {
  698. m_selection.set(buffer_position_at(event.position()), {});
  699. }
  700. if (m_alt_key_held)
  701. m_rectangle_selection = true;
  702. else if (m_rectangle_selection)
  703. m_rectangle_selection = false;
  704. update_copy_action();
  705. update();
  706. }
  707. }
  708. void TerminalWidget::mousemove_event(GUI::MouseEvent& event)
  709. {
  710. auto position = buffer_position_at(event.position());
  711. auto attribute = m_terminal.attribute_at(position);
  712. if (attribute.href_id != m_hovered_href_id) {
  713. if (m_active_href_id.is_null() || m_active_href_id == attribute.href_id) {
  714. m_hovered_href_id = attribute.href_id;
  715. m_hovered_href = attribute.href;
  716. } else {
  717. m_hovered_href_id = {};
  718. m_hovered_href = {};
  719. }
  720. set_tooltip(m_hovered_href);
  721. show_or_hide_tooltip();
  722. if (!m_hovered_href.is_empty())
  723. set_override_cursor(Gfx::StandardCursor::Arrow);
  724. else
  725. set_override_cursor(Gfx::StandardCursor::IBeam);
  726. update();
  727. }
  728. if (!(event.buttons() & GUI::MouseButton::Left))
  729. return;
  730. if (!m_active_href_id.is_null()) {
  731. auto diff = event.position() - m_left_mousedown_position;
  732. auto distance_travelled_squared = diff.x() * diff.x() + diff.y() * diff.y();
  733. constexpr int drag_distance_threshold = 5;
  734. if (distance_travelled_squared <= drag_distance_threshold)
  735. return;
  736. auto drag_operation = GUI::DragOperation::construct();
  737. drag_operation->set_text(m_active_href);
  738. drag_operation->set_data("text/uri-list", m_active_href);
  739. drag_operation->exec();
  740. m_active_href = {};
  741. m_active_href_id = {};
  742. m_hovered_href = {};
  743. m_hovered_href_id = {};
  744. update();
  745. return;
  746. }
  747. auto adjusted_position = event.position().translated(-(frame_thickness() + m_inset), -(frame_thickness() + m_inset));
  748. if (adjusted_position.y() < 0)
  749. m_auto_scroll_direction = AutoScrollDirection::Up;
  750. else if (adjusted_position.y() > m_terminal.rows() * m_line_height)
  751. m_auto_scroll_direction = AutoScrollDirection::Down;
  752. else
  753. m_auto_scroll_direction = AutoScrollDirection::None;
  754. VT::Position old_selection_end = m_selection.end();
  755. m_selection.set_end(position);
  756. if (old_selection_end != m_selection.end()) {
  757. update_copy_action();
  758. update();
  759. }
  760. }
  761. void TerminalWidget::leave_event(Core::Event&)
  762. {
  763. bool should_update = !m_hovered_href.is_empty();
  764. m_hovered_href = {};
  765. m_hovered_href_id = {};
  766. set_tooltip(m_hovered_href);
  767. set_override_cursor(Gfx::StandardCursor::IBeam);
  768. if (should_update)
  769. update();
  770. }
  771. void TerminalWidget::mousewheel_event(GUI::MouseEvent& event)
  772. {
  773. if (!is_scrollable())
  774. return;
  775. m_auto_scroll_direction = AutoScrollDirection::None;
  776. m_scrollbar->set_value(m_scrollbar->value() + event.wheel_delta() * scroll_length());
  777. GUI::Frame::mousewheel_event(event);
  778. }
  779. bool TerminalWidget::is_scrollable() const
  780. {
  781. return m_scrollbar->is_scrollable();
  782. }
  783. int TerminalWidget::scroll_length() const
  784. {
  785. return m_scrollbar->step();
  786. }
  787. String TerminalWidget::selected_text() const
  788. {
  789. StringBuilder builder;
  790. auto normalized_selection = m_selection.normalized();
  791. auto start = normalized_selection.start();
  792. auto end = normalized_selection.end();
  793. for (int row = start.row(); row <= end.row(); ++row) {
  794. int first_column = first_selection_column_on_row(row);
  795. int last_column = last_selection_column_on_row(row);
  796. for (int column = first_column; column <= last_column; ++column) {
  797. auto& line = m_terminal.line(row);
  798. if (line.attribute_at(column).is_untouched()) {
  799. builder.append('\n');
  800. break;
  801. }
  802. // FIXME: This is a bit hackish.
  803. u32 code_point = line.code_point(column);
  804. builder.append(Utf32View(&code_point, 1));
  805. if (column == static_cast<int>(line.length()) - 1 || (m_rectangle_selection && column == last_column)) {
  806. builder.append('\n');
  807. }
  808. }
  809. }
  810. return builder.to_string();
  811. }
  812. int TerminalWidget::first_selection_column_on_row(int row) const
  813. {
  814. auto normalized_selection_start = m_selection.normalized().start();
  815. return row == normalized_selection_start.row() || m_rectangle_selection ? normalized_selection_start.column() : 0;
  816. }
  817. int TerminalWidget::last_selection_column_on_row(int row) const
  818. {
  819. auto normalized_selection_end = m_selection.normalized().end();
  820. return row == normalized_selection_end.row() || m_rectangle_selection ? normalized_selection_end.column() : m_terminal.columns() - 1;
  821. }
  822. void TerminalWidget::terminal_history_changed()
  823. {
  824. bool was_max = m_scrollbar->value() == m_scrollbar->max();
  825. m_scrollbar->set_max(m_terminal.history_size());
  826. if (was_max)
  827. m_scrollbar->set_value(m_scrollbar->max());
  828. m_scrollbar->update();
  829. }
  830. void TerminalWidget::terminal_did_resize(u16 columns, u16 rows)
  831. {
  832. auto pixel_size = widget_size_for_font(font());
  833. m_pixel_width = pixel_size.width();
  834. m_pixel_height = pixel_size.height();
  835. if (!m_in_relayout) {
  836. if (on_terminal_size_change)
  837. on_terminal_size_change(Gfx::IntSize { m_pixel_width, m_pixel_height });
  838. }
  839. if (m_automatic_size_policy) {
  840. set_fixed_size(m_pixel_width, m_pixel_height);
  841. }
  842. update();
  843. winsize ws;
  844. ws.ws_row = rows;
  845. ws.ws_col = columns;
  846. if (m_ptm_fd != -1) {
  847. if (ioctl(m_ptm_fd, TIOCSWINSZ, &ws) < 0) {
  848. // This can happen if we resize just as the shell exits.
  849. dbgln("Notifying the pseudo-terminal about a size change failed.");
  850. }
  851. }
  852. }
  853. void TerminalWidget::beep()
  854. {
  855. if (m_bell_mode == BellMode::Disabled) {
  856. return;
  857. }
  858. if (m_bell_mode == BellMode::AudibleBeep) {
  859. sysbeep();
  860. return;
  861. }
  862. m_visual_beep_timer->restart(200);
  863. m_visual_beep_timer->set_single_shot(true);
  864. m_visual_beep_timer->on_timeout = [this] {
  865. update();
  866. };
  867. update();
  868. }
  869. void TerminalWidget::emit(const u8* data, size_t size)
  870. {
  871. if (write(m_ptm_fd, data, size) < 0) {
  872. perror("TerminalWidget::emit: write");
  873. }
  874. }
  875. void TerminalWidget::set_cursor_style(CursorStyle style)
  876. {
  877. switch (style) {
  878. case None:
  879. m_cursor_blink_timer->stop();
  880. m_cursor_blink_state = false;
  881. break;
  882. case SteadyBlock:
  883. case SteadyUnderline:
  884. case SteadyBar:
  885. m_cursor_blink_timer->stop();
  886. m_cursor_blink_state = true;
  887. break;
  888. case BlinkingBlock:
  889. case BlinkingUnderline:
  890. case BlinkingBar:
  891. m_cursor_blink_state = true;
  892. m_cursor_blink_timer->restart();
  893. break;
  894. default:
  895. dbgln("Cursor style not implemented");
  896. }
  897. m_cursor_style = style;
  898. invalidate_cursor();
  899. }
  900. void TerminalWidget::context_menu_event(GUI::ContextMenuEvent& event)
  901. {
  902. if (m_hovered_href_id.is_null()) {
  903. m_context_menu->popup(event.screen_position());
  904. } else {
  905. m_context_menu_href = m_hovered_href;
  906. // Ask LaunchServer for a list of programs that can handle the right-clicked URL.
  907. auto handlers = Desktop::Launcher::get_handlers_for_url(m_hovered_href);
  908. if (handlers.is_empty()) {
  909. m_context_menu->popup(event.screen_position());
  910. return;
  911. }
  912. m_context_menu_for_hyperlink = GUI::Menu::construct();
  913. RefPtr<GUI::Action> context_menu_default_action;
  914. // Go through the list of handlers and see if we can find a nice display name + icon for them.
  915. // Then add them to the context menu.
  916. // FIXME: Adapt this code when we actually support calling LaunchServer with a specific handler in mind.
  917. for (auto& handler : handlers) {
  918. auto af = Desktop::AppFile::get_for_app(LexicalPath(handler).basename());
  919. if (!af->is_valid())
  920. continue;
  921. auto action = GUI::Action::create(String::formatted("&Open in {}", af->name()), af->icon().bitmap_for_size(16), [this, handler](auto&) {
  922. Desktop::Launcher::open(m_context_menu_href, handler);
  923. });
  924. if (context_menu_default_action.is_null()) {
  925. context_menu_default_action = action;
  926. }
  927. m_context_menu_for_hyperlink->add_action(action);
  928. }
  929. m_context_menu_for_hyperlink->add_action(GUI::Action::create("Copy &URL", [this](auto&) {
  930. GUI::Clipboard::the().set_plain_text(m_context_menu_href);
  931. }));
  932. m_context_menu_for_hyperlink->add_action(GUI::Action::create("Copy &Name", [&](auto&) {
  933. // file://courage/home/anon/something -> /home/anon/something
  934. auto path = URL(m_context_menu_href).path();
  935. // /home/anon/something -> something
  936. auto name = LexicalPath(path).basename();
  937. GUI::Clipboard::the().set_plain_text(name);
  938. }));
  939. m_context_menu_for_hyperlink->add_separator();
  940. m_context_menu_for_hyperlink->add_action(copy_action());
  941. m_context_menu_for_hyperlink->add_action(paste_action());
  942. m_context_menu_for_hyperlink->popup(event.screen_position(), context_menu_default_action);
  943. }
  944. }
  945. void TerminalWidget::drop_event(GUI::DropEvent& event)
  946. {
  947. if (event.mime_data().has_text()) {
  948. event.accept();
  949. auto text = event.mime_data().text();
  950. write(m_ptm_fd, text.characters(), text.length());
  951. } else if (event.mime_data().has_urls()) {
  952. event.accept();
  953. auto urls = event.mime_data().urls();
  954. bool first = true;
  955. for (auto& url : event.mime_data().urls()) {
  956. if (!first) {
  957. write(m_ptm_fd, " ", 1);
  958. first = false;
  959. }
  960. if (url.protocol() == "file")
  961. write(m_ptm_fd, url.path().characters(), url.path().length());
  962. else
  963. write(m_ptm_fd, url.to_string().characters(), url.to_string().length());
  964. }
  965. }
  966. }
  967. void TerminalWidget::did_change_font()
  968. {
  969. GUI::Frame::did_change_font();
  970. m_line_height = font().glyph_height() + m_line_spacing;
  971. if (!size().is_empty())
  972. relayout(size());
  973. }
  974. void TerminalWidget::clear_including_history()
  975. {
  976. m_terminal.clear_including_history();
  977. }
  978. void TerminalWidget::scroll_to_bottom()
  979. {
  980. m_scrollbar->set_value(m_scrollbar->max());
  981. }
  982. void TerminalWidget::scroll_to_row(int row)
  983. {
  984. m_scrollbar->set_value(row);
  985. }
  986. void TerminalWidget::update_copy_action()
  987. {
  988. m_copy_action->set_enabled(has_selection());
  989. }
  990. void TerminalWidget::update_paste_action()
  991. {
  992. m_paste_action->set_enabled(GUI::Clipboard::the().mime_type().starts_with("text/") && !GUI::Clipboard::the().data().is_empty());
  993. }
  994. Gfx::IntSize TerminalWidget::widget_size_for_font(const Gfx::Font& font) const
  995. {
  996. return {
  997. (frame_thickness() * 2) + (m_inset * 2) + (m_terminal.columns() * font.glyph_width('x')) + m_scrollbar->width(),
  998. (frame_thickness() * 2) + (m_inset * 2) + (m_terminal.rows() * (font.glyph_height() + m_line_spacing))
  999. };
  1000. }
  1001. void TerminalWidget::set_font_and_resize_to_fit(const Gfx::Font& font)
  1002. {
  1003. set_font(font);
  1004. resize(widget_size_for_font(font));
  1005. }
  1006. }