342 lines
16 KiB
Python
342 lines
16 KiB
Python
|
# 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))
|