diff --git a/README.md b/README.md index c5ecca0..7ba12bb 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ The selected field is copied to the clipboard using `wl-copy` (Wayland) or `xcli ## History -Selections are saved to `$XDG_CACHE_HOME/bw-menu/history.json` (defaults to `~/.cache/bw-menu/history.json`), up to 10 entries. The most recent selection is at index 0. +Selections are saved to `$XDG_CACHE_HOME/bw-menu/history.json` (defaults to `~/.cache/bw-menu/history.json`), up to `max_history_entries` entries (default 5). The most recent selection is at index 0. ## Configuration @@ -88,6 +88,7 @@ Config file location: `$XDG_CONFIG_HOME/bw-menu/config.yaml` (or `config.yml`), ```yaml rbw_path: '~/.cargo/bin/rbw' +max_history_entries: 5 keybindings: password: 'Return' @@ -95,10 +96,11 @@ keybindings: totp: 'Shift+Return' ``` -| Key | Description | Default | -|----------------|----------------------------------------------------|---------| -| `rbw_path` | Path to the `rbw` binary (`~` is expanded) | `rbw` | -| `keybindings` | Map of vault fields to rofi key names (see below) | see above | +| Key | Description | Default | +|------------------------|----------------------------------------------------|------------| +| `rbw_path` | Path to the `rbw` binary (`~` is expanded) | `rbw` | +| `max_history_entries` | Maximum number of history entries to keep/display | `5` | +| `keybindings` | Map of vault fields to rofi key names (see below) | see above | ### `keybindings` @@ -110,6 +112,8 @@ The **first** entry is bound to rofi's accept key (`kb-accept-entry`). Additiona By default, `bw-menu` calls `rbw` from `$PATH`. If `rbw` is not on your PATH (e.g. when launching via `swaymsg exec`), set `rbw_path` to the full path of the binary. +**Note:** `rbw` must be unlocked before launching `bw-menu` from a window manager keybinding. `rbw unlock` requires pinentry, which cannot run without a terminal. Run `rbw unlock` manually in a terminal once after login — the session stays unlocked until the configured idle timeout. + ## Inspired by diff --git a/src/bw_menu/__main__.py b/src/bw_menu/__main__.py index 64dbbb0..69795c2 100644 --- a/src/bw_menu/__main__.py +++ b/src/bw_menu/__main__.py @@ -10,19 +10,19 @@ def main(): cfg = config.load() if args.command == "select": - _handle_select(args, cfg.rbw_path) + _handle_select(args, cfg.rbw_path, cfg.max_history_entries) elif args.command == "history": - _handle_history(args, cfg.rbw_path) + _handle_history(args, cfg.rbw_path, cfg.max_history_entries) -def _handle_select(args, rbw_path: str): +def _handle_select(args, rbw_path: str, max_history_entries: int): selector = create_selector(args.selector) - selection = selector.run(args, rbw_path) + selection = selector.run(args, rbw_path, max_history_entries) if selection is None: return value = rbw.get_field(selection.entry, selection.field, rbw_path) - history.add(selection.entry) + history.add(selection.entry, max_history_entries) if args.print_result: print(value) @@ -30,9 +30,9 @@ def _handle_select(args, rbw_path: str): clipboard.copy(value) -def _handle_history(args, rbw_path: str): +def _handle_history(args, rbw_path: str, max_history_entries: int): if args.history_command == "list": - for i, entry in enumerate(history.load()): + for i, entry in enumerate(history.load()[:max_history_entries]): print(f"{i}: {format_entry(entry)}") elif args.history_command == "get": diff --git a/src/bw_menu/config.py b/src/bw_menu/config.py index 20436ad..78bf15b 100644 --- a/src/bw_menu/config.py +++ b/src/bw_menu/config.py @@ -20,6 +20,7 @@ class Config: default_factory=lambda: dict(DEFAULT_KEYBINDINGS) ) rbw_path: str = "rbw" + max_history_entries: int = 5 def __config_path() -> Path | None: @@ -84,4 +85,19 @@ def load() -> Config: else: keybindings = parsed - return Config(keybindings=keybindings, rbw_path=rbw_path) + max_history_entries = 5 + if "max_history_entries" in data: + raw = data["max_history_entries"] + if isinstance(raw, int) and raw > 0: + max_history_entries = raw + else: + print( + f"Warning: invalid max_history_entries in {path}, using default", + file=sys.stderr, + ) + + return Config( + keybindings=keybindings, + rbw_path=rbw_path, + max_history_entries=max_history_entries, + ) diff --git a/src/bw_menu/history.py b/src/bw_menu/history.py index a4610c6..2bfe784 100644 --- a/src/bw_menu/history.py +++ b/src/bw_menu/history.py @@ -5,7 +5,7 @@ from pathlib import Path from bw_menu.rbw import Entry -MAX_ENTRIES = 10 +DEFAULT_MAX_ENTRIES = 5 def __history_path() -> Path: @@ -42,11 +42,11 @@ def __save(entries: list[Entry]): path.chmod(0o600) -def add(entry: Entry): +def add(entry: Entry, max_entries: int = DEFAULT_MAX_ENTRIES): entries = load() entries = [e for e in entries if e != entry] entries.insert(0, entry) - entries = entries[:MAX_ENTRIES] + entries = entries[:max_entries] __save(entries) diff --git a/src/bw_menu/selector/__init__.py b/src/bw_menu/selector/__init__.py index 2752ee4..ba935a3 100644 --- a/src/bw_menu/selector/__init__.py +++ b/src/bw_menu/selector/__init__.py @@ -32,9 +32,11 @@ def decode_info(info: str) -> Entry: return Entry(parts[0], parts[1], parts[2]) -def prepare_entries(rbw_path: str = "rbw") -> tuple[list[Entry], list[Entry]]: +def prepare_entries( + rbw_path: str = "rbw", max_history_entries: int = 5 +) -> tuple[list[Entry], list[Entry]]: all_entries = rbw.list_entries(rbw_path) - history_entries = history.load() + history_entries = history.load()[:max_history_entries] all_dict = {e: e for e in all_entries} history_in_list = [all_dict[e] for e in history_entries if e in all_dict] diff --git a/src/bw_menu/selector/fzf.py b/src/bw_menu/selector/fzf.py index 03f0279..a936a4b 100644 --- a/src/bw_menu/selector/fzf.py +++ b/src/bw_menu/selector/fzf.py @@ -10,8 +10,10 @@ from bw_menu.selector import ( class FzfSelector: - def run(self, args, rbw_path: str = "rbw") -> Selection | None: - history_in_list, remaining = prepare_entries(rbw_path) + def run( + self, args, rbw_path: str = "rbw", max_history_entries: int = 5 + ) -> Selection | None: + history_in_list, remaining = prepare_entries(rbw_path, max_history_entries) lines = [] for entry in history_in_list: diff --git a/src/bw_menu/selector/rofi.py b/src/bw_menu/selector/rofi.py index 8d8b125..7a9e8e5 100644 --- a/src/bw_menu/selector/rofi.py +++ b/src/bw_menu/selector/rofi.py @@ -28,7 +28,9 @@ _TYPE_ICONS: dict[str, str] = { class RofiSelector: - def run(self, args, rbw_path: str = "rbw") -> Selection | None: + def run( + self, args, rbw_path: str = "rbw", max_history_entries: int = 5 + ) -> Selection | None: cfg = config.load() keybindings = cfg.keybindings rofi_retv = os.environ.get("ROFI_RETV") @@ -40,7 +42,7 @@ class RofiSelector: rofi_retv = int(rofi_retv) if rofi_retv == 0: - self.__print_entries(keybindings, rbw_path) + self.__print_entries(keybindings, rbw_path, max_history_entries) return None fields = list(keybindings.keys()) @@ -118,8 +120,10 @@ class RofiSelector: subprocess.run(cmd) - def __print_entries(self, keybindings: dict[str, str], rbw_path: str): - history_in_list, remaining = prepare_entries(rbw_path) + def __print_entries( + self, keybindings: dict[str, str], rbw_path: str, max_history_entries: int + ): + history_in_list, remaining = prepare_entries(rbw_path, max_history_entries) print("\0prompt\x1fSelect item") print("\0no-custom\x1ftrue")