From f241a7016adf668cefca2c1b77cc756a389a309b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?= Date: Sat, 20 Oct 2018 09:34:12 +0200 Subject: Move the line editor into its own file Trying to make it reusable in other projects. --- line-editor.c | 288 +++++++++++++++++++++++++++++++++++++++++++++++++++ nncmpp.c | 322 +++++++++++----------------------------------------------- 2 files changed, 347 insertions(+), 263 deletions(-) create mode 100644 line-editor.c diff --git a/line-editor.c b/line-editor.c new file mode 100644 index 0000000..708afd4 --- /dev/null +++ b/line-editor.c @@ -0,0 +1,288 @@ +/* + * line-editor.c: a line editor component for the TUI part of liberty + * + * Copyright (c) 2017 - 2018, Přemysl Janouch + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ + +// This is here just for IDE code model reasons +#ifndef HAVE_LIBERTY +#include "liberty/liberty.c" +#include "liberty/liberty-tui.c" +#endif + +static void +row_buffer_append_c (struct row_buffer *self, ucs4_t c, chtype attrs) +{ + struct row_char current = { .attrs = attrs, .c = c }; + struct row_char invalid = { .attrs = attrs, .c = '?', .width = 1 }; + + current.width = uc_width (current.c, locale_charset ()); + if (current.width < 0 || !app_is_character_in_locale (current.c)) + current = invalid; + + ARRAY_RESERVE (self->chars, 1); + self->chars[self->chars_len++] = current; + self->total_width += current.width; +} + +// --- Line editor ------------------------------------------------------------- + +enum line_editor_action +{ + LINE_EDITOR_B_CHAR, ///< Go back a character + LINE_EDITOR_F_CHAR, ///< Go forward a character + LINE_EDITOR_B_WORD, ///< Go back a word + LINE_EDITOR_F_WORD, ///< Go forward a word + LINE_EDITOR_HOME, ///< Go to start of line + LINE_EDITOR_END, ///< Go to end of line + + LINE_EDITOR_B_DELETE, ///< Delete last character + LINE_EDITOR_F_DELETE, ///< Delete next character + LINE_EDITOR_B_KILL_WORD, ///< Delete last word + LINE_EDITOR_B_KILL_LINE, ///< Delete everything up to BOL + LINE_EDITOR_F_KILL_LINE, ///< Delete everything up to EOL +}; + +struct line_editor +{ + int point; ///< Caret index into line data + ucs4_t *line; ///< Line data, 0-terminated + int *w; ///< Codepoint widths, 0-terminated + size_t len; ///< Editor length + size_t alloc; ///< Editor allocated + char prompt; ///< Prompt character + + void (*on_changed) (void); ///< Callback on text change + void (*on_end) (bool); ///< Callback on abort +}; + +static void +line_editor_free (struct line_editor *self) +{ + free (self->line); + free (self->w); +} + +/// Notify whomever invoked the editor that it's been either confirmed or +/// cancelled and clean up editor state +static void +line_editor_abort (struct line_editor *self, bool status) +{ + self->on_end (status); + self->on_changed = NULL; + + free (self->line); + self->line = NULL; + free (self->w); + self->w = NULL; + self->alloc = 0; + self->len = 0; + self->point = 0; + self->prompt = 0; +} + +/// Start the line editor; remember to fill in "change" and "end" callbacks +static void +line_editor_start (struct line_editor *self, char prompt) +{ + self->alloc = 16; + self->line = xcalloc (sizeof *self->line, self->alloc); + self->w = xcalloc (sizeof *self->w, self->alloc); + self->len = 0; + self->point = 0; + self->prompt = prompt; +} + +static void +line_editor_changed (struct line_editor *self) +{ + self->line[self->len] = 0; + self->w[self->len] = 0; + + if (self->on_changed) + self->on_changed (); +} + +static void +line_editor_move (struct line_editor *self, int to, int from, int len) +{ + memmove (self->line + to, self->line + from, + sizeof *self->line * len); + memmove (self->w + to, self->w + from, + sizeof *self->w * len); +} + +static void +line_editor_insert (struct line_editor *self, ucs4_t codepoint) +{ + while (self->alloc - self->len < 2 /* inserted + sentinel */) + { + self->alloc <<= 1; + self->line = xreallocarray + (self->line, sizeof *self->line, self->alloc); + self->w = xreallocarray + (self->w, sizeof *self->w, self->alloc); + } + + line_editor_move (self, self->point + 1, self->point, + self->len - self->point); + self->line[self->point] = codepoint; + self->w[self->point] = app_is_character_in_locale (codepoint) + ? uc_width (codepoint, locale_charset ()) + : 1 /* the replacement question mark */; + + self->point++; + self->len++; + line_editor_changed (self); +} + +static bool +line_editor_action (struct line_editor *self, enum line_editor_action action) +{ + switch (action) + { + default: + return soft_assert (!"unknown line editor action"); + + case LINE_EDITOR_B_CHAR: + if (self->point < 1) + return false; + do self->point--; + while (self->point > 0 + && !self->w[self->point]); + return true; + case LINE_EDITOR_F_CHAR: + if (self->point + 1 > (int) self->len) + return false; + do self->point++; + while (self->point < (int) self->len + && !self->w[self->point]); + return true; + case LINE_EDITOR_B_WORD: + { + if (self->point < 1) + return false; + int i = self->point; + while (i && self->line[--i] == ' '); + while (i-- && self->line[i] != ' '); + self->point = ++i; + return true; + } + case LINE_EDITOR_F_WORD: + { + if (self->point + 1 > (int) self->len) + return false; + int i = self->point; + while (i < (int) self->len && self->line[i] != ' ') i++; + while (i < (int) self->len && self->line[i] == ' ') i++; + self->point = i; + return true; + } + case LINE_EDITOR_HOME: + self->point = 0; + return true; + case LINE_EDITOR_END: + self->point = self->len; + return true; + + case LINE_EDITOR_B_DELETE: + { + if (self->point < 1) + return false; + int len = 1; + while (self->point - len > 0 + && !self->w[self->point - len]) + len++; + line_editor_move (self, self->point - len, self->point, + self->len - self->point); + self->len -= len; + self->point -= len; + line_editor_changed (self); + return true; + } + case LINE_EDITOR_F_DELETE: + { + if (self->point + 1 > (int) self->len) + return false; + int len = 1; + while (self->point + len < (int) self->len + && !self->w[self->point + len]) + len++; + self->len -= len; + line_editor_move (self, self->point, self->point + len, + self->len - self->point); + line_editor_changed (self); + return true; + } + case LINE_EDITOR_B_KILL_WORD: + { + if (self->point < 1) + return false; + + int i = self->point; + while (i && self->line[--i] == ' '); + while (i-- && self->line[i] != ' '); + i++; + + line_editor_move (self, i, self->point, (self->len - self->point)); + self->len -= self->point - i; + self->point = i; + line_editor_changed (self); + return true; + } + case LINE_EDITOR_B_KILL_LINE: + self->len -= self->point; + line_editor_move (self, 0, self->point, self->len); + self->point = 0; + line_editor_changed (self); + return true; + case LINE_EDITOR_F_KILL_LINE: + self->len = self->point; + line_editor_changed (self); + return true; + } +} + +static int +line_editor_write (const struct line_editor *self, struct row_buffer *row, + int width, chtype attrs) +{ + if (self->prompt) + { + hard_assert (self->prompt < 127); + row_buffer_append_c (row, self->prompt, attrs); + width--; + } + + int following = 0; + for (size_t i = self->point; i < self->len; i++) + following += self->w[i]; + + int preceding = 0; + size_t start = self->point; + while (start && preceding < width / 2) + preceding += self->w[--start]; + + // There can be one extra space at the end of the line but this way we + // don't need to care about non-spacing marks following full-width chars + while (start && width - preceding - following > 2 /* widest char */) + preceding += self->w[--start]; + + // XXX: we should also show < > indicators for overflow but it'd probably + // considerably complicate this algorithm + for (; start < self->len; start++) + row_buffer_append_c (row, self->line[start], attrs); + return !!self->prompt + preceding; +} diff --git a/nncmpp.c b/nncmpp.c index aff53e1..2b20f2b 100644 --- a/nncmpp.c +++ b/nncmpp.c @@ -66,6 +66,9 @@ enum #include "liberty/liberty.c" #include "liberty/liberty-tui.c" +#define HAVE_LIBERTY +#include "line-editor.c" + #include #include #include @@ -630,19 +633,9 @@ static struct app_context int gauge_offset; ///< Offset to the gauge or -1 int gauge_width; ///< Width of the gauge, if present + struct line_editor editor; ///< Line editor struct poller_idle refresh_event; ///< Refresh the screen - // Line editor: - - int editor_point; ///< Caret index into line data - ucs4_t *editor_line; ///< Line data, 0-terminated - int *editor_w; ///< Codepoint widths, 0-terminated - size_t editor_len; ///< Editor length - size_t editor_alloc; ///< Editor allocated - char editor_prompt; ///< Prompt character - void (*on_editor_changed) (void); ///< Callback on text change - void (*on_editor_end) (bool); ///< Callback on abort - // Terminal: termo_t *tk; ///< termo handle @@ -886,8 +879,7 @@ app_free_context (void) strv_free (&g.streams); item_list_free (&g.playlist); - free (g.editor_line); - free (g.editor_w); + line_editor_free (&g.editor); config_free (&g.config); poller_free (&g.poller); @@ -1361,53 +1353,6 @@ app_write_mpd_status (struct row_buffer *buf) row_buffer_free (&right); } -static void -row_buffer_append_c (struct row_buffer *self, ucs4_t c, chtype attrs) -{ - struct row_char current = { .attrs = attrs, .c = c }; - struct row_char invalid = { .attrs = attrs, .c = '?', .width = 1 }; - - current.width = uc_width (current.c, locale_charset ()); - if (current.width < 0 || !app_is_character_in_locale (current.c)) - current = invalid; - - ARRAY_RESERVE (self->chars, 1); - self->chars[self->chars_len++] = current; - self->total_width += current.width; -} - -static int -app_write_editor (struct row_buffer *row) -{ - int limit = COLS; - if (g.editor_prompt) - { - hard_assert (g.editor_prompt < 127); - row_buffer_append_c (row, g.editor_prompt, APP_ATTR (HIGHLIGHT)); - limit--; - } - - int following = 0; - for (size_t i = g.editor_point; i < g.editor_len; i++) - following += g.editor_w[i]; - - int preceding = 0; - size_t start = g.editor_point; - while (start && preceding < limit / 2) - preceding += g.editor_w[--start]; - - // There can be one extra space at the end of the line but this way we - // don't need to care about non-spacing marks following full-width chars - while (start && limit - preceding - following > 2 /* widest char */) - preceding += g.editor_w[--start]; - - // XXX: we should also show < > indicators for overflow but it'd probably - // considerably complicate this algorithm - for (; start < g.editor_len; start++) - row_buffer_append_c (row, g.editor_line[start], APP_ATTR (HIGHLIGHT)); - return !!g.editor_prompt + preceding; -} - static void app_draw_statusbar (void) { @@ -1416,8 +1361,8 @@ app_draw_statusbar (void) struct row_buffer buf = row_buffer_make (); if (g.message) row_buffer_append (&buf, g.message, APP_ATTR (HIGHLIGHT)); - else if (g.editor_line) - caret = app_write_editor (&buf); + else if (g.editor.line) + caret = line_editor_write (&g.editor, &buf, COLS, APP_ATTR (HIGHLIGHT)); else if (g.client.state == MPD_CONNECTED) app_write_mpd_status (&buf); @@ -1645,199 +1590,6 @@ action_resolve (const char *name) return -1; } -// --- Line editor ------------------------------------------------------------- - -// TODO: move the editor out as a component to liberty-tui.c - -/// Notify whomever invoked the editor that it's been either confirmed or -/// cancelled and clean up editor state -static void -app_editor_abort (bool status) -{ - g.on_editor_end (status); - g.on_editor_changed = NULL; - g.on_editor_end = NULL; - - free (g.editor_line); - g.editor_line = NULL; - free (g.editor_w); - g.editor_w = NULL; - g.editor_alloc = 0; - g.editor_len = 0; - g.editor_point = 0; - g.editor_prompt = 0; -} - -/// Start the line editor; remember to fill in "change" and "abort" callbacks -static void -app_editor_start (char prompt) -{ - g.editor_alloc = 16; - g.editor_line = xcalloc (sizeof *g.editor_line, g.editor_alloc); - g.editor_w = xcalloc (sizeof *g.editor_w, g.editor_alloc); - g.editor_len = 0; - g.editor_point = 0; - g.editor_prompt = prompt; - app_invalidate (); -} - -static void -app_editor_changed (void) -{ - g.editor_line[g.editor_len] = 0; - g.editor_w[g.editor_len] = 0; - - if (g.on_editor_changed) - g.on_editor_changed (); -} - -static void -app_editor_move (int to, int from, int len) -{ - memmove (g.editor_line + to, g.editor_line + from, - sizeof *g.editor_line * len); - memmove (g.editor_w + to, g.editor_w + from, - sizeof *g.editor_w * len); -} - -static bool -app_editor_process_action (enum action action) -{ - app_invalidate (); - switch (action) - { - case ACTION_QUIT: - app_editor_abort (false); - return true; - case ACTION_EDITOR_CONFIRM: - app_editor_abort (true); - return true; - default: - return false; - - case ACTION_EDITOR_B_CHAR: - if (g.editor_point < 1) - return false; - do g.editor_point--; - while (g.editor_point > 0 - && !g.editor_w[g.editor_point]); - return true; - case ACTION_EDITOR_F_CHAR: - if (g.editor_point + 1 > (int) g.editor_len) - return false; - do g.editor_point++; - while (g.editor_point < (int) g.editor_len - && !g.editor_w[g.editor_point]); - return true; - case ACTION_EDITOR_B_WORD: - { - if (g.editor_point < 1) - return false; - int i = g.editor_point; - while (i && g.editor_line[--i] == ' '); - while (i-- && g.editor_line[i] != ' '); - g.editor_point = ++i; - return true; - } - case ACTION_EDITOR_F_WORD: - { - if (g.editor_point + 1 > (int) g.editor_len) - return false; - int i = g.editor_point; - while (i < (int) g.editor_len && g.editor_line[i] != ' ') i++; - while (i < (int) g.editor_len && g.editor_line[i] == ' ') i++; - g.editor_point = i; - return true; - } - case ACTION_EDITOR_HOME: - g.editor_point = 0; - return true; - case ACTION_EDITOR_END: - g.editor_point = g.editor_len; - return true; - - case ACTION_EDITOR_B_DELETE: - { - if (g.editor_point < 1) - return false; - int len = 1; - while (g.editor_point - len > 0 - && !g.editor_w[g.editor_point - len]) - len++; - app_editor_move (g.editor_point - len, g.editor_point, - g.editor_len - g.editor_point); - g.editor_len -= len; - g.editor_point -= len; - app_editor_changed (); - return true; - } - case ACTION_EDITOR_F_DELETE: - { - if (g.editor_point + 1 > (int) g.editor_len) - return false; - int len = 1; - while (g.editor_point + len < (int) g.editor_len - && !g.editor_w[g.editor_point + len]) - len++; - g.editor_len -= len; - app_editor_move (g.editor_point, g.editor_point + len, - g.editor_len - g.editor_point); - app_editor_changed (); - return true; - } - case ACTION_EDITOR_B_KILL_WORD: - { - if (g.editor_point < 1) - return false; - - int i = g.editor_point; - while (i && g.editor_line[--i] == ' '); - while (i-- && g.editor_line[i] != ' '); - i++; - - app_editor_move (i, g.editor_point, (g.editor_len - g.editor_point)); - g.editor_len -= g.editor_point - i; - g.editor_point = i; - app_editor_changed (); - return true; - } - case ACTION_EDITOR_B_KILL_LINE: - g.editor_len -= g.editor_point; - app_editor_move (0, g.editor_point, g.editor_len); - g.editor_point = 0; - app_editor_changed (); - return true; - case ACTION_EDITOR_F_KILL_LINE: - g.editor_len = g.editor_point; - app_editor_changed (); - return true; - } -} - -static void -app_editor_insert (ucs4_t codepoint) -{ - while (g.editor_alloc - g.editor_len < 2 /* inserted + sentinel */) - { - g.editor_alloc <<= 1; - g.editor_line = xreallocarray - (g.editor_line, sizeof *g.editor_line, g.editor_alloc); - g.editor_w = xreallocarray - (g.editor_w, sizeof *g.editor_w, g.editor_alloc); - } - - app_editor_move (g.editor_point + 1, g.editor_point, - g.editor_len - g.editor_point); - g.editor_line[g.editor_point] = codepoint; - g.editor_w[g.editor_point] = app_is_character_in_locale (codepoint) - ? uc_width (codepoint, locale_charset ()) - : 1 /* the replacement question mark */; - - g.editor_point++; - g.editor_len++; - app_editor_changed (); -} - // --- User input handling ----------------------------------------------------- static void @@ -1902,7 +1654,7 @@ app_on_editor_end (bool confirmed) return; size_t len; - char *u8 = (char *) u32_to_u8 (g.editor_line, g.editor_len + 1, NULL, &len); + char *u8 = (char *) u32_to_u8 (g.editor.line, g.editor.len + 1, NULL, &len); mpd_client_send_command_raw (c, u8); free (u8); @@ -1932,8 +1684,9 @@ app_process_action (enum action action) app_invalidate (); return true; case ACTION_MPD_COMMAND: - app_editor_start (':'); - g.on_editor_end = app_on_editor_end; + line_editor_start (&g.editor, ':'); + g.editor.on_end = app_on_editor_end; + app_invalidate (); return true; default: return false; @@ -2006,6 +1759,49 @@ app_process_action (enum action action) return false; } +static bool +app_editor_process_action (enum action action) +{ + app_invalidate (); + switch (action) + { + case ACTION_QUIT: + line_editor_abort (&g.editor, false); + g.editor.on_end = NULL; + return true; + case ACTION_EDITOR_CONFIRM: + line_editor_abort (&g.editor, true); + g.editor.on_end = NULL; + return true; + default: + return false; + + case ACTION_EDITOR_B_CHAR: + return line_editor_action (&g.editor, LINE_EDITOR_B_CHAR); + case ACTION_EDITOR_F_CHAR: + return line_editor_action (&g.editor, LINE_EDITOR_F_CHAR); + case ACTION_EDITOR_B_WORD: + return line_editor_action (&g.editor, LINE_EDITOR_B_WORD); + case ACTION_EDITOR_F_WORD: + return line_editor_action (&g.editor, LINE_EDITOR_F_WORD); + case ACTION_EDITOR_HOME: + return line_editor_action (&g.editor, LINE_EDITOR_HOME); + case ACTION_EDITOR_END: + return line_editor_action (&g.editor, LINE_EDITOR_END); + + case ACTION_EDITOR_B_DELETE: + return line_editor_action (&g.editor, LINE_EDITOR_B_DELETE); + case ACTION_EDITOR_F_DELETE: + return line_editor_action (&g.editor, LINE_EDITOR_F_DELETE); + case ACTION_EDITOR_B_KILL_WORD: + return line_editor_action (&g.editor, LINE_EDITOR_B_KILL_WORD); + case ACTION_EDITOR_B_KILL_LINE: + return line_editor_action (&g.editor, LINE_EDITOR_B_KILL_LINE); + case ACTION_EDITOR_F_KILL_LINE: + return line_editor_action (&g.editor, LINE_EDITOR_F_KILL_LINE); + } +} + // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - static bool @@ -2086,8 +1882,8 @@ app_process_mouse (termo_mouse_event_t type, int line, int column, int button, if (type != TERMO_MOUSE_PRESS) return true; - if (g.editor_line) - app_editor_abort (false); + if (g.editor.line) + line_editor_abort (&g.editor, false); if (button == 1) return app_process_left_mouse_click (line, column, double_click); @@ -2270,7 +2066,7 @@ static bool app_process_termo_event (termo_key_t *event) { struct binding dummy = { *event, 0, 0 }, *binding; - if (g.editor_line) + if (g.editor.line) { if ((binding = bsearch (&dummy, g_editor_keys, g_editor_keys_len, sizeof *binding, app_binding_cmp))) @@ -2278,7 +2074,7 @@ app_process_termo_event (termo_key_t *event) if (event->type != TERMO_TYPE_KEY || event->modifiers != 0) return false; - app_editor_insert (event->code.codepoint); + line_editor_insert (&g.editor, event->code.codepoint); app_invalidate (); return true; } @@ -3630,7 +3426,7 @@ app_on_key_timer (void *user_data) termo_key_t event; if (termo_getkey_force (g.tk, &event) == TERMO_RES_KEY) if (!app_process_termo_event (&event)) - app_quit (); + app_quit (); // presumably an ESC, questionable } static void -- cgit v1.2.3