Files
AdvancedReboot/advanced-reboot
Morgan 573610ae4e Initial commit
Add screenshot to README
2025-11-19 19:30:15 +09:00

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)