From ef9fb76de9ef299fbdc8f87f1dd05bdd1eda649e Mon Sep 17 00:00:00 2001 From: "James E. Blair" Date: Sat, 20 Dec 2014 07:35:13 -0800 Subject: Initial commit --- .gitignore | 7 + COPYING | 674 ++++++++++++++++++++++++++++++++++++++++++++++++ README.rst | 63 +++++ example/ansi.ans | 45 ++++ example/demo.rst | 149 +++++++++++ example/gg.jpg | Bin 0 -> 78551 bytes presentty/__init__.py | 0 presentty/ansiparser.py | 185 +++++++++++++ presentty/client.py | 66 +++++ presentty/console.py | 292 +++++++++++++++++++++ presentty/image.py | 168 ++++++++++++ presentty/palette.py | 78 ++++++ presentty/presentty.py | 162 ++++++++++++ presentty/rst.py | 493 +++++++++++++++++++++++++++++++++++ presentty/server.py | 112 ++++++++ presentty/slide.py | 178 +++++++++++++ presentty/text.py | 81 ++++++ presentty/transition.py | 153 +++++++++++ requirements.txt | 7 + setup.cfg | 45 ++++ setup.py | 20 ++ 21 files changed, 2978 insertions(+) create mode 100644 .gitignore create mode 100644 COPYING create mode 100644 README.rst create mode 100644 example/ansi.ans create mode 100644 example/demo.rst create mode 100644 example/gg.jpg create mode 100644 presentty/__init__.py create mode 100644 presentty/ansiparser.py create mode 100644 presentty/client.py create mode 100644 presentty/console.py create mode 100644 presentty/image.py create mode 100644 presentty/palette.py create mode 100644 presentty/presentty.py create mode 100644 presentty/rst.py create mode 100644 presentty/server.py create mode 100644 presentty/slide.py create mode 100644 presentty/text.py create mode 100644 presentty/transition.py create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d1a781 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.egg +*.egg-info +*.pyc +AUTHORS +build/* +ChangeLog +venv/ diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, 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 +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If 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 convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU 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 +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..ddc18d4 --- /dev/null +++ b/README.rst @@ -0,0 +1,63 @@ +Presentty +========= + +Presentty is a console-based presentation program based on +RestructuredText. + +Installation +------------ + +It is recommended, but not required, to install presentty in to a +virtualenv. To cset one up:: + + virtualenv presentty-env + source presentty-env/bin/activate + +To install the latest version from the cheeseshop:: + + pip install presentty + +To install from a git checkout:: + + pip install . + +In order to use the cross-fade transition, you must run presentty in a +256-color capable terminal, such as gnome-terminal or xterm. + +In order for images to render as ascii art, you must have jp2a +installed. + +In order to use figlet and cowsay directives, their respective +programs must be installed. + +Usage +----- + +Presentty takes the path to the presentation file as an argument. + +To run the demonstration included with Presentty, run the following:: + + presentty example/demo.rst + +Other command line options are available; you can see them with:: + + presentty --help + +Once presentty is running, you may start an optional presenter's +console in another window with:: + + presentty-console example/demo.rst + +Once in the presenter's console, you can use the arrow keys and +[enter] to change the current slide, 't' to set the countdown timer +starting value, and [space] to start or stop the timer. + +In either the presenter's console or the main presentty window, the +left and right arrow keys or page-up and page-down navigate between +slides. + +License +------- + +Presentty is licensed under the GPLv3 or later. Please see the +COPYING file for details. diff --git a/example/ansi.ans b/example/ansi.ans new file mode 100644 index 0000000..ea6a694 --- /dev/null +++ b/example/ansi.ans @@ -0,0 +1,45 @@ + ______________ .──. .──. ______________ + +( (  \ \_____)____(_____/ /  ) ) + + \ ) `─────'   `─────' ( / + + )/   \( + + /' `\  + +O  _ _ _ ____ ___ _ _   O + +│  / \ | \ | / ___|_ _| / \ _ __| |_  │ + +│  / _ \ | \| \___ \| | / _ \ | '__| __| │ + +│  / ___ \| |\ |___) | | / ___ \| | | |_  │ + +│ /_/ \_\_| \_|____/___| /_/ \_\_| \__| │ + +│ │ + +O  May be directly included  O + + \. ./  + + )\  ,____ ____,  /( + + / ) / ____\ ____ /____ \ ( \ + +( (_________/_/ ) ( \_\_________) ) + + ~-'  `──' `──'  `-~  + + + + + + + + + + + + \ No newline at end of file diff --git a/example/demo.rst b/example/demo.rst new file mode 100644 index 0000000..bafd9ba --- /dev/null +++ b/example/demo.rst @@ -0,0 +1,149 @@ +.. This is an RST comment. + The following directives, when used at the top of the file, set default + values for all slides: + + This sets the transition style. Values are: 'dissolve', 'pan', or + 'cut'. The optional argument of 'duration' sets the duration of + the transition in seconds (0.4 seconds by default). The same + syntax may be used within a slide to override the transition for + that slide alone. + + .. transition:: dissolve + :duration: 0.4 + + This disables display of the title. Each slide must still have a + title, and it will be used by the presenter console, but it will + not be displayed on the slide. The same syntax may be used within + a slide to hide the title of that individual slide. + + .. hidetitle:: + +.. Slides are defined one at a time by starting a new top-level + section: + +Presentty +========= +.. container:: handout + + Content that is placed in a container called "handout" will not be + displayed with the slides, but will be displayed on the presenter's + console. + +Presentty is a console presentation system based where slides are +authored in reStructuredText. + +Bullet Lists +============ +It is able to display lists of items: + +* Pork + + * Eastern North Carolina + * Lexington + +* Ribs +* Brisket + + +Progressive Display +=================== +Bullet lists may include a *progressive* display: + +.. container:: progressive + + * Red Leicester + * Tilsit + * Caerphilly + + +Table +===== + +=== === + p !p +=== === + T F + F T +=== === + +Dissove Transition +================== +Transitions may be "dissolve," where one slide cross-fades into the next... + +Pan Transition +============== +.. transition:: pan + +...or "pan," where the slides appear horizontally adjacent and move +right to left... + +Cut Transition +============== +.. transition:: cut + +...or "cut," where they abruptly change from one to the next. + +Syntax Highligting +================== +Pygments is used to provide syntax highlighting of code in almost any +language: + +.. code:: python + + class UrwidTranslator(nodes.GenericNodeVisitor): + def depart_Text(self, node): + if self.stack and isinstance(self.stack[-1], 'string'): + # a comment + if self.attr: + t = (self.attr[-1], node.astext()) + else: + t = node.astext() + self.stack[-1].append(t) + visit_literal_block = visit_textelement + +Cowsay +====== +.. cowsay:: Presentty is a console-based presentation program where + reStructuredText is used to author slides. + +| If cowsay is installed, it can easily +| be used to display text. + +Figlet +====== +.. container:: handout + + Cowsay and figlet are non-standard directives and will not appear + if the RST file is rendered with a program other than presentty. + If you want to ensure that the content appears in all forms, you + may wish to run the respective commands manually and copy the + output into a quoted block in the RST file. + +.. figlet:: FIGLET + +| If figlet is installed, it can be +| used to provide large text. + +ANSI Art +======== +.. hidetitle:: +.. container:: handout + + Note that the ansi directive is specific to presentty, and so if an + RST file that includes it is rendered with another program, + included ANSI files will not appear. + +.. ansi:: ansi.ans + +Images +====== +.. container:: handout + + The standard sphinx image directive can be used to include bitmap + images (such as JPEG or PNG files) which will be automatically + converted to ANSI art for display. This feauter requires that PIL + or Pillow (Python Image Library) and jp2a be installed. + +.. image:: gg.jpg + +"Golden Gate Bridge" by Kevin Cole (CC-BY: https://flic.kr/p/7L2Rdu) diff --git a/example/gg.jpg b/example/gg.jpg new file mode 100644 index 0000000..77dcb67 Binary files /dev/null and b/example/gg.jpg differ diff --git a/presentty/__init__.py b/presentty/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/presentty/ansiparser.py b/presentty/ansiparser.py new file mode 100644 index 0000000..a780b42 --- /dev/null +++ b/presentty/ansiparser.py @@ -0,0 +1,185 @@ +# Copyright (C) 2015 James E. Blair +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import re + +import urwid + +class ANSIParser(object): + colors = [ + urwid.BLACK, + urwid.DARK_RED, + urwid.DARK_GREEN, + urwid.BROWN, + urwid.DARK_BLUE, + urwid.DARK_MAGENTA, + urwid.DARK_CYAN, + urwid.LIGHT_GRAY, + urwid.DARK_GRAY, + urwid.LIGHT_RED, + urwid.LIGHT_GREEN, + urwid.YELLOW, + urwid.LIGHT_BLUE, + urwid.LIGHT_MAGENTA, + urwid.LIGHT_CYAN, + urwid.WHITE, + ] + + colors256 = ['0', '6', '8', 'a', 'd', 'f'] + colorsgray = ['3', '7', '11', '13', '15', '19', '23', '27', '31', + '35', '38', '42', '46', '50', '52', '58', '62', '66', + '70', '74', '78', '82', '85', '89', '93'] + + def __init__(self): + self.x = 0 + self.y = 0 + self.text_lines = [] + self.attr_lines = [] + self.background = urwid.AttrSpec('light gray', 'black') + self.attr = self.background + self.resetColor() + self.moveTo(0,0) + + def resetColor(self): + self.bold = False + self.blink = False + self.fg = 7 + self.bg = 0 + + def moveTo(self, x, y): + while x>80: + x-=80 + y+=1 + while y+1 > len(self.text_lines): + self.text_lines.append([u' ' for i in range(80)]) + self.attr_lines.append([self.attr for i in range(80)]) + self.x = x + self.y = y + + def parseSequence(self, seq): + values = [] + buf = '' + for c in seq: + if c in ['\x1b', '[']: + continue + if c == ';': + values.append(int(buf)) + buf = '' + continue + if ord(c) < 64: + buf += c + if buf: + values.append(int(buf)) + if c == 'm': + if not values: + values = [0] + fg256 = None + for v in values: + if fg256 is True: + if v <= 0x08: + self.fg = v + elif v <= 0x0f: + self.fg = v - 0x08 + self.bold = True + elif v <= 0xe7: + r, x = divmod(v-16, 36) + g, x = divmod(x, 6) + b = x % 6 + fg256 = ('#' + + self.colors256[r] + + self.colors256[g] + + self.colors256[b]) + else: + fg256 = 'g' + str(self.colorsgray[v-232]) + elif v == 0: + self.resetColor() + elif v == 1: + self.bold = True + elif v == 5: + self.blink = True + elif v>29 and v<38: + self.fg = v-30 + elif v>39 and v<48: + self.bg = v-40 + elif v==38: + fg256=True + fg = self.fg + if self.bold: + fg += 8 + fgattrs = [] + if self.blink: + fgattrs.append('blink') + if fg256: + fgattrs.append(fg256) + else: + fgattrs.append(self.colors[fg]) + self.attr = urwid.AttrSpec(', '.join(fgattrs), self.colors[self.bg]) + if c == 'A': + if not values: + values = [1] + y = max(self.y-values[0], 0) + self.moveTo(self.x, y) + if c == 'C': + if not values: + values = [1] + x = self.x + values[0] + self.moveTo(x, self.y) + if c == 'H': + self.moveTo(values[1]-1, values[0]-1) + + def parse(self, data): + seq = '' + for char in data: + if seq: + seq += char + if ord(char) >= 64 and char != '[': + self.parseSequence(seq) + seq = '' + continue + if char == '\x1a': + continue + if char == '\x1b': + seq = char + continue + if char == '\r': + self.moveTo(0, self.y) + continue + if char == '\n': + self.moveTo(self.x, self.y+1) + continue + if not seq: + self.text_lines[self.y][self.x] = char + self.attr_lines[self.y][self.x] = self.attr + x = self.x + 1 + self.moveTo(x, self.y) + text = [] + current_attr = self.attr_lines[0][0] + current_text = u'' + for y in range(len(self.text_lines)): + for x in range(80): + char = self.text_lines[y][x] + attr = self.attr_lines[y][x] + if (attr.foreground_number != current_attr.foreground_number or + attr.background_number != current_attr.background_number): + text.append((current_attr, current_text)) + current_attr = attr + current_text = u'' + current_text += char + if (current_attr.background_number==0): + current_text = current_text.rstrip(' ') + current_text += u'\n' + current_text = re.sub('\n+$', '\n', current_text) + text.append((current_attr, current_text)) + return text diff --git a/presentty/client.py b/presentty/client.py new file mode 100644 index 0000000..a819dfd --- /dev/null +++ b/presentty/client.py @@ -0,0 +1,66 @@ +# Copyright (C) 2015 James E. Blair +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import socket + +class Client(object): + def __init__(self, host='127.0.0.1', port=1292): + self.host = host + self.port = port + self.sock = None + self.connect() + + def connect(self): + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.connect((self.host, self.port)) + self.file = self.sock.makefile('rw', 0) + + def list(self): + self.file.write('list\n') + program = [] + while True: + ln = self.file.readline().strip() + if ln == 'end': + break + x, index, title = ln.split(' ', 2) + program.append(title) + return program + + def size(self): + self.file.write('size\n') + ln = self.file.readline().strip() + x, cols, rows = ln.split(' ', 2) + return (int(cols), int(rows)) + + def parseCurrent(self): + ln = self.file.readline().strip() + x, index, progressive_state, title = ln.split(' ', 3) + return (int(index), int(progressive_state)) + + def current(self): + self.file.write('current\n') + return self.parseCurrent() + + def jump(self, index): + self.file.write('jump %i\n' % index) + return self.parseCurrent() + + def next(self): + self.file.write('next\n') + return self.parseCurrent() + + def prev(self): + self.file.write('prev\n') + return self.parseCurrent() diff --git a/presentty/console.py b/presentty/console.py new file mode 100644 index 0000000..d29b864 --- /dev/null +++ b/presentty/console.py @@ -0,0 +1,292 @@ +# Copyright (C) 2015 James E. Blair +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import argparse +import sys +import datetime +import time + +import urwid + +import palette +import client +import slide +import rst + +PALETTE = [ + ('reversed', 'standout', ''), + ('status', 'light red', ''), +] + +class Row(urwid.Button): + def __init__(self, index, title, console): + super(Row, self).__init__('', on_press=console.jump, user_data=index) + col = urwid.Columns([ + ('fixed', 3, urwid.Text('%-2i' % (index+1))), + urwid.Text(title), + ]) + self._w = urwid.AttrMap(col, None, focus_map='reversed') + + def selectable(self): + return True + +class Footer(urwid.WidgetWrap): + def __init__(self): + super(Footer, self).__init__(urwid.Columns([])) + self.position = urwid.Text(u'') + self.timer = urwid.Text(u'') + self._w.contents.append((self.position, ('pack', None, False))) + self._w.contents.append((urwid.Text(u''), ('weight', 1, False))) + self._w.contents.append((self.timer, ('pack', None, False))) + +class Screen(urwid.WidgetWrap): + def __init__(self, console): + super(Screen, self).__init__(urwid.Pile([])) + self.console = console + self.program = [] + self.current = -1 + self.progressive_state = 0 + self.blank_slide = slide.UrwidSlide( + u'', None, urwid.Text(u''), None) + self.timer = 45*60 + self.size = (80, 25) + self.timer_end = None + self.listbox = urwid.ListBox(urwid.SimpleFocusListWalker([])) + self.footer = Footer() + footer = urwid.AttrMap(self.footer, 'status') + self.left = urwid.Pile([]) + self.left.contents.append((self.listbox, ('weight', 1))) + self.left.set_focus(0) + + self.right = urwid.Pile([]) + self.setPreviews() + + self.main = urwid.Columns([]) + self.main.contents.append((self.left, ('weight', 1, False))) + self.main.contents.append((self.right, ('given', self.size[0]+2, False))) + self.main.set_focus(0) + + self._w.contents.append((self.main, ('weight', 1))) + self._w.contents.append((footer, ('pack', 1))) + self._w.set_focus(0) + + def setPreviews(self): + current_slide = next_slide = self.blank_slide + if 0 <= self.current < len(self.program): + current_slide = self.program[self.current] + if 0 <= self.current+1 < len(self.program): + next_slide = self.program[self.current+1] + current_slide.setProgressive(self.progressive_state) + current_box = urwid.LineBox(current_slide, "Current") + next_box = urwid.LineBox(next_slide, "Next") + if current_slide.handout: + notes_box = urwid.LineBox(current_slide.handout, "Notes") + else: + notes_box = None + self.right.contents[:] = [] + self.left.contents[:] = self.left.contents[:1] + cols, rows = self.size + self.right.contents.append((current_box, ('given', rows+2))) + self.right.contents.append((next_box, ('given', rows+2))) + self.right.contents.append((urwid.Filler(urwid.Text(u'')), ('weight', 1))) + if notes_box: + self.left.contents.append((notes_box, ('pack', None))) + + def setProgram(self, program): + self.program = program + self.listbox.body[:] = [] + for i, s in enumerate(program): + self.listbox.body.append(Row(i, s.title, self.console)) + + def setSize(self, size): + self.size = size + cols, rows = size + self.right.contents[0] = (self.right.contents[0][0], ('given', rows+2)) + self.right.contents[1] = (self.right.contents[1][0], ('given', rows+2)) + self.main.contents[1] = (self.main.contents[1][0], ('given', cols+2, False)) + + # Implement this method from the urwid screen interface for the ScreenHinter + def get_cols_rows(self): + return self.size + + def setCurrent(self, state): + index, progressive_state = state + changed = False + if index != self.current: + self.current = index + self.listbox.set_focus(index) + self.listbox._invalidate() + self.footer.position.set_text('%i / %i' % (index+1, len(self.program))) + changed = True + if progressive_state != self.progressive_state: + self.progressive_state = progressive_state + changed = True + if changed: + self.setPreviews() + self.footer.timer.set_text(self.getTime()) + + def getTime(self): + now = time.time() + if self.timer_end: + return str(datetime.timedelta(seconds=(int(self.timer_end-now)))) + else: + return str(datetime.timedelta(seconds=int(self.timer))) + + def setTimer(self, secs): + self.timer = secs + if self.timer_end: + now = time.time() + self.timer_end = now + self.timer + + def startStopTimer(self): + now = time.time() + if self.timer_end: + remain = max(self.timer_end - int(now), 0) + self.timer = remain + self.timer_end = None + else: + self.timer_end = now + self.timer + + def keypress(self, size, key): + if key in (' ', 'x'): + self.startStopTimer() + elif key == 'page up': + self.console.prev() + elif key == 'page down': + self.console.next() + elif key == 'right': + self.console.next() + elif key == 'left': + self.console.prev() + elif key == 't': + self.console.timerDialog() + else: + return super(Screen, self).keypress(size, key) + return None + +class FixedButton(urwid.Button): + def sizing(self): + return frozenset([urwid.FIXED]) + + def pack(self, size, focus=False): + return (len(self.get_label())+4, 1) + +class TimerDialog(urwid.WidgetWrap): + signals = ['set', 'cancel'] + def __init__(self): + set_button = FixedButton('Set') + cancel_button = FixedButton('Cancel') + urwid.connect_signal(set_button, 'click', + lambda button:self._emit('set')) + urwid.connect_signal(cancel_button, 'click', + lambda button:self._emit('cancel')) + button_widgets = [('pack', set_button), + ('pack', cancel_button)] + button_columns = urwid.Columns(button_widgets, dividechars=2) + rows = [] + self.entry = urwid.Edit('Timer: ', edit_text='45:00') + rows.append(self.entry) + rows.append(urwid.Divider()) + rows.append(button_columns) + pile = urwid.Pile(rows) + fill = urwid.Filler(pile, valign='top') + super(TimerDialog, self).__init__(urwid.LineBox(fill, 'Timer')) + + def keypress(self, size, key): + r = super(TimerDialog, self).keypress(size, key) + if r == 'enter': + self._emit('set') + return None + elif r == 'esc': + self._emit('cancel') + return None + return r + +class Console(object): + poll_interval = 0.5 + + def __init__(self, program): + self.screen = Screen(self) + self.loop = urwid.MainLoop(self.screen, palette=PALETTE) + self.client = client.Client() + self.screen.setProgram(program) + self.update() + self.loop.set_alarm_in(self.poll_interval, self.updateCallback) + + def run(self): + self.loop.run() + + def jump(self, widget, index): + self.screen.setCurrent(self.client.jump(index)) + + def next(self): + self.screen.setCurrent(self.client.next()) + + def prev(self): + self.screen.setCurrent(self.client.prev()) + + def updateCallback(self, loop=None, data=None): + self.update() + self.loop.set_alarm_in(self.poll_interval, self.updateCallback) + + def update(self): + self.screen.setSize(self.client.size()) + self.screen.setCurrent(self.client.current()) + + def timerDialog(self): + dialog = TimerDialog() + overlay = urwid.Overlay(dialog, self.loop.widget, + 'center', 30, + 'middle', 6) + self.loop.widget = overlay + urwid.connect_signal(dialog, 'cancel', self.cancelDialog) + urwid.connect_signal(dialog, 'set', self.setTimer) + + def cancelDialog(self, widget): + self.loop.widget = self.screen + + def setTimer(self, widget): + parts = widget.entry.edit_text.split(':') + secs = 0 + if len(parts): + secs += int(parts.pop()) + if len(parts): + secs += int(parts.pop())*60 + if len(parts): + secs += int(parts.pop())*60*60 + self.screen.setTimer(secs) + self.loop.widget = self.screen + + +def main(): + parser = argparse.ArgumentParser( + description='Console-based presentation system') + parser.add_argument('--light', dest='light', + default=False, + action='store_true', + help='use a black on white palette') + parser.add_argument('file', + help='presentation file (RST)') + args = parser.parse_args() + if args.light: + plt = palette.LIGHT_PALETTE + else: + plt = palette.DARK_PALETTE + hinter = slide.ScreenHinter() + parser = rst.PresentationParser(plt, hinter) + program = parser.parse(open(args.file).read()) + c = Console(program) + hinter.setScreen(c.screen) + c.run() diff --git a/presentty/image.py b/presentty/image.py new file mode 100644 index 0000000..9aabee8 --- /dev/null +++ b/presentty/image.py @@ -0,0 +1,168 @@ +# Copyright (C) 2015 James E. Blair +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import subprocess +import HTMLParser +import re + +import PIL +import PIL.ExifTags +import urwid + +import slide + +def nearest_color(x): + if x < 0x30: return '0' + if x < 0x70: return '6' + if x < 0x98: return '8' + if x < 0xc0: return 'a' + if x < 0xe8: return 'd' + return 'f' + +class ANSIImage(urwid.Widget): + def __init__(self, uri, hinter=None): + super(ANSIImage, self).__init__() + self.uri = uri + image = self._loadImage() + self.htmlparser = HTMLParser.HTMLParser() + self.ratio = float(image.size[0])/float(image.size[1]) + self.hinter = hinter + + def _loadImage(self): + image = PIL.Image.open(self.uri) + image.load() + exif = image._getexif() + if exif: + orientation = exif.get(274, 1) + if orientation == 1: + pass + elif orientation == 3: + image = image.rotate(180) + elif orientation == 6: + image = image.rotate(-90) + elif orientation == 8: + image = image.rotate(90) + else: + raise Exception("unknown orientation %s" % orientation) + return image + + def pack(self, size, focus=False): + cols = size[0] + if len(size) > 1: + rows = size[1] + elif self.hinter: + rows = self.hinter.getSize()[1] + else: + rows = None + width = cols + height = int(cols*(1.0/self.ratio)/2.0) + if rows is not None and height > rows: + height = rows + width = int(rows*self.ratio*2.0) + return (width, height) + + def rows(self, size, focus=False): + r = self.pack(size) + return r[1] + + SPAN_RE = re.compile(r"(.*)") + def render(self, size, focus=False): + spanre = self.SPAN_RE + htmlparser = self.htmlparser + width, height = self.pack(size, focus) + jp2a = subprocess.Popen(['jp2a', '--colors', '--fill', + '--width=%s' % width, + '--height=%s' % height, + '--html-raw', '-'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + image = self._loadImage() + image.save(jp2a.stdin, 'JPEG') + jp2a.stdin.close() + data = jp2a.stdout.read() + jp2a.stderr.read() + jp2a.wait() + + line_list = [] + attr_list = [] + line_text = '' + line_attrs = [] + current_attr = [None, 0] + current_fg = None + current_bg = None + current_props = None + for line in data.split('
'): + if not line: + continue + for span in line.split('
'): + if not span: + continue + m = spanre.match(span) + fg, bg, char = m.groups() + if '&' in char: + char = htmlparser.unescape(char) + char = char.encode('utf8') + line_text += char + props = [] + # TODO: if bold is set, append bold to props + fg = ('#'+ + nearest_color(int(fg[0:2], 16)) + + nearest_color(int(fg[2:4], 16)) + + nearest_color(int(fg[4:6], 16))) + bg = ('#'+ + nearest_color(int(bg[0:2], 16)) + + nearest_color(int(bg[2:4], 16)) + + nearest_color(int(bg[4:6], 16))) + if current_fg == fg and current_bg == bg and current_props == props: + current_attr[1] += len(char) + else: + if current_attr[0]: + line_attrs.append(tuple(current_attr)) + fg = ', '.join(props + [fg]) + attr = urwid.AttrSpec(fg, bg) + current_attr = [attr, len(char)] + current_fg = fg + current_bg = bg + current_props = props + line_attrs.append(tuple(current_attr)) + current_attr = [None, 0] + current_fg = None + current_bg = None + line_list.append(line_text) + line_text = '' + attr_list.append(line_attrs) + line_attrs = [] + canvas = urwid.TextCanvas(line_list, attr_list) + return canvas + +def main(): + import PIL.Image + img = PIL.Image.open('/tmp/p/8.jpg') + img.load() + hinter = slide.ScreenHinter() + hinter.set_cols_rows((80, 25)) + w = ANSIImage(img, hinter) + slpile = slide.SlidePile([]) + slpile.contents.append((w, slpile.options())) + pad = slide.SlidePadding(slpile, align='center', width='pack') + fill = slide.SlideFiller(pad) + #w.render((80,25)) + fill.render((80,25)) + screen = urwid.raw_display.Screen() + if True: + with screen.start(): + screen.draw_screen((80,25), fill.render((80,25))) + raw_input() diff --git a/presentty/palette.py b/presentty/palette.py new file mode 100644 index 0000000..6079a95 --- /dev/null +++ b/presentty/palette.py @@ -0,0 +1,78 @@ +# Copyright (C) 2015 James E. Blair +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import urwid + +DARK_PALETTE = { + '_default': urwid.AttrSpec('light gray', 'black'), + + 'emphasis': urwid.AttrSpec('bold, light gray', 'black'), + 'title': urwid.AttrSpec('bold, white', 'black'), + + 'progressive': urwid.AttrSpec('dark gray', 'black'), + + # Based on pygments default colors + + 'whitespace': urwid.AttrSpec('light gray', '#aaa'), + 'comment': urwid.AttrSpec('#688', 'black'), + 'comment-preproc': urwid.AttrSpec('#a80', 'black'), + 'keyword': urwid.AttrSpec('bold, #0f0', 'black'), + 'keyword-pseudo': urwid.AttrSpec('#080', 'black'), + 'keyword-type': urwid.AttrSpec('#a06', 'black'), + 'operator': urwid.AttrSpec('#666', 'black'), + 'operator-word': urwid.AttrSpec('bold, #a0f', 'black'), + 'name-builtin': urwid.AttrSpec('#0d0', 'black'), + 'name-function': urwid.AttrSpec('#00f', 'black'), + 'name-class': urwid.AttrSpec('bold, #00f', 'black'), + 'name-namespace': urwid.AttrSpec('bold, #00f', 'black'), + 'name-exception': urwid.AttrSpec('bold, #d66', 'black'), + 'name-variable': urwid.AttrSpec('#008', 'black'), + 'name-constant': urwid.AttrSpec('#800', 'black'), + 'name-label': urwid.AttrSpec('#aa0', 'black'), + 'name-entity': urwid.AttrSpec('bold, #888', 'black'), + 'name-attribute': urwid.AttrSpec('#880', 'black'), + 'name-tag': urwid.AttrSpec('bold, #080', 'black'), + 'name-decorator': urwid.AttrSpec('#a0f', 'black'), + 'string': urwid.AttrSpec('#a00', 'black'), + 'string-doc': urwid.AttrSpec('light gray', 'black'), + 'string-interpol': urwid.AttrSpec('bold, #a68', 'black'), + 'string-escape': urwid.AttrSpec('bold, #a60', 'black'), + 'string-regex': urwid.AttrSpec('#a68', 'black'), + 'string-symbol': urwid.AttrSpec('#008', 'black'), + 'string-other': urwid.AttrSpec('#080', 'black'), + 'number': urwid.AttrSpec('#666', 'black'), + 'generic-heading': urwid.AttrSpec('bold, #008', 'black'), + 'generic-subheading': urwid.AttrSpec('bold, #808', 'black'), + 'generic-deleted': urwid.AttrSpec('#a00', 'black'), + 'generic-inserted': urwid.AttrSpec('#0a0', 'black'), + 'generic-error': urwid.AttrSpec('#f00', 'black'), + 'generic-emph': urwid.AttrSpec('bold, #fff', 'black'), + 'generic-strong': urwid.AttrSpec('bold, #ddd', 'black'), + 'generic-prompt': urwid.AttrSpec('bold, #008', 'black'), + 'generic-output': urwid.AttrSpec('#888', 'black'), + 'generic-traceback': urwid.AttrSpec('#06d', 'black'), + 'error': urwid.AttrSpec('underline, #f00', 'black'), +} + +LIGHT_PALETTE = {} +for k, v in DARK_PALETTE.items(): + LIGHT_PALETTE[k] = urwid.AttrSpec(v.foreground, 'h15') + +LIGHT_PALETTE.update({ + '_default': urwid.AttrSpec('black', 'h15'), + 'emphasis': urwid.AttrSpec('bold, black', 'h15'), + 'title': urwid.AttrSpec('bold, #000', 'h15'), + 'progressive': urwid.AttrSpec('light gray', 'h15'), +}) diff --git a/presentty/presentty.py b/presentty/presentty.py new file mode 100644 index 0000000..35e19c1 --- /dev/null +++ b/presentty/presentty.py @@ -0,0 +1,162 @@ +# Copyright (C) 2015 James E. Blair +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import argparse +import os +import sys +import time + +import urwid + +import slide +import server +import rst +import palette + + +class Presenter(object): + def __init__(self, palette): + blank = urwid.Text(u'') + self.blank = slide.UrwidSlide('Blank', None, blank, + palette['_default']) + self.current = self.blank + self.program = [] + self.palette = palette + self.pos = -1 + self.loop = urwid.MainLoop(self.blank, + unhandled_input=self.unhandledInput) + self.loop.screen.set_terminal_properties(colors=256) + + self.server_pipe_in = self.loop.watch_pipe(self.serverData) + r,w = os.pipe() + self.server_pipe_out_read = os.fdopen(r) + self.server_pipe_out_write = w + self.server = server.ConsoleServer(self) + self.server.start() + + def serverData(self, data): + parts = data.split() + if parts[0] == 'jump': + try: + index = int(parts[1]) + except Exception: + os.write(self.server_pipe_out_write, 'err\n') + return + if index < 0 or index > len(self.program)-1: + os.write(self.server_pipe_out_write, 'err\n') + return + self.transitionTo(index) + os.write(self.server_pipe_out_write, 'ok\n') + elif parts[0] == 'next': + self.nextSlide() + os.write(self.server_pipe_out_write, 'ok\n') + elif parts[0] == 'prev': + self.prevSlide() + os.write(self.server_pipe_out_write, 'ok\n') + + def setProgram(self, program): + self.program = program + + def run(self): + self.loop.set_alarm_in(0, self.nextSlide) + self.loop.run() + + def unhandledInput(self, key): + if key in ('right', 'page down'): + self.nextSlide() + elif key in ('left', 'page up'): + self.prevSlide() + + def transitionTo(self, index, forward=True): + self.pos = index + current_slide = self.current + new_slide = self.program[index] + if forward: + transition = new_slide.transition + new_slide.resetProgressive() + else: + transition = current_slide.transition + new_slide.resetProgressive(True) + current_slide.stopAnimation() + if forward: + transition.setTargets(current_slide, new_slide) + else: + transition.setTargets(new_slide, current_slide) + self.loop.widget = transition + duration = transition.getDuration() + start = time.time() + now = start + end = start + duration + while duration: + if forward: + progress = min(1-((end-now)/duration), 1.0) + else: + progress = max(((end-now)/duration), 0.0) + transition.setProgress(progress) + self.loop.draw_screen() + now = time.time() + if now >= end: + break + end = time.time() + self.loop.widget = new_slide + self.current = new_slide + self.loop.draw_screen() + current_slide.resetAnimation() + new_slide.startAnimation(self.loop) + + def nextSlide(self, loop=None, data=None): + if self.current.nextProgressive(): + return + if self.pos+1 == len(self.program): + return + self.transitionTo(self.pos+1) + + def prevSlide(self, loop=None, data=None): + if self.current.prevProgressive(): + return + if self.pos == 0: + return + self.transitionTo(self.pos-1, forward=False) + +def main(): + parser = argparse.ArgumentParser( + description='Console-based presentation system') + parser.add_argument('--light', dest='light', + default=False, + action='store_true', + help='use a black on white palette') + parser.add_argument('--warnings', dest='warnings', + default=False, + action='store_true', + help='print RST parser warnings and exit if any') + parser.add_argument('file', + help='presentation file (RST)') + args = parser.parse_args() + if args.light: + plt = palette.LIGHT_PALETTE + else: + plt = palette.DARK_PALETTE + hinter = slide.ScreenHinter() + parser = rst.PresentationParser(plt, hinter) + program = parser.parse(open(args.file).read(), args.file) + if args.warnings: + w = parser.warnings.getvalue() + if w: + print w + sys.exit(1) + p = Presenter(plt) + p.setProgram(program) + hinter.setScreen(p.loop.screen) + p.run() diff --git a/presentty/rst.py b/presentty/rst.py new file mode 100644 index 0000000..41b3f97 --- /dev/null +++ b/presentty/rst.py @@ -0,0 +1,493 @@ +# Copyright (C) 2015 James E. Blair +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import re +import docutils +import docutils.frontend +import docutils.parsers.rst +import docutils.nodes +import cStringIO as StringIO + +import urwid + +import slide +import transition as transition_mod +import image +import ansiparser +import text + +try: + import PIL + import PIL.Image +except ImportError: + PIL = None + +DEFAULT_TRANSITION = 'dissolve' +DEFAULT_TRANSITION_DURATION = 0.4 + +class TextAccumulator(object): + def __init__(self): + self.text = [] + + def append(self, text): + self.text.append(text) + + def getFormattedText(self): + return self.text + + wsre = re.compile('\s+') + + def getFlowedText(self): + ret = [] + for part in self.text: + if isinstance(part, tuple): + ret.append((part[0], self.wsre.sub(u' ', part[1]))) + else: + ret.append(self.wsre.sub(u' ', part)) + if not ret: + return u'' + return ret + +class UrwidTranslator(docutils.nodes.GenericNodeVisitor): + transition_map = {'dissolve': transition_mod.DissolveTransition, + 'cut': transition_mod.CutTransition, + 'pan': transition_mod.PanTransition, + } + + def __init__(self, document, palette, hinter=None, basedir='.'): + docutils.nodes.GenericNodeVisitor.__init__(self, document) + self.program = [] + self.stack = [] + self.default_transition = self._make_transition( + DEFAULT_TRANSITION, + DEFAULT_TRANSITION_DURATION) + self.transition = self.default_transition + self.attr = [] + self.table_columns = [] + self.table_column = [] + self.progressives = [] + self.palette = palette + self.hinter = hinter + self.basedir = basedir + self.slide = None + self.default_hide_title = False + self.hide_title = self.default_hide_title + + def _make_transition(self, name, duration): + tr = self.transition_map[name] + return tr(duration) + + def default_visit(self, node): + """Override for generic, uniform traversals.""" + pass + + def default_departure(self, node): + """Override for generic, uniform traversals.""" + pass + + def _append(self, node, widget, *args, **kw): + if self.stack: + if 'handout' in node.get('classes'): + if self.handout_pile not in self.stack: + container = self.handout_pile + else: + # If the handout pile is in the stack, then ignore + # this class -- it has probably needlessly been + # applied to something deeper in the stack. The + # thing further up will end up in the handout. + container = self.stack[-1] + else: + container = self.stack[-1] + container.contents.append((widget, container.options(*args, **kw))) + + def styled(self, style, text): + if style in self.palette: + return (self.palette[style], text) + return text + + def visit_transition(self, node): + name = node['name'] + duration = node.get('duration', DEFAULT_TRANSITION_DURATION) + self.transition = self._make_transition(name, duration) + + def depart_transition(self, node): + pass + + def visit_hidetitle(self, node): + if self.slide: + self.hide_title = True + else: + self.default_hide_title = True + + def depart_hidetitle(self, node): + pass + + def visit_system_message(self, node): + #print node.astext() + raise docutils.nodes.SkipNode() + + def visit_section(self, node): + self.hide_title = self.default_hide_title + self.transition = self.default_transition + title_pile = slide.SlidePile([]) + title_pad = slide.SlidePadding(title_pile, + align='center', width='pack') + + main_pile = slide.SlidePile([]) + main_pad = slide.SlidePadding(main_pile, align='center', width='pack') + outer_pile = slide.SlidePile([ + ('pack', title_pad), + ('pack', main_pad), + ]) + s = slide.UrwidSlide(u'', self.transition, outer_pile, + self.palette['_default']) + self.slide = s + self.stack.append(main_pile) + self.title_pile = title_pile + + pile = slide.SlidePile([]) + s = slide.Handout(pile, self.palette['_default']) + self.handout = s + self.handout_pile = pile + self.slide.handout = s + + def depart_section(self, node): + self.slide.transition = self.transition + if self.hide_title: + self.title_pile.contents[:] = [] + self.program.append(self.slide) + self.stack.pop() + + def visit_block_quote(self, node): + self.stack.append(slide.SlidePile([])) + + def depart_block_quote(self, node): + pile = self.stack.pop() + pad = slide.SlidePadding(pile, left=2) + self._append(node, pad, 'pack') + + def visit_list_item(self, node): + self.stack.append(slide.SlidePile([])) + + def depart_list_item(self, node): + pile = self.stack.pop() + bullet = urwid.Text(u'* ') + cols = slide.SlideColumns([]) + cols.contents.append((bullet, cols.options('pack'))) + cols.contents.append((pile, cols.options('weight', 1))) + if self.progressives: + cols = urwid.AttrMap(cols, self.palette['progressive']) + self.progressives[-1].append(cols) + self._append(node, cols, 'pack') + + def visit_tgroup(self, node): + self.table_columns.append([]) + self.stack.append(slide.SlidePile([])) + + def visit_colspec(self, node): + self.table_columns[-1].append(node['colwidth']) + + def visit_row(self, node): + self.stack.append(slide.SlideColumns([], dividechars=1)) + self.table_column.append(0) + + def depart_row(self, node): + self.table_column.pop() + cols = self.stack.pop() + self._append(node, cols, 'pack') + + def visit_thead(self, node): + pass + + def depart_thead(self, node): + cols = slide.SlideColumns([], dividechars=1) + for width in self.table_columns[-1]: + cols.contents.append((urwid.Text(u'='*width), + cols.options('given', width))) + self._append(node, cols, 'pack') + + def visit_entry(self, node): + self.stack.append(slide.SlidePile([])) + + def depart_entry(self, node): + colindex = self.table_column[-1] + self.table_column[-1] = colindex + 1 + pile = self.stack.pop() + self._append(node, pile, 'given', self.table_columns[-1][colindex]) + + def depart_tgroup(self, node): + self.table_columns.pop() + pile = self.stack.pop() + self._append(node, pile, 'pack') + + def visit_textelement(self, node): + self.stack.append(TextAccumulator()) + + visit_paragraph = visit_textelement + + def depart_paragraph(self, node): + text = self.stack.pop() + self._append(node, urwid.Text(text.getFlowedText()), 'pack') + + visit_literal_block = visit_textelement + + def depart_literal_block(self, node): + text = self.stack.pop() + text = urwid.Text(text.getFormattedText(), wrap='clip') + pad = slide.SlidePadding(text, width='pack') + self._append(node, pad, 'pack') + + visit_line = visit_textelement + + def depart_line(self, node): + text = self.stack.pop() + self._append(node, urwid.Text(text.getFormattedText(), wrap='clip'), + 'pack') + + visit_title = visit_textelement + + def depart_title(self, node): + text = self.stack.pop() + self.slide.title = node.astext() + widget = urwid.Text(self.styled('title', text.getFlowedText()), + align='center') + self.title_pile.contents.append( + (widget, self.title_pile.options('pack'))) + + def visit_Text(self, node): + pass + + def depart_Text(self, node): + if self.stack and isinstance(self.stack[-1], TextAccumulator): + if self.attr: + t = (self.attr[-1], node.astext()) + else: + t = node.astext() + self.stack[-1].append(t) + + def visit_emphasis(self, node): + self.attr.append(self.palette['emphasis']) + + def depart_emphasis(self, node): + self.attr.pop() + + def visit_inline(self, node): + cls = node.get('classes') + if not cls: + raise docutils.nodes.SkipDeparture() + cls = [x for x in cls if x != 'literal'] + for length in range(len(cls), 0, -1): + clsname = '-'.join(cls[:length]) + if clsname in self.palette: + self.attr.append(self.palette[clsname]) + return + raise docutils.nodes.SkipDeparture() + + def depart_inline(self, node): + self.attr.pop() + + def visit_image(self, node): + if not PIL: + return + uri = node['uri'] + fn = os.path.join(self.basedir, uri) + w = image.ANSIImage(fn, self.hinter) + self._append(node, w, 'pack') + + def visit_ansi(self, node): + interval = node.get('interval', 0.5) + oneshot = node.get('oneshot', False) + animation = slide.AnimatedText(interval, oneshot) + for name in node['names']: + p = ansiparser.ANSIParser() + fn = os.path.join(self.basedir, name) + data = unicode(open(fn).read(), 'utf8') + text = p.parse(data) + animation.addFrame(text) + self.slide.animations.append(animation) + self._append(node, animation, 'pack') + + def depart_ansi(self, node): + pass + + def visit_figlet(self, node): + figlet = text.FigletText(node['text']) + self._append(node, figlet, 'pack') + + def depart_figlet(self, node): + pass + + def visit_cowsay(self, node): + cowsay = text.CowsayText(node['text']) + self._append(node, cowsay, 'pack') + + def depart_cowsay(self, node): + pass + + def visit_container(self, node): + self.stack.append(slide.SlidePile([])) + if 'progressive' in node.get('classes'): + self.progressives.append(self.slide.progressives) + self.slide.progressive_attr = self.palette['progressive'] + + def depart_container(self, node): + pile = self.stack.pop() + self._append(node, pile, 'pack') + if 'progressive' in node.get('classes'): + self.progressives.pop() + +class TransitionDirective(docutils.parsers.rst.Directive): + required_arguments = 1 + option_spec = {'duration': float} + has_content = False + + def run(self): + args = {'name': self.arguments[0]} + duration = self.options.get('duration') + if duration: + args['duration'] = duration + node = transition(**args) + return [node] + +class ANSIDirective(docutils.parsers.rst.Directive): + required_arguments = 1 + final_argument_whitespace = True + option_spec = {'interval': float, + 'oneshot': bool} + has_content = False + + def run(self): + args = {'names': self.arguments[0].split()} + args.update(self.options) + node = ansi(**args) + return [node] + +class FigletDirective(docutils.parsers.rst.Directive): + required_arguments = 1 + has_content = False + final_argument_whitespace = True + + def run(self): + args = {'text': self.arguments[0]} + node = figlet(**args) + return [node] + +class CowsayDirective(docutils.parsers.rst.Directive): + required_arguments = 1 + has_content = False + final_argument_whitespace = True + + def run(self): + args = {'text': self.arguments[0]} + node = cowsay(**args) + return [node] + +class HideTitleDirective(docutils.parsers.rst.Directive): + has_content = False + + def run(self): + node = hidetitle() + return [node] + +class transition(docutils.nodes.Special, docutils.nodes.Invisible, + docutils.nodes.Element): + pass + +class ansi(docutils.nodes.General, docutils.nodes.Inline, + docutils.nodes.Element): + pass + +class figlet(docutils.nodes.General, docutils.nodes.Inline, + docutils.nodes.Element): + pass + +class cowsay(docutils.nodes.General, docutils.nodes.Inline, + docutils.nodes.Element): + pass + +class hidetitle(docutils.nodes.Special, docutils.nodes.Invisible, + docutils.nodes.Element): + pass + +class PresentationParser(object): + def __init__(self, palette, hinter=None): + docutils.parsers.rst.directives.register_directive( + 'transition', TransitionDirective) + docutils.parsers.rst.directives.register_directive( + 'ansi', ANSIDirective) + docutils.parsers.rst.directives.register_directive( + 'figlet', FigletDirective) + docutils.parsers.rst.directives.register_directive( + 'cowsay', CowsayDirective) + docutils.parsers.rst.directives.register_directive( + 'hidetitle', HideTitleDirective) + self.warnings = StringIO.StringIO() + self.settings = docutils.frontend.OptionParser( + components=(docutils.parsers.rst.Parser,), + defaults=dict(warning_stream=self.warnings)).get_default_values() + self.parser = docutils.parsers.rst.Parser() + self.palette = palette + self.hinter = hinter + + def _parse(self, input, filename): + document = docutils.utils.new_document(filename, self.settings) + self.parser.parse(input, document) + visitor = UrwidTranslator(document, self.palette, self.hinter, + os.path.dirname(filename)) + document.walkabout(visitor) + return document, visitor + + def parse(self, input, filename='program'): + document, visitor = self._parse(input, filename) + return visitor.program + +def main(): + import argparse + import palette + + argp = argparse.ArgumentParser(description='Test RST parser') + argp.add_argument('file', help='presentation file (RST)') + argp.add_argument('slides', nargs='?', default=[], + help='slides to render') + argp.add_argument('--render', action='store_true', + help='Fully render a slide') + args = argp.parse_args() + + parser = PresentationParser(palette.DARK_PALETTE) + document, visitor = parser._parse(open(args.file).read(), args.file) + + slides = args.slides + if not slides: + slides = range(len(visitor.program)) + slides = [int(x) for x in slides] + + if not args.render: + print document.pformat() + for i in slides: + print '-'*80 + s = visitor.program[i] + for line in s.render((80,25)).text: + print line + else: + screen = urwid.raw_display.Screen() + with screen.start(): + for i in slides: + s = visitor.program[i] + screen.draw_screen((80,25), s.render((80,25))) + raw_input() + +if __name__ == '__main__': + main() diff --git a/presentty/server.py b/presentty/server.py new file mode 100644 index 0000000..74e64b2 --- /dev/null +++ b/presentty/server.py @@ -0,0 +1,112 @@ +# Copyright (C) 2015 James E. Blair +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import threading +import SocketServer + +class ConsoleHandler(SocketServer.StreamRequestHandler): + def handle(self): + server = self.server.server + while True: + try: + data = self.rfile.readline() + except Exception: + break + if not data: + break + data = data.strip() + if data == 'list': + for i, slide in enumerate(server.list()): + self.wfile.write('slide %i %s\n' % (i, slide.title)) + self.wfile.write('end\n') + elif data == 'current': + i, slide = server.current() + self.wfile.write('current %i %i %s\n' % ( + i, slide.progressive_state, slide.title)) + elif data == 'next': + i, slide = server.next() + self.wfile.write('current %i %i %s\n' % ( + i, slide.progressive_state, slide.title)) + elif data == 'prev': + i, slide = server.prev() + self.wfile.write('current %i %i %s\n' % ( + i, slide.progressive_state, slide.title)) + elif data.startswith('jump'): + parts = data.split() + i, slide = server.jump(int(parts[1].strip())) + self.wfile.write('current %i %i %s\n' % ( + i, slide.progressive_state, slide.title)) + elif data == 'size': + size = server.size() + self.wfile.write('size %s %s\n' % size) + +class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): + allow_reuse_address=True + +class ConsoleServer(object): + def __init__(self, presenter, host='localhost', port=1292): + self.presenter = presenter + self.server = ThreadedTCPServer((host, port), ConsoleHandler) + self.server.server = self + self.lock = threading.Lock() + + def start(self): + self.thread=threading.Thread(target=self._run, name="Console Server") + self.thread.daemon=True + self.thread.start() + + def _run(self): + self.server.serve_forever() + + def stop(self): + self.server.shutdown() + + def list(self): + return self.presenter.program + + def current(self): + s = self.presenter.program[self.presenter.pos] + return (self.presenter.pos, s) + + def size(self): + return self.presenter.loop.screen.get_cols_rows() + + def next(self): + self.lock.acquire() + try: + os.write(self.presenter.server_pipe_in, 'next') + self.presenter.server_pipe_out_read.readline() + return self.current() + finally: + self.lock.release() + + def prev(self): + self.lock.acquire() + try: + os.write(self.presenter.server_pipe_in, 'prev') + self.presenter.server_pipe_out_read.readline() + return self.current() + finally: + self.lock.release() + + def jump(self, pos): + self.lock.acquire() + try: + os.write(self.presenter.server_pipe_in, 'jump %s' % (pos,)) + self.presenter.server_pipe_out_read.readline() + return self.current() + finally: + self.lock.release() diff --git a/presentty/slide.py b/presentty/slide.py new file mode 100644 index 0000000..4894f45 --- /dev/null +++ b/presentty/slide.py @@ -0,0 +1,178 @@ +# Copyright (C) 2015 James E. Blair +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import urwid + +class SlidePile(urwid.Pile): + def pack(self, size, focus=False): + cols = 0 + rows = 0 + for x in self.contents: + c,r = x[0].pack((size[0],)) + if c>cols: + cols = c + rows += r + return (cols, rows) + +class SlidePadding(urwid.Padding): + def pack(self, size, focus=False): + r = self._original_widget.pack(size, focus) + width = max(r[0] + self.left + self.right, self.min_width) + width = min(size[0], width) + return (width, r[1]) + +class SlideColumns(urwid.Columns): + def pack(self, size, focus=False): + cols = self.dividechars * (len(self.contents)-1) + rows = 0 + for widget, packing in self.contents: + if packing[0] == 'given': + allocated_cols = packing[1] + else: + allocated_cols = size[0] + c,r = widget.pack((allocated_cols,)) + if packing[0] == 'given': + c = allocated_cols + if r>rows: + rows = r + cols += c + return (cols, rows) + +class SlideFiller(urwid.Filler): + pass + +class ScreenHinter(object): + # A terrible hack to try to provide some needed context to the + # image widget. + def __init__(self, screen=None): + self.screen = screen + + def setScreen(self, screen): + self.screen = screen + + def getSize(self): + cols, rows = self.screen.get_cols_rows() + return (cols, rows-1) + +class Handout(urwid.WidgetWrap): + def __init__(self, widget, background): + self.background = background + self.pad = SlidePadding(widget, align='center', width='pack') + self.map = urwid.AttrMap(self.pad, self.background) + super(Handout, self).__init__(self.map) + +class UrwidSlide(urwid.WidgetWrap): + def __init__(self, title, transition, widget, background): + self.title = title + self.transition = transition + self.fill = SlideFiller(widget) + self.background = background + self.map = urwid.AttrMap(self.fill, self.background) + self.handout = None + self.animations = [] + self.progressives = [] + self.progressive_attr = None + self.progressive_state = 0 + super(UrwidSlide, self).__init__(self.map) + + def startAnimation(self, loop): + for x in self.animations: + x.startAnimation(loop) + + def stopAnimation(self): + for x in self.animations: + x.stopAnimation() + + def resetAnimation(self): + for x in self.animations: + x.resetAnimation() + + def resetProgressive(self, on=False): + if on: + self.progressive_state = len(self.progressives) + for x in self.progressives: + x.set_attr_map({None: None}) + else: + self.progressive_state = 0 + for x in self.progressives: + x.set_attr_map({None: self.progressive_attr}) + + def nextProgressive(self): + if self.progressive_state >= len(self.progressives): + return False + self.progressives[self.progressive_state].set_attr_map( + {None: None}) + self.progressive_state += 1 + return True + + def prevProgressive(self): + if self.progressive_state <= 0: + return False + self.progressive_state -= 1 + self.progressives[self.progressive_state].set_attr_map( + {None: self.progressive_attr}) + return True + + def setProgressive(self, state): + self.progressive_state = state + for i, x in enumerate(self.progressives): + if i < self.progressive_state: + x.set_attr_map({None: None}) + else: + x.set_attr_map({None: self.progressive_attr}) + +class AnimatedText(urwid.Text): + def __init__(self, interval=0.5, oneshot=False): + super(AnimatedText, self).__init__(u'') + self.frames = [] + self.current = 0 + self.running = False + self.interval = interval + self.oneshot = oneshot + + def addFrame(self, text): + self.frames.append(text) + if len(self.frames) == self.current+1: + self.set_text(text) + + def startAnimation(self, loop): + if self.running: + return + if len(self.frames) == 1: + return + self.running = True + loop.set_alarm_in(self.interval, self.updateCallback) + + def updateCallback(self, loop=None, data=None): + if not self.running: + return + if self.current+1 >= len(self.frames): + if self.oneshot: + self.running = False + return + self.current = 0 + else: + self.current += 1 + self.set_text(self.frames[self.current]) + loop.set_alarm_in(self.interval, self.updateCallback) + + def stopAnimation(self): + if not self.running: + return + self.running = False + + def resetAnimation(self): + self.current = 0 + self.set_text(self.frames[self.current]) diff --git a/presentty/text.py b/presentty/text.py new file mode 100644 index 0000000..e0a88fc --- /dev/null +++ b/presentty/text.py @@ -0,0 +1,81 @@ +# Copyright (C) 2015 James E. Blair +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import subprocess + +import urwid + +class FigletText(urwid.WidgetWrap): + def __init__(self, text, attr=None): + self.text = text + self.attr = attr + output = self._run() + if attr: + widget = urwid.Text((attr, output), wrap='clip') + else: + widget = urwid.Text(output, wrap='clip') + super(FigletText, self).__init__(widget) + + def _run(self): + p = subprocess.Popen(['figlet'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + p.stdin.write(self.text) + p.stdin.close() + data = p.stdout.read() + p.stderr.read() + p.wait() + return data + +class CowsayText(urwid.WidgetWrap): + def __init__(self, text, attr=None): + self.text = text + self.attr = attr + output = self._run() + if attr: + widget = urwid.Text((attr, output), wrap='clip') + else: + widget = urwid.Text(output, wrap='clip') + super(CowsayText, self).__init__(widget) + + def _run(self): + p = subprocess.Popen(['cowsay'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + p.stdin.write(self.text) + p.stdin.close() + data = p.stdout.read() + p.stderr.read() + p.wait() + return data + +def main(): + import slide + w = FigletText("Testing") + slpile = slide.SlidePile([]) + slpile.contents.append((w, slpile.options())) + pad = slide.SlidePadding(slpile, align='center', width='pack') + fill = slide.SlideFiller(pad) + #w.render((80,25)) + fill.render((80,25)) + screen = urwid.raw_display.Screen() + if True: + with screen.start(): + screen.draw_screen((80,25), fill.render((80,25))) + raw_input() +if __name__=='__main__': + main() diff --git a/presentty/transition.py b/presentty/transition.py new file mode 100644 index 0000000..9133629 --- /dev/null +++ b/presentty/transition.py @@ -0,0 +1,153 @@ +# Copyright (C) 2015 James E. Blair +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import urwid + +class Transition(urwid.Widget): + def __init__(self, duration=0.4): + super(Transition, self).__init__() + self.duration = 0.4 + self.old = None + self.new = None + self.progress = 0.0 + + def getDuration(self): + return self.duration + + def setTargets(self, old, new): + self.old = old + self.new = new + self.setProgress(0.0) + + def setProgress(self, progress): + self.progress = progress + self._invalidate() + +class PanTransition(Transition): + def render(self, size, focus=False): + old = self.old.render((size[0], size[1])) + new = self.new.render((size[0], size[1])) + c = urwid.CanvasJoin([(old, None, False, size[0]), + (new, None, False, size[0])]) + #c = urwid.CanvasOverlay(new, old, 6, 0) + offset = int(size[0] * self.progress) + c.pad_trim_left_right(0-offset, 0-(size[0]-offset)) + return c + +class DissolveTransition(Transition): + def __init__(self, *args, **kw): + super(DissolveTransition, self).__init__(*args, **kw) + self._oldbuf = None + self._newbuf = None + self._cache_size = None + + def setTargets(self, old, new): + if old != self.old: + self._oldbuf = None + self._cache_size = None + if new != self.new: + self._newbuf = None + self._cache_size = None + super(DissolveTransition, self).setTargets(old, new) + + def _to_buf(self, canvas): + buf = [] + for line in canvas.content(): + for (attr, cs, text) in line: + for char in unicode(text, 'utf8'): + buf.append((attr, cs, char)) + return buf + + def render(self, size, focus=False): + if self._cache_size != size: + old = self.old.render((size[0], size[1])) + new = self.new.render((size[0], size[1])) + self._oldbuf = self._to_buf(old) + self._newbuf = self._to_buf(new) + self._cache_size = size + line_list = [] + attr_list = [] + line_text = '' + line_attrs = [] + current_attr = [None, 0] + current_rgb = None + current_props = None + background = urwid.AttrSpec('light gray', 'black') + for i in range(len(self._oldbuf)): + oldattr, oldcs, oldchar = self._oldbuf[i] + newattr, newcs, newchar = self._newbuf[i] + oldrgb = oldattr.get_rgb_values() + newrgb = newattr.get_rgb_values() + if None in oldrgb: + oldrgb = background.get_rgb_values() + if None in newrgb: + newrgb = background.get_rgb_values() + if newchar == ' ': + char = oldchar + charattr = oldattr + newrgb = newrgb[3:]*2 + elif oldchar == ' ': + char = newchar + charattr = newattr + oldrgb = oldrgb[3:]*2 + elif self.progress >= 0.5: + char = newchar + charattr = newattr + else: + char = oldchar + charattr = oldattr + char = char.encode('utf8') + line_text += char + rgb = [] + props = [] + if charattr.bold: + props.append('bold') + if charattr.underline: + props.append('underline') + if charattr.standout: + props.append('standout') + if charattr.blink: + props.append('blink') + for x in range(len(oldrgb)): + rgb.append(int(((newrgb[x]-oldrgb[x])*self.progress)+oldrgb[x])>>4) + if current_rgb == rgb and current_props == props: + current_attr[1] += len(char) + else: + if current_attr[0]: + line_attrs.append(tuple(current_attr)) + fg = ', '.join(props + ['#%x%x%x' % tuple(rgb[:3])]) + bg = '#%x%x%x' % tuple(rgb[3:]) + attr = urwid.AttrSpec(fg, bg) + current_attr = [attr, len(char)] + current_rgb = rgb + current_props = props + if (i+1) % size[0] == 0: + line_attrs.append(tuple(current_attr)) + current_attr = [None, 0] + current_rgb = None + line_list.append(line_text) + line_text = '' + attr_list.append(line_attrs) + line_attrs = [] + canvas = urwid.TextCanvas(line_list, attr_list) + return canvas + +class CutTransition(Transition): + def __init__(self, *args, **kw): + super(CutTransition, self).__init__(*args, **kw) + self.duration = 0.0 + + def render(self, size, focus=False): + return self.new.render(size, focus) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7996638 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +pbr>=0.5.21,<1.0 + +urwid +docutils>=0.12 +pygments +pillow # or PIL. Optional; only used for images. + diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..80939da --- /dev/null +++ b/setup.cfg @@ -0,0 +1,45 @@ +# Copyright (C) 2015 James E. Blair +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +[metadata] +name = presentty +summary = Console-based presentation system +description-file = + README.rst +author = James E. Blair +author-email = corvus@gnu.org +classifier = + Environment :: Console + Intended Audience :: Developers + Intended Audience :: Education + Intended Audience :: End Users/Desktop + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) + Programming Language :: Python + Topic :: Multimedia :: Graphics :: Presentation + +[pbr] +warnerrors = True + +[entry_points] +console_scripts = + presentty = presentty.presentty:main + presentty-console = presentty.console:main + +[build_sphinx] +source-dir = doc/source +build-dir = doc/build +all_files = 1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c56e349 --- /dev/null +++ b/setup.py @@ -0,0 +1,20 @@ +# Copyright (C) 2015 James E. Blair +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import setuptools + +setuptools.setup( + setup_requires=['pbr'], + pbr=True) -- cgit