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