~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/ui/text.py

  • Committer: Jelmer Vernooij
  • Date: 2011-12-05 14:12:23 UTC
  • mto: This revision was merged to the branch mainline in revision 6348.
  • Revision ID: jelmer@samba.org-20111205141223-8qxae4h37satlzgq
Move more functionality to vf_search.

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
23
21
import os
24
22
import sys
25
23
import time
26
 
import warnings
27
24
 
28
25
from bzrlib.lazy_import import lazy_import
29
26
lazy_import(globals(), """
 
27
import codecs
 
28
import getpass
 
29
import warnings
 
30
 
30
31
from bzrlib import (
31
32
    debug,
32
33
    progress,
33
34
    osutils,
34
 
    symbol_versioning,
35
35
    trace,
36
36
    )
37
37
 
43
43
    )
44
44
 
45
45
 
 
46
class _ChooseUI(object):
 
47
 
 
48
    """ Helper class for choose implementation.
 
49
    """
 
50
 
 
51
    def __init__(self, ui, msg, choices, default):
 
52
        self.ui = ui
 
53
        self._setup_mode()
 
54
        self._build_alternatives(msg, choices, default)
 
55
 
 
56
    def _setup_mode(self):
 
57
        """Setup input mode (line-based, char-based) and echo-back.
 
58
 
 
59
        Line-based input is used if the BZR_TEXTUI_INPUT environment
 
60
        variable is set to 'line-based', or if there is no controlling
 
61
        terminal.
 
62
        """
 
63
        if os.environ.get('BZR_TEXTUI_INPUT') != 'line-based' and \
 
64
           self.ui.stdin == sys.stdin and self.ui.stdin.isatty():
 
65
            self.line_based = False
 
66
            self.echo_back = True
 
67
        else:
 
68
            self.line_based = True
 
69
            self.echo_back = not self.ui.stdin.isatty()
 
70
 
 
71
    def _build_alternatives(self, msg, choices, default):
 
72
        """Parse choices string.
 
73
 
 
74
        Setup final prompt and the lists of choices and associated
 
75
        shortcuts.
 
76
        """
 
77
        index = 0
 
78
        help_list = []
 
79
        self.alternatives = {}
 
80
        choices = choices.split('\n')
 
81
        if default is not None and default not in range(0, len(choices)):
 
82
            raise ValueError("invalid default index")
 
83
        for c in choices:
 
84
            name = c.replace('&', '').lower()
 
85
            choice = (name, index)
 
86
            if name in self.alternatives:
 
87
                raise ValueError("duplicated choice: %s" % name)
 
88
            self.alternatives[name] = choice
 
89
            shortcut = c.find('&')
 
90
            if -1 != shortcut and (shortcut + 1) < len(c):
 
91
                help = c[:shortcut]
 
92
                help += '[' + c[shortcut + 1] + ']'
 
93
                help += c[(shortcut + 2):]
 
94
                shortcut = c[shortcut + 1]
 
95
            else:
 
96
                c = c.replace('&', '')
 
97
                shortcut = c[0]
 
98
                help = '[%s]%s' % (shortcut, c[1:])
 
99
            shortcut = shortcut.lower()
 
100
            if shortcut in self.alternatives:
 
101
                raise ValueError("duplicated shortcut: %s" % shortcut)
 
102
            self.alternatives[shortcut] = choice
 
103
            # Add redirections for default.
 
104
            if index == default:
 
105
                self.alternatives[''] = choice
 
106
                self.alternatives['\r'] = choice
 
107
            help_list.append(help)
 
108
            index += 1
 
109
 
 
110
        self.prompt = u'%s (%s): ' % (msg, ', '.join(help_list))
 
111
 
 
112
    def _getline(self):
 
113
        line = self.ui.stdin.readline()
 
114
        if '' == line:
 
115
            raise EOFError
 
116
        return line.strip()
 
117
 
 
118
    def _getchar(self):
 
119
        char = osutils.getchar()
 
120
        if char == chr(3): # INTR
 
121
            raise KeyboardInterrupt
 
122
        if char == chr(4): # EOF (^d, C-d)
 
123
            raise EOFError
 
124
        return char
 
125
 
 
126
    def interact(self):
 
127
        """Keep asking the user until a valid choice is made.
 
128
        """
 
129
        if self.line_based:
 
130
            getchoice = self._getline
 
131
        else:
 
132
            getchoice = self._getchar
 
133
        iter = 0
 
134
        while True:
 
135
            iter += 1
 
136
            if 1 == iter or self.line_based:
 
137
                self.ui.prompt(self.prompt)
 
138
            try:
 
139
                choice = getchoice()
 
140
            except EOFError:
 
141
                self.ui.stderr.write('\n')
 
142
                return None
 
143
            except KeyboardInterrupt:
 
144
                self.ui.stderr.write('\n')
 
145
                raise KeyboardInterrupt
 
146
            choice = choice.lower()
 
147
            if choice not in self.alternatives:
 
148
                # Not a valid choice, keep on asking.
 
149
                continue
 
150
            name, index = self.alternatives[choice]
 
151
            if self.echo_back:
 
152
                self.ui.stderr.write(name + '\n')
 
153
            return index
 
154
 
 
155
 
46
156
class TextUIFactory(UIFactory):
47
157
    """A UI factory for Text user interefaces."""
48
158
 
60
170
        self.stderr = stderr
61
171
        # paints progress, network activity, etc
62
172
        self._progress_view = self.make_progress_view()
63
 
        
 
173
 
 
174
    def choose(self, msg, choices, default=None):
 
175
        """Prompt the user for a list of alternatives.
 
176
 
 
177
        Support both line-based and char-based editing.
 
178
 
 
179
        In line-based mode, both the shortcut and full choice name are valid
 
180
        answers, e.g. for choose('prompt', '&yes\n&no'): 'y', ' Y ', ' yes',
 
181
        'YES ' are all valid input lines for choosing 'yes'.
 
182
 
 
183
        An empty line, when in line-based mode, or pressing enter in char-based
 
184
        mode will select the default choice (if any).
 
185
 
 
186
        Choice is echoed back if:
 
187
        - input is char-based; which means a controlling terminal is available,
 
188
          and osutils.getchar is used
 
189
        - input is line-based, and no controlling terminal is available
 
190
        """
 
191
 
 
192
        choose_ui = _ChooseUI(self, msg, choices, default)
 
193
        return choose_ui.interact()
 
194
 
64
195
    def be_quiet(self, state):
65
196
        if state and not self._quiet:
66
197
            self.clear_term()
78
209
        # to clear it.  We might need to separately check for the case of
79
210
        self._progress_view.clear()
80
211
 
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
212
    def get_integer(self, prompt):
94
213
        while True:
95
214
            self.prompt(prompt)
114
233
                password = password[:-1]
115
234
        return password
116
235
 
117
 
    def get_password(self, prompt='', **kwargs):
 
236
    def get_password(self, prompt=u'', **kwargs):
118
237
        """Prompt the user for a password.
119
238
 
120
239
        :param prompt: The prompt to present the user
153
272
        """Construct and return a new ProgressView subclass for this UI.
154
273
        """
155
274
        # with --quiet, never any progress view
156
 
        # <https://bugs.edge.launchpad.net/bzr/+bug/320035>.  Otherwise if the
 
275
        # <https://bugs.launchpad.net/bzr/+bug/320035>.  Otherwise if the
157
276
        # user specifically requests either text or no progress bars, always
158
277
        # do that.  otherwise, guess based on $TERM and tty presence.
159
278
        if self.is_quiet():
198
317
        :param kwargs: Dictionary of arguments to insert into the prompt,
199
318
            to allow UIs to reformat the prompt.
200
319
        """
 
320
        if type(prompt) != unicode:
 
321
            raise ValueError("prompt %r not a unicode string" % prompt)
201
322
        if kwargs:
202
323
            # See <https://launchpad.net/bugs/365891>
203
324
            prompt = prompt % kwargs
204
325
        prompt = prompt.encode(osutils.get_terminal_encoding(), 'replace')
205
326
        self.clear_term()
 
327
        self.stdout.flush()
206
328
        self.stderr.write(prompt)
207
329
 
208
330
    def report_transport_activity(self, transport, byte_count, direction):
229
351
 
230
352
    def show_warning(self, msg):
231
353
        self.clear_term()
 
354
        if isinstance(msg, unicode):
 
355
            te = osutils.get_terminal_encoding()
 
356
            msg = msg.encode(te, 'replace')
232
357
        self.stderr.write("bzr: warning: %s\n" % msg)
233
358
 
234
359
    def _progress_updated(self, task):
249
374
    def _progress_all_finished(self):
250
375
        self._progress_view.clear()
251
376
 
 
377
    def show_user_warning(self, warning_id, **message_args):
 
378
        """Show a text message to the user.
 
379
 
 
380
        Explicitly not for warnings about bzr apis, deprecations or internals.
 
381
        """
 
382
        # eventually trace.warning should migrate here, to avoid logging and
 
383
        # be easier to test; that has a lot of test fallout so for now just
 
384
        # new code can call this
 
385
        if warning_id not in self.suppressed_warnings:
 
386
            self.stderr.write(self.format_user_warning(warning_id, message_args) +
 
387
                '\n')
 
388
 
252
389
 
253
390
class TextProgressView(object):
254
391
    """Display of progress bar and other information on a tty.
285
422
        # correspond reliably to overall command progress
286
423
        self.enable_bar = False
287
424
 
 
425
    def _avail_width(self):
 
426
        # we need one extra space for terminals that wrap on last char
 
427
        w = osutils.terminal_width() 
 
428
        if w is None:
 
429
            return None
 
430
        else:
 
431
            return w - 1
 
432
 
288
433
    def _show_line(self, s):
289
434
        # sys.stderr.write("progress %r\n" % s)
290
 
        width = osutils.terminal_width()
 
435
        width = self._avail_width()
291
436
        if width is not None:
292
 
            # we need one extra space for terminals that wrap on last char
293
 
            width = width - 1
294
437
            s = '%-*.*s' % (width, width, s)
295
438
        self._term_file.write('\r' + s + '\r')
296
439
 
333
476
            return ''
334
477
 
335
478
    def _format_task(self, task):
 
479
        """Format task-specific parts of progress bar.
 
480
 
 
481
        :returns: (text_part, counter_part) both unicode strings.
 
482
        """
336
483
        if not task.show_count:
337
484
            s = ''
338
485
        elif task.current_cnt is not None and task.total_cnt is not None:
348
495
            t = t._parent_task
349
496
            if t.msg:
350
497
                m = t.msg + ':' + m
351
 
        return m + s
 
498
        return m, s
352
499
 
353
500
    def _render_line(self):
354
501
        bar_string = self._render_bar()
355
502
        if self._last_task:
356
 
            task_msg = self._format_task(self._last_task)
 
503
            task_part, counter_part = self._format_task(self._last_task)
357
504
        else:
358
 
            task_msg = ''
 
505
            task_part = counter_part = ''
359
506
        if self._last_task and not self._last_task.show_transport_activity:
360
507
            trans = ''
361
508
        else:
362
509
            trans = self._last_transport_msg
363
 
            if trans:
364
 
                trans += ' | '
365
 
        return (bar_string + trans + task_msg)
 
510
        # the bar separates the transport activity from the message, so even
 
511
        # if there's no bar or spinner, we must show something if both those
 
512
        # fields are present
 
513
        if (task_part or trans) and not bar_string:
 
514
            bar_string = '| '
 
515
        # preferentially truncate the task message if we don't have enough
 
516
        # space
 
517
        avail_width = self._avail_width()
 
518
        if avail_width is not None:
 
519
            # if terminal avail_width is unknown, don't truncate
 
520
            current_len = len(bar_string) + len(trans) + len(task_part) + len(counter_part)
 
521
            gap = current_len - avail_width
 
522
            if gap > 0:
 
523
                task_part = task_part[:-gap-2] + '..'
 
524
        s = trans + bar_string + task_part + counter_part
 
525
        if avail_width is not None:
 
526
            if len(s) < avail_width:
 
527
                s = s.ljust(avail_width)
 
528
            elif len(s) > avail_width:
 
529
                s = s[:avail_width]
 
530
        return s
366
531
 
367
532
    def _repaint(self):
368
533
        s = self._render_line()
424
589
            rate = (self._bytes_since_update
425
590
                    / (now - self._transport_update_time))
426
591
            # using base-10 units (see HACKING.txt).
427
 
            msg = ("%6dkB %5dkB/s" %
 
592
            msg = ("%6dkB %5dkB/s " %
428
593
                    (self._total_byte_count / 1000, int(rate) / 1000,))
429
594
            self._transport_update_time = now
430
595
            self._last_repaint = now