/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/ui/text.py

  • Committer: Jelmer Vernooij
  • Date: 2020-02-07 02:14:30 UTC
  • mto: This revision was merged to the branch mainline in revision 7492.
  • Revision ID: jelmer@jelmer.uk-20200207021430-m49iq3x4x8xlib6x
Drop python2 support.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005-2011 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
"""Text UI, write output to the console."""
 
18
 
 
19
from __future__ import absolute_import
 
20
 
 
21
import codecs
 
22
import io
 
23
import os
 
24
import sys
 
25
import warnings
 
26
 
 
27
from ..lazy_import import lazy_import
 
28
lazy_import(globals(), """
 
29
import getpass
 
30
import time
 
31
 
 
32
from breezy import (
 
33
    debug,
 
34
    progress,
 
35
    )
 
36
""")
 
37
 
 
38
from .. import (
 
39
    config,
 
40
    osutils,
 
41
    trace,
 
42
    )
 
43
from . import (
 
44
    NullProgressView,
 
45
    UIFactory,
 
46
    )
 
47
 
 
48
 
 
49
class _ChooseUI(object):
 
50
 
 
51
    """ Helper class for choose implementation.
 
52
    """
 
53
 
 
54
    def __init__(self, ui, msg, choices, default):
 
55
        self.ui = ui
 
56
        self._setup_mode()
 
57
        self._build_alternatives(msg, choices, default)
 
58
 
 
59
    def _setup_mode(self):
 
60
        """Setup input mode (line-based, char-based) and echo-back.
 
61
 
 
62
        Line-based input is used if the BRZ_TEXTUI_INPUT environment
 
63
        variable is set to 'line-based', or if there is no controlling
 
64
        terminal.
 
65
        """
 
66
        is_tty = self.ui.raw_stdin.isatty()
 
67
        if (os.environ.get('BRZ_TEXTUI_INPUT') != 'line-based' and
 
68
                self.ui.raw_stdin == _unwrap_stream(sys.stdin) and is_tty):
 
69
            self.line_based = False
 
70
            self.echo_back = True
 
71
        else:
 
72
            self.line_based = True
 
73
            self.echo_back = not is_tty
 
74
 
 
75
    def _build_alternatives(self, msg, choices, default):
 
76
        """Parse choices string.
 
77
 
 
78
        Setup final prompt and the lists of choices and associated
 
79
        shortcuts.
 
80
        """
 
81
        index = 0
 
82
        help_list = []
 
83
        self.alternatives = {}
 
84
        choices = choices.split('\n')
 
85
        if default is not None and default not in range(0, len(choices)):
 
86
            raise ValueError("invalid default index")
 
87
        for c in choices:
 
88
            name = c.replace('&', '').lower()
 
89
            choice = (name, index)
 
90
            if name in self.alternatives:
 
91
                raise ValueError("duplicated choice: %s" % name)
 
92
            self.alternatives[name] = choice
 
93
            shortcut = c.find('&')
 
94
            if -1 != shortcut and (shortcut + 1) < len(c):
 
95
                help = c[:shortcut]
 
96
                help += '[' + c[shortcut + 1] + ']'
 
97
                help += c[(shortcut + 2):]
 
98
                shortcut = c[shortcut + 1]
 
99
            else:
 
100
                c = c.replace('&', '')
 
101
                shortcut = c[0]
 
102
                help = '[%s]%s' % (shortcut, c[1:])
 
103
            shortcut = shortcut.lower()
 
104
            if shortcut in self.alternatives:
 
105
                raise ValueError("duplicated shortcut: %s" % shortcut)
 
106
            self.alternatives[shortcut] = choice
 
107
            # Add redirections for default.
 
108
            if index == default:
 
109
                self.alternatives[''] = choice
 
110
                self.alternatives['\r'] = choice
 
111
            help_list.append(help)
 
112
            index += 1
 
113
 
 
114
        self.prompt = u'%s (%s): ' % (msg, ', '.join(help_list))
 
115
 
 
116
    def _getline(self):
 
117
        line = self.ui.stdin.readline()
 
118
        if '' == line:
 
119
            raise EOFError
 
120
        return line.strip()
 
121
 
 
122
    def _getchar(self):
 
123
        char = osutils.getchar()
 
124
        if char == chr(3):  # INTR
 
125
            raise KeyboardInterrupt
 
126
        if char == chr(4):  # EOF (^d, C-d)
 
127
            raise EOFError
 
128
        if isinstance(char, bytes):
 
129
            return char.decode('ascii', 'replace')
 
130
        return char
 
131
 
 
132
    def interact(self):
 
133
        """Keep asking the user until a valid choice is made.
 
134
        """
 
135
        if self.line_based:
 
136
            getchoice = self._getline
 
137
        else:
 
138
            getchoice = self._getchar
 
139
        iter = 0
 
140
        while True:
 
141
            iter += 1
 
142
            if 1 == iter or self.line_based:
 
143
                self.ui.prompt(self.prompt)
 
144
            try:
 
145
                choice = getchoice()
 
146
            except EOFError:
 
147
                self.ui.stderr.write(u'\n')
 
148
                return None
 
149
            except KeyboardInterrupt:
 
150
                self.ui.stderr.write(u'\n')
 
151
                raise
 
152
            choice = choice.lower()
 
153
            if choice not in self.alternatives:
 
154
                # Not a valid choice, keep on asking.
 
155
                continue
 
156
            name, index = self.alternatives[choice]
 
157
            if self.echo_back:
 
158
                self.ui.stderr.write(name + u'\n')
 
159
            return index
 
160
 
 
161
 
 
162
opt_progress_bar = config.Option(
 
163
    'progress_bar', help='Progress bar type.',
 
164
    default_from_env=['BRZ_PROGRESS_BAR'], default=None,
 
165
    invalid='error')
 
166
 
 
167
 
 
168
class TextUIFactory(UIFactory):
 
169
    """A UI factory for Text user interfaces."""
 
170
 
 
171
    def __init__(self, stdin, stdout, stderr):
 
172
        """Create a TextUIFactory."""
 
173
        super(TextUIFactory, self).__init__()
 
174
        self.stdin = stdin
 
175
        self.stdout = stdout
 
176
        self.stderr = stderr
 
177
        self._progress_view = NullProgressView()
 
178
 
 
179
    def __enter__(self):
 
180
        # Choose default encoding and handle py2/3 differences
 
181
        self._setup_streams()
 
182
        # paints progress, network activity, etc
 
183
        self._progress_view = self.make_progress_view()
 
184
        return self
 
185
 
 
186
    def _setup_streams(self):
 
187
        self.raw_stdin = _unwrap_stream(self.stdin)
 
188
        self.stdin = _wrap_in_stream(self.raw_stdin)
 
189
        self.raw_stdout = _unwrap_stream(self.stdout)
 
190
        self.stdout = _wrap_out_stream(self.raw_stdout)
 
191
        self.raw_stderr = _unwrap_stream(self.stderr)
 
192
        self.stderr = _wrap_out_stream(self.raw_stderr)
 
193
 
 
194
    def choose(self, msg, choices, default=None):
 
195
        """Prompt the user for a list of alternatives.
 
196
 
 
197
        Support both line-based and char-based editing.
 
198
 
 
199
        In line-based mode, both the shortcut and full choice name are valid
 
200
        answers, e.g. for choose('prompt', '&yes\n&no'): 'y', ' Y ', ' yes',
 
201
        'YES ' are all valid input lines for choosing 'yes'.
 
202
 
 
203
        An empty line, when in line-based mode, or pressing enter in char-based
 
204
        mode will select the default choice (if any).
 
205
 
 
206
        Choice is echoed back if:
 
207
        - input is char-based; which means a controlling terminal is available,
 
208
          and osutils.getchar is used
 
209
        - input is line-based, and no controlling terminal is available
 
210
        """
 
211
 
 
212
        choose_ui = _ChooseUI(self, msg, choices, default)
 
213
        return choose_ui.interact()
 
214
 
 
215
    def be_quiet(self, state):
 
216
        if state and not self._quiet:
 
217
            self.clear_term()
 
218
        UIFactory.be_quiet(self, state)
 
219
        self._progress_view = self.make_progress_view()
 
220
 
 
221
    def clear_term(self):
 
222
        """Prepare the terminal for output.
 
223
 
 
224
        This will, clear any progress bars, and leave the cursor at the
 
225
        leftmost position."""
 
226
        # XXX: If this is preparing to write to stdout, but that's for example
 
227
        # directed into a file rather than to the terminal, and the progress
 
228
        # bar _is_ going to the terminal, we shouldn't need
 
229
        # to clear it.  We might need to separately check for the case of
 
230
        self._progress_view.clear()
 
231
 
 
232
    def get_integer(self, prompt):
 
233
        while True:
 
234
            self.prompt(prompt)
 
235
            line = self.stdin.readline()
 
236
            try:
 
237
                return int(line)
 
238
            except ValueError:
 
239
                pass
 
240
 
 
241
    def get_non_echoed_password(self):
 
242
        isatty = getattr(self.stdin, 'isatty', None)
 
243
        if isatty is not None and isatty():
 
244
            # getpass() ensure the password is not echoed and other
 
245
            # cross-platform niceties
 
246
            password = getpass.getpass('')
 
247
        else:
 
248
            # echo doesn't make sense without a terminal
 
249
            password = self.stdin.readline()
 
250
            if not password:
 
251
                password = None
 
252
            else:
 
253
                if password[-1] == '\n':
 
254
                    password = password[:-1]
 
255
        return password
 
256
 
 
257
    def get_password(self, prompt=u'', **kwargs):
 
258
        """Prompt the user for a password.
 
259
 
 
260
        :param prompt: The prompt to present the user
 
261
        :param kwargs: Arguments which will be expanded into the prompt.
 
262
                       This lets front ends display different things if
 
263
                       they so choose.
 
264
        :return: The password string, return None if the user
 
265
                 canceled the request.
 
266
        """
 
267
        prompt += ': '
 
268
        self.prompt(prompt, **kwargs)
 
269
        # There's currently no way to say 'i decline to enter a password'
 
270
        # as opposed to 'my password is empty' -- does it matter?
 
271
        return self.get_non_echoed_password()
 
272
 
 
273
    def get_username(self, prompt, **kwargs):
 
274
        """Prompt the user for a username.
 
275
 
 
276
        :param prompt: The prompt to present the user
 
277
        :param kwargs: Arguments which will be expanded into the prompt.
 
278
                       This lets front ends display different things if
 
279
                       they so choose.
 
280
        :return: The username string, return None if the user
 
281
                 canceled the request.
 
282
        """
 
283
        prompt += ': '
 
284
        self.prompt(prompt, **kwargs)
 
285
        username = self.stdin.readline()
 
286
        if not username:
 
287
            username = None
 
288
        else:
 
289
            if username[-1] == '\n':
 
290
                username = username[:-1]
 
291
        return username
 
292
 
 
293
    def make_progress_view(self):
 
294
        """Construct and return a new ProgressView subclass for this UI.
 
295
        """
 
296
        # with --quiet, never any progress view
 
297
        # <https://bugs.launchpad.net/bzr/+bug/320035>.  Otherwise if the
 
298
        # user specifically requests either text or no progress bars, always
 
299
        # do that.  otherwise, guess based on $TERM and tty presence.
 
300
        if self.is_quiet():
 
301
            return NullProgressView()
 
302
        pb_type = config.GlobalStack().get('progress_bar')
 
303
        if pb_type == 'none':  # Explicit requirement
 
304
            return NullProgressView()
 
305
        if (pb_type == 'text' or # Explicit requirement
 
306
                progress._supports_progress(self.stderr)):  # Guess
 
307
            return TextProgressView(self.stderr)
 
308
        # No explicit requirement and no successful guess
 
309
        return NullProgressView()
 
310
 
 
311
    def _make_output_stream_explicit(self, encoding, encoding_type):
 
312
        return TextUIOutputStream(self, self.stdout, encoding, encoding_type)
 
313
 
 
314
    def note(self, msg):
 
315
        """Write an already-formatted message, clearing the progress bar if necessary."""
 
316
        self.clear_term()
 
317
        self.stdout.write(msg + '\n')
 
318
 
 
319
    def prompt(self, prompt, **kwargs):
 
320
        """Emit prompt on the CLI.
 
321
 
 
322
        :param kwargs: Dictionary of arguments to insert into the prompt,
 
323
            to allow UIs to reformat the prompt.
 
324
        """
 
325
        if not isinstance(prompt, str):
 
326
            raise ValueError("prompt %r not a unicode string" % prompt)
 
327
        if kwargs:
 
328
            # See <https://launchpad.net/bugs/365891>
 
329
            prompt = prompt % kwargs
 
330
        self.clear_term()
 
331
        self.stdout.flush()
 
332
        self.stderr.write(prompt)
 
333
        self.stderr.flush()
 
334
 
 
335
    def report_transport_activity(self, transport, byte_count, direction):
 
336
        """Called by transports as they do IO.
 
337
 
 
338
        This may update a progress bar, spinner, or similar display.
 
339
        By default it does nothing.
 
340
        """
 
341
        self._progress_view.show_transport_activity(transport,
 
342
                                                    direction, byte_count)
 
343
 
 
344
    def log_transport_activity(self, display=False):
 
345
        """See UIFactory.log_transport_activity()"""
 
346
        log = getattr(self._progress_view, 'log_transport_activity', None)
 
347
        if log is not None:
 
348
            log(display=display)
 
349
 
 
350
    def show_error(self, msg):
 
351
        self.clear_term()
 
352
        self.stderr.write("bzr: error: %s\n" % msg)
 
353
 
 
354
    def show_message(self, msg):
 
355
        self.note(msg)
 
356
 
 
357
    def show_warning(self, msg):
 
358
        self.clear_term()
 
359
        self.stderr.write("bzr: warning: %s\n" % msg)
 
360
 
 
361
    def _progress_updated(self, task):
 
362
        """A task has been updated and wants to be displayed.
 
363
        """
 
364
        if not self._task_stack:
 
365
            warnings.warn("%r updated but no tasks are active" %
 
366
                          (task,))
 
367
        elif task != self._task_stack[-1]:
 
368
            # We used to check it was the top task, but it's hard to always
 
369
            # get this right and it's not necessarily useful: any actual
 
370
            # problems will be evident in use
 
371
            # warnings.warn("%r is not the top progress task %r" %
 
372
            #     (task, self._task_stack[-1]))
 
373
            pass
 
374
        self._progress_view.show_progress(task)
 
375
 
 
376
    def _progress_all_finished(self):
 
377
        self._progress_view.clear()
 
378
 
 
379
    def show_user_warning(self, warning_id, **message_args):
 
380
        """Show a text message to the user.
 
381
 
 
382
        Explicitly not for warnings about bzr apis, deprecations or internals.
 
383
        """
 
384
        # eventually trace.warning should migrate here, to avoid logging and
 
385
        # be easier to test; that has a lot of test fallout so for now just
 
386
        # new code can call this
 
387
        if warning_id not in self.suppressed_warnings:
 
388
            warning = self.format_user_warning(warning_id, message_args)
 
389
            self.stderr.write(warning + '\n')
 
390
 
 
391
 
 
392
def pad_to_width(line, width, encoding_hint='ascii'):
 
393
    """Truncate or pad unicode line to width.
 
394
 
 
395
    This is best-effort for now, and strings containing control codes or
 
396
    non-ascii text may be cut and padded incorrectly.
 
397
    """
 
398
    s = line.encode(encoding_hint, 'replace')
 
399
    return (b'%-*.*s' % (width, width, s)).decode(encoding_hint)
 
400
 
 
401
 
 
402
class TextProgressView(object):
 
403
    """Display of progress bar and other information on a tty.
 
404
 
 
405
    This shows one line of text, including possibly a network indicator,
 
406
    spinner, progress bar, message, etc.
 
407
 
 
408
    One instance of this is created and held by the UI, and fed updates when a
 
409
    task wants to be painted.
 
410
 
 
411
    Transports feed data to this through the ui_factory object.
 
412
 
 
413
    The Progress views can comprise a tree with _parent_task pointers, but
 
414
    this only prints the stack from the nominated current task up to the root.
 
415
    """
 
416
 
 
417
    def __init__(self, term_file, encoding=None, errors=None):
 
418
        self._term_file = term_file
 
419
        if encoding is None:
 
420
            self._encoding = getattr(term_file, "encoding", None) or "ascii"
 
421
        else:
 
422
            self._encoding = encoding
 
423
        # true when there's output on the screen we may need to clear
 
424
        self._have_output = False
 
425
        self._last_transport_msg = ''
 
426
        self._spin_pos = 0
 
427
        # time we last repainted the screen
 
428
        self._last_repaint = 0
 
429
        # time we last got information about transport activity
 
430
        self._transport_update_time = 0
 
431
        self._last_task = None
 
432
        self._total_byte_count = 0
 
433
        self._bytes_since_update = 0
 
434
        self._bytes_by_direction = {'unknown': 0, 'read': 0, 'write': 0}
 
435
        self._first_byte_time = None
 
436
        self._fraction = 0
 
437
        # force the progress bar to be off, as at the moment it doesn't
 
438
        # correspond reliably to overall command progress
 
439
        self.enable_bar = False
 
440
 
 
441
    def _avail_width(self):
 
442
        # we need one extra space for terminals that wrap on last char
 
443
        w = osutils.terminal_width()
 
444
        if w is None:
 
445
            return None
 
446
        else:
 
447
            return w - 1
 
448
 
 
449
    def _show_line(self, u):
 
450
        width = self._avail_width()
 
451
        if width is not None:
 
452
            u = pad_to_width(u, width, encoding_hint=self._encoding)
 
453
        self._term_file.write('\r' + u + '\r')
 
454
 
 
455
    def clear(self):
 
456
        if self._have_output:
 
457
            self._show_line('')
 
458
        self._have_output = False
 
459
 
 
460
    def _render_bar(self):
 
461
        # return a string for the progress bar itself
 
462
        if self.enable_bar and (
 
463
                (self._last_task is None) or self._last_task.show_bar):
 
464
            # If there's no task object, we show space for the bar anyhow.
 
465
            # That's because most invocations of bzr will end showing progress
 
466
            # at some point, though perhaps only after doing some initial IO.
 
467
            # It looks better to draw the progress bar initially rather than
 
468
            # to have what looks like an incomplete progress bar.
 
469
            spin_str = r'/-\|'[self._spin_pos % 4]
 
470
            self._spin_pos += 1
 
471
            cols = 20
 
472
            if self._last_task is None:
 
473
                completion_fraction = 0
 
474
                self._fraction = 0
 
475
            else:
 
476
                completion_fraction = \
 
477
                    self._last_task._overall_completion_fraction() or 0
 
478
            if (completion_fraction < self._fraction and 'progress' in
 
479
                    debug.debug_flags):
 
480
                debug.set_trace()
 
481
            self._fraction = completion_fraction
 
482
            markers = int(round(float(cols) * completion_fraction)) - 1
 
483
            bar_str = '[' + ('#' * markers + spin_str).ljust(cols) + '] '
 
484
            return bar_str
 
485
        elif (self._last_task is None) or self._last_task.show_spinner:
 
486
            # The last task wanted just a spinner, no bar
 
487
            spin_str = r'/-\|'[self._spin_pos % 4]
 
488
            self._spin_pos += 1
 
489
            return spin_str + ' '
 
490
        else:
 
491
            return ''
 
492
 
 
493
    def _format_task(self, task):
 
494
        """Format task-specific parts of progress bar.
 
495
 
 
496
        :returns: (text_part, counter_part) both unicode strings.
 
497
        """
 
498
        if not task.show_count:
 
499
            s = ''
 
500
        elif task.current_cnt is not None and task.total_cnt is not None:
 
501
            s = ' %d/%d' % (task.current_cnt, task.total_cnt)
 
502
        elif task.current_cnt is not None:
 
503
            s = ' %d' % (task.current_cnt)
 
504
        else:
 
505
            s = ''
 
506
        # compose all the parent messages
 
507
        t = task
 
508
        m = task.msg
 
509
        while t._parent_task:
 
510
            t = t._parent_task
 
511
            if t.msg:
 
512
                m = t.msg + ':' + m
 
513
        return m, s
 
514
 
 
515
    def _render_line(self):
 
516
        bar_string = self._render_bar()
 
517
        if self._last_task:
 
518
            task_part, counter_part = self._format_task(self._last_task)
 
519
        else:
 
520
            task_part = counter_part = ''
 
521
        if self._last_task and not self._last_task.show_transport_activity:
 
522
            trans = ''
 
523
        else:
 
524
            trans = self._last_transport_msg
 
525
        # the bar separates the transport activity from the message, so even
 
526
        # if there's no bar or spinner, we must show something if both those
 
527
        # fields are present
 
528
        if (task_part or trans) and not bar_string:
 
529
            bar_string = '| '
 
530
        # preferentially truncate the task message if we don't have enough
 
531
        # space
 
532
        avail_width = self._avail_width()
 
533
        if avail_width is not None:
 
534
            # if terminal avail_width is unknown, don't truncate
 
535
            current_len = len(bar_string) + len(trans) + \
 
536
                len(task_part) + len(counter_part)
 
537
            # GZ 2017-04-22: Should measure and truncate task_part properly
 
538
            gap = current_len - avail_width
 
539
            if gap > 0:
 
540
                task_part = task_part[:-gap - 2] + '..'
 
541
        s = trans + bar_string + task_part + counter_part
 
542
        if avail_width is not None:
 
543
            if len(s) < avail_width:
 
544
                s = s.ljust(avail_width)
 
545
            elif len(s) > avail_width:
 
546
                s = s[:avail_width]
 
547
        return s
 
548
 
 
549
    def _repaint(self):
 
550
        s = self._render_line()
 
551
        self._show_line(s)
 
552
        self._have_output = True
 
553
 
 
554
    def show_progress(self, task):
 
555
        """Called by the task object when it has changed.
 
556
 
 
557
        :param task: The top task object; its parents are also included
 
558
            by following links.
 
559
        """
 
560
        must_update = task is not self._last_task
 
561
        self._last_task = task
 
562
        now = time.time()
 
563
        if (not must_update) and (now < self._last_repaint + task.update_latency):
 
564
            return
 
565
        if now > self._transport_update_time + 10:
 
566
            # no recent activity; expire it
 
567
            self._last_transport_msg = ''
 
568
        self._last_repaint = now
 
569
        self._repaint()
 
570
 
 
571
    def show_transport_activity(self, transport, direction, byte_count):
 
572
        """Called by transports via the ui_factory, as they do IO.
 
573
 
 
574
        This may update a progress bar, spinner, or similar display.
 
575
        By default it does nothing.
 
576
        """
 
577
        # XXX: there should be a transport activity model, and that too should
 
578
        #      be seen by the progress view, rather than being poked in here.
 
579
        self._total_byte_count += byte_count
 
580
        self._bytes_since_update += byte_count
 
581
        if self._first_byte_time is None:
 
582
            # Note that this isn't great, as technically it should be the time
 
583
            # when the bytes started transferring, not when they completed.
 
584
            # However, we usually start with a small request anyway.
 
585
            self._first_byte_time = time.time()
 
586
        if direction in self._bytes_by_direction:
 
587
            self._bytes_by_direction[direction] += byte_count
 
588
        else:
 
589
            self._bytes_by_direction['unknown'] += byte_count
 
590
        if 'no_activity' in debug.debug_flags:
 
591
            # Can be used as a workaround if
 
592
            # <https://launchpad.net/bugs/321935> reappears and transport
 
593
            # activity is cluttering other output.  However, thanks to
 
594
            # TextUIOutputStream this shouldn't be a problem any more.
 
595
            return
 
596
        now = time.time()
 
597
        if self._total_byte_count < 2000:
 
598
            # a little resistance at first, so it doesn't stay stuck at 0
 
599
            # while connecting...
 
600
            return
 
601
        if self._transport_update_time is None:
 
602
            self._transport_update_time = now
 
603
        elif now >= (self._transport_update_time + 0.5):
 
604
            # guard against clock stepping backwards, and don't update too
 
605
            # often
 
606
            rate = (self._bytes_since_update /
 
607
                    (now - self._transport_update_time))
 
608
            # using base-10 units (see HACKING.txt).
 
609
            msg = ("%6dkB %5dkB/s " %
 
610
                   (self._total_byte_count / 1000, int(rate) / 1000,))
 
611
            self._transport_update_time = now
 
612
            self._last_repaint = now
 
613
            self._bytes_since_update = 0
 
614
            self._last_transport_msg = msg
 
615
            self._repaint()
 
616
 
 
617
    def _format_bytes_by_direction(self):
 
618
        if self._first_byte_time is None:
 
619
            bps = 0.0
 
620
        else:
 
621
            transfer_time = time.time() - self._first_byte_time
 
622
            if transfer_time < 0.001:
 
623
                transfer_time = 0.001
 
624
            bps = self._total_byte_count / transfer_time
 
625
 
 
626
        # using base-10 units (see HACKING.txt).
 
627
        msg = ('Transferred: %.0fkB'
 
628
               ' (%.1fkB/s r:%.0fkB w:%.0fkB'
 
629
               % (self._total_byte_count / 1000.,
 
630
                  bps / 1000.,
 
631
                  self._bytes_by_direction['read'] / 1000.,
 
632
                  self._bytes_by_direction['write'] / 1000.,
 
633
                  ))
 
634
        if self._bytes_by_direction['unknown'] > 0:
 
635
            msg += ' u:%.0fkB)' % (
 
636
                self._bytes_by_direction['unknown'] / 1000.
 
637
                )
 
638
        else:
 
639
            msg += ')'
 
640
        return msg
 
641
 
 
642
    def log_transport_activity(self, display=False):
 
643
        msg = self._format_bytes_by_direction()
 
644
        trace.mutter(msg)
 
645
        if display and self._total_byte_count > 0:
 
646
            self.clear()
 
647
            self._term_file.write(msg + '\n')
 
648
 
 
649
 
 
650
def _get_stream_encoding(stream):
 
651
    encoding = config.GlobalStack().get('output_encoding')
 
652
    if encoding is None:
 
653
        encoding = getattr(stream, "encoding", None)
 
654
    if encoding is None:
 
655
        encoding = osutils.get_terminal_encoding(trace=True)
 
656
    return encoding
 
657
 
 
658
 
 
659
def _unwrap_stream(stream):
 
660
    inner = getattr(stream, "buffer", None)
 
661
    if inner is None:
 
662
        inner = getattr(stream, "stream", stream)
 
663
    return inner
 
664
 
 
665
 
 
666
def _wrap_in_stream(stream, encoding=None, errors='replace'):
 
667
    if encoding is None:
 
668
        encoding = _get_stream_encoding(stream)
 
669
    # Attempt to wrap using io.open if possible, since that can do
 
670
    # line-buffering.
 
671
    try:
 
672
        fileno = stream.fileno()
 
673
    except io.UnsupportedOperation:
 
674
        encoded_stream = codecs.getreader(encoding)(stream, errors=errors)
 
675
        encoded_stream.encoding = encoding
 
676
        return encoded_stream
 
677
    else:
 
678
        return io.open(fileno, encoding=encoding, errors=errors, mode='r', buffering=1)
 
679
 
 
680
 
 
681
def _wrap_out_stream(stream, encoding=None, errors='replace'):
 
682
    if encoding is None:
 
683
        encoding = _get_stream_encoding(stream)
 
684
    encoded_stream = codecs.getwriter(encoding)(stream, errors=errors)
 
685
    encoded_stream.encoding = encoding
 
686
    return encoded_stream
 
687
 
 
688
 
 
689
class TextUIOutputStream(object):
 
690
    """Decorates stream to interact better with progress and change encoding.
 
691
 
 
692
    Before writing to the wrapped stream, progress is cleared. Callers must
 
693
    ensure bulk output is terminated with a newline so progress won't overwrite
 
694
    partial lines.
 
695
 
 
696
    Additionally, the encoding and errors behaviour of the underlying stream
 
697
    can be changed at this point. If errors is set to 'exact' raw bytes may be
 
698
    written to the underlying stream.
 
699
    """
 
700
 
 
701
    def __init__(self, ui_factory, stream, encoding=None, errors='strict'):
 
702
        self.ui_factory = ui_factory
 
703
        # GZ 2017-05-21: Clean up semantics when callers are made saner.
 
704
        inner = _unwrap_stream(stream)
 
705
        self.raw_stream = None
 
706
        if errors == "exact":
 
707
            errors = "strict"
 
708
            self.raw_stream = inner
 
709
        if inner is None:
 
710
            self.wrapped_stream = stream
 
711
            if encoding is None:
 
712
                encoding = _get_stream_encoding(stream)
 
713
        else:
 
714
            self.wrapped_stream = _wrap_out_stream(inner, encoding, errors)
 
715
            if encoding is None:
 
716
                encoding = self.wrapped_stream.encoding
 
717
        self.encoding = encoding
 
718
        self.errors = errors
 
719
 
 
720
    def _write(self, to_write):
 
721
        if isinstance(to_write, bytes):
 
722
            try:
 
723
                to_write = to_write.decode(self.encoding, self.errors)
 
724
            except UnicodeDecodeError:
 
725
                self.raw_stream.write(to_write)
 
726
                return
 
727
        self.wrapped_stream.write(to_write)
 
728
 
 
729
    def flush(self):
 
730
        self.ui_factory.clear_term()
 
731
        self.wrapped_stream.flush()
 
732
 
 
733
    def write(self, to_write):
 
734
        self.ui_factory.clear_term()
 
735
        self._write(to_write)
 
736
 
 
737
    def writelines(self, lines):
 
738
        self.ui_factory.clear_term()
 
739
        for line in lines:
 
740
            self._write(line)