diff --git a/.gitignore b/.gitignore index bee8a64..083d732 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ __pycache__ +.idea diff --git a/__init__.py b/__init__.py index 8e25e71..38e59d4 100644 --- a/__init__.py +++ b/__init__.py @@ -2,9 +2,14 @@ # Copyright (c) 2017 Benedict Dudel # Copyright (c) 2023 Max # Copyright (c) 2023 Pete-Hamlin +# Copyright (c) 2025 poettig import fnmatch import os +import shutil +import subprocess +from pathlib import Path + from albert import * md_iid = "4.0" @@ -13,9 +18,8 @@ md_description = "Manage passwords in pass" md_license = "BSD-3" md_url = "https://github.com/albertlauncher/albert-plugin-python-pass" -md_authors = ["@benedictdudel", "@maxmil", "@Pete-Hamlin", "@okaestne"] +md_authors = ["@benedictdudel", "@maxmil", "@Pete-Hamlin", "@okaestne", "@poettig"] md_maintainers = ["@maxmil", "@okaestne", "@Pete-Hamlin"] -md_bin_dependencies = ["pass"] HOME_DIR = os.environ["HOME"] PASS_DIR = os.environ.get("PASSWORD_STORE_DIR", os.path.join(HOME_DIR, ".password-store/")) @@ -25,12 +29,30 @@ class Plugin(PluginInstance, TriggerQueryHandler): def __init__(self): PluginInstance.__init__(self) TriggerQueryHandler.__init__(self) + self._use_gopass = self.readConfig("use_gopass", bool) or False self._use_otp = self.readConfig("use_otp", bool) or False self._otp_glob = self.readConfig("otp_glob", str) or "*-otp.gpg" + self._pass_executable = "gopass" if self._use_gopass else "pass" - @staticmethod - def makeIcon(): - return makeThemeIcon("dialog-password") + def makeIcon(self): + if self._use_gopass: + return makeImageIcon(Path(__file__).parent / "gopass.png") + else: + return makeThemeIcon("dialog-password") + + @property + def use_gopass(self) -> str: + return self._use_gopass + + @use_gopass.setter + def use_gopass(self, value) -> None: + print(f"Setting _use_gopass to {value}") + self._use_gopass = value + self.writeConfig("use_gopass", value) + + pass_executable = "gopass" if self._use_gopass else "pass" + print(f"Setting _pass_executable to {pass_executable}") + self._pass_executable = pass_executable @property def use_otp(self): @@ -60,6 +82,11 @@ def synopsis(self, query): def configWidget(self): return [ + { + "type": "checkbox", + "property": "use_gopass", + "label": "Use GoPass instead of pass", + }, {"type": "checkbox", "property": "use_otp", "label": "Enable pass OTP extension"}, { "type": "lineedit", @@ -70,7 +97,16 @@ def configWidget(self): ] def handleTriggerQuery(self, query): - if query.string.strip().startswith("generate"): + if not shutil.which(self._pass_executable): + query.add( + StandardItem( + id="executable_not_found", + icon_factory=lambda: Plugin.makeIcon(self), + text=f"{self._pass_executable} not found in $PATH", + subtext=f"Please check if {self._pass_executable} is properly installed." + ) + ) + elif query.string.strip().startswith("generate"): self.generatePassword(query) elif query.string.strip().startswith("otp") and self._use_otp: self.showOtp(query) @@ -83,7 +119,7 @@ def generatePassword(self, query): query.add( StandardItem( id="generate_password", - icon_factory=Plugin.makeIcon, + icon_factory=lambda: Plugin.makeIcon(self), text="Generate a new password", subtext="The new password will be located at %s" % location, input_action_text="pass %s" % query.string, @@ -91,7 +127,7 @@ def generatePassword(self, query): Action( "generate", "Generate", - lambda: runDetachedProcess(["pass", "generate", "--clip", location, "20"]), + lambda: runDetachedProcess([self._pass_executable, "generate", "--clip", location, "20"]), ) ], ) @@ -110,14 +146,14 @@ def showOtp(self, query): results.append( StandardItem( id=password, - icon_factory=Plugin.makeIcon, + icon_factory=lambda: Plugin.makeIcon(self), text=password.split("/")[-1], subtext=password, actions=[ Action( "copy", "Copy", - lambda pwd=password: runDetachedProcess(["pass", "otp", "--clip", pwd]), + lambda pwd=password: runDetachedProcess([self._pass_executable, "otp", "--clip", pwd]), ), ], ), @@ -138,23 +174,23 @@ def showPasswords(self, query): id=password, text=name, subtext=password, - icon_factory=Plugin.makeIcon, + icon_factory=lambda: Plugin.makeIcon(self), input_action_text="pass %s" % password, actions=[ Action( "copy", "Copy", - lambda pwd=password: runDetachedProcess(["pass", "--clip", pwd]), + lambda pwd=password: runDetachedProcess([self._pass_executable, "--clip", pwd]), ), Action( "edit", "Edit", - lambda pwd=password: runDetachedProcess(["pass", "edit", pwd]), + lambda pwd=password: runDetachedProcess([self._pass_executable, "edit", pwd]), ), Action( "remove", "Remove", - lambda pwd=password: runDetachedProcess(["pass", "rm", "--force", pwd]), + lambda pwd=password: runDetachedProcess([self._pass_executable, "rm", "--force", pwd]), ), ], ), @@ -162,12 +198,24 @@ def showPasswords(self, query): query.add(results) - def getPasswords(self, otp=False): + def getPasswordsFromGoPass(self) -> list: + p = subprocess.run([self._pass_executable, "list", "--flat"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, encoding="utf-8") + return p.stdout.splitlines() + + def getPasswordsFromPass(self, otp=False) -> list: passwords = [] for root, dirnames, filenames in os.walk(PASS_DIR, followlinks=True): for filename in fnmatch.filter(filenames, self._otp_glob if otp else "*.gpg"): passwords.append(os.path.join(root, filename.replace(".gpg", "")).replace(PASS_DIR, "")) + return passwords + + def getPasswords(self, otp=False): + if self.use_gopass: + passwords = self.getPasswordsFromGoPass() + else: + passwords = self.getPasswordsFromPass(otp) + return sorted(passwords, key=lambda s: s.lower()) def getPasswordsFromSearch(self, otp_query, otp=False): diff --git a/gopass.png b/gopass.png new file mode 100644 index 0000000..0d6446d Binary files /dev/null and b/gopass.png differ