/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/ui/text.py

  • Committer: Gustav Hartvigsson
  • Date: 2021-01-09 21:36:27 UTC
  • Revision ID: gustav.hartvigsson@gmail.com-20210109213627-h1xwcutzy9m7a99b
Added 'Case Preserving Working Tree Use Cases' from Canonical Wiki

* Addod a page from the Canonical Bazaar wiki
  with information on the scmeatics of case
  perserving filesystems an a case insensitive
  filesystem works.
  
  * Needs re-work, but this will do as it is the
    same inforamoton as what was on the linked
    page in the currint documentation.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Canonical Ltd
 
1
# Copyright (C) 2005-2011 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
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
16
16
 
17
 
 
18
 
"""Text UI, write output to the console.
19
 
"""
 
17
"""Text UI, write output to the console."""
20
18
 
21
19
import codecs
22
 
import getpass
 
20
import io
23
21
import os
24
22
import sys
25
 
import time
26
23
import warnings
27
24
 
28
 
from bzrlib.lazy_import import lazy_import
 
25
from ..lazy_import import lazy_import
29
26
lazy_import(globals(), """
30
 
from bzrlib import (
 
27
import getpass
 
28
import time
 
29
 
 
30
from breezy import (
31
31
    debug,
32
32
    progress,
 
33
    )
 
34
""")
 
35
 
 
36
from .. import (
 
37
    config,
33
38
    osutils,
34
 
    symbol_versioning,
35
39
    trace,
36
40
    )
37
 
 
38
 
""")
39
 
 
40
 
from bzrlib.osutils import watch_sigwinch
41
 
 
42
 
from bzrlib.ui import (
 
41
from . import (
 
42
    NullProgressView,
43
43
    UIFactory,
44
 
    NullProgressView,
45
44
    )
46
45
 
47
46
 
 
47
class _ChooseUI(object):
 
48
 
 
49
    """ Helper class for choose implementation.
 
50
    """
 
51
 
 
52
    def __init__(self, ui, msg, choices, default):
 
53
        self.ui = ui
 
54
        self._setup_mode()
 
55
        self._build_alternatives(msg, choices, default)
 
56
 
 
57
    def _setup_mode(self):
 
58
        """Setup input mode (line-based, char-based) and echo-back.
 
59
 
 
60
        Line-based input is used if the BRZ_TEXTUI_INPUT environment
 
61
        variable is set to 'line-based', or if there is no controlling
 
62
        terminal.
 
63
        """
 
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):
 
67
            self.line_based = False
 
68
            self.echo_back = True
 
69
        else:
 
70
            self.line_based = True
 
71
            self.echo_back = not is_tty
 
72
 
 
73
    def _build_alternatives(self, msg, choices, default):
 
74
        """Parse choices string.
 
75
 
 
76
        Setup final prompt and the lists of choices and associated
 
77
        shortcuts.
 
78
        """
 
79
        index = 0
 
80
        help_list = []
 
81
        self.alternatives = {}
 
82
        choices = choices.split('\n')
 
83
        if default is not None and default not in range(0, len(choices)):
 
84
            raise ValueError("invalid default index")
 
85
        for c in choices:
 
86
            name = c.replace('&', '').lower()
 
87
            choice = (name, index)
 
88
            if name in self.alternatives:
 
89
                raise ValueError("duplicated choice: %s" % name)
 
90
            self.alternatives[name] = choice
 
91
            shortcut = c.find('&')
 
92
            if -1 != shortcut and (shortcut + 1) < len(c):
 
93
                help = c[:shortcut]
 
94
                help += '[' + c[shortcut + 1] + ']'
 
95
                help += c[(shortcut + 2):]
 
96
                shortcut = c[shortcut + 1]
 
97
            else:
 
98
                c = c.replace('&', '')
 
99
                shortcut = c[0]
 
100
                help = '[%s]%s' % (shortcut, c[1:])
 
101
            shortcut = shortcut.lower()
 
102
            if shortcut in self.alternatives:
 
103
                raise ValueError("duplicated shortcut: %s" % shortcut)
 
104
            self.alternatives[shortcut] = choice
 
105
            # Add redirections for default.
 
106
            if index == default:
 
107
                self.alternatives[''] = choice
 
108
                self.alternatives['\r'] = choice
 
109
            help_list.append(help)
 
110
            index += 1
 
111
 
 
112
        self.prompt = u'%s (%s): ' % (msg, ', '.join(help_list))
 
113
 
 
114
    def _getline(self):
 
115
        line = self.ui.stdin.readline()
 
116
        if '' == line:
 
117
            raise EOFError
 
118
        return line.strip()
 
119
 
 
120
    def _getchar(self):
 
121
        char = osutils.getchar()
 
122
        if char == chr(3):  # INTR
 
123
            raise KeyboardInterrupt
 
124
        if char == chr(4):  # EOF (^d, C-d)
 
125
            raise EOFError
 
126
        if isinstance(char, bytes):
 
127
            return char.decode('ascii', 'replace')
 
128
        return char
 
129
 
 
130
    def interact(self):
 
131
        """Keep asking the user until a valid choice is made.
 
132
        """
 
133
        if self.line_based:
 
134
            getchoice = self._getline
 
135
        else:
 
136
            getchoice = self._getchar
 
137
        iter = 0
 
138
        while True:
 
139
            iter += 1
 
140
            if 1 == iter or self.line_based:
 
141
                self.ui.prompt(self.prompt)
 
142
            try:
 
143
                choice = getchoice()
 
144
            except EOFError:
 
145
                self.ui.stderr.write(u'\n')
 
146
                return None
 
147
            except KeyboardInterrupt:
 
148
                self.ui.stderr.write(u'\n')
 
149
                raise
 
150
            choice = choice.lower()
 
151
            if choice not in self.alternatives:
 
152
                # Not a valid choice, keep on asking.
 
153
                continue
 
154
            name, index = self.alternatives[choice]
 
155
            if self.echo_back:
 
156
                self.ui.stderr.write(name + u'\n')
 
157
            return index
 
158
 
 
159
 
 
160
opt_progress_bar = config.Option(
 
161
    'progress_bar', help='Progress bar type.',
 
162
    default_from_env=['BRZ_PROGRESS_BAR'], default=None,
 
163
    invalid='error')
 
164
 
 
165
 
48
166
class TextUIFactory(UIFactory):
49
 
    """A UI factory for Text user interefaces."""
 
167
    """A UI factory for Text user interfaces."""
50
168
 
51
 
    def __init__(self,
52
 
                 stdin=None,
53
 
                 stdout=None,
54
 
                 stderr=None):
55
 
        """Create a TextUIFactory.
56
 
        """
 
169
    def __init__(self, stdin, stdout, stderr):
 
170
        """Create a TextUIFactory."""
57
171
        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
172
        self.stdin = stdin
61
173
        self.stdout = stdout
62
174
        self.stderr = stderr
 
175
        self._progress_view = NullProgressView()
 
176
 
 
177
    def __enter__(self):
 
178
        # Choose default encoding and handle py2/3 differences
 
179
        self._setup_streams()
63
180
        # paints progress, network activity, etc
64
181
        self._progress_view = self.make_progress_view()
65
 
        # hook up the signals to watch for terminal size changes
66
 
        watch_sigwinch()
 
182
        return self
 
183
 
 
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)
 
191
 
 
192
    def choose(self, msg, choices, default=None):
 
193
        """Prompt the user for a list of alternatives.
 
194
 
 
195
        Support both line-based and char-based editing.
 
196
 
 
197
        In line-based mode, both the shortcut and full choice name are valid
 
198
        answers, e.g. for choose('prompt', '&yes\n&no'): 'y', ' Y ', ' yes',
 
199
        'YES ' are all valid input lines for choosing 'yes'.
 
200
 
 
201
        An empty line, when in line-based mode, or pressing enter in char-based
 
202
        mode will select the default choice (if any).
 
203
 
 
204
        Choice is echoed back if:
 
205
        - input is char-based; which means a controlling terminal is available,
 
206
          and osutils.getchar is used
 
207
        - input is line-based, and no controlling terminal is available
 
208
        """
 
209
 
 
210
        choose_ui = _ChooseUI(self, msg, choices, default)
 
211
        return choose_ui.interact()
67
212
 
68
213
    def be_quiet(self, state):
69
214
        if state and not self._quiet:
82
227
        # to clear it.  We might need to separately check for the case of
83
228
        self._progress_view.clear()
84
229
 
85
 
    def get_boolean(self, prompt):
86
 
        while True:
87
 
            self.prompt(prompt + "? [y/n]: ")
88
 
            line = self.stdin.readline().lower()
89
 
            if line in ('y\n', 'yes\n'):
90
 
                return True
91
 
            elif line in ('n\n', 'no\n'):
92
 
                return False
93
 
            elif line in ('', None):
94
 
                # end-of-file; possibly should raise an error here instead
95
 
                return None
96
 
 
97
230
    def get_integer(self, prompt):
98
231
        while True:
99
232
            self.prompt(prompt)
114
247
            password = self.stdin.readline()
115
248
            if not password:
116
249
                password = None
117
 
            elif password[-1] == '\n':
118
 
                password = password[:-1]
 
250
            else:
 
251
                if password[-1] == '\n':
 
252
                    password = password[:-1]
119
253
        return password
120
254
 
121
 
    def get_password(self, prompt='', **kwargs):
 
255
    def get_password(self, prompt=u'', **kwargs):
122
256
        """Prompt the user for a password.
123
257
 
124
258
        :param prompt: The prompt to present the user
149
283
        username = self.stdin.readline()
150
284
        if not username:
151
285
            username = None
152
 
        elif username[-1] == '\n':
153
 
            username = username[:-1]
 
286
        else:
 
287
            if username[-1] == '\n':
 
288
                username = username[:-1]
154
289
        return username
155
290
 
156
291
    def make_progress_view(self):
157
292
        """Construct and return a new ProgressView subclass for this UI.
158
293
        """
159
294
        # with --quiet, never any progress view
160
 
        # <https://bugs.edge.launchpad.net/bzr/+bug/320035>.  Otherwise if the
 
295
        # <https://bugs.launchpad.net/bzr/+bug/320035>.  Otherwise if the
161
296
        # user specifically requests either text or no progress bars, always
162
297
        # do that.  otherwise, guess based on $TERM and tty presence.
163
298
        if self.is_quiet():
164
299
            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)
171
 
        else:
172
 
            return NullProgressView()
 
300
        pb_type = config.GlobalStack().get('progress_bar')
 
301
        if pb_type == 'none':  # Explicit requirement
 
302
            return NullProgressView()
 
303
        if (pb_type == 'text' or # Explicit requirement
 
304
                progress._supports_progress(self.stderr)):  # Guess
 
305
            return TextProgressView(self.stderr)
 
306
        # No explicit requirement and no successful guess
 
307
        return NullProgressView()
173
308
 
174
309
    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)
181
 
                if fileno:
182
 
                    import msvcrt
183
 
                    msvcrt.setmode(fileno(), os.O_BINARY)
184
 
            return TextUIOutputStream(self, self.stdout)
185
 
        else:
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)
 
310
        return TextUIOutputStream(self, self.stdout, encoding, encoding_type)
193
311
 
194
312
    def note(self, msg):
195
313
        """Write an already-formatted message, clearing the progress bar if necessary."""
198
316
 
199
317
    def prompt(self, prompt, **kwargs):
200
318
        """Emit prompt on the CLI.
201
 
        
 
319
 
202
320
        :param kwargs: Dictionary of arguments to insert into the prompt,
203
321
            to allow UIs to reformat the prompt.
204
322
        """
 
323
        if not isinstance(prompt, str):
 
324
            raise ValueError("prompt %r not a unicode string" % prompt)
205
325
        if kwargs:
206
326
            # See <https://launchpad.net/bugs/365891>
207
327
            prompt = prompt % kwargs
208
 
        prompt = prompt.encode(osutils.get_terminal_encoding(), 'replace')
209
328
        self.clear_term()
 
329
        self.stdout.flush()
210
330
        self.stderr.write(prompt)
 
331
        self.stderr.flush()
211
332
 
212
333
    def report_transport_activity(self, transport, byte_count, direction):
213
334
        """Called by transports as they do IO.
216
337
        By default it does nothing.
217
338
        """
218
339
        self._progress_view.show_transport_activity(transport,
219
 
            direction, byte_count)
 
340
                                                    direction, byte_count)
220
341
 
221
342
    def log_transport_activity(self, display=False):
222
343
        """See UIFactory.log_transport_activity()"""
240
361
        """
241
362
        if not self._task_stack:
242
363
            warnings.warn("%r updated but no tasks are active" %
243
 
                (task,))
 
364
                          (task,))
244
365
        elif task != self._task_stack[-1]:
245
366
            # We used to check it was the top task, but it's hard to always
246
367
            # get this right and it's not necessarily useful: any actual
247
368
            # problems will be evident in use
248
 
            #warnings.warn("%r is not the top progress task %r" %
 
369
            # warnings.warn("%r is not the top progress task %r" %
249
370
            #     (task, self._task_stack[-1]))
250
371
            pass
251
372
        self._progress_view.show_progress(task)
262
383
        # be easier to test; that has a lot of test fallout so for now just
263
384
        # new code can call this
264
385
        if warning_id not in self.suppressed_warnings:
265
 
            self.stderr.write(self.format_user_warning(warning_id, message_args) +
266
 
                '\n')
 
386
            warning = self.format_user_warning(warning_id, message_args)
 
387
            self.stderr.write(warning + '\n')
 
388
 
 
389
 
 
390
def pad_to_width(line, width, encoding_hint='ascii'):
 
391
    """Truncate or pad unicode line to width.
 
392
 
 
393
    This is best-effort for now, and strings containing control codes or
 
394
    non-ascii text may be cut and padded incorrectly.
 
395
    """
 
396
    s = line.encode(encoding_hint, 'replace')
 
397
    return (b'%-*.*s' % (width, width, s)).decode(encoding_hint)
267
398
 
268
399
 
269
400
class TextProgressView(object):
270
401
    """Display of progress bar and other information on a tty.
271
402
 
272
 
    This shows one line of text, including possibly a network indicator, spinner,
273
 
    progress bar, message, etc.
 
403
    This shows one line of text, including possibly a network indicator,
 
404
    spinner, progress bar, message, etc.
274
405
 
275
406
    One instance of this is created and held by the UI, and fed updates when a
276
407
    task wants to be painted.
281
412
    this only prints the stack from the nominated current task up to the root.
282
413
    """
283
414
 
284
 
    def __init__(self, term_file):
 
415
    def __init__(self, term_file, encoding=None, errors=None):
285
416
        self._term_file = term_file
 
417
        if encoding is None:
 
418
            self._encoding = getattr(term_file, "encoding", None) or "ascii"
 
419
        else:
 
420
            self._encoding = encoding
286
421
        # true when there's output on the screen we may need to clear
287
422
        self._have_output = False
288
423
        self._last_transport_msg = ''
297
432
        self._bytes_by_direction = {'unknown': 0, 'read': 0, 'write': 0}
298
433
        self._first_byte_time = None
299
434
        self._fraction = 0
300
 
        # force the progress bar to be off, as at the moment it doesn't 
 
435
        # force the progress bar to be off, as at the moment it doesn't
301
436
        # correspond reliably to overall command progress
302
437
        self.enable_bar = False
303
438
 
304
 
    def _show_line(self, s):
305
 
        # sys.stderr.write("progress %r\n" % s)
306
 
        width = osutils.terminal_width()
 
439
    def _avail_width(self):
 
440
        # we need one extra space for terminals that wrap on last char
 
441
        w = osutils.terminal_width()
 
442
        if w is None:
 
443
            return None
 
444
        else:
 
445
            return w - 1
 
446
 
 
447
    def _show_line(self, u):
 
448
        width = self._avail_width()
307
449
        if width is not None:
308
 
            # we need one extra space for terminals that wrap on last char
309
 
            width = width - 1
310
 
            s = '%-*.*s' % (width, width, s)
311
 
        self._term_file.write('\r' + s + '\r')
 
450
            u = pad_to_width(u, width, encoding_hint=self._encoding)
 
451
        self._term_file.write('\r' + u + '\r')
312
452
 
313
453
    def clear(self):
314
454
        if self._have_output:
318
458
    def _render_bar(self):
319
459
        # return a string for the progress bar itself
320
460
        if self.enable_bar and (
321
 
            (self._last_task is None) or self._last_task.show_bar):
 
461
                (self._last_task is None) or self._last_task.show_bar):
322
462
            # If there's no task object, we show space for the bar anyhow.
323
463
            # That's because most invocations of bzr will end showing progress
324
464
            # at some point, though perhaps only after doing some initial IO.
325
465
            # It looks better to draw the progress bar initially rather than
326
466
            # to have what looks like an incomplete progress bar.
327
 
            spin_str =  r'/-\|'[self._spin_pos % 4]
 
467
            spin_str = r'/-\|'[self._spin_pos % 4]
328
468
            self._spin_pos += 1
329
469
            cols = 20
330
470
            if self._last_task is None:
334
474
                completion_fraction = \
335
475
                    self._last_task._overall_completion_fraction() or 0
336
476
            if (completion_fraction < self._fraction and 'progress' in
337
 
                debug.debug_flags):
338
 
                import pdb;pdb.set_trace()
 
477
                    debug.debug_flags):
 
478
                debug.set_trace()
339
479
            self._fraction = completion_fraction
340
480
            markers = int(round(float(cols) * completion_fraction)) - 1
341
481
            bar_str = '[' + ('#' * markers + spin_str).ljust(cols) + '] '
342
482
            return bar_str
343
483
        elif (self._last_task is None) or self._last_task.show_spinner:
344
484
            # The last task wanted just a spinner, no bar
345
 
            spin_str =  r'/-\|'[self._spin_pos % 4]
 
485
            spin_str = r'/-\|'[self._spin_pos % 4]
346
486
            self._spin_pos += 1
347
487
            return spin_str + ' '
348
488
        else:
349
489
            return ''
350
490
 
351
491
    def _format_task(self, task):
 
492
        """Format task-specific parts of progress bar.
 
493
 
 
494
        :returns: (text_part, counter_part) both unicode strings.
 
495
        """
352
496
        if not task.show_count:
353
497
            s = ''
354
498
        elif task.current_cnt is not None and task.total_cnt is not None:
364
508
            t = t._parent_task
365
509
            if t.msg:
366
510
                m = t.msg + ':' + m
367
 
        return m + s
 
511
        return m, s
368
512
 
369
513
    def _render_line(self):
370
514
        bar_string = self._render_bar()
371
515
        if self._last_task:
372
 
            task_msg = self._format_task(self._last_task)
 
516
            task_part, counter_part = self._format_task(self._last_task)
373
517
        else:
374
 
            task_msg = ''
 
518
            task_part = counter_part = ''
375
519
        if self._last_task and not self._last_task.show_transport_activity:
376
520
            trans = ''
377
521
        else:
378
522
            trans = self._last_transport_msg
379
 
            if trans:
380
 
                trans += ' | '
381
 
        return (bar_string + trans + task_msg)
 
523
        # the bar separates the transport activity from the message, so even
 
524
        # if there's no bar or spinner, we must show something if both those
 
525
        # fields are present
 
526
        if (task_part or trans) and not bar_string:
 
527
            bar_string = '| '
 
528
        # preferentially truncate the task message if we don't have enough
 
529
        # space
 
530
        avail_width = self._avail_width()
 
531
        if avail_width is not None:
 
532
            # 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
 
536
            gap = current_len - avail_width
 
537
            if gap > 0:
 
538
                task_part = task_part[:-gap - 2] + '..'
 
539
        s = trans + bar_string + task_part + counter_part
 
540
        if avail_width is not None:
 
541
            if len(s) < avail_width:
 
542
                s = s.ljust(avail_width)
 
543
            elif len(s) > avail_width:
 
544
                s = s[:avail_width]
 
545
        return s
382
546
 
383
547
    def _repaint(self):
384
548
        s = self._render_line()
387
551
 
388
552
    def show_progress(self, task):
389
553
        """Called by the task object when it has changed.
390
 
        
391
 
        :param task: The top task object; its parents are also included 
 
554
 
 
555
        :param task: The top task object; its parents are also included
392
556
            by following links.
393
557
        """
394
558
        must_update = task is not self._last_task
437
601
        elif now >= (self._transport_update_time + 0.5):
438
602
            # guard against clock stepping backwards, and don't update too
439
603
            # often
440
 
            rate = (self._bytes_since_update
441
 
                    / (now - self._transport_update_time))
 
604
            rate = (self._bytes_since_update /
 
605
                    (now - self._transport_update_time))
442
606
            # using base-10 units (see HACKING.txt).
443
 
            msg = ("%6dkB %5dkB/s" %
444
 
                    (self._total_byte_count / 1000, int(rate) / 1000,))
 
607
            msg = ("%6dkB %5dkB/s " %
 
608
                   (self._total_byte_count / 1000, int(rate) / 1000,))
445
609
            self._transport_update_time = now
446
610
            self._last_repaint = now
447
611
            self._bytes_since_update = 0
464
628
                  bps / 1000.,
465
629
                  self._bytes_by_direction['read'] / 1000.,
466
630
                  self._bytes_by_direction['write'] / 1000.,
467
 
                 ))
 
631
                  ))
468
632
        if self._bytes_by_direction['unknown'] > 0:
469
633
            msg += ' u:%.0fkB)' % (
470
634
                self._bytes_by_direction['unknown'] / 1000.
481
645
            self._term_file.write(msg + '\n')
482
646
 
483
647
 
 
648
def _get_stream_encoding(stream):
 
649
    encoding = config.GlobalStack().get('output_encoding')
 
650
    if encoding is None:
 
651
        encoding = getattr(stream, "encoding", None)
 
652
    if encoding is None:
 
653
        encoding = osutils.get_terminal_encoding(trace=True)
 
654
    return encoding
 
655
 
 
656
 
 
657
def _unwrap_stream(stream):
 
658
    inner = getattr(stream, "buffer", None)
 
659
    if inner is None:
 
660
        inner = getattr(stream, "stream", stream)
 
661
    return inner
 
662
 
 
663
 
 
664
def _wrap_in_stream(stream, encoding=None, errors='replace'):
 
665
    if encoding is None:
 
666
        encoding = _get_stream_encoding(stream)
 
667
    # Attempt to wrap using io.open if possible, since that can do
 
668
    # line-buffering.
 
669
    try:
 
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
 
675
    else:
 
676
        return io.open(fileno, encoding=encoding, errors=errors, mode='r', buffering=1)
 
677
 
 
678
 
 
679
def _wrap_out_stream(stream, encoding=None, errors='replace'):
 
680
    if encoding is None:
 
681
        encoding = _get_stream_encoding(stream)
 
682
    encoded_stream = codecs.getwriter(encoding)(stream, errors=errors)
 
683
    encoded_stream.encoding = encoding
 
684
    return encoded_stream
 
685
 
 
686
 
484
687
class TextUIOutputStream(object):
485
 
    """Decorates an output stream so that the terminal is cleared before writing.
486
 
 
487
 
    This is supposed to ensure that the progress bar does not conflict with bulk
488
 
    text output.
 
688
    """Decorates stream to interact better with progress and change encoding.
 
689
 
 
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
 
692
    partial lines.
 
693
 
 
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.
489
697
    """
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
494
 
 
495
 
    # XXX: might need to wrap more methods
496
 
 
497
 
    def __init__(self, ui_factory, wrapped_stream):
 
698
 
 
699
    def __init__(self, ui_factory, stream, encoding=None, errors='strict'):
498
700
        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)
 
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":
 
705
            errors = "strict"
 
706
            self.raw_stream = inner
 
707
        if inner is None:
 
708
            self.wrapped_stream = stream
 
709
            if encoding is None:
 
710
                encoding = _get_stream_encoding(stream)
 
711
        else:
 
712
            self.wrapped_stream = _wrap_out_stream(inner, encoding, errors)
 
713
            if encoding is None:
 
714
                encoding = self.wrapped_stream.encoding
 
715
        self.encoding = encoding
 
716
        self.errors = errors
 
717
 
 
718
    def _write(self, to_write):
 
719
        if isinstance(to_write, bytes):
 
720
            try:
 
721
                to_write = to_write.decode(self.encoding, self.errors)
 
722
            except UnicodeDecodeError:
 
723
                self.raw_stream.write(to_write)
 
724
                return
 
725
        self.wrapped_stream.write(to_write)
504
726
 
505
727
    def flush(self):
506
728
        self.ui_factory.clear_term()
508
730
 
509
731
    def write(self, to_write):
510
732
        self.ui_factory.clear_term()
511
 
        self.wrapped_stream.write(to_write)
 
733
        self._write(to_write)
512
734
 
513
735
    def writelines(self, lines):
514
736
        self.ui_factory.clear_term()
515
 
        self.wrapped_stream.writelines(lines)
 
737
        for line in lines:
 
738
            self._write(line)