#!/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[^ ]*)$", 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}")