diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/CMakeLists.txt | 7 | ||||
| -rw-r--r-- | lib/bemenu.c | 60 | ||||
| -rw-r--r-- | lib/bemenu.h | 227 | ||||
| -rw-r--r-- | lib/draw/curses.c | 258 | ||||
| -rw-r--r-- | lib/filter.c | 64 | ||||
| -rw-r--r-- | lib/internal.h | 39 | ||||
| -rw-r--r-- | lib/item.c | 75 | ||||
| -rw-r--r-- | lib/menu.c | 527 | ||||
| -rw-r--r-- | lib/util.c | 226 | 
9 files changed, 1412 insertions, 71 deletions
| 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 :*/ | 
