summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYorhel <git@yorhel.nl>2014-04-03 15:25:38 +0200
committerYorhel <git@yorhel.nl>2014-04-03 15:27:07 +0200
commitbc791b28fce1766c71ffe22e7e5b77341e6aa449 (patch)
treece0ccc6b68aa6eff236b916b4b39f22f454b3b8d
parent0198c9ac47b15182a0794969294e30d9f70d68ad (diff)
Split ui_util.c into ui_(colors|listing|logwindow|textinput).c
The file was getting somewhat large and there was a logical separation of interfaces already.
-rw-r--r--Makefile.am10
-rw-r--r--src/ui_colors.c204
-rw-r--r--src/ui_listing.c394
-rw-r--r--src/ui_logwindow.c426
-rw-r--r--src/ui_textinput.c445
-rw-r--r--src/ui_util.c1410
6 files changed, 1477 insertions, 1412 deletions
diff --git a/Makefile.am b/Makefile.am
index f8bb418..c24e7d7 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -70,7 +70,10 @@ ncdc_SOURCES=\
src/uit_msg.c\
src/uit_search.c\
src/uit_userlist.c\
- src/ui_util.c\
+ src/ui_colors.c\
+ src/ui_listing.c\
+ src/ui_logwindow.c\
+ src/ui_textinput.c\
src/util.c\
src/vars.c
@@ -134,6 +137,9 @@ src/uit_main.$(OBJEXT): src/uit_main.h
src/uit_msg.$(OBJEXT): src/uit_msg.h
src/uit_search.$(OBJEXT): src/uit_search.h
src/uit_userlist.$(OBJEXT): src/uit_userlist.h
-src/ui_util.$(OBJEXT): src/ui_util.h
+src/ui_colors.$(OBJEXT): src/ui_colors.h
+src/ui_listing.$(OBJEXT): src/ui_listing.h
+src/ui_logwindow.$(OBJEXT): src/ui_logwindow.h
+src/ui_textinput.$(OBJEXT): src/ui_textinput.h
src/util.$(OBJEXT): src/util.h
src/vars.$(OBJEXT): src/vars.h
diff --git a/src/ui_colors.c b/src/ui_colors.c
new file mode 100644
index 0000000..081942e
--- /dev/null
+++ b/src/ui_colors.c
@@ -0,0 +1,204 @@
+/* ncdc - NCurses Direct Connect client
+
+ Copyright (c) 2011-2014 Yoran Heling
+
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be included
+ in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+*/
+
+#include "ncdc.h"
+#include "ui_colors.h"
+
+
+// colors
+
+#if INTERFACE
+
+#define COLOR_DEFAULT (-1)
+
+// name default value
+#define UI_COLORS \
+ C(list_default, "default")\
+ C(list_header, "default,bold")\
+ C(list_select, "default,bold")\
+ C(log_default, "default")\
+ C(log_highlight, "yellow,bold")\
+ C(log_join, "cyan,bold")\
+ C(log_nick, "default")\
+ C(log_ownnick, "default,bold")\
+ C(log_quit, "cyan")\
+ C(log_time, "black,bold")\
+ C(separator, "default,reverse")\
+ C(tab_active, "default,bold")\
+ C(tabprio_high, "magenta,bold")\
+ C(tabprio_low, "black,bold")\
+ C(tabprio_med, "cyan,bold")\
+ C(title, "default,reverse")
+
+enum ui_coltype {
+#define C(n, d) UIC_##n,
+ UI_COLORS
+#undef C
+ UIC_NONE
+};
+
+
+struct ui_color_t {
+ int var;
+ short fg, bg, d_fg, d_bg;
+ int x, d_x, a;
+};
+
+struct ui_attr_t {
+ char name[11];
+ gboolean color : 1;
+ int attr;
+}
+
+struct ui_cursor_t {
+ int x;
+ int y;
+}
+
+#define UIC(n) (ui_colors[(ui_coltype)UIC_##n].a)
+
+#endif // INTERFACE
+
+
+ui_color_t ui_colors[] = {
+#define C(n, d) { VAR_color_##n },
+ UI_COLORS
+#undef C
+ { -1 }
+};
+
+
+ui_attr_t ui_attr_names[] = {
+ { "black", TRUE, COLOR_BLACK },
+ { "blink", FALSE, A_BLINK },
+ { "blue", TRUE, COLOR_BLUE },
+ { "bold", FALSE, A_BOLD },
+ { "cyan", TRUE, COLOR_CYAN },
+ { "default", TRUE, COLOR_DEFAULT },
+ { "green", TRUE, COLOR_GREEN },
+ { "magenta", TRUE, COLOR_MAGENTA },
+ { "red", TRUE, COLOR_RED },
+ { "reverse", FALSE, A_REVERSE },
+ { "underline", FALSE, A_UNDERLINE },
+ { "white", TRUE, COLOR_WHITE },
+ { "yellow", TRUE, COLOR_YELLOW },
+ { "" }
+};
+
+
+static ui_attr_t *ui_attr_by_name(const char *n) {
+ ui_attr_t *a = ui_attr_names;
+ for(; *a->name; a++)
+ if(strcmp(a->name, n) == 0)
+ return a;
+ return NULL;
+}
+
+
+static char *ui_name_by_attr(int n) {
+ ui_attr_t *a = ui_attr_names;
+ for(; *a->name; a++)
+ if(a->attr == n)
+ return a->name;
+ return NULL;
+}
+
+
+gboolean ui_color_str_parse(const char *str, short *fg, short *bg, int *x, GError **err) {
+ int state = 0; // 0 = no fg, 1 = no bg, 2 = both
+ short f = COLOR_DEFAULT, b = COLOR_DEFAULT;
+ int a = 0;
+ char **args = g_strsplit(str, ",", 0);
+ char **arg = args;
+ for(; arg && *arg; arg++) {
+ g_strstrip(*arg);
+ if(!**arg)
+ continue;
+ ui_attr_t *attr = ui_attr_by_name(*arg);
+ if(!attr) {
+ g_set_error(err, 1, 0, "Unknown color or attribute: %s", *arg);
+ g_strfreev(args);
+ return FALSE;
+ }
+ if(!attr->color)
+ a |= attr->attr;
+ else if(!state) {
+ f = attr->attr;
+ state++;
+ } else if(state == 1) {
+ b = attr->attr;
+ state++;
+ } else {
+ g_set_error(err, 1, 0, "Don't know what to do with a third color: %s", *arg);
+ g_strfreev(args);
+ return FALSE;
+ }
+ }
+ g_strfreev(args);
+ if(fg) *fg = f;
+ if(bg) *bg = b;
+ if(x) *x = a;
+ return TRUE;
+}
+
+
+char *ui_color_str_gen(int fd, int bg, int x) {
+ static char buf[100]; // must be smaller than (max_color_name * 2) + (max_attr_name * 3) + 6
+ strcpy(buf, ui_name_by_attr(fd));
+ if(bg != COLOR_DEFAULT) {
+ strcat(buf, ",");
+ strcat(buf, ui_name_by_attr(bg));
+ }
+ ui_attr_t *attr = ui_attr_names;
+ for(; attr->name[0]; attr++)
+ if(!attr->color && x & attr->attr) {
+ strcat(buf, ",");
+ strcat(buf, attr->name);
+ }
+ return buf;
+}
+
+
+// TODO: re-use color pairs when we have too many (>64) color groups
+void ui_colors_update() {
+ int pair = 0;
+ ui_color_t *c = ui_colors;
+ for(; c->var>=0; c++) {
+ g_warn_if_fail(ui_color_str_parse(var_get(0, c->var), &c->fg, &c->bg, &c->x, NULL));
+ init_pair(++pair, c->fg, c->bg);
+ c->a = c->x | COLOR_PAIR(pair);
+ }
+}
+
+
+void ui_colors_init() {
+ if(!has_colors())
+ return;
+
+ start_color();
+ use_default_colors();
+
+ ui_colors_update();
+}
diff --git a/src/ui_listing.c b/src/ui_listing.c
new file mode 100644
index 0000000..f36583d
--- /dev/null
+++ b/src/ui_listing.c
@@ -0,0 +1,394 @@
+/* ncdc - NCurses Direct Connect client
+
+ Copyright (c) 2011-2014 Yoran Heling
+
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be included
+ in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+*/
+
+#include "ncdc.h"
+#include "ui_listing.h"
+
+// Generic listing "widget".
+// This widget allows easy listing, selecting and paging of (dynamic) GSequence
+// lists. The list is managed by the user, but the widget does need to be
+// notified of insertions and deletions.
+
+#if INTERFACE
+
+struct ui_listing_t {
+ GSequence *list;
+ GSequenceIter *sel;
+ GSequenceIter *top;
+ gboolean topisbegin;
+ gboolean selisbegin;
+ gboolean (*skip)(ui_listing_t *, GSequenceIter *, void *);
+ void *dat;
+
+ // fields needed for searching
+ ui_textinput_t *search_box;
+ gchar *query;
+ gint match_start;
+ gint match_end;
+ const char *(*to_string)(GSequenceIter *);
+}
+
+#endif
+
+// error values for ui_listing_t.match_start
+#define REGEX_NO_MATCH -1
+#define REGEX_ERROR -2
+
+
+// TODO: This can be relatively slow (linear search), is used often but rarely
+// changes. Cache this in the struct if it becomes a problem.
+static GSequenceIter *ui_listing_getbegin(ui_listing_t *ul) {
+ GSequenceIter *i = g_sequence_get_begin_iter(ul->list);
+ while(!g_sequence_iter_is_end(i) && ul->skip && ul->skip(ul, i, ul->dat))
+ i = g_sequence_iter_next(i);
+ return i;
+}
+
+
+static GSequenceIter *ui_listing_next(ui_listing_t *ul, GSequenceIter *i) {
+ do
+ i = g_sequence_iter_next(i);
+ while(!g_sequence_iter_is_end(i) && ul->skip && ul->skip(ul, i, ul->dat));
+ return i;
+}
+
+
+static GSequenceIter *ui_listing_prev(ui_listing_t *ul, GSequenceIter *i) {
+ GSequenceIter *begin = ui_listing_getbegin(ul);
+ do
+ i = g_sequence_iter_prev(i);
+ while(!g_sequence_iter_is_begin(i) && i != begin && ul->skip && ul->skip(ul, i, ul->dat));
+ if(g_sequence_iter_is_begin(i))
+ i = begin;
+ return i;
+}
+
+
+// update top/sel in case they used to be the start of the list but aren't anymore
+void ui_listing_inserted(ui_listing_t *ul) {
+ GSequenceIter *begin = ui_listing_getbegin(ul);
+ if(!!ul->topisbegin != !!(ul->top == begin))
+ ul->top = ui_listing_getbegin(ul);
+ if(!!ul->selisbegin != !!(ul->sel == begin))
+ ul->sel = ui_listing_getbegin(ul);
+}
+
+
+// called after the order of the list has changed
+// update sel in case it used to be the start of the list but isn't anymore
+void ui_listing_sorted(ui_listing_t *ul) {
+ if(!!ul->selisbegin != !!(ul->sel == ui_listing_getbegin(ul)))
+ ul->sel = ui_listing_getbegin(ul);
+}
+
+
+static void ui_listing_updateisbegin(ui_listing_t *ul) {
+ GSequenceIter *begin = ui_listing_getbegin(ul);
+ ul->topisbegin = ul->top == begin;
+ ul->selisbegin = ul->sel == begin;
+}
+
+
+// update top/sel in case one of them is removed.
+// call this before using g_sequence_remove()
+void ui_listing_remove(ui_listing_t *ul, GSequenceIter *iter) {
+ if(ul->top == iter)
+ ul->top = ui_listing_prev(ul, iter);
+ if(ul->top == iter)
+ ul->top = ui_listing_next(ul, iter);
+ if(ul->sel == iter) {
+ ul->sel = ui_listing_next(ul, iter);
+ if(g_sequence_iter_is_end(ul->sel))
+ ul->sel = ui_listing_prev(ul, iter);
+ if(ul->sel == iter)
+ ul->sel = g_sequence_get_end_iter(ul->list);
+ }
+ ui_listing_updateisbegin(ul);
+}
+
+
+// called when the skip() function changes behaviour (i.e. some items that were
+// skipped aren't now or the other way around).
+void ui_listing_skipchanged(ui_listing_t *ul) {
+ // sel got hidden? oops!
+ if(!g_sequence_iter_is_end(ul->sel) && ul->skip(ul, ul->sel, ul->dat)) {
+ ul->sel = ui_listing_next(ul, ul->sel);
+ if(g_sequence_iter_is_end(ul->sel))
+ ul->sel = ui_listing_prev(ul, ul->sel);
+ }
+ // top got hidden? oops as well
+ if(!g_sequence_iter_is_end(ul->top) && ul->skip(ul, ul->top, ul->dat))
+ ul->top = ui_listing_prev(ul, ul->top);
+ ui_listing_updateisbegin(ul);
+}
+
+
+ui_listing_t *ui_listing_create(GSequence *list, gboolean (*skip)(ui_listing_t *, GSequenceIter *, void *), void *dat, const char *(*to_string)(GSequenceIter *)) {
+ ui_listing_t *ul = g_slice_new0(ui_listing_t);
+ ul->list = list;
+ ul->sel = ul->top = ui_listing_getbegin(ul);
+ ul->topisbegin = ul->selisbegin = TRUE;
+ ul->skip = skip;
+ ul->dat = dat;
+
+ ul->search_box = NULL;
+ ul->query = NULL;
+ ul->match_start = REGEX_NO_MATCH;
+ ul->to_string = to_string;
+
+ return ul;
+}
+
+
+void ui_listing_free(ui_listing_t *ul) {
+ if(ul->search_box)
+ ui_textinput_free(ul->search_box);
+ if(ul->query)
+ g_free(ul->query);
+ g_slice_free(ui_listing_t, ul);
+}
+
+
+// search next/previous
+static void ui_listing_search_advance(ui_listing_t *ul, GSequenceIter *startpos, gboolean prev) {
+ if(g_sequence_iter_is_end(startpos) && g_sequence_iter_is_end((startpos = ui_listing_getbegin(ul))))
+ return;
+ GRegex *regex = ul->query ? g_regex_new(ul->query, G_REGEX_CASELESS | G_REGEX_OPTIMIZE, 0, NULL) : NULL;
+ if(!regex) {
+ ul->match_start = REGEX_ERROR;
+ return;
+ }
+ ul->match_start = REGEX_NO_MATCH;
+
+ GSequenceIter *pos = startpos;
+ do {
+ const char *candidate = ul->to_string(pos);
+ GMatchInfo *match_info;
+ if(g_regex_match(regex, candidate, 0, &match_info)) {
+ g_match_info_fetch_pos(match_info, 0, &ul->match_start, &ul->match_end);
+ g_match_info_free(match_info);
+ ul->sel = pos;
+ break;
+ }
+ g_match_info_free(match_info);
+
+ pos = (prev ? ui_listing_prev : ui_listing_next)(ul, pos);
+ if(g_sequence_iter_is_begin(pos))
+ pos = ui_listing_prev(ul, g_sequence_get_end_iter(ul->list));
+ else if(g_sequence_iter_is_end(pos))
+ pos = ui_listing_getbegin(ul);
+ } while(ul->match_start == REGEX_NO_MATCH && pos != startpos);
+
+ g_regex_unref(regex);
+}
+
+
+// handle keys in search mode
+static void ui_listing_search(ui_listing_t *ul, guint64 key) {
+ char *completed = NULL;
+ ui_textinput_key(ul->search_box, key, &completed);
+
+ g_free(ul->query);
+ ul->query = completed ? completed : ui_textinput_get(ul->search_box);
+
+ if(completed) {
+ if(ul->match_start < 0) {
+ g_free(ul->query);
+ ul->query = NULL;
+ }
+ ui_textinput_free(ul->search_box);
+ ul->search_box = NULL;
+ ul->match_start = -1;
+ ul->match_end = -1;
+ } else
+ ui_listing_search_advance(ul, ul->sel, FALSE);
+}
+
+
+gboolean ui_listing_key(ui_listing_t *ul, guint64 key, int page) {
+ if(ul->search_box) {
+ ui_listing_search(ul, key);
+ return TRUE;
+ }
+
+ // stop highlighting
+ ul->match_start = REGEX_NO_MATCH;
+
+ switch(key) {
+ case INPT_CHAR('/'): // start search mode
+ if(ul->to_string) {
+ if(ul->query) {
+ g_free(ul->query);
+ ul->query = NULL;
+ }
+ g_assert(!ul->search_box);
+ ul->search_box = ui_textinput_create(FALSE, NULL);
+ }
+ break;
+ case INPT_CHAR(','): // find next
+ ui_listing_search_advance(ul, ui_listing_next(ul, ul->sel), FALSE);
+ break;
+ case INPT_CHAR('.'): // find previous
+ ui_listing_search_advance(ul, ui_listing_prev(ul, ul->sel), TRUE);
+ break;
+ case INPT_KEY(KEY_NPAGE): { // page down
+ int i = page;
+ while(i-- && !g_sequence_iter_is_end(ul->sel))
+ ul->sel = ui_listing_next(ul, ul->sel);
+ if(g_sequence_iter_is_end(ul->sel))
+ ul->sel = ui_listing_prev(ul, ul->sel);
+ break;
+ }
+ case INPT_KEY(KEY_PPAGE): { // page up
+ int i = page;
+ GSequenceIter *begin = ui_listing_getbegin(ul);
+ while(i-- && ul->sel != begin)
+ ul->sel = ui_listing_prev(ul, ul->sel);
+ break;
+ }
+ case INPT_KEY(KEY_DOWN): // item down
+ case INPT_CHAR('j'):
+ ul->sel = ui_listing_next(ul, ul->sel);
+ if(g_sequence_iter_is_end(ul->sel))
+ ul->sel = ui_listing_prev(ul, ul->sel);
+ break;
+ case INPT_KEY(KEY_UP): // item up
+ case INPT_CHAR('k'):
+ ul->sel = ui_listing_prev(ul, ul->sel);
+ break;
+ case INPT_KEY(KEY_HOME): // home
+ ul->sel = ui_listing_getbegin(ul);
+ break;
+ case INPT_KEY(KEY_END): // end
+ ul->sel = ui_listing_prev(ul, g_sequence_get_end_iter(ul->list));
+ break;
+ default:
+ return FALSE;
+ }
+
+ ui_listing_updateisbegin(ul);
+ return TRUE;
+}
+
+
+static void ui_listing_fixtop(ui_listing_t *ul, int height) {
+ // sel before top? top = sel!
+ if(g_sequence_iter_compare(ul->top, ul->sel) > 0)
+ ul->top = ul->sel;
+
+ // does sel still fit on the screen?
+ int i = height;
+ GSequenceIter *n = ul->top;
+ while(n != ul->sel && i > 0) {
+ n = ui_listing_next(ul, n);
+ i--;
+ }
+
+ // Nope? Make sure it fits
+ if(i <= 0) {
+ n = ul->sel;
+ for(i=0; i<height-1; i++)
+ n = ui_listing_prev(ul, n);
+ ul->top = n;
+ }
+
+ // Make sure there's no empty space if we have enough rows to fill the screen
+ i = height;
+ n = ul->top;
+ GSequenceIter *begin = ui_listing_getbegin(ul);
+ while(!g_sequence_iter_is_end(n) && i-- > 0)
+ n = ui_listing_next(ul, n);
+ while(ul->top != begin && i-- > 0)
+ ul->top = ui_listing_prev(ul, ul->top);
+}
+
+
+// Every item is assumed to occupy exactly one line.
+// Returns the relative position of the current page (in percent).
+// The selected row number is written to *cur, to be used with move(cur, 0).
+// TODO: The return value is only correct if no skip function is used or if
+// there are otherwise no hidden rows. It'll give a blatantly wrong number if
+// there are.
+int ui_listing_draw(ui_listing_t *ul, int top, int bottom, ui_cursor_t *cur, void (*cb)(ui_listing_t *, GSequenceIter *, int, void *)) {
+ int search_box_height = !!ul->search_box;
+ int listing_height = 1 + bottom - top - search_box_height;
+ ui_listing_fixtop(ul, listing_height);
+
+ if(cur) {
+ cur->x = 0;
+ cur->y = top;
+ }
+
+ // draw
+ GSequenceIter *n = ul->top;
+ while(top <= bottom - search_box_height && !g_sequence_iter_is_end(n)) {
+ if(cur && n == ul->sel)
+ cur->y = top;
+ cb(ul, n, top, ul->dat);
+ n = ui_listing_next(ul, n);
+ top++;
+ }
+ if(ul->search_box) {
+ const char *status;
+ switch(ul->match_start) {
+ case REGEX_NO_MATCH:
+ status = "no match>";
+ break;
+ case REGEX_ERROR:
+ status = " invalid>";
+ break;
+ default:
+ status = " search>";
+ }
+ mvaddstr(bottom, 0, status);
+ ui_textinput_draw(ul->search_box, bottom, 10, wincols - 10, cur);
+ }
+
+ ui_listing_updateisbegin(ul);
+
+ int last = g_sequence_iter_get_position(g_sequence_get_end_iter(ul->list));
+ return MIN(100, last ? (g_sequence_iter_get_position(ul->top)+listing_height)*100/last : 0);
+}
+
+
+void ui_listing_draw_match(ui_listing_t *ul, GSequenceIter *iter, int y, int x, int max) {
+ const char *str = ul->to_string(iter);
+ if(ul->sel == iter && ul->match_start >= 0) {
+ int ofs1 = 0,
+ ofs2 = ul->match_start,
+ ofs3 = ul->match_end,
+ width1 = substr_columns(str + ofs1, ofs2 - ofs1),
+ width2 = substr_columns(str + ofs2, ofs3 - ofs2);
+ mvaddnstr(y, x, str + ofs1, str_offset_from_columns(str + ofs1, MIN(width1, max)));
+ x += width1, max -= width1;
+ attron(A_REVERSE);
+ mvaddnstr(y, x, str + ofs2, str_offset_from_columns(str + ofs2, MIN(width2, max)));
+ x += width2, max -= width2;
+ attroff(A_REVERSE);
+ mvaddnstr(y, x, str + ofs3, str_offset_from_columns(str + ofs3, max));
+ } else {
+ mvaddnstr(y, x, str, max);
+ }
+}
diff --git a/src/ui_logwindow.c b/src/ui_logwindow.c
new file mode 100644
index 0000000..13f00b3
--- /dev/null
+++ b/src/ui_logwindow.c
@@ -0,0 +1,426 @@
+/* ncdc - NCurses Direct Connect client
+
+ Copyright (c) 2011-2014 Yoran Heling
+
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be included
+ in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+*/
+
+#include "ncdc.h"
+#include "ui_logwindow.h"
+
+/* Log format of some special messages:
+ * Chat message: "<nick> msg"
+ * Chat /me: "** nick msg"
+ * User joined "--> nick has joined." (may be internationalized,
+ * User quit "--< nick has quit." don't depend on the actual message)
+ * Anything else is a system / help message.
+ */
+
+#if INTERFACE
+
+#define LOGWIN_BUF 1023 // must be 2^x-1
+
+struct ui_logwindow_t {
+ int lastlog;
+ int lastvis;
+ logfile_t *logfile;
+ char *buf[LOGWIN_BUF+1];
+ gboolean updated;
+ int (*checkchat)(void *, char *, char *);
+ void *handle;
+};
+
+#endif
+
+
+void ui_logwindow_addline(ui_logwindow_t *lw, const char *msg, gboolean raw, gboolean nolog) {
+ if(lw->lastlog == lw->lastvis)
+ lw->lastvis = lw->lastlog + 1;
+ lw->lastlog++;
+ lw->updated = TRUE;
+
+ /* Replace \t with four spaces, because the log drawing code can't handle tabs. */
+ GString *msgbuf = NULL;
+ const char *msgl = msg;
+ if(strchr(msg, '\t')) {
+ msgbuf = g_string_sized_new(strlen(msg)+20);
+ for(; *msgl; msgl++) {
+ if(*msgl == '\t')
+ g_string_append(msgbuf, " ");
+ else
+ g_string_append_c(msgbuf, *msgl);
+ }
+ msgl = msgbuf->str;
+ }
+
+ char *ts = localtime_fmt("%H:%M:%S ");
+ lw->buf[lw->lastlog & LOGWIN_BUF] = raw ? g_strdup(msgl) : g_strconcat(ts, msgl, NULL);
+ g_free(ts);
+
+ if(msgbuf)
+ g_string_free(msgbuf, TRUE);
+
+ if(!nolog && lw->logfile)
+ logfile_add(lw->logfile, msg);
+
+ int next = (lw->lastlog + 1) & LOGWIN_BUF;
+ if(lw->buf[next]) {
+ g_free(lw->buf[next]);
+ lw->buf[next] = NULL;
+ }
+}
+
+
+static void ui_logwindow_load(ui_logwindow_t *lw, const char *fn, int num) {
+ char **l = file_tail(fn, num);
+ if(!l) {
+ g_warning("Unable to tail log file '%s': %s", fn, g_strerror(errno));
+ return;
+ }
+ int i, len = g_strv_length(l);
+ char *m;
+ for(i=0; i<len; i++) {
+ if(!g_utf8_validate(l[i], -1, NULL))
+ continue;
+ // parse line: [yyyy-mm-dd hh:mm:ss TIMEZONE] <string>
+ char *msg = strchr(l[i], ']');
+ char *time = strchr(l[i], ' ');
+ char *tmp = time ? strchr(time+1, ' ') : NULL;
+ if(l[i][0] != '[' || !msg || !time || !tmp || tmp < time || msg[1] != ' ')
+ continue;
+ time++;
+ *msg = 0;
+ msg += 2;
+ // if this is the first line, display a notice
+ if(!i) {
+ m = g_strdup_printf("-- Backlog starts on %s.", l[i]+1);
+ ui_logwindow_addline(lw, m, FALSE, TRUE);
+ g_free(m);
+ }
+ // display the line
+ *tmp = 0;
+ m = g_strdup_printf("%s %s", time, msg);
+ ui_logwindow_addline(lw, m, TRUE, TRUE);
+ g_free(m);
+ *tmp = ' ';
+ // if this is the last line, display another notice
+ if(i == len-1) {
+ m = g_strdup_printf("-- Backlog ends on %s", l[i]+1);
+ ui_logwindow_addline(lw, m, FALSE, TRUE);
+ g_free(m);
+ ui_logwindow_addline(lw, "", FALSE, TRUE);
+ }
+ }
+ g_strfreev(l);
+}
+
+
+ui_logwindow_t *ui_logwindow_create(const char *file, int load) {
+ ui_logwindow_t *lw = g_new0(ui_logwindow_t, 1);
+ if(file) {
+ lw->logfile = logfile_create(file);
+
+ if(load)
+ ui_logwindow_load(lw, lw->logfile->path, load);
+ }
+ return lw;
+}
+
+
+void ui_logwindow_free(ui_logwindow_t *lw) {
+ logfile_free(lw->logfile);
+ ui_logwindow_clear(lw);
+ g_free(lw);
+}
+
+
+void ui_logwindow_add(ui_logwindow_t *lw, const char *msg) {
+ if(!msg[0]) {
+ ui_logwindow_addline(lw, "", FALSE, FALSE);
+ return;
+ }
+
+ // For chat messages and /me's, prefix every line with "<nick>" or "** nick"
+ char *prefix = NULL;
+ char *tmp;
+ if( (*msg == '<' && (tmp = strchr(msg, '>')) != NULL && *(++tmp) == ' ') || // <nick>
+ (*msg == '*' && msg[1] == '*' && msg[2] == ' ' && (tmp = strchr(msg+3, ' ')) != NULL)) // ** nick
+ prefix = g_strndup(msg, tmp-msg+1);
+
+ // Split \r?\n? stuff into separate log lines
+ gboolean first = TRUE;
+ while(1) {
+ int len = strcspn(msg, "\r\n");
+
+ tmp = !prefix || first ? g_strndup(msg, len) : g_strdup_printf("%s%.*s", prefix, len, msg);
+ ui_logwindow_addline(lw, tmp, FALSE, FALSE);
+ g_free(tmp);
+
+ msg += len;
+ if(!*msg)
+ break;
+ msg += *msg == '\r' && msg[1] == '\n' ? 2 : 1;
+ first = FALSE;
+ }
+
+ g_free(prefix);
+}
+
+
+void ui_logwindow_clear(ui_logwindow_t *lw) {
+ int i;
+ for(i=0; i<=LOGWIN_BUF; i++) {
+ g_free(lw->buf[i]);
+ lw->buf[i] = NULL;
+ }
+ lw->lastlog = lw->lastvis = 0;
+}
+
+
+void ui_logwindow_scroll(ui_logwindow_t *lw, int i) {
+ lw->lastvis += i;
+ // lastvis may never be larger than the last entry present
+ lw->lastvis = MIN(lw->lastvis, lw->lastlog);
+ // lastvis may never be smaller than the last entry still in the log
+ lw->lastvis = MAX(lw->lastlog - LOGWIN_BUF+1, lw->lastvis);
+ // lastvis may never be smaller than one
+ lw->lastvis = MAX(1, lw->lastvis);
+}
+
+
+// Calculate the wrapping points in a line. Storing the mask in *rows, the row
+// where the indent is reset in *ind_row, and returning the number of rows.
+static int ui_logwindow_calc_wrap(char *str, int cols, int indent, int *rows, int *ind_row) {
+ rows[0] = rows[1] = 0;
+ *ind_row = 0;
+ int cur = 1, curcols = 0, i = 0;
+
+ // Appends an entity that will not be wrapped (i.e. a single character or a
+ // word that isn't too long). Does a 'break' if there are too many lines.
+#define append(w, b, ind) \
+ int t_w = w;\
+ if(curcols+t_w > cols) {\
+ if(++cur >= 200)\
+ break;\
+ if(ind && !*ind_row) {\
+ *ind_row = cur-1;\
+ indent = 0;\
+ }\
+ curcols = indent;\
+ }\
+ if(!(cur > 1 && j == i && curcols == indent))\
+ curcols += t_w;\
+ i += b;\
+ rows[cur] = i;
+
+ while(str[i] && cur < 200) {
+ // Determine the width of the current word
+ int j = i;
+ int width = 0;
+ for(; str[j] && str[j] != ' '; j = g_utf8_next_char(str+j)-str)
+ width += gunichar_width(g_utf8_get_char(str+j));
+
+ // Special-case the space
+ if(j == i) {
+ append(1,1, FALSE);
+
+ // If the word still fits on the current line or is smaller than cols*3/4
+ // and cols-indent, then consider it as a single entity
+ } else if(curcols+width <= cols || width < MIN(cols*3/4, cols-indent)) {
+ append(width, j-i, FALSE);
+
+ // Otherwise, wrap on character-boundary and ignore indent
+ } else {
+ char *tmp = str+i;
+ for(; *tmp && *tmp != ' '; tmp = g_utf8_next_char(tmp)) {
+ append(gunichar_width(g_utf8_get_char(tmp)), g_utf8_next_char(tmp)-tmp, TRUE);
+ }
+ }
+ }
+
+#undef append
+ if(!*ind_row)
+ *ind_row = cur;
+ return cur-1;
+}
+
+
+// Determines the colors each part of a log line should have. Returns the
+// highest index to the attr array.
+static int ui_logwindow_calc_color(ui_logwindow_t *lw, char *str, int *sep, int *attr) {
+ sep[0] = 0;
+ int mask = 0;
+
+ // add a mask
+#define addm(from, to, a)\
+ int t_f = from;\
+ if(sep[mask] != t_f) {\
+ sep[mask+1] = t_f;\
+ attr[mask] = UIC(log_default);\
+ mask++;\
+ }\
+ sep[mask] = t_f;\
+ sep[mask+1] = to;\
+ attr[mask] = a;\
+ mask++;
+
+ // time
+ char *msg = strchr(str, ' ');
+ if(msg && msg-str != 8) // Make sure it's not "Day changed to ..", which doesn't have the time prefix
+ msg = NULL;
+ if(msg) {
+ addm(0, msg-str, UIC(log_time));
+ msg++;
+ }
+
+ // chat messages (<nick> and ** nick)
+ char *tmp;
+ if(msg && (
+ (msg[0] == '<' && (tmp = strchr(msg, '>')) != NULL && tmp[1] == ' ') || // <nick>
+ (msg[0] == '*' && msg[1] == '*' && msg[2] == ' ' && (tmp = strchr(msg+3, ' ')) != NULL))) { // ** nick
+ int nickstart = (msg-str) + (msg[0] == '<' ? 1 : 3);
+ int nickend = tmp-str;
+ // check for a highlight or whether it is our own nick
+ char old = tmp[0];
+ tmp[0] = 0;
+ int r = lw->checkchat ? lw->checkchat(lw->handle, str+nickstart, str+nickend+1) : 0;
+ tmp[0] = old;
+ // and use the correct color
+ addm(nickstart, nickend, r == 2 ? UIC(log_ownnick) : r == 1 ? UIC(log_highlight) : UIC(log_nick));
+ }
+
+ // join/quits (--> and --<)
+ if(msg && msg[0] == '-' && msg[1] == '-' && (msg[2] == '>' || msg[2] == '<')) {
+ addm(msg-str, strlen(str), msg[2] == '>' ? UIC(log_join) : UIC(log_quit));
+ }
+
+#undef addm
+ // make sure the last mask is correct and return
+ if(sep[mask+1] != strlen(str)) {
+ sep[mask+1] = strlen(str);
+ attr[mask] = UIC(log_default);
+ }
+ return mask;
+}
+
+
+// Draws a line between x and x+cols on row y (continuing on y-1 .. y-(rows+1) for
+// multiple rows). Returns the actual number of rows written to.
+static int ui_logwindow_drawline(ui_logwindow_t *lw, int y, int x, int nrows, int cols, char *str) {
+ g_return_val_if_fail(nrows > 0, 1);
+
+ // Determine the indentation for multi-line rows. This is:
+ // - Always after the time part (hh:mm:ss )
+ // - For chat messages: after the nick (<nick> )
+ // - For /me's: after the (** )
+ int indent = 0;
+ char *tmp = strchr(str, ' ');
+ if(tmp)
+ indent = tmp-str+1;
+ if(tmp && tmp[1] == '<' && (tmp = strchr(tmp, '>')) != NULL)
+ indent = tmp-str+2;
+ else if(tmp && tmp[1] == '*' && tmp[2] == '*')
+ indent += 3;
+
+ // Convert indent from bytes to columns
+ if(indent && indent <= strlen(str)) {
+ int i = indent;
+ char old = str[i];
+ str[i] = 0;
+ indent = str_columns(str);
+ str[i] = old;
+ }
+
+ // Determine the wrapping boundaries.
+ // Defines a mask over the string: <#0,#1), <#1,#2), ..
+ static int rows[201];
+ int ind_row;
+ int rmask = ui_logwindow_calc_wrap(str, cols, indent, rows, &ind_row);
+
+ // Determine the colors to give each part
+ static int colors_sep[10]; // Mask, similar to the rows array
+ static int colors[10]; // Color attribute for each mask
+ int cmask = ui_logwindow_calc_color(lw, str, colors_sep, colors);
+
+ // print the rows
+ int r = 0, c = 0, lr = 0;
+ if(rmask-r < nrows)
+ move(y - rmask + r, r == 0 || r >= ind_row ? x : x+indent);
+ while(r <= rmask && c <= cmask) {
+ int rend = rows[r+1];
+ int cend = colors_sep[c+1];
+ int rstart = rows[r];
+ int cstart = colors_sep[c];
+ int start = MAX(rstart, cstart);
+ int end = MIN(cend, rend);
+
+ // Ignore spaces at the start of a new line
+ while(r > 0 && lr != r && start < end && str[start] == ' ')
+ start++;
+ if(start < end)
+ lr = r;
+
+ if(start != end && rmask-r < nrows) {
+ attron(colors[c]);
+ addnstr(str+start, end-start);
+ attroff(colors[c]);
+ }
+
+ if(rend <= cend) {
+ r++;
+ if(rmask-r < nrows)
+ move(y - rmask + r, r == 0 || r >= ind_row ? x : x+indent);
+ }
+ if(rend >= cend)
+ c++;
+ }
+
+ return rmask+1;
+}
+
+
+void ui_logwindow_draw(ui_logwindow_t *lw, int y, int x, int rows, int cols) {
+ int top = rows + y - 1;
+ int cur = lw->lastvis;
+ lw->updated = FALSE;
+
+ while(top >= y) {
+ char *str = lw->buf[cur & LOGWIN_BUF];
+ if(!str)
+ break;
+ top -= ui_logwindow_drawline(lw, top, x, top-y+1, cols, str);
+ cur = (cur-1) & LOGWIN_BUF;
+ }
+}
+
+
+gboolean ui_logwindow_key(ui_logwindow_t *lw, guint64 key, int rows) {
+ switch(key) {
+ case INPT_KEY(KEY_NPAGE):
+ ui_logwindow_scroll(lw, rows/2);
+ return TRUE;
+ case INPT_KEY(KEY_PPAGE):
+ ui_logwindow_scroll(lw, -rows/2);
+ return TRUE;
+ }
+ return FALSE;
+}
+
diff --git a/src/ui_textinput.c b/src/ui_textinput.c
new file mode 100644
index 0000000..b8608ea
--- /dev/null
+++ b/src/ui_textinput.c
@@ -0,0 +1,445 @@
+/* ncdc - NCurses Direct Connect client
+
+ Copyright (c) 2011-2014 Yoran Heling
+
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be included
+ in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+*/
+
+#include "ncdc.h"
+#include "ui_textinput.h"
+
+
+// We only have one command history, so the struct and its instance is local to
+// this file, and the functions work with this instead of accepting an instance
+// as argument. The ui_textinput functions also access the struct and static
+// functions, but these don't need to be public - since ui_textinput is defined
+// below.
+
+#define CMDHIST_BUF 511 // must be 2^x-1
+#define CMDHIST_MAXCMD 2000
+
+
+typedef struct ui_cmdhist_t {
+ char *buf[CMDHIST_BUF+1]; // circular buffer
+ char *fn;
+ int last;
+ gboolean ismod;
+} ui_cmdhist_t;
+
+// we only have one command history, so this can be a global
+static ui_cmdhist_t *cmdhist;
+
+
+static void ui_cmdhist_add(const char *str) {
+ int cur = cmdhist->last & CMDHIST_BUF;
+ // ignore empty lines, or lines that are the same as the previous one
+ if(!str || !str[0] || (cmdhist->buf[cur] && 0 == strcmp(str, cmdhist->buf[cur])))
+ return;
+
+ cmdhist->last++;
+ cur = cmdhist->last & CMDHIST_BUF;
+ if(cmdhist->buf[cur]) {
+ g_free(cmdhist->buf[cur]);
+ cmdhist->buf[cur] = NULL;
+ }
+
+ // truncate the string if it is longer than CMDHIST_MAXCMD bytes, making sure
+ // to not truncate within a UTF-8 sequence
+ int len = 0;
+ while(len < CMDHIST_MAXCMD-10 && str[len])
+ len = g_utf8_next_char(str+len) - str;
+ cmdhist->buf[cur] = g_strndup(str, len);
+ cmdhist->ismod = TRUE;
+}
+
+
+void ui_cmdhist_init(const char *file) {
+ static char buf[CMDHIST_MAXCMD+2]; // + \n and \0
+ cmdhist = g_new0(ui_cmdhist_t, 1);
+
+ cmdhist->fn = g_build_filename(db_dir, file, NULL);
+ FILE *f = fopen(cmdhist->fn, "r");
+ if(f) {
+ while(fgets(buf, CMDHIST_MAXCMD+2, f)) {
+ int len = strlen(buf);
+ if(len > 0 && buf[len-1] == '\n')
+ buf[len-1] = 0;
+
+ if(g_utf8_validate(buf, -1, NULL))
+ ui_cmdhist_add(buf);
+ }
+ fclose(f);
+ }
+}
+
+
+// searches the history either backward or forward for the string q. The line 'start' is also counted.
+static int ui_cmdhist_search(gboolean backward, const char *q, int start) {
+ int i;
+ for(i=start; cmdhist->buf[i&CMDHIST_BUF] && (backward ? (i>=MAX(1, cmdhist->last-CMDHIST_BUF)) : (i<=cmdhist->last)); backward ? i-- : i++) {
+ if(g_str_has_prefix(cmdhist->buf[i & CMDHIST_BUF], q))
+ return i;
+ }
+ return -1;
+}
+
+
+static void ui_cmdhist_save() {
+ if(!cmdhist->ismod)
+ return;
+ cmdhist->ismod = FALSE;
+
+ FILE *f = fopen(cmdhist->fn, "w");
+ if(!f) {
+ g_warning("Unable to open history file '%s' for writing: %s", cmdhist->fn, g_strerror(errno));
+ return;
+ }
+
+ int i;
+ for(i=0; i<=CMDHIST_BUF; i++) {
+ char *l = cmdhist->buf[(cmdhist->last+1+i)&CMDHIST_BUF];
+ if(l) {
+ if(fputs(l, f) < 0 || fputc('\n', f) < 0)
+ g_warning("Error writing to history file '%s': %s", cmdhist->fn, strerror(errno));
+ }
+ }
+ if(fclose(f) < 0)
+ g_warning("Error writing to history file '%s': %s", cmdhist->fn, strerror(errno));
+}
+
+
+void ui_cmdhist_close() {
+ int i;
+ ui_cmdhist_save();
+ for(i=0; i<=CMDHIST_BUF; i++)
+ if(cmdhist->buf[i])
+ g_free(cmdhist->buf[i]);
+ g_free(cmdhist->fn);
+ g_free(cmdhist);
+}
+
+
+
+
+
+#if INTERFACE
+
+struct ui_textinput_t {
+ int pos; // position of the cursor, in number of characters
+ GString *str;
+ gboolean usehist;
+ int s_pos;
+ char *s_q;
+ gboolean s_top;
+ void (*complete)(char *, char **);
+ char *c_q, *c_last, **c_sug;
+ int c_cur;
+};
+
+#endif
+
+
+ui_textinput_t *ui_textinput_create(gboolean usehist, void (*complete)(char *, char **)) {
+ ui_textinput_t *ti = g_new0(ui_textinput_t, 1);
+ ti->str = g_string_new("");
+ ti->usehist = usehist;
+ ti->s_pos = -1;
+ ti->complete = complete;
+ return ti;
+}
+
+
+static void ui_textinput_complete_reset(ui_textinput_t *ti) {
+ if(ti->complete) {
+ g_free(ti->c_q);
+ g_free(ti->c_last);
+ g_strfreev(ti->c_sug);
+ ti->c_q = ti->c_last = NULL;
+ ti->c_sug = NULL;
+ }
+}
+
+
+static void ui_textinput_complete(ui_textinput_t *ti) {
+ if(!ti->complete)
+ return;
+ if(!ti->c_q) {
+ ti->c_q = ui_textinput_get(ti);
+ char *sep = g_utf8_offset_to_pointer(ti->c_q, ti->pos);
+ ti->c_last = g_strdup(sep);
+ *(sep) = 0;
+ ti->c_cur = -1;
+ ti->c_sug = g_new0(char *, 25);
+ ti->complete(ti->c_q, ti->c_sug);
+ }
+ if(!ti->c_sug[++ti->c_cur])
+ ti->c_cur = -1;
+ char *first = ti->c_cur < 0 ? ti->c_q : ti->c_sug[ti->c_cur];
+ char *str = g_strconcat(first, ti->c_last, NULL);
+ ui_textinput_set(ti, str);
+ ti->pos = g_utf8_strlen(first, -1);
+ g_free(str);
+ if(!g_strv_length(ti->c_sug))
+ ui_beep = TRUE;
+ // If there is only one suggestion: finalize this auto-completion and reset
+ // state. This may be slightly counter-intuitive, but makes auto-completing
+ // paths a lot less annoying.
+ if(g_strv_length(ti->c_sug) <= 1)
+ ui_textinput_complete_reset(ti);
+}
+
+
+void ui_textinput_free(ui_textinput_t *ti) {
+ ui_textinput_complete_reset(ti);
+ g_string_free(ti->str, TRUE);
+ if(ti->s_q)
+ g_free(ti->s_q);
+ g_free(ti);
+}
+
+
+void ui_textinput_set(ui_textinput_t *ti, const char *str) {
+ g_string_assign(ti->str, str);
+ ti->pos = g_utf8_strlen(ti->str->str, -1);
+}
+
+
+char *ui_textinput_get(ui_textinput_t *ti) {
+ return g_strdup(ti->str->str);
+}
+
+
+
+char *ui_textinput_reset(ui_textinput_t *ti) {
+ char *str = ui_textinput_get(ti);
+ ui_textinput_set(ti, "");
+ if(ti->usehist) {
+ // as a special case, don't allow /password to be logged. /hset password is
+ // okay, since it will be stored anyway.
+ if(!strstr(str, "/password "))
+ ui_cmdhist_add(str);
+ if(ti->s_q)
+ g_free(ti->s_q);
+ ti->s_q = NULL;
+ ti->s_pos = -1;
+ }
+ return str;
+}
+
+
+// must be drawn last, to keep the cursor position correct
+// also not the most efficient function ever, but probably fast enough.
+void ui_textinput_draw(ui_textinput_t *ti, int y, int x, int col, ui_cursor_t *cur) {
+ // | |
+ // "Some random string etc etc"
+ // f # l
+ // f = function(#, strwidth(upto_#), wincols)
+ // if(strwidth(upto_#) < wincols*0.85)
+ // f = 0
+ // else
+ // f = strwidth(upto_#) - wincols*0.85
+ int i;
+
+ // calculate f (in number of columns)
+ int width = 0;
+ char *str = ti->str->str;
+ for(i=0; i<=ti->pos && *str; i++) {
+ width += gunichar_width(g_utf8_get_char(str));
+ str = g_utf8_next_char(str);
+ }
+ int f = width - (col*85)/100;
+ if(f < 0)
+ f = 0;
+
+ // now print it on the screen, starting from column f in the string and
+ // stopping when we're out of screen columns
+ mvhline(y, x, ' ', col);
+ move(y, x);
+ int pos = 0;
+ str = ti->str->str;
+ i = 0;
+ while(*str) {
+ char *ostr = str;
+ str = g_utf8_next_char(str);
+ int l = gunichar_width(g_utf8_get_char(ostr));
+ f -= l;
+ if(f <= -col)
+ break;
+ if(f < 0) {
+ addnstr(ostr, str-ostr);
+ if(i < ti->pos)
+ pos += l;
+ }
+ i++;
+ }
+ x += pos;
+ move(y, x);
+ curs_set(1);
+ if(cur) {
+ cur->x = x;
+ cur->y = y;
+ }
+}
+
+
+static void ui_textinput_search(ui_textinput_t *ti, gboolean backwards) {
+ int start;
+ if(ti->s_pos < 0) {
+ if(!backwards) {
+ ui_beep = TRUE;
+ return;
+ }
+ ti->s_q = ui_textinput_get(ti);
+ start = cmdhist->last;
+ } else
+ start = ti->s_pos+(backwards ? -1 : 1);
+ int pos = ui_cmdhist_search(backwards, ti->s_q, start);
+ if(pos >= 0) {
+ ti->s_pos = pos;
+ ti->s_top = FALSE;
+ ui_textinput_set(ti, cmdhist->buf[pos & CMDHIST_BUF]);
+ } else if(backwards)
+ ui_beep = TRUE;
+ else {
+ ti->s_pos = -1;
+ ui_textinput_set(ti, ti->s_q);
+ g_free(ti->s_q);
+ ti->s_q = NULL;
+ }
+}
+
+
+#define iswordchar(x) (!(\
+ ((x) >= ' ' && (x) <= '/') ||\
+ ((x) >= ':' && (x) <= '@') ||\
+ ((x) >= '[' && (x) <= '`') ||\
+ ((x) >= '{' && (x) <= '~')\
+ ))
+
+
+gboolean ui_textinput_key(ui_textinput_t *ti, guint64 key, char **str) {
+ int chars = g_utf8_strlen(ti->str->str, -1);
+ gboolean completereset = TRUE;
+ switch(key) {
+ case INPT_KEY(KEY_LEFT): // left - cursor one character left
+ if(ti->pos > 0) ti->pos--;
+ break;
+ case INPT_KEY(KEY_RIGHT):// right - cursor one character right
+ if(ti->pos < chars) ti->pos++;
+ break;
+ case INPT_KEY(KEY_END): // end
+ case INPT_CTRL('e'): // C-e - cursor to end
+ ti->pos = chars;
+ break;
+ case INPT_KEY(KEY_HOME): // home
+ case INPT_CTRL('a'): // C-a - cursor to begin
+ ti->pos = 0;
+ break;
+ case INPT_ALT('b'): // Alt+b - cursor one word backward
+ if(ti->pos > 0) {
+ char *pos = g_utf8_offset_to_pointer(ti->str->str, ti->pos-1);
+ while(pos > ti->str->str && !iswordchar(*pos))
+ pos--;
+ while(pos > ti->str->str && iswordchar(*(pos-1)))
+ pos--;
+ ti->pos = g_utf8_strlen(ti->str->str, pos-ti->str->str);
+ }
+ break;
+ case INPT_ALT('f'): // Alt+f - cursor one word forward
+ if(ti->pos < chars) {
+ char *pos = g_utf8_offset_to_pointer(ti->str->str, ti->pos);
+ while(!iswordchar(*pos))
+ pos++;
+ while(*pos && iswordchar(*pos))
+ pos++;
+ ti->pos = g_utf8_strlen(ti->str->str, pos-ti->str->str);
+ }
+ break;
+ case INPT_KEY(KEY_BACKSPACE): // backspace - delete character before cursor
+ if(ti->pos > 0) {
+ char *pos = g_utf8_offset_to_pointer(ti->str->str, ti->pos-1);
+ g_string_erase(ti->str, pos-ti->str->str, g_utf8_next_char(pos)-pos);
+ ti->pos--;
+ }
+ break;
+ case INPT_KEY(KEY_DC): // del - delete character under cursor
+ if(ti->pos < chars) {
+ char *pos = g_utf8_offset_to_pointer(ti->str->str, ti->pos);
+ g_string_erase(ti->str, pos-ti->str->str, g_utf8_next_char(pos)-pos);
+ }
+ break;
+ case INPT_CTRL('w'): // C-w - delete to previous space
+ case INPT_ALT(127): // Alt+backspace
+ if(ti->pos > 0) {
+ char *end = g_utf8_offset_to_pointer(ti->str->str, ti->pos-1);
+ char *begin = end;
+ while(begin > ti->str->str && !iswordchar(*begin))
+ begin--;
+ while(begin > ti->str->str && iswordchar(*(begin-1)))
+ begin--;
+ ti->pos -= g_utf8_strlen(begin, g_utf8_next_char(end)-begin);
+ g_string_erase(ti->str, begin-ti->str->str, g_utf8_next_char(end)-begin);
+ }
+ break;
+ case INPT_ALT('d'): // Alt+d - delete to next space
+ if(ti->pos < chars) {
+ char *begin = g_utf8_offset_to_pointer(ti->str->str, ti->pos);
+ char *end = begin;
+ while(*end == ' ')
+ end++;
+ while(*end && *(end+1) && *(end+1) != ' ')
+ end++;
+ g_string_erase(ti->str, begin-ti->str->str, g_utf8_next_char(end)-begin);
+ }
+ break;
+ case INPT_CTRL('k'): // C-k - delete everything after cursor
+ if(ti->pos < chars)
+ g_string_erase(ti->str, g_utf8_offset_to_pointer(ti->str->str, ti->pos)-ti->str->str, -1);
+ break;
+ case INPT_CTRL('u'): // C-u - delete entire line
+ g_string_erase(ti->str, 0, -1);
+ ti->pos = 0;
+ break;
+ case INPT_KEY(KEY_UP): // up - history search back
+ case INPT_KEY(KEY_DOWN): // down - history search forward
+ if(ti->usehist)
+ ui_textinput_search(ti, key == INPT_KEY(KEY_UP));
+ else
+ return FALSE;
+ break;
+ case INPT_CTRL('i'): // tab - autocomplete
+ ui_textinput_complete(ti);
+ completereset = FALSE;
+ break;
+ case INPT_CTRL('j'): // newline - accept & clear
+ *str = ui_textinput_reset(ti);
+ break;
+ default:
+ if(INPT_TYPE(key) == 1) { // char
+ g_string_insert_unichar(ti->str, g_utf8_offset_to_pointer(ti->str->str, ti->pos)-ti->str->str, INPT_CODE(key));
+ ti->pos++;
+ } else
+ return FALSE;
+ }
+ if(completereset)
+ ui_textinput_complete_reset(ti);
+ return TRUE;
+}
diff --git a/src/ui_util.c b/src/ui_util.c
deleted file mode 100644
index df9624f..0000000
--- a/src/ui_util.c
+++ /dev/null
@@ -1,1410 +0,0 @@
-/* ncdc - NCurses Direct Connect client
-
- Copyright (c) 2011-2014 Yoran Heling
-
- Permission is hereby granted, free of charge, to any person obtaining
- a copy of this software and associated documentation files (the
- "Software"), to deal in the Software without restriction, including
- without limitation the rights to use, copy, modify, merge, publish,
- distribute, sublicense, and/or sell copies of the Software, and to
- permit persons to whom the Software is furnished to do so, subject to
- the following conditions:
-
- The above copyright notice and this permission notice shall be included
- in all copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-*/
-
-#include "ncdc.h"
-#include "ui_util.h"
-
-
-
-
-
-// colors
-
-#if INTERFACE
-
-#define COLOR_DEFAULT (-1)
-
-// name default value
-#define UI_COLORS \
- C(list_default, "default")\
- C(list_header, "default,bold")\
- C(list_select, "default,bold")\
- C(log_default, "default")\
- C(log_highlight, "yellow,bold")\
- C(log_join, "cyan,bold")\
- C(log_nick, "default")\
- C(log_ownnick, "default,bold")\
- C(log_quit, "cyan")\
- C(log_time, "black,bold")\
- C(separator, "default,reverse")\
- C(tab_active, "default,bold")\
- C(tabprio_high, "magenta,bold")\
- C(tabprio_low, "black,bold")\
- C(tabprio_med, "cyan,bold")\
- C(title, "default,reverse")
-
-enum ui_coltype {
-#define C(n, d) UIC_##n,
- UI_COLORS
-#undef C
- UIC_NONE
-};
-
-
-struct ui_color_t {
- int var;
- short fg, bg, d_fg, d_bg;
- int x, d_x, a;
-};
-
-struct ui_attr_t {
- char name[11];
- gboolean color : 1;
- int attr;
-}
-
-struct ui_cursor_t {
- int x;
- int y;
-}
-
-#define UIC(n) (ui_colors[(ui_coltype)UIC_##n].a)
-
-#endif // INTERFACE
-
-
-ui_color_t ui_colors[] = {
-#define C(n, d) { VAR_color_##n },
- UI_COLORS
-#undef C
- { -1 }
-};
-
-
-ui_attr_t ui_attr_names[] = {
- { "black", TRUE, COLOR_BLACK },
- { "blink", FALSE, A_BLINK },
- { "blue", TRUE, COLOR_BLUE },
- { "bold", FALSE, A_BOLD },
- { "cyan", TRUE, COLOR_CYAN },
- { "default", TRUE, COLOR_DEFAULT },
- { "green", TRUE, COLOR_GREEN },
- { "magenta", TRUE, COLOR_MAGENTA },
- { "red", TRUE, COLOR_RED },
- { "reverse", FALSE, A_REVERSE },
- { "underline", FALSE, A_UNDERLINE },
- { "white", TRUE, COLOR_WHITE },
- { "yellow", TRUE, COLOR_YELLOW },
- { "" }
-};
-
-
-static ui_attr_t *ui_attr_by_name(const char *n) {
- ui_attr_t *a = ui_attr_names;
- for(; *a->name; a++)
- if(strcmp(a->name, n) == 0)
- return a;
- return NULL;
-}
-
-
-static char *ui_name_by_attr(int n) {
- ui_attr_t *a = ui_attr_names;
- for(; *a->name; a++)
- if(a->attr == n)
- return a->name;
- return NULL;
-}
-
-
-gboolean ui_color_str_parse(const char *str, short *fg, short *bg, int *x, GError **err) {
- int state = 0; // 0 = no fg, 1 = no bg, 2 = both
- short f = COLOR_DEFAULT, b = COLOR_DEFAULT;
- int a = 0;
- char **args = g_strsplit(str, ",", 0);
- char **arg = args;
- for(; arg && *arg; arg++) {
- g_strstrip(*arg);
- if(!**arg)
- continue;
- ui_attr_t *attr = ui_attr_by_name(*arg);
- if(!attr) {
- g_set_error(err, 1, 0, "Unknown color or attribute: %s", *arg);
- g_strfreev(args);
- return FALSE;
- }
- if(!attr->color)
- a |= attr->attr;
- else if(!state) {
- f = attr->attr;
- state++;
- } else if(state == 1) {
- b = attr->attr;
- state++;
- } else {
- g_set_error(err, 1, 0, "Don't know what to do with a third color: %s", *arg);
- g_strfreev(args);
- return FALSE;
- }
- }
- g_strfreev(args);
- if(fg) *fg = f;
- if(bg) *bg = b;
- if(x) *x = a;
- return TRUE;
-}
-
-
-char *ui_color_str_gen(int fd, int bg, int x) {
- static char buf[100]; // must be smaller than (max_color_name * 2) + (max_attr_name * 3) + 6
- strcpy(buf, ui_name_by_attr(fd));
- if(bg != COLOR_DEFAULT) {
- strcat(buf, ",");
- strcat(buf, ui_name_by_attr(bg));
- }
- ui_attr_t *attr = ui_attr_names;
- for(; attr->name[0]; attr++)
- if(!attr->color && x & attr->attr) {
- strcat(buf, ",");
- strcat(buf, attr->name);
- }
- return buf;
-}
-
-
-// TODO: re-use color pairs when we have too many (>64) color groups
-void ui_colors_update() {
- int pair = 0;
- ui_color_t *c = ui_colors;
- for(; c->var>=0; c++) {
- g_warn_if_fail(ui_color_str_parse(var_get(0, c->var), &c->fg, &c->bg, &c->x, NULL));
- init_pair(++pair, c->fg, c->bg);
- c->a = c->x | COLOR_PAIR(pair);
- }
-}
-
-
-void ui_colors_init() {
- if(!has_colors())
- return;
-
- start_color();
- use_default_colors();
-
- ui_colors_update();
-}
-
-
-
-
-
-// Log window "widget"
-
-
-/* Log format of some special messages:
- * Chat message: "<nick> msg"
- * Chat /me: "** nick msg"
- * User joined "--> nick has joined." (may be internationalized,
- * User quit "--< nick has quit." don't depend on the actual message)
- * Anything else is a system / help message.
- */
-
-#if INTERFACE
-
-#define LOGWIN_BUF 1023 // must be 2^x-1
-
-struct ui_logwindow_t {
- int lastlog;
- int lastvis;
- logfile_t *logfile;
- char *buf[LOGWIN_BUF+1];
- gboolean updated;
- int (*checkchat)(void *, char *, char *);
- void *handle;
-};
-
-#endif
-
-
-void ui_logwindow_addline(ui_logwindow_t *lw, const char *msg, gboolean raw, gboolean nolog) {
- if(lw->lastlog == lw->lastvis)
- lw->lastvis = lw->lastlog + 1;
- lw->lastlog++;
- lw->updated = TRUE;
-
- /* Replace \t with four spaces, because the log drawing code can't handle tabs. */
- GString *msgbuf = NULL;
- const char *msgl = msg;
- if(strchr(msg, '\t')) {
- msgbuf = g_string_sized_new(strlen(msg)+20);
- for(; *msgl; msgl++) {
- if(*msgl == '\t')
- g_string_append(msgbuf, " ");
- else
- g_string_append_c(msgbuf, *msgl);
- }
- msgl = msgbuf->str;
- }
-
- char *ts = localtime_fmt("%H:%M:%S ");
- lw->buf[lw->lastlog & LOGWIN_BUF] = raw ? g_strdup(msgl) : g_strconcat(ts, msgl, NULL);
- g_free(ts);
-
- if(msgbuf)
- g_string_free(msgbuf, TRUE);
-
- if(!nolog && lw->logfile)
- logfile_add(lw->logfile, msg);
-
- int next = (lw->lastlog + 1) & LOGWIN_BUF;
- if(lw->buf[next]) {
- g_free(lw->buf[next]);
- lw->buf[next] = NULL;
- }
-}
-
-
-static void ui_logwindow_load(ui_logwindow_t *lw, const char *fn, int num) {
- char **l = file_tail(fn, num);
- if(!l) {
- g_warning("Unable to tail log file '%s': %s", fn, g_strerror(errno));
- return;
- }
- int i, len = g_strv_length(l);
- char *m;
- for(i=0; i<len; i++) {
- if(!g_utf8_validate(l[i], -1, NULL))
- continue;
- // parse line: [yyyy-mm-dd hh:mm:ss TIMEZONE] <string>
- char *msg = strchr(l[i], ']');
- char *time = strchr(l[i], ' ');
- char *tmp = time ? strchr(time+1, ' ') : NULL;
- if(l[i][0] != '[' || !msg || !time || !tmp || tmp < time || msg[1] != ' ')
- continue;
- time++;
- *msg = 0;
- msg += 2;
- // if this is the first line, display a notice
- if(!i) {
- m = g_strdup_printf("-- Backlog starts on %s.", l[i]+1);
- ui_logwindow_addline(lw, m, FALSE, TRUE);
- g_free(m);
- }
- // display the line
- *tmp = 0;
- m = g_strdup_printf("%s %s", time, msg);
- ui_logwindow_addline(lw, m, TRUE, TRUE);
- g_free(m);
- *tmp = ' ';
- // if this is the last line, display another notice
- if(i == len-1) {
- m = g_strdup_printf("-- Backlog ends on %s", l[i]+1);
- ui_logwindow_addline(lw, m, FALSE, TRUE);
- g_free(m);
- ui_logwindow_addline(lw, "", FALSE, TRUE);
- }
- }
- g_strfreev(l);
-}
-
-
-ui_logwindow_t *ui_logwindow_create(const char *file, int load) {
- ui_logwindow_t *lw = g_new0(ui_logwindow_t, 1);
- if(file) {
- lw->logfile = logfile_create(file);
-
- if(load)
- ui_logwindow_load(lw, lw->logfile->path, load);
- }
- return lw;
-}
-
-
-void ui_logwindow_free(ui_logwindow_t *lw) {
- logfile_free(lw->logfile);
- ui_logwindow_clear(lw);
- g_free(lw);
-}
-
-
-void ui_logwindow_add(ui_logwindow_t *lw, const char *msg) {
- if(!msg[0]) {
- ui_logwindow_addline(lw, "", FALSE, FALSE);
- return;
- }
-
- // For chat messages and /me's, prefix every line with "<nick>" or "** nick"
- char *prefix = NULL;
- char *tmp;
- if( (*msg == '<' && (tmp = strchr(msg, '>')) != NULL && *(++tmp) == ' ') || // <nick>
- (*msg == '*' && msg[1] == '*' && msg[2] == ' ' && (tmp = strchr(msg+3, ' ')) != NULL)) // ** nick
- prefix = g_strndup(msg, tmp-msg+1);
-
- // Split \r?\n? stuff into separate log lines
- gboolean first = TRUE;
- while(1) {
- int len = strcspn(msg, "\r\n");
-
- tmp = !prefix || first ? g_strndup(msg, len) : g_strdup_printf("%s%.*s", prefix, len, msg);
- ui_logwindow_addline(lw, tmp, FALSE, FALSE);
- g_free(tmp);
-
- msg += len;
- if(!*msg)
- break;
- msg += *msg == '\r' && msg[1] == '\n' ? 2 : 1;
- first = FALSE;
- }
-
- g_free(prefix);
-}
-
-
-void ui_logwindow_clear(ui_logwindow_t *lw) {
- int i;
- for(i=0; i<=LOGWIN_BUF; i++) {
- g_free(lw->buf[i]);
- lw->buf[i] = NULL;
- }
- lw->lastlog = lw->lastvis = 0;
-}
-
-
-void ui_logwindow_scroll(ui_logwindow_t *lw, int i) {
- lw->lastvis += i;
- // lastvis may never be larger than the last entry present
- lw->lastvis = MIN(lw->lastvis, lw->lastlog);
- // lastvis may never be smaller than the last entry still in the log
- lw->lastvis = MAX(lw->lastlog - LOGWIN_BUF+1, lw->lastvis);
- // lastvis may never be smaller than one
- lw->lastvis = MAX(1, lw->lastvis);
-}
-
-
-// Calculate the wrapping points in a line. Storing the mask in *rows, the row
-// where the indent is reset in *ind_row, and returning the number of rows.
-static int ui_logwindow_calc_wrap(char *str, int cols, int indent, int *rows, int *ind_row) {
- rows[0] = rows[1] = 0;
- *ind_row = 0;
- int cur = 1, curcols = 0, i = 0;
-
- // Appends an entity that will not be wrapped (i.e. a single character or a
- // word that isn't too long). Does a 'break' if there are too many lines.
-#define append(w, b, ind) \
- int t_w = w;\
- if(curcols+t_w > cols) {\
- if(++cur >= 200)\
- break;\
- if(ind && !*ind_row) {\
- *ind_row = cur-1;\
- indent = 0;\
- }\
- curcols = indent;\
- }\
- if(!(cur > 1 && j == i && curcols == indent))\
- curcols += t_w;\
- i += b;\
- rows[cur] = i;
-
- while(str[i] && cur < 200) {
- // Determine the width of the current word
- int j = i;
- int width = 0;
- for(; str[j] && str[j] != ' '; j = g_utf8_next_char(str+j)-str)
- width += gunichar_width(g_utf8_get_char(str+j));
-
- // Special-case the space
- if(j == i) {
- append(1,1, FALSE);
-
- // If the word still fits on the current line or is smaller than cols*3/4
- // and cols-indent, then consider it as a single entity
- } else if(curcols+width <= cols || width < MIN(cols*3/4, cols-indent)) {
- append(width, j-i, FALSE);
-
- // Otherwise, wrap on character-boundary and ignore indent
- } else {
- char *tmp = str+i;
- for(; *tmp && *tmp != ' '; tmp = g_utf8_next_char(tmp)) {
- append(gunichar_width(g_utf8_get_char(tmp)), g_utf8_next_char(tmp)-tmp, TRUE);
- }
- }
- }
-
-#undef append
- if(!*ind_row)
- *ind_row = cur;
- return cur-1;
-}
-
-
-// Determines the colors each part of a log line should have. Returns the
-// highest index to the attr array.
-static int ui_logwindow_calc_color(ui_logwindow_t *lw, char *str, int *sep, int *attr) {
- sep[0] = 0;
- int mask = 0;
-
- // add a mask
-#define addm(from, to, a)\
- int t_f = from;\
- if(sep[mask] != t_f) {\
- sep[mask+1] = t_f;\
- attr[mask] = UIC(log_default);\
- mask++;\
- }\
- sep[mask] = t_f;\
- sep[mask+1] = to;\
- attr[mask] = a;\
- mask++;
-
- // time
- char *msg = strchr(str, ' ');
- if(msg && msg-str != 8) // Make sure it's not "Day changed to ..", which doesn't have the time prefix
- msg = NULL;
- if(msg) {
- addm(0, msg-str, UIC(log_time));
- msg++;
- }
-
- // chat messages (<nick> and ** nick)
- char *tmp;
- if(msg && (
- (msg[0] == '<' && (tmp = strchr(msg, '>')) != NULL && tmp[1] == ' ') || // <nick>
- (msg[0] == '*' && msg[1] == '*' && msg[2] == ' ' && (tmp = strchr(msg+3, ' ')) != NULL))) { // ** nick
- int nickstart = (msg-str) + (msg[0] == '<' ? 1 : 3);
- int nickend = tmp-str;
- // check for a highlight or whether it is our own nick
- char old = tmp[0];
- tmp[0] = 0;
- int r = lw->checkchat ? lw->checkchat(lw->handle, str+nickstart, str+nickend+1) : 0;
- tmp[0] = old;
- // and use the correct color
- addm(nickstart, nickend, r == 2 ? UIC(log_ownnick) : r == 1 ? UIC(log_highlight) : UIC(log_nick));
- }
-
- // join/quits (--> and --<)
- if(msg && msg[0] == '-' && msg[1] == '-' && (msg[2] == '>' || msg[2] == '<')) {
- addm(msg-str, strlen(str), msg[2] == '>' ? UIC(log_join) : UIC(log_quit));
- }
-
-#undef addm
- // make sure the last mask is correct and return
- if(sep[mask+1] != strlen(str)) {
- sep[mask+1] = strlen(str);
- attr[mask] = UIC(log_default);
- }
- return mask;
-}
-
-
-// Draws a line between x and x+cols on row y (continuing on y-1 .. y-(rows+1) for
-// multiple rows). Returns the actual number of rows written to.
-static int ui_logwindow_drawline(ui_logwindow_t *lw, int y, int x, int nrows, int cols, char *str) {
- g_return_val_if_fail(nrows > 0, 1);
-
- // Determine the indentation for multi-line rows. This is:
- // - Always after the time part (hh:mm:ss )
- // - For chat messages: after the nick (<nick> )
- // - For /me's: after the (** )
- int indent = 0;
- char *tmp = strchr(str, ' ');
- if(tmp)
- indent = tmp-str+1;
- if(tmp && tmp[1] == '<' && (tmp = strchr(tmp, '>')) != NULL)
- indent = tmp-str+2;
- else if(tmp && tmp[1] == '*' && tmp[2] == '*')
- indent += 3;
-
- // Convert indent from bytes to columns
- if(indent && indent <= strlen(str)) {
- int i = indent;
- char old = str[i];
- str[i] = 0;
- indent = str_columns(str);
- str[i] = old;
- }
-
- // Determine the wrapping boundaries.
- // Defines a mask over the string: <#0,#1), <#1,#2), ..
- static int rows[201];
- int ind_row;
- int rmask = ui_logwindow_calc_wrap(str, cols, indent, rows, &ind_row);
-
- // Determine the colors to give each part
- static int colors_sep[10]; // Mask, similar to the rows array
- static int colors[10]; // Color attribute for each mask
- int cmask = ui_logwindow_calc_color(lw, str, colors_sep, colors);
-
- // print the rows
- int r = 0, c = 0, lr = 0;
- if(rmask-r < nrows)
- move(y - rmask + r, r == 0 || r >= ind_row ? x : x+indent);
- while(r <= rmask && c <= cmask) {
- int rend = rows[r+1];
- int cend = colors_sep[c+1];
- int rstart = rows[r];
- int cstart = colors_sep[c];
- int start = MAX(rstart, cstart);
- int end = MIN(cend, rend);
-
- // Ignore spaces at the start of a new line
- while(r > 0 && lr != r && start < end && str[start] == ' ')
- start++;
- if(start < end)
- lr = r;
-
- if(start != end && rmask-r < nrows) {
- attron(colors[c]);
- addnstr(str+start, end-start);
- attroff(colors[c]);
- }
-
- if(rend <= cend) {
- r++;
- if(rmask-r < nrows)
- move(y - rmask + r, r == 0 || r >= ind_row ? x : x+indent);
- }
- if(rend >= cend)
- c++;
- }
-
- return rmask+1;
-}
-
-
-void ui_logwindow_draw(ui_logwindow_t *lw, int y, int x, int rows, int cols) {
- int top = rows + y - 1;
- int cur = lw->lastvis;
- lw->updated = FALSE;
-
- while(top >= y) {
- char *str = lw->buf[cur & LOGWIN_BUF];
- if(!str)
- break;
- top -= ui_logwindow_drawline(lw, top, x, top-y+1, cols, str);
- cur = (cur-1) & LOGWIN_BUF;
- }
-}
-
-
-gboolean ui_logwindow_key(ui_logwindow_t *lw, guint64 key, int rows) {
- switch(key) {
- case INPT_KEY(KEY_NPAGE):
- ui_logwindow_scroll(lw, rows/2);
- return TRUE;
- case INPT_KEY(KEY_PPAGE):
- ui_logwindow_scroll(lw, -rows/2);
- return TRUE;
- }
- return FALSE;
-}
-
-
-
-
-
-
-// Command history
-// We only have one command history, so the struct and its instance is local to
-// this file, and the functions work with this instead of accepting an instance
-// as argument. The ui_textinput functions also access the struct and static
-// functions, but these don't need to be public - since ui_textinput is defined
-// below.
-
-#define CMDHIST_BUF 511 // must be 2^x-1
-#define CMDHIST_MAXCMD 2000
-
-
-typedef struct ui_cmdhist_t {
- char *buf[CMDHIST_BUF+1]; // circular buffer
- char *fn;
- int last;
- gboolean ismod;
-} ui_cmdhist_t;
-
-// we only have one command history, so this can be a global
-static ui_cmdhist_t *cmdhist;
-
-
-static void ui_cmdhist_add(const char *str) {
- int cur = cmdhist->last & CMDHIST_BUF;
- // ignore empty lines, or lines that are the same as the previous one
- if(!str || !str[0] || (cmdhist->buf[cur] && 0 == strcmp(str, cmdhist->buf[cur])))
- return;
-
- cmdhist->last++;
- cur = cmdhist->last & CMDHIST_BUF;
- if(cmdhist->buf[cur]) {
- g_free(cmdhist->buf[cur]);
- cmdhist->buf[cur] = NULL;
- }
-
- // truncate the string if it is longer than CMDHIST_MAXCMD bytes, making sure
- // to not truncate within a UTF-8 sequence
- int len = 0;
- while(len < CMDHIST_MAXCMD-10 && str[len])
- len = g_utf8_next_char(str+len) - str;
- cmdhist->buf[cur] = g_strndup(str, len);
- cmdhist->ismod = TRUE;
-}
-
-
-void ui_cmdhist_init(const char *file) {
- static char buf[CMDHIST_MAXCMD+2]; // + \n and \0
- cmdhist = g_new0(ui_cmdhist_t, 1);
-
- cmdhist->fn = g_build_filename(db_dir, file, NULL);
- FILE *f = fopen(cmdhist->fn, "r");
- if(f) {
- while(fgets(buf, CMDHIST_MAXCMD+2, f)) {
- int len = strlen(buf);
- if(len > 0 && buf[len-1] == '\n')
- buf[len-1] = 0;
-
- if(g_utf8_validate(buf, -1, NULL))
- ui_cmdhist_add(buf);
- }
- fclose(f);
- }
-}
-
-
-// searches the history either backward or forward for the string q. The line 'start' is also counted.
-// (only used by ui_textinput below, so can be static)
-static int ui_cmdhist_search(gboolean backward, const char *q, int start) {
- int i;
- for(i=start; cmdhist->buf[i&CMDHIST_BUF] && (backward ? (i>=MAX(1, cmdhist->last-CMDHIST_BUF)) : (i<=cmdhist->last)); backward ? i-- : i++) {
- if(g_str_has_prefix(cmdhist->buf[i & CMDHIST_BUF], q))
- return i;
- }
- return -1;
-}
-
-
-static void ui_cmdhist_save() {
- if(!cmdhist->ismod)
- return;
- cmdhist->ismod = FALSE;
-
- FILE *f = fopen(cmdhist->fn, "w");
- if(!f) {
- g_warning("Unable to open history file '%s' for writing: %s", cmdhist->fn, g_strerror(errno));
- return;
- }
-
- int i;
- for(i=0; i<=CMDHIST_BUF; i++) {
- char *l = cmdhist->buf[(cmdhist->last+1+i)&CMDHIST_BUF];
- if(l) {
- if(fputs(l, f) < 0 || fputc('\n', f) < 0)
- g_warning("Error writing to history file '%s': %s", cmdhist->fn, strerror(errno));
- }
- }
- if(fclose(f) < 0)
- g_warning("Error writing to history file '%s': %s", cmdhist->fn, strerror(errno));
-}
-
-
-void ui_cmdhist_close() {
- int i;
- ui_cmdhist_save();
- for(i=0; i<=CMDHIST_BUF; i++)
- if(cmdhist->buf[i])
- g_free(cmdhist->buf[i]);
- g_free(cmdhist->fn);
- g_free(cmdhist);
-}
-
-
-
-
-// Text input "widget"
-
-#if INTERFACE
-
-struct ui_textinput_t {
- int pos; // position of the cursor, in number of characters
- GString *str;
- gboolean usehist;
- int s_pos;
- char *s_q;
- gboolean s_top;
- void (*complete)(char *, char **);
- char *c_q, *c_last, **c_sug;
- int c_cur;
-};
-
-#endif
-
-
-
-ui_textinput_t *ui_textinput_create(gboolean usehist, void (*complete)(char *, char **)) {
- ui_textinput_t *ti = g_new0(ui_textinput_t, 1);
- ti->str = g_string_new("");
- ti->usehist = usehist;
- ti->s_pos = -1;
- ti->complete = complete;
- return ti;
-}
-
-
-static void ui_textinput_complete_reset(ui_textinput_t *ti) {
- if(ti->complete) {
- g_free(ti->c_q);
- g_free(ti->c_last);
- g_strfreev(ti->c_sug);
- ti->c_q = ti->c_last = NULL;
- ti->c_sug = NULL;
- }
-}
-
-
-static void ui_textinput_complete(ui_textinput_t *ti) {
- if(!ti->complete)
- return;
- if(!ti->c_q) {
- ti->c_q = ui_textinput_get(ti);
- char *sep = g_utf8_offset_to_pointer(ti->c_q, ti->pos);
- ti->c_last = g_strdup(sep);
- *(sep) = 0;
- ti->c_cur = -1;
- ti->c_sug = g_new0(char *, 25);
- ti->complete(ti->c_q, ti->c_sug);
- }
- if(!ti->c_sug[++ti->c_cur])
- ti->c_cur = -1;
- char *first = ti->c_cur < 0 ? ti->c_q : ti->c_sug[ti->c_cur];
- char *str = g_strconcat(first, ti->c_last, NULL);
- ui_textinput_set(ti, str);
- ti->pos = g_utf8_strlen(first, -1);
- g_free(str);
- if(!g_strv_length(ti->c_sug))
- ui_beep = TRUE;
- // If there is only one suggestion: finalize this auto-completion and reset
- // state. This may be slightly counter-intuitive, but makes auto-completing
- // paths a lot less annoying.
- if(g_strv_length(ti->c_sug) <= 1)
- ui_textinput_complete_reset(ti);
-}
-
-
-void ui_textinput_free(ui_textinput_t *ti) {
- ui_textinput_complete_reset(ti);
- g_string_free(ti->str, TRUE);
- if(ti->s_q)
- g_free(ti->s_q);
- g_free(ti);
-}
-
-
-void ui_textinput_set(ui_textinput_t *ti, const char *str) {
- g_string_assign(ti->str, str);
- ti->pos = g_utf8_strlen(ti->str->str, -1);
-}
-
-
-char *ui_textinput_get(ui_textinput_t *ti) {
- return g_strdup(ti->str->str);
-}
-
-
-
-char *ui_textinput_reset(ui_textinput_t *ti) {
- char *str = ui_textinput_get(ti);
- ui_textinput_set(ti, "");
- if(ti->usehist) {
- // as a special case, don't allow /password to be logged. /hset password is
- // okay, since it will be stored anyway.
- if(!strstr(str, "/password "))
- ui_cmdhist_add(str);
- if(ti->s_q)
- g_free(ti->s_q);
- ti->s_q = NULL;
- ti->s_pos = -1;
- }
- return str;
-}
-
-
-// must be drawn last, to keep the cursor position correct
-// also not the most efficient function ever, but probably fast enough.
-void ui_textinput_draw(ui_textinput_t *ti, int y, int x, int col, ui_cursor_t *cur) {
- // | |
- // "Some random string etc etc"
- // f # l
- // f = function(#, strwidth(upto_#), wincols)
- // if(strwidth(upto_#) < wincols*0.85)
- // f = 0
- // else
- // f = strwidth(upto_#) - wincols*0.85
- int i;
-
- // calculate f (in number of columns)
- int width = 0;
- char *str = ti->str->str;
- for(i=0; i<=ti->pos && *str; i++) {
- width += gunichar_width(g_utf8_get_char(str));
- str = g_utf8_next_char(str);
- }
- int f = width - (col*85)/100;
- if(f < 0)
- f = 0;
-
- // now print it on the screen, starting from column f in the string and
- // stopping when we're out of screen columns
- mvhline(y, x, ' ', col);
- move(y, x);
- int pos = 0;
- str = ti->str->str;
- i = 0;
- while(*str) {
- char *ostr = str;
- str = g_utf8_next_char(str);
- int l = gunichar_width(g_utf8_get_char(ostr));
- f -= l;
- if(f <= -col)
- break;
- if(f < 0) {
- addnstr(ostr, str-ostr);
- if(i < ti->pos)
- pos += l;
- }
- i++;
- }
- x += pos;
- move(y, x);
- curs_set(1);
- if(cur) {
- cur->x = x;
- cur->y = y;
- }
-}
-
-
-static void ui_textinput_search(ui_textinput_t *ti, gboolean backwards) {
- int start;
- if(ti->s_pos < 0) {
- if(!backwards) {
- ui_beep = TRUE;
- return;
- }
- ti->s_q = ui_textinput_get(ti);
- start = cmdhist->last;
- } else
- start = ti->s_pos+(backwards ? -1 : 1);
- int pos = ui_cmdhist_search(backwards, ti->s_q, start);
- if(pos >= 0) {
- ti->s_pos = pos;
- ti->s_top = FALSE;
- ui_textinput_set(ti, cmdhist->buf[pos & CMDHIST_BUF]);
- } else if(backwards)
- ui_beep = TRUE;
- else {
- ti->s_pos = -1;
- ui_textinput_set(ti, ti->s_q);
- g_free(ti->s_q);
- ti->s_q = NULL;
- }
-}
-
-
-#define iswordchar(x) (!(\
- ((x) >= ' ' && (x) <= '/') ||\
- ((x) >= ':' && (x) <= '@') ||\
- ((x) >= '[' && (x) <= '`') ||\
- ((x) >= '{' && (x) <= '~')\
- ))
-
-
-gboolean ui_textinput_key(ui_textinput_t *ti, guint64 key, char **str) {
- int chars = g_utf8_strlen(ti->str->str, -1);
- gboolean completereset = TRUE;
- switch(key) {
- case INPT_KEY(KEY_LEFT): // left - cursor one character left
- if(ti->pos > 0) ti->pos--;
- break;
- case INPT_KEY(KEY_RIGHT):// right - cursor one character right
- if(ti->pos < chars) ti->pos++;
- break;
- case INPT_KEY(KEY_END): // end
- case INPT_CTRL('e'): // C-e - cursor to end
- ti->pos = chars;
- break;
- case INPT_KEY(KEY_HOME): // home
- case INPT_CTRL('a'): // C-a - cursor to begin
- ti->pos = 0;
- break;
- case INPT_ALT('b'): // Alt+b - cursor one word backward
- if(ti->pos > 0) {
- char *pos = g_utf8_offset_to_pointer(ti->str->str, ti->pos-1);
- while(pos > ti->str->str && !iswordchar(*pos))
- pos--;
- while(pos > ti->str->str && iswordchar(*(pos-1)))
- pos--;
- ti->pos = g_utf8_strlen(ti->str->str, pos-ti->str->str);
- }
- break;
- case INPT_ALT('f'): // Alt+f - cursor one word forward
- if(ti->pos < chars) {
- char *pos = g_utf8_offset_to_pointer(ti->str->str, ti->pos);
- while(!iswordchar(*pos))
- pos++;
- while(*pos && iswordchar(*pos))
- pos++;
- ti->pos = g_utf8_strlen(ti->str->str, pos-ti->str->str);
- }
- break;
- case INPT_KEY(KEY_BACKSPACE): // backspace - delete character before cursor
- if(ti->pos > 0) {
- char *pos = g_utf8_offset_to_pointer(ti->str->str, ti->pos-1);
- g_string_erase(ti->str, pos-ti->str->str, g_utf8_next_char(pos)-pos);
- ti->pos--;
- }
- break;
- case INPT_KEY(KEY_DC): // del - delete character under cursor
- if(ti->pos < chars) {
- char *pos = g_utf8_offset_to_pointer(ti->str->str, ti->pos);
- g_string_erase(ti->str, pos-ti->str->str, g_utf8_next_char(pos)-pos);
- }
- break;
- case INPT_CTRL('w'): // C-w - delete to previous space
- case INPT_ALT(127): // Alt+backspace
- if(ti->pos > 0) {
- char *end = g_utf8_offset_to_pointer(ti->str->str, ti->pos-1);
- char *begin = end;
- while(begin > ti->str->str && !iswordchar(*begin))
- begin--;
- while(begin > ti->str->str && iswordchar(*(begin-1)))
- begin--;
- ti->pos -= g_utf8_strlen(begin, g_utf8_next_char(end)-begin);
- g_string_erase(ti->str, begin-ti->str->str, g_utf8_next_char(end)-begin);
- }
- break;
- case INPT_ALT('d'): // Alt+d - delete to next space
- if(ti->pos < chars) {
- char *begin = g_utf8_offset_to_pointer(ti->str->str, ti->pos);
- char *end = begin;
- while(*end == ' ')
- end++;
- while(*end && *(end+1) && *(end+1) != ' ')
- end++;
- g_string_erase(ti->str, begin-ti->str->str, g_utf8_next_char(end)-begin);
- }
- break;
- case INPT_CTRL('k'): // C-k - delete everything after cursor
- if(ti->pos < chars)
- g_string_erase(ti->str, g_utf8_offset_to_pointer(ti->str->str, ti->pos)-ti->str->str, -1);
- break;
- case INPT_CTRL('u'): // C-u - delete entire line
- g_string_erase(ti->str, 0, -1);
- ti->pos = 0;
- break;
- case INPT_KEY(KEY_UP): // up - history search back
- case INPT_KEY(KEY_DOWN): // down - history search forward
- if(ti->usehist)
- ui_textinput_search(ti, key == INPT_KEY(KEY_UP));
- else
- return FALSE;
- break;
- case INPT_CTRL('i'): // tab - autocomplete
- ui_textinput_complete(ti);
- completereset = FALSE;
- break;
- case INPT_CTRL('j'): // newline - accept & clear
- *str = ui_textinput_reset(ti);
- break;
- default:
- if(INPT_TYPE(key) == 1) { // char
- g_string_insert_unichar(ti->str, g_utf8_offset_to_pointer(ti->str->str, ti->pos)-ti->str->str, INPT_CODE(key));
- ti->pos++;
- } else
- return FALSE;
- }
- if(completereset)
- ui_textinput_complete_reset(ti);
- return TRUE;
-}
-
-
-
-
-
-
-// Generic listing "widget".
-// This widget allows easy listing, selecting and paging of (dynamic) GSequence
-// lists. The list is managed by the user, but the widget does need to be
-// notified of insertions and deletions.
-
-#if INTERFACE
-
-struct ui_listing_t {
- GSequence *list;
- GSequenceIter *sel;
- GSequenceIter *top;
- gboolean topisbegin;
- gboolean selisbegin;
- gboolean (*skip)(ui_listing_t *, GSequenceIter *, void *);
- void *dat;
-
- // fields needed for searching
- ui_textinput_t *search_box;
- gchar *query;
- gint match_start;
- gint match_end;
- const char *(*to_string)(GSequenceIter *);
-}
-
-#endif
-
-// error values for ui_listing_t.match_start
-#define REGEX_NO_MATCH -1
-#define REGEX_ERROR -2
-
-
-// TODO: This can be relatively slow (linear search), is used often but rarely
-// changes. Cache this in the struct if it becomes a problem.
-static GSequenceIter *ui_listing_getbegin(ui_listing_t *ul) {
- GSequenceIter *i = g_sequence_get_begin_iter(ul->list);
- while(!g_sequence_iter_is_end(i) && ul->skip && ul->skip(ul, i, ul->dat))
- i = g_sequence_iter_next(i);
- return i;
-}
-
-
-static GSequenceIter *ui_listing_next(ui_listing_t *ul, GSequenceIter *i) {
- do
- i = g_sequence_iter_next(i);
- while(!g_sequence_iter_is_end(i) && ul->skip && ul->skip(ul, i, ul->dat));
- return i;
-}
-
-
-static GSequenceIter *ui_listing_prev(ui_listing_t *ul, GSequenceIter *i) {
- GSequenceIter *begin = ui_listing_getbegin(ul);
- do
- i = g_sequence_iter_prev(i);
- while(!g_sequence_iter_is_begin(i) && i != begin && ul->skip && ul->skip(ul, i, ul->dat));
- if(g_sequence_iter_is_begin(i))
- i = begin;
- return i;
-}
-
-
-// update top/sel in case they used to be the start of the list but aren't anymore
-void ui_listing_inserted(ui_listing_t *ul) {
- GSequenceIter *begin = ui_listing_getbegin(ul);
- if(!!ul->topisbegin != !!(ul->top == begin))
- ul->top = ui_listing_getbegin(ul);
- if(!!ul->selisbegin != !!(ul->sel == begin))
- ul->sel = ui_listing_getbegin(ul);
-}
-
-
-// called after the order of the list has changed
-// update sel in case it used to be the start of the list but isn't anymore
-void ui_listing_sorted(ui_listing_t *ul) {
- if(!!ul->selisbegin != !!(ul->sel == ui_listing_getbegin(ul)))
- ul->sel = ui_listing_getbegin(ul);
-}
-
-
-static void ui_listing_updateisbegin(ui_listing_t *ul) {
- GSequenceIter *begin = ui_listing_getbegin(ul);
- ul->topisbegin = ul->top == begin;
- ul->selisbegin = ul->sel == begin;
-}
-
-
-// update top/sel in case one of them is removed.
-// call this before using g_sequence_remove()
-void ui_listing_remove(ui_listing_t *ul, GSequenceIter *iter) {
- if(ul->top == iter)
- ul->top = ui_listing_prev(ul, iter);
- if(ul->top == iter)
- ul->top = ui_listing_next(ul, iter);
- if(ul->sel == iter) {
- ul->sel = ui_listing_next(ul, iter);
- if(g_sequence_iter_is_end(ul->sel))
- ul->sel = ui_listing_prev(ul, iter);
- if(ul->sel == iter)
- ul->sel = g_sequence_get_end_iter(ul->list);
- }
- ui_listing_updateisbegin(ul);
-}
-
-
-// called when the skip() function changes behaviour (i.e. some items that were
-// skipped aren't now or the other way around).
-void ui_listing_skipchanged(ui_listing_t *ul) {
- // sel got hidden? oops!
- if(!g_sequence_iter_is_end(ul->sel) && ul->skip(ul, ul->sel, ul->dat)) {
- ul->sel = ui_listing_next(ul, ul->sel);
- if(g_sequence_iter_is_end(ul->sel))
- ul->sel = ui_listing_prev(ul, ul->sel);
- }
- // top got hidden? oops as well
- if(!g_sequence_iter_is_end(ul->top) && ul->skip(ul, ul->top, ul->dat))
- ul->top = ui_listing_prev(ul, ul->top);
- ui_listing_updateisbegin(ul);
-}
-
-
-ui_listing_t *ui_listing_create(GSequence *list, gboolean (*skip)(ui_listing_t *, GSequenceIter *, void *), void *dat, const char *(*to_string)(GSequenceIter *)) {
- ui_listing_t *ul = g_slice_new0(ui_listing_t);
- ul->list = list;
- ul->sel = ul->top = ui_listing_getbegin(ul);
- ul->topisbegin = ul->selisbegin = TRUE;
- ul->skip = skip;
- ul->dat = dat;
-
- ul->search_box = NULL;
- ul->query = NULL;
- ul->match_start = REGEX_NO_MATCH;
- ul->to_string = to_string;
-
- return ul;
-}
-
-
-void ui_listing_free(ui_listing_t *ul) {
- if(ul->search_box)
- ui_textinput_free(ul->search_box);
- if(ul->query)
- g_free(ul->query);
- g_slice_free(ui_listing_t, ul);
-}
-
-
-// search next/previous
-static void ui_listing_search_advance(ui_listing_t *ul, GSequenceIter *startpos, gboolean prev) {
- if(g_sequence_iter_is_end(startpos) && g_sequence_iter_is_end((startpos = ui_listing_getbegin(ul))))
- return;
- GRegex *regex = ul->query ? g_regex_new(ul->query, G_REGEX_CASELESS | G_REGEX_OPTIMIZE, 0, NULL) : NULL;
- if(!regex) {
- ul->match_start = REGEX_ERROR;
- return;
- }
- ul->match_start = REGEX_NO_MATCH;
-
- GSequenceIter *pos = startpos;
- do {
- const char *candidate = ul->to_string(pos);
- GMatchInfo *match_info;
- if(g_regex_match(regex, candidate, 0, &match_info)) {
- g_match_info_fetch_pos(match_info, 0, &ul->match_start, &ul->match_end);
- g_match_info_free(match_info);
- ul->sel = pos;
- break;
- }
- g_match_info_free(match_info);
-
- pos = (prev ? ui_listing_prev : ui_listing_next)(ul, pos);
- if(g_sequence_iter_is_begin(pos))
- pos = ui_listing_prev(ul, g_sequence_get_end_iter(ul->list));
- else if(g_sequence_iter_is_end(pos))
- pos = ui_listing_getbegin(ul);
- } while(ul->match_start == REGEX_NO_MATCH && pos != startpos);
-
- g_regex_unref(regex);
-}
-
-
-// handle keys in search mode
-static void ui_listing_search(ui_listing_t *ul, guint64 key) {
- char *completed = NULL;
- ui_textinput_key(ul->search_box, key, &completed);
-
- g_free(ul->query);
- ul->query = completed ? completed : ui_textinput_get(ul->search_box);
-
- if(completed) {
- if(ul->match_start < 0) {
- g_free(ul->query);
- ul->query = NULL;
- }
- ui_textinput_free(ul->search_box);
- ul->search_box = NULL;
- ul->match_start = -1;
- ul->match_end = -1;
- } else
- ui_listing_search_advance(ul, ul->sel, FALSE);
-}
-
-
-gboolean ui_listing_key(ui_listing_t *ul, guint64 key, int page) {
- if(ul->search_box) {
- ui_listing_search(ul, key);
- return TRUE;
- }
-
- // stop highlighting
- ul->match_start = REGEX_NO_MATCH;
-
- switch(key) {
- case INPT_CHAR('/'): // start search mode
- if(ul->to_string) {
- if(ul->query) {
- g_free(ul->query);
- ul->query = NULL;
- }
- g_assert(!ul->search_box);
- ul->search_box = ui_textinput_create(FALSE, NULL);
- }
- break;
- case INPT_CHAR(','): // find next
- ui_listing_search_advance(ul, ui_listing_next(ul, ul->sel), FALSE);
- break;
- case INPT_CHAR('.'): // find previous
- ui_listing_search_advance(ul, ui_listing_prev(ul, ul->sel), TRUE);
- break;
- case INPT_KEY(KEY_NPAGE): { // page down
- int i = page;
- while(i-- && !g_sequence_iter_is_end(ul->sel))
- ul->sel = ui_listing_next(ul, ul->sel);
- if(g_sequence_iter_is_end(ul->sel))
- ul->sel = ui_listing_prev(ul, ul->sel);
- break;
- }
- case INPT_KEY(KEY_PPAGE): { // page up
- int i = page;
- GSequenceIter *begin = ui_listing_getbegin(ul);
- while(i-- && ul->sel != begin)
- ul->sel = ui_listing_prev(ul, ul->sel);
- break;
- }
- case INPT_KEY(KEY_DOWN): // item down
- case INPT_CHAR('j'):
- ul->sel = ui_listing_next(ul, ul->sel);
- if(g_sequence_iter_is_end(ul->sel))
- ul->sel = ui_listing_prev(ul, ul->sel);
- break;
- case INPT_KEY(KEY_UP): // item up
- case INPT_CHAR('k'):
- ul->sel = ui_listing_prev(ul, ul->sel);
- break;
- case INPT_KEY(KEY_HOME): // home
- ul->sel = ui_listing_getbegin(ul);
- break;
- case INPT_KEY(KEY_END): // end
- ul->sel = ui_listing_prev(ul, g_sequence_get_end_iter(ul->list));
- break;
- default:
- return FALSE;
- }
-
- ui_listing_updateisbegin(ul);
- return TRUE;
-}
-
-
-static void ui_listing_fixtop(ui_listing_t *ul, int height) {
- // sel before top? top = sel!
- if(g_sequence_iter_compare(ul->top, ul->sel) > 0)
- ul->top = ul->sel;
-
- // does sel still fit on the screen?
- int i = height;
- GSequenceIter *n = ul->top;
- while(n != ul->sel && i > 0) {
- n = ui_listing_next(ul, n);
- i--;
- }
-
- // Nope? Make sure it fits
- if(i <= 0) {
- n = ul->sel;
- for(i=0; i<height-1; i++)
- n = ui_listing_prev(ul, n);
- ul->top = n;
- }
-
- // Make sure there's no empty space if we have enough rows to fill the screen
- i = height;
- n = ul->top;
- GSequenceIter *begin = ui_listing_getbegin(ul);
- while(!g_sequence_iter_is_end(n) && i-- > 0)
- n = ui_listing_next(ul, n);
- while(ul->top != begin && i-- > 0)
- ul->top = ui_listing_prev(ul, ul->top);
-}
-
-
-// Every item is assumed to occupy exactly one line.
-// Returns the relative position of the current page (in percent).
-// The selected row number is written to *cur, to be used with move(cur, 0).
-// TODO: The return value is only correct if no skip function is used or if
-// there are otherwise no hidden rows. It'll give a blatantly wrong number if
-// there are.
-int ui_listing_draw(ui_listing_t *ul, int top, int bottom, ui_cursor_t *cur, void (*cb)(ui_listing_t *, GSequenceIter *, int, void *)) {
- int search_box_height = !!ul->search_box;
- int listing_height = 1 + bottom - top - search_box_height;
- ui_listing_fixtop(ul, listing_height);
-
- if(cur) {
- cur->x = 0;
- cur->y = top;
- }
-
- // draw
- GSequenceIter *n = ul->top;
- while(top <= bottom - search_box_height && !g_sequence_iter_is_end(n)) {
- if(cur && n == ul->sel)
- cur->y = top;
- cb(ul, n, top, ul->dat);
- n = ui_listing_next(ul, n);
- top++;
- }
- if(ul->search_box) {
- const char *status;
- switch(ul->match_start) {
- case REGEX_NO_MATCH:
- status = "no match>";
- break;
- case REGEX_ERROR:
- status = " invalid>";
- break;
- default:
- status = " search>";
- }
- mvaddstr(bottom, 0, status);
- ui_textinput_draw(ul->search_box, bottom, 10, wincols - 10, cur);
- }
-
- ui_listing_updateisbegin(ul);
-
- int last = g_sequence_iter_get_position(g_sequence_get_end_iter(ul->list));
- return MIN(100, last ? (g_sequence_iter_get_position(ul->top)+listing_height)*100/last : 0);
-}
-
-
-void ui_listing_draw_match(ui_listing_t *ul, GSequenceIter *iter, int y, int x, int max) {
- const char *str = ul->to_string(iter);
- if(ul->sel == iter && ul->match_start >= 0) {
- int ofs1 = 0,
- ofs2 = ul->match_start,
- ofs3 = ul->match_end,
- width1 = substr_columns(str + ofs1, ofs2 - ofs1),
- width2 = substr_columns(str + ofs2, ofs3 - ofs2);
- mvaddnstr(y, x, str + ofs1, str_offset_from_columns(str + ofs1, MIN(width1, max)));
- x += width1, max -= width1;
- attron(A_REVERSE);
- mvaddnstr(y, x, str + ofs2, str_offset_from_columns(str + ofs2, MIN(width2, max)));
- x += width2, max -= width2;
- attroff(A_REVERSE);
- mvaddnstr(y, x, str + ofs3, str_offset_from_columns(str + ofs3, max));
- } else {
- mvaddnstr(y, x, str, max);
- }
-}