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
from __future__ import absolute_import
28
from bzrlib.lazy_import import lazy_import
27
from ..lazy_import import lazy_import
29
28
lazy_import(globals(), """
40
from bzrlib.osutils import watch_sigwinch
42
from bzrlib.ui import (
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,
48
171
class TextUIFactory(UIFactory):
49
"""A UI factory for Text user interefaces."""
172
"""A UI factory for Text user interfaces."""
55
"""Create a TextUIFactory.
174
def __init__(self, stdin, stdout, stderr):
175
"""Create a TextUIFactory."""
57
176
super(TextUIFactory, self).__init__()
58
# TODO: there's no good reason not to pass all three streams, maybe we
59
# should deprecate the default values...
60
177
self.stdin = stdin
61
178
self.stdout = stdout
62
179
self.stderr = stderr
180
self._progress_view = NullProgressView()
183
# Choose default encoding and handle py2/3 differences
184
self._setup_streams()
63
185
# paints progress, network activity, etc
64
186
self._progress_view = self.make_progress_view()
65
# hook up the signals to watch for terminal size changes
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()
68
218
def be_quiet(self, state):
69
219
if state and not self._quiet:
149
288
username = self.stdin.readline()
152
elif username[-1] == '\n':
153
username = username[:-1]
292
if username[-1] == '\n':
293
username = username[:-1]
156
296
def make_progress_view(self):
157
297
"""Construct and return a new ProgressView subclass for this UI.
159
299
# with --quiet, never any progress view
160
# <https://bugs.edge.launchpad.net/bzr/+bug/320035>. Otherwise if the
300
# <https://bugs.launchpad.net/bzr/+bug/320035>. Otherwise if the
161
301
# user specifically requests either text or no progress bars, always
162
302
# do that. otherwise, guess based on $TERM and tty presence.
163
303
if self.is_quiet():
164
304
return NullProgressView()
165
elif os.environ.get('BZR_PROGRESS_BAR') == 'text':
166
return TextProgressView(self.stderr)
167
elif os.environ.get('BZR_PROGRESS_BAR') == 'none':
168
return NullProgressView()
169
elif progress._supports_progress(self.stderr):
170
return TextProgressView(self.stderr)
172
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()
174
314
def _make_output_stream_explicit(self, encoding, encoding_type):
175
if encoding_type == 'exact':
176
# force sys.stdout to be binary stream on win32;
177
# NB: this leaves the file set in that mode; may cause problems if
178
# one process tries to do binary and then text output
179
if sys.platform == 'win32':
180
fileno = getattr(self.stdout, 'fileno', None)
183
msvcrt.setmode(fileno(), os.O_BINARY)
184
return TextUIOutputStream(self, self.stdout)
186
encoded_stdout = codecs.getwriter(encoding)(self.stdout,
187
errors=encoding_type)
188
# For whatever reason codecs.getwriter() does not advertise its encoding
189
# it just returns the encoding of the wrapped file, which is completely
190
# bogus. So set the attribute, so we can find the correct encoding later.
191
encoded_stdout.encoding = encoding
192
return TextUIOutputStream(self, encoded_stdout)
315
return TextUIOutputStream(self, self.stdout, encoding, encoding_type)
194
317
def note(self, msg):
195
318
"""Write an already-formatted message, clearing the progress bar if necessary."""
262
388
# be easier to test; that has a lot of test fallout so for now just
263
389
# new code can call this
264
390
if warning_id not in self.suppressed_warnings:
265
self.stderr.write(self.format_user_warning(warning_id, message_args) +
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)
269
405
class TextProgressView(object):
270
406
"""Display of progress bar and other information on a tty.
272
This shows one line of text, including possibly a network indicator, spinner,
273
progress bar, message, etc.
408
This shows one line of text, including possibly a network indicator,
409
spinner, progress bar, message, etc.
275
411
One instance of this is created and held by the UI, and fed updates when a
276
412
task wants to be painted.
364
513
t = t._parent_task
366
515
m = t.msg + ':' + m
369
518
def _render_line(self):
370
519
bar_string = self._render_bar()
371
520
if self._last_task:
372
task_msg = self._format_task(self._last_task)
521
task_part, counter_part = self._format_task(self._last_task)
523
task_part = counter_part = ''
375
524
if self._last_task and not self._last_task.show_transport_activity:
378
527
trans = self._last_transport_msg
381
return (bar_string + trans + task_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:
383
552
def _repaint(self):
384
553
s = self._render_line()
481
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
484
692
class TextUIOutputStream(object):
485
"""Decorates an output stream so that the terminal is cleared before writing.
487
This is supposed to ensure that the progress bar does not conflict with bulk
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.
490
# XXX: this does not handle the case of writing part of a line, then doing
491
# progress bar output: the progress bar will probably write over it.
492
# one option is just to buffer that text until we have a full line;
493
# another is to save and restore it
495
# XXX: might need to wrap more methods
497
def __init__(self, ui_factory, wrapped_stream):
704
def __init__(self, ui_factory, stream, encoding=None, errors='strict'):
498
705
self.ui_factory = ui_factory
499
self.wrapped_stream = wrapped_stream
500
# this does no transcoding, but it must expose the underlying encoding
501
# because some callers need to know what can be written - see for
502
# example unescape_for_display.
503
self.encoding = getattr(wrapped_stream, 'encoding', None)
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)
506
733
self.ui_factory.clear_term()