]> Dogcows Code - chaz/yoink/blobdiff - build/dialog.c
build system enhancements
[chaz/yoink] / build / dialog.c
diff --git a/build/dialog.c b/build/dialog.c
new file mode 100644 (file)
index 0000000..ee8a2a8
--- /dev/null
@@ -0,0 +1,578 @@
+
+/*]  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;
+}
+
This page took 0.029983 seconds and 4 git commands to generate.