[modules/vault] Add a new password vault module ("pass")
Add a new module that can be used to copy passwords from a password store into the clipboard. Currently, only "pass" is supported. As long as only bumblebee is used, it will also show which password is currently in the clipboard and how long it will still stay there.
This commit is contained in:
parent
6deb80edda
commit
451de4544c
3 changed files with 103 additions and 9 deletions
79
bumblebee/modules/vault.py
Normal file
79
bumblebee/modules/vault.py
Normal file
|
@ -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 = "<click-for-password>"
|
||||
|
||||
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
|
|
@ -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("<FocusOut>", self._on_focus_out)
|
||||
self._menu.bind("<Leave>", 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("<FocusOut>", self._on_focus_out)
|
||||
else:
|
||||
self._root = parent.root()
|
||||
self._root.withdraw()
|
||||
self._menu = tk.Menu(self._root, tearoff=0)
|
||||
self._menu.bind("<FocusOut>", self._on_focus_out)
|
||||
if leave:
|
||||
self._menu.bind("<Leave>", 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()
|
||||
|
|
BIN
screenshots/vault.png
Normal file
BIN
screenshots/vault.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
Loading…
Reference in a new issue