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