fabulous-0.3.0/000077500000000000000000000000001274117624500133435ustar00rootroot00000000000000fabulous-0.3.0/.gitignore000066400000000000000000000002001274117624500153230ustar00rootroot00000000000000*.pyc *.pyo *.pyc *.o *.so *~ *.rej *.orig *.mo .figleaf* *.egg-info *.egg build deps local_settings.py .build dist pip-log.txt fabulous-0.3.0/.travis.yml000066400000000000000000000010661274117624500154570ustar00rootroot00000000000000language: python dist: trusty python: - 2.6 - 2.7 - 3.3 - 3.4 - 3.5 - pypy addons: apt: packages: - libfreetype6-dev - libjpeg8-dev - liblcms2-dev - libtiff5-dev - libwebp-dev - python-tk - tcl8.6-dev - tk8.6-dev - zlib1g-dev script: - pip install Pillow==2.3.0 - python -m doctest -v fabulous/*.py - python setup.py install - yes | fabulous-demo - pip install sphinx==1.2.2 - pip install sphinxcontrib-programoutput - sphinx-build docs gh-pages notifications: email: false fabulous-0.3.0/AUTHORS000066400000000000000000000006671274117624500144240ustar00rootroot00000000000000# This the official list of Fabulous authors for copyright purposes. # This file is distinct from the CONTRIBUTORS files. # See the latter for an explanation. # Names should be added to this file as: # Name or Organization # The email address is not required for organizations. Justine Alexandra Roberts Tunney Alan Trick Xavier Basty Red Hat Inc. Google Inc. fabulous-0.3.0/CONTRIBUTORS000066400000000000000000000004711274117624500152250ustar00rootroot00000000000000# The AUTHORS file lists the copyright holders; this file # lists people. # # Names should be added to this file as: # Name Justine Tunney Alan Trick Ralph Bean Simon Chopin Xavier Basty fabulous-0.3.0/LICENSE.txt000066400000000000000000000261361274117624500151760ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. fabulous-0.3.0/MANIFEST.in000066400000000000000000000004221274117624500150770ustar00rootroot00000000000000include AUTHORS include CONTRIBUTORS include LICENSE.txt include MANIFEST.in include ez_setup.py include fabulous/_xterm256.c include fabulous/balls.png include fabulous/fonts/LICENSE.txt include fabulous/fonts/NotoEmoji-Regular.ttf include fabulous/fonts/NotoSans-Bold.ttf fabulous-0.3.0/README.rst000066400000000000000000000012361274117624500150340ustar00rootroot00000000000000.. -*-restructuredtext-*- =================== Fabulous |travis| =================== .. |TRAVIS| image:: https://travis-ci.org/jart/fabulous.png?branch=master :target: https://travis-ci.org/jart/fabulous Fabulous is a Python library (and command line tools) designed to make the output of terminal applications look *fabulous*. Fabulous allows you to print colors, images, and stylized text to the console (without curses.) Fabulous also offers features to improve the usability of Python's standard logging system. Documentation is available at `jart.github.io/fabulous`_. .. _jart.github.io/fabulous: https://jart.github.io/fabulous fabulous-0.3.0/docs/000077500000000000000000000000001274117624500142735ustar00rootroot00000000000000fabulous-0.3.0/docs/Makefile000066400000000000000000000060701274117624500157360ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/nemesis.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/nemesis.qhc" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." fabulous-0.3.0/docs/conf.py000066400000000000000000000147451274117624500156050ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # fabulous documentation build configuration file, created by # sphinx-quickstart on Tue Apr 20 02:12:28 2010. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os import fabulous # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.append(os.path.abspath('..')) # -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.coverage', 'sphinx.ext.pngmath', 'sphinxcontrib.programoutput'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8' # The master toctree document. master_doc = 'index' # General information about the project. project = u'fabulous' copyright = u'2016, Justine Tunney' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = fabulous.__version__ # The full version, including alpha/beta/rc tags. release = fabulous.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = ['_build'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'fabulousdoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'fabulous.tex', u'Fabulous Documentation', u'Justine Tunney', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'http://docs.python.org/': None} fabulous-0.3.0/docs/images/000077500000000000000000000000001274117624500155405ustar00rootroot00000000000000fabulous-0.3.0/docs/images/fabulous-demo.png000066400000000000000000002710061274117624500210160ustar00rootroot00000000000000PNG  IHDRAssRGB pHYs  tIME 6 . IDATxwXTG( b!+{ĆQӈĘ%1ذ&( ADMl_bWETlQTR.;.aA><,SϜ9sΝˁ B!AAAT#yyyqUN-[rC899!??֭[h֬{ԩSܹ3JKK ǹs8quXly<Ο?77*AA Caa!V#==ϟ1RSSgϞ\(+++ܿ999ʒ[ZZp81H[HJJ@ ܹs<Ϡ߾}|>_AW  Bի駟2ʗ:=磴1OCD"D"Q1Rʶ% b%Mp8|_  <}/^~g*YGJJѻԙjgU'*gU~I$([r5֯&g  Bwrss!dq\PFV>ʨJ/..Fvv6 !Gs'?j HNfi(--ǬY0k,YfumHVU>AANAAdeeS)mK=a`)S'b1LMMUYYUUN:1bի&N}!33S d&  Bbu>P6ZޱFs?ubڴiؼy3|>x<b1֭1cŋxT#'?311uY---Ez0fA,ƍCDDҔC*AAaQpVU9nrsر3g믿BAHHƗ9ª-..Ƌ/t\qqS:UURRzaڴiGhh(>} L8ӦMömې&Y@ [n̜9AT}b̘1ѹNC@NՃԏkʠE$@tY ,Ypq1YYYغu+x<\f#3Nx022RjK~y%yǏ"233iӦ!!!wx) ȱQX[[cРAh߾=QTT{ѣHLL$3ФI,^qܸq%KJHjgV-6Ö-[0}t?ϟ?ƍ!pԖնuUv|kV;򎷪\.?~ .@$(8\.b6m°actZ@3iӦ f̘'N`ڵHKKCZի }\o;dT UA1N Vp8P#srr{:gU"d3EQQ"##ajjɖ:011QpVx$+(+8p ƍrrr?S%Lhh(*{+e۶m u`̙Xnܹ#˗s!!!}zSN{QFELL N8!;>} ϟ?DZc_Ɏ_۷o,--DFFE޽;j׮ܹwU(믿bСWrrrp!=zҥKQ\\O>?ڵkc Rҭ!hݺ5LMMqEY_++4h KKK<}9`۷/ԩl?8xB]eT}h7)ݺuS9lYU\jGmժF ;;;@RR>k׮I gUݚUm3011̙3 H///̞=@ P?OK_^2Ovup84*t7N.9Ļzb###|WHOOG׮]1uTx<lmm_>ӦMSÇ#)) vvv1c5jxt ӦMî]0ydrJ;tLܿ`0`Ǝ/b___> <>>>8}4k :GFii):ViӦ]Mx>iI׎KKK[hڴ)qU:5EN:-..%ϟllذX~=4hY=YUgffbZRRZjA"@,vڲY Vvm> 55%%%8}4Wʷw^ٳʖؽ{7rrr]v{ 8teR>}ڏBQQ KHHP򄇇#//yyy?믿}U$u;fhӦ ;lڴ ԩѰaC|G y9q%jJ}W_~G_7MQ]H[&&&x$ n߾_~N&;_ݞc)S͛71###<{ ;&NS*ժ]'77ZBBB5k:UUb$&&())A6mp)nZѽ>p8ݻ7rrr*u#.e˖=eTfiYYY`'N@qq1?ǃ1+++Z[[UVVVl0Ɛ-/_z|o'/8|LY\FFN:`޽{qɸ~^Odzvttsm˖-ann8N:j,'i[N)RQ}coC[]}HS{ѣ_C" 99zAmP666Fhh18c <>ğYYY*P&6??GvpEleEI>gggX[[YZZ[n8s JJJ*H@={fkY%˜9s`ii_~W^Ö-[T:s^,ZZZ"33>N8={([ǏfffnݺSҔx9QvmdggvgϞUyQgmmt'MaDpttD߾}q5СCz?bccw^]vׯ0:t耜ܾ}۠֡?M&Y{6dפ7@ڜU]H[Ʊc`ggGGG3fB̪:mrqM$&&Ti{*>{aڵ066V묪ɓ'077}Zuxfdd(9cXZZbȑ[Vzz: OOO 2{Ő!C@~7!!!`aܸq2Dʵkt+K S6mp.\6oǎp59 &`ܸqe/4vD-[bРA駟h{6[.\\\#Fhm_Ry , A5ΪS#n*VZgw*OvvQJU3<{, X޴i&MEٳg }aXr%`5r W_) ?w1`Z .ݻcx9݋lfҥK/P^=dggcݲ5RD"={tP YfS.\P!tuap\ 4k֬AZZ8;*СC(**€vZ )) 7cjeT}Mxl۶ SLܹsٚTu6nt񁃃cHJJBxx8=#k G(2[B={S08}4ܐI#Bo(۷oD":WY:u>"ϟGvv6lllн{w4h7n1z?|ּ\.6l@  tZ_E3f s*k oAT=bWf wš#CqmH$]#Ajm^#xzzbС077G^^nܸQu93I>paU<;v˗UD lfIA(gϞӧO ~kܸ1:wlx3رccHIImdeɓ'k̷zjԩSwʕ+O#@ʜUooow|>666 I^^AAYDY-))ǏI[AADsAAQ#UMS=b  u4~   ^<Z\\L' P߹N   jMs@AAT^YAADMuVW%  jJ*AAQcUrW  Wۨ)S׸NTg\  UV o^fHKO?R[ζk AM7FO( jU]l)ťWZRYxWui-m]:⹓066VJɁHD WpO'U^}JAUxMKչr.2K'WҶaia^#yճm^NoŪVͲA!O? P϶R-]a9;T\\&AذqJy냮,77oD"QstI?[I߷n%s>כggo #zîi+qs&-o0ܸyKԳm;Bѡ,W_S{ }gY˶k&MEFFk9Pzͱ>x3&O>OADeUƘ֠.|_#5=-]S[C3233w8=,SgT]GwJԮm4ibF}4oT/ gł11gϡ_ޕ֣/x,S޳{O;Hq^ÆⓀ/˩3p`_n_A y'΀ߌp м-T֞Տѿw83b* (P.BvfVV:&GwQAصCCCLZzzJyתήg75m{?t5@+˙6hdq}cO^9t("`KZ ތEa3G Ǡ+->U ϠA o^++K@AAhH[o]KKySPP]{z:{*oWG_N#{݇D_8MAߗY }'OG q k#-=d7ev<ϜC]9g\- ro4֬kc_~^apu/yGU)SSSqbqv.e?7 bO򎬾,o#-=8AQʶb.|wz ރ T>h^]x3wM,uOZhb1"`@>y:}6gTY8]9C|ۧj {- >~ꨨ~m0CԵ1AĻ*2i>[%?HX\7oa*XRdff!## Rw3XۃC'܇,ӫק|G xBWhM/M&H~}kE_Hm033 _#Gxن  *2t{\?tMFm0Ï>tmiA4}aG@Ǯg;ztV9c >l!gШ+m aѧAdM@gіwO˰ۥhֲFLDNt(_qN?eۙ0z ?[`q@ ޕ;m'$'" Rϭ SAa  RuMx6SAaHgYAA5Y%_  *yAADMuV=mv  G(<AAQ uAAQsQ鬊D"!jȐ!سguVmH$ 4^o6d/t$xmΪGzj۷?#,,,JEn?seĉ >m*)ŻļBA`ghԨ+=ԩSJE9QQQ5Z+++ܻw}Ϟ="s"i60BA,_fHJKKm۶ Djj*͛P|طobccl ~~~ڵ+BBY;;;iӦ>V\GIJJǏѲeK\z,?Ĉ#CX͛B8;;cԩX`*2S~}888~UXhЎ5>kiiiXf ]UMm}}}1|p`HIIQ+]uml٣GL<5SÇ~@_^񫈽hyĉƍq i&*cE[^4QO5   6ZH' \.999;v(elܹ:믿FA={8p`죏>bJ-[Ǝ?~<[lB[-[ WH;v,ei_54h.\Ȇ^43g#F(UK.e>>>4y/^СۺuNTt3((Hm]ulڴix}uչ2nc" t]>A+ucp٣@c21}EKZ|OpϟiӦٳ:`ĉpppɓc߾}ԩSz"H$9*+".n \.$ LLLNqT_ ggg888!.."Xp!>k{BxiOCx[c]: k wgVrrr 0~x),,D&Makk~ ˗/,-663f̀R[nÆ ͛7p|xxxxXcǎa8r(,,Dbb"-ZoooرL\.<<<ֶUׯ_;|> P.>>'OVy$Ke]^ >|RLjJkS,#.. Uio*u6uMmU{E} jm6,^!!!HOO׫h/J'ݿ IDAT/8|0F ,Ppp0x<mۦsÆ pssCdd$z`e @޽]t ?VpJ*C=p])U6mB>}SSS:7l؀FaJuj2.M6CBB0w\l߾c)لkSh|Xz,m߾} ,^k׮r{Qg󚨌]{DE} /XoߞܹZ֩SaÆ1*oO !<-\7`Ϭu5brMu`UB (PP~Ji\l޼ZL|ӑٳgt )) ׯ7X~i)I TA G(2U  u" _  B|RAՁty>6_6oTTE xU vjTH:[^Ot WY}5jbcc4̟?5A9ATʑ|ל###ڵKi*]uֵkW 44ʜQF!22EEEhٲ%ѢE 2Z Y%ʕ+8pBΝ;QXX+W>Z~PDKKK"77 :{Ǐ#77EEE8{,FS/_^)beKa]p!]@=zL̛7O*kyvڅ >(P)BU>cwU(7~x6~xcBZ6/ vR344Tkiii1@ `> ϟ{OcߵŋNeuecz鬢/tRVV-6k֬J rUrV)PP!gUUKHH`| jժcWYU377Wk"1ٯʼ6f2scYդZ\ڵ>Q]S@9(P0c͍ B:<O DcJNj:\.0`[l;U!~///Y111Z!7`ҤI6mhퟪT.]))):ŋƍ<?#য়~-f:5QQ]AȠU (jfu„ ,55;wYݵk+,,dׯ_gڵcجYXFFW* >ݿ_6'dvb,77ڵYZZj͍EEE\VXX.]Ć b0=h-**b/^d{i>c?V껶Ο? XZZ7oVUTךa]S@ݞYϭAA5uR"  j.AAAA9AA[_CAADU#_>   j   g   Y%  Y%  rV  rV  U   g   g   >xWDڞ)  JgVE",TjSF``A?;cBet-[6^MT#yA=ZPzxxBu:m5F˻|=VEYU-G8\5jSFrr2:vZ'(]mg!CCC?l\כ,C_4UE0}tӧc…2ei PuV888uVsuCY.]য়~Btt4_~qcl&\tV777$&&V[{իY;ȳgмyi6l@޽hӦ z`Ya ե?;wϓ1xyy!,, cǎŴip,YAqV{M6!&&FafIo@XXLLLXfN[[[#((غu+ڶmX3عs' qaiiP"77ZWvpUbΝِ_$aaaؽ{ ֭CTT|}}?H___DFFbݺu:˩nVSd6kIeؾ};F#55UqRe󹹹ذa>cXZZ?둛+={ /^T(.… ԩz…Jټt\i{133p#ʗ_~8ŋAÆ Rk֭àA*}.___DEEaq]G:}֭[#G*\l\~]v<ԩsѣ=z46n܈ ( ''ڔ+0uTk6lc„ 󃟟&L KѶm[L:Ug=P(/1uTY?N< 5~R&M7o+hj+ ѣ6nܨ8X$&&bz }OOODGGv5ۣvڰ]V);;Ǎ1O+m/`ϯKceʔ)8yVY*rwm֭e˖!"" ビ'ObΜ9U:P#xQ )) sABBnݺX*9ѣGܹspqqڵkuj]vE@@TBWh:.]<==Ҽչsgr ** 8}l-$899aҥ())ALL &M@'G2r4[@ ' R-[0p@JS0ƐSX,FBB:t>˗/C,3f̀-\AƯK.2e ^xaPY߅cχN/UQ㏈ʼn' v]ٿ?JJJ~;JAS~pvv);5q̟?ӦMٳgQPP(4$ FbfddYYYբ dddԈaB">i]%DW'XIg*ԡ&NN]dXTh|.ޚ.N:A (-NK.\@={^XXC`` ܹNf͚锯<[[zSvIw" 7^믿0h 4k _FaX,F\\"""&M(t;ȑ#J陙hժR|||<&O7n܀}N~Ikܸ1nܸQ:51|p٬ill(Mܿ_6$񯗗FԣGc:~:1l0OlTFMX[[֭[zDDuʺ M\\:t5]UCrrr 0~xKll,f̘sssʽJ0^ZQ۞Xh^x_XX8UKtM}xeigΜ޽{Y"""/ @i<]]$HիW+d/###ҥKԩ_Ȏ;SSSr6ly{nNRN8mբmVOOO5 7no {{n̝;Wai͚58wvVݻHe ܹs1w\رCr6m… 1qDOl,YY4૯BAAluK˧a̘1k,C<6~!۶mŋpT8X{ ƬYm6l'(.mѢE2޲BM^kGEm ({c?VZUi] BDDD %%˗/qԇěG(JUg 3p-ZtҨ*bŊv0 UʺuUQ3A4*//35){ Dg  *pV 9AAJAAa*@AATY%  Y%  rV  rV  U  U   g   Y%  Y%  rV  rV  U  bgU$ɂp8 6eUzG*WAuN@[\o<AaΪ<<c}U P<]F|/EEn ި(Y0ڴDHnܐv$NK+K؋AQXX^LLp='y8cCnT~vU>^[>n],]=\;uDjc,[\D'V0$B`X@@裏,N-[Ǝ?~<[lBH$R*k9___fjjΝ|z"|UmQ!`V `r?l `{҂6eڌuVq`-#t-FiMO|e̮3q`pY Ni^[XK5r:ɹb[o *m|]|.~P,{*D},N_T޳L&m}p5tb5tb-)+}̬8<>3k>F|X5b#d y*p8ڎY }<֪'qyK,`ė]0G6f(׭UO& X+[G6kЇJhW,ϝf0SjVN3q(=c&Re8M[ؘ݃q;2ZilO#U+vF<5g&355e bRWoī[]Ȉٝ8MU  B!SYҥ L/^(9 BII bbb㣓3Kp`Æ t@~`L0[v[p1߿pSsR +W7N\4p梃 a4ݣ(-_u&_'9:L tj Nmiso|({b>(M~TGeD4m&< c,yЧmX iW1TR{˩,IfxaK9T%N,JJKp;&<;IFMh(\+IΞo`H$ʵmhp\%c*QKϘU={1w:y8=c7֯ #d' pQ\6ѭGWX)=P YéЁZPP2>##ʪeXXX ##$_E@iu".TcbHJKҢWfg. }E _N&7(.t.PĪJS erp9 ڢ,@g-'cpƊ)LKMDF8r107=touK*K^X=MTTMH^odGyCr9{D` JwBr*藵^;Ğ;o[IL&NkĞ;tH$]PTTL'R"^Ō3`nn֭[@ ap:LjJrqզk."i7ƍ"T% @$A<˥ߐpNZ*0nbZ\"0s`@#>=2G 8plY`{uNDqamI3뿧T8/ :7BvCrRhp&1QPne&cqypiSɃө7|#,)-Mzɢ!MH9HΟ{9x |>xU10uuoL9{#dァlsQV-:iDu8xضmީ6l"##ѣG+Tc)9iĉh۶m)m۶8qYlI=#({ KՎ8ƙUǕ]h…#LtwCX൥,V.\RҀI+vT5X=UեЯ3pt3XCd򀵻௓z8Ffp ݐ|fBZʅPiGѤTEK & <)`{{(FvY! G!ChѨ{|#u߰2ƇC?F/ڎgRMh p0.FzqJDHKO{Gܞ̅Ѳgۨ(..Baa!ETx Te׸UnR4ibQY  UM V[{Ђf `` w)[/r.lѰ'?3f,EcI ` ,//v& xj Bi^'#C8/ÌoF=x5UEeRAA5Y  U"H*]Jp8Xh/_^+>RN] Ċ+TŪ)0flb"#UK)ng/|f&!~:\R?Z^tY5EeKVVք6CDRUJ9HNN6Hg*+Kuɩ رi5PU|[?Z)Y5wP#!rIԑ% sV+k[DK4 X[Ь*A0`Ç#%%˗/GJJ hڴ)߿+WѣGZŢE`gghGGG(ś]vFYӊʢH$BDD<==yiHNNG4tvŹ3瑓}F#}D\$\OrMأY-gԾ-Z /}oNKGvmp8q<~jCB9gO NfkFF}?:M1i2ŭp` ܾl]v%\|nSsF5 p<Q_=@Bi.FfJ$8C ΅k()f4~(,(EAai!1lP{&֝)spCO0odK!;oϺh|H-EN6.Bf=/߸Vmνz7c9=N,͇WRR_gΝ;aPY-((N<9sq=ΝN ɓAyrبt`ȢKI&)}iKUENNlllNii3l#8@zN-kc2l݊?CǞE/ϓ;7VNPVO0eu+L45U/龁 'qhص@Ɂ@:?#lT;ve:s" -;`_gxoqjԳǬa3ݧyf g)5};ߗ]6{Bs7bo!4bҀvʷ+:Y4a7hG9u go_ڷڴn]]]aڢv4oBN0mMʽ쯿psh--ayΝcT~$.B܁+++,Y_G o1:za-]DUά߿%%%ؿ?Ǝ0GL:99aҥr&MmFKL2/T8Er((( : @JJlԺ 5@vmQۼ6ܲ{Zfe߈oҸl/*JJTM['dgGz˺:3 B^~IgM3)f2꯼e_i}ulႉǣQzeBmӤl?wjU6˺lwf *x;&iO,9h2όâ_?;wFi^^>ev /)LeXi)gnY-3f46o?Y?|A:-,,*eC}С_'ڵ?ATZ8ogddYYYxݲe3):,,,wڛFqx9FzWHkDmM6{f5O"Q-izWUEy*;W(M8q#'EbV"払2p8ˁDtNSũL][ SWo`@ﺰkd؋4'c IԌ[?' _T0U_ {r!43iT>oI~vIU.]v而={ yqfVomG#?1n\¹s }ѧO4j/*άQ7YNUҙ|> /ݺu 6lcLj[ZZΝ;r =W^رcfԕ|>wG\t5ͷ⋕3 WPh4eAR)QQQTgTTRIh衼sJPer@@Ge{kStwPzdd( *R vF4"l{=K.(`c)AX|17KI,K,G,))l9>*##쯾iHgN*b   XufTvQF,iX/4K|㹇}-܃WWZj~Yg߹-/6Wkޜ0l#{Tu򻳷hY\cISUW[7֯:T*.QQQ-_sÊz78*Kќ$n_:uҀAnj w wۘ~2z Lۺ&##ݻ˷E/񖽲d`[ymH|2ۢT#V*zk:D~A9pdoN w:&cVy饗PE葬1"** oooRRR 3R֨*l͍qѻwo4ekjz{{/OO>٪*V%))`Ο?{~_o$##EoG~nϚ |lJ pxhGY7.+Z\Ly{;:OÐ~<~i\[JiNVVn@瑎hSYݥ8ᑡNwugX>\σ|[~RooUl]୿YK`Hg[ާ~ZĕS;h>>wz~hZfgs{Mߗi~y꽘ڕ˴m ;?m$wtp{?IY9-gt` n7yR< k1ѧ?: ?~mVj-bbJ7۷\ɆߜP#3ueƟ:9ja/л{vZӳCqk7X+\??m.]ݙQ>+ f7g…F%.""tnWj)OkvyĉQqƙ8q" 6̰/yڒnԨQ&;myzzSKyZ?o| X,3P>~I,ZB1Nxul06TN/mϩ}_T,6]9Kv8t:=f@q_-5{;%+'y<<9c<ܗ3΃8rH;D z9n߿~\ɠCh}p!^x|$#ǐq4w]pG8|'C9˭%]ڪIk)nD#8O&^9(99(@p~ i:9J*p8tzj$ܸAqWVֽ 23/ҹs'O䋄ڵkgV=+G0o2a۷`Էź&''SPP-[>}M3w\ظq#cƌq ^z1fMQQiiiٔ-֮]K~~>Sp \ٹJk~+;,;Gjd8j9n[TRLON跙/ʫZϿ%a~w \u,{?';8Q![n ˯0X-g+|ޛ3V{w=o6 ޸{zFѴ5e9-7u'3OO\MInky-tf!('{Y6jZ>TqQAY۷P{6566IIj ?Mn(IIF ź(%x'NmYɳyv+n73G|Hff&}?Gg&ǟuCǎTAKc@VZZZk3SReyMugk ;W\5kFV}`S n|Ϝ%S9:~OL_hb|is]WRTa,vvް`g]|3uF3{JEpT7>=ȿLjrol{(ƐH-Ԗ.6ؙFsm)ݹғ'q\tp:꩑|Zsɦ_;on]d?H߲~m\̼DJUZl nnnbUBP+؃Z{|vv6۷ hӦ]ioرcqww7;v j5C 1yeo~UƑ#GhݺE#HэcC';lN̽6kY7%. O]P"3W6b)e]t_ƭ>+yDPR^gvfW/n#?qaoWd)t~~]^?0tĆ]{OkN~8;^be~ٳpڭڙS'Oӷ_w}4mZ&RM|m(ᅨIֳtr( GnRݻ7?MS1cs̩򢢢;wnhb\xr_VTy2.f]z_Mv*=zC^5Ev;ZnUբ( Թ]^CBQz3Tz-N@o$:8{+_plF 'V @ {?Qs8pr)7T5cvkz ?5}=_4ݦ*l ԋژTAab4ÚZl"*ZVե6n QQQ&㪏$BlLQb璗ع%#]Kݠ*ӣG222j25եͺBFFݻw[VTPl\ŖUv%ظ;^}ik5%00Cݑ_u?t[&pp<4 4 LƬʹs3gΝۛH|}} 낞={ZjŌ3&55DG||Iƍwh4|liuu*V%))`Ο?ɶ-cͼۛŒ<^Wdddg&ZaÆo/rp _߼ycǎ[ow;èQhݺ5NN[S駟2e^y*ӿС84in GZ,87n%cD õS'X&=ğ_~is{ޖiWӦyGZCRݎ> ٱ K]ps`B@ QKۉe2fUx(*)3Rzv =@iMm^v~Ғ|m쁓m{'_<[EF'OK~e㿔P/ v&y۔[q1© ַ {_+ԙi#_V8::|z'g2sb>ziݿtذk;=5gT׺%cO }W&<;.=gA4.2t~ܟbYjViC55ѭ+#zNeN,^ʞq5l%Jw^8U߮^emp!~oy0sNAIv oo];q(.E\m5z#] T?(Y( &|ؼy3'ΰaضm'N'yf(,4zzzc6(7~Mt%ݨQQREDDNXX٭S-սnYMjtԉX jՊN:9s&?-Z0;;:#FPSw=dL@XX߾=]qv{hсcжW,-ڎwOs4 \]~ҶBZ=}-ڎmXr.ѵ.n>^ns޿8[yG>V#=|A=}ym?<}D 9I?ף=)K6%syCewq_<3C_B^[`{e9 `|3蝥4ո T.רtƧ}x{7~uihBt/K__G6^mCv&??(Z5Neё-zT+W>7ws9u8nM5x{u!uYݰaEEElذÇy)**"-- IDAT0 Сg65jMzŘ1cqYolut%ڵkϷٰf̝;"6nȘ1cj0({z1 >CڷoKι^yMիWͦ_HJJ[̹s)xIO7\ӴΫҴOjj}x4Pb~Baqڳ> *ezve|Mے٣jcT"mnHIQY ΃EӼfcG**Tr욎W7i%\o_͇l@׳[߶fdysoӆ}9=~3hԓh剓Z|Ӻ5z>pqԭ7.QdPRo7oټwՆϠV~)pzakYGǚsiεyU AKc&T*gdgeeΕ+Wպ轤E]͚5#++nY]( * ß)@||<׏{IZKDGG׿njIn_- /2_ދՒ U[)~K[|`wå?qjwJ̖*P9Rj̔K?z?q87&7k{-AsJE>`A+(o@ޔE,D'4TU?}, ͿrݲkԦfE?؎^-[o(.`dzIm׬l_ ǡRy%v@}o+4nwQPX]ɫYU)6y 2s1P 25yvv6۷7>qrrbȐ!&#Gкuk۷3vXMdեtR-6]`{Ͳ+lO}ǭ޸zҒ|i,wOCqpPs=rw6xD, fϷ-_go}7w2_7}̠8;TAz ^vƿt_ 4xAmff]X)ՓffkVpsrbYaz9vjܖ~W3.^:]PT8o!Vo{]7=-eA/cUѐD cbcc dpBt+W$::Pc$%%jjdӦM/\GGG-[fvjuu*]uf-gݺux-սgӦMvjBpp0+Vȑ#o9sm۶ɓp{eFyM2YfOmqmթSdPsdL@ҥ7nFkf:GOg9[^T~_F@a6[kr$rW jBu(J q&p񷥴|x}F囬zj(~GjIvrD.JǗ%\>@)%V k))ns=;bfxN]Ƨkʍͅ1hrF!˧?=}j;)O04!~Q+k.HΟL7vRoL^G:ɾZvǘ=n^NԒFib[=<ͅR8U)DQ~$t219y_+9y7(.)!F%%%4UKYѾ]Ggj6w{CWP4r[JŌ3nU*{&<<߲GEE1w\5ʮ:ʷ۬vo|&>XOwl~Um(.l p^]Ui]ꃝ2Wz*Ill?Z͐OXƙ?pdo꟝!5DVE UV(dffsKn44^ϒ"\9Ȋcқ\<\>Zn-İTI g.1FWYZJcCU;'iMKز[VcQKk6dnS&KK56KX T[> @AA̡hnv  ` 1VAA1VAAAUAAAUAAAcUAAcUAAXAA1VAA1VAAAUAAAUAAAcUAAX 4/V+uAA' <:D\\ME@R[/ӦMɓw\. B1īʙ3g>}R XcN  u`z* mj g,X߼ysIMMeҥdőV5ZYÃrrr~}s( 97n ;;T>gW^M~~QZKRLMLL m\[WU>}:˗/gʔ)5.ڽn+ʲ`֬YCbb"o  h4PE>{xx(/2o<#yxxL4]0`Vnݺ)K.5Ȓ;*8[dBBB(ҧO[n(ݻM#$$D޽('ϊfϞ4mT?~VlW}Ç+jZ1b1Uk׮qymu^9VUF4iD TVXaWy$H A'h4ɜG ??C'׮]K~~>]һwo"## ^H=Ǐ'""}q1oNaaa2knҥI=JJJ ^`ܹx=$MQQiii\cㆣ[˳vod زe CA;c5((j|R͛7Md3gΤK.k׎l0|lݝ\2j/jsm]zcRQ'{o(888PZZJ&MLs"   ZYj޽=ڬQ\\̮]HJJ"!!6m$ƍ>}:...vz |||j-p1P 2G6꒕E@@...ڝAAh8F&LPJew=ڧ.ʿZƅxVAۨ1Jՠ~uV'==}fٳAsϱ}vtzjkAF$H 4mmK.)(=zPw(\x.t(dggYݟ{9EQeӦMr=( ,hTm-A h P'BϓO>i$_j߿:M !!\rssIHHãf͚*ޤI嫯2䕒4i6۽{mG:|EQŮ6n[WL7{liӦk$H*AZ{%۷O)((0\WRRbiӦ(W#c577$*j(ӧ˗+!!!CulJhh(j*F68p(q[bZkutwuQ궵 $*AZ7V/_(*Z[UGGG2JKKEQL[y:88(_?Pٱc(wUY7o*(Ί(RXXhVMն}vZ{괵5=$HYqp\|_|5}v]@׮]Β?}4=z0߰a!!!<Ҫ믿QFбc*wz)y8(QomV}^8wMmz |||l1~)ZyTfmkkTA gU Y9rry%??_YjYիJN@?~噤P>CYz*WV<<<300PINNVrsseϞ=ʠA{~AS~Ai߾]cV ݻw+8bN\pU^ٱc\tI'|ٳg,UV̘1oooRSSM~~~ǛW? M///&%%0w1'--#FTaXh;w4+gUhZL<͛oҩS'.]DLL U괋V\p~0|+Zu0w\Ο?ox=z4^^^8::x^]="##???e)@,&WD`(Ф̘ԧ . LUH\.U/Ka!/08{ *{U5SVa2*??06oLDD!~ĉ3l0mfTVĉټy3aaa===1 2W Azz:aaa[-̘1l޼pHZZL0777|}}dժU$''3gӒTQFqQI FoasR^ug[UK]Q>|_\\̮]HJJ"!!6m=BhhhÑ#Gɜ.;ժпǙc=B+蹣5P p$O\\!>66@֯_O@@ .4JrJM 8 @RR& lڴ ^h?8֭ȫoҢE f̘ah&M 7nȢEh֬s !!-[b ޽;~iFll,^^^$&&WZUYCFW]cW_.եb>+ꩿ655^{ydL4?ׯK[i&ew204䩥ϓloBYq@>6j<@7굲h E]&QxTA=T*U}Q/^ܠ0gs ^φZm5**JQTv$4ޠi $ܩA(*Fԗg6j1118p@nݚXx1o^ziӦqIdj &Я_?@JE(..YU1gk5g2}Ƞ{ڵ.Yy,7AFíV 00h=ךš5k>|8 'N32 *jDDWf&ٳgOv؁׮]3Y6kZ >OOOƎرc{ 3pr!=2J^ Ti̿/Zje&&&TbbbVZ` 7YXZː1cmv7nfe9{ܹI4G~jˌ;8:vhާJ2ymMH||<׮]ڵkXיPUj~z,X`o޼9ѤtRtZ-g,_)Sؤ! Z1cƐg}S\\iiifD|jgUdddXVٚQ 8=WWC~~V.'@)>/ PYڐ}ܪUPP~K}u֭0tqDOA[EuVZ2|pEV+ÇW ? 1bڴ^ٳ0C%''+NNNf׿ˬY#F(jZ V7|Ӑgbb+&LP z*VR9s(UIk~[o)ݺukOsT䤤ԙF.-3<<\quuU&Md 0@QJnݔKy ǵkXz֬YYRRұcGѱʼ*TOKIINN[F=Aj5z.~]PEIH] IDAT0M7{4m(t}(JnϻwycVmzZUOE=Q'tIw(j $HE묚xV7l@QQ6lCFE,--fJظq!=Xߟ4ظqٴL:;xb@7`Ĉ2c j53fйpssc„ 9={.m۶?GyַÜ6mZ2ϵkגOll!k׮DFF¬YLǏ7jE׊KVVFDD =Bۚ`15K!!͝IOݻut1NkGߊz;Y^N\i#Gڞg])PݽV+ͼy())!227n ,YN:pe~:[m=Z-}]tAŗ_~Yq~[nŋL,|Rj ['uC:u~kz5COA6Vprr"((hBѱc BV3d=j.;;[8|08r[6%]|_\\̮]HJJ"!!6m=,Bhhhcm5YiݱG tǐro-o~orO;<5&4\;tǯbx}5J_;v ? hHJJccc dpBt+W$::pc$%%jM&آˢExYnoI-1cD&M>oܸEѬY3Ν @BB-[dŊ$&&ҽ{w>S4ڵȑ#ٓWv5dڵ\r˗l2._̺uLV:t02KiXHLLu\+c.ϊkSSSyט7oAϤIϩ->\Y5(78sm3p4q"$&ҥdIU7a.͸qj`k{|1z^ࡇ}|r.^hVNJy0l0|}}qvv66dե*=Zf„ a7mĢE(..<322޽;v{_ِh,ca-݇ڼJPAÌە֬Yqtt$(({'Z5 &&}qM#y`` ?ٴdե. Çرc:u*aaa^O>5Ɩ-[0O2 m7k+ѳPWVKjܖـ~KIe~[f,Xf=l6_Lj+ Mzkh{Ah0jrr2lٲ-Zؔ?iiiQa U9;;3uT:vŋ #F 443fV1cpssc„ 9={Y+7^]jiӦz<???6l@QQiiif=k׮%??XC\׮]$%%Yfx̏?n8AC^ 6QgY+; =듲V 61f3yPWq{_+yJ-[Qme*38SfHJ])P/jAA63rhu9u:2i3וؐQV~՚M뭼ڴic?p@Yf ͣlzW[n%,, 777\]]yg9u#GкukyYUW[dn߼y3cƌݝf͚?\ypsy1P 2=0޽{=zY ɉ >l/..f׮]$%%`" BCCEϪ{u?6V\vL[bC~GzNgϲ3fȾ*ӏgmo7?'B#M-"**0 ^[Xr%ѸuU^jj*٨7|3f:Y~Ν;;w. L8+VѣGO 7mڄ?.dENd:t02VlaڵDDD|r֭<ٴiٴ)&X꟱DFF2zhN>'|bS~LSRSSYv-oaWF9c* y摜l"ѣVӺukŋhPL<$9{ &NZ BZT*uwo޳(ׯ_o V$4huyxxŘ[Nzo JM.\\o޽֭CEE°vZ)`BAAJJJ}wڰsN~ &=ǵhooQǏ5k`ƌv,immիW1m4>}[nutسgp%梽]q'"}ߞ-=mmmV}v-G50wXgw{)Fp<O& {`Z/*|ܻ <8DοΔ{gID4L⥗^Bu1== PTV(O[ܽ=ܼrJ>|Xz_YY VI㡇!44eeeh4ɁB҅~;t,Y7n ..o!t&ݮpE! AAAXt)YsBB k-;3ό 444`ҥ8vWߑ233q/l~aa!bbbyf,X0RpK_yl۶bUG9SN{ǡ_燀2燌Ah_l(Ҍv]&|qez88y~YeɹR8,x|* !8u:l9FJhqL0LjӃyvZDFFb}RR㑝 \.,ム aٲe8~8\ڿ.]½{O8믿n~˖-;w.}vtww`0>w\.ƍu'kxx8VkE((?ѬYZlܸ>EV#Ur#22b[XjbbbpCQ·+!?d?>ylNQ6ՋiϾy \~Ro?P*}ʕ+1f;)))裏,Ǐ~#c߾}O\.w^\[AP8_NU_^|8'm|m3'MGD^TTTZ JF3Rlڴ : uV[lol^ Bcc#rssݻsa˖-RC!**L~~~:u*Ξ=~Abuu5ӱk.)={Xӝ;wW_1`Νx7 PVV6$"""jBrrXz5*++Gyz/{+WGk0ގ7, e]ĠkyΛ7/x zPUUfBT7ߔ(Fzz:q~_p!< &s,Z;UoľnDDX|9(**hޢ7p8q"j5 jkkR,(O[ܽ={\~6ZC77huyxx8JJJuPNCMM bccގիWƏ5k`ƌvlwNCqqHP(0/_vt:Z[[qUL6 O֭[]:wu:ك\t hoo~$''cĉH߷崧ꐶ=:G`7"3tgLM7, NY6VӲ | O11"maqDDäVHIIAaa!^z%/݁ZJeuˁ3V&+WÇj())AZZ46{XXBCCQVVF( )]hh(:;;m`|$-ā_vh#h(.i3ό 444`ҥ8v"J@aa!bbbyf,Xs0RpK_yl۶ qDhh> B`u U}sIʒOXXj{ q0Bl9FJhqL0LjӃyvZDFFbҗyRR㑝 \.rム aٲe8~4P`ɪҼYf!++ ظq#}Q 1pףZj` Z[[WWY__HmcժUA@@yE9hf#Nk#9y})WnR7DCQN"!g0鮞dnA}P(PոppmAv؁jQSS8ᵵXbcƌ͛7zଵ $&&o޲:Ceuf:ס<7l؀3gbԩXx1bcc>B`0`ԨQ)رcdDL {-,J[vp@'0E>Ȥqqq僾{ L>c2eJO?46mڄ* TMBBB,.%=z* AAA DBB.^xYL<#w $''[ fl-s'{t?JrK,l R ???(Joλ{.PSSr󴣣@||G9i$={ֻ…^3߼5ռS`Siޢ^utn#"luP***ZJFTWii)6mڄ~Hb{uuuغu+-f@vvty۷ ؈\@yy9222{nܹsزeСCpݿ^ek+""",6RPP,$''K\3w:W\Դ`@{{;e%%%Dff&J~ IDATKK=RQQQ^Ѯ%al/pP=}} ƎR$)IWzxo9;X}߬>sga|U c+""/Ve2ϟ}==g˘ް999ĩSir0篪j| Ag0pp:N3??#_ܞN޲yKDDnnmY%"\4*/SVy^pp0U"N]=J+%Xx#{u:4yb{^&ay6j?L矷 9ze@`޼yزe P]]7/DGG#//s碭ͫW87<;H-8aΜ9Z6(..UUUhnn/J%|Mi΁h#??'Op `zh9s-BSS+c69bBHz΢y+JmĉPP(JXgꫯCcc#r9P~uΜ-pq<t}tܹ/&6l̙31uT,^&!|||`00j(sر2`}zHlOo[^m >Kk]""7m*?p&"t+ @Dx#ۃ{u:4yb{E&AV;}~Տ;8z.2{3Z=$r7ȓݲ̇|̃kyΛ7/x z|*\gFFtR;vLjirg 33~"&&7oƂ \>ha* GAzz4W^mېj9Nzcş9~3X_ W"Os8\IeY jUz5xqVzz:VZ8P]q8I$S+GAigQit6R^r^%anC-7/s(ID4ơ^2lPC[/ j\pYYYZ'M߱c_ׯ===ZXBJс1c͛þg%k-C6?LCqܰafΜSbň~ !QFycǎEGG7@0<'T? !۳EX \ hE/}J`r477#..r|Зooܸӧ{l{zSL7駟ƦMPUU~IHHGBR!((HHHŋϞ=ɓ'{``Ò%KRϟR\.ǒ%K,.;“=:݇'N 99j8TJ%T*rݻhjjBMM -ᎎDGG# )Ip.!sYZ)/Ejeݛzl?iޢ*fXkv\򛎈OAj* fP-[شiua֭ޚ5k%~xo>cۃBcc#rs$///GFFv ;w[l:tQQQᎺ֭mvL} ++ R$GEDDX\Cms}(((իQYYdβwlQSS+W ''ߺ˓ 33(--H]GEE9QrK)C!:#<#r)N`jt'V};GeWhF!އ19x'ŘH~Wsp2 GJJ RSSޞeζqhkkÜ9s4"mjueGD8TUUP*x79 .|Ȥqqq僾|{ L>c2eJO?46mڄ* TMBBB,.=z* AAA DBB.^rYLO8dA#;XSnlQ'}[$]߷UQk2vlZ m0qދ:Uxn>Ȼu+L#%%C_xo2fgg{XϜTVVԩS9zWj5rssz`= zwY<)88}Yt=bB+աOslY%"A>D߳_lY%a֖U""e= ,Hh}N'Mpdel0'}j%{rTV""Ot˪:[s޼yxO@ɓصkke4#::K.EXXX^sE[[۠Vc9xy4qhkkÜ9s6 =d*..UUUhnn/J%|Mih΁h#??'O `";Μ9E%XFX|9(**h7p8q"j5 jkkR,(O[ܽ={Ϸ\~6ZC7huyxx8JJJuP &=審All,۱zjwڰsN:V?~<֬Y3fڵkGssݲJc!;; uuu^q٪NV\zӦMӧuV>+:{ABB.]\K?1qDZirfu(ܡ @{#n2P01^Zdд{`Ҕp,P8>~ͳ]Aiǁ_S#|| X8~cfz8~-b$".F FJJ K/Y| ԺT*V-ޞy~6Yr%>,VEII Ҥ12h4@PHBCCis?@CC.]cǎI}e_ (,,kqU8pV >|*j†Ⓐ;t٫B`X`˟8jJ‘#G.Wm6$&&:pCgg'BCC=/Pg`OSşAh_ %Pܻg ,;lfkQתToս|CKoꋎ`ID4bUF^#G`„ eV߿sk"22۷o\lri+!-- ˖-ǥ1۝z@ZmmFww7 ysEqq1bܸqՋ5k弤q IDATP[[7Gu,EDD`y#glmm^M?\e*g}}="##-UV!&&9KI@$Vn-ϞRY?sz?*ߴL޼uz_Ҏ_}T8Ph(Yk:gK7n==^)SOcӦMB^^ހCBB,.=z* AAA DBB.^Mٳ>#4iΞ=<ÂM7,ǚZS/'xߪ=Kzmnn:rجEO5([hw7/:"n? V"jT*h4ABbӦM סc(WWW[b{k֬dggKǾ}V.466"77P^^ ޽8wl"?tN(((@VV;عs'x d29=[YPPիWc@銋n:,_|""",61r>l{kjjpaRM #//OZVRRLdff#uP1|toh|I``eO|c9uEҕ};XV!{emb?z0k<`zU[[d?>RRR:-cvvW J:u^YoyZFnn?>G=Y \,DDVu:tOg~~G==g !"Ph`.o0Xvg*~!`* 1x"""waU"""""DDDD`*1X%""""bJDDDD`1X%""""DDDDD VlsdQF9ѣ阎tǏ7  زJDDDD VU"""""DDDD`*U""""bJDDDD`1X%""""ϑ۝֭[Lt#6]GG<-DDDD`*1X%""""bJDDDD V1X%""""DDDDD V*U"""""Yi)""""8U"""""DDDD`*1X%""""bJDDDD`1X%""""DDDDD VlW:<5ƺ}uúvk:ְݚ.F]e]5]k٨kze1X%""""DDDDD V*U"""""DDDD`*1X%""""bJDDDDdCkdEk5X׬krƖU""""bJDDDD`1X%""""DDDDD VU"""""DDDD`*m~$+cX׬k5YD$a*1X%""""bJDDDD VU"""""DDDDD V*U""""bJDDDD`6?z1k5X׬k"e1X%""""DDDDD V*U"""""DDDD`*1X%""""bJDDDDdCk=Ɋ5uMk5IزJDDDD VU"""""DDDD`*U""""bJDDDD`1X%""""ϡdEy uͺ&5뚈$lY%""""DDDDD V*U""""bJDDDD`*1X%""""bJDDDD V& ~-kF``1X%""""DDDDD V*U"""""DDDD`*1X%""""bJDDDD`u=ػw/t:hߘn™3gP\\/ !2c\|9^u~Ny߉uZVJ%J%~_᫯ºuXK.7nC=NDDDC|7(//?.tHII}m6ii&we(..VNh̖n|ӧ0 !Í7*7VWTT"@L$B@~~>ꐟB/[bݺuصk^yg:}seBee%bbb)%""z tCBBK/$,@)7~3!ٳ~;iYMMo߶̖8@CC4o̙޽{R;\ܺu ۷owGBxx86mڄhZT~Xk{Z[[נ gF^#G6""\`UTLo`@bb"ܹoن 0sLL:/FllZ1cƠ2S:X`sYk{LbН{l% Fo^ɎpDDD4U'N@rrݻhjjBMM 1eٲ~u0~wI&-Og?JrK,sFl[gutt ::tz[6;F&?wdRPPիWКv -%-- ]]]7R#po^{5|WdRWtβ/#e{JJJLSHDDDo5ADDDD#Jpp0G""""*1X%""""bJDDDD VU"""""DDDDD V*U""""bJDDDD`*1X%""""bJDDDD ?V d0iix'""2a*Y :MCA&fݙ|̷Յ?3g JD VɑPzbGR0_|?PZZ5uGD`8uyeee8u""",Þ ]]](//GHH<ӟ⣏>BWWn߾G"11ѥ6n|ӧJJJpDFFbƌAIIݷ8lŴ}nܸT3piz9VTT[[ж2uT|g}6Z퐶 8qI^hhB/"]RRHJJBZ,뻍ޛ~yڵkB!Å\.O=t+//B`1{l!} F-Μ9#:$=*ZZZDPPs 2L !$..N̙3G!_W@|GR}X¢\4GvVY? !X|HNNv8q`O*'N LR'Oz^Z޽{F-Bܺu˥`_]]]vtB!Kk.'|||\SY̙z^ܾ}[DEE9\h`[e1~iZfoLyVY;!AAA"88*'N ?3G`r0|}}^v&򌍍3< ̘1uuu/1c3ͷ;i$C.cҤIns___[Xx1~8Tw[ϤkyZ+ǚ.KAD`\~/8'|5k}~iKܹs-mmm2e +L^垞>}k׮eDD4l8VDDDD`h8ۄ """")x1X%""""bJDDDD VU"""""DDDDD V*U""""bUV˸wúSΔE1r8v(9K,[3~:BCC1i$\|yXwʙ;LTplڡ VGBy-P#"""/88}H )˃ V`-_rv 4 ]]](//GHHE?/m68uyeee8u"""evGZ?OG oѣGоt[4-33giw:֗ttvv֭[0}t@II ܹH̘1===())q,DDD4r"Vֲ~[=Z B!DRRHJJBQ^^nrYLShhB/"yY>C{o?ky\vM!Dxx⩧UUU. [imƞ7s;a/'<)KT*E{{{\~鋦ii7럮*~7\jU+ܹsɓSJ)3);vl[}tMiܹ1͜93_>=?|`@L2%UUUns7}4a„4~4}t63i;7nL)TWWRJ)mذaロ-Zs,X՗W^y%{wRJ骫JݻSJ)]s5_>Ǐ|0~)ҡt_}AGvȁK)lٲ4qns=7?ӺuҺuҊ+y7p ǧMQ,X,FcccL6-5nnػwoի{#"1JR1sH)ҥKccR,X v_>̙֭bR)oO|vZzuJx׺~7Nyem7uxꩧb߾}]5kرckkqEE{{{M4MN*Y'`Tq\*]X F@X>Bg޼yiDӼO:STjlN:稩)p Nh z(~U" G:ujZreZ~}۝uٛv͆G?Ѵe˖\rI?>f͚*w<'|~w_ּ466vyg~k׮C$:Yty[׃ib1;us9'Q,?ǎ̜ujmmbk׮}C]8qbDDCߚiP՚4iR ɓ';u;vMaX,tĉ^R)ۻ>iP[mcǎ|r?Das^NgXs6mݻwNJ+.ƌ3dVu|v޹]ּn[o51-[6uM睇uSo&L+Wx饗uUbΦL=PDDUMӴV@3ذo舎%V/۷o ."awߍ9sDP.S_auر=:N s̘1q#<jW +xy8o> W\qEJXreJԧ>U;yzZGKKKsԩSc…qѸ;*1g(J]]p MӴVO5w"DD׬~鈈xcĉj{nkڵkc޽qy/}/9""Ν[z56ÇGDUW]q׷,Zǧ>0aB̘1#"";g|E2p꼌3&:^xq8qD hX;yz0_׻:oǿ2/BDDms΍{ MӴVW^R)^{n^q;é}nԩSOž}K)Ś5kرckkqEE{{{<:T}2eJݻ7z衊[o5mmmz^_cccJxc̙RKv)X`A޽MQ,X,FcccL6}eS IDATΙ3'֭[b1JRl߾=͛'LGwu7f͊_=JR-;ky~7~GJ))~T7Y睇u]\b޽q^FD9Ͼ;g:::◿ey|?CwފY4MVvz 7MMMoǏg>1zϞ=8q".Ҙ8qbW:ƦoM4aU*^yh.F\?Sݻ7~ӟƟ=XTWW[MӴ3 V hiiIo:ґ#G[ojjj?T,M(V(Fǭ/~4o߾t㏧ &S8q YF|СC#ҤIҞ={Ҙ1cҿW*$QʝU`;x`WPM)?i`.ˬmذu:uϜk,-LBsvfH7AP( %  **   #ĸ~=kɺZ²hښ5kF˙~1<lf~H^U1x櫙z|g糺:VzМYͬmٲeF.y9:ڰu:u4cƌڮ]fJT*k^?ٳ3k]_t֖9/Ί**   ** @B---)-Yڅe|юř5k֌;2k;0zڜS>C{ZZ{泺:Vz _ͬmٲeX簿F.Y3U5u4ss(睙[fvuVB*# **  **UB!ZZZ'>'}.kk֬9k'ˬ͞=;vgߟYknnάۙ+2V[[[ؿgO~~cccjkkm1UUU߅^~㏧ /Ƕu_7-[VX`;|jnJk֬IO?t W) B"6miӦ]w=XwsNs=]Q|Sٳ[VW;N\ve1vخSk qٳ#"b۶mL+uuuqWGDġC4}ˣ&.]~O'  x6[FqɓO>/~:M0!vwc~* _BWu?w|魷JUUU] f}ݗ㎴{o);ZjU{=ܓ/Ud* b&Q O`VV@X@XaaUVV@X@XaUUVV@Xa""EV},mܸ1>|8Je˖pnOm;HN̙3վ/[n%m߾=JgϞIP(DJI4mЭ)""0aB\s5vڧNӘ1c.""vѭw~~0=?AӴ~BªikmmmGmڴiGؼys\|/7n7|3xnrCgv}j=bܹGƁbҥR뮻.^|([novWR;w.?yj? tǏǎV;|p|W_}5""J׈iªi-_<"":::ӟtW!""ژ={vDDl۶[ع߿"wH ˗/yr;#"7M]]]\}qСH)ž}""K.ǬYb͚5ݶ[xq,^8""^x>goĝ DDDSSSڵk~3`O05iiof͊<><@X,wĉkGGǀBgVkɓ'w{|cǎ(J;vl}oڴ)""v+V3fLĤI""?[?V#"{ؼys̚5[nװ:nܸA!4MX4Mh1cFDDt WzjznDD̙3' Be1c_+VjSNGv{m""⢋.bM P1>l7ߤ &|#)?){RJ˖-K'N1Z%w}7-Z}{KǏO>`SJ)]veݞ`TWWRJW_3GM)4}{/K?ӔRJ?T(\$iZڿ6^yhoo#GtUԩS㩧}EGGG޾ʹz뭷޽{-V^;s̉uEXR۷oy^{<^^?;};ӵ]|}ݱk׮xwߍ~iZV hiiH_親`g ;sgamNηP;) aUUVV@XaaU2}sKov:qD:rw6l;FZ1WU(e;zw矟O~:r Vn;TAo$LS` ʅՑ.`uf`Պ _S[[[W}ڴi!T,SCCC6mZm^~qo'xb}iooOMMMiܹWNR)555K/W_oҋ/bjooO[nM7|sƞu}Ν.s YE]mۖӆ s|GIi/#(FJiЭS^mQSSK.R466FDŋcmoݗvGDěom}gzǾ}""K.ǬYb͚5:y_x>ǐwn{*5s~7""n;!""ژ={vDDl۶"ׄiikB!5Nƞ7s;a1ۣGFDĤIP(tONֹii?럮*~}\jU+ܹsɓSJ)3);vl[}tMiܹ1͜93_>=?|/``ǐwns^LRUUU8=NYݸqcJ)TWWRJiÆ C7Z}ݴhѢ^`+){tRJW]uU7o^mvޝRJk}/?~<=oO)ꫯ=z4Ӈ~!-[t>s)-[&NF|O6-X,FXƘ6mZ|+z뭱whkkիW8FDDcccJxc̙RKv)X`A޽;:::=7}Ι3'֭[b1JRl߾=͛qz(Jk_:{o;=e(Ɛwnkkk?q;v,6nح6uxꩧb߾}], iUmU^gӟʹygϞ|}S53 MO?OҽkR`V9+`Ƙx3yRSSӐq>L8wMMMn`U3?N:5\2_׾u0:@k#fʕ+,0p|N=ܓ>^zS/RЇ>nV;^:Jsnľؘںu׾ѣiҥ=;9/6nܘ|O4wܔRJ]tQڶm[jooO6liӦtȑښ6oޜ.pSJ)ohjjJ^zi,u]^|T,S{{{ںukZ}g_:W K%>ŋc //_#RJ1رcGWh8qDѣ1iҤ( S,{|SvmmmeD^? 6mݻwNJ+.ƌ3~V5M4rauX߳Z,͉'N]Z*]qiܹ](;2eJNǎ;ӟ4ҿ"g߳U馛ܹsSccc9sfZ~}z+`3矟RJiΝ~}hX]`AK)ꫯiǏ=RJiC?I/iѢEj[l|RJ)-[,M8U?TRJsZΚxWgJ)޽;5\to=z}tn|s׬YRɷ^:JRkq,{wމ~;뭷{XzuƏ8vXlܸ[mԩSOž}ٟ2~N2%=ЀDcccJxc̙}3 ,ݻwznjs̉uEXR۷oy{{[y+o>j=Pݻ7m1i {V ͈x뭷θ~p tƝJ݁gSSSp ^4M4MXsرhjjOyUiiCVU铅h~*='S **  ** 0:3ǣiIfm/c_ˬէzP  **   ** b&ѣG^5gV?fJiP( %  **   #8S04f̘]ܕ]jnnά 2*)*TWWgu6k֏:o|"에],**   ** @q`hܵZmͩ=gyiNYhɒ0X[𬝳g9+3k?-8qgaUUVV@XaaUUV _UP31-YYM)XvFsv힭ϮݟG%Gsjnx;VjufT*X`P ;\**   **0BT hii1#O4VammyA4W~fO[T#W^Yuˬuy6yfs}f홯VgJyT(Y`VV@X@XaaUVV@X@XP(DKK!3k555Wdj,1ͩ}k3Vuֲj+hɣe.r/kM9зB*# **  **LR*nqZƚ1rjK: ZS0"y'wVV@X@XaaUVV@X@Xa3GTӄ IDATf̘Yk׮ZfjkG`>]KkgՖw+gl ;   *   B b3fȬݵkWfmwZsfs>\s7g.1C^ssy=[YrP( %  **   #DUP3hNvɒZm;C={uu_[Ssyluڞ={2kRP( %  **   #8SP촹Ďb垽k7lȬ=95vdv,ޑYkmm?urk[nMmYcXR]N)ՖyYsKHvzƯglOf~mKBUF.aaUUVV@XaaUU! B 8ɣ9-U5gjv9g{K-gYڅ -]S3k[l1gB*# **  **LӥRKKg䍡y;#䬉T?]Yץ[(k z!T>vSΆ3;   *   ƙT*e֚s֎9[ssn5%1{Ã3K;G k(0!e]bd_cޤ  **   *g ``ϩ-Ϭ}yɒܜ]-súYٚ}Ê9{ISN`y,@G\Ԏn˽6k+/o<   **   ** b&4%emW[nvGw9Ux[g~+J%V-٩~HZ}S5B wVUUVV@X@XaUUVV`g `xTWWg>qcf&V[[Y{ₜ4gjd:yGU;vdZ[[7]ppy̑-k׺ziA}a$qgaUUVV@XaaUUV 8SãT*U|۷oϬmn6#\kknjKKݰyBksgY}}9Z **  **o)ӯT*Us9sjbgV N|NWj\9QUUVV@X@XaUUVV@X|L_R$tٵGsgj˨UdD쭚sYe~ٯgN**   ** @q,q͙%K2kKj\4jyZjyHZVZ3_x/K0  wVUUVV@X@XaUUVV`* b&UuuuYd޷"6g#gb+#̗י_]YۿuX+J^0*P( %  **   #DUP3@3k555e5u]%Ն@}ssfOfs֮֊oW+J^0*P( %  **   #DUP3@EUWW]MM͈Hh:R*P( %  **   #8SP(JúP8x`YUWW*͝UUVV@X@XaUUVV@X|LTVT2 P!  **   *   **  **gy楿J6m2vw7'ִso~3?3H)TUU5lǼҗ[`gqgui|z뭷Ҳe {gmX; AXtА>яv=iӦtwkצ'x;/=i[V˻j'7ʹaÆiӦnwojyMґ#GRkkkڼysSJs>=z48p -]N~ϓؘںmsLUUU=w<_>= /mosoe˖+V??mc?i\%vO/~駟N&LH);#ݻ7=c=CokcΜ9q]w 1;Ն?#mg|+1w~3oʽ63ޞ_}L0!Xvm?sFDė% BVF;so|#q8s"ӦM8rH͛/R\wu/FXغuk|ySVGƁbҥ}%unW,X,FCCCL6-pz(JsθρɵrCw^zg_~96nof<ޮ䭉3gO~(Jz#" .8a5|_p몦 V_o߾v=uggm۶H)ž}""K.ǬYb͚5ZWWuCٗ/ŋGDDCCC~ϓ / j[?3y7v}ݿB'>/u/oM]c4R)""&L0చ Y@?]|yDDttt__ǧ?~ĉ#"k V}>NjbN'NRlڴ)""v+V3fLEرc{c3q|ɒw%m[͛7 ̄Uj4wC&u]OQ /`WXQұnYLu,_$J/֮]kx {)3[= _@zg=}L2SJ闩9j)MU6Ӝ8s]l>/^Zj{v߾}[oNvjC)_j|)+Ss=g[oey{疴z뭷}7eAܺ\rݹs'عsK %YmD"f@Un^_>XWh1=#I V Yx@z+ W->@XpHVU$ YHVU$ Y-5\{wߕ*.9׻M9VSS4SiD"ߞt '褓N޽{ڨrRHtZZZJݶZ ^SzH$Rd5LB9uid5,HV|Պ\p_ulgtZ\X~qiӦM6mڤ;n6Ah۶mzobuΝ;uO(߸qvܩ3S}YjllL;vիKjt%ԟB_yuYa-$sK/|>7NٟouQIVϙ>Sfr\.7x}r߁.[F"U YZ[Wk^a}_c9FtQsEM{\5>/2]|J:suVW|<*y}|v.=XeK_;f6/m$I$_dhhH|ϪUuy$I###?_+V͞={$I^xai?;h+%x i5HN:餒P8nggt~iIҺutҊ_6-nWujy??l:vttԦi{qMkR=;>}\.g7n8ZNm>vsd׮]kx {IIV{챇7>]׵O>|޾ vŊ%Hgƍm>/=gml~ 7_}R{ݵkkT FQ~߿駟n_|E;>>nmeA*v*4.ngG3跿N>d&_Tb$N?饗^ 7@$X+hvڒei?Zk8?z^yQ\`矟}?g}}k]]]*·oZk|]9>|7&NZkz'ykk~eż]߿Zkga,Yb=\yyG33IvҥZks7& BP&G}]lڃN*?cJG`ѢE&[F.T>FGG'=wߝ}3Zk-[f#HY%K&$l?+y!+ɮ\(&sԾǰdu~&LZk={>h;;;QG5zt>;HV FHVkz蜷ywk|\hќ=RT2$kgAz 6.w )xӵ$IYT¯ߧte]v.bis9ںu+Rߩ· ' IzW5 *ZJU$I'tR9§~Zn:-]t=sI矯+VLx6-n75][8/Kz衺ճ!}ӟ.x ל3? 2S=%iϞ= /zw~z]y啒45ܕ:3~~ܼy3pe7n|޾ϞץQ 믿n:T<=\k.FFݿqG%N|>owe9+ɮ]־vƍ;/hmJ`||ܾn*,u3ݷԶ IDATors:Ld=Xo>{뭷y~N/3S]jݳgϔl2uO>iGGGm>/]bE>y4ܙmzݷoߴAD׬ҩG?Qko6u2l}F֦T*%qxL+!DH$b%ML&3gDe}cY2}  BHIJZUv\. cǎ*te,jJRjmmUھ};+JDcys*J31&  XY*:e !YU-U$h /2̴ohFy[WyBP<1Fսly2(3QRc^X(XYX,&u庮feBh46R)9 4000qyTsVxkht#VV(u]|>Aec+]F_ϽϢѨڔJ8 b]BR%Z}d2Y2z~}L&moo}ac J9 Vk]*)رS2z}F|rR)*m߾|->+|VV45XjRF_gYcM$ac J9 XYBNaHVuBd$*‹L&3&4sUsPn!ey5)[hy1Lz>9XYX,&u庮ftBh46R)9 400#U|Qq(r:&ɚ-~9<ɤ3иR1 [hoor @;v(`Tl!K4˕Jڪ }vVV|Qq(VV85XjR࠲l唕{jmFjkkS*8 @AeYԼ^EQ)JqA s4GeǨJ9`:Hǭ+|߷===6Lֽ^dZ9WdJl7yDqخ.i*WKK1joo8X˚8^4UGGJ8W`ΣcTvG0qXY C$ kս^yg16HL?ΕƘDeh.ەrA̴5ώy1Mam_n3~.59x(@b1+u588l6gLѨڔJ8 |Sʝ>ws9ӉD"\}d29r}Vj$Ik}o+w^7]HKA [hoor @;v(`LWV>f[y\|RZ[[oT+ai_n.2D"a19f#>>!R:&B_---2ƨ]hhhHcccU)??QrFѡnR)A>r9NlrUhH$c=ϫIY̥YcM$A5H$20$ܺ E UdMcqE&MJ x<.c<ϫ{P>kq\>>!R:&߯c.q444˜vnC4UGGJP8+QH$c=ϫXj]ϹωZϗ0ͳZ!L}(ydxg16Hpnu[YUR$ V@Un]"Y*@ "L& zU%(#jRVn]9QT}\iacy1tJ*0X,&u庮f522Rrp̉j̥ZJh46R)9 400a:D"JӅ&ɚ["ss(#7FdZSbǭ.rg,Ktt ZZZZdQ{{АƪRVn]9QT}\icFѡnR)A>r9:8*ADcy5)+.D8D5RYsQG<ϳH$;UY@(E"`!YU-U$h /2̴o,lu4qg9Fqcy^\$dQ<oa/VVib1+u588l6tdY4U[[RQpU>˙רPʜGD"˙DR)sq{{\U.SڱcG,sȲh4˗+JUAhU]Ye^s9VV bF"y^$y5D"1Yμ&1J+g9fXY2Yr**HVU4ŅLf7mR>,$ܺ FjkkS*8 @z*㇅z1U!|߷===6L֤~i~FdZy \!qY?>>>!R:&ǂ߯c.q444//h4uww+J))˕ya+hL㰲JD"y^MJycĴ5G,shܕUY5fDd[W HV@ i,.d2Ӿo@xy `!U+w* #*VVq>=ϓ1Fxkq2XY*,u]|>Ae+ >]5F]ѨڔJ8 Tmz.#rx*AT!|߷===6LΩ,LvZ׳uI&?Fi{`9Qǭ.rg,Ktt(CZZZdQ{{Аf, >]5F]Ѩ:::ݭT* ק\.Wgrs |N:rUf$ kͩ,LvZ׳Ru<cl"v|&KDWVfBd U$ V@Un]"Y*@ "L& sǭ:%čUy1e##*]ePTxa0ͫ_%>Q}F֦T*%q4<<<9亮\U>࠲٬FFFfa=&Evuu)Nevtt[TJAO\ns_---2ƨ]hhhHccc3o-Zץ S_} SοR>Q_㰲J1&i39H$1z7cϴ]eՈZץ S_}՘DFϿR>r*Pc.L+ *@hd Y@X\xd}Z@=p*w,#B]VnK5ƨQPy1tx TX,r]W|^f UYm`*/Fg@u4U[[RQ0HǭD}cdmP~5<#0d2i{{{Eqخ.iy jii1FrGCCC UYm`*/Fg@u4UGGJpUf$ k煺60jQ3<cl"C5H$20$ܺ E UdMcqE&MJ x<.c<ϫv'|uyy1tx TX,r]W|^f){j sԺ Ph46R)9 400a#$"[%J&ɊmW} sԺ <d2i{{{Eqخ.iy jii1FrGCCC+{j sԺ Ph4uww+J))1!8+QH$c=ϫv'0K"l*׬ "_r**HVU4ŅLf7qg@(q7 1>>!R:&&߯c.q444)œK%CJR @}}}r bpU Dcy [F,̹4xg16H0V 5H$2r m IDAT0$ܺ E UdMcqE&MJ 0x<.c<ϫIYuuۛ}lkZ3dQ<nP$IXLu]y *jdd*e֥mo>k=ϢѨڔJ8 9@$qAğ}cdMʭKc[}s%Ik}oǭ.rg,Ktt~v9!UܺԺ>gY4UGGJ88A#HXc<&e֥mo>k1<ϳH$8tekVJHd/XaHVuBd$*‹L&3훸3,\u`N+>(Ɯ|yXYHѨڔJ8 g!u]|>AeY]Vjz}rtHǭA#L^>o{zzl2XYQ}6{z|<"x*˗+JUAh%u]r9A;vW)JYVVk9>_J9*AQFxg16HL}H$c=ϫXYQ}6{zqrzeudCʭZ$ YHV4^d2i7?Pܺ `2oWFZ3L怕UX@b\וT6خeҟgK9F֦T*%qx x*A mOOM&u߮eҟgK9x$Ik}iǭ.rg,KttLZZZdQ{{Аj]5?k0͗Z/Ls>CJR @}}}r|8MqVV j$ k}j5Jza/n_ycAsXYUR$ V@Un]"Y*@ "L& zU% X<1FUlrj݆Fi_3ԓ9ex< H,r]W|^f){rj݆Fi_3ԓ9?uY4U[[RQ0NӈD"fy"q֏ObNIEq ill-u}POehTV*RS.i㰲JP#HXc࠲٬FFFjj%L\ ]цh46R)9 400a&4"[% ۞L&j%L\ ]݆d2i{{{ (u]*)رc+u S0_j!jJRjmmUھ};+AsD"a1u S0_J/x3mm?r,˴yf}{S{{;g) Y$$d;8{WۻkByww}{cof|.Y^p/mٲŞuYvѢEşR6SlڴZk\`矷)~Z?m\yv~=ڵkKZ;c%K5kw1izz?}yo-Gg2fj]׵=МGADD$Z\.W^yEwyG}T\NsOg>o,B޽[__B/silllֲtvvJlgwޤvUJSo38Cv?}]q3U_f?-[64O*kppPX&%+Wq\.7gի5>>>[ny睧SN9E'teJqjtttʲB:W3SҌe6U_ϦLZk%m:ꨣt!N*cp |*r몟ꪫLkttT^{WJY[~7ZZZgٍ7ިJ{tEX7ؖkÆ tuG,jD"ߦ'*H'X HV@ d Y*@ d Y*@ d YBNV3 = *9V+V?^>9v&)@Yʕ+kPq뮻SO[hSv駟PץK7tSȹU*uLVkO׿um޼YW\qݫ);zz'C]?^S6lPLXIZhU({wV<^| [le]}K_҄^O}SzꩧsI{߲et"ȄmɤnF}Ӟ={twj޽޽[N=TKe=>Owoj߾}Fݭ|>;OW_}qR*WW:S//ş۾Wd2[N8N,??sק_~yֺ4N{wwVZ7SU}7Nϋ.HW]uN:1zŲ[n~cu?9L}~򓟜rw+Xvxnf|.Y^p/.ymM^N-[س:.Zh>鴽 ?  HĖ|^{W[n՝wީ?~{}Qr9s=%Nu5k_}QH>Ȥ8 m۶Mӟ8 w.lٲ emۦ:KtO~IǝjUOuԟWkk?}r-ڰaz-}mV,~VLVx [3fKV,YzKK,њ5kt|>O>yχWo|+e=A{~_jʕZd.R%eʕZxV\]v;'?.b?r۩zWկ~Uԧ#s:J+Wtm|ڷk.}ŋ'%?uUWMT+o袋.ҥKjժI۽:;g?l٢M6M9$,Y™@%mٲEy{&{щ'}{9S]3[ tuo=iGyDvۤ91Sgg[nEu]ŲzJ7o$|7|ő_>`7nXf/nڴ^z饶Ǜ 6-֭+v^{駇0ե^ `5Uj(׾kVg~}صsv-*ϯ|+OIPH$b5-POHOf2e(G%jŊzKg9.|9NV+Hʙ38Cwuz)}[R4mʎ>Ӌ k]tM7U{6zb꘬ʉ'ڼy ݻWW_}uSvիO.~_7]ߧR)mذ8& կBٻᆱz/N(߲e.2۷O_җ&~SSO={NcW-[kVD&lL&u7}{٣;S{-gzuꩧ꥗^w],{O~Ro~SS4}ݧnywymLS]P:h yD% NHbH0ڨ`| DHPA% :-B@SD(-0(/hQ^L+ùRP{^^뜸8={=zO ^ׯ6O"6_bN87jxlyFJJ PQQ!r7vv&R* a8}4ZO{KA|*fُۛEFF&^lܲw5&HXbb"koog`رc,((ׯedd0P233YyyXaaagkkn_FF+++t,11`l OXjjC;{,S*nO%%%,!! BeV__/xj4mows; @ ټu}VIHHHHH>nZfTVV"88;NsU`ZbrƢ?~@gg'>DDD8- ࠛى(III Dtt4zzzx:{*R`4j H{rzݻ<_dd$8tQPPɄ2YA?/xFFFr;Bnn..]Z r;AAǏ \Q99 00);;cÇH$ŃܞpU( 477crr'%%s'LNNBPpr_L9zyDY?_Grr24 UшB bB ̜ 6 k׮嵏"$$ׯ_GEEJ%Kd2Brxm_d2||| 000v`Νvœ7nfEEEHKKCSSz{C&- ^1T*š+_?|T ___aÆ s+v}}}0 hnnvz! ' V644zsl6W=z;v{F}^SS@ nNm۶h4B*m_b1 oc8A*իWZ<󫭭EBB lt:H$ܾ}gӕ/xrzyyyhllҚhxׄ95_ٌ|TUUq:Ʉ@II \Bw>AA,]`i&vʕ+Yss3L$}\-ٳgƻx" _/(/&_~+uէOPWWC 山1CVc߾}Z8x::::`Xjɓ'%  w״AA3?+  pb\:,dvڅvmz2B( X]#)]&d… ,%X}©S둤S{.ž  X($ Μ9(J "''gYZPΝ;ڗ`~z>..z+Xh% o7)}5 ޾}<}Ao0/`xxGq7tiii0LqOVuVb(((uTVVbppb`ddaaax9][[RSS;;w B]]aٰyfѣxTpX~=Ο?ϵy:!44+p lܸV/_gUf{vv6RRR044 9-3ٔJPTH$sӧjS]  <}V1~,22555dgg3???疽k׮1DY{{;+--e]]] +((`ǎcAAA~,## B >[[[tS222XYY+--e +,,dw^}:8RSS<ٳgRt}*))a L(-[z|4Shkۙl [XLL JBBBBBYu5l6immt:[6WZՊ\;/>66#_DD:;;gttt ""AoXπTNDEEqHJJB`` S9JVXD"ݻ#..wu"##L&a͚5|Oާ322r<͖ۙX,8trss___Nw%j@UUC Xܸ]?~P(HOOLLLipg1`IDATLٙnu>D"Ann.)) ߟslAA,b%%% fռbѣGرcݻth4444Vpt:l۶ FR555n"a0}v^ǏcddġR^j<_mm-`0`SA"<|OާdzkCcc#Fû&\ݕͩf磪әL&JJJp bM67ofW\ɚ\.g"軏j@ `lϞ= 6ŋYxxxAy1X,V>}R(jjչV`6O/4[޳!TTT`hhi܍lɦT*JD"@ pC~~>N> VӞA!Yf';;<]vI$Yii)bXAA;v +//gL(LV^^0VXXꠛꗑ8]ii)KLLdXaa!۽{yΞ=˔J%SII KHH`Bmٲ׻勧y6fF[d`0(&x6oݺbbbhU%ϪۯĠfNkk+&&&ܲj*XV梸y񱱱Ǐy"""ىϟ?z}8uvv"**kFRR޸ΞʩT*FhZB$a޽qqq{C?O.N7hd2 k֬q|>=i΄bCxrK.AVNAbhllB@zz:WTNgbbbN38;; `tΘ{!$ rss公,\z ͘IIIc P("&\w>S^^"hfii)z=Ǒ Fbh4| XnŪP(8B!2334fڵky QQQRz{{~|2 Br/^pLd2 pv===عs'u0ƍlGQQ5ސdwK3$''LJW>y*iqʗ}z<>@*)))~cccذaÜʦnG__ ^o{B b ())^1ͨ{=Ž;p=Cp_@ [a۶m0JqX ۷=~###E|JxV+Ojkk???:ot|>=\^G^^섴&h45jlNl6#??UUUd2PRR+WНOAKoXmڴݼy4rJr9D}}o߾ECC>}7 _0<<#G8؛yݴ4L&ڸ'~Pغu+bCP`ݺux *++188ȍcX0220<{ ՜ xΝ;aٰlؼy3rrrpQ^ j5Ç?Ç;حA||<Ο??:R~ |Xҿo ?Or8>|+J%;HOOիWyLlyRįs>̖ۙ8v87;$V\Iw;AA,b111lFee%yڊ t:lZ V(..Fkk+w^|ll,G^tvv@DDbp~ڻ@DGGi-* FZDػw/G\\޽EFFM'::0L(++Ú5k32Ogdd/r;Bnn..]Z r;AAˤX=~8P(ә1cl3ݾ3>|D\>>bɓ'PTNCW'ϵRHIIÆ 4wW6v;`0zC(ҝOA˭Xmhh@II z=FGG4lFuu5{v؁{!== :۶mhT*EMM۾b l߾q(bT*ūW`Zy:OW[[ 9tH$}6Ϧ+_񬭭z=g'555A Wswesjft& ---\r| X*bM67o:}/Yr%knnfrD>_u={,x/^d?$Kɗ%?s D,3U>}B]])P\\ Z}j \bV]033XLAAX,fξ%̶'A17bAOAJAW,11VIAA?ϟ?޽{߽v   fbbbvVoڕ}X,~??E  aEf_Ixal07@\\E  ܿ>gD lٝwE,?pAA?/OCC֬kRNVTIENDB`fabulous-0.3.0/docs/index.rst000066400000000000000000000221541274117624500161400ustar00rootroot00000000000000.. fabulous documentation master file, created by sphinx-quickstart on Tue Apr 20 02:12:28 2010. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. ========== Fabulous ========== .. toctree:: :maxdepth: 2 :Version: 0.3.0 :Founder: Justine Alexandra Roberts Tunney :Copyright: Copyright 2016 The Fabulous Authors. All rights reserved. :License: Apache 2.0 / OFL :Support: Python 2.6, 2.7, 3.3, 3.4, 3.5, and pypy :Source: `github.com/jart/fabulous`_ Fabulous is a Python library (and command line tools) designed to make the output of terminal applications look *fabulous*. Fabulous allows you to print colors, images, and stylized text to the console (without curses.) Fabulous also offers features to improve the usability of Python's standard logging system. .. _github.com/jart/fabulous: https://github.com/jart/fabulous Installation ============ The following prerequisites should be installed, but they are not mandatory. They help Fabulous run faster and make the full feature set available:: sudo apt-get install gcc python-imaging Fabulous can be installed from CheeseShop:: sudo pip install fabulous Fabulous can also be installed manually from the source archive:: wget https://github.com/jart/fabulous/releases/download/0.3.0/fabulous-0.3.0.tar.gz tar -xvzf fabulous-0.3.0.tar.gz cd fabulous-0.3.0 sudo python setup.py install Once installed, run the demo:: fabulous-demo .. image:: images/fabulous-demo.png Examples ======== Colors ------ 4-bit colors and styles are standard and work almost everywhere. They are useful in helping make your program output easier to read:: from fabulous.color import bold, magenta, highlight_red print bold(magenta('hello world')) print highlight_red('DANGER WILL ROBINSON!') print bold('hello') + ' ' + magenta(' world') assert len(bold('test')) == 4 8-bit color works in most modern terminals, such as gnome-terminal and Terminal.app:: from fabulous import fg256, bg256 print fg256('#F0F', 'hello world') print fg256('magenta', 'hello world') Fancy Text ---------- This is something neat you can use when you program starts up to display its name with style:: from fabulous import text print text.Text("Fabulous!", color='#0099ff', shadow=True, skew=5) Images ------ Fabulous lets you print images, which is more fun than useful. Fabulous' unique method of printing images really shines when used with semi-transparent PNG files. When blending backgrounds, Fabulous assumes by default that your terminal has a black background. Don't worry if your image is huge, it'll be resized by default to fit your terminal:: from fabulous import utils, image print image.Image("balls.png") # adjust for a white background utils.term.bgcolor = 'white' print image.Image("balls.png") Image printing may perform slowly depending on whether or not Fabulous is able to compile ``~/.xterm256.so`` on the fly. This is a tiny library that makes color quantization go much faster. The pure Python version of the algorithm is really slow because it's implemented as a brute force nearest neighbor over Euclidean distance search. Although an O(1) version of this algorithm exists with slightly less correctness. Your humble author simply hasn't had the time to implement it in this library. If you like this image printing feature, then please check out hiptext_ which is a C++ program written by the same author as Fabulous. It offers a much richer version of this same functionality. It can even play videos in the terminal. Also be sure to check out rickrollrc_. .. _hiptext: https://github.com/jart/hiptext .. _rickrollrc: https://github.com/keroserene/rickrollrc Commands ======== fabulous-text ------------- .. program-output:: fabulous-text --help fabulous-image -------------- .. program-output:: fabulous-image --help fabulous-demo ------------- Displays a demo showing what Fabulous can do. fabulous-gotham --------------- The :command:`fabulous-gotham` command is a gothic poetry generator. It is a gimmick feature that uses a simple mad lib algorithm. It has no concept of meter or rhyme. Users wanting a *proper* poetry generator should consider poemy2_ which uses markov chains and isledict. It's also written by the same author as Fabulous. .. _poemy2: https://github.com/jart/poemy2 fabulous-rotatingcube --------------------- The :command:`fabulous-rotatingcube` command is another gimmick feature that animates a wireframe rotating cube in the terminal. It runs until you hit Ctrl+C. Library ======= .. automodule:: fabulous.color :members: .. automodule:: fabulous.xterm256 :members: .. automodule:: fabulous.text :members: .. automodule:: fabulous.image :members: .. automodule:: fabulous.logs :members: .. automodule:: fabulous.widget :members: .. automodule:: fabulous.term :members: .. automodule:: fabulous.rlcomplete :members: .. automodule:: fabulous.gotham :members: .. automodule:: fabulous.rotating_cube :members: .. automodule:: fabulous.debug :members: .. automodule:: fabulous.utils :members: Terminal Support ================ Supported Terminals ------------------- =============== ======= ======= === ========= ======= ======= ====== Terminal default bright dim underline blink reverse hidden =============== ======= ======= === ========= ======= ======= ====== xterm yes yes yes yes yes yes yes linux yes yes yes bright yes yes no rxvt yes yes no yes bright yes no Windows [0]_ yes yes yes no no yes yes PuTTY [1]_ yes yes no yes [2]_ yes no Cygwin SSH [3]_ yes yes no [4]_ [4]_ [2]_ yes =============== ======= ======= === ========= ======= ======= ====== Currently unsupported, but should support ----------------------------------------- =============== ======= ======= === ========= ======= ======= ====== Terminal default bright dim underline blink reverse hidden =============== ======= ======= === ========= ======= ======= ====== dtterm yes yes yes yes reverse yes yes teraterm yes reverse no yes rev/red yes no aixterm kinda normal no yes no yes yes Mac Terminal yes yes no yes yes yes yes =============== ======= ======= === ========= ======= ======= ====== Unsupported and will not support -------------------------------- Windows Telnet It thinks it supports ANSI control, but it's so horribly buggy its best to ignore it all together. (``TERM = ansi``) .. [0] The default windows terminal, ``cmd.exe`` does not set the ``TERM`` variable, so detection is done by checking if the string ``'win32'`` is in ``sys.platform``. This This method has some limitations, particularly with remote terminal. But if you're allowing remote access to a Windows computer you probably have bigger problems. .. [1] Putty has the ``TERM`` variable set to ``xterm`` by default .. [2] Makes background bright .. [3] Cygwin's SSH support's ANSI, but the regular terminal does not, check for win32 first, then check for cygwin. That should give us the cases when cygwin is used through SSH or telnet or something. (``TERM = cygwin``) .. [4] Sets foreground color to cyan Alternatives ============ Here's how Fabulous compares to other similar libraries: - fabulous_: Licensed Apache 2.0. Focuses on delivering useful features in the simplest, most user-friendly way possible (without a repulsive name.) Written in pure-python but will attempt to auto-magically compile/link a speedup library. ~5,000 lines of code. - libcaca_: WTFPL. This is the established and respected standard for doing totally insane things with ascii art (ever wanted to watch a movie on the command line?) Weighing in at ~72k lines of C, this project is a monster. It uses an older, more complex text/dithering-based rendering method. Compared to fabulous, some images look better, some worse. I found the docs somewhat difficult to follow and couldn't find support for transparency or 256-colors. - asciiporn_: GPL. Similar to libcaca but has an interesting feature for drawing math graphs to the terminal... Needs to compile C code, requires numpy/python2.6, and I couldn't get the darn thing to work. Aprox 17k lines of code. - pygments_: BSD. Has *excellent* support for terminal syntax highlighting. - termcolor_: GPL. Only supports 4-bit ANSI colors. .. _fabulous: http://pypi.python.org/pypi/fabulous .. _libcaca: http://caca.zoy.org/ .. _termcolor: http://pypi.python.org/pypi/termcolor .. _pygments: http://pygments.org/ .. _asciiporn: http://pypi.python.org/pypi/asciiporn/2009.05.01 License ======= Fabulous code and documentation are licensed Apache 2.0: .. include:: ../LICENSE.txt :literal: The bundled Google Noto Fonts are licensed under the SIL Open Font License, Version 1.1: .. include:: ../fabulous/fonts/LICENSE.txt :literal: fabulous-0.3.0/ez_setup.py000066400000000000000000000241461274117624500155620ustar00rootroot00000000000000#!/usr/bin/python """Bootstrap setuptools installation If you want to use setuptools in your package's setup.py, just include this file in the same directory with it, and add this to the top of your setup.py:: from ez_setup import use_setuptools use_setuptools() If you want to require a specific version of setuptools, set a download mirror, or use an alternate download directory, you can do so by supplying the appropriate options to ``use_setuptools()``. This file can also be run as a script to install or upgrade setuptools. """ import sys DEFAULT_VERSION = "0.6c11" DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] md5_data = { 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', 'setuptools-0.6c10-py2.3.egg': 'ce1e2ab5d3a0256456d9fc13800a7090', 'setuptools-0.6c10-py2.4.egg': '57d6d9d6e9b80772c59a53a8433a5dd4', 'setuptools-0.6c10-py2.5.egg': 'de46ac8b1c97c895572e5e8596aeb8c7', 'setuptools-0.6c10-py2.6.egg': '58ea40aef06da02ce641495523a0b7f5', 'setuptools-0.6c11-py2.3.egg': '2baeac6e13d414a9d28e7ba5b5a596de', 'setuptools-0.6c11-py2.4.egg': 'bd639f9b0eac4c42497034dec2ec0c2b', 'setuptools-0.6c11-py2.5.egg': '64c94f3bf7a72a13ec83e0b24f2749b2', 'setuptools-0.6c11-py2.6.egg': 'bfa92100bd772d5a213eedd356d64086', 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03', 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', } import sys, os try: from hashlib import md5 except ImportError: from md5 import md5 def _validate_md5(egg_name, data): if egg_name in md5_data: digest = md5(data).hexdigest() if digest != md5_data[egg_name]: print >>sys.stderr, ( "md5 validation of %s failed! (Possible download problem?)" % egg_name ) sys.exit(2) return data def use_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, download_delay=15 ): """Automatically find/download setuptools and make it available on sys.path `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where setuptools will be downloaded, if it is not already available. If `download_delay` is specified, it should be the number of seconds that will be paused before initiating a download, should one be required. If an older version of setuptools is installed, this routine will print a message to ``sys.stderr`` and raise SystemExit in an attempt to abort the calling script. """ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules def do_download(): egg = download_setuptools(version, download_base, to_dir, download_delay) sys.path.insert(0, egg) import setuptools; setuptools.bootstrap_install_from = egg try: import pkg_resources except ImportError: return do_download() try: pkg_resources.require("setuptools>="+version); return except pkg_resources.VersionConflict, e: if was_imported: print >>sys.stderr, ( "The required version of setuptools (>=%s) is not available, and\n" "can't be installed while this script is running. Please install\n" " a more recent version first, using 'easy_install -U setuptools'." "\n\n(Currently using %r)" ) % (version, e.args[0]) sys.exit(2) else: del pkg_resources, sys.modules['pkg_resources'] # reload ok return do_download() except pkg_resources.DistributionNotFound: return do_download() def download_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay = 15 ): """Download setuptools from a specified location and return its filename `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where the egg will be downloaded. `delay` is the number of seconds to pause before an actual download attempt. """ import urllib2, shutil egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) url = download_base + egg_name saveto = os.path.join(to_dir, egg_name) src = dst = None if not os.path.exists(saveto): # Avoid repeated downloads try: from distutils import log if delay: log.warn(""" --------------------------------------------------------------------------- This script requires setuptools version %s to run (even to display help). I will attempt to download it for you (from %s), but you may need to enable firewall access for this script first. I will start the download in %d seconds. (Note: if this machine does not have network access, please obtain the file %s and place it in this directory before rerunning this script.) ---------------------------------------------------------------------------""", version, download_base, delay, url ); from time import sleep; sleep(delay) log.warn("Downloading %s", url) src = urllib2.urlopen(url) # Read/write all in one block, so we don't create a corrupt file # if the download is interrupted. data = _validate_md5(egg_name, src.read()) dst = open(saveto,"wb"); dst.write(data) finally: if src: src.close() if dst: dst.close() return os.path.realpath(saveto) def main(argv, version=DEFAULT_VERSION): """Install or upgrade setuptools and EasyInstall""" try: import setuptools except ImportError: egg = None try: egg = download_setuptools(version, delay=0) sys.path.insert(0,egg) from setuptools.command.easy_install import main return main(list(argv)+[egg]) # we're done here finally: if egg and os.path.exists(egg): os.unlink(egg) else: if setuptools.__version__ == '0.0.1': print >>sys.stderr, ( "You have an obsolete version of setuptools installed. Please\n" "remove it from your system entirely before rerunning this script." ) sys.exit(2) req = "setuptools>="+version import pkg_resources try: pkg_resources.require(req) except pkg_resources.VersionConflict: try: from setuptools.command.easy_install import main except ImportError: from easy_install import main main(list(argv)+[download_setuptools(delay=0)]) sys.exit(0) # try to force an exit else: if argv: from setuptools.command.easy_install import main main(argv) else: print "Setuptools version",version,"or greater has been installed." print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' def update_md5(filenames): """Update our built-in md5 registry""" import re for name in filenames: base = os.path.basename(name) f = open(name,'rb') md5_data[base] = md5(f.read()).hexdigest() f.close() data = [" %r: %r,\n" % it for it in md5_data.items()] data.sort() repl = "".join(data) import inspect srcfile = inspect.getsourcefile(sys.modules[__name__]) f = open(srcfile, 'rb'); src = f.read(); f.close() match = re.search("\nmd5_data = {\n([^}]+)}", src) if not match: print >>sys.stderr, "Internal error!" sys.exit(2) src = src[:match.start(1)] + repl + src[match.end(1):] f = open(srcfile,'w') f.write(src) f.close() if __name__=='__main__': if len(sys.argv)>2 and sys.argv[1]=='--md5update': update_md5(sys.argv[2:]) else: main(sys.argv[1:]) fabulous-0.3.0/fabulous/000077500000000000000000000000001274117624500151635ustar00rootroot00000000000000fabulous-0.3.0/fabulous/__init__.py000066400000000000000000000012241274117624500172730ustar00rootroot00000000000000# Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. VERSION = (0, 3, 0, 'final', 0) __version__ = '0.3.0' fabulous-0.3.0/fabulous/_xterm256.c000066400000000000000000000070551274117624500170710ustar00rootroot00000000000000/* * Copyright 2016 The Fabulous Authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Optimized Code For Quantizing Colors to xterm256 * * These functions are equivalent to the ones found in xterm256.py but * orders of a magnitude faster and should compile quickly (fractions * of a second) on most systems with very little risk of * complications. * * Color quantization is very complex. This works by treating RGB * values as 3D euclidean space and brute-force searching for the * nearest neighbor. */ typedef struct { int r; int g; int b; } rgb_t; int CUBE_STEPS[] = { 0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF }; rgb_t BASIC16[] = { { 0, 0, 0 }, { 205, 0, 0}, { 0, 205, 0 }, { 205, 205, 0 }, { 0, 0, 238}, { 205, 0, 205 }, { 0, 205, 205 }, { 229, 229, 229}, { 127, 127, 127 }, { 255, 0, 0 }, { 0, 255, 0}, { 255, 255, 0 }, { 92, 92, 255 }, { 255, 0, 255}, { 0, 255, 255 }, { 255, 255, 255 } }; rgb_t COLOR_TABLE[256]; rgb_t xterm_to_rgb(int xcolor) { rgb_t res; if (xcolor < 16) { res = BASIC16[xcolor]; } else if (16 <= xcolor && xcolor <= 231) { xcolor -= 16; res.r = CUBE_STEPS[(xcolor / 36) % 6]; res.g = CUBE_STEPS[(xcolor / 6) % 6]; res.b = CUBE_STEPS[xcolor % 6]; } else if (232 <= xcolor && xcolor <= 255) { res.r = res.g = res.b = 8 + (xcolor - 232) * 0x0A; } return res; } /** * This function provides a quick and dirty way to serialize an rgb_t * struct to an int which can be decoded by our Python code using * ctypes. */ int xterm_to_rgb_i(int xcolor) { rgb_t res = xterm_to_rgb(xcolor); return (res.r << 16) | (res.g << 8) | (res.b << 0); } #define sqr(x) ((x) * (x)) /** * Quantize RGB values to an xterm 256-color ID */ int rgb_to_xterm(int r, int g, int b) { int best_match = 0; int smallest_distance = 1000000000; int c, d; for (c = 16; c < 256; c++) { d = sqr(COLOR_TABLE[c].r - r) + sqr(COLOR_TABLE[c].g - g) + sqr(COLOR_TABLE[c].b - b); if (d < smallest_distance) { smallest_distance = d; best_match = c; } } return best_match; } /* int rgb_to_xterm(int r, int g, int b) */ /* { */ /* int best_match = 0; */ /* int smallest_distance = 1000000000; */ /* int c, d; */ /* for (c = 0; c < 16; c++) { */ /* d = sqr(BASIC16[c].r - r) + */ /* sqr(BASIC16[c].g - g) + */ /* sqr(BASIC16[c].b - b); */ /* if (d < smallest_distance) { */ /* smallest_distance = d; */ /* best_match = c; */ /* } */ /* } */ /* return best_match; */ /* } */ int init() { int c; for (c = 0; c < 256; c++) { COLOR_TABLE[c] = xterm_to_rgb(c); } return 0; } fabulous-0.3.0/fabulous/balls.png000066400000000000000000001524741274117624500170030ustar00rootroot00000000000000PNG  IHDR,gAMA7tEXtSoftwareAdobe ImageReadyqe<IDATx u&"2>ݸ xiKd[#[K)R>f?D]]{kydemY-ɲdEڤHM @@_udfľ""+xDVeeU)ŋ/^R~xŋ/^xċ/^xŋ/@xŋ ^x/^xŋ/^> ^RB<@ >LJm]]{BqIܮ]c"y=,BkG!ԧ?4plnD QAZC (P   PNQƉJ_ o!!okO>DgKG0ŋ 8DVPC@P+r`y?Ԇ_R_2HUVI#| 0wjf9!Y@3KLDN_@ŏTj~z/^<\pL *w)d$~ᕣ08> V C AT/*\E *`hh.DfC'3ph&?w|ᡝIz ״/^xPkVi8 D90< dž@)A˗ o VmT 6욞9=F B-k1,bc&m3uM<׼zVk$HRG\<T]ŋ fчwKF{(\aٚAdMHcc‚H_׼&0 [kEm1imAmsJ<ZȄV]F"D &mR>Jx@PQ?(RgV nM=G B@B!ʂBxvYyO^zB8&+x?Q &׌ !Bo~5ŋ+(Nd*V y=cŝ)3Rg2ĔH^ S u-*IyYccw\:ulܪ *0y,[ @ PBRgd7^x_ UxC1(8-(|g:By30`{.kul<,P&X, $0/! }wt7ŋ/@^u!o t(* ޲"`MIG&Hhh At/nz?!T|. *%ӀE F֍@HBSV){7Cŋ }|g@ avXbШtQi,a%FE^u/}9;ksؾN Jt#ǠDB0D޼c\/^|G?\@or֎@ m?fkJZ*ܢ> #pP,9;g8צLTW"QWJԔ3ah"swO31 \k1'|&d o-"IqOH% _/z%pg\ϊ"Z)kep&9Le sE^ aFd0aAž$p?PJ>sJtP@zx_z B%b `/!+`*.$cy7mĠ3xDYCGbȎ{@DP!3\ -h>r~urS~gI}ҧzx.^WI!ot@8c@(lجAgR1VkV˃T`VLlDE'XsN,ޥf71~8:d_*+D j^5x߄vŋWGX!MpBMWFL]Oi5AGSLX ! OE^7fF5W X#L%_\7`>5U5[=俾/B%qH` ?I|iBiME,GD?02)v?|z~G>3#ȫD8 !'/H&Mڮq_hEf,Bd n`1ˆN`nk5j O"`S"':2I"kR'> W+:\Zt wzn[=rfhԎE$OW:<-|`zR#*Ƌ\Po 8š6XcS4SBv^u=2ZTbBXCa Kx,:ZX@릲dkW %u4kzv}zINZ w>8Wv7^OfT H-Fa(BFf(@&K_R.}I1I$G4hz~ Y>;(nx*\J52fd"ZRzDهJ0&#R ]?%Y5W_]Ef, m/*ʚ 8w>yFy!>P],|⛞t^A87.Z ң$"*}P.TVjb J2"P1Nd IC3iB3n@53Y|ӬXcݹ9^}jNj @ \"Li MTC+pf mӕ u`N3oJИ(*.,8lowIV^+&)RR|WhhM/9 TL8L [z#ZYE`eF`j+#TpHyGץT|S !I:Z8 ΟvwC NXފ@5wly 6jڼ:Qf>9pQyg8ұ]X["䚐H(Nagua%c]_]=ۋx@UԺ)n=8oG/z!0,kF&ao9F F&76S[FUE%MJQ'4)YBOIxAx̋ҩ)*gV<!od&@qiZ6RB҄钲 YVg!fڝ`8 m^ݑMjE`ۺ1sQUJ5PdVPҼJ.EP!. kz[c<@_d`.ƾMAKk̈g7ԽTTqk|xzݰspqPiRBUS w^yTJLCA cbw3p!@KE Iap(0>alt1WA): ƭs\I0 < ^ĚbOe0$HCd1U1W Ǐ<}HVǡ^}-lZV bnVr+ƒT Uذb V 3`fnb u[Hy#~;#-/@D$ m|@acL^X;ws"0yQy&?]!b6a~"o !Iv!b! ܕFlFUaK@=8ՇC8&ZX.)n50>2T4 2:R@3r0Uېj!.$Ym̐LT 2lXV Fs_`]T?Ӷ?C;z1ʵbT(DTl&*jtO 43HDVLQ\H[S2B3o[R EVP6_%Ȅ"&( ab n,%#ӓR GqRa eյoA91DlUi+0p ȦMZg 2U"hQ___}0T? 8caOzī%/@5J0=@]RlJW:GCGU:`pE2&,Hr*_b 8/JnhHyH9pTK Olα&~z_qKaw}d&\ݯz֪?,~8y+/RU41&Bb.S̊&?&Tn(ȗTNYO' 4aY58}0X(9`Ea6 o'v~ɫ&/@Eި͎]f< #f#5[rQfZd8"HR~3kܵ>(I!$25+|Jt`4c3+=o=K|] ^=yre!d8E)q&19 <ȄE櫂J|3"H+mI;4m: :dNHhm))M ߥPQHURTvA Ra j/iͥMW܉¡QB1MwÊU-h6p[_1>O}eDw% ѐU\#H6MU VaLQbwtw#K1Fx[*™pj$ HVl&!n0%e#u$2u+;u6h;f-czV1Hx6$TH kAƤ΃ya?>v%c SP+<Q2OUNo;6WBsZǥoR4Ui[:fyYB/sH}kts4a'>=~-$iͬSw ""aWco;M-݈~; ,x樋mRΙ`DA#[7&2*̘d5V20w=qf)w_R\@8 ֭7o\s1a?l8%tzɺ'\au$/i+R JiV<s@08}`}~j3^Uy{C(=|Z`c6LP,a )VpMCkCE"QǴC1S|fB˜p((3_8s0k OvZT0C^ !ˇW p4$:MR:Ʃ5ujnqK} dQ Dܺذb|P@ZU@.ET\? g)9WC` 6P8Bi{h * +I6m?I.n^sY)#΢J9s Ӗ /Ƥ&+_'` Cx.=8gf@ំr nW@bvKWYD;K>6bp؅sflD-kIߑ6c';Xxm> ^eyr}7߶ηcToV$;-M~R~h$AvI|G|= 9r-:M4ZQ4@Pz8՛:>y7w_@u9ҦVoAnF a\OFqG|$:aAw&/PC CMع` d\@_o' n4ZӛxOJm{%x+U^9SaAı ֶ6$Y(QH:u?N 5C)FiCwɓjC+aÐGc/cj=&>Pnx3UϢj~Lt%7NI\KslHhı<`Yݬ:YHnw&#3B\%0^b8፴]괐5(TjjY\6G~F?"Q6Pybck'>is\g9<Ǽ00Ame(痁k c0fD7:pKRƪ:?q>ȧwyS R~KAIj릅ri\f0YpLnʹm8bLBN]GpDSƝz.7V2e޵m,˨K,8CGS-qNV.sG6" XfO0TC 1G3z `ܢg!;.@tNbfZ=#0JC5a jp)A*]qRkG䒛#$*jXQ)m4W!X,ĵmTu95 ;ׇ%LQtmY:b /k>KsWcy>w9nZI x BGv\HދλɄe BW@ǣɱLaຼc7aps^Qc/MvYmQvjaW18;wWj.TjO:՗ P~z{fkNo:EE #Mz #K$mMi [U.) +k#SS@gN.k-eL[2(D0'܎_xn)kOنWxw^$xk?1 BX`*f-bQ>w}6A6)NUg]݃\ NR}g6>+0?@&9_yȥV>R~s_AM]" f-Q^9G$sKkȅтh`S}trԳʙдJJEI2Y!VZ5bW>zЉ؎SgGJ*_j i6˧Бee5,U/Jzj50`3`5mg^j R9>^HHM=3{%{󿯔N<EРT!:!?H}0,8y柝s׶B,iX[ e7pSUaKujb 䗔Ti__^NI8pLNNBܸl ) 0kSgT4;MUV.DX}y7VG+ ` tH UY'B&SP-Wan…eIE~/[zSoQ2_3| B=8o(-0FYj*):ˮ bϪ2 M#בu,z>C(%Ņ}%:XSÁ?k ~Y B,p*O ML,˕u{,/L^%Kni&˚릅 EQ=y.`F\qۯNE^y 2L:rw MMҪBEҏOPb{@]a_nkm^F;W%a;Y ʙR.<444Hp2ӂ B%gٿ˾/|o֦͛dzr߾}ewo۶ ߵcΝ;^w};mJpras>%}6}ß|휋,D[ ;z[=9UN(Թy;FL!R/`o\SI_'ˋK-o{'fwOUko. zq SSSE L~ ڏ pEX4'ƱDh)לeD pVvu]0x ?ͺȠ23YAYT]/K0"y_pbW& I~v~FkUUz  Q))ϚCAa'9q'"\pp>!PD{ Fiq8|Xrʘby|\'lNDAL%!/}}[m#TDR⢪j$ -raaA5Mmɒv(t䢁DÍ8+\@ UA%mG_iaF0Je*gNTi=fckːL ENfh*C5!/vB3nS]We^<\"q~w19=k¶hnJMj F&?q ƴEԌ[{:DnIMV X|JRb'8.8|/?Oס1ZT'x$ 7 ^bEQ:??uW "wTx-p?`jRB^ґofЭ몍 ʢ!rE_'zbNQVҙ7vJ"u"6ؚ xRn,s q5 We^<\B@ _ܿ탿;[{)" (7U4m爭0p$nlL`T>W)z?̃BqksijxܖZ&Dq /|pؾc$8hZk @BI)w@B+A8E.5#2.ps\݄ߺ^lp!:|:3aٺXѢػNMg\ #lLf2^%vG&px–ZcgRb X2j5rAFy@3?ꍧl%]oZi eS?'d*}Q!PafAnA6deSM n"O,qI[8FEjI iGKT*@uG+MP̣ీk #,(MVUҮٳ)U"$4.؄݁܄a Ƅ%uĮ6VEʜ8 Xpv!Vv_\xk4]>~1):Gb>M96[ޫ2/@. `F pVIZj%£'遅'{D:i(8Dv0ȉn2r -?8UI_`ыKsSaH 5_gJ?JIZ ?^Z7L/KFȔZl_SqYcJlrg4sL""̫) g,-ΕlŠtEk2n+=HLojplxnLW>vPFyWh'7u׫jq}>6o/ <I`{aK+W ;;uGI<bT7B ] 5x;^y݋. K*EWD[RZU  ,}`W^uvu絛ZGˢBWN[A:۽t Tmrf"l \~Dh3ձI%1S3Cpp|m׮pq)d8Fi AdpX3i&d(&@.w6JH)rP4RHAl7HȪ%kJwگ/7bqSqA .incMV[ eq!gTTt-o @(_bQi-j9ƪ_ %"-?#J^;}ip0P,äa)M} v\ +av RHʿzR)axqioth,[iuAڲeFz]t!VRLATBU, 3Ÿ0mʆڜ:j 1XI3;Δ^6bW, Jv^M@Dvl0]DIcnR7a"Ȳzm &j ʼxHٙ _"$0Xy`A}y ⍣Ŋp\ӾZmft;e)NS `SG-枱`RoTFt@-VdB`-ضTS7Z0pJ;$^8_=_;u48zvF̴0E JaEWC']z`hqZ>5c>(pI(d\|Xb"vzexΜE "W}YB3P*!0S1@uVThG G1kTzòߓ?a'ѕ6t\1X(QtUR.]E|U HPKU*\ZD0YTI9['B>*: N?|kߠ,kG`@ eY)* qL$IOch]q)^r(t- ݑEN|vQX >[5Ou-H*8+/# 든#KP+G я?AZZmW~5i9.[,dg:zs牌A i˹31ESiԖ]1hi!xUe={P'?ʴKɬE@R( 4! !RE*:hllDδ57dGOڇysr`AS>pU5RyJ 7 |I=99͎ <‼wY'R+̇ǰ٭R((BWc8{=N54XEe%D?0-6B$WĀ4AO$%])fTtδe{\GBWх K-bbEPolwa/ic^yrMZ#A3v sI\.RZv?) `C۝#[J˽HkYUt . 1B:]I*{^Qظq >sLwt 2>{wl*݂ qS"K62.&U\ 3VTC'5!i6MA)\Nݕ  e(Th]J) bq\ Nב& % "Jǥr휠^AfPe>{̋ $W2??3 0̨ ]sTf7oGP|G*LF smxk}-iUwHARR a}5\TA RBAT2ArwFi@P 4砱Ѐ~nh5@ۤ/=[3XW>[!T_<,*QJvݦ:jk]UwmƏ#8d*01؆g!9&s_!J{* s3Y~=Tp CfJM9`@ q&,zzϾÐALPp=GvD=EFEwdrrΏ|9 8'~ ^ٝPWj( XBRFG``J|<Q /FƆ$r5?x&341A <̝:3/3ǎnk6PKbH{5,A`%p8ؽ/2i#*P\F8MXt|iT ̕<\Z* ^<\-o߾ln:aMOPi^.ș:K0۬%r\v\?\M}aeB%S kDz=룰,6U9VJƨKoq%Uj}0r9 MNЪUP]6 eJ&A$:-Wr@s!M5[7`YX8u?<:pff}#AD')@M#P~j7ep:n~m;WnHRGP^C P 8̈́w-hbh:INi]Cv:| 3 s<8uƻPUc3j2- CC0<1c&aprCfS^e`/+aLNu*\pR|*~FF`pJ{HN@;sߙwN@1l_ҹrU.ctyedFqּv^iP@lJ)P-WYH1+hΡ_b5zjq Dɘ= (WJ069 +LڵPX4HTXL6.l',QPnWpl^8{/}%hQwHdpn37Ca`1Pq-#NqiU\pIʚ1MMt@)V\9ˍזxz~*l$1R!իalXyAX(ڶ1hhЎCV~ -UzIexVppl>[?3ߙ?5 V(ő|eqYZw,mgor?R['KqV8?zjakPUW % 3ê뮁e'6Ӝu#NoxU>퉾Mja5`> nHˊ *ϓv~W!vo ^<8d?T2QM&BqÔ1U8T9oX<.{Svاi.2r@!`ƛ^ x~?☯+<p|D=en꾞PǪݲ a!eYtБi+*CV-7!JD#ILÖ3}%X[ *!ZLUa':i_eLV[d+#ܸw%5%#x"ʷ "R.:T+WJtu>r5:dTs3=9|]{%Ytj+yh׾(3Pƃ Wkn@f2 10]VK0Sp_"0pgtI`ek )5IfI8=U1gphEnqKRuw ^ylT?p A[7pRf+ԃ!Ze:|!ުϹot&Ӝ+H|\J$@ٰsЪ7avSt:/Pʷ\q6R2;"Ù6bDRSp4A@İz6\)E+ Ƌ ^r/{ *ڇ`knkشZM.x(J {"Sr=啦.iT_l;F9Wjbf;yO<MnYx3GauYq v>SȷAF|Z [05*sex7?KO!rbEW,`Ͱbja&#--KW%Hvm;^Z { [4_Ȯխq_?jHJU bp@!!v~f;>,# (B54%n_Ĵ֦"7բԙ~W|x<39<*cd/& '8ޔhpo#} ;'3]yMX^xLm!~Nt"}T`i /+U;gWR|.Xזbtiɕ9]X"×:@m ,قZkԁP߽dړIx~k@Ә39eلe piBK7pAРEs$m-d+2M(邰HGKh/@\YM 5c"f08<4G\yKcEU^5۹^/^wX{̕Qu~vj7~wx|: Mfj͸W @uo]aơNL܂5QyY3|=E5;Ӵ >o84: JDQc$3BV/@\jY'ɦf*8Tis~T}C\Y#ipַ!;ǻp`ӏ'>!^L (UP_PYr% PضIX*ncQ}zh@u9vΫg;3q)d2RQAB°ZS*8{A!0QWB8Mgb 3Uey( յ1덧ja $j AAÙZ@HA: "e(P.)BKBYz`U^?FqÅSL6kvi.EX0 1,tTYx;܇!\^g{=xY:I\KRZem)AWn5tOӔ+sj]#>"VǾZ!N*cvDW$™<ҙ37C#_\\%A.hJI](Az8}|p?OG}P!F MMaNbV (⚖|6XJKRBNup˔VO2qplCHl~:bڎ[LpZcI 6" xR>?@\AyעJHa>^BILU#zgj M:sK/NK{3v7jOW]ǰ $ X(@C؜E~©o*ȩ2 PVq"@r枂C_/v\=:Vi֚65hS3}s3trxȕ~.lnIPe "5( pMaW!=?FA&0E ^#EY,O ތ\F*EDv eZ7 +B! ~wʸ#VrHx)@I}Vbȼj\ q+6=Ol{|M݈x^(>C&{rid o'MQ@BT$>R(B'5: "*_mwDwK" "c3/; YYîjF'zB@ ($ϠY ~dA)ߞ$NfUyL<0po$كd jM p@)inM BAi Gg/. !EiºA9 oǷmȿi3,6ȰB!ZJ # EXj ۣJ$Fv"}~il4Z\<, ,4Z0ߠufE> R/.\v>L`2u}O5D|/ߍRAP{pZQ,4?r[lPIfvr&H._Xmn6݁Eaa eg\2wMf17kږ{!͠Xؙ@you*NRy/ԧ!8-OW*|އ(YX@DX`КLaH,ڙ2u?:ۿf يoӶ3!DV\.@ }@}&Ebb:ȴQr%MQaN~1׬dՔ FrH M]3sp,ԡ4jud(qp5xwzO옾Ç)AX ;F`hg.OzrUof<@;VQҤlQDڪ-P[V1 #8WoR|]\C\=!6CF魓;Ǟ2㌙rgq.lf0ģ T5u:Q989y>s\ ߘR`^`cbgyj_ef~6$䘃8ʗgz7:r[DwK"pf>AHQ z+s_O'<-!z8ψԖMhO,p(eA8BwqPu8l2`$4"`Y4FaX6#U( ,sGz?5M+qi826RxVc3p 9;ώxa2\^=q ~k ^.}"MkD2e-3#VzE]溣ml>joC*bxVbp{N]svNϹ3#tgtV8`.b5 *U-k2p <0䯰 ý446;2)p!բ>׷vs8x wՍK).PAe<6kW.c>P+T.@!0vL:] >>Pq 蟋I6̃d(!` F\/׬(ѠX}e @֤U&0D9@ȁbMVښ%9I-xLth'K#FAwz;pݏmT-z:nAGN(oXbJ$(H!sʚ\ۖ8߇ >v[^]j<iT~^ZLa& a(Dr,,ES"NwT&Ь%!0ղq#; GNAFRg3D)P?"{IL잛Y?=~j4hFo$$^`Y Z@ƻkGuرpFDm D@IX艆hf4ytOwO?UsϹy3+Fο*ww 0FwEs;]_WǐWM9B"F ?WQ\ӌt&00y,I2Bka˨o3d6 bìwEM5^MD <uCc `Fxu2],؀jΐ1h6- F[2@W (135Ws^{wUh$J+cfA{P3=_q;ӍSz ɑ{ϝrO?^8}^6$ 9B|u  a[FC3XkD޵:$y feZ[ ~ 3EZ0qF4j,,31$A{([^L`qᜋQRɴUH#D~nkuoƫ?0z@̪woxdZQMk7 L3@}יdK毠Nڿ 9^yzI=,CVOsϾpM&R)r+!sE#ki`J`5M:b*T/s@갹%y%΃G1j{id \Z†7d˚2IzQcY&H:P36w,-U43I~T #OSc['Z^M`2R;3iظ5Qb &zҔj #q7N~0яw:ېew{\nۻ,wRT@|M=aqΗcXy^͌7OuY5#ƾpz׹(V?qw~}ÉclJ|}a?Cn27i_p@Jtg%q1Q΃N*xL+h aUU26og|!ۮk)fI.akJ*pfWq*!ș-`N˞Ji%R<.h<)n'l<(RrAHpQHT ּR@}&֣~ﭷM+?P| xxmnUKvwvnPɴ)B(%w+QUׁ/{s,Y2 FQPpd2Ke "vkx8{,/x$ˊk:"70kQ*r!ʹBxKVx/q}Ե3j*R"zϟr[2E@de#c+zi͕oe:FX穊E6AȌdJj4 d4͸j׍Kp!!/B lړ\$@7*'أZpeX[%HG(䇞?P`v$s~ Z"px)Ƞ!~Vy-}Ck[$ap_ؽݵ M`M<2 ZY±c,ۗvly1 7}+vcMLXuXFUǟ=A RS} ?A:_qw0˻G}1cjYՔ (;3@3tJM[io`sOdVqL|e*6w̘Tx a5EHG&Y"YI?A+ srUMɈM/ᔪr9pY6z">Fh7?IN58ۮwW+ihfFF67g% s%Շ@>_#Oձ$k~q㪍I>mqvh^$kJ0"|L7V RXjmKZJю|PZߞ+2<F;zf޵&194v8q0zSIޅ{X&N f-OBٳgk@)+L݌ԓO>)^[7HbΆ_s3xH\Gs[Ob^vq&<̵&p5!|DZU2h ͩ4:vUJVľxYF;Gb16m5jYXr[6[e ܇P-lS/v:O$'BFjt҇gDWHCbC!Ն*% Cm+fv1g^ %^Hs#H7(M66eO=γ{z64%? Uw|xTܤ%+솎G;|=vK0]9BR;w"]7k?6H+gr[ SO!w+e\Kaf.v-ד,X"$_f(tvFZԓ\^O®\|&Jbm?k3VVH5{p1rA:Vg- HbZdes}{G\ܻ:XnZG\|ۘBt,gxՋzm9O7}ԝd1e)eyiA]\\w7&E%Gt?'P9Mk \!,-ׁz'bdBd|5xࡳ"FU,7=y1k^lg=2ݐI#%Z)l4u1`Ȟ M<(mj5*:~ {,5y;w|管t1W}θt[-.E?*ziNLz} xior0^k/"9IS3E&HrH-ņ^wĝ>:x!rGL 9?@aEZ6 JBb5s5P2u'"!q={lCyv ìcļP s84^er1~D2'cXL]ܘ7g朦돸E=OU|̔e\Udtc+e_  ]6EH9^8]ЂZ߽my*x>,1æ@d1S3Ezwy\L[`F'N-%ހf40*) eNefe@*4N"Vpa/aL{ۻ"VבxM@LAtmUv}̳7^ B@7[]wOISJ;]M$㛴O T y\-Z "YX% /d'R}(ڀ>G5]; .@ޝ0k &6!"[ iW@@$3V I+EnɹClUBL/-8>cA7W!ОSW&K HP^yУh]M_kySFS\J٦M W5tj]SV4aڻ;=;wIT,,-%q"F~(R' yY^%)DCgF_31n,E`[[|'KW;n2ugb?GC<d/8I9a#R2q ź*bdOS@h_}(ATY2p, XvDO*kۄ?cA LpzB6Nȋ&sg9mD{As7"x5"9,Є ^"33nlNʑ88y Ni*oEĹ%):P[屴J48E`ug Je oKݳgE-cJ),l-p}GЉ#N*},<Q\4 KElHy0 mm@[Ea5!] s>ѩܻy~y)4% O+n GPо4@vAvv, OX$ܥf gڅD|miތxڅ<먥ʫ34 }X0#sٗn\@:fQdJ@PªVP$RqCT BjLӃgӓ9y_Ky ɞNκ Q}{Ǵ+BODzjpi"_ b}1:ƞo#l&'PK;oE$7$4zBxNkG>;X@vݠssg`Q(O#RX#  kы Z|~Ths l=}]|4ىiG)DP 0LEQfRP/1yR0ȀQyt9}r)KXFm^XNC+ΰZܑ{܁"2բEEtq`ܬ"p7~9893s풡[h'@=0M"{Iվ%IR8q|ΔPr&Jʴ /ӫ?>X@vS.kb@ `X܋' (ѵ(&-썅mm&r0끘w,#` H|y>DJbARFu0+k݊;'vU]Mۿvs*ԙn͆_u+ nRk<;%~ MsܗJ8%3ǩT㰴U}BV/tоU@^ȦJl"AEuY=|*c@0qaZL;{AQlqy!VWniA+Wߢ/>:@v} ݝoz~}2YKSl)HUJFVG }y Ԡmp{׼P̹V(=S_6$b>XvPqüYP5aI,fVS>tncV\_ҏF#I p2@w5W+OT#a k$? c8es ƛף 5T=ɾ7^WLqxEqy'hˋ nҢf1OKHT'Wp֫;QgS"R|_ ӫ-Ïm@~9.+>P-^+}iW}X_&;wӌƮmi%e4L&NG>fZtmj=0c*mRt읕bf]ټiT$BN[3jm+(&lߧXGjB#k?5~=9 HIOQVȈ-. =BV`{T 炄0A- rHt%jbv vbF_:qͲxq춶&{"i/z@ALWػ%TbW>6mc$bb@}"f0u@ As;w ]I2~@ ɈqWB>Cpgʀ) \kZ- *vPa=n%M2b}xS=MRX0r4NovG_s{dLzTfs}gP{ַ8t3 .V4jc!Ȥ:0ʃ١ ]?< t4MX .Mm%1mւdtNZՏGAގcdLCcŚKg=AyG4=*y%(vD2cnV/H@k?~w^]}=S$i),\v4Zd_)dP@~][B=^3)X=Xϧ:Z0"H9D0L[&[$+k)2 8u֎7. G8/a% !]dtfPvyGcl>Bm5L餀mJeSAhÌLeP} I{ìN0px3g#4U[{ɶɌߙL`~^-k e,imiHvw7ʦqf`tԝF#ki˙x?dy3-`f;%"& H;rD$7빞wsPεT~ܟ7}7,b Sxy/)tуeACUu<>"kk[:38KWu`>M+xt=$Ee4a\iwTLc=wXV k>w0Ȕ<*l)&I.l_(ӌ6xڛ̘zcH,R\򜌾,nYa#Do$nHCzIUAoԉvSK_lK_}_z&|8h[#QB oX4 13)ت܎X/g^v"OYHb&ĸ4F'q{v!;g 2]3o*ߵ8.jbFm\R+B* 흞He36@ ^[jأ^QU?׌, @Q孅94@f >q/u@N:o+X)y-J5a}29gߗe r /ͦmZh] Qc\Hta(74 o85.s_ d>DiϨtaӅd kŲ%>!wĞ^_#Ed)'n2fmYۈLf/Fh:ǁQ}էvĐpqp5QL<FPDwm̸_CGs'x(,*ɝ7ơdiY:)[ziw䡔С^/c ~5{Y[,iݣՑp:x2E41ZhO־כ+2k]<~{wztQfk^ZFQ.qxJ2|r:C;L߸A=7nHr6 a|Fnft,5dzѹrΫGA;*PZ煵x⬵V;g_EeZ}r_H&5f%'.s - cw>ټ鏽mY;~[>$ŕRHvyhZo`ύE%s:ͽPH*]QiggN@hH UXsLmyo?};(lv&Uu k`sn9MˋM=ה M)c^7}l:VkpoNa潨'"3L:xRRES%pp*_CknM.Hla|[=A.b;5Ft3w _7&[ΌA5^7G쏛y&y4|X5kVu;)q|X :n<u Y@x!oj"0WɝHA^ZMmϙq;c/ 2Mfs0nyד\!kQtlx0%s&]@С3ۿV$m& #}}dLN)ft6~/#J90vj7ѕ[/AJt+z#|0fhF*SprClO %ŷ jsd \b}X@}4~mg*hvR 0y3((Ah45#J@ɔ<6Z`L_AB޲Ի6 Z9ծ:(A,&JYq-DMkO@zMtZVr!ٻ?ʗ;DŽEj$}wr%3yDr~UF,V7N~o`6;ʼ/w#ff/mۀ|ғ }~yO]Wko@ڨ="`h\TXdr_n(,Hj] .\A{R;MV$KX|ӆҪƮwƷw G5]k\H`"Xm4s-ĴR&&9Z[LAvZ .BcARd݈TD K1`T<"N `iI@zk@q c/-.Ɨ 0"\/2eY}P0lnnh$e.\0t#=^M+@ 6~?GQK#1΅5 vB~ǾX~sc|ݹ |N& +++A4N|q}H 1CRV-Y 49>zu-deivH{;IhLfxC9<q1lUxxLSqȹÀ'>_ j2Z5CmmmkN{w ϕV KHwQDQbdF%Rj!s|z\|q +tj 4H}rDD*Dy < R+9ptҜؘ!:$\r* A-U3#rA0!)\8X,1|loLJ4hpVL@N1UĺDr]20v+x/}eeV2r=y]IP}#+1+^P *Li0ȵy CW6#ЁkB0 w( c"ꝙgm}hvN@w[2/bAC[e ;'fjP-Hͤ3=Ĥlf2ǮF$j`\nK6ΗUW\*d\>|f¹E>#kJ%D/}\dzWҺ2 mZ%^HL'=bhY-h.H1z#2VZAfR<,)m鶤cx/}rc_#L=8 QН 4`b*hL}lO4*HѤ8['<+@Io=v`n,RJ6ͽOC))wmb"]'Q홃8K&WݘUu7) 7^3RT0hFv:dԪJԄE&54l|Ɖi0d={L&J2l^uMGFy~ߵU'PAe`FH!QU;UEUβ(ΗQTb#adއ3yUO=/14QȗJxip7?.=WհVS*xg"s27w6u"q5c:K^cm Cd1ihJ'>0vX^^f O ZJ" Lc0o`%zI<UٷwxJ1Z]̐A[)iBUJ幷YJaV; Z͇ӈu*3 dVaN0l<ɯԽ}~ׂQ g=ң+5,d։0ziHH::Wz`d[ͿZ6Htc" m4`8 sJtN5]&!Ƕ`5# h ]+rؑk]zm5%: 1š0v[L]---=*!e6~´"/t,,.N=V _ x;p5p`+_>Zӛ42s4]Z87vs*@x%V8̈cVx%b }eR) (ow-h!5.:)/Ǚth@wH 4R)hY9ibUg Kda]<!*B&Lg)ZB\W.1ȐiH_y<"sZ=n|}(n6{=VMF&:bdTL ؇3>.,&){$togt|x^_gt!lT!EF_y=薀3^9P+ 9t~ 6:8&$TX^c"^#Ea+jJ[2k?l8o ok{onPHK7}{@#ޢ@Tȹ$CkaeĜ<̅It ˂v"0ϻ@<ͨ mm2n#`bmz?y$cy Xpkby1Y[:y#p剹[e ZgFH;Jydl/m㢷 ~O&n F7>XYKrXוּ8x@3:[ZjyզXop+ɧ;7ួ4Eęg왐BHq;4B\9 ,".І[QfSfE#eGrp@oK,7/s]ٿ>!sc)#fF^ԳإT[ڃ%8mG\AuC^냚s&t!626*t>7)hhi;1Xq l Ʉ ib# E#&2^ߌkP# |V YW[r($=݋$ܥ 8 'v}a5 EE[4rs(RaKk|EҘߤ>` x>ύ*~IJB`O!ˑY Ֆ :@$&K!ͱjbe4'uL9mer: 걥4^٧ߠCd$6mRKI;5bu:hs<<{$>օXФLc7рG;FegxuD0+ <R[ݐk&ʅ*uڦ7)r+}Y]`TȨ]!ѹ;J2FcEXb `y{[b"I}Y(Do+CGsDTU &wcy , I3ͤLr---\xLO5,+'@@7TQW=+7*./dFWs~E]. zR! $UcTk-ƴ"*.!CS:T&1'Hx|>A˹@+u"n:jyAVeiJvZPjdUkއ/ *b%6J`c#RƗqMәu^PCAҔCaAqז,5/`F< &aK#SFL]R݅Mg9){{6$EaN`L[ݡfiCg ]6fAU΃^[2SG$$'@Ƴ串-5c.۪:3#ˤyFU`rk=@ nB]q\dy#vUϫ+ỳw.!*_ k=(F$ ;KL֤uK-Y5BGQm E`}K__ciJiiKZR!&M,[RxH.נy(Cun3t'ߤstvaJ]]]de"2fr ^e1=PJM5j;};<]ȶ<ŶG=G71\sefAJ{FRDbd12x7?L_O'o]۞7F<R2'T0i<5$if{k&S:gԓrnb6=nUpüx+;fޔHX"E.P*]D(r6gQ]@KAX^^Ƭ!{JbNFYY:V/ȅ؈yE=) fV>T[ռˊ ,ʳ\Fao/|v=:l#*kԀ:zQq&oұMbE9uc=v𚖔5CSms0AB/n2CU jGG\ӦՆ}xgi,208η%A\pw^TSEQY8ʽ A$}cX LGb۴xhתoos C$FiYd( *s +d .Iq:mr[Ёpgw8 !-|VOl zW:g\}OtNlܤy] ,f0`'ͮ}F%KE?3X!KiF9ĥ*2Y1òZG0=w l Wu *pqqEz6j+ ihh(pkAIt3 23`EҀGPUGHN<>>&5ʞ`[kg IlU{lmmZ$Ydu޲bB6A[JM]G~+fdFC+U #Ć]:sPON@FyEИHUSv!( d8հ{}ƂB1/EӜ-.‚Paq{*z3tSҷ9(jv%/R}yd"0S$Yf99]8ItVtHAt(I\*K-Iu8]F6 icgD{2"R N=9R>L&X`T>7)8F]84tgÀ#om * fj`\txϧ ggYCBa,89NTzp]5G\3],hSYд^$+uc"M3Gۛڴ!Պd'*%>TɲrwexĦ#aU.èfr(P63p׮Lga&1\ ø"6P>}:``!Df,H`Xgꪘ+BE KCMaϛ4<~o6MV<}՛1eφ0?o3!ɼQ< }nss5x]>/*T˹RO1@5<HN6r6̸#taE0ӜqR=[[!.#6 C\eJb=d"L-c~#Y=HGdCEC#;w$G] o *bA%oUYu"tc/}Jg&6GT-ӍIʸ m98sZ=F]['ӓ=.{ϵ 2Hn8p%{N#' 4σC{!ϳgI~g€bTzpYw0բ'U\Ϡ#PbN4^xC x\4eyh7@3& { >5HDN")uC;ym r"cPłv>ƙXz_zkfz :j=f(FCaл-hOͺۚ2/ +Zuqat_tT00D=g3fsq9Ҕ]l5F!INY}ķ vvΗ -MDg،lhH|Cv4zFa5qrwOЍt_geMYZiu}ۺ'# XM>{# d,Jol_yEˢV<02ګw{3aoZ]ԉ2T_:ǂg Y2Z_yvgF 3UG'Rvf,;h`k}WH 5o0fxjmR|֓mUaD4 5iU%^Pe 4@,FWɧ +(Q;v}feh_gx9Cx(ͳ0wIɩuË2i?uoΥ)* 56YDj|yatWlY)t1 =@WkA8!`C1TہeA9YGn ĽrJƫSм74N-DoČBM=MKgyr|g`hW\;  rѺuڱwr g(=cZ1GPM[sl/M9՗\i'ߝ^ o7ߺ_;oʍ&u#Ad騵cBE;HĬ tc~m'$ 1yT 1@l'<&P6:Vq`+ݲ0zDTro}Pzx1^ 0^GV{hr"'bk،DT%jͷ.M\ kҎ.xhbBO*Vd'&guj 3Qs!-;xиGv"S$s4!q`ԻiyT5T',y|IyўLk9*a5 2a cƚ^$~/` jMH!wḾꩈ7^]ٴOV{Ț B7Y`~fZ Hn |= ][W+S D%`]KL) q u20[,<z Gm-:Xgw?/ 1z;٥@5XZ2sLsɂ+<X!dl!,'1ju^F5q` ]ZgEAKbFFQ]|~&&,hk@"4I 1$ɁI =ef+g\g\8b1ac,_#DkrB[/ϊƪP,%&$7=n3'H\f R@R= Z»Y"V:G. kH r Z*4,f߇>Ew?|QŠh#7#k:vG]إ ƫ ~@Y}NUo7×vU@g1q=W3Ǐ^mR};y7]W{ ; ϡ;fPNU<\8&6_5밅6ÂBRhU=R]Zs>'toRc c߂q{[o+dx>Bo۫%5tYE+u3iuJY*$oHzsj f#/@eAc pB;-ԢHZC\k}۸.1W JY>5>E'tO8Rc{1aX92[CSd8/.3d3hhK!. qχ:fEZ*]Ӏ]#؛ _<dRIɬgIesX,9o|[F~. luXĕO}j@1Wڿ' WJ8C3n)_(*J~!bGUYYBd:1@a9(dq޿]-+>J鹒B'hZ8BDkOR+qCX.~z _/#1h kxUϓ8ޜy_*9)4 Kj(3HcºJGRMR})˵ XPM֝@~S5niAiZ5 =95/DL: tM@"G}|֧=g cj/xbgvK敓©y%Υ%zdM3jjGBT;jhMx /j*u2Ň٧B[55}jIТB>5-X[o 8BOݏm}O>6x 0@qpSqEx'2a r>L@fM0Ǽ~MkyTkE&X.>_tm9sPv]}SdQ m%pp8ǔ>0nYS* CJE6 Z4q'ôUxS_Y>֡}+Ç_~2?Gda Ber⟑Qm7d]LFS~2|;[Ia,5ϠV2u sj2o^QVPtFeGQgk"Yi @\1ʼ"B@D*=&/c.O~]v[rg? ) H89W@Qw ܱ 8WVV( n錫ٳHlGc:@1W(,k߂bf?)yCu\`oyx!{ms \p&dxGD?ײ~I"!JO Q(ZvRуaY,6-E7Wo ayUNN%Z 4TkaII`2f/qYyzb HAy8ų?\;Ƌx $y#OVDD8z Ça"dx2R_SpxIWV&w-wJBB 6Xʼi}ax6jkU@`Tc\j>LěWKۘKcukN" _V; ;@i@4PPb`c<;Z<\y8z➇˰ U080]e! *-"pP#j N֢wYe!4{ee}tPXT a)+uTC[ٷV@R0XFIV\hb⁨7"בbY0EIڅiZ< ޳ҔhϪ°y?S||9r(Cj z~L``GEއ`5m>?ϭ3DžYzL c9x pf~qqOPx\>okߴahj|kQHFYZ]+Tin6;)29~6 RKfeֆMNUu`\^ Xain DZZ4U[(/A%RT jr֞b{zkxTa&Ie{|@)m ž >o:<9r>q℣]D0^L/bc㷶hdS#GӋnxF0A`Qc %a {z4dWZ`*gL+8i#AlaD^O^?煷ܬ1\pg0cC@`= GS(bПg~{6õoPŻ9xd%x| ZQ+5G_a>fY2^ }UX8 ))v)]C]VMDJi饚1Htk9W/q|(Lq󾞐sENdkURw=)W z?2uUr+x!^؋;(ڨ#6ч1a\ę֨ i/}@h[ L7򙯞wkǿ?B}w Gn^>xčNjb&)+ktU(W":XSzP:ZA2sߜBu?:LiM0RZJ[Y8*eui`=ѱުsų_xNJƎ{eǨTHz!ҒEJ%Ud N~1aAU66UU1x-?y0Wx)GG٣gzί׼p] Gn^wüR, Җ^ %/Syklgn y!a g>$OB 8ѓKYWhUN&уXO=3\q *NjzX< 00sѥҀ ;90F z% cø,q Ƞx ڌVoBao,h)xcW?t-n o6,Y-,:Ei!>$5lm}2ee5S|hZb$0W1c+zAڿ# CG}8!Njcm~\^fU8 w8T7%fMR=K0|yyyq1M YX0GWd2 0^ mFΫXe1K ,Jvox>ЩwR_[G2̼+b*xvĀ:X /F mZ޺9T<{B@&b!Blǻ[5g/ ڪ(q:(GN#}*& WL: ՉupׅՃt 0 ƛAd%WUtw(fj8Q-y-cF_p[n xn%i #z"ҸG/b蘌=A*`8ljJ,.-` 3-Dqѳ1:- ev~ǁ2@,`*Ⱥ'y#~\Fj!u.̩+J6ԕdV\xen6RjD ,rz5Ff̌wbuֳG/½?9ppݝWW=oCR.+wwuWLw[g4ufWb1+zVn^{LZ*(>WԦ7}3Cx^,̄y2A@<ž0H'p7)8&;+z,k^~[1ݽo;WgUC&OdTK~PΖ۠pD|r )Ūq V󵻿tVL/g_7naY|_NXw?_I}UC"GI?`䚋Fp4Uu* Cp7,7uUV\n)],~sP_/ݽF)21nh2^KУF钪MRB7*!'|ꑥ-zwU`.>jN! ssx,jh7?~ըͺF n즪c?YEљ6j/>ա>שGTU 46ƐLjWB$+W*bv`s#A|^1pAja`v&!MD:S#r-(Cͪ ;;zBy{WQ#Q#nب GZ7[!m=7FyTE)բ_ܯoױ7 q-#!KGr{:QH)惃v*]+cݮ&khzڿ 滎dW^4Qng A<1@֐U Rҹa`vT`6ZvRcol}Ѳ7 s7w=!mݵm۹ÓOf'4gb5Bp$O~Yx^uNj*{U% mB0JDw8+3C(yXx'أEL9%Ӧ@Ua6 }H:ĉ`φ5W"3)mF!( 4Ro̝;;mSG9>4dzٮ*o>ҮsIn(JDbn==Zˁj^cáM=K"NtxBz ư49;;+`kRPJD/p͛`+hk"qݵ.:T8rH>DR=O$rKK0 =N!@OazPޚ:q!ZWkIS!ed ײ3lNI]^^ݦ9r':ABPY..!=L+v!uݘ-yUHIC H CJ*}<ΆG.:rq<@LJ இ(:)*ȉ$o˵ו!ʨ=K tdAu0R'k,"yx@UH /_d/DoM܉.(%Fr!b>5j"['>/[0FTqCu;z%s?!*DkB Q"R6B2-fjĶJ6 F`'zX,訚Nv+L@* u}uu/..d2bES@׎ Hr;3RPVj*^OQ!ڲB$\)zRWG(A7w}l P !/ڥR:I}َXH(2%^9QyӆQ8d2/SKuCN LΓUv놌N2cxoMQ-(r4A4oG)xt*LcNΒdtVG`jp/D3H\t(/%<TI.ەc² b!;1ܯ.׵3Q5"pb)#dwvKƋh d6g1p R5Z`OOOP'Hy!FBE d"D $U33ےU~5ZF_M AB d<&#5oP;zQΙVH AꊤH:. osիWr4p,Hj 4HڔiCwkuY,]|$' qԵ k\ ԏFGn ޢ2TDĂ]^^D~A<쩓'MҽPUr||$Ղ 9&YIGE{., pBA@%w}ubJm(S!d Ci{xxT4A@W2wwT^RHA[ k[<8&ek ϙL:A}zzaU*}t<.HͤU~~bu%kk'XI-BS[N!K%GZv{I1OAAyG=ȩ"1i*館4fI @<`4Kɒ{yf'[BQ% ிֻϨA!Q+{hEaSQ& \LٺQ*Ip~IYD΁l`gP ?F,sw:oB$G6|DO7`n汤 ω`w&)` I(A$ BA@ AA@ AA! H A  H A  A$ BA$ BA@ AA! AA! H A  )) _x_IENDB`fabulous-0.3.0/fabulous/casts.py000066400000000000000000000025351274117624500166570ustar00rootroot00000000000000# Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """A series of functions that provide useful casts from strings into other common objects. Mainly for usage with input_object in the above package. """ from os import path def yes_no(value): """For a yes or no question, returns a boolean. """ if value.lower() in ('yes','y'): return True if value.lower() in ('no','n'): return False raise ValueError("value should be 'yes' or 'no'") def file(value, **kwarg): """value should be a path to file in the filesystem. returns a file object """ #a bit weird, but I don't want to hard code default values try: f = open(value, **kwarg) except IOError as e: raise ValueError("unable to open %s : %s" % (path.abspath(value), e)) return f fabulous-0.3.0/fabulous/color.py000066400000000000000000000546221274117624500166640ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ fabulous.color ~~~~~~~~~~~~~~ The color module provides an object-oriented abstraction for stylized text inside the terminal. This includes things like bold text, blinking text, 4-bit ANSI colors and 8-bit xterm256 colors. """ import sys import functools from fabulous.compatibility import printy from fabulous import utils, xterm256, grapefruit try: unicode = unicode except NameError: unicode = str basestring = (str, bytes) OVERLINE = u'\u203e' def esc(*codes): """Produces an ANSI escape code string from a list of integers This is a low level function that is abstracted by the other functions and classes in this module. """ return "\x1b[%sm" % (";".join([str(c) for c in codes])) class ColorString(object): r"""Abstract base class for stylized string-like objects. Subclasses make it possible to compose stylized text:: >>> str(red("hello")) '\x1b[31mhello\x1b[39m' >>> str(bold(red("hello"))) '\x1b[1m\x1b[31mhello\x1b[39m\x1b[22m' >>> str(plain("hello ", bold("world"))) 'hello \x1b[1mworld\x1b[22m' These objects also provide string length without taking into consideration the ANSI escape codes:: >>> len(red("hello")) 5 >>> len(str(red("hello"))) 15 >>> len(bold(red("hello"))) 5 >>> len(bold("hello ", red("world"))) 11 """ sep = "" fmt = "%s" def __init__(self, *items): self.items = items def __str__(self): return self.fmt % (self.sep.join([unicode(s) for s in self.items])) def __repr__(self): return repr(unicode(self)) def __len__(self): return sum([len(item) for item in self.items]) def __add__(self, cs): if not isinstance(cs, (basestring, ColorString)): msg = "Concatenatation failed: %r + %r (Not a ColorString or str)" raise TypeError(msg % (type(cs), type(self))) return ColorString(self, cs) def __radd__(self, cs): if not isinstance(cs, (basestring, ColorString)): msg = "Concatenatation failed: %r + %r (Not a ColorString or str)" raise TypeError(msg % (type(self), type(cs))) return ColorString(cs, self) @property def as_utf8(self): """A more readable way to say ``unicode(color).encode('utf8')`` """ return unicode(self).encode('utf8') class ColorString256(ColorString): r"""Base class for 256-color stylized string-like objects. See the :class:`.fg256`, :class:`.bg256`, :class:`.highlight256`, and :class:`.complement256` classes for more information. """ def __init__(self, color, *items): (r, g, b) = parse_color(color) self.color = xterm256.rgb_to_xterm(r, g, b) self.items = items def __str__(self): return self.fmt % ( self.color, self.sep.join([unicode(s) for s in self.items])) class plain(ColorString): r"""Plain text wrapper This class is useful for concatenating plain strings with :class:`.ColorString` objects. For example:: from fabulous.color import plain >>> len(plain("hello ", bold("kitty"))) 11 """ pass class bold(ColorString): r"""Bold text wrapper This class creates a string-like object containing bold or bright text. It also brightens the foreground and background colors. This is supported by all terminals that support ANSI color codes. Example usage:: from fabulous.color import bold print bold('i am bold!') print plain('hello ', bold('world')) The ANSI escape codes are as follows:: >>> str(bold("hello")) '\x1b[1mhello\x1b[22m' """ fmt = esc(1) + "%s" + esc(22) class italic(ColorString): r"""Italic text wrapper This class creates a string-like object containing italic text, which is supported by almost no terminals. The ANSI escape codes are as follows:: >>> str(italic("hello")) '\x1b[3mhello\x1b[23m' """ fmt = esc(3) + "%s" + esc(23) class underline(ColorString): r"""Underline text wrapper This class creates a string-like object containing underline text. This is supported by SOME terminals, as documented in the terminal support section. Example usage:: from fabulous.color import underline print underline('i am underlined!') print plain('hello ', underline('world')) The ANSI escape codes are as follows:: >>> str(underline("hello")) '\x1b[4mhello\x1b[24m' """ fmt = esc(4) + "%s" + esc(24) class underline2(ColorString): r"""Alternative underline text wrapper See also: :class:`.underline`. The ANSI escape codes are as follows:: >>> str(underline2("hello")) '\x1b[21mhello\x1b[24m' """ fmt = esc(21) + "%s" + esc(24) class strike(ColorString): r"""Strike-through text wrapper This class creates a string-like object containing strike-through text, which is supported by very few terminals. Example usage:: from fabulous.color import strike print strike('i am stricken!') print plain('hello ', strike('world')) The ANSI escape codes are as follows:: >>> str(strike("hello")) '\x1b[9mhello\x1b[29m' """ fmt = esc(9) + "%s" + esc(29) class blink(ColorString): r"""Blinking text wrapper This class creates a string-like object containing blinking text. This is supported by SOME terminals, as documented in the terminal support section. Example usage:: from fabulous.color import blink print blink('i am underlined!') print plain('hello ', blink('world')) The ANSI escape codes are as follows:: >>> str(blink("hello")) '\x1b[5mhello\x1b[25m' """ fmt = esc(5) + "%s" + esc(25) class flip(ColorString): r"""Flips background and foreground colors For example:: from fabulous.color import flip, red print flip(red('hello')) Is equivalent to the following on a black terminal:: from fabulous.color import black, red_bg print red_bg(black('hello')) The ANSI escape codes are as follows:: >>> str(flip("hello")) '\x1b[7mhello\x1b[27m' """ fmt = esc(7) + "%s" + esc(27) class black(ColorString): r"""Black foreground text wrapper This class creates a string-like object containing text with a black foreground. Example usage:: from fabulous.color import black print black('i am black!') print plain('hello ', black('world')) Text can be made dark grey by using :class:`.bold`:: from fabulous.color import bold, black print bold(black('i am dark grey!')) The ANSI escape codes are as follows:: >>> str(black("hello")) '\x1b[30mhello\x1b[39m' """ fmt = esc(30) + "%s" + esc(39) class red(ColorString): r"""Red foreground text wrapper This class creates a string-like object containing text with a red foreground. Example usage:: from fabulous.color import red print red('i am red!') print plain('hello ', red('world')) Text can be made bright red by using :class:`.bold`:: from fabulous.color import bold, red print bold(red('i am bright red!')) The ANSI escape codes are as follows:: >>> str(red("hello")) '\x1b[31mhello\x1b[39m' """ fmt = esc(31) + "%s" + esc(39) class green(ColorString): r"""Green foreground text wrapper This class creates a string-like object containing text with a green foreground. Example usage:: from fabulous.color import green print green('i am green!') print plain('hello ', green('world')) Text can be made bright green by using :class:`.bold`:: from fabulous.color import bold, green print bold(green('i am bright green!')) The ANSI escape codes are as follows:: >>> str(green("hello")) '\x1b[32mhello\x1b[39m' """ fmt = esc(32) + "%s" + esc(39) class yellow(ColorString): r"""Yellow foreground text wrapper This class creates a string-like object containing text with a "yellow" foreground, which in many terminals is actually going to look more brownish. Example usage:: from fabulous.color import yellow print yellow('i am yellow brownish!') print plain('hello ', yellow('world')) Text can be made true bright yellow by using :class:`.bold`:: from fabulous.color import bold, yellow print bold(yellow('i am bright yellow!')) The ANSI escape codes are as follows:: >>> str(yellow("hello")) '\x1b[33mhello\x1b[39m' """ fmt = esc(33) + "%s" + esc(39) class blue(ColorString): r"""Blue foreground text wrapper This class creates a string-like object containing text with a blue foreground. Example usage:: from fabulous.color import blue print blue('i am dark blue!') print plain('hello ', blue('world')) Text can be made sky blue by using :class:`.bold`:: from fabulous.color import bold, blue print bold(blue('i am sky blue!')) The ANSI escape codes are as follows:: >>> str(blue("hello")) '\x1b[34mhello\x1b[39m' """ fmt = esc(34) + "%s" + esc(39) class magenta(ColorString): r"""Purple/magenta foreground text wrapper This class creates a string-like object containing text with a magenta foreground. Although in many terminals, it's going to look more purple. Example usage:: from fabulous.color import magenta print magenta('i am magenta purplish!') print plain('hello ', magenta('world')) Text can be made hot pink by using :class:`.bold`:: from fabulous.color import bold, magenta print bold(magenta('i am hot pink!')) The ANSI escape codes are as follows:: >>> str(magenta("hello")) '\x1b[35mhello\x1b[39m' """ fmt = esc(35) + "%s" + esc(39) class cyan(ColorString): r"""Cyan foreground text wrapper This class creates a string-like object containing text with a cyan foreground. Example usage:: from fabulous.color import cyan print cyan('i am cyan!') print plain('hello ', cyan('world')) Text can be made bright cyan by using :class:`.bold`:: from fabulous.color import bold, cyan print bold(cyan('i am bright cyan!')) The ANSI escape codes are as follows:: >>> str(cyan("hello")) '\x1b[36mhello\x1b[39m' """ fmt = esc(36) + "%s" + esc(39) class white(ColorString): r"""White foreground text wrapper This class creates a string-like object containing text with a light grey foreground. Example usage:: from fabulous.color import white print white('i am light grey!') print plain('hello ', white('world')) Text can be made true white by using :class:`.bold`:: from fabulous.color import bold, white print bold(white('i am bold white!')) The ANSI escape codes are as follows:: >>> str(white("hello")) '\x1b[37mhello\x1b[39m' """ fmt = esc(37) + "%s" + esc(39) class highlight_black(ColorString): r"""Dark grey highlight text wrapper This is equivalent to composing :class:`.bold`, :class:`.flip`, and :class:`.black`. """ fmt = esc(1, 30, 7) + "%s" + esc(22, 27, 39) class highlight_red(ColorString): r"""Red highlight text wrapper This is equivalent to composing :class:`.bold`, :class:`.flip`, and :class:`.red`. """ fmt = esc(1, 31, 7) + "%s" + esc(22, 27, 39) class highlight_green(ColorString): r"""Green highlight text wrapper This is equivalent to composing :class:`.bold`, :class:`.flip`, and :class:`.green`. """ fmt = esc(1, 32, 7) + "%s" + esc(22, 27, 39) class highlight_yellow(ColorString): r"""Yellow highlight text wrapper This is equivalent to composing :class:`.bold`, :class:`.flip`, and :class:`.yellow`. """ fmt = esc(1, 33, 7) + "%s" + esc(22, 27, 39) class highlight_blue(ColorString): r"""Blue highlight text wrapper This is equivalent to composing :class:`.bold`, :class:`.flip`, and :class:`.blue`. """ fmt = esc(1, 34, 7) + "%s" + esc(22, 27, 39) class highlight_magenta(ColorString): r"""Hot pink highlight text wrapper This is equivalent to composing :class:`.bold`, :class:`.flip`, and :class:`.magenta`. """ fmt = esc(1, 35, 7) + "%s" + esc(22, 27, 39) class highlight_cyan(ColorString): r"""Cyan highlight text wrapper This is equivalent to composing :class:`.bold`, :class:`.flip`, and :class:`.cyan`. """ fmt = esc(1, 36, 7) + "%s" + esc(22, 27, 39) class highlight_white(ColorString): r"""White highlight text wrapper This is equivalent to composing :class:`.bold`, :class:`.flip`, and :class:`.yellow`. """ fmt = esc(1, 37, 7) + "%s" + esc(22, 27, 39) class black_bg(ColorString): r"""Black background text wrapper This class creates a string-like object containing text with a black background. On properly configured terminals, this will do nothing. Example usage:: from fabulous.color import black_bg print black_bg('i have a black background!') print plain('hello ', black_bg('world')) The ANSI escape codes are as follows:: >>> str(black_bg("hello")) '\x1b[40mhello\x1b[49m' """ fmt = esc(40) + "%s" + esc(49) class red_bg(ColorString): r"""Red background text wrapper This class creates a string-like object containing text with a red background. Example usage:: from fabulous.color import red_bg print red_bg('i have a red background!') print plain('hello ', red_bg('world')) The ANSI escape codes are as follows:: >>> str(red_bg("hello")) '\x1b[41mhello\x1b[49m' """ fmt = esc(41) + "%s" + esc(49) class green_bg(ColorString): r"""Green background text wrapper This class creates a string-like object containing text with a green background. Example usage:: from fabulous.color import green_bg print green_bg('i have a green background!') print plain('hello ', green_bg('world')) The ANSI escape codes are as follows:: >>> str(green_bg("hello")) '\x1b[42mhello\x1b[49m' """ fmt = esc(42) + "%s" + esc(49) class yellow_bg(ColorString): r"""Yellow background text wrapper This class creates a string-like object containing text with a yellow background. Example usage:: from fabulous.color import yellow_bg print yellow_bg('i have a yellow background!') print plain('hello ', yellow_bg('world')) The ANSI escape codes are as follows:: >>> str(yellow_bg("hello")) '\x1b[43mhello\x1b[49m' """ fmt = esc(43) + "%s" + esc(49) class blue_bg(ColorString): r"""Blue background text wrapper This class creates a string-like object containing text with a blue background. Example usage:: from fabulous.color import blue_bg print blue_bg('i have a blue background!') print plain('hello ', blue_bg('world')) The ANSI escape codes are as follows:: >>> str(blue_bg("hello")) '\x1b[44mhello\x1b[49m' """ fmt = esc(44) + "%s" + esc(49) class magenta_bg(ColorString): r"""Magenta background text wrapper This class creates a string-like object containing text with a magenta background. Example usage:: from fabulous.color import magenta_bg print magenta_bg('i have a magenta background!') print plain('hello ', magenta_bg('world')) The ANSI escape codes are as follows:: >>> str(magenta_bg("hello")) '\x1b[45mhello\x1b[49m' """ fmt = esc(45) + "%s" + esc(49) class cyan_bg(ColorString): r"""Cyan background text wrapper This class creates a string-like object containing text with a cyan background. Example usage:: from fabulous.color import cyan_bg print cyan_bg('i have a cyan background!') print plain('hello ', cyan_bg('world')) The ANSI escape codes are as follows:: >>> str(cyan_bg("hello")) '\x1b[46mhello\x1b[49m' """ fmt = esc(46) + "%s" + esc(49) class white_bg(ColorString): r"""White background text wrapper This class creates a string-like object containing text with a white background. Example usage:: from fabulous.color import white_bg print white_bg('i have a white background!') print plain('hello ', white_bg('world')) The ANSI escape codes are as follows:: >>> str(white_bg("hello")) '\x1b[47mhello\x1b[49m' """ fmt = esc(47) + "%s" + esc(49) class fg256(ColorString256): r"""xterm256 foreground color wrapper This class creates a string-like object that has an xterm256 color. The color is specified as a CSS color code, which is automatically quantized to the available set of xterm colors. These colors are more dependable than the 4-bit colors, because 8-bit colors don't get changed by the terminal theme. They will consistently be the requested color, which is calculated using a simple math formula. However it is worth noting that in Terminal.app on Mac OS, 8-bit colors appear to be designed rather than formulaic, so they look much nicer. Example usage:: from fabulous import fg256, plain print fg256('#F00', 'i am red!') print fg256('#FF0000', 'i am red!') print fg256('magenta', 'i am', ' magenta!') print plain('hello ', fg256('magenta', 'world')) The ANSI escape codes look as follows:: >>> str(fg256('red', 'hello')) '\x1b[38;5;196mhello\x1b[39m' """ fmt = esc(38, 5, "%d") + "%s" + esc(39) class bg256(ColorString256): r"""xterm256 background color wrapper This class creates a string-like object that has an xterm256 color. The color is specified as a CSS color code, which is automatically quantized to the available set of xterm colors. These colors are more dependable than the 4-bit colors, because 8-bit colors don't get changed by the terminal theme. They will consistently be the requested color. However it is worth noting that in Terminal.app on Mac OS, 8-bit background colors are ever so slightly different than their foreground equivalent. Therefore Terminal.app has effectively 512 colors. Example usage:: from fabulous import bg256, plain print bg256('#F00', 'i have a red background!') print bg256('#FF0000', 'i have a red background!') print bg256('magenta', 'i have a', ' magenta background!') print plain('hello ', bg256('magenta', 'world')) The ANSI escape codes look as follows:: >>> str(bg256('red', 'hello')) '\x1b[48;5;196mhello\x1b[49m' """ fmt = esc(48, 5, "%d") + "%s" + esc(49) class highlight256(ColorString256): r"""Highlighted 8-bit color text This is equivalent to composing :class:`.bold`, :class:`.flip`, and :class:`.fg256`. """ fmt = esc(1, 38, 5, "%d", 7) + "%s" + esc(27, 39, 22) class complement256(ColorString256): r"""Highlighted 8-bit color text This class composes :class:`.bold`, :class:`.flip`, and :class:`.bg256`. Then it invokes :meth:`complement` to supply the polar opposite :class:`fg256` color. This looks kind of hideous at the moment. We're planning on finding a better formula for complementary colors in the future. """ fmt = esc(1, 38, 5, "%d", 48, 5, "%d") + "%s" + esc(49, 39, 22) def __init__(self, color, *items): self.bg = xterm256.rgb_to_xterm(*parse_color(color)) self.fg = xterm256.rgb_to_xterm(*complement(color)) self.items = items def __str__(self): return self.fmt % ( self.fg, self.bg, self.sep.join([unicode(s) for s in self.items])) def h1(title, line=OVERLINE): """Prints bold text with line beneath it spanning width of terminal """ width = utils.term.width printy(bold(title.center(width)).as_utf8) printy(bold((line * width)[:width]).as_utf8) def parse_color(color): r"""Turns a color into an (r, g, b) tuple >>> parse_color('white') (255, 255, 255) >>> parse_color('#ff0000') (255, 0, 0) >>> parse_color('#f00') (255, 0, 0) >>> parse_color((255, 0, 0)) (255, 0, 0) >>> from fabulous import grapefruit >>> parse_color(grapefruit.Color((0.0, 1.0, 0.0))) (0, 255, 0) """ if isinstance(color, basestring): color = grapefruit.Color.NewFromHtml(color) if isinstance(color, int): (r, g, b) = xterm256.xterm_to_rgb(color) elif hasattr(color, 'rgb'): (r, g, b) = [int(c * 255.0) for c in color.rgb] else: (r, g, b) = color assert isinstance(r, int) and 0 <= r <= 255 assert isinstance(g, int) and 0 <= g <= 255 assert isinstance(b, int) and 0 <= b <= 255 return (r, g, b) def complement(color): r"""Calculates polar opposite of color This isn't guaranteed to look good >_> (especially with brighter, higher intensity colors.) This will be replaced with a formula that produces better looking colors in the future. >>> complement('red') (0, 255, 76) >>> complement((0, 100, 175)) (175, 101, 0) """ (r, g, b) = parse_color(color) gcolor = grapefruit.Color((r / 255.0, g / 255.0, b / 255.0)) complement = gcolor.ComplementaryColor() (r, g, b) = [int(c * 255.0) for c in complement.rgb] return (r, g, b) def section(title, bar=OVERLINE, strm=sys.stdout): """Helper function for testing demo routines """ width = utils.term.width printy(bold(title.center(width))) printy(bold((bar * width)[:width])) fabulous-0.3.0/fabulous/compatibility.py000066400000000000000000000017541274117624500204150ustar00rootroot00000000000000# Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function import sys def printy(s): """Python 2/3 compatible print-like function""" if hasattr(s, 'as_utf8'): if hasattr(sys.stdout, 'buffer'): sys.stdout.buffer.write(s.as_utf8) sys.stdout.buffer.write(b"\n") else: sys.stdout.write(s.as_utf8) sys.stdout.write(b"\n") else: print(s) fabulous-0.3.0/fabulous/debug.py000066400000000000000000000037301274117624500166260ustar00rootroot00000000000000# Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ fabulous.debug ~~~~~~~~~~~~~~ The debug module provides the ability to print images as ASCII. It isn't a good ASCII representation like cacalib. This module is mostly intended for debugging purposes (hence the name.) """ from __future__ import print_function import sys import itertools from fabulous import image class DebugImage(image.Image): """Visualize optimization techniques used by :class:`Image` """ def reduce(self, colors): need_reset = False line = '' for color, items in itertools.groupby(colors): if color is None: if need_reset: line = line[:-1] + ">" need_reset = False line += 'T' + (self.pad * len(list(items)))[1:] elif color == "EOL": if need_reset: line = line[:-1] + ">" need_reset = False yield line.rstrip(' T') else: yield line.rstrip(' T') line = '' else: need_reset = True line += '<' + (self.pad * len(list(items)))[1:] def main(): """I provide a command-line interface for this module """ for imgpath in sys.argv[1:]: for line in DebugImage(imgpath): print(line) if __name__ == '__main__': main() fabulous-0.3.0/fabulous/demo.py000066400000000000000000000145741274117624500164740ustar00rootroot00000000000000# Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os from fabulous import text, utils, image, debug, xterm256 from fabulous.color import * from fabulous.compatibility import printy import fabulous try: input = raw_input except NameError: pass def wait(): input("\nPress " + bold("enter") + " for more fun... ") printy("") def demo_image(): section("Semi-Transparent PNG") imp = " from fabulous import image\n " printy(bold(imp + 'print image.Image("balls.png")\n')) balls = 'balls.png' fabdir = os.path.dirname(fabulous.__file__) for fn in ['balls.png', 'fabulous/balls.png', os.path.join(fabdir, 'balls.png')]: if os.path.exists(fn): balls = fn break if not os.path.exists(balls): import urllib ugh = urllib.urlopen('http://lobstertech.com/media/img/balls.png') open('balls.png', 'w').write(ugh.read()) balls = 'balls.png' for line in image.Image(balls): printy(line) wait() section("Yes the output is optimized (JELLY-FISH)") imp = " from fabulous import debug\n " printy(bold(imp + 'print debug.DebugImage("balls.png")\n')) for line in debug.DebugImage(balls): printy(line) wait() def demo_text(): section('Fabulous Text Rendering') imp = " from fabulous import text\n " printy(bold(imp + 'print text.Text("Fabulous", shadow=True, skew=5)\n')) printy(text.Text("Fabulous", shadow=True, skew=5)) wait() def demo_color_4bit(): section("Fabulous 4-Bit Colors") printy(("style(...): " + bold("bold") +" "+ underline("underline") +" "+ flip("flip") + " (YMMV: " + italic("italic") +" "+ underline2("underline2") +" "+ strike("strike") +" "+ blink("blink") + ")\n")) printy(("color(...) " + black("black") +" "+ red("red") +" "+ green("green") +" "+ yellow("yellow") +" "+ blue("blue") +" "+ magenta("magenta") +" "+ cyan("cyan") +" "+ white("white"))) printy(("bold(color(...)) " + bold(black("black") +" "+ red("red") +" "+ green("green") +" "+ yellow("yellow") +" "+ blue("blue") +" "+ magenta("magenta") +" "+ cyan("cyan") +" "+ white("white")))) printy(plain( 'highlight_color(...) ', highlight_black('black'), ' ', highlight_red('red'), ' ', highlight_green('green'), ' ', highlight_yellow('yellow'), ' ', highlight_blue('blue'), ' ', highlight_magenta('magenta'), ' ', highlight_cyan('cyan'), ' ', highlight_white('white'))) printy(("bold(color_bg(...)) " + bold(black_bg("black") +" "+ red_bg("red") +" "+ green_bg("green") +" "+ yellow_bg("yellow") +" "+ blue_bg("blue") +" "+ magenta_bg("magenta") +" "+ cyan_bg("cyan") +" "+ white_bg("white")))) wait() def demo_color_8bit(): section("Fabulous 8-Bit Colors") for code in ["bold(fg256('red', ' lorem ipsum '))", "bold(bg256('#ff0000', ' lorem ipsum '))", "highlight256((255, 0, 0), ' lorem ipsum ')", "highlight256('#09a', ' lorem ipsum ')", "highlight256('green', ' lorem ipsum ')", "highlight256('magenta', ' lorem ipsum ')", "highlight256('indigo', ' lorem ipsum ')", "highlight256('orange', ' lorem ipsum ')", "highlight256('orangered', ' lorem ipsum ')"]: printy("%-42s %s" % (code, eval(code))) printy('') # grayscales line = " " for xc in range(232, 256): line += bg256(xc, ' ') printy(line) line = " " for xc in range(232, 256)[::-1]: line += bg256(xc, ' ') printy(line) printy('') cube_color = lambda x,y,z: 16 + x + y*6 + z*6*6 for y in range(6): line = " " for z in range(6): for x in range(6): line += bg256(cube_color(x, y, z), ' ') line += " " printy(line) wait() def full_chart(): # grayscales line = " " for xc in range(232, 256): line += bg256(xc, ' ') printy(line) line = " " for xc in range(232, 256)[::-1]: line += bg256(xc, ' ') printy(line) printy('') # cube printy("") cube_color = lambda x,y,z: 16 + x + y*6 + z*6*6 for y in range(6): line = " " for z in range(6): for x in range(6): line += bg256(cube_color(x, y, z), ' ') line += " " printy(line) printy("") def f(xc): s = highlight256(xc, "color %03d" % (xc)) rgb = xterm256.xterm_to_rgb(xc) rgbs = ' (%3d, %3d, %3d)' % rgb if rgb[0] == rgb[1] == rgb[2]: s += bold(rgbs) else: s += rgbs s += ' (%08d, %08d, %08d)' % tuple([int(bin(n)[2:]) for n in rgb]) return s def l(c1, c2): c1, c2 = f(c1), f(c2) assert len(c1) == len(c2) half = width // 2 assert half > len(c1) pad = " " * ((width // 2 - len(c1)) // 2) printy("%(pad)s%(c1)s%(pad)s%(pad)s%(c2)s" % { 'pad': pad, 'c1': c1, 'c2': c2}) width = utils.term.width for z1, z2 in zip((0, 2, 4), (1, 3, 5)): for y1, y2 in zip(range(6), range(6)): for x1, x2 in zip(range(6), range(6)): l(cube_color(x1, y1, z1), cube_color(x2, y2, z2)) printy("") def main(): # full_chart() demo_color_4bit() demo_color_8bit() demo_text() demo_image() if __name__ == '__main__': main() fabulous-0.3.0/fabulous/experimental/000077500000000000000000000000001274117624500176605ustar00rootroot00000000000000fabulous-0.3.0/fabulous/experimental/__init__.py000066400000000000000000000011351274117624500217710ustar00rootroot00000000000000# Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. fabulous-0.3.0/fabulous/experimental/canvas.py000066400000000000000000000026501274117624500215100ustar00rootroot00000000000000# Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import with_statement import time import curses class Canvas(object): def __init__(self, encoding='UTF-8'): self.encoding = encoding def __enter__(self): self.win = curses.initscr() curses.start_color() curses.init_color(200, 1000, 300, 0) curses.init_pair(1, 200, curses.COLOR_WHITE) return self def __exit__(self, type_, value, traceback): curses.endwin() def __setitem__(self, xy, val): self.win.attron(curses.color_pair(1)) (x, y) = xy self.win.addch(x, y, val) if __name__ == '__main__': import locale locale.setlocale(locale.LC_ALL, '') encoding = locale.getpreferredencoding() with Canvas(encoding=encoding) as canvas: canvas[5, 5] = 'Y' canvas.win.refresh() time.sleep(5.0) fabulous-0.3.0/fabulous/gotham.py000066400000000000000000000111351274117624500170150ustar00rootroot00000000000000# Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ fabulous.gotham ~~~~~~~~~~~~~~~ This is a gimmick feature that generates silly gothic poetry. This uses a simple mad lib algorithm. It has no concept of meter or rhyme. If you want a *proper* poetry generator, check out poemy2_ which uses markov chains and isledict. It's written by the same author as Fabulous. This module can be run as a command line tool:: jart@compy:~$ fabulous-gotham jart@compy:~$ python -m fabulous.gotham .. _poemy2: https://github.com/jart/poemy2 """ from __future__ import print_function import sys import random import itertools try: next except NameError: next = lambda x: x.next() them = ['angels', 'mourners', 'shadows', 'storm clouds', 'memories', 'condemned', 'hand of Heaven', 'stroke of death', 'damned', 'witches', 'corpses'] them_verb = ['follow', 'hover close', 'approach', 'loom', 'taunt', 'laugh', 'surround', 'compell', 'scour'] adj = ['cold', 'dead', 'dark', 'frozen', 'angry', 'ghastly', 'unholy', 'cunning', 'deep', 'morose', 'maligned', 'rotting', 'sickly'] me_part = ['neck', 'heart', 'head', 'eyes', 'soul', 'blood', 'essence', 'wisdom'] feeling = ['pain', 'horror', 'frenzy', 'agony', 'numbness', 'fear', 'love', 'terror', 'madness', 'torment', 'bitterness', 'misery'] angst = ['care', 'understand', 'question'] me_verb = ['flee', 'dance', 'flail madly', 'fall limply', 'hang my head', 'try to run', 'cry out', 'call your name', 'beg forgiveness', 'bleed', 'tremble', 'hear'] action = ['sever', 'crush', 'mutilate', 'slay', 'wound', 'smite', 'drip', 'melt', 'cast', 'mourn', 'avenge'] place = ['the witching hour', 'the gates of hell', 'the door', 'the path', 'death', 'my doom', 'oblivion', 'the end of life', 'Hell', 'nothingness', 'purgatory', 'void', 'earth', 'tomb', 'broken ground', 'barren land', 'swirling dust'] def lorem_gotham(): """Cheesy Gothic Poetry Generator Uses Python generators to yield eternal angst. When you need to generate random verbiage to test your code or typographic design, let's face it... Lorem Ipsum and "the quick brown fox" are old and boring! What you need is something with *flavor*, the kind of thing a depressed teenager with a lot of black makeup would write. """ w = lambda l: l[random.randrange(len(l))] er = lambda w: w[:-1]+'ier' if w.endswith('y') else (w+'r' if w.endswith('e') else w+'er') s = lambda w: w+'s' punc = lambda c, *l: " ".join(l)+c sentence = lambda *l: lambda: " ".join(l) pick = lambda *l: (l[random.randrange(len(l))])() while True: yield pick( sentence('the',w(adj),w(them),'and the',w(them),w(them_verb)), sentence('delivering me to',w(place)), sentence('they',w(action),'my',w(me_part),'and',w(me_verb),'with all my',w(feeling)), sentence('in the',w(place),'my',w(feeling),'shall',w(me_verb)), sentence(punc(',', er(w(adj)),'than the a petty',w(feeling))), sentence(er(w(adj)),'than',w(them),'in',w(place)), sentence(punc('!','oh my',w(me_part)),punc('!','the',w(feeling))), sentence('no one',s(w(angst)),'why the',w(them),w(them_verb + me_verb))) def lorem_gotham_title(): """Names your poem """ w = lambda l: l[random.randrange(len(l))] sentence = lambda *l: lambda: " ".join(l) pick = lambda *l: (l[random.randrange(len(l))])() return pick( sentence('why i',w(me_verb)), sentence(w(place)), sentence('a',w(adj),w(adj),w(place)), sentence('the',w(them))) def main(): """I provide a command-line interface for this module """ print() print("-~*~--~*~--~*~--~*~--~*~--~*~--~*~--~*~--~*~--~*~-") print(lorem_gotham_title().center(50)) print("-~*~--~*~--~*~--~*~--~*~--~*~--~*~--~*~--~*~--~*~-") print() poem = lorem_gotham() for n in range(16): if n in (4, 8, 12): print() print(next(poem)) print() if __name__ == '__main__': main() fabulous-0.3.0/fabulous/grapefruit.py000066400000000000000000001545021274117624500177140ustar00rootroot00000000000000#!/usr/bin/python # -*- coding: utf-8 -*- # # Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ fabulous.grapefruit ~~~~~~~~~~~~~~~~~~~ The grapefruit provides routines for color manipulation. This module is a bundled version of the grapefruit_ library. .. _grapefruit: https://github.com/xav/grapefruit """ from __future__ import division import sys # $Id$ __author__ = 'Xavier Basty ' __version__ = '0.1a3' # The default white reference, use 2° Standard Observer, D65 (daylight) _DEFAULT_WREF = (0.95043, 1.00000, 1.08890) _oneThird = 1.0 / 3 _srgbGammaCorrInv = 0.03928 / 12.92 _sixteenHundredsixteenth = 16.0 / 116 _RybWheel = ( 0, 26, 52, 83, 120, 130, 141, 151, 162, 177, 190, 204, 218, 232, 246, 261, 275, 288, 303, 317, 330, 338, 345, 352, 360) _RgbWheel = ( 0, 8, 17, 26, 34, 41, 48, 54, 60, 81, 103, 123, 138, 155, 171, 187, 204, 219, 234, 251, 267, 282, 298, 329, 360) class Color: '''Hold a color value. Example usage: To create an instance of the grapefruit.Color from RGB values: >>> from fabulous import grapefruit >>> r, g, b = 1, 0.5, 0 >>> col = grapefruit.Color.NewFromRgb(r, g, b) To get the values of the color in another colorspace: >>> h, s, v = col.hsv >>> l, a, b = col.lab To get the complementary of a color: >>> compl = col.ComplementaryColor(mode='rgb') >>> print(compl.hsl) (210.0, 1.0, 0.5) To directly convert RGB values to their HSL equivalent: >>> h, s, l = Color.RgbToHsl(r, g, b) ''' WHITE_REFERENCE = { 'std_A' : (1.09847, 1.00000, 0.35582), 'std_B' : (0.99093, 1.00000, 0.85313), 'std_C' : (0.98071, 1.00000, 1.18225), 'std_D50' : (0.96421, 1.00000, 0.82519), 'std_D55' : (0.95680, 1.00000, 0.92148), 'std_D65' : (0.95043, 1.00000, 1.08890), 'std_D75' : (0.94972, 1.00000, 1.22639), 'std_E' : (1.00000, 1.00000, 1.00000), 'std_F1' : (0.92834, 1.00000, 1.03665), 'std_F2' : (0.99145, 1.00000, 0.67316), 'std_F3' : (1.03753, 1.00000, 0.49861), 'std_F4' : (1.09147, 1.00000, 0.38813), 'std_F5' : (0.90872, 1.00000, 0.98723), 'std_F6' : (0.97309, 1.00000, 0.60191), 'std_F7' : (0.95017, 1.00000, 1.08630), 'std_F8' : (0.96413, 1.00000, 0.82333), 'std_F9' : (1.00365, 1.00000, 0.67868), 'std_F10' : (0.96174, 1.00000, 0.81712), 'std_F11' : (1.00899, 1.00000, 0.64262), 'std_F12' : (1.08046, 1.00000, 0.39228), 'sup_A' : (1.11142, 1.00000, 0.35200), 'sup_B' : (0.99178, 1.00000, 0.84349), 'sup_C' : (0.97286, 1.00000, 1.16145), 'sup_D50' : (0.96721, 1.00000, 0.81428), 'sup_D55' : (0.95797, 1.00000, 0.90925), 'sup_D65' : (0.94810, 1.00000, 1.07305), 'sup_D75' : (0.94417, 1.00000, 1.20643), 'sup_E' : (1.00000, 1.00000, 1.00000), 'sup_F1' : (0.94791, 1.00000, 1.03191), 'sup_F2' : (1.03245, 1.00000, 0.68990), 'sup_F3' : (1.08968, 1.00000, 0.51965), 'sup_F4' : (1.14961, 1.00000, 0.40963), 'sup_F5' : (0.93369, 1.00000, 0.98636), 'sup_F6' : (1.02148, 1.00000, 0.62074), 'sup_F7' : (0.95780, 1.00000, 1.07618), 'sup_F8' : (0.97115, 1.00000, 0.81135), 'sup_F9' : (1.02116, 1.00000, 0.67826), 'sup_F10' : (0.99001, 1.00000, 0.83134), 'sup_F11' : (1.03820, 1.00000, 0.65555), 'sup_F12' : (1.11428, 1.00000, 0.40353)} NAMED_COLOR = { 'aliceblue': '#f0f8ff', 'antiquewhite': '#faebd7', 'aqua': '#00ffff', 'aquamarine': '#7fffd4', 'azure': '#f0ffff', 'beige': '#f5f5dc', 'bisque': '#ffe4c4', 'black': '#000000', 'blanchedalmond': '#ffebcd', 'blue': '#0000ff', 'blueviolet': '#8a2be2', 'brown': '#a52a2a', 'burlywood': '#deb887', 'cadetblue': '#5f9ea0', 'chartreuse': '#7fff00', 'chocolate': '#d2691e', 'coral': '#ff7f50', 'cornflowerblue': '#6495ed', 'cornsilk': '#fff8dc', 'crimson': '#dc143c', 'cyan': '#00ffff', 'darkblue': '#00008b', 'darkcyan': '#008b8b', 'darkgoldenrod': '#b8860b', 'darkgray': '#a9a9a9', 'darkgrey': '#a9a9a9', 'darkgreen': '#006400', 'darkkhaki': '#bdb76b', 'darkmagenta': '#8b008b', 'darkolivegreen': '#556b2f', 'darkorange': '#ff8c00', 'darkorchid': '#9932cc', 'darkred': '#8b0000', 'darksalmon': '#e9967a', 'darkseagreen': '#8fbc8f', 'darkslateblue': '#483d8b', 'darkslategray': '#2f4f4f', 'darkslategrey': '#2f4f4f', 'darkturquoise': '#00ced1', 'darkviolet': '#9400d3', 'deeppink': '#ff1493', 'deepskyblue': '#00bfff', 'dimgray': '#696969', 'dimgrey': '#696969', 'dodgerblue': '#1e90ff', 'firebrick': '#b22222', 'floralwhite': '#fffaf0', 'forestgreen': '#228b22', 'fuchsia': '#ff00ff', 'gainsboro': '#dcdcdc', 'ghostwhite': '#f8f8ff', 'gold': '#ffd700', 'goldenrod': '#daa520', 'gray': '#808080', 'grey': '#808080', 'green': '#008000', 'greenyellow': '#adff2f', 'honeydew': '#f0fff0', 'hotpink': '#ff69b4', 'indianred': '#cd5c5c', 'indigo': '#4b0082', 'ivory': '#fffff0', 'khaki': '#f0e68c', 'lavender': '#e6e6fa', 'lavenderblush': '#fff0f5', 'lawngreen': '#7cfc00', 'lemonchiffon': '#fffacd', 'lightblue': '#add8e6', 'lightcoral': '#f08080', 'lightcyan': '#e0ffff', 'lightgoldenrodyellow': '#fafad2', 'lightgreen': '#90ee90', 'lightgray': '#d3d3d3', 'lightgrey': '#d3d3d3', 'lightpink': '#ffb6c1', 'lightsalmon': '#ffa07a', 'lightseagreen': '#20b2aa', 'lightskyblue': '#87cefa', 'lightslategray': '#778899', 'lightslategrey': '#778899', 'lightsteelblue': '#b0c4de', 'lightyellow': '#ffffe0', 'lime': '#00ff00', 'limegreen': '#32cd32', 'linen': '#faf0e6', 'magenta': '#ff00ff', 'maroon': '#800000', 'mediumaquamarine': '#66cdaa', 'mediumblue': '#0000cd', 'mediumorchid': '#ba55d3', 'mediumpurple': '#9370db', 'mediumseagreen': '#3cb371', 'mediumslateblue': '#7b68ee', 'mediumspringgreen': '#00fa9a', 'mediumturquoise': '#48d1cc', 'mediumvioletred': '#c71585', 'midnightblue': '#191970', 'mintcream': '#f5fffa', 'mistyrose': '#ffe4e1', 'moccasin': '#ffe4b5', 'navajowhite': '#ffdead', 'navy': '#000080', 'oldlace': '#fdf5e6', 'olive': '#808000', 'olivedrab': '#6b8e23', 'orange': '#ffa500', 'orangered': '#ff4500', 'orchid': '#da70d6', 'palegoldenrod': '#eee8aa', 'palegreen': '#98fb98', 'paleturquoise': '#afeeee', 'palevioletred': '#db7093', 'papayawhip': '#ffefd5', 'peachpuff': '#ffdab9', 'peru': '#cd853f', 'pink': '#ffc0cb', 'plum': '#dda0dd', 'powderblue': '#b0e0e6', 'purple': '#800080', 'red': '#ff0000', 'rosybrown': '#bc8f8f', 'royalblue': '#4169e1', 'saddlebrown': '#8b4513', 'salmon': '#fa8072', 'sandybrown': '#f4a460', 'seagreen': '#2e8b57', 'seashell': '#fff5ee', 'sienna': '#a0522d', 'silver': '#c0c0c0', 'skyblue': '#87ceeb', 'slateblue': '#6a5acd', 'slategray': '#708090', 'slategrey': '#708090', 'snow': '#fffafa', 'springgreen': '#00ff7f', 'steelblue': '#4682b4', 'tan': '#d2b48c', 'teal': '#008080', 'thistle': '#d8bfd8', 'tomato': '#ff6347', 'turquoise': '#40e0d0', 'violet': '#ee82ee', 'wheat': '#f5deb3', 'white': '#ffffff', 'whitesmoke': '#f5f5f5', 'yellow': '#ffff00', 'yellowgreen': '#9acd32'} def __init__(self, values, mode='rgb', alpha=1.0, wref=_DEFAULT_WREF): '''Instantiate a new grapefruit.Color object. Parameters: :values: The values of this color, in the specified representation. :mode: The representation mode used for values. :alpha: the alpha value (transparency) of this color. :wref: The whitepoint reference, default is 2° D65. ''' if not(isinstance(values, tuple)): raise TypeError('values must be a tuple') if mode=='rgb': self.__rgb = values self.__hsl = Color.RgbToHsl(*values) elif mode=='hsl': self.__hsl = values self.__rgb = Color.HslToRgb(*values) else: raise ValueError('Invalid color mode: ' + mode) self.__a = alpha self.__wref = wref def __ne__(self, other): return not self.__eq__(other) def __eq__(self, other): try: if isinstance(other, Color): return (self.__rgb==other.__rgb) and (self.__a==other.__a) if len(other) != 4: return False return list(self.__rgb + (self.__a,)) == list(other) except TypeError: return False except AttributeError: return False def __repr__(self): return str(self.__rgb + (self.__a,)) def __str__(self): '''A string representation of this grapefruit.Color instance. Returns: The RGBA representation of this grapefruit.Color instance. ''' return '(%g, %g, %g, %g)' % (self.__rgb + (self.__a,)) if sys.version_info[0] < 3: def __unicode__(self): '''A unicode string representation of this grapefruit.Color instance. Returns: The RGBA representation of this grapefruit.Color instance. ''' return unicode('%g, %g, %g, %g)') % (self.__rgb + (self.__a,)) def __iter__(self): return iter(self.__rgb + (self.__a,)) def __len__(self): return 4 def __GetIsLegal(self): return all(0.0 <= v <= 1.0 for v in self) isLegal = property(fget=__GetIsLegal, doc='Boolean indicating whether the color is within the legal gamut.') def __GetNearestLegal(self): def clamp(x, lo, hi): if x < lo: return lo elif x > hi: return hi else: return x return Color.NewFromRgb(*[clamp(v, 0.0, 1.0) for v in self]) nearestLegal = property(fget=__GetNearestLegal, doc='The nearest legal color.') @staticmethod def RgbToHsl(r, g, b): '''Convert the color from RGB coordinates to HSL. Parameters: :r: The Red component value [0...1] :g: The Green component value [0...1] :b: The Blue component value [0...1] Returns: The color as an (h, s, l) tuple in the range: h[0...360], s[0...1], l[0...1] >>> Color.RgbToHsl(1, 0.5, 0) (30.0, 1.0, 0.5) ''' minVal = min(r, g, b) # min RGB value maxVal = max(r, g, b) # max RGB value l = (maxVal + minVal) / 2.0 if minVal==maxVal: return (0.0, 0.0, l) # achromatic (gray) d = maxVal - minVal # delta RGB value if l < 0.5: s = d / (maxVal + minVal) else: s = d / (2.0 - maxVal - minVal) dr, dg, db = [(maxVal-val) / d for val in (r, g, b)] if r==maxVal: h = db - dg elif g==maxVal: h = 2.0 + dr - db else: h = 4.0 + dg - dr h = (h*60.0) % 360.0 return (h, s, l) @staticmethod def _HueToRgb(n1, n2, h): h %= 6.0 if h < 1.0: return n1 + ((n2-n1) * h) if h < 3.0: return n2 if h < 4.0: return n1 + ((n2-n1) * (4.0 - h)) return n1 @staticmethod def HslToRgb(h, s, l): '''Convert the color from HSL coordinates to RGB. Parameters: :h: The Hue component value [0...1] :s: The Saturation component value [0...1] :l: The Lightness component value [0...1] Returns: The color as an (r, g, b) tuple in the range: r[0...1], g[0...1], b[0...1] >>> Color.HslToRgb(30.0, 1.0, 0.5) (1.0, 0.5, 0.0) ''' if s==0: return (l, l, l) # achromatic (gray) if l<0.5: n2 = l * (1.0 + s) else: n2 = l+s - (l*s) n1 = (2.0 * l) - n2 h /= 60.0 hueToRgb = Color._HueToRgb r = hueToRgb(n1, n2, h + 2) g = hueToRgb(n1, n2, h) b = hueToRgb(n1, n2, h - 2) return (r, g, b) @staticmethod def RgbToHsv(r, g, b): '''Convert the color from RGB coordinates to HSV. Parameters: :r: The Red component value [0...1] :g: The Green component value [0...1] :b: The Blue component value [0...1] Returns: The color as an (h, s, v) tuple in the range: h[0...360], s[0...1], v[0...1] >>> Color.RgbToHsv(1, 0.5, 0) (30.0, 1.0, 1.0) ''' v = float(max(r, g, b)) d = v - min(r, g, b) if d==0: return (0.0, 0.0, v) s = d / v dr, dg, db = [(v - val) / d for val in (r, g, b)] if r==v: h = db - dg # between yellow & magenta elif g==v: h = 2.0 + dr - db # between cyan & yellow else: # b==v h = 4.0 + dg - dr # between magenta & cyan h = (h*60.0) % 360.0 return (h, s, v) @staticmethod def HsvToRgb(h, s, v): '''Convert the color from RGB coordinates to HSV. Parameters: :h: The Hus component value [0...1] :s: The Saturation component value [0...1] :v: The Value component [0...1] Returns: The color as an (r, g, b) tuple in the range: r[0...1], g[0...1], b[0...1] >>> Color.HslToRgb(30.0, 1.0, 0.5) (1.0, 0.5, 0.0) ''' if s==0: return (v, v, v) # achromatic (gray) h /= 60.0 h = h % 6.0 i = int(h) f = h - i if not(i&1): f = 1-f # if i is even m = v * (1.0 - s) n = v * (1.0 - (s * f)) if i==0: return (v, n, m) if i==1: return (n, v, m) if i==2: return (m, v, n) if i==3: return (m, n, v) if i==4: return (n, m, v) return (v, m, n) @staticmethod def RgbToYiq(r, g, b): '''Convert the color from RGB to YIQ. Parameters: :r: The Red component value [0...1] :g: The Green component value [0...1] :b: The Blue component value [0...1] Returns: The color as an (y, i, q) tuple in the range: y[0...1], i[0...1], q[0...1] >>> '(%g, %g, %g)' % Color.RgbToYiq(1, 0.5, 0) '(0.592263, 0.458874, -0.0499818)' ''' y = (r * 0.29895808) + (g * 0.58660979) + (b *0.11443213) i = (r * 0.59590296) - (g * 0.27405705) - (b *0.32184591) q = (r * 0.21133576) - (g * 0.52263517) + (b *0.31129940) return (y, i, q) @staticmethod def YiqToRgb(y, i, q): '''Convert the color from YIQ coordinates to RGB. Parameters: :y: Tte Y component value [0...1] :i: The I component value [0...1] :q: The Q component value [0...1] Returns: The color as an (r, g, b) tuple in the range: r[0...1], g[0...1], b[0...1] >>> '(%g, %g, %g)' % Color.YiqToRgb(0.592263, 0.458874, -0.0499818) '(1, 0.5, 5.442e-07)' ''' r = y + (i * 0.9562) + (q * 0.6210) g = y - (i * 0.2717) - (q * 0.6485) b = y - (i * 1.1053) + (q * 1.7020) return (r, g, b) @staticmethod def RgbToYuv(r, g, b): '''Convert the color from RGB coordinates to YUV. Parameters: :r: The Red component value [0...1] :g: The Green component value [0...1] :b: The Blue component value [0...1] Returns: The color as an (y, u, v) tuple in the range: y[0...1], u[-0.436...0.436], v[-0.615...0.615] >>> '(%g, %g, %g)' % Color.RgbToYuv(1, 0.5, 0) '(0.5925, -0.29156, 0.357505)' ''' y = (r * 0.29900) + (g * 0.58700) + (b * 0.11400) u = -(r * 0.14713) - (g * 0.28886) + (b * 0.43600) v = (r * 0.61500) - (g * 0.51499) - (b * 0.10001) return (y, u, v) @staticmethod def YuvToRgb(y, u, v): '''Convert the color from YUV coordinates to RGB. Parameters: :y: The Y component value [0...1] :u: The U component value [-0.436...0.436] :v: The V component value [-0.615...0.615] Returns: The color as an (r, g, b) tuple in the range: r[0...1], g[0...1], b[0...1] >>> '(%g, %g, %g)' % Color.YuvToRgb(0.5925, -0.2916, 0.3575) '(0.999989, 0.500015, -6.3276e-05)' ''' r = y + (v * 1.13983) g = y - (u * 0.39465) - (v * 0.58060) b = y + (u * 2.03211) return (r, g, b) @staticmethod def RgbToXyz(r, g, b): '''Convert the color from sRGB to CIE XYZ. The methods assumes that the RGB coordinates are given in the sRGB colorspace (D65). .. note:: Compensation for the sRGB gamma correction is applied before converting. Parameters: :r: The Red component value [0...1] :g: The Green component value [0...1] :b: The Blue component value [0...1] Returns: The color as an (x, y, z) tuple in the range: x[0...1], y[0...1], z[0...1] >>> '(%g, %g, %g)' % Color.RgbToXyz(1, 0.5, 0) '(0.488941, 0.365682, 0.0448137)' ''' r, g, b = [((v <= 0.03928) and [v / 12.92] or [((v+0.055) / 1.055) **2.4])[0] for v in (r, g, b)] x = (r * 0.4124) + (g * 0.3576) + (b * 0.1805) y = (r * 0.2126) + (g * 0.7152) + (b * 0.0722) z = (r * 0.0193) + (g * 0.1192) + (b * 0.9505) return (x, y, z) @staticmethod def XyzToRgb(x, y, z): '''Convert the color from CIE XYZ coordinates to sRGB. .. note:: Compensation for sRGB gamma correction is applied before converting. Parameters: :x: The X component value [0...1] :y: The Y component value [0...1] :z: The Z component value [0...1] Returns: The color as an (r, g, b) tuple in the range: r[0...1], g[0...1], b[0...1] >>> '(%g, %g, %g)' % Color.XyzToRgb(0.488941, 0.365682, 0.0448137) '(1, 0.5, 6.81883e-08)' ''' r = (x * 3.2406255) - (y * 1.5372080) - (z * 0.4986286) g = -(x * 0.9689307) + (y * 1.8757561) + (z * 0.0415175) b = (x * 0.0557101) - (y * 0.2040211) + (z * 1.0569959) return tuple((((v <= _srgbGammaCorrInv) and [v * 12.92] or [(1.055 * (v ** (1/2.4))) - 0.055])[0] for v in (r, g, b))) @staticmethod def XyzToLab(x, y, z, wref=_DEFAULT_WREF): '''Convert the color from CIE XYZ to CIE L*a*b*. Parameters: :x: The X component value [0...1] :y: The Y component value [0...1] :z: The Z component value [0...1] :wref: The whitepoint reference, default is 2° D65. Returns: The color as an (L, a, b) tuple in the range: L[0...100], a[-1...1], b[-1...1] >>> '(%g, %g, %g)' % Color.XyzToLab(0.488941, 0.365682, 0.0448137) '(66.9518, 0.43084, 0.739692)' >>> '(%g, %g, %g)' % Color.XyzToLab(0.488941, 0.365682, 0.0448137, Color.WHITE_REFERENCE['std_D50']) '(66.9518, 0.411663, 0.67282)' ''' # White point correction x /= wref[0] y /= wref[1] z /= wref[2] # Nonlinear distortion and linear transformation x, y, z = [((v > 0.008856) and [v**_oneThird] or [(7.787 * v) + _sixteenHundredsixteenth])[0] for v in (x, y, z)] # Vector scaling l = (116 * y) - 16 a = 5.0 * (x - y) b = 2.0 * (y - z) return (l, a, b) @staticmethod def LabToXyz(l, a, b, wref=_DEFAULT_WREF): '''Convert the color from CIE L*a*b* to CIE 1931 XYZ. Parameters: :l: The L component [0...100] :a: The a component [-1...1] :b: The a component [-1...1] :wref: The whitepoint reference, default is 2° D65. Returns: The color as an (x, y, z) tuple in the range: x[0...q], y[0...1], z[0...1] >>> '(%g, %g, %g)' % Color.LabToXyz(66.9518, 0.43084, 0.739692) '(0.488941, 0.365682, 0.0448137)' >>> '(%g, %g, %g)' % Color.LabToXyz(66.9518, 0.411663, 0.67282, Color.WHITE_REFERENCE['std_D50']) '(0.488941, 0.365682, 0.0448138)' ''' y = (l + 16) / 116 x = (a / 5.0) + y z = y - (b / 2.0) return tuple((((v > 0.206893) and [v**3] or [(v - _sixteenHundredsixteenth) / 7.787])[0] * w for v, w in zip((x, y, z), wref))) @staticmethod def CmykToCmy(c, m, y, k): '''Convert the color from CMYK coordinates to CMY. Parameters: :c: The Cyan component value [0...1] :m: The Magenta component value [0...1] :y: The Yellow component value [0...1] :k: The Black component value [0...1] Returns: The color as an (c, m, y) tuple in the range: c[0...1], m[0...1], y[0...1] >>> '(%g, %g, %g)' % Color.CmykToCmy(1, 0.32, 0, 0.5) '(1, 0.66, 0.5)' ''' mk = 1-k return ((c*mk + k), (m*mk + k), (y*mk + k)) @staticmethod def CmyToCmyk(c, m, y): '''Convert the color from CMY coordinates to CMYK. Parameters: :c: The Cyan component value [0...1] :m: The Magenta component value [0...1] :y: The Yellow component value [0...1] Returns: The color as an (c, m, y, k) tuple in the range: c[0...1], m[0...1], y[0...1], k[0...1] >>> '(%g, %g, %g, %g)' % Color.CmyToCmyk(1, 0.66, 0.5) '(1, 0.32, 0, 0.5)' ''' k = min(c, m, y) if k==1.0: return (0.0, 0.0, 0.0, 1.0) mk = 1-k return ((c-k) / mk, (m-k) / mk, (y-k) / mk, k) @staticmethod def RgbToCmy(r, g, b): '''Convert the color from RGB coordinates to CMY. Parameters: :r: The Red component value [0...1] :g: The Green component value [0...1] :b: The Blue component value [0...1] Returns: The color as an (c, m, y) tuple in the range: c[0...1], m[0...1], y[0...1] >>> Color.RgbToCmy(1, 0.5, 0) (0, 0.5, 1) ''' return (1-r, 1-g, 1-b) @staticmethod def CmyToRgb(c, m, y): '''Convert the color from CMY coordinates to RGB. Parameters: :c: The Cyan component value [0...1] :m: The Magenta component value [0...1] :y: The Yellow component value [0...1] Returns: The color as an (r, g, b) tuple in the range: r[0...1], g[0...1], b[0...1] >>> Color.CmyToRgb(0, 0.5, 1) (1, 0.5, 0) ''' return (1-c, 1-m, 1-y) @staticmethod def RgbToIntTuple(r, g, b): '''Convert the color from (r, g, b) to an int tuple. Parameters: :r: The Red component value [0...1] :g: The Green component value [0...1] :b: The Blue component value [0...1] Returns: The color as an (r, g, b) tuple in the range: r[0...255], g[0...2551], b[0...2551] >>> Color.RgbToIntTuple(1, 0.5, 0) (255, 128, 0) ''' return tuple(int(round(v*255)) for v in (r, g, b)) @staticmethod def IntTupleToRgb(intTuple): '''Convert a tuple of ints to (r, g, b). Parameters: The color as an (r, g, b) integer tuple in the range: r[0...255], g[0...255], b[0...255] Returns: The color as an (r, g, b) tuple in the range: r[0...1], g[0...1], b[0...1] >>> '(%g, %g, %g)' % Color.IntTupleToRgb((255, 128, 0)) '(1, 0.501961, 0)' ''' return tuple(v / 255 for v in intTuple) @staticmethod def RgbToHtml(r, g, b): '''Convert the color from (r, g, b) to #RRGGBB. Parameters: :r: The Red component value [0...1] :g: The Green component value [0...1] :b: The Blue component value [0...1] Returns: A CSS string representation of this color (#RRGGBB). >>> Color.RgbToHtml(1, 0.5, 0) '#ff8000' ''' return '#%02x%02x%02x' % tuple((min(round(v*255), 255) for v in (r, g, b))) @staticmethod def HtmlToRgb(html): '''Convert the HTML color to (r, g, b). Parameters: :html: the HTML definition of the color (#RRGGBB or #RGB or a color name). Returns: The color as an (r, g, b) tuple in the range: r[0...1], g[0...1], b[0...1] Throws: :ValueError: If html is neither a known color name or a hexadecimal RGB representation. >>> '(%g, %g, %g)' % Color.HtmlToRgb('#ff8000') '(1, 0.501961, 0)' >>> '(%g, %g, %g)' % Color.HtmlToRgb('ff8000') '(1, 0.501961, 0)' >>> '(%g, %g, %g)' % Color.HtmlToRgb('#f60') '(1, 0.4, 0)' >>> '(%g, %g, %g)' % Color.HtmlToRgb('f60') '(1, 0.4, 0)' >>> '(%g, %g, %g)' % Color.HtmlToRgb('lemonchiffon') '(1, 0.980392, 0.803922)' ''' html = html.strip().lower() if html[0]=='#': html = html[1:] elif html in Color.NAMED_COLOR: html = Color.NAMED_COLOR[html][1:] if len(html)==6: rgb = html[:2], html[2:4], html[4:] elif len(html)==3: rgb = ['%c%c' % (v,v) for v in html] else: raise ValueError('input #%s is not in #RRGGBB format' % html) return tuple(((int(n, 16) / 255.0) for n in rgb)) @staticmethod def RgbToPil(r, g, b): '''Convert the color from RGB to a PIL-compatible integer. Parameters: :r: The Red component value [0...1] :g: The Green component value [0...1] :b: The Blue component value [0...1] Returns: A PIL compatible integer (0xBBGGRR). >>> '0x%06x' % Color.RgbToPil(1, 0.5, 0) '0x0080ff' ''' r, g, b = [min(int(round(v*255)), 255) for v in (r, g, b)] return (b << 16) + (g << 8) + r @staticmethod def PilToRgb(pil): '''Convert the color from a PIL-compatible integer to RGB. Parameters: pil: a PIL compatible color representation (0xBBGGRR) Returns: The color as an (r, g, b) tuple in the range: the range: r: [0...1] g: [0...1] b: [0...1] >>> '(%g, %g, %g)' % Color.PilToRgb(0x0080ff) '(1, 0.501961, 0)' ''' r = 0xff & pil g = 0xff & (pil >> 8) b = 0xff & (pil >> 16) return tuple((v / 255.0 for v in (r, g, b))) @staticmethod def _WebSafeComponent(c, alt=False): '''Convert a color component to its web safe equivalent. Parameters: :c: The component value [0...1] :alt: If True, return the alternative value instead of the nearest one. Returns: The web safe equivalent of the component value. ''' # This sucks, but floating point between 0 and 1 is quite fuzzy... # So we just change the scale a while to make the equality tests # work, otherwise it gets wrong at some decimal far to the right. sc = c * 100.0 # If the color is already safe, return it straight away d = sc % 20 if d==0: return c # Get the lower and upper safe values l = sc - d u = l + 20 # Return the 'closest' value according to the alt flag if alt: if (sc-l) >= (u-sc): return l/100.0 else: return u/100.0 else: if (sc-l) >= (u-sc): return u/100.0 else: return l/100.0 @staticmethod def RgbToWebSafe(r, g, b, alt=False): '''Convert the color from RGB to 'web safe' RGB Parameters: :r: The Red component value [0...1] :g: The Green component value [0...1] :b: The Blue component value [0...1] :alt: If True, use the alternative color instead of the nearest one. Can be used for dithering. Returns: The color as an (r, g, b) tuple in the range: the range: r[0...1], g[0...1], b[0...1] >>> '(%g, %g, %g)' % Color.RgbToWebSafe(1, 0.55, 0.0) '(1, 0.6, 0)' ''' webSafeComponent = Color._WebSafeComponent return tuple((webSafeComponent(v, alt) for v in (r, g, b))) @staticmethod def RgbToGreyscale(r, g, b): '''Convert the color from RGB to its greyscale equivalent Parameters: :r: The Red component value [0...1] :g: The Green component value [0...1] :b: The Blue component value [0...1] Returns: The color as an (r, g, b) tuple in the range: the range: r[0...1], g[0...1], b[0...1] >>> '(%g, %g, %g)' % Color.RgbToGreyscale(1, 0.8, 0) '(0.6, 0.6, 0.6)' ''' v = (r + g + b) / 3.0 return (v, v, v) @staticmethod def RgbToRyb(hue): '''Maps a hue on the RGB color wheel to Itten's RYB wheel. Parameters: :hue: The hue on the RGB color wheel [0...360] Returns: An approximation of the corresponding hue on Itten's RYB wheel. >>> Color.RgbToRyb(15) 26.0 ''' d = hue % 15 i = int(hue / 15) x0 = _RybWheel[i] x1 = _RybWheel[i+1] return x0 + (x1-x0) * d / 15 @staticmethod def RybToRgb(hue): '''Maps a hue on Itten's RYB color wheel to the standard RGB wheel. Parameters: :hue: The hue on Itten's RYB color wheel [0...360] Returns: An approximation of the corresponding hue on the standard RGB wheel. >>> Color.RybToRgb(15) 8.0 ''' d = hue % 15 i = int(hue / 15) x0 = _RgbWheel[i] x1 = _RgbWheel[i+1] return x0 + (x1-x0) * d / 15 @staticmethod def NewFromRgb(r, g, b, alpha=1.0, wref=_DEFAULT_WREF): '''Create a new instance based on the specifed RGB values. Parameters: :r: The Red component value [0...1] :g: The Green component value [0...1] :b: The Blue component value [0...1] :alpha: The color transparency [0...1], default is opaque :wref: The whitepoint reference, default is 2° D65. Returns: A grapefruit.Color instance. >>> Color.NewFromRgb(1.0, 0.5, 0.0) (1.0, 0.5, 0.0, 1.0) >>> Color.NewFromRgb(1.0, 0.5, 0.0, 0.5) (1.0, 0.5, 0.0, 0.5) ''' return Color((r, g, b), 'rgb', alpha, wref) @staticmethod def NewFromHsl(h, s, l, alpha=1.0, wref=_DEFAULT_WREF): '''Create a new instance based on the specifed HSL values. Parameters: :h: The Hue component value [0...1] :s: The Saturation component value [0...1] :l: The Lightness component value [0...1] :alpha: The color transparency [0...1], default is opaque :wref: The whitepoint reference, default is 2° D65. Returns: A grapefruit.Color instance. >>> Color.NewFromHsl(30, 1, 0.5) (1.0, 0.5, 0.0, 1.0) >>> Color.NewFromHsl(30, 1, 0.5, 0.5) (1.0, 0.5, 0.0, 0.5) ''' return Color((h, s, l), 'hsl', alpha, wref) @staticmethod def NewFromHsv(h, s, v, alpha=1.0, wref=_DEFAULT_WREF): '''Create a new instance based on the specifed HSV values. Parameters: :h: The Hus component value [0...1] :s: The Saturation component value [0...1] :v: The Value component [0...1] :alpha: The color transparency [0...1], default is opaque :wref: The whitepoint reference, default is 2° D65. Returns: A grapefruit.Color instance. >>> Color.NewFromHsv(30, 1, 1) (1.0, 0.5, 0.0, 1.0) >>> Color.NewFromHsv(30, 1, 1, 0.5) (1.0, 0.5, 0.0, 0.5) ''' h2, s, l = Color.RgbToHsl(*Color.HsvToRgb(h, s, v)) return Color((h, s, l), 'hsl', alpha, wref) @staticmethod def NewFromYiq(y, i, q, alpha=1.0, wref=_DEFAULT_WREF): '''Create a new instance based on the specifed YIQ values. Parameters: :y: The Y component value [0...1] :i: The I component value [0...1] :q: The Q component value [0...1] :alpha: The color transparency [0...1], default is opaque :wref: The whitepoint reference, default is 2° D65. Returns: A grapefruit.Color instance. >>> str(Color.NewFromYiq(0.5922, 0.45885,-0.05)) '(0.999902, 0.499955, -6.6905e-05, 1)' >>> str(Color.NewFromYiq(0.5922, 0.45885,-0.05, 0.5)) '(0.999902, 0.499955, -6.6905e-05, 0.5)' ''' return Color(Color.YiqToRgb(y, i, q), 'rgb', alpha, wref) @staticmethod def NewFromYuv(y, u, v, alpha=1.0, wref=_DEFAULT_WREF): '''Create a new instance based on the specifed YUV values. Parameters: :y: The Y component value [0...1] :u: The U component value [-0.436...0.436] :v: The V component value [-0.615...0.615] :alpha: The color transparency [0...1], default is opaque :wref: The whitepoint reference, default is 2° D65. Returns: A grapefruit.Color instance. >>> str(Color.NewFromYuv(0.5925, -0.2916, 0.3575)) '(0.999989, 0.500015, -6.3276e-05, 1)' >>> str(Color.NewFromYuv(0.5925, -0.2916, 0.3575, 0.5)) '(0.999989, 0.500015, -6.3276e-05, 0.5)' ''' return Color(Color.YuvToRgb(y, u, v), 'rgb', alpha, wref) @staticmethod def NewFromXyz(x, y, z, alpha=1.0, wref=_DEFAULT_WREF): '''Create a new instance based on the specifed CIE-XYZ values. Parameters: :x: The Red component value [0...1] :y: The Green component value [0...1] :z: The Blue component value [0...1] :alpha: The color transparency [0...1], default is opaque :wref: The whitepoint reference, default is 2° D65. Returns: A grapefruit.Color instance. >>> str(Color.NewFromXyz(0.488941, 0.365682, 0.0448137)) '(1, 0.5, 6.81883e-08, 1)' >>> str(Color.NewFromXyz(0.488941, 0.365682, 0.0448137, 0.5)) '(1, 0.5, 6.81883e-08, 0.5)' ''' return Color(Color.XyzToRgb(x, y, z), 'rgb', alpha, wref) @staticmethod def NewFromLab(l, a, b, alpha=1.0, wref=_DEFAULT_WREF): '''Create a new instance based on the specifed CIE-LAB values. Parameters: :l: The L component [0...100] :a: The a component [-1...1] :b: The a component [-1...1] :alpha: The color transparency [0...1], default is opaque :wref: The whitepoint reference, default is 2° D65. Returns: A grapefruit.Color instance. >>> str(Color.NewFromLab(66.9518, 0.43084, 0.739692)) '(1, 0.5, 1.09491e-08, 1)' >>> str(Color.NewFromLab(66.9518, 0.43084, 0.739692, wref=Color.WHITE_REFERENCE['std_D50'])) '(1.01238, 0.492011, -0.14311, 1)' >>> str(Color.NewFromLab(66.9518, 0.43084, 0.739692, 0.5)) '(1, 0.5, 1.09491e-08, 0.5)' >>> str(Color.NewFromLab(66.9518, 0.43084, 0.739692, 0.5, Color.WHITE_REFERENCE['std_D50'])) '(1.01238, 0.492011, -0.14311, 0.5)' ''' return Color(Color.XyzToRgb(*Color.LabToXyz(l, a, b, wref)), 'rgb', alpha, wref) @staticmethod def NewFromCmy(c, m, y, alpha=1.0, wref=_DEFAULT_WREF): '''Create a new instance based on the specifed CMY values. Parameters: :c: The Cyan component value [0...1] :m: The Magenta component value [0...1] :y: The Yellow component value [0...1] :alpha: The color transparency [0...1], default is opaque :wref: The whitepoint reference, default is 2° D65. Returns: A grapefruit.Color instance. >>> Color.NewFromCmy(0, 0.5, 1) (1, 0.5, 0, 1.0) >>> Color.NewFromCmy(0, 0.5, 1, 0.5) (1, 0.5, 0, 0.5) ''' return Color(Color.CmyToRgb(c, m, y), 'rgb', alpha, wref) @staticmethod def NewFromCmyk(c, m, y, k, alpha=1.0, wref=_DEFAULT_WREF): '''Create a new instance based on the specifed CMYK values. Parameters: :c: The Cyan component value [0...1] :m: The Magenta component value [0...1] :y: The Yellow component value [0...1] :k: The Black component value [0...1] :alpha: The color transparency [0...1], default is opaque :wref: The whitepoint reference, default is 2° D65. Returns: A grapefruit.Color instance. >>> str(Color.NewFromCmyk(1, 0.32, 0, 0.5)) '(0, 0.34, 0.5, 1)' >>> str(Color.NewFromCmyk(1, 0.32, 0, 0.5, 0.5)) '(0, 0.34, 0.5, 0.5)' ''' return Color(Color.CmyToRgb(*Color.CmykToCmy(c, m, y, k)), 'rgb', alpha, wref) @staticmethod def NewFromHtml(html, alpha=1.0, wref=_DEFAULT_WREF): '''Create a new instance based on the specifed HTML color definition. Parameters: :html: The HTML definition of the color (#RRGGBB or #RGB or a color name). :alpha: The color transparency [0...1], default is opaque. :wref: The whitepoint reference, default is 2° D65. Returns: A grapefruit.Color instance. >>> str(Color.NewFromHtml('#ff8000')) '(1, 0.501961, 0, 1)' >>> str(Color.NewFromHtml('ff8000')) '(1, 0.501961, 0, 1)' >>> str(Color.NewFromHtml('#f60')) '(1, 0.4, 0, 1)' >>> str(Color.NewFromHtml('f60')) '(1, 0.4, 0, 1)' >>> str(Color.NewFromHtml('lemonchiffon')) '(1, 0.980392, 0.803922, 1)' >>> str(Color.NewFromHtml('#ff8000', 0.5)) '(1, 0.501961, 0, 0.5)' ''' return Color(Color.HtmlToRgb(html), 'rgb', alpha, wref) @staticmethod def NewFromPil(pil, alpha=1.0, wref=_DEFAULT_WREF): '''Create a new instance based on the specifed PIL color. Parameters: :pil: A PIL compatible color representation (0xBBGGRR) :alpha: The color transparency [0...1], default is opaque :wref: The whitepoint reference, default is 2° D65. Returns: A grapefruit.Color instance. >>> str(Color.NewFromPil(0x0080ff)) '(1, 0.501961, 0, 1)' >>> str(Color.NewFromPil(0x0080ff, 0.5)) '(1, 0.501961, 0, 0.5)' ''' return Color(Color.PilToRgb(pil), 'rgb', alpha, wref) def __GetAlpha(self): return self.__a alpha = property(fget=__GetAlpha, doc='The transparency of this color. 0.0 is transparent and 1.0 is fully opaque.') def __GetWRef(self): return self.__wref whiteRef = property(fget=__GetWRef, doc='the white reference point of this color.') def __GetRGB(self): return self.__rgb rgb = property(fget=__GetRGB, doc='The RGB values of this Color.') def __GetHue(self): return self.__hsl[0] hue = property(fget=__GetHue, doc='The hue of this color.') def __GetHSL(self): return self.__hsl hsl = property(fget=__GetHSL, doc='The HSL values of this Color.') def __GetHSV(self): h, s, v = Color.RgbToHsv(*self.__rgb) return (self.__hsl[0], s, v) hsv = property(fget=__GetHSV, doc='The HSV values of this Color.') def __GetYIQ(self): return Color.RgbToYiq(*self.__rgb) yiq = property(fget=__GetYIQ, doc='The YIQ values of this Color.') def __GetYUV(self): return Color.RgbToYuv(*self.__rgb) yuv = property(fget=__GetYUV, doc='The YUV values of this Color.') def __GetXYZ(self): return Color.RgbToXyz(*self.__rgb) xyz = property(fget=__GetXYZ, doc='The CIE-XYZ values of this Color.') def __GetLAB(self): return Color.XyzToLab(wref=self.__wref, *Color.RgbToXyz(*self.__rgb)) lab = property(fget=__GetLAB, doc='The CIE-LAB values of this Color.') def __GetCMY(self): return Color.RgbToCmy(*self.__rgb) cmy = property(fget=__GetCMY, doc='The CMY values of this Color.') def __GetCMYK(self): return Color.CmyToCmyk(*Color.RgbToCmy(*self.__rgb)) cmyk = property(fget=__GetCMYK, doc='The CMYK values of this Color.') def __GetIntTuple(self): return Color.RgbToIntTuple(*self.__rgb) intTuple = property(fget=__GetIntTuple, doc='This Color as a tuple of integers in the range [0...255]') def __GetHTML(self): return Color.RgbToHtml(*self.__rgb) html = property(fget=__GetHTML, doc='This Color as an HTML color definition.') def __GetPIL(self): return Color.RgbToPil(*self.__rgb) pil = property(fget=__GetPIL, doc='This Color as a PIL compatible value.') def __GetwebSafe(self): return Color.RgbToWebSafe(*self.__rgb) webSafe = property(fget=__GetwebSafe, doc='The web safe color nearest to this one (RGB).') def __GetGreyscale(self): return Color.RgbToGreyscale(*self.rgb) greyscale = property(fget=__GetGreyscale, doc='The greyscale equivalent to this color (RGB).') def ColorWithAlpha(self, alpha): '''Create a new instance based on this one with a new alpha value. Parameters: :alpha: The transparency of the new color [0...1]. Returns: A grapefruit.Color instance. >>> Color.NewFromRgb(1.0, 0.5, 0.0, 1.0).ColorWithAlpha(0.5) (1.0, 0.5, 0.0, 0.5) ''' return Color(self.__rgb, 'rgb', alpha, self.__wref) def ColorWithWhiteRef(self, wref, labAsRef=False): '''Create a new instance based on this one with a new white reference. Parameters: :wref: The whitepoint reference. :labAsRef: If True, the L*a*b* values of the current instance are used as reference for the new color; otherwise, the RGB values are used as reference. Returns: A grapefruit.Color instance. >>> c = Color.NewFromRgb(1.0, 0.5, 0.0, 1.0, Color.WHITE_REFERENCE['std_D65']) >>> c2 = c.ColorWithWhiteRef(Color.WHITE_REFERENCE['sup_D50']) >>> c2.rgb (1.0, 0.5, 0.0) >>> '(%g, %g, %g)' % c2.whiteRef '(0.96721, 1, 0.81428)' >>> c2 = c.ColorWithWhiteRef(Color.WHITE_REFERENCE['sup_D50'], labAsRef=True) >>> '(%g, %g, %g)' % c2.rgb '(1.01463, 0.490339, -0.148131)' >>> '(%g, %g, %g)' % c2.whiteRef '(0.96721, 1, 0.81428)' >>> '(%g, %g, %g)' % c.lab '(66.9518, 0.43084, 0.739692)' >>> '(%g, %g, %g)' % c2.lab '(66.9518, 0.43084, 0.739693)' ''' if labAsRef: l, a, b = self.__GetLAB() return Color.NewFromLab(l, a, b, self.__a, wref) else: return Color(self.__rgb, 'rgb', self.__a, wref) def ColorWithHue(self, hue): '''Create a new instance based on this one with a new hue. Parameters: :hue: The hue of the new color [0...360]. Returns: A grapefruit.Color instance. >>> Color.NewFromHsl(30, 1, 0.5).ColorWithHue(60) (1.0, 1.0, 0.0, 1.0) >>> Color.NewFromHsl(30, 1, 0.5).ColorWithHue(60).hsl (60, 1, 0.5) ''' h, s, l = self.__hsl return Color((hue, s, l), 'hsl', self.__a, self.__wref) def ColorWithSaturation(self, saturation): '''Create a new instance based on this one with a new saturation value. .. note:: The saturation is defined for the HSL mode. Parameters: :saturation: The saturation of the new color [0...1]. Returns: A grapefruit.Color instance. >>> Color.NewFromHsl(30, 1, 0.5).ColorWithSaturation(0.5) (0.75, 0.5, 0.25, 1.0) >>> Color.NewFromHsl(30, 1, 0.5).ColorWithSaturation(0.5).hsl (30, 0.5, 0.5) ''' h, s, l = self.__hsl return Color((h, saturation, l), 'hsl', self.__a, self.__wref) def ColorWithLightness(self, lightness): '''Create a new instance based on this one with a new lightness value. Parameters: :lightness: The lightness of the new color [0...1]. Returns: A grapefruit.Color instance. >>> Color.NewFromHsl(30, 1, 0.5).ColorWithLightness(0.25) (0.5, 0.25, 0.0, 1.0) >>> Color.NewFromHsl(30, 1, 0.5).ColorWithLightness(0.25).hsl (30, 1, 0.25) ''' h, s, l = self.__hsl return Color((h, s, lightness), 'hsl', self.__a, self.__wref) def DarkerColor(self, level): '''Create a new instance based on this one but darker. Parameters: :level: The amount by which the color should be darkened to produce the new one [0...1]. Returns: A grapefruit.Color instance. >>> Color.NewFromHsl(30, 1, 0.5).DarkerColor(0.25) (0.5, 0.25, 0.0, 1.0) >>> Color.NewFromHsl(30, 1, 0.5).DarkerColor(0.25).hsl (30, 1, 0.25) ''' h, s, l = self.__hsl return Color((h, s, max(l - level, 0)), 'hsl', self.__a, self.__wref) def LighterColor(self, level): '''Create a new instance based on this one but lighter. Parameters: :level: The amount by which the color should be lightened to produce the new one [0...1]. Returns: A grapefruit.Color instance. >>> Color.NewFromHsl(30, 1, 0.5).LighterColor(0.25) (1.0, 0.75, 0.5, 1.0) >>> Color.NewFromHsl(30, 1, 0.5).LighterColor(0.25).hsl (30, 1, 0.75) ''' h, s, l = self.__hsl return Color((h, s, min(l + level, 1)), 'hsl', self.__a, self.__wref) def Saturate(self, level): '''Create a new instance based on this one but more saturated. Parameters: :level: The amount by which the color should be saturated to produce the new one [0...1]. Returns: A grapefruit.Color instance. >>> Color.NewFromHsl(30, 0.5, 0.5).Saturate(0.25) (0.875, 0.5, 0.125, 1.0) >>> Color.NewFromHsl(30, 0.5, 0.5).Saturate(0.25).hsl (30, 0.75, 0.5) ''' h, s, l = self.__hsl return Color((h, min(s + level, 1), l), 'hsl', self.__a, self.__wref) def Desaturate(self, level): '''Create a new instance based on this one but less saturated. Parameters: :level: The amount by which the color should be desaturated to produce the new one [0...1]. Returns: A grapefruit.Color instance. >>> Color.NewFromHsl(30, 0.5, 0.5).Desaturate(0.25) (0.625, 0.5, 0.375, 1.0) >>> Color.NewFromHsl(30, 0.5, 0.5).Desaturate(0.25).hsl (30, 0.25, 0.5) ''' h, s, l = self.__hsl return Color((h, max(s - level, 0), l), 'hsl', self.__a, self.__wref) def WebSafeDither(self): '''Return the two websafe colors nearest to this one. Returns: A tuple of two grapefruit.Color instances which are the two web safe colors closest this one. >>> c = Color.NewFromRgb(1.0, 0.45, 0.0) >>> c1, c2 = c.WebSafeDither() >>> str(c1) '(1, 0.4, 0, 1)' >>> str(c2) '(1, 0.6, 0, 1)' ''' return ( Color(Color.RgbToWebSafe(*self.__rgb), 'rgb', self.__a, self.__wref), Color(Color.RgbToWebSafe(alt=True, *self.__rgb), 'rgb', self.__a, self.__wref)) def Gradient(self, target, steps=100): '''Create a list with the gradient colors between this and the other color. Parameters: :target: The grapefruit.Color at the other end of the gradient. :steps: The number of gradients steps to create. Returns: A list of grapefruit.Color instances. >>> c1 = Color.NewFromRgb(1.0, 0.0, 0.0, alpha=1) >>> c2 = Color.NewFromRgb(0.0, 1.0, 0.0, alpha=0) >>> c1.Gradient(c2, 3) [(0.75, 0.25, 0.0, 0.75), (0.5, 0.5, 0.0, 0.5), (0.25, 0.75, 0.0, 0.25)] ''' gradient = [] rgba1 = self.__rgb + (self.__a,) rgba2 = target.__rgb + (target.__a,) steps += 1 for n in range(1, steps): d = 1.0*n/steps r = (rgba1[0]*(1-d)) + (rgba2[0]*d) g = (rgba1[1]*(1-d)) + (rgba2[1]*d) b = (rgba1[2]*(1-d)) + (rgba2[2]*d) a = (rgba1[3]*(1-d)) + (rgba2[3]*d) gradient.append(Color((r, g, b), 'rgb', a, self.__wref)) return gradient def ComplementaryColor(self, mode='ryb'): '''Create a new instance which is the complementary color of this one. Parameters: :mode: Select which color wheel to use for the generation (ryb/rgb). Returns: A grapefruit.Color instance. >>> Color.NewFromHsl(30, 1, 0.5).ComplementaryColor(mode='rgb') (0.0, 0.5, 1.0, 1.0) >>> Color.NewFromHsl(30, 1, 0.5).ComplementaryColor(mode='rgb').hsl (210, 1, 0.5) ''' h, s, l = self.__hsl if mode == 'ryb': h = Color.RgbToRyb(h) h = (h+180)%360 if mode == 'ryb': h = Color.RybToRgb(h) return Color((h, s, l), 'hsl', self.__a, self.__wref) def MonochromeScheme(self): '''Return 4 colors in the same hue with varying saturation/lightness. Returns: A tuple of 4 grapefruit.Color in the same hue as this one, with varying saturation/lightness. >>> c = Color.NewFromHsl(30, 0.5, 0.5) >>> ['(%g, %g, %g)' % clr.hsl for clr in c.MonochromeScheme()] ['(30, 0.2, 0.8)', '(30, 0.5, 0.3)', '(30, 0.2, 0.6)', '(30, 0.5, 0.8)'] ''' def _wrap(x, min, thres, plus): if (x-min) < thres: return x + plus else: return x-min h, s, l = self.__hsl s1 = _wrap(s, 0.3, 0.1, 0.3) l1 = _wrap(l, 0.5, 0.2, 0.3) s2 = s l2 = _wrap(l, 0.2, 0.2, 0.6) s3 = s1 l3 = max(0.2, l + (1-l)*0.2) s4 = s l4 = _wrap(l, 0.5, 0.2, 0.3) return ( Color((h, s1, l1), 'hsl', self.__a, self.__wref), Color((h, s2, l2), 'hsl', self.__a, self.__wref), Color((h, s3, l3), 'hsl', self.__a, self.__wref), Color((h, s4, l4), 'hsl', self.__a, self.__wref)) def TriadicScheme(self, angle=120, mode='ryb'): '''Return two colors forming a triad or a split complementary with this one. Parameters: :angle: The angle between the hues of the created colors. The default value makes a triad. :mode: Select which color wheel to use for the generation (ryb/rgb). Returns: A tuple of two grapefruit.Color forming a color triad with this one or a split complementary. >>> c1 = Color.NewFromHsl(30, 1, 0.5) >>> c2, c3 = c1.TriadicScheme(mode='rgb') >>> c2.hsl (150.0, 1, 0.5) >>> c3.hsl (270.0, 1, 0.5) >>> c2, c3 = c1.TriadicScheme(angle=40, mode='rgb') >>> c2.hsl (190.0, 1, 0.5) >>> c3.hsl (230.0, 1, 0.5) ''' h, s, l = self.__hsl angle = min(angle, 120) / 2.0 if mode == 'ryb': h = Color.RgbToRyb(h) h += 180 h1 = (h - angle) % 360 h2 = (h + angle) % 360 if mode == 'ryb': h1 = Color.RybToRgb(h1) h2 = Color.RybToRgb(h2) return ( Color((h1, s, l), 'hsl', self.__a, self.__wref), Color((h2, s, l), 'hsl', self.__a, self.__wref)) def TetradicScheme(self, angle=30, mode='ryb'): '''Return three colors froming a tetrad with this one. Parameters: :angle: The angle to substract from the adjacent colors hues [-90...90]. You can use an angle of zero to generate a square tetrad. :mode: Select which color wheel to use for the generation (ryb/rgb). Returns: A tuple of three grapefruit.Color forming a color tetrad with this one. >>> col = Color.NewFromHsl(30, 1, 0.5) >>> [c.hsl for c in col.TetradicScheme(mode='rgb', angle=30)] [(90, 1, 0.5), (210, 1, 0.5), (270, 1, 0.5)] ''' h, s, l = self.__hsl if mode == 'ryb': h = Color.RgbToRyb(h) h1 = (h + 90 - angle) % 360 h2 = (h + 180) % 360 h3 = (h + 270 - angle) % 360 if mode == 'ryb': h1 = Color.RybToRgb(h1) h2 = Color.RybToRgb(h2) h3 = Color.RybToRgb(h3) return ( Color((h1, s, l), 'hsl', self.__a, self.__wref), Color((h2, s, l), 'hsl', self.__a, self.__wref), Color((h3, s, l), 'hsl', self.__a, self.__wref)) def AnalogousScheme(self, angle=30, mode='ryb'): '''Return two colors analogous to this one. Args: :angle: The angle between the hues of the created colors and this one. :mode: Select which color wheel to use for the generation (ryb/rgb). Returns: A tuple of grapefruit.Colors analogous to this one. >>> c1 = Color.NewFromHsl(30, 1, 0.5) >>> c2, c3 = c1.AnalogousScheme(angle=60, mode='rgb') >>> c2.hsl (330, 1, 0.5) >>> c3.hsl (90, 1, 0.5) >>> c2, c3 = c1.AnalogousScheme(angle=10, mode='rgb') >>> c2.hsl (20, 1, 0.5) >>> c3.hsl (40, 1, 0.5) ''' h, s, l = self.__hsl if mode == 'ryb': h = Color.RgbToRyb(h) h += 360 h1 = (h - angle) % 360 h2 = (h + angle) % 360 if mode == 'ryb': h1 = Color.RybToRgb(h1) h2 = Color.RybToRgb(h2) return (Color((h1, s, l), 'hsl', self.__a, self.__wref), Color((h2, s, l), 'hsl', self.__a, self.__wref)) def AlphaBlend(self, other): '''Alpha-blend this color on the other one. Args: :other: The grapefruit.Color to alpha-blend with this one. Returns: A grapefruit.Color instance which is the result of alpha-blending this color on the other one. >>> c1 = Color.NewFromRgb(1, 0.5, 0, 0.2) >>> c2 = Color.NewFromRgb(1, 1, 1, 0.8) >>> c3 = c1.AlphaBlend(c2) >>> str(c3) '(1, 0.875, 0.75, 0.84)' ''' # get final alpha channel fa = self.__a + other.__a - (self.__a * other.__a) # get percentage of source alpha compared to final alpha if fa==0: sa = 0 else: sa = min(1.0, self.__a/other.__a) # destination percentage is just the additive inverse da = 1.0 - sa sr, sg, sb = [v * sa for v in self.__rgb] dr, dg, db = [v * da for v in other.__rgb] return Color((sr+dr, sg+dg, sb+db), 'rgb', fa, self.__wref) def Blend(self, other, percent=0.5): '''Blend this color with the other one. Args: :other: the grapefruit.Color to blend with this one. Returns: A grapefruit.Color instance which is the result of blending this color on the other one. >>> c1 = Color.NewFromRgb(1, 0.5, 0, 0.2) >>> c2 = Color.NewFromRgb(1, 1, 1, 0.6) >>> c3 = c1.Blend(c2) >>> str(c3) '(1, 0.75, 0.5, 0.4)' ''' dest = 1.0 - percent rgb = tuple(((u * percent) + (v * dest) for u, v in zip(self.__rgb, other.__rgb))) a = (self.__a * percent) + (other.__a * dest) return Color(rgb, 'rgb', a, self.__wref) def _test(): import doctest reload(doctest) doctest.testmod() if __name__=='__main__': _test() # vim: ts=2 sts=2 sw=2 et fabulous-0.3.0/fabulous/image.py000066400000000000000000000151441274117624500166240ustar00rootroot00000000000000# Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ fabulous.image ~~~~~~~~~~~~~~ The image module makes it possible to print images to the terminal. This module is available as a command line tool:: jart@compy:~$ fabulous-image foo.png jart@compy:~$ python -m fabulous.image foo.png """ import sys import itertools from fabulous import utils, xterm256, grapefruit from fabulous.compatibility import printy class Image(object): """Printing image files to a terminal I use :mod:`PIL` to turn your image file into a bitmap, resize it so it'll fit inside your terminal, and implement methods so I can behave like a string or iterable. When resizing, I'll assume that a single character on the terminal display is one pixel wide and two pixels tall. For most fonts this is the best way to preserve the aspect ratio of your image. All colors are are quantized by :mod:`fabulous.xterm256` to the 256 colors supported by modern terminals. When quantizing semi-transparant pixels (common in text or PNG files) I'll ask :class:`TerminalInfo` for the background color I should use to solidify the color. Fully transparent pixels will be rendered as a blank space without color so we don't need to mix in a background color. I also put a lot of work into optimizing the output line-by-line so it needs as few ANSI escape sequences as possible. If your terminal is kinda slow, you're gonna want to buy me a drink ;) You can use :class:`DebugImage` to visualize these optimizations. The generated output will only include spaces with different background colors. In the future routines will be provided to overlay text on top of these images. """ pad = ' ' def __init__(self, path, width=None): utils.pil_check() from PIL import Image as PillsPillsPills self.img = PillsPillsPills.open(path) # when reading pixels, gifs will return colors corresponding # to a palette if we don't do this :\ self.img = self.img.convert("RGBA") self.resize(width) def __iter__(self): """I allow Image to behave as an iterable By using me with a for loop, you can use each line as they're created. When printing a large image, this helps you not have to wait for the whole thing to be converted. :return: Yields lines of text (without line end character) """ # strip out blank lines for line in self.reduce(self.convert()): if line.strip(): yield line yield "" def __str__(self): """I return the entire image as one big string Unlike the iteration approach, you have to wait for the entire image to be converted. :return: String containing all lines joined together. """ return "\n".join(self) @property def size(self): """Returns size of image """ return self.img.size def resize(self, width=None): """Resizes image to fit inside terminal Called by the constructor automatically. """ (iw, ih) = self.size if width is None: width = min(iw, utils.term.width) elif isinstance(width, basestring): percents = dict([(pct, '%s%%' % (pct)) for pct in range(101)]) width = percents[width] height = int(float(ih) * (float(width) / float(iw))) height //= 2 self.img = self.img.resize((width, height)) def reduce(self, colors): """Converts color codes into optimized text This optimizer works by merging adjacent colors so we don't have to repeat the same escape codes for each pixel. There is no loss of information. :param colors: Iterable yielding an xterm color code for each pixel, None to indicate a transparent pixel, or ``'EOL'`` to indicate th end of a line. :return: Yields lines of optimized text. """ need_reset = False line = [] for color, items in itertools.groupby(colors): if color is None: if need_reset: line.append("\x1b[49m") need_reset = False line.append(self.pad * len(list(items))) elif color == "EOL": if need_reset: line.append("\x1b[49m") need_reset = False yield "".join(line) else: line.pop() yield "".join(line) line = [] else: need_reset = True line.append("\x1b[48;5;%dm%s" % ( color, self.pad * len(list(items)))) def convert(self): """Yields xterm color codes for each pixel in image """ (width, height) = self.img.size bgcolor = utils.term.bgcolor self.img.load() for y in range(height): for x in range(width): rgba = self.img.getpixel((x, y)) if len(rgba) == 4 and rgba[3] == 0: yield None elif len(rgba) == 3 or rgba[3] == 255: yield xterm256.rgb_to_xterm(*rgba[:3]) else: color = grapefruit.Color.NewFromRgb( *[c / 255.0 for c in rgba]) rgba = grapefruit.Color.AlphaBlend(color, bgcolor).rgb yield xterm256.rgb_to_xterm( *[int(c * 255.0) for c in rgba]) yield "EOL" def main(): """Main function for :command:`fabulous-image`.""" import optparse parser = optparse.OptionParser() parser.add_option( "-w", "--width", dest="width", type="int", default=None, help=("Width of printed image in characters. Default: %default")) (options, args) = parser.parse_args(args=sys.argv[1:]) for imgpath in args: for line in Image(imgpath, options.width): printy(line) if __name__ == '__main__': main() fabulous-0.3.0/fabulous/logs.py000066400000000000000000000103601274117624500165010ustar00rootroot00000000000000# Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ fabulous.logs ~~~~~~~~~~~~~ Utilities for transient logging. This is very useful tool for monitoring what your Python scripts are doing. It allows you to have full verbosity without drowning out important error messages: .. code-block:: python import time, logging from fabulous import logs logs.basicConfig(level='WARNING') for n in range(20): logging.debug("verbose stuff you don't care about") time.sleep(0.1) logging.warning("something bad happened!") for n in range(20): logging.debug("verbose stuff you don't care about") time.sleep(0.1) """ import sys import logging from fabulous import utils class TransientStreamHandler(logging.StreamHandler): """Standard Python logging Handler for Transient Console Logging Logging transiently means that verbose logging messages like DEBUG will only appear on the last line of your terminal for a short period of time and important messages like WARNING will scroll like normal text. This allows you to log lots of messages without the important stuff getting drowned out. This module integrates with the standard Python logging module. """ def __init__(self, strm=sys.stderr, level=logging.WARNING): logging.StreamHandler.__init__(self, strm) if isinstance(level, int): self.levelno = level else: self.levelno = logging._levelNames[level] self.need_cr = False self.last = "" self.parent = logging.StreamHandler def close(self): if self.need_cr: self.stream.write("\n") self.need_cr = False self.parent.close(self) def write(self, data): if self.need_cr: width = max(min(utils.term.width, len(self.last)), len(data)) fmt = "\r%-" + str(width) + "s\n" + self.last else: fmt = "%s\n" try: self.stream.write(fmt % (data)) except UnicodeError: self.stream.write(fmt % (data.encode("UTF-8"))) def transient_write(self, data): if self.need_cr: self.stream.write('\r') else: self.need_cr = True width = utils.term.width for line in data.rstrip().split('\n'): if line: if len(line) > width: line = line[:width - 3] + '...' line_width = max(min(width, len(self.last)), len(line)) fmt = "%-" + str(line_width) + "s" self.last = line try: self.stream.write(fmt % (line)) except UnicodeError: self.stream.write(fmt % (line.encode("UTF-8"))) else: self.stream.write('\r') self.stream.flush() def emit(self, record): try: msg = self.format(record) if record.levelno >= self.levelno: self.write(msg) else: self.transient_write(msg) except (KeyboardInterrupt, SystemExit): raise except: self.handleError(record) def basicConfig(level=logging.WARNING, transient_level=logging.NOTSET): """Shortcut for setting up transient logging I am a replica of ``logging.basicConfig`` which installs a transient logging handler to stderr. """ fmt = "%(asctime)s [%(levelname)s] [%(name)s:%(lineno)d] %(message)s" logging.root.setLevel(transient_level) # <--- IMPORTANT hand = TransientStreamHandler(level=level) hand.setFormatter(logging.Formatter(fmt)) logging.root.addHandler(hand) fabulous-0.3.0/fabulous/prompt.py000066400000000000000000000167171274117624500170720ustar00rootroot00000000000000# Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Input and output functions """ import sys import os import os.path import term from term import stdout, stderr, display __all__ = ["input_object","query","file_chooser"] # this constant is here because float() and int() give error messages # that would confuse most sane users. ERROR_MESSAGE = ( display('bright','red') + 'Error: ' + display('default') + '%s' + '\a' + os.linesep ) NICE_INPUT_ERRORS = { float: "The input ('%s') must be a number", int: "The input ('%s') must be an integer (-1, 0, 1, 2, etc.)" } DEFAULT_INPUT_ERRORS = "Bad input (%s)" def input_object(prompt_text, cast = None, default = None, prompt_ext = ': ', castarg = [], castkwarg = {}): """Gets input from the command line and validates it. prompt_text A string. Used to prompt the user. Do not include a trailing space. prompt_ext Added on to the prompt at the end. At the moment this must not include any control stuff because it is send directly to raw_input cast This can be any callable object (class, function, type, etc). It simply calls the cast with the given arguements and returns the result. If a ValueError is raised, it will output an error message and prompt the user again. Because some builtin python objects don't do casting in the way that we might like you can easily write a wrapper function that looks and the input and returns the appropriate object or exception. Look in the cast submodule for examples. If cast is None, then it will do nothing (and you will have a string) default function returns this value if the user types nothing in. This is can be used to cancel the input so-to-speek castarg, castkwarg list and dictionary. Extra arguments passed on to the cast. """ while True: stdout.write(prompt_text) value = stdout.raw_input(prompt_ext) if value == '': return default try: if cast != None: value = cast(value, *castarg, **castkwarg) except ValueError as details: if cast in NICE_INPUT_ERRORS: # see comment above this constant stderr.write(ERROR_MESSAGE % (NICE_INPUT_ERRORS[cast] % details)) else: stderr.write(ERROR_MESSAGE % (DEFAULT_INPUT_ERRORS % str(details))) continue return value def query(question, values, default=None, list_values = False, ignorecase = True ): """Preset a few options The question argument is a string, nothing magical. The values argument accepts input in two different forms. The simpler form (a tuple with strings) looks like: .. code-block:: python ('Male','Female') And it will pop up a question asking the user for a gender and requiring the user to enter either 'male' or 'female' (case doesn't matter unless you set the third arguement to false). The other form is something like: .. code-block:: python ({'values':('Male','M'),'fg':'cyan'}, {'values':('Female','F'),'fg':'magenta'}) This will pop up a question with Male/Female (each with appropriate colouring). Additionally, if the user types in just 'M', it will be treated as if 'Male' was typed in. The first item in the 'values' tuple is treated as default and is the one that is returned by the function if the user chooses one in that group. In addition the function can handle non-string objects quite fine. It simple displays the output object.__str__() and compares the user's input against that. So the the code .. code-block:: python query("Python rocks? ",(True, False)) will return a bool (True) when the user types in the string 'True' (Of course there isn't any other reasonable answer than True anyways :P) ``default`` is the value function returns if the user types nothing in. This is can be used to cancel the input so-to-speek Using list_values = False will display a list, with descriptions printed out from the 'desc' keyword """ values = list(values) for i in range(len(values)): if not isinstance(values[i], dict): values[i] = {'values': [values[i]]} try: import readline, rlcomplete wordlist = [ str(v) for value in values for v in value['values']] completer = rlcomplete.ListCompleter(wordlist, ignorecase) readline.parse_and_bind("tab: complete") readline.set_completer(completer.complete) except ImportError: pass valuelist = [] for item in values: entry = ( display('bright', item.get('fg'), item.get('bg')) + str(item['values'][0]) + display(['default']) ) if str(item['values'][0]) == str(default): entry = '['+entry+']' if list_values: entry += ' : ' + item['desc'] valuelist.append(entry) if list_values: question += os.linesep + os.linesep.join(valuelist) + os.linesep else: question += ' (' + '/'.join(valuelist) + ')' return input_object(question, cast = query_cast, default=default, castarg=[values,ignorecase]) def query_cast(value, answers, ignorecase = False): """A cast function for query Answers should look something like it does in query """ if ignorecase: value = value.lower() for item in answers: for a in item['values']: if ignorecase and (value == str(a).lower()): return item['values'][0] elif value == a: return item['values'][0] raise ValueError("Response '%s' not understood, please try again." % value) def file_chooser(prompt_text = "Enter File: ", default=None, filearg=[], filekwarg={}): """A simple tool to get a file from the user. Takes keyworded arguemnts and passes them to open(). If the user enters nothing the function will return the ``default`` value. Otherwise it continues to prompt the user until it get's a decent response. filekwarg may contain arguements passed on to ``open()``. """ try: import readline, rlcomplete completer = rlcomplete.PathCompleter() readline.set_completer_delims(completer.delims) readline.parse_and_bind("tab: complete") readline.set_completer(completer.complete) except ImportError: pass while True: f = raw_input(prompt_text) if f == '': return default f = os.path.expanduser(f) if len(f) != 0 and f[0] == os.path.sep: f = os.path.abspath(f) try: return open(f, *filearg, **filekwarg) except IOError as e: stderr.write(ERROR_MESSAGE % ("unable to open %s : %s" % (f, e))) if __name__ == '__main__': import doctest doctest.testmod() fabulous-0.3.0/fabulous/queries.py000066400000000000000000000015751274117624500172220ustar00rootroot00000000000000# Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Example values for the answers argument in prompt.query() """ true_false = ({'values':(True, 'T', 'Yes','Y',),'fg':'green'}, {'values':(False, 'F', 'No', 'N'),'fg':'red'}) sex = ({'values':('Male','M'),'fg':'cyan'}, {'values':('Female','F'),'fg':'magenta'}) fabulous-0.3.0/fabulous/rlcomplete.py000066400000000000000000000104031274117624500177010ustar00rootroot00000000000000# Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ fabulous.rlcomplete ~~~~~~~~~~~~~~~~~~~ Readline related stuff. """ import os class Completer(object): """A base class for completers. Child classes should implement the completelist method. """ text = None delims = '\t\n' def __init__(self): pass def complete(self, text, state): """The actual completion method This method is not meant to be overridden. Override the completelist method instead. It will make your life much easier. For more detail see documentation for readline.set_completer """ if text != self.text: self.matches = self.completelist(text) self.text = text try: return self.matches[state] except IndexError: return None def completelist(self, text): """Returns a list. The list contains a series of strings which are the suggestions for the given string ``text``. It is valid to have no suggestions (empty list returned). """ return [] class ListCompleter(Completer): """A class that does completion based on a predefined list. """ def __init__(self, words, ignorecase): self.words = words self.ignorecase = ignorecase def completelist(self,text): if self.ignorecase: return [w for w in self.words if w.lower().startswith(text.lower())] else: return [w for w in self.words if w.startswith(text)] class PathCompleter(Completer): """Does completion based on file paths. """ def buildpath(self, base, *paths): path = os.path.join(base,*paths) if os.path.isdir(os.path.expanduser(path)) and path[-1] != os.path.sep: path += os.path.sep return path @staticmethod def matchuserhome(prefix): """To find matches that start with prefix. For example, if prefix = '~user' this returns list of possible matches in form of ['~userspam','~usereggs'] etc. matchuserdir('~') returns all users """ if not prefix.startswith('~'): raise ValueError("prefix must start with ~") try: import pwd except ImportError: try: import winpwd as pwd except ImportError: return [] return ['~' + u[0] for u in pwd.getpwall() if u[0].startswith(prefix[1:])] def completelist(self, text): """Return a list of potential matches for completion n.b. you want to complete to a file in the current working directory that starts with a ~, use ./~ when typing in. Paths that start with ~ are magical and specify users' home paths """ path = os.path.expanduser(text) if len(path) == 0 or path[0] != os.path.sep: path = os.path.join(os.getcwd(), path) if text == '~': dpath = dtext = '' bpath = '~' files = ['~/'] elif text.startswith('~') and text.find('/', 1) < 0: return self.matchuserhome(text) else: dtext = os.path.dirname(text) dpath = os.path.dirname(path) bpath = os.path.basename(path) files = os.listdir(dpath) if bpath =='': matches = [self.buildpath(text, f) for f in files if not f.startswith('.')] else: matches = [self.buildpath(dtext, f) for f in files if f.startswith(bpath)] if len(matches) == 0 and os.path.basename(path)=='..': files = os.listdir(path) matches = [os.path.join(text, f) for f in files] return matches fabulous-0.3.0/fabulous/rotating_cube.py000066400000000000000000000114231274117624500203630ustar00rootroot00000000000000# Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ fabulous.rotating_cube ~~~~~~~~~~~~~~~~~~~~~~ Command for animating a wireframe rotating cube in the terminal. """ from __future__ import with_statement from __future__ import division import sys import time from math import cos, sin, pi from fabulous import color, utils class Frame(object): """Canvas object for drawing a frame to be printed """ def __enter__(self): self.width = utils.term.width self.height = utils.term.height * 2 self.canvas = [[' ' for x in range(self.width)] for y in range(self.height // 2)] return self def __exit__(self, type_, value, traceback): sys.stdout.write(self.render()) sys.stdout.flush() def __setitem__(self, p, c): (x, y) = p self.canvas[int(y // 2)][int(x)] = c def line(self, x0, y0, x1, y1, c='*'): r"""Draws a line Who would have thought this would be so complicated? Thanks again Wikipedia_ <3 .. _Wikipedia: http://en.wikipedia.org/wiki/Bresenham's_line_algorithm """ steep = abs(y1 - y0) > abs(x1 - x0) if steep: (x0, y0) = (y0, x0) (x1, y1) = (y1, x1) if x0 > x1: (x0, x1) = (x1, x0) (y0, y1) = (y1, y0) deltax = x1 - x0 deltay = abs(y1 - y0) error = deltax / 2 y = y0 if y0 < y1: ystep = 1 else: ystep = -1 for x in range(x0, x1 - 1): if steep: self[y, x] = c else: self[x, y] = c error = error - deltay if error < 0: y = y + ystep error = error + deltax def render(self): return "\n".join(["".join(line) for line in self.canvas]) def rotating_cube(degree_change=3, frame_rate=3): """Rotating cube program How it works: 1. Create two imaginary ellipses 2. Sized to fit in the top third and bottom third of screen 3. Create four imaginary points on each ellipse 4. Make those points the top and bottom corners of your cube 5. Connect the lines and render 6. Rotate the points on the ellipses and repeat """ degrees = 0 while True: t1 = time.time() with Frame() as frame: oval_width = frame.width oval_height = frame.height / 3.0 cube_height = int(oval_height * 2) (p1_x, p1_y) = ellipse_point(degrees, oval_width, oval_height) (p2_x, p2_y) = ellipse_point(degrees + 90, oval_width, oval_height) (p3_x, p3_y) = ellipse_point(degrees + 180, oval_width, oval_height) (p4_x, p4_y) = ellipse_point(degrees + 270, oval_width, oval_height) degrees = (degrees + degree_change) % 360 # connect square thing at top frame.line(p1_x, p1_y, p2_x, p2_y) frame.line(p2_x, p2_y, p3_x, p3_y) frame.line(p3_x, p3_y, p4_x, p4_y) frame.line(p4_x, p4_y, p1_x, p1_y) # connect top to bottom frame.line(p1_x, p1_y, p1_x, p1_y + cube_height) frame.line(p2_x, p2_y, p2_x, p2_y + cube_height) frame.line(p3_x, p3_y, p3_x, p3_y + cube_height) frame.line(p4_x, p4_y, p4_x, p4_y + cube_height) # connect square thing at bottom frame.line(p1_x, p1_y + cube_height, p2_x, p2_y + cube_height) frame.line(p2_x, p2_y + cube_height, p3_x, p3_y + cube_height) frame.line(p3_x, p3_y + cube_height, p4_x, p4_y + cube_height) frame.line(p4_x, p4_y + cube_height, p1_x, p1_y + cube_height) elapsed = (time.time() - t1) time.sleep(abs(1.0 / frame_rate - elapsed)) def ellipse_point(degrees, width, height): width -= 1 height -= 1 radians = degrees * (pi / 180.0) x = width/2.0 * cos(1) * sin(radians) - width/2.0 * sin(1) * cos(radians) y = height/2.0 * sin(1) * sin(radians) + height/2.0 * cos(1) * cos(radians) x = int(x + width/2.0) y = int(y + height/2.0) return (x, y) def main(): try: rotating_cube() except KeyboardInterrupt: pass if __name__ == '__main__': main() fabulous-0.3.0/fabulous/term.py000066400000000000000000000751021274117624500165110ustar00rootroot00000000000000# Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ fabulous.term ~~~~~~~~~~~~~ Terminal abstraction layer. Provides standard capabilites to a variety of terminals. Support information is being worked on. .. code-block:: python import os os.stdout.write('spam' + display('bright','yellow','white') + 'eggs' + display('default') + os.linesep) **Warning:** on IPython setting sys.stdout to stdout will break readline **Caveat:** Failure to flush after ouput can cause weird ordering behaviour when writing to stdout and stderr simutaniously. This should fix the worst of it, but application developers should be warned not to rely on the state of things between call between one method call and another """ __all__ = ['display', 'stdin', 'stdout', 'stderr', 'Term', 'UnixTerm', 'CursesTerm', 'WinTerm', 'Win32Term', 'WinCTypesTerm'] import sys import os import re try: unicode = unicode except NameError: unicode = str basestring = (str, bytes) # pylint: disable-msg=W0613 # pylint: disable-msg=W0102 # pylint: disable-msg=C0103 # pylint: disable-msg=W0142 # pylint: disable-msg=R0201 # pylint: disable-msg=W0511 class Term(object): """A file-like object which also supports terminal features. This is a base class for dumb terminals. It supports almost nothing. """ def __init__(self, stream): """Under Construction (:P) When overriding this in subclasses, please call Term.__init__(self, stream) first. Class specific imports are being done in the contructors and references to the modules are stored as instance variables. This method is cleaner and works nicely with inheritance. """ self.stream = stream def bell(self): """Causes the computer to beep Use sparingly, it is mainly to alert the user if something potentialy bad may be happening. """ pass def display(self, codes=[], fg=None, bg=None): """Not for public consumption (yet) Just use display() and stdout.write() for now. run this at the beginning:: (codes, fg, bg) = Magic.displayformat(codes, fg, bg) """ pass def move(self, place, distance = 1): """Move cursor position The valid values for place are: up Move up a line. down Move to the next line. This also puts you at the beginning of the line. left Move one place to the left. right Move one place to the right. beginning of line Move to the beginning of the current line. beginning of screen Move to the beginning of the screen. """ pass def clear(self, scope = 'screen'): """clears part of the screen The valid values for scope are: right Clears a single space directly to the right. left Clears a single space directly to the left. line Clears the current line. screen Clears the whole screen. beginning of line Clears from the current position to the beginning of the line end of line Clears from the current position to the end of the line end of screen Clears from the current position to the end of the screen N.b. this is not the same as deleting. After a place is cleared it should still be there, but with nothing in it. Also, this should not change the position of the cursor. """ pass def get_size(self): """Get the width and height of the terminal. Returns either a tuple of two integers or None. If two integers are returned, the first one is the number of columns (or width) and the second value is the number of lines (or height). If None is returned, then the terminal does not support this feature. If you still need to have a value to fall back on (75, 25) is a fairly descent fallback. """ return None def set_title(self, name): """Sets the title of the terminal """ pass # methods below here are also methods of the file object def isatty(self): """Returns True if the terminal is a terminal This should always be True. If it's not somebody is being rather nauty. """ return self.stream.isatty() def fileno(self): """Returns the stream's file descriptor as an integer""" return self.stream.fileno() # write-specific methods def write(self, text): """Parses text and prints proper output to the terminal This method will extract escape codes from the text and handle them as well as possible for whichever platform is being used. At the moment only the display escape codes are supported. """ escape_parts = re.compile('\x01?\x1b\\[([0-9;]*)m\x02?') chunks = escape_parts.split(text) i = 0 for chunk in chunks: if chunk != '': if i % 2 == 0: self.stream.write(chunk) else: c = chunk.split(';') r = Magic.rdisplay(c) self.display(**r) #see caveat 0 self.flush() i += 1 def writelines(self, sequence_of_strings): """Write out a sequence of strings Note that newlines are not added. The sequence may be any iterable object producing strings. This is equivalent to calling write() for each string. """ map(self.write, sequence_of_strings) def flush(self): """Ensure the text is ouput to the screen. The write() method will do this automatically, so only use this when using self.stream.write(). """ return self.stream.flush() # read-specific methods, they are in need of help def getch(self): """Don't use this yet It doesn't belong here but I haven't yet thought about a proper way to implement this feature and the features that will depend on it. """ pass def raw_input(self, prompt): """Don't use this yet It doesn't belong here but I haven't yet thought about a proper way to implement this feature and the features that will depend on it. """ return raw_input(prompt) def next(self): return self.stream.next() def readline(self, *args, **kwargs): return self.stream.readline(*args, **kwargs) def readlines(self, *args, **kwargs): return self.stream.readlines(*args, **kwargs) def read(self, *args, **kwargs): return self.stream.read(*args, **kwargs) # read-only properties @property def mode(self): return self.stream.mode @property def newlines(self): return self.stream.newlines @property def encoding(self): return self.stream.encoding @property def softspace(self): return self.stream.softspace @property def name(self): return self.stream.name class UnixTerm(Term): def __init__(self, stream): import termios import tty self.termios = termios self.tty = tty Term.__init__(self, stream) def getch(self): """Don't use this yet It doesn't belong here but I haven't yet thought about a proper way to implement this feature and the features that will depend on it. """ return NotImplemented fno = stdout.fileno() mode = self.termios.tcgetattr(fno) try: self.tty.setraw(fno, self.termios.TCSANOW) ch = self.read(1) finally: self.termios.tcsetattr(fno, self.termios.TCSANOW, mode) return ch class CursesTerm(UnixTerm): def __init__(self, stream): import curses self.curses = curses UnixTerm.__init__(self, stream) if not sys.stdout.isatty(): return self.curses.setupterm() def bell(self): self.stream.write(self._get_cap('bel')) def display(self, codes=[], fg=None, bg=None): """Displays the codes using ANSI escapes """ codes, fg, bg = Magic.displayformat(codes, fg, bg) self.stream.write(Magic.display(codes, fg, bg)) self.flush() def move(self, place, distance = 1): """see doc in Term class""" for d in range(distance): self.stream.write(self._get_cap('move '+place)) self.flush() def clear(self, scope = 'screen'): """see doc in Term class""" if scope == 'line': self.clear('beginning of line') self.clear('end of line') else: self.stream.write(self._get_cap('clear '+scope)) self.flush() def get_size(self): """see doc in Term class""" self.curses.setupterm() return self.curses.tigetnum('cols'), self.curses.tigetnum('lines') def set_title(self, name): self.write(Magic.OSC + '0;'+str(name) + "\x07") def _get_cap(self, cap): strcaps = { 'move up':'cuu1', 'move down':'cud1', 'move left':'cub1', 'move right':'cuf1', 'move beginning of line':'cr', 'move beginning of screen':'home', 'clear beginning of line':'el1','clear end of line':'el', 'clear screen':'clear', 'clear end of screen':'ed', 'clear left':'kbs','clear right':'dch1', 'delete line':'dl1', 'bell':'bel'} if cap in ('cols','lines'): self.curses.setupterm() c = self.curses.tigetnum(cap) if c > 0: return c elif strcaps.has_key(cap): c = self.curses.tigetstr(strcaps[cap]) if c != '': return c raise ValueError("capability '%s' not supported" % cap) class WinTerm(Term): """Windows version of terminal control This class should not be used by itself, use either Win32Terminal or WinCTypesTerminal classes that subclasses of this class. This class makes extensive use of the Windows API The official documentation for the API is on MSDN (look for 'console functions') """ # TODO: is there a better way to get this? STD_INPUT_HANDLE = -10 STD_OUTPUT_HANDLE = -11 STD_ERROR_HANDLE = -12 # These contants are defined in PyWin32 # You can combine the values by doing a bitwise or (|) # for example FG_BLUE | FG_RED would give magenta (0x05) # # these contants are just numbers, It's most useful to think of # them in binary FG_BLUE = 1 << 0 FG_GREEN = 1 << 1 FG_RED = 1 << 2 FG_INTENSITY = 1 << 3 BG_BLUE = 1 << 4 BG_GREEN = 1 << 5 BG_RED = 1 << 6 BG_INTENSITY = 1 << 7 FG_ALL = FG_BLUE | FG_GREEN | FG_RED BG_ALL = BG_BLUE | BG_GREEN | BG_RED # there are also these codes, but according to tcsh's win32/console.c: # COMMON_LVB_REVERSE_VIDEO is buggy, so I'm staying away from it. Future # versions should implement COMMON_LVB_UNDERSCORE. # COMMON_LVB_REVERSE_VIDEO = 0x4000 # COMMON_LVB_UNDERSCORE 0x8000 FG = { 'black': 0, 'red': FG_RED, 'green': FG_GREEN, 'yellow': FG_GREEN | FG_RED, 'blue': FG_BLUE, 'magenta': FG_BLUE | FG_RED, 'cyan': FG_BLUE | FG_GREEN, 'white': FG_BLUE | FG_GREEN | FG_RED, } BG = { 'black':0, 'red':BG_RED, 'green':BG_GREEN, 'yellow':BG_GREEN | BG_RED, 'blue':BG_BLUE, 'magenta':BG_BLUE | BG_RED, 'cyan':BG_BLUE | BG_GREEN, 'white':BG_BLUE | BG_GREEN | BG_RED, } default_attributes = None hidden_output = False reverse_output = False reverse_input = False dim_output = False real_fg = None def __init__(self, stream): import msvcrt self.msvcrt = msvcrt Term.__init__(self, stream) self._stdout_handle = self._get_std_handle(self.STD_OUTPUT_HANDLE) self._stderr_handle = self._get_std_handle(self.STD_ERROR_HANDLE) self.default_attributes = self._get_console_info()['attributes'] self.real_fg = self.default_attributes & 0x7 def display(self, codes=[], fg=None, bg=None): """Displays codes using Windows kernel calls """ codes, fg, bg = Magic.displayformat(codes, fg, bg) color = 0 for c in codes: try: f = getattr(self, '_display_' + c) out = f() if out: color |= out except AttributeError: pass cfg, cfgi, cbg, cbgi = self._split_attributes( self._get_console_info()['attributes']) if self.reverse_input: cfg, cbg = (cbg // 0x10), (cfg * 0x10) cfgi, cbgi = (cbgi // 0x10), (cfgi * 0x10) if fg != None: color |= self.FG[fg] self.real_fg = self.FG[fg] else: color |= cfg if bg != None: color |= self.BG[bg] else: color |= cbg color |= (cfgi | cbgi) fg, fgi, bg, bgi = self._split_attributes(color) if self.dim_output: # intense black fg = 0 fgi = self.FG_INTENSITY if self.reverse_output: fg, bg = (bg // 0x10), (fg * 0x10) fgi, bgi = (bgi // 0x10), (fgi * 0x10) self.reverse_input = True if self.hidden_output: fg = (bg // 0x10) fgi = (bgi // 0x10) self._set_attributes(fg | fgi | bg | bgi) def get_size(self): """see doc in Term class""" attr = self._get_console_info() cols = attr['window']['right'] - attr['window']['left'] + 1 lines = attr['window']['bottom'] - attr['window']['top'] + 1 return cols, lines def _get_std_handle(self, handleno): """Returns a handle from GetStdHandle handleno is one of: * self.STD_OUTPUT_HANDLE for stdout * self.STD_ERROR_HANDLE for stderr """ #TODO: is NotImplemented the proper way to do this? return NotImplemented def _get_console_info(self): """Get information from GetConsoleScreenBufferInfo Returns a dictionary with the following keys:: max size position window attributes size Note: the y part of size is misleading """ return NotImplemented def _clear_console(self, length, start): """Clears a part of the console Has a similar effect as writing out spaces. length: int length of cleared section start : tuple of x and y coords to start at """ return NotImplemented def _set_attributes(self, code): """ Set console attributes with `code` Not implemented here. To be implemented by subclasses. """ return NotImplemented def _split_attributes(self, attrs): """Spilt attribute code Takes an attribute code and returns a tuple containing foreground (fg), foreground intensity (fgi), background (bg), and background intensity (bgi) Attributes can be joined using ``fg | fgi | bg | bgi`` """ fg = attrs & self.FG_ALL fgi = attrs & self.FG_INTENSITY bg = attrs & self.BG_ALL bgi = attrs & self.BG_INTENSITY return fg, fgi, bg, bgi def _undim(self): self.dim_output = False if self.reverse_input: a = self._get_console_info()['attributes'] & 0x8f self._set_attributes( (self.real_fg * 0x10) | a) else: a = self._get_console_info()['attributes'] & 0xf8 self._set_attributes(self.real_fg | a) def _display_default(self): self.hidden_output = False self.reverse_output = False self.reverse_input = False self.dim_output = False self.real_fg = self.default_attributes & 0x7 self._set_attributes(self.default_attributes) def _display_bright(self): self._undim() return self.FG_INTENSITY def _display_dim(self): self.dim_output = True def _display_reverse(self): self.reverse_output = True def _display_hidden(self): self.hidden_output = True def _get_position(self): """Set the cursor's current position Returns a tuple in the form (x, y) """ pos = self._get_console_info()['position'] return pos['x'], pos['y'] def _set_position(self, coord): """Set the cursor's position coord is a tuple in the form (x, y) """ return NotImplemented def move(self, place, distance = 1): """see doc in Term class""" x, y = self._get_position() if place == 'up': y -= distance elif place == 'down': for i in range(distance): print nx, ny = self._get_position() y = ny self.move('beginning of line') elif place == 'left': x -= distance elif place == 'right': x += distance elif place == 'beginning of line': x = 0 elif place == 'beginning of screen': x = 0 y = self._get_console_info()['window']['top'] else: raise ValueError("invalid place to move") self._set_position((x, y)) def clear(self, scope = 'screen'): """see doc in Term class According to http://support.microsoft.com/kb/99261 the best way to clear the console is to write out empty spaces """ #TODO: clear attributes too if scope == 'screen': bos = (0, self._get_console_info()['window']['top']) cols, lines = self.get_size() length = cols * lines self._clear_console(length, bos) self.move('beginning of screen') elif scope == ' beginning of line': pass elif scope == 'end of line': curx, cury = self._get_position() cols, lines = self.get_size() coord = (curx, cury) length = cols - curx self._clear_console(length, coord) elif scope == 'end of screen': curx, cury = self._get_position() coord = (curx, cury) cols, lines = self.get_size() length = (lines - cury) * cols - curx self._clear_console(length, coord) elif scope == 'line': curx, cury = self._get_position() coord = (0, cury) cols, lines = self.get_size() self._clear_console(cols, coord) self._set_position((curx, cury)) elif scope == 'left': self.move('left') self.write(' ') elif scope == 'right': self.write(' ') self.move('left') else: raise ValueError("invalid scope to clear") def getch(self): """Don't use this yet It doesn't belong here but I haven't yet thought about a proper way to implement this feature and the features that will depend on it. """ return NotImplemented return self.msvcrt.getch() def bell(self): self.stream.write('\x07') class Win32Term(WinTerm): """PyWin32 version of Windows terminal control. Uses the PyWin32 Libraries . ActiveState has good documentation for them: Main page: http://aspn.activestate.com/ASPN/docs/ActivePython/2.4/pywin32/PyWin32.html Console related objects and methods: http://aspn.activestate.com/ASPN/docs/ActivePython/2.4/pywin32/PyConsoleScreenBuffer.html """ def __init__(self, stream): import win32console self.win32console = win32console WinTerm.__init__(self, stream) def set_title(self, name): return self.win32console.SetConsoleTitle(name) def _get_console_info(self): # example output from GetConsoleScreenBufferInfo # {'MaximumWindowSize': PyCOORDType(X=80,Y=82), # 'CursorPosition': PyCOORDType(X=0,Y=6), # 'Window': PySMALL_RECTType(Left=0,Top=0,Right=79,Bottom=24), # 'Attributes': 7, # 'Size': PyCOORDType(X=80,Y=300)} attrs = self._stdout_handle.GetConsoleScreenBufferInfo() return {'max size': self._pyCoord_dict(attrs['MaximumWindowSize']), 'position': self._pyCoord_dict(attrs['CursorPosition']), 'window': self._pySMALL_RECTType_dict(attrs['Window']), 'attributes': attrs['Attributes'], # y part of size value is misleading 'size': self._pyCoord_dict(attrs['Size']) } def _get_std_handle(self, handle): return self.win32console.GetStdHandle(handle) def _get_title(self): return self.win32console.GetConsoleTitle() def _set_attributes(self, attr): self._stdout_handle.SetConsoleTextAttribute(attr) def _set_position(self, coord): coord = self.win32console.PyCOORDType(coord[0], coord[1]) self._stdout_handle.SetConsoleCursorPosition(coord) def _clear_console(self, length, start): # length: int # start : tuple of x and y coords char = unicode(' ') coord = self.win32console.PyCOORDType(start[0], start[1]) # char is unicode self._stdout_handle.FillConsoleOutputCharacter( char, length, coord) def _pyCoord_dict(self, coord): return { 'x': coord.X, 'y': coord.Y} def _pySMALL_RECTType_dict(self, rect): return { 'left': rect.Left, 'top': rect.Top, 'right': rect.Right, 'bottom': rect.Bottom} class WinCTypesTerm(WinTerm): """CTypes version of Windows terminal control. It requires the CTypes libraries As of Python 2.5, CTypes is included in Python by default. User's of previous version of Python will have to install it if they what to use this. """ def __init__(self, stream): import ctypes self.ctypes = ctypes WinTerm.__init__(self, stream) def set_title(self, name): self.ctypes.windll.kernel32.SetConsoleTitleA(name) def _get_console_info(self): # From IPython's winconsole.py, by Alexander Belchenko import struct csbi = self.ctypes.create_string_buffer(22) res = self.ctypes.windll.kernel32.GetConsoleScreenBufferInfo( self._stdout_handle, csbi) (bufx, bufy, curx, cury, wattr, left, top, right, bottom, maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw) return {'max size': {'x':maxx, 'y':maxy }, 'position': {'x':curx, 'y':cury }, 'window': {'left': left, 'top': top, 'right': right, 'bottom': bottom}, 'attributes': wattr, # y part of size value is misleading 'size': {'x':maxx, 'y':maxy } } def _get_std_handle(self, handle): return self.ctypes.windll.kernel32.GetStdHandle(handle) def _get_title(self): """According to http://support.microsoft.com/kb/124103 the buffer size is 1024 Does not support unicode, only ANSI""" #TODO: unicode support strbuffer = self.ctypes.create_string_buffer(1024) size = self.ctypes.c_short(1024) #unicode versions are (Get|Set)ConsolTitleW self.ctypes.windll.kernel32.GetConsoleTitleA(strbuffer, size) return strbuffer.value def _set_attributes(self, attr): self.ctypes.windll.kernel32.SetConsoleTextAttribute( self._stdout_handle, attr) def _set_position(self, coord): coord = self._get_coord(coord) self.ctypes.windll.kernel32.SetConsoleCursorPosition( self._stdout_handle, coord) def _clear_console(self, length, start): # length: int # start : tuple of x and y coords char = self.ctypes.c_char(' ') coord = self._get_coord(start) charswritten = self.ctypes.c_int() clength = self.ctypes.c_int(length) self.ctypes.windll.kernel32.FillConsoleOutputCharacterA( self._stdout_handle, char, clength, coord, charswritten) def _get_coord(self, coord): """ It's a hack, see fixcoord in pyreadline's console.py (revision 1289) """ x, y = coord return self.ctypes.c_int(y << 16 | x) class Magic(object): """Special codes and what not Don't use these alone see http://vt100.net/docs/vt100-ug/chapter3.html Based on the ANSI X3.64 standard. See http://en.wikipedia.org/wiki/ANSI_X3.64 """ ESCAPE = '\x1b' CSI = ESCAPE +'[' OSC = ESCAPE +']' # see the reset method RESET = ESCAPE + 'c' # pylint: disable-msg=E0602 DISPLAY = {'default':0, 'bright':1, 'dim':2, 'underline':4, 'blink':5, 'reverse':7, 'hidden':8 } rDISPLAY = dict( (v, k) for k, v in DISPLAY.items()) # Yellow is a bit weird, xterm and rxvt display dark yellow, while linux # and Windows display a more brown-ish color. Bright yellow is always # yellow. Order is important here COLORS = { 'black':0, 'red':1, 'green':2, 'yellow':3, 'blue':4, 'magenta':5, 'cyan':6, 'white':7 } rCOLORS = dict( (v, k) for k, v in COLORS.items()) # pylint: enable-msg=E0602 # TODO: setf from curses uses the colors in a different order for @staticmethod def displayformat(codes=[], fg=None, bg=None): """Makes sure all arguments are valid""" if isinstance(codes, basestring): codes = [codes] else: codes = list(codes) for code in codes: if code not in Magic.DISPLAY.keys(): raise ValueError("'%s' not a valid display value" % code) for color in (fg, bg): if color != None: if color not in Magic.COLORS.keys(): raise ValueError("'%s' not a valid color" % color) return [codes, fg, bg] @staticmethod def rdisplay(codes): """Reads a list of codes and generates dict >>> Magic.rdisplay([]) {} >>> result = Magic.rdisplay([1,2,34,46]) >>> sorted(result.keys()) ['bg', 'codes', 'fg'] >>> sorted(result['codes']) ['bright', 'dim'] >>> result['bg'] 'cyan' >>> result['fg'] 'blue' """ dcodes = [] fg = bg = None for code in codes: code = int(code) offset = code // 10 decimal = code % 10 if offset == 3 and decimal in Magic.COLORS.values(): fg = decimal elif offset == 4 and decimal in Magic.COLORS.values(): bg = decimal elif code in Magic.DISPLAY.values(): dcodes.append(code) else: pass # drop unhandled values r = {} if len(codes): r['codes'] = [Magic.rDISPLAY[c] for c in dcodes] if fg != None: r['fg'] = Magic.rCOLORS[fg] if bg != None: r['bg'] = Magic.rCOLORS[bg] return r @staticmethod def display(codes=[], fg=None, bg=None): codes, fg, bg = Magic.displayformat(codes, fg, bg) codes = [str(Magic.DISPLAY[code]) for code in codes] if fg != None: codes.append(str(30 + Magic.COLORS[fg])) if bg != None: codes.append(str(40 + Magic.COLORS[bg])) return Magic.CSI + ";".join(codes) + 'm' def display(codes=[], fg=None, bg=None): """Returns an ANSI display code. This is useful when writing to an Term codes A list containing strings. The strings should one of the keys in ``Magic.DISPLAY``. It can also be just a single string. fg, bg A string. Explicitly for setting the foreground or background. Use one of the keys in ``Magic.COLORS``. .. code-block:: python # give bright blue foreground and white background with underline display(('bright','underline'),'blue','white') # gives a blue foreground display(fg='blue') # resets the color to the default. display('default') Avoid using black or white. Depending on the situation the default background/foreground is normally black or white, but it's hard to tell which. Bare terminals are normally white on black, but virtual terminals run from X or another GUI system are often black on white. This can lead to unpredicatble results. If you want reversed colours, use the 'reverse' code, and if you want to set the colors back to their original colors, use the 'default' code. Also, be prudent with your use of 'hidden' and 'blink'. Several terminals do not support them (and for good reason too), they can be really annoying and make reading difficult. """ return Magic.display(codes, fg, bg) # try to use the Windows method first because their are some terminals on # MS Windows that support both the Windows and curses methods, but their # curses implementations are buggy. def _get_terms(): terms = None if 'win32' in sys.platform or 'cygwin' == sys.platform: terms = _get_term(WinCTypesTerm) or _get_term(Win32Term) if not terms: terms = (_get_term(CursesTerm) or _get_term(UnixTerm) or _get_term(Term)) return terms def _get_term(termclass): try: return (termclass(sys.stdin), termclass(sys.stdout), termclass(sys.stderr)) except ImportError: return None stdin, stdout, stderr = _get_terms() fabulous-0.3.0/fabulous/test_transientlogging.py000066400000000000000000000043261274117624500221570ustar00rootroot00000000000000# Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import itertools from fabulous.logs import * from fabulous.gotham import * from fabulous.color import * try: next except NameError: next = lambda x: x.next() logger = logging.getLogger('fabulous') def luv(): msg = "hello there how are you? i love you! sincerely compy <3 <3" while True: for n in itertools.chain(range(len(msg)), reversed(range(len(msg)))): yield msg[:n+1] def bad_things(): yield red("godzilla attack") yield bold(red("magnetic poles reverse")) yield red("caffeine use criminalized") yield bold(red("more horrible things happening....")) yield highlight_red("THIS INFORMATION IS NOT BEING DROWNED OUT!") yield bold(red("hip hip hooray")) def test_transientlogger(): import random, time happy, nightmare = luv(), bad_things() try: while True: if random.randrange(60) == 0: logger.warning(next(nightmare)) else: logger.debug(next(happy)) time.sleep(0.02) except StopIteration: pass def test_transientlogger2(): import time, random gothic = lorem_gotham() try: while True: if random.randrange(20) == 0: logger.warning(red(next(gothic))) else: logger.debug(next(gothic)) time.sleep(0.1) except StopIteration: pass if __name__ == '__main__': basicConfig(level=logging.WARNING) logging.warning("RUNNING TEST: test_transientlogger()") test_transientlogger() logging.warning("RUNNING TEST: test_transientlogger2()") test_transientlogger2() fabulous-0.3.0/fabulous/text.py000066400000000000000000000215571274117624500165330ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ fabulous.text ~~~~~~~~~~~~~ The text module makes it possible to print TrueType text to the terminal. This functionality is available on the command line:: jart@compy:~$ fabulous-text --help jart@compy:~$ fabulous-text --skew=5 --shadow 'Fabulous!' jart@compy:~$ python -m fabulous.text --help Or as a Python library: .. code-block:: python from fabulous import text print text.Text("Fabulous!", color='#0099ff', shadow=True, skew=5) To make things simple, Fabulous bundles the following Google Noto Fonts which look good and are guaranteed to work no matter what: - NotoSans-Bold - NotoEmoji-Regular For other fonts, Fabulous will do its best to figure out where they are stored. If Fabulous has trouble finding your font, try using an absolute path *with* the extension. It's also possible to put the font in the ``~/.fonts`` directory and then running ``fc-cache -fv ~/.fonts``. You can run ``fabulous-text --list`` to see what fonts are available. """ from __future__ import print_function import os import sys from fabulous import utils, image, grapefruit from fabulous.compatibility import printy try: unicode = unicode except NameError: unicode = str basestring = (str, bytes) class Text(image.Image): u"""Renders TrueType Text to Terminal I'm a sub-class of :class:`fabulous.image.Image`. My job is limited to simply getting things ready. I do this by: - Turning your text into an RGB-Alpha bitmap image using :mod:`PIL` - Applying way cool effects (if you choose to enable them) For example:: >>> assert Text("Fabulous", shadow=True, skew=5) >>> txt = Text("lorem ipsum", font="NotoSans-Bold") >>> len(str(txt)) > 0 True >>> txt = Text(u"😃", font="NotoSans-Bold") >>> len(str(txt)) > 0 True :param text: The text you want to display as a string. :param fsize: The font size in points. This obviously end up looking much larger because in fabulous a single character is treated as one horizontal pixel and two vertical pixels. :param color: The color (specified as you would in HTML/CSS) of your text. For example Red could be specified as follows: ``red``, ``#00F`` or ``#0000FF``. :param shadow: If true, render a simple drop-shadow beneath text. The Fabulous logo uses this feature. :param skew: Skew size in pixels. This applies an affine transform to shift the top-most pixels to the right. The Fabulous logo uses a five pixel skew. :param font: The TrueType font you want. If this is not an absolute path, Fabulous will search for your font by globbing the specified name in various directories. """ def __init__(self, text, fsize=23, color="#0099ff", shadow=False, skew=None, font='NotoSans-Bold'): utils.pil_check() from PIL import Image, ImageFont, ImageDraw self.text = text self.color = grapefruit.Color.NewFromHtml(color) self.font = ImageFont.truetype(resolve_font(font), fsize) skew = skew or 0 size = tuple([n + 3 + skew for n in self.font.getsize(self.text)]) self.img = Image.new("RGBA", size, (0, 0, 0, 0)) cvs = ImageDraw.Draw(self.img) if shadow: cvs.text((2 + skew, 2), self.text, font=self.font, fill=(150, 150, 150, 150)) cvs.text((1 + skew, 1), self.text, font=self.font, fill=self.color.html) if skew: self.img = self.img.transform( size, Image.AFFINE, (1.0, 0.1 * skew, -1.0 * skew, 0.0, 1.0, 0.0)) self.resize(None) class FontNotFound(ValueError): """I get raised when the font-searching hueristics fail This class extends the standard :exc:`ValueError` exception so you don't have to import me if you don't want to. """ def resolve_font(name): """Turns font names into absolute filenames This is case sensitive. The extension should be omitted. For example:: >>> path = resolve_font('NotoSans-Bold') >>> fontdir = os.path.join(os.path.dirname(__file__), 'fonts') >>> noto_path = os.path.join(fontdir, 'NotoSans-Bold.ttf') >>> noto_path = os.path.abspath(noto_path) >>> assert path == noto_path Absolute paths are allowed:: >>> resolve_font(noto_path) == noto_path True Raises :exc:`FontNotFound` on failure:: >>> try: ... resolve_font('blahahaha') ... assert False ... except FontNotFound: ... pass """ if os.path.exists(name): return os.path.abspath(name) fonts = get_font_files() if name in fonts: return fonts[name] raise FontNotFound("Can't find %r :'( Try adding it to ~/.fonts" % name) @utils.memoize def get_font_files(): """Returns a list of all font files we could find Returned as a list of dir/files tuples:: get_font_files() -> {'FontName': '/abs/FontName.ttf', ...] For example:: >>> fonts = get_font_files() >>> 'NotoSans-Bold' in fonts True >>> fonts['NotoSans-Bold'].endswith('/NotoSans-Bold.ttf') True """ roots = [ '/usr/share/fonts/truetype', # where ubuntu puts fonts '/usr/share/fonts', # where fedora puts fonts os.path.expanduser('~/.fonts'), # custom user fonts os.path.abspath(os.path.join(os.path.dirname(__file__), 'fonts')), ] result = {} for root in roots: for path, dirs, names in os.walk(root): for name in names: if name.endswith(('.ttf', '.otf')): result[name[:-4]] = os.path.join(path, name) return result def main(): """Main function for :command:`fabulous-text`.""" import optparse parser = optparse.OptionParser() parser.add_option( "-l", "--list", dest="list", action="store_true", default=False, help=("List available fonts")) parser.add_option( "-S", "--skew", dest="skew", type="int", default=None, help=("Apply skew effect (measured in pixels) to make it look " "extra cool. For example, Fabulous' logo logo is skewed " "by 5 pixels. Default: %default")) parser.add_option( "-C", "--color", dest="color", default="#0099ff", help=("Color of your text. This can be specified as you would " "using HTML/CSS. Default: %default")) parser.add_option( "-B", "--term-color", dest="term_color", default=None, help=("If you terminal background isn't black, please change " "this value to the proper background so semi-transparent " "pixels will blend properly.")) parser.add_option( "-F", "--font", dest="font", default='NotoSans-Bold', help=("Name of font file, or absolute path to one. Use the --list " "flag to see what fonts are available. Fabulous bundles the " "NotoSans-Bold and NotoEmoji-Regular fonts, which are guaranteed " "to work. Default: %default")) parser.add_option( "-Z", "--size", dest="fsize", type="int", default=23, help=("Size of font in points. Default: %default")) parser.add_option( "-s", "--shadow", dest="shadow", action="store_true", default=False, help=("Size of font in points. Default: %default")) (options, args) = parser.parse_args(args=sys.argv[1:]) if options.list: print("\n".join(sorted(get_font_files()))) return if options.term_color: utils.term.bgcolor = options.term_color text = " ".join(args) if not isinstance(text, unicode): text = text.decode('utf-8') for line in text.split("\n"): fab_text = Text(line, skew=options.skew, color=options.color, font=options.font, fsize=options.fsize, shadow=options.shadow) for chunk in fab_text: printy(chunk) if __name__ == '__main__': main() fabulous-0.3.0/fabulous/utils.py000066400000000000000000000104631274117624500167010ustar00rootroot00000000000000# Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ fabulous.utils ~~~~~~~~~~~~~~ Miscellaneous utilities for Fabulous. """ import os import sys import fcntl import struct import termios import textwrap import functools from fabulous import grapefruit def memoize(function): """A very simple memoize decorator to optimize pure-ish functions Don't use this unless you've examined the code and see the potential risks. """ cache = {} @functools.wraps(function) def _memoize(*args): if args in cache: return cache[args] result = function(*args) cache[args] = result return result return function class TerminalInfo(object): """Quick and easy access to some terminal information I'll tell you the terminal width/height and it's background color. You don't need to use me directly. Just access the global :data:`term` instance:: >>> assert term.width > 0 >>> assert term.height > 0 It's important to know the background color when rendering PNG images with semi-transparency. Because there's no way to detect this, black will be the default:: >>> term.bgcolor (0.0, 0.0, 0.0, 1.0) >>> from fabulous import grapefruit >>> isinstance(term.bgcolor, grapefruit.Color) True If you use a white terminal, you'll need to manually change this:: >>> term.bgcolor = 'white' >>> term.bgcolor (1.0, 1.0, 1.0, 1.0) >>> term.bgcolor = grapefruit.Color.NewFromRgb(0.0, 0.0, 0.0, 1.0) >>> term.bgcolor (0.0, 0.0, 0.0, 1.0) """ def __init__(self, bgcolor='black'): self.bgcolor = bgcolor @property def termfd(self): """Returns file descriptor number of terminal This will look at all three standard i/o file descriptors and return whichever one is actually a TTY in case you're redirecting i/o through pipes. """ for fd in (2, 1, 0): if os.isatty(fd): return fd raise Exception("No TTY could be found") @property def dimensions(self): """Returns terminal dimensions Don't save this information for long periods of time because the user might resize their terminal. :return: Returns ``(width, height)``. If there's no terminal to be found, we'll just return ``(79, 40)``. """ try: call = fcntl.ioctl(self.termfd, termios.TIOCGWINSZ, "\000" * 8) except IOError: return (79, 40) else: height, width = struct.unpack("hhhh", call)[:2] return (width, height) @property def width(self): """Returns width of terminal in characters """ return self.dimensions[0] @property def height(self): """Returns height of terminal in lines """ return self.dimensions[1] def _get_bgcolor(self): return self._bgcolor def _set_bgcolor(self, color): if isinstance(color, grapefruit.Color): self._bgcolor = color else: self._bgcolor = grapefruit.Color.NewFromHtml(color) bgcolor = property(_get_bgcolor, _set_bgcolor) term = TerminalInfo() def pil_check(): """Check for PIL library, printing friendly error if not found We need PIL for the :mod:`fabulous.text` and :mod:`fabulous.image` modules to work. Because PIL can be very tricky to install, it's not listed in the ``setup.py`` requirements list. """ try: import PIL except ImportError: raise ImportError("Please install PIL to use this feature: " "https://pillow.readthedocs.io/en/latest" "/installation.html") fabulous-0.3.0/fabulous/widget.py000066400000000000000000000173331274117624500170270ustar00rootroot00000000000000# Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ fabulous.widget ~~~~~~~~~~~~~~~ Widget library using terminate. """ import os import math from datetime import datetime # import textwrap from term import stdout, display class ProgressBar(object): """A 3-line progress bar, which looks like:: title 39% [================>----------------------------] message p = ProgressBar('spam') # create bar p.update(0, 'starting spam') # start printing it out p.update(50, 'spam almost ready') # progress p.update(100, 'spam complete') """ # content, length TITLE_FORMAT = {'text':display('bright','cyan') + '%s' + display('default'), 'length':0, 'padding':0 } BAR_FORMAT = {'text':' %3d%% ' + '[%s'+display('dim')+'%s'+display('default')+']', 'length':8, 'padding':2 } MESSAGE_FORMAT = {'text': '%s', 'length': 0, 'padding': 0 } def __init__(self, title = None): """ """ self.drawn = False cols = stdout.get_size()[0] self.width = cols -1 # TODO: make a better fix for systems that put \n on new line self.title = [] self.barlines = 0 self.message = [] self.messageline = None self.refresh = False self.set_title(title) def set_title(self, title = None): """ """ if title == None: self.title = [] else: length = self.width - self.TITLE_FORMAT['padding']*2 - self.TITLE_FORMAT['length'] text = title[:length].center(length) # we need to keep it on one line for now padding = ' ' * self.TITLE_FORMAT['padding'] self.title = [padding + (self.TITLE_FORMAT['text'] % text) + padding] self.refresh = self.drawn #lines = [(padding + line + padding) for line in textwrap.wrap( # text, self.width - (self.TITLE_FORMAT['padding']*2), # replace_whitespace=False)] #self.title = os.linesep.split( # self.TITLE_FORMAT['text'] % os.linesep.join(lines)) def get_title(self): """ """ return self.title def get_bar(self, percent): """ """ barlength = self.width - self.BAR_FORMAT['padding']*2 - self.BAR_FORMAT['length'] full = int( math.ceil(barlength * (percent / 100.0)) ) empty = int(barlength - full) if full == 0 or empty == 0: fullpiece = ('=' * full) else: fullpiece = ('=' * (full-1)) + '>' emptypiece = ('-' * empty) return [(self.BAR_FORMAT['text'] % (percent, fullpiece, emptypiece))] def set_message(self, message = None): """ """ """""" if message == None: self.message = [] else: length = self.width - self.MESSAGE_FORMAT['padding']*2 - self.MESSAGE_FORMAT['length'] text = message[:length].center(length) # we need to keep it on one line for now padding = ' ' * self.MESSAGE_FORMAT['padding'] self.message = [padding + (self.MESSAGE_FORMAT['text'] % text) + padding] def get_message(self): """returns None or string""" if self.message == []: return None else: return os.linesep.join(self.message) def update(self, percent, message = None, test = False): """ """ if self.refresh: self.clear() if self.drawn: stdout.move('beginning of line') stdout.move('up', len(self.message) + self.barlines) else: title = self.get_title() if title != None: for line in self.get_title(): stdout.write(line + os.linesep) self.drawn = True bar = self.get_bar(percent) refresh = (len(bar) != self.barlines) self.barlines = len(bar) for line in bar: stdout.clear('line') stdout.write(line) stdout.move('down') stdout.move('beginning of line') if (message != self.get_message()) or refresh: stdout.clear('end of screen') self.set_message(message) for line in self.message: stdout.write(line) stdout.move('down') else: stdout.move('down', len(self.message)) def clear(self): """ """ if self.drawn: stdout.move('beginning of line') stdout.move('up', len(self.message)) stdout.move('up', self.barlines) stdout.move('up', len(self.get_title())) stdout.clear('end of screen') self.drawn = False self.refresh = False class TimedProgressBar(ProgressBar): """A 3-line progress bar, which looks like:: title 39% [================>----------------------------] ETA mm:ss message p = ProgressBar('spam') # create bar p.update(0, 'starting spam') # start printing it out p.update(50, 'spam almost ready') # progress p.update(100, 'spam complete') """ BAR_FORMAT = {'text':' %3d%% ' + '[%s'+display('dim')+'%s'+display('default')+']', 'length':13, 'padding':2 } ' ETA 12:23' # what fraction of percent it acurate too precision = 100 def __init__(self, title = None): ProgressBar.__init__(self, title) self.start = datetime.today() def get_bar(self, percent): now = datetime.today() timed = now - self.start etatext = '' etadiv = int(percent*self.precision) if timed.seconds >= 1: etatext += ' ' if int(percent * self.precision) !=0: eta = (timed * 100 * self.precision)/int(percent * self.precision) days = eta.days min, sec = divmod(eta.seconds, 60) hours, min = divmod(min, 60) if days == 1: etatext += '1 day, ' elif days: etatext += '%d days, ' % days if hours: etatext += '%02d:' % hours etatext += '%02d:%02d' % (min, sec) else: etatext += 'Never' barlength = (self.width - self.BAR_FORMAT['padding']*2 - self.BAR_FORMAT['length'] - len(etatext)) full = int( math.ceil(barlength * (percent / 100.0)) ) empty = int(barlength - full) if full == 0 or empty == 0: fullpiece = ('=' * full) else: fullpiece = ('=' * (full-1)) + '>' emptypiece = ('-' * empty) return [(self.BAR_FORMAT['text'] % (percent, fullpiece, emptypiece))+etatext] class Spinner(object): spinners=['/','-','\\','|',] def __init__(self): self.drawn = False self.state = 0 def spin(self): if self.drawn == True: self.clear() else: self.drawn = True stdout.write(self.spinners[self.state]) self.state = (self.state + 1) % len(self.spinners) def clear(self): stdout.clear('left') stdout.move('left') fabulous-0.3.0/fabulous/xterm256.py000066400000000000000000000102601274117624500171300ustar00rootroot00000000000000# Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """ fabulous.xterm256 ~~~~~~~~~~~~~~~~~ The xterm256 module provides support for the 256 colors supported by xterm as well as quantizing 24-bit RGB color to xterm color ids. Color quantization may perform slowly depending on whether or not Fabulous is able to compile ``~/.xterm256.so`` on the fly. This is a tiny library that makes color quantization go much faster. The pure Python version of the algorithm is really slow because it's implemented as a brute force nearest neighbor over Euclidean distance search. Although an O(1) version of this algorithm exists with slightly less correctness. Your humble author simply hasn't had the time to implement it in this library. """ import logging CUBE_STEPS = [0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF] BASIC16 = ((0, 0, 0), (205, 0, 0), (0, 205, 0), (205, 205, 0), (0, 0, 238), (205, 0, 205), (0, 205, 205), (229, 229, 229), (127, 127, 127), (255, 0, 0), (0, 255, 0), (255, 255, 0), (92, 92, 255), (255, 0, 255), (0, 255, 255), (255, 255, 255)) def xterm_to_rgb(xcolor): """Convert xterm Color ID to an RGB value All 256 values are precalculated and stored in :data:`COLOR_TABLE` """ assert 0 <= xcolor <= 255 if xcolor < 16: # basic colors return BASIC16[xcolor] elif 16 <= xcolor <= 231: # color cube xcolor -= 16 return (CUBE_STEPS[xcolor // 36 % 6], CUBE_STEPS[xcolor // 6 % 6], CUBE_STEPS[xcolor % 6]) elif 232 <= xcolor <= 255: # gray tone c = 8 + (xcolor - 232) * 0x0A return (c, c, c) COLOR_TABLE = [xterm_to_rgb(i) for i in range(256)] def rgb_to_xterm(r, g, b): """Quantize RGB values to an xterm 256-color ID This works by envisioning the RGB values for all 256 xterm colors as 3D euclidean space and brute-force searching for the nearest neighbor. This is very slow. If you're very lucky, :func:`compile_speedup` will replace this function automatically with routines in `_xterm256.c`. """ if r < 5 and g < 5 and b < 5: return 16 best_match = 0 smallest_distance = 10000000000 for c in range(16, 256): d = (COLOR_TABLE[c][0] - r) ** 2 + \ (COLOR_TABLE[c][1] - g) ** 2 + \ (COLOR_TABLE[c][2] - b) ** 2 if d < smallest_distance: smallest_distance = d best_match = c return best_match def compile_speedup(): """Tries to compile/link the C version of this module Like it really makes a huge difference. With a little bit of luck this should *just work* for you. You need: - Python >= 2.5 for ctypes library - gcc (``sudo apt-get install gcc``) """ import os import ctypes from os.path import join, dirname, getmtime, exists, expanduser # library = join(dirname(__file__), '_xterm256.so') library = expanduser('~/.xterm256.so') sauce = join(dirname(__file__), '_xterm256.c') if not exists(library) or getmtime(sauce) > getmtime(library): build = "gcc -fPIC -shared -o %s %s" % (library, sauce) if (os.system(build + " >/dev/null 2>&1") != 0): raise OSError("GCC error") xterm256_c = ctypes.cdll.LoadLibrary(library) xterm256_c.init() def xterm_to_rgb(xcolor): res = xterm256_c.xterm_to_rgb_i(xcolor) return ((res >> 16) & 0xFF, (res >> 8) & 0xFF, res & 0xFF) return (xterm256_c.rgb_to_xterm, xterm_to_rgb) try: (rgb_to_xterm, xterm_to_rgb) = compile_speedup() except OSError: logging.debug("fabulous failed to compile xterm256 speedup code") fabulous-0.3.0/python-fabulous.spec000066400000000000000000000031321274117624500173550ustar00rootroot00000000000000%global modname fabulous Name: python-fabulous Version: 0.3.0 Release: 2%{?dist} Summary: Makes your terminal output totally fabulous Group: Development/Languages License: Apache 2.0 / OFL URL: https://jart.github.io/fabulous Source0: https://github.com/jart/fabulous/releases/download/0.3.0/fabulous-0.3.0.tar.gz BuildArch: noarch BuildRequires: gcc BuildRequires: python-devel BuildRequires: python-setuptools Requires: python-imaging %description fabulous is a python module for producing fabulously colored terminal output. Run the demo to see what's available:: $ python -m fabulous.demo %prep %setup -q -n %{modname}-%{version} %build %{__python} setup.py build %install %{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT %files %defattr(-,root,root,-) %doc README COPYING %{python_sitelib}/* %changelog * Tue Jul 12 2016 Justine Tunney - 0.3.0-1 - Update for version 0.3.0 - Add Python 3 support * Mon Jul 7 2016 Justine Tunney - 0.2.1-1 - Update for version 0.2.1 * Mon Jul 4 2016 Justine Tunney - 0.2.0-1 - Update for version 0.2.0 - Change MIT code to Apache 2.0 licence - Use Google Noto Fonts * Sun Jul 3 2016 Justine Tunney - 0.1.8-1 - Update for version 0.1.8 - Remove grapefruit dependency * Fri Apr 20 2012 Ralph Bean - 0.1.5-2 - Included README and COPYING in the doc macro * Thu Apr 05 2012 Ralph Bean - 0.1.5-1 - initial package for Fedora fabulous-0.3.0/setup.py000066400000000000000000000050771274117624500150660ustar00rootroot00000000000000# Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # http://packages.python.org/distribute/setuptools.html # http://diveintopython3.org/packaging.html # http://wiki.python.org/moin/CheeseShopTutorial # http://pypi.python.org/pypi?:action=list_classifiers import os try: import setuptools except ImportError: import ez_setup ez_setup.use_setuptools(version='0.6c11') import setuptools def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() version = __import__('fabulous').__version__ setuptools.setup( name = 'fabulous', version = version, url = 'https://jart.github.io/fabulous', author = 'Justine Tunney', author_email = 'jtunney@gmail.com', description = 'Makes your terminal output totally fabulous', download_url = ('https://github.com/jart/fabulous/releases' '/download/' + version + '/fabulous-' + version + '.tar.gz'), long_description = read('README.rst'), license = 'Apache 2.0 / OFL', packages = ['fabulous', 'fabulous.experimental'], zip_safe = False, include_package_data = True, entry_points = { 'console_scripts': [ 'fabulous-demo = fabulous.demo:main', 'fabulous-gotham = fabulous.gotham:main', 'fabulous-image = fabulous.image:main', 'fabulous-rotatingcube = fabulous.rotating_cube:main', 'fabulous-text = fabulous.text:main', ], }, classifiers = [ "Development Status :: 5 - Production/Stable", "License :: OSI Approved :: Apache Software License", "Environment :: Console", "Intended Audience :: Developers", "Programming Language :: C", "Programming Language :: Python", "Topic :: Utilities", "Topic :: Artistic Software", "Topic :: System :: Logging", "Topic :: Multimedia :: Graphics" ], ) fabulous-0.3.0/tests/000077500000000000000000000000001274117624500145055ustar00rootroot00000000000000fabulous-0.3.0/tests/casts_test.py000077500000000000000000000023641274117624500172430ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import unittest from fabulous import casts class TestControl(unittest.TestCase): def test_casts_yes_no(self): self.failUnlessEqual(casts.yes_no('y'), True) self.failUnlessEqual(casts.yes_no('n'), False) self.failUnlessEqual(casts.yes_no('Yes'), True) self.failUnlessEqual(casts.yes_no('nO'), False) self.failUnlessRaises(ValueError, casts.yes_no, 'spam') def test_casts_file(self): self.failUnlessEqual( type(casts.file(__file__)), type(open(__file__)) ) self.failUnlessRaises(ValueError, casts.file, '') if __name__ == '__main__': unittest.main() fabulous-0.3.0/tests/doctests.txt000066400000000000000000000005511274117624500170770ustar00rootroot00000000000000 # note: this requires minimock >>> from minimock import Mock >>> terminal.raw_input = Mock('terminal.raw_input') >>> terminal.raw_input.mock_returns = 123 >>> stdout.write = Mock('stdout.write') >>> input_object('Hello World', int) Called stdout.write('Hello World') Called terminal.raw_input(': ') 123fabulous-0.3.0/tests/manual/000077500000000000000000000000001274117624500157625ustar00rootroot00000000000000fabulous-0.3.0/tests/manual/clear.py000066400000000000000000000012471274117624500174260ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from fabulous.term import stdout stdout.clear() fabulous-0.3.0/tests/manual/colors.py000066400000000000000000000031431274117624500176360ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from fabulous.term import display, stdout, Magic stdout.write(display('default')) stdout.write("default text\n") stdout.write("regular foreground test:\n") for color in Magic.COLORS.iterkeys(): stdout.write(display(fg=color)) stdout.write(" " + color + '\n') stdout.write(display('default')) stdout.write("regular background test:\n") for color in Magic.COLORS.iterkeys(): stdout.write(display(bg=color)) stdout.write(" " + color + '\n') stdout.write(display('default')) stdout.write("bright foreground test:\n") stdout.write(display('bright')) for color in Magic.COLORS.iterkeys(): stdout.write(display(fg=color)) stdout.write(" " + color + '\n') stdout.write(display('default')) stdout.write("dim foreground test:\n") stdout.write(display('dim')) for color in Magic.COLORS.iterkeys(): stdout.write(display(fg=color)) stdout.write(" " + color + '\n') stdout.write(display('bright','red')) stdout.write("bright red\n") stdout.write(display("default")) fabulous-0.3.0/tests/manual/convulsions.py000077500000000000000000000017771274117624500207350ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """I am *not* responsible if this causes severe physical or mental injury """ from fabulous.term import display, stdout, Magic try: while True: for c in Magic.COLORS.iterkeys(): stdout.write(display(bg=c)) stdout.move('down',4) except KeyboardInterrupt: stdout.write(display('default')) print print "Interupt from keyboard. Exiting." fabulous-0.3.0/tests/manual/filechooser.py000077500000000000000000000012731274117624500206440ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from fabulous.prompt import file_chooser f = file_chooser() print f fabulous-0.3.0/tests/manual/info.py000066400000000000000000000014221274117624500172660ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Prints out information that is useful for debugging """ from fabulous.term import stdout, stderr print "term.stdout",stdout print "term.stderr",stderr fabulous-0.3.0/tests/manual/longtimedprogressbar.py000077500000000000000000000020221274117624500225670ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """This is a long timed progress bar. Don't expect it to finish any time soon. It updates every 0 to 10 seconds. Send it a keyboard interupt (Ctrl-C) to stop.""" from fabulous.widget import TimedProgressBar import time import random print __doc__ p = TimedProgressBar('bar') p.precision=10000 n = 100000 for i in range(n): p.update(float(i)/n *100, 'update '+str(i)) time.sleep(random.random()* 10) fabulous-0.3.0/tests/manual/move.py000066400000000000000000000016141274117624500173040ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Outputs something like: spaand eggs george """ from fabulous.term import stdout stdout.write('spam') stdout.move('left') stdout.write('and') stdout.move('right') stdout.write('eggs') stdout.move('left',3) stdout.move('down') stdout.write('george') print # trailing newline fabulous-0.3.0/tests/manual/progressbar.py000077500000000000000000000017231274117624500206730ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from fabulous.widget import ProgressBar import time p = ProgressBar('bar') p.update(0) time.sleep(.2) p.update(20, 'safe epam and eggs') time.sleep(.2) p.update(50, None) time.sleep(.2) p.update(40) time.sleep(.2) p.set_title('baz') p.update(20, 'spam and eggs') time.sleep(.2) p.update(70) time.sleep(.2) p.update(80) time.sleep(.2) p.update(100) fabulous-0.3.0/tests/manual/spinner.py000066400000000000000000000013531274117624500200140ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from fabulous.widget import Spinner import time s = Spinner() for i in range(100): s.spin() time.sleep(.01) fabulous-0.3.0/tests/manual/timedprogressbar.py000077500000000000000000000015051274117624500217140ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from fabulous.widget import TimedProgressBar import time import random p = TimedProgressBar('bar') n = 100 for i in range(n): p.update(float(i)/n *100, 'update '+str(i)) time.sleep(random.random()) fabulous-0.3.0/tests/manual/title.py000066400000000000000000000013711274117624500174570ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from fabulous.term import stdout import time stdout.set_title("Terminal title has been changed") raw_input("Press Enter to exit") fabulous-0.3.0/tests/rlcomplete_test.py000077500000000000000000000027231274117624500202730ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import unittest from fabulous import rlcomplete class TestControl(unittest.TestCase): def test_ListCompleter(self): c = rlcomplete.ListCompleter(['foo','bar','baz'], True) self.failUnlessEqual(c.completelist('a'), []) self.failUnlessEqual(c.completelist('B'), ['bar','baz']) self.failUnlessEqual(c.completelist('f'), ['foo']) c = rlcomplete.ListCompleter(['foo','bar','baz'], False) self.failUnlessEqual(c.completelist('B'), []) self.failUnlessEqual(c.completelist('b'), ['bar','baz']) def test_PathCompleter(self): #c = rlcomplete.PathCompleter() #self.failUnlessEqual(c.completelist('m'), ['manual/']) pass if __name__ == '__main__': unittest.main() #Desktop/ bin/ lib/ personal/ src/ workspace/ #Templates/ doc/ media/ share/ tmp/ www/ fabulous-0.3.0/tests/term_test.py000077500000000000000000000040451274117624500170730ustar00rootroot00000000000000#!/usr/bin/env python # # Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from fabulous.term import display, Magic, Term import os import unittest devnull = open(os.devnull, 'w') stdout = Term(devnull) class TestTerm(unittest.TestCase): def set_display(self,codes=[], fg=None, bg=None): stdout.write(display(codes, fg, bg)) def test_color(self): self.set_display('default') stdout.write("default text") stdout.write("regular foreground test:") for color in Magic.COLORS.keys(): self.set_display(fg=color) stdout.write(" " + color) self.set_display('default') stdout.write("regular background test:") for color in Magic.COLORS.keys(): self.set_display(bg=color) stdout.write(" " + color) self.set_display('default') stdout.write("bright foreground test:") self.set_display('bright') for color in Magic.COLORS.keys(): self.set_display(fg=color) stdout.write(" " + color) self.set_display('default') stdout.write("dim foreground test:") self.set_display('dim') for color in Magic.COLORS.keys(): self.set_display(fg=color) stdout.write(" " + color) self.set_display('bright','red') stdout.write("bright red") self.set_display("default") if __name__ == '__main__': unittest.main() fabulous-0.3.0/update-gh-pages.sh000077500000000000000000000022621274117624500166570ustar00rootroot00000000000000#!/bin/bash # # Copyright 2016 The Fabulous Authors. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. [[ "$(basename "$(pwd)")" == "fabulous" ]] || { echo not fabulous dir; exit 1; } git diff --shortstat | grep -q . && { echo repo is dirty; exit 1; } set -ex sudo chown -R $USER . chmod -R o+rX . sudo python setup.py install sudo chown -R jart . git clean -fdx rm -rf gh-pages sphinx-build docs /tmp/fabulous-gh-pages git checkout gh-pages cp -R /tmp/fabulous-gh-pages/* . cp -R /tmp/fabulous-gh-pages/.doctrees . cp /tmp/fabulous-gh-pages/.buildinfo . git add . git commit -m 'Rebuild Sphinx documentation' git push origin gh-pages git checkout master rm -rf /tmp/fabulous-gh-pages