--- /dev/null
+
+/*] 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 <assert.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <sys/wait.h>
+#include <unistd.h>
+
+#define LUA_LIB
+#include "lua.h"
+#include "lauxlib.h"
+
+
+#ifdef NDEBUG
+#define printarray(A)
+#else
+#include <stdio.h>
+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;
+}
+