class Progress(object): """ Handle progress indication using callbacks. This class will create an object that stores information about a running yumsync process. It stores information about each repository being synced, including total packages, completed packages, and the status of the repository metadata. This makes it possible to display aggregated status of multiple repositories during a sync. """ repos = {} totals = { 'numpkgs': 0, 'dlpkgs': 0, 'md_complete': 0, 'md_total': 0, 'errors': 0 } errors = [] def __init__(self): """ records the time the sync started. and initialise blessings terminal """ self.start = datetime.datetime.now() self.linecount = 0 if sys.stdout.isatty(): self.term = Terminal() sys.stdout.write(self.term.clear()) def __del__(self): """ destructor - need to reset the terminal .""" if sys.stdout.isatty(): sys.stdout.write(self.term.normal) sys.stdout.write(self.term.move(self.linecount, 0)) sys.stdout.flush() def update(self, repo_id, set_total=None, pkgs_downloaded=None, pkg_exists=None, repo_metadata=None, repo_error=None): """ Handles updating the object itself. This method will be called any time the number of packages in a repository becomes known, when any package finishes downloading, when repository metadata begins indexing and when it completes. """ if not repo_id in self.repos: self.repos[repo_id] = {'numpkgs': 0, 'dlpkgs': 0, 'repomd': ''} self.totals['md_total'] += 1 if set_total: self.repos[repo_id]['numpkgs'] = set_total self.totals['numpkgs'] = 0 for _, repo in six.iteritems(self.repos): self.totals['numpkgs'] += repo['numpkgs'] if pkgs_downloaded: self.repos[repo_id]['dlpkgs'] += pkgs_downloaded self.totals['dlpkgs'] += pkgs_downloaded if repo_metadata: self.repos[repo_id]['repomd'] = repo_metadata if repo_metadata == 'complete': self.totals['md_complete'] += 1 if repo_error: self.totals['errors'] += 1 if self.repos[repo_id]['repomd'] != 'complete': self.totals['md_total'] -= 1 self.errors.append((repo_id, repo_error)) if sys.stdout.isatty(): self.formatted() def color(self, string, color=None): if color and hasattr(self.term, color): return '{}{}{}'.format(getattr(self.term, color), string, self.term.normal) return string @classmethod def pct(cls, current, total): """ Calculate a percentage. """ if total == 0: return "0" val = current / float(total) * 100 formatted = '{:0.1f}%'.format(val) return formatted def elapsed(self): """ Calculate and return elapsed time. This function does dumb rounding by just plucking off anything past a dot "." in a time delta between two datetime.datetime()'s. """ return str(datetime.datetime.now() - self.start).split('.')[0] def format_header(self): repos = self.repos.keys() max_repo = len(max(repos, key=len)) repo = '{:<{}s}'.format('Repository', max_repo) done = '{:>5s}'.format('Done') total = '{:>5s}'.format('Total') complete = 'Packages' metadata = 'Metadata' header_str = '{} {}/{} {} {}'.format(repo, done, total, complete, metadata) return header_str, len(repo), len(done), len(total), len( complete), len(metadata) @classmethod def format_line(cls, reponame, package_counts, percent, repomd): """ Return a string formatted for output. Since there is a common column layout in the progress indicator, we can we can implement the printf-style formatter in a function. """ return '{} {} {} {}'.format(reponame, package_counts, percent, repomd) def represent_repo_pkgs(self, repo_id, a, b): """ Format the ratio of packages in a repository. """ numpkgs = self.repos[repo_id]['numpkgs'] dlpkgs = self.repos[repo_id]['dlpkgs'] return self.represent_pkgs(dlpkgs, numpkgs, a, b) def represent_total_pkgs(self, a, b): """ Format the total number of packages in all repositories. """ numpkgs = self.totals['numpkgs'] dlpkgs = self.totals['dlpkgs'] return self.represent_pkgs(dlpkgs, numpkgs, a, b) @classmethod def represent_pkgs(cls, dlpkgs, numpkgs, a, b): """ Represent a package ratio. This will display nothing if the number of packages is 0 or unknown, or typical done/total if total is > 0. """ if numpkgs == 0: return '{:^{}s}'.format('-', a + b + 1) else: return '{0:>{2}}/{1:<{3}}'.format(dlpkgs, numpkgs, a, b) def represent_repo_percent(self, repo_id, length): """ Display the percentage of packages downloaded in a repository. """ numpkgs = self.repos[repo_id]['numpkgs'] dlpkgs = self.repos[repo_id]['dlpkgs'] return self.represent_percent(dlpkgs, numpkgs, length) def represent_total_percent(self, length): """ Display the overall percentage of downloaded packages. """ numpkgs = self.totals['numpkgs'] dlpkgs = self.totals['dlpkgs'] return self.represent_percent(dlpkgs, numpkgs, length) def represent_total_metadata_percent(self, length): """ Display the overall percentage of metadata completion. """ a = self.totals['md_total'] b = self.totals['md_complete'] return self.represent_percent(b, a, length) def represent_percent(self, dlpkgs, numpkgs, length): """ Display a percentage of completion. If the number of packages is unknown, nothing is displayed. Otherwise, a number followed by the percent sign is displayed. """ if dlpkgs == 0: return '{:^{}s}'.format('-', length) else: return '{:^{}s}'.format(self.pct(dlpkgs, numpkgs), length) def represent_repomd(self, repo_id, length): """ Display the current status of repository metadata. """ if not self.repos[repo_id]['repomd']: return '{:^{}s}'.format('-', length) else: return self.repos[repo_id]['repomd'] def represent_repo(self, repo_id, h1, h2, h3, h4, h5): """ Represent an entire repository in one line. This makes calls to the other methods of this class to create a formatted string, which makes nice columns. """ repo = '{:<{}s}'.format(repo_id, h1) if 'error' in self.repos[repo_id]: repo = self.color(repo, 'red') else: repo = self.color(repo, 'blue') packages = self.represent_repo_pkgs(repo_id, h2, h3) percent = self.represent_repo_percent(repo_id, h4) metadata = self.represent_repomd(repo_id, h5) if percent == 'complete': percent = self.color(percent, 'green') if metadata == 'building' or ( (isinstance(metadata, int) or metadata.isdigit()) and int(metadata) <= 100): if isinstance(metadata, str) and metadata.isdigit(): metadata += "%" if isinstance(metadata, int): metadata = "{}%".format(metadata) metadata = self.color(metadata, 'yellow') elif metadata == 'complete': metadata = self.color(metadata, 'green') return self.format_line(repo, packages, percent, metadata) def represent_total(self, h1, h2, h3, h4, h5): total = self.color('{:>{}s}'.format('Total', h1), 'yellow') packages = self.represent_total_pkgs(h2, h3) percent = self.represent_total_percent(h4) metadata = self.represent_total_metadata_percent(h5) if percent == 'complete': percent = self.color(percent, 'green') if metadata == 'complete': metadata = self.color(metadata, 'green') return self.format_line(total, packages, percent, metadata) def emit(self, line=''): numlines = len(line.split('\n')) self.linecount += numlines with self.term.location(x=0, y=self.linecount - numlines): sys.stdout.write(line) sys.stdout.write(self.term.clear_eol()) def formatted(self): """ Print all known progress data in a nicely formatted table. This method keeps track of what it has printed before, so that it can backtrack over the console screen, clearing out the previous flush and printing out a new one. This method is called any time any value is updated, which is what gives us that real-time feeling. Unfortunately, the YUM library calls print directly rather than just throwing exceptions and handling them in the presentation layer, so this means that yumsync's output will be slightly flawed if YUM prints something directly to the screen from a worker process. """ # Remove repos with errors from totals if self.totals['errors'] > 0: for repo_id, error in self.errors: if repo_id in self.repos: if not 'error' in self.repos[repo_id]: self.totals['dlpkgs'] -= self.repos[repo_id]['dlpkgs'] self.totals['numpkgs'] -= self.repos[repo_id][ 'numpkgs'] self.repos[repo_id]['error'] = True self.linecount = 0 # reset line counter header, h1, h2, h3, h4, h5 = self.format_header() self.emit('-' * len(header)) self.emit(self.color('{}'.format(header), 'green')) self.emit('-' * len(header)) error_repos = [] complete_repos = [] metadata_repos = [] other_repos = [] for repo_id in sorted(self.repos): if 'error' in self.repos[repo_id]: error_repos.append(repo_id) elif self.repos[repo_id]['repomd'] == 'complete': complete_repos.append(repo_id) elif self.repos[repo_id]['repomd']: metadata_repos.append(repo_id) else: other_repos.append(repo_id) for repo_id in sorted(self.repos): self.emit(self.represent_repo(repo_id, h1, h2, h3, h4, h5)) self.emit('-' * len(header)) self.emit(self.represent_total(h1, h2, h3, h4, h5)) self.emit('-' * len(header)) # Append errors to output if any found. if self.totals['errors'] > 0: self.emit( self.color('Errors ({}):'.format(self.totals['errors']), 'red')) for repo_id, error in self.errors: self.emit(self.color('{}: {}'.format(repo_id, error), 'red')) with self.term.location(x=0, y=self.linecount): sys.stdout.write(self.term.clear_eos()) sys.stdout.flush()
class ConsoleRender(object): def __init__(self, event_generator=None, theme=None, *args, **kwargs): super(ConsoleRender, self).__init__(*args, **kwargs) self._event_gen = event_generator or events.KeyEventGenerator() self.terminal = Terminal() self._previous_error = None self._position = 0 self._theme = theme or themes.Default() def render(self, question, answers=None): question.answers = answers or {} if question.ignore: return question.default clazz = self.render_factory(question.kind) render = clazz(question, terminal=self.terminal, theme=self._theme, show_default=question.show_default) self.clear_eos() try: return self._event_loop(render) finally: print('') def _event_loop(self, render): try: while True: self._relocate() self._print_status_bar(render) self._print_header(render) self._print_options(render) self._process_input(render) self._force_initial_column() except errors.EndOfInput as e: self._go_to_end(render) return e.selection def _print_status_bar(self, render): if self._previous_error is None: self.clear_bottombar() return self.render_error(self._previous_error) self._previous_error = None def _print_options(self, render): for message, symbol, color in render.get_options(): if hasattr(message, 'decode'): # python 2 message = message.decode('utf-8') self.print_line(' {color}{s} {m}{t.normal}', m=message, color=color, s=symbol) def _print_header(self, render): base = render.get_header() header = (base[:self.width - 9] + '...' if len(base) > self.width - 6 else base) default_value = ' ({color}{default}{normal})'.format( default=render.question.default, color=self._theme.Question.default_color, normal=self.terminal.normal) show_default = render.question.default and render.show_default header += default_value if show_default else '' msg_template = "{t.move_up}{t.clear_eol}{tq.brackets_color}["\ "{tq.mark_color}?{tq.brackets_color}]{t.normal} {msg}" # ensure any user input with { or } will not cause a formatting error escaped_current_value = render.get_current_value().replace( '{', '{{').replace('}', '}}') self.print_str('\n%s: %s' % (msg_template, escaped_current_value), msg=header, lf=not render.title_inline, tq=self._theme.Question) def _process_input(self, render): try: ev = self._event_gen.next() if isinstance(ev, events.KeyPressed): render.process_input(ev.value) except errors.EndOfInput as e: try: render.question.validate(e.selection) raise except errors.ValidationError as e: self._previous_error = ('"{e}" is not a valid {q}.'.format( e=e.value, q=render.question.name)) def _relocate(self): print(self._position * self.terminal.move_up, end='') self._force_initial_column() self._position = 0 def _go_to_end(self, render): positions = len(list(render.get_options())) - self._position if positions > 0: print(self._position * self.terminal.move_down, end='') self._position = 0 def _force_initial_column(self): self.print_str('\r') def render_error(self, message): if message: symbol = '>> ' size = len(symbol) + 1 length = len(message) message = message.rstrip() message = (message if length + size < self.width else message[:self.width - (size + 3)] + '...') self.render_in_bottombar( '{t.red}{s}{t.normal}{t.bold}{msg}{t.normal} '.format( msg=message, s=symbol, t=self.terminal)) def render_in_bottombar(self, message): with self.terminal.location(0, self.height - 2): self.clear_eos() self.print_str(message) def clear_bottombar(self): with self.terminal.location(0, self.height - 2): self.clear_eos() def render_factory(self, question_type): matrix = { 'text': Text, 'password': Password, 'confirm': Confirm, 'list': List, 'checkbox': Checkbox, 'path': Path, } if question_type not in matrix: raise errors.UnknownQuestionTypeError() return matrix.get(question_type) def print_line(self, base, lf=True, **kwargs): self.print_str(base + self.terminal.clear_eol(), lf=lf, **kwargs) def print_str(self, base, lf=False, **kwargs): if lf: self._position += 1 print(base.format(t=self.terminal, **kwargs), end='\n' if lf else '') sys.stdout.flush() def clear_eos(self): print(self.terminal.clear_eos(), end='') @property def width(self): return self.terminal.width or 80 @property def height(self): return self.terminal.width or 24
elif root and cmd.split(" ")[0] == "exit": task = input("Do you really want to leave root? (YES/No) ") if task != "No" or task != "N" or task != "n" or task != "no": root = False elif not root and cmd.split( " ")[0] == "exit" or not root and cmd.split(" ")[0] == "quit": quit_task = input( wrap.bold( "This action will cause the program to self destruct. " + "(YES/No) ")) if quit_task != "No": time_count = 3000 print(wrap.clear()) with wrap.location(0, 0): print(wrap.bold("Make a wish!")) for i in range(time_count, 0, -1): with wrap.location(0, wrap.height - 2): min = int(str(i / 1000).split(".")[0]) mills = int(str(i / 1000).split(".")[1]) print( wrap.bold_red( 'Remained %d.%d seconds before destruction. ' % (min, mills))) time.sleep(0.01) with wrap.location(0, wrap.height - 2): print(wrap.clear_eos()) with wrap.location(0, wrap.height - 2): print(wrap.bold_red_blink("Time's up!")) time.sleep(1) run = False
class ConsoleRender(object): def __init__(self, event_generator=None, theme=None, *args, **kwargs): super(ConsoleRender, self).__init__(*args, **kwargs) self._event_gen = event_generator or events.KeyEventGenerator() self.terminal = Terminal() self._previous_error = None self._position = 0 self._theme = theme or themes.BasicTheme() def render(self, question, answers=None): question.answers = answers or {} if question.ignore: return question.default clazz = self.render_factory(question.kind) # TODO pass theme class render = clazz(question, terminal=self.terminal, theme=self._theme) self.clear_eos() try: return self._event_loop(render) finally: print('') def _event_loop(self, render): try: while True: self._relocate() self._print_status_bar(render) self._print_header(render) self._print_options(render) self._process_input(render) self._force_initial_column() except errors.EndOfInput as e: self._go_to_end(render) return e.selection def _print_status_bar(self, render): if self._previous_error is None: self.clear_bottombar() return self.render_error(self._previous_error) self._previous_error = None def _print_options(self, render): for message, symbol, color in render.get_options(): self.print_line(' {color}{s} {m}{t.normal}', m=message, color=color, s=symbol) def _print_header(self, render): base = render.get_header() header = (base[:self.width - 9] + '...' if len(base) > self.width - 6 else base) header += ' ({c})'.format(c=render.question.default) msg_template = '{t.move_up}{t.clear_eol}%(msg_prefix)s{t.normal} {msg}%(msg_postfix)s{t.normal}' % { 'msg_prefix': self._theme.question_message_prefix, 'msg_postfix': self._theme.question_message_postfix } self.print_str('\n%s %s' % (msg_template, render.current), msg=header, lf=not render.title_inline, theme=self._theme) def _process_input(self, render): try: ev = self._event_gen.next() if isinstance(ev, events.KeyPressed): render.process_input(ev.value) except errors.EndOfInput as e: try: render.question.validate(e.selection) raise except errors.ValidationError as e: self._previous_error = ('"{e}" is not a valid {q}.'.format( e=e.value, q=render.question.name)) def _relocate(self): print(self._position * self.terminal.move_up, end='') self._force_initial_column() self._position = 0 def _go_to_end(self, render): positions = len(list(render.get_options())) - self._position if positions > 0: print(self._position * self.terminal.move_down, end='') self._position = 0 def _force_initial_column(self): self.print_str('\r') def render_error(self, message): if message: symbol = '>> ' size = len(symbol) + 1 length = len(message) message = message.rstrip() message = (message if length + size < self.width else message[:self.width - (size + 3)] + '...') self.render_in_bottombar( '{t.red}{s}{t.normal}{t.bold}{msg}{t.normal} '.format( msg=message, s=symbol, t=self.terminal)) def render_in_bottombar(self, message): with self.terminal.location(0, self.height - 2): self.clear_eos() self.print_str(message) def clear_bottombar(self): with self.terminal.location(0, self.height - 2): self.clear_eos() def render_factory(self, question_type): matrix = { 'text': Text, 'password': Password, 'confirm': Confirm, 'list': List, 'checkbox': Checkbox, } if question_type not in matrix: raise errors.UnknownQuestionTypeError() return matrix.get(question_type) def print_line(self, base, lf=True, **kwargs): self.print_str(base + self.terminal.clear_eol(), lf=lf, **kwargs) def print_str(self, base, lf=False, **kwargs): if lf: self._position += 1 print(base.format(t=self.terminal, **kwargs), end='\n' if lf else '') sys.stdout.flush() def clear_eos(self): print(self.terminal.clear_eos(), end='') @property def width(self): return self.terminal.width or 80 @property def height(self): return self.terminal.width or 24
class ConsoleRender(object): def __init__(self, event_generator=None, *args, **kwargs): super(ConsoleRender, self).__init__(*args, **kwargs) self._event_gen = event_generator or events.KeyEventGenerator() self.terminal = Terminal() self._previous_error = None self._position = 0 def render(self, question, answers=None): question.answers = answers or {} if question.ignore: return question.default clazz = self.render_factory(question.kind) render = clazz(question, self.terminal) self.clear_eos() try: return self._event_loop(render) finally: print('') def _event_loop(self, render): try: while True: self._relocate() self._print_status_bar(render) self._print_header(render) self._print_options(render) self._process_input(render) self._force_initial_column() except errors.EndOfInput as e: self._go_to_end(render) return e.selection def _print_status_bar(self, render): if self._previous_error is None: self.clear_bottombar() return self.render_error(self._previous_error) self._previous_error = None def _print_options(self, render): for message, symbol, color in render.get_options(): self.print_line(' {color}{s} {m}{t.normal}', m=message, color=color, s=symbol) def _print_header(self, render): base = render.get_header() header = (base[:self.width - 9] + '...' if len(base) > self.width - 6 else base) header += ': {c}'.format(c=render.get_current_value()) self.print_str( '\n{t.move_up}{t.clear_eol}[{t.yellow}?{t.normal}] {msg}', msg=header, lf=not render.title_inline) def _process_input(self, render): try: ev = self._event_gen.next() if isinstance(ev, events.KeyPressed): render.process_input(ev.value) except errors.EndOfInput as e: try: render.question.validate(e.selection) raise except errors.ValidationError as e: self._previous_error = ('"{e}" is not a valid {q}.' .format(e=e.value, q=render.question.name)) def _relocate(self): print(self._position * self.terminal.move_up, end='') self._force_initial_column() self._position = 0 def _go_to_end(self, render): positions = len(list(render.get_options())) - self._position if positions > 0: print(self._position * self.terminal.move_down, end='') self._position = 0 def _force_initial_column(self): self.print_str('\r') def render_error(self, message): if message: symbol = '>> ' size = len(symbol) + 1 length = len(message) message = message.rstrip() message = (message if length + size < self.width else message[:self.width - (size + 3)] + '...') self.render_in_bottombar( '{t.red}{s}{t.normal}{t.bold}{msg}{t.normal} ' .format(msg=message, s=symbol, t=self.terminal) ) def render_in_bottombar(self, message): with self.terminal.location(0, self.height - 2): self.clear_eos() self.print_str(message) def clear_bottombar(self): with self.terminal.location(0, self.height - 2): self.clear_eos() def render_factory(self, question_type): matrix = { 'text': Text, 'password': Password, 'confirm': Confirm, 'list': List, 'checkbox': Checkbox, } if question_type not in matrix: raise errors.UnknownQuestionTypeError() return matrix.get(question_type) def print_line(self, base, lf=True, **kwargs): self.print_str(base + self.terminal.clear_eol(), lf=lf, **kwargs) def print_str(self, base, lf=False, **kwargs): if lf: self._position += 1 print(base.format(t=self.terminal, **kwargs), end='\n' if lf else '') sys.stdout.flush() def clear_eos(self): print(self.terminal.clear_eos(), end='') @property def width(self): return self.terminal.width or 80 @property def height(self): return self.terminal.width or 24
class Progress(object): """ Handle progress indication using callbacks. This class will create an object that stores information about a running yumsync process. It stores information about each repository being synced, including total packages, completed packages, and the status of the repository metadata. This makes it possible to display aggregated status of multiple repositories during a sync. """ repos = {} totals = { 'numpkgs': 0, 'dlpkgs': 0, 'md_complete': 0, 'md_total': 0, 'errors':0 } errors = [] def __init__(self): """ records the time the sync started. and initialise blessings terminal """ self.start = datetime.datetime.now() self.linecount = 0 if sys.stdout.isatty(): self.term = Terminal() sys.stdout.write(self.term.clear()) def __del__(self): """ destructor - need to reset the terminal .""" if sys.stdout.isatty(): sys.stdout.write(self.term.normal) sys.stdout.write(self.term.move(self.linecount, 0)) sys.stdout.flush() def update(self, repo_id, set_total=None, pkgs_downloaded=None, pkg_exists=None, repo_metadata=None, repo_error=None): """ Handles updating the object itself. This method will be called any time the number of packages in a repository becomes known, when any package finishes downloading, when repository metadata begins indexing and when it completes. """ if not repo_id in self.repos: self.repos[repo_id] = {'numpkgs':0, 'dlpkgs':0, 'repomd':''} self.totals['md_total'] += 1 if set_total: self.repos[repo_id]['numpkgs'] = set_total self.totals['numpkgs'] += set_total if pkgs_downloaded: self.repos[repo_id]['dlpkgs'] += pkgs_downloaded self.totals['dlpkgs'] += pkgs_downloaded if repo_metadata: self.repos[repo_id]['repomd'] = repo_metadata if repo_metadata == 'complete': self.totals['md_complete'] += 1 if repo_error: self.totals['errors'] += 1 if self.repos[repo_id]['repomd'] != 'complete': self.totals['md_total'] -= 1 self.errors.append((repo_id, repo_error)) if sys.stdout.isatty(): self.formatted() def color(self, string, color=None): if color and hasattr(self.term, color): return '{}{}{}'.format(getattr(self.term, color), string, self.term.normal) return string def pct(self, current, total): """ Calculate a percentage. """ val = current / float(total) * 100 formatted = '{:0.1f}%'.format(val) return formatted if int(val) < 100 else 'complete' def elapsed(self): """ Calculate and return elapsed time. This function does dumb rounding by just plucking off anything past a dot "." in a time delta between two datetime.datetime()'s. """ return str(datetime.datetime.now() - self.start).split('.')[0] def format_header(self, repos): max_repo = len(max(repos, key=len)) repo = '{:<{}s}'.format('Repository', max_repo) done = '{:>5s}'.format('Done') total = '{:>5s}'.format('Total') complete = 'Packages' metadata = 'Metadata' header_str = '{} {}/{} {} {}'.format(repo, done, total, complete, metadata) return header_str, len(repo), len(done), len(total), len(complete), len(metadata) def format_line(self, reponame, package_counts, percent, repomd): """ Return a string formatted for output. Since there is a common column layout in the progress indicator, we can we can implement the printf-style formatter in a function. """ return '{} {} {} {}'.format(reponame, package_counts, percent, repomd) def represent_repo_pkgs(self, repo_id, a, b): """ Format the ratio of packages in a repository. """ numpkgs = self.repos[repo_id]['numpkgs'] dlpkgs = self.repos[repo_id]['dlpkgs'] return self.represent_pkgs(dlpkgs, numpkgs, a, b) def represent_total_pkgs(self, a, b): """ Format the total number of packages in all repositories. """ numpkgs = self.totals['numpkgs'] dlpkgs = self.totals['dlpkgs'] return self.represent_pkgs(dlpkgs, numpkgs, a, b) def represent_pkgs(self, dlpkgs, numpkgs, a, b): """ Represent a package ratio. This will display nothing if the number of packages is 0 or unknown, or typical done/total if total is > 0. """ if numpkgs == 0: return '{:^{}s}'.format('-', a + b + 1) elif dlpkgs >= numpkgs: return '{:>{}}'.format(dlpkgs, a + b + 1) else: return '{0:>{2}}/{1:<{3}}'.format(dlpkgs, numpkgs, a, b) def represent_repo_percent(self, repo_id, length): """ Display the percentage of packages downloaded in a repository. """ numpkgs = self.repos[repo_id]['numpkgs'] dlpkgs = self.repos[repo_id]['dlpkgs'] return self.represent_percent(dlpkgs, numpkgs, length) def represent_total_percent(self, length): """ Display the overall percentage of downloaded packages. """ numpkgs = self.totals['numpkgs'] dlpkgs = self.totals['dlpkgs'] return self.represent_percent(dlpkgs, numpkgs, length) def represent_total_metadata_percent(self, length): """ Display the overall percentage of metadata completion. """ a = self.totals['md_total'] b = self.totals['md_complete'] return self.represent_percent(b, a, length) def represent_percent(self, dlpkgs, numpkgs, length): """ Display a percentage of completion. If the number of packages is unknown, nothing is displayed. Otherwise, a number followed by the percent sign is displayed. """ if dlpkgs == 0: return '{:^{}s}'.format('-', length) else: return '{:^{}s}'.format(self.pct(dlpkgs, numpkgs), length) def represent_repomd(self, repo_id, length): """ Display the current status of repository metadata. """ if not self.repos[repo_id]['repomd']: return '{:^{}s}'.format('-', length) else: return self.repos[repo_id]['repomd'] def represent_repo(self, repo_id, h1, h2, h3, h4, h5): """ Represent an entire repository in one line. This makes calls to the other methods of this class to create a formatted string, which makes nice columns. """ repo = '{:<{}s}'.format(repo_id, h1) if 'error' in self.repos[repo_id]: repo = self.color(repo, 'red') packages = self.color('{:^{}s}'.format('error', h2 + h3 + 1), 'red') percent = self.color('{:^{}s}'.format('-', h4), 'red') metadata = self.color('{:^{}s}'.format('-', h5), 'red') else: repo = self.color(repo, 'blue') packages = self.represent_repo_pkgs(repo_id, h2, h3) percent = self.represent_repo_percent(repo_id, h4) metadata = self.represent_repomd(repo_id, h5) if percent == 'complete': percent = self.color(percent, 'green') if metadata == 'building': metadata = self.color(metadata, 'yellow') elif metadata == 'complete': metadata = self.color(metadata, 'green') return self.format_line(repo, packages, percent, metadata) def represent_total(self, h1, h2, h3, h4, h5): total = self.color('{:>{}s}'.format('Total', h1), 'yellow') packages = self.represent_total_pkgs(h2, h3) percent = self.represent_total_percent(h4) metadata = self.represent_total_metadata_percent(h5) if percent == 'complete': percent = self.color(percent, 'green') if metadata == 'complete': metadata = self.color(metadata, 'green') return self.format_line(total, packages, percent, metadata) def emit(self, line=''): numlines = len(line.split('\n')) self.linecount += numlines with self.term.location(x=0, y=self.linecount - numlines): sys.stdout.write(line) sys.stdout.write(self.term.clear_eol()) def formatted(self): """ Print all known progress data in a nicely formatted table. This method keeps track of what it has printed before, so that it can backtrack over the console screen, clearing out the previous flush and printing out a new one. This method is called any time any value is updated, which is what gives us that real-time feeling. Unfortunately, the YUM library calls print directly rather than just throwing exceptions and handling them in the presentation layer, so this means that yumsync's output will be slightly flawed if YUM prints something directly to the screen from a worker process. """ # Remove repos with errors from totals if self.totals['errors'] > 0: for repo_id, error in self.errors: if repo_id in self.repos: if not 'error' in self.repos[repo_id]: self.totals['dlpkgs'] -= self.repos[repo_id]['dlpkgs'] self.totals['numpkgs'] -= self.repos[repo_id]['numpkgs'] self.repos[repo_id]['error'] = True self.linecount = 0 # reset line counter header, h1, h2, h3, h4, h5 = self.format_header(self.repos.keys()) self.emit('-' * len(header)) self.emit(self.color('{}'.format(header), 'green')) self.emit('-' * len(header)) error_repos = [] complete_repos = [] metadata_repos = [] other_repos = [] for repo_id in sorted(self.repos): if 'error' in self.repos[repo_id]: error_repos.append(repo_id) elif self.repos[repo_id]['repomd'] == 'complete': complete_repos.append(repo_id) elif self.repos[repo_id]['repomd']: metadata_repos.append(repo_id) else: other_repos.append(repo_id) for repo_id in itertools.chain(error_repos, complete_repos, metadata_repos, other_repos): self.emit(self.represent_repo(repo_id, h1, h2, h3, h4, h5)) self.emit('-' * len(header)) self.emit(self.represent_total(h1, h2, h3, h4, h5)) self.emit('-' * len(header)) # Append errors to output if any found. if self.totals['errors'] > 0: self.emit(self.color('Errors ({}):'.format(self.totals['errors']), 'red')) for repo_id, error in self.errors: self.emit(self.color('{}: {}'.format(repo_id, error), 'red')) with self.term.location(x=0, y=self.linecount): sys.stdout.write(self.term.clear_eos()) sys.stdout.flush()
class ConsoleRender(object): def __init__(self, event_generator=None, theme=None, *args, **kwargs): super(ConsoleRender, self).__init__(*args, **kwargs) self._event_gen = event_generator or events.KeyEventGenerator() self.terminal = Terminal() self._previous_error = None self._position = 0 self._theme = theme or themes.Default() def render(self, question, answers=None): question.answers = answers or {} if question.ignore: return question.default clazz = self.render_factory(question.kind) render = clazz(question, terminal=self.terminal, theme=self._theme, show_default=question.show_default) self.clear_eos() try: return self._event_loop(render) finally: print('') def _event_loop(self, render): try: while True: self._relocate() self._print_status_bar(render) self._print_header(render) self._print_options(render) self._process_input(render) self._force_initial_column() except errors.EndOfInput as e: self._go_to_end(render) return e.selection def _print_status_bar(self, render): if self._previous_error is None: self.clear_bottombar() return self.render_error(self._previous_error) self._previous_error = None def _print_options(self, render): for message, symbol, color in render.get_options(): if hasattr(message, 'decode'): # python 2 message = message.decode('utf-8') self.print_line(' {color}{s} {m}{t.normal}', m=message, color=color, s=symbol) def _print_header(self, render): base = render.get_header() header = (base[:self.width - 9] + '...' if len(base) > self.width - 6 else base) default_value = ' ({color}{default}{normal})'.format( default=render.question.default, color=self._theme.Question.default_color, normal=self.terminal.normal) show_default = render.question.default and render.show_default header += default_value if show_default else '' msg_template = "{t.move_up}{t.clear_eol}{tq.brackets_color}["\ "{tq.mark_color}?{tq.brackets_color}]{t.normal} {msg}" # ensure any user input with { or } will not cause a formatting error escaped_current_value = ( str(render.get_current_value()) .replace('{', '{{') .replace('}', '}}') ) self.print_str( '\n%s: %s' % (msg_template, escaped_current_value), msg=header, lf=not render.title_inline, tq=self._theme.Question) def _process_input(self, render): try: ev = self._event_gen.next() if isinstance(ev, events.KeyPressed): render.process_input(ev.value) except errors.ValidationError as e: self._previous_error = e.value except errors.EndOfInput as e: try: render.question.validate(e.selection) raise except errors.ValidationError as e: self._previous_error = render.handle_validation_error(e) def _relocate(self): print(self._position * self.terminal.move_up, end='') self._force_initial_column() self._position = 0 def _go_to_end(self, render): positions = len(list(render.get_options())) - self._position if positions > 0: print(self._position * self.terminal.move_down, end='') self._position = 0 def _force_initial_column(self): self.print_str('\r') def render_error(self, message): if message: symbol = '>> ' size = len(symbol) + 1 length = len(message) message = message.rstrip() message = (message if length + size < self.width else message[:self.width - (size + 3)] + '...') self.render_in_bottombar( '{t.red}{s}{t.normal}{t.bold}{msg}{t.normal} ' .format(msg=message, s=symbol, t=self.terminal) ) def render_in_bottombar(self, message): with self.terminal.location(0, self.height - 2): self.clear_eos() self.print_str(message) def clear_bottombar(self): with self.terminal.location(0, self.height - 2): self.clear_eos() def render_factory(self, question_type): matrix = { 'text': Text, 'editor': Editor, 'password': Password, 'confirm': Confirm, 'list': List, 'checkbox': Checkbox, 'path': Path, } if question_type not in matrix: raise errors.UnknownQuestionTypeError() return matrix.get(question_type) def print_line(self, base, lf=True, **kwargs): self.print_str(base + self.terminal.clear_eol(), lf=lf, **kwargs) def print_str(self, base, lf=False, **kwargs): if lf: self._position += 1 print(base.format(t=self.terminal, **kwargs), end='\n' if lf else '') sys.stdout.flush() def clear_eos(self): print(self.terminal.clear_eos(), end='') @property def width(self): return self.terminal.width or 80 @property def height(self): return self.terminal.width or 24