/************************************************************************** * prompt.c -- This file is part of GNU nano. * * * * Copyright (C) 1999-2011, 2013-2018 Free Software Foundation, Inc. * * Copyright (C) 2016, 2018 Benno Schulenberg * * * * GNU nano is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published * * by the Free Software Foundation, either version 3 of the License, * * or (at your option) any later version. * * * * GNU nano is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty * * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * * See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see http://www.gnu.org/licenses/. * * * **************************************************************************/ #include "proto.h" #include static char *prompt = NULL; /* The prompt string used for statusbar questions. */ #ifdef ENABLE_MOUSE /* Handle a mouse click on the statusbar prompt or the shortcut list. */ int do_statusbar_mouse(void) { int click_row, click_col; int retval = get_mouseinput(&click_row, &click_col, TRUE); /* We can click on the statusbar window text to move the cursor. */ if (retval == 0 && wmouse_trafo(bottomwin, &click_row, &click_col, FALSE)) { size_t start_col = strlenpt(prompt) + 2; /* Move to where the click occurred. */ if (click_row == 0 && click_col >= start_col) { statusbar_x = actual_x(answer, get_statusbar_page_start(start_col, start_col + statusbar_xplustabs()) + click_col - start_col); update_the_statusbar(); } } return retval; } #endif /* Read in a keystroke, interpret it if it is a shortcut or toggle, and * return it. Set finished to TRUE if we're done after running * or trying to run a function associated with a shortcut key. */ int do_statusbar_input(bool *finished) { int input; /* The character we read in. */ static int *kbinput = NULL; /* The input buffer. */ static size_t kbinput_len = 0; /* The length of the input buffer. */ const sc *shortcut; *finished = FALSE; /* Read in a character. */ input = get_kbinput(bottomwin, VISIBLE); #ifndef NANO_TINY if (input == KEY_WINCH) return KEY_WINCH; #endif #ifdef ENABLE_MOUSE /* If we got a mouse click and it was on a shortcut, read in the * shortcut character. */ if (input == KEY_MOUSE) { if (do_statusbar_mouse() == 1) input = get_kbinput(bottomwin, BLIND); else return ERR; } #endif /* Check for a shortcut in the current list. */ shortcut = get_shortcut(&input); /* If we got a non-high-bit control key, a meta key sequence, or a * function key, and it's not a shortcut or toggle, throw it out. */ if (shortcut == NULL) { if (is_ascii_cntrl_char(input) || meta_key || !is_byte(input)) { beep(); input = ERR; } } /* If the keystroke isn't a shortcut nor a toggle, it's a normal text * character: add the it to the input buffer, when allowed. */ if (input != ERR && shortcut == NULL) { /* Only accept input when not in restricted mode, or when not at * the "Write File" prompt, or when there is no filename yet. */ if (!ISSET(RESTRICTED) || currmenu != MWRITEFILE || openfile->filename[0] == '\0') { kbinput_len++; kbinput = (int *)nrealloc(kbinput, kbinput_len * sizeof(int)); kbinput[kbinput_len - 1] = input; } } /* If we got a shortcut, or if there aren't any other keystrokes waiting * after the one we read in, we need to insert all the characters in the * input buffer (if not empty) into the answer. */ if ((shortcut || get_key_buffer_len() == 0) && kbinput != NULL) { /* Inject all characters in the input buffer at once, filtering out * control characters. */ do_statusbar_output(kbinput, kbinput_len, TRUE); /* Empty the input buffer. */ kbinput_len = 0; free(kbinput); kbinput = NULL; } if (shortcut) { if (shortcut->func == do_tab || shortcut->func == do_enter) ; else if (shortcut->func == do_left) do_statusbar_left(); else if (shortcut->func == do_right) do_statusbar_right(); #ifndef NANO_TINY else if (shortcut->func == do_prev_word_void) do_statusbar_prev_word(); else if (shortcut->func == do_next_word_void) do_statusbar_next_word(); #endif else if (shortcut->func == do_home) do_statusbar_home(); else if (shortcut->func == do_end) do_statusbar_end(); /* When in restricted mode at the "Write File" prompt and the * filename isn't blank, disallow any input and deletion. */ else if (ISSET(RESTRICTED) && currmenu == MWRITEFILE && openfile->filename[0] != '\0' && (shortcut->func == do_verbatim_input || shortcut->func == do_cut_text_void || shortcut->func == do_uncut_text || shortcut->func == do_delete || shortcut->func == do_backspace)) ; #ifdef ENABLE_NANORC else if (shortcut->func == (functionptrtype)implant) implant(shortcut->expansion); #endif else if (shortcut->func == do_verbatim_input) do_statusbar_verbatim_input(); else if (shortcut->func == do_cut_text_void) do_statusbar_cut_text(); else if (shortcut->func == do_delete) do_statusbar_delete(); else if (shortcut->func == do_backspace) do_statusbar_backspace(); else if (shortcut->func == do_uncut_text) { if (cutbuffer != NULL) do_statusbar_uncut_text(); } else { /* Handle any other shortcut in the current menu, setting finished * to TRUE to indicate that we're done after running or trying to * run its associated function. */ if (!ISSET(VIEW_MODE) || okay_for_view(shortcut)) shortcut->func(); *finished = TRUE; } } return input; } /* The user typed input_len multibyte characters. Add them to the answer, * filtering out ASCII control characters if filtering is TRUE. */ void do_statusbar_output(int *the_input, size_t input_len, bool filtering) { char *output = charalloc(input_len + 1); char onechar[MAXCHARLEN]; size_t char_len, i, j = 0; /* Copy the typed stuff so it can be treated. */ for (i = 0; i < input_len; i++) output[i] = (char)the_input[i]; output[i] = '\0'; while (j < input_len) { /* Encode any NUL byte as 0x0A. */ if (output[j] == '\0') output[j] = '\n'; /* Interpret the next multibyte character. */ char_len = parse_mbchar(output + j, onechar, NULL); j += char_len; /* When filtering, skip any ASCII control character. */ if (filtering && is_ascii_cntrl_char(*(output + j - char_len))) continue; /* Insert the typed character into the existing answer string. */ answer = charealloc(answer, strlen(answer) + char_len + 1); charmove(answer + statusbar_x + char_len, answer + statusbar_x, strlen(answer) - statusbar_x + 1); strncpy(answer + statusbar_x, onechar, char_len); statusbar_x += char_len; } free(output); update_the_statusbar(); } /* Move to the beginning of the answer. */ void do_statusbar_home(void) { statusbar_x = 0; update_the_statusbar(); } /* Move to the end of the answer. */ void do_statusbar_end(void) { statusbar_x = strlen(answer); update_the_statusbar(); } /* Move left one character. */ void do_statusbar_left(void) { if (statusbar_x > 0) { statusbar_x = move_mbleft(answer, statusbar_x); update_the_statusbar(); } } /* Move right one character. */ void do_statusbar_right(void) { if (answer[statusbar_x] != '\0') { statusbar_x = move_mbright(answer, statusbar_x); update_the_statusbar(); } } /* Backspace over one character. */ void do_statusbar_backspace(void) { if (statusbar_x > 0) { statusbar_x = move_mbleft(answer, statusbar_x); do_statusbar_delete(); } } /* Delete one character. */ void do_statusbar_delete(void) { if (answer[statusbar_x] != '\0') { int char_len = parse_mbchar(answer + statusbar_x, NULL, NULL); charmove(answer + statusbar_x, answer + statusbar_x + char_len, strlen(answer) - statusbar_x - char_len + 1); update_the_statusbar(); } } /* Zap some or all text from the answer. */ void do_statusbar_cut_text(void) { if (!ISSET(CUT_FROM_CURSOR)) statusbar_x = 0; answer[statusbar_x] = '\0'; update_the_statusbar(); } #ifndef NANO_TINY /* Move to the next word in the answer. */ void do_statusbar_next_word(void) { bool seen_space = !is_word_mbchar(answer + statusbar_x, FALSE); bool seen_word = !seen_space; /* Move forward until we reach either the end or the start of a word, * depending on whether the AFTER_ENDS flag is set or not. */ while (answer[statusbar_x] != '\0') { statusbar_x = move_mbright(answer, statusbar_x); if (ISSET(AFTER_ENDS)) { /* If this is a word character, continue; else it's a separator, * and if we've already seen a word, then it's a word end. */ if (is_word_mbchar(answer + statusbar_x, FALSE)) seen_word = TRUE; else if (seen_word) break; } else { /* If this is not a word character, then it's a separator; else * if we've already seen a separator, then it's a word start. */ if (!is_word_mbchar(answer + statusbar_x, FALSE)) seen_space = TRUE; else if (seen_space) break; } } update_the_statusbar(); } /* Move to the previous word in the answer. */ void do_statusbar_prev_word(void) { bool seen_a_word = FALSE, step_forward = FALSE; /* Move backward until we pass over the start of a word. */ while (statusbar_x != 0) { statusbar_x = move_mbleft(answer, statusbar_x); if (is_word_mbchar(answer + statusbar_x, FALSE)) seen_a_word = TRUE; else if (seen_a_word) { /* This is space now: we've overshot the start of the word. */ step_forward = TRUE; break; } } if (step_forward) /* Move one character forward again to sit on the start of the word. */ statusbar_x = move_mbright(answer, statusbar_x); update_the_statusbar(); } #endif /* !NANO_TINY */ /* Get verbatim input and inject it into the answer, without filtering. */ void do_statusbar_verbatim_input(void) { int *kbinput; size_t kbinput_len; kbinput = get_verbatim_kbinput(bottomwin, &kbinput_len); do_statusbar_output(kbinput, kbinput_len, FALSE); free(kbinput); } /* Return the zero-based column position of the cursor in the answer. */ size_t statusbar_xplustabs(void) { return strnlenpt(answer, statusbar_x); } /* Paste the first line of the cutbuffer into the current answer. */ void do_statusbar_uncut_text(void) { size_t pastelen = strlen(cutbuffer->data); char *fusion = charalloc(strlen(answer) + pastelen + 1); /* Concatenate: the current answer before the cursor, the first line * of the cutbuffer, plus the rest of the current answer. */ strncpy(fusion, answer, statusbar_x); strncpy(fusion + statusbar_x, cutbuffer->data, pastelen); strcpy(fusion + statusbar_x + pastelen, answer + statusbar_x); free(answer); answer = fusion; statusbar_x += pastelen; } /* Return the column number of the first character of the answer that is * displayed in the statusbar when the cursor is at the given column, * with the available room for the answer starting at base. Note that * (0 <= column - get_statusbar_page_start(column) < COLS). */ size_t get_statusbar_page_start(size_t base, size_t column) { if (column == base || column < COLS - 1) return 0; else if (COLS > base + 2) return column - base - 1 - (column - base - 1) % (COLS - base - 2); else return column - 2; } /* Reinitialize the cursor position in the answer. */ void reinit_statusbar_x(void) { statusbar_x = HIGHEST_POSITIVE; } /* Redraw the promptbar and place the cursor at the right spot. */ void update_the_statusbar(void) { size_t base = strlenpt(prompt) + 2; size_t the_page, end_page, column; char *expanded; the_page = get_statusbar_page_start(base, base + strnlenpt(answer, statusbar_x)); end_page = get_statusbar_page_start(base, base + strlenpt(answer) - 1); /* Color the promptbar over its full width. */ wattron(bottomwin, interface_color_pair[TITLE_BAR]); blank_statusbar(); mvwaddstr(bottomwin, 0, 0, prompt); waddch(bottomwin, ':'); waddch(bottomwin, (the_page == 0) ? ' ' : '<'); expanded = display_string(answer, the_page, COLS - base - 1, FALSE); waddstr(bottomwin, expanded); free(expanded); waddch(bottomwin, (the_page >= end_page) ? ' ' : '>'); wattroff(bottomwin, interface_color_pair[TITLE_BAR]); /* Work around a cursor-misplacement bug in VTEs. */ wmove(bottomwin, 0, 0); wrefresh(bottomwin); /* Place the cursor at statusbar_x in the answer. */ column = base + statusbar_xplustabs(); wmove(bottomwin, 0, column - get_statusbar_page_start(base, column)); wnoutrefresh(bottomwin); } /* Get a string of input at the statusbar prompt. */ functionptrtype acquire_an_answer(int *actual, bool allow_tabs, bool allow_files, bool *listed, filestruct **history_list, void (*refresh_func)(void)) { int kbinput = ERR; bool finished; functionptrtype func; #ifdef ENABLE_TABCOMP bool tabbed = FALSE; /* Whether we've pressed Tab. */ #endif #ifdef ENABLE_HISTORIES char *history = NULL; /* The current history string. */ char *magichistory = NULL; /* The temporary string typed at the bottom of the history, if * any. */ #ifdef ENABLE_TABCOMP int last_kbinput = ERR; /* The key we pressed before the current key. */ size_t complete_len = 0; /* The length of the original string that we're trying to * tab complete, if any. */ #endif #endif /* ENABLE_HISTORIES */ if (statusbar_x > strlen(answer)) statusbar_x = strlen(answer); update_the_statusbar(); while (TRUE) { kbinput = do_statusbar_input(&finished); #ifndef NANO_TINY /* If the window size changed, go reformat the prompt string. */ if (kbinput == KEY_WINCH) { refresh_func(); *actual = KEY_WINCH; #ifdef ENABLE_HISTORIES free(magichistory); #endif return NULL; } #endif /* !NANO_TINY */ func = func_from_key(&kbinput); if (func == do_cancel || func == do_enter) break; #ifdef ENABLE_TABCOMP if (func != do_tab) tabbed = FALSE; if (func == do_tab) { #ifdef ENABLE_HISTORIES if (history_list != NULL) { if (last_kbinput != the_code_for(do_tab, TAB_CODE)) complete_len = strlen(answer); if (complete_len > 0) { answer = get_history_completion(history_list, answer, complete_len); statusbar_x = strlen(answer); } } else #endif if (allow_tabs) answer = input_tab(answer, allow_files, &statusbar_x, &tabbed, refresh_func, listed); } else #endif /* ENABLE_TABCOMP */ #ifdef ENABLE_HISTORIES if (func == get_history_older_void) { if (history_list != NULL) { /* If we're scrolling up at the bottom of the history list * and answer isn't blank, save answer in magichistory. */ if ((*history_list)->next == NULL && *answer != '\0') magichistory = mallocstrcpy(magichistory, answer); /* Get the older search from the history list and save it in * answer. If there is no older search, don't do anything. */ if ((history = get_history_older(history_list)) != NULL) { answer = mallocstrcpy(answer, history); statusbar_x = strlen(answer); } /* This key has a shortcut-list entry when it's used to * move to an older search, which means that finished has * been set to TRUE. Set it back to FALSE here, so that * we aren't kicked out of the statusbar prompt. */ finished = FALSE; } } else if (func == get_history_newer_void) { if (history_list != NULL) { /* Get the newer search from the history list and save it in * answer. If there is no newer search, don't do anything. */ if ((history = get_history_newer(history_list)) != NULL) { answer = mallocstrcpy(answer, history); statusbar_x = strlen(answer); } /* If, after scrolling down, we're at the bottom of the * history list, answer is blank, and magichistory is set, * save magichistory in answer. */ if ((*history_list)->next == NULL && *answer == '\0' && magichistory != NULL) { answer = mallocstrcpy(answer, magichistory); statusbar_x = strlen(answer); } /* This key has a shortcut-list entry when it's used to * move to a newer search, which means that finished has * been set to TRUE. Set it back to FALSE here, so that * we aren't kicked out of the statusbar prompt. */ finished = FALSE; } } else #endif /* ENABLE_HISTORIES */ if (func == do_help_void) { /* This key has a shortcut-list entry when it's used to go to * the help browser or display a message indicating that help * is disabled, which means that finished has been set to TRUE. * Set it back to FALSE here, so that we aren't kicked out of * the statusbar prompt. */ finished = FALSE; } /* If we have a shortcut with an associated function, break out if * we're finished after running or trying to run the function. */ if (finished) break; update_the_statusbar(); #if defined(ENABLE_HISTORIES) && defined(ENABLE_TABCOMP) last_kbinput = kbinput; #endif } #ifdef ENABLE_HISTORIES /* Set the current position in the history list to the bottom. */ if (history_list != NULL) { history_reset(*history_list); free(magichistory); } #endif *actual = kbinput; return func; } /* Ask a question on the statusbar. The prompt will be stored in the * static prompt, which should be NULL initially, and the answer will be * stored in the answer global. Returns -1 on aborted enter, -2 on a * blank string, and 0 otherwise, the valid shortcut key caught. * curranswer is any editable text that we want to put up by default, * and refresh_func is the function we want to call to refresh the edit * window. * * The allow_tabs parameter indicates whether we should allow tabs to be * interpreted. The allow_files parameter indicates whether we should * allow all files (as opposed to just directories) to be tab completed. */ int do_prompt(bool allow_tabs, bool allow_files, int menu, const char *curranswer, filestruct **history_list, void (*refresh_func)(void), const char *msg, ...) { va_list ap; int retval; functionptrtype func = NULL; bool listed = FALSE; /* Save a possible current statusbar x position and prompt. */ size_t was_statusbar_x = statusbar_x; char *saved_prompt = prompt; bottombars(menu); if (answer != curranswer || answer == NULL) answer = mallocstrcpy(answer, curranswer); #ifndef NANO_TINY redo_theprompt: #endif prompt = charalloc((COLS * MAXCHARLEN) + 1); va_start(ap, msg); vsnprintf(prompt, COLS * MAXCHARLEN, msg, ap); va_end(ap); /* Reserve five columns for colon plus angles plus answer, ":". */ prompt[actual_x(prompt, (COLS < 5) ? 0 : COLS - 5)] = '\0'; func = acquire_an_answer(&retval, allow_tabs, allow_files, &listed, history_list, refresh_func); free(prompt); prompt = saved_prompt; #ifndef NANO_TINY if (retval == KEY_WINCH) goto redo_theprompt; #endif /* If we're done with this prompt, restore the x position to what * it was at a possible previous prompt. */ if (func == do_cancel || func == do_enter) statusbar_x = was_statusbar_x; /* If we left the prompt via Cancel or Enter, set the return value * properly. */ if (func == do_cancel) retval = -1; else if (func == do_enter) retval = (*answer == '\0') ? -2 : 0; wipe_statusbar(); #ifdef ENABLE_TABCOMP /* If we've done tab completion, there might still be a list of * filename matches on the edit window. Clear them off. */ if (listed) refresh_func(); #endif return retval; } /* Ask a simple Yes/No (and optionally All) question, specified in msg, * on the statusbar. Return 1 for Yes, 0 for No, 2 for All (if all is * TRUE when passed in), and -1 for Cancel. */ int do_yesno_prompt(bool all, const char *msg) { int response = -2, width = 16; /* TRANSLATORS: For the next three strings, specify the starting letters * of the translations for "Yes"/"No"/"All". The first letter of each of * these strings MUST be a single-byte letter; others may be multi-byte. */ const char *yesstr = _("Yy"); const char *nostr = _("Nn"); const char *allstr = _("Aa"); while (response == -2) { #ifdef ENABLE_NLS char letter[MAXCHARLEN + 1]; int index = 0; #endif int kbinput; if (!ISSET(NO_HELP)) { char shortstr[MAXCHARLEN + 2]; /* Temporary string for (translated) " Y", " N" and " A". */ if (COLS < 32) width = COLS / 2; /* Clear the shortcut list from the bottom of the screen. */ blank_bottombars(); /* Now show the ones for "Yes", "No", "Cancel" and maybe "All". */ sprintf(shortstr, " %c", yesstr[0]); wmove(bottomwin, 1, 0); post_one_key(shortstr, _("Yes"), width); shortstr[1] = nostr[0]; wmove(bottomwin, 2, 0); post_one_key(shortstr, _("No"), width); if (all) { shortstr[1] = allstr[0]; wmove(bottomwin, 1, width); post_one_key(shortstr, _("All"), width); } wmove(bottomwin, 2, width); post_one_key("^C", _("Cancel"), width); } /* Color the statusbar over its full width and display the question. */ wattron(bottomwin, interface_color_pair[TITLE_BAR]); blank_statusbar(); mvwaddnstr(bottomwin, 0, 0, msg, actual_x(msg, COLS - 1)); wattroff(bottomwin, interface_color_pair[TITLE_BAR]); wnoutrefresh(bottomwin); currmenu = MYESNO; /* When not replacing, show the cursor while waiting for a key. */ kbinput = get_kbinput(bottomwin, !all); #ifdef ENABLE_NLS letter[index++] = (unsigned char)kbinput; #ifdef ENABLE_UTF8 /* If the received code is a UTF-8 starter byte, get also the * continuation bytes and assemble them into one letter. */ if (using_utf8() && 0xC0 <= kbinput && kbinput <= 0xF7) { int extras = (kbinput / 16) % 4 + (kbinput <= 0xCF ? 1 : 0); while (extras <= get_key_buffer_len() && extras-- > 0) letter[index++] = (unsigned char)get_kbinput(bottomwin, !all); } #endif letter[index] = '\0'; /* See if the typed letter is in the Yes, No, or All strings. */ if (strstr(yesstr, letter) != NULL) response = 1; else if (strstr(nostr, letter) != NULL) response = 0; else if (all && strstr(allstr, letter) != NULL) response = 2; else #endif /* ENABLE_NLS */ if (strchr("Yy", kbinput) != NULL) response = 1; else if (strchr("Nn", kbinput) != NULL) response = 0; else if (all && strchr("Aa", kbinput) != NULL) response = 2; else if (func_from_key(&kbinput) == do_cancel) response = -1; #ifdef ENABLE_MOUSE else if (kbinput == KEY_MOUSE) { int mouse_x, mouse_y; /* We can click on the Yes/No/All shortcuts to select an answer. */ if (get_mouseinput(&mouse_x, &mouse_y, FALSE) == 0 && wmouse_trafo(bottomwin, &mouse_y, &mouse_x, FALSE) && mouse_x < (width * 2) && mouse_y > 0) { int x = mouse_x / width; int y = mouse_y - 1; /* x == 0 means Yes or No, y == 0 means Yes or All. */ response = -2 * x * y + x - y + 1; if (response == 2 && !all) response = -2; } } #endif /* ENABLE_MOUSE */ } return response; }