Moved to a more robust GTK UI. gtkui
authorRyan C. Gordon <icculus@icculus.org>
Sat, 17 Oct 2015 20:24:39 -0400
branchgtkui
changeset 45 cf6a06f368e6
parent 44 2150bce729df
child 46 fe4f59680246
Moved to a more robust GTK UI.

This removes the popup windows, in favor of something searchable, etc, but
it's not without its problems, too, so it's sitting in branch until I decide
I really like it or I should replace it with SDL or something. :)

(If someone can figure out how to get the window to steal focus when popped
up, that would fix the most egregious issue, but there are other small
details that need fixing, too.)
1pass.c
1pass.lua
keyhook.c
--- a/1pass.c	Tue Mar 31 20:18:10 2015 -0400
+++ b/1pass.c	Sat Oct 17 20:24:39 2015 -0400
@@ -17,7 +17,13 @@
 #include "base64.h"
 #include "md5.h"
 #include "keyhook.h"
+
 #include <gtk/gtk.h>
+#include <gdk/gdk.h>
+#include <gdk/gdkx.h>
+#include <gdk/gdkkeysyms.h>
+#include <X11/Xlib.h>
+#include <X11/Xlibint.h>
 
 #define STATICARRAYLEN(x) ( (sizeof ((x))) / (sizeof ((x)[0])) )
 
@@ -184,6 +190,13 @@
 static const uint8_t zero16[16] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };
 static const char saltprefix[] = { 'S', 'a', 'l', 't', 'e', 'd', '_', '_' };
 
+static int makeLuaCallback(lua_State *L, const int idx)
+{
+    assert(lua_isfunction(L, idx));
+    lua_pushvalue(L, idx);  // copy the Lua callback (luaL_ref() pops it).
+    return luaL_ref(L, LUA_REGISTRYINDEX);
+} // makeLuaCallback
+
 static inline int retvalStringBytes(lua_State *L, const uint8_t *str, size_t len)
 {
     if (str != NULL)
@@ -375,6 +388,9 @@
     gtk_container_add(GTK_CONTAINER(content_area), entry);
 
     gtk_window_set_position(GTK_WINDOW(dialog), GTK_WIN_POS_MOUSE);
+    gtk_window_set_keep_above(GTK_WINDOW(dialog), TRUE);
+    gtk_window_set_skip_taskbar_hint(GTK_WINDOW(dialog), TRUE);
+    gtk_window_set_skip_pager_hint(GTK_WINDOW(dialog), TRUE);
     gtk_widget_show_all(dialog);
     gtk_window_set_keep_above(GTK_WINDOW(dialog), TRUE);
     const int ok = (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT);
@@ -392,67 +408,211 @@
     gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD), str, -1);
 } // copyToClipboard
 
+static gboolean checkForEscapeKey(GtkWidget *widget, GdkEvent *event, gpointer arg)
+{
+    if ((event->type == GDK_KEY_PRESS) && (event->key.keyval == GDK_KEY_Escape))
+    {
+        // !!! FIXME: this is a little hacky
+        lua_getglobal(luaState, "escapePressed");
+        lua_call(luaState, 0, 0);
+        return TRUE;
+    } // if
 
-static int makeGuiMenu(lua_State *L)
+    return FALSE;  // pass this to other event handlers.
+} // checkForEscapeKey
+
+static gboolean wasSearchDeleteText = FALSE;  // HACK to workaround gtk+ nonsense.
+static void searchChanged(GtkEditable *editable, gpointer arg)
 {
-    return retvalPointer(L, gtk_menu_new());
-} // makeGuiMenu
+    const int callback = (int) ((size_t)arg);
+    GtkWidget *vbox = gtk_widget_get_parent(GTK_WIDGET(editable));
+    lua_rawgeti(luaState, LUA_REGISTRYINDEX, callback);
+    lua_pushlightuserdata(luaState, vbox);
+    lua_pushstring(luaState, gtk_entry_get_text(GTK_ENTRY(editable)));
+    lua_call(luaState, 2, 0);
+    gtk_widget_grab_focus(GTK_WIDGET(editable));
+    if (wasSearchDeleteText)  // HACK to workaround gtk+ nonsense.
+    {
+        gtk_editable_set_position(editable, -1);
+        wasSearchDeleteText = FALSE;
+    } // if
+} // searchChanged
 
+// HACK to workaround gtk+ nonsense.
+static void searchDeleteText(GtkEditable *editable, gint start_pos, gint end_pos, gpointer user_data)
+{
+    wasSearchDeleteText = TRUE;
+} // searchDeleteText
 
-static void clickedMenuItem(void *arg)
+static void destroyLuaCallback(const int callback)
 {
-    // This is the callback from GTK+; now call into our actual Lua callback!
-    const int callback = (int) ((size_t)arg);
-    lua_rawgeti(luaState, LUA_REGISTRYINDEX, callback);
+    //printf("unref callback %d\n", callback);
+    luaL_unref(luaState, LUA_REGISTRYINDEX, callback);
+} // destroyLuaCallback
+
+static void destroyCallback(GtkWidget *widget, gpointer arg)
+{
+    destroyLuaCallback((int) ((size_t)arg));
+} // destroyCallback
+
+static void destroyTopLevelMenu(GtkWidget *widget, gpointer arg)
+{
+    // !!! FIXME: hack
+    int *cbs = (int *) arg;
+    lua_rawgeti(luaState, LUA_REGISTRYINDEX, cbs[1]);
     lua_call(luaState, 0, 0);
+    destroyLuaCallback(cbs[0]);
+    destroyLuaCallback(cbs[1]);
+    free(cbs);
+} // destroyTopLevelMenu
+
+#if 0
+static gboolean
+mappedWindow(GtkWidget *widget, GdkEvent *event, gpointer user_data)
+{
+    GdkEventClient e;
+    memset(&e, '\0', sizeof (e));
+    e.type = GDK_CLIENT_EVENT;
+    e.window = gtk_widget_get_window(widget);
+    e.send_event = 1;
+    e.message_type = gdk_atom_intern_static_string("_NET_ACTIVE_WINDOW");
+    e.data_format = 32;
+    e.data.l[0] = 1;
+    e.data.l[1] = (long) gdk_x11_get_server_time(e.window);
+    e.data.l[2] = 0;
+
+    gdk_window_raise (e.window);
+    gdk_event_send_client_message((GdkEvent *) &e, gdk_x11_drawable_get_xid(gtk_widget_get_root_window(widget)));
+    return TRUE;
+}
+#endif
+
+static int guiCreateTopLevelMenu(lua_State *L)
+{
+    const char *title = luaL_checkstring(L, 1);
+    const int changedCallback = makeLuaCallback(L, 2);
+    const int destroyedCallback = makeLuaCallback(L, 3);
+
+    int *cbs = (int *) malloc(sizeof (int) * 2);  // !!! FIXME: hack
+    cbs[0] = changedCallback;
+    cbs[1] = destroyedCallback;
+
+    GtkWindow *window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
+    gtk_window_set_keep_above(window, TRUE);
+    gtk_window_set_skip_taskbar_hint(window, TRUE);
+    gtk_window_set_skip_pager_hint(window, TRUE);
+    g_signal_connect(window, "destroy", G_CALLBACK(destroyTopLevelMenu), cbs);
+    gtk_window_set_title(window, title);
+    gtk_window_set_position(window, GTK_WIN_POS_MOUSE);
+    gtk_window_set_decorated(window, FALSE);
+    gtk_window_set_resizable(window, FALSE);
+    GtkEntry *search = GTK_ENTRY(gtk_entry_new());
+    g_signal_connect(search, "key-press-event", G_CALLBACK(checkForEscapeKey), window);
+    gtk_entry_set_text(search, "Search...");
+    g_signal_connect(search, "changed", G_CALLBACK(searchChanged), (gpointer) ((size_t)changedCallback));
+    g_signal_connect(search, "delete-text", G_CALLBACK(searchDeleteText), 0); // HACK to workaround gtk+ nonsense.
+
+//    g_signal_connect(window, "map-event", G_CALLBACK(mappedWindow), NULL);
+
+
+    GtkVBox *vbox = GTK_VBOX(gtk_vbox_new(FALSE, 0));
+    gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(vbox));
+
+    gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(search), FALSE, FALSE, 0);
+    gtk_widget_show(GTK_WIDGET(search));
+
+    GtkWidget *vsep = GTK_WIDGET(gtk_vseparator_new());
+    gtk_box_pack_start(GTK_BOX(vbox), vsep, FALSE, FALSE, 0);
+    gtk_widget_show(vsep);
+
+    return retvalPointer(L, vbox);
+} // guiCreateTopLevelMenu
+
+static void clickedMenuItem(GtkButton *button, gpointer arg)
+{
+    lua_rawgeti(luaState, LUA_REGISTRYINDEX, (int) ((size_t)arg));
+    lua_pushlightuserdata(luaState, button);
+    lua_call(luaState, 1, 0);
 } // clickedMenuItem
 
-#if 0  // !!! FIXME: figure out how to fire this.
-static void deletedMenuItem(void *arg)
+static int guiAddMenuItem(lua_State *L)
 {
-    // Clean up the Lua function we referenced in the Registry.
-    const int callback = (int) ((size_t)arg);
-printf("unref callback %d\n", callback);
-    luaL_unref(luaState, LUA_REGISTRYINDEX, callback);
-} // deletedMenuItem
-#endif
+    GtkWidget *vbox = (GtkWidget *) lua_touserdata(L, 1);
+    const char *label = luaL_checkstring(L, 2);
+    const int callback = makeLuaCallback(L, 3);
+    GtkWidget *item = GTK_WIDGET(gtk_button_new_with_label(label));
+    g_signal_connect(item, "key-press-event", G_CALLBACK(checkForEscapeKey), NULL);
+    g_signal_connect(item, "clicked", G_CALLBACK(clickedMenuItem), (gpointer) ((size_t)callback));
+    g_signal_connect(item, "destroy", G_CALLBACK(destroyCallback), (gpointer) ((size_t)callback));
 
-static int appendGuiMenuItem(lua_State *L)
+    //gtk_button_set_image(button, gtk_image_new_from_stock(GTK_STOCK_OPEN, GTK_ICON_SIZE_MENU));
+    gtk_button_set_alignment (GTK_BUTTON(item), 0.0f, 0.5f);
+    gtk_button_set_relief(GTK_BUTTON(item), GTK_RELIEF_NONE);
+    gtk_box_pack_start(GTK_BOX(vbox), item, FALSE, FALSE, 0);
+    gtk_widget_show(item);
+    return retvalPointer(L, item);
+} // guiAddMenuItem
+
+static int guiRemoveAllMenuItems(lua_State *L)
 {
-    const int argc = lua_gettop(L);
-    GtkWidget *menu = (GtkWidget *) lua_touserdata(L, 1);
-    const char *label = luaL_checkstring(L, 2);
-    GtkWidget *item = gtk_menu_item_new_with_label(label);
+    GtkWidget *vbox = (GtkWidget *) lua_touserdata(L, 1);
+    GList *children = gtk_container_get_children(GTK_CONTAINER(vbox));
+    GList *iter;
 
-    if ((argc >= 3) && (!lua_isnil(L, 3)))
+    gtk_widget_hide(vbox);
+    for (iter = children; iter != NULL; iter = g_list_next(iter))
     {
-        assert(lua_isfunction(L, 3));
-        lua_pushvalue(L, 3);  // copy the Lua callback (luaL_ref() pops it).
-        const int callback = luaL_ref(L, LUA_REGISTRYINDEX);
-        gtk_signal_connect_object(GTK_OBJECT(item), "activate", GTK_SIGNAL_FUNC(clickedMenuItem), (gpointer) ((size_t)callback));
-    } // if
+        if (G_OBJECT_TYPE(iter->data) == GTK_TYPE_BUTTON)
+             gtk_widget_destroy(GTK_WIDGET(iter->data));
+    } // for
+    g_list_free(children);
 
-    gtk_widget_show(item);
-    gtk_menu_append(menu, item);
-    return retvalPointer(L, item);
-} // appendGuiMenuItem
+    return 0;
+} // guiRemoveAllMenuItems
 
+static int guiDestroyMenu(lua_State *L)
+{
+    GtkWidget *widget = (GtkWidget *) lua_touserdata(L, 1);
+    gtk_widget_destroy(gtk_widget_get_toplevel(widget));
+    return 0;
+} // guiDestroyMenu
 
-static int setGuiMenuItemSubmenu(lua_State *L)
+static int guiShowWindow(lua_State *L)
 {
-    GtkMenuItem *item = (GtkMenuItem *) lua_touserdata(L, 1);
-    GtkWidget *submenu = (GtkWidget *) lua_touserdata(L, 2);
-    gtk_menu_item_set_submenu(item, submenu);
+    GtkWidget *widget = (GtkWidget *) lua_touserdata(L, 1);
+    //gtk_container_resize_children(GTK_CONTAINER(vbox));
+    gtk_widget_show(widget);
+    GtkWidget *toplevel = gtk_widget_get_toplevel(widget);
+    gtk_window_present(GTK_WINDOW(toplevel));
     return 0;
-} // setGuiMenuItemSubmenu
+} // guiShowWindow
 
+static int guiCreateSubMenu(lua_State *L)
+{
+    GtkWidget *widget = (GtkWidget *) lua_touserdata(L, 1);
+    GtkWidget *topwindow = gtk_widget_get_toplevel(widget);
+    GtkWindow *window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL));
+    gtk_window_set_keep_above(window, TRUE);
+    gtk_window_set_skip_taskbar_hint(window, TRUE);
+    gtk_window_set_skip_pager_hint(window, TRUE);
+    //g_signal_connect(window, "destroy", G_CALLBACK(destroySubMenu), topwindow);
+    gtk_window_set_decorated(window, FALSE);
 
-static int popupGuiMenu(lua_State *L)
-{
-    GtkMenu *menu = (GtkMenu *) lua_touserdata(L, 1);
-    gtk_menu_popup(menu, NULL, NULL, NULL, NULL, 0, gtk_get_current_event_time());
-    return 0;
-} // popupGuiMenu
+    GtkVBox *vbox = GTK_VBOX(gtk_vbox_new(FALSE, 0));
+    gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(vbox));
+
+    // line the new submenu up...
+    //  !!! FIXME: if overflow off right end of screen, go to the left of (widget) instead.
+    gint basex, basey, x, y;
+    gtk_window_get_position(GTK_WINDOW(topwindow), &basex, &basey);
+    gtk_widget_translate_coordinates(widget, topwindow, 0, 0, &x, &y);
+    x += basex;
+    y += basey;
+    x += widget->allocation.width;
+    gtk_window_move(window, x, y);
+
+    return retvalPointer(L, vbox);
+} // guiCreateSubMenu
 
 
 static int setPowermateLED_Lua(lua_State *L)
@@ -553,15 +713,18 @@
     // Set up initial C functions, etc we want to expose to Lua code...
     luaSetCFunc(luaState, decryptUsingPBKDF2, "decryptUsingPBKDF2");
     luaSetCFunc(luaState, decryptBase64UsingKey, "decryptBase64UsingKey");
-    luaSetCFunc(luaState, makeGuiMenu, "makeGuiMenu");
-    luaSetCFunc(luaState, appendGuiMenuItem, "appendGuiMenuItem");
-    luaSetCFunc(luaState, setGuiMenuItemSubmenu, "setGuiMenuItemSubmenu");
-    luaSetCFunc(luaState, popupGuiMenu, "popupGuiMenu");
     luaSetCFunc(luaState, giveControlToGui, "giveControlToGui");
     luaSetCFunc(luaState, runGuiPasswordPrompt, "runGuiPasswordPrompt");
     luaSetCFunc(luaState, copyToClipboard, "copyToClipboard");
     luaSetCFunc(luaState, setPowermateLED_Lua, "setPowermateLED");
 
+    luaSetCFunc(luaState, guiCreateTopLevelMenu, "guiCreateTopLevelMenu");
+    luaSetCFunc(luaState, guiCreateSubMenu, "guiCreateSubMenu");
+    luaSetCFunc(luaState, guiAddMenuItem, "guiAddMenuItem");
+    luaSetCFunc(luaState, guiRemoveAllMenuItems, "guiRemoveAllMenuItems");
+    luaSetCFunc(luaState, guiDestroyMenu, "guiDestroyMenu");
+    luaSetCFunc(luaState, guiShowWindow, "guiShowWindow");
+
     // Set up argv table...
     lua_newtable(luaState);
     int i;
--- a/1pass.lua	Tue Mar 31 20:18:10 2015 -0400
+++ b/1pass.lua	Sat Oct 17 20:24:39 2015 -0400
@@ -6,6 +6,16 @@
 local items = nil
 local faveitems = nil
 local keyhookRunning = false
+local keyhookGuiMenus = nil
+
+
+local function runGarbageCollector()
+    --local memused = math.floor(collectgarbage("count") * 1024.0)
+    --print("Collecting garbage (currently using " .. memused .. " bytes).")
+    collectgarbage()
+    --local newmemused = math.floor(collectgarbage("count") * 1024.0)
+    --print("Now using " .. newmemused .. " bytes (" .. memused - newmemused .. " bytes savings).")
+end
 
 local passwordTypeNameMap = {
     ["webforms.WebForm"] = "Logins",
@@ -103,6 +113,26 @@
     return load_json(basedir .. "/contents.js")
 end
 
+local function makeMenu()
+    return {}
+end
+
+local function appendMenuItem(menu, text, callback)
+    local item = {}
+    item["text"] = text
+    if callback ~= nil then
+        item["callback"] = callback
+    end
+    menu[#menu+1] = item
+    return item
+end
+
+local function setMenuItemSubmenu(menuitem, submenu)
+    menuitem["submenu"] = submenu
+end
+
+
+
 local function build_secret_menuitem(menu, type, str, hidden)
     if str == nil then
         return nil
@@ -117,9 +147,9 @@
     local callback = function()
         copyToClipboard(str)
         --print("Copied data [" .. str .. "] to clipboard.")
-        keyhookRunning = false
+        guiDestroyMenu(keyhookGuiMenus[1])
     end
-    return appendGuiMenuItem(menu, text, callback)
+    return appendMenuItem(menu, text, callback)
 end
 
 
@@ -294,7 +324,7 @@
     end
     --dumptable("secure " .. info.name, secure)
 
-    local menuitem = appendGuiMenuItem(menu, info.name)
+    local menuitem = appendMenuItem(menu, info.name)
 
     if secret_menuitem_builders[info.type] == nil then
         print("WARNING: don't know how to handle items of type " .. info.type)
@@ -307,9 +337,9 @@
         faveitems[metadata.faveIndex] = { info=info, secure=secure }
     end
 
-    local submenu = makeGuiMenu()
+    local submenu = makeMenu()
     secret_menuitem_builders[info.type](submenu, info, secure)
-    setGuiMenuItemSubmenu(menuitem, submenu)
+    setMenuItemSubmenu(menuitem, submenu)
 end
 
 local function prepItems()
@@ -336,9 +366,12 @@
     password = argv[2]  -- might be nil, don't reset if on command line.
     keys["SL5"] = nil
     passwordUnlockTime = nil
-    keyhookRunning = false
     setPowermateLED(false)
-    collectgarbage()
+
+    -- kill the popup if it exists.
+    if (keyhookGuiMenus ~= nil) and (keyhookGuiMenus[1] ~= nil) then
+        guiDestroyMenu(keyhookGuiMenus[1])
+    end
 end
 
 function pumpLua()  -- not local! Called from C!
@@ -354,12 +387,118 @@
     end
 end
 
+function escapePressed()  -- not local! Called from C!
+    if keyhookGuiMenus[1] then
+        guiDestroyMenu(keyhookGuiMenus[1])
+    end
+end
+
+
+local buildGuiMenuList
+
+local function spawnSubMenu(button, submenu, depth)
+    local guimenu = guiCreateSubMenu(button)
+
+    for i = #keyhookGuiMenus, depth, -1 do
+        if keyhookGuiMenus[i] then
+            --print("Destroying conflicting submenu at depth " .. i)
+            guiDestroyMenu(keyhookGuiMenus[i])
+            keyhookGuiMenus[i] = nil
+        end
+    end
+
+    --print("New submenu at depth " .. depth)
+    keyhookGuiMenus[depth] = guimenu
+
+    buildGuiMenuList(guimenu, submenu)
+    guiShowWindow(guimenu)
+end
+
+local function buildGuiMenuItem(guimenu, item)
+    local cb = item["callback"]
+    if cb == nil then
+        local submenu = item["submenu"]
+        local depth = #keyhookGuiMenus+1
+        cb = function (button)
+            return spawnSubMenu(button, submenu, depth)
+        end
+    end
+    guiAddMenuItem(guimenu, item["text"], cb)
+end
+
+buildGuiMenuList = function(guimenu, list)
+    for i,v in ipairs(list) do
+        buildGuiMenuItem(guimenu, v)
+    end
+end
+
+local function buildSearchResultsMenuCategory(guimenu, menu, str)
+    local submenu = menu["submenu"]
+    if not submenu then return end
+
+    local name = menu["text"]
+    -- !!! FIXME: hacky. We should really list favorites first anyhow.
+    if name == "Favorites" then return end
+
+    for i,v in ipairs(submenu) do
+        if string.find(string.lower(v["text"]), str, 1, true) ~= nil then
+            buildGuiMenuItem(guimenu, v)
+        end
+    end
+end
+
+local function buildSearchResultsMenuList(guimenu, topmenu, str)
+    for i,v in ipairs(topmenu) do
+        buildSearchResultsMenuCategory(guimenu, v, str)
+    end
+end
+
+local function searchEntryChanged(guimenu, str, topmenu)
+    --print("search changed to '" .. str .. "' ...")
+    guiRemoveAllMenuItems(guimenu)
+    if str == "" then
+        buildGuiMenuList(guimenu, topmenu)
+    else
+        buildSearchResultsMenuList(guimenu, topmenu, string.lower(str))
+    end
+    guiShowWindow(guimenu)
+end
+
+local function handleMenuDestroyed()
+    --print("Destroying main menu...")
+    for i,v in ipairs(keyhookGuiMenus) do
+        if i > 1 then
+            guiDestroyMenu(v)
+        end
+    end
+    keyhookGuiMenus = nil
+    keyhookRunning = false
+
+    runGarbageCollector()
+end
+
+local function launchGuiMenu(topmenu)
+    local guimenu = guiCreateTopLevelMenu("1pass",
+
+        function(guimenu, str) -- search text changed callback
+            return searchEntryChanged(guimenu, str, topmenu)
+        end,
+
+        function()  -- window destroyed callback
+            handleMenuDestroyed()
+        end
+    )
+    keyhookGuiMenus = {}
+    keyhookGuiMenus[#keyhookGuiMenus+1] = guimenu
+    buildGuiMenuList(guimenu, topmenu)
+    guiShowWindow(guimenu)
+end
 
 function keyhookPressed()  -- not local! Called from C!
---print("keyhookPressed: running==" .. tostring(keyhookRunning))
---    if keyhookRunning then
---        return
---    end
+    --print("keyhookPressed: running==" .. tostring(keyhookRunning))
+    if keyhookRunning then
+        return
+    end
 
     keyhookRunning = true
 
@@ -387,13 +526,13 @@
         return
     end
 
-    local topmenu = makeGuiMenu()
-    local favesmenu = makeGuiMenu()
+    local topmenu = makeMenu()
+    local favesmenu = makeMenu()
     faveitems = {}
 
-    setGuiMenuItemSubmenu(appendGuiMenuItem(topmenu, "Favorites"), favesmenu)
+    setMenuItemSubmenu(appendMenuItem(topmenu, "Favorites"), favesmenu)
 
-    appendGuiMenuItem(topmenu, "Lock keychain", function() lockKeychain() end)
+    appendMenuItem(topmenu, "Lock keychain", function() lockKeychain() end)
 
     for orderi,type in ipairs(passwordTypeOrdering) do
         local bucket = items[type]
@@ -402,13 +541,13 @@
             if realname == nil then
                 realname = type
             end
-            local menuitem = appendGuiMenuItem(topmenu, realname)
-            local submenu = makeGuiMenu()
+            local menuitem = appendMenuItem(topmenu, realname)
+            local submenu = makeMenu()
             table.sort(bucket, function(a, b) return a.name < b.name end)
             for i,v in pairs(bucket) do
                 build_secret_menuitems(v, submenu)
             end
-            setGuiMenuItemSubmenu(menuitem, submenu)
+            setMenuItemSubmenu(menuitem, submenu)
         else
             --print("no bucket found for item type '" .. type .. "'")
         end
@@ -433,16 +572,16 @@
 
     for i,v in favepairs(faveitems) do
         --dumptable("fave " .. i, v)
-        local menuitem = appendGuiMenuItem(favesmenu, v.info.name)
-        local submenu = makeGuiMenu()
+        local menuitem = appendMenuItem(favesmenu, v.info.name)
+        local submenu = makeMenu()
         secret_menuitem_builders[v.info.type](submenu, v.info, v.secure)
-        setGuiMenuItemSubmenu(menuitem, submenu)
+        setMenuItemSubmenu(menuitem, submenu)
     end
 
     favepairs = nil
     faveitems = nil
 
-    popupGuiMenu(topmenu)
+    launchGuiMenu(topmenu)
 end
 
 
--- a/keyhook.c	Tue Mar 31 20:18:10 2015 -0400
+++ b/keyhook.c	Sat Oct 17 20:24:39 2015 -0400
@@ -1,6 +1,8 @@
 // !!! FIXME: this is X11 specific.  :(
 
 #include <stdio.h>
+
+#if 1
 #include <stdlib.h>
 #include <string.h>
 #include <pthread.h>
@@ -130,5 +132,21 @@
     return sawKeyCombo;
 } // pumpKeyHook
 
+#else
+
+int initKeyHook(void) { return 1; }
+void deinitKeyHook(void) {}
+
+int pumpKeyHook(void)
+{
+static int x = 50;
+const int retval = (x == 50);
+if (++x > 50) x = 0;
+if (retval) printf("fire it!\n");
+return retval;
+}
+
+#endif
+
 // end of keyhook.c ...