Add configurable rbw_path option
When launching bw-menu via swaymsg exec, ~/.cargo/bin is not on PATH, so the hardcoded "rbw" subprocess calls fail. The new rbw_path config option lets users point to the full path of their rbw binary. Tilde expansion is supported. The config loader now treats each key independently so a config with only rbw_path (no keybindings) works.
This commit is contained in:
parent
c1137b30e3
commit
fc8af3bd24
19
README.md
19
README.md
@ -87,16 +87,29 @@ Selections are saved to `$XDG_CACHE_HOME/bw-menu/history.json` (defaults to `~/.
|
|||||||
Config file location: `$XDG_CONFIG_HOME/bw-menu/config.yaml` (or `config.yml`), defaults to `~/.config/bw-menu/config.yaml`.
|
Config file location: `$XDG_CONFIG_HOME/bw-menu/config.yaml` (or `config.yml`), defaults to `~/.config/bw-menu/config.yaml`.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
rbw_path: '~/.cargo/bin/rbw'
|
||||||
|
|
||||||
keybindings:
|
keybindings:
|
||||||
password: Return
|
password: 'Return'
|
||||||
username: Control+Return
|
username: 'Control+Return'
|
||||||
totp: Shift+Return
|
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 |
|
||||||
|
|
||||||
|
### `keybindings`
|
||||||
|
|
||||||
Each entry maps a vault field to a [rofi key name](https://davatorium.github.io/rofi/current/rofi-keys.5/). Available fields: `password`, `username`, `totp`, `name`.
|
Each entry maps a vault field to a [rofi key name](https://davatorium.github.io/rofi/current/rofi-keys.5/). Available fields: `password`, `username`, `totp`, `name`.
|
||||||
|
|
||||||
The **first** entry is bound to rofi's accept key (`kb-accept-entry`). Additional entries become custom keybindings (`kb-custom-1`, `kb-custom-2`, …).
|
The **first** entry is bound to rofi's accept key (`kb-accept-entry`). Additional entries become custom keybindings (`kb-custom-1`, `kb-custom-2`, …).
|
||||||
|
|
||||||
|
### `rbw_path`
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
|
||||||
## Inspired by
|
## Inspired by
|
||||||
|
|
||||||
|
|||||||
@ -1,26 +1,27 @@
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
from bw_menu import rbw, history, clipboard
|
from bw_menu import rbw, history, clipboard, config
|
||||||
from bw_menu.cli import parse_args
|
from bw_menu.cli import parse_args
|
||||||
from bw_menu.selector import format_entry, create_selector
|
from bw_menu.selector import format_entry, create_selector
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
|
cfg = config.load()
|
||||||
|
|
||||||
if args.command == "select":
|
if args.command == "select":
|
||||||
_handle_select(args)
|
_handle_select(args, cfg.rbw_path)
|
||||||
elif args.command == "history":
|
elif args.command == "history":
|
||||||
_handle_history(args)
|
_handle_history(args, cfg.rbw_path)
|
||||||
|
|
||||||
|
|
||||||
def _handle_select(args):
|
def _handle_select(args, rbw_path: str):
|
||||||
selector = create_selector(args.selector)
|
selector = create_selector(args.selector)
|
||||||
selection = selector.run(args)
|
selection = selector.run(args, rbw_path)
|
||||||
if selection is None:
|
if selection is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
value = rbw.get_field(selection.entry, selection.field)
|
value = rbw.get_field(selection.entry, selection.field, rbw_path)
|
||||||
history.add(selection.entry)
|
history.add(selection.entry)
|
||||||
|
|
||||||
if args.print_result:
|
if args.print_result:
|
||||||
@ -29,7 +30,7 @@ def _handle_select(args):
|
|||||||
clipboard.copy(value)
|
clipboard.copy(value)
|
||||||
|
|
||||||
|
|
||||||
def _handle_history(args):
|
def _handle_history(args, rbw_path: str):
|
||||||
if args.history_command == "list":
|
if args.history_command == "list":
|
||||||
for i, entry in enumerate(history.load()):
|
for i, entry in enumerate(history.load()):
|
||||||
print(f"{i}: {format_entry(entry)}")
|
print(f"{i}: {format_entry(entry)}")
|
||||||
@ -40,7 +41,7 @@ def _handle_history(args):
|
|||||||
print(f"No history entry at index {args.index}", file=sys.stderr)
|
print(f"No history entry at index {args.index}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
value = rbw.get_field(entry, args.field)
|
value = rbw.get_field(entry, args.field, rbw_path)
|
||||||
if args.print_result:
|
if args.print_result:
|
||||||
print(value)
|
print(value)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -19,6 +19,7 @@ class Config:
|
|||||||
keybindings: dict[str, str] = field(
|
keybindings: dict[str, str] = field(
|
||||||
default_factory=lambda: dict(DEFAULT_KEYBINDINGS)
|
default_factory=lambda: dict(DEFAULT_KEYBINDINGS)
|
||||||
)
|
)
|
||||||
|
rbw_path: str = "rbw"
|
||||||
|
|
||||||
|
|
||||||
def __config_path() -> Path | None:
|
def __config_path() -> Path | None:
|
||||||
@ -38,32 +39,49 @@ def load() -> Config:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
data = yaml.safe_load(path.read_text())
|
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,
|
|
||||||
)
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
return Config()
|
|
||||||
keybindings = {str(k): str(v) for k, v in kb.items()}
|
|
||||||
invalid = {k for k in keybindings if k not in VALID_FIELDS}
|
|
||||||
if invalid:
|
|
||||||
print(
|
|
||||||
f"Warning: invalid keybinding field(s): {', '.join(sorted(invalid))}. "
|
|
||||||
f"Valid fields: {', '.join(sorted(VALID_FIELDS))}. Using defaults",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
return Config()
|
|
||||||
return Config(keybindings=keybindings)
|
|
||||||
except yaml.YAMLError:
|
except yaml.YAMLError:
|
||||||
print(
|
print(
|
||||||
f"Warning: config file {path} could not be parsed, using defaults",
|
f"Warning: config file {path} could not be parsed, using defaults",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
return Config()
|
return Config()
|
||||||
|
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
print(
|
||||||
|
f"Warning: config file {path} has invalid structure, using defaults",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return Config()
|
||||||
|
|
||||||
|
rbw_path = "rbw"
|
||||||
|
if "rbw_path" in data:
|
||||||
|
raw = data["rbw_path"]
|
||||||
|
if isinstance(raw, str) and raw:
|
||||||
|
rbw_path = os.path.expanduser(raw)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"Warning: invalid rbw_path in {path}, using default",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
keybindings = dict(DEFAULT_KEYBINDINGS)
|
||||||
|
if "keybindings" in data:
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
parsed = {str(k): str(v) for k, v in kb.items()}
|
||||||
|
invalid = {k for k in parsed if k not in VALID_FIELDS}
|
||||||
|
if invalid:
|
||||||
|
print(
|
||||||
|
f"Warning: invalid keybinding field(s): {', '.join(sorted(invalid))}. "
|
||||||
|
f"Valid fields: {', '.join(sorted(VALID_FIELDS))}. Using defaults",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
keybindings = parsed
|
||||||
|
|
||||||
|
return Config(keybindings=keybindings, rbw_path=rbw_path)
|
||||||
|
|||||||
@ -12,8 +12,8 @@ class Entry:
|
|||||||
type: str = field(default="Login", compare=False)
|
type: str = field(default="Login", compare=False)
|
||||||
|
|
||||||
|
|
||||||
def list_entries() -> list[Entry]:
|
def list_entries(rbw_path: str = "rbw") -> list[Entry]:
|
||||||
result = run(["rbw", "list", "--raw"], capture_output=True, encoding="utf-8")
|
result = run([rbw_path, "list", "--raw"], capture_output=True, encoding="utf-8")
|
||||||
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
print("Error calling rbw. Is it correctly configured?", file=sys.stderr)
|
print("Error calling rbw. Is it correctly configured?", file=sys.stderr)
|
||||||
@ -32,22 +32,22 @@ def list_entries() -> list[Entry]:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_field(entry: Entry, field: str) -> str:
|
def get_field(entry: Entry, field: str, rbw_path: str = "rbw") -> str:
|
||||||
if field == "name":
|
if field == "name":
|
||||||
return entry.name
|
return entry.name
|
||||||
if field == "totp":
|
if field == "totp":
|
||||||
return __get_totp(entry)
|
return __get_totp(entry, rbw_path)
|
||||||
if field == "username":
|
if field == "username":
|
||||||
return entry.username
|
return entry.username
|
||||||
|
|
||||||
data = __load_raw(entry)
|
data = __load_raw(entry, rbw_path)
|
||||||
if field == "password":
|
if field == "password":
|
||||||
return data.get("data", {}).get("password") or ""
|
return data.get("data", {}).get("password") or ""
|
||||||
raise ValueError(f"Unknown field: {field}")
|
raise ValueError(f"Unknown field: {field}")
|
||||||
|
|
||||||
|
|
||||||
def __get_totp(entry: Entry) -> str:
|
def __get_totp(entry: Entry, rbw_path: str) -> str:
|
||||||
cmd = ["rbw", "code", entry.name]
|
cmd = [rbw_path, "code", entry.name]
|
||||||
if entry.username:
|
if entry.username:
|
||||||
cmd.append(entry.username)
|
cmd.append(entry.username)
|
||||||
if entry.folder:
|
if entry.folder:
|
||||||
@ -60,8 +60,8 @@ def __get_totp(entry: Entry) -> str:
|
|||||||
return result.stdout.strip()
|
return result.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
def __load_raw(entry: Entry) -> dict:
|
def __load_raw(entry: Entry, rbw_path: str) -> dict:
|
||||||
cmd = ["rbw", "get", "--raw", entry.name]
|
cmd = [rbw_path, "get", "--raw", entry.name]
|
||||||
if entry.username:
|
if entry.username:
|
||||||
cmd.append(entry.username)
|
cmd.append(entry.username)
|
||||||
if entry.folder:
|
if entry.folder:
|
||||||
|
|||||||
@ -32,8 +32,8 @@ def decode_info(info: str) -> Entry:
|
|||||||
return Entry(parts[0], parts[1], parts[2])
|
return Entry(parts[0], parts[1], parts[2])
|
||||||
|
|
||||||
|
|
||||||
def prepare_entries() -> tuple[list[Entry], list[Entry]]:
|
def prepare_entries(rbw_path: str = "rbw") -> tuple[list[Entry], list[Entry]]:
|
||||||
all_entries = rbw.list_entries()
|
all_entries = rbw.list_entries(rbw_path)
|
||||||
history_entries = history.load()
|
history_entries = history.load()
|
||||||
|
|
||||||
all_dict = {e: e for e in all_entries}
|
all_dict = {e: e for e in all_entries}
|
||||||
|
|||||||
@ -10,8 +10,8 @@ from bw_menu.selector import (
|
|||||||
|
|
||||||
|
|
||||||
class FzfSelector:
|
class FzfSelector:
|
||||||
def run(self, args) -> Selection | None:
|
def run(self, args, rbw_path: str = "rbw") -> Selection | None:
|
||||||
history_in_list, remaining = prepare_entries()
|
history_in_list, remaining = prepare_entries(rbw_path)
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
for entry in history_in_list:
|
for entry in history_in_list:
|
||||||
|
|||||||
@ -28,7 +28,7 @@ _TYPE_ICONS: dict[str, str] = {
|
|||||||
|
|
||||||
|
|
||||||
class RofiSelector:
|
class RofiSelector:
|
||||||
def run(self, args) -> Selection | None:
|
def run(self, args, rbw_path: str = "rbw") -> Selection | None:
|
||||||
cfg = config.load()
|
cfg = config.load()
|
||||||
keybindings = cfg.keybindings
|
keybindings = cfg.keybindings
|
||||||
rofi_retv = os.environ.get("ROFI_RETV")
|
rofi_retv = os.environ.get("ROFI_RETV")
|
||||||
@ -40,7 +40,7 @@ class RofiSelector:
|
|||||||
rofi_retv = int(rofi_retv)
|
rofi_retv = int(rofi_retv)
|
||||||
|
|
||||||
if rofi_retv == 0:
|
if rofi_retv == 0:
|
||||||
self.__print_entries(keybindings)
|
self.__print_entries(keybindings, rbw_path)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
fields = list(keybindings.keys())
|
fields = list(keybindings.keys())
|
||||||
@ -118,8 +118,8 @@ class RofiSelector:
|
|||||||
|
|
||||||
subprocess.run(cmd)
|
subprocess.run(cmd)
|
||||||
|
|
||||||
def __print_entries(self, keybindings: dict[str, str]):
|
def __print_entries(self, keybindings: dict[str, str], rbw_path: str):
|
||||||
history_in_list, remaining = prepare_entries()
|
history_in_list, remaining = prepare_entries(rbw_path)
|
||||||
|
|
||||||
print("\0prompt\x1fSelect item")
|
print("\0prompt\x1fSelect item")
|
||||||
print("\0no-custom\x1ftrue")
|
print("\0no-custom\x1ftrue")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user