mirror of
https://github.com/morgan9e/AdvancedReboot
synced 2026-04-14 00:14:35 +09:00
271 lines
9.5 KiB
Python
Executable File
271 lines
9.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import gi
|
|
gi.require_version('Gtk', '4.0')
|
|
gi.require_version('Adw', '1')
|
|
from gi.repository import Gtk, Adw, GLib, Gio
|
|
import subprocess
|
|
import re
|
|
import sys
|
|
import os
|
|
|
|
class BootEntry:
|
|
def __init__(self, num, name, active=False):
|
|
self.num = num
|
|
self.name = name
|
|
self.active = active
|
|
|
|
class AdvancedRebootWindow(Adw.ApplicationWindow):
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
|
|
self.set_default_size(600, 500)
|
|
self.set_title("Advanced Reboot Options")
|
|
|
|
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
main_box.set_spacing(0)
|
|
|
|
header = Adw.HeaderBar()
|
|
main_box.append(header)
|
|
|
|
title_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
|
|
title_box.set_margin_top(20)
|
|
title_box.set_margin_bottom(20)
|
|
title_box.set_margin_start(20)
|
|
title_box.set_margin_end(20)
|
|
|
|
title_label = Gtk.Label(label="Choose Boot Option")
|
|
title_label.add_css_class("title-1")
|
|
title_box.append(title_label)
|
|
|
|
main_box.append(title_box)
|
|
|
|
separator1 = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
|
|
main_box.append(separator1)
|
|
|
|
scrolled = Gtk.ScrolledWindow()
|
|
scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
|
scrolled.set_vexpand(True)
|
|
scrolled.set_margin_top(12)
|
|
scrolled.set_margin_bottom(12)
|
|
scrolled.set_margin_start(12)
|
|
scrolled.set_margin_end(12)
|
|
|
|
self.listbox = Gtk.ListBox()
|
|
self.listbox.set_selection_mode(Gtk.SelectionMode.SINGLE)
|
|
self.listbox.add_css_class("boxed-list")
|
|
|
|
self.boot_entries = self.get_boot_entries()
|
|
self.selected_entry = None
|
|
|
|
if not self.boot_entries:
|
|
error_label = Gtk.Label(label="No EFI boot entries found!")
|
|
error_label.add_css_class("dim-label")
|
|
error_label.set_margin_top(40)
|
|
error_label.set_margin_bottom(40)
|
|
scrolled.set_child(error_label)
|
|
else:
|
|
for entry in self.boot_entries:
|
|
row = self.create_boot_entry_row(entry)
|
|
self.listbox.append(row)
|
|
|
|
self.listbox.connect("row-selected", self.on_row_selected)
|
|
self.listbox.connect("row-activated", self.on_row_activated)
|
|
|
|
scrolled.set_child(self.listbox)
|
|
|
|
main_box.append(scrolled)
|
|
|
|
separator2 = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
|
|
main_box.append(separator2)
|
|
|
|
button_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
|
button_box.set_margin_top(12)
|
|
button_box.set_margin_bottom(12)
|
|
button_box.set_margin_start(12)
|
|
button_box.set_margin_end(12)
|
|
button_box.set_halign(Gtk.Align.END)
|
|
|
|
cancel_btn = Gtk.Button(label="Cancel")
|
|
cancel_btn.connect("clicked", lambda x: self.close())
|
|
button_box.append(cancel_btn)
|
|
|
|
self.reboot_to_btn = Gtk.Button(label="Reboot to Selected")
|
|
self.reboot_to_btn.add_css_class("destructive-action")
|
|
self.reboot_to_btn.set_sensitive(False)
|
|
self.reboot_to_btn.connect("clicked", self.on_reboot)
|
|
button_box.append(self.reboot_to_btn)
|
|
|
|
main_box.append(button_box)
|
|
|
|
self.set_content(main_box)
|
|
|
|
def create_boot_entry_row(self, entry):
|
|
row = Adw.ActionRow()
|
|
|
|
clean_name = self.clean_boot_name(entry.name)
|
|
row.set_title(clean_name)
|
|
|
|
subtitle = f"Boot entry: {entry.num}"
|
|
if entry.active:
|
|
subtitle += " • Currently active"
|
|
row.set_subtitle(subtitle)
|
|
|
|
if entry.active:
|
|
checkmark = Gtk.Image.new_from_icon_name("object-select-symbolic")
|
|
row.add_suffix(checkmark)
|
|
|
|
row.entry = entry
|
|
|
|
return row
|
|
|
|
def clean_boot_name(self, name):
|
|
for separator in ['HD(', 'PciRoot(', 'File(', '\t']:
|
|
if separator in name:
|
|
name = name.split(separator)[0]
|
|
|
|
return name.strip()
|
|
|
|
def get_boot_entries(self):
|
|
try:
|
|
result = subprocess.run(['efibootmgr'],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True)
|
|
entries = []
|
|
|
|
current_match = re.search(r'BootCurrent: ([0-9A-F]+)', result.stdout)
|
|
current = current_match.group(1) if current_match else None
|
|
|
|
for line in result.stdout.split('\n'):
|
|
match = re.match(r'Boot([0-9A-F]+)\*?\s+(.+)', line)
|
|
if match:
|
|
num = match.group(1)
|
|
name = match.group(2)
|
|
active = (num == current)
|
|
entries.append(BootEntry(num, name, active))
|
|
|
|
return entries
|
|
except subprocess.CalledProcessError:
|
|
return []
|
|
except FileNotFoundError:
|
|
return []
|
|
except Exception as e:
|
|
print(f"Error getting boot entries: {e}", file=sys.stderr)
|
|
return []
|
|
|
|
def on_row_selected(self, listbox, row):
|
|
if row is None:
|
|
self.selected_entry = None
|
|
self.reboot_to_btn.set_sensitive(False)
|
|
self.reboot_to_btn.set_label("Reboot to Selected")
|
|
return
|
|
|
|
self.selected_entry = row.entry
|
|
self.reboot_to_btn.set_sensitive(True)
|
|
|
|
clean_name = self.clean_boot_name(row.entry.name)
|
|
self.reboot_to_btn.set_label(f"Reboot to {clean_name}")
|
|
|
|
def on_row_activated(self, listbox, row):
|
|
self.selected_entry = row.entry
|
|
self.reboot_to_btn.set_sensitive(True)
|
|
self.on_reboot(None)
|
|
|
|
def on_reboot(self, button):
|
|
if not self.selected_entry:
|
|
return
|
|
clean_name = self.clean_boot_name(self.selected_entry.name)
|
|
|
|
dialog = Adw.MessageDialog.new(self)
|
|
dialog.set_heading("Confirm Reboot")
|
|
dialog.set_body(f"Selected: <b>{clean_name}</b>")
|
|
dialog.set_body_use_markup(True)
|
|
dialog.add_response("cancel", "Cancel")
|
|
dialog.add_response("reboot", "Reboot Now")
|
|
dialog.set_response_appearance("reboot", Adw.ResponseAppearance.DESTRUCTIVE)
|
|
dialog.set_default_response("cancel")
|
|
dialog.set_close_response("cancel")
|
|
dialog.connect("response", self.on_reboot_response)
|
|
dialog.present()
|
|
|
|
def on_reboot_response(self, dialog, response):
|
|
if response == "reboot":
|
|
try:
|
|
subprocess.run(['efibootmgr', '--bootnext', self.selected_entry.num],
|
|
check=True)
|
|
subprocess.run(['systemctl', 'reboot'], check=True)
|
|
except subprocess.CalledProcessError as e:
|
|
self.show_error("Failed to set boot entry",
|
|
f"Command failed with exit code {e.returncode}")
|
|
except Exception as e:
|
|
self.show_error("Error", str(e))
|
|
|
|
def show_error(self, heading, body):
|
|
dialog = Adw.MessageDialog.new(self)
|
|
dialog.set_heading(heading)
|
|
dialog.set_body(body)
|
|
dialog.add_response("ok", "OK")
|
|
dialog.set_default_response("ok")
|
|
dialog.present()
|
|
|
|
class AdvancedRebootApp(Adw.Application):
|
|
def __init__(self):
|
|
super().__init__(application_id='com.example.AdvancedReboot',
|
|
flags=Gio.ApplicationFlags.FLAGS_NONE)
|
|
|
|
def do_activate(self):
|
|
win = AdvancedRebootWindow(application=self)
|
|
win.present()
|
|
|
|
def get_user_theme_preference():
|
|
try:
|
|
original_user = os.environ.get('SUDO_USER') or os.environ.get('PKEXEC_UID')
|
|
|
|
if original_user:
|
|
if original_user.isdigit():
|
|
import pwd
|
|
original_user = pwd.getpwuid(int(original_user)).pw_name
|
|
import pwd
|
|
user_home = pwd.getpwnam(original_user).pw_dir
|
|
|
|
user_runtime_dir = f"/run/user/{pwd.getpwnam(original_user).pw_uid}"
|
|
|
|
result = subprocess.run(
|
|
['sudo', '-u', original_user, 'gsettings', 'get', 'org.gnome.desktop.interface', 'color-scheme'],
|
|
capture_output=True,
|
|
text=True,
|
|
env={
|
|
'DBUS_SESSION_BUS_ADDRESS': f'unix:path={user_runtime_dir}/bus',
|
|
'HOME': user_home
|
|
}
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
theme = result.stdout.strip().strip("'")
|
|
return theme
|
|
except Exception as e:
|
|
print(f"Could not get user theme: {e}", file=sys.stderr)
|
|
|
|
return None
|
|
|
|
if __name__ == '__main__':
|
|
if os.geteuid() != 0:
|
|
print("Error: This program must be run as root", file=sys.stderr)
|
|
print(f"Use: pkexec {sys.argv[0]}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
user_theme = get_user_theme_preference()
|
|
|
|
app = AdvancedRebootApp()
|
|
|
|
if user_theme:
|
|
style_manager = Adw.StyleManager.get_default()
|
|
if 'dark' in user_theme.lower():
|
|
style_manager.set_color_scheme(Adw.ColorScheme.FORCE_DARK)
|
|
elif 'light' in user_theme.lower():
|
|
style_manager.set_color_scheme(Adw.ColorScheme.FORCE_LIGHT)
|
|
else:
|
|
style_manager.set_color_scheme(Adw.ColorScheme.DEFAULT)
|
|
|
|
app.run(None) |