~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/ui/text.py

  • Committer: Tarmac
  • Author(s): Vincent Ladeuil
  • Date: 2017-01-30 14:42:05 UTC
  • mfrom: (6620.1.1 trunk)
  • Revision ID: tarmac-20170130144205-r8fh2xpmiuxyozpv
Merge  2.7 into trunk including fix for bug #1657238 [r=vila]

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2008, 2009, 2010 Canonical Ltd
 
1
# Copyright (C) 2005-2011 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
18
18
"""Text UI, write output to the console.
19
19
"""
20
20
 
21
 
import codecs
22
 
import getpass
 
21
from __future__ import absolute_import
 
22
 
23
23
import os
24
24
import sys
25
25
import time
26
 
import warnings
27
26
 
28
27
from bzrlib.lazy_import import lazy_import
29
28
lazy_import(globals(), """
 
29
import codecs
 
30
import getpass
 
31
import warnings
 
32
 
30
33
from bzrlib import (
 
34
    config,
31
35
    debug,
32
36
    progress,
33
37
    osutils,
34
 
    symbol_versioning,
35
38
    trace,
36
39
    )
37
40
 
43
46
    )
44
47
 
45
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 BZR_TEXTUI_INPUT environment
 
63
        variable is set to 'line-based', or if there is no controlling
 
64
        terminal.
 
65
        """
 
66
        if os.environ.get('BZR_TEXTUI_INPUT') != 'line-based' and \
 
67
           self.ui.stdin == sys.stdin and self.ui.stdin.isatty():
 
68
            self.line_based = False
 
69
            self.echo_back = True
 
70
        else:
 
71
            self.line_based = True
 
72
            self.echo_back = not self.ui.stdin.isatty()
 
73
 
 
74
    def _build_alternatives(self, msg, choices, default):
 
75
        """Parse choices string.
 
76
 
 
77
        Setup final prompt and the lists of choices and associated
 
78
        shortcuts.
 
79
        """
 
80
        index = 0
 
81
        help_list = []
 
82
        self.alternatives = {}
 
83
        choices = choices.split('\n')
 
84
        if default is not None and default not in range(0, len(choices)):
 
85
            raise ValueError("invalid default index")
 
86
        for c in choices:
 
87
            name = c.replace('&', '').lower()
 
88
            choice = (name, index)
 
89
            if name in self.alternatives:
 
90
                raise ValueError("duplicated choice: %s" % name)
 
91
            self.alternatives[name] = choice
 
92
            shortcut = c.find('&')
 
93
            if -1 != shortcut and (shortcut + 1) < len(c):
 
94
                help = c[:shortcut]
 
95
                help += '[' + c[shortcut + 1] + ']'
 
96
                help += c[(shortcut + 2):]
 
97
                shortcut = c[shortcut + 1]
 
98
            else:
 
99
                c = c.replace('&', '')
 
100
                shortcut = c[0]
 
101
                help = '[%s]%s' % (shortcut, c[1:])
 
102
            shortcut = shortcut.lower()
 
103
            if shortcut in self.alternatives:
 
104
                raise ValueError("duplicated shortcut: %s" % shortcut)
 
105
            self.alternatives[shortcut] = choice
 
106
            # Add redirections for default.
 
107
            if index == default:
 
108
                self.alternatives[''] = choice
 
109
                self.alternatives['\r'] = choice
 
110
            help_list.append(help)
 
111
            index += 1
 
112
 
 
113
        self.prompt = u'%s (%s): ' % (msg, ', '.join(help_list))
 
114
 
 
115
    def _getline(self):
 
116
        line = self.ui.stdin.readline()
 
117
        if '' == line:
 
118
            raise EOFError
 
119
        return line.strip()
 
120
 
 
121
    def _getchar(self):
 
122
        char = osutils.getchar()
 
123
        if char == chr(3): # INTR
 
124
            raise KeyboardInterrupt
 
125
        if char == chr(4): # EOF (^d, C-d)
 
126
            raise EOFError
 
127
        return char
 
128
 
 
129
    def interact(self):
 
130
        """Keep asking the user until a valid choice is made.
 
131
        """
 
132
        if self.line_based:
 
133
            getchoice = self._getline
 
134
        else:
 
135
            getchoice = self._getchar
 
136
        iter = 0
 
137
        while True:
 
138
            iter += 1
 
139
            if 1 == iter or self.line_based:
 
140
                self.ui.prompt(self.prompt)
 
141
            try:
 
142
                choice = getchoice()
 
143
            except EOFError:
 
144
                self.ui.stderr.write('\n')
 
145
                return None
 
146
            except KeyboardInterrupt:
 
147
                self.ui.stderr.write('\n')
 
148
                raise KeyboardInterrupt
 
149
            choice = choice.lower()
 
150
            if choice not in self.alternatives:
 
151
                # Not a valid choice, keep on asking.
 
152
                continue
 
153
            name, index = self.alternatives[choice]
 
154
            if self.echo_back:
 
155
                self.ui.stderr.write(name + '\n')
 
156
            return index
 
157
 
 
158
 
 
159
opt_progress_bar = config.Option(
 
160
    'progress_bar', help='Progress bar type.',
 
161
    default_from_env=['BZR_PROGRESS_BAR'], default=None,
 
162
    invalid='error')
 
163
 
 
164
 
46
165
class TextUIFactory(UIFactory):
47
 
    """A UI factory for Text user interefaces."""
 
166
    """A UI factory for Text user interfaces."""
48
167
 
49
168
    def __init__(self,
50
169
                 stdin=None,
60
179
        self.stderr = stderr
61
180
        # paints progress, network activity, etc
62
181
        self._progress_view = self.make_progress_view()
63
 
        
 
182
 
 
183
    def choose(self, msg, choices, default=None):
 
184
        """Prompt the user for a list of alternatives.
 
185
 
 
186
        Support both line-based and char-based editing.
 
187
 
 
188
        In line-based mode, both the shortcut and full choice name are valid
 
189
        answers, e.g. for choose('prompt', '&yes\n&no'): 'y', ' Y ', ' yes',
 
190
        'YES ' are all valid input lines for choosing 'yes'.
 
191
 
 
192
        An empty line, when in line-based mode, or pressing enter in char-based
 
193
        mode will select the default choice (if any).
 
194
 
 
195
        Choice is echoed back if:
 
196
        - input is char-based; which means a controlling terminal is available,
 
197
          and osutils.getchar is used
 
198
        - input is line-based, and no controlling terminal is available
 
199
        """
 
200
 
 
201
        choose_ui = _ChooseUI(self, msg, choices, default)
 
202
        return choose_ui.interact()
 
203
 
64
204
    def be_quiet(self, state):
65
205
        if state and not self._quiet:
66
206
            self.clear_term()
78
218
        # to clear it.  We might need to separately check for the case of
79
219
        self._progress_view.clear()
80
220
 
81
 
    def get_boolean(self, prompt):
82
 
        while True:
83
 
            self.prompt(prompt + "? [y/n]: ")
84
 
            line = self.stdin.readline().lower()
85
 
            if line in ('y\n', 'yes\n'):
86
 
                return True
87
 
            elif line in ('n\n', 'no\n'):
88
 
                return False
89
 
            elif line in ('', None):
90
 
                # end-of-file; possibly should raise an error here instead
91
 
                return None
92
 
 
93
221
    def get_integer(self, prompt):
94
222
        while True:
95
223
            self.prompt(prompt)
110
238
            password = self.stdin.readline()
111
239
            if not password:
112
240
                password = None
113
 
            elif password[-1] == '\n':
114
 
                password = password[:-1]
 
241
            else:
 
242
                password = password.decode(self.stdin.encoding)
 
243
 
 
244
                if password[-1] == '\n':
 
245
                    password = password[:-1]
115
246
        return password
116
247
 
117
 
    def get_password(self, prompt='', **kwargs):
 
248
    def get_password(self, prompt=u'', **kwargs):
118
249
        """Prompt the user for a password.
119
250
 
120
251
        :param prompt: The prompt to present the user
145
276
        username = self.stdin.readline()
146
277
        if not username:
147
278
            username = None
148
 
        elif username[-1] == '\n':
149
 
            username = username[:-1]
 
279
        else:
 
280
            username = username.decode(self.stdin.encoding)
 
281
            if username[-1] == '\n':
 
282
                username = username[:-1]
150
283
        return username
151
284
 
152
285
    def make_progress_view(self):
153
286
        """Construct and return a new ProgressView subclass for this UI.
154
287
        """
155
288
        # with --quiet, never any progress view
156
 
        # <https://bugs.edge.launchpad.net/bzr/+bug/320035>.  Otherwise if the
 
289
        # <https://bugs.launchpad.net/bzr/+bug/320035>.  Otherwise if the
157
290
        # user specifically requests either text or no progress bars, always
158
291
        # do that.  otherwise, guess based on $TERM and tty presence.
159
292
        if self.is_quiet():
160
293
            return NullProgressView()
161
 
        elif os.environ.get('BZR_PROGRESS_BAR') == 'text':
162
 
            return TextProgressView(self.stderr)
163
 
        elif os.environ.get('BZR_PROGRESS_BAR') == 'none':
164
 
            return NullProgressView()
165
 
        elif progress._supports_progress(self.stderr):
166
 
            return TextProgressView(self.stderr)
167
 
        else:
168
 
            return NullProgressView()
 
294
        pb_type = config.GlobalStack().get('progress_bar')
 
295
        if pb_type == 'none': # Explicit requirement
 
296
            return NullProgressView()
 
297
        if (pb_type == 'text' # Explicit requirement
 
298
            or progress._supports_progress(self.stderr)): # Guess
 
299
            return TextProgressView(self.stderr)
 
300
        # No explicit requirement and no successful guess
 
301
        return NullProgressView()
169
302
 
170
303
    def _make_output_stream_explicit(self, encoding, encoding_type):
171
304
        if encoding_type == 'exact':
198
331
        :param kwargs: Dictionary of arguments to insert into the prompt,
199
332
            to allow UIs to reformat the prompt.
200
333
        """
 
334
        if type(prompt) != unicode:
 
335
            raise ValueError("prompt %r not a unicode string" % prompt)
201
336
        if kwargs:
202
337
            # See <https://launchpad.net/bugs/365891>
203
338
            prompt = prompt % kwargs
204
 
        prompt = prompt.encode(osutils.get_terminal_encoding(), 'replace')
 
339
        try:
 
340
            prompt = prompt.encode(self.stderr.encoding)
 
341
        except (UnicodeError, AttributeError):
 
342
            # If stderr has no encoding attribute or can't properly encode,
 
343
            # fallback to terminal encoding for robustness (better display
 
344
            # something to the user than aborting with a traceback).
 
345
            prompt = prompt.encode(osutils.get_terminal_encoding(), 'replace')
205
346
        self.clear_term()
 
347
        self.stdout.flush()
206
348
        self.stderr.write(prompt)
207
349
 
208
350
    def report_transport_activity(self, transport, byte_count, direction):
229
371
 
230
372
    def show_warning(self, msg):
231
373
        self.clear_term()
 
374
        if isinstance(msg, unicode):
 
375
            te = osutils.get_terminal_encoding()
 
376
            msg = msg.encode(te, 'replace')
232
377
        self.stderr.write("bzr: warning: %s\n" % msg)
233
378
 
234
379
    def _progress_updated(self, task):
238
383
            warnings.warn("%r updated but no tasks are active" %
239
384
                (task,))
240
385
        elif task != self._task_stack[-1]:
241
 
            warnings.warn("%r is not the top progress task %r" %
242
 
                (task, self._task_stack[-1]))
 
386
            # We used to check it was the top task, but it's hard to always
 
387
            # get this right and it's not necessarily useful: any actual
 
388
            # problems will be evident in use
 
389
            #warnings.warn("%r is not the top progress task %r" %
 
390
            #     (task, self._task_stack[-1]))
 
391
            pass
243
392
        self._progress_view.show_progress(task)
244
393
 
245
394
    def _progress_all_finished(self):
246
395
        self._progress_view.clear()
247
396
 
 
397
    def show_user_warning(self, warning_id, **message_args):
 
398
        """Show a text message to the user.
 
399
 
 
400
        Explicitly not for warnings about bzr apis, deprecations or internals.
 
401
        """
 
402
        # eventually trace.warning should migrate here, to avoid logging and
 
403
        # be easier to test; that has a lot of test fallout so for now just
 
404
        # new code can call this
 
405
        if warning_id not in self.suppressed_warnings:
 
406
            self.stderr.write(self.format_user_warning(warning_id, message_args) +
 
407
                '\n')
 
408
 
248
409
 
249
410
class TextProgressView(object):
250
411
    """Display of progress bar and other information on a tty.
261
422
    this only prints the stack from the nominated current task up to the root.
262
423
    """
263
424
 
264
 
    def __init__(self, term_file):
 
425
    def __init__(self, term_file, encoding=None, errors="replace"):
265
426
        self._term_file = term_file
 
427
        if encoding is None:
 
428
            self._encoding = getattr(term_file, "encoding", None) or "ascii"
 
429
        else:
 
430
            self._encoding = encoding
 
431
        self._encoding_errors = errors
266
432
        # true when there's output on the screen we may need to clear
267
433
        self._have_output = False
268
434
        self._last_transport_msg = ''
281
447
        # correspond reliably to overall command progress
282
448
        self.enable_bar = False
283
449
 
284
 
    def _show_line(self, s):
285
 
        # sys.stderr.write("progress %r\n" % s)
286
 
        width = osutils.terminal_width()
 
450
    def _avail_width(self):
 
451
        # we need one extra space for terminals that wrap on last char
 
452
        w = osutils.terminal_width() 
 
453
        if w is None:
 
454
            return None
 
455
        else:
 
456
            return w - 1
 
457
 
 
458
    def _show_line(self, u):
 
459
        s = u.encode(self._encoding, self._encoding_errors)
 
460
        width = self._avail_width()
287
461
        if width is not None:
288
 
            # we need one extra space for terminals that wrap on last char
289
 
            width = width - 1
 
462
            # GZ 2012-03-28: Counting bytes is wrong for calculating width of
 
463
            #                text but better than counting codepoints.
290
464
            s = '%-*.*s' % (width, width, s)
291
465
        self._term_file.write('\r' + s + '\r')
292
466
 
329
503
            return ''
330
504
 
331
505
    def _format_task(self, task):
 
506
        """Format task-specific parts of progress bar.
 
507
 
 
508
        :returns: (text_part, counter_part) both unicode strings.
 
509
        """
332
510
        if not task.show_count:
333
511
            s = ''
334
512
        elif task.current_cnt is not None and task.total_cnt is not None:
344
522
            t = t._parent_task
345
523
            if t.msg:
346
524
                m = t.msg + ':' + m
347
 
        return m + s
 
525
        return m, s
348
526
 
349
527
    def _render_line(self):
350
528
        bar_string = self._render_bar()
351
529
        if self._last_task:
352
 
            task_msg = self._format_task(self._last_task)
 
530
            task_part, counter_part = self._format_task(self._last_task)
353
531
        else:
354
 
            task_msg = ''
 
532
            task_part = counter_part = ''
355
533
        if self._last_task and not self._last_task.show_transport_activity:
356
534
            trans = ''
357
535
        else:
358
536
            trans = self._last_transport_msg
359
 
            if trans:
360
 
                trans += ' | '
361
 
        return (bar_string + trans + task_msg)
 
537
        # the bar separates the transport activity from the message, so even
 
538
        # if there's no bar or spinner, we must show something if both those
 
539
        # fields are present
 
540
        if (task_part or trans) and not bar_string:
 
541
            bar_string = '| '
 
542
        # preferentially truncate the task message if we don't have enough
 
543
        # space
 
544
        avail_width = self._avail_width()
 
545
        if avail_width is not None:
 
546
            # if terminal avail_width is unknown, don't truncate
 
547
            current_len = len(bar_string) + len(trans) + len(task_part) + len(counter_part)
 
548
            gap = current_len - avail_width
 
549
            if gap > 0:
 
550
                task_part = task_part[:-gap-2] + '..'
 
551
        s = trans + bar_string + task_part + counter_part
 
552
        if avail_width is not None:
 
553
            if len(s) < avail_width:
 
554
                s = s.ljust(avail_width)
 
555
            elif len(s) > avail_width:
 
556
                s = s[:avail_width]
 
557
        return s
362
558
 
363
559
    def _repaint(self):
364
560
        s = self._render_line()
417
613
        elif now >= (self._transport_update_time + 0.5):
418
614
            # guard against clock stepping backwards, and don't update too
419
615
            # often
420
 
            rate = self._bytes_since_update / (now - self._transport_update_time)
421
 
            msg = ("%6dKB %5dKB/s" %
422
 
                    (self._total_byte_count>>10, int(rate)>>10,))
 
616
            rate = (self._bytes_since_update
 
617
                    / (now - self._transport_update_time))
 
618
            # using base-10 units (see HACKING.txt).
 
619
            msg = ("%6dkB %5dkB/s " %
 
620
                    (self._total_byte_count / 1000, int(rate) / 1000,))
423
621
            self._transport_update_time = now
424
622
            self._last_repaint = now
425
623
            self._bytes_since_update = 0
435
633
                transfer_time = 0.001
436
634
            bps = self._total_byte_count / transfer_time
437
635
 
438
 
        msg = ('Transferred: %.0fKiB'
439
 
               ' (%.1fK/s r:%.0fK w:%.0fK'
440
 
               % (self._total_byte_count / 1024.,
441
 
                  bps / 1024.,
442
 
                  self._bytes_by_direction['read'] / 1024.,
443
 
                  self._bytes_by_direction['write'] / 1024.,
 
636
        # using base-10 units (see HACKING.txt).
 
637
        msg = ('Transferred: %.0fkB'
 
638
               ' (%.1fkB/s r:%.0fkB w:%.0fkB'
 
639
               % (self._total_byte_count / 1000.,
 
640
                  bps / 1000.,
 
641
                  self._bytes_by_direction['read'] / 1000.,
 
642
                  self._bytes_by_direction['write'] / 1000.,
444
643
                 ))
445
644
        if self._bytes_by_direction['unknown'] > 0:
446
 
            msg += ' u:%.0fK)' % (
447
 
                self._bytes_by_direction['unknown'] / 1024.
 
645
            msg += ' u:%.0fkB)' % (
 
646
                self._bytes_by_direction['unknown'] / 1000.
448
647
                )
449
648
        else:
450
649
            msg += ')'