"""Python interface to whiptail command.""" import subprocess import time import logging import shlex from concurrent.futures import ThreadPoolExecutor from collections.abc import Sequence from typing import Any, Dict, List, Tuple, Optional, Union from .const import TITLE, WIDTH, HEIGHT, LIST_HEIGHT ItemType = Union[str, Tuple[Any, str]] _LOGGER = logging.getLogger() def whiptail(*args) -> Optional[str]: proc = subprocess.Popen( ["whiptail", "--title", TITLE] + list(args), stderr=subprocess.PIPE, ) _stdout, stderr = proc.communicate() if proc.returncode != 0: return None return stderr.decode("utf-8") def menu( text: str, items: Sequence[ItemType], selected_item: Optional[str] = None, menu_args: Optional[Sequence[str]] = None, ) -> Optional[str]: assert items, "No items" item_map: Dict[Optional[str], str] = {} item_args: List[str] = [] selected_tag: Optional[str] = None for i, item in enumerate(items): item_id = str(i) item_args.append(item_id) if isinstance(item, str): item_map[item_id] = item item_args.append(item) if selected_item == item: selected_tag = item_id else: item_map[item_id] = item[0] item_args.append(item[1]) if selected_item == item[0]: selected_tag = item_id menu_args = list(menu_args) if menu_args is not None else [] if selected_tag is not None: menu_args.extend(["--default-item", selected_tag]) result = whiptail( "--notags", *menu_args, "--menu", text, HEIGHT, WIDTH, LIST_HEIGHT, *item_args ) return item_map.get(result) def inputbox(text: str, init: Optional[Any] = None) -> Optional[str]: return whiptail("--inputbox", text, HEIGHT, WIDTH, str(init) or "") def passwordbox(text: str) -> Optional[str]: return whiptail("--passwordbox", text, HEIGHT, WIDTH) def radiolist( text: str, items: Sequence[ItemType], selected_item: Any, *args, ) -> Optional[Any]: assert items, "No items" item_map: Dict[str, ItemType] = {} item_args: List[str] = [] for i, item in enumerate(items): item_id = str(i) item_args.append(item_id) if isinstance(item, str): item_map[item_id] = item item_args.append(item) if item == selected_item: item_args.append("1") else: item_args.append("0") else: item_map[item_id] = item[0] item_args.append(item[1]) if item[0] == selected_item: item_args.append("1") else: item_args.append("0") result = whiptail( "--notags", *args, "--radiolist", text, HEIGHT, WIDTH, LIST_HEIGHT, *item_args ) if result is None: return None return item_map.get(result, result) def checklist( text: str, items: Sequence[ItemType], selected_items: Sequence[Any], *args, ) -> Optional[List[Any]]: assert items, "No items" item_map: Dict[str, ItemType] = {} item_args: List[str] = [] for i, item in enumerate(items): item_id = str(i) item_args.append(item_id) if isinstance(item, str): item_map[item_id] = item item_args.append(item) if item in selected_items: item_args.append("1") else: item_args.append("0") else: item_map[item_id] = item[0] item_args.append(item[1]) if item[0] in selected_items: item_args.append("1") else: item_args.append("0") result = whiptail( "--notags", *args, "--checklist", text, HEIGHT, WIDTH, LIST_HEIGHT, *item_args ) if result is None: return None return [ item_map.get(result_item, result_item) for result_item in shlex.split(result) ] def yesno(text: str) -> bool: return whiptail("--yesno", text, HEIGHT, WIDTH) is not None def msgbox(text: str) -> None: whiptail("--msgbox", text, HEIGHT, WIDTH) def gauge(text: str, seconds: int, parts: int = 20) -> None: proc = subprocess.Popen( ["whiptail", "--title", TITLE, "--gauge", text, HEIGHT, WIDTH, "0"], stdin=subprocess.PIPE, stderr=subprocess.PIPE, text=True, ) assert proc.stdin is not None percent = 0 while percent <= 100: time.sleep(seconds / parts) percent += int(100 / parts) print(percent, file=proc.stdin, flush=True) proc.communicate() def error(reason: str) -> None: msgbox(f"An error occurred while {reason}.\n" "See local/installer.log for details.") # ----------------------------------------------------------------------------- def run_with_gauge( text: str, commands: Sequence[Sequence[str]], sudo_password: Optional[str] = None ) -> bool: proc = subprocess.Popen( ["whiptail", "--title", TITLE, "--gauge", text, HEIGHT, WIDTH, "0"], stdin=subprocess.PIPE, stderr=subprocess.PIPE, text=True, ) assert proc.stdin is not None percent = 0 seconds = 5 parts = 20 with ThreadPoolExecutor() as executor: for command in commands: future = executor.submit(_run_command, command, sudo_password) while not future.done(): time.sleep(seconds / parts) percent += int(100 / parts) if percent > 100: percent = 0 print(percent, file=proc.stdin, flush=True) if not future.result(): # Error occurred return False proc.communicate() return True def _run_command(command: Sequence[str], sudo_password: Optional[str] = None) -> bool: try: assert command proc_input: Optional[str] = None if (command[0] == "sudo") and (sudo_password is not None): proc_input = sudo_password proc = subprocess.Popen( command, stdin=subprocess.PIPE, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, text=True, ) _stdout, stderr = proc.communicate(proc_input) if proc.returncode != 0: _LOGGER.error("Error running command: %s", command) _LOGGER.error(stderr) return False except Exception: _LOGGER.exception("Error running command: %s", command) return False return True