Simplify plugin-helper state configuration

This commit is contained in:
pleb
2026-07-01 13:43:39 -07:00
parent 1be1353835
commit 407abfe6ec
6 changed files with 213 additions and 199 deletions
+10 -51
View File
@@ -6,7 +6,7 @@ import sys
from pathlib import Path
from typing import Any
from .config import Profile, repo_root, resolve_runtime_config
from .config import repo_root, resolve_runtime_config
from .bootstrap import run_bootstrap
from .bsipa import check_bsipa_health
from .checker import check_lock
@@ -63,8 +63,6 @@ def _add_common(parser: argparse.ArgumentParser, *, suppress_default: bool = Fal
default = argparse.SUPPRESS if suppress_default else None
parser.add_argument("--instances-root", default=default, help="BSManager instances root")
parser.add_argument("--state-dir", default=default, help="plugin-helper state directory")
parser.add_argument("--profile", default=default, help="Configured profile id from plugin-helper.local.toml")
parser.add_argument("--config", default=default, help="Path to plugin-helper TOML config")
def build_parser() -> argparse.ArgumentParser:
@@ -225,37 +223,6 @@ def _common_parent() -> argparse.ArgumentParser:
return parent
def _menu_profiles(
*,
profiles: tuple[Profile, ...],
selected_profile: Profile | None,
instances_roots: list[Path],
state_root: Path,
use_config_profiles: bool,
) -> list[Profile]:
if selected_profile:
return [
Profile(
id=selected_profile.id if len(instances_roots) == 1 else f"{selected_profile.id}-{index}",
label=selected_profile.label,
instances_root=root,
state_dir=state_root,
)
for index, root in enumerate(instances_roots, start=1)
]
if use_config_profiles and profiles:
return list(profiles)
return [
Profile(
id=f"root-{index}",
label=str(root),
instances_root=root,
state_dir=state_root,
)
for index, root in enumerate(instances_roots, start=1)
]
def _run_menu(
*,
runtime: Any,
@@ -269,35 +236,29 @@ def _run_menu(
print("Install project dependencies, for example: python -m pip install -e .")
return 1
profiles = _menu_profiles(
profiles=runtime.profiles,
selected_profile=runtime.selected_profile,
instances_roots=runtime.instances_roots,
state_root=runtime.state_root,
use_config_profiles=runtime.config_loaded and not explicit_instances_root and not explicit_state_dir,
)
choices: list[InstallationChoice] = []
for profile in profiles:
for instance in list_instances(profile.instances_root):
for index, root in enumerate(runtime.instances_roots, start=1):
install_label = str(root) if len(runtime.instances_roots) > 1 else "Default"
for instance in list_instances(root):
choices.append(
InstallationChoice(
profile_id=profile.id,
profile_label=profile.label,
install_id=f"root-{index}",
install_label=install_label,
instance_name=instance.name,
instance_path=instance.path,
state_root=profile.state_dir,
state_root=runtime.state_root,
)
)
if not choices:
searched = ", ".join(str(profile.instances_root) for profile in profiles)
searched = ", ".join(str(root) for root in runtime.instances_roots)
print(f"No Beat Saber instances found under {searched}")
return 1
setup_hint = None
if not runtime.config_loaded and not runtime.selected_profile and not explicit_instances_root and not explicit_state_dir:
if not runtime.config_loaded and not explicit_instances_root and not explicit_state_dir:
setup_hint = (
f"No {runtime.config_path.name} found; using default roots and default state. "
f"Copy plugin-helper.toml.example to {runtime.config_path.name} to map each install to a state dir."
f"Copy plugin-helper.toml.example to {runtime.config_path.name} to set a custom state dir."
)
app = PluginHelperTui(choices=choices, repo_root=repo_root(), setup_hint=setup_hint)
result = app.run()
@@ -321,8 +282,6 @@ def run(argv: list[str] | None = None) -> int:
runtime = resolve_runtime_config(
instances_root_value=getattr(args, "instances_root", None),
state_dir_value=getattr(args, "state_dir", None),
profile_id=getattr(args, "profile", None),
config_path=getattr(args, "config", None),
)
inst_roots = runtime.instances_roots
st_root = runtime.state_root
+59 -56
View File
@@ -7,27 +7,21 @@ from pathlib import Path
from typing import Any
WINDOWS_INSTANCES_ROOT = Path("/home/pleb/Windows/Users/pleb/BSManager/BSInstances")
LOCAL_INSTANCES_ROOT = Path.home() / ".local/share/BSManager/BSInstances"
DEFAULT_INSTANCES_ROOTS = (WINDOWS_INSTANCES_ROOT, LOCAL_INSTANCES_ROOT)
DEFAULT_INSTANCES_ROOT = WINDOWS_INSTANCES_ROOT
DEFAULT_INSTANCES_ROOT = LOCAL_INSTANCES_ROOT
LOCAL_CONFIG_NAME = "plugin-helper.local.toml"
@dataclass(frozen=True)
class Profile:
id: str
label: str
instances_root: Path
state_dir: Path
class LocalConfig:
instances_roots: list[Path] | None
state_root: Path | None
@dataclass(frozen=True)
class RuntimeConfig:
instances_roots: list[Path]
state_root: Path
profiles: tuple[Profile, ...]
selected_profile: Profile | None
config_path: Path
config_loaded: bool
@@ -40,16 +34,19 @@ def instances_root(value: str | None = None) -> Path:
return instances_roots(value)[0]
def instances_roots(value: str | None = None) -> list[Path]:
def instances_roots(value: str | None = None, *, base: Path | None = None) -> list[Path]:
raw = value or os.environ.get("PLUGIN_HELPER_INSTANCES_ROOT")
if raw:
return [Path(item).expanduser() for item in raw.split(os.pathsep) if item]
return list(DEFAULT_INSTANCES_ROOTS)
return _resolve_path_list(raw, base or repo_root())
return [DEFAULT_INSTANCES_ROOT]
def state_root(value: str | None = None) -> Path:
if value:
return _resolve_path(value, repo_root())
env_state = os.environ.get("PLUGIN_HELPER_STATE_DIR")
if env_state:
return _resolve_path(env_state, repo_root())
xdg_state = os.environ.get("XDG_STATE_HOME")
base = Path(xdg_state).expanduser() if xdg_state else Path.home() / ".local" / "state"
return base / "plugin-helper"
@@ -66,75 +63,81 @@ def _resolve_path(value: str | Path, base: Path) -> Path:
return (base / path).resolve()
def load_profiles(config_path: str | Path | None = None, *, root: Path | None = None) -> tuple[tuple[Profile, ...], Path, bool]:
def _resolve_path_list(value: str, base: Path) -> list[Path]:
return [_resolve_path(item, base) for item in value.split(os.pathsep) if item]
def load_local_config(config_path: str | Path | None = None, *, root: Path | None = None) -> tuple[LocalConfig, Path, bool]:
repo = root or repo_root()
path = _resolve_path(config_path, repo) if config_path else default_config_path(repo)
if not path.exists():
if config_path:
raise FileNotFoundError(f"plugin-helper config not found: {path}")
return (), path, False
return LocalConfig(instances_roots=None, state_root=None), path, False
with path.open("rb") as handle:
data: dict[str, Any] = tomllib.load(handle)
profiles: list[Profile] = []
seen: set[str] = set()
base = path.parent
for item in data.get("profiles", []):
profile_id = item["id"]
if profile_id in seen:
raise ValueError(f"{path}: duplicate profile id: {profile_id}")
seen.add(profile_id)
profiles.append(
Profile(
id=profile_id,
label=item.get("label", profile_id),
instances_root=_resolve_path(item["instances_root"], base),
state_dir=_resolve_path(item["state_dir"], base),
)
)
return tuple(profiles), path, True
instances_value = data.get("instances_root")
state_value = data.get("state_dir")
return (
LocalConfig(
instances_roots=_resolve_path_list(instances_value, base) if instances_value else None,
state_root=_resolve_path(state_value, base) if state_value else None,
),
path,
True,
)
def profile_by_id(profiles: tuple[Profile, ...], profile_id: str) -> Profile:
for profile in profiles:
if profile.id == profile_id:
return profile
available = ", ".join(profile.id for profile in profiles) or "(none)"
raise KeyError(f"unknown profile: {profile_id}; available profiles: {available}")
def _env_instances_roots(root: Path) -> list[Path] | None:
value = os.environ.get("PLUGIN_HELPER_INSTANCES_ROOT")
return _resolve_path_list(value, root) if value else None
def _env_state_root(root: Path) -> Path | None:
value = os.environ.get("PLUGIN_HELPER_STATE_DIR")
return _resolve_path(value, root) if value else None
def _default_state_root() -> Path:
xdg_state = os.environ.get("XDG_STATE_HOME")
base = Path(xdg_state).expanduser() if xdg_state else Path.home() / ".local" / "state"
return base / "plugin-helper"
def _default_instances_roots() -> list[Path]:
return [DEFAULT_INSTANCES_ROOT]
def resolve_runtime_config(
*,
instances_root_value: str | None = None,
state_dir_value: str | None = None,
profile_id: str | None = None,
config_path: str | Path | None = None,
root: Path | None = None,
) -> RuntimeConfig:
repo = root or repo_root()
profiles, loaded_path, loaded = load_profiles(config_path, root=repo)
selected = profile_by_id(profiles, profile_id) if profile_id else None
local_config, loaded_path, loaded = load_local_config(root=repo)
if instances_root_value:
resolved_instances = instances_roots(instances_root_value)
elif selected:
resolved_instances = [selected.instances_root]
else:
resolved_instances = instances_roots(None)
if state_dir_value:
resolved_state = _resolve_path(state_dir_value, repo)
elif selected:
resolved_state = selected.state_dir
else:
resolved_state = state_root(None)
resolved_instances = (
_resolve_path_list(instances_root_value, repo)
if instances_root_value
else _env_instances_roots(repo)
or local_config.instances_roots
or _default_instances_roots()
)
resolved_state = (
_resolve_path(state_dir_value, repo)
if state_dir_value
else _env_state_root(repo)
or local_config.state_root
or _default_state_root()
)
return RuntimeConfig(
instances_roots=resolved_instances,
state_root=resolved_state,
profiles=profiles,
selected_profile=selected,
config_path=loaded_path,
config_loaded=loaded,
)
+5 -5
View File
@@ -18,8 +18,8 @@ from .state import load_installed_state
@dataclass(frozen=True)
class InstallationChoice:
profile_id: str
profile_label: str
install_id: str
install_label: str
instance_name: str
instance_path: Path
state_root: Path
@@ -189,10 +189,10 @@ class PluginHelperTui(App[int]):
self._set_title("Choose Beat Saber Installation")
table = self.query_one(DataTable)
table.clear(columns=True)
table.add_columns("Profile", "Version", "Instance Path", "State Dir")
table.add_columns("Install", "Version", "Instance Path", "State Dir")
for choice in self.choices:
table.add_row(
choice.profile_label,
choice.install_label,
choice.instance_name,
str(choice.instance_path),
str(choice.state_root),
@@ -208,7 +208,7 @@ class PluginHelperTui(App[int]):
return
target = self.selected_installation
self.mode = "plugins"
self._set_title(f"{target.profile_label} / {target.instance_name}")
self._set_title(f"{target.install_label} / {target.instance_name}")
table = self.query_one(DataTable)
table.clear(columns=True)
table.add_columns("Status", "Name", "ID", "Version", "Files", "Asset")