14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
"""Text UI, write output to the console."""
18
"""Text UI, write output to the console.
19
21
from __future__ import absolute_import
26
from ..lazy_import import lazy_import
27
from bzrlib.lazy_import import lazy_import
27
28
lazy_import(globals(), """
42
from ..sixish import (
43
from bzrlib.ui import (
61
59
def _setup_mode(self):
62
60
"""Setup input mode (line-based, char-based) and echo-back.
64
Line-based input is used if the BRZ_TEXTUI_INPUT environment
62
Line-based input is used if the BZR_TEXTUI_INPUT environment
65
63
variable is set to 'line-based', or if there is no controlling
68
is_tty = self.ui.raw_stdin.isatty()
69
if (os.environ.get('BRZ_TEXTUI_INPUT') != 'line-based' and
70
self.ui.raw_stdin == sys.stdin and is_tty):
66
if os.environ.get('BZR_TEXTUI_INPUT') != 'line-based' and \
67
self.ui.stdin == sys.stdin and self.ui.stdin.isatty():
71
68
self.line_based = False
72
69
self.echo_back = True
74
71
self.line_based = True
75
self.echo_back = not is_tty
72
self.echo_back = not self.ui.stdin.isatty()
77
74
def _build_alternatives(self, msg, choices, default):
78
75
"""Parse choices string.
147
142
choice = getchoice()
149
self.ui.stderr.write(u'\n')
144
self.ui.stderr.write('\n')
151
146
except KeyboardInterrupt:
152
self.ui.stderr.write(u'\n')
147
self.ui.stderr.write('\n')
148
raise KeyboardInterrupt
154
149
choice = choice.lower()
155
150
if choice not in self.alternatives:
156
151
# Not a valid choice, keep on asking.
158
153
name, index = self.alternatives[choice]
159
154
if self.echo_back:
160
self.ui.stderr.write(name + u'\n')
155
self.ui.stderr.write(name + '\n')
164
159
opt_progress_bar = config.Option(
165
160
'progress_bar', help='Progress bar type.',
166
default_from_env=['BRZ_PROGRESS_BAR'], default=None,
161
default_from_env=['BZR_PROGRESS_BAR'], default=None,
170
165
class TextUIFactory(UIFactory):
171
166
"""A UI factory for Text user interfaces."""
173
def __init__(self, stdin, stdout, stderr):
174
"""Create a TextUIFactory."""
172
"""Create a TextUIFactory.
175
174
super(TextUIFactory, self).__init__()
175
# TODO: there's no good reason not to pass all three streams, maybe we
176
# should deprecate the default values...
176
177
self.stdin = stdin
177
178
self.stdout = stdout
178
179
self.stderr = stderr
179
self._progress_view = NullProgressView()
182
# Choose default encoding and handle py2/3 differences
183
self._setup_streams()
184
180
# paints progress, network activity, etc
185
181
self._progress_view = self.make_progress_view()
188
def _setup_streams(self):
189
self.raw_stdin = _unwrap_stream(self.stdin)
190
self.stdin = _wrap_in_stream(self.raw_stdin)
191
self.raw_stdout = _unwrap_stream(self.stdout)
192
self.stdout = _wrap_out_stream(self.raw_stdout)
193
self.raw_stderr = _unwrap_stream(self.stderr)
194
self.stderr = _wrap_out_stream(self.raw_stderr)
196
183
def choose(self, msg, choices, default=None):
197
184
"""Prompt the user for a list of alternatives.
311
301
return NullProgressView()
313
303
def _make_output_stream_explicit(self, encoding, encoding_type):
314
return TextUIOutputStream(self, self.stdout, encoding, encoding_type)
304
if encoding_type == 'exact':
305
# force sys.stdout to be binary stream on win32;
306
# NB: this leaves the file set in that mode; may cause problems if
307
# one process tries to do binary and then text output
308
if sys.platform == 'win32':
309
fileno = getattr(self.stdout, 'fileno', None)
312
msvcrt.setmode(fileno(), os.O_BINARY)
313
return TextUIOutputStream(self, self.stdout)
315
encoded_stdout = codecs.getwriter(encoding)(self.stdout,
316
errors=encoding_type)
317
# For whatever reason codecs.getwriter() does not advertise its encoding
318
# it just returns the encoding of the wrapped file, which is completely
319
# bogus. So set the attribute, so we can find the correct encoding later.
320
encoded_stdout.encoding = encoding
321
return TextUIOutputStream(self, encoded_stdout)
316
323
def note(self, msg):
317
324
"""Write an already-formatted message, clearing the progress bar if necessary."""
321
328
def prompt(self, prompt, **kwargs):
322
329
"""Emit prompt on the CLI.
324
331
:param kwargs: Dictionary of arguments to insert into the prompt,
325
332
to allow UIs to reformat the prompt.
327
if not isinstance(prompt, text_type):
334
if type(prompt) != unicode:
328
335
raise ValueError("prompt %r not a unicode string" % prompt)
330
337
# See <https://launchpad.net/bugs/365891>
331
338
prompt = prompt % kwargs
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')
332
346
self.clear_term()
333
347
self.stdout.flush()
334
348
self.stderr.write(prompt)
386
403
# be easier to test; that has a lot of test fallout so for now just
387
404
# new code can call this
388
405
if warning_id not in self.suppressed_warnings:
389
warning = self.format_user_warning(warning_id, message_args)
390
self.stderr.write(warning + '\n')
393
def pad_to_width(line, width, encoding_hint='ascii'):
394
"""Truncate or pad unicode line to width.
396
This is best-effort for now, and strings containing control codes or
397
non-ascii text may be cut and padded incorrectly.
399
s = line.encode(encoding_hint, 'replace')
400
return (b'%-*.*s' % (width, width, s)).decode(encoding_hint)
406
self.stderr.write(self.format_user_warning(warning_id, message_args) +
403
410
class TextProgressView(object):
404
411
"""Display of progress bar and other information on a tty.
406
This shows one line of text, including possibly a network indicator,
407
spinner, progress bar, message, etc.
413
This shows one line of text, including possibly a network indicator, spinner,
414
progress bar, message, etc.
409
416
One instance of this is created and held by the UI, and fed updates when a
410
417
task wants to be painted.
415
422
this only prints the stack from the nominated current task up to the root.
418
def __init__(self, term_file, encoding=None, errors=None):
425
def __init__(self, term_file, encoding=None, errors="replace"):
419
426
self._term_file = term_file
420
427
if encoding is None:
421
428
self._encoding = getattr(term_file, "encoding", None) or "ascii"
423
430
self._encoding = encoding
431
self._encoding_errors = errors
424
432
# true when there's output on the screen we may need to clear
425
433
self._have_output = False
426
434
self._last_transport_msg = ''
435
443
self._bytes_by_direction = {'unknown': 0, 'read': 0, 'write': 0}
436
444
self._first_byte_time = None
437
445
self._fraction = 0
438
# force the progress bar to be off, as at the moment it doesn't
446
# force the progress bar to be off, as at the moment it doesn't
439
447
# correspond reliably to overall command progress
440
448
self.enable_bar = False
442
450
def _avail_width(self):
443
451
# we need one extra space for terminals that wrap on last char
444
w = osutils.terminal_width()
452
w = osutils.terminal_width()
450
458
def _show_line(self, u):
459
s = u.encode(self._encoding, self._encoding_errors)
451
460
width = self._avail_width()
452
461
if width is not None:
453
u = pad_to_width(u, width, encoding_hint=self._encoding)
454
self._term_file.write('\r' + u + '\r')
462
# GZ 2012-03-28: Counting bytes is wrong for calculating width of
463
# text but better than counting codepoints.
464
s = '%-*.*s' % (width, width, s)
465
self._term_file.write('\r' + s + '\r')
457
468
if self._have_output:
647
657
self._term_file.write(msg + '\n')
650
def _get_stream_encoding(stream):
651
encoding = config.GlobalStack().get('output_encoding')
653
encoding = getattr(stream, "encoding", None)
655
encoding = osutils.get_terminal_encoding(trace=True)
659
def _unwrap_stream(stream):
660
inner = getattr(stream, "buffer", None)
662
inner = getattr(stream, "stream", stream)
666
def _wrap_in_stream(stream, encoding=None, errors='replace'):
668
encoding = _get_stream_encoding(stream)
669
encoded_stream = codecs.getreader(encoding)(stream, errors=errors)
670
encoded_stream.encoding = encoding
671
return encoded_stream
674
def _wrap_out_stream(stream, encoding=None, errors='replace'):
676
encoding = _get_stream_encoding(stream)
677
encoded_stream = codecs.getwriter(encoding)(stream, errors=errors)
678
encoded_stream.encoding = encoding
679
return encoded_stream
682
660
class TextUIOutputStream(object):
683
"""Decorates stream to interact better with progress and change encoding.
685
Before writing to the wrapped stream, progress is cleared. Callers must
686
ensure bulk output is terminated with a newline so progress won't overwrite
689
Additionally, the encoding and errors behaviour of the underlying stream
690
can be changed at this point. If errors is set to 'exact' raw bytes may be
691
written to the underlying stream.
661
"""Decorates an output stream so that the terminal is cleared before writing.
663
This is supposed to ensure that the progress bar does not conflict with bulk
694
def __init__(self, ui_factory, stream, encoding=None, errors='strict'):
666
# XXX: this does not handle the case of writing part of a line, then doing
667
# progress bar output: the progress bar will probably write over it.
668
# one option is just to buffer that text until we have a full line;
669
# another is to save and restore it
671
# XXX: might need to wrap more methods
673
def __init__(self, ui_factory, wrapped_stream):
695
674
self.ui_factory = ui_factory
696
# GZ 2017-05-21: Clean up semantics when callers are made saner.
697
inner = _unwrap_stream(stream)
698
self.raw_stream = None
699
if errors == "exact":
701
self.raw_stream = inner
703
self.wrapped_stream = stream
705
encoding = _get_stream_encoding(stream)
707
self.wrapped_stream = _wrap_out_stream(inner, encoding, errors)
709
encoding = self.wrapped_stream.encoding
710
self.encoding = encoding
713
def _write(self, to_write):
714
if isinstance(to_write, bytes):
716
to_write = to_write.decode(self.encoding, self.errors)
717
except UnicodeDecodeError:
718
self.raw_stream.write(to_write)
720
self.wrapped_stream.write(to_write)
675
self.wrapped_stream = wrapped_stream
676
# this does no transcoding, but it must expose the underlying encoding
677
# because some callers need to know what can be written - see for
678
# example unescape_for_display.
679
self.encoding = getattr(wrapped_stream, 'encoding', None)
723
682
self.ui_factory.clear_term()