diff --git a/default.py b/default.py new file mode 100644 index 0000000..85502ac --- /dev/null +++ b/default.py @@ -0,0 +1,341 @@ +# The Mac OS Updater +## Install macOS updates from the TUI. + +import os; import time as clock + + +## app information +class app: + title = "The Mac OS Updater" + description = "Install macOS updates from the TUI." + version = 22.0616 ### YY.MMDD + +## localization +class texts: + current_language = "en" + + class screens: + class titles: + s_2_0 = 'Download macOS' + s_2_1 = 'Advanced Options' + s_2_2 = 'Beta Update Branch Removal' + s_2_2_1 = 'Disabling Beta Branch Search…' + + class messages: + s_2_1 = 'Select the beta update branch to be included.' + s_2_2 = 'Do you want to remove the beta update branch when looking for updates? ' + s_2_3 = 'Please read and agree to the lisence before to include the public beta update branch.' + s_2_4 = 'Please read and agree to the lisence before to include the developer beta update branch.' + + class menus: + main = ['quit', 'refresh', 'install updates', 'download macOS', 'help'] + s_2_0 = ['⬅︎' , 'refresh', 'advanced', 'download: Type an OS version number to download it.'] + s_2_1 = ['⬅︎' , '🚫', 'beta release', 'developer release'] + s_2_2 = ['⬅︎' , 'revert'] + s_2_3 = ['⬅︎' , 'agree'] + s_2_4 = ['⬅︎' , 'agree'] + + class status: + class checkCompatibility: + checking = 'Checking compatibility…' + done = '' + + class errors: + class compatibility: + incompatibility = "There is no use running this outside macOS." + noSoftwareUpdater = "There is no CLI software updater. \nAre you using an old version of macOS?" + + class quit: + terminationPrompt = "The program refuses to proceed, so it will be closed. \nTo continue, click enter or quit the program yourself." + + class default: + syntaxerr = "There is a spelling mistake!" + typeerr = "Too bad, something couldn't be converted." + attributeerr = "Something was not linked in properly. " + + class screens: + s_2_2_1: 'Sorry. You are still stuck with the beta branch search enabled. \nPress ^c to go back, and come back to this page to try again.' +try: + ## on Terminal outputs + ### text formatting + class formatting: + clear = '\033[0m' + class color: + purple = '\033[95m' + cyan = '\033[96m' + cyan_dark = '\033[36m' + blue = '\033[94m' + green = '\033[92m' + yellow = '\033[93m' + red = '\033[91m' + class font: + bold = '\033[1m' + underline = '\033[4m' + + ### screen functions + def cls(times = 1): #### to clear screen + times = int(abs(times)); + for _ in range(times): + try: + ipy_str = str(type(get_ipython())) + if 'zmqshell' in ipy_str: from IPython.display import clear_output as clear; clear() + elif 'terminal' in ipy_str: clear + except NameError: ##### The user is running directly from the terminal, which is expected. + import os; return(os.system('clear')) + + class info: ### screen messages + def status(message): print(formatting.font.bold + 'ℹ️\t' + formatting.clear + message) + def err(message): print(formatting.font.bold + formatting.color.red + '❌\t' + formatting.clear + formatting.clear + message) + def warning(message): print(formatting.font.bold + formatting.color.yellow + '⚠️\t' + formatting.clear + formatting.clear + message) + def success(message): print(formatting.font.bold + formatting.color.green + '✅\t' + formatting.clear + formatting.clear + message) + + ### terminate with fail function + def fail(message = '', displayAppInformation = True, critical = True): + import time; + + if critical: + try: + if displayAppInformation: operations.display_title_screen(); print('\n') + info.err(message); input('\n\n' + texts.errors.quit.terminationPrompt); raise KeyboardInterrupt + except KeyboardInterrupt: cls(); exit() + else: print(message) + time.sleep(1); + + class user: ## user data + class navigation: ### TLI data + screen = 0 + response = '' + + class window: + try: + width = int((os.get_terminal_size()).columns) + lines = int((os.get_terminal_size()).lines) + except OSError: + width = False + lines = False + + class compatibility: ### for operations.check_compatibility() + isRunningMacOS = (os.name =='posix' and not(os.system('cd /Applications')) and not(os.system('cd /Library')) and not(os.system('cd /System/Library'))) + hasCLIUpdaterInstalled = not(os.system('softwareupdate --help >/dev/null 2>&1')) #### Check if softwareupdate command is there. + + class shell: + try: #### determine Python type, if running in terminal, ipython, or jupyter + ipy_str = str(type(get_ipython())) + if 'zmqshell' in ipy_str: pythonType = 'jupyter' + elif 'terminal' in ipy_str: pythonType = 'ipython' + except: + pythonType = 'terminal' + + class operations: ## all operations + def display_title_screen(message = ''): ### display title screen and optional message + screenManager.menu.titleBar(app.title); print (app.description + '\nV' + str(app.version)) #### display app information + message = str(message).strip() + if message != '': print(message) #### display message + + def check_compatibility(): ### compatibility check + cls() + operations.display_title_screen(texts.status.checkCompatibility.checking) + + import os + if user.compatibility.isRunningMacOS: + if user.compatibility.hasCLIUpdaterInstalled: + return True + else: + fail(texts.errors.compatibility.noSoftwareUpdater); return False + else: + fail(texts.errors.compatibility.noSoftwareUpdater); return False + + class software: + def check_updates(): + import os + return(os.system('softwareupdate -l')) + def check_OS_installers(): + import os + return(os.system('softwareupdate --list-full-installers')) + + class installer_branch: + def none(): + import os; return(os.system('sudo /System/Library/PrivateFrameworks/Seeding.framework/Versions/A/Resources/seedutil unenroll')) + + def publicBeta(): + import os; return(os.system('sudo /System/Library/PrivateFrameworks/Seeding.framework/Versions/A/Resources/seedutil enroll PublicSeed')) + + def privateBeta(): + import os; return(os.system('sudo /System/Library/PrivateFrameworks/Seeding.framework/Versions/A/Resources/seedutil enroll DeveloperSeed')) + + class screenManager: ## TLI + + class graphical: + def horizontalLine(times = 1): + times = int(abs(times)) + if times == 0: + times = 1 + + for _ in range(times): + screenManager.redefineDimensions() + + if user.navigation.window.width != False: + totalText = '' + for _ in range(user.navigation.window.width): totalText = totalText + '—' + print(totalText) + else: print('') + + class menu: ### for menus + def titleBar(title): + title = title.strip() + titleLength = len(title) + + totalText = '' + if user.navigation.window.width != False: + for _ in range(int(((user.navigation.window.width - titleLength)/2)-1)): totalText = totalText + '—' + totalText = (totalText + ' ') + + totalText = (totalText + formatting.font.bold + title + formatting.clear) + + if user.navigation.window.width != False: + totalText = (totalText + ' ') + for _ in range(int(((user.navigation.window.width - titleLength)/2)-1)): totalText = totalText + '—' + + print(totalText); return(totalText) + + def topMenu(menuShortcut, menuList, displayHorizontalLineBelow = True): #### bottom menu + entryNumber = 0; displayText = '|' + for menuItem in menuList: + entryNumber = menuList.index(menuItem) + + if menuShortcut[entryNumber].strip() == '': + displayText = (displayText + menuItem + '|') + else: + displayText = (displayText + formatting.font.bold + menuShortcut[entryNumber] + formatting.clear + ' ' + menuItem + '|') + print(displayText); + if displayHorizontalLineBelow: screenManager.graphical.horizontalLine() + + def inputMenu(menuShortcut, menuList, backSupported = True, passNumbers = False, passAnything = False): #### input + while True: + screenManager.graphical.horizontalLine(); + try: + response = input() + response = response.strip() + except KeyboardInterrupt: ##### User cancelled. + if backSupported: return -1; + else: print("", end="\r") ##### Loop again. + else: + if response in menuShortcut or response in menuList or passAnything: + return(response) + elif passNumbers: + try: response = float(response) + except: print("", end="\r") + else: return(response) + else: print("", end="\r") + + def promptMenu(promptTitle, promptDescription, promptOptions, promptShortcuts, canCancel = True, extraInputAllowed = False, numbersAllowed = False): + screenManager.menu.titleBar(promptTitle.strip()) + + print(promptDescription) + + if user.navigation.window.lines != False: + for _ in range((user.navigation.window.lines)/4): print('') + else: print('\n\n') + + screenManager.menu.topMenu(promptShortcuts, promptOptions) + + user.navigation.response = screenManager.menu.inputMenu(promptShortcuts, promptOptions, canCancel, numbersAllowed, extraInputAllowed) + return(user.navigation.response) + + def redefineDimensions(): + try: user.navigation.window.width = int((os.get_terminal_size()).columns) + except OSError: user.navigation.window.width = False + + try: user.navigation.window.lines = int((os.get_terminal_size()).lines) + except OSError: user.navigation.window.lines = False + + def do(): + while user.navigation.screen >= 0: + cls(); screenManager.redefineDimensions() + + if user.navigation.screen < 1: ### when opened + if user.navigation.screen == 0: + operations.display_title_screen() + clock.sleep(1) + user.navigation.screen = 0.1 + + elif user.navigation.screen == 0.1: + if operations.check_compatibility(): user.navigation.screen = 1 + + elif user.navigation.screen < 2: + if user.navigation.screen == 1: + menuShortcut = ['^c', 'r', 'i', 'd', '?'] + + screenManager.menu.titleBar(app.title); screenManager.menu.topMenu(menuShortcut, texts.menus.main) + + operations.software.check_updates() + + user.navigation.response = screenManager.menu.inputMenu(menuShortcut, texts.menus.main) + + if user.navigation.response == -1 or user.navigation.response == '^c': + user.navigation.screen = -1 + elif user.navigation.response == 'd': + user.navigation.screen = 2 + + elif user.navigation.screen < 3: ### full installer downloader, necessary and recommended to upgrade the OS + if user.navigation.screen == 2: + menuShortcut = ['^c', 'r', 'A', ''] + + screenManager.menu.titleBar(texts.screens.titles.s_2_0); screenManager.menu.topMenu(menuShortcut, texts.menus.s_2_0) + + operations.software.check_OS_installers() + + user.navigation.response = screenManager.menu.inputMenu(menuShortcut, texts.menus.s_2_0, True, True) + + if user.navigation.response == -1 or user.navigation.response == '^c': + user.navigation.screen = 1 + + elif user.navigation.response == 'A': + user.navigation.screen = 2.1 + + elif user.navigation.screen == 2.1: + menuShortcut = ['^c', 'n', 'b', 'D'] + + screenManager.menu.promptMenu(texts.screens.titles.s_2_1, texts.screens.messages.s_2_1, texts.menus.s_2_1, menuShortcut) + + if user.navigation.response == -1 or user.navigation.response == '^c': + user.navigation.screen = 2 + + elif user.navigation.response == 'n' or user.navigation.response == '🚫': + user.navigation.screen = 2.2 + + elif user.navigation.screen == 2.2: + menuShortcut = ['^c', ''] + + screenManager.menu.promptMenu(texts.screens.titles.s_2_2, texts.screens.messages.s_2_2, texts.menus.s_2_2, menuShortcut) + + if user.navigation.response == -1 or user.navigation.response == '^c': + user.navigation.screen = 2.1 + + elif user.navigation.response == '' or user.navigation.response == 'revert': + user.navigation.screen == 2.21 + + elif user.navigation.screen == 2.21: + screenManager.menu.titleBar(texts.screens.titles.s_2_2_1); + + if (operations.software.installer_branch.none()): + try: + info.err(texts.errors.screens.s_2_2_1) + except KeyboardInterrupt: + user.navigation.screen = 2.1 + else: + user.navigation.screen = 2.1 + + if user.navigation.screen == -1: + cls() + + def main(): + screenManager.do() + + main() + +except SyntaxError as e: fail(texts.errors.default.syntaxerr + '\n' + str(e)) +except NameError as e: fail(texts.errors.default.syntaxerr + '\n' + str(e)) +except IndentationError as e: fail(texts.errors.default.syntaxerr + '\n' + str(e)) +except TypeError as e: fail(texts.errors.default.typeerr + '\n' + str(e)) +except AttributeError as e: fail(texts.errors.default.attributeerr + '\n' + str(e))