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