diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a3382b4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,12 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.6 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format + + - repo: https://github.com/RobertCraigie/pyright-python + rev: v1.1.408 + hooks: + - id: pyright diff --git a/CLAUDE.md b/CLAUDE.md index c72ae90..d31a783 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,9 +12,10 @@ bw-menu is a rofi/fzf frontend for Bitwarden via the `rbw` CLI. It lets users in uv sync # install dependencies and set up virtualenv bw-menu select # run with rofi (default) bw-menu select --selector fzf # run with fzf +uv run pre-commit run --all-files # run linting/formatting/type-checking ``` -There are no tests or linting configured. +Linting (ruff), formatting (ruff-format), and type checking (pyright) are configured via pre-commit. No tests. ## Architecture diff --git a/pyproject.toml b/pyproject.toml index 86e09ee..5f3d3e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,5 +8,16 @@ version = "0.1.0" requires-python = ">=3.10" dependencies = ["pyyaml"] +[dependency-groups] +dev = ["pre-commit", "pyright"] + [project.scripts] bw-menu = "bw_menu.__main__:main" + +[tool.ruff] +target-version = "py310" +src = ["src"] + +[tool.pyright] +venvPath = "." +venv = ".venv" diff --git a/src/bw_menu/cli.py b/src/bw_menu/cli.py index a69140f..cb824ee 100644 --- a/src/bw_menu/cli.py +++ b/src/bw_menu/cli.py @@ -4,13 +4,17 @@ from importlib.metadata import version def parse_args(): parser = argparse.ArgumentParser(prog="bw-menu") - parser.add_argument("--version", action="version", version=f"%(prog)s {version('bw-menu')}") + parser.add_argument( + "--version", action="version", version=f"%(prog)s {version('bw-menu')}" + ) subparsers = parser.add_subparsers(dest="command", required=True) # select subcommand select_parser = subparsers.add_parser("select") select_parser.add_argument("--selector", default="rofi", choices=["rofi", "fzf"]) - select_parser.add_argument("--field", default="password", choices=["username", "password", "totp"]) + select_parser.add_argument( + "--field", default="password", choices=["username", "password", "totp"] + ) select_parser.add_argument("--print", dest="print_result", action="store_true") # rofi script-mode passes the selected title as a positional arg — accept and ignore it select_parser.add_argument("_rofi_arg", nargs="?", help=argparse.SUPPRESS) @@ -23,7 +27,9 @@ def parse_args(): get_parser = history_sub.add_parser("get") get_parser.add_argument("index", type=int) - get_parser.add_argument("--field", default="password", choices=["username", "password", "totp"]) + get_parser.add_argument( + "--field", default="password", choices=["username", "password", "totp"] + ) get_parser.add_argument("--print", dest="print_result", action="store_true") return parser.parse_args() diff --git a/src/bw_menu/config.py b/src/bw_menu/config.py index 9fd6756..4224e9f 100644 --- a/src/bw_menu/config.py +++ b/src/bw_menu/config.py @@ -12,7 +12,9 @@ VALID_FIELDS = {"password", "username", "totp"} @dataclass class Config: - keybindings: dict[str, str] = field(default_factory=lambda: dict(DEFAULT_KEYBINDINGS)) + keybindings: dict[str, str] = field( + default_factory=lambda: dict(DEFAULT_KEYBINDINGS) + ) def __config_path() -> Path: @@ -28,17 +30,28 @@ def load() -> Config: try: data = yaml.safe_load(path.read_text()) if not isinstance(data, dict) or "keybindings" not in data: - print(f"Warning: config file {path} has invalid structure, using defaults", file=sys.stderr) + print( + f"Warning: config file {path} has invalid structure, using defaults", + file=sys.stderr, + ) return Config() kb = data["keybindings"] if not isinstance(kb, dict) or not kb: - print(f"Warning: config file {path} has invalid keybindings, using defaults", file=sys.stderr) + print( + f"Warning: config file {path} has invalid keybindings, using defaults", + file=sys.stderr, + ) return Config() keybindings = {str(k): str(v) for k, v in kb.items()} invalid = {v for v in keybindings.values() if v not in VALID_FIELDS} if invalid: - raise ValueError(f"Invalid keybinding field(s): {', '.join(sorted(invalid))}. Valid fields: {', '.join(sorted(VALID_FIELDS))}") + raise ValueError( + f"Invalid keybinding field(s): {', '.join(sorted(invalid))}. Valid fields: {', '.join(sorted(VALID_FIELDS))}" + ) return Config(keybindings=keybindings) except yaml.YAMLError: - print(f"Warning: config file {path} could not be parsed, using defaults", file=sys.stderr) + print( + f"Warning: config file {path} could not be parsed, using defaults", + file=sys.stderr, + ) return Config() diff --git a/src/bw_menu/history.py b/src/bw_menu/history.py index 6a9c378..37014b5 100644 --- a/src/bw_menu/history.py +++ b/src/bw_menu/history.py @@ -27,7 +27,12 @@ def load() -> list[Entry]: def __save(entries: list[Entry]): path = __history_path() path.parent.mkdir(parents=True, exist_ok=True) - data = {"entries": [{"name": e.name, "folder": e.folder, "username": e.username} for e in entries]} + data = { + "entries": [ + {"name": e.name, "folder": e.folder, "username": e.username} + for e in entries + ] + } path.write_text(json.dumps(data)) path.chmod(0o600) diff --git a/src/bw_menu/rbw.py b/src/bw_menu/rbw.py index a4f5b01..a3e8e09 100644 --- a/src/bw_menu/rbw.py +++ b/src/bw_menu/rbw.py @@ -20,7 +20,9 @@ def list_entries() -> list[Entry]: sys.exit(2) data = json.loads(result.stdout.strip()) - return [Entry(item["name"], item["folder"] or "", item["user"] or "") for item in data] + return [ + Entry(item["name"], item["folder"] or "", item["user"] or "") for item in data + ] def get_field(entry: Entry, field: str) -> str: diff --git a/src/bw_menu/selector/fzf.py b/src/bw_menu/selector/fzf.py index 39d99c5..ddec76c 100644 --- a/src/bw_menu/selector/fzf.py +++ b/src/bw_menu/selector/fzf.py @@ -1,6 +1,12 @@ import subprocess -from bw_menu.selector import Selection, format_entry, encode_info, decode_info, prepare_entries +from bw_menu.selector import ( + Selection, + format_entry, + encode_info, + decode_info, + prepare_entries, +) class FzfSelector: diff --git a/src/bw_menu/selector/rofi.py b/src/bw_menu/selector/rofi.py index cd75762..81736d0 100644 --- a/src/bw_menu/selector/rofi.py +++ b/src/bw_menu/selector/rofi.py @@ -4,7 +4,13 @@ import sys from pathlib import Path from bw_menu import config -from bw_menu.selector import Selection, format_entry, encode_info, decode_info, prepare_entries +from bw_menu.selector import ( + Selection, + format_entry, + encode_info, + decode_info, + prepare_entries, +) class RofiSelector: @@ -48,10 +54,14 @@ class RofiSelector: cmd = [ "rofi", - "-show", "bw", - "-modes", f"bw:{script} select --selector rofi", - "-kb-accept-entry", keys[0], - "-kb-accept-custom", "", + "-show", + "bw", + "-modes", + f"bw:{script} select --selector rofi", + "-kb-accept-entry", + keys[0], + "-kb-accept-custom", + "", ] for i, key in enumerate(keys[1:], start=1): cmd.extend([f"-kb-custom-{i}", key]) @@ -61,8 +71,8 @@ class RofiSelector: def __print_entries(self, keybindings: dict[str, str]): history_in_list, remaining = prepare_entries() - print(f"\0prompt\x1fSelect item") - print(f"\0no-custom\x1ftrue") + print("\0prompt\x1fSelect item") + print("\0no-custom\x1ftrue") hints = [] for key, field in keybindings.items(): @@ -72,7 +82,9 @@ class RofiSelector: for entry in history_in_list: display = format_entry(entry) - print(f"{display}\0info\x1f{encode_info(entry)}\x1ficon\x1fdocument-open-recent") + print( + f"{display}\0info\x1f{encode_info(entry)}\x1ficon\x1fdocument-open-recent" + ) for entry in remaining: display = format_entry(entry) diff --git a/uv.lock b/uv.lock index 9d0aa8a..42c0cfd 100644 --- a/uv.lock +++ b/uv.lock @@ -10,9 +10,104 @@ dependencies = [ { name = "pyyaml" }, ] +[package.dev-dependencies] +dev = [ + { name = "pre-commit" }, + { name = "pyright" }, +] + [package.metadata] requires-dist = [{ name = "pyyaml" }] +[package.metadata.requires-dev] +dev = [ + { name = "pre-commit" }, + { name = "pyright" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "filelock" +version = "3.24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/a8/dae62680be63cbb3ff87cfa2f51cf766269514ea5488479d42fec5aa6f3a/filelock-3.24.2.tar.gz", hash = "sha256:c22803117490f156e59fafce621f0550a7a853e2bbf4f87f112b11d469b6c81b", size = 37601, upload-time = "2026-02-16T02:50:45.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/04/a94ebfb4eaaa08db56725a40de2887e95de4e8641b9e902c311bfa00aa39/filelock-3.24.2-py3-none-any.whl", hash = "sha256:667d7dc0b7d1e1064dd5f8f8e80bdac157a6482e8d2e02cd16fd3b6b33bd6556", size = 24152, upload-time = "2026-02-16T02:50:44Z" }, +] + +[[package]] +name = "identify" +version = "2.6.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/8d/e8b97e6bd3fb6fb271346f7981362f1e04d6a7463abd0de79e1fda17c067/identify-2.6.16.tar.gz", hash = "sha256:846857203b5511bbe94d5a352a48ef2359532bc8f6727b5544077a0dcfb24980", size = 99360, upload-time = "2026-01-12T18:58:58.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.408" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -76,3 +171,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c1/ef/d9d4ce633df789bf3430bd81fb0d8b9d9465dfc1d1f0deb3fb62cd80f5c2/virtualenv-20.37.0.tar.gz", hash = "sha256:6f7e2064ed470aa7418874e70b6369d53b66bcd9e9fd5389763e96b6c94ccb7c", size = 5864710, upload-time = "2026-02-16T16:17:59.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/4b/6cf85b485be7ec29db837ec2a1d8cd68bc1147b1abf23d8636c5bd65b3cc/virtualenv-20.37.0-py3-none-any.whl", hash = "sha256:5d3951c32d57232ae3569d4de4cc256c439e045135ebf43518131175d9be435d", size = 5837480, upload-time = "2026-02-16T16:17:57.341Z" }, +]