/* * Copyright (c) 2010 Jiri Svoboda * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * - The name of the author may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include /** Seek direction */ typedef enum { seek_backward = -1, seek_forward = 1 } seek_dir_t; static void tinput_init(tinput_t *); static void tinput_insert_string(tinput_t *, const char *); static void tinput_sel_get_bounds(tinput_t *, size_t *, size_t *); static bool tinput_sel_active(tinput_t *); static void tinput_sel_all(tinput_t *); static void tinput_sel_delete(tinput_t *); static void tinput_key_ctrl(tinput_t *, kbd_event_t *); static void tinput_key_shift(tinput_t *, kbd_event_t *); static void tinput_key_ctrl_shift(tinput_t *, kbd_event_t *); static void tinput_key_unmod(tinput_t *, kbd_event_t *); static void tinput_pre_seek(tinput_t *, bool); static void tinput_post_seek(tinput_t *, bool); /** Create a new text input field. */ tinput_t *tinput_new(void) { tinput_t *ti; ti = malloc(sizeof(tinput_t)); if (ti == NULL) return NULL; tinput_init(ti); return ti; } /** Destroy text input field. */ void tinput_destroy(tinput_t *ti) { free(ti); } static void tinput_display_tail(tinput_t *ti, size_t start, size_t pad) { wchar_t dbuf[INPUT_MAX_SIZE + 1]; size_t sa; size_t sb; tinput_sel_get_bounds(ti, &sa, &sb); console_set_pos(ti->console, (ti->col0 + start) % ti->con_cols, ti->row0 + (ti->col0 + start) / ti->con_cols); console_set_style(ti->console, STYLE_NORMAL); size_t p = start; if (p < sa) { memcpy(dbuf, ti->buffer + p, (sa - p) * sizeof(wchar_t)); dbuf[sa - p] = '\0'; printf("%ls", dbuf); p = sa; } if (p < sb) { console_flush(ti->console); console_set_style(ti->console, STYLE_SELECTED); memcpy(dbuf, ti->buffer + p, (sb - p) * sizeof(wchar_t)); dbuf[sb - p] = '\0'; printf("%ls", dbuf); p = sb; } console_flush(ti->console); console_set_style(ti->console, STYLE_NORMAL); if (p < ti->nc) { memcpy(dbuf, ti->buffer + p, (ti->nc - p) * sizeof(wchar_t)); dbuf[ti->nc - p] = '\0'; printf("%ls", dbuf); } for (p = 0; p < pad; p++) putchar(' '); console_flush(ti->console); } static char *tinput_get_str(tinput_t *ti) { return wstr_to_astr(ti->buffer); } static void tinput_position_caret(tinput_t *ti) { console_set_pos(ti->console, (ti->col0 + ti->pos) % ti->con_cols, ti->row0 + (ti->col0 + ti->pos) / ti->con_cols); } /** Update row0 in case the screen could have scrolled. */ static void tinput_update_origin(tinput_t *ti) { sysarg_t width = ti->col0 + ti->nc; sysarg_t rows = (width / ti->con_cols) + 1; /* Update row0 if the screen scrolled. */ if (ti->row0 + rows > ti->con_rows) ti->row0 = ti->con_rows - rows; } static void tinput_insert_char(tinput_t *ti, wchar_t c) { if (ti->nc == INPUT_MAX_SIZE) return; sysarg_t new_width = ti->col0 + ti->nc + 1; if (new_width % ti->con_cols == 0) { /* Advancing to new line. */ sysarg_t new_height = (new_width / ti->con_cols) + 1; if (new_height >= ti->con_rows) { /* Disallow text longer than 1 page for now. */ return; } } size_t i; for (i = ti->nc; i > ti->pos; i--) ti->buffer[i] = ti->buffer[i - 1]; ti->buffer[ti->pos] = c; ti->pos += 1; ti->nc += 1; ti->buffer[ti->nc] = '\0'; ti->sel_start = ti->pos; tinput_display_tail(ti, ti->pos - 1, 0); tinput_update_origin(ti); tinput_position_caret(ti); } static void tinput_insert_string(tinput_t *ti, const char *str) { size_t ilen = min(str_length(str), INPUT_MAX_SIZE - ti->nc); if (ilen == 0) return; sysarg_t new_width = ti->col0 + ti->nc + ilen; sysarg_t new_height = (new_width / ti->con_cols) + 1; if (new_height >= ti->con_rows) { /* Disallow text longer than 1 page for now. */ return; } if (ti->nc > 0) { size_t i; for (i = ti->nc; i > ti->pos; i--) ti->buffer[i + ilen - 1] = ti->buffer[i - 1]; } size_t off = 0; size_t i = 0; while (i < ilen) { wchar_t c = str_decode(str, &off, STR_NO_LIMIT); if (c == '\0') break; /* Filter out non-printable chars. */ if (c < 32) c = 32; ti->buffer[ti->pos + i] = c; i++; } ti->pos += ilen; ti->nc += ilen; ti->buffer[ti->nc] = '\0'; ti->sel_start = ti->pos; tinput_display_tail(ti, ti->pos - ilen, 0); tinput_update_origin(ti); tinput_position_caret(ti); } static void tinput_backspace(tinput_t *ti) { if (tinput_sel_active(ti)) { tinput_sel_delete(ti); return; } if (ti->pos == 0) return; size_t i; for (i = ti->pos; i < ti->nc; i++) ti->buffer[i - 1] = ti->buffer[i]; ti->pos -= 1; ti->nc -= 1; ti->buffer[ti->nc] = '\0'; ti->sel_start = ti->pos; tinput_display_tail(ti, ti->pos, 1); tinput_position_caret(ti); } static void tinput_delete(tinput_t *ti) { if (tinput_sel_active(ti)) { tinput_sel_delete(ti); return; } if (ti->pos == ti->nc) return; ti->pos += 1; ti->sel_start = ti->pos; tinput_backspace(ti); } static void tinput_seek_cell(tinput_t *ti, seek_dir_t dir, bool shift_held) { tinput_pre_seek(ti, shift_held); if (dir == seek_forward) { if (ti->pos < ti->nc) ti->pos += 1; } else { if (ti->pos > 0) ti->pos -= 1; } tinput_post_seek(ti, shift_held); } static void tinput_seek_word(tinput_t *ti, seek_dir_t dir, bool shift_held) { tinput_pre_seek(ti, shift_held); if (dir == seek_forward) { if (ti->pos == ti->nc) return; while (true) { ti->pos += 1; if (ti->pos == ti->nc) break; if ((ti->buffer[ti->pos - 1] == ' ') && (ti->buffer[ti->pos] != ' ')) break; } } else { if (ti->pos == 0) return; while (true) { ti->pos -= 1; if (ti->pos == 0) break; if (ti->buffer[ti->pos - 1] == ' ' && ti->buffer[ti->pos] != ' ') break; } } tinput_post_seek(ti, shift_held); } static void tinput_seek_vertical(tinput_t *ti, seek_dir_t dir, bool shift_held) { tinput_pre_seek(ti, shift_held); if (dir == seek_forward) { if (ti->pos + ti->con_cols <= ti->nc) ti->pos = ti->pos + ti->con_cols; } else { if (ti->pos >= ti->con_cols) ti->pos = ti->pos - ti->con_cols; } tinput_post_seek(ti, shift_held); } static void tinput_seek_max(tinput_t *ti, seek_dir_t dir, bool shift_held) { tinput_pre_seek(ti, shift_held); if (dir == seek_backward) ti->pos = 0; else ti->pos = ti->nc; tinput_post_seek(ti, shift_held); } static void tinput_pre_seek(tinput_t *ti, bool shift_held) { if ((tinput_sel_active(ti)) && (!shift_held)) { /* Unselect and redraw. */ ti->sel_start = ti->pos; tinput_display_tail(ti, 0, 0); tinput_position_caret(ti); } } static void tinput_post_seek(tinput_t *ti, bool shift_held) { if (shift_held) { /* Selecting text. Need redraw. */ tinput_display_tail(ti, 0, 0); } else { /* Shift not held. Keep selection empty. */ ti->sel_start = ti->pos; } tinput_position_caret(ti); } static void tinput_history_insert(tinput_t *ti, char *str) { if (ti->hnum < HISTORY_LEN) { ti->hnum += 1; } else { if (ti->history[HISTORY_LEN] != NULL) free(ti->history[HISTORY_LEN]); } size_t i; for (i = ti->hnum; i > 1; i--) ti->history[i] = ti->history[i - 1]; ti->history[1] = str_dup(str); if (ti->history[0] != NULL) { free(ti->history[0]); ti->history[0] = NULL; } } static void tinput_set_str(tinput_t *ti, char *str) { str_to_wstr(ti->buffer, INPUT_MAX_SIZE, str); ti->nc = wstr_length(ti->buffer); ti->pos = ti->nc; ti->sel_start = ti->pos; } static void tinput_sel_get_bounds(tinput_t *ti, size_t *sa, size_t *sb) { if (ti->sel_start < ti->pos) { *sa = ti->sel_start; *sb = ti->pos; } else { *sa = ti->pos; *sb = ti->sel_start; } } static bool tinput_sel_active(tinput_t *ti) { return (ti->sel_start != ti->pos); } static void tinput_sel_all(tinput_t *ti) { ti->sel_start = 0; ti->pos = ti->nc; tinput_display_tail(ti, 0, 0); tinput_position_caret(ti); } static void tinput_sel_delete(tinput_t *ti) { size_t sa; size_t sb; tinput_sel_get_bounds(ti, &sa, &sb); if (sa == sb) return; memmove(ti->buffer + sa, ti->buffer + sb, (ti->nc - sb) * sizeof(wchar_t)); ti->pos = ti->sel_start = sa; ti->nc -= (sb - sa); ti->buffer[ti->nc] = '\0'; tinput_display_tail(ti, sa, sb - sa); tinput_position_caret(ti); } static void tinput_sel_copy_to_cb(tinput_t *ti) { size_t sa; size_t sb; tinput_sel_get_bounds(ti, &sa, &sb); char *str; if (sb < ti->nc) { wchar_t tmp_c = ti->buffer[sb]; ti->buffer[sb] = '\0'; str = wstr_to_astr(ti->buffer + sa); ti->buffer[sb] = tmp_c; } else str = wstr_to_astr(ti->buffer + sa); if (str == NULL) goto error; if (clipboard_put_str(str) != EOK) goto error; free(str); return; error: /* TODO: Give the user some kind of warning. */ return; } static void tinput_paste_from_cb(tinput_t *ti) { char *str; int rc = clipboard_get_str(&str); if ((rc != EOK) || (str == NULL)) { /* TODO: Give the user some kind of warning. */ return; } tinput_insert_string(ti, str); free(str); } static void tinput_history_seek(tinput_t *ti, int offs) { if (offs >= 0) { if (ti->hpos + offs > ti->hnum) return; } else { if (ti->hpos < (size_t) -offs) return; } if (ti->history[ti->hpos] != NULL) { free(ti->history[ti->hpos]); ti->history[ti->hpos] = NULL; } ti->history[ti->hpos] = tinput_get_str(ti); ti->hpos += offs; int pad = (int) ti->nc - str_length(ti->history[ti->hpos]); if (pad < 0) pad = 0; tinput_set_str(ti, ti->history[ti->hpos]); tinput_display_tail(ti, 0, pad); tinput_update_origin(ti); tinput_position_caret(ti); } /** Initialize text input field. * * Must be called before using the field. It clears the history. */ static void tinput_init(tinput_t *ti) { ti->console = console_init(stdin, stdout); ti->hnum = 0; ti->hpos = 0; ti->history[0] = NULL; } /** Read in one line of input. * * @param ti Text input. * @param dstr Place to save pointer to new string. * * @return EOK on success * @return ENOENT if user requested abort * @return EIO if communication with console failed * */ int tinput_read(tinput_t *ti, char **dstr) { console_flush(ti->console); if (console_get_size(ti->console, &ti->con_cols, &ti->con_rows) != EOK) return EIO; if (console_get_pos(ti->console, &ti->col0, &ti->row0) != EOK) return EIO; ti->pos = 0; ti->sel_start = 0; ti->nc = 0; ti->buffer[0] = '\0'; ti->done = false; ti->exit_clui = false; while (!ti->done) { console_flush(ti->console); kbd_event_t ev; if (!console_get_kbd_event(ti->console, &ev)) return EIO; if (ev.type != KEY_PRESS) continue; if (((ev.mods & KM_CTRL) != 0) && ((ev.mods & (KM_ALT | KM_SHIFT)) == 0)) tinput_key_ctrl(ti, &ev); if (((ev.mods & KM_SHIFT) != 0) && ((ev.mods & (KM_CTRL | KM_ALT)) == 0)) tinput_key_shift(ti, &ev); if (((ev.mods & KM_CTRL) != 0) && ((ev.mods & KM_SHIFT) != 0) && ((ev.mods & KM_ALT) == 0)) tinput_key_ctrl_shift(ti, &ev); if ((ev.mods & (KM_CTRL | KM_ALT | KM_SHIFT)) == 0) tinput_key_unmod(ti, &ev); if (ev.c >= ' ') { tinput_sel_delete(ti); tinput_insert_char(ti, ev.c); } } if (ti->exit_clui) return ENOENT; ti->pos = ti->nc; tinput_position_caret(ti); putchar('\n'); char *str = tinput_get_str(ti); if (str_cmp(str, "") != 0) tinput_history_insert(ti, str); ti->hpos = 0; *dstr = str; return EOK; } static void tinput_key_ctrl(tinput_t *ti, kbd_event_t *ev) { switch (ev->key) { case KC_LEFT: tinput_seek_word(ti, seek_backward, false); break; case KC_RIGHT: tinput_seek_word(ti, seek_forward, false); break; case KC_UP: tinput_seek_vertical(ti, seek_backward, false); break; case KC_DOWN: tinput_seek_vertical(ti, seek_forward, false); break; case KC_X: tinput_sel_copy_to_cb(ti); tinput_sel_delete(ti); break; case KC_C: tinput_sel_copy_to_cb(ti); break; case KC_V: tinput_sel_delete(ti); tinput_paste_from_cb(ti); break; case KC_A: tinput_sel_all(ti); break; case KC_Q: /* Signal libary client to quit interactive loop. */ ti->done = true; ti->exit_clui = true; break; default: break; } } static void tinput_key_ctrl_shift(tinput_t *ti, kbd_event_t *ev) { switch (ev->key) { case KC_LEFT: tinput_seek_word(ti, seek_backward, true); break; case KC_RIGHT: tinput_seek_word(ti, seek_forward, true); break; case KC_UP: tinput_seek_vertical(ti, seek_backward, true); break; case KC_DOWN: tinput_seek_vertical(ti, seek_forward, true); break; default: break; } } static void tinput_key_shift(tinput_t *ti, kbd_event_t *ev) { switch (ev->key) { case KC_LEFT: tinput_seek_cell(ti, seek_backward, true); break; case KC_RIGHT: tinput_seek_cell(ti, seek_forward, true); break; case KC_UP: tinput_seek_vertical(ti, seek_backward, true); break; case KC_DOWN: tinput_seek_vertical(ti, seek_forward, true); break; case KC_HOME: tinput_seek_max(ti, seek_backward, true); break; case KC_END: tinput_seek_max(ti, seek_forward, true); break; default: break; } } static void tinput_key_unmod(tinput_t *ti, kbd_event_t *ev) { switch (ev->key) { case KC_ENTER: case KC_NENTER: ti->done = true; break; case KC_BACKSPACE: tinput_backspace(ti); break; case KC_DELETE: tinput_delete(ti); break; case KC_LEFT: tinput_seek_cell(ti, seek_backward, false); break; case KC_RIGHT: tinput_seek_cell(ti, seek_forward, false); break; case KC_HOME: tinput_seek_max(ti, seek_backward, false); break; case KC_END: tinput_seek_max(ti, seek_forward, false); break; case KC_UP: tinput_history_seek(ti, 1); break; case KC_DOWN: tinput_history_seek(ti, -1); break; default: break; } }