Keep TUI cursor stable after plugin toggle
This commit is contained in:
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user