Add pre-commit with ruff, ruff-format, and pyright

This commit is contained in:
Navid Sassan 2026-02-17 20:33:58 +01:00
parent d621ebb81e
commit 487f2caf39
10 changed files with 207 additions and 20 deletions

12
.pre-commit-config.yaml Normal file
View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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:

View File

@ -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:

View File

@ -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)

119
uv.lock generated
View File

@ -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" },
]