1#! /usr/bin/env python3
2# -*- coding: utf-8 -*-
3
4# demo.py --- Demonstration program and cheap test suite for pythondialog
5#
6# Copyright (C) 2000  Robb Shecter, Sultanbek Tezadov
7# Copyright (C) 2002-2019  Florent Rougon
8#
9# This program is in the public domain.
10
11"""Demonstration program for pythondialog.
12
13This is a program demonstrating most of the possibilities offered by
14the pythondialog module (which is itself a Python interface to the
15well-known dialog utility, or any other program compatible with
16dialog).
17
18Executive summary
19-----------------
20
21If you are looking for a very simple example of pythondialog usage,
22short and straightforward, please refer to simple_example.py. The
23file you are now reading serves more as a demonstration of what can
24be done with pythondialog and as a cheap test suite than as a first
25time tutorial. However, it can also be used to learn how to invoke
26the various widgets. The following paragraphs explain what you should
27keep in mind if you read it for this purpose.
28
29
30Most of the code in the MyApp class (which defines the actual
31contents of the demo) relies on a class called MyDialog implemented
32here that:
33
34  1. wraps all widget-producing calls in a way that automatically
35     spawns a "confirm quit" dialog box if the user presses the
36     Escape key or chooses the Cancel button, and then redisplays the
37     original widget if the user doesn't actually want to quit;
38
39  2. provides a few additional dialog-related methods and convenience
40     wrappers.
41
42The handling in (1) is completely automatic, implemented with
43MyDialog.__getattr__() returning decorated versions of the
44widget-producing methods of dialog.Dialog. Therefore, most of the
45demo can be read as if the module-level 'd' attribute were a
46dialog.Dialog instance whereas it is actually a MyDialog instance.
47The only meaningful difference is that MyDialog.<widget>() will never
48return a CANCEL or ESC code (attributes of 'd', or more generally of
49dialog.Dialog). The reason is that these return codes are
50automatically handled by the MyDialog.__getattr__() machinery to
51display the "confirm quit" dialog box.
52
53In some cases (e.g., fselect_demo()), I wanted the "Cancel" button to
54perform a specific action instead of spawning the "confirm quit"
55dialog box. To achieve this, the widget is invoked using
56dialog.Dialog.<widget> instead of MyDialog.<widget>, and the return
57code is handled in a semi-manual way. A prominent feature that needs
58such special-casing is the yesno widget, because the "No" button
59corresponds to the CANCEL exit code, which in general must not be
60interpreted as an attempt to quit the program!
61
62To sum it up, you can read most of the code in the MyApp class (which
63defines the actual contents of the demo) as if 'd' were a
64dialog.Dialog instance. Just keep in mind that there is a little
65magic behind the scenes that automatically handles the CANCEL and ESC
66Dialog exit codes, which wouldn't be the case if 'd' were a
67dialog.Dialog instance. For a first introduction to pythondialog with
68simple stuff and absolutely no magic, please have a look at
69simple_example.py.
70
71"""
72
73
74import sys, os, locale, stat, time, getopt, subprocess, traceback, textwrap
75import pprint
76import dialog
77from dialog import DialogBackendVersion
78
79progname = os.path.basename(sys.argv[0])
80progversion = "0.12-autowidgetsize"
81version_blurb = """Demonstration program and cheap test suite for pythondialog.
82
83Copyright (C) 2002-2010, 2013-2016  Florent Rougon
84Copyright (C) 2000  Robb Shecter, Sultanbek Tezadov
85
86This is free software; see the source for copying conditions.  There is NO
87warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE."""
88
89default_debug_filename = "pythondialog.debug"
90
91usage = """Usage: {progname} [option ...]
92Demonstration program and cheap test suite for pythondialog.
93
94Options:
95  -t, --test-suite             test all widgets; implies '--fast'
96  -f, --fast                   fast mode (e.g., makes the gauge demo run faster)
97      --debug                  enable logging of all dialog command lines
98      --debug-file=FILE        where to write debug information (default:
99                               {debug_file} in the current directory)
100  -E, --debug-expand-file-opt  expand the '--file' options in the debug file
101                               generated by '--debug'
102      --help                   display this message and exit
103      --version                output version information and exit""".format(
104    progname=progname, debug_file=default_debug_filename)
105
106# Global parameters
107params = {}
108
109# We'll use a module-level attribute 'd' ("global") to store the MyDialog
110# instance that is used throughout the demo. This object could alternatively be
111# passed to the MyApp constructor and stored there as a class or instance
112# attribute. However, for the sake of readability, we'll simply use a global
113# (d.msgbox(...) versus self.d.msgbox(...), etc.).
114d = None
115
116tw = textwrap.TextWrapper(width=78, break_long_words=False,
117                          break_on_hyphens=True)
118from textwrap import dedent
119
120try:
121    from textwrap import indent
122except ImportError:
123    try:
124        callable                # Normally, should be __builtins__.callable
125    except NameError:
126        # Python 3.1 doesn't have the 'callable' builtin function. Let's
127        # provide ours.
128        def callable(f):
129            return hasattr(f, '__call__')
130
131    def indent(text, prefix, predicate=None):
132        l = []
133
134        for line in text.splitlines(True):
135            if (callable(predicate) and predicate(line)) \
136                    or (not callable(predicate) and predicate) \
137                    or (predicate is None and line.strip()):
138                line = prefix + line
139            l.append(line)
140
141        return ''.join(l)
142
143
144class MyDialog:
145    """Wrapper class for dialog.Dialog.
146
147    This class behaves similarly to dialog.Dialog. The differences
148    are that:
149
150      1. MyDialog wraps all widget-producing methods in a way that
151         automatically spawns a "confirm quit" dialog box if the user
152         presses the Escape key or chooses the Cancel button, and
153         then redisplays the original widget if the user doesn't
154         actually want to quit.
155
156      2. MyDialog provides a few additional dialog-related methods
157         and convenience wrappers.
158
159    Please refer to the module docstring and to the particular
160    methods for more details.
161
162    """
163    def __init__(self, Dialog_instance):
164        self.dlg = Dialog_instance
165
166    def check_exit_request(self, code, ignore_Cancel=False):
167        if code == self.CANCEL and ignore_Cancel:
168            # Ignore the Cancel button, i.e., don't interpret it as an exit
169            # request; instead, let the caller handle CANCEL himself.
170            return True
171
172        if code in (self.CANCEL, self.ESC):
173            button_name = { self.CANCEL: "Cancel",
174                            self.ESC: "Escape" }
175            msg = "You pressed {0} in the last dialog box. Do you want " \
176                "to exit this demo?".format(button_name[code])
177            # 'self.dlg' instead of 'self' here, because we want to use the
178            # original yesno() method from the Dialog class instead of the
179            # decorated method returned by self.__getattr__().
180            if self.dlg.yesno(msg) == self.OK:
181                sys.exit(0)
182            else:               # "No" button chosen, or ESC pressed
183                return False    # in the "confirm quit" dialog
184        else:
185            return True
186
187    def widget_loop(self, method):
188        """Decorator to handle eventual exit requests from a Dialog widget.
189
190        method -- a dialog.Dialog method that returns either a Dialog
191                  exit code, or a sequence whose first element is a
192                  Dialog exit code (cf. the docstring of the Dialog
193                  class in dialog.py)
194
195        Return a wrapper function that behaves exactly like 'method',
196        except for the following point:
197
198          If the Dialog exit code obtained from 'method' is CANCEL or
199          ESC (attributes of dialog.Dialog), a "confirm quit" dialog
200          is spawned; depending on the user choice, either the
201          program exits or 'method' is called again, with the same
202          arguments and same handling of the exit status. In other
203          words, the wrapper function builds a loop around 'method'.
204
205        The above condition on 'method' is satisfied for all
206        dialog.Dialog widget-producing methods. More formally, these
207        are the methods defined with the @widget decorator in
208        dialog.py, i.e., that have an "is_widget" attribute set to
209        True.
210
211        """
212        # One might want to use @functools.wraps here, but since the wrapper
213        # function is very likely to be used only once and then
214        # garbage-collected, this would uselessly add a little overhead inside
215        # __getattr__(), where widget_loop() is called.
216        def wrapper(*args, **kwargs):
217            while True:
218                res = method(*args, **kwargs)
219
220                if hasattr(method, "retval_is_code") \
221                        and getattr(method, "retval_is_code"):
222                    code = res
223                else:
224                    code = res[0]
225
226                if self.check_exit_request(code):
227                    break
228            return res
229
230        return wrapper
231
232    def __getattr__(self, name):
233        # This is where the "magic" of this class originates from.
234        # Please refer to the module and self.widget_loop()
235        # docstrings if you want to understand the why and the how.
236        obj = getattr(self.dlg, name)
237        if hasattr(obj, "is_widget") and getattr(obj, "is_widget"):
238            return self.widget_loop(obj)
239        else:
240            return obj
241
242    def clear_screen(self):
243        # This program comes with ncurses
244        program = "clear"
245
246        try:
247            p = subprocess.Popen([program], shell=False, stdout=None,
248                                 stderr=None, close_fds=True)
249            retcode = p.wait()
250        except os.error as e:
251            self.msgbox("Unable to execute program '%s': %s." % (program,
252                                                              e.strerror),
253                     title="Error")
254            return False
255
256        if retcode > 0:
257            msg = "Program %s returned exit status %d." % (program, retcode)
258        elif retcode < 0:
259            msg = "Program %s was terminated by signal %d." % (program, -retcode)
260        else:
261            return True
262
263        self.msgbox(msg)
264        return False
265
266    def _Yesno(self, *args, **kwargs):
267        """Convenience wrapper around dialog.Dialog.yesno().
268
269        Return the same exit code as would return
270        dialog.Dialog.yesno(), except for ESC which is handled as in
271        the rest of the demo, i.e. make it spawn the "confirm quit"
272        dialog.
273
274        """
275        # self.yesno() automatically spawns the "confirm quit" dialog if ESC or
276        # the "No" button is pressed, because of self.__getattr__(). Therefore,
277        # we have to use self.dlg.yesno() here and call
278        # self.check_exit_request() manually.
279        while True:
280            code = self.dlg.yesno(*args, **kwargs)
281            # If code == self.CANCEL, it means the "No" button was chosen;
282            # don't interpret this as a wish to quit the program!
283            if self.check_exit_request(code, ignore_Cancel=True):
284                break
285
286        return code
287
288    def Yesno(self, *args, **kwargs):
289        """Convenience wrapper around dialog.Dialog.yesno().
290
291        Return True if "Yes" was chosen, False if "No" was chosen,
292        and handle ESC as in the rest of the demo, i.e. make it spawn
293        the "confirm quit" dialog.
294
295        """
296        return self._Yesno(*args, **kwargs) == self.dlg.OK
297
298    def Yesnohelp(self, *args, **kwargs):
299        """Convenience wrapper around dialog.Dialog.yesno().
300
301        Return "yes", "no", "extra" or "help" depending on the button
302        that was pressed to close the dialog. ESC is handled as in
303        the rest of the demo, i.e. it spawns the "confirm quit"
304        dialog.
305
306        """
307        kwargs["help_button"] = True
308        code = self._Yesno(*args, **kwargs)
309        d = { self.dlg.OK:     "yes",
310              self.dlg.CANCEL: "no",
311              self.dlg.EXTRA:  "extra",
312              self.dlg.HELP:   "help" }
313
314        return d[code]
315
316
317# Dummy context manager to make sure the debug file is closed on exit, be it
318# normal or abnormal, and to avoid having two code paths, one for normal mode
319# and one for debug mode.
320class DummyContextManager:
321    def __enter__(self):
322        return self
323
324    def __exit__(self, *exc):
325        return False
326
327
328class MyApp:
329    def __init__(self):
330        # The MyDialog instance 'd' could be passed via the constructor and
331        # stored here as a class or instance attribute. However, for the sake
332        # of readability, we'll simply use a module-level attribute ("global")
333        # (d.msgbox(...) versus self.d.msgbox(...), etc.).
334        global d
335        # If you want to use Xdialog (pathnames are also OK for the 'dialog'
336        # argument), you can use:
337        #   dialog.Dialog(dialog="Xdialog", compat="Xdialog", ...)
338        #
339        # With the 'autowidgetsize' option enabled, pythondialog's
340        # widget-producing methods behave as if width=0, height=0, etc. had
341        # been passed, except where these parameters are explicitely specified
342        # with different values.
343        self.Dialog_instance = dialog.Dialog(dialog="cdialog",
344                                             autowidgetsize=True)
345        # See the module docstring at the top of the file to understand the
346        # purpose of MyDialog.
347        d = MyDialog(self.Dialog_instance)
348        backtitle = "pythondialog demo"
349        d.set_background_title(backtitle)
350        # These variables take the background title into account
351        self.max_lines, self.max_cols = d.maxsize(backtitle=backtitle)
352        self.demo_context = self.setup_debug()
353        # Warn if the terminal is smaller than this size
354        self.min_rows, self.min_cols = 24, 80
355        self.term_rows, self.term_cols, self.backend_version = \
356            self.get_term_size_and_backend_version()
357
358        # With Python strings, we don't need dialog to change '\n' to a newline
359        # char: tell the dialog program to use the strings as we prepared them.
360        # The same technique can be used to pass other options to dialog(1).
361        d.add_persistent_args(["--no-nl-expand"])
362
363    def setup_debug(self):
364        if params["debug"]:
365            debug_file = open(params["debug_filename"], "w")
366            d.setup_debug(True, file=debug_file,
367                          expand_file_opt=params["debug_expand_file_opt"])
368            return debug_file
369        else:
370            return DummyContextManager()
371
372    def get_term_size_and_backend_version(self):
373        # Avoid running '<backend> --print-version' every time we need the
374        # version
375        backend_version = d.cached_backend_version
376        if not backend_version:
377            print(tw.fill(
378                  "Unable to retrieve the version of the dialog-like backend. "
379                  "Not running cdialog?") + "\nPress Enter to continue.",
380                  file=sys.stderr)
381            input()
382
383        term_rows, term_cols = d.maxsize(use_persistent_args=False)
384        if term_rows < self.min_rows or term_cols < self.min_cols:
385            print(tw.fill(dedent("""\
386             Your terminal has less than {0} rows or less than {1} columns;
387             you may experience problems with the demo. You have been warned."""
388                                 .format(self.min_rows, self.min_cols)))
389                  + "\nPress Enter to continue.")
390            input()
391
392        return (term_rows, term_cols, backend_version)
393
394    def run(self):
395        with self.demo_context:
396            if params["testsuite_mode"]:
397                # Show the additional widgets before the "normal demo", so that
398                # I can test new widgets quickly and simply hit Ctrl-C once
399                # they've been shown.
400                self.additional_widgets()
401
402            # "Normal" demo
403            self.demo()
404
405    def demo(self):
406        d.msgbox("""\
407Hello, and welcome to the pythondialog {pydlg_version} demonstration program.
408
409You can scroll through this dialog box with the Page Up and Page Down keys. \
410Please note that some of the dialogs will not work, and cause the demo to \
411stop, if your terminal is too small. The recommended size is (at least) \
412{min_rows} rows by {min_cols} columns.
413
414This script is being run by a Python interpreter identified as follows:
415
416{py_version}
417
418The dialog-like program displaying this message box reports version \
419{backend_version} and a terminal size of {rows} rows by {cols} columns."""
420                 .format(
421                pydlg_version=dialog.__version__,
422                backend_version=self.backend_version,
423                py_version=indent(sys.version, "  "),
424                rows=self.term_rows, cols=self.term_cols,
425                min_rows=self.min_rows, min_cols=self.min_cols),
426                 width=60, height=17)
427
428        self.progressbox_demo_with_file_descriptor()
429        # First dialog version where the programbox widget works fine
430        if self.dialog_version_check("1.2-20140112"):
431            self.programbox_demo()
432        self.infobox_demo()
433        self.gauge_demo()
434        answer = self.yesno_demo(with_help=True)
435        self.msgbox_demo(answer)
436        self.textbox_demo()
437        self.timeout_demo()
438        name = self.inputbox_demo_with_help()
439        size, weight, city, state, country, last_will1, last_will2, \
440            last_will3, last_will4, secret_code = self.mixedform_demo()
441        self.form_demo_with_help()
442        favorite_day = self.menu_demo(name, city, state, country, size, weight,
443                                      secret_code, last_will1, last_will2,
444                                      last_will3, last_will4)
445
446        if self.dialog_version_check("1.2-20130902",
447                                     "the menu demo with help facilities",
448                                     explain=True):
449            self.menu_demo_with_help()
450
451        toppings = self.checklist_demo()
452        if self.dialog_version_check("1.2-20130902",
453                                     "the checklist demo with help facilities",
454                                     explain=True):
455            self.checklist_demo_with_help()
456
457        sandwich = self.radiolist_demo()
458
459        if self.dialog_version_check("1.2-20121230", "the rangebox demo", explain=True):
460            nb_engineers = self.rangebox_demo()
461        else:
462            nb_engineers = None
463
464        if self.dialog_version_check("1.2-20121230", "the buildlist demo", explain=True):
465            desert_island_stuff = self.buildlist_demo()
466        else:
467            desert_island_stuff = None
468
469        if self.dialog_version_check("1.2-20130902",
470                                     "the buildlist demo with help facilities",
471                                     explain=True):
472            self.buildlist_demo_with_help()
473
474        date = self.calendar_demo_with_help()
475        time_ = self.timebox_demo()
476
477        password = self.passwordbox_demo()
478        self.scrollbox_demo(name, favorite_day, toppings, sandwich,
479                            nb_engineers, desert_island_stuff, date, time_,
480                            password)
481
482        if self.dialog_version_check("1.2-20121230", "the treeview demo",
483                                     explain=True):
484            if self.dialog_version_check("1.2-20130902"):
485                self.treeview_demo_with_help()
486            else:
487                self.treeview_demo()
488
489        self.mixedgauge_demo()
490        self.editbox_demo("/etc/passwd")
491        self.inputmenu_demo()
492        d.msgbox("""\
493Haha. You thought it was over. Wrong. Even more fun is to come!
494
495Now, please select a file you would like to see growing (or not...).""",
496                 width=75, height=8)
497
498        # Looks nicer if the screen is not completely filled by the widget,
499        # hence the -1.
500        self.tailbox_demo(height=self.max_lines-1,
501                          width=self.max_cols)
502
503        directory = self.dselect_demo()
504
505        timeout = 2 if params["fast_mode"] else 20
506        self.pause_demo(timeout)
507
508        d.clear_screen()
509        if not params["fast_mode"]:
510            # Rest assured, this is not necessary in any way: it is only a
511            # psychological trick to try to give the impression of a reboot
512            # (cf. pause_demo(); would be even nicer with a "visual bell")...
513            time.sleep(1)
514
515    def additional_widgets(self):
516        # Requires a careful choice of the file to be of any interest
517        self.progressbox_demo_with_filepath()
518        # This can be confusing without any pause if the user specified a
519        # regular file.
520        time.sleep(1 if params["fast_mode"] else 2)
521
522        # programbox_demo is fine right after
523        # progressbox_demo_with_file_descriptor in demo(), but there was a
524        # little bug in dialog that made the first two lines disappear too
525        # early. This bug has been fixed in version 1.2-20140112, therefore
526        # we'll run the programbox_demo as part of the main demo if the dialog
527        # version is >= than this one, otherwise we'll keep it here.
528        if self.dialog_version_check("1.1-20110302", "the programbox demo",
529                                     explain=True):
530            # First dialog version where the programbox widget works fine
531            if not self.dialog_version_check("1.2-20140112"):
532                self.programbox_demo()
533        # Almost identical to mixedform (mixedform being more powerful). Also,
534        # there is now form_demo_with_help() which uses the form widget.
535        self.form_demo()
536        # Almost identical to passwordbox
537        self.passwordform_demo()
538
539    def dialog_version_check(self, version_string, feature="", *, start="",
540                             explain=False):
541        if d.compat != "dialog":
542            # non-dialog implementations are not affected by
543            # 'dialog_version_check'.
544            return True
545
546        minimum_version = DialogBackendVersion.fromstring(version_string)
547        res = (d.cached_backend_version >= minimum_version)
548
549        if explain and not res:
550            self.too_old_dialog_version(feature=feature, start=start,
551                                        min=version_string)
552
553        return res
554
555    def too_old_dialog_version(self, feature="", *, start="", min=None):
556        assert (feature and not start) or (not feature and start), \
557            (feature, start)
558        if not start:
559            start = "Skipping {0},".format(feature)
560
561        d.msgbox(
562            "{start} because it requires dialog {min} or later; "
563            "however, it appears that you are using version {used}.".format(
564                start=start, min=min, used=d.cached_backend_version),
565            width=60, height=9, title="Demo skipped")
566
567    def progressbox_demo_with_filepath(self):
568        widget = "progressbox"
569
570        # First, ask the user for a file (possibly FIFO)
571        d.msgbox(self.FIFO_HELP(widget), width=72, height=20)
572        path = self.fselect_demo(widget, allow_FIFOs=True,
573                                 title="Please choose a file to be shown as "
574                                 "with 'tail -f'")
575        if path is None:
576            # User chose to abort
577            return
578        else:
579            d.progressbox(file_path=path, width=78, height=20,
580                          text="You can put some header text here",
581                          title="Progressbox example with a file path")
582
583    def progressboxoid(self, widget, func_name, text, **kwargs):
584        # Since this is just a demo, I will not try to catch os.error exceptions
585        # in this function, for the sake of readability.
586        read_fd, write_fd = os.pipe()
587
588        child_pid = os.fork()
589        if child_pid == 0:
590            try:
591                # We are in the child process. We MUST NOT raise any exception.
592                # No need for this one in the child process
593                os.close(read_fd)
594
595                # Python file objects are easier to use than file descriptors.
596                # For a start, you don't have to check the number of bytes
597                # actually written every time...
598                # "buffering = 1" means wfile is going to be line-buffered
599                with os.fdopen(write_fd, mode="w", buffering=1) as wfile:
600                    for line in text.split('\n'):
601                        wfile.write(line + '\n')
602                        time.sleep(0.02 if params["fast_mode"] else 1.2)
603
604                os._exit(0)
605            except:
606                os._exit(127)
607
608        # We are in the father process. No need for write_fd anymore.
609        os.close(write_fd)
610        # Call d.progressbox() if widget == "progressbox"
611        #      d.programbox() if widget == "programbox"
612        # etc.
613        getattr(d, widget)(
614            fd=read_fd,
615            title="{0} example with a file descriptor".format(widget),
616            **kwargs)
617
618        # Now that the progressbox is over (second child process, running the
619        # dialog-like program), we can wait() for the first child process.
620        # Otherwise, we could have a deadlock in case the pipe gets full, since
621        # dialog wouldn't be reading it.
622        exit_info = os.waitpid(child_pid, 0)[1]
623        if os.WIFEXITED(exit_info):
624            exit_code = os.WEXITSTATUS(exit_info)
625        elif os.WIFSIGNALED(exit_info):
626            d.msgbox("%s(): first child process terminated by signal %d" %
627                     (func_name, os.WTERMSIG(exit_info)))
628        else:
629            assert False, "How the hell did we manage to get here?"
630
631        if exit_code != 0:
632            d.msgbox("%s(): first child process ended with exit status %d"
633                     % (func_name, exit_code))
634
635    def progressbox_demo_with_file_descriptor(self):
636        func_name = "progressbox_demo_with_file_descriptor"
637        text = """\
638A long time ago in a galaxy far,
639far away...
640
641
642
643
644
645A NEW HOPE
646
647It was a period of intense
648sucking. Graphical toolkits for
649Python were all nice and clean,
650but they were, well, graphical.
651And as every one knows, REAL
652PROGRAMMERS ALWAYS WORK ON VT-100
653TERMINALS. In text mode.
654
655Besides, those graphical toolkits
656were usually too complex for
657simple programs, so most FLOSS
658geeks ended up writing
659command-line tools except when
660they really needed the full power
661of mainstream graphical toolkits,
662such as Qt, GTK+ and wxWidgets.
663
664But... thanks to people like
665Thomas E. Dickey, there are now
666at our disposal several free
667software command-line programs,
668such as dialog, that allow easy
669building of graphically-oriented
670interfaces in text-mode
671terminals. These are good for
672tasks where line-oriented
673interfaces are not well suited,
674as well as for the increasingly
675common type who runs away as soon
676as he sees something remotely
677resembling a command line.
678
679But this is not for Python! I want
680my poney!
681
682Seeing this unacceptable
683situation, Robb Shecter had the
684idea, back in the olden days of
685Y2K (when the world was supposed
686to suddenly collapse, remember?),
687to wrap a dialog interface into a
688Python module called dialog.py.
689
690pythondialog was born. Florent
691Rougon, who was looking for
692something like that in 2002,
693found the idea rather cool and
694improved the module during the
695following years...""" + 15*'\n'
696
697        return self.progressboxoid("progressbox", func_name, text,
698                                   width=78, height=20)
699
700    def programbox_demo(self):
701        func_name = "programbox_demo"
702        text = """\
703The 'progressbox' widget
704has a little brother
705called 'programbox'
706that displays text
707read from a pipe
708and only adds an OK button
709when the pipe indicates EOF
710(End Of File).
711
712This can be used
713to display the output
714of some external program.
715
716This will be done right away if you choose "Yes" in the next dialog.
717This choice will cause 'find /usr/bin' to be run with subprocess.Popen()
718and the output to be displayed, via a pipe, in a 'programbox' widget."""
719        self.progressboxoid("programbox", func_name, text, width=78, height=20)
720
721        if d.Yesno("Do you want to run 'find /usr/bin' in a programbox widget?"):
722            try:
723                devnull = subprocess.DEVNULL
724            except AttributeError: # Python < 3.3
725                devnull_context = devnull = open(os.devnull, "wb")
726            else:
727                devnull_context = DummyContextManager()
728
729            args = ["find", "/usr/bin"]
730            with devnull_context:
731                p = subprocess.Popen(args, stdout=subprocess.PIPE,
732                                     stderr=devnull, close_fds=True)
733                # One could use title=... instead of text=... to put the text
734                # in the title bar.
735                d.programbox(fd=p.stdout.fileno(),
736                             text="Example showing the output of a command "
737                             "with programbox", width=78, height=20)
738                retcode = p.wait()
739
740            # Context manager support for subprocess.Popen objects requires
741            # Python 3.2 or later.
742            p.stdout.close()
743            return retcode
744        else:
745            return None
746
747    def infobox_demo(self):
748        d.infobox("One moment, please. Just wasting some time here to "
749                  "show you the infobox...")
750
751        time.sleep(0.5 if params["fast_mode"] else 4.0)
752
753    def gauge_demo(self):
754        d.gauge_start("Progress: 0%", title="Still testing your patience...")
755
756        for i in range(1, 101):
757            if i < 50:
758                d.gauge_update(i, "Progress: {0}%".format(i), update_text=True)
759            elif i == 50:
760                d.gauge_update(i, "Over {0}%. Good.".format(i),
761                               update_text=True)
762            elif i == 80:
763                d.gauge_update(i, "Yeah, this boring crap will be over Really "
764                               "Soon Now.", update_text=True)
765            else:
766                d.gauge_update(i)
767
768            time.sleep(0.01 if params["fast_mode"] else 0.1)
769
770        d.gauge_stop()
771
772    def mixedgauge_demo(self):
773        for i in range(1, 101, 20):
774            d.mixedgauge("This is the 'text' part of the mixedgauge\n"
775                         "and this is a forced new line.",
776                         title="'mixedgauge' demo",
777                         percent=int(round(72+28*i/100)),
778                         elements=[("Task 1", "Foobar"),
779                                   ("Task 2", 0),
780                                   ("Task 3", 1),
781                                   ("Task 4", 2),
782                                   ("Task 5", 3),
783                                   ("", 8),
784                                   ("Task 6", 5),
785                                   ("Task 7", 6),
786                                   ("Task 8", 7),
787                                   ("", ""),
788                                   # 0 is the dialog special code for
789                                   # "Succeeded", so these must not be equal to
790                                   # zero! That is why I made the range() above
791                                   # start at 1.
792                                   ("Task 9", -max(1, 100-i)),
793                                   ("Task 10", -i)])
794            time.sleep(0.5 if params["fast_mode"] else 2)
795
796    def yesno_demo(self, with_help=True):
797        if not with_help:
798            # Simple version, without the "Help" button (the return value is
799            # True or False):
800            return d.Yesno("\nDo you like this demo?", yes_label="Yes, I do",
801                           no_label="No, I do not", height=10, width=40,
802                           title="An Important Question")
803
804        # 'yesno' dialog box with custom Yes, No and Help buttons
805        while True:
806            reply = d.Yesnohelp("\nDo you like this demo?",
807                                yes_label="Yes, I do", no_label="No, I do not",
808                                help_label="Please help me!", height=10,
809                                width=60, title="An Important Question")
810            if reply == "yes":
811                return True
812            elif reply == "no":
813                return False
814            elif reply == "help":
815                d.msgbox("""\
816I can hear your cry for help, and would really like to help you. However, I \
817am afraid there is not much I can do for you here; you will have to decide \
818for yourself on this matter.
819
820Keep in mind that you can always rely on me. \
821You have all my support, be brave!""",
822                         height=15, width=60,
823                         title="From Your Faithful Servant")
824            else:
825                assert False, "Unexpected reply from MyDialog.Yesnohelp(): " \
826                    + repr(reply)
827
828    def msgbox_demo(self, answer):
829        if answer:
830            msg = "Excellent! Press OK to see its source code (or another " \
831            "file if not in the correct directory)."
832        else:
833            msg = "Well, feel free to send your complaints to /dev/null!\n\n" \
834                "Sincerely yours, etc."
835
836        d.msgbox(msg, height=7, width=50)
837
838    def textbox_demo(self):
839        # Better use the absolute path for displaying in the dialog title
840        filepath = os.path.abspath(__file__)
841        code = d.textbox(filepath, width=76,
842                         title="Contents of {0}".format(filepath),
843                         extra_button=True, extra_label="Stop it now!")
844
845        if code == "extra":
846            d.msgbox("Your wish is my command, Master.", width=40,
847                     title="Exiting")
848            sys.exit(0)
849
850    def timeout_demo(self):
851        msg = "If you take more than five seconds to validate this, I'll " \
852              "know. :)"
853        code = d.msgbox(msg, title="Widget with a timeout", timeout=5)
854        if code == d.TIMEOUT:
855            d.msgbox("Timeout expired.")
856        else:
857            d.msgbox("You answered in less than five seconds.")
858
859    def inputbox_demo(self):
860        code, answer = d.inputbox("What's your name?", init="Snow White")
861        return answer
862
863    def inputbox_demo_with_help(self):
864        init_str = "Snow White"
865        while True:
866            code, answer = d.inputbox("What's your name?", init=init_str,
867                                      title="'inputbox' demo", help_button=True)
868
869            if code == "help":
870                d.msgbox("Help from the 'inputbox' demo. The string entered "
871                         "so far is {0!r}.".format(answer),
872                         title="'inputbox' demo")
873                init_str = answer
874            else:
875                break
876
877        return answer
878
879    def form_demo(self):
880        elements = [
881            ("Size (cm)", 1, 1, "175", 1, 20, 4, 3),
882            ("Weight (kg)", 2, 1, "85", 2, 20, 4, 3),
883            ("City", 3, 1, "Groboule-les-Bains", 3, 20, 15, 25),
884            ("State", 4, 1, "Some Lost Place", 4, 20, 15, 25),
885            ("Country", 5, 1, "Nowhereland", 5, 20, 15, 20),
886            ("My", 6, 1, "I hereby declare that, upon leaving this "
887             "world, all", 6, 20, 0, 0),
888            ("Very", 7, 1, "my fortune shall be transferred to Florent "
889             "Rougon's", 7, 20, 0, 0),
890            ("Last", 8, 1, "bank account number 000 4237 4587 32454/78 at "
891             "Banque", 8, 20, 0, 0),
892            ("Will", 9, 1, "Cantonale Vaudoise, Lausanne, Switzerland.",
893             9, 20, 0, 0) ]
894
895        code, fields = d.form("Please fill in some personal information:",
896                              elements, width=77)
897        return fields
898
899    def form_demo_with_help(self, item_help=True):
900        # This function is slightly complex because it provides help support
901        # with 'help_status=True', and optionally also with 'item_help=True'
902        # together with 'help_tags=True'. For a very simple version (without
903        # any help support), see form_demo() above.
904        minver_for_helptags = "1.2-20130902"
905
906        if item_help:
907            if self.dialog_version_check(minver_for_helptags):
908                complement = """'item_help=True' is also used in conjunction \
909with 'help_tags=True' in order to display per-item help at the bottom of the \
910widget."""
911            else:
912                item_help = False
913                complement = """'item_help=True' is not used, because to make \
914it consistent with the 'item_help=False' case, dialog {min} or later is \
915required (for the --help-tags option); however, it appears that you are using \
916version {used}.""".format(min=minver_for_helptags,
917                          used=d.cached_backend_version)
918        else:
919            complement = """'item_help=True' is not used, because it has \
920been disabled; therefore, there is no per-item help at the bottom of the \
921widget."""
922
923        text = """\
924This is a demo for the 'form' widget, which is similar to 'mixedform' but \
925a bit simpler in that it has no notion of field type (to hide contents such \
926as passwords).
927
928This demo uses 'help_button=True' to provide a Help button \
929and 'help_status=True' to allow redisplaying the widget in the same state \
930when leaving the help dialog. {complement}""".format(complement=complement)
931
932        elements = [ ("Fruit",  1, 8, "mirabelle plum",  1, 20, 18, 30),
933                     ("Color",  2, 8, "yellowish",       2, 20, 18, 30),
934                     ("Flavor", 3, 8, "sweet when ripe", 3, 20, 18, 30),
935                     ("Origin", 4, 8, "Lorraine",        4, 20, 18, 30) ]
936
937        more_kwargs = {}
938
939        if item_help:
940            more_kwargs.update({ "item_help": True,
941                                 "help_tags": True })
942            elements = [ list(l) + [ "Help text for item {0}".format(i+1) ]
943                         for i, l in enumerate(elements) ]
944
945        while True:
946            code, t = d.form(text, elements, height=20, width=65,
947                             title="'form' demo with help facilities",
948                             help_button=True, help_status=True, **more_kwargs)
949
950            if code == "help":
951                label, status, elements = t
952                d.msgbox("You asked for help concerning the field labelled "
953                         "{0!r}.".format(label))
954            else:
955                # 't' contains the list of items as filled by the user
956                break
957
958        answers = '\n'.join(t)
959        d.msgbox("Your answers:\n\n{0}".format(indent(answers, "  ")),
960                 title="'form' demo with help facilities", no_collapse=True)
961        return t
962
963    def mixedform_demo(self):
964        HIDDEN    = 0x1
965        READ_ONLY = 0x2
966
967        elements = [
968            ("Size (cm)", 1, 1, "175", 1, 20, 4, 3, 0x0),
969            ("Weight (kg)", 2, 1, "85", 2, 20, 4, 3, 0x0),
970            ("City", 3, 1, "Groboule-les-Bains", 3, 20, 15, 25, 0x0),
971            ("State", 4, 1, "Some Lost Place", 4, 20, 15, 25, 0x0),
972            ("Country", 5, 1, "Nowhereland", 5, 20, 15, 20, 0x0),
973            ("My", 6, 1, "I hereby declare that, upon leaving this "
974             "world, all", 6, 20, 54, 0, READ_ONLY),
975            ("Very", 7, 1, "my fortune shall be transferred to Florent "
976             "Rougon's", 7, 20, 54, 0, READ_ONLY),
977            ("Last", 8, 1, "bank account number 000 4237 4587 32454/78 at "
978             "Banque", 8, 20, 54, 0, READ_ONLY),
979            ("Will", 9, 1, "Cantonale Vaudoise, Lausanne, Switzerland.",
980             9, 20, 54, 0, READ_ONLY),
981            ("Read-only field...", 10, 1, "... that doesn't go into the "
982             "output list", 10, 20, 0, 0, 0x0),
983            (r"\/3r`/ 53kri7 (0d3", 11, 1, "", 11, 20, 15, 20, HIDDEN) ]
984
985        code, fields = d.mixedform(
986            "Please fill in some personal information:", elements, width=77)
987
988        return fields
989
990    def passwordform_demo(self):
991        elements = [
992            ("Secret field 1", 1, 1, "", 1, 20, 12, 0),
993            ("Secret field 2", 2, 1, "", 2, 20, 12, 0),
994            ("Secret field 3", 3, 1, "Providing a non-empty initial content "
995             "(like this) for an invisible field can be very confusing!",
996             3, 20, 30, 160)]
997
998        code, fields = d.passwordform(
999            "Please enter all your secret passwords.\n\nOn purpose here, "
1000            "nothing is echoed when you type in the passwords. If you want "
1001            "asterisks, use the 'insecure' keyword argument as in the "
1002            "passwordbox demo.",
1003            elements, width=77, height=15, title="Passwordform demo")
1004
1005        d.msgbox("Secret password 1: '%s'\n"
1006                 "Secret password 2: '%s'\n"
1007                 "Secret password 3: '%s'" % tuple(fields),
1008                 width=60, height=20, title="The Whole Truth Now Revealed")
1009
1010        return fields
1011
1012    def menu_demo(self, name, city, state, country, size, weight, secret_code,
1013                  last_will1, last_will2, last_will3, last_will4):
1014        text = """\
1015Hello, %s from %s, %s, %s, %s cm, %s kg.
1016Thank you for giving us your Very Secret Code '%s'.
1017
1018As expressly stated in the previous form, your Last Will reads: "%s"
1019
1020All that was very interesting, thank you. However, in order to know you \
1021better and provide you with the best possible customer service, we would \
1022still need to know your favorite day of the week. Please indicate your \
1023preference below.""" \
1024            % (name, city, state, country, size, weight, secret_code,
1025               ' '.join([last_will1, last_will2, last_will3, last_will4]))
1026
1027        code, tag = d.menu(text, height=23, width=76, menu_height=7,
1028            choices=[("Monday", "Being the first day of the week..."),
1029                     ("Tuesday", "Comes after Monday"),
1030                     ("Wednesday", "Before Thursday day"),
1031                     ("Thursday", "Itself after Wednesday"),
1032                     ("Friday", "The best day of all"),
1033                     ("Saturday", "Well, I've had enough, thanks"),
1034                     ("Sunday", "Let's rest a little bit")])
1035
1036        return tag
1037
1038    def menu_demo_with_help(self):
1039        text = """Sample 'menu' dialog box with help_button=True and \
1040item_help=True."""
1041
1042        while True:
1043            code, tag = d.menu(text, height=16, width=60, menu_height=7,
1044                choices=[("Tag 1", "Item 1", "Help text for item 1"),
1045                         ("Tag 2", "Item 2", "Help text for item 2"),
1046                         ("Tag 3", "Item 3", "Help text for item 3"),
1047                         ("Tag 4", "Item 4", "Help text for item 4"),
1048                         ("Tag 5", "Item 5", "Help text for item 5"),
1049                         ("Tag 6", "Item 6", "Help text for item 6"),
1050                         ("Tag 7", "Item 7", "Help text for item 7"),
1051                         ("Tag 8", "Item 8", "Help text for item 8")],
1052                               title="A menu with help facilities",
1053                               help_button=True, item_help=True, help_tags=True)
1054
1055            if code == "help":
1056                d.msgbox("You asked for help concerning the item identified by "
1057                         "tag {0!r}.".format(tag), height=8, width=40)
1058            else:
1059                break
1060
1061        d.msgbox("You have chosen the item identified by tag "
1062                 "{0!r}.".format(tag), height=8, width=40)
1063
1064    def checklist_demo(self):
1065        # We could put non-empty items here (not only the tag for each entry)
1066        code, tags = d.checklist(text="What sandwich toppings do you like?",
1067                                 height=15, width=54, list_height=7,
1068                                 choices=[("Catsup", "",             False),
1069                                          ("Mustard", "",            False),
1070                                          ("Pesto", "",              False),
1071                                          ("Mayonnaise", "",          True),
1072                                          ("Horse radish","",        True),
1073                                          ("Sun-dried tomatoes", "", True)],
1074                                 title="Do you prefer ham or spam?",
1075                                 backtitle="And now, for something "
1076                                 "completely different...")
1077        return tags
1078
1079    SAMPLE_DATA_FOR_BUILDLIST_AND_CHECKLIST = [
1080        ("Tag 1", "Item 1", True,  "Help text for item 1"),
1081        ("Tag 2", "Item 2", False, "Help text for item 2"),
1082        ("Tag 3", "Item 3", False, "Help text for item 3"),
1083        ("Tag 4", "Item 4", True,  "Help text for item 4"),
1084        ("Tag 5", "Item 5", True,  "Help text for item 5"),
1085        ("Tag 6", "Item 6", False, "Help text for item 6"),
1086        ("Tag 7", "Item 7", True,  "Help text for item 7"),
1087        ("Tag 8", "Item 8", False, "Help text for item 8") ]
1088
1089    def checklist_demo_with_help(self):
1090        text = """Sample 'checklist' dialog box with help_button=True, \
1091item_help=True and help_status=True."""
1092        choices = self.SAMPLE_DATA_FOR_BUILDLIST_AND_CHECKLIST
1093
1094        while True:
1095            code, t = d.checklist(text, choices=choices,
1096                                  title="A checklist with help facilities",
1097                                  help_button=True, item_help=True,
1098                                  help_tags=True, help_status=True)
1099            if code == "help":
1100                tag, selected_tags, choices = t
1101                d.msgbox("You asked for help concerning the item identified "
1102                         "by tag {0!r}.".format(tag), height=7, width=60)
1103            else:
1104                # 't' contains the list of tags corresponding to checked items
1105                break
1106
1107        s = '\n'.join(t)
1108        d.msgbox("The tags corresponding to checked items are:\n\n"
1109                 "{0}".format(indent(s, "  ")), height=15, width=60,
1110                 title="'checklist' demo with help facilities",
1111                 no_collapse=True)
1112
1113    def radiolist_demo(self):
1114        choices = [
1115            ("Hamburger",       "2 slices of bread, a steak...", False),
1116            ("Hotdog",          "doesn't bite any more",         False),
1117            ("Burrito",         "no se lo que es",               False),
1118            ("Doener",          "Huh?",                          False),
1119            ("Falafel",         "Erm...",                        False),
1120            ("Bagel",           "Of course!",                    False),
1121            ("Big Mac",         "Ah, that's easy!",              True),
1122            ("Whopper",         "Erm, sorry",                    False),
1123            ("Quarter Pounder", 'called "le Big Mac" in France', False),
1124            ("Peanut Butter and Jelly", "Well, that's your own business...",
1125                                                                 False),
1126            ("Grilled cheese",  "And nothing more?",             False) ]
1127
1128        while True:
1129            code, t = d.radiolist(
1130                "What's your favorite kind of sandwich?", width=68,
1131                choices=choices, help_button=True, help_status=True)
1132
1133            if code == "help":
1134                # Prepare to redisplay the radiolist in the same state as it
1135                # was before the user pressed the Help button.
1136                tag, selected, choices = t
1137                d.msgbox("You asked for help about something called {0!r}. "
1138                         "Sorry, but I am quite incompetent in this matter."
1139                         .format(tag))
1140            else:
1141                # 't' is the chosen tag
1142                break
1143
1144        return t
1145
1146    def rangebox_demo(self):
1147        nb = 10                 # initial value
1148
1149        while True:
1150            code, nb = d.rangebox("""\
1151How many Microsoft(TM) engineers are needed to prepare such a sandwich?
1152
1153You can use the Up and Down arrows, Page Up and Page Down, Home and End keys \
1154to change the value; you may also use the Tab key, Left and Right arrows \
1155and any of the 0-9 keys to change a digit of the value.""",
1156                                  min=1, max=20, init=nb,
1157                                  extra_button=True, extra_label="Joker")
1158            if code == "ok":
1159                break
1160            elif code == "extra":
1161                d.msgbox("Well, {0} may be enough. Or not, depending on the "
1162                         "phase of the moon...".format(nb))
1163            else:
1164                assert False, "Unexpected Dialog exit code: {0!r}".format(code)
1165
1166        return nb
1167
1168    def buildlist_demo(self):
1169        items0 = [("A Monty Python DVD",                             False),
1170                  ("A Monty Python script",                          False),
1171                  ('A DVD of "Barry Lyndon" by Stanley Kubrick',     False),
1172                  ('A DVD of "The Good, the Bad and the Ugly" by Sergio Leone',
1173                                                                     False),
1174                  ('A DVD of "The Trial" by Orson Welles',           False),
1175                  ('The Trial, by Franz Kafka',                      False),
1176                  ('Animal Farm, by George Orwell',                  False),
1177                  ('Notre-Dame de Paris, by Victor Hugo',            False),
1178                  ('Les Misérables, by Victor Hugo',                 False),
1179                  ('Le Lys dans la Vallée, by Honoré de Balzac',     False),
1180                  ('Les Rois Maudits, by Maurice Druon',             False),
1181                  ('A Georges Brassens CD',                          False),
1182                  ("A book of Georges Brassens' songs",              False),
1183                  ('A Nina Simone CD',                               False),
1184                  ('Javier Vazquez y su Salsa - La Verdad',          False),
1185                  ('The last Justin Bieber album',                   False),
1186                  ('A printed copy of the Linux kernel source code', False),
1187                  ('A CD player',                                    False),
1188                  ('A DVD player',                                   False),
1189                  ('An MP3 player',                                  False)]
1190
1191        # Use the name as tag, item string and item-help string; the item-help
1192        # will be useful for long names because it is displayed in a place
1193        # that is large enough to avoid truncation. If not using
1194        # item_help=True, then the last element of eash tuple must be omitted.
1195        items = [ (tag, tag, status, tag) for (tag, status) in items0 ]
1196
1197        text = """If you were stranded on a desert island, what would you \
1198take?
1199
1200Press the space bar to toggle the status of an item between selected (on \
1201the left) and unselected (on the right). You can use the TAB key or \
1202^ and $ to change the focus between the different parts of the widget.
1203
1204(this widget is called with item_help=True and visit_items=True)"""
1205
1206        code, l = d.buildlist(text, items=items, visit_items=True,
1207                              item_help=True,
1208                              title="A simple 'buildlist' demo")
1209        return l
1210
1211    def buildlist_demo_with_help(self):
1212        text = """Sample 'buildlist' dialog box with help_button=True, \
1213item_help=True, help_status=True, and visit_items=False.
1214
1215Keys: SPACE   select or deselect the highlighted item, i.e.,
1216              move it between the left and right lists
1217      ^       move the focus to the left list
1218      $       move the focus to the right list
1219      TAB     move focus
1220      ENTER   press the focused button"""
1221        items = self.SAMPLE_DATA_FOR_BUILDLIST_AND_CHECKLIST
1222
1223        while True:
1224            code, t = d.buildlist(text, items=items,
1225                                  title="A 'buildlist' with help facilities",
1226                                  help_button=True, item_help=True,
1227                                  help_tags=True, help_status=True,
1228                                  no_collapse=True)
1229            if code == "help":
1230                tag, selected_tags, items = t
1231                d.msgbox("You asked for help concerning the item identified "
1232                         "by tag {0!r}.".format(tag), height=7, width=60)
1233            else:
1234                # 't' contains the list of tags corresponding to selected items
1235                break
1236
1237        s = '\n'.join(t)
1238        d.msgbox("The tags corresponding to selected items are:\n\n"
1239                 "{0}".format(indent(s, "  ")), height=15, width=60,
1240                 title="'buildlist' demo with help facilities",
1241                 no_collapse=True)
1242
1243    def calendar_demo(self):
1244        code, date = d.calendar("When do you think Georg Cantor was born?")
1245        return date
1246
1247    def calendar_demo_with_help(self):
1248        # Start with the current date
1249        day, month, year = -1, -1, -1
1250
1251        while True:
1252            code, date = d.calendar("When do you think Georg Cantor was born?",
1253                                    day=day, month=month, year=year,
1254                                    title="'calendar' demo",
1255                                    help_button=True)
1256            if code == "help":
1257                day, month, year = date
1258                d.msgbox("Help dialog for date {0:04d}-{1:02d}-{2:02d}.".format(
1259                        year, month, day), title="'calendar' demo")
1260            else:
1261                break
1262
1263        return date
1264
1265    def comment_on_Cantor_date_of_birth(self, day, month, year):
1266        complement = """\
1267For your information, Georg Ferdinand Ludwig Philip Cantor, a great \
1268mathematician, was born on March 3, 1845 in Saint Petersburg, and died on \
1269January 6, 1918. Among other things, Georg Cantor laid the foundation for \
1270the set theory (which is at the basis of most modern mathematics) \
1271and was the first person to give a rigorous definition of real numbers."""
1272
1273        if (year, month, day) == (1845, 3, 3):
1274            return "Spot-on! I'm impressed."
1275        elif year == 1845:
1276            return "You guessed the year right. {0}".format(complement)
1277        elif abs(year-1845) < 30:
1278            return "Not too far. {0}".format(complement)
1279        else:
1280            return "Well, not quite. {0}".format(complement)
1281
1282    def timebox_demo(self):
1283        # Get the current time (to display initially in the timebox)
1284        tm = time.localtime()
1285        init_hour, init_min, init_sec = tm.tm_hour, tm.tm_min, tm.tm_sec
1286        # tm.tm_sec can be 60 or even 61 according to the doc of the time module!
1287        init_sec = min(59, init_sec)
1288
1289        code, (hour, minute, second) = d.timebox(
1290            "And at what time, if I may ask?",
1291            hour=init_hour, minute=init_min, second=init_sec)
1292
1293        return (hour, minute, second)
1294
1295    def passwordbox_demo(self):
1296        # 'insecure' keyword argument only asks dialog to echo asterisks when
1297        # the user types characters. Not *that* bad.
1298        code, password = d.passwordbox("What is your root password, "
1299                                       "so that I can crack your system "
1300                                       "right now?", height=10, width=60,
1301                                       insecure=True)
1302        return password
1303
1304    def scrollbox_demo(self, name, favorite_day, toppings, sandwich,
1305                       nb_engineers, desert_island_stuff, date, time_,
1306                       password):
1307        tw71 = textwrap.TextWrapper(width=71, break_long_words=False,
1308                                    break_on_hyphens=True)
1309
1310        if nb_engineers is not None:
1311            sandwich_comment = " (the preparation of which requires, " \
1312                "according to you, {nb_engineers} MS {engineers})".format(
1313                nb_engineers=nb_engineers,
1314                engineers="engineers" if nb_engineers != 1 else "engineer")
1315        else:
1316            sandwich_comment = ""
1317
1318        sandwich_report = "Favorite sandwich: {sandwich}{comment}".format(
1319            sandwich=sandwich, comment=sandwich_comment)
1320
1321        if desert_island_stuff is None:
1322            # The widget was not available, the user didn't see anything.
1323            desert_island_string = ""
1324        else:
1325            if len(desert_island_stuff) == 0:
1326                desert_things = " nothing!"
1327            else:
1328                desert_things = "\n\n  " + "\n  ".join(desert_island_stuff)
1329
1330            desert_island_string = \
1331                "\nOn a desert island, you would take:{0}\n".format(
1332                desert_things)
1333
1334        day, month, year = date
1335        hour, minute, second = time_
1336        msg = """\
1337Here are some vital statistics about you:
1338
1339Name: {name}
1340Favorite day of the week: {favday}
1341Favorite sandwich toppings:{toppings}
1342{sandwich_report}
1343{desert_island_string}
1344Your answer about Georg Cantor's date of birth: \
1345{year:04d}-{month:02d}-{day:02d}
1346(at precisely {hour:02d}:{min:02d}:{sec:02d}!)
1347
1348{comment}
1349
1350Your root password is: ************************** (looks good!)""".format(
1351            name=name, favday=favorite_day,
1352            toppings="\n    ".join([''] + toppings),
1353            sandwich_report=tw71.fill(sandwich_report),
1354            desert_island_string=desert_island_string,
1355            year=year, month=month, day=day,
1356            hour=hour, min=minute, sec=second,
1357            comment=tw71.fill(
1358              self.comment_on_Cantor_date_of_birth(day, month, year)))
1359        d.scrollbox(msg, height=20, width=75, title="Great Report of the Year")
1360
1361    TREEVIEW_BASE_TEXT = """\
1362This is an example of the 'treeview' widget{options}. Nodes are labelled in a \
1363way that reflects their position in the tree, but this is not a requirement: \
1364you are free to name them the way you like.
1365
1366Node 0 is the root node. It has 3 children tagged 0.1, 0.2 and 0.3. \
1367You should now select a node with the space bar."""
1368
1369    def treeview_demo(self):
1370        code, tag = d.treeview(self.TREEVIEW_BASE_TEXT.format(options=""),
1371                               nodes=[ ("0", "node 0", False, 0),
1372                                       ("0.1", "node 0.1", False, 1),
1373                                       ("0.2", "node 0.2", False, 1),
1374                                       ("0.2.1", "node 0.2.1", False, 2),
1375                                       ("0.2.1.1", "node 0.2.1.1", True, 3),
1376                                       ("0.2.2", "node 0.2.2", False, 2),
1377                                       ("0.3", "node 0.3", False, 1),
1378                                       ("0.3.1", "node 0.3.1", False, 2),
1379                                       ("0.3.2", "node 0.3.2", False, 2) ],
1380                               title="'treeview' demo")
1381
1382        d.msgbox("You selected the node tagged {0!r}.".format(tag),
1383                 title="treeview demo")
1384        return tag
1385
1386    def treeview_demo_with_help(self):
1387        text = self.TREEVIEW_BASE_TEXT.format(
1388            options=" with help_button=True, item_help=True and "
1389            "help_status=True")
1390
1391        nodes = [ ("0",       "node 0",       False, 0, "Help text 1"),
1392                  ("0.1",     "node 0.1",     False, 1, "Help text 2"),
1393                  ("0.2",     "node 0.2",     False, 1, "Help text 3"),
1394                  ("0.2.1",   "node 0.2.1",   False, 2, "Help text 4"),
1395                  ("0.2.1.1", "node 0.2.1.1", True,  3, "Help text 5"),
1396                  ("0.2.2",   "node 0.2.2",   False, 2, "Help text 6"),
1397                  ("0.3",     "node 0.3",     False, 1, "Help text 7"),
1398                  ("0.3.1",   "node 0.3.1",   False, 2, "Help text 8"),
1399                  ("0.3.2",   "node 0.3.2",   False, 2, "Help text 9") ]
1400
1401        while True:
1402            code, t = d.treeview(text, nodes=nodes,
1403                                 title="'treeview' demo with help facilities",
1404                                 help_button=True, item_help=True,
1405                                 help_tags=True, help_status=True)
1406
1407            if code == "help":
1408                # Prepare to redisplay the treeview in the same state as it
1409                # was before the user pressed the Help button.
1410                tag, selected_tag, nodes = t
1411                d.msgbox("You asked for help about the node with tag {0!r}."
1412                         .format(tag))
1413            else:
1414                # 't' is the chosen tag
1415                break
1416
1417        d.msgbox("You selected the node tagged {0!r}.".format(t),
1418                 title="'treeview' demo")
1419        return t
1420
1421    def editbox_demo(self, filepath):
1422        if os.path.isfile(filepath):
1423            code, text = d.editbox(filepath, 20, 60,
1424                                   title="A Cheap Text Editor")
1425            d.scrollbox(text, title="Resulting text")
1426        else:
1427            d.msgbox("Skipping the first part of the 'editbox' demo, "
1428                     "as '{0}' can't be found.".format(filepath),
1429                     title="'msgbox' demo")
1430
1431        l = ["In the previous dialog, the initial contents was",
1432             "explicitly written to a file. With Dialog.editbox_str(),",
1433             "you can provide it as a string and pythondialog will",
1434             "automatically create and delete a temporary file for you",
1435             "holding this text for dialog.\n"] + \
1436             [ "This is line {0} of a boring sample text.".format(i+1)
1437               for i in range(100) ]
1438        code, text = d.editbox_str('\n'.join(l), 0, 0,
1439                                   title="A Cheap Text Editor")
1440        d.scrollbox(text, title="Resulting text")
1441
1442    def inputmenu_demo(self):
1443        choices = [ ("1st_tag", "Item 1 text"),
1444                    ("2nd_tag", "Item 2 text"),
1445                    ("3rd_tag", "Item 3 text") ]
1446
1447        for i in range(4, 21):
1448            choices.append(("%dth_tag" % i, "Item %d text" % i))
1449
1450        while True:
1451            code, tag, new_item_text = d.inputmenu(
1452                "Demonstration of 'inputmenu'. Any single item can be either "
1453                "accepted as is, or renamed.",
1454                width=60, menu_height=10, choices=choices,
1455                help_button=True, title="'inputmenu' demo")
1456
1457            if code == "help":
1458                d.msgbox("You asked for help about the item with tag {0!r}."
1459                         .format(tag))
1460                continue
1461            elif code == "accepted":
1462                text = "The item corresponding to tag {0!r} was " \
1463                    "accepted.".format(tag)
1464            elif code == "renamed":
1465                text = "The item corresponding to tag {0!r} was renamed to " \
1466                    "{1!r}.".format(tag, new_item_text)
1467            else:
1468                text = "Unexpected exit code from 'inputmenu': {0!r}.\n\n" \
1469                    "It may be a bug. Please report.".format(code)
1470
1471            break
1472
1473        d.msgbox(text, height=10, width=60,
1474                 title="Outcome of the 'inputmenu' demo")
1475
1476    # Help strings used in several places
1477    FSELECT_HELP = """\
1478Hint: the complete file path must be entered in the bottom field. One \
1479convenient way to achieve this is to use the SPACE bar when the desired file \
1480is highlighted in the top-right list.
1481
1482As usual, you can use the TAB and arrow keys to move between controls. If in \
1483the bottom field, the SPACE key provides auto-completion."""
1484
1485    # The following help text was initially meant to be used for several
1486    # widgets (at least progressbox and tailbox). Currently (dialog version
1487    # 1.2-20130902), "dialog --tailbox" doesn't seem to work with FIFOs, so the
1488    # "flexibility" of the help text is unused (another text is used when
1489    # demonstrating --tailbox). However, this might change in the future...
1490    def FIFO_HELP(self, widget):
1491        return """\
1492For demos based on the {widget} widget, you may use a FIFO, also called \
1493"named pipe". This is a special kind of file, to which you will be able to \
1494easily append data. With the {widget} widget, you can see the data stream \
1495flow in real time.
1496
1497To create a FIFO, you can use the commmand mkfifo(1), like this:
1498
1499  % mkfifo /tmp/my_shiny_new_fifo
1500
1501Then, you can cat(1) data to the FIFO like this:
1502
1503  % cat >>/tmp/my_shiny_new_fifo
1504  First line of text
1505  Second line of text
1506  ...
1507
1508You can end the input to cat(1) by typing Ctrl-D at the beginning of a \
1509line.""".format(widget=widget)
1510
1511    def fselect_demo(self, widget, init_path=None, allow_FIFOs=False, **kwargs):
1512        init_path = init_path or params["home_dir"]
1513        # Make sure the directory we chose ends with os.sep so that dialog
1514        # shows its contents right away
1515        if not init_path.endswith(os.sep):
1516            init_path += os.sep
1517
1518        while True:
1519            # We want to let the user quit this particular dialog with Cancel
1520            # without having to bother choosing a file, therefore we use the
1521            # original fselect() from dialog.Dialog and interpret the return
1522            # code manually. (By default, the MyDialog class defined in this
1523            # file intercepts the CANCEL and ESC exit codes and causes them to
1524            # spawn the "confirm quit" dialog.)
1525            code, path = self.Dialog_instance.fselect(
1526                init_path, height=10, width=60, help_button=True, **kwargs)
1527
1528            # Display the "confirm quit" dialog if the user pressed ESC.
1529            if not d.check_exit_request(code, ignore_Cancel=True):
1530                continue
1531
1532            # Provide an easy way out...
1533            if code == d.CANCEL:
1534                path = None
1535                break
1536            elif code == "help":
1537                d.msgbox("Help about {0!r} from the 'fselect' dialog.".format(
1538                        path), title="'fselect' demo")
1539                init_path = path
1540            elif code == d.OK:
1541                # Of course, one can use os.path.isfile(path) here, but we want
1542                # to allow regular files *and* possibly FIFOs. Since there is
1543                # no os.path.is*** convenience function for FIFOs, let's go
1544                # with os.stat.
1545                try:
1546                    mode = os.stat(path)[stat.ST_MODE]
1547                except os.error as e:
1548                    d.msgbox("Error: {0}".format(e))
1549                    continue
1550
1551                # Accept FIFOs only if allow_FIFOs is True
1552                if stat.S_ISREG(mode) or (allow_FIFOs and stat.S_ISFIFO(mode)):
1553                    break
1554                else:
1555                    if allow_FIFOs:
1556                        help_text = """\
1557You are expected to select a *file* here (possibly a FIFO), or press the \
1558Cancel button.\n\n%s
1559
1560For your convenience, I will reproduce the FIFO help text here:\n\n%s""" \
1561                            % (self.FSELECT_HELP, self.FIFO_HELP(widget))
1562                    else:
1563                        help_text = """\
1564You are expected to select a regular *file* here, or press the \
1565Cancel button.\n\n%s""" % (self.FSELECT_HELP,)
1566
1567                    d.msgbox(help_text, width=72, height=20)
1568            else:
1569                d.msgbox("Unexpected exit code from Dialog.fselect(): {0}.\n\n"
1570                         "It may be a bug. Please report.".format(code))
1571        return path
1572
1573    def dselect_demo(self, init_dir=None):
1574        init_dir = init_dir or params["home_dir"]
1575        # Make sure the directory we chose ends with os.sep so that dialog
1576        # shows its contents right away
1577        if not init_dir.endswith(os.sep):
1578            init_dir += os.sep
1579
1580        while True:
1581            code, path = d.dselect(init_dir, 10, 50,
1582                                   title="Please choose a directory",
1583                                   help_button=True)
1584            if code == "help":
1585                d.msgbox("Help about {0!r} from the 'dselect' dialog.".format(
1586                        path), title="'dselect' demo")
1587                init_dir = path
1588            # When Python 3.2 is old enough, we'll be able to check if
1589            # path.endswith(os.sep) and remove the trailing os.sep if this
1590            # does not change the path according to os.path.samefile().
1591            elif not os.path.isdir(path):
1592                d.msgbox("Hmm. It seems that {0!r} is not a directory".format(
1593                        path), title="'dselect' demo")
1594            else:
1595                break
1596
1597        d.msgbox("Directory '%s' thanks you for choosing him." % path)
1598        return path
1599
1600    def tailbox_demo(self, height=22, width=78):
1601        widget = "tailbox"
1602
1603        # First, ask the user for a file.
1604        # Strangely (dialog version 1.2-20130902 bug?), "dialog --tailbox"
1605        # doesn't work with FIFOs: "Error moving file pointer in last_lines()"
1606        # and DIALOG_ERROR exit status.
1607        path = self.fselect_demo(widget, allow_FIFOs=False,
1608                                 title="Please choose a file to be shown as "
1609                                 "with 'tail -f'")
1610        # Now, the tailbox
1611        if path is None:
1612            # User chose to abort
1613            return
1614        else:
1615            d.tailbox(path, height, width, title="Tailbox example")
1616
1617    def pause_demo(self, seconds):
1618        d.pause("""\
1619Ugh, sorry. pythondialog is still in development, and its advanced circuitry \
1620detected internal error number 0x666. That's a pretty nasty one, you know.
1621
1622I am embarrassed. I don't know how to tell you, but we are going to have to \
1623reboot. In %d seconds.
1624
1625Fasten your seatbelt...""" % seconds, height=18, seconds=seconds)
1626
1627
1628def process_command_line():
1629    global params
1630
1631    try:
1632        opts, args = getopt.getopt(sys.argv[1:], "ftE",
1633                                   ["test-suite",
1634                                    "fast",
1635                                    "debug",
1636                                    "debug-file=",
1637                                    "debug-expand-file-opt",
1638                                    "help",
1639                                    "version"])
1640    except getopt.GetoptError:
1641        print(usage, file=sys.stderr)
1642        return ("exit", 1)
1643
1644    # Let's start with the options that don't require any non-option argument
1645    # to be present
1646    for option, value in opts:
1647        if option == "--help":
1648            print(usage)
1649            return ("exit", 0)
1650        elif option == "--version":
1651            print("%s %s\n%s" % (progname, progversion, version_blurb))
1652            return ("exit", 0)
1653
1654    # Now, require a correct invocation.
1655    if len(args) != 0:
1656        print(usage, file=sys.stderr)
1657        return ("exit", 1)
1658
1659    # Default values for parameters
1660    params = { "fast_mode": False,
1661               "testsuite_mode": False,
1662               "debug": False,
1663               "debug_filename": default_debug_filename,
1664               "debug_expand_file_opt": False }
1665
1666    # Get the home directory, if any, and store it in params (often useful).
1667    root_dir = os.sep           # This is OK for Unix-like systems
1668    params["home_dir"] = os.getenv("HOME", root_dir)
1669
1670    # General option processing
1671    for option, value in opts:
1672        if option in ("-t", "--test-suite"):
1673            params["testsuite_mode"] = True
1674            # --test-suite implies --fast
1675            params["fast_mode"] = True
1676        elif option in ("-f", "--fast"):
1677            params["fast_mode"] = True
1678        elif option == "--debug":
1679            params["debug"] = True
1680        elif option == "--debug-file":
1681            params["debug_filename"] = value
1682        elif option in ("-E", "--debug-expand-file-opt"):
1683            params["debug_expand_file_opt"] = True
1684        else:
1685            # The options (such as --help) that cause immediate exit
1686            # were already checked, and caused the function to return.
1687            # Therefore, if we are here, it can't be due to any of these
1688            # options.
1689            assert False, "Unexpected option received from the " \
1690                "getopt module: '%s'" % option
1691
1692    return ("continue", None)
1693
1694
1695def main():
1696    """This demo shows the main features of pythondialog."""
1697    locale.setlocale(locale.LC_ALL, '')
1698
1699    what_to_do, code = process_command_line()
1700    if what_to_do == "exit":
1701        sys.exit(code)
1702
1703    try:
1704        app = MyApp()
1705        app.run()
1706    except dialog.error as exc_instance:
1707        # The error that causes a PythonDialogErrorBeforeExecInChildProcess to
1708        # be raised happens in the child process used to run the dialog-like
1709        # program, and the corresponding traceback is printed right away from
1710        # that child process when the error is encountered. Therefore, don't
1711        # print a second, not very useful traceback for this kind of exception.
1712        if not isinstance(exc_instance,
1713                          dialog.PythonDialogErrorBeforeExecInChildProcess):
1714            print(traceback.format_exc(), file=sys.stderr)
1715
1716        print("Error (see above for a traceback):\n\n{0}".format(
1717                exc_instance), file=sys.stderr)
1718        sys.exit(1)
1719
1720    sys.exit(0)
1721
1722
1723if __name__ == "__main__": main()
1724