diff --git a/archinstall/applications/power_management.py b/archinstall/applications/power_management.py new file mode 100644 index 0000000000..94b67ebfc0 --- /dev/null +++ b/archinstall/applications/power_management.py @@ -0,0 +1,39 @@ +from typing import TYPE_CHECKING + +from archinstall.lib.models.application import PowerManagement, PowerManagementConfiguration +from archinstall.lib.output import debug + +if TYPE_CHECKING: + from archinstall.lib.installer import Installer + + +class PowerManagementApp: + @property + def ppd_packages(self) -> list[str]: + return [ + 'power-profiles-daemon', + ] + + @property + def tuned_packages(self) -> list[str]: + return [ + 'tuned', + 'tuned-ppd', + ] + + def install( + self, + install_session: 'Installer', + power_management_config: PowerManagementConfiguration, + ) -> None: + debug(f'Installing power management daemon: {power_management_config.power_management.value}') + + if power_management_config.power_management == PowerManagement.NO_POWER_MANAGEMENT: + debug('No power management daemon selected, skipping installation.') + return + + match power_management_config.power_management: + case PowerManagement.POWER_PROFILES_DAEMON: + install_session.add_additional_packages(self.ppd_packages) + case PowerManagement.TUNED: + install_session.add_additional_packages(self.tuned_packages) diff --git a/archinstall/lib/applications/application_handler.py b/archinstall/lib/applications/application_handler.py index 990b49fb69..15fa539244 100644 --- a/archinstall/lib/applications/application_handler.py +++ b/archinstall/lib/applications/application_handler.py @@ -2,9 +2,10 @@ from archinstall.applications.audio import AudioApp from archinstall.applications.bluetooth import BluetoothApp +from archinstall.applications.power_management import PowerManagementApp from archinstall.applications.print_service import PrintServiceApp from archinstall.lib.models import Audio -from archinstall.lib.models.application import ApplicationConfiguration +from archinstall.lib.models.application import ApplicationConfiguration, PowerManagement from archinstall.lib.models.users import User if TYPE_CHECKING: @@ -26,6 +27,12 @@ def install_applications(self, install_session: 'Installer', app_config: Applica users, ) + if app_config.power_management_config and app_config.power_management_config.power_management != PowerManagement.NO_POWER_MANAGEMENT: + PowerManagementApp().install( + install_session, + app_config.power_management_config, + ) + if app_config.print_service_config and app_config.print_service_config.enabled: PrintServiceApp().install(install_session) diff --git a/archinstall/lib/applications/application_menu.py b/archinstall/lib/applications/application_menu.py index 30ca4dbb84..774c7a57c5 100644 --- a/archinstall/lib/applications/application_menu.py +++ b/archinstall/lib/applications/application_menu.py @@ -1,7 +1,16 @@ from typing import override +from archinstall.lib.hardware import SysInfo from archinstall.lib.menu.abstract_menu import AbstractSubMenu -from archinstall.lib.models.application import ApplicationConfiguration, Audio, AudioConfiguration, BluetoothConfiguration, PrintServiceConfiguration +from archinstall.lib.models.application import ( + ApplicationConfiguration, + Audio, + AudioConfiguration, + BluetoothConfiguration, + PowerManagement, + PowerManagementConfiguration, + PrintServiceConfiguration, +) from archinstall.lib.translationhandler import tr from archinstall.tui.curses_menu import SelectMenu from archinstall.tui.menu_item import MenuItem, MenuItemGroup @@ -54,8 +63,21 @@ def _define_menu_options(self) -> list[MenuItem]: preview_action=self._prev_print_service, key='print_service_config', ), + MenuItem( + text=tr('Power management daemon'), + action=select_power_management, + preview_action=self._prev_power_management, + enabled=SysInfo.has_battery(), + key='power_management_config', + ), ] + def _prev_power_management(self, item: MenuItem) -> str | None: + if item.value is not None: + config: PowerManagementConfiguration = item.value + return f'{tr("Power management daemon")}: {config.power_management.value}' + return None + def _prev_bluetooth(self, item: MenuItem) -> str | None: if item.value is not None: bluetooth_config: BluetoothConfiguration = item.value @@ -81,6 +103,29 @@ def _prev_print_service(self, item: MenuItem) -> str | None: return None +def select_power_management(preset: PowerManagementConfiguration | None = None) -> PowerManagementConfiguration | None: + items = [MenuItem(a.value, value=a) for a in PowerManagement] + group = MenuItemGroup(items) + + if preset: + group.set_focus_by_value(preset.power_management) + + result = SelectMenu[PowerManagement]( + group, + allow_skip=True, + alignment=Alignment.CENTER, + frame=FrameProperties.min(tr('Power management daemon')), + ).run() + + match result.type_: + case ResultType.Skip: + return preset + case ResultType.Selection: + return PowerManagementConfiguration(power_management=result.get_value()) + case ResultType.Reset: + raise ValueError('Unhandled result type') + + def select_bluetooth(preset: BluetoothConfiguration | None) -> BluetoothConfiguration | None: group = MenuItemGroup.yes_no() group.focus_item = MenuItem.no() diff --git a/archinstall/lib/hardware.py b/archinstall/lib/hardware.py index 3e675f32b4..459a004149 100644 --- a/archinstall/lib/hardware.py +++ b/archinstall/lib/hardware.py @@ -210,6 +210,18 @@ def graphics_devices(self) -> dict[str, str]: class SysInfo: + @staticmethod + def has_battery() -> bool: + for type_path in Path('/sys/class/power_supply/').glob('*/type'): + try: + with open(type_path) as f: + if f.read().strip() == 'Battery': + return True + except OSError: + continue + + return False + @staticmethod def has_wifi() -> bool: ifaces = list(list_interfaces().values()) diff --git a/archinstall/lib/models/application.py b/archinstall/lib/models/application.py index f5f9e99d8f..715155f8cd 100644 --- a/archinstall/lib/models/application.py +++ b/archinstall/lib/models/application.py @@ -3,6 +3,16 @@ from typing import Any, NotRequired, TypedDict +class PowerManagement(StrEnum): + NO_POWER_MANAGEMENT = 'No power management daemon' + POWER_PROFILES_DAEMON = 'power-profiles-daemon' + TUNED = auto() + + +class PowerManagementConfigSerialization(TypedDict): + power_management: str + + class BluetoothConfigSerialization(TypedDict): enabled: bool @@ -24,6 +34,7 @@ class PrintServiceConfigSerialization(TypedDict): class ApplicationSerialization(TypedDict): bluetooth_config: NotRequired[BluetoothConfigSerialization] audio_config: NotRequired[AudioConfigSerialization] + power_management_config: NotRequired[PowerManagementConfigSerialization] print_service_config: NotRequired[PrintServiceConfigSerialization] @@ -55,6 +66,22 @@ def parse_arg(arg: dict[str, Any]) -> 'BluetoothConfiguration': return BluetoothConfiguration(arg['enabled']) +@dataclass +class PowerManagementConfiguration: + power_management: PowerManagement + + def json(self) -> PowerManagementConfigSerialization: + return { + 'power_management': self.power_management.value, + } + + @staticmethod + def parse_arg(arg: dict[str, Any]) -> 'PowerManagementConfiguration': + return PowerManagementConfiguration( + PowerManagement(arg['power_management']), + ) + + @dataclass class PrintServiceConfiguration: enabled: bool @@ -71,6 +98,7 @@ def parse_arg(arg: dict[str, Any]) -> 'PrintServiceConfiguration': class ApplicationConfiguration: bluetooth_config: BluetoothConfiguration | None = None audio_config: AudioConfiguration | None = None + power_management_config: PowerManagementConfiguration | None = None print_service_config: PrintServiceConfiguration | None = None @staticmethod @@ -90,6 +118,9 @@ def parse_arg( if args and (audio_config := args.get('audio_config')) is not None: app_config.audio_config = AudioConfiguration.parse_arg(audio_config) + if args and (power_management_config := args.get('power_management_config')) is not None: + app_config.power_management_config = PowerManagementConfiguration.parse_arg(power_management_config) + if args and (print_service_config := args.get('print_service_config')) is not None: app_config.print_service_config = PrintServiceConfiguration.parse_arg(print_service_config) @@ -104,6 +135,9 @@ def json(self) -> ApplicationSerialization: if self.audio_config: config['audio_config'] = self.audio_config.json() + if self.power_management_config: + config['power_management_config'] = self.power_management_config.json() + if self.print_service_config: config['print_service_config'] = self.print_service_config.json() diff --git a/archinstall/locales/base.pot b/archinstall/locales/base.pot index 4afd74ab7f..4facbf9201 100644 --- a/archinstall/locales/base.pot +++ b/archinstall/locales/base.pot @@ -1788,6 +1788,9 @@ msgstr "" msgid "Would you like to configure the print service?" msgstr "" +msgid "Power management daemon" +msgstr "" + msgid "Authentication" msgstr ""