summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJari Vetoniemi <mailroxas@gmail.com>2014-04-10 01:09:35 +0300
committerJari Vetoniemi <mailroxas@gmail.com>2014-04-10 01:10:05 +0300
commit67be25fbe43274340de89049fec7098cdf998b47 (patch)
treef8d6be283944fc90e1c224c461f6e92b3ae78d6f
parenta3498b25f414a888aa01ffd1cf9b2e44f6c54764 (diff)
downloadbemenu-67be25fbe43274340de89049fec7098cdf998b47.tar.gz
bemenu-67be25fbe43274340de89049fec7098cdf998b47.tar.bz2
bemenu-67be25fbe43274340de89049fec7098cdf998b47.zip
Basic working bemenu with curses backend
-rw-r--r--client/client.c95
-rw-r--r--lib/CMakeLists.txt7
-rw-r--r--lib/bemenu.c60
-rw-r--r--lib/bemenu.h227
-rw-r--r--lib/draw/curses.c258
-rw-r--r--lib/filter.c64
-rw-r--r--lib/internal.h39
-rw-r--r--lib/item.c75
-rw-r--r--lib/menu.c527
-rw-r--r--lib/util.c226
10 files changed, 1504 insertions, 74 deletions
diff --git a/client/client.c b/client/client.c
index 559651f..1c30498 100644
--- a/client/client.c
+++ b/client/client.c
@@ -6,8 +6,83 @@
*/
#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <stddef.h>
+#include <assert.h>
#include <bemenu.h>
+static ptrdiff_t getLine(char **outLine, size_t *outAllocated, FILE *stream)
+{
+ size_t len = 0, allocated;
+ char *s, *buffer;
+
+ assert(outLine != NULL);
+ assert(outAllocated != NULL);
+
+ if (stream == NULL || feof(stream) || ferror(stream))
+ return -1;
+
+ allocated = *outAllocated;
+ buffer = *outLine;
+
+ if (buffer == NULL || allocated == 0) {
+ if (!(buffer = calloc(1, (allocated = 1024) + 1)))
+ return -1;
+ }
+
+ for (s = buffer;;) {
+ if (fgets(s, allocated - (s - buffer), stream) == NULL)
+ return -1;
+
+ len = strlen(s);
+ if (feof(stream))
+ break;
+
+ if (len > 0 && s[len - 1] == '\n')
+ break;
+
+ if (len + 1 >= allocated - (s - buffer)) {
+ void *tmp = realloc(buffer, 2 * allocated);
+ if (!tmp)
+ break;
+
+ buffer = tmp;
+ s = buffer + allocated - 1;
+ memset(s, 0, allocated - (s - buffer));
+ allocated *= 2;
+ } else {
+ s += len;
+ }
+ }
+
+ *outAllocated = allocated;
+ *outLine = buffer;
+
+ if (s[len - 1] == '\n')
+ s[len - 1] = 0;
+
+ return s - buffer + len;
+}
+
+static void readItemsToMenuFromStdin(bmMenu *menu)
+{
+ ptrdiff_t len;
+ size_t size;
+ char *line = NULL;
+
+ while ((len = getLine(&line, &size, stdin)) != -1) {
+ bmItem *item = bmItemNew((len > 0 ? line : NULL));
+ if (!item)
+ break;
+
+ bmMenuAddItem(menu, item);
+ }
+
+ if (line)
+ free(line);
+}
+
/**
* Main method
*
@@ -21,16 +96,30 @@ int main(int argc, char **argv)
{
(void)argc, (void)argv;
- bmMenu *menu = bmMenuNew(BM_DRAW_MODE_NONE);
+ bmMenu *menu = bmMenuNew(BM_DRAW_MODE_CURSES);
if (!menu)
return EXIT_FAILURE;
- bmMenuRender(menu);
+ bmMenuSetTitle(menu, "bemenu");
+ readItemsToMenuFromStdin(menu);
+
+ bmKey key;
+ unsigned int unicode;
+ int status = 0;
+ do {
+ bmMenuRender(menu);
+ key = bmMenuGetKey(menu, &unicode);
+ } while ((status = bmMenuRunWithKey(menu, key, unicode)) == BM_RUN_RESULT_RUNNING);
+
+ if (status == BM_RUN_RESULT_SELECTED) {
+ bmItem *item = bmMenuGetSelectedItem(menu);
+ printf("%s\n", bmItemGetText(item));
+ }
bmMenuFree(menu);
- return EXIT_SUCCESS;
+ return (status == BM_RUN_RESULT_SELECTED ? EXIT_SUCCESS : EXIT_FAILURE);
}
/* vim: set ts=8 sw=4 tw=0 :*/
diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt
index cf489fd..e669ef5 100644
--- a/lib/CMakeLists.txt
+++ b/lib/CMakeLists.txt
@@ -1,6 +1,9 @@
# Sources
SET(BEMENU_SOURCE
- bemenu.c
+ menu.c
+ item.c
+ filter.c
+ util.c
draw/curses.c
)
SET(BEMENU_INCLUDE)
@@ -22,6 +25,6 @@ ENDIF ()
# Compile
INCLUDE_DIRECTORIES(${BEMENU_INCLUDE})
ADD_LIBRARY(bemenu ${BEMENU_SOURCE})
-TARGET_LINK_LIBRARIES(bemenu ${BEMENU_LIBRARIES})
+TARGET_LINK_LIBRARIES(bemenu ${BEMENU_LIBRARIES} dl)
# vim: set ts=8 sw=4 tw=0 :
diff --git a/lib/bemenu.c b/lib/bemenu.c
deleted file mode 100644
index d4ae77e..0000000
--- a/lib/bemenu.c
+++ /dev/null
@@ -1,60 +0,0 @@
-/**
- * @file bemenu.c
- */
-
-#include "internal.h"
-#include <stdlib.h>
-#include <assert.h>
-
-/**
- * Create new bmMenu instance.
- *
- * @param drawMode Render method to be used for this menu instance.
- * @return bmMenu for new menu instance, NULL if creation failed.
- */
-bmMenu* bmMenuNew(bmDrawMode drawMode)
-{
- bmMenu *menu = calloc(1, sizeof(bmMenu));
-
- menu->drawMode = drawMode;
-
- if (!menu)
- return NULL;
-
- switch (menu->drawMode) {
- default:break;
- }
-
- return menu;
-}
-
-/**
- * Release bmMenu instance.
- *
- * @param menu bmMenu instance to be freed from memory.
- */
-void bmMenuFree(bmMenu *menu)
-{
- assert(menu != NULL);
-
- if (menu->renderApi.free)
- menu->renderApi.free();
-
- free(menu);
-}
-
-/**
- * Create new bmMenu instance.
- *
- * @param drawMode Render method to be used for this menu instance.
- * @return bmMenu for new menu instance, NULL if creation failed.
- */
-void bmMenuRender(bmMenu *menu)
-{
- assert(menu != NULL);
-
- if (menu->renderApi.render)
- menu->renderApi.render(menu->items, menu->itemsCount);
-}
-
-/* vim: set ts=8 sw=4 tw=0 :*/
diff --git a/lib/bemenu.h b/lib/bemenu.h
index 3fdfb0b..b4b9b30 100644
--- a/lib/bemenu.h
+++ b/lib/bemenu.h
@@ -5,17 +5,71 @@
*/
/**
- * Draw mode constants for setting bmMenu instance draw mode.
+ * Draw mode constants for bmMenu instance draw mode.
*
* BM_DRAW_MODE_LAST is provided for enumerating draw modes.
* Instancing with it however provides exactly same functionality as BM_DRAW_MODE_NONE.
*/
typedef enum bmDrawMode {
BM_DRAW_MODE_NONE,
+ BM_DRAW_MODE_CURSES,
BM_DRAW_MODE_LAST
} bmDrawMode;
+/**
+ * Filter mode constants for bmMenu instance filter mode.
+ *
+ * BM_FILTER_MODE_LAST is provided for enumerating filter modes.
+ * Using it as filter mode however provides exactly same functionality as BM_FILTER_MODE_DMENU.
+ */
+typedef enum bmFilterMode {
+ BM_FILTER_MODE_DMENU,
+ BM_FILTER_MODE_DMENU_CASE_INSENSITIVE,
+ BM_FILTER_MODE_LAST
+} bmFilterMode;
+
+/**
+ * Result constants from bmMenuRunWithKey function.
+ *
+ * BM_RUN_RESULT_RUNNING means that menu is running and thus should be still renderer && ran.
+ * BM_RUN_RESULT_SELECTED means that menu was closed and items were selected.
+ * BM_RUN_RESULT_CANCEL means that menu was closed and selection was canceled.
+ */
+typedef enum bmRunResult {
+ BM_RUN_RESULT_RUNNING,
+ BM_RUN_RESULT_SELECTED,
+ BM_RUN_RESULT_CANCEL,
+} bmRunResult;
+
+/**
+ * Key constants.
+ *
+ * BM_KEY_LAST is provided for enumerating keys.
+ */
+typedef enum bmKey {
+ BM_KEY_NONE,
+ BM_KEY_UP,
+ BM_KEY_DOWN,
+ BM_KEY_LEFT,
+ BM_KEY_RIGHT,
+ BM_KEY_HOME,
+ BM_KEY_END,
+ BM_KEY_PAGE_UP,
+ BM_KEY_PAGE_DOWN,
+ BM_KEY_BACKSPACE,
+ BM_KEY_DELETE,
+ BM_KEY_LINE_DELETE_LEFT,
+ BM_KEY_LINE_DELETE_RIGHT,
+ BM_KEY_WORD_DELETE,
+ BM_KEY_TAB,
+ BM_KEY_ESCAPE,
+ BM_KEY_RETURN,
+ BM_KEY_UNICODE,
+ BM_KEY_LAST
+} bmKey;
+
typedef struct _bmMenu bmMenu;
+typedef struct _bmItem bmItem;
/**
* Create new bmMenu instance.
@@ -33,10 +87,179 @@ bmMenu* bmMenuNew(bmDrawMode drawMode);
void bmMenuFree(bmMenu *menu);
/**
+ * Release items inside bmMenu instance.
+ *
+ * @param menu bmMenu instance which items will be freed from memory.
+ */
+void bmMenuFreeItems(bmMenu *menu);
+
+/**
+ * Set active filter mode to bmMenu instance.
+ *
+ * @param menu bmMenu instance where to set filter mode.
+ * @param mode bmFilterMode constant.
+ */
+void bmMenuSetFilterMode(bmMenu *menu, bmFilterMode mode);
+
+/**
+ * Get active filter mode from bmMenu instance.
+ *
+ * @param menu bmMenu instance where to get filter mode.
+ * @return bmFilterMode constant.
+ */
+bmFilterMode bmMenuGetFilterMode(const bmMenu *menu);
+
+/**
+ * Set title to bmMenu instance.
+ *
+ * @param menu bmMenu instance where to set title.
+ * @param title C "string" to set as title, can be NULL for empty title.
+ */
+int bmMenuSetTitle(bmMenu *menu, const char *title);
+
+/**
+ * Get title from bmMenu instance.
+ *
+ * @param menu bmMenu instance where to get title from.
+ * @return Pointer to null terminated C "string", can be NULL for empty title.
+ */
+const char* bmMenuGetTitle(const bmMenu *menu);
+
+/**
+ * Add item to bmMenu instance at specific index.
+ *
+ * @param menu bmMenu instance where item will be added.
+ * @param item bmItem instance to add.
+ * @param index Index where item will be added.
+ * @return 1 on successful add, 0 on failure.
+ */
+int bmMenuAddItemAt(bmMenu *menu, bmItem *item, unsigned int index);
+
+/**
+ * Add item to bmMenu instance.
+ *
+ * @param menu bmMenu instance where item will be added.
+ * @param item bmItem instance to add.
+ * @return 1 on successful add, 0 on failure.
+ */
+int bmMenuAddItem(bmMenu *menu, bmItem *item);
+
+/**
+ * Remove item from bmMenu instance at specific index.
+ *
+ * @param menu bmMenu instance from where item will be removed.
+ * @param index Index of item to remove.
+ * @return 1 on successful add, 0 on failure.
+ */
+int bmMenuRemoveItemAt(bmMenu *menu, unsigned int index);
+
+/**
+ * Remove item from bmMenu instance.
+ * The item won't be freed, use bmItemFree to do that.
+ *
+ * @param menu bmMenu instance from where item will be removed.
+ * @param item bmItem instance to remove.
+ * @return 1 on successful add, 0 on failure.
+ */
+int bmMenuRemoveItem(bmMenu *menu, bmItem *item);
+
+/**
+ * Get selected item from bmMenu instance.
+ *
+ * @param menu bmMenu instance from where to get selected item.
+ * @return Selected bmItem instance, NULL if none selected.
+ */
+bmItem* bmMenuGetSelectedItem(const bmMenu *menu);
+
+/**
+ * Get items from bmMenu instance.
+ *
+ * @param menu bmMenu instance from where to get items.
+ * @param nmemb Reference to unsigned int where total count of returned items will be stored.
+ * @return Pointer to array of bmItem pointers.
+ */
+bmItem** bmMenuGetItems(const bmMenu *menu, unsigned int *nmemb);
+
+/**
+ * Get filtered (displayed) items from bmMenu instance.
+ *
+ * @warning The pointer returned by this function _will_ be invalid when menu internally filters its list again.
+ * Do not store this pointer.
+ *
+ * @param menu bmMenu instance from where to get filtered items.
+ * @param nmemb Reference to unsigned int where total count of returned items will be stored.
+ * @return Pointer to array of bmItem pointers.
+ */
+bmItem** bmMenuGetFilteredItems(const bmMenu *menu, unsigned int *nmemb);
+
+/**
+ * Set items to bmMenu instance.
+ * Will replace all the old items on bmMenu instance.
+ *
+ * If items is NULL, or nmemb is zero, all items will be freed from the menu.
+ *
+ * @param menu bmMenu instance where items will be set.
+ * @param items Array of bmItem pointers to set.
+ * @param nmemb Total count of items in array.
+ * @return 1 on successful set, 0 on failure.
+ */
+int bmMenuSetItems(bmMenu *menu, const bmItem **items, unsigned int nmemb);
+
+/**
* Render bmMenu instance using chosen draw method.
*
* @param menu bmMenu instance to be rendered.
*/
-void bmMenuRender(bmMenu *menu);
+void bmMenuRender(const bmMenu *menu);
+
+/**
+ * Poll key and unicode from underlying UI toolkit.
+ *
+ * This function will block on CURSES draw mode.
+ *
+ * @param menu bmMenu instance from which to poll.
+ * @param unicode Reference to unsigned int.
+ * @return bmKey for polled key.
+ */
+bmKey bmMenuGetKey(bmMenu *menu, unsigned int *unicode);
+
+/**
+ * Advances menu logic with key and unicode as input.
+ *
+ * @param menu bmMenu instance to be advanced.
+ * @return bmRunResult for menu state.
+ */
+bmRunResult bmMenuRunWithKey(bmMenu *menu, bmKey key, unsigned int unicode);
+
+/**
+ * Allocate a new item.
+ *
+ * @param text Pointer to null terminated C "string", can be NULL for empty text.
+ * @return bmItem for new item instance, NULL if creation failed.
+ */
+bmItem* bmItemNew(const char *text);
+
+/**
+ * Release bmItem instance.
+ *
+ * @param item bmItem instance to be freed from memory.
+ */
+void bmItemFree(bmItem *item);
+
+/**
+ * Set text to bmItem instance.
+ *
+ * @param item bmItem instance where to set text.
+ * @param text C "string" to set as text, can be NULL for empty text.
+ */
+int bmItemSetText(bmItem *item, const char *text);
+
+/**
+ * Get text from bmItem instance.
+ *
+ * @param item bmItem instance where to get text from.
+ * @return Pointer to null terminated C "string", can be NULL for empty text.
+ */
+const char* bmItemGetText(const bmItem *item);
/* vim: set ts=8 sw=4 tw=0 :*/
diff --git a/lib/draw/curses.c b/lib/draw/curses.c
index c873496..48a146a 100644
--- a/lib/draw/curses.c
+++ b/lib/draw/curses.c
@@ -2,8 +2,260 @@
* @file curses.c
*/
-/*
- * code goes here
- */
+#include "../internal.h"
+#include <wchar.h>
+#include <string.h>
+#include <stdlib.h>
+#include <locale.h>
+#include <ncurses.h>
+#include <dlfcn.h>
+#include <assert.h>
+
+/* ncurses.h likes to define stuff for us.
+ * This unforunately mangles with our struct. */
+#undef erase
+#undef refresh
+#undef mvprintw
+#undef move
+#undef init_pair
+#undef attroff
+#undef attron
+#undef getmaxx
+#undef getmaxy
+#undef timeout
+
+static struct curses {
+ void *handle;
+ WINDOW *stdscr;
+ WINDOW* (*initscr)(void);
+ int (*endwin)(void);
+ int (*refresh)(void);
+ int (*erase)(void);
+ int (*get_wch)(wint_t *wch);
+ int (*mvprintw)(int x, int y, const char *fmt, ...);
+ int (*move)(int x, int y);
+ int (*init_pair)(short color, short f, short b);
+ int (*attroff)(int attrs);
+ int (*attron)(int attrs);
+ int (*start_color)(void);
+ int (*getmaxx)(WINDOW *win);
+ int (*getmaxy)(WINDOW *win);
+ int (*keypad)(WINDOW *win, bool bf);
+ int *ESCDELAY;
+} curses;
+
+static void _bmDrawCursesDrawLine(int pair, int y, const char *format, ...)
+{
+ static int ncols = 0;
+ static char *buffer = NULL;
+ int new_ncols = curses.getmaxx(curses.stdscr);
+
+ if (new_ncols <= 0)
+ return;
+
+ if (!buffer || new_ncols > ncols) {
+ if (buffer)
+ free(buffer);
+
+ ncols = new_ncols;
+
+ if (!(buffer = calloc(1, ncols + 1)))
+ return;
+ }
+
+ va_list args;
+ va_start(args, format);
+ int tlen = vsnprintf(NULL, 0, format, args) + 1;
+ if (tlen > ncols)
+ tlen = ncols;
+ va_end(args);
+
+ va_start(args, format);
+ vsnprintf(buffer, tlen, format, args);
+ va_end(args);
+
+ memset(buffer + tlen - 1, ' ', ncols - tlen + 1);
+
+ if (pair > 0)
+ curses.attron(COLOR_PAIR(pair));
+
+ curses.mvprintw(y, 0, buffer);
+
+ if (pair > 0)
+ curses.attroff(COLOR_PAIR(pair));
+}
+
+static void _bmDrawCursesRender(const bmMenu *menu)
+{
+ if (!curses.stdscr) {
+ freopen("/dev/tty", "rw", stdin);
+ setlocale(LC_CTYPE, "");
+ if ((curses.stdscr = curses.initscr()) == NULL)
+ return;
+
+ *curses.ESCDELAY = 25;
+ curses.keypad(curses.stdscr, true);
+
+ curses.start_color();
+ curses.init_pair(1, COLOR_BLACK, COLOR_RED);
+ curses.init_pair(2, COLOR_RED, COLOR_BLACK);
+ }
+
+ const unsigned int lines = curses.getmaxy(curses.stdscr);
+ curses.erase();
+
+ size_t titleLen = (menu->title ? strlen(menu->title) + 1 : 0);
+
+ _bmDrawCursesDrawLine(0, 0, "%*s%s", titleLen, "", menu->filter);
+
+ if (menu->title) {
+ curses.attron(COLOR_PAIR(1));
+ curses.mvprintw(0, 0, menu->title);
+ curses.attroff(COLOR_PAIR(1));
+ }
+
+ unsigned int i, cl = 1;
+ unsigned int itemsCount;
+ bmItem **items = bmMenuGetFilteredItems(menu, &itemsCount);
+ for (i = (menu->index / (lines - 1)) * (lines - 1); i < itemsCount && cl < lines; ++i) {
+ int selected = (items[i] == bmMenuGetSelectedItem(menu));
+ _bmDrawCursesDrawLine((selected ? 2 : 0), cl++, "%s%s", (selected ? ">> " : " "), items[i]->text);
+ }
+
+ curses.move(0, titleLen + menu->cursesCursor);
+ curses.refresh();
+}
+
+static void _bmDrawCursesEndWin(void)
+{
+ if (curses.endwin)
+ curses.endwin();
+
+ curses.stdscr = NULL;
+}
+
+static bmKey _bmDrawCursesGetKey(unsigned int *unicode)
+{
+ assert(unicode != NULL);
+ *unicode = 0;
+
+ if (!curses.stdscr)
+ return BM_KEY_NONE;
+
+ curses.get_wch(unicode);
+ switch (*unicode) {
+ case 16: /* C-p */
+ case KEY_UP: return BM_KEY_UP;
+
+ case 14: /* C-n */
+ case KEY_DOWN: return BM_KEY_DOWN;
+
+ case 2: /* C-b */
+ case KEY_LEFT: return BM_KEY_LEFT;
+
+ case 6: /* C-f */
+ case KEY_RIGHT: return BM_KEY_RIGHT;
+
+ case 1: /* C-a */
+ case KEY_HOME: return BM_KEY_HOME;
+
+ case 5: /* C-e */
+ case KEY_END: return BM_KEY_END;
+
+ case KEY_PPAGE: return BM_KEY_PAGE_UP;
+ case KEY_NPAGE: return BM_KEY_PAGE_DOWN;
+
+ case 8: /* C-h */
+ case KEY_BACKSPACE: return BM_KEY_BACKSPACE;
+
+ case 4: /* C-d */
+ case KEY_DC: return BM_KEY_DELETE;
+
+ case 21: return BM_KEY_LINE_DELETE_LEFT; /* C-u */
+ case 11: return BM_KEY_LINE_DELETE_RIGHT; /* C-k */
+ case 23: return BM_KEY_WORD_DELETE; /* C-w */
+
+ case 9: return BM_KEY_TAB; /* Tab */
+
+ case 10: /* Return */
+ _bmDrawCursesEndWin();
+ return BM_KEY_RETURN;
+
+ case 7: /* C-g */
+ case 27: /* Escape */
+ _bmDrawCursesEndWin();
+ return BM_KEY_ESCAPE;
+
+ default: break;
+ }
+
+ return BM_KEY_UNICODE;
+}
+
+static void _bmDrawCursesFree(void)
+{
+ _bmDrawCursesEndWin();
+
+ if (curses.handle)
+ dlclose(curses.handle);
+
+ memset(&curses, 0, sizeof(curses));
+}
+
+int _bmDrawCursesInit(struct _bmRenderApi *api)
+{
+ memset(&curses, 0, sizeof(curses));
+
+ /* FIXME: hardcoded and not cross-platform */
+ curses.handle = dlopen("/usr/lib/libncursesw.so.5", RTLD_LAZY);
+
+ if (!curses.handle)
+ return 0;
+
+#define bmLoadFunction(x) (curses.x = dlsym(curses.handle, #x))
+
+ if (!bmLoadFunction(initscr))
+ goto function_pointer_exception;
+ if (!bmLoadFunction(endwin))
+ goto function_pointer_exception;
+ if (!bmLoadFunction(refresh))
+ goto function_pointer_exception;
+ if (!bmLoadFunction(get_wch))
+ goto function_pointer_exception;
+ if (!bmLoadFunction(erase))
+ goto function_pointer_exception;
+ if (!bmLoadFunction(mvprintw))
+ goto function_pointer_exception;
+ if (!bmLoadFunction(move))
+ goto function_pointer_exception;
+ if (!bmLoadFunction(init_pair))
+ goto function_pointer_exception;
+ if (!bmLoadFunction(attroff))
+ goto function_pointer_exception;
+ if (!bmLoadFunction(attron))
+ goto function_pointer_exception;
+ if (!bmLoadFunction(start_color))
+ goto function_pointer_exception;
+ if (!bmLoadFunction(getmaxx))
+ goto function_pointer_exception;
+ if (!bmLoadFunction(getmaxy))
+ goto function_pointer_exception;
+ if (!bmLoadFunction(keypad))
+ goto function_pointer_exception;
+ if (!bmLoadFunction(ESCDELAY))
+ goto function_pointer_exception;
+
+#undef bmLoadFunction
+
+ api->getKey = _bmDrawCursesGetKey;
+ api->render = _bmDrawCursesRender;
+ api->free = _bmDrawCursesFree;
+
+ return 1;
+
+function_pointer_exception:
+ _bmDrawCursesFree();
+ return 0;
+}
/* vim: set ts=8 sw=4 tw=0 :*/
diff --git a/lib/filter.c b/lib/filter.c
new file mode 100644
index 0000000..b1cf668
--- /dev/null
+++ b/lib/filter.c
@@ -0,0 +1,64 @@
+/**
+ * @file filter.c
+ */
+
+#include "internal.h"
+#include <stdlib.h>
+#include <assert.h>
+#include <string.h>
+
+/**
+ * Filter that mimics the vanilla dmenu filtering.
+ *
+ * @param menu bmMenu instance to filter.
+ * @param count unsigned int reference to filtered items count.
+ * @param selected unsigned int reference to new selected item index.
+ * @return Pointer to array of bmItem pointers.
+ */
+bmItem** _bmFilterDmenu(bmMenu *menu, unsigned int *count, unsigned int *selected)
+{
+ assert(menu != NULL);
+ assert(count != NULL);
+ assert(selected != NULL);
+ *count = *selected = 0;
+
+ /* FIXME: not real dmenu like filtering at all */
+
+ bmItem **filtered = calloc(menu->itemsCount, sizeof(bmItem*));
+ if (!filtered)
+ return NULL;
+
+ unsigned int i, f;
+ for (f = i = 0; i < menu->itemsCount; ++i) {
+ bmItem *item = menu->items[i];
+ if (item->text && strstr(item->text, menu->filter)) {
+ if (f == 0 || item == bmMenuGetSelectedItem(menu))
+ *selected = f;
+ filtered[f++] = item;
+ }
+ }
+
+ return _bmShrinkItemList(&filtered, menu->itemsCount, (*count = f));
+}
+
+/**
+ * Filter that mimics the vanilla case-insensitive dmenu filtering.
+ *
+ * @param menu bmMenu instance to filter.
+ * @param count unsigned int reference to filtered items count.
+ * @param selected unsigned int reference to new selected item index.
+ * @return Pointer to array of bmItem pointers.
+ */
+bmItem** _bmFilterDmenuCaseInsensitive(bmMenu *menu, unsigned int *count, unsigned int *selected)
+{
+ assert(menu != NULL);
+ assert(count != NULL);
+ assert(selected != NULL);
+ *count = *selected = 0;
+
+ /* FIXME: stub */
+
+ return NULL;
+}
+
+/* vim: set ts=8 sw=4 tw=0 :*/
diff --git a/lib/internal.h b/lib/internal.h
index c79ffd1..786e082 100644
--- a/lib/internal.h
+++ b/lib/internal.h
@@ -4,6 +4,10 @@
#include "bemenu.h"
+#ifndef size_t
+# include <stddef.h> /* for size_t */
+#endif
+
/**
* Internal bmItem struct that is not exposed to public.
* Represents a single item in menu.
@@ -17,7 +21,8 @@ struct _bmItem {
* Renderers should be able to fill this one as they see fit.
*/
struct _bmRenderApi {
- void (*render)(struct _bmItem **items, unsigned int nmemb);
+ bmKey (*getKey)(unsigned int *unicode);
+ void (*render)(const bmMenu *menu);
void (*free)(void);
};
@@ -25,10 +30,36 @@ struct _bmRenderApi {
* Internal bmMenu struct that is not exposed to public.
*/
struct _bmMenu {
- bmDrawMode drawMode;
struct _bmRenderApi renderApi;
- struct _bmItem **items;
- unsigned int itemsCount;
+ struct _bmItem **items, **filteredItems;
+ char *title, filter[1024];
+ unsigned int cursor, cursesCursor;
+ unsigned int itemsCount, allocatedCount;
+ unsigned int filteredCount;
+ unsigned int index;
+ bmFilterMode filterMode;
+ bmDrawMode drawMode;
};
+/* draw/curses.c */
+int _bmDrawCursesInit(struct _bmRenderApi *api);
+
+/* menu.c */
+int _bmMenuShouldRenderItem(const bmMenu *menu, const bmItem *item);
+
+/* filter.c */
+bmItem** _bmFilterDmenu(bmMenu *menu, unsigned int *count, unsigned int *selected);
+bmItem** _bmFilterDmenuCaseInsensitive(bmMenu *menu, unsigned int *count, unsigned int *selected);
+
+/* util.c */
+char* _bmStrdup(const char *s);
+bmItem** _bmShrinkItemList(bmItem ***list, size_t osize, size_t nsize);
+int _bmUtf8StringScreenWidth(const char *string);
+size_t _bmUtf8RuneNext(const char *string, size_t start);
+size_t _bmUtf8RunePrev(const char *string, size_t start);
+size_t _bmUtf8RuneWidth(const char *rune, unsigned int u8len);
+size_t _bmUtf8RuneRemove(char *string, size_t start, size_t *runeWidth);
+size_t _bmUtf8RuneInsert(char *string, size_t bufSize, size_t start, const char *rune, unsigned int u8len, size_t *runeWidth);
+size_t _bmUnicodeInsert(char *string, size_t bufSize, size_t start, unsigned int unicode, size_t *runeWidth);
+
/* vim: set ts=8 sw=4 tw=0 :*/
diff --git a/lib/item.c b/lib/item.c
new file mode 100644
index 0000000..40ebb52
--- /dev/null
+++ b/lib/item.c
@@ -0,0 +1,75 @@
+/**
+ * @file item.c
+ */
+
+#include "internal.h"
+#include <stdlib.h>
+#include <assert.h>
+#include <string.h>
+
+/**
+ * Allocate a new item.
+ *
+ * @param text Pointer to null terminated C "string", can be NULL for empty text.
+ * @return bmItem instance.
+ */
+bmItem* bmItemNew(const char *text)
+{
+ bmItem *item = calloc(1, sizeof(bmItem));
+
+ if (!item)
+ return NULL;
+
+ bmItemSetText(item, text);
+ return item;
+}
+
+/**
+ * Release bmItem instance.
+ *
+ * @param item bmItem instance to be freed from memory.
+ */
+void bmItemFree(bmItem *item)
+{
+ assert(item != NULL);
+
+ if (item->text)
+ free(item->text);
+
+ free(item);
+}
+
+/**
+ * Set text to bmItem instance.
+ *
+ * @param item bmItem instance where to set text.
+ * @param text C "string" to set as text, can be NULL for empty text.
+ */
+int bmItemSetText(bmItem *item, const char *text)
+{
+ assert(item != NULL);
+
+ char *copy = NULL;
+ if (text && !(copy = _bmStrdup(text)))
+ return 0;
+
+ if (item->text)
+ free(item->text);
+
+ item->text = copy;
+ return 1;
+}
+
+/**
+ * Get text from bmItem instance.
+ *
+ * @param item bmItem instance where to get text from.
+ * @return Pointer to null terminated C "string", can be NULL for empty text.
+ */
+const char* bmItemGetText(const bmItem *item)
+{
+ assert(item != NULL);
+ return item->text;
+}
+
+/* vim: set ts=8 sw=4 tw=0 :*/
diff --git a/lib/menu.c b/lib/menu.c
new file mode 100644
index 0000000..50311cf
--- /dev/null
+++ b/lib/menu.c
@@ -0,0 +1,527 @@
+/**
+ * @file bemenu.c
+ */
+
+#include "internal.h"
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <assert.h>
+
+static bmItem** (*filterFunc[BM_FILTER_MODE_LAST])(bmMenu *menu, unsigned int *count, unsigned int *selected) = {
+ _bmFilterDmenu,
+ _bmFilterDmenuCaseInsensitive
+};
+
+static void _bmMenuFilter(bmMenu *menu)
+{
+ assert(menu != NULL);
+
+ if (menu->filteredItems)
+ free(menu->filteredItems);
+
+ menu->filteredCount = 0;
+ menu->filteredItems = NULL;
+
+ unsigned int count, selected;
+ bmItem **filtered = filterFunc[menu->filterMode](menu, &count, &selected);
+
+ menu->filteredItems = filtered;
+ menu->filteredCount = count;
+ menu->index = selected;
+}
+
+static int _bmMenuGrowItems(bmMenu *menu)
+{
+ void *tmp;
+ static const unsigned int step = 32;
+ unsigned int nsize = sizeof(bmItem*) * (menu->allocatedCount + step);
+
+ if (!(tmp = realloc(menu->items, nsize))) {
+ if (!(tmp = malloc(nsize)))
+ return 0;
+
+ memcpy(tmp, menu->items, sizeof(bmItem*) * menu->allocatedCount);
+ }
+
+ menu->items = tmp;
+ menu->allocatedCount += step;
+ memset(&menu->items[menu->itemsCount], 0, sizeof(bmItem*) * (menu->allocatedCount - menu->itemsCount));
+ return 1;
+}
+
+/**
+ * Create new bmMenu instance.
+ *
+ * @param drawMode Render method to be used for this menu instance.
+ * @return bmMenu for new menu instance, NULL if creation failed.
+ */
+bmMenu* bmMenuNew(bmDrawMode drawMode)
+{
+ bmMenu *menu = calloc(1, sizeof(bmMenu));
+
+ menu->drawMode = drawMode;
+
+ if (!menu)
+ return NULL;
+
+ int status = 1;
+
+ switch (menu->drawMode) {
+ case BM_DRAW_MODE_CURSES:
+ status = _bmDrawCursesInit(&menu->renderApi);
+ break;
+
+ default:break;
+ }
+
+ if (status == 0) {
+ bmMenuFree(menu);
+ return NULL;
+ }
+
+ return menu;
+}
+
+/**
+ * Release bmMenu instance.
+ *
+ * @param menu bmMenu instance to be freed from memory.
+ */
+void bmMenuFree(bmMenu *menu)
+{
+ assert(menu != NULL);
+
+ if (menu->renderApi.free)
+ menu->renderApi.free();
+
+ if (menu->title)
+ free(menu->title);
+
+ bmMenuFreeItems(menu);
+ free(menu);
+}
+
+/**
+ * Release items inside bmMenu instance.
+ *
+ * @param menu bmMenu instance which items will be freed from memory.
+ */
+void bmMenuFreeItems(bmMenu *menu)
+{
+ assert(menu != NULL);
+
+ unsigned int i;
+ for (i = 0; i < menu->itemsCount; ++i)
+ bmItemFree(menu->items[i]);
+
+ free(menu->items);
+ menu->allocatedCount = menu->itemsCount = 0;
+ menu->items = NULL;
+}
+
+/**
+ * Set active filter mode to bmMenu instance.
+ *
+ * @param menu bmMenu instance where to set filter mode.
+ * @param mode bmFilterMode constant.
+ */
+void bmMenuSetFilterMode(bmMenu *menu, bmFilterMode mode)
+{
+ assert(menu != NULL);
+
+ bmFilterMode oldMode = mode;
+ menu->filterMode = (mode >= BM_FILTER_MODE_LAST ? BM_FILTER_MODE_DMENU : mode);
+
+ if (oldMode != mode)
+ _bmMenuFilter(menu);
+}
+
+/**
+ * Get active filter mode from bmMenu instance.
+ *
+ * @param menu bmMenu instance where to get filter mode.
+ * @return bmFilterMode constant.
+ */
+bmFilterMode bmMenuGetFilterMode(const bmMenu *menu)
+{
+ assert(menu != NULL);
+ return menu->filterMode;
+}
+
+/**
+ * Set title to bmMenu instance.
+ *
+ * @param menu bmMenu instance where to set title.
+ * @param title C "string" to set as title, can be NULL for empty title.
+ */
+int bmMenuSetTitle(bmMenu *menu, const char *title)
+{
+ assert(menu != NULL);
+
+ char *copy = NULL;
+ if (title && !(copy = _bmStrdup(title)))
+ return 0;
+
+ if (menu->title)
+ free(menu->title);
+
+ menu->title = copy;
+ return 1;
+}
+
+/**
+ * Get title from bmMenu instance.
+ *
+ * @param menu bmMenu instance where to get title from.
+ * @return Pointer to null terminated C "string", can be NULL for empty title.
+ */
+const char* bmMenuGetTitle(const bmMenu *menu)
+{
+ assert(menu != NULL);
+ return menu->title;
+}
+
+/**
+ * Add item to bmMenu instance at specific index.
+ *
+ * @param menu bmMenu instance where item will be added.
+ * @param item bmItem instance to add.
+ * @param index Index where item will be added.
+ * @return 1 on successful add, 0 on failure.
+ */
+int bmMenuAddItemAt(bmMenu *menu, bmItem *item, unsigned int index)
+{
+ assert(menu != NULL);
+ assert(item != NULL);
+
+ if (menu->itemsCount >= menu->allocatedCount && !_bmMenuGrowItems(menu))
+ return 0;
+
+ if (index + 1 != menu->itemsCount) {
+ unsigned int i = index;
+ memmove(&menu->items[i + 1], &menu->items[i], sizeof(bmItem*) * (menu->itemsCount - i));
+ }
+
+ menu->items[index] = item;
+ menu->itemsCount++;
+ return 1;
+}
+
+/**
+ * Add item to bmMenu instance.
+ *
+ * @param menu bmMenu instance where item will be added.
+ * @param item bmItem instance to add.
+ * @return 1 on successful add, 0 on failure.
+ */
+int bmMenuAddItem(bmMenu *menu, bmItem *item)
+{
+ return bmMenuAddItemAt(menu, item, menu->itemsCount);
+}
+
+/**
+ * Remove item from bmMenu instance at specific index.
+ *
+ * @param menu bmMenu instance from where item will be removed.
+ * @param index Index of item to remove.
+ * @return 1 on successful add, 0 on failure.
+ */
+int bmMenuRemoveItemAt(bmMenu *menu, unsigned int index)
+{
+ assert(menu != NULL);
+
+ unsigned int i = index;
+ if (i >= menu->itemsCount)
+ return 0;
+
+ memmove(&menu->items[i], &menu->items[i], sizeof(bmItem*) * (menu->itemsCount - i));
+ return 1;
+}
+
+/**
+ * Remove item from bmMenu instance.
+ *
+ * @param menu bmMenu instance from where item will be removed.
+ * @param item bmItem instance to remove.
+ * @return 1 on successful add, 0 on failure.
+ */
+int bmMenuRemoveItem(bmMenu *menu, bmItem *item)
+{
+ assert(menu != NULL);
+ assert(item != NULL);
+
+ unsigned int i;
+ for (i = 0; i < menu->itemsCount && menu->items[i] != item; ++i);
+ return bmMenuRemoveItemAt(menu, i);
+}
+
+/**
+ * Get selected item from bmMenu instance.
+ *
+ * @param menu bmMenu instance from where to get selected item.
+ * @return Selected bmItem instance, NULL if none selected.
+ */
+bmItem* bmMenuGetSelectedItem(const bmMenu *menu)
+{
+ assert(menu != NULL);
+
+ unsigned int count;
+ bmItem **items = bmMenuGetFilteredItems(menu, &count);
+
+ if (!items || count < menu->index)
+ return NULL;
+
+ return items[menu->index];
+}
+
+/**
+ * Get items from bmMenu instance.
+ *
+ * @param menu bmMenu instance from where to get items.
+ * @param nmemb Reference to unsigned int where total count of returned items will be stored.
+ * @return Pointer to array of bmItem pointers.
+ */
+bmItem** bmMenuGetItems(const bmMenu *menu, unsigned int *nmemb)
+{
+ assert(menu != NULL);
+
+ if (nmemb)
+ *nmemb = menu->itemsCount;
+
+ return menu->items;
+}
+
+/**
+ * Get filtered (displayed) items from bmMenu instance.
+ *
+ * @warning The pointer returned by this function _will_ be invalid when menu internally filters its list again.
+ * Do not store this pointer.
+ *
+ * @param menu bmMenu instance from where to get filtered items.
+ * @param nmemb Reference to unsigned int where total count of returned items will be stored.
+ * @return Pointer to array of bmItem pointers.
+ */
+bmItem** bmMenuGetFilteredItems(const bmMenu *menu, unsigned int *nmemb)
+{
+ assert(menu != NULL);
+
+ if (nmemb)
+ *nmemb = (menu->filteredItems ? menu->filteredCount : menu->itemsCount);
+
+ return (menu->filteredItems ? menu->filteredItems : menu->items);
+}
+
+/**
+ * Set items to bmMenu instance.
+ * Will replace all the old items on bmMenu instance.
+ *
+ * @param menu bmMenu instance where items will be set.
+ * @param items Array of bmItem pointers to set.
+ * @param nmemb Total count of items in array.
+ * @return 1 on successful set, 0 on failure.
+ */
+int bmMenuSetItems(bmMenu *menu, const bmItem **items, unsigned int nmemb)
+{
+ assert(menu != NULL);
+
+ if (items == NULL || nmemb == 0) {
+ bmMenuFreeItems(menu);
+ return 1;
+ }
+
+ bmItem **newItems;
+ if (!(newItems = calloc(sizeof(bmItem*), nmemb)))
+ return 0;
+
+ memcpy(newItems, items, sizeof(bmItem*) * nmemb);
+ bmMenuFreeItems(menu);
+
+ menu->items = newItems;
+ menu->allocatedCount = menu->itemsCount = nmemb;
+ return 1;
+}
+
+/**
+ * Create new bmMenu instance.
+ *
+ * @param drawMode Render method to be used for this menu instance.
+ * @return bmMenu for new menu instance, NULL if creation failed.
+ */
+void bmMenuRender(const bmMenu *menu)
+{
+ assert(menu != NULL);
+
+ if (menu->renderApi.render)
+ menu->renderApi.render(menu);
+}
+
+/**
+ * Poll key and unicode from underlying UI toolkit.
+ *
+ * This function will block on CURSES draw mode.
+ *
+ * @param menu bmMenu instance from which to poll.
+ * @param unicode Reference to unsigned int.
+ * @return bmKey for polled key.
+ */
+bmKey bmMenuGetKey(bmMenu *menu, unsigned int *unicode)
+{
+ assert(menu != NULL);
+ assert(unicode != NULL);
+
+ *unicode = 0;
+ bmKey key = BM_KEY_NONE;
+
+ if (menu->renderApi.getKey)
+ key = menu->renderApi.getKey(unicode);
+
+ return key;
+}
+
+/**
+ * Advances menu logic with key and unicode as input.
+ *
+ * @param menu bmMenu instance to be advanced.
+ * @return bmRunResult for menu state.
+ */
+bmRunResult bmMenuRunWithKey(bmMenu *menu, bmKey key, unsigned int unicode)
+{
+ assert(menu != NULL);
+ char *oldFilter = _bmStrdup(menu->filter);
+ unsigned int itemsCount = (menu->filteredItems ? menu->filteredCount : menu->itemsCount);
+
+ switch (key) {
+ case BM_KEY_LEFT:
+ {
+ unsigned int oldCursor = menu->cursor;
+ menu->cursor -= _bmUtf8RunePrev(menu->filter, menu->cursor);
+ menu->cursesCursor -= _bmUtf8RuneWidth(menu->filter + menu->cursor, oldCursor - menu->cursor);
+ }
+ break;
+
+ case BM_KEY_RIGHT:
+ {
+ unsigned int oldCursor = menu->cursor;
+ menu->cursor += _bmUtf8RuneNext(menu->filter, menu->cursor);
+ menu->cursesCursor += _bmUtf8RuneWidth(menu->filter + oldCursor, menu->cursor - oldCursor);
+ }
+ break;
+
+ case BM_KEY_HOME:
+ menu->cursesCursor = menu->cursor = 0;
+ break;
+
+ case BM_KEY_END:
+ menu->cursor = strlen(menu->filter);
+ menu->cursesCursor = _bmUtf8StringScreenWidth(menu->filter);
+ break;
+
+ case BM_KEY_UP:
+ if (menu->index > 0)
+ menu->index--;
+ break;
+
+ case BM_KEY_DOWN:
+ if (menu->index < itemsCount - 1)
+ menu->index++;
+ break;
+
+ case BM_KEY_PAGE_UP:
+ menu->index = 0;
+ break;
+
+ case BM_KEY_PAGE_DOWN:
+ menu->index = itemsCount - 1;
+ break;
+
+ case BM_KEY_BACKSPACE:
+ {
+ size_t width;
+ menu->cursor -= _bmUtf8RuneRemove(menu->filter, menu->cursor, &width);
+ menu->cursesCursor -= width;
+ }
+ break;
+
+ case BM_KEY_DELETE:
+ _bmUtf8RuneRemove(menu->filter, menu->cursor + 1, NULL);
+ break;
+
+ case BM_KEY_LINE_DELETE_LEFT:
+ {
+ while (menu->cursor > 0) {
+ size_t width;
+ menu->cursor -= _bmUtf8RuneRemove(menu->filter, menu->cursor, &width);
+ menu->cursesCursor -= width;
+ }
+ }
+ break;
+
+ case BM_KEY_LINE_DELETE_RIGHT:
+ menu->filter[menu->cursor] = 0;
+ break;
+
+ case BM_KEY_WORD_DELETE:
+ {
+ while (menu->cursor < strlen(menu->filter) && !isspace(menu->filter[menu->cursor])) {
+ unsigned int oldCursor = menu->cursor;
+ menu->cursor += _bmUtf8RuneNext(menu->filter, menu->cursor);
+ menu->cursesCursor += _bmUtf8RuneWidth(menu->filter + oldCursor, menu->cursor - oldCursor);
+ }
+ while (menu->cursor > 0 && isspace(menu->filter[menu->cursor - 1])) {
+ unsigned int oldCursor = menu->cursor;
+ menu->cursor -= _bmUtf8RunePrev(menu->filter, menu->cursor);
+ menu->cursesCursor -= _bmUtf8RuneWidth(menu->filter + menu->cursor, oldCursor - menu->cursor);
+ }
+ while (menu->cursor > 0 && !isspace(menu->filter[menu->cursor - 1])) {
+ size_t width;
+ menu->cursor -= _bmUtf8RuneRemove(menu->filter, menu->cursor, &width);
+ menu->cursesCursor -= width;
+ }
+ }
+ break;
+
+ case BM_KEY_UNICODE:
+ {
+ size_t width;
+ menu->cursor += _bmUnicodeInsert(menu->filter, sizeof(menu->filter) - 1, menu->cursor, unicode, &width);
+ menu->cursesCursor += width;
+ }
+ break;
+
+ case BM_KEY_TAB:
+ {
+ bmItem *selected = bmMenuGetSelectedItem(menu);
+ if (selected && bmItemGetText(selected)) {
+ const char *text = bmItemGetText(selected);
+ size_t len = strlen(text);
+
+ if (len > sizeof(menu->filter) - 1)
+ len = sizeof(menu->filter) - 1;
+
+ memset(menu->filter, 0, strlen(menu->filter));
+ memcpy(menu->filter, text, len);
+ menu->cursor = strlen(menu->filter);
+ menu->cursesCursor = _bmUtf8StringScreenWidth(menu->filter);
+ }
+ }
+ break;
+
+ case BM_KEY_RETURN:
+ return BM_RUN_RESULT_SELECTED;
+
+ case BM_KEY_ESCAPE:
+ return BM_RUN_RESULT_CANCEL;
+
+ default: break;
+ }
+
+ if (oldFilter && strcmp(oldFilter, menu->filter))
+ _bmMenuFilter(menu);
+
+ if (oldFilter)
+ free(oldFilter);
+ return BM_RUN_RESULT_RUNNING;
+}
+
+/* vim: set ts=8 sw=4 tw=0 :*/
diff --git a/lib/util.c b/lib/util.c
new file mode 100644
index 0000000..8c9bc0f
--- /dev/null
+++ b/lib/util.c
@@ -0,0 +1,226 @@
+/**
+ * @file util.c
+ */
+
+#include "internal.h"
+#define _XOPEN_SOURCE 700
+#include <wchar.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <assert.h>
+
+/**
+ * Portable strdup.
+ *
+ * @param string C "string" to copy.
+ * @return Copy of the given C "string".
+ */
+char* _bmStrdup(const char *string)
+{
+ size_t len = strlen(string);
+ if (len == 0)
+ return NULL;
+
+ void *copy = calloc(1, len + 1);
+ if (copy == NULL)
+ return NULL;
+
+ return (char *)memcpy(copy, string, len);
+}
+
+/**
+ * Shrink bmItem** list pointer.
+ *
+ * Useful helper function for filter functions.
+ *
+ * @param list Pointer to pointer to list of bmItem pointers.
+ * @param osize Current size of the list.
+ * @param nsize New size the list will be shrinked to.
+ * @return Pointer to list of bmItem pointers.
+ */
+bmItem** _bmShrinkItemList(bmItem ***list, size_t osize, size_t nsize)
+{
+ if (nsize >= osize)
+ return *list;
+
+ void *tmp = malloc(sizeof(bmItem*) * nsize);
+ if (!tmp)
+ return *list;
+
+ memcpy(tmp, *list, sizeof(bmItem*) * nsize);
+ free(*list);
+ *list = tmp;
+ return *list;
+}
+
+/**
+ * Determite columns needed to display UTF8 string.
+ *
+ * @param string C "string" to determite.
+ * @return Number of columns, or -1 on failure.
+ */
+int _bmUtf8StringScreenWidth(const char *string)
+{
+ if (!string)
+ return 0;
+
+ int num_char = mbstowcs(NULL, string, 0) + 1;
+ wchar_t *wstring = malloc((num_char + 1) * sizeof (wstring[0]));
+
+ if (mbstowcs(wstring, string, num_char) == (size_t)(-1)) {
+ free(wstring);
+ return strlen(string);
+ }
+
+ int length = wcswidth(wstring, num_char);
+ free(wstring);
+ return length;
+}
+
+/**
+ * Figure out how many bytes to shift to next UTF8 rune.
+ *
+ * @param string C "string" which contains the runes.
+ * @param start Offset where to figure out next rune. (cursor)
+ * @return Number of bytes to next UTF8 rune.
+ */
+size_t _bmUtf8RuneNext(const char *string, size_t start)
+{
+ assert(string != NULL);
+
+ size_t len = strlen(string), i = start;
+ if (len == 0 || len <= i || !*string)
+ return 0;
+
+ while (++i < len && (string[i] & 0xc0) == 0x80);
+ return i - start;
+}
+
+/**
+ * Figure out how many bytes to shift to previous UTF8 rune.
+ *
+ * @param string C "string" which contains the runes.
+ * @param start Offset where to figure out previous rune. (cursor)
+ * @return Number of bytes to previous UTF8 rune.
+ */
+size_t _bmUtf8RunePrev(const char *string, size_t start)
+{
+ assert(string != NULL);
+
+ size_t len = strlen(string), i = start;
+ if (i == 0 || len < start || !*string)
+ return 0;
+
+ while (--i > 0 && (string[i] & 0xc0) == 0x80);
+ return start - i;
+}
+
+/**
+ * Figure out how many columns are needed to display UTF8 rune.
+ *
+ * @param rune Buffer which contains the rune.
+ * @param u8len Byte length of the rune.
+ * @return Number of columns, or -1 on failure.
+ */
+size_t _bmUtf8RuneWidth(const char *rune, unsigned int u8len)
+{
+ assert(rune != NULL);
+ char mb[5] = { 0, 0, 0, 0, 0 };
+ memcpy(mb, rune, (u8len > 4 ? 4 : u8len));
+ return _bmUtf8StringScreenWidth(mb);
+}
+
+/**
+ * Remove previous UTF8 rune from buffer.
+ *
+ * @param string Null terminated C "string".
+ * @param start Start offset where to delete from. (cursor)
+ * @param runeWidth Reference to size_t, return number of columns for removed rune, or -1 on failure.
+ * @return Number of bytes removed from buffer.
+ */
+size_t _bmUtf8RuneRemove(char *string, size_t start, size_t *runeWidth)
+{
+ assert(string != NULL);
+
+ if (runeWidth)
+ *runeWidth = 0;
+
+ size_t len = strlen(string), oldStart = start;
+ if (len == 0 || len < start || !*string)
+ return 0;
+
+ start -= _bmUtf8RunePrev(string, start);
+
+ if (runeWidth)
+ *runeWidth = _bmUtf8RuneWidth(string + start, oldStart - start);
+
+ memmove(string + start, string + oldStart, len - oldStart);
+ string[len - (oldStart - start)] = 0;
+ return (oldStart - start);
+}
+
+/**
+ * Insert UTF8 rune to buffer.
+ *
+ * @param string Null terminated C "string".
+ * @param bufSize Size of the buffer.
+ * @param start Start offset where to insert to. (cursor)
+ * @param rune Buffer to insert to string.
+ * @param u8len Byte length of the rune.
+ * @param runeWidth Reference to size_t, return number of columns for inserted rune, or -1 on failure.
+ * @return Number of bytes inserted to buffer.
+ */
+size_t _bmUtf8RuneInsert(char *string, size_t bufSize, size_t start, const char *rune, unsigned int u8len, size_t *runeWidth)
+{
+ assert(string != NULL);
+
+ if (runeWidth)
+ *runeWidth = 0;
+
+ size_t len = strlen(string);
+ if (len + u8len >= bufSize)
+ return 0;
+
+ if (u8len == 1 && iscntrl(*rune))
+ return 0;
+
+ char *str = string + start;
+ memmove(str + u8len, str, len - start);
+ memcpy(str, rune, u8len);
+
+ if (runeWidth)
+ *runeWidth = _bmUtf8RuneWidth(rune, u8len);
+ return u8len;
+}
+
+/**
+ * Insert unicode character to UTF8 buffer.
+ *
+ * @param string Null terminated C "string".
+ * @param bufSize Size of the buffer.
+ * @param start Start offset where to insert to. (cursor)
+ * @param unicode Unicode character to insert.
+ * @param runeWidth Reference to size_t, return number of columns for inserted rune, or -1 on failure.
+ * @return Number of bytes inserted to buffer.
+ */
+size_t _bmUnicodeInsert(char *string, size_t bufSize, size_t start, unsigned int unicode, size_t *runeWidth)
+{
+ assert(string != NULL);
+
+ char u8len = ((unicode < 0x80) ? 1 : ((unicode < 0x800) ? 2 : ((unicode < 0x10000) ? 3 : 4)));
+ char mb[5] = { 0, 0, 0, 0 };
+
+ if (u8len == 1) {
+ mb[0] = unicode;
+ } else {
+ size_t i, j;
+ for (i = j = u8len; j > 1; --j) mb[j - 1] = 0x80 | (0x3F & (unicode >> ((i - j) * 6)));
+ mb[0] = (~0) << (8 - i);
+ mb[0] |= (unicode >> (i * 6 - 6));
+ }
+
+ return _bmUtf8RuneInsert(string, bufSize, start, mb, u8len, runeWidth);
+}
+
+/* vim: set ts=8 sw=4 tw=0 :*/