From 451de4544c0c7ab896a5197de29fb95f67a457d0 Mon Sep 17 00:00:00 2001 From: Tobias Witek Date: Sat, 6 Jul 2019 20:28:21 +0200 Subject: [PATCH] [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. --- bumblebee/modules/vault.py | 79 +++++++++++++++++++++++++++++++++++++ bumblebee/popup_v2.py | 33 +++++++++++----- screenshots/vault.png | Bin 0 -> 2267 bytes 3 files changed, 103 insertions(+), 9 deletions(-) create mode 100644 bumblebee/modules/vault.py create mode 100644 screenshots/vault.png 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 0000000000000000000000000000000000000000..be9d1b7190d4c75d0cc08c253cc83a27cb7238cb GIT binary patch literal 2267 zcmV<12qgE3P)+#a!O~$Ml&T%sZOTIiGz#o%#Ot&a>9@o8MaRGwWSzW-y9M${^tX z9-%+mfPnuA1a%+)Ku`w)*a+%C0Kk8?4$O_TtBc~Ri{kI!j+owRX{?i@e)bxRg?w$Is3`9xgn-8_S|F8M@RY%1 zH8yu$&AiW-$8QrGtH0bzJ!DMMLg^!dE`h;dY%KKseQldsXr6n>ZJmAWxZFbN1G}A8 z>#9vm(C-yhCSJZbF@g38F8}qcC>A?GryJQGoE+z=v)OX>U%hO02A}VJ^NySNnNW`n zrmFy;^iCuI#QdC{mG_w4WV?a1ZWXv*GcM)s zgC}*|^phg}>M5P7N{e*0RNjw_l2h(J{{1xoh>BwO_-@%~t}liYZEB&#Cg;4Obh1+d zytmkGB#Glh-wqD{BeOI+zk*wQy@~E73;p1eX#k+HRG!k(EkO{^&p5r#_5dIL-5gB4 zTrKc;@mqHv0RRR70DcIFwK83GV7K#jXS3Mk?B>><>0?A&4|G?kTs^;kn}f-bh^s-z zE^U6q3`c&>ld1YQUm~S zEIQrV?Kl7!I~*d}1apdDgPGnzf2Y{wY&*|0uc@7>=K_fmcutnPqe=F?C!0M^g@#`l z7#uF(lw|+~Q3w-5N{f0YE`cIyuH)NC-o~i+6PP^WlG^x!!h1(}1vK zd(Sh9^0GQwU%nwMDtqDN6JGYD{_jz{kMDPvmXe$@K(6b7w3Or}^4|2U(%1_*Q<~hJ zOqyD|ldjwYfWe`Wkke_SW1rCHaYoQ}Hkj!J?Xv&zKoYHIkR1u|-s0(MIa$FcD6X!t zgPXp$o8{%3#bp)s01%&&yH;P*ej|zV$aqjuS5egn02C^Xr+!^SJ9`bNeA!%H*=V<@DtdMZdY_ zOXs%EK6RDP`%oLp)h=7sDay+r2!azAds;;S01A~Bm5@n3v$Lyb;Mc-udBx9w!Q>W4 z5QN&+qoypsQd6n;;mbAp%i6m7JG$QR;a^E{0lNqQU|?wE?a(j=gPGhxTTAJ1pv&OU zNJPS|DY1$_A z$}Rkz9eg)D`fhl1#{Sb`PXWUGvMpu(`yQ3SU` zdLxwv01=0Oz>0hxPF~To!qO@e!xbJ|&A)Rt_1r^dSNV+b=GJZvRe6;q3lmavgZ&&C z3`RTc4Ilmy7J|ioxuGoPmk#sh5`uQxuQOOqPPv;`^lbV7d7KxaB;n}-6nsw`Q^hd7hTYU$|J*HPs}^mSAz z%{2b3YN;=obiKH_wHE-KYz!}6D}3G5#bO}{0-VFDw3H+PkE1h~C68)$>`xF87BSFO zLy1$UJ=)8aKYSd2-PE;MURFa@p`)vx5C7D*-eszv7YzIyoU9CyJ-2QSCOMan0w7Ml z5i=G6%<|m$iqh%ff9|*9=kAUs0B|Gw;X>KDr$ar|ROED4C?`kz8|bOeMrR5e1AJ^{rHMI(Px;UR0Fs`%n}f+>d0B7w^(&Sw zzFSZM00V;~^JR$a4qmR73UV^+YhU;EF(-UAR23u$;s(0vbBXwt_CAz&b8EMS(aM_o zRs=z+>s!s%uI%g{;KTpT+(+NLm@CLh3kwVF^4N$V005$**ll*k`%k2vh`ByIGR7^J z$GMRMlgYZ8d7pihe>XhpvoAhmx6`G#AO?d`aKF0tbvpn!ZC-mg&;^6R2nk^-3&;Rq z?tFN3>=RKEUwkVZ0EC5wDhtT>AJ>G2UqR8COjKf4dSY;>|JKv78MCy?p`f%{94B@- z&_z~SvaYfH$B;yni9qLiP8ms0BWR~x2kmWO*oD5g?*Sm{eCCmTt`hHsjWvuM5L;g3ILTaC@VFWeEc-dhyT*@+66Mi zOXMI7!1e61>bjOW;y5~;;kqk|y-`I0Bl!OTPm&`2p4U(>-zfgGjr