diff --git a/bumblebee/modules/vault.py b/bumblebee/modules/vault.py new file mode 100644 index 0000000..1b7ac03 --- /dev/null +++ b/bumblebee/modules/vault.py @@ -0,0 +1,79 @@ +# pylint: disable=C0111,R0903 + +"""Copy passwords from a password store into the clipboard (currently supports only "pass") + +Parameters: + * vault.duration: Duration until password is cleared from clipboard (defaults to 30) + * vault.location: Location of the password store (defaults to ~/.password-store) + * vault.offx: x-axis offset of popup menu (defaults to 0) + * vault.offy: y-axis offset of popup menu (defaults to 0) +""" + + +# TODO: +# - support multiple backends by abstracting the menu structure into a tree +# - build the menu and the actions based on that abstracted tree +# + +import os +import time +import threading +import bumblebee.util +import bumblebee.popup_v2 +import bumblebee.input +import bumblebee.output +import bumblebee.engine + +def build_menu(parent, current_directory, callback): + with os.scandir(current_directory) as it: + for entry in it: + if entry.name.startswith("."): continue + if entry.is_file(): + name = entry.name[:entry.name.rfind(".")] + parent.add_menuitem(name, callback=lambda : callback(os.path.join(current_directory, name))) + + else: + submenu = bumblebee.popup_v2.PopupMenu(parent, leave=False) + build_menu(submenu, os.path.join(current_directory, entry.name), callback) + parent.add_cascade(entry.name, submenu) + +class Module(bumblebee.engine.Module): + def __init__(self, engine, config): + super(Module, self).__init__(engine, config, + bumblebee.output.Widget(full_text=self.text) + ) + self._duration = int(self.parameter("duration", 30)) + self._offx = int(self.parameter("offx", 0)) + self._offy = int(self.parameter("offy", 0)) + self._path = os.path.expanduser(self.parameter("location", "~/.password-store/")) + self._reset() + engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, + cmd=self.popup) + + def popup(self, widget): + menu = bumblebee.popup_v2.PopupMenu(leave=False) + + build_menu(menu, self._path, self._callback) + menu.show(widget, offset_x=self._offx, offset_y=self._offy) + + def _reset(self): + self._timer = None + self._text = "" + + def _callback(self, secret_name): + secret_name = secret_name.replace(self._path, "") # remove common path + if self._timer: + self._timer.cancel() + # bumblebee.util.execute hangs for some reason + os.system("PASSWORD_STORE_CLIP_TIME={} pass -c {} > /dev/null 2>&1".format(self._duration, secret_name)) + self._timer = threading.Timer(self._duration, self._reset) + self._timer.start() + self._start = int(time.time()) + self._text = secret_name + + def text(self, widget): + if self._timer: + return "{} ({}s)".format(self._text, self._duration - (int(time.time()) - self._start)) + return self._text + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/bumblebee/popup_v2.py b/bumblebee/popup_v2.py index 7d0ad50..4d08c73 100644 --- a/bumblebee/popup_v2.py +++ b/bumblebee/popup_v2.py @@ -13,14 +13,27 @@ except ImportError: import functools - class PopupMenu(object): - def __init__(self): - self._root = tk.Tk() - self._root.withdraw() - self._menu = tk.Menu(self._root) - self._menu.bind("", self._on_focus_out) - self._menu.bind("", self._on_focus_out) + def __init__(self, parent=None, leave=True): + + if not parent: + self._root = tk.Tk() + self._root.withdraw() + self._menu = tk.Menu(self._root, tearoff=0) + self._menu.bind("", self._on_focus_out) + else: + self._root = parent.root() + self._root.withdraw() + self._menu = tk.Menu(self._root, tearoff=0) + self._menu.bind("", self._on_focus_out) + if leave: + self._menu.bind("", self._on_focus_out) + + def root(self): + return self._root + + def menu(self): + return self._menu def _on_focus_out(self, event=None): self._root.destroy() @@ -29,13 +42,15 @@ class PopupMenu(object): self._root.destroy() callback() + def add_cascade(self, menuitem, submenu): + self._menu.add_cascade(label=menuitem, menu=submenu.menu()) def add_menuitem(self, menuitem, callback): self._menu.add_command(label=menuitem, command=functools.partial(self._on_click, callback)) - def show(self, event): + def show(self, event, offset_x=0, offset_y=0): try: - self._menu.tk_popup(event['x'], event['y']) + self._menu.tk_popup(event['x'] + offset_x, event['y'] + offset_y) finally: self._menu.grab_release() self._root.mainloop() diff --git a/screenshots/vault.png b/screenshots/vault.png new file mode 100644 index 0000000..be9d1b7 Binary files /dev/null and b/screenshots/vault.png differ