diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/CMakeLists.txt | 42 | ||||
-rw-r--r-- | lib/bemenu.c | 9 | ||||
-rw-r--r-- | lib/bemenu.h | 484 | ||||
-rw-r--r-- | lib/draw/curses.c | 497 | ||||
-rw-r--r-- | lib/filter.c | 188 | ||||
-rw-r--r-- | lib/internal.h | 188 | ||||
-rw-r--r-- | lib/item.c | 96 | ||||
-rw-r--r-- | lib/library.c | 15 | ||||
-rw-r--r-- | lib/list.c | 139 | ||||
-rw-r--r-- | lib/menu.c | 727 | ||||
-rw-r--r-- | lib/util.c | 314 | ||||
-rw-r--r-- | lib/version.h.in | 3 |
12 files changed, 2680 insertions, 22 deletions
diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 46c532b..a52bc7b 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -1,13 +1,16 @@ -SET(CMAKE_LIBRARY_OUTPUT_DIRECTORY lib) -SET(CMAKE_ARCHIVE_OUTPUT_DIRECTORY lib) - # Sources SET(BEMENU_SOURCE - bemenu.c + menu.c + item.c + list.c + util.c + filter.c + library.c draw/curses.c ) -SET(BEMENU_INCLUDE) -SET(BEMENU_LIBRARIES) + +# Configure +CONFIGURE_FILE(version.h.in version.h @ONLY) # Warnings IF (CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX) @@ -22,9 +25,30 @@ IF (UNIX AND CMAKE_COMPILER_IS_GNUCC) ENDIF () ENDIF () +# Parse soversion version +STRING(REGEX MATCHALL "[0-9]+" VERSION_COMPONENTS ${BEMENU_VERSION}) +LIST(GET VERSION_COMPONENTS 0 SOVERSION) + # Compile -INCLUDE_DIRECTORIES(${BEMENU_INCLUDE}) -ADD_LIBRARY(bemenu ${BEMENU_SOURCE}) -TARGET_LINK_LIBRARIES(bemenu ${BEMENU_LIBRARIES}) +INCLUDE_DIRECTORIES(${BEMENU_INCLUDE} ${CMAKE_CURRENT_BINARY_DIR}) +ADD_LIBRARY(bemenu SHARED ${BEMENU_SOURCE}) +SET_TARGET_PROPERTIES(bemenu PROPERTIES + VERSION ${BEMENU_VERSION} + SOVERSION ${SOVERSION}) +TARGET_LINK_LIBRARIES(bemenu dl) + +# Install +INSTALL(TARGETS bemenu DESTINATION lib) +INSTALL(FILES bemenu.h DESTINATION include) + +# Unexport +SET(BEMENU_INCLUDES) +SET(BEMENU_INCLUDE_DIRS) +SET(BEMENU_LIBRARIES) + +# Export +SET(BEMENU_INCLUDES "bemenu.h" CACHE STRING "bemenu includes exported from CMake") +SET(BEMENU_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR} CACHE STRING "bemenu public header include path exported from CMake") +SET(BEMENU_LIBRARIES "bemenu" "dl" CACHE STRING "bemenu libraries exported from CMake") # vim: set ts=8 sw=4 tw=0 : diff --git a/lib/bemenu.c b/lib/bemenu.c deleted file mode 100644 index 2f92f27..0000000 --- a/lib/bemenu.c +++ /dev/null @@ -1,9 +0,0 @@ -/** - * @file bemenu.c - */ - -/* - * code goes here - */ - -/* vim: set ts=8 sw=4 tw=0 :*/ diff --git a/lib/bemenu.h b/lib/bemenu.h new file mode 100644 index 0000000..5457cca --- /dev/null +++ b/lib/bemenu.h @@ -0,0 +1,484 @@ +/** + * @file bemenu.h + * + * Public API header. + */ + +typedef struct _bmMenu bmMenu; +typedef struct _bmItem bmItem; + +/** + * @defgroup Library + * @brief Library functions. + * + * Query library version, etc... + */ + +/** + * @defgroup Menu + * @brief Menu container. + * + * Holds all the items, runs logic and gets rendered. + */ + +/** + * @defgroup Item + * @brief Item container. + * + * Contains properties for visual representation of item. + */ + +/** + * @addtogroup Library + * @{ */ + +/** + * @name Library Version + * @{ */ + +/** + * Get version of the library in 'major.minor.patch' format. + * + * @see @link http://semver.org/ Semantic Versioning @endlink + * + * @return Null terminated C "string" to version string. + */ +const char* bmVersion(void); + +/** @} Library Version */ + +/** @} Library */ + +/** + * @addtogroup Menu + * @{ */ + +/** + * Draw mode constants for bmMenu instance draw mode. + * + * @link ::bmDrawMode BM_DRAW_MODE_LAST @endlink 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. + * + * @link ::bmFilterMode BM_FILTER_MODE_LAST @endlink 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. + * + * - @link ::bmRunResult BM_RUN_RESULT_RUNNING @endlink means that menu is running and thus should be still renderer && ran. + * - @link ::bmRunResult BM_RUN_RESULT_SELECTED @endlink means that menu was closed and items were selected. + * - @link ::bmRunResult BM_RUN_RESULT_CANCEL @endlink 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. + * + * @link ::bmKey BM_KEY_LAST @endlink 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_SHIFT_PAGE_UP, + BM_KEY_SHIFT_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_SHIFT_RETURN, + BM_KEY_CONTROL_RETURN, + BM_KEY_UNICODE, + BM_KEY_LAST +} bmKey; + +/** + * @name Menu Memory + * @{ */ + +/** + * 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); + +/** + * Release bmMenu instance. + * + * @param menu bmMenu instance to be freed from memory. + */ +void bmMenuFree(bmMenu *menu); + +/** + * Release items inside bmMenu instance. + * + * @param menu bmMenu instance which items will be freed from memory. + */ +void bmMenuFreeItems(bmMenu *menu); + +/** @} Menu Memory */ + +/** + * @name Menu Properties + * @{ */ + +/** + * Set userdata pointer to bmMenu instance. + * Userdata will be carried unmodified by the instance. + * + * @param menu bmMenu instance where to set userdata pointer. + * @param userdata Pointer to userdata. + */ +void bmMenuSetUserdata(bmMenu *menu, void *userdata); + +/** + * Get userdata pointer from bmMenu instance. + * + * @param menu bmMenu instance which userdata pointer to get. + * @return Pointer for unmodified userdata. + */ +void* bmMenuGetUserdata(bmMenu *menu); + +/** + * Set filter text to bmMenu instance. + * + * @param menu bmMenu instance where to set filter. + * @param filter Null terminated C "string" to act as filter. + */ +void bmMenuSetFilter(bmMenu *menu, const char *filter); + +/** + * Get filter text from bmMenu instance. + * + * @param menu bmMenu instance where to get filter. + * @return Const pointer to current filter text, may be **NULL** if empty. + */ +const char* bmMenuGetFilter(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 selection wrapping on/off. + * + * @param menu bmMenu instance where to toggle selection wrapping. + * @param int 1 == on, 0 == off. + */ +void bmMenuSetWrap(bmMenu *menu, int wrap); + +/** + * Get selection wrapping state. + * + * @param menu bmMenu instance where to get selection wrapping state. + * @return int for wrap state. + */ +int bmMenuGetWrap(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); + +/** @} Properties */ + +/** + * @name Menu Items + * @{ */ + +/** + * 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. + * + * @warning The item won't be freed, use bmItemFree to do that. + * + * @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. + * + * @warning 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); + +/** + * Highlight item in menu by index. + * + * @param menu bmMenu instance from where to highlight item. + * @param index Index of item to highlight. + * @return 1 on successful highlight, 0 on failure. + */ +int bmMenuSetHighlightedIndex(bmMenu *menu, unsigned int index); + +/** + * Highlight item in menu. + * + * @param menu bmMenu instance from where to highlight item. + * @param item bmItem instance to highlight. + * @return 1 on successful highlight, 0 on failure. + */ +int bmMenuSetHighlighted(bmMenu *menu, bmItem *item); + +/** + * Get highlighted item from bmMenu instance. + * + * @warning The pointer returned by this function may be invalid after items change. + * + * @param menu bmMenu instance from where to get highlighted item. + * @return Selected bmItem instance, **NULL** if none highlighted. + */ +bmItem* bmMenuGetHighlightedItem(const bmMenu *menu); + +/** + * Set selected items to 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 bmMenuSetSelectedItems(bmMenu *menu, bmItem **items, unsigned int nmemb); + +/** + * Get selected items from bmMenu instance. + * + * @warning The pointer returned by this function may be invalid after selection or items change. + * + * @param menu bmMenu instance from where to get selected items. + * @param outNmemb Reference to unsigned int where total count of returned items will be stored. + * @return Pointer to array of bmItem pointers. + */ +bmItem** bmMenuGetSelectedItems(const bmMenu *menu, unsigned int *outNmemb); + +/** + * 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); + +/** + * Get items from bmMenu instance. + * + * @warning The pointer returned by this function may be invalid after removing or adding new items. + * + * @param menu bmMenu instance from where to get items. + * @param outNmemb 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 *outNmemb); + +/** + * 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 outNmemb 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 *outNmemb); + +/** @} Menu Items */ + +/** + * @name Menu Logic + * @{ */ + +/** + * Render bmMenu instance using chosen draw method. + * + * @param menu bmMenu instance to be rendered. + */ +void bmMenuRender(const bmMenu *menu); + +/** + * Trigger filtering of menu manually. + * This is useful when adding new items and want to dynamically see them filtered. + * + * Do note that filtering might be heavy, so you should only call it after batch manipulation of items. + * Not after manipulation of each single item. + * + * @param menu bmMenu instance which to filter. + */ +void bmMenuFilter(bmMenu *menu); + +/** + * Poll key and unicode from underlying UI toolkit. + * + * This function will block on @link ::bmDrawMode BM_DRAW_MODE_CURSES @endlink draw mode. + * + * @param menu bmMenu instance from which to poll. + * @param outUnicode Reference to unsigned int. + * @return bmKey for polled key. + */ +bmKey bmMenuGetKey(bmMenu *menu, unsigned int *outUnicode); + +/** + * Advances menu logic with key and unicode as input. + * + * @param menu bmMenu instance to be advanced. + * @param key Key input that will advance menu logic. + * @param unicode Unicode input that will advance menu logic. + * @return bmRunResult for menu state. + */ +bmRunResult bmMenuRunWithKey(bmMenu *menu, bmKey key, unsigned int unicode); + +/** @} Menu Logic */ + +/** @} Menu */ + +/** + * @addtogroup Item + * @{ */ + +/** + * @name Item Memory + * @{ */ + +/** + * 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); + +/** @} Item Memory */ + +/** + * @name Item Properties + * @{ */ + +/** + * Set userdata pointer to bmItem instance. + * Userdata will be carried unmodified by the instance. + * + * @param item bmItem instance where to set userdata pointer. + * @param userdata Pointer to userdata. + */ +void bmItemSetUserdata(bmItem *item, void *userdata); + +/** + * Get userdata pointer from bmItem instance. + * + * @param item bmItem instance which userdata pointer to get. + * @return Pointer for unmodified userdata. + */ +void* bmItemGetUserdata(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); + +/** @} Item Properties */ + +/** @} Item */ + +/* vim: set ts=8 sw=4 tw=0 :*/ diff --git a/lib/draw/curses.c b/lib/draw/curses.c index c873496..a1f23ef 100644 --- a/lib/draw/curses.c +++ b/lib/draw/curses.c @@ -1,9 +1,498 @@ +#include "../internal.h" + +#if __APPLE__ +# define _C99_SOURCE +# include <stdio.h> /* vsnprintf */ +# undef _C99_SOURCE +#endif + +#define _XOPEN_SOURCE 500 +#include <signal.h> /* sigaction */ +#include <stdarg.h> /* vsnprintf */ +#undef _XOPEN_SOURCE + +#include <wchar.h> +#include <unistd.h> +#include <string.h> +#include <stdlib.h> +#include <locale.h> +#include <ncurses.h> +#include <dlfcn.h> +#include <assert.h> + +#if _WIN32 +static const char *TTY = "CON"; +#else +static const char *TTY = "/dev/tty"; +#endif + +#if __APPLE__ + const char *DL_PATH[] = { + "libncursesw.5.dylib", + "libncurses.5.dylib", + NULL + }; +#elif _WIN32 +# error FIXME: Compile with windows... or use relative path? +#else + const char *DL_PATH[] = { + "libncursesw.so.5", + "libncurses.so.5", + NULL + }; +#endif + +/* these are implemented as macros in older curses */ +#ifndef NCURSES_OPAQUE +static int wrap_getmaxx(WINDOW *win) { return getmaxx(win); } +static int wrap_getmaxy(WINDOW *win) { return getmaxy(win); } +#endif + +/* ncurses.h likes to define stuff for us. + * This unforunately mangles with our struct. */ +#undef erase +#undef getch +#undef get_wch +#undef refresh +#undef mvprintw +#undef move +#undef init_pair +#undef attroff +#undef attron +#undef getmaxx +#undef getmaxy + /** - * @file curses.c + * Dynamically loaded curses API. */ +static struct curses { + struct sigaction abrtAction; + struct sigaction segvAction; + struct sigaction winchAction; + void *handle; + WINDOW *stdscr; + WINDOW* (*initscr)(void); + int (*endwin)(void); + int (*refresh)(void); + int (*erase)(void); + int (*getch)(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 (*use_default_colors)(void); + int (*getmaxx)(WINDOW *win); + int (*getmaxy)(WINDOW *win); + int (*keypad)(WINDOW *win, bool bf); + int (*curs_set)(int visibility); + int (*flushinp)(void); + int (*noecho)(void); + int (*raw)(void); + int *ESCDELAY; + int oldStdin; + int oldStdout; +} curses; -/* - * code goes here - */ +static int _bmDrawCursesResizeBuffer(char **buffer, size_t *osize, size_t nsize) +{ + assert(buffer); + assert(osize); + + if (nsize == 0 || nsize <= *osize) + return 0; + + void *tmp; + if (!*buffer || !(tmp = realloc(*buffer, nsize))) { + if (!(tmp = malloc(nsize))) + return 0; + + if (*buffer) { + memcpy(tmp, *buffer, *osize); + free(*buffer); + } + } + + *buffer = tmp; + *osize = nsize; + return 1; +} + +#if __GNUC__ +__attribute__((format(printf, 3, 4))) +#endif +static void _bmDrawCursesDrawLine(int pair, int y, const char *format, ...) +{ + static size_t blen = 0; + static char *buffer = NULL; + + size_t ncols = curses.getmaxx(curses.stdscr); + if (ncols <= 0) + return; + + va_list args; + va_start(args, format); + size_t nlen = vsnprintf(NULL, 0, format, args); + va_end(args); + + if ((!buffer || nlen > blen) && !_bmDrawCursesResizeBuffer(&buffer, &blen, nlen + 1)) + return; + + va_start(args, format); + vsnprintf(buffer, blen - 1, format, args); + va_end(args); + + size_t dw = 0, i = 0; + while (dw < ncols && i < nlen) { + if (buffer[i] == '\t') buffer[i] = ' '; + int next = _bmUtf8RuneNext(buffer, i); + dw += _bmUtf8RuneWidth(buffer + i, next); + i += (next ? next : 1); + } + + if (dw < ncols) { + /* line is too short, widen it */ + size_t offset = i + (ncols - dw); + if (blen <= offset && !_bmDrawCursesResizeBuffer(&buffer, &blen, offset + 1)) + return; + + memset(buffer + nlen, ' ', offset - nlen); + buffer[offset] = 0; + } else if (i < blen) { + /* line is too long, shorten it */ + i -= _bmUtf8RunePrev(buffer, i - (dw - ncols)) - 1; + size_t cc = dw - (dw - ncols); + + size_t offset = i - (dw - ncols) + (ncols - cc) + 1; + if (blen <= offset) { + int diff = offset - blen + 1; + if (!_bmDrawCursesResizeBuffer(&buffer, &blen, blen + diff)) + return; + } + + memset(buffer + i - (dw - ncols), ' ', (ncols - cc) + 1); + buffer[offset] = 0; + } + + if (pair > 0) + curses.attron(COLOR_PAIR(pair)); + + curses.mvprintw(y, 0, "%s", buffer); + + if (pair > 0) + curses.attroff(COLOR_PAIR(pair)); +} + +static void _bmDrawCursesRender(const bmMenu *menu) +{ + if (!curses.stdscr) { + curses.oldStdin = dup(STDIN_FILENO); + curses.oldStdout = dup(STDOUT_FILENO); + + freopen(TTY, "w", stdout); + freopen(TTY, "r", stdin); + + setlocale(LC_CTYPE, ""); + + if ((curses.stdscr = curses.initscr()) == NULL) + return; + + *curses.ESCDELAY = 25; + curses.flushinp(); + curses.keypad(curses.stdscr, true); + curses.curs_set(1); + curses.noecho(); + curses.raw(); + + curses.start_color(); + curses.use_default_colors(); + curses.init_pair(1, COLOR_BLACK, COLOR_RED); + curses.init_pair(2, COLOR_RED, -1); + } + + const unsigned int lines = curses.getmaxy(curses.stdscr); + curses.erase(); + + unsigned int ncols = curses.getmaxx(curses.stdscr); + unsigned int titleLen = (menu->title ? strlen(menu->title) + 1 : 0); + + if (titleLen >= ncols) + titleLen = 0; + + unsigned int ccols = ncols - titleLen - 1; + unsigned int dcols = 0, doffset = menu->cursor; + + while (doffset > 0 && dcols < ccols) { + int prev = _bmUtf8RunePrev(menu->filter, doffset); + dcols += _bmUtf8RuneWidth(menu->filter + doffset - prev, prev); + doffset -= (prev ? prev : 1); + } + + _bmDrawCursesDrawLine(0, 0, "%*s%s", titleLen, "", (menu->filter ? menu->filter + doffset : "")); + + if (menu->title && titleLen > 0) { + 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 highlighted = (items[i] == bmMenuGetHighlightedItem(menu)); + int color = (highlighted ? 2 : (_bmMenuItemIsSelected(menu, items[i]) ? 1 : 0)); + _bmDrawCursesDrawLine(color, cl++, "%s%s", (highlighted ? ">> " : " "), (items[i]->text ? items[i]->text : "")); + } + + curses.move(0, titleLen + (menu->cursesCursor < ccols ? menu->cursesCursor : ccols)); + curses.refresh(); +} + +static unsigned int _bmDrawCursesDisplayedCount(const bmMenu *menu) +{ + (void)menu; + return (curses.stdscr ? curses.getmaxy(curses.stdscr) : 0); +} + +static void _bmDrawCursesEndWin(void) +{ + if (!curses.stdscr) + return; + + freopen(TTY, "w", stdout); + + if (curses.refresh) + curses.refresh(); + + if (curses.endwin) + curses.endwin(); + + dup2(curses.oldStdin, STDIN_FILENO); + dup2(curses.oldStdout, STDOUT_FILENO); + close(curses.oldStdin); + close(curses.oldStdout); + + curses.stdscr = NULL; +} + +static bmKey _bmDrawCursesGetKey(unsigned int *unicode) +{ + assert(unicode); + *unicode = 0; + + if (!curses.stdscr) + return BM_KEY_NONE; + + if (curses.get_wch) + curses.get_wch((wint_t*)unicode); + else if (curses.getch) + *unicode = curses.getch(); + + switch (*unicode) { +#if KEY_RESIZE + case KEY_RESIZE: + return BM_KEY_NONE; +#endif + + 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 391: /* S-Home */ + case KEY_HOME: + return BM_KEY_HOME; + + case 5: /* C-e */ + case 386: /* S-End */ + case KEY_END: + return BM_KEY_END; + + case KEY_PPAGE: /* Page up */ + return BM_KEY_PAGE_UP; + + case KEY_NPAGE: /* Page down */ + return BM_KEY_PAGE_DOWN; + + case 550: /* C-Page up */ + case 398: /* S-Page up */ + return BM_KEY_SHIFT_PAGE_UP; + + case 545: /* C-Page down */ + case 396: /* S-Page down */ + return BM_KEY_SHIFT_PAGE_DOWN; + + case 8: /* C-h */ + case 127: /* Delete */ + case KEY_BACKSPACE: + return BM_KEY_BACKSPACE; + + case 4: /* C-d */ + case KEY_DC: + return BM_KEY_DELETE; + + case 383: /* S-Del */ + case 21: /* C-u */ + return BM_KEY_LINE_DELETE_LEFT; + + case 11: /* C-k */ + return BM_KEY_LINE_DELETE_RIGHT; + + case 23: /* C-w */ + return BM_KEY_WORD_DELETE; + + case 9: /* Tab */ + return BM_KEY_TAB; + + case 18: /* C-r */ + return BM_KEY_CONTROL_RETURN; + + case 20: /* C-t */ + case 331: /* Insert */ + _bmDrawCursesEndWin(); + return BM_KEY_SHIFT_RETURN; + + 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); + + sigaction(SIGABRT, &curses.abrtAction, NULL); + sigaction(SIGSEGV, &curses.segvAction, NULL); + sigaction(SIGWINCH, &curses.winchAction, NULL); + memset(&curses, 0, sizeof(curses)); +} + +static void _bmDrawCursesCrashHandler(int sig) +{ + (void)sig; + _bmDrawCursesFree(); +} + +static void _bmDrawCursesResizeHandler(int sig) +{ + (void)sig; + if (!curses.stdscr) + return; + + curses.endwin(); + curses.refresh(); +} + +int _bmDrawCursesInit(struct _bmRenderApi *api) +{ + memset(&curses, 0, sizeof(curses)); + const char *lib = NULL, *func = NULL; + + unsigned int i; + for (i = 0; DL_PATH[i] && !curses.handle; ++i) + curses.handle = dlopen((lib = DL_PATH[i]), RTLD_LAZY); + + if (!curses.handle) + return 0; + +#define bmLoadFunction(x) (curses.x = dlsym(curses.handle, (func = #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) && !bmLoadFunction(getch)) + 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(use_default_colors)) + goto function_pointer_exception; + if (!bmLoadFunction(keypad)) + goto function_pointer_exception; + if (!bmLoadFunction(curs_set)) + goto function_pointer_exception; + if (!bmLoadFunction(flushinp)) + goto function_pointer_exception; + if (!bmLoadFunction(noecho)) + goto function_pointer_exception; + if (!bmLoadFunction(raw)) + goto function_pointer_exception; + if (!bmLoadFunction(ESCDELAY)) + goto function_pointer_exception; + +#ifndef NCURSES_OPAQUE + curses.getmaxx = wrap_getmaxx; + curses.getmaxy = wrap_getmaxy; +#else + if (!bmLoadFunction(getmaxx)) + goto function_pointer_exception; + if (!bmLoadFunction(getmaxy)) + goto function_pointer_exception; +#endif + +#undef bmLoadFunction + + api->displayedCount = _bmDrawCursesDisplayedCount; + api->getKey = _bmDrawCursesGetKey; + api->render = _bmDrawCursesRender; + api->free = _bmDrawCursesFree; + + struct sigaction action; + memset(&action, 0, sizeof(struct sigaction)); + action.sa_handler = _bmDrawCursesCrashHandler; + sigaction(SIGABRT, &action, &curses.abrtAction); + sigaction(SIGSEGV, &action, &curses.segvAction); + + action.sa_handler = _bmDrawCursesResizeHandler; + sigaction(SIGWINCH, &action, &curses.winchAction); + return 1; + +function_pointer_exception: + fprintf(stderr, "-!- Could not load function '%s' from '%s'\n", func, lib); + _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..04733c5 --- /dev/null +++ b/lib/filter.c @@ -0,0 +1,188 @@ +#include "internal.h" +#include <stdlib.h> +#include <assert.h> +#include <string.h> + +/** + * 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. + */ +static bmItem** _bmFilterShrinkList(bmItem ***inOutList, size_t osize, size_t nsize) +{ + assert(inOutList); + + if (nsize == 0) { + free(*inOutList); + return (*inOutList = NULL); + } + + if (nsize >= osize) + return *inOutList; + + void *tmp = malloc(sizeof(bmItem*) * nsize); + if (!tmp) + return *inOutList; + + memcpy(tmp, *inOutList, sizeof(bmItem*) * nsize); + free(*inOutList); + return (*inOutList = tmp); +} + +/** + * Text filter tokenizer helper. + * + * @param menu bmMenu instance which filter to tokenize. + * @param outTokv char pointer reference to list of tokens, this should be freed after use. + * @param outTokc unsigned int reference to number of tokens. + * @return Pointer to buffer that contains tokenized string, this should be freed after use. + */ +static char* _bmFilterTokenize(bmMenu *menu, char ***outTokv, unsigned int *outTokc) +{ + assert(menu); + assert(outTokv); + assert(outTokc); + *outTokv = NULL; + *outTokc = 0; + + char **tokv = NULL, *buffer = NULL; + if (!(buffer = _bmStrdup(menu->filter))) + goto fail; + + char *s; + for (s = buffer; *s && *s == ' '; ++s); + + char **tmp = NULL; + size_t pos = 0, next; + unsigned int tokc = 0, tokn = 0; + for (; (pos = _bmStripToken(s, " ", &next)) > 0; tokv = tmp) { + if (++tokc > tokn && !(tmp = realloc(tokv, ++tokn * sizeof(char*)))) + goto fail; + + tmp[tokc - 1] = s; + s += next; + } + + *outTokv = tmp; + *outTokc = tokc; + return buffer; + +fail: + if (buffer) + free(buffer); + if (tokv) + free(tokv); + return NULL; +} + +/** + * Dmenu filterer that accepts substring function. + * + * @param menu bmMenu instance to filter. + * @param addition This will be 1, if filter is same as previous filter with something appended. + * @param fstrstr Substring function used to match items. + * @param outNmemb unsigned int reference to filtered items outNmemb. + * @return Pointer to array of bmItem pointers. + */ +bmItem** _bmFilterDmenuFun(bmMenu *menu, char addition, char* (*fstrstr)(const char *a, const char *b), int (*fstrncmp)(const char *a, const char *b, size_t len), unsigned int *outNmemb) +{ + assert(menu); + assert(fstrstr); + assert(fstrncmp); + assert(outNmemb); + *outNmemb = 0; + + unsigned int itemsCount; + bmItem **items; + + if (addition) { + items = bmMenuGetFilteredItems(menu, &itemsCount); + } else { + items = bmMenuGetItems(menu, &itemsCount); + } + + char *buffer = NULL; + bmItem **filtered = calloc(itemsCount, sizeof(bmItem*)); + if (!filtered) + goto fail; + + char **tokv; + unsigned int tokc; + if (!(buffer = _bmFilterTokenize(menu, &tokv, &tokc))) + goto fail; + + size_t len = (tokc ? strlen(tokv[0]) : 0); + unsigned int i, f, e; + for (e = f = i = 0; i < itemsCount; ++i) { + bmItem *item = items[i]; + if (!item->text && tokc != 0) + continue; + + if (tokc && item->text) { + unsigned int t; + for (t = 0; t < tokc && fstrstr(item->text, tokv[t]); ++t); + if (t < tokc) + continue; + } + + if (tokc && item->text && !fstrncmp(tokv[0], item->text, len + 1)) { /* exact matches */ + memmove(&filtered[1], filtered, f * sizeof(bmItem*)); + filtered[0] = item; + e++; /* where do exact matches end */ + } else if (tokc && item->text && !fstrncmp(tokv[0], item->text, len)) { /* prefixes */ + memmove(&filtered[e + 1], &filtered[e], (f - e) * sizeof(bmItem*)); + filtered[e] = item; + e++; /* where do exact matches end */ + } else { + filtered[f] = item; + } + f++; /* where do all matches end */ + } + + if (buffer) + free(buffer); + if (tokv) + free(tokv); + + return _bmFilterShrinkList(&filtered, menu->items.count, (*outNmemb = f)); + +fail: + if (filtered) + free(filtered); + if (buffer) + free(buffer); + return NULL; +} + +/** + * Filter that mimics the vanilla dmenu filtering. + * + * @param menu bmMenu instance to filter. + * @param addition This will be 1, if filter is same as previous filter with something appended. + * @param outNmemb unsigned int reference to filtered items outNmemb. + * @return Pointer to array of bmItem pointers. + */ +bmItem** _bmFilterDmenu(bmMenu *menu, char addition, unsigned int *outNmemb) +{ + return _bmFilterDmenuFun(menu, addition, strstr, strncmp, outNmemb); +} + +/** + * Filter that mimics the vanilla case-insensitive dmenu filtering. + * + * @param menu bmMenu instance to filter. + * @param addition This will be 1, if filter is same as previous filter with something appended. + * @param outNmemb unsigned int reference to filtered items outNmemb. + * @return Pointer to array of bmItem pointers. + */ +bmItem** _bmFilterDmenuCaseInsensitive(bmMenu *menu, char addition, unsigned int *outNmemb) +{ + return _bmFilterDmenuFun(menu, addition, _bmStrupstr, _bmStrnupcmp, outNmemb); +} + +/* vim: set ts=8 sw=4 tw=0 :*/ diff --git a/lib/internal.h b/lib/internal.h new file mode 100644 index 0000000..2fc9393 --- /dev/null +++ b/lib/internal.h @@ -0,0 +1,188 @@ +#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. + */ +struct _bmItem { + /** + * Userdata pointer. + * This pointer will be passed around with the item untouched. + */ + void *userdata; + + /** + * Primary text shown on item as null terminated C "string". + * Matching will be done against this text as well. + */ + char *text; +}; + +/** + * Internal bmRenderApi struct. + * Renderers should be able to fill this one as they see fit. + */ +struct _bmRenderApi { + /** + * Get count of displayed items by the underlying renderer. + */ + unsigned int (*displayedCount)(const bmMenu *menu); + + /** + * If the underlying renderer is a UI toolkit. (curses, etc...) + * There might be possibility to get user input, and this should be thus implemented. + */ + bmKey (*getKey)(unsigned int *unicode); + + /** + * Tells underlying renderer to draw the menu. + */ + void (*render)(const bmMenu *menu); + + /** + * Release underlying renderer. + */ + void (*free)(void); +}; + +struct _bmItemList { + /** + * Items in the list. + */ + struct _bmItem **list; + + /** + * Number of items. + */ + unsigned int count; + + /** + * Number of allocated items. + */ + unsigned int allocated; +}; + +/** + * Internal bmMenu struct that is not exposed to public. + */ +struct _bmMenu { + /** + * Userdata pointer. + * This pointer will be passed around with the menu untouched. + */ + void *userdata; + + /** + * Underlying renderer access. + */ + struct _bmRenderApi renderApi; + + /** + * Items contained in menu instance. + */ + struct _bmItemList items; + + /** + * Filtered/displayed items contained in menu instance. + */ + struct _bmItemList filtered; + + /** + * Selected items. + */ + struct _bmItemList selection; + + /** + * Menu instance title. + */ + char *title; + + /** + * Text used to filter matches. + */ + char *filter; + + /** + * Used as optimization. + */ + char *oldFilter; + + /** + * Size of filter buffer + */ + size_t filterSize; + + /** + * Current byte offset on filter text. + */ + unsigned int cursor; + + /** + * Current column/cursor position on filter text. + */ + unsigned int cursesCursor; + + /** + * Current filtered/highlighted item index in menu instance. + * This index is valid for the list returned by bmMenuGetFilteredItems. + */ + unsigned int index; + + /** + * Current filtering method in menu instance. + */ + bmFilterMode filterMode; + + /** + * Drawing mode used in menu instance. + */ + bmDrawMode drawMode; + + /** + * Should selection be wrapped? + */ + char wrap; +}; + +/* draw/curses.c */ +int _bmDrawCursesInit(struct _bmRenderApi *api); + +/* menu.c */ +int _bmMenuItemIsSelected(const bmMenu *menu, const bmItem *item); + +/* filter.c */ +bmItem** _bmFilterDmenu(bmMenu *menu, char addition, unsigned int *outNmemb); +bmItem** _bmFilterDmenuCaseInsensitive(bmMenu *menu, char addition, unsigned int *outNmemb); + +/* list.c */ +void _bmItemListFreeList(struct _bmItemList *list); +void _bmItemListFreeItems(struct _bmItemList *list); +bmItem** _bmItemListGetItems(const struct _bmItemList *list, unsigned int *outNmemb); +int _bmItemListSetItemsNoCopy(struct _bmItemList *list, bmItem **items, unsigned int nmemb); +int _bmItemListSetItems(struct _bmItemList *list, const bmItem **items, unsigned int nmemb); +int _bmItemListGrow(struct _bmItemList *list, unsigned int step); +int _bmItemListAddItemAt(struct _bmItemList *list, bmItem *item, unsigned int index); +int _bmItemListAddItem(struct _bmItemList *list, bmItem *item); +int _bmItemListRemoveItemAt(struct _bmItemList *list, unsigned int index); +int _bmItemListRemoveItem(struct _bmItemList *list, const bmItem *item); + +/* util.c */ +char* _bmStrdup(const char *s); +size_t _bmStripToken(char *string, const char *token, size_t *outNext); +int _bmStrupcmp(const char *hay, const char *needle); +int _bmStrnupcmp(const char *hay, const char *needle, size_t len); +char* _bmStrupstr(const char *hay, const char *needle); +bmItem** _bmShrinkItemList(bmItem ***inOutList, 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 *outRuneWidth); +size_t _bmUtf8RuneInsert(char **string, size_t *bufSize, size_t start, const char *rune, unsigned int u8len, size_t *outRuneWidth); +size_t _bmUnicodeInsert(char **string, size_t *bufSize, size_t start, unsigned int unicode, size_t *outRuneWidth); + +/* vim: set ts=8 sw=4 tw=0 :*/ diff --git a/lib/item.c b/lib/item.c new file mode 100644 index 0000000..f2a879a --- /dev/null +++ b/lib/item.c @@ -0,0 +1,96 @@ +#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 for new item instance, **NULL** if creation failed. + */ +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); + + if (item->text) + free(item->text); + + free(item); +} + +/** + * Set userdata pointer to bmItem instance. + * Userdata will be carried unmodified by the instance. + * + * @param item bmItem instance where to set userdata pointer. + * @param userdata Pointer to userdata. + */ +void bmItemSetUserdata(bmItem *item, void *userdata) +{ + assert(item); + item->userdata = userdata; +} + +/** + * Get userdata pointer from bmItem instance. + * + * @param item bmItem instance which userdata pointer to get. + * @return Pointer for unmodified userdata. + */ +void* bmItemGetUserdata(bmItem *item) +{ + assert(item); + return item->userdata; +} + +/** + * 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); + + 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); + return item->text; +} + +/* vim: set ts=8 sw=4 tw=0 :*/ diff --git a/lib/library.c b/lib/library.c new file mode 100644 index 0000000..f432eda --- /dev/null +++ b/lib/library.c @@ -0,0 +1,15 @@ +#include "version.h" + +/** + * Get version of the library in 'major.minor.patch' format. + * + * @see @link http://semver.org/ Semantic Versioning @endlink + * + * @return Null terminated C "string" to version string. + */ +const char *bmVersion(void) +{ + return BM_VERSION; +} + +/* vim: set ts=8 sw=4 tw=0 :*/ diff --git a/lib/list.c b/lib/list.c new file mode 100644 index 0000000..203adf2 --- /dev/null +++ b/lib/list.c @@ -0,0 +1,139 @@ +#include "internal.h" +#include <stdlib.h> +#include <string.h> +#include <assert.h> + +void _bmItemListFreeList(struct _bmItemList *list) +{ + assert(list); + + if (list->list) + free(list->list); + + list->allocated = list->count = 0; + list->list = NULL; +} + +void _bmItemListFreeItems(struct _bmItemList *list) +{ + assert(list); + + unsigned int i; + for (i = 0; i < list->count; ++i) + bmItemFree(list->list[i]); + + _bmItemListFreeList(list); +} + +bmItem** _bmItemListGetItems(const struct _bmItemList *list, unsigned int *outNmemb) +{ + assert(list); + + if (outNmemb) + *outNmemb = list->count; + + return list->list; +} + +/** !!! Frees the old list, not items !!! */ +int _bmItemListSetItemsNoCopy(struct _bmItemList *list, bmItem **items, unsigned int nmemb) +{ + assert(list); + + _bmItemListFreeList(list); + + if (!items || nmemb == 0) { + items = NULL; + nmemb = 0; + } + + list->list = items; + list->allocated = list->count = nmemb; + return 1; +} + +/** !!! Frees the old items and list !!! */ +int _bmItemListSetItems(struct _bmItemList *list, const bmItem **items, unsigned int nmemb) +{ + assert(list); + + if (!items || nmemb == 0) { + _bmItemListFreeItems(list); + return 1; + } + + bmItem **newItems; + if (!(newItems = calloc(sizeof(bmItem*), nmemb))) + return 0; + + memcpy(newItems, items, sizeof(bmItem*) * nmemb); + return _bmItemListSetItemsNoCopy(list, newItems, nmemb); +} + +int _bmItemListGrow(struct _bmItemList *list, unsigned int step) +{ + assert(list); + + void *tmp; + unsigned int nsize = sizeof(bmItem*) * (list->allocated + step); + + if (!list->list || !(tmp = realloc(list->list, nsize))) { + if (!(tmp = malloc(nsize))) + return 0; + + if (list->list) { + memcpy(tmp, list->list, sizeof(bmItem*) * list->allocated); + free(list->list); + } + } + + list->list = tmp; + list->allocated += step; + memset(&list->list[list->count], 0, sizeof(bmItem*) * (list->allocated - list->count)); + return 1; +} + +int _bmItemListAddItemAt(struct _bmItemList *list, bmItem *item, unsigned int index) +{ + assert(list); + assert(item); + + if ((!list->list || list->allocated <= list->count) && !_bmItemListGrow(list, 32)) + return 0; + + if (index + 1 != list->count) { + unsigned int i = index; + memmove(&list->list[i + 1], &list->list[i], sizeof(bmItem*) * (list->count - i)); + } + + list->list[index] = item; + list->count++; + return 1; +} + +int _bmItemListAddItem(struct _bmItemList *list, bmItem *item) +{ + assert(list); + return _bmItemListAddItemAt(list, item, list->count); +} + +int _bmItemListRemoveItemAt(struct _bmItemList *list, unsigned int index) +{ + assert(list); + + unsigned int i = index; + if (!list->list || list->count <= i) + return 0; + + memmove(&list->list[i], &list->list[i], sizeof(bmItem*) * (list->count - i)); + return 1; +} + +int _bmItemListRemoveItem(struct _bmItemList *list, const bmItem *item) +{ + unsigned int i; + for (i = 0; i < list->count && list->list[i] != item; ++i); + return _bmItemListRemoveItemAt(list, i); +} + +/* vim: set ts=8 sw=4 tw=0 :*/ diff --git a/lib/menu.c b/lib/menu.c new file mode 100644 index 0000000..b7ab021 --- /dev/null +++ b/lib/menu.c @@ -0,0 +1,727 @@ +#include "internal.h" +#include <stdlib.h> +#include <string.h> +#include <ctype.h> +#include <assert.h> + +/** + * Filter function map. + */ +static bmItem** (*filterFunc[BM_FILTER_MODE_LAST])(bmMenu *menu, char addition, unsigned int *outNmemb) = { + _bmFilterDmenu, /* BM_FILTER_DMENU */ + _bmFilterDmenuCaseInsensitive /* BM_FILTER_DMENU_CASE_INSENSITIVE */ +}; + +int _bmMenuItemIsSelected(const bmMenu *menu, const bmItem *item) +{ + assert(menu); + assert(item); + + unsigned int i, count; + bmItem **items = bmMenuGetSelectedItems(menu, &count); + for (i = 0; i < count && items[i] != item; ++i); + return (i < count); +} + +/** + * 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); + + if (menu->renderApi.free) + menu->renderApi.free(); + + if (menu->title) + free(menu->title); + + if (menu->filter) + free(menu->filter); + + if (menu->oldFilter) + free(menu->oldFilter); + + 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); + _bmItemListFreeList(&menu->selection); + _bmItemListFreeList(&menu->filtered); + _bmItemListFreeItems(&menu->items); +} + +/** + * Set userdata pointer to bmMenu instance. + * Userdata will be carried unmodified by the instance. + * + * @param menu bmMenu instance where to set userdata pointer. + * @param userdata Pointer to userdata. + */ +void bmMenuSetUserdata(bmMenu *menu, void *userdata) +{ + assert(menu); + menu->userdata = userdata; +} + +/** + * Get userdata pointer from bmMenu instance. + * + * @param menu bmMenu instance which userdata pointer to get. + * @return Pointer for unmodified userdata. + */ +void* bmMenuGetUserdata(bmMenu *menu) +{ + assert(menu); + return menu->userdata; +} + +/** + * Set filter text to bmMenu instance. + * + * @param menu bmMenu instance where to set filter. + * @param filter Null terminated C "string" to act as filter. + */ +void bmMenuSetFilter(bmMenu *menu, const char *filter) +{ + assert(menu); + + if (menu->filter) + free(menu->filter); + + menu->filter = (filter ? _bmStrdup(filter) : NULL); + menu->filterSize = (filter ? strlen(filter) : 0); +} + +/** + * Get filter text from bmMenu instance. + * + * @param menu bmMenu instance where to get filter. + * @return Const pointer to current filter text, may be **NULL** if empty. + */ +const char* bmMenuGetFilter(bmMenu *menu) +{ + assert(menu); + return menu->filter; +} + +/** + * 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); + menu->filterMode = (mode >= BM_FILTER_MODE_LAST ? BM_FILTER_MODE_DMENU : 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) +{ + assert(menu); + return menu->filterMode; +} + +/** + * Set selection wrapping on/off. + * + * @param menu bmMenu instance where to toggle selection wrapping. + * @param int 1 == on, 0 == off. + */ +void bmMenuSetWrap(bmMenu *menu, int wrap) +{ + assert(menu); + menu->wrap = (wrap ? 1 : 0); +} + +/** + * Get selection wrapping state. + * + * @param menu bmMenu instance where to get selection wrapping state. + * @return int for wrap state. + */ +int bmMenuGetWrap(const bmMenu *menu) +{ + assert(menu); + return menu->wrap; +} + +/** + * 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); + + 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); + 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); + return _bmItemListAddItemAt(&menu->items, item, 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) +{ + return _bmItemListAddItem(&menu->items, item); +} + +/** + * Remove item from bmMenu instance at specific index. + * + * @warning The item won't be freed, use bmItemFree to do that. + * + * @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); + + if (!menu->items.list || menu->items.count <= index) + return 0; + + bmItem *item = menu->items.list[index]; + int ret = _bmItemListRemoveItemAt(&menu->items, index); + + if (ret) { + _bmItemListRemoveItem(&menu->selection, item); + _bmItemListRemoveItem(&menu->filtered, item); + } + + return ret; +} + +/** + * Remove item from bmMenu instance. + * + * @warning 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) +{ + assert(menu); + + int ret = _bmItemListRemoveItem(&menu->items, item); + + if (ret) { + _bmItemListRemoveItem(&menu->selection, item); + _bmItemListRemoveItem(&menu->filtered, item); + } + + return ret; +} + +/** + * Highlight item in menu by index. + * + * @param menu bmMenu instance from where to highlight item. + * @param index Index of item to highlight. + * @return 1 on successful highlight, 0 on failure. + */ +int bmMenuSetHighlightedIndex(bmMenu *menu, unsigned int index) +{ + assert(menu); + + unsigned int itemsCount; + bmMenuGetFilteredItems(menu, &itemsCount); + + if (itemsCount <= index) + return 0; + + return (menu->index = index); +} + +/** + * Highlight item in menu. + * + * @param menu bmMenu instance from where to highlight item. + * @param item bmItem instance to highlight. + * @return 1 on successful highlight, 0 on failure. + */ +int bmMenuSetHighlighted(bmMenu *menu, bmItem *item) +{ + assert(menu); + + unsigned int i, itemsCount; + bmItem **items = bmMenuGetFilteredItems(menu, &itemsCount); + for (i = 0; i < itemsCount && items[i] != item; ++i); + + if (itemsCount <= i) + return 0; + + return (menu->index = i); +} + +/** + * Get highlighted item from bmMenu instance. + * + * @warning The pointer returned by this function may be invalid after items change. + * + * @param menu bmMenu instance from where to get highlighted item. + * @return Selected bmItem instance, **NULL** if none highlighted. + */ +bmItem* bmMenuGetHighlightedItem(const bmMenu *menu) +{ + assert(menu); + + unsigned int count; + bmItem **items = bmMenuGetFilteredItems(menu, &count); + + if (!items || count <= menu->index) + return NULL; + + return items[menu->index]; +} + +/** + * Set selected items to 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 bmMenuSetSelectedItems(bmMenu *menu, bmItem **items, unsigned int nmemb) +{ + assert(menu); + + bmItem **newItems; + if (!(newItems = calloc(sizeof(bmItem*), nmemb))) + return 0; + + memcpy(newItems, items, sizeof(bmItem*) * nmemb); + return _bmItemListSetItemsNoCopy(&menu->selection, newItems, nmemb); +} + +/** + * Get selected items from bmMenu instance. + * + * @warning The pointer returned by this function may be invalid after selection or items change. + * + * @param menu bmMenu instance from where to get selected items. + * @param outNmemb Reference to unsigned int where total count of returned items will be stored. + * @return Pointer to array of bmItem pointers. + */ +bmItem** bmMenuGetSelectedItems(const bmMenu *menu, unsigned int *outNmemb) +{ + assert(menu); + return _bmItemListGetItems(&menu->selection, outNmemb); +} + +/** + * 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) +{ + assert(menu); + + int ret = _bmItemListSetItems(&menu->items, items, nmemb); + + if (ret) { + _bmItemListFreeList(&menu->selection); + _bmItemListFreeList(&menu->filtered); + } + + return ret; +} + +/** + * Get items from bmMenu instance. + * + * @warning The pointer returned by this function may be invalid after removing or adding new items. + * + * @param menu bmMenu instance from where to get items. + * @param outNmemb 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 *outNmemb) +{ + assert(menu); + return _bmItemListGetItems(&menu->items, outNmemb); +} + +/** + * 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 outNmemb 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 *outNmemb) +{ + assert(menu); + + if (menu->filter && strlen(menu->filter)) + return _bmItemListGetItems(&menu->filtered, outNmemb); + + return _bmItemListGetItems(&menu->items, outNmemb); +} + +/** + * Render bmMenu instance using chosen draw method. + * + * @param menu bmMenu instance to be rendered. + */ +void bmMenuRender(const bmMenu *menu) +{ + assert(menu); + + if (menu->renderApi.render) + menu->renderApi.render(menu); +} + +/** + * Trigger filtering of menu manually. + * This is useful when adding new items and want to dynamically see them filtered. + * + * Do note that filtering might be heavy, so you should only call it after batch manipulation of items. + * Not after manipulation of each single item. + * + * @param menu bmMenu instance which to filter. + */ +void bmMenuFilter(bmMenu *menu) +{ + assert(menu); + + char addition = 0; + size_t len = (menu->filter ? strlen(menu->filter) : 0); + + if (!len || !menu->items.list || menu->items.count <= 0) { + _bmItemListFreeList(&menu->filtered); + + if (menu->oldFilter) + free(menu->oldFilter); + + menu->oldFilter = NULL; + return; + } + + if (menu->oldFilter) { + size_t oldLen = strlen(menu->oldFilter); + addition = (oldLen < len && !memcmp(menu->oldFilter, menu->filter, oldLen)); + } + + if (menu->oldFilter && addition && menu->filtered.count <= 0) + return; + + if (menu->oldFilter && !strcmp(menu->filter, menu->oldFilter)) + return; + + unsigned int count; + bmItem **filtered = filterFunc[menu->filterMode](menu, addition, &count); + + _bmItemListSetItemsNoCopy(&menu->filtered, filtered, count); + menu->index = 0; + + if (menu->oldFilter) + free(menu->oldFilter); + + menu->oldFilter = _bmStrdup(menu->filter); +} + +/** + * Poll key and unicode from underlying UI toolkit. + * + * This function will block on @link ::bmDrawMode BM_DRAW_MODE_CURSES @endlink draw mode. + * + * @param menu bmMenu instance from which to poll. + * @param outUnicode Reference to unsigned int. + * @return bmKey for polled key. + */ +bmKey bmMenuGetKey(bmMenu *menu, unsigned int *outUnicode) +{ + assert(menu); + assert(outUnicode); + + *outUnicode = 0; + bmKey key = BM_KEY_NONE; + + if (menu->renderApi.getKey) + key = menu->renderApi.getKey(outUnicode); + + return key; +} + +/** + * Advances menu logic with key and unicode as input. + * + * @param menu bmMenu instance to be advanced. + * @param key Key input that will advance menu logic. + * @param unicode Unicode input that will advance menu logic. + * @return bmRunResult for menu state. + */ +bmRunResult bmMenuRunWithKey(bmMenu *menu, bmKey key, unsigned int unicode) +{ + assert(menu); + + unsigned int itemsCount; + bmMenuGetFilteredItems(menu, &itemsCount); + + unsigned int displayed = 0; + if (menu->renderApi.displayedCount) + displayed = menu->renderApi.displayedCount(menu); + + if (!displayed) + displayed = itemsCount; + + switch (key) { + case BM_KEY_LEFT: + if (menu->filter) { + 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: + if (menu->filter) { + 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 = (menu->filter ? strlen(menu->filter) : 0); + menu->cursesCursor = (menu->filter ? _bmUtf8StringScreenWidth(menu->filter) : 0); + break; + + case BM_KEY_UP: + if (menu->index > 0) { + menu->index--; + } else if (menu->wrap) { + menu->index = itemsCount - 1; + } + break; + + case BM_KEY_DOWN: + if (menu->index < itemsCount - 1) { + menu->index++; + } else if (menu->wrap) { + menu->index = 0; + } + break; + + case BM_KEY_PAGE_UP: + menu->index = (menu->index < displayed ? 0 : menu->index - (displayed - 1)); + break; + + case BM_KEY_PAGE_DOWN: + menu->index = (menu->index + displayed >= itemsCount ? itemsCount - 1 : menu->index + (displayed - 1)); + break; + + case BM_KEY_SHIFT_PAGE_UP: + menu->index = 0; + break; + + case BM_KEY_SHIFT_PAGE_DOWN: + menu->index = itemsCount - 1; + break; + + case BM_KEY_BACKSPACE: + if (menu->filter) { + size_t width; + menu->cursor -= _bmUtf8RuneRemove(menu->filter, menu->cursor, &width); + menu->cursesCursor -= width; + } + break; + + case BM_KEY_DELETE: + if (menu->filter) + _bmUtf8RuneRemove(menu->filter, menu->cursor + 1, NULL); + break; + + case BM_KEY_LINE_DELETE_LEFT: + if (menu->filter) { + 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: + if (menu->filter) + menu->filter[menu->cursor] = 0; + break; + + case BM_KEY_WORD_DELETE: + if (menu->filter) { + 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, &menu->filterSize, menu->cursor, unicode, &width); + menu->cursesCursor += width; + } + break; + + case BM_KEY_TAB: + { + const char *text; + bmItem *highlighted = bmMenuGetHighlightedItem(menu); + if (highlighted && (text = bmItemGetText(highlighted))) { + bmMenuSetFilter(menu, text); + menu->cursor = (menu->filter ? strlen(menu->filter) : 0); + menu->cursesCursor = (menu->filter ? _bmUtf8StringScreenWidth(menu->filter) : 0); + } + } + break; + + case BM_KEY_CONTROL_RETURN: + case BM_KEY_RETURN: + { + bmItem *highlighted = bmMenuGetHighlightedItem(menu); + if (highlighted && !_bmMenuItemIsSelected(menu, highlighted)) + _bmItemListAddItem(&menu->selection, highlighted); + } + break; + + case BM_KEY_SHIFT_RETURN: + case BM_KEY_ESCAPE: + _bmItemListFreeList(&menu->selection); + break; + + default: break; + } + + bmMenuFilter(menu); + + switch (key) { + case BM_KEY_SHIFT_RETURN: + case BM_KEY_RETURN: return BM_RUN_RESULT_SELECTED; + case BM_KEY_ESCAPE: return BM_RUN_RESULT_CANCEL; + default: break; + } + + 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..a47ff8d --- /dev/null +++ b/lib/util.c @@ -0,0 +1,314 @@ +#include "internal.h" + +#define _XOPEN_SOURCE +#include <wchar.h> /* wcswidth */ +#undef _XOPEN_SOURCE + +#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) +{ + assert(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); +} + +/** + * Replaces next token in string with '\0' and returns position for the replaced token. + * + * @param string C "string" where token will be replaced. + * @param outNext Reference to position of next delimiter, or 0 if none. + * @return Position of the replaced token. + */ +size_t _bmStripToken(char *string, const char *token, size_t *outNext) +{ + size_t len = strcspn(string, token); + + if (outNext) + *outNext = len + (string[len] != 0); + + string[len] = 0; + return len; +} + +/** + * Portable case-insensitive strcmp. + * + * @param hay C "string" to match against. + * @param needle C "string" to match. + * @return Less than, equal to or greater than zero if hay is lexicographically less than, equal to or greater than needle. + */ +int _bmStrupcmp(const char *hay, const char *needle) +{ + return _bmStrnupcmp(hay, needle, strlen(hay)); +} + +/** + * Portable case-insensitive strncmp. + * + * @param hay C "string" to match against. + * @param needle C "string" to match. + * @return Less than, equal to or greater than zero if hay is lexicographically less than, equal to or greater than needle. + */ +int _bmStrnupcmp(const char *hay, const char *needle, size_t len) +{ + size_t i = 0; + unsigned char a = 0, b = 0; + + const unsigned char *p1 = (const unsigned char*)hay; + const unsigned char *p2 = (const unsigned char*)needle; + + for (i = 0; len > 0; --len, ++i) + if ((a = toupper(*p1++)) != (b = toupper(*p2++))) + return a - b; + + return a - b; +} + +/** + * Portable case-insensitive strstr. + * + * @param hay C "string" to substring against. + * @param needle C "string" to substring. + */ +char* _bmStrupstr(const char *hay, const char *needle) +{ + size_t i, r = 0, p = 0, len, len2; + + if ((len = strlen(hay)) < (len2 = strlen(needle))) + return NULL; + + if (!_bmStrnupcmp(hay, needle, len2)) + return (char*)hay; + + for (i = 0; i < len; ++i) { + if (p == len2) + return (char*)hay + r; + + if (toupper(hay[i]) == toupper(needle[p++])) { + if (!r) + r = i; + } else { + if (r) + i = r; + r = p = 0; + } + } + + return (p == len2 ? (char*)hay + r : NULL); +} + +/** + * 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) +{ + assert(string); + + char *mstr = _bmStrdup(string); + if (!mstr) + return strlen(string); + + char *s; + for (s = mstr; *s; ++s) if (*s == '\t') *s = ' '; + + int num_char = mbstowcs(NULL, mstr, 0) + 1; + wchar_t *wstring = malloc((num_char + 1) * sizeof (wstring[0])); + + if (mbstowcs(wstring, mstr, num_char) == (size_t)(-1)) { + free(wstring); + int len = strlen(mstr); + free(mstr); + return len; + } + + int length = wcswidth(wstring, num_char); + free(wstring); + free(mstr); + 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); + + 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); + + 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); + 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 outRuneWidth 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 *outRuneWidth) +{ + assert(string); + + if (outRuneWidth) + *outRuneWidth = 0; + + size_t len = strlen(string), oldStart = start; + if (len == 0 || len < start || !*string) + return 0; + + start -= _bmUtf8RunePrev(string, start); + + if (outRuneWidth) + *outRuneWidth = _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 inOutString Reference to buffer. + * @param inOutBufSize Reference to 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 outRuneWidth 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 **inOutString, size_t *inOutBufSize, size_t start, const char *rune, unsigned int u8len, size_t *outRuneWidth) +{ + assert(inOutString); + assert(inOutBufSize); + + if (outRuneWidth) + *outRuneWidth = 0; + + if (u8len == 1 && !isprint(*rune)) + return 0; + + size_t len = (*inOutString ? strlen(*inOutString) : 0); + if (!*inOutString && !(*inOutString = calloc(1, (*inOutBufSize = u8len + 1)))) + return 0; + + if (len + u8len >= *inOutBufSize) { + void *tmp; + if (!(tmp = realloc(*inOutString, (*inOutBufSize * 2)))) { + if (!(tmp = malloc((*inOutBufSize * 2)))) + return 0; + + memcpy(tmp, *inOutString, *inOutBufSize); + free(*inOutString); + } + + memset(tmp + *inOutBufSize, 0, *inOutBufSize); + *inOutString = tmp; + *inOutBufSize *= 2; + } + + char *str = *inOutString + start; + memmove(str + u8len, str, len - start); + memcpy(str, rune, u8len); + (*inOutString)[len + u8len] = 0; + + if (outRuneWidth) + *outRuneWidth = _bmUtf8RuneWidth(rune, u8len); + return u8len; +} + +/** + * Insert unicode character to UTF8 buffer. + * + * @param inOutString Reference to buffer. + * @param inOutBufSize Reference to size of the buffer. + * @param start Start offset where to insert to. (cursor) + * @param unicode Unicode character to insert. + * @param outRuneWidth 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 **inOutString, size_t *inOutBufSize, size_t start, unsigned int unicode, size_t *outRuneWidth) +{ + assert(inOutString); + assert(inOutBufSize); + + 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(inOutString, inOutBufSize, start, mb, u8len, outRuneWidth); +} + +/* vim: set ts=8 sw=4 tw=0 :*/ diff --git a/lib/version.h.in b/lib/version.h.in new file mode 100644 index 0000000..6be8b5f --- /dev/null +++ b/lib/version.h.in @@ -0,0 +1,3 @@ +static const char *BM_VERSION = "@BEMENU_VERSION@"; + +/* vim: set ts=8 sw=4 tw=0 :*/ |