Skip to content
23 changes: 21 additions & 2 deletions config_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ def get_complete_root_statuses(config_path: str) -> dict[str, Any]:
A dictionary like: {'global_status': bool, 'instance_statuses': {name: bool}}
"""
instance_statuses: dict[str, bool] = {}
display_names: dict[str, str] = {}

global_status: bool = False

if not os.path.isfile(config_path):
Expand All @@ -111,6 +113,16 @@ def get_complete_root_statuses(config_path: str) -> dict[str, Any]:
+ r'\s*=\s*"([^"]*)"',
re.IGNORECASE,
)

instance_pattern_name = 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 +143,15 @@ 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
match_name = instance_pattern_name.match(stripped_line)
if match_name:
instance_name, value = match_name.group(1), match_name.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 {"global_status": False, "instance_statuses": {}, "display_names": {}}

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
51 changes: 43 additions & 8 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
QCheckBox,
QMessageBox,
QFileDialog,
QScrollArea,
)
from PyQt5.QtCore import Qt, QTimer, QThread, QObject, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QIcon
Expand Down Expand Up @@ -114,15 +115,38 @@ def init_ui(self) -> None:
self.path_label.setWordWrap(True)
main_layout.addWidget(self.path_label)

# Instances group box
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)

# Scroll area inside the group box
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_scroll_area.setMaximumHeight(1600)
#self.instance_scroll_area.setMinimumHeight(800)
#self.instance_scroll_area.setMinimumWidth(1000)

# Widget that contains the QGridLayout
self.instance_container = QWidget()
self.instance_layout = QGridLayout(self.instance_container)

# define our columns in the grid (instance ID, display name, Root, R/W)
self.instance_layout.setColumnStretch(0, 3)
self.instance_layout.setColumnStretch(1, 4)
self.instance_layout.setColumnStretch(2, 1)
self.instance_layout.setHorizontalSpacing(15)
self.instance_group.setLayout(self.instance_layout)
self.instance_layout.setColumnStretch(3, 1)
self.instance_layout.setHorizontalSpacing(10)

# Put the grid container inside the scroll area
self.instance_scroll_area.setWidget(self.instance_container)

# Put the scroll area inside the group box
instance_group_layout.addWidget(self.instance_scroll_area)
main_layout.addWidget(self.instance_group)

# Bottom buttons remain outside the scroll area
button_layout = QHBoxLayout()
self.root_toggle_button = QPushButton("Toggle Root")
self.root_toggle_button.clicked.connect(self.handle_toggle_root)
Expand Down Expand Up @@ -163,7 +187,8 @@ def init_ui(self) -> None:
main_layout.addWidget(self.status_label)

self.setLayout(main_layout)
self.setMinimumWidth(550)
self.setMinimumSize(650, 450)
self.resize(1000, 800)

def initialize_paths_and_instances(self) -> None:
logger.info("Initializing BlueStacks paths and instances...")
Expand Down Expand Up @@ -251,6 +276,7 @@ def update_instance_data(self) -> None:
patch_mode = inst.get("patch_mode", False) # 5.22.150.1014+ uses the patches
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 = {entry for entry in (os.listdir(data_path) if os.path.isdir(data_path) else []) if os.path.isdir(os.path.join(data_path, entry))}
all_instance_names = set(instance_root_statuses.keys()) | disk_instances
Expand All @@ -266,6 +292,8 @@ def update_instance_data(self) -> None:
elif is_readonly is False: rw_mode = constants.MODE_READWRITE

individual_root_on = instance_root_statuses.get(name, False)
display_name = display_names.get(name, name)

if patch_mode:
# 5.22.150.1014+: app root = the guest su binary actually patched
# (tracked by the Data.vhdx sidecar). enable_root_access only makes
Expand All @@ -290,6 +318,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_name,
"patch_mode": patch_mode,
}

Expand Down Expand Up @@ -318,11 +347,17 @@ def update_instance_checkboxes(self, preserve_selection: bool = True):
checkbox.setChecked(unique_id in previous_selection)

root_text = "On" if data["root_enabled"] else "Off"
qlabel_root = QLabel(f"Root: {root_text}")
if root_text == "On":
qlabel_root.setStyleSheet("background-color: green; color: white; padding: 2px;")
else:
qlabel_root.setStyleSheet("padding: 2px;")
rw_text = "On" if data["rw_mode"] == constants.MODE_READWRITE else "Off"

self.instance_layout.addWidget(checkbox, row, 0)
self.instance_layout.addWidget(QLabel(f"Root: {root_text}"), row, 1)
self.instance_layout.addWidget(QLabel(f"R/W: {rw_text}"), row, 2)
self.instance_layout.addWidget(QLabel(data["display_name"]), row, 1)
self.instance_layout.addWidget(qlabel_root, row, 2)
self.instance_layout.addWidget(QLabel(f"R/W: {rw_text}"), row, 3)
self.instance_checkboxes[unique_id] = {"checkbox": checkbox}

def _toggle_single_instance_root(self, unique_id, progress=None):
Expand Down