diff options
| -rw-r--r-- | line-editor.c | 288 | ||||
| -rw-r--r-- | nncmpp.c | 322 | 
2 files changed, 347 insertions, 263 deletions
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 <p@janouch.name> + * + * 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; +} @@ -66,6 +66,9 @@ enum  #include "liberty/liberty.c"  #include "liberty/liberty-tui.c" +#define HAVE_LIBERTY +#include "line-editor.c" +  #include <math.h>  #include <locale.h>  #include <termios.h> @@ -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); @@ -1362,53 +1354,6 @@ app_write_mpd_status (struct row_buffer *buf)  }  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)  {  	int caret = -1; @@ -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  | 
