/*] Copyright (c) 2011, Charles McGarvey [******************************* **] All rights reserved. * * vi:ts=4 sw=4 tw=75 * * Distributable under the terms and conditions of the 2-clause BSD license; * see the file COPYING for a complete text of the license. * **************************************************************************/ #define LUA_DIALOG_NAME "dialog" #define LUA_DIALOG_VERSION "1.0" #include #include #include #include #include #include #define LUA_LIB #include "lua.h" #include "lauxlib.h" #ifdef NDEBUG #define printarray(A) #else #include static void printarray(const char* argv[]) { printf("%s", argv[0]); int i; for (i = 1; argv[i]; ++i) printf(" %s", argv[i]); } #endif /** * Fork and execute a command with arguments and optionally get a file * descriptor connected to one of the child's own file descriptors. The * process id of the child is returned, or -1 on error. If fd is not NULL, * a pipe will be created and connected to *fd. If *fd is 0, then *fd will * be set to a write file descriptor connected to the child's standard * input. If *fd is not 0, then *fd will be set to a read file descriptor * set connected to the specified file descriptor of the child. In either * case, the caller has the responsibility to close fd when it is no longer * needed. */ static pid_t myexec(const char* command, char* const argv[], int* fd) { int parentFd = -1; int p[2]; pid_t child; if (fd) { if (pipe(p) != 0) return -1; parentFd = (*fd == 0); } if (!(child = fork())) { if (fd) { close(p[parentFd]); if (dup2(p[!parentFd], *fd) == -1) _exit(127); } execv(command, argv); _exit(127); } if (child == -1) { if (parentFd != -1) { close(p[0]); close(p[1]); } return -1; } if (parentFd != -1) { close(p[!parentFd]); *fd = p[parentFd]; } return child; } /** * Wait on a child process. Returns the exit status of the process if the * child terminates normally, or -1 on error or abnormal child termination. */ static int mywait(pid_t pid) { int status; if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status)) return -1; return WEXITSTATUS(status); } /** * Read from a file descriptor until EOF and push contents to the top of * the Lua stack. Closes the file descriptor afterward. */ static void pushstream(lua_State* L, int fd) { luaL_Buffer B; luaL_buffinit(L, &B); char buffer[BUFSIZ]; ssize_t bytes; while ((bytes = read(fd, buffer, sizeof(buffer)))) { if (bytes == -1) break; luaL_addlstring(&B, buffer, bytes); } close(fd); luaL_pushresult(&B); } /** * Write some arbitrary bytes to a file descriptor, appending a newline. */ static void writelstring(int fd, const char* str, size_t len) { ssize_t bytes; while ((bytes = write(fd, str, len))) { if (bytes == -1) break; str += bytes; len -= bytes; } bytes = write(fd, "\n", 1); } /** * Write a NULL-terminate string to a file descriptor. Uses writelstring. */ static void writestring(int fd, const char* str) { writelstring(fd, str, strlen(str)); } /** * Write a Lua value, that is able to be converted to a string, to a file * descriptor. */ static void tostream(lua_State* L, int index, int fd) { size_t len = 0; const char* buffer = lua_tolstring(L, index, &len); writelstring(fd, buffer, len); } /** * Add one or more strings to the end of a NULL-terminated array of * strings. The parameter list itself should also be terminated with a * NULL. The terminating NULL of the array of strings will be moved back * as needed. It is the responsibility of the caller to assert there is * enough room in the array so that an overflow doesn't occur. Strings are * not duplicated as they are added to the array. */ static void addstrings(const char* argv[], ...) { va_list ap; int i; for (i = 0; argv[i]; ++i); va_start(ap, argv); const char* arg = va_arg(ap, const char*); while (arg) { argv[i++] = arg; arg = va_arg(ap, const char*); } va_end(ap); argv[i] = NULL; } /** * Search an array of strings for a particular string, returning the index * where the first equivalent string is found or -1 if it wasn't found. */ static int searchstrings(const char* argv[], const char* str) { int i; for (i = 0; argv[i]; ++i) if (strcmp(argv[i], str) == 0) return i; return -1; } /** * Add the command and backtitle to the argument list from the Lua state. */ static void addcommand(lua_State* L, const char* argv[]) { lua_getglobal(L, LUA_DIALOG_NAME); lua_getfield(L, -1, "command"); lua_getfield(L, -2, "title"); addstrings(argv, lua_tostring(L, -2), NULL); if (lua_isstring(L, -1)) { addstrings(argv, "--backtitle", lua_tostring(L, -1), NULL); } lua_pop(L, 3); } /** * Add the height and width to the argument list from the Lua state. */ static int addsize(lua_State* L, const char* argv[], int extra, int n) { int m = n; lua_getglobal(L, LUA_DIALOG_NAME); if (lua_isnumber(L, m)) lua_pushvalue(L, m++); else lua_getfield(L, -1, "height"); if (lua_isnumber(L, m)) lua_pushvalue(L, m++); else lua_getfield(L, -2, "width"); addstrings(argv, lua_tostring(L, -2), lua_tostring(L, -1), NULL); if (extra) addstrings(argv, lua_tostring(L, -2), NULL); lua_pop(L, 3); return m - n; } /** * Add the dialog title to the argument list from the Lua state. The title * should be at index 1 on the stack, and an error is thrown if it is not * found. */ static void addtitle(lua_State* L, const char* argv[]) { addstrings(argv, "--title", luaL_checkstring(L, 1), NULL); } /** * Add the caption to the argument list from the Lua state. The caption * should be at index 2 on the stack, and an error is thrown if it is not * found. */ static void addcaption(lua_State* L, const char* argv[]) { addstrings(argv, luaL_optstring(L, 2, ""), NULL); } /** * Get the string found at a certain index on the Lua stack, returned in * the text argument. Returns whether the text was set. */ static int getstring(lua_State* L, const char* argv[], int n, const char** text) { if (lua_type(L, n) != LUA_TSTRING) return 0; *text = lua_tostring(L, n); return 1; } /** * Add extra arguments to the argument list from the Lua state. The extra * arguments exist in a table where the key is a flag and its value (if it * is a string) is added as a flag option. */ static int addextra(lua_State* L, const char* argv[], int n) { if (!lua_istable(L, n)) return 0; lua_pushnil(L); while (lua_next(L, n)) { addstrings(argv, lua_tostring(L, -2), NULL); if (lua_isstring(L, -1)) addstrings(argv, lua_tostring(L, -1), NULL); lua_pop(L, 1); } return 1; } /** * Add the default item to the argument list from the Lua stack for a menu * command. Returns whether or not the arguments were found and added. If * the value on the stack is nil, no arguments are added to the list, but * the return value is still 1. */ static int addselected(lua_State* L, const char* argv[], int n) { if (lua_isnil(L, n)) return 1; if (!lua_isstring(L, n)) return 0; addstrings(argv, "--default-item", lua_tostring(L, n), NULL); return 1; } /** * Add menu items to the argument list from the Lua stack. They should * exist in a table at index 3 of a menu command. Each item should be a * table itself with two or three strings, depending on whether --item-help * exists in the argument list. */ static void addmenuitems(lua_State* L, const char* argv[]) { int fields = 2; if (searchstrings(argv, "--item-help") != -1) fields = 3; if (!lua_istable(L, 3)) luaL_argerror(L, 3, "menu items"); int i; for (i = 1;; ++i) { lua_pushinteger(L, i); lua_gettable(L, 3); if (lua_isnil(L, -1)) { lua_pop(L, 1); break; } else if (lua_istable(L, -1)) { int subtable = lua_gettop(L); lua_pushnil(L); int j; for (j = 0; j < fields; ++j) { if (!lua_next(L, subtable)) luaL_argerror(L, 3, "not enough fields"); addstrings(argv, lua_tostring(L, -1), NULL); lua_pop(L, 1); } lua_pop(L, 1); } else { if (fields == 2) addstrings(argv, "", "", NULL); else addstrings(argv, "", "", "", NULL); } lua_pop(L, 1); } } /** * Fill out the argument list from the Lua state according to the standard * 3 or 4 argument dialog command format. */ static void addargs(lua_State* L, const char* command, int nargs, const char* argv[]) { assert((nargs == 3 || nargs == 4) && "nargs should be 3 or 4"); addcommand(L, argv); addtitle(L, argv); const char* text = NULL; int n = 3; if (nargs == 4) n += getstring(L, argv, n, &text); n += addextra(L, argv, n); addstrings(argv, command, NULL); addcaption(L, argv); addsize(L, argv, 0, n); if (text) addstrings(argv, text, NULL); } /** * Close a gauge dialog if one is running. If a gauge is in progress, the * status code will be pushed onto the stack and 1 is returned; otherwise, * 0 is returned. */ static void closegauge(lua_State* L) { lua_getfield(L, LUA_REGISTRYINDEX, "dialog_gauge_pid"); lua_getfield(L, LUA_REGISTRYINDEX, "dialog_gauge_fd"); if (!lua_isnumber(L, -2) || !lua_isnumber(L, -1)) { lua_pop(L, 2); return; } pid_t pid = lua_tointeger(L, -2); int fd = lua_tointeger(L, -1); lua_pop(L, 2); lua_pushnil(L); lua_setfield(L, LUA_REGISTRYINDEX, "dialog_gauge_pid"); lua_pushnil(L); lua_setfield(L, LUA_REGISTRYINDEX, "dialog_gauge_fd"); close(fd); int code = mywait(pid); if (code == -1) luaL_error(L, "dialog killed abnormally"); } /** * Updates the progress of a gauge dialog. Takes a number argument in the * range of 0-100 or 0.0-1.0 as the progress. Optionally takes a string as * the second argument which causes the caption to change. The last call * to this function should be with no arguments to end the gauge dialog. * Returns nothing except the last call which returns the status code of * the dialog which should always be OK. */ static int updategauge(lua_State* L) { if (!lua_isnumber(L, 1)) { closegauge(L); return 0; } lua_getfield(L, LUA_REGISTRYINDEX, "dialog_gauge_fd"); if (!lua_isnumber(L, -1)) return 0; int fd = lua_tointeger(L, -1); lua_Number percent = lua_tonumber(L, 1); if (0.0 <= percent && percent <= 1.0) percent *= 100.0; lua_pushinteger(L, (lua_Integer)percent); lua_replace(L, 1); if (lua_isstring(L, 2)) { writestring(fd, "XXX"); tostream(L, 1, fd); tostream(L, 2, fd); writestring(fd, "XXX"); } else { tostream(L, 1, fd); } return 0; } /** * Display a gauge dialog. Required arguments are dialog title and * caption. Optional arguments include a table of extra flags. Returns a * new function which can be used to update the progress. */ static int dialog_gauge(lua_State* L) { const char* argv[40] = {NULL}; closegauge(L); addargs(L, "--gauge", 3, argv); int fd = 0; pid_t pid = myexec(argv[0], (char* const*)argv, &fd); if (pid == -1) luaL_error(L, "dialog failed to execute"); lua_pushinteger(L, pid); lua_setfield(L, LUA_REGISTRYINDEX, "dialog_gauge_pid"); lua_pushinteger(L, fd); lua_setfield(L, LUA_REGISTRYINDEX, "dialog_gauge_fd"); lua_pushcfunction(L, updategauge); return 1; } /** * Display a menu dialog. Required arguments are dialog title, caption, * and table of menu items. Optional arguments are selected item and table * of extra flags. */ static int dialog_menu(lua_State* L) { const char* argv[1024] = {NULL}; closegauge(L); addcommand(L, argv); addtitle(L, argv); int n = 4; n += addselected(L, argv, n); n += addextra(L, argv, n); addstrings(argv, "--menu", NULL); addcaption(L, argv); addsize(L, argv, 1, n); addmenuitems(L, argv); printarray(argv); int fd = 2; pid_t pid = myexec(argv[0], (char* const*)argv, &fd); if (pid == -1) luaL_error(L, "dialog failed to execute"); int code = mywait(pid); if (code == -1) luaL_error(L, "dialog killed abnormally"); lua_pushinteger(L, code); pushstream(L, fd); return 2; } /** * Display a message dialog. Required arguments are dialog title and * caption. Optional arguments include a table of extra flags. */ static int dialog_msgbox(lua_State* L) { const char* argv[40] = {NULL}; closegauge(L); addargs(L, "--msgbox", 3, argv); pid_t pid = myexec(argv[0], (char* const*)argv, NULL); if (pid == -1) luaL_error(L, "dialog failed to execute"); int code = mywait(pid); if (code == -1) luaL_error(L, "dialog killed abnormally"); lua_pushinteger(L, code); return 1; } /** * Display an input dialog. Required arguments are dialog title and * caption. Optional arguments are the default text and a table of extra * flags. */ static int dialog_inputbox(lua_State* L) { const char* argv[40] = {NULL}; closegauge(L); addargs(L, "--inputbox", 4, argv); int fd = 2; pid_t pid = myexec(argv[0], (char* const*)argv, &fd); if (pid == -1) luaL_error(L, "dialog failed to execute"); int code = mywait(pid); if (code == -1) luaL_error(L, "dialog killed abnormally"); lua_pushinteger(L, code); pushstream(L, fd); return 2; } /** * Display a yes/no dialog. Required arguments are dialog title and * caption. Optional arguments include a table of extra flags. */ static int dialog_yesno(lua_State* L) { const char* argv[40] = {NULL}; closegauge(L); addargs(L, "--yesno", 3, argv); pid_t pid = myexec(argv[0], (char* const*)argv, NULL); if (pid == -1) luaL_error(L, "dialog failed to execute"); int code = mywait(pid); if (code == -1) luaL_error(L, "dialog killed abnormally"); lua_pushinteger(L, code); return 1; } /** * Register the module functions and find a dialog command in the path. */ LUALIB_API int luaopen_dialog(lua_State* L) { const struct luaL_Reg dialog_funcs[] = { {"gauge", dialog_gauge}, {"inputbox", dialog_inputbox}, {"menu", dialog_menu}, {"msgbox", dialog_msgbox}, {"yesno", dialog_yesno}, {NULL, NULL} }; luaL_register(L, LUA_DIALOG_NAME, dialog_funcs); const char* names[] = {getenv("DIALOG"), "dialog", "cdialog"}; int i; for (i = 0; i < 3; ++i) { if (names[i]) { char* path = strdup(getenv("PATH")); char* token; char** paths = &path; while ((token = strsep(paths, ":"))) { luaL_Buffer B; luaL_buffinit(L, &B); luaL_addstring(&B, token); luaL_addstring(&B, "/"); luaL_addstring(&B, names[i]); luaL_pushresult(&B); if (access(lua_tostring(L, -1), X_OK) == 0) { lua_setfield(L, -2, "command"); goto break2; } lua_pop(L, 1); } free(path); } } luaL_error(L, "cannot find dialog executable in the path; set DIALOG"); break2: lua_pushinteger(L, 0); lua_setfield(L, -2, "height"); lua_pushinteger(L, 0); lua_setfield(L, -2, "width"); return 1; }