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
if os.environ.get('BRZ_TEXTUI_INPUT') != 'line-based' and \
69
self.ui.stdin == sys.stdin and self.ui.stdin.isatty():
70
self.line_based = False
73
self.line_based = True
74
self.echo_back = not self.ui.stdin.isatty()
76
def _build_alternatives(self, msg, choices, default):
77
"""Parse choices string.
79
Setup final prompt and the lists of choices and associated
84
self.alternatives = {}
85
choices = choices.split('\n')
86
if default is not None and default not in range(0, len(choices)):
87
raise ValueError("invalid default index")
89
name = c.replace('&', '').lower()
90
choice = (name, index)
91
if name in self.alternatives:
92
raise ValueError("duplicated choice: %s" % name)
93
self.alternatives[name] = choice
94
shortcut = c.find('&')
95
if -1 != shortcut and (shortcut + 1) < len(c):
97
help += '[' + c[shortcut + 1] + ']'
98
help += c[(shortcut + 2):]
99
shortcut = c[shortcut + 1]
101
c = c.replace('&', '')
103
help = '[%s]%s' % (shortcut, c[1:])
104
shortcut = shortcut.lower()
105
if shortcut in self.alternatives:
106
raise ValueError("duplicated shortcut: %s" % shortcut)
107
self.alternatives[shortcut] = choice
108
# Add redirections for default.
110
self.alternatives[''] = choice
111
self.alternatives['\r'] = choice
112
help_list.append(help)
115
self.prompt = u'%s (%s): ' % (msg, ', '.join(help_list))
118
line = self.ui.stdin.readline()
124
char = osutils.getchar()
125
if char == chr(3): # INTR
126
raise KeyboardInterrupt
127
if char == chr(4): # EOF (^d, C-d)
129
return char.decode("ascii", "replace")
132
"""Keep asking the user until a valid choice is made.
135
getchoice = self._getline
137
getchoice = self._getchar
141
if 1 == iter or self.line_based:
142
self.ui.prompt(self.prompt)
146
self.ui.stderr.write(u'\n')
148
except KeyboardInterrupt:
149
self.ui.stderr.write(u'\n')
151
choice = choice.lower()
152
if choice not in self.alternatives:
153
# Not a valid choice, keep on asking.
155
name, index = self.alternatives[choice]
157
self.ui.stderr.write(name + u'\n')
161
opt_progress_bar = config.Option(
162
'progress_bar', help='Progress bar type.',
163
default_from_env=['BRZ_PROGRESS_BAR'], default=None,
48
167
class TextUIFactory(UIFactory):
49
"""A UI factory for Text user interefaces."""
168
"""A UI factory for Text user interfaces."""
55
"""Create a TextUIFactory.
170
def __init__(self, stdin, stdout, stderr):
171
"""Create a TextUIFactory."""
57
172
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
173
self.stdin = stdin
61
174
self.stdout = stdout
62
175
self.stderr = stderr
63
176
# paints progress, network activity, etc
64
177
self._progress_view = self.make_progress_view()
65
# hook up the signals to watch for terminal size changes
179
def choose(self, msg, choices, default=None):
180
"""Prompt the user for a list of alternatives.
182
Support both line-based and char-based editing.
184
In line-based mode, both the shortcut and full choice name are valid
185
answers, e.g. for choose('prompt', '&yes\n&no'): 'y', ' Y ', ' yes',
186
'YES ' are all valid input lines for choosing 'yes'.
188
An empty line, when in line-based mode, or pressing enter in char-based
189
mode will select the default choice (if any).
191
Choice is echoed back if:
192
- input is char-based; which means a controlling terminal is available,
193
and osutils.getchar is used
194
- input is line-based, and no controlling terminal is available
197
choose_ui = _ChooseUI(self, msg, choices, default)
198
return choose_ui.interact()
68
200
def be_quiet(self, state):
69
201
if state and not self._quiet:
149
270
username = self.stdin.readline()
152
elif username[-1] == '\n':
153
username = username[:-1]
274
if username[-1] == '\n':
275
username = username[:-1]
156
278
def make_progress_view(self):
157
279
"""Construct and return a new ProgressView subclass for this UI.
159
281
# with --quiet, never any progress view
160
# <https://bugs.edge.launchpad.net/bzr/+bug/320035>. Otherwise if the
282
# <https://bugs.launchpad.net/bzr/+bug/320035>. Otherwise if the
161
283
# user specifically requests either text or no progress bars, always
162
284
# do that. otherwise, guess based on $TERM and tty presence.
163
285
if self.is_quiet():
164
286
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()
287
pb_type = config.GlobalStack().get('progress_bar')
288
if pb_type == 'none': # Explicit requirement
289
return NullProgressView()
290
if (pb_type == 'text' # Explicit requirement
291
or progress._supports_progress(self.stderr)): # Guess
292
return TextProgressView(self.stderr)
293
# No explicit requirement and no successful guess
294
return NullProgressView()
174
296
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)
297
return TextUIOutputStream(self, self.stdout, encoding, encoding_type)
194
299
def note(self, msg):
195
300
"""Write an already-formatted message, clearing the progress bar if necessary."""
262
369
# be easier to test; that has a lot of test fallout so for now just
263
370
# new code can call this
264
371
if warning_id not in self.suppressed_warnings:
265
self.stderr.write(self.format_user_warning(warning_id, message_args) +
372
warning = self.format_user_warning(warning_id, message_args)
373
self.stderr.write(warning + '\n')
376
def pad_to_width(line, width, encoding_hint='ascii'):
377
"""Truncate or pad unicode line to width.
379
This is best-effort for now, and strings containing control codes or
380
non-ascii text may be cut and padded incorrectly.
382
s = line.encode(encoding_hint, 'replace')
383
return (b'%-*.*s' % (width, width, s)).decode(encoding_hint)
269
386
class TextProgressView(object):
270
387
"""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.
389
This shows one line of text, including possibly a network indicator,
390
spinner, progress bar, message, etc.
275
392
One instance of this is created and held by the UI, and fed updates when a
276
393
task wants to be painted.
364
494
t = t._parent_task
366
496
m = t.msg + ':' + m
369
499
def _render_line(self):
370
500
bar_string = self._render_bar()
371
501
if self._last_task:
372
task_msg = self._format_task(self._last_task)
502
task_part, counter_part = self._format_task(self._last_task)
504
task_part = counter_part = ''
375
505
if self._last_task and not self._last_task.show_transport_activity:
378
508
trans = self._last_transport_msg
381
return (bar_string + trans + task_msg)
509
# the bar separates the transport activity from the message, so even
510
# if there's no bar or spinner, we must show something if both those
512
if (task_part or trans) and not bar_string:
514
# preferentially truncate the task message if we don't have enough
516
avail_width = self._avail_width()
517
if avail_width is not None:
518
# if terminal avail_width is unknown, don't truncate
519
current_len = len(bar_string) + len(trans) + len(task_part) + len(counter_part)
520
# GZ 2017-04-22: Should measure and truncate task_part properly
521
gap = current_len - avail_width
523
task_part = task_part[:-gap-2] + '..'
524
s = trans + bar_string + task_part + counter_part
525
if avail_width is not None:
526
if len(s) < avail_width:
527
s = s.ljust(avail_width)
528
elif len(s) > avail_width:
383
532
def _repaint(self):
384
533
s = self._render_line()
481
630
self._term_file.write(msg + '\n')
633
def _get_stream_encoding(stream):
634
encoding = config.GlobalStack().get('output_encoding')
636
encoding = getattr(stream, "encoding", None)
638
encoding = osutils.get_terminal_encoding(trace=True)
642
def _unwrap_stream(stream):
643
inner = getattr(stream, "buffer", None)
645
inner = getattr(stream, "stream", None)
649
def _wrap_in_stream(stream, encoding=None, errors='replace'):
651
encoding = _get_stream_encoding(stream)
652
encoded_stream = codecs.getreader(encoding)(stream, errors=errors)
653
encoded_stream.encoding = encoding
654
return encoded_stream
657
def _wrap_out_stream(stream, encoding=None, errors='replace'):
659
encoding = _get_stream_encoding(stream)
660
encoded_stream = codecs.getwriter(encoding)(stream, errors=errors)
661
encoded_stream.encoding = encoding
662
return encoded_stream
484
665
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
666
"""Decorates stream to interact better with progress and change encoding.
668
Before writing to the wrapped stream, progress is cleared. Callers must
669
ensure bulk output is terminated with a newline so progress won't overwrite
672
Additionally, the encoding and errors behaviour of the underlying stream
673
can be changed at this point. If errors is set to 'exact' raw bytes may be
674
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):
677
def __init__(self, ui_factory, stream, encoding=None, errors='strict'):
498
678
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)
679
# GZ 2017-05-21: Clean up semantics when callers are made saner.
680
inner = _unwrap_stream(stream)
681
self.raw_stream = None
682
if errors == "exact":
684
self.raw_stream = inner
686
self.wrapped_stream = stream
688
encoding = _get_stream_encoding(stream)
690
self.wrapped_stream = _wrap_out_stream(inner, encoding, errors)
692
encoding = self.wrapped_stream.encoding
693
self.encoding = encoding
696
def _write(self, to_write):
697
if isinstance(to_write, bytes):
699
to_write = to_write.decode(self.encoding, self.errors)
700
except UnicodeDecodeError:
701
self.raw_stream.write(to_write)
703
self.wrapped_stream.write(to_write)
506
706
self.ui_factory.clear_term()