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
18
"""Text UI, write output to the console.
17
"""Text UI, write output to the console."""
21
19
from __future__ import absolute_import
27
from breezy.lazy_import import lazy_import
26
from ..lazy_import import lazy_import
28
27
lazy_import(globals(), """
33
31
from breezy import (
43
from breezy.ui import (
142
141
choice = getchoice()
144
self.ui.stderr.write('\n')
143
self.ui.stderr.write(u'\n')
146
145
except KeyboardInterrupt:
147
self.ui.stderr.write('\n')
148
raise KeyboardInterrupt
146
self.ui.stderr.write(u'\n')
149
148
choice = choice.lower()
150
149
if choice not in self.alternatives:
151
150
# Not a valid choice, keep on asking.
153
152
name, index = self.alternatives[choice]
154
153
if self.echo_back:
155
self.ui.stderr.write(name + '\n')
154
self.ui.stderr.write(name + u'\n')
165
164
class TextUIFactory(UIFactory):
166
165
"""A UI factory for Text user interfaces."""
172
"""Create a TextUIFactory.
167
def __init__(self, stdin, stdout, stderr):
168
"""Create a TextUIFactory."""
174
169
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...
177
170
self.stdin = stdin
178
171
self.stdout = stdout
179
172
self.stderr = stderr
301
291
return NullProgressView()
303
293
def _make_output_stream_explicit(self, 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)
294
return TextUIOutputStream(self, self.stdout, encoding, encoding_type)
323
296
def note(self, msg):
324
297
"""Write an already-formatted message, clearing the progress bar if necessary."""
328
301
def prompt(self, prompt, **kwargs):
329
302
"""Emit prompt on the CLI.
331
304
:param kwargs: Dictionary of arguments to insert into the prompt,
332
305
to allow UIs to reformat the prompt.
334
if type(prompt) != unicode:
307
if not isinstance(prompt, unicode):
335
308
raise ValueError("prompt %r not a unicode string" % prompt)
337
310
# See <https://launchpad.net/bugs/365891>
338
311
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')
346
312
self.clear_term()
347
313
self.stdout.flush()
348
314
self.stderr.write(prompt)
403
366
# be easier to test; that has a lot of test fallout so for now just
404
367
# new code can call this
405
368
if warning_id not in self.suppressed_warnings:
406
self.stderr.write(self.format_user_warning(warning_id, message_args) +
369
warning = self.format_user_warning(warning_id, message_args)
370
self.stderr.write(warning + '\n')
373
def pad_to_width(line, width, encoding_hint='ascii'):
374
"""Truncate or pad unicode line to width.
376
This is best-effort for now, and strings containing control codes or
377
non-ascii text may be cut and padded incorrectly.
379
s = line.encode(encoding_hint, 'replace')
380
return (b'%-*.*s' % (width, width, s)).decode(encoding_hint)
410
383
class TextProgressView(object):
411
384
"""Display of progress bar and other information on a tty.
413
This shows one line of text, including possibly a network indicator, spinner,
414
progress bar, message, etc.
386
This shows one line of text, including possibly a network indicator,
387
spinner, progress bar, message, etc.
416
389
One instance of this is created and held by the UI, and fed updates when a
417
390
task wants to be painted.
422
395
this only prints the stack from the nominated current task up to the root.
425
def __init__(self, term_file, encoding=None, errors="replace"):
398
def __init__(self, term_file, encoding=None, errors=None):
426
399
self._term_file = term_file
427
400
if encoding is None:
428
401
self._encoding = getattr(term_file, "encoding", None) or "ascii"
430
403
self._encoding = encoding
431
self._encoding_errors = errors
432
404
# true when there's output on the screen we may need to clear
433
405
self._have_output = False
434
406
self._last_transport_msg = ''
443
415
self._bytes_by_direction = {'unknown': 0, 'read': 0, 'write': 0}
444
416
self._first_byte_time = None
445
417
self._fraction = 0
446
# force the progress bar to be off, as at the moment it doesn't
418
# force the progress bar to be off, as at the moment it doesn't
447
419
# correspond reliably to overall command progress
448
420
self.enable_bar = False
450
422
def _avail_width(self):
451
423
# we need one extra space for terminals that wrap on last char
452
w = osutils.terminal_width()
424
w = osutils.terminal_width()
458
430
def _show_line(self, u):
459
s = u.encode(self._encoding, self._encoding_errors)
460
431
width = self._avail_width()
461
432
if width is not None:
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')
433
u = pad_to_width(u, width, encoding_hint=self._encoding)
434
self._term_file.write('\r' + u + '\r')
468
437
if self._have_output:
657
627
self._term_file.write(msg + '\n')
630
def _get_stream_encoding(stream):
631
encoding = config.GlobalStack().get('output_encoding')
633
encoding = getattr(stream, "encoding", None)
635
encoding = osutils.get_terminal_encoding(trace=True)
639
def _unwrap_stream(stream):
640
inner = getattr(stream, "buffer", None)
642
inner = getattr(stream, "stream", None)
646
def _wrap_in_stream(stream, encoding=None, errors='replace'):
648
encoding = _get_stream_encoding(stream)
649
encoded_stream = codecs.getreader(encoding)(stream, errors=errors)
650
encoded_stream.encoding = encoding
651
return encoded_stream
654
def _wrap_out_stream(stream, encoding=None, errors='replace'):
656
encoding = _get_stream_encoding(stream)
657
encoded_stream = codecs.getwriter(encoding)(stream, errors=errors)
658
encoded_stream.encoding = encoding
659
return encoded_stream
660
662
class TextUIOutputStream(object):
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
663
"""Decorates stream to interact better with progress and change encoding.
665
Before writing to the wrapped stream, progress is cleared. Callers must
666
ensure bulk output is terminated with a newline so progress won't overwrite
669
Additionally, the encoding and errors behaviour of the underlying stream
670
can be changed at this point. If errors is set to 'exact' raw bytes may be
671
written to the underlying stream.
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):
674
def __init__(self, ui_factory, stream, encoding=None, errors='strict'):
674
675
self.ui_factory = ui_factory
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)
676
# GZ 2017-05-21: Clean up semantics when callers are made saner.
677
inner = _unwrap_stream(stream)
678
self.raw_stream = None
679
if errors == "exact":
681
self.raw_stream = inner
683
self.wrapped_stream = stream
685
encoding = _get_stream_encoding(stream)
687
self.wrapped_stream = _wrap_out_stream(inner, encoding, errors)
689
encoding = self.wrapped_stream.encoding
690
self.encoding = encoding
693
def _write(self, to_write):
694
if isinstance(to_write, bytes):
696
to_write = to_write.decode(self.encoding, self.errors)
697
except UnicodeDecodeError:
698
self.raw_stream.write(to_write)
700
self.wrapped_stream.write(to_write)
682
703
self.ui_factory.clear_term()