1# -*- coding: utf-8 -*- 2# Copyright 2017 - 2020 Avram Lubkin, All Rights Reserved 3 4# This Source Code Form is subject to the terms of the Mozilla Public 5# License, v. 2.0. If a copy of the MPL was not distributed with this 6# file, You can obtain one at http://mozilla.org/MPL/2.0/. 7 8""" 9**Enlighten status bar submodule** 10 11Provides StatusBar class 12""" 13 14import time 15 16from enlighten._basecounter import PrintableCounter 17from enlighten._util import (EnlightenWarning, FORMAT_MAP_SUPPORT, format_time, 18 Justify, raise_from_none, warn_best_level) 19 20 21STATUS_FIELDS = {'elapsed', 'fill'} 22 23 24class StatusBar(PrintableCounter): 25 """ 26 Args: 27 enabled(bool): Status (Default: :py:data:`True`) 28 color(str): Color as a string or RGB tuple see :ref:`Status Color <status_color>` 29 fields(dict): Additional fields used for :ref:`formating <status_format>` 30 fill(str): Fill character used in formatting and justifying text (Default: ' ') 31 justify(str): 32 One of :py:attr:`Justify.CENTER`, :py:attr:`Justify.LEFT`, :py:attr:`Justify.RIGHT` 33 leave(True): Leave status bar after closing (Default: :py:data:`True`) 34 min_delta(float): Minimum time, in seconds, between refreshes (Default: 0.1) 35 status_format(str): Status bar format, see :ref:`Format <status_format>` 36 37 Status bar class 38 39 A :py:class:`StatusBar` instance should be created with the :py:meth:`Manager.status_bar` 40 method. 41 42 .. _status_color: 43 44 **Status Color** 45 46 Color works similarly to color on :py:class:`Counter`, except it affects the entire status bar. 47 See :ref:`Series Color <series_color>` for more information. 48 49 .. _status_format: 50 51 **Format** 52 53 There are two ways to populate the status bar, direct and formatted. Direct takes 54 precedence over formatted. 55 56 .. _status_format_direct: 57 58 **Direct Status** 59 60 Direct status is used when arguments are passed to :py:meth:`Manager.status_bar` or 61 :py:meth:`StatusBar.update`. Any arguments are coerced to strings and joined with a space. 62 For example: 63 64 .. code-block:: python 65 66 67 status_bar.update('Hello', 'World!') 68 # Example output: Hello World! 69 70 status_bar.update('Hello World!') 71 # Example output: Hello World! 72 73 count = [1, 2, 3, 4] 74 status_bar.update(*count) 75 # Example output: 1 2 3 4 76 77 .. _status_format_formatted: 78 79 **Formatted Status** 80 81 Formatted status uses the format specified in the ``status_format`` parameter to populate 82 the status bar. 83 84 .. code-block:: python 85 86 'Current Stage: {stage}' 87 88 # Example output 89 'Current Stage: Testing' 90 91 Available fields: 92 93 - elapsed(:py:class:`str`) - Time elapsed since instance was created 94 - fill(:py:class:`str`) - Filled with :py:attr:`fill` until line is width of terminal. 95 May be used multiple times. Minimum width is 3. 96 97 .. note:: 98 99 The status bar is only updated when :py:meth:`StatusBar.update` or 100 :py:meth:`StatusBar.refresh` is called, so fields like ``elapsed`` 101 will need additional calls to appear dynamic. 102 103 User-defined fields: 104 105 Users can define fields in two ways, the ``fields`` parameter and by passing keyword 106 arguments to :py:meth:`Manager.status_bar` or :py:meth:`StatusBar.update` 107 108 The ``fields`` parameter can be used to pass a dictionary of additional 109 user-defined fields. The dictionary values can be updated after initialization to allow 110 for dynamic fields. Any fields that share names with available fields are ignored. 111 112 If fields are passed as keyword arguments to :py:meth:`Manager.status_bar` or 113 :py:meth:`StatusBar.update`, they take precedent over the ``fields`` parameter. 114 115 116 **Instance Attributes** 117 118 .. py:attribute:: elapsed 119 120 :py:class:`float` - Time since start 121 122 .. py:attribute:: enabled 123 124 :py:class:`bool` - Current status 125 126 .. py:attribute:: manager 127 128 :py:class:`Manager` - Manager Instance 129 130 .. py:attribute:: position 131 132 :py:class:`int` - Current position 133 134 """ 135 136 __slots__ = ('fields', '_justify', 'status_format', '_static', '_fields') 137 138 def __init__(self, *args, **kwargs): 139 140 super(StatusBar, self).__init__(keywords=kwargs) 141 142 self.fields = kwargs.pop('fields', {}) 143 self._justify = None 144 self.justify = kwargs.pop('justify', Justify.LEFT) 145 self.status_format = kwargs.pop('status_format', None) 146 self._fields = kwargs 147 self._static = ' '.join(str(arg) for arg in args) if args else None 148 149 @property 150 def justify(self): 151 """ 152 Maps to justify method determined by ``justify`` parameter 153 """ 154 return self._justify 155 156 @justify.setter 157 def justify(self, value): 158 159 if value in (Justify.LEFT, Justify.CENTER, Justify.RIGHT): 160 self._justify = getattr(self.manager.term, value) 161 162 else: 163 raise ValueError("justify must be one of Justify.LEFT, Justify.CENTER, ", 164 "Justify.RIGHT, not: '%r'" % value) 165 166 def format(self, width=None, elapsed=None): 167 """ 168 Args: 169 width (int): Width in columns to make progress bar 170 elapsed(float): Time since started. Automatically determined if :py:data:`None` 171 172 Returns: 173 :py:class:`str`: Formatted status bar 174 175 Format status bar 176 """ 177 178 width = width or self.manager.width 179 justify = self.justify 180 181 # If static message was given, just return it 182 if self._static is not None: 183 rtn = self._static 184 185 # If there is no format, return empty 186 elif self.status_format is None: 187 rtn = '' 188 189 # Generate from format 190 else: 191 fields = self.fields.copy() 192 fields.update(self._fields) 193 194 # Warn on reserved fields 195 reserved_fields = (set(fields) & STATUS_FIELDS) 196 if reserved_fields: 197 warn_best_level('Ignoring reserved fields specified as user-defined fields: %s' % 198 ', '.join(reserved_fields), 199 EnlightenWarning) 200 201 elapsed = elapsed if elapsed is not None else self.elapsed 202 fields['elapsed'] = format_time(elapsed) 203 fields['fill'] = u'{0}' 204 205 # Format 206 try: 207 if FORMAT_MAP_SUPPORT: 208 rtn = self.status_format.format_map(fields) 209 else: # pragma: no cover 210 rtn = self.status_format.format(**fields) 211 except KeyError as e: 212 raise_from_none(ValueError('%r specified in format, but not provided' % e.args[0])) 213 214 rtn = self._fill_text(rtn, width) 215 216 return self._colorize(justify(rtn, width=width, fillchar=self.fill)) 217 218 def update(self, *objects, **fields): # pylint: disable=arguments-differ 219 """ 220 Args: 221 objects(list): Values for :ref:`Direct Status <status_format_direct>` 222 force(bool): Force refresh even if ``min_delta`` has not been reached 223 fields(dict): Fields for for :ref:`Formatted Status <status_format_formatted>` 224 225 Update status and redraw 226 227 Status bar is only redrawn if ``min_delta`` seconds past since the last update 228 """ 229 230 force = fields.pop('force', False) 231 232 self._static = ' '.join(str(obj) for obj in objects) if objects else None 233 self._fields.update(fields) 234 235 if self.enabled: 236 currentTime = time.time() 237 if force or currentTime - self.last_update >= self.min_delta: 238 self.refresh(elapsed=currentTime - self.start) 239