Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions config_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,19 @@ def get_complete_root_statuses(config_path: str) -> dict[str, Any]:
config_path: Path to the bluestacks.conf file.

Returns:
A dictionary like: {'global_status': bool, 'instance_statuses': {name: bool}}
A dictionary like:
{'global_status': bool, 'instance_statuses': {name: bool}, 'display_names': {name: str}}
"""
instance_statuses: dict[str, bool] = {}
display_names: dict[str, str] = {}
global_status: bool = False
empty_result = {"global_status": False, "instance_statuses": {}, "display_names": {}}

if not os.path.isfile(config_path):
logger.warning(
f"Config file not found for reading root statuses: {config_path}"
)
return {"global_status": False, "instance_statuses": {}}
return empty_result

# FIX: Regex for both instance-specific and global root keys
instance_pattern = re.compile(
Expand All @@ -111,6 +114,14 @@ def get_complete_root_statuses(config_path: str) -> dict[str, Any]:
+ r'\s*=\s*"([^"]*)"',
re.IGNORECASE,
)
display_name_pattern = re.compile(
r"^"
+ re.escape(constants.INSTANCE_PREFIX)
+ r"([^.]+)"
+ re.escape(constants.DISPLAY_NAME_KEY)
+ r'\s*=\s*"([^"]*)"',
re.IGNORECASE,
)
global_pattern = re.compile(
r"^" + re.escape(constants.FEATURE_ROOTING_KEY) + r'\s*=\s*"1"', re.IGNORECASE
)
Expand All @@ -131,8 +142,18 @@ def get_complete_root_statuses(config_path: str) -> dict[str, Any]:
instance_name, value = match.group(1), match.group(2)
is_enabled = value == "1"
instance_statuses[instance_name] = is_enabled

# Check for instance display-name key
name_match = display_name_pattern.match(stripped_line)
if name_match:
instance_name, value = name_match.group(1), name_match.group(2)
display_names[instance_name] = value
except Exception:
logger.exception(f"Error reading config file {config_path} for root statuses.")
return {"global_status": False, "instance_statuses": {}}
return empty_result

return {"global_status": global_status, "instance_statuses": instance_statuses}
return {
"global_status": global_status,
"instance_statuses": instance_statuses,
"display_names": display_names,
}
1 change: 1 addition & 0 deletions constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

INSTANCE_PREFIX = "bst.instance."
ENABLE_ROOT_KEY = ".enable_root_access"
DISPLAY_NAME_KEY = ".display_name"
FEATURE_ROOTING_KEY = "bst.feature.rooting"
BLUESTACKS_CONF_FILENAME = "bluestacks.conf"

Expand Down
37 changes: 37 additions & 0 deletions tests/test_config_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import config_handler


def _write_conf(tmp_path, text):
conf = tmp_path / "bluestacks.conf"
conf.write_text(text, encoding="utf-8")
return str(conf)


def test_get_complete_root_statuses_parses_display_names(tmp_path):
conf = _write_conf(tmp_path, '\n'.join([
'bst.instance.Nougat64.enable_root_access="1"',
'bst.instance.Nougat64.display_name="Main Farm Bot"',
'bst.instance.Pie64.enable_root_access="0"',
'bst.instance.Pie64.display_name="Alt Account"',
'bst.instance.NoName.enable_root_access="1"',
'bst.feature.rooting="1"',
]))

result = config_handler.get_complete_root_statuses(conf)

assert result["global_status"] is True
assert result["instance_statuses"] == {"Nougat64": True, "Pie64": False, "NoName": True}
assert result["display_names"] == {"Nougat64": "Main Farm Bot", "Pie64": "Alt Account"}


def test_get_complete_root_statuses_missing_file_includes_display_names_key(tmp_path):
result = config_handler.get_complete_root_statuses(str(tmp_path / "does_not_exist.conf"))
assert result == {"global_status": False, "instance_statuses": {}, "display_names": {}}


def test_get_complete_root_statuses_unreadable_file_includes_display_names_key(tmp_path, monkeypatch):
conf = _write_conf(tmp_path, 'bst.instance.Pie64.enable_root_access="1"')
monkeypatch.setattr(config_handler, "open", lambda *a, **k: (_ for _ in ()).throw(OSError("boom")), raising=False)

result = config_handler.get_complete_root_statuses(conf)
assert result == {"global_status": False, "instance_statuses": {}, "display_names": {}}
49 changes: 49 additions & 0 deletions tests/test_instances_page.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QLabel

import constants
from views.instances_page import InstancesPage
Expand Down Expand Up @@ -80,3 +81,51 @@ def test_clicking_fix_it_emits_go_to_dashboard(qtbot):
page.show()
with qtbot.waitSignal(page.go_to_dashboard_requested, timeout=1000):
qtbot.mouseClick(page.banner_fix_button, Qt.LeftButton)


def test_set_instances_shows_display_name_and_falls_back_to_unique_id(qtbot):
page = InstancesPage()
qtbot.addWidget(page)
page.show()
data = {
"Nougat64 (Normal)": {"root_enabled": True, "rw_mode": constants.MODE_READWRITE,
"display_name": "Main Farm Bot"},
"Pie64 (Normal)": {"root_enabled": False, "rw_mode": constants.MODE_READWRITE},
}
page.set_instances(data)

labels = [page.instance_layout.itemAt(i).widget() for i in range(page.instance_layout.count())]
texts = {w.text() for w in labels if isinstance(w, QLabel)}
assert "Main Farm Bot" in texts
assert "Pie64 (Normal)" in texts # no display_name key -> falls back to unique_id


def test_set_instances_highlights_rooted_instances_green(qtbot):
page = InstancesPage()
qtbot.addWidget(page)
page.show()
data = {
"Rooted (Normal)": {"root_enabled": True, "rw_mode": constants.MODE_READWRITE},
"NotRooted (Normal)": {"root_enabled": False, "rw_mode": constants.MODE_READWRITE},
}
page.set_instances(data)

root_labels = {
w.text(): w for i in range(page.instance_layout.count())
if isinstance((w := page.instance_layout.itemAt(i).widget()), QLabel)
and w.text().startswith("Root:")
}
assert "green" in root_labels["Root: On"].styleSheet()
assert "green" not in root_labels["Root: Off"].styleSheet()


def test_set_instances_refresh_does_not_leak_widgets(qtbot):
page = InstancesPage()
qtbot.addWidget(page)
page.show()
data = {"Pie64 (Normal)": {"root_enabled": True, "rw_mode": constants.MODE_READWRITE}}
page.set_instances(data)
count_after_first = page.instance_layout.count()

page.set_instances(data)
assert page.instance_layout.count() == count_after_first
41 changes: 32 additions & 9 deletions views/instances_page.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""Instances page: instance grid + Toggle Root/R-W + patch-gating banner."""
from __future__ import annotations

from PyQt5.QtCore import pyqtSignal
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGridLayout, QGroupBox, QCheckBox, QLabel,
QPushButton, QHBoxLayout,
QPushButton, QHBoxLayout, QScrollArea,
)

import constants
Expand Down Expand Up @@ -32,13 +32,26 @@ def __init__(self, parent=None):
layout.addLayout(banner_row)
self.set_engine_locked_banner(False)

# Instances group box: a scroll area wraps the grid so large instance
# counts (20+) don't force the window taller than the screen.
self.instance_group = QGroupBox("Instances")
self.instance_layout = QGridLayout()
self.instance_layout.setColumnStretch(0, 4)
self.instance_layout.setColumnStretch(1, 1)
instance_group_layout = QVBoxLayout(self.instance_group)

self.instance_scroll_area = QScrollArea()
self.instance_scroll_area.setWidgetResizable(True)
self.instance_scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.instance_scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)

self.instance_container = QWidget()
self.instance_layout = QGridLayout(self.instance_container)
self.instance_layout.setColumnStretch(0, 3)
self.instance_layout.setColumnStretch(1, 4)
self.instance_layout.setColumnStretch(2, 1)
self.instance_layout.setColumnStretch(3, 1)
self.instance_layout.setHorizontalSpacing(15)
self.instance_group.setLayout(self.instance_layout)

self.instance_scroll_area.setWidget(self.instance_container)
instance_group_layout.addWidget(self.instance_scroll_area)
layout.addWidget(self.instance_group)

button_row = QHBoxLayout()
Expand Down Expand Up @@ -73,11 +86,21 @@ def set_instances(self, instance_data: dict, preserve_selection: bool = True) ->
data = instance_data[unique_id]
checkbox = QCheckBox(unique_id)
checkbox.setChecked(unique_id in previous_selection)
root_text = "On" if data.get("root_enabled") else "Off"
display_name = data.get("display_name", unique_id)
root_on = bool(data.get("root_enabled"))
root_text = "On" if root_on else "Off"
rw_text = "On" if data.get("rw_mode") == constants.MODE_READWRITE else "Off"

root_label = QLabel("Root: %s" % root_text)
if root_on:
root_label.setStyleSheet("background-color: green; color: white; padding: 2px;")
else:
root_label.setStyleSheet("padding: 2px;")

self.instance_layout.addWidget(checkbox, row, 0)
self.instance_layout.addWidget(QLabel("Root: %s" % root_text), row, 1)
self.instance_layout.addWidget(QLabel("R/W: %s" % rw_text), row, 2)
self.instance_layout.addWidget(QLabel(display_name), row, 1)
self.instance_layout.addWidget(root_label, row, 2)
self.instance_layout.addWidget(QLabel("R/W: %s" % rw_text), row, 3)
self.checkboxes[unique_id] = checkbox

def selected_ids(self) -> list[str]:
Expand Down
2 changes: 2 additions & 0 deletions views/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ def update_instance_data(self) -> None:
patch_mode = inst.get("patch_mode", False)
root_info = config_handler.get_complete_root_statuses(config_path)
instance_root_statuses = root_info['instance_statuses']
display_names = root_info.get('display_names', {})

disk_instances = set()
if os.path.isdir(data_path):
Expand Down Expand Up @@ -333,6 +334,7 @@ def update_instance_data(self) -> None:
"rw_mode": rw_mode,
"root_enabled": effective_root_status,
"individual_root_status": individual_root_on,
"display_name": display_names.get(name, name),
"patch_mode": patch_mode,
}

Expand Down