1
0
Fork 0
mirror of https://github.com/Cloudef/bemenu synced 2024-06-02 21:06:20 +02:00

Basic working bemenu with curses backend

This commit is contained in:
Jari Vetoniemi 2014-04-10 01:09:35 +03:00
parent a3498b25f4
commit 67be25fbe4
10 changed files with 1504 additions and 74 deletions

View File

@ -6,8 +6,83 @@
*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stddef.h>
#include <assert.h>
#include <bemenu.h>
static ptrdiff_t getLine(char **outLine, size_t *outAllocated, FILE *stream)
{
size_t len = 0, allocated;
char *s, *buffer;
assert(outLine != NULL);
assert(outAllocated != NULL);
if (stream == NULL || feof(stream) || ferror(stream))
return -1;
allocated = *outAllocated;
buffer = *outLine;
if (buffer == NULL || allocated == 0) {
if (!(buffer = calloc(1, (allocated = 1024) + 1)))
return -1;
}
for (s = buffer;;) {
if (fgets(s, allocated - (s - buffer), stream) == NULL)
return -1;
len = strlen(s);
if (feof(stream))
break;
if (len > 0 && s[len - 1] == '\n')
break;
if (len + 1 >= allocated - (s - buffer)) {
void *tmp = realloc(buffer, 2 * allocated);
if (!tmp)
break;
buffer = tmp;
s = buffer + allocated - 1;
memset(s, 0, allocated - (s - buffer));
allocated *= 2;
} else {
s += len;
}
}
*outAllocated = allocated;
*outLine = buffer;
if (s[len - 1] == '\n')
s[len - 1] = 0;
return s - buffer + len;
}
static void readItemsToMenuFromStdin(bmMenu *menu)
{
ptrdiff_t len;
size_t size;
char *line = NULL;
while ((len = getLine(&line, &size, stdin)) != -1) {
bmItem *item = bmItemNew((len > 0 ? line : NULL));
if (!item)
break;
bmMenuAddItem(menu, item);
}
if (line)
free(line);
}
/**
* Main method
*
@ -21,16 +96,30 @@ int main(int argc, char **argv)
{
(void)argc, (void)argv;
bmMenu *menu = bmMenuNew(BM_DRAW_MODE_NONE);
bmMenu *menu = bmMenuNew(BM_DRAW_MODE_CURSES);
if (!menu)
return EXIT_FAILURE;
bmMenuRender(menu);
bmMenuSetTitle(menu, "bemenu");
readItemsToMenuFromStdin(menu);
bmKey key;
unsigned int unicode;
int status = 0;
do {
bmMenuRender(menu);
key = bmMenuGetKey(menu, &unicode);
} while ((status = bmMenuRunWithKey(menu, key, unicode)) == BM_RUN_RESULT_RUNNING);
if (status == BM_RUN_RESULT_SELECTED) {
bmItem *item = bmMenuGetSelectedItem(menu);
printf("%s\n", bmItemGetText(item));
}
bmMenuFree(menu);
return EXIT_SUCCESS;
return (status == BM_RUN_RESULT_SELECTED ? EXIT_SUCCESS : EXIT_FAILURE);
}
/* vim: set ts=8 sw=4 tw=0 :*/

View File

@ -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 :

View File

@ -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 :*/

View File

@ -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.
@ -32,11 +86,180 @@ 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 :*/

View File

@ -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 :*/

64
lib/filter.c Normal file
View File

@ -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 :*/

View File

@ -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 :*/

75
lib/item.c Normal file
View File

@ -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 :*/

527
lib/menu.c Normal file
View File

@ -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 :*/

226
lib/util.c Normal file
View File

@ -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 :*/