/// Returns the length of the "shared prefix" of the two lines, which is the run of matching text /// and colors. If the prefix ends on a combining character, do not include the previous character /// in the prefix. static size_t line_shared_prefix(const line_t &a, const line_t &b) { size_t idx, max = std::min(a.size(), b.size()); for (idx = 0; idx < max; idx++) { wchar_t ac = a.char_at(idx), bc = b.char_at(idx); if (fish_wcwidth(ac) < 1 || fish_wcwidth(bc) < 1) { // Possible combining mark, return one index prior. if (idx > 0) idx--; break; } // We're done if the text or colors are different. if (ac != bc || a.color_at(idx) != b.color_at(idx)) break; } return idx; }
/// Print the specified string, but use at most the specified amount of space. If the whole string /// can't be fitted, ellipsize it. /// /// \param str the string to print /// \param color the color to apply to every printed character /// \param max the maximum space that may be used for printing /// \param has_more if this flag is true, this is not the entire string, and the string should be /// ellisiszed even if the string fits but takes up the whole space. static int print_max(const wcstring &str, highlight_spec_t color, int max, bool has_more, line_t *line) { int written = 0; for (size_t i = 0; i < str.size(); i++) { wchar_t c = str.at(i); if (written + fish_wcwidth(c) > max) break; if ((written + fish_wcwidth(c) == max) && (has_more || i + 1 < str.size())) { line->append(ellipsis_char, color); written += fish_wcwidth(ellipsis_char); break; } line->append(c, color); written += fish_wcwidth(c); } return written; }
/// Like fish_wcwidth, but returns 0 for control characters instead of -1. static int fish_wcwidth_min_0(wchar_t widechar) { return std::max(0, fish_wcwidth(widechar)); }
void s_write( screen_t *s, const wchar_t *prompt, const wchar_t *commandline, size_t explicit_len, const int *c, const int *indent, size_t cursor_pos ) { screen_data_t::cursor_t cursor_arr; size_t prompt_width; size_t screen_width; int current_line_width = 0, newline_count = 0, explicit_portion_width = 0; size_t max_line_width = 0; CHECK( s, ); CHECK( prompt, ); CHECK( commandline, ); CHECK( c, ); CHECK( indent, ); /* If we are using a dumb terminal, don't try any fancy stuff, just print out the text. */ if( is_dumb() ) { char *prompt_narrow = wcs2str( prompt ); char *buffer_narrow = wcs2str( commandline ); write_loop( 1, "\r", 1 ); write_loop( 1, prompt_narrow, strlen( prompt_narrow ) ); write_loop( 1, buffer_narrow, strlen( buffer_narrow ) ); free( prompt_narrow ); free( buffer_narrow ); return; } prompt_width = calc_prompt_width( prompt ); screen_width = common_get_width(); s_check_status( s ); /* Ignore prompts wider than the screen - only print a two character placeholder... It would be cool to truncate the prompt, but because it can contain escape sequences, this is harder than you'd think. */ if( prompt_width >= screen_width ) { prompt = L"> "; prompt_width = 2; } /* Completely ignore impossibly small screens */ if( screen_width < 4 ) { return; } /* Check if we are overflowing */ size_t last_char_that_fits = 0; for( size_t i=0; commandline[i]; i++ ) { if( commandline[i] == L'\n' ) { if( current_line_width > max_line_width ) max_line_width = current_line_width; current_line_width = indent[i]*INDENT_STEP; newline_count++; } else { int width = fish_wcwidth(commandline[i]); current_line_width += width; if (i < explicit_len) explicit_portion_width += width; if (prompt_width + current_line_width < screen_width) last_char_that_fits = i; } } if( current_line_width > max_line_width ) max_line_width = current_line_width; s->desired.resize(0); s->desired.cursor.x = s->desired.cursor.y = 0; /* If we cannot fit with the autosuggestion, but we can fit without it, truncate the autosuggestion. We limit this check to just one line to avoid confusion; not sure how well this would work with multiple lines */ wcstring truncated_autosuggestion_line; if (newline_count == 0 && prompt_width + max_line_width >= screen_width && prompt_width + explicit_portion_width < screen_width) { assert(screen_width - prompt_width >= 1); max_line_width = screen_width - prompt_width - 1; truncated_autosuggestion_line = wcstring(commandline, 0, last_char_that_fits); commandline = truncated_autosuggestion_line.c_str(); } for( size_t i=0; i<prompt_width; i++ ) { s_desired_append_char( s, L' ', 0, 0, prompt_width ); } /* If overflowing, give the prompt its own line to improve the situation. */ if( max_line_width + prompt_width >= screen_width ) { s_desired_append_char( s, L'\n', 0, 0, 0 ); prompt_width=0; } size_t i; for( i=0; commandline[i]; i++ ) { int col = c[i]; if( i == cursor_pos ) { col = 0; } if( i == cursor_pos ) { cursor_arr = s->desired.cursor; } s_desired_append_char( s, commandline[i], col, indent[i], prompt_width ); } if( i == cursor_pos ) { cursor_arr = s->desired.cursor; } s->desired.cursor = cursor_arr; s_update( s, prompt ); s_save_status( s ); }
/** Update the screen to match the desired output. */ static void s_update( screen_t *scr, const wchar_t *prompt ) { size_t prompt_width = calc_prompt_width( prompt ); int screen_width = common_get_width(); int need_clear = scr->need_clear; data_buffer_t output; scr->need_clear = 0; if( scr->actual_width != screen_width ) { need_clear = 1; s_move( scr, &output, 0, 0 ); scr->actual_width = screen_width; s_reset( scr, false ); } if( wcscmp( prompt, scr->actual_prompt.c_str() ) ) { s_move( scr, &output, 0, 0 ); s_write_str( &output, prompt ); scr->actual_prompt = prompt; scr->actual.cursor.x = (int)prompt_width; } for (size_t i=0; i < scr->desired.line_count(); i++) { const line_t &o_line = scr->desired.line(i); line_t &s_line = scr->actual.create_line(i); size_t start_pos = (i==0 ? prompt_width : 0); int current_width = 0; if( need_clear ) { s_move( scr, &output, (int)start_pos, (int)i ); s_write_mbs( &output, clr_eol); s_line.clear(); } /* Note that skip_remaining is a width, not a character count */ size_t skip_remaining = start_pos; /* Compute how much we should skip. At a minimum we skip over the prompt. But also skip over the shared prefix of what we want to output now, and what we output before, to avoid repeatedly outputting it. */ size_t shared_prefix = line_shared_prefix(o_line, s_line); if (shared_prefix > 0) { int prefix_width = fish_wcswidth(&o_line.text.at(0), shared_prefix); if (prefix_width > skip_remaining) skip_remaining = prefix_width; } /* Skip over skip_remaining width worth of characters */ size_t j = 0; for ( ; j < o_line.size(); j++) { int width = fish_wcwidth(o_line.char_at(j)); if (skip_remaining <= width) break; skip_remaining -= width; current_width += width; } /* Skip over zero-width characters (e.g. combining marks at the end of the prompt) */ for ( ; j < o_line.size(); j++) { int width = fish_wcwidth(o_line.char_at(j)); if (width > 0) break; } /* Now actually output stuff */ for ( ; j < o_line.size(); j++) { s_move( scr, &output, current_width, (int)i ); s_set_color( scr, &output, o_line.color_at(j) ); s_write_char( scr, &output, o_line.char_at(j) ); current_width += fish_wcwidth(o_line.char_at(j)); } /* If we wrote more on this line last time, clear it */ int prev_length = (s_line.text.empty() ? 0 : fish_wcswidth(&s_line.text.at(0), s_line.text.size())); if (prev_length > current_width ) { s_move( scr, &output, current_width, (int)i ); s_write_mbs( &output, clr_eol); } } /* Clear remaining lines */ for( size_t i=scr->desired.line_count(); i < scr->actual.line_count(); i++ ) { s_move( scr, &output, 0, (int)i ); s_write_mbs( &output, clr_eol); } s_move( scr, &output, scr->desired.cursor.x, scr->desired.cursor.y ); s_set_color( scr, &output, 0xffffffff); if( ! output.empty() ) { write_loop( 1, &output.at(0), output.size() ); } /* We have now synced our actual screen against our desired screen. Note that this is a big assignment! */ scr->actual = scr->desired; }
/** Convert a wide character to a multibyte string and append it to the buffer. */ static void s_write_char( screen_t *s, data_buffer_t *b, wchar_t c ) { scoped_buffer_t scoped_buffer(b); s->actual.cursor.x+=fish_wcwidth( c ); writech( c ); }
/** Appends a character to the end of the line that the output cursor is on. This function automatically handles linebreaks and lines longer than the screen width. */ static void s_desired_append_char( screen_t *s, wchar_t b, int c, int indent, size_t prompt_width ) { int line_no = s->desired.cursor.y; switch( b ) { case L'\n': { int i; s->desired.create_line(s->desired.line_count()); s->desired.cursor.y++; s->desired.cursor.x=0; for( i=0; i < prompt_width+indent*INDENT_STEP; i++ ) { s_desired_append_char( s, L' ', 0, indent, prompt_width ); } break; } case L'\r': { line_t ¤t = s->desired.line(line_no); current.clear(); s->desired.cursor.x = 0; break; } default: { int screen_width = common_get_width(); int cw = fish_wcwidth(b); s->desired.create_line(line_no); /* Check if we are at the end of the line. If so, continue on the next line. */ if( (s->desired.cursor.x + cw) > screen_width ) { line_no = (int)s->desired.line_count(); s->desired.add_line(); s->desired.cursor.y++; s->desired.cursor.x=0; for( size_t i=0; i < prompt_width; i++ ) { s_desired_append_char( s, L' ', 0, indent, prompt_width ); } } line_t &line = s->desired.line(line_no); line.append(b, c); s->desired.cursor.x+= cw; break; } } }
/** Calculate the width of the specified prompt. Does some clever magic to detect common escape sequences that may be embeded in a prompt, such as color codes. */ static size_t calc_prompt_width( const wchar_t *prompt ) { size_t res = 0; size_t j, k; for( j=0; prompt[j]; j++ ) { if( prompt[j] == L'\x1b' ) { /* This is the start of an escape code. Try to guess it's width. */ size_t p; int len=0; bool found = false; /* Detect these terminfo color escapes with parameter value 0..7, all of which don't move the cursor */ char * const esc[] = { set_a_foreground, set_a_background, set_foreground, set_background, } ; /* Detect these semi-common terminfo escapes without any parameter values, all of which don't move the cursor */ char * const esc2[] = { enter_bold_mode, exit_attribute_mode, enter_underline_mode, exit_underline_mode, enter_standout_mode, exit_standout_mode, flash_screen, enter_subscript_mode, exit_subscript_mode, enter_superscript_mode, exit_superscript_mode, enter_blink_mode, enter_italics_mode, exit_italics_mode, enter_reverse_mode, enter_shadow_mode, exit_shadow_mode, enter_standout_mode, exit_standout_mode, enter_secure_mode } ; for( p=0; p < sizeof esc / sizeof *esc && !found; p++ ) { if( !esc[p] ) continue; for( k=0; k<8; k++ ) { len = try_sequence( tparm(esc[p],k), &prompt[j] ); if( len ) { j += (len-1); found = true; break; } } } // PCA for term256 support, let's just detect the escape codes directly if (! found) { len = is_term256_escape(&prompt[j]); if (len) { j += (len - 1); found = true; } } for( p=0; p < (sizeof(esc2)/sizeof(char *)) && !found; p++ ) { if( !esc2[p] ) continue; /* Test both padded and unpadded version, just to be safe. Most versions of tparm don't actually seem to do anything these days. */ len = maxi( try_sequence( tparm(esc2[p]), &prompt[j] ), try_sequence( esc2[p], &prompt[j] )); if( len ) { j += (len-1); found = true; } } if( !found ) { if( prompt[j+1] == L'k' ) { const env_var_t term_name = env_get_string( L"TERM" ); if( !term_name.missing() && wcsstr( term_name.c_str(), L"screen" ) == term_name ) { const wchar_t *end; j+=2; found = true; end = wcsstr( &prompt[j], L"\x1b\\" ); if( end ) { /* You'd thing this should be '(end-prompt)+2', in order to move j past the end of the string, but there is a 'j++' at the end of each lap, so j should always point to the last menged character, e.g. +1. */ j = (end-prompt)+1; } else { break; } } } } } else if( prompt[j] == L'\t' ) { res = next_tab_stop( res ); } else if( prompt[j] == L'\n' ) { res = 0; } else { /* Ordinary decent character. Just add width. */ res += fish_wcwidth( prompt[j] ); } } return res; }
/* Like fish_wcwidth, but returns 0 for control characters instead of -1 */ static int fish_wcwidth_min_0(wchar_t wc) { return maxi(0, fish_wcwidth(wc)); }