Line data Source code
1 : /*
2 : Mosh: the mobile shell
3 : Copyright 2012 Keith Winstein
4 :
5 : This program is free software: you can redistribute it and/or modify
6 : it under the terms of the GNU General Public License as published by
7 : the Free Software Foundation, either version 3 of the License, or
8 : (at your option) any later version.
9 :
10 : This program 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 General Public License for more details.
14 :
15 : You should have received a copy of the GNU General Public License
16 : along with this program. If not, see <http://www.gnu.org/licenses/>.
17 :
18 : In addition, as a special exception, the copyright holders give
19 : permission to link the code of portions of this program with the
20 : OpenSSL library under certain conditions as described in each
21 : individual source file, and distribute linked combinations including
22 : the two.
23 :
24 : You must obey the GNU General Public License in all respects for all
25 : of the code used other than OpenSSL. If you modify file(s) with this
26 : exception, you may extend this exception to your version of the
27 : file(s), but you are not obligated to do so. If you do not wish to do
28 : so, delete this exception statement from your version. If you delete
29 : this exception statement from all source files in the program, then
30 : also delete it here.
31 : */
32 :
33 : #include <stdio.h>
34 :
35 : #include "terminaldisplay.h"
36 : #include "terminalframebuffer.h"
37 :
38 : using namespace Terminal;
39 :
40 : /* Print a new "frame" to the terminal, using ANSI/ECMA-48 escape codes. */
41 :
42 399034 : static const Renditions & initial_rendition( void )
43 : {
44 399034 : const static Renditions blank = Renditions( 0 );
45 399034 : return blank;
46 : }
47 :
48 448 : std::string Display::open() const
49 : {
50 1344 : return std::string( smcup ? smcup : "" ) + std::string( "\033[?1h" );
51 : }
52 :
53 448 : std::string Display::close() const
54 : {
55 1344 : return std::string( "\033[?1l\033[0m\033[?25h"
56 : "\033[?1003l\033[?1002l\033[?1001l\033[?1000l"
57 : "\033[?1015l\033[?1006l\033[?1005l" ) +
58 1792 : std::string( rmcup ? rmcup : "" );
59 : }
60 :
61 24199 : std::string Display::new_frame( bool initialized, const Framebuffer &last, const Framebuffer &f ) const
62 : {
63 24199 : FrameState frame( last );
64 :
65 24199 : char tmp[ 64 ];
66 :
67 : /* has bell been rung? */
68 24199 : if ( f.get_bell_count() != frame.last_frame.get_bell_count() ) {
69 0 : frame.append( '\007' );
70 : }
71 24199 : typedef Terminal::Framebuffer::title_type title_type;
72 :
73 : /* has icon name or window title changed? */
74 24199 : if ( has_title && f.is_title_initialized() &&
75 : ( (!initialized)
76 0 : || (f.get_icon_name() != frame.last_frame.get_icon_name())
77 0 : || (f.get_window_title() != frame.last_frame.get_window_title()) ) ) {
78 : /* set icon name and window title */
79 0 : if ( f.get_icon_name() == f.get_window_title() ) {
80 : /* write combined Icon Name and Window Title */
81 0 : frame.append( "\033]0;" );
82 0 : const title_type &window_title( f.get_window_title() );
83 0 : for ( title_type::const_iterator i = window_title.begin();
84 0 : i != window_title.end();
85 0 : i++ ) {
86 0 : frame.append( *i );
87 : }
88 0 : frame.append( '\007' );
89 : /* ST is more correct, but BEL more widely supported */
90 : } else {
91 : /* write Icon Name */
92 0 : frame.append( "\033]1;" );
93 0 : const title_type &icon_name( f.get_icon_name() );
94 0 : for ( title_type::const_iterator i = icon_name.begin();
95 0 : i != icon_name.end();
96 0 : i++ ) {
97 0 : frame.append( *i );
98 : }
99 0 : frame.append( '\007' );
100 :
101 0 : frame.append( "\033]2;" );
102 0 : const title_type &window_title( f.get_window_title() );
103 0 : for ( title_type::const_iterator i = window_title.begin();
104 0 : i != window_title.end();
105 0 : i++ ) {
106 0 : frame.append( *i );
107 : }
108 0 : frame.append( '\007' );
109 : }
110 :
111 : }
112 :
113 : /* has clipboard changed? */
114 24199 : if (f.get_clipboard() != frame.last_frame.get_clipboard()) {
115 0 : frame.append( "\033]52;c;" );
116 0 : const title_type &clipboard( f.get_clipboard() );
117 0 : for ( title_type::const_iterator i = clipboard.begin();
118 0 : i != clipboard.end();
119 0 : i++ ) {
120 0 : frame.append( *i );
121 : }
122 0 : frame.append( '\007' );
123 : }
124 :
125 : /* has reverse video state changed? */
126 24199 : if ( (!initialized)
127 23751 : || (f.ds.reverse_video != frame.last_frame.ds.reverse_video) ) {
128 : /* set reverse video */
129 448 : snprintf( tmp, 64, "\033[?5%c", (f.ds.reverse_video ? 'h' : 'l') );
130 448 : frame.append( tmp );
131 : }
132 :
133 : /* has size changed? */
134 24199 : if ( (!initialized)
135 23751 : || (f.ds.get_width() != frame.last_frame.ds.get_width())
136 47884 : || (f.ds.get_height() != frame.last_frame.ds.get_height()) ) {
137 : /* reset scrolling region */
138 580 : frame.append( "\033[r" );
139 :
140 : /* clear screen */
141 580 : frame.append( "\033[0m\033[H\033[2J" );
142 580 : initialized = false;
143 580 : frame.cursor_x = frame.cursor_y = 0;
144 580 : frame.current_rendition = initial_rendition();
145 : } else {
146 23619 : frame.cursor_x = frame.last_frame.ds.get_cursor_col();
147 23619 : frame.cursor_y = frame.last_frame.ds.get_cursor_row();
148 23619 : frame.current_rendition = frame.last_frame.ds.get_renditions();
149 : }
150 :
151 : /* is cursor visibility initialized? */
152 23619 : if ( !initialized ) {
153 580 : frame.cursor_visible = false;
154 580 : frame.append( "\033[?25l" );
155 : }
156 :
157 24199 : int frame_y = 0;
158 24199 : Framebuffer::row_pointer blank_row;
159 24199 : Framebuffer::rows_type rows( frame.last_frame.get_rows() );
160 : /* Extend rows if we've gotten a resize and new is wider than old */
161 24199 : if ( frame.last_frame.ds.get_width() < f.ds.get_width() ) {
162 900 : for ( Framebuffer::rows_type::iterator p = rows.begin(); p != rows.end(); p++ ) {
163 864 : *p = make_shared<Row>( **p );
164 1728 : (*p)->cells.resize( f.ds.get_width(), Cell( f.ds.get_background_rendition() ) );
165 : }
166 : }
167 : /* Add rows if we've gotten a resize and new is taller than old */
168 24199 : if ( static_cast<int>( rows.size() ) < f.ds.get_height() ) {
169 : // get a proper blank row
170 30 : const size_t w = f.ds.get_width();
171 30 : const color_type c = 0;
172 30 : blank_row = make_shared<Row>( w, c );
173 30 : rows.resize( f.ds.get_height(), blank_row );
174 : }
175 :
176 : /* shortcut -- has display moved up by a certain number of lines? */
177 24199 : if ( initialized ) {
178 391229 : int lines_scrolled = 0;
179 391229 : int scroll_height = 0;
180 :
181 391229 : for ( int row = 0; row < f.ds.get_height(); row++ ) {
182 376608 : const Row *new_row = f.get_row( 0 );
183 376608 : const Row *old_row = &*rows.at( row );
184 376608 : if ( ! ( new_row == old_row || *new_row == *old_row ) ) {
185 367610 : continue;
186 : }
187 : /* if row 0, we're looking at ourselves and probably didn't scroll */
188 8998 : if ( row == 0 ) {
189 : break;
190 : }
191 : /* found a scroll */
192 17613 : lines_scrolled = row;
193 : scroll_height = 1;
194 :
195 : /* how big is the region that was scrolled? */
196 16131 : for ( int region_height = 1;
197 17613 : lines_scrolled + region_height < f.ds.get_height();
198 : region_height++ ) {
199 33326 : if ( *f.get_row( region_height )
200 16663 : == *rows.at( lines_scrolled + region_height ) ) {
201 16131 : scroll_height = region_height + 1;
202 : } else {
203 : break;
204 : }
205 : }
206 :
207 : break;
208 : }
209 :
210 16103 : if ( scroll_height ) {
211 1482 : frame_y = scroll_height;
212 :
213 1482 : if ( lines_scrolled ) {
214 : /* Now we need a proper blank row. */
215 1482 : if ( blank_row.get() == NULL ) {
216 1482 : const size_t w = f.ds.get_width();
217 1482 : const color_type c = 0;
218 1482 : blank_row = make_shared<Row>( w, c );
219 : }
220 1482 : frame.update_rendition( initial_rendition(), true );
221 :
222 1482 : int top_margin = 0;
223 1482 : int bottom_margin = top_margin + lines_scrolled + scroll_height - 1;
224 :
225 1482 : assert( bottom_margin < f.ds.get_height() );
226 :
227 : /* Common case: if we're already on the bottom line and we're scrolling the whole
228 : * screen, just do a CR and LFs.
229 : */
230 1482 : if ( scroll_height + lines_scrolled == f.ds.get_height()
231 1482 : && frame.cursor_y + 1 == f.ds.get_height() ) {
232 950 : frame.append( '\r' );
233 950 : frame.append( lines_scrolled, '\n' );
234 950 : frame.cursor_x = 0;
235 : } else {
236 : /* set scrolling region */
237 532 : snprintf( tmp, 64, "\033[%d;%dr",
238 : top_margin + 1, bottom_margin + 1);
239 532 : frame.append( tmp );
240 :
241 : /* go to bottom of scrolling region */
242 532 : frame.cursor_x = frame.cursor_y = -1;
243 532 : frame.append_silent_move( bottom_margin, 0 );
244 :
245 : /* scroll */
246 532 : frame.append( lines_scrolled, '\n' );
247 :
248 : /* reset scrolling region */
249 532 : frame.append( "\033[r" );
250 : /* invalidate cursor position after unsetting scrolling region */
251 532 : frame.cursor_x = frame.cursor_y = -1;
252 : }
253 :
254 : /* do the move in our local index */
255 36441 : for ( int i = top_margin; i <= bottom_margin; i++ ) {
256 34959 : if ( i + lines_scrolled <= bottom_margin ) {
257 17613 : rows.at( i ) = rows.at( i + lines_scrolled );
258 : } else {
259 52305 : rows.at( i ) = blank_row;
260 : }
261 : }
262 : }
263 : }
264 : }
265 :
266 : /* Now update the display, row by row */
267 : bool wrap = false;
268 585602 : for ( ; frame_y < f.ds.get_height(); frame_y++ ) {
269 561403 : wrap = put_row( initialized, frame, f, frame_y, *rows.at( frame_y ), wrap );
270 : }
271 :
272 : /* has cursor location changed? */
273 24199 : if ( (!initialized)
274 23619 : || (f.ds.get_cursor_row() != frame.cursor_y)
275 42837 : || (f.ds.get_cursor_col() != frame.cursor_x) ) {
276 11077 : frame.append_move( f.ds.get_cursor_row(), f.ds.get_cursor_col() );
277 : }
278 :
279 : /* has cursor visibility changed? */
280 24199 : if ( (!initialized)
281 23619 : || (f.ds.cursor_visible != frame.cursor_visible) ) {
282 14925 : if ( f.ds.cursor_visible ) {
283 14925 : frame.append( "\033[?25h" );
284 : } else {
285 0 : frame.append( "\033[?25l" );
286 : }
287 : }
288 :
289 : /* have renditions changed? */
290 24199 : frame.update_rendition( f.ds.get_renditions(), !initialized );
291 :
292 : /* has bracketed paste mode changed? */
293 24199 : if ( (!initialized)
294 23619 : || (f.ds.bracketed_paste != frame.last_frame.ds.bracketed_paste) ) {
295 600 : frame.append( f.ds.bracketed_paste ? "\033[?2004h" : "\033[?2004l" );
296 : }
297 :
298 : /* has mouse reporting mode changed? */
299 24199 : if ( (!initialized)
300 23619 : || (f.ds.mouse_reporting_mode != frame.last_frame.ds.mouse_reporting_mode) ) {
301 580 : if (f.ds.mouse_reporting_mode == DrawState::MOUSE_REPORTING_NONE) {
302 580 : frame.append("\033[?1003l");
303 580 : frame.append("\033[?1002l");
304 580 : frame.append("\033[?1001l");
305 580 : frame.append("\033[?1000l");
306 : } else {
307 0 : if (frame.last_frame.ds.mouse_reporting_mode != DrawState::MOUSE_REPORTING_NONE) {
308 0 : snprintf(tmp, sizeof(tmp), "\033[?%dl", frame.last_frame.ds.mouse_reporting_mode);
309 0 : frame.append(tmp);
310 : }
311 0 : snprintf(tmp, sizeof(tmp), "\033[?%dh", f.ds.mouse_reporting_mode);
312 0 : frame.append(tmp);
313 : }
314 : }
315 :
316 : /* has mouse focus mode changed? */
317 24199 : if ( (!initialized)
318 23619 : || (f.ds.mouse_focus_event != frame.last_frame.ds.mouse_focus_event) ) {
319 580 : frame.append( f.ds.mouse_focus_event ? "\033[?1004h" : "\033[?1004l" );
320 : }
321 :
322 : /* has mouse encoding mode changed? */
323 24199 : if ( (!initialized)
324 23619 : || (f.ds.mouse_encoding_mode != frame.last_frame.ds.mouse_encoding_mode) ) {
325 580 : if (f.ds.mouse_encoding_mode == DrawState::MOUSE_ENCODING_DEFAULT) {
326 580 : frame.append("\033[?1015l");
327 580 : frame.append("\033[?1006l");
328 580 : frame.append("\033[?1005l");
329 : } else {
330 0 : if (frame.last_frame.ds.mouse_encoding_mode != DrawState::MOUSE_ENCODING_DEFAULT) {
331 0 : snprintf(tmp, sizeof(tmp), "\033[?%dl", frame.last_frame.ds.mouse_encoding_mode);
332 0 : frame.append(tmp);
333 : }
334 0 : snprintf(tmp, sizeof(tmp), "\033[?%dh", f.ds.mouse_encoding_mode);
335 0 : frame.append(tmp);
336 : }
337 : }
338 :
339 24199 : return frame.str;
340 49910 : }
341 :
342 561403 : bool Display::put_row( bool initialized, FrameState &frame, const Framebuffer &f, int frame_y, const Row &old_row, bool wrap ) const
343 : {
344 561403 : char tmp[ 64 ];
345 561403 : int frame_x = 0;
346 :
347 561403 : const Row &row = *f.get_row( frame_y );
348 561403 : const Row::cells_type &cells = row.cells;
349 561403 : const Row::cells_type &old_cells = old_row.cells;
350 :
351 : /* If we're forced to write the first column because of wrap, go ahead and do so. */
352 561403 : if ( wrap ) {
353 1522 : const Cell &cell = cells.at( 0 );
354 1522 : frame.update_rendition( cell.get_renditions() );
355 1522 : frame.append_cell( cell );
356 1522 : frame_x += cell.get_width();
357 1522 : frame.cursor_x += cell.get_width();
358 : }
359 :
360 : /* If rows are the same object, we don't need to do anything at all. */
361 561403 : if (initialized && &row == &old_row ) {
362 : return false;
363 : }
364 :
365 385328 : const bool wrap_this = row.get_wrap();
366 385328 : const int row_width = f.ds.get_width();
367 385328 : int clear_count = 0;
368 385328 : bool wrote_last_cell = false;
369 385328 : Renditions blank_renditions = initial_rendition();
370 :
371 : /* iterate for every cell */
372 31062206 : while ( frame_x < row_width ) {
373 :
374 30676878 : const Cell &cell = cells.at( frame_x );
375 :
376 : /* Does cell need to be drawn? Skip all this. */
377 59698767 : if ( initialized
378 30676878 : && !clear_count
379 30676878 : && ( cell == old_cells.at( frame_x ) ) ) {
380 29021889 : frame_x += cell.get_width();
381 29021889 : continue;
382 : }
383 :
384 : /* Slurp up all the empty cells */
385 1654989 : if ( cell.empty() ) {
386 1067386 : if ( !clear_count ) {
387 14331 : blank_renditions = cell.get_renditions();
388 : }
389 1067386 : if ( cell.get_renditions() == blank_renditions ) {
390 : /* Remember run of blank cells */
391 1067386 : clear_count++;
392 1067386 : frame_x++;
393 1067386 : continue;
394 : }
395 : }
396 :
397 : /* Clear or write cells within the row (not to end). */
398 587603 : if ( clear_count ) {
399 : /* Move to the right position. */
400 0 : frame.append_silent_move( frame_y, frame_x - clear_count );
401 0 : frame.update_rendition( blank_renditions );
402 0 : bool can_use_erase = has_bce || ( frame.current_rendition == initial_rendition() );
403 0 : if ( can_use_erase && has_ech && clear_count > 4 ) {
404 0 : snprintf( tmp, 64, "\033[%dX", clear_count );
405 0 : frame.append( tmp );
406 : } else {
407 0 : frame.append( clear_count, ' ' );
408 0 : frame.cursor_x = frame_x;
409 : }
410 0 : clear_count = 0;
411 : // If the current character is *another* empty cell in a different rendition,
412 : // we restart counting and continue here
413 0 : if ( cell.empty() ) {
414 0 : blank_renditions = cell.get_renditions();
415 0 : clear_count = 1;
416 0 : frame_x++;
417 0 : continue;
418 : }
419 : }
420 :
421 :
422 : /* Now draw a character cell. */
423 : /* Move to the right position. */
424 587603 : const int cell_width = cell.get_width();
425 : /* If we are about to print the last character in a wrapping row,
426 : trash the cursor position to force explicit positioning. We do
427 : this because our input terminal state may have the cursor on
428 : the autowrap column ("column 81"), but our output terminal
429 : states always snap the cursor to the true last column ("column
430 : 80"), and we want to be able to apply the diff to either, for
431 : verification. */
432 587603 : if ( wrap_this && frame_x + cell_width >= row_width ) {
433 1522 : frame.cursor_x = frame.cursor_y = -1;
434 : }
435 587603 : frame.append_silent_move( frame_y, frame_x );
436 587603 : frame.update_rendition( cell.get_renditions() );
437 587603 : frame.append_cell( cell );
438 587603 : frame_x += cell_width;
439 587603 : frame.cursor_x += cell_width;
440 587603 : if ( frame_x >= row_width ) {
441 2500 : wrote_last_cell = true;
442 : }
443 : }
444 :
445 : /* End of line. */
446 :
447 : /* Clear or write empty cells at EOL. */
448 385328 : if ( clear_count ) {
449 : /* Move to the right position. */
450 14331 : frame.append_silent_move( frame_y, frame_x - clear_count );
451 14331 : frame.update_rendition( blank_renditions );
452 :
453 14331 : bool can_use_erase = has_bce || ( frame.current_rendition == initial_rendition() );
454 14331 : if ( can_use_erase && !wrap_this ) {
455 14283 : frame.append( "\033[K" );
456 : } else {
457 48 : frame.append( clear_count, ' ' );
458 48 : frame.cursor_x = frame_x;
459 48 : wrote_last_cell = true;
460 : }
461 : }
462 :
463 385328 : if ( ! ( wrote_last_cell
464 2548 : && (frame_y < f.ds.get_height() - 1) ) ) {
465 382782 : return false;
466 : }
467 : /* To hint that a word-select should group the end of one line
468 : with the beginning of the next, we let the real cursor
469 : actually wrap around in cases where it wrapped around for us. */
470 2546 : if ( wrap_this ) {
471 : /* Update our cursor, and ask for wrap on the next row. */
472 1522 : frame.cursor_x = 0;
473 1522 : frame.cursor_y++;
474 1522 : return true;
475 : }
476 : /* Resort to CR/LF and update our cursor. */
477 1024 : frame.append( "\r\n" );
478 1024 : frame.cursor_x = 0;
479 1024 : frame.cursor_y++;
480 1024 : return false;
481 : }
482 :
483 24199 : FrameState::FrameState( const Framebuffer &s_last )
484 24199 : : str(), cursor_x(0), cursor_y(0), current_rendition( 0 ),
485 24199 : cursor_visible( s_last.ds.cursor_visible ),
486 24199 : last_frame( s_last )
487 : {
488 : /* Preallocate for better performance. Make a guess-- doesn't matter for correctness */
489 24199 : str.reserve( last_frame.ds.get_width() * last_frame.ds.get_height() * 4 );
490 24199 : }
491 :
492 602466 : void FrameState::append_silent_move( int y, int x )
493 : {
494 602466 : if ( cursor_x == x && cursor_y == y ) return;
495 : /* turn off cursor if necessary before moving cursor */
496 179045 : if ( cursor_visible ) {
497 14345 : append( "\033[?25l" );
498 14345 : cursor_visible = false;
499 : }
500 179045 : append_move( y, x );
501 : }
502 :
503 190122 : void FrameState::append_move( int y, int x )
504 : {
505 190122 : const int last_x = cursor_x;
506 190122 : const int last_y = cursor_y;
507 190122 : cursor_x = x;
508 190122 : cursor_y = y;
509 : // Only optimize if cursor pos is known
510 190122 : if ( last_x != -1 && last_y != -1 ) {
511 : // Can we use CR and/or LF? They're cheap and easier to trace.
512 187536 : if ( x == 0 && y - last_y >= 0 && y - last_y < 5 ) {
513 182289 : if ( last_x != 0 ) {
514 164539 : append( '\r' );
515 : }
516 182289 : append( y - last_y, '\n' );
517 364604 : return;
518 : }
519 : // Backspaces are good too.
520 5247 : if ( y == last_y && x - last_x < 0 && x - last_x > -5 ) {
521 26 : append( last_x - x, '\b' );
522 26 : return;
523 : }
524 : // More optimizations are possible.
525 : }
526 7807 : char tmp[ 64 ];
527 7807 : snprintf( tmp, 64, "\033[%d;%dH", y + 1, x + 1 );
528 7807 : append( tmp );
529 : }
530 :
531 629137 : void FrameState::update_rendition(const Renditions &r, bool force) {
532 629137 : if ( force || !(current_rendition == r) ) {
533 : /* print renditions */
534 63772 : append_string( r.sgr() );
535 31886 : current_rendition = r;
536 : }
537 629137 : }
|