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
|
py -m venv .venv
|
||||||
.\.venv\Scripts\Activate.ps1
|
.\.venv\Scripts\Activate.ps1
|
||||||
py -m pip install -e .
|
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
|
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()
|
self._show_installations()
|
||||||
return
|
return
|
||||||
target = self.selected_installation
|
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.mode = "plugins"
|
||||||
self._set_title(f"{target.install_label} / {target.instance_name}")
|
self._set_title(f"{target.install_label} / {target.instance_name}")
|
||||||
table = self.query_one(DataTable)
|
table = self.query_one(DataTable)
|
||||||
@@ -285,6 +288,8 @@ class PluginHelperTui(App[int]):
|
|||||||
str(plugin["fileCount"]),
|
str(plugin["fileCount"]),
|
||||||
plugin["asset"],
|
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 not preserve_status:
|
||||||
if self.plugin_rows:
|
if self.plugin_rows:
|
||||||
back_hint = "" if len(self.choices) == 1 else " b returns to installations."
|
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
|
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):
|
class PluginHelperTuiTests(unittest.IsolatedAsyncioTestCase):
|
||||||
async def test_installation_picker_shows_duplicate_instances_with_state_dirs(self) -> None:
|
async def test_installation_picker_shows_duplicate_instances_with_state_dirs(self) -> None:
|
||||||
choices = [
|
choices = [
|
||||||
@@ -1523,6 +1592,25 @@ class PluginHelperTuiTests(unittest.IsolatedAsyncioTestCase):
|
|||||||
self.assertNotIn("example", updated["plugins"])
|
self.assertNotIn("example", updated["plugins"])
|
||||||
self.assertIn("example", updated["disabledPlugins"])
|
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:
|
async def test_space_enables_disabled_plugin_from_asset(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
app, instance, state = _make_tui_fixture(Path(tmp), disabled=True)
|
app, instance, state = _make_tui_fixture(Path(tmp), disabled=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user