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