install

installer
Log | Files | Refs | README | LICENSE | git clone https://git.ne02ptzero.me/git/install

commit 41f4257bbfef6a0752152174eb631470af97323f
parent fe137c981534360852a7a60112906dd4d482084c
Author: Ne02ptzero <louis@ne02ptzero.me>
Date:   Mon, 14 Nov 2016 17:50:03 +0100

Add(New codebase):

Now based on dialog

Diffstat:
Amain.py | 91+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/AUTHORS | 6++++++
Apythondialog/COPYING | 510+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/COPYING.Sphinx | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/ChangeLog | 1532+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/INSTALL | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/MANIFEST.in | 6++++++
Apythondialog/PKG-INFO | 303+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/README.distributors | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/README.rst | 282+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/TODO | 5+++++
Apythondialog/__init__.py | 0
Apythondialog/dialog.py | 3746+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/doc/DialogBackendVersion.rst | 10++++++++++
Apythondialog/doc/Dialog_class_overview.rst | 522+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/doc/Makefile | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/doc/_static/README.txt | 2++
Apythondialog/doc/_templates/README.txt | 2++
Apythondialog/doc/conf.py | 278+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/doc/exceptions.rst | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/doc/glossary.rst | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/doc/index.rst | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/doc/internals.rst | 25+++++++++++++++++++++++++
Apythondialog/doc/intro/example.py | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/doc/intro/intro.rst | 226+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/doc/screenshots/buildlist.png | 0
Apythondialog/doc/screenshots/calendar.png | 0
Apythondialog/doc/screenshots/checklist.png | 0
Apythondialog/doc/screenshots/dselect.png | 0
Apythondialog/doc/screenshots/editbox.png | 0
Apythondialog/doc/screenshots/form.png | 0
Apythondialog/doc/screenshots/fselect.png | 0
Apythondialog/doc/screenshots/gauge.png | 0
Apythondialog/doc/screenshots/infobox.png | 0
Apythondialog/doc/screenshots/inputbox.png | 0
Apythondialog/doc/screenshots/inputmenu.png | 0
Apythondialog/doc/screenshots/intro/example-infobox.png | 0
Apythondialog/doc/screenshots/intro/example-menu.png | 0
Apythondialog/doc/screenshots/intro/example-msgbox.png | 0
Apythondialog/doc/screenshots/menu.png | 0
Apythondialog/doc/screenshots/mixedform.png | 0
Apythondialog/doc/screenshots/mixedgauge.png | 0
Apythondialog/doc/screenshots/msgbox.png | 0
Apythondialog/doc/screenshots/passwordbox.png | 0
Apythondialog/doc/screenshots/passwordform.png | 0
Apythondialog/doc/screenshots/pause.png | 0
Apythondialog/doc/screenshots/programbox.png | 0
Apythondialog/doc/screenshots/progressbox.png | 0
Apythondialog/doc/screenshots/radiolist.png | 0
Apythondialog/doc/screenshots/rangebox.png | 0
Apythondialog/doc/screenshots/scrollbox.png | 0
Apythondialog/doc/screenshots/tailbox.png | 0
Apythondialog/doc/screenshots/textbox.png | 0
Apythondialog/doc/screenshots/timebox.png | 0
Apythondialog/doc/screenshots/treeview.png | 0
Apythondialog/doc/screenshots/yesno.png | 0
Apythondialog/doc/widgets.rst | 381+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/examples/demo.py | 1702+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/examples/simple_example.py | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/examples/with-autowidgetsize/demo.py | 1708+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/examples/with-autowidgetsize/simple_example.py | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apythondialog/setup.cfg | 5+++++
Apythondialog/setup.py | 140+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascreens/__init__.py | 0
Ascreens/main_menu/__init__.py | 0
Ascreens/main_menu/main_menu.py | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
66 files changed, 12625 insertions(+), 0 deletions(-)

diff --git a/main.py b/main.py @@ -0,0 +1,91 @@ +#! /usr/bin/env python3 +################################### LICENSE #################################### +# Copyright 2016 Morphux # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +################################################################################ + +## +# main.py +# Created: 14/11/2016 +# By: Louis Solofrizzo <louis@morphux.org> +## + +import locale +from pythondialog.dialog import * + +## +# Global configuration +## +title = "Morphux Installer" +version = "1.0" + +class Main: + +## +# Variables +## + modules = {} + d = Dialog(dialog="dialog",autowidgetsize=True) + conf_lst = {} + screens = [] + +## +# Functions +## + # Construct function + def __init__(self): + self.load_screens() + self.get_screens_infos() + locale.setlocale(locale.LC_ALL, '') + self.d.set_background_title(title + ", version " + version) + self.main() + + + # Load differents modules in the path screens/ + def load_screens(self, path = "screens"): + res = {} + lst = os.listdir(path) + dir = [] + for d in lst: + s = os.path.abspath(path) + os.sep + d + if os.path.isdir(s) and os.path.exists(s + os.sep + "__init__.py"): + dir.append(d) + for d in dir: + res[d] = __import__(path + "." + d + "." + d, fromlist = ["*"]) + res[d] = getattr(res[d], d.title()) + self.modules = res + self.get_screens_infos() + + # Get infos on screens + def get_screens_infos(self): + config = {} + for name, klass in self.modules.items(): + print("Reading "+ name +" module ... ", end="") + klass = klass() + config = klass.init(self.d, self.conf_lst) + self.screens.append(klass) + print("Done !") + + def main(self): + id = 0 + t_id = 0 + while 1: + t_id = self.screens[id].main() + if (t_id == -2 or (t_id == -1 and id == 0)): + return 0 + elif (t_id != id): + id = t_id + + +main = Main() diff --git a/pythondialog/AUTHORS b/pythondialog/AUTHORS @@ -0,0 +1,6 @@ +-*- coding: utf-8 -*- + +Robb Shecter <robb@acm.org> +Sultanbek Tezadov (http://sultan.da.ru/) +Peter Åstrand <peter@cendio.se> +Florent Rougon <f.rougon@free.fr> (current maintainer) diff --git a/pythondialog/COPYING b/pythondialog/COPYING @@ -0,0 +1,510 @@ + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations +below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it +becomes a de-facto standard. To achieve this, non-free programs must +be allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control +compilation and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at least + three years, to give the same user the materials specified in + Subsection 6a, above, for a charge no more than the cost of + performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply, and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License +may add an explicit geographical distribution limitation excluding those +countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms +of the ordinary General Public License). + + To apply these terms, attach the following notices to the library. +It is safest to attach them to the start of each source file to most +effectively convey the exclusion of warranty; and each file should +have at least the "copyright" line and a pointer to where the full +notice is found. + + + <one line to give the library's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or +your school, if any, to sign a "copyright disclaimer" for the library, +if necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James + Random Hacker. + + <signature of Ty Coon>, 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/pythondialog/COPYING.Sphinx b/pythondialog/COPYING.Sphinx @@ -0,0 +1,99 @@ +The doc/Makefile file is a derived work from Sphinx (cf. +sphinx/quickstart.py in the Sphinx source), whose licensing +information is the following (the "AUTHORS file" mentioned refers to +the Sphinx source and is reproduced below): + + +Sphinx licensing information +============================ + +Copyright (c) 2007-2013 by the Sphinx team (see AUTHORS file). +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +Sphinx AUTHORS file +=================== + +Sphinx is written and maintained by Georg Brandl <georg@python.org>. + +Substantial parts of the templates were written by Armin Ronacher +<armin.ronacher@active-4.com>. + +Other contributors, listed alphabetically, are: + +* Andi Albrecht -- agogo theme +* Henrique Bastos -- SVG support for graphviz extension +* Daniel Bültmann -- todo extension +* Etienne Desautels -- apidoc module +* Michael Droettboom -- inheritance_diagram extension +* Charles Duffy -- original graphviz extension +* Kevin Dunn -- MathJax extension +* Josip Dzolonga -- coverage builder +* Hernan Grecco -- search improvements +* Horst Gutmann -- internationalization support +* Martin Hans -- autodoc improvements +* Doug Hellmann -- graphviz improvements +* Dave Kuhlman -- original LaTeX writer +* Blaise Laflamme -- pyramid theme +* Thomas Lamb -- linkcheck builder +* Łukasz Langa -- partial support for autodoc +* Robert Lehmann -- gettext builder (GSOC project) +* Dan MacKinlay -- metadata fixes +* Martin Mahner -- nature theme +* Will Maier -- directory HTML builder +* Jacob Mason -- websupport library (GSOC project) +* Roland Meister -- epub builder +* Ezio Melotti -- collapsible sidebar JavaScript +* Daniel Neuhäuser -- JavaScript domain, Python 3 support (GSOC) +* Christopher Perkins -- autosummary integration +* Benjamin Peterson -- unittests +* T. Powers -- HTML output improvements +* Stefan Seefeld -- toctree improvements +* Shibukawa Yoshiki -- pluggable search API and Japanese search +* Antonio Valentino -- qthelp builder +* Pauli Virtanen -- autodoc improvements, autosummary extension +* Stefan van der Walt -- autosummary extension +* Thomas Waldmann -- apidoc module fixes +* John Waltman -- Texinfo builder +* Barry Warsaw -- setup command improvements +* Sebastian Wiesner -- image handling, distutils support +* Joel Wurtz -- cellspanning support in LaTeX + +Many thanks for all contributions! + +There are also a few modules or functions incorporated from other +authors and projects: + +* sphinx.util.jsdump uses the basestring encoding from simplejson, + written by Bob Ippolito, released under the MIT license +* sphinx.util.stemmer was written by Vivake Gupta, placed in the + Public Domain + + +Local Variables: +coding: utf-8 +fill-column: 70 +End: diff --git a/pythondialog/ChangeLog b/pythondialog/ChangeLog @@ -0,0 +1,1532 @@ +2016-05-07 Florent Rougon <f.rougon@free.fr> + + Release 3.4.0 + + README.rst: link to the blessings library, add link to ncurses home page + +2016-05-07 Florent Rougon <f.rougon@free.fr> + + Add 'week_start' common option, mapped to dialog's --week-start option + + * The value may be an integer or a string (cf. dialog's man page for + more details). + + * Using this requires dialog 1.3-20160126 or later. + +2016-05-07 Florent Rougon <f.rougon@free.fr> + + Update copyright notices and demo version + + Fix typo + +2016-05-07 Florent Rougon <f.rougon@free.fr> + + Doc build: suppress warnings about :option:`--unknown-option` references + + * The documentation contains many references to dialog options. These + are not defined in the pythondialog Manual, which with recent Sphinx + (>= 1.3 or something like that) causes a lot of warnings. Fortunately, + these can be suppressed (specifically for unknown option references) + with 'suppress_warnings' in conf.py starting from Sphinx 1.4.0. + + * This is what this commit does. Alternatively, the option references + could be replaced with dumb markup such as ``--unknown-option``. + +2016-05-07 Florent Rougon <f.rougon@free.fr> + + Add demo example for Dialog.editbox_str() + +2016-05-06 Florent Rougon <f.rougon@free.fr> + + Add Dialog.editbox_str() + + * dialog.py (Dialog.editbox_str): new method. It is a convenience + wrapper around Dialog.editbox() that automatically creates and deletes a + temporary file containing the initial box contents which is passed as a + string (Dialog.editbox() needs it in a file). + +2016-01-28 Florent Rougon <f.rougon@free.fr> + + Fix bug in demo.py when /etc/passwd is inexistent + + * examples/demo.py (MyApp.editbox_demo): display a message when + /etc/passwd is inexistent instead of raising an exception (trying to + display the result, which doesn't exist). + +2015-05-28 Florent Rougon <f.rougon@free.fr> + + Release 3.3.0 + +2015-05-28 Florent Rougon <f.rougon@free.fr> + + Minor changes + + * doc/Dialog_class_overview.rst: fix a typo + + * doc/conf.py: update copyright years for the pythondialog Manual + + * examples/demo.py: update version number + cosmetic change + + * examples/with-autowidgetsize/demo.py: ditto + +2015-05-27 Florent Rougon <f.rougon@free.fr> + + Use html_theme = 'classic' with Sphinx 1.3b3 or later + + * doc/conf.py: change the HTML theme name to 'classic' if the Sphinx + version is 1.3b3 or later (the old name 'default' is now deprecated). + +2015-05-27 Florent Rougon <f.rougon@free.fr> + + Allow passing dialog arguments via a temporary file + + * dialog.py (Dialog.__init__): new 'pass_args_via_file' parameter. The + default value (None) enables argument passing via --file only if the + dialog version is recent enough to offer a reliable --file + implementation (i.e., 1.2-20150513 or later). + + * dialog.py (Dialog.setup_debug): new 'expand_file_opt' parameter to + allow producing debug log files with full dialog commands as before, + instead of dialog calls containing only one --file option followed by a + path to a temporary file. + + * dialog.py (Dialog._quote_arg_for_file_opt): new method (quotes + arguments for safe inclusion via --file). + + * dialog.py (Dialog._call_program): honor Dialog.pass_args_via_file and + return a 3-tuple instead of a 2-tuple (new element: path to the + temporary file containing the dialog arguments, or None if + Dialog.pass_args_via_file is false). + + * dialog.py (Dialog._handle_program_exit): new method, which wraps + Dialog._wait_for_program_termination() and makes sure the temporary + file, if any, is removed when it is not needed anymore. + + * dialog.py (Dialog._perform): adapt to the change in the return value + of Dialog._call_program() and use Dialog._handle_program_exit() instead + of Dialog._wait_for_program_termination(). + + * dialog.py (Dialog.gauge_start, Dialog.gauge_stop): similar changes. + + * doc/Dialog_class_overview.rst: add two notes to make it clear that the + dialog arguments may be included indirectly via --file, depending on the + pythondialog and dialog versions, as well as on the arguments passed to + Dialog.__init__(). + + * README.rst: mention the 'expand_file_opt' parameter along with + Dialog.setup_debug(). + + * examples/demo.py: new option (-E, --debug-expand-file-opt) to allow + obtaining the same dialog commands as before in the file generated by + --debug (i.e., with the --file options expanded instead of referring to + the temporary files). + +2015-05-25 Florent Rougon <f.rougon@free.fr> + + Fix message for InadequateBackendVersion in _dialog_version_check() + + * dialog.py (Dialog._dialog_version_check): the message passed when + raising InadequateBackendVersion had a hardcoded "the programbox widget" + substring where it should have been using the 'feature' parameter. + +2015-04-10 Florent Rougon <f.rougon@free.fr> + + Release 3.2.2 + +2015-04-04 Florent Rougon <f.rougon@free.fr> + + Release 3.2.2rc1 + + Minor improvements to README.rst and INSTALL (formatting + date) + +2015-04-04 Florent Rougon <f.rougon@free.fr> + + Update the 'with-autowidgetsize' demo + + * examples/with-autowidgetsize/demo.py: apply yesterday's demo.py + changes to this file too. + +2015-04-03 Florent Rougon <f.rougon@free.fr> + + README.rst: fix command line for running the demo + + * README.rst: the given command only worked if the directory containing + dialog.py was in sys.path. Prepend 'PYTHONPATH=. ' to the suggested + command to fix this. + +2015-04-03 Florent Rougon <f.rougon@free.fr> + + README.rst: improve formatting and references to external projects + +2015-04-03 Florent Rougon <f.rougon@free.fr> + + demo: fix bug when buildlist is not available + + * demo.py: the demo used to crash on the report when the desert island + question had not been asked because the backend was too old for + buildlist. This is now fixed. + +2015-04-03 Florent Rougon <f.rougon@free.fr> + + Fix backend version check for several widgets + + * dialog.py: + - minimum version for buildlist is 1.2-20121230, not 1.2; + - minimum version for programbox is 1.1-20110302, not 1.1; + - minimum version for rangebox is 1.2-20121230, not 1.2; + - minimum version for treeview is 1.2-20121230, not 1.2. + + * demo.py: analogous changes to display user-friendly messages when the + backend is too old. + +2015-04-03 Florent Rougon <f.rougon@free.fr> + + demo: provide fallback for Python versions that don't have 'callable' + + * examples/demo.py: provide a custom 'callable' function for old Python + versions (such as 3.1) that don't have the 'callable' builtin. + +2015-04-03 Florent Rougon <f.rougon@free.fr> + + demo: don't use contextlib anymore + + * examples/demo.py: contextlib is nice but not really useful here. Since + its use in this file causes problems under Python 3.1 at least, don't + use it anymore for now. + +2015-04-03 Florent Rougon <f.rougon@free.fr> + + Workaround for old dialog versions that print the version on stdout + + * dialog.py: some dialog versions print the version on stdout when using + --print-version (e.g., 1.1-20100428). In this case, the output read on + stderr is empty. This commit works around this problem by rerunning the + dialog program, capturing its stdout instead of stderr. The penalty is + minimal since the backend version is cached. + +2014-10-30 Florent Rougon <f.rougon@free.fr> + + Release 3.2.1 + +2014-10-30 Florent Rougon <f.rougon@free.fr> + + Fix awkward wording in the license text for the tutorial example.py + + * doc/intro/example.py: replace "Neither the name of the Florent Rougon + (...)" with something more appropriate. + +2014-10-30 Florent Rougon <f.rougon@free.fr> + + [Manual] Improve the tutorial + + * doc/intro/example.py: remove the useless "import dialog" statement at + the beginning of the file + + * Expand on the example: + - introduce the msgbox and infobox widgets as well; + - present the 'title', 'backtitle', 'width' and 'height' common + options; + - show how to deal with the Dialog exit codes; + - mention the autowidgetsize feature; + - suggest good practices such as locale.setlocale(locale.LC_ALL, '') + at the beginning of the program. + +2014-10-18 Florent Rougon <f.rougon@free.fr> + + Add a label for the Glossary in the doc + + * doc/glossary.rst: add a label before the Glossary section (the + sphinx-build program from Debian unstable complains about a missing + label without this change). + +2014-10-15 Florent Rougon <f.rougon@free.fr> + + Release 3.2.0 + +2014-10-15 Sphinx team <no-email@example.com> + + Add the Makefile generated by sphinx-quickstart + +2014-10-15 Florent Rougon <f.rougon@free.fr> + + Convert the documentation to Sphinx format + + * The new documentation is much nicer and more convenient to use. + +2014-10-14 Florent Rougon <f.rougon@free.fr> + + Add a line for .rst files to .gitattributes + +2014-09-29 Florent Rougon <f.rougon@free.fr> + + Fix calendar default values and docstring + + * dialog.py (Dialog.calendar): change the default values for day, month + and year from 0 to -1. This is unfortunately BACKWARD-INCOMPATIBLE, + but 0 doesn't work well for day and month in dialog, and it would be + quite unintuitive to have them default to -1 with the year still + defaulting to 0. + + * dialog.py (Dialog.calendar): improve the docstring clarity. + + * examples/demo.py (calendar_demo_with_help): use -1 for the initial + values of day, month and year. This is only for clarity; the previous + code would work just as well. + +2014-08-20 Florent Rougon <f.rougon@free.fr> + + Release 3.1.0 + +2014-08-20 Florent Rougon <f.rougon@free.fr> + + Miscellaneous little changes to ancillary files + + * MANIFEST.in: remove "global-exclude *~" and + "global-exclude .gitattributes" because they cause warnings upon + installation with pip that may frighten users, and only provide a safety + net against hypothetical bugs that don't exist in the current version. + + * TODO: update the file now that the 'autowidgetsize' option has been + implemented. + + * setup.py: don't print the "No .git directory, using the 'ChangeLog' + file as is" message anymore, as it pollutes the installation with pip, + possibly causing users to fear that something went wrong. + +2014-08-20 Florent Rougon <f.rougon@free.fr> + + Change 'autowidgetsize' into a Dialog constructor option + + * dialog.py: remove the 'features' mechanism and change 'autowidgetsize' + into a Dialog constructor option (boolean, keyword-only argument). This + is simpler, doesn't require any recent package and makes it possible to + have several Dialog instances with different values for the + 'autowidgetsize' setting in the same process... The 'features' mechanism + is removed because it will have no use after this change. + + * examples/with-autowidgetsize/demo.py, + examples/with-autowidgetsize/simple_example.py: adapt to this change. + +2014-08-18 Florent Rougon <f.rougon@free.fr> + + New 'examples' directory; examples using 'autowidgetsize' + + * demo.py, simple_example.py: move to the new 'examples' directory. + + * New subdirectory of 'examples' named 'with-autowidgetsize', containing + copies of demo.py and simple_example.py using the 'autowidgetsize' + option (small modifications have been necessary in order to preserve + correct display). + +2014-08-19 Florent Rougon <f.rougon@free.fr> + + demo.py: programbox and progressboxoid changes + + * demo.py (MyApp.programbox_demo): remove the \n character at the end of + 'text'. Otherwise, dialog will see *two* newline chars in a row at the + end and will faithfully print a blank line. This, in turn, may cause + unwanted scrolling if the window doesn't have enough space for that + line. + + * demo.py: run programbox_demo from MyApp.demo() if the dialog version + is >= 1.2-20140112, otherwise leave it in MyApp.additional_widgets(). + Versions older than 1.2-20140112 have a bug that makes it unsuitable + for the main demo. + + * demo.py (MyApp.progressboxoid): allow additional arguments to be + passed as **kwargs. + +2014-08-18 Florent Rougon <f.rougon@free.fr> + + New 'autowidgetsize' feature + + * dialog.py: add the 'autowidgetsize' feature. When enabled, + pythondialog's widget-producing methods behave as if width=0, height=0, + etc. had been passed, except where these parameters are explicitely + specified with different values. This feature is currently marked as + experimental, please give some feedback. + + Note: in order to differentiate between a default value obtained when + 'autowidgetsize' is disabled and an explicitely-specified width, height, + etc., the size parameters modified by this commit now default to None. + In order to compensate for this information loss, the effective default + values when 'autowidgetsize' is disabled are now mentioned in the + docstrings of the corresponding widget-producing methods. + + * dialog.py (Dialog._default_size): new method returning the appropriate + default values for the size parameters depending on whether + 'autowidgetsize' is enabled or not. This allows tight factoring of the + code that had to be added to almost every widget. + +2014-08-18 Florent Rougon <f.rougon@free.fr> + + New 'features' concept + + * dialog.py: new concept called 'features' that is similar to Python's + __future__ statement. The general idea is that users can globally modify + pythondialog's behavior by calling: + + dialog.enable_feature(FEATURE) + + right after importing the 'dialog' module, in order to ensure consistent + behavior. In this call, FEATURE must be a member of the 'dialog.Feature' + enum. Using this mechanism therefore requires a Python installation + containing the 'enum' module. Since it is part of the standard library + in Python 3.4, this should not be a significant problem. + + Note: the Python 3.4 'enum' module has been backported to older Python + versions under the name 'enum34'. + + * dialog.py: this commit adds the following module-level variables: + + _HAS_ENUM, _features + + as well as the Feature class (which is an enum). + + * dialog.py (enable_feature, disable_feature, feature_enabled_p): new + functions. + +2014-07-23 Florent Rougon <f.rougon@free.fr> + + Remove _create_temporary_directory() in favor of tempfile.NamedTemporaryFile() + + * dialog.py (Dialog.scrollbox): use tempfile.NamedTemporaryFile() + instead of our custom _create_temporary_directory() function. + + * There was no security problem in _create_temporary_directory() as far + as I know, however it is usually better to use well-tested library + functions when possible instead of custom ones. When + _create_temporary_directory() was written, what was available from the + tempfile module was not satisfactory, but this is not the case anymore. + + * dialog.py: BACKWARD INCOMPATIBILITY: the + UnableToCreateTemporaryDirectory exception is not defined anymore. + Dialog.scrollbox() now creates a temporary file without any temporary + directory, therefore there is no place anymore for this exception to be + used. The equivalent condition in tempfile.NamedTemporaryFile() + generates an OSError exception (more precisely, a FileExistsError in + Python 3.3 or later, which is a subclass of OSError). As usual, this + exception is wrapped by pythondialog and seen as a PythonDialogOSError + by user code. + + * As a conclusion, wherever user code was expecting + UnableToCreateTemporaryDirectory in previous versions, it should now + expect a PythonDialogOSError, consistently with the tempfile module and + OSError wrapping by pythondialog. + +2014-07-18 Florent Rougon <f.rougon@free.fr> + + Improve installation instructions + + * INSTALL: better explain the pip-based installation method. In + particular, indicate in which situations it is safe or unsafe to use. + + * README.rst: minor improvement. + +2014-05-20 Florent Rougon <f.rougon@free.fr> + + Declare .gitattributes and .gitignore with 'export-ignore' + + * .gitattributes: declare these two files in .gitattributes with the + 'export-ignore' attribute, for the benefit of 'git archive'. + +2014-05-20 Florent Rougon <f.rougon@free.fr> + + Update the pip documentation URL + + * README.rst: update the pip documentation URL which changed according + to <https://mail.python.org/pipermail/distutils-sig/2014-May/024180.html>. + +2014-01-19 Florent Rougon <f.rougon@free.fr> + + Exclude *~ and .gitattributes files from source distributions + + * MANIFEST.in: safety measure for future updates that might + inadvertently introduce undesired files in the source dist. + +2014-01-13 Florent Rougon <f.rougon@free.fr> + + Introduce end-of-line normalization for text files in the repository + +2013-11-01 Florent Rougon <f.rougon@free.fr> + + Release 3.0.1 + +2013-10-27 Florent Rougon <f.rougon@free.fr> + + Make sure ChangeLog.init and ChangeLog are read/written using UTF-8 + + * setup.py: make sure ChangeLog.init and ChangeLog are read/written + using UTF-8 (regardless of locale settings), consistently with the Git + repository and release tarballs. + +2013-10-18 Florent Rougon <f.rougon@free.fr> + + Minor improvements + + * dialog.py (Dialog.backend_version): when raising + UnableToRetrieveBackendVersion, print the exit code (which is now a + string) with repr()-style representation. + + * demo.py: + - use the print() function instead of sys.stderr.write(); + - minor improvement of the buildlist demo. + + * setup.py: + - add "ncurses" and "terminal" as keywords; + - use a 'with' statement to open and close 'README.rst'. + + * README.rst: mainly, write appropriate text concerning the Python 2 + backport. + +2013-10-12 Florent Rougon <f.rougon@free.fr> + + Release 3.0.0 + + Minor improvements + + Add buildlist support + +2013-10-12 Florent Rougon <f.rougon@free.fr> + + Better support for parsing dialog output + + * This commit allows parsing a dialog output style used by the buildlist + widget, that was not supported before. + + * dialog.py (Dialog._parse_quoted_string): new optional 'start' + argument. + + * dialog.py (Dialog._split_shellstyle_arglist): new function to parse a + list of strings quoted in POSIX shell style, in order to process + buildlist output. + + * dialog.py (Dialog._parse_help): new optional 'multival_on_single_line' + argument to allow parsing of the buildlist help output. + +2013-10-12 Florent Rougon <f.rougon@free.fr> + + Improve Extra button support + + * With this commit, Extra button support should be as good as in the + dialog backend. If not, it's a bug. + + * dialog.py: fix support for the Extra button in calendar, rangebox, + timebox and treeview, where None was returned instead of the value one + could reasonably expect. + + * dialog.py: document usage of the Extra button in the Dialog class + docstring. + + * demo.py: add examples of Extra button usage in textbox_demo() and + rangebox_demo(). + +2013-10-12 Florent Rougon <f.rougon@free.fr> + + Full help support for all widgets + + * dialog.py: add support for --help-status and --help-tags. + + * dialog.py: review all widgets, making sure that all help-related + options can effectively be used (in particular 'item_help' and + 'help_status') and that the relevant output is returned to the caller in + a suitable form. + + * dialog.py: compatibility should not be affected for 'menu' nor for + basic usage when only the "help" exit code is checked, without parsing + the output. With this commit, widget-producing methods do check if the + exit code is "help" and, in that case, parse the output in order to + return a useful and easy to use Python object to the caller. This means + that "HELP " prefixes are removed, shell-style quoting and + backslash-escaping is undone, and that the output of --help-status is + returned as a hopefully useful data structure. + + * demo.py: add help support to many widgets, showing how to use the new + facilities. + +2013-10-06 Florent Rougon <f.rougon@free.fr> + + Nicer exit codes for widget-producing methods + + * The purpose of this commit is: + - to bring consistency among the exit codes (before: integers mixed + with "help" for 'menu' and "accepted"/"renamed" for 'inputmenu'; + after: all strings); + - to merge DIALOG_HELP and DIALOG_ITEM_HELP into a single high-level + code (Dialog.HELP, which is the string "help"); + - to make user code shorter and nicer (d.OK or "ok" vs. d.DIALOG_OK + for instance, where 'd' is a Dialog instance); + - to provide the exit codes as class attributes instead of instance + attributes, since they shouldn't depend on any particular instance; + - to prevent future backward-compatibility problems by raising an + exception when receiving an unknown exit code from the backend + instead of passing it through. + + Backward-compatibility is only affected for code that relies on + d.DIALOG_OK and friends being integers, or having a particular value. + Most of the code using these attributes simply compare them with the + return value (or the first element of the return value) of + widget-producing methods; this kind of use will work as before, with + only a DeprecationWarning to suggest using the new attributes (see + README.rst for instructions on how to enable DeprecationWarning's). + + * dialog.py: make widget-producing methods return a high-level "Dialog + exit code" such as "ok", "cancel", "esc", "extra" and "help" instead of + the integer exit status of the dialog backend. The Dialog exit codes are + available as Dialog.OK, Dialog.CANCEL, etc. (class attributes). The + low-level exit codes (integers) are now stored as private class + attributes: Dialog._DIALOG_OK, Dialog._DIALOG_CANCEL, etc. In order to + preserve backward-compatibility in most cases, the Dialog class has + properties named DIALOG_OK, DIALOG_CANCEL, etc. that emit a + DeprecationWarning and return the corresponding high-level code (i.e, + Dialog.OK, Dialog.CANCEL, etc.). DIALOG_HELP and DIALOG_ITEM_HELP are + both mapped to Dialog.HELP. + + * demo.py: use the new attributes of the Dialog class (OK, CANCEL, etc.) + instead of the deprecated attributes (DIALOG_OK, DIALOG_CANCEL, etc.) of + Dialog instances. Also change various strings and comments to use the + new terminology (Dialog exit code AKA high-level exit code vs. dialog + exit status AKA low-level exit code). + + * simple_example.py: ditto. + +2013-10-04 Florent Rougon <f.rougon@free.fr> + + More metadata for Dialog widget-producing methods + + * This commit prepares for a transition to user-visible Dialog exit + codes (note the capital D) that are strings, such as "ok" or "cancel". + + * dialog.py (retval_is_code): new decorator that sets the attribute of + the same name on its argument. This attribute allows to reliably detect + if a widget-producing method returns a dialog exit status or a sequence + whose first element is a dialog exit status. + + * dialog.py: apply the decorator to the relevant widget-producing + methods. + + * demo.py (MyDialog.widget_loop): use the new attribute instead of + testing whether the return value of a widget-producing method supports + indexing. + +2013-10-01 Florent Rougon <f.rougon@free.fr> + + Release 2.14.1 + +2013-10-01 Florent Rougon <f.rougon@free.fr> + + Fixes for the programbox demo + + * demo.py (programbox_demo): the programbox demo (only run in + --test-suite mode) uses subprocess.DEVNULL, which was added in Python + 3.3. Provide an alternate method when this feature is not available. + + * demo.py (programbox_demo): close the pipe used to communicate with + dialog when it is not needed anymore (otherwise, running the demo with + warnings enabled [python3 -Wd demo.py 2>/path/to/file] shows a + ResourceWarning). + +2013-10-01 Florent Rougon <f.rougon@free.fr> + + Better reporting of dialog errors + + * dialog.py: when dialog exits with status DIALOG_ERROR, write its + output as part of the DialogError exception that is raised. This should + make it much easier to understand the cause of errors. This requires + reading the dialog output before wait()ing for it to exit, which is a + good thing in any case, as big amounts of output could cause a kind of + deadlock, since dialog would be blocked with its output pipe full while + we would be wait()ing for it to exit. + +2013-09-30 Florent Rougon <f.rougon@free.fr> + + Easier management of the ChangeLog file + + * Rename ChangeLog to ChangeLog.init and modify setup.py to + automatically generate, when invoked, an up-to-date ChangeLog file with + the oldest entries from ChangeLog.init and the newest ones from the Git + log. + + * README.distributors: new file explaining this mechanism and how to + prepare a package (or new release) from the Git repository. + +2013-09-26 Florent Rougon <f.rougon@free.fr> + + Decorate Dialog.form with the 'widget' decorator + + * dialog.py: in commit cfcf412d86c5d8daebcf004d4e9fe02ecbb881b3, it was + forgotten to use the 'widget' decorator on Dialog.form. This commit + fixes this bug. + +2013-09-20 Florent Rougon <f.rougon@free.fr> + + Clarify the rules concerning the 'widget' decorator + + * dialog.py: be more precise about the interface that must be offered by + methods decorated with the 'widget' decorator (and therefore having the + 'is_widget' attribute). Also, don't use the "widget-producing" + expression to qualify methods that are not eligible for this decorator + (such as Dialog.gauge_update()). + + * demo.py: similar precisions. + +2013-09-17 Florent Rougon <f.rougon@free.fr> + + Prepare for the 2.14.0 release + + * dialog.py: minor docstring fix + + * dialog.py: bump the version number + +2013-09-17 Florent Rougon <f.rougon@free.fr> + + Add a sample program really intended for newcomers + + * simple_example.py: new sample program, short, with absolutely no + magic, intended for first-timers. + + * dialog.py: adapt the comments to point to this new file. + +2013-09-17 Florent Rougon <f.rougon@free.fr> + + Improve structure and ESC handling in the demo + + * demo.py: new MyApp class that implements the core of the demo. This + class relies on a new MyDialog class that automatically wraps every + widget-producing method of dialog.Dialog in order to display the + "confirm quit" dialog if the user presses the Escape key or the Cancel + button. This class also provides a few dialog-related methods used in + the demo. + + * demo.py: This new structure should completely fix handling of the + Escape key, which was not satisfactory in previous versions since it + required a while loop for every widget call that made the code redundant + and harder to read. The new wrapping mechanism is completely transparent + for most of the code in MyApp, which thus becomes shorter, more reliable + and easier to read. The "magic" is contained within the MyDialog class. + + * A sample program for newcomers to pythondialog with absolutely no + magic will be added in the next commit (simple_example.py). + +2013-09-15 Florent Rougon <f.rougon@free.fr> + + Add an 'is_widget' attribute to Dialog widget-producing methods + + * dialog.py (widget): new decorator to mark the Dialog methods that + provide a widget. As explained in the docstring, this allows code to + perform automatic operations on these specific methods. For instance, + one can define a class that behaves similarly to Dialog, except that + after every widget-producing call, it spawns a "confirm quit" dialog if + the widget returned DIALOG_ESC, and loops in case the user doesn't + actually want to quit. + +2013-09-14 Florent Rougon <f.rougon@free.fr> + + Check for too old versions of the backend + + * dialog.py: new exception InadequateBackendVersion + + * dialog.py: use it to abort when the user tries to use a programbox, + rangebox or treeview widget with a dialog version that does not + implement the widget in question. + + * demo.py: for the latest widgets added to dialog, check the version of + the backend if it's dialog and display an explanation instead of the + widget demo when it is too old. + +2013-09-14 Florent Rougon <f.rougon@free.fr> + + Backend version caching and comparing + + * dialog.py (Dialog.backend_version): slight modification to the API: + when the dialog-like backend does not return DIALOG_OK, raise + UnableToRetrieveBackendVersion (new exception) instead of returning + None. This way, the method either returns a string or raises an + exception. + + * dialog.py: new DialogBackendVersion class (and BackendVersion abstract + base class) for parsing the version string of the dialog backend, + storing it in a structured format, and providing easy and reliable + comparisons between versions using the standard comparison operators (<, + <=, ==, !=, >=, >). + + * dialog.py (Dialog.__init__): retrieve the backend version and store + the corresponding (Dialog)BackendVersion instance into a public + 'cached_backend_version' attribute. This should avoid having to run + '<backend> --print-version' every time someone needs the version. + + * demo.py: use Dialog.cached_backend_version. + +2013-09-11 Florent Rougon <f.rougon@free.fr> + + Minor fixes + + * dialog.py: prefix OSErrorHandling with an underscore. + + * dialog.py: rename *_rec private attributes to the corresponding *_cre + (Compiled Regular Expression). + + * dialog.py: add treeview to the list of widgets in the Dialog class + docstring (forgotten when the widget support was added). + +2013-09-11 Florent Rougon <f.rougon@free.fr> + + Misc demo improvements + + * demo.py: move a bunch of widget demos from additional_widgets() to the + main demo() function to give them more visibility. The remaining widgets + in additional_widgets() either have little warts (like programbox which + is waiting for a fix in the dialog backend), are cumbersome to use in + the demo (this is the case of progressbox_demo_with_filepath), or are + almost identical to widgets already presented in the main part of the + demo. + + * demo.py: use the current local time to initialize the timebox widget. + + * demo.py: minor improvements. + +2013-09-11 Florent Rougon <f.rougon@free.fr> + + Add support for 'treeview' + +2013-09-10 Florent Rougon <f.rougon@free.fr> + + Add support for 'rangebox' + + Add support for 'programbox' + +2013-09-07 Florent Rougon <f.rougon@free.fr> + + Factor out OSError and IOError handling + + * dialog.py: use a context manager to factor out OSError and IOError + handling throughout the module. + +2013-09-06 Florent Rougon <f.rougon@free.fr> + + Add support for new dialog common options + + * dialog.py: add support for --default-button and --no-tags. + +2013-09-06 Florent Rougon <f.rougon@free.fr> + + Add version_info to dialog.py + + * dialog.py: new 'version_info' module-level attribute, similar to what + the 'sys' module provides. This avoids the need to parse __version__ + when one wants to extract for instance the major and/or minor version + number of pythondialog. + +2013-09-06 Florent Rougon <f.rougon@free.fr> + + Minor improvements + +2013-08-22 Florent Rougon <f.rougon@free.fr> + + Remove the installation prefix from setup.cfg and release 2.13.1 + + * From now on, pythondialog releases will follow a major.minor.micro + versioning scheme. + + * setup.cfg: remove the /usr/local installation prefix that breaks + installations with pip in a virtualenv, at least. + + * INSTALL: update the installation and uninstallation instructions to + document and favor the method based on pip. + +2013-08-20 Florent Rougon <f.rougon@free.fr> + + Demo improvement with Dialog.maxsize() + + * demo.py: pass the available (terminal-dependent) width and height to + demo() and additional_widgets(); use this information to display a + tailbox widget that almost fills the screen (whatever the terminal + size). + +2013-08-19 Florent Rougon <f.rougon@free.fr> + + Release 2.13 + + * setup.cfg: don't build zip files anymore, as I don't think anyone + using pythondialog could be unable to uncompress a tar.gz or tar.bz2 + file. + + * demo.py: "Mayonnaise" is spelt with two n's, dammit! + +2013-08-19 Florent Rougon <f.rougon@free.fr> + + New method Dialog.set_background_title(), better Dialog doc + + * dialog.py (Dialog.set_background_title): new method. Now that we deal + with dash escaping, such a method does become useful since it avoids + users having to use the low-level Dialog.add_persistent_args() and + bother with dash escaping. + + * dialog.py (Dialog.setBackgroundTitle): this old method from + pythondialog 1.0 is still deprecated because its case style is + inconsistent with the rest of pythondialog; it simply calls + Dialog.set_background_title() after printing a DeprecationWarning. + + * dialog.py (Dialog): add set_background_title, maxsize and + backend_version to the docstring and reorganize a bit the list of public + methods. + + * demo.py: use the new method. + +2013-08-19 Florent Rougon <f.rougon@free.fr> + + Add demo code for the timebox widget + +2013-08-16 Florent Rougon <f.rougon@free.fr> + + Prepare for the 2.12 release + + * dialog.py: add a copyright notice for Peter Åstrand because of commit + v2.09-2-ga3429a4. + + * setup.py: put python3 in the shebang line; change 'url' and + 'download_url' to point to SourceForge. + + * Minor documentation updates and editorial changes for the release. + +2013-08-16 Florent Rougon <f.rougon@free.fr> + + Provide an implementation of textwrap.indent() in demo.py + + This is for Python versions < 3.3, where this function is not in the + standard library. + +2013-08-16 Florent Rougon <f.rougon@free.fr> + + Make debugging easier + + * dialog.py: there is no need to copy/paste Python code from the + DEBUGGING file into dialog.py anymore in order to see the command lines + corresponding to the dialog calls. Just call + d.setup_debug(True, file=<file object>) to turn debugging on and + d.setup_debug(False) to turn it off, where d is obviously a Dialog + instance. + + * demo.py: new options --debug and --debug-file to activate debugging + and specify where the debug log is written to. + + * DEBUGGING: not useful anymore. + * MANIFEST.in: don't ship DEBUGGING anymore. + + * README.rst: mention this in a new "Troubleshooting" section. + +2013-08-15 Florent Rougon <f.rougon@free.fr> + + New dialog.__version__ attribute + + * dialog.py: new dialog.__version__ attribute + + * demo.py: use it to print the pythondialog version in the first screen + + * setup.py: use dialog.__version__ + +2013-08-15 Florent Rougon <f.rougon@free.fr> + + Add support for --print-version and --print-maxsize + + * dialog.py (Dialog.backend_version, Dialog.maxsize): new methods that + respectively extract the relevant info from dialog --print-version and + --print-maxsize. + + * demo.py: use them to display the backend version and terminal size in + the first screen (+ Python version as a bonus); also warn if the + terminal is too small, which might be the cause of Ubuntu#694824. + +2013-08-15 Florent Rougon <f.rougon@free.fr> + + New arg 'use_persistent_args' for _call_program() and _perform() + + * dialog.py (Dialog._call_program, Dialog._perform): new argument + 'use_persistent_args' that allows to skip the insertion of + dialog_persistent_arglist. This is necessary to use for instance + --print-version without emptying dialog_persistent_arglist first. + + * dialog.py (Dialog._call_program, Dialog._perform): to enhance + readability and maintainability, make all keyword arguments of these + methods keyword-only. + +2013-08-13 Florent Rougon <f.rougon@free.fr> + + Add support for common options from dialog 1.1-20120215 + + * dialog.py: add support for dialog common options --ascii-lines, + --colors, --column-separator, --date-format, --exit-label, + --extra-button, --extra-label, --hfile, --hline, --keep-tite, + --keep-window, --no-collapse, --no-lines, --no-mouse, --no-nl-expand, + --no-ok, --scrollbar, --time-format, --trace and --visit-items + (closes: Ubuntu#739873). + +2013-08-12 Florent Rougon <f.rougon@free.fr> + + Improve exception handling + + * dialog.py: don't assume that exceptions are strings; thus, use str() + to obtain the message when translating from a "foreign" exception to a + subclass of dialog.error. + + * dialog.py: use str(e) instead of e.strerror when e is an OSError + instance, because e.strerror does not contain the file name (even when + there is one available in e.filename), contrary to str(e). + + * dialog.py: prepare the transition from PythonDialogIOError to + PythonDialogOSError. PythonDialogIOError is now a subclass of + PythonDialogOSError so that users can safely replace "except + PythonDialogIOError" clauses with "except PythonDialogOSError" even if + running under Python < 3.3. pythondialog will raise PythonDialogOSError + instead of PythonDialogIOError when Python stops distinguishing between + IOError and OSError, i.e. when running under Python 3.3 or later. + + * dialog.py: use "raise ... from ..." (available in Python >= 3.0) where + appropriate. + +2013-08-11 Florent Rougon <f.rougon@free.fr> + + Conform to the gauge protocol described in dialog(1) + + * dialog.py (Dialog.gauge_update): actually conform to the gauge + protocol described in dialog(1); this fixes a slight synchronization bug + when 'update_text' is True (closes: SF#3; thanks to MapK for spotting + this and providing a patch). + + * dialog.py (Dialog.gauge_update): raise BadPythonDialogUsage if the + 'percent' argument is not an integer, since this is the only type + accepted by the gauge protocol as described in dialog(1). + +2013-08-10 Florent Rougon <f.rougon@free.fr> + + Escape non-option arguments to dialog that start with -- + + * dialog.py (_dash_escape): new function that inserts a '--' element + before every element in a list that starts with '--' (returns a new + list). + + * dialog.py (_dash_escape_nf): same as _dash_escape, but ignores the + first element. + + * dialog.py (Dialog.dash_escape, Dialog.dash_escape_nf): new class + methods for public use, for those using Dialog.add_persistent_args(). + + * dialog.py (Dialog._call_program): new keyword argument 'dash_escape' + to control the dash-escaping of the argument list; by default, the + appropriate part is filtered through _dash_escape_nf(). + + * dialog.py (Dialog._perform): new keyword argument 'dash_escape' that + is passed as is to Dialog._call_program() (closes: SF#5; thanks to + Denilson Figueiredo de Sá for the report). + + * dialog.py: use _dash_escape_nf() where appropriate in + _common_args_syntax for options that are passed as keyword arguments + (e.g., title="..."). + + * dialog.py (Dialog.setBackgroundTitle): use self.dash_escape_nf() in + this long-deprecated method. + +2013-07-28 Florent Rougon <f.rougon@free.fr> + + Standard-behaving __str__ and __repr__ for our exceptions + + * dialog.py:error.__str__(): return the exception message instead of + something enclosed in angle brackets. + + * Compatibility is not affected unless one had the ill-advised idea to + parse the return value of __str__. The complete_message() method of + exceptions behaves exactly as before. + + * demo.py: use the new, more standard method, to retrieve the error + message. + +2013-07-28 Florent Rougon <f.rougon@free.fr> + + Use os.path.abspath(__file__) to refer to the demo source file + + * This avoids problems when running the demo from a directory that is + not the one containing demo.py (closes: SF#4). + +2013-07-27 Florent Rougon <f.rougon@free.fr> + + * README: rename to README.rst, convert to reStructuredText and + update for Python 3 + * INSTALL: update for Python 3 + * MANIFEST.in: include README.rst + * setup.py: update for Python 3 and the README -> README.rst + change, email address update + +2013-06-19 Florent Rougon <f.rougon@free.fr> + + Replace the question for the calendar demo with a non-perishable one + + * demo.py: the question about an estimate of Debian *** release is + bound to become out-of-date. Replace it with a question that will + not need a change every couple of years. + +2013-06-19 Florent Rougon <f.rougon@free.fr> + + Minor improvements + + * demo.py: use locale.setlocale(locale.LC_ALL, '') to properly + initialize the locales + + * dialog.py: code readability improvement + +2013-06-19 Florent Rougon <f.rougon@free.fr> + + Minor bug fixes + + * dialog.py(_to_onoff): regexp match against "on$"/"off$" instead + of "on"/"off". + + * dialog.py: more rigorous handling of "title" and "help_button" + keyword arguments. For instance, passing 'title=None' to a + function that gets this argument through its **kwargs results in a + kwargs dictionary that does contain a "title" key (with None as + the corresponding value); therefore '"title" not in kwargs' is not + a great test to determine if the title should be set to a default + value. + +2013-06-19 Florent Rougon <f.rougon@free.fr> + + * Favor usage of True/False over 1/0 and on/off in dialog.py and + demo.py, docstrings included. + +2013-06-19 Florent Rougon <f.rougon@free.fr> + + Improve error handling + + * dialog.py(_to_onoff): actually raise an exception when the given + parameter is a string that doesn't represent a boolean according + to the regexps. + + * demo.py: when handling exceptions at top level, start by + printing a traceback, which is always very practical to find the + origin of problems. + +2013-06-18 Florent Rougon <f.rougon@free.fr> + + * Replace assert statements with PythonDialogBug exceptions. + + This is less practical for coding in dialog.py but makes it easier + for dialog.py users to catch all (most) exceptions that come from + pythondialog. + +2013-06-18 Florent Rougon <f.rougon@free.fr> + + Cosmetic changes + + * Change deprecated %u into %d. + + * Remove trailing whitespace. + + * Coding-style update for the generic exception. + +2013-06-18 Florent Rougon <f.rougon@free.fr> + + Improve error reporting between fork() and execve() + + * Print a traceback if an error happens between fork() and + execve() when calling dialog. + +2013-06-18 Florent Rougon <f.rougon@free.fr> + + * Basic porting to Python 3. + +2010-03-17 Florent Rougon <f.rougon@free.fr> + + * dialog.py: Move the definition of _simple_option() before its uses, + which according to + http://sourceforge.net/tracker/index.php?func=detail&aid=1679190&group_id=58155&atid=486715 + "makes pydev happy when browsing the code". + + * dialog.py: Remove the "True = 0 == 0" and "False = 0 == 1" + compatibility measures that were still in place for Python + versions < 2.3. Such old versions are not supported anymore; besides, + True and False are becoming reserved words in Python 3. + +2010-03-16 Florent Rougon <f.rougon@free.fr> + + * Release 2.11. + + * README: I thought I was adding a valuable precision in version 2.09 + when I changed "LGPL" to "LGPL version 2.1" in the README file (after + looking at COPYING), but actually, the terms at the beginning of + dialog.py are "either version 2.1 of the License, or (at your option) + any later version". Sorry about that, fixed. + + * demo.py: use "if <test> <expr1> else <expr2>" expressions more + often, since they are allowed in Python >= 2.5... + +2010-03-16 Florent Rougon <flo@via.ecp.fr> + + * Release 2.10. + + * dialog.py: add Peter Åstrand's modifications to deal with + Xdialog's incompatibilities with respect to dialog: + - new "use_stdout" keyword argument for Dialog.__init__() + - factoring of final newline removals with the addition of + Dialog._strip_xdialog_newline(). + + * dialog.py: add support for dialog options --no-label, + --yes-label and --insecure (the last one not being so dangerous as + the name seems to imply: "Makes the password widget friendlier but + less secure, by echoing asterisks for each character"; BTW, kdm + has a very nice way of handling this issue IMO: an option that + echoes each character with x asterisks, where x = 3 IIRC). + +2010-03-14 Florent Rougon <flo@via.ecp.fr> + + * Release 2.09. + + * dialog.py: new supported widgets: editbox, inputmenu, mixedform, + mixedgauge, pause, passwordform, progressbox. + + * demo.py: general improvements, such as: + - hopefully more helpful dialog when the user fails to select + a file in fselect; + - since actually selecting a file with this widget is already + boring after the second time, the widget can be exited by + pressing Esc or the Cancel button (in which case the parts + of the demo that need the file path will be skipped). + - replace a few calls to Dialog.scrollbox() by calls to + Dialog.msgbox(), since the latter provides nice automatic + line wrapping. + + * demo.py: support GNU-style option passing with getopt.py; you + can use --help to get a list of available options + * demo.py: no need to change a global variable anymore to switch + the demo to "fast mode", just use --fast + * demo.py: add --test-suite mode, mainly for developers: it + tests *all* widgets, not only those included in the "default + mode", and automatically enables "fast mode". + + * dialog.py(__call_program): make the function more generic. stdin + redirection doesn't involve automatic pipe(2) creation in + __call_program() anymore; instead, __call_program expects a file + descriptor when it is asked to redirect dialog's stdin (parameter + 'redir_child_stdin_from_fd'). + + The caller may still decide to create a pipe and pass its file + descriptor for reading as the 'redir_child_stdin_from_fd' + parameter, but the new possibility of redirecting dialog's stdin + from an arbitrary file descriptor allows for instance to redirect + it from an existing file, network socket... This is used to + implement --progressbox cleanly. + + This change has the additional benefit of simplifying the API, + since __call_program()'s return value is always a 2-element tuple + now. + + * dialog.py(__call_program): new close_fds option causing the + child process to close the specified file descriptors before the + execve(2) system call. This is useful for instance to have the + child close an end of a pipe he isn't going to use. Without that, + deadlocks could happen because of the child never seeing EOF from + the pipe. + + * dialog.py: use warnings.warn(..., DeprecationWarning) for + obsolete functions. + + * dialog.py: remove convoluted syntax *(<list>,) that was used at + several places. I don't see any use for this syntax anymore, and + changing it to simply <list> didn't make the universe collapse (so + far). + + * dialog.py: prefix attributes for internal use (such as + Dialog._call_program) with a single underscore instead of a double + one: we don't need the name mangling here. These underscores in + dialog.py are just an indication that the attribute is "internal" + and thus subject to API changes, etc. Thanks to Peter Åstrand for + pointing this out. + + * setup.py: + - improve the long description, use ReStructuredText + - add Trove classifiers + - add download_url + + * Review and update README, TODO... + +2010-02-19 Florent Rougon <flo@via.ecp.fr> + + * dialog.py: add support for --dselect + * dialog.py: add support for DIALOG_ITEM_HELP + * demo.py: small fixes + +2009-10-31 Florent Rougon <flo@via.ecp.fr> + + * Released 2.08, skipping version 2.07 to avoid creating confusion + with the 2.7 version released by Peter Åstrand in 2004. + +2009-02-04 Florent Rougon <flo@via.ecp.fr> + + * Add support for --form. + + * dialog.py(__call_program): compute the argument list before + forking, otherwise things get difficult to understand if this + computation raises an exception. + +2004-03-29 Florent Rougon <flo@via.ecp.fr> + + * Released 2.06. + +2004-03-19 Florent Rougon <flo@via.ecp.fr> + + * dialog.py: fixed a bug with the default_item "common argument" + (corresponding to dialog's --default-item option) thanks to Peter + Mathiasson. + +2004-03-16 Florent Rougon <flo@via.ecp.fr> + + * demo.py: make sure the directory passed to --fselect ends with + os.sep so that its contents can be seen right away in the file + selection box displayed by dialog. + +2004-03-15 Florent Rougon <flo@via.ecp.fr> + + * dialog.py: fix a bug (the standard output of the process running + dialog used to be connected to a pipe) that rendered pythondialog + unusable with recent versions of dialog (thanks, Peter Åstrand!). + + * dialog.py: the generic exception is now 'error' ('dialog.error', + if you understand this better). The old name is still there for + backward-compatibility. + * Several new exceptions have appeared: PythonDialogSystemError, + PythonDialogIOError, PythonDialogOSError, + PythonDialogErrorBeforeExecInChildProcess, + PythonDialogReModuleError, UnableToCreateTemporaryDirectory, + PythonDialogBug, ProbablyPythonBug. + * dialog.py: the ExecutableNotFound exception was not mentioned in + the docstring module. Fixed. + * dialog.py: the complete_message() of 'error' instances does not + write a dot at the end of the message anymore. + * dialog.py: rename the ExceptionPrettyIdentifier attribute of + 'error' subclasses to ExceptionShortDescription. + * dialog.py: the constructor of 'error' instances can now be + called with no argument if no useful message can be added to the + ExceptionShortDescription. + + * dialog.py: added the new DIALOG_EXTRA and DIALOG_HELP return + codes that appeared somewhere between versions 0.9a-20020309a and + 0.9b-20040301 of dialog. + + * dialog.py: Remove the possibility to choose (from Dialog's + constructor) the values of the DIALOG_{OK,CANCEL,ERROR,...} + environment variables passed to dialog because this is absolutely + useless as far as I can see and clutters the API. + + * dialog.py: Dialog's constructor has a new 'compat' parameter + that can be used to enable a compatibility mode with dialog-like + programs whose interface is only slightly different from that of + dialog. The demo runs fine with Xdialog 2.0.6 in the "Xdialog" + compatibility mode. However, I don't want the special cases to + expand too much, so it would be really better for you to report + bugs if your dialog-like program is not dialog-compatible! + + * dialog.py: don't try any PATH components (nor any components + from ":/bin:/usr/bin" if PATH is undefined) if the 'dialog' + parameter of Dialog's constructor contains a '/'. + + * dialog.py: somewhere between versions 0.9a-20020309a and + 0.9b-20040301, dialog started not to quote tags in the output of + --checklist if they didn't contain any space. Adapt the dialog + invocation (now using --separate-output) and the output parsing + accordingly (still works with 0.9a-20020309a). + + * dialog.py: the data fed to dialog in Dialog.gauge_update() + happened to work with dialog 0.9a-20020309a but did not conform to + the manual page and presumably broke Xdialog. This is fixed, + thanks to Peter Åstrand. + + * dialog.py: modifications in Dialog.scrollbox(): + - don't use the old insecure tempfile.mktemp() anymore + (tempfile.mkstemp() didn't exist before Python 2.3) + - don't display a title for the box if no title was in kwargs + - the return value is now that of the dialog-like program + - UnableToCreateTemporaryDirectory is now raised if for some + strange reason, we cannot create a temporary directory + + * dialog.py: review every function to catch possible exceptions + such as IOError and OSError, in order to turn them into subclasses + of 'error' (such as PythonDialogIOError and PythonDialogOSError). + Rare beasts such as MemoryError are still not caught. Such an + enterprise would be unreasonable, if not simply impossible. + * dialog.py: updated the various docstrings to show which + exceptions every function can raise. To make this manageable, many + functions refer to the docstrings of internal, heavily-used + functions such as Dialog.__perform whose docstrings are, + unfortunately but rightfully, not included in the HTML + documentation generated by pydoc. Some sort of automated + documentation generation system would be needed to solve this + problem in a satisfactory way. + + * demo.py: minor update with respect to Debian sarge's release + date forecasts... + +2003-09-16 Florent Rougon <flo@via.ecp.fr> + + * Released 2.05. + + * Changed the "private" class names to start with two underscores + instead of one. + +2003-09-01 Florent Rougon <flo@via.ecp.fr> + + * Released 2.04. + + * dialog.py: Replaced the apply() calls with calls using the + "extended call syntax" since apply() is deprecated since the + release of Python 2.3. + +2002-09-05 Florent Rougon <flo@via.ecp.fr> + + * Released 2.03. + + * dialog.py: Reorganized the documentation between the module's + docstring and that of the Dialog class. + + * README: Added the "history" section and other improvements. + +2002-09-04 Florent Rougon <flo@via.ecp.fr> + + * dialog.py: Prefixed global variables with an underscore and + made some cosmetic fixes. + + * dialog.py: Removed the set_background_title alias to + setBackgroundTitle since nobody uses it yet and setBackgroundTitle + is obsolete. Even though I prefer set_background_title to + setBackgroundTitle for consistency with other methods, there is no + point in adding an obsolete method. + +2002-09-03 Florent Rougon <flo@via.ecp.fr> + + * Packaged pythondialog with Distutils. + + * Improved the README. + +2002-09-02 Florent Rougon <flo@via.ecp.fr> + + * dialog.py: Improved the common_args_syntax initialization so + that simple dialog Common Options can be passed as foo=1 keyword + arguments with 1 having a real boolean meaning (was not the case + before). + + * Handled the licensing stuff : GNU LGPL license for dialog.py and + public domain for demo.py. Added the COPYING file. + + * dialog.py: Wrote a proper module docstring. + + * demo.py: Added the last widgets to the demo. + +2002-08-30 Florent Rougon <flo@via.ecp.fr> + + * dialog.py: Added the calendar, fselect, passwordbox, tailbox and + timebox widgets. + +2002-08-29 Florent Rougon <flo@via.ecp.fr> + + * dialog.py (_wait_for_program_termination): Raises a DialogError + exception when dialog returns self.DIALOG_ERROR. Exceptions are so + handy. + + * demo.py: Cleaned up the demo code making it easier (I hope) to + focus on a given widget, added error handling. + * dialog.py: Took the demo to put it in a new, separate file: + demo.py. + + +2002-08-21 Florent Rougon <flo@via.ecp.fr> + + * dialog.py: Documented the widgets properly. + * dialog.py: Generalized the widgets that didn't offer all the + capabilities available from dialog. + * dialog.py: Went through the various widgets so that the + associated methods return the dialog exit status (with additional + information, if relevant). + + * dialog.py: Finally cleaned up the method used to collect the + arguments given to dialog. They are now stored as elements of a + list and properly quoted to protect them from shell expansion just + before the call to popen3. Now, you should be safe with any + character in the strings passed to dialog (labels, texts, etc.). + +2002-08-09 Florent Rougon <flo@via.ecp.fr> + + * dialog.py: All boxes should now support the common arguments + scheme (well, I still have to look at the gauge closer). + + * dialog.py: Added support for "common arguments"; now, you can + use any combination of the _common options_ for the dialog program + when creating a box. Example: + + d.checklist(<usual args for a checklist box>, + title="...", backtitle="...", <other common args>) + + One thing is still broken: we must shell-escape the strings so + that apostrophes (') used to delimit shell arguments don't clash + with apostrophes used these arguments themselves. + + * dialog.py: Added support for "persistent arguments" arguments + (--backtitle is a good candidate for this). + + * dialog.py: Removed a leftover debugging print in _perform. + + * dialog.py: Simplified the DIALOG* arguments handling. + +2002-08-08 Florent Rougon <flo@via.ecp.fr> + + * dialog.py: Added a short docstring for the module. + + * dialog.py: Rewrote checklist to get all the features from the + corresponding option in dialog. + + * dialog.py: Rewrote _perform; now, we can use the DIALOG* + environment variables set in the constructor, which allows us + distinguish between ESC pressed and a dialog error, among others; + also, we use no temporary file anymore to store dialog's stderr. + + * dialog.py: Added some exceptions. + + * dialog.py: Rewrote __init__; now, we can set choose in the + constructor the DIALOG* environment variables to pass the dialog + program as well as this program (could be whiptail for instance) + from the constructor (and the environment variables can be + different for two Dialog instances used in the same python + process...). + + * dialog.py: Renamed __foo methods to _foo. + +2002-08-07 Florent Rougon <flo@via.ecp.fr> + + * dialog.py: Cosmetic fixes. + + * dialog.py: Added a proper GPL header (dialog.py only had a short + mention about its license being the GNU GPL). + + * Split dialog.py, creating a Changelog file in the format + described in the GNU Coding Standards, as well as README, AUTHORS, + COPYING and TODO. + +2000-07-30 Sultanbek Tezadov (http://sultan.da.ru/) + + * dialog.py: Added the gauge widget. + + * dialog.py: Added a 'title' option to some widgets. + + * dialog.py: Added a 'checked' option to the checklist dialog; + clicking "Cancel" is now recognizable. + + * dialog.py: Added a 'selected' option to the radiolist dialog; + clicking "Cancel" is now recognizable. + + * dialog.py: Some other cosmetic changes and improvements. + +2000-??-?? Robb Shecter <robb@acm.org> + + * Initial release. + + +# Local Variables: +# coding: utf-8 +# End: diff --git a/pythondialog/INSTALL b/pythondialog/INSTALL @@ -0,0 +1,159 @@ +INSTALLATION +============ + +pythondialog is packaged with Distutils. With the current state of +Python packaging and installation tools, there are several ways to +install pythondialog from source. + +Probably, the easiest and cleanest method at this date (April 2015) is +to use pip, possibly inside a virtual environment created with +virtualenv or venv.py/pyvenv. Typically, assuming you have a working pip +setup (see below), you just have to run one of the following commands: + + pip install pythondialog (which normally installs from PyPI) + +or + + pip install /path/to/python3-pythondialog-X.Y.Z.tar.gz + +or + + pip install https://url/to/python3-pythondialog-X.Y.Z.tar.gz + +or + + pip install http://url/to/python3-pythondialog-X.Y.Z.tar.gz + +Notes: + - old versions of pip don't support https; + - upgrades can be done by passing '--upgrade' or '-U' to the + 'pip install' command; please refer to the pip documentation for + details. + +Uninstallation should be as easy as: + + pip uninstall pythondialog + +For more information about pip, venv.py/pyvenv and virtualenv, you can +visit the following pages: + + <https://pip.pypa.io/> + <https://docs.python.org/3/library/venv.html> + <https://virtualenv.pypa.io/> + + +So, what is a "working pip setup"? +---------------------------------- + +What I call a "working pip setup" here is a setup where a command that +is typically something like 'pip', 'pip3' or 'pip3.4' (for a Python 3.4 +installation) can be run to safely install, upgrade and remove packages +into or from a particular Python installation. + +Normally, a given 'pip' executable is tied to a particular Python +installation that can be discovered by running 'pip --version' (or +'pip3 --version', etc., depending on the particular 'pip' executable +name; for sanity's sake, we'll assume 'pip' in the rest of this +document). For instance, the following would indicate a Python 3.4 +installation based at /some/path: + + % pip --version + pip 1.5.6 from /some/path/lib/python3.4/site-packages (python 3.4) + +In general, it is safe to run 'pip' from a Python interpreter you +installed yourself without a Linux package manager or similar. It is +also safe if the Python interpreter running 'pip' lives in a virtual +environment created with virtualenv or venv.py/pyvenv. The one case +where you should probably avoid using a given 'pip' executable is if it +runs directly under a system Python (typically, /usr/bin/python or +/usr/bin/pythonX.Y) installed with the package manager of your Linux +distribution (or any other OS, for that matter): in such a situation, +'pip' might, if run with superuser privileges, mess with files under the +control of the OS package manager (i.e., 'dpkg' on Debian and its +derivatives, 'rpm' on Redhat and Suse, etc.). + +Depending on how you installed Python, you may need superuser privileges +to install, upgrade or remove Python packages inside that installation. +For instance, this will be the case if you compiled Python yourself and +performed the installation step ('make install') as root. However, +running 'pip' with superuser privileges should not be necessary nor +desirable inside a virtual environment created with virtualenv or +venv.py/pyvenv. + + +Old way, without pip +-------------------- + +The following instructions explain how to install pythondialog directly +from its setup.py, without using pip. If possible, the method based on +pip is preferable because it makes uninstallation standard and easy. + +Here are the steps: + - make this file's directory your shell's current directory + - optionally edit setup.cfg (cf. the "Installing Python Modules" + chapter of the Python documentation). + + - a) 1) type: + + python3 ./setup.py build + + (depending on your system and the Python version you want to + install for, you may have to replace "python3" with, for + instance, "python3.2" or "python3.3") + + 2) then, as root (after replacing /usr/local with the actual + installation prefix you want to use): + + python3 ./setup.py install --prefix=/usr/local \ + --record /path/to/foo + + where foo is a file of your choice which will contain the list + of all files installed on your system by the preceding + command. This will make uninstallation easy (you could ommit + the "--record /path/to/foo", but uninstallation could not be + automated, then). + + OR + + b) type, as root (after replacing the installation prefix): + + python3 ./setup.py install --prefix=/usr/local \ + --record /path/to/foo + + This will automatically build the package before installing it. + The observations made in a) also apply here. + + +If this default installation is not what you wish, please read the +Distutils documentation which should be available in the "Installing +Python Modules" chapter of the Python documentation. + + +UNINSTALLATION +============== + +If you installed pythondialog with pip, you can uninstall it with the +following command: + + pip uninstall pythondialog + +(which should be run under the same account that was used to run the +"pip install" command) + +Otherwise, if you have followed the old installation procedure, you have +a /path/to/foo file that contains all the files the installation process +put on your system. Great! All you have to do is: + + while read file; do rm -f "$file"; done < /path/to/foo + +under a POSIX-style shell and with appropriate privileges (maybe root, +depending on how you installed pythondialog). + +Note: this will handle file names with spaces correctly, unlike the +simpler "rm -f $(cat /path/to/foo)". + + +# Local Variables: +# coding: utf-8 +# fill-column: 72 +# End: diff --git a/pythondialog/MANIFEST.in b/pythondialog/MANIFEST.in @@ -0,0 +1,6 @@ +include README.rst README.distributors COPYING COPYING.Sphinx INSTALL AUTHORS +include TODO ChangeLog MANIFEST.in +recursive-include examples *.py +recursive-include doc *.py *.txt *.rst *.png Makefile +prune doc/screenshots/thumbs +prune doc/_build diff --git a/pythondialog/PKG-INFO b/pythondialog/PKG-INFO @@ -0,0 +1,303 @@ +Metadata-Version: 1.1 +Name: pythondialog +Version: 3.4.0 +Summary: A Python interface to the UNIX dialog utility and mostly-compatible programs +Home-page: http://pythondialog.sourceforge.net/ +Author: Florent Rougon +Author-email: f.rougon@free.fr +License: UNKNOWN +Download-URL: http://sourceforge.net/projects/pythondialog/files/pythondialog/3.4.0/python3-pythondialog-3.4.0.tar.bz2 +Description: =============================================================================== + Python wrapper for the UNIX "dialog" utility + =============================================================================== + Easy writing of graphical interfaces for terminal-based applications + ------------------------------------------------------------------------------- + + Overview + -------- + + pythondialog is a Python wrapper for the UNIX dialog_ utility + originally written by Savio Lam and later rewritten by Thomas E. Dickey. + Its purpose is to provide an easy to use, pythonic and as complete as + possible interface to dialog_ from Python code. + + .. _dialog: http://invisible-island.net/dialog/dialog.html + + pythondialog is free software, licensed under the GNU LGPL (GNU Lesser + General Public License). Its home page is located at: + + http://pythondialog.sourceforge.net/ + + and contains a `short example`_, screenshots_, a `summary of the recent + changes`_, links to the `documentation`_, the `Git repository`_, the + `mailing list`_, the `issue tracker`_, etc. + + .. _short example: http://pythondialog.sourceforge.net/#example + .. _screenshots: http://pythondialog.sourceforge.net/gallery.html + .. _summary of the recent changes: + http://pythondialog.sourceforge.net/news.html + .. _documentation: http://pythondialog.sourceforge.net/doc/ + .. _Git repository: https://sourceforge.net/p/pythondialog/code/ + .. _mailing list: https://sourceforge.net/p/pythondialog/mailman/ + .. _issue tracker: https://sourceforge.net/p/pythondialog/_list/tickets + + If you want to get a quick idea of what this module allows one to do, + you can download a release tarball and run ``demo.py``:: + + PYTHONPATH=. python3 examples/demo.py + + + What is pythondialog good for? What are its limitations? + -------------------------------------------------------- + + As you might infer from the name, dialog is a high-level program that + generates dialog boxes. So is pythondialog. They allow you to build nice + interfaces quickly and easily, but you don't have full control over the + widgets, nor can you create new widgets without modifying dialog itself. + If you need to do low-level stuff, you should have a look at `ncurses`_ + (cf. the ``curses`` module in the Python standard library), `blessings`_ + or slang instead. For sophisticated text-mode interfaces, the `Urwid + Python library`_ looks rather interesting, too. + + .. _ncurses: http://invisible-island.net/ncurses/ncurses.html + .. _blessings: https://github.com/erikrose/blessings + .. _Urwid Python library: http://excess.org/urwid/ + + + Requirements + ------------ + + * As of version 2.12, the reference implementation of pythondialog + (which this file belongs to) requires Python 3.0 or later in the 3.x + series. pythondialog 3.4.0 has been tested with Python 3.5. + + * However, in order to help users who are somehow forced to still use + Python 2 (even though Python 3.0 was released on December 3, 2008), a + backport of the reference implementation to Python 2 has been + prepared. At the time of this writing, the latest pythondialog version + backported this way is 3.4.0. For up-to-date information about this + backport, please visit the `pythondialog home page`_. + + .. _pythondialog home page: http://pythondialog.sourceforge.net/ + + * Apart from that, pythondialog requires the dialog_ program (or a + drop-in replacement for dialog). You can download dialog from: + + http://invisible-island.net/dialog/dialog.html + + Note that some features of pythondialog may require recent versions of + dialog. + + + Quick installation instructions + ------------------------------- + + If you have a working `pip <https://pypi.python.org/pypi/pip>`_ setup, + you should be able to install pythondialog with:: + + pip install pythondialog + + When doing so, make sure that your ``pip`` executable runs with the + Python 3 installation you want to install pythondialog for. + + For more detailed instructions, you can read the ``INSTALL`` file from a + release tarball. You may also want to consult the `pip documentation + <https://pip.pypa.io/>`_. + + + Documentation + ------------- + + The pythondialog Manual + ^^^^^^^^^^^^^^^^^^^^^^^ + + The pythondialog Manual is written in `reStructuredText`_ format for the + `Sphinx`_ documentation generator. The HTML documentation for the latest + version of pythondialog as rendered by Sphinx should be available at: + + http://pythondialog.sourceforge.net/doc/ + + .. _pythondialog Manual: http://pythondialog.sourceforge.net/doc/ + .. _reStructuredText: http://docutils.sourceforge.net/rst.html + .. _Sphinx: http://sphinx-doc.org/ + .. _LaTeX: http://latex-project.org/ + .. _Make: http://www.gnu.org/software/make/ + + The sources for the pythondialog Manual are located in the ``doc`` + top-level directory of the pythondialog distribution, but the + documentation build process pulls many parts from dialog.py, mainly + docstrings. + + To generate the documentation yourself from dialog.py and the sources in + the ``doc`` directory, first make sure you have `Sphinx`_ and `Make`_ + installed. Then, you can go to the ``doc`` directory and type, for + instance:: + + make html + + You will then find the output in the ``_build/html`` subdirectory of + ``doc``. `Sphinx`_ can build the documentation in many other formats. + For instance, if you have `LaTeX`_ installed, you can generate the + pythondialog Manual in PDF format using:: + + make latexpdf + + You can run ``make`` from the ``doc`` directory to see a list of the + available formats. Run ``make clean`` to clean up after the + documentation build process. + + For those who have installed `Sphinx`_ but not `Make`_, it is still + possible to build the documentation with a command such as:: + + sphinx-build -b html . _build/html + + run from the ``doc`` directory. Please refer to `sphinx-build`_ for more + details. + + .. _sphinx-build: http://sphinx-doc.org/invocation.html + + + Reading the docstrings from an interactive Python interpreter + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + If you have already installed pythondialog, you may consult its + docstrings in an interactive Python interpreter this way:: + + >>> import dialog; help(dialog) + + but only parts of the documentation are available using this method, and + the result is much less convenient to use than the `pythondialog + Manual`_ as generated by `Sphinx`_. + + + Enabling Deprecation Warnings + ----------------------------- + + There are a few places in ``dialog.py`` that send a + ``DeprecationWarning`` to warn developers about obsolete features. + However, because of: + + - the dialog output to the terminal; + - the fact that such warnings are silenced by default since Python 2.7 + and 3.2; + + you have to do two things in order to see them: + + - redirect the standard error stream to a file; + - enable the warnings for the Python interpreter. + + For instance, to see the warnings produced when running the demo, you + can do:: + + PYTHONPATH=. python3 -Wd examples/demo.py 2>/path/to/file + + and examine ``/path/to/file``. This can also help you to find files that + are still open when your program exits. + + **Note:** + + If your program is terminated by an unhandled exception while stderr + is redirected as in the preceding command, you won't see the traceback + until you examine the file stderr was redirected to. This can be + disturbing, as your program may exit with no apparent reason in such + conditions. + + For more explanations and other methods to enable deprecation warnings, + please refer to: + + http://docs.python.org/3/whatsnew/2.7.html + + + Troubleshooting + --------------- + + If you have a problem with a pythondialog call, you should read its + documentation and the dialog(1) manual page. If this is not enough, you + can enable logging of shell command-line equivalents of all dialog calls + made by your program with a simple call to ``Dialog.setup_debug()``, + first available in pythondialog 2.12 (the ``expand_file_opt`` parameter + may be useful in versions 3.3 and later). An example of this can be + found in ``demo.py`` from the ``examples`` directory. + + As of version 2.12, you can also enable this debugging facility for + ``demo.py`` by calling it with the ``--debug`` flag (possibly combined + with ``--debug-expand-file-opt`` in pythondialog 3.3 and later, cf. + ``demo.py --help``). + + + Using Xdialog instead of dialog + ------------------------------- + + As far as I can tell, `Xdialog`_ has not been ported to `GTK+`_ version + 2 or later. It is not in `Debian`_ stable nor unstable (June 23, 2013). + It is not installed on my system (because of the GTK+ 1.2 dependency), + and according to the Xdialog-specific patches I received from Peter + Åstrand in 2004, was not a drop-in replacement for `dialog`_ (in + particular, Xdialog seemed to want to talk to the caller through stdout + instead of stderr, grrrrr!). + + .. _Xdialog: http://xdialog.free.fr/ + .. _GTK+: http://www.gtk.org/ + .. _Debian: http://www.debian.org/ + + All this to say that, even though I didn't remove the options to use + another backend than dialog, nor did I remove the handful of little, + non-invasive modifications that help pythondialog work better with + `Xdialog`_, I don't really support the latter. I test everything with + dialog, and nothing with Xdialog. + + That being said, here is the *old* text of this section (from 2004), in + case you are still interested: + + Starting with 2.06, there is an "Xdialog" compatibility mode that you + can use if you want pythondialog to run the graphical Xdialog program + (which *should* be found under http://xdialog.free.fr/) instead of + dialog (text-mode, based on the ncurses library). + + The primary supported platform is still dialog, but as long as only + small modifications are enough to make pythondialog work with Xdialog, + I am willing to support Xdialog if people are interested in it (which + turned out to be the case for Xdialog). + + The demo.py from pythondialog 2.06 has been tested with Xdialog 2.0.6 + and found to work well (barring Xdialog's annoying behaviour with the + file selection dialog box). + + + Whiptail, anyone? + ----------------- + + Well, pythondialog seems not to work very well with whiptail. The reason + is that whiptail is not compatible with dialog anymore. Although you can + tell pythondialog the program you want it to invoke, only programs that + are mostly dialog-compatible are supported. + + + History + ------- + + pythondialog was originally written by Robb Shecter. Sultanbek Tezadov + added some features to it (mainly the first gauge implementation, I + guess). Florent Rougon rewrote most parts of the program to make it more + robust and flexible so that it can give access to most features of the + dialog program. Peter Åstrand took over maintainership between 2004 and + 2009, with particular care for the `Xdialog`_ support. Florent Rougon + took over maintainership again starting from 2009... + + .. + # Local Variables: + # coding: utf-8 + # fill-column: 72 + # End: + +Keywords: dialog,ncurses,Xdialog,text-mode interface,terminal +Platform: Unix +Classifier: Programming Language :: Python :: 3 +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console :: Curses +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL) +Classifier: Operating System :: Unix +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Software Development :: User Interfaces +Classifier: Topic :: Software Development :: Widget Sets diff --git a/pythondialog/README.distributors b/pythondialog/README.distributors @@ -0,0 +1,65 @@ +-*- coding: utf-8 -*- + +Packaging from a release tarball +================================ + +Packaging from an official release tarball ("source distribution" in +distutils-speak) should be fairly straightforward, as every source +distribution should contain a ChangeLog file that is ready to use. The rest of +this file mainly concerns developers and people wanting to package +pythondialog from a clone of the Git repository. + + +Management of the ChangeLog file +================================ + +Starting from version 2.14.1, the ChangeLog file is not part of the Git +repository anymore, because it is automatically generated from the Git log +with the gitlog-to-changelog[1] program. But it must be present in every +released tarball or package! + + [1] http://git.savannah.gnu.org/gitweb/?p=gnulib.git;a=blob_plain;f=build-aux/gitlog-to-changelog + +To make this as easy as possible, setup.py does the following: + - create or refresh (overwriting) ChangeLog from ChangeLog.init and the Git + log if there is a .git subdirectory in the current directory. This is very + quick and done every time setup.py is run. For this to work, + gitlog-to-changelog must be in the PATH and executable. + - use the existing ChangeLog file if there is no .git subdirectory (this is + for people building from a release tarball as opposed to a clone of the + Git repository); + - include the ChangeLog file in every source distribution made with + "setup.py sdist". + + +Packaging from a clone of the Git repository +============================================ + +If you want to prepare a package from a clone of the Git repository, you +should: + + - make sure the 'version_info' variable towards the top of dialog.py + indicates a Git snapshot, with something like: + + version_info = VersionInfo(2, 14, 1, ".git20130930") + + - install gitlog-to-changelog (single-file Perl script, see above for the + download location) and make sure setup.py can find it. Unless you modify + setup.py, this means you have to make it executable and put it somewhere + in your PATH. You may have to replace the first lines of shell+Perl crap + with a proper shebang line (such as "#! /usr/bin/perl"). You can run + 'gitlog-to-changelog --help' to check that it is working. + + - run 'setup.py sdist' to generate the ChangeLog and prepare a source + distribution. Alternatively, if you only want to generate the ChangeLog, + you can use a command such as the following, which writes its output in + the UTF-8 encoding: + + python3 -c \ + 'import setup; setup.generate_changelog("ChangeLog", write_to_stdout=True)' \ + >/path/to/generated/ChangeLog + + If you want to do some testing of the command by piping the output into + a pager such as less, don't forget to type Ctrl-L to refresh the initial + screen, because it may be garbled due to the messages sent to stderr + (alternatively, you can redirect stderr). diff --git a/pythondialog/README.rst b/pythondialog/README.rst @@ -0,0 +1,282 @@ +=============================================================================== +Python wrapper for the UNIX "dialog" utility +=============================================================================== +Easy writing of graphical interfaces for terminal-based applications +------------------------------------------------------------------------------- + +Overview +-------- + +pythondialog is a Python wrapper for the UNIX dialog_ utility +originally written by Savio Lam and later rewritten by Thomas E. Dickey. +Its purpose is to provide an easy to use, pythonic and as complete as +possible interface to dialog_ from Python code. + +.. _dialog: http://invisible-island.net/dialog/dialog.html + +pythondialog is free software, licensed under the GNU LGPL (GNU Lesser +General Public License). Its home page is located at: + + http://pythondialog.sourceforge.net/ + +and contains a `short example`_, screenshots_, a `summary of the recent +changes`_, links to the `documentation`_, the `Git repository`_, the +`mailing list`_, the `issue tracker`_, etc. + +.. _short example: http://pythondialog.sourceforge.net/#example +.. _screenshots: http://pythondialog.sourceforge.net/gallery.html +.. _summary of the recent changes: + http://pythondialog.sourceforge.net/news.html +.. _documentation: http://pythondialog.sourceforge.net/doc/ +.. _Git repository: https://sourceforge.net/p/pythondialog/code/ +.. _mailing list: https://sourceforge.net/p/pythondialog/mailman/ +.. _issue tracker: https://sourceforge.net/p/pythondialog/_list/tickets + +If you want to get a quick idea of what this module allows one to do, +you can download a release tarball and run ``demo.py``:: + + PYTHONPATH=. python3 examples/demo.py + + +What is pythondialog good for? What are its limitations? +-------------------------------------------------------- + +As you might infer from the name, dialog is a high-level program that +generates dialog boxes. So is pythondialog. They allow you to build nice +interfaces quickly and easily, but you don't have full control over the +widgets, nor can you create new widgets without modifying dialog itself. +If you need to do low-level stuff, you should have a look at `ncurses`_ +(cf. the ``curses`` module in the Python standard library), `blessings`_ +or slang instead. For sophisticated text-mode interfaces, the `Urwid +Python library`_ looks rather interesting, too. + +.. _ncurses: http://invisible-island.net/ncurses/ncurses.html +.. _blessings: https://github.com/erikrose/blessings +.. _Urwid Python library: http://excess.org/urwid/ + + +Requirements +------------ + +* As of version 2.12, the reference implementation of pythondialog + (which this file belongs to) requires Python 3.0 or later in the 3.x + series. pythondialog 3.4.0 has been tested with Python 3.5. + +* However, in order to help users who are somehow forced to still use + Python 2 (even though Python 3.0 was released on December 3, 2008), a + backport of the reference implementation to Python 2 has been + prepared. At the time of this writing, the latest pythondialog version + backported this way is 3.4.0. For up-to-date information about this + backport, please visit the `pythondialog home page`_. + + .. _pythondialog home page: http://pythondialog.sourceforge.net/ + +* Apart from that, pythondialog requires the dialog_ program (or a + drop-in replacement for dialog). You can download dialog from: + + http://invisible-island.net/dialog/dialog.html + + Note that some features of pythondialog may require recent versions of + dialog. + + +Quick installation instructions +------------------------------- + +If you have a working `pip <https://pypi.python.org/pypi/pip>`_ setup, +you should be able to install pythondialog with:: + + pip install pythondialog + +When doing so, make sure that your ``pip`` executable runs with the +Python 3 installation you want to install pythondialog for. + +For more detailed instructions, you can read the ``INSTALL`` file from a +release tarball. You may also want to consult the `pip documentation +<https://pip.pypa.io/>`_. + + +Documentation +------------- + +The pythondialog Manual +^^^^^^^^^^^^^^^^^^^^^^^ + +The pythondialog Manual is written in `reStructuredText`_ format for the +`Sphinx`_ documentation generator. The HTML documentation for the latest +version of pythondialog as rendered by Sphinx should be available at: + + http://pythondialog.sourceforge.net/doc/ + +.. _pythondialog Manual: http://pythondialog.sourceforge.net/doc/ +.. _reStructuredText: http://docutils.sourceforge.net/rst.html +.. _Sphinx: http://sphinx-doc.org/ +.. _LaTeX: http://latex-project.org/ +.. _Make: http://www.gnu.org/software/make/ + +The sources for the pythondialog Manual are located in the ``doc`` +top-level directory of the pythondialog distribution, but the +documentation build process pulls many parts from dialog.py, mainly +docstrings. + +To generate the documentation yourself from dialog.py and the sources in +the ``doc`` directory, first make sure you have `Sphinx`_ and `Make`_ +installed. Then, you can go to the ``doc`` directory and type, for +instance:: + + make html + +You will then find the output in the ``_build/html`` subdirectory of +``doc``. `Sphinx`_ can build the documentation in many other formats. +For instance, if you have `LaTeX`_ installed, you can generate the +pythondialog Manual in PDF format using:: + + make latexpdf + +You can run ``make`` from the ``doc`` directory to see a list of the +available formats. Run ``make clean`` to clean up after the +documentation build process. + +For those who have installed `Sphinx`_ but not `Make`_, it is still +possible to build the documentation with a command such as:: + + sphinx-build -b html . _build/html + +run from the ``doc`` directory. Please refer to `sphinx-build`_ for more +details. + +.. _sphinx-build: http://sphinx-doc.org/invocation.html + + +Reading the docstrings from an interactive Python interpreter +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you have already installed pythondialog, you may consult its +docstrings in an interactive Python interpreter this way:: + + >>> import dialog; help(dialog) + +but only parts of the documentation are available using this method, and +the result is much less convenient to use than the `pythondialog +Manual`_ as generated by `Sphinx`_. + + +Enabling Deprecation Warnings +----------------------------- + +There are a few places in ``dialog.py`` that send a +``DeprecationWarning`` to warn developers about obsolete features. +However, because of: + + - the dialog output to the terminal; + - the fact that such warnings are silenced by default since Python 2.7 + and 3.2; + +you have to do two things in order to see them: + + - redirect the standard error stream to a file; + - enable the warnings for the Python interpreter. + +For instance, to see the warnings produced when running the demo, you +can do:: + + PYTHONPATH=. python3 -Wd examples/demo.py 2>/path/to/file + +and examine ``/path/to/file``. This can also help you to find files that +are still open when your program exits. + +**Note:** + + If your program is terminated by an unhandled exception while stderr + is redirected as in the preceding command, you won't see the traceback + until you examine the file stderr was redirected to. This can be + disturbing, as your program may exit with no apparent reason in such + conditions. + +For more explanations and other methods to enable deprecation warnings, +please refer to: + + http://docs.python.org/3/whatsnew/2.7.html + + +Troubleshooting +--------------- + +If you have a problem with a pythondialog call, you should read its +documentation and the dialog(1) manual page. If this is not enough, you +can enable logging of shell command-line equivalents of all dialog calls +made by your program with a simple call to ``Dialog.setup_debug()``, +first available in pythondialog 2.12 (the ``expand_file_opt`` parameter +may be useful in versions 3.3 and later). An example of this can be +found in ``demo.py`` from the ``examples`` directory. + +As of version 2.12, you can also enable this debugging facility for +``demo.py`` by calling it with the ``--debug`` flag (possibly combined +with ``--debug-expand-file-opt`` in pythondialog 3.3 and later, cf. +``demo.py --help``). + + +Using Xdialog instead of dialog +------------------------------- + +As far as I can tell, `Xdialog`_ has not been ported to `GTK+`_ version +2 or later. It is not in `Debian`_ stable nor unstable (June 23, 2013). +It is not installed on my system (because of the GTK+ 1.2 dependency), +and according to the Xdialog-specific patches I received from Peter +Åstrand in 2004, was not a drop-in replacement for `dialog`_ (in +particular, Xdialog seemed to want to talk to the caller through stdout +instead of stderr, grrrrr!). + +.. _Xdialog: http://xdialog.free.fr/ +.. _GTK+: http://www.gtk.org/ +.. _Debian: http://www.debian.org/ + +All this to say that, even though I didn't remove the options to use +another backend than dialog, nor did I remove the handful of little, +non-invasive modifications that help pythondialog work better with +`Xdialog`_, I don't really support the latter. I test everything with +dialog, and nothing with Xdialog. + +That being said, here is the *old* text of this section (from 2004), in +case you are still interested: + + Starting with 2.06, there is an "Xdialog" compatibility mode that you + can use if you want pythondialog to run the graphical Xdialog program + (which *should* be found under http://xdialog.free.fr/) instead of + dialog (text-mode, based on the ncurses library). + + The primary supported platform is still dialog, but as long as only + small modifications are enough to make pythondialog work with Xdialog, + I am willing to support Xdialog if people are interested in it (which + turned out to be the case for Xdialog). + + The demo.py from pythondialog 2.06 has been tested with Xdialog 2.0.6 + and found to work well (barring Xdialog's annoying behaviour with the + file selection dialog box). + + +Whiptail, anyone? +----------------- + +Well, pythondialog seems not to work very well with whiptail. The reason +is that whiptail is not compatible with dialog anymore. Although you can +tell pythondialog the program you want it to invoke, only programs that +are mostly dialog-compatible are supported. + + +History +------- + +pythondialog was originally written by Robb Shecter. Sultanbek Tezadov +added some features to it (mainly the first gauge implementation, I +guess). Florent Rougon rewrote most parts of the program to make it more +robust and flexible so that it can give access to most features of the +dialog program. Peter Åstrand took over maintainership between 2004 and +2009, with particular care for the `Xdialog`_ support. Florent Rougon +took over maintainership again starting from 2009... + +.. + # Local Variables: + # coding: utf-8 + # fill-column: 72 + # End: diff --git a/pythondialog/TODO b/pythondialog/TODO @@ -0,0 +1,5 @@ +* Support for --output-separator + +* Check for terminal resizing?.. + +* Support --tailboxbg diff --git a/pythondialog/__init__.py b/pythondialog/__init__.py diff --git a/pythondialog/dialog.py b/pythondialog/dialog.py @@ -0,0 +1,3746 @@ +# dialog.py --- A Python interface to the ncurses-based "dialog" utility +# -*- coding: utf-8 -*- +# +# Copyright (C) 2002-2004, 2009-2010, 2013-2016 Florent Rougon +# Copyright (C) 2004 Peter Åstrand +# Copyright (C) 2000 Robb Shecter, Sultanbek Tezadov +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, +# MA 02110-1301 USA. + +"""Python interface to :program:`dialog`-like programs. + +This module provides a Python interface to :program:`dialog`-like +programs such as :program:`dialog` and :program:`Xdialog`. + +It provides a :class:`Dialog` class that retains some parameters such as +the program name and path as well as the values to pass as DIALOG* +environment variables to the chosen program. + +See the pythondialog manual for detailed documentation. + +""" + +import collections +_VersionInfo = collections.namedtuple( + "VersionInfo", ("major", "minor", "micro", "releasesuffix")) + +class VersionInfo(_VersionInfo): + """Class used to represent the version of pythondialog. + + This class is based on :func:`collections.namedtuple` and has the + following field names: ``major``, ``minor``, ``micro``, + ``releasesuffix``. + + .. versionadded:: 2.14 + """ + def __str__(self): + """Return a string representation of the version.""" + res = ".".join( ( str(elt) for elt in self[:3] ) ) + if self.releasesuffix: + res += self.releasesuffix + return res + + def __repr__(self): + return "{0}.{1}".format(__name__, _VersionInfo.__repr__(self)) + +#: Version of pythondialog as a :class:`VersionInfo` instance. +#: +#: .. versionadded:: 2.14 +version_info = VersionInfo(3, 4, 0, None) +#: Version of pythondialog as a string. +#: +#: .. versionadded:: 2.12 +__version__ = str(version_info) + + +import sys, os, tempfile, random, re, warnings, traceback +from contextlib import contextmanager +from textwrap import dedent + +# This is not for calling programs, only to prepare the shell commands that are +# written to the debug log when debugging is enabled. +try: + from shlex import quote as _shell_quote +except ImportError: + def _shell_quote(s): + return "'%s'" % s.replace("'", "'\"'\"'") + + +# Exceptions raised by this module +# +# When adding, suppressing, renaming exceptions or changing their +# hierarchy, don't forget to update the module's docstring. +class error(Exception): + """Base class for exceptions in pythondialog.""" + def __init__(self, message=None): + self.message = message + + def __str__(self): + return self.complete_message() + + def __repr__(self): + return "{0}.{1}({2!r})".format(__name__, self.__class__.__name__, + self.message) + + def complete_message(self): + if self.message: + return "{0}: {1}".format(self.ExceptionShortDescription, + self.message) + else: + return self.ExceptionShortDescription + + ExceptionShortDescription = "{0} generic exception".format("pythondialog") + +# For backward-compatibility +# +# Note: this exception was not documented (only the specific ones were), so +# the backward-compatibility binding could be removed relatively easily. +PythonDialogException = error + +class ExecutableNotFound(error): + """Exception raised when the :program:`dialog` executable can't be found.""" + ExceptionShortDescription = "Executable not found" + +class PythonDialogBug(error): + """Exception raised when pythondialog finds a bug in his own code.""" + ExceptionShortDescription = "Bug in pythondialog" + +# Yeah, the "Probably" makes it look a bit ugly, but: +# - this is more accurate +# - this avoids a potential clash with an eventual PythonBug built-in +# exception in the Python interpreter... +class ProbablyPythonBug(error): + """Exception raised when pythondialog behaves in a way that seems to \ +indicate a Python bug.""" + ExceptionShortDescription = "Bug in python, probably" + +class BadPythonDialogUsage(error): + """Exception raised when pythondialog is used in an incorrect way.""" + ExceptionShortDescription = "Invalid use of pythondialog" + +class PythonDialogSystemError(error): + """Exception raised when pythondialog cannot perform a "system \ +operation" (e.g., a system call) that should work in "normal" situations. + + This is a convenience exception: :exc:`PythonDialogIOError`, + :exc:`PythonDialogOSError` and + :exc:`PythonDialogErrorBeforeExecInChildProcess` all derive from + this exception. As a consequence, watching for + :exc:`PythonDialogSystemError` instead of the aformentioned + exceptions is enough if you don't need precise details about these + kinds of errors. + + Don't confuse this exception with Python's builtin + :exc:`SystemError` exception. + + """ + ExceptionShortDescription = "System error" + +class PythonDialogOSError(PythonDialogSystemError): + """Exception raised when pythondialog catches an :exc:`OSError` exception \ +that should be passed to the calling program.""" + ExceptionShortDescription = "OS error" + +class PythonDialogIOError(PythonDialogOSError): + """Exception raised when pythondialog catches an :exc:`IOError` exception \ +that should be passed to the calling program. + + This exception should not be raised starting from Python 3.3, as the + built-in exception :exc:`IOError` becomes an alias of + :exc:`OSError`. + + .. versionchanged:: 2.12 + :exc:`PythonDialogIOError` is now a subclass of + :exc:`PythonDialogOSError` in order to help with the transition + from :exc:`IOError` to :exc:`OSError` in the Python language. + With this change, you can safely replace ``except + PythonDialogIOError`` clauses with ``except PythonDialogOSError`` + even if running under Python < 3.3. + + """ + ExceptionShortDescription = "IO error" + +class PythonDialogErrorBeforeExecInChildProcess(PythonDialogSystemError): + """Exception raised when an exception is caught in a child process \ +before the exec sytem call (included). + + This can happen in uncomfortable situations such as: + + - the system being out of memory; + - the maximum number of open file descriptors being reached; + - the :program:`dialog`-like program being removed (or made + non-executable) between the time we found it with + :func:`_find_in_path` and the time the exec system call + attempted to execute it; + - the Python program trying to call the :program:`dialog`-like + program with arguments that cannot be represented in the user's + locale (:envvar:`LC_CTYPE`). + + """ + ExceptionShortDescription = "Error in a child process before the exec " \ + "system call" + +class PythonDialogReModuleError(PythonDialogSystemError): + """Exception raised when pythondialog catches a :exc:`re.error` exception.""" + ExceptionShortDescription = "'re' module error" + +class UnexpectedDialogOutput(error): + """Exception raised when the :program:`dialog`-like program returns \ +something not expected by pythondialog.""" + ExceptionShortDescription = "Unexpected dialog output" + +class DialogTerminatedBySignal(error): + """Exception raised when the :program:`dialog`-like program is \ +terminated by a signal.""" + ExceptionShortDescription = "dialog-like terminated by a signal" + +class DialogError(error): + """Exception raised when the :program:`dialog`-like program exits \ +with the code indicating an error.""" + ExceptionShortDescription = "dialog-like terminated due to an error" + +class UnableToRetrieveBackendVersion(error): + """Exception raised when we cannot retrieve the version string of the \ +:program:`dialog`-like backend. + + .. versionadded:: 2.14 + """ + ExceptionShortDescription = "Unable to retrieve the version of the \ +dialog-like backend" + +class UnableToParseBackendVersion(error): + """Exception raised when we cannot parse the version string of the \ +:program:`dialog`-like backend. + + .. versionadded:: 2.14 + """ + ExceptionShortDescription = "Unable to parse as a dialog-like backend \ +version string" + +class UnableToParseDialogBackendVersion(UnableToParseBackendVersion): + """Exception raised when we cannot parse the version string of the \ +:program:`dialog` backend. + + .. versionadded:: 2.14 + """ + ExceptionShortDescription = "Unable to parse as a dialog version string" + +class InadequateBackendVersion(error): + """Exception raised when the backend version in use is inadequate \ +in a given situation. + + .. versionadded:: 2.14 + """ + ExceptionShortDescription = "Inadequate backend version" + + +@contextmanager +def _OSErrorHandling(): + try: + yield + except OSError as e: + raise PythonDialogOSError(str(e)) from e + except IOError as e: + raise PythonDialogIOError(str(e)) from e + + +try: + # Values accepted for checklists + _on_cre = re.compile(r"on$", re.IGNORECASE) + _off_cre = re.compile(r"off$", re.IGNORECASE) + + _calendar_date_cre = re.compile( + r"(?P<day>\d\d)/(?P<month>\d\d)/(?P<year>\d\d\d\d)$") + _timebox_time_cre = re.compile( + r"(?P<hour>\d\d):(?P<minute>\d\d):(?P<second>\d\d)$") +except re.error as e: + raise PythonDialogReModuleError(str(e)) from e + + +# From dialog(1): +# +# All options begin with "--" (two ASCII hyphens, for the benefit of those +# using systems with deranged locale support). +# +# A "--" by itself is used as an escape, i.e., the next token on the +# command-line is not treated as an option, as in: +# dialog --title -- --Not an option +def _dash_escape(args): + """Escape all elements of *args* that need escaping. + + *args* may be any sequence and is not modified by this function. + Return a new list where every element that needs escaping has been + escaped. + + An element needs escaping when it starts with two ASCII hyphens + (``--``). Escaping consists in prepending an element composed of two + ASCII hyphens, i.e., the string ``'--'``. + + """ + res = [] + + for arg in args: + if arg.startswith("--"): + res.extend(("--", arg)) + else: + res.append(arg) + + return res + +# We need this function in the global namespace for the lambda +# expressions in _common_args_syntax to see it when they are called. +def _dash_escape_nf(args): # nf: non-first + """Escape all elements of *args* that need escaping, except the first one. + + See :func:`_dash_escape` for details. Return a new list. + + """ + if not args: + raise PythonDialogBug("not a non-empty sequence: {0!r}".format(args)) + l = _dash_escape(args[1:]) + l.insert(0, args[0]) + return l + +def _simple_option(option, enable): + """Turn on or off the simplest :term:`dialog common options`.""" + if enable: + return (option,) + else: + # This will not add any argument to the command line + return () + + +# This dictionary allows us to write the dialog common options in a Pythonic +# way (e.g. dialog_instance.checklist(args, ..., title="Foo", no_shadow=True)). +# +# Options such as --separate-output should obviously not be set by the user +# since they affect the parsing of dialog's output: +_common_args_syntax = { + "ascii_lines": lambda enable: _simple_option("--ascii-lines", enable), + "aspect": lambda ratio: _dash_escape_nf(("--aspect", str(ratio))), + "backtitle": lambda backtitle: _dash_escape_nf(("--backtitle", backtitle)), + # Obsolete according to dialog(1) + "beep": lambda enable: _simple_option("--beep", enable), + # Obsolete according to dialog(1) + "beep_after": lambda enable: _simple_option("--beep-after", enable), + # Warning: order = y, x! + "begin": lambda coords: _dash_escape_nf( + ("--begin", str(coords[0]), str(coords[1]))), + "cancel_label": lambda s: _dash_escape_nf(("--cancel-label", s)), + # Old, unfortunate choice of key, kept for backward compatibility + "cancel": lambda s: _dash_escape_nf(("--cancel-label", s)), + "clear": lambda enable: _simple_option("--clear", enable), + "colors": lambda enable: _simple_option("--colors", enable), + "column_separator": lambda s: _dash_escape_nf(("--column-separator", s)), + "cr_wrap": lambda enable: _simple_option("--cr-wrap", enable), + "create_rc": lambda filename: _dash_escape_nf(("--create-rc", filename)), + "date_format": lambda s: _dash_escape_nf(("--date-format", s)), + "defaultno": lambda enable: _simple_option("--defaultno", enable), + "default_button": lambda s: _dash_escape_nf(("--default-button", s)), + "default_item": lambda s: _dash_escape_nf(("--default-item", s)), + "exit_label": lambda s: _dash_escape_nf(("--exit-label", s)), + "extra_button": lambda enable: _simple_option("--extra-button", enable), + "extra_label": lambda s: _dash_escape_nf(("--extra-label", s)), + "help": lambda enable: _simple_option("--help", enable), + "help_button": lambda enable: _simple_option("--help-button", enable), + "help_label": lambda s: _dash_escape_nf(("--help-label", s)), + "help_status": lambda enable: _simple_option("--help-status", enable), + "help_tags": lambda enable: _simple_option("--help-tags", enable), + "hfile": lambda filename: _dash_escape_nf(("--hfile", filename)), + "hline": lambda s: _dash_escape_nf(("--hline", s)), + "ignore": lambda enable: _simple_option("--ignore", enable), + "insecure": lambda enable: _simple_option("--insecure", enable), + "item_help": lambda enable: _simple_option("--item-help", enable), + "keep_tite": lambda enable: _simple_option("--keep-tite", enable), + "keep_window": lambda enable: _simple_option("--keep-window", enable), + "max_input": lambda size: _dash_escape_nf(("--max-input", str(size))), + "no_cancel": lambda enable: _simple_option("--no-cancel", enable), + "nocancel": lambda enable: _simple_option("--nocancel", enable), + "no_collapse": lambda enable: _simple_option("--no-collapse", enable), + "no_kill": lambda enable: _simple_option("--no-kill", enable), + "no_label": lambda s: _dash_escape_nf(("--no-label", s)), + "no_lines": lambda enable: _simple_option("--no-lines", enable), + "no_mouse": lambda enable: _simple_option("--no-mouse", enable), + "no_nl_expand": lambda enable: _simple_option("--no-nl-expand", enable), + "no_ok": lambda enable: _simple_option("--no-ok", enable), + "no_shadow": lambda enable: _simple_option("--no-shadow", enable), + "no_tags": lambda enable: _simple_option("--no-tags", enable), + "ok_label": lambda s: _dash_escape_nf(("--ok-label", s)), + # cf. Dialog.maxsize() + "print_maxsize": lambda enable: _simple_option("--print-maxsize", + enable), + "print_size": lambda enable: _simple_option("--print-size", enable), + # cf. Dialog.backend_version() + "print_version": lambda enable: _simple_option("--print-version", + enable), + "scrollbar": lambda enable: _simple_option("--scrollbar", enable), + "separate_output": lambda enable: _simple_option("--separate-output", + enable), + "separate_widget": lambda s: _dash_escape_nf(("--separate-widget", s)), + "shadow": lambda enable: _simple_option("--shadow", enable), + # Obsolete according to dialog(1) + "size_err": lambda enable: _simple_option("--size-err", enable), + "sleep": lambda secs: _dash_escape_nf(("--sleep", str(secs))), + "stderr": lambda enable: _simple_option("--stderr", enable), + "stdout": lambda enable: _simple_option("--stdout", enable), + "tab_correct": lambda enable: _simple_option("--tab-correct", enable), + "tab_len": lambda n: _dash_escape_nf(("--tab-len", str(n))), + "time_format": lambda s: _dash_escape_nf(("--time-format", s)), + "timeout": lambda secs: _dash_escape_nf(("--timeout", str(secs))), + "title": lambda title: _dash_escape_nf(("--title", title)), + "trace": lambda filename: _dash_escape_nf(("--trace", filename)), + "trim": lambda enable: _simple_option("--trim", enable), + "version": lambda enable: _simple_option("--version", enable), + "visit_items": lambda enable: _simple_option("--visit-items", enable), + "week_start": lambda start: _dash_escape_nf( + ("--week-start", str(start) if isinstance(start, int) else start)), + "yes_label": lambda s: _dash_escape_nf(("--yes-label", s)) } + + +def _find_in_path(prog_name): + """Search an executable in the :envvar:`PATH`. + + If :envvar:`PATH` is not defined, the default path + ``:/bin:/usr/bin`` is used. + + Return a path to the file or ``None`` if no readable and executable + file is found. + + Notable exception: + + :exc:`PythonDialogOSError` + + """ + with _OSErrorHandling(): + # Note that the leading empty component in the default value for PATH + # could lead to the returned path not being absolute. + PATH = os.getenv("PATH", ":/bin:/usr/bin") # see the execvp(3) man page + for d in PATH.split(":"): + file_path = os.path.join(d, prog_name) + if os.path.isfile(file_path) \ + and os.access(file_path, os.R_OK | os.X_OK): + return file_path + return None + + +def _path_to_executable(f): + """Find a path to an executable. + + Find a path to an executable, using the same rules as the POSIX + exec*p functions (see execvp(3) for instance). + + If *f* contains a ``/``, it is assumed to be a path and is simply + checked for read and write permissions; otherwise, it is looked for + according to the contents of the :envvar:`PATH` environment + variable, which defaults to ``:/bin:/usr/bin`` if unset. + + The returned path is not necessarily absolute. + + Notable exceptions: + + - :exc:`ExecutableNotFound` + - :exc:`PythonDialogOSError` + + """ + with _OSErrorHandling(): + if '/' in f: + if os.path.isfile(f) and \ + os.access(f, os.R_OK | os.X_OK): + res = f + else: + raise ExecutableNotFound("%s cannot be read and executed" % f) + else: + res = _find_in_path(f) + if res is None: + raise ExecutableNotFound( + "can't find the executable for the dialog-like " + "program") + + return res + + +def _to_onoff(val): + """Convert boolean expressions to ``"on"`` or ``"off"``. + + :return: + - ``"on"`` if *val* is ``True``, a non-zero integer, ``"on"`` or + any case variation thereof; + - ``"off"`` if *val* is ``False``, ``0``, ``"off"`` or any case + variation thereof. + + Notable exceptions: + + - :exc:`PythonDialogReModuleError` + - :exc:`BadPythonDialogUsage` + + """ + if isinstance(val, (bool, int)): + return "on" if val else "off" + elif isinstance(val, str): + try: + if _on_cre.match(val): + return "on" + elif _off_cre.match(val): + return "off" + except re.error as e: + raise PythonDialogReModuleError(str(e)) from e + + raise BadPythonDialogUsage("invalid boolean value: {0!r}".format(val)) + + +def _compute_common_args(mapping): + """Compute the list of arguments for :term:`dialog common options`. + + Compute a list of the command-line arguments to pass to + :program:`dialog` from a keyword arguments dictionary for options + listed as "common options" in the manual page for :program:`dialog`. + These are the options that are not tied to a particular widget. + + This allows one to specify these options in a pythonic way, such + as:: + + d.checklist(<usual arguments for a checklist>, + title="...", + backtitle="...") + + instead of having to pass them with strings like ``"--title foo"`` + or ``"--backtitle bar"``. + + Notable exceptions: none + + """ + args = [] + for option, value in mapping.items(): + args.extend(_common_args_syntax[option](value)) + return args + + +# Classes for dealing with the version of dialog-like backend programs +if sys.hexversion >= 0x030200F0: + import abc + # Abstract base class + class BackendVersion(metaclass=abc.ABCMeta): + @abc.abstractmethod + def __str__(self): + raise NotImplementedError() + + if sys.hexversion >= 0x030300F0: + @classmethod + @abc.abstractmethod + def fromstring(cls, s): + raise NotImplementedError() + else: # for Python 3.2 + @abc.abstractclassmethod + def fromstring(cls, s): + raise NotImplementedError() + + @abc.abstractmethod + def __lt__(self, other): + raise NotImplementedError() + + @abc.abstractmethod + def __le__(self, other): + raise NotImplementedError() + + @abc.abstractmethod + def __eq__(self, other): + raise NotImplementedError() + + @abc.abstractmethod + def __ne__(self, other): + raise NotImplementedError() + + @abc.abstractmethod + def __gt__(self, other): + raise NotImplementedError() + + @abc.abstractmethod + def __ge__(self, other): + raise NotImplementedError() +else: + class BackendVersion: + pass + + +class DialogBackendVersion(BackendVersion): + """Class representing possible versions of the :program:`dialog` backend. + + The purpose of this class is to make it easy to reliably compare + between versions of the :program:`dialog` backend. It encapsulates + the specific details of the backend versioning scheme to allow + eventual adaptations to changes in this scheme without affecting + external code. + + The version is represented by two components in this class: the + :dfn:`dotted part` and the :dfn:`rest`. For instance, in the + ``'1.2'`` version string, the dotted part is ``[1, 2]`` and the rest + is the empty string. However, in version ``'1.2-20130902'``, the + dotted part is still ``[1, 2]``, but the rest is the string + ``'-20130902'``. + + Instances of this class can be created with the constructor by + specifying the dotted part and the rest. Alternatively, an instance + can be created from the corresponding version string (e.g., + ``'1.2-20130902'``) using the :meth:`fromstring` class method. This + is particularly useful with the result of + :samp:`{d}.backend_version()`, where *d* is a :class:`Dialog` + instance. Actually, the main constructor detects if its first + argument is a string and calls :meth:`!fromstring` in this case as a + convenience. Therefore, all of the following expressions are valid + to create a DialogBackendVersion instance:: + + DialogBackendVersion([1, 2]) + DialogBackendVersion([1, 2], "-20130902") + DialogBackendVersion("1.2-20130902") + DialogBackendVersion.fromstring("1.2-20130902") + + If *bv* is a :class:`DialogBackendVersion` instance, + :samp:`str({bv})` is a string representing the same version (for + instance, ``"1.2-20130902"``). + + Two :class:`DialogBackendVersion` instances can be compared with the + usual comparison operators (``<``, ``<=``, ``==``, ``!=``, ``>=``, + ``>``). The algorithm is designed so that the following order is + respected (after instanciation with :meth:`fromstring`):: + + 1.2 < 1.2-20130902 < 1.2-20130903 < 1.2.0 < 1.2.0-20130902 + + among other cases. Actually, the *dotted parts* are the primary keys + when comparing and *rest* strings act as secondary keys. *Dotted + parts* are compared with the standard Python list comparison and + *rest* strings using the standard Python string comparison. + + """ + try: + _backend_version_cre = re.compile(r"""(?P<dotted> (\d+) (\.\d+)* ) + (?P<rest>.*)$""", re.VERBOSE) + except re.error as e: + raise PythonDialogReModuleError(str(e)) from e + + def __init__(self, dotted_part_or_str, rest=""): + """Create a :class:`DialogBackendVersion` instance. + + Please see the class docstring for details. + + """ + if isinstance(dotted_part_or_str, str): + if rest: + raise BadPythonDialogUsage( + "non-empty 'rest' with 'dotted_part_or_str' as string: " + "{0!r}".format(rest)) + else: + tmp = self.__class__.fromstring(dotted_part_or_str) + dotted_part_or_str, rest = tmp.dotted_part, tmp.rest + + for elt in dotted_part_or_str: + if not isinstance(elt, int): + raise BadPythonDialogUsage( + "when 'dotted_part_or_str' is not a string, it must " + "be a sequence (or iterable) of integers; however, " + "{0!r} is not an integer.".format(elt)) + + self.dotted_part = list(dotted_part_or_str) + self.rest = rest + + def __repr__(self): + return "{0}.{1}({2!r}, rest={3!r})".format( + __name__, self.__class__.__name__, self.dotted_part, self.rest) + + def __str__(self): + return '.'.join(map(str, self.dotted_part)) + self.rest + + @classmethod + def fromstring(cls, s): + """Create a :class:`DialogBackendVersion` instance from a \ +:program:`dialog` version string. + + :param str s: a :program:`dialog` version string + :return: + a :class:`DialogBackendVersion` instance representing the same + string + + Notable exceptions: + + - :exc:`UnableToParseDialogBackendVersion` + - :exc:`PythonDialogReModuleError` + + """ + try: + mo = cls._backend_version_cre.match(s) + if not mo: + raise UnableToParseDialogBackendVersion(s) + dotted_part = [ int(x) for x in mo.group("dotted").split(".") ] + rest = mo.group("rest") + except re.error as e: + raise PythonDialogReModuleError(str(e)) from e + + return cls(dotted_part, rest) + + def __lt__(self, other): + return (self.dotted_part, self.rest) < (other.dotted_part, other.rest) + + def __le__(self, other): + return (self.dotted_part, self.rest) <= (other.dotted_part, other.rest) + + def __eq__(self, other): + return (self.dotted_part, self.rest) == (other.dotted_part, other.rest) + + # Python 3.2 has a decorator (functools.total_ordering) to automate this. + def __ne__(self, other): + return not (self == other) + + def __gt__(self, other): + return not (self <= other) + + def __ge__(self, other): + return not (self < other) + + +def widget(func): + """Decorator to mark :class:`Dialog` methods that provide widgets. + + This allows code to perform automatic operations on these specific + methods. For instance, one can define a class that behaves similarly + to :class:`Dialog`, except that after every widget-producing call, + it spawns a "confirm quit" dialog if the widget returned + :attr:`Dialog.ESC`, and loops in case the user doesn't actually want + to quit. + + When it is unclear whether a method should have the decorator or + not, the return value is used to draw the line. For instance, among + :meth:`Dialog.gauge_start`, :meth:`Dialog.gauge_update` and + :meth:`Dialog.gauge_stop`, only the last one has the decorator + because it returns a :term:`Dialog exit code`, whereas the first two + don't return anything meaningful. + + Note: + + Some widget-producing methods return the Dialog exit code, but + other methods return a *sequence*, the first element of which is + the Dialog exit code; the ``retval_is_code`` attribute, which is + set by the decorator of the same name, allows to programmatically + discover the interface a given method conforms to. + + .. versionadded:: 2.14 + + """ + func.is_widget = True + return func + + +def retval_is_code(func): + """Decorator for :class:`Dialog` widget-producing methods whose \ +return value is the :term:`Dialog exit code`. + + This decorator is intended for widget-producing methods whose return + value consists solely of the Dialog exit code. When this decorator + is *not* used on a widget-producing method, the Dialog exit code + must be the first element of the return value. + + .. versionadded:: 3.0 + + """ + func.retval_is_code = True + return func + + +def _obsolete_property(name, replacement=None): + if replacement is None: + replacement = name + + def getter(self): + warnings.warn("the DIALOG_{name} attribute of Dialog instances is " + "obsolete; use the Dialog.{repl} class attribute " + "instead.".format(name=name, repl=replacement), + DeprecationWarning) + return getattr(self, replacement) + + return getter + + +# Main class of the module +class Dialog: + """Class providing bindings for :program:`dialog`-compatible programs. + + This class allows you to invoke :program:`dialog` or a compatible + program in a pythonic way to quickly and easily build simple but + nice text interfaces. + + An application typically creates one instance of the :class:`Dialog` + class and uses it for all its widgets, but it is possible to + concurrently use several instances of this class with different + parameters (such as the background title) if you have a need for + this. + + """ + try: + _print_maxsize_cre = re.compile(r"""^MaxSize:[ \t]+ + (?P<rows>\d+),[ \t]* + (?P<columns>\d+)[ \t]*$""", + re.VERBOSE) + _print_version_cre = re.compile( + r"^Version:[ \t]+(?P<version>.+?)[ \t]*$", re.MULTILINE) + except re.error as e: + raise PythonDialogReModuleError(str(e)) from e + + # DIALOG_OK, DIALOG_CANCEL, etc. are environment variables controlling + # the dialog backend exit status in the corresponding situation ("low-level + # exit status/code"). + # + # Note: + # - 127 must not be used for any of the DIALOG_* values. It is used + # when a failure occurs in the child process before it exec()s + # dialog (where "before" includes a potential exec() failure). + # - 126 is also used (although in presumably rare situations). + _DIALOG_OK = 0 + _DIALOG_CANCEL = 1 + _DIALOG_ESC = 2 + _DIALOG_ERROR = 3 + _DIALOG_EXTRA = 4 + _DIALOG_HELP = 5 + _DIALOG_ITEM_HELP = 6 + # cf. also _lowlevel_exit_codes and _dialog_exit_code_ll_to_hl which are + # created by __init__(). It is not practical to define everything here, + # because there is no equivalent of 'self' for the class outside method + # definitions. + _lowlevel_exit_code_varnames = frozenset(("OK", "CANCEL", "ESC", "ERROR", + "EXTRA", "HELP", "ITEM_HELP")) + + # High-level exit codes, AKA "Dialog exit codes". These are the codes that + # pythondialog-based applications should use. + # + #: :term:`Dialog exit code` corresponding to the ``DIALOG_OK`` + #: :term:`dialog exit status` + OK = "ok" + #: :term:`Dialog exit code` corresponding to the ``DIALOG_CANCEL`` + #: :term:`dialog exit status` + CANCEL = "cancel" + #: :term:`Dialog exit code` corresponding to the ``DIALOG_ESC`` + #: :term:`dialog exit status` + ESC = "esc" + #: :term:`Dialog exit code` corresponding to the ``DIALOG_EXTRA`` + #: :term:`dialog exit status` + EXTRA = "extra" + #: :term:`Dialog exit code` corresponding to the ``DIALOG_HELP`` and + #: ``DIALOG_ITEM_HELP`` :term:`dialog exit statuses <dialog exit status>` + HELP = "help" + + # Define properties to maintain backward-compatibility while warning about + # the obsolete attributes (which used to refer to the low-level exit codes + # in pythondialog 2.x). + # + #: Obsolete property superseded by :attr:`Dialog.OK` since version 3.0 + DIALOG_OK = property(_obsolete_property("OK"), + doc="Obsolete property superseded by Dialog.OK") + #: Obsolete property superseded by :attr:`Dialog.CANCEL` since version 3.0 + DIALOG_CANCEL = property(_obsolete_property("CANCEL"), + doc="Obsolete property superseded by Dialog.CANCEL") + #: Obsolete property superseded by :attr:`Dialog.ESC` since version 3.0 + DIALOG_ESC = property(_obsolete_property("ESC"), + doc="Obsolete property superseded by Dialog.ESC") + #: Obsolete property superseded by :attr:`Dialog.EXTRA` since version 3.0 + DIALOG_EXTRA = property(_obsolete_property("EXTRA"), + doc="Obsolete property superseded by Dialog.EXTRA") + #: Obsolete property superseded by :attr:`Dialog.HELP` since version 3.0 + DIALOG_HELP = property(_obsolete_property("HELP"), + doc="Obsolete property superseded by Dialog.HELP") + # We treat DIALOG_ITEM_HELP and DIALOG_HELP the same way in pythondialog, + # since both indicate the same user action ("Help" button pressed). + # + #: Obsolete property superseded by :attr:`Dialog.HELP` since version 3.0 + DIALOG_ITEM_HELP = property(_obsolete_property("ITEM_HELP", + replacement="HELP"), + doc="Obsolete property superseded by Dialog.HELP") + + @property + def DIALOG_ERROR(self): + warnings.warn("the DIALOG_ERROR attribute of Dialog instances is " + "obsolete. Since the corresponding exit status is " + "automatically translated into a DialogError exception, " + "users should not see nor need this attribute. If you " + "think you have a good reason to use it, please expose " + "your situation on the pythondialog mailing-list.", + DeprecationWarning) + # There is no corresponding high-level code; and if the user *really* + # wants to know the (integer) error exit status, here it is... + return self._DIALOG_ERROR + + def __init__(self, dialog="dialog", DIALOGRC=None, + compat="dialog", use_stdout=None, *, autowidgetsize=False, + pass_args_via_file=None): + """Constructor for :class:`Dialog` instances. + + :param str dialog: + name of (or path to) the :program:`dialog`-like program to + use; if it contains a ``'/'``, it is assumed to be a path and + is used as is; otherwise, it is looked for according to the + contents of the :envvar:`PATH` environment variable, which + defaults to ``":/bin:/usr/bin"`` if unset. + :param str DIALOGRC: + string to pass to the :program:`dialog`-like program as the + :envvar:`DIALOGRC` environment variable, or ``None`` if no + modification to the environment regarding this variable should + be done in the call to the :program:`dialog`-like program + :param str compat: + compatibility mode (see :ref:`below + <Dialog-constructor-compat-arg>`) + :param bool use_stdout: + read :program:`dialog`'s standard output stream instead of its + standard error stream in order to get most "results" + (user-supplied strings, selected items, etc.; basically, + everything except the exit status). This is for compatibility + with :program:`Xdialog` and should only be used if you have a + good reason to do so. + :param bool autowidgetsize: + whether to enable *autowidgetsize* mode. When enabled, all + pythondialog widget-producing methods will behave as if + ``width=0``, ``height=0``, etc. had been passed, except where + these parameters are explicitely specified with different + values. This has the effect that, by default, the + :program:`dialog` backend will automatically compute a + suitable size for the widgets. More details about this option + are given :ref:`below <autowidgetsize>`. + :param pass_args_via_file: + whether to use the :option:`--file` option with a temporary + file in order to pass arguments to the :program:`dialog` + backend, instead of including them directly into the argument + list; using :option:`--file` has the advantage of not exposing + the “real” arguments to other users through the process table. + With the default value (``None``), the option is enabled if + the :program:`dialog` version is recent enough to offer a + reliable :option:`--file` implementation (i.e., 1.2-20150513 + or later). + :type pass_args_via_file: bool or ``None`` + :return: a :class:`Dialog` instance + + .. _Dialog-constructor-compat-arg: + + The officially supported :program:`dialog`-like program in + pythondialog is the well-known dialog_ program written in C, + based on the ncurses_ library. + + .. _dialog: http://invisible-island.net/dialog/dialog.html + .. _ncurses: http://invisible-island.net/ncurses/ncurses.html + + If you want to use a different program such as Xdialog_, you + should indicate the executable file name with the *dialog* + argument **and** the compatibility type that you think it + conforms to with the *compat* argument. Currently, *compat* can + be either ``"dialog"`` (for :program:`dialog`; this is the + default) or ``"Xdialog"`` (for, well, :program:`Xdialog`). + + .. _Xdialog: http://xdialog.free.fr/ + + The *compat* argument allows me to cope with minor differences + in behaviour between the various programs implementing the + :program:`dialog` interface (not the text or graphical + interface, I mean the API). However, having to support various + APIs simultaneously is ugly and I would really prefer you to + report bugs to the relevant maintainers when you find + incompatibilities with :program:`dialog`. This is for the + benefit of pretty much everyone that relies on the + :program:`dialog` interface. + + Notable exceptions: + + - :exc:`ExecutableNotFound` + - :exc:`PythonDialogOSError` + - :exc:`UnableToRetrieveBackendVersion` + - :exc:`UnableToParseBackendVersion` + + .. versionadded:: 3.1 + Support for the *autowidgetsize* parameter. + + .. versionadded:: 3.3 + Support for the *pass_args_via_file* parameter. + + """ + # DIALOGRC differs from the Dialog._DIALOG_* attributes in that: + # 1. It is an instance attribute instead of a class attribute. + # 2. It should be a string if not None. + # 3. We may very well want it to be unset. + if DIALOGRC is not None: + self.DIALOGRC = DIALOGRC + + # Mapping from "OK", "CANCEL", ... to the corresponding dialog exit + # statuses (integers). + self._lowlevel_exit_codes = { + name: getattr(self, "_DIALOG_" + name) + for name in self._lowlevel_exit_code_varnames } + + # Mapping from dialog exit status (integer) to Dialog exit code ("ok", + # "cancel", ... strings referred to by Dialog.OK, Dialog.CANCEL, ...); + # in other words, from low-level to high-level exit code. + self._dialog_exit_code_ll_to_hl = {} + for name in self._lowlevel_exit_code_varnames: + intcode = self._lowlevel_exit_codes[name] + + if name == "ITEM_HELP": + self._dialog_exit_code_ll_to_hl[intcode] = self.HELP + elif name == "ERROR": + continue + else: + self._dialog_exit_code_ll_to_hl[intcode] = getattr(self, name) + + self._dialog_prg = _path_to_executable(dialog) + self.compat = compat + self.autowidgetsize = autowidgetsize + self.dialog_persistent_arglist = [] + + # Use stderr or stdout for reading dialog's output? + if self.compat == "Xdialog": + # Default to using stdout for Xdialog + self.use_stdout = True + else: + self.use_stdout = False + if use_stdout is not None: + # Allow explicit setting + self.use_stdout = use_stdout + if self.use_stdout: + self.add_persistent_args(["--stdout"]) + + self.setup_debug(False) + + if compat == "dialog": + # Temporary setting to ensure that self.backend_version() + # will be able to run even if dialog is too old to support + # --file correctly. Will be overwritten later. + self.pass_args_via_file = False + self.cached_backend_version = DialogBackendVersion.fromstring( + self.backend_version()) + else: + # Xdialog doesn't seem to offer --print-version (2013-09-12) + self.cached_backend_version = None + + if pass_args_via_file is not None: + # Always respect explicit settings + self.pass_args_via_file = pass_args_via_file + elif self.cached_backend_version is not None: + self.pass_args_via_file = self.cached_backend_version >= \ + DialogBackendVersion("1.2-20150513") + else: + # Xdialog doesn't seem to offer --file (2015-05-24) + self.pass_args_via_file = False + + @classmethod + def dash_escape(cls, args): + """ + Escape all elements of *args* that need escaping for :program:`dialog`. + + *args* may be any sequence and is not modified by this method. + Return a new list where every element that needs escaping has + been escaped. + + An element needs escaping when it starts with two ASCII hyphens + (``--``). Escaping consists in prepending an element composed of + two ASCII hyphens, i.e., the string ``'--'``. + + All high-level :class:`Dialog` methods automatically perform + :term:`dash escaping` where appropriate. In particular, this is + the case for every method that provides a widget: :meth:`yesno`, + :meth:`msgbox`, etc. You only need to do it yourself when + calling a low-level method such as :meth:`add_persistent_args`. + + .. versionadded:: 2.12 + + """ + return _dash_escape(args) + + @classmethod + def dash_escape_nf(cls, args): + """ + Escape all elements of *args* that need escaping, except the first one. + + See :meth:`dash_escape` for details. Return a new list. + + All high-level :class:`Dialog` methods automatically perform dash + escaping where appropriate. In particular, this is the case + for every method that provides a widget: :meth:`yesno`, :meth:`msgbox`, + etc. You only need to do it yourself when calling a low-level + method such as :meth:`add_persistent_args`. + + .. versionadded:: 2.12 + + """ + return _dash_escape_nf(args) + + def add_persistent_args(self, args): + """Add arguments to use for every subsequent dialog call. + + This method cannot guess which elements of *args* are dialog + options (such as ``--title``) and which are not (for instance, + you might want to use ``--title`` or even ``--`` as an argument + to a dialog option). Therefore, this method does not perform any + kind of :term:`dash escaping`; you have to do it yourself. + :meth:`dash_escape` and :meth:`dash_escape_nf` may be useful for + this purpose. + + """ + self.dialog_persistent_arglist.extend(args) + + def set_background_title(self, text): + """Set the background title for dialog. + + :param str text: string to use as background title + + .. versionadded:: 2.13 + + """ + self.add_persistent_args(self.dash_escape_nf(("--backtitle", text))) + + # For compatibility with the old dialog + def setBackgroundTitle(self, text): + """Set the background title for :program:`dialog`. + + :param str text: background title to use behind widgets + + .. deprecated:: 2.03 + Use :meth:`set_background_title` instead. + + """ + warnings.warn("Dialog.setBackgroundTitle() has been obsolete for " + "many years; use Dialog.set_background_title() instead", + DeprecationWarning) + self.set_background_title(text) + + def setup_debug(self, enable, file=None, always_flush=False, *, + expand_file_opt=False): + """Setup the debugging parameters. + + :param bool enable: whether to enable or disable debugging + :param file file: where to write debugging information + :param bool always_flush: whether to call :meth:`file.flush` + after each command written + :param bool expand_file_opt: + when :meth:`Dialog.__init__` has been called with + :samp:`{pass_args_via_file}=True`, this option causes the + :option:`--file` options that would normally be written to + *file* to be expanded, yielding a similar result to what would + be obtained with :samp:`{pass_args_via_file}=False` (but + contrary to :samp:`{pass_args_via_file}=False`, this only + affects *file*, not the actual :program:`dialog` calls). This + is useful, for instance, for copying some of the + :program:`dialog` commands into a shell. + + When *enable* is true, all :program:`dialog` commands are + written to *file* using POSIX shell syntax. In this case, you'll + probably want to use either :samp:`{expand_file_opt}=True` in + this method or :samp:`{pass_args_via_file}=False` in + :meth:`Dialog.__init__`, otherwise you'll mostly see + :program:`dialog` calls containing only one :option:`--file` + option followed by a path to a temporary file. + + .. versionadded:: 2.12 + + .. versionadded:: 3.3 + Support for the *expand_file_opt* parameter. + + """ + self._debug_enabled = enable + + if not hasattr(self, "_debug_logfile"): + self._debug_logfile = None + # Allows to switch debugging on and off without having to pass the file + # object again and again. + if file is not None: + self._debug_logfile = file + + if enable and self._debug_logfile is None: + raise BadPythonDialogUsage( + "you must specify a file object when turning debugging on") + + self._debug_always_flush = always_flush + self._expand_file_opt = expand_file_opt + self._debug_first_output = True + + def _write_command_to_file(self, env, arglist): + envvar_settings_list = [] + + if "DIALOGRC" in env: + envvar_settings_list.append( + "DIALOGRC={0}".format(_shell_quote(env["DIALOGRC"]))) + + for var in self._lowlevel_exit_code_varnames: + varname = "DIALOG_" + var + envvar_settings_list.append( + "{0}={1}".format(varname, _shell_quote(env[varname]))) + + command_str = ' '.join(envvar_settings_list + + list(map(_shell_quote, arglist))) + s = "{separator}{cmd}\n\nArgs: {args!r}\n".format( + separator="" if self._debug_first_output else ("-" * 79) + "\n", + cmd=command_str, args=arglist) + + self._debug_logfile.write(s) + if self._debug_always_flush: + self._debug_logfile.flush() + + self._debug_first_output = False + + def _quote_arg_for_file_opt(self, argument): + """ + Transform a :program:`dialog` argument for safe inclusion via :option:`--file`. + + Since arguments in a file included via :option:`--file` are + separated by whitespace, they must be quoted for + :program:`dialog` in a way similar to shell quoting. + + """ + l = ['"'] + + for c in argument: + if c in ('"', '\\'): + l.append("\\" + c) + else: + l.append(c) + + return ''.join(l + ['"']) + + def _call_program(self, cmdargs, *, dash_escape="non-first", + use_persistent_args=True, + redir_child_stdin_from_fd=None, close_fds=(), **kwargs): + """Do the actual work of invoking the :program:`dialog`-like program. + + Communication with the :program:`dialog`-like program is + performed through one :manpage:`pipe(2)` and optionally a + user-specified file descriptor, depending on + *redir_child_stdin_from_fd*. The pipe allows the parent process + to read what :program:`dialog` writes on its standard error + stream [#]_. + + If *use_persistent_args* is ``True`` (the default), the elements + of ``self.dialog_persistent_arglist`` are passed as the first + arguments to ``self._dialog_prg``; otherwise, + ``self.dialog_persistent_arglist`` is not used at all. The + remaining arguments are those computed from *kwargs* followed by + the elements of *cmdargs*. + + If *dash_escape* is the string ``"non-first"``, then every + element of *cmdargs* that starts with ``'--'`` is escaped by + prepending an element consisting of ``'--'``, except the first + one (which is usually a :program:`dialog` option such as + ``'--yesno'``). In order to disable this escaping mechanism, + pass the string ``"none"`` as *dash_escape*. + + If *redir_child_stdin_from_fd* is not ``None``, it should be an + open file descriptor (i.e., an integer). That file descriptor + will be connected to :program:`dialog`'s standard input. This is + used by the gauge widget to feed data to :program:`dialog`, as + well as for :meth:`progressbox` in order to allow + :program:`dialog` to read data from a possibly-growing file. + + If *redir_child_stdin_from_fd* is ``None``, the standard input + in the child process (which runs :program:`dialog`) is not + redirected in any way. + + If *close_fds* is passed, it should be a sequence of file + descriptors that will be closed by the child process before it + exec()s the :program:`dialog`-like program. + + Notable exception: + + :exc:`PythonDialogOSError` (if any of the pipe(2) or close(2) + system calls fails...) + + .. [#] standard ouput stream if *use_stdout* is ``True`` + + """ + # We want to define DIALOG_OK, DIALOG_CANCEL, etc. in the + # environment of the child process so that we know (and + # even control) the possible dialog exit statuses. + new_environ = {} + new_environ.update(os.environ) + for var, value in self._lowlevel_exit_codes.items(): + varname = "DIALOG_" + var + new_environ[varname] = str(value) + if hasattr(self, "DIALOGRC"): + new_environ["DIALOGRC"] = self.DIALOGRC + + if dash_escape == "non-first": + # Escape all elements of 'cmdargs' that start with '--', except the + # first one. + cmdargs = self.dash_escape_nf(cmdargs) + elif dash_escape != "none": + raise PythonDialogBug("invalid value for 'dash_escape' parameter: " + "{0!r}".format(dash_escape)) + + arglist = [ self._dialog_prg ] + + if use_persistent_args: + arglist.extend(self.dialog_persistent_arglist) + + arglist.extend(_compute_common_args(kwargs) + cmdargs) + orig_args = arglist[:] # New object, copy of 'arglist' + + if self.pass_args_via_file: + tmpfile = tempfile.NamedTemporaryFile( + mode="w", prefix="pythondialog.tmp", delete=False) + with tmpfile as f: + f.write(' '.join( ( self._quote_arg_for_file_opt(arg) + for arg in arglist[1:] ) )) + args_file = tmpfile.name + arglist[1:] = ["--file", args_file] + else: + args_file = None + + if self._debug_enabled: + # Write the complete command line with environment variables + # setting to the debug log file (POSIX shell syntax for easy + # copy-pasting into a terminal, followed by repr(arglist)). + self._write_command_to_file( + new_environ, orig_args if self._expand_file_opt else arglist) + + # Create a pipe so that the parent process can read dialog's + # output on stderr (stdout with 'use_stdout') + with _OSErrorHandling(): + # rfd = File Descriptor for Reading + # wfd = File Descriptor for Writing + (child_output_rfd, child_output_wfd) = os.pipe() + + child_pid = os.fork() + if child_pid == 0: + # We are in the child process. We MUST NOT raise any exception. + try: + # 1) If the write end of a pipe isn't closed, the read end + # will never see EOF, which can indefinitely block the + # child waiting for input. To avoid this, the write end + # must be closed in the father *and* child processes. + # 2) The child process doesn't need child_output_rfd. + for fd in close_fds + (child_output_rfd,): + os.close(fd) + # We want: + # - to keep a reference to the father's stderr for error + # reporting (and use line-buffering for this stream); + # - dialog's output on stderr[*] to go to child_output_wfd; + # - data written to fd 'redir_child_stdin_from_fd' + # (if not None) to go to dialog's stdin. + # + # [*] stdout with 'use_stdout' + father_stderr = os.fdopen(os.dup(2), mode="w", buffering=1) + os.dup2(child_output_wfd, 1 if self.use_stdout else 2) + if redir_child_stdin_from_fd is not None: + os.dup2(redir_child_stdin_from_fd, 0) + + os.execve(self._dialog_prg, arglist, new_environ) + except: + print(traceback.format_exc(), file=father_stderr) + father_stderr.close() + os._exit(127) + + # Should not happen unless there is a bug in Python + os._exit(126) + + # We are in the father process. + # + # It is essential to close child_output_wfd, otherwise we will never + # see EOF while reading on child_output_rfd and the parent process + # will block forever on the read() call. + # [ after the fork(), the "reference count" of child_output_wfd from + # the operating system's point of view is 2; after the child exits, + # it is 1 until the father closes it itself; then it is 0 and a read + # on child_output_rfd encounters EOF once all the remaining data in + # the pipe has been read. ] + with _OSErrorHandling(): + os.close(child_output_wfd) + return (child_pid, child_output_rfd, args_file) + + def _wait_for_program_termination(self, child_pid, child_output_rfd): + """Wait for a :program:`dialog`-like process to terminate. + + This function waits for the specified process to terminate, + raises the appropriate exceptions in case of abnormal + termination and returns the :term:`Dialog exit code` and stderr + [#stream]_ output of the process as a tuple: :samp:`({hl_exit_code}, + {output_string})`. + + *child_output_rfd* must be the file descriptor for the + reading end of the pipe created by :meth:`_call_program`, the + writing end of which was connected by :meth:`_call_program` + to the child process's standard error [#stream]_. + + This function reads the process output on the standard error + [#stream]_ from *child_output_rfd* and closes this file + descriptor once this is done. + + Notable exceptions: + + - :exc:`DialogTerminatedBySignal` + - :exc:`DialogError` + - :exc:`PythonDialogErrorBeforeExecInChildProcess` + - :exc:`PythonDialogIOError` if the Python version is < 3.3 + - :exc:`PythonDialogOSError` + - :exc:`PythonDialogBug` + - :exc:`ProbablyPythonBug` + + .. [#stream] standard output if ``self.use_stdout`` is ``True`` + + """ + # Read dialog's output on its stderr (stdout with 'use_stdout') + with _OSErrorHandling(): + with os.fdopen(child_output_rfd, "r") as f: + child_output = f.read() + # The closing of the file object causes the end of the pipe we used + # to read dialog's output on its stderr to be closed too. This is + # important, otherwise invoking dialog enough times would + # eventually exhaust the maximum number of open file descriptors. + + exit_info = os.waitpid(child_pid, 0)[1] + if os.WIFEXITED(exit_info): + ll_exit_code = os.WEXITSTATUS(exit_info) + # As we wait()ed for the child process to terminate, there is no + # need to call os.WIFSTOPPED() + elif os.WIFSIGNALED(exit_info): + raise DialogTerminatedBySignal("the dialog-like program was " + "terminated by signal %d" % + os.WTERMSIG(exit_info)) + else: + raise PythonDialogBug("please report this bug to the " + "pythondialog maintainer(s)") + + if ll_exit_code == self._DIALOG_ERROR: + raise DialogError( + "the dialog-like program exited with status {0} (which was " + "passed to it as the DIALOG_ERROR environment variable). " + "Sometimes, the reason is simply that dialog was given a " + "height or width parameter that is too big for the terminal " + "in use. Its output, with leading and trailing whitespace " + "stripped, was:\n\n{1}".format(ll_exit_code, + child_output.strip())) + elif ll_exit_code == 127: + raise PythonDialogErrorBeforeExecInChildProcess(dedent("""\ + possible reasons include: + - the dialog-like program could not be executed (this can happen + for instance if the Python program is trying to call the + dialog-like program with arguments that cannot be represented + in the user's locale [LC_CTYPE]); + - the system is out of memory; + - the maximum number of open file descriptors has been reached; + - a cosmic ray hit the system memory and flipped nasty bits. + There ought to be a traceback above this message that describes + more precisely what happened.""")) + elif ll_exit_code == 126: + raise ProbablyPythonBug( + "a child process returned with exit status 126; this might " + "be the exit status of the dialog-like program, for some " + "unknown reason (-> probably a bug in the dialog-like " + "program); otherwise, we have probably found a python bug") + + try: + hl_exit_code = self._dialog_exit_code_ll_to_hl[ll_exit_code] + except KeyError: + raise PythonDialogBug( + "unexpected low-level exit status (new code?): {0!r}".format( + ll_exit_code)) + + return (hl_exit_code, child_output) + + def _handle_program_exit(self, child_pid, child_output_rfd, args_file): + """Handle exit of a :program:`dialog`-like process. + + This method: + + - waits for the :program:`dialog`-like program termination; + - removes the temporary file used to pass its argument list, + if any; + - and returns the appropriate :term:`Dialog exit code` along + with whatever output it produced. + + Notable exceptions: + + any exception raised by :meth:`_wait_for_program_termination` + + """ + try: + exit_code, output = \ + self._wait_for_program_termination(child_pid, + child_output_rfd) + finally: + with _OSErrorHandling(): + if args_file is not None and os.path.exists(args_file): + os.unlink(args_file) + + return (exit_code, output) + + def _perform(self, cmdargs, *, dash_escape="non-first", + use_persistent_args=True, **kwargs): + """Perform a complete :program:`dialog`-like program invocation. + + This method: + + - invokes the :program:`dialog`-like program; + - waits for its termination; + - removes the temporary file used to pass its argument list, + if any; + - and returns the appropriate :term:`Dialog exit code` along + with whatever output it produced. + + See :meth:`_call_program` for a description of the parameters. + + Notable exceptions: + + any exception raised by :meth:`_call_program` or + :meth:`_handle_program_exit` + + """ + child_pid, child_output_rfd, args_file = \ + self._call_program(cmdargs, dash_escape=dash_escape, + use_persistent_args=use_persistent_args, + **kwargs) + exit_code, output = self._handle_program_exit(child_pid, + child_output_rfd, + args_file) + + return (exit_code, output) + + def _strip_xdialog_newline(self, output): + """Remove trailing newline (if any) in \ +:program:`Xdialog`-compatibility mode""" + if self.compat == "Xdialog" and output.endswith("\n"): + output = output[:-1] + return output + + # This is for compatibility with the old dialog.py + def _perform_no_options(self, cmd): + """Call :program:`dialog` without passing any more options.""" + + warnings.warn("Dialog._perform_no_options() has been obsolete for " + "many years", DeprecationWarning) + return os.system(self._dialog_prg + ' ' + cmd) + + # For compatibility with the old dialog.py + def clear(self): + """Clear the screen. + + Equivalent to the :option:`--clear` option of :program:`dialog`. + + .. deprecated:: 2.03 + You may use the :manpage:`clear(1)` program instead. + cf. ``clear_screen()`` in :file:`examples/demo.py` for an + example. + + """ + warnings.warn("Dialog.clear() has been obsolete for many years.\n" + "You may use the clear(1) program to clear the screen.\n" + "cf. clear_screen() in examples/demo.py for an example", + DeprecationWarning) + self._perform_no_options('--clear') + + def _help_status_on(self, kwargs): + return ("--help-status" in self.dialog_persistent_arglist + or kwargs.get("help_status", False)) + + def _parse_quoted_string(self, s, start=0): + """Parse a quoted string from a :program:`dialog` help output.""" + if start >= len(s) or s[start] != '"': + raise PythonDialogBug("quoted string does not start with a double " + "quote: {0!r}".format(s)) + + l = [] + i = start + 1 + + while i < len(s) and s[i] != '"': + if s[i] == "\\": + i += 1 + if i >= len(s): + raise PythonDialogBug( + "quoted string ends with a backslash: {0!r}".format(s)) + l.append(s[i]) + i += 1 + + if s[i] != '"': + raise PythonDialogBug("quoted string does not and with a double " + "quote: {0!r}".format(s)) + + return (''.join(l), i+1) + + def _split_shellstyle_arglist(self, s): + """Split an argument list with shell-style quoting performed \ +by :program:`dialog`. + + Any argument in 's' may or may not be quoted. Quoted + arguments are always expected to be enclosed in double quotes + (more restrictive than what the POSIX shell allows). + + This function could maybe be replaced with shlex.split(), + however: + - shlex only handles Unicode strings in Python 2.7.3 and + above; + - the bulk of the work is done by _parse_quoted_string(), + which is probably still needed in _parse_help(), where + one needs to parse things such as 'HELP <id> <status>' in + which <id> may be quoted but <status> is never quoted, + even if it contains spaces or quotes. + + """ + s = s.rstrip() + l = [] + i = 0 + + while i < len(s): + if s[i] == '"': + arg, i = self._parse_quoted_string(s, start=i) + if i < len(s) and s[i] != ' ': + raise PythonDialogBug( + "expected a space or end-of-string after quoted " + "string in {0!r}, but found {1!r}".format(s, s[i])) + # Start of the next argument, or after the end of the string + i += 1 + l.append(arg) + else: + try: + end = s.index(' ', i) + except ValueError: + end = len(s) + + l.append(s[i:end]) + # Start of the next argument, or after the end of the string + i = end + 1 + + return l + + def _parse_help(self, output, kwargs, *, multival=False, + multival_on_single_line=False, raw_format=False): + """Parse the dialog help output from a widget. + + 'kwargs' should contain the keyword arguments used in the + widget call that produced the help output. + + 'multival' is for widgets that return a list of values as + opposed to a single value. + + 'raw_format' is for widgets that don't start their help + output with the string "HELP ". + + """ + l = output.splitlines() + + if raw_format: + # This format of the help output is either empty or consists of + # only one line (possibly terminated with \n). It is + # encountered with --calendar and --inputbox, among others. + if len(l) > 1: + raise PythonDialogBug("raw help feedback unexpected as " + "multiline: {0!r}".format(output)) + elif len(l) == 0: + return "" + else: + return l[0] + + # Simple widgets such as 'yesno' will fall in this case if they use + # this method. + if not l: + return None + + # The widgets that actually use --help-status always have the first + # help line indicating the active item; there is no risk of + # confusing this line with the first line produced by --help-status. + if not l[0].startswith("HELP "): + raise PythonDialogBug( + "unexpected help output that does not start with 'HELP ': " + "{0!r}".format(output)) + + # Everything that follows "HELP "; what it contains depends on whether + # --item-help and/or --help-tags were passed to dialog. + s = l[0][5:] + + if not self._help_status_on(kwargs): + return s + + if multival: + if multival_on_single_line: + args = self._split_shellstyle_arglist(s) + if not args: + raise PythonDialogBug( + "expected a non-empty space-separated list of " + "possibly-quoted strings in this help output: {0!r}" + .format(output)) + return (args[0], args[1:]) + else: + return (s, l[1:]) + else: + if not s: + raise PythonDialogBug( + "unexpected help output whose first line is 'HELP '") + elif s[0] != '"': + l2 = s.split(' ', 1) + if len(l2) == 1: + raise PythonDialogBug( + "expected 'HELP <id> <status>' in the help output, " + "but couldn't find any space after 'HELP '") + else: + return tuple(l2) + else: + help_id, after_index = self._parse_quoted_string(s) + if not s[after_index:].startswith(" "): + raise PythonDialogBug( + "expected 'HELP <quoted_id> <status>' in the help " + "output, but couldn't find any space after " + "'HELP <quoted_id>'") + return (help_id, s[after_index+1:]) + + def _widget_with_string_output(self, args, kwargs, + strip_xdialog_newline=False, + raw_help=False): + """Generic implementation for a widget that produces a single string. + + The help output must be present regardless of whether + --help-status was passed or not. + + """ + code, output = self._perform(args, **kwargs) + + if strip_xdialog_newline: + output = self._strip_xdialog_newline(output) + + if code == self.HELP: + # No check for --help-status + help_data = self._parse_help(output, kwargs, raw_format=raw_help) + return (code, help_data) + else: + return (code, output) + + def _widget_with_no_output(self, widget_name, args, kwargs): + """Generic implementation for a widget that produces no output.""" + code, output = self._perform(args, **kwargs) + + if output: + raise PythonDialogBug( + "expected an empty output from {0!r}, but got: {1!r}".format( + widget_name, output)) + + return code + + def _dialog_version_check(self, version_string, feature): + if self.compat == "dialog": + minimum_version = DialogBackendVersion.fromstring(version_string) + + if self.cached_backend_version < minimum_version: + raise InadequateBackendVersion( + "{0} requires dialog {1} or later, " + "but you seem to be using version {2}".format( + feature, minimum_version, self.cached_backend_version)) + + def backend_version(self): + """Get the version of the :program:`dialog`-like program (backend). + + If the version of the :program:`dialog`-like program can be + retrieved, return it as a string; otherwise, raise + :exc:`UnableToRetrieveBackendVersion`. + + This version is not to be confused with the pythondialog + version. + + In most cases, you should rather use the + :attr:`cached_backend_version` attribute of :class:`Dialog` + instances, because: + + - it avoids calling the backend every time one needs the + version; + - it is a :class:`BackendVersion` instance (or instance of a + subclass) that allows easy and reliable comparisons between + versions; + - the version string corresponding to a + :class:`BackendVersion` instance (or instance of a subclass) + can be obtained with :func:`str`. + + Notable exceptions: + + - :exc:`UnableToRetrieveBackendVersion` + - :exc:`PythonDialogReModuleError` + - any exception raised by :meth:`Dialog._perform` + + .. versionadded:: 2.12 + + .. versionchanged:: 2.14 + Raise :exc:`UnableToRetrieveBackendVersion` instead of + returning ``None`` when the version of the + :program:`dialog`-like program can't be retrieved. + + """ + code, output = self._perform(["--print-version"], + use_persistent_args=False) + + # Workaround for old dialog versions + if code == self.OK and not (output.strip() or self.use_stdout): + # output.strip() is empty and self.use_stdout is False. + # This can happen with old dialog versions (1.1-20100428 + # apparently does that). Try again, reading from stdout this + # time. + self.use_stdout = True + code, output = self._perform(["--stdout", "--print-version"], + use_persistent_args=False, + dash_escape="none") + self.use_stdout = False + + if code == self.OK: + try: + mo = self._print_version_cre.match(output) + if mo: + return mo.group("version") + else: + raise UnableToRetrieveBackendVersion( + "unable to parse the output of '{0} --print-version': " + "{1!r}".format(self._dialog_prg, output)) + except re.error as e: + raise PythonDialogReModuleError(str(e)) from e + else: + raise UnableToRetrieveBackendVersion( + "exit code {0!r} from the backend".format(code)) + + def maxsize(self, **kwargs): + """Get the maximum size of dialog boxes. + + If the exit status from the backend corresponds to + :attr:`Dialog.OK`, return a :samp:`({lines}, {cols})` tuple of + integers; otherwise, return ``None``. + + If you want to obtain the number of lines and columns of the + terminal, you should call this method with + ``use_persistent_args=False``, because :program:`dialog` options + such as :option:`--backtitle` modify the returned values. + + Notable exceptions: + + - :exc:`PythonDialogReModuleError` + - any exception raised by :meth:`Dialog._perform` + + .. versionadded:: 2.12 + + """ + code, output = self._perform(["--print-maxsize"], **kwargs) + if code == self.OK: + try: + mo = self._print_maxsize_cre.match(output) + if mo: + return tuple(map(int, mo.group("rows", "columns"))) + else: + raise PythonDialogBug( + "Unable to parse the output of '{0} --print-maxsize': " + "{1!r}".format(self._dialog_prg, output)) + except re.error as e: + raise PythonDialogReModuleError(str(e)) from e + else: + return None + + def _default_size(self, values, defaults): + # If 'autowidgetsize' is enabled, set the default values for the + # width/height/... parameters of widget-producing methods to 0 (this + # will actually be done by the caller, this function is only a helper). + if self.autowidgetsize: + defaults = (0,) * len(defaults) + + # For every element of 'values': keep it if different from None, + # otherwise replace it with the corresponding value from 'defaults'. + return [ v if v is not None else defaults[i] + for i, v in enumerate(values) ] + + @widget + def buildlist(self, text, height=0, width=0, list_height=0, items=[], + **kwargs): + """Display a buildlist box. + + :param str text: text to display in the box + :param int height: height of the box + :param int width: width of the box + :param int list_height: height of the selected and unselected + list boxes + :param items: + an iterable of :samp:`({tag}, {item}, {status})` tuples where + *status* specifies the initial selected/unselected state of + each entry; can be ``True`` or ``False``, ``1`` or ``0``, + ``"on"`` or ``"off"`` (``True``, ``1`` and ``"on"`` meaning + selected), or any case variation of these two strings. + + :return: a tuple of the form :samp:`({code}, {tags})` where: + + - *code* is a :term:`Dialog exit code`; + - *tags* is a list of the tags corresponding to the selected + items, in the order they have in the list on the right. + + :rtype: tuple + + A :meth:`!buildlist` dialog is similar in logic to the + :meth:`checklist`, but differs in presentation. In this widget, + two lists are displayed, side by side. The list on the left + shows unselected items. The list on the right shows selected + items. As items are selected or unselected, they move between + the two lists. The *status* component of *items* specifies which + items are initially selected. + + +--------------+------------------------------------------------+ + | Key | Action | + +==============+================================================+ + | :kbd:`Space` | select or deselect the highlighted item, | + | | *i.e.*, move it between the left and right | + | | lists | + +--------------+------------------------------------------------+ + | :kbd:`^` | move the focus to the left list | + +--------------+------------------------------------------------+ + | :kbd:`$` | move the focus to the right list | + +--------------+------------------------------------------------+ + | :kbd:`Tab` | move focus (see *visit_items* below) | + +--------------+------------------------------------------------+ + | :kbd:`Enter` | press the focused button | + +--------------+------------------------------------------------+ + + If called with ``visit_items=True``, the :kbd:`Tab` key can move + the focus to the left and right lists, which is probably more + intuitive for users than the default behavior that requires + using :kbd:`^` and :kbd:`$` for this purpose. + + This widget requires dialog >= 1.2-20121230. + + Notable exceptions: + + any exception raised by :meth:`Dialog._perform` or :func:`_to_onoff` + + .. versionadded:: 3.0 + + """ + self._dialog_version_check("1.2-20121230", "the buildlist widget") + + cmd = ["--buildlist", text, str(height), str(width), str(list_height)] + for t in items: + cmd.extend([ t[0], t[1], _to_onoff(t[2]) ] + list(t[3:])) + + code, output = self._perform(cmd, **kwargs) + + if code == self.HELP: + help_data = self._parse_help(output, kwargs, multival=True, + multival_on_single_line=True) + if self._help_status_on(kwargs): + help_id, selected_tags = help_data + items = [ [ tag, item, tag in selected_tags ] + rest + for (tag, item, status, *rest) in items ] + return (code, (help_id, selected_tags, items)) + else: + return (code, help_data) + elif code in (self.OK, self.EXTRA): + return (code, self._split_shellstyle_arglist(output)) + else: + return (code, None) + + def _calendar_parse_date(self, date_str): + try: + mo = _calendar_date_cre.match(date_str) + except re.error as e: + raise PythonDialogReModuleError(str(e)) from e + + if not mo: + raise UnexpectedDialogOutput( + "the dialog-like program returned the following " + "unexpected output (a date string was expected) from the " + "calendar box: {0!r}".format(date_str)) + + return [ int(s) for s in mo.group("day", "month", "year") ] + + @widget + def calendar(self, text, height=None, width=0, day=-1, month=-1, year=-1, + **kwargs): + """Display a calendar dialog box. + + :param str text: text to display in the box + :param height: height of the box (minus the calendar height) + :type height: int or ``None`` + :param int width: width of the box + :param int day: inititial day highlighted + :param int month: inititial month displayed + :param int year: inititial year selected + :return: a tuple of the form :samp:`({code}, {date})` where: + + - *code* is a :term:`Dialog exit code`; + - *date* is a list of the form :samp:`[{day}, {month}, + {year}]`, where *day*, *month* and *year* are integers + corresponding to the date chosen by the user. + + :rtype: tuple + + A :meth:`!calendar` box displays day, month and year in + separately adjustable windows. If *year* is given as ``0``, the + current date is used as initial value; otherwise, if any of the + values for *day*, *month* and *year* is negative, the current + date's corresponding value is used. You can increment or + decrement any of those using the :kbd:`Left`, :kbd:`Up`, + :kbd:`Right` and :kbd:`Down` arrows. Use :kbd:`Tab` or + :kbd:`Backtab` to move between windows. + + Default values for the size parameters when the + :ref:`autowidgetsize <autowidgetsize>` option is disabled: + ``height=6, width=0``. + + Notable exceptions: + + - any exception raised by :meth:`Dialog._perform` + - :exc:`UnexpectedDialogOutput` + - :exc:`PythonDialogReModuleError` + + .. versionchanged:: 3.2 + The default values for *day*, *month* and *year* have been + changed from ``0`` to ``-1``. + + """ + (height,) = self._default_size((height, ), (6,)) + (code, output) = self._perform( + ["--calendar", text, str(height), str(width), str(day), + str(month), str(year)], + **kwargs) + + if code == self.HELP: + # The output does not depend on whether --help-status was passed + # (dialog 1.2-20130902). + help_data = self._parse_help(output, kwargs, raw_format=True) + return (code, self._calendar_parse_date(help_data)) + elif code in (self.OK, self.EXTRA): + return (code, self._calendar_parse_date(output)) + else: + return (code, None) + + @widget + def checklist(self, text, height=None, width=None, list_height=None, + choices=[], **kwargs): + """Display a checklist box. + + :param str text: text to display in the box + :param height: height of the box + :type height: int or ``None`` + :param width: width of the box + :type width: int or ``None`` + :param list_height: + number of entries displayed in the box at a given time (the + contents can be scrolled) + :type list_height: int or ``None`` + :param choices: + an iterable of :samp:`({tag}, {item}, {status})` tuples where + *status* specifies the initial selected/unselected state of + each entry; can be ``True`` or ``False``, ``1`` or ``0``, + ``"on"`` or ``"off"`` (``True``, ``1`` and ``"on"`` meaning + selected), or any case variation of these two strings. + :return: a tuple of the form :samp:`({code}, [{tag}, ...])` + whose first element is a :term:`Dialog exit code` and second + element lists all tags for the entries selected by the user. + If the user exits with :kbd:`Esc` or :guilabel:`Cancel`, the + returned tag list is empty. + + :rtype: tuple + + Default values for the size parameters when the + :ref:`autowidgetsize <autowidgetsize>` option is disabled: + ``height=15, width=54, list_height=7``. + + Notable exceptions: + + any exception raised by :meth:`Dialog._perform` or :func:`_to_onoff` + + """ + height, width, list_height = self._default_size( + (height, width, list_height), (15, 54, 7)) + cmd = ["--checklist", text, str(height), str(width), str(list_height)] + for t in choices: + t = [ t[0], t[1], _to_onoff(t[2]) ] + list(t[3:]) + cmd.extend(t) + + # The dialog output cannot be parsed reliably (at least in dialog + # 0.9b-20040301) without --separate-output (because double quotes in + # tags are escaped with backslashes, but backslashes are not + # themselves escaped and you have a problem when a tag ends with a + # backslash--the output makes you think you've encountered an embedded + # double-quote). + kwargs["separate_output"] = True + + (code, output) = self._perform(cmd, **kwargs) + # Since we used --separate-output, the tags are separated by a newline + # in the output. There is also a final newline after the last tag. + + if code == self.HELP: + help_data = self._parse_help(output, kwargs, multival=True) + if self._help_status_on(kwargs): + help_id, selected_tags = help_data + choices = [ [ tag, item, tag in selected_tags ] + rest + for (tag, item, status, *rest) in choices ] + return (code, (help_id, selected_tags, choices)) + else: + return (code, help_data) + else: + return (code, output.split('\n')[:-1]) + + def _form_updated_items(self, status, elements): + """Return a complete list with up-to-date items from 'status'. + + Return a new list of same length as 'elements'. Items are + taken from 'status', except when data inside 'elements' + indicates a read-only field: such items are not output by + dialog ... --help-status ..., and therefore have to be + extracted from 'elements' instead of 'status'. + + Actually, for 'mixedform', the elements that are defined as + read-only using the attribute instead of a non-positive + field_length are not concerned by this function, since they + are included in the --help-status output. + + """ + res = [] + for i, (label, yl, xl, item, yi, xi, field_length, *rest) \ + in enumerate(elements): + res.append(status[i] if field_length > 0 else item) + + return res + + def _generic_form(self, widget_name, method_name, text, elements, height=0, + width=0, form_height=0, **kwargs): + cmd = ["--%s" % widget_name, text, str(height), str(width), + str(form_height)] + + if not elements: + raise BadPythonDialogUsage( + "{0}.{1}.{2}: empty ELEMENTS sequence: {3!r}".format( + __name__, type(self).__name__, method_name, elements)) + + elt_len = len(elements[0]) # for consistency checking + for i, elt in enumerate(elements): + if len(elt) != elt_len: + raise BadPythonDialogUsage( + "{0}.{1}.{2}: ELEMENTS[0] has length {3}, whereas " + "ELEMENTS[{4}] has length {5}".format( + __name__, type(self).__name__, method_name, + elt_len, i, len(elt))) + + # Give names to make the code more readable + if widget_name in ("form", "passwordform"): + label, yl, xl, item, yi, xi, field_length, input_length = \ + elt[:8] + rest = elt[8:] # optional "item_help" string + elif widget_name == "mixedform": + label, yl, xl, item, yi, xi, field_length, input_length, \ + attributes = elt[:9] + rest = elt[9:] # optional "item_help" string + else: + raise PythonDialogBug( + "unexpected widget name in {0}.{1}._generic_form(): " + "{2!r}".format(__name__, type(self).__name__, widget_name)) + + for name, value in (("label", label), ("item", item)): + if not isinstance(value, str): + raise BadPythonDialogUsage( + "{0}.{1}.{2}: {3!r} element not a string: {4!r}".format( + __name__, type(self).__name__, + method_name, name, value)) + + cmd.extend((label, str(yl), str(xl), item, str(yi), str(xi), + str(field_length), str(input_length))) + if widget_name == "mixedform": + cmd.append(str(attributes)) + # "item help" string when using --item-help, nothing otherwise + cmd.extend(rest) + + (code, output) = self._perform(cmd, **kwargs) + + if code == self.HELP: + help_data = self._parse_help(output, kwargs, multival=True) + if self._help_status_on(kwargs): + help_id, status = help_data + # 'status' does not contain the fields marked as read-only in + # 'elements'. Build a list containing all up-to-date items. + updated_items = self._form_updated_items(status, elements) + # Reconstruct 'elements' with the updated items taken from + # 'status'. + elements = [ [ label, yl, xl, updated_item ] + rest for + ((label, yl, xl, item, *rest), updated_item) in + zip(elements, updated_items) ] + return (code, (help_id, status, elements)) + else: + return (code, help_data) + else: + return (code, output.split('\n')[:-1]) + + @widget + def form(self, text, elements, height=0, width=0, form_height=0, **kwargs): + """Display a form consisting of labels and fields. + + :param str text: text to display in the box + :param elements: sequence describing the labels and + fields (see below) + :param int height: height of the box + :param int width: width of the box + :param int form_height: number of form lines displayed at the + same time + :return: a tuple of the form :samp:`({code}, {list})` where: + + - *code* is a :term:`Dialog exit code`; + - *list* gives the contents of every editable field on exit, + with the same order as in *elements*. + + :rtype: tuple + + A :meth:`!form` box consists in a series of :dfn:`fields` and + associated :dfn:`labels`. This type of dialog is suitable for + adjusting configuration parameters and similar tasks. + + Each element of *elements* must itself be a sequence + :samp:`({label}, {yl}, {xl}, {item}, {yi}, {xi}, {field_length}, + {input_length})` containing the various parameters concerning a + given field and the associated label. + + *label* is a string that will be displayed at row *yl*, column + *xl*. *item* is a string giving the initial value for the field, + which will be displayed at row *yi*, column *xi* (row and column + numbers starting from 1). + + *field_length* and *input_length* are integers that respectively + specify the number of characters used for displaying the field + and the maximum number of characters that can be entered for + this field. These two integers also determine whether the + contents of the field can be modified, as follows: + + - if *field_length* is zero, the field cannot be altered and + its contents determines the displayed length; + + - if *field_length* is negative, the field cannot be altered + and the opposite of *field_length* gives the displayed + length; + + - if *input_length* is zero, it is set to *field_length*. + + Notable exceptions: + + - :exc:`BadPythonDialogUsage` + - any exception raised by :meth:`Dialog._perform` + + """ + return self._generic_form("form", "form", text, elements, + height, width, form_height, **kwargs) + + @widget + def passwordform(self, text, elements, height=0, width=0, form_height=0, + **kwargs): + """Display a form consisting of labels and invisible fields. + + This widget is identical to the :meth:`form` box, except that + all text fields are treated as :meth:`passwordbox` widgets + rather than :meth:`inputbox` widgets. + + By default (as in :program:`dialog`), nothing is echoed to the + terminal as the user types in the invisible fields. This can be + confusing to users. Use ``insecure=True`` (keyword argument) if + you want an asterisk to be echoed for each character entered by + the user. + + Notable exceptions: + + - :exc:`BadPythonDialogUsage` + - any exception raised by :meth:`Dialog._perform` + + """ + return self._generic_form("passwordform", "passwordform", text, + elements, height, width, form_height, + **kwargs) + + @widget + def mixedform(self, text, elements, height=0, width=0, form_height=0, + **kwargs): + """Display a form consisting of labels and fields. + + :param str text: text to display in the box + :param elements: sequence describing the labels and + fields (see below) + :param int height: height of the box + :param int width: width of the box + :param int form_height: number of form lines displayed at the + same time + :return: a tuple of the form :samp:`({code}, {list})` where: + + - *code* is a :term:`Dialog exit code`; + - *list* gives the contents of every field on exit, with the + same order as in *elements*. + + :rtype: tuple + + A :meth:`!mixedform` box is very similar to a :meth:`form` box, + and differs from the latter by allowing field attributes to be + specified. + + Each element of *elements* must itself be a sequence + :samp:`({label}, {yl}, {xl}, {item}, {yi}, {xi}, {field_length}, + {input_length}, {attributes})` containing the various parameters + concerning a given field and the associated label. + + *attributes* is an integer interpreted as a bit mask with the + following meaning (bit 0 being the least significant bit): + + +------------+-----------------------------------------------+ + | Bit number | Meaning | + +============+===============================================+ + | 0 | the field should be hidden (e.g., a password) | + +------------+-----------------------------------------------+ + | 1 | the field should be read-only (e.g., a label) | + +------------+-----------------------------------------------+ + + For all other parameters, please refer to the documentation of + the :meth:`form` box. + + The return value is the same as would be with the :meth:`!form` + box, except that fields marked as read-only with bit 1 of + *attributes* are also included in the output list. + + Notable exceptions: + + - :exc:`BadPythonDialogUsage` + - any exception raised by :meth:`Dialog._perform` + + """ + return self._generic_form("mixedform", "mixedform", text, elements, + height, width, form_height, **kwargs) + + @widget + def dselect(self, filepath, height=0, width=0, **kwargs): + """Display a directory selection dialog box. + + :param str filepath: initial path + :param int height: height of the box + :param int width: width of the box + :return: a tuple of the form :samp:`({code}, {path})` where: + + - *code* is a :term:`Dialog exit code`; + - *path* is the directory chosen by the user. + + :rtype: tuple + + The directory selection dialog displays a text entry window + in which you can type a directory, and above that a window + with directory names. + + Here, *filepath* can be a path to a file, in which case the + directory window will display the contents of the path and the + text entry window will contain the preselected directory. + + Use :kbd:`Tab` or the arrow keys to move between the windows. + Within the directory window, use the :kbd:`Up` and :kbd:`Down` + arrow keys to scroll the current selection. Use the :kbd:`Space` + bar to copy the current selection into the text entry window. + + Typing any printable character switches focus to the text entry + window, entering that character as well as scrolling the + directory window to the closest match. + + Use :kbd:`Enter` or the :guilabel:`OK` button to accept the + current value in the text entry window and exit. + + Notable exceptions: + + any exception raised by :meth:`Dialog._perform` + + """ + # The help output does not depend on whether --help-status was passed + # (dialog 1.2-20130902). + return self._widget_with_string_output( + ["--dselect", filepath, str(height), str(width)], + kwargs, raw_help=True) + + @widget + def editbox(self, filepath, height=0, width=0, **kwargs): + """Display a basic text editor dialog box. + + :param str filepath: path to a file which determines the initial + contents of the dialog box + :param int height: height of the box + :param int width: width of the box + :return: a tuple of the form :samp:`({code}, {text})` where: + + - *code* is a :term:`Dialog exit code`; + - *text* is the contents of the text entry window on exit. + + :rtype: tuple + + The :meth:`!editbox` dialog displays a copy of the file + contents. You may edit it using the :kbd:`Backspace`, + :kbd:`Delete` and cursor keys to correct typing errors. It also + recognizes :kbd:`Page Up` and :kbd:`Page Down`. Unlike the + :meth:`inputbox`, you must tab to the :guilabel:`OK` or + :guilabel:`Cancel` buttons to close the dialog. Pressing the + :kbd:`Enter` key within the box will split the corresponding + line. + + Notable exceptions: + + any exception raised by :meth:`Dialog._perform` + + .. seealso:: method :meth:`editbox_str` + + """ + return self._widget_with_string_output( + ["--editbox", filepath, str(height), str(width)], + kwargs) + + def editbox_str(self, init_contents, *args, **kwargs): + """ + Display a basic text editor dialog box (wrapper around :meth:`editbox`). + + :param str init_contents: + initial contents of the dialog box + :param args: positional arguments to pass to :meth:`editbox` + :param kwargs: keyword arguments to pass to :meth:`editbox` + :return: a tuple of the form :samp:`({code}, {text})` where: + + - *code* is a :term:`Dialog exit code`; + - *text* is the contents of the text entry window on exit. + + :rtype: tuple + + The :meth:`!editbox_str` method is a thin wrapper around + :meth:`editbox`. :meth:`!editbox_str` accepts a string as its + first argument, instead of a file path. That string is written + to a temporary file whose path is passed to :meth:`!editbox` + along with the arguments specified via *args* and *kwargs*. + Please refer to :meth:`!editbox`\'s documentation for more + details. + + Notes: + + - the temporary file is deleted before the method returns; + - if *init_contents* does not end with a newline character + (``'\\n'``), then this method automatically adds one. This + is done in order to avoid unexpected behavior resulting from + the fact that, before version 1.3-20160209, + :program:`dialog`\'s editbox widget ignored the last line of + the input file unless it was terminated by a newline + character. + + Notable exceptions: + + - :exc:`PythonDialogOSError` + - any exception raised by :meth:`Dialog._perform` + + .. versionadded:: 3.4 + + .. seealso:: method :meth:`editbox` + + """ + if not init_contents.endswith('\n'): + # Before version 1.3-20160209, dialog's --editbox widget + # doesn't read the last line of the input file unless it + # ends with a '\n' character. + init_contents += '\n' + + with _OSErrorHandling(): + tmpfile = tempfile.NamedTemporaryFile( + mode="w", prefix="pythondialog.tmp", delete=False) + try: + with tmpfile as f: + f.write(init_contents) + # The temporary file is now closed. According to the tempfile + # module documentation, this is necessary if we want to be able + # to reopen it reliably regardless of the platform. + + res = self.editbox(tmpfile.name, *args, **kwargs) + finally: + # The test should always succeed, but I prefer being on the + # safe side. + if os.path.exists(tmpfile.name): + os.unlink(tmpfile.name) + + return res + + @widget + def fselect(self, filepath, height=0, width=0, **kwargs): + """Display a file selection dialog box. + + :param str filepath: initial path + :param int height: height of the box + :param int width: width of the box + :return: a tuple of the form :samp:`({code}, {path})` where: + + - *code* is a :term:`Dialog exit code`; + - *path* is the path chosen by the user (the last element of + which may be a directory or a file). + + :rtype: tuple + + The file selection dialog displays a text entry window in + which you can type a file name (or directory), and above that + two windows with directory names and file names. + + Here, *filepath* can be a path to a file, in which case the file + and directory windows will display the contents of the path and + the text entry window will contain the preselected file name. + + Use :kbd:`Tab` or the arrow keys to move between the windows. + Within the directory or file name windows, use the :kbd:`Up` and + :kbd:`Down` arrow keys to scroll the current selection. Use the + :kbd:`Space` bar to copy the current selection into the text + entry window. + + Typing any printable character switches focus to the text entry + window, entering that character as well as scrolling the + directory and file name windows to the closest match. + + Use :kbd:`Enter` or the :guilabel:`OK` button to accept the + current value in the text entry window, or the + :guilabel:`Cancel` button to cancel. + + Notable exceptions: + + any exception raised by :meth:`Dialog._perform` + + """ + # The help output does not depend on whether --help-status was passed + # (dialog 1.2-20130902). + return self._widget_with_string_output( + ["--fselect", filepath, str(height), str(width)], + kwargs, strip_xdialog_newline=True, raw_help=True) + + def gauge_start(self, text="", height=None, width=None, percent=0, + **kwargs): + """Display a gauge box. + + :param str text: text to display in the box + :param height: height of the box + :type height: int or ``None`` + :param width: width of the box + :type width: int or ``None`` + :param int percent: initial percentage shown in the meter + :return: undefined + + A gauge box displays a meter along the bottom of the box. The + meter indicates a percentage. + + This function starts the :program:`dialog`-like program, telling + it to display a gauge box containing a text and an initial + percentage in the meter. + + + .. rubric:: Gauge typical usage + + Gauge typical usage (assuming that *d* is an instance of the + :class:`Dialog` class) looks like this:: + + d.gauge_start() + # do something + d.gauge_update(10) # 10% of the whole task is done + # ... + d.gauge_update(100, "any text here") # work is done + exit_code = d.gauge_stop() # cleanup actions + + + Default values for the size parameters when the + :ref:`autowidgetsize <autowidgetsize>` option is disabled: + ``height=8, width=54``. + + Notable exceptions: + + - any exception raised by :meth:`_call_program` + - :exc:`PythonDialogOSError` + + """ + height, width = self._default_size((height, width), (8, 54)) + with _OSErrorHandling(): + # We need a pipe to send data to the child (dialog) process's + # stdin while it is running. + # rfd = File Descriptor for Reading + # wfd = File Descriptor for Writing + (child_stdin_rfd, child_stdin_wfd) = os.pipe() + + child_pid, child_output_rfd, args_file = self._call_program( + ["--gauge", text, str(height), str(width), str(percent)], + redir_child_stdin_from_fd=child_stdin_rfd, + close_fds=(child_stdin_wfd,), **kwargs) + + # fork() is done. We don't need child_stdin_rfd in the father + # process anymore. + os.close(child_stdin_rfd) + + self._gauge_process = { + "pid": child_pid, + "stdin": os.fdopen(child_stdin_wfd, "w"), + "child_output_rfd": child_output_rfd, + "args_file": args_file + } + + def gauge_update(self, percent, text="", update_text=False): + """Update a running gauge box. + + :param int percent: new percentage to show in the gauge + meter + :param str text: new text to optionally display in the + box + :param bool update_text: whether to update the text in the box + :return: undefined + + This function updates the percentage shown by the meter of a + running gauge box (meaning :meth:`gauge_start` must have been + called previously). If *update_text* is ``True``, the text + displayed in the box is also updated. + + See the :meth:`gauge_start` method documentation for information + about how to use a gauge. + + Notable exception: + + :exc:`PythonDialogIOError` (:exc:`PythonDialogOSError` from + Python 3.3 onwards) can be raised if there is an I/O error + while trying to write to the pipe used to talk to the + :program:`dialog`-like program. + + """ + if not isinstance(percent, int): + raise BadPythonDialogUsage( + "the 'percent' argument of gauge_update() must be an integer, " + "but {0!r} is not".format(percent)) + + if update_text: + gauge_data = "XXX\n{0}\n{1}\nXXX\n".format(percent, text) + else: + gauge_data = "{0}\n".format(percent) + with _OSErrorHandling(): + self._gauge_process["stdin"].write(gauge_data) + self._gauge_process["stdin"].flush() + + # For "compatibility" with the old dialog.py... + def gauge_iterate(*args, **kwargs): + """Update a running gauge box. + + .. deprecated:: 2.03 + Use :meth:`gauge_update` instead. + + """ + warnings.warn("Dialog.gauge_iterate() has been obsolete for " + "many years", DeprecationWarning) + gauge_update(*args, **kwargs) + + @widget + @retval_is_code + def gauge_stop(self): + """Terminate a running gauge widget. + + :return: a :term:`Dialog exit code` + :rtype: str + + This function performs the appropriate cleanup actions to + terminate a running gauge started with :meth:`gauge_start`. + + See the :meth:`!gauge_start` method documentation for + information about how to use a gauge. + + Notable exceptions: + + - any exception raised by :meth:`_handle_program_exit`; + - :exc:`PythonDialogIOError` (:exc:`PythonDialogOSError` from + Python 3.3 onwards) can be raised if closing the pipe used + to talk to the :program:`dialog`-like program fails. + + """ + p = self._gauge_process + # Close the pipe that we are using to feed dialog's stdin + with _OSErrorHandling(): + p["stdin"].close() + # According to dialog(1), the output should always be empty. + exit_code = self._handle_program_exit(p["pid"], + p["child_output_rfd"], + p["args_file"])[0] + return exit_code + + @widget + @retval_is_code + def infobox(self, text, height=None, width=None, **kwargs): + """Display an information dialog box. + + :param str text: text to display in the box + :param height: height of the box + :type height: int or ``None`` + :param width: width of the box + :type width: int or ``None`` + :return: a :term:`Dialog exit code` + :rtype: str + + An info box is basically a message box. However, in this case, + :program:`dialog` will exit immediately after displaying the + message to the user. The screen is not cleared when + :program:`dialog` exits, so that the message will remain on the + screen after the method returns. This is useful when you want to + inform the user that some operations are carrying on that may + require some time to finish. + + Default values for the size parameters when the + :ref:`autowidgetsize <autowidgetsize>` option is disabled: + ``height=10, width=30``. + + Notable exceptions: + + any exception raised by :meth:`Dialog._perform` + + """ + height, width = self._default_size((height, width), (10, 30)) + return self._widget_with_no_output( + "infobox", + ["--infobox", text, str(height), str(width)], + kwargs) + + @widget + def inputbox(self, text, height=None, width=None, init='', **kwargs): + """Display an input dialog box. + + :param str text: text to display in the box + :param height: height of the box + :type height: int or ``None`` + :param width: width of the box + :type width: int or ``None`` + :param str init: default input string + :return: a tuple of the form :samp:`({code}, {string})` where: + + - *code* is a :term:`Dialog exit code`; + - *string* is the string entered by the user. + + :rtype: tuple + + An input box is useful when you want to ask questions that + require the user to input a string as the answer. If *init* is + supplied, it is used to initialize the input string. When + entering the string, the :kbd:`Backspace` key can be used to + correct typing errors. If the input string is longer than can + fit in the dialog box, the input field will be scrolled. + + Default values for the size parameters when the + :ref:`autowidgetsize <autowidgetsize>` option is disabled: + ``height=10, width=30``. + + Notable exceptions: + + any exception raised by :meth:`Dialog._perform` + + """ + height, width = self._default_size((height, width), (10, 30)) + # The help output does not depend on whether --help-status was passed + # (dialog 1.2-20130902). + return self._widget_with_string_output( + ["--inputbox", text, str(height), str(width), init], + kwargs, strip_xdialog_newline=True, raw_help=True) + + @widget + def inputmenu(self, text, height=0, width=None, menu_height=None, + choices=[], **kwargs): + """Display an inputmenu dialog box. + + :param str text: text to display in the box + :param int height: height of the box + :param width: width of the box + :type width: int or ``None`` + :param menu_height: height of the menu (scrollable part) + :type menu_height: int or ``None`` + :param choices: an iterable of :samp:`({tag}, {item})` + tuples, the meaning of which is explained + below + :return: see :ref:`below <inputmenu-return-value>` + + + .. rubric:: Overview + + An :meth:`!inputmenu` box is a dialog box that can be used to + present a list of choices in the form of a menu for the user to + choose. Choices are displayed in the given order. The main + differences with the :meth:`menu` dialog box are: + + - entries are not automatically centered, but left-adjusted; + + - the current entry can be renamed by pressing the + :guilabel:`Rename` button, which allows editing the *item* + part of the current entry. + + Each menu entry consists of a *tag* string and an *item* string. + The :dfn:`tag` gives the entry a name to distinguish it from the + other entries in the menu and to provide quick keyboard access. + The :dfn:`item` is a short description of the option that the + entry represents. + + The user can move between the menu entries by pressing the + :kbd:`Up` and :kbd:`Down` arrow keys or the first letter of the + tag as a hot key. There are *menu_height* lines (not entries!) + displayed in the scrollable part of the menu at one time. + + At the time of this writing (with :program:`dialog` + 1.2-20140219), it is not possible to add an Extra button to this + widget, because internally, the :guilabel:`Rename` button *is* + the Extra button. + + .. note:: + + It is strongly advised not to put any space in tags, otherwise + the :program:`dialog` output can be ambiguous if the + corresponding entry is renamed, causing pythondialog to return + a wrong tag string and new item text. + + The reason is that in this case, the :program:`dialog` output + is :samp:`RENAMED {tag} {item}` and pythondialog cannot guess + whether spaces after the :samp:`RENAMED` + *space* prefix + belong to the *tag* or the new *item* text. + + .. note:: + + There is no point in calling this method with + ``help_status=True``, because it is not possible to rename + several items nor is it possible to choose the + :guilabel:`Help` button (or any button other than + :guilabel:`Rename`) once one has started to rename an item. + + .. _inputmenu-return-value: + + .. rubric:: Return value + + Return a tuple of the form :samp:`({exit_info}, {tag}, + {new_item_text})` where: + + + *exit_info* is either: + + - the string ``"accepted"``, meaning that an entry was + accepted without renaming; + - the string ``"renamed"``, meaning that an entry was + accepted after being renamed; + - one of the standard :term:`Dialog exit codes <Dialog exit + code>` :attr:`Dialog.CANCEL`, :attr:`Dialog.ESC` or + :attr:`Dialog.HELP` (:attr:`Dialog.EXTRA` can't be + returned, because internally, the :guilabel:`Rename` + button *is* the Extra button). + + + *tag* indicates which entry was accepted (with or without + renaming), if any. If no entry was accepted (e.g., if the + dialog was exited with the :guilabel:`Cancel` button), then + *tag* is ``None``. + + + *new_item_text* gives the new *item* part of the renamed + entry if *exit_info* is ``"renamed"``, otherwise it is + ``None``. + + Default values for the size parameters when the + :ref:`autowidgetsize <autowidgetsize>` option is disabled: + ``height=0, width=60, menu_height=7``. + + Notable exceptions: + + any exception raised by :meth:`Dialog._perform` + + """ + width, menu_height = self._default_size((width, menu_height), (60, 7)) + cmd = ["--inputmenu", text, str(height), str(width), str(menu_height)] + for t in choices: + cmd.extend(t) + (code, output) = self._perform(cmd, **kwargs) + + if code == self.HELP: + help_id = self._parse_help(output, kwargs) + return (code, help_id, None) + elif code == self.OK: + return ("accepted", output, None) + elif code == self.EXTRA: + if not output.startswith("RENAMED "): + raise PythonDialogBug( + "'output' does not start with 'RENAMED ': {0!r}".format( + output)) + t = output.split(' ', 2) + return ("renamed", t[1], t[2]) + else: + return (code, None, None) + + @widget + def menu(self, text, height=None, width=None, menu_height=None, choices=[], + **kwargs): + """Display a menu dialog box. + + :param str text: text to display in the box + :param height: height of the box + :type height: int or ``None`` + :param width: width of the box + :type width: int or ``None`` + :param menu_height: number of entries displayed in the box + (which can be scrolled) at a given time + :type menu_height: int or ``None`` + :param choices: an iterable of :samp:`({tag}, {item})` + tuples, the meaning of which is explained + below + :return: a tuple of the form :samp:`({code}, {tag})` where: + + - *code* is a :term:`Dialog exit code`; + - *tag* is the tag string corresponding to the item that the + user chose. + + :rtype: tuple + + As its name suggests, a :meth:`!menu` box is a dialog box that + can be used to present a list of choices in the form of a menu + for the user to choose. Choices are displayed in the given + order. + + Each menu entry consists of a *tag* string and an *item* string. + The :dfn:`tag` gives the entry a name to distinguish it from the + other entries in the menu and to provide quick keyboard access. + The :dfn:`item` is a short description of the option that the + entry represents. + + The user can move between the menu entries by pressing the + :kbd:`Up` and :kbd:`Down` arrow keys, the first letter of the + tag as a hotkey, or the number keys :kbd:`1` through :kbd:`9`. + There are *menu_height* entries displayed in the menu at one + time, but it will be scrolled if there are more entries than + that. + + Default values for the size parameters when the + :ref:`autowidgetsize <autowidgetsize>` option is disabled: + ``height=15, width=54, menu_height=7``. + + Notable exceptions: + + any exception raised by :meth:`Dialog._perform` + + """ + height, width, menu_height = self._default_size( + (height, width, menu_height), (15, 54, 7)) + cmd = ["--menu", text, str(height), str(width), str(menu_height)] + for t in choices: + cmd.extend(t) + + return self._widget_with_string_output( + cmd, kwargs, strip_xdialog_newline=True) + + @widget + @retval_is_code + def mixedgauge(self, text, height=0, width=0, percent=0, elements=[], + **kwargs): + """Display a mixed gauge dialog box. + + :param str text: text to display in the middle of the box, + between the elements list and the progress + bar + :param int height: height of the box + :param int width: width of the box + :param int percent: integer giving the percentage for the global + progress bar + :param elements: an iterable of :samp:`({tag}, {item})` + tuples, the meaning of which is explained + below + :return: a :term:`Dialog exit code` + :rtype: str + + A :meth:`!mixedgauge` box displays a list of "elements" with + status indication for each of them, followed by a text and + finally a global progress bar along the bottom of the box. + + The top part ("elements") is suitable for displaying a task + list. One element is displayed per line, with its *tag* part on + the left and its *item* part on the right. The *item* part is a + string that is displayed on the right of the same line. + + The *item* part of an element can be an arbitrary string. + Special values listed in the :manpage:`dialog(3)` manual page + are translated into a status indication for the corresponding + task (*tag*), such as: "Succeeded", "Failed", "Passed", + "Completed", "Done", "Skipped", "In Progress", "Checked", "N/A" + or a progress bar. + + A progress bar for an element is obtained by supplying a + negative number for the *item*. For instance, ``"-75"`` will + cause a progress bar indicating 75% to be displayed on the + corresponding line. + + For your convenience, if an *item* appears to be an integer or a + float, it will be converted to a string before being passed to + the :program:`dialog`-like program. + + *text* is shown as a sort of caption between the list and the + global progress bar. The latter displays *percent* as the + percentage of completion. + + Contrary to the regular :ref:`gauge widget <gauge-widget>`, + :meth:`!mixedgauge` is completely static. You have to call + :meth:`!mixedgauge` several times in order to display different + percentages in the global progress bar or various status + indicators for a given task. + + .. note:: + + Calling :meth:`!mixedgauge` several times is likely to cause + unwanted flickering because of the screen initializations + performed by :program:`dialog` on every run. + + Notable exceptions: + + any exception raised by :meth:`Dialog._perform` + + """ + cmd = ["--mixedgauge", text, str(height), str(width), str(percent)] + for t in elements: + cmd.extend( (t[0], str(t[1])) ) + return self._widget_with_no_output("mixedgauge", cmd, kwargs) + + @widget + @retval_is_code + def msgbox(self, text, height=None, width=None, **kwargs): + """Display a message dialog box, with scrolling and line wrapping. + + :param str text: text to display in the box + :param height: height of the box + :type height: int or ``None`` + :param width: width of the box + :type width: int or ``None`` + :return: a :term:`Dialog exit code` + :rtype: str + + Display *text* in a message box, with a scrollbar and percentage + indication if *text* is too long to fit in a single "screen". + + An :meth:`!msgbox` is very similar to a :meth:`yesno` box. The + only difference between an :meth:`!msgbox` and a :meth:`!yesno` + box is that the former only has a single :guilabel:`OK` button. + You can use :meth:`!msgbox` to display any message you like. + After reading the message, the user can press the :kbd:`Enter` + key so that :program:`dialog` will exit and the calling program + can continue its operation. + + :meth:`!msgbox` performs automatic line wrapping. If you want to + force a newline at some point, simply insert it in *text*. In + other words (with the default settings), newline characters in + *text* **are** respected; the line wrapping process performed by + :program:`dialog` only inserts **additional** newlines when + needed. If you want no automatic line wrapping, consider using + :meth:`scrollbox`. + + Default values for the size parameters when the + :ref:`autowidgetsize <autowidgetsize>` option is disabled: + ``height=10, width=30``. + + Notable exceptions: + + any exception raised by :meth:`Dialog._perform` + + """ + height, width = self._default_size((height, width), (10, 30)) + return self._widget_with_no_output( + "msgbox", + ["--msgbox", text, str(height), str(width)], + kwargs) + + @widget + @retval_is_code + def pause(self, text, height=None, width=None, seconds=5, **kwargs): + """Display a pause dialog box. + + :param str text: text to display in the box + :param height: height of the box + :type height: int or ``None`` + :param width: width of the box + :type width: int or ``None`` + :param int seconds: number of seconds to pause for + :return: + a :term:`Dialog exit code` (which is :attr:`Dialog.OK` if the + widget ended automatically after *seconds* seconds or if the + user pressed the :guilabel:`OK` button) + :rtype: str + + A :meth:`!pause` box displays a text and a meter along the + bottom of the box, during a specified amount of time + (*seconds*). The meter indicates how many seconds remain until + the end of the pause. The widget exits when the specified number + of seconds is elapsed, or immediately if the user presses the + :guilabel:`OK` button, the :guilabel:`Cancel` button or the + :kbd:`Esc` key. + + Default values for the size parameters when the + :ref:`autowidgetsize <autowidgetsize>` option is disabled: + ``height=15, width=60``. + + Notable exceptions: + + any exception raised by :meth:`Dialog._perform` + + """ + height, width = self._default_size((height, width), (15, 60)) + return self._widget_with_no_output( + "pause", + ["--pause", text, str(height), str(width), str(seconds)], + kwargs) + + @widget + def passwordbox(self, text, height=None, width=None, init='', **kwargs): + """Display a password input dialog box. + + :param str text: text to display in the box + :param height: height of the box + :type height: int or ``None`` + :param width: width of the box + :type width: int or ``None`` + :param str init: default input password + :return: a tuple of the form :samp:`({code}, {password})` where: + + - *code* is a :term:`Dialog exit code`; + - *password* is the password entered by the user. + + :rtype: tuple + + A :meth:`!passwordbox` is similar to an :meth:`inputbox`, except + that the text the user enters is not displayed. This is useful + when prompting for passwords or other sensitive information. Be + aware that if anything is passed in *init*, it will be visible + in the system's process table to casual snoopers. Also, it is + very confusing to the user to provide them with a default + password they cannot see. For these reasons, using *init* is + highly discouraged. + + By default (as in :program:`dialog`), nothing is echoed to the + terminal as the user enters the sensitive text. This can be + confusing to users. Use ``insecure=True`` (keyword argument) if + you want an asterisk to be echoed for each character entered by + the user. + + Default values for the size parameters when the + :ref:`autowidgetsize <autowidgetsize>` option is disabled: + ``height=10, width=60``. + + Notable exceptions: + + any exception raised by :meth:`Dialog._perform` + + """ + height, width = self._default_size((height, width), (10, 60)) + # The help output does not depend on whether --help-status was passed + # (dialog 1.2-20130902). + return self._widget_with_string_output( + ["--passwordbox", text, str(height), str(width), init], + kwargs, strip_xdialog_newline=True, raw_help=True) + + def _progressboxoid(self, widget, file_path=None, file_flags=os.O_RDONLY, + fd=None, text=None, height=20, width=78, **kwargs): + if (file_path is None and fd is None) or \ + (file_path is not None and fd is not None): + raise BadPythonDialogUsage( + "{0}.{1}.{2}: either 'file_path' or 'fd' must be provided, and " + "not both at the same time".format( + __name__, self.__class__.__name__, widget)) + + with _OSErrorHandling(): + if file_path is not None: + if fd is not None: + raise PythonDialogBug( + "unexpected non-None value for 'fd': {0!r}".format(fd)) + # No need to pass 'mode', as the file is not going to be + # created here. + fd = os.open(file_path, file_flags) + + try: + args = [ "--{0}".format(widget) ] + if text is not None: + args.append(text) + args.extend([str(height), str(width)]) + + kwargs["redir_child_stdin_from_fd"] = fd + code = self._widget_with_no_output(widget, args, kwargs) + finally: + with _OSErrorHandling(): + if file_path is not None: + # We open()ed file_path ourselves, let's close it now. + os.close(fd) + + return code + + @widget + @retval_is_code + def progressbox(self, file_path=None, file_flags=os.O_RDONLY, + fd=None, text=None, height=None, width=None, **kwargs): + """ + Display a possibly growing stream in a dialog box, as with ``tail -f``. + + A file, or more generally a stream that can be read from, must + be specified with either: + + :param str file_path: path to the file that is going to be displayed + :param file_flags: + flags used when opening *file_path*; those are passed to + :func:`os.open` (not the built-in :func:`open` function!). By + default, only one flag is set: :data:`os.O_RDONLY`. + + or + + :param int fd: file descriptor for the stream to be displayed + + Remaining parameters: + + :param text: caption continuously displayed at the top, above + the stream text, or ``None`` to disable the + caption + :param height: height of the box + :type height: int or ``None`` + :param width: width of the box + :type width: int or ``None`` + :return: a :term:`Dialog exit code` + :rtype: str + + Display the contents of the specified file, updating the dialog + box whenever the file grows, as with the ``tail -f`` command. + + The file can be specified in two ways: + + - either by giving its path (and optionally :func:`os.open` + flags) with parameters *file_path* and *file_flags*; + + - or by passing its file descriptor with parameter *fd* (in + which case it may not even be a file; for instance, it could + be an anonymous pipe created with :func:`os.pipe`). + + Default values for the size parameters when the + :ref:`autowidgetsize <autowidgetsize>` option is disabled: + ``height=20, width=78``. + + Notable exceptions: + + - :exc:`PythonDialogOSError` (:exc:`PythonDialogIOError` if + the Python version is < 3.3) + - any exception raised by :meth:`Dialog._perform` + + """ + height, width = self._default_size((height, width), (20, 78)) + return self._progressboxoid( + "progressbox", file_path=file_path, file_flags=file_flags, + fd=fd, text=text, height=height, width=width, **kwargs) + + @widget + @retval_is_code + def programbox(self, file_path=None, file_flags=os.O_RDONLY, + fd=None, text=None, height=None, width=None, **kwargs): + """ + Display a possibly growing stream in a dialog box, as with ``tail -f``. + + A :meth:`!programbox` is very similar to a :meth:`progressbox`. + The only difference between a :meth:`!programbox` and a + :meth:`!progressbox` is that a :meth:`!programbox` displays an + :guilabel:`OK` button, but only after the input stream has been + exhausted (i.e., *End Of File* has been reached). + + This dialog box can be used to display the piped output of an + external program. After the program completes, the user can + press the :kbd:`Enter` key to close the dialog and resume + execution of the calling program. + + The parameters and exceptions are the same as for + :meth:`progressbox`. Please refer to the corresponding + documentation. + + Default values for the size parameters when the + :ref:`autowidgetsize <autowidgetsize>` option is disabled: + ``height=20, width=78``. + + This widget requires :program:`dialog` >= 1.1-20110302. + + .. versionadded:: 2.14 + + """ + self._dialog_version_check("1.1-20110302", "the programbox widget") + + height, width = self._default_size((height, width), (20, 78)) + return self._progressboxoid( + "programbox", file_path=file_path, file_flags=file_flags, + fd=fd, text=text, height=height, width=width, **kwargs) + + @widget + def radiolist(self, text, height=None, width=None, list_height=None, + choices=[], **kwargs): + """Display a radiolist box. + + :param str text: text to display in the box + :param height: height of the box + :type height: int or ``None`` + :param width: width of the box + :type width: int or ``None`` + :param list_height: number of entries displayed in the box + (which can be scrolled) at a given time + :type list_height: int or ``None`` + :param choices: + an iterable of :samp:`({tag}, {item}, {status})` tuples + where *status* specifies the initial selected/unselected + state of each entry; can be ``True`` or ``False``, ``1`` or + ``0``, ``"on"`` or ``"off"`` (``True``, ``1`` and ``"on"`` + meaning selected), or any case variation of these two + strings. No more than one entry should be set to ``True``. + :return: a tuple of the form :samp:`({code}, {tag})` where: + + - *code* is a :term:`Dialog exit code`; + - *tag* is the tag string corresponding to the entry that was + chosen by the user. + + :rtype: tuple + + A :meth:`!radiolist` box is similar to a :meth:`menu` box. The + main differences are presentation and that the + :meth:`!radiolist` allows you to indicate which entry is + initially selected, by setting its status to ``True``. + + If the user exits with :kbd:`Esc` or :guilabel:`Cancel`, or if + all entries were initially set to ``False`` and not altered + before the user chose :guilabel:`OK`, the returned tag is the + empty string. + + Default values for the size parameters when the + :ref:`autowidgetsize <autowidgetsize>` option is disabled: + ``height=15, width=54, list_height=7``. + + Notable exceptions: + + any exception raised by :meth:`Dialog._perform` or :func:`_to_onoff` + + """ + height, width, list_height = self._default_size( + (height, width, list_height), (15, 54, 7)) + + cmd = ["--radiolist", text, str(height), str(width), str(list_height)] + for t in choices: + cmd.extend([ t[0], t[1], _to_onoff(t[2]) ] + list(t[3:])) + (code, output) = self._perform(cmd, **kwargs) + + output = self._strip_xdialog_newline(output) + + if code == self.HELP: + help_data = self._parse_help(output, kwargs) + if self._help_status_on(kwargs): + help_id, selected_tag = help_data + # Reconstruct 'choices' with the selected item inferred from + # 'selected_tag'. + choices = [ [ tag, item, tag == selected_tag ] + rest for + (tag, item, status, *rest) in choices ] + return (code, (help_id, selected_tag, choices)) + else: + return (code, help_data) + else: + return (code, output) + + @widget + def rangebox(self, text, height=0, width=0, min=None, max=None, init=None, + **kwargs): + """Display a range dialog box. + + :param str text: text to display above the actual range control + :param int height: height of the box + :param int width: width of the box + :param int min: minimum value for the range control + :param int max: maximum value for the range control + :param int init: initial value for the range control + :return: a tuple of the form :samp:`({code}, {val})` where: + + - *code* is a :term:`Dialog exit code`; + - *val* is an integer: the value chosen by the user. + + :rtype: tuple + + The :meth:`!rangebox` dialog allows the user to select from a + range of integers using a kind of slider. The range control + shows the current value as a bar (like the :ref:`gauge dialog + <gauge-widget>`). + + The :kbd:`Tab` and arrow keys move the cursor between the + buttons and the range control. When the cursor is on the latter, + you can change the value with the following keys: + + +-----------------------+----------------------------+ + | Key | Action | + +=======================+============================+ + | :kbd:`Left` and | select a digit to modify | + | :kbd:`Right` arrows | | + +-----------------------+----------------------------+ + | :kbd:`+` / :kbd:`-` | increment/decrement the | + | | selected digit by one unit | + +-----------------------+----------------------------+ + | :kbd:`0`–:kbd:`9` | set the selected digit to | + | | the given value | + +-----------------------+----------------------------+ + + Some keys are also recognized in all cursor positions: + + +------------------+--------------------------------------+ + | Key | Action | + +==================+======================================+ + | :kbd:`Home` / | set the value to its minimum or | + | :kbd:`End` | maximum | + +------------------+--------------------------------------+ + | :kbd:`Page Up` / | decrement/increment the value so | + | :kbd:`Page Down` | that the slider moves by one column | + +------------------+--------------------------------------+ + + This widget requires :program:`dialog` >= 1.2-20121230. + + Notable exceptions: + + any exception raised by :meth:`Dialog._perform` + + .. versionadded:: 2.14 + + """ + self._dialog_version_check("1.2-20121230", "the rangebox widget") + + for name in ("min", "max", "init"): + if not isinstance(locals()[name], int): + raise BadPythonDialogUsage( + "{0!r} argument not an int: {1!r}".format(name, + locals()[name])) + (code, output) = self._perform( + ["--rangebox", text] + [ str(i) for i in + (height, width, min, max, init) ], + **kwargs) + + if code == self.HELP: + help_data = self._parse_help(output, kwargs, raw_format=True) + # The help output does not depend on whether --help-status was + # passed (dialog 1.2-20130902). + return (code, int(help_data)) + elif code in (self.OK, self.EXTRA): + return (code, int(output)) + else: + return (code, None) + + @widget + @retval_is_code + def scrollbox(self, text, height=None, width=None, **kwargs): + """Display a string in a scrollable box, with no line wrapping. + + :param str text: string to display in the box + :param height: height of the box + :type height: int or ``None`` + :param width: width of the box + :type width: int or ``None`` + :return: a :term:`Dialog exit code` + :rtype: str + + This method is a layer on top of :meth:`textbox`. The + :meth:`!textbox` widget in :program:`dialog` allows one to + display file contents only. This method can be used to display + any text in a scrollable box. This is simply done by creating a + temporary file, calling :meth:`!textbox` and deleting the + temporary file afterwards. + + The text is not automatically wrapped. New lines in the + scrollable box will be placed exactly as in *text*. If you want + automatic line wrapping, you should use the :meth:`msgbox` + widget instead (the :mod:`textwrap` module from the Python + standard library is also worth knowing about). + + Default values for the size parameters when the + :ref:`autowidgetsize <autowidgetsize>` option is disabled: + ``height=20, width=78``. + + Notable exceptions: + + :exc:`PythonDialogOSError` (:exc:`PythonDialogIOError` if the + Python version is < 3.3) + + .. versionchanged:: 3.1 + :exc:`UnableToCreateTemporaryDirectory` exception can't be + raised anymore. The equivalent condition now raises + :exc:`PythonDialogOSError`. + + """ + height, width = self._default_size((height, width), (20, 78)) + + with _OSErrorHandling(): + tmpfile = tempfile.NamedTemporaryFile( + mode="w", prefix="pythondialog.tmp", delete=False) + try: + with tmpfile as f: + f.write(text) + # The temporary file is now closed. According to the tempfile + # module documentation, this is necessary if we want to be able + # to reopen it reliably regardless of the platform. + + # Ask for an empty title unless otherwise specified + if kwargs.get("title", None) is None: + kwargs["title"] = "" + + return self._widget_with_no_output( + "textbox", + ["--textbox", tmpfile.name, str(height), str(width)], + kwargs) + finally: + # The test should always succeed, but I prefer being on the + # safe side. + if os.path.exists(tmpfile.name): + os.unlink(tmpfile.name) + + @widget + @retval_is_code + def tailbox(self, filepath, height=None, width=None, **kwargs): + """Display the contents of a file in a dialog box, as with ``tail -f``. + + :param str filepath: path to a file, the contents of which is to + be displayed in the box + :param height: height of the box + :type height: int or ``None`` + :param width: width of the box + :type width: int or ``None`` + :return: a :term:`Dialog exit code` + :rtype: str + + Display the contents of the file specified with *filepath*, + updating the dialog box whenever the file grows, as with the + ``tail -f`` command. + + Default values for the size parameters when the + :ref:`autowidgetsize <autowidgetsize>` option is disabled: + ``height=20, width=60``. + + Notable exceptions: + + any exception raised by :meth:`Dialog._perform` + + """ + height, width = self._default_size((height, width), (20, 60)) + return self._widget_with_no_output( + "tailbox", + ["--tailbox", filepath, str(height), str(width)], + kwargs) + # No tailboxbg widget, at least for now. + + @widget + @retval_is_code + def textbox(self, filepath, height=None, width=None, **kwargs): + """Display the contents of a file in a dialog box. + + :param str filepath: path to a file, the contents of which is to + be displayed in the box + :param height: height of the box + :type height: int or ``None`` + :param width: width of the box + :type width: int or ``None`` + :return: a :term:`Dialog exit code` + :rtype: str + + A :meth:`!textbox` lets you display the contents of a text file + in a dialog box. It is like a simple text file viewer. The user + can move through the file using the :kbd:`Up` and :kbd:`Down` + arrow keys, :kbd:`Page Up` and :kbd:`Page Down` as well as the + :kbd:`Home` and :kbd:`End` keys available on most keyboards. If + the lines are too long to be displayed in the box, the + :kbd:`Left` and :kbd:`Right` arrow keys can be used to scroll + the text region horizontally. For more convenience, forward and + backward search functions are also provided. + + Default values for the size parameters when the + :ref:`autowidgetsize <autowidgetsize>` option is disabled: + ``height=20, width=60``. + + Notable exceptions: + + any exception raised by :meth:`Dialog._perform` + + """ + height, width = self._default_size((height, width), (20, 60)) + # This is for backward compatibility... not that it is + # stupid, but I prefer explicit programming. + if kwargs.get("title", None) is None: + kwargs["title"] = filepath + + return self._widget_with_no_output( + "textbox", + ["--textbox", filepath, str(height), str(width)], + kwargs) + + def _timebox_parse_time(self, time_str): + try: + mo = _timebox_time_cre.match(time_str) + except re.error as e: + raise PythonDialogReModuleError(str(e)) from e + + if not mo: + raise UnexpectedDialogOutput( + "the dialog-like program returned the following " + "unexpected output (a time string was expected) with the " + "--timebox option: {0!r}".format(time_str)) + + return [ int(s) for s in mo.group("hour", "minute", "second") ] + + @widget + def timebox(self, text, height=None, width=None, hour=-1, minute=-1, + second=-1, **kwargs): + """Display a time dialog box. + + :param str text: text to display in the box + :param height: height of the box + :type height: int or ``None`` + :param int width: width of the box + :type width: int or ``None`` + :param int hour: inititial hour selected + :param int minute: inititial minute selected + :param int second: inititial second selected + :return: a tuple of the form :samp:`({code}, {time})` where: + + - *code* is a :term:`Dialog exit code`; + - *time* is a list of the form :samp:`[{hour}, {minute}, + {second}]`, where *hour*, *minute* and *second* are integers + corresponding to the time chosen by the user. + + :rtype: tuple + + :meth:`timebox` is a dialog box which allows one to select an + hour, minute and second. If any of the values for *hour*, + *minute* and *second* is negative, the current time's + corresponding value is used. You can increment or decrement any + of those using the :kbd:`Left`, :kbd:`Up`, :kbd:`Right` and + :kbd:`Down` arrows. Use :kbd:`Tab` or :kbd:`Backtab` to move + between windows. + + Default values for the size parameters when the + :ref:`autowidgetsize <autowidgetsize>` option is disabled: + ``height=3, width=30``. + + Notable exceptions: + + - any exception raised by :meth:`Dialog._perform` + - :exc:`PythonDialogReModuleError` + - :exc:`UnexpectedDialogOutput` + + """ + height, width = self._default_size((height, width), (3, 30)) + (code, output) = self._perform( + ["--timebox", text, str(height), str(width), + str(hour), str(minute), str(second)], + **kwargs) + + if code == self.HELP: + help_data = self._parse_help(output, kwargs, raw_format=True) + # The help output does not depend on whether --help-status was + # passed (dialog 1.2-20130902). + return (code, self._timebox_parse_time(help_data)) + elif code in (self.OK, self.EXTRA): + return (code, self._timebox_parse_time(output)) + else: + return (code, None) + + @widget + def treeview(self, text, height=0, width=0, list_height=0, + nodes=[], **kwargs): + """Display a treeview box. + + :param str text: text to display at the top of the box + :param int height: height of the box + :param int width: width of the box + :param int list_height: + number of lines reserved for the main part of the box, + where the tree is displayed + :param nodes: + an iterable of :samp:`({tag}, {item}, {status}, {depth})` tuples + describing nodes, where: + + - *tag* is used to indicate which node was selected by + the user on exit; + - *item* is the text displayed for the node; + - *status* specifies the initial selected/unselected + state of each entry; can be ``True`` or ``False``, + ``1`` or ``0``, ``"on"`` or ``"off"`` (``True``, ``1`` + and ``"on"`` meaning selected), or any case variation + of these two strings; + - *depth* is a non-negative integer indicating the depth + of the node in the tree (``0`` for the root node). + + :return: a tuple of the form :samp:`({code}, {tag})` where: + + - *code* is a :term:`Dialog exit code`; + - *tag* is the tag of the selected node. + + Display nodes organized in a tree structure. Each node has a + *tag*, an *item* text, a selected *status*, and a *depth* in + the tree. Only the *item* texts are displayed in the widget; + *tag*\s are only used for the return value. Only one node can + be selected at a given time, as for the :meth:`radiolist` + widget. + + This widget requires :program:`dialog` >= 1.2-20121230. + + Notable exceptions: + + any exception raised by :meth:`Dialog._perform` or :func:`_to_onoff` + + .. versionadded:: 2.14 + + """ + self._dialog_version_check("1.2-20121230", "the treeview widget") + cmd = ["--treeview", text, str(height), str(width), str(list_height)] + + nselected = 0 + for i, t in enumerate(nodes): + if not isinstance(t[3], int): + raise BadPythonDialogUsage( + "fourth element of node {0} not an int: {1!r}".format( + i, t[3])) + + status = _to_onoff(t[2]) + if status == "on": + nselected += 1 + + cmd.extend([ t[0], t[1], status, str(t[3]) ] + list(t[4:])) + + if nselected != 1: + raise BadPythonDialogUsage( + "exactly one node must be selected, not {0}".format(nselected)) + + (code, output) = self._perform(cmd, **kwargs) + + if code == self.HELP: + help_data = self._parse_help(output, kwargs) + if self._help_status_on(kwargs): + help_id, selected_tag = help_data + # Reconstruct 'nodes' with the selected item inferred from + # 'selected_tag'. + nodes = [ [ tag, item, tag == selected_tag ] + rest for + (tag, item, status, *rest) in nodes ] + return (code, (help_id, selected_tag, nodes)) + else: + return (code, help_data) + elif code in (self.OK, self.EXTRA): + return (code, output) + else: + return (code, None) + + @widget + @retval_is_code + def yesno(self, text, height=None, width=None, **kwargs): + """Display a yes/no dialog box. + + :param str text: text to display in the box + :param height: height of the box + :type height: int or ``None`` + :param width: width of the box + :type width: int or ``None`` + :return: a :term:`Dialog exit code` + :rtype: str + + Display a dialog box containing *text* and two buttons labelled + :guilabel:`Yes` and :guilabel:`No` by default. + + The box size is *height* rows by *width* columns. If *text* is + too long to fit in one line, it will be automatically divided + into multiple lines at appropriate places. *text* may also + contain the substring ``"\\n"`` or newline characters to control + line breaking explicitly. + + This :meth:`!yesno` dialog box is useful for asking questions + that require the user to answer either "yes" or "no". These are + the default button labels, however they can be freely set with + the ``yes_label`` and ``no_label`` keyword arguments. The user + can switch between the buttons by pressing the :kbd:`Tab` key. + + Default values for the size parameters when the + :ref:`autowidgetsize <autowidgetsize>` option is disabled: + ``height=10, width=30``. + + Notable exceptions: + + any exception raised by :meth:`Dialog._perform` + + """ + height, width = self._default_size((height, width), (10, 30)) + return self._widget_with_no_output( + "yesno", + ["--yesno", text, str(height), str(width)], + kwargs) diff --git a/pythondialog/doc/DialogBackendVersion.rst b/pythondialog/doc/DialogBackendVersion.rst @@ -0,0 +1,10 @@ +.. currentmodule:: dialog + +The :class:`!DialogBackendVersion` class +======================================== + +.. autoclass:: DialogBackendVersion + :show-inheritance: + :members: + :undoc-members: + diff --git a/pythondialog/doc/Dialog_class_overview.rst b/pythondialog/doc/Dialog_class_overview.rst @@ -0,0 +1,522 @@ +.. currentmodule:: dialog + +:class:`Dialog` class overview +============================== + +Initializing a :class:`Dialog` instance +--------------------------------------- + +Since all widgets in pythondialog are implemented as methods of the +:class:`Dialog` class, a pythondialog-based application usually starts by +creating a :class:`!Dialog` instance. + +.. autoclass:: Dialog + :members: __init__ + +.. _autowidgetsize: + +.. rubric:: About the *autowidgetsize* option + +The *autowidgetsize* option should be convenient in situations where figuring +out suitable widget size parameters is a burden, for instance when developing +little scripts that don't need too much visual polishing, when a widget is +used to display data, the size of which is not easily predictable, or simply +when one doesn't want to hardcode the widget size. + +This option is implemented in the following way: for a given size parameter +(for instance, *width*) of a given widget, the default value in the +widget-producing method is now ``None`` if it previously had a non-zero +default. At runtime, if the value seen by the widget-producing method is not +``None``, it is used as is; on the contrary, if that value is ``None``, it is +automatically replaced with: + + - ``0`` if the :class:`Dialog` instance has been initialized with + *autowidgetsize* set to ``True``; + - the old default otherwise, in order to preserve backward-comptability. + +.. note:: + + - the *autowidgetsize* option is currently marked as experimental, please + give some feedback; + - you may encounter questionable results if you only set one of the *width* + and *height* parameters to ``0`` for a given widget (seen in + :program:`dialog` 1.2-20140219). + +.. warning:: + + You should not explicitly pass ``None`` for a size parameter such as *width* + or *height*. If you want a fixed size, specify it directly (as an int); + otherwise, either use the *autowidgetsize* option or set the parameter to + ``0`` (e.g., ``width=0``). + + +.. _passing-dialog-common-options: + +Passing :program:`dialog` "common options" +------------------------------------------ + +Every widget method has a \*\*kwargs argument allowing you to pass +:term:`common options <dialog common options>` (see the :manpage:`dialog(1)` +manual page) to :program:`dialog` for this widget call. For instance, if *d* +is a :class:`Dialog` instance, you can write:: + + d.checklist(args, ..., title="A Great Title", no_shadow=True) + +The *no_shadow* option is worth looking at: + + #. It is an option that takes no argument as far as :program:`dialog` is + concerned (unlike the :option:`--title` option, for instance). When you + list it as a keyword argument, the option is really passed to + :program:`dialog` only if the value you gave it evaluates to ``True`` in + a boolean context. For instance, ``no_shadow=True`` will cause + :option:`--no-shadow` to be passed to :program:`dialog` whereas + ``no_shadow=False`` will cause this option not to be passed to + :program:`dialog` at all. + + #. It is an option that has a hyphen (``-``) in its name, which you must + change into an underscore (``_``) to pass it as a Python keyword + argument. Therefore, :option:`--no-shadow` is passed by giving a + ``no_shadow=True`` keyword argument to :class:`Dialog` methods (the + leading two dashes are also consistently removed). + +.. note:: + + When :meth:`Dialog.__init__` is called with + :samp:`{pass_args_via_file}=True` (or without any explicit setting for this + option, and the pythondialog as well as :program:`dialog` versions are + recent enough so that the option is enabled by default), then the options + are not directly passed to :program:`dialog`. Instead, all options are + written to a temporary file which :program:`dialog` is pointed to via + :option:`--file`. This ensures better confidentiality with respect to other + users of the same computer. + + +.. versionadded:: 2.14 + Support for the *default_button* and *no_tags* common options. + +.. versionadded:: 3.0 + Proper support for the *extra_button*, *item_help* and *help_status* common + options. + + +Return value of widget-producing methods +---------------------------------------- + +Most :class:`Dialog` methods that create a widget (actually: all methods that +supervise the exit of a widget) return a value which fits into one of these +categories: + + #. The return value is a :term:`Dialog exit code` (see below). + + #. The return value is a sequence whose first element is a Dialog exit code + (the rest of the sequence being related to what the user entered in the + widget). + +For instance, :meth:`Dialog.yesno` returns a single Dialog exit code that will +typically be :attr:`Dialog.OK` or :attr:`Dialog.CANCEL`, depending on the +button chosen by the user. However, :meth:`Dialog.checklist` returns a tuple +of the form :samp:`({code}, [{tag}, ...])` whose first element is a Dialog +exit code and second element lists all tags for the entries selected by the +user. + +.. _Dialog-exit-code: + +"Dialog exit code" (high-level) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +A :dfn:`Dialog exit code`, or :dfn:`high-level exit code`, is a string +indicating how/why a widget-producing method ended. Most widgets return one of +the :term:`standard Dialog exit codes <standard Dialog exit code>`: ``"ok"``, +``"cancel"``, ``"esc"``, ``"help"`` and ``"extra"``, respectively available as +:attr:`Dialog.OK`, :attr:`Dialog.CANCEL`, :attr:`Dialog.ESC`, +:attr:`Dialog.HELP` and :attr:`Dialog.EXTRA`, *i.e.,* attributes of the +:class:`Dialog` class. However, some widgets may return additional, +non-standard exit codes; for instance, the :meth:`~Dialog.inputmenu` widget +may return ``"accepted"`` or ``"renamed"`` in addition to the standard Dialog +exit codes. + +When getting a Dialog exit code from a widget-producing method, user code +should compare it with :attr:`Dialog.OK` and friends (or equivalently, with +``"ok"`` and friends) using the ``==`` operator. This allows to easily replace +:attr:`Dialog.OK` and friends with objects that compare the same with ``"ok"`` +and ``u"ok"`` in Python 2, for instance. + +The following attributes of the :class:`Dialog` class hold the :term:`standard +Dialog exit codes <standard Dialog exit code>`: + +.. autoattribute:: Dialog.OK + +.. autoattribute:: Dialog.CANCEL + +.. autoattribute:: Dialog.ESC + +.. autoattribute:: Dialog.EXTRA + +.. autoattribute:: Dialog.HELP + +The following attributes are obsolete and should not be used in pythondialog +3.0 and later: + +.. autoattribute:: Dialog.DIALOG_OK + +.. autoattribute:: Dialog.DIALOG_CANCEL + +.. autoattribute:: Dialog.DIALOG_ESC + +.. autoattribute:: Dialog.DIALOG_EXTRA + +.. autoattribute:: Dialog.DIALOG_HELP + +.. autoattribute:: Dialog.DIALOG_ITEM_HELP + +.. _dialog-exit-status: + +"dialog exit status" (low-level) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When returning from a widget call, the :term:`Dialog exit code` is normally +derived by pythondialog from an integer called :dfn:`dialog exit status`, or +:dfn:`low-level exit code`. This integer is returned by the :program:`dialog` +backend upon exit. The different possible values for the dialog exit status +are referred to as ``DIALOG_OK``, ``DIALOG_CANCEL``, ``DIALOG_ESC``, +``DIALOG_ERROR``, ``DIALOG_EXTRA``, ``DIALOG_HELP`` and ``DIALOG_ITEM_HELP`` +in the :manpage:`dialog(1)` manual page. + +.. note:: + + - ``DIALOG_HELP`` and ``DIALOG_ITEM_HELP`` both map to :attr:`Dialog.HELP` + in pythondialog, because they both correspond to the same user action and + the difference brings no information that the caller does not already + have; + + - ``DIALOG_ERROR`` has no counterpart as a :class:`Dialog` attribute, + because it is automatically translated into a :exc:`DialogError` exception + when received. + +In pythondialog 2.x, the low-level exit codes were available as the +``DIALOG_OK``, ``DIALOG_CANCEL``, etc. attributes of :class:`Dialog` +instances. For compatibility, the :class:`Dialog` class has attributes of the +same names that are mapped to :attr:`Dialog.OK`, :attr:`Dialog.CANCEL`, etc., +but their use is deprecated as of pythondialog 3.0. + + +Adding an Extra button +---------------------- + +With most widgets, it is possible to add a supplementary button called +:dfn:`Extra button`. To do that, you simply have to use ``extra_button=True`` +(keyword argument) in the widget call. By default, the button text is "Extra", +but you can specify another string with the *extra_label* keyword argument. + +When the widget exits, you know if the :guilabel:`Extra` button was pressed if +the :term:`Dialog exit code` is :attr:`Dialog.EXTRA` (``"extra"``). Normally, +the rest of the return value is the same as if the widget had been closed with +:guilabel:`OK`. Therefore, if the widget normally returns a list of three +integers, for instance, you can expect to get the same information if +:guilabel:`Extra` is pressed instead of :guilabel:`OK`. + +.. note:: + + This feature can be particularly useful in combination with the *yes_label*, + *no_label*, *help_button* and *help_label* :term:`common options <dialog + common options>` to provide a completely different set of buttons than the + default for a given widget. + + +Providing on-line help facilities +--------------------------------- + +With most :program:`dialog` widgets, it is possible to provide online help to +the final user. At the time of this writing (October 2014), there are three +main options governing these help facilities in the :program:`dialog` backend: +:option:`--help-button`, :option:`--item-help` and :option:`--help-status`. +Since :program:`dialog` 1.2-20130902, there is also :option:`--help-tags` that +modifies the way :option:`--item-help` works. As explained previously +(:ref:`passing-dialog-common-options`), in order to use these options in +pythondialog, you can pass the *help_button*, *item_help*, *help_status* and +*help_tags* keyword arguments to :class:`Dialog` widget-producing methods. + +Adding a :guilabel:`Help` button +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +In order to provide a :guilabel:`Help` button in addition to the normal +buttons of a widget, you can pass ``help_button=True`` (keyword argument) to +the corresponding :class:`Dialog` method. For instance, if *d* is a +:class:`Dialog` instance, you can write:: + + code = d.yesno("<text>", height=10, width=40, help_button=True) + +or:: + + code, answer = d.inputbox("<text>", init="<init>", + help_button=True) + +When the method returns, the :term:`Dialog exit code` is :attr:`Dialog.HELP` +(i.e., the string ``"help"``) if the user pressed the :guilabel:`Help` button. +Apart from that, it works exactly as if ``help_button=True`` had not been +used. In the last example, if the user presses the :guilabel:`Help` button, +*answer* will contain the user input, just as if :guilabel:`OK` had been +pressed. Similarly, if you write:: + + code, t = d.checklist( + "<text>", height=0, width=0, list_height=0, + choices=[ ("Tag 1", "Item 1", False), + ("Tag 2", "Item 2", True), + ("Tag 3", "Item 3", True) ], + help_button=True) + +and find that ``code == Dialog.HELP``, then *t* contains the tag string for +the highlighted item when the :guilabel:`Help` button was pressed. + +Finally, note that it is possible to choose the text written on the +:guilabel:`Help` button by supplying a string as the *help_label* keyword +argument. + +.. _providing-inline-per-item-help: + +Providing inline per-item help +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +In addition to, or instead of the :guilabel:`Help` button, you can provide +:dfn:`item-specific help` that is normally displayed at the bottom of the +widget. This can be done by passing the ``item_help=True`` keyword argument to +the widget-producing method and by including the item-specific help strings in +the appropriate argument. + +For widgets where item-specific help makes sense (i.e., there are several +elements that can be highlighted), there is usually a parameter, often called +*elements*, *choices*, *nodes*..., that must be provided as an iterable +describing the various lines/items/nodes/... that can be highlighted in the +widget. When ``item_help=True`` is passed, every element of this iterable must +be completed with a string which is the :dfn:`item-help string` of the element +(using :manpage:`dialog(1)` terminology). For instance, the following call +with no inline per-item help support:: + + code, t = d.checklist( + "<text>", height=0, width=0, list_height=0, + choices=[ ("Tag 1", "Item 1", False), + ("Tag 2", "Item 2", True), + ("Tag 3", "Item 3", True) ], + help_button=True) + +can be altered this way to provide inline item-specific help:: + + code, t = d.checklist( + "<text>", height=0, width=0, list_height=0, + choices=[ ("Tag 1", "Item 1", False, "Help 1"), + ("Tag 2", "Item 2", True, "Help 2"), + ("Tag 3", "Item 3", True, "Help 3") ], + help_button=True, item_help=True, help_tags=True) + +With this modification, the item-help string for the highlighted item is +displayed in the bottom line of the screen and updated as the user highlights +other items. + +If you don't want a :guilabel:`Help` button, just use ``item_help=True`` +without ``help_button=True`` (*help_tags* doesn't matter in this case). Then, +you have the inline help at the bottom of the screen, and the following +discussion about the return value can be ignored. + +If the user chooses the :guilabel:`Help` button, *code* will be equal to +:attr:`Dialog.HELP` (``"help"``) and *t* will contain the tag string +corresponding to the highlighted item when the :guilabel:`Help` button was +pressed (``"Tag 1/2/3"`` in the example). This is because of the *help_tags* +option; without it (or with ``help_tags=False``), *t* would have contained the +:term:`item-help string` of the highlighted choice (``"Help 1/2/3"`` in the +example). + +If you remember what was said earlier, if ``item_help=True`` had not been used +in the previous example, *t* would still contain the tag of the highlighted +choice if the user closed the widget with the :guilabel:`Help` button. This is +the same as when using ``item_help=True`` in combination with +``help_tags=True``; however, you would get the :term:`item-help string` +instead if *help_tags* were ``False`` (which is the default, as in the +:program:`dialog` backend, and in order to preserve compatibility with the +:meth:`Dialog.menu` implementation that is several years old). + +Therefore, I recommend for consistency to use ``help_tags=True`` whenever +possible when specifying ``item_help=True``. This makes ``"--help-tags"`` a +good candidate for use with :meth:`Dialog.add_persistent_args` to avoid +repeating it over and over. However, there are two cases where +``help_tags=True`` cannot be used: + + - when the version of the :program:`dialog` backend is lower than + 1.2-20130902 (the :option:`--help-tags` option was added in this version); + - when using empty or otherwise identical tags for presentation purposes + (unless you don't need to tell which element was highlighted when the + :guilabel:`Help` button was pressed, in which case it doesn't matter to be + unable to discriminate between the tags). + +Getting the widget status before the :guilabel:`Help` button was pressed +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Typically, when the user chooses :guilabel:`Help` in a widget, the application +will display a dialog box such as :meth:`~Dialog.textbox`, +:meth:`~Dialog.msgbox` or :meth:`~Dialog.scrollbox` and redisplay the original +widget afterwards. For simple widgets such as :meth:`~Dialog.inputbox`, when +the :term:`Dialog exit code` is equal to :attr:`Dialog.HELP`, the return value +contains enough information to redisplay the widget in the same state it had +when :guilabel:`Help` was chosen. However, for more complex widgets such as +:meth:`~Dialog.radiolist` (resp. :meth:`~Dialog.checklist`, or +:meth:`~Dialog.form` and its derivatives), knowing the highlighted item is not +enough to restore the widget state after processing the help request: one +needs to know the checked item (resp. list of checked items, or form contents). + +This is where the *help_status* keyword argument becomes useful. Example:: + + code, t = d.checklist( + "<text>", height=0, width=0, list_height=0, + choices=[ ("Tag 1", "Item 1", False), + ("Tag 2", "Item 2", True), + ("Tag 3", "Item 3", True) ], + help_button=True, help_status=True) + +When :guilabel:`Help` is chosen, ``code == Dialog.HELP`` and *t* is a tuple of +the form :samp:`({tag}, {selected_tags}, {choices})` where: + + - *tag* gives the tag string of the highlighted item (which would be the + value of *t* if *help_status* were set to ``False``); + - *selected_tags* is the... list of selected tags (note that highlighting + and selecting an item are different things!); + - *choices* is a list built from the original *choices* argument of the + :meth:`~Dialog.checklist` call and from the list of selected tags, that + can be used as is to create a widget with the same items and selection + state as the original widget had when :guilabel:`Help` was chosen. + +Normally, pythondialog should always provide something similar to the last +item in the previous example in order to make it as easy as possible to +redisplay the widget in the appropriate state. To know precisely what is +returned with ``help_status=True``, the best way is probably to experiment +and/or read the code (by the way, there are many examples of widgets with +various combinations of the *help_button*, *item_help* and *help_status* +keyword arguments in the demo). + +.. note:: + + The various options related to help support are not mutually exclusive; they + may be used together to provide good help support. + +It is also worth noting that the documentation of the various widget-producing +methods is written, in most cases, under the assumption that the widget was +closed "normally" (typically, with the :guilabel:`OK` or :guilabel:`Extra` +button). For instance, a widget documentation may state that the method +returns a tuple of the form :samp:`({code}, {tag})` where *tag* is ..., but +actually, if using ``item_help=True`` with ``help_tags=False``, the *tag* may +very well be an :term:`item-help string`, and if using ``help_status=True``, +it is likely to be a structured object such as a tuple or list. Of course, +handling all these possible variations for all widgets would be a tedious task +and would probably significantly degrade the readability of said +documentation. + +.. versionadded:: 3.0 + Proper support for the *item_help* and *help_status* common options. + + +Screen-related methods +---------------------- + +Getting the terminal size: + +.. automethod:: Dialog.maxsize + +.. automethod:: Dialog.set_background_title + +Obsolete methods +^^^^^^^^^^^^^^^^ + +.. automethod:: Dialog.setBackgroundTitle + +.. automethod:: Dialog.clear + + +Checking the versions of pythondialog and its backend +----------------------------------------------------- + +Version of pythondialog +^^^^^^^^^^^^^^^^^^^^^^^ + +.. autoclass:: VersionInfo + :members: + :special-members: + +.. autodata:: version_info + :annotation: + +.. autodata:: __version__ + :annotation: + + +Version of the backend +^^^^^^^^^^^^^^^^^^^^^^ + +The :class:`Dialog` constructor retrieves the version string of the +:program:`dialog` backend and stores it as an instance of a +:class:`BackendVersion` subclass into the +:attr:`Dialog.cached_backend_version` attribute. This allows doing things such +as (*d* being a :class:`Dialog` instance):: + + if d.compat == "dialog" and \ + d.cached_backend_version >= DialogBackendVersion("1.2-20130902"): + ... + +in a reliable way, allowing to fix the parsing and comparison algorithms right +in the appropriate :class:`BackendVersion` subclass, should the +:program:`dialog`-like backend versioning scheme change in unforeseen ways. + +As :program:`Xdialog` seems to be dead and not to support +:option:`--print-version`, the :attr:`Dialog.cached_backend_version` attribute +is set to ``None`` in :program:`Xdialog`-compatibility mode (2013-09-12). +Should this ever change, one should define an :class:`XDialogBackendVersion` +class to handle the particularities of the :program:`Xdialog` versioning +scheme. + +.. attribute:: Dialog.cached_backend_version + + Instance of a :class:`BackendVersion` subclass; it is initialized by the + :class:`Dialog` constructor and used to store the backend version, avoiding + the need to repeatedly call ``dialog --print-version`` or a similar + command, depending on the backend. + + When using the :program:`dialog` backend, + :attr:`Dialog.cached_backend_version` is a :class:`DialogBackendVersion` + instance. + +.. automethod:: Dialog.backend_version + + +Enabling debug facilities +------------------------- + +.. automethod:: Dialog.setup_debug + + +Miscellaneous methods +--------------------- + +.. automethod:: Dialog.add_persistent_args + +.. note:: + + When :meth:`Dialog.__init__` is called with + :samp:`{pass_args_via_file}=True` (or without any explicit setting for this + option, and the pythondialog as well as :program:`dialog` versions are + recent enough so that the option is enabled by default), then the arguments + are not directly passed to :program:`dialog`. Instead, all arguments are + written to a temporary file which :program:`dialog` is pointed to via + :option:`--file`. This ensures better confidentiality with respect to other + users of the same computer. + +.. automethod:: Dialog.dash_escape + +.. automethod:: Dialog.dash_escape_nf + +.. _examples-of-dash-escaping: + +A contrived example using these methods could be the following, which sets a +weird background title starting with two dashes (``--My little program``) for +the life duration of a :class:`Dialog` instance *d*:: + + d.add_persistent_args(d.dash_escape_nf( + ["--backtitle", "--My little program"])) + +or, equivalently:: + + d.add_persistent_args(["--backtitle"] + + d.dash_escape(["--My little program"])) diff --git a/pythondialog/doc/Makefile b/pythondialog/doc/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make <target>' where <target> is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pythondialog.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pythondialog.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/pythondialog" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pythondialog" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/pythondialog/doc/_static/README.txt b/pythondialog/doc/_static/README.txt @@ -0,0 +1,2 @@ +This file ensures the containing directory will be part of Git checkouts and +release tarballs. diff --git a/pythondialog/doc/_templates/README.txt b/pythondialog/doc/_templates/README.txt @@ -0,0 +1,2 @@ +This file ensures the containing directory will be part of Git checkouts and +release tarballs. diff --git a/pythondialog/doc/conf.py b/pythondialog/doc/conf.py @@ -0,0 +1,278 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# pythondialog documentation build configuration file, created by +# sphinx-quickstart on Fri Sep 19 13:32:09 2014. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os +import sphinx + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath('..')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +if sphinx.version_info >= (1, 4, 0): + # Don't warn when something such as :option:`--some-option` refers to an + # option not documented here. + suppress_warnings = ['ref.option'] + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'sphinx.ext.viewcode', +] + +intersphinx_mapping = {'python': ('http://docs.python.org/3', None)} + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'pythondialog' +# This applies to the documentation, according to the Sphinx output. +copyright = '2002-2016, Florent Rougon, Thomas Dickey' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +import dialog +# The short X.Y version. +version = ".".join( ( str(elt) for elt in dialog.version_info[:2] ) ) +# The full version, including alpha/beta/rc tags. +release = dialog.__version__ +del dialog + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +if sphinx.version_info >= (1, 3, 0, 'beta', 3): + html_theme = 'classic' +else: + html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# "<project> v<release> documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +html_domain_indices = False + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a <link> tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = 'http://pythondialog.sourceforge.net/doc' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'pythondialogdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +'papersize': 'a4paper', + +# The font size ('10pt', '11pt' or '12pt'). +'pointsize': '11pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'pythondialog.tex', 'pythondialog Manual', + 'Florent Rougon, Thomas Dickey', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'pythondialog', 'pythondialog Manual', + ['Florent Rougon, Thomas Dickey'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'pythondialog', 'pythondialog Manual', + 'Florent Rougon, Thomas Dickey', 'pythondialog', + 'Python module for making simple terminal-based user interfaces.', + 'Libraries'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/pythondialog/doc/exceptions.rst b/pythondialog/doc/exceptions.rst @@ -0,0 +1,98 @@ +.. currentmodule:: dialog + +pythondialog-specific exceptions +================================ + +Class hierarchy +--------------- + +Here is the hierarchy of notable exceptions raised by this module: + +| :exc:`error` +| :exc:`ExecutableNotFound` +| :exc:`BadPythonDialogUsage` +| :exc:`PythonDialogSystemError` +| :exc:`PythonDialogOSError` +| :exc:`PythonDialogIOError` (should not be raised starting from + Python 3.3, as :exc:`IOError` becomes + an alias of :exc:`OSError`) +| :exc:`PythonDialogErrorBeforeExecInChildProcess` +| :exc:`PythonDialogReModuleError` +| :exc:`UnexpectedDialogOutput` +| :exc:`DialogTerminatedBySignal` +| :exc:`DialogError` +| :exc:`UnableToRetrieveBackendVersion` +| :exc:`UnableToParseBackendVersion` +| :exc:`UnableToParseDialogBackendVersion` +| :exc:`InadequateBackendVersion` +| :exc:`PythonDialogBug` +| :exc:`ProbablyPythonBug` + +As you can see, every exception *exc* among them verifies:: + + issubclass(exc, error) + +so if you don't need fine-grained error handling, simply catch :exc:`error` +(which will probably be accessible as :exc:`dialog.error` from your program) +and you should be safe. + +.. versionchanged:: 2.12 + :exc:`PythonDialogIOError` is now a subclass of :exc:`PythonDialogOSError` + in order to help with the transition from :exc:`IOError` to :exc:`OSError` + in the Python language. With this change, you can safely replace ``except + PythonDialogIOError`` clauses with ``except PythonDialogOSError`` even if + running under Python < 3.3. + + +Detailed list +------------- + +.. autoexception:: error + +.. autoexception:: ExecutableNotFound + :show-inheritance: + +.. autoexception:: BadPythonDialogUsage + :show-inheritance: + +.. autoexception:: PythonDialogSystemError + :show-inheritance: + +.. autoexception:: PythonDialogOSError + :show-inheritance: + +.. autoexception:: PythonDialogIOError + :show-inheritance: + +.. autoexception:: PythonDialogErrorBeforeExecInChildProcess + :show-inheritance: + +.. autoexception:: PythonDialogReModuleError + :show-inheritance: + +.. autoexception:: UnexpectedDialogOutput + :show-inheritance: + +.. autoexception:: DialogTerminatedBySignal + :show-inheritance: + +.. autoexception:: DialogError + :show-inheritance: + +.. autoexception:: UnableToRetrieveBackendVersion + :show-inheritance: + +.. autoexception:: UnableToParseBackendVersion + :show-inheritance: + +.. autoexception:: UnableToParseDialogBackendVersion + :show-inheritance: + +.. autoexception:: InadequateBackendVersion + :show-inheritance: + +.. autoexception:: PythonDialogBug + :show-inheritance: + +.. autoexception:: ProbablyPythonBug + :show-inheritance: diff --git a/pythondialog/doc/glossary.rst b/pythondialog/doc/glossary.rst @@ -0,0 +1,103 @@ +.. _glossary: + +Glossary +======== + +.. currentmodule:: dialog + +.. glossary:: + + dash escaping + In a :program:`dialog` argument list, :dfn:`dash escaping` consists in + prepending an element composed of two ASCII hyphens, i.e., the string + ``'--'``, before every element that starts with two ASCII hyphens + (``--``). + + Every :program:`dialog` option starts with ``--`` (e.g., + :option:`--yesno`), but there are valid cases where one needs to pass + arguments to :program:`dialog` that start with ``--`` without having + :program:`dialog` interpret them as options. For instance, one may want + to print a text or label that starts with ``--``. In such a case, in + order to avoid confusing the argument with a :program:`dialog` option, + one must prepend an argument consisting solely of two ASCII hyphens + (``--``). This is what is called *dash escaping* here. + + For instance, in order to display a message box containing the text + ``--Not an option`` using POSIX shell syntax (the double quotes ``"`` + are stripped by the shell, :program:`dialog` does not see them): + + .. code-block:: sh + + dialog --msgbox -- "--Not an option" 0 0 # correct + + dialog --msgbox "--Not an option" 0 0 # incorrect + + .. note:: + + In pythondialog, most :class:`Dialog` public methods + (:meth:`~Dialog.msgbox`, :meth:`~Dialog.yesno`, :meth:`~Dialog.menu`, + etc.) know that the arguments they receive are not to be used as + :program:`dialog` options, and therefore automatically perform dash + escaping whenever needed to avoid having :program:`dialog` treat them + as options. At the time of this writing, the only public method that + requires you to be careful about leading double-dashes is the + low-level :meth:`Dialog.add_persistent_args`, because it directly + passes all its arguments to :program:`dialog` and cannot reliably + guess which of these the user wants to be treated as + :program:`dialog` options and which they want to be treated as + *arguments* to a :program:`dialog` option. + + See these :ref:`examples of dash escaping in pythondialog + <examples-of-dash-escaping>` using :meth:`Dialog.dash_escape` and + :meth:`Dialog.dash_escape_nf`. + + Dialog exit code + high-level exit code + A :dfn:`Dialog exit code`, or :dfn:`high-level exit code`, is a string + indicating how/why a widget-producing method ended. Most widgets return + one of the :term:`standard Dialog exit codes <standard Dialog exit + code>` (e.g., ``"ok"``, available as :attr:`Dialog.OK`). However, some + widgets may return additional, non-standard exit codes; for instance, + the :meth:`~Dialog.inputmenu` widget may return ``"accepted"`` or + ``"renamed"`` in addition to the standard Dialog exit codes. + + When returning from a widget call, the Dialog exit code is normally + derived from the :term:`dialog exit status`, also known as + :term:`low-level exit code`. + + See :ref:`Dialog-exit-code` for more details. + + standard Dialog exit code + A :dfn:`standard Dialog exit code` is a particular :term:`Dialog exit + code`. Namely, it is one of the following strings: ``"ok"``, + ``"cancel"``, ``"esc"``, ``"help"`` and ``"extra"``, respectively + available as :attr:`Dialog.OK`, :attr:`Dialog.CANCEL`, + :attr:`Dialog.ESC`, :attr:`Dialog.HELP` and :attr:`Dialog.EXTRA`, + *i.e.,* attributes of the :class:`Dialog` class. + + dialog exit status + low-level exit code + The :dfn:`dialog exit status`, or :dfn:`low-level exit code`, is an + integer returned by the :program:`dialog` backend upon exit, whose + different possible values are referred to as ``DIALOG_OK``, + ``DIALOG_CANCEL``, ``DIALOG_ESC``, ``DIALOG_ERROR``, ``DIALOG_EXTRA``, + ``DIALOG_HELP`` and ``DIALOG_ITEM_HELP`` in the :manpage:`dialog(1)` + manual page. + + See :ref:`dialog-exit-status` for more details. + + dialog common options + Options that may be passed to many widgets using keyword arguments, for + instance *defaultno*, *yes_label*, *extra_button* or + *visit_items*. These options roughly correspond to those listed in + :manpage:`dialog(1)` under the *Common Options* section. + + See :ref:`passing-dialog-common-options` for more details. + + item-help string + When using ``item_help=True`` in a widget-producing method call, every + item must have an associated string, called its :dfn:`item-help string`, + that is normally displayed by :program:`dialog` at the bottom of the + screen whenever the item is highlighted. + + See :ref:`providing-inline-per-item-help` for more details. diff --git a/pythondialog/doc/index.rst b/pythondialog/doc/index.rst @@ -0,0 +1,65 @@ +.. meta:: + :description: pythondialog's documentation + :keywords: pythondialog, Python, dialog, ncurses, Xdialog, terminal-based + interface, text-mode interface, manual + +################### +pythondialog Manual +################### + +.. module:: dialog + :synopsis: A Python interface to the UNIX dialog utility and mostly-compatible programs + :platform: Unix +.. codeauthor:: Florent Rougon +.. codeauthor:: Robb Shecter +.. codeauthor:: Peter Åstrand +.. codeauthor:: Sultanbek Tezadov +.. sectionauthor:: Florent Rougon + +This manual documents pythondialog_, a Python wrapper for the dialog_ +utility originally written by Savio Lam, and later rewritten by Thomas +E. Dickey. Its purpose is to provide an easy to use, pythonic and +comprehensive Python interface to :program:`dialog`. This allows one to make +simple text-mode user interfaces on Unix-like systems. + +.. _pythondialog: http://pythondialog.sourceforge.net/ +.. _dialog: http://invisible-island.net/dialog/dialog.html + +pythondialog's functionality is contained within the :mod:`dialog` Python +module. This module doesn't contain much Unix-specific code, if +any; however, its backend of reference, which is the :program:`dialog` +program, only works on Unix-like platforms so far as I can tell. Given a +suitable backend, the :mod:`dialog` module could work on other platforms. + + +************* +Main Contents +************* + +.. toctree:: + :maxdepth: 2 + + intro/intro + Dialog_class_overview + widgets + DialogBackendVersion + exceptions + internals + +.. reference + +.. Either this, or use the “orphan” metadata since I don't want the glossary +.. to appear in the table of contents. +.. toctree:: + :hidden: + + glossary + + +********** +Appendices +********** + +* :ref:`glossary` +* :ref:`genindex` +* :ref:`search` diff --git a/pythondialog/doc/internals.rst b/pythondialog/doc/internals.rst @@ -0,0 +1,25 @@ +.. currentmodule:: dialog + +Internals +========= + +.. warning:: + + The functions and methods listed in this section are implementation details + of pythondialog. **Do not use them in your programs**, as they are likely to + change in incompatible ways without prior notice. The only reason they are + documented here is because some public methods or functions refer to them + when listing the notable exceptions they may raise. + + +.. autofunction:: _to_onoff + +.. autofunction:: widget + +.. autofunction:: retval_is_code + +.. automethod:: Dialog._call_program + +.. automethod:: Dialog._wait_for_program_termination + +.. automethod:: Dialog._perform diff --git a/pythondialog/doc/intro/example.py b/pythondialog/doc/intro/example.py @@ -0,0 +1,65 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright (C) 2014 Florent Rougon +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Florent Rougon nor the names of other +# contributors to this file may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL FLORENT ROUGON BE LIABLE FOR ANY DIRECT, +# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import sys +import locale +import time + +from dialog import Dialog + +# This is almost always a good thing to do at the beginning of your programs. +locale.setlocale(locale.LC_ALL, '') + +d = Dialog(dialog="dialog") + +button_names = {d.OK: "OK", + d.CANCEL: "Cancel", + d.HELP: "Help", + d.EXTRA: "Extra"} + +code, tag = d.menu("Some text that will be displayed above the menu entries", + choices=[("Tag 1", "Item text 1"), + ("Tag 2", "Item text 2"), + ("Tag 3", "Item text 3")]) + +if code == d.ESC: + d.msgbox("You got out of the menu by pressing the Escape key.") +else: + text = "You got out of the menu by choosing the {} button".format( + button_names[code]) + + if code != d.CANCEL: + text += ", and the highlighted entry at that time had tag {!r}".format( + tag) + + d.msgbox(text + ".", width=40, height=10) + +d.infobox("Bye bye...", width=0, height=0, title="This is the end") +time.sleep(2) + +sys.exit(0) diff --git a/pythondialog/doc/intro/intro.rst b/pythondialog/doc/intro/intro.rst @@ -0,0 +1,226 @@ +.. currentmodule:: dialog + +A gentle introduction +===================== + +A minimal program using pythondialog starts with the creation of a +:class:`Dialog` instance:: + + from dialog import Dialog + + d = Dialog(dialog="dialog") + +The *dialog* parameter indicates the executable to use to invoke the backend +(which must be compatible with dialog_). For instance, one might use something +like ``dialog="/home/dave/src/dialog-1.2-20140219/dialog"``. The default value +is ``"dialog"``, and since it does not contain any slash (``/``), it is looked +up in the :envvar:`PATH` environment variable. See :meth:`Dialog.__init__` for +a description of all parameters that can be passed to the :class:`Dialog` +constructor. + +.. _dialog: http://invisible-island.net/dialog/dialog.html + + +Offering a choice between several options using :meth:`!menu` +------------------------------------------------------------- + +Once you have a :class:`Dialog` instance, you can call any widget-producing +method, as documented in :ref:`widgets`. For instance, if you want to display +a menu offering three choices:: + + code, tag = d.menu("Some text that will be displayed above the menu entries", + choices=[("Tag 1", "Item text 1"), + ("Tag 2", "Item text 2"), + ("Tag 3", "Item text 3")]) + +When the method returns: + + - *code* will be equal to ``d.OK`` if there was no error and the user chose + an entry (instead of pressing :kbd:`Esc`). See :ref:`Dialog-exit-code` for + more details on how to interpret the value of *code*. + - *tag* will contain the name of the tag corresponding to the selected + entry: ``"Tag 1"``, ``"Tag 2"`` or ``"Tag 3"`` (assuming that ``code == + d.OK``). + +While we kept this :meth:`~Dialog.menu` example as simple as possible, it +would be very easy to add a title line at the top of the widget window. For +this, all you need to do is to add a :samp:`title={...}` keyword argument to +the :meth:`~!Dialog.menu` method call. It is also possible to display a +background title using :samp:`backtitle={...}`, and in case you want the same +background title for all widgets, :meth:`Dialog.set_background_title` is your +friend. + +.. figure:: ../screenshots/intro/example-menu.png + :align: center + + A simple example using :meth:`Dialog.menu` + + +Displaying a message with :meth:`~!Dialog.msgbox` +------------------------------------------------- + +We can expand on the previous example by displaying an :meth:`~Dialog.msgbox` +indicating what the user has done to exit from the :meth:`~Dialog.menu`. +First, we can define a mapping from the :term:`Dialog exit codes <Dialog exit +code>` for the standard buttons to the corresponding button labels:: + + button_names = {d.OK: "OK", + d.CANCEL: "Cancel", + d.HELP: "Help", + d.EXTRA: "Extra"} + +Of course, in the previous :meth:`~Dialog.menu` widget call, the only codes +that can be returned are ``d.OK`` and ``d.CANCEL``, respectively corresponding +to the :guilabel:`OK` and :guilabel:`Cancel` buttons. Thus, we could do with +only two key-value pairs in ``button_names`` for this particular example, +however it seems cleaner and not outrageously expensive to declare the codes +for the four standard buttons like this. + +In addition to these :term:`Dialog exit codes <Dialog exit code>`, the +:meth:`~Dialog.menu` widget call can return ``d.ESC``, indicating that the +user pressed the :kbd:`Esc` key. Therefore, we are going to check for this one +too. Here it goes:: + + if code == d.ESC: + d.msgbox("You got out of the menu by pressing the Escape key.") + else: + text = "You got out of the menu by choosing the {} button".format( + button_names[code]) + + if code != d.CANCEL: + text += ", and the highlighted entry at that time had tag {!r}".format( + tag) + + d.msgbox(text + ".", width=40, height=10) + +(users of Python < 3.1 should replace ``{}`` with ``{0}`` and ``{!r}`` with +``{0!r}``) + +The above code for dealing with the :kbd:`Esc` key is pretty straightforward. +It relies on the default values to determine the width and height of the +:meth:`~Dialog.msgbox`, which are acceptable in this case. On the other hand, +the default width for :meth:`~!Dialog.msgbox` seemed too small for the message +displayed in the *else* clause, causing very irregular line lengths. In order +to compensate for this problem, we have explicitely specified the width and +height of the :meth:`~!Dialog.msgbox` using keyword arguments +(``width=40, height=10``). + +.. figure:: ../screenshots/intro/example-msgbox.png + :align: center + + A message displayed with :meth:`Dialog.msgbox` + + +Displaying a transient message with :meth:`~!Dialog.infobox` +------------------------------------------------------------ + +We can finish this little example with a widget call that displays a message +and immediately returns to the caller without waiting for the user to react. +Typically, the :meth:`~Dialog.infobox` is used to display some information +while a time-consuming operation is being performed. In this case, since we +don't have anything particularly useful to do but still want the user to be +able to read the message, we are going to wait using :func:`time.sleep`:: + + d.infobox("Bye bye...", width=0, height=0, title="This is the end") + time.sleep(2) + +We also addressed the problem of determining the widget size in a different +way as exposed earlier. By using ``width=0, height=0``, we ask +:program:`dialog` to automatically determine suitable width and height +parameters for the dialog box. If you like this method and would like to have +it used by default for all widgets without having to specify ``width=0, +height=0`` every time, you can enable the :ref:`autowidgetsize feature +<autowidgetsize>`. + +For the sake of the example, we've also specified a window title for the +:meth:`~Dialog.infobox` using a :samp:`title={...}` keyword argument. This can +be done with most widgets, and is entirely optional. + +.. figure:: ../screenshots/intro/example-infobox.png + :align: center + + A transient message displayed with :meth:`Dialog.infobox` + +Of course, the :func:`time.sleep` call requires an:: + + import time + +statement that you, careful reader, had already added. In order to exit +cleanly from our program, I also suggest to end with:: + + sys.exit(0) + +which requires an:: + + import sys + +at the top of your script. And finally, since you don't want to take bad +habits, I would also suggest starting your program with:: + + locale.setlocale(locale.LC_ALL, '') + +which in turn requires an:: + + import locale + +at the top. + + +Putting it all together +----------------------- + +If we put all the pieces from this chapter together and reorder a tiny bit to +improve readability, we obtain the code for our example program:: + + import sys + import locale + import time + + from dialog import Dialog + + # This is almost always a good thing to do at the beginning of your programs. + locale.setlocale(locale.LC_ALL, '') + + d = Dialog(dialog="dialog") + + button_names = {d.OK: "OK", + d.CANCEL: "Cancel", + d.HELP: "Help", + d.EXTRA: "Extra"} + + code, tag = d.menu("Some text that will be displayed above the menu entries", + choices=[("Tag 1", "Item text 1"), + ("Tag 2", "Item text 2"), + ("Tag 3", "Item text 3")]) + + if code == d.ESC: + d.msgbox("You got out of the menu by pressing the Escape key.") + else: + text = "You got out of the menu by choosing the {} button".format( + button_names[code]) + + if code != d.CANCEL: + text += ", and the highlighted entry at that time had tag {!r}".format( + tag) + + d.msgbox(text + ".", width=40, height=10) + + d.infobox("Bye bye...", width=0, height=0, title="This is the end") + time.sleep(2) + + sys.exit(0) + + +Other examples +-------------- + +For an example that is slightly different from the one exposed in this +chapter, you can look at the :file:`simple_example.py` file that comes with +pythondialog, in the :file:`examples` directory. It is a very simple and +straightforward example using a few basic widgets. The `pythondialog website +<http://pythondialog.sourceforge.net/>`_ also has a very simple example that +can be used to get started. + +Once you are comfortable with the basics, you can study the :file:`demo.py` +file that illustrates most features of pythondialog (also from the +:file:`examples` directory), or more directly :file:`dialog.py`. diff --git a/pythondialog/doc/screenshots/buildlist.png b/pythondialog/doc/screenshots/buildlist.png Binary files differ. diff --git a/pythondialog/doc/screenshots/calendar.png b/pythondialog/doc/screenshots/calendar.png Binary files differ. diff --git a/pythondialog/doc/screenshots/checklist.png b/pythondialog/doc/screenshots/checklist.png Binary files differ. diff --git a/pythondialog/doc/screenshots/dselect.png b/pythondialog/doc/screenshots/dselect.png Binary files differ. diff --git a/pythondialog/doc/screenshots/editbox.png b/pythondialog/doc/screenshots/editbox.png Binary files differ. diff --git a/pythondialog/doc/screenshots/form.png b/pythondialog/doc/screenshots/form.png Binary files differ. diff --git a/pythondialog/doc/screenshots/fselect.png b/pythondialog/doc/screenshots/fselect.png Binary files differ. diff --git a/pythondialog/doc/screenshots/gauge.png b/pythondialog/doc/screenshots/gauge.png Binary files differ. diff --git a/pythondialog/doc/screenshots/infobox.png b/pythondialog/doc/screenshots/infobox.png Binary files differ. diff --git a/pythondialog/doc/screenshots/inputbox.png b/pythondialog/doc/screenshots/inputbox.png Binary files differ. diff --git a/pythondialog/doc/screenshots/inputmenu.png b/pythondialog/doc/screenshots/inputmenu.png Binary files differ. diff --git a/pythondialog/doc/screenshots/intro/example-infobox.png b/pythondialog/doc/screenshots/intro/example-infobox.png Binary files differ. diff --git a/pythondialog/doc/screenshots/intro/example-menu.png b/pythondialog/doc/screenshots/intro/example-menu.png Binary files differ. diff --git a/pythondialog/doc/screenshots/intro/example-msgbox.png b/pythondialog/doc/screenshots/intro/example-msgbox.png Binary files differ. diff --git a/pythondialog/doc/screenshots/menu.png b/pythondialog/doc/screenshots/menu.png Binary files differ. diff --git a/pythondialog/doc/screenshots/mixedform.png b/pythondialog/doc/screenshots/mixedform.png Binary files differ. diff --git a/pythondialog/doc/screenshots/mixedgauge.png b/pythondialog/doc/screenshots/mixedgauge.png Binary files differ. diff --git a/pythondialog/doc/screenshots/msgbox.png b/pythondialog/doc/screenshots/msgbox.png Binary files differ. diff --git a/pythondialog/doc/screenshots/passwordbox.png b/pythondialog/doc/screenshots/passwordbox.png Binary files differ. diff --git a/pythondialog/doc/screenshots/passwordform.png b/pythondialog/doc/screenshots/passwordform.png Binary files differ. diff --git a/pythondialog/doc/screenshots/pause.png b/pythondialog/doc/screenshots/pause.png Binary files differ. diff --git a/pythondialog/doc/screenshots/programbox.png b/pythondialog/doc/screenshots/programbox.png Binary files differ. diff --git a/pythondialog/doc/screenshots/progressbox.png b/pythondialog/doc/screenshots/progressbox.png Binary files differ. diff --git a/pythondialog/doc/screenshots/radiolist.png b/pythondialog/doc/screenshots/radiolist.png Binary files differ. diff --git a/pythondialog/doc/screenshots/rangebox.png b/pythondialog/doc/screenshots/rangebox.png Binary files differ. diff --git a/pythondialog/doc/screenshots/scrollbox.png b/pythondialog/doc/screenshots/scrollbox.png Binary files differ. diff --git a/pythondialog/doc/screenshots/tailbox.png b/pythondialog/doc/screenshots/tailbox.png Binary files differ. diff --git a/pythondialog/doc/screenshots/textbox.png b/pythondialog/doc/screenshots/textbox.png Binary files differ. diff --git a/pythondialog/doc/screenshots/timebox.png b/pythondialog/doc/screenshots/timebox.png Binary files differ. diff --git a/pythondialog/doc/screenshots/treeview.png b/pythondialog/doc/screenshots/treeview.png Binary files differ. diff --git a/pythondialog/doc/screenshots/yesno.png b/pythondialog/doc/screenshots/yesno.png Binary files differ. diff --git a/pythondialog/doc/widgets.rst b/pythondialog/doc/widgets.rst @@ -0,0 +1,381 @@ +.. currentmodule:: dialog + +.. _widgets: + +The :class:`Dialog` widgets +=========================== + +This section describes all widgets (or dialog boxes) offered by the +:class:`Dialog` class. The descriptions of many of them are adapted from the +:manpage:`dialog(1)` manual page, with the kind permission of `Thomas Dickey +<http://invisible-island.net/>`_. + + +.. note:: + + All unqualified method names in this section are methods of the + :class:`Dialog` class. In other words, whenever a method :meth:`!foo` is + mentioned, you have to understand :meth:`!dialog.Dialog.foo`. + +.. warning:: + + Concerning the older widgets that have fixed defaults for the length + parameters such as *width* and *height*: + + Even though explicitely setting one of these length parameters to ``None`` + will not cause any error in this version, please don't do it. If you know + the size you want, specify it directly (e.g., ``width=78``). On the other + hand, if you want :program:`dialog` to automagically figure out a suitable + size, you have two options: + + - either enable the :ref:`autowidgetsize <autowidgetsize>` option and + make sure not to specify the length parameter in the widget call; + - or explicitely set it to ``0`` (e.g., ``width=0``). + + +Displaying multi-line text +-------------------------- + +Message box +^^^^^^^^^^^ + +.. automethod:: Dialog.msgbox + +.. figure:: screenshots/msgbox.png + :align: center + + :meth:`~Dialog.msgbox` example + + +Text box +^^^^^^^^ + +.. automethod:: Dialog.textbox + +.. figure:: screenshots/textbox.png + :align: center + + :meth:`~Dialog.textbox` example + + +Scroll box +^^^^^^^^^^ + +.. Automethod:: Dialog.scrollbox + +.. figure:: screenshots/scrollbox.png + :align: center + + :meth:`~Dialog.scrollbox` example + + +Edit box +^^^^^^^^ + +.. automethod:: Dialog.editbox + +.. figure:: screenshots/editbox.png + :align: center + + :meth:`~Dialog.editbox` example + +.. automethod:: Dialog.editbox_str + + +Progress box +^^^^^^^^^^^^ + +.. automethod:: Dialog.progressbox + +.. figure:: screenshots/progressbox.png + :align: center + + :meth:`~Dialog.progressbox` example + + +Program box +^^^^^^^^^^^ + +.. automethod:: Dialog.programbox + +.. figure:: screenshots/programbox.png + :align: center + + :meth:`~Dialog.programbox` example + + +Tail box +^^^^^^^^ + +.. automethod:: Dialog.tailbox + +.. figure:: screenshots/tailbox.png + :align: center + + :meth:`~Dialog.tailbox` example + + + +Displaying transient messages +----------------------------- + +Info box +^^^^^^^^ + +.. automethod:: Dialog.infobox + +.. figure:: screenshots/infobox.png + :align: center + + :meth:`~Dialog.infobox` example + + +Pause +^^^^^ + +.. automethod:: Dialog.pause + +.. figure:: screenshots/pause.png + :align: center + + :meth:`~Dialog.pause` example + + +Progress meters +--------------- + +.. _gauge-widget: + +Regular gauge +^^^^^^^^^^^^^ + +.. automethod:: Dialog.gauge_start + +.. automethod:: Dialog.gauge_update + +.. automethod:: Dialog.gauge_iterate + +.. automethod:: Dialog.gauge_stop + +.. figure:: screenshots/gauge.png + :align: center + + :meth:`~Dialog.gauge` example + + +Mixed gauge +^^^^^^^^^^^ + +.. automethod:: Dialog.mixedgauge + +.. figure:: screenshots/mixedgauge.png + :align: center + + :meth:`~Dialog.mixedgauge` example + + +List-like widgets +----------------- + +Build list +^^^^^^^^^^ + +.. automethod:: Dialog.buildlist + +.. figure:: screenshots/buildlist.png + :align: center + + :meth:`~Dialog.buildlist` example + + +Check list +^^^^^^^^^^ + +.. automethod:: Dialog.checklist + +.. figure:: screenshots/checklist.png + :align: center + + :meth:`~Dialog.checklist` example + + +Menu +^^^^ + +.. automethod:: Dialog.menu + +.. figure:: screenshots/menu.png + :align: center + + :meth:`~Dialog.menu` example + + +Radio list +^^^^^^^^^^ + +.. automethod:: Dialog.radiolist + +.. figure:: screenshots/radiolist.png + :align: center + + :meth:`~Dialog.radiolist` example + + +Tree view +^^^^^^^^^ + +.. automethod:: Dialog.treeview + +.. figure:: screenshots/treeview.png + :align: center + + :meth:`~Dialog.treeview` example + + + +Single-line input fields +------------------------ + +Input box +^^^^^^^^^ + +.. automethod:: Dialog.inputbox + +.. figure:: screenshots/inputbox.png + :align: center + + :meth:`~Dialog.inputbox` example + + +Input menu +^^^^^^^^^^ + +.. automethod:: Dialog.inputmenu + +.. figure:: screenshots/inputmenu.png + :align: center + + :meth:`~Dialog.inputmenu` example + + +Password box +^^^^^^^^^^^^ + +.. automethod:: Dialog.passwordbox + +.. figure:: screenshots/passwordbox.png + :align: center + + :meth:`~Dialog.passwordbox` example + + + +Forms +----- + +Form +^^^^ + +.. automethod:: Dialog.form + +.. figure:: screenshots/form.png + :align: center + + :meth:`~Dialog.form` example + + +Mixed form +^^^^^^^^^^ + +.. automethod:: Dialog.mixedform + +.. figure:: screenshots/mixedform.png + :align: center + + :meth:`~Dialog.mixedform` example + + +Password form +^^^^^^^^^^^^^ + +.. automethod:: Dialog.passwordform + +.. figure:: screenshots/passwordform.png + :align: center + + :meth:`~Dialog.passwordform` example + + +Selecting files and directories +------------------------------- + +Directory selection +^^^^^^^^^^^^^^^^^^^ + +.. automethod:: Dialog.dselect + +.. figure:: screenshots/dselect.png + :align: center + + :meth:`~Dialog.dselect` example + + +File or directory selection +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automethod:: Dialog.fselect + +.. figure:: screenshots/fselect.png + :align: center + + :meth:`~Dialog.fselect` example + + +Date and time +------------- + +Calendar +^^^^^^^^ + +.. automethod:: Dialog.calendar + +.. figure:: screenshots/calendar.png + :align: center + + :meth:`~Dialog.calendar` example + + +Time box +^^^^^^^^ + +.. automethod:: Dialog.timebox + +.. figure:: screenshots/timebox.png + :align: center + + :meth:`~Dialog.timebox` example + + +Miscellaneous +------------- + +Range box +^^^^^^^^^ + +.. automethod:: Dialog.rangebox + +.. figure:: screenshots/rangebox.png + :align: center + + :meth:`~Dialog.rangebox` example + + +Yes/No +^^^^^^ + +.. automethod:: Dialog.yesno + +.. figure:: screenshots/yesno.png + :align: center + + :meth:`~Dialog.yesno` example diff --git a/pythondialog/examples/demo.py b/pythondialog/examples/demo.py @@ -0,0 +1,1702 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- + +# demo.py --- Demonstration program and cheap test suite for pythondialog +# +# Copyright (C) 2002-2010, 2013-2016 Florent Rougon +# Copyright (C) 2000 Robb Shecter, Sultanbek Tezadov +# +# This program is in the public domain. + +"""Demonstration program for pythondialog. + +This is a program demonstrating most of the possibilities offered by +the pythondialog module (which is itself a Python interface to the +well-known dialog utility, or any other program compatible with +dialog). + +Executive summary +----------------- + +If you are looking for a very simple example of pythondialog usage, +short and straightforward, please refer to simple_example.py. The +file you are now reading serves more as a demonstration of what can +be done with pythondialog and as a cheap test suite than as a first +time tutorial. However, it can also be used to learn how to invoke +the various widgets. The following paragraphs explain what you should +keep in mind if you read it for this purpose. + + +Most of the code in the MyApp class (which defines the actual +contents of the demo) relies on a class called MyDialog implemented +here that: + + 1. wraps all widget-producing calls in a way that automatically + spawns a "confirm quit" dialog box if the user presses the + Escape key or chooses the Cancel button, and then redisplays the + original widget if the user doesn't actually want to quit; + + 2. provides a few additional dialog-related methods and convenience + wrappers. + +The handling in (1) is completely automatic, implemented with +MyDialog.__getattr__() returning decorated versions of the +widget-producing methods of dialog.Dialog. Therefore, most of the +demo can be read as if the module-level 'd' attribute were a +dialog.Dialog instance whereas it is actually a MyDialog instance. +The only meaningful difference is that MyDialog.<widget>() will never +return a CANCEL or ESC code (attributes of 'd', or more generally of +dialog.Dialog). The reason is that these return codes are +automatically handled by the MyDialog.__getattr__() machinery to +display the "confirm quit" dialog box. + +In some cases (e.g., fselect_demo()), I wanted the "Cancel" button to +perform a specific action instead of spawning the "confirm quit" +dialog box. To achieve this, the widget is invoked using +dialog.Dialog.<widget> instead of MyDialog.<widget>, and the return +code is handled in a semi-manual way. A prominent feature that needs +such special-casing is the yesno widget, because the "No" button +corresponds to the CANCEL exit code, which in general must not be +interpreted as an attempt to quit the program! + +To sum it up, you can read most of the code in the MyApp class (which +defines the actual contents of the demo) as if 'd' were a +dialog.Dialog instance. Just keep in mind that there is a little +magic behind the scenes that automatically handles the CANCEL and ESC +Dialog exit codes, which wouldn't be the case if 'd' were a +dialog.Dialog instance. For a first introduction to pythondialog with +simple stuff and absolutely no magic, please have a look at +simple_example.py. + +""" + + +import sys, os, locale, stat, time, getopt, subprocess, traceback, textwrap +import pprint +import dialog +from dialog import DialogBackendVersion + +progname = os.path.basename(sys.argv[0]) +progversion = "0.12" +version_blurb = """Demonstration program and cheap test suite for pythondialog. + +Copyright (C) 2002-2010, 2013-2016 Florent Rougon +Copyright (C) 2000 Robb Shecter, Sultanbek Tezadov + +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.""" + +default_debug_filename = "pythondialog.debug" + +usage = """Usage: {progname} [option ...] +Demonstration program and cheap test suite for pythondialog. + +Options: + -t, --test-suite test all widgets; implies '--fast' + -f, --fast fast mode (e.g., makes the gauge demo run faster) + --debug enable logging of all dialog command lines + --debug-file=FILE where to write debug information (default: + {debug_file} in the current directory) + -E, --debug-expand-file-opt expand the '--file' options in the debug file + generated by '--debug' + --help display this message and exit + --version output version information and exit""".format( + progname=progname, debug_file=default_debug_filename) + +# Global parameters +params = {} + +# We'll use a module-level attribute 'd' ("global") to store the MyDialog +# instance that is used throughout the demo. This object could alternatively be +# passed to the MyApp constructor and stored there as a class or instance +# attribute. However, for the sake of readability, we'll simply use a global +# (d.msgbox(...) versus self.d.msgbox(...), etc.). +d = None + +tw = textwrap.TextWrapper(width=78, break_long_words=False, + break_on_hyphens=True) +from textwrap import dedent + +try: + from textwrap import indent +except ImportError: + try: + callable # Normally, should be __builtins__.callable + except NameError: + # Python 3.1 doesn't have the 'callable' builtin function. Let's + # provide ours. + def callable(f): + return hasattr(f, '__call__') + + def indent(text, prefix, predicate=None): + l = [] + + for line in text.splitlines(True): + if (callable(predicate) and predicate(line)) \ + or (not callable(predicate) and predicate) \ + or (predicate is None and line.strip()): + line = prefix + line + l.append(line) + + return ''.join(l) + + +class MyDialog: + """Wrapper class for dialog.Dialog. + + This class behaves similarly to dialog.Dialog. The differences + are that: + + 1. MyDialog wraps all widget-producing methods in a way that + automatically spawns a "confirm quit" dialog box if the user + presses the Escape key or chooses the Cancel button, and + then redisplays the original widget if the user doesn't + actually want to quit. + + 2. MyDialog provides a few additional dialog-related methods + and convenience wrappers. + + Please refer to the module docstring and to the particular + methods for more details. + + """ + def __init__(self, Dialog_instance): + self.dlg = Dialog_instance + + def check_exit_request(self, code, ignore_Cancel=False): + if code == self.CANCEL and ignore_Cancel: + # Ignore the Cancel button, i.e., don't interpret it as an exit + # request; instead, let the caller handle CANCEL himself. + return True + + if code in (self.CANCEL, self.ESC): + button_name = { self.CANCEL: "Cancel", + self.ESC: "Escape" } + msg = "You pressed {0} in the last dialog box. Do you want " \ + "to exit this demo?".format(button_name[code]) + # 'self.dlg' instead of 'self' here, because we want to use the + # original yesno() method from the Dialog class instead of the + # decorated method returned by self.__getattr__(). + if self.dlg.yesno(msg) == self.OK: + sys.exit(0) + else: # "No" button chosen, or ESC pressed + return False # in the "confirm quit" dialog + else: + return True + + def widget_loop(self, method): + """Decorator to handle eventual exit requests from a Dialog widget. + + method -- a dialog.Dialog method that returns either a Dialog + exit code, or a sequence whose first element is a + Dialog exit code (cf. the docstring of the Dialog + class in dialog.py) + + Return a wrapper function that behaves exactly like 'method', + except for the following point: + + If the Dialog exit code obtained from 'method' is CANCEL or + ESC (attributes of dialog.Dialog), a "confirm quit" dialog + is spawned; depending on the user choice, either the + program exits or 'method' is called again, with the same + arguments and same handling of the exit status. In other + words, the wrapper function builds a loop around 'method'. + + The above condition on 'method' is satisfied for all + dialog.Dialog widget-producing methods. More formally, these + are the methods defined with the @widget decorator in + dialog.py, i.e., that have an "is_widget" attribute set to + True. + + """ + # One might want to use @functools.wraps here, but since the wrapper + # function is very likely to be used only once and then + # garbage-collected, this would uselessly add a little overhead inside + # __getattr__(), where widget_loop() is called. + def wrapper(*args, **kwargs): + while True: + res = method(*args, **kwargs) + + if hasattr(method, "retval_is_code") \ + and getattr(method, "retval_is_code"): + code = res + else: + code = res[0] + + if self.check_exit_request(code): + break + return res + + return wrapper + + def __getattr__(self, name): + # This is where the "magic" of this class originates from. + # Please refer to the module and self.widget_loop() + # docstrings if you want to understand the why and the how. + obj = getattr(self.dlg, name) + if hasattr(obj, "is_widget") and getattr(obj, "is_widget"): + return self.widget_loop(obj) + else: + return obj + + def clear_screen(self): + # This program comes with ncurses + program = "clear" + + try: + p = subprocess.Popen([program], shell=False, stdout=None, + stderr=None, close_fds=True) + retcode = p.wait() + except os.error as e: + self.msgbox("Unable to execute program '%s': %s." % (program, + e.strerror), + title="Error") + return False + + if retcode > 0: + msg = "Program %s returned exit status %d." % (program, retcode) + elif retcode < 0: + msg = "Program %s was terminated by signal %d." % (program, -retcode) + else: + return True + + self.msgbox(msg) + return False + + def _Yesno(self, *args, **kwargs): + """Convenience wrapper around dialog.Dialog.yesno(). + + Return the same exit code as would return + dialog.Dialog.yesno(), except for ESC which is handled as in + the rest of the demo, i.e. make it spawn the "confirm quit" + dialog. + + """ + # self.yesno() automatically spawns the "confirm quit" dialog if ESC or + # the "No" button is pressed, because of self.__getattr__(). Therefore, + # we have to use self.dlg.yesno() here and call + # self.check_exit_request() manually. + while True: + code = self.dlg.yesno(*args, **kwargs) + # If code == self.CANCEL, it means the "No" button was chosen; + # don't interpret this as a wish to quit the program! + if self.check_exit_request(code, ignore_Cancel=True): + break + + return code + + def Yesno(self, *args, **kwargs): + """Convenience wrapper around dialog.Dialog.yesno(). + + Return True if "Yes" was chosen, False if "No" was chosen, + and handle ESC as in the rest of the demo, i.e. make it spawn + the "confirm quit" dialog. + + """ + return self._Yesno(*args, **kwargs) == self.dlg.OK + + def Yesnohelp(self, *args, **kwargs): + """Convenience wrapper around dialog.Dialog.yesno(). + + Return "yes", "no", "extra" or "help" depending on the button + that was pressed to close the dialog. ESC is handled as in + the rest of the demo, i.e. it spawns the "confirm quit" + dialog. + + """ + kwargs["help_button"] = True + code = self._Yesno(*args, **kwargs) + d = { self.dlg.OK: "yes", + self.dlg.CANCEL: "no", + self.dlg.EXTRA: "extra", + self.dlg.HELP: "help" } + + return d[code] + + +# Dummy context manager to make sure the debug file is closed on exit, be it +# normal or abnormal, and to avoid having two code paths, one for normal mode +# and one for debug mode. +class DummyContextManager: + def __enter__(self): + return self + + def __exit__(self, *exc): + return False + + +class MyApp: + def __init__(self): + # The MyDialog instance 'd' could be passed via the constructor and + # stored here as a class or instance attribute. However, for the sake + # of readability, we'll simply use a module-level attribute ("global") + # (d.msgbox(...) versus self.d.msgbox(...), etc.). + global d + # If you want to use Xdialog (pathnames are also OK for the 'dialog' + # argument), you can use: + # dialog.Dialog(dialog="Xdialog", compat="Xdialog") + self.Dialog_instance = dialog.Dialog(dialog="dialog") + # See the module docstring at the top of the file to understand the + # purpose of MyDialog. + d = MyDialog(self.Dialog_instance) + backtitle = "pythondialog demo" + d.set_background_title(backtitle) + # These variables take the background title into account + self.max_lines, self.max_cols = d.maxsize(backtitle=backtitle) + self.demo_context = self.setup_debug() + # Warn if the terminal is smaller than this size + self.min_rows, self.min_cols = 24, 80 + self.term_rows, self.term_cols, self.backend_version = \ + self.get_term_size_and_backend_version() + + def setup_debug(self): + if params["debug"]: + debug_file = open(params["debug_filename"], "w") + d.setup_debug(True, file=debug_file, + expand_file_opt=params["debug_expand_file_opt"]) + return debug_file + else: + return DummyContextManager() + + def get_term_size_and_backend_version(self): + # Avoid running '<backend> --print-version' every time we need the + # version + backend_version = d.cached_backend_version + if not backend_version: + print(tw.fill( + "Unable to retrieve the version of the dialog-like backend. " + "Not running cdialog?") + "\nPress Enter to continue.", + file=sys.stderr) + input() + + term_rows, term_cols = d.maxsize(use_persistent_args=False) + if term_rows < self.min_rows or term_cols < self.min_cols: + print(tw.fill(dedent("""\ + Your terminal has less than {0} rows or less than {1} columns; + you may experience problems with the demo. You have been warned.""" + .format(self.min_rows, self.min_cols))) + + "\nPress Enter to continue.") + input() + + return (term_rows, term_cols, backend_version) + + def run(self): + with self.demo_context: + if params["testsuite_mode"]: + # Show the additional widgets before the "normal demo", so that + # I can test new widgets quickly and simply hit Ctrl-C once + # they've been shown. + self.additional_widgets() + + # "Normal" demo + self.demo() + + def demo(self): + d.msgbox("""\ +Hello, and welcome to the pythondialog {pydlg_version} demonstration program. + +You can scroll through this dialog box with the Page Up and Page Down keys. \ +Please note that some of the dialogs will not work, and cause the demo to \ +stop, if your terminal is too small. The recommended size is (at least) \ +{min_rows} rows by {min_cols} columns. + +This script is being run by a Python interpreter identified as follows: + +{py_version} + +The dialog-like program displaying this message box reports version \ +{backend_version} and a terminal size of {rows} rows by {cols} columns.""" + .format( + pydlg_version=dialog.__version__, + backend_version=self.backend_version, + py_version=indent(sys.version, " "), + rows=self.term_rows, cols=self.term_cols, + min_rows=self.min_rows, min_cols=self.min_cols), + width=60, height=17) + + self.progressbox_demo_with_file_descriptor() + # First dialog version where the programbox widget works fine + if self.dialog_version_check("1.2-20140112"): + self.programbox_demo() + self.infobox_demo() + self.gauge_demo() + answer = self.yesno_demo(with_help=True) + self.msgbox_demo(answer) + self.textbox_demo() + name = self.inputbox_demo_with_help() + size, weight, city, state, country, last_will1, last_will2, \ + last_will3, last_will4, secret_code = self.mixedform_demo() + self.form_demo_with_help() + favorite_day = self.menu_demo(name, city, state, country, size, weight, + secret_code, last_will1, last_will2, + last_will3, last_will4) + + if self.dialog_version_check("1.2-20130902", + "the menu demo with help facilities", + explain=True): + self.menu_demo_with_help() + + toppings = self.checklist_demo() + if self.dialog_version_check("1.2-20130902", + "the checklist demo with help facilities", + explain=True): + self.checklist_demo_with_help() + + sandwich = self.radiolist_demo() + + if self.dialog_version_check("1.2-20121230", "the rangebox demo", explain=True): + nb_engineers = self.rangebox_demo() + else: + nb_engineers = None + + if self.dialog_version_check("1.2-20121230", "the buildlist demo", explain=True): + desert_island_stuff = self.buildlist_demo() + else: + desert_island_stuff = None + + if self.dialog_version_check("1.2-20130902", + "the buildlist demo with help facilities", + explain=True): + self.buildlist_demo_with_help() + + date = self.calendar_demo_with_help() + time_ = self.timebox_demo() + + password = self.passwordbox_demo() + self.scrollbox_demo(name, favorite_day, toppings, sandwich, + nb_engineers, desert_island_stuff, date, time_, + password) + + if self.dialog_version_check("1.2-20121230", "the treeview demo", + explain=True): + if self.dialog_version_check("1.2-20130902"): + self.treeview_demo_with_help() + else: + self.treeview_demo() + + self.mixedgauge_demo() + self.editbox_demo("/etc/passwd") + self.inputmenu_demo() + d.msgbox("""\ +Haha. You thought it was over. Wrong. Even more fun is to come! + +Now, please select a file you would like to see growing (or not...).""", + width=75) + + # Looks nicer if the screen is not completely filled by the widget, + # hence the -1. + self.tailbox_demo(height=self.max_lines-1, + width=self.max_cols) + + directory = self.dselect_demo() + + timeout = 2 if params["fast_mode"] else 20 + self.pause_demo(timeout) + + d.clear_screen() + if not params["fast_mode"]: + # Rest assured, this is not necessary in any way: it is only a + # psychological trick to try to give the impression of a reboot + # (cf. pause_demo(); would be even nicer with a "visual bell")... + time.sleep(1) + + def additional_widgets(self): + # Requires a careful choice of the file to be of any interest + self.progressbox_demo_with_filepath() + # This can be confusing without any pause if the user specified a + # regular file. + time.sleep(1 if params["fast_mode"] else 2) + + # programbox_demo is fine right after + # progressbox_demo_with_file_descriptor in demo(), but there was a + # little bug in dialog that made the first two lines disappear too + # early. This bug has been fixed in version 1.2-20140112, therefore + # we'll run the programbox_demo as part of the main demo if the dialog + # version is >= than this one, otherwise we'll keep it here. + if self.dialog_version_check("1.1-20110302", "the programbox demo", + explain=True): + # First dialog version where the programbox widget works fine + if not self.dialog_version_check("1.2-20140112"): + self.programbox_demo() + # Almost identical to mixedform (mixedform being more powerful). Also, + # there is now form_demo_with_help() which uses the form widget. + self.form_demo() + # Almost identical to passwordbox + self.passwordform_demo() + + def dialog_version_check(self, version_string, feature="", *, start="", + explain=False): + if d.compat != "dialog": + # non-dialog implementations are not affected by + # 'dialog_version_check'. + return True + + minimum_version = DialogBackendVersion.fromstring(version_string) + res = (d.cached_backend_version >= minimum_version) + + if explain and not res: + self.too_old_dialog_version(feature=feature, start=start, + min=version_string) + + return res + + def too_old_dialog_version(self, feature="", *, start="", min=None): + assert (feature and not start) or (not feature and start), \ + (feature, start) + if not start: + start = "Skipping {0},".format(feature) + + d.msgbox( + "{start} because it requires dialog {min} or later; " + "however, it appears that you are using version {used}.".format( + start=start, min=min, used=d.cached_backend_version), + width=60, height=9, title="Demo skipped") + + def progressbox_demo_with_filepath(self): + widget = "progressbox" + + # First, ask the user for a file (possibly FIFO) + d.msgbox(self.FIFO_HELP(widget), width=72, height=20) + path = self.fselect_demo(widget, allow_FIFOs=True, + title="Please choose a file to be shown as " + "with 'tail -f'") + if path is None: + # User chose to abort + return + else: + d.progressbox(file_path=path, + text="You can put some header text here", + title="Progressbox example with a file path") + + def progressboxoid(self, widget, func_name, text, **kwargs): + # Since this is just a demo, I will not try to catch os.error exceptions + # in this function, for the sake of readability. + read_fd, write_fd = os.pipe() + + child_pid = os.fork() + if child_pid == 0: + try: + # We are in the child process. We MUST NOT raise any exception. + # No need for this one in the child process + os.close(read_fd) + + # Python file objects are easier to use than file descriptors. + # For a start, you don't have to check the number of bytes + # actually written every time... + # "buffering = 1" means wfile is going to be line-buffered + with os.fdopen(write_fd, mode="w", buffering=1) as wfile: + for line in text.split('\n'): + wfile.write(line + '\n') + time.sleep(0.02 if params["fast_mode"] else 1.2) + + os._exit(0) + except: + os._exit(127) + + # We are in the father process. No need for write_fd anymore. + os.close(write_fd) + # Call d.progressbox() if widget == "progressbox" + # d.programbox() if widget == "programbox" + # etc. + getattr(d, widget)( + fd=read_fd, + title="{0} example with a file descriptor".format(widget), + **kwargs) + + # Now that the progressbox is over (second child process, running the + # dialog-like program), we can wait() for the first child process. + # Otherwise, we could have a deadlock in case the pipe gets full, since + # dialog wouldn't be reading it. + exit_info = os.waitpid(child_pid, 0)[1] + if os.WIFEXITED(exit_info): + exit_code = os.WEXITSTATUS(exit_info) + elif os.WIFSIGNALED(exit_info): + d.msgbox("%s(): first child process terminated by signal %d" % + (func_name, os.WTERMSIG(exit_info))) + else: + assert False, "How the hell did we manage to get here?" + + if exit_code != 0: + d.msgbox("%s(): first child process ended with exit status %d" + % (func_name, exit_code)) + + def progressbox_demo_with_file_descriptor(self): + func_name = "progressbox_demo_with_file_descriptor" + text = """\ +A long time ago in a galaxy far, +far away... + + + + + +A NEW HOPE + +It was a period of intense +sucking. Graphical toolkits for +Python were all nice and clean, +but they were, well, graphical. +And as every one knows, REAL +PROGRAMMERS ALWAYS WORK ON VT-100 +TERMINALS. In text mode. + +Besides, those graphical toolkits +were usually too complex for +simple programs, so most FLOSS +geeks ended up writing +command-line tools except when +they really needed the full power +of mainstream graphical toolkits, +such as Qt, GTK+ and wxWidgets. + +But... thanks to people like +Thomas E. Dickey, there are now +at our disposal several free +software command-line programs, +such as dialog, that allow easy +building of graphically-oriented +interfaces in text-mode +terminals. These are good for +tasks where line-oriented +interfaces are not well suited, +as well as for the increasingly +common type who runs away as soon +as he sees something remotely +resembling a command line. + +But this is not for Python! I want +my poney! + +Seeing this unacceptable +situation, Robb Shecter had the +idea, back in the olden days of +Y2K (when the world was supposed +to suddenly collapse, remember?), +to wrap a dialog interface into a +Python module called dialog.py. + +pythondialog was born. Florent +Rougon, who was looking for +something like that in 2002, +found the idea rather cool and +improved the module during the +following years...""" + 15*'\n' + + return self.progressboxoid("progressbox", func_name, text) + + def programbox_demo(self): + func_name = "programbox_demo" + text = """\ +The 'progressbox' widget +has a little brother +called 'programbox' +that displays text +read from a pipe +and only adds an OK button +when the pipe indicates EOF +(End Of File). + +This can be used +to display the output +of some external program. + +This will be done right away if you choose "Yes" in the next dialog. +This choice will cause 'find /usr/bin' to be run with subprocess.Popen() +and the output to be displayed, via a pipe, in a 'programbox' widget.""" + self.progressboxoid("programbox", func_name, text) + + if d.Yesno("Do you want to run 'find /usr/bin' in a programbox widget?"): + try: + devnull = subprocess.DEVNULL + except AttributeError: # Python < 3.3 + devnull_context = devnull = open(os.devnull, "wb") + else: + devnull_context = DummyContextManager() + + args = ["find", "/usr/bin"] + with devnull_context: + p = subprocess.Popen(args, stdout=subprocess.PIPE, + stderr=devnull, close_fds=True) + # One could use title=... instead of text=... to put the text + # in the title bar. + d.programbox(fd=p.stdout.fileno(), + text="Example showing the output of a command " + "with programbox") + retcode = p.wait() + + # Context manager support for subprocess.Popen objects requires + # Python 3.2 or later. + p.stdout.close() + return retcode + else: + return None + + def infobox_demo(self): + d.infobox("One moment, please. Just wasting some time here to " + "show you the infobox...") + + time.sleep(0.5 if params["fast_mode"] else 4.0) + + def gauge_demo(self): + d.gauge_start("Progress: 0%", title="Still testing your patience...") + + for i in range(1, 101): + if i < 50: + d.gauge_update(i, "Progress: {0}%".format(i), update_text=True) + elif i == 50: + d.gauge_update(i, "Over {0}%. Good.".format(i), + update_text=True) + elif i == 80: + d.gauge_update(i, "Yeah, this boring crap will be over Really " + "Soon Now.", update_text=True) + else: + d.gauge_update(i) + + time.sleep(0.01 if params["fast_mode"] else 0.1) + + d.gauge_stop() + + def mixedgauge_demo(self): + for i in range(1, 101, 20): + d.mixedgauge("This is the 'text' part of the mixedgauge\n" + "and this is a forced new line.", + title="'mixedgauge' demo", + percent=int(round(72+28*i/100)), + elements=[("Task 1", "Foobar"), + ("Task 2", 0), + ("Task 3", 1), + ("Task 4", 2), + ("Task 5", 3), + ("", 8), + ("Task 6", 5), + ("Task 7", 6), + ("Task 8", 7), + ("", ""), + # 0 is the dialog special code for + # "Succeeded", so these must not be equal to + # zero! That is why I made the range() above + # start at 1. + ("Task 9", -max(1, 100-i)), + ("Task 10", -i)]) + time.sleep(0.5 if params["fast_mode"] else 2) + + def yesno_demo(self, with_help=True): + if not with_help: + # Simple version, without the "Help" button (the return value is + # True or False): + return d.Yesno("\nDo you like this demo?", yes_label="Yes, I do", + no_label="No, I do not", height=10, width=40, + title="An Important Question") + + # 'yesno' dialog box with custom Yes, No and Help buttons + while True: + reply = d.Yesnohelp("\nDo you like this demo?", + yes_label="Yes, I do", no_label="No, I do not", + help_label="Please help me!", height=10, + width=60, title="An Important Question") + if reply == "yes": + return True + elif reply == "no": + return False + elif reply == "help": + d.msgbox("""\ +I can hear your cry for help, and would really like to help you. However, I \ +am afraid there is not much I can do for you here; you will have to decide \ +for yourself on this matter. + +Keep in mind that you can always rely on me. \ +You have all my support, be brave!""", + height=15, width=60, + title="From Your Faithful Servant") + else: + assert False, "Unexpected reply from MyDialog.Yesnohelp(): " \ + + repr(reply) + + def msgbox_demo(self, answer): + if answer: + msg = "Excellent! Press OK to see its source code (or another " \ + "file if not in the correct directory)." + else: + msg = "Well, feel free to send your complaints to /dev/null!\n\n" \ + "Sincerely yours, etc." + + d.msgbox(msg, width=50) + + def textbox_demo(self): + # Better use the absolute path for displaying in the dialog title + filepath = os.path.abspath(__file__) + code = d.textbox(filepath, width=76, + title="Contents of {0}".format(filepath), + extra_button=True, extra_label="Stop it now!") + + if code == "extra": + d.msgbox("Your wish is my command, Master.", width=40, + title="Exiting") + sys.exit(0) + + def inputbox_demo(self): + code, answer = d.inputbox("What's your name?", init="Snow White") + return answer + + def inputbox_demo_with_help(self): + init_str = "Snow White" + while True: + code, answer = d.inputbox("What's your name?", init=init_str, + title="'inputbox' demo", help_button=True) + + if code == "help": + d.msgbox("Help from the 'inputbox' demo. The string entered " + "so far is {0!r}.".format(answer), + title="'inputbox' demo") + init_str = answer + else: + break + + return answer + + def form_demo(self): + elements = [ + ("Size (cm)", 1, 1, "175", 1, 20, 4, 3), + ("Weight (kg)", 2, 1, "85", 2, 20, 4, 3), + ("City", 3, 1, "Groboule-les-Bains", 3, 20, 15, 25), + ("State", 4, 1, "Some Lost Place", 4, 20, 15, 25), + ("Country", 5, 1, "Nowhereland", 5, 20, 15, 20), + ("My", 6, 1, "I hereby declare that, upon leaving this " + "world, all", 6, 20, 0, 0), + ("Very", 7, 1, "my fortune shall be transferred to Florent " + "Rougon's", 7, 20, 0, 0), + ("Last", 8, 1, "bank account number 000 4237 4587 32454/78 at " + "Banque", 8, 20, 0, 0), + ("Will", 9, 1, "Cantonale Vaudoise, Lausanne, Switzerland.", + 9, 20, 0, 0) ] + + code, fields = d.form("Please fill in some personal information:", + elements, width=77) + return fields + + def form_demo_with_help(self, item_help=True): + # This function is slightly complex because it provides help support + # with 'help_status=True', and optionally also with 'item_help=True' + # together with 'help_tags=True'. For a very simple version (without + # any help support), see form_demo() above. + minver_for_helptags = "1.2-20130902" + + if item_help: + if self.dialog_version_check(minver_for_helptags): + complement = """'item_help=True' is also used in conjunction \ +with 'help_tags=True' in order to display per-item help at the bottom of the \ +widget.""" + else: + item_help = False + complement = """'item_help=True' is not used, because to make \ +it consistent with the 'item_help=False' case, dialog {min} or later is \ +required (for the --help-tags option); however, it appears that you are using \ +version {used}.""".format(min=minver_for_helptags, + used=d.cached_backend_version) + else: + complement = """'item_help=True' is not used, because it has \ +been disabled; therefore, there is no per-item help at the bottom of the \ +widget.""" + + text = """\ +This is a demo for the 'form' widget, which is similar to 'mixedform' but \ +a bit simpler in that it has no notion of field type (to hide contents such \ +as passwords). + +This demo uses 'help_button=True' to provide a Help button \ +and 'help_status=True' to allow redisplaying the widget in the same state \ +when leaving the help dialog. {complement}""".format(complement=complement) + + elements = [ ("Fruit", 1, 8, "mirabelle plum", 1, 20, 18, 30), + ("Color", 2, 8, "yellowish", 2, 20, 18, 30), + ("Flavor", 3, 8, "sweet when ripe", 3, 20, 18, 30), + ("Origin", 4, 8, "Lorraine", 4, 20, 18, 30) ] + + more_kwargs = {} + + if item_help: + more_kwargs.update({ "item_help": True, + "help_tags": True }) + elements = [ list(l) + [ "Help text for item {0}".format(i+1) ] + for i, l in enumerate(elements) ] + + while True: + code, t = d.form(text, elements, height=20, width=65, + title="'form' demo with help facilities", + help_button=True, help_status=True, **more_kwargs) + + if code == "help": + label, status, elements = t + d.msgbox("You asked for help concerning the field labelled " + "{0!r}.".format(label), width=50) + else: + # 't' contains the list of items as filled by the user + break + + answers = '\n'.join(t) + d.msgbox("Your answers:\n\n{0}".format(indent(answers, " ")), + width=0, height=0, + title="'form' demo with help facilities", no_collapse=True) + return t + + def mixedform_demo(self): + HIDDEN = 0x1 + READ_ONLY = 0x2 + + elements = [ + ("Size (cm)", 1, 1, "175", 1, 20, 4, 3, 0x0), + ("Weight (kg)", 2, 1, "85", 2, 20, 4, 3, 0x0), + ("City", 3, 1, "Groboule-les-Bains", 3, 20, 15, 25, 0x0), + ("State", 4, 1, "Some Lost Place", 4, 20, 15, 25, 0x0), + ("Country", 5, 1, "Nowhereland", 5, 20, 15, 20, 0x0), + ("My", 6, 1, "I hereby declare that, upon leaving this " + "world, all", 6, 20, 54, 0, READ_ONLY), + ("Very", 7, 1, "my fortune shall be transferred to Florent " + "Rougon's", 7, 20, 54, 0, READ_ONLY), + ("Last", 8, 1, "bank account number 000 4237 4587 32454/78 at " + "Banque", 8, 20, 54, 0, READ_ONLY), + ("Will", 9, 1, "Cantonale Vaudoise, Lausanne, Switzerland.", + 9, 20, 54, 0, READ_ONLY), + ("Read-only field...", 10, 1, "... that doesn't go into the " + "output list", 10, 20, 0, 0, 0x0), + ("\/3r`/ 53kri7 (0d3", 11, 1, "", 11, 20, 15, 20, HIDDEN) ] + + code, fields = d.mixedform( + "Please fill in some personal information:", elements, width=77) + + return fields + + def passwordform_demo(self): + elements = [ + ("Secret field 1", 1, 1, "", 1, 20, 12, 0), + ("Secret field 2", 2, 1, "", 2, 20, 12, 0), + ("Secret field 3", 3, 1, "Providing a non-empty initial content " + "(like this) for an invisible field can be very confusing!", + 3, 20, 30, 160)] + + code, fields = d.passwordform( + "Please enter all your secret passwords.\n\nOn purpose here, " + "nothing is echoed when you type in the passwords. If you want " + "asterisks, use the 'insecure' keyword argument as in the " + "passwordbox demo.", + elements, width=77, height=15, title="Passwordform demo") + + d.msgbox("Secret password 1: '%s'\n" + "Secret password 2: '%s'\n" + "Secret password 3: '%s'" % tuple(fields), + width=60, height=20, title="The Whole Truth Now Revealed") + + return fields + + def menu_demo(self, name, city, state, country, size, weight, secret_code, + last_will1, last_will2, last_will3, last_will4): + text = """\ +Hello, %s from %s, %s, %s, %s cm, %s kg. +Thank you for giving us your Very Secret Code '%s'. + +As expressly stated in the previous form, your Last Will reads: "%s" + +All that was very interesting, thank you. However, in order to know you \ +better and provide you with the best possible customer service, we would \ +still need to know your favorite day of the week. Please indicate your \ +preference below.""" \ + % (name, city, state, country, size, weight, secret_code, + ' '.join([last_will1, last_will2, last_will3, last_will4])) + + code, tag = d.menu(text, height=23, width=76, + choices=[("Monday", "Being the first day of the week..."), + ("Tuesday", "Comes after Monday"), + ("Wednesday", "Before Thursday day"), + ("Thursday", "Itself after Wednesday"), + ("Friday", "The best day of all"), + ("Saturday", "Well, I've had enough, thanks"), + ("Sunday", "Let's rest a little bit")]) + + return tag + + def menu_demo_with_help(self): + text = """Sample 'menu' dialog box with help_button=True and \ +item_help=True.""" + + while True: + code, tag = d.menu(text, height=16, width=60, + choices=[("Tag 1", "Item 1", "Help text for item 1"), + ("Tag 2", "Item 2", "Help text for item 2"), + ("Tag 3", "Item 3", "Help text for item 3"), + ("Tag 4", "Item 4", "Help text for item 4"), + ("Tag 5", "Item 5", "Help text for item 5"), + ("Tag 6", "Item 6", "Help text for item 6"), + ("Tag 7", "Item 7", "Help text for item 7"), + ("Tag 8", "Item 8", "Help text for item 8")], + title="A menu with help facilities", + help_button=True, item_help=True, help_tags=True) + + if code == "help": + d.msgbox("You asked for help concerning the item identified by " + "tag {0!r}.".format(tag), height=8, width=40) + else: + break + + d.msgbox("You have chosen the item identified by tag " + "{0!r}.".format(tag), height=8, width=40) + + def checklist_demo(self): + # We could put non-empty items here (not only the tag for each entry) + code, tags = d.checklist(text="What sandwich toppings do you like?", + height=15, width=54, list_height=7, + choices=[("Catsup", "", False), + ("Mustard", "", False), + ("Pesto", "", False), + ("Mayonnaise", "", True), + ("Horse radish","", True), + ("Sun-dried tomatoes", "", True)], + title="Do you prefer ham or spam?", + backtitle="And now, for something " + "completely different...") + return tags + + SAMPLE_DATA_FOR_BUILDLIST_AND_CHECKLIST = [ + ("Tag 1", "Item 1", True, "Help text for item 1"), + ("Tag 2", "Item 2", False, "Help text for item 2"), + ("Tag 3", "Item 3", False, "Help text for item 3"), + ("Tag 4", "Item 4", True, "Help text for item 4"), + ("Tag 5", "Item 5", True, "Help text for item 5"), + ("Tag 6", "Item 6", False, "Help text for item 6"), + ("Tag 7", "Item 7", True, "Help text for item 7"), + ("Tag 8", "Item 8", False, "Help text for item 8") ] + + def checklist_demo_with_help(self): + text = """Sample 'checklist' dialog box with help_button=True, \ +item_help=True and help_status=True.""" + choices = self.SAMPLE_DATA_FOR_BUILDLIST_AND_CHECKLIST + + while True: + code, t = d.checklist(text, height=0, width=0, list_height=0, + choices=choices, + title="A checklist with help facilities", + help_button=True, item_help=True, + help_tags=True, help_status=True) + if code == "help": + tag, selected_tags, choices = t + d.msgbox("You asked for help concerning the item identified " + "by tag {0!r}.".format(tag), height=7, width=60) + else: + # 't' contains the list of tags corresponding to checked items + break + + s = '\n'.join(t) + d.msgbox("The tags corresponding to checked items are:\n\n" + "{0}".format(indent(s, " ")), height=15, width=60, + title="'checklist' demo with help facilities", + no_collapse=True) + + def radiolist_demo(self): + choices = [ + ("Hamburger", "2 slices of bread, a steak...", False), + ("Hotdog", "doesn't bite any more", False), + ("Burrito", "no se lo que es", False), + ("Doener", "Huh?", False), + ("Falafel", "Erm...", False), + ("Bagel", "Of course!", False), + ("Big Mac", "Ah, that's easy!", True), + ("Whopper", "Erm, sorry", False), + ("Quarter Pounder", 'called "le Big Mac" in France', False), + ("Peanut Butter and Jelly", "Well, that's your own business...", + False), + ("Grilled cheese", "And nothing more?", False) ] + + while True: + code, t = d.radiolist( + "What's your favorite kind of sandwich?", width=68, + choices=choices, help_button=True, help_status=True) + + if code == "help": + # Prepare to redisplay the radiolist in the same state as it + # was before the user pressed the Help button. + tag, selected, choices = t + d.msgbox("You asked for help about something called {0!r}. " + "Sorry, but I am quite incompetent in this matter." + .format(tag)) + else: + # 't' is the chosen tag + break + + return t + + def rangebox_demo(self): + nb = 10 # initial value + + while True: + code, nb = d.rangebox("""\ +How many Microsoft(TM) engineers are needed to prepare such a sandwich? + +You can use the Up and Down arrows, Page Up and Page Down, Home and End keys \ +to change the value; you may also use the Tab key, Left and Right arrows \ +and any of the 0-9 keys to change a digit of the value.""", + min=1, max=20, init=nb, + extra_button=True, extra_label="Joker") + if code == "ok": + break + elif code == "extra": + d.msgbox("Well, {0} may be enough. Or not, depending on the " + "phase of the moon...".format(nb)) + else: + assert False, "Unexpected Dialog exit code: {0!r}".format(code) + + return nb + + def buildlist_demo(self): + items0 = [("A Monty Python DVD", False), + ("A Monty Python script", False), + ('A DVD of "Barry Lyndon" by Stanley Kubrick', False), + ('A DVD of "The Good, the Bad and the Ugly" by Sergio Leone', + False), + ('A DVD of "The Trial" by Orson Welles', False), + ('The Trial, by Franz Kafka', False), + ('Animal Farm, by George Orwell', False), + ('Notre-Dame de Paris, by Victor Hugo', False), + ('Les Misérables, by Victor Hugo', False), + ('Le Lys dans la Vallée, by Honoré de Balzac', False), + ('Les Rois Maudits, by Maurice Druon', False), + ('A Georges Brassens CD', False), + ("A book of Georges Brassens' songs", False), + ('A Nina Simone CD', False), + ('Javier Vazquez y su Salsa - La Verdad', False), + ('The last Justin Bieber album', False), + ('A printed copy of the Linux kernel source code', False), + ('A CD player', False), + ('A DVD player', False), + ('An MP3 player', False)] + + # Use the name as tag, item string and item-help string; the item-help + # will be useful for long names because it is displayed in a place + # that is large enough to avoid truncation. If not using + # item_help=True, then the last element of eash tuple must be omitted. + items = [ (tag, tag, status, tag) for (tag, status) in items0 ] + + text = """If you were stranded on a desert island, what would you \ +take? + +Press the space bar to toggle the status of an item between selected (on \ +the left) and unselected (on the right). You can use the TAB key or \ +^ and $ to change the focus between the different parts of the widget. + +(this widget is called with item_help=True and visit_items=True)""" + + code, l = d.buildlist(text, items=items, visit_items=True, + item_help=True, + title="A simple 'buildlist' demo") + return l + + def buildlist_demo_with_help(self): + text = """Sample 'buildlist' dialog box with help_button=True, \ +item_help=True, help_status=True, and visit_items=False. + +Keys: SPACE select or deselect the highlighted item, i.e., + move it between the left and right lists + ^ move the focus to the left list + $ move the focus to the right list + TAB move focus + ENTER press the focused button""" + items = self.SAMPLE_DATA_FOR_BUILDLIST_AND_CHECKLIST + + while True: + code, t = d.buildlist(text, height=0, width=0, list_height=0, + items=items, + title="A 'buildlist' with help facilities", + help_button=True, item_help=True, + help_tags=True, help_status=True, + no_collapse=True) + if code == "help": + tag, selected_tags, items = t + d.msgbox("You asked for help concerning the item identified " + "by tag {0!r}.".format(tag), height=7, width=60) + else: + # 't' contains the list of tags corresponding to selected items + break + + s = '\n'.join(t) + d.msgbox("The tags corresponding to selected items are:\n\n" + "{0}".format(indent(s, " ")), height=15, width=60, + title="'buildlist' demo with help facilities", + no_collapse=True) + + def calendar_demo(self): + code, date = d.calendar("When do you think Georg Cantor was born?") + return date + + def calendar_demo_with_help(self): + # Start with the current date + day, month, year = -1, -1, -1 + + while True: + code, date = d.calendar("When do you think Georg Cantor was born?", + day=day, month=month, year=year, + title="'calendar' demo", + help_button=True) + if code == "help": + day, month, year = date + d.msgbox("Help dialog for date {0:04d}-{1:02d}-{2:02d}.".format( + year, month, day), title="'calendar' demo") + else: + break + + return date + + def comment_on_Cantor_date_of_birth(self, day, month, year): + complement = """\ +For your information, Georg Ferdinand Ludwig Philip Cantor, a great \ +mathematician, was born on March 3, 1845 in Saint Petersburg, and died on \ +January 6, 1918. Among other things, Georg Cantor laid the foundation for \ +the set theory (which is at the basis of most modern mathematics) \ +and was the first person to give a rigorous definition of real numbers.""" + + if (year, month, day) == (1845, 3, 3): + return "Spot-on! I'm impressed." + elif year == 1845: + return "You guessed the year right. {0}".format(complement) + elif abs(year-1845) < 30: + return "Not too far. {0}".format(complement) + else: + return "Well, not quite. {0}".format(complement) + + def timebox_demo(self): + # Get the current time (to display initially in the timebox) + tm = time.localtime() + init_hour, init_min, init_sec = tm.tm_hour, tm.tm_min, tm.tm_sec + # tm.tm_sec can be 60 or even 61 according to the doc of the time module! + init_sec = min(59, init_sec) + + code, (hour, minute, second) = d.timebox( + "And at what time, if I may ask?", + hour=init_hour, minute=init_min, second=init_sec) + + return (hour, minute, second) + + def passwordbox_demo(self): + # 'insecure' keyword argument only asks dialog to echo asterisks when + # the user types characters. Not *that* bad. + code, password = d.passwordbox("What is your root password, " + "so that I can crack your system " + "right now?", insecure=True) + return password + + def scrollbox_demo(self, name, favorite_day, toppings, sandwich, + nb_engineers, desert_island_stuff, date, time_, + password): + tw71 = textwrap.TextWrapper(width=71, break_long_words=False, + break_on_hyphens=True) + + if nb_engineers is not None: + sandwich_comment = " (the preparation of which requires, " \ + "according to you, {nb_engineers} MS {engineers})".format( + nb_engineers=nb_engineers, + engineers="engineers" if nb_engineers != 1 else "engineer") + else: + sandwich_comment = "" + + sandwich_report = "Favorite sandwich: {sandwich}{comment}".format( + sandwich=sandwich, comment=sandwich_comment) + + if desert_island_stuff is None: + # The widget was not available, the user didn't see anything. + desert_island_string = "" + else: + if len(desert_island_stuff) == 0: + desert_things = " nothing!" + else: + desert_things = "\n\n " + "\n ".join(desert_island_stuff) + + desert_island_string = \ + "\nOn a desert island, you would take:{0}\n".format( + desert_things) + + day, month, year = date + hour, minute, second = time_ + msg = """\ +Here are some vital statistics about you: + +Name: {name} +Favorite day of the week: {favday} +Favorite sandwich toppings:{toppings} +{sandwich_report} +{desert_island_string} +Your answer about Georg Cantor's date of birth: \ +{year:04d}-{month:02d}-{day:02d} +(at precisely {hour:02d}:{min:02d}:{sec:02d}!) + +{comment} + +Your root password is: ************************** (looks good!)""".format( + name=name, favday=favorite_day, + toppings="\n ".join([''] + toppings), + sandwich_report=tw71.fill(sandwich_report), + desert_island_string=desert_island_string, + year=year, month=month, day=day, + hour=hour, min=minute, sec=second, + comment=tw71.fill( + self.comment_on_Cantor_date_of_birth(day, month, year))) + d.scrollbox(msg, height=20, width=75, title="Great Report of the Year") + + TREEVIEW_BASE_TEXT = """\ +This is an example of the 'treeview' widget{options}. Nodes are labelled in a \ +way that reflects their position in the tree, but this is not a requirement: \ +you are free to name them the way you like. + +Node 0 is the root node. It has 3 children tagged 0.1, 0.2 and 0.3. \ +You should now select a node with the space bar.""" + + def treeview_demo(self): + code, tag = d.treeview(self.TREEVIEW_BASE_TEXT.format(options=""), + nodes=[ ("0", "node 0", False, 0), + ("0.1", "node 0.1", False, 1), + ("0.2", "node 0.2", False, 1), + ("0.2.1", "node 0.2.1", False, 2), + ("0.2.1.1", "node 0.2.1.1", True, 3), + ("0.2.2", "node 0.2.2", False, 2), + ("0.3", "node 0.3", False, 1), + ("0.3.1", "node 0.3.1", False, 2), + ("0.3.2", "node 0.3.2", False, 2) ], + title="'treeview' demo") + + d.msgbox("You selected the node tagged {0!r}.".format(tag), + title="treeview demo") + return tag + + def treeview_demo_with_help(self): + text = self.TREEVIEW_BASE_TEXT.format( + options=" with help_button=True, item_help=True and " + "help_status=True") + + nodes = [ ("0", "node 0", False, 0, "Help text 1"), + ("0.1", "node 0.1", False, 1, "Help text 2"), + ("0.2", "node 0.2", False, 1, "Help text 3"), + ("0.2.1", "node 0.2.1", False, 2, "Help text 4"), + ("0.2.1.1", "node 0.2.1.1", True, 3, "Help text 5"), + ("0.2.2", "node 0.2.2", False, 2, "Help text 6"), + ("0.3", "node 0.3", False, 1, "Help text 7"), + ("0.3.1", "node 0.3.1", False, 2, "Help text 8"), + ("0.3.2", "node 0.3.2", False, 2, "Help text 9") ] + + while True: + code, t = d.treeview(text, nodes=nodes, + title="'treeview' demo with help facilities", + help_button=True, item_help=True, + help_tags=True, help_status=True) + + if code == "help": + # Prepare to redisplay the treeview in the same state as it + # was before the user pressed the Help button. + tag, selected_tag, nodes = t + d.msgbox("You asked for help about the node with tag {0!r}." + .format(tag)) + else: + # 't' is the chosen tag + break + + d.msgbox("You selected the node tagged {0!r}.".format(t), + title="'treeview' demo") + return t + + def editbox_demo(self, filepath): + if os.path.isfile(filepath): + code, text = d.editbox(filepath, 20, 60, + title="A Cheap Text Editor") + d.scrollbox(text, title="Resulting text") + else: + d.msgbox("Skipping the first part of the 'editbox' demo, " + "as '{0}' can't be found.".format(filepath), + title="'msgbox' demo") + + l = ["In the previous dialog, the initial contents was", + "explicitly written to a file. With Dialog.editbox_str(),", + "you can provide it as a string and pythondialog will", + "automatically create and delete a temporary file for you", + "holding this text for dialog.\n"] + \ + [ "This is line {0} of a boring sample text.".format(i+1) + for i in range(100) ] + code, text = d.editbox_str('\n'.join(l), 0, 0, + title="A Cheap Text Editor") + d.scrollbox(text, title="Resulting text") + + def inputmenu_demo(self): + choices = [ ("1st_tag", "Item 1 text"), + ("2nd_tag", "Item 2 text"), + ("3rd_tag", "Item 3 text") ] + + for i in range(4, 21): + choices.append(("%dth_tag" % i, "Item %d text" % i)) + + while True: + code, tag, new_item_text = d.inputmenu( + "Demonstration of 'inputmenu'. Any single item can be either " + "accepted as is, or renamed.", + height=0, width=60, menu_height=10, choices=choices, + help_button=True, title="'inputmenu' demo") + + if code == "help": + d.msgbox("You asked for help about the item with tag {0!r}." + .format(tag)) + continue + elif code == "accepted": + text = "The item corresponding to tag {0!r} was " \ + "accepted.".format(tag) + elif code == "renamed": + text = "The item corresponding to tag {0!r} was renamed to " \ + "{1!r}.".format(tag, new_item_text) + else: + text = "Unexpected exit code from 'inputmenu': {0!r}.\n\n" \ + "It may be a bug. Please report.".format(code) + + break + + d.msgbox(text, width=60, title="Outcome of the 'inputmenu' demo") + + # Help strings used in several places + FSELECT_HELP = """\ +Hint: the complete file path must be entered in the bottom field. One \ +convenient way to achieve this is to use the SPACE bar when the desired file \ +is highlighted in the top-right list. + +As usual, you can use the TAB and arrow keys to move between controls. If in \ +the bottom field, the SPACE key provides auto-completion.""" + + # The following help text was initially meant to be used for several + # widgets (at least progressbox and tailbox). Currently (dialog version + # 1.2-20130902), "dialog --tailbox" doesn't seem to work with FIFOs, so the + # "flexibility" of the help text is unused (another text is used when + # demonstrating --tailbox). However, this might change in the future... + def FIFO_HELP(self, widget): + return """\ +For demos based on the {widget} widget, you may use a FIFO, also called \ +"named pipe". This is a special kind of file, to which you will be able to \ +easily append data. With the {widget} widget, you can see the data stream \ +flow in real time. + +To create a FIFO, you can use the commmand mkfifo(1), like this: + + % mkfifo /tmp/my_shiny_new_fifo + +Then, you can cat(1) data to the FIFO like this: + + % cat >>/tmp/my_shiny_new_fifo + First line of text + Second line of text + ... + +You can end the input to cat(1) by typing Ctrl-D at the beginning of a \ +line.""".format(widget=widget) + + def fselect_demo(self, widget, init_path=None, allow_FIFOs=False, **kwargs): + init_path = init_path or params["home_dir"] + # Make sure the directory we chose ends with os.sep so that dialog + # shows its contents right away + if not init_path.endswith(os.sep): + init_path += os.sep + + while True: + # We want to let the user quit this particular dialog with Cancel + # without having to bother choosing a file, therefore we use the + # original fselect() from dialog.Dialog and interpret the return + # code manually. (By default, the MyDialog class defined in this + # file intercepts the CANCEL and ESC exit codes and causes them to + # spawn the "confirm quit" dialog.) + code, path = self.Dialog_instance.fselect( + init_path, height=10, width=60, help_button=True, **kwargs) + + # Display the "confirm quit" dialog if the user pressed ESC. + if not d.check_exit_request(code, ignore_Cancel=True): + continue + + # Provide an easy way out... + if code == d.CANCEL: + path = None + break + elif code == "help": + d.msgbox("Help about {0!r} from the 'fselect' dialog.".format( + path), title="'fselect' demo") + init_path = path + elif code == d.OK: + # Of course, one can use os.path.isfile(path) here, but we want + # to allow regular files *and* possibly FIFOs. Since there is + # no os.path.is*** convenience function for FIFOs, let's go + # with os.stat. + try: + mode = os.stat(path)[stat.ST_MODE] + except os.error as e: + d.msgbox("Error: {0}".format(e)) + continue + + # Accept FIFOs only if allow_FIFOs is True + if stat.S_ISREG(mode) or (allow_FIFOs and stat.S_ISFIFO(mode)): + break + else: + if allow_FIFOs: + help_text = """\ +You are expected to select a *file* here (possibly a FIFO), or press the \ +Cancel button.\n\n%s + +For your convenience, I will reproduce the FIFO help text here:\n\n%s""" \ + % (self.FSELECT_HELP, self.FIFO_HELP(widget)) + else: + help_text = """\ +You are expected to select a regular *file* here, or press the \ +Cancel button.\n\n%s""" % (self.FSELECT_HELP,) + + d.msgbox(help_text, width=72, height=20) + else: + d.msgbox("Unexpected exit code from Dialog.fselect(): {0}.\n\n" + "It may be a bug. Please report.".format(code)) + return path + + def dselect_demo(self, init_dir=None): + init_dir = init_dir or params["home_dir"] + # Make sure the directory we chose ends with os.sep so that dialog + # shows its contents right away + if not init_dir.endswith(os.sep): + init_dir += os.sep + + while True: + code, path = d.dselect(init_dir, 10, 50, + title="Please choose a directory", + help_button=True) + if code == "help": + d.msgbox("Help about {0!r} from the 'dselect' dialog.".format( + path), title="'dselect' demo") + init_dir = path + # When Python 3.2 is old enough, we'll be able to check if + # path.endswith(os.sep) and remove the trailing os.sep if this + # does not change the path according to os.path.samefile(). + elif not os.path.isdir(path): + d.msgbox("Hmm. It seems that {0!r} is not a directory".format( + path), title="'dselect' demo") + else: + break + + d.msgbox("Directory '%s' thanks you for choosing him." % path) + return path + + def tailbox_demo(self, height=22, width=78): + widget = "tailbox" + + # First, ask the user for a file. + # Strangely (dialog version 1.2-20130902 bug?), "dialog --tailbox" + # doesn't work with FIFOs: "Error moving file pointer in last_lines()" + # and DIALOG_ERROR exit status. + path = self.fselect_demo(widget, allow_FIFOs=False, + title="Please choose a file to be shown as " + "with 'tail -f'") + # Now, the tailbox + if path is None: + # User chose to abort + return + else: + d.tailbox(path, height, width, title="Tailbox example") + + def pause_demo(self, seconds): + d.pause("""\ +Ugh, sorry. pythondialog is still in development, and its advanced circuitry \ +detected internal error number 0x666. That's a pretty nasty one, you know. + +I am embarrassed. I don't know how to tell you, but we are going to have to \ +reboot. In %d seconds. + +Fasten your seatbelt...""" % seconds, height=18, seconds=seconds) + + +def process_command_line(): + global params + + try: + opts, args = getopt.getopt(sys.argv[1:], "ftE", + ["test-suite", + "fast", + "debug", + "debug-file=", + "debug-expand-file-opt", + "help", + "version"]) + except getopt.GetoptError: + print(usage, file=sys.stderr) + return ("exit", 1) + + # Let's start with the options that don't require any non-option argument + # to be present + for option, value in opts: + if option == "--help": + print(usage) + return ("exit", 0) + elif option == "--version": + print("%s %s\n%s" % (progname, progversion, version_blurb)) + return ("exit", 0) + + # Now, require a correct invocation. + if len(args) != 0: + print(usage, file=sys.stderr) + return ("exit", 1) + + # Default values for parameters + params = { "fast_mode": False, + "testsuite_mode": False, + "debug": False, + "debug_filename": default_debug_filename, + "debug_expand_file_opt": False } + + # Get the home directory, if any, and store it in params (often useful). + root_dir = os.sep # This is OK for Unix-like systems + params["home_dir"] = os.getenv("HOME", root_dir) + + # General option processing + for option, value in opts: + if option in ("-t", "--test-suite"): + params["testsuite_mode"] = True + # --test-suite implies --fast + params["fast_mode"] = True + elif option in ("-f", "--fast"): + params["fast_mode"] = True + elif option == "--debug": + params["debug"] = True + elif option == "--debug-file": + params["debug_filename"] = value + elif option in ("-E", "--debug-expand-file-opt"): + params["debug_expand_file_opt"] = True + else: + # The options (such as --help) that cause immediate exit + # were already checked, and caused the function to return. + # Therefore, if we are here, it can't be due to any of these + # options. + assert False, "Unexpected option received from the " \ + "getopt module: '%s'" % option + + return ("continue", None) + + +def main(): + """This demo shows the main features of pythondialog.""" + locale.setlocale(locale.LC_ALL, '') + + what_to_do, code = process_command_line() + if what_to_do == "exit": + sys.exit(code) + + try: + app = MyApp() + app.run() + except dialog.error as exc_instance: + # The error that causes a PythonDialogErrorBeforeExecInChildProcess to + # be raised happens in the child process used to run the dialog-like + # program, and the corresponding traceback is printed right away from + # that child process when the error is encountered. Therefore, don't + # print a second, not very useful traceback for this kind of exception. + if not isinstance(exc_instance, + dialog.PythonDialogErrorBeforeExecInChildProcess): + print(traceback.format_exc(), file=sys.stderr) + + print("Error (see above for a traceback):\n\n{0}".format( + exc_instance), file=sys.stderr) + sys.exit(1) + + sys.exit(0) + + +if __name__ == "__main__": main() diff --git a/pythondialog/examples/simple_example.py b/pythondialog/examples/simple_example.py @@ -0,0 +1,108 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- + +# simple_example.py --- Short and straightforward example using pythondialog +# Copyright (C) 2013 Florent Rougon +# +# This program is in the public domain. + +import sys, locale +from dialog import Dialog + +# This is almost always a good thing to do at the beginning of your programs. +locale.setlocale(locale.LC_ALL, '') + +# Initialize a dialog.Dialog instance +d = Dialog(dialog="dialog") +d.set_background_title("A Simple Example") + + +# ***************************************************************************** +# * 'msgbox' example * +# ***************************************************************************** +d.msgbox("""\ +This is a very simple example of a program using pythondialog. + +Contrary to what is done in demo.py, the Dialog exit code for the Escape key \ +is not checked after every call, therefore it is not so easy to exit from \ +this program as it is for the demo. The goal here is to show basic \ +pythondialog usage in its simplest form. + +With not too old versions of dialog, the size of dialog boxes is \ +automatically computed when one passes width=0 and height=0 to the \ +widget call. This is the method used here in most cases.""", + width=0, height=0, title="'msgbox' example") + + +# ***************************************************************************** +# * 'yesno' example * +# ***************************************************************************** + +# The 'no_collapse=True' used in the following call tells dialog not to replace +# multiple contiguous spaces in the text string with a single space. +code = d.yesno("""\ +The 'yesno' widget allows one to display a text with two buttons beneath, \ +which by default are labelled "Yes" and "No". + +The return value is not simply True or False: for consistency with \ +dialog and the other widgets, the return code allows to distinguish \ +between: + + OK/Yes Dialog.OK (equal to the string "ok") + Cancel/No Dialog.CANCEL (equal to the string "cancel") + <Escape> Dialog.ESC when the Escape key is pressed + Help Dialog.HELP when help_button=True was passed and the + Help button is pressed (only for 'menu' in + pythondialog 2.x) + Extra Dialog.EXTRA when extra_button=True was passed and the + Extra button is pressed + +The DIALOG_ERROR exit status of dialog has no equivalent in this list, \ +because pythondialog translates it into an exception.""", + width=0, height=0, title="'yesno' example", no_collapse=True, + help_button=True) + +if code == d.OK: + msg = "You chose the 'OK/Yes' button in the previous dialog." +elif code == d.CANCEL: + msg = "You chose the 'Cancel/No' button in the previous dialog." +elif code == d.ESC: + msg = "You pressed the Escape key in the previous dialog." +elif code == d.HELP: + msg = "You chose the 'Help' button in the previous dialog." +else: + msg = "Unexpected exit code from d.yesno(). Please report a bug." + +d.msgbox(msg, width=50, height=7) + + +# ***************************************************************************** +# * 'inputbox' example * +# ***************************************************************************** +code, user_input = d.inputbox("""\ +The 'inputbox' widget can be used to read input (as a string) from the user. \ +You can test it now:""", + init="Initial contents", + width=0, height=0, title="'inputbox' example", + help_button=True, extra_button=True, + extra_label="Cool button") + +if code == d.OK: + msg = "Your input in the previous dialog was '{0}'.".format(user_input) +elif code == d.CANCEL: + msg = "You chose the 'Cancel' button in the previous dialog." +elif code == d.ESC: + msg = "You pressed the Escape key in the previous dialog." +elif code == d.HELP: + msg = "You chose the 'Help' button with input '{0}' in the previous " \ + "dialog.".format(user_input) +elif code == d.EXTRA: + msg = 'You chose the Extra button ("Cool button") with input \'{0}\' ' \ + "in the previous dialog.".format(user_input) +else: + msg = "Unexpected exit code from d.inputbox(). Please report a bug." + +d.msgbox("{0}\n\nThis little sample program is now finished. Bye bye!".format( + msg), width=0, height=0, title="Bye bye!") + +sys.exit(0) diff --git a/pythondialog/examples/with-autowidgetsize/demo.py b/pythondialog/examples/with-autowidgetsize/demo.py @@ -0,0 +1,1708 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- + +# demo.py --- Demonstration program and cheap test suite for pythondialog +# +# Copyright (C) 2002-2010, 2013-2016 Florent Rougon +# Copyright (C) 2000 Robb Shecter, Sultanbek Tezadov +# +# This program is in the public domain. + +"""Demonstration program for pythondialog. + +This is a program demonstrating most of the possibilities offered by +the pythondialog module (which is itself a Python interface to the +well-known dialog utility, or any other program compatible with +dialog). + +Executive summary +----------------- + +If you are looking for a very simple example of pythondialog usage, +short and straightforward, please refer to simple_example.py. The +file you are now reading serves more as a demonstration of what can +be done with pythondialog and as a cheap test suite than as a first +time tutorial. However, it can also be used to learn how to invoke +the various widgets. The following paragraphs explain what you should +keep in mind if you read it for this purpose. + + +Most of the code in the MyApp class (which defines the actual +contents of the demo) relies on a class called MyDialog implemented +here that: + + 1. wraps all widget-producing calls in a way that automatically + spawns a "confirm quit" dialog box if the user presses the + Escape key or chooses the Cancel button, and then redisplays the + original widget if the user doesn't actually want to quit; + + 2. provides a few additional dialog-related methods and convenience + wrappers. + +The handling in (1) is completely automatic, implemented with +MyDialog.__getattr__() returning decorated versions of the +widget-producing methods of dialog.Dialog. Therefore, most of the +demo can be read as if the module-level 'd' attribute were a +dialog.Dialog instance whereas it is actually a MyDialog instance. +The only meaningful difference is that MyDialog.<widget>() will never +return a CANCEL or ESC code (attributes of 'd', or more generally of +dialog.Dialog). The reason is that these return codes are +automatically handled by the MyDialog.__getattr__() machinery to +display the "confirm quit" dialog box. + +In some cases (e.g., fselect_demo()), I wanted the "Cancel" button to +perform a specific action instead of spawning the "confirm quit" +dialog box. To achieve this, the widget is invoked using +dialog.Dialog.<widget> instead of MyDialog.<widget>, and the return +code is handled in a semi-manual way. A prominent feature that needs +such special-casing is the yesno widget, because the "No" button +corresponds to the CANCEL exit code, which in general must not be +interpreted as an attempt to quit the program! + +To sum it up, you can read most of the code in the MyApp class (which +defines the actual contents of the demo) as if 'd' were a +dialog.Dialog instance. Just keep in mind that there is a little +magic behind the scenes that automatically handles the CANCEL and ESC +Dialog exit codes, which wouldn't be the case if 'd' were a +dialog.Dialog instance. For a first introduction to pythondialog with +simple stuff and absolutely no magic, please have a look at +simple_example.py. + +""" + + +import sys, os, locale, stat, time, getopt, subprocess, traceback, textwrap +import pprint +import dialog +from dialog import DialogBackendVersion + +progname = os.path.basename(sys.argv[0]) +progversion = "0.12-autowidgetsize" +version_blurb = """Demonstration program and cheap test suite for pythondialog. + +Copyright (C) 2002-2010, 2013-2016 Florent Rougon +Copyright (C) 2000 Robb Shecter, Sultanbek Tezadov + +This is free software; see the source for copying conditions. There is NO +warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.""" + +default_debug_filename = "pythondialog.debug" + +usage = """Usage: {progname} [option ...] +Demonstration program and cheap test suite for pythondialog. + +Options: + -t, --test-suite test all widgets; implies '--fast' + -f, --fast fast mode (e.g., makes the gauge demo run faster) + --debug enable logging of all dialog command lines + --debug-file=FILE where to write debug information (default: + {debug_file} in the current directory) + -E, --debug-expand-file-opt expand the '--file' options in the debug file + generated by '--debug' + --help display this message and exit + --version output version information and exit""".format( + progname=progname, debug_file=default_debug_filename) + +# Global parameters +params = {} + +# We'll use a module-level attribute 'd' ("global") to store the MyDialog +# instance that is used throughout the demo. This object could alternatively be +# passed to the MyApp constructor and stored there as a class or instance +# attribute. However, for the sake of readability, we'll simply use a global +# (d.msgbox(...) versus self.d.msgbox(...), etc.). +d = None + +tw = textwrap.TextWrapper(width=78, break_long_words=False, + break_on_hyphens=True) +from textwrap import dedent + +try: + from textwrap import indent +except ImportError: + try: + callable # Normally, should be __builtins__.callable + except NameError: + # Python 3.1 doesn't have the 'callable' builtin function. Let's + # provide ours. + def callable(f): + return hasattr(f, '__call__') + + def indent(text, prefix, predicate=None): + l = [] + + for line in text.splitlines(True): + if (callable(predicate) and predicate(line)) \ + or (not callable(predicate) and predicate) \ + or (predicate is None and line.strip()): + line = prefix + line + l.append(line) + + return ''.join(l) + + +class MyDialog: + """Wrapper class for dialog.Dialog. + + This class behaves similarly to dialog.Dialog. The differences + are that: + + 1. MyDialog wraps all widget-producing methods in a way that + automatically spawns a "confirm quit" dialog box if the user + presses the Escape key or chooses the Cancel button, and + then redisplays the original widget if the user doesn't + actually want to quit. + + 2. MyDialog provides a few additional dialog-related methods + and convenience wrappers. + + Please refer to the module docstring and to the particular + methods for more details. + + """ + def __init__(self, Dialog_instance): + self.dlg = Dialog_instance + + def check_exit_request(self, code, ignore_Cancel=False): + if code == self.CANCEL and ignore_Cancel: + # Ignore the Cancel button, i.e., don't interpret it as an exit + # request; instead, let the caller handle CANCEL himself. + return True + + if code in (self.CANCEL, self.ESC): + button_name = { self.CANCEL: "Cancel", + self.ESC: "Escape" } + msg = "You pressed {0} in the last dialog box. Do you want " \ + "to exit this demo?".format(button_name[code]) + # 'self.dlg' instead of 'self' here, because we want to use the + # original yesno() method from the Dialog class instead of the + # decorated method returned by self.__getattr__(). + if self.dlg.yesno(msg) == self.OK: + sys.exit(0) + else: # "No" button chosen, or ESC pressed + return False # in the "confirm quit" dialog + else: + return True + + def widget_loop(self, method): + """Decorator to handle eventual exit requests from a Dialog widget. + + method -- a dialog.Dialog method that returns either a Dialog + exit code, or a sequence whose first element is a + Dialog exit code (cf. the docstring of the Dialog + class in dialog.py) + + Return a wrapper function that behaves exactly like 'method', + except for the following point: + + If the Dialog exit code obtained from 'method' is CANCEL or + ESC (attributes of dialog.Dialog), a "confirm quit" dialog + is spawned; depending on the user choice, either the + program exits or 'method' is called again, with the same + arguments and same handling of the exit status. In other + words, the wrapper function builds a loop around 'method'. + + The above condition on 'method' is satisfied for all + dialog.Dialog widget-producing methods. More formally, these + are the methods defined with the @widget decorator in + dialog.py, i.e., that have an "is_widget" attribute set to + True. + + """ + # One might want to use @functools.wraps here, but since the wrapper + # function is very likely to be used only once and then + # garbage-collected, this would uselessly add a little overhead inside + # __getattr__(), where widget_loop() is called. + def wrapper(*args, **kwargs): + while True: + res = method(*args, **kwargs) + + if hasattr(method, "retval_is_code") \ + and getattr(method, "retval_is_code"): + code = res + else: + code = res[0] + + if self.check_exit_request(code): + break + return res + + return wrapper + + def __getattr__(self, name): + # This is where the "magic" of this class originates from. + # Please refer to the module and self.widget_loop() + # docstrings if you want to understand the why and the how. + obj = getattr(self.dlg, name) + if hasattr(obj, "is_widget") and getattr(obj, "is_widget"): + return self.widget_loop(obj) + else: + return obj + + def clear_screen(self): + # This program comes with ncurses + program = "clear" + + try: + p = subprocess.Popen([program], shell=False, stdout=None, + stderr=None, close_fds=True) + retcode = p.wait() + except os.error as e: + self.msgbox("Unable to execute program '%s': %s." % (program, + e.strerror), + title="Error") + return False + + if retcode > 0: + msg = "Program %s returned exit status %d." % (program, retcode) + elif retcode < 0: + msg = "Program %s was terminated by signal %d." % (program, -retcode) + else: + return True + + self.msgbox(msg) + return False + + def _Yesno(self, *args, **kwargs): + """Convenience wrapper around dialog.Dialog.yesno(). + + Return the same exit code as would return + dialog.Dialog.yesno(), except for ESC which is handled as in + the rest of the demo, i.e. make it spawn the "confirm quit" + dialog. + + """ + # self.yesno() automatically spawns the "confirm quit" dialog if ESC or + # the "No" button is pressed, because of self.__getattr__(). Therefore, + # we have to use self.dlg.yesno() here and call + # self.check_exit_request() manually. + while True: + code = self.dlg.yesno(*args, **kwargs) + # If code == self.CANCEL, it means the "No" button was chosen; + # don't interpret this as a wish to quit the program! + if self.check_exit_request(code, ignore_Cancel=True): + break + + return code + + def Yesno(self, *args, **kwargs): + """Convenience wrapper around dialog.Dialog.yesno(). + + Return True if "Yes" was chosen, False if "No" was chosen, + and handle ESC as in the rest of the demo, i.e. make it spawn + the "confirm quit" dialog. + + """ + return self._Yesno(*args, **kwargs) == self.dlg.OK + + def Yesnohelp(self, *args, **kwargs): + """Convenience wrapper around dialog.Dialog.yesno(). + + Return "yes", "no", "extra" or "help" depending on the button + that was pressed to close the dialog. ESC is handled as in + the rest of the demo, i.e. it spawns the "confirm quit" + dialog. + + """ + kwargs["help_button"] = True + code = self._Yesno(*args, **kwargs) + d = { self.dlg.OK: "yes", + self.dlg.CANCEL: "no", + self.dlg.EXTRA: "extra", + self.dlg.HELP: "help" } + + return d[code] + + +# Dummy context manager to make sure the debug file is closed on exit, be it +# normal or abnormal, and to avoid having two code paths, one for normal mode +# and one for debug mode. +class DummyContextManager: + def __enter__(self): + return self + + def __exit__(self, *exc): + return False + + +class MyApp: + def __init__(self): + # The MyDialog instance 'd' could be passed via the constructor and + # stored here as a class or instance attribute. However, for the sake + # of readability, we'll simply use a module-level attribute ("global") + # (d.msgbox(...) versus self.d.msgbox(...), etc.). + global d + # If you want to use Xdialog (pathnames are also OK for the 'dialog' + # argument), you can use: + # dialog.Dialog(dialog="Xdialog", compat="Xdialog", ...) + # + # With the 'autowidgetsize' option enabled, pythondialog's + # widget-producing methods behave as if width=0, height=0, etc. had + # been passed, except where these parameters are explicitely specified + # with different values. + self.Dialog_instance = dialog.Dialog(dialog="dialog", + autowidgetsize=True) + # See the module docstring at the top of the file to understand the + # purpose of MyDialog. + d = MyDialog(self.Dialog_instance) + backtitle = "pythondialog demo" + d.set_background_title(backtitle) + # These variables take the background title into account + self.max_lines, self.max_cols = d.maxsize(backtitle=backtitle) + self.demo_context = self.setup_debug() + # Warn if the terminal is smaller than this size + self.min_rows, self.min_cols = 24, 80 + self.term_rows, self.term_cols, self.backend_version = \ + self.get_term_size_and_backend_version() + + def setup_debug(self): + if params["debug"]: + debug_file = open(params["debug_filename"], "w") + d.setup_debug(True, file=debug_file, + expand_file_opt=params["debug_expand_file_opt"]) + return debug_file + else: + return DummyContextManager() + + def get_term_size_and_backend_version(self): + # Avoid running '<backend> --print-version' every time we need the + # version + backend_version = d.cached_backend_version + if not backend_version: + print(tw.fill( + "Unable to retrieve the version of the dialog-like backend. " + "Not running cdialog?") + "\nPress Enter to continue.", + file=sys.stderr) + input() + + term_rows, term_cols = d.maxsize(use_persistent_args=False)