"""WizardKit: Setup - Windows""" # vim: sts=2 sw=2 ts=2 import logging import os import platform import re import sys import time from subprocess import CalledProcessError, DEVNULL from wk.cfg.main import KIT_NAME_FULL, KIT_NAME_SHORT from wk.exe import ( get_procs, run_program, popen_program, wait_for_procs, ) from wk.io import ( delete_folder, get_path_obj, non_clobber_path, rename_item, ) from wk.kit.tools import ( ARCH, download_tool, extract_archive, extract_tool, find_kit_dir, get_tool_path, run_tool, ) from wk.log import format_log_path, update_log_path from wk.os.win import ( reg_delete_value, reg_read_value, reg_set_value, reg_write_settings, disable_service, enable_service, stop_service, ) from wk.repairs.win import ( backup_all_browser_profiles, backup_registry, create_custom_power_plan, create_system_restore_point, enable_windows_updates, export_power_plans, reset_power_plans, set_system_restore_size, ) from wk.std import ( GenericError, GenericWarning, Menu, TryAndPrint, abort, ask, clear_screen, color_string, pause, print_info, print_standard, print_warning, set_title, show_data, sleep, strip_colors, ) # STATIC VARIABLES LOG = logging.getLogger(__name__) CONEMU_EXE = get_tool_path('ConEmu', 'ConEmu', check=False) IN_CONEMU = 'ConEmuPID' in os.environ LIBREOFFICE_XCU_DATA = ''' Impress MS PowerPoint 2007 XML Calc MS Excel 2007 XML MS Word 2007 XML false ''' MENU_PRESETS = Menu() OS_VERSION = float(platform.win32_ver()[0]) PROGRAMFILES_32 = os.environ.get( 'PROGRAMFILES(X86)', os.environ.get( 'PROGRAMFILES', r'C:\Program Files (x86)', ), ) PROGRAMFILES_64 = os.environ.get( 'PROGRAMW6432', os.environ.get( 'PROGRAMFILES', r'C:\Program Files', ), ) REG_OPEN_SHELL_SETTINGS = { 'HKCU': { r'Software\OpenShell\StartMenu': ( ('ShowedStyle2', 1, 'DWORD'), ), r'Software\OpenShell\StartMenu\Settings': ( ('MenuStyle', 'Win7', 'SZ'), ('RecentPrograms', 'Recent', 'SZ'), ('SkinW7', 'Fluent-Metro', 'SZ'), ('SkinVariationW7', '', 'SZ'), ('SkipMetro', 1, 'DWORD'), ( 'SkinOptionsW7', [ # NOTE: Seems to need all options specified to work? 'DARK_MAIN=1', 'METRO_MAIN=0', 'LIGHT_MAIN=0', 'AUTOMODE_MAIN=0', 'DARK_SUBMENU=0', 'METRO_SUBMENU=0', 'LIGHT_SUBMENU=0', 'AUTOMODE_SUBMENU=1', 'SUBMENU_SEPARATORS=1', 'DARK_SEARCH=0', 'METRO_SEARCH=0', 'LIGHT_SEARCH=1', 'AUTOMODE_SEARCH=0', 'SEARCH_FRAME=1', 'SEARCH_COLOR=0', 'SMALL_SEARCH=0', 'MODERN_SEARCH=1', 'SEARCH_ITALICS=0', 'NONE=0', 'SEPARATOR=0', 'TWO_TONE=1', 'CLASSIC_SELECTOR=1', 'HALF_SELECTOR=0', 'CURVED_MENUSEL=1', 'CURVED_SUBMENU=0', 'SELECTOR_REVEAL=0', 'TRANSPARENT=0', 'OPAQUE_SUBMENU=1', 'OPAQUE_MENU=0', 'OPAQUE=0', 'STANDARD=1', 'SMALL_MAIN2=0', 'SMALL_ICONS=0', 'COMPACT_SUBMENU=0', 'PRESERVE_MAIN2=0', 'LESS_PADDING=0', 'EXTRA_PADDING=1', '24_PADDING=0', 'LARGE_PROGRAMS=0', 'TRANSPARENT_SHUTDOWN=0', 'OUTLINE_SHUTDOWN=0', 'BUTTON_SHUTDOWN=1', 'EXPERIMENTAL_SHUTDOWN=0', 'LARGE_FONT=0', 'CONNECTED_BORDER=1', 'FLOATING_BORDER=0', 'LARGE_SUBMENU=0', 'LARGE_LISTS=0', 'THIN_MAIN2=0', 'EXPERIMENTAL_MAIN2=1', 'USER_IMAGE=1', 'USER_OUTSIDE=0', 'SCALING_USER=1', '56=0', '64=0', 'TRANSPARENT_USER=0', 'UWP_SCROLLBAR=1', 'MODERN_SCROLLBAR=0', 'OLD_ICONS=0', 'NEW_ICONS=1', 'SMALL_ARROWS=0', 'ICON_FRAME=0', 'SEARCH_SEPARATOR=0', 'NO_PROGRAMS_BUTTON=0', ], 'MULTI_SZ', ), ), }, } SYSTEMDRIVE = os.environ.get('SYSTEMDRIVE', 'C:') WIDTH = 50 TRY_PRINT = TryAndPrint() TRY_PRINT.width = WIDTH TRY_PRINT.verbose = True for error in ('CalledProcessError', 'FileNotFoundError'): TRY_PRINT.add_error(error) # Auto Setup def build_menus(base_menus, title, presets): """Build menus, returns dict.""" menus = {} menus['Main'] = Menu(title=f'{title}\n{color_string("Main Menu", "GREEN")}') # Main Menu for entry in base_menus['Actions']: menus['Main'].add_action(entry.name, entry.details) for group in base_menus['Groups']: menus['Main'].add_option(group, {'Selected': True}) # Run groups for group, entries in base_menus['Groups'].items(): menus[group] = Menu(title=f'{title}\n{color_string(group, "GREEN")}') for entry in entries: menus[group].add_option(entry.name, entry.details) menus[group].add_action('All') menus[group].add_action('None') menus[group].add_action('Main Menu', {'Separator': True}) menus[group].add_action('Quit') # Initialize main menu display names menus['Main'].update() # Fix Function references for group, menu in menus.items(): if group not in base_menus['Groups']: continue for name in menu.options: _function = menu.options[name]['Function'] if isinstance(_function, str): menu.options[name]['Function'] = getattr( sys.modules[__name__], _function, ) # Update presets Menu MENU_PRESETS.title = f'{title}\n{color_string("Load Preset", "GREEN")}' MENU_PRESETS.add_option('Default') for name in presets: MENU_PRESETS.add_option(name) MENU_PRESETS.add_action('Main Menu') MENU_PRESETS.add_action('Quit') MENU_PRESETS.update() # Done return menus def load_preset(menus, presets): """Load menu settings from preset.""" selection = MENU_PRESETS.simple_select() # Exit early if 'Main Menu' in selection: return if 'Quit' in selection: raise SystemExit # Default case if 'Default' in selection: for menu in menus.values(): for name in menu.options: menu.options[name]['Selected'] = True return # Load preset preset = presets[selection[0]] for group, menu in menus.items(): group_enabled = group in preset for name in menu.options: value = group_enabled and name in preset[group] menu.options[name]['Selected'] = value def run_auto_setup(base_menus, presets): """Run Auto Setup.""" update_log_path(dest_name='Auto Setup', timestamp=True) title = f'{KIT_NAME_FULL}: Auto Setup' clear_screen() set_title(title) print_info(title) print('') # Generate menus print_standard('Initializing...') menus = build_menus(base_menus, title, presets) # Show Menu show_main_menu(base_menus, menus, presets) # Start setup clear_screen() print_standard(title) print('') print_info('Running setup') # Run setup for group, menu in menus.items(): if group in ('Main', 'Options'): continue try: run_group(group, menu) except KeyboardInterrupt: abort() # Done print_info('Done') pause('Press Enter to exit...') def run_group(group, menu): """Run entries in group if appropriate.""" print_info(f' {group}') for name, details in menu.options.items(): name_str = strip_colors(name) # Not selected if not details.get('Selected', False): show_data(f'{name_str}...', 'Skipped', 'YELLOW', width=WIDTH) continue # Selected details['Function']() def show_main_menu(base_menus, menus, presets): """Show main menu and handle actions.""" while True: update_main_menu(menus) selection = menus['Main'].simple_select(update=False) if selection[0] in base_menus['Groups'] or selection[0] == 'Options': show_sub_menu(menus[selection[0]]) if selection[0] == 'Load Preset': load_preset(menus, presets) elif 'Start' in selection: break elif 'Quit' in selection: raise SystemExit def show_sub_menu(menu): """Show sub-menu and handle sub-menu actions.""" while True: selection = menu.advanced_select() if 'Main Menu' in selection: break if 'Quit' in selection: raise SystemExit # Select all or none value = 'All' in selection for name in menu.options: if not menu.options[name].get('Disabled', False): menu.options[name]['Selected'] = value def update_main_menu(menus): """Update main menu based on current selections.""" index = 1 skip = 'Reboot' for name in menus['Main'].options: checkmark = ' ' selected = [ _v['Selected'] for _k, _v in menus[name].options.items() if _k != skip ] if all(selected): checkmark = '✓' elif any(selected): checkmark = '-' display_name = f' {index}: [{checkmark}] {name}' index += 1 menus['Main'].options[name]['Display Name'] = display_name # Auto Repairs: Wrapper Functions def auto_backup_registry(): """Backup registry.""" TRY_PRINT.run('Backup Registry...', backup_registry) def auto_backup_browser_profiles(): """Backup browser profiles.""" backup_all_browser_profiles(use_try_print=True) def auto_backup_power_plans(): """Backup power plans.""" TRY_PRINT.run('Backup Power Plans...', export_power_plans) def auto_reset_power_plans(): """Reset power plans.""" TRY_PRINT.run('Reset Power Plans...', reset_power_plans) def auto_set_custom_power_plan(): """Set custom power plan.""" TRY_PRINT.run('Set Custom Power Plan...', create_custom_power_plan) def auto_enable_regback(): """Enable RegBack.""" TRY_PRINT.run( 'Enable RegBack...', reg_set_value, 'HKLM', r'System\CurrentControlSet\Control\Session Manager\Configuration Manager', 'EnablePeriodicBackup', 1, 'DWORD', ) def auto_system_restore_enable(): """Enable System Restore.""" cmd = [ 'powershell', '-Command', 'Enable-ComputerRestore', '-Drive', SYSTEMDRIVE, ] TRY_PRINT.run('Enable System Restore...', run_program, cmd=cmd) def auto_system_restore_set_size(): """Set System Restore size.""" TRY_PRINT.run('Set System Restore Size...', set_system_restore_size) def auto_system_restore_create(): """Create System Restore point.""" TRY_PRINT.run('Create System Restore...', create_system_restore_point) def auto_windows_updates_enable(): """Enable Windows Updates.""" TRY_PRINT.run('Enable Windows Updates...', enable_windows_updates) # Auto Setup: Wrapper Functions def auto_config_open_shell(): """Configure Open Shell.""" TRY_PRINT.run('Open Shell...', reg_write_settings, REG_OPEN_SHELL_SETTINGS) def auto_install_libreoffice(): """Install LibreOffice. NOTE: It is assumed that auto_install_vcredists() will be run before this function to satisfy the vcredist=False usage. """ TRY_PRINT.run('LibreOffice...', install_libreoffice, vcredist=False) def auto_install_open_shell(): """Install Open Shell.""" TRY_PRINT.run('Open Shell...', install_open_shell) def auto_install_vcredists(): """Install latest supported Visual C++ runtimes.""" TRY_PRINT.run('Visual C++ Runtimes...', install_vcredists) # Configure Functions # TODO? # Install Functions def install_libreoffice( register_mso_types=True, use_mso_formats=False, vcredist=True): """Install LibreOffice.""" installer = find_kit_dir('Installers').joinpath('LibreOffice.msi') xcu_dir = get_path_obj(f'{os.environ.get("APPDATA")}/LibreOffice/4/user') xcu_file = xcu_dir.joinpath('registrymodifications.xcu') # Set default save formats to MSO types if use_mso_formats and not xcu_file.exists(): xcu_dir.mkdir(parents=True, exist_ok=True) with open(xcu_file, 'w', encoding='utf-8', newline='\n') as _f: _f.write(LIBREOFFICE_XCU_DATA) # Build cmd cmd = [ 'msiexec', '/passive', '/norestart', '/i', installer, 'REBOOTYESNO=No', f'VC_REDIST={1 if vcredist else 0}', ] if register_mso_types: cmd.append('REGISTER_ALL_MSO_TYPES=1') else: cmd.append('REGISTER_NO_MSO_TYPES=1') # Install LibreOffice run_program(cmd) def install_open_shell(): """Install Open Shell (just the Start Menu).""" installer = get_tool_path('OpenShell', 'OpenShell', check=False) download_tool('OpenShell', 'OpenShell') download_tool('OpenShell', 'Fluent-Metro', suffix='zip') cmd = [installer, '/qn', 'ADDLOCAL=StartMenu'] run_program(cmd) # Install Skin skin_zip = installer.with_name('Fluent-Metro.zip') extract_archive(skin_zip, f'{PROGRAMFILES_64}/Open-Shell/Skins', '-aoa') # Add scheduled task to handle OS upgrades cmd = ['schtasks', '/query', '/tn', 'Open-Shell OS upgrade check'] proc = run_program(cmd, check=False) if proc.returncode == 0: # Task already exists, bail and leave current task unmodified return cmd = [ 'schtasks', '/create', '/ru', r'NT AUTHORITY\SYSTEM', '/sc', 'ONSTART', '/tn', 'Open-Shell OS upgrade check', '/tr', r'"%PROGRAMFILES%\Open-Shell\StartMenu.exe" -upgrade -silent', ] run_program(cmd) def install_vcredists(): """Install latest supported Visual C++ runtimes.""" for year in (2012, 2013, 2019): cmd_args = ['/install', '/passive', '/norestart'] if year == 2012: cmd_args.pop(0) name = f'VCRedist_{year}_x32' download_tool('VCRedist', name) installer = get_tool_path('VCRedist', name) run_program([installer, *cmd_args]) if ARCH == '64': name = f'{name[:-2]}64' download_tool('VCRedist', name) installer = get_tool_path('VCRedist', name) run_program([installer, *cmd_args]) # Misc Functions ## TODO? # Tool Functions ## TODO? # OS Built-in Functions ## TODO? if __name__ == '__main__': print("This file is not meant to be called directly.")