1# dialog.py --- A Python interface to the ncurses-based "dialog" utility
2# -*- coding: utf-8 -*-
3#
4# Copyright (C) 2002-2019  Florent Rougon
5# Copyright (C) 2004  Peter Åstrand
6# Copyright (C) 2000  Robb Shecter, Sultanbek Tezadov
7#
8# This library is free software; you can redistribute it and/or
9# modify it under the terms of the GNU Lesser General Public
10# License as published by the Free Software Foundation; either
11# version 2.1 of the License, or (at your option) any later version.
12#
13# This library is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16# Lesser General Public License for more details.
17#
18# You should have received a copy of the GNU Lesser General Public
19# License along with this library; if not, write to the Free Software
20# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston,
21# MA  02110-1301 USA.
22
23"""Python interface to :program:`dialog`-like programs.
24
25This module provides a Python interface to :program:`dialog`-like
26programs such as :program:`dialog` and :program:`Xdialog`.
27
28It provides a :class:`Dialog` class that retains some parameters such as
29the program name and path as well as the values to pass as DIALOG*
30environment variables to the chosen program.
31
32See the pythondialog manual for detailed documentation.
33
34"""
35
36import collections
37import os
38import random
39import re
40import sys
41import tempfile
42import traceback
43import warnings
44from contextlib import contextmanager
45from textwrap import dedent
46
47_VersionInfo = collections.namedtuple(
48    "VersionInfo", ("major", "minor", "micro", "releasesuffix"))
49
50class VersionInfo(_VersionInfo):
51    """Class used to represent the version of pythondialog.
52
53    This class is based on :func:`collections.namedtuple` and has the
54    following field names: ``major``, ``minor``, ``micro``,
55    ``releasesuffix``.
56
57    .. versionadded:: 2.14
58    """
59    def __str__(self):
60        """Return a string representation of the version."""
61        res = ".".join( ( str(elt) for elt in self[:3] ) )
62        if self.releasesuffix:
63            res += self.releasesuffix
64        return res
65
66    def __repr__(self):
67        return "{0}.{1}".format(__name__, _VersionInfo.__repr__(self))
68
69#: Version of pythondialog as a :class:`VersionInfo` instance.
70#:
71#: .. versionadded:: 2.14
72version_info = VersionInfo(3, 5, 2, None)
73#: Version of pythondialog as a string.
74#:
75#: .. versionadded:: 2.12
76__version__ = str(version_info)
77
78
79# This is not for calling programs, only to prepare the shell commands that are
80# written to the debug log when debugging is enabled.
81try:
82    from shlex import quote as _shell_quote
83except ImportError:
84    def _shell_quote(s):
85        return "'%s'" % s.replace("'", "'\"'\"'")
86
87
88# Exceptions raised by this module
89#
90# When adding, suppressing, renaming exceptions or changing their
91# hierarchy, don't forget to update the module's docstring.
92class error(Exception):
93    """Base class for exceptions in pythondialog."""
94    def __init__(self, message=None):
95        self.message = message
96
97    def __str__(self):
98        return self.complete_message()
99
100    def __repr__(self):
101        return "{0}.{1}({2!r})".format(__name__, self.__class__.__name__,
102                                       self.message)
103
104    def complete_message(self):
105        if self.message:
106            return "{0}: {1}".format(self.ExceptionShortDescription,
107                                     self.message)
108        else:
109            return self.ExceptionShortDescription
110
111    ExceptionShortDescription = "{0} generic exception".format("pythondialog")
112
113# For backward-compatibility
114#
115# Note: this exception was not documented (only the specific ones were), so
116#       the backward-compatibility binding could be removed relatively easily.
117PythonDialogException = error
118
119class ExecutableNotFound(error):
120    """Exception raised when the :program:`dialog` executable can't be found."""
121    ExceptionShortDescription = "Executable not found"
122
123class PythonDialogBug(error):
124    """Exception raised when pythondialog finds a bug in his own code."""
125    ExceptionShortDescription = "Bug in pythondialog"
126
127# Yeah, the "Probably" makes it look a bit ugly, but:
128#   - this is more accurate
129#   - this avoids a potential clash with an eventual PythonBug built-in
130#     exception in the Python interpreter...
131class ProbablyPythonBug(error):
132    """Exception raised when pythondialog behaves in a way that seems to \
133indicate a Python bug."""
134    ExceptionShortDescription = "Bug in python, probably"
135
136class BadPythonDialogUsage(error):
137    """Exception raised when pythondialog is used in an incorrect way."""
138    ExceptionShortDescription = "Invalid use of pythondialog"
139
140class PythonDialogSystemError(error):
141    """Exception raised when pythondialog cannot perform a "system \
142operation" (e.g., a system call) that should work in "normal" situations.
143
144    This is a convenience exception: :exc:`PythonDialogIOError`,
145    :exc:`PythonDialogOSError` and
146    :exc:`PythonDialogErrorBeforeExecInChildProcess` all derive from
147    this exception. As a consequence, watching for
148    :exc:`PythonDialogSystemError` instead of the aformentioned
149    exceptions is enough if you don't need precise details about these
150    kinds of errors.
151
152    Don't confuse this exception with Python's builtin
153    :exc:`SystemError` exception.
154
155    """
156    ExceptionShortDescription = "System error"
157
158class PythonDialogOSError(PythonDialogSystemError):
159    """Exception raised when pythondialog catches an :exc:`OSError` exception \
160that should be passed to the calling program."""
161    ExceptionShortDescription = "OS error"
162
163class PythonDialogIOError(PythonDialogOSError):
164    """Exception raised when pythondialog catches an :exc:`IOError` exception \
165that should be passed to the calling program.
166
167    This exception should not be raised starting from Python 3.3, as the
168    built-in exception :exc:`IOError` becomes an alias of
169    :exc:`OSError`.
170
171    .. versionchanged:: 2.12
172       :exc:`PythonDialogIOError` is now a subclass of
173       :exc:`PythonDialogOSError` in order to help with the transition
174       from :exc:`IOError` to :exc:`OSError` in the Python language.
175       With this change, you can safely replace ``except
176       PythonDialogIOError`` clauses with ``except PythonDialogOSError``
177       even if running under Python < 3.3.
178
179    """
180    ExceptionShortDescription = "IO error"
181
182class PythonDialogErrorBeforeExecInChildProcess(PythonDialogSystemError):
183    """Exception raised when an exception is caught in a child process \
184before the exec sytem call (included).
185
186    This can happen in uncomfortable situations such as:
187
188      - the system being out of memory;
189      - the maximum number of open file descriptors being reached;
190      - the :program:`dialog`-like program being removed (or made
191        non-executable) between the time we found it with
192        :func:`_find_in_path` and the time the exec system call
193        attempted to execute it;
194      - the Python program trying to call the :program:`dialog`-like
195        program with arguments that cannot be represented in the user's
196        locale (:envvar:`LC_CTYPE`).
197
198    """
199    ExceptionShortDescription = "Error in a child process before the exec " \
200                                "system call"
201
202class PythonDialogReModuleError(PythonDialogSystemError):
203    """Exception raised when pythondialog catches a :exc:`re.error` exception."""
204    ExceptionShortDescription = "'re' module error"
205
206class UnexpectedDialogOutput(error):
207    """Exception raised when the :program:`dialog`-like program returns \
208something not expected by pythondialog."""
209    ExceptionShortDescription = "Unexpected dialog output"
210
211class DialogTerminatedBySignal(error):
212    """Exception raised when the :program:`dialog`-like program is \
213terminated by a signal."""
214    ExceptionShortDescription = "dialog-like terminated by a signal"
215
216class DialogError(error):
217    """Exception raised when the :program:`dialog`-like program exits \
218with the code indicating an error."""
219    ExceptionShortDescription = "dialog-like terminated due to an error"
220
221class UnableToRetrieveBackendVersion(error):
222    """Exception raised when we cannot retrieve the version string of the \
223:program:`dialog`-like backend.
224
225    .. versionadded:: 2.14
226    """
227    ExceptionShortDescription = "Unable to retrieve the version of the \
228dialog-like backend"
229
230class UnableToParseBackendVersion(error):
231    """Exception raised when we cannot parse the version string of the \
232:program:`dialog`-like backend.
233
234    .. versionadded:: 2.14
235    """
236    ExceptionShortDescription = "Unable to parse as a dialog-like backend \
237version string"
238
239class UnableToParseDialogBackendVersion(UnableToParseBackendVersion):
240    """Exception raised when we cannot parse the version string of the \
241:program:`dialog` backend.
242
243    .. versionadded:: 2.14
244    """
245    ExceptionShortDescription = "Unable to parse as a dialog version string"
246
247class InadequateBackendVersion(error):
248    """Exception raised when the backend version in use is inadequate \
249in a given situation.
250
251    .. versionadded:: 2.14
252    """
253    ExceptionShortDescription = "Inadequate backend version"
254
255
256@contextmanager
257def _OSErrorHandling():
258    try:
259        yield
260    except OSError as e:
261        raise PythonDialogOSError(str(e)) from e
262    except IOError as e:
263        raise PythonDialogIOError(str(e)) from e
264
265
266try:
267    # Values accepted for checklists
268    _on_cre = re.compile(r"on$", re.IGNORECASE)
269    _off_cre = re.compile(r"off$", re.IGNORECASE)
270
271    _calendar_date_cre = re.compile(
272        r"(?P<day>\d\d)/(?P<month>\d\d)/(?P<year>\d\d\d\d)$")
273    _timebox_time_cre = re.compile(
274        r"(?P<hour>\d\d):(?P<minute>\d\d):(?P<second>\d\d)$")
275except re.error as e:
276    raise PythonDialogReModuleError(str(e)) from e
277
278
279# From dialog(1):
280#
281#   All options begin with "--" (two ASCII hyphens, for the benefit of those
282#   using systems with deranged locale support).
283#
284#   A "--" by itself is used as an escape, i.e., the next token on the
285#   command-line is not treated as an option, as in:
286#        dialog --title -- --Not an option
287def _dash_escape(args):
288    """Escape all elements of *args* that need escaping.
289
290    *args* may be any sequence and is not modified by this function.
291    Return a new list where every element that needs escaping has been
292    escaped.
293
294    An element needs escaping when it starts with two ASCII hyphens
295    (``--``). Escaping consists in prepending an element composed of two
296    ASCII hyphens, i.e., the string ``'--'``.
297
298    """
299    res = []
300
301    for arg in args:
302        if arg.startswith("--"):
303            res.extend(("--", arg))
304        else:
305            res.append(arg)
306
307    return res
308
309# We need this function in the global namespace for the lambda
310# expressions in _common_args_syntax to see it when they are called.
311def _dash_escape_nf(args):      # nf: non-first
312    """Escape all elements of *args* that need escaping, except the first one.
313
314    See :func:`_dash_escape` for details. Return a new list.
315
316    """
317    if not args:
318        raise PythonDialogBug("not a non-empty sequence: {0!r}".format(args))
319    l = _dash_escape(args[1:])
320    l.insert(0, args[0])
321    return l
322
323def _simple_option(option, enable):
324    """Turn on or off the simplest :term:`dialog common options`."""
325    if enable:
326        return (option,)
327    else:
328        # This will not add any argument to the command line
329        return ()
330
331
332# This dictionary allows us to write the dialog common options in a Pythonic
333# way (e.g. dialog_instance.checklist(args, ..., title="Foo", no_shadow=True)).
334#
335# Options such as --separate-output should obviously not be set by the user
336# since they affect the parsing of dialog's output:
337_common_args_syntax = {
338    "ascii_lines": lambda enable: _simple_option("--ascii-lines", enable),
339    "aspect": lambda ratio: _dash_escape_nf(("--aspect", str(ratio))),
340    "backtitle": lambda backtitle: _dash_escape_nf(("--backtitle", backtitle)),
341    # Obsolete according to dialog(1)
342    "beep": lambda enable: _simple_option("--beep", enable),
343    # Obsolete according to dialog(1)
344    "beep_after": lambda enable: _simple_option("--beep-after", enable),
345    # Warning: order = y, x!
346    "begin": lambda coords: _dash_escape_nf(
347        ("--begin", str(coords[0]), str(coords[1]))),
348    "cancel_label": lambda s: _dash_escape_nf(("--cancel-label", s)),
349    # Old, unfortunate choice of key, kept for backward compatibility
350    "cancel": lambda s: _dash_escape_nf(("--cancel-label", s)),
351    "clear": lambda enable: _simple_option("--clear", enable),
352    "colors": lambda enable: _simple_option("--colors", enable),
353    "column_separator": lambda s: _dash_escape_nf(("--column-separator", s)),
354    "cr_wrap": lambda enable: _simple_option("--cr-wrap", enable),
355    "create_rc": lambda filename: _dash_escape_nf(("--create-rc", filename)),
356    "date_format": lambda s: _dash_escape_nf(("--date-format", s)),
357    "defaultno": lambda enable: _simple_option("--defaultno", enable),
358    "default_button": lambda s: _dash_escape_nf(("--default-button", s)),
359    "default_item": lambda s: _dash_escape_nf(("--default-item", s)),
360    "exit_label": lambda s: _dash_escape_nf(("--exit-label", s)),
361    "extra_button": lambda enable: _simple_option("--extra-button", enable),
362    "extra_label": lambda s: _dash_escape_nf(("--extra-label", s)),
363    "help": lambda enable: _simple_option("--help", enable),
364    "help_button": lambda enable: _simple_option("--help-button", enable),
365    "help_label": lambda s: _dash_escape_nf(("--help-label", s)),
366    "help_status": lambda enable: _simple_option("--help-status", enable),
367    "help_tags": lambda enable: _simple_option("--help-tags", enable),
368    "hfile": lambda filename: _dash_escape_nf(("--hfile", filename)),
369    "hline": lambda s: _dash_escape_nf(("--hline", s)),
370    "ignore": lambda enable: _simple_option("--ignore", enable),
371    "insecure": lambda enable: _simple_option("--insecure", enable),
372    "item_help": lambda enable: _simple_option("--item-help", enable),
373    "keep_tite": lambda enable: _simple_option("--keep-tite", enable),
374    "keep_window": lambda enable: _simple_option("--keep-window", enable),
375    "max_input": lambda size: _dash_escape_nf(("--max-input", str(size))),
376    "no_cancel": lambda enable: _simple_option("--no-cancel", enable),
377    "nocancel": lambda enable: _simple_option("--nocancel", enable),
378    "no_collapse": lambda enable: _simple_option("--no-collapse", enable),
379    "no_kill": lambda enable: _simple_option("--no-kill", enable),
380    "no_label": lambda s: _dash_escape_nf(("--no-label", s)),
381    "no_lines": lambda enable: _simple_option("--no-lines", enable),
382    "no_mouse": lambda enable: _simple_option("--no-mouse", enable),
383    "no_nl_expand": lambda enable: _simple_option("--no-nl-expand", enable),
384    "no_ok": lambda enable: _simple_option("--no-ok", enable),
385    "no_shadow": lambda enable: _simple_option("--no-shadow", enable),
386    "no_tags": lambda enable: _simple_option("--no-tags", enable),
387    "ok_label": lambda s: _dash_escape_nf(("--ok-label", s)),
388    # cf. Dialog.maxsize()
389    "print_maxsize": lambda enable: _simple_option("--print-maxsize",
390                                                   enable),
391    "print_size": lambda enable: _simple_option("--print-size", enable),
392    # cf. Dialog.backend_version()
393    "print_version": lambda enable: _simple_option("--print-version",
394                                                   enable),
395    "scrollbar": lambda enable: _simple_option("--scrollbar", enable),
396    "separate_output": lambda enable: _simple_option("--separate-output",
397                                                     enable),
398    "separate_widget": lambda s: _dash_escape_nf(("--separate-widget", s)),
399    "shadow": lambda enable: _simple_option("--shadow", enable),
400    # Obsolete according to dialog(1)
401    "size_err": lambda enable: _simple_option("--size-err", enable),
402    "sleep": lambda secs: _dash_escape_nf(("--sleep", str(secs))),
403    "stderr": lambda enable: _simple_option("--stderr", enable),
404    "stdout": lambda enable: _simple_option("--stdout", enable),
405    "tab_correct": lambda enable: _simple_option("--tab-correct", enable),
406    "tab_len": lambda n: _dash_escape_nf(("--tab-len", str(n))),
407    "time_format": lambda s: _dash_escape_nf(("--time-format", s)),
408    "timeout": lambda secs: _dash_escape_nf(("--timeout", str(secs))),
409    "title": lambda title: _dash_escape_nf(("--title", title)),
410    "trace": lambda filename: _dash_escape_nf(("--trace", filename)),
411    "trim": lambda enable: _simple_option("--trim", enable),
412    "version": lambda enable: _simple_option("--version", enable),
413    "visit_items": lambda enable: _simple_option("--visit-items", enable),
414    "week_start": lambda start: _dash_escape_nf(
415        ("--week-start", str(start) if isinstance(start, int) else start)),
416    "yes_label": lambda s: _dash_escape_nf(("--yes-label", s)) }
417
418
419def _find_in_path(prog_name):
420    """Search an executable in the :envvar:`PATH`.
421
422    If :envvar:`PATH` is not defined, the default path
423    ``/bin:/usr/bin`` is used.
424
425    Return a path to the file, or ``None`` if no file with a matching
426    basename as well as read and execute permissions is found.
427
428    Notable exception:
429
430      :exc:`PythonDialogOSError`
431
432    """
433    with _OSErrorHandling():
434        PATH = os.getenv("PATH", "/bin:/usr/bin") # see the execvp(3) man page
435        for d in PATH.split(os.pathsep):
436            file_path = os.path.join(d, prog_name)
437            if os.path.isfile(file_path) \
438               and os.access(file_path, os.R_OK | os.X_OK):
439                return file_path
440        return None
441
442
443def _path_to_executable(f):
444    """Find a path to an executable.
445
446    Find a path to an executable, using the same rules as the POSIX
447    exec*p() functions (see execvp(3) for instance).
448
449    If *f* contains a ``/`` character, it must be a relative or absolute
450    path to a file that has read and execute permissions. If *f* does
451    not contain a ``/`` character, it is looked for according to the
452    contents of the :envvar:`PATH` environment variable, which defaults
453    to ``/bin:/usr/bin`` if unset.
454
455    The return value is the result of calling :func:`os.path.realpath`
456    on the path found according to the rules described in the previous
457    paragraph.
458
459    Notable exceptions:
460
461      - :exc:`ExecutableNotFound`
462      - :exc:`PythonDialogOSError`
463
464    """
465    with _OSErrorHandling():
466        if '/' in f:
467            if os.path.isfile(f) and os.access(f, os.R_OK | os.X_OK):
468                res = f
469            else:
470                raise ExecutableNotFound("%s cannot be read and executed" % f)
471        else:
472            res = _find_in_path(f)
473            if res is None:
474                raise ExecutableNotFound(
475                    "can't find the executable for the dialog-like "
476                    "program")
477
478    return os.path.realpath(res)
479
480
481def _to_onoff(val):
482    """Convert boolean expressions to ``"on"`` or ``"off"``.
483
484    :return:
485      - ``"on"`` if *val* is ``True``, a non-zero integer, ``"on"`` or
486        any case variation thereof;
487      - ``"off"`` if *val* is ``False``, ``0``, ``"off"`` or any case
488        variation thereof.
489
490    Notable exceptions:
491
492      - :exc:`PythonDialogReModuleError`
493      - :exc:`BadPythonDialogUsage`
494
495    """
496    if isinstance(val, (bool, int)):
497        return "on" if val else "off"
498    elif isinstance(val, str):
499        try:
500            if _on_cre.match(val):
501                return "on"
502            elif _off_cre.match(val):
503                return "off"
504        except re.error as e:
505            raise PythonDialogReModuleError(str(e)) from e
506
507    raise BadPythonDialogUsage("invalid boolean value: {0!r}".format(val))
508
509
510def _compute_common_args(mapping):
511    """Compute the list of arguments for :term:`dialog common options`.
512
513    Compute a list of the command-line arguments to pass to
514    :program:`dialog` from a keyword arguments dictionary for options
515    listed as "common options" in the manual page for :program:`dialog`.
516    These are the options that are not tied to a particular widget.
517
518    This allows one to specify these options in a pythonic way, such
519    as::
520
521       d.checklist(<usual arguments for a checklist>,
522                   title="...",
523                   backtitle="...")
524
525    instead of having to pass them with strings like ``"--title foo"``
526    or ``"--backtitle bar"``.
527
528    Notable exceptions: none
529
530    """
531    args = []
532    for option, value in mapping.items():
533        args.extend(_common_args_syntax[option](value))
534    return args
535
536
537# Classes for dealing with the version of dialog-like backend programs
538if sys.hexversion >= 0x030200F0:
539    import abc
540    # Abstract base class
541    class BackendVersion(metaclass=abc.ABCMeta):
542        @abc.abstractmethod
543        def __str__(self):
544            raise NotImplementedError()
545
546        if sys.hexversion >= 0x030300F0:
547            @classmethod
548            @abc.abstractmethod
549            def fromstring(cls, s):
550                raise NotImplementedError()
551        else:                   # for Python 3.2
552            @abc.abstractclassmethod
553            def fromstring(cls, s):
554                raise NotImplementedError()
555
556        @abc.abstractmethod
557        def __lt__(self, other):
558            raise NotImplementedError()
559
560        @abc.abstractmethod
561        def __le__(self, other):
562            raise NotImplementedError()
563
564        @abc.abstractmethod
565        def __eq__(self, other):
566            raise NotImplementedError()
567
568        @abc.abstractmethod
569        def __ne__(self, other):
570            raise NotImplementedError()
571
572        @abc.abstractmethod
573        def __gt__(self, other):
574            raise NotImplementedError()
575
576        @abc.abstractmethod
577        def __ge__(self, other):
578            raise NotImplementedError()
579else:
580    class BackendVersion:
581        pass
582
583
584class DialogBackendVersion(BackendVersion):
585    """Class representing possible versions of the :program:`dialog` backend.
586
587    The purpose of this class is to make it easy to reliably compare
588    between versions of the :program:`dialog` backend. It encapsulates
589    the specific details of the backend versioning scheme to allow
590    eventual adaptations to changes in this scheme without affecting
591    external code.
592
593    The version is represented by two components in this class: the
594    :dfn:`dotted part` and the :dfn:`rest`. For instance, in the
595    ``'1.2'`` version string, the dotted part is ``[1, 2]`` and the rest
596    is the empty string. However, in version ``'1.2-20130902'``, the
597    dotted part is still ``[1, 2]``, but the rest is the string
598    ``'-20130902'``.
599
600    Instances of this class can be created with the constructor by
601    specifying the dotted part and the rest. Alternatively, an instance
602    can be created from the corresponding version string (e.g.,
603    ``'1.2-20130902'``) using the :meth:`fromstring` class method. This
604    is particularly useful with the result of
605    :samp:`{d}.backend_version()`, where *d* is a :class:`Dialog`
606    instance. Actually, the main constructor detects if its first
607    argument is a string and calls :meth:`!fromstring` in this case as a
608    convenience. Therefore, all of the following expressions are valid
609    to create a DialogBackendVersion instance::
610
611      DialogBackendVersion([1, 2])
612      DialogBackendVersion([1, 2], "-20130902")
613      DialogBackendVersion("1.2-20130902")
614      DialogBackendVersion.fromstring("1.2-20130902")
615
616    If *bv* is a :class:`DialogBackendVersion` instance,
617    :samp:`str({bv})` is a string representing the same version (for
618    instance, ``"1.2-20130902"``).
619
620    Two :class:`DialogBackendVersion` instances can be compared with the
621    usual comparison operators (``<``, ``<=``, ``==``, ``!=``, ``>=``,
622    ``>``). The algorithm is designed so that the following order is
623    respected (after instanciation with :meth:`fromstring`)::
624
625      1.2 < 1.2-20130902 < 1.2-20130903 < 1.2.0 < 1.2.0-20130902
626
627    among other cases. Actually, the *dotted parts* are the primary keys
628    when comparing and *rest* strings act as secondary keys. *Dotted
629    parts* are compared with the standard Python list comparison and
630    *rest* strings using the standard Python string comparison.
631
632    """
633    try:
634        _backend_version_cre = re.compile(r"""(?P<dotted> (\d+) (\.\d+)* )
635                                              (?P<rest>.*)$""", re.VERBOSE)
636    except re.error as e:
637        raise PythonDialogReModuleError(str(e)) from e
638
639    def __init__(self, dotted_part_or_str, rest=""):
640        """Create a :class:`DialogBackendVersion` instance.
641
642        Please see the class docstring for details.
643
644        """
645        if isinstance(dotted_part_or_str, str):
646            if rest:
647                raise BadPythonDialogUsage(
648                    "non-empty 'rest' with 'dotted_part_or_str' as string: "
649                    "{0!r}".format(rest))
650            else:
651                tmp = self.__class__.fromstring(dotted_part_or_str)
652                dotted_part_or_str, rest = tmp.dotted_part, tmp.rest
653
654        for elt in dotted_part_or_str:
655            if not isinstance(elt, int):
656                raise BadPythonDialogUsage(
657                    "when 'dotted_part_or_str' is not a string, it must "
658                    "be a sequence (or iterable) of integers; however, "
659                    "{0!r} is not an integer.".format(elt))
660
661        self.dotted_part = list(dotted_part_or_str)
662        self.rest = rest
663
664    def __repr__(self):
665        return "{0}.{1}({2!r}, rest={3!r})".format(
666            __name__, self.__class__.__name__, self.dotted_part, self.rest)
667
668    def __str__(self):
669        return '.'.join(map(str, self.dotted_part)) + self.rest
670
671    @classmethod
672    def fromstring(cls, s):
673        """Create a :class:`DialogBackendVersion` instance from a \
674:program:`dialog` version string.
675
676        :param str s: a :program:`dialog` version string
677        :return:
678          a :class:`DialogBackendVersion` instance representing the same
679          string
680
681        Notable exceptions:
682
683          - :exc:`UnableToParseDialogBackendVersion`
684          - :exc:`PythonDialogReModuleError`
685
686          """
687        try:
688            mo = cls._backend_version_cre.match(s)
689            if not mo:
690                raise UnableToParseDialogBackendVersion(s)
691            dotted_part = [ int(x) for x in mo.group("dotted").split(".") ]
692            rest = mo.group("rest")
693        except re.error as e:
694            raise PythonDialogReModuleError(str(e)) from e
695
696        return cls(dotted_part, rest)
697
698    def __lt__(self, other):
699        return (self.dotted_part, self.rest) < (other.dotted_part, other.rest)
700
701    def __le__(self, other):
702        return (self.dotted_part, self.rest) <= (other.dotted_part, other.rest)
703
704    def __eq__(self, other):
705        return (self.dotted_part, self.rest) == (other.dotted_part, other.rest)
706
707    # Python 3.2 has a decorator (functools.total_ordering) to automate this.
708    def __ne__(self, other):
709        return not (self == other)
710
711    def __gt__(self, other):
712        return not (self <= other)
713
714    def __ge__(self, other):
715        return not (self < other)
716
717
718def widget(func):
719    """Decorator to mark :class:`Dialog` methods that provide widgets.
720
721    This allows code to perform automatic operations on these specific
722    methods. For instance, one can define a class that behaves similarly
723    to :class:`Dialog`, except that after every widget-producing call,
724    it spawns a "confirm quit" dialog if the widget returned
725    :attr:`Dialog.ESC`, and loops in case the user doesn't actually want
726    to quit.
727
728    When it is unclear whether a method should have the decorator or
729    not, the return value is used to draw the line. For instance, among
730    :meth:`Dialog.gauge_start`, :meth:`Dialog.gauge_update` and
731    :meth:`Dialog.gauge_stop`, only the last one has the decorator
732    because it returns a :term:`Dialog exit code`, whereas the first two
733    don't return anything meaningful.
734
735    Note:
736
737      Some widget-producing methods return the Dialog exit code, but
738      other methods return a *sequence*, the first element of which is
739      the Dialog exit code; the ``retval_is_code`` attribute, which is
740      set by the decorator of the same name, allows to programmatically
741      discover the interface a given method conforms to.
742
743    .. versionadded:: 2.14
744
745    """
746    func.is_widget = True
747    return func
748
749
750def retval_is_code(func):
751    """Decorator for :class:`Dialog` widget-producing methods whose \
752return value is the :term:`Dialog exit code`.
753
754    This decorator is intended for widget-producing methods whose return
755    value consists solely of the Dialog exit code. When this decorator
756    is *not* used on a widget-producing method, the Dialog exit code
757    must be the first element of the return value.
758
759    .. versionadded:: 3.0
760
761    """
762    func.retval_is_code = True
763    return func
764
765
766def _obsolete_property(name, replacement=None):
767    if replacement is None:
768        replacement = name
769
770    def getter(self):
771        warnings.warn("the DIALOG_{name} attribute of Dialog instances is "
772                      "obsolete; use the Dialog.{repl} class attribute "
773                      "instead.".format(name=name, repl=replacement),
774                      DeprecationWarning)
775        return getattr(self, replacement)
776
777    return getter
778
779
780# Main class of the module
781class Dialog:
782    """Class providing bindings for :program:`dialog`-compatible programs.
783
784    This class allows you to invoke :program:`dialog` or a compatible
785    program in a pythonic way to quickly and easily build simple but
786    nice text interfaces.
787
788    An application typically creates one instance of the :class:`Dialog`
789    class and uses it for all its widgets, but it is possible to
790    concurrently use several instances of this class with different
791    parameters (such as the background title) if you have a need for
792    this.
793
794    """
795    try:
796        _print_maxsize_cre = re.compile(r"""^MaxSize:[ \t]+
797                                            (?P<rows>\d+),[ \t]*
798                                            (?P<columns>\d+)[ \t]*$""",
799                                        re.VERBOSE)
800        _print_version_cre = re.compile(
801            r"^Version:[ \t]+(?P<version>.+?)[ \t]*$", re.MULTILINE)
802    except re.error as e:
803        raise PythonDialogReModuleError(str(e)) from e
804
805    # DIALOG_OK, DIALOG_CANCEL, etc. are environment variables controlling
806    # the dialog backend exit status in the corresponding situation ("low-level
807    # exit status/code").
808    #
809    # Note:
810    #    - 127 must not be used for any of the DIALOG_* values. It is used
811    #      when a failure occurs in the child process before it exec()s
812    #      dialog (where "before" includes a potential exec() failure).
813    #    - 126 is also used (although in presumably rare situations).
814    _DIALOG_OK        = 0
815    _DIALOG_CANCEL    = 1
816    _DIALOG_ESC       = 2
817    _DIALOG_ERROR     = 3
818    _DIALOG_EXTRA     = 4
819    _DIALOG_HELP      = 5
820    _DIALOG_ITEM_HELP = 6
821    _DIALOG_TIMEOUT   = 7
822    # cf. also _lowlevel_exit_codes and _dialog_exit_code_ll_to_hl which are
823    # created by __init__(). It is not practical to define everything here,
824    # because there is no equivalent of 'self' for the class outside method
825    # definitions.
826    _lowlevel_exit_code_varnames = frozenset(
827        ("OK", "CANCEL", "ESC", "ERROR", "EXTRA", "HELP", "ITEM_HELP",
828         "TIMEOUT"))
829
830    # High-level exit codes, AKA "Dialog exit codes". These are the codes that
831    # pythondialog-based applications should use.
832    #
833    #: :term:`Dialog exit code` corresponding to the ``DIALOG_OK``
834    #: :term:`dialog exit status`
835    OK     = "ok"
836    #: :term:`Dialog exit code` corresponding to the ``DIALOG_CANCEL``
837    #: :term:`dialog exit status`
838    CANCEL = "cancel"
839    #: :term:`Dialog exit code` corresponding to the ``DIALOG_ESC``
840    #: :term:`dialog exit status`
841    ESC    = "esc"
842    #: :term:`Dialog exit code` corresponding to the ``DIALOG_EXTRA``
843    #: :term:`dialog exit status`
844    EXTRA  = "extra"
845    #: :term:`Dialog exit code` corresponding to the ``DIALOG_HELP`` and
846    #: ``DIALOG_ITEM_HELP`` :term:`dialog exit statuses <dialog exit status>`
847    HELP   = "help"
848    #: :term:`Dialog exit code` corresponding to the ``DIALOG_TIMEOUT``
849    #: :term:`dialog exit status`
850    TIMEOUT = "timeout"
851
852    # Define properties to maintain backward-compatibility while warning about
853    # the obsolete attributes (which used to refer to the low-level exit codes
854    # in pythondialog 2.x).
855    #
856    #: Obsolete property superseded by :attr:`Dialog.OK` since version 3.0
857    DIALOG_OK        = property(_obsolete_property("OK"),
858                         doc="Obsolete property superseded by Dialog.OK")
859    #: Obsolete property superseded by :attr:`Dialog.CANCEL` since version 3.0
860    DIALOG_CANCEL    = property(_obsolete_property("CANCEL"),
861                         doc="Obsolete property superseded by Dialog.CANCEL")
862    #: Obsolete property superseded by :attr:`Dialog.ESC` since version 3.0
863    DIALOG_ESC       = property(_obsolete_property("ESC"),
864                         doc="Obsolete property superseded by Dialog.ESC")
865    #: Obsolete property superseded by :attr:`Dialog.EXTRA` since version 3.0
866    DIALOG_EXTRA     = property(_obsolete_property("EXTRA"),
867                         doc="Obsolete property superseded by Dialog.EXTRA")
868    #: Obsolete property superseded by :attr:`Dialog.HELP` since version 3.0
869    DIALOG_HELP      = property(_obsolete_property("HELP"),
870                         doc="Obsolete property superseded by Dialog.HELP")
871    # We treat DIALOG_ITEM_HELP and DIALOG_HELP the same way in pythondialog,
872    # since both indicate the same user action ("Help" button pressed).
873    #
874    #: Obsolete property superseded by :attr:`Dialog.HELP` since version 3.0
875    DIALOG_ITEM_HELP = property(_obsolete_property("ITEM_HELP",
876                                                   replacement="HELP"),
877                         doc="Obsolete property superseded by Dialog.HELP")
878
879    @property
880    def DIALOG_ERROR(self):
881        warnings.warn("the DIALOG_ERROR attribute of Dialog instances is "
882                      "obsolete. Since the corresponding exit status is "
883                      "automatically translated into a DialogError exception, "
884                      "users should not see nor need this attribute. If you "
885                      "think you have a good reason to use it, please expose "
886                      "your situation on the pythondialog mailing-list.",
887                      DeprecationWarning)
888        # There is no corresponding high-level code; and if the user *really*
889        # wants to know the (integer) error exit status, here it is...
890        return self._DIALOG_ERROR
891
892    def __init__(self, dialog="cdialog", DIALOGRC=None,
893                 compat="dialog", use_stdout=None, *, autowidgetsize=False,
894                 pass_args_via_file=None):
895        """Constructor for :class:`Dialog` instances.
896
897        :param str dialog:
898          name of (or path to) the :program:`dialog`-like program to
899          use. If it contains a slash (``/``), it must be a relative or
900          absolute path to a file that has read and execute permissions,
901          and is used as is; otherwise, it is looked for according to
902          the contents of the :envvar:`PATH` environment variable, which
903          defaults to ``/bin:/usr/bin`` if unset. In case you decide to
904          use a relative path containing a ``/``, be *very careful*
905          about the current directory at the time the Dialog instance is
906          created. Indeed, if for instance you use ``"foobar/dialog"``
907          and your program creates the Dialog instance at a time where
908          the current directory is for instance ``/tmp``, then
909          ``/tmp/foobar/dialog`` will be run, which could be risky. If
910          you don't understand this, stay with the default, use a value
911          containing no ``/``, or use an absolute path (i.e., one
912          starting with a ``/``).
913        :param str DIALOGRC:
914          string to pass to the :program:`dialog`-like program as the
915          :envvar:`DIALOGRC` environment variable, or ``None`` if no
916          modification to the environment regarding this variable should
917          be done in the call to the :program:`dialog`-like program
918        :param str compat:
919          compatibility mode (see :ref:`below
920          <Dialog-constructor-compat-arg>`)
921        :param bool use_stdout:
922          read :program:`dialog`'s standard output stream instead of its
923          standard error stream in order to get most "results"
924          (user-supplied strings, selected items, etc.; basically,
925          everything except the exit status). This is for compatibility
926          with :program:`Xdialog` and should only be used if you have a
927          good reason to do so.
928        :param bool autowidgetsize:
929          whether to enable *autowidgetsize* mode. When enabled, all
930          pythondialog widget-producing methods will behave as if
931          ``width=0``, ``height=0``, etc. had been passed, except where
932          these parameters are explicitely specified with different
933          values. This has the effect that, by default, the
934          :program:`dialog` backend will automatically compute a
935          suitable size for the widgets. More details about this option
936          are given :ref:`below <autowidgetsize>`.
937        :param pass_args_via_file:
938          whether to use the :option:`--file` option with a temporary
939          file in order to pass arguments to the :program:`dialog`
940          backend, instead of including them directly into the argument
941          list; using :option:`--file` has the advantage of not exposing
942          the “real” arguments to other users through the process table.
943          With the default value (``None``), the option is enabled if
944          the :program:`dialog` version is recent enough to offer a
945          reliable :option:`--file` implementation (i.e., 1.2-20150513
946          or later).
947        :type pass_args_via_file: bool or ``None``
948        :return: a :class:`Dialog` instance
949
950        .. _Dialog-constructor-compat-arg:
951
952        The officially supported :program:`dialog`-like program in
953        pythondialog is the well-known dialog_ program written in C,
954        based on the ncurses_ library.
955
956        .. _dialog: https://invisible-island.net/dialog/dialog.html
957        .. _ncurses: https://invisible-island.net/ncurses/ncurses.html
958
959        If you want to use a different program such as Xdialog_, you
960        should indicate the executable file name with the *dialog*
961        argument **and** the compatibility type that you think it
962        conforms to with the *compat* argument. Currently, *compat* can
963        be either ``"dialog"`` (for :program:`dialog`; this is the
964        default) or ``"Xdialog"`` (for, well, :program:`Xdialog`).
965
966        .. _Xdialog: http://xdialog.free.fr/
967
968        The *compat* argument allows me to cope with minor differences
969        in behaviour between the various programs implementing the
970        :program:`dialog` interface (not the text or graphical
971        interface, I mean the API). However, having to support various
972        APIs simultaneously is ugly and I would really prefer you to
973        report bugs to the relevant maintainers when you find
974        incompatibilities with :program:`dialog`. This is for the
975        benefit of pretty much everyone that relies on the
976        :program:`dialog` interface.
977
978        Notable exceptions:
979
980          - :exc:`ExecutableNotFound`
981          - :exc:`PythonDialogOSError`
982          - :exc:`UnableToRetrieveBackendVersion`
983          - :exc:`UnableToParseBackendVersion`
984
985        .. versionadded:: 3.1
986           Support for the *autowidgetsize* parameter.
987
988        .. versionadded:: 3.3
989           Support for the *pass_args_via_file* parameter.
990
991        """
992        # DIALOGRC differs from the Dialog._DIALOG_* attributes in that:
993        #   1. It is an instance attribute instead of a class attribute.
994        #   2. It should be a string if not None.
995        #   3. We may very well want it to be unset.
996        if DIALOGRC is not None:
997            self.DIALOGRC = DIALOGRC
998
999        # Mapping from "OK", "CANCEL", ... to the corresponding dialog exit
1000        # statuses (integers).
1001        self._lowlevel_exit_codes = {
1002            name: getattr(self, "_DIALOG_" + name)
1003            for name in self._lowlevel_exit_code_varnames }
1004
1005        # Mapping from dialog exit status (integer) to Dialog exit code ("ok",
1006        # "cancel", ... strings referred to by Dialog.OK, Dialog.CANCEL, ...);
1007        # in other words, from low-level to high-level exit code.
1008        self._dialog_exit_code_ll_to_hl = {}
1009        for name in self._lowlevel_exit_code_varnames:
1010            intcode = self._lowlevel_exit_codes[name]
1011
1012            if name == "ITEM_HELP":
1013                self._dialog_exit_code_ll_to_hl[intcode] = self.HELP
1014            elif name == "ERROR":
1015                continue
1016            else:
1017                self._dialog_exit_code_ll_to_hl[intcode] = getattr(self, name)
1018
1019        self._dialog_prg = _path_to_executable(dialog)
1020        self.compat = compat
1021        self.autowidgetsize = autowidgetsize
1022        self.dialog_persistent_arglist = []
1023
1024        # Use stderr or stdout for reading dialog's output?
1025        if self.compat == "Xdialog":
1026            # Default to using stdout for Xdialog
1027            self.use_stdout = True
1028        else:
1029            self.use_stdout = False
1030        if use_stdout is not None:
1031            # Allow explicit setting
1032            self.use_stdout = use_stdout
1033        if self.use_stdout:
1034            self.add_persistent_args(["--stdout"])
1035
1036        self.setup_debug(False)
1037
1038        if compat == "dialog":
1039            # Temporary setting to ensure that self.backend_version()
1040            # will be able to run even if dialog is too old to support
1041            # --file correctly. Will be overwritten later.
1042            self.pass_args_via_file = False
1043            self.cached_backend_version = DialogBackendVersion.fromstring(
1044                self.backend_version())
1045        else:
1046            # Xdialog doesn't seem to offer --print-version (2013-09-12)
1047            self.cached_backend_version = None
1048
1049        if pass_args_via_file is not None:
1050            # Always respect explicit settings
1051            self.pass_args_via_file = pass_args_via_file
1052        elif self.cached_backend_version is not None:
1053            self.pass_args_via_file = self.cached_backend_version >= \
1054                                      DialogBackendVersion("1.2-20150513")
1055        else:
1056            # Xdialog doesn't seem to offer --file (2015-05-24)
1057            self.pass_args_via_file = False
1058
1059    @classmethod
1060    def dash_escape(cls, args):
1061        """
1062        Escape all elements of *args* that need escaping for :program:`dialog`.
1063
1064        *args* may be any sequence and is not modified by this method.
1065        Return a new list where every element that needs escaping has
1066        been escaped.
1067
1068        An element needs escaping when it starts with two ASCII hyphens
1069        (``--``). Escaping consists in prepending an element composed of
1070        two ASCII hyphens, i.e., the string ``'--'``.
1071
1072        All high-level :class:`Dialog` methods automatically perform
1073        :term:`dash escaping` where appropriate. In particular, this is
1074        the case for every method that provides a widget: :meth:`yesno`,
1075        :meth:`msgbox`, etc. You only need to do it yourself when
1076        calling a low-level method such as :meth:`add_persistent_args`.
1077
1078        .. versionadded:: 2.12
1079
1080        """
1081        return _dash_escape(args)
1082
1083    @classmethod
1084    def dash_escape_nf(cls, args):
1085        """
1086        Escape all elements of *args* that need escaping, except the first one.
1087
1088        See :meth:`dash_escape` for details. Return a new list.
1089
1090        All high-level :class:`Dialog` methods automatically perform dash
1091        escaping where appropriate. In particular, this is the case
1092        for every method that provides a widget: :meth:`yesno`, :meth:`msgbox`,
1093        etc. You only need to do it yourself when calling a low-level
1094        method such as :meth:`add_persistent_args`.
1095
1096        .. versionadded:: 2.12
1097
1098        """
1099        return _dash_escape_nf(args)
1100
1101    def add_persistent_args(self, args):
1102        """Add arguments to use for every subsequent dialog call.
1103
1104        This method cannot guess which elements of *args* are dialog
1105        options (such as ``--title``) and which are not (for instance,
1106        you might want to use ``--title`` or even ``--`` as an argument
1107        to a dialog option). Therefore, this method does not perform any
1108        kind of :term:`dash escaping`; you have to do it yourself.
1109        :meth:`dash_escape` and :meth:`dash_escape_nf` may be useful for
1110        this purpose.
1111
1112        """
1113        self.dialog_persistent_arglist.extend(args)
1114
1115    def set_background_title(self, text):
1116        """Set the background title for dialog.
1117
1118        :param str text: string to use as background title
1119
1120        .. versionadded:: 2.13
1121
1122        """
1123        self.add_persistent_args(self.dash_escape_nf(("--backtitle", text)))
1124
1125    # For compatibility with the old dialog
1126    def setBackgroundTitle(self, text):
1127        """Set the background title for :program:`dialog`.
1128
1129        :param str text: background title to use behind widgets
1130
1131        .. deprecated:: 2.03
1132          Use :meth:`set_background_title` instead.
1133
1134        """
1135        warnings.warn("Dialog.setBackgroundTitle() has been obsolete for "
1136                      "many years; use Dialog.set_background_title() instead",
1137                      DeprecationWarning)
1138        self.set_background_title(text)
1139
1140    def setup_debug(self, enable, file=None, always_flush=False, *,
1141                    expand_file_opt=False):
1142        """Setup the debugging parameters.
1143
1144        :param bool enable:       whether to enable or disable debugging
1145        :param file file:         where to write debugging information
1146        :param bool always_flush: whether to call :meth:`file.flush`
1147                                  after each command written
1148        :param bool expand_file_opt:
1149          when :meth:`Dialog.__init__` has been called with
1150          :samp:`{pass_args_via_file}=True`, this option causes the
1151          :option:`--file` options that would normally be written to
1152          *file* to be expanded, yielding a similar result to what would
1153          be obtained with :samp:`{pass_args_via_file}=False` (but
1154          contrary to :samp:`{pass_args_via_file}=False`, this only
1155          affects *file*, not the actual :program:`dialog` calls). This
1156          is useful, for instance, for copying some of the
1157          :program:`dialog` commands into a shell.
1158
1159        When *enable* is true, all :program:`dialog` commands are
1160        written to *file* using POSIX shell syntax. In this case, you'll
1161        probably want to use either :samp:`{expand_file_opt}=True` in
1162        this method or :samp:`{pass_args_via_file}=False` in
1163        :meth:`Dialog.__init__`, otherwise you'll mostly see
1164        :program:`dialog` calls containing only one :option:`--file`
1165        option followed by a path to a temporary file.
1166
1167        .. versionadded:: 2.12
1168
1169        .. versionadded:: 3.3
1170           Support for the *expand_file_opt* parameter.
1171
1172        """
1173        self._debug_enabled = enable
1174
1175        if not hasattr(self, "_debug_logfile"):
1176            self._debug_logfile = None
1177        # Allows to switch debugging on and off without having to pass the file
1178        # object again and again.
1179        if file is not None:
1180            self._debug_logfile = file
1181
1182        if enable and self._debug_logfile is None:
1183            raise BadPythonDialogUsage(
1184                "you must specify a file object when turning debugging on")
1185
1186        self._debug_always_flush = always_flush
1187        self._expand_file_opt = expand_file_opt
1188        self._debug_first_output = True
1189
1190    def _write_command_to_file(self, env, arglist):
1191        envvar_settings_list = []
1192
1193        if "DIALOGRC" in env:
1194            envvar_settings_list.append(
1195                "DIALOGRC={0}".format(_shell_quote(env["DIALOGRC"])))
1196
1197        for var in self._lowlevel_exit_code_varnames:
1198            varname = "DIALOG_" + var
1199            envvar_settings_list.append(
1200                "{0}={1}".format(varname, _shell_quote(env[varname])))
1201
1202        command_str = ' '.join(envvar_settings_list +
1203                               list(map(_shell_quote, arglist)))
1204        s = "{separator}{cmd}\n\nArgs: {args!r}\n".format(
1205            separator="" if self._debug_first_output else ("-" * 79) + "\n",
1206            cmd=command_str, args=arglist)
1207
1208        self._debug_logfile.write(s)
1209        if self._debug_always_flush:
1210            self._debug_logfile.flush()
1211
1212        self._debug_first_output = False
1213
1214    def _quote_arg_for_file_opt(self, argument):
1215        """
1216        Transform a :program:`dialog` argument for safe inclusion via :option:`--file`.
1217
1218        Since arguments in a file included via :option:`--file` are
1219        separated by whitespace, they must be quoted for
1220        :program:`dialog` in a way similar to shell quoting.
1221
1222        """
1223        l = ['"']
1224
1225        for c in argument:
1226            if c in ('"', '\\'):
1227                l.append("\\" + c)
1228            else:
1229                l.append(c)
1230
1231        return ''.join(l + ['"'])
1232
1233    def _call_program(self, cmdargs, *, dash_escape="non-first",
1234                      use_persistent_args=True,
1235                      redir_child_stdin_from_fd=None, close_fds=(), **kwargs):
1236        """Do the actual work of invoking the :program:`dialog`-like program.
1237
1238        Communication with the :program:`dialog`-like program is
1239        performed through one :manpage:`pipe(2)` and optionally a
1240        user-specified file descriptor, depending on
1241        *redir_child_stdin_from_fd*. The pipe allows the parent process
1242        to read what :program:`dialog` writes on its standard error
1243        stream [#]_.
1244
1245        If *use_persistent_args* is ``True`` (the default), the elements
1246        of ``self.dialog_persistent_arglist`` are passed as the first
1247        arguments to ``self._dialog_prg``; otherwise,
1248        ``self.dialog_persistent_arglist`` is not used at all. The
1249        remaining arguments are those computed from *kwargs* followed by
1250        the elements of *cmdargs*.
1251
1252        If *dash_escape* is the string ``"non-first"``, then every
1253        element of *cmdargs* that starts with ``'--'`` is escaped by
1254        prepending an element consisting of ``'--'``, except the first
1255        one (which is usually a :program:`dialog` option such as
1256        ``'--yesno'``). In order to disable this escaping mechanism,
1257        pass the string ``"none"`` as *dash_escape*.
1258
1259        If *redir_child_stdin_from_fd* is not ``None``, it should be an
1260        open file descriptor (i.e., an integer). That file descriptor
1261        will be connected to :program:`dialog`'s standard input. This is
1262        used by the gauge widget to feed data to :program:`dialog`, as
1263        well as for :meth:`progressbox` in order to allow
1264        :program:`dialog` to read data from a possibly-growing file.
1265
1266        If *redir_child_stdin_from_fd* is ``None``, the standard input
1267        in the child process (which runs :program:`dialog`) is not
1268        redirected in any way.
1269
1270        If *close_fds* is passed, it should be a sequence of file
1271        descriptors that will be closed by the child process before it
1272        exec()s the :program:`dialog`-like program.
1273
1274        Notable exception:
1275
1276          :exc:`PythonDialogOSError` (if any of the pipe(2) or close(2)
1277          system calls fails...)
1278
1279        .. [#] standard ouput stream if *use_stdout* is ``True``
1280
1281        """
1282        # We want to define DIALOG_OK, DIALOG_CANCEL, etc. in the
1283        # environment of the child process so that we know (and
1284        # even control) the possible dialog exit statuses.
1285        new_environ = {}
1286        new_environ.update(os.environ)
1287        for var, value in self._lowlevel_exit_codes.items():
1288            varname = "DIALOG_" + var
1289            new_environ[varname] = str(value)
1290        if hasattr(self, "DIALOGRC"):
1291            new_environ["DIALOGRC"] = self.DIALOGRC
1292
1293        if dash_escape == "non-first":
1294            # Escape all elements of 'cmdargs' that start with '--', except the
1295            # first one.
1296            cmdargs = self.dash_escape_nf(cmdargs)
1297        elif dash_escape != "none":
1298            raise PythonDialogBug("invalid value for 'dash_escape' parameter: "
1299                                  "{0!r}".format(dash_escape))
1300
1301        arglist = [ self._dialog_prg ]
1302
1303        if use_persistent_args:
1304            arglist.extend(self.dialog_persistent_arglist)
1305
1306        arglist.extend(_compute_common_args(kwargs) + cmdargs)
1307        orig_args = arglist[:] # New object, copy of 'arglist'
1308
1309        if self.pass_args_via_file:
1310            tmpfile = tempfile.NamedTemporaryFile(
1311                mode="w", prefix="pythondialog.tmp", delete=False)
1312            with tmpfile as f:
1313                f.write(' '.join( ( self._quote_arg_for_file_opt(arg)
1314                                    for arg in arglist[1:] ) ))
1315            args_file = tmpfile.name
1316            arglist[1:] = ["--file", args_file]
1317        else:
1318            args_file = None
1319
1320        if self._debug_enabled:
1321            # Write the complete command line with environment variables
1322            # setting to the debug log file (POSIX shell syntax for easy
1323            # copy-pasting into a terminal, followed by repr(arglist)).
1324            self._write_command_to_file(
1325                new_environ, orig_args if self._expand_file_opt else arglist)
1326
1327        # Create a pipe so that the parent process can read dialog's
1328        # output on stderr (stdout with 'use_stdout')
1329        with _OSErrorHandling():
1330            # rfd = File Descriptor for Reading
1331            # wfd = File Descriptor for Writing
1332            (child_output_rfd, child_output_wfd) = os.pipe()
1333
1334        child_pid = os.fork()
1335        if child_pid == 0:
1336            # We are in the child process. We MUST NOT raise any exception.
1337            try:
1338                # 1) If the write end of a pipe isn't closed, the read end
1339                #    will never see EOF, which can indefinitely block the
1340                #    child waiting for input. To avoid this, the write end
1341                #    must be closed in the father *and* child processes.
1342                # 2) The child process doesn't need child_output_rfd.
1343                for fd in close_fds + (child_output_rfd,):
1344                    os.close(fd)
1345                # We want:
1346                #   - to keep a reference to the father's stderr for error
1347                #     reporting (and use line-buffering for this stream);
1348                #   - dialog's output on stderr[*] to go to child_output_wfd;
1349                #   - data written to fd 'redir_child_stdin_from_fd'
1350                #     (if not None) to go to dialog's stdin.
1351                #
1352                #       [*] stdout with 'use_stdout'
1353                father_stderr = os.fdopen(os.dup(2), mode="w", buffering=1)
1354                os.dup2(child_output_wfd, 1 if self.use_stdout else 2)
1355                if redir_child_stdin_from_fd is not None:
1356                    os.dup2(redir_child_stdin_from_fd, 0)
1357
1358                os.execve(self._dialog_prg, arglist, new_environ)
1359            except:
1360                print(traceback.format_exc(), file=father_stderr)
1361                father_stderr.close()
1362                os._exit(127)
1363
1364            # Should not happen unless there is a bug in Python
1365            os._exit(126)
1366
1367        # We are in the father process.
1368        #
1369        # It is essential to close child_output_wfd, otherwise we will never
1370        # see EOF while reading on child_output_rfd and the parent process
1371        # will block forever on the read() call.
1372        # [ after the fork(), the "reference count" of child_output_wfd from
1373        #   the operating system's point of view is 2; after the child exits,
1374        #   it is 1 until the father closes it itself; then it is 0 and a read
1375        #   on child_output_rfd encounters EOF once all the remaining data in
1376        #   the pipe has been read. ]
1377        with _OSErrorHandling():
1378            os.close(child_output_wfd)
1379        return (child_pid, child_output_rfd, args_file)
1380
1381    def _wait_for_program_termination(self, child_pid, child_output_rfd):
1382        """Wait for a :program:`dialog`-like process to terminate.
1383
1384        This function waits for the specified process to terminate,
1385        raises the appropriate exceptions in case of abnormal
1386        termination and returns the :term:`Dialog exit code` and stderr
1387        [#stream]_ output of the process as a tuple: :samp:`({hl_exit_code},
1388        {output_string})`.
1389
1390        *child_output_rfd* must be the file descriptor for the
1391        reading end of the pipe created by :meth:`_call_program`, the
1392        writing end of which was connected by :meth:`_call_program`
1393        to the child process's standard error [#stream]_.
1394
1395        This function reads the process output on the standard error
1396        [#stream]_ from *child_output_rfd* and closes this file
1397        descriptor once this is done.
1398
1399        Notable exceptions:
1400
1401          - :exc:`DialogTerminatedBySignal`
1402          - :exc:`DialogError`
1403          - :exc:`PythonDialogErrorBeforeExecInChildProcess`
1404          - :exc:`PythonDialogIOError`    if the Python version is < 3.3
1405          - :exc:`PythonDialogOSError`
1406          - :exc:`PythonDialogBug`
1407          - :exc:`ProbablyPythonBug`
1408
1409        .. [#stream] standard output if ``self.use_stdout`` is ``True``
1410
1411        """
1412        # Read dialog's output on its stderr (stdout with 'use_stdout')
1413        with _OSErrorHandling():
1414            with os.fdopen(child_output_rfd, "r") as f:
1415                child_output = f.read()
1416            # The closing of the file object causes the end of the pipe we used
1417            # to read dialog's output on its stderr to be closed too. This is
1418            # important, otherwise invoking dialog enough times would
1419            # eventually exhaust the maximum number of open file descriptors.
1420
1421        exit_info = os.waitpid(child_pid, 0)[1]
1422        if os.WIFEXITED(exit_info):
1423            ll_exit_code = os.WEXITSTATUS(exit_info)
1424        # As we wait()ed for the child process to terminate, there is no
1425        # need to call os.WIFSTOPPED()
1426        elif os.WIFSIGNALED(exit_info):
1427            raise DialogTerminatedBySignal("the dialog-like program was "
1428                                           "terminated by signal %d" %
1429                                           os.WTERMSIG(exit_info))
1430        else:
1431            raise PythonDialogBug("please report this bug to the "
1432                                  "pythondialog maintainer(s)")
1433
1434        if ll_exit_code == self._DIALOG_ERROR:
1435            raise DialogError(
1436                "the dialog-like program exited with status {0} (which was "
1437                "passed to it as the DIALOG_ERROR environment variable). "
1438                "Sometimes, the reason is simply that dialog was given a "
1439                "height or width parameter that is too big for the terminal "
1440                "in use. Its output, with leading and trailing whitespace "
1441                "stripped, was:\n\n{1}".format(ll_exit_code,
1442                                               child_output.strip()))
1443        elif ll_exit_code == 127:
1444            raise PythonDialogErrorBeforeExecInChildProcess(dedent("""\
1445            possible reasons include:
1446              - the dialog-like program could not be executed (this can happen
1447                for instance if the Python program is trying to call the
1448                dialog-like program with arguments that cannot be represented
1449                in the user's locale [LC_CTYPE]);
1450              - the system is out of memory;
1451              - the maximum number of open file descriptors has been reached;
1452              - a cosmic ray hit the system memory and flipped nasty bits.
1453            There ought to be a traceback above this message that describes
1454            more precisely what happened."""))
1455        elif ll_exit_code == 126:
1456            raise ProbablyPythonBug(
1457                "a child process returned with exit status 126; this might "
1458                "be the exit status of the dialog-like program, for some "
1459                "unknown reason (-> probably a bug in the dialog-like "
1460                "program); otherwise, we have probably found a python bug")
1461
1462        try:
1463            hl_exit_code = self._dialog_exit_code_ll_to_hl[ll_exit_code]
1464        except KeyError:
1465            raise PythonDialogBug(
1466                "unexpected low-level exit status (new code?): {0!r}".format(
1467                    ll_exit_code))
1468
1469        return (hl_exit_code, child_output)
1470
1471    def _handle_program_exit(self, child_pid, child_output_rfd, args_file):
1472        """Handle exit of a :program:`dialog`-like process.
1473
1474        This method:
1475
1476          - waits for the :program:`dialog`-like program termination;
1477          - removes the temporary file used to pass its argument list,
1478            if any;
1479          - and returns the appropriate :term:`Dialog exit code` along
1480            with whatever output it produced.
1481
1482        Notable exceptions:
1483
1484          any exception raised by :meth:`_wait_for_program_termination`
1485
1486        """
1487        try:
1488            exit_code, output = \
1489                    self._wait_for_program_termination(child_pid,
1490                                                       child_output_rfd)
1491        finally:
1492            with _OSErrorHandling():
1493                if args_file is not None and os.path.exists(args_file):
1494                    os.unlink(args_file)
1495
1496        return (exit_code, output)
1497
1498    def _perform(self, cmdargs, *, dash_escape="non-first",
1499                 use_persistent_args=True, **kwargs):
1500        """Perform a complete :program:`dialog`-like program invocation.
1501
1502        This method:
1503
1504          - invokes the :program:`dialog`-like program;
1505          - waits for its termination;
1506          - removes the temporary file used to pass its argument list,
1507            if any;
1508          - and returns the appropriate :term:`Dialog exit code` along
1509            with whatever output it produced.
1510
1511        See :meth:`_call_program` for a description of the parameters.
1512
1513        Notable exceptions:
1514
1515          any exception raised by :meth:`_call_program` or
1516          :meth:`_handle_program_exit`
1517
1518        """
1519        child_pid, child_output_rfd, args_file = \
1520                    self._call_program(cmdargs, dash_escape=dash_escape,
1521                                       use_persistent_args=use_persistent_args,
1522                                       **kwargs)
1523        exit_code, output = self._handle_program_exit(child_pid,
1524                                                      child_output_rfd,
1525                                                      args_file)
1526        # dialog outputs '\ntimeout\n' when a timeout occurs, but this is
1527        # useless for us and would disturb output parsing code.
1528        if exit_code == self.TIMEOUT:
1529            output = ""
1530
1531        return (exit_code, output)
1532
1533    def _strip_xdialog_newline(self, output):
1534        """Remove trailing newline (if any) in \
1535:program:`Xdialog`-compatibility mode"""
1536        if self.compat == "Xdialog" and output.endswith("\n"):
1537            output = output[:-1]
1538        return output
1539
1540    # This is for compatibility with the old dialog.py
1541    def _perform_no_options(self, cmd):
1542        """Call :program:`dialog` without passing any more options."""
1543
1544        warnings.warn("Dialog._perform_no_options() has been obsolete for "
1545                      "many years", DeprecationWarning)
1546        return os.system(self._dialog_prg + ' ' + cmd)
1547
1548    # For compatibility with the old dialog.py
1549    def clear(self):
1550        """Clear the screen.
1551
1552        Equivalent to the :option:`--clear` option of :program:`dialog`.
1553
1554        .. deprecated:: 2.03
1555          You may use the :manpage:`clear(1)` program instead.
1556          cf. ``clear_screen()`` in :file:`examples/demo.py` for an
1557          example.
1558
1559        """
1560        warnings.warn("Dialog.clear() has been obsolete for many years.\n"
1561                      "You may use the clear(1) program to clear the screen.\n"
1562                      "cf. clear_screen() in examples/demo.py for an example",
1563                      DeprecationWarning)
1564        self._perform_no_options('--clear')
1565
1566    def _help_status_on(self, kwargs):
1567        return ("--help-status" in self.dialog_persistent_arglist
1568                or kwargs.get("help_status", False))
1569
1570    def _parse_quoted_string(self, s, start=0):
1571        """Parse a quoted string from a :program:`dialog` help output."""
1572        if start >= len(s) or s[start] != '"':
1573            raise PythonDialogBug("quoted string does not start with a double "
1574                                  "quote: {0!r}".format(s))
1575
1576        l = []
1577        i = start + 1
1578
1579        while i < len(s) and s[i] != '"':
1580            if s[i] == "\\":
1581                i += 1
1582                if i >= len(s):
1583                    raise PythonDialogBug(
1584                        "quoted string ends with a backslash: {0!r}".format(s))
1585            l.append(s[i])
1586            i += 1
1587
1588        if s[i] != '"':
1589            raise PythonDialogBug("quoted string does not and with a double "
1590                                  "quote: {0!r}".format(s))
1591
1592        return (''.join(l), i+1)
1593
1594    def _split_shellstyle_arglist(self, s):
1595        """Split an argument list with shell-style quoting performed \
1596by :program:`dialog`.
1597
1598        Any argument in 's' may or may not be quoted. Quoted
1599        arguments are always expected to be enclosed in double quotes
1600        (more restrictive than what the POSIX shell allows).
1601
1602        This function could maybe be replaced with shlex.split(),
1603        however:
1604          - shlex only handles Unicode strings in Python 2.7.3 and
1605            above;
1606          - the bulk of the work is done by _parse_quoted_string(),
1607            which is probably still needed in _parse_help(), where
1608            one needs to parse things such as 'HELP <id> <status>' in
1609            which <id> may be quoted but <status> is never quoted,
1610            even if it contains spaces or quotes.
1611
1612        """
1613        s = s.rstrip()
1614        l = []
1615        i = 0
1616
1617        while i < len(s):
1618            if s[i] == '"':
1619                arg, i = self._parse_quoted_string(s, start=i)
1620                if i < len(s) and s[i] != ' ':
1621                    raise PythonDialogBug(
1622                        "expected a space or end-of-string after quoted "
1623                        "string in {0!r}, but found {1!r}".format(s, s[i]))
1624                # Start of the next argument, or after the end of the string
1625                i += 1
1626                l.append(arg)
1627            else:
1628                try:
1629                    end = s.index(' ', i)
1630                except ValueError:
1631                    end = len(s)
1632
1633                l.append(s[i:end])
1634                # Start of the next argument, or after the end of the string
1635                i = end + 1
1636
1637        return l
1638
1639    def _parse_help(self, output, kwargs, *, multival=False,
1640                    multival_on_single_line=False, raw_format=False):
1641        """Parse the dialog help output from a widget.
1642
1643        'kwargs' should contain the keyword arguments used in the
1644        widget call that produced the help output.
1645
1646        'multival' is for widgets that return a list of values as
1647        opposed to a single value.
1648
1649        'raw_format' is for widgets that don't start their help
1650        output with the string "HELP ".
1651
1652        """
1653        l = output.splitlines()
1654
1655        if raw_format:
1656            # This format of the help output is either empty or consists of
1657            # only one line (possibly terminated with \n). It is
1658            # encountered with --calendar and --inputbox, among others.
1659            if len(l) > 1:
1660                raise PythonDialogBug("raw help feedback unexpected as "
1661                                      "multiline: {0!r}".format(output))
1662            elif len(l) == 0:
1663                return ""
1664            else:
1665                return l[0]
1666
1667        # Simple widgets such as 'yesno' will fall in this case if they use
1668        # this method.
1669        if not l:
1670            return None
1671
1672        # The widgets that actually use --help-status always have the first
1673        # help line indicating the active item; there is no risk of
1674        # confusing this line with the first line produced by --help-status.
1675        if not l[0].startswith("HELP "):
1676            raise PythonDialogBug(
1677                "unexpected help output that does not start with 'HELP ': "
1678                "{0!r}".format(output))
1679
1680        # Everything that follows "HELP "; what it contains depends on whether
1681        # --item-help and/or --help-tags were passed to dialog.
1682        s = l[0][5:]
1683
1684        if not self._help_status_on(kwargs):
1685            return s
1686
1687        if multival:
1688            if multival_on_single_line:
1689                args = self._split_shellstyle_arglist(s)
1690                if not args:
1691                    raise PythonDialogBug(
1692                        "expected a non-empty space-separated list of "
1693                        "possibly-quoted strings in this help output: {0!r}"
1694                        .format(output))
1695                return (args[0], args[1:])
1696            else:
1697                return (s, l[1:])
1698        else:
1699            if not s:
1700                raise PythonDialogBug(
1701                    "unexpected help output whose first line is 'HELP '")
1702            elif s[0] != '"':
1703                l2 = s.split(' ', 1)
1704                if len(l2) == 1:
1705                    raise PythonDialogBug(
1706                        "expected 'HELP <id> <status>' in the help output, "
1707                        "but couldn't find any space after 'HELP '")
1708                else:
1709                    return tuple(l2)
1710            else:
1711                help_id, after_index = self._parse_quoted_string(s)
1712                if not s[after_index:].startswith(" "):
1713                    raise PythonDialogBug(
1714                        "expected 'HELP <quoted_id> <status>' in the help "
1715                        "output, but couldn't find any space after "
1716                        "'HELP <quoted_id>'")
1717                return (help_id, s[after_index+1:])
1718
1719    def _widget_with_string_output(self, args, kwargs,
1720                                   strip_xdialog_newline=False,
1721                                   raw_help=False):
1722        """Generic implementation for a widget that produces a single string.
1723
1724        The help output must be present regardless of whether
1725        --help-status was passed or not.
1726
1727        """
1728        code, output = self._perform(args, **kwargs)
1729
1730        if strip_xdialog_newline:
1731            output = self._strip_xdialog_newline(output)
1732
1733        if code == self.HELP:
1734            # No check for --help-status
1735            help_data = self._parse_help(output, kwargs, raw_format=raw_help)
1736            return (code, help_data)
1737        else:
1738            return (code, output)
1739
1740    def _widget_with_no_output(self, widget_name, args, kwargs):
1741        """Generic implementation for a widget that produces no output."""
1742        code, output = self._perform(args, **kwargs)
1743
1744        if output:
1745            raise PythonDialogBug(
1746                "expected an empty output from {0!r}, but got: {1!r}".format(
1747                    widget_name, output))
1748
1749        return code
1750
1751    def _dialog_version_check(self, version_string, feature):
1752        if self.compat == "dialog":
1753            minimum_version = DialogBackendVersion.fromstring(version_string)
1754
1755            if self.cached_backend_version < minimum_version:
1756                raise InadequateBackendVersion(
1757                    "{0} requires dialog {1} or later, "
1758                    "but you seem to be using version {2}".format(
1759                        feature, minimum_version, self.cached_backend_version))
1760
1761    def backend_version(self):
1762        """Get the version of the :program:`dialog`-like program (backend).
1763
1764        If the version of the :program:`dialog`-like program can be
1765        retrieved, return it as a string; otherwise, raise
1766        :exc:`UnableToRetrieveBackendVersion`.
1767
1768        This version is not to be confused with the pythondialog
1769        version.
1770
1771        In most cases, you should rather use the
1772        :attr:`cached_backend_version` attribute of :class:`Dialog`
1773        instances, because:
1774
1775          - it avoids calling the backend every time one needs the
1776            version;
1777          - it is a :class:`BackendVersion` instance (or instance of a
1778            subclass) that allows easy and reliable comparisons between
1779            versions;
1780          - the version string corresponding to a
1781            :class:`BackendVersion` instance (or instance of a subclass)
1782            can be obtained with :func:`str`.
1783
1784        Notable exceptions:
1785
1786          - :exc:`UnableToRetrieveBackendVersion`
1787          - :exc:`PythonDialogReModuleError`
1788          - any exception raised by :meth:`Dialog._perform`
1789
1790        .. versionadded:: 2.12
1791
1792        .. versionchanged:: 2.14
1793           Raise :exc:`UnableToRetrieveBackendVersion` instead of
1794           returning ``None`` when the version of the
1795           :program:`dialog`-like program can't be retrieved.
1796
1797        """
1798        code, output = self._perform(["--print-version"],
1799                                     use_persistent_args=False)
1800
1801        # Workaround for old dialog versions
1802        if code == self.OK and not (output.strip() or self.use_stdout):
1803            # output.strip() is empty and self.use_stdout is False.
1804            # This can happen with old dialog versions (1.1-20100428
1805            # apparently does that). Try again, reading from stdout this
1806            # time.
1807            self.use_stdout = True
1808            code, output = self._perform(["--stdout", "--print-version"],
1809                                         use_persistent_args=False,
1810                                         dash_escape="none")
1811            self.use_stdout = False
1812
1813        if code == self.OK:
1814            try:
1815                mo = self._print_version_cre.match(output)
1816                if mo:
1817                    return mo.group("version")
1818                else:
1819                    raise UnableToRetrieveBackendVersion(
1820                        "unable to parse the output of '{0} --print-version': "
1821                        "{1!r}".format(self._dialog_prg, output))
1822            except re.error as e:
1823                raise PythonDialogReModuleError(str(e)) from e
1824        else:
1825            raise UnableToRetrieveBackendVersion(
1826                "exit code {0!r} from the backend".format(code))
1827
1828    def maxsize(self, **kwargs):
1829        """Get the maximum size of dialog boxes.
1830
1831        If the exit status from the backend corresponds to
1832        :attr:`Dialog.OK`, return a :samp:`({lines}, {cols})` tuple of
1833        integers; otherwise, return ``None``.
1834
1835        If you want to obtain the number of lines and columns of the
1836        terminal, you should call this method with
1837        ``use_persistent_args=False``, because :program:`dialog` options
1838        such as :option:`--backtitle` modify the returned values.
1839
1840        Notable exceptions:
1841
1842          - :exc:`PythonDialogReModuleError`
1843          - any exception raised by :meth:`Dialog._perform`
1844
1845        .. versionadded:: 2.12
1846
1847        """
1848        code, output = self._perform(["--print-maxsize"], **kwargs)
1849        if code == self.OK:
1850            try:
1851                mo = self._print_maxsize_cre.match(output)
1852                if mo:
1853                    return tuple(map(int, mo.group("rows", "columns")))
1854                else:
1855                    raise PythonDialogBug(
1856                        "Unable to parse the output of '{0} --print-maxsize': "
1857                        "{1!r}".format(self._dialog_prg, output))
1858            except re.error as e:
1859                raise PythonDialogReModuleError(str(e)) from e
1860        else:
1861            return None
1862
1863    def _default_size(self, values, defaults):
1864        # If 'autowidgetsize' is enabled, set the default values for the
1865        # width/height/... parameters of widget-producing methods to 0 (this
1866        # will actually be done by the caller, this function is only a helper).
1867        if self.autowidgetsize:
1868            defaults = (0,) * len(defaults)
1869
1870        # For every element of 'values': keep it if different from None,
1871        # otherwise replace it with the corresponding value from 'defaults'.
1872        return [ v if v is not None else defaults[i]
1873                 for i, v in enumerate(values) ]
1874
1875    @widget
1876    def buildlist(self, text, height=0, width=0, list_height=0, items=[],
1877                  **kwargs):
1878        """Display a buildlist box.
1879
1880        :param str text:        text to display in the box
1881        :param int height:      height of the box
1882        :param int width:       width of the box
1883        :param int list_height: height of the selected and unselected
1884                                list boxes
1885        :param items:
1886          an iterable of :samp:`({tag}, {item}, {status})` tuples where
1887          *status* specifies the initial selected/unselected state of
1888          each entry; can be ``True`` or ``False``, ``1`` or ``0``,
1889          ``"on"`` or ``"off"`` (``True``, ``1`` and ``"on"`` meaning
1890          selected), or any case variation of these two strings.
1891
1892        :return: a tuple of the form :samp:`({code}, {tags})` where:
1893
1894          - *code* is a :term:`Dialog exit code`;
1895          - *tags* is a list of the tags corresponding to the selected
1896            items, in the order they have in the list on the right.
1897
1898        :rtype: tuple
1899
1900        A :meth:`!buildlist` dialog is similar in logic to the
1901        :meth:`checklist`, but differs in presentation. In this widget,
1902        two lists are displayed, side by side. The list on the left
1903        shows unselected items. The list on the right shows selected
1904        items. As items are selected or unselected, they move between
1905        the two lists. The *status* component of *items* specifies which
1906        items are initially selected.
1907
1908        +--------------+------------------------------------------------+
1909        |     Key      |                     Action                     |
1910        +==============+================================================+
1911        | :kbd:`Space` | select or deselect the highlighted item,       |
1912        |              | *i.e.*, move it between the left and right     |
1913        |              | lists                                          |
1914        +--------------+------------------------------------------------+
1915        | :kbd:`^`     | move the focus to the left list                |
1916        +--------------+------------------------------------------------+
1917        | :kbd:`$`     | move the focus to the right list               |
1918        +--------------+------------------------------------------------+
1919        | :kbd:`Tab`   | move focus (see *visit_items* below)           |
1920        +--------------+------------------------------------------------+
1921        | :kbd:`Enter` | press the focused button                       |
1922        +--------------+------------------------------------------------+
1923
1924        If called with ``visit_items=True``, the :kbd:`Tab` key can move
1925        the focus to the left and right lists, which is probably more
1926        intuitive for users than the default behavior that requires
1927        using :kbd:`^` and :kbd:`$` for this purpose.
1928
1929        This widget requires dialog >= 1.2-20121230.
1930
1931        Notable exceptions:
1932
1933          any exception raised by :meth:`Dialog._perform` or :func:`_to_onoff`
1934
1935        .. versionadded:: 3.0
1936
1937        """
1938        self._dialog_version_check("1.2-20121230", "the buildlist widget")
1939
1940        cmd = ["--buildlist", text, str(height), str(width), str(list_height)]
1941        for t in items:
1942            cmd.extend([ t[0], t[1], _to_onoff(t[2]) ] + list(t[3:]))
1943
1944        code, output = self._perform(cmd, **kwargs)
1945
1946        if code == self.HELP:
1947            help_data = self._parse_help(output, kwargs, multival=True,
1948                                         multival_on_single_line=True)
1949            if self._help_status_on(kwargs):
1950                help_id, selected_tags = help_data
1951                items = [ [ tag, item, tag in selected_tags ] + rest
1952                            for (tag, item, status, *rest) in items ]
1953                return (code, (help_id, selected_tags, items))
1954            else:
1955                return (code, help_data)
1956        elif code in (self.OK, self.EXTRA):
1957            return (code, self._split_shellstyle_arglist(output))
1958        else:
1959            return (code, None)
1960
1961    def _calendar_parse_date(self, date_str):
1962        try:
1963            mo = _calendar_date_cre.match(date_str)
1964        except re.error as e:
1965            raise PythonDialogReModuleError(str(e)) from e
1966
1967        if not mo:
1968            raise UnexpectedDialogOutput(
1969                "the dialog-like program returned the following "
1970                "unexpected output (a date string was expected) from the "
1971                "calendar box: {0!r}".format(date_str))
1972
1973        return [ int(s) for s in mo.group("day", "month", "year") ]
1974
1975    @widget
1976    def calendar(self, text, height=None, width=0, day=-1, month=-1, year=-1,
1977                 **kwargs):
1978        """Display a calendar dialog box.
1979
1980        :param str text:  text to display in the box
1981        :param height:    height of the box (minus the calendar height)
1982        :type height:     int or ``None``
1983        :param int width: width of the box
1984        :param int day:   inititial day highlighted
1985        :param int month: inititial month displayed
1986        :param int year:  inititial year selected
1987        :return: a tuple of the form :samp:`({code}, {date})` where:
1988
1989          - *code* is a :term:`Dialog exit code`;
1990          - *date* is a list of the form :samp:`[{day}, {month},
1991            {year}]`, where *day*, *month* and *year* are integers
1992            corresponding to the date chosen by the user.
1993
1994        :rtype: tuple
1995
1996        A :meth:`!calendar` box displays day, month and year in
1997        separately adjustable windows. If *year* is given as ``0``, the
1998        current date is used as initial value; otherwise, if any of the
1999        values for *day*, *month* and *year* is negative, the current
2000        date's corresponding value is used. You can increment or
2001        decrement any of those using the :kbd:`Left`, :kbd:`Up`,
2002        :kbd:`Right` and :kbd:`Down` arrows. Use :kbd:`Tab` or
2003        :kbd:`Backtab` to move between windows.
2004
2005        Default values for the size parameters when the
2006        :ref:`autowidgetsize <autowidgetsize>` option is disabled:
2007        ``height=6, width=0``.
2008
2009        Notable exceptions:
2010
2011          - any exception raised by :meth:`Dialog._perform`
2012          - :exc:`UnexpectedDialogOutput`
2013          - :exc:`PythonDialogReModuleError`
2014
2015        .. versionchanged:: 3.2
2016           The default values for *day*, *month* and *year* have been
2017           changed from ``0`` to ``-1``.
2018
2019        """
2020        (height,) = self._default_size((height, ), (6,))
2021        (code, output) = self._perform(
2022            ["--calendar", text, str(height), str(width), str(day),
2023               str(month), str(year)],
2024            **kwargs)
2025
2026        if code == self.HELP:
2027            # The output does not depend on whether --help-status was passed
2028            # (dialog 1.2-20130902).
2029            help_data = self._parse_help(output, kwargs, raw_format=True)
2030            return (code, self._calendar_parse_date(help_data))
2031        elif code in (self.OK, self.EXTRA):
2032            return (code, self._calendar_parse_date(output))
2033        else:
2034            return (code, None)
2035
2036    @widget
2037    def checklist(self, text, height=None, width=None, list_height=None,
2038                  choices=[], **kwargs):
2039        """Display a checklist box.
2040
2041        :param str text:    text to display in the box
2042        :param height:      height of the box
2043        :type height:       int or ``None``
2044        :param width:       width of the box
2045        :type width:        int or ``None``
2046        :param list_height:
2047          number of entries displayed in the box at a given time (the
2048          contents can be scrolled)
2049        :type list_height:  int or ``None``
2050        :param choices:
2051          an iterable of :samp:`({tag}, {item}, {status})` tuples where
2052          *status* specifies the initial selected/unselected state of
2053          each entry; can be ``True`` or ``False``, ``1`` or ``0``,
2054          ``"on"`` or ``"off"`` (``True``, ``1`` and ``"on"`` meaning
2055          selected), or any case variation of these two strings.
2056        :return: a tuple of the form :samp:`({code}, [{tag}, ...])`
2057          whose first element is a :term:`Dialog exit code` and second
2058          element lists all tags for the entries selected by the user.
2059          If the user exits with :kbd:`Esc` or :guilabel:`Cancel`, the
2060          returned tag list is empty.
2061
2062        :rtype: tuple
2063
2064        Default values for the size parameters when the
2065        :ref:`autowidgetsize <autowidgetsize>` option is disabled:
2066        ``height=15, width=54, list_height=7``.
2067
2068        Notable exceptions:
2069
2070          any exception raised by :meth:`Dialog._perform` or :func:`_to_onoff`
2071
2072        """
2073        height, width, list_height = self._default_size(
2074            (height, width, list_height), (15, 54, 7))
2075        cmd = ["--checklist", text, str(height), str(width), str(list_height)]
2076        for t in choices:
2077            t = [ t[0], t[1], _to_onoff(t[2]) ] + list(t[3:])
2078            cmd.extend(t)
2079
2080        # The dialog output cannot be parsed reliably (at least in dialog
2081        # 0.9b-20040301) without --separate-output (because double quotes in
2082        # tags are escaped with backslashes, but backslashes are not
2083        # themselves escaped and you have a problem when a tag ends with a
2084        # backslash--the output makes you think you've encountered an embedded
2085        # double-quote).
2086        kwargs["separate_output"] = True
2087
2088        (code, output) = self._perform(cmd, **kwargs)
2089        # Since we used --separate-output, the tags are separated by a newline
2090        # in the output. There is also a final newline after the last tag.
2091
2092        if code == self.HELP:
2093            help_data = self._parse_help(output, kwargs, multival=True)
2094            if self._help_status_on(kwargs):
2095                help_id, selected_tags = help_data
2096                choices = [ [ tag, item, tag in selected_tags ] + rest
2097                            for (tag, item, status, *rest) in choices ]
2098                return (code, (help_id, selected_tags, choices))
2099            else:
2100                return (code, help_data)
2101        else:
2102            return (code, output.split('\n')[:-1])
2103
2104    def _form_updated_items(self, status, elements):
2105        """Return a complete list with up-to-date items from 'status'.
2106
2107        Return a new list of same length as 'elements'. Items are
2108        taken from 'status', except when data inside 'elements'
2109        indicates a read-only field: such items are not output by
2110        dialog ... --help-status ..., and therefore have to be
2111        extracted from 'elements' instead of 'status'.
2112
2113        Actually, for 'mixedform', the elements that are defined as
2114        read-only using the attribute instead of a non-positive
2115        field_length are not concerned by this function, since they
2116        are included in the --help-status output.
2117
2118        """
2119        res = []
2120        for i, (label, yl, xl, item, yi, xi, field_length, *rest) \
2121                in enumerate(elements):
2122            res.append(status[i] if field_length > 0 else item)
2123
2124        return res
2125
2126    def _generic_form(self, widget_name, method_name, text, elements, height=0,
2127                      width=0, form_height=0, **kwargs):
2128        cmd = ["--%s" % widget_name, text, str(height), str(width),
2129               str(form_height)]
2130
2131        if not elements:
2132            raise BadPythonDialogUsage(
2133                "{0}.{1}.{2}: empty ELEMENTS sequence: {3!r}".format(
2134                    __name__, type(self).__name__, method_name, elements))
2135
2136        elt_len = len(elements[0]) # for consistency checking
2137        for i, elt in enumerate(elements):
2138            if len(elt) != elt_len:
2139                raise BadPythonDialogUsage(
2140                    "{0}.{1}.{2}: ELEMENTS[0] has length {3}, whereas "
2141                    "ELEMENTS[{4}] has length {5}".format(
2142                        __name__, type(self).__name__, method_name,
2143                        elt_len, i, len(elt)))
2144
2145            # Give names to make the code more readable
2146            if widget_name in ("form", "passwordform"):
2147                label, yl, xl, item, yi, xi, field_length, input_length = \
2148                    elt[:8]
2149                rest = elt[8:]  # optional "item_help" string
2150            elif widget_name == "mixedform":
2151                label, yl, xl, item, yi, xi, field_length, input_length, \
2152                    attributes = elt[:9]
2153                rest = elt[9:]  # optional "item_help" string
2154            else:
2155                raise PythonDialogBug(
2156                    "unexpected widget name in {0}.{1}._generic_form(): "
2157                    "{2!r}".format(__name__, type(self).__name__, widget_name))
2158
2159            for name, value in (("label", label), ("item", item)):
2160                if not isinstance(value, str):
2161                    raise BadPythonDialogUsage(
2162                        "{0}.{1}.{2}: {3!r} element not a string: {4!r}".format(
2163                            __name__, type(self).__name__,
2164                            method_name, name, value))
2165
2166            cmd.extend((label, str(yl), str(xl), item, str(yi), str(xi),
2167                        str(field_length), str(input_length)))
2168            if widget_name == "mixedform":
2169                cmd.append(str(attributes))
2170            # "item help" string when using --item-help, nothing otherwise
2171            cmd.extend(rest)
2172
2173        (code, output) = self._perform(cmd, **kwargs)
2174
2175        if code == self.HELP:
2176            help_data = self._parse_help(output, kwargs, multival=True)
2177            if self._help_status_on(kwargs):
2178                help_id, status = help_data
2179                # 'status' does not contain the fields marked as read-only in
2180                # 'elements'. Build a list containing all up-to-date items.
2181                updated_items = self._form_updated_items(status, elements)
2182                # Reconstruct 'elements' with the updated items taken from
2183                # 'status'.
2184                elements = [ [ label, yl, xl, updated_item ] + rest for
2185                             ((label, yl, xl, item, *rest), updated_item) in
2186                             zip(elements, updated_items) ]
2187                return (code, (help_id, status, elements))
2188            else:
2189                return (code, help_data)
2190        else:
2191            return (code, output.split('\n')[:-1])
2192
2193    @widget
2194    def form(self, text, elements, height=0, width=0, form_height=0, **kwargs):
2195        """Display a form consisting of labels and fields.
2196
2197        :param str text:        text to display in the box
2198        :param elements:        sequence describing the labels and
2199                                fields (see below)
2200        :param int height:      height of the box
2201        :param int width:       width of the box
2202        :param int form_height: number of form lines displayed at the
2203                                same time
2204        :return: a tuple of the form :samp:`({code}, {list})` where:
2205
2206          - *code* is a :term:`Dialog exit code`;
2207          - *list* gives the contents of every editable field on exit,
2208            with the same order as in *elements*.
2209
2210        :rtype: tuple
2211
2212        A :meth:`!form` box consists in a series of :dfn:`fields` and
2213        associated :dfn:`labels`. This type of dialog is suitable for
2214        adjusting configuration parameters and similar tasks.
2215
2216        Each element of *elements* must itself be a sequence
2217        :samp:`({label}, {yl}, {xl}, {item}, {yi}, {xi}, {field_length},
2218        {input_length})` containing the various parameters concerning a
2219        given field and the associated label.
2220
2221        *label* is a string that will be displayed at row *yl*, column
2222        *xl*. *item* is a string giving the initial value for the field,
2223        which will be displayed at row *yi*, column *xi* (row and column
2224        numbers starting from 1).
2225
2226        *field_length* and *input_length* are integers that respectively
2227        specify the number of characters used for displaying the field
2228        and the maximum number of characters that can be entered for
2229        this field. These two integers also determine whether the
2230        contents of the field can be modified, as follows:
2231
2232          - if *field_length* is zero, the field cannot be altered and
2233            its contents determines the displayed length;
2234
2235          - if *field_length* is negative, the field cannot be altered
2236            and the opposite of *field_length* gives the displayed
2237            length;
2238
2239          - if *input_length* is zero, it is set to *field_length*.
2240
2241        Notable exceptions:
2242
2243          - :exc:`BadPythonDialogUsage`
2244          - any exception raised by :meth:`Dialog._perform`
2245
2246        """
2247        return self._generic_form("form", "form", text, elements,
2248                                  height, width, form_height, **kwargs)
2249
2250    @widget
2251    def passwordform(self, text, elements, height=0, width=0, form_height=0,
2252                     **kwargs):
2253        """Display a form consisting of labels and invisible fields.
2254
2255        This widget is identical to the :meth:`form` box, except that
2256        all text fields are treated as :meth:`passwordbox` widgets
2257        rather than :meth:`inputbox` widgets.
2258
2259        By default (as in :program:`dialog`), nothing is echoed to the
2260        terminal as the user types in the invisible fields. This can be
2261        confusing to users. Use ``insecure=True`` (keyword argument) if
2262        you want an asterisk to be echoed for each character entered by
2263        the user.
2264
2265        Notable exceptions:
2266
2267          - :exc:`BadPythonDialogUsage`
2268          - any exception raised by :meth:`Dialog._perform`
2269
2270        """
2271        return self._generic_form("passwordform", "passwordform", text,
2272                                  elements, height, width, form_height,
2273                                  **kwargs)
2274
2275    @widget
2276    def mixedform(self, text, elements, height=0, width=0, form_height=0,
2277                  **kwargs):
2278        """Display a form consisting of labels and fields.
2279
2280        :param str text:        text to display in the box
2281        :param elements:        sequence describing the labels and
2282                                fields (see below)
2283        :param int height:      height of the box
2284        :param int width:       width of the box
2285        :param int form_height: number of form lines displayed at the
2286                                same time
2287        :return: a tuple of the form :samp:`({code}, {list})` where:
2288
2289          - *code* is a :term:`Dialog exit code`;
2290          - *list* gives the contents of every field on exit, with the
2291            same order as in *elements*.
2292
2293        :rtype: tuple
2294
2295        A :meth:`!mixedform` box is very similar to a :meth:`form` box,
2296        and differs from the latter by allowing field attributes to be
2297        specified.
2298
2299        Each element of *elements* must itself be a sequence
2300        :samp:`({label}, {yl}, {xl}, {item}, {yi}, {xi}, {field_length},
2301        {input_length}, {attributes})` containing the various parameters
2302        concerning a given field and the associated label.
2303
2304        *attributes* is an integer interpreted as a bit mask with the
2305        following meaning (bit 0 being the least significant bit):
2306
2307        +------------+-----------------------------------------------+
2308        | Bit number |                    Meaning                    |
2309        +============+===============================================+
2310        |     0      | the field should be hidden (e.g., a password) |
2311        +------------+-----------------------------------------------+
2312        |     1      | the field should be read-only (e.g., a label) |
2313        +------------+-----------------------------------------------+
2314
2315        For all other parameters, please refer to the documentation of
2316        the :meth:`form` box.
2317
2318        The return value is the same as would be with the :meth:`!form`
2319        box, except that fields marked as read-only with bit 1 of
2320        *attributes* are also included in the output list.
2321
2322        Notable exceptions:
2323
2324          - :exc:`BadPythonDialogUsage`
2325          - any exception raised by :meth:`Dialog._perform`
2326
2327        """
2328        return self._generic_form("mixedform", "mixedform", text, elements,
2329                                  height, width, form_height, **kwargs)
2330
2331    @widget
2332    def dselect(self, filepath, height=0, width=0, **kwargs):
2333        """Display a directory selection dialog box.
2334
2335        :param str filepath: initial path
2336        :param int height:   height of the box
2337        :param int width:    width of the box
2338        :return: a tuple of the form :samp:`({code}, {path})` where:
2339
2340          - *code* is a :term:`Dialog exit code`;
2341          - *path* is the directory chosen by the user.
2342
2343        :rtype: tuple
2344
2345        The directory selection dialog displays a text entry window
2346        in which you can type a directory, and above that a window
2347        with directory names.
2348
2349        Here, *filepath* can be a path to a file, in which case the
2350        directory window will display the contents of the path and the
2351        text entry window will contain the preselected directory.
2352
2353        Use :kbd:`Tab` or the arrow keys to move between the windows.
2354        Within the directory window, use the :kbd:`Up` and :kbd:`Down`
2355        arrow keys to scroll the current selection. Use the :kbd:`Space`
2356        bar to copy the current selection into the text entry window.
2357
2358        Typing any printable character switches focus to the text entry
2359        window, entering that character as well as scrolling the
2360        directory window to the closest match.
2361
2362        Use :kbd:`Enter` or the :guilabel:`OK` button to accept the
2363        current value in the text entry window and exit.
2364
2365        Notable exceptions:
2366
2367          any exception raised by :meth:`Dialog._perform`
2368
2369        """
2370        # The help output does not depend on whether --help-status was passed
2371        # (dialog 1.2-20130902).
2372        return self._widget_with_string_output(
2373            ["--dselect", filepath, str(height), str(width)],
2374            kwargs, raw_help=True)
2375
2376    @widget
2377    def editbox(self, filepath, height=0, width=0, **kwargs):
2378        """Display a basic text editor dialog box.
2379
2380        :param str filepath: path to a file which determines the initial
2381                             contents of the dialog box
2382        :param int height:   height of the box
2383        :param int width:    width of the box
2384        :return: a tuple of the form :samp:`({code}, {text})` where:
2385
2386          - *code* is a :term:`Dialog exit code`;
2387          - *text* is the contents of the text entry window on exit.
2388
2389        :rtype: tuple
2390
2391        The :meth:`!editbox` dialog displays a copy of the file
2392        contents. You may edit it using the :kbd:`Backspace`,
2393        :kbd:`Delete` and cursor keys to correct typing errors. It also
2394        recognizes :kbd:`Page Up` and :kbd:`Page Down`. Unlike the
2395        :meth:`inputbox`, you must tab to the :guilabel:`OK` or
2396        :guilabel:`Cancel` buttons to close the dialog. Pressing the
2397        :kbd:`Enter` key within the box will split the corresponding
2398        line.
2399
2400        Notable exceptions:
2401
2402          any exception raised by :meth:`Dialog._perform`
2403
2404        .. seealso:: method :meth:`editbox_str`
2405
2406        """
2407        return self._widget_with_string_output(
2408            ["--editbox", filepath, str(height), str(width)],
2409            kwargs)
2410
2411    def editbox_str(self, init_contents, *args, **kwargs):
2412        """
2413        Display a basic text editor dialog box (wrapper around :meth:`editbox`).
2414
2415        :param str init_contents:
2416                          initial contents of the dialog box
2417        :param args:      positional arguments to pass to :meth:`editbox`
2418        :param kwargs:    keyword arguments to pass to :meth:`editbox`
2419        :return: a tuple of the form :samp:`({code}, {text})` where:
2420
2421          - *code* is a :term:`Dialog exit code`;
2422          - *text* is the contents of the text entry window on exit.
2423
2424        :rtype: tuple
2425
2426        The :meth:`!editbox_str` method is a thin wrapper around
2427        :meth:`editbox`. :meth:`!editbox_str` accepts a string as its
2428        first argument, instead of a file path. That string is written
2429        to a temporary file whose path is passed to :meth:`!editbox`
2430        along with the arguments specified via *args* and *kwargs*.
2431        Please refer to :meth:`!editbox`\'s documentation for more
2432        details.
2433
2434        Notes:
2435
2436          - the temporary file is deleted before the method returns;
2437          - if *init_contents* does not end with a newline character
2438            (``'\\n'``), then this method automatically adds one. This
2439            is done in order to avoid unexpected behavior resulting from
2440            the fact that, before version 1.3-20160209,
2441            :program:`dialog`\'s editbox widget ignored the last line of
2442            the input file unless it was terminated by a newline
2443            character.
2444
2445        Notable exceptions:
2446
2447          - :exc:`PythonDialogOSError`
2448          - any exception raised by :meth:`Dialog._perform`
2449
2450        .. versionadded:: 3.4
2451
2452        .. seealso:: method :meth:`editbox`
2453
2454        """
2455        if not init_contents.endswith('\n'):
2456            # Before version 1.3-20160209, dialog's --editbox widget
2457            # doesn't read the last line of the input file unless it
2458            # ends with a '\n' character.
2459            init_contents += '\n'
2460
2461        with _OSErrorHandling():
2462            tmpfile = tempfile.NamedTemporaryFile(
2463                mode="w", prefix="pythondialog.tmp", delete=False)
2464            try:
2465                with tmpfile as f:
2466                    f.write(init_contents)
2467                # The temporary file is now closed. According to the tempfile
2468                # module documentation, this is necessary if we want to be able
2469                # to reopen it reliably regardless of the platform.
2470
2471                res = self.editbox(tmpfile.name, *args, **kwargs)
2472            finally:
2473                # The test should always succeed, but I prefer being on the
2474                # safe side.
2475                if os.path.exists(tmpfile.name):
2476                    os.unlink(tmpfile.name)
2477
2478        return res
2479
2480    @widget
2481    def fselect(self, filepath, height=0, width=0, **kwargs):
2482        """Display a file selection dialog box.
2483
2484        :param str filepath: initial path
2485        :param int height:   height of the box
2486        :param int width:    width of the box
2487        :return: a tuple of the form :samp:`({code}, {path})` where:
2488
2489          - *code* is a :term:`Dialog exit code`;
2490          - *path* is the path chosen by the user (the last element of
2491            which may be a directory or a file).
2492
2493        :rtype: tuple
2494
2495        The file selection dialog displays a text entry window in
2496        which you can type a file name (or directory), and above that
2497        two windows with directory names and file names.
2498
2499        Here, *filepath* can be a path to a file, in which case the file
2500        and directory windows will display the contents of the path and
2501        the text entry window will contain the preselected file name.
2502
2503        Use :kbd:`Tab` or the arrow keys to move between the windows.
2504        Within the directory or file name windows, use the :kbd:`Up` and
2505        :kbd:`Down` arrow keys to scroll the current selection. Use the
2506        :kbd:`Space` bar to copy the current selection into the text
2507        entry window.
2508
2509        Typing any printable character switches focus to the text entry
2510        window, entering that character as well as scrolling the
2511        directory and file name windows to the closest match.
2512
2513        Use :kbd:`Enter` or the :guilabel:`OK` button to accept the
2514        current value in the text entry window, or the
2515        :guilabel:`Cancel` button to cancel.
2516
2517        Notable exceptions:
2518
2519          any exception raised by :meth:`Dialog._perform`
2520
2521        """
2522        # The help output does not depend on whether --help-status was passed
2523        # (dialog 1.2-20130902).
2524        return self._widget_with_string_output(
2525            ["--fselect", filepath, str(height), str(width)],
2526            kwargs, strip_xdialog_newline=True, raw_help=True)
2527
2528    def gauge_start(self, text="", height=None, width=None, percent=0,
2529                    **kwargs):
2530        """Display a gauge box.
2531
2532        :param str text:    text to display in the box
2533        :param height:      height of the box
2534        :type height:       int or ``None``
2535        :param width:       width of the box
2536        :type width:        int or ``None``
2537        :param int percent: initial percentage shown in the meter
2538        :return:            undefined
2539
2540        A gauge box displays a meter along the bottom of the box. The
2541        meter indicates a percentage.
2542
2543        This function starts the :program:`dialog`-like program, telling
2544        it to display a gauge box containing a text and an initial
2545        percentage in the meter.
2546
2547
2548        .. rubric:: Gauge typical usage
2549
2550        Gauge typical usage (assuming that *d* is an instance of the
2551        :class:`Dialog` class) looks like this::
2552
2553            d.gauge_start()
2554            # do something
2555            d.gauge_update(10)       # 10% of the whole task is done
2556            # ...
2557            d.gauge_update(100, "any text here") # work is done
2558            exit_code = d.gauge_stop()           # cleanup actions
2559
2560
2561        Default values for the size parameters when the
2562        :ref:`autowidgetsize <autowidgetsize>` option is disabled:
2563        ``height=8, width=54``.
2564
2565        Notable exceptions:
2566
2567          - any exception raised by :meth:`_call_program`
2568          - :exc:`PythonDialogOSError`
2569
2570        """
2571        height, width = self._default_size((height, width), (8, 54))
2572        with _OSErrorHandling():
2573            # We need a pipe to send data to the child (dialog) process's
2574            # stdin while it is running.
2575            # rfd = File Descriptor for Reading
2576            # wfd = File Descriptor for Writing
2577            (child_stdin_rfd, child_stdin_wfd)  = os.pipe()
2578
2579            child_pid, child_output_rfd, args_file = self._call_program(
2580                ["--gauge", text, str(height), str(width), str(percent)],
2581                redir_child_stdin_from_fd=child_stdin_rfd,
2582                close_fds=(child_stdin_wfd,), **kwargs)
2583
2584            # fork() is done. We don't need child_stdin_rfd in the father
2585            # process anymore.
2586            os.close(child_stdin_rfd)
2587
2588            self._gauge_process = {
2589                "pid": child_pid,
2590                "stdin": os.fdopen(child_stdin_wfd, "w"),
2591                "child_output_rfd": child_output_rfd,
2592                "args_file": args_file
2593                }
2594
2595    def gauge_update(self, percent, text="", update_text=False):
2596        """Update a running gauge box.
2597
2598        :param int percent:      new percentage to show in the gauge
2599                                 meter
2600        :param str text:         new text to optionally display in the
2601                                 box
2602        :param bool update_text: whether to update the text in the box
2603        :return:                 undefined
2604
2605        This function updates the percentage shown by the meter of a
2606        running gauge box (meaning :meth:`gauge_start` must have been
2607        called previously). If *update_text* is ``True``, the text
2608        displayed in the box is also updated.
2609
2610        See the :meth:`gauge_start` method documentation for information
2611        about how to use a gauge.
2612
2613        Notable exception:
2614
2615          :exc:`PythonDialogIOError` (:exc:`PythonDialogOSError` from
2616          Python 3.3 onwards) can be raised if there is an I/O error
2617          while trying to write to the pipe used to talk to the
2618          :program:`dialog`-like program.
2619
2620        """
2621        if not isinstance(percent, int):
2622            raise BadPythonDialogUsage(
2623                "the 'percent' argument of gauge_update() must be an integer, "
2624                "but {0!r} is not".format(percent))
2625
2626        if update_text:
2627            gauge_data = "XXX\n{0}\n{1}\nXXX\n".format(percent, text)
2628        else:
2629            gauge_data = "{0}\n".format(percent)
2630        with _OSErrorHandling():
2631            self._gauge_process["stdin"].write(gauge_data)
2632            self._gauge_process["stdin"].flush()
2633
2634    # For "compatibility" with the old dialog.py...
2635    def gauge_iterate(*args, **kwargs):
2636        """Update a running gauge box.
2637
2638        .. deprecated:: 2.03
2639          Use :meth:`gauge_update` instead.
2640
2641        """
2642        warnings.warn("Dialog.gauge_iterate() has been obsolete for "
2643                      "many years", DeprecationWarning)
2644        gauge_update(*args, **kwargs)
2645
2646    @widget
2647    @retval_is_code
2648    def gauge_stop(self):
2649        """Terminate a running gauge widget.
2650
2651        :return:         a :term:`Dialog exit code`
2652        :rtype:          str
2653
2654        This function performs the appropriate cleanup actions to
2655        terminate a running gauge started with :meth:`gauge_start`.
2656
2657        See the :meth:`!gauge_start` method documentation for
2658        information about how to use a gauge.
2659
2660        Notable exceptions:
2661
2662          - any exception raised by :meth:`_handle_program_exit`;
2663          - :exc:`PythonDialogIOError` (:exc:`PythonDialogOSError` from
2664            Python 3.3 onwards) can be raised if closing the pipe used
2665            to talk to the :program:`dialog`-like program fails.
2666
2667        """
2668        p = self._gauge_process
2669        # Close the pipe that we are using to feed dialog's stdin
2670        with _OSErrorHandling():
2671            p["stdin"].close()
2672        # According to dialog(1), the output should always be empty.
2673        exit_code = self._handle_program_exit(p["pid"],
2674                                              p["child_output_rfd"],
2675                                              p["args_file"])[0]
2676        return exit_code
2677
2678    @widget
2679    @retval_is_code
2680    def infobox(self, text, height=None, width=None, **kwargs):
2681        """Display an information dialog box.
2682
2683        :param str text: text to display in the box
2684        :param height:   height of the box
2685        :type height:    int or ``None``
2686        :param width:    width of the box
2687        :type width:     int or ``None``
2688        :return:         a :term:`Dialog exit code`
2689        :rtype:          str
2690
2691        An info box is basically a message box. However, in this case,
2692        :program:`dialog` will exit immediately after displaying the
2693        message to the user. The screen is not cleared when
2694        :program:`dialog` exits, so that the message will remain on the
2695        screen after the method returns. This is useful when you want to
2696        inform the user that some operations are carrying on that may
2697        require some time to finish.
2698
2699        Default values for the size parameters when the
2700        :ref:`autowidgetsize <autowidgetsize>` option is disabled:
2701        ``height=10, width=30``.
2702
2703        Notable exceptions:
2704
2705          any exception raised by :meth:`Dialog._perform`
2706
2707        """
2708        height, width = self._default_size((height, width), (10, 30))
2709        return self._widget_with_no_output(
2710            "infobox",
2711            ["--infobox", text, str(height), str(width)],
2712            kwargs)
2713
2714    @widget
2715    def inputbox(self, text, height=None, width=None, init='', **kwargs):
2716        """Display an input dialog box.
2717
2718        :param str text: text to display in the box
2719        :param height:   height of the box
2720        :type height:    int or ``None``
2721        :param width:    width of the box
2722        :type width:     int or ``None``
2723        :param str init: default input string
2724        :return: a tuple of the form :samp:`({code}, {string})` where:
2725
2726          - *code* is a :term:`Dialog exit code`;
2727          - *string* is the string entered by the user.
2728
2729        :rtype: tuple
2730
2731        An input box is useful when you want to ask questions that
2732        require the user to input a string as the answer. If *init* is
2733        supplied, it is used to initialize the input string. When
2734        entering the string, the :kbd:`Backspace` key can be used to
2735        correct typing errors. If the input string is longer than can
2736        fit in the dialog box, the input field will be scrolled.
2737
2738        Default values for the size parameters when the
2739        :ref:`autowidgetsize <autowidgetsize>` option is disabled:
2740        ``height=10, width=30``.
2741
2742        Notable exceptions:
2743
2744          any exception raised by :meth:`Dialog._perform`
2745
2746        """
2747        height, width = self._default_size((height, width), (10, 30))
2748        # The help output does not depend on whether --help-status was passed
2749        # (dialog 1.2-20130902).
2750        return self._widget_with_string_output(
2751            ["--inputbox", text, str(height), str(width), init],
2752            kwargs, strip_xdialog_newline=True, raw_help=True)
2753
2754    @widget
2755    def inputmenu(self, text, height=0, width=None, menu_height=None,
2756                  choices=[], **kwargs):
2757        """Display an inputmenu dialog box.
2758
2759        :param str text:    text to display in the box
2760        :param int height:  height of the box
2761        :param width:       width of the box
2762        :type width:        int or ``None``
2763        :param menu_height: height of the menu (scrollable part)
2764        :type menu_height:  int or ``None``
2765        :param choices:     an iterable of :samp:`({tag}, {item})`
2766                            tuples, the meaning of which is explained
2767                            below
2768        :return:            see :ref:`below <inputmenu-return-value>`
2769
2770
2771        .. rubric:: Overview
2772
2773        An :meth:`!inputmenu` box is a dialog box that can be used to
2774        present a list of choices in the form of a menu for the user to
2775        choose. Choices are displayed in the given order. The main
2776        differences with the :meth:`menu` dialog box are:
2777
2778          - entries are not automatically centered, but left-adjusted;
2779
2780          - the current entry can be renamed by pressing the
2781            :guilabel:`Rename` button, which allows editing the *item*
2782            part of the current entry.
2783
2784        Each menu entry consists of a *tag* string and an *item* string.
2785        The :dfn:`tag` gives the entry a name to distinguish it from the
2786        other entries in the menu and to provide quick keyboard access.
2787        The :dfn:`item` is a short description of the option that the
2788        entry represents.
2789
2790        The user can move between the menu entries by pressing the
2791        :kbd:`Up` and :kbd:`Down` arrow keys or the first letter of the
2792        tag as a hot key. There are *menu_height* lines (not entries!)
2793        displayed in the scrollable part of the menu at one time.
2794
2795        At the time of this writing (with :program:`dialog`
2796        1.2-20140219), it is not possible to add an Extra button to this
2797        widget, because internally, the :guilabel:`Rename` button *is*
2798        the Extra button.
2799
2800        .. note::
2801
2802          It is strongly advised not to put any space in tags, otherwise
2803          the :program:`dialog` output can be ambiguous if the
2804          corresponding entry is renamed, causing pythondialog to return
2805          a wrong tag string and new item text.
2806
2807          The reason is that in this case, the :program:`dialog` output
2808          is :samp:`RENAMED {tag} {item}` and pythondialog cannot guess
2809          whether spaces after the :samp:`RENAMED` + *space* prefix
2810          belong to the *tag* or the new *item* text.
2811
2812        .. note::
2813
2814          There is no point in calling this method with
2815          ``help_status=True``, because it is not possible to rename
2816          several items nor is it possible to choose the
2817          :guilabel:`Help` button (or any button other than
2818          :guilabel:`Rename`) once one has started to rename an item.
2819
2820        .. _inputmenu-return-value:
2821
2822        .. rubric:: Return value
2823
2824        Return a tuple of the form :samp:`({exit_info}, {tag},
2825        {new_item_text})` where:
2826
2827          + *exit_info* is either:
2828
2829            - the string ``"accepted"``, meaning that an entry was
2830              accepted without renaming;
2831            - the string ``"renamed"``, meaning that an entry was
2832              accepted after being renamed;
2833            - one of the standard :term:`Dialog exit codes <Dialog exit
2834              code>` :attr:`Dialog.CANCEL`, :attr:`Dialog.ESC` or
2835              :attr:`Dialog.HELP` (:attr:`Dialog.EXTRA` can't be
2836              returned, because internally, the :guilabel:`Rename`
2837              button *is* the Extra button).
2838
2839          + *tag* indicates which entry was accepted (with or without
2840            renaming), if any. If no entry was accepted (e.g., if the
2841            dialog was exited with the :guilabel:`Cancel` button), then
2842            *tag* is ``None``.
2843
2844          + *new_item_text* gives the new *item* part of the renamed
2845            entry if *exit_info* is ``"renamed"``, otherwise it is
2846            ``None``.
2847
2848        Default values for the size parameters when the
2849        :ref:`autowidgetsize <autowidgetsize>` option is disabled:
2850        ``height=0, width=60, menu_height=7``.
2851
2852        Notable exceptions:
2853
2854          any exception raised by :meth:`Dialog._perform`
2855
2856        """
2857        width, menu_height = self._default_size((width, menu_height), (60, 7))
2858        cmd = ["--inputmenu", text, str(height), str(width), str(menu_height)]
2859        for t in choices:
2860            cmd.extend(t)
2861        (code, output) = self._perform(cmd, **kwargs)
2862
2863        if code == self.HELP:
2864            help_id = self._parse_help(output, kwargs)
2865            return (code, help_id, None)
2866        elif code == self.OK:
2867            return ("accepted", output, None)
2868        elif code == self.EXTRA:
2869            if not output.startswith("RENAMED "):
2870                raise PythonDialogBug(
2871                    "'output' does not start with 'RENAMED ': {0!r}".format(
2872                        output))
2873            t = output.split(' ', 2)
2874            return ("renamed", t[1], t[2])
2875        else:
2876            return (code, None, None)
2877
2878    @widget
2879    def menu(self, text, height=None, width=None, menu_height=None, choices=[],
2880             **kwargs):
2881        """Display a menu dialog box.
2882
2883        :param str text:        text to display in the box
2884        :param height:      height of the box
2885        :type height:       int or ``None``
2886        :param width:       width of the box
2887        :type width:        int or ``None``
2888        :param menu_height: number of entries displayed in the box
2889                            (which can be scrolled) at a given time
2890        :type menu_height:  int or ``None``
2891        :param choices:     an iterable of :samp:`({tag}, {item})`
2892                            tuples, the meaning of which is explained
2893                            below
2894        :return: a tuple of the form :samp:`({code}, {tag})` where:
2895
2896          - *code* is a :term:`Dialog exit code`;
2897          - *tag* is the tag string corresponding to the item that the
2898            user chose.
2899
2900        :rtype: tuple
2901
2902        As its name suggests, a :meth:`!menu` box is a dialog box that
2903        can be used to present a list of choices in the form of a menu
2904        for the user to choose. Choices are displayed in the given
2905        order.
2906
2907        Each menu entry consists of a *tag* string and an *item* string.
2908        The :dfn:`tag` gives the entry a name to distinguish it from the
2909        other entries in the menu and to provide quick keyboard access.
2910        The :dfn:`item` is a short description of the option that the
2911        entry represents.
2912
2913        The user can move between the menu entries by pressing the
2914        :kbd:`Up` and :kbd:`Down` arrow keys, the first letter of the
2915        tag as a hotkey, or the number keys :kbd:`1` through :kbd:`9`.
2916        There are *menu_height* entries displayed in the menu at one
2917        time, but it will be scrolled if there are more entries than
2918        that.
2919
2920        Default values for the size parameters when the
2921        :ref:`autowidgetsize <autowidgetsize>` option is disabled:
2922        ``height=15, width=54, menu_height=7``.
2923
2924        Notable exceptions:
2925
2926          any exception raised by :meth:`Dialog._perform`
2927
2928        """
2929        height, width, menu_height = self._default_size(
2930            (height, width, menu_height), (15, 54, 7))
2931        cmd = ["--menu", text, str(height), str(width), str(menu_height)]
2932        for t in choices:
2933            cmd.extend(t)
2934
2935        return self._widget_with_string_output(
2936            cmd, kwargs, strip_xdialog_newline=True)
2937
2938    @widget
2939    @retval_is_code
2940    def mixedgauge(self, text, height=0, width=0, percent=0, elements=[],
2941             **kwargs):
2942        """Display a mixed gauge dialog box.
2943
2944        :param str text:    text to display in the middle of the box,
2945                            between the elements list and the progress
2946                            bar
2947        :param int height:  height of the box
2948        :param int width:   width of the box
2949        :param int percent: integer giving the percentage for the global
2950                            progress bar
2951        :param elements:    an iterable of :samp:`({tag}, {item})`
2952                            tuples, the meaning of which is explained
2953                            below
2954        :return:            a :term:`Dialog exit code`
2955        :rtype:             str
2956
2957        A :meth:`!mixedgauge` box displays a list of "elements" with
2958        status indication for each of them, followed by a text and
2959        finally a global progress bar along the bottom of the box.
2960
2961        The top part ("elements") is suitable for displaying a task
2962        list. One element is displayed per line, with its *tag* part on
2963        the left and its *item* part on the right. The *item* part is a
2964        string that is displayed on the right of the same line.
2965
2966        The *item* part of an element can be an arbitrary string.
2967        Special values listed in the :manpage:`dialog(3)` manual page
2968        are translated into a status indication for the corresponding
2969        task (*tag*), such as: "Succeeded", "Failed", "Passed",
2970        "Completed", "Done", "Skipped", "In Progress", "Checked", "N/A"
2971        or a progress bar.
2972
2973        A progress bar for an element is obtained by supplying a
2974        negative number for the *item*. For instance, ``"-75"`` will
2975        cause a progress bar indicating 75% to be displayed on the
2976        corresponding line.
2977
2978        For your convenience, if an *item* appears to be an integer or a
2979        float, it will be converted to a string before being passed to
2980        the :program:`dialog`-like program.
2981
2982        *text* is shown as a sort of caption between the list and the
2983        global progress bar. The latter displays *percent* as the
2984        percentage of completion.
2985
2986        Contrary to the regular :ref:`gauge widget <gauge-widget>`,
2987        :meth:`!mixedgauge` is completely static. You have to call
2988        :meth:`!mixedgauge` several times in order to display different
2989        percentages in the global progress bar or various status
2990        indicators for a given task.
2991
2992        .. note::
2993
2994           Calling :meth:`!mixedgauge` several times is likely to cause
2995           unwanted flickering because of the screen initializations
2996           performed by :program:`dialog` on every run.
2997
2998        Notable exceptions:
2999
3000          any exception raised by :meth:`Dialog._perform`
3001
3002        """
3003        cmd = ["--mixedgauge", text, str(height), str(width), str(percent)]
3004        for t in elements:
3005            cmd.extend( (t[0], str(t[1])) )
3006        return self._widget_with_no_output("mixedgauge", cmd, kwargs)
3007
3008    @widget
3009    @retval_is_code
3010    def msgbox(self, text, height=None, width=None, **kwargs):
3011        """Display a message dialog box, with scrolling and line wrapping.
3012
3013        :param str text: text to display in the box
3014        :param height:   height of the box
3015        :type height:    int or ``None``
3016        :param width:    width of the box
3017        :type width:     int or ``None``
3018        :return:         a :term:`Dialog exit code`
3019        :rtype:          str
3020
3021        Display *text* in a message box, with a scrollbar and percentage
3022        indication if *text* is too long to fit in a single "screen".
3023
3024        An :meth:`!msgbox` is very similar to a :meth:`yesno` box. The
3025        only difference between an :meth:`!msgbox` and a :meth:`!yesno`
3026        box is that the former only has a single :guilabel:`OK` button.
3027        You can use :meth:`!msgbox` to display any message you like.
3028        After reading the message, the user can press the :kbd:`Enter`
3029        key so that :program:`dialog` will exit and the calling program
3030        can continue its operation.
3031
3032        :meth:`!msgbox` performs automatic line wrapping. If you want to
3033        force a newline at some point, simply insert it in *text*. In
3034        other words (with the default settings), newline characters in
3035        *text* **are** respected; the line wrapping process performed by
3036        :program:`dialog` only inserts **additional** newlines when
3037        needed. If you want no automatic line wrapping, consider using
3038        :meth:`scrollbox`.
3039
3040        Default values for the size parameters when the
3041        :ref:`autowidgetsize <autowidgetsize>` option is disabled:
3042        ``height=10, width=30``.
3043
3044        Notable exceptions:
3045
3046          any exception raised by :meth:`Dialog._perform`
3047
3048        """
3049        height, width = self._default_size((height, width), (10, 30))
3050        return self._widget_with_no_output(
3051            "msgbox",
3052            ["--msgbox", text, str(height), str(width)],
3053            kwargs)
3054
3055    @widget
3056    @retval_is_code
3057    def pause(self, text, height=None, width=None, seconds=5, **kwargs):
3058        """Display a pause dialog box.
3059
3060        :param str text:    text to display in the box
3061        :param height:      height of the box
3062        :type height:       int or ``None``
3063        :param width:       width of the box
3064        :type width:        int or ``None``
3065        :param int seconds: number of seconds to pause for
3066        :return:
3067          a :term:`Dialog exit code` (which is :attr:`Dialog.OK` if the
3068          widget ended automatically after *seconds* seconds or if the
3069          user pressed the :guilabel:`OK` button)
3070        :rtype:             str
3071
3072        A :meth:`!pause` box displays a text and a meter along the
3073        bottom of the box, during a specified amount of time
3074        (*seconds*). The meter indicates how many seconds remain until
3075        the end of the pause. The widget exits when the specified number
3076        of seconds is elapsed, or immediately if the user presses the
3077        :guilabel:`OK` button, the :guilabel:`Cancel` button or the
3078        :kbd:`Esc` key.
3079
3080        Default values for the size parameters when the
3081        :ref:`autowidgetsize <autowidgetsize>` option is disabled:
3082        ``height=15, width=60``.
3083
3084        Notable exceptions:
3085
3086          any exception raised by :meth:`Dialog._perform`
3087
3088        """
3089        height, width = self._default_size((height, width), (15, 60))
3090        return self._widget_with_no_output(
3091            "pause",
3092            ["--pause", text, str(height), str(width), str(seconds)],
3093            kwargs)
3094
3095    @widget
3096    def passwordbox(self, text, height=None, width=None, init='', **kwargs):
3097        """Display a password input dialog box.
3098
3099        :param str text:  text to display in the box
3100        :param height:    height of the box
3101        :type height:     int or ``None``
3102        :param width:     width of the box
3103        :type width:      int or ``None``
3104        :param str init:  default input password
3105        :return: a tuple of the form :samp:`({code}, {password})` where:
3106
3107          - *code* is a :term:`Dialog exit code`;
3108          - *password* is the password entered by the user.
3109
3110        :rtype: tuple
3111
3112        A :meth:`!passwordbox` is similar to an :meth:`inputbox`, except
3113        that the text the user enters is not displayed. This is useful
3114        when prompting for passwords or other sensitive information. Be
3115        aware that if anything is passed in *init*, it will be visible
3116        in the system's process table to casual snoopers. Also, it is
3117        very confusing to the user to provide them with a default
3118        password they cannot see. For these reasons, using *init* is
3119        highly discouraged.
3120
3121        By default (as in :program:`dialog`), nothing is echoed to the
3122        terminal as the user enters the sensitive text. This can be
3123        confusing to users. Use ``insecure=True`` (keyword argument) if
3124        you want an asterisk to be echoed for each character entered by
3125        the user.
3126
3127        Default values for the size parameters when the
3128        :ref:`autowidgetsize <autowidgetsize>` option is disabled:
3129        ``height=10, width=60``.
3130
3131        Notable exceptions:
3132
3133          any exception raised by :meth:`Dialog._perform`
3134
3135        """
3136        height, width = self._default_size((height, width), (10, 60))
3137        # The help output does not depend on whether --help-status was passed
3138        # (dialog 1.2-20130902).
3139        return self._widget_with_string_output(
3140            ["--passwordbox", text, str(height), str(width), init],
3141            kwargs, strip_xdialog_newline=True, raw_help=True)
3142
3143    def _progressboxoid(self, widget, file_path=None, file_flags=os.O_RDONLY,
3144                        fd=None, text=None, height=20, width=78, **kwargs):
3145        if (file_path is None and fd is None) or \
3146                (file_path is not None and fd is not None):
3147            raise BadPythonDialogUsage(
3148                "{0}.{1}.{2}: either 'file_path' or 'fd' must be provided, and "
3149                "not both at the same time".format(
3150                    __name__, self.__class__.__name__, widget))
3151
3152        with _OSErrorHandling():
3153            if file_path is not None:
3154                if fd is not None:
3155                    raise PythonDialogBug(
3156                        "unexpected non-None value for 'fd': {0!r}".format(fd))
3157                # No need to pass 'mode', as the file is not going to be
3158                # created here.
3159                fd = os.open(file_path, file_flags)
3160
3161            try:
3162                args = [ "--{0}".format(widget) ]
3163                if text is not None:
3164                    args.append(text)
3165                args.extend([str(height), str(width)])
3166
3167                kwargs["redir_child_stdin_from_fd"] = fd
3168                code = self._widget_with_no_output(widget, args, kwargs)
3169            finally:
3170                with _OSErrorHandling():
3171                    if file_path is not None:
3172                        # We open()ed file_path ourselves, let's close it now.
3173                        os.close(fd)
3174
3175        return code
3176
3177    @widget
3178    @retval_is_code
3179    def progressbox(self, file_path=None, file_flags=os.O_RDONLY,
3180                    fd=None, text=None, height=None, width=None, **kwargs):
3181        """
3182        Display a possibly growing stream in a dialog box, as with ``tail -f``.
3183
3184        A file, or more generally a stream that can be read from, must
3185        be specified with either:
3186
3187        :param str file_path: path to the file that is going to be displayed
3188        :param file_flags:
3189          flags used when opening *file_path*; those are passed to
3190          :func:`os.open` (not the built-in :func:`open` function!). By
3191          default, only one flag is set: :data:`os.O_RDONLY`.
3192
3193        or
3194
3195        :param int fd: file descriptor for the stream to be displayed
3196
3197        Remaining parameters:
3198
3199        :param text:   caption continuously displayed at the top, above
3200                       the stream text, or ``None`` to disable the
3201                       caption
3202        :param height: height of the box
3203        :type height:  int or ``None``
3204        :param width:  width of the box
3205        :type width:   int or ``None``
3206        :return:       a :term:`Dialog exit code`
3207        :rtype:        str
3208
3209        Display the contents of the specified file, updating the dialog
3210        box whenever the file grows, as with the ``tail -f`` command.
3211
3212        The file can be specified in two ways:
3213
3214          - either by giving its path (and optionally :func:`os.open`
3215            flags) with parameters *file_path* and *file_flags*;
3216
3217          - or by passing its file descriptor with parameter *fd* (in
3218            which case it may not even be a file; for instance, it could
3219            be an anonymous pipe created with :func:`os.pipe`).
3220
3221        Default values for the size parameters when the
3222        :ref:`autowidgetsize <autowidgetsize>` option is disabled:
3223        ``height=20, width=78``.
3224
3225        Notable exceptions:
3226
3227          - :exc:`PythonDialogOSError` (:exc:`PythonDialogIOError` if
3228            the Python version is < 3.3)
3229          - any exception raised by :meth:`Dialog._perform`
3230
3231        """
3232        height, width = self._default_size((height, width), (20, 78))
3233        return self._progressboxoid(
3234            "progressbox", file_path=file_path, file_flags=file_flags,
3235            fd=fd, text=text, height=height, width=width, **kwargs)
3236
3237    @widget
3238    @retval_is_code
3239    def programbox(self, file_path=None, file_flags=os.O_RDONLY,
3240                   fd=None, text=None, height=None, width=None, **kwargs):
3241        """
3242        Display a possibly growing stream in a dialog box, as with ``tail -f``.
3243
3244        A :meth:`!programbox` is very similar to a :meth:`progressbox`.
3245        The only difference between a :meth:`!programbox` and a
3246        :meth:`!progressbox` is that a :meth:`!programbox` displays an
3247        :guilabel:`OK` button, but only after the input stream has been
3248        exhausted (i.e., *End Of File* has been reached).
3249
3250        This dialog box can be used to display the piped output of an
3251        external program. After the program completes, the user can
3252        press the :kbd:`Enter` key to close the dialog and resume
3253        execution of the calling program.
3254
3255        The parameters and exceptions are the same as for
3256        :meth:`progressbox`. Please refer to the corresponding
3257        documentation.
3258
3259        Default values for the size parameters when the
3260        :ref:`autowidgetsize <autowidgetsize>` option is disabled:
3261        ``height=20, width=78``.
3262
3263        This widget requires :program:`dialog` >= 1.1-20110302.
3264
3265        .. versionadded:: 2.14
3266
3267        """
3268        self._dialog_version_check("1.1-20110302", "the programbox widget")
3269
3270        height, width = self._default_size((height, width), (20, 78))
3271        return self._progressboxoid(
3272            "programbox", file_path=file_path, file_flags=file_flags,
3273            fd=fd, text=text, height=height, width=width, **kwargs)
3274
3275    @widget
3276    def radiolist(self, text, height=None, width=None, list_height=None,
3277                  choices=[], **kwargs):
3278        """Display a radiolist box.
3279
3280        :param str text:    text to display in the box
3281        :param height:      height of the box
3282        :type height:       int or ``None``
3283        :param width:       width of the box
3284        :type width:        int or ``None``
3285        :param list_height: number of entries displayed in the box
3286                            (which can be scrolled) at a given time
3287        :type list_height:  int or ``None``
3288        :param choices:
3289          an iterable of :samp:`({tag}, {item}, {status})` tuples
3290          where *status* specifies the initial selected/unselected
3291          state of each entry; can be ``True`` or ``False``, ``1`` or
3292          ``0``, ``"on"`` or ``"off"`` (``True``, ``1`` and ``"on"``
3293          meaning selected), or any case variation of these two
3294          strings. No more than one entry should be set to ``True``.
3295        :return: a tuple of the form :samp:`({code}, {tag})` where:
3296
3297          - *code* is a :term:`Dialog exit code`;
3298          - *tag* is the tag string corresponding to the entry that was
3299            chosen by the user.
3300
3301        :rtype: tuple
3302
3303        A :meth:`!radiolist` box is similar to a :meth:`menu` box. The
3304        main differences are presentation and that the
3305        :meth:`!radiolist` allows you to indicate which entry is
3306        initially selected, by setting its status to ``True``.
3307
3308        If the user exits with :kbd:`Esc` or :guilabel:`Cancel`, or if
3309        all entries were initially set to ``False`` and not altered
3310        before the user chose :guilabel:`OK`, the returned tag is the
3311        empty string.
3312
3313        Default values for the size parameters when the
3314        :ref:`autowidgetsize <autowidgetsize>` option is disabled:
3315        ``height=15, width=54, list_height=7``.
3316
3317        Notable exceptions:
3318
3319          any exception raised by :meth:`Dialog._perform` or :func:`_to_onoff`
3320
3321        """
3322        height, width, list_height = self._default_size(
3323            (height, width, list_height), (15, 54, 7))
3324
3325        cmd = ["--radiolist", text, str(height), str(width), str(list_height)]
3326        for t in choices:
3327            cmd.extend([ t[0], t[1], _to_onoff(t[2]) ] + list(t[3:]))
3328        (code, output) = self._perform(cmd, **kwargs)
3329
3330        output = self._strip_xdialog_newline(output)
3331
3332        if code == self.HELP:
3333            help_data = self._parse_help(output, kwargs)
3334            if self._help_status_on(kwargs):
3335                help_id, selected_tag = help_data
3336                # Reconstruct 'choices' with the selected item inferred from
3337                # 'selected_tag'.
3338                choices = [ [ tag, item, tag == selected_tag ] + rest for
3339                            (tag, item, status, *rest) in choices ]
3340                return (code, (help_id, selected_tag, choices))
3341            else:
3342                return (code, help_data)
3343        else:
3344            return (code, output)
3345
3346    @widget
3347    def rangebox(self, text, height=0, width=0, min=None, max=None, init=None,
3348                 **kwargs):
3349        """Display a range dialog box.
3350
3351        :param str text:   text to display above the actual range control
3352        :param int height: height of the box
3353        :param int width:  width of the box
3354        :param int min:    minimum value for the range control
3355        :param int max:    maximum value for the range control
3356        :param int init:   initial value for the range control
3357        :return: a tuple of the form :samp:`({code}, {val})` where:
3358
3359          - *code* is a :term:`Dialog exit code`;
3360          - *val* is an integer: the value chosen by the user.
3361
3362        :rtype: tuple
3363
3364        The :meth:`!rangebox` dialog allows the user to select from a
3365        range of integers using a kind of slider. The range control
3366        shows the current value as a bar (like the :ref:`gauge dialog
3367        <gauge-widget>`).
3368
3369        The :kbd:`Tab` and arrow keys move the cursor between the
3370        buttons and the range control. When the cursor is on the latter,
3371        you can change the value with the following keys:
3372
3373        +-----------------------+----------------------------+
3374        |          Key          |           Action           |
3375        +=======================+============================+
3376        | :kbd:`Left` and       | select a digit to modify   |
3377        | :kbd:`Right` arrows   |                            |
3378        +-----------------------+----------------------------+
3379        | :kbd:`+` / :kbd:`-`   | increment/decrement the    |
3380        |                       | selected digit by one unit |
3381        +-----------------------+----------------------------+
3382        | :kbd:`0`–:kbd:`9`     | set the selected digit to  |
3383        |                       | the given value            |
3384        +-----------------------+----------------------------+
3385
3386        Some keys are also recognized in all cursor positions:
3387
3388        +------------------+--------------------------------------+
3389        |       Key        |                Action                |
3390        +==================+======================================+
3391        | :kbd:`Home` /    | set the value to its minimum or      |
3392        | :kbd:`End`       | maximum                              |
3393        +------------------+--------------------------------------+
3394        | :kbd:`Page Up` / | decrement/increment the value so     |
3395        | :kbd:`Page Down` | that the slider moves by one column  |
3396        +------------------+--------------------------------------+
3397
3398        This widget requires :program:`dialog` >= 1.2-20121230.
3399
3400        Notable exceptions:
3401
3402          any exception raised by :meth:`Dialog._perform`
3403
3404        .. versionadded:: 2.14
3405
3406        """
3407        self._dialog_version_check("1.2-20121230", "the rangebox widget")
3408
3409        for name in ("min", "max", "init"):
3410            if not isinstance(locals()[name], int):
3411                raise BadPythonDialogUsage(
3412                    "{0!r} argument not an int: {1!r}".format(name,
3413                                                              locals()[name]))
3414        (code, output) = self._perform(
3415            ["--rangebox", text] + [ str(i) for i in
3416                                     (height, width, min, max, init) ],
3417            **kwargs)
3418
3419        if code == self.HELP:
3420            help_data = self._parse_help(output, kwargs, raw_format=True)
3421            # The help output does not depend on whether --help-status was
3422            # passed (dialog 1.2-20130902).
3423            return (code, int(help_data))
3424        elif code in (self.OK, self.EXTRA):
3425            return (code, int(output))
3426        else:
3427            return (code, None)
3428
3429    @widget
3430    @retval_is_code
3431    def scrollbox(self, text, height=None, width=None, **kwargs):
3432        """Display a string in a scrollable box, with no line wrapping.
3433
3434        :param str text: string to display in the box
3435        :param height:   height of the box
3436        :type height:    int or ``None``
3437        :param width:    width of the box
3438        :type width:     int or ``None``
3439        :return:         a :term:`Dialog exit code`
3440        :rtype:          str
3441
3442        This method is a layer on top of :meth:`textbox`. The
3443        :meth:`!textbox` widget in :program:`dialog` allows one to
3444        display file contents only. This method can be used to display
3445        any text in a scrollable box. This is simply done by creating a
3446        temporary file, calling :meth:`!textbox` and deleting the
3447        temporary file afterwards.
3448
3449        The text is not automatically wrapped. New lines in the
3450        scrollable box will be placed exactly as in *text*. If you want
3451        automatic line wrapping, you should use the :meth:`msgbox`
3452        widget instead (the :mod:`textwrap` module from the Python
3453        standard library is also worth knowing about).
3454
3455        Default values for the size parameters when the
3456        :ref:`autowidgetsize <autowidgetsize>` option is disabled:
3457        ``height=20, width=78``.
3458
3459        Notable exceptions:
3460
3461          :exc:`PythonDialogOSError` (:exc:`PythonDialogIOError` if the
3462          Python version is < 3.3)
3463
3464        .. versionchanged:: 3.1
3465           :exc:`UnableToCreateTemporaryDirectory` exception can't be
3466           raised anymore. The equivalent condition now raises
3467           :exc:`PythonDialogOSError`.
3468
3469        """
3470        height, width = self._default_size((height, width), (20, 78))
3471
3472        with _OSErrorHandling():
3473            tmpfile = tempfile.NamedTemporaryFile(
3474                mode="w", prefix="pythondialog.tmp", delete=False)
3475            try:
3476                with tmpfile as f:
3477                    f.write(text)
3478                # The temporary file is now closed. According to the tempfile
3479                # module documentation, this is necessary if we want to be able
3480                # to reopen it reliably regardless of the platform.
3481
3482                # Ask for an empty title unless otherwise specified
3483                if kwargs.get("title", None) is None:
3484                    kwargs["title"] = ""
3485
3486                return self._widget_with_no_output(
3487                    "textbox",
3488                    ["--textbox", tmpfile.name, str(height), str(width)],
3489                    kwargs)
3490            finally:
3491                # The test should always succeed, but I prefer being on the
3492                # safe side.
3493                if os.path.exists(tmpfile.name):
3494                    os.unlink(tmpfile.name)
3495
3496    @widget
3497    @retval_is_code
3498    def tailbox(self, filepath, height=None, width=None, **kwargs):
3499        """Display the contents of a file in a dialog box, as with ``tail -f``.
3500
3501        :param str filepath: path to a file, the contents of which is to
3502                             be displayed in the box
3503        :param height:       height of the box
3504        :type height:        int or ``None``
3505        :param width:        width of the box
3506        :type width:         int or ``None``
3507        :return:             a :term:`Dialog exit code`
3508        :rtype:              str
3509
3510        Display the contents of the file specified with *filepath*,
3511        updating the dialog box whenever the file grows, as with the
3512        ``tail -f`` command.
3513
3514        Default values for the size parameters when the
3515        :ref:`autowidgetsize <autowidgetsize>` option is disabled:
3516        ``height=20, width=60``.
3517
3518        Notable exceptions:
3519
3520          any exception raised by :meth:`Dialog._perform`
3521
3522        """
3523        height, width = self._default_size((height, width), (20, 60))
3524        return self._widget_with_no_output(
3525            "tailbox",
3526            ["--tailbox", filepath, str(height), str(width)],
3527            kwargs)
3528    # No tailboxbg widget, at least for now.
3529
3530    @widget
3531    @retval_is_code
3532    def textbox(self, filepath, height=None, width=None, **kwargs):
3533        """Display the contents of a file in a dialog box.
3534
3535        :param str filepath: path to a file, the contents of which is to
3536                             be displayed in the box
3537        :param height:       height of the box
3538        :type height:        int or ``None``
3539        :param width:        width of the box
3540        :type width:         int or ``None``
3541        :return:             a :term:`Dialog exit code`
3542        :rtype:              str
3543
3544        A :meth:`!textbox` lets you display the contents of a text file
3545        in a dialog box. It is like a simple text file viewer. The user
3546        can move through the file using the :kbd:`Up` and :kbd:`Down`
3547        arrow keys, :kbd:`Page Up` and :kbd:`Page Down` as well as the
3548        :kbd:`Home` and :kbd:`End` keys available on most keyboards. If
3549        the lines are too long to be displayed in the box, the
3550        :kbd:`Left` and :kbd:`Right` arrow keys can be used to scroll
3551        the text region horizontally. For more convenience, forward and
3552        backward search functions are also provided.
3553
3554        Default values for the size parameters when the
3555        :ref:`autowidgetsize <autowidgetsize>` option is disabled:
3556        ``height=20, width=60``.
3557
3558        Notable exceptions:
3559
3560          any exception raised by :meth:`Dialog._perform`
3561
3562        """
3563        height, width = self._default_size((height, width), (20, 60))
3564        # This is for backward compatibility... not that it is
3565        # stupid, but I prefer explicit programming.
3566        if kwargs.get("title", None) is None:
3567            kwargs["title"] = filepath
3568
3569        return self._widget_with_no_output(
3570            "textbox",
3571            ["--textbox", filepath, str(height), str(width)],
3572            kwargs)
3573
3574    def _timebox_parse_time(self, time_str):
3575        try:
3576            mo = _timebox_time_cre.match(time_str)
3577        except re.error as e:
3578            raise PythonDialogReModuleError(str(e)) from e
3579
3580        if not mo:
3581            raise UnexpectedDialogOutput(
3582                "the dialog-like program returned the following "
3583                "unexpected output (a time string was expected) with the "
3584                "--timebox option: {0!r}".format(time_str))
3585
3586        return [ int(s) for s in mo.group("hour", "minute", "second") ]
3587
3588    @widget
3589    def timebox(self, text, height=None, width=None, hour=-1, minute=-1,
3590                second=-1, **kwargs):
3591        """Display a time dialog box.
3592
3593        :param str text:   text to display in the box
3594        :param height:     height of the box
3595        :type height:      int or ``None``
3596        :param int width:  width of the box
3597        :type width:       int or ``None``
3598        :param int hour:   inititial hour selected
3599        :param int minute: inititial minute selected
3600        :param int second: inititial second selected
3601        :return: a tuple of the form :samp:`({code}, {time})` where:
3602
3603          - *code* is a :term:`Dialog exit code`;
3604          - *time* is a list of the form :samp:`[{hour}, {minute},
3605            {second}]`, where *hour*, *minute* and *second* are integers
3606            corresponding to the time chosen by the user.
3607
3608        :rtype: tuple
3609
3610        :meth:`timebox` is a dialog box which allows one to select an
3611        hour, minute and second. If any of the values for *hour*,
3612        *minute* and *second* is negative, the current time's
3613        corresponding value is used. You can increment or decrement any
3614        of those using the :kbd:`Left`, :kbd:`Up`, :kbd:`Right` and
3615        :kbd:`Down` arrows. Use :kbd:`Tab` or :kbd:`Backtab` to move
3616        between windows.
3617
3618        Default values for the size parameters when the
3619        :ref:`autowidgetsize <autowidgetsize>` option is disabled:
3620        ``height=3, width=30``.
3621
3622        Notable exceptions:
3623
3624          - any exception raised by :meth:`Dialog._perform`
3625          - :exc:`PythonDialogReModuleError`
3626          - :exc:`UnexpectedDialogOutput`
3627
3628        """
3629        height, width = self._default_size((height, width), (3, 30))
3630        (code, output) = self._perform(
3631            ["--timebox", text, str(height), str(width),
3632               str(hour), str(minute), str(second)],
3633            **kwargs)
3634
3635        if code == self.HELP:
3636            help_data = self._parse_help(output, kwargs, raw_format=True)
3637            # The help output does not depend on whether --help-status was
3638            # passed (dialog 1.2-20130902).
3639            return (code, self._timebox_parse_time(help_data))
3640        elif code in (self.OK, self.EXTRA):
3641            return (code, self._timebox_parse_time(output))
3642        else:
3643            return (code, None)
3644
3645    @widget
3646    def treeview(self, text, height=0, width=0, list_height=0,
3647                 nodes=[], **kwargs):
3648        """Display a treeview box.
3649
3650        :param str text:        text to display at the top of the box
3651        :param int height:      height of the box
3652        :param int width:       width of the box
3653        :param int list_height:
3654          number of lines reserved for the main part of the box,
3655          where the tree is displayed
3656        :param nodes:
3657          an iterable of :samp:`({tag}, {item}, {status}, {depth})` tuples
3658          describing nodes, where:
3659
3660            - *tag* is used to indicate which node was selected by
3661              the user on exit;
3662            - *item* is the text displayed for the node;
3663            - *status* specifies the initial selected/unselected
3664              state of each entry; can be ``True`` or ``False``,
3665              ``1`` or ``0``, ``"on"`` or ``"off"`` (``True``, ``1``
3666              and ``"on"`` meaning selected), or any case variation
3667              of these two strings;
3668            - *depth* is a non-negative integer indicating the depth
3669              of the node in the tree (``0`` for the root node).
3670
3671        :return: a tuple of the form :samp:`({code}, {tag})` where:
3672
3673          - *code* is a :term:`Dialog exit code`;
3674          - *tag* is the tag of the selected node.
3675
3676        Display nodes organized in a tree structure. Each node has a
3677        *tag*, an *item* text, a selected *status*, and a *depth* in
3678        the tree. Only the *item* texts are displayed in the widget;
3679        *tag*\s are only used for the return value. Only one node can
3680        be selected at a given time, as for the :meth:`radiolist`
3681        widget.
3682
3683        This widget requires :program:`dialog` >= 1.2-20121230.
3684
3685        Notable exceptions:
3686
3687          any exception raised by :meth:`Dialog._perform` or :func:`_to_onoff`
3688
3689        .. versionadded:: 2.14
3690
3691        """
3692        self._dialog_version_check("1.2-20121230", "the treeview widget")
3693        cmd = ["--treeview", text, str(height), str(width), str(list_height)]
3694
3695        nselected = 0
3696        for i, t in enumerate(nodes):
3697            if not isinstance(t[3], int):
3698                raise BadPythonDialogUsage(
3699                    "fourth element of node {0} not an int: {1!r}".format(
3700                        i, t[3]))
3701
3702            status = _to_onoff(t[2])
3703            if status == "on":
3704                nselected += 1
3705
3706            cmd.extend([ t[0], t[1], status, str(t[3]) ] + list(t[4:]))
3707
3708        if nselected != 1:
3709            raise BadPythonDialogUsage(
3710                "exactly one node must be selected, not {0}".format(nselected))
3711
3712        (code, output) = self._perform(cmd, **kwargs)
3713
3714        if code == self.HELP:
3715            help_data = self._parse_help(output, kwargs)
3716            if self._help_status_on(kwargs):
3717                help_id, selected_tag = help_data
3718                # Reconstruct 'nodes' with the selected item inferred from
3719                # 'selected_tag'.
3720                nodes = [ [ tag, item, tag == selected_tag ] + rest for
3721                          (tag, item, status, *rest) in nodes ]
3722                return (code, (help_id, selected_tag, nodes))
3723            else:
3724                return (code, help_data)
3725        elif code in (self.OK, self.EXTRA):
3726            return (code, output)
3727        else:
3728            return (code, None)
3729
3730    @widget
3731    @retval_is_code
3732    def yesno(self, text, height=None, width=None, **kwargs):
3733        """Display a yes/no dialog box.
3734
3735        :param str text: text to display in the box
3736        :param height:   height of the box
3737        :type height:    int or ``None``
3738        :param width:    width of the box
3739        :type width:     int or ``None``
3740        :return:         a :term:`Dialog exit code`
3741        :rtype:          str
3742
3743        Display a dialog box containing *text* and two buttons labelled
3744        :guilabel:`Yes` and :guilabel:`No` by default.
3745
3746        The box size is *height* rows by *width* columns. If *text* is
3747        too long to fit in one line, it will be automatically divided
3748        into multiple lines at appropriate places. *text* may also
3749        contain the substring ``"\\n"`` or newline characters to control
3750        line breaking explicitly.
3751
3752        This :meth:`!yesno` dialog box is useful for asking questions
3753        that require the user to answer either "yes" or "no". These are
3754        the default button labels, however they can be freely set with
3755        the ``yes_label`` and ``no_label`` keyword arguments. The user
3756        can switch between the buttons by pressing the :kbd:`Tab` key.
3757
3758        Default values for the size parameters when the
3759        :ref:`autowidgetsize <autowidgetsize>` option is disabled:
3760        ``height=10, width=30``.
3761
3762        Notable exceptions:
3763
3764          any exception raised by :meth:`Dialog._perform`
3765
3766        """
3767        height, width = self._default_size((height, width), (10, 30))
3768        return self._widget_with_no_output(
3769            "yesno",
3770            ["--yesno", text, str(height), str(width)],
3771            kwargs)
3772