diff --git a/ncdc/CMakeLists.txt b/ncdc/CMakeLists.txt index 6e72672..8be1135 100644 --- a/ncdc/CMakeLists.txt +++ b/ncdc/CMakeLists.txt @@ -7,6 +7,7 @@ PKG_CHECK_MODULES(CONFUSE REQUIRED libconfuse) SET(TARGET "ncdc") SET(SOURCES + "include/ncdc/autocomplete.h" "include/ncdc/cmds.h" "include/ncdc/config.h" "include/ncdc/input.h" @@ -15,6 +16,7 @@ SET(SOURCES "include/ncdc/textview.h" "include/ncdc/treeview.h" "src/ack.c" + "src/autocomplete.c" "src/cmds.c" "src/config.c" "src/close.c" diff --git a/ncdc/include/ncdc/autocomplete.h b/ncdc/include/ncdc/autocomplete.h new file mode 100644 index 0000000..0dfecc7 --- /dev/null +++ b/ncdc/include/ncdc/autocomplete.h @@ -0,0 +1,40 @@ +/* + * Part of ncdc - a discord client for the console + * Copyright (C) 2019 Florian Stinglmayr + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef NCDC_AUTOCOMPLETE_H +#define NCDC_AUTOCOMPLETE_H + +#include + +struct ncdc_autocomplete_; +typedef struct ncdc_autocomplete_ *ncdc_autocomplete_t; + +ncdc_autocomplete_t ncdc_autocomplete_new(void); + +bool ncdc_autocomplete_prepare(ncdc_autocomplete_t a, + wchar_t const *s, ssize_t sz, + size_t pos); +void ncdc_autocomplete_completions(ncdc_autocomplete_t a, + wchar_t **words, ssize_t num); +bool ncdc_autocomplete_complete(ncdc_autocomplete_t a, int *newpos); +void ncdc_autocomplete_reset(ncdc_autocomplete_t a); + +int ncdc_autocomplete_word_index(ncdc_autocomplete_t a); +wchar_t const *ncdc_autocomplete_completed(ncdc_autocomplete_t a); + +#endif diff --git a/ncdc/include/ncdc/cmds.h b/ncdc/include/ncdc/cmds.h index b9e5c95..83ffb58 100644 --- a/ncdc/include/ncdc/cmds.h +++ b/ncdc/include/ncdc/cmds.h @@ -47,6 +47,8 @@ bool ncdc_dispatch(ncdc_mainwindow_t n, wchar_t const *s); * sub commands. for example usage see the friends command */ ncdc_commands_t *ncdc_find_cmd(ncdc_commands_t *cmds, wchar_t const *name); +wchar_t **ncdc_cmd_names(void); +size_t ncdc_cmd_names_size(void); bool ncdc_cmd_ack(ncdc_mainwindow_t n, size_t ac, wchar_t **av, wchar_t const *f); bool ncdc_cmd_close(ncdc_mainwindow_t n, size_t ac, wchar_t **av, wchar_t const *f); diff --git a/ncdc/include/ncdc/input.h b/ncdc/include/ncdc/input.h index 6538970..8b9e503 100644 --- a/ncdc/include/ncdc/input.h +++ b/ncdc/include/ncdc/input.h @@ -21,6 +21,7 @@ #include #include +#include struct ncdc_input_; typedef struct ncdc_input_ *ncdc_input_t; diff --git a/ncdc/include/ncdc/ncdc.h b/ncdc/include/ncdc/ncdc.h index 9c77d06..34bbb04 100644 --- a/ncdc/include/ncdc/ncdc.h +++ b/ncdc/include/ncdc/ncdc.h @@ -89,9 +89,12 @@ char *read_char(FILE *stream); int aswprintf(wchar_t **buffer, wchar_t const *fmt, ...); char *w_convert(wchar_t const *w); wchar_t* wcsndup(const wchar_t* string, size_t maxlen); + size_t w_strlenv(wchar_t **s); void w_strfreev(wchar_t **s); +wchar_t **w_strdupv(wchar_t **s, ssize_t sz); wchar_t *w_joinv(wchar_t const **v, size_t len); + wchar_t **w_tokenise(wchar_t const *w); wchar_t *w_next_tok(wchar_t const *w); wchar_t const *w_next_word(wchar_t const *w, ssize_t len); diff --git a/ncdc/src/autocomplete.c b/ncdc/src/autocomplete.c new file mode 100644 index 0000000..5149636 --- /dev/null +++ b/ncdc/src/autocomplete.c @@ -0,0 +1,215 @@ +/* + * Part of ncdc - a discord client for the console + * Copyright (C) 2019 Florian Stinglmayr + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +struct ncdc_autocomplete_ +{ + dc_refable_t ref; + + wchar_t *buffer; + wchar_t *word; + wchar_t *completed; + + wchar_t **completions; + size_t completions_size; + size_t search_pos; + + size_t buffer_size; + size_t pos; + size_t word_pos; + size_t start; + size_t end; +}; + +static void ncdc_autocomplete_free(ncdc_autocomplete_t a) +{ + return_if_true(a == NULL,); + + free(a->buffer); + a->buffer = NULL; + + free(a->completed); + a->completed = NULL; + + free(a->word); + a->word = NULL; + + w_strfreev(a->completions); + a->completions = NULL; + + free(a); +} + +ncdc_autocomplete_t ncdc_autocomplete_new(void) +{ + ncdc_autocomplete_t c = calloc(1, sizeof(struct ncdc_autocomplete_)); + return_if_true(c == NULL, NULL); + + c->ref.cleanup = (dc_cleanup_t)ncdc_autocomplete_free; + + return dc_ref(c); +} + +void ncdc_autocomplete_reset(ncdc_autocomplete_t a) +{ + return_if_true(a == NULL,); + + free(a->buffer); + a->buffer = NULL; + + free(a->word); + a->word = NULL; + + free(a->completed); + a->completed = NULL; + + w_strfreev(a->completions); + a->completions = NULL; + a->completions_size = 0; + + a->buffer_size = a->search_pos = a->pos = a->word_pos = a->start = a->end = 0; +} + +bool ncdc_autocomplete_prepare(ncdc_autocomplete_t a, + wchar_t const *s, ssize_t sz, + size_t pos) +{ + return_if_true(a == NULL || s == NULL, false); + + size_t start = pos, end = pos; + ssize_t i = 0, words = 0; + bool inword = false; + + return_if_true(a->buffer != NULL && a->word != NULL, true); + + if (sz < 0) { + sz = wcslen(s); + } + + /* figure out the current word for autocompletion + */ + for (start = pos; !iswspace(s[start]) && start > 0; start--) + ; + + for (end = pos; !iswspace(s[end]) && pos < sz; end++) + ; + + /* count words backwards, to figure out what position our + * current word has in the string. this is important for + * others because they might have positional arguments + */ + if (start > 0) { + for (i = start; i >= 0; i--) { + if (iswspace(s[i])) { + inword = false; + } else if (!inword) { + inword = true; + ++words; + } + } + } + + free(a->buffer); + a->buffer = wcsndup(s, sz); + a->buffer_size = sz; + + a->start = start; + a->end = end; + + a->pos = pos; + a->word_pos = words; + + a->word = wcsndup(s + start, end); + + return true; +} + +void ncdc_autocomplete_completions(ncdc_autocomplete_t a, + wchar_t **words, ssize_t num) +{ + return_if_true(a == NULL,); + + w_strfreev(a->completions); + a->completions = NULL; + a->completions_size = 0; + + if (words != NULL) { + a->completions = w_strdupv(words, num); + a->completions_size = w_strlenv(words); + } +} + +int ncdc_autocomplete_word_index(ncdc_autocomplete_t a) +{ + return_if_true(a == NULL, -1); + return a->word_pos; +} + +bool ncdc_autocomplete_complete(ncdc_autocomplete_t a, int *newpos) +{ + bool found = false; + size_t wordlen = 0, foundlen = 0; + wchar_t const *item = NULL; + + return_if_true(a == NULL, false); + + wordlen = wcslen(a->word); + + for (; a->search_pos < a->completions_size; a->search_pos++) { + item = a->completions[a->search_pos]; + if (wcsncmp(a->word, item, wordlen) == 0) { + found = true; + break; + } + } + + ++a->search_pos; + + if (a->search_pos >= a->completions_size) { + a->search_pos = 0; /* wrap around search */ + } + + if (!found) { + return false; + } + + free(a->completed); + a->completed = NULL; + + foundlen = wcslen(item); + + a->completed = calloc(wcslen(a->buffer) + foundlen + 1, sizeof(wchar_t)); + return_if_true(a->completed == NULL, false); + + wcsncat(a->completed, a->buffer, a->start); + wcscat(a->completed, item); + wcscat(a->completed, a->buffer + a->end); + + if (newpos != NULL) { + *newpos = a->start + foundlen; + } + + return true; +} + +wchar_t const *ncdc_autocomplete_completed(ncdc_autocomplete_t a) +{ + return_if_true(a == NULL, NULL); + return a->completed; +} diff --git a/ncdc/src/cmds.c b/ncdc/src/cmds.c index 35be31b..e3a351b 100644 --- a/ncdc/src/cmds.c +++ b/ncdc/src/cmds.c @@ -44,6 +44,9 @@ static pthread_t thr; static pthread_mutex_t mtx; static pthread_cond_t cnd; +static wchar_t **names; +static size_t names_size; + typedef struct { ncdc_commands_t *cmd; wchar_t *f; @@ -93,6 +96,8 @@ bool ncdc_dispatch_init(void) { return_if_true(queue != NULL, true); + size_t i = 0; + queue = g_queue_new(); return_if_true(queue == NULL, false); @@ -101,6 +106,16 @@ bool ncdc_dispatch_init(void) pthread_create(&thr, NULL, async_dispatcher, NULL); + /* build a table of names for the autocompletion, note that + * the cmds array is already one size too large (to hold a NULL) + */ + names_size = (sizeof(cmds) / sizeof(ncdc_commands_t)) - 1; + names = calloc(names_size + 1, sizeof(wchar_t*)); + + for (i = 0; cmds[i].name != NULL; i++) { + names[i] = wcsdup(cmds[i].name); + } + return true; } @@ -121,6 +136,10 @@ bool ncdc_dispatch_deinit(void) g_queue_free(queue); queue = NULL; + w_strfreev(names); + names = NULL; + names_size = 0; + return true; } @@ -137,6 +156,16 @@ ncdc_commands_t *ncdc_find_cmd(ncdc_commands_t *cmds, wchar_t const *name) return NULL; } +wchar_t **ncdc_cmd_names(void) +{ + return names; +} + +size_t ncdc_cmd_names_size(void) +{ + return names_size; +} + bool ncdc_dispatch(ncdc_mainwindow_t n, wchar_t const *s) { wchar_t **tokens = NULL; diff --git a/ncdc/src/input.c b/ncdc/src/input.c index 3542d86..a088b6b 100644 --- a/ncdc/src/input.c +++ b/ncdc/src/input.c @@ -19,6 +19,7 @@ #include #include #include +#include struct ncdc_input_ { @@ -30,6 +31,8 @@ struct ncdc_input_ ncdc_input_callback_t callback; void *callback_arg; + + ncdc_autocomplete_t autocomplete; }; static void ncdc_input_enter(ncdc_input_t p); @@ -38,6 +41,8 @@ static void ncdc_input_free(ncdc_input_t p) { g_array_unref(p->buffer); + dc_unref(p->autocomplete); + free(p); } @@ -53,9 +58,60 @@ ncdc_input_t ncdc_input_new(void) p->keys = keys_emacs; + p->autocomplete = ncdc_autocomplete_new(); + return p; } +static void ncdc_input_autocomplete(ncdc_input_t i) +{ + bool complete = false; + bool ret = false; + wchar_t const *completed = NULL; + int newpos = 0; + + ret = ncdc_autocomplete_prepare(i->autocomplete, + (wchar_t const*)i->buffer->data, -1, + i->cursor + ); + if (!ret) { + return; + } + + if (ncdc_autocomplete_word_index(i->autocomplete) == 0) { + /* check if we have a command, and if so, autocomplete for + * commands. + */ + if (g_array_index(i->buffer, wchar_t, 0) == L'/') { + ncdc_autocomplete_completions(i->autocomplete, + ncdc_cmd_names(), + ncdc_cmd_names_size() + ); + complete = true; + } + } + + if (complete) { + ret = ncdc_autocomplete_complete(i->autocomplete, &newpos); + if (ret) { + completed = ncdc_autocomplete_completed(i->autocomplete); + /* exchange our buffer, for the autocompleted buffer + */ + g_array_remove_range(i->buffer, 0, i->buffer->len); + g_array_append_vals(i->buffer, completed, wcslen(completed)); + i->cursor = newpos; + + if (i->cursor >= i->buffer->len) { + wchar_t space = L' '; + g_array_append_val(i->buffer, space); + ++i->cursor; + } + } + } else { + ncdc_autocomplete_reset(i->autocomplete); + } +} + void ncdc_input_feed(ncdc_input_t input, wchar_t const *c, size_t sz) { return_if_true(input == NULL,); @@ -63,11 +119,16 @@ void ncdc_input_feed(ncdc_input_t input, wchar_t const *c, size_t sz) if (c[0] == '\r') { ncdc_input_enter(input); + ncdc_autocomplete_reset(input->autocomplete); + } else if (c[0] == '\t') { + ncdc_input_autocomplete(input); } else if ((bind = ncdc_find_keybinding(input->keys, c, sz)) != NULL) { bind->handler(input); + ncdc_autocomplete_reset(input->autocomplete); } else if (iswprint(c[0])) { g_array_insert_vals(input->buffer, input->cursor, &c[0], 1); input->cursor += wcswidth(&c[0], 1); + ncdc_autocomplete_reset(input->autocomplete); } } diff --git a/ncdc/src/util.c b/ncdc/src/util.c index 5aabdc8..b220e3e 100644 --- a/ncdc/src/util.c +++ b/ncdc/src/util.c @@ -113,6 +113,29 @@ void w_strfreev(wchar_t **s) free(s); } +wchar_t **w_strdupv(wchar_t **s, ssize_t sz) +{ + wchar_t **copy = NULL; + size_t i = 0; + + if (sz < 0) { + sz = w_strlenv(s); + } + + copy = calloc(sz + 1, sizeof(wchar_t*)); + return_if_true(copy == NULL, NULL); + + for (i = 0; i < sz; i++) { + copy[i] = wcsdup(s[i]); + if (copy[i] == NULL) { + w_strfreev(copy); + return NULL; + } + } + + return copy; +} + wchar_t **w_tokenise(wchar_t const *str) { wchar_t const *p = NULL, *start_of_word = NULL;