~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/ui/text.py

  • Committer: Patch Queue Manager
  • Date: 2015-09-30 16:43:21 UTC
  • mfrom: (6603.2.2 fix-keep-dirty)
  • Revision ID: pqm@pqm.ubuntu.com-20150930164321-ct2v2qnmvimqt8qf
(vila) Avoid associating dirty patch headers with the previous file in the
 patch. (Colin Watson)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-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
 
38
41
""")
39
42
 
40
 
from bzrlib.osutils import watch_sigwinch
41
 
 
42
43
from bzrlib.ui import (
43
44
    UIFactory,
44
45
    NullProgressView,
45
46
    )
46
47
 
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 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
 
48
165
class TextUIFactory(UIFactory):
49
 
    """A UI factory for Text user interefaces."""
 
166
    """A UI factory for Text user interfaces."""
50
167
 
51
168
    def __init__(self,
52
169
                 stdin=None,
62
179
        self.stderr = stderr
63
180
        # paints progress, network activity, etc
64
181
        self._progress_view = self.make_progress_view()
65
 
        # hook up the signals to watch for terminal size changes
66
 
        watch_sigwinch()
 
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()
67
203
 
68
204
    def be_quiet(self, state):
69
205
        if state and not self._quiet:
82
218
        # to clear it.  We might need to separately check for the case of
83
219
        self._progress_view.clear()
84
220
 
85
 
    def get_boolean(self, prompt):
86
 
        while True:
87
 
            self.prompt(prompt + "? [y/n]: ")
88
 
            line = self.stdin.readline().lower()
89
 
            if line in ('y\n', 'yes\n'):
90
 
                return True
91
 
            elif line in ('n\n', 'no\n'):
92
 
                return False
93
 
            elif line in ('', None):
94
 
                # end-of-file; possibly should raise an error here instead
95
 
                return None
96
 
 
97
221
    def get_integer(self, prompt):
98
222
        while True:
99
223
            self.prompt(prompt)
114
238
            password = self.stdin.readline()
115
239
            if not password:
116
240
                password = None
117
 
            elif password[-1] == '\n':
118
 
                password = password[:-1]
 
241
            else:
 
242
                password = password.decode(self.stdin.encoding)
 
243
 
 
244
                if password[-1] == '\n':
 
245
                    password = password[:-1]
119
246
        return password
120
247
 
121
 
    def get_password(self, prompt='', **kwargs):
 
248
    def get_password(self, prompt=u'', **kwargs):
122
249
        """Prompt the user for a password.
123
250
 
124
251
        :param prompt: The prompt to present the user
149
276
        username = self.stdin.readline()
150
277
        if not username:
151
278
            username = None
152
 
        elif username[-1] == '\n':
153
 
            username = username[:-1]
 
279
        else:
 
280
            username = username.decode(self.stdin.encoding)
 
281
            if username[-1] == '\n':
 
282
                username = username[:-1]
154
283
        return username
155
284
 
156
285
    def make_progress_view(self):
157
286
        """Construct and return a new ProgressView subclass for this UI.
158
287
        """
159
288
        # with --quiet, never any progress view
160
 
        # <https://bugs.edge.launchpad.net/bzr/+bug/320035>.  Otherwise if the
 
289
        # <https://bugs.launchpad.net/bzr/+bug/320035>.  Otherwise if the
161
290
        # user specifically requests either text or no progress bars, always
162
291
        # do that.  otherwise, guess based on $TERM and tty presence.
163
292
        if self.is_quiet():
164
293
            return NullProgressView()
165
 
        elif os.environ.get('BZR_PROGRESS_BAR') == 'text':
166
 
            return TextProgressView(self.stderr)
167
 
        elif os.environ.get('BZR_PROGRESS_BAR') == 'none':
168
 
            return NullProgressView()
169
 
        elif progress._supports_progress(self.stderr):
170
 
            return TextProgressView(self.stderr)
171
 
        else:
172
 
            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()
173
302
 
174
303
    def _make_output_stream_explicit(self, encoding, encoding_type):
175
304
        if encoding_type == 'exact':
202
331
        :param kwargs: Dictionary of arguments to insert into the prompt,
203
332
            to allow UIs to reformat the prompt.
204
333
        """
 
334
        if type(prompt) != unicode:
 
335
            raise ValueError("prompt %r not a unicode string" % prompt)
205
336
        if kwargs:
206
337
            # See <https://launchpad.net/bugs/365891>
207
338
            prompt = prompt % kwargs
208
 
        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')
209
346
        self.clear_term()
 
347
        self.stdout.flush()
210
348
        self.stderr.write(prompt)
211
349
 
212
350
    def report_transport_activity(self, transport, byte_count, direction):
233
371
 
234
372
    def show_warning(self, msg):
235
373
        self.clear_term()
 
374
        if isinstance(msg, unicode):
 
375
            te = osutils.get_terminal_encoding()
 
376
            msg = msg.encode(te, 'replace')
236
377
        self.stderr.write("bzr: warning: %s\n" % msg)
237
378
 
238
379
    def _progress_updated(self, task):
281
422
    this only prints the stack from the nominated current task up to the root.
282
423
    """
283
424
 
284
 
    def __init__(self, term_file):
 
425
    def __init__(self, term_file, encoding=None, errors="replace"):
285
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
286
432
        # true when there's output on the screen we may need to clear
287
433
        self._have_output = False
288
434
        self._last_transport_msg = ''
301
447
        # correspond reliably to overall command progress
302
448
        self.enable_bar = False
303
449
 
304
 
    def _show_line(self, s):
305
 
        # sys.stderr.write("progress %r\n" % s)
306
 
        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()
307
461
        if width is not None:
308
 
            # we need one extra space for terminals that wrap on last char
309
 
            width = width - 1
 
462
            # GZ 2012-03-28: Counting bytes is wrong for calculating width of
 
463
            #                text but better than counting codepoints.
310
464
            s = '%-*.*s' % (width, width, s)
311
465
        self._term_file.write('\r' + s + '\r')
312
466
 
349
503
            return ''
350
504
 
351
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
        """
352
510
        if not task.show_count:
353
511
            s = ''
354
512
        elif task.current_cnt is not None and task.total_cnt is not None:
364
522
            t = t._parent_task
365
523
            if t.msg:
366
524
                m = t.msg + ':' + m
367
 
        return m + s
 
525
        return m, s
368
526
 
369
527
    def _render_line(self):
370
528
        bar_string = self._render_bar()
371
529
        if self._last_task:
372
 
            task_msg = self._format_task(self._last_task)
 
530
            task_part, counter_part = self._format_task(self._last_task)
373
531
        else:
374
 
            task_msg = ''
 
532
            task_part = counter_part = ''
375
533
        if self._last_task and not self._last_task.show_transport_activity:
376
534
            trans = ''
377
535
        else:
378
536
            trans = self._last_transport_msg
379
 
            if trans:
380
 
                trans += ' | '
381
 
        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
382
558
 
383
559
    def _repaint(self):
384
560
        s = self._render_line()
440
616
            rate = (self._bytes_since_update
441
617
                    / (now - self._transport_update_time))
442
618
            # using base-10 units (see HACKING.txt).
443
 
            msg = ("%6dkB %5dkB/s" %
 
619
            msg = ("%6dkB %5dkB/s " %
444
620
                    (self._total_byte_count / 1000, int(rate) / 1000,))
445
621
            self._transport_update_time = now
446
622
            self._last_repaint = now