Example #1
0
 def __init__(self):
     self.core = Core()
     self.session = PromptSession()
     self.table_data = None
     self.table = None
     self.completer = None
     self.chandle = windll.kernel32.GetStdHandle(-11)
     sys.tracebacklimit = 0
     init()
Example #2
0
 def __init__(self):
     self.core = Core()
     self.session = PromptSession(reserve_space_for_menu=7)
     self.tableData = None
     self.table = None
     self.cfSlugs = None
     self.wowiSlugs = None
     self.completer = None
     self.chandle = windll.kernel32.GetStdHandle(-11)
     sys.tracebacklimit = 0
Example #3
0
 def __init__(self):
     self.core = Core()
     self.session = PromptSession(reserve_space_for_menu=6)
     self.headless = False
     self.console = None
     self.table = None
     self.cfSlugs = None
     self.wowiSlugs = None
     self.completer = None
     self.os = platform.system()
     install()
Example #4
0
 def __init__(self):
     self.core = Core()
     self.session = PromptSession(reserve_space_for_menu=6, complete_in_thread=True)
     self.headless = False
     self.console = None
     self.table = None
     self.slugs = None
     self.tipsDatabase = None
     self.completer = None
     self.os = platform.system()
     install()
Example #5
0
class TUI:
    def __init__(self):
        self.core = Core()
        self.session = PromptSession(reserve_space_for_menu=6)
        self.headless = False
        self.console = None
        self.table = None
        self.cfSlugs = None
        self.wowiSlugs = None
        self.completer = None
        self.os = platform.system()
        install()

    def start(self):
        # Check if headless mode was requested
        if len(sys.argv) == 2 and sys.argv[1].lower() == 'headless':
            self.headless = True
        self.setup_console()
        self.print_header()
        # Check if executable is in good location
        if not glob.glob('World*.app') and not glob.glob('Wow*.exe') or \
                not os.path.isdir(Path('Interface/AddOns')) or not os.path.isdir('WTF'):
            self.console.print(
                '[bold red]This executable should be placed in the same directory where Wow.exe, '
                'WowClassic.exe or World of Warcraft.app is located.[/bold red]\n\n'
            )
            pause(self.headless)
            sys.exit(1)
        # Detect Classic client
        if os.path.basename(os.getcwd()) == '_classic_':
            self.core.clientType = 'wow_classic'
            set_terminal_title(f'CurseBreaker v{__version__} - Classic')
        # Check if client have write access
        try:
            with open('PermissionTest', 'w') as _:
                pass
            os.remove('PermissionTest')
        except IOError:
            self.console.print(
                '[bold red]CurseBreaker doesn\'t have write rights for the current directory.\n'
                'Try starting it with administrative privileges.[/bold red]\n\n'
            )
            pause(self.headless)
            sys.exit(1)
        self.auto_update()
        self.core.init_config()
        self.setup_table()
        # Curse URI Support
        if len(sys.argv) == 2 and 'twitch://' in sys.argv[1]:
            try:
                self.c_install(sys.argv[1].strip())
            except Exception as e:
                self.handle_exception(e)
            timeout(self.headless)
            sys.exit(0)
        if len(sys.argv) == 2 and '.ccip' in sys.argv[1]:
            try:
                path = sys.argv[1].strip()
                self.c_install(self.core.parse_cf_xml(path))
                if os.path.exists(path):
                    os.remove(path)
            except Exception as e:
                self.handle_exception(e)
            timeout(self.headless)
            sys.exit(0)
        # CLI command
        if len(sys.argv) >= 2:
            command = ' '.join(sys.argv[1:]).split(' ', 1)
            if command[0].lower() == 'headless':
                pass
            elif getattr(self, f'c_{command[0].lower()}', False):
                try:
                    getattr(self, f'c_{command[0].lower()}')(
                        command[1].strip() if len(command) > 1 else False)
                except Exception as e:
                    self.handle_exception(e)
                sys.exit(0)
            else:
                self.console.print('Command not found.')
                sys.exit(0)
        # Addons auto update
        if len(self.core.config['Addons']) > 0:
            if not self.headless:
                self.console.print(
                    'Automatic update of all addons will start in 5 seconds.\n'
                    'Press any button to enter interactive mode.',
                    highlight=False)
            starttime = time.time()
            keypress = None
            while True:
                if self.headless:
                    break
                elif kbhit():
                    keypress = getch()
                    break
                elif time.time() - starttime > 5:
                    break
            if not keypress:
                if not self.headless:
                    self.print_header()
                try:
                    self.c_update(None, True)
                    if self.core.backup_check():
                        self.setup_table()
                        self.console.print(
                            f'\n[green]Backing up WTF directory{"!" if self.headless else ":"}[/green]'
                        )
                        self.core.backup_wtf(
                            None if self.headless else self.console)
                    if self.core.config['WAUsername'] != 'DISABLED':
                        self.setup_table()
                        self.c_wa_update(None, False)
                except Exception as e:
                    self.handle_exception(e)
                self.console.print('')
                self.print_log()
                pause(self.headless)
                sys.exit(0)
        if self.headless:
            sys.exit(1)
        self.setup_completer()
        self.print_header()
        self.console.print(
            'Use command [green]help[/green] or press [green]TAB[/green] to see a list of available comm'
            'ands.\nCommand [green]exit[/green] or pressing [green]CTRL+D[/green] will close the applica'
            'tion.\n\n')
        if len(self.core.config['Addons']) == 0:
            self.console.print(
                'Command [green]import[/green] might be used to detect already installed addons.\n\n'
            )
        # Prompt session
        while True:
            try:
                command = self.session.prompt(
                    HTML('<ansibrightgreen>CB></ansibrightgreen> '),
                    completer=self.completer)
            except KeyboardInterrupt:
                continue
            except EOFError:
                break
            else:
                command = command.split(' ', 1)
                if getattr(self, f'c_{command[0].lower()}', False):
                    try:
                        self.setup_table()
                        getattr(self, f'c_{command[0].lower()}')(
                            command[1].strip() if len(command) > 1 else False)
                        self.setup_completer()
                    except Exception as e:
                        self.handle_exception(e)
                else:
                    self.console.print('Command not found.')

    def auto_update(self):
        if getattr(sys, 'frozen', False):
            try:
                if os.path.isfile(sys.executable + '.old'):
                    try:
                        os.remove(sys.executable + '.old')
                    except PermissionError:
                        pass
                payload = requests.get(
                    'https://api.github.com/repos/AcidWeb/CurseBreaker/releases/latest',
                    headers=HEADERS).json()
                if 'name' in payload and 'body' in payload and 'assets' in payload:
                    remoteversion = payload['name']
                    changelog = payload['body']
                    url = None
                    for binary in payload['assets']:
                        if (self.os == 'Windows' and '.exe' in binary['name'])\
                                or (self.os == 'Darwin' and '.zip' in binary['name'])\
                                or (self.os == 'Linux' and '.gz' in binary['name']):
                            url = binary['browser_download_url']
                            break
                    if url and StrictVersion(
                            remoteversion[1:]) > StrictVersion(__version__):
                        self.console.print(
                            '[green]Updating CurseBreaker...[/green]')
                        shutil.move(sys.executable, sys.executable + '.old')
                        payload = requests.get(url, headers=HEADERS)
                        if self.os == 'Darwin':
                            zipfile.ZipFile(io.BytesIO(
                                payload.content)).extractall()
                        else:
                            with open(sys.executable, 'wb') as f:
                                if self.os == 'Windows':
                                    f.write(payload.content)
                                elif self.os == 'Linux':
                                    f.write(gzip.decompress(payload.content))
                        os.chmod(sys.executable, 0o775)
                        self.console.print(
                            f'[bold green]Update complete! Please restart the application.[/bold green]'
                            f'\n\n[green]Changelog:[/green]\n{changelog}\n\n')
                        self.print_log()
                        pause(self.headless)
                        sys.exit(0)
            except Exception as e:
                self.console.print(
                    f'[bold red]Update failed!\n\nReason: {str(e)}[/bold red]\n\n'
                )
                self.print_log()
                pause(self.headless)
                sys.exit(1)

    def handle_exception(self, e, table=True):
        if self.table.row_count > 1 and table:
            self.console.print(self.table)
        if getattr(sys, 'frozen', False):
            sys.tracebacklimit = 0
        if isinstance(e, list):
            for es in e:
                self.console.print(
                    Traceback.from_exception(exc_type=es.__class__,
                                             exc_value=es,
                                             traceback=es.__traceback__))
        else:
            self.console.print(
                Traceback.from_exception(exc_type=e.__class__,
                                         exc_value=e,
                                         traceback=e.__traceback__))

    def print_header(self):
        clear()
        if self.headless:
            self.console.print(
                f'[bold green]CurseBreaker[/bold green] [bold red]v{__version__}[/bold red] | '
                f'[yellow]{datetime.now()}[/yellow]',
                highlight=False)
        else:
            self.console.print(
                Rule(
                    f'[bold green]CurseBreaker[/bold green] [bold red]v{__version__}[/bold red]'
                ))
            self.console.print('\n')

    def print_log(self):
        if self.headless:
            html = self.console.export_html(inline_styles=True,
                                            theme=HEADLESS_TERMINAL_THEME)
            with open('CurseBreaker.html', 'a+', encoding='utf-8') as log:
                log.write(html)

    def setup_console(self):
        if self.headless:
            self.console = Console(record=True)
            if self.os == 'Windows':
                window = windll.kernel32.GetConsoleWindow()
                if window:
                    windll.user32.ShowWindow(window, 0)
        elif 'WINDIR' in os.environ and 'WT_SESSION' not in os.environ:
            set_terminal_size(100, 50)
            windll.kernel32.SetConsoleScreenBufferSize(
                windll.kernel32.GetStdHandle(-11), wintypes._COORD(100, 200))
            self.console = Console(width=97)
        elif self.os == 'Darwin':
            set_terminal_size(100, 50)
            self.console = Console()
        else:
            self.console = Console()

    def setup_completer(self):
        if not self.cfSlugs or not self.wowiSlugs:
            # noinspection PyBroadException
            try:
                self.cfSlugs = pickle.load(
                    gzip.open(
                        io.BytesIO(
                            requests.get(
                                'https://storage.googleapis.com/cursebreaker/cfslugs.pickle.gz',
                                headers=HEADERS).content)))
                self.wowiSlugs = pickle.load(
                    gzip.open(
                        io.BytesIO(
                            requests.get(
                                'https://storage.googleapis.com/cursebreaker/wowislugs.pickle.gz',
                                headers=HEADERS).content)))
            except Exception:
                self.cfSlugs = []
                self.wowiSlugs = []
        commands = [
            'install', 'uninstall', 'update', 'force_update', 'wa_update',
            'status', 'orphans', 'search', 'import', 'export', 'toggle_backup',
            'toggle_dev', 'toggle_wa', 'set_wa_api', 'set_wa_wow_account',
            'uri_integration', 'help', 'exit'
        ]
        addons = sorted(self.core.config['Addons'],
                        key=lambda k: k['Name'].lower())
        for addon in addons:
            name = f'"{addon["Name"]}"' if ',' in addon["Name"] else addon[
                "Name"]
            commands.extend([
                f'uninstall {name}', f'update {name}', f'force_update {name}',
                f'toggle_dev {name}', f'status {name}'
            ])
        for item in self.cfSlugs:
            commands.append(f'install cf:{item}')
        for item in self.wowiSlugs:
            commands.append(f'install wowi:{item}')
        commands.extend([
            'install ElvUI', 'install ElvUI:Dev', 'install Tukui',
            'install SLE:Dev'
        ])
        accounts = self.core.detect_accounts()
        for account in accounts:
            commands.append(f'set_wa_wow_account {account}')
        self.completer = WordCompleter(commands,
                                       ignore_case=True,
                                       sentence=True)

    def setup_table(self):
        self.table = Table(box=box.SQUARE)
        self.table.add_column('Status',
                              header_style='bold white',
                              justify='center')
        self.table.add_column('Name', header_style='bold white')
        self.table.add_column('Version', header_style='bold white')

    def c_install(self, args):
        if args:
            if args.startswith('-i '):
                args = args[3:]
                optignore = True
            else:
                optignore = False
            addons = [
                addon.strip()
                for addon in list(reader([args], skipinitialspace=True))[0]
            ]
            with Progress('{task.completed}/{task.total}',
                          '|',
                          BarColumn(bar_width=self.console.width),
                          '|',
                          auto_refresh=False,
                          console=self.console) as progress:
                task = progress.add_task('', total=len(addons))
                while not progress.finished:
                    for addon in addons:
                        installed, name, version = self.core.add_addon(
                            addon, optignore)
                        if installed:
                            self.table.add_row('[green]Installed[/green]',
                                               name, version)
                        else:
                            self.table.add_row(
                                '[bold black]Already installed[/bold black]',
                                name, version)
                        progress.update(task, advance=1, refresh=True)
            self.console.print(self.table)
        else:
            self.console.print(
                '[green]Usage:[/green]\n\tThis command accepts a comma-separated list of links as an arg'
                'ument.\n\tOption [bold white]-i[/bold white] will disable the client version check.\n[b'
                'old green]Supported URL:[/bold green]\n\thttps://www.curseforge.com/wow/addons/[[addon_'
                'name]] [bold white]|[/bold white] cf:[[addon_name]]\n\thttps://www.wowinterface.com/dow'
                'nloads/[[addon_name]] [bold white]|[/bold white] wowi:[[addon_id]]\n\thttps://www.tukui'
                '.org/addons.php?id=[[addon_id]] [bold white]|[/bold white] tu:[[addon_id]]\n\thttps://w'
                'ww.tukui.org/classic-addons.php?id=[[addon_id]] [bold white]|[/bold white] tuc:[[addon_'
                'id]]\n\tElvUI [bold white]|[/bold white] ElvUI:Dev\n\tTukui\n\tSLE:Dev',
                highlight=False)

    def c_uninstall(self, args):
        if args:
            addons = [
                addon.strip()
                for addon in list(reader([args], skipinitialspace=True))[0]
            ]
            with Progress('{task.completed}/{task.total}',
                          '|',
                          BarColumn(bar_width=self.console.width),
                          '|',
                          auto_refresh=False,
                          console=self.console) as progress:
                task = progress.add_task('', total=len(addons))
                while not progress.finished:
                    for addon in addons:
                        name, version = self.core.del_addon(addon)
                        if name:
                            self.table.add_row(
                                f'[bold red]Uninstalled[/bold red]', name,
                                version)
                        else:
                            self.table.add_row(
                                f'[bold black]Not installed[/bold black]',
                                addon, '')
                        progress.update(task, advance=1, refresh=True)
            self.console.print(self.table)
        else:
            self.console.print(
                '[green]Usage:[/green]\n\tThis command accepts a comma-separated list of links as an arg'
                'ument.\n[bold green]Supported URL:[/bold green]\n\thttps://www.curseforge.com/wow/addon'
                's/[[addon_name]] [bold white]|[/bold white] cf:[[addon_name]]\n\thttps://www.wowinterfa'
                'ce.com/downloads/[[addon_name]] [bold white]|[/bold white] wowi:[[addon_id]]\n\thttps:/'
                '/www.tukui.org/addons.php?id=[[addon_id]] [bold white]|[/bold white] tu:[[addon_id]]'
                '\n\thttps://www.tukui.org/classic-addons.php?id=[[addon_id]] [bold white]|[/bold white]'
                ' tuc:[[addon_id]]\n\tElvUI [bold white]|[/bold white] ElvUI:Dev\n\tTukui\n\tSLE:Dev',
                highlight=False)

    def c_update(self, args, addline=False, update=True, force=False):
        if len(self.core.cfCache) > 0 or len(self.core.wowiCache) > 0:
            self.core.cfCache = {}
            self.core.wowiCache = {}
            self.core.checksumCache = {}
        if args:
            addons = [
                addon.strip()
                for addon in list(reader([args], skipinitialspace=True))[0]
            ]
        else:
            addons = sorted(self.core.config['Addons'],
                            key=lambda k: k['Name'].lower())
        exceptions = []
        with Progress(
                '{task.completed:.0f}/{task.total}',
                '|',
                BarColumn(bar_width=self.console.width + 1),
                '|',
                auto_refresh=False,
                console=None if self.headless else self.console) as progress:
            task = progress.add_task('', total=len(addons))
            if not args:
                self.core.bulk_check(addons)
                self.core.bulk_check_checksum(addons, progress)
            while not progress.finished:
                for addon in addons:
                    try:
                        name, versionnew, versionold, modified = self.core.\
                            update_addon(addon if isinstance(addon, str) else addon['URL'], update, force)
                        if versionold:
                            if versionold == versionnew:
                                if modified:
                                    self.table.add_row(
                                        '[bold red]Modified[/bold red]', name,
                                        versionold)
                                else:
                                    self.table.add_row(
                                        '[green]Up-to-date[/green]', name,
                                        versionold)
                            else:
                                if modified:
                                    self.table.add_row(
                                        '[bold red]Update suppressed[/bold red]',
                                        name, versionold)
                                else:
                                    self.table.add_row(
                                        f'[yellow]{"Updated " if update else "Update available"}'
                                        f'[/yellow]', name,
                                        f'[yellow]{versionnew}[/yellow]')
                        else:
                            self.table.add_row(
                                f'[bold black]Not installed[/bold black]',
                                addon, '')
                    except Exception as e:
                        exceptions.append(e)
                    progress.update(task,
                                    advance=1 if args else 0.5,
                                    refresh=True)
        if addline:
            self.console.print('\n')
        self.console.print(self.table)
        if len(addons) == 0:
            self.console.print(
                'Apparently there are no addons installed by CurseBreaker.\n'
                'Command [green]import[/green] might be used to detect already installed addons.'
            )
        if len(exceptions) > 0:
            self.handle_exception(exceptions, False)

    def c_force_update(self, args):
        if args:
            self.c_update(args, False, True, True)
        else:
            self.console.print(
                '[green]Usage:[/green]\n\tThis command accepts a comma-separated list of links or addon '
                'names as an argument.')

    def c_status(self, args):
        self.c_update(args, False, False)

    def c_orphans(self, _):
        orphansd, orphansf = self.core.find_orphans()
        self.console.print(
            '[green]Directories that are not part of any installed addon:[/green]'
        )
        for orphan in sorted(orphansd):
            self.console.print(orphan.replace('[GIT]',
                                              '[yellow][[GIT]][/yellow]'),
                               highlight=False)
        self.console.print(
            '\n[green]Files that are leftovers after no longer installed addons:[/green]'
        )
        for orphan in sorted(orphansf):
            self.console.print(orphan, highlight=False)

    def c_uri_integration(self, _):
        if self.os == 'Windows':
            self.core.create_reg()
            self.console.print(
                'CurseBreaker.reg file was created. Attempting to import...')
            out = os.system('"' + str(
                Path(os.path.dirname(sys.executable), 'CurseBreaker.reg')) +
                            '"')
            if out != 0:
                self.console.print(
                    'Import failed. Please try to import REG file manually.')
            else:
                os.remove('CurseBreaker.reg')
        else:
            self.console.print('This feature is available only on Windows.')

    def c_toggle_dev(self, args):
        if args:
            status = self.core.dev_toggle(args)
            if status is None:
                self.console.print(
                    '[bold red]This addon doesn\'t exist or it is not installed yet.[/bold red]'
                )
            elif status == 0:
                self.console.print(
                    'Addon switched to the [yellow]beta[/yellow] channel.')
            elif status == 1:
                self.console.print(
                    'Addon switched to the [red]alpha[/red] channel.')
            elif status == 2:
                self.console.print(
                    'Addon switched to the [green]stable[/green] channel.')
        else:
            self.console.print(
                '[green]Usage:[/green]\n\tThis command accepts an addon name as an argument.'
            )

    def c_toggle_backup(self, _):
        status = self.core.backup_toggle()
        self.console.print(
            'Backup of WTF directory is now:',
            '[green]ENABLED[/green]' if status else '[red]DISABLED[/red]')

    def c_toggle_wa(self, args):
        if args:
            if args == self.core.config['WAUsername']:
                self.console.print(
                    f'WeakAuras version check is now: [green]ENABLED[/green]\nAuras created by '
                    f'[bold white]{self.core.config["WAUsername"]}[/bold white] are now included.'
                )
                self.core.config['WAUsername'] = ''
            else:
                self.core.config['WAUsername'] = args.strip()
                self.console.print(
                    f'WeakAuras version check is now: [green]ENABLED[/green]\nAuras created by '
                    f'[bold white]{self.core.config["WAUsername"]}[/bold white] are now ignored.'
                )
        else:
            if self.core.config['WAUsername'] == 'DISABLED':
                self.core.config['WAUsername'] = ''
                self.console.print(
                    'WeakAuras version check is now: [green]ENABLED[/green]')
            else:
                self.core.config['WAUsername'] = '******'
                shutil.rmtree(Path('Interface/AddOns/WeakAurasCompanion'),
                              ignore_errors=True)
                self.console.print(
                    'WeakAuras version check is now: [red]DISABLED[/red]')
        self.core.save_config()

    def c_set_wa_api(self, args):
        if args:
            self.console.print('Wago API key is now set.')
            self.core.config['WAAPIKey'] = args.strip()
            self.core.save_config()
        elif self.core.config['WAAPIKey'] != '':
            self.console.print('Wago API key is now removed.')
            self.core.config['WAAPIKey'] = ''
            self.core.save_config()
        else:
            self.console.print(
                '[green]Usage:[/green]\n\tThis command accepts API key as an argument.'
            )

    def c_set_wa_wow_account(self, args):
        if args:
            args = args.strip()
            if os.path.isfile(
                    Path(f'WTF/Account/{args}/SavedVariables/WeakAuras.lua')):
                self.console.print(
                    f'WoW account name set to: [bold white]{args}[/bold white]'
                )
                self.core.config['WAAccountName'] = args
                self.core.save_config()
            else:
                self.console.print('Incorrect WoW account name.')
        else:
            self.console.print(
                '[green]Usage:[/green]\n\tThis command accepts the WoW account name as an argument.'
            )

    def c_wa_update(self, _, verbose=True):
        if os.path.isdir(Path('Interface/AddOns/WeakAuras')):
            accounts = self.core.detect_accounts()
            if len(accounts) == 0:
                return
            elif len(accounts) > 1 and self.core.config['WAAccountName'] == '':
                if verbose:
                    self.console.print(
                        'More than one WoW account detected.\nPlease use [bold white]set_wa_wow_account['
                        '/white] command to set the correct account name.')
                else:
                    self.console.print(
                        '\n[green]More than one WoW account detected.[/green]\nPlease use [bold white]se'
                        't_wa_wow_account[/bold white] command to set the correct account name.'
                    )
                return
            elif len(
                    accounts) == 1 and self.core.config['WAAccountName'] == '':
                self.core.config['WAAccountName'] = accounts[0]
                self.core.save_config()
            wa = WagoUpdater(self.core.config['WAUsername'],
                             self.core.config['WAAccountName'],
                             self.core.config['WAAPIKey'])
            if self.core.waCompanionVersion != self.core.config[
                    'WACompanionVersion']:
                self.core.config[
                    'WACompanionVersion'] = self.core.waCompanionVersion
                self.core.save_config()
                force = True
            else:
                force = False
            wa.parse_storage()
            status = wa.check_updates()
            wa.install_companion(self.core.clientType, force)
            wa.install_data()
            if verbose:
                self.console.print('[green]Outdated WeakAuras:[/green]')
                for aura in status[0]:
                    self.console.print(aura, highlight=False)
                self.console.print('\n[green]Detected WeakAuras:[/green]')
                for aura in status[1]:
                    self.console.print(aura, highlight=False)
            else:
                self.console.print(
                    f'\n[green]The number of outdated WeakAuras:[/green] {len(status[0])}',
                    highlight=False)
        elif verbose:
            self.console.print('WeakAuras addon is not installed.')

    def c_search(self, args):
        if args:
            results = self.core.search(args)
            self.console.print('[green]Top results of your search:[/green]')
            for url in results:
                if self.core.check_if_installed(url):
                    self.console.print(f'{url} [yellow][[Installed]][/yellow]',
                                       highlight=False)
                else:
                    self.console.print(url, highlight=False)
        else:
            self.console.print(
                '[green]Usage:[/green]\n\tThis command accepts a search query as an argument.'
            )

    def c_import(self, args):
        hit, partial_hit, miss = self.core.detect_addons()
        if args == 'install' and len(hit) > 0:
            self.c_install(','.join(hit))
        else:
            self.console.print(f'[green]Addons found:[/green]')
            for addon in hit:
                self.console.print(addon)
            self.console.print(f'\n[yellow]Possible matches:[/yellow]')
            for addon in partial_hit:
                self.console.print(' [bold white]or[/bold white] '.join(addon))
            self.console.print(f'\n[red]Unknown directories:[/red]')
            for addon in miss:
                self.console.print(f'{addon}')
            self.console.print(
                f'\nExecute [bold white]import install[/bold white] command to install all detected addo'
                f'ns.\nPossible matches need to be installed manually with the [bold white]install[/bold'
                f' white] command.')

    def c_export(self, _):
        self.console.print(self.core.export_addons(), highlight=False)

    def c_help(self, _):
        self.console.print(
            '[green]install [URL][/green]\n\tCommand accepts a comma-separated list of links.\n'
            '[green]uninstall [URL/Name][/green]\n\tCommand accepts a comma-separated list of links or'
            ' addon names.\n'
            '[green]update [URL/Name][/green]\n\tCommand accepts a comma-separated list of links or ad'
            'don names.\n\tIf no argument is provided all non-modified addons will be updated.\n'
            '[green]force_update [URL/Name][/green]\n\tCommand accepts a comma-separated list of links'
            ' or addon names.\n\tSelected addons will be reinstalled or updated regardless of their cu'
            'rrent state.\n'
            '[green]wa_update[/green]\n\tCommand detects all installed WeakAuras and generate WeakAura'
            's Companion payload.\n'
            '[green]status[/green]\n\tPrints the current state of all installed addons.\n'
            '[green]orphans[/green]\n\tPrints list of orphaned directories and files.\n'
            '[green]search [Keyword][/green]\n\tExecutes addon search on CurseForge.\n'
            '[green]import[/green]\n\tCommand attempts to import already installed addons.\n'
            '[green]export[/green]\n\tCommand prints list of all installed addons in a form suitable f'
            'or sharing.\n'
            '[green]toggle_backup[/green]\n\tEnables/disables automatic daily backup of WTF directory.'
            '\n[green]toggle_dev [Name][/green]\n\tCommand accepts an addon name as argument.\n\tPrior'
            'itizes alpha/beta versions for the provided addon.\n'
            '[green]toggle_wa [Username][/green]\n\tEnables/disables automatic WeakAuras updates.\n\tI'
            'f a username is provided check will start to ignore the specified author.\n'
            '[green]set_wa_api [API key][/green]\n\tSets Wago API key required to access private auras'
            '.\n\tIt can be procured here: [link=https://wago.io/account]https://wago.io/account[/link]'
            '\n[green]set_wa_wow_account [Account name][/green]\n\tSets WoW account used by WeakAuras up'
            'dater.\n\tNeeded only if WeakAuras are used on more than one WoW account.\n'
            '[green]uri_integration[/green]\n\tEnables integration with CurseForge page.\n\t[i]"Install"'
            '[/i] button will now start this application.\n'
            '\n[bold green]Supported URL:[/bold green]\n\thttps://www.curseforge.com/wow/addons/[[addon_'
            'name]] [bold white]|[/bold white] cf:[[addon_name]]\n\thttps://www.wowinterface.com/downloa'
            'ds/[[addon_name]] [bold white]|[/bold white] wowi:[[addon_id]]\n\thttps://www.tukui.org/add'
            'ons.php?id=[[addon_id]] [bold white]|[/bold white] tu:[[addon_id]]\n\thttps://www.tukui.org'
            '/classic-addons.php?id=[[addon_id]] [bold white]|[/bold white] tuc:[[addon_id]]\n\tElvUI [b'
            'old white]|[/bold white] ElvUI:Dev\n\tTukui\n\tSLE:Dev',
            highlight=False)

    def c_exit(self, _):
        sys.exit(0)
Example #6
0
class TUI:
    def __init__(self):
        self.core = Core()
        self.session = PromptSession(reserve_space_for_menu=6, complete_in_thread=True)
        self.headless = False
        self.console = None
        self.table = None
        self.slugs = None
        self.tipsDatabase = None
        self.completer = None
        self.os = platform.system()
        install()

    def start(self):
        # Check if headless mode was requested
        if len(sys.argv) == 2 and sys.argv[1].lower() == 'headless':
            self.headless = True
        self.setup_console()
        self.print_header()
        self.core.init_master_config()
        # Check if executable is in good location
        if not glob.glob('World*.app') and not glob.glob('Wow*.exe') or \
                not os.path.isdir(Path('Interface/AddOns')) or not os.path.isdir('WTF'):
            self.console.print('[bold red]This executable should be placed in the same directory where Wow.exe, '
                               'WowClassic.exe or World of Warcraft.app is located. Additionally, make sure that '
                               'this WoW installation was started at least once.[/bold red]\n')
            pause(self.headless)
            sys.exit(1)
        # Detect Classic client
        if os.path.basename(os.getcwd()) == '_classic_':
            self.core.clientType = 'wow_classic'
            set_terminal_title(f'CurseBreaker v{__version__} - Classic')
        # Check if client have write access
        try:
            with open('PermissionTest', 'w') as _:
                pass
            os.remove('PermissionTest')
        except IOError:
            self.console.print('[bold red]CurseBreaker doesn\'t have write rights for the current directory.\n'
                               'Try starting it with administrative privileges.[/bold red]\n')
            pause(self.headless)
            sys.exit(1)
        self.auto_update()
        try:
            self.core.init_config()
        except RuntimeError:
            self.console.print('[bold red]The config file is corrupted. Restore the earlier version from backup.'
                               '[/bold red]\n')
            pause(self.headless)
            sys.exit(1)
        self.setup_table()
        # Curse URI Support
        if len(sys.argv) == 2 and 'twitch://' in sys.argv[1]:
            try:
                self.c_install(sys.argv[1].strip())
            except Exception as e:
                self.handle_exception(e)
            timeout(self.headless)
            sys.exit(0)
        if len(sys.argv) == 2 and '.ccip' in sys.argv[1]:
            try:
                path = sys.argv[1].strip()
                self.c_install(self.core.parse_cf_xml(path))
                if os.path.exists(path):
                    os.remove(path)
            except Exception as e:
                self.handle_exception(e)
            timeout(self.headless)
            sys.exit(0)
        # CLI command
        if len(sys.argv) >= 2:
            command = ' '.join(sys.argv[1:]).split(' ', 1)
            if command[0].lower() == 'headless':
                pass
            elif getattr(self, f'c_{command[0].lower()}', False):
                try:
                    getattr(self, f'c_{command[0].lower()}')(command[1].strip() if len(command) > 1 else False)
                except Exception as e:
                    self.handle_exception(e)
                sys.exit(0)
            else:
                self.console.print('Command not found.')
                sys.exit(0)
        # Addons auto update
        if len(self.core.config['Addons']) > 0 and self.core.config['AutoUpdate']:
            if not self.headless:
                self.console.print('Automatic update of all addons will start in 5 seconds.\n'
                                   'Press any button to enter interactive mode.', highlight=False)
            kb = KBHit()
            starttime = time.time()
            keypress = None
            while True:
                if self.headless:
                    break
                elif kb.kbhit():
                    keypress = kb.getch()
                    break
                elif time.time() - starttime > 5:
                    break
            kb.set_normal_term()
            if not keypress:
                if not self.headless:
                    self.print_header()
                try:
                    self.motd_parser()
                    self.c_update(None, True)
                    if self.core.backup_check():
                        self.setup_table()
                        self.console.print(f'\n[green]Backing up WTF directory{"!" if self.headless else ":"}[/green]')
                        self.core.backup_wtf(None if self.headless else self.console)
                    if self.core.config['WAUsername'] != 'DISABLED':
                        self.setup_table()
                        self.c_wago_update(None, False)
                except Exception as e:
                    self.handle_exception(e)
                self.console.print('')
                self.print_log()
                pause(self.headless)
                sys.exit(0)
        if self.headless:
            sys.exit(1)
        self.setup_completer()
        self.print_header()
        self.console.print('Use command [green]help[/green] or press [green]TAB[/green] to see a list of available comm'
                           'ands.\nCommand [green]exit[/green] or pressing [green]CTRL+D[/green] will close the applica'
                           'tion.\n')
        if len(self.core.config['Addons']) == 0:
            self.console.print('Command [green]import[/green] might be used to detect already installed addons.\n')
        self.motd_parser()
        # Prompt session
        while True:
            try:
                command = self.session.prompt(HTML('<ansibrightgreen>CB></ansibrightgreen> '), completer=self.completer)
            except KeyboardInterrupt:
                continue
            except EOFError:
                break
            else:
                command = command.split(' ', 1)
                if getattr(self, f'c_{command[0].lower()}', False):
                    try:
                        self.setup_table()
                        getattr(self, f'c_{command[0].lower()}')(command[1].strip() if len(command) > 1 else False)
                        self.setup_completer()
                    except Exception as e:
                        self.handle_exception(e)
                else:
                    self.console.print('Command not found.')

    def auto_update(self):
        if getattr(sys, 'frozen', False) and 'CURSEBREAKER_VARDEXMODE' not in os.environ:
            try:
                if os.path.isfile(sys.executable + '.old'):
                    try:
                        os.remove(sys.executable + '.old')
                    except PermissionError:
                        pass
                payload = requests.get('https://api.github.com/repos/AcidWeb/CurseBreaker/releases/latest',
                                       headers=HEADERS, timeout=5).json()
                if 'name' in payload and 'body' in payload and 'assets' in payload:
                    remoteversion = payload['name']
                    changelog = payload['body']
                    url = None
                    for binary in payload['assets']:
                        if (self.os == 'Windows' and '.exe' in binary['name'])\
                                or (self.os == 'Darwin' and '.zip' in binary['name'])\
                                or (self.os == 'Linux' and '.gz' in binary['name']):
                            url = binary['browser_download_url']
                            break
                    if url and StrictVersion(remoteversion[1:]) > StrictVersion(__version__):
                        self.console.print('[green]Updating CurseBreaker...[/green]')
                        shutil.move(sys.executable, sys.executable + '.old')
                        payload = requests.get(url, headers=HEADERS, timeout=5)
                        if self.os == 'Darwin':
                            zipfile.ZipFile(io.BytesIO(payload.content)).extractall(path=os.path.dirname(
                                os.path.abspath(sys.executable)))
                        else:
                            with open(sys.executable, 'wb') as f:
                                if self.os == 'Windows':
                                    f.write(payload.content)
                                elif self.os == 'Linux':
                                    f.write(gzip.decompress(payload.content))
                        os.chmod(sys.executable, 0o775)
                        self.console.print(f'[bold green]Update complete! The application will be restarted now.'
                                           f'[/bold green]\n\n[green]Changelog:[/green]\n{changelog}\n')
                        self.print_log()
                        pause(self.headless)
                        subprocess.call([sys.executable] + sys.argv[1:])
                        sys.exit(0)
            except Exception as e:
                self.console.print(f'[bold red]Update failed!\n\nReason: {str(e)}[/bold red]\n')
                self.print_log()
                pause(self.headless)
                sys.exit(1)

    def motd_parser(self):
        payload = requests.get('https://storage.googleapis.com/cursebreaker/motd', headers=HEADERS, timeout=5)
        if payload.status_code == 200:
            self.console.print(Panel(payload.content.decode('UTF-8'), title='MOTD', border_style='red'))
            self.console.print('')

    def handle_exception(self, e, table=True):
        if self.table.row_count > 1 and table:
            self.console.print(self.table)
        if getattr(sys, 'frozen', False) and 'CURSEBREAKER_DEBUG' not in os.environ:
            sys.tracebacklimit = 0
            width = 0
        else:
            width = 100
        if isinstance(e, list):
            for es in e:
                self.console.print(Traceback.from_exception(exc_type=es.__class__, exc_value=es,
                                                            traceback=es.__traceback__, width=width))
        else:
            self.console.print(Traceback.from_exception(exc_type=e.__class__, exc_value=e,
                                                        traceback=e.__traceback__, width=width))

    def print_header(self):
        clear()
        if self.headless:
            self.console.print(f'[bold green]CurseBreaker[/bold green] [bold red]v{__version__}[/bold red] | '
                               f'[yellow]{datetime.now()}[/yellow]', highlight=False)
        else:
            self.console.print(Rule(f'[bold green]CurseBreaker[/bold green] [bold red]v{__version__}[/bold red]'))
            self.console.print('')

    def print_log(self):
        if self.headless:
            html = self.console.export_html(inline_styles=True, theme=HEADLESS_TERMINAL_THEME)
            with open('CurseBreaker.html', 'a+', encoding='utf-8') as log:
                log.write(html)

    def setup_console(self):
        if self.headless:
            self.console = Console(record=True)
            if self.os == 'Windows':
                window = windll.kernel32.GetConsoleWindow()
                if window:
                    windll.user32.ShowWindow(window, 0)
        elif 'WINDIR' in os.environ and 'WT_SESSION' not in os.environ and 'ALACRITTY_LOG' not in os.environ:
            set_terminal_size(100, 50)
            windll.kernel32.SetConsoleScreenBufferSize(windll.kernel32.GetStdHandle(-11), wintypes._COORD(100, 200))
            self.console = Console(width=97)
        else:
            self.console = Console()

    def setup_completer(self):
        if not self.slugs:
            # noinspection PyBroadException
            try:
                self.slugs = pickle.load(gzip.open(io.BytesIO(
                    requests.get('https://storage.googleapis.com/cursebreaker/slugs.pickle.gz',
                                 headers=HEADERS, timeout=5).content)))
            except Exception:
                self.slugs = {'cf': [], 'wowi': [], 'tukui': []}
        addons = []
        for addon in sorted(self.core.config['Addons'], key=lambda k: k['Name'].lower()):
            addons.append(addon['Name'])
        slugs = ['ElvUI', 'Tukui']
        for item in self.slugs['cf']:
            slugs.append(f'cf:{item}')
        for item in self.slugs['wowi']:
            slugs.append(f'wowi:{item}')
        slugs.extend(['ElvUI:Dev', 'Shadow&Light:Dev'])
        accounts = []
        for account in self.core.detect_accounts():
            accounts.append(account)
        self.completer = NestedCompleter.from_nested_dict({
            'install': WordCompleter(slugs, ignore_case=True, match_middle=True, WORD=True),
            'uninstall': WordCompleter(addons, ignore_case=True),
            'update': WordCompleter(addons, ignore_case=True),
            'force_update': WordCompleter(addons, ignore_case=True),
            'wago_update': None,
            'status': WordCompleter(addons, ignore_case=True),
            'orphans': None,
            'search': None,
            'recommendations': None,
            'import': {'install': None},
            'export': None,
            'toggle': {'authors': None,
                       'autoupdate': None,
                       'backup': None,
                       'channel': WordCompleter(addons + ['global'], ignore_case=True, sentence=True),
                       'compact_mode': None,
                       'pinning': WordCompleter(addons, ignore_case=True, sentence=True),
                       'wago': None},
            'set': {'wago_api': None,
                    'wago_wow_account': WordCompleter(accounts, ignore_case=True, sentence=True)},
            'show': {'dependencies': None},
            'uri_integration': None,
            'help': None,
            'exit': None
        })

    def setup_table(self):
        self.table = Table(box=box.SQUARE)
        self.table.add_column('Status', header_style='bold white', no_wrap=True, justify='center')
        self.table.add_column('Name / Author' if self.core.config['ShowAuthors'] else 'Name', header_style='bold white')
        self.table.add_column('Version', header_style='bold white')

    def parse_args(self, args):
        parsed = []
        for addon in sorted(self.core.config['Addons'], key=lambda k: len(k['Name']), reverse=True):
            if addon['Name'] in args or addon['URL'] in args:
                parsed.append(addon['Name'])
                args = args.replace(addon['Name'], '', 1)
        return sorted(parsed)

    def parse_link(self, text, link, dev=None, authors=None, uiversion=None):
        if dev == 1:
            dev = ' [bold][B][/bold]'
        elif dev == 2:
            dev = ' [bold][A][/bold]'
        else:
            dev = ''
        if authors and self.core.config['ShowAuthors']:
            authors.sort()
            authors = f' [bold black]by {", ".join(authors)}[/bold black]'
        else:
            authors = ''
        if uiversion and uiversion not in [self.core.masterConfig['RetailVersion'],
                                           self.core.masterConfig['ClassicVersion']]:
            uiversion = ' [bold yellow][!][/bold yellow]'
        else:
            uiversion = ''
        if link:
            obj = Text.from_markup(f'[link={link}]{text}[/link]{dev}{authors}{uiversion}')
        else:
            obj = Text.from_markup(f'{text}{dev}{authors}{uiversion}')
        obj.no_wrap = True
        return obj

    def c_install(self, args, recursion=False):
        if args:
            optignore = False
            pargs = split(args)
            if '-i' in pargs:
                optignore = True
                args = args.replace('-i', '', 1)
            dependencies = DependenciesParser(self.core)
            args = re.sub(r'([a-zA-Z0-9_:])([ ]+)([a-zA-Z0-9_:])', r'\1,\3', args)
            addons = [addon.strip() for addon in list(reader([args], skipinitialspace=True))[0]]
            with Progress('{task.completed}/{task.total}', '|', BarColumn(bar_width=None), '|',
                          auto_refresh=False, console=self.console) as progress:
                task = progress.add_task('', total=len(addons))
                while not progress.finished:
                    for addon in addons:
                        installed, name, version, deps = self.core.add_addon(addon, optignore)
                        if installed:
                            self.table.add_row('[green]Installed[/green]', Text(name, no_wrap=True),
                                               Text(version, no_wrap=True))
                            if not recursion:
                                dependencies.add_dependency(deps)
                        else:
                            self.table.add_row('[bold black]Already installed[/bold black]',
                                               Text(name, no_wrap=True), Text(version, no_wrap=True))
                        progress.update(task, advance=1, refresh=True)
            self.console.print(self.table)
            dependencies = dependencies.parse_dependency()
            if dependencies:
                self.setup_table()
                self.console.print('Installing dependencies:')
                self.c_install(dependencies, recursion=True)
        else:
            self.console.print('[green]Usage:[/green]\n\tThis command accepts a space-separated list of links as an arg'
                               'ument.[bold white]\n\tFlags:[/bold white]\n\t\t[bold white]-i[/bold white] - Disable th'
                               'e client version check.\n[bold green]Supported URL:[/bold green]\n\thttps://www.cursefo'
                               'rge.com/wow/addons/\[addon_name] [bold white]|[/bold white] cf:\[addon_name]\n\thttps:/'
                               '/www.wowinterface.com/downloads/\[addon_name] [bold white]|[/bold white] wowi:\[addon_i'
                               'd]\n\thttps://www.tukui.org/addons.php?id=\[addon_id] [bold white]|[/bold white] tu:\[a'
                               'ddon_id]\n\thttps://www.tukui.org/classic-addons.php?id=\[addon_id] [bold white]|[/bold'
                               ' white] tuc:\[addon_id]\n\thttps://github.com/\[username]/\[repository_name] [bold whit'
                               'e]|[/bold white] gh:\[username]/\[repository_name]\n\tElvUI [bold white]|[/bold white] '
                               'ElvUI:Dev\n\tTukui\n\tShadow&Light:Dev', highlight=False)

    def c_uninstall(self, args):
        if args:
            optkeep = False
            pargs = split(args)
            if '-k' in pargs:
                optkeep = True
                args = args.replace('-k', '', 1)
            addons = self.parse_args(args)
            with Progress('{task.completed}/{task.total}', '|', BarColumn(bar_width=None), '|',
                          auto_refresh=False, console=self.console) as progress:
                task = progress.add_task('', total=len(addons))
                while not progress.finished:
                    for addon in addons:
                        name, version = self.core.del_addon(addon, optkeep)
                        if name:
                            self.table.add_row(f'[bold red]Uninstalled[/bold red]',
                                               Text(name, no_wrap=True), Text(version, no_wrap=True))
                        else:
                            self.table.add_row(f'[bold black]Not installed[/bold black]',
                                               Text(addon, no_wrap=True), Text('', no_wrap=True))
                        progress.update(task, advance=1, refresh=True)
            self.console.print(self.table)
        else:
            self.console.print('[green]Usage:[/green]\n\tThis command accepts a space-separated list of addon names or '
                               'full links as an argument.\n\t[bold white]Flags:[/bold white]\n\t\t[bold white]-k[/bold'
                               ' white] - Keep the addon files after uninstalling.', highlight=False)

    def c_update(self, args, addline=False, update=True, force=False, provider=False, reversecompact=False):
        compact = not self.core.config['CompactMode'] if reversecompact else self.core.config['CompactMode']
        if len(self.core.cfCache) > 0 or len(self.core.wowiCache) > 0:
            self.core.cfCache = {}
            self.core.wowiCache = {}
            self.core.checksumCache = {}
        if args:
            addons = self.parse_args(args)
            compacted = -1
        else:
            addons = sorted(self.core.config['Addons'], key=lambda k: k['Name'].lower())
            compacted = 0
        exceptions = []
        dependencies = DependenciesParser(self.core)
        with Progress('{task.completed:.0f}/{task.total}', '|', BarColumn(bar_width=None), '|',
                      auto_refresh=False, console=None if self.headless else self.console) as progress:
            task = progress.add_task('', total=len(addons))
            if not args:
                self.core.bulk_check(addons)
                self.core.bulk_check_checksum(addons, progress)
            while not progress.finished:
                for addon in addons:
                    try:
                        name, authors, versionnew, versionold, uiversion, modified, blocked, source, sourceurl,\
                            changelog, deps, dstate = self.core.update_addon(
                                addon if isinstance(addon, str) else addon['URL'], update, force)
                        dependencies.add_dependency(deps)
                        if provider:
                            source = f' [bold white]{source}[/bold white]'
                        else:
                            source = ''
                        if versionold:
                            if versionold == versionnew:
                                if modified:
                                    self.table.add_row(f'[bold red]Modified[/bold red]{source}',
                                                       self.parse_link(name, sourceurl, authors=authors),
                                                       self.parse_link(versionold, changelog, dstate,
                                                                       uiversion=uiversion))
                                else:
                                    if compact and compacted > -1:
                                        compacted += 1
                                    else:
                                        self.table.add_row(f'[green]Up-to-date[/green]{source}',
                                                           self.parse_link(name, sourceurl, authors=authors),
                                                           self.parse_link(versionold, changelog, dstate,
                                                                           uiversion=uiversion))
                            else:
                                if modified or blocked:
                                    self.table.add_row(f'[bold red]Update suppressed[/bold red]{source}',
                                                       self.parse_link(name, sourceurl, authors=authors),
                                                       self.parse_link(versionold, changelog, dstate,
                                                                       uiversion=uiversion))
                                else:
                                    version = self.parse_link(versionnew, changelog, dstate, uiversion=uiversion)
                                    version.stylize('yellow')
                                    self.table.add_row(
                                        f'[yellow]{"Updated" if update else "Update available"}[/yellow]{source}',
                                        self.parse_link(name, sourceurl, authors=authors),
                                        version)
                        else:
                            self.table.add_row(f'[bold black]Not installed[/bold black]{source}',
                                               Text(addon, no_wrap=True),
                                               Text('', no_wrap=True))
                    except Exception as e:
                        exceptions.append(e)
                    progress.update(task, advance=1 if args else 0.5, refresh=True)
        if addline:
            self.console.print('')
        self.console.print(self.table)
        dependencies = dependencies.parse_dependency()
        if dependencies and update:
            self.setup_table()
            self.console.print('Installing dependencies:')
            self.c_install(dependencies, recursion=True)
        if compacted > 0:
            self.console.print(f'Additionally [green]{compacted}[/green] addons are up-to-date.')
        if len(addons) == 0:
            self.console.print('Apparently there are no addons installed by CurseBreaker.\n'
                               'Command [green]import[/green] might be used to detect already installed addons.')
        if len(exceptions) > 0:
            self.handle_exception(exceptions, False)

    def c_force_update(self, args):
        if args:
            self.c_update(args, False, True, True)
        else:
            # noinspection PyTypeChecker
            answer = confirm(HTML('<ansibrightred>Execute a forced update of all addons and overwrite ALL local '
                                  'changes?</ansibrightred>'))
            if answer:
                self.c_update(False, False, True, True)

    def c_status(self, args):
        optsource = False
        optcompact = False
        if args:
            pargs = split(args)
            if '-s' in pargs:
                optsource = True
                args = args.replace('-s', '', 1)
            if '-a' in pargs:
                optcompact = True
                args = args.replace('-a', '', 1)
            args = args.strip()
        self.c_update(args, False, False, False, optsource, optcompact)

    def c_orphans(self, _):
        orphansd, orphansf = self.core.find_orphans()
        self.console.print('[green]Directories that are not part of any installed addon:[/green]')
        for orphan in sorted(orphansd):
            self.console.print(orphan.replace('[GIT]', '[yellow]\[GIT][/yellow]'), highlight=False)
        self.console.print('\n[green]Files that are leftovers after no longer installed addons:[/green]')
        for orphan in sorted(orphansf):
            self.console.print(orphan, highlight=False)

    def c_uri_integration(self, _):
        if self.os == 'Windows':
            self.core.create_reg()
            self.console.print('CurseBreaker.reg file was created. Attempting to import...')
            out = os.system('"' + str(Path(os.path.dirname(sys.executable), 'CurseBreaker.reg')) + '"')
            if out != 0:
                self.console.print('Import failed. Please try to import REG file manually.')
            else:
                os.remove('CurseBreaker.reg')
        else:
            self.console.print('This feature is available only on Windows.')

    def c_toggle(self, args):
        args = args.strip()
        if args.startswith('channel'):
            args = args[8:]
            if args:
                status = self.core.dev_toggle(args)
                if status is None:
                    self.console.print('[bold red]This addon doesn\'t exist or it is not installed yet.[/bold red]')
                elif status == -1:
                    self.console.print('[bold red]This feature can be only used with CurseForge addons.[/bold red]')
                elif status == 0:
                    self.console.print(
                        'All CurseForge addons are now switched' if args == 'global' else 'Addon switched',
                        'to the [yellow]beta[/yellow] channel.')
                elif status == 1:
                    self.console.print(
                        'All CurseForge addons are now switched' if args == 'global' else 'Addon switched',
                        'to the [red]alpha[/red] channel.')
                elif status == 2:
                    self.console.print(
                        'All CurseForge addons are now switched' if args == 'global' else 'Addon switched',
                        'to the [green]stable[/green] channel.')
            else:
                self.console.print('[green]Usage:[/green]\n\tThis command accepts an addon name (or "global") as an'
                                   ' argument.', highlight=False)
        elif args.startswith('pinning'):
            args = args[8:]
            if args:
                status = self.core.block_toggle(args)
                if status is None:
                    self.console.print('[bold red]This addon does not exist or it is not installed yet.[/bold red]')
                elif status:
                    self.console.print('Updates for this addon are now [red]suppressed[/red].')
                else:
                    self.console.print('Updates for this addon are [green]no longer suppressed[/green].')
            else:
                self.console.print('[green]Usage:[/green]\n\tThis command accepts an addon name as an argument.')
        elif args.startswith('wago'):
            args = args[5:]
            if args:
                if args == self.core.config['WAUsername']:
                    self.console.print(f'Wago version check is now: [green]ENABLED[/green]\nEntries created by '
                                       f'[bold white]{self.core.config["WAUsername"]}[/bold white] are now included.')
                    self.core.config['WAUsername'] = ''
                else:
                    self.core.config['WAUsername'] = args.strip()
                    self.console.print(f'Wago version check is now: [green]ENABLED[/green]\nEntries created by '
                                       f'[bold white]{self.core.config["WAUsername"]}[/bold white] are now ignored.')
            else:
                if self.core.config['WAUsername'] == 'DISABLED':
                    self.core.config['WAUsername'] = ''
                    self.console.print('Wago version check is now: [green]ENABLED[/green]')
                else:
                    self.core.config['WAUsername'] = '******'
                    shutil.rmtree(Path('Interface/AddOns/WeakAurasCompanion'), ignore_errors=True)
                    self.console.print('Wago version check is now: [red]DISABLED[/red]')
            self.core.save_config()
        elif args.startswith('authors'):
            status = self.core.generic_toggle('ShowAuthors')
            self.console.print('The authors listing is on now:',
                               '[green]ENABLED[/green]' if status else '[red]DISABLED[/red]')
        elif args.startswith('autoupdate'):
            status = self.core.generic_toggle('AutoUpdate')
            self.console.print('The automatic addon update on startup is now:',
                               '[green]ENABLED[/green]' if status else '[red]DISABLED[/red]')
        elif args.startswith('backup'):
            status = self.core.generic_toggle('Backup', 'Enabled')
            self.console.print('Backup of WTF directory is now:',
                               '[green]ENABLED[/green]' if status else '[red]DISABLED[/red]')
        elif args.startswith('compact_mode'):
            status = self.core.generic_toggle('CompactMode')
            self.console.print('Table compact mode is now:',
                               '[green]ENABLED[/green]' if status else '[red]DISABLED[/red]')

    def c_set(self, args):
        args = args.strip()
        if args.startswith('wago_api'):
            args = args[9:]
            if args:
                self.console.print('Wago API key is now set.')
                self.core.config['WAAPIKey'] = args.strip()
                self.core.save_config()
            elif self.core.config['WAAPIKey'] != '':
                self.console.print('Wago API key is now removed.')
                self.core.config['WAAPIKey'] = ''
                self.core.save_config()
            else:
                self.console.print('[green]Usage:[/green]\n\tThis command accepts API key as an argument.')
        elif args.startswith('wago_wow_account'):
            args = args[17:]
            if args:
                args = args.strip()
                if os.path.isfile(Path(f'WTF/Account/{args}/SavedVariables/WeakAuras.lua')) or \
                        os.path.isfile(Path(f'WTF/Account/{args}/SavedVariables/Plater.lua')):
                    self.console.print(f'WoW account name set to: [bold white]{args}[/bold white]')
                    self.core.config['WAAccountName'] = args
                    self.core.save_config()
                else:
                    self.console.print('Incorrect WoW account name.')
            else:
                self.console.print('[green]Usage:[/green]\n\tThis command accepts the WoW account name as an argument.')
        else:
            self.console.print('Unknown option.')

    def c_show(self, args):
        args = args.strip()
        if args.startswith('dependencies'):
            addons = sorted(list(filter(lambda k: k['URL'].startswith('https://www.curseforge.com/wow/addons/'),
                                        self.core.config['Addons'])), key=lambda k: k['Name'].lower())
            self.core.bulk_check(addons)
            for addon in addons:
                dependencies = DependenciesParser(self.core)
                name, _, _, _, _, _, _, _, _, _, deps, _ = self.core.update_addon(addon['URL'], False, False)
                dependencies.add_dependency(deps)
                deps = dependencies.parse_dependency(output=True)
                if len(deps) > 0:
                    self.console.print(f'[green]{name}[/green]\n{", ".join(deps)}')

    def c_wago_update(self, _, verbose=True):
        if os.path.isdir(Path('Interface/AddOns/WeakAuras')) or os.path.isdir(Path('Interface/AddOns/Plater')):
            accounts = self.core.detect_accounts()
            if len(accounts) == 0:
                return
            elif len(accounts) > 1 and self.core.config['WAAccountName'] == '':
                if verbose:
                    self.console.print('More than one WoW account detected.\nPlease use [bold white]set wago_wow_accoun'
                                       't[''/bold white] command to set the correct account name.')
                else:
                    self.console.print('\n[green]More than one WoW account detected.[/green]\nPlease use [bold white]se'
                                       't wago_wow_account[/bold white] command to set the correct account name.')
                return
            elif len(accounts) == 1 and self.core.config['WAAccountName'] == '':
                self.core.config['WAAccountName'] = accounts[0]
                self.core.save_config()
            wago = WagoUpdater(self.core.config, self.core.masterConfig)
            if self.core.masterConfig['WagoVersion'] != self.core.config['WACompanionVersion']:
                self.core.config['WACompanionVersion'] = self.core.masterConfig['WagoVersion']
                self.core.save_config()
                force = True
            else:
                force = False
            wago.install_companion(self.core.clientType, force)
            statuswa, statusplater = wago.update()
            if verbose:
                if len(statuswa[0]) > 0 or len(statuswa[1]) > 0:
                    self.console.print('[green]Outdated WeakAuras:[/green]')
                    for aura in statuswa[0]:
                        self.console.print(f'[link={aura[1]}]{aura[0]}[/link]', highlight=False)
                    self.console.print('\n[green]Detected WeakAuras:[/green]')
                    for aura in statuswa[1]:
                        self.console.print(f'[link={aura[1]}]{aura[0]}[/link]', highlight=False)
                if len(statusplater[0]) > 0 or len(statusplater[1]) > 0:
                    if len(statuswa[0]) != 0 or len(statuswa[1]) != 0:
                        self.console.print('')
                    self.console.print('[green]Outdated Plater profiles/scripts:[/green]')
                    for aura in statusplater[0]:
                        self.console.print(f'[link={aura[1]}]{aura[0]}[/link]', highlight=False)
                    self.console.print('\n[green]Detected Plater profiles/scripts:[/green]')
                    for aura in statusplater[1]:
                        self.console.print(f'[link={aura[1]}]{aura[0]}[/link]', highlight=False)
            else:
                if len(statuswa[0]) > 0:
                    self.console.print(f'\n[green]The number of outdated WeakAuras:[/green] '
                                       f'{len(statuswa[0])}', highlight=False)
                if len(statusplater[0]) > 0:
                    self.console.print(f'\n[green]The number of outdated Plater profiles/scripts:[/green] '
                                       f'{len(statusplater[0])}', highlight=False)
        elif verbose:
            self.console.print('No compatible addon is installed.')

    def c_search(self, args):
        if args:
            results = self.core.search(args)
            self.console.print('[green]Top results of your search:[/green]')
            for url in results:
                if self.core.check_if_installed(url):
                    self.console.print(f'[link={url}]{url}[/link] [yellow]\[Installed][/yellow]', highlight=False)
                else:
                    self.console.print(f'[link={url}]{url}[/link]', highlight=False)
        else:
            self.console.print('[green]Usage:[/green]\n\tThis command accepts a search query as an argument.')

    def c_recommendations(self, _):
        if not self.tipsDatabase:
            # noinspection PyBroadException
            try:
                self.tipsDatabase = pickle.load(gzip.open(io.BytesIO(
                    requests.get('https://storage.googleapis.com/cursebreaker/recommendations.pickle.gz',
                                 headers=HEADERS, timeout=5).content)))
            except Exception:
                self.tipsDatabase = {}
        if len(self.tipsDatabase) > 0:
            found = False
            for tip in self.tipsDatabase:
                breaker = False
                for addon, data in tip['Addons'].items():
                    check = True if self.core.check_if_installed(addon) else False
                    breaker = check == data['Installed']
                if breaker:
                    found = True
                    recomendation = tip["Recomendation"].replace('|n', '\n')
                    self.console.print(f'[bold white underline]{tip["Title"]}[/bold white underline] by [green]'
                                       f'{tip["Author"]}[/green]\n\n{recomendation}\n', highlight=False)
            if not found:
                self.console.print('Not found any recommendations for you. Good job!')

    def c_import(self, args):
        hit, partial_hit, miss = self.core.detect_addons()
        if args == 'install' and len(hit) > 0:
            self.c_install(','.join(hit))
        else:
            self.console.print(f'[green]Addons found:[/green]')
            for addon in hit:
                self.console.print(addon, highlight=False)
            self.console.print(f'\n[yellow]Possible matches:[/yellow]')
            for addon in partial_hit:
                self.console.print(' [bold white]or[/bold white] '.join(addon), highlight=False)
            self.console.print(f'\n[red]Unknown directories:[/red]')
            for addon in miss:
                self.console.print(f'{addon}', highlight=False)
            self.console.print(f'\nExecute [bold white]import install[/bold white] command to install all detected addo'
                               f'ns.\nPossible matches need to be installed manually with the [bold white]install[/bold'
                               f' white] command.\nAddons that are available only on WoWInterface and/or Tukui are not '
                               f'detected by this process.')

    def c_export(self, _):
        payload = self.core.export_addons()
        pyperclip.copy(payload)
        self.console.print(f'{payload}\n\nThe command above was copied to the clipboard.', highlight=False)

    def c_help(self, _):
        self.console.print('[green]install [URL][/green]\n\tCommand accepts a space-separated list of links.\n\t[bold w'
                           'hite]Flags:[/bold white]\n\t\t[bold white]-i[/bold white] - Disable the client version chec'
                           'k.\n'
                           '[green]uninstall [URL/Name][/green]\n\tCommand accepts a space-separated list of addon name'
                           's or full links.\n\t[bold white]Flags:[/bold white]\n\t\t[bold white]-k[/bold white] - Keep'
                           ' the addon files after uninstalling.\n'
                           '[green]update [URL/Name][/green]\n\tCommand accepts a space-separated list of addon names o'
                           'r full links.\n\tIf no argument is provided all non-modified addons will be updated.\n'
                           '[green]force_update [URL/Name][/green]\n\tCommand accepts a space-separated list of addon n'
                           'ames or full links.\n\tSelected addons will be reinstalled or updated regardless of their c'
                           'urrent state.\n\tIf no argument is provided all addons will be forcefully updated.\n'
                           '[green]wago_update[/green]\n\tCommand detects all installed WeakAuras and Plater profiles/s'
                           'cripts.\n\tAnd then generate WeakAuras Companion payload.\n'
                           '[green]status[/green]\n\tPrints the current state of all installed addons.\n\t[bold yellow]'
                           '[!][/bold yellow] mark means that the latest release is not updated yet for the current WoW'
                           ' version.\n\t[bold white]Flags:[/bold white]\n\t\t[bold white]-a[/bold white] - Temporary r'
                           'everse the table compacting option.\n\t\t[bold white]-s[/bold white] - Display the source o'
                           'f the addons.\n'
                           '[green]orphans[/green]\n\tPrints list of orphaned directories and files.\n'
                           '[green]search [Keyword][/green]\n\tExecutes addon search on CurseForge.\n'
                           '[green]recommendations[/green]\n\tCheck the list of currently installed addons against a co'
                           'mmunity-driven database of tips.\n'
                           '[green]import[/green]\n\tCommand attempts to import already installed addons.\n'
                           '[green]export[/green]\n\tCommand prints list of all installed addons in a form suitable f'
                           'or sharing.\n'
                           '[green]toggle authors[/green]\n\tEnables/disables the display of addon author names in the '
                           'table.\n'
                           '[green]toggle autoupdate[/green]\n\tEnables/disables the automatic addon update on startup'
                           '.\n'
                           '[green]toggle backup[/green]\n\tEnables/disables automatic daily backup of WTF directory.\n'
                           '[green]toggle channel [Name][/green]\n\tCommand accepts an addon name (or "global") as argu'
                           'ment.\n\tPrioritizes alpha/beta versions for the provided addon.\n'
                           '[green]toggle compact_mode [/green]\n\tEnables/disables compact table mode that hides entri'
                           'es of up-to-date addons.\n'
                           '[green]toggle pinning [Name][/green]\n\tCommand accepts an addon name as argument.\n\tBlock'
                           's/unblocks updating of the provided addon.\n'
                           '[green]toggle wago [Username][/green]\n\tEnables/disables automatic Wago updates.\n\tIf a u'
                           'sername is provided check will start to ignore the specified author.\n'
                           '[green]set wago_api [API key][/green]\n\tSets Wago API key required to access private entri'
                           'es.\n\tIt can be procured here:'
                           ' [link=https://wago.io/account]https://wago.io/account[/link]\n'
                           '[green]set wago_wow_account [Account name][/green]\n\tSets WoW account used by Wago updater'
                           '.\n\tNeeded only if compatibile addons are used on more than one WoW account.\n'
                           '[green]show dependencies[/green]\n\tDisplay a list of dependencies of all installed addons.'
                           '\n[green]uri_integration[/green]\n\tEnables integration with CurseForge page.\n\t[i]"Instal'
                           'l"[/i] button will now start this application.\n'
                           '\n[bold green]Supported URL:[/bold green]\n\thttps://www.curseforge.com/wow/addons/\[addon_'
                           'name] [bold white]|[/bold white] cf:\[addon_name]\n\thttps://www.wowinterface.com/downloads'
                           '/\[addon_name] [bold white]|[/bold white] wowi:\[addon_id]\n\thttps://www.tukui.org/addons.'
                           'php?id=\[addon_id] [bold white]|[/bold white] tu:\[addon_id]\n\thttps://www.tukui.org/class'
                           'ic-addons.php?id=\[addon_id] [bold white]|[/bold white] tuc:\[addon_id]\n\thttps://github.c'
                           'om/\[username]/\[repository_name] [bold white]|[/bold white] gh:\[username]/\[repository_na'
                           'me]\n\tElvUI [bold white]|[/bold white] ElvUI:Dev\n\tTukui\n\tShadow&Light:Dev',
                           highlight=False)

    def c_exit(self, _):
        sys.exit(0)
Example #7
0
class TUI:
    def __init__(self):
        self.core = Core()
        self.session = PromptSession(reserve_space_for_menu=7)
        self.tableData = None
        self.table = None
        self.cfSlugs = None
        self.wowiSlugs = None
        self.completer = None
        self.os = platform.system()
        if self.os == 'Windows':
            self.chandle = windll.kernel32.GetStdHandle(-11)
        sys.tracebacklimit = 0

    def start(self):
        self.setup_console()
        self.print_header()
        # Check if executable is in good location
        if not glob.glob('World*.app') and not glob.glob('Wow*.exe') or \
                not os.path.isdir(Path('Interface/AddOns')) or not os.path.isdir('WTF'):
            printft(
                HTML(
                    '<ansibrightred>This executable should be placed in the same directory where Wow.exe, '
                    'WowClassic.exe or World of Warcraft.app is located.</ansibrightred>\n'
                ))
            pause()
            sys.exit(1)
        # Detect Classic client
        if os.path.basename(os.getcwd()) == '_classic_':
            self.core.clientType = 'wow_classic'
            set_terminal_title(f'CurseBreaker v{__version__} - Classic')
        # Check if client have write access
        try:
            with open('PermissionTest', 'w') as _:
                pass
            os.remove('PermissionTest')
        except IOError:
            printft(
                HTML(
                    '<ansibrightred>CurseBreaker doesn\'t have write rights for the current directory.\n'
                    'Try starting it with administrative privileges.</ansibrightred>\n'
                ))
            pause()
            sys.exit(1)
        self.auto_update()
        self.core.init_config()
        self.setup_table()
        # Curse URI Support
        if len(sys.argv) == 2 and 'twitch://' in sys.argv[1]:
            try:
                self.c_install(sys.argv[1].strip())
            except Exception as e:
                self.handle_exception(e)
            timeout()
            sys.exit(0)
        if len(sys.argv) == 2 and '.ccip' in sys.argv[1]:
            try:
                path = sys.argv[1].strip()
                self.c_install(self.core.parse_cf_xml(path))
                if os.path.exists(path):
                    os.remove(path)
            except Exception as e:
                self.handle_exception(e)
            timeout()
            sys.exit(0)
        # CLI command
        if len(sys.argv) >= 2:
            command = ' '.join(sys.argv[1:]).split(' ', 1)
            if getattr(self, f'c_{command[0].lower()}', False):
                try:
                    getattr(self, f'c_{command[0].lower()}')(
                        command[1].strip() if len(command) > 1 else False)
                except Exception as e:
                    self.handle_exception(e)
            else:
                printft('Command not found.')
            sys.exit(0)
        # Addons auto update
        if len(self.core.config['Addons']) > 0:
            printft('Automatic update of all addons will start in 5 seconds.\n'
                    'Press any button to enter interactive mode.')
            starttime = time.time()
            keypress = None
            while True:
                if kbhit():
                    keypress = getch()
                    break
                elif time.time() - starttime > 5:
                    break
            if not keypress:
                if len(self.core.config['Addons']) > 35:
                    self.setup_console(len(self.core.config['Addons']))
                self.print_header()
                try:
                    self.c_update(None, True)
                    if self.core.backup_check():
                        self.setup_table()
                        printft(
                            HTML(
                                '\n<ansigreen>Backing up WTF directory:</ansigreen>'
                            ))
                        self.core.backup_wtf()
                    if self.core.config['WAUsername'] != 'DISABLED':
                        self.setup_table()
                        self.c_wa_update(None, False)
                except Exception as e:
                    self.handle_exception(e)
                printft('')
                pause()
                sys.exit(0)
        self.setup_completer()
        self.setup_console(len(self.core.config['Addons']))
        self.print_header()
        printft(
            HTML(
                'Use command <ansigreen>help</ansigreen> or press <ansigreen>TAB</ansigreen> to see a list of avai'
                'lable commands.\nCommand <ansigreen>exit</ansigreen> or pressing <ansigreen>CTRL+D</ansigreen> wi'
                'll close the application.\n'))
        if len(self.core.config['Addons']) == 0:
            printft(
                HTML(
                    'Command <ansigreen>import</ansigreen> might be used to detect already installed addons.\n'
                ))
        # Prompt session
        while True:
            try:
                command = self.session.prompt(
                    HTML('<ansibrightgreen>CB></ansibrightgreen> '),
                    completer=self.completer)
            except KeyboardInterrupt:
                continue
            except EOFError:
                break
            else:
                command = command.split(' ', 1)
                if getattr(self, f'c_{command[0].lower()}', False):
                    try:
                        self.setup_table()
                        getattr(self, f'c_{command[0].lower()}')(
                            command[1].strip() if len(command) > 1 else False)
                        self.setup_completer()
                    except Exception as e:
                        self.handle_exception(e)
                else:
                    printft('Command not found.')

    def auto_update(self):
        if getattr(sys, 'frozen', False):
            try:
                if os.path.isfile(sys.executable + '.old'):
                    try:
                        os.remove(sys.executable + '.old')
                    except PermissionError:
                        pass
                payload = requests.get(
                    'https://api.github.com/repos/AcidWeb/CurseBreaker/releases/latest',
                    headers=HEADERS).json()
                remoteversion = payload['name']
                changelog = payload['body']
                url = None
                for binary in payload['assets']:
                    if (self.os == 'Windows' and '.exe' in binary['name'])\
                            or (self.os == 'Darwin' and '.zip' in binary['name'])\
                            or (self.os == 'Linux' and '.gz' in binary['name']):
                        url = binary['browser_download_url']
                        break
                if url and StrictVersion(
                        remoteversion[1:]) > StrictVersion(__version__):
                    printft(
                        HTML(
                            '<ansigreen>Updating CurseBreaker...</ansigreen>'))
                    shutil.move(sys.executable, sys.executable + '.old')
                    payload = requests.get(url, headers=HEADERS)
                    if self.os == 'Darwin':
                        zipfile.ZipFile(io.BytesIO(
                            payload.content)).extractall()
                    else:
                        with open(sys.executable, 'wb') as f:
                            if self.os == 'Windows':
                                f.write(payload.content)
                            elif self.os == 'Linux':
                                f.write(gzip.decompress(payload.content))
                    os.chmod(sys.executable, 0o775)
                    printft(
                        HTML(
                            f'<ansibrightgreen>Update complete! Please restart the application.</ansibrightgreen'
                            f'>\n\n<ansigreen>Changelog:</ansigreen>\n{changelog}\n'
                        ))
                    pause()
                    sys.exit(0)
            except Exception as e:
                printft(
                    HTML(
                        f'<ansibrightred>Update failed!\n\nReason: {str(e)}</ansibrightred>\n'
                    ))
                pause()
                sys.exit(1)

    def handle_exception(self, e, table=True):
        if len(self.tableData) > 1 and table:
            self.sanitize_table()
            printft(ANSI(self.table.table))
        if getattr(sys, 'frozen', False):
            if isinstance(e, list):
                for es in e:
                    printft(
                        HTML(f'\n<ansibrightred>{str(es)}</ansibrightred>'))
            else:
                printft(HTML(f'\n<ansibrightred>{str(e)}</ansibrightred>'))
        else:
            if isinstance(e, list):
                for es in e:
                    traceback.print_exception(es,
                                              es,
                                              es.__traceback__,
                                              limit=1000)
            else:
                traceback.print_exc(limit=1000)

    def print_header(self):
        clear()
        printft(
            HTML(
                f'<ansibrightblack>~~~ <ansibrightgreen>CurseBreaker</ansibrightgreen> <ansibrightred>v'
                f'{__version__}</ansibrightred> ~~~</ansibrightblack>\n'))

    def setup_console(self, buffer=0):
        if getattr(sys, 'frozen', False) and self.os == 'Windows':
            if buffer > 0:
                windll.kernel32.SetConsoleScreenBufferSize(
                    self.chandle, wintypes._COORD(100,
                                                  100 + round(buffer, -2)))
            else:
                windll.kernel32.SetConsoleWindowInfo(
                    self.chandle, True,
                    byref(wintypes.SMALL_RECT(0, 0, 99, 49)))
                windll.kernel32.SetConsoleScreenBufferSize(
                    self.chandle, wintypes._COORD(100, 50))
        elif self.os == 'Darwin':
            set_terminal_size(100, 50)

    def setup_completer(self):
        if not self.cfSlugs or not self.wowiSlugs:
            # noinspection PyBroadException
            try:
                self.cfSlugs = pickle.load(
                    gzip.open(
                        io.BytesIO(
                            requests.get(
                                'https://storage.googleapis.com/cursebreaker/cfslugs.pickle.gz',
                                headers=HEADERS).content)))
                self.wowiSlugs = pickle.load(
                    gzip.open(
                        io.BytesIO(
                            requests.get(
                                'https://storage.googleapis.com/cursebreaker/wowislugs.pickle.gz',
                                headers=HEADERS).content)))
            except Exception:
                self.cfSlugs = []
                self.wowiSlugs = []
        commands = [
            'install', 'uninstall', 'update', 'force_update', 'wa_update',
            'status', 'orphans', 'search', 'import', 'export', 'toggle_backup',
            'toggle_dev', 'toggle_wa', 'set_wa_api', 'set_wa_wow_account',
            'uri_integration', 'help', 'exit'
        ]
        addons = sorted(self.core.config['Addons'],
                        key=lambda k: k['Name'].lower())
        for addon in addons:
            name = f'"{addon["Name"]}"' if ',' in addon["Name"] else addon[
                "Name"]
            commands.extend([
                f'uninstall {name}', f'update {name}', f'force_update {name}',
                f'toggle_dev {name}', f'status {name}'
            ])
        for item in self.cfSlugs:
            commands.append(f'install cf:{item}')
        for item in self.wowiSlugs:
            commands.append(f'install wowi:{item}')
        commands.extend(
            ['install ElvUI', 'install ElvUI:Dev', 'install Tukui'])
        wa = WeakAuraUpdater('', '', '')
        accounts = wa.get_accounts()
        for account in accounts:
            commands.append(f'set_wa_wow_account {account}')
        self.completer = WordCompleter(commands,
                                       ignore_case=True,
                                       sentence=True)

    def setup_table(self):
        self.tableData = [[
            f'{AC.LIGHTWHITE_EX}Status{AC.RESET}',
            f'{AC.LIGHTWHITE_EX}Name{AC.RESET}',
            f'{AC.LIGHTWHITE_EX}Version{AC.RESET}'
        ]]
        self.table = SingleTable(
            self.tableData) if self.os == 'Windows' else UnicodeSingleTable(
                self.tableData)
        self.table.justify_columns[0] = 'center'

    def sanitize_table(self):
        if not self.table.ok:
            mwidth = self.table.column_max_width(1)
            for row in self.table.table_data[1:]:
                if len(row[1]) > mwidth:
                    row[1] = row[1][:mwidth - 3] + '...'

    def c_install(self, args):
        if args:
            if args.startswith('-i '):
                args = args[3:]
                optignore = True
            else:
                optignore = False
            addons = [
                addon.strip()
                for addon in list(reader([args], skipinitialspace=True))[0]
            ]
            with tqdm(total=len(addons),
                      bar_format='{n_fmt}/{total_fmt} |{bar}|') as pbar:
                for addon in addons:
                    installed, name, version = self.core.add_addon(
                        addon, optignore)
                    if installed:
                        self.tableData.append(
                            [f'{AC.GREEN}Installed{AC.RESET}', name, version])
                    else:
                        self.tableData.append([
                            f'{AC.LIGHTBLACK_EX}Already installed{AC.RESET}',
                            name, version
                        ])
                    pbar.update(1)
            self.sanitize_table()
            printft(ANSI(self.table.table))
        else:
            printft(
                HTML(
                    '<ansigreen>Usage:</ansigreen>\n\tThis command accepts a comma-separated list of links as an a'
                    'rgument.\n\tOption <ansiwhite>-i</ansiwhite> will disable the client version check.\n<ansigre'
                    'en>Supported URLs:</ansigreen>\n\thttps://www.curseforge.com/wow/addons/[addon_name] <ansiwhi'
                    'te>|</ansiwhite> cf:[addon_name]\n\thttps://www.wowinterface.com/downloads/[addon_name] <ansi'
                    'white>|</ansiwhite> wowi:[addon_id]\n\thttps://www.tukui.org/addons.php?id=[addon_id] <ansiwh'
                    'ite>|</ansiwhite> tu:[addon_id]\n\thttps://www.tukui.org/classic-addons.php?id=[addon_id] <an'
                    'siwhite>|</ansiwhite> tuc:[addon_id]\n\tElvUI <ansiwhite>|</ansiwhite> ElvU'
                    'I:Dev\n\tTukui'))

    def c_uninstall(self, args):
        if args:
            addons = [
                addon.strip()
                for addon in list(reader([args], skipinitialspace=True))[0]
            ]
            with tqdm(total=len(addons),
                      bar_format='{n_fmt}/{total_fmt} |{bar}|') as pbar:
                for addon in addons:
                    name, version = self.core.del_addon(addon)
                    if name:
                        self.tableData.append([
                            f'{AC.LIGHTRED_EX}Uninstalled{AC.RESET}', name,
                            version
                        ])
                    else:
                        self.tableData.append([
                            f'{AC.LIGHTBLACK_EX}Not installed{AC.RESET}',
                            addon, ''
                        ])
                    pbar.update(1)
            self.sanitize_table()
            printft(ANSI(self.table.table))
        else:
            printft(
                HTML(
                    '<ansigreen>Usage:</ansigreen>\n\tThis command accepts a comma-separated list of links as an a'
                    'rgument.\n<ansigreen>Supported URLs:</ansigreen>\n\thttps://www.curseforge.com/wow/addons/[ad'
                    'don_name] <ansiwhite>|</ansiwhite> cf:[addon_name]\n\thttps://www.wowinterface.com/downloads/'
                    '[addon_name] <ansiwhite>|</ansiwhite> wowi:[addon_id]\n\thttps://www.tukui.org/addons.php?id='
                    '[addon_id] <ansiwhite>|</ansiwhite> tu:[addon_id]\n\thttps://www.tukui.org/classic-addons.php'
                    '?id=[addon_id] <ansiwhite>|</ansiwhite> tuc:[addon_id]\n\tElvUI <ansiwhite>|</ansiwhite> ElvU'
                    'I:Dev\n\tTukui'))

    def c_update(self, args, addline=False, update=True, force=False):
        if len(self.core.cfCache) > 0 or len(self.core.wowiCache) > 0:
            self.core.cfCache = {}
            self.core.wowiCache = {}
        if args:
            addons = [
                addon.strip()
                for addon in list(reader([args], skipinitialspace=True))[0]
            ]
        else:
            addons = sorted(self.core.config['Addons'],
                            key=lambda k: k['Name'].lower())
            self.core.bulk_check(addons)
        with tqdm(total=len(addons),
                  bar_format='{n_fmt}/{total_fmt} |{bar}|') as pbar:
            exceptions = []
            for addon in addons:
                try:
                    name, versionnew, versionold, modified = self.core.\
                        update_addon(addon if isinstance(addon, str) else addon['URL'], update, force)
                    if versionold:
                        if versionold == versionnew:
                            if modified:
                                self.tableData.append([
                                    f'{AC.LIGHTRED_EX}Modified{AC.RESET}',
                                    name, versionold
                                ])
                            else:
                                self.tableData.append([
                                    f'{AC.GREEN}Up-to-date{AC.RESET}', name,
                                    versionold
                                ])
                        else:
                            if modified:
                                self.tableData.append([
                                    f'{AC.LIGHTRED_EX}Update suppressed{AC.RESET}',
                                    name, versionold
                                ])
                            else:
                                self.tableData.append([
                                    f'{AC.YELLOW}{"Updated " if update else "Update available"}'
                                    f'{AC.RESET}', name,
                                    f'{AC.YELLOW}{versionnew}{AC.RESET}'
                                ])
                    else:
                        self.tableData.append([
                            f'{AC.LIGHTBLACK_EX}Not installed{AC.RESET}',
                            addon, ''
                        ])
                except Exception as e:
                    exceptions.append(e)
                pbar.update(1)
        self.sanitize_table()
        printft(ANSI('\n' + self.table.table if addline else self.table.table))
        if len(exceptions) > 0:
            self.handle_exception(exceptions, False)

    def c_force_update(self, args):
        if args:
            self.c_update(args, False, True, True)
        else:
            printft(
                HTML(
                    '<ansigreen>Usage:</ansigreen>\n\tThis command accepts a comma-separated list of links or addo'
                    'n names as an argument.'))

    def c_status(self, args):
        self.c_update(args, False, False)

    def c_orphans(self, _):
        orphansd, orphansf = self.core.find_orphans()
        printft(
            HTML(
                '<ansigreen>Directories that are not part of any installed addon:</ansigreen>'
            ))
        for orphan in sorted(orphansd):
            printft(
                HTML(orphan.replace('[GIT]',
                                    '<ansiyellow>[GIT]</ansiyellow>')))
        printft(
            HTML(
                '\n<ansigreen>Files that are leftovers after no longer installed addons:</ansigreen>'
            ))
        for orphan in sorted(orphansf):
            printft(orphan)

    def c_uri_integration(self, _):
        if self.os == 'Windows':
            self.core.create_reg()
            printft(
                'CurseBreaker.reg file was created. Attempting to import...')
            out = os.system('"' + str(
                Path(os.path.dirname(sys.executable), 'CurseBreaker.reg')) +
                            '"')
            if out != 0:
                printft(
                    'Import failed. Please try to import REG file manually.')
            else:
                os.remove('CurseBreaker.reg')
        else:
            printft('This feature is available only on Windows.')

    def c_symlink_protection(self, _):
        if self.os == 'Windows':
            printft(HTML('<ansigreen>Directories tweaked:</ansigreen>'))
            for root, dirs, _ in os.walk(self.core.path / '..' / '..'):
                for d in dirs:
                    path = Path(root) / d
                    if os.path.islink(path):
                        set_icon(path, Path("C:/Windows/System32/SHELL32.dll"),
                                 4)
                        print(path.resolve())
        else:
            printft('This feature is available only on Windows.')

    def c_toggle_dev(self, args):
        if args:
            status = self.core.dev_toggle(args)
            if status is None:
                printft(
                    HTML(
                        '<ansibrightred>This addon does not exist or it is not installed yet.</ansibrightred>'
                    ))
            elif status:
                printft('This addon will now prioritize alpha/beta versions.')
            else:
                printft(
                    'This addon will not longer prioritize alpha/beta versions.'
                )
        else:
            printft(
                HTML(
                    '<ansigreen>Usage:</ansigreen>\n\tThis command accepts an addon name as an argument.'
                ))

    def c_toggle_backup(self, _):
        status = self.core.backup_toggle()
        printft(
            'Backup of WTF directory is now:',
            HTML('<ansigreen>ENABLED</ansigreen>')
            if status else HTML('<ansired>DISABLED</ansired>'))

    def c_toggle_wa(self, args):
        if args:
            if args == self.core.config['WAUsername']:
                printft(
                    HTML(
                        f'WeakAuras version check is now: <ansigreen>ENABLED</ansigreen>\n'
                        f'Auras created by <ansiwhite>{self.core.config["WAUsername"]}</ansiwhite>'
                        f' are now included.'))
                self.core.config['WAUsername'] = ''
            else:
                self.core.config['WAUsername'] = args.strip()
                printft(
                    HTML(
                        f'WeakAuras version check is now: <ansigreen>ENABLED</ansigreen>\n'
                        f'Auras created by <ansiwhite>{self.core.config["WAUsername"]}</ansiwhite>'
                        f' are now ignored.'))
        else:
            if self.core.config['WAUsername'] == 'DISABLED':
                self.core.config['WAUsername'] = ''
                printft(
                    HTML(
                        'WeakAuras version check is now: <ansigreen>ENABLED</ansigreen>'
                    ))
            else:
                self.core.config['WAUsername'] = '******'
                shutil.rmtree(Path('Interface/AddOns/WeakAurasCompanion'),
                              ignore_errors=True)
                printft(
                    HTML(
                        'WeakAuras version check is now: <ansired>DISABLED</ansired>'
                    ))
        self.core.save_config()

    def c_set_wa_api(self, args):
        if args:
            printft('Wago API key is now set.')
            self.core.config['WAAPIKey'] = args.strip()
            self.core.save_config()
        elif self.core.config['WAAPIKey'] != '':
            printft('Wago API key is now removed.')
            self.core.config['WAAPIKey'] = ''
            self.core.save_config()
        else:
            printft(
                HTML(
                    '<ansigreen>Usage:</ansigreen>\n\tThis command accepts API key as an argument.'
                ))

    def c_set_wa_wow_account(self, args):
        if args:
            args = args.strip()
            if os.path.isfile(
                    Path(f'WTF/Account/{args}/SavedVariables/WeakAuras.lua')):
                printft(
                    HTML(
                        f'WoW account name set to: <ansiwhite>{args}</ansiwhite>'
                    ))
                self.core.config['WAAccountName'] = args
                self.core.save_config()
            else:
                printft('Incorrect WoW account name.')
        else:
            printft(
                HTML(
                    '<ansigreen>Usage:</ansigreen>\n\tThis command accepts the WoW account name as an argument.'
                ))

    def c_wa_update(self, _, verbose=True):
        if os.path.isdir(Path('Interface/AddOns/WeakAuras')):
            wa = WeakAuraUpdater(
                '' if self.core.config['WAUsername'] == 'DISABLED' else
                self.core.config['WAUsername'],
                self.core.config['WAAccountName'],
                self.core.config['WAAPIKey'])
            accounts = wa.get_accounts()
            if len(accounts) > 1:
                if verbose:
                    printft(
                        HTML(
                            'More than one WoW account detected.\nPlease use <ansiwhite>set_wa_wow_account</ansiwh'
                            'ite> command to set the correct account name.'))
                else:
                    printft(
                        HTML(
                            '\n<ansigreen>More than one WoW account detected.</ansigreen>\nPlease use <ansiwhite>s'
                            'et_wa_wow_account</ansiwhite> command to set the correct account name.'
                        ))
                return
            if wa.accountName:
                if not self.core.config['WAAccountName']:
                    self.core.config['WAAccountName'] = wa.accountName
                    self.core.save_config()
                if self.core.waCompanionVersion != self.core.config[
                        'WACompanionVersion']:
                    self.core.config[
                        'WACompanionVersion'] = self.core.waCompanionVersion
                    self.core.save_config()
                    force = True
                else:
                    force = False
                wa.parse_storage()
                status = wa.check_updates()
                wa.install_companion(self.core.clientType, force)
                wa.install_data()
                if verbose:
                    printft(HTML('<ansigreen>Outdated WeakAuras:</ansigreen>'))
                    for aura in status[0]:
                        printft(aura)
                    printft(
                        HTML('\n<ansigreen>Detected WeakAuras:</ansigreen>'))
                    for aura in status[1]:
                        printft(aura)
                else:
                    printft(
                        HTML(
                            f'\n<ansigreen>The number of outdated WeakAuras:</ansigreen> {len(status[0])}'
                        ))
        elif verbose:
            printft('WeakAuras addon is not installed.')

    def c_search(self, args):
        if args:
            results = self.core.search(args)
            printft(HTML('<ansigreen>Top results of your search:</ansigreen>'))
            for url in results:
                if self.core.check_if_installed(url):
                    printft(
                        HTML(f'{url} <ansiyellow>[Installed]</ansiyellow>'))
                else:
                    printft(url)
        else:
            printft(
                HTML(
                    '<ansigreen>Usage:</ansigreen>\n\tThis command accepts a search query as an argument.'
                ))

    def c_import(self, args):
        hit, partial_hit, miss = self.core.detect_addons()
        if args == 'install' and len(hit) > 0:
            self.c_install(','.join(hit))
        else:
            printft(HTML(f'<ansigreen>Addons found:</ansigreen>'))
            for addon in hit:
                printft(addon)
            printft(HTML(f'\n<ansiyellow>Possible matches:</ansiyellow>'))
            for addon in partial_hit:
                printft(HTML(' <ansiwhite>or</ansiwhite> '.join(addon)))
            printft(HTML(f'\n<ansired>Unknown directories:</ansired>'))
            for addon in miss:
                printft(f'{addon}')
            printft(
                HTML(
                    f'\nExecute <ansiwhite>import install</ansiwhite> command to install all detected addons.\n'
                    f'Possible matches need to be installed manually with the <ansiwhite>install</ansiwhite>'
                    f' command.'))

    def c_export(self, _):
        printft(self.core.export_addons())

    def c_help(self, _):
        printft(
            HTML(
                '<ansigreen>install [URL]</ansigreen>\n\tCommand accepts a comma-separated list of links.\n'
                '<ansigreen>uninstall [URL/Name]</ansigreen>\n\tCommand accepts a comma-separated list of links or'
                ' addon names.\n'
                '<ansigreen>update [URL/Name]</ansigreen>\n\tCommand accepts a comma-separated list of links or ad'
                'don names.\n\tIf no argument is provided all non-modified addons will be updated.\n'
                '<ansigreen>force_update [URL/Name]</ansigreen>\n\tCommand accepts a comma-separated list of links'
                ' or addon names.\n\tSelected addons will be reinstalled or updated regardless of their current st'
                'ate.\n'
                '<ansigreen>wa_update</ansigreen>\n\tCommand detects all installed WeakAuras and generate WeakAura'
                's Companion payload.\n'
                '<ansigreen>status</ansigreen>\n\tPrints the current state of all installed addons.\n'
                '<ansigreen>orphans</ansigreen>\n\tPrints list of orphaned directories and files.\n'
                '<ansigreen>search [Keyword]</ansigreen>\n\tExecutes addon search on CurseForge.\n'
                '<ansigreen>import</ansigreen>\n\tCommand attempts to import already installed addons.\n'
                '<ansigreen>export</ansigreen>\n\tCommand prints list of all installed addons in a form suitable f'
                'or sharing.\n'
                '<ansigreen>toggle_backup</ansigreen>\n\tEnables/disables automatic daily backup of WTF directory.'
                '\n<ansigreen>toggle_dev [Name]</ansigreen>\n\tCommand accepts an addon name as argument.\n\tPrior'
                'itizes alpha/beta versions for the provided addon.\n'
                '<ansigreen>toggle_wa [Username]</ansigreen>\n\tEnables/disables automatic WeakAuras updates.\n\tI'
                'f a username is provided check will start to ignore the specified author.\n'
                '<ansigreen>set_wa_api [API key]</ansigreen>\n\tSets Wago API key required to access private auras'
                '.\n\tIt can be procured here: https://wago.io/account\n'
                '<ansigreen>set_wa_wow_account [Account name]</ansigreen>\n\tSets WoW account used by WeakAuras up'
                'dater.\n\tNeeded only if WeakAuras are used on more than one WoW account.\n'
                '<ansigreen>uri_integration</ansigreen>\n\tEnables integration with CurseForge page. "Install" but'
                'ton will now start this application.\n'
                '\n<ansibrightgreen>Supported URL:</ansibrightgreen>\n\thttps://www.curseforge.com/wow/addons/[add'
                'on_name] <ansiwhite>|</ansiwhite> cf:[addon_name]\n\thttps://www.wowinterface.com/downloads/[addo'
                'n_name] <ansiwhite>|</ansiwhite> wowi:[addon_id]\n\thttps://www.tukui.org/addons.php?id=[addon_id'
                '] <ansiwhite>|</ansiwhite> tu:[addon_id]\n\thttps://www.tukui.org/classic-addons.php?id=[addon_id'
                '] <ansiwhite>|</ansiwhite> tuc:[addon_id]\n\tElvUI <ansiwhite>|</ansiwhite> ElvUI:Dev\n\tTukui'
            ))

    def c_exit(self, _):
        sys.exit(0)
Example #8
0
class TUI:
    def __init__(self):
        self.core = Core()
        self.session = PromptSession()
        self.table_data = None
        self.table = None
        self.completer = None
        self.chandle = windll.kernel32.GetStdHandle(-11)
        sys.tracebacklimit = 0
        init()

    def start(self):
        self.setup_console()
        self.print_header()
        # Check if executable is in good location
        if not os.path.isfile('Wow.exe') or not os.path.isdir(
                'Interface\\AddOns') or not os.path.isdir('WTF'):
            printft(
                HTML(
                    '<ansibrightred>This executable should be placed in the same directory where Wow.exe is locate'
                    'd.</ansibrightred>\n'))
            os.system('pause')
            sys.exit(1)
        # Check if client have write access
        try:
            with open('PermissionTest', 'w') as _:
                pass
            os.remove('PermissionTest')
        except IOError:
            printft(
                HTML(
                    '<ansibrightred>CurseBreaker doesn\'t have write rights for the current directory.\n'
                    'Try starting it with administrative privileges.</ansibrightred>\n'
                ))
            os.system('pause')
            sys.exit(1)
        self.auto_update()
        self.core.init_config()
        self.setup_completer()
        self.setup_table()
        # Curse URI Support
        if len(sys.argv) == 2 and 'twitch://' in sys.argv[1]:
            try:
                self.c_install(sys.argv[1].strip())
            except Exception as e:
                self.handle_exception(e)
            os.system('timeout /t 5')
            sys.exit(0)
        # CLI command
        if len(sys.argv) >= 2:
            command = ' '.join(sys.argv[1:]).split(' ', 1)
            if getattr(self, f'c_{command[0].lower()}', False):
                try:
                    getattr(self, f'c_{command[0].lower()}')(
                        command[1].strip() if len(command) > 1 else False)
                except Exception as e:
                    self.handle_exception(e)
            else:
                printft('Command not found.')
            sys.exit(0)
        # Addons auto update
        if len(self.core.config['Addons']) > 0:
            printft('Automatic update of all addons will start in 5 seconds.\n'
                    'Press any button to enter interactive mode.')
            starttime = time.time()
            keypress = None
            while True:
                if msvcrt.kbhit():
                    keypress = msvcrt.getch()
                    break
                elif time.time() - starttime > 5:
                    break
            if not keypress:
                if len(self.core.config['Addons']) > 37:
                    self.setup_console(True)
                self.print_header()
                try:
                    self.c_update(False, True)
                    if self.core.backup_check():
                        self.setup_table()
                        printft(
                            HTML(
                                '\n<ansigreen>Backing up WTF directory:</ansigreen>'
                            ))
                        self.core.backup_wtf()
                except Exception as e:
                    self.handle_exception(e)
                printft('')
                os.system('pause')
                sys.exit(0)
        self.setup_console(True)
        self.print_header()
        printft(
            HTML(
                'Use command <ansigreen>help</ansigreen> or press <ansigreen>TAB</ansigreen> to see a list of avai'
                'lable commands.\nCommand <ansigreen>exit</ansigreen> or pressing <ansigreen>CTRL+D</ansigreen> wi'
                'll close the application.\n'))
        # Prompt session
        while True:
            try:
                command = self.session.prompt(
                    HTML('<ansibrightgreen>CB></ansibrightgreen> '),
                    completer=self.completer)
            except KeyboardInterrupt:
                continue
            except EOFError:
                break
            else:
                command = command.split(' ', 1)
                if getattr(self, f'c_{command[0].lower()}', False):
                    try:
                        self.setup_table()
                        getattr(self, f'c_{command[0].lower()}')(
                            command[1].strip() if len(command) > 1 else False)
                        self.setup_completer()
                    except Exception as e:
                        self.handle_exception(e)
                else:
                    printft('Command not found.')

    def auto_update(self):
        if getattr(sys, 'frozen', False):
            try:
                payload = requests.get(
                    'https://api.github.com/repos/AcidWeb/CurseBreaker/releases/latest'
                ).json()
                remoteversion = payload['name']
                changelog = payload['body']
                url = payload['assets'][0]['browser_download_url']
                if StrictVersion(
                        remoteversion[1:]) > StrictVersion(__version__):
                    printft(
                        HTML(
                            '<ansigreen>Updating CurseBreaker...</ansigreen>'))
                    if os.path.isfile(sys.executable + '.old'):
                        os.remove(sys.executable + '.old')
                    shutil.move(sys.executable, sys.executable + '.old')
                    payload = requests.get(url)
                    with open(sys.executable, 'wb') as f:
                        f.write(payload.content)
                    printft(
                        HTML(
                            f'<ansibrightgreen>Update complete! Please restart the application.</ansibrightgreen'
                            f'>\n\n<ansigreen>Changelog:</ansigreen>\n{changelog}\n'
                        ))
                    os.system('pause')
                    sys.exit(0)
            except Exception as e:
                printft(
                    HTML(
                        f'<ansibrightred>Update failed!\n\nReason: {str(e)}</ansibrightred>\n'
                    ))
                os.system('pause')
                sys.exit(1)

    def handle_exception(self, e):
        if len(self.table_data) > 1:
            print(self.table.table)
        if getattr(sys, 'frozen', False):
            printft(HTML(f'\n<ansibrightred>{str(e)}</ansibrightred>'))
        else:
            sys.tracebacklimit = 1000
            traceback.print_exc()

    def print_header(self):
        os.system('cls')
        printft(
            HTML(
                f'<ansibrightblack>~~~ <ansibrightgreen>CurseBreaker</ansibrightgreen> <ansibrightred>v'
                f'{__version__}</ansibrightred> ~~~</ansibrightblack>\n'))

    def setup_console(self, buffer=False):
        if getattr(sys, 'frozen', False):
            if buffer:
                windll.kernel32.SetConsoleScreenBufferSize(
                    self.chandle, wintypes._COORD(100, 100))
            else:
                windll.kernel32.SetConsoleWindowInfo(
                    self.chandle, True,
                    byref(wintypes.SMALL_RECT(0, 0, 99, 49)))
                windll.kernel32.SetConsoleScreenBufferSize(
                    self.chandle, wintypes._COORD(100, 50))
        else:
            os.system('mode con: cols=100 lines=50')

    def setup_completer(self):
        commands = [
            'install', 'uninstall', 'update', 'force_update', 'status',
            'orphans', 'search', 'toggle_backup', 'toggle_dev',
            'uri_integration', 'help', 'exit'
        ]
        addons = sorted(self.core.config['Addons'],
                        key=lambda k: k['Name'].lower())
        for addon in addons:
            commands.extend([
                f'uninstall {addon["Name"]}', f'update {addon["Name"]}',
                f'force_update {addon["Name"]}', f'toggle_dev {addon["Name"]}',
                f'status {addon["Name"]}'
            ])
        self.completer = WordCompleter(commands,
                                       ignore_case=True,
                                       sentence=True)

    def setup_table(self):
        self.table_data = [[
            f'{Fore.LIGHTWHITE_EX}Status{Fore.RESET}',
            f'{Fore.LIGHTWHITE_EX}Name{Fore.RESET}',
            f'{Fore.LIGHTWHITE_EX}Version{Fore.RESET}'
        ]]
        self.table = SingleTable(self.table_data)
        self.table.justify_columns[0] = 'center'

    def c_install(self, args):
        if args:
            addons = args.split(',')
            with tqdm(total=len(addons),
                      bar_format='{n_fmt}/{total_fmt} |{bar}|') as pbar:
                for addon in addons:
                    installed, name, version = self.core.add_addon(addon)
                    if installed:
                        self.table_data.append([
                            f'{Fore.GREEN}Installed{Fore.RESET}', name, version
                        ])
                    else:
                        self.table_data.append([
                            f'{Fore.LIGHTBLACK_EX}Already installed{Fore.RESET}',
                            name, version
                        ])
                    pbar.update(1)
            print(self.table.table)
        else:
            printft(
                HTML(
                    '<ansigreen>Usage:</ansigreen>\n\tThis command accepts a comma-separated list of links as an a'
                    'rgument.\n<ansigreen>Supported URLs:</ansigreen>\n\thttps://www.curseforge.com/wow/addons/[ad'
                    'don_name]\n\thttps://www.wowinterface.com/downloads/[addon_name]\n\tElvUI\n\tElvUI:Dev\n\tTuk'
                    'UI'))

    def c_uninstall(self, args):
        if args:
            addons = args.split(',')
            with tqdm(total=len(addons),
                      bar_format='{n_fmt}/{total_fmt} |{bar}|') as pbar:
                for addon in addons:
                    name, version = self.core.del_addon(addon)
                    if name:
                        self.table_data.append([
                            f'{Fore.LIGHTRED_EX}Uninstalled{Fore.RESET}', name,
                            version
                        ])
                    else:
                        self.table_data.append([
                            f'{Fore.LIGHTBLACK_EX}Not installed{Fore.RESET}',
                            addon, ''
                        ])
                    pbar.update(1)
            print(self.table.table)
        else:
            printft(
                HTML(
                    '<ansigreen>Usage:</ansigreen>\n\tThis command accepts a comma-separated list of links or addo'
                    'n names as an argument.\n<ansigreen>Supported URLs:</ansigreen>\n\thttps://www.curseforge.com'
                    '/wow/addons/[addon_name]\n\thttps://www.wowinterface.com/downloads/[addon_name]\n\tElvUI\n\tE'
                    'lvUI:Dev\n\tTukUI'))

    def c_update(self, args, addline=False, update=True, force=False):
        if args:
            addons = args.split(',')
        else:
            addons = sorted(self.core.config['Addons'],
                            key=lambda k: k['Name'].lower())
        with tqdm(total=len(addons),
                  bar_format='{n_fmt}/{total_fmt} |{bar}|') as pbar:
            for addon in addons:
                name, versionnew, versionold, modified = self.core.\
                    update_addon(addon if isinstance(addon, str) else addon['URL'], update, force)
                if versionold:
                    if versionold == versionnew:
                        if modified:
                            self.table_data.append([
                                f'{Fore.LIGHTRED_EX}Modified{Fore.RESET}',
                                name, versionold
                            ])
                        else:
                            self.table_data.append([
                                f'{Fore.GREEN}Up-to-date{Fore.RESET}', name,
                                versionold
                            ])
                    else:
                        if modified:
                            self.table_data.append([
                                f'{Fore.LIGHTRED_EX}Update suppressed{Fore.RESET}',
                                name, versionold
                            ])
                        else:
                            self.table_data.append([
                                f'{Fore.YELLOW}{"Updated" if update else "Update available"}'
                                f'{Fore.RESET}', name,
                                f'{Fore.YELLOW}{versionnew}{Fore.RESET}'
                            ])
                else:
                    self.table_data.append([
                        f'{Fore.LIGHTBLACK_EX}Not installed{Fore.RESET}',
                        addon, ''
                    ])
                pbar.update(1)
        print('\n' + self.table.table if addline else self.table.table)

    def c_force_update(self, args):
        if args:
            self.c_update(args, False, True, True)
        else:
            printft(
                HTML(
                    '<ansigreen>Usage:</ansigreen>\n\tThis command accepts a comma-separated list of links or addo'
                    'n names as an argument.'))

    def c_status(self, args):
        self.c_update(args, False, False)

    def c_orphans(self, _):
        orphansd, orphansf = self.core.find_orphans()
        printft(
            HTML(
                '<ansigreen>Directories that are not part of any installed addon:</ansigreen>'
            ))
        for orphan in sorted(orphansd):
            printft(
                HTML(orphan.replace('[GIT]',
                                    '<ansiyellow>[GIT]</ansiyellow>')))
        printft(
            HTML(
                '\n<ansigreen>Files that are leftovers after no longer installed addons:</ansigreen>'
            ))
        for orphan in sorted(orphansf):
            printft(orphan)

    def c_uri_integration(self, _):
        self.core.create_reg()
        printft('CurseBreaker.reg file was created. Attempting to import...')
        out = os.system('Reg import CurseBreaker.reg')
        if out != 0:
            printft('Import failed. Please try to import REG file manually.')
        else:
            os.remove('CurseBreaker.reg')

    def c_toggle_dev(self, args):
        if args:
            status = self.core.dev_toggle(args)
            if status is None:
                printft(
                    HTML(
                        '<ansibrightred>This addon does not exist or it is not installed yet.</ansibrightred>'
                    ))
            elif status:
                printft('This addon will now prioritize alpha/beta versions.')
            else:
                printft(
                    'This addon will not longer prioritize alpha/beta versions.'
                )
        else:
            printft(
                HTML(
                    '<ansigreen>Usage:</ansigreen>\n\tThis command accepts an addon name as an argument.'
                ))

    def c_toggle_backup(self, _):
        status = self.core.backup_toggle()
        printft(
            'Backup of WTF directory is now:',
            HTML('<ansigreen>ENABLED</ansigreen>')
            if status else HTML('<ansired>DISABLED</ansired>'))

    def c_search(self, args):
        if args:
            results = self.core.search(args)
            printft(HTML('<ansigreen>Top results of your search:</ansigreen>'))
            for url in results:
                if self.core.check_if_installed(url):
                    printft(
                        HTML(f'{url} <ansiyellow>[Installed]</ansiyellow>'))
                else:
                    printft(url)
        else:
            printft(
                HTML(
                    '<ansigreen>Usage:</ansigreen>\n\tThis command accepts a search query as an argument.'
                ))

    def c_help(self, _):
        printft(
            HTML(
                '<ansigreen>install [URL]</ansigreen>\n\tCommand accepts a comma-separated list of links.'
            ))
        printft(
            HTML(
                '<ansigreen>uninstall [URL/Name]</ansigreen>\n\tCommand accepts a comma-separated list of links or'
                ' addon names.'))
        printft(
            HTML(
                '<ansigreen>update [URL/Name]</ansigreen>\n\tCommand accepts a comma-separated list of links or ad'
                'don names.\n\tIf no argument is provided all non-modified addons will be updated.'
            ))
        printft(
            HTML(
                '<ansigreen>force_update [URL/Name]</ansigreen>\n\tCommand accepts a comma-separated list of links'
                ' or addon names.\n\tSelected addons will be reinstalled or updated regardless of their current st'
                'ate.'))
        printft(
            HTML(
                '<ansigreen>status</ansigreen>\n\tPrints the current state of all installed addons.'
            ))
        printft(
            HTML(
                '<ansigreen>orphans</ansigreen>\n\tPrints list of orphaned directories and files.'
            ))
        printft(
            HTML(
                '<ansigreen>search [Keyword]</ansigreen>\n\tExecute addon search on CurseForge.'
            ))
        printft(
            HTML(
                '<ansigreen>toggle_backup</ansigreen>\n\tEnable/disable automatic daily backup of WTF directory.'
            ))
        printft(
            HTML(
                '<ansigreen>toggle_dev</ansigreen>\n\tThis command accepts an addon name as an argument.\n\tPriori'
                'tize alpha/beta versions for the provided addon.'))
        printft(
            HTML(
                '<ansigreen>uri_integration</ansigreen>\n\tEnable integration with CurseForge page. "Install" butt'
                'on will now start this application.'))
        printft(
            HTML(
                '\n<ansibrightgreen>Supported URLs:</ansibrightgreen>\n\thttps://www.curseforge.com/wow/addons/[ad'
                'don_name]\n\thttps://www.wowinterface.com/downloads/[addon_name]\n\tElvUI\n\tElvUI:Dev\n\tTukUI'
            ))

    def c_exit(self, _):
        sys.exit(0)