i3/.config/i3/pass_typer.py

144 lines
4.3 KiB
Python
Executable File

#!/usr/bin/env python3
import dataclasses
import getpass
import os
import re
import subprocess
from collections.abc import Iterator
from functools import partial
from pathlib import Path
from typing import Optional
PASSWORD_STORE = Path(
os.environ.get("PASSWORD_STORE_DIR", Path("~/.password-store").expanduser())
)
cmd = partial(subprocess.run, capture_output=True, encoding="ascii")
@dataclasses.dataclass
class ServerMatch:
folder: Optional[str]
user: Optional[str]
host: str
@classmethod
def from_destination(cls, server_name: str) -> 'ServerMatch':
user, _, host = server_name.rpartition("@")
return cls(None, user if user != "" else None, host).alias()
def alias(self) -> 'ServerMatch':
# TODO: this still feels a bit hacky
host = self.host
if host.endswith(".sawtooth.claremontmakerspace.org"):
self.folder = "cms"
host = self.host.removesuffix(".sawtooth.claremontmakerspace.org")
if host == "salt":
self.folder = "cms"
self.host = "cms-net-svcs"
elif host.lower().startswith("cms-wall-display") or host.startswith("iPad"):
self.folder = "cms"
self.host = "ipads"
elif host in ("octopi-taz-6", "octopi-lulzbot-mini"):
self.folder = "cms"
self.host = "octopi"
elif re.match(".*-pidgey(.vpn)?", host):
self.folder = "tsrc"
self.host = "pidgey"
return self
def to_globs(self) -> Iterator[Path]:
path = Path("servers")
if self.folder is not None:
path = path / Path(self.folder)
else:
path = path / "**"
user_candidates = [None, getpass.getuser()]
if self.user is None:
user_candidates.append("*")
else:
user_candidates.insert(0, self.user)
# try host without each subdomain level
host_parts = self.host.split(".")
host_candidates = [
".".join(host_parts[:i]) for i in range(len(host_parts), 0, -1)
]
host_candidates.append(self.host + ".*")
# generate all combinations of user and host
for user in user_candidates:
for host in host_candidates:
if user is None:
yield path / f"{host}.gpg"
else:
yield path / f"{user}@{host}.gpg"
def to_paths(self) -> Iterator[Path]:
print("Candidate globs:")
for glob in self.to_globs():
print(glob)
for path in PASSWORD_STORE.glob(str(glob)):
if not path.parent.match("**/borg"):
yield path
def __str__(self) -> str:
folder = self.folder if self.folder is not None else '**'
if self.user is not None and self.user != "":
return f"{folder}/{self.user}@{self.host}"
else:
return f"{folder}/{self.host}"
def notify(summary: str, body: str) -> None:
subprocess.run(["notify-send", summary, body])
def rofi_select(options: list[str]) -> str:
options_str = "\n".join(options)
rofi = cmd(["rofi", "-dmenu"], input=options_str)
return rofi.stdout.strip()
def get_password(password_name: str) -> str:
pass_result = cmd(["pass", password_name])
password, _, _ = pass_result.stdout.partition("\n")
return password
def select_and_type(server_match: ServerMatch) -> None:
paths = server_match.to_paths()
file_list = list(
dict.fromkeys(str(f.relative_to(PASSWORD_STORE).with_suffix("")) for f in paths)
)
if len(file_list) == 0:
notify(f"No matches found for '{server_match}'", "")
return
selected = rofi_select(file_list)
if selected:
password = get_password(selected)
subprocess.run(["xdotool", "type", password + "\n"])
window_name = cmd(["xdotool", "getactivewindow", "getwindowname"]).stdout.strip()
ssh_match = re.search(
":([^ ]+=[^ ]* )*(mosh|ssh).* (?P<destination>[^ ]*)$", window_name
)
if ssh_match:
server_match = ServerMatch.from_destination(ssh_match.group("destination"))
notify(f"Matched server '{server_match}'", f"Window name: {window_name}")
select_and_type(server_match)
else:
notify("Window name did not match any rules", f"Window name: {window_name}")