/* * inputwin.c * * Copyright (C) 2012 - 2014 James Booth * * This file is part of Profanity. * * Profanity 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. * * Profanity 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 Profanity. If not, see . * * In addition, as a special exception, the copyright holders give permission to * link the code of portions of this program with the OpenSSL library under * certain conditions as described in each individual source file, and * distribute linked combinations including the two. * * You must obey the GNU General Public License in all respects for all of the * code used other than OpenSSL. If you modify file(s) with this exception, you * may extend this exception to your version of the file(s), but you are not * obligated to do so. If you do not wish to do so, delete this exception * statement from your version. If you delete this exception statement from all * source files in the program, then also delete it here. * */ #define _XOPEN_SOURCE_EXTENDED #include "config.h" #include #include #include #ifdef HAVE_NCURSESW_NCURSES_H #include #elif HAVE_NCURSES_H #include #endif #include "command/command.h" #include "common.h" #include "config/accounts.h" #include "config/preferences.h" #include "config/theme.h" #include "tools/history.h" #include "log.h" #include "muc.h" #include "profanity.h" #include "roster_list.h" #include "ui/ui.h" #include "ui/statusbar.h" #include "ui/inputwin.h" #include "ui/windows.h" #include "xmpp/xmpp.h" #define _inp_win_update_virtual() pnoutrefresh(inp_win, 0, pad_start, wrows-1, 0, wrows-1, wcols-1) #define KEY_CTRL_A 0001 #define KEY_CTRL_B 0002 #define KEY_CTRL_D 0004 #define KEY_CTRL_E 0005 #define KEY_CTRL_F 0006 #define KEY_CTRL_N 0016 #define KEY_CTRL_P 0020 #define KEY_CTRL_U 0025 #define KEY_CTRL_W 0027 #define MAX_HISTORY 100 #define INP_WIN_MAX 1000 static WINDOW *inp_win; static History history; static char line[INP_WIN_MAX]; static int line_bytes_len; static int line_utf8_pos; static int pad_start = 0; static int wrows, wcols; static int _handle_edit(int key_type, const wint_t ch); static int _handle_alt_key(int key); static void _handle_backspace(void); static int _printable(const wint_t ch); static void _clear_input(void); static void _go_to_end(void); static void _delete_previous_word(void); void create_input_window(void) { #ifdef NCURSES_REENTRANT set_escdelay(25); #else ESCDELAY = 25; #endif getmaxyx(stdscr, wrows, wcols); inp_win = newpad(1, INP_WIN_MAX); wbkgd(inp_win, theme_attrs(THEME_INPUT_TEXT));; keypad(inp_win, TRUE); wmove(inp_win, 0, 0); _inp_win_update_virtual(); history = history_new(MAX_HISTORY); line_bytes_len = 0; line_utf8_pos = 0; } void inp_win_resize(void) { int inp_x; getmaxyx(stdscr, wrows, wcols); inp_x = getcurx(inp_win); // if lost cursor off screen, move contents to show it if (inp_x >= pad_start + wcols) { pad_start = inp_x - (wcols / 2); if (pad_start < 0) { pad_start = 0; } } wbkgd(inp_win, theme_attrs(THEME_INPUT_TEXT));; _inp_win_update_virtual(); } void inp_non_block(gint timeout) { wtimeout(inp_win, timeout); } void inp_block(void) { wtimeout(inp_win, -1); } char * inp_read(int *key_type, wint_t *ch) { // echo off, and get some more input noecho(); *key_type = wget_wch(inp_win, ch); int display_len = utf8_display_len(line); gboolean in_command = FALSE; if ((display_len > 0 && line[0] == '/') || (display_len == 0 && *ch == '/')) { in_command = TRUE; } if (*key_type == ERR) { prof_handle_idle(); } if ((*key_type != ERR) && (*key_type != KEY_CODE_YES) && !in_command && _printable(*ch)) { prof_handle_activity(); } // if it wasn't an arrow key etc if (!_handle_edit(*key_type, *ch)) { if (_printable(*ch) && *key_type != KEY_CODE_YES) { if (line_bytes_len >= INP_WIN_MAX) { *ch = ERR; return NULL; } int col = getcurx(inp_win); int utf8_len = g_utf8_strlen(line, -1); // handle insert if not at end of input if (line_utf8_pos < utf8_len) { char bytes[MB_CUR_MAX]; size_t utf8_ch_len = wcrtomb(bytes, *ch, NULL); bytes[utf8_ch_len] = '\0'; gchar *start = g_utf8_substring(line, 0, line_utf8_pos); gchar *end = g_utf8_substring(line, line_utf8_pos, utf8_len); GString *new_line = g_string_new(start); g_string_append(new_line, bytes); g_string_append(new_line, end); int old_pos = line_utf8_pos; inp_replace_input(new_line->str); line_utf8_pos = old_pos+1; g_free(start); g_free(end); g_string_free(new_line, TRUE); col++; gunichar uni = g_utf8_get_char(bytes); if (g_unichar_iswide(uni)) { col++; } wmove(inp_win, 0, col); // otherwise just append } else { int display_len = utf8_display_len(line); char bytes[MB_CUR_MAX+1]; size_t utf8_ch_len = wcrtomb(bytes, *ch, NULL); // wcrtomb can return (size_t) -1 if (utf8_ch_len < MB_CUR_MAX) { int i; for (i = 0 ; i < utf8_ch_len; i++) { line[line_bytes_len++] = bytes[i]; } line[line_bytes_len] = '\0'; bytes[utf8_ch_len] = '\0'; waddstr(inp_win, bytes); line_utf8_pos++; col++; gunichar uni = g_utf8_get_char(bytes); if (g_unichar_iswide(uni)) { col++; } wmove(inp_win, 0, col); // if gone over screen size follow input int wrows, wcols; getmaxyx(stdscr, wrows, wcols); if (display_len - pad_start > wcols-2) { pad_start++; _inp_win_update_virtual(); } } } cmd_reset_autocomplete(); } } echo(); char *result = NULL; if (*ch == '\n') { line[line_bytes_len] = '\0'; result = strdup(line); line[0] = '\0'; line_bytes_len = 0; line_utf8_pos = 0; } if (*ch != ERR && *key_type != ERR) { cons_debug("CURR COL = %d", getcurx(inp_win)); cons_debug("CURR UNI = %d", line_utf8_pos); cons_debug(""); } return result; } void inp_get_password(char *passwd) { _clear_input(); _inp_win_update_virtual(); doupdate(); noecho(); mvwgetnstr(inp_win, 0, 1, passwd, MAX_PASSWORD_SIZE); wmove(inp_win, 0, 0); echo(); status_bar_clear(); } void inp_put_back(void) { _inp_win_update_virtual(); } void inp_replace_input(const char * const new_input) { strncpy(line, new_input, INP_WIN_MAX); line_bytes_len = strlen(line); inp_win_reset(); waddstr(inp_win, line); _go_to_end(); } void inp_win_reset(void) { _clear_input(); pad_start = 0; _inp_win_update_virtual(); } void inp_history_append(char *inp) { history_append(history, inp); } static void _clear_input(void) { werase(inp_win); wmove(inp_win, 0, 0); } /* * Deal with command editing, return 1 if ch was an edit * key press: up, down, left, right or backspace * return 0 if it wasn't */ static int _handle_edit(int key_type, const wint_t ch) { char *prev = NULL; char *next = NULL; int col = getcurx(inp_win); int next_ch; int display_size = utf8_display_len(line); int utf8_len = g_utf8_strlen(line, -1); // CTRL-LEFT if ((key_type == KEY_CODE_YES) && (ch == 547 || ch == 545 || ch == 544 || ch == 540 || ch == 539) && (col > 0)) { line[line_bytes_len] = '\0'; gchar *curr_ch = g_utf8_offset_to_pointer(line, col); curr_ch = g_utf8_find_prev_char(line, curr_ch); gchar *prev_ch; gunichar curr_uni; gunichar prev_uni; while (curr_ch != NULL) { curr_uni = g_utf8_get_char(curr_ch); if (g_unichar_isspace(curr_uni)) { curr_ch = g_utf8_find_prev_char(line, curr_ch); } else { prev_ch = g_utf8_find_prev_char(line, curr_ch); if (prev_ch == NULL) { curr_ch = NULL; break; } else { prev_uni = g_utf8_get_char(prev_ch); if (g_unichar_isspace(prev_uni)) { break; } else { curr_ch = prev_ch; } } } } if (curr_ch == NULL) { col = 0; wmove(inp_win, 0, col); } else { glong offset = g_utf8_pointer_to_offset(line, curr_ch); col = offset; wmove(inp_win, 0, col); } // if gone off screen to left, jump left (half a screen worth) if (col <= pad_start) { pad_start = pad_start - (wcols / 2); if (pad_start < 0) { pad_start = 0; } _inp_win_update_virtual(); } return 1; // CTRL-RIGHT } else if ((key_type == KEY_CODE_YES) && (ch == 562 || ch == 560 || ch == 555 || ch == 559 || ch == 554) && (col < display_size)) { line[line_bytes_len] = '\0'; gchar *curr_ch = g_utf8_offset_to_pointer(line, col); gchar *next_ch = g_utf8_find_next_char(curr_ch, NULL); gunichar curr_uni; gunichar next_uni; gboolean moved = FALSE; while (g_utf8_pointer_to_offset(line, next_ch) < display_size) { curr_uni = g_utf8_get_char(curr_ch); next_uni = g_utf8_get_char(next_ch); curr_ch = next_ch; next_ch = g_utf8_find_next_char(next_ch, NULL); if (!g_unichar_isspace(curr_uni) && g_unichar_isspace(next_uni) && moved) { break; } else { moved = TRUE; } } if (next_ch == NULL) { col = display_size; wmove(inp_win, 0, col); } else { glong offset = g_utf8_pointer_to_offset(line, curr_ch); if (offset == display_size - 1) { col = offset + 1; } else { col = offset; } wmove(inp_win, 0, col); } // if gone off screen to right, jump right (half a screen worth) if (col > pad_start + wcols) { pad_start = pad_start + (wcols / 2); _inp_win_update_virtual(); } return 1; // ALT-LEFT } else if ((key_type == KEY_CODE_YES) && (ch == 537 || ch == 542)) { ui_previous_win(); return 1; // ALT-RIGHT } else if ((key_type == KEY_CODE_YES) && (ch == 552 || ch == 557)) { ui_next_win(); return 1; // other editing keys } else { switch(ch) { case 27: // ESC // check for ALT-key next_ch = wgetch(inp_win); if (next_ch != ERR) { return _handle_alt_key(next_ch); } else { line_bytes_len = 0; inp_win_reset(); return 1; } case 127: _handle_backspace(); return 1; case KEY_BACKSPACE: if (key_type != KEY_CODE_YES) { return 0; } _handle_backspace(); return 1; case KEY_DC: // DEL if (key_type != KEY_CODE_YES) { return 0; } case KEY_CTRL_D: if (col == display_size-1) { gchar *start = g_utf8_substring(line, 0, col); for (line_bytes_len = 0; line_bytes_len < strlen(start); line_bytes_len++) { line[line_bytes_len] = start[line_bytes_len]; } line[line_bytes_len] = '\0'; g_free(start); _clear_input(); waddstr(inp_win, line); } else if (col < display_size-1) { gchar *start = g_utf8_substring(line, 0, col); gchar *end = g_utf8_substring(line, col+1, line_bytes_len); GString *new = g_string_new(start); g_string_append(new, end); for (line_bytes_len = 0; line_bytes_len < strlen(new->str); line_bytes_len++) { line[line_bytes_len] = new->str[line_bytes_len]; } line[line_bytes_len] = '\0'; g_free(start); g_free(end); g_string_free(new, FALSE); _clear_input(); waddstr(inp_win, line); wmove(inp_win, 0, col); } return 1; case KEY_LEFT: if (key_type != KEY_CODE_YES) { return 0; } case KEY_CTRL_B: if (line_utf8_pos > 0) { col--; gchar *curr_ch = g_utf8_offset_to_pointer(line, line_utf8_pos); gchar *prev_ch = g_utf8_find_prev_char(line, curr_ch); if (prev_ch) { gunichar uni = g_utf8_get_char(prev_ch); if (g_unichar_iswide(uni)) { col--; } } wmove(inp_win, 0, col); line_utf8_pos--; // current position off screen to left if (col - 1 < pad_start) { pad_start--; _inp_win_update_virtual(); } } return 1; case KEY_RIGHT: if (key_type != KEY_CODE_YES) { return 0; } case KEY_CTRL_F: if (line_utf8_pos < utf8_len) { col++; gchar *curr_ch = g_utf8_offset_to_pointer(line, line_utf8_pos); if (curr_ch) { gunichar uni = g_utf8_get_char(curr_ch); if (g_unichar_iswide(uni)) { col++; } } wmove(inp_win, 0, col); line_utf8_pos++; // current position off screen to right if ((col + 1 - pad_start) >= wcols) { pad_start++; _inp_win_update_virtual(); } } return 1; case KEY_UP: if (key_type != KEY_CODE_YES) { return 0; } case KEY_CTRL_P: line[line_bytes_len] = '\0'; prev = history_previous(history, line); if (prev) { inp_replace_input(prev); } return 1; case KEY_DOWN: if (key_type != KEY_CODE_YES) { return 0; } case KEY_CTRL_N: line[line_bytes_len] = '\0'; next = history_next(history, line); if (next) { inp_replace_input(next); } else if (line_bytes_len != 0) { line[line_bytes_len] = '\0'; history_append(history, line); inp_replace_input(""); } return 1; case KEY_HOME: if (key_type != KEY_CODE_YES) { return 0; } case KEY_CTRL_A: wmove(inp_win, 0, 0); pad_start = 0; _inp_win_update_virtual(); return 1; case KEY_END: if (key_type != KEY_CODE_YES) { return 0; } case KEY_CTRL_E: _go_to_end(); return 1; case 9: // tab if (line_bytes_len != 0) { line[line_bytes_len] = '\0'; if ((strncmp(line, "/", 1) != 0) && (ui_current_win_type() == WIN_MUC)) { char *result = muc_autocomplete(line); if (result) { inp_replace_input(result); free(result); } } else if (strncmp(line, "/", 1) == 0) { char *result = cmd_autocomplete(line); if (result) { inp_replace_input(result); free(result); } } } return 1; case KEY_CTRL_W: _delete_previous_word(); return 1; break; case KEY_CTRL_U: while (getcurx(inp_win) > 0) { _delete_previous_word(); } return 1; break; default: return 0; } } } static void _handle_backspace(void) { int col = getcurx(inp_win); int utf8_len = g_utf8_strlen(line, -1); roster_reset_search_attempts(); if (utf8_len > 0) { // if at end, delete last char if (line_utf8_pos >= utf8_len) { gchar *new_line = g_utf8_substring(line, 0, utf8_len-1); inp_replace_input(new_line); // if in middle, delete and shift chars left } else if (line_utf8_pos > 0 && line_utf8_pos < utf8_len) { gchar *del_char = g_utf8_offset_to_pointer(line, line_utf8_pos-1); gunichar uni = g_utf8_get_char(del_char); gchar *start = g_utf8_substring(line, 0, line_utf8_pos-1); gchar *end = g_utf8_substring(line, line_utf8_pos, utf8_len); GString *new_line = g_string_new(start); g_string_append(new_line, end); int old_pos = line_utf8_pos; inp_replace_input(new_line->str); line_utf8_pos = old_pos-1; g_free(start); g_free(end); g_string_free(new_line, TRUE); col--; if (g_unichar_iswide(uni)) { col--; } wmove(inp_win, 0, col); } // if gone off screen to left, jump left (half a screen worth) if (col <= pad_start) { pad_start = pad_start - (wcols / 2); if (pad_start < 0) { pad_start = 0; } _inp_win_update_virtual(); } } } static int _handle_alt_key(int key) { switch (key) { case '1': ui_switch_win(1); break; case '2': ui_switch_win(2); break; case '3': ui_switch_win(3); break; case '4': ui_switch_win(4); break; case '5': ui_switch_win(5); break; case '6': ui_switch_win(6); break; case '7': ui_switch_win(7); break; case '8': ui_switch_win(8); break; case '9': ui_switch_win(9); break; case '0': ui_switch_win(0); break; case KEY_LEFT: ui_previous_win(); break; case KEY_RIGHT: ui_next_win(); break; case 263: case 127: _delete_previous_word(); break; default: break; } return 1; } static void _delete_previous_word(void) { int end_del = getcurx(inp_win); int start_del = end_del; line[line_bytes_len] = '\0'; gchar *curr_ch = g_utf8_offset_to_pointer(line, end_del); curr_ch = g_utf8_find_prev_char(line, curr_ch); gchar *prev_ch; gunichar curr_uni; gunichar prev_uni; while (curr_ch != NULL) { curr_uni = g_utf8_get_char(curr_ch); if (g_unichar_isspace(curr_uni)) { curr_ch = g_utf8_find_prev_char(line, curr_ch); } else { prev_ch = g_utf8_find_prev_char(line, curr_ch); if (prev_ch == NULL) { curr_ch = NULL; break; } else { prev_uni = g_utf8_get_char(prev_ch); if (g_unichar_isspace(prev_uni)) { break; } else { curr_ch = prev_ch; } } } } if (curr_ch == NULL) { start_del = 0; } else { start_del = g_utf8_pointer_to_offset(line, curr_ch); } gint len = g_utf8_strlen(line, -1); gchar *start_string = g_utf8_substring(line, 0, start_del); gchar *end_string = g_utf8_substring(line, end_del, len); int i; for (i = 0; i < strlen(start_string); i++) { line[i] = start_string[i]; } for (i = 0; i < strlen(end_string); i++) { line[strlen(start_string)+i] = end_string[i]; } line_bytes_len = strlen(start_string)+i; line[line_bytes_len] = '\0'; _clear_input(); waddstr(inp_win, line); wmove(inp_win, 0, start_del); // if gone off screen to left, jump left (half a screen worth) if (start_del <= pad_start) { pad_start = pad_start - (wcols / 2); if (pad_start < 0) { pad_start = 0; } _inp_win_update_virtual(); } } static void _go_to_end(void) { int display_len = utf8_display_len(line); wmove(inp_win, 0, display_len); line_utf8_pos = g_utf8_strlen(line, -1); if (display_len > wcols-2) { pad_start = display_len - wcols + 1; _inp_win_update_virtual(); } } static int _printable(const wint_t ch) { char bytes[MB_CUR_MAX+1]; size_t utf_len = wcrtomb(bytes, ch, NULL); bytes[utf_len] = '\0'; gunichar unichar = g_utf8_get_char(bytes); return g_unichar_isprint(unichar) && (ch != KEY_MOUSE); }