From de065032856db9263c8c1b33bf9c566d33437ed6 Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Sat, 17 Oct 2015 20:24:39 -0400 Subject: [PATCH] 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 | 247 ++++++++++++++++++++++++++++++++++++++++++++---------- 1pass.lua | 183 +++++++++++++++++++++++++++++++++++----- keyhook.c | 18 ++++ 3 files changed, 384 insertions(+), 64 deletions(-) diff --git a/1pass.c b/1pass.c index 56125aa..691efe7 100644 --- a/1pass.c +++ b/1pass.c @@ -17,7 +17,13 @@ #include "base64.h" #include "md5.h" #include "keyhook.h" + #include +#include +#include +#include +#include +#include #define STATICARRAYLEN(x) ( (sizeof ((x))) / (sizeof ((x)[0])) ) @@ -184,6 +190,13 @@ static lua_State *luaState = NULL; 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 @@ static int runGuiPasswordPrompt(lua_State *L) 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 @@ static int copyToClipboard(lua_State *L) gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD), str, -1); } // copyToClipboard - -static int makeGuiMenu(lua_State *L) +static gboolean checkForEscapeKey(GtkWidget *widget, GdkEvent *event, gpointer arg) { - return retvalPointer(L, gtk_menu_new()); -} // makeGuiMenu + 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 + return FALSE; // pass this to other event handlers. +} // checkForEscapeKey -static void clickedMenuItem(void *arg) +static gboolean wasSearchDeleteText = FALSE; // HACK to workaround gtk+ nonsense. +static void searchChanged(GtkEditable *editable, gpointer arg) { - // This is the callback from GTK+; now call into our actual Lua callback! const int callback = (int) ((size_t)arg); + GtkWidget *vbox = gtk_widget_get_parent(GTK_WIDGET(editable)); lua_rawgeti(luaState, LUA_REGISTRYINDEX, callback); - lua_call(luaState, 0, 0); -} // clickedMenuItem + 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 -#if 0 // !!! FIXME: figure out how to fire this. -static void deletedMenuItem(void *arg) +// HACK to workaround gtk+ nonsense. +static void searchDeleteText(GtkEditable *editable, gint start_pos, gint end_pos, gpointer user_data) { - // Clean up the Lua function we referenced in the Registry. - const int callback = (int) ((size_t)arg); -printf("unref callback %d\n", callback); + wasSearchDeleteText = TRUE; +} // searchDeleteText + +static void destroyLuaCallback(const int callback) +{ + //printf("unref callback %d\n", callback); luaL_unref(luaState, LUA_REGISTRYINDEX, callback); -} // deletedMenuItem +} // 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 appendGuiMenuItem(lua_State *L) +static int guiCreateTopLevelMenu(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); + const char *title = luaL_checkstring(L, 1); + const int changedCallback = makeLuaCallback(L, 2); + const int destroyedCallback = makeLuaCallback(L, 3); - if ((argc >= 3) && (!lua_isnil(L, 3))) - { - 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 + 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 + +static int guiAddMenuItem(lua_State *L) +{ + 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)); + + //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); - gtk_menu_append(menu, item); return retvalPointer(L, item); -} // appendGuiMenuItem - +} // guiAddMenuItem -static int setGuiMenuItemSubmenu(lua_State *L) +static int guiRemoveAllMenuItems(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 *vbox = (GtkWidget *) lua_touserdata(L, 1); + GList *children = gtk_container_get_children(GTK_CONTAINER(vbox)); + GList *iter; + + gtk_widget_hide(vbox); + for (iter = children; iter != NULL; iter = g_list_next(iter)) + { + if (G_OBJECT_TYPE(iter->data) == GTK_TYPE_BUTTON) + gtk_widget_destroy(GTK_WIDGET(iter->data)); + } // for + g_list_free(children); + return 0; -} // setGuiMenuItemSubmenu +} // 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 popupGuiMenu(lua_State *L) +static int guiShowWindow(lua_State *L) { - GtkMenu *menu = (GtkMenu *) lua_touserdata(L, 1); - gtk_menu_popup(menu, NULL, NULL, NULL, NULL, 0, gtk_get_current_event_time()); + 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; -} // popupGuiMenu +} // 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); + + 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 @@ static int initLua(const int argc, char **argv) // 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; diff --git a/1pass.lua b/1pass.lua index 7b5f580..127385b 100644 --- a/1pass.lua +++ b/1pass.lua @@ -6,6 +6,16 @@ local password = argv[2] 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 @@ local function loadContents() 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 function build_secret_menuitem(menu, type, str, hidden) 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 @@ local function build_secret_menuitems(info, menu) 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 @@ local function build_secret_menuitems(info, menu) 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 @@ local function lockKeychain() 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 @@ function pumpLua() -- not local! Called from C! 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 @@ function keyhookPressed() -- not local! Called from C! 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 @@ function keyhookPressed() -- not local! Called from C! 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 @@ function keyhookPressed() -- not local! Called from C! 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 diff --git a/keyhook.c b/keyhook.c index 4e1d6a7..5f2653d 100644 --- a/keyhook.c +++ b/keyhook.c @@ -1,6 +1,8 @@ // !!! FIXME: this is X11 specific. :( #include + +#if 1 #include #include #include @@ -130,5 +132,21 @@ int pumpKeyHook(void) 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 ...