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.
21
from __future__ import absolute_import
17
"""Text UI, write output to the console."""
27
from bzrlib.lazy_import import lazy_import
25
from ..lazy_import import lazy_import
28
26
lazy_import(globals(), """
43
from bzrlib.ui import (
59
57
def _setup_mode(self):
60
58
"""Setup input mode (line-based, char-based) and echo-back.
62
Line-based input is used if the BZR_TEXTUI_INPUT environment
60
Line-based input is used if the BRZ_TEXTUI_INPUT environment
63
61
variable is set to 'line-based', or if there is no controlling
66
if os.environ.get('BZR_TEXTUI_INPUT') != 'line-based' and \
67
self.ui.stdin == sys.stdin and self.ui.stdin.isatty():
64
is_tty = self.ui.raw_stdin.isatty()
65
if (os.environ.get('BRZ_TEXTUI_INPUT') != 'line-based' and
66
self.ui.raw_stdin == _unwrap_stream(sys.stdin) and is_tty):
68
67
self.line_based = False
69
68
self.echo_back = True
71
70
self.line_based = True
72
self.echo_back = not self.ui.stdin.isatty()
71
self.echo_back = not is_tty
74
73
def _build_alternatives(self, msg, choices, default):
75
74
"""Parse choices string.
142
143
choice = getchoice()
144
self.ui.stderr.write('\n')
145
self.ui.stderr.write(u'\n')
146
147
except KeyboardInterrupt:
147
self.ui.stderr.write('\n')
148
raise KeyboardInterrupt
148
self.ui.stderr.write(u'\n')
149
150
choice = choice.lower()
150
151
if choice not in self.alternatives:
151
152
# Not a valid choice, keep on asking.
153
154
name, index = self.alternatives[choice]
154
155
if self.echo_back:
155
self.ui.stderr.write(name + '\n')
156
self.ui.stderr.write(name + u'\n')
159
160
opt_progress_bar = config.Option(
160
161
'progress_bar', help='Progress bar type.',
161
default_from_env=['BZR_PROGRESS_BAR'], default=None,
162
default_from_env=['BRZ_PROGRESS_BAR'], default=None,
165
166
class TextUIFactory(UIFactory):
166
167
"""A UI factory for Text user interfaces."""
172
"""Create a TextUIFactory.
169
def __init__(self, stdin, stdout, stderr):
170
"""Create a TextUIFactory."""
174
171
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
172
self.stdin = stdin
178
173
self.stdout = stdout
179
174
self.stderr = stderr
175
self._progress_view = NullProgressView()
178
# Choose default encoding and handle py2/3 differences
179
self._setup_streams()
180
180
# paints progress, network activity, etc
181
181
self._progress_view = self.make_progress_view()
184
def _setup_streams(self):
185
self.raw_stdin = _unwrap_stream(self.stdin)
186
self.stdin = _wrap_in_stream(self.raw_stdin)
187
self.raw_stdout = _unwrap_stream(self.stdout)
188
self.stdout = _wrap_out_stream(self.raw_stdout)
189
self.raw_stderr = _unwrap_stream(self.stderr)
190
self.stderr = _wrap_out_stream(self.raw_stderr)
183
192
def choose(self, msg, choices, default=None):
184
193
"""Prompt the user for a list of alternatives.
292
298
if self.is_quiet():
293
299
return NullProgressView()
294
300
pb_type = config.GlobalStack().get('progress_bar')
295
if pb_type == 'none': # Explicit requirement
301
if pb_type == 'none': # Explicit requirement
296
302
return NullProgressView()
297
if (pb_type == 'text' # Explicit requirement
298
or progress._supports_progress(self.stderr)): # Guess
303
if (pb_type == 'text' or # Explicit requirement
304
progress._supports_progress(self.stderr)): # Guess
299
305
return TextProgressView(self.stderr)
300
306
# No explicit requirement and no successful guess
301
307
return NullProgressView()
303
309
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)
310
return TextUIOutputStream(self, self.stdout, encoding, encoding_type)
323
312
def note(self, msg):
324
313
"""Write an already-formatted message, clearing the progress bar if necessary."""
328
317
def prompt(self, prompt, **kwargs):
329
318
"""Emit prompt on the CLI.
331
320
:param kwargs: Dictionary of arguments to insert into the prompt,
332
321
to allow UIs to reformat the prompt.
334
if type(prompt) != unicode:
323
if not isinstance(prompt, str):
335
324
raise ValueError("prompt %r not a unicode string" % prompt)
337
326
# See <https://launchpad.net/bugs/365891>
338
327
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
328
self.clear_term()
347
329
self.stdout.flush()
348
330
self.stderr.write(prompt)
350
333
def report_transport_activity(self, transport, byte_count, direction):
351
334
"""Called by transports as they do IO.
403
383
# be easier to test; that has a lot of test fallout so for now just
404
384
# new code can call this
405
385
if warning_id not in self.suppressed_warnings:
406
self.stderr.write(self.format_user_warning(warning_id, message_args) +
386
warning = self.format_user_warning(warning_id, message_args)
387
self.stderr.write(warning + '\n')
390
def pad_to_width(line, width, encoding_hint='ascii'):
391
"""Truncate or pad unicode line to width.
393
This is best-effort for now, and strings containing control codes or
394
non-ascii text may be cut and padded incorrectly.
396
s = line.encode(encoding_hint, 'replace')
397
return (b'%-*.*s' % (width, width, s)).decode(encoding_hint)
410
400
class TextProgressView(object):
411
401
"""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.
403
This shows one line of text, including possibly a network indicator,
404
spinner, progress bar, message, etc.
416
406
One instance of this is created and held by the UI, and fed updates when a
417
407
task wants to be painted.
422
412
this only prints the stack from the nominated current task up to the root.
425
def __init__(self, term_file, encoding=None, errors="replace"):
415
def __init__(self, term_file, encoding=None, errors=None):
426
416
self._term_file = term_file
427
417
if encoding is None:
428
418
self._encoding = getattr(term_file, "encoding", None) or "ascii"
430
420
self._encoding = encoding
431
self._encoding_errors = errors
432
421
# true when there's output on the screen we may need to clear
433
422
self._have_output = False
434
423
self._last_transport_msg = ''
443
432
self._bytes_by_direction = {'unknown': 0, 'read': 0, 'write': 0}
444
433
self._first_byte_time = None
445
434
self._fraction = 0
446
# force the progress bar to be off, as at the moment it doesn't
435
# force the progress bar to be off, as at the moment it doesn't
447
436
# correspond reliably to overall command progress
448
437
self.enable_bar = False
450
439
def _avail_width(self):
451
440
# we need one extra space for terminals that wrap on last char
452
w = osutils.terminal_width()
441
w = osutils.terminal_width()
458
447
def _show_line(self, u):
459
s = u.encode(self._encoding, self._encoding_errors)
460
448
width = self._avail_width()
461
449
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')
450
u = pad_to_width(u, width, encoding_hint=self._encoding)
451
self._term_file.write('\r' + u + '\r')
468
454
if self._have_output:
472
458
def _render_bar(self):
473
459
# return a string for the progress bar itself
474
460
if self.enable_bar and (
475
(self._last_task is None) or self._last_task.show_bar):
461
(self._last_task is None) or self._last_task.show_bar):
476
462
# If there's no task object, we show space for the bar anyhow.
477
463
# That's because most invocations of bzr will end showing progress
478
464
# at some point, though perhaps only after doing some initial IO.
479
465
# It looks better to draw the progress bar initially rather than
480
466
# to have what looks like an incomplete progress bar.
481
spin_str = r'/-\|'[self._spin_pos % 4]
467
spin_str = r'/-\|'[self._spin_pos % 4]
482
468
self._spin_pos += 1
484
470
if self._last_task is None:
488
474
completion_fraction = \
489
475
self._last_task._overall_completion_fraction() or 0
490
476
if (completion_fraction < self._fraction and 'progress' in
492
import pdb;pdb.set_trace()
493
479
self._fraction = completion_fraction
494
480
markers = int(round(float(cols) * completion_fraction)) - 1
495
481
bar_str = '[' + ('#' * markers + spin_str).ljust(cols) + '] '
497
483
elif (self._last_task is None) or self._last_task.show_spinner:
498
484
# The last task wanted just a spinner, no bar
499
spin_str = r'/-\|'[self._spin_pos % 4]
485
spin_str = r'/-\|'[self._spin_pos % 4]
500
486
self._spin_pos += 1
501
487
return spin_str + ' '
544
530
avail_width = self._avail_width()
545
531
if avail_width is not None:
546
532
# if terminal avail_width is unknown, don't truncate
547
current_len = len(bar_string) + len(trans) + len(task_part) + len(counter_part)
533
current_len = len(bar_string) + len(trans) + \
534
len(task_part) + len(counter_part)
535
# GZ 2017-04-22: Should measure and truncate task_part properly
548
536
gap = current_len - avail_width
550
task_part = task_part[:-gap-2] + '..'
538
task_part = task_part[:-gap - 2] + '..'
551
539
s = trans + bar_string + task_part + counter_part
552
540
if avail_width is not None:
553
541
if len(s) < avail_width:
613
601
elif now >= (self._transport_update_time + 0.5):
614
602
# guard against clock stepping backwards, and don't update too
616
rate = (self._bytes_since_update
617
/ (now - self._transport_update_time))
604
rate = (self._bytes_since_update /
605
(now - self._transport_update_time))
618
606
# using base-10 units (see HACKING.txt).
619
607
msg = ("%6dkB %5dkB/s " %
620
(self._total_byte_count / 1000, int(rate) / 1000,))
608
(self._total_byte_count / 1000, int(rate) / 1000,))
621
609
self._transport_update_time = now
622
610
self._last_repaint = now
623
611
self._bytes_since_update = 0
657
645
self._term_file.write(msg + '\n')
648
def _get_stream_encoding(stream):
649
encoding = config.GlobalStack().get('output_encoding')
651
encoding = getattr(stream, "encoding", None)
653
encoding = osutils.get_terminal_encoding(trace=True)
657
def _unwrap_stream(stream):
658
inner = getattr(stream, "buffer", None)
660
inner = getattr(stream, "stream", stream)
664
def _wrap_in_stream(stream, encoding=None, errors='replace'):
666
encoding = _get_stream_encoding(stream)
667
# Attempt to wrap using io.open if possible, since that can do
670
fileno = stream.fileno()
671
except io.UnsupportedOperation:
672
encoded_stream = codecs.getreader(encoding)(stream, errors=errors)
673
encoded_stream.encoding = encoding
674
return encoded_stream
676
return io.open(fileno, encoding=encoding, errors=errors, mode='r', buffering=1)
679
def _wrap_out_stream(stream, encoding=None, errors='replace'):
681
encoding = _get_stream_encoding(stream)
682
encoded_stream = codecs.getwriter(encoding)(stream, errors=errors)
683
encoded_stream.encoding = encoding
684
return encoded_stream
660
687
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
688
"""Decorates stream to interact better with progress and change encoding.
690
Before writing to the wrapped stream, progress is cleared. Callers must
691
ensure bulk output is terminated with a newline so progress won't overwrite
694
Additionally, the encoding and errors behaviour of the underlying stream
695
can be changed at this point. If errors is set to 'exact' raw bytes may be
696
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):
699
def __init__(self, ui_factory, stream, encoding=None, errors='strict'):
674
700
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)
701
# GZ 2017-05-21: Clean up semantics when callers are made saner.
702
inner = _unwrap_stream(stream)
703
self.raw_stream = None
704
if errors == "exact":
706
self.raw_stream = inner
708
self.wrapped_stream = stream
710
encoding = _get_stream_encoding(stream)
712
self.wrapped_stream = _wrap_out_stream(inner, encoding, errors)
714
encoding = self.wrapped_stream.encoding
715
self.encoding = encoding
718
def _write(self, to_write):
719
if isinstance(to_write, bytes):
721
to_write = to_write.decode(self.encoding, self.errors)
722
except UnicodeDecodeError:
723
self.raw_stream.write(to_write)
725
self.wrapped_stream.write(to_write)
682
728
self.ui_factory.clear_term()