pax_global_header00006660000000000000000000000064135500142650014513gustar00rootroot0000000000000052 comment=5e4503f659090b8bde69d7d35823540acc6362b9 highline-2.0.3/000077500000000000000000000000001355001426500133045ustar00rootroot00000000000000highline-2.0.3/.gitignore000066400000000000000000000000641355001426500152740ustar00rootroot00000000000000doc pkg .DS_Store coverage Gemfile.lock *.swp *.swo highline-2.0.3/.rubocop.yml000066400000000000000000000040641355001426500155620ustar00rootroot00000000000000# Bellow the changes to default Rubocop behavior. # See options at: # https://github.com/bbatsov/rubocop/tree/master/config AllCops: TargetRubyVersion: 2.1 # General # Multi-line method chaining should be done with leading dots. Layout/DotPosition: EnforcedStyle: trailing SupportedStyles: - leading - trailing # Enabling # -------- Style/CollectionMethods: Description: 'Preferred collection methods.' StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#map-find-select-reduce-size' Enabled: true Style/StringMethods: Description: 'Checks if configured preferred methods are used over non-preferred.' Enabled: true # Disabling # --------- # Modifying # --------- # Check quotes usage according to lint rule below. Style/StringLiterals: Enabled: true EnforcedStyle: double_quotes # Configuration parameters: SupportedStyles. # SupportedStyles: snake_case, camelCase Naming/MethodName: EnforcedStyle: snake_case Exclude: - 'test/**/*' # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles, AutoCorrectEncodingComment. # SupportedStyles: when_needed, always, never Style/Encoding: Enabled: true # If we fix this, it will change method signature. # Save it for major release. Style/OptionalArguments: Exclude: - 'lib/highline/list_renderer.rb' # TemplateRenderer should never fail on method missing. Style/MethodMissing: Exclude: - 'lib/highline/template_renderer.rb' # This is a breaking change for MRI 1.9. # Revoke when updating to MRI 2.0 as minimum. # Cop supports --auto-correct. # Configuration parameters: MinSize, SupportedStyles. # SupportedStyles: percent, brackets Style/SymbolArray: EnforcedStyle: brackets # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent Layout/IndentHeredoc: Exclude: - 'examples/page_and_wrap.rb' - 'highline.gemspec' - 'test/acceptance/acceptance.rb' # Cop supports --auto-correct. Lint/ScriptPermission: Enabled: false highline-2.0.3/.simplecov000066400000000000000000000001171355001426500153050ustar00rootroot00000000000000unless SimpleCov.running SimpleCov.start do add_filter "test_" end end highline-2.0.3/.travis.yml000066400000000000000000000014551355001426500154220ustar00rootroot00000000000000--- language: ruby dist: trusty cache: bundler script: "bundle exec rake test" rvm: - 2.0 - 2.1 - 2.2 - 2.3 - 2.4 - jruby-19mode # JRuby in 1.9 mode - jruby-head notifications: email: false matrix: allow_failures: - rvm: 1.9 - rvm: ruby-head - rvm: rbx-3.81 - rvm: jruby-19mode # JRuby in 1.9 mode - rvm: jruby-head fast_finish: true include: - rvm: 1.9 before_install: - "gem update --system -N" - "gem update bundler -N" - rvm: 2.5 before_install: - gem install bundler - rvm: 2.6 before_install: - gem install bundler - rvm: ruby-head before_install: - gem install bundler - rvm: rbx-3.81 before_install: - gem install bundler bundler_args: --without code_quality highline-2.0.3/AUTHORS000066400000000000000000000003751355001426500143610ustar00rootroot00000000000000James Edward Gray II:: {james@grayproductions.net}[mailto:james@grayproductions.net] Gregory Brown:: {gregory.t.brown@gmail.com}[mailto:gregory.t.brown@gmail.com] Richard LeBer:: {richard.leber@gmail.com}[mailto:richard.leber@gmail.com]highline-2.0.3/COPYING000066400000000000000000000432541355001426500143470ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. highline-2.0.3/Changelog.md000066400000000000000000000563621355001426500155310ustar00rootroot00000000000000## Change Log Below is a complete listing of changes for each revision of HighLine. ### 2.0.3 / 2019-10-11 * PR #245 - Suppress `Psych.safe_load` arg warn (@koic) ### 2.0.2 / 2019-04-08 * PR #243 - Add new capital_letter option to menu index (@Ana06) * This is a really special PR. It has come from "The Biggest Mobprogramming Session Ever" and around 250 people participated in crafting it! * PR #241 - CI: Add 2.6 (@olleolleolle) * PR #240 - Avoid YARD warning UnknownParam (@olleolleolle) ### 2.0.1 / 2019-01-23 * PR #238 / I #237 - Rescue Errno::ENOTTY when pipeing ### 2.0.0 / 2018-06-10 * Release 2.0.0 (major version release). ### 2.0.0-develop.16 / 2018-05-12 * PR #231 - Deprecate safe_level of ERB.new in Ruby 2.6 (@koic) * PR #230 - Fix behavior when shell and gather options are selected together ### 2.0.0-develop.15 / 2017-12-28 * PR #229 - Update .travis.yml. Add Ruby 2.5 to matrix (@abinoam) ### 2.0.0-develop.14 / 2017-11-21 * PR #222 / I #221 - Fix inconsistent behaviour when using agree with readline (@abinoam, @ailisp) ### 2.0.0-develop.13 / 2017-11-05 * PR #219 - Make possible to use a callable as response (@abinoam) ### 2.0.0-develop.12 / 2017-10-19 * PR #218 - Ease transition from 1.7.x to 2.0.x (@abinoam) * Copy use_color from HighLine.default_instance * Expose IOConsoleCompatible * PR #216 - Update .appveyor.yml - Fix Windows CI (@abinoam) ### 2.0.0-develop.11 / 2017-09-25 * PR #215 - Apply several Rubocop stylistic suggestions (@abinoam) * Update gemspec/Gemfile to newer standards * Update travis configuration fixing 1.9 problem * Adjust .rubocop.yml with things we don't want to change ### 2.0.0-develop.10 / 2017-06-29 * PR #214 - Remove `$terminal` (global variable) (@abinoam) * Use HighLine.default_instance instead * Reorganize/Group code at lib/highline.rb ### 2.0.0-develop.9 / 2017-06-24 * PR #211 / PR #212 - HighLine#use_color= and use_color? as instance methods (@abinoam, @phiggins) * PR #203 / I #191 - Default values are shown in menus by Frederico (@fredrb) * PR #201 / I #198 - Confirm in question now accepts Proc (@mmihira) * PR #197 - Some HighLine::Menu improvements * Move Menu::MenuItem to Menu::Item with its own file * Some small refactorings ### 2.0.0-develop.8 / 2016-06-03 * PR #195 - Add PRONTO to development group at Gemfile by Abinoam Jr. (@abinoam) ### 2.0.0-develop.7 / 2016-05-31 * PR #194 - Indices coloring on HighLine::Menu by Aregic (@aregic) * PR #190 - Add Ruby 2.3.0 to travis matrix by Koichi (@koic/ruby-23) * PR #189 - Improve #agree tests by @kevinoid ### 2.0.0-develop.6 / 2016-02-01 * PR #184 - Menu improvements, bug fixes, and more tests by Geoff Lee (@matrinox) * Add third arg to menu that overides the choice displayed to the user * FIX: autocomplete prompt does not include menu choices after the first * Add specs to cover the new features and the bug fix * PR #183 - Fix menu example in README.md by Fabien Foerster (@fabienfoerster) ### 2.0.0-develop.5 / 2015-12-27 * Fix #180 with PR #181 - Make it possible to overwrite the menu prompt shown on errors. ### 2.0.0-develop.4 / 2015-12-14 This versions makes the code documentation 100% 'A' grade on inch. We have used inch and http://inch-ci.org to guide the priorities on documentation production. The grade 'A' (on inch) number of objects on master branch was 44,22% (153/346). After this PR we have a 100% grade 'A' (344 objects). There's already a inch-ci.org badge on README.md. And now it's all green! We also bring some improvement on CodeClimate scores. #### CHANGES SUMMARY * PR #179 - Make inch happy. Grade "A" for the whole HighLine documentation. By Abinoam Jr. (@abinoam) * PR #178 - Improve score on Code Climate by applying some refactoring. By Abinoam Jr. (@abinoam) * PR #172 - Initial work on documentation by Abinoam Jr. (@abinoam) * Use yard * Use inch * New Readme file * Fix #166 with PR #173 by (@matugm) ### 2.0.0-develop.3 / 2015-10-28 This version brings some improvements on documentation (switch to Yardoc). This is the first 2.0.0-develop.x version to be release as gem. ### 2.0.0-develop.2 / 2015-09-09 (by Abinoam P. Marques Jr. - @abinoam) #### NOTES This version brings greater compatibility with JRuby and Windows. But we still have a lot of small issues in both platforms. We were able to unify/converge all approaches into using io/console, so we could delete old code that relied solely on stty, termios, java api and windows apis (DL and Fiddle). Another improvement is the beginning of what I called "acceptance tests". If you type ```rake acceptance``` you'll be guided through some tests where you have to input some thing and see if everything work as expected. This makes easier to catch bugs that otherwise would be over-sighted. #### CHANGES SUMMARY * Fix Simplecov - it was reporting erroneous code coverage * Add new tests. Improves code coverage * Extract HighLine::BuiltinStyles * Try to avoid nil checking * Try to avoid class variables (mis)use * Fix RDoc include path and some small fixes to the docs * Move HighLine::String to its own file * Add HighLine::Terminal::IOConsole - Add an IOConsoleCompatibility module with some stubbed methods for using at StringIO, File and Tempfile to help on tests. - Any enviroment that can require 'io/console' will use HighLine::Terminal::IOConsole by default. This kind of unifies most environments where HighLine runs. For example, we can use Terminal::IOConsole on JRuby!!! * Add ruby-head and JRuby (19mode and head) to Travis CI matrix. Yes, this our first step to a more peaceful JRuby compatibility. * Add AppVeyor Continuous Integration for Windows * Add _acceptance_ tests for HighLine - Use ```rake acceptance``` to run them - Basically it interactively asks the user to confirm if some expected HighLine behavior is actually happening. After that it gather some environment debug information, so the use could send to the HighLine contributors in case of failure. * Remove old and unused files (as a result of relying on io/console) - JRuby - Windows (DL and Fiddle) - Termios * Fix some small (old and new) bugs * Make some more tuning for Windows compatibility * Make some more tuning for JRuby compatibility ### 2.0.0-develop.1 / 2015-06-11 This is the first development version of the 2.0.0 series. It's the begining of a refactoring phase on HighLine development cycle. #### SOME HISTORY In 2014 I emailed James Edward Gray II (@JEG2) about HighLine. One of his ideas was to completely refactor the library so that it could be easier to reuse and improve it. I've began my contributions to HighLine trying to fix some of the open issues at that time so that we could "freeze" a stable version of HighLine that people could rely on. Then I've began to study HighLine source code with James' help and started to refactor some parts of the code. Abinoam P. Marques Jr. (@abinoam) #### NOTES * This release differs from current master branch by more than 180 commits. * The main changes will be only summarized bellow (as there are many, and a detailed description of each is not productive). * You could try `git log -p` to see all of them. * During the last commits, all possible efforts were taken to preserve the tests passing status. * 100% test passing gives you no guarantee that this new version will work for you. This happens for many reasons. One of them is that we don't currently have 100% test coverage. * So, this version is not suitable for use in production. * [Metric_fu](https://github.com/metricfu/metric_fu) and [Code Climate](https://codeclimate.com/github/abinoam/highline) were used here not to strictly "guide" what should be changed, but to have some way to objectively measure the progresses made so far. #### CHANGES SUMMARY * Extracted a lot of smaller methods from bigger ones * Extracted smaller classes/modules from bigger ones, so they could be self contained with less external dependencies as possible, for example: * HighLine::Statement * HighLine::List * HighLine::ListRenderer * HighLine::TemplateRenderer * HighLine::Question::AnswerConverter * HighLine::Terminal * HighLine::Terminal::UnixStty * HighLine::Paginator * HighLine::Wrapper * After extracting each class/module some refactoring were applied to them lowering code complexity #### METRICS SUMMARY Some of the metrics used to track progress are summarized bellow. Some of them have got a lot better as Flay, Flog and Reek, others like Cane haven't (probably because we didn't commented out the new code yet) __CODECLIMATE__ * GPA: 3.60 -> 3.67 (higher is better) __CANE__ - reports code quality threshold violations (lower is better) * Total 92 -> 105 * Methods exceeding allowed Abc complexity: 14 -> 10 * Lines violating style requirements: 69 -> 72 * Class definitions requiring comments: 9 -> 23 __FLAY__ - analyzes ruby code for structural similarities (code duplication - lower is better) * Total: 490 -> 94 __FLOG__ - measures code complexity (lower is better) * Top 5% average: 127.9458 -> 40.99812 * Average: 17.37982 -> 7.663875 * Total: 2158.5 -> 1969.6 __REEK__ - detects common code smells in ruby code (lower is better) * DuplicateMethodCall: 144 -> 54 * TooManyStatements: 26 -> 30 ### 1.7.3 / 2015-06-29 * Add HighLine::Simulator tests (Bala Paranj (@bparanj) and Abinoam Marques Jr. (@abinoam), #142, PR #143) ### 1.7.2 / 2015-04-19 #### Bug fixes * Fix #138 (a regression of #131). PR #139. ### 1.7.1 / 2015-02-24 #### Enhancements * Add travis CI configuration (Eli Young (@elyscape), #130) * Add Rubinius to Build Matrix with Allowed Failure (Brandon Fish (bjfish), #132) * Make some adjustments on tests (Abinoam Marques Jr., #133, #134) * Drop support for Ruby 1.8 (Abinoam Marques Jr., #134) #### Bug fixes * Fix IO.console.winsize returning reversed column and line values (Fission Xuiptz (@fissionxuiptz)), #131) ### 1.7.0 / 2015-02-18 #### Bug fixes * Fix correct encoding of statements to output encoding (Dāvis (davispuh), #110) * Fix character echoing when echo is false and multibyte character is typed (Abinoam Marques Jr., #117 #118) * Fix backspace support on Cyrillic (Abinoam Marques Jr., #115 #118) * Fix returning wrong encoding when echo is false (Abinoam Marques Jr., #116 #118) * Fix Question #limit and #realine incompatibilities (Abinoam Marques Jr. #113 #120) * Fix/improve string coercion on #say (Abinoam Marques Jr., #98 #122) * Fix #terminal_size returning nil in some terminals (Abinoam Marques Jr., #85 #123) #### Enhancements * Improve #format_statement String coercion (Michael Bishop (michaeljbishop), #104) * Update homepage url on gemspec (Rubyforge->GitHub) (Edward Anderson (nilbus), #107) * Update COPYING file (Vít Ondruch (voxik), #109) * Improve multi-byte encoding support (Abinoam Marques Jr., #115 #116 #117 #118) * Make :grey -> :gray and :light -> :bright aliases (Abinoam Marques Jr., #114 #119) * Return the default object (as it is) when no answer given (Abinoam Marques Jr., #112 #121) * Added test for Yaml serialization of HighLine::String (Abinoam Marques Jr., #69 #124) * Make improvements on Changelog and Rakefile (Abinoam Marques Jr., #126 #127 #128) ### 1.6.21 * Improved Windows integration (by Ronie Henrich). * Clarified menu choice error messages (by Keith Bennett). ### 1.6.20 * Fixed a bug with FFI::NCurses integration (by agentdave). * Improved StringExtensions performance (by John Leach). * Various JRuby fixes (by presidentbeef). ### 1.6.19 * Fixed `terminal_size()` with jline2 (by presidentbeef). ### 1.6.18 * Fixed a long supported interface that was accidentally broken with a recent change (by Rubem Nakamura Carneiro). ### 1.6.17 * Added encoding support to menus (by David Lyons). * Some minor fixes to SystemExtensions (by whiteleaf and presidentbeef). ### 1.6.16 * Added the new indention feature (by davispuh). * Separated auto-completion from the answer type (by davispuh). * Improved JRuby support (by rsutphin). * General code clean up (by stomar). * Made HighLine#say() a little smarter with regard to color escapes (by Kenneth Murphy). ### 1.6.15 * Added support for nil arguments in lists (by Eric Saxby). * Fixed HighLine's termios integration (by Jens Wille). ### 1.6.14 * Added JRuby 1.7 support (by Mina Nagy). * Take into account color escape sequences when wrapping text (by Mark J. Titorenko). ### 1.6.13 * Removed unneeded Shebang lines (by Scott Gonyea). * Protect the String passed to Question.new from modification (by michael). * Added a retype-to-verify setting (by michael). ### 1.6.12 * Silenced warnings (by James McEwan). ### 1.6.11 * Fixed a bad test. (Fix by Diego Elio Pettenò.) ### 1.6.10 * Fixed a regression that prevented asking for String arguments (by Jeffery Sman.) * Fixed a testing incompatibility (by Hans de Graaff.) ### 1.6.9 * The new list modes now properly ignore escapes when sizing. * Added a project gemspec file. * Fixed a bug that prevented the use of termios (by tomdz). * Switch to JLine to provide better echo support on JRuby (by tomdz). ### 1.6.8 * Fix missing ERASE_CHAR reference (by Aaron Gifford). ### 1.6.7 * Fixed bug introduced in 1.6.6 attempted fix (by Aaron Gifford). ### 1.6.6 * Fixed old style references causing HighLine::String errors (by Aaron Gifford). ### 1.6.5 * HighLine#list() now correctly handles empty lists (fix by Lachlan Dowding). * HighLine#list() now supports :uneven_columns_across and :uneven_columns_down modes. ### 1.6.4 * Add introspection methods to color_scheme: definition, keys, to_hash. * Add tests for new methods. ### 1.6.3 * Add color NONE. * Add RGB color capability. * Made 'color' available as a class or instance method of HighLine, for instance: HighLine.color("foo", :blue)) or highline_obj.color("foo", :blue) are now both possible and equivalent. * Add HighLine::String class with convenience methods: #color (alias #foreground), #on (alias #background), colors, and styles. See lib/string_extensions.rb. * Add (optional) ability to extend String with the same convenience methods from HighLine::String, using Highline.colorize_strings. ### 1.6.2 * Correctly handle STDIN being closed before we receive any data (fix by mleinart). * Try if msvcrt, if we can't load crtdll on Windows (fix by pepijnve). * A fix for nil_on_handled not running the action (reported by Andrew Davey). ### 1.6.1 * Fixed raw_no_echo_mode so that it uses stty -icanon rather than cbreak as cbreak does not appear to be the posixly correct argument. It fails on Solaris if cbreak is used. * Fixed an issue that kept Menu from showing the correct choices for disambiguation. * Removed a circular require that kept Ruby 1.9.2 from loading HighLine. * Fixed a bug that caused infinite looping when wrapping text without spaces. * Fixed it so that :auto paging accounts for the two lines it adds. * On JRuby, improved error message about ffi-ncurses. Before 1.5.3, HighLine was silently swallowing error messages when ffi-ncurses gem was installed without ncurses present on the system. * Reverted Aaron Simmons's patch to allow redirecting STDIN on Windows. This is the only way we could find to restore HighLine's character reading to working order. ### 1.5.2 * Added support for using the ffi-ncurses gem which is supported in JRuby. * Added gem build instructions. ### 1.5.1 * Fixed the long standing echo true bug. (reported by Lauri Tuominen) * Improved Windows API calls to support the redirection of STDIN. (patch by Aaron Simmons) * Updated gem specification to avoid a deprecated call. * Made a minor documentation clarification about character mode support. * Worked around some API changes in Ruby's standard library in Ruby 1.9. (patch by Jake Benilov) ### 1.5.0 * Fixed a bug that would prevent Readline from showing all completions. (reported by Yaohan Chen) * Added the ability to pass a block to HighLine#agree(). (patch by Yaohan Chen) ### 1.4.0 * Made the code grabbing terminal size a little more cross-platform by adding support for Solaris. (patch by Ronald Braswell and Coey Minear) ### 1.2.9 * Additional work on the backspacing issue. (patch by Jeremy Hinegardner) * Fixed Readline prompt bug. (patch by Jeremy Hinegardner) ### 1.2.8 * Fixed backspacing past the prompt and interrupting a prompt bugs. (patch by Jeremy Hinegardner) ### 1.2.7 * Fixed the stty indent bug. * Fixed the echo backspace bug. * Added HighLine::track_eof=() setting to work are threaded eof?() calls. ### 1.2.6 Patch by Jeremy Hinegardner: * Added ColorScheme support. * Added HighLine::Question.overwrite mode. * Various documentation fixes. ### 1.2.5 * Really fixed the bug I tried to fix in 1.2.4. ### 1.2.4 * Fixed a crash causing bug when using menus, reported by Patrick Hof. ### 1.2.3 * Treat Cygwin like a Posix OS, instead of a native Windows environment. ### 1.2.2 * Minor documentation corrections. * Applied Thomas Werschleiln's patch to fix termio buffering on Solaris. * Applied Justin Bailey's patch to allow canceling paged output. * Fixed a documentation bug in the description of character case settings. * Added a notice about termios in HighLine::Question#echo. * Finally working around the infamous "fast typing" bug ### 1.2.1 * Applied Justin Bailey's fix for the page_print() infinite loop bug. * Made a SystemExtensions module to expose OS level functionality other libraries may want to access. * Publicly exposed the get_character() method, per user requests. * Added terminal_size(), output_cols(), and output_rows() methods. * Added :auto setting for warp_at=() and page_at=(). ### 1.2.0 * Improved RubyForge and gem spec project descriptions. * Added basic examples to README. * Added a VERSION constant. * Added support for hidden menu commands. * Added Object.or_ask() when using highline/import. ### 1.0.4 * Moved the HighLine project to Subversion. * HighLine's color escapes can now be disabled. * Fixed EOF bug introduced in the last release. * Updated HighLine web page. * Moved to a forked development/stable version numbering. ### 1.0.2 * Removed old and broken help tests. * Fixed test case typo found by David A. Black. * Added ERb escapes processing to lists, for coloring list items. Color escapes do not add to list element size. * HighLine now throws EOFError when input is exhausted. ### 1.0.1 * Minor bug fix: Moved help initialization to before response building, so help would show up in the default responses. ### 1.0.0 * Fixed documentation typo pointed out by Gavin Kistner. * Added gather = ... option to question for fetching entire Arrays or Hashes filled with answers. You can set +gather+ to a count of answers to collect, a String or Regexp matching the end of input, or a Hash where each key can be used in a new question. * Added File support to HighLine.ask(). You can specify a _directory_ and a _glob_ pattern that combine into a list of file choices the user can select from. You can choose to receive the user's answer as an open filehandle or as a Pathname object. * Added Readline support for history and editing. * Added tab completion for menu and file selection selection (requires Readline). * Added an optional character limit for input. * Added a complete help system to HighLine's shell menu creation tools. ### 0.6.1 * Removed termios dependancy in gem, to fix Windows' install. ### 0.6.0 * Implemented HighLine.choose() for menu handling. * Provided shortcut choose(item1, item2, ...) for simple menus. * Allowed Ruby code to be attached to each menu item, to create a complete menu solution. * Provided for total customization of the menu layout. * Allowed for menu selection by index, name or both. * Added a _shell_ mode to allow menu selection with additional details following the name. * Added a list() utility method that can be invoked just like color(). It can layout Arrays for you in any output in the modes :columns_across, :columns_down, :inline and :rows * Added support for echo = "*" style settings. User code can now choose the echo character this way. * Modified HighLine to user the "termios" library for character input, if available. Will return to old behavior (using "stty"), if "termios" cannot be loaded. * Improved "stty" state restoring code. * Fixed "stty" code to handle interrupt signals. * Improved the default auto-complete error message and exposed this message through the +responses+ interface as :no_completion. ### 0.5.0 * Implemented echo = false for HighLine::Question objects, primarily to make fetching passwords trivial. * Fixed an auto-complete bug that could cause a crash when the user gave an answer that didn't complete to any valid choice. * Implemented +case+ for HighLine::Question objects to provide character case conversions on given answers. Can be set to :up, :down, or :capitalize. * Exposed @answer to the response system, to allow response that are aware of incorrect input. * Implemented +confirm+ for HighLine::Question objects to allow for verification for sensitive user choices. If set to +true+, user will have to answer an "Are you sure? " question. Can also be set to the question to confirm with the user. ### 0.4.0 * Added @wrap_at and @page_at settings and accessors to HighLine, to control text flow. * Implemented line wrapping with adjustable limit. * Implemented paged printing with adjustable limit. ### 0.3.0 * Added support for installing with setup.rb. * All output is now treated as an ERb sequence, allowing Ruby code to be embedded in output strings. * Added support for ANSI color sequences in say(). (And everything else by extension.) * Added whitespace handling for answers. Can be set to :strip, :chomp, :collapse, :strip_and_collapse, :chomp_and_collapse, :remove, or :none. * Exposed question details to ERb completion through @question, to allow for intelligent responses. * Simplified HighLine internals using @question. * Added support for fetching single character input either with getc() or HighLine's own cross-platform terminal input routine. * Improved type conversion to handle user defined classes. ### 0.2.0 / 2005-04-29 * Added Unit Tests to cover an already fixed output bug in the future. * Added Rakefile and setup test action (default). * Renamed HighLine::Answer to HighLine::Question to better illustrate its role. * Renamed fetch_line() to get_response() to better define its goal. * Simplified explain_error in terms of the Question object. * Renamed accept?() to in_range?() to better define purpose. * Reworked valid?() into valid_answer?() to better fit Question object. * Reworked @member into @in, to make it easier to remember and switched implementation to include?(). * Added range checks for @above and @below. * Fixed the bug causing ask() to swallow NoMethodErrors. * Rolled ask_on_error() into responses. * Redirected imports to Kernel from Object. * Added support for validate = lambda { ... }. * Added default answer support. * Fixed bug that caused ask() to die with an empty question. * Added complete documentation. * Improve the implemetation of agree() to be the intended "yes" or "no" only question. * Added Rake tasks for documentation and packaging. * Moved project to RubyForge. ### 0.1.0 * Initial release as the solution to {Ruby Quiz #29}[http://www.rubyquiz.com/quiz29.html]. highline-2.0.3/Gemfile000066400000000000000000000011031355001426500145720ustar00rootroot00000000000000# encoding: utf-8 source "https://rubygems.org" git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } # Specify your gem's dependencies in tgem.gemspec gemspec platform :ruby do # Running only on MRI gem "simplecov", group: :test end group :code_quality do gem "flog", require: false gem "pronto", require: false, platform: :ruby gem "pronto-flay", require: false, platform: :ruby gem "pronto-poper", require: false, platform: :ruby gem "pronto-reek", require: false, platform: :ruby gem "pronto-rubocop", require: false, platform: :ruby end highline-2.0.3/LICENSE000066400000000000000000000005271355001426500143150ustar00rootroot00000000000000= License Terms Distributed under the user's choice of the {GPL Version 2}[http://www.gnu.org/licenses/old-licenses/gpl-2.0.html] (see COPYING for details) or the {Ruby software license}[http://www.ruby-lang.org/en/LICENSE.txt] by James Edward Gray II and Greg Brown. Please email James[mailto:james@grayproductions.net] with any questions. highline-2.0.3/README.md000066400000000000000000000135661355001426500145760ustar00rootroot00000000000000HighLine ======== [![Build Status](https://travis-ci.org/JEG2/highline.svg?branch=master)](https://travis-ci.org/JEG2/highline) [![Build status](https://ci.appveyor.com/api/projects/status/4p05fijpah77d28x/branch/master?svg=true)](https://ci.appveyor.com/project/JEG2/highline/branch/master) [![Gem Version](https://badge.fury.io/rb/highline.svg)](https://badge.fury.io/rb/highline) [![Code Climate](https://codeclimate.com/github/JEG2/highline/badges/gpa.svg)](https://codeclimate.com/github/JEG2/highline) [![Test Coverage](https://codeclimate.com/github/JEG2/highline/badges/coverage.svg)](https://codeclimate.com/github/JEG2/highline/coverage) [![Inline docs](http://inch-ci.org/github/JEG2/highline.svg?branch=master)](http://inch-ci.org/github/JEG2/highline) Description ----------- Welcome to HighLine. HighLine was designed to ease the tedious tasks of doing console input and output with low-level methods like ```gets``` and ```puts```. HighLine provides a robust system for requesting data from a user, without needing to code all the error checking and validation rules and without needing to convert the typed Strings into what your program really needs. Just tell HighLine what you're after, and let it do all the work. Documentation ------------- See: [Rubydoc.info for HighLine](http://www.rubydoc.info/github/JEG2/highline/master). Specially [HighLine](http://www.rubydoc.info/github/JEG2/highline/master/HighLine) and [HighLine::Question](http://www.rubydoc.info/github/JEG2/highline/master/HighLine/Question). Usage ----- ```ruby require 'highline' # Basic usage cli = HighLine.new answer = cli.ask "What do you think?" puts "You have answered: #{answer}" # Default answer cli.ask("Company? ") { |q| q.default = "none" } # Validation cli.ask("Age? ", Integer) { |q| q.in = 0..105 } cli.ask("Name? (last, first) ") { |q| q.validate = /\A\w+, ?\w+\Z/ } # Type conversion for answers: cli.ask("Birthday? ", Date) cli.ask("Interests? (comma sep list) ", lambda { |str| str.split(/,\s*/) }) # Reading passwords: cli.ask("Enter your password: ") { |q| q.echo = false } cli.ask("Enter your password: ") { |q| q.echo = "x" } # ERb based output (with HighLine's ANSI color tools): cli.say("This should be <%= color('bold', BOLD) %>!") # Menus: cli.choose do |menu| menu.prompt = "Please choose your favorite programming language? " menu.choice(:ruby) { cli.say("Good choice!") } menu.choices(:python, :perl) { cli.say("Not from around here, are you?") } menu.default = :ruby end ## Using colored indices on Menus HighLine::Menu.index_color = :rgb_77bbff # set default index color cli.choose do |menu| menu.index_color = :rgb_999999 # override default color of index # you can also use constants like :blue menu.prompt = "Please choose your favorite programming language? " menu.choice(:ruby) { cli.say("Good choice!") } menu.choices(:python, :perl) { cli.say("Not from around here, are you?") } end ``` If you want to save some characters, you can inject/import HighLine methods on Kernel by doing the following. Just be sure to avoid name collisions in the top-level namespace. ```ruby require 'highline/import' say "Now you can use #say directly" ``` For more examples see the examples/ directory of this project. Requirements ------------ HighLine from version >= 1.7.0 requires ruby >= 1.9.3 Installing ---------- To install HighLine, use the following command: ```sh $ gem install highline ``` (Add `sudo` if you're installing under a POSIX system as root) If you're using [Bundler](http://bundler.io/), add this to your Gemfile: ```ruby source "https://rubygems.org" gem 'highline' ``` And then run: ```sh $ bundle ``` If you want to build the gem locally, use the following command from the root of the sources: ```sh $ rake package ``` You can also build and install directly: ```sh $ rake install ``` Contributing ------------ 1. Open an issue - https://github.com/JEG2/highline/issues 2. Fork the repository - https://github.com/JEG2/highline/fork 3. Clone it locally - ```git clone git@github.com:YOUR-USERNAME/highline.git``` 4. Add the main HighLine repository as the __upstream__ remote - ```cd highline``` # to enter the cloned repository directory. - ```git remote add upstream https://github.com/JEG2/highline``` 5. Keep your fork in sync with __upstream__ - ```git fetch upstream``` - ```git checkout master``` - ```git merge upstream/master``` 6. Create your feature branch - ```git checkout -b your_branch``` 7. Hack the source code, run the tests and __pronto__ - ```rake test``` - ```rake acceptance``` - ```pronto run``` 8. Commit your changes - ```git commit -am "Your commit message"``` 9. Push it - ```git push``` 10. Open a pull request - https://github.com/JEG2/highline/pulls Details on: * GitHub Guide to Contributing to Open Source - https://guides.github.com/activities/contributing-to-open-source/ * GitHub issues - https://guides.github.com/features/issues/ * Forking - https://help.github.com/articles/fork-a-repo/ * Cloning - https://help.github.com/articles/cloning-a-repository/ * Adding upstream - https://help.github.com/articles/configuring-a-remote-for-a-fork/ * Syncing your fork - https://help.github.com/articles/syncing-a-fork/ * Branching - https://git-scm.com/book/en/v2/Git-Branching-Basic-Branching-and-Merging * Commiting - https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository * Pushing - https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes The Core HighLine Team ---------------------- * [James Edward Gray II](https://github.com/JEG2) - Author * [Gregory Brown](https://github.com/practicingruby) - Core contributor * [Abinoam P. Marques Jr.](https://github.com/abinoam) - Core contributor _For a list of people who have contributed to the codebase, see [GitHub's list of contributors](https://github.com/JEG2/highline/contributors)._ highline-2.0.3/Rakefile000066400000000000000000000005561355001426500147570ustar00rootroot00000000000000# encoding: utf-8 require "rake/testtask" require "bundler/gem_tasks" task default: [:test] Rake::TestTask.new do |test| test.libs = %w[lib test] test.verbose = true test.warning = true test.test_files = FileList["test/test*.rb"] end desc "Run some interactive acceptance tests" task :acceptance do load "test/acceptance/acceptance.rb" end highline-2.0.3/TODO000066400000000000000000000001661355001426500137770ustar00rootroot00000000000000= To Do List The following is a list of planned expansions for HighLine, in no particular order. * Rent this space. highline-2.0.3/appveyor.yml000066400000000000000000000012521355001426500156740ustar00rootroot00000000000000version: 2.0.{build}-{branch} cache: - vendor/bundle environment: matrix: - RUBY_VERSION: "193" - RUBY_VERSION: "200" - RUBY_VERSION: "200-x64" - RUBY_VERSION: "21" - RUBY_VERSION: "21-x64" - RUBY_VERSION: "22" - RUBY_VERSION: "22-x64" - RUBY_VERSION: "23" - RUBY_VERSION: "23-x64" - RUBY_VERSION: "24" - RUBY_VERSION: "24-x64" matrix: allow_failures: - RUBY_VERSION: "193" install: - set PATH=C:\Ruby%RUBY_VERSION%\bin;%PATH% - bundle config --local path vendor/bundle - bundle install --retry=3 --without code_quality build: off before_test: - ruby -v - gem -v - bundle -v test_script: - bundle exec rake highline-2.0.3/doc/000077500000000000000000000000001355001426500140515ustar00rootroot00000000000000highline-2.0.3/doc/.cvsignore000066400000000000000000000000051355001426500160440ustar00rootroot00000000000000html highline-2.0.3/examples/000077500000000000000000000000001355001426500151225ustar00rootroot00000000000000highline-2.0.3/examples/ansi_colors.rb000066400000000000000000000017221355001426500177640ustar00rootroot00000000000000#!/usr/bin/env ruby # encoding: utf-8 # ansi_colors.rb # # Created by James Edward Gray II on 2005-05-03. # Copyright 2005 Gray Productions. All rights reserved. require "rubygems" require "highline/import" # Supported color sequences. colors = %w[black red green yellow blue magenta cyan white] # Using color() with symbols. colors.each_with_index do |c, i| say("This should be <%= color('#{c}', :#{c}) %>!") say("This should be <%= color('#{colors[i - 1]} on #{c}', \ :#{colors[i - 1]}, :on_#{c} ) %>!") end # Using color with constants. say("This should be <%= color('bold', BOLD) %>!") say("This should be <%= color('underlined', UNDERLINE) %>!") # Using constants only. say("This might even <%= BLINK %>blink<%= CLEAR %>!") # It even works with list wrapping. erb_digits = %w[Zero One Two Three Four] + ["<%= color('Five', :blue) %%>"] + %w[Six Seven Eight Nine] say("<%= list(#{erb_digits.inspect}, :columns_down, 3) %>") highline-2.0.3/examples/asking_for_arrays.rb000066400000000000000000000007201355001426500211510ustar00rootroot00000000000000#!/usr/bin/env ruby # encoding: utf-8 # asking_for_arrays.rb # # Created by James Edward Gray II on 2005-07-05. # Copyright 2005 Gray Productions. All rights reserved. require "rubygems" require "highline/import" require "pp" puts "Using: #{HighLine.default_instance.class}" puts grades = ask("Enter test scores (or a blank line to quit):", ->(ans) { ans =~ /^-?\d+$/ ? Integer(ans) : ans }) do |q| q.gather = "" end say("Grades:") pp grades highline-2.0.3/examples/basic_usage.rb000066400000000000000000000044461355001426500177240ustar00rootroot00000000000000#!/usr/bin/env ruby # encoding: utf-8 # basic_usage.rb # # Created by James Edward Gray II on 2005-04-28. # Copyright 2005 Gray Productions. All rights reserved. require "rubygems" require "highline/import" require "yaml" puts "Using: #{HighLine.default_instance.terminal.class}" puts contacts = [] # Just define a parse class method and use the class # as a parser for HighLine#ask # class NameClass def self.parse(string) raise ArgumentError, "Invalid name format." unless string =~ /^\s*(\w+),\s*(\w+)\s*$/ new(Regexp.last_match(2), Regexp.last_match(1)) end def initialize(first, last) @first = first @last = last end attr_reader :first, :last end loop do entry = {} # basic output say("Enter a contact:") # basic input entry[:name] = ask("Name? (last, first) ", NameClass) do |q| q.validate = /\A\w+, ?\w+\Z/ end entry[:company] = ask("Company? ") { |q| q.default = "none" } entry[:address] = ask("Address? ") entry[:city] = ask("City? ") entry[:state] = ask("State? ") do |q| q.case = :up q.validate = /\A[A-Z]{2}\Z/ end entry[:zip] = ask("Zip? ") do |q| q.validate = /\A\d{5}(?:-?\d{4})?\Z/ end entry[:phone] = ask("Phone? ", lambda { |p| p.delete("^0-9"). sub(/\A(\d{3})/, '(\1) '). sub(/(\d{4})\Z/, '-\1') }) do |q| q.validate = ->(p) { p.delete("^0-9").length == 10 } q.responses[:not_valid] = "Enter a phone numer with area code." end entry[:age] = ask("Age? ", Integer) { |q| q.in = 0..105 } entry[:birthday] = ask("Birthday? ", Date) entry[:interests] = ask("Interests? (comma separated list) ", ->(str) { str.split(/,\s*/) }) entry[:description] = ask("Enter a description for this contact.") do |q| q.whitespace = :strip_and_collapse end contacts << entry # shortcut for yes and no questions break unless agree("Enter another contact? ", true) end if agree("Save these contacts? ", true) file_name = ask("Enter a file name: ") do |q| q.validate = /\A\w+\Z/ q.confirm = true end File.open("#{file_name}.yaml", "w") { |file| YAML.dump(contacts, file) } end highline-2.0.3/examples/color_scheme.rb000066400000000000000000000014761355001426500201210ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: utf-8 # color_scheme.rb # # Created by Jeremy Hinegardner on 2007-01-24 # Copyright 2007 Jeremy Hinegardner. All rights reserved require "rubygems" require "highline/import" # Create a color scheme, naming color patterns with symbol names. ft = HighLine::ColorScheme.new do |cs| cs[:headline] = [:bold, :yellow, :on_black] cs[:horizontal_line] = [:bold, :white, :on_blue] cs[:even_row] = [:green] cs[:odd_row] = [:magenta] end # Assign that color scheme to HighLine... HighLine.color_scheme = ft # ...and use it. say("<%= color('Headline', :headline) %>") say("<%= color('-'*20, :horizontal_line) %>") # Setup a toggle for rows. i = true ("A".."D").each do |row| row_color = i ? :even_row : :odd_row say("<%= color('#{row}', '#{row_color}') %>") i = !i end highline-2.0.3/examples/get_character.rb000066400000000000000000000005031355001426500202400ustar00rootroot00000000000000#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "highline/import" puts "Using: #{HighLine.default_instance.terminal.class}" puts choices = "ynaq" answer = ask("Your choice [#{choices}]? ") do |q| q.echo = false q.character = true q.validate = /\A[#{choices}]\Z/ end say("Your choice: #{answer}") highline-2.0.3/examples/limit.rb000066400000000000000000000005551355001426500165720ustar00rootroot00000000000000#!/usr/bin/env ruby -w # encoding: utf-8 # limit.rb # # Created by James Edward Gray II on 2008-11-12. # Copyright 2008 Gray Productions. All rights reserved. require "rubygems" require "highline/import" puts "Using: #{HighLine.default_instance.terminal.class}" puts text = ask("Enter text (max 10 chars): ") { |q| q.limit = 10 } puts "You entered: #{text}!" highline-2.0.3/examples/menus.rb000066400000000000000000000034471355001426500166060ustar00rootroot00000000000000#!/usr/bin/env ruby # encoding: utf-8 require "rubygems" require "highline/import" puts "Using: #{HighLine.default_instance.terminal.class}" puts # The old way, using ask() and say()... choices = %w[ruby python perl] say("This is the old way using ask() and say()...") say("Please choose your favorite programming language:") say(choices.map { |c| " #{c}\n" }.join) case ask("? ", choices) when "ruby" say("Good choice!") else say("Not from around here, are you?") end # The new and improved choose()... say("\nThis is the new mode (default)...") choose do |menu| menu.prompt = "Please choose your favorite programming language? " menu.choice(:ruby) { say("Good choice!") } menu.choices(:python, :perl) { say("Not from around here, are you?") } menu.default = :ruby end say("\nThis is letter indexing...") choose do |menu| menu.index = :letter # Use :capital_letter for uppercase menu.index_suffix = ") " menu.prompt = "Please choose your favorite programming language? " menu.choice(:ruby) { say("Good choice!") } menu.choices(:python, :perl) { say("Not from around here, are you?") } end say("\nThis is with a different layout...") choose do |menu| menu.layout = :one_line menu.header = "Languages" menu.prompt = "Favorite? " menu.choice(:ruby) { say("Good choice!") } menu.choices(:python, :perl) { say("Not from around here, are you?") } end say("\nYou can even build shells...") loop do choose do |menu| menu.layout = :menu_only menu.shell = true menu.choice(:load, "Load a file.") do |_command, details| say("Loading file with options: #{details}...") end menu.choice(:save, "Save a file.") do |_command, details| say("Saving file with options: #{details}...") end menu.choice(:quit, "Exit program.") { exit } end end highline-2.0.3/examples/overwrite.rb000066400000000000000000000011511355001426500174730ustar00rootroot00000000000000#!/usr/bin/env ruby # encoding: utf-8 # overwrite.rb # # Created by Jeremy Hinegardner on 2007-01-24 # Copyright 2007 Jeremy Hinegardner. All rights reserved require "rubygems" require "highline/import" puts "Using: #{HighLine.default_instance.terminal.class}" puts prompt = "here is your password:" ask( "#{prompt} <%= color('mypassword', RED, BOLD) %> (Press Any Key to blank) " ) do |q| q.overwrite = true q.echo = false # overwrite works best when echo is false. q.character = true # if this is set to :getc then overwrite does not work end say("<%= color('Look! blanked out!', GREEN) %>") highline-2.0.3/examples/page_and_wrap.rb000066400000000000000000000666731355001426500202600ustar00rootroot00000000000000#!/usr/bin/env ruby # encoding: utf-8 # page_and_wrap.rb # # Created by James Edward Gray II on 2005-05-07. # Copyright 2005 Gray Productions. All rights reserved. require "rubygems" require "highline/import" HighLine.default_instance.wrap_at = 80 HighLine.default_instance.page_at = 22 say(<: ") do |q| q.echo = "*" q.verify_match = true q.gather = { "Enter a password" => "", "Please type it again for verification" => "" } end puts "Your password is now #{pass}!" highline-2.0.3/examples/trapping_eof.rb000066400000000000000000000006571355001426500201340ustar00rootroot00000000000000#!/usr/bin/env ruby # encoding: utf-8 # trapping_eof.rb # # Created by James Edward Gray II on 2006-02-20. # Copyright 2006 Gray Productions. All rights reserved. require "rubygems" require "highline/import" loop do begin name = ask("What's your name?") break if name == "exit" puts "Hello, #{name}!" rescue EOFError # HighLine throws this if @input.eof? break end end puts "Goodbye, dear friend." exit highline-2.0.3/examples/using_readline.rb000066400000000000000000000006011355001426500204340ustar00rootroot00000000000000#!/usr/bin/env ruby # encoding: utf-8 # using_readline.rb # # Created by James Edward Gray II on 2005-07-06. # Copyright 2005 Gray Productions. All rights reserved. require "rubygems" require "highline/import" loop do cmd = ask("Enter command: ", %w[save sample load reset quit]) do |q| q.readline = true end say("Executing \"#{cmd}\"...") break if cmd == "quit" end highline-2.0.3/highline.gemspec000066400000000000000000000022371355001426500164440ustar00rootroot00000000000000# coding: utf-8 lib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "highline/version" Gem::Specification.new do |spec| spec.name = "highline" spec.version = HighLine::VERSION spec.author = "James Edward Gray II" spec.email = "james@graysoftinc.com" spec.summary = "HighLine is a high-level command-line IO library." spec.description = <(yn) { yn.downcase[0] == "y" }) do |q| q.validate = /\A(?:y(?:es)?|no?)\Z/i q.responses[:not_valid] = 'Please enter "yes" or "no".' q.responses[:ask_on_error] = :question q.character = character q.completion = %w[yes no] yield q if block_given? end end # # This method is the primary interface for user input. Just provide a # _question_ to ask the user, the _answer_type_ you want returned, and # optionally a code block setting up details of how you want the question # handled. See {#say} for details on the format of _question_, and # {Question} for more information about _answer_type_ and what's # valid in the code block. # # Raises EOFError if input is exhausted. # # @param (see Question.build) # @return answer converted to the class in answer_type def ask(template_or_question, answer_type = nil, &details) question = Question.build(template_or_question, answer_type, &details) if question.gather QuestionAsker.new(question, self).gather_answers else QuestionAsker.new(question, self).ask_once end end # # This method is HighLine's menu handler. For simple usage, you can just # pass all the menu items you wish to display. At that point, choose() will # build and display a menu, walk the user through selection, and return # their choice among the provided items. You might use this in a case # statement for quick and dirty menus. # # However, choose() is capable of much more. If provided, a block will be # passed a HighLine::Menu object to configure. Using this method, you can # customize all the details of menu handling from index display, to building # a complete shell-like menuing system. See HighLine::Menu for all the # methods it responds to. # # Raises EOFError if input is exhausted. # # @param items [Array] # @param details [Proc] to be passed to Menu.new # @return [String] answer def choose(*items, &details) menu = Menu.new(&details) menu.choices(*items) unless items.empty? # Set auto-completion menu.completion = menu.options # Set _answer_type_ so we can double as the Question for ask(). # menu.option = normal menu selection, by index or name menu.answer_type = menu.shell ? shell_style_lambda(menu) : menu.options selected = ask(menu) return unless selected if menu.shell if menu.gather selection = [] details = [] selected.each do |value| selection << value[0] details << value[1] end else selection, details = selected end else selection = selected end if menu.gather menu.gather_selected(self, selection, details) else menu.select(self, selection, details) end end # Convenience method to craft a lambda suitable for # beind used in autocompletion operations by {#choose} # @return [lambda] lambda to be used in autocompletion operations def shell_style_lambda(menu) lambda do |command| # shell-style selection first_word = command.to_s.split.first || "" options = menu.options options.extend(OptionParser::Completion) answer = options.complete(first_word) raise Question::NoAutoCompleteMatch unless answer [answer.last, command.sub(/^\s*#{first_word}\s*/, "")] end end # # This method provides easy access to ANSI color sequences, without the user # needing to remember to CLEAR at the end of each sequence. Just pass the # _string_ to color, followed by a list of _colors_ you would like it to be # affected by. The _colors_ can be HighLine class constants, or symbols # (:blue for BLUE, for example). A CLEAR will automatically be embedded to # the end of the returned String. # # This method returns the original _string_ unchanged if use_color? # is +false+. # # @param string [String] string to be colored # @param colors [Array] array of colors like [:red, :blue] # @return [String] (ANSI escaped) colored string # @example # cli = HighLine.new # cli.color("Sustainable", :green, :bold) # # => "\e[32m\e[1mSustainable\e[0m" # # # As class method (delegating to HighLine.default_instance) # HighLine.color("Sustainable", :green, :bold) # def color(string, *colors) return string unless use_color? HighLine.Style(*colors).color(string) end # In case you just want the color code, without the embedding and # the CLEAR sequence. # # @param colors [Array] # @return [String] ANSI escape code for the given colors. # # @example # s = HighLine.Style(:red, :blue) # s.code # => "\e[31m\e[34m" # # HighLine.color_code(:red, :blue) # => "\e[31m\e[34m" # # cli = HighLine.new # cli.color_code(:red, :blue) # => "\e[31m\e[34m" # def color_code(*colors) HighLine.Style(*colors).code end # Remove color codes from a string. # @param string [String] to be decolorized # @return [String] without the ANSI escape sequence (colors) def uncolor(string) Style.uncolor(string) end # Renders a list of itens using a {ListRenderer} # @param items [Array] # @param mode [Symbol] # @param option # @return [String] # @see ListRenderer#initialize ListRenderer#initialize for parameter details def list(items, mode = :rows, option = nil) ListRenderer.new(items, mode, option, self).render end # # The basic output method for HighLine objects. If the provided _statement_ # ends with a space or tab character, a newline will not be appended (output # will be flush()ed). All other cases are passed straight to Kernel.puts(). # # The _statement_ argument is processed as an ERb template, supporting # embedded Ruby code. The template is evaluated within a HighLine # instance's binding for providing easy access to the ANSI color constants # and the HighLine#color() method. # # @param statement [Statement, String] what to be said def say(statement) statement = render_statement(statement) return if statement.empty? statement = (indentation + statement) # Don't add a newline if statement ends with whitespace, OR # if statement ends with whitespace before a color escape code. if /[ \t](\e\[\d+(;\d+)*m)?\Z/ =~ statement output.print(statement) output.flush else output.puts(statement) end end # Renders a statement using {HighLine::Statement} # @param statement [String] any string # @return [Statement] rendered statement def render_statement(statement) Statement.new(statement, self).to_s end # # Set to an integer value to cause HighLine to wrap output lines at the # indicated character limit. When +nil+, the default, no wrapping occurs. If # set to :auto, HighLine will attempt to determine the columns # available for the @output or use a sensible default. # def wrap_at=(setting) @wrap_at = setting == :auto ? output_cols : setting end # # Set to an integer value to cause HighLine to page output lines over the # indicated line limit. When +nil+, the default, no paging occurs. If # set to :auto, HighLine will attempt to determine the rows available # for the @output or use a sensible default. # def page_at=(setting) @page_at = setting == :auto ? output_rows - 2 : setting end # # Outputs indentation with current settings # def indentation " " * @indent_size * @indent_level end # # Executes block or outputs statement with indentation # # @param increase [Integer] how much to increase indentation # @param statement [Statement, String] to be said # @param multiline [Boolean] # @return [void] # @see #multi_indent def indent(increase = 1, statement = nil, multiline = nil) @indent_level += increase multi = @multi_indent @multi_indent ||= multiline begin if block_given? yield self else say(statement) end ensure @multi_indent = multi @indent_level -= increase end end # # Outputs newline # def newline @output.puts end # # Returns the number of columns for the console, or a default it they cannot # be determined. # def output_cols return 80 unless @output.tty? terminal.terminal_size.first rescue NoMethodError return 80 end # # Returns the number of rows for the console, or a default if they cannot be # determined. # def output_rows return 24 unless @output.tty? terminal.terminal_size.last rescue NoMethodError return 24 end # Call #puts on the HighLine's output stream # @param args [String] same args for Kernel#puts def puts(*args) @output.puts(*args) end # # Creates a new HighLine instance with the same options # def new_scope self.class.new(@input, @output, @wrap_at, @page_at, @indent_size, @indent_level) end private # Adds a layer of scope (new_scope) to ask a question inside a # question, without destroying instance data def confirm(question) new_scope.agree(question.confirm_question(self)) end # # A helper method used by HighLine::Question.verify_match # for finding whether a list of answers match or differ # from each other. # def unique_answers(list) (list.respond_to?(:values) ? list.values : list).uniq end def last_answer(answers) answers.respond_to?(:values) ? answers.values.last : answers.last end # Get response one line at time # @param question [Question] # @return [String] response def get_response_line_mode(question) if question.echo == true && !question.limit get_line(question) else get_line_raw_no_echo_mode(question) end end # # Read a line of input from the input stream and process whitespace as # requested by the Question object. # # If Question's _readline_ property is set, that library will be used to # fetch input. *WARNING*: This ignores the currently set input stream. # # Raises EOFError if input is exhausted. # def get_line(question) terminal.get_line(question, self) end def get_line_raw_no_echo_mode(question) line = "" terminal.raw_no_echo_mode_exec do loop do character = terminal.get_character break unless character break if ["\n", "\r"].include? character # honor backspace and delete if character == "\b" || character == "\u007F" chopped = line.chop! output_erase_char if chopped && question.echo elsif character == "\e" ignore_arrow_key else line << character say_last_char_or_echo_char(line, question) end @output.flush break if line_overflow_for_question?(line, question) end end say_new_line_or_overwrite(question) question.format_answer(line) end def say_new_line_or_overwrite(question) if question.overwrite @output.print("\r#{HighLine.Style(:erase_line).code}") @output.flush else say("\n") end end def ignore_arrow_key 2.times do terminal.get_character end end def say_last_char_or_echo_char(line, question) @output.print(line[-1]) if question.echo == true @output.print(question.echo) if question.echo && question.echo != true end def line_overflow_for_question?(line, question) question.limit && line.size == question.limit end def output_erase_char @output.print("\b#{HighLine.Style(:erase_char).code}") end # Get response using #getc # @param question [Question] # @return [String] response def get_response_getc_mode(question) terminal.raw_no_echo_mode_exec do response = @input.getc question.format_answer(response) end end # Get response each character per turn # @param question [Question] # @return [String] response def get_response_character_mode(question) terminal.raw_no_echo_mode_exec do response = terminal.get_character if question.overwrite erase_current_line else echo = question.get_echo_for_response(response) say("#{echo}\n") end question.format_answer(response) end end def erase_current_line @output.print("\r#{HighLine.Style(:erase_line).code}") @output.flush end public :get_response_character_mode, :get_response_line_mode public :get_response_getc_mode def actual_length(text) Wrapper.actual_length text end # Check to see if there's already a HighLine.default_instance or if # this is the first time the method is called (eg: at # HighLine.default_instance initialization). # If there's already one, copy use_color settings. # This is here most to help migrate code from HighLine 1.7.x to 2.0.x # # @return [Boolean] def default_use_color if HighLine.default_instance HighLine.default_instance.use_color else true end end end HighLine.default_instance = HighLine.new require "highline/string" highline-2.0.3/lib/highline/000077500000000000000000000000001355001426500156415ustar00rootroot00000000000000highline-2.0.3/lib/highline/builtin_styles.rb000066400000000000000000000077621355001426500212530ustar00rootroot00000000000000# coding: utf-8 class HighLine # Builtin Styles that are included at HighLine initialization. # It has the basic styles like :bold and :underline. module BuiltinStyles # Included callback # @param base [Class, Module] base class def self.included(base) base.extend ClassMethods end # Basic styles' ANSI escape codes like :bold => "\e[1m" STYLE_LIST = { erase_line: "\e[K", erase_char: "\e[P", clear: "\e[0m", reset: "\e[0m", bold: "\e[1m", dark: "\e[2m", underline: "\e[4m", underscore: "\e[4m", blink: "\e[5m", reverse: "\e[7m", concealed: "\e[8m" }.freeze STYLE_LIST.each do |style_name, code| style = String(style_name).upcase const_set style, code const_set style + "_STYLE", Style.new(name: style_name, code: code, builtin: true) end # Basic Style names like CLEAR, BOLD, UNDERLINE STYLES = %w[CLEAR RESET BOLD DARK UNDERLINE UNDERSCORE BLINK REVERSE CONCEALED].freeze # A Hash with the basic colors an their ANSI escape codes. COLOR_LIST = { black: { code: "\e[30m", rgb: [0, 0, 0] }, red: { code: "\e[31m", rgb: [128, 0, 0] }, green: { code: "\e[32m", rgb: [0, 128, 0] }, blue: { code: "\e[34m", rgb: [0, 0, 128] }, yellow: { code: "\e[33m", rgb: [128, 128, 0] }, magenta: { code: "\e[35m", rgb: [128, 0, 128] }, cyan: { code: "\e[36m", rgb: [0, 128, 128] }, white: { code: "\e[37m", rgb: [192, 192, 192] }, gray: { code: "\e[37m", rgb: [192, 192, 192] }, grey: { code: "\e[37m", rgb: [192, 192, 192] }, none: { code: "\e[38m", rgb: [0, 0, 0] } }.freeze COLOR_LIST.each do |color_name, attributes| color = String(color_name).upcase style = Style.new( name: color_name, code: attributes[:code], rgb: attributes[:rgb], builtin: true ) const_set color + "_STYLE", style end # The builtin styles basic colors like black, red, green. BASIC_COLORS = %w[BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE GRAY GREY NONE].freeze colors = BASIC_COLORS.dup BASIC_COLORS.each do |color| bright_color = "BRIGHT_#{color}" colors << bright_color const_set bright_color + "_STYLE", const_get(color + "_STYLE").bright light_color = "LIGHT_#{color}" colors << light_color const_set light_color + "_STYLE", const_get(color + "_STYLE").light end # The builtin styles' colors like LIGHT_RED and BRIGHT_BLUE. COLORS = colors colors.each do |color| const_set color, const_get("#{color}_STYLE").code const_set "ON_#{color}_STYLE", const_get("#{color}_STYLE").on const_set "ON_#{color}", const_get("ON_#{color}_STYLE").code end ON_NONE_STYLE.rgb = [255, 255, 255] # Override; white background # BuiltinStyles class methods to be extended. module ClassMethods # Regexp to match against RGB style constant names. RGB_COLOR_PATTERN = /^(ON_)?(RGB_)([A-F0-9]{6})(_STYLE)?$/ # const_missing callback for automatically respond to # builtin constants (without explicitly defining them) # @param name [Symbol] missing constant name def const_missing(name) raise NameError, "Bad color or uninitialized constant #{name}" unless name.to_s =~ RGB_COLOR_PATTERN on = Regexp.last_match(1) suffix = Regexp.last_match(4) code_name = if suffix Regexp.last_match(1).to_s + Regexp.last_match(2) + Regexp.last_match(3) else name.to_s end style_name = code_name + "_STYLE" style = Style.rgb(Regexp.last_match(3)) style = style.on if on const_set(style_name, style) const_set(code_name, style.code) suffix ? style : style.code end end end end highline-2.0.3/lib/highline/color_scheme.rb000066400000000000000000000101741355001426500206330ustar00rootroot00000000000000# coding: utf-8 #-- # color_scheme.rb # # Created by Jeremy Hinegardner on 2007-01-24 # Copyright 2007. All rights reserved # # This is Free Software. See LICENSE and COPYING for details class HighLine # # ColorScheme objects encapsulate a named set of colors to be used in the # {HighLine.color} method call. For example, by applying a ColorScheme that # has a :warning color then the following could be used: # # color("This is a warning", :warning) # # A ColorScheme contains named sets of HighLine color constants. # # @example Instantiating a color scheme, applying it to HighLine, # and using it: # ft = HighLine::ColorScheme.new do |cs| # cs[:headline] = [ :bold, :yellow, :on_black ] # cs[:horizontal_line] = [ :bold, :white ] # cs[:even_row] = [ :green ] # cs[:odd_row] = [ :magenta ] # end # # HighLine.color_scheme = ft # say("<%= color('Headline', :headline) %>") # say("<%= color('-'*20, :horizontal_line) %>") # i = true # ("A".."D").each do |row| # if i then # say("<%= color('#{row}', :even_row ) %>") # else # say("<%= color('#{row}', :odd_row) %>") # end # i = !i # end # # class ColorScheme # # Create an instance of HighLine::ColorScheme. The customization can # happen as a passed in Hash or via the yielded block. Keys are # converted to :symbols and values are converted to HighLine # constants. # # @param h [Hash] def initialize(h = nil) @scheme = {} load_from_hash(h) if h yield self if block_given? end # Load multiple colors from key/value pairs. # @param h [Hash] def load_from_hash(h) h.each_pair do |color_tag, constants| self[color_tag] = constants end end # Does this color scheme include the given tag name? # @param color_tag [#to_sym] # @return [Boolean] def include?(color_tag) @scheme.keys.include?(to_symbol(color_tag)) end # Allow the scheme to be accessed like a Hash. # @param color_tag [#to_sym] # @return [Style] def [](color_tag) @scheme[to_symbol(color_tag)] end # Retrieve the original form of the scheme # @param color_tag [#to_sym] def definition(color_tag) style = @scheme[to_symbol(color_tag)] style && style.list end # Retrieve the keys in the scheme # @return [Array] of keys def keys @scheme.keys end # Allow the scheme to be set like a Hash. # @param color_tag [#to_sym] # @param constants [Array] Array of Style symbols def []=(color_tag, constants) @scheme[to_symbol(color_tag)] = HighLine::Style.new(name: color_tag.to_s.downcase.to_sym, list: constants, no_index: true) end # Retrieve the color scheme hash (in original definition format) # @return [Hash] scheme as Hash. It may be reused in a new ColorScheme. def to_hash @scheme.each_with_object({}) do |pair, hsh| key, value = pair hsh[key] = value.list end end private # Return a normalized representation of a color name. def to_symbol(t) t.to_s.downcase end # Return a normalized representation of a color setting. def to_constant(v) v = v.to_s if v.is_a?(Symbol) if v.is_a?(::String) HighLine.const_get(v.upcase) else v end end end # A sample ColorScheme. class SampleColorScheme < ColorScheme SAMPLE_SCHEME = { critical: [:yellow, :on_red], error: [:bold, :red], warning: [:bold, :yellow], notice: [:bold, :magenta], info: [:bold, :cyan], debug: [:bold, :green], row_even: [:cyan], row_odd: [:magenta] }.freeze # # Builds the sample scheme with settings for :critical, # :error, :warning, :notice, :info, # :debug, :row_even, and :row_odd colors. # def initialize(_h = nil) super(SAMPLE_SCHEME) end end end highline-2.0.3/lib/highline/compatibility.rb000066400000000000000000000011411355001426500210340ustar00rootroot00000000000000# coding: utf-8 unless STDIN.respond_to? :getbyte # HighLine adds #getbyte alias to #getc when #getbyte is not available. class IO # alias to #getc when #getbyte is not available alias getbyte getc end # HighLine adds #getbyte alias to #getc when #getbyte is not available. class StringIO # alias to #getc when #getbyte is not available alias getbyte getc end end unless "".respond_to? :each_line # HighLine adds #each_line alias to #each when each_line is not available. class String # alias to #each when each_line is not available. alias each_line each end end highline-2.0.3/lib/highline/custom_errors.rb000066400000000000000000000025711355001426500211010ustar00rootroot00000000000000# encoding: utf-8 class HighLine # Internal HighLine errors. module CustomErrors # An error that responds to :explanation_key class ExplainableError < StandardError # Explanation key as Symbol or nil. Used to # select the proper error message to be displayed. # @return [nil, Symbol] explanation key to get the # proper error message. def explanation_key nil end end # Bare Question error class QuestionError < ExplainableError # (see ExplainableError#explanation_key) def explanation_key nil end end # Invalid Question error class NotValidQuestionError < ExplainableError # (see ExplainableError#explanation_key) def explanation_key :not_valid end end # Out of Range Question error class NotInRangeQuestionError < ExplainableError # (see ExplainableError#explanation_key) def explanation_key :not_in_range end end # Unconfirmed Question error class NoConfirmationQuestionError < ExplainableError # (see ExplainableError#explanation_key) def explanation_key nil end end # Unavailable auto complete error class NoAutoCompleteMatch < ExplainableError # (see ExplainableError#explanation_key) def explanation_key :no_completion end end end end highline-2.0.3/lib/highline/import.rb000066400000000000000000000026541355001426500175070ustar00rootroot00000000000000# coding: utf-8 # import.rb # # Created by James Edward Gray II on 2005-04-26. # Copyright 2005 Gray Productions. All rights reserved. # # This is Free Software. See LICENSE and COPYING for details. require "highline" require "forwardable" # # require "highline/import" adds shortcut methods to Kernel, making # {HighLine#agree}, {HighLine#ask}, {HighLine#choose} and {HighLine#say} # globally available. This is handy for # quick and dirty input and output. These methods use HighLine.default_instance # which is initialized to use $stdin and $stdout (you are free # to change this). # Otherwise, these methods are identical to their {HighLine} counterparts, # see that class for detailed explanations. # module Kernel extend Forwardable def_instance_delegators :HighLine, :agree, :ask, :choose, :say end # When requiring 'highline/import' HighLine adds {#or_ask} to Object so # it is globally available. class Object # # Tries this object as a _first_answer_ for a HighLine::Question. See that # attribute for details. # # *Warning*: This Object will be passed to String() before set. # # @param args [Array<#to_s>] # @param details [lambda] block to be called with the question # instance as argument. # @return [String] answer def or_ask(*args, &details) ask(*args) do |question| question.first_answer = String(self) yield(question) if details end end end highline-2.0.3/lib/highline/io_console_compatible.rb000066400000000000000000000010371355001426500225170ustar00rootroot00000000000000# coding: utf-8 require "stringio" require "tempfile" # # On tests, we try to simulate input output with # StringIO, Tempfile and File objects. # # For this to be accomplished, we have to do some # tweaking so that they respond adequately to the # called methods during tests. # module IOConsoleCompatible def getch getc end attr_accessor :echo def winsize [24, 80] end end class Tempfile include IOConsoleCompatible end class File include IOConsoleCompatible end class StringIO include IOConsoleCompatible end highline-2.0.3/lib/highline/list.rb000066400000000000000000000104251355001426500171430ustar00rootroot00000000000000# coding: utf-8 class HighLine # List class with some convenience methods like {#col_down}. class List # Original given *items* argument. # It's frozen at initialization time and # all later transformations will happen on {#list}. # @return [Array] attr_reader :items # Number of columns for each list row. # @return [Integer] attr_reader :cols # Columns turn into rows in transpose mode. # @return [Boolean] # # @example A two columns array like this: # [ [ "a", "b" ], # [ "c", "d" ], # [ "e", "f" ], # [ "g", "h" ], # [ "i", "j" ] ] # # @example When in transpose mode will be like this: # [ [ "a", "c", "e", "g", "i" ], # [ "b", "d", "f", "h", "j" ] ] # # @see #col_down_mode attr_reader :transpose_mode # Content are distributed first by column in col down mode. # @return [Boolean] # # @example A two columns array like this: # [ [ "a", "b" ], # [ "c", "d" ], # [ "e", "f" ], # [ "g", "h" ], # [ "i", "j" ] ] # # @example In col down mode will be like this: # [ [ "a", "f"], # [ "b", "g"], # [ "c", "h"], # [ "d", "i"], # [ "e", "j"] ] # # @see #transpose_mode attr_reader :col_down_mode # @param items [#to_a] an array of items to compose the list. # @param options [Hash] a hash of options to tailor the list. # @option options [Boolean] :transpose (false) set {#transpose_mode}. # @option options [Boolean] :col_down (false) set {#col_down_mode}. # @option options [Integer] :cols (1) set {#cols}. def initialize(items, options = {}) @items = items.to_a.dup.freeze @transpose_mode = options.fetch(:transpose) { false } @col_down_mode = options.fetch(:col_down) { false } @cols = options.fetch(:cols) { 1 } build end # Transpose the (already sliced by rows) list, # turning its rows into columns. # @return [self] def transpose first_row = @list[0] other_rows = @list[1..-1] @list = first_row.zip(*other_rows) self end # Slice the list by rows and transpose it. # @return [self] def col_down slice_by_rows transpose self end # Slice the list by rows. The row count is calculated # indirectly based on the {#cols} param and the items count. # @return [self] def slice_by_rows @list = items_sliced_by_rows self end # Slice the list by cols based on the {#cols} param. # @return [self] def slice_by_cols @list = items_sliced_by_cols self end # Set the cols number. # @return [self] def cols=(cols) @cols = cols build end # Returns an Array representation of the list # in its current state. # @return [Array] @list.dup def list @list.dup end # (see #list) def to_a list end # Stringfies the list in its current state. # It joins each individual _cell_ with the current # {#row_join_string} between them. # It joins each individual row with a # newline character. So the returned String is # suitable to be directly outputed # to the screen, preserving row/columns divisions. # @return [String] def to_s list.map { |row| stringfy(row) }.join end # The String that will be used to join each # cell of the list and stringfying it. # @return [String] defaults to " " (space) def row_join_string @row_join_string ||= " " end # Set the {#row_join_string}. # @see #row_join_string attr_writer :row_join_string # Returns the row join string size. # Useful for calculating the actual size of # rendered list. # @return [Integer] def row_join_str_size row_join_string.size end private def build slice_by_cols transpose if transpose_mode col_down if col_down_mode self end def items_sliced_by_cols items.each_slice(cols).to_a end def items_sliced_by_rows items.each_slice(row_count).to_a end def row_count (items.count / cols.to_f).ceil end def stringfy(row) row.compact.join(row_join_string) + "\n" end end end highline-2.0.3/lib/highline/list_renderer.rb000066400000000000000000000163371355001426500210410ustar00rootroot00000000000000# coding: utf-8 require "highline/template_renderer" require "highline/wrapper" require "highline/list" class HighLine # # This class is a utility for quickly and easily laying out lists # to be used by HighLine. # class ListRenderer # Items list # @return [Array] attr_reader :items # @return [Symbol] the current mode the List is being rendered # @see #initialize for more details see mode parameter of #initialize attr_reader :mode # Changes the behaviour of some modes. Example, in :inline mode # the option is treated as the 'end separator' (defaults to " or ") # @return option parameter that changes the behaviour of some modes. attr_reader :option # @return [HighLine] context attr_reader :highline # The only required parameters are _items_ and _highline_. # @param items [Array] the Array of items to list # @param mode [Symbol] controls how that list is formed # @param option has different effects, depending on the _mode_. # @param highline [HighLine] a HighLine instance to direct the output to. # # Recognized modes are: # # :columns_across:: _items_ will be placed in columns, # flowing from left to right. If given, # _option_ is the number of columns to be # used. When absent, columns will be # determined based on _wrap_at_ or a # default of 80 characters. # :columns_down:: Identical to :columns_across, # save flow goes down. # :uneven_columns_across:: Like :columns_across but each # column is sized independently. # :uneven_columns_down:: Like :columns_down but each # column is sized independently. # :inline:: All _items_ are placed on a single # line. The last two _items_ are # separated by _option_ or a default of # " or ". All other _items_ are # separated by ", ". # :rows:: The default mode. Each of the _items_ # is placed on its own line. The _option_ # parameter is ignored in this mode. # # Each member of the _items_ Array is passed through ERb and thus can # contain their own expansions. Color escape expansions do not contribute to # the final field width. def initialize(items, mode = :rows, option = nil, highline) @highline = highline @mode = mode @option = option @items = render_list_items(items) end # Render the list using the appropriate mode and options. # @return [String] rendered list as String def render return "" if items.empty? case mode when :inline list_inline_mode when :columns_across list_columns_across_mode when :columns_down list_columns_down_mode when :uneven_columns_across list_uneven_columns_mode when :uneven_columns_down list_uneven_columns_down_mode else list_default_mode end end private def render_list_items(items) items.to_ary.map do |item| item = String(item) template = if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+ ERB.new(item, trim_mode: "%") else ERB.new(item, nil, "%") end template_renderer = HighLine::TemplateRenderer.new(template, self, highline) template_renderer.render end end def list_default_mode items.map { |i| "#{i}\n" }.join end def list_inline_mode end_separator = option || " or " if items.size == 1 items.first else items[0..-2].join(", ") + "#{end_separator}#{items.last}" end end def list_columns_across_mode HighLine::List.new(right_padded_items, cols: col_count).to_s end def list_columns_down_mode HighLine::List.new( right_padded_items, cols: col_count, col_down: true ).to_s end def list_uneven_columns_mode(list = nil) list ||= HighLine::List.new(items) col_max = option || items.size col_max.downto(1) do |column_count| list.cols = column_count widths = get_col_widths(list) if column_count == 1 || # last guess inside_line_size_limit?(widths) || # good guess option # defined by user return pad_uneven_rows(list, widths) end end end def list_uneven_columns_down_mode list = HighLine::List.new(items, col_down: true) list_uneven_columns_mode(list) end def pad_uneven_rows(list, widths) right_padded_list = Array(list).map do |row| right_pad_row(row.compact, widths) end stringfy_list(right_padded_list) end def stringfy_list(list) list.map { |row| row_to_s(row) }.join end def row_to_s(row) row.compact.join(row_join_string) + "\n" end def right_pad_row(row, widths) row.zip(widths).map do |field, width| right_pad_field(field, width) end end def right_pad_field(field, width) field = String(field) # nil protection pad_size = width - actual_length(field) field + (pad_char * pad_size) end def get_col_widths(lines) lines = transpose(lines) get_segment_widths(lines) end def get_row_widths(lines) get_segment_widths(lines) end def get_segment_widths(lines) lines.map do |col| actual_lengths_for(col).max end end def actual_lengths_for(line) line.map do |item| actual_length(item) end end def transpose(lines) lines = Array(lines) first_line = lines.shift first_line.zip(*lines) end def inside_line_size_limit?(widths) line_size = widths.reduce(0) { |sum, n| sum + n + row_join_str_size } line_size <= line_size_limit + row_join_str_size end def actual_length(text) HighLine::Wrapper.actual_length text end def items_max_length @items_max_length ||= max_length(items) end def max_length(items) items.map { |item| actual_length(item) }.max end def line_size_limit @line_size_limit ||= (highline.wrap_at || 80) end def row_join_string @row_join_string ||= " " end attr_writer :row_join_string def row_join_str_size row_join_string.size end def col_count_calculate (line_size_limit + row_join_str_size) / (items_max_length + row_join_str_size) end def col_count option || col_count_calculate end def right_padded_items items.map do |item| right_pad_field(item, items_max_length) end end def pad_char " " end def row_count (items.count / col_count.to_f).ceil end end end highline-2.0.3/lib/highline/menu.rb000077500000000000000000000460751355001426500171510ustar00rootroot00000000000000# coding: utf-8 #-- # menu.rb # # Created by Gregory Thomas Brown on 2005-05-10. # Copyright 2005. All rights reserved. # # This is Free Software. See LICENSE and COPYING for details. require "highline/question" require "highline/menu/item" class HighLine # # Menu objects encapsulate all the details of a call to # {HighLine#choose HighLine#choose}. # Using the accessors and {Menu#choice} and {Menu#choices}, the block passed # to {HighLine#choose} can detail all aspects of menu display and control. # class Menu < Question # Pass +false+ to _color_ to turn off HighLine::Menu's # index coloring. # Pass a color and the Menu's indices will be colored. class << self attr_writer :index_color end # Initialize it self.index_color = false # Returns color used for coloring Menu's indices class << self attr_reader :index_color end # # Create an instance of HighLine::Menu. All customization is done # through the passed block, which should call accessors, {#choice} and # {#choices} as needed to define the Menu. Note that Menus are also # {HighLine::Question Questions}, so all that functionality is available # to the block as well. # # @example Implicit menu creation through HighLine#choose # cli = HighLine.new # answer = cli.choose do |menu| # menu.prompt = "Please choose your favorite programming language? " # menu.choice(:ruby) { say("Good choice!") } # menu.choices(:python, :perl) { say("Not from around here, are you?") } # end def initialize # # Initialize Question objects with ignored values, we'll # adjust ours as needed. # super("Ignored", [], &nil) # avoiding passing the block along @items = [] @hidden_items = [] @help = Hash.new("There's no help for that topic.") @index = :number @index_suffix = ". " @select_by = :index_or_name @flow = :rows @list_option = nil @header = nil @prompt = "? " @layout = :list @shell = false @nil_on_handled = false # Used for coloring Menu indices. # Set it to default. But you may override it. @index_color = self.class.index_color # Override Questions responses, we'll set our own. @responses = {} # Context for action code. @highline = nil yield self if block_given? init_help if @shell && !@help.empty? end # # An _index_ to append to each menu item in display. See # Menu.index=() for details. # attr_reader :index # # The String placed between an _index_ and a menu item. Defaults to # ". ". Switches to " ", when _index_ is set to a String (like "-"). # attr_accessor :index_suffix # # The _select_by_ attribute controls how the user is allowed to pick a # menu item. The available choices are: # # :index:: The user is allowed to type the numerical # or alphabetical index for their selection. # :index_or_name:: Allows both methods from the # :index option and the # :name option. # :name:: Menu items are selected by typing a portion # of the item name that will be # auto-completed. # attr_accessor :select_by # # This attribute is passed directly on as the mode to HighLine.list() by # all the preset layouts. See that method for appropriate settings. # attr_accessor :flow # # This setting is passed on as the third parameter to HighLine.list() # by all the preset layouts. See that method for details of its # effects. Defaults to +nil+. # attr_accessor :list_option # # Used by all the preset layouts to display title and/or introductory # information, when set. Defaults to +nil+. # attr_accessor :header # # Used by all the preset layouts to ask the actual question to fetch a # menu selection from the user. Defaults to "? ". # attr_accessor :prompt # # An ERb _layout_ to use when displaying this Menu object. See # Menu.layout=() for details. # attr_reader :layout # # When set to +true+, responses are allowed to be an entire line of # input, including details beyond the command itself. Only the first # "word" of input will be matched against the menu choices, but both the # command selected and the rest of the line will be passed to provided # action blocks. Defaults to +false+. # attr_accessor :shell # # When +true+, any selected item handled by provided action code will # return +nil+, instead of the results to the action code. This may # prove handy when dealing with mixed menus where only the names of # items without any code (and +nil+, of course) will be returned. # Defaults to +false+. # attr_accessor :nil_on_handled # # The color of the index when displaying the menu. See Style class for # available colors. # attr_accessor :index_color # # Adds _name_ to the list of available menu items. Menu items will be # displayed in the order they are added. # # An optional _action_ can be associated with this name and if provided, # it will be called if the item is selected. The result of the method # will be returned, unless _nil_on_handled_ is set (when you would get # +nil+ instead). In _shell_ mode, a provided block will be passed the # command chosen and any details that followed the command. Otherwise, # just the command is passed. The @highline variable is set to # the current HighLine context before the action code is called and can # thus be used for adding output and the like. # # @param name [#to_s] menu item title/header/name to be displayed. # @param action [Proc] callback action to be run when the item is selected. # @param help [String] help/hint string to be displayed. # @return [void] # @example (see HighLine::Menu#initialize) # @example Use of help string on menu items # cli = HighLine.new # cli.choose do |menu| # menu.shell = true # # menu.choice(:load, text: 'Load a file', # help: "Load a file using your favourite editor.") # menu.choice(:save, help: "Save data in file.") # menu.choice(:quit, help: "Exit program.") # # menu.help("rules", "The rules of this system are as follows...") # end def choice(name, help = nil, text = nil, &action) item = Menu::Item.new(name, text: text, help: help, action: action) @items << item @help.merge!(item.item_help) update_responses # rebuild responses based on our settings end # # This method helps reduce the namespaces in the original call, # which would look like this: HighLine::Menu::Item.new(...) # With #build_item, it looks like this: menu.build_item(...) # @param *args splat args, the same args you would pass to an # initialization of HighLine::Menu::Item # @return [HighLine::Menu::Item] the menu item def build_item(*args) Menu::Item.new(*args) end # # Adds an item directly to the menu. If you want more configuration # or options, use this method # # @param item [Menu::Item] item containing choice fields and more # @return [void] def add_item(item) @items << item @help.merge!(item.item_help) update_responses end # # A shortcut for multiple calls to the sister method {#choice}. Be # warned: An _action_ set here will apply to *all* provided # _names_. This is considered to be a feature, so you can easily # hand-off interface processing to a different chunk of code. # @param names [Array<#to_s>] menu item titles/headers/names to be # displayed. # @param action (see #choice) # @return [void] # @example (see HighLine::Menu#initialize) # # choice has more options available to you, like longer text or help (and # of course, individual actions) # def choices(*names, &action) names.each { |n| choice(n, &action) } end # Identical to {#choice}, but the item will not be listed for the user. # @see #choice # @param name (see #choice) # @param help (see #choice) # @param action (see #choice) # @return (see #choice) def hidden(name, help = nil, &action) item = Menu::Item.new(name, text: name, help: help, action: action) @hidden_items << item @help.merge!(item.item_help) end # # Sets the indexing style for this Menu object. Indexes are appended to # menu items, when displayed in list form. The available settings are: # # :number:: Menu items will be indexed numerically, starting # with 1. This is the default method of indexing. # :letter:: Items will be indexed alphabetically, starting # with a. # :none:: No index will be appended to menu items. # any String:: Will be used as the literal _index_. # # Setting the _index_ to :none or a literal String also adjusts # _index_suffix_ to a single space and _select_by_ to :name. # Because of this, you should make a habit of setting the _index_ first. # def index=(style) @index = style return unless @index == :none || @index.is_a?(::String) # Default settings. @index_suffix = " " @select_by = :name end # # Initializes the help system by adding a :help choice, some # action code, and the default help listing. # def init_help return if @items.include?(:help) topics = @help.keys.sort help_help = if @help.include?("help") @help["help"] else "This command will display helpful messages about " \ "functionality, like this one. To see the help for " \ "a specific topic enter:\n\thelp [TOPIC]\nTry asking " \ "for help on any of the following:\n\n" \ "<%= list(#{topics.inspect}, :columns_across) %>" end choice(:help, help_help) do |_command, topic| topic.strip! topic.downcase! if topic.empty? @highline.say(@help["help"]) else @highline.say("= #{topic}\n\n#{@help[topic]}") end end end # # Used to set help for arbitrary topics. Use the topic "help" # to override the default message. Mainly for internal use. # # @param topic [String] the menu item header/title/name to be associated # with a help message. # @param help [String] the help message to be associated with the menu # item/title/name. def help(topic, help) @help[topic] = help end # # Setting a _layout_ with this method also adjusts some other attributes # of the Menu object, to ideal defaults for the chosen _layout_. To # account for that, you probably want to set a _layout_ first in your # configuration block, if needed. # # Accepted settings for _layout_ are: # # :list:: The default _layout_. The _header_ if set # will appear at the top on its own line with # a trailing colon. Then the list of menu # items will follow. Finally, the _prompt_ # will be used as the ask()-like question. # :one_line:: A shorter _layout_ that fits on one line. # The _header_ comes first followed by a # colon and spaces, then the _prompt_ with menu # items between trailing parenthesis. # :menu_only:: Just the menu items, followed up by a likely # short _prompt_. # any ERb String:: Will be taken as the literal _layout_. This # String can access header, # menu and prompt, but is # otherwise evaluated in the TemplateRenderer # context so each method is properly delegated. # # If set to either :one_line, or :menu_only, _index_ # will default to :none and _flow_ will default to # :inline. # def layout=(new_layout) @layout = new_layout # Default settings. case @layout when :one_line, :menu_only self.index = :none @flow = :inline end end # # This method returns all possible options for auto-completion, based # on the settings of _index_ and _select_by_. # def options case @select_by when :index map_items_by_index when :name map_items_by_name else map_items_by_index + map_items_by_name end end def map_items_by_index if [:letter, :capital_letter].include?(@index) # @ and ` are the previous ASCII characters to A and a respectively prev_char = (@index == :capital_letter ? '@' : '`') all_items.map { prev_char.succ!.dup } else (1..all_items.size).map(&:to_s) end end def map_items_by_name all_items.map(&:name) end def all_items @items + @hidden_items end # # This method processes the auto-completed user selection, based on the # rules for this Menu object. If an action was provided for the # selection, it will be executed as described in {#choice}. # # @param highline_context [HighLine] a HighLine instance to be used # as context. # @param selection [String, Integer] index or title of the selected # menu item. # @param details additional parameter to be passed when in shell mode. # @return [nil, Object] if @nil_on_handled is set it returns +nil+, # else it returns the action return value. def select(highline_context, selection, details = nil) # add in any hidden menu commands items = all_items # Find the selected action. selected_item = find_item_from_selection(items, selection) # Run or return it. @highline = highline_context value_for_selected_item(selected_item, details) end def find_item_from_selection(items, selection) if selection =~ /^\d+$/ # is a number? get_item_by_number(items, selection) else get_item_by_letter(items, selection) end end # Returns the menu item referenced by its index # @param selection [Integer] menu item's index. def get_item_by_number(items, selection) items[selection.to_i - 1] end # Returns the menu item referenced by its title/header/name. # @param selection [String] menu's title/header/name def get_item_by_letter(items, selection) item = items.find { |i| i.name == selection } return item if item # 97 is the "a" letter at ascii table # Ex: For "a" it will return 0, and for "c" it will return 2 index = selection.downcase.ord - 97 items[index] end def value_for_selected_item(item, details) if item.action result = if @shell item.action.call(item.name, details) else item.action.call(item.name) end @nil_on_handled ? nil : result else item.name end end def gather_selected(highline_context, selections, details = nil) @highline = highline_context # add in any hidden menu commands items = all_items if selections.is_a?(Array) value_for_array_selections(items, selections, details) elsif selections.is_a?(Hash) value_for_hash_selections(items, selections, details) else raise ArgumentError, "selections must be either Array or Hash" end end def value_for_array_selections(items, selections, details) # Find the selected items and return values selected_items = selections.map do |selection| find_item_from_selection(items, selection) end index = 0 selected_items.map do |selected_item| value = value_for_selected_item(selected_item, self.shell ? details[index] : nil) index += 1 value end end def value_for_hash_selections(items, selections, details) # Find the selected items and return in hash form index = 0 selections.each_with_object({}) do |(key, selection), memo| selected_item = find_item_from_selection(items, selection) value = value_for_selected_item(selected_item, self.shell ? details[index] : nil) index += 1 memo[key] = value end end def decorate_index(index) if index_color HighLine.color(index, index_color) else index end end # # Allows Menu objects to pass as Arrays, for use with HighLine.list(). # This method returns all menu items to be displayed, complete with # indexes. # def to_ary @items.map.with_index { |item, ix| decorate_item(item.text.to_s, ix) } end def decorate_item(text, ix) decorated, non_decorated = mark_for_decoration(text, ix) decorate_index(decorated) + non_decorated end def mark_for_decoration(text, ix) case @index when :number ["#{ix + 1}#{@index_suffix}", text] when :letter, :capital_letter first_letter = (@index == :capital_letter ? 'A' : 'a') ["#{(first_letter.ord + ix).chr}#{@index_suffix}", text] when :none [text, ""] else ["#{index}#{@index_suffix}", text] end end # # Allows Menu to behave as a String, just like Question. Returns the # _layout_ to be rendered, which is used by HighLine.say(). # def to_s case @layout when :list %(<%= header ? "#{header}:\n" : '' %>) + parse_list + show_default_if_any + "<%= prompt %>" when :one_line %(<%= header ? "#{header}: " : '' %>) + "<%= prompt %>" \ "(" + parse_list + ")" + show_default_if_any + "<%= prompt[/\s*$/] %>" when :menu_only parse_list + show_default_if_any + "<%= prompt %>" else @layout end end def parse_list "<%= list( menu, #{@flow.inspect}, #{@list_option.inspect} ) %>" end def show_default_if_any default.to_s.empty? ? "" : "(#{default}) " end # # This method will update the intelligent responses to account for # Menu specific differences. Calls the superclass' (Question's) # build_responses method, overriding its default arguments to specify # 'options' will be used to populate choice lists. # def update_responses build_responses(options) end end end highline-2.0.3/lib/highline/menu/000077500000000000000000000000001355001426500166055ustar00rootroot00000000000000highline-2.0.3/lib/highline/menu/item.rb000066400000000000000000000016511355001426500200730ustar00rootroot00000000000000# encoding: utf-8 class HighLine class Menu < Question # Represents an Item of a HighLine::Menu. # class Item attr_reader :name, :text, :help, :action # # @param name [String] The name that is matched against the user input # @param attributes [Hash] options Hash to tailor menu item to your needs # @option attributes text: [String] The text that displays for that # choice (defaults to name) # @option attributes help: [String] help/hint string to be displayed. # @option attributes action: [Block] a block that gets called when choice # is selected # def initialize(name, attributes) @name = name @text = attributes[:text] || @name @help = attributes[:help] @action = attributes[:action] end def item_help return {} unless help { name.to_s.downcase => help } end end end end highline-2.0.3/lib/highline/paginator.rb000066400000000000000000000032411355001426500201520ustar00rootroot00000000000000# coding: utf-8 class HighLine # Take the task of paginating some piece of text given a HighLine context class Paginator # @return [HighLine] HighLine context attr_reader :highline # Returns a HighLine::Paginator instance where you can # call {#page_print} on it. # @param highline [HighLine] context # @example # HighLine::Paginator.new(highline).page_print(statement) def initialize(highline) @highline = highline end # # Page print a series of at most _page_at_ lines for _output_. After each # page is printed, HighLine will pause until the user presses enter/return # then display the next page of data. # # Note that the final page of _output_ is *not* printed, but returned # instead. This is to support any special handling for the final sequence. # # @param text [String] text to be paginated # @return [String] last line if paging is aborted def page_print(text) return text unless highline.page_at lines = text.lines.to_a while lines.size > highline.page_at highline.puts lines.slice!(0...highline.page_at).join highline.puts # Return last line if user wants to abort paging return "...\n#{lines.last}" unless continue_paging? end lines.join end # # Ask user if they wish to continue paging output. Allows them to # type "q" to cancel the paging process. # def continue_paging? command = highline.new_scope.ask( "-- press enter/return to continue or q to stop -- " ) { |q| q.character = true } command !~ /\A[qQ]\Z/ # Only continue paging if Q was not hit. end end end highline-2.0.3/lib/highline/question.rb000077500000000000000000000553101355001426500200440ustar00rootroot00000000000000# coding: utf-8 #-- # question.rb # # Created by James Edward Gray II on 2005-04-26. # Copyright 2005 Gray Productions. All rights reserved. # # This is Free Software. See LICENSE and COPYING for details. require "English" require "optparse" require "date" require "pathname" require "highline/question/answer_converter" class HighLine # # Question objects contain all the details of a single invocation of # HighLine.ask(). The object is initialized by the parameters passed to # HighLine.ask() and then queried to make sure each step of the input # process is handled according to the users wishes. # class Question include CustomErrors # # If _template_or_question_ is already a Question object just return it. # If not, build it. # # @param template_or_question [String, Question] what to ask # @param answer_type [Class] to what class to convert the answer # @param details to be passed to Question.new # @return [Question] def self.build(template_or_question, answer_type = nil, &details) if template_or_question.is_a? Question template_or_question else Question.new(template_or_question, answer_type, &details) end end # # Create an instance of HighLine::Question. Expects a _template_ to ask # (can be "") and an _answer_type_ to convert the answer to. # The _answer_type_ parameter must be a type recognized by # Question.convert(). If given, a block is yielded the new Question # object to allow custom initialization. # # @param template [String] any String # @param answer_type [Class] the type the answer will be converted to it. def initialize(template, answer_type) # initialize instance data @template = String(template).dup @answer_type = answer_type @completion = @answer_type @echo = true @whitespace = :strip @case = nil @in = nil @first_answer = nil @glob = "*" @overwrite = false @user_responses = {} @internal_responses = default_responses_hash @directory = Pathname.new(File.expand_path(File.dirname($PROGRAM_NAME))) # allow block to override settings yield self if block_given? # finalize responses based on settings build_responses end # The ERb template of the question to be asked. attr_accessor :template # The answer, set by HighLine#ask attr_accessor :answer # The type that will be used to convert this answer. attr_accessor :answer_type # For Auto-completion attr_accessor :completion # # Can be set to +true+ to use HighLine's cross-platform character reader # instead of fetching an entire line of input. (Note: HighLine's character # reader *ONLY* supports STDIN on Windows and Unix.) Can also be set to # :getc to use that method on the input stream. # # *WARNING*: The _echo_ and _overwrite_ attributes for a question are # ignored when using the :getc method. # attr_accessor :character # # Allows you to set a character limit for input. # # *WARNING*: This option forces a character by character read. # attr_accessor :limit # # Can be set to +true+ or +false+ to control whether or not input will # be echoed back to the user. A setting of +true+ will cause echo to # match input, but any other true value will be treated as a String to # echo for each character typed. # # This requires HighLine's character reader. See the _character_ # attribute for details. # # *Note*: When using HighLine to manage echo on Unix based systems, we # recommend installing the termios gem. Without it, it's possible to type # fast enough to have letters still show up (when reading character by # character only). # attr_accessor :echo # # Use the Readline library to fetch input. This allows input editing as # well as keeping a history. In addition, tab will auto-complete # within an Array of choices or a file listing. # # *WARNING*: This option is incompatible with all of HighLine's # character reading modes and it causes HighLine to ignore the # specified _input_ stream. # attr_accessor :readline # # Used to control whitespace processing for the answer to this question. # See HighLine::Question.remove_whitespace() for acceptable settings. # attr_accessor :whitespace # # Used to control character case processing for the answer to this question. # See HighLine::Question.change_case() for acceptable settings. # attr_accessor :case # Used to provide a default answer to this question. attr_accessor :default # # If set to a Regexp, the answer must match (before type conversion). # Can also be set to a Proc which will be called with the provided # answer to validate with a +true+ or +false+ return. # attr_accessor :validate # Used to control range checks for answer. attr_accessor :above, :below # If set, answer must pass an include?() check on this object. attr_accessor :in # # Asks a yes or no confirmation question, to ensure a user knows what # they have just agreed to. The confirm attribute can be set to : # +true+ : In this case the question will be, "Are you sure?". # Proc : The Proc is yielded the answer given. The Proc must # output a string which is then used as the confirm # question. # String : The String must use ERB syntax. The String is # evaluated with access to question and answer and # is then used as the confirm question. # When set to +false+ or +nil+ (the default), answers are not confirmed. attr_accessor :confirm # # When set, the user will be prompted for multiple answers which will # be collected into an Array or Hash and returned as the final answer. # # You can set _gather_ to an Integer to have an Array of exactly that # many answers collected, or a String/Regexp to match an end input which # will not be returned in the Array. # # Optionally _gather_ can be set to a Hash. In this case, the question # will be asked once for each key and the answers will be returned in a # Hash, mapped by key. The @key variable is set before each # question is evaluated, so you can use it in your question. # attr_accessor :gather # # When set to +true+ multiple entries will be collected according to # the setting for _gather_, except they will be required to match # each other. Multiple identical entries will return a single answer. # attr_accessor :verify_match # # When set to a non *nil* value, this will be tried as an answer to the # question. If this answer passes validations, it will become the result # without the user ever being prompted. Otherwise this value is discarded, # and this Question is resolved as a normal call to HighLine.ask(). # attr_writer :first_answer # # The directory from which a user will be allowed to select files, when # File or Pathname is specified as an _answer_type_. Initially set to # Pathname.new(File.expand_path(File.dirname($0))). # attr_accessor :directory # # The glob pattern used to limit file selection when File or Pathname is # specified as an _answer_type_. Initially set to "*". # attr_accessor :glob # # A Hash that stores the various responses used by HighLine to notify # the user. The currently used responses and their purpose are as # follows: # # :ambiguous_completion:: Used to notify the user of an # ambiguous answer the auto-completion # system cannot resolve. # :ask_on_error:: This is the question that will be # redisplayed to the user in the event # of an error. Can be set to # :question to repeat the # original question. # :invalid_type:: The error message shown when a type # conversion fails. # :no_completion:: Used to notify the user that their # selection does not have a valid # auto-completion match. # :not_in_range:: Used to notify the user that a # provided answer did not satisfy # the range requirement tests. # :not_valid:: The error message shown when # validation checks fail. # def responses @user_responses end # # When set to +true+ the question is asked, but output does not progress to # the next line. The Cursor is moved back to the beginning of the question # line and it is cleared so that all the contents of the line disappear from # the screen. # attr_accessor :overwrite # # Returns the provided _answer_string_ or the default answer for this # Question if a default was set and the answer is empty. # # @param answer_string [String] # @return [String] the answer itself or a default message. def answer_or_default(answer_string) return default if answer_string.empty? && default answer_string end # # Called late in the initialization process to build intelligent # responses based on the details of this Question object. # Also used by Menu#update_responses. # # @return [Hash] responses Hash winner (new and old merge). # @param message_source [Class] Array or String for example. # Same as {#answer_type}. def build_responses(message_source = answer_type) append_default if [::String, Symbol].include? default.class new_hash = build_responses_new_hash(message_source) # Update our internal responses with the new hash # generated from the message source @internal_responses = @internal_responses.merge(new_hash) end def default_responses_hash { ask_on_error: "? ", mismatch: "Your entries didn't match." } end # When updating the responses hash, it generates the new one. # @param message_source (see #build_responses) # @return [Hash] responses hash def build_responses_new_hash(message_source) { ambiguous_completion: "Ambiguous choice. Please choose one of " + choice_error_str(message_source) + ".", invalid_type: "You must enter a valid #{message_source}.", no_completion: "You must choose one of " + choice_error_str(message_source) + ".", not_in_range: "Your answer isn't within the expected range " \ "(#{expected_range}).", not_valid: "Your answer isn't valid (must match " \ "#{validate.inspect})." } end # This is the actual responses hash that gets used in determining output # Notice that we give @user_responses precedence over the responses # generated internally via build_response def final_responses @internal_responses.merge(@user_responses) end def final_response(error) response = final_responses[error] if response.respond_to?(:call) response.call(answer) else response end end # # Returns the provided _answer_string_ after changing character case by # the rules of this Question. Valid settings for whitespace are: # # +nil+:: Do not alter character case. # (Default.) # :up:: Calls upcase(). # :upcase:: Calls upcase(). # :down:: Calls downcase(). # :downcase:: Calls downcase(). # :capitalize:: Calls capitalize(). # # An unrecognized choice (like :none) is treated as +nil+. # # @param answer_string [String] # @return [String] upcased, downcased, capitalized # or unchanged answer String. def change_case(answer_string) if [:up, :upcase].include?(@case) answer_string.upcase elsif [:down, :downcase].include?(@case) answer_string.downcase elsif @case == :capitalize answer_string.capitalize else answer_string end end # # Transforms the given _answer_string_ into the expected type for this # Question. Currently supported conversions are: # # [...]:: Answer must be a member of the passed Array. # Auto-completion is used to expand partial # answers. # lambda {...}:: Answer is passed to lambda for conversion. # Date:: Date.parse() is called with answer. # DateTime:: DateTime.parse() is called with answer. # File:: The entered file name is auto-completed in # terms of _directory_ + _glob_, opened, and # returned. # Float:: Answer is converted with Kernel.Float(). # Integer:: Answer is converted with Kernel.Integer(). # +nil+:: Answer is left in String format. (Default.) # Pathname:: Same as File, save that a Pathname object is # returned. # String:: Answer is converted with Kernel.String(). # HighLine::String:: Answer is converted with HighLine::String() # Regexp:: Answer is fed to Regexp.new(). # Symbol:: The method to_sym() is called on answer and # the result returned. # any other Class:: The answer is passed on to # Class.parse(). # # This method throws ArgumentError, if the conversion cannot be # completed for any reason. # def convert AnswerConverter.new(self).convert end # Run {#in_range?} and raise an error if not succesful def check_range raise NotInRangeQuestionError unless in_range? end # Try to auto complete answer_string # @param answer_string [String] # @return [String] def choices_complete(answer_string) # cheating, using OptionParser's Completion module choices = selection choices.extend(OptionParser::Completion) answer = choices.complete(answer_string) raise NoAutoCompleteMatch unless answer answer end # Returns an English explanation of the current range settings. def expected_range expected = [] expected << "above #{above}" if above expected << "below #{below}" if below expected << "included in #{@in.inspect}" if @in case expected.size when 0 then "" when 1 then expected.first when 2 then expected.join(" and ") else expected[0..-2].join(", ") + ", and #{expected.last}" end end # Returns _first_answer_, which will be unset following this call. def first_answer @first_answer ensure @first_answer = nil end # Returns true if _first_answer_ is set. def first_answer? true if @first_answer end # # Returns +true+ if the _answer_object_ is greater than the _above_ # attribute, less than the _below_ attribute and include?()ed in the # _in_ attribute. Otherwise, +false+ is returned. Any +nil+ attributes # are not checked. # def in_range? (!above || answer > above) && (!below || answer < below) && (!@in || @in.include?(answer)) end # # Returns the provided _answer_string_ after processing whitespace by # the rules of this Question. Valid settings for whitespace are: # # +nil+:: Do not alter whitespace. # :strip:: Calls strip(). (Default.) # :chomp:: Calls chomp(). # :collapse:: Collapses all whitespace runs to a # single space. # :strip_and_collapse:: Calls strip(), then collapses all # whitespace runs to a single space. # :chomp_and_collapse:: Calls chomp(), then collapses all # whitespace runs to a single space. # :remove:: Removes all whitespace. # # An unrecognized choice (like :none) is treated as +nil+. # # This process is skipped for single character input. # # @param answer_string [String] # @return [String] answer string with whitespaces removed def remove_whitespace(answer_string) if !whitespace answer_string elsif [:strip, :chomp].include?(whitespace) answer_string.send(whitespace) elsif whitespace == :collapse answer_string.gsub(/\s+/, " ") elsif [:strip_and_collapse, :chomp_and_collapse].include?(whitespace) result = answer_string.send(whitespace.to_s[/^[a-z]+/]) result.gsub(/\s+/, " ") elsif whitespace == :remove answer_string.gsub(/\s+/, "") else answer_string end end # Convert to String, remove whitespace and change case # when necessary # @param answer_string [String] # @return [String] converted String def format_answer(answer_string) answer_string = String(answer_string) answer_string = remove_whitespace(answer_string) change_case(answer_string) end # # Returns an Array of valid answers to this question. These answers are # only known when _answer_type_ is set to an Array of choices, File, or # Pathname. Any other time, this method will return an empty Array. # def selection if completion.is_a?(Array) completion elsif [File, Pathname].include?(completion) Dir[File.join(directory.to_s, glob)].map do |file| File.basename(file) end else [] end end # Stringifies the template to be asked. def to_s template end # # Returns +true+ if the provided _answer_string_ is accepted by the # _validate_ attribute or +false+ if it's not. # # It's important to realize that an answer is validated after whitespace # and case handling. # def valid_answer? !validate || (validate.is_a?(Regexp) && answer =~ validate) || (validate.is_a?(Proc) && validate[answer]) end # # Return a line or character of input, as requested for this question. # Character input will be returned as a single character String, # not an Integer. # # This question's _first_answer_ will be returned instead of input, if set. # # Raises EOFError if input is exhausted. # # @param highline [HighLine] context # @return [String] a character or line def get_response(highline) return first_answer if first_answer? case character when :getc highline.get_response_getc_mode(self) when true highline.get_response_character_mode(self) else highline.get_response_line_mode(self) end end # Uses {#get_response} but returns a default answer # using {#answer_or_default} in case no answers was # returned. # # @param highline [HighLine] context # @return [String] def get_response_or_default(highline) self.answer = answer_or_default(get_response(highline)) end # Returns the String to be shown when asking for an answer confirmation. # @param highline [HighLine] context # @return [String] default "Are you sure?" if {#confirm} is +true+ # @return [String] {#confirm} rendered as a template if it is a String def confirm_question(highline) if confirm == true "Are you sure? " elsif confirm.is_a?(Proc) confirm.call(answer) else # evaluate ERb under initial scope, so it will have # access to question and answer template = if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+ ERB.new(confirm, trim_mode: "%") else ERB.new(confirm, nil, "%") end template_renderer = TemplateRenderer.new(template, self, highline) template_renderer.render end end # Provides the String to be asked when at an error situation. # It may be just the question itself (repeat on error). # @return [self] if :ask_on_error on responses Hash is set to :question # @return [String] if :ask_on_error on responses Hash is set to # something else def ask_on_error_msg if final_responses[:ask_on_error] == :question self elsif final_responses[:ask_on_error] final_responses[:ask_on_error] end end # readline() needs to handle its own output, but readline only supports # full line reading. Therefore if question.echo is anything but true, # the prompt will not be issued. And we have to account for that now. # Also, JRuby-1.7's ConsoleReader.readLine() needs to be passed the prompt # to handle line editing properly. # @param highline [HighLine] context # @return [void] def show_question(highline) highline.say(self) end # Returns an echo string that is adequate for this Question settings. # @param response [String] # @return [String] the response itself if {#echo} is +true+. # @return [String] echo character if {#echo} is truethy. Mainly a String. # @return [String] empty string if {#echo} is falsy. def get_echo_for_response(response) # actually true, not only truethy value if echo == true response # any truethy value, probably a String elsif echo echo # any falsy value, false or nil else "" end end private # # Adds the default choice to the end of question between |...|. # Trailing whitespace is preserved so the function of HighLine.say() is # not affected. # def append_default if template =~ /([\t ]+)\Z/ template << "|#{default}|#{Regexp.last_match(1)}" elsif template == "" template << "|#{default}| " elsif template[-1, 1] == "\n" template[-2, 0] = " |#{default}|" else template << " |#{default}|" end end def choice_error_str(message_source) if message_source.is_a? Array "[" + message_source.join(", ") + "]" else message_source.inspect end end end end highline-2.0.3/lib/highline/question/000077500000000000000000000000001355001426500175105ustar00rootroot00000000000000highline-2.0.3/lib/highline/question/answer_converter.rb000066400000000000000000000052201355001426500234220ustar00rootroot00000000000000# coding: utf-8 require "forwardable" class HighLine class Question # It provides all answer conversion flow. class AnswerConverter extend Forwardable def_delegators :@question, :answer, :answer=, :check_range, :directory, :answer_type, :choices_complete # It should be initialized with a Question object. # The class will get the answer from {Question#answer} # and then convert it to the proper {Question#answer_type}. # It is mainly used by {Question#convert} # # @param question [Question] def initialize(question) @question = question end # Based on the given Question object's settings, # it makes the conversion and returns the answer. # @return [Object] the converted answer. def convert return unless answer_type self.answer = convert_by_answer_type check_range answer end # @return [HighLine::String] answer converted to a HighLine::String def to_string HighLine::String(answer) end # That's a weird name for a method! # But it's working ;-) define_method "to_highline::string" do HighLine::String(answer) end # @return [Integer] answer converted to an Integer def to_integer Kernel.send(:Integer, answer) end # @return [Float] answer converted to a Float def to_float Kernel.send(:Float, answer) end # @return [Symbol] answer converted to an Symbol def to_symbol answer.to_sym end # @return [Regexp] answer converted to a Regexp def to_regexp Regexp.new(answer) end # @return [File] answer converted to a File def to_file self.answer = choices_complete(answer) File.open(File.join(directory.to_s, answer.last)) end # @return [Pathname] answer converted to an Pathname def to_pathname self.answer = choices_complete(answer) Pathname.new(File.join(directory.to_s, answer.last)) end # @return [Array] answer converted to an Array def to_array self.answer = choices_complete(answer) answer.last end # @return [Proc] answer converted to an Proc def to_proc answer_type.call(answer) end private def convert_by_answer_type if answer_type.respond_to? :parse answer_type.parse(answer) elsif answer_type.is_a? Class send("to_#{answer_type.name.downcase}") else send("to_#{answer_type.class.name.downcase}") end end end end end highline-2.0.3/lib/highline/question_asker.rb000066400000000000000000000076361355001426500212360ustar00rootroot00000000000000# encoding: utf-8 class HighLine # Deals with the task of "asking" a question class QuestionAsker # @return [Question] question to be asked attr_reader :question include CustomErrors # To do its work QuestionAsker needs a Question # to be asked and a HighLine context where to # direct output. # # @param question [Question] question to be asked # @param highline [HighLine] context def initialize(question, highline) @question = question @highline = highline end # # Gets just one answer, as opposed to #gather_answers # # @return [String] answer def ask_once question.show_question(@highline) begin question.get_response_or_default(@highline) raise NotValidQuestionError unless question.valid_answer? question.convert if question.confirm confirmation = @highline.send(:confirm, question) raise NoConfirmationQuestionError unless confirmation end rescue ExplainableError => e explain_error(e.explanation_key) retry rescue ArgumentError => error case error.message when /ambiguous/ # the assumption here is that OptionParser::Completion#complete # (used for ambiguity resolution) throws exceptions containing # the word 'ambiguous' whenever resolution fails explain_error(:ambiguous_completion) retry when /invalid value for/ explain_error(:invalid_type) retry else raise end end question.answer end ## Multiple questions # # Collects an Array/Hash full of answers as described in # HighLine::Question.gather(). # # @return [Array, Hash] answers def gather_answers verify_match = question.verify_match answers = [] # when verify_match is set this loop will repeat until unique_answers == 1 loop do answers = gather_answers_based_on_type break unless verify_match && (@highline.send(:unique_answers, answers).size > 1) explain_error(:mismatch) end verify_match ? @highline.send(:last_answer, answers) : answers end # Gather multiple integer values based on {Question#gather} count # @return [Array] answers def gather_integer gather_with_array do |answers| (question.gather - 1).times { answers << ask_once } end end # Gather multiple values until any of them matches the # {Question#gather} Regexp. # @return [Array] answers def gather_regexp gather_with_array do |answers| answers << ask_once until answer_matches_regex(answers.last) answers.pop end end # Gather multiple values and store them on a Hash # with keys provided by the Hash on {Question#gather} # @return [Hash] def gather_hash sorted_keys = question.gather.keys.sort_by(&:to_s) sorted_keys.each_with_object({}) do |key, answers| @highline.key = key answers[key] = ask_once end end private ## Delegate to Highline def explain_error(explanation_key) # eg: :not_valid, :not_in_range @highline.say(question.final_response(explanation_key)) @highline.say(question.ask_on_error_msg) end def gather_with_array [].tap do |answers| answers << ask_once question.template = "" yield answers end end def answer_matches_regex(answer) if question.gather.is_a?(::String) || question.gather.is_a?(Symbol) answer.to_s == question.gather.to_s elsif question.gather.is_a?(Regexp) answer.to_s =~ question.gather end end def gather_answers_based_on_type case question.gather when Integer gather_integer when ::String, Symbol, Regexp gather_regexp when Hash gather_hash end end end end highline-2.0.3/lib/highline/simulate.rb000066400000000000000000000030451355001426500200130ustar00rootroot00000000000000# coding: utf-8 #-- # simulate.rb # # Created by Andy Rossmeissl on 2012-04-29. # Copyright 2005 Gray Productions. All rights reserved. # # This is Free Software. See LICENSE and COPYING for details. # # adapted from https://gist.github.com/194554 class HighLine # Simulates Highline input for use in tests. class Simulate # Creates a simulator with an array of Strings as a script # @param strings [Array] preloaded string to be used # as input buffer when simulating. def initialize(strings) @strings = strings end # Simulate StringIO#gets by shifting a string off of the script def gets @strings.shift end # Simulate StringIO#getbyte by shifting a single character off of # the next line of the script def getbyte line = gets return if line.empty? char = line.slice! 0 @strings.unshift line char end # The simulator handles its own EOF def eof? false end # A wrapper method that temporarily replaces the Highline # instance in HighLine.default_instance with an instance of this object # for the duration of the block # # @param strings [String] preloaded string buffer that # will feed the input operations when simulating. def self.with(*strings) @input = HighLine.default_instance.instance_variable_get :@input HighLine.default_instance.instance_variable_set :@input, new(strings) yield ensure HighLine.default_instance.instance_variable_set :@input, @input end end end highline-2.0.3/lib/highline/statement.rb000066400000000000000000000043531355001426500201770ustar00rootroot00000000000000# coding: utf-8 require "highline/wrapper" require "highline/paginator" require "highline/template_renderer" class HighLine # This class handles proper formatting based # on a HighLine context, applying wrapping, # pagination, indentation and color rendering # when necessary. It's used by {HighLine#render_statement} # @see HighLine#render_statement class Statement # The source object to be stringfied and formatted. attr_reader :source # The HighLine context # @return [HighLine] attr_reader :highline # The stringfied source object # @return [String] attr_reader :template_string # It needs the input String and the HighLine context # @param source [#to_s] # @param highline [HighLine] context def initialize(source, highline) @highline = highline @source = source @template_string = stringfy(source) end # Returns the formated statement. # Applies wrapping, pagination, indentation and color rendering # based on HighLine instance settings. # @return [String] formated statement def statement @statement ||= format_statement end # (see #statement) # Delegates to {#statement} def to_s statement end def self.const_missing(constant) HighLine.const_get(constant) end private def stringfy(template_string) String(template_string || "").dup end def format_statement return template_string if template_string.empty? statement = render_template statement = HighLine::Wrapper.wrap(statement, highline.wrap_at) statement = HighLine::Paginator.new(highline).page_print(statement) statement = statement.gsub(/\n(?!$)/, "\n#{highline.indentation}") if highline.multi_indent statement end def render_template # Assigning to a local var so it may be # used inside instance eval block template_renderer = TemplateRenderer.new(template, source, highline) template_renderer.render end def template @template ||= if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+ ERB.new(template_string, trim_mode: "%") else ERB.new(template_string, nil, "%") end end end end highline-2.0.3/lib/highline/string.rb000066400000000000000000000025471355001426500175040ustar00rootroot00000000000000# coding: utf-8 require "highline/string_extensions" class HighLine # # HighLine::String is a subclass of String with convenience methods added # for colorization. # # Available convenience methods include: # * 'color' method e.g. highline_string.color(:bright_blue, # :underline) # * colors e.g. highline_string.magenta # * RGB colors e.g. highline_string.rgb_ff6000 # or highline_string.rgb(255,96,0) # * background colors e.g. highline_string.on_magenta # * RGB background colors e.g. highline_string.on_rgb_ff6000 # or highline_string.on_rgb(255,96,0) # * styles e.g. highline_string.underline # # Additionally, convenience methods can be chained, for instance the # following are equivalent: # highline_string.bright_blue.blink.underline # highline_string.color(:bright_blue, :blink, :underline) # HighLine.color(highline_string, :bright_blue, :blink, :underline) # # For those less squeamish about possible conflicts, the same convenience # methods can be added to the built-in String class, as follows: # # require 'highline' # Highline.colorize_strings # class String < ::String include StringExtensions end end highline-2.0.3/lib/highline/string_extensions.rb000066400000000000000000000073231355001426500217600ustar00rootroot00000000000000# coding: utf-8 class HighLine #:nodoc: # Returns a HighLine::String from any given String. # @param s [String] # @return [HighLine::String] from the given string. def self.String(s) # rubocop:disable Naming/MethodName HighLine::String.new(s) end # HighLine extensions for String class. # Included by HighLine::String. module StringExtensions STYLE_METHOD_NAME_PATTERN = /^(on_)?rgb_([0-9a-fA-F]{6})$/ # Included hook. Actions to take when being included. # @param base [Class, Module] base class def self.included(base) define_builtin_style_methods(base) define_style_support_methods(base) end # At include time, it defines all basic style # support method like #color, #on, #uncolor, # #rgb, #on_rgb and the #method_missing callback # @return [void] # @param base [Class, Module] the base class/module # def self.define_style_support_methods(base) base.class_eval do undef :color if method_defined? :color undef :foreground if method_defined? :foreground def color(*args) self.class.new(HighLine.color(self, *args)) end alias_method :foreground, :color undef :on if method_defined? :on def on(arg) color(("on_" + arg.to_s).to_sym) end undef :uncolor if method_defined? :uncolor def uncolor self.class.new(HighLine.uncolor(self)) end undef :rgb if method_defined? :rgb def rgb(*colors) color_code = setup_color_code(*colors) color("rgb_#{color_code}".to_sym) end undef :on_rgb if method_defined? :on_rgb def on_rgb(*colors) color_code = setup_color_code(*colors) color("on_rgb_#{color_code}".to_sym) end # @todo Chain existing method_missing? undef :method_missing if method_defined? :method_missing def method_missing(method, *_args) if method.to_s =~ STYLE_METHOD_NAME_PATTERN color(method) else super end end undef :respond_to_missing if method_defined? :respond_to_missing def respond_to_missing?(method_name, include_private = false) method_name.to_s =~ STYLE_METHOD_NAME_PATTERN || super end private def setup_color_code(*colors) color_code = colors.map do |color| if color.is_a?(Numeric) format("%02x", color) else color.to_s end end.join raise "Bad RGB color #{colors.inspect}" unless color_code =~ /^[a-fA-F0-9]{6}/ color_code end end end # At include time, it defines all basic builtin styles. # @param base [Class, Module] base Class/Module def self.define_builtin_style_methods(base) HighLine::COLORS.each do |color| color = color.downcase base.class_eval <<-METHOD_DEFINITION undef :#{color} if method_defined? :#{color} def #{color} color(:#{color}) end METHOD_DEFINITION base.class_eval <<-METHOD_DEFINITION undef :on_#{color} if method_defined? :on_#{color} def on_#{color} on(:#{color}) end METHOD_DEFINITION HighLine::STYLES.each do |style| style = style.downcase base.class_eval <<-METHOD_DEFINITION undef :#{style} if method_defined? :#{style} def #{style} color(:#{style}) end METHOD_DEFINITION end end end end # Adds color support to the base String class def self.colorize_strings ::String.send(:include, StringExtensions) end end highline-2.0.3/lib/highline/style.rb000077500000000000000000000227021355001426500173340ustar00rootroot00000000000000# coding: utf-8 #-- # originally color_scheme.rb # # Created by Richard LeBer on 2011-06-27. # Copyright 2011. All rights reserved # # This is Free Software. See LICENSE and COPYING for details class HighLine #:nodoc: # Creates a style using {.find_or_create_style} or # {.find_or_create_style_list} # @param args [Array] style properties # @return [Style] def self.Style(*args) args = args.compact.flatten if args.size == 1 find_or_create_style(args.first) else find_or_create_style_list(*args) end end # Search for a Style with the given properties and return it. # If there's no matched Style, it creates one. # You can pass a Style, String or a Hash. # @param arg [Style, String, Hash] style properties # @return [Style] found or creted Style def self.find_or_create_style(arg) if arg.is_a?(Style) Style.list[arg.name] || Style.index(arg) elsif arg.is_a?(::String) && arg =~ /^\e\[/ # arg is a code styles = Style.code_index[arg] if styles styles.first else Style.new(code: arg) end elsif Style.list[arg] Style.list[arg] elsif HighLine.color_scheme && HighLine.color_scheme[arg] HighLine.color_scheme[arg] elsif arg.is_a?(Hash) Style.new(arg) elsif arg.to_s.downcase =~ /^rgb_([a-f0-9]{6})$/ Style.rgb(Regexp.last_match(1)) elsif arg.to_s.downcase =~ /^on_rgb_([a-f0-9]{6})$/ Style.rgb(Regexp.last_match(1)).on else raise NameError, "#{arg.inspect} is not a defined Style" end end # Find a Style list or create a new one. # @param args [Array] an Array of Symbols of each style # that will be on the style list. # @return [Style] Style list # @example Creating a Style list of the combined RED and BOLD styles. # style_list = HighLine.find_or_create_style_list(:red, :bold) def self.find_or_create_style_list(*args) name = args Style.list[name] || Style.new(list: args) end # ANSI styles to be used by HighLine. class Style # Index the given style. # Uses @code_index (Hash) as repository. # @param style [Style] # @return [Style] the given style def self.index(style) if style.name @styles ||= {} @styles[style.name] = style end unless style.list @code_index ||= {} @code_index[style.code] ||= [] @code_index[style.code].reject! do |indexed_style| indexed_style.name == style.name end @code_index[style.code] << style end style end # Clear all custom Styles, restoring the Style index to # builtin styles only. # @return [void] def self.clear_index # reset to builtin only styles @styles = list.select { |_name, style| style.builtin } @code_index = {} @styles.each_value { |style| index(style) } end # Converts all given color codes to hexadecimal and # join them in a single string. If any given color code # is already a String, doesn't perform any convertion. # # @param colors [Array] color codes # @return [String] all color codes joined # @example # HighLine::Style.rgb_hex(9, 10, "11") # => "090a11" def self.rgb_hex(*colors) colors.map do |color| color.is_a?(Numeric) ? format("%02x", color) : color.to_s end.join end # Split an rgb code string into its 3 numerical compounds. # @param hex [String] rgb code string like "010F0F" # @return [Array] numerical compounds like [1, 15, 15] # @example # HighLine::Style.rgb_parts("090A0B") # => [9, 10, 11] def self.rgb_parts(hex) hex.scan(/../).map { |part| part.to_i(16) } end # Search for or create a new Style from the colors provided. # @param colors (see .rgb_hex) # @return [Style] a Style with the rgb colors provided. # @example Creating a new Style based on rgb code # rgb_style = HighLine::Style.rgb(9, 10, 11) # # rgb_style.name # => :rgb_090a0b # rgb_style.code # => "\e[38;5;16m" # rgb_style.rgb # => [9, 10, 11] # def self.rgb(*colors) hex = rgb_hex(*colors) name = ("rgb_" + hex).to_sym style = list[name] return style if style parts = rgb_parts(hex) new(name: name, code: "\e[38;5;#{rgb_number(parts)}m", rgb: parts) end # Returns the rgb number to be used as escape code on ANSI terminals. # @param parts [Array] three numerical codes for red, green # and blue # @return [Numeric] to be used as escape code on ANSI terminals def self.rgb_number(*parts) parts = parts.flatten 16 + parts.reduce(0) do |kode, part| kode * 6 + (part / 256.0 * 6.0).floor end end # From an ANSI number (color escape code), craft an 'rgb_hex' code of it # @param ansi_number [Integer] ANSI escape code # @return [String] all color codes joined as {.rgb_hex} def self.ansi_rgb_to_hex(ansi_number) raise "Invalid ANSI rgb code #{ansi_number}" unless (16..231).cover?(ansi_number) parts = (ansi_number - 16). to_s(6). rjust(3, "0"). scan(/./). map { |d| (d.to_i * 255.0 / 6.0).ceil } rgb_hex(*parts) end # @return [Hash] list of all cached Styles def self.list @styles ||= {} end # @return [Hash] list of all cached Style codes def self.code_index @code_index ||= {} end # Remove any ANSI color escape sequence of the given String. # @param string [String] # @return [String] def self.uncolor(string) string.gsub(/\e\[\d+(;\d+)*m/, "") end # Style name # @return [Symbol] the name of the Style attr_reader :name # When a compound Style, returns a list of its components. # @return [Array] components of a Style list attr_reader :list # @return [Array] the three numerical rgb codes. Ex: [10, 12, 127] attr_accessor :rgb # @return [Boolean] true if the Style is builtin or not. attr_accessor :builtin # Single color/styles have :name, :code, :rgb (possibly), :builtin # Compound styles have :name, :list, :builtin # # @param defn [Hash] options for the Style to be created. def initialize(defn = {}) @definition = defn @name = defn[:name] @code = defn[:code] @rgb = defn[:rgb] @list = defn[:list] @builtin = defn[:builtin] if @rgb hex = self.class.rgb_hex(@rgb) @name ||= "rgb_" + hex elsif @list @name ||= @list end self.class.index self unless defn[:no_index] end # Duplicate current Style using the same definition used to create it. # @return [Style] duplicated Style def dup self.class.new(@definition) end # @return [Hash] the definition used to create this Style def to_hash @definition end # Uses the Style definition to add ANSI color escape codes # to a a given String # @param string [String] to be colored # @return [String] an ANSI colored string def color(string) code + string + HighLine::CLEAR end # @return [String] all codes of the Style list joined together # (if a Style list) # @return [String] the Style code def code if @list @list.map { |element| HighLine.Style(element).code }.join else @code end end # @return [Integer] the RED component of the rgb code def red @rgb && @rgb[0] end # @return [Integer] the GREEN component of the rgb code def green @rgb && @rgb[1] end # @return [Integer] the BLUE component of the rgb code def blue @rgb && @rgb[2] end # Duplicate Style with some minor changes # @param new_name [Symbol] # @param options [Hash] Style attributes to be changed # @return [Style] new Style with changed attributes def variant(new_name, options = {}) raise "Cannot create a variant of a style list (#{inspect})" if @list new_code = options[:code] || code if options[:increment] raise "Unexpected code in #{inspect}" unless new_code =~ /^(.*?)(\d+)(.*)/ new_code = Regexp.last_match(1) + (Regexp.last_match(2).to_i + options[:increment]).to_s + Regexp.last_match(3) end new_rgb = options[:rgb] || @rgb self.class.new(to_hash.merge(name: new_name, code: new_code, rgb: new_rgb)) end # Uses the color as background and return a new style. # @return [Style] def on new_name = ("on_" + @name.to_s).to_sym self.class.list[new_name] ||= variant(new_name, increment: 10) end # @return [Style] a brighter version of this Style def bright create_bright_variant(:bright) end # @return [Style] a lighter version of this Style def light create_bright_variant(:light) end private def create_bright_variant(variant_name) raise "Cannot create a #{name} variant of a style list (#{inspect})" if @list new_name = ("#{variant_name}_" + @name.to_s).to_sym new_rgb = if @rgb == [0, 0, 0] [128, 128, 128] else @rgb.map { |color| color.zero? ? 0 : [color + 128, 255].min } end find_style(new_name) || variant(new_name, increment: 60, rgb: new_rgb) end def find_style(name) self.class.list[name] end end end highline-2.0.3/lib/highline/template_renderer.rb000066400000000000000000000033131355001426500216670ustar00rootroot00000000000000# coding: utf-8 require "forwardable" class HighLine # Renders an erb template taking a {Question} and a {HighLine} instance # as context. class TemplateRenderer extend Forwardable def_delegators :@highline, :color, :list, :key def_delegators :@source, :answer_type, :prompt, :header, :answer # @return [ERB] ERB template being rendered attr_reader :template # @return [Question, Menu] Question instance used as context attr_reader :source # @return [HighLine] HighLine instance used as context attr_reader :highline # Initializes the TemplateRenderer object with its template and # HighLine and Question contexts. # # @param template [ERB] ERB template. # @param source [Question] question object. # @param highline [HighLine] HighLine instance. def initialize(template, source, highline) @template = template @source = source @highline = highline end # @return [String] rendered template def render template.result(binding) end # Returns an error message when the called method # is not available. # @return [String] error message. def method_missing(method, *args) "Method #{method} with args #{args.inspect} " \ "is not available on #{inspect}. " \ "Try #{methods(false).sort.inspect}" end # @return [Question, Menu] {#source} attribute. def menu source end # If some constant is missing at this TemplateRenderer instance, # get it from HighLine. Useful to get color and style contants. # @param name [Symbol] automatically passed constant's name as Symbol def self.const_missing(name) HighLine.const_get(name) end end end highline-2.0.3/lib/highline/terminal.rb000077500000000000000000000116431355001426500200110ustar00rootroot00000000000000# coding: utf-8 #-- # terminal.rb # # Originally created by James Edward Gray II on 2006-06-14 as # system_extensions.rb. # Copyright 2006 Gray Productions. All rights reserved. # # This is Free Software. See LICENSE and COPYING for details. require "highline/compatibility" class HighLine # Basic Terminal class which HighLine will direct # input and output to. # The specialized Terminals all decend from this HighLine::Terminal class class Terminal # Probe for and return a suitable Terminal instance # @param input [IO] input stream # @param output [IO] output stream def self.get_terminal(input, output) # First of all, probe for io/console begin require "io/console" require "highline/terminal/io_console" terminal = HighLine::Terminal::IOConsole.new(input, output) rescue LoadError require "highline/terminal/unix_stty" terminal = HighLine::Terminal::UnixStty.new(input, output) end terminal.initialize_system_extensions terminal end # @return [IO] input stream attr_reader :input # @return [IO] output stream attr_reader :output # Creates a terminal instance based on given input and output streams. # @param input [IO] input stream # @param output [IO] output stream def initialize(input, output) @input = input @output = output end # An initialization callback. # It is called by {.get_terminal}. def initialize_system_extensions; end # @return [Array] two value terminal # size like [columns, lines] def terminal_size [80, 24] end # Enter Raw No Echo mode. def raw_no_echo_mode; end # Yieds a block to be executed in Raw No Echo mode and # then restore the terminal state. def raw_no_echo_mode_exec raw_no_echo_mode yield ensure restore_mode end # Restore terminal to its default mode def restore_mode; end # Get one character from the terminal # @return [String] one character def get_character; end # rubocop:disable Naming/AccessorMethodName # Get one line from the terminal and format accordling. # Use readline if question has readline mode set. # @param question [HighLine::Question] # @param highline [HighLine] def get_line(question, highline) raw_answer = if question.readline get_line_with_readline(question, highline) else get_line_default(highline) end question.format_answer(raw_answer) end # Get one line using #readline_read # @param (see #get_line) def get_line_with_readline(question, highline) require "readline" # load only if needed raw_answer = readline_read(question) if !raw_answer && highline.track_eof? raise EOFError, "The input stream is exhausted." end raw_answer || "" end # Use readline to read one line # @param question [HighLine::Question] question from where to get # autocomplete candidate strings def readline_read(question) # prep auto-completion unless question.selection.empty? Readline.completion_proc = lambda do |str| question.selection.grep(/\A#{Regexp.escape(str)}/) end end # work-around ugly readline() warnings old_verbose = $VERBOSE $VERBOSE = nil raw_answer = run_preserving_stty do Readline.readline("", true) end $VERBOSE = old_verbose raw_answer end # Get one line from terminal using default #gets method. # @param highline (see #get_line) def get_line_default(highline) raise EOFError, "The input stream is exhausted." if highline.track_eof? && highline.input.eof? highline.input.gets end # @!group Enviroment queries # Running on JRuby? def jruby? defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby" end # Running on Rubinius? def rubinius? defined?(RUBY_ENGINE) && RUBY_ENGINE == "rbx" end # Running on Windows? def windows? defined?(RUBY_PLATFORM) && (RUBY_PLATFORM =~ /mswin|mingw|cygwin/) end # @!endgroup # Returns the class name as String. Useful for debuggin. # @return [String] class name. Ex: "HighLine::Terminal::IOConsole" def character_mode self.class.name end private # Yield a block using stty shell commands to preserve the terminal state. def run_preserving_stty save_stty yield ensure restore_stty end # Saves terminal state using shell stty command. def save_stty @stty_save = begin `stty -g`.chomp rescue StandardError nil end end # Restores terminal state using shell stty command. def restore_stty system("stty", @stty_save) if @stty_save end end end highline-2.0.3/lib/highline/terminal/000077500000000000000000000000001355001426500174545ustar00rootroot00000000000000highline-2.0.3/lib/highline/terminal/io_console.rb000066400000000000000000000015151355001426500221340ustar00rootroot00000000000000# frozen_string_literal: true class HighLine class Terminal # io/console option for HighLine::Terminal. # It's the most used terminal. # TODO: We're rescuing when not a terminal. # We should make a more robust implementation. class IOConsole < Terminal # (see Terminal#terminal_size) def terminal_size output.winsize.reverse rescue Errno::ENOTTY end # (see Terminal#raw_no_echo_mode) def raw_no_echo_mode input.echo = false rescue Errno::ENOTTY end # (see Terminal#restore_mode) def restore_mode input.echo = true rescue Errno::ENOTTY end # (see Terminal#get_character) def get_character input.getch # from ruby io/console rescue Errno::ENOTTY input.getc end end end end highline-2.0.3/lib/highline/terminal/ncurses.rb000066400000000000000000000015151355001426500214650ustar00rootroot00000000000000# coding: utf-8 class HighLine class Terminal # NCurses HighLine::Terminal # @note Code migrated +UNTESTED+ from the old code base to the new # terminal api. class NCurses < Terminal require "ffi-ncurses" # (see Terminal#raw_no_echo_mode) def raw_no_echo_mode FFI::NCurses.initscr FFI::NCurses.cbreak end # (see Terminal#restore_mode) def restore_mode FFI::NCurses.endwin end # # (see Terminal#terminal_size) # A ncurses savvy method to fetch the console columns, and rows. # def terminal_size size = [80, 40] FFI::NCurses.initscr begin size = FFI::NCurses.getmaxyx(FFI::NCurses.stdscr).reverse ensure FFI::NCurses.endwin end size end end end end highline-2.0.3/lib/highline/terminal/unix_stty.rb000066400000000000000000000025421355001426500220520ustar00rootroot00000000000000# coding: utf-8 class HighLine class Terminal # HighLine::Terminal option that uses external "stty" program # to control terminal options. class UnixStty < Terminal # A Unix savvy method using stty to fetch the console columns, and rows. # ... stty does not work in JRuby # @return (see Terminal#terminal_size) def terminal_size begin require "io/console" winsize = begin IO.console.winsize.reverse rescue NoMethodError nil end return winsize if winsize rescue LoadError end if /solaris/ =~ RUBY_PLATFORM && `stty` =~ /\brows = (\d+).*\bcolumns = (\d+)/ [Regexp.last_match(2), Regexp.last_match(1)].map(&:to_i) elsif `stty size` =~ /^(\d+)\s(\d+)$/ [Regexp.last_match(2).to_i, Regexp.last_match(1).to_i] else [80, 24] end end # (see Terminal#raw_no_echo_mode) def raw_no_echo_mode @state = `stty -g` system "stty raw -echo -icanon isig" end # (see Terminal#restore_mode) def restore_mode system "stty #{@state}" print "\r" end # (see Terminal#get_character) def get_character(input = STDIN) input.getc end end end end highline-2.0.3/lib/highline/version.rb000066400000000000000000000001511355001426500176500ustar00rootroot00000000000000# coding: utf-8 class HighLine # The version of the installed library. VERSION = "2.0.3".freeze end highline-2.0.3/lib/highline/wrapper.rb000066400000000000000000000032441355001426500176510ustar00rootroot00000000000000# coding: utf-8 require "English" class HighLine # A simple Wrapper module that is aware of ANSI escape codes. # It compensates for the ANSI escape codes so it works on the # actual (visual) line length. module Wrapper # # Wrap a sequence of _lines_ at _wrap_at_ characters per line. Existing # newlines will not be affected by this process, but additional newlines # may be added. # # @param text [String] text to be wrapped # @param wrap_at [#to_i] column count to wrap the text into def self.wrap(text, wrap_at) return text unless wrap_at wrap_at = Integer(wrap_at) wrapped = [] text.each_line do |line| # take into account color escape sequences when wrapping wrap_at += (line.length - actual_length(line)) while line =~ /([^\n]{#{wrap_at + 1},})/ search = Regexp.last_match(1).dup replace = Regexp.last_match(1).dup index = replace.rindex(" ", wrap_at) if index replace[index, 1] = "\n" replace.sub!(/\n[ \t]+/, "\n") line.sub!(search, replace) else line[$LAST_MATCH_INFO.begin(1) + wrap_at, 0] = "\n" end end wrapped << line end wrapped.join end # # Returns the length of the passed +string_with_escapes+, minus and color # sequence escapes. # # @param string_with_escapes [String] any ANSI colored String # @return [Integer] length based on the visual size of the String # (without the escape codes) def self.actual_length(string_with_escapes) string_with_escapes.to_s.gsub(/\e\[\d{1,2}m/, "").length end end end highline-2.0.3/site/000077500000000000000000000000001355001426500142505ustar00rootroot00000000000000highline-2.0.3/site/.cvsignore000066400000000000000000000000121355001426500162410ustar00rootroot00000000000000.DS_Store highline-2.0.3/site/highline.css000066400000000000000000000016551355001426500165600ustar00rootroot00000000000000body { margin: 0; padding: 0; text-align: center; background: #abc; font: 13px Trebuchet MS, Lucida Sans Unicode, Arial, sans-serif; } #container { margin: 0 auto; margin-top: 10px; position: relative; width: 785px; background: #fff repeat; padding-top: 20px; } #container #header { padding-top: 20px; height: 141px; width: 785px; background: url(images/logo.png) center no-repeat; } #container #content { text-align: left; margin-left: 230px; width: 500px; } #container #sidebar { text-align: left; position: absolute; top: 181px; left: 0; width: 170px; margin-right: 25px; } #container #footer { font: 9px Arial; padding: 4px; border-top: 1px solid; color: #aaa; } ul { list-style: none; } li { border-bottom: 1px solid #e8e8e8; padding-top: 10px; } a { text-decoration: none; color: #15528a; } pre { padding: 5px; padding-left: 20px; font: 11px Courier; } highline-2.0.3/site/images/000077500000000000000000000000001355001426500155155ustar00rootroot00000000000000highline-2.0.3/site/images/logo.png000066400000000000000000004226071355001426500171760ustar00rootroot00000000000000PNG  IHDRbKGD pHYs  tIME"l IDATxܽMm]v4\k}νꫲ]+DP`$#HAK @BRDn"H9VFb!-BH *W.31){kIcε}+SOzzw~Zc9Ƙ?__YUD䭜+UêgE'OѧTmUU5Q""?+"7ϫ}nkmsW_zNoޫmE}wwk} UE̼]_rsDy~}o#" h@ "A1a8=[?I|)|۾y N9BPif&;ޯBE k{_ 8⭇W8F$+M80< yWo0bHyY@Qd@f>?BT! tY]Jwlvľ׬1ZJAkq{]b~_1{Q>_sb벪2Hvmֵ_JL[ -b |DWC7-|7!"Wa`}{ T"f_lxGM>*z`mڽcG{_gkj\e@ 1ss@RFfep)m}DɈLFtٸ`?oD$+'Og<#1h&w-?B d<>$NUF" mL pR FDq~ -3(3T x !H )!< NW9#O3J sF|/('Sx"\"ʄ,@ k7ЪPN(<`. ξ D ë3>DiRFcAdB+8T';A( RTW01 "@.!Zp~(L)Tb*( CLj8 v*~`^Q(h")y!`ߗģY9C ; 0t:+T+bfƨT7 R9xa-XzQ}o>b U+Fu!ہ2^wfVZJF !zƁS 7fCrT Y 'V1 B덈0 1g-n_eOHHC^BbrSVPjTP"`*:[k8(ms-*;΂6ֿJܬGvroLߢ an.I]QI{{v5_o m{ϴi)8VRK{/HJZ8nA2mN%-k#;h߳YagR;=AE)w #/<]G?_"VU(OcE֖2[@K$ 则a#͖>bxoѱ{7{=&Y'[0}m3"c@r%pP}XAӄ> .G 8"dgֈdyX@{u;qL2cpP',E>-G?xF#RqĊa4`l.{M)9RD 10D לR|} R0_+_GHv?%g((3PXqcZ@k˱1t6v A"-z=]Cl8;}Pԥ гi<{@:u<^X6bt%od9`D}e^A3-د\28 80gg؃K ɇ*35rE\QJc+Hծ&uH$.~C! 1s?5WoH:WXtWH^!kڎ*a$ @E}XPl M#Z7 ]_O?_ꎿD~}E]mQo݃JVs/.UT|G w uLJ-RETFkrvOQ [ Rj "Dsw [7žկ@p@D ϸ}CdDŽq l:@ (P)feLYQ!b 4]Iq>'S*5>|4y "W(2(hcVƜO]qy(3|>Ɯ3i6I@(N?|LrA{o40lbbL tiBEM/98-(9#_'\^] Hl-:A kUKq3Z7bEZn$YBU@)oU3huՑ{zvj^F@x>gHQ!"!!"05ϠB^! ;31%lÀ33t>#y +(ץ(2lV0L~;^ N!AЂR2J O [`e{ໂwíAde-&2^q==SivT>:ҿ=wOw d7c9Rb@n-Y.6М%N#Bȗ"" ߱Mi[ٵ&a _g+4$ -@qfzjqaZb.fcA0.!#ς˄g::>SbJ%[:FP02!Q0d7 JVFJ1t-Pyvq$< 4\g{@7+Dc$JvxrYdUAFUt=Cq~0o$"Kۘu" 9пV)6KX)0A|Vv e$gDxڻ^;Uj" 'g VIথS^XKӳ,U9 ;-W؁NH1x*- [KB@%v U^$Y*INjN"Y4տU*׵Ѽ ^tWLyOJ'i_kW{ܐƊ=vS*n9]='O?ܿ!NU }W+j:imw{έaˌn5g/f- +_2h"g`\G戡~K7fy)_͒<[QBG  Tvab!O R8AI Hego E0W23øt 0bJI<4z9!"8btNLׯQ"<+>D(Ą(aXq~~?!g˸?`s:a3Ha2&'2#j 쌠̝]&I!ugm 3HPoK6:%-b̹r \uPeyq&i1u7 PLZoV,yPe7@IMZZEI /@R((%B`W%1Q\IM5Oe(Y'M6-YJJ dwf Y!S"89(Y;,䭌bqlܐm pv2׷+K@Ά ;U(3J\J}4w4N+6Dht:vVD0ճpSAwEʘ /tQ+u\;N^{u+;A ֙FJAp`:I1p\ݎFxD}]]}R˭|QQI馏E? ?cG#t[ -S[a%vo߳<8߂ӽM#^䛂׾%H;1èu -buCG21!x6!0&'diP6OEr3f̈9rc)g"1NXaל-# S:-~t;iR'<#+. H)zE`&D6V] Rkz+4!q8 X !iyBWOͨ Qp)B'0 S@z-H0M@}La'LӄRfcuc'6Q:! t/eB/f4#*,[\J@kƾ ~'Koik]{5#_+c%^Akmbriz{{ʇ~@Qe66FYv<*#KR fb̲*;]dY&xfoJR2Y4Djj;ciiUyLB3DWHu6[F]_6k'A5@4XzTB˕ZK7N cUBM(.F%_ !ĔLVD b-O~M^ ?TiѷDJfM6 F6,'_XWZ/3IYV½nAKNi  IDATT u|@푕0 t7 5gj-lVfB=;8!ٸ"eWK=>П!wW>*_7"NUpR3"Gvo=`5@v*Kɓ`L^yB/b=BI&YO14F!@4cgP0IM 1{O>1UU옧d)!~τj) HCBL#b)XWlZfH) ,c4HK]: ?y9ئQZBKV@DuoN\vo)0봅+H: 3k š-uy ~MRi'Km$uK+YA]Vg0)MMpZh)*]Z ji*JK<A2' 9#\;˽C2j2Rc1+|X=*]teFJ>%ے_]\ } Eծ8Ч8"g1NkmPR k3V;]cܿ/ Q"=vpmY#O^9s1VT{҈-Sexg !T}d}:YFgPdLڧ39L} Gvauim;ba1160=*b(FƷA<4#g4>b[^p:pDfa /yfjtcicNip=R~ŝ>zuiԬҞRTԓЊ8 W2Ku Oj7cz|r3&iDŽ/yrDԒ2)5wk*B RZ9]̨M;+KT nhHb N01H\9i0 &ҮbHDȞTU}I7I:ϨOD?o|MY? 3{K{`{я/Iu=Cct{r!VdMbq:*D}=Q 힩Mu{ad7i?"!|`Y/#e{gS55.IZ+jN:UUQ+ V̫OL1My}5b*"Ԇ+y޺XѬK&})<͘+斤U=BZbTNu&7K:^e)fnulљCLvP@PtBh_=%Zc=mz ʁoFhͱ/ nogw瘯ľ.6=g7P]MŨ_.k{ ~kIm-yDh `9$ftmpZr7EDmt%j7z`T?V)]UKxQ2﷣o^r[}XBRZv$&︧a~=OkYO)%Gi/go8֛䖃>{7a_Ġ)01Q!w`f60kI8 RL`>A <>Z&:&*!Dh.̱*o#tPiʘfӈӌ2M(O]6If L>ghP i0 \ èu\"-m  " I<#@9\.Efܭl\`RCyי!E@P 3( 4xCd`b6VUQ @X<=_D@eK|hlue.ȥC[ά6hٴ7Rɝɯ(;y][?ӀFP˝ÒF:wʈVM ]RĒjk$ޯI6YlW|Ύbp k1K^n?7ŏZH`r.nv y)&uRbQWY7FNQy-</Q1YrΜ'0 [foyJ@p:RO/.J*< J x}FEsy y6p1uXl3ͤKZ1HB[QdQ 珞l֭ vdP5Q|5p_=G72)赎4{NߗQG:#&w[0|*+2}M}JM<7,}W`OzrThmAXG$9wbs/5C:CU\[mNd,x5d6!C'Oldcb HHc$⦋5&5x/sAW і0 )Y<:4 e*x~)1bEM:P-fT<"2o0r)gE㫙2!ds,؀,%"kqsd>%0h,L1sFlHkbI*fC p<%4h@p]F-f(x D7U9#dRh]b{K7q1ӀEpͯ4IbZE涶}[llAQ3Ѯ2]P3ʍ1hE^DMcFnTN:_FRdm uXȋҚ4Ŋ`gS 6͔܌>u3ڰ Pt{.E OZ, ň12@_*Rg\DewY{?L{41Np;u.>͚lSbDAB0bϋiTAAlSY~Iw<^l[zLKZA3]ςJ})K7Xye"M'r]3aٺ;K;;uG`c^92Vd v J宵jg,7 H+P:Iq-A7HdϪ!Nwy}{{婿(|# KYGͶץǸ{Z>~.cYgӾqR M^+3. &yeU(̵I Jȳˌ_Muϗ y )|pN>=k\/&Y _)(Ⱦhc'Fi9Ԓlp:[0$ oNՌkSDq~'`y2-3 s Cnv,dauZ?$/ b44&!_:I]ִgw1xp >iَKuw#&5<[4n.uI\; ;pxѿFCj l%mznRnF/蒜ҕ=Dkdh"]W17{3X MZ{u+:iOݪQ~] 8?Ao2־=MGEjJl|3IJ?a_⃪9?DD鞖H3z-SU1Jq̣$-[^];+}"*z*Ťl1ao\v2xɣNy/-XGY۶5uݷöHc]#/rCf #kۣe Ւ,(P8)$iu1+T ^8+R!E|-Lӌi7\AZ[ " B p/d/2ᙁ<`d|tyQ1L'h;,q„a<)udp4(2e l2+!$@9%(8>^J07K.Hg^dl9p }<%YTF|2<[ZϧLk5 Z7j& xFL@F6Uw d: %[ cPƳaZw򢑙|MiƩ4W`Z%a`nR+@AWL&Wo\RB"#!*nb$kpJ7Zd @X7>BK3wiIO*IZq <ic,EP @i,mga;R Z3 d)<]Zr1.@}5Yw 1f:LCXy4KP)TUҐ,IʧV'ZZN.sQpHȳڠ:"N.MB!R e|y e;i^f-cMo*ZdRZ׍e5 Xz ~vÓZ:]8Ćہ,~=Z/ndDܪ3Ǹ𼺶(uAY Nrd6XmM䕟B )A7NrfVY"Ji  %hDD?DϏ?4ڱt7nu[,~UʲYX6fKuSs2ˤYW_c=GbѾt%LIm(HAFZbhEzL~j& IҺ"1a1.Shi{EUq+tWBɕpҵӿ݄]ǁ&쁑}RE4 hDRP(25`2u鐴p\]nqe=F%Չ2M9v.ӕw&\. k;JhXGii%~H+2jCL-@nuͥu,g`u-&P n(3kMej4uհ{~g } _~~sORpπ&o$߂=f^Vo8^m]oafWytyu;UD1gHQzMGї4[C꽮ͽ]t{#1qn"MTV-fT:9c*Ú#H} IDATA޲ѫٵ}A"Wi.yItO#cGǪ{aͦBDFjz4*l6n8 zan$U<+FNM^6^d*E!1 ;lh^|Rjc/׌:Uڲ&mi^GJ*dwD%(JZḓ='jNu@KT?ɻnZ]~ Mj.o 6 vwfj7E;˪sTqIj-mrjڦjKb;f^$jQlGTzet~v \oyn > o>pWKz̍GK%~T`鷏!Z4=KϾ\1=pGë򕪃% ^w/%_cw-\li\Q:gJYF 6mڜksU`]ە%ېVLAIۘwbB e4(cEcLRø5@)z1%PxI Z 1&cs1]tΡh ul<,~v@Y*tv-pՎ,(dS0C$jāq@ Cɇ'8[Q4 ѴK5 =0dB~|  ,̈́HR`n LȬ渙>լMm_XriՆ6/=og9 Ygс6L{XKMHW ~(7SMCLVv~)9 ʻxě^/@Wmeaad"@uB]a)bVHSrnJ";$Sct])+C~T/U1pv7 1Goh=IW ФÞ1{Ɯt n}~)6{J)imR yލA-pߓ[0kl?V^{Xw#3䔲|j 0\Պߐ-3JDfAPk T@Rg!: lX4ZpMI穁fc UslBw7H6/٦erݬKm1lCuL][`28+-g:Răkt3J) NcGdvjh4<6$'W Pf+A3!\};6siiN4*@JW (Ęz:ITPc!!vMCWK)"#'> 2maӾJtԭ]Y~3\)kqw S4@5iDcP'lK'QWvwWj\j?&A|P3z|@nk}r _uԱ} 40YXi61]3 c:ٹ!B>7ڀb̕t#]W%KQ˙J{֨̈́k>ۤl6 8YwlSqi/}#O DK8m~>K`kG3#.=!mni9ߩs9s.Tg#s"flpa'R+,r4vT]\r !: sÅADP{DxZl>u)jQ`g*Yn]i( $:g+֯8JDmiaKG\RSГa_&qur*u)hǣd& KMN>x4s5rs VQ:vP4- #}UpYߕu4奴]`a?uwnPv|ejbߝN^se TѫyvSJֹS{F(Ʃ=:yq*̿2r*Bu$/{N|>}ax]=|:P0rj2s\}Mu9#,!*^zd5ǭI P;#tᬩ`'15l^0 1( bbz%ֱ`۞?bD]ꩩ&hSGӎ4by<R߇m8JYҡ8:;{s.UMnnOj)"R3Q Pn=tK>,mm{C]L=XcZ=k'u?zk dX.ЍY.9*kuՂP-uti5}{^l,4߻# (Cč1t럧tyԯ4B:~9U#sm Dq8;iN]X>=uCV`TQ\D{n8\B=H^)Od0:Sz@|_;|~]^J[sTQh8朒InnI*V+&7R*uJpgz!$̣j2]y)vȉْBcQs"OaRuh?V(}UR s }(l^>JI#gJK\M#qW`zxk:6w\H',cU?MGҙʸ(J5f5lF%s.5|Y/GWEQ,y t}WWZx9 JZt6O8ws ktxjo6:߇\#Aх$kࣨĜ {NGW~3&v#婧dxBd9>|/~OIGdx_T;]uClw&J6X\`D 1ڶ'C,Vk7zߌS ;J#,+Oo(mHaHS8mA\E,yЪV >:*ntM]@EaE-y4!߆A:p{Ϊ  m?qKhmǶYJP;Sru,Q,Ҁn涏CT:jӶmZ@\{&  :Pӱ). T:?A2y LL8idG:`YvJ)Qdq*F4h%n,Q9kY[ ˞ i*^Ӷ^p>]FF, 𝬅rVBr8ne>~Bņ<%$!g_|TWu){a4] 9ՑצIO"CNut 9,WѴ{Noχ$|L{g!s<):31CWW Qտ藨I˝UWs,#!3|) /wSn<:]uu=SerS66zv{5yuܜ:,fx==Ż"h!7E7P2d R͚js@HL<7'FXna'i\hQafxPbQhSGV+[",kŲX׊aͮ v%ahiwg> WTIe$A =;^q+,en?s$  U@B )eQn)3 ol[h.wi&Fją%PԥQmb]i?>Lҳ5lB70Ij+VC UQIJxRGRf9eY7uY xG)p.G*h7B~Ss[ z # !L#~_N&Q7cqh&Jtj:rtac 9n}?7"h|tAhli}&U 6UJ ϸ w8]c\>t1lAr4*M?E۟Rr!%*;|xV9-DH{'ُQ"wWALq:W$gHҟ?9o W?:?\B _3ɴ0WԹpb_u\&w igyٌ|Gċۂv1h 3VܤdxGց"wrh* ۘ/Thhis^enZnnVtIt/V(ax zTבJQ %V9*7CMМEhf IDATnAqulq@uhe161;RR3se,}1Mhm'w@f k8._0 &Zoؾo =G@z\ ǂ>P<>ޱ>rY PV\`upQj]zU RZ! BRFKi@HXEM8l&ZJ jfT% ۳Ԋ ̐ SHӱwxwl'iT!?7u0C𐬸@JL&eYDv#ÐH߱[eAYUŒ:!%& @I' JS~3Ϣ(q4kz)=O3ġQ ,D!͖)3|ָyEޛ-5r\,SAeXZ3_\u8UؿGg=IG~U'5 }~_%Bc%>4ܹ|Q\QYzWDӜ\t3l\b=Qsw4zf1!ٸ]Y%Y3Fs4BbR-* ]wV7Wc,Nř#D)+`Y BkL1ŵQ r\dq2+W4DfEO3DqVR+F@V$mV[X܀{48$.Q*uEoW`LFQwq٘@3߾[zӂw9vw̱mߡ.̂(: xrMQ]ϲXmzs&4 }d[2H+KkX:/wۇ&$*ѯcfcUq_1=O+h:rן, n aoxba0o|ԍgۧC2m#2?K@@ dF[]?z|HTMm<ٱEFSnd 8u$/rlh>y;!<4"uJ>ARARqFf{{>i o2@8/VH_wh߀y=~LCR8`!J?Cf)ezrMrx<ݘ;Op G.{S:>WN09JEh5a {8}N|_^eG`H'X ~WNU`;\UAzr^!^c~9ER\*2̞%>W(@2-]Wl5}Es7@ 7IU'tgf\_ߝQg:Ё*S7\7N˃Q1d$6C,J797fEǨYLQiU n*o!7t>!EM; ʆ,}>RF4rBߨH9}s쵰siD~"*S&0F9 s)dz}us%YҞa~r -&j5+lЛ`oݍFKM_f*Nٞnmc'".5c#xSWjGNǩ R` $֑3ڌDؔ}#MRlY7 =wd` dq6QFc!"+ZV/؏v=DG>{X '#oBwHcY^=Zw"B]l:Q%iTXt$|h:0I~պQ93hq|Mi5{ T¾1 Z(x۔ghzݘa?1j/>"oQJd<&+Wk]%r5մ~ nf(zpa:\|˒+{Q:S@$iHk2N뤿k;'h$dL?{39K-!QDNN+$Œ zFTEd|=[t24Ytc2>T@:7+y֐Ld_v|NI]u֐d:I敟'UgvgC]ĝUazU?w]s0]|.64Ќ58O:eJ d;Wsq~?ߐJteLw/OS&9WCPUPwޙD/ݾVdY:X|8ZP#9oS`9}fCpqAX y^Hٹr~.h=ǡ:,z; sPSb?a> #<A`C@Nřx- Q'6@7V f1"e-;Շ Z&U{DR)źZPҾEzq?hy{FxށGZx< vH(k5ngMoY2 Œ,rF8ZT,5FL*8.543h">+-[=,ņOSr\4<%LfQdt+ \0 ͢eQ3''ܮ0.j:I % w+e?d)jYY2jC߻>T VF DRvԲBJ1ݓ4?zL)I{TeoG10m0J(ӁK ߀i}W_ƒX#|PĸS$OIa <<5b- jN%I?#Y\}eNͭ-j'4L UD+]TW?JVsb`J2sE* ?""/yuFh\TG~֦_YκWӃ+MzK9ί3$R;*PzZAwtAP.OB56e]G4|4XMdVm~?f2;kƸGE:eZ!dnwT/BKSZVfjDswIu0g^.u}HR+FWӒQGYk2f+fܒH43.; w~츆ᎊ/TQ+A:c*HKEo)(h{I.*Q:mPIFe/bJ>>=7s ib" aQXX@jVSCF -ώNٙI9&g4szަ._oDs6tn>g Irk[X Iy Z!SRPe0Ulo$e Z؞,rB?ȇ'.لCsmψ=>fO59 䟅Ҡ)]ag 9YC@Ƹih]Q7P2_Nߋ)]a `$T'Br ɛv@Ϛϝ_z8~^$"[ np<"NٙXEhLgnmިGj/"t%%9ZX>ȎE|R il,jv{}!10fz{*A/g!14LgFTgS745/aE̜ږEV P#-Ӑ# e`'rp [箛JҠ.ێSB" kS7`ՙ{{율SNhx3YoLM5 1r(NYTNS(Mp^<3hW-d(zݚ 0Jt: &%V0;w6js9*'fáRB+>d|כ7Sc?!t8cf^G0.ېߕ">b i"lo` CFٌF$?G .Kg_뻜LW˿bTyCL +`s<0b:ˌ] pH0 QxHHFԄיrQp@Vߗ*"GW2dswWl_Kg}ҞͳTR= wkqkgT;kl("J~?rsA}up:F>k^0s%R`{ʌ}f?HABw&}DYa}_M :3.ݜ&35/j߃K<]W(:2LVSOa5# X hBۃ@ RaA%5cxH ψ@#/C)gt8p^ Qw"UQ^h݈4~|#cD/EZ<r\hw xladmA}RǶ:&t 1l9Ç:q譡nomw]V(ʺ0Xd4>](T+ SٛQU(Q dTzLĸ_/ &BWyA 2Gvɷm~>q NJ}XL(ujYӾy0ij#RV0WYo/fX R>H|7{9-byh*<n{dOLRԠHCMз݋r?1vаnmBAŤbAiTj@]j*Sؒɽ-]}f^4 9랎F6hUg)9-WDFr1)1VbRH)hgB|+|$Oy-:9z0x_^˞ P)i(Iic  5[+%=^2=L\ٴg!X9R&vf= oW?}@w& =Շ}U_&Hq?W:;UWfۯ]U`ԫG* pF)zWZ ̝|2.ܹ?oXIJ c }f3^}\gޥ3OL¼Z%. RM:Ð=,R"\˅2;eĮGmǶG&yQ] ww9:tĽ̚@Iת<tX_R;eʎMo`3SWiƋ, pQ̇M46M/r'7< IDAT ^F1hYO?cZG[Ђ,f=3?1RABt?eeĤwl#HAPвZLqTK%fP G4Ci\:ra1K(ſ&#@mvLC~(XҪu0W;`>oO[g&uZ뤴x}ёQlF6&J3nO`7)9akdb :trw`kT&dfdBW;q{q8fžwrԞby ~ bw{SRԥBuFD/ε !N)Ⅷ;4a&=5XTSYƉyfVN:Xd/q2B6 Et#y8i|RB5f9pʁ-m]*!{qs2_twEګﺓP2Ǟi)wl :ү(L+} $mYJ?tTIh;5J~$=JQR3A8hB=S۹> 06?\ ) xнy6 ؋x%DF59y';cyְ O̢dE@`䗃sM"BΔĝj{l<Ӂ9晊;OmJ1*M 5 nv5JdpE,#mtm +yKk]y^q^dm7z&bRgLSvc"+1L"ϛ2 )hBOתR4컢 >&qMcņ ˴q-%fo?$et enP)`}Wf=Bu @P~ȧaQ91Ӑm>!8wϔ8% /E ڌaO:#>a̍`Qgu "d;[Ur%I3&*+]vU]Z]!xyW~{]+[pَא^|W_uiW\t5:dWҤ+C{B-Bmk(R{2u`(\dpThWJaF2b9Bqe8QvUP-x=wA _GRWp7݌>XԙNxnOl;#e]!(xvԺc}TŀO-9M;G;z}R^RI/򎑨A+ +T>?EJ(&^&:?~7Oh%ݻS#)> ^+#= Vѵ "NEW5/|`:z5J@ OK. *Ket:l2c |RA-~?S A8h]Eli֤kwH(^ر"z00wgѻuSULZYPcCEN=<3wRPuh:uԤ]"h Cc.(RGX΁-N1kgLkv+է% Z_;B/e׊VE,R{L?-|>Zf/"3~=Aeÿ) >:3t'9dp~ w|?=wwlŨP"AJs(%]:5Q ztݨS݌Vz%;҈8ϕ\E.E%lP¿чA7̑~Z2<iTi뿐O_xxhz阼 r ҳăSJLsJ&1UQ4i@pQP =Yf5.( ACZg4`<]cM 0^f'tL"(i;.)Th"04U5ihS)Az.#'e!Oi0<3xto&(q[#8wU'㊦rFuѯ }z"N~rWB^suxEB]C>.y`: wӃn_w(5JJӌF:Yɾ,FIlKet, zG1-| ;gX$csn d)wGiCxRPE{wV E4#90Y eDiZJRLR-q@S*C!A)HǞW45 6$(F։XXDTaźB8Ek)%P)^@ C)bT;;nvMw%g`LHkWOкgy@֙Rդ#Tf.ĺr4@IbwEzPV\3I`+R "y8W3uط} cv?*R&/j%F !2&+x9i) w%MoFuşAޡ]Wh膔1bDJ)l0?Aet(;]| x ]vtCmPPZ #4/ԣhO]uB e*X ̶ـ=t@~|+Scod`L \moYV,<@Do F2ؒSvA-JpUhho>Rte@;K?\xV9wsMN|V )lj{mz@ESh3D1'#Eݵ] z瑨fe&_CBcGeAWTC8$Ҝz>KEuC՗%_?xF==k-vd?B3Lh1uUk\'sW>sį W;J:t6WBuJg#;9'Wێ,/ga3U,mʹZG |౐pMM":t)pbΊx"K .car:Ǥb󤌏GskX [#N,ӓ\&BP2ǂV&T*0 $vXXTn=%Pd]hx &P M"5>['ɜ6:}s ljr"H̠'6XpJ-#gYSxN1h{gE}rdpKsADWU iLߞL\ }md;;:xC.cw X!̓MmڲII7ŧ\" W%2[ۊ V@lۆ1ZI]~A`3YV|W7?0օ,G7Id2 C2QS (u*m}=[@K#b09)YTN&٥tWtV[[AS6 ]/0kňUi*C?]eżtb8b H6XTq~xY*d{k9Yf0Djo=|&G#${UeU̩^cp*Mʤp! rL`HtdƐD L%{LgHAWA+_WWWW{U j_iH(wɯW\Czee6Ϟ_+Y~9:]5g9͕9zAֲy0kF}LGH"(]:w>w=,#kgl8ToFLCzVKOpڴm<5*a$lnݨx !boeM$ Jnޜ|u㌊3 f689kFs i 5.7/Y~QN#[;N뎵yD:ӣRXMte~mN-$fD!1#HHx0{~vp~ yK pƷoFZ}5zL/UUQ+ O$(3Y`R@|U<֚]JfoM&u,$X Q+ ThnnL+6c4}B|O01n2,޵(0/.?ٖe}D4F;ְA92j2;ѹ0 "';zo@W 1tVvL.9=JH+F:(M<>è̋^6([p)Z{sd0 9@Mgd (⍠by@7IRlN9^Fc] rLdE6qÈ MJ #7sՔs4bI:h񞀭2AL|>H8~"+8al[j:M:(&iSZ&5^42\~Ձ]a)Я~Ft4&Wɤ",OJ\ܥqW!\iͻχs9yl= Qug^<`(*cx4wWSqRAS-ޅ>$<<(@"xr:,[k=\j㑇9#qXaG(R j 3źGPQ@ԩ2<ޫBK\7bՍ}32G#Q!:@GpqC4Ħq=]k4óuc@"wRøⱮvTO?AqDx{5TXrݒ{<N,6n5$#y矠}X ;HLm2̡Q*Tz=<&[Dzmi4I]r(}i. 8%Hv.3i; }CAԡ;:\du% XN!\qX47c>BBH~oNL;2JvO۵W$L$%,GQ$[ɼ(߬CVv 8<b='CFaSLWz̘wsȿKJc3wJ#FֻT׻CƟ$a__ޮ^YZuÿķ=g`!O!۪kKY@Z' : pT+#xEE! /灩 Ck/Ӭ(qY7 *(_&! h%IdFwwlޠL`R"Fo.GepS{HB F UA&37ى+ﱰd9IиF/JiBA1i>gbzi,͓NzM' qߐ .phn5J̔`Ptpσr)Q AhC%%`Úf@Cs5ؔ0Y"zO *e1@Tݬ&]Kh{ömsAI+b a4&փ_@Jܰ}|;ުPM-]6kM`\VێwpϙϽyy5FڎmRG2Gp}rҽ>l"|B`]>yo_+I;S1 *IH]fs@!m|a0]v3ͅrh\bM9d .j B61J&LNb{tTC:/~%YyJ՟矛._u$U|mW;!Z){ﰌ W֯t_u^%SyiΓs{컠 tA7KL&21@ uDFtcıpa A-P1 9d"%''plnMT@Z\B#{ic~q,$6h3Ğl6]x[XF(K}kc|eϞ\UYյ!p5 ͑a#s*Ϧk̸G/ g(&7/~!RYpAG"g}L4iXPD46"2 wMhgBVcG ݹ݄XkH%lٲy'.ɏpeȉ.(61j2 IDAT}ov(`m۰=V,*.!;ШV%~Ov]`fmzн Bs6ʐ7a4| Kطelfm{"1{9̾ 0@S\kFž4C@3RƄwĉ#l:#䌲vʚI)0)r5ý7g܇I5;ߟx^Dd?.X˂WSH.kqwm\EE6W,AhijތV o4Gg/Z/T5rl tbEWB6(!$XJyjw#C$'ĂeP* F kfFL伪dZlYW1 ``6kD TH*!C|KJ TT?I%,8"6xE .9M22%stU*LOsc&j{(;=5^6u):i*ENZv| 2="T$eيF!UHڄR.U `RDR b~&yR pLO E?A.=Sk(*a[G5Qz;1KxI>ͫH*tԵᇲVkw udGd#A62Іղ|-x 厼 p8D3 ,L5zG<)t:B`As(VQ˛n3OA6V$0^е` P@æWNN4E6A/dyj(vn&mx:G޺Vjr>ce {"cB%\œC69N,&n!P#@H#ZU)D !hFiA>s&M@:nIŇDPF5 8k*ʎ6"PȖ>Z$GZ|9_ʊM;LA|&," }ٗo%BtT[5U"Q|aJ)1(&;=gHd5e.q g*M[ 8]R#Rt̥O$Oꌎa\ 쏛Qz1JTq>IQI 97W"As=605rǵ6^ WlJT8 gql8b01^^b םR惓KޛbEM4NSsЖ;&"ŵ.,t6i*PEpm@2Z} g͇~12c`n.мXkZxŸ`yOp`J},&T D:.\Yӝbը'B1VEȅ{jPRˤ `` n&!F #a8hUTa $~ +/9Q=% E@f/AԐg-ecm%y*4`N(JiP 萓pc+5)\2ZT.1c aˇ*@'bL\$װ4x C_ Tyrg!L2.UX.%. "~FbRhӤ߻<|٥6K4&IVKqs=n6J9y. (y5P@ЗPxuFbF=9ް2=Pa R**RTڊI'6buX9ѢlxI˖"B- I\xdة_&FUk%4(LǤʅ~R,` ?`2٩wUu ZȆQQUb R"QwXB*Cy0btu֩p<؄dl.c.ƨVʡa}L6(lT$M7>($ʊ+U#xH!"N1ʤpfgD ͛a*cXip7n.Z֬ÞRC)?.5_X@ك17 -R 1Ht$3Ul@"1RNC.OQK@%[CҲ>MTv)oo)$.|II.TQ5Fu^d}e\\Ki ]r)y$$$υWGq2qq30Z&'e`G朇kH_R y;/N  uY(?c;M`l[Yx߅PZtїN9f<'1{StK$PLC8Aix \LʢB^T:M Șug#D/ǡf(RcdNKPCM {%l𧘒:,Cp}<)&ax<6S TE*ۛ^VG&@'/:(Lbr׌?}PFPڢ*вdcc:nWpKټp&mַkNǠc4jGe;ڢg+tѝ"P@ĥ@;47h!tE: MB1^I/vX\(>FsA(&7J1'ʴؤMi TvC;;N/ ,cH-lU()u4˂TN!jse:0o#.P"eft"hN_ۮ߳DBhC# Az0DL*$$)U Nځ :&!O ZL) j~>Eź*QcH[%CM4cds!"Z) =1H.JVB*3Fk'i'!!G+'*ND\g)迍KKD4nf&/2I19|jGv\jRX ~0—'MbObǂ}pp`'qD7 pGqPu,.ƶ W_kIq ;/+ii!0yS9X6pq粪$51 )۫e0c B: όc4ԑ t( N)LE"q%a Z*+v:myjA+WJ _Y$ ͻ E*"eYҮtDƻ1q5i Bm$$(F jLO\Su[;ZQKg}Ng'Gj" (ŻDhy:%ZY̤ l(xxr0rhA@'υ4͡??XS},[6ΞAXkQu* u n7O<[te[?Y8'2}֚|ފ@"[³sgCqLr659͕Mto!sm%_\N>we I *l{7.DCKbUTpR1>Z(PQ)2Rg9")lꐂԈ#O}N cgR/CjMrRѼt,+ )e4H%:_+.VXΔ-f 53ҡsAbdk)U Cڈ0%$%8z|A7**&@Q.?# Qz6I)ryR0ЅȖ;KV=SGB) qZI8Z8dh!|I1rQ$+)dcd4̌&*- jnDVħ'uijx򰶒<;DI1wnvX>vܹ99wi !*c`;7:=yfU˦f0?ϛ٬F7zDtMb4&)TgR/r )Em|&Ibb_Qr Hm4$H5|~RJU;ഭA-EKfҐۜEa%*A @-BaS闆lLDeO: 1u"d}w-Ld׋%'91aWI0$h PtV ;^Dׯ HZIXxL*$ kr HتWt}ә):Ef1 ?Ca.qH| H@bl)dYcHG"ɫDA.Uf*2I^YG  8wfggUW]5Q2!{YDsiٳgqW/zCqGώ?v̙3E]׸ZAOO~z=lٲELm.Ҋ1/J>}N©S~z\zPǷ?O`ƍǩS077㢋.`m<q8˥< m9pgOz`0;Be{0NՅyI. a NXoHS) UZqа#]4 UU۝°vP;VVjpn5fEܴfh_eL 1Rx `aYEЦ5go֓ b F-WqJD4Q醇5@ۨsᛰ`hjb[a4؁"dIPIIs@+ãa.Pjx磁h|ĚN2 A}"F}%P';~- <>@C52)hd"5k4DXA™*i|etQ#FHP^/x%`!Q]x]-B1|+Eڌt `P%zK+]te3sYtwrHAo_8E""/40$:cmy 6y1 A[gƽ)Y2BA& pY|(mҪ f .N љ^Nw 41QFd elI`M*Jv\&f3{SOY;"ÈF-eA5i.U[c L~lye*&%(kyc/qL:)37?,]$1 r.t!y<0kŕW^9gy< ^{5]'r^$uZO>ĉ˿ O 2y /??ꫯ"Ԣx饗`?#^z(lTAH覍 %e,@t̽hjuݤ "|d9he(s0 hMdI>c"N `])bZFgui3!E1%7V6hBv͎.hE^\0HT C~ѿY+Z&l9FWJLIV"0lA JokN,:Z<9|'IK^z%;v _~"[oŏc;v,.zD-j~x衇PK|8bs\:죋gk+_=wbKNj9.-[6X3.|==ك|Zk֮]~nٲeG?{ iӦ.bo6>O/zPo30.%u>~F6~t=bFe#͉79 ``C@3-ƚĆ3G";cdCjHT^ʿYkIkQDW{j 5J @LhBm+X+ ha&cH6yY*L$^EM;,RWNUU< ` ,ZԩR- (*IfC q7>`8GPN6VF8`@.ʹ@{.UX]nM,!p;~?  0Znŷ (S,+ps,Prra(V@XLx&R-Ytt4%j&PĘr[iĮ3jT%i=D+PʔY !]!k]8Мq3(ئ?_tP^ÛG=N" 7^ڊFc֬Ywx׻ޅUVavv_W{n:u ^{-9ٟfff{xGK﮻¯w^}'NWUk㦛n?v؁//_{Ap8q7˗/Gi ={i<K_Dw_eXk/vJѣv￿5YMؘ$f׀'4'c[.`E` 0&JC2؊ ZNk, |vk^$^(O˄ьA( TLdC '*ͅt9pd?tp^xį9Bp m}dڡ2'{UqZ$RKYH)EP%DEu3b$ s5 4H۔94jX@kHJIl%H7jA l|@!q磄gq0Tkc Ѥ |!&(PCDS2׎sC4uWaJ;^RpA#>ssLs<:UOT| E,w#SZB7fȒ;p hPY+foyA{7QZe45)I#69to)xqQ@)sLkA* *0\T SA兽hY/s"Vy)-ţL>u~A̦T U$ '=1TIX r̿%F,Lu)IoAZC|=J !Jƅ1eVh>橃/ c24t ?wؙYl'xfb'WIjՒHy:(O-tRM*#s~Q]cR2+$c:21˴I1("h`Lg!Q)qpڸq_C<\{i;v :RطoN<*"/bXkl2r-뮻@D8z(>իWq KLS Y_Q%4eeOAJ#:vvDӱ:5RԨ6ҳ :XkUA+9KLB$"Zk=z BxGpYc=z+V,7 ֭ç>) lݺvg?YܰFSb.PFZA+C gưCڪtmQf U8)1gZ %SY-XI,])x"hcaa%3!-RRyg:k6G]~!๋ T\$(+6r (P;Jq#&a4 }pBB[. C')m*V7%롬5bZFzbGڲ0bqS7),&~7PjY^T&TJ+_T3N'J^Ƒ(KbauS>5 l2)h!63v6xJQ1[ҨKh:LMMC9J qI_SlW;b<]Ĵ6ؘ,,~RoH/ʆShkS*%-3lY&U A?;.Q|R*/PJᩧZ?!%ttu/R{w}k8{,a2h*jժdl2<Әx㍰ֶ~Ǥ]w֮]ǁ/2^}U8pbŊ$Yr%6l؀n3gp (penZlٲw}w sssعs'?g3$6UUajjj4b)q)u8sM8:In 8y$ci&n7 7~aQSU.rl߾=v3<{{}݇7.ҿǟ B!P!uH+#ŐGz.w| K]my\ 4!h˩ߚ<,]a;'Qu@8r)Yv!UR + * 2C&UqP̎yJ̘ҧ*zڣ!ЪB  $f::}O}ql|Z9Erj(:X-L2Vu,BSh:uIuby!S0[^BC0bAzF-2L- I0C#2'Wnj|*9`*tehS]^w4#,2'_Zq ^*kP=: _7bFr)M-v|>\o!6{(ʀ\{c Tq`;43ىJ'%RT Е)GGER%lB!iq^`&tQ_ $M TꌣԧumtPFiq v (m$TYRQi$㈛H/mSBS:L\Un ,}.}_$cPU-[jf92Ngal2 Nn,߁9H`{G3`8I;71!^EdjQmOb# Cf{^X^IIiw^_ǰj*;w=~ᩧŽ;vK75N>gy{__җa{݈aذan݊Ç&Μ9+WqlhZ Ѣx~wkv:XSŅ 3/RoǷm<!-oy l21Dkt9gϝ; Z9ܹ;wĦMp_2;6XCC|ַW^y+VoNǪ܈B;Zh2բ lT0ם߿wHYukG z`C4a/;9H$FLⳄeo|J)LL[樤\O ~H-FćoQPHwT"ime py*Mr(7YoH499bHdaDdcZI19ZxxO2&lM jc,ZpȔeR}PSCFPi-_HA!5|z.E6zύ~R&6Oh, u1Z|A(m^ šSk=X8wytmW6nR /InBb*[?RQ4)8!USSk(N4ڢ ,{ߴd)Y˱@E*xǶ/1 ׍х#5k),JCx#g%hEt:"EtmeΧI$+<1RE52G8ȳ@D*'^b==_4pP4xrG5*HB(988#!S9P+M"'r f55>:+DDbc(`IoqS5 )}$Iւ&+uy НIbV(rxk W6G"fY-$ IDAT ij:7 1' ^XXb׮]ؾ};.b\|Ře]c N>'|x;033<={6yIfS5x ,ۇ__ƍcСC[qWn|۶mw]hyf\~hN ӧQ5fggfnHZXr%mۆSNxd_;uN< )ǏƍO<֬YK/+V[VyXbE>GEܹsiɓ' k߿k֬C=(>k֬IԠ4 @DtMزeK*Ν;yt݄tɓh&O]q~۷qצiDe,7XX;8F]K&MٜNd(Ga5H]%!(ҖaPFB̰L"VBJ#Tt0YgGySf6R!;1@J àw{&-H@- !Ur8(SMN32!DXTv[uyXI8t㴋ZeL4 eetLҶ[:`,Q4 \Ri@+cym3='FѱR!=F$+RPpWK˃1Fs.bFeqLS)('זT5߫,Ϭ2Y6n>H=E3K:tBnsgѭ,zSS,%V$N,_k+83\Kih, Pu(m-Ҕ7JI&$5I&ǻ"ۅ55i:d`Q_(01B\l}QefRns}89h75qY9s{\s5ؽ{7q={!kW_`0mr V\ =î]pرܿ?zXblpʕ+k.mo{aϞ=uV(vZxqaڵ O?4^~el۶ pcvvw#G9cǎa۶mXz5~__Q5Ξ=>ߙ3g/cժU7.ڵk} PJg4 87MWsa͚5زe .(p)LMMazzu*øKn:| _'P5^uvm7˗_#<ضm>b͚5J' DK|*KSx,Bjfx5a8fTQHI &޷]]koKY)mȣnke=PAЩ:Q<8w4\]C˱:[WC,uSں`8IJe){ 4'JB]sh@V9lK!OXr΄Hg"6nC!rҩoOEZ%TXJI殲D)w-ZcZ Ţ*)DS8ۭUU6'c##z0ˁ$v so ]NP=>gFp"4N%.{޸ m,py30<;1 G=$JLF ifne#DwMwsC T L<F<(@JmZ)#"ؔRAFo2R4ҏbJJ'b:mJ~*m{hkZMJ3Ck+(9 ?emT۰a~~)}mZc͚5x׻׷\y啸Bzjlڴ)G{6oތwp8Ė-[055}Z^e˖}oznnÚ5kpAz=z=>}MӤtc?~^{-.|#ڵkqW[oMb \wu8~88055 ,_%1ھ};n7Z=;5rJ|ӟʕ+pqTU [nEq7Zz5Ž;~z={֭͛w^4M/n{/& 6a֭Zho,SR FCbHLhw@αyOiVЌF$HeTHDgPU҅JQBHE"x@۝bqn++iHҐ(x )$)l&RZEy P/&6מn$}eDD^;PeNJ/ 9T܆ؐf3s"A (*&ȸxFY,"ش;m*6y *6_җ¸"}|0L2O>+9.4ބ~D 텅T??ڡ Yǽ ̔3 4zF2b<16|_k-,,೟,oN8_~?OpOOvډB= =bJ(g8iLOOZTz8WBǣ>*<}s=Lt,cf) 6 D"ؘ&蝄(#\3qNl긁noSeJEH+t:tۃ50 Q~v۫3'&w&uGUyxxTDA&F5Mdk<\#y cBZW)FqNYX ;5C#Z:PU,I_͸13330}NM0kSLMZϹq~ZweJW@uh޹!/PVi1(bьm/@,J~D?N@BhkevMP@JT j]%8BQcMuN*mVK!OV{AIQaĈ0 =u춦,TeJ qgIc}d4+t)j1ax)+[Avg(bzBo:xEXfƄ&%i/g9x dBu#FrO$; +E2=ΓkV'hb!-jAUH>&۲K3)R,0i|Η:Z3.r\Ú2y` |MMXD#Ǜ4GG<:%qqAXɱMȑ#Xn.rl޼ xcӦMBN:7,=oXk3*le ]!CwWnKNH}s;F{(%p ]mcxlCMSәko?:%YPugk}}|;wt{ /"g>H-ST䪵n PdZ9 RB:Ar g?"J(J(I*acmP;NTJ5#2jC Rن[dGYꈱVPA&p0$#BA_Rc)P<#ș;"#MT+cBȻDX)|f2%B56pQd XC+n ZeQ@iKZEb'Qu H 8W6TC]'5q'ʏQ*cbsla3 Ps2C)OS[ 4G#ja  y#"^$me˥QcJcqi^Cj8KZ2`ףIu(zE0?7!ǰ ȌM+LcuX@XGtbuD] c- ]@*  ՇT%zS8<:a]y JJeGDj]%T,rEv\!lM]r&(`92l jǡeR BX*,hREnoHJxlttB`#OMBp"͇`3dF3Y4KHKPq `Z&"Xz/t՚T$fRWq)ZG"MBz l !QHs$ϝWiN DCz$I \t,{SIJhg+nYXK롖>:iSЅLoe$bjPBG_e"| si%+gHrI-*k{ܼ-XGn\^dw6ތ r"|Lb3v I0NFiGRNԡ{XEɧjv^i" k]wՒ uXmy9$ u✞{ǎ>2w5룮%d%R 5>b`Hˆ'1[mD M3ۦН 䘜I2.!)cS,X]L$%ǞǰC-%] @FIxr`HIT^`B{.Z(jeZ*ƍR8G͟Xك4CFD B(N5u5f"INt֢i78sCsK~r^@J޷$;"#FCEx-9o0]PgYTB)Pw0A `H{0uLPnde33Ebz8tsi ý@dӚR;kc!(R?væ]{ >$< P!+LfL;NYiM<}!BhMV%J&8%L`bïC˿/b:,[vRjbiVuèK)H?_aoVX*T׍n]Zjw ѕuȟSܳֆ^@.t4B,K{ク+pQ E[b߾}زe˚rNFMDΗD#|OÝwމ;Q캉ea/ ==P,Yc4=)8CdFWZű"G)|O,iE \ (MР ņ\`3D~PqRL5+p20EbB{fisDas:uk2ug0 Cn!XӚO 1Y1$::w:?Y7&\@9Kq;= ]Uyp'4l]Q&u;hC0tc Q+\ GmJt'SS3͛VGemxX Pe!*EB%2~j~++x MUSw\h~LfM*5R*0 Iv#5If2c|YA(D"0E(#R7?$B'˳p5߫쾇_bQ 0:oJAJK!!Nk7:;]$CVtp2ue fNA@Az m\!;$Z꘏Xk!-S-6$O t9c#"57*Pi|z@rcB@)1ZF}ҁg:o(ECU4s0BAON7Y؀(@XG}@"3gΐ"򹐭-Ŷ@n%sDAs"FE;\gTnJ(95_̺a5푌kܺ7+G lkGDB\2&zQ1G=QZQ2Q,d5ģ_y啸+G& kFkkIdEzX,LOߝ:EENQ:ib)FJ Wms}*j{b|HN\\!Iox 6ao]G_;YNC4eC-|k!{f>5 U<$IFq&4 HRֽ:2ٸՅ '+Jq"$$TYbvvlڴ ˋKbblQ{L-y&b8di + >LX:xệ{:L .\t @hzDPEgv8T|{:".6}*6nJ& RL"eoH(xBv,fZlܶ7*T14ͤ3d &ARE`Ma"HouUd1ޅY>!jp=q(G$dE4 a\а77iFX3r)>G ׻\n9v7;*^ ߜQFUERXFLߊwFm2G:tܨ3ΦLAN븈#Xv?6%ƱK^RDR,YV~978p`á1:JRȅgݴҳ~hzS'4%uϩ(3@&8FBdT&y x޸$=q5ڼe!@c 6}# SBFUzA{n(A{M1 CxÒlA(QnLݩs-P<L({|+AL3\Kq*+,/ 1\xX7=G9(UEJm=(v"DUujc TQ_aATw͕րh OІTd擬%UP!QL9g:,)qQ]p1=15_,ű BB钒^l4r5dl2R?icMa^ JHcp XgJqeEr[1"Hn"zc5դUDeBfyĤ0fg~dvyWМpje zbYdCSZOdP>4TX?CD ՘ke$H8 g %.V8N|2Ǐ1NqDt`Ba^U11a4tfBM#C!s~~J)l۶ [n}[VU;w˸kWuGoVKfĜI6*G3v}y>@W>s%eRo2نE^揻VHk=QQ2|qI(P!5/‚84I- 2MI1H )4oRD uy31@D8ڸ+!euzם4ea+R7'+$*kES"mdr퇻d=EiÑ-UL0Jݾ{:jE`"EA:G"V9GLqQ %ﲠFRPq7ᰮjma@+8a'Tb'xzb9 )F#\T$? gƚy(NECw u.03h|F(m̯6+K,/c 4 #'Ig-p)8vZeۘ$ k8畡Ьi`@(aL'c*TM ]jH'ׅ(T֢〯^jEBlS:4)qWp.c[R8G$"nH y!ulLhs΅A9T;߄ :R$’18%2=vwgQ"t&fy8MU")Gpyhj#v=A^+ ^t>HkcCJfMQ23L՚h&D&ˑ4MfJZ+#ƃE1SO' x{ߋ}c-ɨzs/~ȑ#X\\WՑϵL#ٌto$f,ε:kp[tټ]SZQr%p䇃7;Lkv֢qɔ>CJA gWHx4Fu D= k,f\ox'q1q=K"iԮ8w h x{+zn |_rkgH;ĥ765awm,˴XZ<ך-d%:"]&2rrKi43FYmi_FbSK0ʀ=_Eӥl]GiC|A)8Q75D Ҥs !`[^3  齤WAlZ %e_(:%ͱLs/>Ǡ﬈! *F& ` H;g;(!MslWdtNZkt`4B#A"a5zNV( OaWa!x2@SMbmTCHE#Gc~~w}7~[{~--"( ;wNB;ik|]^#¼_;}~A*.{]jpL|uLVLkQkތ璏ߌZWgB;6Q#BBq!!\@0P)E) Oɋ:.d>0 |t4.NkTٱe1Oq6g~ψaf ySh\$潅WRS3bTC2ކ}jR r-gk%[,A=pʍ[a` Q45V:#L6إ-Zʉ>{$2BY^ǔhdi"[~β)O"ќ Сu{qiLMMkh `7SrәR ^z)wرc# .{cp 2tG(%v=7io}[0$3# M]C[+%z]p|֒w?n(CqNjԈ浦 ]d:FRqglV{h)ZP[8EϭqJi}C w> 'z(֤6n^Jo uPBRq>dHt RRPb] CZVsli:]ZHL$RLZuGA̵?^c9Hos1JedR>{6o8t׍@JW6\d;=DNI rGݚd:d*2EJfn73[g+Bl]S*A#>|-L pMd>]hj"{,̟j*ZPE (櫢x5"YgSQ=$A6XL.4W"_PJCBzF(+{4h^Z?Rf)ka|:TzB*佱ctpjئBRn+% !#0H].iv/jU=4PnDjßI4dDz];CX2ZN^r:Ɔ;2#) 6v ,Ev!G֨L=g;9(Bǖ}6AQ8>4\q_R#ADkb4.v\*E  P%Ix%͡H,XbXO sRDldMπNmo?.)Y%2- 3U 3"u=Pt (SQ1ۭ a<g?=G?q<կ~k-vލ}199#GpQu+/:.㛬FkUUgC=#G@]vᮻ/~/=vڅO7g}ro+N> nf|uZs8|0uV={[l?q\uUߏ^z ػw/fff099}s8|0z!>|w^|_^k->x/"pONN+ /_݋'Objj w:_~u]cÆ [qALMMWcǎayy]tqY߿\p~_ĉ>s=X\\___"كe`߾}ؾ};>Ǐ,Kq?7W_׾5;O} ۷o" O<9;w( 9sg/G꺆1_=Od"JGM֢`LBj!i({!!n,)6H땘CW@()EĔXpy!aMР</jƭy(#C:)ܥwM)(ve05If/M)HuG2 ˥i r\pxm#r+rD */J6oBc>.P:C}B,*P3Dru$K,UHM>@Jq@t"TV"#{IFBґ'!kQ˘R +HHLMbîi f-nYKI}braM^0+X <.6ze Z,ʢ00 7ժiSq}o+[6ǦuP'bi) zl ڧιdU!K0Dij@'cLïE5\Dy]f(-"(Ȱ> QCu;7$ZthZI>MObQ8r| 1IE,Ik-nH@:ǔc· f]nk+$mO#4m:r'f|pl ! G]"p)(PYCGfAn،rr.R۹o 63֘ΧfUD_%!j#S w %'aψ@ʘ+Q) bR-9GNɮ]wB~&s011^x_ױou]cvvΝrcL4_}U|cp]w*/s6: IDAT4]Z.M3îbÇm|0aɜm{g *2|<񘮅N=ϖL&$UKoD3-!.L4D2 --Iw gg0\Ԗm߸Z{C:*74wzdҫŇVN;W:J1Ar@'1eUH ĀXK~~ u]cӦMXny`ޓoݺuqa<8s z'&pw.? k-oߎ{;vΝ;qwk_ذaʲN8p`{M6( ^W\z Ǐǫn O,Kvmعs'?C׾5xquaee/B|K/q1׿7ߌ< ʲĭފ-[ğ-܂;vY;} ~ #7ty C]'>x8 xꩧb>رЇG;qáC9uV8p{}K>7 jwIJ! Yu]h1[\jͼg$ &RDVupJBJeArI[gd 2;t.`RlׄXލpaX:ca|\6o-bp]FmE-.tWfstch,*"@b4c,`XIGKb:lH@ @qN@J)RP2Bcq"Dwu=8ka -_-it#" ] ]J=FpHJJk BbIA]兤@Y, QUCqם Pe(1J`q@EFK}!! ઈ0-s;[%S!p8.݃\%&m}W_سp,r/KXLj:;H$bd^'"K<i]15>?G܀ɱg:O ix bYB9!xPor:m^!oӁ\fx(S>'}~cwbi FJ( ':J&>gzTΫTǃG0Ohh[b4&.sfHjoCHApX0 `jZ,9j6BOo 34}p9,iPa-``j8'ԩZ#O?cW3>F':1 AFUU-L-:MO333-[`||{޽pibݺuQSvލ.-]pwСCyԼфv% .w?\uUj?ѣG_ O>$oߎkf۶m[Kk=11 baaBTJa۰o.(v/R'P5֘p̙@||5v؁oZmoC=@ɵue?n{zz:\:{8q,,,'LOOcrr2>M6۴inFx111^z 'Oă>sW]uv7q~+'bBR)Mp+HDR=IKBMc) /R )Ca )e`PN &{ڸ1X[@SpuT\zHyeuRA̵2Nid;r#4Qy?*LHKN88b'3sBbY<-|ێh} {(>(}OP50چ[!t?vBZmԨ6Hv+)`MJ+hy&EZ\ )"6%H-D967dŘ:F[cX& h3IIfȥuM@AK2iΟ9g"t!v0JQzH2ׅB C@H*Cc 195jLLS\Ԭ6…BYHOk @:s*q(<5to Z&'&~jʘz-#IeT13'Bx߼Z˞kazS}@yXc`&0^PBlBr$& \<2>YIRGihqY^#UIö 7_)RfIs?,-p&H[!71!Z"; 6w}v8`sx X$+¥T`􁞒6g=@6I@*-Գ 9ȳctz`UF+4^5Dq fVƈ 5SqsQȦ2j%DL & D'çLHYH:ݔ`Q[ ENUU)u}-I(Μ9BTU:a-V3>33C^{~WcOG1{/<#عs'.r\p8pz!<'?Xu0`0@Q1:H)I4Ml۶}o݋??T^{ }#y7+s((aӧ-@E A<;Yxd RPZ`$F2#茭$UY>&QJ7h O.jpXc,yaVBG#U6s#; Shud1mR)%^8#"@ !#8 efswx], Z^AP%ŭM_o rZłұ"'=~YOKv,tg21sn3sٛ". MNy% 8)a >C2BH|z&MtNMCGNJ^ꮌ& ΝRs+oW^y۷oǺuSOA)혞J"7MǏc˖- ;w.RI?cǎ_17ps w^\yCc|5'? ? /7tӛJuB!4 ~_صkWQy9 Chq7׿5z)ٳ~>(u]i<wcǎǕW^}G]ב?$0>x;iӦQ/..b0@J99s!Ν;=z4~ӧ1??#Gm\|y߿?8N>GyW]u}Yw:<9I&Gv%"/q05FYZ.]t3ݨI &aH=% Pr2Q{l~N S4ɪ fϞ#bcתsavfN]Щ^#@{4CM+ )a=ΐPYpd% 2n‰H6nh *A,ȭoeoţS lZ{̤QE$&&Q5*Li8aR 0P0uͯE!Dld Oc}EaH&Tg e4YXX(pqƉ04Z8CrkBn FL4ưT*u[07E:51ccǎO )%p9[9 cbѣG+N:[V<0x0??>}wɓ'wv‰'3,!:d]4)ˀR\Orl D֛hVϿ*Ӹ겋pE;Z=_v4s? G\g#D`i|MLMZ ]]!d99%Td:Jj~$N:aO+ɳX)!4`C*\jASʂ%HϺF%+Ӵ%L(Nr1DIMNpgˎEΥ;R kMZEIalYŽH XǗXd{ QV,@JeG>%/Qǻ>y~R.4K3]!Znqed|&7nĝw2m߾;w177=b=Ee9@%T++N5tQr/0l JF03*4 @S\(ʲR0^QiȡJUS($/ H T X۸[w,KX"HJahrO 읇$F~%ð\%cBhgN9.PpMU 7EW/D  Q1qftϣ@V0G,ua0*}34߃m>Ǘ_~<mۆؿ?~eY4 W_}g|_hRBJ47cff~;8{,^yݻ|_|q"!&_h:C+;dR͒H^DSJ2V+8XqcR5xEYB:eҊ b L, _W@@Gz++CAgQ-,Yzy3gPB4^̓l[7aݻ9w,BJ B"0C]!vJŕp SNIp*`B1q1HHHWtus{Mu ԭ>g}Z)>Tai2Ƅ511YV<[i ׬X]~S4V)59?`:(P42},cч3<7pX>ഢ=~#/ұ~(B[Zk)t]ܑ ÈQaY51x֫)P 5~ir1vC?T@Jtݞaߑfݮyӗ7E}]q"~,371׿}ww>(?sg$!LkK .u*kad.ai{)hi\E?zr^0b!kz*'rZ"1+P 2Qcfj>65c?}|7Y^Y!\q,24-'Y)RȾEW{n@o|Li&m?go{<>Zgcci՞r*3R91e k !&\]h'b^@JSvW@NWZM:$M D Ku68Nidsz,̓ɃOfN><Bsj!8OsbK:Y@/m"W+MS.K y/|Y~2plK12ч;QkR"kf%)aLtO![QQ[1OtLiF1f],)Ke.0 AISy*d )wnvNVKǘ -K!xDSXD -ťmNb%@Nxc)nӴm{G$zHM>яx~x.#?#sqs?sbtgÍ7x_Lf#<ƒ> -꺞%D]^z>_Xђ~^*#`粈[ 4*-g:OԕŇvNyJ#̗w-#vqVb^RN8cSWcD+D%LyGBш  D9'4&XK[*x2(CߏCt`$1Deen&(:ɱM`64 :,(Ib4eU.N{e5>~՚{j+hj7X-b#Gu0v޼w c,]>U<Y+-N)VGJDo3@c4{_;ڍk`]O?,ܸ W\xϼȾrJ a!kU!fRPUT24+bq =wvَ{vg(Q낏B!`sZ9ZW(eX*Bl+G̊jƘԝL9:[*ѧXC"RYĺme I#OHu0S)P֢A)p]Y )i~1 ̮4c#F+nԵ#eE'6h*9;R >ʐgOgG6_Ob⏿-KLS_m]_ $K4C/}ۿw|ŗ~,LnI 꺢Gڶ&\ؑFB}|_׿ռer׵-J)>}W3=ŏПŎo|y⩛MUS؟N=u}Z5oEC&/ =pK-kL@c|'@[WמFx+RUY& =y+prSOx;8>'hTd~W>Ÿ?L"STm䙓Ldz)'xw~b:3SSsw+w>*x[|c?|n, $.m>e:fvӓ#^׽e{)̳|_׿/_goeF,mwtd_͆=3l6/]ajpձg~g1a@k= iwT9Z||*T&|$tx z: ^ձ^ mbn ڗ[09V_L+?(LdNsbFi4~c֐ϲ3 9D :kyHӟTq3 ֢S.Z]UEg mU "Vah"6c+ TJgڭtEJ-IS–Zω*R T|XU҇(9:ڰ=9mju4&(Uj{µ=g8uŭ3~InY֬O0~y'lf\!c`$:%prq'tjnZ5m0̡2wC6mZcXaiØ3++Xc )&GJww]sM|iOӟ9$y3:4kC  :?Pi!i卑#mU&Kbh,A)ڦ.E먴&!}$"PTVg1i]M#S8þ:ʙ={λμՊtSr`QZ+1дm] CmBjFg8kPz6bw[U)gg֜ě=EQS: ]?]9rTh Z)F컁) ƬFsz|DU[u#(MUTf zKy۾k +^ #}d$~l+bLZ)+lvg6/tx+_!uo_mo|U3+)e[]7i܋"H*)Dъ׽!Ÿx7_WN׼K_7?'>O$h/.hT+P DVcՖx&IJ)r%f: Q{{iZ(F@-%_ :+y Z>]1(v!~oJP/䠫UW7~wh+/}|_[7τnr53Dޥ򎷽?ǿ/.:k[^w}5/hbdj4XvGn3HZ{Z I[Z:.il NiK!>/Wuz!~0wc?J)NOOo@as]z/~??;ܸq%s=_dy(7/cy=H[Zy.ؖ:,_s`lYMᶞ32Tp$>U,'4VVkID5Z-ǐi+H \yY!'l(Q饢 VZhtB4l1}b=M&XJ nЅwpnBĪV詜'M[kzpz! at}jV놬Z'Vڝ֖Gv;\UYmۖv~=89n9ܺɑ54њS^~JՄnzyV'J)q.].E'S Z3ڀ1bIfR"(C]:*%I:J~Q1R5ۣ u%χ6Ju5ԕ cJe"Q\mHG*AQk.D7ĈՆ>e'" /p5M%c k @m5)$Vΰw#GkFi,)sr"ȺmC6` (RJ:%S buu%i늌,g=׶[qnf~t{VMCUWcY.zh#i($)QW?߼P릵Ykt ZP9|?`*æM2%+Gel*1ukGO JkVΕq{2o}-OJNB/ܕRμ?NNժFr@ i>م9=9tMW羅#SB *kfՒj=_cJ]7ۚ#8nY' g ڌh4&3u[E磕XއJwI-SCb!9QJbB#* }C/^@;Ӻ1%KI#_DAHiC99I5GTRY"@5$`a.;&I:H>Ӌ̇D[99Sk߮[R0 CV>Cc5ݾj ![|\Ca7Ѝemh Vo׸^ =p Tƒ21[+N:f{툓6G[uʑS'b4G-.% $h͍klS;KVPH2as_"$=S&B-q%EeSbAYO$O#zyB:m%*+Q X h\ #b솞kۊ4F!+ہc2[*b5zVMU$y0bqa`UY6~d,g5hcd틞2 cxpbϪ3E4QW5MSCqI8cx-W-/vjVMG5>fU ӟ8 %hpXGSs!@Ŝ9ZSYmdNO6+]`[@tF6!B]Ux0FeЙ<4_x9^pˤtb3jBlZafyGlڊ=oyq=Z8+ӧ's y!_?e|gWWquMmSJ)߱c#"naynGӥ+[@.SœO=?_BC+y^bN ⢔od,n8ZIfxK[is]7>)nݺ(I(|w׼vb<-z&]?PW{΍Xc _׽~F 0_ޣ[Ɋ6]S;gfW-3W Y,{[ڡZ#XDh XKbVЕV"҈HJKX$uGmee¬3j+v@XON͔-іhl-֟Ӗ U̓'4]&hnbqo=e$LR*iN֜6|* mMC5sn/a(В±8Wqw*|嶖 *qU]4AY%K|psMDZ޳êƐӶq97kKzK}xWM:c\8`=O*$GZ$jfL.ւѧHa#q9P+KH,8D4AIw"TRTEԨ&ktV5ѳ+~60mZٴ-q`YV!Qӈe]S1^(C(|XsƈJ7unheblVu1\?{e]U3䣖&8nw]DdJTMհ>:b^& 4qUj\k4XEqOY$$fGASd ©%F3Ay(UxX)E묤[iSD 'Gn)7oޢic>@S'ۻ=medUŎkGkƺṋsnlZ:?2gVgz*m8?߱ij.w4U-Q.zH#ÀQrm]s{Hں;jŎu]qcvH[9w{:pb{N6mCӶbQi =VkatGM8zoFtfM?Zb}'>(uJe,{ wu uӰ^!rmuþ8Z7캑Uc;՜ }s]'!F_O|qm7|W~rZ3џu94\k~14&0/9F)xW' "x黁yȵo7v綩 1rtFZN+R‡E^"3}/(fD_""uhqFbvG>[j~?:>/}̃5SVwb9YZ+' 9yw{;3{s׍c~>CUW8x'VB/G[?jwd{i>wm9˰;i.詛z1+X0d+y"ci.8i\AizOBBV"hL =PcX몥\)1>^0ʔ⢵|m!Qˀd2\dѴ+3[@hW2>$bP vJDR4tZ&}~~N] 0%`;ViJifisaxΉ>KuwF.ysm⪮[r}3 Wm{.0]c)Ȝa~Kp{xއ%>r8X%c[v×=aaWQ(Sfε*i\:1<-󬠶b(K,":UM&q/I·ĉ5%1mbNlz)|G+ƑXZC²1,8JpI9a-0CN4OTmϑͦM[*!&KLVu.&BU;ƌ>bdeScf̢&8&xFkVFDs Hg4Q,lSϹe@E&QYZ\̌e<k=[;:N CmcGIS '*JgqjEu#YFJǫVQ2ynWs8F,'w1O>‹wU4:*Ya^lXC1=wRa_6#lD\OABGeR%u3(bεx`/JnE'Έ.|kNs,9ObE}qֈۺ}ܧ.סtL\Gvnׄb%N3tW'_7)]cOr7fwLq<5oXl__x}$L+gIAgak-~I9q5_:>9Cߍ4mMI9*uKY[W"w#33@Cc\JLXn*SՎ>PU]ж!*wSYr @s|mP|'C"=Mm0FJrQJ eV6S׆\λ.ϧQ>z,N`0 cG@X MC~w0:eI3 [>. v3ӱ,;痶8S"FpZ"..i8և.i7Wy_uއ8PKP}$t((>tiYj":z%}*kIUșn/"};HYw7hZ^K}뢛lec`7EĦF)5S]1d%9S֩,#poiA.~D(V("^=;Yw}ӄ d0Xt6Ǒ8Eī^4"'QwHR^v1/>̺mJ ρ߻_mu=(Wk:+5rnW sc@Ir絖{7p֛8c= }?S2zn(;Y(QbA)U|L0պasXkFlO͔3g@|?j:;|_rպGKJ\;=nTeaHVK C=Ue麁黁v H5C)8Ef{1ciTchV=M{lTuE0*vF(#{0nAW4톽ezUE?;MY'mKC1 :4wq˘X&쇾zkILR^R<@ti, RJmb43xJ;U"%z> aQ0lhylT^L%oW",rR<KqH?Ҏq1&(;Q&,N;qr97S\+t6uOQJ"3(-#ZԖ8%tk,>80QNIO̚"Qզc+AS(' g* FhƋyeic(=d![5 XV=:F| Mg%v·6E̩u&fX\ӈUXocP)p 0nFĕ![#ޏ%#fC[̠3)⴦WXEUĥF)H,lPLYaKw̐e)hI蓢FVe|ᾇ1:1DqҰVňjh2"am+*H7 "8Αb]$ymjob7R55H96uvhu< >mZ|RlũpkwAnojib?l }T&6+bT҉<)jcg)TFM-Skusi9hjG}iJˆbh4yǛ ~x橛g b.Z4 ,OUKS[@( `JPrF//yE߱^Ֆsٴ cPSLbBmǟxZ`0(\e))ntQerJ&O4R*?/X[NA:)fqe0AJe뙲2-A2j%&KUUxg>gH?'p:/4!XVz]/#KE9:,{ ƗҾp^/'霗|,kwIpz(>7%YM|tmtiCjt,0⤲Li=E`b ksuo”Ϣ*TCh9#<;ɦQ5UI MXU;w]w{q PV,R!PG}#UJ:B}8u-X"ȠpۭEX!(}~d9ڄNncx٫RfDJ u R$ET*-$`)Lt [ \JAQIo抓8lW-1eF{bB(G떮1VS׎5TeMh'}tra8وLwЃwc޻}?ziMU9tT׎,)@5qrfM#JA*!X8nDB!PNGk*3/YuM Agg\ݾi6w.ٟ])xɟ MmUBwm+T"c5}7 B{+='fu2Z3ƒTHTD6ҏThƜ3]^(<,?݇6S, Ӿt:L ~ĶP.),var#Β6,v' KU~!M1QNhKV‡ j%B&, ;YȾs*,N]G(M$JpKD ^eQ!iK¬2J XH Z'G/e2lVQB~'y*%(fQԘrO1#1ҕ"E.3]Cմ:1XFI:ҥ6't3IDEs(PP# JIșl{ߒΗH3N] azSSG4eP%\#og§m5" d'5NgX[cK.]%Slkrv,VBΙdF3_r&u~I78)i-c7`%$Eb3{쇑KfPc3 ~FiVuCHҝ 1aK%9 _1tHTGaҒBtϋ-0Je  /ݣ4JbV8-JpRɌq 䌌-x/ 2 *?mQ1&r Lˉ^RIYo`ty>+0!'AWBLozq-g*G!<}2قG<-J jՖT`ެV5UeqI2V*rїtWG]Y[^ns_JLJ>q|mLҡgݮ+~왪\[ɖX2MhʟU#z6F4&ְl6b+)Rr ^X!&\Uqq LhW5ݮmJ܌3 }T.Fb(AwTTgk׉9ж+w=S@u׍ԕצэv]`݊C7DVM$3i.eVîX#Ϙ]/S2kn nm]h1jM LC9u'u=7?ZM\,–]exa^.UU@mz`BaI>_%&w GC6`p/ C;ȉ>ӲPZe~x?u Зӌ%8,ArpL]ܗet {(f]^i>B0`G⚤SeLJ48eE,RJ0y(f[]#ts )#X$T, LIG!N-?ڙKkFEVb(v!'B )`S(ԅ(`{2OC✪kJ~Pi0XXn]ƵpRfȉ=Mf$яgYE9i h^yt F'~o/ }J_.? Q2V} ~:%1| Y~ \Afr\sfftd54 !fZew{κ$ գ*}8zch*~ %4HIĜ컀26W)q 3 #hvF"~ُcIt >sT1ƈ(>)B+Ȳt}'OB~˦MdD7(D& ̮Ϻ=Z mLe`0RthٮLbw9R:;ʽl˔wc.yw cz_<貴9ozga0 !"ARK8rRIb;rR哵" IDATr%%SJ"U";(q,Y`  aett{.Ox==}T3-w9{~ px x㾗$ 7>Ǟ|52I%\7co{ZSߵ,lExtS^>Ų>1(B+XeX,A"T0&W{4U%H]zt?/+,mmapc 8+Lptj @5l(EevُS9|i(uKnOk4Bb{#z[5/cG֙Wa=|N)D;3{[WyJi,LAnW\/\pSE/{ySu5ɉH+s6{(y`4\7n=VkYZvFHSU劓Jc :I,6Z˹&XlOتʠP)YuCɉ;SCfjcV=XԆ2z8֐e使!g*LzyIiIQL=KHYMAiK4Z3qK~&Dv6EIHSjzΩ^ 3}/cNOnJ-sX{W~Jә'w &m9M54 Ee#./!X))mrep1ŤpyLPaDϔ19dF"|S+0GAfadB}w,PNJUbb/Y.Q#ab14r ~2*xvDm C oyhLE !83V?\Mp+D/!(ȁ?#Y8M7%R TYA 8:i{9poϧ* 4nASJ* 'JBΓц*&s|hM+>d+F0SF փm)][ OhԓOaXm; pcG׷qQ1u.+ĀHu9QqbF  Ӑ)f"t W!!xv} abbkK)m[Ά (q/]!<6aTo.;y'gUJ&Qc5_`_u=Hi{B#FdY]%;c~aܻ߁Y(+/|㡯?ފxZ'鸽 byvqNTq]ZABqY}D JT5Fn*s~ػ'B3r^&=s#.0 j)0ً;Wia3 C. \4ZIH_4$]ŀ~pbȈtu(,uwsq۶-?_Ԣogpx@b qҬc?x۾~=1ݏ?Ԉ `lH{vW\Ccֆ91JZ5dp@]@?c>;\@T|ܲV Sx"X٣]k~ D࡭âP:-GOhʓ s0D˔iDCh'4V˵&_W}&:KD %M,JDzh/E%b|ܧϑ T|ҩW't*"_ʼn^S"S7at ˛/:咅'UinT`d6S:l*ri&4aL)iXی>) h!e*zOMt}9u~)')&j*Ꝿ^hT6L5RS"I/(Jr*&JB,eؙS΅*2O")54w$@,C"/:VWJn.90@E-$4a`jJ#j.nm mlWĮ! ĈY$HZz֑@\S (FhAļ@$&I5LP|ȯMdY} R0pVyCȅEy152_7(A{a( m$H+rcY-&A" EPphVx H@dJZO$ S68!-+Ppy8?ZC Y7 hmbյ]\D]c*(T5ĎVvkjڰ϶\kMj+ xsw.9-x޷w#vw1q8OX-0shTU"8`j;o=jHHgOdZWhlw${[s=KW(PIIVw5OpO!E :nzp8{ ^+FBRJX!ȸ[ E0.l fD,[_5HwTRh2 ?Ha_o[Qibvx[Yv༤9F>} 0@*_Z{oָމsbk/q)qnoue9kpˮG>)1N }硔+>ZWfaьlnIGpUS# y9xëE ZI`VrL?fTSlVk Z`w>DS>%x^ž|1a~٬ 1;} #1BUWa+1 hsrI-)4 ON8,J#eڔ&4~ZȲل@OiSS)1}]>wٜ4L?i@(SXkdSCX>v|Ԩ輒D\!M]cphè7)B /B*5^`ɲ r M>p'SbC:\"K- r;4{/ F bNcP*((Z^Ue` Cн36XH§W(٤#ŌE"yǨ7eJP$(ژL B \m {"( J.ăxGG t+ğGP)JqD ׆7!3H#2;,Le/X"3Gy"Ĕ8PO?c걵5|](`\¹AŠxL_Il+ϡO>8^h1x,;"Tj jwBa\qk,cowT 8S_?x W/_a~=9sEJai<%.`Q3Wuޝޣ90Me%ET.Z ȿ$hqpbP _~[_O7vEyy|%}3w זbdt8{ g7_xxgOgʭ8׿e7ߏ}mp3?6?uqc4}w[@,_sЮx>obogks׬I5x7_LQ)nGeHKAO}ao~CBSWh`٭ ~C?Gc]w>?_W7TZlgO.N _~Q9%ٮ8C-WX1j2 AXF!׵xzkFùym(W6OʕVhWOkX)ch"|a05;Eh[lVUhWcAR Z@&`{A%Y]\)nn4wtdS%,Kosa>getBstanYPTo 5J&,VK>,KXk1ͰÜ]I̴6+d.Z-UJ|o~d,7=fx1>t1m:'KǙi?}SAfJGRF4_cX(X4YQRAh/(("XR\Gj@Odӥ(!46 Rv W:wQs@) ه^-|@쯭2 UmaBD m5泆'M-(;&P"rm؞R \:6#-$5%1ޏbP `~o.恬e@dogF(ץT(b"c%"\%uj(KAq P`.ewPYh'ϖF67Js H >x4/=C+~.`tS!d>*TOFcp8=]hü:;FubqntpH\-iױ" D ktw\>`wg}ktSY\P&!o>p?vp* ٻ;[({S9 ٣z| n|l>Ge,*L_S58k7]5p}o~o/8fA/g.ˏ|mήgqmiXN!)iFe>Jk*_ҽ\!);'Y @Q401XyAH94L+Ý0VϿ0~O}ޑrM'|}w$.5yq=#$~H{#-asJ^z=W1x6i+} ~wһn n\ȶR=x$R!%\=8§?Us8wC|_{ KOzvx/qvWjM "_yu+/\ 7%OH8ܛ5DfGo5g^d3x}>i4݋{g~ wyb[،S~P#O|Ǜj鍯+^~7.v;shGx+x~ŷ>*+B`kj'Tm+AT1Ԣ⵹G>d"|e o2BL%SZõ&NG@m>,;==UwD]Z0H#ǐRIO>;Eq/c@'״R+nK:B8V+}s=|ɲN9!\EWYhL"cBg)^:a6eO°kcX.8>>(|>G4k\fSZNi1\&NӢ:=W9(BqO,SD Mt~gZ'=~|Me6%Nҙ|cS~+"SlT5ǨEؼ`p3hVPJ Ƥ'-edgz.Z;]e27[i5rSКy*%\48ÖZ@py)bq9@41Fe !Uݠj.dUžԹgcy/GHHEFiy󨫚~7yBbk ZB`ggX;g}];`gg9t]Ӳ}c>VfH ]`Bۮlel+7k SS8{E mգc,W];/]f'}7 xe\;<15so?s{;b=[m ?셬HENqŽB5+akWd- %ujމwB^*-#vp3%=F/?Gs/ ><[:?N}5u׏x*;~Hpn?wr0 ltd4L$>xeDqH|[ϟs\1eƔ>%c<ZvI~ Z}ǭ*cy^05ҥ+*B{3L*ժ߹r!=r眀YS)i8haGGK/GW,;(c;*ʢl6i#):B*~=_m69͡\%A6NFu>*i 5Aszzkn(ite T +hcAP 9G%QjŎlf{%'<cxbi8wCuNRɍytt5yNLD1I)]RVg<=;I)d-)%}f+T]K{dQ XB.bOoקo @uExk+AϷQ흂O^Z-}$Tٱ):= MIaO/ZxXVY:-B8>>Έw*SZ N0%TS:L MڔC ET vx@]9ЩifBNiIԗ$.ٟkNEaS~mftatR3pB"})2_J)܆XR9h2j4P8c8dl6FS0&Ai&1#4)ĶPjvsԵR1kPuvVjh2ciG"D75yt9G)Ɖ؇[0`;< ]fV+t0>%3&Bz֘~wtRQMdȒ1EӓLBKF YFOC\O*,IS_埦x H0x3!m&Mh&JIԒM/thu )+7I)uf떍d˂7JWl.912w=whi4]8 bB sGT(5m4{GMghJ%4yӉ1CTP!/RL6@ HAE@T np19IsaEGryP<}095U:[o&"?d،~$n!E5.YHWlfb(hK54|`ğQfR$W<|mCsV+^v} ŤΜwX.V|݉ǴVº +qX6n X1xoagwJG^P.6PF2~@7t:Fٵ+("^[n.^g4N( I#.]pppCF[]y'!cw|D ݪl530n;Q/0Fv"Vgwe4(tG p䮁36u )!8!tSx;0֢mWl atc_}N<ݭoA_dK0+\ ^~l~p}} 6-@/4Z+Jg_~T( N[408؇k!a)R3B12fARӜ T]*̘ݜDhNiI9 }&,I-+,cƎ ۋē4.nR0+X y0A墘L`c˜Ao3+|zrS&Ϗ&;1'E kϗl:#Qh.m~ti@ɔ&a0[K=\'-lšJA8D2!Z\dQzL 1#"+][Vx1LESbRH0mhNBR;E\RRo*Hڛ MYoBָBN*K唏_67+Z7+_ )YbI&{$fH^Oⱝp)Hdad1H@E|4%8HtUV5m*Dqtt(lC1u-3k(~v;Nmxgx՜ 0F=j -rn>gje]~@w /\}MSۃ"̚Wzh]3Ρa2}wwM1mqT^ (Dԍm*nQ*. \Ak[4M^ԢQU]\2Ei*c0!rU.PHgnֈpnϿ?{QeOxWy/g;s5uJ MQ63Z\y*9ѥ 72%Ot$ScQ AE2sBz srtv>w12ElN2T% 3=in k瘧l[68]͟'Og y#*'c,49JIhZ}n;ZO&(4h=)h(Md"DנWZLn[  XO~wV"c*Z,[҂R$rı.5 H씢%I" i=mFK4rjbl9ƛQB^ !? B}s%&)뤂~ ROjnN|75y'D1?|~e N["P,B :Ѓm9v c vwjX,0FcW[u!Z.\9ΞCߣ*UmGA ͬrr!ꊃ45vHEln ;fׯ:4MXZkhOR=4~@۶4z U]T umer*V]׳(:Dlomt9V0JV`28MAgl`+A9i+Sah-DOz<?7ss|?(O7@pky>I}j kAOf'BAO.6Hh¤$ A+&Ai,AKeYJ U1]2^JΝ όSNx/ <S#_ƦHhz@juT)7(QƑLD1XWɵl}ç8sY"Jr/ũ7CwWGZ8%: P@ ~ia`Rכ΍kJ/Yǭ_))*XagZ@,- 1pKT F*&+2bIv'|,J!0"L*!ş= Hk-{kd[9rL兙MMDVpyM M"(7/ZXi [apYQ,9 J捯Mx2)xtFȂtߕ.W)yPDy@t*y"y5GZFnyeĹ{DI; ]#BaM1@QY`kר#F-XgKۭ9%N:_!~;^s}xͫ_' /\l2up6b?kWǬX~#ԄtĴfMvykC%;/\?OqQL"&p"xn8{!ͯQEH_g!3+1a4 FihDu!*g;(^m#46抃o*ad-O.~s5ڍZ<2Ȣp|"(s2}YSƥ=u15I*)E/"`5ݼU1$8B䜦 *HCY~h}Y瞗uzDZ3f@I@ߔ݀cjt![Boa>i!;(CO'T گj/M*ӿ75nꜦ)ǽF':Lɘe16m BSԃ~ ~JϴeK4&ZSr|#e⹩N6l:M\X}3O2=5fSd5Z<pG DF'r㩀?geCV3% 96U.߳+ys NJЀb1"y D}DyUR"S#$G)t&\1'd#IU~X4s48dÈ c@2DЩ]uGJLa0_k. 8 v)@"hHM۩5 A@L0ǘS$+˦#O0ko Ec9ky~b $*F# M]m[}aZi14.WhuȔ̺e` V֢kPȳ;U T4 U?§ڗ;/`wkc\x𥇿{1Sɘx !8(U^%E,HAZnb0 VedZ#Xt.t֑֢D#ʾމxe7caש )Em8$() Q)ggz w٘>c|u{L/XNSnqt"?j0s)@DhЦ"za_ɝ^HB֮q7sKO 6'ƛFepR~3ikZ8o*|7 SOz<׼BNo:/kAVQt؉/a=[XY4#$BrGõQjl)5Tʅ2 g$Ԏu)'/e"d rV4UFX0EC.4.14XŌR|TJ8T&13" K& .{~d/"9uSNnPh!!f= BB׏eCao };{b@)~8j5H}aA<ھCe,Be5_= IDAT.]B0ߚc6cfTGb| NJgϟC+PQo*o ĵ|p*8wvv8kBӧUquPC=;bO;nV5XZɩ(48iW< S8pͬbjy?Ϸ )ʢijnZJ[sTEZnaׯ\fa Y `+ #dB1!yQI#6v)?JNhjcdb!3ы L ^3 [$.`0qߘ&h, p|h46(ɭ&:JBQ$S$ i},^Z>2fC_xUD)-f-Qt~{ڀtVO \ kdlJa/)Xȳd:U{Z w#8YM]g SAi֊v$w}.E1'.UUU.H7Vn%]^`BsZx2%e!n2| ԡ)oyz{Ml$ğ$o?)%$ӹJǺqdSA.sRh&fIn-'5'=ͼاlo6Id9M͍I D&v ;HI>͈$T6W= UJ(!H4(1-`*!" Be*P&qy 𒲩?cd@[sY2Fe!DƬ"zTU;kLsî\LX7\x|y⡡@_Seh8z"fGy3ڂ$%**i0`zvXXѵ( 2D^cq|aŅ[5~`Qd4pc>yVUm.Wّ,d݇Zt>-xC@m,n9'"px|,uZCLTةg]V w\3i [n=k5^W칳r211slq.?,f3t gΞz [~w"(㣫XaXulI@-yݯ5|-\?ijaN+2@Ty63 7*1z.@p:V\0}m0~:A1xx'EZ@[dV!f7ڎtm4BɤB=AhK!%eR&$avΥVJ ;OYkD0=jl.- IA΁9kb]W(6d@,N4EV';-mF! 1A&J YO)yݤ6*s)E'1幥u BȢ[5-؂ػegYԪh8db棗aX,OH t'zx+〸 >ux]Ƭ`2*DIRjO(O|ԇh?!%r 1FV78LYLEtjשh?=.5#S&wz/S1&KM9Qx"1@QJ!8'q_ Pvz9_L8׵`Ssw2Qt J)\Y[W;YT6 >xTMsΣͱj[@={._|>}gW[ΟDž ڵi*2qr۶-oYs_s;"D&QuLBDJYId.jjIy0i+$!Dk ڲ JB :;4JN"5¾P\BL9) H1FNMbSi%bp%)E*Q$8UJޠCh]P5T~&FR4%: NDoB;&$O!ѲF)B*ӔpqI1(Ғ20u h.H\$@.2V#%Uq)M؎fI[4CZliFԿ`Fy$_a4$g 4 j@r!;_'Vj?w_k7>1¦ve&TY 4Ϭhf-5!҉zRmKԵDq1!,ŶӀ/ cK8Sc#;^z'cK'!Ǜ0'<ݬ^EbvSx&ʓ~礢f/F^ +Sߺ\ބ!T3XS&@ Q֔тTz˗hQFVXX]1|Fʒ $2=kK:8a K,Nx5H?XX]y\9g*hqݰ10ZL"xON]k,cV EQ5 ]4!v; KK/}/)c}uZh!yD@-5Lo3Ija7$n;Ł>z.8M%>x횒JW50P=|OZV4pU`mᵂPVP>Ν{¯}<}iE+,RԔ: hPJΝ/WWn6(2Z =Bq*5$)} 6}C5ꝁȼ,I2VE*^mS5w5E\Lyj\.F2"3k*7İ$ "J+3]сBpոL2+:~h@"HZ4E2b%]n|_#dF)R?N'6Qp^hH=X9 \HzIʁA])#O*>!! Ǭ1)G#S,4BN?N D,R94j^'(9I.; n~ ,ɓ T"p,+hEG#Yd' _=/rsL!?dy D:>mr3d$hq|| 5ѳpb. ܔ_c\n>~]̋\>1yO->n5VaL!*M1>J~|˾/ufRq_ԡY.t>qf0727,%2bBB =S"" JD’E-BFɡY;(x ?ɂEdd!YU܅v$' )J| m3BF#B dF_%-laL,u;oT5hA ZK9 %С* h)ѳD3x/`Z>=C>ױZGOwI>v;˓:_t]jxO%FxB$#N-̐.CRQk]*2Ib*ޗEQPeIf"ziFrN?TErOl2H-ForqZvHAB  * TBpې|:5dqZg4Ă>#&-D3@RcB&j2^\`bWFJg䯤l,T 8]C겧.d6aB\a8QIҭKҮEwM =]|SW !—*OWly=GawC@~EsΜ&1'_;ךG)ȮsJN|| "=p a/1O:\-oC?|YfAf:뇦6x)]Fc—B3B mbL&d!5ϢEEZI%̵/5ftmL;Jѩ wG˾P61M i,^#gmڔ|^- <)mch>ME_u㦑R``ѵ;QGv2נZSB1fK4Is0"H`xPDvT(s ꪄ@y{_U 0!,J>o-x ` (̟ۖZi8nZ,K]C'YCbE pG)b Ck{^_/1CُS} k~T8QPe0 AGcPٱ%j6!L"(aDɚ,1q:Ac+]Y i3=i<Ѥ 6ɦ 2O7-y& ]P3Xa= Tw?Rw­wk!5s*̾xޡͻww-U9|!>O#ƍǏXpsjGYMN79$C?Jd^WQ}CO!c&}M?7e?m?ĢM{?q| Ė1V9%38# ^g@rS^iq<^iEYRR g\ ):d,R&Nn'zKhx )EB.ND&PlI@|\~O:L2cHfiQW<'1ȏXqt|`m0<֦b*\h-~ؑIЛTRC pou`(UR[KtyBEM@l8CG,pz\GY(y>u5M@ oO6 ؘ޹ kY{I9%n5UUm[RK\__S ~Qpu~BSx!451y (BiJvT`B̪1M??EYVxH-S!/B)ZoׯXC@T_iBMB9QVOn{l8P¤b)1;'k1&0Y3ǏrjG$ӏQ2>#x1.g)u<"q̙ 386ybJЈ(Y64t3"%{uEߖ_K`ojFV`V_HH1l1"\czwkdƽ1A ~:k[lMz(k2#%&~O/^ !3,λya-Q'.R>{y!}9G撈 9%\t#9+}@IBw xK8/!m,PGT(· XBBg98ϼdG IXc^ ym :0jQ(wDQRKFD)>ϔ@{?<?QIΣ|zU2Ѳ5h| 4 n3D":3J ,wGm;I83/ 3ڹ3+RrdW1]%wa@%jFʨE: u4#NtQ JGGG@ڶE8>:55/:XgiQuMRcDRкvEY8jV:xॗ^zRXv-x >v;Jz>=9vVL%__vU :@퀪(eу< eCe;$4$!%sSjh@w|>'( ##cЊ̷Ӆ˪SIg;%B K_!X{HEkPW%ԩ. uSeBrJ P d6Sw)jXTyN*aRIv+yti ͦeq:tK i;wD3p@g>1gHhplhN73Jy~Iދ^d`)E`zpD4cC0|*MB~By1o|cO!qCq m*uL@ׁ;!X8;`Z(Bχw~l23j I- IDATq0jiRT%v&Itx> YXgQ75<[,G8999shz,,8u]zGlA]ӷ xݶha¦nkZ fH:::Bu \%v;ev;eZQJ(trDlX/͆vc{X%KfX*`EH@)imn$Eq8b}0Ǟ4)(QzzO:~]c=i1RF)o-Rr)9vD`4altJI^kͭM47Fn$ё;8Igp%!ue&}T-NÞDLB4T"uG!$? )AF6"20"8 2Na>K\Q' S?&F՘=-GwM &Hxd h::˥Go~ZW{yo Fʋ9fyoKO&PW h |&iPωxOIޏXŐ=r6f@3B BC:G%]?0Sӭ̼?{|sY`m7Land.=D&/Dm9|G GbM<11F͐@{^^+o$h^HPpA?nGu:o_}K5r^Oeױ\UՄsԚw,˽XEgsP[,~sN\&֜okړ>Ӕ`T(?:yZkD{f_=fByV!̡I¡ob4Gw&h,Th 9!9="~$ ?h>Bb7"bhR`72"Y%wC$ׅ,Y8 1nuchQI*a3{bp#0aӘ:{dog]JktQk]fxoayoe!PW%nv-ϙ @?lR J25<^0B#c`!mWWv USo٠Kc 8;=EY8Ә=BbnH_%VGK-\ M|Ԁ ¡(%Ja d@QH, g,{,XZJ.J~B 9RP,MRRCJ_hM? ANBeUo#zH8*KTe `5^\l4..HLWx1 A)Kf*UO2K)i^67Iʢnvm EÅ;i bXQiCIB`׶sHeKd1D*6"B)-%d7%TWEA5mO|p9?4caY @!E1" %=XTY^/\J3N^%.D4sW76И|_i>If2}Zƒxg8uMRDs.I#,Oi.b:&#Ԍz& T29ؘ)&:sμM1+uKo2%NY9fLxMfۓGiT bQ~"ho+cB }9Pen/yQ5mNw|H XUU5Q%2e2E{Qz" yqc$DNg\z101 .qLYcxq1B*TUul{[P,-h/i쇨26tM~wثw51SL"Ȁ;ЕR"pe* bA/Sg3 9CM0a< %Cs>vl0(AfѴA 芯]ss֣qS̷O8"ͮy`Rī ~%~%$Vsf&JF,1R4 &Kͧذ{^  LףY,xOdi{5EB(-8S21*6@ˢ@YTJ(J?FUK!ڋ4rޢ*J3jѝ&ɝ";r]!LA:uefk f\*b%bMćQ Q'i/7 ]wX֝g@RQ$eDqnHS&E`If8*k!!ܰ+oxݖ4nϊ׉( BMIG89tD7n: 56%.cJIF |ى" -!N} !m8@%ubkKtqG3Y 7|3|K_z{b(7撔eܜlx3i9vT&!Y;B,܃NN=rCQ k=V05h-Qh. pzskquu}c VI*MG\e!%5X zzz-Cףnv\v;HPZl-CeY@)f `\`Zj(MݦiX,A[H T!҃ h* cBE *L#qR+ba%,焂1:֍9(6za3ݮEUb%  ƫ$7Y.xsPTi\.1k8JRq:%'{Qi\( v;/pTEHYۡjjH譔m25*1ZNE!KuX"YTU!liQa S6 @iL.iǘ蝳w|@J% y^NwHbd&!פ 3QkENoM$ciC"315;%c&bϙr!ÏT؊Fx(a K-xgrED'Rd$L'cnI4 0^)btI{C{nBs= wַe/BR8ґ\>ש|.o/`n :M:Q’kcD`=?H'v'1NxdǮL\ⵜ>utnA'NuݜX>$CžЦC]2UXY+!NYYӢ}4ď8FxI :b8-HH!G.dS E1rfMs9L /CFFs-MuZHK>awLPv@TI3,@o<:r8;p0/J4P(Tg$$ z~Gkɢ,p4n5JBmTW?Eo`!蝁e ;tpv;x(UJay|fJ TuS|vD@jTMn -8K/ m%,5XYZ׵膎Z}PJ^"=(L3#t3)@~c"`l*gN{*&v9"󣙜q"#^C/B "??#Sk,suf̋ܤMCLXd%@b:䲤O&sT!3h>{p0G_\?yx?kEEDp=>~g] %b$B:>Ihwk[]`׶À 0 Tsݢk]KGG*\4[X?0mԥ6A[H,Dњ<A3$48;Mwys's$ 'q\$8=#cRKN,jǃd%Z(^<Edtji*Ja+MZ3^HI$D9&sF93C'f Usb 9E~J9IFAپBHFr$LX'>}%sl0=/<&7]zOӦt LLrF~z>*e â=$_AVHD;"eCjtM> 4N}l5$K0N C5c|M,5ygh8*ܹx8GBF8 ༸3ci*s<(}<4*4%jЫJ\QbВ |mr&|=~M>m0Ơ독}܇|V9ˡۇf~n'>g/}m>7&f)iS4kG>uø p'w>C7fҙNU92ς[ )2  =tAj;w,*,-?}V+H6ƴJ)ɰw}}su`>]viP%C۵xw=.c V%|kF.RRsC2`N3SVWi펂u2# :l6v]JuR_-_G#NMTte| \2|A@^VPPՌVM*((1yD2ϓ_0]3 b5-.tNwtO 3+B !- D$' d6ErhÞd;fzO_{'L; 'RY&3p6G1?!)37Fm}߲ BN:zDOf\"M2n w7|/5\Vs% yRhwn IDAT.Q#yba"7y懆\I7't"~M|/0$o~0_ 5ʋ}nx #wg66mhUχ(7DOh}ݟ؏7|Z)9i?1U0CUAdh,)ńl=F~Ď{1.+)GlZ"N-y#ߤT/xxE`жk C MS}vb~ܞͼ / g=gXGdl]G!O]>:yApS|b-dc).6 Äf3GiO$, ,D)YO:7Mj5A<{k-Lއd0~"|mf_bPѿKew(^iUwhʒJN_b㞴x>Q&lT^ó9&L,}>"-~QG/s ^Ɇ8t,&1{o-vBnjċ/Amɗ?~.ݮg^ǣG`ۡ`"5Σn*4p7&EQ:Te )$o(}8;;vmGJ!nZB}?{ݻwNOΰYoqu&8;=î\0h`E]ר 2MKP4kc)[ǟU5׶gg̀OBS , yt0<.JXpzv%=FJBcREǝP.) tLɁއҒH[`nw] UU"x- vK/;$uf$Cs * eUCHICAzMސi^Ұgd#qw0mZ&C,pzzÇ>@4#1Ќ+cw8! 7!Ey# 4Ӧ PjLĥr:I+s"|,܅ >&+5O3W[eH .6ԥM]\(cNX4=c|2nN>?T3b v5&H$6Vvԕ'_NZcJ -&tĄP&LӞ Z2ʀR!8'_&pb_al<+ܔ˄x}f>X>ejN\,L 2^K??폿ۨo6BJ78ra/)wZb*,˄:̻EQ( }S! DdOg<N}4 (ɋU-$Rєw˵֩R{m@zs}^f׸PWUty=" 7ڧ?mc{)!vg#b15ސQũIV],@NFyDzCƛڏ2݉%˓К9L\a"eQ@"u`/B(E>H.hjLj EfO!˯#@z.JRruIo|{ˣuz{9z^*a ޠ, (.5Kl&?x'g'g8;}OðVz כ5I!Xo뜅%ЂMs d.U++yytkl7[H)Ke ܘ\$ a0Z{}JWm;:T$<5iffv>ٟw/Ѷ;4y%EޗE6u]m(}CXA70X(MIYiT|E[: (KAKp5pr.Ԅ;!(bmcnvEQx z]t>+x6 sEQ X,Wx vɼ||tz]b۶D4jJ4)8WJXM#ߤbDP< z'"WG v4&# =L5CE '^\cu>@*9HI> u0PײT8= RYh81*b5Mhz&%eJ=s= |Ȥ|h"ӨLR+KO2Y?Lt葼bx2"FP,>(@RhN913f6/.%!fb*tR.FLe=t=\kc(Y5tR32 ' >JA4-qL=<+$]+>ӡsl3bbg)k,~+2pKx9(s{.3B]X)59סo6;9:9%js2N4ǎ}F1v/n>N |y˼|]n܇ }C7` 3_Oi% 37R]D&!2èO}ˈy|)ԡKIC]cVTUCh`쀳pzzr|z uӠXn89:M;#l9EQL~|`WeG &SyHZkҽ*j 5Ke9\q8C;?WČky<}%uɛfSM"N^L\4wP!+ę;yF k-O)oQc'[Io2EAD/Mg#SJL23ʩ%'H&{]I㎱MNR LC<2IvMfw0|S{H$OsECj  wÿ_ _WBx{!R !yqKU">/8 M:߹.!DԐ}n5ˌ v{rnZJ+33[o=1M468)81css}c<a8Z{w?u b C2HPV%677q.FhD#ךF}[%ȱW#L3TU{`1 3@ e "$"?C*ԥ%XRwFcצ8HaV#Crx#wHߟ P " ug_>ER)ϦLOӢTf>Tk:8c8Gux;wh.d;q?MZ`A}g~i^:p[G鑄4vZ}:id(c=#C6H3A:H#4p\`>?/`>|~ %|{_!͆8s[>)!6F#y,KuR i(rgsFb400g(4&187GnP'Iݻ(/7n\p0dh.]3>(p^|Epq=+I}Li]W#`U]"Nb GC_PJ)E_aooϟ8` +G'],W+,+{rB#MT,"$ ?aQ 8l` Cj}^kʏ8 l#)F^W5D@,d"olԪ#ld֖-8{ Ɠ ~y ??zJq5 0F/FJ ~kOQ$kDd\c0P,qF.TYXeF>e?Ќ^fr3p$"#QNjĘWXM5&h&vS)K1#fDj⦉PU)cKBs B/6 ޤ a޳Cb }!K7n< 3R"( 8L}S#){\`2=@*ܣ#n޾ _w˜5߾_ }ֱ3ǀlc/ LeHTh!"Cb6u5mڮ3t:UL\+()@k/j{]ZƿV{$Qi!EDw  "G]|/4 ]读 ~+nj*kVkHIDATGZgRWIZ0|*hd1zt|^Lc4ⷞ_I1XW+pP+DQ48Zu]ۘ.G(%YH k-qMlno?2(E4FDN.ֈ"@Slk$#h4HSM)-4ZJ$FX\ͩ N 9~=}L&80=!M3# $8@'H Bd4 D Q&{6ʪ`&pw~70hd=&׬"Qk`%CH!8c-޳Z/S-%Q שy먪5w v]<} ^( JK4x`v4˿|FMyBS&amؓNd7 l!"lo*רʵ=Q0̒,H\&TBIk%SaqĂOm$M6)ܞ51k8^-D="4WH (4ZCՆ4؀T lP+ GHShbe@hHҶT7ަn>uiY՝7h(`"yddVf;[N (#f~/u8 \9ɦwZ2L߄NEs, !T$Dk{o(ɹMҝFfFYϓ7"Y;.cgP&,8p{a~t7'G2L[Ii1CX=EDֺ"5>'iK }r*|_%x*IENDB`highline-2.0.3/site/index.html000066400000000000000000000030321355001426500162430ustar00rootroot00000000000000 HighLine

HighLine is about…

Saving time.

Command line interfaces are meant to be easy. So why shouldn’t building them be easy, too? HighLine provides a solid toolset to help you get the job done cleanly so you can focus on the real task at hand, your task.

Clean and intuitive design.

Want to get a taste for how HighLine is used? Take a look at this simple example, which asks a user for a zip code, automatically does validation, and returns the result:

zip = ask("Zip?  ") { |q| q.validate = /\A\d{5}(?:-?\d{4})?\Z/ }

Hassle-free Installation.

Installation is easy via RubyGems. Simply enter the command:

sudo gem install highline

and you’ll be on your way! Of course, manual installation is an option, too.

highline-2.0.3/test/000077500000000000000000000000001355001426500142635ustar00rootroot00000000000000highline-2.0.3/test/acceptance/000077500000000000000000000000001355001426500163515ustar00rootroot00000000000000highline-2.0.3/test/acceptance/acceptance.rb000066400000000000000000000035621355001426500207720ustar00rootroot00000000000000#!/usr/bin/env ruby # coding: utf-8 current_dir = File.dirname(File.expand_path(__FILE__)) # All acceptance test files begins with 'at_' acceptance_test_files = Dir["#{current_dir}/at_*"] # Load each acceptance test file making # all tests to be run acceptance_test_files.each { |file| load file } # Print a report report = < should be green!" end t.question = "Do you see the word 'grass' on green color (y/n)? " end highline-2.0.3/test/acceptance/at_echo_false.rb000066400000000000000000000012311355001426500214470ustar00rootroot00000000000000# coding: utf-8 require_relative "acceptance_test" HighLine::AcceptanceTest.check do |t| t.desc = "This step checks if the 'echo = false' " \ "setting is effective in hiding the user " \ "typed characters.\n" \ "This functionality is useful when asking " \ "for passwords.\n" \ "When typing the characters you should not " \ "see any of them on the screen." t.action = proc do answer = ask "Enter some characters and press : " do |q| q.echo = false end puts "You've entered -> #{answer} <-" end t.question = "Were the characters adequately hidden " \ "when you typed them (y/n)? " end highline-2.0.3/test/acceptance/at_readline.rb000066400000000000000000000027741355001426500211570ustar00rootroot00000000000000# coding: utf-8 require_relative "acceptance_test" HighLine::AcceptanceTest.check do |t| t.desc = "This step checks if the readline autocomplete " \ "feature is working. \n" \ "The test has 5 options you can choose from: " \ "save, sample, exec, exit and load.\n" \ "If you type the first character of one of them and then press \n" \ "the key you should see the options available for autocomplete.\n\n" \ "For example, if I type 's' and then I press I should see a list\n" \ "with 'save' and 'sample' as possible options for autocomplete.\n\n" \ "Although, if I type 'l' and then press the key it should be \n" \ "readly autcompleted as 'load', because 'load' is the only option\n" \ "that begins with the 'l' letter in this particular case.\n\n" \ "If I don't type any character but press two times, I should\n" \ "be able to see ALL available options.\n\n" \ "Please, play with Readline autocomplete for a while, pressing \n" \ "to see that it really gets the selected answer.\n" \ "When ready, just type 'exit' and the loop will finish.\n\n" \ "Don't forget to answer 'y' (yes) or 'n' (no) to the question at the end." t.action = proc do loop do cmd = ask "Enter command: ", %w[save sample exec exit load] do |q| q.readline = true end say("Executing \"#{cmd}\"...") break if cmd == "exit" end end t.question = "Did the Readline autocomplete work fine (y/n)? " end highline-2.0.3/test/acceptance/at_readline_agree.rb000066400000000000000000000007711355001426500223150ustar00rootroot00000000000000# coding: utf-8 require_relative "acceptance_test" HighLine::AcceptanceTest.check do |t| t.desc = "This step checks if the readline works well with agree.\n" \ "You should press and readline should give the default " \ "(yes/no) options to autocomplete." t.action = proc do answer = agree("Do you agree?") { |q| q.readline = true } puts "You've entered -> #{answer} <-" end t.question = "Did HighLine#agree worked well using question.readline = true (y/n)? " end highline-2.0.3/test/string_methods.rb000066400000000000000000000026151355001426500176450ustar00rootroot00000000000000#!/usr/bin/env ruby # coding: utf-8 # string_methods.rb # # Created by Richard LeBer 2011-06-27 # # This is Free Software. See LICENSE and COPYING for details. # # String class convenience methods module StringMethods def test_color assert_equal("\e[34mstring\e[0m", @string.color(:blue)) assert_equal("\e[1m\e[47mstring\e[0m", @string.color(:bold, :on_white)) assert_equal("\e[45mstring\e[0m", @string.on(:magenta)) assert_equal("\e[36mstring\e[0m", @string.cyan) assert_equal("\e[41m\e[5mstring\e[0m\e[0m", @string.blink.on_red) assert_equal("\e[38;5;137mstring\e[0m", @string.color(:rgb_906030)) assert_equal("\e[38;5;101mstring\e[0m", @string.rgb("606030")) assert_equal("\e[38;5;107mstring\e[0m", @string.rgb("60", "90", "30")) assert_equal("\e[38;5;107mstring\e[0m", @string.rgb(96, 144, 48)) assert_equal("\e[38;5;173mstring\e[0m", @string.rgb_c06030) assert_equal("\e[48;5;137mstring\e[0m", @string.color(:on_rgb_906030)) assert_equal("\e[48;5;101mstring\e[0m", @string.on_rgb("606030")) assert_equal("\e[48;5;107mstring\e[0m", @string.on_rgb("60", "90", "30")) assert_equal("\e[48;5;107mstring\e[0m", @string.on_rgb(96, 144, 48)) assert_equal("\e[48;5;173mstring\e[0m", @string.on_rgb_c06030) end def test_uncolor colored_string = HighLine::String("\e[38;5;137mstring\e[0m") assert_equal "string", colored_string.uncolor end end highline-2.0.3/test/test_answer_converter.rb000066400000000000000000000013401355001426500212330ustar00rootroot00000000000000#!/usr/bin/env ruby # coding: utf-8 require "test_helper" require "highline/question" class TestAnswerConverter < Minitest::Test def test_integer_convertion question = HighLine::Question.new("What's your age?", Integer) question.answer = "18" answer_converter = HighLine::Question::AnswerConverter.new(question) refute_equal "18", answer_converter.convert assert_equal 18, answer_converter.convert end def test_float_convertion question = HighLine::Question.new("Write PI", Float) question.answer = "3.14159" answer_converter = HighLine::Question::AnswerConverter.new(question) refute_equal "3.14159", answer_converter.convert assert_equal 3.14159, answer_converter.convert end end highline-2.0.3/test/test_color_scheme.rb000066400000000000000000000066121355001426500203160ustar00rootroot00000000000000#!/usr/bin/env ruby # coding: utf-8 # tc_color_scheme.rb # # Created by Jeremy Hinegardner on 2007-01-24. # Copyright 2007 Jeremy Hinegardner. All rights reserved. # # This is Free Software. See LICENSE and COPYING for details. require "test_helper" require "highline" require "stringio" class TestColorScheme < Minitest::Test def setup HighLine.reset @input = StringIO.new @output = StringIO.new @terminal = HighLine.new(@input, @output) end def test_using_color_scheme refute(HighLine.using_color_scheme?) HighLine.color_scheme = HighLine::ColorScheme.new assert(true, HighLine.using_color_scheme?) end def test_scheme HighLine.color_scheme = HighLine::SampleColorScheme.new @terminal.say("This should be <%= color('warning yellow', :warning) %>.") assert_equal("This should be \e[1m\e[33mwarning yellow\e[0m.\n", @output.string) @output.rewind @terminal.say("This should be <%= color('warning yellow', 'warning') %>.") assert_equal("This should be \e[1m\e[33mwarning yellow\e[0m.\n", @output.string) @output.rewind @terminal.say("This should be <%= color('warning yellow', 'WarNing') %>.") assert_equal("This should be \e[1m\e[33mwarning yellow\e[0m.\n", @output.string) @output.rewind # Check that keys are available, and as expected assert_equal %w[critical error warning notice info debug row_even row_odd].sort, HighLine.color_scheme.keys.sort # Color scheme doesn't care if we use symbols or strings. # And it isn't case-sensitive warning1 = HighLine.color_scheme[:warning] warning2 = HighLine.color_scheme["warning"] warning3 = HighLine.color_scheme[:wArning] warning4 = HighLine.color_scheme["warniNg"] assert_instance_of HighLine::Style, warning1 assert_instance_of HighLine::Style, warning2 assert_instance_of HighLine::Style, warning3 assert_instance_of HighLine::Style, warning4 assert_equal warning1, warning2 assert_equal warning1, warning3 assert_equal warning1, warning4 # Nonexistent keys return nil assert_nil HighLine.color_scheme[:nonexistent] # Same as above, for definitions defn1 = HighLine.color_scheme.definition(:warning) defn2 = HighLine.color_scheme.definition("warning") defn3 = HighLine.color_scheme.definition(:wArning) defn4 = HighLine.color_scheme.definition("warniNg") assert_instance_of Array, defn1 assert_instance_of Array, defn2 assert_instance_of Array, defn3 assert_instance_of Array, defn4 assert_equal [:bold, :yellow], defn1 assert_equal [:bold, :yellow], defn2 assert_equal [:bold, :yellow], defn3 assert_equal [:bold, :yellow], defn4 assert_nil HighLine.color_scheme.definition(:nonexistent) color_scheme_hash = HighLine.color_scheme.to_hash assert_instance_of Hash, color_scheme_hash assert_equal %w[critical error warning notice info debug row_even row_odd].sort, color_scheme_hash.keys.sort assert_instance_of Array, HighLine.color_scheme.definition(:warning) assert_equal [:bold, :yellow], HighLine.color_scheme.definition(:warning) # turn it back off, should raise an exception HighLine.color_scheme = nil assert_raises(NameError) do @terminal.say("This should be <%= color('nothing at all', :error) %>.") end end end highline-2.0.3/test/test_helper.rb000066400000000000000000000010701355001426500171240ustar00rootroot00000000000000#!/usr/bin/env ruby # coding: utf-8 require "English" # Run code coverage only for mri require "simplecov" if RUBY_ENGINE == "ruby" # Compatibility module for StringIO, File # and Tempfile. Necessary for some tests. require "highline/io_console_compatible" require "highline" debug_message = "Tests will be run under:\n" debug_message << " - #{HighLine.new.terminal.class}\n" debug_message << " - HighLine::VERSION #{HighLine::VERSION}\n" debug_message << " - #{RUBY_DESCRIPTION}\n" if defined? RUBY_DESCRIPTION puts debug_message require "minitest/autorun" highline-2.0.3/test/test_highline.rb000077500000000000000000001403621355001426500174470ustar00rootroot00000000000000#!/usr/bin/env ruby # coding: utf-8 # tc_highline.rb # # Created by James Edward Gray II on 2005-04-26. # Copyright 2005 Gray Productions. All rights reserved. # # This is Free Software. See LICENSE and COPYING for details. require "test_helper" require "highline" require "stringio" require "readline" require "tempfile" # if HighLine::CHARACTER_MODE == "Win32API" # class HighLine # # Override Windows' character reading so it's not tied to STDIN. # def get_character( input = STDIN ) # input.getc # end # end # end class TestHighLine < Minitest::Test def setup HighLine.reset @input = StringIO.new @output = StringIO.new @terminal = HighLine.new(@input, @output) end def test_agree_valid_yes_answers valid_yes_answers = %w[y yes Y YES] valid_yes_answers.each do |user_input| @input << "#{user_input}\n" @input.rewind assert_equal true, @terminal.agree("Yes or no? ") assert_equal "Yes or no? ", @output.string @input.truncate(@input.rewind) @output.truncate(@output.rewind) end end def test_agree_valid_no_answers valid_no_answers = %w[n no N NO] valid_no_answers.each do |user_input| @input << "#{user_input}\n" @input.rewind assert_equal false, @terminal.agree("Yes or no? ") assert_equal "Yes or no? ", @output.string @input.truncate(@input.rewind) @output.truncate(@output.rewind) end end def test_agree_invalid_answers invalid_answers = ["ye", "yuk", "nope", "Oh yes", "Oh no", "Hell no!"] invalid_answers.each do |user_input| # Each invalid answer, should be followed by a 'y' # (as the question is reasked) @input << "#{user_input}\ny\n" @input.rewind assert_equal true, @terminal.agree("Yes or no? ") # It reasks the question if the answer is invalid assert_equal "Yes or no? Please enter \"yes\" or \"no\".\nYes or no? ", @output.string @input.truncate(@input.rewind) @output.truncate(@output.rewind) end end def test_agree_with_getc @input.truncate(@input.rewind) @input << "yellow" @input.rewind assert_equal(true, @terminal.agree("Yes or no? ", :getc)) end def test_agree_with_block @input << "\n\n" @input.rewind assert_equal(true, @terminal.agree("Yes or no? ") { |q| q.default = "y" }) assert_equal(false, @terminal.agree("Yes or no? ") { |q| q.default = "n" }) end def test_ask name = "James Edward Gray II" @input << name << "\n" @input.rewind assert_equal(name, @terminal.ask("What is your name? ")) assert_raises(EOFError) { @terminal.ask("Any input left? ") } end def test_ask_string name = "James Edward Gray II" @input << name << "\n" @input.rewind assert_equal(name, @terminal.ask("What is your name? ", String)) assert_raises(EOFError) { @terminal.ask("Any input left? ", String) } end def test_ask_string_converting name = "James Edward Gray II" @input << name << "\n" @input.rewind answer = @terminal.ask("What is your name? ", String) assert_instance_of HighLine::String, answer @input.rewind answer = @terminal.ask("What is your name? ", HighLine::String) assert_instance_of HighLine::String, answer assert_raises(EOFError) do @terminal.ask("Any input left? ", HighLine::String) end end def test_indent text = "Testing...\n" @terminal.indent_level = 1 @terminal.say(text) assert_equal(" " * 3 + text, @output.string) @output.truncate(@output.rewind) @terminal.indent_level = 3 @terminal.say(text) assert_equal(" " * 9 + text, @output.string) @output.truncate(@output.rewind) @terminal.indent_level = 0 @terminal.indent_size = 5 @terminal.indent(2, text) assert_equal(" " * 10 + text, @output.string) @output.truncate(@output.rewind) @terminal.indent_level = 0 @terminal.indent_size = 4 @terminal.indent do @terminal.say(text) end assert_equal(" " * 4 + text, @output.string) @output.truncate(@output.rewind) @terminal.indent_size = 2 @terminal.indent(3) do |t| t.say(text) end assert_equal(" " * 6 + text, @output.string) @output.truncate(@output.rewind) @terminal.indent do |t| t.indent do t.indent do t.indent do |tt| tt.say(text) end end end end assert_equal(" " * 8 + text, @output.string) text = "Multi\nLine\nIndentation\n" indent = " " * 4 @terminal.indent_level = 2 @output.truncate(@output.rewind) @terminal.say(text) assert_equal("#{indent}Multi\n#{indent}Line\n#{indent}Indentation\n", @output.string) @output.truncate(@output.rewind) @terminal.multi_indent = false @terminal.say(text) assert_equal("#{indent}Multi\nLine\nIndentation\n", @output.string) @output.truncate(@output.rewind) @terminal.indent(0, text, true) assert_equal("#{indent}Multi\n#{indent}Line\n#{indent}Indentation\n", @output.string) end def test_newline @terminal.newline @terminal.newline assert_equal("\n\n", @output.string) end def test_bug_fixes # auto-complete bug @input << "ruby\nRuby\n" @input.rewind languages = [:Perl, :Python, :Ruby] answer = @terminal.ask("What is your favorite programming language? ", languages) assert_equal(languages.last, answer) @input.truncate(@input.rewind) @input << "ruby\n" @input.rewind answer = @terminal.ask("What is your favorite programming language? ", languages) do |q| q.case = :capitalize end assert_equal(languages.last, answer) # poor auto-complete error message @input.truncate(@input.rewind) @input << "lisp\nruby\n" @input.rewind @output.truncate(@output.rewind) answer = @terminal.ask("What is your favorite programming language? ", languages) do |q| q.case = :capitalize end assert_equal(languages.last, answer) assert_equal("What is your favorite programming language? " \ "You must choose one of [Perl, Python, Ruby].\n" \ "? ", @output.string) end def test_case_changes @input << "jeg2\n" @input.rewind answer = @terminal.ask("Enter your initials ") do |q| q.case = :up end assert_equal("JEG2", answer) @input.truncate(@input.rewind) @input << "cRaZY\n" @input.rewind answer = @terminal.ask("Enter a search string: ") do |q| q.case = :down end assert_equal("crazy", answer) end def test_ask_with_overwrite @input << "Yes, sure!\n" @input.rewind answer = @terminal.ask("Do you like Ruby? ") do |q| q.overwrite = true q.echo = false end assert_equal("Yes, sure!", answer) erase_sequence = "\r#{HighLine.Style(:erase_line).code}" assert_equal("Do you like Ruby? #{erase_sequence}", @output.string) end def test_ask_with_overwrite_and_character_mode @input << "Y" @input.rewind answer = @terminal.ask("Do you like Ruby (Y/N)? ") do |q| q.overwrite = true q.echo = false q.character = true end assert_equal("Y", answer) erase_sequence = "\r#{HighLine.Style(:erase_line).code}" assert_equal("Do you like Ruby (Y/N)? #{erase_sequence}", @output.string) end def test_character_echo @input << "password\r" @input.rewind answer = @terminal.ask("Please enter your password: ") do |q| q.echo = "*" end assert_equal("password", answer) assert_equal("Please enter your password: ********\n", @output.string) @input.truncate(@input.rewind) @input << "2" @input.rewind @output.truncate(@output.rewind) answer = @terminal.ask("Select an option (1, 2 or 3): ", Integer) do |q| q.echo = "*" q.character = true end assert_equal(2, answer) assert_equal("Select an option (1, 2 or 3): *\n", @output.string) end def test_backspace_does_not_enter_prompt @input << "\b\b" @input.rewind answer = @terminal.ask("Please enter your password: ") do |q| q.echo = "*" end assert_equal("", answer) assert_equal("Please enter your password: \n", @output.string) end def test_after_some_chars_backspace_does_not_enter_prompt_when_ascii @input << "apple\b\b\b\b\b\b\b\b\b\b" @input.rewind answer = @terminal.ask("Please enter your password: ") do |q| q.echo = "*" end assert_equal("", answer) # There's only enough backspaces to clear the given string assert_equal(5, @output.string.count("\b")) end def test_after_some_chars_backspace_does_not_enter_prompt_when_utf8 @input << "maçã\b\b\b\b\b\b\b\b" @input.rewind answer = @terminal.ask("Please enter your password: ") do |q| q.echo = "*" end assert_equal("", answer) # There's only enough backspaces to clear the given string assert_equal(4, @output.string.count("\b")) end def test_readline_mode # # Rubinius (and JRuby) seems to be ignoring # Readline input and output assignments. This # ruins testing. # # But it doesn't mean readline is not working # properly on rubinius or jruby. # terminal = @terminal.terminal if terminal.jruby? || terminal.rubinius? || terminal.windows? skip "We can't test Readline on JRuby, Rubinius and Windows yet" end # Creating Tempfiles here because Readline.input # and Readline.output only accepts a File object # as argument (not any duck type as StringIO) temp_stdin = Tempfile.new "temp_stdin" temp_stdout = Tempfile.new "temp_stdout" Readline.input = @input = File.open(temp_stdin.path, "w+") Readline.output = @output = File.open(temp_stdout.path, "w+") @terminal = HighLine.new(@input, @output) @input << "any input\n" @input.rewind answer = @terminal.ask("Prompt: ") do |q| q.readline = true end @output.rewind output = @output.read assert_equal "any input", answer assert_match "Prompt: any input\n", output @input.close @output.close Readline.input = STDIN Readline.output = STDOUT end def test_readline_mode_with_limit_set temp_stdin = Tempfile.new "temp_stdin" temp_stdout = Tempfile.new "temp_stdout" Readline.input = @input = File.open(temp_stdin.path, "w+") Readline.output = @output = File.open(temp_stdout.path, "w+") @terminal = HighLine.new(@input, @output) @input << "any input\n" @input.rewind answer = @terminal.ask("Prompt: ") do |q| q.limit = 50 q.readline = true end @output.rewind output = @output.read assert_equal "any input", answer assert_equal "Prompt: any input\n", output @input.close @output.close Readline.input = STDIN Readline.output = STDOUT end def test_readline_on_non_echo_question_has_prompt @input << "you can't see me" @input.rewind answer = @terminal.ask("Please enter some hidden text: ") do |q| q.readline = true q.echo = "*" end assert_equal("you can't see me", answer) assert_equal("Please enter some hidden text: ****************\n", @output.string) end def test_character_reading # WARNING: This method does NOT cover Unix and Windows savvy testing! @input << "12345" @input.rewind answer = @terminal.ask("Enter a single digit: ", Integer) do |q| q.character = :getc end assert_equal(1, answer) end def test_frozen_statement @terminal.say("This is a frozen statement".freeze) assert_equal("This is a frozen statement\n", @output.string) end def test_color @terminal.say("This should be <%= BLUE %>blue<%= CLEAR %>!") assert_equal("This should be \e[34mblue\e[0m!\n", @output.string) @output.truncate(@output.rewind) @terminal.say("This should be " \ "<%= BOLD + ON_WHITE %>bold on white<%= CLEAR %>!") assert_equal("This should be \e[1m\e[47mbold on white\e[0m!\n", @output.string) @output.truncate(@output.rewind) @terminal.say("This should be <%= color('cyan', CYAN) %>!") assert_equal("This should be \e[36mcyan\e[0m!\n", @output.string) @output.truncate(@output.rewind) @terminal.say("This should be " \ "<%= color('blinking on red', :blink, :on_red) %>!") assert_equal("This should be \e[5m\e[41mblinking on red\e[0m!\n", @output.string) @output.truncate(@output.rewind) @terminal.say("This should be <%= NONE %>none<%= CLEAR %>!") assert_equal("This should be \e[38mnone\e[0m!\n", @output.string) @output.truncate(@output.rewind) @terminal.say("This should be <%= RGB_906030 %>rgb_906030<%= CLEAR %>!") assert_equal("This should be \e[38;5;137mrgb_906030\e[0m!\n", @output.string) @output.truncate(@output.rewind) @terminal.say( "This should be <%= ON_RGB_C06030 %>on_rgb_c06030<%= CLEAR %>!" ) assert_equal("This should be \e[48;5;173mon_rgb_c06030\e[0m!\n", @output.string) # Relying on const_missing assert_instance_of HighLine::Style, HighLine::ON_RGB_C06031_STYLE assert_instance_of String, HighLine::ON_RGB_C06032 assert_raises(NameError) { HighLine::ON_RGB_ZZZZZZ } # Retrieving color_code from a style assert_equal "\e[41m", @terminal.color_code([HighLine::ON_RED_STYLE]) @output.truncate(@output.rewind) # Does class method work, too? @terminal.say( "This should be <%= HighLine.color('reverse underlined magenta', " \ ":reverse, :underline, :magenta) %>!" ) assert_equal( "This should be \e[7m\e[4m\e[35mreverse underlined magenta\e[0m!\n", @output.string ) @output.truncate(@output.rewind) # turn off color old_setting = HighLine.use_color? HighLine.use_color = false @terminal.use_color = false @terminal.say("This should be <%= color('cyan', CYAN) %>!") assert_equal("This should be cyan!\n", @output.string) HighLine.use_color = old_setting @terminal.use_color = old_setting end def test_color_setting_per_instance require "highline/import" old_glob_instance = HighLine.default_instance old_setting = HighLine.use_color? gterm_input = StringIO.new gterm_output = StringIO.new HighLine.default_instance = HighLine.new(gterm_input, gterm_output) # It can set coloring at HighLine class cli_input = StringIO.new cli_output = StringIO.new cli = HighLine.new(cli_input, cli_output) # Testing with both use_color setted to true HighLine.use_color = true @terminal.use_color = true cli.use_color = true say("This should be <%= color('cyan', CYAN) %>!") assert_equal("This should be \e[36mcyan\e[0m!\n", gterm_output.string) @terminal.say("This should be <%= color('cyan', CYAN) %>!") assert_equal("This should be \e[36mcyan\e[0m!\n", @output.string) cli.say("This should be <%= color('cyan', CYAN) %>!") assert_equal("This should be \e[36mcyan\e[0m!\n", cli_output.string) gterm_output.truncate(gterm_output.rewind) @output.truncate(@output.rewind) cli_output.truncate(cli_output.rewind) # Testing with both use_color setted to false HighLine.use_color = false @terminal.use_color = false cli.use_color = false say("This should be <%= color('cyan', CYAN) %>!") assert_equal("This should be cyan!\n", gterm_output.string) @terminal.say("This should be <%= color('cyan', CYAN) %>!") assert_equal("This should be cyan!\n", @output.string) cli.say("This should be <%= color('cyan', CYAN) %>!") assert_equal("This should be cyan!\n", cli_output.string) gterm_output.truncate(gterm_output.rewind) @output.truncate(@output.rewind) cli_output.truncate(cli_output.rewind) # Now check when class and instance doesn't agree about use_color # Class false, instance true HighLine.use_color = false @terminal.use_color = false cli.use_color = true say("This should be <%= color('cyan', CYAN) %>!") assert_equal("This should be cyan!\n", gterm_output.string) @terminal.say("This should be <%= color('cyan', CYAN) %>!") assert_equal("This should be cyan!\n", @output.string) cli.say("This should be <%= color('cyan', CYAN) %>!") assert_equal("This should be \e[36mcyan\e[0m!\n", cli_output.string) gterm_output.truncate(gterm_output.rewind) @output.truncate(@output.rewind) cli_output.truncate(cli_output.rewind) # Class true, instance false HighLine.use_color = true @terminal.use_color = true cli.use_color = false say("This should be <%= color('cyan', CYAN) %>!") assert_equal("This should be \e[36mcyan\e[0m!\n", gterm_output.string) @terminal.say("This should be <%= color('cyan', CYAN) %>!") assert_equal("This should be \e[36mcyan\e[0m!\n", @output.string) cli.say("This should be <%= color('cyan', CYAN) %>!") assert_equal("This should be cyan!\n", cli_output.string) gterm_output.truncate(gterm_output.rewind) @output.truncate(@output.rewind) cli_output.truncate(cli_output.rewind) HighLine.use_color = old_setting @terminal.use_color = old_setting HighLine.default_instance = old_glob_instance end def test_reset_use_color HighLine.use_color = false refute HighLine.use_color? HighLine.reset_use_color assert HighLine.use_color? end def test_reset_use_color_when_highline_reset HighLine.use_color = false refute HighLine.use_color? HighLine.reset assert HighLine.use_color? end def test_uncolor # instance method assert_equal( "This should be reverse underlined magenta!\n", @terminal.uncolor( "This should be \e[7m\e[4m\e[35mreverse underlined magenta\e[0m!\n" ) ) @output.truncate(@output.rewind) # class method assert_equal( "This should be reverse underlined magenta!\n", HighLine.uncolor( "This should be \e[7m\e[4m\e[35mreverse underlined magenta\e[0m!\n" ) ) @output.truncate(@output.rewind) # RGB color assert_equal( "This should be rgb_906030!\n", @terminal.uncolor( "This should be \e[38;5;137mrgb_906030\e[0m!\n" ) ) end def test_grey_is_the_same_of_gray @terminal.say("<%= GRAY %>") gray_code = @output.string.dup @output.truncate(@output.rewind) @terminal.say("<%= GREY %>") grey_code = @output.string.dup @output.truncate(@output.rewind) assert_equal gray_code, grey_code end def test_light_is_the_same_as_bright @terminal.say("<%= BRIGHT_BLUE %>") bright_blue_code = @output.string.dup @output.truncate(@output.rewind) @terminal.say("<%= LIGHT_BLUE %>") light_blue_code = @output.string.dup @output.truncate(@output.rewind) assert_equal bright_blue_code, light_blue_code end def test_confirm @input << "junk.txt\nno\nsave.txt\ny\n" @input.rewind answer = @terminal.ask("Enter a filename: ") do |q| q.confirm = "Are you sure you want to overwrite <%= answer %>? " q.responses[:ask_on_error] = :question end assert_equal("save.txt", answer) assert_equal("Enter a filename: " \ "Are you sure you want to overwrite junk.txt? " \ "Enter a filename: " \ "Are you sure you want to overwrite save.txt? ", @output.string) @input.truncate(@input.rewind) @input << "junk.txt\nyes\nsave.txt\nn\n" @input.rewind @output.truncate(@output.rewind) answer = @terminal.ask("Enter a filename: ") do |q| q.confirm = "Are you sure you want to overwrite <%= answer %>? " end assert_equal("junk.txt", answer) assert_equal("Enter a filename: " \ "Are you sure you want to overwrite junk.txt? ", @output.string) @input.truncate(@input.rewind) @input << "junk.txt\nyes\nsave.txt\nn\n" @input.rewind @output.truncate(@output.rewind) scoped_variable = { "junk.txt" => "20mb" } answer = @terminal.ask("Enter a filename: ") do |q| q.confirm = proc do |checking_answer| "Are you sure you want to overwrite #{checking_answer} with size " \ "of #{scoped_variable[checking_answer]}? " end end assert_equal("junk.txt", answer) assert_equal("Enter a filename: " \ "Are you sure you want to overwrite junk.txt " \ "with size of 20mb? ", @output.string) end def test_generic_confirm_with_true @input << "junk.txt\nno\nsave.txt\ny\n" @input.rewind answer = @terminal.ask("Enter a filename: ") do |q| q.confirm = true q.responses[:ask_on_error] = :question end assert_equal("save.txt", answer) assert_equal("Enter a filename: " \ "Are you sure? " \ "Enter a filename: " \ "Are you sure? ", @output.string) @input.truncate(@input.rewind) @input << "junk.txt\nyes\nsave.txt\nn\n" @input.rewind @output.truncate(@output.rewind) answer = @terminal.ask("Enter a filename: ") do |q| q.confirm = true end assert_equal("junk.txt", answer) assert_equal("Enter a filename: " \ "Are you sure? ", @output.string) end def test_defaults @input << "\nNo Comment\n" @input.rewind answer = @terminal.ask("Are you sexually active? ") do |q| q.validate = /\Ay(?:es)?|no?|no comment\Z/i end assert_equal("No Comment", answer) @input.truncate(@input.rewind) @input << "\nYes\n" @input.rewind @output.truncate(@output.rewind) answer = @terminal.ask("Are you sexually active? ") do |q| q.default = "No Comment" q.validate = /\Ay(?:es)?|no?|no comment\Z/i end assert_equal("No Comment", answer) assert_equal("Are you sexually active? |No Comment| ", @output.string) end def test_default_with_String @input << "\n" @input.rewind answer = @terminal.ask("Question: ") do |q| q.default = "string" end assert_equal "string", answer assert_equal "Question: |string| ", @output.string end def test_default_with_Symbol # With a Symbol, it should show up the String version # at prompt, but return the Symbol as answer @input << "\n" @input.rewind answer = @terminal.ask("Question: ") do |q| q.default = :string end assert_equal :string, answer assert_equal "Question: |string| ", @output.string end def test_default_with_non_String_objects # With a non-string object, it should not show # any 'default' at prompt line. And should # return the "default" object, without conversion. @input << "\n" @input.rewind default_non_string_object = Object.new answer = @terminal.ask("Question: ") do |q| q.default = default_non_string_object end assert_equal default_non_string_object, answer assert_equal "Question: ", @output.string end def test_string_preservation @input << "Maybe\nYes\n" @input.rewind my_string = "Is that your final answer? " @terminal.ask(my_string) { |q| q.default = "Possibly" } @terminal.ask(my_string) { |q| q.default = "Maybe" } assert_equal("Is that your final answer? ", my_string) end def test_empty @input << "\n" @input.rewind answer = @terminal.ask("") do |q| q.default = "yes" q.validate = /\Ay(?:es)?|no?\Z/i end assert_equal("yes", answer) end def test_erb @terminal.say("The integers from 1 to 10 are:\n" \ "% (1...10).each do |n|\n" \ "\t<%= n %>,\n" \ "% end\n" \ "\tand 10") assert_equal("The integers from 1 to 10 are:\n" \ "\t1,\n\t2,\n\t3,\n\t4,\n\t5,\n" \ "\t6,\n\t7,\n\t8,\n\t9,\n\tand 10\n", @output.string) end def test_files @input << "#{File.basename(__FILE__)[0, 7]}\n" @input.rewind assert_equal "test_hi\n", @input.read @input.rewind file = @terminal.ask("Select a file: ", File) do |q| q.directory = File.expand_path(File.dirname(__FILE__)) q.glob = "*.rb" end assert_instance_of(File, file) assert_equal("#!/usr/bin/env ruby\n", file.gets) file.close @input.rewind pathname = @terminal.ask("Select a file: ", Pathname) do |q| q.directory = File.expand_path(File.dirname(__FILE__)) q.glob = "*.rb" end assert_instance_of(Pathname, pathname) assert_equal(File.size(__FILE__), pathname.size) end def test_gather_with_integer @input << "James\nDana\nStorm\nGypsy\n\n" @input.rewind answers = @terminal.ask("Enter four names:") do |q| q.gather = 4 end assert_equal(%w[James Dana Storm Gypsy], answers) assert_equal("\n", @input.gets) assert_equal("Enter four names:\n", @output.string) end def test_gather_with_an_empty_string @input << "James\nDana\nStorm\nGypsy\n\n" @input.rewind answers = @terminal.ask("Enter four names:") do |q| q.gather = "" end assert_equal(%w[James Dana Storm Gypsy], answers) end def test_gather_with_regexp @input << "James\nDana\nStorm\nGypsy\n\n" @input.rewind answers = @terminal.ask("Enter four names:") do |q| q.gather = /^\s*$/ end assert_equal(%w[James Dana Storm Gypsy], answers) end def test_gather_with_hash @input << "29\n49\n30\n" @input.rewind answers = @terminal.ask("<%= key %>: ", Integer) do |q| q.gather = { "Age" => 0, "Wife's Age" => 0, "Father's Age" => 0 } end assert_equal({ "Age" => 29, "Wife's Age" => 30, "Father's Age" => 49 }, answers) assert_equal("Age: Father's Age: Wife's Age: ", @output.string) end def test_typing_verification @input << "all work and no play makes jack a dull boy\n" * 3 @input.rewind answer = @terminal.ask("How's work? ") do |q| q.gather = 3 q.verify_match = true end assert_equal("all work and no play makes jack a dull boy", answer) @input.truncate(@input.rewind) @input << "all play and no work makes jack a mere toy\n" @input << "all work and no play makes jack a dull boy\n" * 5 @input.rewind @output.truncate(@output.rewind) answer = @terminal.ask("How are things going? ") do |q| q.gather = 3 q.verify_match = true q.responses[:mismatch] = "Typing mismatch!" q.responses[:ask_on_error] = "" end assert_equal("all work and no play makes jack a dull boy", answer) # now try using a hash for gather @input.truncate(@input.rewind) @input << "Password\nPassword\n" @input.rewind @output.truncate(@output.rewind) answer = @terminal.ask("<%= key %>: ") do |q| q.verify_match = true q.gather = { "Enter a password" => "", "Please type it again" => "" } end assert_equal("Password", answer) @input.truncate(@input.rewind) @input << "Password\nMistake\nPassword\nPassword\n" @input.rewind @output.truncate(@output.rewind) answer = @terminal.ask("<%= key %>: ") do |q| q.verify_match = true q.responses[:mismatch] = "Typing mismatch!" q.responses[:ask_on_error] = "" q.gather = { "Enter a password" => "", "Please type it again" => "" } end assert_equal("Password", answer) assert_equal("Enter a password: " \ "Please type it again: " \ "Typing mismatch!\n" \ "Enter a password: " \ "Please type it again: ", @output.string) end def test_lists digits = %w[Zero One Two Three Four Five Six Seven Eight Nine] erb_digits = digits.dup erb_digits[erb_digits.index("Five")] = "<%= color('Five', :blue) %%>" @terminal.say("<%= list(#{digits.inspect}) %>") assert_equal(digits.map { |d| "#{d}\n" }.join, @output.string) @output.truncate(@output.rewind) @terminal.say("<%= list(#{digits.inspect}, :inline) %>") assert_equal(digits[0..-2].join(", ") + " or #{digits.last}\n", @output.string) @output.truncate(@output.rewind) @terminal.say("<%= list(#{digits.inspect}, :inline, ' and ') %>") assert_equal(digits[0..-2].join(", ") + " and #{digits.last}\n", @output.string) @output.truncate(@output.rewind) @terminal.say("<%= list(#{digits.inspect}, :columns_down, 3) %>") assert_equal("Zero Four Eight\n" \ "One Five Nine \n" \ "Two Six \n" \ "Three Seven\n", @output.string) @output.truncate(@output.rewind) @terminal.say("<%= list(#{erb_digits.inspect}, :columns_down, 3) %>") assert_equal("Zero Four Eight\n" \ "One \e[34mFive\e[0m Nine \n" \ "Two Six \n" \ "Three Seven\n", @output.string) colums_of_twenty = ["12345678901234567890"] * 5 @output.truncate(@output.rewind) @terminal.say("<%= list(#{colums_of_twenty.inspect}, :columns_down) %>") assert_equal("12345678901234567890 12345678901234567890 " \ "12345678901234567890\n" \ "12345678901234567890 12345678901234567890\n", @output.string) @output.truncate(@output.rewind) @terminal.say("<%= list(#{digits.inspect}, :columns_across, 3) %>") assert_equal("Zero One Two \n" \ "Three Four Five \n" \ "Six Seven Eight\n" \ "Nine \n", @output.string) colums_of_twenty.pop @output.truncate(@output.rewind) @terminal.say("<%= list( #{colums_of_twenty.inspect}, :columns_across ) %>") assert_equal("12345678901234567890 12345678901234567890 " \ "12345678901234567890\n" \ "12345678901234567890\n", @output.string) @output.truncate(@output.rewind) wide = %w[0123456789 a b c d e f g h i j k l m n o p q r s t u v w x y z] @terminal.say("<%= list( #{wide.inspect}, :uneven_columns_across ) %>") assert_equal("0123456789 a b c d e f g h i j k l m n o " \ "p q r s t u v w\n" \ "x y z\n", @output.string) @output.truncate(@output.rewind) @terminal.say("<%= list( #{wide.inspect}, :uneven_columns_across, 10 ) %>") assert_equal("0123456789 a b c d e f g h i\n" \ "j k l m n o p q r s\n" \ "t u v w x y z\n", @output.string) @output.truncate(@output.rewind) @terminal.say("<%= list( #{wide.inspect}, :uneven_columns_down ) %>") assert_equal("0123456789 b d f h j l n p r t v x z\n" \ "a c e g i k m o q s u w y\n", @output.string) @output.truncate(@output.rewind) @terminal.say("<%= list( #{wide.inspect}, :uneven_columns_down, 10 ) %>") assert_equal("0123456789 c f i l o r u x\n" \ "a d g j m p s v y\n" \ "b e h k n q t w z\n", @output.string) end def test_lists_with_zero_items modes = [nil, :rows, :inline, :columns_across, :columns_down] modes.each do |mode| result = @terminal.list([], mode) assert_equal("", result) end end def test_lists_with_nil_items modes = [nil] modes.each do |mode| result = @terminal.list([nil], mode) assert_equal("\n", result) end end def test_lists_with_one_item items = ["Zero"] modes = { nil => "Zero\n", :rows => "Zero\n", :inline => "Zero", :columns_across => "Zero\n", :columns_down => "Zero\n" } modes.each do |mode, expected| result = @terminal.list(items, mode) assert_equal(expected, result) end end def test_lists_with_two_items items = %w[Zero One] modes = { nil => "Zero\nOne\n", :rows => "Zero\nOne\n", :inline => "Zero or One", :columns_across => "Zero One \n", :columns_down => "Zero One \n" } modes.each do |mode, expected| result = @terminal.list(items, mode) assert_equal(expected, result) end end def test_lists_with_three_items items = %w[Zero One Two] modes = { nil => "Zero\nOne\nTwo\n", :rows => "Zero\nOne\nTwo\n", :inline => "Zero, One or Two", :columns_across => "Zero One Two \n", :columns_down => "Zero One Two \n" } modes.each do |mode, expected| result = @terminal.list(items, mode) assert_equal(expected, result) end end def test_mode main_char_modes = %w[ HighLine::Terminal::IOConsole HighLine::Terminal::NCurses HighLine::Terminal::UnixStty ] assert( main_char_modes.include?(@terminal.terminal.character_mode), "#{@terminal.terminal.character_mode} not in list" ) end class NameClass def self.parse(string) raise ArgumentError, "Invalid name format." unless string =~ /^\s*(\w+),\s*(\w+)\s+(\w+)\s*$/ new(Regexp.last_match(2), Regexp.last_match(3), Regexp.last_match(1)) end def initialize(first, middle, last) @first = first @middle = middle @last = last end attr_reader :first, :middle, :last end def test_my_class_conversion @input << "Gray, James Edward\n" @input.rewind answer = @terminal.ask("Your name? ", NameClass) do |q| q.validate = lambda do |name| names = name.split(/,\s*/) return false unless names.size == 2 return false if names.first =~ /\s/ names.last.split.size == 2 end end assert_instance_of(NameClass, answer) assert_equal("Gray", answer.last) assert_equal("James", answer.first) assert_equal("Edward", answer.middle) end def test_no_echo @input << "password\r" @input.rewind answer = @terminal.ask("Please enter your password: ") do |q| q.echo = false end assert_equal("password", answer) assert_equal("Please enter your password: \n", @output.string) @input.rewind @output.truncate(@output.rewind) answer = @terminal.ask("Pick a letter or number: ") do |q| q.character = true q.echo = false end assert_equal("p", answer) assert_equal("a", @input.getc.chr) assert_equal("Pick a letter or number: \n", @output.string) end def test_correct_string_encoding_when_echo_false @input << "ação\r" # An UTF-8 portuguese word for 'action' @input.rewind answer = @terminal.ask("Please enter your password: ") do |q| q.echo = false end assert_equal "ação", answer assert_equal Encoding::UTF_8, answer.encoding end def test_backspace_with_ascii_when_echo_false @input << "password\b\r" @input.rewind answer = @terminal.ask("Please enter your password: ") do |q| q.echo = false end refute_equal("password", answer) assert_equal("passwor", answer) end def test_backspace_with_utf8_when_echo_false @input << "maçã\b\r" @input.rewind answer = @terminal.ask("Please enter your password: ") do |q| q.echo = false end refute_equal("maçã", answer) assert_equal("maç", answer) end def test_echoing_with_utf8_when_echo_is_star @input << "maçã\r" @input.rewind answer = @terminal.ask("Type: ") do |q| q.echo = "*" end assert_equal("Type: ****\n", @output.string) assert_equal("maçã", answer) end def test_range_requirements @input << "112\n-541\n28\n" @input.rewind answer = @terminal.ask("Tell me your age.", Integer) do |q| q.in = 0..105 end assert_equal(28, answer) assert_equal("Tell me your age.\n" \ "Your answer isn't within the expected range " \ "(included in 0..105).\n" \ "? " \ "Your answer isn't within the expected range " \ "(included in 0..105).\n" \ "? ", @output.string) @input.truncate(@input.rewind) @input << "1\n-541\n28\n" @input.rewind @output.truncate(@output.rewind) answer = @terminal.ask("Tell me your age.", Integer) do |q| q.above = 3 end assert_equal(28, answer) assert_equal("Tell me your age.\n" \ "Your answer isn't within the expected range " \ "(above 3).\n" \ "? " \ "Your answer isn't within the expected range " \ "(above 3).\n" \ "? ", @output.string) @input.truncate(@input.rewind) @input << "1\n28\n-541\n" @input.rewind @output.truncate(@output.rewind) answer = @terminal.ask("Lowest numer you can think of?", Integer) do |q| q.below = 0 end assert_equal(-541, answer) assert_equal("Lowest numer you can think of?\n" \ "Your answer isn't within the expected range " \ "(below 0).\n" \ "? " \ "Your answer isn't within the expected range " \ "(below 0).\n" \ "? ", @output.string) @input.truncate(@input.rewind) @input << "-541\n11\n6\n" @input.rewind @output.truncate(@output.rewind) answer = @terminal.ask("Enter a low even number: ", Integer) do |q| q.above = 0 q.below = 10 end assert_equal(6, answer) assert_equal("Enter a low even number: " \ "Your answer isn't within the expected range " \ "(above 0 and below 10).\n" \ "? " \ "Your answer isn't within the expected range " \ "(above 0 and below 10).\n" \ "? ", @output.string) @input.truncate(@input.rewind) @input << "1\n-541\n6\n" @input.rewind @output.truncate(@output.rewind) answer = @terminal.ask("Enter a low even number: ", Integer) do |q| q.above = 0 q.below = 10 q.in = [2, 4, 6, 8] end assert_equal(6, answer) assert_equal("Enter a low even number: " \ "Your answer isn't within the expected range " \ "(above 0, below 10, and included in [2, 4, 6, 8]).\n" \ "? " \ "Your answer isn't within the expected range " \ "(above 0, below 10, and included in [2, 4, 6, 8]).\n" \ "? ", @output.string) end def test_reask number = 61_676 @input << "Junk!\n" << number << "\n" @input.rewind answer = @terminal.ask("Favorite number? ", Integer) assert_kind_of(Integer, number) assert_equal(number, answer) assert_equal("Favorite number? " \ "You must enter a valid Integer.\n" \ "? ", @output.string) @input.rewind @output.truncate(@output.rewind) answer = @terminal.ask("Favorite number? ", Integer) do |q| q.responses[:ask_on_error] = :question q.responses[:invalid_type] = "Not a valid number!" end assert_kind_of(Integer, number) assert_equal(number, answer) assert_equal("Favorite number? " \ "Not a valid number!\n" \ "Favorite number? ", @output.string) @input.truncate(@input.rewind) @input << "gen\ngene\n" @input.rewind @output.truncate(@output.rewind) answer = @terminal.ask("Select a mode: ", [:generate, :gentle]) assert_instance_of(Symbol, answer) assert_equal(:generate, answer) assert_equal("Select a mode: " \ "Ambiguous choice. " \ "Please choose one of [generate, gentle].\n" \ "? ", @output.string) end def test_response_embedding @input << "112\n-541\n28\n" @input.rewind answer = @terminal.ask("Tell me your age.", Integer) do |q| q.in = 0..105 q.responses[:not_in_range] = "Need a #{q.answer_type}" \ " #{q.expected_range}." end assert_equal(28, answer) assert_equal("Tell me your age.\n" \ "Need a Integer included in 0..105.\n" \ "? " \ "Need a Integer included in 0..105.\n" \ "? ", @output.string) end def test_say @terminal.say("This will have a newline.") assert_equal("This will have a newline.\n", @output.string) @output.truncate(@output.rewind) @terminal.say("This will also have one newline.\n") assert_equal("This will also have one newline.\n", @output.string) @output.truncate(@output.rewind) @terminal.say("This will not have a newline. ") assert_equal("This will not have a newline. ", @output.string) @output.truncate(@output.rewind) @terminal.say("This will not have a newline.\t") assert_equal("This will not have a newline.\t", @output.string) @output.truncate(@output.rewind) @terminal.say("This will not\n end with a newline. ") assert_equal("This will not\n end with a newline. ", @output.string) @output.truncate(@output.rewind) @terminal.say("This will \nend with a newline.") assert_equal("This will \nend with a newline.\n", @output.string) @output.truncate(@output.rewind) colorized = @terminal.color("This will not have a newline. ", :green) @terminal.say(colorized) assert_equal("\e[32mThis will not have a newline. \e[0m", @output.string) @output.truncate(@output.rewind) colorized = @terminal.color("This will have a newline.", :green) @terminal.say(colorized) assert_equal("\e[32mThis will have a newline.\e[0m\n", @output.string) @output.truncate(@output.rewind) @terminal.say(nil) assert_equal("", @output.string) end def test_say_handles_non_string_argument integer = 10 hash = { a: 20 } @terminal.say(integer) assert_equal String(integer), @output.string.chomp @output.truncate(@output.rewind) @terminal.say(hash) assert_equal String(hash), @output.string.chomp end def test_terminal_size assert(@terminal.terminal.terminal_size[0] > 0) assert(@terminal.terminal.terminal_size[1] > 0) end def test_type_conversion number = 61_676 @input << number << "\n" @input.rewind answer = @terminal.ask("Favorite number? ", Integer) assert_kind_of(Integer, answer) assert_equal(number, answer) @input.truncate(@input.rewind) number = 1_000_000_000_000_000_000_000_000_000_000 @input << number << "\n" @input.rewind answer = @terminal.ask("Favorite number? ", Integer) assert_kind_of(Integer, answer) assert_equal(number, answer) @input.truncate(@input.rewind) number = 10.5002 @input << number << "\n" @input.rewind answer = @terminal.ask("Favorite number? ", ->(n) { n.to_f.abs.round }) assert_kind_of(Integer, answer) assert_equal(11, answer) @input.truncate(@input.rewind) animal = :dog @input << animal << "\n" @input.rewind answer = @terminal.ask("Favorite animal? ", Symbol) assert_instance_of(Symbol, answer) assert_equal(animal, answer) @input.truncate(@input.rewind) @input << "16th June 1976\n" @input.rewind answer = @terminal.ask("Enter your birthday.", Date) assert_instance_of(Date, answer) assert_equal(16, answer.day) assert_equal(6, answer.month) assert_equal(1976, answer.year) @input.truncate(@input.rewind) pattern = "^yes|no$" @input << pattern << "\n" @input.rewind answer = @terminal.ask("Give me a pattern to match with: ", Regexp) assert_instance_of(Regexp, answer) assert_equal(/#{pattern}/, answer) @input.truncate(@input.rewind) @input << "gen\n" @input.rewind answer = @terminal.ask("Select a mode: ", [:generate, :run]) assert_instance_of(Symbol, answer) assert_equal(:generate, answer) end def test_validation @input << "system 'rm -rf /'\n105\n0b101_001\n" @input.rewind answer = @terminal.ask("Enter a binary number: ") do |q| q.validate = /\A(?:0b)?[01_]+\Z/ end assert_equal("0b101_001", answer) assert_equal("Enter a binary number: " \ "Your answer isn't valid " \ "(must match /\\A(?:0b)?[01_]+\\Z/).\n" \ "? " \ "Your answer isn't valid " \ "(must match /\\A(?:0b)?[01_]+\\Z/).\n" \ "? ", @output.string) @input.truncate(@input.rewind) @input << "Gray II, James Edward\n" \ "Gray, Dana Ann Leslie\n" \ "Gray, James Edward\n" @input.rewind answer = @terminal.ask("Your name? ") do |q| q.validate = lambda do |name| names = name.split(/,\s*/) return false unless names.size == 2 return false if names.first =~ /\s/ names.last.split.size == 2 end end assert_equal("Gray, James Edward", answer) end def test_validation_with_overriding_static_message @input << "Not valid answer\n" \ "42\n" @input.rewind answer = @terminal.ask("Enter only numbers: ") do |question| question.validate = ->(ans) { ans =~ /\d+/ } question.responses[:not_valid] = "We accept only numbers over here!" end assert_equal("42", answer) assert_equal( "Enter only numbers: We accept only numbers over here!\n" \ "? ", @output.string ) end def test_validation_with_overriding_dynamic_message @input << "Forty two\n" \ "42\n" @input.rewind answer = @terminal.ask("Enter only numbers: ") do |question| question.validate = ->(ans) { ans =~ /\d+/ } question.responses[:not_valid] = ->(ans) { "#{ans} is not a valid answer" } end assert_equal("42", answer) assert_equal( "Enter only numbers: " \ "Forty two is not a valid answer\n" \ "? ", @output.string ) end def test_whitespace @input << " A lot\tof \t space\t \there! \n" @input.rewind answer = @terminal.ask("Enter a whitespace filled string: ") do |q| q.whitespace = :chomp end assert_equal(" A lot\tof \t space\t \there! ", answer) @input.rewind answer = @terminal.ask("Enter a whitespace filled string: ") do |q| q.whitespace = :strip end assert_equal("A lot\tof \t space\t \there!", answer) @input.rewind answer = @terminal.ask("Enter a whitespace filled string: ") do |q| q.whitespace = :collapse end assert_equal(" A lot of space here! ", answer) @input.rewind answer = @terminal.ask("Enter a whitespace filled string: ") assert_equal("A lot\tof \t space\t \there!", answer) @input.rewind answer = @terminal.ask("Enter a whitespace filled string: ") do |q| q.whitespace = :strip_and_collapse end assert_equal("A lot of space here!", answer) @input.rewind answer = @terminal.ask("Enter a whitespace filled string: ") do |q| q.whitespace = :remove end assert_equal("Alotofspacehere!", answer) @input.rewind answer = @terminal.ask("Enter a whitespace filled string: ") do |q| q.whitespace = :none end assert_equal(" A lot\tof \t space\t \there! \n", answer) @input.rewind answer = @terminal.ask("Enter a whitespace filled string: ") do |q| q.whitespace = nil end assert_equal(" A lot\tof \t space\t \there! \n", answer) end def test_track_eof assert_raises(EOFError) { @terminal.ask("Any input left? ") } # turn EOF tracking old_instance = HighLine.default_instance HighLine.default_instance = HighLine.new(StringIO.new, StringIO.new) HighLine.track_eof = false begin require "highline/import" # this will still blow up, nothing available ask("And now? ") rescue StandardError # but HighLine's safe guards are off refute_equal(EOFError, $ERROR_INFO.class) end HighLine.default_instance = old_instance end def test_version refute_nil(HighLine::VERSION) assert_instance_of(String, HighLine::VERSION) assert(HighLine::VERSION.frozen?) assert_match(/\A\d+\.\d+\.\d+(-.*)?/, HighLine::VERSION) end end highline-2.0.3/test/test_import.rb000066400000000000000000000031121355001426500171560ustar00rootroot00000000000000#!/usr/bin/env ruby # coding: utf-8 # tc_import.rb # # Created by James Edward Gray II on 2005-04-26. # Copyright 2005 Gray Productions. All rights reserved. # # This is Free Software. See LICENSE and COPYING for details. require "test_helper" require "highline/import" require "stringio" class TestImport < Minitest::Test def test_import assert_respond_to(self, :agree) assert_respond_to(self, :ask) assert_respond_to(self, :choose) assert_respond_to(self, :say) end def test_healthy_default_instance_after_import refute_nil HighLine.default_instance assert_instance_of HighLine, HighLine.default_instance # If correctly initialized, it will contain several ins vars. refute_empty HighLine.default_instance.instance_variables end def test_or_ask old_instance = HighLine.default_instance input = StringIO.new output = StringIO.new HighLine.default_instance = HighLine.new(input, output) input << "10\n" input.rewind assert_equal(10, nil.or_ask("How much? ", Integer)) input.rewind assert_equal(20, "20".or_ask("How much? ", Integer)) assert_equal(20, 20.or_ask("How much? ", Integer)) assert_equal(10, 20.or_ask("How much? ", Integer) { |q| q.in = 1..10 }) ensure HighLine.default_instance = old_instance end def test_redirection old_instance = HighLine.default_instance HighLine.default_instance = HighLine.new(nil, (output = StringIO.new)) say("Testing...") assert_equal("Testing...\n", output.string) ensure HighLine.default_instance = old_instance end end highline-2.0.3/test/test_list.rb000066400000000000000000000022051355001426500166210ustar00rootroot00000000000000#!/usr/bin/env ruby # coding: utf-8 require "test_helper" require "highline/list" class TestHighLineList < Minitest::Test def setup @items = %w[a b c d e f g h i j] end def test_in_2_cols list_in_two_cols = [%w[a b], %w[c d], %w[e f], %w[g h], %w[i j]] highline_list = HighLine::List.new(@items, cols: 2) assert_equal list_in_two_cols, highline_list.list end def test_in_2_cols_col_down col_down_list = [%w[a f], %w[b g], %w[c h], %w[d i], %w[e j]] highline_list = HighLine::List.new(@items, cols: 2, col_down: true) assert_equal col_down_list, highline_list.list end def test_in_2_cols_transposed transposed_list = [%w[a c e g i], %w[b d f h j]] highline_list = HighLine::List.new(@items, cols: 2, transpose: true) assert_equal transposed_list, highline_list.list end def test_in_3_cols list_in_three_cols = [%w[a b c], %w[d e f], %w[g h i], ["j"]] highline_list = HighLine::List.new(@items, cols: 3) assert_equal list_in_three_cols, highline_list.list end end highline-2.0.3/test/test_menu.rb000077500000000000000000000465761355001426500166400ustar00rootroot00000000000000#!/usr/bin/env ruby # coding: utf-8 # tc_menu.rb # # Created by Gregory Thomas Brown on 2005-05-10. # Copyright 2005. All rights reserved. # # This is Free Software. See LICENSE and COPYING for details. require "test_helper" require "highline" require "stringio" class TestMenu < Minitest::Test def setup HighLine.reset @input = StringIO.new @output = StringIO.new @terminal = HighLine.new(@input, @output) end def test_choices @input << "2\n" @input.rewind output = @terminal.choose do |menu| menu.choices("Sample1", "Sample2", "Sample3") end assert_equal("Sample2", output) end def test_default @input << "\n" @input.rewind output = @terminal.choose do |menu| menu.choices("Sample1", "Sample2", "Sample3") menu.default = "Sample1" end assert_equal("Sample1", output) end def test_flow @input << "Sample1\n" @input.rewind @terminal.choose do |menu| # Default: menu.flow = :rows menu.choice "Sample1" menu.choice "Sample2" menu.choice "Sample3" end assert_equal("1. Sample1\n2. Sample2\n3. Sample3\n? ", @output.string) @output.truncate(@output.rewind) @input.rewind @terminal.choose do |menu| menu.flow = :columns_across menu.choice "Sample1" menu.choice "Sample2" menu.choice "Sample3" end assert_equal("1. Sample1 2. Sample2 3. Sample3\n? ", @output.string) @output.truncate(@output.rewind) @input.rewind @terminal.choose do |menu| menu.flow = :inline menu.index = :none menu.choice "Sample1" menu.choice "Sample2" menu.choice "Sample3" end assert_equal("Sample1, Sample2 or Sample3? ", @output.string) end def test_unicode_flow @input << "1\n" @input.rewind @terminal.choose do |menu| # Default: menu.flow = :rows menu.choice "Unicode right single quotation mark: ’" end assert_equal( "1. Unicode right single quotation mark: ’\n? ". encode(@output.external_encoding, undef: :replace), @output.string ) end def test_text_override_index_selects_name @input << "1\n" @input.rewind selected = @terminal.choose do |menu| menu.choice("Sample1", nil, "Sample2") menu.choice("Sample2", nil, "Sample1") end assert_equal(selected, "Sample1") assert_equal("1. Sample2\n" \ "2. Sample1\n" \ "? ", @output.string) end def test_text_override_selections_matches_name @input << "Sample2\n" @input.rewind selected = @terminal.choose do |menu| menu.choice("Sample1", nil, "Sample2") menu.choice("Sample2", nil, "Sample1") end assert_equal(selected, "Sample2") assert_equal("1. Sample2\n" \ "2. Sample1\n" \ "? ", @output.string) end def test_menu_add_item_index_selects_name @input << "1\n" @input.rewind selected = @terminal.choose do |menu| menu.add_item(HighLine::Menu::Item.new("Sample1", text: "Sample2")) menu.add_item(HighLine::Menu::Item.new("Sample2", text: "Sample1")) end assert_equal(selected, "Sample1") assert_equal("1. Sample2\n" \ "2. Sample1\n" \ "? ", @output.string) end def test_menu_add_item_selections_matches_name @input << "Sample2\n" @input.rewind selected = @terminal.choose do |menu| menu.add_item(HighLine::Menu::Item.new("Sample1", text: "Sample2")) menu.add_item(HighLine::Menu::Item.new("Sample2", text: "Sample1")) end assert_equal(selected, "Sample2") assert_equal("1. Sample2\n" \ "2. Sample1\n" \ "? ", @output.string) end def test_menu_build_item @input << "Sample2\n" @input.rewind selected = @terminal.choose do |menu| menu.add_item(menu.build_item("Sample1", text: "Sample2")) menu.add_item(menu.build_item("Sample2", text: "Sample1")) end assert_equal(selected, "Sample2") assert_equal("1. Sample2\n" \ "2. Sample1\n" \ "? ", @output.string) end def test_help @input << "help\nhelp load\nhelp rules\nhelp missing\n" @input.rewind 4.times do @terminal.choose do |menu| menu.shell = true menu.choice(:load, "Load a file.") menu.choice(:save, "Save data in file.") menu.choice(:quit, "Exit program.") menu.help("rules", "The rules of this system are as follows...") end end assert_equal("1. load\n2. save\n3. quit\n4. help\n? " \ "This command will display helpful messages about " \ "functionality, like this one. To see the help for a " \ "specific topic enter:\n" \ "\thelp [TOPIC]\n" \ "Try asking for help on any of the following:\n" \ "\nload quit rules save \n" \ "1. load\n2. save\n3. quit\n4. help\n? " \ "= load\n\n" \ "Load a file.\n" \ "1. load\n2. save\n3. quit\n4. help\n? " \ "= rules\n\n" \ "The rules of this system are as follows...\n" \ "1. load\n2. save\n3. quit\n4. help\n? " \ "= missing\n\n" \ "There's no help for that topic.\n", @output.string) end def test_index @input << "Sample1\n" @input.rewind @terminal.choose do |menu| # Default: menu.index = :number menu.choice "Sample1" menu.choice "Sample2" menu.choice "Sample3" end assert_equal("1. Sample1\n2. Sample2\n3. Sample3\n? ", @output.string) @output.truncate(@output.rewind) @input.rewind @terminal.choose do |menu| menu.index = :letter menu.index_suffix = ") " menu.choice "Sample1" menu.choice "Sample2" menu.choice "Sample3" end assert_equal("a) Sample1\nb) Sample2\nc) Sample3\n? ", @output.string) @output.truncate(@output.rewind) @input.rewind @terminal.choose do |menu| menu.index = :none menu.choice "Sample1" menu.choice "Sample2" menu.choice "Sample3" end assert_equal("Sample1\nSample2\nSample3\n? ", @output.string) @output.truncate(@output.rewind) @input.rewind @terminal.choose do |menu| menu.index = "*" menu.choice "Sample1" menu.choice "Sample2" menu.choice "Sample3" end assert_equal("* Sample1\n* Sample2\n* Sample3\n? ", @output.string) end def test_index_with_color index_color = :rgb_77bbff @input << "Sample1\n" @input.rewind @terminal.choose do |menu| # Default: menu.index = :number menu.index_color = index_color menu.choice "Sample1" menu.choice "Sample2" menu.choice "Sample3" end assert_equal( HighLine.color("1. ", index_color) + "Sample1\n" + HighLine.color("2. ", index_color) + "Sample2\n" + HighLine.color("3. ", index_color) + "Sample3\n" \ "? ", @output.string ) @output.truncate(@output.rewind) @input.rewind @terminal.choose do |menu| menu.index = :letter menu.index_suffix = ") " menu.index_color = index_color menu.choice "Sample1" menu.choice "Sample2" menu.choice "Sample3" end assert_equal( HighLine.color("a) ", index_color) + "Sample1\n" + HighLine.color("b) ", index_color) + "Sample2\n" + HighLine.color("c) ", index_color) + "Sample3\n? ", @output.string ) @output.truncate(@output.rewind) @input.rewind @terminal.choose do |menu| menu.index = :none menu.index_color = index_color menu.choice "Sample1" menu.choice "Sample2" menu.choice "Sample3" end assert_equal( HighLine.color("Sample1", index_color) + "\n" + HighLine.color("Sample2", index_color) + "\n" + HighLine.color("Sample3", index_color) + "\n? ", @output.string ) @output.truncate(@output.rewind) @input.rewind @terminal.choose do |menu| menu.index = "*" menu.index_color = index_color menu.choice "Sample1" menu.choice "Sample2" menu.choice "Sample3" end colored_asterix = HighLine.color("* ", index_color) assert_equal( "#{colored_asterix}Sample1\n" \ "#{colored_asterix}Sample2\n" \ "#{colored_asterix}Sample3\n? ", @output.string ) end def test_layouts @input << "save\n" @input.rewind @terminal.choose(:load, :save, :quit) # Default: layout = :list assert_equal("1. load\n2. save\n3. quit\n? ", @output.string) @input.rewind @output.truncate(@output.rewind) @terminal.choose(:load, :save, :quit) do |menu| menu.header = "File Menu" end assert_equal("File Menu:\n" \ "1. load\n2. save\n3. quit\n? ", @output.string) @input.rewind @output.truncate(@output.rewind) @terminal.choose(:load, :save, :quit) do |menu| menu.layout = :one_line menu.header = "File Menu" menu.prompt = "Operation? " end assert_equal("File Menu: Operation? " \ "(load, save or quit) ", @output.string) @input.rewind @output.truncate(@output.rewind) @terminal.choose(:load, :save, :quit) do |menu| menu.layout = :menu_only end assert_equal("load, save or quit? ", @output.string) @input.rewind @output.truncate(@output.rewind) @terminal.choose(:load, :save, :quit) do |menu| menu.layout = "<%= list(menu) %>File Menu: " end assert_equal("1. load\n2. save\n3. quit\nFile Menu: ", @output.string) end def test_list_option @input << "l\n" @input.rewind @terminal.choose(:load, :save, :quit) do |menu| menu.layout = :menu_only menu.list_option = ", or " end assert_equal("load, save, or quit? ", @output.string) end def test_nil_on_handled @input << "3\n3\n2\n" @input.rewind # Shows that by default proc results are returned. output = @terminal.choose do |menu| menu.choice("Sample1") { "output1" } menu.choice("Sample2") { "output2" } menu.choice("Sample3") { "output3" } end assert_equal("output3", output) # # Shows that they can be replaced with +nil+ by setting # _nil_on_handled to +true+. # output = @terminal.choose do |menu| menu.nil_on_handled = true menu.choice("Sample1") { "output1" } menu.choice("Sample2") { "output2" } menu.choice("Sample3") { "output3" } end assert_nil output # Shows that a menu item without a proc will be returned no matter what. output = @terminal.choose do |menu| menu.choice "Sample1" menu.choice "Sample2" menu.choice "Sample3" end assert_equal("Sample2", output) end def test_passed_command @input << "q\n" @input.rewind selected = nil @terminal.choose do |menu| menu.choices(:load, :save, :quit) { |command| selected = command } end assert_equal(:quit, selected) end def test_question_options @input << "save\n" @input.rewind answer = @terminal.choose(:Load, :Save, :Quit) do |menu| menu.case = :capitalize end assert_equal(:Save, answer) @input.rewind answer = @terminal.choose(:Load, :Save, :Quit) do |menu| menu.case = :capitalize menu.character = :getc end assert_equal(:Save, answer) assert_equal("a", @input.getc) end def test_select_by @input << "Sample1\n2\n" @input.rewind selected = @terminal.choose do |menu| menu.choice "Sample1" menu.choice "Sample2" menu.choice "Sample3" end assert_equal("Sample1", selected) @input.rewind selected = @terminal.choose do |menu| menu.select_by = :index menu.choice "Sample1" menu.choice "Sample2" menu.choice "Sample3" end assert_equal("Sample2", selected) @input.rewind selected = @terminal.choose do |menu| menu.select_by = :name menu.choice "Sample1" menu.choice "Sample2" menu.choice "Sample3" end assert_equal("Sample1", selected) end def test_hidden @input << "Hidden\n4\n" @input.rewind selected = @terminal.choose do |menu| menu.choice "Sample1" menu.choice "Sample2" menu.choice "Sample3" menu.hidden "Hidden!" end assert_equal("Hidden!", selected) assert_equal("1. Sample1\n2. Sample2\n3. Sample3\n? ", @output.string) @input.rewind selected = @terminal.choose do |menu| menu.select_by = :index menu.choice "Sample1" menu.choice "Sample2" menu.choice "Sample3" menu.hidden "Hidden!" end assert_equal("Hidden!", selected) @input.rewind selected = @terminal.choose do |menu| menu.select_by = :name menu.choice "Sample1" menu.choice "Sample2" menu.choice "Sample3" menu.hidden "Hidden!" end assert_equal("Hidden!", selected) @input.rewind end def test_select_by_letter @input << "b\n" @input.rewind selected = @terminal.choose do |menu| menu.index = :letter menu.choice :save menu.choice :load menu.choice :quit end assert_equal(:load, selected) end def test_select_by_capital_letter @input << "B\n" @input.rewind selected = @terminal.choose do |menu| menu.index = :capital_letter menu.choice :save menu.choice :load menu.choice :quit end assert_equal(:load, selected) end def test_shell @input << "save --some-option my_file.txt\n" @input.rewind selected = nil options = nil answer = @terminal.choose do |menu| menu.choices(:load, :quit) menu.choice(:save) do |command, details| selected = command options = details "Saved!" end menu.shell = true end assert_equal("Saved!", answer) assert_equal(:save, selected) assert_equal("--some-option my_file.txt", options) @input.rewind @input << "save\nload\nquit\n" @input.rewind answer = @terminal.choose do |menu| menu.choices(:load) do |command, details| "Loaded!" end menu.choice(:save) do |command, details| "Saved!" end menu.choice(:quit) do |command, details| "Quited!" end menu.shell = true menu.gather = 3; end assert_equal(["Saved!","Loaded!","Quited!"], answer) end def test_simple_menu_shortcut @input << "3\n" @input.rewind selected = @terminal.choose(:save, :load, :quit) assert_equal(:quit, selected) end def test_symbols @input << "3\n" @input.rewind selected = @terminal.choose do |menu| menu.choices(:save, :load, :quit) end assert_equal(:quit, selected) end def test_paged_print_infinite_loop_bug @terminal.page_at = 5 # Will page twice, so start with two new lines @input << "\n\n3\n" @input.rewind # Sadly this goes into an infinite loop without the fix to page_print selected = @terminal.choose(* 1..10) assert_equal(selected, 3) end def test_cancel_paging # Tests that paging can be cancelled halfway through @terminal.page_at = 5 # Will page twice, so stop after first page and make choice 3 @input << "q3\n" @input.rewind selected = @terminal.choose(* 1..10) assert_equal(selected, 3) # Make sure paging message appeared assert(@output.string.index("press enter/return to continue or q to stop"), "Paging message did not appear.") # Make sure it only appeared once assert(@output.string !~ /q to stop.*q to stop/m, "Paging message appeared more than once.") end def test_autocomplete_prompt @input << "lisp\nRuby\n" @input.rewind # answer = @terminal.choose do |menu| menu.choice(:Perl) menu.choice(:Python) menu.choice(:Ruby) menu.prompt = "What is your favorite programming language? " end assert_equal("1. Perl\n" \ "2. Python\n" \ "3. Ruby\n" \ "What is your favorite programming language? " \ "You must choose one of [1, 2, 3, Perl, Python, Ruby].\n" \ "? ", @output.string) end # Issue #180 - https://github.com/JEG2/highline/issues/180 def test_menu_prompt @input << "2\n1\n" @input.rewind # selected = @terminal.choose do |menu| menu.responses[:ask_on_error] = "> " menu.prompt = "> " menu.choice :exit, "Exit cube editor" end prompt = "> " first_asking = "1. exit\n" error_message = "You must choose one of [1, exit].\n" # Same prompt when repeating question complete_interaction = first_asking + prompt + error_message + prompt assert_equal complete_interaction, @output.string end def test_menu_gather_integer @input << "Sample1\nlast\n" @input.rewind selected = @terminal.choose do |menu| menu.gather = 2 menu.choice "Sample1" menu.choice "Sample2" menu.choice "last" end assert_equal(%w[Sample1 last], selected) assert_equal("1. Sample1\n" \ "2. Sample2\n" \ "3. last\n" \ "? 1. Sample1\n" \ "2. Sample2\n" \ "3. last\n" \ "? ", @output.string) end def test_menu_gather_string @input << "Sample1\nlast\n" @input.rewind selected = @terminal.choose do |menu| menu.gather = :last menu.choice "Sample1" menu.choice "Sample2" menu.choice :last end assert_equal(["Sample1"], selected) assert_equal("1. Sample1\n" \ "2. Sample2\n" \ "3. last\n" \ "? 1. Sample1\n" \ "2. Sample2\n" \ "3. last\n" \ "? ", @output.string) end def test_menu_gather_symbol @input << "Sample1\nlast\n" @input.rewind selected = @terminal.choose do |menu| menu.gather = "last" menu.choice "Sample1" menu.choice "Sample2" menu.choice "last" end assert_equal(["Sample1"], selected) assert_equal("1. Sample1\n" \ "2. Sample2\n" \ "3. last\n" \ "? 1. Sample1\n" \ "2. Sample2\n" \ "3. last\n" \ "? ", @output.string) end def test_menu_gather_regexp @input << "Sample1\nlast\n" @input.rewind selected = @terminal.choose do |menu| menu.gather = /la/ menu.choice "Sample1" menu.choice "Sample2" menu.choice "last" end assert_equal(["Sample1"], selected) assert_equal("1. Sample1\n" \ "2. Sample2\n" \ "3. last\n" \ "? 1. Sample1\n" \ "2. Sample2\n" \ "3. last\n" \ "? ", @output.string) end def test_menu_gather_hash @input << "Sample1\n3\n" @input.rewind selected = @terminal.choose do |menu| menu.gather = { "First" => true, second: true } menu.choice "Sample1" menu.choice "Sample2" menu.choice "last" end assert_equal({ "First" => "Sample1", second: "last" }, selected) assert_equal("1. Sample1\n" \ "2. Sample2\n" \ "3. last\n" \ "? 1. Sample1\n" \ "2. Sample2\n" \ "3. last\n" \ "? ", @output.string) end end highline-2.0.3/test/test_paginator.rb000066400000000000000000000032511355001426500176340ustar00rootroot00000000000000#!/usr/bin/env ruby # coding: utf-8 require "test_helper" require "highline" class TestHighLinePaginator < Minitest::Test def setup HighLine.reset @input = StringIO.new @output = StringIO.new @terminal = HighLine.new(@input, @output) end def test_paging @terminal.page_at = 22 @input << "\n\n" @input.rewind @terminal.say((1..50).map { |n| "This is line #{n}.\n" }.join) assert_equal((1..22).map { |n| "This is line #{n}.\n" }.join + "\n-- press enter/return to continue or q to stop -- \n\n" + (23..44).map { |n| "This is line #{n}.\n" }.join + "\n-- press enter/return to continue or q to stop -- \n\n" + (45..50).map { |n| "This is line #{n}.\n" }.join, @output.string) end def test_statement_lines_count_equal_to_page_at_shouldnt_paginate @terminal.page_at = 6 @input << "\n" @input.rewind list = "a\nb\nc\nd\ne\nf\n" @terminal.say(list) assert_equal(list, @output.string) end def test_statement_with_one_line_bigger_than_page_at_should_paginate @terminal.page_at = 6 @input << "\n" @input.rewind list = "a\nb\nc\nd\ne\nf\ng\n" paginated = "a\nb\nc\nd\ne\nf\n" \ "\n-- press enter/return to continue or q to stop -- \n\n" \ "g\n" @terminal.say(list) assert_equal(paginated, @output.string) end def test_quiting_paging_shouldnt_raise # See https://github.com/JEG2/highline/issues/168 @terminal.page_at = 6 @input << "q" @input.rewind list = "a\nb\nc\nd\ne\nf\n" # expect not to raise an error on next line @terminal.say(list) end end highline-2.0.3/test/test_question_asker.rb000066400000000000000000000007311355001426500207040ustar00rootroot00000000000000# encoding: utf-8 require "test_helper" class TestQuestion < Minitest::Test def setup @input = StringIO.new @output = StringIO.new @highline = HighLine.new(@input, @output) @question = HighLine::Question.new("How are you?", nil) @asker = HighLine::QuestionAsker.new(@question, @highline) end def test_ask_once answer = "Very good, thanks for asking!" @input.string = answer assert_equal answer, @asker.ask_once end end highline-2.0.3/test/test_simulator.rb000066400000000000000000000007731355001426500176750ustar00rootroot00000000000000# encoding: utf-8 require "test_helper" require "highline/import" require "highline/simulate" class SimulatorTest < Minitest::Test def setup input = StringIO.new output = StringIO.new HighLine.default_instance = HighLine.new(input, output) end def test_simulator HighLine::Simulate.with("Bugs Bunny", "18") do name = ask("What is your name?") assert_equal "Bugs Bunny", name age = ask("What is your age?") assert_equal "18", age end end end highline-2.0.3/test/test_string_extension.rb000066400000000000000000000043361355001426500212570ustar00rootroot00000000000000#!/usr/bin/env ruby # coding: utf-8 # tc_string_extension.rb # # Created by Richard LeBer 2011-06-27 # # This is Free Software. See LICENSE and COPYING for details. require "test_helper" require "highline" require "stringio" require "string_methods" # FakeString is here just to avoid # using HighLine.colorize_strings # on tests class FakeString < String include HighLine::StringExtensions end class TestStringExtension < Minitest::Test def setup HighLine.reset @string = FakeString.new "string" end def teardown HighLine.reset end include StringMethods def test_Highline_String_is_yaml_serializable require "yaml" return if Gem::Version.new(YAML::VERSION) < Gem::Version.new("2.0.2") highline_string = HighLine::String.new("Yaml didn't messed with HighLine::String") yaml_highline_string = highline_string.to_yaml yaml_loaded_string = if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('3.1.0') # Ruby 2.6+ YAML.safe_load(yaml_highline_string, permitted_classes: [HighLine::String]) else YAML.safe_load(yaml_highline_string, [HighLine::String]) end assert_equal "Yaml didn't messed with HighLine::String", yaml_loaded_string assert_equal highline_string, yaml_loaded_string assert_instance_of HighLine::String, yaml_loaded_string end def test_highline_string_respond_to_color assert HighLine::String.new("highline string").respond_to? :color end def test_normal_string_doesnt_respond_to_color refute "normal_string".respond_to? :color end def test_highline_string_still_raises_for_non_available_messages assert_raises(NoMethodError) do @string.unknown_message end end def test_String_includes_StringExtension_when_receives_colorize_strings @include_received = 0 caller = proc { @include_received += 1 } ::String.stub :include, caller do HighLine.colorize_strings end assert_equal 1, @include_received end def test_respond_to_dynamic_style_methods string = HighLine::String.new("pirarucu") assert_respond_to(string, :on_rgb_123456) assert_respond_to(string, :rgb_abcdef) end end highline-2.0.3/test/test_string_highline.rb000066400000000000000000000016361355001426500210320ustar00rootroot00000000000000#!/usr/bin/env ruby # coding: utf-8 # tc_highline_string.rb # # Created by Richard LeBer 2011-06-27 # # This is Free Software. See LICENSE and COPYING for details. require "test_helper" require "highline" require "stringio" require "string_methods" class TestHighLineString < Minitest::Test def setup HighLine.reset @string = HighLine::String.new("string") end def test_string_class # Basic constructor assert_equal HighLine::String, @string.class assert_equal "string", @string # Alternative constructor method new_string = HighLine::String("string") assert_equal HighLine::String, new_string.class assert_equal @string, new_string # String methods work assert_equal 6, @string.size assert_equal "STRING", @string.upcase end include StringMethods def test_string_class_is_unchanged assert_raises(::NoMethodError) { "string".color(:blue) } end end highline-2.0.3/test/test_style.rb000077500000000000000000000607071355001426500170240ustar00rootroot00000000000000#!/usr/bin/env ruby # coding: utf-8 # tc_style.rb # # Created by Richard LeBer on 2011-06-11. # # This is Free Software. See LICENSE and COPYING for details. require "test_helper" require "highline" require "stringio" class TestStyle < Minitest::Test def setup HighLine.reset @input = StringIO.new @output = StringIO.new @terminal = HighLine.new(@input, @output) @style1 = HighLine::Style.new(name: :foo, code: "\e[99m", rgb: [1, 2, 3]) @style2 = HighLine::Style.new(name: :lando, code: "\e[98m") @style3 = HighLine::Style.new(name: [:foo, :lando], list: [:foo, :lando]) @style4 = HighLine::Style(:rgb_654321) @added_styles_on_setup = 4 # update here if added more styles @added_codes_to_index = 3 # :foo, :lando and :rgb_654321 end def test_clear_index_reset_styles_to_builtin styles_size_after_setup = HighLine::Style.list.size expected_styles_size = styles_size_after_setup - @added_styles_on_setup HighLine::Style.clear_index styles_size_after_clear_index = HighLine::Style.list.size assert_equal expected_styles_size, styles_size_after_clear_index end def test_clear_index_reset_code_index_to_builtin code_index = HighLine::Style.code_index code_index_array = code_index.map { |_code, style_array| style_array }.flatten expected_code_index_array_size = code_index_array.size - @added_codes_to_index HighLine::Style.clear_index cleared_code_index = HighLine::Style.code_index cleared_code_index_array = cleared_code_index.map { |_code, style_array| style_array }.flatten assert_equal expected_code_index_array_size, cleared_code_index_array.size end def test_style_method # Retrieve a style from an existing Style (no new Style created) new_style = @style1.dup # This will replace @style1 in the indexes s = HighLine.Style(@style1) assert_instance_of HighLine::Style, s # i.e. s===the latest style created, but not the one searched for assert_same new_style, s # Retrieve a style from a new Style (no new Style created) s2 = HighLine::Style.new(name: :bar, code: "\e[97m") s = HighLine.Style(s2) assert_instance_of HighLine::Style, s assert_same s2, s # Create a builtin style from an existing ANSI escape string s = HighLine.Style("\e[1m") assert_instance_of HighLine::Style, s assert_nil s.list assert_equal "\e[1m", s.code assert_equal :bold, s.name assert s.builtin # Create a builtin style from a new ANSI escape string s = HighLine.Style("\e[96m") assert_instance_of HighLine::Style, s assert_nil s.list assert_equal "\e[96m", s.code assert s.builtin # Create a non-builtin style from a string s = HighLine.Style("\e[109m") assert_instance_of HighLine::Style, s assert_nil s.list assert_equal "\e[109m", s.code refute s.builtin # Create a builtin style from a symbol s = HighLine.Style(:red) assert_instance_of HighLine::Style, s assert_nil s.list assert_equal :red, s.name assert s.builtin # Retrieve an existing style by name (no new Style created) s = HighLine.Style(@style2.name) assert_instance_of HighLine::Style, s assert_same @style2, s # See below for color scheme tests # Create style from a Hash s = HighLine.Style(name: :han, code: "blah", rgb: "phooey") assert_instance_of HighLine::Style, s assert_equal :han, s.name assert_equal "blah", s.code assert_equal "phooey", s.rgb # Create style from an RGB foreground color code s = HighLine.Style(:rgb_1f2e3d) assert_instance_of HighLine::Style, s assert_equal :rgb_1f2e3d, s.name assert_equal "\e[38;5;23m", s.code # Trust me; more testing below assert_equal [31, 46, 61], s.rgb # 0x1f==31, 0x2e==46, 0x3d=61 # Create style from an RGB background color code s = HighLine.Style(:on_rgb_1f2e3d) assert_instance_of HighLine::Style, s assert_equal :on_rgb_1f2e3d, s.name assert_equal "\e[48;5;23m", s.code # Trust me; more testing below assert_equal [31, 46, 61], s.rgb # 0x1f==31, 0x2e==46, 0x3d=61 # Create a style list s1 = HighLine.Style(:bold, :red) assert_instance_of HighLine::Style, s1 assert_equal [:bold, :red], s1.list # Find an existing style list s2 = HighLine.Style(:bold, :red) assert_instance_of HighLine::Style, s2 assert_same s1, s2 # Create a style list with nils s1 = HighLine.Style(:underline, nil, :blue) assert_instance_of HighLine::Style, s1 assert_equal [:underline, :blue], s1.list # Raise an error for an undefined style assert_raises(::NameError) { HighLine.Style(:fubar) } end def test_no_color_scheme HighLine.color_scheme = nil assert_raises(::NameError) { HighLine.Style(:critical) } end def test_with_color_scheme HighLine.color_scheme = HighLine::SampleColorScheme.new s = HighLine.Style(:critical) assert_instance_of HighLine::Style, s assert_equal :critical, s.name assert_equal [:yellow, :on_red], s.list end def test_builtin_foreground_colors_defined HighLine::COLORS.each do |color| style = HighLine.const_get(color + "_STYLE") assert_instance_of HighLine::Style, style assert_equal color.downcase.to_sym, style.name assert style.builtin code = HighLine.const_get(color) assert_instance_of String, code, "Bad code for #{color}" end end def test_builtin_background_colors_defined HighLine::COLORS.each do |color| style = HighLine.const_get("ON_" + color + "_STYLE") assert_instance_of HighLine::Style, style assert_equal "ON_#{color}".downcase.to_sym, style.name assert style.builtin code = HighLine.const_get("ON_" + color) assert_instance_of String, code, "Bad code for ON_#{color}" end end def test_builtin_styles_defined HighLine::STYLES.each do |style_constant| style = HighLine.const_get(style_constant + "_STYLE") assert_instance_of HighLine::Style, style assert_equal style_constant.downcase.to_sym, style.name assert style.builtin code = HighLine.const_get(style_constant) assert_instance_of String, code, "Bad code for #{style_constant}" end end def test_index # Add a Style with a new name and code assert_nil HighLine::Style.list[:s1] assert_nil HighLine::Style.code_index["foo"] s1 = HighLine::Style.new(name: :s1, code: "foo") refute_nil HighLine::Style.list[:s1] assert_same s1, HighLine::Style.list[:s1] assert_equal :s1, HighLine::Style.list[:s1].name assert_equal "foo", HighLine::Style.list[:s1].code styles = HighLine::Style.list.size codes = HighLine::Style.code_index.size assert_instance_of Array, HighLine::Style.code_index["foo"] assert_equal 1, HighLine::Style.code_index["foo"].size assert_same s1, HighLine::Style.code_index["foo"].last assert_equal :s1, HighLine::Style.code_index["foo"].last.name assert_equal "foo", HighLine::Style.code_index["foo"].last.code # Add another Style with a new name and code assert_nil HighLine::Style.list[:s2] assert_nil HighLine::Style.code_index["bar"] s2 = HighLine::Style.new(name: :s2, code: "bar") assert_equal styles + 1, HighLine::Style.list.size assert_equal codes + 1, HighLine::Style.code_index.size refute_nil HighLine::Style.list[:s2] assert_same s2, HighLine::Style.list[:s2] assert_equal :s2, HighLine::Style.list[:s2].name assert_equal "bar", HighLine::Style.list[:s2].code assert_instance_of Array, HighLine::Style.code_index["bar"] assert_equal 1, HighLine::Style.code_index["bar"].size assert_same s2, HighLine::Style.code_index["bar"].last assert_equal :s2, HighLine::Style.code_index["bar"].last.name assert_equal "bar", HighLine::Style.code_index["bar"].last.code # Add a Style with an existing name s3_before = HighLine::Style.list[:s2] refute_nil HighLine::Style.list[:s2] assert_nil HighLine::Style.code_index["baz"] s3 = HighLine::Style.new(name: :s2, code: "baz") refute_same s2, s3 refute_same s3_before, s3 assert_equal styles + 1, HighLine::Style.list.size assert_equal codes + 2, HighLine::Style.code_index.size refute_nil HighLine::Style.list[:s2] assert_same s3, HighLine::Style.list[:s2] refute_same s2, HighLine::Style.list[:s2] assert_equal :s2, HighLine::Style.list[:s2].name assert_equal "baz", HighLine::Style.list[:s2].code assert_instance_of Array, HighLine::Style.code_index["baz"] assert_equal 1, HighLine::Style.code_index["baz"].size assert_same s3, HighLine::Style.code_index["baz"].last assert_equal :s2, HighLine::Style.code_index["baz"].last.name assert_equal "baz", HighLine::Style.code_index["baz"].last.code # Add a Style with an existing code assert_equal 1, HighLine::Style.code_index["baz"].size s4 = HighLine::Style.new(name: :s4, code: "baz") assert_equal styles + 2, HighLine::Style.list.size assert_equal codes + 2, HighLine::Style.code_index.size refute_nil HighLine::Style.list[:s4] assert_same s4, HighLine::Style.list[:s4] assert_equal :s4, HighLine::Style.list[:s4].name assert_equal "baz", HighLine::Style.list[:s4].code assert_equal 2, HighLine::Style.code_index["baz"].size # Unchanged from last time assert_same s3, HighLine::Style.code_index["baz"].first assert_equal :s2, HighLine::Style.code_index["baz"].first.name assert_equal "baz", HighLine::Style.code_index["baz"].first.code assert_same s4, HighLine::Style.code_index["baz"].last assert_equal :s4, HighLine::Style.code_index["baz"].last.name assert_equal "baz", HighLine::Style.code_index["baz"].last.code end def test_rgb_hex assert_equal "abcdef", HighLine::Style.rgb_hex("abcdef") assert_equal "ABCDEF", HighLine::Style.rgb_hex("AB", "CD", "EF") assert_equal "010203", HighLine::Style.rgb_hex(1, 2, 3) assert_equal "123456", HighLine::Style.rgb_hex(18, 52, 86) end def test_rgb_parts assert_equal [1, 2, 3], HighLine::Style.rgb_parts("010203") assert_equal [18, 52, 86], HighLine::Style.rgb_parts("123456") end def test_rgb s = HighLine::Style.rgb(1, 2, 3) assert_instance_of HighLine::Style, s assert_equal :rgb_010203, s.name assert_equal [1, 2, 3], s.rgb assert_equal "\e[38;5;16m", s.code s = HighLine::Style.rgb("12", "34", "56") assert_instance_of HighLine::Style, s assert_equal :rgb_123456, s.name assert_equal [0x12, 0x34, 0x56], s.rgb assert_equal "\e[38;5;24m", s.code s = HighLine::Style.rgb("abcdef") assert_instance_of HighLine::Style, s assert_equal :rgb_abcdef, s.name assert_equal [0xab, 0xcd, 0xef], s.rgb assert_equal "\e[38;5;189m", s.code end def test_rgb_number # ANSI RGB coding splits 0..255 into equal sixths, and then the # red green and blue are encoded in base 6, plus 16, i.e. # 16 + 36*(red_level) + 6*(green_level) + blue_level, # where each of red_level, green_level, and blue_level are in # the range 0..5 # This test logic works because 42 is just below 1/6 of 255, # and 43 is just above assert_equal 16 + 0 * 36 + 0 * 6 + 0, HighLine::Style.rgb_number(0, 0, 0) assert_equal 16 + 0 * 36 + 0 * 6 + 0, HighLine::Style.rgb_number(0, 0, 42) assert_equal 16 + 0 * 36 + 0 * 6 + 1, HighLine::Style.rgb_number(0, 0, 43) assert_equal 16 + 0 * 36 + 0 * 6 + 0, HighLine::Style.rgb_number(0, 42, 0) assert_equal 16 + 0 * 36 + 0 * 6 + 0, HighLine::Style.rgb_number(0, 42, 42) assert_equal 16 + 0 * 36 + 0 * 6 + 1, HighLine::Style.rgb_number(0, 42, 43) assert_equal 16 + 0 * 36 + 1 * 6 + 0, HighLine::Style.rgb_number(0, 43, 0) assert_equal 16 + 0 * 36 + 1 * 6 + 0, HighLine::Style.rgb_number(0, 43, 42) assert_equal 16 + 0 * 36 + 1 * 6 + 1, HighLine::Style.rgb_number(0, 43, 43) assert_equal 16 + 0 * 36 + 0 * 6 + 0, HighLine::Style.rgb_number(42, 0, 0) assert_equal 16 + 0 * 36 + 0 * 6 + 0, HighLine::Style.rgb_number(42, 0, 42) assert_equal 16 + 0 * 36 + 0 * 6 + 1, HighLine::Style.rgb_number(42, 0, 43) assert_equal 16 + 0 * 36 + 0 * 6 + 0, HighLine::Style.rgb_number(42, 42, 0) assert_equal 16 + 0 * 36 + 0 * 6 + 0, HighLine::Style.rgb_number(42, 42, 42) assert_equal 16 + 0 * 36 + 0 * 6 + 1, HighLine::Style.rgb_number(42, 42, 43) assert_equal 16 + 0 * 36 + 1 * 6 + 0, HighLine::Style.rgb_number(42, 43, 0) assert_equal 16 + 0 * 36 + 1 * 6 + 0, HighLine::Style.rgb_number(42, 43, 42) assert_equal 16 + 0 * 36 + 1 * 6 + 1, HighLine::Style.rgb_number(42, 43, 43) assert_equal 16 + 1 * 36 + 0 * 6 + 0, HighLine::Style.rgb_number(43, 0, 0) assert_equal 16 + 1 * 36 + 0 * 6 + 0, HighLine::Style.rgb_number(43, 0, 42) assert_equal 16 + 1 * 36 + 0 * 6 + 1, HighLine::Style.rgb_number(43, 0, 43) assert_equal 16 + 1 * 36 + 0 * 6 + 0, HighLine::Style.rgb_number(43, 42, 0) assert_equal 16 + 1 * 36 + 0 * 6 + 0, HighLine::Style.rgb_number(43, 42, 42) assert_equal 16 + 1 * 36 + 0 * 6 + 1, HighLine::Style.rgb_number(43, 42, 43) assert_equal 16 + 1 * 36 + 1 * 6 + 0, HighLine::Style.rgb_number(43, 43, 0) assert_equal 16 + 1 * 36 + 1 * 6 + 0, HighLine::Style.rgb_number(43, 43, 42) assert_equal 16 + 1 * 36 + 1 * 6 + 1, HighLine::Style.rgb_number(43, 43, 43) assert_equal 16 + 5 * 36 + 5 * 6 + 5, HighLine::Style.rgb_number(255, 255, 255) end def test_ansi_rgb_to_hex ansi_rgb_to_hex = ->(rgb) { HighLine::Style.ansi_rgb_to_hex(rgb) } assert_equal "000000", ansi_rgb_to_hex.call(16 + 0 * 36 + 0 * 6 + 0) assert_equal "000000", ansi_rgb_to_hex.call(16 + 0 * 36 + 0 * 6 + 0) assert_equal "00002b", ansi_rgb_to_hex.call(16 + 0 * 36 + 0 * 6 + 1) assert_equal "000000", ansi_rgb_to_hex.call(16 + 0 * 36 + 0 * 6 + 0) assert_equal "000000", ansi_rgb_to_hex.call(16 + 0 * 36 + 0 * 6 + 0) assert_equal "00002b", ansi_rgb_to_hex.call(16 + 0 * 36 + 0 * 6 + 1) assert_equal "002b00", ansi_rgb_to_hex.call(16 + 0 * 36 + 1 * 6 + 0) assert_equal "002b00", ansi_rgb_to_hex.call(16 + 0 * 36 + 1 * 6 + 0) assert_equal "002b2b", ansi_rgb_to_hex.call(16 + 0 * 36 + 1 * 6 + 1) assert_equal "000000", ansi_rgb_to_hex.call(16 + 0 * 36 + 0 * 6 + 0) assert_equal "000000", ansi_rgb_to_hex.call(16 + 0 * 36 + 0 * 6 + 0) assert_equal "00002b", ansi_rgb_to_hex.call(16 + 0 * 36 + 0 * 6 + 1) assert_equal "000000", ansi_rgb_to_hex.call(16 + 0 * 36 + 0 * 6 + 0) assert_equal "000000", ansi_rgb_to_hex.call(16 + 0 * 36 + 0 * 6 + 0) assert_equal "00002b", ansi_rgb_to_hex.call(16 + 0 * 36 + 0 * 6 + 1) assert_equal "002b00", ansi_rgb_to_hex.call(16 + 0 * 36 + 1 * 6 + 0) assert_equal "002b00", ansi_rgb_to_hex.call(16 + 0 * 36 + 1 * 6 + 0) assert_equal "002b2b", ansi_rgb_to_hex.call(16 + 0 * 36 + 1 * 6 + 1) assert_equal "2b0000", ansi_rgb_to_hex.call(16 + 1 * 36 + 0 * 6 + 0) assert_equal "2b0000", ansi_rgb_to_hex.call(16 + 1 * 36 + 0 * 6 + 0) assert_equal "2b002b", ansi_rgb_to_hex.call(16 + 1 * 36 + 0 * 6 + 1) assert_equal "2b0000", ansi_rgb_to_hex.call(16 + 1 * 36 + 0 * 6 + 0) assert_equal "2b0000", ansi_rgb_to_hex.call(16 + 1 * 36 + 0 * 6 + 0) assert_equal "2b002b", ansi_rgb_to_hex.call(16 + 1 * 36 + 0 * 6 + 1) assert_equal "2b2b00", ansi_rgb_to_hex.call(16 + 1 * 36 + 1 * 6 + 0) assert_equal "2b2b00", ansi_rgb_to_hex.call(16 + 1 * 36 + 1 * 6 + 0) assert_equal "2b2b2b", ansi_rgb_to_hex.call(16 + 1 * 36 + 1 * 6 + 1) # 0xd5 is the smallest number where n/255.0*6.0 > 5 assert_equal "d5d5d5", ansi_rgb_to_hex.call(16 + 5 * 36 + 5 * 6 + 5) end def test_list list_size = HighLine::Style.list.size # Add a Style with a new name and code assert_nil HighLine::Style.list[:s5] s5 = HighLine::Style.new(name: :s5, code: "foo") refute_nil HighLine::Style.list[:s5] assert_equal list_size + 1, HighLine::Style.list.size refute_nil HighLine::Style.list[:s5] assert_same s5, HighLine::Style.list[:s5] assert_equal :s5, HighLine::Style.list[:s5].name assert_equal "foo", HighLine::Style.list[:s5].code # Add another Style with a new name and code assert_nil HighLine::Style.list[:s6] s6 = HighLine::Style.new(name: :s6, code: "bar") assert_equal list_size + 2, HighLine::Style.list.size refute_nil HighLine::Style.list[:s6] assert_same s6, HighLine::Style.list[:s6] assert_equal :s6, HighLine::Style.list[:s6].name assert_equal "bar", HighLine::Style.list[:s6].code # Add a Style with an existing name s7 = HighLine::Style.new(name: :s6, code: "baz") # No net addition to list assert_equal list_size + 2, HighLine::Style.list.size refute_nil HighLine::Style.list[:s6] assert_same s7, HighLine::Style.list[:s6] # New one replaces old one refute_same s6, HighLine::Style.list[:s6] assert_equal :s6, HighLine::Style.list[:s6].name assert_equal "baz", HighLine::Style.list[:s6].code end def test_code_index list_size = HighLine::Style.code_index.size # Add a Style with a new name and code assert_nil HighLine::Style.code_index["chewie"] HighLine::Style.new(name: :s8, code: "chewie") assert_equal list_size + 1, HighLine::Style.code_index.size assert_instance_of Array, HighLine::Style.code_index["chewie"] assert_equal 1, HighLine::Style.code_index["chewie"].size assert_equal :s8, HighLine::Style.code_index["chewie"].last.name assert_equal "chewie", HighLine::Style.code_index["chewie"].last.code # Add another Style with a new name and code assert_nil HighLine::Style.code_index["c3po"] HighLine::Style.new(name: :s9, code: "c3po") assert_equal list_size + 2, HighLine::Style.code_index.size assert_instance_of Array, HighLine::Style.code_index["c3po"] assert_equal 1, HighLine::Style.code_index["c3po"].size assert_equal :s9, HighLine::Style.code_index["c3po"].last.name assert_equal "c3po", HighLine::Style.code_index["c3po"].last.code # Add a Style with an existing code assert_equal 1, HighLine::Style.code_index["c3po"].size HighLine::Style.new(name: :s10, code: "c3po") assert_equal list_size + 2, HighLine::Style.code_index.size assert_equal 2, HighLine::Style.code_index["c3po"].size assert_equal :s10, HighLine::Style.code_index["c3po"].last.name assert_equal "c3po", HighLine::Style.code_index["c3po"].last.code end def test_uncolor # Normal color assert_equal( "This should be reverse underlined magenta!\n", HighLine::Style.uncolor( "This should be \e[7m\e[4m\e[35mreverse underlined magenta\e[0m!\n" ) ) # RGB color assert_equal( "This should be rgb_906030!\n", HighLine::Style.uncolor("This should be \e[38;5;137mrgb_906030\e[0m!\n") ) end def test_color assert_equal "\e[99mstring\e[0m", @style1.color("string") # simple style assert_equal "\e[99m\e[98mstring\e[0m", @style3.color("string") # Style list end def test_code assert_equal "\e[99m", @style1.code # simple style assert_equal "\e[99m\e[98m", @style3.code # Style list end def test_red assert_equal 0x65, @style4.red assert_equal 0, HighLine::Style(:none).red # Probably reliable assert_equal 0, HighLine::Style(:black).red # Probably reliable assert_equal 255, HighLine::Style(:bright_magenta).red # Seems reliable assert_equal 255, HighLine::Style(:on_none).red # Probably reliable end def test_green assert_equal 0x43, @style4.green assert_equal 0, HighLine::Style(:none).green # Probably reliable assert_equal 0, HighLine::Style(:black).green # Probably reliable assert HighLine::Style(:bright_cyan).green >= 240 # Probably reliable assert_equal 255, HighLine::Style(:on_none).green # Probably reliable end def test_blue assert_equal 0x21, @style4.blue assert_equal 0, HighLine::Style(:none).blue # Probably reliable assert_equal 0, HighLine::Style(:black).blue # Probably reliable assert_equal 255, HighLine::Style(:bright_blue).blue # Probably reliable assert_equal 255, HighLine::Style(:on_none).blue # Probably reliable end def test_builtin assert HighLine::Style(:red).builtin assert !@style1.builtin end def test_variant style1_name = @style1.name style1_code = @style1.code style1_rgb = @style1.rgb s1 = @style1.variant(:new_foo1, code: "abracadabra") assert_instance_of HighLine::Style, s1 refute_same @style1, s1 # This is a copy assert_equal :new_foo1, s1.name # Changed assert_equal "abracadabra", s1.code # Changed assert_equal [1, 2, 3], s1.rgb # Unchanged s2 = @style1.variant(:new_foo2, increment: -15) assert_instance_of HighLine::Style, s2 refute_same @style1, s2 # This is a copy assert_equal :new_foo2, s2.name # Changed assert_equal "\e[84m", s2.code # 99 (original code) - 15 assert_equal [1, 2, 3], s2.rgb # Unchanged s3 = @style1.variant(:new_foo3, code: "\e[55m", increment: 15) assert_instance_of HighLine::Style, s3 refute_same @style1, s3 # This is a copy assert_equal :new_foo3, s3.name # Changed assert_equal "\e[70m", s3.code # 99 (new code) + 15 assert_equal [1, 2, 3], s3.rgb # Unchanged s4 = @style1.variant(:new_foo4, code: "\e[55m", increment: 15, rgb: "blah") assert_instance_of HighLine::Style, s4 refute_same @style1, s4 # This is a copy assert_equal :new_foo4, s4.name # Changed assert_equal "\e[70m", s4.code # 99 (new code) + 15 assert_equal "blah", s4.rgb # Changed s5 = @style1.variant(:new_foo5) assert_instance_of HighLine::Style, s5 refute_same @style1, s5 # This is a copy assert_equal :new_foo5, s5.name # Changed assert_equal "\e[99m", s5.code # Unchanged assert_equal [1, 2, 3], s5.rgb # Unchanged # No @style1's have been harmed in the running of this test assert_equal style1_name, @style1.name assert_equal style1_code, @style1.code assert_equal style1_rgb, @style1.rgb # Can't create a variant of a list style assert_raises(::RuntimeError) { @style3.variant(:new_foo6) } end def test_on style1_name = @style1.name style1_code = @style1.code style1_rgb = @style1.rgb s1 = @style1.on assert_instance_of HighLine::Style, s1 refute_same @style1, s1 # This is a copy assert_equal :on_foo, s1.name # Changed assert_equal "\e[109m", s1.code # Changed assert_equal [1, 2, 3], s1.rgb # Unchanged # No @style1's have been harmed in the running of this test assert_equal style1_name, @style1.name assert_equal style1_code, @style1.code assert_equal style1_rgb, @style1.rgb # Can't create a variant of a list style assert_raises(::RuntimeError) { @style3.on } end def test_bright style1_name = @style1.name style1_code = @style1.code style1_rgb = @style1.rgb s1 = @style1.bright assert_instance_of HighLine::Style, s1 refute_same @style1, s1 # This is a copy assert_equal :bright_foo, s1.name # Changed assert_equal "\e[159m", s1.code # Changed assert_equal [129, 130, 131], s1.rgb # Changed # No @style1's have been harmed in the running of this test assert_equal style1_name, @style1.name assert_equal style1_code, @style1.code assert_equal style1_rgb, @style1.rgb s2_base = HighLine::Style.new(name: :leia, code: "\e[92m", rgb: [0, 0, 14]) s2 = s2_base.bright assert_instance_of HighLine::Style, s2 refute_same s2_base, s2 # This is a copy assert_equal :bright_leia, s2.name # Changed assert_equal "\e[152m", s2.code # Changed assert_equal [0, 0, 142], s2.rgb # Changed s3_base = HighLine::Style.new(name: :luke, code: "\e[93m", rgb: [20, 21, 0]) s3 = s3_base.bright assert_instance_of HighLine::Style, s3 refute_same s3_base, s3 # This is a copy assert_equal :bright_luke, s3.name # Changed assert_equal "\e[153m", s3.code # Changed assert_equal [148, 149, 0], s3.rgb # Changed s4_base = HighLine::Style.new(name: :r2d2, code: "\e[94m", rgb: [0, 0, 0]) s4 = s4_base.bright assert_instance_of HighLine::Style, s4 refute_same s4_base, s4 # This is a copy assert_equal :bright_r2d2, s4.name # Changed assert_equal "\e[154m", s4.code # Changed assert_equal [128, 128, 128], s4.rgb # Changed; special case # Can't create a variant of a list style assert_raises(::RuntimeError) { @style3.bright } end def test_light_do_the_same_as_bright bright_style = @style1.bright light_style = @style1.light refute_equal bright_style, light_style assert_equal :bright_foo, bright_style.name assert_equal :light_foo, light_style.name assert_equal bright_style.code, light_style.code assert_equal bright_style.rgb, light_style.rgb end end highline-2.0.3/test/test_wrapper.rb000066400000000000000000000125441355001426500173350ustar00rootroot00000000000000#!/usr/bin/env ruby # coding: utf-8 require "test_helper" require "highline/wrapper" class TestHighLineWrapper < Minitest::Test def setup @wrap_at = 80 end def wrap(text) HighLine::Wrapper.wrap text, @wrap_at end def test_dont_wrap_if_line_is_shorter_than_wrap_at wrapped = wrap("This is a very short line.\n") assert_equal "This is a very short line.\n", wrapped end def test_wrap_long_lines_correctly long_line = "This is a long flowing paragraph meant to span " \ "several lines. This text should definitely be " \ "wrapped at the set limit, in the result. Your code " \ "does well with things like this.\n\n" wrapped_long_line = "This is a long flowing paragraph meant to span " \ "several lines. This text should\n" \ "definitely be wrapped at the set limit, in the " \ "result. Your code does well with\n" \ "things like this.\n\n" wrapped = wrap(long_line) assert_equal wrapped_long_line, wrapped end def test_dont_wrap_already_well_wrapped_text well_formatted_text = " * This is a simple embedded list.\n" \ " * You're code should not mess with this...\n" \ " * Because it's already formatted correctly and does not\n" \ " exceed the limit!\n" wrapped = wrap(well_formatted_text) assert_equal well_formatted_text, wrapped end def test_wrap_single_word_longer_than_wrap_at wrapped = wrap("-=" * 50) assert_equal(("-=" * 40 + "\n") + ("-=" * 10), wrapped) end def test_wrap_plain_text line = "123 567 901 345" 1.upto(25) do |wrap_at| wrapped = HighLine::Wrapper.wrap(line, wrap_at) case wrap_at when 1 assert_equal "1\n2\n3\n5\n6\n7\n9\n0\n1\n3\n4\n5", wrapped when 2 assert_equal "12\n3\n56\n7\n90\n1\n34\n5", wrapped when 3..6 assert_equal "123\n567\n901\n345", wrapped when 7..10 assert_equal "123 567\n901 345", wrapped when 11..14 assert_equal "123 567 901\n345", wrapped when 15..25 assert_equal "123 567 901 345", wrapped end end end def test_wrap_whole_colored_text skip "TODO: Implement whole colored text wrapping!" line = "\e[31m123 567 901 345\e[0m" 1.upto(25) do |wrap_at| wrapped = HighLine::Wrapper.wrap(line, wrap_at) case wrap_at when 1 assert_equal "\e[31m1\n2\n3\n5\n6\n7\n9\n0\n1\n3\n4\n5\e[0m", wrapped when 2 assert_equal "\e[31m12\n3\n56\n7\n90\n1\n34\n5\e[0m", wrapped when 3..6 assert_equal "\e[31m123\n567\n901\n345\e[0m", wrapped when 7..10 assert_equal "\e[31m123 567\n901 345\e[0m", wrapped when 11..14 assert_equal "\e[31m123 567 901\n345\e[0m", wrapped when 15..25 assert_equal "\e[31m123 567 901 345\e[0m", wrapped end end end def test_wrap_partially_colored_text skip "TODO: Implement middle colored text wrapping!" line = "123 567 \e[31m901\e[0m 345" 1.upto(25) do |wrap_at| wrapped = HighLine::Wrapper.wrap(line, wrap_at) case wrap_at when 1 assert_equal "1\n2\n3\n5\n6\n7\n\e[31m9\n0\n1\e[0m\n3\n4\n5", wrapped when 2 assert_equal "12\n3\n56\n7\n\e[31m90\n1\e[0m\n34\n5", wrapped when 3..6 assert_equal "123\n567\n\e[31m901\e[0m\n345", wrapped when 7..10 assert_equal "123 567\n\e[31m901\e[0m 345", wrapped when 11..14 assert_equal "123 567 \e[31m901\e[0m\n345", wrapped when 15..25 assert_equal "123 567 \e[31m901\e[0m 345", wrapped end end end def test_wrap_text_with_partially_colored_word_in_the_middle skip "TODO: Implement middle partially colored text wrapping!" line = "123 567 9\e[31m0\e[0m1 345" 1.upto(25) do |wrap_at| wrapped = HighLine::Wrapper.wrap(line, wrap_at) case wrap_at when 1 assert_equal "1\n2\n3\n5\n6\n7\n9\n\e[31m0\e[0m\n1\n3\n4\n5", wrapped when 2 assert_equal "12\n3\n56\n7\n9\e[31m0\e[0m\n1\n34\n5", wrapped when 3..6 assert_equal "123\n567\n9\e[31m0\e[0m1\n345", wrapped when 7..10 assert_equal "123 567\n9\e[31m0\e[0m1 345", wrapped when 11..14 assert_equal "123 567 9\e[31m0\e[0m1\n345", wrapped when 15..25 assert_equal "123 567 9\e[31m0\e[0m1 345", wrapped end end end def test_wrap_when_multibyte_characters_present line_ascii = "Sera um passaro?" line_utf8 = "Será um pássaro?" assert_equal 16, line_ascii.size assert_equal 16, line_ascii.bytesize assert_equal 16, line_utf8.size assert_equal 18, line_utf8.bytesize 1.upto(18) do |wrap_at| wrapped = HighLine::Wrapper.wrap(line_utf8, wrap_at) case wrap_at when 1 assert_equal "S\ne\nr\ná\nu\nm\np\ná\ns\ns\na\nr\no\n?", wrapped when 2 assert_equal "Se\nrá\num\npá\nss\nar\no?", wrapped when 3 assert_equal "Ser\ná\num\npás\nsar\no?", wrapped when 4 assert_equal "Será\num\npáss\naro?", wrapped when 5 assert_equal "Será\num\npássa\nro?", wrapped when 6 assert_equal "Será\num\npássar\no?", wrapped when 7 assert_equal "Será um\npássaro\n?", wrapped when 15..8 assert_equal "Será um\npássaro?", wrapped when 16..18 assert_equal "Será um pássaro?", wrapped end end end end