mirror of
https://github.com/morgan9e/AdvancedReboot
synced 2026-04-14 00:14:35 +09:00
Initial commit
Add screenshot to README
This commit is contained in:
271
advanced-reboot
Executable file
271
advanced-reboot
Executable file
@@ -0,0 +1,271 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user