Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
__pycache__
.idea
78 changes: 63 additions & 15 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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/"))
Expand All @@ -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):
Expand Down Expand Up @@ -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",
Expand All @@ -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)
Expand All @@ -83,15 +119,15 @@ 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,
actions=[
Action(
"generate",
"Generate",
lambda: runDetachedProcess(["pass", "generate", "--clip", location, "20"]),
lambda: runDetachedProcess([self._pass_executable, "generate", "--clip", location, "20"]),
)
],
)
Expand All @@ -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]),
),
],
),
Expand All @@ -138,36 +174,48 @@ 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]),
),
],
),
)

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):
Expand Down
Binary file added gopass.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.