/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 bzrlib/ui/text.py

More work on roundtrip push support.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2008, 2009 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
 
 
18
 
"""Text UI, write output to the console.
19
 
"""
20
 
 
21
 
import codecs
22
 
import getpass
23
 
import os
24
 
import sys
25
 
import time
26
 
import warnings
27
 
 
28
 
from bzrlib.lazy_import import lazy_import
29
 
lazy_import(globals(), """
30
 
from bzrlib import (
31
 
    debug,
32
 
    progress,
33
 
    osutils,
34
 
    symbol_versioning,
35
 
    )
36
 
 
37
 
""")
38
 
 
39
 
from bzrlib.ui import (
40
 
    UIFactory,
41
 
    NullProgressView,
42
 
    )
43
 
 
44
 
 
45
 
class TextUIFactory(UIFactory):
46
 
    """A UI factory for Text user interefaces."""
47
 
 
48
 
    def __init__(self,
49
 
                 stdin=None,
50
 
                 stdout=None,
51
 
                 stderr=None):
52
 
        """Create a TextUIFactory.
53
 
        """
54
 
        super(TextUIFactory, self).__init__()
55
 
        # TODO: there's no good reason not to pass all three streams, maybe we
56
 
        # should deprecate the default values...
57
 
        self.stdin = stdin
58
 
        self.stdout = stdout
59
 
        self.stderr = stderr
60
 
        # paints progress, network activity, etc
61
 
        self._progress_view = self.make_progress_view()
62
 
        
63
 
    def clear_term(self):
64
 
        """Prepare the terminal for output.
65
 
 
66
 
        This will, clear any progress bars, and leave the cursor at the
67
 
        leftmost position."""
68
 
        # XXX: If this is preparing to write to stdout, but that's for example
69
 
        # directed into a file rather than to the terminal, and the progress
70
 
        # bar _is_ going to the terminal, we shouldn't need
71
 
        # to clear it.  We might need to separately check for the case of
72
 
        self._progress_view.clear()
73
 
 
74
 
    def get_boolean(self, prompt):
75
 
        while True:
76
 
            self.prompt(prompt + "? [y/n]: ")
77
 
            line = self.stdin.readline().lower()
78
 
            if line in ('y\n', 'yes\n'):
79
 
                return True
80
 
            elif line in ('n\n', 'no\n'):
81
 
                return False
82
 
            elif line in ('', None):
83
 
                # end-of-file; possibly should raise an error here instead
84
 
                return None
85
 
 
86
 
    def get_integer(self, prompt):
87
 
        while True:
88
 
            self.prompt(prompt)
89
 
            line = self.stdin.readline()
90
 
            try:
91
 
                return int(line)
92
 
            except ValueError:
93
 
                pass
94
 
 
95
 
    def get_non_echoed_password(self):
96
 
        isatty = getattr(self.stdin, 'isatty', None)
97
 
        if isatty is not None and isatty():
98
 
            # getpass() ensure the password is not echoed and other
99
 
            # cross-platform niceties
100
 
            password = getpass.getpass('')
101
 
        else:
102
 
            # echo doesn't make sense without a terminal
103
 
            password = self.stdin.readline()
104
 
            if not password:
105
 
                password = None
106
 
            elif password[-1] == '\n':
107
 
                password = password[:-1]
108
 
        return password
109
 
 
110
 
    def get_password(self, prompt='', **kwargs):
111
 
        """Prompt the user for a password.
112
 
 
113
 
        :param prompt: The prompt to present the user
114
 
        :param kwargs: Arguments which will be expanded into the prompt.
115
 
                       This lets front ends display different things if
116
 
                       they so choose.
117
 
        :return: The password string, return None if the user
118
 
                 canceled the request.
119
 
        """
120
 
        prompt += ': '
121
 
        self.prompt(prompt, **kwargs)
122
 
        # There's currently no way to say 'i decline to enter a password'
123
 
        # as opposed to 'my password is empty' -- does it matter?
124
 
        return self.get_non_echoed_password()
125
 
 
126
 
    def get_username(self, prompt, **kwargs):
127
 
        """Prompt the user for a username.
128
 
 
129
 
        :param prompt: The prompt to present the user
130
 
        :param kwargs: Arguments which will be expanded into the prompt.
131
 
                       This lets front ends display different things if
132
 
                       they so choose.
133
 
        :return: The username string, return None if the user
134
 
                 canceled the request.
135
 
        """
136
 
        prompt += ': '
137
 
        self.prompt(prompt, **kwargs)
138
 
        username = self.stdin.readline()
139
 
        if not username:
140
 
            username = None
141
 
        elif username[-1] == '\n':
142
 
            username = username[:-1]
143
 
        return username
144
 
 
145
 
    def make_progress_view(self):
146
 
        """Construct and return a new ProgressView subclass for this UI.
147
 
        """
148
 
        # if the user specifically requests either text or no progress bars,
149
 
        # always do that.  otherwise, guess based on $TERM and tty presence.
150
 
        if os.environ.get('BZR_PROGRESS_BAR') == 'text':
151
 
            return TextProgressView(self.stderr)
152
 
        elif os.environ.get('BZR_PROGRESS_BAR') == 'none':
153
 
            return NullProgressView()
154
 
        elif progress._supports_progress(self.stderr):
155
 
            return TextProgressView(self.stderr)
156
 
        else:
157
 
            return NullProgressView()
158
 
 
159
 
    def _make_output_stream_explicit(self, encoding, encoding_type):
160
 
        if encoding_type == 'exact':
161
 
            # force sys.stdout to be binary stream on win32; 
162
 
            # NB: this leaves the file set in that mode; may cause problems if
163
 
            # one process tries to do binary and then text output
164
 
            if sys.platform == 'win32':
165
 
                fileno = getattr(self.stdout, 'fileno', None)
166
 
                if fileno:
167
 
                    import msvcrt
168
 
                    msvcrt.setmode(fileno(), os.O_BINARY)
169
 
            return TextUIOutputStream(self, self.stdout)
170
 
        else:
171
 
            encoded_stdout = codecs.getwriter(encoding)(self.stdout,
172
 
                errors=encoding_type)
173
 
            # For whatever reason codecs.getwriter() does not advertise its encoding
174
 
            # it just returns the encoding of the wrapped file, which is completely
175
 
            # bogus. So set the attribute, so we can find the correct encoding later.
176
 
            encoded_stdout.encoding = encoding
177
 
            return TextUIOutputStream(self, encoded_stdout)
178
 
 
179
 
    def note(self, msg):
180
 
        """Write an already-formatted message, clearing the progress bar if necessary."""
181
 
        self.clear_term()
182
 
        self.stdout.write(msg + '\n')
183
 
 
184
 
    def prompt(self, prompt, **kwargs):
185
 
        """Emit prompt on the CLI.
186
 
        
187
 
        :param kwargs: Dictionary of arguments to insert into the prompt,
188
 
            to allow UIs to reformat the prompt.
189
 
        """
190
 
        if kwargs:
191
 
            # See <https://launchpad.net/bugs/365891>
192
 
            prompt = prompt % kwargs
193
 
        prompt = prompt.encode(osutils.get_terminal_encoding(), 'replace')
194
 
        self.clear_term()
195
 
        self.stderr.write(prompt)
196
 
 
197
 
    def report_transport_activity(self, transport, byte_count, direction):
198
 
        """Called by transports as they do IO.
199
 
 
200
 
        This may update a progress bar, spinner, or similar display.
201
 
        By default it does nothing.
202
 
        """
203
 
        self._progress_view.show_transport_activity(transport,
204
 
            direction, byte_count)
205
 
 
206
 
    def show_error(self, msg):
207
 
        self.clear_term()
208
 
        self.stderr.write("bzr: error: %s\n" % msg)
209
 
 
210
 
    def show_message(self, msg):
211
 
        self.note(msg)
212
 
 
213
 
    def show_warning(self, msg):
214
 
        self.clear_term()
215
 
        self.stderr.write("bzr: warning: %s\n" % msg)
216
 
 
217
 
    def _progress_updated(self, task):
218
 
        """A task has been updated and wants to be displayed.
219
 
        """
220
 
        if not self._task_stack:
221
 
            warnings.warn("%r updated but no tasks are active" %
222
 
                (task,))
223
 
        elif task != self._task_stack[-1]:
224
 
            warnings.warn("%r is not the top progress task %r" %
225
 
                (task, self._task_stack[-1]))
226
 
        self._progress_view.show_progress(task)
227
 
 
228
 
    def _progress_all_finished(self):
229
 
        self._progress_view.clear()
230
 
 
231
 
 
232
 
class TextProgressView(object):
233
 
    """Display of progress bar and other information on a tty.
234
 
 
235
 
    This shows one line of text, including possibly a network indicator, spinner,
236
 
    progress bar, message, etc.
237
 
 
238
 
    One instance of this is created and held by the UI, and fed updates when a
239
 
    task wants to be painted.
240
 
 
241
 
    Transports feed data to this through the ui_factory object.
242
 
 
243
 
    The Progress views can comprise a tree with _parent_task pointers, but
244
 
    this only prints the stack from the nominated current task up to the root.
245
 
    """
246
 
 
247
 
    def __init__(self, term_file):
248
 
        self._term_file = term_file
249
 
        # true when there's output on the screen we may need to clear
250
 
        self._have_output = False
251
 
        self._last_transport_msg = ''
252
 
        self._spin_pos = 0
253
 
        # time we last repainted the screen
254
 
        self._last_repaint = 0
255
 
        # time we last got information about transport activity
256
 
        self._transport_update_time = 0
257
 
        self._last_task = None
258
 
        self._total_byte_count = 0
259
 
        self._bytes_since_update = 0
260
 
        self._fraction = 0
261
 
        # force the progress bar to be off, as at the moment it doesn't 
262
 
        # correspond reliably to overall command progress
263
 
        self.enable_bar = False
264
 
 
265
 
    def _show_line(self, s):
266
 
        # sys.stderr.write("progress %r\n" % s)
267
 
        width = osutils.terminal_width()
268
 
        if width is not None:
269
 
            # we need one extra space for terminals that wrap on last char
270
 
            width = width - 1
271
 
            s = '%-*.*s' % (width, width, s)
272
 
        self._term_file.write('\r' + s + '\r')
273
 
 
274
 
    def clear(self):
275
 
        if self._have_output:
276
 
            self._show_line('')
277
 
        self._have_output = False
278
 
 
279
 
    def _render_bar(self):
280
 
        # return a string for the progress bar itself
281
 
        if self.enable_bar and (
282
 
            (self._last_task is None) or self._last_task.show_bar):
283
 
            # If there's no task object, we show space for the bar anyhow.
284
 
            # That's because most invocations of bzr will end showing progress
285
 
            # at some point, though perhaps only after doing some initial IO.
286
 
            # It looks better to draw the progress bar initially rather than
287
 
            # to have what looks like an incomplete progress bar.
288
 
            spin_str =  r'/-\|'[self._spin_pos % 4]
289
 
            self._spin_pos += 1
290
 
            cols = 20
291
 
            if self._last_task is None:
292
 
                completion_fraction = 0
293
 
                self._fraction = 0
294
 
            else:
295
 
                completion_fraction = \
296
 
                    self._last_task._overall_completion_fraction() or 0
297
 
            if (completion_fraction < self._fraction and 'progress' in
298
 
                debug.debug_flags):
299
 
                import pdb;pdb.set_trace()
300
 
            self._fraction = completion_fraction
301
 
            markers = int(round(float(cols) * completion_fraction)) - 1
302
 
            bar_str = '[' + ('#' * markers + spin_str).ljust(cols) + '] '
303
 
            return bar_str
304
 
        elif self._last_task.show_spinner:
305
 
            # The last task wanted just a spinner, no bar
306
 
            spin_str =  r'/-\|'[self._spin_pos % 4]
307
 
            self._spin_pos += 1
308
 
            return spin_str + ' '
309
 
        else:
310
 
            return ''
311
 
 
312
 
    def _format_task(self, task):
313
 
        if not task.show_count:
314
 
            s = ''
315
 
        elif task.current_cnt is not None and task.total_cnt is not None:
316
 
            s = ' %d/%d' % (task.current_cnt, task.total_cnt)
317
 
        elif task.current_cnt is not None:
318
 
            s = ' %d' % (task.current_cnt)
319
 
        else:
320
 
            s = ''
321
 
        # compose all the parent messages
322
 
        t = task
323
 
        m = task.msg
324
 
        while t._parent_task:
325
 
            t = t._parent_task
326
 
            if t.msg:
327
 
                m = t.msg + ':' + m
328
 
        return m + s
329
 
 
330
 
    def _render_line(self):
331
 
        bar_string = self._render_bar()
332
 
        if self._last_task:
333
 
            task_msg = self._format_task(self._last_task)
334
 
        else:
335
 
            task_msg = ''
336
 
        if self._last_task and not self._last_task.show_transport_activity:
337
 
            trans = ''
338
 
        else:
339
 
            trans = self._last_transport_msg
340
 
            if trans:
341
 
                trans += ' | '
342
 
        return (bar_string + trans + task_msg)
343
 
 
344
 
    def _repaint(self):
345
 
        s = self._render_line()
346
 
        self._show_line(s)
347
 
        self._have_output = True
348
 
 
349
 
    def show_progress(self, task):
350
 
        """Called by the task object when it has changed.
351
 
        
352
 
        :param task: The top task object; its parents are also included 
353
 
            by following links.
354
 
        """
355
 
        must_update = task is not self._last_task
356
 
        self._last_task = task
357
 
        now = time.time()
358
 
        if (not must_update) and (now < self._last_repaint + task.update_latency):
359
 
            return
360
 
        if now > self._transport_update_time + 10:
361
 
            # no recent activity; expire it
362
 
            self._last_transport_msg = ''
363
 
        self._last_repaint = now
364
 
        self._repaint()
365
 
 
366
 
    def show_transport_activity(self, transport, direction, byte_count):
367
 
        """Called by transports via the ui_factory, as they do IO.
368
 
 
369
 
        This may update a progress bar, spinner, or similar display.
370
 
        By default it does nothing.
371
 
        """
372
 
        # XXX: Probably there should be a transport activity model, and that
373
 
        # too should be seen by the progress view, rather than being poked in
374
 
        # here.
375
 
        if not self._have_output:
376
 
            # As a workaround for <https://launchpad.net/bugs/321935> we only
377
 
            # show transport activity when there's already a progress bar
378
 
            # shown, which time the application code is expected to know to
379
 
            # clear off the progress bar when it's going to send some other
380
 
            # output.  Eventually it would be nice to have that automatically
381
 
            # synchronized.
382
 
            return
383
 
        self._total_byte_count += byte_count
384
 
        self._bytes_since_update += byte_count
385
 
        now = time.time()
386
 
        if self._total_byte_count < 2000:
387
 
            # a little resistance at first, so it doesn't stay stuck at 0
388
 
            # while connecting...
389
 
            return
390
 
        if self._transport_update_time is None:
391
 
            self._transport_update_time = now
392
 
        elif now >= (self._transport_update_time + 0.5):
393
 
            # guard against clock stepping backwards, and don't update too
394
 
            # often
395
 
            rate = self._bytes_since_update / (now - self._transport_update_time)
396
 
            msg = ("%6dKB %5dKB/s" %
397
 
                    (self._total_byte_count>>10, int(rate)>>10,))
398
 
            self._transport_update_time = now
399
 
            self._last_repaint = now
400
 
            self._bytes_since_update = 0
401
 
            self._last_transport_msg = msg
402
 
            self._repaint()
403
 
 
404
 
 
405
 
class TextUIOutputStream(object):
406
 
    """Decorates an output stream so that the terminal is cleared before writing.
407
 
 
408
 
    This is supposed to ensure that the progress bar does not conflict with bulk
409
 
    text output.
410
 
    """
411
 
    # XXX: this does not handle the case of writing part of a line, then doing
412
 
    # progress bar output: the progress bar will probably write over it.
413
 
    # one option is just to buffer that text until we have a full line;
414
 
    # another is to save and restore it
415
 
 
416
 
    # XXX: might need to wrap more methods
417
 
 
418
 
    def __init__(self, ui_factory, wrapped_stream):
419
 
        self.ui_factory = ui_factory
420
 
        self.wrapped_stream = wrapped_stream
421
 
        # this does no transcoding, but it must expose the underlying encoding
422
 
        # because some callers need to know what can be written - see for
423
 
        # example unescape_for_display.
424
 
        self.encoding = getattr(wrapped_stream, 'encoding', None)
425
 
 
426
 
    def flush(self):
427
 
        self.ui_factory.clear_term()
428
 
        self.wrapped_stream.flush()
429
 
 
430
 
    def write(self, to_write):
431
 
        self.ui_factory.clear_term()
432
 
        self.wrapped_stream.write(to_write)
433
 
 
434
 
    def writelines(self, lines):
435
 
        self.ui_factory.clear_term()
436
 
        self.wrapped_stream.writelines(lines)