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.
21
from __future__ import absolute_import
25
from ..lazy_import import lazy_import
27
from brzlib.lazy_import import lazy_import
26
28
lazy_import(globals(), """
43
from brzlib.ui import (
57
59
def _setup_mode(self):
58
60
"""Setup input mode (line-based, char-based) and echo-back.
60
Line-based input is used if the BRZ_TEXTUI_INPUT environment
62
Line-based input is used if the BZR_TEXTUI_INPUT environment
61
63
variable is set to 'line-based', or if there is no controlling
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):
66
if os.environ.get('BZR_TEXTUI_INPUT') != 'line-based' and \
67
self.ui.stdin == sys.stdin and self.ui.stdin.isatty():
67
68
self.line_based = False
68
69
self.echo_back = True
70
71
self.line_based = True
71
self.echo_back = not is_tty
72
self.echo_back = not self.ui.stdin.isatty()
73
74
def _build_alternatives(self, msg, choices, default):
74
75
"""Parse choices string.
143
142
choice = getchoice()
145
self.ui.stderr.write(u'\n')
144
self.ui.stderr.write('\n')
147
146
except KeyboardInterrupt:
148
self.ui.stderr.write(u'\n')
147
self.ui.stderr.write('\n')
148
raise KeyboardInterrupt
150
149
choice = choice.lower()
151
150
if choice not in self.alternatives:
152
151
# Not a valid choice, keep on asking.
154
153
name, index = self.alternatives[choice]
155
154
if self.echo_back:
156
self.ui.stderr.write(name + u'\n')
155
self.ui.stderr.write(name + '\n')
160
159
opt_progress_bar = config.Option(
161
160
'progress_bar', help='Progress bar type.',
162
default_from_env=['BRZ_PROGRESS_BAR'], default=None,
161
default_from_env=['BZR_PROGRESS_BAR'], default=None,
166
165
class TextUIFactory(UIFactory):
167
166
"""A UI factory for Text user interfaces."""
169
def __init__(self, stdin, stdout, stderr):
170
"""Create a TextUIFactory."""
172
"""Create a TextUIFactory.
171
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...
172
177
self.stdin = stdin
173
178
self.stdout = stdout
174
179
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)
192
183
def choose(self, msg, choices, default=None):
193
184
"""Prompt the user for a list of alternatives.
298
292
if self.is_quiet():
299
293
return NullProgressView()
300
294
pb_type = config.GlobalStack().get('progress_bar')
301
if pb_type == 'none': # Explicit requirement
295
if pb_type == 'none': # Explicit requirement
302
296
return NullProgressView()
303
if (pb_type == 'text' or # Explicit requirement
304
progress._supports_progress(self.stderr)): # Guess
297
if (pb_type == 'text' # Explicit requirement
298
or progress._supports_progress(self.stderr)): # Guess
305
299
return TextProgressView(self.stderr)
306
300
# No explicit requirement and no successful guess
307
301
return NullProgressView()
309
303
def _make_output_stream_explicit(self, encoding, encoding_type):
310
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)
312
323
def note(self, msg):
313
324
"""Write an already-formatted message, clearing the progress bar if necessary."""
317
328
def prompt(self, prompt, **kwargs):
318
329
"""Emit prompt on the CLI.
320
331
:param kwargs: Dictionary of arguments to insert into the prompt,
321
332
to allow UIs to reformat the prompt.
323
if not isinstance(prompt, str):
334
if type(prompt) != unicode:
324
335
raise ValueError("prompt %r not a unicode string" % prompt)
326
337
# See <https://launchpad.net/bugs/365891>
327
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')
328
346
self.clear_term()
329
347
self.stdout.flush()
330
348
self.stderr.write(prompt)
333
350
def report_transport_activity(self, transport, byte_count, direction):
334
351
"""Called by transports as they do IO.
383
403
# be easier to test; that has a lot of test fallout so for now just
384
404
# new code can call this
385
405
if warning_id not in self.suppressed_warnings:
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)
406
self.stderr.write(self.format_user_warning(warning_id, message_args) +
400
410
class TextProgressView(object):
401
411
"""Display of progress bar and other information on a tty.
403
This shows one line of text, including possibly a network indicator,
404
spinner, progress bar, message, etc.
413
This shows one line of text, including possibly a network indicator, spinner,
414
progress bar, message, etc.
406
416
One instance of this is created and held by the UI, and fed updates when a
407
417
task wants to be painted.
412
422
this only prints the stack from the nominated current task up to the root.
415
def __init__(self, term_file, encoding=None, errors=None):
425
def __init__(self, term_file, encoding=None, errors="replace"):
416
426
self._term_file = term_file
417
427
if encoding is None:
418
428
self._encoding = getattr(term_file, "encoding", None) or "ascii"
420
430
self._encoding = encoding
431
self._encoding_errors = errors
421
432
# true when there's output on the screen we may need to clear
422
433
self._have_output = False
423
434
self._last_transport_msg = ''
432
443
self._bytes_by_direction = {'unknown': 0, 'read': 0, 'write': 0}
433
444
self._first_byte_time = None
434
445
self._fraction = 0
435
# 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
436
447
# correspond reliably to overall command progress
437
448
self.enable_bar = False
439
450
def _avail_width(self):
440
451
# we need one extra space for terminals that wrap on last char
441
w = osutils.terminal_width()
452
w = osutils.terminal_width()
447
458
def _show_line(self, u):
459
s = u.encode(self._encoding, self._encoding_errors)
448
460
width = self._avail_width()
449
461
if width is not None:
450
u = pad_to_width(u, width, encoding_hint=self._encoding)
451
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')
454
468
if self._have_output:
458
472
def _render_bar(self):
459
473
# return a string for the progress bar itself
460
474
if self.enable_bar and (
461
(self._last_task is None) or self._last_task.show_bar):
475
(self._last_task is None) or self._last_task.show_bar):
462
476
# If there's no task object, we show space for the bar anyhow.
463
477
# That's because most invocations of bzr will end showing progress
464
478
# at some point, though perhaps only after doing some initial IO.
465
479
# It looks better to draw the progress bar initially rather than
466
480
# to have what looks like an incomplete progress bar.
467
spin_str = r'/-\|'[self._spin_pos % 4]
481
spin_str = r'/-\|'[self._spin_pos % 4]
468
482
self._spin_pos += 1
470
484
if self._last_task is None:
474
488
completion_fraction = \
475
489
self._last_task._overall_completion_fraction() or 0
476
490
if (completion_fraction < self._fraction and 'progress' in
492
import pdb;pdb.set_trace()
479
493
self._fraction = completion_fraction
480
494
markers = int(round(float(cols) * completion_fraction)) - 1
481
495
bar_str = '[' + ('#' * markers + spin_str).ljust(cols) + '] '
483
497
elif (self._last_task is None) or self._last_task.show_spinner:
484
498
# The last task wanted just a spinner, no bar
485
spin_str = r'/-\|'[self._spin_pos % 4]
499
spin_str = r'/-\|'[self._spin_pos % 4]
486
500
self._spin_pos += 1
487
501
return spin_str + ' '
530
544
avail_width = self._avail_width()
531
545
if avail_width is not None:
532
546
# if terminal avail_width is unknown, don't truncate
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
547
current_len = len(bar_string) + len(trans) + len(task_part) + len(counter_part)
536
548
gap = current_len - avail_width
538
task_part = task_part[:-gap - 2] + '..'
550
task_part = task_part[:-gap-2] + '..'
539
551
s = trans + bar_string + task_part + counter_part
540
552
if avail_width is not None:
541
553
if len(s) < avail_width:
601
613
elif now >= (self._transport_update_time + 0.5):
602
614
# guard against clock stepping backwards, and don't update too
604
rate = (self._bytes_since_update /
605
(now - self._transport_update_time))
616
rate = (self._bytes_since_update
617
/ (now - self._transport_update_time))
606
618
# using base-10 units (see HACKING.txt).
607
619
msg = ("%6dkB %5dkB/s " %
608
(self._total_byte_count / 1000, int(rate) / 1000,))
620
(self._total_byte_count / 1000, int(rate) / 1000,))
609
621
self._transport_update_time = now
610
622
self._last_repaint = now
611
623
self._bytes_since_update = 0
645
657
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
687
660
class TextUIOutputStream(object):
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.
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
699
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):
700
674
self.ui_factory = ui_factory
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)
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)
728
682
self.ui_factory.clear_term()