commit 573610ae4ee4017074f520bf4359d6de427e264b Author: Morgan Date: Wed Nov 19 19:29:34 2025 +0900 Initial commit Add screenshot to README diff --git a/README.md b/README.md new file mode 100644 index 0000000..0724b14 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +![Screenshot](image.png) diff --git a/advanced-reboot b/advanced-reboot new file mode 100755 index 0000000..0e6de48 --- /dev/null +++ b/advanced-reboot @@ -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: {clean_name}") + 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) \ No newline at end of file diff --git a/com.example.advancedreboot.desktop b/com.example.advancedreboot.desktop new file mode 100644 index 0000000..8f33ca1 --- /dev/null +++ b/com.example.advancedreboot.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=Advanced Reboot +Comment=Choose boot entry for next reboot +Exec=pkexec /usr/local/bin/advanced-reboot +Icon=system-reboot +Terminal=false +Type=Application +Categories=System;Settings; diff --git a/com.example.advancedreboot.policy b/com.example.advancedreboot.policy new file mode 100644 index 0000000..0c70d6d --- /dev/null +++ b/com.example.advancedreboot.policy @@ -0,0 +1,20 @@ + + + + Custom + https://example.com + + + Run Advanced Reboot Tool + Authentication is required to access boot configuration and reboot + system-reboot + + no + no + auth_admin + + /usr/local/bin/advanced-reboot + true + + diff --git a/image.png b/image.png new file mode 100644 index 0000000..e12f986 Binary files /dev/null and b/image.png differ diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..ff4b898 --- /dev/null +++ b/install.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +sudo install -m 755 advanced-reboot /usr/local/bin/ + +sudo install -m 644 com.example.advancedreboot.policy /usr/share/polkit-1/actions/ + +sudo install -m 644 com.example.advancedreboot.desktop /usr/share/applications/ \ No newline at end of file