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
26
from ..lazy_import import lazy_import
29
27
lazy_import(globals(), """
40
from bzrlib.osutils import watch_sigwinch
42
from bzrlib.ui import (
42
from ..sixish import (
51
class _ChooseUI(object):
53
""" Helper class for choose implementation.
56
def __init__(self, ui, msg, choices, default):
59
self._build_alternatives(msg, choices, default)
61
def _setup_mode(self):
62
"""Setup input mode (line-based, char-based) and echo-back.
64
Line-based input is used if the BRZ_TEXTUI_INPUT environment
65
variable is set to 'line-based', or if there is no controlling
68
is_tty = self.ui.raw_stdin.isatty()
69
if (os.environ.get('BRZ_TEXTUI_INPUT') != 'line-based' and
70
self.ui.raw_stdin == _unwrap_stream(sys.stdin) and is_tty):
71
self.line_based = False
74
self.line_based = True
75
self.echo_back = not is_tty
77
def _build_alternatives(self, msg, choices, default):
78
"""Parse choices string.
80
Setup final prompt and the lists of choices and associated
85
self.alternatives = {}
86
choices = choices.split('\n')
87
if default is not None and default not in range(0, len(choices)):
88
raise ValueError("invalid default index")
90
name = c.replace('&', '').lower()
91
choice = (name, index)
92
if name in self.alternatives:
93
raise ValueError("duplicated choice: %s" % name)
94
self.alternatives[name] = choice
95
shortcut = c.find('&')
96
if -1 != shortcut and (shortcut + 1) < len(c):
98
help += '[' + c[shortcut + 1] + ']'
99
help += c[(shortcut + 2):]
100
shortcut = c[shortcut + 1]
102
c = c.replace('&', '')
104
help = '[%s]%s' % (shortcut, c[1:])
105
shortcut = shortcut.lower()
106
if shortcut in self.alternatives:
107
raise ValueError("duplicated shortcut: %s" % shortcut)
108
self.alternatives[shortcut] = choice
109
# Add redirections for default.
111
self.alternatives[''] = choice
112
self.alternatives['\r'] = choice
113
help_list.append(help)
116
self.prompt = u'%s (%s): ' % (msg, ', '.join(help_list))
119
line = self.ui.stdin.readline()
125
char = osutils.getchar()
126
if char == chr(3): # INTR
127
raise KeyboardInterrupt
128
if char == chr(4): # EOF (^d, C-d)
130
if isinstance(char, bytes):
131
return char.decode('ascii', 'replace')
135
"""Keep asking the user until a valid choice is made.
138
getchoice = self._getline
140
getchoice = self._getchar
144
if 1 == iter or self.line_based:
145
self.ui.prompt(self.prompt)
149
self.ui.stderr.write(u'\n')
151
except KeyboardInterrupt:
152
self.ui.stderr.write(u'\n')
154
choice = choice.lower()
155
if choice not in self.alternatives:
156
# Not a valid choice, keep on asking.
158
name, index = self.alternatives[choice]
160
self.ui.stderr.write(name + u'\n')
164
opt_progress_bar = config.Option(
165
'progress_bar', help='Progress bar type.',
166
default_from_env=['BRZ_PROGRESS_BAR'], default=None,
48
170
class TextUIFactory(UIFactory):
49
"""A UI factory for Text user interefaces."""
171
"""A UI factory for Text user interfaces."""
55
"""Create a TextUIFactory.
173
def __init__(self, stdin, stdout, stderr):
174
"""Create a TextUIFactory."""
57
175
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
176
self.stdin = stdin
61
177
self.stdout = stdout
62
178
self.stderr = stderr
179
self._progress_view = NullProgressView()
182
# Choose default encoding and handle py2/3 differences
183
self._setup_streams()
63
184
# paints progress, network activity, etc
64
185
self._progress_view = self.make_progress_view()
65
# hook up the signals to watch for terminal size changes
188
def _setup_streams(self):
189
self.raw_stdin = _unwrap_stream(self.stdin)
190
self.stdin = _wrap_in_stream(self.raw_stdin)
191
self.raw_stdout = _unwrap_stream(self.stdout)
192
self.stdout = _wrap_out_stream(self.raw_stdout)
193
self.raw_stderr = _unwrap_stream(self.stderr)
194
self.stderr = _wrap_out_stream(self.raw_stderr)
196
def choose(self, msg, choices, default=None):
197
"""Prompt the user for a list of alternatives.
199
Support both line-based and char-based editing.
201
In line-based mode, both the shortcut and full choice name are valid
202
answers, e.g. for choose('prompt', '&yes\n&no'): 'y', ' Y ', ' yes',
203
'YES ' are all valid input lines for choosing 'yes'.
205
An empty line, when in line-based mode, or pressing enter in char-based
206
mode will select the default choice (if any).
208
Choice is echoed back if:
209
- input is char-based; which means a controlling terminal is available,
210
and osutils.getchar is used
211
- input is line-based, and no controlling terminal is available
214
choose_ui = _ChooseUI(self, msg, choices, default)
215
return choose_ui.interact()
68
217
def be_quiet(self, state):
69
218
if state and not self._quiet:
149
287
username = self.stdin.readline()
152
elif username[-1] == '\n':
153
username = username[:-1]
291
if username[-1] == '\n':
292
username = username[:-1]
156
295
def make_progress_view(self):
157
296
"""Construct and return a new ProgressView subclass for this UI.
159
298
# with --quiet, never any progress view
160
# <https://bugs.edge.launchpad.net/bzr/+bug/320035>. Otherwise if the
299
# <https://bugs.launchpad.net/bzr/+bug/320035>. Otherwise if the
161
300
# user specifically requests either text or no progress bars, always
162
301
# do that. otherwise, guess based on $TERM and tty presence.
163
302
if self.is_quiet():
164
303
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()
304
pb_type = config.GlobalStack().get('progress_bar')
305
if pb_type == 'none': # Explicit requirement
306
return NullProgressView()
307
if (pb_type == 'text' or # Explicit requirement
308
progress._supports_progress(self.stderr)): # Guess
309
return TextProgressView(self.stderr)
310
# No explicit requirement and no successful guess
311
return NullProgressView()
174
313
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)
314
return TextUIOutputStream(self, self.stdout, encoding, encoding_type)
194
316
def note(self, msg):
195
317
"""Write an already-formatted message, clearing the progress bar if necessary."""
262
387
# be easier to test; that has a lot of test fallout so for now just
263
388
# new code can call this
264
389
if warning_id not in self.suppressed_warnings:
265
self.stderr.write(self.format_user_warning(warning_id, message_args) +
390
warning = self.format_user_warning(warning_id, message_args)
391
self.stderr.write(warning + '\n')
394
def pad_to_width(line, width, encoding_hint='ascii'):
395
"""Truncate or pad unicode line to width.
397
This is best-effort for now, and strings containing control codes or
398
non-ascii text may be cut and padded incorrectly.
400
s = line.encode(encoding_hint, 'replace')
401
return (b'%-*.*s' % (width, width, s)).decode(encoding_hint)
269
404
class TextProgressView(object):
270
405
"""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.
407
This shows one line of text, including possibly a network indicator,
408
spinner, progress bar, message, etc.
275
410
One instance of this is created and held by the UI, and fed updates when a
276
411
task wants to be painted.
364
512
t = t._parent_task
366
514
m = t.msg + ':' + m
369
517
def _render_line(self):
370
518
bar_string = self._render_bar()
371
519
if self._last_task:
372
task_msg = self._format_task(self._last_task)
520
task_part, counter_part = self._format_task(self._last_task)
522
task_part = counter_part = ''
375
523
if self._last_task and not self._last_task.show_transport_activity:
378
526
trans = self._last_transport_msg
381
return (bar_string + trans + task_msg)
527
# the bar separates the transport activity from the message, so even
528
# if there's no bar or spinner, we must show something if both those
530
if (task_part or trans) and not bar_string:
532
# preferentially truncate the task message if we don't have enough
534
avail_width = self._avail_width()
535
if avail_width is not None:
536
# if terminal avail_width is unknown, don't truncate
537
current_len = len(bar_string) + len(trans) + \
538
len(task_part) + len(counter_part)
539
# GZ 2017-04-22: Should measure and truncate task_part properly
540
gap = current_len - avail_width
542
task_part = task_part[:-gap - 2] + '..'
543
s = trans + bar_string + task_part + counter_part
544
if avail_width is not None:
545
if len(s) < avail_width:
546
s = s.ljust(avail_width)
547
elif len(s) > avail_width:
383
551
def _repaint(self):
384
552
s = self._render_line()
481
649
self._term_file.write(msg + '\n')
652
def _get_stream_encoding(stream):
653
encoding = config.GlobalStack().get('output_encoding')
655
encoding = getattr(stream, "encoding", None)
657
encoding = osutils.get_terminal_encoding(trace=True)
661
def _unwrap_stream(stream):
662
inner = getattr(stream, "buffer", None)
664
inner = getattr(stream, "stream", stream)
668
def _wrap_in_stream(stream, encoding=None, errors='replace'):
670
encoding = _get_stream_encoding(stream)
671
encoded_stream = codecs.getreader(encoding)(stream, errors=errors)
672
encoded_stream.encoding = encoding
673
return encoded_stream
676
def _wrap_out_stream(stream, encoding=None, errors='replace'):
678
encoding = _get_stream_encoding(stream)
679
encoded_stream = codecs.getwriter(encoding)(stream, errors=errors)
680
encoded_stream.encoding = encoding
681
return encoded_stream
484
684
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
685
"""Decorates stream to interact better with progress and change encoding.
687
Before writing to the wrapped stream, progress is cleared. Callers must
688
ensure bulk output is terminated with a newline so progress won't overwrite
691
Additionally, the encoding and errors behaviour of the underlying stream
692
can be changed at this point. If errors is set to 'exact' raw bytes may be
693
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):
696
def __init__(self, ui_factory, stream, encoding=None, errors='strict'):
498
697
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)
698
# GZ 2017-05-21: Clean up semantics when callers are made saner.
699
inner = _unwrap_stream(stream)
700
self.raw_stream = None
701
if errors == "exact":
703
self.raw_stream = inner
705
self.wrapped_stream = stream
707
encoding = _get_stream_encoding(stream)
709
self.wrapped_stream = _wrap_out_stream(inner, encoding, errors)
711
encoding = self.wrapped_stream.encoding
712
self.encoding = encoding
715
def _write(self, to_write):
716
if isinstance(to_write, bytes):
718
to_write = to_write.decode(self.encoding, self.errors)
719
except UnicodeDecodeError:
720
self.raw_stream.write(to_write)
722
self.wrapped_stream.write(to_write)
506
725
self.ui_factory.clear_term()