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."""
19
from __future__ import absolute_import
19
"""Text UI, write output to the console.
27
from ..lazy_import import lazy_import
26
from bzrlib.lazy_import import lazy_import
28
27
lazy_import(globals(), """
43
from ..sixish import (
52
class _ChooseUI(object):
54
""" Helper class for choose implementation.
57
def __init__(self, ui, msg, choices, default):
60
self._build_alternatives(msg, choices, default)
62
def _setup_mode(self):
63
"""Setup input mode (line-based, char-based) and echo-back.
65
Line-based input is used if the BRZ_TEXTUI_INPUT environment
66
variable is set to 'line-based', or if there is no controlling
69
is_tty = self.ui.raw_stdin.isatty()
70
if (os.environ.get('BRZ_TEXTUI_INPUT') != 'line-based' and
71
self.ui.raw_stdin == _unwrap_stream(sys.stdin) and is_tty):
72
self.line_based = False
75
self.line_based = True
76
self.echo_back = not is_tty
78
def _build_alternatives(self, msg, choices, default):
79
"""Parse choices string.
81
Setup final prompt and the lists of choices and associated
86
self.alternatives = {}
87
choices = choices.split('\n')
88
if default is not None and default not in range(0, len(choices)):
89
raise ValueError("invalid default index")
91
name = c.replace('&', '').lower()
92
choice = (name, index)
93
if name in self.alternatives:
94
raise ValueError("duplicated choice: %s" % name)
95
self.alternatives[name] = choice
96
shortcut = c.find('&')
97
if -1 != shortcut and (shortcut + 1) < len(c):
99
help += '[' + c[shortcut + 1] + ']'
100
help += c[(shortcut + 2):]
101
shortcut = c[shortcut + 1]
103
c = c.replace('&', '')
105
help = '[%s]%s' % (shortcut, c[1:])
106
shortcut = shortcut.lower()
107
if shortcut in self.alternatives:
108
raise ValueError("duplicated shortcut: %s" % shortcut)
109
self.alternatives[shortcut] = choice
110
# Add redirections for default.
112
self.alternatives[''] = choice
113
self.alternatives['\r'] = choice
114
help_list.append(help)
117
self.prompt = u'%s (%s): ' % (msg, ', '.join(help_list))
120
line = self.ui.stdin.readline()
126
char = osutils.getchar()
127
if char == chr(3): # INTR
128
raise KeyboardInterrupt
129
if char == chr(4): # EOF (^d, C-d)
131
if isinstance(char, bytes):
132
return char.decode('ascii', 'replace')
136
"""Keep asking the user until a valid choice is made.
139
getchoice = self._getline
141
getchoice = self._getchar
145
if 1 == iter or self.line_based:
146
self.ui.prompt(self.prompt)
150
self.ui.stderr.write(u'\n')
152
except KeyboardInterrupt:
153
self.ui.stderr.write(u'\n')
155
choice = choice.lower()
156
if choice not in self.alternatives:
157
# Not a valid choice, keep on asking.
159
name, index = self.alternatives[choice]
161
self.ui.stderr.write(name + u'\n')
165
opt_progress_bar = config.Option(
166
'progress_bar', help='Progress bar type.',
167
default_from_env=['BRZ_PROGRESS_BAR'], default=None,
171
class TextUIFactory(UIFactory):
172
"""A UI factory for Text user interfaces."""
174
def __init__(self, stdin, stdout, stderr):
175
"""Create a TextUIFactory."""
176
super(TextUIFactory, self).__init__()
180
self._progress_view = NullProgressView()
183
# Choose default encoding and handle py2/3 differences
184
self._setup_streams()
38
from bzrlib.ui import CLIUIFactory
41
class TextUIFactory(CLIUIFactory):
42
"""A UI factory for Text user interefaces."""
49
"""Create a TextUIFactory.
51
:param bar_type: The type of progress bar to create. It defaults to
52
letting the bzrlib.progress.ProgressBar factory auto
55
super(TextUIFactory, self).__init__(stdin=stdin,
56
stdout=stdout, stderr=stderr)
58
symbol_versioning.warn(symbol_versioning.deprecated_in((1, 11, 0))
59
% "bar_type parameter")
185
60
# paints progress, network activity, etc
186
self._progress_view = self.make_progress_view()
189
def _setup_streams(self):
190
self.raw_stdin = _unwrap_stream(self.stdin)
191
self.stdin = _wrap_in_stream(self.raw_stdin)
192
self.raw_stdout = _unwrap_stream(self.stdout)
193
self.stdout = _wrap_out_stream(self.raw_stdout)
194
self.raw_stderr = _unwrap_stream(self.stderr)
195
self.stderr = _wrap_out_stream(self.raw_stderr)
197
def choose(self, msg, choices, default=None):
198
"""Prompt the user for a list of alternatives.
200
Support both line-based and char-based editing.
202
In line-based mode, both the shortcut and full choice name are valid
203
answers, e.g. for choose('prompt', '&yes\n&no'): 'y', ' Y ', ' yes',
204
'YES ' are all valid input lines for choosing 'yes'.
206
An empty line, when in line-based mode, or pressing enter in char-based
207
mode will select the default choice (if any).
209
Choice is echoed back if:
210
- input is char-based; which means a controlling terminal is available,
211
and osutils.getchar is used
212
- input is line-based, and no controlling terminal is available
215
choose_ui = _ChooseUI(self, msg, choices, default)
216
return choose_ui.interact()
218
def be_quiet(self, state):
219
if state and not self._quiet:
221
UIFactory.be_quiet(self, state)
222
self._progress_view = self.make_progress_view()
61
self._progress_view = TextProgressView(self.stderr)
224
63
def clear_term(self):
225
64
"""Prepare the terminal for output.
232
71
# to clear it. We might need to separately check for the case of
233
72
self._progress_view.clear()
235
def get_integer(self, prompt):
238
line = self.stdin.readline()
244
def get_non_echoed_password(self):
245
isatty = getattr(self.stdin, 'isatty', None)
246
if isatty is not None and isatty():
247
# getpass() ensure the password is not echoed and other
248
# cross-platform niceties
249
password = getpass.getpass('')
251
# echo doesn't make sense without a terminal
252
password = self.stdin.readline()
256
if password[-1] == '\n':
257
password = password[:-1]
260
def get_password(self, prompt=u'', **kwargs):
261
"""Prompt the user for a password.
263
:param prompt: The prompt to present the user
264
:param kwargs: Arguments which will be expanded into the prompt.
265
This lets front ends display different things if
267
:return: The password string, return None if the user
268
canceled the request.
271
self.prompt(prompt, **kwargs)
272
# There's currently no way to say 'i decline to enter a password'
273
# as opposed to 'my password is empty' -- does it matter?
274
return self.get_non_echoed_password()
276
def get_username(self, prompt, **kwargs):
277
"""Prompt the user for a username.
279
:param prompt: The prompt to present the user
280
:param kwargs: Arguments which will be expanded into the prompt.
281
This lets front ends display different things if
283
:return: The username string, return None if the user
284
canceled the request.
287
self.prompt(prompt, **kwargs)
288
username = self.stdin.readline()
292
if username[-1] == '\n':
293
username = username[:-1]
296
def make_progress_view(self):
297
"""Construct and return a new ProgressView subclass for this UI.
299
# with --quiet, never any progress view
300
# <https://bugs.launchpad.net/bzr/+bug/320035>. Otherwise if the
301
# user specifically requests either text or no progress bars, always
302
# do that. otherwise, guess based on $TERM and tty presence.
304
return NullProgressView()
305
pb_type = config.GlobalStack().get('progress_bar')
306
if pb_type == 'none': # Explicit requirement
307
return NullProgressView()
308
if (pb_type == 'text' or # Explicit requirement
309
progress._supports_progress(self.stderr)): # Guess
310
return TextProgressView(self.stderr)
311
# No explicit requirement and no successful guess
312
return NullProgressView()
314
def _make_output_stream_explicit(self, encoding, encoding_type):
315
return TextUIOutputStream(self, self.stdout, encoding, encoding_type)
317
74
def note(self, msg):
318
75
"""Write an already-formatted message, clearing the progress bar if necessary."""
320
77
self.stdout.write(msg + '\n')
322
def prompt(self, prompt, **kwargs):
323
"""Emit prompt on the CLI.
325
:param kwargs: Dictionary of arguments to insert into the prompt,
326
to allow UIs to reformat the prompt.
328
if not isinstance(prompt, text_type):
329
raise ValueError("prompt %r not a unicode string" % prompt)
331
# See <https://launchpad.net/bugs/365891>
332
prompt = prompt % kwargs
335
self.stderr.write(prompt)
338
79
def report_transport_activity(self, transport, byte_count, direction):
339
80
"""Called by transports as they do IO.
341
82
This may update a progress bar, spinner, or similar display.
342
83
By default it does nothing.
344
self._progress_view.show_transport_activity(transport,
345
direction, byte_count)
347
def log_transport_activity(self, display=False):
348
"""See UIFactory.log_transport_activity()"""
349
log = getattr(self._progress_view, 'log_transport_activity', None)
353
def show_error(self, msg):
355
self.stderr.write("bzr: error: %s\n" % msg)
357
def show_message(self, msg):
360
def show_warning(self, msg):
362
self.stderr.write("bzr: warning: %s\n" % msg)
85
self._progress_view._show_transport_activity(transport,
86
direction, byte_count)
364
88
def _progress_updated(self, task):
365
89
"""A task has been updated and wants to be displayed.
367
91
if not self._task_stack:
368
92
warnings.warn("%r updated but no tasks are active" %
370
94
elif task != self._task_stack[-1]:
371
# We used to check it was the top task, but it's hard to always
372
# get this right and it's not necessarily useful: any actual
373
# problems will be evident in use
374
# warnings.warn("%r is not the top progress task %r" %
375
# (task, self._task_stack[-1]))
95
warnings.warn("%r is not the top progress task %r" %
96
(task, self._task_stack[-1]))
377
97
self._progress_view.show_progress(task)
379
99
def _progress_all_finished(self):
380
100
self._progress_view.clear()
382
def show_user_warning(self, warning_id, **message_args):
383
"""Show a text message to the user.
385
Explicitly not for warnings about bzr apis, deprecations or internals.
387
# eventually trace.warning should migrate here, to avoid logging and
388
# be easier to test; that has a lot of test fallout so for now just
389
# new code can call this
390
if warning_id not in self.suppressed_warnings:
391
warning = self.format_user_warning(warning_id, message_args)
392
self.stderr.write(warning + '\n')
395
def pad_to_width(line, width, encoding_hint='ascii'):
396
"""Truncate or pad unicode line to width.
398
This is best-effort for now, and strings containing control codes or
399
non-ascii text may be cut and padded incorrectly.
401
s = line.encode(encoding_hint, 'replace')
402
return (b'%-*.*s' % (width, width, s)).decode(encoding_hint)
405
103
class TextProgressView(object):
406
104
"""Display of progress bar and other information on a tty.
408
This shows one line of text, including possibly a network indicator,
409
spinner, progress bar, message, etc.
106
This shows one line of text, including possibly a network indicator, spinner,
107
progress bar, message, etc.
411
109
One instance of this is created and held by the UI, and fed updates when a
412
110
task wants to be painted.
463
143
def _render_bar(self):
464
144
# return a string for the progress bar itself
465
if self.enable_bar and (
466
(self._last_task is None) or self._last_task.show_bar):
145
if (self._last_task is None) or self._last_task.show_bar:
467
146
# If there's no task object, we show space for the bar anyhow.
468
147
# That's because most invocations of bzr will end showing progress
469
148
# at some point, though perhaps only after doing some initial IO.
470
149
# It looks better to draw the progress bar initially rather than
471
150
# to have what looks like an incomplete progress bar.
472
spin_str = r'/-\|'[self._spin_pos % 4]
151
spin_str = r'/-\|'[self._spin_pos % 4]
473
152
self._spin_pos += 1
475
154
if self._last_task is None:
476
155
completion_fraction = 0
479
157
completion_fraction = \
480
158
self._last_task._overall_completion_fraction() or 0
481
if (completion_fraction < self._fraction and 'progress' in
484
self._fraction = completion_fraction
485
159
markers = int(round(float(cols) * completion_fraction)) - 1
486
160
bar_str = '[' + ('#' * markers + spin_str).ljust(cols) + '] '
488
elif (self._last_task is None) or self._last_task.show_spinner:
162
elif self._last_task.show_spinner:
489
163
# The last task wanted just a spinner, no bar
490
spin_str = r'/-\|'[self._spin_pos % 4]
164
spin_str = r'/-\|'[self._spin_pos % 4]
491
165
self._spin_pos += 1
492
166
return spin_str + ' '
496
170
def _format_task(self, task):
497
"""Format task-specific parts of progress bar.
499
:returns: (text_part, counter_part) both unicode strings.
501
171
if not task.show_count:
503
173
elif task.current_cnt is not None and task.total_cnt is not None:
513
183
t = t._parent_task
515
185
m = t.msg + ':' + m
518
188
def _render_line(self):
519
189
bar_string = self._render_bar()
520
190
if self._last_task:
521
task_part, counter_part = self._format_task(self._last_task)
523
task_part = counter_part = ''
524
if self._last_task and not self._last_task.show_transport_activity:
527
trans = self._last_transport_msg
528
# the bar separates the transport activity from the message, so even
529
# if there's no bar or spinner, we must show something if both those
531
if (task_part or trans) and not bar_string:
533
# preferentially truncate the task message if we don't have enough
535
avail_width = self._avail_width()
536
if avail_width is not None:
537
# if terminal avail_width is unknown, don't truncate
538
current_len = len(bar_string) + len(trans) + \
539
len(task_part) + len(counter_part)
540
# GZ 2017-04-22: Should measure and truncate task_part properly
541
gap = current_len - avail_width
543
task_part = task_part[:-gap - 2] + '..'
544
s = trans + bar_string + task_part + counter_part
545
if avail_width is not None:
546
if len(s) < avail_width:
547
s = s.ljust(avail_width)
548
elif len(s) > avail_width:
191
task_msg = self._format_task(self._last_task)
194
trans = self._last_transport_msg
197
return (bar_string + trans + task_msg)
552
199
def _repaint(self):
553
200
s = self._render_line()
571
218
self._last_repaint = now
574
def show_transport_activity(self, transport, direction, byte_count):
221
def _show_transport_activity(self, transport, direction, byte_count):
575
222
"""Called by transports via the ui_factory, as they do IO.
577
224
This may update a progress bar, spinner, or similar display.
578
225
By default it does nothing.
580
# XXX: there should be a transport activity model, and that too should
581
# be seen by the progress view, rather than being poked in here.
227
# XXX: Probably there should be a transport activity model, and that
228
# too should be seen by the progress view, rather than being poked in
582
230
self._total_byte_count += byte_count
583
231
self._bytes_since_update += byte_count
584
if self._first_byte_time is None:
585
# Note that this isn't great, as technically it should be the time
586
# when the bytes started transferring, not when they completed.
587
# However, we usually start with a small request anyway.
588
self._first_byte_time = time.time()
589
if direction in self._bytes_by_direction:
590
self._bytes_by_direction[direction] += byte_count
592
self._bytes_by_direction['unknown'] += byte_count
593
if 'no_activity' in debug.debug_flags:
594
# Can be used as a workaround if
595
# <https://launchpad.net/bugs/321935> reappears and transport
596
# activity is cluttering other output. However, thanks to
597
# TextUIOutputStream this shouldn't be a problem any more.
599
232
now = time.time()
600
if self._total_byte_count < 2000:
601
# a little resistance at first, so it doesn't stay stuck at 0
602
# while connecting...
604
233
if self._transport_update_time is None:
605
234
self._transport_update_time = now
606
235
elif now >= (self._transport_update_time + 0.5):
607
236
# guard against clock stepping backwards, and don't update too
609
rate = (self._bytes_since_update /
610
(now - self._transport_update_time))
611
# using base-10 units (see HACKING.txt).
612
msg = ("%6dkB %5dkB/s " %
613
(self._total_byte_count / 1000, int(rate) / 1000,))
238
rate = self._bytes_since_update / (now - self._transport_update_time)
239
scheme = getattr(transport, '_scheme', None) or repr(transport)
240
if direction == 'read':
242
elif direction == 'write':
246
msg = ("%.7s %s %6dKB %5dKB/s" %
247
(scheme, dir_char, self._total_byte_count>>10, int(rate)>>10,))
614
248
self._transport_update_time = now
615
249
self._last_repaint = now
616
250
self._bytes_since_update = 0
617
251
self._last_transport_msg = msg
620
def _format_bytes_by_direction(self):
621
if self._first_byte_time is None:
624
transfer_time = time.time() - self._first_byte_time
625
if transfer_time < 0.001:
626
transfer_time = 0.001
627
bps = self._total_byte_count / transfer_time
629
# using base-10 units (see HACKING.txt).
630
msg = ('Transferred: %.0fkB'
631
' (%.1fkB/s r:%.0fkB w:%.0fkB'
632
% (self._total_byte_count / 1000.,
634
self._bytes_by_direction['read'] / 1000.,
635
self._bytes_by_direction['write'] / 1000.,
637
if self._bytes_by_direction['unknown'] > 0:
638
msg += ' u:%.0fkB)' % (
639
self._bytes_by_direction['unknown'] / 1000.
645
def log_transport_activity(self, display=False):
646
msg = self._format_bytes_by_direction()
648
if display and self._total_byte_count > 0:
650
self._term_file.write(msg + '\n')
653
def _get_stream_encoding(stream):
654
encoding = config.GlobalStack().get('output_encoding')
656
encoding = getattr(stream, "encoding", None)
658
encoding = osutils.get_terminal_encoding(trace=True)
662
def _unwrap_stream(stream):
663
inner = getattr(stream, "buffer", None)
665
inner = getattr(stream, "stream", stream)
669
def _wrap_in_stream(stream, encoding=None, errors='replace'):
671
encoding = _get_stream_encoding(stream)
672
# Attempt to wrap using io.open if possible, since that can do
675
fileno = stream.fileno()
676
except io.UnsupportedOperation:
677
encoded_stream = codecs.getreader(encoding)(stream, errors=errors)
678
encoded_stream.encoding = encoding
679
return encoded_stream
681
return io.open(fileno, encoding=encoding, errors=errors, mode='r', buffering=1)
684
def _wrap_out_stream(stream, encoding=None, errors='replace'):
686
encoding = _get_stream_encoding(stream)
687
encoded_stream = codecs.getwriter(encoding)(stream, errors=errors)
688
encoded_stream.encoding = encoding
689
return encoded_stream
692
class TextUIOutputStream(object):
693
"""Decorates stream to interact better with progress and change encoding.
695
Before writing to the wrapped stream, progress is cleared. Callers must
696
ensure bulk output is terminated with a newline so progress won't overwrite
699
Additionally, the encoding and errors behaviour of the underlying stream
700
can be changed at this point. If errors is set to 'exact' raw bytes may be
701
written to the underlying stream.
704
def __init__(self, ui_factory, stream, encoding=None, errors='strict'):
705
self.ui_factory = ui_factory
706
# GZ 2017-05-21: Clean up semantics when callers are made saner.
707
inner = _unwrap_stream(stream)
708
self.raw_stream = None
709
if errors == "exact":
711
self.raw_stream = inner
713
self.wrapped_stream = stream
715
encoding = _get_stream_encoding(stream)
717
self.wrapped_stream = _wrap_out_stream(inner, encoding, errors)
719
encoding = self.wrapped_stream.encoding
720
self.encoding = encoding
723
def _write(self, to_write):
724
if isinstance(to_write, bytes):
726
to_write = to_write.decode(self.encoding, self.errors)
727
except UnicodeDecodeError:
728
self.raw_stream.write(to_write)
730
self.wrapped_stream.write(to_write)
733
self.ui_factory.clear_term()
734
self.wrapped_stream.flush()
736
def write(self, to_write):
737
self.ui_factory.clear_term()
738
self._write(to_write)
740
def writelines(self, lines):
741
self.ui_factory.clear_term()