diff --git a/README.md b/README.md index 7a817ef..0ca0c35 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ one `[[profiles]]` entry, that profile is selected automatically when py -m venv .venv .\.venv\Scripts\Activate.ps1 py -m pip install -e . -py -m plugin_helper menu +py -m plugin_helper py -m plugin_helper --config plugin-helper.windows.toml --profile windows installed --instance 1.44.1 ``` diff --git a/src/plugin_helper/tui.py b/src/plugin_helper/tui.py index 82c096e..7caea3d 100644 --- a/src/plugin_helper/tui.py +++ b/src/plugin_helper/tui.py @@ -256,6 +256,9 @@ class PluginHelperTui(App[int]): self._show_installations() return target = self.selected_installation + selected_row: int | None = None + if self.mode == "plugins" and self.plugin_rows: + selected_row = self._cursor_index(len(self.plugin_rows)) self.mode = "plugins" self._set_title(f"{target.install_label} / {target.instance_name}") table = self.query_one(DataTable) @@ -285,6 +288,8 @@ class PluginHelperTui(App[int]): str(plugin["fileCount"]), plugin["asset"], ) + if selected_row is not None and self.plugin_rows: + table.move_cursor(row=min(selected_row, len(self.plugin_rows) - 1)) if not preserve_status: if self.plugin_rows: back_hint = "" if len(self.choices) == 1 else " b returns to installations." diff --git a/tests/test_plugin_helper.py b/tests/test_plugin_helper.py index c09053f..f289ea7 100644 --- a/tests/test_plugin_helper.py +++ b/tests/test_plugin_helper.py @@ -1471,6 +1471,75 @@ sha256 = "{sha256_file(asset)}" return PluginHelperTui(choices=[choice], repo_root=repo), instance, state +def _make_two_plugin_tui_fixture(root: Path) -> tuple[PluginHelperTui, Path, Path]: + repo = root / "repo" + instance_root = root / "instances" + instance = instance_root / "1.40.8" + state = root / "state" + (repo / "registry").mkdir(parents=True) + (repo / "locks").mkdir() + (instance / "Beat Saber_Data").mkdir(parents=True) + (instance / "Plugins").mkdir() + + plugins_state: dict[str, dict] = {} + lock_entries: list[str] = [] + registry_entries: list[str] = [] + for plugin_id, name, filename in ( + ("alpha", "Alpha", "Alpha.dll"), + ("beta", "Beta", "Beta.dll"), + ): + asset = plugin_downloads_dir(state, "1.40.8", plugin_id) / filename + asset.write_bytes(f"{plugin_id} dll".encode()) + (instance / "Plugins" / filename).write_bytes(f"{plugin_id} dll".encode()) + registry_entries.append( + f""" +[[plugins]] +id = "{plugin_id}" +name = "{name}" +repo = "owner/{plugin_id}" +asset_patterns = ["*.dll"] +install_strategy = "dll-to-plugins" +""".lstrip() + ) + lock_entries.append( + f""" +[[plugins]] +id = "{plugin_id}" +repo = "owner/{plugin_id}" +tag = "v1.0.0" +asset = "{filename}" +sha256 = "{sha256_file(asset)}" +""".lstrip() + ) + plugins_state[plugin_id] = { + "installedAt": "2026-06-14T17:18:40Z", + "files": [{"path": f"Plugins/{filename}", "sha256": sha256_file(asset), "size": asset.stat().st_size}], + } + + (repo / "registry" / "plugins.toml").write_text("".join(registry_entries), encoding="utf-8") + (repo / "locks" / "1.40.8.lock.toml").write_text( + f""" +beat_saber_version = "1.40.8" +instance = "1.40.8" +{"".join(lock_entries)} +""".lstrip(), + encoding="utf-8", + ) + save_installed_state( + state, + "1.40.8", + {"instance": "1.40.8", "plugins": plugins_state, "disabledPlugins": {}}, + ) + choice = InstallationChoice( + install_id="test", + install_label="Test Install", + instance_name="1.40.8", + instance_path=instance, + state_root=state, + ) + return PluginHelperTui(choices=[choice], repo_root=repo), instance, state + + class PluginHelperTuiTests(unittest.IsolatedAsyncioTestCase): async def test_installation_picker_shows_duplicate_instances_with_state_dirs(self) -> None: choices = [ @@ -1523,6 +1592,25 @@ class PluginHelperTuiTests(unittest.IsolatedAsyncioTestCase): self.assertNotIn("example", updated["plugins"]) self.assertIn("example", updated["disabledPlugins"]) + async def test_space_toggle_keeps_cursor_on_selected_plugin(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + app, instance, _state = _make_two_plugin_tui_fixture(Path(tmp)) + + async with app.run_test() as pilot: + table = app.query_one(DataTable) + self.assertEqual(table.row_count, 2) + await pilot.press("down") + await pilot.pause() + self.assertEqual(table.cursor_row, 1) + self.assertEqual(app.plugin_rows[table.cursor_row]["id"], "beta") + await pilot.press("space") + await pilot.pause() + self.assertEqual(table.cursor_row, 1) + self.assertEqual(app.plugin_rows[table.cursor_row]["id"], "beta") + + self.assertFalse((instance / "Plugins" / "Beta.dll").exists()) + self.assertTrue((instance / "Plugins" / "Alpha.dll").exists()) + async def test_space_enables_disabled_plugin_from_asset(self) -> None: with tempfile.TemporaryDirectory() as tmp: app, instance, state = _make_tui_fixture(Path(tmp), disabled=True)