1 /*
2 * Copyright (C) 2001-2004,2009,2010 Red Hat, Inc.
3 * Copyright © 2008, 2009, 2010 Christian Persch
4 *
5 * This library is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU Lesser General Public License as published
7 * by the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU Lesser General Public License for more details.
14 *
15 * You should have received a copy of the GNU Lesser General Public License
16 * along with this library. If not, see <https://www.gnu.org/licenses/>.
17 */
18
19 #include "config.h"
20
21 #include <math.h>
22 #include <search.h>
23 #include <stdlib.h>
24 #include <string.h>
25 #include <sys/ioctl.h>
26 #include <errno.h>
27 #include <fcntl.h>
28 #ifdef HAVE_SYS_TERMIOS_H
29 #include <sys/termios.h>
30 #endif
31 #ifdef HAVE_STROPTS_H
32 #include <stropts.h>
33 #endif
34 #ifdef HAVE_SYS_STREAM_H
35 #include <sys/stream.h>
36 #endif
37 #ifdef HAVE_TERMIOS_H
38 #include <termios.h>
39 #endif
40
41 #include <glib.h>
42 #include <glib-unix.h>
43 #include <glib/gi18n-lib.h>
44
45 #include <vte/vte.h>
46 #include "vteinternal.hh"
47 #include "bidi.hh"
48 #include "buffer.h"
49 #include "debug.h"
50 #include "reaper.hh"
51 #include "ring.hh"
52 #include "ringview.hh"
53 #include "caps.hh"
54 #include "widget.hh"
55
56 #ifdef HAVE_WCHAR_H
57 #include <wchar.h>
58 #endif
59 #ifdef HAVE_SYS_SYSLIMITS_H
60 #include <sys/syslimits.h>
61 #endif
62 #ifdef HAVE_SYS_WAIT_H
63 #include <sys/wait.h>
64 #endif
65 #include <glib.h>
66 #include <glib-object.h>
67 #include <gdk/gdk.h>
68 #include <gtk/gtk.h>
69 #include <pango/pango.h>
70 #include "keymap.h"
71 #include "marshal.h"
72 #include "vtepty.h"
73 #include "vtegtk.hh"
74 #include "cxx-utils.hh"
75 #include "gobject-glue.hh"
76
77 #ifdef WITH_A11Y
78 #include "vteaccess.h"
79 #endif
80
81 #include <new> /* placement new */
82
83 using namespace std::literals;
84
85 #ifndef HAVE_ROUND
round(double x)86 static inline double round(double x) {
87 if(x - floor(x) < 0.5) {
88 return floor(x);
89 } else {
90 return ceil(x);
91 }
92 }
93 #endif
94
95 #define WORD_CHAR_EXCEPTIONS_DEFAULT "-#%&+,./=?@\\_~\302\267"sv
96
97 #define I_(string) (g_intern_static_string(string))
98
99 #define VTE_DRAW_OPAQUE (1.0)
100
101 namespace vte {
102 namespace terminal {
103
104 static int _vte_unichar_width(gunichar c, int utf8_ambiguous_width);
105 static void stop_processing(vte::terminal::Terminal* that);
106 static void add_process_timeout(vte::terminal::Terminal* that);
107 static void add_update_timeout(vte::terminal::Terminal* that);
108 static void remove_update_timeout(vte::terminal::Terminal* that);
109
110 static gboolean process_timeout (gpointer data) noexcept;
111 static gboolean update_timeout (gpointer data) noexcept;
112 static cairo_region_t *vte_cairo_get_clip_region (cairo_t *cr);
113
114 /* these static variables are guarded by the GDK mutex */
115 static guint process_timeout_tag = 0;
116 static gboolean in_process_timeout;
117 static guint update_timeout_tag = 0;
118 static gboolean in_update_timeout;
119 static GList *g_active_terminals;
120
121 class Terminal::ProcessingContext {
122 public:
123 vte::grid::row_t m_bbox_top{-G_MAXINT};
124 vte::grid::row_t m_bbox_bottom{G_MAXINT};
125 bool m_modified{false};
126 bool m_bottom{false};
127 bool m_invalidated_text{false};
128 bool m_in_scroll_region{false};
129 bool m_saved_cursor_visible{false};
130 CursorStyle m_saved_cursor_style;
131 VteVisualPosition m_saved_cursor;
132 VteScreen const* m_saved_screen{nullptr};
133
ProcessingContext(Terminal const & terminal)134 ProcessingContext(Terminal const& terminal) noexcept
135 {
136 auto screen = m_saved_screen = terminal.m_screen;
137
138 // FIXMEchpe make this a method on VteScreen
139 m_bottom = screen->insert_delta == (long)screen->scroll_delta;
140
141 /* Save the current cursor position. */
142 m_saved_cursor = screen->cursor;
143 m_saved_cursor_visible = terminal.m_modes_private.DEC_TEXT_CURSOR();
144 m_saved_cursor_style = terminal.m_cursor_style;
145
146 m_in_scroll_region = terminal.m_scrolling_restricted
147 && (screen->cursor.row >= (screen->insert_delta + terminal.m_scrolling_region.start))
148 && (screen->cursor.row <= (screen->insert_delta + terminal.m_scrolling_region.end));
149
150 //context.modified = false;
151 //context.invalidated_text = false;
152
153 //context.bbox_bottom = -G_MAXINT;
154 //context.bbox_top = G_MAXINT;
155 }
156
157 ~ProcessingContext() = default;
158
159 ProcessingContext(ProcessingContext const&) = delete;
160 ProcessingContext(ProcessingContext&&) = delete;
161
162 ProcessingContext& operator=(ProcessingContext const&) = delete;
163 ProcessingContext& operator=(ProcessingContext&&) = delete;
164
165 [[gnu::always_inline]]
pre_GRAPHIC(Terminal const & terminal)166 inline void pre_GRAPHIC(Terminal const& terminal) noexcept
167 {
168 m_bbox_top = std::min(m_bbox_top,
169 terminal.m_screen->cursor.row);
170 }
171
172 [[gnu::always_inline]]
post_GRAPHIC(Terminal & terminal)173 inline void post_GRAPHIC(Terminal& terminal) noexcept
174 {
175 auto const* screen = terminal.m_screen;
176
177 if (terminal.m_line_wrapped) {
178 terminal.m_line_wrapped = false;
179 /* line wrapped, correct bbox */
180 if (m_invalidated_text &&
181 (screen->cursor.row > m_bbox_bottom + VTE_CELL_BBOX_SLACK ||
182 screen->cursor.row < m_bbox_top - VTE_CELL_BBOX_SLACK)) {
183 terminal.invalidate_rows_and_context(m_bbox_top, m_bbox_bottom);
184 m_bbox_bottom = -G_MAXINT;
185 m_bbox_top = G_MAXINT;
186 }
187 m_bbox_top = std::min(m_bbox_top,
188 screen->cursor.row);
189 }
190 /* Add the cells over which we have moved to the region
191 * which we need to refresh for the user. */
192 m_bbox_bottom = std::max(m_bbox_bottom,
193 screen->cursor.row);
194
195 m_invalidated_text = true;
196 m_modified = true;
197 }
198
199 [[gnu::always_inline]]
post_CMD(Terminal & terminal)200 inline void post_CMD(Terminal& terminal) noexcept
201 {
202 m_modified = true;
203
204 // FIXME terminal.m_screen may be != m_saved_screen, check for that!
205
206 auto const* screen = terminal.m_screen;
207 auto const new_in_scroll_region = terminal.m_scrolling_restricted &&
208 (screen->cursor.row >= (screen->insert_delta + terminal.m_scrolling_region.start)) &&
209 (screen->cursor.row <= (screen->insert_delta + terminal.m_scrolling_region.end));
210
211 /* if we have moved greatly during the sequence handler, or moved
212 * into a scroll_region from outside it, restart the bbox.
213 */
214 if (m_invalidated_text &&
215 ((new_in_scroll_region && !m_in_scroll_region) ||
216 (screen->cursor.row > m_bbox_bottom + VTE_CELL_BBOX_SLACK ||
217 screen->cursor.row < m_bbox_top - VTE_CELL_BBOX_SLACK))) {
218 terminal.invalidate_rows_and_context(m_bbox_top, m_bbox_bottom);
219 m_invalidated_text = false;
220 m_bbox_bottom = -G_MAXINT;
221 m_bbox_top = G_MAXINT;
222 }
223
224 m_in_scroll_region = new_in_scroll_region;
225 }
226
227 }; // class ProcessingContext
228
229 static int
_vte_unichar_width(gunichar c,int utf8_ambiguous_width)230 _vte_unichar_width(gunichar c, int utf8_ambiguous_width)
231 {
232 if (G_LIKELY (c < 0x80))
233 return 1;
234 if (G_UNLIKELY (g_unichar_iszerowidth (c)))
235 return 0;
236 if (G_UNLIKELY (g_unichar_iswide (c)))
237 return 2;
238 if (G_LIKELY (utf8_ambiguous_width == 1))
239 return 1;
240 if (G_UNLIKELY (g_unichar_iswide_cjk (c)))
241 return 2;
242 return 1;
243 }
244
245 static void
vte_g_array_fill(GArray * array,gconstpointer item,guint final_size)246 vte_g_array_fill(GArray *array, gconstpointer item, guint final_size)
247 {
248 if (array->len >= final_size)
249 return;
250
251 final_size -= array->len;
252 do {
253 g_array_append_vals(array, item, 1);
254 } while (--final_size);
255 }
256
257 void
unset_widget()258 Terminal::unset_widget() noexcept
259 {
260 m_real_widget = nullptr;
261 m_terminal = nullptr;
262 m_widget = nullptr;
263 }
264
265 // FIXMEchpe replace this with a method on VteRing
266 VteRowData*
ring_insert(vte::grid::row_t position,bool fill)267 Terminal::ring_insert(vte::grid::row_t position,
268 bool fill)
269 {
270 VteRowData *row;
271 VteRing *ring = m_screen->row_data;
272 bool const not_default_bg = (m_color_defaults.attr.back() != VTE_DEFAULT_BG);
273
274 while (G_UNLIKELY (_vte_ring_next (ring) < position)) {
275 row = _vte_ring_append (ring, get_bidi_flags());
276 if (not_default_bg)
277 _vte_row_data_fill (row, &m_color_defaults, m_column_count);
278 }
279 row = _vte_ring_insert (ring, position, get_bidi_flags());
280 if (fill && not_default_bg)
281 _vte_row_data_fill (row, &m_color_defaults, m_column_count);
282 return row;
283 }
284
285 // FIXMEchpe replace this with a method on VteRing
286 VteRowData*
ring_append(bool fill)287 Terminal::ring_append(bool fill)
288 {
289 return ring_insert(_vte_ring_next(m_screen->row_data), fill);
290 }
291
292 // FIXMEchpe replace this with a method on VteRing
293 void
ring_remove(vte::grid::row_t position)294 Terminal::ring_remove(vte::grid::row_t position)
295 {
296 _vte_ring_remove(m_screen->row_data, position);
297 }
298
299 /* Reset defaults for character insertion. */
300 void
reset_default_attributes(bool reset_hyperlink)301 Terminal::reset_default_attributes(bool reset_hyperlink)
302 {
303 auto const hyperlink_idx_save = m_defaults.attr.hyperlink_idx;
304 m_defaults = m_color_defaults = basic_cell;
305 if (!reset_hyperlink)
306 m_defaults.attr.hyperlink_idx = hyperlink_idx_save;
307 }
308
309 //FIXMEchpe this function is bad
310 inline vte::view::coord_t
scroll_delta_pixel() const311 Terminal::scroll_delta_pixel() const
312 {
313 return round(m_screen->scroll_delta * m_cell_height);
314 }
315
316 /*
317 * Terminal::pixel_to_row:
318 * @y: Y coordinate is relative to viewport, top padding excluded
319 *
320 * Returns: absolute row
321 */
322 inline vte::grid::row_t
pixel_to_row(vte::view::coord_t y) const323 Terminal::pixel_to_row(vte::view::coord_t y) const
324 {
325 return (scroll_delta_pixel() + y) / m_cell_height;
326 }
327
328 /*
329 * Terminal::pixel_to_row:
330 * @row: absolute row
331 *
332 * Returns: Y coordinate relative to viewport with top padding excluded. If the row is
333 * outside the viewport, may return any value < 0 or >= height
334 */
335 inline vte::view::coord_t
row_to_pixel(vte::grid::row_t row) const336 Terminal::row_to_pixel(vte::grid::row_t row) const
337 {
338 // FIXMEchpe this is bad!
339 return row * m_cell_height - (glong)round(m_screen->scroll_delta * m_cell_height);
340 }
341
342 inline vte::grid::row_t
first_displayed_row() const343 Terminal::first_displayed_row() const
344 {
345 return pixel_to_row(0);
346 }
347
348 inline vte::grid::row_t
last_displayed_row() const349 Terminal::last_displayed_row() const
350 {
351 /* Get the logical row number displayed at the bottom pixel position */
352 auto r = pixel_to_row(m_view_usable_extents.height() - 1);
353
354 /* If we have an extra padding at the bottom which is currently unused,
355 * this number is one too big. Adjust here.
356 * E.g. have a terminal of size 80 x 24.5.
357 * Initially the bottom displayed row is (0-based) 23, but r is now 24.
358 * After producing more than a screenful of content and scrolling back
359 * all the way to the top, the bottom displayed row is (0-based) 24. */
360 r = MIN (r, m_screen->insert_delta + m_row_count - 1);
361 return r;
362 }
363
364 /* Checks if the cursor is potentially at least partially onscreen.
365 * An outline cursor has an additional height of VTE_LINE_WIDTH pixels.
366 * It's also intentionally painted over the padding, up to VTE_LINE_WIDTH
367 * pixels under the real contents area. This method takes these into account.
368 * Only checks the cursor's row; not its visibility, shape, or offscreen column.
369 */
370 inline bool
cursor_is_onscreen() const371 Terminal::cursor_is_onscreen() const noexcept
372 {
373 /* Note: the cursor can only be offscreen below the visible area, not above. */
374 auto cursor_top = row_to_pixel (m_screen->cursor.row) - VTE_LINE_WIDTH;
375 auto display_bottom = m_view_usable_extents.height() + MIN(m_padding.bottom, VTE_LINE_WIDTH);
376 return cursor_top < display_bottom;
377 }
378
379 /* Invalidate the requested rows. This is to be used when only the desired
380 * rendering changes but not the underlying data, e.g. moving or blinking
381 * cursor, highligthing with the mouse etc.
382 *
383 * Note that row_end is inclusive. This is not as nice as end-exclusive,
384 * but saves us from a +1 almost everywhere where this method is called.
385 */
386 void
invalidate_rows(vte::grid::row_t row_start,vte::grid::row_t row_end)387 Terminal::invalidate_rows(vte::grid::row_t row_start,
388 vte::grid::row_t row_end /* inclusive */)
389 {
390 if (G_UNLIKELY (!widget_realized()))
391 return;
392
393 if (m_invalidated_all)
394 return;
395
396 if (G_UNLIKELY (row_end < row_start))
397 return;
398
399 _vte_debug_print (VTE_DEBUG_UPDATES,
400 "Invalidating rows %ld..%ld.\n",
401 row_start, row_end);
402 _vte_debug_print (VTE_DEBUG_WORK, "?");
403
404 /* Scrolled back, visible parts didn't change. */
405 if (row_start > last_displayed_row())
406 return;
407
408 /* Recognize if we're about to invalidate everything. */
409 if (row_start <= first_displayed_row() &&
410 row_end >= last_displayed_row()) {
411 invalidate_all();
412 return;
413 }
414
415 cairo_rectangle_int_t rect;
416 /* Convert the column and row start and end to pixel values
417 * by multiplying by the size of a character cell.
418 * Always include the extra pixel border and overlap pixel.
419 */
420 // FIXMEegmont invalidate the left and right padding too
421 rect.x = -1;
422 int xend = m_column_count * m_cell_width + 1;
423 rect.width = xend - rect.x;
424
425 /* Always add at least VTE_LINE_WIDTH pixels so the outline block cursor fits */
426 rect.y = row_to_pixel(row_start) - std::max(cell_overflow_top(), VTE_LINE_WIDTH);
427 int yend = row_to_pixel(row_end + 1) + std::max(cell_overflow_bottom(), VTE_LINE_WIDTH);
428 rect.height = yend - rect.y;
429
430 _vte_debug_print (VTE_DEBUG_UPDATES,
431 "Invalidating pixels at (%d,%d)x(%d,%d).\n",
432 rect.x, rect.y, rect.width, rect.height);
433
434 if (m_active_terminals_link != nullptr) {
435 g_array_append_val(m_update_rects, rect);
436 /* Wait a bit before doing any invalidation, just in
437 * case updates are coming in really soon. */
438 add_update_timeout(this);
439 } else {
440 auto allocation = get_allocated_rect();
441 rect.x += allocation.x + m_padding.left;
442 rect.y += allocation.y + m_padding.top;
443 cairo_region_t *region = cairo_region_create_rectangle(&rect);
444 gtk_widget_queue_draw_region(m_widget, region);
445 cairo_region_destroy(region);
446 }
447
448 _vte_debug_print (VTE_DEBUG_WORK, "!");
449 }
450
451 /* Invalidate the requested rows, extending the region in both directions up to
452 * an explicit newline (or a safety limit) to invalidate entire paragraphs of text.
453 * This is to be used whenever the underlying data changes, because any such
454 * change might alter the desired BiDi, syntax highlighting etc. of all other
455 * rows of the involved paragraph(s).
456 *
457 * Note that row_end is inclusive. This is not as nice as end-exclusive,
458 * but saves us from a +1 almost everywhere where this method is called.
459 */
460 void
invalidate_rows_and_context(vte::grid::row_t row_start,vte::grid::row_t row_end)461 Terminal::invalidate_rows_and_context(vte::grid::row_t row_start,
462 vte::grid::row_t row_end /* inclusive */)
463 {
464 if (G_UNLIKELY (!widget_realized()))
465 return;
466
467 if (m_invalidated_all)
468 return;
469
470 if (G_UNLIKELY (row_end < row_start))
471 return;
472
473 _vte_debug_print (VTE_DEBUG_UPDATES,
474 "Invalidating rows %ld..%ld and context.\n",
475 row_start, row_end);
476
477 /* Safety limit: Scrolled back by so much that changes to the
478 * writable area may not affect the current viewport's rendering. */
479 if (m_screen->insert_delta - VTE_RINGVIEW_PARAGRAPH_LENGTH_MAX > last_displayed_row())
480 return;
481
482 /* Extending the start is a bit tricky.
483 * First extend it (towards lower numbered indices), but only up to
484 * insert_delta - 1. Remember that the row at insert_delta - 1 is
485 * still in the ring, hence checking its soft_wrapped flag is fast. */
486 while (row_start >= m_screen->insert_delta) {
487 if (!m_screen->row_data->is_soft_wrapped(row_start - 1))
488 break;
489 row_start--;
490 }
491
492 /* If we haven't seen a newline yet, stop walking backwards row by row.
493 * This is because we might need to access row_stream in order to check
494 * the wrapped state, a way too expensive operation while processing
495 * incoming data. Let displaying do extra work instead.
496 * So just invalidate everything to the top. */
497 if (row_start < m_screen->insert_delta) {
498 row_start = first_displayed_row();
499 }
500
501 /* Extending the end is simple. Just walk until we go offscreen or
502 * find an explicit newline. */
503 while (row_end < last_displayed_row()) {
504 if (!m_screen->row_data->is_soft_wrapped(row_end))
505 break;
506 row_end++;
507 }
508
509 invalidate_rows(row_start, row_end);
510 }
511
512 /* Convenience methods */
513 void
invalidate_row(vte::grid::row_t row)514 Terminal::invalidate_row(vte::grid::row_t row)
515 {
516 invalidate_rows(row, row);
517 }
518
519 void
invalidate_row_and_context(vte::grid::row_t row)520 Terminal::invalidate_row_and_context(vte::grid::row_t row)
521 {
522 invalidate_rows_and_context(row, row);
523 }
524
525 /* This is only used by the selection code, so no need to extend the area. */
526 void
invalidate(vte::grid::span const & s)527 Terminal::invalidate(vte::grid::span const& s)
528 {
529 if (!s.empty())
530 invalidate_rows(s.start_row(), s.last_row());
531 }
532
533 /* Invalidates the symmetrical difference ("XOR" area) of the two spans.
534 * This is only used by the selection code, so no need to extend the area. */
535 void
invalidate_symmetrical_difference(vte::grid::span const & a,vte::grid::span const & b,bool block)536 Terminal::invalidate_symmetrical_difference(vte::grid::span const& a, vte::grid::span const& b, bool block)
537 {
538 if (a.empty() || b.empty() || a.start() >= b.end() || b.start() >= a.end()) {
539 /* One or both are empty (invalidate() will figure out which), or disjoint intervals. */
540 invalidate (a);
541 invalidate (b);
542 return;
543 }
544
545 if (block) {
546 /* We could optimize when the columns don't change, probably not worth it. */
547 invalidate_rows (std::min (a.start_row(), b.start_row()),
548 std::max (a.last_row(), b.last_row()));
549 } else {
550 if (a.start() != b.start()) {
551 invalidate_rows (std::min (a.start_row(), b.start_row()),
552 std::max (a.start_row(), b.start_row()));
553 }
554 if (a.end() != b.end()) {
555 invalidate_rows (std::min (a.last_row(), b.last_row()),
556 std::max (a.last_row(), b.last_row()));
557 }
558 }
559 }
560
561 void
invalidate_all()562 Terminal::invalidate_all()
563 {
564 if (G_UNLIKELY (!widget_realized()))
565 return;
566
567 if (m_invalidated_all) {
568 return;
569 }
570
571 _vte_debug_print (VTE_DEBUG_WORK, "*");
572 _vte_debug_print (VTE_DEBUG_UPDATES, "Invalidating all.\n");
573
574 /* replace invalid regions with one covering the whole terminal */
575 reset_update_rects();
576 m_invalidated_all = TRUE;
577
578 if (m_active_terminals_link != nullptr) {
579 auto allocation = get_allocated_rect();
580 cairo_rectangle_int_t rect;
581 rect.x = -m_padding.left;
582 rect.y = -m_padding.top;
583 rect.width = allocation.width;
584 rect.height = allocation.height;
585
586 g_array_append_val(m_update_rects, rect);
587 /* Wait a bit before doing any invalidation, just in
588 * case updates are coming in really soon. */
589 add_update_timeout(this);
590 } else {
591 gtk_widget_queue_draw(m_widget);
592 }
593 }
594
595 /* Find the row in the given position in the backscroll buffer.
596 * Note that calling this method may invalidate the return value of
597 * a previous find_row_data() call. */
598 // FIXMEchpe replace this with a method on VteRing
599 VteRowData const*
find_row_data(vte::grid::row_t row) const600 Terminal::find_row_data(vte::grid::row_t row) const
601 {
602 VteRowData const* rowdata = nullptr;
603
604 if (G_LIKELY(_vte_ring_contains(m_screen->row_data, row))) {
605 rowdata = _vte_ring_index(m_screen->row_data, row);
606 }
607 return rowdata;
608 }
609
610 /* Find the row in the given position in the backscroll buffer. */
611 // FIXMEchpe replace this with a method on VteRing
612 VteRowData*
find_row_data_writable(vte::grid::row_t row) const613 Terminal::find_row_data_writable(vte::grid::row_t row) const
614 {
615 VteRowData *rowdata = nullptr;
616
617 if (G_LIKELY (_vte_ring_contains(m_screen->row_data, row))) {
618 rowdata = _vte_ring_index_writable(m_screen->row_data, row);
619 }
620 return rowdata;
621 }
622
623 /* Find the character an the given position in the backscroll buffer.
624 * Note that calling this method may invalidate the return value of
625 * a previous find_row_data() call. */
626 // FIXMEchpe replace this with a method on VteRing
627 VteCell const*
find_charcell(vte::grid::column_t col,vte::grid::row_t row) const628 Terminal::find_charcell(vte::grid::column_t col,
629 vte::grid::row_t row) const
630 {
631 VteRowData const* rowdata;
632 VteCell const* ret = nullptr;
633
634 if (_vte_ring_contains(m_screen->row_data, row)) {
635 rowdata = _vte_ring_index(m_screen->row_data, row);
636 ret = _vte_row_data_get (rowdata, col);
637 }
638 return ret;
639 }
640
641 // FIXMEchpe replace this with a method on VteRing
642 vte::grid::column_t
find_start_column(vte::grid::column_t col,vte::grid::row_t row) const643 Terminal::find_start_column(vte::grid::column_t col,
644 vte::grid::row_t row) const
645 {
646 VteRowData const* row_data = find_row_data(row);
647 if (G_UNLIKELY (col < 0))
648 return col;
649 if (row_data != nullptr) {
650 const VteCell *cell = _vte_row_data_get (row_data, col);
651 while (col > 0 && cell != NULL && cell->attr.fragment()) {
652 cell = _vte_row_data_get (row_data, --col);
653 }
654 }
655 return MAX(col, 0);
656 }
657
658 // FIXMEchpe replace this with a method on VteRing
659 vte::grid::column_t
find_end_column(vte::grid::column_t col,vte::grid::row_t row) const660 Terminal::find_end_column(vte::grid::column_t col,
661 vte::grid::row_t row) const
662 {
663 VteRowData const* row_data = find_row_data(row);
664 gint columns = 0;
665 if (G_UNLIKELY (col < 0))
666 return col;
667 if (row_data != NULL) {
668 const VteCell *cell = _vte_row_data_get (row_data, col);
669 while (col > 0 && cell != NULL && cell->attr.fragment()) {
670 cell = _vte_row_data_get (row_data, --col);
671 }
672 if (cell) {
673 columns = cell->attr.columns() - 1;
674 }
675 }
676 // FIXMEchp m__column_count - 1 ?
677 return MIN(col + columns, m_column_count);
678 }
679
680 /* Sets the line ending to hard wrapped (explicit newline).
681 * Takes care of invalidating if this operation splits a paragraph into two. */
682 void
set_hard_wrapped(vte::grid::row_t row)683 Terminal::set_hard_wrapped(vte::grid::row_t row)
684 {
685 /* We can set the row just above insert_delta to hard wrapped. */
686 g_assert_cmpint(row, >=, m_screen->insert_delta - 1);
687 g_assert_cmpint(row, <, m_screen->insert_delta + m_row_count);
688
689 VteRowData *row_data = find_row_data_writable(row);
690
691 /* It's okay for this row not to be covered by the ring. */
692 if (row_data == nullptr || !row_data->attr.soft_wrapped)
693 return;
694
695 row_data->attr.soft_wrapped = false;
696
697 m_ringview.invalidate();
698 invalidate_rows_and_context(row, row + 1);
699 }
700
701 /* Sets the line ending to soft wrapped (overflow to the next line).
702 * Takes care of invalidating if this operation joins two paragraphs into one.
703 * Also makes sure that the joined new paragraph receives the first one's bidi flags. */
704 void
set_soft_wrapped(vte::grid::row_t row)705 Terminal::set_soft_wrapped(vte::grid::row_t row)
706 {
707 g_assert_cmpint(row, >=, m_screen->insert_delta);
708 g_assert_cmpint(row, <, m_screen->insert_delta + m_row_count);
709
710 VteRowData *row_data = find_row_data_writable(row);
711 g_assert(row_data != nullptr);
712
713 if (row_data->attr.soft_wrapped)
714 return;
715
716 row_data->attr.soft_wrapped = true;
717
718 /* Each paragraph has to have consistent bidi flags across all of its rows.
719 * Spread the first paragraph's flags across the second one (if they differ). */
720 guint8 bidi_flags = row_data->attr.bidi_flags;
721 vte::grid::row_t i = row + 1;
722 row_data = find_row_data_writable(i);
723 if (row_data != nullptr && row_data->attr.bidi_flags != bidi_flags) {
724 do {
725 row_data->attr.bidi_flags = bidi_flags;
726 if (!row_data->attr.soft_wrapped)
727 break;
728 row_data = find_row_data_writable(++i);
729 } while (row_data != nullptr);
730 }
731
732 m_ringview.invalidate();
733 invalidate_rows_and_context(row, row + 1);
734 }
735
736 /* Determine the width of the portion of the preedit string which lies
737 * to the left of the cursor, or the entire string, in columns. */
738 // FIXMEchpe this is for the view, so use int not gssize
739 // FIXMEchpe this is only ever called with left_only=false, so remove the param
740 gssize
get_preedit_width(bool left_only)741 Terminal::get_preedit_width(bool left_only)
742 {
743 gssize ret = 0;
744
745 char const *preedit = m_im_preedit.c_str();
746 for (int i = 0;
747 // FIXMEchpe preddit is != NULL at the start, and next_char never returns NULL either
748 (preedit != NULL) &&
749 (preedit[0] != '\0') &&
750 (!left_only || (i < m_im_preedit_cursor));
751 i++) {
752 gunichar c = g_utf8_get_char(preedit);
753 ret += _vte_unichar_width(c, m_utf8_ambiguous_width);
754 preedit = g_utf8_next_char(preedit);
755 }
756
757 return ret;
758 }
759
760 /* Determine the length of the portion of the preedit string which lies
761 * to the left of the cursor, or the entire string, in gunichars. */
762 // FIXMEchpe this returns gssize but inside it uses int...
763 gssize
get_preedit_length(bool left_only)764 Terminal::get_preedit_length(bool left_only)
765 {
766 ssize_t i = 0;
767
768 char const *preedit = m_im_preedit.c_str();
769 for (i = 0;
770 // FIXMEchpe useless check, see above
771 (preedit != NULL) &&
772 (preedit[0] != '\0') &&
773 (!left_only || (i < m_im_preedit_cursor));
774 i++) {
775 preedit = g_utf8_next_char(preedit);
776 }
777
778 return i;
779 }
780
781 void
invalidate_cursor_once(bool periodic)782 Terminal::invalidate_cursor_once(bool periodic)
783 {
784 if (G_UNLIKELY(!widget_realized()))
785 return;
786
787 if (m_invalidated_all) {
788 return;
789 }
790
791 if (periodic) {
792 if (!m_cursor_blinks) {
793 return;
794 }
795 }
796
797 if (m_modes_private.DEC_TEXT_CURSOR()) {
798 auto row = m_screen->cursor.row;
799
800 _vte_debug_print(VTE_DEBUG_UPDATES,
801 "Invalidating cursor in row %ld.\n",
802 row);
803 invalidate_row(row);
804 }
805 }
806
807 /* Invalidate the cursor repeatedly. */
808 // FIXMEchpe this continually adds and removes the blink timeout. Find a better solution
809 bool
cursor_blink_timer_callback()810 Terminal::cursor_blink_timer_callback()
811 {
812 m_cursor_blink_state = !m_cursor_blink_state;
813 m_cursor_blink_time += m_cursor_blink_cycle;
814
815 invalidate_cursor_once(true);
816
817 /* only disable the blink if the cursor is currently shown.
818 * else, wait until next time.
819 */
820 if (m_cursor_blink_time / 1000 >= m_cursor_blink_timeout &&
821 m_cursor_blink_state) {
822 return false;
823 }
824
825 m_cursor_blink_timer.schedule(m_cursor_blink_cycle, vte::glib::Timer::Priority::eLOW);
826 return false;
827 }
828
829 /* Emit a "selection_changed" signal. */
830 void
emit_selection_changed()831 Terminal::emit_selection_changed()
832 {
833 _vte_debug_print(VTE_DEBUG_SIGNALS,
834 "Emitting `selection-changed'.\n");
835 g_signal_emit(m_terminal, signals[SIGNAL_SELECTION_CHANGED], 0);
836 }
837
838 /* Emit a "commit" signal.
839 * FIXMEchpe: remove this function
840 */
841 void
emit_commit(std::string_view const & str)842 Terminal::emit_commit(std::string_view const& str)
843 {
844 if (str.size() == 0)
845 return;
846
847 if (!widget() || !widget()->should_emit_signal(SIGNAL_COMMIT))
848 return;
849
850 _vte_debug_print(VTE_DEBUG_SIGNALS,
851 "Emitting `commit' of %" G_GSSIZE_FORMAT" bytes.\n", str.size());
852
853 // FIXMEchpe we do know for a fact that all uses of this function
854 // actually passed a 0-terminated string, so we can use @str directly
855 std::string result{str}; // 0-terminated
856
857 _VTE_DEBUG_IF(VTE_DEBUG_KEYBOARD) {
858 for (size_t i = 0; i < result.size(); i++) {
859 if ((((guint8) result[i]) < 32) ||
860 (((guint8) result[i]) > 127)) {
861 g_printerr(
862 "Sending <%02x> "
863 "to child.\n",
864 result[i]);
865 } else {
866 g_printerr(
867 "Sending '%c' "
868 "to child.\n",
869 result[i]);
870 }
871 }
872 }
873
874 g_signal_emit(m_terminal, signals[SIGNAL_COMMIT], 0, result.c_str(), (guint)result.size());
875 }
876
877 void
queue_contents_changed()878 Terminal::queue_contents_changed()
879 {
880 _vte_debug_print(VTE_DEBUG_SIGNALS,
881 "Queueing `contents-changed'.\n");
882 m_contents_changed_pending = true;
883 }
884
885 //FIXMEchpe this has only one caller
886 void
queue_cursor_moved()887 Terminal::queue_cursor_moved()
888 {
889 _vte_debug_print(VTE_DEBUG_SIGNALS,
890 "Queueing `cursor-moved'.\n");
891 m_cursor_moved_pending = true;
892 }
893
894 void
emit_eof()895 Terminal::emit_eof()
896 {
897 if (widget())
898 widget()->emit_eof();
899 }
900
901 static gboolean
emit_eof_idle_cb(VteTerminal * terminal)902 emit_eof_idle_cb(VteTerminal *terminal)
903 try
904 {
905 _vte_terminal_get_impl(terminal)->emit_eof();
906
907 return G_SOURCE_REMOVE;
908 }
909 catch (...)
910 {
911 vte::log_exception();
912 return G_SOURCE_REMOVE;
913 }
914
915 void
queue_eof()916 Terminal::queue_eof()
917 {
918 _vte_debug_print(VTE_DEBUG_SIGNALS, "Queueing `eof'.\n");
919
920 g_idle_add_full(G_PRIORITY_HIGH,
921 (GSourceFunc)emit_eof_idle_cb,
922 g_object_ref(m_terminal),
923 g_object_unref);
924 }
925
926 void
emit_child_exited()927 Terminal::emit_child_exited()
928 {
929 auto const status = m_child_exit_status;
930 m_child_exit_status = -1;
931
932 if (widget())
933 widget()->emit_child_exited(status);
934 }
935
936 static gboolean
emit_child_exited_idle_cb(VteTerminal * terminal)937 emit_child_exited_idle_cb(VteTerminal *terminal)
938 try
939 {
940 _vte_terminal_get_impl(terminal)->emit_child_exited();
941
942 return G_SOURCE_REMOVE;
943 }
944 catch (...)
945 {
946 vte::log_exception();
947 return G_SOURCE_REMOVE;
948 }
949
950 /* Emit a "child-exited" signal on idle, so that if the handler destroys
951 * the terminal, we're not deep within terminal code callstack
952 */
953 void
queue_child_exited()954 Terminal::queue_child_exited()
955 {
956 _vte_debug_print(VTE_DEBUG_SIGNALS, "Queueing `child-exited'.\n");
957 m_child_exited_after_eos_pending = false;
958
959 g_idle_add_full(G_PRIORITY_HIGH,
960 (GSourceFunc)emit_child_exited_idle_cb,
961 g_object_ref(m_terminal),
962 g_object_unref);
963 }
964
965 bool
child_exited_eos_wait_callback()966 Terminal::child_exited_eos_wait_callback()
967 {
968 /* If we get this callback, there has been some time elapsed
969 * after child-exited, but no EOS yet. This happens for example
970 * when the primary child started other processes in the background,
971 * which inherited the PTY, and thus keep it open, see
972 * https://gitlab.gnome.org/GNOME/vte/issues/204
973 *
974 * Force an EOS.
975 */
976 if (pty())
977 pty_io_read(pty()->fd(), G_IO_HUP);
978
979 return false; // don't run again
980 }
981
982 /* Emit a "char-size-changed" signal. */
983 void
emit_char_size_changed(int width,int height)984 Terminal::emit_char_size_changed(int width,
985 int height)
986 {
987 _vte_debug_print(VTE_DEBUG_SIGNALS,
988 "Emitting `char-size-changed'.\n");
989 /* FIXME on next API break, change the signature */
990 g_signal_emit(m_terminal, signals[SIGNAL_CHAR_SIZE_CHANGED], 0,
991 (guint)width, (guint)height);
992 }
993
994 /* Emit an "increase-font-size" signal. */
995 void
emit_increase_font_size()996 Terminal::emit_increase_font_size()
997 {
998 _vte_debug_print(VTE_DEBUG_SIGNALS,
999 "Emitting `increase-font-size'.\n");
1000 g_signal_emit(m_terminal, signals[SIGNAL_INCREASE_FONT_SIZE], 0);
1001 }
1002
1003 /* Emit a "decrease-font-size" signal. */
1004 void
emit_decrease_font_size()1005 Terminal::emit_decrease_font_size()
1006 {
1007 _vte_debug_print(VTE_DEBUG_SIGNALS,
1008 "Emitting `decrease-font-size'.\n");
1009 g_signal_emit(m_terminal, signals[SIGNAL_DECREASE_FONT_SIZE], 0);
1010 }
1011
1012 /* Emit a "text-inserted" signal. */
1013 void
emit_text_inserted()1014 Terminal::emit_text_inserted()
1015 {
1016 #ifdef WITH_A11Y
1017 if (!m_accessible_emit) {
1018 return;
1019 }
1020 _vte_debug_print(VTE_DEBUG_SIGNALS,
1021 "Emitting `text-inserted'.\n");
1022 g_signal_emit(m_terminal, signals[SIGNAL_TEXT_INSERTED], 0);
1023 #endif
1024 }
1025
1026 /* Emit a "text-deleted" signal. */
1027 void
emit_text_deleted()1028 Terminal::emit_text_deleted()
1029 {
1030 #ifdef WITH_A11Y
1031 if (!m_accessible_emit) {
1032 return;
1033 }
1034 _vte_debug_print(VTE_DEBUG_SIGNALS,
1035 "Emitting `text-deleted'.\n");
1036 g_signal_emit(m_terminal, signals[SIGNAL_TEXT_DELETED], 0);
1037 #endif
1038 }
1039
1040 /* Emit a "text-modified" signal. */
1041 void
emit_text_modified()1042 Terminal::emit_text_modified()
1043 {
1044 #ifdef WITH_A11Y
1045 if (!m_accessible_emit) {
1046 return;
1047 }
1048 _vte_debug_print(VTE_DEBUG_SIGNALS,
1049 "Emitting `text-modified'.\n");
1050 g_signal_emit(m_terminal, signals[SIGNAL_TEXT_MODIFIED], 0);
1051 #endif
1052 }
1053
1054 /* Emit a "text-scrolled" signal. */
1055 void
emit_text_scrolled(long delta)1056 Terminal::emit_text_scrolled(long delta)
1057 {
1058 #ifdef WITH_A11Y
1059 if (!m_accessible_emit) {
1060 return;
1061 }
1062 _vte_debug_print(VTE_DEBUG_SIGNALS,
1063 "Emitting `text-scrolled'(%ld).\n", delta);
1064 // FIXMEchpe fix signal signature?
1065 g_signal_emit(m_terminal, signals[SIGNAL_TEXT_SCROLLED], 0, (int)delta);
1066 #endif
1067 }
1068
1069 void
emit_copy_clipboard()1070 Terminal::emit_copy_clipboard()
1071 {
1072 _vte_debug_print(VTE_DEBUG_SIGNALS, "Emitting 'copy-clipboard'.\n");
1073 g_signal_emit(m_terminal, signals[SIGNAL_COPY_CLIPBOARD], 0);
1074 }
1075
1076 void
emit_paste_clipboard()1077 Terminal::emit_paste_clipboard()
1078 {
1079 _vte_debug_print(VTE_DEBUG_SIGNALS, "Emitting 'paste-clipboard'.\n");
1080 g_signal_emit(m_terminal, signals[SIGNAL_PASTE_CLIPBOARD], 0);
1081 }
1082
1083 /* Emit a "hyperlink_hover_uri_changed" signal. */
1084 void
emit_hyperlink_hover_uri_changed(const GdkRectangle * bbox)1085 Terminal::emit_hyperlink_hover_uri_changed(const GdkRectangle *bbox)
1086 {
1087 GObject *object = G_OBJECT(m_terminal);
1088
1089 _vte_debug_print(VTE_DEBUG_SIGNALS,
1090 "Emitting `hyperlink-hover-uri-changed'.\n");
1091 g_signal_emit(m_terminal, signals[SIGNAL_HYPERLINK_HOVER_URI_CHANGED], 0, m_hyperlink_hover_uri, bbox);
1092 g_object_notify_by_pspec(object, pspecs[PROP_HYPERLINK_HOVER_URI]);
1093 }
1094
1095 void
deselect_all()1096 Terminal::deselect_all()
1097 {
1098 if (!m_selection_resolved.empty()) {
1099 _vte_debug_print(VTE_DEBUG_SELECTION,
1100 "Deselecting all text.\n");
1101
1102 m_selection_origin = m_selection_last = { -1, -1, 1 };
1103 resolve_selection();
1104
1105 /* Don't free the current selection, as we need to keep
1106 * hold of it for async copying from the clipboard. */
1107
1108 emit_selection_changed();
1109 }
1110 }
1111
1112 /* Clear the cache of the screen contents we keep. */
1113 void
match_contents_clear()1114 Terminal::match_contents_clear()
1115 {
1116 match_hilite_clear();
1117 if (m_match_contents != nullptr) {
1118 g_free(m_match_contents);
1119 m_match_contents = nullptr;
1120 }
1121 if (m_match_attributes != nullptr) {
1122 g_array_free(m_match_attributes, TRUE);
1123 m_match_attributes = nullptr;
1124 }
1125 }
1126
1127 void
match_contents_refresh()1128 Terminal::match_contents_refresh()
1129
1130 {
1131 match_contents_clear();
1132 GArray *array = g_array_new(FALSE, TRUE, sizeof(struct _VteCharAttributes));
1133 auto match_contents = get_text_displayed(true /* wrap */,
1134 array);
1135 m_match_contents = g_string_free(match_contents, FALSE);
1136 m_match_attributes = array;
1137 }
1138
1139 void
regex_match_remove_all()1140 Terminal::regex_match_remove_all() noexcept
1141 {
1142 auto& match_regexes = match_regexes_writable();
1143 match_regexes.clear();
1144 match_regexes.shrink_to_fit();
1145
1146 match_hilite_clear();
1147 }
1148
1149 void
regex_match_remove(int tag)1150 Terminal::regex_match_remove(int tag) noexcept
1151 {
1152 auto i = regex_match_get_iter(tag);
1153 if (i == std::end(m_match_regexes))
1154 return;
1155
1156 match_regexes_writable().erase(i);
1157 }
1158
1159 /*
1160 * match_rowcol_to_offset:
1161 * @terminal:
1162 * @column:
1163 * @row:
1164 * @offset_ptr: (out):
1165 * @sattr_ptr: (out):
1166 * @ettr_ptr: (out):
1167 *
1168 * Maps (row, column) to an offset in m_match_attributes, and returns
1169 * that offset in @offset_ptr, and the start and end of the corresponding
1170 * line in @sattr_ptr and @eattr_ptr.
1171 */
1172 bool
match_rowcol_to_offset(vte::grid::column_t column,vte::grid::row_t row,gsize * offset_ptr,gsize * sattr_ptr,gsize * eattr_ptr)1173 Terminal::match_rowcol_to_offset(vte::grid::column_t column,
1174 vte::grid::row_t row,
1175 gsize *offset_ptr,
1176 gsize *sattr_ptr,
1177 gsize *eattr_ptr)
1178 {
1179 /* FIXME: use gsize, after making sure the code below doesn't underflow offset */
1180 gssize offset, sattr, eattr;
1181 struct _VteCharAttributes *attr = NULL;
1182
1183 /* Map the pointer position to a portion of the string. */
1184 // FIXME do a bsearch here?
1185 eattr = m_match_attributes->len;
1186 for (offset = eattr; offset--; ) {
1187 attr = &g_array_index(m_match_attributes,
1188 struct _VteCharAttributes,
1189 offset);
1190 if (row < attr->row) {
1191 eattr = offset;
1192 }
1193 if (row == attr->row &&
1194 column >= attr->column && column < attr->column + attr->columns) {
1195 break;
1196 }
1197 }
1198
1199 _VTE_DEBUG_IF(VTE_DEBUG_REGEX) {
1200 if (offset < 0)
1201 g_printerr("Cursor is not on a character.\n");
1202 else {
1203 gunichar c;
1204 char utf[7];
1205 c = g_utf8_get_char (m_match_contents + offset);
1206 utf[g_unichar_to_utf8(g_unichar_isprint(c) ? c : 0xFFFD, utf)] = 0;
1207
1208 g_printerr("Cursor is on character U+%04X '%s' at %" G_GSSIZE_FORMAT ".\n",
1209 c, utf, offset);
1210 }
1211 }
1212
1213 /* If the pointer isn't on a matchable character, bug out. */
1214 if (offset < 0) {
1215 return false;
1216 }
1217
1218 /* If the pointer is on a newline, bug out. */
1219 if (m_match_contents[offset] == '\0') {
1220 _vte_debug_print(VTE_DEBUG_EVENTS,
1221 "Cursor is on newline.\n");
1222 return false;
1223 }
1224
1225 /* Snip off any final newlines. */
1226 while (m_match_contents[eattr] == '\n' ||
1227 m_match_contents[eattr] == '\0') {
1228 eattr--;
1229 }
1230 /* and scan forwards to find the end of this line */
1231 while (!(m_match_contents[eattr] == '\n' ||
1232 m_match_contents[eattr] == '\0')) {
1233 eattr++;
1234 }
1235
1236 /* find the start of row */
1237 if (row == 0) {
1238 sattr = 0;
1239 } else {
1240 for (sattr = offset; sattr > 0; sattr--) {
1241 attr = &g_array_index(m_match_attributes,
1242 struct _VteCharAttributes,
1243 sattr);
1244 if (row > attr->row) {
1245 break;
1246 }
1247 }
1248 }
1249 /* Scan backwards to find the start of this line */
1250 while (sattr > 0 &&
1251 ! (m_match_contents[sattr] == '\n' ||
1252 m_match_contents[sattr] == '\0')) {
1253 sattr--;
1254 }
1255 /* and skip any initial newlines. */
1256 while (m_match_contents[sattr] == '\n' ||
1257 m_match_contents[sattr] == '\0') {
1258 sattr++;
1259 }
1260 if (eattr <= sattr) { /* blank line */
1261 return false;
1262 }
1263 if (eattr <= offset || sattr > offset) {
1264 /* nothing to match on this line */
1265 return false;
1266 }
1267
1268 *offset_ptr = offset;
1269 *sattr_ptr = sattr;
1270 *eattr_ptr = eattr;
1271
1272 _VTE_DEBUG_IF(VTE_DEBUG_REGEX) {
1273 struct _VteCharAttributes *_sattr, *_eattr;
1274 _sattr = &g_array_index(m_match_attributes,
1275 struct _VteCharAttributes,
1276 sattr);
1277 _eattr = &g_array_index(m_match_attributes,
1278 struct _VteCharAttributes,
1279 eattr - 1);
1280 g_printerr("Cursor is in line from %" G_GSIZE_FORMAT "(%ld,%ld) to %" G_GSIZE_FORMAT "(%ld,%ld)\n",
1281 sattr, _sattr->column, _sattr->row,
1282 eattr - 1, _eattr->column, _eattr->row);
1283 }
1284
1285 return true;
1286 }
1287
1288 /* creates a pcre match context with appropriate limits */
1289 vte::Freeable<pcre2_match_context_8>
create_match_context()1290 Terminal::create_match_context()
1291 {
1292 auto context = vte::take_freeable(pcre2_match_context_create_8(nullptr /* general context */));
1293 pcre2_set_match_limit_8(context.get(), 65536); /* should be plenty */
1294 pcre2_set_recursion_limit_8(context.get(), 64); /* should be plenty */
1295
1296 return context;
1297 }
1298
1299 bool
match_check_pcre(pcre2_match_data_8 * match_data,pcre2_match_context_8 * match_context,vte::base::Regex const * regex,uint32_t match_flags,gsize sattr,gsize eattr,gsize offset,char ** result_ptr,gsize * start,gsize * end,gsize * sblank_ptr,gsize * eblank_ptr)1300 Terminal::match_check_pcre(pcre2_match_data_8 *match_data,
1301 pcre2_match_context_8 *match_context,
1302 vte::base::Regex const* regex,
1303 uint32_t match_flags,
1304 gsize sattr,
1305 gsize eattr,
1306 gsize offset,
1307 char **result_ptr,
1308 gsize *start,
1309 gsize *end,
1310 gsize *sblank_ptr,
1311 gsize *eblank_ptr)
1312 {
1313 int (* match_fn) (const pcre2_code_8 *,
1314 PCRE2_SPTR8, PCRE2_SIZE, PCRE2_SIZE, uint32_t,
1315 pcre2_match_data_8 *, pcre2_match_context_8 *);
1316 gsize sblank = 0, eblank = G_MAXSIZE;
1317 gsize position, line_length;
1318 const char *line;
1319 int r = 0;
1320
1321 if (regex->jited())
1322 match_fn = pcre2_jit_match_8;
1323 else
1324 match_fn = pcre2_match_8;
1325
1326 line = m_match_contents;
1327 /* FIXME: what we really want is to pass the whole data to pcre2_match, but
1328 * limit matching to between sattr and eattr, so that the extra data can
1329 * satisfy lookahead assertions. This needs new pcre2 API though.
1330 */
1331 line_length = eattr;
1332
1333 /* Iterate throught the matches until we either find one which contains the
1334 * offset, or we get no more matches.
1335 */
1336 pcre2_set_offset_limit_8(match_context, eattr);
1337 position = sattr;
1338 while (position < eattr &&
1339 ((r = match_fn(regex->code(),
1340 (PCRE2_SPTR8)line, line_length, /* subject, length */
1341 position, /* start offset */
1342 match_flags |
1343 PCRE2_NO_UTF_CHECK | PCRE2_NOTEMPTY | PCRE2_PARTIAL_SOFT /* FIXME: HARD? */,
1344 match_data,
1345 match_context)) >= 0 || r == PCRE2_ERROR_PARTIAL)) {
1346 gsize ko = offset;
1347 gsize rm_so, rm_eo;
1348 gsize *ovector;
1349
1350 ovector = pcre2_get_ovector_pointer_8(match_data);
1351 rm_so = ovector[0];
1352 rm_eo = ovector[1];
1353 if (G_UNLIKELY(rm_so == PCRE2_UNSET || rm_eo == PCRE2_UNSET))
1354 break;
1355
1356 /* The offsets should be "sane". We set NOTEMPTY, but check anyway */
1357 if (G_UNLIKELY(position == rm_eo)) {
1358 /* rm_eo is before the end of subject string's length, so this is safe */
1359 position = g_utf8_next_char(line + rm_eo) - line;
1360 continue;
1361 }
1362
1363 _VTE_DEBUG_IF(VTE_DEBUG_REGEX) {
1364 gchar *result;
1365 struct _VteCharAttributes *_sattr, *_eattr;
1366 result = g_strndup(line + rm_so, rm_eo - rm_so);
1367 _sattr = &g_array_index(m_match_attributes,
1368 struct _VteCharAttributes,
1369 rm_so);
1370 _eattr = &g_array_index(m_match_attributes,
1371 struct _VteCharAttributes,
1372 rm_eo - 1);
1373 g_printerr("%s match `%s' from %" G_GSIZE_FORMAT "(%ld,%ld) to %" G_GSIZE_FORMAT "(%ld,%ld) (%" G_GSSIZE_FORMAT ").\n",
1374 r == PCRE2_ERROR_PARTIAL ? "Partial":"Full",
1375 result,
1376 rm_so,
1377 _sattr->column,
1378 _sattr->row,
1379 rm_eo - 1,
1380 _eattr->column,
1381 _eattr->row,
1382 offset);
1383 g_free(result);
1384 }
1385
1386 /* advance position */
1387 position = rm_eo;
1388
1389 /* FIXME: do handle newline / partial matches at end of line/start of next line */
1390 if (r == PCRE2_ERROR_PARTIAL)
1391 continue;
1392
1393 /* If the pointer is in this substring, then we're done. */
1394 if (ko >= rm_so && ko < rm_eo) {
1395 *result_ptr = g_strndup(line + rm_so, rm_eo - rm_so);
1396 *start = rm_so;
1397 *end = rm_eo - 1;
1398 return true;
1399 }
1400
1401 if (ko >= rm_eo && rm_eo > sblank) {
1402 sblank = rm_eo;
1403 }
1404 if (ko < rm_so && rm_so < eblank) {
1405 eblank = rm_so;
1406 }
1407 }
1408
1409 if (G_UNLIKELY(r < PCRE2_ERROR_PARTIAL))
1410 _vte_debug_print(VTE_DEBUG_REGEX, "Unexpected pcre2_match error code: %d\n", r);
1411
1412 *sblank_ptr = sblank;
1413 *eblank_ptr = eblank;
1414 return false;
1415 }
1416
1417 char *
match_check_internal_pcre(vte::grid::column_t column,vte::grid::row_t row,MatchRegex const ** match,size_t * start,size_t * end)1418 Terminal::match_check_internal_pcre(vte::grid::column_t column,
1419 vte::grid::row_t row,
1420 MatchRegex const** match,
1421 size_t* start,
1422 size_t* end)
1423 {
1424 gsize offset, sattr, eattr, start_blank, end_blank;
1425
1426 _vte_debug_print(VTE_DEBUG_REGEX,
1427 "Checking for pcre match at (%ld,%ld).\n", row, column);
1428
1429 if (!match_rowcol_to_offset(column, row,
1430 &offset, &sattr, &eattr))
1431 return nullptr;
1432
1433 start_blank = sattr;
1434 end_blank = eattr;
1435
1436 auto match_context = create_match_context();
1437 auto match_data = vte::take_freeable(pcre2_match_data_create_8(256 /* should be plenty */,
1438 nullptr /* general context */));
1439
1440 /* Now iterate over each regex we need to match against. */
1441 char* dingu_match{nullptr};
1442 for (auto const& rem : m_match_regexes) {
1443 gsize sblank, eblank;
1444
1445 if (match_check_pcre(match_data.get(), match_context.get(),
1446 rem.regex(),
1447 rem.match_flags(),
1448 sattr, eattr, offset,
1449 &dingu_match,
1450 start, end,
1451 &sblank, &eblank)) {
1452 _vte_debug_print(VTE_DEBUG_REGEX, "Matched dingu with tag %d\n", rem.tag());
1453 *match = std::addressof(rem);
1454 break;
1455 }
1456
1457 if (sblank > start_blank) {
1458 start_blank = sblank;
1459 }
1460 if (eblank < end_blank) {
1461 end_blank = eblank;
1462 }
1463 }
1464
1465 if (dingu_match == nullptr) {
1466 /* If we get here, there was no dingu match.
1467 * Record smallest span where none of the dingus match.
1468 */
1469 *start = start_blank;
1470 *end = end_blank - 1;
1471 *match = nullptr;
1472
1473 _VTE_DEBUG_IF(VTE_DEBUG_REGEX) {
1474 struct _VteCharAttributes *_sattr, *_eattr;
1475 _sattr = &g_array_index(m_match_attributes,
1476 struct _VteCharAttributes,
1477 start_blank);
1478 _eattr = &g_array_index(m_match_attributes,
1479 struct _VteCharAttributes,
1480 end_blank - 1);
1481 g_printerr("No-match region from %" G_GSIZE_FORMAT "(%ld,%ld) to %" G_GSIZE_FORMAT "(%ld,%ld)\n",
1482 start_blank, _sattr->column, _sattr->row,
1483 end_blank - 1, _eattr->column, _eattr->row);
1484 }
1485 }
1486
1487 return dingu_match;
1488 }
1489
1490 /*
1491 * vte_terminal_match_check_internal:
1492 * @terminal:
1493 * @column:
1494 * @row:
1495 * @match: (out):
1496 * @start: (out):
1497 * @end: (out):
1498 *
1499 * Checks m_match_contents for dingu matches, and returns the start, and
1500 * end of the match in @start, @end, and the matched regex in @match.
1501 * If no match occurs, @match will be set to %nullptr,
1502 * and if they are nonzero, @start and @end mark the smallest span in the @row
1503 * in which none of the dingus match.
1504 *
1505 * Returns: (transfer full): the matched string, or %nullptr
1506 */
1507 char *
match_check_internal(vte::grid::column_t column,vte::grid::row_t row,MatchRegex const ** match,size_t * start,size_t * end)1508 Terminal::match_check_internal(vte::grid::column_t column,
1509 vte::grid::row_t row,
1510 MatchRegex const** match,
1511 size_t* start,
1512 size_t* end)
1513 {
1514 if (m_match_contents == nullptr) {
1515 match_contents_refresh();
1516 }
1517
1518 assert(match != nullptr);
1519 assert(start != nullptr);
1520 assert(end != nullptr);
1521
1522 *match = nullptr;
1523 *start = 0;
1524 *end = 0;
1525
1526 return match_check_internal_pcre(column, row, match, start, end);
1527 }
1528
1529 char*
regex_match_check(vte::grid::column_t column,vte::grid::row_t row,int * tag)1530 Terminal::regex_match_check(vte::grid::column_t column,
1531 vte::grid::row_t row,
1532 int* tag)
1533 {
1534 long delta = m_screen->scroll_delta;
1535 _vte_debug_print(VTE_DEBUG_EVENTS | VTE_DEBUG_REGEX,
1536 "Checking for match at (%ld,%ld).\n",
1537 row, column);
1538
1539 char* ret{nullptr};
1540 Terminal::MatchRegex const* match{nullptr};
1541
1542 if (m_match_span.contains(row + delta, column)) {
1543 match = regex_match_current(); /* may be nullptr */
1544 ret = g_strdup(m_match);
1545 } else {
1546 gsize start, end;
1547
1548 ret = match_check_internal(column, row + delta,
1549 &match,
1550 &start, &end);
1551 }
1552 _VTE_DEBUG_IF(VTE_DEBUG_EVENTS | VTE_DEBUG_REGEX) {
1553 if (ret != NULL) g_printerr("Matched `%s'.\n", ret);
1554 }
1555 if (tag != nullptr)
1556 *tag = (match != nullptr) ? match->tag() : -1;
1557
1558 return ret;
1559 }
1560
1561 /*
1562 * Terminal::view_coords_from_event:
1563 * @event: a mouse event
1564 *
1565 * Translates the event coordinates to view coordinates, by
1566 * subtracting the padding and window offset.
1567 * Coordinates < 0 or >= m_usable_view_extents.width() or .height()
1568 * mean that the event coordinates are outside the usable area
1569 * at that side; use view_coords_visible() to check for that.
1570 */
1571 vte::view::coords
view_coords_from_event(vte::platform::MouseEvent const & event) const1572 Terminal::view_coords_from_event(vte::platform::MouseEvent const& event) const
1573 {
1574 return vte::view::coords(event.x() - m_padding.left, event.y() - m_padding.top);
1575 }
1576
1577 bool
widget_realized() const1578 Terminal::widget_realized() const noexcept
1579 {
1580 return m_real_widget ? m_real_widget->realized() : false;
1581 }
1582
1583 /*
1584 * Terminal::grid_coords_from_event:
1585 * @event: a mouse event
1586 *
1587 * Translates the event coordinates to view coordinates, by
1588 * subtracting the padding and window offset.
1589 * Coordinates < 0 or >= m_usable_view_extents.width() or .height()
1590 * mean that the event coordinates are outside the usable area
1591 * at that side; use grid_coords_visible() to check for that.
1592 */
1593 vte::grid::coords
grid_coords_from_event(vte::platform::MouseEvent const & event) const1594 Terminal::grid_coords_from_event(vte::platform::MouseEvent const& event) const
1595 {
1596 return grid_coords_from_view_coords(view_coords_from_event(event));
1597 }
1598
1599 /*
1600 * Terminal::confined_grid_coords_from_event:
1601 * @event: a mouse event
1602 *
1603 * Like grid_coords_from_event(), but also confines the coordinates
1604 * to an actual cell in the visible area.
1605 */
1606 vte::grid::coords
confined_grid_coords_from_event(vte::platform::MouseEvent const & event) const1607 Terminal::confined_grid_coords_from_event(vte::platform::MouseEvent const& event) const
1608 {
1609 auto pos = view_coords_from_event(event);
1610 return confined_grid_coords_from_view_coords(pos);
1611 }
1612
1613 /*
1614 * Terminal::grid_coords_from_view_coords:
1615 * @pos: the view coordinates
1616 *
1617 * Translates view coordinates to grid coordinates. If the view coordinates point to
1618 * cells that are not visible, may return any value < 0 or >= m_column_count, and
1619 * < first_displayed_row() or > last_displayed_row(), resp.
1620 */
1621 vte::grid::coords
grid_coords_from_view_coords(vte::view::coords const & pos) const1622 Terminal::grid_coords_from_view_coords(vte::view::coords const& pos) const
1623 {
1624 /* Our caller had to update the ringview (we can't do because we're const). */
1625 g_assert(m_ringview.is_updated());
1626
1627 vte::grid::column_t col;
1628 if (pos.x >= 0 && pos.x < m_view_usable_extents.width())
1629 col = pos.x / m_cell_width;
1630 else if (pos.x < 0)
1631 col = -1;
1632 else
1633 col = m_column_count;
1634
1635 vte::grid::row_t row = pixel_to_row(pos.y);
1636
1637 /* BiDi: convert to logical column. */
1638 vte::base::BidiRow const* bidirow = m_ringview.get_bidirow(confine_grid_row(row));
1639 col = bidirow->vis2log(col);
1640
1641 return vte::grid::coords(row, col);
1642 }
1643
1644 vte::grid::row_t
confine_grid_row(vte::grid::row_t const & row) const1645 Terminal::confine_grid_row(vte::grid::row_t const& row) const
1646 {
1647 auto first_row = first_displayed_row();
1648 auto last_row = last_displayed_row();
1649
1650 return vte::clamp(row, first_row, last_row);
1651 }
1652
1653 /*
1654 * Terminal::confined_grid_coords_from_view_coords:
1655 * @pos: the view coordinates
1656 *
1657 * Like grid_coords_from_view_coords(), but also confines the coordinates
1658 * to an actual cell in the visible area.
1659 */
1660 vte::grid::coords
confined_grid_coords_from_view_coords(vte::view::coords const & pos) const1661 Terminal::confined_grid_coords_from_view_coords(vte::view::coords const& pos) const
1662 {
1663 auto rowcol = grid_coords_from_view_coords(pos);
1664 return confine_grid_coords(rowcol);
1665 }
1666
1667 /*
1668 * Terminal::view_coords_from_grid_coords:
1669 * @rowcol: the grid coordinates
1670 *
1671 * Translates grid coordinates to view coordinates. If the view coordinates are
1672 * outside the usable area, may return any value < 0 or >= m_usable_view_extents.
1673 *
1674 * Returns: %true if the coordinates are inside the usable area
1675 */
1676 vte::view::coords
view_coords_from_grid_coords(vte::grid::coords const & rowcol) const1677 Terminal::view_coords_from_grid_coords(vte::grid::coords const& rowcol) const
1678 {
1679 return vte::view::coords(rowcol.column() * m_cell_width,
1680 row_to_pixel(rowcol.row()));
1681 }
1682
1683 bool
view_coords_visible(vte::view::coords const & pos) const1684 Terminal::view_coords_visible(vte::view::coords const& pos) const
1685 {
1686 return pos.x >= 0 && pos.x < m_view_usable_extents.width() &&
1687 pos.y >= 0 && pos.y < m_view_usable_extents.height();
1688 }
1689
1690 bool
grid_coords_visible(vte::grid::coords const & rowcol) const1691 Terminal::grid_coords_visible(vte::grid::coords const& rowcol) const
1692 {
1693 return rowcol.column() >= 0 &&
1694 rowcol.column() < m_column_count &&
1695 rowcol.row() >= first_displayed_row() &&
1696 rowcol.row() <= last_displayed_row();
1697 }
1698
1699 vte::grid::coords
confine_grid_coords(vte::grid::coords const & rowcol) const1700 Terminal::confine_grid_coords(vte::grid::coords const& rowcol) const
1701 {
1702 /* Confine clicks to the nearest actual cell. This is especially useful for
1703 * fullscreen vte so that you can click on the very edge of the screen.
1704 */
1705 auto first_row = first_displayed_row();
1706 auto last_row = last_displayed_row();
1707
1708 return vte::grid::coords(CLAMP(rowcol.row(), first_row, last_row),
1709 CLAMP(rowcol.column(), 0, m_column_count - 1));
1710 }
1711
1712 /*
1713 * Track mouse click and drag positions (the "origin" and "last" coordinates) with half cell accuracy,
1714 * that is, know whether the event occurred over the left/start or right/end half of the cell.
1715 * This is required because some selection modes care about the cell over which the event occurred,
1716 * while some care about the closest boundary between cells.
1717 *
1718 * Storing the actual view coordinates would become problematic when the font size changes (bug 756058),
1719 * and would cause too much work when the mouse moves within the half cell.
1720 *
1721 * Left/start margin or anything further to the left/start is denoted by column -1's right half,
1722 * right/end margin or anything further to the right/end is denoted by column m_column_count's left half.
1723 *
1724 * BiDi: returns logical position (start or end) for normal selection modes, visual position (left or
1725 * right) for block mode.
1726 */
1727 vte::grid::halfcoords
selection_grid_halfcoords_from_view_coords(vte::view::coords const & pos) const1728 Terminal::selection_grid_halfcoords_from_view_coords(vte::view::coords const& pos) const
1729 {
1730 /* Our caller had to update the ringview (we can't do because we're const). */
1731 g_assert(m_ringview.is_updated());
1732
1733 vte::grid::row_t row = pixel_to_row(pos.y);
1734 vte::grid::column_t col;
1735 vte::grid::half_t half;
1736
1737 if (pos.x < 0) {
1738 col = -1;
1739 half = 1;
1740 } else if (pos.x >= m_column_count * m_cell_width) {
1741 col = m_column_count;
1742 half = 0;
1743 } else {
1744 col = pos.x / m_cell_width;
1745 half = (pos.x * 2 / m_cell_width) % 2;
1746 }
1747
1748 if (!m_selection_block_mode) {
1749 /* BiDi: convert from visual to logical half column. */
1750 vte::base::BidiRow const* bidirow = m_ringview.get_bidirow(confine_grid_row(row));
1751
1752 if (bidirow->vis_is_rtl(col))
1753 half = 1 - half;
1754 col = bidirow->vis2log(col);
1755 }
1756
1757 return { row, vte::grid::halfcolumn_t(col, half) };
1758 }
1759
1760 /*
1761 * Called on Shift+Click to continue (extend or shrink) the previous selection.
1762 * Swaps the two endpoints of the selection if needed, so that m_selection_origin
1763 * contains the new fixed point and m_selection_last is the newly dragged end.
1764 * In block mode it might even switch to the other two corners.
1765 * As per GTK+'s generic selection behavior, retains the origin and last
1766 * endpoints if the Shift+click happened inside the selection.
1767 */
1768 void
selection_maybe_swap_endpoints(vte::view::coords const & pos)1769 Terminal::selection_maybe_swap_endpoints(vte::view::coords const& pos)
1770 {
1771 if (m_selection_resolved.empty())
1772 return;
1773
1774 /* Need to ensure the ringview is updated. */
1775 ringview_update();
1776
1777 auto current = selection_grid_halfcoords_from_view_coords (pos);
1778
1779 if (m_selection_block_mode) {
1780 if ((current.row() <= m_selection_origin.row() && m_selection_origin.row() < m_selection_last.row()) ||
1781 (current.row() >= m_selection_origin.row() && m_selection_origin.row() > m_selection_last.row())) {
1782 // FIXME see if we can use std::swap()
1783 auto tmp = m_selection_origin.row();
1784 m_selection_origin.set_row(m_selection_last.row());
1785 m_selection_last.set_row(tmp);
1786 }
1787 if ((current.halfcolumn() <= m_selection_origin.halfcolumn() && m_selection_origin.halfcolumn() < m_selection_last.halfcolumn()) ||
1788 (current.halfcolumn() >= m_selection_origin.halfcolumn() && m_selection_origin.halfcolumn() > m_selection_last.halfcolumn())) {
1789 // FIXME see if we can use std::swap()
1790 auto tmp = m_selection_origin.halfcolumn();
1791 m_selection_origin.set_halfcolumn(m_selection_last.halfcolumn());
1792 m_selection_last.set_halfcolumn(tmp);
1793 }
1794 } else {
1795 if ((current <= m_selection_origin && m_selection_origin < m_selection_last) ||
1796 (current >= m_selection_origin && m_selection_origin > m_selection_last)) {
1797 using std::swap;
1798 swap(m_selection_origin, m_selection_last);
1799 }
1800 }
1801
1802 _vte_debug_print(VTE_DEBUG_SELECTION,
1803 "Selection maybe swap endpoints: origin=%s last=%s\n",
1804 m_selection_origin.to_string(),
1805 m_selection_last.to_string());
1806 }
1807
1808 bool
rowcol_from_event(vte::platform::MouseEvent const & event,long * column,long * row)1809 Terminal::rowcol_from_event(vte::platform::MouseEvent const& event,
1810 long *column,
1811 long *row)
1812 {
1813 auto rowcol = grid_coords_from_event(event);
1814 if (!grid_coords_visible(rowcol))
1815 return false;
1816
1817 *column = rowcol.column();
1818 *row = rowcol.row();
1819 return true;
1820 }
1821
1822 char *
hyperlink_check(vte::platform::MouseEvent const & event)1823 Terminal::hyperlink_check(vte::platform::MouseEvent const& event)
1824 {
1825 long col, row;
1826 const char *hyperlink;
1827 const char *separator;
1828
1829 if (!m_allow_hyperlink)
1830 return NULL;
1831
1832 /* Need to ensure the ringview is updated. */
1833 ringview_update();
1834
1835 if (!rowcol_from_event(event, &col, &row))
1836 return NULL;
1837
1838 _vte_ring_get_hyperlink_at_position(m_screen->row_data, row, col, false, &hyperlink);
1839
1840 if (hyperlink != NULL) {
1841 /* URI is after the first semicolon */
1842 separator = strchr(hyperlink, ';');
1843 g_assert(separator != NULL);
1844 hyperlink = separator + 1;
1845 }
1846
1847 _vte_debug_print (VTE_DEBUG_HYPERLINK,
1848 "hyperlink_check: \"%s\"\n",
1849 hyperlink);
1850
1851 return g_strdup(hyperlink);
1852 }
1853
1854 char *
regex_match_check(vte::platform::MouseEvent const & event,int * tag)1855 Terminal::regex_match_check(vte::platform::MouseEvent const& event,
1856 int *tag)
1857 {
1858 long col, row;
1859
1860 /* Need to ensure the ringview is updated. */
1861 ringview_update();
1862
1863 if (!rowcol_from_event(event, &col, &row))
1864 return FALSE;
1865
1866 /* FIXME Shouldn't rely on a deprecated, not sub-row aware method. */
1867 // FIXMEchpe fix this scroll_delta substraction!
1868 return regex_match_check(col, row - (long)m_screen->scroll_delta, tag);
1869 }
1870
1871 bool
regex_match_check_extra(vte::platform::MouseEvent const & event,vte::base::Regex const ** regexes,size_t n_regexes,uint32_t match_flags,char ** matches)1872 Terminal::regex_match_check_extra(vte::platform::MouseEvent const& event,
1873 vte::base::Regex const** regexes,
1874 size_t n_regexes,
1875 uint32_t match_flags,
1876 char** matches)
1877 {
1878 gsize offset, sattr, eattr;
1879 bool any_matches = false;
1880 long col, row;
1881 guint i;
1882
1883 assert(regexes != nullptr || n_regexes == 0);
1884 assert(matches != nullptr);
1885
1886 /* Need to ensure the ringview is updated. */
1887 ringview_update();
1888
1889 if (!rowcol_from_event(event, &col, &row))
1890 return false;
1891
1892 if (m_match_contents == nullptr) {
1893 match_contents_refresh();
1894 }
1895
1896 if (!match_rowcol_to_offset(col, row,
1897 &offset, &sattr, &eattr))
1898 return false;
1899
1900 auto match_context = create_match_context();
1901 auto match_data = vte::take_freeable(pcre2_match_data_create_8(256 /* should be plenty */,
1902 nullptr /* general context */));
1903
1904 for (i = 0; i < n_regexes; i++) {
1905 gsize start, end, sblank, eblank;
1906 char *match_string;
1907
1908 g_return_val_if_fail(regexes[i] != nullptr, false);
1909
1910 if (match_check_pcre(match_data.get(), match_context.get(),
1911 regexes[i], match_flags,
1912 sattr, eattr, offset,
1913 &match_string,
1914 &start, &end,
1915 &sblank, &eblank)) {
1916 _vte_debug_print(VTE_DEBUG_REGEX, "Matched regex with text: %s\n", match_string);
1917 matches[i] = match_string;
1918 any_matches = true;
1919 } else
1920 matches[i] = nullptr;
1921 }
1922
1923 return any_matches;
1924 }
1925
1926 /* Emit an adjustment changed signal on our adjustment object. */
1927 void
emit_adjustment_changed()1928 Terminal::emit_adjustment_changed()
1929 {
1930 if (m_adjustment_changed_pending) {
1931 bool changed = false;
1932 gdouble current, v;
1933
1934 auto vadjustment = m_vadjustment.get();
1935
1936 auto const freezer = vte::glib::FreezeObjectNotify{vadjustment};
1937
1938 v = _vte_ring_delta (m_screen->row_data);
1939 current = gtk_adjustment_get_lower(vadjustment);
1940 if (!_vte_double_equal(current, v)) {
1941 _vte_debug_print(VTE_DEBUG_ADJ,
1942 "Changing lower bound from %.0f to %f\n",
1943 current, v);
1944 gtk_adjustment_set_lower(vadjustment, v);
1945 changed = true;
1946 }
1947
1948 v = m_screen->insert_delta + m_row_count;
1949 current = gtk_adjustment_get_upper(vadjustment);
1950 if (!_vte_double_equal(current, v)) {
1951 _vte_debug_print(VTE_DEBUG_ADJ,
1952 "Changing upper bound from %.0f to %f\n",
1953 current, v);
1954 gtk_adjustment_set_upper(vadjustment, v);
1955 changed = true;
1956 }
1957
1958 /* The step increment should always be one. */
1959 v = gtk_adjustment_get_step_increment(vadjustment);
1960 if (!_vte_double_equal(v, 1)) {
1961 _vte_debug_print(VTE_DEBUG_ADJ,
1962 "Changing step increment from %.0lf to 1\n", v);
1963 gtk_adjustment_set_step_increment(vadjustment, 1);
1964 changed = true;
1965 }
1966
1967 /* Set the number of rows the user sees to the number of rows the
1968 * user sees. */
1969 v = gtk_adjustment_get_page_size(vadjustment);
1970 if (!_vte_double_equal(v, m_row_count)) {
1971 _vte_debug_print(VTE_DEBUG_ADJ,
1972 "Changing page size from %.0f to %ld\n",
1973 v, m_row_count);
1974 gtk_adjustment_set_page_size(vadjustment,
1975 m_row_count);
1976 changed = true;
1977 }
1978
1979 /* Clicking in the empty area should scroll one screen, so set the
1980 * page size to the number of visible rows. */
1981 v = gtk_adjustment_get_page_increment(vadjustment);
1982 if (!_vte_double_equal(v, m_row_count)) {
1983 _vte_debug_print(VTE_DEBUG_ADJ,
1984 "Changing page increment from "
1985 "%.0f to %ld\n",
1986 v, m_row_count);
1987 gtk_adjustment_set_page_increment(vadjustment,
1988 m_row_count);
1989 changed = true;
1990 }
1991
1992 if (changed)
1993 _vte_debug_print(VTE_DEBUG_SIGNALS,
1994 "Emitting adjustment_changed.\n");
1995 m_adjustment_changed_pending = FALSE;
1996 }
1997 if (m_adjustment_value_changed_pending) {
1998 double v, delta;
1999 _vte_debug_print(VTE_DEBUG_SIGNALS,
2000 "Emitting adjustment_value_changed.\n");
2001 m_adjustment_value_changed_pending = FALSE;
2002
2003 auto vadjustment = m_vadjustment.get();
2004 v = gtk_adjustment_get_value(vadjustment);
2005 if (!_vte_double_equal(v, m_screen->scroll_delta)) {
2006 /* this little dance is so that the scroll_delta is
2007 * updated immediately, but we still handled scrolling
2008 * via the adjustment - e.g. user interaction with the
2009 * scrollbar
2010 */
2011 delta = m_screen->scroll_delta;
2012 m_screen->scroll_delta = v;
2013 gtk_adjustment_set_value(vadjustment, delta);
2014 }
2015 }
2016 }
2017
2018 /* Queue an adjustment-changed signal to be delivered when convenient. */
2019 // FIXMEchpe this has just one caller, fold it into the call site
2020 void
queue_adjustment_changed()2021 Terminal::queue_adjustment_changed()
2022 {
2023 m_adjustment_changed_pending = true;
2024 add_update_timeout(this);
2025 }
2026
2027 void
queue_adjustment_value_changed(double v)2028 Terminal::queue_adjustment_value_changed(double v)
2029 {
2030 if (!_vte_double_equal(v, m_screen->scroll_delta)) {
2031 _vte_debug_print(VTE_DEBUG_ADJ,
2032 "Adjustment value changed to %f\n",
2033 v);
2034 m_screen->scroll_delta = v;
2035 m_adjustment_value_changed_pending = true;
2036 add_update_timeout(this);
2037 }
2038 }
2039
2040 void
queue_adjustment_value_changed_clamped(double v)2041 Terminal::queue_adjustment_value_changed_clamped(double v)
2042 {
2043 auto vadjustment = m_vadjustment.get();
2044 auto const lower = gtk_adjustment_get_lower(vadjustment);
2045 auto const upper = gtk_adjustment_get_upper(vadjustment);
2046
2047 v = CLAMP(v, lower, MAX (lower, upper - m_row_count));
2048
2049 queue_adjustment_value_changed(v);
2050 }
2051
2052 void
adjust_adjustments()2053 Terminal::adjust_adjustments()
2054 {
2055 g_assert(m_screen != nullptr);
2056 g_assert(m_screen->row_data != nullptr);
2057
2058 queue_adjustment_changed();
2059
2060 /* The lower value should be the first row in the buffer. */
2061 long delta = _vte_ring_delta(m_screen->row_data);
2062 /* Snap the insert delta and the cursor position to be in the visible
2063 * area. Leave the scrolling delta alone because it will be updated
2064 * when the adjustment changes. */
2065 m_screen->insert_delta = MAX(m_screen->insert_delta, delta);
2066 m_screen->cursor.row = MAX(m_screen->cursor.row,
2067 m_screen->insert_delta);
2068
2069 if (m_screen->scroll_delta > m_screen->insert_delta) {
2070 queue_adjustment_value_changed(m_screen->insert_delta);
2071 }
2072 }
2073
2074 /* Update the adjustment field of the widget. This function should be called
2075 * whenever we add rows to or remove rows from the history or switch screens. */
2076 void
adjust_adjustments_full()2077 Terminal::adjust_adjustments_full()
2078 {
2079 g_assert(m_screen != NULL);
2080 g_assert(m_screen->row_data != NULL);
2081
2082 adjust_adjustments();
2083 queue_adjustment_changed();
2084 }
2085
2086 /* Scroll a fixed number of lines up or down in the current screen. */
2087 void
scroll_lines(long lines)2088 Terminal::scroll_lines(long lines)
2089 {
2090 double destination;
2091 _vte_debug_print(VTE_DEBUG_ADJ, "Scrolling %ld lines.\n", lines);
2092 /* Calculate the ideal position where we want to be before clamping. */
2093 destination = m_screen->scroll_delta;
2094 /* Snap to whole cell offset. */
2095 if (lines > 0)
2096 destination = floor(destination);
2097 else if (lines < 0)
2098 destination = ceil(destination);
2099 destination += lines;
2100 /* Tell the scrollbar to adjust itself. */
2101 queue_adjustment_value_changed_clamped(destination);
2102 }
2103
2104 /* Scroll so that the scroll delta is the minimum value. */
2105 void
maybe_scroll_to_top()2106 Terminal::maybe_scroll_to_top()
2107 {
2108 queue_adjustment_value_changed(_vte_ring_delta(m_screen->row_data));
2109 }
2110
2111 void
maybe_scroll_to_bottom()2112 Terminal::maybe_scroll_to_bottom()
2113 {
2114 queue_adjustment_value_changed(m_screen->insert_delta);
2115 _vte_debug_print(VTE_DEBUG_ADJ,
2116 "Snapping to bottom of screen\n");
2117 }
2118
2119 /*
2120 * Terminal::set_encoding:
2121 * @charset: (allow-none): target charset, or %NULL to use UTF-8
2122 *
2123 * Changes the encoding the terminal will expect data from the child to
2124 * be encoded with. If @charset is %NULL, it uses "UTF-8".
2125 *
2126 * Returns: %true if the encoding could be changed to the specified one
2127 */
2128 bool
set_encoding(char const * charset,GError ** error)2129 Terminal::set_encoding(char const* charset,
2130 GError** error)
2131 {
2132 auto const to_utf8 = bool{charset == nullptr || g_ascii_strcasecmp(charset, "UTF-8") == 0};
2133
2134 #ifdef WITH_ICU
2135 /* Note that if the current data syntax is not a primary one, the change
2136 * will only be applied when returning to the primrary data syntax.
2137 */
2138
2139 if (to_utf8) {
2140 if (primary_data_syntax() == DataSyntax::ECMA48_UTF8)
2141 return true;
2142
2143 m_converter.reset();
2144 m_primary_data_syntax = DataSyntax::ECMA48_UTF8;
2145 } else {
2146 if (primary_data_syntax() == DataSyntax::ECMA48_PCTERM &&
2147 m_converter->charset() == charset)
2148 return true;
2149
2150 try {
2151 auto converter = vte::base::ICUConverter::make(charset, error);
2152 if (!converter)
2153 return false;
2154
2155 m_converter = std::move(converter);
2156 m_primary_data_syntax = DataSyntax::ECMA48_PCTERM;
2157
2158 } catch (...) {
2159 return vte::glib::set_error_from_exception(error);
2160 }
2161 }
2162
2163 /* Note: we DON'T convert any pending output from the previous charset to
2164 * the new charset, since that is in general not possible without loss, and
2165 * also the output may include binary data (Terminal::feed_child_binary()).
2166 * So we just clear the outgoing queue. (FIXMEchpe: instead, we could flush
2167 * the outgooing and only change charsets once it's empty.)
2168 * Do not clear the incoming queue.
2169 */
2170 _vte_byte_array_clear(m_outgoing);
2171
2172 reset_decoder();
2173
2174 if (pty())
2175 pty()->set_utf8(primary_data_syntax() == DataSyntax::ECMA48_UTF8);
2176
2177 _vte_debug_print(VTE_DEBUG_IO,
2178 "Set terminal encoding to `%s'.\n",
2179 encoding());
2180
2181 return true;
2182
2183 #else
2184
2185 if (to_utf8)
2186 return true;
2187
2188 g_set_error_literal(error, G_CONVERT_ERROR, G_CONVERT_ERROR_NO_CONVERSION,
2189 "ICU support not available");
2190 return false;
2191 #endif
2192 }
2193
2194 bool
set_cjk_ambiguous_width(int width)2195 Terminal::set_cjk_ambiguous_width(int width)
2196 {
2197 g_assert(width == 1 || width == 2);
2198
2199 if (m_utf8_ambiguous_width == width)
2200 return false;
2201
2202 m_utf8_ambiguous_width = width;
2203 return true;
2204 }
2205
2206 // FIXMEchpe replace this with a method on VteRing
2207 VteRowData *
insert_rows(guint cnt)2208 Terminal::insert_rows (guint cnt)
2209 {
2210 VteRowData *row;
2211 do {
2212 row = ring_append(false);
2213 } while(--cnt);
2214 return row;
2215 }
2216
2217 /* Make sure we have enough rows and columns to hold data at the current
2218 * cursor position. */
2219 VteRowData *
ensure_row()2220 Terminal::ensure_row()
2221 {
2222 VteRowData *row;
2223
2224 /* Figure out how many rows we need to add. */
2225 auto const delta = m_screen->cursor.row - _vte_ring_next(m_screen->row_data) + 1;
2226 if (delta > 0) {
2227 row = insert_rows(delta);
2228 adjust_adjustments();
2229 } else {
2230 /* Find the row the cursor is in. */
2231 row = _vte_ring_index_writable(m_screen->row_data, m_screen->cursor.row);
2232 }
2233 g_assert(row != NULL);
2234
2235 return row;
2236 }
2237
2238 VteRowData *
ensure_cursor()2239 Terminal::ensure_cursor()
2240 {
2241 VteRowData *row = ensure_row();
2242 _vte_row_data_fill(row, &basic_cell, m_screen->cursor.col);
2243
2244 return row;
2245 }
2246
2247 /* Update the insert delta so that the screen which includes it also
2248 * includes the end of the buffer. */
2249 void
update_insert_delta()2250 Terminal::update_insert_delta()
2251 {
2252 /* The total number of lines. Add one to the cursor offset
2253 * because it's zero-based. */
2254 auto rows = _vte_ring_next(m_screen->row_data);
2255 auto delta = m_screen->cursor.row - rows + 1;
2256 if (G_UNLIKELY (delta > 0)) {
2257 insert_rows(delta);
2258 rows = _vte_ring_next(m_screen->row_data);
2259 }
2260
2261 /* Make sure that the bottom row is visible, and that it's in
2262 * the buffer (even if it's empty). This usually causes the
2263 * top row to become a history-only row. */
2264 delta = m_screen->insert_delta;
2265 delta = MIN(delta, rows - m_row_count);
2266 delta = MAX(delta,
2267 m_screen->cursor.row - (m_row_count - 1));
2268 delta = MAX(delta, _vte_ring_delta(m_screen->row_data));
2269
2270 /* Adjust the insert delta and scroll if needed. */
2271 if (delta != m_screen->insert_delta) {
2272 m_screen->insert_delta = delta;
2273 adjust_adjustments();
2274 }
2275 }
2276
2277 /* Apply the desired mouse pointer, based on certain member variables. */
2278 void
apply_mouse_cursor()2279 Terminal::apply_mouse_cursor()
2280 {
2281 if (!widget_realized())
2282 return;
2283
2284 /* Show the cursor if over the widget and not autohidden, this is obvious.
2285 * Also show the cursor if outside the widget regardless of the autohidden state, so that if a popover is opened
2286 * and then the cursor returns (which doesn't trigger enter/motion events), it is visible.
2287 * That is, only hide the cursor if it's over the widget and is autohidden.
2288 * See bug 789390 and bug 789536 comment 6 for details. */
2289 if (!(m_mouse_autohide && m_mouse_cursor_autohidden && m_mouse_cursor_over_widget)) {
2290 if (m_hyperlink_hover_idx != 0) {
2291 _vte_debug_print(VTE_DEBUG_CURSOR,
2292 "Setting hyperlink mouse cursor.\n");
2293 m_real_widget->set_cursor(vte::platform::Widget::CursorType::eHyperlink);
2294 } else if (regex_match_has_current()) {
2295 m_real_widget->set_cursor(regex_match_current()->cursor());
2296 } else if (m_mouse_tracking_mode != MouseTrackingMode::eNONE) {
2297 _vte_debug_print(VTE_DEBUG_CURSOR,
2298 "Setting mousing cursor.\n");
2299 m_real_widget->set_cursor(vte::platform::Widget::CursorType::eMousing);
2300 } else {
2301 _vte_debug_print(VTE_DEBUG_CURSOR,
2302 "Setting default mouse cursor.\n");
2303 m_real_widget->set_cursor(vte::platform::Widget::CursorType::eDefault);
2304 }
2305 } else {
2306 _vte_debug_print(VTE_DEBUG_CURSOR,
2307 "Setting to invisible cursor.\n");
2308 m_real_widget->set_cursor(vte::platform::Widget::CursorType::eInvisible);
2309 }
2310 }
2311
2312 /* Show or hide the pointer if autohiding is enabled. */
2313 void
set_pointer_autohidden(bool autohidden)2314 Terminal::set_pointer_autohidden(bool autohidden)
2315 {
2316 if (autohidden == m_mouse_cursor_autohidden)
2317 return;
2318
2319 m_mouse_cursor_autohidden = autohidden;
2320
2321 if (m_mouse_autohide) {
2322 hyperlink_hilite_update();
2323 match_hilite_update();
2324 apply_mouse_cursor();
2325 }
2326 }
2327
2328 /*
2329 * Get the actually used color from the palette.
2330 * The return value can be NULL only if entry is one of VTE_CURSOR_BG,
2331 * VTE_CURSOR_FG, VTE_HIGHLIGHT_BG or VTE_HIGHLIGHT_FG.
2332 */
2333 vte::color::rgb const*
get_color(int entry) const2334 Terminal::get_color(int entry) const
2335 {
2336 VtePaletteColor const* palette_color = &m_palette[entry];
2337 guint source;
2338 for (source = 0; source < G_N_ELEMENTS(palette_color->sources); source++)
2339 if (palette_color->sources[source].is_set)
2340 return &palette_color->sources[source].color;
2341 return nullptr;
2342 }
2343
2344 /* Set up a palette entry with a more-or-less match for the requested color. */
2345 void
set_color(int entry,int source,vte::color::rgb const & proposed)2346 Terminal::set_color(int entry,
2347 int source,
2348 vte::color::rgb const& proposed)
2349 {
2350 g_assert(entry >= 0 && entry < VTE_PALETTE_SIZE);
2351
2352 VtePaletteColor *palette_color = &m_palette[entry];
2353
2354 _vte_debug_print(VTE_DEBUG_MISC,
2355 "Set %s color[%d] to (%04x,%04x,%04x).\n",
2356 source == VTE_COLOR_SOURCE_ESCAPE ? "escape" : "API",
2357 entry, proposed.red, proposed.green, proposed.blue);
2358
2359 if (palette_color->sources[source].is_set &&
2360 palette_color->sources[source].color == proposed) {
2361 return;
2362 }
2363 palette_color->sources[source].is_set = TRUE;
2364 palette_color->sources[source].color = proposed;
2365
2366 /* If we're not realized yet, there's nothing else to do. */
2367 if (!widget_realized())
2368 return;
2369
2370 /* and redraw */
2371 if (entry == VTE_CURSOR_BG || entry == VTE_CURSOR_FG)
2372 invalidate_cursor_once();
2373 else
2374 invalidate_all();
2375 }
2376
2377 void
reset_color(int entry,int source)2378 Terminal::reset_color(int entry,
2379 int source)
2380 {
2381 g_assert(entry >= 0 && entry < VTE_PALETTE_SIZE);
2382
2383 VtePaletteColor *palette_color = &m_palette[entry];
2384
2385 _vte_debug_print(VTE_DEBUG_MISC,
2386 "Reset %s color[%d].\n",
2387 source == VTE_COLOR_SOURCE_ESCAPE ? "escape" : "API",
2388 entry);
2389
2390 if (!palette_color->sources[source].is_set) {
2391 return;
2392 }
2393 palette_color->sources[source].is_set = FALSE;
2394
2395 /* If we're not realized yet, there's nothing else to do. */
2396 if (!widget_realized())
2397 return;
2398
2399 /* and redraw */
2400 if (entry == VTE_CURSOR_BG || entry == VTE_CURSOR_FG)
2401 invalidate_cursor_once();
2402 else
2403 invalidate_all();
2404 }
2405
2406 bool
set_background_alpha(double alpha)2407 Terminal::set_background_alpha(double alpha)
2408 {
2409 g_assert(alpha >= 0. && alpha <= 1.);
2410
2411 if (_vte_double_equal(alpha, m_background_alpha))
2412 return false;
2413
2414 _vte_debug_print(VTE_DEBUG_MISC,
2415 "Setting background alpha to %.3f\n", alpha);
2416 m_background_alpha = alpha;
2417
2418 invalidate_all();
2419
2420 return true;
2421 }
2422
2423 void
set_colors_default()2424 Terminal::set_colors_default()
2425 {
2426 set_colors(nullptr, nullptr, nullptr, 0);
2427 }
2428
2429 /*
2430 * Terminal::set_colors:
2431 * @terminal: a #VteTerminal
2432 * @foreground: (allow-none): the new foreground color, or %NULL
2433 * @background: (allow-none): the new background color, or %NULL
2434 * @palette: (array length=palette_size zero-terminated=0): the color palette
2435 * @palette_size: the number of entries in @palette
2436 *
2437 * @palette specifies the new values for the 256 palette colors: 8 standard colors,
2438 * their 8 bright counterparts, 6x6x6 color cube, and 24 grayscale colors.
2439 * Omitted entries will default to a hardcoded value.
2440 *
2441 * @palette_size must be 0, 8, 16, 232 or 256.
2442 *
2443 * If @foreground is %NULL and @palette_size is greater than 0, the new foreground
2444 * color is taken from @palette[7]. If @background is %NULL and @palette_size is
2445 * greater than 0, the new background color is taken from @palette[0].
2446 */
2447 void
set_colors(vte::color::rgb const * foreground,vte::color::rgb const * background,vte::color::rgb const * new_palette,gsize palette_size)2448 Terminal::set_colors(vte::color::rgb const* foreground,
2449 vte::color::rgb const* background,
2450 vte::color::rgb const* new_palette,
2451 gsize palette_size)
2452 {
2453 _vte_debug_print(VTE_DEBUG_MISC,
2454 "Set color palette [%" G_GSIZE_FORMAT " elements].\n",
2455 palette_size);
2456
2457 /* Accept NULL as the default foreground and background colors if we
2458 * got a palette. */
2459 if ((foreground == NULL) && (palette_size >= 8)) {
2460 foreground = &new_palette[7];
2461 }
2462 if ((background == NULL) && (palette_size >= 8)) {
2463 background = &new_palette[0];
2464 }
2465
2466 /* Initialize each item in the palette if we got any entries to work
2467 * with. */
2468 for (gsize i = 0; i < G_N_ELEMENTS(m_palette); i++) {
2469 vte::color::rgb color;
2470 bool unset = false;
2471
2472 if (i < 16) {
2473 color.blue = (i & 4) ? 0xc000 : 0;
2474 color.green = (i & 2) ? 0xc000 : 0;
2475 color.red = (i & 1) ? 0xc000 : 0;
2476 if (i > 7) {
2477 color.blue += 0x3fff;
2478 color.green += 0x3fff;
2479 color.red += 0x3fff;
2480 }
2481 }
2482 else if (i < 232) {
2483 int j = i - 16;
2484 int r = j / 36, g = (j / 6) % 6, b = j % 6;
2485 int red = (r == 0) ? 0 : r * 40 + 55;
2486 int green = (g == 0) ? 0 : g * 40 + 55;
2487 int blue = (b == 0) ? 0 : b * 40 + 55;
2488 color.red = red | red << 8 ;
2489 color.green = green | green << 8;
2490 color.blue = blue | blue << 8;
2491 } else if (i < 256) {
2492 int shade = 8 + (i - 232) * 10;
2493 color.red = color.green = color.blue = shade | shade << 8;
2494 }
2495 else switch (i) {
2496 case VTE_DEFAULT_BG:
2497 if (background) {
2498 color = *background;
2499 } else {
2500 color.red = 0;
2501 color.blue = 0;
2502 color.green = 0;
2503 }
2504 break;
2505 case VTE_DEFAULT_FG:
2506 if (foreground) {
2507 color = *foreground;
2508 } else {
2509 color.red = 0xc000;
2510 color.blue = 0xc000;
2511 color.green = 0xc000;
2512 }
2513 break;
2514 case VTE_BOLD_FG:
2515 unset = true;
2516 break;
2517 case VTE_HIGHLIGHT_BG:
2518 unset = true;
2519 break;
2520 case VTE_HIGHLIGHT_FG:
2521 unset = true;
2522 break;
2523 case VTE_CURSOR_BG:
2524 unset = true;
2525 break;
2526 case VTE_CURSOR_FG:
2527 unset = true;
2528 break;
2529 }
2530
2531 /* Override from the supplied palette if there is one. */
2532 if (i < palette_size) {
2533 color = new_palette[i];
2534 }
2535
2536 /* Set up the color entry. */
2537 if (unset)
2538 reset_color(i, VTE_COLOR_SOURCE_API);
2539 else
2540 set_color(i, VTE_COLOR_SOURCE_API, color);
2541 }
2542 }
2543
2544 /*
2545 * Terminal::set_color_bold:
2546 * @bold: (allow-none): the new bold color or %NULL
2547 *
2548 * Sets the color used to draw bold text in the default foreground color.
2549 * If @bold is %NULL then the default color is used.
2550 */
2551 void
set_color_bold(vte::color::rgb const & color)2552 Terminal::set_color_bold(vte::color::rgb const& color)
2553 {
2554 _vte_debug_print(VTE_DEBUG_MISC,
2555 "Set %s color to (%04x,%04x,%04x).\n", "bold",
2556 color.red, color.green, color.blue);
2557 set_color(VTE_BOLD_FG, VTE_COLOR_SOURCE_API, color);
2558 }
2559
2560 void
reset_color_bold()2561 Terminal::reset_color_bold()
2562 {
2563 _vte_debug_print(VTE_DEBUG_MISC,
2564 "Reset %s color.\n", "bold");
2565 reset_color(VTE_BOLD_FG, VTE_COLOR_SOURCE_API);
2566 }
2567
2568 /*
2569 * Terminal::set_color_foreground:
2570 * @foreground: the new foreground color
2571 *
2572 * Sets the foreground color used to draw normal text.
2573 */
2574 void
set_color_foreground(vte::color::rgb const & color)2575 Terminal::set_color_foreground(vte::color::rgb const& color)
2576 {
2577 _vte_debug_print(VTE_DEBUG_MISC,
2578 "Set %s color to (%04x,%04x,%04x).\n", "foreground",
2579 color.red, color.green, color.blue);
2580 set_color(VTE_DEFAULT_FG, VTE_COLOR_SOURCE_API, color);
2581 }
2582
2583 /*
2584 * Terminal::set_color_background:
2585 * @background: the new background color
2586 *
2587 * Sets the background color for text which does not have a specific background
2588 * color assigned. Only has effect when no background image is set and when
2589 * the terminal is not transparent.
2590 */
2591 void
set_color_background(vte::color::rgb const & color)2592 Terminal::set_color_background(vte::color::rgb const& color)
2593 {
2594 _vte_debug_print(VTE_DEBUG_MISC,
2595 "Set %s color to (%04x,%04x,%04x).\n", "background",
2596 color.red, color.green, color.blue);
2597 set_color(VTE_DEFAULT_BG, VTE_COLOR_SOURCE_API, color);
2598 }
2599
2600 /*
2601 * Terminal::set_color_cursor_background:
2602 * @cursor_background: (allow-none): the new color to use for the text cursor, or %NULL
2603 *
2604 * Sets the background color for text which is under the cursor. If %NULL, text
2605 * under the cursor will be drawn with foreground and background colors
2606 * reversed.
2607 */
2608 void
set_color_cursor_background(vte::color::rgb const & color)2609 Terminal::set_color_cursor_background(vte::color::rgb const& color)
2610 {
2611 _vte_debug_print(VTE_DEBUG_MISC,
2612 "Set %s color to (%04x,%04x,%04x).\n", "cursor background",
2613 color.red, color.green, color.blue);
2614 set_color(VTE_CURSOR_BG, VTE_COLOR_SOURCE_API, color);
2615 }
2616
2617 void
reset_color_cursor_background()2618 Terminal::reset_color_cursor_background()
2619 {
2620 _vte_debug_print(VTE_DEBUG_MISC,
2621 "Reset %s color.\n", "cursor background");
2622 reset_color(VTE_CURSOR_BG, VTE_COLOR_SOURCE_API);
2623 }
2624
2625 /*
2626 * Terminal::set_color_cursor_foreground:
2627 * @cursor_foreground: (allow-none): the new color to use for the text cursor, or %NULL
2628 *
2629 * Sets the foreground color for text which is under the cursor. If %NULL, text
2630 * under the cursor will be drawn with foreground and background colors
2631 * reversed.
2632 */
2633 void
set_color_cursor_foreground(vte::color::rgb const & color)2634 Terminal::set_color_cursor_foreground(vte::color::rgb const& color)
2635 {
2636 _vte_debug_print(VTE_DEBUG_MISC,
2637 "Set %s color to (%04x,%04x,%04x).\n", "cursor foreground",
2638 color.red, color.green, color.blue);
2639 set_color(VTE_CURSOR_FG, VTE_COLOR_SOURCE_API, color);
2640 }
2641
2642 void
reset_color_cursor_foreground()2643 Terminal::reset_color_cursor_foreground()
2644 {
2645 _vte_debug_print(VTE_DEBUG_MISC,
2646 "Reset %s color.\n", "cursor foreground");
2647 reset_color(VTE_CURSOR_FG, VTE_COLOR_SOURCE_API);
2648 }
2649
2650 /*
2651 * Terminal::set_color_highlight_background:
2652 * @highlight_background: (allow-none): the new color to use for highlighted text, or %NULL
2653 *
2654 * Sets the background color for text which is highlighted. If %NULL,
2655 * it is unset. If neither highlight background nor highlight foreground are set,
2656 * highlighted text (which is usually highlighted because it is selected) will
2657 * be drawn with foreground and background colors reversed.
2658 */
2659 void
set_color_highlight_background(vte::color::rgb const & color)2660 Terminal::set_color_highlight_background(vte::color::rgb const& color)
2661 {
2662 _vte_debug_print(VTE_DEBUG_MISC,
2663 "Set %s color to (%04x,%04x,%04x).\n", "highlight background",
2664 color.red, color.green, color.blue);
2665 set_color(VTE_HIGHLIGHT_BG, VTE_COLOR_SOURCE_API, color);
2666 }
2667
2668 void
reset_color_highlight_background()2669 Terminal::reset_color_highlight_background()
2670 {
2671 _vte_debug_print(VTE_DEBUG_MISC,
2672 "Reset %s color.\n", "highlight background");
2673 reset_color(VTE_HIGHLIGHT_BG, VTE_COLOR_SOURCE_API);
2674 }
2675
2676 /*
2677 * Terminal::set_color_highlight_foreground:
2678 * @highlight_foreground: (allow-none): the new color to use for highlighted text, or %NULL
2679 *
2680 * Sets the foreground color for text which is highlighted. If %NULL,
2681 * it is unset. If neither highlight background nor highlight foreground are set,
2682 * highlighted text (which is usually highlighted because it is selected) will
2683 * be drawn with foreground and background colors reversed.
2684 */
2685 void
set_color_highlight_foreground(vte::color::rgb const & color)2686 Terminal::set_color_highlight_foreground(vte::color::rgb const& color)
2687 {
2688 _vte_debug_print(VTE_DEBUG_MISC,
2689 "Set %s color to (%04x,%04x,%04x).\n", "highlight foreground",
2690 color.red, color.green, color.blue);
2691 set_color(VTE_HIGHLIGHT_FG, VTE_COLOR_SOURCE_API, color);
2692 }
2693
2694 void
reset_color_highlight_foreground()2695 Terminal::reset_color_highlight_foreground()
2696 {
2697 _vte_debug_print(VTE_DEBUG_MISC,
2698 "Reset %s color.\n", "highlight foreground");
2699 reset_color(VTE_HIGHLIGHT_FG, VTE_COLOR_SOURCE_API);
2700 }
2701
2702 /*
2703 * Terminal::cleanup_fragments:
2704 * @start: the starting column, inclusive
2705 * @end: the end column, exclusive
2706 *
2707 * Needs to be called before modifying the contents in the cursor's row,
2708 * between the two given columns. Cleans up TAB and CJK fragments to the
2709 * left of @start and to the right of @end. If a CJK is split in half,
2710 * the remaining half is replaced by a space. If a TAB at @start is split,
2711 * it is replaced by spaces. If a TAB at @end is split, it is replaced by
2712 * a shorter TAB. @start and @end can be equal if characters will be
2713 * inserted at the location rather than overwritten.
2714 *
2715 * The area between @start and @end is not cleaned up, hence the whole row
2716 * can be left in an inconsistent state. It is expected that the caller
2717 * will fill up that range afterwards, resulting in a consistent row again.
2718 *
2719 * Invalidates the cells that visually change outside of the range,
2720 * because the caller can't reasonably be expected to take care of this.
2721 */
2722 void
cleanup_fragments(long start,long end)2723 Terminal::cleanup_fragments(long start,
2724 long end)
2725 {
2726 VteRowData *row = ensure_row();
2727 const VteCell *cell_start;
2728 VteCell *cell_end, *cell_col;
2729 gboolean cell_start_is_fragment;
2730 long col;
2731
2732 g_assert(end >= start);
2733
2734 /* Remember whether the cell at start is a fragment. We'll need to know it when
2735 * handling the left hand side, but handling the right hand side first might
2736 * overwrite it if start == end (inserting to the middle of a character). */
2737 cell_start = _vte_row_data_get (row, start);
2738 cell_start_is_fragment = cell_start != NULL && cell_start->attr.fragment();
2739
2740 /* On the right hand side, try to replace a TAB by a shorter TAB if we can.
2741 * This requires that the TAB on the left (which might be the same TAB) is
2742 * not yet converted to spaces, so start on the right hand side. */
2743 cell_end = _vte_row_data_get_writable (row, end);
2744 if (G_UNLIKELY (cell_end != NULL && cell_end->attr.fragment())) {
2745 col = end;
2746 do {
2747 col--;
2748 g_assert(col >= 0); /* The first cell can't be a fragment. */
2749 cell_col = _vte_row_data_get_writable (row, col);
2750 } while (cell_col->attr.fragment());
2751 if (cell_col->c == '\t') {
2752 _vte_debug_print(VTE_DEBUG_MISC,
2753 "Replacing right part of TAB with a shorter one at %ld (%ld cells) => %ld (%ld cells)\n",
2754 col, (long) cell_col->attr.columns(), end, (long) cell_col->attr.columns() - (end - col));
2755 cell_end->c = '\t';
2756 cell_end->attr.set_fragment(false);
2757 g_assert(cell_col->attr.columns() > end - col);
2758 cell_end->attr.set_columns(cell_col->attr.columns() - (end - col));
2759 } else {
2760 _vte_debug_print(VTE_DEBUG_MISC,
2761 "Cleaning CJK right half at %ld\n",
2762 end);
2763 g_assert(end - col == 1 && cell_col->attr.columns() == 2);
2764 cell_end->c = ' ';
2765 cell_end->attr.set_fragment(false);
2766 cell_end->attr.set_columns(1);
2767 invalidate_row_and_context(m_screen->cursor.row); /* FIXME can we do cheaper? */
2768 }
2769 }
2770
2771 /* Handle the left hand side. Converting longer TABs to shorter ones probably
2772 * wouldn't make that much sense here, so instead convert to spaces. */
2773 if (G_UNLIKELY (cell_start_is_fragment)) {
2774 gboolean keep_going = TRUE;
2775 col = start;
2776 do {
2777 col--;
2778 g_assert(col >= 0); /* The first cell can't be a fragment. */
2779 cell_col = _vte_row_data_get_writable (row, col);
2780 if (!cell_col->attr.fragment()) {
2781 if (cell_col->c == '\t') {
2782 _vte_debug_print(VTE_DEBUG_MISC,
2783 "Replacing left part of TAB with spaces at %ld (%ld => %ld cells)\n",
2784 col, (long)cell_col->attr.columns(), start - col);
2785 /* nothing to do here */
2786 } else {
2787 _vte_debug_print(VTE_DEBUG_MISC,
2788 "Cleaning CJK left half at %ld\n",
2789 col);
2790 g_assert(start - col == 1);
2791 invalidate_row_and_context(m_screen->cursor.row); /* FIXME can we do cheaper? */
2792 }
2793 keep_going = FALSE;
2794 }
2795 cell_col->c = ' ';
2796 cell_col->attr.set_fragment(false);
2797 cell_col->attr.set_columns(1);
2798 } while (keep_going);
2799 }
2800 }
2801
2802 /* Cursor down, with scrolling. */
2803 void
cursor_down(bool explicit_sequence)2804 Terminal::cursor_down(bool explicit_sequence)
2805 {
2806 long start, end;
2807
2808 if (m_scrolling_restricted) {
2809 start = m_screen->insert_delta + m_scrolling_region.start;
2810 end = m_screen->insert_delta + m_scrolling_region.end;
2811 } else {
2812 start = m_screen->insert_delta;
2813 end = start + m_row_count - 1;
2814 }
2815 if (m_screen->cursor.row == end) {
2816 if (m_scrolling_restricted) {
2817 if (start == m_screen->insert_delta) {
2818 /* Set the boundary to hard wrapped where
2819 * we're about to tear apart the contents. */
2820 set_hard_wrapped(m_screen->cursor.row);
2821 /* Scroll this line into the scrollback
2822 * buffer by inserting a line at the next
2823 * line and scrolling the area up. */
2824 m_screen->insert_delta++;
2825 m_screen->cursor.row++;
2826 /* Update start and end, too. */
2827 start++;
2828 end++;
2829 ring_insert(m_screen->cursor.row, false);
2830 /* Repaint the affected lines, which is _below_
2831 * the region (bug 131). No need to extend,
2832 * set_hard_wrapped() took care of invalidating
2833 * the context lines if necessary. */
2834 invalidate_rows(m_screen->cursor.row,
2835 m_screen->insert_delta + m_row_count - 1);
2836 /* Force scroll. */
2837 adjust_adjustments();
2838 } else {
2839 /* Set the boundaries to hard wrapped where
2840 * we're about to tear apart the contents. */
2841 set_hard_wrapped(start - 1);
2842 set_hard_wrapped(end);
2843 /* Scroll by removing a line and inserting a new one. */
2844 ring_remove(start);
2845 ring_insert(end, true);
2846 /* Repaint the affected lines. No need to extend,
2847 * set_hard_wrapped() took care of invalidating
2848 * the context lines if necessary. */
2849 invalidate_rows(start, end);
2850 }
2851 } else {
2852 /* Scroll up with history. */
2853 m_screen->cursor.row++;
2854 update_insert_delta();
2855 }
2856
2857 /* Handle bce (background color erase), however, diverge from xterm:
2858 * only fill the new row with the background color if scrolling
2859 * happens due to an explicit escape sequence, not due to autowrapping.
2860 * See bug 754596 for details. */
2861 bool const not_default_bg = (m_color_defaults.attr.back() != VTE_DEFAULT_BG);
2862
2863 if (explicit_sequence && not_default_bg) {
2864 VteRowData *rowdata = ensure_row();
2865 _vte_row_data_fill (rowdata, &m_color_defaults, m_column_count);
2866 }
2867 } else if (m_screen->cursor.row < m_screen->insert_delta + m_row_count - 1) {
2868 /* Otherwise, just move the cursor down; unless it's already in the last
2869 * physical row (which is possible with scrolling region, see #176). */
2870 m_screen->cursor.row++;
2871 }
2872 }
2873
2874 /* Drop the scrollback. */
2875 void
drop_scrollback()2876 Terminal::drop_scrollback()
2877 {
2878 /* Only for normal screen; alternate screen doesn't have a scrollback. */
2879 _vte_ring_drop_scrollback (m_normal_screen.row_data,
2880 m_normal_screen.insert_delta);
2881
2882 if (m_screen == &m_normal_screen) {
2883 queue_adjustment_value_changed(m_normal_screen.insert_delta);
2884 adjust_adjustments_full();
2885 }
2886 }
2887
2888 /* Restore cursor on a screen. */
2889 void
restore_cursor(VteScreen * screen__)2890 Terminal::restore_cursor(VteScreen *screen__)
2891 {
2892 screen__->cursor.col = screen__->saved.cursor.col;
2893 screen__->cursor.row = screen__->insert_delta + CLAMP(screen__->saved.cursor.row,
2894 0, m_row_count - 1);
2895
2896 m_modes_ecma.set_modes(screen__->saved.modes_ecma);
2897
2898 m_modes_private.set_DEC_REVERSE_IMAGE(screen__->saved.reverse_mode);
2899 m_modes_private.set_DEC_ORIGIN(screen__->saved.origin_mode);
2900
2901 m_defaults = screen__->saved.defaults;
2902 m_color_defaults = screen__->saved.color_defaults;
2903 m_character_replacements[0] = screen__->saved.character_replacements[0];
2904 m_character_replacements[1] = screen__->saved.character_replacements[1];
2905 m_character_replacement = screen__->saved.character_replacement;
2906 }
2907
2908 /* Save cursor on a screen__. */
2909 void
save_cursor(VteScreen * screen__)2910 Terminal::save_cursor(VteScreen *screen__)
2911 {
2912 screen__->saved.cursor.col = screen__->cursor.col;
2913 screen__->saved.cursor.row = screen__->cursor.row - screen__->insert_delta;
2914
2915 screen__->saved.modes_ecma = m_modes_ecma.get_modes();
2916
2917 screen__->saved.reverse_mode = m_modes_private.DEC_REVERSE_IMAGE();
2918 screen__->saved.origin_mode = m_modes_private.DEC_ORIGIN();
2919
2920 screen__->saved.defaults = m_defaults;
2921 screen__->saved.color_defaults = m_color_defaults;
2922 screen__->saved.character_replacements[0] = m_character_replacements[0];
2923 screen__->saved.character_replacements[1] = m_character_replacements[1];
2924 screen__->saved.character_replacement = m_character_replacement;
2925 }
2926
2927 /* Insert a single character into the stored data array. */
2928 void
insert_char(gunichar c,bool insert,bool invalidate_now)2929 Terminal::insert_char(gunichar c,
2930 bool insert,
2931 bool invalidate_now)
2932 {
2933 VteCellAttr attr;
2934 VteRowData *row;
2935 long col;
2936 int columns, i;
2937 bool line_wrapped = false; /* cursor moved before char inserted */
2938 gunichar c_unmapped = c;
2939
2940 /* DEC Special Character and Line Drawing Set. VT100 and higher (per XTerm docs). */
2941 static const gunichar line_drawing_map[32] = {
2942 0x0020, /* _ => blank (space) */
2943 0x25c6, /* ` => diamond */
2944 0x2592, /* a => checkerboard */
2945 0x2409, /* b => HT symbol */
2946 0x240c, /* c => FF symbol */
2947 0x240d, /* d => CR symbol */
2948 0x240a, /* e => LF symbol */
2949 0x00b0, /* f => degree */
2950 0x00b1, /* g => plus/minus */
2951 0x2424, /* h => NL symbol */
2952 0x240b, /* i => VT symbol */
2953 0x2518, /* j => downright corner */
2954 0x2510, /* k => upright corner */
2955 0x250c, /* l => upleft corner */
2956 0x2514, /* m => downleft corner */
2957 0x253c, /* n => cross */
2958 0x23ba, /* o => scan line 1/9 */
2959 0x23bb, /* p => scan line 3/9 */
2960 0x2500, /* q => horizontal line (also scan line 5/9) */
2961 0x23bc, /* r => scan line 7/9 */
2962 0x23bd, /* s => scan line 9/9 */
2963 0x251c, /* t => left t */
2964 0x2524, /* u => right t */
2965 0x2534, /* v => bottom t */
2966 0x252c, /* w => top t */
2967 0x2502, /* x => vertical line */
2968 0x2264, /* y => <= */
2969 0x2265, /* z => >= */
2970 0x03c0, /* { => pi */
2971 0x2260, /* | => not equal */
2972 0x00a3, /* } => pound currency sign */
2973 0x00b7, /* ~ => bullet */
2974 };
2975
2976 insert |= m_modes_ecma.IRM();
2977
2978 /* If we've enabled the special drawing set, map the characters to
2979 * Unicode. */
2980 if (G_UNLIKELY (*m_character_replacement == VTE_CHARACTER_REPLACEMENT_LINE_DRAWING)) {
2981 if (c >= 95 && c <= 126)
2982 c = line_drawing_map[c - 95];
2983 }
2984
2985 /* Figure out how many columns this character should occupy. */
2986 columns = _vte_unichar_width(c, m_utf8_ambiguous_width);
2987
2988 /* If we're autowrapping here, do it. */
2989 col = m_screen->cursor.col;
2990 if (G_UNLIKELY (columns && col + columns > m_column_count)) {
2991 if (m_modes_private.DEC_AUTOWRAP()) {
2992 _vte_debug_print(VTE_DEBUG_ADJ,
2993 "Autowrapping before character\n");
2994 /* Wrap. */
2995 /* XXX clear to the end of line */
2996 col = m_screen->cursor.col = 0;
2997 /* Mark this line as soft-wrapped. */
2998 row = ensure_row();
2999 set_soft_wrapped(m_screen->cursor.row);
3000 cursor_down(false);
3001 ensure_row();
3002 apply_bidi_attributes(m_screen->cursor.row, row->attr.bidi_flags, VTE_BIDI_FLAG_ALL);
3003 } else {
3004 /* Don't wrap, stay at the rightmost column. */
3005 col = m_screen->cursor.col =
3006 m_column_count - columns;
3007 }
3008 line_wrapped = true;
3009 }
3010
3011 _vte_debug_print(VTE_DEBUG_PARSER,
3012 "Inserting U+%04X '%lc' (colors %" G_GUINT64_FORMAT ") (%ld+%d, %ld), delta = %ld; ",
3013 (unsigned int)c, g_unichar_isprint(c) ? c : 0xfffd,
3014 m_color_defaults.attr.colors(),
3015 col, columns, (long)m_screen->cursor.row,
3016 (long)m_screen->insert_delta);
3017
3018 //FIXMEchpe
3019 if (G_UNLIKELY(c == 0))
3020 goto not_inserted;
3021
3022 if (G_UNLIKELY (columns == 0)) {
3023
3024 /* It's a combining mark */
3025
3026 long row_num;
3027 VteCell *cell;
3028
3029 _vte_debug_print(VTE_DEBUG_PARSER, "combining U+%04X", c);
3030
3031 row_num = m_screen->cursor.row;
3032 row = NULL;
3033 if (G_UNLIKELY (col == 0)) {
3034 /* We are at first column. See if the previous line softwrapped.
3035 * If it did, move there. Otherwise skip inserting. */
3036
3037 if (G_LIKELY (row_num > 0)) {
3038 row_num--;
3039 row = find_row_data_writable(row_num);
3040
3041 if (row) {
3042 if (!row->attr.soft_wrapped)
3043 row = NULL;
3044 else
3045 col = _vte_row_data_length (row);
3046 }
3047 }
3048 } else {
3049 row = find_row_data_writable(row_num);
3050 }
3051
3052 if (G_UNLIKELY (!row || !col))
3053 goto not_inserted;
3054
3055 /* Combine it on the previous cell */
3056
3057 col--;
3058 cell = _vte_row_data_get_writable (row, col);
3059
3060 if (G_UNLIKELY (!cell))
3061 goto not_inserted;
3062
3063 /* Find the previous cell */
3064 while (cell && cell->attr.fragment() && col > 0)
3065 cell = _vte_row_data_get_writable (row, --col);
3066 if (G_UNLIKELY (!cell || cell->c == '\t'))
3067 goto not_inserted;
3068
3069 /* Combine the new character on top of the cell string */
3070 c = _vte_unistr_append_unichar (cell->c, c);
3071
3072 /* And set it */
3073 columns = cell->attr.columns();
3074 for (i = 0; i < columns; i++) {
3075 cell = _vte_row_data_get_writable (row, col++);
3076 cell->c = c;
3077 }
3078
3079 goto done;
3080 } else {
3081 m_last_graphic_character = c_unmapped;
3082 }
3083
3084 /* Make sure we have enough rows to hold this data. */
3085 row = ensure_cursor();
3086 g_assert(row != NULL);
3087
3088 if (insert) {
3089 cleanup_fragments(col, col);
3090 for (i = 0; i < columns; i++)
3091 _vte_row_data_insert (row, col + i, &basic_cell);
3092 } else {
3093 cleanup_fragments(col, col + columns);
3094 _vte_row_data_fill (row, &basic_cell, col + columns);
3095 }
3096
3097 attr = m_defaults.attr;
3098 attr.set_columns(columns);
3099
3100 {
3101 VteCell *pcell = _vte_row_data_get_writable (row, col);
3102 pcell->c = c;
3103 pcell->attr = attr;
3104 col++;
3105 }
3106
3107 /* insert wide-char fragments */
3108 attr.set_fragment(true);
3109 for (i = 1; i < columns; i++) {
3110 VteCell *pcell = _vte_row_data_get_writable (row, col);
3111 pcell->c = c;
3112 pcell->attr = attr;
3113 col++;
3114 }
3115 if (_vte_row_data_length (row) > m_column_count)
3116 cleanup_fragments(m_column_count, _vte_row_data_length (row));
3117 _vte_row_data_shrink (row, m_column_count);
3118
3119 m_screen->cursor.col = col;
3120
3121 done:
3122 /* Signal that this part of the window needs drawing. */
3123 if (G_UNLIKELY (invalidate_now)) {
3124 invalidate_row_and_context(m_screen->cursor.row);
3125 }
3126
3127 /* We added text, so make a note of it. */
3128 m_text_inserted_flag = TRUE;
3129
3130 not_inserted:
3131 _vte_debug_print(VTE_DEBUG_ADJ|VTE_DEBUG_PARSER,
3132 "insertion delta => %ld.\n",
3133 (long)m_screen->insert_delta);
3134
3135 m_line_wrapped = line_wrapped;
3136 }
3137
3138 guint8
get_bidi_flags() const3139 Terminal::get_bidi_flags() const noexcept
3140 {
3141 return (m_modes_ecma.BDSM() ? VTE_BIDI_FLAG_IMPLICIT : 0) |
3142 (m_bidi_rtl ? VTE_BIDI_FLAG_RTL : 0) |
3143 (m_modes_private.VTE_BIDI_AUTO() ? VTE_BIDI_FLAG_AUTO : 0) |
3144 (m_modes_private.VTE_BIDI_BOX_MIRROR() ? VTE_BIDI_FLAG_BOX_MIRROR : 0);
3145 }
3146
3147 /* Apply the specified BiDi parameters on the paragraph beginning at the specified line. */
3148 void
apply_bidi_attributes(vte::grid::row_t start,guint8 bidi_flags,guint8 bidi_flags_mask)3149 Terminal::apply_bidi_attributes(vte::grid::row_t start, guint8 bidi_flags, guint8 bidi_flags_mask)
3150 {
3151 vte::grid::row_t row = start;
3152 VteRowData *rowdata;
3153
3154 bidi_flags &= bidi_flags_mask;
3155
3156 _vte_debug_print(VTE_DEBUG_BIDI,
3157 "Applying BiDi parameters from row %ld.\n", row);
3158
3159 rowdata = _vte_ring_index_writable (m_screen->row_data, row);
3160 if (rowdata == nullptr || (rowdata->attr.bidi_flags & bidi_flags_mask) == bidi_flags) {
3161 _vte_debug_print(VTE_DEBUG_BIDI,
3162 "BiDi parameters didn't change for this paragraph.\n");
3163 return;
3164 }
3165
3166 while (true) {
3167 rowdata->attr.bidi_flags &= ~bidi_flags_mask;
3168 rowdata->attr.bidi_flags |= bidi_flags;
3169
3170 if (!rowdata->attr.soft_wrapped)
3171 break;
3172
3173 rowdata = _vte_ring_index_writable (m_screen->row_data, row + 1);
3174 if (rowdata == nullptr)
3175 break;
3176 row++;
3177 }
3178
3179 _vte_debug_print(VTE_DEBUG_BIDI,
3180 "Applied BiDi parameters to rows %ld..%ld.\n", start, row);
3181
3182 m_ringview.invalidate();
3183 invalidate_rows(start, row);
3184 }
3185
3186 /* Apply the current BiDi parameters covered by bidi_flags_mask on the current paragraph
3187 * if the cursor is at the first position of this paragraph. */
3188 void
maybe_apply_bidi_attributes(guint8 bidi_flags_mask)3189 Terminal::maybe_apply_bidi_attributes(guint8 bidi_flags_mask)
3190 {
3191 _vte_debug_print(VTE_DEBUG_BIDI,
3192 "Maybe applying BiDi parameters on current paragraph.\n");
3193
3194 if (m_screen->cursor.col != 0) {
3195 _vte_debug_print(VTE_DEBUG_BIDI,
3196 "No, cursor not in first column.\n");
3197 return;
3198 }
3199
3200 auto row = m_screen->cursor.row;
3201
3202 if (row > _vte_ring_delta (m_screen->row_data)) {
3203 const VteRowData *rowdata = _vte_ring_index (m_screen->row_data, row - 1);
3204 if (rowdata != nullptr && rowdata->attr.soft_wrapped) {
3205 _vte_debug_print(VTE_DEBUG_BIDI,
3206 "No, we're not after a hard wrap.\n");
3207 return;
3208 }
3209 }
3210
3211 _vte_debug_print(VTE_DEBUG_BIDI,
3212 "Yes, applying.\n");
3213
3214 apply_bidi_attributes (row, get_bidi_flags(), bidi_flags_mask);
3215 }
3216
3217 static void
reaper_child_exited_cb(VteReaper * reaper,int ipid,int status,vte::terminal::Terminal * that)3218 reaper_child_exited_cb(VteReaper *reaper,
3219 int ipid,
3220 int status,
3221 vte::terminal::Terminal* that) noexcept
3222 try
3223 {
3224 that->child_watch_done(pid_t{ipid}, status);
3225 // @that might be destroyed at this point
3226 }
3227 catch (...)
3228 {
3229 vte::log_exception();
3230 }
3231
3232 void
child_watch_done(pid_t pid,int status)3233 Terminal::child_watch_done(pid_t pid,
3234 int status)
3235 {
3236 if (pid != m_pty_pid)
3237 return;
3238
3239 _VTE_DEBUG_IF (VTE_DEBUG_LIFECYCLE) {
3240 g_printerr ("Child[%d] exited with status %d\n",
3241 pid, status);
3242 #ifdef HAVE_SYS_WAIT_H
3243 if (WIFEXITED (status)) {
3244 g_printerr ("Child[%d] exit code %d.\n",
3245 pid, WEXITSTATUS (status));
3246 } else if (WIFSIGNALED (status)) {
3247 g_printerr ("Child[%d] dies with signal %d.\n",
3248 pid, WTERMSIG (status));
3249 }
3250 #endif
3251 }
3252
3253 /* Disconnect from the reaper */
3254 if (m_reaper) {
3255 g_signal_handlers_disconnect_by_func(m_reaper,
3256 (gpointer)reaper_child_exited_cb,
3257 this);
3258 g_object_unref(m_reaper);
3259 m_reaper = nullptr;
3260 }
3261
3262 m_pty_pid = -1;
3263
3264 /* If we still have a PTY, or data to process, defer emitting the signals
3265 * until we have EOF on the PTY, so that we can process all pending data.
3266 */
3267 if (pty() || !m_incoming_queue.empty()) {
3268 m_child_exit_status = status;
3269 m_child_exited_after_eos_pending = true;
3270
3271 m_child_exited_eos_wait_timer.schedule_seconds(2); // FIXME: better value?
3272 } else {
3273 m_child_exited_after_eos_pending = false;
3274
3275 if (widget())
3276 widget()->emit_child_exited(status);
3277 }
3278 }
3279
3280 static void
mark_input_source_invalid_cb(vte::terminal::Terminal * that)3281 mark_input_source_invalid_cb(vte::terminal::Terminal* that)
3282 {
3283 _vte_debug_print (VTE_DEBUG_IO, "Removed PTY input source\n");
3284 that->m_pty_input_source = 0;
3285 }
3286
3287 /* Read and handle data from the child. */
3288 static gboolean
io_read_cb(int fd,GIOCondition condition,vte::terminal::Terminal * that)3289 io_read_cb(int fd,
3290 GIOCondition condition,
3291 vte::terminal::Terminal* that)
3292 {
3293 return that->pty_io_read(fd, condition);
3294 }
3295
3296 void
connect_pty_read()3297 Terminal::connect_pty_read()
3298 {
3299 if (m_pty_input_source != 0 || !pty())
3300 return;
3301
3302 _vte_debug_print (VTE_DEBUG_IO, "Adding PTY input source\n");
3303
3304 m_pty_input_source = g_unix_fd_add_full(VTE_CHILD_INPUT_PRIORITY,
3305 pty()->fd(),
3306 (GIOCondition)(G_IO_IN | G_IO_PRI | G_IO_HUP | G_IO_ERR),
3307 (GUnixFDSourceFunc)io_read_cb,
3308 this,
3309 (GDestroyNotify)mark_input_source_invalid_cb);
3310 }
3311
3312 static void
mark_output_source_invalid_cb(vte::terminal::Terminal * that)3313 mark_output_source_invalid_cb(vte::terminal::Terminal* that)
3314 {
3315 _vte_debug_print (VTE_DEBUG_IO, "Removed PTY output source\n");
3316 that->m_pty_output_source = 0;
3317 }
3318
3319 /* Send locally-encoded characters to the child. */
3320 static gboolean
io_write_cb(int fd,GIOCondition condition,vte::terminal::Terminal * that)3321 io_write_cb(int fd,
3322 GIOCondition condition,
3323 vte::terminal::Terminal* that)
3324 {
3325 return that->pty_io_write(fd, condition);
3326 }
3327
3328 void
connect_pty_write()3329 Terminal::connect_pty_write()
3330 {
3331 if (m_pty_output_source != 0 || !pty())
3332 return;
3333
3334 g_warn_if_fail(m_input_enabled);
3335
3336 /* Anything to write? */
3337 if (_vte_byte_array_length(m_outgoing) == 0)
3338 return;
3339
3340 /* Do one write. FIXMEchpe why? */
3341 if (!pty_io_write (pty()->fd(), G_IO_OUT))
3342 return;
3343
3344 _vte_debug_print (VTE_DEBUG_IO, "Adding PTY output source\n");
3345
3346 m_pty_output_source = g_unix_fd_add_full(VTE_CHILD_OUTPUT_PRIORITY,
3347 pty()->fd(),
3348 G_IO_OUT,
3349 (GUnixFDSourceFunc)io_write_cb,
3350 this,
3351 (GDestroyNotify)mark_output_source_invalid_cb);
3352 }
3353
3354 void
disconnect_pty_read()3355 Terminal::disconnect_pty_read()
3356 {
3357 if (m_pty_input_source != 0) {
3358 _vte_debug_print (VTE_DEBUG_IO, "Removing PTY input source\n");
3359 g_source_remove(m_pty_input_source);
3360 // FIXMEchpe the destroy notify should already have done this!
3361 m_pty_input_source = 0;
3362 }
3363 }
3364
3365 void
disconnect_pty_write()3366 Terminal::disconnect_pty_write()
3367 {
3368 if (m_pty_output_source != 0) {
3369 _vte_debug_print (VTE_DEBUG_IO, "Removing PTY output source\n");
3370 g_source_remove(m_pty_output_source);
3371 // FIXMEchpe the destroy notify should already have done this!
3372 m_pty_output_source = 0;
3373 }
3374 }
3375
3376 void
pty_termios_changed()3377 Terminal::pty_termios_changed()
3378 {
3379 _vte_debug_print(VTE_DEBUG_IO, "Termios changed\n");
3380 }
3381
3382 void
pty_scroll_lock_changed(bool locked)3383 Terminal::pty_scroll_lock_changed(bool locked)
3384 {
3385 _vte_debug_print(VTE_DEBUG_IO, "Output %s (^%c)\n",
3386 locked ? "stopped" : "started",
3387 locked ? 'Q' : 'S');
3388 }
3389
3390 /*
3391 * Terminal::watch_child:
3392 * @child_pid: a #pid_t
3393 *
3394 * Watches @child_pid. When the process exists, the #VteTerminal::child-exited
3395 * signal will be called with the child's exit status.
3396 *
3397 * Prior to calling this function, a #VtePty must have been set in @terminal
3398 * using vte_terminal_set_pty().
3399 * When the child exits, the terminal's #VtePty will be set to %NULL.
3400 *
3401 * Note: g_child_watch_add() or g_child_watch_add_full() must not have
3402 * been called for @child_pid, nor a #GSource for it been created with
3403 * g_child_watch_source_new().
3404 *
3405 * Note: when using the g_spawn_async() family of functions,
3406 * the %G_SPAWN_DO_NOT_REAP_CHILD flag MUST have been passed.
3407 */
3408 void
watch_child(pid_t child_pid)3409 Terminal::watch_child (pid_t child_pid)
3410 {
3411 // FIXMEchpe: support passing child_pid = -1 to remove the wathch
3412 g_assert(child_pid != -1);
3413 if (!pty())
3414 return;
3415
3416 auto const freezer = vte::glib::FreezeObjectNotify{m_terminal};
3417
3418 /* Set this as the child's pid. */
3419 m_pty_pid = child_pid;
3420
3421 /* Catch a child-exited signal from the child pid. */
3422 auto reaper = vte_reaper_ref();
3423 vte_reaper_add_child(child_pid);
3424 if (reaper != m_reaper) {
3425 if (m_reaper) {
3426 g_signal_handlers_disconnect_by_func(m_reaper,
3427 (gpointer)reaper_child_exited_cb,
3428 this);
3429 g_object_unref(m_reaper);
3430 }
3431 m_reaper = reaper; /* adopts */
3432 g_signal_connect(m_reaper, "child-exited",
3433 G_CALLBACK(reaper_child_exited_cb),
3434 this);
3435 } else {
3436 g_object_unref(reaper);
3437 }
3438
3439 /* FIXMEchpe: call set_size() here? */
3440 }
3441
3442 /* Reset the input method context. */
3443 void
im_reset()3444 Terminal::im_reset()
3445 {
3446 if (widget())
3447 widget()->im_reset();
3448
3449 im_preedit_reset();
3450 }
3451
3452 void
process_incoming()3453 Terminal::process_incoming()
3454 {
3455 _vte_debug_print(VTE_DEBUG_IO,
3456 "Handler processing %" G_GSIZE_FORMAT " bytes over %" G_GSIZE_FORMAT " chunks.\n",
3457 m_input_bytes,
3458 m_incoming_queue.size());
3459 _vte_debug_print (VTE_DEBUG_WORK, "(");
3460
3461 /* We should only be called when there's data to process. */
3462 g_assert(!m_incoming_queue.empty());
3463
3464 // FIXMEchpe move to context
3465 m_line_wrapped = false;
3466
3467 auto bytes_processed = ssize_t{0};
3468
3469 auto context = ProcessingContext{*this};
3470
3471 while (!m_incoming_queue.empty()) {
3472 auto& chunk = m_incoming_queue.front();
3473
3474 assert((bool)chunk);
3475
3476 auto const start = chunk->begin_reading();
3477
3478 _VTE_DEBUG_IF(VTE_DEBUG_IO) {
3479 _vte_debug_print(VTE_DEBUG_IO,
3480 "Processing data syntax %d chunk %p starting at offset %u\n",
3481 (int)current_data_syntax(),
3482 (void*)chunk.get(),
3483 unsigned(chunk->begin_reading() - chunk->data()));
3484
3485 _vte_debug_hexdump("Incoming buffer",
3486 chunk->begin_reading(),
3487 chunk->size_reading());
3488 }
3489
3490 switch (current_data_syntax()) {
3491 case DataSyntax::ECMA48_UTF8:
3492 process_incoming_utf8(context, *chunk);
3493 break;
3494
3495 #ifdef WITH_ICU
3496 case DataSyntax::ECMA48_PCTERM:
3497 process_incoming_pcterm(context, *chunk);
3498 break;
3499 #endif
3500
3501 default:
3502 g_assert_not_reached();
3503 break;
3504 }
3505
3506 bytes_processed += size_t(chunk->begin_reading() - start);
3507
3508 _vte_debug_print(VTE_DEBUG_IO, "read %d bytes, chunk %s, data syntax now %d\n",
3509 int(chunk->begin_reading() - start),
3510 chunk->has_reading()?"has more":"finished",
3511 (int)current_data_syntax());
3512 // If all data from this chunk has been processed, go to the next one
3513 if (!chunk->has_reading())
3514 m_incoming_queue.pop();
3515 }
3516
3517 #ifdef VTE_DEBUG
3518 /* Some safety checks: ensure the visible parts of the buffer
3519 * are all in the buffer. */
3520 g_assert_cmpint(m_screen->insert_delta, >=, _vte_ring_delta(m_screen->row_data));
3521
3522 /* The cursor shouldn't be above or below the addressable
3523 * part of the display buffer. */
3524 g_assert_cmpint(m_screen->cursor.row, >=, m_screen->insert_delta);
3525 #endif
3526
3527 if (context.m_modified) {
3528 /* Keep the cursor on-screen if we scroll on output, or if
3529 * we're currently at the bottom of the buffer. */
3530 update_insert_delta();
3531 if (m_scroll_on_output || context.m_bottom) {
3532 maybe_scroll_to_bottom();
3533 }
3534 /* Deselect the current selection if its contents are changed
3535 * by this insertion. */
3536 if (!m_selection_resolved.empty()) {
3537 //FIXMEchpe: this is atrocious
3538 auto selection = get_selected_text();
3539 if ((selection == nullptr) ||
3540 (m_selection[vte::to_integral(vte::platform::ClipboardType::PRIMARY)] == nullptr) ||
3541 (strcmp(selection->str, m_selection[vte::to_integral(vte::platform::ClipboardType::PRIMARY)]->str) != 0)) {
3542 deselect_all();
3543 }
3544 if (selection)
3545 g_string_free(selection, TRUE);
3546 }
3547 }
3548
3549 if (context.m_modified || (m_screen != context.m_saved_screen)) {
3550 m_ringview.invalidate();
3551 /* Signal that the visible contents changed. */
3552 queue_contents_changed();
3553 }
3554
3555 emit_pending_signals();
3556
3557 if (context.m_invalidated_text) {
3558 invalidate_rows_and_context(context.m_bbox_top, context.m_bbox_bottom);
3559 }
3560
3561 if ((context.m_saved_cursor.col != m_screen->cursor.col) ||
3562 (context.m_saved_cursor.row != m_screen->cursor.row)) {
3563 /* invalidate the old and new cursor positions */
3564 if (context.m_saved_cursor_visible)
3565 invalidate_row(context.m_saved_cursor.row);
3566 invalidate_cursor_once();
3567 check_cursor_blink();
3568 /* Signal that the cursor moved. */
3569 queue_cursor_moved();
3570 } else if ((context.m_saved_cursor_visible != m_modes_private.DEC_TEXT_CURSOR()) ||
3571 (context.m_saved_cursor_style != m_cursor_style)) {
3572 invalidate_row(context.m_saved_cursor.row);
3573 check_cursor_blink();
3574 }
3575
3576 /* Tell the input method where the cursor is. */
3577 im_update_cursor();
3578
3579 /* After processing some data, do a hyperlink GC. The multiplier is totally arbitrary, feel free to fine tune. */
3580 _vte_ring_hyperlink_maybe_gc(m_screen->row_data, bytes_processed * 8);
3581
3582 _vte_debug_print (VTE_DEBUG_WORK, ")");
3583 _vte_debug_print (VTE_DEBUG_IO,
3584 "%" G_GSIZE_FORMAT " bytes in %" G_GSIZE_FORMAT " chunks left to process.\n",
3585 m_input_bytes,
3586 m_incoming_queue.size());
3587 }
3588
3589 /* Note that this code is mostly copied to process_incoming_pcterm() below; any non-charset-decoding
3590 * related changes made here need to be made there, too.
3591 */
3592 void
process_incoming_utf8(ProcessingContext & context,vte::base::Chunk & chunk)3593 Terminal::process_incoming_utf8(ProcessingContext& context,
3594 vte::base::Chunk& chunk)
3595 {
3596 auto seq = vte::parser::Sequence{m_parser};
3597
3598 auto const iend = chunk.end_reading();
3599 auto ip = chunk.begin_reading();
3600
3601 while (ip < iend) {
3602
3603 switch (m_utf8_decoder.decode(*(ip++))) {
3604 case vte::base::UTF8Decoder::REJECT_REWIND:
3605 /* Rewind the stream.
3606 * Note that this will never lead to a loop, since in the
3607 * next round this byte *will* be consumed.
3608 */
3609 --ip;
3610 [[fallthrough]];
3611 case vte::base::UTF8Decoder::REJECT:
3612 m_utf8_decoder.reset();
3613 /* Fall through to insert the U+FFFD replacement character. */
3614 [[fallthrough]];
3615 case vte::base::UTF8Decoder::ACCEPT: {
3616 auto rv = m_parser.feed(m_utf8_decoder.codepoint());
3617 if (G_UNLIKELY(rv < 0)) {
3618 #ifdef DEBUG
3619 uint32_t c = m_utf8_decoder.codepoint();
3620 char c_buf[7];
3621 g_snprintf(c_buf, sizeof(c_buf), "%lc", c);
3622 char const* wp_str = g_unichar_isprint(c) ? c_buf : _vte_debug_sequence_to_string(c_buf, -1);
3623 _vte_debug_print(VTE_DEBUG_PARSER, "Parser error on U+%04X [%s]!\n",
3624 c, wp_str);
3625 #endif
3626 break;
3627 }
3628
3629 #ifdef VTE_DEBUG
3630 if (rv != VTE_SEQ_NONE)
3631 g_assert((bool)seq);
3632 #endif
3633
3634 _VTE_DEBUG_IF(VTE_DEBUG_PARSER) {
3635 if (rv != VTE_SEQ_NONE) {
3636 seq.print();
3637 }
3638 }
3639
3640 // FIXMEchpe this assumes that the only handler inserting
3641 // a character is GRAPHIC, which isn't true (at least ICH, REP, SUB
3642 // also do, and invalidate directly for now)...
3643
3644 switch (rv) {
3645 case VTE_SEQ_GRAPHIC: {
3646
3647 context.pre_GRAPHIC(*this);
3648
3649 // does insert_char(c, false, false)
3650 GRAPHIC(seq);
3651 _vte_debug_print(VTE_DEBUG_PARSER,
3652 "Last graphic is now U+%04X %lc\n",
3653 m_last_graphic_character,
3654 g_unichar_isprint(m_last_graphic_character) ? m_last_graphic_character : 0xfffd);
3655
3656 context.post_GRAPHIC(*this);
3657 break;
3658 }
3659
3660 case VTE_SEQ_NONE:
3661 case VTE_SEQ_IGNORE:
3662 break;
3663
3664 default: {
3665 switch (seq.command()) {
3666 #define _VTE_CMD_HANDLER(cmd) \
3667 case VTE_CMD_##cmd: cmd(seq); break;
3668 #define _VTE_CMD_HANDLER_R(cmd) \
3669 case VTE_CMD_##cmd: if (cmd(seq)) { \
3670 context.post_CMD(*this); \
3671 goto switched_data_syntax; \
3672 } \
3673 break;
3674 #define _VTE_CMD_HANDLER_NOP(cmd)
3675 #include "parser-cmd-handlers.hh"
3676 #undef _VTE_CMD_HANDLER
3677 #undef _VTE_CMD_HANDLER_NOP
3678 #undef _VTE_CMD_HANDLER_R
3679 default:
3680 _vte_debug_print(VTE_DEBUG_PARSER,
3681 "Unknown parser command %d\n", seq.command());
3682 break;
3683 }
3684
3685 m_last_graphic_character = 0;
3686
3687 context.post_CMD(*this);
3688 break;
3689 }
3690 }
3691 break;
3692 }
3693 }
3694 }
3695
3696 if (chunk.eos() && ip == iend) {
3697 m_eos_pending = true;
3698 /* If there's an unfinished character in the queue, insert a replacement character */
3699 if (m_utf8_decoder.flush()) {
3700 insert_char(m_utf8_decoder.codepoint(), false, true);
3701 }
3702 }
3703
3704 #pragma GCC diagnostic push
3705 #pragma GCC diagnostic ignored "-Wunused-label"
3706 switched_data_syntax:
3707 #pragma GCC diagnostic pop
3708
3709 // Update start for data consumed
3710 chunk.set_begin_reading(ip);
3711 }
3712
3713 #ifdef WITH_ICU
3714
3715 /* Note that this is mostly a copy of process_incoming_utf8() above; any non-charset-decoding
3716 * related changes made here need to be made there, too.
3717 */
3718 void
process_incoming_pcterm(ProcessingContext & context,vte::base::Chunk & chunk)3719 Terminal::process_incoming_pcterm(ProcessingContext& context,
3720 vte::base::Chunk& chunk)
3721 {
3722 auto seq = vte::parser::Sequence{m_parser};
3723
3724 auto& decoder = m_converter->decoder();
3725
3726 auto eos = bool{false};
3727 auto flush = bool{false};
3728
3729 auto const iend = chunk.end_reading();
3730 auto ip = chunk.begin_reading();
3731
3732 start:
3733 while (ip < iend || flush) {
3734 switch (decoder.decode(&ip, flush)) {
3735 case vte::base::ICUDecoder::Result::eSomething: {
3736 auto rv = m_parser.feed(decoder.codepoint());
3737 if (G_UNLIKELY(rv < 0)) {
3738 #ifdef VTE_DEBUG
3739 uint32_t c = decoder.codepoint();
3740 char c_buf[7];
3741 g_snprintf(c_buf, sizeof(c_buf), "%lc", c);
3742 char const* wp_str = g_unichar_isprint(c) ? c_buf : _vte_debug_sequence_to_string(c_buf, -1);
3743 _vte_debug_print(VTE_DEBUG_PARSER, "Parser error on U+%04X [%s]!\n",
3744 c, wp_str);
3745 #endif
3746 break;
3747 }
3748
3749 #ifdef VTE_DEBUG
3750 if (rv != VTE_SEQ_NONE)
3751 g_assert((bool)seq);
3752 #endif
3753
3754 _VTE_DEBUG_IF(VTE_DEBUG_PARSER) {
3755 if (rv != VTE_SEQ_NONE) {
3756 seq.print();
3757 }
3758 }
3759
3760 // FIXMEchpe this assumes that the only handler inserting
3761 // a character is GRAPHIC, which isn't true (at least ICH, REP, SUB
3762 // also do, and invalidate directly for now)...
3763
3764 switch (rv) {
3765 case VTE_SEQ_GRAPHIC: {
3766
3767 context.pre_GRAPHIC(*this);
3768
3769 // does insert_char(c, false, false)
3770 GRAPHIC(seq);
3771 _vte_debug_print(VTE_DEBUG_PARSER,
3772 "Last graphic is now U+%04X %lc\n",
3773 m_last_graphic_character,
3774 g_unichar_isprint(m_last_graphic_character) ? m_last_graphic_character : 0xfffd);
3775
3776 context.post_GRAPHIC(*this);
3777 break;
3778 }
3779
3780 case VTE_SEQ_NONE:
3781 case VTE_SEQ_IGNORE:
3782 break;
3783
3784 default: {
3785 switch (seq.command()) {
3786 #define _VTE_CMD_HANDLER(cmd) \
3787 case VTE_CMD_##cmd: cmd(seq); break;
3788 #define _VTE_CMD_HANDLER_R(cmd) \
3789 case VTE_CMD_##cmd: \
3790 if (cmd(seq)) { \
3791 context.post_CMD(*this); \
3792 goto switched_data_syntax; \
3793 } \
3794 break;
3795 #define _VTE_CMD_HANDLER_NOP(cmd)
3796 #include "parser-cmd-handlers.hh"
3797 #undef _VTE_CMD_HANDLER
3798 #undef _VTE_CMD_HANDLER_NOP
3799 #undef _VTE_CMD_HANDLER_R
3800 default:
3801 _vte_debug_print(VTE_DEBUG_PARSER,
3802 "Unknown parser command %d\n", seq.command());
3803 break;
3804 }
3805
3806 m_last_graphic_character = 0;
3807
3808 context.post_CMD(*this);
3809 break;
3810 }
3811 }
3812 break;
3813 }
3814 case vte::base::ICUDecoder::Result::eNothing:
3815 flush = false;
3816 break;
3817
3818 case vte::base::ICUDecoder::Result::eError:
3819 // FIXMEchpe do we need ++ip here?
3820 decoder.reset();
3821 break;
3822
3823 }
3824 }
3825
3826 if (eos) {
3827 /* Done processing the last chunk */
3828 m_eos_pending = true;
3829 return;
3830 }
3831
3832 #pragma GCC diagnostic push
3833 #pragma GCC diagnostic ignored "-Wunused-label"
3834 switched_data_syntax:
3835 #pragma GCC diagnostic pop
3836
3837 // Update start for data consumed
3838 chunk.set_begin_reading(ip);
3839
3840 if (chunk.eos() && ip == chunk.end_reading()) {
3841 /* On EOS, we still need to flush the decoder before we can finish */
3842 eos = flush = true;
3843 goto start;
3844 }
3845 }
3846
3847 #endif /* WITH_ICU */
3848
3849 bool
pty_io_read(int const fd,GIOCondition const condition)3850 Terminal::pty_io_read(int const fd,
3851 GIOCondition const condition)
3852 {
3853 _vte_debug_print (VTE_DEBUG_WORK, ".");
3854 _vte_debug_print(VTE_DEBUG_IO, "::pty_io_read condition %02x\n", condition);
3855
3856 /* We need to check for EOS so that we can shut down the PTY.
3857 * When we get G_IO_HUP without G_IO_IN, we can process the EOF now.
3858 * However when we get G_IO_IN | G_IO_HUP, there is still data to be
3859 * read in this round, and in potentially more rounds; read the data
3860 * now, do *not* process the EOS (unless the read returns EIO, which
3861 * does happen and appears to mean that despite G_IO_IN no data was
3862 * actually available to be read, not even the cpkt header), and
3863 * otherwise wait for further calls which will have G_IO_HUP (and
3864 * possibly G_IO_IN again).
3865 */
3866 auto eos = bool{condition == G_IO_HUP};
3867
3868 /* There is data to read */
3869 auto err = int{0};
3870 auto again = bool{true};
3871 vte::base::Chunk* chunk{nullptr};
3872 if (condition & (G_IO_IN | G_IO_PRI)) {
3873 guchar *bp;
3874 int rem, len;
3875 guint bytes, max_bytes;
3876
3877 /* Limit the amount read between updates, so as to
3878 * 1. maintain fairness between multiple terminals;
3879 * 2. prevent reading the entire output of a command in one
3880 * pass, i.e. we always try to refresh the terminal ~40Hz.
3881 * See time_process_incoming() where we estimate the
3882 * maximum number of bytes we can read/process in between
3883 * updates.
3884 */
3885 max_bytes = m_active_terminals_link != nullptr ?
3886 g_list_length(g_active_terminals) - 1 : 0;
3887 if (max_bytes) {
3888 max_bytes = m_max_input_bytes / max_bytes;
3889 } else {
3890 max_bytes = m_max_input_bytes;
3891 }
3892 bytes = m_input_bytes;
3893
3894 /* If possible, try adding more data to the chunk at the back of the queue */
3895 if (!m_incoming_queue.empty())
3896 chunk = m_incoming_queue.back().get();
3897
3898 do {
3899 /* No chunk, chunk sealed or at least ¾ full? Get a new chunk */
3900 if (!chunk ||
3901 chunk->sealed() ||
3902 chunk->capacity_writing() < chunk->capacity() / 4) {
3903 m_incoming_queue.push(vte::base::Chunk::get(chunk));
3904 chunk = m_incoming_queue.back().get();
3905 }
3906
3907 rem = chunk->capacity_writing();
3908 bp = chunk->begin_writing();
3909 len = 0;
3910 do {
3911 #if defined(TIOCPKT)
3912 /* We'd like to read (fd, bp, rem); but due to TIOCPKT mode
3913 * there's an extra input byte returned at the beginning.
3914 * We need to see what that byte is, but otherwise drop it
3915 * and write continuously to chunk->data.
3916 */
3917 auto const save = bp[-1];
3918 errno = 0;
3919 auto ret = read(fd, bp - 1, rem + 1);
3920 auto const pkt_header = bp[-1];
3921 bp[-1] = save;
3922
3923 switch (ret){
3924 case -1:
3925 err = errno;
3926 goto out;
3927 case 0:
3928 eos = true;
3929 goto out;
3930 default:
3931 ret--;
3932
3933 if (pkt_header == TIOCPKT_DATA) {
3934 bp += ret;
3935 rem -= ret;
3936 len += ret;
3937 } else {
3938 if (pkt_header & TIOCPKT_IOCTL) {
3939 /* We'd like to always be informed when the termios change,
3940 * so we can e.g. detect when no-echo is en/disabled and
3941 * change the cursor/input method/etc., but unfortunately
3942 * the kernel only sends this flag when (old or new) 'local flags'
3943 * include EXTPROC, which is not used often, and due to its side
3944 * effects, cannot be enabled by vte by default.
3945 *
3946 * FIXME: improve the kernel! see discussion in bug 755371
3947 * starting at comment 12
3948 */
3949 pty_termios_changed();
3950 }
3951 if (pkt_header & TIOCPKT_STOP) {
3952 pty_scroll_lock_changed(true);
3953 }
3954 if (pkt_header & TIOCPKT_START) {
3955 pty_scroll_lock_changed(false);
3956 }
3957 }
3958 break;
3959 }
3960 #elif defined(__sun) && defined(HAVE_STROPTS_H)
3961 static unsigned char ctl_s[128];
3962 struct strbuf ctlbuf, databuf;
3963 int ret, flags = 0;
3964 bool have_data = false;
3965
3966 ctlbuf.buf = (caddr_t)ctl_s;
3967 ctlbuf.maxlen = sizeof(ctl_s);
3968 databuf.buf = (caddr_t)bp;
3969 databuf.maxlen = rem;
3970
3971 ret = getmsg(fd, &ctlbuf, &databuf, &flags);
3972 if (ret == -1) {
3973 err = errno;
3974 goto out;
3975 } else if (ctlbuf.len == 1) {
3976 switch (ctl_s[0]) {
3977 case M_IOCTL:
3978 pty_termios_changed();
3979 break;
3980 case M_STOP:
3981 pty_scroll_lock_changed(true);
3982 break;
3983 case M_START:
3984 pty_scroll_lock_changed(false);
3985 break;
3986 case M_DATA:
3987 have_data = true;
3988 break;
3989 }
3990 } else if (ctlbuf.len == -1 && databuf.len != -1) {
3991 // MOREDATA
3992 have_data = true;
3993 }
3994
3995 if (have_data) {
3996 if (databuf.len == 0) {
3997 eos = true;
3998 goto out;
3999 }
4000 bp += databuf.len;
4001 rem -= databuf.len;
4002 len += databuf.len;
4003 }
4004 #else /* neither TIOCPKT nor STREAMS pty */
4005 int ret = read(fd, bp, rem);
4006 switch (ret) {
4007 case -1:
4008 err = errno;
4009 goto out;
4010 case 0:
4011 eos = true;
4012 goto out;
4013 default:
4014 bp += ret;
4015 rem -= ret;
4016 len += ret;
4017 break;
4018 }
4019 #endif /* */
4020 } while (rem);
4021 out:
4022 chunk->add_size(len);
4023 bytes += len;
4024 } while (bytes < max_bytes &&
4025 // This means that a read into a not-yet-¾-full
4026 // chunk used up all the available capacity, so
4027 // let's assume that we can read more and thus
4028 // we'll get a new chunk in the loop above and
4029 // continue on. (See commit 49a0cdf11.)
4030 // Note also that on EOS or error, this condition
4031 // is false (since there was capacity, but it wasn't
4032 // used up).
4033 chunk->capacity_writing() == 0);
4034
4035 /* We may have an empty chunk at the back of the queue, but
4036 * that doesn't matter, we'll fill it next time.
4037 */
4038
4039 if (!is_processing()) {
4040 add_process_timeout(this);
4041 }
4042 m_pty_input_active = len != 0;
4043 m_input_bytes = bytes;
4044 again = bytes < max_bytes;
4045
4046 _vte_debug_print (VTE_DEBUG_IO, "read %d/%d bytes, again? %s, active? %s\n",
4047 bytes, max_bytes,
4048 again ? "yes" : "no",
4049 m_pty_input_active ? "yes" : "no");
4050 }
4051
4052 if (condition & G_IO_ERR)
4053 err = EIO;
4054
4055 /* Error? */
4056 switch (err) {
4057 case 0: /* no error */
4058 break;
4059 case EIO: /* EOS */
4060 eos = true;
4061 break;
4062 case EAGAIN:
4063 case EBUSY: /* do nothing */
4064 break;
4065 default:
4066 auto errsv = vte::libc::ErrnoSaver{};
4067 _vte_debug_print (VTE_DEBUG_IO, "Error reading from child: %s",
4068 g_strerror(errsv));
4069 break;
4070 }
4071
4072 if (eos) {
4073 _vte_debug_print(VTE_DEBUG_IO, "got PTY EOF\n");
4074
4075 /* Make a note of the EOS; but do not process it since there may be data
4076 * to be processed first in the incoming queue.
4077 */
4078 if (!chunk || chunk->sealed()) {
4079 m_incoming_queue.push(vte::base::Chunk::get(chunk));
4080 chunk = m_incoming_queue.back().get();
4081 }
4082
4083 chunk->set_sealed();
4084 chunk->set_eos();
4085
4086 /* Cancel wait timer */
4087 m_child_exited_eos_wait_timer.abort();
4088
4089 /* Need to process the EOS */
4090 if (!is_processing()) {
4091 add_process_timeout(this);
4092 }
4093
4094 again = false;
4095 }
4096
4097 return again;
4098 }
4099
4100 /*
4101 * Terminal::feed:
4102 * @data: data
4103 *
4104 * Interprets @data as if it were data received from a child process.
4105 */
4106 void
feed(std::string_view const & data,bool start_processing_)4107 Terminal::feed(std::string_view const& data,
4108 bool start_processing_)
4109 {
4110 auto length = data.size();
4111 auto ptr = data.data();
4112
4113 vte::base::Chunk* chunk = nullptr;
4114 if (!m_incoming_queue.empty()) {
4115 auto& achunk = m_incoming_queue.back();
4116 if (length < achunk->capacity_writing() && !achunk->sealed())
4117 chunk = achunk.get();
4118 }
4119 if (chunk == nullptr) {
4120 m_incoming_queue.push(vte::base::Chunk::get(nullptr));
4121 chunk = m_incoming_queue.back().get();
4122 }
4123
4124 /* Break the incoming data into chunks. */
4125 do {
4126 auto rem = chunk->capacity_writing();
4127 auto len = std::min(length, rem);
4128 memcpy (chunk->begin_writing(), ptr, len);
4129 chunk->add_size(len);
4130 length -= len;
4131 if (length == 0)
4132 break;
4133
4134 ptr += len;
4135
4136 /* Get another chunk for the remaining data */
4137 m_incoming_queue.push(vte::base::Chunk::get(chunk));
4138 chunk = m_incoming_queue.back().get();
4139 } while (true);
4140
4141 if (start_processing_)
4142 start_processing();
4143 }
4144
4145 bool
pty_io_write(int const fd,GIOCondition const condition)4146 Terminal::pty_io_write(int const fd,
4147 GIOCondition const condition)
4148 {
4149 auto const count = write(fd,
4150 m_outgoing->data,
4151 _vte_byte_array_length(m_outgoing));
4152 if (count != -1) {
4153 _VTE_DEBUG_IF (VTE_DEBUG_IO) {
4154 _vte_debug_hexdump("Outgoing buffer written",
4155 (uint8_t const*)m_outgoing->data,
4156 count);
4157 }
4158 _vte_byte_array_consume(m_outgoing, count);
4159 }
4160
4161 /* Run again if there are more bytes to write */
4162 return _vte_byte_array_length(m_outgoing) != 0;
4163 }
4164
4165 /* Send some UTF-8 data to the child. */
4166 void
send_child(std::string_view const & data)4167 Terminal::send_child(std::string_view const& data)
4168 {
4169 // FIXMEchpe remove
4170 if (!m_input_enabled)
4171 return;
4172
4173 /* Note that for backward compatibility, we need to emit the
4174 * ::commit signal even if there is no PTY. See issue vte#222.
4175 *
4176 * We use the primary data syntax to decide on the format.
4177 */
4178
4179 switch (primary_data_syntax()) {
4180 case DataSyntax::ECMA48_UTF8:
4181 emit_commit(data);
4182 if (pty())
4183 _vte_byte_array_append(m_outgoing, data.data(), data.size());
4184 break;
4185
4186 #ifdef WITH_ICU
4187 case DataSyntax::ECMA48_PCTERM: {
4188 auto converted = m_converter->convert(data);
4189
4190 emit_commit(converted);
4191 if (pty())
4192 _vte_byte_array_append(m_outgoing, converted.data(), converted.size());
4193 break;
4194 }
4195 #endif
4196
4197 default:
4198 g_assert_not_reached();
4199 return;
4200 }
4201
4202 /* If we need to start waiting for the child pty to
4203 * become available for writing, set that up here. */
4204 connect_pty_write();
4205 }
4206
4207 /*
4208 * VteTerminal::feed_child:
4209 * @str: data to send to the child
4210 *
4211 * Sends UTF-8 text to the child as if it were entered by the user
4212 * at the keyboard.
4213 *
4214 * Does nothing if input is disabled.
4215 */
4216 void
feed_child(std::string_view const & str)4217 Terminal::feed_child(std::string_view const& str)
4218 {
4219 if (!m_input_enabled)
4220 return;
4221
4222 send_child(str);
4223 }
4224
4225 /*
4226 * Terminal::feed_child_binary:
4227 * @data: data to send to the child
4228 *
4229 * Sends a block of binary data to the child.
4230 *
4231 * Does nothing if input is disabled.
4232 */
4233 void
feed_child_binary(std::string_view const & data)4234 Terminal::feed_child_binary(std::string_view const& data)
4235 {
4236 if (!m_input_enabled)
4237 return;
4238
4239 /* If there's a place for it to go, add the data to the
4240 * outgoing buffer. */
4241 if (!pty())
4242 return;
4243
4244 emit_commit(data);
4245 _vte_byte_array_append(m_outgoing, data.data(), data.size());
4246
4247 /* If we need to start waiting for the child pty to
4248 * become available for writing, set that up here. */
4249 connect_pty_write();
4250 }
4251
4252 void
send(vte::parser::u8SequenceBuilder const & builder,bool c1,vte::parser::u8SequenceBuilder::Introducer introducer,vte::parser::u8SequenceBuilder::ST st)4253 Terminal::send(vte::parser::u8SequenceBuilder const& builder,
4254 bool c1,
4255 vte::parser::u8SequenceBuilder::Introducer introducer,
4256 vte::parser::u8SequenceBuilder::ST st) noexcept
4257 {
4258 std::string str;
4259 builder.to_string(str, c1, -1, introducer, st);
4260 feed_child(str);
4261 }
4262
4263 void
send(vte::parser::Sequence const & seq,vte::parser::u8SequenceBuilder const & builder)4264 Terminal::send(vte::parser::Sequence const& seq,
4265 vte::parser::u8SequenceBuilder const& builder) noexcept
4266 {
4267 // FIXMEchpe always take c1 & ST from @seq?
4268 if (seq.type() == VTE_SEQ_OSC &&
4269 builder.type() == VTE_SEQ_OSC) {
4270 /* If we reply to a BEL-terminated OSC, reply with BEL-terminated OSC
4271 * as well, see https://bugzilla.gnome.org/show_bug.cgi?id=722446 and
4272 * https://gitlab.gnome.org/GNOME/vte/issues/65 .
4273 */
4274 send(builder, false,
4275 vte::parser::u8SequenceBuilder::Introducer::DEFAULT,
4276 seq.st() == 0x7 ? vte::parser::u8SequenceBuilder::ST::BEL
4277 : vte::parser::u8SequenceBuilder::ST::DEFAULT);
4278 } else {
4279 send(builder, false);
4280 }
4281 }
4282
4283 void
send(unsigned int type,std::initializer_list<int> params)4284 Terminal::send(unsigned int type,
4285 std::initializer_list<int> params) noexcept
4286 {
4287 // FIXMEchpe take c1 & ST from @seq
4288 send(vte::parser::ReplyBuilder{type, params}, false);
4289 }
4290
4291 void
reply(vte::parser::Sequence const & seq,unsigned int type,std::initializer_list<int> params)4292 Terminal::reply(vte::parser::Sequence const& seq,
4293 unsigned int type,
4294 std::initializer_list<int> params) noexcept
4295 {
4296 send(seq, vte::parser::ReplyBuilder{type, params});
4297 }
4298
4299 #if 0
4300 void
4301 Terminal::reply(vte::parser::Sequence const& seq,
4302 unsigned int type,
4303 std::initializer_list<int> params,
4304 std::string const& str) noexcept
4305 {
4306 vte::parser::ReplyBuilder reply_builder{type, params};
4307 reply_builder.set_string(str);
4308 send(seq, reply_builder);
4309 }
4310 #endif
4311
4312 void
reply(vte::parser::Sequence const & seq,unsigned int type,std::initializer_list<int> params,vte::parser::ReplyBuilder const & builder)4313 Terminal::reply(vte::parser::Sequence const& seq,
4314 unsigned int type,
4315 std::initializer_list<int> params,
4316 vte::parser::ReplyBuilder const& builder) noexcept
4317 {
4318 std::string str;
4319 builder.to_string(str, true, -1,
4320 vte::parser::ReplyBuilder::Introducer::NONE,
4321 vte::parser::ReplyBuilder::ST::NONE);
4322
4323 vte::parser::ReplyBuilder reply_builder{type, params};
4324 reply_builder.set_string(std::move(str));
4325 send(seq, reply_builder);
4326 }
4327
4328 void
reply(vte::parser::Sequence const & seq,unsigned int type,std::initializer_list<int> params,char const * format,...)4329 Terminal::reply(vte::parser::Sequence const& seq,
4330 unsigned int type,
4331 std::initializer_list<int> params,
4332 char const* format,
4333 ...) noexcept
4334 {
4335 char buf[128];
4336 va_list vargs;
4337 va_start(vargs, format);
4338 auto len = g_vsnprintf(buf, sizeof(buf), format, vargs);
4339 va_end(vargs);
4340 g_assert_cmpint(len, <, sizeof(buf));
4341
4342 vte::parser::ReplyBuilder builder{type, params};
4343 builder.set_string(std::string{buf});
4344
4345 send(seq, builder);
4346 }
4347
4348 void
im_commit(std::string_view const & str)4349 Terminal::im_commit(std::string_view const& str)
4350 {
4351 if (!m_input_enabled)
4352 return;
4353
4354 _vte_debug_print(VTE_DEBUG_EVENTS,
4355 "Input method committed `%s'.\n", std::string{str}.c_str());
4356 send_child(str);
4357
4358 /* Committed text was committed because the user pressed a key, so
4359 * we need to obey the scroll-on-keystroke setting. */
4360 if (m_scroll_on_keystroke && m_input_enabled) {
4361 maybe_scroll_to_bottom();
4362 }
4363 }
4364
4365 void
im_preedit_set_active(bool active)4366 Terminal::im_preedit_set_active(bool active) noexcept
4367 {
4368 m_im_preedit_active = active;
4369 }
4370
4371 void
im_preedit_reset()4372 Terminal::im_preedit_reset() noexcept
4373 {
4374 m_im_preedit.clear();
4375 m_im_preedit.shrink_to_fit();
4376 m_im_preedit_cursor = 0;
4377 m_im_preedit_attrs.reset();
4378 }
4379
4380 void
im_preedit_changed(std::string_view const & str,int cursorpos,vte::Freeable<PangoAttrList> attrs)4381 Terminal::im_preedit_changed(std::string_view const& str,
4382 int cursorpos,
4383 vte::Freeable<PangoAttrList> attrs) noexcept
4384 {
4385 /* Queue the area where the current preedit string is being displayed
4386 * for repainting. */
4387 invalidate_cursor_once();
4388
4389 im_preedit_reset();
4390 m_im_preedit = str;
4391 m_im_preedit_attrs = std::move(attrs);
4392 m_im_preedit_cursor = cursorpos;
4393
4394 /* Invalidate again with the new cursor position */
4395 invalidate_cursor_once();
4396
4397 /* And tell the input method where the cursor is on the screen */
4398 im_update_cursor();
4399 }
4400
4401 bool
im_retrieve_surrounding()4402 Terminal::im_retrieve_surrounding()
4403 {
4404 /* FIXME: implement this! Bug #726191 */
4405 return false;
4406 }
4407
4408 bool
im_delete_surrounding(int offset,int n_chars)4409 Terminal::im_delete_surrounding(int offset,
4410 int n_chars)
4411 {
4412 /* FIXME: implement this! Bug #726191 */
4413 return false;
4414 }
4415
4416 void
im_update_cursor()4417 Terminal::im_update_cursor()
4418 {
4419 if (!widget_realized())
4420 return;
4421
4422 cairo_rectangle_int_t rect;
4423 rect.x = m_screen->cursor.col * m_cell_width + m_padding.left +
4424 get_preedit_width(false) * m_cell_width;
4425 rect.width = m_cell_width; // FIXMEchpe: if columns > 1 ?
4426 rect.y = row_to_pixel(m_screen->cursor.row) + m_padding.top;
4427 rect.height = m_cell_height;
4428 m_real_widget->im_set_cursor_location(&rect);
4429 }
4430
4431 void
set_border_padding(GtkBorder const * padding)4432 Terminal::set_border_padding(GtkBorder const* padding)
4433 {
4434 if (memcmp(padding, &m_padding, sizeof(*padding)) != 0) {
4435 _vte_debug_print(VTE_DEBUG_MISC | VTE_DEBUG_WIDGET_SIZE,
4436 "Setting padding to (%d,%d,%d,%d)\n",
4437 padding->left, padding->right,
4438 padding->top, padding->bottom);
4439
4440 m_padding = *padding;
4441 update_view_extents();
4442 gtk_widget_queue_resize(m_widget);
4443 } else {
4444 _vte_debug_print(VTE_DEBUG_MISC | VTE_DEBUG_WIDGET_SIZE,
4445 "Keeping padding the same at (%d,%d,%d,%d)\n",
4446 padding->left, padding->right,
4447 padding->top, padding->bottom);
4448
4449 }
4450 }
4451
4452 void
set_cursor_aspect(float aspect)4453 Terminal::set_cursor_aspect(float aspect)
4454 {
4455 if (_vte_double_equal(aspect, m_cursor_aspect_ratio))
4456 return;
4457
4458 m_cursor_aspect_ratio = aspect;
4459 invalidate_cursor_once();
4460 }
4461
4462 void
widget_style_updated()4463 Terminal::widget_style_updated()
4464 {
4465 // FIXMEchpe: remove taking font info from the widget style
4466 update_font_desc();
4467 }
4468
4469 void
add_cursor_timeout()4470 Terminal::add_cursor_timeout()
4471 {
4472 if (m_cursor_blink_timer)
4473 return; /* already added */
4474
4475 m_cursor_blink_time = 0;
4476 m_cursor_blink_timer.schedule(m_cursor_blink_cycle, vte::glib::Timer::Priority::eLOW);
4477 }
4478
4479 void
remove_cursor_timeout()4480 Terminal::remove_cursor_timeout()
4481 {
4482 if (!m_cursor_blink_timer)
4483 return; /* already removed */
4484
4485 m_cursor_blink_timer.abort();
4486 if (!m_cursor_blink_state) {
4487 invalidate_cursor_once();
4488 m_cursor_blink_state = true;
4489 }
4490 }
4491
4492 /* Activates / disactivates the cursor blink timer to reduce wakeups */
4493 void
check_cursor_blink()4494 Terminal::check_cursor_blink()
4495 {
4496 if (m_has_focus &&
4497 m_cursor_blinks &&
4498 m_modes_private.DEC_TEXT_CURSOR())
4499 add_cursor_timeout();
4500 else
4501 remove_cursor_timeout();
4502 }
4503
4504 void
beep()4505 Terminal::beep()
4506 {
4507 if (m_audible_bell)
4508 m_real_widget->beep();
4509 }
4510
4511 bool
widget_key_press(vte::platform::KeyEvent const & event)4512 Terminal::widget_key_press(vte::platform::KeyEvent const& event)
4513 {
4514 char *normal = NULL;
4515 gsize normal_length = 0;
4516 struct termios tio;
4517 gboolean scrolled = FALSE, steal = FALSE, modifier = FALSE, handled,
4518 suppress_alt_esc = FALSE, add_modifiers = FALSE;
4519 guint keyval = 0;
4520 gunichar keychar = 0;
4521 char keybuf[VTE_UTF8_BPC];
4522
4523 /* If it's a keypress, record that we got the event, in case the
4524 * input method takes the event from us. */
4525 // FIXMEchpe this is ::widget_key_press; what other event type could it even be!?
4526 if (event.is_key_press()) {
4527 /* Store a copy of the key. */
4528 keyval = event.keyval();
4529 m_modifiers = event.modifiers();
4530
4531 // FIXMEchpe?
4532 if (m_cursor_blink_timer) {
4533 remove_cursor_timeout();
4534 add_cursor_timeout();
4535 }
4536
4537 /* Determine if this is just a modifier key. */
4538 modifier = _vte_keymap_key_is_modifier(keyval);
4539
4540 /* Unless it's a modifier key, hide the pointer. */
4541 if (!modifier) {
4542 set_pointer_autohidden(true);
4543 }
4544
4545 _vte_debug_print(VTE_DEBUG_EVENTS,
4546 "Keypress, modifiers=0x%x, "
4547 "keyval=0x%x\n",
4548 m_modifiers,
4549 keyval);
4550
4551 /* We steal many keypad keys here. */
4552 if (!m_im_preedit_active) {
4553 switch (keyval) {
4554 case GDK_KEY_KP_Add:
4555 case GDK_KEY_KP_Subtract:
4556 case GDK_KEY_KP_Multiply:
4557 case GDK_KEY_KP_Divide:
4558 case GDK_KEY_KP_Enter:
4559 steal = TRUE;
4560 break;
4561 default:
4562 break;
4563 }
4564 if (m_modifiers & VTE_ALT_MASK) {
4565 steal = TRUE;
4566 }
4567 switch (keyval) {
4568 case GDK_KEY_ISO_Lock:
4569 case GDK_KEY_ISO_Level2_Latch:
4570 case GDK_KEY_ISO_Level3_Shift:
4571 case GDK_KEY_ISO_Level3_Latch:
4572 case GDK_KEY_ISO_Level3_Lock:
4573 case GDK_KEY_ISO_Level5_Shift:
4574 case GDK_KEY_ISO_Level5_Latch:
4575 case GDK_KEY_ISO_Level5_Lock:
4576 case GDK_KEY_ISO_Group_Shift:
4577 case GDK_KEY_ISO_Group_Latch:
4578 case GDK_KEY_ISO_Group_Lock:
4579 case GDK_KEY_ISO_Next_Group:
4580 case GDK_KEY_ISO_Next_Group_Lock:
4581 case GDK_KEY_ISO_Prev_Group:
4582 case GDK_KEY_ISO_Prev_Group_Lock:
4583 case GDK_KEY_ISO_First_Group:
4584 case GDK_KEY_ISO_First_Group_Lock:
4585 case GDK_KEY_ISO_Last_Group:
4586 case GDK_KEY_ISO_Last_Group_Lock:
4587 case GDK_KEY_Multi_key:
4588 case GDK_KEY_Codeinput:
4589 case GDK_KEY_SingleCandidate:
4590 case GDK_KEY_MultipleCandidate:
4591 case GDK_KEY_PreviousCandidate:
4592 case GDK_KEY_Kanji:
4593 case GDK_KEY_Muhenkan:
4594 case GDK_KEY_Henkan_Mode:
4595 /* case GDK_KEY_Henkan: is GDK_KEY_Henkan_Mode */
4596 case GDK_KEY_Romaji:
4597 case GDK_KEY_Hiragana:
4598 case GDK_KEY_Katakana:
4599 case GDK_KEY_Hiragana_Katakana:
4600 case GDK_KEY_Zenkaku:
4601 case GDK_KEY_Hankaku:
4602 case GDK_KEY_Zenkaku_Hankaku:
4603 case GDK_KEY_Touroku:
4604 case GDK_KEY_Massyo:
4605 case GDK_KEY_Kana_Lock:
4606 case GDK_KEY_Kana_Shift:
4607 case GDK_KEY_Eisu_Shift:
4608 case GDK_KEY_Eisu_toggle:
4609 /* case GDK_KEY_Kanji_Bangou: is GDK_KEY_Codeinput */
4610 /* case GDK_KEY_Zen_Koho: is GDK_KEY_MultipleCandidate */
4611 /* case GDK_KEY_Mae_Koho: is GDK_KEY_PreviousCandidate */
4612 /* case GDK_KEY_kana_switch: is GDK_KEY_ISO_Group_Shift */
4613 case GDK_KEY_Hangul:
4614 case GDK_KEY_Hangul_Start:
4615 case GDK_KEY_Hangul_End:
4616 case GDK_KEY_Hangul_Hanja:
4617 case GDK_KEY_Hangul_Jamo:
4618 case GDK_KEY_Hangul_Romaja:
4619 /* case GDK_KEY_Hangul_Codeinput: is GDK_KEY_Codeinput */
4620 case GDK_KEY_Hangul_Jeonja:
4621 case GDK_KEY_Hangul_Banja:
4622 case GDK_KEY_Hangul_PreHanja:
4623 case GDK_KEY_Hangul_PostHanja:
4624 /* case GDK_KEY_Hangul_SingleCandidate: is GDK_KEY_SingleCandidate */
4625 /* case GDK_KEY_Hangul_MultipleCandidate: is GDK_KEY_MultipleCandidate */
4626 /* case GDK_KEY_Hangul_PreviousCandidate: is GDK_KEY_PreviousCandidate */
4627 case GDK_KEY_Hangul_Special:
4628 /* case GDK_KEY_Hangul_switch: is GDK_KEY_ISO_Group_Shift */
4629
4630 steal = FALSE;
4631 break;
4632 default:
4633 break;
4634 }
4635 }
4636 }
4637
4638 /* Let the input method at this one first. */
4639 if (!steal && m_input_enabled) {
4640 if (m_real_widget->im_filter_keypress(event)) {
4641 _vte_debug_print(VTE_DEBUG_EVENTS,
4642 "Keypress taken by IM.\n");
4643 return true;
4644 }
4645 }
4646
4647 /* Now figure out what to send to the child. */
4648 if (event.is_key_press() && !modifier) {
4649 handled = FALSE;
4650 /* Map the key to a sequence name if we can. */
4651 switch (event.keyval()) {
4652 case GDK_KEY_BackSpace:
4653 switch (m_backspace_binding) {
4654 case EraseMode::eASCII_BACKSPACE:
4655 normal = g_strdup("");
4656 normal_length = 1;
4657 suppress_alt_esc = FALSE;
4658 break;
4659 case EraseMode::eASCII_DELETE:
4660 normal = g_strdup("");
4661 normal_length = 1;
4662 suppress_alt_esc = FALSE;
4663 break;
4664 case EraseMode::eDELETE_SEQUENCE:
4665 normal = g_strdup("\e[3~");
4666 normal_length = 4;
4667 add_modifiers = TRUE;
4668 suppress_alt_esc = TRUE;
4669 break;
4670 case EraseMode::eTTY:
4671 if (pty() &&
4672 tcgetattr(pty()->fd(), &tio) != -1)
4673 {
4674 normal = g_strdup_printf("%c", tio.c_cc[VERASE]);
4675 normal_length = 1;
4676 }
4677 suppress_alt_esc = FALSE;
4678 break;
4679 case EraseMode::eAUTO:
4680 default:
4681 #ifndef _POSIX_VDISABLE
4682 #define _POSIX_VDISABLE '\0'
4683 #endif
4684 if (pty() &&
4685 tcgetattr(pty()->fd(), &tio) != -1 &&
4686 tio.c_cc[VERASE] != _POSIX_VDISABLE)
4687 {
4688 normal = g_strdup_printf("%c", tio.c_cc[VERASE]);
4689 normal_length = 1;
4690 }
4691 else
4692 {
4693 normal = g_strdup("");
4694 normal_length = 1;
4695 suppress_alt_esc = FALSE;
4696 }
4697 suppress_alt_esc = FALSE;
4698 break;
4699 }
4700 /* Toggle ^H vs ^? if Ctrl is pressed */
4701 if (normal_length == 1 && m_modifiers & GDK_CONTROL_MASK) {
4702 if (normal[0] == '\010')
4703 normal[0] = '\177';
4704 else if (normal[0] == '\177')
4705 normal[0] = '\010';
4706 }
4707 handled = TRUE;
4708 break;
4709 case GDK_KEY_KP_Delete:
4710 case GDK_KEY_Delete:
4711 switch (m_delete_binding) {
4712 case EraseMode::eASCII_BACKSPACE:
4713 normal = g_strdup("\010");
4714 normal_length = 1;
4715 break;
4716 case EraseMode::eASCII_DELETE:
4717 normal = g_strdup("\177");
4718 normal_length = 1;
4719 break;
4720 case EraseMode::eTTY:
4721 if (pty() &&
4722 tcgetattr(pty()->fd(), &tio) != -1)
4723 {
4724 normal = g_strdup_printf("%c", tio.c_cc[VERASE]);
4725 normal_length = 1;
4726 }
4727 suppress_alt_esc = FALSE;
4728 break;
4729 case EraseMode::eDELETE_SEQUENCE:
4730 case EraseMode::eAUTO:
4731 default:
4732 normal = g_strdup("\e[3~");
4733 normal_length = 4;
4734 add_modifiers = TRUE;
4735 break;
4736 }
4737 handled = TRUE;
4738 /* FIXMEchpe: why? this overrides the FALSE set above? */
4739 suppress_alt_esc = TRUE;
4740 break;
4741 case GDK_KEY_KP_Insert:
4742 case GDK_KEY_Insert:
4743 if (m_modifiers & GDK_SHIFT_MASK) {
4744 if (m_modifiers & GDK_CONTROL_MASK) {
4745 emit_paste_clipboard();
4746 handled = TRUE;
4747 suppress_alt_esc = TRUE;
4748 } else {
4749 widget()->clipboard_request_text(vte::platform::ClipboardType::PRIMARY);
4750 handled = TRUE;
4751 suppress_alt_esc = TRUE;
4752 }
4753 } else if (m_modifiers & GDK_CONTROL_MASK) {
4754 emit_copy_clipboard();
4755 handled = TRUE;
4756 suppress_alt_esc = TRUE;
4757 }
4758 break;
4759 /* Keypad/motion keys. */
4760 case GDK_KEY_KP_Up:
4761 case GDK_KEY_Up:
4762 if (m_screen == &m_normal_screen &&
4763 m_modifiers & GDK_CONTROL_MASK &&
4764 m_modifiers & GDK_SHIFT_MASK) {
4765 scroll_lines(-1);
4766 scrolled = TRUE;
4767 handled = TRUE;
4768 suppress_alt_esc = TRUE;
4769 }
4770 break;
4771 case GDK_KEY_KP_Down:
4772 case GDK_KEY_Down:
4773 if (m_screen == &m_normal_screen &&
4774 m_modifiers & GDK_CONTROL_MASK &&
4775 m_modifiers & GDK_SHIFT_MASK) {
4776 scroll_lines(1);
4777 scrolled = TRUE;
4778 handled = TRUE;
4779 suppress_alt_esc = TRUE;
4780 }
4781 break;
4782 case GDK_KEY_KP_Page_Up:
4783 case GDK_KEY_Page_Up:
4784 if (m_screen == &m_normal_screen &&
4785 m_modifiers & GDK_SHIFT_MASK) {
4786 scroll_pages(-1);
4787 scrolled = TRUE;
4788 handled = TRUE;
4789 suppress_alt_esc = TRUE;
4790 }
4791 break;
4792 case GDK_KEY_KP_Page_Down:
4793 case GDK_KEY_Page_Down:
4794 if (m_screen == &m_normal_screen &&
4795 m_modifiers & GDK_SHIFT_MASK) {
4796 scroll_pages(1);
4797 scrolled = TRUE;
4798 handled = TRUE;
4799 suppress_alt_esc = TRUE;
4800 }
4801 break;
4802 case GDK_KEY_KP_Home:
4803 case GDK_KEY_Home:
4804 if (m_screen == &m_normal_screen &&
4805 m_modifiers & GDK_SHIFT_MASK) {
4806 maybe_scroll_to_top();
4807 scrolled = TRUE;
4808 handled = TRUE;
4809 }
4810 break;
4811 case GDK_KEY_KP_End:
4812 case GDK_KEY_End:
4813 if (m_screen == &m_normal_screen &&
4814 m_modifiers & GDK_SHIFT_MASK) {
4815 maybe_scroll_to_bottom();
4816 scrolled = TRUE;
4817 handled = TRUE;
4818 }
4819 break;
4820 /* Let Shift +/- tweak the font, like XTerm does. */
4821 case GDK_KEY_KP_Add:
4822 case GDK_KEY_KP_Subtract:
4823 if (m_modifiers & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)) {
4824 switch (keyval) {
4825 case GDK_KEY_KP_Add:
4826 emit_increase_font_size();
4827 handled = TRUE;
4828 suppress_alt_esc = TRUE;
4829 break;
4830 case GDK_KEY_KP_Subtract:
4831 emit_decrease_font_size();
4832 handled = TRUE;
4833 suppress_alt_esc = TRUE;
4834 break;
4835 }
4836 }
4837 break;
4838 default:
4839 break;
4840 }
4841 /* If the above switch statement didn't do the job, try mapping
4842 * it to a literal or capability name. */
4843 if (handled == FALSE) {
4844 if (G_UNLIKELY (m_enable_bidi &&
4845 m_modes_private.VTE_BIDI_SWAP_ARROW_KEYS() &&
4846 (keyval == GDK_KEY_Left ||
4847 keyval == GDK_KEY_Right ||
4848 keyval == GDK_KEY_KP_Left ||
4849 keyval == GDK_KEY_KP_Right))) {
4850 /* In keyboard arrow swapping mode, the left and right arrows need swapping
4851 * if the cursor stands inside a (possibly autodetected) RTL paragraph. */
4852 ensure_row();
4853 VteRowData const *row_data = find_row_data(m_screen->cursor.row);
4854 bool rtl;
4855 if ((row_data->attr.bidi_flags & (VTE_BIDI_FLAG_IMPLICIT | VTE_BIDI_FLAG_AUTO))
4856 == (VTE_BIDI_FLAG_IMPLICIT | VTE_BIDI_FLAG_AUTO)) {
4857 /* Implicit paragraph with autodetection. Need to run the BiDi algorithm
4858 * to get the autodetected direction.
4859 * m_ringview is for the onscreen contents and the cursor may be offscreen.
4860 * Better leave that alone and use a temporary ringview for the cursor's row. */
4861 vte::base::RingView ringview;
4862 ringview.set_ring(m_screen->row_data);
4863 ringview.set_rows(m_screen->cursor.row, 1);
4864 ringview.set_width(m_column_count);
4865 ringview.update();
4866 rtl = ringview.get_bidirow(m_screen->cursor.row)->base_is_rtl();
4867 } else {
4868 /* Not an implicit paragraph with autodetection, no autodetection
4869 * is required. Take the direction straight from the stored data. */
4870 rtl = !!(row_data->attr.bidi_flags & VTE_BIDI_FLAG_RTL);
4871 }
4872 if (rtl) {
4873 switch (keyval) {
4874 case GDK_KEY_Left:
4875 keyval = GDK_KEY_Right;
4876 break;
4877 case GDK_KEY_Right:
4878 keyval = GDK_KEY_Left;
4879 break;
4880 case GDK_KEY_KP_Left:
4881 keyval = GDK_KEY_KP_Right;
4882 break;
4883 case GDK_KEY_KP_Right:
4884 keyval = GDK_KEY_KP_Left;
4885 break;
4886 }
4887 }
4888 }
4889
4890 _vte_keymap_map(keyval, m_modifiers,
4891 m_modes_private.DEC_APPLICATION_CURSOR_KEYS(),
4892 m_modes_private.DEC_APPLICATION_KEYPAD(),
4893 &normal,
4894 &normal_length);
4895 /* If we found something this way, suppress
4896 * escape-on-alt. */
4897 if (normal != NULL && normal_length > 0) {
4898 suppress_alt_esc = TRUE;
4899 }
4900 }
4901
4902 /* Shall we do this here or earlier? See bug 375112 and bug 589557 */
4903 if (m_modifiers & GDK_CONTROL_MASK && widget())
4904 keyval = widget()->key_event_translate_ctrlkey(event);
4905
4906 /* If we didn't manage to do anything, try to salvage a
4907 * printable string. */
4908 if (handled == FALSE && normal == NULL) {
4909
4910 /* Convert the keyval to a gunichar. */
4911 keychar = gdk_keyval_to_unicode(keyval);
4912 normal_length = 0;
4913 if (keychar != 0) {
4914 /* Convert the gunichar to a string. */
4915 normal_length = g_unichar_to_utf8(keychar,
4916 keybuf);
4917 if (normal_length != 0) {
4918 normal = (char *)g_malloc(normal_length + 1);
4919 memcpy(normal, keybuf, normal_length);
4920 normal[normal_length] = '\0';
4921 } else {
4922 normal = NULL;
4923 }
4924 }
4925 if ((normal != NULL) &&
4926 (m_modifiers & GDK_CONTROL_MASK)) {
4927 /* Replace characters which have "control"
4928 * counterparts with those counterparts. */
4929 for (size_t i = 0; i < normal_length; i++) {
4930 if ((((guint8)normal[i]) >= 0x40) &&
4931 (((guint8)normal[i]) < 0x80)) {
4932 normal[i] &= (~(0x60));
4933 }
4934 }
4935 }
4936 _VTE_DEBUG_IF (VTE_DEBUG_EVENTS) {
4937 if (normal) g_printerr(
4938 "Keypress, modifiers=0x%x, "
4939 "keyval=0x%x, cooked string=`%s'.\n",
4940 m_modifiers,
4941 keyval, normal);
4942 }
4943 }
4944 /* If we got normal characters, send them to the child. */
4945 if (normal != NULL) {
4946 if (add_modifiers) {
4947 _vte_keymap_key_add_key_modifiers(keyval,
4948 m_modifiers,
4949 m_modes_private.DEC_APPLICATION_CURSOR_KEYS(),
4950 &normal,
4951 &normal_length);
4952 }
4953 if (m_modes_private.XTERM_META_SENDS_ESCAPE() &&
4954 !suppress_alt_esc &&
4955 (normal_length > 0) &&
4956 (m_modifiers & VTE_ALT_MASK)) {
4957 feed_child(_VTE_CAP_ESC, 1);
4958 }
4959 if (normal_length > 0) {
4960 send_child({normal, normal_length});
4961 }
4962 g_free(normal);
4963 }
4964 /* Keep the cursor on-screen. */
4965 if (!scrolled && !modifier &&
4966 m_scroll_on_keystroke && m_input_enabled) {
4967 maybe_scroll_to_bottom();
4968 }
4969 return true;
4970 }
4971 return false;
4972 }
4973
4974 bool
widget_key_release(vte::platform::KeyEvent const & event)4975 Terminal::widget_key_release(vte::platform::KeyEvent const& event)
4976 {
4977 m_modifiers = event.modifiers();
4978
4979 if (m_input_enabled &&
4980 m_real_widget->im_filter_keypress(event))
4981 return true;
4982
4983 return false;
4984 }
4985
4986 static const guint8 word_char_by_category[] = {
4987 [G_UNICODE_CONTROL] = 2,
4988 [G_UNICODE_FORMAT] = 2,
4989 [G_UNICODE_UNASSIGNED] = 2,
4990 [G_UNICODE_PRIVATE_USE] = 0,
4991 [G_UNICODE_SURROGATE] = 2,
4992 [G_UNICODE_LOWERCASE_LETTER] = 1,
4993 [G_UNICODE_MODIFIER_LETTER] = 1,
4994 [G_UNICODE_OTHER_LETTER] = 1,
4995 [G_UNICODE_TITLECASE_LETTER] = 1,
4996 [G_UNICODE_UPPERCASE_LETTER] = 1,
4997 [G_UNICODE_SPACING_MARK] = 0,
4998 [G_UNICODE_ENCLOSING_MARK] = 0,
4999 [G_UNICODE_NON_SPACING_MARK] = 0,
5000 [G_UNICODE_DECIMAL_NUMBER] = 1,
5001 [G_UNICODE_LETTER_NUMBER] = 1,
5002 [G_UNICODE_OTHER_NUMBER] = 1,
5003 [G_UNICODE_CONNECT_PUNCTUATION] = 0,
5004 [G_UNICODE_DASH_PUNCTUATION] = 0,
5005 [G_UNICODE_CLOSE_PUNCTUATION] = 0,
5006 [G_UNICODE_FINAL_PUNCTUATION] = 0,
5007 [G_UNICODE_INITIAL_PUNCTUATION] = 0,
5008 [G_UNICODE_OTHER_PUNCTUATION] = 0,
5009 [G_UNICODE_OPEN_PUNCTUATION] = 0,
5010 [G_UNICODE_CURRENCY_SYMBOL] = 0,
5011 [G_UNICODE_MODIFIER_SYMBOL] = 0,
5012 [G_UNICODE_MATH_SYMBOL] = 0,
5013 [G_UNICODE_OTHER_SYMBOL] = 0,
5014 [G_UNICODE_LINE_SEPARATOR] = 2,
5015 [G_UNICODE_PARAGRAPH_SEPARATOR] = 2,
5016 [G_UNICODE_SPACE_SEPARATOR] = 2,
5017 };
5018
5019 /*
5020 * Terminal::is_word_char:
5021 * @c: a candidate Unicode code point
5022 *
5023 * Checks if a particular character is considered to be part of a word or not.
5024 *
5025 * Returns: %TRUE if the character is considered to be part of a word
5026 */
5027 bool
is_word_char(gunichar c) const5028 Terminal::is_word_char(gunichar c) const
5029 {
5030 const guint8 v = word_char_by_category[g_unichar_type(c)];
5031
5032 if (v)
5033 return v == 1;
5034
5035 /* Do we have an exception? */
5036 return std::find(std::begin(m_word_char_exceptions), std::end(m_word_char_exceptions), char32_t(c)) != std::end(m_word_char_exceptions);
5037 }
5038
5039 /* Check if the characters in the two given locations are in the same class
5040 * (word vs. non-word characters).
5041 * Note that calling this method may invalidate the return value of
5042 * a previous find_row_data() call. */
5043 bool
is_same_class(vte::grid::column_t acol,vte::grid::row_t arow,vte::grid::column_t bcol,vte::grid::row_t brow) const5044 Terminal::is_same_class(vte::grid::column_t acol,
5045 vte::grid::row_t arow,
5046 vte::grid::column_t bcol,
5047 vte::grid::row_t brow) const
5048 {
5049 VteCell const* pcell = nullptr;
5050 bool word_char;
5051 if ((pcell = find_charcell(acol, arow)) != nullptr && pcell->c != 0) {
5052 /* Group together if they're fragments of the very same character (not just character value) */
5053 if (arow == brow) {
5054 auto a2 = acol, b2 = bcol;
5055 while (a2 > 0 && find_charcell(a2, arow)->attr.fragment()) a2--;
5056 while (b2 > 0 && find_charcell(b2, brow)->attr.fragment()) b2--;
5057 if (a2 == b2)
5058 return true;
5059 }
5060
5061 word_char = is_word_char(_vte_unistr_get_base(pcell->c));
5062
5063 /* Lets not group non-wordchars together (bug #25290) */
5064 if (!word_char)
5065 return false;
5066
5067 pcell = find_charcell(bcol, brow);
5068 if (pcell == NULL || pcell->c == 0) {
5069 return false;
5070 }
5071 if (word_char != is_word_char(_vte_unistr_get_base(pcell->c))) {
5072 return false;
5073 }
5074 return true;
5075 }
5076 return false;
5077 }
5078
5079 /*
5080 * Convert the mouse click or drag location (left or right half of a cell) into a selection endpoint
5081 * (a boundary between characters), extending the selection according to the current mode, in the
5082 * direction given in the @after parameter.
5083 *
5084 * All four selection modes require different strategies.
5085 *
5086 * In char mode, what matters is which vertical character boundary is closer, taking multi-cell characters
5087 * (CJKs, TABs) into account. Given the string "abcdef", if the user clicks on the boundary between "a"
5088 * and "b" (perhaps on the right half of "a", perhaps on the left half of "b"), and moves the mouse to the
5089 * boundary between "e" and "f" (perhaps a bit over "e", perhaps a bit over "f"), the selection should be
5090 * "bcde". By dragging the mouse back to approximately the click location, it is possible to select the
5091 * empty string. This is the common sense behavior impemented by basically every graphical toolkit
5092 * (unfortunately not by many terminal emulators), and also the one we go for.
5093 *
5094 * Word mode is the trickiest one. Many implementations have weird corner case bugs (e.g. don't highlight
5095 * a word if you double click on the second half of its last letter, or even highlight it if you click on
5096 * the first half of the following space). I think it is expected that double-clicking anywhere over a
5097 * word (including the first half of its first letter, or the last half of its last letter), but over no
5098 * other character, selects this entire word. By dragging the mouse it's not possible to select nothing,
5099 * the word (or non-word character) initially clicked on is always part of the selection. (An exception
5100 * is when clicking occurred over the margin, or an unused cell in a soft-wrapped row (due to CJK
5101 * wrapping).) Also, for symmetry reasons, the word (or non-word character) under the current mouse
5102 * location is also always selected.
5103 *
5104 * Line (paragraph) mode is conceptually quite similar to word mode (the cell, thus the entire row under
5105 * the click's location is always included), but is much easier to implement.
5106 *
5107 * In block mode, similarly to char mode, we care about vertical character boundary. (This is somewhat
5108 * debatable, as results in asymmetrical behavior along the two axes: a rectangle can disappear by
5109 * becoming zero wide, but not zero high.) We cannot take care of CJKs at the endpoints now because CJKs
5110 * can cross the boundary in any included row. Taking care of them needs to go to cell_is_selected_*().
5111 * We don't care about used vs. unused cells either. The event coordinate is simply rounded to the
5112 * nearest vertical cell boundary.
5113 */
5114 vte::grid::coords
resolve_selection_endpoint(vte::grid::halfcoords const & rowcolhalf,bool after) const5115 Terminal::resolve_selection_endpoint(vte::grid::halfcoords const& rowcolhalf, bool after) const
5116 {
5117 auto row = rowcolhalf.row();
5118 auto col = rowcolhalf.halfcolumn().column(); /* Points to an actual cell now. At the end of this
5119 method it'll point to a boundary. */
5120 auto half = rowcolhalf.halfcolumn().half(); /* 0 for left half, 1 for right half of the cell. */
5121 VteRowData const* rowdata;
5122 VteCell const* cell;
5123 int len;
5124
5125 if (m_selection_block_mode) {
5126 /* Just find the nearest cell boundary within the line, not caring about CJKs, unused
5127 * cells, or wrapping at EOL. The @after parameter is unused in this mode. */
5128 col += half;
5129 col = std::clamp (col, 0L, m_column_count);
5130 } else {
5131 switch (m_selection_type) {
5132 case SelectionType::eCHAR:
5133 /* Find the nearest actual character boundary, taking CJKs and TABs into account.
5134 * If at least halfway through the first unused cell, or over the right margin
5135 * then wrap to the beginning of the next line.
5136 * The @after parameter is unused in this mode. */
5137 if (col < 0) {
5138 col = 0;
5139 } else if (col >= m_column_count) {
5140 /* If on the right padding, select the entire line including a possible
5141 * newline character. This way if a line is fully filled and ends in a
5142 * newline, there's only a half cell width for which the line is selected
5143 * without the newline, but at least there's a way to include the newline
5144 * by moving the mouse to the right (bug 724253). */
5145 col = 0;
5146 row++;
5147 } else {
5148 vte::grid::column_t char_begin, char_end; /* cell boundaries */
5149 rowdata = find_row_data(row);
5150 if (rowdata && col < _vte_row_data_nonempty_length(rowdata)) {
5151 /* Clicked over a used cell. Check for multi-cell characters. */
5152 char_begin = col;
5153 while (char_begin > 0) {
5154 cell = _vte_row_data_get (rowdata, char_begin);
5155 if (!cell->attr.fragment())
5156 break;
5157 char_begin--;
5158 }
5159 cell = _vte_row_data_get (rowdata, char_begin);
5160 char_end = char_begin + cell->attr.columns();
5161 } else {
5162 /* Clicked over unused area. Just go with cell boundaries. */
5163 char_begin = col;
5164 char_end = char_begin + 1;
5165 }
5166 /* Which boundary is closer? */
5167 if (col * 2 + half < char_begin + char_end)
5168 col = char_begin;
5169 else
5170 col = char_end;
5171
5172 /* Maybe wrap to the beginning of the next line. */
5173 if (col > (rowdata ? _vte_row_data_nonempty_length(rowdata) : 0)) {
5174 col = 0;
5175 row++;
5176 }
5177 }
5178 break;
5179
5180 case SelectionType::eWORD:
5181 /* Initialization for the cumbersome cases where the click didn't occur over an actual used cell. */
5182 rowdata = find_row_data(row);
5183 if (col < 0) {
5184 /* Clicked over the left margin.
5185 * - If within a word (that is, the first letter in this row, and the last
5186 * letter of the previous row belong to the same word) then select the
5187 * letter according to the direction and continue expanding.
5188 * - Otherwise stop, the boundary is here before the first letter. */
5189 if (row > 0 &&
5190 (rowdata = find_row_data(row - 1)) != nullptr &&
5191 rowdata->attr.soft_wrapped &&
5192 (len = _vte_row_data_nonempty_length(rowdata)) > 0 &&
5193 is_same_class(len - 1, row - 1, 0, row) /* invalidates rowdata! */) {
5194 if (!after) {
5195 col = len - 1;
5196 row--;
5197 } else {
5198 col = 0;
5199 }
5200 /* go on with expanding */
5201 } else {
5202 col = 0; /* end-exclusive */
5203 break; /* done, don't expand any more */
5204 }
5205 } else if (col >= (rowdata ? _vte_row_data_nonempty_length(rowdata) : 0)) {
5206 /* Clicked over the right margin, or right unused area.
5207 * - If within a word (that is, the last letter in this row, and the first
5208 * letter of the next row belong to the same word) then select the letter
5209 * according to the direction and continue expanding.
5210 * - Otherwise, if the row is soft-wrapped and we're over the unused area
5211 * (which can happen if a CJK wrapped) or over the right margin, then
5212 * stop, the boundary is wrapped to the beginning of the next row.
5213 * - Otherwise select the newline only and stop. */
5214 if (rowdata != nullptr &&
5215 rowdata->attr.soft_wrapped) {
5216 if ((len = _vte_row_data_nonempty_length(rowdata)) > 0 &&
5217 is_same_class(len - 1, row, 0, row + 1) /* invalidates rowdata! */) {
5218 if (!after) {
5219 col = len - 1;
5220 } else {
5221 col = 0;
5222 row++;
5223 }
5224 /* go on with expanding */
5225 } else {
5226 col = 0; /* end-exclusive */
5227 row++;
5228 break; /* done, don't expand any more */
5229 }
5230 } else {
5231 if (!after) {
5232 col = rowdata ? _vte_row_data_nonempty_length(rowdata) : 0; /* end-exclusive */
5233 } else {
5234 col = 0; /* end-exclusive */
5235 row++;
5236 }
5237 break; /* done, don't expand any more */
5238 }
5239 }
5240
5241 /* Expand in the given direction. */
5242 if (!after) {
5243 /* Keep selecting to the left (and then up) as long as the next character
5244 * we look at is of the same class as the current start point. */
5245 while (true) {
5246 /* Back up within the row. */
5247 for (; col > 0; col--) {
5248 if (!is_same_class(col - 1, row, col, row)) {
5249 break;
5250 }
5251 }
5252 if (col > 0) {
5253 /* We hit a stopping point, so stop. */
5254 break;
5255 }
5256 if (row == 0) {
5257 /* At the very beginning. */
5258 break;
5259 }
5260 rowdata = find_row_data(row - 1);
5261 if (!rowdata || !rowdata->attr.soft_wrapped) {
5262 /* Reached a hard newline. */
5263 break;
5264 }
5265 len = _vte_row_data_nonempty_length(rowdata);
5266 /* len might be smaller than m_column_count if a CJK wrapped */
5267 if (!is_same_class(len - 1, row - 1, col, row) /* invalidates rowdata! */) {
5268 break;
5269 }
5270 /* Move on to the previous line. */
5271 col = len - 1;
5272 row--;
5273 }
5274 } else {
5275 /* Keep selecting to the right (and then down) as long as the next character
5276 * we look at is of the same class as the current end point. */
5277 while (true) {
5278 rowdata = find_row_data(row);
5279 if (!rowdata) {
5280 break;
5281 }
5282 len = _vte_row_data_nonempty_length(rowdata);
5283 bool soft_wrapped = rowdata->attr.soft_wrapped;
5284 /* Move forward within the row. */
5285 for (; col < len - 1; col++) {
5286 if (!is_same_class(col, row, col + 1, row) /* invalidates rowdata! */) {
5287 break;
5288 }
5289 }
5290 if (col < len - 1) {
5291 /* We hit a stopping point, so stop. */
5292 break;
5293 }
5294 if (!soft_wrapped) {
5295 /* Reached a hard newline. */
5296 break;
5297 }
5298 if (!is_same_class(col, row, 0, row + 1)) {
5299 break;
5300 }
5301 /* Move on to the next line. */
5302 col = 0;
5303 row++;
5304 }
5305 col++; /* col points to an actual cell, we need end-exclusive instead. */
5306 }
5307 break;
5308
5309 case SelectionType::eLINE:
5310 if (!after) {
5311 /* Back up as far as we can go. */
5312 while (row > 0 &&
5313 _vte_ring_contains (m_screen->row_data, row - 1) &&
5314 m_screen->row_data->is_soft_wrapped(row - 1)) {
5315 row--;
5316 }
5317 } else {
5318 /* Move forward as far as we can go. */
5319 while (_vte_ring_contains (m_screen->row_data, row) &&
5320 m_screen->row_data->is_soft_wrapped(row)) {
5321 row++;
5322 }
5323 row++; /* One more row, since the column is 0. */
5324 }
5325 col = 0;
5326 break;
5327 }
5328 }
5329
5330 return { row, col };
5331 }
5332
5333 /*
5334 * Creates the selection's span from the origin and last coordinates.
5335 *
5336 * The origin and last points might be in reverse order; in block mode they might even point to the
5337 * two other corners of the rectangle than the ones we're interested in.
5338 * The resolved span will contain the endpoints in the proper order.
5339 *
5340 * In word & line (paragraph) modes it extends the selection accordingly.
5341 *
5342 * Also makes sure to invalidate the regions that changed, and update m_selecting_had_delta.
5343 *
5344 * FIXMEegmont it always resolves both endpoints. With a bit of extra bookkeeping it could usually
5345 * just resolve the moving one.
5346 */
5347 void
resolve_selection()5348 Terminal::resolve_selection()
5349 {
5350 if (m_selection_origin.row() < 0 || m_selection_last.row() < 0) {
5351 invalidate (m_selection_resolved);
5352 m_selection_resolved.clear();
5353 _vte_debug_print(VTE_DEBUG_SELECTION, "Selection resolved to %s.\n", m_selection_resolved.to_string());
5354 return;
5355 }
5356
5357 auto m_selection_resolved_old = m_selection_resolved;
5358
5359 if (m_selection_block_mode) {
5360 auto top = std::min (m_selection_origin.row(), m_selection_last.row());
5361 auto bottom = std::max (m_selection_origin.row(), m_selection_last.row());
5362 auto left = std::min (m_selection_origin.halfcolumn(), m_selection_last.halfcolumn());
5363 auto right = std::max (m_selection_origin.halfcolumn(), m_selection_last.halfcolumn());
5364
5365 auto topleft = resolve_selection_endpoint ({ top, left }, false);
5366 auto bottomright = resolve_selection_endpoint ({ bottom, right }, true);
5367
5368 if (topleft.column() == bottomright.column()) {
5369 m_selection_resolved.clear();
5370 } else {
5371 m_selection_resolved.set (topleft, bottomright);
5372 }
5373 } else {
5374 auto start = std::min (m_selection_origin, m_selection_last);
5375 auto end = std::max (m_selection_origin, m_selection_last);
5376
5377 m_selection_resolved.set (resolve_selection_endpoint (start, false),
5378 resolve_selection_endpoint (end, true));
5379 }
5380
5381 if (!m_selection_resolved.empty())
5382 m_selecting_had_delta = true;
5383
5384 _vte_debug_print(VTE_DEBUG_SELECTION, "Selection resolved to %s.\n", m_selection_resolved.to_string());
5385
5386 invalidate_symmetrical_difference (m_selection_resolved_old, m_selection_resolved, m_selection_block_mode);
5387 }
5388
5389 void
modify_selection(vte::view::coords const & pos)5390 Terminal::modify_selection (vte::view::coords const& pos)
5391 {
5392 g_assert (m_selecting);
5393
5394 /* Need to ensure the ringview is updated. */
5395 ringview_update();
5396
5397 auto current = selection_grid_halfcoords_from_view_coords (pos);
5398
5399 if (current == m_selection_last)
5400 return;
5401
5402 _vte_debug_print(VTE_DEBUG_SELECTION,
5403 "Selection dragged to %s.\n",
5404 current.to_string());
5405
5406 m_selection_last = current;
5407 resolve_selection();
5408 }
5409
5410 /* Check if a cell is selected or not. BiDi: the coordinate is logical. */
5411 bool
cell_is_selected_log(vte::grid::column_t lcol,vte::grid::row_t row) const5412 Terminal::cell_is_selected_log(vte::grid::column_t lcol,
5413 vte::grid::row_t row) const
5414 {
5415 /* Our caller had to update the ringview (we can't do because we're const). */
5416 g_assert(m_ringview.is_updated());
5417
5418 if (m_selection_block_mode) {
5419 /* In block mode, make sure CJKs and TABs aren't cut in half. */
5420 while (lcol > 0) {
5421 VteCell const* cell = find_charcell(lcol, row);
5422 if (!cell || !cell->attr.fragment())
5423 break;
5424 lcol--;
5425 }
5426 /* Convert to visual. */
5427 vte::base::BidiRow const* bidirow = m_ringview.get_bidirow(row);
5428 vte::grid::column_t vcol = bidirow->log2vis(lcol);
5429 return m_selection_resolved.box_contains ({ row, vcol });
5430 } else {
5431 /* In normal modes, resolve_selection() made sure to generate such boundaries for m_selection_resolved. */
5432 return m_selection_resolved.contains ({ row, lcol });
5433 }
5434 }
5435
5436 /* Check if a cell is selected or not. BiDi: the coordinate is visual. */
5437 bool
cell_is_selected_vis(vte::grid::column_t vcol,vte::grid::row_t row) const5438 Terminal::cell_is_selected_vis(vte::grid::column_t vcol,
5439 vte::grid::row_t row) const
5440 {
5441 /* Our caller had to update the ringview (we can't do because we're const). */
5442 g_assert(m_ringview.is_updated());
5443
5444 /* Convert to logical column. */
5445 vte::base::BidiRow const* bidirow = m_ringview.get_bidirow(row);
5446 vte::grid::column_t lcol = bidirow->vis2log(vcol);
5447
5448 return cell_is_selected_log(lcol, row);
5449 }
5450
5451 void
widget_clipboard_text_received(vte::platform::Clipboard const & clipboard,std::string_view const & data)5452 Terminal::widget_clipboard_text_received(vte::platform::Clipboard const& clipboard,
5453 std::string_view const& data)
5454 {
5455 gchar *paste, *p;
5456 gsize run;
5457 unsigned char c;
5458
5459 auto const len = data.size();
5460 auto text = data.data();
5461
5462 /* Convert newlines to carriage returns, which more software
5463 * is able to cope with (cough, pico, cough).
5464 * Filter out control chars except HT, CR (even stricter than xterm).
5465 * Also filter out C1 controls: U+0080 (0xC2 0x80) - U+009F (0xC2 0x9F). */
5466 p = paste = (gchar *) g_malloc(len + 1);
5467 while (p != nullptr && text[0] != '\0') {
5468 run = strcspn(text, "\x01\x02\x03\x04\x05\x06\x07"
5469 "\x08\x0A\x0B\x0C\x0E\x0F"
5470 "\x10\x11\x12\x13\x14\x15\x16\x17"
5471 "\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"
5472 "\x7F\xC2");
5473 memcpy(p, text, run);
5474 p += run;
5475 text += run;
5476 switch (text[0]) {
5477 case '\x00':
5478 break;
5479 case '\x0A':
5480 *p = '\x0D';
5481 p++;
5482 text++;
5483 break;
5484 case '\xC2':
5485 c = text[1];
5486 if (c >= 0x80 && c <= 0x9F) {
5487 /* Skip both bytes of a C1 */
5488 text += 2;
5489 } else {
5490 /* Move along, nothing to see here */
5491 *p = '\xC2';
5492 p++;
5493 text++;
5494 }
5495 break;
5496 default:
5497 /* Swallow this byte */
5498 text++;
5499 break;
5500 }
5501 }
5502
5503 bool const bracketed_paste = m_modes_private.XTERM_READLINE_BRACKETED_PASTE();
5504 // FIXMEchpe can we not hardcode C0 controls here?
5505 if (bracketed_paste)
5506 feed_child("\e[200~"sv);
5507 // FIXMEchpe add a way to avoid the extra string copy done here
5508 feed_child(paste, p - paste);
5509 if (bracketed_paste)
5510 feed_child("\e[201~"sv);
5511 g_free(paste);
5512 }
5513
5514 bool
feed_mouse_event(vte::grid::coords const & rowcol,int button,bool is_drag,bool is_release)5515 Terminal::feed_mouse_event(vte::grid::coords const& rowcol /* confined */,
5516 int button,
5517 bool is_drag,
5518 bool is_release)
5519 {
5520 unsigned char cb = 0;
5521
5522 /* Don't send events on scrollback contents: bug 755187. */
5523 if (grid_coords_in_scrollback(rowcol))
5524 return false;
5525
5526 /* Make coordinates 1-based. */
5527 auto cx = rowcol.column() + 1;
5528 auto cy = rowcol.row() - m_screen->insert_delta + 1;
5529
5530 /* Encode the button information in cb. */
5531 switch (button) {
5532 case 0: /* No button, just dragging. */
5533 cb = 3;
5534 break;
5535 case 1: /* Left. */
5536 cb = 0;
5537 break;
5538 case 2: /* Middle. */
5539 cb = 1;
5540 break;
5541 case 3: /* Right. */
5542 cb = 2;
5543 break;
5544 case 4:
5545 cb = 64; /* Scroll up. */
5546 break;
5547 case 5:
5548 cb = 65; /* Scroll down. */
5549 break;
5550 }
5551
5552 /* With the exception of the 1006 mode, button release is also encoded here. */
5553 /* Note that if multiple extensions are enabled, the 1006 is used, so it's okay to check for only that. */
5554 if (is_release && !m_modes_private.XTERM_MOUSE_EXT_SGR()) {
5555 cb = 3;
5556 }
5557
5558 /* Encode the modifiers. */
5559 if (m_mouse_tracking_mode >= MouseTrackingMode::eSEND_XY_ON_BUTTON) {
5560 if (m_modifiers & GDK_SHIFT_MASK) {
5561 cb |= 4;
5562 }
5563 if (m_modifiers & VTE_ALT_MASK) {
5564 cb |= 8;
5565 }
5566 if (m_modifiers & GDK_CONTROL_MASK) {
5567 cb |= 16;
5568 }
5569 }
5570
5571 /* Encode a drag event. */
5572 if (is_drag) {
5573 cb |= 32;
5574 }
5575
5576 /* Check the extensions in decreasing order of preference. Encoding the release event above assumes that 1006 comes first. */
5577 if (m_modes_private.XTERM_MOUSE_EXT_SGR()) {
5578 /* xterm's extended mode (1006) */
5579 send(is_release ? VTE_REPLY_XTERM_MOUSE_EXT_SGR_REPORT_BUTTON_RELEASE
5580 : VTE_REPLY_XTERM_MOUSE_EXT_SGR_REPORT_BUTTON_PRESS,
5581 {cb, (int)cx, (int)cy});
5582 } else if (cx <= 223 && cy <= 223) {
5583 /* legacy mode */
5584 char buf[8];
5585 size_t len = g_snprintf(buf, sizeof(buf), _VTE_CAP_CSI "M%c%c%c", 32 + cb, 32 + (guchar)cx, 32 + (guchar)cy);
5586
5587 /* Send event direct to the child, this is binary not text data */
5588 feed_child_binary({buf, len});
5589 }
5590
5591 return true;
5592 }
5593
5594 void
feed_focus_event(bool in)5595 Terminal::feed_focus_event(bool in)
5596 {
5597 send(in ? VTE_REPLY_XTERM_FOCUS_IN : VTE_REPLY_XTERM_FOCUS_OUT, {});
5598 }
5599
5600 void
feed_focus_event_initial()5601 Terminal::feed_focus_event_initial()
5602 {
5603 /* We immediately send the terminal a focus event, since otherwise
5604 * it has no way to know the current status.
5605 */
5606 feed_focus_event(m_has_focus);
5607 }
5608
5609 void
maybe_feed_focus_event(bool in)5610 Terminal::maybe_feed_focus_event(bool in)
5611 {
5612 if (m_modes_private.XTERM_FOCUS())
5613 feed_focus_event(in);
5614 }
5615
5616 /*
5617 * Terminal::maybe_send_mouse_button:
5618 * @terminal:
5619 * @event:
5620 *
5621 * Sends a mouse button click or release notification to the application,
5622 * if the terminal is in mouse tracking mode.
5623 *
5624 * Returns: %TRUE iff the event was consumed
5625 */
5626 bool
maybe_send_mouse_button(vte::grid::coords const & unconfined_rowcol,vte::platform::MouseEvent const & event)5627 Terminal::maybe_send_mouse_button(vte::grid::coords const& unconfined_rowcol,
5628 vte::platform::MouseEvent const& event)
5629 {
5630 switch (event.type()) {
5631 case vte::platform::EventBase::Type::eMOUSE_PRESS:
5632 if (event.press_count() != 1 ||
5633 (m_mouse_tracking_mode < MouseTrackingMode::eSEND_XY_ON_CLICK)) {
5634 return false;
5635 }
5636 break;
5637 case vte::platform::EventBase::Type::eMOUSE_RELEASE:
5638 if (event.press_count() != 1 ||
5639 (m_mouse_tracking_mode < MouseTrackingMode::eSEND_XY_ON_BUTTON)) {
5640 return false;
5641 }
5642 break;
5643 default:
5644 return false;
5645 }
5646
5647 auto rowcol = confine_grid_coords(unconfined_rowcol);
5648 return feed_mouse_event(rowcol,
5649 event.button_value(),
5650 false /* not drag */,
5651 event.is_mouse_release());
5652 }
5653
5654 /*
5655 * Terminal::maybe_send_mouse_drag:
5656 * @terminal:
5657 * @event:
5658 *
5659 * Sends a mouse motion notification to the application,
5660 * if the terminal is in mouse tracking mode.
5661 *
5662 * Returns: %TRUE iff the event was consumed
5663 */
5664 bool
maybe_send_mouse_drag(vte::grid::coords const & unconfined_rowcol,vte::platform::MouseEvent const & event)5665 Terminal::maybe_send_mouse_drag(vte::grid::coords const& unconfined_rowcol,
5666 vte::platform::MouseEvent const& event)
5667 {
5668 /* Need to ensure the ringview is updated. */
5669 ringview_update();
5670
5671 auto rowcol = confine_grid_coords(unconfined_rowcol);
5672
5673 /* First determine if we even want to send notification. */
5674 switch (event.type()) {
5675 case vte::platform::EventBase::Type::eMOUSE_MOTION:
5676 if (m_mouse_tracking_mode < MouseTrackingMode::eCELL_MOTION_TRACKING)
5677 return false;
5678
5679 if (m_mouse_tracking_mode < MouseTrackingMode::eALL_MOTION_TRACKING) {
5680
5681 if (m_mouse_pressed_buttons == 0) {
5682 return false;
5683 }
5684 /* The xterm doc is not clear as to whether
5685 * all-tracking also sends degenerate same-cell events;
5686 * we don't.
5687 */
5688 if (rowcol == confined_grid_coords_from_view_coords(m_mouse_last_position))
5689 return false;
5690 }
5691 break;
5692 default:
5693 return false;
5694 }
5695
5696 /* As per xterm, report the leftmost pressed button - if any. */
5697 int button;
5698 if (m_mouse_pressed_buttons & 1)
5699 button = 1;
5700 else if (m_mouse_pressed_buttons & 2)
5701 button = 2;
5702 else if (m_mouse_pressed_buttons & 4)
5703 button = 3;
5704 else
5705 button = 0;
5706
5707 return feed_mouse_event(rowcol,
5708 button,
5709 true /* drag */,
5710 false /* not release */);
5711 }
5712
5713 /*
5714 * Terminal::hyperlink_invalidate_and_get_bbox
5715 *
5716 * Invalidates cells belonging to the non-zero hyperlink idx, in order to
5717 * stop highlighting the previously hovered hyperlink or start highlighting
5718 * the new one. Optionally stores the coordinates of the bounding box.
5719 */
5720 void
hyperlink_invalidate_and_get_bbox(vte::base::Ring::hyperlink_idx_t idx,GdkRectangle * bbox)5721 Terminal::hyperlink_invalidate_and_get_bbox(vte::base::Ring::hyperlink_idx_t idx,
5722 GdkRectangle *bbox)
5723 {
5724 auto first_row = first_displayed_row();
5725 auto end_row = last_displayed_row() + 1;
5726 vte::grid::row_t row, top = LONG_MAX, bottom = -1;
5727 vte::grid::column_t col, left = LONG_MAX, right = -1;
5728 const VteRowData *rowdata;
5729
5730 g_assert (idx != 0);
5731
5732 for (row = first_row; row < end_row; row++) {
5733 rowdata = _vte_ring_index(m_screen->row_data, row);
5734 if (rowdata != NULL) {
5735 bool do_invalidate_row = false;
5736 for (col = 0; col < rowdata->len; col++) {
5737 if (G_UNLIKELY (rowdata->cells[col].attr.hyperlink_idx == idx)) {
5738 do_invalidate_row = true;
5739 top = MIN(top, row);
5740 bottom = MAX(bottom, row);
5741 left = MIN(left, col);
5742 right = MAX(right, col);
5743 }
5744 }
5745 if (G_UNLIKELY (do_invalidate_row)) {
5746 invalidate_row(row);
5747 }
5748 }
5749 }
5750
5751 if (bbox == NULL)
5752 return;
5753
5754 /* If bbox != NULL, we're looking for the new hovered hyperlink which always has onscreen bits. */
5755 g_assert (top != LONG_MAX && bottom != -1 && left != LONG_MAX && right != -1);
5756
5757 auto allocation = get_allocated_rect();
5758 bbox->x = allocation.x + m_padding.left + left * m_cell_width;
5759 bbox->y = allocation.y + m_padding.top + row_to_pixel(top);
5760 bbox->width = (right - left + 1) * m_cell_width;
5761 bbox->height = (bottom - top + 1) * m_cell_height;
5762 _vte_debug_print (VTE_DEBUG_HYPERLINK,
5763 "Hyperlink bounding box: x=%d y=%d w=%d h=%d\n",
5764 bbox->x, bbox->y, bbox->width, bbox->height);
5765 }
5766
5767 /*
5768 * Terminal::hyperlink_hilite_update:
5769 *
5770 * Checks the coordinates for hyperlink. Updates m_hyperlink_hover_idx
5771 * and m_hyperlink_hover_uri, and schedules to update the highlighting.
5772 */
5773 void
hyperlink_hilite_update()5774 Terminal::hyperlink_hilite_update()
5775 {
5776 const VteRowData *rowdata;
5777 bool do_check_hilite;
5778 vte::grid::coords rowcol;
5779 vte::base::Ring::hyperlink_idx_t new_hyperlink_hover_idx = 0;
5780 GdkRectangle bbox;
5781 const char *separator;
5782
5783 if (!m_allow_hyperlink)
5784 return;
5785
5786 _vte_debug_print (VTE_DEBUG_HYPERLINK,
5787 "hyperlink_hilite_update\n");
5788
5789 /* Need to ensure the ringview is updated. */
5790 ringview_update();
5791
5792 /* m_mouse_last_position contains the current position, see bug 789536 comment 24. */
5793 auto pos = m_mouse_last_position;
5794
5795 /* Whether there's any chance we'd highlight something */
5796 do_check_hilite = view_coords_visible(pos) &&
5797 m_mouse_cursor_over_widget &&
5798 !(m_mouse_autohide && m_mouse_cursor_autohidden) &&
5799 !m_selecting;
5800 if (do_check_hilite) {
5801 rowcol = grid_coords_from_view_coords(pos);
5802 rowdata = find_row_data(rowcol.row());
5803 if (rowdata && rowcol.column() < rowdata->len) {
5804 new_hyperlink_hover_idx = rowdata->cells[rowcol.column()].attr.hyperlink_idx;
5805 }
5806 }
5807
5808 if (new_hyperlink_hover_idx == m_hyperlink_hover_idx) {
5809 _vte_debug_print (VTE_DEBUG_HYPERLINK,
5810 "hyperlink did not change\n");
5811 return;
5812 }
5813
5814 /* Invalidate cells of the old hyperlink. */
5815 if (m_hyperlink_hover_idx != 0) {
5816 hyperlink_invalidate_and_get_bbox(m_hyperlink_hover_idx, NULL);
5817 }
5818
5819 /* This might be different from new_hyperlink_hover_idx. If in the stream, that one contains
5820 * the pseudo idx VTE_HYPERLINK_IDX_TARGET_IN_STREAM and now a real idx is allocated.
5821 * Plus, the ring's internal belief of the hovered hyperlink is also updated. */
5822 if (do_check_hilite) {
5823 m_hyperlink_hover_idx = _vte_ring_get_hyperlink_at_position(m_screen->row_data, rowcol.row(), rowcol.column(), true, &m_hyperlink_hover_uri);
5824 } else {
5825 m_hyperlink_hover_idx = 0;
5826 m_hyperlink_hover_uri = nullptr;
5827 }
5828
5829 /* Invalidate cells of the new hyperlink. Get the bounding box. */
5830 if (m_hyperlink_hover_idx != 0) {
5831 /* URI is after the first semicolon */
5832 separator = strchr(m_hyperlink_hover_uri, ';');
5833 g_assert(separator != NULL);
5834 m_hyperlink_hover_uri = separator + 1;
5835
5836 hyperlink_invalidate_and_get_bbox(m_hyperlink_hover_idx, &bbox);
5837 g_assert(bbox.width > 0 && bbox.height > 0);
5838 }
5839 _vte_debug_print(VTE_DEBUG_HYPERLINK,
5840 "Hover idx: %d \"%s\"\n",
5841 m_hyperlink_hover_idx,
5842 m_hyperlink_hover_uri);
5843
5844 /* Underlining hyperlinks has precedence over regex matches. So when the hovered hyperlink changes,
5845 * the regex match might need to become or stop being underlined. */
5846 if (regex_match_has_current())
5847 invalidate_match_span();
5848
5849 apply_mouse_cursor();
5850
5851 emit_hyperlink_hover_uri_changed(m_hyperlink_hover_idx != 0 ? &bbox : NULL);
5852 }
5853
5854 /*
5855 * Terminal::match_hilite_clear:
5856 *
5857 * Reset match variables and invalidate the old match region if highlighted.
5858 */
5859 void
match_hilite_clear()5860 Terminal::match_hilite_clear()
5861 {
5862 if (regex_match_has_current())
5863 invalidate_match_span();
5864
5865 m_match_span.clear();
5866 m_match_current = nullptr;
5867
5868 g_free(m_match);
5869 m_match = nullptr;
5870 }
5871
5872 /* This is only used by the dingu matching code, so no need to extend the area. */
5873 void
invalidate_match_span()5874 Terminal::invalidate_match_span()
5875 {
5876 _vte_debug_print(VTE_DEBUG_EVENTS,
5877 "Invalidating match span %s\n", m_match_span.to_string());
5878 invalidate(m_match_span);
5879 }
5880
5881 /*
5882 * Terminal::match_hilite_update:
5883 *
5884 * Checks the coordinates for dingu matches, setting m_match_span to
5885 * the match region or the no-matches region, and if there is a match,
5886 * sets it to display highlighted.
5887 */
5888 void
match_hilite_update()5889 Terminal::match_hilite_update()
5890 {
5891 /* Need to ensure the ringview is updated. */
5892 ringview_update();
5893
5894 /* m_mouse_last_position contains the current position, see bug 789536 comment 24. */
5895 auto pos = m_mouse_last_position;
5896
5897 glong col = pos.x / m_cell_width;
5898 glong row = pixel_to_row(pos.y);
5899
5900 /* BiDi: convert to logical column. */
5901 vte::base::BidiRow const* bidirow = m_ringview.get_bidirow(confine_grid_row(row));
5902 col = bidirow->vis2log(col);
5903
5904 _vte_debug_print(VTE_DEBUG_EVENTS,
5905 "Match hilite update (%ld, %ld) -> %ld, %ld\n",
5906 pos.x, pos.y, col, row);
5907
5908 /* Whether there's any chance we'd highlight something */
5909 bool do_check_hilite = view_coords_visible(pos) &&
5910 m_mouse_cursor_over_widget &&
5911 !(m_mouse_autohide && m_mouse_cursor_autohidden) &&
5912 !m_selecting;
5913 if (!do_check_hilite) {
5914 if (regex_match_has_current())
5915 match_hilite_clear();
5916 return;
5917 }
5918
5919 if (m_match_span.contains(row, col)) {
5920 /* Already highlighted. */
5921 return;
5922 }
5923
5924 /* Reset match variables and invalidate the old match region if highlighted */
5925 match_hilite_clear();
5926
5927 /* Check for matches. */
5928 gsize start, end;
5929 auto new_match = match_check_internal(col,
5930 row,
5931 &m_match_current,
5932 &start,
5933 &end);
5934
5935 /* Read the new locations. */
5936 if (start < m_match_attributes->len &&
5937 end < m_match_attributes->len) {
5938 struct _VteCharAttributes const *sa, *ea;
5939 sa = &g_array_index(m_match_attributes,
5940 struct _VteCharAttributes,
5941 start);
5942 ea = &g_array_index(m_match_attributes,
5943 struct _VteCharAttributes,
5944 end);
5945
5946 /* convert from inclusive to exclusive (a.k.a. boundary) ending, taking a possible last CJK character into account */
5947 m_match_span = vte::grid::span(sa->row, sa->column, ea->row, ea->column + ea->columns);
5948 }
5949
5950 g_assert(!m_match); /* from match_hilite_clear() above */
5951 m_match = new_match;
5952
5953 if (m_match) {
5954 _vte_debug_print(VTE_DEBUG_EVENTS,
5955 "Matched %s.\n", m_match_span.to_string());
5956 invalidate_match_span();
5957 } else {
5958 _vte_debug_print(VTE_DEBUG_EVENTS,
5959 "No matches %s.\n", m_match_span.to_string());
5960 }
5961
5962 apply_mouse_cursor();
5963 }
5964
5965 void
widget_clipboard_data_clear(vte::platform::Clipboard const & clipboard)5966 Terminal::widget_clipboard_data_clear(vte::platform::Clipboard const& clipboard)
5967 {
5968 if (m_changing_selection)
5969 return;
5970
5971 switch (clipboard.type()) {
5972 case vte::platform::ClipboardType::PRIMARY:
5973 if (m_selection_owned[vte::to_integral(vte::platform::ClipboardType::PRIMARY)] &&
5974 !m_selection_resolved.empty()) {
5975 _vte_debug_print(VTE_DEBUG_SELECTION, "Lost selection.\n");
5976 deselect_all();
5977 }
5978
5979 [[fallthrough]];
5980 case vte::platform::ClipboardType::CLIPBOARD:
5981 m_selection_owned[vte::to_integral(clipboard.type())] = false;
5982 break;
5983 }
5984 }
5985
5986 std::optional<std::string_view>
widget_clipboard_data_get(vte::platform::Clipboard const & clipboard,vte::platform::ClipboardFormat format)5987 Terminal::widget_clipboard_data_get(vte::platform::Clipboard const& clipboard,
5988 vte::platform::ClipboardFormat format)
5989 {
5990 auto const sel = vte::to_integral(clipboard.type());
5991
5992 if (m_selection[sel] == nullptr)
5993 return std::nullopt;
5994
5995 _VTE_DEBUG_IF(VTE_DEBUG_SELECTION) {
5996 g_printerr("Setting selection %d (%" G_GSIZE_FORMAT " UTF-8 bytes.) for target %s\n",
5997 sel,
5998 m_selection[sel]->len,
5999 format == vte::platform::ClipboardFormat::HTML ? "HTML" : "TEXT");
6000 _vte_debug_hexdump("Selection data", (uint8_t const*)m_selection[sel]->str, m_selection[sel]->len);
6001 }
6002
6003 return std::string_view{m_selection[sel]->str, m_selection[sel]->len};
6004 }
6005
6006 /* Convert the internal color code (either index or RGB) into RGB. */
6007 template <unsigned int redbits,
6008 unsigned int greenbits,
6009 unsigned int bluebits>
6010 void
rgb_from_index(guint index,vte::color::rgb & color) const6011 Terminal::rgb_from_index(guint index,
6012 vte::color::rgb& color) const
6013 {
6014 bool dim = false;
6015 if (!(index & VTE_RGB_COLOR_MASK(redbits, greenbits, bluebits)) && (index & VTE_DIM_COLOR)) {
6016 index &= ~VTE_DIM_COLOR;
6017 dim = true;
6018 }
6019
6020 if (index >= VTE_LEGACY_COLORS_OFFSET && index < VTE_LEGACY_COLORS_OFFSET + VTE_LEGACY_FULL_COLOR_SET_SIZE)
6021 index -= VTE_LEGACY_COLORS_OFFSET;
6022 if (index < VTE_PALETTE_SIZE) {
6023 color = *get_color(index);
6024 if (dim) {
6025 /* magic formula taken from xterm */
6026 color.red = color.red * 2 / 3;
6027 color.green = color.green * 2 / 3;
6028 color.blue = color.blue * 2 / 3;
6029 }
6030 } else if (index & VTE_RGB_COLOR_MASK(redbits, greenbits, bluebits)) {
6031 color.red = VTE_RGB_COLOR_GET_COMPONENT(index, greenbits + bluebits, redbits) * 0x101U;
6032 color.green = VTE_RGB_COLOR_GET_COMPONENT(index, bluebits, greenbits) * 0x101U;
6033 color.blue = VTE_RGB_COLOR_GET_COMPONENT(index, 0, bluebits) * 0x101U;
6034 } else {
6035 g_assert_not_reached();
6036 }
6037 }
6038
6039 GString*
get_text(vte::grid::row_t start_row,vte::grid::column_t start_col,vte::grid::row_t end_row,vte::grid::column_t end_col,bool block,bool wrap,GArray * attributes)6040 Terminal::get_text(vte::grid::row_t start_row,
6041 vte::grid::column_t start_col,
6042 vte::grid::row_t end_row,
6043 vte::grid::column_t end_col,
6044 bool block,
6045 bool wrap,
6046 GArray *attributes)
6047 {
6048 const VteCell *pcell = NULL;
6049 GString *string;
6050 struct _VteCharAttributes attr;
6051 vte::color::rgb fore, back;
6052 std::unique_ptr<vte::base::RingView> ringview;
6053 vte::base::BidiRow const *bidirow = nullptr;
6054 vte::grid::column_t vcol;
6055
6056 if (attributes)
6057 g_array_set_size (attributes, 0);
6058
6059 string = g_string_new(NULL);
6060 memset(&attr, 0, sizeof(attr));
6061
6062 if (start_col < 0)
6063 start_col = 0;
6064
6065 if (m_enable_bidi && block) {
6066 /* Rectangular selection operates on the visual contents, not the logical.
6067 * m_ringview corresponds to the currently onscreen bits, therefore does not
6068 * necessarily include the entire selection. Also we want m_ringview's size
6069 * to be limited, even if the user selects a giant rectangle.
6070 * So use a new ringview for the selection. */
6071 ringview = std::make_unique<vte::base::RingView>();
6072 ringview->set_ring(m_screen->row_data);
6073 ringview->set_rows(start_row, end_row - start_row + 1);
6074 ringview->set_width(m_column_count);
6075 ringview->update();
6076 }
6077
6078 vte::grid::column_t lcol = block ? 0 : start_col;
6079 vte::grid::row_t row;
6080 for (row = start_row; row < end_row + 1; row++, lcol = 0) {
6081 VteRowData const* row_data = find_row_data(row);
6082 gsize last_empty, last_nonempty;
6083 vte::grid::column_t last_emptycol, last_nonemptycol;
6084 vte::grid::column_t line_last_column = (!block && row == end_row) ? end_col : m_column_count;
6085
6086 last_empty = last_nonempty = string->len;
6087 last_emptycol = last_nonemptycol = -1;
6088
6089 attr.row = row;
6090 attr.column = lcol;
6091 pcell = NULL;
6092 if (row_data != NULL) {
6093 bidirow = ringview ? ringview->get_bidirow(row) : nullptr;
6094 while (lcol < line_last_column &&
6095 (pcell = _vte_row_data_get (row_data, lcol))) {
6096
6097 /* In block mode, we scan each row from its very beginning to its very end in logical order,
6098 * and here filter out the characters that are visually outside of the block. */
6099 if (bidirow) {
6100 vcol = bidirow->log2vis(lcol);
6101 if (vcol < start_col || vcol >= end_col) {
6102 lcol++;
6103 continue;
6104 }
6105 }
6106
6107 attr.column = lcol;
6108
6109 /* If it's not part of a multi-column character,
6110 * and passes the selection criterion, add it to
6111 * the selection. */
6112 if (!pcell->attr.fragment()) {
6113 /* Store the attributes of this character. */
6114 // FIXMEchpe shouldn't this use determine_colors?
6115 uint32_t fg, bg, dc;
6116 vte_color_triple_get(pcell->attr.colors(), &fg, &bg, &dc);
6117 rgb_from_index<8, 8, 8>(fg, fore);
6118 rgb_from_index<8, 8, 8>(bg, back);
6119 attr.fore.red = fore.red;
6120 attr.fore.green = fore.green;
6121 attr.fore.blue = fore.blue;
6122 attr.back.red = back.red;
6123 attr.back.green = back.green;
6124 attr.back.blue = back.blue;
6125 attr.underline = (pcell->attr.underline() == 1);
6126 attr.strikethrough = pcell->attr.strikethrough();
6127 attr.columns = pcell->attr.columns();
6128
6129 /* Store the cell string */
6130 if (pcell->c == 0) {
6131 /* Empty cells of nondefault background color are
6132 * stored as NUL characters. Treat them as spaces,
6133 * but make a note of the last occurrence. */
6134 g_string_append_c (string, ' ');
6135 last_empty = string->len;
6136 last_emptycol = lcol;
6137 } else {
6138 _vte_unistr_append_to_string (pcell->c, string);
6139 last_nonempty = string->len;
6140 last_nonemptycol = lcol;
6141 }
6142
6143 /* If we added text to the string, record its
6144 * attributes, one per byte. */
6145 if (attributes) {
6146 vte_g_array_fill(attributes,
6147 &attr, string->len);
6148 }
6149 }
6150
6151 lcol++;
6152 }
6153 }
6154
6155 /* Empty cells of nondefault background color can appear anywhere in a line,
6156 * not just at the end, e.g. between "foo" and "bar" here:
6157 * echo -e '\e[46mfoo\e[K\e[7Gbar\e[m'
6158 * Strip off the trailing ones, preserve the middle ones. */
6159 if (last_empty > last_nonempty) {
6160
6161 lcol = last_emptycol + 1;
6162
6163 if (row_data != NULL) {
6164 while ((pcell = _vte_row_data_get (row_data, lcol))) {
6165 lcol++;
6166
6167 if (pcell->attr.fragment())
6168 continue;
6169
6170 if (pcell->c != 0)
6171 break;
6172 }
6173 }
6174 if (pcell == NULL) {
6175 g_string_truncate(string, last_nonempty);
6176 if (attributes)
6177 g_array_set_size(attributes, string->len);
6178 attr.column = last_nonemptycol;
6179 }
6180 }
6181
6182 /* Adjust column, in case we want to append a newline */
6183 //FIXMEchpe MIN ?
6184 attr.column = MAX(m_column_count, attr.column + 1);
6185
6186 /* Add a newline in block mode. */
6187 if (block) {
6188 string = g_string_append_c(string, '\n');
6189 }
6190 /* Else, if the last visible column on this line was in range and
6191 * not soft-wrapped, append a newline. */
6192 else if (row < end_row) {
6193 /* If we didn't softwrap, add a newline. */
6194 /* XXX need to clear row->soft_wrap on deletion! */
6195 if (!m_screen->row_data->is_soft_wrapped(row)) {
6196 string = g_string_append_c(string, '\n');
6197 }
6198 }
6199
6200 /* Make sure that the attributes array is as long as the string. */
6201 if (attributes) {
6202 vte_g_array_fill (attributes, &attr, string->len);
6203 }
6204 }
6205
6206 /* Sanity check. */
6207 if (attributes != nullptr)
6208 g_assert_cmpuint(string->len, ==, attributes->len);
6209
6210 return string;
6211 }
6212
6213 GString*
get_text_displayed(bool wrap,GArray * attributes)6214 Terminal::get_text_displayed(bool wrap,
6215 GArray *attributes)
6216 {
6217 return get_text(first_displayed_row(), 0,
6218 last_displayed_row() + 1, 0,
6219 false /* block */, wrap,
6220 attributes);
6221 }
6222
6223 /* This is distinct from just using first/last_displayed_row since a11y
6224 * doesn't know about sub-row displays.
6225 */
6226 GString*
get_text_displayed_a11y(bool wrap,GArray * attributes)6227 Terminal::get_text_displayed_a11y(bool wrap,
6228 GArray *attributes)
6229 {
6230 return get_text(m_screen->scroll_delta, 0,
6231 m_screen->scroll_delta + m_row_count - 1 + 1, 0,
6232 false /* block */, wrap,
6233 attributes);
6234 }
6235
6236 GString*
get_selected_text(GArray * attributes)6237 Terminal::get_selected_text(GArray *attributes)
6238 {
6239 return get_text(m_selection_resolved.start_row(),
6240 m_selection_resolved.start_column(),
6241 m_selection_resolved.end_row(),
6242 m_selection_resolved.end_column(),
6243 m_selection_block_mode,
6244 true /* wrap */,
6245 attributes);
6246 }
6247
6248 #ifdef VTE_DEBUG
6249 unsigned int
checksum_area(vte::grid::row_t start_row,vte::grid::column_t start_col,vte::grid::row_t end_row,vte::grid::column_t end_col)6250 Terminal::checksum_area(vte::grid::row_t start_row,
6251 vte::grid::column_t start_col,
6252 vte::grid::row_t end_row,
6253 vte::grid::column_t end_col)
6254 {
6255 unsigned int checksum = 0;
6256
6257 auto text = get_text(start_row, start_col, end_row, end_col,
6258 true /* block */, false /* wrap */,
6259 nullptr /* not interested in attributes */);
6260 if (text == nullptr)
6261 return checksum;
6262
6263 char const* end = (char const*)text->str + text->len;
6264 for (char const *p = text->str; p < end; p = g_utf8_next_char(p)) {
6265 auto const c = g_utf8_get_char(p);
6266 if (c == '\n')
6267 continue;
6268 checksum += c;
6269 }
6270 g_string_free(text, true);
6271
6272 return checksum & 0xffff;
6273 }
6274 #endif /* VTE_DEBUG */
6275
6276 /*
6277 * Compares the visual attributes of a VteCellAttr for equality, but ignores
6278 * attributes that tend to change from character to character or are otherwise
6279 * strange (in particular: fragment, columns).
6280 */
6281 // FIXMEchpe: make VteCellAttr a class with operator==
6282 static bool
vte_terminal_cellattr_equal(VteCellAttr const * attr1,VteCellAttr const * attr2)6283 vte_terminal_cellattr_equal(VteCellAttr const* attr1,
6284 VteCellAttr const* attr2)
6285 {
6286 //FIXMEchpe why exclude DIM here?
6287 return (((attr1->attr ^ attr2->attr) & VTE_ATTR_ALL_MASK) == 0 &&
6288 attr1->colors() == attr2->colors() &&
6289 attr1->hyperlink_idx == attr2->hyperlink_idx);
6290 }
6291
6292 /*
6293 * Wraps a given string according to the VteCellAttr in HTML tags. Used
6294 * old-style HTML (and not CSS) for better compatibility with, for example,
6295 * evolution's mail editor component.
6296 */
6297 char *
cellattr_to_html(VteCellAttr const * attr,char const * text) const6298 Terminal::cellattr_to_html(VteCellAttr const* attr,
6299 char const* text) const
6300 {
6301 GString *string;
6302 guint fore, back, deco;
6303
6304 string = g_string_new(text);
6305
6306 determine_colors(attr, false, false, &fore, &back, &deco);
6307
6308 if (attr->bold()) {
6309 g_string_prepend(string, "<b>");
6310 g_string_append(string, "</b>");
6311 }
6312 if (attr->italic()) {
6313 g_string_prepend(string, "<i>");
6314 g_string_append(string, "</i>");
6315 }
6316 /* <u> should be inside <font> so that it inherits its color by default */
6317 if (attr->underline() != 0) {
6318 static const char styles[][7] = {"", "single", "double", "wavy"};
6319 char *tag, *colorattr;
6320
6321 if (deco != VTE_DEFAULT_FG) {
6322 vte::color::rgb color;
6323
6324 rgb_from_index<4, 5, 4>(deco, color);
6325 colorattr = g_strdup_printf(";text-decoration-color:#%02X%02X%02X",
6326 color.red >> 8,
6327 color.green >> 8,
6328 color.blue >> 8);
6329 } else {
6330 colorattr = g_strdup("");
6331 }
6332
6333 tag = g_strdup_printf("<u style=\"text-decoration-style:%s%s\">",
6334 styles[attr->underline()],
6335 colorattr);
6336 g_string_prepend(string, tag);
6337 g_free(tag);
6338 g_free(colorattr);
6339 g_string_append(string, "</u>");
6340 }
6341 if (fore != VTE_DEFAULT_FG || attr->reverse()) {
6342 vte::color::rgb color;
6343 char *tag;
6344
6345 rgb_from_index<8, 8, 8>(fore, color);
6346 tag = g_strdup_printf("<font color=\"#%02X%02X%02X\">",
6347 color.red >> 8,
6348 color.green >> 8,
6349 color.blue >> 8);
6350 g_string_prepend(string, tag);
6351 g_free(tag);
6352 g_string_append(string, "</font>");
6353 }
6354 if (back != VTE_DEFAULT_BG || attr->reverse()) {
6355 vte::color::rgb color;
6356 char *tag;
6357
6358 rgb_from_index<8, 8, 8>(back, color);
6359 tag = g_strdup_printf("<span style=\"background-color:#%02X%02X%02X\">",
6360 color.red >> 8,
6361 color.green >> 8,
6362 color.blue >> 8);
6363 g_string_prepend(string, tag);
6364 g_free(tag);
6365 g_string_append(string, "</span>");
6366 }
6367 if (attr->strikethrough()) {
6368 g_string_prepend(string, "<strike>");
6369 g_string_append(string, "</strike>");
6370 }
6371 if (attr->overline()) {
6372 g_string_prepend(string, "<span style=\"text-decoration-line:overline\">");
6373 g_string_append(string, "</span>");
6374 }
6375 if (attr->blink()) {
6376 g_string_prepend(string, "<blink>");
6377 g_string_append(string, "</blink>");
6378 }
6379 /* reverse and invisible are not supported */
6380
6381 return g_string_free(string, FALSE);
6382 }
6383
6384 /*
6385 * Similar to find_charcell(), but takes a VteCharAttribute for
6386 * indexing and returns the VteCellAttr.
6387 */
6388 VteCellAttr const*
char_to_cell_attr(VteCharAttributes const * attr) const6389 Terminal::char_to_cell_attr(VteCharAttributes const* attr) const
6390 {
6391 VteCell const* cell = find_charcell(attr->column, attr->row);
6392 if (cell)
6393 return &cell->attr;
6394 return nullptr;
6395 }
6396
6397 /*
6398 * Terminal::attributes_to_html:
6399 * @text: A string as returned by the vte_terminal_get_* family of functions.
6400 * @attrs: (array) (element-type Vte.CharAttributes): text attributes, as created by vte_terminal_get_*
6401 *
6402 * Marks the given text up according to the given attributes, using HTML <span>
6403 * commands, and wraps the string in a <pre> element. The attributes have to be
6404 * "fresh" in the sense that the terminal must not have changed since they were
6405 * obtained using the vte_terminal_get* function.
6406 *
6407 * Returns: (transfer full): a newly allocated text string, or %NULL.
6408 */
6409 GString*
attributes_to_html(GString * text_string,GArray * attrs)6410 Terminal::attributes_to_html(GString* text_string,
6411 GArray* attrs)
6412 {
6413 GString *string;
6414 guint from,to;
6415 const VteCellAttr *attr;
6416 char *escaped, *marked;
6417
6418 char const* text = text_string->str;
6419 auto len = text_string->len;
6420 g_assert_cmpuint(len, ==, attrs->len);
6421
6422 /* Initial size fits perfectly if the text has no attributes and no
6423 * characters that need to be escaped
6424 */
6425 string = g_string_sized_new (len + 11);
6426
6427 g_string_append(string, "<pre>");
6428 /* Find streches with equal attributes. Newlines are treated specially,
6429 * so that the <span> do not cover multiple lines.
6430 */
6431 from = to = 0;
6432 while (text[from] != '\0') {
6433 g_assert(from == to);
6434 if (text[from] == '\n') {
6435 g_string_append_c(string, '\n');
6436 from = ++to;
6437 } else {
6438 attr = char_to_cell_attr(
6439 &g_array_index(attrs, VteCharAttributes, from));
6440 while (text[to] != '\0' && text[to] != '\n' &&
6441 vte_terminal_cellattr_equal(attr,
6442 char_to_cell_attr(
6443 &g_array_index(attrs, VteCharAttributes, to))))
6444 {
6445 to++;
6446 }
6447 escaped = g_markup_escape_text(text + from, to - from);
6448 marked = cellattr_to_html(attr, escaped);
6449 g_string_append(string, marked);
6450 g_free(escaped);
6451 g_free(marked);
6452 from = to;
6453 }
6454 }
6455 g_string_append(string, "</pre>");
6456
6457 return string;
6458 }
6459
6460 /* Place the selected text onto the clipboard. Do this asynchronously so that
6461 * we get notified when the selection we placed on the clipboard is replaced. */
6462 void
widget_copy(vte::platform::ClipboardType type,vte::platform::ClipboardFormat format)6463 Terminal::widget_copy(vte::platform::ClipboardType type,
6464 vte::platform::ClipboardFormat format)
6465 {
6466 /* Only put HTML on the CLIPBOARD, not PRIMARY */
6467 assert(type == vte::platform::ClipboardType::CLIPBOARD ||
6468 format == vte::platform::ClipboardFormat::TEXT);
6469
6470 /* Chuck old selected text and retrieve the newly-selected text. */
6471 GArray *attributes = g_array_new(FALSE, TRUE, sizeof(struct _VteCharAttributes));
6472 auto selection = get_selected_text(attributes);
6473
6474 auto const sel = vte::to_integral(type);
6475 if (m_selection[sel]) {
6476 g_string_free(m_selection[sel], TRUE);
6477 m_selection[sel] = nullptr;
6478 }
6479
6480 if (selection == nullptr) {
6481 g_array_free(attributes, TRUE);
6482 m_selection_owned[sel] = false;
6483 return;
6484 }
6485
6486 if (format == vte::platform::ClipboardFormat::HTML) {
6487 m_selection[sel] = attributes_to_html(selection, attributes);
6488 g_string_free(selection, TRUE);
6489 } else {
6490 m_selection[sel] = selection;
6491 }
6492
6493 g_array_free (attributes, TRUE);
6494
6495 /* Place the text on the clipboard. */
6496 _vte_debug_print(VTE_DEBUG_SELECTION,
6497 "Assuming ownership of selection.\n");
6498
6499 m_selection_owned[sel] = true;
6500 m_selection_format[sel] = format;
6501
6502 m_changing_selection = true;
6503 widget()->clipboard_offer_data(type, format);
6504 m_changing_selection = false;
6505 }
6506
6507 /* Confine coordinates into the visible area. Padding is already subtracted. */
6508 void
confine_coordinates(long * xp,long * yp)6509 Terminal::confine_coordinates(long *xp,
6510 long *yp)
6511 {
6512 long x = *xp;
6513 long y = *yp;
6514 long y_stop;
6515
6516 /* Allow to use the bottom extra padding only if there's content there. */
6517 y_stop = MIN(m_view_usable_extents.height(),
6518 row_to_pixel(m_screen->insert_delta + m_row_count));
6519
6520 if (y < 0) {
6521 y = 0;
6522 if (!m_selection_block_mode)
6523 x = 0;
6524 } else if (y >= y_stop) {
6525 y = y_stop - 1;
6526 if (!m_selection_block_mode)
6527 x = m_column_count * m_cell_width - 1;
6528 }
6529 if (x < 0) {
6530 x = 0;
6531 } else if (x >= m_column_count * m_cell_width) {
6532 x = m_column_count * m_cell_width - 1;
6533 }
6534
6535 *xp = x;
6536 *yp = y;
6537 }
6538
6539 /* Start selection at the location of the event.
6540 * In case of regular selection, this is called with the original click's location
6541 * once the mouse has moved by the gtk drag threshold.
6542 */
6543 void
start_selection(vte::view::coords const & pos,SelectionType type)6544 Terminal::start_selection (vte::view::coords const& pos,
6545 SelectionType type)
6546 {
6547 if (m_selection_block_mode)
6548 type = SelectionType::eCHAR;
6549
6550 /* Need to ensure the ringview is updated. */
6551 ringview_update();
6552
6553 m_selection_origin = m_selection_last = selection_grid_halfcoords_from_view_coords(pos);
6554
6555 /* Record the selection type. */
6556 m_selection_type = type;
6557 m_selecting = TRUE;
6558 m_selecting_had_delta = false; /* resolve_selection() below will most likely flip it to true. */
6559 m_will_select_after_threshold = false;
6560
6561 _vte_debug_print(VTE_DEBUG_SELECTION,
6562 "Selection started at %s.\n",
6563 m_selection_origin.to_string());
6564
6565 /* Take care of updating the display. */
6566 resolve_selection();
6567
6568 /* Temporarily stop caring about input from the child. */
6569 disconnect_pty_read();
6570 }
6571
6572 bool
maybe_end_selection()6573 Terminal::maybe_end_selection()
6574 {
6575 if (m_selecting) {
6576 /* Copy only if something was selected. */
6577 if (!m_selection_resolved.empty() &&
6578 m_selecting_had_delta) {
6579 widget_copy(vte::platform::ClipboardType::PRIMARY,
6580 vte::platform::ClipboardFormat::TEXT);
6581 emit_selection_changed();
6582 }
6583 stop_autoscroll(); /* Required before setting m_selecting to false, see #105. */
6584 m_selecting = false;
6585
6586 /* Reconnect to input from the child if we paused it. */
6587 connect_pty_read();
6588
6589 return true;
6590 }
6591
6592 if (m_will_select_after_threshold)
6593 return true;
6594
6595 return false;
6596 }
6597
6598 /*
6599 * Terminal::select_all:
6600 *
6601 * Selects all text within the terminal. Note that we only select the writable
6602 * region, *not* the scrollback buffer, due to this potentially selecting so
6603 * much data that putting it on the clipboard either hangs the process for a long
6604 * time or even crash it directly. (FIXME!)
6605 */
6606 void
select_all()6607 Terminal::select_all()
6608 {
6609 deselect_all();
6610
6611 m_selecting_had_delta = TRUE;
6612
6613 m_selection_resolved.set({m_screen->insert_delta, 0},
6614 {_vte_ring_next(m_screen->row_data), 0});
6615
6616 _vte_debug_print(VTE_DEBUG_SELECTION, "Selecting *all* text.\n");
6617
6618 widget_copy(vte::platform::ClipboardType::PRIMARY,
6619 vte::platform::ClipboardFormat::TEXT);
6620 emit_selection_changed();
6621
6622 invalidate_all();
6623 }
6624
6625 bool
mouse_autoscroll_timer_callback()6626 Terminal::mouse_autoscroll_timer_callback()
6627 {
6628 bool extend = false;
6629 long x, y, xmax, ymax;
6630 glong adj;
6631
6632 auto again = bool{true};
6633
6634 /* Provide an immediate effect for mouse wigglers. */
6635 if (m_mouse_last_position.y < 0) {
6636 if (m_vadjustment) {
6637 /* Try to scroll up by one line. */
6638 adj = m_screen->scroll_delta - 1;
6639 queue_adjustment_value_changed_clamped(adj);
6640 extend = true;
6641 }
6642 _vte_debug_print(VTE_DEBUG_EVENTS, "Autoscrolling down.\n");
6643 }
6644 if (m_mouse_last_position.y >= m_view_usable_extents.height()) {
6645 if (m_vadjustment) {
6646 /* Try to scroll up by one line. */
6647 adj = m_screen->scroll_delta + 1;
6648 queue_adjustment_value_changed_clamped(adj);
6649 extend = true;
6650 }
6651 _vte_debug_print(VTE_DEBUG_EVENTS, "Autoscrolling up.\n");
6652 }
6653 if (extend) {
6654 // FIXMEchpe use confine_view_coords here
6655 /* Don't select off-screen areas. That just confuses people. */
6656 xmax = m_column_count * m_cell_width;
6657 ymax = m_row_count * m_cell_height;
6658
6659 x = CLAMP(m_mouse_last_position.x, 0, xmax);
6660 y = CLAMP(m_mouse_last_position.y, 0, ymax);
6661 /* If we clamped the Y, mess with the X to get the entire
6662 * lines. */
6663 if (m_mouse_last_position.y < 0 && !m_selection_block_mode) {
6664 x = 0;
6665 }
6666 if (m_mouse_last_position.y >= ymax && !m_selection_block_mode) {
6667 x = m_column_count * m_cell_width;
6668 }
6669 /* Extend selection to cover the newly-scrolled area. */
6670 modify_selection(vte::view::coords(x, y));
6671 } else {
6672 /* Stop autoscrolling. */
6673 again = false;
6674 }
6675 return again;
6676 }
6677
6678 /* Start autoscroll. */
6679 void
start_autoscroll()6680 Terminal::start_autoscroll()
6681 {
6682 if (m_mouse_autoscroll_timer)
6683 return;
6684
6685 m_mouse_autoscroll_timer.schedule(666 / m_row_count, // FIXME WTF?
6686 vte::glib::Timer::Priority::eLOW);
6687 }
6688
6689 bool
widget_mouse_motion(vte::platform::MouseEvent const & event)6690 Terminal::widget_mouse_motion(vte::platform::MouseEvent const& event)
6691 {
6692 /* Need to ensure the ringview is updated. */
6693 ringview_update();
6694
6695 auto pos = view_coords_from_event(event);
6696 auto rowcol = grid_coords_from_view_coords(pos);
6697
6698 _vte_debug_print(VTE_DEBUG_EVENTS,
6699 "Motion notify %s %s\n",
6700 pos.to_string(), rowcol.to_string());
6701
6702 m_modifiers = event.modifiers();
6703
6704 if (m_will_select_after_threshold) {
6705 if (!gtk_drag_check_threshold(m_widget,
6706 m_mouse_last_position.x,
6707 m_mouse_last_position.y,
6708 pos.x, pos.y))
6709 return true;
6710
6711 start_selection(vte::view::coords(m_mouse_last_position.x, m_mouse_last_position.y),
6712 SelectionType::eCHAR);
6713 }
6714
6715 auto handled = bool{false};
6716 if (m_selecting &&
6717 (m_mouse_handled_buttons & 1) != 0) {
6718 _vte_debug_print(VTE_DEBUG_EVENTS, "Mousing drag 1.\n");
6719 modify_selection(pos);
6720
6721 /* Start scrolling if we need to. */
6722 if (pos.y < 0 || pos.y >= m_view_usable_extents.height()) {
6723 /* Give mouse wigglers something. */
6724 stop_autoscroll();
6725 mouse_autoscroll_timer_callback();
6726 start_autoscroll();
6727 }
6728
6729 handled = true;
6730 }
6731
6732 if (!handled && m_input_enabled)
6733 maybe_send_mouse_drag(rowcol, event);
6734
6735 if (pos != m_mouse_last_position) {
6736 m_mouse_last_position = pos;
6737
6738 set_pointer_autohidden(false);
6739 hyperlink_hilite_update();
6740 match_hilite_update();
6741 }
6742
6743 return handled;
6744 }
6745
6746 bool
widget_mouse_press(vte::platform::MouseEvent const & event)6747 Terminal::widget_mouse_press(vte::platform::MouseEvent const& event)
6748 {
6749 bool handled = false;
6750 gboolean start_selecting = FALSE, extend_selecting = FALSE;
6751
6752 /* Need to ensure the ringview is updated. */
6753 ringview_update();
6754
6755 auto pos = view_coords_from_event(event);
6756 auto rowcol = grid_coords_from_view_coords(pos);
6757
6758 m_modifiers = event.modifiers();
6759
6760 switch (event.press_count()) {
6761 case 1: /* single click */
6762 _vte_debug_print(VTE_DEBUG_EVENTS,
6763 "Button %d single-click at %s\n",
6764 event.button_value(),
6765 rowcol.to_string());
6766 /* Handle this event ourselves. */
6767 switch (event.button()) {
6768 case vte::platform::MouseEvent::Button::eLEFT:
6769 _vte_debug_print(VTE_DEBUG_EVENTS,
6770 "Handling click ourselves.\n");
6771 /* Grab focus. */
6772 if (!m_has_focus)
6773 widget()->grab_focus();
6774
6775 /* If we're in event mode, and the user held down the
6776 * shift key, we start selecting. */
6777 if (m_mouse_tracking_mode != MouseTrackingMode::eNONE) {
6778 if (m_modifiers & GDK_SHIFT_MASK) {
6779 start_selecting = TRUE;
6780 }
6781 } else {
6782 /* If the user hit shift, then extend the
6783 * selection instead. */
6784 if ((m_modifiers & GDK_SHIFT_MASK) &&
6785 !m_selection_resolved.empty()) {
6786 extend_selecting = TRUE;
6787 } else {
6788 start_selecting = TRUE;
6789 }
6790 }
6791 if (start_selecting) {
6792 deselect_all();
6793 m_will_select_after_threshold = true;
6794 m_selection_block_mode = !!(m_modifiers & GDK_CONTROL_MASK);
6795 handled = true;
6796 }
6797 if (extend_selecting) {
6798 /* The whole selection code needs to be
6799 * rewritten. For now, put this here to
6800 * fix bug 614658 */
6801 m_selecting = TRUE;
6802 selection_maybe_swap_endpoints (pos);
6803 modify_selection(pos);
6804 handled = true;
6805 }
6806 break;
6807 /* Paste if the user pressed shift or we're not sending events
6808 * to the app. */
6809 case vte::platform::MouseEvent::Button::eMIDDLE:
6810 if ((m_modifiers & GDK_SHIFT_MASK) ||
6811 m_mouse_tracking_mode == MouseTrackingMode::eNONE) {
6812 if (widget()->primary_paste_enabled()) {
6813 widget()->clipboard_request_text(vte::platform::ClipboardType::PRIMARY);
6814 handled = true;
6815 }
6816 }
6817 break;
6818 case vte::platform::MouseEvent::Button::eRIGHT:
6819 default:
6820 break;
6821 }
6822 if (event.button_value() >= 1 && event.button_value() <= 3) {
6823 if (handled)
6824 m_mouse_handled_buttons |= (1 << (event.button_value() - 1));
6825 else
6826 m_mouse_handled_buttons &= ~(1 << (event.button_value() - 1));
6827 }
6828 /* If we haven't done anything yet, try sending the mouse
6829 * event to the app. */
6830 if (handled == FALSE) {
6831 handled = maybe_send_mouse_button(rowcol, event);
6832 }
6833 break;
6834 case 2: /* double click */
6835 _vte_debug_print(VTE_DEBUG_EVENTS,
6836 "Button %d double-click at %s\n",
6837 event.button_value(),
6838 rowcol.to_string());
6839 switch (event.button()) {
6840 case vte::platform::MouseEvent::Button::eLEFT:
6841 if (m_will_select_after_threshold) {
6842 start_selection(pos,
6843 SelectionType::eCHAR);
6844 handled = true;
6845 }
6846 if ((m_mouse_handled_buttons & 1) != 0) {
6847 start_selection(pos,
6848 SelectionType::eWORD);
6849 handled = true;
6850 }
6851 break;
6852 case vte::platform::MouseEvent::Button::eMIDDLE:
6853 case vte::platform::MouseEvent::Button::eRIGHT:
6854 default:
6855 break;
6856 }
6857 break;
6858 case 3: /* triple click */
6859 _vte_debug_print(VTE_DEBUG_EVENTS,
6860 "Button %d triple-click at %s\n",
6861 event.button_value(),
6862 rowcol.to_string());
6863 switch (event.button()) {
6864 case vte::platform::MouseEvent::Button::eLEFT:
6865 if ((m_mouse_handled_buttons & 1) != 0) {
6866 start_selection(pos,
6867 SelectionType::eLINE);
6868 handled = true;
6869 }
6870 break;
6871 case vte::platform::MouseEvent::Button::eMIDDLE:
6872 case vte::platform::MouseEvent::Button::eRIGHT:
6873 default:
6874 break;
6875 }
6876 default:
6877 break;
6878 }
6879
6880 /* Save the pointer state for later use. */
6881 if (event.button_value() >= 1 && event.button_value() <= 3)
6882 m_mouse_pressed_buttons |= (1 << (event.button_value() - 1));
6883
6884 m_mouse_last_position = pos;
6885
6886 set_pointer_autohidden(false);
6887 hyperlink_hilite_update();
6888 match_hilite_update();
6889
6890 return handled;
6891 }
6892
6893 bool
widget_mouse_release(vte::platform::MouseEvent const & event)6894 Terminal::widget_mouse_release(vte::platform::MouseEvent const& event)
6895 {
6896 bool handled = false;
6897
6898 /* Need to ensure the ringview is updated. */
6899 ringview_update();
6900
6901 auto pos = view_coords_from_event(event);
6902 auto rowcol = grid_coords_from_view_coords(pos);
6903
6904 stop_autoscroll();
6905
6906 m_modifiers = event.modifiers();
6907
6908 switch (event.type()) {
6909 case vte::platform::EventBase::Type::eMOUSE_RELEASE:
6910 _vte_debug_print(VTE_DEBUG_EVENTS,
6911 "Button %d released at %s\n",
6912 event.button_value(), rowcol.to_string());
6913 switch (event.button()) {
6914 case vte::platform::MouseEvent::Button::eLEFT:
6915 if ((m_mouse_handled_buttons & 1) != 0)
6916 handled = maybe_end_selection();
6917 break;
6918 case vte::platform::MouseEvent::Button::eMIDDLE:
6919 handled = (m_mouse_handled_buttons & 2) != 0;
6920 m_mouse_handled_buttons &= ~2;
6921 break;
6922 case vte::platform::MouseEvent::Button::eRIGHT:
6923 default:
6924 break;
6925 }
6926 if (!handled && m_input_enabled) {
6927 handled = maybe_send_mouse_button(rowcol, event);
6928 }
6929 break;
6930 default:
6931 break;
6932 }
6933
6934 /* Save the pointer state for later use. */
6935 if (event.button_value() >= 1 && event.button_value() <= 3)
6936 m_mouse_pressed_buttons &= ~(1 << (event.button_value() - 1));
6937
6938 m_mouse_last_position = pos;
6939 m_will_select_after_threshold = false;
6940
6941 set_pointer_autohidden(false);
6942 hyperlink_hilite_update();
6943 match_hilite_update();
6944
6945 return handled;
6946 }
6947
6948 void
widget_focus_in()6949 Terminal::widget_focus_in()
6950 {
6951 _vte_debug_print(VTE_DEBUG_EVENTS, "Focus in.\n");
6952
6953 m_has_focus = true;
6954 widget()->grab_focus();
6955
6956 /* We only have an IM context when we're realized, and there's not much
6957 * point to painting the cursor if we don't have a window. */
6958 if (widget_realized()) {
6959 m_cursor_blink_state = TRUE;
6960
6961 /* If blinking gets enabled now, do a full repaint.
6962 * If blinking gets disabled, only repaint if there's blinking stuff present
6963 * (we could further optimize by checking its current phase). */
6964 if (m_text_blink_mode == TextBlinkMode::eFOCUSED ||
6965 (m_text_blink_mode == TextBlinkMode::eUNFOCUSED && m_text_blink_timer)) {
6966 invalidate_all();
6967 }
6968
6969 check_cursor_blink();
6970
6971 m_real_widget->im_focus_in();
6972 invalidate_cursor_once();
6973 maybe_feed_focus_event(true);
6974 }
6975 }
6976
6977 void
widget_focus_out()6978 Terminal::widget_focus_out()
6979 {
6980 _vte_debug_print(VTE_DEBUG_EVENTS, "Focus out.\n");
6981
6982 /* We only have an IM context when we're realized, and there's not much
6983 * point to painting ourselves if we don't have a window. */
6984 if (widget_realized()) {
6985 maybe_feed_focus_event(false);
6986
6987 maybe_end_selection();
6988
6989 /* If blinking gets enabled now, do a full repaint.
6990 * If blinking gets disabled, only repaint if there's blinking stuff present
6991 * (we could further optimize by checking its current phase). */
6992 if (m_text_blink_mode == TextBlinkMode::eUNFOCUSED ||
6993 (m_text_blink_mode == TextBlinkMode::eFOCUSED && m_text_blink_timer)) {
6994 invalidate_all();
6995 }
6996
6997 m_real_widget->im_focus_out();
6998 invalidate_cursor_once();
6999
7000 m_mouse_pressed_buttons = 0;
7001 m_mouse_handled_buttons = 0;
7002 }
7003
7004 m_has_focus = false;
7005 check_cursor_blink();
7006 }
7007
7008 void
widget_mouse_enter(vte::platform::MouseEvent const & event)7009 Terminal::widget_mouse_enter(vte::platform::MouseEvent const& event)
7010 {
7011 auto pos = view_coords_from_event(event);
7012
7013 // FIXMEchpe read event modifiers here
7014
7015 _vte_debug_print(VTE_DEBUG_EVENTS, "Enter at %s\n", pos.to_string());
7016
7017 m_mouse_cursor_over_widget = TRUE;
7018 m_mouse_last_position = pos;
7019
7020 set_pointer_autohidden(false);
7021 hyperlink_hilite_update();
7022 match_hilite_update();
7023 apply_mouse_cursor();
7024 }
7025
7026 void
widget_mouse_leave(vte::platform::MouseEvent const & event)7027 Terminal::widget_mouse_leave(vte::platform::MouseEvent const& event)
7028 {
7029 auto pos = view_coords_from_event(event);
7030
7031 // FIXMEchpe read event modifiers here
7032
7033 _vte_debug_print(VTE_DEBUG_EVENTS, "Leave at %s\n", pos.to_string());
7034
7035 m_mouse_cursor_over_widget = FALSE;
7036 m_mouse_last_position = pos;
7037
7038 hyperlink_hilite_update();
7039 match_hilite_update();
7040 apply_mouse_cursor();
7041 }
7042
7043 /* Apply the changed metrics, and queue a resize if need be.
7044 *
7045 * The cell's height consists of 4 parts, from top to bottom:
7046 * - char_spacing.top: half of the extra line spacing,
7047 * - char_ascent: the font's ascent,
7048 * - char_descent: the font's descent,
7049 * - char_spacing.bottom: the other half of the extra line spacing.
7050 * Extra line spacing is typically 0, beef up cell_height_scale to get actual pixels
7051 * here. Similarly, increase cell_width_scale to get nonzero char_spacing.{left,right}.
7052 */
7053 void
apply_font_metrics(int cell_width_unscaled,int cell_height_unscaled,int cell_width,int cell_height,int char_ascent,int char_descent,GtkBorder char_spacing)7054 Terminal::apply_font_metrics(int cell_width_unscaled,
7055 int cell_height_unscaled,
7056 int cell_width,
7057 int cell_height,
7058 int char_ascent,
7059 int char_descent,
7060 GtkBorder char_spacing)
7061 {
7062 int char_height;
7063 bool resize = false, cresize = false;
7064
7065 /* Sanity check for broken font changes. */
7066 cell_width_unscaled = MAX(cell_width_unscaled, 1);
7067 cell_height_unscaled = MAX(cell_height_unscaled, 2);
7068 cell_width = MAX(cell_width, 1);
7069 cell_height = MAX(cell_height, 2);
7070 char_ascent = MAX(char_ascent, 1);
7071 char_descent = MAX(char_descent, 1);
7072
7073 /* For convenience only. */
7074 char_height = char_ascent + char_descent;
7075
7076 /* Change settings, and keep track of when we've changed anything. */
7077 if (cell_width_unscaled != m_cell_width_unscaled) {
7078 cresize = true;
7079 m_cell_width_unscaled = cell_width_unscaled;
7080 }
7081 if (cell_height_unscaled != m_cell_height_unscaled) {
7082 cresize = true;
7083 m_cell_height_unscaled = cell_height_unscaled;
7084 }
7085 if (cell_width != m_cell_width) {
7086 resize = cresize = true;
7087 m_cell_width = cell_width;
7088 }
7089 if (cell_height != m_cell_height) {
7090 resize = cresize = true;
7091 m_cell_height = cell_height;
7092 }
7093 if (char_ascent != m_char_ascent) {
7094 resize = true;
7095 m_char_ascent = char_ascent;
7096 }
7097 if (char_descent != m_char_descent) {
7098 resize = true;
7099 m_char_descent = char_descent;
7100 }
7101 if (memcmp(&char_spacing, &m_char_padding, sizeof(GtkBorder)) != 0) {
7102 resize = true;
7103 m_char_padding = char_spacing;
7104 }
7105 m_line_thickness = MAX (MIN (char_descent / 2, char_height / 14), 1);
7106 /* FIXME take these from pango_font_metrics_get_{underline,strikethrough}_{position,thickness} */
7107 m_underline_thickness = m_line_thickness;
7108 m_underline_position = MIN (char_spacing.top + char_ascent + m_line_thickness, cell_height - m_underline_thickness);
7109 m_double_underline_thickness = m_line_thickness;
7110 /* FIXME make sure this doesn't reach the baseline (switch to thinner lines, or one thicker line in that case) */
7111 m_double_underline_position = MIN (char_spacing.top + char_ascent + m_line_thickness, cell_height - 3 * m_double_underline_thickness);
7112 m_undercurl_thickness = m_line_thickness;
7113 m_undercurl_position = MIN (char_spacing.top + char_ascent + m_line_thickness, cell_height - _vte_draw_get_undercurl_height(cell_width, m_undercurl_thickness));
7114 m_strikethrough_thickness = m_line_thickness;
7115 m_strikethrough_position = char_spacing.top + char_ascent - char_height / 4;
7116 m_overline_thickness = m_line_thickness;
7117 m_overline_position = char_spacing.top; /* FIXME */
7118 m_regex_underline_thickness = 1; /* FIXME */
7119 m_regex_underline_position = char_spacing.top + char_height - m_regex_underline_thickness; /* FIXME */
7120
7121 /* Queue a resize if anything's changed. */
7122 if (resize) {
7123 if (widget_realized()) {
7124 gtk_widget_queue_resize_no_redraw(m_widget);
7125 }
7126 }
7127 /* Emit a signal that the font changed. */
7128 if (cresize) {
7129 if (pty()) {
7130 /* Update pixel size of PTY. */
7131 pty()->set_size(m_row_count,
7132 m_column_count,
7133 m_cell_height_unscaled,
7134 m_cell_width_unscaled);
7135 }
7136 emit_char_size_changed(m_cell_width, m_cell_height);
7137 }
7138 /* Repaint. */
7139 invalidate_all();
7140 }
7141
7142 void
ensure_font()7143 Terminal::ensure_font()
7144 {
7145 {
7146 /* Load default fonts, if no fonts have been loaded. */
7147 if (!m_has_fonts) {
7148 update_font_desc();
7149 }
7150 if (m_fontdirty) {
7151 int cell_width_unscaled, cell_height_unscaled;
7152 int cell_width, cell_height;
7153 int char_ascent, char_descent;
7154 GtkBorder char_spacing;
7155 m_fontdirty = false;
7156
7157 if (!_vte_double_equal(m_font_scale, 1.)) {
7158 m_draw.set_text_font(
7159 m_widget,
7160 m_unscaled_font_desc.get(),
7161 m_cell_width_scale,
7162 m_cell_height_scale);
7163 m_draw.get_text_metrics(
7164 &cell_width_unscaled, &cell_height_unscaled,
7165 nullptr, nullptr, nullptr);
7166 }
7167
7168 m_draw.set_text_font(
7169 m_widget,
7170 m_fontdesc.get(),
7171 m_cell_width_scale,
7172 m_cell_height_scale);
7173 m_draw.get_text_metrics(
7174 &cell_width, &cell_height,
7175 &char_ascent, &char_descent,
7176 &char_spacing);
7177
7178 if (_vte_double_equal(m_font_scale, 1.)) {
7179 cell_width_unscaled = cell_width;
7180 cell_height_unscaled = cell_height;
7181 }
7182
7183 apply_font_metrics(cell_width_unscaled, cell_height_unscaled,
7184 cell_width, cell_height,
7185 char_ascent, char_descent,
7186 char_spacing);
7187 }
7188 }
7189 }
7190
7191 void
update_font()7192 Terminal::update_font()
7193 {
7194 /* We'll get called again later */
7195 if (!m_unscaled_font_desc)
7196 return;
7197
7198 auto desc = vte::take_freeable(pango_font_description_copy(m_unscaled_font_desc.get()));
7199
7200 double size = pango_font_description_get_size(desc.get());
7201 if (pango_font_description_get_size_is_absolute(desc.get())) {
7202 pango_font_description_set_absolute_size(desc.get(), m_font_scale * size);
7203 } else {
7204 pango_font_description_set_size(desc.get(), m_font_scale * size);
7205 }
7206
7207 m_fontdesc = std::move(desc);
7208 m_fontdirty = true;
7209 m_has_fonts = true;
7210
7211 /* Set the drawing font. */
7212 if (widget_realized()) {
7213 ensure_font();
7214 }
7215 }
7216
7217 /*
7218 * Terminal::set_font_desc:
7219 * @font_desc: (allow-none): a #PangoFontDescription for the desired font, or %nullptr
7220 *
7221 * Sets the font used for rendering all text displayed by the terminal,
7222 * overriding any fonts set using gtk_widget_modify_font(). The terminal
7223 * will immediately attempt to load the desired font, retrieve its
7224 * metrics, and attempt to resize itself to keep the same number of rows
7225 * and columns. The font scale is applied to the specified font.
7226 */
7227 bool
set_font_desc(vte::Freeable<PangoFontDescription> font_desc)7228 Terminal::set_font_desc(vte::Freeable<PangoFontDescription> font_desc)
7229 {
7230 m_api_font_desc = std::move(font_desc);
7231 return update_font_desc();
7232 }
7233
7234 bool
update_font_desc()7235 Terminal::update_font_desc()
7236 {
7237 auto desc = vte::Freeable<PangoFontDescription>{};
7238
7239 auto context = gtk_widget_get_style_context(m_widget);
7240 gtk_style_context_save(context);
7241 gtk_style_context_set_state (context, GTK_STATE_FLAG_NORMAL);
7242 gtk_style_context_get(context, GTK_STATE_FLAG_NORMAL, "font",
7243 &vte::get_freeable(desc),
7244 nullptr);
7245 gtk_style_context_restore(context);
7246
7247 pango_font_description_set_family_static(desc.get(), "monospace");
7248
7249 if (m_api_font_desc) {
7250 pango_font_description_merge(desc.get(), m_api_font_desc.get(), true);
7251 _VTE_DEBUG_IF(VTE_DEBUG_MISC) {
7252 if (desc) {
7253 char *tmp;
7254 tmp = pango_font_description_to_string(desc.get());
7255 g_printerr("Using pango font \"%s\".\n", tmp);
7256 g_free (tmp);
7257 }
7258 }
7259 } else {
7260 _vte_debug_print(VTE_DEBUG_MISC,
7261 "Using default monospace font.\n");
7262 }
7263
7264 /* Sanitise the font description.
7265 *
7266 * Gravity makes no sense in vte.
7267 * Style needs to be default, and weight needs to allow bolding,
7268 * since those are set via SGR attributes.
7269 *
7270 * Allowing weight <= medium does not absolutely guarantee that bold
7271 * actually is bolder than the specified weight, but should be good enough
7272 * until we can check that the actually loaded bold font is weightier than
7273 * the normal font (FIXME!).
7274 *
7275 * As a special exception, allow any weight if bold-is-bright is enabled.
7276 */
7277 pango_font_description_unset_fields(desc.get(),
7278 PangoFontMask(PANGO_FONT_MASK_GRAVITY |
7279 PANGO_FONT_MASK_STYLE));
7280 if ((pango_font_description_get_set_fields(desc.get()) & PANGO_FONT_MASK_WEIGHT) &&
7281 (pango_font_description_get_weight(desc.get()) > PANGO_WEIGHT_MEDIUM) &&
7282 !m_bold_is_bright) {
7283 pango_font_description_set_weight(desc.get(),
7284 std::min(pango_font_description_get_weight(desc.get()),
7285 PANGO_WEIGHT_MEDIUM));
7286 }
7287
7288 bool const same_desc = m_unscaled_font_desc &&
7289 pango_font_description_equal(m_unscaled_font_desc.get(), desc.get());
7290
7291 /* Note that we proceed to recreating the font even if the description
7292 * are the same. This is because maybe screen
7293 * font options were changed, or new fonts installed. Those will be
7294 * detected at font creation time and respected.
7295 */
7296
7297 m_unscaled_font_desc = std::move(desc);
7298 update_font();
7299
7300 return !same_desc;
7301 }
7302
7303 bool
set_font_scale(gdouble scale)7304 Terminal::set_font_scale(gdouble scale)
7305 {
7306 /* FIXME: compare old and new scale in pixel space */
7307 if (_vte_double_equal(scale, m_font_scale))
7308 return false;
7309
7310 m_font_scale = scale;
7311 update_font();
7312
7313 return true;
7314 }
7315
7316 bool
set_cell_width_scale(double scale)7317 Terminal::set_cell_width_scale(double scale)
7318 {
7319 /* FIXME: compare old and new scale in pixel space */
7320 if (_vte_double_equal(scale, m_cell_width_scale))
7321 return false;
7322
7323 m_cell_width_scale = scale;
7324 /* Set the drawing font. */
7325 m_fontdirty = true;
7326 if (widget_realized()) {
7327 ensure_font();
7328 }
7329
7330 return true;
7331 }
7332
7333 bool
set_cell_height_scale(double scale)7334 Terminal::set_cell_height_scale(double scale)
7335 {
7336 /* FIXME: compare old and new scale in pixel space */
7337 if (_vte_double_equal(scale, m_cell_height_scale))
7338 return false;
7339
7340 m_cell_height_scale = scale;
7341 /* Set the drawing font. */
7342 m_fontdirty = true;
7343 if (widget_realized()) {
7344 ensure_font();
7345 }
7346
7347 return true;
7348 }
7349
7350 /* Read and refresh our perception of the size of the PTY. */
7351 void
refresh_size()7352 Terminal::refresh_size()
7353 {
7354 if (!pty())
7355 return;
7356
7357 int rows, columns;
7358 if (!pty()->get_size(&rows, &columns)) {
7359 /* Error reading PTY size, use defaults */
7360 rows = VTE_ROWS;
7361 columns = VTE_COLUMNS;
7362 }
7363
7364 if (m_row_count == rows &&
7365 m_column_count == columns)
7366 return;
7367
7368 m_row_count = rows;
7369 m_column_count = columns;
7370 m_tabstops.resize(columns);
7371 }
7372
7373 /* Resize the given screen (normal or alternate) of the terminal. */
7374 void
screen_set_size(VteScreen * screen_,long old_columns,long old_rows,bool do_rewrap)7375 Terminal::screen_set_size(VteScreen *screen_,
7376 long old_columns,
7377 long old_rows,
7378 bool do_rewrap)
7379 {
7380 VteRing *ring = screen_->row_data;
7381 VteVisualPosition cursor_saved_absolute;
7382 VteVisualPosition below_viewport;
7383 VteVisualPosition below_current_paragraph;
7384 VteVisualPosition selection_start, selection_end;
7385 VteVisualPosition *markers[7];
7386 gboolean was_scrolled_to_top = ((long) ceil(screen_->scroll_delta) == _vte_ring_delta(ring));
7387 gboolean was_scrolled_to_bottom = ((long) screen_->scroll_delta == screen_->insert_delta);
7388 glong old_top_lines;
7389 double new_scroll_delta;
7390
7391 if (m_selection_block_mode && do_rewrap && old_columns != m_column_count)
7392 deselect_all();
7393
7394 _vte_debug_print(VTE_DEBUG_RESIZE,
7395 "Resizing %s screen_\n"
7396 "Old insert_delta=%ld scroll_delta=%f\n"
7397 " cursor (absolute) row=%ld col=%ld\n"
7398 " cursor_saved (relative to insert_delta) row=%ld col=%ld\n",
7399 screen_ == &m_normal_screen ? "normal" : "alternate",
7400 screen_->insert_delta, screen_->scroll_delta,
7401 screen_->cursor.row, screen_->cursor.col,
7402 screen_->saved.cursor.row, screen_->saved.cursor.col);
7403
7404 cursor_saved_absolute.row = screen_->saved.cursor.row + screen_->insert_delta;
7405 cursor_saved_absolute.col = screen_->saved.cursor.col;
7406 below_viewport.row = screen_->scroll_delta + old_rows;
7407 below_viewport.col = 0;
7408 below_current_paragraph.row = screen_->cursor.row + 1;
7409 while (below_current_paragraph.row < _vte_ring_next(ring)
7410 && _vte_ring_index(ring, below_current_paragraph.row - 1)->attr.soft_wrapped) {
7411 below_current_paragraph.row++;
7412 }
7413 below_current_paragraph.col = 0;
7414 memset(&markers, 0, sizeof(markers));
7415 markers[0] = &cursor_saved_absolute;
7416 markers[1] = &below_viewport;
7417 markers[2] = &below_current_paragraph;
7418 markers[3] = &screen_->cursor;
7419 if (!m_selection_resolved.empty()) {
7420 selection_start.row = m_selection_resolved.start_row();
7421 selection_start.col = m_selection_resolved.start_column();
7422 selection_end.row = m_selection_resolved.end_row();
7423 selection_end.col = m_selection_resolved.end_column();
7424 markers[4] = &selection_start;
7425 markers[5] = &selection_end;
7426 }
7427
7428 old_top_lines = below_current_paragraph.row - screen_->insert_delta;
7429
7430 if (do_rewrap && old_columns != m_column_count)
7431 _vte_ring_rewrap(ring, m_column_count, markers);
7432
7433 if (_vte_ring_length(ring) > m_row_count) {
7434 /* The content won't fit without scrollbars. Before figuring out the position, we might need to
7435 drop some lines from the ring if the cursor is not at the bottom, as XTerm does. See bug 708213.
7436 This code is really tricky, see ../doc/rewrap.txt for details! */
7437 glong new_top_lines, drop1, drop2, drop3, drop;
7438 screen_->insert_delta = _vte_ring_next(ring) - m_row_count;
7439 new_top_lines = below_current_paragraph.row - screen_->insert_delta;
7440 drop1 = _vte_ring_length(ring) - m_row_count;
7441 drop2 = _vte_ring_next(ring) - below_current_paragraph.row;
7442 drop3 = old_top_lines - new_top_lines;
7443 drop = MIN(MIN(drop1, drop2), drop3);
7444 if (drop > 0) {
7445 int new_ring_next = screen_->insert_delta + m_row_count - drop;
7446 _vte_debug_print(VTE_DEBUG_RESIZE,
7447 "Dropping %ld [== MIN(%ld, %ld, %ld)] rows at the bottom\n",
7448 drop, drop1, drop2, drop3);
7449 _vte_ring_shrink(ring, new_ring_next - _vte_ring_delta(ring));
7450 }
7451 }
7452
7453 if (!m_selection_resolved.empty()) {
7454 m_selection_resolved.set ({ selection_start.row, selection_start.col },
7455 { selection_end.row, selection_end.col });
7456 }
7457
7458 /* Figure out new insert and scroll deltas */
7459 if (_vte_ring_length(ring) <= m_row_count) {
7460 /* Everything fits without scrollbars. Align at top. */
7461 screen_->insert_delta = _vte_ring_delta(ring);
7462 new_scroll_delta = screen_->insert_delta;
7463 _vte_debug_print(VTE_DEBUG_RESIZE,
7464 "Everything fits without scrollbars\n");
7465 } else {
7466 /* Scrollbar required. Can't afford unused lines at bottom. */
7467 screen_->insert_delta = _vte_ring_next(ring) - m_row_count;
7468 if (was_scrolled_to_bottom) {
7469 /* Was scrolled to bottom, keep this way. */
7470 new_scroll_delta = screen_->insert_delta;
7471 _vte_debug_print(VTE_DEBUG_RESIZE,
7472 "Scroll to bottom\n");
7473 } else if (was_scrolled_to_top) {
7474 /* Was scrolled to top, keep this way. Not sure if this special case is worth it. */
7475 new_scroll_delta = _vte_ring_delta(ring);
7476 _vte_debug_print(VTE_DEBUG_RESIZE,
7477 "Scroll to top\n");
7478 } else {
7479 /* Try to scroll so that the bottom visible row stays.
7480 More precisely, the character below the bottom left corner stays in that
7481 (invisible) row.
7482 So if the bottom of the screen_ was at a hard line break then that hard
7483 line break will stay there.
7484 TODO: What would be the best behavior if the bottom of the screen_ is a
7485 soft line break, i.e. only a partial line is visible at the bottom? */
7486 new_scroll_delta = below_viewport.row - m_row_count;
7487 /* Keep the old fractional part. */
7488 new_scroll_delta += screen_->scroll_delta - floor(screen_->scroll_delta);
7489 _vte_debug_print(VTE_DEBUG_RESIZE,
7490 "Scroll so bottom row stays\n");
7491 }
7492 }
7493
7494 /* Don't clamp, they'll be clamped when restored. Until then remember off-screen_ values
7495 since they might become on-screen_ again on subsequent resizes. */
7496 screen_->saved.cursor.row = cursor_saved_absolute.row - screen_->insert_delta;
7497 screen_->saved.cursor.col = cursor_saved_absolute.col;
7498
7499 _vte_debug_print(VTE_DEBUG_RESIZE,
7500 "New insert_delta=%ld scroll_delta=%f\n"
7501 " cursor (absolute) row=%ld col=%ld\n"
7502 " cursor_saved (relative to insert_delta) row=%ld col=%ld\n\n",
7503 screen_->insert_delta, new_scroll_delta,
7504 screen_->cursor.row, screen_->cursor.col,
7505 screen_->saved.cursor.row, screen_->saved.cursor.col);
7506
7507 if (screen_ == m_screen)
7508 queue_adjustment_value_changed(new_scroll_delta);
7509 else
7510 screen_->scroll_delta = new_scroll_delta;
7511 }
7512
7513 void
set_size(long columns,long rows)7514 Terminal::set_size(long columns,
7515 long rows)
7516 {
7517 glong old_columns, old_rows;
7518
7519 _vte_debug_print(VTE_DEBUG_RESIZE,
7520 "Setting PTY size to %ldx%ld.\n",
7521 columns, rows);
7522
7523 old_rows = m_row_count;
7524 old_columns = m_column_count;
7525
7526 if (pty()) {
7527 /* Try to set the terminal size, and read it back,
7528 * in case something went awry.
7529 */
7530 if (!pty()->set_size(rows,
7531 columns,
7532 m_cell_height_unscaled,
7533 m_cell_width_unscaled)) {
7534 // nothing we can do here
7535 }
7536 refresh_size();
7537 } else {
7538 m_row_count = rows;
7539 m_column_count = columns;
7540 m_tabstops.resize(columns);
7541 }
7542 if (old_rows != m_row_count || old_columns != m_column_count) {
7543 m_scrolling_restricted = FALSE;
7544
7545 _vte_ring_set_visible_rows(m_normal_screen.row_data, m_row_count);
7546 _vte_ring_set_visible_rows(m_alternate_screen.row_data, m_row_count);
7547
7548 /* Resize the normal screen and (if rewrapping is enabled) rewrap it even if the alternate screen is visible: bug 415277 */
7549 screen_set_size(&m_normal_screen, old_columns, old_rows, m_rewrap_on_resize);
7550 /* Resize the alternate screen if it's the current one, but never rewrap it: bug 336238 comment 60 */
7551 if (m_screen == &m_alternate_screen)
7552 screen_set_size(&m_alternate_screen, old_columns, old_rows, false);
7553
7554 /* Ensure scrollback buffers cover the screen. */
7555 set_scrollback_lines(m_scrollback_lines);
7556
7557 /* Ensure the cursor is valid */
7558 m_screen->cursor.row = CLAMP (m_screen->cursor.row,
7559 _vte_ring_delta (m_screen->row_data),
7560 MAX (_vte_ring_delta (m_screen->row_data),
7561 _vte_ring_next (m_screen->row_data) - 1));
7562
7563 adjust_adjustments_full();
7564 gtk_widget_queue_resize_no_redraw(m_widget);
7565 /* Our visible text changed. */
7566 emit_text_modified();
7567 }
7568 }
7569
7570 /* Redraw the widget. */
7571 static void
vte_terminal_vadjustment_value_changed_cb(vte::terminal::Terminal * that)7572 vte_terminal_vadjustment_value_changed_cb(vte::terminal::Terminal* that) noexcept
7573 try
7574 {
7575 that->vadjustment_value_changed();
7576 }
7577 catch (...)
7578 {
7579 vte::log_exception();
7580 }
7581
7582 void
vadjustment_value_changed()7583 Terminal::vadjustment_value_changed()
7584 {
7585 /* Read the new adjustment value and save the difference. */
7586 auto const adj = gtk_adjustment_get_value(m_vadjustment.get());
7587 double dy = adj - m_screen->scroll_delta;
7588 m_screen->scroll_delta = adj;
7589
7590 /* Sanity checks. */
7591 if (G_UNLIKELY(!widget_realized()))
7592 return;
7593
7594 /* FIXME: do this check in pixel space */
7595 if (!_vte_double_equal(dy, 0)) {
7596 _vte_debug_print(VTE_DEBUG_ADJ,
7597 "Scrolling by %f\n", dy);
7598
7599 invalidate_all();
7600 match_contents_clear();
7601 emit_text_scrolled(dy);
7602 queue_contents_changed();
7603 } else {
7604 _vte_debug_print(VTE_DEBUG_ADJ, "Not scrolling\n");
7605 }
7606 }
7607
7608 void
widget_set_vadjustment(vte::glib::RefPtr<GtkAdjustment> && adjustment)7609 Terminal::widget_set_vadjustment(vte::glib::RefPtr<GtkAdjustment>&& adjustment)
7610 {
7611 if (adjustment && adjustment == m_vadjustment)
7612 return;
7613 if (!adjustment && m_vadjustment)
7614 return;
7615
7616 if (m_vadjustment) {
7617 /* Disconnect our signal handlers from this object. */
7618 g_signal_handlers_disconnect_by_func(m_vadjustment.get(),
7619 (void*)vte_terminal_vadjustment_value_changed_cb,
7620 this);
7621 }
7622
7623 if (adjustment)
7624 m_vadjustment = std::move(adjustment);
7625 else
7626 m_vadjustment = vte::glib::make_ref_sink(GTK_ADJUSTMENT(gtk_adjustment_new(0, 0, 0, 0, 0, 0)));
7627
7628 /* We care about the offset, not the top or bottom. */
7629 g_signal_connect_swapped(m_vadjustment.get(),
7630 "value-changed",
7631 G_CALLBACK(vte_terminal_vadjustment_value_changed_cb),
7632 this);
7633 }
7634
Terminal(vte::platform::Widget * w,VteTerminal * t)7635 Terminal::Terminal(vte::platform::Widget* w,
7636 VteTerminal *t) :
7637 m_real_widget(w),
7638 m_terminal(t),
7639 m_widget(&t->widget),
7640 m_normal_screen(VTE_SCROLLBACK_INIT, true),
7641 m_alternate_screen(VTE_ROWS, false),
7642 m_screen(&m_normal_screen)
7643 {
7644 widget_set_vadjustment({});
7645
7646 /* Inits allocation to 1x1 @ -1,-1 */
7647 cairo_rectangle_int_t allocation;
7648 gtk_widget_get_allocation(m_widget, &allocation);
7649 set_allocated_rect(allocation);
7650
7651 /* NOTE! We allocated zeroed memory, just fill in non-zero stuff. */
7652
7653 // FIXMEegmont make this store row indices only, maybe convert to a bitmap
7654 m_update_rects = g_array_sized_new(FALSE /* zero terminated */,
7655 FALSE /* clear */,
7656 sizeof(cairo_rectangle_int_t),
7657 32 /* preallocated size */);
7658
7659 /* Set up dummy metrics, value != 0 to avoid division by 0 */
7660 // FIXMEchpe this is wrong. These values must not be used before
7661 // the view has been set up, so if they are, that's a bug
7662 m_cell_width = 1;
7663 m_cell_height = 1;
7664 m_char_ascent = 1;
7665 m_char_descent = 1;
7666 m_char_padding = {0, 0, 0, 0};
7667 m_line_thickness = 1;
7668 m_underline_position = 1;
7669 m_double_underline_position = 1;
7670 m_undercurl_position = 1.;
7671 m_strikethrough_position = 1;
7672 m_overline_position = 1;
7673 m_regex_underline_position = 1;
7674
7675 reset_default_attributes(true);
7676
7677 /* Set up the desired palette. */
7678 set_colors_default();
7679 for (auto i = 0; i < VTE_PALETTE_SIZE; i++)
7680 m_palette[i].sources[VTE_COLOR_SOURCE_ESCAPE].is_set = FALSE;
7681
7682 /* Set up I/O encodings. */
7683 m_outgoing = _vte_byte_array_new();
7684
7685 /* Setting the terminal type and size requires the PTY master to
7686 * be set up properly first. */
7687 set_size(VTE_COLUMNS, VTE_ROWS);
7688
7689 /* Default is 0, forces update in vte_terminal_set_scrollback_lines */
7690 set_scrollback_lines(VTE_SCROLLBACK_INIT);
7691
7692 /* Initialize the saved cursor. */
7693 save_cursor(&m_normal_screen);
7694 save_cursor(&m_alternate_screen);
7695
7696 /* Matching data. */
7697 m_match_span.clear(); // FIXMEchpe unnecessary
7698 match_hilite_clear(); // FIXMEchpe unnecessary
7699
7700 /* Word chars */
7701 set_word_char_exceptions(WORD_CHAR_EXCEPTIONS_DEFAULT);
7702
7703 update_view_extents();
7704
7705 #ifdef VTE_DEBUG
7706 if (g_test_flags != 0) {
7707 feed("\e[1m\e[31mWARNING:\e[39m Test mode enabled. This is insecure!\e[0m\n\e[G"sv, false);
7708 }
7709 #endif
7710
7711 #ifndef WITH_GNUTLS
7712 std::string str{"\e[1m\e[31m"};
7713 str.append(_("WARNING"));
7714 str.append(":\e[39m ");
7715 str.append(_("GnuTLS not enabled; data will be written to disk unencrypted!"));
7716 str.append("\e[0m\n\e[G");
7717
7718 feed(str, false);
7719 #endif
7720 }
7721
7722 void
widget_get_preferred_width(int * minimum_width,int * natural_width)7723 Terminal::widget_get_preferred_width(int *minimum_width,
7724 int *natural_width)
7725 {
7726 _vte_debug_print(VTE_DEBUG_LIFECYCLE, "vte_terminal_get_preferred_width()\n");
7727
7728 ensure_font();
7729
7730 refresh_size();
7731
7732 *minimum_width = m_cell_width * 2; /* have room for a CJK or emoji */
7733 *natural_width = m_cell_width * m_column_count;
7734
7735 *minimum_width += m_padding.left +
7736 m_padding.right;
7737 *natural_width += m_padding.left +
7738 m_padding.right;
7739
7740 _vte_debug_print(VTE_DEBUG_WIDGET_SIZE,
7741 "[Terminal %p] minimum_width=%d, natural_width=%d for %ldx%ld cells (padding %d,%d;%d,%d).\n",
7742 m_terminal,
7743 *minimum_width, *natural_width,
7744 m_column_count,
7745 m_row_count,
7746 m_padding.left, m_padding.right, m_padding.top, m_padding.bottom);
7747 }
7748
7749 void
widget_get_preferred_height(int * minimum_height,int * natural_height)7750 Terminal::widget_get_preferred_height(int *minimum_height,
7751 int *natural_height)
7752 {
7753 _vte_debug_print(VTE_DEBUG_LIFECYCLE, "vte_terminal_get_preferred_height()\n");
7754
7755 ensure_font();
7756
7757 refresh_size();
7758
7759 *minimum_height = m_cell_height * 1;
7760 *natural_height = m_cell_height * m_row_count;
7761
7762 *minimum_height += m_padding.top +
7763 m_padding.bottom;
7764 *natural_height += m_padding.top +
7765 m_padding.bottom;
7766
7767 _vte_debug_print(VTE_DEBUG_WIDGET_SIZE,
7768 "[Terminal %p] minimum_height=%d, natural_height=%d for %ldx%ld cells (padding %d,%d;%d,%d).\n",
7769 m_terminal,
7770 *minimum_height, *natural_height,
7771 m_column_count,
7772 m_row_count,
7773 m_padding.left, m_padding.right, m_padding.top, m_padding.bottom);
7774 }
7775
7776 void
widget_size_allocate(GtkAllocation * allocation)7777 Terminal::widget_size_allocate(GtkAllocation *allocation)
7778 {
7779 glong width, height;
7780 gboolean repaint, update_scrollback;
7781
7782 _vte_debug_print(VTE_DEBUG_LIFECYCLE,
7783 "vte_terminal_size_allocate()\n");
7784
7785 width = (allocation->width - (m_padding.left + m_padding.right)) /
7786 m_cell_width;
7787 height = (allocation->height - (m_padding.top + m_padding.bottom)) /
7788 m_cell_height;
7789 width = MAX(width, 1);
7790 height = MAX(height, 1);
7791
7792 _vte_debug_print(VTE_DEBUG_WIDGET_SIZE,
7793 "[Terminal %p] Sizing window to %dx%d (%ldx%ld, padding %d,%d;%d,%d).\n",
7794 m_terminal,
7795 allocation->width, allocation->height,
7796 width, height,
7797 m_padding.left, m_padding.right, m_padding.top, m_padding.bottom);
7798
7799 auto current_allocation = get_allocated_rect();
7800
7801 repaint = current_allocation.width != allocation->width
7802 || current_allocation.height != allocation->height;
7803 update_scrollback = current_allocation.height != allocation->height;
7804
7805 /* Set our allocation to match the structure. */
7806 gtk_widget_set_allocation(m_widget, allocation);
7807 set_allocated_rect(*allocation);
7808
7809 if (width != m_column_count
7810 || height != m_row_count
7811 || update_scrollback)
7812 {
7813 /* Set the size of the pseudo-terminal. */
7814 set_size(width, height);
7815
7816 /* Notify viewers that the contents have changed. */
7817 queue_contents_changed();
7818 }
7819
7820 if (widget_realized()) {
7821 /* Force a repaint if we were resized. */
7822 if (repaint) {
7823 reset_update_rects();
7824 invalidate_all();
7825 }
7826 }
7827 }
7828
7829 void
widget_unmap()7830 Terminal::widget_unmap()
7831 {
7832 m_ringview.pause();
7833 }
7834
7835 void
widget_unrealize()7836 Terminal::widget_unrealize()
7837 {
7838 /* Deallocate the cursors. */
7839 m_mouse_cursor_over_widget = FALSE;
7840
7841 match_hilite_clear();
7842
7843 m_im_preedit_active = FALSE;
7844
7845 /* Drop font cache */
7846 m_draw.clear_font_cache();
7847 m_fontdirty = true;
7848
7849 /* Remove the cursor blink timeout function. */
7850 remove_cursor_timeout();
7851
7852 /* Remove the contents blink timeout function. */
7853 m_text_blink_timer.abort();
7854
7855 /* Cancel any pending redraws. */
7856 remove_update_timeout(this);
7857
7858 /* Cancel any pending signals */
7859 m_contents_changed_pending = FALSE;
7860 m_cursor_moved_pending = FALSE;
7861 m_text_modified_flag = FALSE;
7862 m_text_inserted_flag = FALSE;
7863 m_text_deleted_flag = FALSE;
7864
7865 /* Clear modifiers. */
7866 m_modifiers = 0;
7867
7868 /* Free any selected text, but if we currently own the selection,
7869 * throw the text onto the clipboard without an owner so that it
7870 * doesn't just disappear. */
7871 for (auto sel_type : {vte::platform::ClipboardType::CLIPBOARD,
7872 vte::platform::ClipboardType::PRIMARY}) {
7873 auto const sel = vte::to_integral(sel_type);
7874 if (m_selection[sel] != nullptr) {
7875 if (m_selection_owned[sel]) {
7876 // FIXMEchpe we should check m_selection_format[sel]
7877 // and also put text/html on if it's HTML format
7878 widget()->clipboard_set_text(sel_type,
7879 {m_selection[sel]->str,
7880 m_selection[sel]->len});
7881 }
7882 g_string_free(m_selection[sel], TRUE);
7883 m_selection[sel] = nullptr;
7884 }
7885 }
7886 }
7887
7888 void
set_blink_settings(bool blink,int blink_time,int blink_timeout)7889 Terminal::set_blink_settings(bool blink,
7890 int blink_time,
7891 int blink_timeout) noexcept
7892 {
7893 m_cursor_blink_cycle = blink_time / 2;
7894 m_cursor_blink_timeout = blink_timeout;
7895
7896 update_cursor_blinks();
7897
7898 /* Misuse gtk-cursor-blink-time for text blinking as well. This might change in the future. */
7899 m_text_blink_cycle = m_cursor_blink_cycle;
7900 if (m_text_blink_timer) {
7901 /* The current phase might have changed, and an already installed
7902 * timer to blink might fire too late. So remove the timer and
7903 * repaint the contents (which will install a correct new timer). */
7904 m_text_blink_timer.abort();
7905 invalidate_all();
7906 }
7907 }
7908
~Terminal()7909 Terminal::~Terminal()
7910 {
7911 /* Make sure not to change selection while in destruction. See issue vte#89. */
7912 m_changing_selection = true;
7913
7914 terminate_child();
7915 unset_pty(false /* don't notify widget */);
7916 remove_update_timeout(this);
7917
7918 /* Stop processing input. */
7919 stop_processing(this);
7920
7921 /* Free matching data. */
7922 if (m_match_attributes != NULL) {
7923 g_array_free(m_match_attributes, TRUE);
7924 }
7925 g_free(m_match_contents);
7926
7927 if (m_search_attrs)
7928 g_array_free (m_search_attrs, TRUE);
7929
7930 /* Disconnect from autoscroll requests. */
7931 stop_autoscroll();
7932
7933 /* Cancel pending adjustment change notifications. */
7934 m_adjustment_changed_pending = FALSE;
7935
7936 /* Stop listening for child-exited signals. */
7937 if (m_reaper) {
7938 g_signal_handlers_disconnect_by_func(m_reaper,
7939 (gpointer)reaper_child_exited_cb,
7940 this);
7941 g_object_unref(m_reaper);
7942 }
7943
7944 /* Discard any pending data. */
7945 _vte_byte_array_free(m_outgoing);
7946 m_outgoing = nullptr;
7947
7948 /* Free public-facing data. */
7949 if (m_vadjustment) {
7950 /* Disconnect our signal handlers from this object. */
7951 g_signal_handlers_disconnect_by_func(m_vadjustment.get(),
7952 (void*)vte_terminal_vadjustment_value_changed_cb,
7953 this);
7954 }
7955
7956 /* Update rects */
7957 g_array_free(m_update_rects, TRUE /* free segment */);
7958 }
7959
7960 void
widget_realize()7961 Terminal::widget_realize()
7962 {
7963 m_mouse_cursor_over_widget = FALSE; /* We'll receive an enter_notify_event if the window appears under the cursor. */
7964
7965 m_im_preedit_active = FALSE;
7966
7967 /* Clear modifiers. */
7968 m_modifiers = 0;
7969
7970 // Create the font cache
7971 ensure_font();
7972 }
7973
7974 // FIXMEchpe probably @attr should be passed by ref
7975 void
determine_colors(VteCellAttr const * attr,bool is_selected,bool is_cursor,guint * pfore,guint * pback,guint * pdeco) const7976 Terminal::determine_colors(VteCellAttr const* attr,
7977 bool is_selected,
7978 bool is_cursor,
7979 guint *pfore,
7980 guint *pback,
7981 guint *pdeco) const
7982 {
7983 guint fore, back, deco;
7984
7985 g_assert(attr);
7986
7987 /* Start with cell colors */
7988 vte_color_triple_get(attr->colors(), &fore, &back, &deco);
7989
7990 /* Reverse-mode switches default fore and back colors */
7991 if (G_UNLIKELY (m_modes_private.DEC_REVERSE_IMAGE())) {
7992 if (fore == VTE_DEFAULT_FG)
7993 fore = VTE_DEFAULT_BG;
7994 if (back == VTE_DEFAULT_BG)
7995 back = VTE_DEFAULT_FG;
7996 }
7997
7998 /* Handle bold by using set bold color or brightening */
7999 if (attr->bold()) {
8000 if (fore == VTE_DEFAULT_FG && get_color(VTE_BOLD_FG) != NULL) {
8001 fore = VTE_BOLD_FG;
8002 } else if (m_bold_is_bright &&
8003 fore >= VTE_LEGACY_COLORS_OFFSET &&
8004 fore < VTE_LEGACY_COLORS_OFFSET + VTE_LEGACY_COLOR_SET_SIZE) {
8005 fore += VTE_COLOR_BRIGHT_OFFSET;
8006 }
8007 }
8008
8009 /* Handle dim colors. Only apply to palette colors, dimming direct RGB wouldn't make sense.
8010 * Apply to the foreground color only, but do this before handling reverse/highlight so that
8011 * those can be used to dim the background instead. */
8012 if (attr->dim() && !(fore & VTE_RGB_COLOR_MASK(8, 8, 8))) {
8013 fore |= VTE_DIM_COLOR;
8014 }
8015
8016 /* Reverse cell? */
8017 if (attr->reverse()) {
8018 using std::swap;
8019 swap(fore, back);
8020 }
8021
8022 /* Selection: use hightlight back/fore, or inverse */
8023 if (is_selected) {
8024 /* XXX what if hightlight back is same color as current back? */
8025 bool do_swap = true;
8026 if (get_color(VTE_HIGHLIGHT_BG) != NULL) {
8027 back = VTE_HIGHLIGHT_BG;
8028 do_swap = false;
8029 }
8030 if (get_color(VTE_HIGHLIGHT_FG) != NULL) {
8031 fore = VTE_HIGHLIGHT_FG;
8032 do_swap = false;
8033 }
8034 if (do_swap) {
8035 using std::swap;
8036 swap(fore, back);
8037 }
8038 }
8039
8040 /* Cursor: use cursor back, or inverse */
8041 if (is_cursor) {
8042 /* XXX what if cursor back is same color as current back? */
8043 bool do_swap = true;
8044 if (get_color(VTE_CURSOR_BG) != NULL) {
8045 back = VTE_CURSOR_BG;
8046 do_swap = false;
8047 }
8048 if (get_color(VTE_CURSOR_FG) != NULL) {
8049 fore = VTE_CURSOR_FG;
8050 do_swap = false;
8051 }
8052 if (do_swap) {
8053 using std::swap;
8054 swap(fore, back);
8055 }
8056 }
8057
8058 /* Invisible? */
8059 /* FIXME: This is dead code, this is not where we actually handle invisibile.
8060 * Instead, draw_cells() is not called from draw_rows().
8061 * That is required for the foreground to be transparent if so is the background. */
8062 if (attr->invisible()) {
8063 fore = back;
8064 deco = VTE_DEFAULT_FG;
8065 }
8066
8067 *pfore = fore;
8068 *pback = back;
8069 *pdeco = deco;
8070 }
8071
8072 void
determine_colors(VteCell const * cell,bool highlight,guint * fore,guint * back,guint * deco) const8073 Terminal::determine_colors(VteCell const* cell,
8074 bool highlight,
8075 guint *fore,
8076 guint *back,
8077 guint *deco) const
8078 {
8079 determine_colors(cell ? &cell->attr : &basic_cell.attr,
8080 highlight, false /* not cursor */,
8081 fore, back, deco);
8082 }
8083
8084 void
determine_cursor_colors(VteCell const * cell,bool highlight,guint * fore,guint * back,guint * deco) const8085 Terminal::determine_cursor_colors(VteCell const* cell,
8086 bool highlight,
8087 guint *fore,
8088 guint *back,
8089 guint *deco) const
8090 {
8091 determine_colors(cell ? &cell->attr : &basic_cell.attr,
8092 highlight, true /* cursor */,
8093 fore, back, deco);
8094 }
8095
8096 void
resolve_normal_colors(VteCell const * cell,unsigned * pfore,unsigned * pback,vte::color::rgb & fg,vte::color::rgb & bg)8097 Terminal::resolve_normal_colors(VteCell const* cell,
8098 unsigned* pfore,
8099 unsigned* pback,
8100 vte::color::rgb& fg,
8101 vte::color::rgb& bg)
8102 {
8103 auto deco = unsigned{};
8104 determine_colors(cell, false, pfore, pback, &deco);
8105 rgb_from_index<8, 8, 8>(*pfore, fg);
8106 rgb_from_index<8, 8, 8>(*pback, bg);
8107 }
8108
8109 // FIXMEchpe this constantly removes and reschedules the timer. improve this!
8110 bool
text_blink_timer_callback()8111 Terminal::text_blink_timer_callback()
8112 {
8113 invalidate_all();
8114 return false; /* don't run again */
8115 }
8116
8117 /* Draw a string of characters with similar attributes. */
8118 void
draw_cells(vte::view::DrawingContext::TextRequest * items,gssize n,uint32_t fore,uint32_t back,uint32_t deco,bool clear,bool draw_default_bg,uint32_t attr,bool hyperlink,bool hilite,int column_width,int row_height)8119 Terminal::draw_cells(vte::view::DrawingContext::TextRequest* items,
8120 gssize n,
8121 uint32_t fore,
8122 uint32_t back,
8123 uint32_t deco,
8124 bool clear,
8125 bool draw_default_bg,
8126 uint32_t attr,
8127 bool hyperlink,
8128 bool hilite,
8129 int column_width,
8130 int row_height)
8131 {
8132 int i, xl, xr, y;
8133 gint columns = 0;
8134 vte::color::rgb fg, bg, dc;
8135
8136 g_assert(n > 0);
8137 #if 0
8138 _VTE_DEBUG_IF(VTE_DEBUG_CELLS) {
8139 GString *str = g_string_new (NULL);
8140 gchar *tmp;
8141 for (i = 0; i < n; i++) {
8142 g_string_append_unichar (str, items[i].c);
8143 }
8144 tmp = g_string_free (str, FALSE);
8145 g_printerr ("draw_cells('%s', fore=%d, back=%d, deco=%d, bold=%d,"
8146 " ul=%d, strike=%d, ol=%d, blink=%d,"
8147 " hyperlink=%d, hilite=%d, boxed=%d)\n",
8148 tmp, fore, back, deco, bold,
8149 underline, strikethrough, overline, blink,
8150 hyperlink, hilite, boxed);
8151 g_free (tmp);
8152 }
8153 #endif
8154
8155 rgb_from_index<8, 8, 8>(fore, fg);
8156 rgb_from_index<8, 8, 8>(back, bg);
8157 // FIXMEchpe defer resolving deco color until we actually need to draw an underline?
8158 if (deco == VTE_DEFAULT_FG)
8159 dc = fg;
8160 else
8161 rgb_from_index<4, 5, 4>(deco, dc);
8162
8163 if (clear && (draw_default_bg || back != VTE_DEFAULT_BG)) {
8164 /* Paint the background. */
8165 i = 0;
8166 while (i < n) {
8167 xl = items[i].x;
8168 xr = items[i].x + items[i].columns * column_width;
8169 y = items[i].y;
8170 /* Items are not necessarily contiguous in LTR order.
8171 * Combine as long as they form a single visual run. */
8172 for (i++; i < n && items[i].y == y; i++) {
8173 if (G_LIKELY (items[i].x == xr)) {
8174 xr += items[i].columns * column_width; /* extend to the right */
8175 } else if (items[i].x + items[i].columns * column_width == xl) {
8176 xl = items[i].x; /* extend to the left */
8177 } else {
8178 break; /* break the run */
8179 }
8180 }
8181
8182 m_draw.fill_rectangle(
8183 xl,
8184 y,
8185 xr - xl, row_height,
8186 &bg, VTE_DRAW_OPAQUE);
8187 }
8188 }
8189
8190 if (attr & VTE_ATTR_BLINK) {
8191 /* Notify the caller that cells with the "blink" attribute were encountered (regardless of
8192 * whether they're actually painted or skipped now), so that the caller can set up a timer
8193 * to make them blink if it wishes to. */
8194 m_text_to_blink = true;
8195
8196 /* This is for the "off" state of blinking text. Invisible text could also be handled here,
8197 * but it's not, it's handled outside by not even calling this method.
8198 * Setting fg = bg and painting the text would not work for two reasons: it'd be opaque
8199 * even if the background is translucent, and this method can be called with a continuous
8200 * run of identical fg, yet different bg colored cells. So we simply bail out. */
8201 if (!m_text_blink_state)
8202 return;
8203 }
8204
8205 /* Draw whatever SFX are required. Do this before drawing the letters,
8206 * so that if the descent of a letter crosses an underline of a different color,
8207 * it's the letter's color that wins. Other kinds of decorations always have the
8208 * same color as the text, so the order is irrelevant there. */
8209 if ((attr & (VTE_ATTR_UNDERLINE_MASK |
8210 VTE_ATTR_STRIKETHROUGH_MASK |
8211 VTE_ATTR_OVERLINE_MASK |
8212 VTE_ATTR_BOXED_MASK)) |
8213 hyperlink | hilite) {
8214 i = 0;
8215 while (i < n) {
8216 xl = items[i].x;
8217 xr = items[i].x + items[i].columns * column_width;
8218 columns = items[i].columns;
8219 y = items[i].y;
8220 /* Items are not necessarily contiguous in LTR order.
8221 * Combine as long as they form a single visual run. */
8222 for (i++; i < n && items[i].y == y; i++) {
8223 if (G_LIKELY (items[i].x == xr)) {
8224 xr += items[i].columns * column_width; /* extend to the right */
8225 columns += items[i].columns;
8226 } else if (items[i].x + items[i].columns * column_width == xl) {
8227 xl = items[i].x; /* extend to the left */
8228 columns += items[i].columns;
8229 } else {
8230 break; /* break the run */
8231 }
8232 }
8233 switch (vte_attr_get_value(attr, VTE_ATTR_UNDERLINE_VALUE_MASK, VTE_ATTR_UNDERLINE_SHIFT)) {
8234 case 1:
8235 m_draw.draw_line(
8236 xl,
8237 y + m_underline_position,
8238 xr - 1,
8239 y + m_underline_position + m_underline_thickness - 1,
8240 VTE_LINE_WIDTH,
8241 &dc, VTE_DRAW_OPAQUE);
8242 break;
8243 case 2:
8244 m_draw.draw_line(
8245 xl,
8246 y + m_double_underline_position,
8247 xr - 1,
8248 y + m_double_underline_position + m_double_underline_thickness - 1,
8249 VTE_LINE_WIDTH,
8250 &dc, VTE_DRAW_OPAQUE);
8251 m_draw.draw_line(
8252 xl,
8253 y + m_double_underline_position + 2 * m_double_underline_thickness,
8254 xr - 1,
8255 y + m_double_underline_position + 3 * m_double_underline_thickness - 1,
8256 VTE_LINE_WIDTH,
8257 &dc, VTE_DRAW_OPAQUE);
8258 break;
8259 case 3:
8260 m_draw.draw_undercurl(
8261 xl,
8262 y + m_undercurl_position,
8263 m_undercurl_thickness,
8264 columns,
8265 &dc, VTE_DRAW_OPAQUE);
8266 break;
8267 }
8268 if (attr & VTE_ATTR_STRIKETHROUGH) {
8269 m_draw.draw_line(
8270 xl,
8271 y + m_strikethrough_position,
8272 xr - 1,
8273 y + m_strikethrough_position + m_strikethrough_thickness - 1,
8274 VTE_LINE_WIDTH,
8275 &fg, VTE_DRAW_OPAQUE);
8276 }
8277 if (attr & VTE_ATTR_OVERLINE) {
8278 m_draw.draw_line(
8279 xl,
8280 y + m_overline_position,
8281 xr - 1,
8282 y + m_overline_position + m_overline_thickness - 1,
8283 VTE_LINE_WIDTH,
8284 &fg, VTE_DRAW_OPAQUE);
8285 }
8286 if (hilite) {
8287 m_draw.draw_line(
8288 xl,
8289 y + m_regex_underline_position,
8290 xr - 1,
8291 y + m_regex_underline_position + m_regex_underline_thickness - 1,
8292 VTE_LINE_WIDTH,
8293 &fg, VTE_DRAW_OPAQUE);
8294 } else if (hyperlink) {
8295 for (double j = 1.0 / 6.0; j < columns; j += 0.5) {
8296 m_draw.fill_rectangle(
8297 xl + j * column_width,
8298 y + m_regex_underline_position,
8299 MAX(column_width / 6.0, 1.0),
8300 m_regex_underline_thickness,
8301 &fg, VTE_DRAW_OPAQUE);
8302 }
8303 }
8304 if (attr & VTE_ATTR_BOXED) {
8305 m_draw.draw_rectangle(
8306 xl,
8307 y,
8308 xr - xl,
8309 row_height,
8310 &fg, VTE_DRAW_OPAQUE);
8311 }
8312 }
8313 }
8314
8315 m_draw.draw_text(
8316 items, n,
8317 attr,
8318 &fg, VTE_DRAW_OPAQUE);
8319 }
8320
8321 /* FIXME: we don't have a way to tell GTK+ what the default text attributes
8322 * should be, so for now at least it's assuming white-on-black is the norm and
8323 * is using "black-on-white" to signify "inverse". Pick up on that state and
8324 * fix things. Do this here, so that if we suddenly get red-on-black, we'll do
8325 * the right thing. */
8326 void
fudge_pango_colors(GSList * attributes,VteCell * cells,gsize n)8327 Terminal::fudge_pango_colors(GSList *attributes,
8328 VteCell *cells,
8329 gsize n)
8330 {
8331 gsize i, sumlen = 0;
8332 struct _fudge_cell_props{
8333 gboolean saw_fg, saw_bg;
8334 vte::color::rgb fg, bg;
8335 guint index;
8336 }*props = g_newa (struct _fudge_cell_props, n);
8337
8338 for (i = 0; i < n; i++) {
8339 gchar ubuf[7];
8340 gint len = g_unichar_to_utf8 (cells[i].c, ubuf);
8341 props[i].index = sumlen;
8342 props[i].saw_fg = props[i].saw_bg = FALSE;
8343 sumlen += len;
8344 }
8345
8346 while (attributes != NULL) {
8347 PangoAttribute *attr = (PangoAttribute *)attributes->data;
8348 PangoAttrColor *color;
8349 switch (attr->klass->type) {
8350 case PANGO_ATTR_FOREGROUND:
8351 for (i = 0; i < n; i++) {
8352 if (props[i].index < attr->start_index) {
8353 continue;
8354 }
8355 if (props[i].index >= attr->end_index) {
8356 break;
8357 }
8358 props[i].saw_fg = TRUE;
8359 color = (PangoAttrColor*) attr;
8360 props[i].fg = color->color;
8361 }
8362 break;
8363 case PANGO_ATTR_BACKGROUND:
8364 for (i = 0; i < n; i++) {
8365 if (props[i].index < attr->start_index) {
8366 continue;
8367 }
8368 if (props[i].index >= attr->end_index) {
8369 break;
8370 }
8371 props[i].saw_bg = TRUE;
8372 color = (PangoAttrColor*) attr;
8373 props[i].bg = color->color;
8374 }
8375 break;
8376 default:
8377 break;
8378 }
8379 attributes = g_slist_next(attributes);
8380 }
8381
8382 for (i = 0; i < n; i++) {
8383 if (props[i].saw_fg && props[i].saw_bg &&
8384 (props[i].fg.red == 0xffff) &&
8385 (props[i].fg.green == 0xffff) &&
8386 (props[i].fg.blue == 0xffff) &&
8387 (props[i].bg.red == 0) &&
8388 (props[i].bg.green == 0) &&
8389 (props[i].bg.blue == 0)) {
8390 cells[i].attr.copy_colors(m_color_defaults.attr);
8391 cells[i].attr.set_reverse(true);
8392 }
8393 }
8394 }
8395
8396 /* Apply the attribute given in the PangoAttribute to the list of cells. */
8397 void
apply_pango_attr(PangoAttribute * attr,VteCell * cells,gsize n_cells)8398 Terminal::apply_pango_attr(PangoAttribute *attr,
8399 VteCell *cells,
8400 gsize n_cells)
8401 {
8402 guint i, ival;
8403 PangoAttrInt *attrint;
8404 PangoAttrColor *attrcolor;
8405
8406 switch (attr->klass->type) {
8407 case PANGO_ATTR_FOREGROUND:
8408 case PANGO_ATTR_BACKGROUND:
8409 attrcolor = (PangoAttrColor*) attr;
8410 ival = VTE_RGB_COLOR(8, 8, 8,
8411 ((attrcolor->color.red & 0xFF00) >> 8),
8412 ((attrcolor->color.green & 0xFF00) >> 8),
8413 ((attrcolor->color.blue & 0xFF00) >> 8));
8414 for (i = attr->start_index;
8415 i < attr->end_index && i < n_cells;
8416 i++) {
8417 if (attr->klass->type == PANGO_ATTR_FOREGROUND) {
8418 cells[i].attr.set_fore(ival);
8419 }
8420 if (attr->klass->type == PANGO_ATTR_BACKGROUND) {
8421 cells[i].attr.set_back(ival);
8422 }
8423 }
8424 break;
8425 case PANGO_ATTR_UNDERLINE_COLOR:
8426 attrcolor = (PangoAttrColor*) attr;
8427 ival = VTE_RGB_COLOR(4, 5, 4,
8428 ((attrcolor->color.red & 0xFF00) >> 8),
8429 ((attrcolor->color.green & 0xFF00) >> 8),
8430 ((attrcolor->color.blue & 0xFF00) >> 8));
8431 for (i = attr->start_index;
8432 i < attr->end_index && i < n_cells;
8433 i++) {
8434 if (attr->klass->type == PANGO_ATTR_UNDERLINE) {
8435 cells[i].attr.set_deco(ival);
8436 }
8437 }
8438 break;
8439 case PANGO_ATTR_STRIKETHROUGH:
8440 attrint = (PangoAttrInt*) attr;
8441 ival = attrint->value;
8442 for (i = attr->start_index;
8443 (i < attr->end_index) && (i < n_cells);
8444 i++) {
8445 cells[i].attr.set_strikethrough(ival != FALSE);
8446 }
8447 break;
8448 case PANGO_ATTR_UNDERLINE:
8449 attrint = (PangoAttrInt*) attr;
8450 ival = attrint->value;
8451 for (i = attr->start_index;
8452 (i < attr->end_index) && (i < n_cells);
8453 i++) {
8454 unsigned int underline = 0;
8455 switch (ival) {
8456 case PANGO_UNDERLINE_SINGLE:
8457 underline = 1;
8458 break;
8459 case PANGO_UNDERLINE_DOUBLE:
8460 underline = 2;
8461 break;
8462 case PANGO_UNDERLINE_ERROR:
8463 underline = 3; /* wavy */
8464 break;
8465 case PANGO_UNDERLINE_NONE:
8466 case PANGO_UNDERLINE_LOW: /* FIXME */
8467 underline = 0;
8468 break;
8469 }
8470 cells[i].attr.set_underline(underline);
8471 }
8472 break;
8473 case PANGO_ATTR_WEIGHT:
8474 attrint = (PangoAttrInt*) attr;
8475 ival = attrint->value;
8476 for (i = attr->start_index;
8477 (i < attr->end_index) && (i < n_cells);
8478 i++) {
8479 cells[i].attr.set_bold(ival >= PANGO_WEIGHT_BOLD);
8480 }
8481 break;
8482 case PANGO_ATTR_STYLE:
8483 attrint = (PangoAttrInt*) attr;
8484 ival = attrint->value;
8485 for (i = attr->start_index;
8486 (i < attr->end_index) && (i < n_cells);
8487 i++) {
8488 cells[i].attr.set_italic(ival != PANGO_STYLE_NORMAL);
8489 }
8490 break;
8491 default:
8492 break;
8493 }
8494 }
8495
8496 /* Convert a PangoAttrList and a location in that list to settings in a
8497 * charcell structure. The cells array is assumed to contain enough items
8498 * so that all ranges in the attribute list can be mapped into the array, which
8499 * typically means that the cell array should have the same length as the
8500 * string (byte-wise) which the attributes describe. */
8501 void
translate_pango_cells(PangoAttrList * attrs,VteCell * cells,gsize n_cells)8502 Terminal::translate_pango_cells(PangoAttrList *attrs,
8503 VteCell *cells,
8504 gsize n_cells)
8505 {
8506 PangoAttribute *attr;
8507 PangoAttrIterator *attriter;
8508 GSList *list, *listiter;
8509 guint i;
8510
8511 for (i = 0; i < n_cells; i++) {
8512 cells[i] = m_color_defaults;
8513 }
8514
8515 attriter = pango_attr_list_get_iterator(attrs);
8516 if (attriter != NULL) {
8517 do {
8518 list = pango_attr_iterator_get_attrs(attriter);
8519 if (list != NULL) {
8520 for (listiter = list;
8521 listiter != NULL;
8522 listiter = g_slist_next(listiter)) {
8523 attr = (PangoAttribute *)listiter->data;
8524 apply_pango_attr(attr, cells, n_cells);
8525 }
8526 attr = (PangoAttribute *)list->data;
8527 fudge_pango_colors(
8528 list,
8529 cells +
8530 attr->start_index,
8531 MIN(n_cells, attr->end_index) -
8532 attr->start_index);
8533 g_slist_free_full(list, (GDestroyNotify)pango_attribute_destroy);
8534 }
8535 } while (pango_attr_iterator_next(attriter) == TRUE);
8536 pango_attr_iterator_destroy(attriter);
8537 }
8538 }
8539
8540 /* Draw the listed items using the given attributes. Tricky because the
8541 * attribute string is indexed by byte in the UTF-8 representation of the string
8542 * of characters. Because we draw a character at a time, this is slower. */
8543 void
draw_cells_with_attributes(vte::view::DrawingContext::TextRequest * items,gssize n,PangoAttrList * attrs,bool draw_default_bg,gint column_width,gint height)8544 Terminal::draw_cells_with_attributes(vte::view::DrawingContext::TextRequest* items,
8545 gssize n,
8546 PangoAttrList *attrs,
8547 bool draw_default_bg,
8548 gint column_width,
8549 gint height)
8550 {
8551 int i, j, cell_count;
8552 VteCell *cells;
8553 char scratch_buf[VTE_UTF8_BPC];
8554 guint fore, back, deco;
8555
8556 /* Note: since this function is only called with the pre-edit text,
8557 * all the items contain gunichar only, not vteunistr. */
8558 // FIXMEchpe is that really true for all input methods?
8559
8560 uint32_t const attr_mask = m_allow_bold ? ~0 : ~VTE_ATTR_BOLD_MASK;
8561
8562 for (i = 0, cell_count = 0; i < n; i++) {
8563 cell_count += g_unichar_to_utf8(items[i].c, scratch_buf);
8564 }
8565 cells = g_new(VteCell, cell_count);
8566 translate_pango_cells(attrs, cells, cell_count);
8567 for (i = 0, j = 0; i < n; i++) {
8568 determine_colors(&cells[j], false, &fore, &back, &deco);
8569 draw_cells(items + i, 1,
8570 fore,
8571 back,
8572 deco,
8573 TRUE, draw_default_bg,
8574 cells[j].attr.attr & attr_mask,
8575 m_allow_hyperlink && cells[j].attr.hyperlink_idx != 0,
8576 FALSE, column_width, height);
8577 j += g_unichar_to_utf8(items[i].c, scratch_buf);
8578 }
8579 g_free(cells);
8580 }
8581
8582 void
ringview_update()8583 Terminal::ringview_update()
8584 {
8585 auto first_row = first_displayed_row();
8586 auto last_row = last_displayed_row();
8587 if (cursor_is_onscreen())
8588 last_row = std::max(last_row, m_screen->cursor.row);
8589
8590 m_ringview.set_ring (m_screen->row_data);
8591 m_ringview.set_rows (first_row, last_row - first_row + 1);
8592 m_ringview.set_width (m_column_count);
8593 m_ringview.set_enable_bidi (m_enable_bidi);
8594 m_ringview.set_enable_shaping (m_enable_shaping);
8595 m_ringview.update ();
8596 }
8597
8598 /* Paint the contents of a given row at the given location. Take advantage
8599 * of multiple-draw APIs by finding runs of characters with identical
8600 * attributes and bundling them together. */
8601 void
draw_rows(VteScreen * screen_,cairo_region_t const * region,vte::grid::row_t start_row,vte::grid::row_t end_row,gint start_y,gint column_width,gint row_height)8602 Terminal::draw_rows(VteScreen *screen_,
8603 cairo_region_t const* region,
8604 vte::grid::row_t start_row,
8605 vte::grid::row_t end_row,
8606 gint start_y, /* must be the start of a row */
8607 gint column_width,
8608 gint row_height)
8609 {
8610 vte::grid::row_t row;
8611 vte::grid::column_t i, j, lcol, vcol;
8612 int y;
8613 guint fore = VTE_DEFAULT_FG, nfore, back = VTE_DEFAULT_BG, nback, deco = VTE_DEFAULT_FG, ndeco;
8614 gboolean hyperlink = FALSE, nhyperlink; /* non-hovered explicit hyperlink, needs dashed underlining */
8615 gboolean hilite = FALSE, nhilite; /* hovered explicit hyperlink or regex match, needs continuous underlining */
8616 gboolean selected;
8617 gboolean nrtl = FALSE, rtl; /* for debugging */
8618 uint32_t attr = 0, nattr;
8619 guint item_count;
8620 const VteCell *cell;
8621 VteRowData const* row_data;
8622 vte::base::BidiRow const* bidirow;
8623
8624 auto const column_count = m_column_count;
8625 uint32_t const attr_mask = m_allow_bold ? ~0 : ~VTE_ATTR_BOLD_MASK;
8626
8627 /* Need to ensure the ringview is updated. */
8628 ringview_update();
8629
8630 auto items = g_newa(vte::view::DrawingContext::TextRequest, column_count);
8631
8632 /* Paint the background.
8633 * Do it first for all the cells we're about to paint, before drawing the glyphs,
8634 * so that overflowing bits of a glyph (to the right or downwards) won't be
8635 * chopped off by another cell's background, not even across changes of the
8636 * background or any other attribute.
8637 * Process each row independently. */
8638 int const rect_width = get_allocated_width();
8639
8640 /* The rect contains the area of the row, and is moved row-wise in the loop. */
8641 auto rect = cairo_rectangle_int_t{-m_padding.left, start_y, rect_width, row_height};
8642 for (row = start_row, y = start_y;
8643 row < end_row;
8644 row++, y += row_height, rect.y = y /* same as rect.y += row_height */) {
8645 /* Check whether we need to draw this row at all */
8646 if (cairo_region_contains_rectangle(region, &rect) == CAIRO_REGION_OVERLAP_OUT)
8647 continue;
8648
8649 row_data = find_row_data(row);
8650 bidirow = m_ringview.get_bidirow(row);
8651
8652 _VTE_DEBUG_IF (VTE_DEBUG_BIDI) {
8653 /* Debug: Highlight the paddings of RTL rows with a slightly different background. */
8654 if (bidirow->base_is_rtl()) {
8655 vte::color::rgb bg;
8656 rgb_from_index<8, 8, 8>(VTE_DEFAULT_BG, bg);
8657 /* Go halfway towards #C0C0C0. */
8658 bg.red = (bg.red + 0xC000) / 2;
8659 bg.green = (bg.green + 0xC000) / 2;
8660 bg.blue = (bg.blue + 0xC000) / 2;
8661 m_draw.fill_rectangle(
8662 -m_padding.left,
8663 y,
8664 m_padding.left,
8665 row_height,
8666 &bg, VTE_DRAW_OPAQUE);
8667 m_draw.fill_rectangle(
8668 column_count * column_width,
8669 y,
8670 rect_width - m_padding.left - column_count * column_width,
8671 row_height,
8672 &bg, VTE_DRAW_OPAQUE);
8673 }
8674 }
8675
8676 i = j = 0;
8677 /* Walk the line.
8678 * Locate runs of identical bg colors within a row, and paint each run as a single rectangle. */
8679 do {
8680 /* Get the first cell's contents. */
8681 cell = row_data ? _vte_row_data_get (row_data, bidirow->vis2log(i)) : nullptr;
8682 /* Find the colors for this cell. */
8683 selected = cell_is_selected_vis(i, row);
8684 determine_colors(cell, selected, &fore, &back, &deco);
8685 rtl = bidirow->vis_is_rtl(i);
8686
8687 while (++j < column_count) {
8688 /* Retrieve the next cell. */
8689 cell = row_data ? _vte_row_data_get (row_data, bidirow->vis2log(j)) : nullptr;
8690 /* Resolve attributes to colors where possible and
8691 * compare visual attributes to the first character
8692 * in this chunk. */
8693 selected = cell_is_selected_vis(j, row);
8694 determine_colors(cell, selected, &nfore, &nback, &ndeco);
8695 nrtl = bidirow->vis_is_rtl(j);
8696 if (nback != back || (_vte_debug_on (VTE_DEBUG_BIDI) && nrtl != rtl)) {
8697 break;
8698 }
8699 }
8700 if (back != VTE_DEFAULT_BG) {
8701 vte::color::rgb bg;
8702 rgb_from_index<8, 8, 8>(back, bg);
8703 m_draw.fill_rectangle(
8704 i * column_width,
8705 y,
8706 (j - i) * column_width,
8707 row_height,
8708 &bg, VTE_DRAW_OPAQUE);
8709 }
8710
8711 _VTE_DEBUG_IF (VTE_DEBUG_BIDI) {
8712 /* Debug: Highlight RTL letters and RTL rows with a slightly different background. */
8713 vte::color::rgb bg;
8714 rgb_from_index<8, 8, 8>(back, bg);
8715 /* Go halfway towards #C0C0C0. */
8716 bg.red = (bg.red + 0xC000) / 2;
8717 bg.green = (bg.green + 0xC000) / 2;
8718 bg.blue = (bg.blue + 0xC000) / 2;
8719 int y1 = y + round(row_height / 8.);
8720 int y2 = y + row_height - round(row_height / 8.);
8721 /* Paint the top and bottom eighth of the cell with this more gray background
8722 * if the paragraph has a resolved RTL base direction. */
8723 if (bidirow->base_is_rtl()) {
8724 m_draw.fill_rectangle(
8725 i * column_width,
8726 y,
8727 (j - i) * column_width,
8728 y1 - y,
8729 &bg, VTE_DRAW_OPAQUE);
8730 m_draw.fill_rectangle(
8731 i * column_width,
8732 y2,
8733 (j - i) * column_width,
8734 y + row_height - y2,
8735 &bg, VTE_DRAW_OPAQUE);
8736 }
8737 /* Paint the middle three quarters of the cell with this more gray background
8738 * if the current character has a resolved RTL direction. */
8739 if (rtl) {
8740 m_draw.fill_rectangle(
8741 i * column_width,
8742 y1,
8743 (j - i) * column_width,
8744 y2 - y1,
8745 &bg, VTE_DRAW_OPAQUE);
8746 }
8747 }
8748
8749 /* We'll need to continue at the first cell which didn't
8750 * match the first one in this set. */
8751 i = j;
8752 } while (i < column_count);
8753 }
8754
8755
8756 /* Render the text.
8757 * The rect contains the area of the row (enlarged a bit at the top and bottom
8758 * to allow the text to overdraw a bit), and is moved row-wise in the loop.
8759 */
8760 rect = cairo_rectangle_int_t{-m_padding.left,
8761 start_y - cell_overflow_top(),
8762 rect_width,
8763 row_height + cell_overflow_top() + cell_overflow_bottom()};
8764
8765 for (row = start_row, y = start_y;
8766 row < end_row;
8767 row++, y += row_height, rect.y += row_height) {
8768 /* Check whether we need to draw this row at all */
8769 if (cairo_region_contains_rectangle(region, &rect) == CAIRO_REGION_OVERLAP_OUT)
8770 continue;
8771
8772 row_data = find_row_data(row);
8773 if (row_data == NULL)
8774 continue; /* Skip row. */
8775
8776 /* Ensure that drawing is restricted to the cell (plus the overdraw area) */
8777 _vte_draw_autoclip_t clipper{m_draw, &rect};
8778
8779 bidirow = m_ringview.get_bidirow(row);
8780
8781 /* Walk the line in logical order.
8782 * Locate runs of identical attributes within a row, and draw each run using a single draw_cells() call. */
8783 item_count = 0;
8784 // FIXME No need for the "< column_count" safety cap once bug 135 is addressed.
8785 for (lcol = 0; lcol < row_data->len && lcol < column_count; ) {
8786 vcol = bidirow->log2vis(lcol);
8787
8788 /* Get the character cell's contents. */
8789 cell = _vte_row_data_get (row_data, lcol);
8790 g_assert(cell != nullptr);
8791
8792 nhyperlink = (m_allow_hyperlink && cell->attr.hyperlink_idx != 0);
8793 nhilite = (nhyperlink && cell->attr.hyperlink_idx == m_hyperlink_hover_idx) ||
8794 (!nhyperlink && regex_match_has_current() && m_match_span.contains(row, lcol));
8795 if (cell->c == 0 ||
8796 ((cell->c == ' ' || cell->c == '\t') && // FIXME '\t' is newly added now, double check
8797 cell->attr.has_none(VTE_ATTR_UNDERLINE_MASK |
8798 VTE_ATTR_STRIKETHROUGH_MASK |
8799 VTE_ATTR_OVERLINE_MASK) &&
8800 !nhyperlink &&
8801 !nhilite) ||
8802 cell->attr.fragment() ||
8803 cell->attr.invisible()) {
8804 /* Skip empty or fragment cell, but erase on ' ' and '\t', since
8805 * it may be overwriting an image. */
8806 lcol++;
8807 continue;
8808 }
8809
8810 /* Find the colors for this cell. */
8811 nattr = cell->attr.attr;
8812 selected = cell_is_selected_log(lcol, row);
8813 determine_colors(cell, selected, &nfore, &nback, &ndeco);
8814
8815 /* See if it no longer fits the run. */
8816 if (item_count > 0 &&
8817 (((attr ^ nattr) & (VTE_ATTR_BOLD_MASK |
8818 VTE_ATTR_ITALIC_MASK |
8819 VTE_ATTR_UNDERLINE_MASK |
8820 VTE_ATTR_STRIKETHROUGH_MASK |
8821 VTE_ATTR_OVERLINE_MASK |
8822 VTE_ATTR_BLINK_MASK |
8823 VTE_ATTR_INVISIBLE_MASK)) || // FIXME or just simply "attr != nattr"?
8824 fore != nfore ||
8825 back != nback ||
8826 deco != ndeco ||
8827 hyperlink != nhyperlink ||
8828 hilite != nhilite)) {
8829 /* Draw the completed run of cells and start a new one. */
8830 draw_cells(items, item_count,
8831 fore, back, deco, FALSE, FALSE,
8832 attr & attr_mask,
8833 hyperlink, hilite,
8834 column_width, row_height);
8835 item_count = 0;
8836 }
8837
8838 /* Combine with subsequent spacing marks. */
8839 vteunistr c = cell->c;
8840 j = lcol + cell->attr.columns();
8841 if (G_UNLIKELY (lcol == 0 && g_unichar_ismark (_vte_unistr_get_base (cell->c)))) {
8842 /* A rare special case: the first cell contains a spacing mark.
8843 * Place on top of a NBSP, along with additional spacing marks if any,
8844 * and display beginning at offscreen column -1.
8845 * Additional spacing marks, if any, will be combined by the loop below. */
8846 c = _vte_unistr_append_unistr (0x00A0, cell->c);
8847 lcol = -1;
8848 }
8849 // FIXME No need for the "< column_count" safety cap once bug 135 is addressed.
8850 while (j < row_data->len && j < column_count) {
8851 /* Combine with subsequent spacing marks. */
8852 cell = _vte_row_data_get (row_data, j);
8853 if (cell && !cell->attr.fragment() && g_unichar_ismark (_vte_unistr_get_base (cell->c))) {
8854 c = _vte_unistr_append_unistr (c, cell->c);
8855 j += cell->attr.columns();
8856 } else {
8857 break;
8858 }
8859 }
8860
8861 attr = nattr;
8862 fore = nfore;
8863 back = nback;
8864 deco = ndeco;
8865 hyperlink = nhyperlink;
8866 hilite = nhilite;
8867
8868 g_assert_cmpint (item_count, <, column_count);
8869 items[item_count].c = bidirow->vis_get_shaped_char(vcol, c);
8870 items[item_count].columns = j - lcol;
8871 items[item_count].x = (vcol - (bidirow->vis_is_rtl(vcol) ? items[item_count].columns - 1 : 0)) * column_width;
8872 items[item_count].y = y;
8873 items[item_count].mirror = bidirow->vis_is_rtl(vcol);
8874 items[item_count].box_mirror = !!(row_data->attr.bidi_flags & VTE_BIDI_FLAG_BOX_MIRROR);
8875 item_count++;
8876
8877 g_assert_cmpint (j, >, lcol);
8878 lcol = j;
8879 }
8880
8881 /* Draw the last run of cells in the row. */
8882 if (item_count > 0) {
8883 draw_cells(items, item_count,
8884 fore, back, deco, FALSE, FALSE,
8885 attr & attr_mask,
8886 hyperlink, hilite,
8887 column_width, row_height);
8888 }
8889 }
8890 }
8891
8892 void
paint_cursor()8893 Terminal::paint_cursor()
8894 {
8895 vte::view::DrawingContext::TextRequest item;
8896 vte::grid::row_t drow;
8897 vte::grid::column_t lcol, vcol;
8898 int width, height, cursor_width;
8899 guint fore, back, deco;
8900 vte::color::rgb bg;
8901 int x, y;
8902 gboolean blink, selected, focus;
8903
8904 //FIXMEchpe this should already be reflected in the m_cursor_blink_state below
8905 if (!m_modes_private.DEC_TEXT_CURSOR())
8906 return;
8907
8908 if (m_im_preedit_active)
8909 return;
8910
8911 focus = m_has_focus;
8912 blink = m_cursor_blink_state;
8913
8914 if (focus && !blink)
8915 return;
8916
8917 lcol = m_screen->cursor.col;
8918 drow = m_screen->cursor.row;
8919 width = m_cell_width;
8920 height = m_cell_height;
8921
8922 if (!cursor_is_onscreen())
8923 return;
8924 if (CLAMP(lcol, 0, m_column_count - 1) != lcol)
8925 return;
8926
8927 /* Need to ensure the ringview is updated. */
8928 ringview_update();
8929
8930 /* Find the first cell of the character "under" the cursor.
8931 * This is for CJK. For TAB, paint the cursor where it really is. */
8932 VteRowData const *row_data = find_row_data(drow);
8933 vte::base::BidiRow const *bidirow = m_ringview.get_bidirow(drow);
8934
8935 auto cell = find_charcell(lcol, drow);
8936 while (cell != NULL && cell->attr.fragment() && cell->c != '\t' && lcol > 0) {
8937 lcol--;
8938 cell = find_charcell(lcol, drow);
8939 }
8940
8941 /* Draw the cursor. */
8942 vcol = bidirow->log2vis(lcol);
8943 item.c = (cell && cell->c) ? bidirow->vis_get_shaped_char(vcol, cell->c) : ' ';
8944 item.columns = item.c == '\t' ? 1 : cell ? cell->attr.columns() : 1;
8945 item.x = (vcol - ((cell && bidirow->vis_is_rtl(vcol)) ? cell->attr.columns() - 1 : 0)) * width;
8946 item.y = row_to_pixel(drow);
8947 item.mirror = bidirow->vis_is_rtl(vcol);
8948 item.box_mirror = (row_data && (row_data->attr.bidi_flags & VTE_BIDI_FLAG_BOX_MIRROR));
8949 auto const attr = cell && cell->c ? cell->attr.attr : 0;
8950
8951 selected = cell_is_selected_log(lcol, drow);
8952 determine_cursor_colors(cell, selected, &fore, &back, &deco);
8953 rgb_from_index<8, 8, 8>(back, bg);
8954
8955 x = item.x;
8956 y = item.y;
8957
8958 switch (decscusr_cursor_shape()) {
8959
8960 case CursorShape::eIBEAM: {
8961 /* Draw at the very left of the cell (before the spacing), even in case of CJK.
8962 * IMO (egmont) not overrunning the letter improves readability, vertical movement
8963 * looks good (no zigzag even when a somewhat wider glyph that starts filling up
8964 * the left spacing, or CJK that begins further to the right is encountered),
8965 * and also this is where it looks good if background colors change, including
8966 * Shift+arrows highlighting experience in some editors. As per the behavior of
8967 * word processors, don't increase the height by the line spacing. */
8968 int stem_width;
8969
8970 stem_width = (int) (((float) (m_char_ascent + m_char_descent)) * m_cursor_aspect_ratio + 0.5);
8971 stem_width = CLAMP (stem_width, VTE_LINE_WIDTH, m_cell_width);
8972
8973 /* The I-beam goes to the right edge of the cell if its character has RTL resolved direction. */
8974 if (bidirow->vis_is_rtl(vcol))
8975 x += item.columns * m_cell_width - stem_width;
8976
8977 m_draw.fill_rectangle(
8978 x, y + m_char_padding.top, stem_width, m_char_ascent + m_char_descent,
8979 &bg, VTE_DRAW_OPAQUE);
8980
8981 /* Show the direction of the current character if the paragraph contains a mixture
8982 * of directions.
8983 * FIXME Do this for the other cursor shapes, too. Need to find a good visual design. */
8984 if (focus && bidirow->has_foreign())
8985 m_draw.fill_rectangle(
8986 bidirow->vis_is_rtl(vcol) ? x - stem_width : x + stem_width,
8987 y + m_char_padding.top,
8988 stem_width, stem_width,
8989 &bg, VTE_DRAW_OPAQUE);
8990 break;
8991 }
8992
8993 case CursorShape::eUNDERLINE: {
8994 /* The width is at least the overall width of the cell (or two cells) minus the two
8995 * half spacings on the two edges. That is, underlines under a CJK are more than twice
8996 * as wide as narrow characters in case of letter spacing. Plus, if necessary, the width
8997 * is increased to span under the entire glyph. Vertical position is not affected by
8998 * line spacing. */
8999
9000 int line_height, left, right;
9001
9002 /* use height (not width) so underline and ibeam will
9003 * be equally visible */
9004 line_height = (int) (((float) (m_char_ascent + m_char_descent)) * m_cursor_aspect_ratio + 0.5);
9005 line_height = CLAMP (line_height, VTE_LINE_WIDTH, m_char_ascent + m_char_descent);
9006
9007 left = m_char_padding.left;
9008 right = item.columns * m_cell_width - m_char_padding.right;
9009
9010 if (cell && cell->c != 0 && cell->c != ' ' && cell->c != '\t') {
9011 int l, r;
9012 m_draw.get_char_edges(cell->c, cell->attr.columns(), attr, l, r);
9013 left = MIN(left, l);
9014 right = MAX(right, r);
9015 }
9016
9017 m_draw.fill_rectangle(
9018 x + left, y + m_cell_height - m_char_padding.bottom - line_height,
9019 right - left, line_height,
9020 &bg, VTE_DRAW_OPAQUE);
9021 break;
9022 }
9023
9024 case CursorShape::eBLOCK:
9025 /* Include the spacings in the cursor, see bug 781479 comments 39-44.
9026 * Make the cursor even wider if the glyph is wider. */
9027
9028 cursor_width = item.columns * width;
9029 if (cell && cell->c != 0 && cell->c != ' ' && cell->c != '\t') {
9030 int l, r;
9031 m_draw.get_char_edges(cell->c, cell->attr.columns(), attr, l /* unused */, r);
9032 cursor_width = MAX(cursor_width, r);
9033 }
9034
9035 uint32_t const attr_mask = m_allow_bold ? ~0 : ~VTE_ATTR_BOLD_MASK;
9036
9037 if (focus) {
9038 /* just reverse the character under the cursor */
9039 m_draw.fill_rectangle(
9040 x, y,
9041 cursor_width, height,
9042 &bg, VTE_DRAW_OPAQUE);
9043
9044 if (cell && cell->c != 0 && cell->c != ' ' && cell->c != '\t') {
9045 draw_cells(
9046 &item, 1,
9047 fore, back, deco, TRUE, FALSE,
9048 cell->attr.attr & attr_mask,
9049 m_allow_hyperlink && cell->attr.hyperlink_idx != 0,
9050 FALSE,
9051 width,
9052 height);
9053 }
9054
9055 } else {
9056 /* draw a box around the character */
9057 m_draw.draw_rectangle(
9058 x - VTE_LINE_WIDTH,
9059 y - VTE_LINE_WIDTH,
9060 cursor_width + 2*VTE_LINE_WIDTH,
9061 height + 2*VTE_LINE_WIDTH,
9062 &bg, VTE_DRAW_OPAQUE);
9063 }
9064
9065 break;
9066 }
9067 }
9068
9069 void
paint_im_preedit_string()9070 Terminal::paint_im_preedit_string()
9071 {
9072 int vcol, columns;
9073 long row;
9074 long width, height;
9075 int i, len;
9076
9077 if (m_im_preedit.empty())
9078 return;
9079
9080 /* Need to ensure the ringview is updated. */
9081 ringview_update();
9082
9083 /* Get the row's BiDi information. */
9084 row = m_screen->cursor.row;
9085 if (row < first_displayed_row() || row > last_displayed_row())
9086 return;
9087 vte::base::BidiRow const *bidirow = m_ringview.get_bidirow(row);
9088
9089 /* Keep local copies of rendering information. */
9090 width = m_cell_width;
9091 height = m_cell_height;
9092
9093 /* Find out how many columns the pre-edit string takes up. */
9094 columns = get_preedit_width(false);
9095 len = get_preedit_length(false);
9096
9097 /* If the pre-edit string won't fit on the screen if we start
9098 * drawing it at the cursor's position, move it left. */
9099 vcol = bidirow->log2vis(m_screen->cursor.col);
9100 if (vcol + columns > m_column_count) {
9101 vcol = MAX(0, m_column_count - columns);
9102 }
9103
9104 /* Draw the preedit string, boxed. */
9105 if (len > 0) {
9106 const char *preedit = m_im_preedit.c_str();
9107 int preedit_cursor;
9108
9109 auto items = g_new0(vte::view::DrawingContext::TextRequest, len);
9110 for (i = columns = 0; i < len; i++) {
9111 items[i].c = g_utf8_get_char(preedit);
9112 items[i].columns = _vte_unichar_width(items[i].c,
9113 m_utf8_ambiguous_width);
9114 items[i].x = (vcol + columns) * width;
9115 items[i].y = row_to_pixel(m_screen->cursor.row);
9116 columns += items[i].columns;
9117 preedit = g_utf8_next_char(preedit);
9118 }
9119 if (G_LIKELY(m_clear_background)) {
9120 m_draw.clear(
9121 vcol * width,
9122 row_to_pixel(m_screen->cursor.row),
9123 width * columns,
9124 height,
9125 get_color(VTE_DEFAULT_BG), m_background_alpha);
9126 }
9127 draw_cells_with_attributes(
9128 items, len,
9129 m_im_preedit_attrs.get(),
9130 TRUE,
9131 width, height);
9132 preedit_cursor = m_im_preedit_cursor;
9133
9134 if (preedit_cursor >= 0 && preedit_cursor < len) {
9135 uint32_t fore, back, deco;
9136 vte_color_triple_get(m_color_defaults.attr.colors(), &fore, &back, &deco);
9137
9138 /* Cursored letter in reverse. */
9139 draw_cells(
9140 &items[preedit_cursor], 1,
9141 fore, back, deco,
9142 TRUE, /* clear */
9143 TRUE, /* draw_default_bg */
9144 VTE_ATTR_NONE | VTE_ATTR_BOXED,
9145 FALSE, /* hyperlink */
9146 FALSE, /* hilite */
9147 width, height);
9148 }
9149 g_free(items);
9150 }
9151 }
9152
9153 void
widget_draw(cairo_t * cr)9154 Terminal::widget_draw(cairo_t *cr)
9155 {
9156 cairo_rectangle_int_t clip_rect;
9157 cairo_region_t *region;
9158 int allocated_width, allocated_height;
9159 int extra_area_for_cursor;
9160 bool text_blink_enabled_now;
9161 gint64 now = 0;
9162
9163 if (!gdk_cairo_get_clip_rectangle (cr, &clip_rect))
9164 return;
9165
9166 _vte_debug_print(VTE_DEBUG_LIFECYCLE, "vte_terminal_draw()\n");
9167 _vte_debug_print (VTE_DEBUG_WORK, "+");
9168 _vte_debug_print (VTE_DEBUG_UPDATES, "Draw (%d,%d)x(%d,%d)\n",
9169 clip_rect.x, clip_rect.y,
9170 clip_rect.width, clip_rect.height);
9171
9172 region = vte_cairo_get_clip_region (cr);
9173 if (region == NULL)
9174 return;
9175
9176 allocated_width = get_allocated_width();
9177 allocated_height = get_allocated_height();
9178
9179 /* Designate the start of the drawing operation and clear the area. */
9180 m_draw.set_cairo(cr);
9181
9182 if (G_LIKELY(m_clear_background)) {
9183 m_draw.clear(0, 0,
9184 allocated_width, allocated_height,
9185 get_color(VTE_DEFAULT_BG), m_background_alpha);
9186 }
9187
9188 /* Clip vertically, for the sake of smooth scrolling. We want the top and bottom paddings to be unused.
9189 * Don't clip horizontally so that antialiasing can legally overflow to the right padding. */
9190 cairo_save(cr);
9191 cairo_rectangle(cr, 0, m_padding.top, allocated_width, allocated_height - m_padding.top - m_padding.bottom);
9192 cairo_clip(cr);
9193
9194 cairo_translate(cr, m_padding.left, m_padding.top);
9195
9196 /* Transform to view coordinates */
9197 cairo_region_translate(region, -m_padding.left, -m_padding.top);
9198
9199 /* Whether blinking text should be visible now */
9200 m_text_blink_state = true;
9201 text_blink_enabled_now = (unsigned)m_text_blink_mode & (unsigned)(m_has_focus ? TextBlinkMode::eFOCUSED : TextBlinkMode::eUNFOCUSED);
9202 if (text_blink_enabled_now) {
9203 now = g_get_monotonic_time() / 1000;
9204 if (now % (m_text_blink_cycle * 2) >= m_text_blink_cycle)
9205 m_text_blink_state = false;
9206 }
9207 /* Painting will flip this if it encounters any cell with blink attribute */
9208 m_text_to_blink = false;
9209
9210 /* and now paint them */
9211 auto const first_row = first_displayed_row();
9212 draw_rows(m_screen,
9213 region,
9214 first_row,
9215 last_displayed_row() + 1,
9216 row_to_pixel(first_row),
9217 m_cell_width,
9218 m_cell_height);
9219
9220 paint_im_preedit_string();
9221
9222 cairo_restore(cr);
9223
9224 /* Re-clip, allowing VTE_LINE_WIDTH more pixel rows for the outline cursor. */
9225 /* TODOegmont: It's really ugly to do it here. */
9226 cairo_save(cr);
9227 extra_area_for_cursor = (decscusr_cursor_shape() == CursorShape::eBLOCK && !m_has_focus) ? VTE_LINE_WIDTH : 0;
9228 cairo_rectangle(cr, 0, m_padding.top - extra_area_for_cursor, allocated_width, allocated_height - m_padding.top - m_padding.bottom + 2 * extra_area_for_cursor);
9229 cairo_clip(cr);
9230
9231 cairo_translate(cr, m_padding.left, m_padding.top);
9232
9233 paint_cursor();
9234
9235 cairo_restore(cr);
9236
9237 /* Done with various structures. */
9238 m_draw.set_cairo(nullptr);
9239
9240 cairo_region_destroy (region);
9241
9242 /* If painting encountered any cell with blink attribute, we might need to set up a timer.
9243 * Blinking is implemented using a one-shot (not repeating) timer that keeps getting reinstalled
9244 * here as long as blinking cells are encountered during (re)painting. This way there's no need
9245 * for an explicit step to stop the timer when blinking cells are no longer present, this happens
9246 * implicitly by the timer not getting reinstalled anymore (often after a final unnecessary but
9247 * harmless repaint). */
9248 if (G_UNLIKELY (m_text_to_blink && text_blink_enabled_now && !m_text_blink_timer))
9249 m_text_blink_timer.schedule(m_text_blink_cycle - now % m_text_blink_cycle,
9250 vte::glib::Timer::Priority::eLOW);
9251
9252 m_invalidated_all = FALSE;
9253 }
9254
9255 /* Handle an expose event by painting the exposed area. */
9256 static cairo_region_t *
vte_cairo_get_clip_region(cairo_t * cr)9257 vte_cairo_get_clip_region (cairo_t *cr)
9258 {
9259 cairo_rectangle_list_t *list;
9260 cairo_region_t *region;
9261 int i;
9262
9263 list = cairo_copy_clip_rectangle_list (cr);
9264 if (list->status == CAIRO_STATUS_CLIP_NOT_REPRESENTABLE) {
9265 cairo_rectangle_int_t clip_rect;
9266
9267 cairo_rectangle_list_destroy (list);
9268
9269 if (!gdk_cairo_get_clip_rectangle (cr, &clip_rect))
9270 return NULL;
9271 return cairo_region_create_rectangle (&clip_rect);
9272 }
9273
9274
9275 region = cairo_region_create ();
9276 for (i = list->num_rectangles - 1; i >= 0; --i) {
9277 cairo_rectangle_t *rect = &list->rectangles[i];
9278 cairo_rectangle_int_t clip_rect;
9279
9280 clip_rect.x = floor (rect->x);
9281 clip_rect.y = floor (rect->y);
9282 clip_rect.width = ceil (rect->x + rect->width) - clip_rect.x;
9283 clip_rect.height = ceil (rect->y + rect->height) - clip_rect.y;
9284
9285 if (cairo_region_union_rectangle (region, &clip_rect) != CAIRO_STATUS_SUCCESS) {
9286 cairo_region_destroy (region);
9287 region = NULL;
9288 break;
9289 }
9290 }
9291
9292 cairo_rectangle_list_destroy (list);
9293 return region;
9294 }
9295
9296 bool
widget_mouse_scroll(vte::platform::ScrollEvent const & event)9297 Terminal::widget_mouse_scroll(vte::platform::ScrollEvent const& event)
9298 {
9299 gdouble v;
9300 gint cnt, i;
9301 int button;
9302
9303 /* Need to ensure the ringview is updated. */
9304 ringview_update();
9305
9306 m_modifiers = event.modifiers();
9307 m_mouse_smooth_scroll_delta += event.dy();
9308
9309 /* If we're running a mouse-aware application, map the scroll event
9310 * to a button press on buttons four and five. */
9311 if (m_mouse_tracking_mode != MouseTrackingMode::eNONE) {
9312 cnt = m_mouse_smooth_scroll_delta;
9313 if (cnt == 0)
9314 return true;
9315 m_mouse_smooth_scroll_delta -= cnt;
9316 _vte_debug_print(VTE_DEBUG_EVENTS,
9317 "Scroll application by %d lines, smooth scroll delta set back to %f\n",
9318 cnt, m_mouse_smooth_scroll_delta);
9319
9320 button = cnt > 0 ? 5 : 4;
9321 if (cnt < 0)
9322 cnt = -cnt;
9323 for (i = 0; i < cnt; i++) {
9324 /* Encode the parameters and send them to the app. */
9325 feed_mouse_event(grid_coords_from_view_coords(m_mouse_last_position),
9326 button,
9327 false /* not drag */,
9328 false /* not release */);
9329 }
9330 return true;
9331 }
9332
9333 v = MAX (1., ceil (gtk_adjustment_get_page_increment (m_vadjustment.get()) / 10.));
9334 _vte_debug_print(VTE_DEBUG_EVENTS,
9335 "Scroll speed is %d lines per non-smooth scroll unit\n",
9336 (int) v);
9337 if (m_screen == &m_alternate_screen &&
9338 m_modes_private.XTERM_ALTBUF_SCROLL()) {
9339 char *normal;
9340 gsize normal_length;
9341
9342 cnt = v * m_mouse_smooth_scroll_delta;
9343 if (cnt == 0)
9344 return true;
9345 m_mouse_smooth_scroll_delta -= cnt / v;
9346 _vte_debug_print(VTE_DEBUG_EVENTS,
9347 "Scroll by %d lines, smooth scroll delta set back to %f\n",
9348 cnt, m_mouse_smooth_scroll_delta);
9349
9350 /* In the alternate screen there is no scrolling,
9351 * so fake a few cursor keystrokes. */
9352
9353 _vte_keymap_map (
9354 cnt > 0 ? GDK_KEY_Down : GDK_KEY_Up,
9355 m_modifiers,
9356 m_modes_private.DEC_APPLICATION_CURSOR_KEYS(),
9357 m_modes_private.DEC_APPLICATION_KEYPAD(),
9358 &normal,
9359 &normal_length);
9360 if (cnt < 0)
9361 cnt = -cnt;
9362 for (i = 0; i < cnt; i++) {
9363 send_child({normal, normal_length});
9364 }
9365 g_free (normal);
9366 return true;
9367 } else if (m_fallback_scrolling) {
9368 /* Perform a history scroll. */
9369 double dcnt = m_screen->scroll_delta + v * m_mouse_smooth_scroll_delta;
9370 queue_adjustment_value_changed_clamped(dcnt);
9371 m_mouse_smooth_scroll_delta = 0;
9372 return true;
9373 }
9374 return false;
9375 }
9376
9377 bool
set_audible_bell(bool setting)9378 Terminal::set_audible_bell(bool setting)
9379 {
9380 if (setting == m_audible_bell)
9381 return false;
9382
9383 m_audible_bell = setting;
9384 return true;
9385 }
9386
9387 bool
set_text_blink_mode(TextBlinkMode setting)9388 Terminal::set_text_blink_mode(TextBlinkMode setting)
9389 {
9390 if (setting == m_text_blink_mode)
9391 return false;
9392
9393 m_text_blink_mode = setting;
9394 invalidate_all();
9395
9396 return true;
9397 }
9398
9399 bool
set_enable_bidi(bool setting)9400 Terminal::set_enable_bidi(bool setting)
9401 {
9402 if (setting == m_enable_bidi)
9403 return false;
9404
9405 m_enable_bidi = setting;
9406 m_ringview.invalidate();
9407 invalidate_all();
9408
9409 /* Chances are that we can free up some BiDi/shaping buffers that we
9410 * won't need for a while. */
9411 if (!setting)
9412 m_ringview.pause();
9413
9414 return true;
9415 }
9416
9417 bool
set_enable_shaping(bool setting)9418 Terminal::set_enable_shaping(bool setting)
9419 {
9420 if (setting == m_enable_shaping)
9421 return false;
9422
9423 m_enable_shaping = setting;
9424 m_ringview.invalidate();
9425 invalidate_all();
9426
9427 /* Chances are that we can free up some BiDi/shaping buffers that we
9428 * won't need for a while. */
9429 if (!setting)
9430 m_ringview.pause();
9431
9432 return true;
9433 }
9434
9435 bool
set_allow_bold(bool setting)9436 Terminal::set_allow_bold(bool setting)
9437 {
9438 if (setting == m_allow_bold)
9439 return false;
9440
9441 m_allow_bold = setting;
9442 invalidate_all();
9443
9444 return true;
9445 }
9446
9447 bool
set_bold_is_bright(bool setting)9448 Terminal::set_bold_is_bright(bool setting)
9449 {
9450 if (setting == m_bold_is_bright)
9451 return false;
9452
9453 m_bold_is_bright = setting;
9454
9455 /* Need to re-sanitise the font description to ensure bold is distinct. */
9456 update_font_desc();
9457
9458 invalidate_all();
9459
9460 return true;
9461 }
9462
9463 bool
set_allow_hyperlink(bool setting)9464 Terminal::set_allow_hyperlink(bool setting)
9465 {
9466 if (setting == m_allow_hyperlink)
9467 return false;
9468
9469 if (setting == false) {
9470 m_hyperlink_hover_idx = _vte_ring_get_hyperlink_at_position(m_screen->row_data, -1, -1, true, NULL);
9471 g_assert (m_hyperlink_hover_idx == 0);
9472 m_hyperlink_hover_uri = NULL;
9473 emit_hyperlink_hover_uri_changed(NULL); /* FIXME only emit if really changed */
9474 m_defaults.attr.hyperlink_idx = _vte_ring_get_hyperlink_idx(m_screen->row_data, NULL);
9475 g_assert (m_defaults.attr.hyperlink_idx == 0);
9476 }
9477
9478 m_allow_hyperlink = setting;
9479 invalidate_all();
9480
9481 return true;
9482 }
9483
9484 bool
set_fallback_scrolling(bool set)9485 Terminal::set_fallback_scrolling(bool set)
9486 {
9487 if (set == m_fallback_scrolling)
9488 return false;
9489
9490 m_fallback_scrolling = set;
9491 return true;
9492 }
9493
9494 bool
set_scroll_on_output(bool scroll)9495 Terminal::set_scroll_on_output(bool scroll)
9496 {
9497 if (scroll == m_scroll_on_output)
9498 return false;
9499
9500 m_scroll_on_output = scroll;
9501 return true;
9502 }
9503
9504 bool
set_scroll_on_keystroke(bool scroll)9505 Terminal::set_scroll_on_keystroke(bool scroll)
9506 {
9507 if (scroll == m_scroll_on_keystroke)
9508 return false;
9509
9510 m_scroll_on_keystroke = scroll;
9511 return true;
9512 }
9513
9514 bool
set_rewrap_on_resize(bool rewrap)9515 Terminal::set_rewrap_on_resize(bool rewrap)
9516 {
9517 if (rewrap == m_rewrap_on_resize)
9518 return false;
9519
9520 m_rewrap_on_resize = rewrap;
9521 return true;
9522 }
9523
9524 void
update_cursor_blinks()9525 Terminal::update_cursor_blinks()
9526 {
9527 bool blink = false;
9528
9529 switch (decscusr_cursor_blink()) {
9530 case CursorBlinkMode::eSYSTEM:
9531 gboolean v;
9532 g_object_get(gtk_widget_get_settings(m_widget),
9533 "gtk-cursor-blink",
9534 &v, nullptr);
9535 blink = v != FALSE;
9536 break;
9537 case CursorBlinkMode::eON:
9538 blink = true;
9539 break;
9540 case CursorBlinkMode::eOFF:
9541 blink = false;
9542 break;
9543 }
9544
9545 if (m_cursor_blinks == blink)
9546 return;
9547
9548 m_cursor_blinks = blink;
9549 check_cursor_blink();
9550 }
9551
9552 bool
set_cursor_blink_mode(CursorBlinkMode mode)9553 Terminal::set_cursor_blink_mode(CursorBlinkMode mode)
9554 {
9555 if (mode == m_cursor_blink_mode)
9556 return false;
9557
9558 m_cursor_blink_mode = mode;
9559 update_cursor_blinks();
9560
9561 return true;
9562 }
9563
9564 bool
set_cursor_shape(CursorShape shape)9565 Terminal::set_cursor_shape(CursorShape shape)
9566 {
9567 if (shape == m_cursor_shape)
9568 return false;
9569
9570 m_cursor_shape = shape;
9571 invalidate_cursor_once();
9572
9573 return true;
9574 }
9575
9576 /* DECSCUSR set cursor style */
9577 bool
set_cursor_style(CursorStyle style)9578 Terminal::set_cursor_style(CursorStyle style)
9579 {
9580 if (m_cursor_style == style)
9581 return false;
9582
9583 m_cursor_style = style;
9584 update_cursor_blinks();
9585 /* and this will also make cursor shape match the DECSCUSR style */
9586 invalidate_cursor_once();
9587
9588 return true;
9589 }
9590
9591 /*
9592 * Terminal::decscusr_cursor_blink:
9593 *
9594 * Returns the cursor blink mode set by DECSCUSR. If DECSCUSR was never
9595 * called, or it set the blink mode to terminal default, this returns the
9596 * value set via API or in dconf. Internal use only.
9597 *
9598 * Return value: cursor blink mode
9599 */
9600 Terminal::CursorBlinkMode
decscusr_cursor_blink() const9601 Terminal::decscusr_cursor_blink() const noexcept
9602 {
9603 switch (m_cursor_style) {
9604 default:
9605 case CursorStyle::eTERMINAL_DEFAULT:
9606 return m_cursor_blink_mode;
9607 case CursorStyle::eBLINK_BLOCK:
9608 case CursorStyle::eBLINK_UNDERLINE:
9609 case CursorStyle::eBLINK_IBEAM:
9610 return CursorBlinkMode::eON;
9611 case CursorStyle::eSTEADY_BLOCK:
9612 case CursorStyle::eSTEADY_UNDERLINE:
9613 case CursorStyle::eSTEADY_IBEAM:
9614 return CursorBlinkMode::eOFF;
9615 }
9616 }
9617
9618 /*
9619 * Terminal::decscusr_cursor_shape:
9620 * @terminal: a #VteTerminal
9621 *
9622 * Returns the cursor shape set by DECSCUSR. If DECSCUSR was never called,
9623 * or it set the cursor shape to terminal default, this returns the value
9624 * set via API. Internal use only.
9625 *
9626 * Return value: cursor shape
9627 */
9628 Terminal::CursorShape
decscusr_cursor_shape() const9629 Terminal::decscusr_cursor_shape() const noexcept
9630 {
9631 switch (m_cursor_style) {
9632 default:
9633 case CursorStyle::eTERMINAL_DEFAULT:
9634 return m_cursor_shape;
9635 case CursorStyle::eBLINK_BLOCK:
9636 case CursorStyle::eSTEADY_BLOCK:
9637 return CursorShape::eBLOCK;
9638 case CursorStyle::eBLINK_UNDERLINE:
9639 case CursorStyle::eSTEADY_UNDERLINE:
9640 return CursorShape::eUNDERLINE;
9641 case CursorStyle::eBLINK_IBEAM:
9642 case CursorStyle::eSTEADY_IBEAM:
9643 return CursorShape::eIBEAM;
9644 }
9645 }
9646
9647 bool
set_scrollback_lines(long lines)9648 Terminal::set_scrollback_lines(long lines)
9649 {
9650 glong low, high, next;
9651 double scroll_delta;
9652 VteScreen *scrn;
9653
9654 if (lines < 0)
9655 lines = G_MAXLONG;
9656
9657 #if 0
9658 /* FIXME: this breaks the scrollbar range, bug #562511 */
9659 if (lines == m_scrollback_lines)
9660 return false;
9661 #endif
9662
9663 _vte_debug_print (VTE_DEBUG_MISC,
9664 "Setting scrollback lines to %ld\n", lines);
9665
9666 m_scrollback_lines = lines;
9667
9668 /* The main screen gets the full scrollback buffer. */
9669 scrn = &m_normal_screen;
9670 lines = MAX (lines, m_row_count);
9671 next = MAX (m_screen->cursor.row + 1,
9672 _vte_ring_next (scrn->row_data));
9673 _vte_ring_resize (scrn->row_data, lines);
9674 low = _vte_ring_delta (scrn->row_data);
9675 high = lines + MIN (G_MAXLONG - lines, low - m_row_count + 1);
9676 scrn->insert_delta = CLAMP (scrn->insert_delta, low, high);
9677 scrn->scroll_delta = CLAMP (scrn->scroll_delta, low, scrn->insert_delta);
9678 next = MIN (next, scrn->insert_delta + m_row_count);
9679 if (_vte_ring_next (scrn->row_data) > next){
9680 _vte_ring_shrink (scrn->row_data, next - low);
9681 }
9682
9683 /* The alternate scrn isn't allowed to scroll at all. */
9684 scrn = &m_alternate_screen;
9685 _vte_ring_resize (scrn->row_data, m_row_count);
9686 scrn->scroll_delta = _vte_ring_delta (scrn->row_data);
9687 scrn->insert_delta = _vte_ring_delta (scrn->row_data);
9688 if (_vte_ring_next (scrn->row_data) > scrn->insert_delta + m_row_count){
9689 _vte_ring_shrink (scrn->row_data, m_row_count);
9690 }
9691
9692 /* Adjust the scrollbar to the new location. */
9693 /* Hack: force a change in scroll_delta even if the value remains, so that
9694 vte_term_q_adj_val_changed() doesn't shortcut to no-op, see bug 676075. */
9695 scroll_delta = m_screen->scroll_delta;
9696 m_screen->scroll_delta = -1;
9697 queue_adjustment_value_changed(scroll_delta);
9698 adjust_adjustments_full();
9699
9700 return true;
9701 }
9702
9703 bool
set_backspace_binding(EraseMode binding)9704 Terminal::set_backspace_binding(EraseMode binding)
9705 {
9706 if (binding == m_backspace_binding)
9707 return false;
9708
9709 m_backspace_binding = binding;
9710 return true;
9711 }
9712
9713 bool
set_delete_binding(EraseMode binding)9714 Terminal::set_delete_binding(EraseMode binding)
9715 {
9716 if (binding == m_delete_binding)
9717 return false;
9718
9719 m_delete_binding = binding;
9720 return true;
9721 }
9722
9723 bool
set_mouse_autohide(bool autohide)9724 Terminal::set_mouse_autohide(bool autohide)
9725 {
9726 if (autohide == m_mouse_autohide)
9727 return false;
9728
9729 m_mouse_autohide = autohide;
9730
9731 if (m_mouse_cursor_autohidden) {
9732 hyperlink_hilite_update();
9733 match_hilite_update();
9734 apply_mouse_cursor();
9735 }
9736 return true;
9737 }
9738
9739 void
reset_decoder()9740 Terminal::reset_decoder()
9741 {
9742 switch (primary_data_syntax()) {
9743 case DataSyntax::ECMA48_UTF8:
9744 m_utf8_decoder.reset();
9745 break;
9746
9747 #ifdef WITH_ICU
9748 case DataSyntax::ECMA48_PCTERM:
9749 m_converter->decoder().reset();
9750 break;
9751 #endif
9752
9753 default:
9754 g_assert_not_reached();
9755 }
9756 }
9757
9758 void
reset_data_syntax()9759 Terminal::reset_data_syntax()
9760 {
9761 if (current_data_syntax() == primary_data_syntax())
9762 return;
9763
9764 switch (current_data_syntax()) {
9765 default:
9766 break;
9767 }
9768
9769 pop_data_syntax();
9770 }
9771
9772 /*
9773 * Terminal::reset:
9774 * @clear_tabstops: whether to reset tabstops
9775 * @clear_history: whether to empty the terminal's scrollback buffer
9776 *
9777 * Resets as much of the terminal's internal state as possible, discarding any
9778 * unprocessed input data, resetting character attributes, cursor state,
9779 * national character set state, status line, terminal modes (insert/delete),
9780 * selection state, and encoding.
9781 *
9782 */
9783 void
reset(bool clear_tabstops,bool clear_history,bool from_api)9784 Terminal::reset(bool clear_tabstops,
9785 bool clear_history,
9786 bool from_api)
9787 {
9788 if (from_api && !m_input_enabled)
9789 return;
9790
9791 auto const freezer = vte::glib::FreezeObjectNotify{m_terminal};
9792
9793 m_bell_pending = false;
9794
9795 /* Clear the output buffer. */
9796 _vte_byte_array_clear(m_outgoing);
9797
9798 /* Reset charset substitution state. */
9799
9800 /* Reset decoder */
9801 reset_decoder();
9802
9803 /* Reset parser */
9804 reset_data_syntax();
9805 m_parser.reset();
9806 m_last_graphic_character = 0;
9807
9808 /* Reset modes */
9809 m_modes_ecma.reset();
9810 m_modes_private.clear_saved();
9811 m_modes_private.reset();
9812
9813 /* Reset tabstops */
9814 if (clear_tabstops) {
9815 m_tabstops.reset();
9816 }
9817
9818 /* Window title stack */
9819 if (clear_history) {
9820 m_window_title_stack.clear();
9821 }
9822
9823 update_mouse_protocol();
9824
9825 /* Reset the color palette. Only the 256 indexed colors, not the special ones, as per xterm. */
9826 for (int i = 0; i < 256; i++)
9827 m_palette[i].sources[VTE_COLOR_SOURCE_ESCAPE].is_set = FALSE;
9828 /* Reset the default attributes. Reset the alternate attribute because
9829 * it's not a real attribute, but we need to treat it as one here. */
9830 reset_default_attributes(true);
9831 /* Reset charset modes. */
9832 m_character_replacements[0] = VTE_CHARACTER_REPLACEMENT_NONE;
9833 m_character_replacements[1] = VTE_CHARACTER_REPLACEMENT_NONE;
9834 m_character_replacement = &m_character_replacements[0];
9835 /* Clear the scrollback buffers and reset the cursors. Switch to normal screen. */
9836 if (clear_history) {
9837 m_screen = &m_normal_screen;
9838 m_normal_screen.scroll_delta = m_normal_screen.insert_delta =
9839 _vte_ring_reset(m_normal_screen.row_data);
9840 m_normal_screen.cursor.row = m_normal_screen.insert_delta;
9841 m_normal_screen.cursor.col = 0;
9842 m_alternate_screen.scroll_delta = m_alternate_screen.insert_delta =
9843 _vte_ring_reset(m_alternate_screen.row_data);
9844 m_alternate_screen.cursor.row = m_alternate_screen.insert_delta;
9845 m_alternate_screen.cursor.col = 0;
9846 /* Adjust the scrollbar to the new location. */
9847 /* Hack: force a change in scroll_delta even if the value remains, so that
9848 vte_term_q_adj_val_changed() doesn't shortcut to no-op, see bug 730599. */
9849 m_screen->scroll_delta = -1;
9850 queue_adjustment_value_changed(m_screen->insert_delta);
9851 adjust_adjustments_full();
9852 }
9853 /* DECSCUSR cursor style */
9854 set_cursor_style(CursorStyle::eTERMINAL_DEFAULT);
9855 /* Reset restricted scrolling regions, leave insert mode, make
9856 * the cursor visible again. */
9857 m_scrolling_restricted = FALSE;
9858 /* Reset the visual bits of selection on hard reset, see bug 789954. */
9859 if (clear_history) {
9860 deselect_all();
9861 stop_autoscroll(); /* Required before setting m_selecting to false, see #105. */
9862 m_selecting = FALSE;
9863 m_selecting_had_delta = FALSE;
9864 m_selection_origin = m_selection_last = { -1, -1, 1 };
9865 m_selection_resolved.clear();
9866 }
9867
9868 /* Reset mouse motion events. */
9869 m_mouse_pressed_buttons = 0;
9870 m_mouse_handled_buttons = 0;
9871 m_mouse_last_position = vte::view::coords(-1, -1);
9872 m_mouse_smooth_scroll_delta = 0.;
9873 /* Clear modifiers. */
9874 m_modifiers = 0;
9875
9876 /* Reset the saved cursor. */
9877 save_cursor(&m_normal_screen);
9878 save_cursor(&m_alternate_screen);
9879 /* BiDi */
9880 m_bidi_rtl = FALSE;
9881 /* Cause everything to be redrawn (or cleared). */
9882 invalidate_all();
9883
9884 /* Reset XTerm window controls */
9885 m_xterm_wm_iconified = false;
9886 }
9887
9888 void
unset_pty(bool notify_widget)9889 Terminal::unset_pty(bool notify_widget)
9890 {
9891 /* This may be called from inside or from widget,
9892 * and must notify the widget if not called from it.
9893 */
9894
9895 disconnect_pty_read();
9896 disconnect_pty_write();
9897
9898 m_child_exited_eos_wait_timer.abort();
9899
9900 /* Clear incoming and outgoing queues */
9901 m_input_bytes = 0;
9902 m_incoming_queue = {};
9903 _vte_byte_array_clear(m_outgoing);
9904
9905 stop_processing(this); // FIXMEchpe only if m_incoming_queue.empty() !!!
9906
9907 reset_decoder();
9908
9909 m_pty.reset();
9910
9911 if (notify_widget && widget())
9912 widget()->unset_pty();
9913 }
9914
9915 bool
set_pty(vte::base::Pty * new_pty)9916 Terminal::set_pty(vte::base::Pty *new_pty)
9917 {
9918 if (pty().get() == new_pty)
9919 return false;
9920
9921 if (pty()) {
9922 unset_pty(false /* don't notify widget */);
9923 }
9924
9925 m_pty = vte::base::make_ref(new_pty);
9926 if (!new_pty)
9927 return true;
9928
9929 set_size(m_column_count, m_row_count);
9930
9931 if (!pty()->set_utf8(primary_data_syntax() == DataSyntax::ECMA48_UTF8)) {
9932 // nothing we can do here
9933 }
9934
9935 /* Open channels to listen for input on. */
9936 connect_pty_read();
9937
9938 return true;
9939 }
9940
9941 bool
terminate_child()9942 Terminal::terminate_child() noexcept
9943 {
9944 if (m_pty_pid == -1)
9945 return false;
9946
9947 auto pgrp = getpgid(m_pty_pid);
9948 if (pgrp != -1 && pgrp != getpgid(getpid())) {
9949 kill(-pgrp, SIGHUP);
9950 }
9951
9952 kill(m_pty_pid, SIGHUP);
9953 m_pty_pid = -1;
9954
9955 return true;
9956 }
9957
9958 /* We need this bit of glue to ensure that accessible objects will always
9959 * get signals. */
9960 void
subscribe_accessible_events()9961 Terminal::subscribe_accessible_events()
9962 {
9963 #ifdef WITH_A11Y
9964 m_accessible_emit = true;
9965 #endif
9966 }
9967
9968 void
select_text(vte::grid::column_t start_col,vte::grid::row_t start_row,vte::grid::column_t end_col,vte::grid::row_t end_row)9969 Terminal::select_text(vte::grid::column_t start_col,
9970 vte::grid::row_t start_row,
9971 vte::grid::column_t end_col,
9972 vte::grid::row_t end_row)
9973 {
9974 deselect_all();
9975
9976 m_selection_type = SelectionType::eCHAR;
9977 m_selecting_had_delta = true;
9978 m_selection_resolved.set ({ start_row, start_col },
9979 { end_row, end_col });
9980 widget_copy(vte::platform::ClipboardType::PRIMARY,
9981 vte::platform::ClipboardFormat::TEXT);
9982 emit_selection_changed();
9983
9984 invalidate_rows(start_row, end_row);
9985 }
9986
9987 void
select_empty(vte::grid::column_t col,vte::grid::row_t row)9988 Terminal::select_empty(vte::grid::column_t col,
9989 vte::grid::row_t row)
9990 {
9991 select_text(col, row, col, row);
9992 }
9993
9994 static void
remove_process_timeout_source(void)9995 remove_process_timeout_source(void)
9996 {
9997 if (process_timeout_tag == 0)
9998 return;
9999
10000 _vte_debug_print(VTE_DEBUG_TIMEOUT, "Removing process timeout\n");
10001 g_source_remove (process_timeout_tag);
10002 process_timeout_tag = 0;
10003 }
10004
10005 static void
add_update_timeout(vte::terminal::Terminal * that)10006 add_update_timeout(vte::terminal::Terminal* that)
10007 {
10008 if (update_timeout_tag == 0) {
10009 _vte_debug_print (VTE_DEBUG_TIMEOUT,
10010 "Starting update timeout\n");
10011 update_timeout_tag =
10012 g_timeout_add_full (GDK_PRIORITY_REDRAW,
10013 VTE_UPDATE_TIMEOUT,
10014 update_timeout, NULL,
10015 NULL);
10016 }
10017 if (!in_process_timeout) {
10018 remove_process_timeout_source();
10019 }
10020 if (that->m_active_terminals_link == nullptr) {
10021 _vte_debug_print (VTE_DEBUG_TIMEOUT,
10022 "Adding terminal to active list\n");
10023 that->m_active_terminals_link = g_active_terminals =
10024 g_list_prepend(g_active_terminals, that);
10025 }
10026 }
10027
10028 void
reset_update_rects()10029 Terminal::reset_update_rects()
10030 {
10031 g_array_set_size(m_update_rects, 0);
10032 m_invalidated_all = FALSE;
10033 }
10034
10035 static bool
remove_from_active_list(vte::terminal::Terminal * that)10036 remove_from_active_list(vte::terminal::Terminal* that)
10037 {
10038 if (that->m_active_terminals_link == nullptr ||
10039 that->m_update_rects->len != 0)
10040 return false;
10041
10042 _vte_debug_print(VTE_DEBUG_TIMEOUT, "Removing terminal from active list\n");
10043 g_active_terminals = g_list_delete_link(g_active_terminals, that->m_active_terminals_link);
10044 that->m_active_terminals_link = nullptr;
10045 return true;
10046 }
10047
10048 static void
stop_processing(vte::terminal::Terminal * that)10049 stop_processing(vte::terminal::Terminal* that)
10050 {
10051 if (!remove_from_active_list(that))
10052 return;
10053
10054 if (g_active_terminals != nullptr)
10055 return;
10056
10057 if (!in_process_timeout) {
10058 remove_process_timeout_source();
10059 }
10060 if (in_update_timeout == FALSE &&
10061 update_timeout_tag != 0) {
10062 _vte_debug_print(VTE_DEBUG_TIMEOUT, "Removing update timeout\n");
10063 g_source_remove (update_timeout_tag);
10064 update_timeout_tag = 0;
10065 }
10066 }
10067
10068 static void
remove_update_timeout(vte::terminal::Terminal * that)10069 remove_update_timeout(vte::terminal::Terminal* that)
10070 {
10071 that->reset_update_rects();
10072 stop_processing(that);
10073 }
10074
10075 static void
add_process_timeout(vte::terminal::Terminal * that)10076 add_process_timeout(vte::terminal::Terminal* that)
10077 {
10078 _vte_debug_print(VTE_DEBUG_TIMEOUT,
10079 "Adding terminal to active list\n");
10080 that->m_active_terminals_link = g_active_terminals =
10081 g_list_prepend(g_active_terminals, that);
10082 if (update_timeout_tag == 0 &&
10083 process_timeout_tag == 0) {
10084 _vte_debug_print(VTE_DEBUG_TIMEOUT,
10085 "Starting process timeout\n");
10086 process_timeout_tag =
10087 g_timeout_add (VTE_DISPLAY_TIMEOUT,
10088 process_timeout, NULL);
10089 }
10090 }
10091
10092 void
start_processing()10093 Terminal::start_processing()
10094 {
10095 if (!is_processing())
10096 add_process_timeout(this);
10097 }
10098
10099 void
emit_pending_signals()10100 Terminal::emit_pending_signals()
10101 {
10102 auto const freezer = vte::glib::FreezeObjectNotify{m_terminal};
10103
10104 emit_adjustment_changed();
10105
10106 if (m_pending_changes & vte::to_integral(PendingChanges::TITLE)) {
10107 if (m_window_title != m_window_title_pending) {
10108 m_window_title.swap(m_window_title_pending);
10109
10110 _vte_debug_print(VTE_DEBUG_SIGNALS,
10111 "Emitting `window-title-changed'.\n");
10112 g_signal_emit(freezer.get(), signals[SIGNAL_WINDOW_TITLE_CHANGED], 0);
10113 g_object_notify_by_pspec(freezer.get(), pspecs[PROP_WINDOW_TITLE]);
10114 }
10115
10116 m_window_title_pending.clear();
10117 }
10118
10119 if (m_pending_changes & vte::to_integral(PendingChanges::CWD)) {
10120 if (m_current_directory_uri != m_current_directory_uri_pending) {
10121 m_current_directory_uri.swap(m_current_directory_uri_pending);
10122
10123 _vte_debug_print(VTE_DEBUG_SIGNALS,
10124 "Emitting `current-directory-uri-changed'.\n");
10125 g_signal_emit(freezer.get(), signals[SIGNAL_CURRENT_DIRECTORY_URI_CHANGED], 0);
10126 g_object_notify_by_pspec(freezer.get(), pspecs[PROP_CURRENT_DIRECTORY_URI]);
10127 }
10128
10129 m_current_directory_uri_pending.clear();
10130 }
10131
10132 if (m_pending_changes & vte::to_integral(PendingChanges::CWF)) {
10133 if (m_current_file_uri != m_current_file_uri_pending) {
10134 m_current_file_uri.swap(m_current_file_uri_pending);
10135
10136 _vte_debug_print(VTE_DEBUG_SIGNALS,
10137 "Emitting `current-file-uri-changed'.\n");
10138 g_signal_emit(freezer.get(), signals[SIGNAL_CURRENT_FILE_URI_CHANGED], 0);
10139 g_object_notify_by_pspec(freezer.get(), pspecs[PROP_CURRENT_FILE_URI]);
10140 }
10141
10142 m_current_file_uri_pending.clear();
10143 }
10144
10145 m_pending_changes = 0;
10146
10147 /* Flush any pending "inserted" signals. */
10148
10149 if (m_cursor_moved_pending) {
10150 _vte_debug_print(VTE_DEBUG_SIGNALS,
10151 "Emitting `cursor-moved'.\n");
10152 g_signal_emit(freezer.get(), signals[SIGNAL_CURSOR_MOVED], 0);
10153 m_cursor_moved_pending = false;
10154 }
10155 if (m_text_modified_flag) {
10156 _vte_debug_print(VTE_DEBUG_SIGNALS,
10157 "Emitting buffered `text-modified'.\n");
10158 emit_text_modified();
10159 m_text_modified_flag = false;
10160 }
10161 if (m_text_inserted_flag) {
10162 _vte_debug_print(VTE_DEBUG_SIGNALS,
10163 "Emitting buffered `text-inserted'\n");
10164 emit_text_inserted();
10165 m_text_inserted_flag = false;
10166 }
10167 if (m_text_deleted_flag) {
10168 _vte_debug_print(VTE_DEBUG_SIGNALS,
10169 "Emitting buffered `text-deleted'\n");
10170 emit_text_deleted();
10171 m_text_deleted_flag = false;
10172 }
10173 if (m_contents_changed_pending) {
10174 /* Update hyperlink and dingus match set. */
10175 match_contents_clear();
10176 if (m_mouse_cursor_over_widget) {
10177 hyperlink_hilite_update();
10178 match_hilite_update();
10179 }
10180
10181 _vte_debug_print(VTE_DEBUG_SIGNALS,
10182 "Emitting `contents-changed'.\n");
10183 g_signal_emit(m_terminal, signals[SIGNAL_CONTENTS_CHANGED], 0);
10184 m_contents_changed_pending = false;
10185 }
10186 if (m_bell_pending) {
10187 auto const timestamp = g_get_monotonic_time();
10188 if ((timestamp - m_bell_timestamp) >= VTE_BELL_MINIMUM_TIME_DIFFERENCE) {
10189 beep();
10190 emit_bell();
10191
10192 m_bell_timestamp = timestamp;
10193 }
10194
10195 m_bell_pending = false;
10196 }
10197
10198 auto const eos = m_eos_pending;
10199 if (m_eos_pending) {
10200 queue_eof();
10201 m_eos_pending = false;
10202
10203 unset_pty();
10204 }
10205
10206 if (m_child_exited_after_eos_pending && eos) {
10207 /* The signal handler could destroy the terminal, so send the signal on idle */
10208 queue_child_exited();
10209 m_child_exited_after_eos_pending = false;
10210 }
10211 }
10212
10213 void
time_process_incoming()10214 Terminal::time_process_incoming()
10215 {
10216 g_timer_reset(process_timer);
10217 process_incoming();
10218 auto elapsed = g_timer_elapsed(process_timer, NULL) * 1000;
10219 gssize target = VTE_MAX_PROCESS_TIME / elapsed * m_input_bytes;
10220 m_max_input_bytes = (m_max_input_bytes + target) / 2;
10221 }
10222
10223 bool
process(bool emit_adj_changed)10224 Terminal::process(bool emit_adj_changed)
10225 {
10226 if (pty()) {
10227 if (m_pty_input_active ||
10228 m_pty_input_source == 0) {
10229 m_pty_input_active = false;
10230 /* Do one read directly. FIXMEchpe: Why? */
10231 pty_io_read(pty()->fd(), G_IO_IN);
10232 }
10233 connect_pty_read();
10234 }
10235 if (emit_adj_changed)
10236 emit_adjustment_changed();
10237
10238 bool is_active = !m_incoming_queue.empty();
10239 if (is_active) {
10240 if (VTE_MAX_PROCESS_TIME) {
10241 time_process_incoming();
10242 } else {
10243 process_incoming();
10244 }
10245 m_input_bytes = 0;
10246 } else
10247 emit_pending_signals();
10248
10249 return is_active;
10250 }
10251
10252
10253 /* We need to keep a reference to the terminals in the
10254 * g_active_terminals list while iterating over it, since
10255 * in some language bindings the callbacks we emit
10256 * during processing may cause their GC to run, causing
10257 * later elements in this list to be removed from the list.
10258 * See issue vte#270.
10259 */
10260
10261 static void
unref_active_terminals(GList * list)10262 unref_active_terminals(GList* list)
10263 {
10264 g_list_free_full(list, GDestroyNotify(g_object_unref));
10265 }
10266
10267 static auto
ref_active_terminals()10268 ref_active_terminals() noexcept
10269 {
10270 GList* list = nullptr;
10271 for (auto l = g_active_terminals; l != nullptr; l = l->next) {
10272 auto that = reinterpret_cast<vte::terminal::Terminal*>(l->data);
10273 list = g_list_prepend(list, g_object_ref(that->vte_terminal()));
10274 }
10275
10276 return std::unique_ptr<GList, decltype(&unref_active_terminals)>{list, &unref_active_terminals};
10277 }
10278
10279 /* This function is called after DISPLAY_TIMEOUT ms.
10280 * It makes sure initial output is never delayed by more than DISPLAY_TIMEOUT
10281 */
10282 static gboolean
process_timeout(gpointer data)10283 process_timeout (gpointer data) noexcept
10284 try
10285 {
10286 GList *l, *next;
10287 gboolean again;
10288
10289 in_process_timeout = TRUE;
10290
10291 _vte_debug_print (VTE_DEBUG_WORK, "<");
10292 _vte_debug_print (VTE_DEBUG_TIMEOUT,
10293 "Process timeout: %d active\n",
10294 g_list_length(g_active_terminals));
10295
10296 auto death_grip = ref_active_terminals();
10297
10298 for (l = g_active_terminals; l != NULL; l = next) {
10299 auto that = reinterpret_cast<vte::terminal::Terminal*>(l->data);
10300 bool active;
10301
10302 next = l->next;
10303
10304 if (l != g_active_terminals) {
10305 _vte_debug_print (VTE_DEBUG_WORK, "T");
10306 }
10307
10308 // FIXMEchpe find out why we don't emit_adjustment_changed() here!!
10309 active = that->process(false);
10310
10311 if (!active) {
10312 remove_from_active_list(that);
10313 }
10314 }
10315
10316 _vte_debug_print (VTE_DEBUG_WORK, ">");
10317
10318 if (g_active_terminals != nullptr && update_timeout_tag == 0) {
10319 again = TRUE;
10320 } else {
10321 _vte_debug_print(VTE_DEBUG_TIMEOUT,
10322 "Stopping process timeout\n");
10323 process_timeout_tag = 0;
10324 again = FALSE;
10325 }
10326
10327 in_process_timeout = FALSE;
10328
10329 if (again) {
10330 /* Force us to relinquish the CPU as the child is running
10331 * at full tilt and making us run to keep up...
10332 */
10333 g_usleep (0);
10334 } else if (update_timeout_tag == 0) {
10335 /* otherwise free up memory used to capture incoming data */
10336 vte::base::Chunk::prune();
10337 }
10338
10339 return again;
10340 }
10341 catch (...)
10342 {
10343 vte::log_exception();
10344 return true; // false?
10345 }
10346
10347 bool
invalidate_dirty_rects_and_process_updates()10348 Terminal::invalidate_dirty_rects_and_process_updates()
10349 {
10350 if (G_UNLIKELY(!widget_realized()))
10351 return false;
10352
10353 if (G_UNLIKELY (!m_update_rects->len))
10354 return false;
10355
10356 auto region = cairo_region_create();
10357 auto n_rects = m_update_rects->len;
10358 for (guint i = 0; i < n_rects; i++) {
10359 cairo_rectangle_int_t *rect = &g_array_index(m_update_rects, cairo_rectangle_int_t, i);
10360 cairo_region_union_rectangle(region, rect);
10361 }
10362 g_array_set_size(m_update_rects, 0);
10363 m_invalidated_all = false;
10364
10365 auto allocation = get_allocated_rect();
10366 cairo_region_translate(region,
10367 allocation.x + m_padding.left,
10368 allocation.y + m_padding.top);
10369
10370 /* and perform the merge with the window visible area */
10371 gtk_widget_queue_draw_region(m_widget, region);
10372 cairo_region_destroy (region);
10373
10374 return true;
10375 }
10376
10377 static gboolean
update_repeat_timeout(gpointer data)10378 update_repeat_timeout (gpointer data)
10379 {
10380 GList *l, *next;
10381 bool again;
10382
10383 in_update_timeout = TRUE;
10384
10385 _vte_debug_print (VTE_DEBUG_WORK, "[");
10386 _vte_debug_print (VTE_DEBUG_TIMEOUT,
10387 "Repeat timeout: %d active\n",
10388 g_list_length(g_active_terminals));
10389
10390 auto death_grip = ref_active_terminals();
10391
10392 for (l = g_active_terminals; l != NULL; l = next) {
10393 auto that = reinterpret_cast<vte::terminal::Terminal*>(l->data);
10394
10395 next = l->next;
10396
10397 if (l != g_active_terminals) {
10398 _vte_debug_print (VTE_DEBUG_WORK, "T");
10399 }
10400
10401 that->process(true);
10402
10403 again = that->invalidate_dirty_rects_and_process_updates();
10404 if (!again) {
10405 remove_from_active_list(that);
10406 }
10407 }
10408
10409 _vte_debug_print (VTE_DEBUG_WORK, "]");
10410
10411 /* We only stop the timer if no update request was received in this
10412 * past cycle. Technically, always stop this timer object and maybe
10413 * reinstall a new one because we need to delay by the amount of time
10414 * it took to repaint the screen: bug 730732.
10415 */
10416 if (g_active_terminals == nullptr) {
10417 _vte_debug_print(VTE_DEBUG_TIMEOUT,
10418 "Stopping update timeout\n");
10419 update_timeout_tag = 0;
10420 again = false;
10421 } else {
10422 update_timeout_tag =
10423 g_timeout_add_full (G_PRIORITY_DEFAULT_IDLE,
10424 VTE_UPDATE_REPEAT_TIMEOUT,
10425 update_repeat_timeout, NULL,
10426 NULL);
10427 again = true;
10428 }
10429
10430 in_update_timeout = FALSE;
10431
10432 if (again) {
10433 /* Force us to relinquish the CPU as the child is running
10434 * at full tilt and making us run to keep up...
10435 */
10436 g_usleep (0);
10437 } else {
10438 /* otherwise free up memory used to capture incoming data */
10439 vte::base::Chunk::prune();
10440 }
10441
10442 return FALSE; /* If we need to go again, we already have a new timer for that. */
10443 }
10444
10445 static gboolean
update_timeout(gpointer data)10446 update_timeout (gpointer data) noexcept
10447 try
10448 {
10449 GList *l, *next;
10450
10451 in_update_timeout = TRUE;
10452
10453 _vte_debug_print (VTE_DEBUG_WORK, "{");
10454 _vte_debug_print (VTE_DEBUG_TIMEOUT,
10455 "Update timeout: %d active\n",
10456 g_list_length(g_active_terminals));
10457
10458 remove_process_timeout_source();
10459
10460 for (l = g_active_terminals; l != NULL; l = next) {
10461 auto that = reinterpret_cast<vte::terminal::Terminal*>(l->data);
10462
10463 next = l->next;
10464
10465 if (l != g_active_terminals) {
10466 _vte_debug_print (VTE_DEBUG_WORK, "T");
10467 }
10468
10469 that->process(true);
10470
10471 that->invalidate_dirty_rects_and_process_updates();
10472 }
10473
10474 _vte_debug_print (VTE_DEBUG_WORK, "}");
10475
10476 /* Set a timer such that we do not invalidate for a while. */
10477 /* This limits the number of times we draw to ~40fps. */
10478 update_timeout_tag =
10479 g_timeout_add_full (G_PRIORITY_DEFAULT_IDLE,
10480 VTE_UPDATE_REPEAT_TIMEOUT,
10481 update_repeat_timeout, NULL,
10482 NULL);
10483 in_update_timeout = FALSE;
10484
10485 return FALSE;
10486 }
10487 catch (...)
10488 {
10489 vte::log_exception();
10490 return true; // false?
10491 }
10492
10493 bool
write_contents_sync(GOutputStream * stream,VteWriteFlags flags,GCancellable * cancellable,GError ** error)10494 Terminal::write_contents_sync (GOutputStream *stream,
10495 VteWriteFlags flags,
10496 GCancellable *cancellable,
10497 GError **error)
10498 {
10499 return _vte_ring_write_contents (m_screen->row_data,
10500 stream, flags,
10501 cancellable, error);
10502 }
10503
10504 /*
10505 * Buffer search
10506 */
10507
10508 /* TODO Add properties & signals */
10509
10510 /*
10511 * Terminal::search_set_regex:
10512 * @regex: (allow-none): a #VteRegex, or %nullptr
10513 * @flags: PCRE2 match flags, or 0
10514 *
10515 * Sets the regex to search for. Unsets the search regex when passed %nullptr.
10516 */
10517 bool
search_set_regex(vte::base::RefPtr<vte::base::Regex> && regex,uint32_t flags)10518 Terminal::search_set_regex (vte::base::RefPtr<vte::base::Regex>&& regex,
10519 uint32_t flags)
10520 {
10521 if (regex == m_search_regex &&
10522 flags == m_search_regex_match_flags)
10523 return false;
10524
10525 m_search_regex = std::move(regex);
10526 m_search_regex_match_flags = flags;
10527
10528 invalidate_all();
10529
10530 return true;
10531 }
10532
10533 bool
search_set_wrap_around(bool wrap)10534 Terminal::search_set_wrap_around(bool wrap)
10535 {
10536 if (wrap == m_search_wrap_around)
10537 return false;
10538
10539 m_search_wrap_around = wrap;
10540 return true;
10541 }
10542
10543 bool
search_rows(pcre2_match_context_8 * match_context,pcre2_match_data_8 * match_data,vte::grid::row_t start_row,vte::grid::row_t end_row,bool backward)10544 Terminal::search_rows(pcre2_match_context_8 *match_context,
10545 pcre2_match_data_8 *match_data,
10546 vte::grid::row_t start_row,
10547 vte::grid::row_t end_row,
10548 bool backward)
10549 {
10550 int start, end;
10551 long start_col, end_col;
10552 VteCharAttributes *ca;
10553 GArray *attrs;
10554 gdouble value, page_size;
10555
10556 auto row_text = get_text(start_row, 0,
10557 end_row, 0,
10558 false /* block */,
10559 true /* wrap */,
10560 nullptr);
10561
10562 int (* match_fn) (const pcre2_code_8 *,
10563 PCRE2_SPTR8, PCRE2_SIZE, PCRE2_SIZE, uint32_t,
10564 pcre2_match_data_8 *, pcre2_match_context_8 *);
10565 gsize *ovector, so, eo;
10566 int r;
10567
10568 if (m_search_regex->jited())
10569 match_fn = pcre2_jit_match_8;
10570 else
10571 match_fn = pcre2_match_8;
10572
10573 r = match_fn(m_search_regex->code(),
10574 (PCRE2_SPTR8)row_text->str, row_text->len , /* subject, length */
10575 0, /* start offset */
10576 m_search_regex_match_flags |
10577 PCRE2_NO_UTF_CHECK | PCRE2_NOTEMPTY | PCRE2_PARTIAL_SOFT /* FIXME: HARD? */,
10578 match_data,
10579 match_context);
10580
10581 if (r == PCRE2_ERROR_NOMATCH) {
10582 g_string_free (row_text, TRUE);
10583 return false;
10584 }
10585 // FIXME: handle partial matches (PCRE2_ERROR_PARTIAL)
10586 if (r < 0) {
10587 g_string_free (row_text, TRUE);
10588 return false;
10589 }
10590
10591 ovector = pcre2_get_ovector_pointer_8(match_data);
10592 so = ovector[0];
10593 eo = ovector[1];
10594 if (G_UNLIKELY(so == PCRE2_UNSET || eo == PCRE2_UNSET)) {
10595 g_string_free (row_text, TRUE);
10596 return false;
10597 }
10598
10599 start = so;
10600 end = eo;
10601
10602 /* Fetch text again, with attributes */
10603 g_string_free(row_text, TRUE);
10604 if (!m_search_attrs)
10605 m_search_attrs = g_array_new (FALSE, TRUE, sizeof (VteCharAttributes));
10606 attrs = m_search_attrs;
10607 row_text = get_text(start_row, 0,
10608 end_row, 0,
10609 false /* block */,
10610 true /* wrap */,
10611 attrs);
10612
10613 ca = &g_array_index (attrs, VteCharAttributes, start);
10614 start_row = ca->row;
10615 start_col = ca->column;
10616 ca = &g_array_index (attrs, VteCharAttributes, end - 1);
10617 end_row = ca->row;
10618 end_col = ca->column + ca->columns;
10619
10620 g_string_free (row_text, TRUE);
10621
10622 select_text(start_col, start_row, end_col, end_row);
10623 /* Quite possibly the math here should not access adjustment directly... */
10624 value = gtk_adjustment_get_value(m_vadjustment.get());
10625 page_size = gtk_adjustment_get_page_size(m_vadjustment.get());
10626 if (backward) {
10627 if (end_row < value || end_row > value + page_size - 1)
10628 queue_adjustment_value_changed_clamped(end_row - page_size + 1);
10629 } else {
10630 if (start_row < value || start_row > value + page_size - 1)
10631 queue_adjustment_value_changed_clamped(start_row);
10632 }
10633
10634 return true;
10635 }
10636
10637 bool
search_rows_iter(pcre2_match_context_8 * match_context,pcre2_match_data_8 * match_data,vte::grid::row_t start_row,vte::grid::row_t end_row,bool backward)10638 Terminal::search_rows_iter(pcre2_match_context_8 *match_context,
10639 pcre2_match_data_8 *match_data,
10640 vte::grid::row_t start_row,
10641 vte::grid::row_t end_row,
10642 bool backward)
10643 {
10644 const VteRowData *row;
10645 long iter_start_row, iter_end_row;
10646
10647 if (backward) {
10648 iter_start_row = end_row;
10649 while (iter_start_row > start_row) {
10650 iter_end_row = iter_start_row;
10651
10652 do {
10653 iter_start_row--;
10654 row = find_row_data(iter_start_row);
10655 } while (row && row->attr.soft_wrapped);
10656
10657 if (search_rows(match_context, match_data,
10658 iter_start_row, iter_end_row, backward))
10659 return true;
10660 }
10661 } else {
10662 iter_end_row = start_row;
10663 while (iter_end_row < end_row) {
10664 iter_start_row = iter_end_row;
10665
10666 do {
10667 row = find_row_data(iter_end_row);
10668 iter_end_row++;
10669 } while (row && row->attr.soft_wrapped);
10670
10671 if (search_rows(match_context, match_data,
10672 iter_start_row, iter_end_row, backward))
10673 return true;
10674 }
10675 }
10676
10677 return false;
10678 }
10679
10680 bool
search_find(bool backward)10681 Terminal::search_find (bool backward)
10682 {
10683 vte::grid::row_t buffer_start_row, buffer_end_row;
10684 vte::grid::row_t last_start_row, last_end_row;
10685 bool match_found = true;
10686
10687 if (!m_search_regex)
10688 return false;
10689
10690 /* TODO
10691 * Currently We only find one result per extended line, and ignore columns
10692 * Moreover, the whole search thing is implemented very inefficiently.
10693 */
10694
10695 auto match_context = create_match_context();
10696 auto match_data = vte::take_freeable(pcre2_match_data_create_8(256 /* should be plenty */,
10697 nullptr /* general context */));
10698
10699 buffer_start_row = _vte_ring_delta (m_screen->row_data);
10700 buffer_end_row = _vte_ring_next (m_screen->row_data);
10701
10702 if (!m_selection_resolved.empty()) {
10703 last_start_row = m_selection_resolved.start_row();
10704 last_end_row = m_selection_resolved.end_row() + 1;
10705 } else {
10706 last_start_row = m_screen->scroll_delta + m_row_count;
10707 last_end_row = m_screen->scroll_delta;
10708 }
10709 last_start_row = MAX (buffer_start_row, last_start_row);
10710 last_end_row = MIN (buffer_end_row, last_end_row);
10711
10712 /* If search fails, we make an empty selection at the last searched
10713 * position... */
10714 if (backward) {
10715 if (search_rows_iter(match_context.get(), match_data.get(),
10716 buffer_start_row, last_start_row, backward))
10717 goto found;
10718 if (m_search_wrap_around &&
10719 search_rows_iter(match_context.get(), match_data.get(),
10720 last_end_row, buffer_end_row, backward))
10721 goto found;
10722 if (!m_selection_resolved.empty()) {
10723 if (m_search_wrap_around)
10724 select_empty(m_selection_resolved.start_column(), m_selection_resolved.start_row());
10725 else
10726 select_empty(-1, buffer_start_row - 1);
10727 }
10728 match_found = false;
10729 } else {
10730 if (search_rows_iter(match_context.get(), match_data.get(),
10731 last_end_row, buffer_end_row, backward))
10732 goto found;
10733 if (m_search_wrap_around &&
10734 search_rows_iter(match_context.get(), match_data.get(),
10735 buffer_start_row, last_start_row, backward))
10736 goto found;
10737 if (!m_selection_resolved.empty()) {
10738 if (m_search_wrap_around)
10739 select_empty(m_selection_resolved.end_column(), m_selection_resolved.end_row());
10740 else
10741 select_empty(0, buffer_end_row);
10742 }
10743 match_found = false;
10744 }
10745
10746 found:
10747
10748 return match_found;
10749 }
10750
10751 /*
10752 * Terminal::set_input_enabled:
10753 * @enabled: whether to enable user input
10754 *
10755 * Enables or disables user input. When user input is disabled,
10756 * the terminal's child will not receive any key press, or mouse button
10757 * press or motion events sent to it.
10758 *
10759 * Returns: %true iff the setting changed
10760 */
10761 bool
set_input_enabled(bool enabled)10762 Terminal::set_input_enabled (bool enabled)
10763 {
10764 if (enabled == m_input_enabled)
10765 return false;
10766
10767 m_input_enabled = enabled;
10768
10769 auto context = gtk_widget_get_style_context(m_widget);
10770
10771 /* FIXME: maybe hide cursor when input disabled, too? */
10772
10773 if (enabled) {
10774 if (m_has_focus)
10775 widget()->im_focus_in();
10776
10777 gtk_style_context_remove_class (context, GTK_STYLE_CLASS_READ_ONLY);
10778 } else {
10779 im_reset();
10780 if (m_has_focus)
10781 widget()->im_focus_out();
10782
10783 disconnect_pty_write();
10784 _vte_byte_array_clear(m_outgoing);
10785
10786 gtk_style_context_add_class (context, GTK_STYLE_CLASS_READ_ONLY);
10787 }
10788
10789 return true;
10790 }
10791
10792 std::optional<std::vector<char32_t>>
process_word_char_exceptions(std::string_view str_view) const10793 Terminal::process_word_char_exceptions(std::string_view str_view) const noexcept
10794 {
10795 auto str = str_view.data();
10796
10797 auto array = std::vector<char32_t>{};
10798 array.reserve(g_utf8_strlen(str, -1));
10799
10800 for (auto const* p = str; *p; p = g_utf8_next_char(p)) {
10801 auto const c = g_utf8_get_char(p);
10802
10803 /* For forward compatibility reasons, we skip
10804 * characters that aren't supposed to be here,
10805 * instead of erroring out.
10806 */
10807 /* '-' must only be used* at the start of the string */
10808 if (c == (gunichar)'-' && p != str)
10809 continue;
10810 if (!g_unichar_isgraph(c))
10811 continue;
10812 if (g_unichar_isspace(c))
10813 continue;
10814 if (g_unichar_isalnum(c))
10815 continue;
10816
10817 array.push_back(c);
10818 }
10819
10820 /* Sort the result since we want to use bsearch on it */
10821 std::sort(std::begin(array), std::end(array));
10822
10823 /* Check that no character occurs twice */
10824 for (size_t i = 1; i < array.size(); ++i) {
10825 if (array[i-1] != array[i])
10826 continue;
10827
10828 return std::nullopt;
10829 }
10830
10831 #if 0
10832 /* Debug */
10833 for (auto const c : array) {
10834 char utf[7];
10835 utf[g_unichar_to_utf8(c, utf)] = '\0';
10836 g_printerr("Word char exception: U+%04X %s\n", c, utf);
10837 }
10838 #endif
10839
10840 return array;
10841 }
10842
10843 /*
10844 * Terminal::set_word_char_exceptions:
10845 * @exceptions: a string of ASCII punctuation characters, or %nullptr
10846 *
10847 * With this function you can provide a set of characters which will
10848 * be considered parts of a word when doing word-wise selection, in
10849 * addition to the default which only considers alphanumeric characters
10850 * part of a word.
10851 *
10852 * The characters in @exceptions must be non-alphanumeric, each character
10853 * must occur only once, and if @exceptions contains the character
10854 * U+002D HYPHEN-MINUS, it must be at the start of the string.
10855 *
10856 * Use %nullptr to reset the set of exception characters to the default.
10857 *
10858 * Returns: %true if the word char exceptions changed
10859 */
10860 bool
set_word_char_exceptions(std::optional<std::string_view> stropt)10861 Terminal::set_word_char_exceptions(std::optional<std::string_view> stropt)
10862 {
10863 if (auto array = process_word_char_exceptions(stropt ? stropt.value() : WORD_CHAR_EXCEPTIONS_DEFAULT)) {
10864 m_word_char_exceptions = *array;
10865 return true;
10866 }
10867
10868 return false;
10869 }
10870
10871 void
set_clear_background(bool setting)10872 Terminal::set_clear_background(bool setting)
10873 {
10874 if (m_clear_background == setting)
10875 return;
10876
10877 m_clear_background = setting;
10878 invalidate_all();
10879 }
10880
10881 } // namespace terminal
10882 } // namespace vte
10883