pax_global_header00006660000000000000000000000064140366204460014517gustar00rootroot0000000000000052 comment=5152fb5abd4cb3a5aaefc31ec316618e1eb5787a tty-prompt-0.23.1/000077500000000000000000000000001403662044600137415ustar00rootroot00000000000000tty-prompt-0.23.1/.editorconfig000066400000000000000000000002261403662044600164160ustar00rootroot00000000000000root = true [*.rb] charset = utf-8 end_of_line = lf insert_final_newline = true indent_style = space indent_size = 2 trim_trailing_whitespace = true tty-prompt-0.23.1/.github/000077500000000000000000000000001403662044600153015ustar00rootroot00000000000000tty-prompt-0.23.1/.github/FUNDING.yml000066400000000000000000000000241403662044600171120ustar00rootroot00000000000000github: piotrmurach tty-prompt-0.23.1/.github/ISSUE_TEMPLATE.md000066400000000000000000000011371403662044600200100ustar00rootroot00000000000000### Are you in the right place? * For issues or feature requests file a GitHub issue in this repository * For general questions or discussion post in [Discussions](https://github.com/piotrmurach/tty-prompt/discussions) ### Describe the problem A brief description of the issue/feature. ### Steps to reproduce the problem ``` Your code here to reproduce the issue ``` ### Actual behaviour What happened? This could be a description, log output, error raised etc... ### Expected behaviour What did you expect to happen? ### Describe your environment * OS version: * Ruby version: * TTY::Prompt version: tty-prompt-0.23.1/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000007561403662044600211120ustar00rootroot00000000000000### Describe the change What does this Pull Request do? ### Why are we doing this? Any related context as to why is this is a desirable change. ### Benefits How will the library improve? ### Drawbacks Possible drawbacks applying this change. ### Requirements - [ ] Tests written & passing locally? - [ ] Code style checked? - [ ] Rebased with `master` branch? - [ ] Documentation updated? - [ ] Changelog updated? tty-prompt-0.23.1/.github/workflows/000077500000000000000000000000001403662044600173365ustar00rootroot00000000000000tty-prompt-0.23.1/.github/workflows/ci.yml000066400000000000000000000026671403662044600204670ustar00rootroot00000000000000--- name: CI on: push: branches: - master paths-ignore: - "benchmarks/**" - "examples/**" - "*.md" pull_request: branches: - master paths-ignore: - "benchmarks/**" - "examples/**" - "*.md" jobs: tests: name: Ruby ${{ matrix.ruby }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: - ubuntu-latest ruby: - 2.3 - 2.4 - 2.5 - 2.6 - 3.0 - ruby-head - jruby-9.2.13.0 - jruby-head - truffleruby-head include: - ruby: 2.1 os: ubuntu-latest coverage: false bundler: 1 - ruby: 2.2 os: ubuntu-latest coverage: false bundler: 1 - ruby: 2.7 os: ubuntu-latest coverage: true bundler: latest env: COVERAGE: ${{ matrix.coverage }} COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} continue-on-error: ${{ endsWith(matrix.ruby, 'head') }} steps: - uses: actions/checkout@v2 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler: ${{ matrix.bundler }} - name: Install dependencies run: bundle install --jobs 4 --retry 3 - name: Run tests run: bundle exec rake ci tty-prompt-0.23.1/.gitignore000066400000000000000000000002231403662044600157260ustar00rootroot00000000000000/.bundle/ /.ruby-version /.ruby-gemset /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ *.bundle *.so *.o *.a mkmf.log tty-prompt-0.23.1/.rspec000066400000000000000000000000511403662044600150520ustar00rootroot00000000000000--color --require spec_helper --warnings tty-prompt-0.23.1/.rubocop.yml000066400000000000000000000026021403662044600162130ustar00rootroot00000000000000AllCops: NewCops: enable Layout/BlockAlignment: Exclude: - "spec/**/*" Layout/FirstArrayElementIndentation: Enabled: false Layout/FirstHashElementIndentation: Enabled: false Layout/HashAlignment: Exclude: - "spec/**/*" Layout/LineLength: Max: 80 Exclude: - "spec/**/*" Layout/SpaceInsideHashLiteralBraces: EnforcedStyle: no_space Lint/AssignmentInCondition: Enabled: false Lint/ConstantDefinitionInBlock: Exclude: - "spec/**/*" Metrics/AbcSize: Max: 35 Exclude: - "spec/**/*" Metrics/BlockLength: CountComments: true Max: 25 IgnoredMethods: [] Exclude: - "spec/**/*" Metrics/ClassLength: Max: 1500 Metrics/CyclomaticComplexity: Enabled: false Metrics/MethodLength: Max: 20 Exclude: - "spec/**/*" Naming/BinaryOperatorParameterName: Enabled: false Style/AccessorGrouping: Enabled: false Style/AccessModifierDeclarations: Enabled: false Style/AsciiComments: Enabled: false Style/BlockDelimiters: Enabled: false Style/CommentedKeyword: Enabled: false Style/FormatString: EnforcedStyle: percent Style/FormatStringToken: EnforcedStyle: template Style/HashConversion: Enabled: false Style/LambdaCall: EnforcedStyle: braces Exclude: - "spec/**/*" Style/StringLiterals: EnforcedStyle: double_quotes Style/StringConcatenation: Exclude: - "spec/**/*" Style/TrivialAccessors: Enabled: false tty-prompt-0.23.1/CHANGELOG.md000066400000000000000000000412061403662044600155550ustar00rootroot00000000000000# Change log ## [v0.23.1] - 2021-04-17 ### Changed * Change validate to allow access to invalid input inside the message ### Fixed * Fix Choice#from to differentiate between no value being set and nil value ## [v0.23.0] - 2020-12-14 ### Added * Add the ability to provide an arbitrary array of values to Prompt::Slider by Katelyn Schiesser (@slowbro) ### Changed * Change to allow default option to be choice name as well as index in select, multi_select and enum_select prompts ### Fixed * Fix left and right key navigation while filtering choices in the #select and #multi_select prompts ## [v0.22.0] - 2020-07-20 ### Added * Add #slider format customization with a proc by Sven Pchnit(@2called-chaos) * Add convert message customization * Add conversion of input into Array, Hash or URI object * Add callable objects as possible values in :active_color and :help_color * Add shortcuts to select of all/reverse choices in #multi_select prompt * Add :help option to #slider prompt * Add :quiet option to remove final prompt output by Katelyn Schiesser (@slowbro) * Add :show_help option to control help display in #select, #multi_select, #enum_select and #slider prompts ### Changed * Changed question :validation option to :validate by Sven Pachnit(@2called-chaos) * Change ConverterRegistry to store only proc values and simplify interface * Change Converters to stop raising errors and print console error messages instead * Change :range conversion to handle float numbers * Change yes?/no? prompt to infer default value from words and raise when no boolean can be deduced * Change Prompt#new to use keyword arguments * Change #select/#multi_select prompts default help text * Change #multi_select to preserve original ordering in returned answers * Change to remove necromancer dependency * Change TTY::TestPrompt to TTY::Prompt::Test * Change #select,#multi_select & #enum_select to allow mix of options and block parameters configuration * Change to allow filtering through symbol choice names * Change all errors to inherit from common Error class ### Fixed * Fix multiline prompt to return default value when no input provided * Fix color option overriding in say, ok, error and warn prompts * Fix Prompt#inspect format to display all public attributes ## [v0.21.0] - 2020-03-08 ### Added * Add :min option to #multi_select prompt by Katelyn Schiesser(@slowbro) ### Changed * Change gemspec to remove test artifacts ### Fixed * Fix :help_color option for multi_select prompt by @robbystk ## [v0.20.0] - 2019-11-24 ### Changed * Change to update tty-reader dependency * Change gemspec to include metadata ### Fixed * Fix Choice#from to differentiate between nil and false by Katelyn Schiesser(@slowbro) * Fix yes? and no? prompts to stop raising on invalid/blank input by Katelyn Schiesser(@slowbro) * Fix Ruby 2.7 keyword arguments warnings * Fix question validation to work with nil input ## [v0.19.0] - 2019-05-27 ### Added * Add Prompt#debug to allow displaying values in terminal's top right corner * Add :max to limit number of choices in #multi_select prompt * Add :value to pre populate #ask prompt line content * Add :auto_hint to expand default hint in #expand prompt by Ewoudt Kellerman(@hellola) * Add Timer to track and timeout code execution ### Changed * Change Paginator to expose #start_index & #end_index * Change Paginator to figure out #start_index based on per page size and adjust boundaries to match active selection * Change #ask prompt to allow no question * Change #enum_select to automatically assigned non-disabled default option * Change #enum_select to set default choice when navigating by page * Change #select & #multi_select to allow navigation by page with left/right keys * Change #keypress to use Timer * Change Choice#from to allow any object coercible to string * Change to remove test artifacts from the gem bundle * Change to remove timers dependency * Change to update tty-reader dependency ## [v0.18.1] - 2018-12-29 ### Changed * Change #multi_select & #select to auto select first non-disabled active choice ### Fixed * Fix #select, #multi_select & #enum_select to allow for symbols as choice names ## [v0.18.0] - 2018-11-24 ### Changed * Change to update tty-reader dependency * Remove encoding magic comments ### Fixed * Fix #keypress to stop using the :nonblock option * Fix input reading to correctly capture the Esc key(#84) * Fix line editing when cursor is on second to last character(#94) ## [v0.17.2] - 2018-11-01 ### Fixed * Fix #yes? & #no? prompt suffix option to all non-standard characters by Rui(@rpbaltazar) ## [v0.17.1] - 2018-10-03 ### Change * Change #select, #multi_select to allow alphanumeric, punctuation and space characters in filters ### Fixed * Fix #select by making filter an array to avoid frozen string issues by Chris Hoffman(@yarmiganosca) ## [v0.17.0] - 2018-08-05 ### Changed * Change to update tty-reader & tty-cursor dependencies * Change to directly require files in gemspec ## [v0.16.1] - 2018-04-29 ### Fixed * Fix key events subscription to only listen for the current prompt events ## [v0.16.0] - 2018-03-11 ### Added * Add :disabled key to Choice * Add ability to disable choices in #select, #multi_selct & #enum_select prompts * Add #frozen_string_literal to all files ### Changed * Change Choice#from to allow parsing different data structures * Change all classes to prevent strings mutations * Change Timeout to cleanly terminate keypress input without raising errors ### Fixed * Fix #select, #enum_select & #multi_select navigation to work correctly with items longer than terminal screen width * Fix timeout on Ruby 2.5 and stop raising Timeout::Error ## [v0.15.0] - 2018-02-08 ### Added * Add ability to filter list items in #select, #multi_select & #enum_selct prompts by Saverio Miroddi(@saveriomiroddi) * Add support for array of values for an answer collector key by Danny Hadley(@dadleyy) ### Changed * Relax dependency on timers by Andy Brody(@brodygov) ## [v0.14.0] - 2018-01-01 ### Added * Add :cycle option to #select, #multi_select & #enum_select prompts to allow toggling between infinite and bounded list by Jonas Müller(@muellerj) ### Changed * Change #multi_selct, #select & #enum_select to stop cycling options by default by Jona Müller(@muellerj) * Change gemspec to require ruby >= 2.0.0 * Change #slider prompt to display slider next to query and help underneath * Change to use tty-reader v0.2.0 with new line editing features for processing long inputs ### Fixed * Fix Paginator & EnumPaginator to allow only positive integer values by Andy Brody(@ab) * Fix EnumSelect to report on default option out of range and raise correctly * Fix #ask :file & :path converters to correctly locate the files * Fix #ask, #multiline to correctly handle long strings that wrap around screen * Fix #slider prompt to correctly scale sliding ## [v0.13.2] - 2017-08-30 ### Changed * Change to extract TTY::Prompt::Reader to its own dependency ## [v0.13.1] - 2017-08-16 ### Added * Add ability to manually cancel the time scheduler ### Changed * Change #keypress to use new scheduler cancelling * Change Reader to inline interrupt to allow for early exit ### Fix * Fix keypress reading on Windows to distinguish between blocking & non-blocking IO ## [v0.13.0] - 2017-08-11 ### Changed * Change Timeout to use clock time instead of sleep to measure interval * Upgrade tty-cursor to fix save & restore ### Fixed * Fix keypress with timeout option to cleanly stop timeout thread * Fix Reader on Windows to stop blocking when waiting for key press ## [v0.12.0] - 2017-03-19 ### Added * Add Multiline question type * Add Keypress question type * Add Reader::History for storing buffered lines * Add Reader::Line for line abstraction ### Changed * Remove :read option from Question * Chnage Reader#read_line to handle raw mode for processing special characters such as Ctrl+x, navigate through history buffer using up/down arrows, allow editing current line by moving left/right with arrow keys and inserting content * Change Reader#read_multiline to gather multi line input correctly, skip empty lines and terminate when Ctrl+d and Ctrl+z are pressed * Change Reader::Mode to check if tty is available by Matt Martyn (@MMartyn) * Change #keypress prompt to correctly refresh line and accept :keys & :timeout options ### Fixed * Fix issue with #select, #multi_selct, #enum_select when choices are provided as hash object together with prompt options. * Fix issue with default parameter for yes?/no? prompt by Carlos Fonseca (@carlosefonseca) * Fix List#help to allow setting help text through DSL ## [v0.11.0] - 2017-02-26 ### Added * Add Console for reading input characters on Unix systems * Add WinConsole for reading input characters on Windows systems * Add WindowsApi to allow for calls to external Windows api * Add echo support to multilist by Keith Keith T. Garner(@ktgeek) ### Changed * Change Reader to use Console for input reading * Change Codes to use codepoints instead of strings * Change Reader#read_line to match #gets behaviour * Change Symbols to provide Unicode support on windows * Change Slider to display Unicode when possible * Change ConverterRegistry to be immutable * Change Reader to expose #trigger in place of #publish for events firing ### Fixed * Fix `modify` throwing exception, when user enters empty input by Igor Rzegocki(@ajgon) * Fix #clear_line behaviour by using tty-cursor 0.4.0 to work in all terminals * Fix paging issue for lists shorter than :per_page value repeating title * Fix #mask prompt to correctly match input on Windows * Fix @mask to use default error messages * Fix #select & #multi_select prompts to allow changing options with arrow keys on Windows * Fix #echo to work correctly in zsh shell by štef(@d4be4st) * Fix Slider#keyright event accepting max value outside of range * Fix 2.4.0 conversion errors by using necromancer 0.4.0 * Fix #enum_select preventing selection of first item ## [v0.10.1] - 2017-02-06 ### Fixed * Fix File namespacing ## [v0.10.0] - 2017-01-01 ### Added * Add :enable_color option for toggling colors support ### Changed * Update pastel dependency version ## [v0.9.0] - 2016-12-20 ### Added * Add ability to paginate choices list for #select, #multi_select & #enum_select with :per_page, :page_info and :default options * Add ability to switch through options in #select & #multi_select using the tab key ### Fixed * Fix readers to accept multibyte characters reported by Jaehyun Shin(@keepcosmos) ## [v0.8.0] - 2016-11-29 ### Added * Add ability to publish custom key events for VIM keybindings customisations etc... ### Fixed * Fix Reader#read_char to use Ruby internal buffers instead of direct system call by @kke(Kimmo Lehto) * Fix issue with #ask required & validate checks to take into account required when validating values * Fix bug with #read_keypress to handle function keys and meta navigation keys * Fix issue with default messages not displaying for `range`, `required` and `validate` ## [v0.7.1] - 2016-08-07 ### Fixed * Fix Reader::Mode to include standard io library ## [v0.7.0] - 2016-07-17 ### Added * Add :interrupt_handler option to customise keyboard interrupt behaviour ### Changed * Remove tty-platform dependency ### Fixed * Fix Reader#read_keypress issue when handling interrupt signal by Ondrej Moravcik(@ondra-m) * Fix raw & echo modes to use standard library support by Kim Burgestrand(@Burgestrand) ## [v0.6.0] - 2016-05-21 ### Changed * Upgrade tty-cursor dependency ### Fixed * Fix issue with reader trapping signals by @kylekyle * Fix expand to use new prev_line implementation ## [v0.5.0] - 2016-03-28 ### Added * Add ConfirmQuestion for #yes? & #no? calls * Add ability to collect more than one answer through #collect call * Add Choices#find_by for selecting choice based on attribute * Add Prompt#expand for expanding key options * Add :active_color, :help_color, :prefix options for customizing prompts display ### Changed * Change Choice#from to allow for coersion of complex objects with keys * Change Choices#pluck to search through object attributes * Change #select :enum option help text to display actual numbers range ### Fixed * Fix #no? to correctly ask negative question by @ondra-m * Fix #ask :default option to handle nil or empty string * Fix #multi_select :default option and color changing ## [v0.4.0] - 2016-02-08 ### Added * Add :enum option for #select & #multi_select to allow for numerical selection by @rtoshiro * Add new key event types to KeyEvent * Add #slider for picking values from range of numbers * Add #enum_select for selecting option from enumerated list * Add ability to configure error messages for #ask call * Add new ConversionError type ### Changed * Move #blank? to Utils * Update pastel dependency ## [v0.3.0] - 2015-12-28 ### Added * Add prefix option to prompt to customize #ask, #select, #multi_select * Add default printing to #ask * Add #yes?/#no? boolean queries * Add Evaluator and Result for validation checking to Question * Add ability for #ask to display error messages on failed validation * Add ability to specify in-built names for validation e.i. :email * Add KeyEvent for keyboard events publishing to Reader * Add #read_multiline to Reader * Add :convert option for ask configuration * Add ability to specify custom proc converters * Add #ask_keypress to gather character input * Add #ask_multiline to gather multiline input * Add MaskedQuestion & #mask method for masking input stream characters ### Changed * Change Reader#read_keypress to be robust and read correctly byte sequences * Change Reader#getc to #read_line and extend arguments with echo option * Extract cursor movement to dependency tty-cursor * Change List & MultiList to subscribe to keyboard events * Change to move mode inside reader namespace * Remove Response & Error objects * Remove :char option from #ask * Change :read option to specify mode of reading out of :line, :multiline, :keypress * Rename #confirm to #ok ## [v0.2.0] - 2015-11-23 ### Added * Add ability to select choice form list #select * Add ability to select multiple options #multi_select * Add :read option to #ask for reading specific type input ### Changed * Change #ask api to be similar to #select and #multi_select behaviour * Change #ask :argument option to be :required * Remove :valid option from #ask as #select is a better solution ## [v0.1.0] - 2015-11-01 * Initial implementation and release [v0.23.1]: https://github.com/piotrmurach/tty-prompt/compare/v0.23.0...v0.23.1 [v0.23.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.22.0...v0.23.0 [v0.22.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.21.0...v0.22.0 [v0.21.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.20.0...v0.21.0 [v0.20.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.19.0...v0.20.0 [v0.19.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.18.1...v0.19.0 [v0.18.1]: https://github.com/piotrmurach/tty-prompt/compare/v0.18.0...v0.18.1 [v0.18.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.17.2...v0.18.0 [v0.17.2]: https://github.com/piotrmurach/tty-prompt/compare/v0.17.1...v0.17.2 [v0.17.1]: https://github.com/piotrmurach/tty-prompt/compare/v0.17.0...v0.17.1 [v0.17.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.16.1...v0.17.0 [v0.16.1]: https://github.com/piotrmurach/tty-prompt/compare/v0.16.0...v0.16.1 [v0.16.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.15.0...v0.16.0 [v0.15.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.14.0...v0.15.0 [v0.14.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.13.2...v0.14.0 [v0.13.2]: https://github.com/piotrmurach/tty-prompt/compare/v0.13.1...v0.13.2 [v0.13.1]: https://github.com/piotrmurach/tty-prompt/compare/v0.13.0...v0.13.1 [v0.13.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.12.0...v0.13.0 [v0.12.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.11.0...v0.12.0 [v0.11.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.10.1...v0.11.0 [v0.10.1]: https://github.com/piotrmurach/tty-prompt/compare/v0.10.0...v0.10.1 [v0.10.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.9.0...v0.10.0 [v0.9.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.8.0...v0.9.0 [v0.8.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.7.1...v0.8.0 [v0.7.1]: https://github.com/piotrmurach/tty-prompt/compare/v0.7.0...v0.7.1 [v0.7.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.6.0...v0.7.0 [v0.6.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.5.0...v0.6.0 [v0.5.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.4.0...v0.5.0 [v0.4.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.3.0...v0.4.0 [v0.3.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.2.0...v0.3.0 [v0.2.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.1.0...v0.2.0 [v0.1.0]: https://github.com/piotrmurach/tty-prompt/compare/v0.1.0 tty-prompt-0.23.1/CODE_OF_CONDUCT.md000066400000000000000000000062401403662044600165420ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at piotr@piotrmurach.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] [homepage]: https://contributor-covenant.org [version]: https://contributor-covenant.org/version/1/4/ tty-prompt-0.23.1/Gemfile000066400000000000000000000006371403662044600152420ustar00rootroot00000000000000source "https://rubygems.org" gemspec # gem "tty-reader", git: "https://github.com/piotrmurach/tty-reader" # gem "pastel", git: "https://github.com/piotrmurach/pastel" gem "json", "2.4.1" if RUBY_VERSION == "2.0.0" group :test do gem "benchmark-ips", "~> 2.7.2" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.5.0") gem "coveralls_reborn", "~> 0.21.0" gem "simplecov", "~> 0.21.0" end end tty-prompt-0.23.1/LICENSE.txt000066400000000000000000000020771403662044600155720ustar00rootroot00000000000000Copyright (c) 2015 Piotr Murach (piotrmurach.com) MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. tty-prompt-0.23.1/README.md000066400000000000000000001410451403662044600152250ustar00rootroot00000000000000
TTY Toolkit logo
# TTY::Prompt [![Gitter](https://badges.gitter.im/Join%20Chat.svg)][gitter] [![Gem Version](https://badge.fury.io/rb/tty-prompt.svg)][gem] [![Actions CI](https://github.com/piotrmurach/tty-prompt/workflows/CI/badge.svg?branch=master)][gh_actions_ci] [![Build status](https://ci.appveyor.com/api/projects/status/4cguoiah5dprbq7n?svg=true)][appveyor] [![Code Climate](https://codeclimate.com/github/piotrmurach/tty-prompt/badges/gpa.svg)][codeclimate] [![Coverage Status](https://coveralls.io/repos/github/piotrmurach/tty-prompt/badge.svg)][coverage] [![Inline docs](http://inch-ci.org/github/piotrmurach/tty-prompt.svg?branch=master)][inchpages] [gitter]: https://gitter.im/piotrmurach/tty [gem]: http://badge.fury.io/rb/tty-prompt [gh_actions_ci]: https://github.com/piotrmurach/tty-prompt/actions?query=workflow%3ACI [travis]: http://travis-ci.org/piotrmurach/tty-prompt [appveyor]: https://ci.appveyor.com/project/piotrmurach/tty-prompt [codeclimate]: https://codeclimate.com/github/piotrmurach/tty-prompt [coverage]: https://coveralls.io/github/piotrmurach/tty-prompt [inchpages]: http://inch-ci.org/github/piotrmurach/tty-prompt > A beautiful and powerful interactive command line prompt. **TTY::Prompt** provides independent prompt component for [TTY](https://github.com/piotrmurach/tty) toolkit. ## Features * Number of prompt types for gathering user input * A robust API for validating complex inputs * User friendly error feedback * Intuitive DSL for creating complex menus * Ability to page long menus * Support for Linux, OS X, FreeBSD and Windows systems ## Windows support `tty-prompt` works across all Unix and Windows systems in the "best possible" way. On Windows, it uses Win32 API in place of terminal device to provide matching functionality. Since Unix terminals provide richer set of features than Windows PowerShell consoles, expect to have a better experience on Unix-like platform. Some features like `select` or `multi_select` menus may not work on Windows when run from Git Bash. See GitHub suggested [fixes](https://github.com/git-for-windows/git/wiki/FAQ#some-native-console-programs-dont-work-when-run-from-git-bash-how-to-fix-it). For Windows, consider installing [ConEmu](https://conemu.github.io/), [cmder](http://cmder.net/) or [PowerCmd](http://www.powercmd.com/). ## Installation Add this line to your application's Gemfile: ```ruby gem "tty-prompt" ``` And then execute: $ bundle Or install it yourself as: $ gem install tty-prompt ## Contents * [1. Usage](#1-usage) * [2. Interface](#2-interface) * [2.1 ask](#21-ask) * [2.1.1 :convert](#211-convert) * [2.1.2 :default](#212-default) * [2.1.3 :value](#213-value) * [2.1.4 :echo](#214-echo) * [2.1.5 error messages](#215-error-messages) * [2.1.6 :in](#216-in) * [2.1.7 :modify](#217-modify) * [2.1.8 :required](#218-required) * [2.1.9 :validate](#219-validate) * [2.2 keypress](#22-keypress) * [2.2.1 :timeout](#221-timeout) * [2.3 multiline](#23-multiline) * [2.4 mask](#24-mask) * [2.5 yes?/no?](#25-yesno) * [2.6 menu](#26-menu) * [2.6.1 choices](#261-choices) * [2.6.1.1 :disabled](#2611-disabled) * [2.6.2 select](#262-select) * [2.6.2.1 :cycle](#2621-cycle) * [2.6.2.2 :enum](#2622-enum) * [2.6.2.3 :help](#2623-help) * [2.6.2.4 :marker](#2624-marker) * [2.6.2.5 :per_page](#2625-per_page) * [2.6.2.6 :disabled](#2626-disabled) * [2.6.2.7 :filter](#2627-filter) * [2.6.3 multi_select](#263-multi_select) * [2.6.3.1 :cycle](#2631-cycle) * [2.6.3.2 :enum](#2632-enum) * [2.6.3.3 :help](#2633-help) * [2.6.3.4 :per_page](#2634-per_page) * [2.6.3.5 :disabled](#2635-disabled) * [2.6.3.6 :echo](#2636-echo) * [2.6.3.7 :filter](#2637-filter) * [2.6.3.8 :min](#2638-min) * [2.6.3.9 :max](#2639-max) * [2.6.4 enum_select](#264-enum_select) * [2.6.4.1 :per_page](#2641-per_page) * [2.6.4.1 :disabled](#2641-disabled) * [2.7 expand](#27-expand) * [2.7.1 :auto_hint](#271-auto_hint) * [2.8 collect](#28-collect) * [2.9 suggest](#29-suggest) * [2.10 slider](#210-slider) * [2.11 say](#211-say) * [2.11.1 ok](#2111-ok) * [2.11.2 warn](#2112-warn) * [2.11.3 error](#2113-error) * [2.12 keyboard events](#212-keyboard-events) * [3. settings](#3-settings) * [3.1 :symbols](#31-symbols) * [3.2 :active_color](#32-active_color) * [3.3 :enable_color](#33-enable_color) * [3.4 :help_color](#34-help_color) * [3.5 :interrupt](#35-interrupt) * [3.6 :prefix](#36-prefix) * [3.7 :quiet](#37-quiet) * [3.8 :track_history](#38-track_history) ## 1. Usage In order to start asking questions on the command line, create prompt: ```ruby require "tty-prompt" prompt = TTY::Prompt.new ``` And then call `ask` with the question for simple input: ```ruby prompt.ask("What is your name?", default: ENV["USER"]) # => What is your name? (piotr) ``` To confirm input use `yes?`: ```ruby prompt.yes?("Do you like Ruby?") # => Do you like Ruby? (Y/n) ``` If you want to input password or secret information use `mask`: ```ruby prompt.mask("What is your secret?") # => What is your secret? •••• ``` Asking question with list of options couldn't be easier using `select` like so: ```ruby prompt.select("Choose your destiny?", %w(Scorpion Kano Jax)) # => # Choose your destiny? (Use ↑/↓ arrow keys, press Enter to select) # ‣ Scorpion # Kano # Jax ``` Also, asking multiple choice questions is a breeze with `multi_select`: ```ruby choices = %w(vodka beer wine whisky bourbon) prompt.multi_select("Select drinks?", choices) # => # # Select drinks? (Use ↑/↓ arrow keys, press Space to select and Enter to finish)" # ‣ ⬡ vodka # ⬡ beer # ⬡ wine # ⬡ whisky # ⬡ bourbon ``` To ask for a selection from enumerated list you can use `enum_select`: ```ruby choices = %w(emacs nano vim) prompt.enum_select("Select an editor?", choices) # => # # Select an editor? # 1) emacs # 2) nano # 3) vim # Choose 1-3 [1]: ``` However, if you have a lot of options to choose from you may want to use `expand`: ```ruby choices = [ { key: "y", name: "overwrite this file", value: :yes }, { key: "n", name: "do not overwrite this file", value: :no }, { key: "a", name: "overwrite this file and all later files", value: :all }, { key: "d", name: "show diff", value: :diff }, { key: "q", name: "quit; do not overwrite this file ", value: :quit } ] prompt.expand("Overwrite Gemfile?", choices) # => # Overwrite Gemfile? (enter "h" for help) [y,n,a,d,q,h] ``` If you wish to collect more than one answer use `collect`: ```ruby result = prompt.collect do key(:name).ask("Name?") key(:age).ask("Age?", convert: :int) key(:address) do key(:street).ask("Street?", required: true) key(:city).ask("City?") key(:zip).ask("Zip?", validate: /\A\d{3}\Z/) end end # => # {:name => "Piotr", :age => 30, :address => {:street => "Street", :city => "City", :zip => "123"}} ``` ## 2. Interface ### 2.1 ask In order to ask a basic question do: ```ruby prompt.ask("What is your name?") ``` However, to prompt for more complex input you can use robust API by passing hash of properties or using a block like so: ```ruby prompt.ask("What is your name?") do |q| q.required true q.validate /\A\w+\Z/ q.modify :capitalize end ``` #### 2.1.1 `:convert` The `convert` property is used to convert input to a required type. By default no conversion of input is performed. To change this use one of the following conversions: * `:boolean`|`:bool` - e.g. 'yes/1/y/t/' becomes `true`, 'no/0/n/f' becomes `false` * `:date` - parses dates formats "28/03/2020", "March 28th 2020" * `:time` - parses time formats "11:20:03" * `:float` - e.g. `-1` becomes `-1.0` * `:int`|`:integer` - e.g. `+1` becomes `1` * `:sym`|`:symbol` - e.g. "foo" becomes `:foo` * `:filepath` - converts to file path * `:path`|`:pathname` - converts to `Pathname` object * `:range` - e.g. '1-10' becomes `1..10` range object * `:regexp` - e.g. "foo|bar" becomes `/foo|bar/` * `:uri` - converts to `URI` object * `:list`|`:array` - e.g. 'a,b,c' becomes `["a", "b", "c"]` * `:map`|`:hash` - e.g. 'a:1 b:2 c:3' becomes `{a: "1", b: "2", c: "3"}` In addition you can specify a plural or append `list` or `array` to any base type: * `:ints` or `:int_list` - will convert to a list of integers * `:floats` or `:float_list` - will convert to a list of floats * `:bools` or `:bool_list` - will convert to a list of booleans, e.g. `t,f,t` becomes `[true, false, true]` Similarly, you can append `map` or `hash` to any base type: * `:int_map`|`:integer_map`|`:int_hash` - will convert to a hash of integers, e.g `a:1 b:2 c:3` becomes `{a: 1, b: 2, c: 3}` * `:bool_map` | `:boolean_map`|`:bool_hash` - will convert to a hash of booleans, e.g `a:t b:f c:t` becomes `{a: true, b: false, c: true}` By default, `map` converts keys to symbols, if you wish to use strings instead specify key type like so: * `:str_int_map` - will convert to a hash of string keys and integer values * `:string_integer_hash` - will convert to a hash of string keys and integer values For example, if you are interested in range type as answer do the following: ```ruby prompt.ask("Provide range of numbers?", convert: :range) # Provide range of numbers? 1-10 # => 1..10 ``` If, on the other hand, you wish to convert input to a hash of integer values do: ```ruby prompt.ask("Provide keys and values:", convert: :int_map) # Provide keys and values: a=1 b=2 c=3 # => {a: 1, b: 2, c: 3} ``` If a user provides a wrong type for conversion an error message will be printed in the console: ```ruby prompt.ask("Provide digit:", convert: :float) # Provide digit: x # >> Cannot convert `x` into 'float' type ``` You can further customize error message: ```ruby prompt.ask("Provide digit:", convert: :float) do |q| q.convert(:float, "Wrong value of %{value} for %{type} conversion") # or q.convert :float q.messages[:convert?] = "Wrong value of %{value} for %{type} conversion" end ``` You can also provide a custom conversion like so: ```ruby prompt.ask("Ingredients? (comma sep list)") do |q| q.convert -> (input) { input.split(/,\s*/) } end # Ingredients? (comma sep list) milk, eggs, flour # => ["milk", "eggs", "flour"] ``` #### 2.1.2 `:default` The `:default` option is used if the user presses return key: ```ruby prompt.ask("What is your name?", default: "Anonymous") # => # What is your name? (Anonymous) ``` #### 2.1.3 `:value` To pre-populate the input line for editing use `:value` option: ```ruby prompt.ask("What is your name?", value: "Piotr") # => # What is your name? Piotr ``` #### 2.1.4 `:echo` To control whether the input is shown back in terminal or not use `:echo` option like so: ```ruby prompt.ask("password:", echo: false) ``` #### 2.1.5 error messages By default `tty-prompt` comes with predefined error messages for `convert`, `required`, `in`, `validate` options. You can change these and configure to your liking either by passing message as second argument with the option: ```ruby prompt.ask("What is your email?") do |q| q.validate(/\A\w+@\w+\.\w+\Z/, "Invalid email address") end ``` Or change the `messages` key entry out of `:convert?`, `:range?`, `:required?` and `:valid?`: ```ruby prompt.ask("What is your email?") do |q| q.validate(/\A\w+@\w+\.\w+\Z/) q.messages[:valid?] = "Invalid email address" end ``` To change default range validation error message do: ```ruby prompt.ask("How spicy on scale (1-5)? ") do |q| q.in "1-5" q.messages[:range?] = "%{value} out of expected range %{in}" end ``` #### 2.1.6 `:in` In order to check that provided input falls inside a range of inputs use the `in` option. For example, if we wanted to ask a user for a single digit in given range we may do following: ```ruby prompt.ask("Provide number in range: 0-9?") { |q| q.in("0-9") } ``` #### 2.1.7 `:modify` Set the `:modify` option if you want to handle whitespace or letter capitalization. ```ruby prompt.ask("Enter text:") do |q| q.modify :strip, :collapse end ``` Available letter casing settings are: ```ruby :up # change to upper case :down # change to small case :capitalize # capitalize each word ``` Available whitespace settings are: ```ruby :trim # remove whitespace from both ends of the input :strip # same as :trim :chomp # remove whitespace at the end of input :collapse # reduce all whitespace to single character :remove # remove all whitespace ``` #### 2.1.8 `:required` To ensure that input is provided use `:required` option: ```ruby prompt.ask("What's your phone number?", required: true) # What's your phone number? # >> Value must be provided ``` #### 2.1.9 `:validate` In order to validate that input matches a given pattern you can pass the `validate` option/method. Validate accepts `Regex`, `Proc` or `Symbol`. ```ruby prompt.ask("What is your username?") do |q| q.validate(/\A[^.]+\.[^.]+\Z/) end ``` The above can also be expressed as a `Proc`: ```ruby prompt.ask("What is your username?") do |q| q.validate ->(input) { input =~ /\A[^.]+\.[^.]+\Z/ } end ``` There is a built-in validation for `:email` and you can use it directly like so: ```ruby prompt.ask("What is your email?") { |q| q.validate :email } ``` The default validation message is `"Your answer is invalid (must match %{valid})"` and you can customise it by passing in a second argument: ```ruby prompt.ask("What is your username?") do |q| q.validate(/\A[^.]+\.[^.]+\Z/, "Invalid username: %{value}, must match %{valid}") end ``` The default message can also be set using `messages` and the `:valid?` key: ```ruby prompt.ask("What is your username?") do |q| q.validate(/\A[^.]+\.[^.]+\Z/) q.messages[:valid?] = "Invalid username: %{value}, must match %{valid}") end ``` ### 2.2. keypress In order to ask question that awaits a single character answer use `keypress` prompt like so: ```ruby prompt.keypress("Press key ?") # Press key? # => a ``` By default any key is accepted but you can limit keys by using `:keys` option. Any key event names such as `:space` or `:ctrl_k` are valid: ```ruby prompt.keypress("Press space or enter to continue", keys: [:space, :return]) ``` #### 2.2.1 timeout Timeout can be set using `:timeout` option to expire prompt and allow the script to continue automatically: ```ruby prompt.keypress("Press any key to continue, resumes automatically in 3 seconds ...", timeout: 3) ``` In addition the `keypress` recognises `:countdown` token when inserted inside the question. It will automatically countdown the time in seconds: ```ruby prompt.keypress("Press any key to continue, resumes automatically in :countdown ...", timeout: 3) ``` ### 2.3 multiline Asking for multiline input can be done with `multiline` method. The reading of input will terminate when `Ctrl+d` or `Ctrl+z` is pressed. Empty lines will not be included in the returned array. ```ruby prompt.multiline("Description?") # Description? (Press CTRL-D or CTRL-Z to finish) # I know not all that may be coming, # but be it what it will, # I'll go to it laughing. # => # ["I know not all that may be coming,\n", "but be it what it will,\n", "I'll go to it laughing.\n"] ``` The `multiline` uses similar options to those supported by `ask` prompt. For example, to provide default description: ```ruby prompt.multiline("Description?", default: "A super sweet prompt.") ``` Or using DSL: ```ruby prompt.multiline("Description?") do |q| q.default "A super sweet prompt." q.help "Press thy ctrl+d to end" end ``` ### 2.4 mask If you require input of confidential information use `mask` method. By default each character that is printed is replaced by `•` symbol. All configuration options applicable to `ask` method can be used with `mask` as well. ```ruby prompt.mask("What is your secret?") # => What is your secret? •••• ``` The masking character can be changed by passing the `:mask` key: ```ruby heart = prompt.decorate(prompt.symbols[:heart] + " ", :magenta) prompt.mask("What is your secret?", mask: heart) # => What is your secret? ❤ ❤ ❤ ❤ ❤ ``` If you don't wish to show any output use `:echo` option like so: ```ruby prompt.mask("What is your secret?", echo: false) ``` You can also provide validation for your mask to enforce for instance strong passwords: ```ruby prompt.mask("What is your secret?", mask: heart) do |q| q.validate(/[a-z\ ]{5,15}/) end ``` ### 2.5 yes?/no? In order to display a query asking for boolean input from user use `yes?` like so: ```ruby prompt.yes?("Do you like Ruby?") # => # Do you like Ruby? (Y/n) ``` You can further customize question by passing `suffix`, `positive`, `negative` and `convert` options. The `suffix` changes text of available options, the `positive` specifies display string for successful answer and `negative` changes display string for negative answer. The final value is a boolean provided the `convert` option evaluates to boolean. It's enough to provide the `suffix` option for the prompt to accept matching answers with correct labels: ```ruby prompt.yes?("Are you a human?") do |q| q.suffix "Yup/nope" end # => # Are you a human? (Yup/nope) ``` Alternatively, instead of `suffix` option provide the `positive` and `negative` labels: ```ruby prompt.yes?("Are you a human?") do |q| q.default false q.positive "Yup" q.negative "Nope" end # => # Are you a human? (yup/Nope) ``` Finally, providing all available options you can ask fully customized question: ```ruby prompt.yes?("Are you a human?") do |q| q.suffix "Agree/Disagree" q.positive "Agree" q.negative "Disagree" q.convert -> (input) { !input.match(/^agree$/i).nil? } end # => # Are you a human? (Agree/Disagree) ``` There is also the opposite for asking confirmation of negative question: ```ruby prompt.no?("Do you hate Ruby?") # => # Do you hate Ruby? (y/N) ``` Similarly to `yes?` method, you can supply the same options to customize the question. ### 2.6 menu ### 2.6.1 choices There are many ways in which you can add menu choices. The simplest way is to create an array of values: ```ruby choices = %w(small medium large) ``` By default the choice name is also the value the prompt will return when selected. To provide custom values, you can provide a hash with keys as choice names and their respective values: ```ruby choices = {small: 1, medium: 2, large: 3} prompt.select("What size?", choices) # => # What size? (Press ↑/↓ arrow to move and Enter to select) # ‣ small # medium # large ``` Finally, you can define an array of choices where each choice is a hash value with `:name` & `:value` keys which can include other options for customising individual choices: ```ruby choices = [ {name: "small", value: 1}, {name: "medium", value: 2, disabled: "(out of stock)"}, {name: "large", value: 3} ] ``` You can specify `:key` as an additional option which will be used as short name for selecting the choice via keyboard key press. Another way to create menu with choices is using the DSL and the `choice` method. For example, the previous array of choices with hash values can be translated as: ```ruby prompt.select("What size?") do |menu| menu.choice name: "small", value: 1 menu.choice name: "medium", value: 2, disabled: "(out of stock)" menu.choice name: "large", value: 3 end # => # What size? (Press ↑/↓ arrow to move and Enter to select) # ‣ small # ✘ medium (out of stock) # large ``` or in a more compact way: ```ruby prompt.select("What size?") do |menu| menu.choice "small", 1 menu.choice "medium", 2, disabled: "(out of stock)" menu.choice "large", 3 end ``` #### 2.6.1.1 `:disabled` The `:disabled` key indicates to display a choice as currently unavailable to select. Disabled choices are displayed with a cross `✘` character next to them. If the choice is disabled, it cannot be selected. The value for the `:disabled` is used next to the choice to provide reason for excluding it from the selection menu. For example: ```ruby choices = [ {name: "small", value: 1}, {name: "medium", value: 2, disabled: "(out of stock)"}, {name: "large", value: 3} ] ``` ### 2.6.2 select For asking questions involving list of options use `select` method by passing the question and possible choices: ```ruby prompt.select("Choose your destiny?", %w(Scorpion Kano Jax)) # => # Choose your destiny? (Use ↑/↓ arrow keys, press Enter to select) # ‣ Scorpion # Kano # Jax ``` You can also provide options through DSL using the `choice` method for single entry and/or `choices` for more than one choice: ```ruby prompt.select("Choose your destiny?") do |menu| menu.choice "Scorpion" menu.choice "Kano" menu.choice "Jax" end # => # Choose your destiny? (Use ↑/↓ arrow keys, press Enter to select) # ‣ Scorpion # Kano # Jax ``` By default the choice name is used as return value, but you can provide your custom values including a `Proc` object: ```ruby prompt.select("Choose your destiny?") do |menu| menu.choice "Scorpion", 1 menu.choice "Kano", 2 menu.choice "Jax", -> { "Nice choice captain!" } end # => # Choose your destiny? (Use ↑/↓ arrow keys, press Enter to select) # ‣ Scorpion # Kano # Jax ``` If you wish you can also provide a simple hash to denote choice name and its value like so: ```ruby choices = {"Scorpion" => 1, "Kano" => 2, "Jax" => 3} prompt.select("Choose your destiny?", choices) ``` To mark particular answer as selected use `default` with either an index of the choice starting from `1` or a choice's name: ```ruby prompt.select("Choose your destiny?") do |menu| menu.default 3 # or menu.default "Jax" menu.choice "Scorpion", 1 menu.choice "Kano", 2 menu.choice "Jax", 3 end # => # Choose your destiny? (Use ↑/↓ arrow keys, press Enter to select) # Scorpion # Kano # ‣ Jax ``` #### 2.6.2.1 `:cycle` You can navigate the choices using the arrow keys or define your own key mappings (see [keyboard events](#212-keyboard-events). When reaching the top/bottom of the list, the selection does not cycle around by default. If you wish to enable cycling, you can pass `cycle: true` to `select` and `multi_select`: ```ruby prompt.select("Choose your destiny?", %w(Scorpion Kano Jax), cycle: true) # => # Choose your destiny? (Use ↑/↓ arrow keys, press Enter to select) # ‣ Scorpion # Kano # Jax ``` #### 2.6.2.2 `:enum` For ordered choices set `enum` to any delimiter String. In that way, you can use arrows keys and numbers (0-9) to select the item. ```ruby prompt.select("Choose your destiny?") do |menu| menu.enum "." menu.choice "Scorpion", 1 menu.choice "Kano", 2 menu.choice "Jax", 3 end # => # Choose your destiny? (Use ↑/↓ arrow or number (0-9) keys, press Enter to select) # 1. Scorpion # 2. Kano # ‣ 3. Jax ``` #### 2.6.2.3 `:help` You can configure help message with `:help` and when to display it with `:show_help` options. The help can be displayed on `start`, `never` or `always`: ```ruby choices = %w(Scorpion Kano Jax) prompt.select("Choose your destiny?", choices, help: "(Bash keyboard keys)", show_help: :always) # => # Choose your destiny? (Bash keyboard keys) # > Scorpion # Kano # Jax ``` #### 2.6.2.4 `:marker` You can configure active marker like so: ```ruby choices = %w(Scorpion Kano Jax) prompt.select("Choose your destiny?", choices, symbols: { marker: ">" }) # => # Choose your destiny? (Use ↑/↓ and ←/→ arrow keys, press Enter to select) # > Scorpion # Kano # Jax ``` #### 2.6.2.5 `:per_page` By default the menu is paginated if selection grows beyond `6` items. To change this setting use `:per_page` configuration. ```ruby letters = ("A".."Z").to_a prompt.select("Choose your letter?", letters, per_page: 4) # => # Which letter? (Use ↑/↓ and ←/→ arrow keys, press Enter to select) # ‣ A # B # C # D ``` You can also customise page navigation text using `:help` option: ```ruby letters = ("A".."Z").to_a prompt.select("Choose your letter?") do |menu| menu.per_page 4 menu.help "(Wiggle thy finger up/down and left/right to see more)" menu.choices letters end # => # Which letter? (Wiggle thy finger up/down and left/right to see more) # ‣ A # B # C # D ``` #### 2.6.2.6 `:disabled` To disable menu choice, use the `:disabled` key with a value that explains the reason for the choice being unavailable. For example, out of all warriors, the Goro is currently injured: ```ruby warriors = [ "Scorpion", "Kano", { name: "Goro", disabled: "(injury)" }, "Jax", "Kitana", "Raiden" ] ``` The disabled choice will be displayed with a cross `✘` character next to it and followed by an explanation: ```ruby prompt.select("Choose your destiny?", warriors) # => # Choose your destiny? (Use ↑/↓ arrow keys, press Enter to select) # ‣ Scorpion # Kano # ✘ Goro (injury) # Jax # Kitana # Raiden ``` #### 2.6.2.7 `:filter` To activate dynamic list searching on letter/number key presses use `:filter` option: ```ruby warriors = %w(Scorpion Kano Jax Kitana Raiden) prompt.select("Choose your destiny?", warriors, filter: true) # => # Choose your destiny? (Use ↑/↓ arrow keys, press Enter to select, and letter keys to filter) # ‣ Scorpion # Kano # Jax # Kitana # Raiden ``` After the user presses "k": ```ruby # => # Choose your destiny? (Filter: "k") # ‣ Kano # Kitana ``` After the user presses "ka": ```ruby # => # Choose your destiny? (Filter: "ka") # ‣ Kano ``` Filter characters can be deleted partially or entirely via, respectively, Backspace and Canc. If the user changes or deletes a filter, the choices previously selected remain selected. ### 2.6.3 multi_select For asking questions involving multiple selection list use `multi_select` method by passing the question and possible choices: ```ruby choices = %w(vodka beer wine whisky bourbon) prompt.multi_select("Select drinks?", choices) # => # # Select drinks? (Use ↑/↓ arrow keys, press Space to select and Enter to finish)" # ‣ ⬡ vodka # ⬡ beer # ⬡ wine # ⬡ whisky # ⬡ bourbon ``` As a return value, the `multi_select` will always return an array by default populated with the names of the choices. If you wish to return custom values for the available choices do: ```ruby choices = {vodka: 1, beer: 2, wine: 3, whisky: 4, bourbon: 5} prompt.multi_select("Select drinks?", choices) # Provided that vodka and beer have been selected, the function will return # => [1, 2] ``` Similar to `select` method, you can also provide options through DSL using the `choice` method for single entry and/or `choices` call for more than one choice: ```ruby prompt.multi_select("Select drinks?") do |menu| menu.choice :vodka, {score: 1} menu.choice :beer, 2 menu.choice :wine, 3 menu.choices whisky: 4, bourbon: 5 end ``` To mark choice(s) as selected use the `default` option with either index(s) of the choice(s) starting from `1` or choice name(s): ```ruby prompt.multi_select("Select drinks?") do |menu| menu.default 2, 5 # or menu.default :beer, :whisky menu.choice :vodka, {score: 10} menu.choice :beer, {score: 20} menu.choice :wine, {score: 30} menu.choice :whisky, {score: 40} menu.choice :bourbon, {score: 50} end # => # Select drinks? beer, bourbon # ⬡ vodka # ⬢ beer # ⬡ wine # ⬡ whisky # ‣ ⬢ bourbon ``` #### 2.6.3.1 `:cycle` Also like, `select`, the method takes an option `cycle` (which defaults to `false`), which lets you configure whether the selection should cycle around when reaching the top/bottom of the list when navigating: ```ruby prompt.multi_select("Select drinks?", %w(vodka beer wine), cycle: true) ``` #### 2.6.3.2 `:enum` Like `select`, for ordered choices set `enum` to any delimiter String. In that way, you can use arrows keys and numbers (0-9) to select the item. ```ruby prompt.multi_select("Select drinks?") do |menu| menu.enum ")" menu.choice :vodka, {score: 10} menu.choice :beer, {score: 20} menu.choice :wine, {score: 30} menu.choice :whisky, {score: 40} menu.choice :bourbon, {score: 50} end # => # Select drinks? beer, bourbon # ⬡ 1) vodka # ⬢ 2) beer # ⬡ 3) wine # ⬡ 4) whisky # ‣ ⬢ 5) bourbon ``` And when you press enter you will see the following selected: ```ruby # Select drinks? beer, bourbon # => [{score: 20}, {score: 50}] ``` #### 2.6.3.3 `:help` You can configure help message with `:help` and when to display it with `:show_help` options. The help can be displayed on `start`, `never` or `always`: ```ruby choices = {vodka: 1, beer: 2, wine: 3, whisky: 4, bourbon: 5} prompt.multi_select("Select drinks?", choices, help: "Press beer can against keyboard", show_help: :always) # => # Select drinks? (Press beer can against keyboard)" # ‣ ⬡ vodka # ⬡ beer # ⬡ wine # ⬡ whisky # ⬡ bourbon ``` #### 2.6.3.4 `:per_page` By default the menu is paginated if selection grows beyond `6` items. To change this setting use `:per_page` configuration. ```ruby letters = ("A".."Z").to_a prompt.multi_select("Choose your letter?", letters, per_page: 4) # => # Which letter? (Use ↑/↓ and ←/→ arrow keys, press Space to select and Enter to finish) # ‣ ⬡ A # ⬡ B # ⬡ C # ⬡ D ``` #### 2.6.3.5 `:disabled` To disable menu choice, use the `:disabled` key with a value that explains the reason for the choice being unavailable. For example, out of all drinks, the sake and beer are currently out of stock: ```ruby drinks = [ "bourbon", {name: "sake", disabled: "(out of stock)"}, "vodka", {name: "beer", disabled: "(out of stock)"}, "wine", "whisky" ] ``` The disabled choice will be displayed with a cross `✘` character next to it and followed by an explanation: ```ruby prompt.multi_select("Choose your favourite drink?", drinks) # => # Choose your favourite drink? (Use ↑/↓ arrow keys, press Space to select and Enter to finish) # ‣ ⬡ bourbon # ✘ sake (out of stock) # ⬡ vodka # ✘ beer (out of stock) # ⬡ wine # ⬡ whisky ``` #### 2.6.3.6 `:echo` To control whether the selected items are shown on the question header use the :echo option: ```ruby choices = %w(vodka beer wine whisky bourbon) prompt.multi_select("Select drinks?", choices, echo: false) # => # Select drinks? # ⬡ vodka # ⬢ 2) beer # ⬡ 3) wine # ⬡ 4) whisky # ‣ ⬢ 5) bourbon ``` #### 2.6.3.7 `:filter` To activate dynamic list filtering on letter/number typing, use the :filter option: ```ruby choices = %w(vodka beer wine whisky bourbon) prompt.multi_select("Select drinks?", choices, filter: true) # => # Select drinks? (Use ↑/↓ arrow keys, press Space to select and Enter to finish, and letter keys to filter) # ‣ ⬡ vodka # ⬡ beer # ⬡ wine # ⬡ whisky # ⬡ bourbon ``` After the user presses "w": ```ruby # Select drinks? (Filter: "w") # ‣ ⬡ wine # ⬡ whisky ``` Filter characters can be deleted partially or entirely via, respectively, Backspace and Canc. If the user changes or deletes a filter, the choices previously selected remain selected. The `filter` option is not compatible with `enum`. #### 2.6.3.8 `:min` To force the minimum number of choices an user must select, use the `:min` option: ```ruby choices = %w(vodka beer wine whisky bourbon) prompt.multi_select("Select drinks?", choices, min: 3) # => # Select drinks? (min. 3) vodka, beer # ⬢ vodka # ⬢ beer # ⬡ wine # ⬡ wiskey # ‣ ⬡ bourbon ``` #### 2.6.3.9 `:max` To limit the number of choices an user can select, use the `:max` option: ```ruby choices = %w(vodka beer wine whisky bourbon) prompt.multi_select("Select drinks?", choices, max: 3) # => # Select drinks? (max. 3) vodka, beer, whisky # ⬢ vodka # ⬢ beer # ⬡ wine # ⬢ whisky # ‣ ⬡ bourbon ``` ### 2.6.4 enum_select In order to ask for standard selection from indexed list you can use `enum_select` and pass question together with possible choices: ```ruby choices = %w(emacs nano vim) prompt.enum_select("Select an editor?", choices) # => # # Select an editor? # 1) nano # 2) vim # 3) emacs # Choose 1-3 [1]: ``` Similar to `select` and `multi_select`, you can provide question options through DSL using `choice` method and/or `choices` like so: ```ruby choices = %w(nano vim emacs) prompt.enum_select("Select an editor?") do |menu| menu.choice :nano, "/bin/nano" menu.choice :vim, "/usr/bin/vim" menu.choice :emacs, "/usr/bin/emacs" end # => # # Select an editor? # 1) nano # 2) vim # 3) emacs # Choose 1-3 [1]: # # Select an editor? /bin/nano ``` You can change the indexed numbers formatting by passing `enum` option. The `default` option lets you specify which choice to mark as selected by default. It accepts an index of the choice starting from `1` or a choice name: ```ruby choices = %w(nano vim emacs) prompt.enum_select("Select an editor?") do |menu| menu.default 2 # or menu.defualt "/usr/bin/vim" menu.enum "." menu.choice :nano, "/bin/nano" menu.choice :vim, "/usr/bin/vim" menu.choice :emacs, "/usr/bin/emacs" end # => # # Select an editor? # 1. nano # 2. vim # 3. emacs # Choose 1-3 [2]: # # Select an editor? /usr/bin/vim ``` #### 2.6.4.1 `:per_page` By default the menu is paginated if selection grows beyond `6` items. To change this setting use `:per_page` configuration. ```ruby letters = ("A".."Z").to_a prompt.enum_select("Choose your letter?", letters, per_page: 4) # => # Which letter? # 1) A # 2) B # 3) C # 4) D # Choose 1-26 [1]: # (Press tab/right or left to reveal more choices) ``` #### 2.6.4.2 `:disabled` To make a choice unavailable use the `:disabled` option and, if you wish, as value provide a reason: ```ruby choices = [ {name: "Emacs", disabled: "(not installed)"}, "Atom", "GNU nano", {name: "Notepad++", disabled: "(not installed)"}, "Sublime", "Vim" ] ``` The disabled choice will be displayed with a cross ✘ character next to it and followed by an explanation: ```ruby prompt.enum_select("Select an editor", choices) # => # Select an editor # ✘ 1) Emacs (not installed) # 2) Atom # 3) GNU nano # ✘ 4) Notepad++ (not installed) # 5) Sublime # 6) Vim # Choose 1-6 [2]: ``` ### 2.7 expand The `expand` provides a compact way to ask a question with many options. As first argument `expand` takes the message to display and as a second an array of choices. Compared to the `select`, `multi_select` and `enum_select`, the choices need to be objects that include `:key`, `:name` and `:value` keys. The `:key` must be a single character. The help choice is added automatically as the last option under the key `h`. ```ruby choices = [ { key: "y", name: "overwrite this file", value: :yes }, { key: "n", name: "do not overwrite this file", value: :no }, { key: "q", name: "quit; do not overwrite this file ", value: :quit } ] ``` The choices can also be provided through DSL using the `choice` method. The `:value` can be a primitive value or `Proc` instance that gets executed and whose value is used as returned type. For example: ```ruby prompt.expand("Overwrite Gemfile?") do |q| q.choice key: "y", name: "Overwrite" do :ok end q.choice key: "n", name: "Skip", value: :no q.choice key: "a", name: "Overwrite all", value: :all q.choice key: "d", name: "Show diff", value: :diff q.choice key: "q", name: "Quit", value: :quit end ``` The first element in the array of choices or provided via `choice` DSL will be the default choice, you can change that by passing `default` option. ```ruby prompt.expand("Overwrite Gemfile?", choices) # => # Overwrite Gemfile? (enter "h" for help) [y,n,q,h] ``` Each time user types an option a hint will be displayed: ```ruby # Overwrite Gemfile? (enter "h" for help) [y,n,a,d,q,h] y # >> overwrite this file ``` If user types `h` and presses enter, an expanded view will be shown which further allows to refine the choice: ```ruby # Overwrite Gemfile? # y - overwrite this file # n - do not overwrite this file # q - quit; do not overwrite this file # h - print help # Choice [y]: ``` Run `examples/expand.rb` to see the prompt in action. #### 2.7.1 `:auto_hint` To show hint by default use `:auto_hint` option: ```ruby prompt.expand("Overwrite Gemfile?", choices, auto_hint: true) # => # Overwrite Gemfile? (enter "h" for help) [y,n,q,h] # >> overwrite this file ``` ### 2.8 collect In order to collect more than one answer use `collect` method. Using the `key` you can describe the answers key name. All the methods for asking user input such as `ask`, `mask`, `select` can be directly invoked on the key. The key composition is very flexible by allowing nested keys. If you want the value to be automatically converted to required type use [convert](#221-convert). For example to gather some contact information do: ```ruby prompt.collect do key(:name).ask("Name?") key(:age).ask("Age?", convert: :int) key(:address) do key(:street).ask("Street?", required: true) key(:city).ask("City?") key(:zip).ask("Zip?", validate: /\A\d{3}\Z/) end end # => # {:name => "Piotr", :age => 30, :address => {:street => "Street", :city => "City", :zip => "123"}} ``` In order to collect _mutliple values_ for a given key in a loop, chain `values` onto the `key` desired: ```ruby result = prompt.collect do key(:name).ask("Name?") key(:age).ask("Age?", convert: :int) while prompt.yes?("continue?") key(:addresses).values do key(:street).ask("Street?", required: true) key(:city).ask("City?") key(:zip).ask("Zip?", validate: /\A\d{3}\Z/) end end end # => # { # :name => "Piotr", # :age => 30, # :addresses => [ # {:street => "Street", :city => "City", :zip => "123"}, # {:street => "Street", :city => "City", :zip => "234"} # ] # } ``` ### 2.9 suggest To suggest possible matches for the user input use `suggest` method like so: ```ruby prompt.suggest("sta", ["stage", "stash", "commit", "branch"]) # => # Did you mean one of these? # stage # stash ``` To customize query text presented pass `:single_text` and `:plural_text` options to respectively change the message when one match is found or many. ```ruby possible = %w(status stage stash commit branch blame) prompt.suggest("b", possible, indent: 4, single_text: "Perhaps you meant?") # => # Perhaps you meant? # blame ``` ### 2.10 slider If you'd rather not display all possible values in a vertical list, you may consider using `slider`. The slider provides easy visual way of picking a value marked by `●` symbol. For integers, you can set `:min`(defaults to 0), `:max` and `:step`(defaults to 1) options to configure slider range: ```ruby prompt.slider("Volume", min: 0, max: 100, step: 5) # => # Volume ──────────●────────── 50 # (Use ←/→ arrow keys, press Enter to select) ``` For everything else, you can provide an array of your desired choices: ```ruby prompt.slider("Letter", ('a'..'z').to_a) # => # Letter ────────────●───────────── m # (Use ←/→ arrow keys, press Enter to select) ``` By default the slider is configured to pick middle of the range as a start value, you can change this by using the `:default` option: ```ruby prompt.slider("Volume", max: 100, step: 5, default: 75) # => # Volume ───────────────●────── 75 # (Use ←/→ arrow keys, press Enter to select) ``` You can also select the default value by name: ```ruby prompt.slider("Letter", ('a'..'z').to_a, default: 'q') # => # Letter ──────────────────●─────── q # (Use ←/→ arrow keys, press Enter to select) ``` You can also change the default slider formatting using the `:format`. The value must contain the `:slider` token to show current value and any `sprintf` compatible flag for number display, in our case `%d`: ```ruby prompt.slider("Volume", max: 100, step: 5, default: 75, format: "|:slider| %d%%") # => # Volume |───────────────●──────| 75% # (Use ←/→ arrow keys, press Enter to select) ``` You can also specify slider range with decimal numbers. For example, to have a step of `0.5` and display each value with a single decimal place use `%f` as format: ```ruby prompt.slider("Volume", max: 10, step: 0.5, default: 5, format: "|:slider| %.1f") # => # Volume |───────────────●──────| 7.5 # (Use ←/→ arrow keys, press Enter to select) ``` You can alternatively provide a proc/lambda to customize your formatting even further: ```ruby slider_format = -> (slider, value) { "|#{slider}| #{value.zero? ? "muted" : "%.1f"}" % value } prompt.slider("Volume", max: 10, step: 0.5, default: 0, format: slider_format) # => # Volume |●─────────────────────| muted # (Use ←/→ arrow keys, press Enter to select) ``` If you wish to change the slider handle and the slider range display use `:symbols` option: ```ruby prompt.slider("Volume", max: 100, step: 5, default: 75, symbols: {bullet: "x", line: "_"}) # => # Volume _______________x______ 75% # (Use ←/→ arrow keys, press Enter to select) ``` You can configure help message with `:help` and when to display with `:show_help` options. The help can be displayed on `start`, `never` or `always`: ```ruby prompt.slider("Volume", max: 10, default: 7, help: "(Move arrows left and right to set value)", show_help: :always) # => # Volume ───────────────●────── 7 # (Move arrows left and right to set value) ``` Slider can be configured through DSL as well: ```ruby prompt.slider("What size?") do |range| range.max 100 range.step 5 range.default 75 range.format "|:slider| %d%%" end # => # Volume |───────────────●──────| 75% # (Use ←/→ arrow keys, press Enter to select) ``` ```ruby prompt.slider("What letter?") do |range| range.choices ('a'..'z').to_a range.format "|:slider| %s" range.default 'q' end # => # What letter? |──────────────────●───────| q # (Use ←/→ arrow keys, press Enter to select) ``` ### 2.11 say To simply print message out to standard output use `say` like so: ```ruby prompt.say(...) ``` The `say` method also accepts option `:color` which supports all the colors provided by [pastel](https://github.com/piotrmurach/pastel#3-supported-colors) **TTY::Prompt** provides more specific versions of `say` method to better express intention behind the message such as `ok`, `warn` and `error`. #### 2.11.1 ok Print message(s) in green do: ```ruby prompt.ok(...) ``` #### 2.12.2 warn Print message(s) in yellow do: ```ruby prompt.warn(...) ``` #### 2.11.3 error Print message(s) in red do: ```ruby prompt.error(...) ``` #### 2.12 keyboard events All the prompt types, when a key is pressed, fire key press events. You can subscribe to listen to this events by calling `on` with type of event name. ```ruby prompt.on(:keypress) { |event| ... } ``` The event object is yielded to a block whenever particular event fires. The event has `key` and `value` methods. Further, the `key` responds to following messages: * `name` - the name of the event such as :up, :down, letter or digit * `meta` - true if event is non-standard key associated * `shift` - true if shift has been pressed with the key * `ctrl` - true if ctrl has been pressed with the key For example, to add vim like key navigation to `select` prompt one would do the following: ```ruby prompt.on(:keypress) do |event| if event.value == "j" prompt.trigger(:keydown) end if event.value == "k" prompt.trigger(:keyup) end end ``` You can subscribe to more than one event: ```ruby prompt.on(:keypress) { |key| ... } .on(:keydown) { |key| ... } ``` The available events are: * `:keypress` * `:keydown` * `:keyup` * `:keyleft` * `:keyright` * `:keynum` * `:keytab` * `:keyenter` * `:keyreturn` * `:keyspace` * `:keyescape` * `:keydelete` * `:keybackspace` ## 3 settings ### 3.1. `:symbols` Many prompts use symbols to display information. You can overwrite the default symbols for all the prompts using the `:symbols` key and hash of symbol names as value: ```ruby prompt = TTY::Prompt.new(symbols: {marker: ">"}) ``` The following symbols can be overwritten: | Symbols | Unicode | ASCII | | ----------- |:-------:|:-----:| | tick | `✓` | `√` | | cross | `✘` | `x` | | marker | `‣` | `>` | | dot | `•` | `.` | | bullet | `●` | `O` | | line | `─` | `-` | | radio_on | `⬢` | `(*)` | | radio_off | `⬡` | `( )` | | arrow_up | `↑` | `↑` | | arrow_down | `↓` | `↓` | | arrow_left | `←` | `←` | | arrow_right| `→` | `→` | ### 3.2 `:active_color` All prompt types support `:active_color` option. By default it's set to `:green` value. The `select`, `multi_select`, `enum_select` and `expand` prompts use the active color to highlight the currently selected choice. The answer provided by the user is also highlighted with the active color. This `:active_color` as value accepts either a color symbol or callable object. For example, to change all prompts active color to `:cyan` do: ```ruby prompt = TTY::Prompt.new(active_color: :cyan) ``` You could also use `pastel`: ```ruby notice = Pastel.new.cyan.on_blue.detach prompt = TTY::Prompt.new(active_color: notice) ```` Or use coloring of your own choice: ``` prompt = TTY::Prompt.new(active_color: ->(str) { my-color-gem(str) }) ``` This option can be applied either globally for all prompts or individually: ```ruby prompt.select("What size?", %w(Large Medium Small), active_color: :cyan) ``` Please [see pastel](https://github.com/piotrmurach/pastel#3-supported-colors) for all supported colors. ### 3.3 `:enable_color` If you wish to disable coloring for a prompt simply pass `:enable_color` option ```ruby prompt = TTY::Prompt.new(enable_color: false) ``` ### 3.4 `:help_color` The `:help_color` option is used to customize the display color for all the help text. By default it's set to `:bright_black` value. Prompts such as `select`, `multi_select`, `expand` support `:help_color`. This option can be applied either globally for all prompts or individually. The `:help_color` option as value accepts either a color symbol or callable object. For example, to change all prompts help color to `:cyan` do: ```ruby prompt = TTY::Prompt.new(help_color: :cyan) ``` You could also use `pastel`: ```ruby notice = Pastel.new.cyan.on_blue.detach prompt = TTY::Prompt.new(help_color: notice) ```` Or use coloring of your own choice: ```ruby prompt = TTY::Prompt.new(help_color: ->(str) { my-color-gem(str) }) ``` Or configure `:help_color` for an individual prompt: ```ruby prompt.select("What size?", %w(Large Medium Small), help_color: :cyan) ``` Please [see pastel](https://github.com/piotrmurach/pastel#3-supported-colors) for all supported colors. ### 3.5 `:interrupt` By default `InputInterrupt` error will be raised when the user hits the interrupt key(Control-C). However, you can customise this behaviour by passing the `:interrupt` option. The available options are: * `:signal` - sends interrupt signal * `:exit` - exits with status code * `:noop` - skips handler * custom proc For example, to send interrupt signal do: ```ruby prompt = TTY::Prompt.new(interrupt: :signal) ``` ### 3.6 `:prefix` You can prefix each question asked using the `:prefix` option. This option can be applied either globally for all prompts or individual for each one: ```ruby prompt = TTY::Prompt.new(prefix: "[?] ") ``` ### 3.7 `:quiet` Prompts such as `select`, `multi_select`, `expand`, `slider` support `:quiet` which is used to disable re-echoing of the question and answer after selection is done. This option can be applied either globally for all prompts or individually. ```ruby # global prompt = TTY::Prompt.new(quiet: true) # single prompt prompt.select("What is your favorite color?", %w(blue yellow orange)) ```` ### 3.8 `:track_history` The prompts that accept line input such as `multiline` or `ask` provide history buffer that tracks all the lines entered during `TTY::Prompt.new` interactions. The history buffer provides previous or next lines when user presses up/down arrows respectively. However, if you wish to disable this behaviour use `:track_history` option like so: ```ruby prompt = TTY::Prompt.new(track_history: false) ``` ## Contributing 1. Fork it ( https://github.com/piotrmurach/tty-prompt/fork ) 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create a new Pull Request This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct. ## Copyright Copyright (c) 2015 Piotr Murach. See LICENSE for further details. tty-prompt-0.23.1/Rakefile000066400000000000000000000002171403662044600154060ustar00rootroot00000000000000require "bundler/gem_tasks" FileList['tasks/**/*.rake'].each(&method(:import)) desc 'Run all specs' task ci: %w[ spec ] task default: :spec tty-prompt-0.23.1/appveyor.yml000066400000000000000000000013661403662044600163370ustar00rootroot00000000000000--- skip_commits: files: - "benchmarks/**" - "examples/**" - "*.md" install: - set PATH=C:\Ruby%ruby_version%\bin;%PATH% - gem install bundler -v '< 2.0' - bundle config mirror.https://rubygems.org http://rubygems.org - bundle install before_test: - ruby -v - gem -v - bundle -v build: off test_script: - bundle exec rake ci environment: matrix: - 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" - ruby_version: "25" - ruby_version: "25-x64" - ruby_version: "26" - ruby_version: "26-x64" tty-prompt-0.23.1/assets/000077500000000000000000000000001403662044600152435ustar00rootroot00000000000000tty-prompt-0.23.1/assets/demo.gif000066400000000000000000017003461403662044600166710ustar00rootroot00000000000000GIF89a21   #  9I  c     l(   y)$   ?x39#-Q3f.M)  G:( !g9"$(")7#$%((;-(=S**2*D,P-[0?U0C70O1!1+/12119G2d@6L94/9?V9[;?I;b=F?>;>B^IBkJD_EREgF81HDGKKUL@5NF>NcQN[NbONJPZfQhRSVSjYSkSpUWYU|dUeVVS\PD^YV_i`_b`m`reG:egmesjgVIhihi\Vi_`jZJj]KkOHkntltmtnkjnlnn~prmqurh_s~uruvzvv~~xzsl|zv~~}~61ǽ¾ƿúƻɻ! NETSCAPE2.0!,2H*\ȰÇ#JHŋ3jȱǏ ?b8"ɓ(S\ɲ˗0c48̛8sɳϟ/kJѣH*U(tӧPJJU`ӪXjʵī^ÊKOfӪ]˶WnʝKw,ںx۳) ,ȁ +^̸1-ѢˈO̙ ǠCmjꉿ'f@)E;,m5g ЂI=@KX/F&|vE 0(@@BV/c C|͋ 8Ѷ+6akn_ si]w}'ނ %T sL SA-`j6 i&rX ̀_:`-< 0 X $@n`XfyS(@3bA  llN>첣@aN*1O<ktb$3ʀ3.p.l#ϟ¬ Cl5  ;tMSn' #fUya @ >PA ى09t,@$`C /#"#3`#  3) S+2J0ӋH P, 3--*ebUNrw-t =C6Ak@8(P3ـp.L36۰C)@ #i${B `x D38q)3 HsBt q0@iT|g;r (13N6O0O*3t6,32C L2 `3"8s65%pCE= 3IP"V|1zܥb+Dv.K:;0Msd,p\g,AjG}ͰF<1) ‹A;|q|԰6&@721j5^h0`cGC1!&Rأ hF p[: d!IL!$XF<@U ƻpp A={Blh#HGpj}w @Cmp3É(8v!fCְf2AHhQ] ^`5U9` dA4Ʊ=f2'x@y 1FLƉ'Ll]@i@EF 24lj `L76o#P`^@ z@g0J.da :%y G>R:=I t;e鞩ozc?qH 3 4 åP.RYKβ^L@z4_2dΎy<3y1z8x>d~5A)4hѐt-J/Zіt1N˙ӞA-R˘ԦNujQV_ծ5 'Zֶup^k׾-bۆN6ifc Ύ-jζ\n7Y-rwN7VЭvGR-zY~g-;OxJx .W(u8.?&O)w.9c<6ys 9.F8ғ3=Nz.xS:ӭ^7=f?{Ӯh~;ܓ-v6=~{/X>h=2Dk@xJL؄PXR8VfFU\Xbd[Xqhl^ngdtHcsx؆z8hv؇|񇀸ey8'X-%h|w&Xmqm<؉e8nxb؊疊 8T&(U8xmXixȆf(8Ԩx8me8Xx蘎긎؎8Xx}؏9Yy ua ِɐEyY"ّIb#ّ*(y,5)7I ْ"=i)ɓ>9@Y.IBJII9YG; A+I\ٕ^]D!cIdI`gfIibl2omYxus{ry~|)gIzY٘}ٗiYy yiy"!,2'H*\ȰÇ#JHŋ3jȱǏ ?!ɓ(S\ɲ˗0c48̛8sɳϟ/kJѣH*U(tӧPJJU`ӪXjʵī^ÊKOfӪ]˶WnʝKw,ںxS͂ LpE'OlDžL|`6[ẻ CnrbL@꼜'tZL3A4(83@# LAP#ˤ50@3*, 4@f*tF,qJ'hFB3(P+P H M#pIĦ P*hA@ 9!dXP$M=(<3 #?ϾLIADC B pjGO 3 @ʼn31tR@B 2 %"O%: `y> @UxAvT;@#3BB!Qkek;Em $. 0`@9D# / 3w8H Ti~peC98O%+:g*6`3 h  T&H=$=#8> m7X9 PPpJԠI4Ti,a@ġ8LhG zZaD<͠sbV@0` TRu jM p0d$nT/83&̠G cxc-H@3$0Ja:Səf)T (4$4)iW H{(@.& T'CX,ŐĘhu8pvcg>*|`(1)8Cp'rH@q@(8$hyASD/k y͜cOJz `x#,.>8&N`z Js4b<ț Gb AQacDPP`L"bVS(ļG@Z&1˨h9VUIOU6&*(0ɔhE::#&:D# yc(`$tU "v4_Mewf@/ $L` Dc9q 6x@jH'ډ[CK\ˑuRAӆ)._tKevQC;\@i!ьrb/ۦ$Et$(6TIH0Έj<ܐdwcx=ab HI&{dN}|)[42le(sW2d1G63c5#v3|^9]1π6iMhQЈFѐl'}S^47ݞGsӢ4GQԂ15WUqq5gEm5wm]]3M=2 ;3Iq6MiS}Q6ks16q'17dm"q7u16a518^E;|!~#.S8ϻ7C&?rz~9ѝŚ9}9-F_7ғ~3}N.mS׾:֧?^_6.v#f?{Ӯv_;>˽ީC~g6>w? __񂇼s.y\˼N9)Go4CѓЦ?}Sz>x~=,ùgs>Ѿ}-|P×7m3OSֿ>}4sd>,s22e?<cX/v(b րx^5Yx\8d؁A c7_v|&(d뗂,Xgi24X=8g7$؃>b:4D(JFxTJ(E(؄}Pd"8FyVcU\+^&Bdh!fxj(l؆pr8\v\u愇|~{(NhUgE؈\划8XExK؉xsAxna5vc8Xx؋8XxwQVaHẌШ8xޘ帍؍ȍ与(HM%ZX!я'UY Y ُyi5 &yI+ɒ)/ِ$ &98)/ٓ-%yA@]LٔJWPR$dcTWa1QWS9%db Xfٕ`iYydɕSU9keٕpl9[|IxZY}Wi)jgٗ)i m9_niIi™ i ~9y陥iL!,2'H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0I͛8sɳϟ@ JϙF*]ʴӧBJիX'Jʵׯ`wbٳhJːpʝ딬At7]зÈ-pB0Đ#KN D N`A@ 9 $pӨt͝O`\XN6[uT F8Q 8"% |PAڞ{O|m+ĄlV\:A3?Buטx&XvzLPI6$B[B @ B( !B fP@cP @_#'68 '@$6+B`6L`$W B!ʼnhhS &HІ?@!d@вB!@,$@0!(tN>u`#>b >L@l238*P" ;a4&wnȶN C i#SH,6M7:B.L`sJ.(%|LK gT3p,~" >08L,("Ln>p/1p D&wU,,ɨ #P2m (tsE/\3VS -bH3 uO* P7pMӍ@q'4 TLO\p'>Xr+ɤflˌ7Q_N>P D N"c:0p?-/ 1,%% P%B bA#T;E`̨:y sI 5LFX?@Q|8k]$N-Ђ 2L"O<S 5;zLk8)m$EUt 3<X#ј@:Q1B5& ,( 6n+VvC͘@M`#j$ @A  @pF' LZD,n qO? 4723P C>\%` j\% '&%0y_'I` pgtA І}آ#1Rwhc0Z:AsH K E q?dm-PP"oc\%r l(EЇ*!T!\ T/8= B 2 ( r0V3Q"}чI<܃ @G0d`r5ansF$'tqazPF`1Sģh>P,Pt`A},CVD ;Q%h8{&t*茦#`OO yg W[捳9w,h9Ї1:җ3q:ԧmS|:ַs`:ǞVi6?q;5;);O1<8;M/ww7/n{^$.'Ot֓u`/PC{=-/Ꮮ/}3/S=Ѯ퓽;??ӯ~L?.:4s}7z!}GwV} 8wȀ yXwxׁx |7${ x"*X,؂(x0Hq&8lW6}'98p=xs7XkDv@xBXJxuI؄ȄP~R8WVHX\^bdXxhS׆n~qu<8Vvnx LJor'HgȈ?rO8VXpj(o oXzHx(H88x wm؋{8STɈqȸ-ӌθ2E7֘&ؘ ܈ E'H蘎옊 gXXss褏X!7Cyǒ ِ%Ie 9)UYtp"i!Y]w(i$.v0^ђ4N3ye:y6ٓȓ@I?9SFfIPRKYVyXZ\ٕ^`b9dYfyhjlcT kU9vWwrzO zI}iyi YiɘO)Y9ٙ)m ywy!,*'H*\ȰÇ#JHŋ3jȱǏ 3Iɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]r$ӧPJJӪXjʵׯ`ÊKٳhӪ]˶mXpKݻx˗" L1 ^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰i*무j뭸뮼+k&6F+Vkfv+k覫+k9]F!,1'!,1'!,1'!,p G'p D8i'N 9u4ŋ3jȱǏ CIɓ(S\ɲ c*؉.sɳϟ@ `A`tX'ѧPJJՓZ'+B- B9iճhӪ]6-(6Ka͉ LÈ ;Ang(XFSbN̹Ϡ[:;R놅 аc˞~S.O߇e!NLȉZ@w߾yoɳk]^Cဇw_Ͼ}v FF`wr38q@}XFH '1 Q x$("a0A!Ԡ! "4) +#-jFzCF)ErHWp#/^'є`_`%\ND~b&{f0' t@ |ҖaEzi衈&裐F*餔Vj,馜v駠*ꨤjꩨꪬ꫰x*무j뭸뮪:+"i&6F+Vkfv+k禫ٱ+k,l 7l)!,2'HP*\ȰÇ#JHŋ3jȱǏ CIɁ\ɲ˗0cʜIM*&͟@ Jѣ%sLPJJU-H @Օ@ځ8M:Kݻx/$ 'Zmc&^,:M˘: # Ex/Ys>|8u O=M66m-.6dN$Gn=~7!@. 7Ye^;A޷֞l ~]xB2eN˟O!L$g3 0@X0V+`A  R TpTpB X5L XP_YH P !B (`!`$FAAC|I6b}TVie]M?H701 08@!07A0h@GNcƜ@'؃O*1 *PHwyBd34L  '>H  ř蓅!P(t\qw宼z 3"PM1 T@LKÍ)dK05<3,q3*3, t/>4'I @/*Бd?Э*p@( ?*a4ԐA,P$c* ; 2B @2O(13@_@4Ld!l-n5^護Zl=_. H3Ɨk231L0O/)AP<i^b%&9𝌜 C#3,C ND"<ϨOAXdn @r +L4@`3A:Lր08(`Q}-N ү4̈́3Q7P#<Ҁ:r4ot[g&Hw2G#X9! AU}8i- ct $K ILp m82,$@x1` Bpk@.>F H!X XRc 1)@ `=&ЯG8߆q>/;p0;f ,GO$ -B0Kɨee.8`Jr(b60c#{(= D&%% R \ X<NOꡈ D*b( `z@,`LЇs=LB/CdL`2ГYt FݝA'FpãrM`(L4T\H(@#ȝc8> ګ@) ͪV `ͺf/8"$ Dj8##bAP !|% #x_@!Dc Se= ?)۪jWb:K`d H`.,2A YW !=3Yp;SZv!i T,Ar;J-9(M)@H[2d#z2 b il°@:[a G 0' plUI)~+8[F[Kt`x1d!r"l^gX$.{#:RN=[h skMb#Re2dmP (},t_Nf Aڪ妑xo2K0g!x>8+hˉZ:$ rB['qz bNf[ΎMj[ζn{MrNvMzη~NO;'N[ϸ7{ GN(OWn0gN8Ϲw@ЇNHOҗNԧN[XϺַ{`NhOpNxϻOO;񐏼'O[ϼ7{GOқOWֻgO=OO;7O[Ͼ{OOOϿ{8Xx ؀8Xx؁ "8$yf5('k-H/*35(q9ȃ=4(6H8C屄X0A`F]L~uLhg?KW(YJXMdXcX^`؅bmXkȂ\(og8;١DV}xlH|k'hg-!,1'!,p-X @ A(aN vB8A AM8Hq'N'RX#B/)bNHb|p@kq¤2&7B]2:NtYq` 4kQQ$c:q |DxaPSvK%A+ \L7!@޽/)f;` +@ <էr ёb `/J]N-tǯ]!}oΞW&xw@!,p "'(A[`C 8qiIt-HC -ZdQF&I '&X1 ' R%'H:YvPC |@%ԨT*Pߔ(!8nN0W5P rm$AYUvB?g9oߴ oG<7EhXK(D(p1<1,ĄxQ|޾|'Ɗ2O ۦNi2Z@8)bgn0 >T0_}N>O/LuѥɁ"ϨB{+t%;_I/' 7GaGQ@`ه' @xi!@!,1'!, D'Li T0 tp`I.6$p#E&q q I>N9R.cʜIJ 2qR !,` D'H`$EB b0H!,bĉӤN8r @dA'`X 'LdXҢJ 7z @b)̂)\r%ǏP} >yL*oB%Y #B剧<4Xo t pD bQ[NQ*A/&۸s'sA@! 6`s'PDsFF)`oBL8X"L7Jt55YR8MO 7;CO&(L?SXPB #,ts (<$zO I'8S Q@Y@3>3%0W?0d&?=t QAd 1Gbg&0 `ĄV;`@q$kU9U' 4'$0B7[ $D #4vKDYSy Дi$Vvi#P4B&Z!/0jP (L@ p& AC3dq "P[.60rlH T@gB@2 η)Av83Y =+*"u  Q`gK0 VB@gĞ do I .\$ Ukg2\ TZ9lAlS9_N;4 d3s̉; ItNlRIrN, 0ci0B@Q]Ja]ig MD00q(!,1'!, 'L@I8Mp#"lȐ!*bF)NTDI  \`)rˇj#NbN:0U"\g 3[NPN 04`HB-XœS Z(ʝ[ '¤Zm+\"&֯s OB[QZqԉ;M7aТW!鹪'Bp\C' @! FN +l d'N% a^@@'N>^ pMA@!,x5'HP*\Ȱaƒ#JHŋ/  3*8\q ,Ehʁ1+^KB , $sŠ`rimң ܿ 1ANBZutt=j*P ]q't:P@ 7Ar!BXA x 'NK5@B\  JLp7JB2&`b߂?MB V( 9A9QiNP'.CLA dt dL4ZA*4(PqP $Dn5OF@Шۙ3К ]t8i3B֜pTk0P‚ 3MDI 6 $`uI+LpDT} !őjlb g" a' ] IgFv8D~$gB[Q+Zv[2!,1'!,1'!,1'!,1'!,p='H@0ȰÇ "TŋbȱƎ !b$(H`qቆ#'p0S͛1#*0 0,MHBj頸tRE o  w06ìHVpBB@b*DLP( pAH(P@g L:!BXc!PΝXoBzq#/s#c%$!R 9A$0@ do TL @fppqqj3EH*,PT]>\ tAcʰ+yT@WP ԗ9v1)д& S (LzzЂTnX0b kq+MzC"eH/iCEԱd$+F ] MRamBs 32ӌ9cX@@!,'H*\ȰÇAH!P bȱǏ CIɓ(S\ɲˎ)NqB͗8sɳϟ@ mi17*]ʴӧPJ5XhҩXjʵג&+ HVx`]x~˷߿p6C= hͫ7˘3kf9& H8',hHdu7˞Mf^;Ֆ@hʖ"81OtaǶMJZp"O4 pb!#29yP@ehy @') b (`-8H @ q"F+<nx@A 4hPM!nDo46Bq,G Pa?i睚ARAHG> A< "HHF7@Hy ҉穨 وnA'T4pDHA!ЁCԑbyցK8r9f;Cri9Hi`*<+`p FvIT,(-w4(X졨A[ $$R!.acWaA,IaeetGDTVL~3(k#@K %@&P1`,pdt 鶮zcZCC`5} P]0%3 -_ t$j8T(*j X@x6p;/x>Ђ$x#`,ŔzrT"hp`hS1  FqZ[bQd8W@ g-@!oYY:(C] ~0 fPY] 8&F.tX @@U ~JL$Tdbop*X 1 xgxP P% jP " mgg hx7 QLaD0 Ijw  ˇ H(` m|~𐶧 {Xg0 Q v e@3ti|\0-06 p0 E{V" ~wK"'P~@ f 'H pPUaeБpP}  o 5[k؆-p( sܠ %/ X $ Ц m$ P vu8P, =0< ,@ + 3@r@p8eK%st0p~`X @pq4 PawP VpqUY7NTZb梲p @;t$` @0AANa& P` </A 8 SH80 C:@I9 0d =Vo%P 9 ,  L, khz zJQ˥e08  = mʅ350!P }JP0 +`@ C\@ 7 3 8 qt_5 !=T곩*Pm -`nz C>p ̗A1T$0 aGR: <G?P 0N1Y'Ӊw 7@e t"PƩZR )mH$rZ(p̀ pvH ?ܳg`  p]" /PuPʀ @ +0@ow`~0$XZ(U0 `AF Y wnj o1A 0 P j>X:m P&h4]`d0zbj1dK-0 0'qq%J\0 PP p 5 @t p` `0 Ѕ 80g` ~P ?"`{P0 1fP# PxtW}]P p0f%P 0 *LP l"0Vvv* m v$aA 0 @Tmڏz/` bkxLj“ !@i E~ePmL`Y5QImX## * 4Y 09C=$#pU6pԀ ڠ0ZbChgWPxhHa~Q800Pꢤ0 PŧЛ/ 8TyKJ|xSL˹0d;z ȬU|Q40g &i }:~ eЦ'0@* XI Pp p`eC˯/`{w"I j`f˾x7pZ -PPKEhz dG Y<^}K %`cGty?P\mZEq5U @p &cjn.6bG(P,aq\^0{>H#˱l҅kh P-@k=q1#005@0U0ȆN0*P`"Pl8 0 / [RGP37 v'P2ZKa3qH> 2p\ c?}I9wK'A_`jU~ awu9.0 Kd@`CzӝK>wwj}p  @% -M4 e ` Pŧ,~ ~yHa!8 ?LjՕ :`C <  pqd$Б^P p?5@ 5m Nmtyqe\@ t/JT0p\G\' ~T,] Ltf f˩Xaq@>^V 7`fȩd P =c;. |\ڢ]g0H@F hn4/Ԡb@@ۊP3uڦM Hܥtk. -/ӬK'TPT͜0 8H.4 p  Cpߍ}n=MCH'0|A ~\P 0`wo'C =~ pʹU 4@ v AJ ƺy$/B)0  z.KqqP 5|'P- 45Lt @a zG2Yp#py- tJ~^W ZM E PM <ڄ孂k}4@@ %08cN̰ , 0 G{۠]4@m CF w`!`3 0>˔ް`}1 *LE(LJ͊ 4 w8Hpp^)@pd:0AL PU,j҉HǕ,Z դ NȤТL %^z$*I&PhI+'yef ੥C 2"j \Bi"j-O N(D0gRFشYI$BU+E2VqUH;d4gҌjH I^ ѩV'FO`SNyg#0Tj8O hNjԀ2Ͽ,$4 e  8 Z!B# ATP)Ì(K,W8 Ff8RX/l* YJ=h8g0` @e{&}PAUܹD.XL|A5Prgk!1}AIWķ|zqpf`EΕj!&4 EYf9ڨVRfH%Wfep&e" ɫj:^0ℱ q%KWqH? ڽjMeZ/w:eO4h NMPiW=!QFAO=ثXY\1T넔 ^DPKalႺdb+|5gN ^s& `X3T })($׋(W>X AeZ| MC1@ XD,pw: N@\EJc@pȧ?@5ND` MUs۲ġVt|ʛ& 0Tk͈**kY:F-.';̓&< ֽ 0J;u94AKbT@L?u!1`BAk( 5@BPm8hCtF aE2x ,@ˤ20 BQ p( ZqU<*@ |~3Q2؂]*fxp?1r@3'k"ׅ->  ڔ6Eұ䐈:*QP<@83CX&^b.X^dwL \+4`،B?#D#Mea]%.Yĝ|P-)MpCmȈ@wx]!R4#ե,4DcL*0Epez=q!`DyjiQO~T}t&$B*.'3U2Q"V2PCŪSe43&maAvudAp +E,&<+"3I8J G6=VTup2XǂjࣗjPib|'l8S)C;Rdjul[-t^RbyXggQ-,v*HNfբʩv'M [Z = O‚aE&Ze($`MegRh2/|dWC`ðgHw YzS"pj_quEaY.6Ukd'[ ­fYnJ\Lɇ ;ֵk2rwoxt C+B & U`!- cuhlޔީƼ./pz Zc6os jc_Q(=:7b/Du`Ѐ@{cOhG/:R;Gi1@@s 05@ :0sxH+Ch#O0Ѐ:P;@97ȁ:hz3H)$$D4TţAؽ$S 1(M\ "4c7@07 HGX8x3\NO$U3o{$@ &`F  TPFh=>$8) MWF: :"PtgFM!E 0X>O9(1р:T?[7E-;o hx)XȔTɕ(4uȷ7sP!)ӹ9ؘ  @Q;\ $B\^H9bXPA/? 8`G 8apWyęF™@PfXiH z!!D c5ǠA8i18#5vՁLfN5p+:r/f#I*$ .0<$Ӌ&$ca9&]5CӋ !`!S^4k'y( }Idz(+99(K<\!cOz-5ښ<ȓz>DN@M7$,*PM728'6 Qq݆{0 784?L 3ȌN '$#:L X B(21;.;` ?PA4+,:,@ & %>|5Yh)0d@3 1 !MS qVy DӍ#;O" S cLy<4yA`(5˰92!3pN>pYɻ>'Q<:QF:% d0 P@@B`N{ #h`B:9PDD) NV ha@tN `o|! E#?&YQblMOKCF#D"GA<"X$h(q"-. OFZ\ec-2Qm-`F.CG.8xP 4у yDQ0Xqjot 4Fd *[bAHh밒G4`8$ :dըIa0?%0m9v-3FacؘAf l# pqG:Bd7ƶN sbG)TЂTE0ϕ)8.rZ/, ʒ8X wCV,ܡZb6f1`>@Vhcπ( Ntӝz.8 VnH+ Fil*P @hAJw nuR2qP *Geh6̀ ,r 8Vu\B{ ષqe9Qd{3X*-#bZ ,S U4,$*X&v,E?(Ra'1P{0j y0VC! 9pKY4G0(@w  Tu `?,cXjd@)|0dʾ7)G7`<s|ī Q=LmxA3d(sn wXBe(O16TZY 2eTn"3(  `!| 6>r j]=g ́dt7ڈ,$e@ 'Q"5rT(C &@-ČF v렢 $Ixi3xNNIjs{%kfqyP#7Kk/ݤ]-;K!,'( *\ȰÇ#J`B-bȱǏ CIɓ(SXʗ0Ztqf̛8sɳϟ5ړ%ѣH*]ͦPWҌJիXs81CAdz@W ^Ǫ]˶۴' v灲!8A@ݿ ^m'XXXv *0rk̹@| q9^Xa\MM&opARػjMb %I8 j@ \r@A8lb@2,\GLmwރF(aH!0o @hq@ i whV8!L##s$(!Ps(dԱd4@#\v).5ॎLFy   IoEd7zugzzN`L)D~uPG'XZv)Vt@mF  (XN  b+lR IP*AXB@`9jbq:{R @yMО*X 04r8rTpDR&k. 9N*  pkM Zx1d Uze"*4c: l+vjY\foK#â oyknC>d.(8fP6|+xID ]F wŢ:0D=8.G :xG:CUB$\[&%.eQ@@yO^@tBɛwer[”^ 0'.l ޥ;-Pk ,L[Z:O8fIJ :Ȫr_Y`-X B4j#H *P,e]DN C;1)[0曈pr ԯhV h  `D`@W.`x >Dfĥi$ $> =,}a B@Jnz9@ jn6 &@O!{BZ d ! ,@dYX^GSRNˁ(ctrI bOyd RL`r c,l\pҖ*~9@P7y`4Lã$рY dp+bq㱟 \pho(4,Ch7:o"[cCq(2,& YȂDSu""ו< 09 "/]X PQL M l^ XG [B0u2 u#T O_CB ԕ "⵶.\ sH0@\QehgK@ :@ BM*9yy{PZiĹ D}@ F,WM|Bсg Qw;@ WH3SA,++")u\34f*Y Pȩ,_OWE0S1}A 7L"4H d1iKܸ 'hA-78&x)f x dcpS ذ g!!fE8X!ZZbl@-xsrC! &Ba5 t -[˘L7B">H8?i>Áڊd8bbuɊM@DM] <5j#ByB]1C MB͈v=BA?F4D^$ +@: ]vb4 `Om'U ?, ?@ cUX cj7a]W~X-`m QiXuoW)bX4Y& 3A%M{g%.'$`t "@z*24J*7qAR6pR!$° J Z MC eKV0bp #S$M0%l !z'wJJao0,h^y]0d73ICFD>uB!=p]x6m`o (X,4<`@QlO"v j jx`ba1t6 `~d!0 eC0 h O\J4YqHpMH^1 Q0$`3k-PWP P F@"3 X%/A3 A0QSP)6SxQ00 Fzu)7PbsMD!p&ҷn: 6y`U3 6 D51"P(XSR7DmV YM0e- 7nEy2 K`OYjQ/[-g1SĔF j@z: ( 2Ub 8 H >xD"i  mx0@Q}u?(Q4m*Є @)t ! bB'`j@ hE00gQ SG&.7pBD Bk/VQ? %aas0b)P *tPC ` Rp0ڀ34*b>001A4TuS1b F7FP`e[DDuS2ř`XM"w e!&6 ) rg,`;U/n  S %S+$ (\VU6Q*0]%_5,!RG0 lSAKaMH$}4KbXd5 %Y ͐  (cA9f:2p jv>S  OPi U:Ӫm$pO\'F2 ~Q$顿m 0~?1DnQ03X 1"S8}bazp-@^: w^Gdy]w^I qJ]2,9Qk ! &w~{앿_;T@(| LnhcC8P ,@jP8dFY吷li 0Ațx#`p0Nr`9ŰO .p F$`%MDJ|o( 0"cFqhd2/PՖ.iKYH6V_6f A a65x ^1 y \M IOocm;+S I }$&|'oOi!gJ"|&M)c0 qt!Gh'hk"@dMw1* w?-ǦdЪj).p,P^vA+.dP;3}I 3t_3]p[m65ks1OW m{pPN+%JU#tX֑Fqlk3Lӑ.e_m UANr),& Z?=g_b+QTtF 1b"3R RQ}+@''~y!v¡ |E ;'p;t˜l$In9JVHS1C.Ur1ki!:dJ٭AD a.{<1<(C-j36&.<07D{|5%GC5D-Z-xwYAtk88"D.JVMK1|@t8>\!`N^ܢ= m>`NV'!.+чݸ.95::a.:QYW*Kn{=鎴 NhF`#T^@JW1O>;ba~^[gEeFktCA> haZF4TjA/!"{-Z34~P.>B98zWqZ-soޢs6;9GX.d;C.g%I98?aViQ!~..F/9 a7mCu/MX]{OLU/(M֑06*`9e!h $@"_P!dCn-.?ʿ=&`/!_/[:R˿5G9s :f -`'pw RVݟaؘpCT"CLppƒu&@ xāF@DRJ-]SL5męSN=}J@)l!BESJD!IS Ι V]~VXe&H锤LɠFzf P J;ߑ`&"8bƍ?Ydd;m807nT84A '|!]HTin޽}flHLT#L?:& &e u1ApI 7G^z>4`hz`tOJ ܸ&خ=dA۫jb @BI&o` '9q%Kɹ $|0FgƝ"$BVHdP#1< l2K-QH@QH:xC@Lahk Z@f B KEeȼԠ: 0pʂ< C(0NhG_5VY;jWg5W]wW_6Xa%XcE6Ye> XPզ De7\U'ՁZ 67^yPHB^&`HW Ķ g&y&!1 9dpCϔd Bu"nt}&d e9g%9+* +XP }aqw]j@ ΘH7tp:fE:u&`x RC~A d0^ fKĪIww=V@Dj$_e`Q$i @ tS Gi.QCuoQֶ*+A@0PvM%H[\ɘMZ=?;غ`@Q/ɌY@Q!-2e 'J^jj=; nJM HGN>~*%R`Э S!Ƒ X@s@۳>{$>ȿ#l1Β @|<d?:|?@ܩ"{ ę pAl.6# n=X`A*^HYP>kP 8'<((Ʌ>j|Rp'$ã:Q" #) s 9:$ 1l"e# gX7 ṎSe9aQ GySǚo)hV_X"\|3 y eP&|gПyb*q1ERH v a|ƉX ]xoN!Jl';Y$ϣljXe ȁ8-~RnYDLA dzc % es൹1e/Z 9ёS%UėhҲ P'&SsG1yHi~誕5 թ)&i! &FX~Z .ZD3,@cG( h ";ЗD':2E < rpdR $`^^SQ='iY :8~B4^ !6xH'Y`q'x%N& *,YnB ":ֲVLm'%k)&z^ p Rb(@?f"´B0TlNC`4F1 GduzA=  6hҪ@tBOwAB(єߍ`'@(НV[+sqo R e'tGr=vmmLIGx7UQ¡@(̼w8E,gdqEŐ i?drg' $ش$y5ЄMF: u QB^9B2>{AY|+ANyh2po8Y ?%c*hFu#9FwQp'k<ʌ˰HLbP`n{9 b(UB圀EԐT`^ ԓ ] m&~٘@҂(&h6fZ[DH\} ND  ,K!0Z_Hx*ZQC4mCRPQ&$"x 1 Th%!1mY/a48t-؍t.J8AfDPxYy8A |ݩD84!B >"=f`cŅ$A& !.&{!+:\|A^TN8NRjGRE (xI뱠!"j:s"5(D){ Z 9ֲ D:0xHJ@0.:EF)~5X ˡh'BS L` -PB0ÐmЀq p q |"{"jue2w j0p~"1 -Pz1+n<p6rP@P 8n~pY_;f@+5N[>MF6a4rfpB "d'> U+4o80eo1 $c0x p0 "xU[!F2H#Aut]"Gr? 5xx`cTw pr pa7| mgm)VbهzZ2*u3 y&!> A `G-l,p0T`/1>D* LB0EuP'@+{ =a!D1"8( H';$p&P#AhQ+f$pq3"%Yb]5SB3bioXkWOerFPo@-?q/]X2z-)p S@ A,_pI\%+i' w'0ToA33,-6aR-L':F676F#vݣ/ҳ0_* 4 'cA/җ9@Z)R}KAHjN"QTLjAX "v! &`R*fz% @h2`=!  r2ι 7 # j@_J$G:vf֪jaګjz牬!v Ԭ:"p!9ت ڭ- u: z r |u+zߡ׊Zt  BWv1:C`B n#~x(1'rR 0M Ez ?[m$Fq(ܪN#R;VkX 2"VWz҆Bj'q05D*P 7 ా DT}v)I * :a 1n OL ;5<w6p@Vpi$J+m/p'1<A`^O ~%~b7 |ϻ; <O4 ͌:oY,W'@wQ ^[lY<9C3qLh,̐mf\g$ ;+R/455#lY 0a`B830fS]\;:.vP ڼia#кG0Ҝ\ySM>/4, a`0 !vE1I<Jj4F,s~m)uE/ȰY+ `pY']˜k݂X=n͢L qSQ*Y=ԭ&]]̍.1k  R: aۘhMP Fk  ϛFP\cA='o(9a  >o0+;:=E+ ]#:Љ  GDf ;<7>TRG+opK_}FAq.(:$`pd#Z"#0::"qjb*nqs"uwh*'}RLk ct#b1. !Ѥqa֧1J>+qDA0vB0V6˰߱>~)~$>'& 2Wq8%UKa{.aB*$BtdOpðޫQ<x\ܮ>+ډ0.0+Qͫ:% 39+ \ qqd 1 Z0e` ]zi0 0|T-DQ><ϫ 0{C=2%ZMrMɾ]fOe:&!,.(:I'Ha*\ȰÇ#Jxfŋ aqCBȓ(SF@ ;ABpLƲ5._zM`Ƙ3k6vqK7ׄ;tiO/.Bkpn RS P@O`9}@}胕NUGZ& @̈-o'Av ſ7ϿI7\+L`@`M F vTg+І4#Z(FrGz4a@ qA L$&V !1Pj108\8{v!0AYl@33^PЈ@9 '*P0A ˖$2''@@LlPrf3 43Y3kA_h@ș1 < c@A@r 4ƶ{4ekN3VZg1*ZA0в4l䯔H 9Anx1P+A LD̂R!Sm]{Ě&@@@Fܼ`8R8@) u촫T+<WN S>lB/9P;oL@FpJрs_>%#{M~Z |<& 0P* 34Cjˢ)B{3 yx{H<@qb@_x Dl:E|~@l{xfQGt08PB@ozP uͺALO0!`ɫoeM' D:uzPm ua9 T _9G|'LdP MEHQ@~8MI 4'6V4NN{j"g$Qf&a ELEb(~p-1^3hA0)5(*c%Ј $4YX%dA`&m`Bx%*>)/̉%|%z!Nڴ,G!c M7j\X$! AhT:)YeB)"V3&e3ߤe%`H 1'Le 4|<͊P "LdIZf2J}U4^ 3|?IѢԓ",H>iPk@˄KB7&>t*Jӣ?- pIk==PMICPP<  2 0i hb4M 85Toc1R)pc8D|x]@ĭ: L_3&k v.< `/"vJ;b)T~ 1!N^ y >8΋*hAFr `CL^U Q~r&8Xg*2i{'@0joZ HAY P= @󙏑r҉˘& Lؑ˾s}d/v`]*4(iw W4RC\GR@DŽh'MΊإfO(jr xېR0pWpkv9*v$,ulH'5Q;_ rcBd.wL*N]sZ O;ݽHT/H笅PP3*pr.o2!g}*- (:O*t4 K\f;p vfw $,aOTPz+&=NuPP{ح:?-!9g1.K Z;0JɈ Weq S&0dH@X@1!#ߥr;:E VdҌs@G=WL f-`4.m#XIrgIwW P;CJS +A #j6;] +n1^>Vz7, nW$g/(KTrrs@uC7n1?!1`r50C" frA( C7Nz&vp`Zvb2%8:p^’2SD_aZFgwQ  pe xP ކ# 0p y$%(!V85 ~`r<ȍO+"~a,`Q_'X)2@@`)d(RS &7f 0o S0 q@"xr (oCPQx" pL68 t"( @D >PP 5p v # %h9{rcVQP+et P5PEQ*4#x8%b-/A@! 5|U4zh`EJ6P Z_ 58' +x0@0t+R *\aIQ YL0pBÒHa 0A Ulb 20z3 <$P)F6x%x )YI +"*p@ 0A%fdI6(p$DA88 P@(80r5gY7!; ;=!И@wlp8#F q1 )7V!= W8iT"V % AI%p_@ "(Od''(+W`@ 019 nb a*gzp2bP'V%;) Kކ[ 0 Unp^ۘUX1`  g1; 0Sp|-5* H"ژLZCoG 0+4{4>9;;[71(;s?D 0 +"P F) )0zQ W8p%UpБwR e@5-@3G5 f6(FG r( Pm+6&31,0zBfUmEppM qcZ$P{f" bs~-b~*a9wT{"ϣcQYtV 20 =/̐ s0*s%p'x` F$0X ").@}!jc, J;"IrYaLd(Z,sܶ2!Fn 0fE g J' x )t9F!+F9jØUp"̛0t^c[ f౥M %ݜdy=MкrfBqR[czqr GИ۝sa^1V$N? T7&:+G -g;FM$dn^qA)xa0@rp+g8Sp9.$! h8+SW$#`U#prf@H1Ӗ. SFCޮ` $ywa`}D`U^дg VDipQOq+Ρa.X@ :_2V{(P UTr @ڴ A `V9n`4YN[ -x 7Lō 3%%?🥐'q'KWWX1 76%cXzb>m!%3Q#0LsUxPlxx'@-0C Dj P$ `dY$ @ p9P` Pq^ ۢĄ &\0=|1|2az&T2A Mv`Bb ]N% 9={Z`HSNx~0^咄KXN5KքQ3()@-^-r ( f F2)E8s >BZ&8Tj9nܚ*HL- f&{& 2ʸ*(dQ1 Y+EP*Y a6@Mtп0Q&`£9,` JBo xHJH! *S5j@a)& ( Xز))Gi8DB M*iL Jh4Z`ZA-8Pl H0 >pNp'(Q풲:JLXVth9EV/!CC XDMdFm2X$Wa ǭ A TbJK:Gr' ] f@X61&@`о6+7"s4}iJ_7$dΙʛ2!XrgXJNap7_wX 9L`R |AQ>KЃ(%^tNMmlwp7kN`eg)Д}B{a'5p78Ǟ6Khn!qY͐:2" :9εu/W3y?1%@Fv@ {F}R3| ;=3ݾ 5˵i@oP5`mT:j!* `HI}|U!S@[øo 0ɺP'BF3!pPz7c l\^>~C+RH-ͱ$/~G%ϿíoLf|*X Z $i`˿@H?+xyX Ơ)aqPf0 d (K8%KQ yɗs"4I0Zv?Ȇ {xp D$#/\/쉣Q ` (qnpLȅPXB0?L.1| 2 p(q nX d0n:+XA@OD-AD#(y<k`5`Pa. P @e`>_ EXS. 0y( 9drikx`pH_`$؀S u]h]<FqQTahu\xj`bnGjX]Z=Bȍ (`  p}I^4Ş}f$G_ȝGQ4svSЇpkXF' Ȇ}Ć ({ l^ɯHQCN1NEKK9$ ۱/0˻4#,̾-KŴ̓äLL̑!%AbC<,R(" ׸$M_(\*dجT9MM0,L)Ĭ@^&$JGɠX !@$&\@xȇx(v rR|> wX Лcԁ?8F.40C@0 Yt < u,3 Pɞ0xAe> -Ćȅund4Wb8? J-@E G!`t) <` !JwAN*ESX6OP? HĨ'Ax@ɶ9XEX4%TX+xwJ(]y g'OaH\`BUoQHX E .Ch ]5u &HOg@$QD%rVhV3E*nmt}ARSb=J VtU{l4W&җzU|%|Øzr= :!`eI* PFZ #@f*hȫZ?9Q  ⠟0i$)؛˔yȇ|Bq:HE!) q0"2L! pxɩ!M!S X}Yٳ>Z Ex^64 PxxzrGdR()؈z'd٦ZZQ.*  B{MXUp&$`yxu bmI|c LeƄ7WhoЕ}  t`^Bap؊s+\܊c!`߰d` }@O%vIwJ [0EhFЗQGꀒ[tdlNUMW_j3);%8F$X`hdj,_yȅ@! PKKg*ֈ @n`Hb~@M .X C ' p[D-Ձx(d) f yu b0E@ Peq^2:f(j8|~Pbepb_Ё3S\u0n,j- ,a&E:Μvlx5b 0_{D x-d( YmQ,]8HX)PɸPd-u S`}$)(89I$G@fX@}p5Xa[  fhKCfH,9.*jkXt~ `, } J Ї!9eI܂VW O?8m`(^( p]n臐Xk8E=E[)Q' `J hvSDIi`m8`H3P֠JCcj0К⏖EKO| xy` :хlր }g*s=AF2/m(DqщC/zЌjt(@? R~t,IҔs,uK_ΘԜ48ootO ԠV&<*Rԥ2N}*T*թRV*Vխr^*X*V$ !, x'H*\ȰaC @Ë3jȱǏ CIɓ(SdQbE+cʜI͛8sܙR@ JѣHLʴӧPJU) 0jʵׯ81L ٳhӪ]˶۷pKb@ݻx"߿?uÈ+U.Le=80ˠC񄟣S^װc˞M۸sH NBƓ+MqУKrwc]sl>@AD?P})OYח`T@|H_V"zg$Pȓz1ȟZ$dxj* P'PV 8VBH@hAd8QPdj0YXbVx6_A+Fx@5M9T}GJA!Ș,LpA2l}2.%S 2lȜT"hU: t(4 93 X: @(Cg$z**hq cI Oy$SY@" AԀrC8(`¿@LIJZ̤&7Nz (GIRL*WV򕰌,gIZ̥.wˆԱ 0IbL2f:Ќ4IjZ̦6nz 8IrL:9Y@LBzJW!Sׂ@@`;8AAc#gJ&N,T~!Ѐ7Ú4NA5z+]xNQcL& h I9B>dS &$HH Fm)O 4fDR5 # f`` (|ph}k 9>gP9xU{+`Ǹv$b2} ev0 !,1'!,1'!,1'!,1'!,1'!,1'!,1'!,1'!,1'!,1'!,1'!,1'!,1'!, x'H*\ȰaC @Ë3jȱǏ CIɓ(SdQbE+cʜI͛8sܙB@ JѣHLʴӧPJU* `Nʵׯ`q5ٳhӪ]˶۷p<0VݻxuDV߿?$È+Q0,LA4x0 ˠC 񄟣S^vװc˞M۸s NƓ+MwУKkUVν;Ëz*'H@ ( „ [ `X@o6'1x u dcUX A 8|8WP]5" FE@@L1 x;"֥`hQ`) b9>5<g @P"+ s-+k0, N YRX(i2L'Y4 }PEa 6[O_3 aMApCf}w[r6 ,ޤ`V@{ Q B$u./ J{uY` k"eE 'Dfk$F>|:Qa̖&$ *a6|O34G:;Hi(FgH'6@`vb(|u)9K*EC9HFTN[eGK@Ѓ w% ih"r%8 HiR a $"hB}h. l \QiJ*E/1-CF1ym|5( D94R$9Ai!q<Q$#HNz (GIRL*WV򕰌,gIZ̥.w^ 0Ib.DqL2f:Ќ4IjZ̦6nz 8IrL:v28CcVLF0NL*x8њcdpHʑ*s@LC A hF yb8`cOt ĩ AQ 8̘@jpUXb`IJAhKIY5xMK@X:I *h(]!ȽqFdUv% {X\dO)ɖ%f3g)!, x'H*\ȰaC @Ë3jȱǏ CIɓ(SdQbE+cʜI͛8sܙR@ JѣHLʴӧPJU) 0jʵׯ81L ٳhӪ]˶۷pKb@ݻx"߿?uÈ+U.Le=80ˠC񄟣S^װc˞M۸sH NBƓ+MqУKrwc]sl>@AD?P})OYח`T@|H_V"zg$Pȓz1ȟZ$dxj* P'PV 8VBH@hAd8QPdj0YXbVx6_A+Fx@5M9T}GJA!Ș,LpA2l}2.%S 2lȜT"hU: t(4 93 X: @(Cg$z**hq cI Oy$SY@" AԀrC8(`¿@LIJZ̤&7Nz (GIRL*WV򕰌,gIZ̥.wˆԱ 0IbL2f:Ќ4IjZ̦6nz 8IrL:9Y@LBzJW!Sׂ@@`;8AAc#gJ&N,T~!Ѐ7Ú4NA5z+]xNQcL& h I9B>dS &$HH Fm)O 4fDR5 # f`` (|ph}k 9>gP9xU{+`Ǹv$b2} ev0 !,1'!,1'!, o'H*\ȰÁ% Ș3kޘŗUR ͨS-m㕣Y<ܛ&w 뛸ݱ7N{og7`>ww̕ k<}޽z{/-ut'_y  ^~ _y0q` 2_BQg^݇t vw^c%8y>:t7fduXz:>c{xn9~})+Ũo qv6 mZq<%+r&2UJ3yntŝfiF+Bå4cSmWZ@Oi~N6r K]3 x [[0}{EbnSM7mJ>(ê8N|w NV:Pl3?{ޒe)N~j|^zƲWVYp9zR 5,ocu<{lF|W :G*MnBCZC}OQ{Z= g"" X Ap P@|#ߖ"'0 mhdœ}GA0T{at>:-a[DE0mLUW(&VjLS%!E@ v81p+X CB'bp|2ˆTEX ͗&+CRw]!Љ9ܨ-D+KQh@1 0p`(A>X-R (`'@#6 T8YO!)-J+'K P@ (pL 8bzR+A})0˃p<:b#2I9 BG< 'ZDp b>O- mIKd`x1r8a/ ` p!c*0UX X!TS8P2'pPdc %D6쑏xC, HaNC8X` $0%dc6Q``SMbF yPC#8l1 aC:E*qkl6j1bC@+a3A,!NbK\>P e@Qv#p F0P F ^@S3ЁS0ІD`tthAv4.qt wD5 @1 ]hQIP@00^ qP `P@F`bQ5@ Ykh@ D=BX|`9A(00B`F3ꑎ\Ĥ}jbH9q.b83@;2pzp pd ?X@+0|868&d 5(&Qb4Ѐ#7hh2P v@iPk@.d` \҉v1   q: @ kT έ70\ F$6t# 1 G7cC.|7[vQs0&BHňtP@J0=< [0Ob _; 3 4 }y PDOڌ q &6}S?c`&lAdS?f x%~Fq B'~`]Mw'~wKS6 pQQAB o$?Ł4jxK  ` [%(( `u @gK8h58x1Jd$9SR^ R!87ʇ7O7~T8y0 wD@j@f !8 tp0  RHgPc{G@a{Tt@F `@M0 Msr@^hA،o@u` LFP s0u@i8(F|ntqHK^0Q_KPF@j _Qwr"y }_@ 1'0_uEFi`lpl8#ٔdq`q/ D$0p+`n[9 !P}Lz-@jt9YcP(E ry0Ԗ`~{]р6gsq uPp@i^ ȌٜV}i" h p$ $y$y[m$0 83Pp@QVb_Abp…(K(WBT}u8x.:x}:JFuX~w!&tB/cL|*…$DdEi|BZV~,@7aJKgd0wр'dFaz@Zr:PP~( Хt~Os:NQ 0euzz~|*Mֵ~QZL!mA H  ĩ:pTx"ЪHP ګR:jX@Tz̺HN:K0ԚOAQBZHh꺮ڮZxH:$Eگ;;Z;0Y 00 0B0/ [>N۱ #{Nxʲ02;4[6{V <۳>/ @k!,o'H*\ȰBJHŋBȱǏ ɓ(Lɲ˗0cʜI͛8sɳϟ@ Jѣ)P@ҦPJJf LUCe&2':3%"QpfQ`}DPMbl6J䅽S@7<(Y8)8P π2<'0@ j\/.3Mוg!,1'!,@95'L@'PP!*`*T@' 0‚" \p$Pń T8 & pJ ˁ0Л+@XEEI1_$x(TX뉧EUS1a)Cb81`'V0X`J|i |^x R Lb+ $8Da̩y%D  8`fm bQa@b=(2N _`K`+ AD=KJ"1)SlrYV"S*`+sHtTmARZS#j8K!ֲ"DO(Y IN`kU<"Y"!.T@̺^ @9h\!D pd%A!A *{Ҍ bEjX!,2'HP*\ȰÇ#JLH"3jȱǏ CIɓ'\2ŋ-cʜI͛8sPA@ JѣHӧPJJ5! '0L:T*]˶۷[ha'Dh֬޿ ց0@,((v0:LśHblS{d 0.ͺ)x~I {Q(f&@N>@M'`*Dj F-%xoӫWx_Q{ЄNAIF߀@؁|V A 4~ ҀA0)tB("k ]@Pp P^Yp +7Ȑ'B~ PF2 4? a@+UEԙ4k4?"P",oS ^&72 d.*-P9ɕ0LN7:/`%;: lE­-tB9A|=~.xH.n BPB (2h@TĔ (7k&T( ]&괃VzW[)vBi9 @>4of. k{gwσ9W<6=ճh_y{<@ Ж]JMPVJEœ^B @Y0M#8{LCX@( `c(L+ ؁@ @ `}x oHGXfE DYASl` '4e SH `lpd@ql (b,2! oQǮM0$q C@ pL@ $Wh@_@2n <a q1Xz3=a9&cN  ,`qYJ 7dhB9a c8lA͂&%40+Lnl<1UWJoP`nYȅ82zcHA Jo H `L  0tL@,83H&`5V #NA~WӲ:54b Ѕ >$ (2!MUF@ JF:rjUˬ SҺQ"@ށuN B<'p1ɥQ @Gjw(LO޺evMU|WC ~c "}(>G˱>ַo.Peg &PhE-ގX P N : `S``A.3MR`Y .Y@2PcY@*3U " @ :,>LQyoLn.K/D5ps+χ&E:' |.d'{Y*Oh嗼#67%pl f"Kf\SvNV"( z$H@&hA` ֛Li:Y|\L@4*К+j7U@x\a":PftCZcEI:{ʣ A^Z "#k+p$&aHyFoa밄֩3a8TE*Kq q<~Ϥ[9+nx7PpH,X@:GpLPn{|%/)ԬV cF;MLH7!g܃帱?s<(@Dp`#06it{-5&@,Cb@'¨ Ԡ[ꈁ@~1opöUAU1[;G>4,YրS@ްbkԩ캥 .@^6B%S1݃h=Ikp VƼ#b,`أC4|:`QkS'Pp ;I>u7E!0Q$;ZDB"W1,\QGԁpFZA"#8/!Oe-;cy4-jejʣg?w1 .rL0VCIK@o&P<%VxF:!̤K&KP&хVdѕOv'A+BCo+A0 a |uE"u\>K[-! /TwH xy0*[rr*!2wl$`&W=H#%^0_#qahU0h1-?$ y! _ a0bG>{33 #ua$+n(bhss!' 1* ِ8;ATR' ",)P P  :HW> u`rp$<8A>{ &U %Z"ؓUw(WaD"aqW2ba@j=g&-A?|q 4 &I6+iܓ=Qyoc~Sgc{-wÐ:1 [1+jQc(,0b]c151<52YV,LųӀX*A<+vKsC0@pb`YSdc(!; F@@WQZ! `hawׇj@$59s- 0|v4 8b`*I%P^(p`8Ee$dS|  H  Z" Qs (d:[) 7($f 3RPМ2Օr V!G;`jP&\:QJpsTh 7+ 5 0 U" 0@0p'YZ5/P{[A 0 e 20Qj 6dZC@ݣqWYZtU:a6  ;2-X'N@B-' . +`%eX1N݃Jr#!0p] > Fe[HcПt); >S013LSH-5Q'p8%;PK|J벘cny!0 -!b!3@.!(3(ZAHme&HF V#b>&Pn7"M(-SAL!Aø.?p! 8l[X>Q>\&@f3t;~j@KS ^!+ekc Ӊ;&vA{/ԚDXM"Oah<6:D[1CeAbhMek6 %vFUu )PSr 1-f  *0 7P8p]sNv:)Q:6L7*02<4\6|8:<>@B2:-Z0(9Ľ ӡyJx3Nd?%-Qܶ,Cb-سuǛ|Ab.Su\yӿ<ރa!'sVf^t6˴le[ː˼\V4\P\Ȝʼ<\|؜ڼ<\|<\|=]} =]} "=$=CI\B!,Px0'HvԂ#JHŋjp Ə CIC#S\I,cʜY% @LHO.q@ϣH#j`By&t)W͛կ .Zp yK$$Xq B,Bر @CX.@Q>{"m{pc7> ``]`6YB(qdKOLM k`˳C@ЦM ɕdޜ` BX% Bv\7,08 * `z4@%P_Q@,߄ 3 0Њ<`B 2sfLLpL8C ; !pFXY`qF854<;dLJFT M$1N7@9LA1 m +0ig.`38(5G< !CI  ք%(D2 "P8dQ- ,^-p@R5!`*z,p Y` @ UNT`'k唓@+@Ny-Z LN/G;fv^G.|P(B!PI<"ī0 t+@,1Ld !LUDtB "@ MNhrZ.  ^ \ .%v EFqUCf3e~%Ѷ /زE@70'8p\%$Ɗ ,pl- A4uKҊ-%B_*dvwSDuvL RxA AH˾@-k_MASДR8eq@ހp@_$C 4Ha{hE/l! l raR4 +]$A$ZQ9U /71 4FxTs B9A$lыML"BpH8QA14=,@G#b 3x1&L_&qx1*FPAPOP! EpP@P<b0AhhchB#?`aDL$-\ŎЊX&!@8` ^:Bjb-#0u @$M@rP;09`)!I1@8%0Ms$' lS |p4࠺  t h"Q@7:(m@Bs`VhHsL0xg9"!>Ԑr 1h0Ǫ c!PEtL'`@#0LMTC!x Gh:$ AS<`V(,@xJ۾U!jաcC '<$,(>p4@ U@"J`~PqT1LϹ2Qψo h@:G်6 Z`}|Pc (!8Az@:  d:p"`^p M<(XYG>kFP*<s(T1j@x$ͻ $@ !P_S&%`YЁ&s5#czyB \ݯ5T&cX!AL!uqXY;AB,ڞeOGcJ׽NPu lPx r<|0Dk^#8hAH+q ,l$E=Y=(S|1n 7R>bAP  N~}cC!, xx'HP*\ȰÇ#:!ŋ3jȱǂ&#ɓ):7ʗ07Džɳg s]D=!"J5y@Z& դ,B(PJc)eӪ]V pʝKݻx˷oñ@/\N@a?:nj+!/Hg =gͣK8bKNtwrzPWX**{ B'~(76WH ؗ P26L6B d0 @O ?~}Ȁ@6Pa t@w'^8 pAs6 )-> Acu`@"@ӰN"PH.@X3 AMfJ!o'E!Fl 1 le# Dq Rh0 @9hs HqЈ xA ":B@@DP 1A,A4ϵE A H~s\ ,:H Є(ȅЈw-(&ЁE-@ hU$a `p#`KCֶ׀Daw b_{ _h0 ${FxYx! ^İ-<zbс@u~<@xp r {ҁn oV\Ÿ Hd_!0:9ᒁ@P/ԁ\b 8P(.WM"qP&K!h[VĔ& M g{Nma.r B H  p Aa+dZL1ػVvOs@`* )HU ":hB|rnu { ub~CTB[ ``#T14D>Zc &u'0& !b=(Z yO@ w33B01X vEjv T q_ٕtKpϣis#sv`_/EW+0HCCQWU&3'he$Dqa0xKoa4[4rsE H!vF7e3WbA;EV40``]LegJx ]_}GTsY,pMUTAUeh0LbbD)e1t*v~K qFL$^G2CE(8ыxkzVr8cF2ac(38G0A0 TT\ 5H\'cb(X+uQ/ /`\$[1a_ A!, x'HP @\ȰÇ#JH!Š3jȱ0`.vIh4ɲeGuz,cJ8sFT't **]J0P&/Ub5կ,$Klˬ霘]˶[!ʝKݻx˷߿[x7`ĊPr ʑn'6kل eXTIԑ5SMNF C-5LJA0052@BP%:CLBD:LJA-N s ! h0CAAwm$9!(SP7S<Ҍ7Bb&bUzPaCA+(Іq"~ĕbesvb8<1@NT*jy]LP:+qAĶ' [`@809,>Dj/{%=g*ga0BgЊP {:P1WnD A ߆+kCU}68Ct/+C 0^]X^e[AVN84/pCL3@ 1}~GTPa=9<TXXc,`$yc3,u2?B1TВ`@0 hZ HP@e`5dHX8Tر  e6Cѣ'瘛08KAU@Fs!6'%RCD'Ƞ0LEa^, 4_ RHrM@$ 7M?ӈ ٰ)k'8DFzFtA,dH0T-A~8M`CE~G<E3\e'T-!*&. Х |en%8l#6pnf*`&=3K .yW7"/)  @9dF8$@0H sX+=9,t`!ZW@ .+XHT='//{a@ h@+q T0= h? SE< Ƹ/: xšʹT H' d:+YBdk Hxp[sےy~a@)`1 4 (*|!`@ϸ8H HF8Ɓ ^)7M[C >LaElڰJX\cF<~!`D5a ߰'r!e@-PS>4р$@2 ݐq+4@F (p, h] t iC(@5a 1}IiT H@*(!2-[l0 >(OPd# 3 )>@3ta@ @8P  /qj\1 cB:qRC,ӦvA !\b< (3KhȂЌn.64 gAw3\XG8ьṭkX1LC: lXcx `T)|l!̺B`A \'`FB /S3рH h7t lBZ058p )`6|!Ah0PZnV/~Qj%$l@‘1чG|O3O8~hߠ>Bۏj6C%P1EkC864whcH", ׈D0k4 E=h1*'XF8lk&;3рHt Bsd̃Τ |!,^t*'H  *\ȰÇ#JdpC3jp UIr"%S\ɲ˗0cʜI͛8s:#Ntr9'QĄ.10@-LX l^ ,@|foA˥֛"r| C(8n6!wNdj108 <=w 2cn0FPq/3L0@H8F|MAM :@a7)LG`5EZ18xVt"A#t} Qe8@<U0A:*M7P D4pz.9ფF 1@ @ & 8 'Bn"L H @Xi %'e Um$R_闘 0|PV&JT+DO.[i* *fVU UjBaUZ@!lKҔ%Q5F &H#hL@#6Dȵ09@lVG !P '#| 5A=ȥA|XIPq>FZP^ ;Tu" =oBDH%&OmLDȒFL;-e;;_eCCAXF]a@MC!,.fx'Hp@ X",jȱD*܈VG(=ʜiQńqwѣ=j 1"JAXjh5֯`J K,DZfӪ]˶۷pʝKݻx˷LrOÕW͛aćq+y˘tmΠCM,S^ͺk,^˾쩶oͻY+dœ+r9?Nu*L@qaٻW@\⃣>}71`Q@fxr07p@$P':LLp=EC5( 5S 8!^==4LGf"CBXipr@,0@hc3Ԁ] \=RYm!aiNGihlp)tix|矀*蠄IJG]=Ǩ=1ZV@!,.jr'H@P 'P8"B,jȱ#C0TqNd *\IB$$8ā(t8"% `B(kjxS!aZ,L`PE RZjĬ[u6`F Q 1UvUx|\" aS2D ,Mt̸!۳̝ Q \V0 Aɸ P 7x1%!'DKg,B r]wmśB2%>h A"cL%׸  6rO1-€فPW% L \S c \ grgJuy9"yB(0P3Lp aD V3, >0ءx5Iy%"y ( 6ckLdJ kc6ZcHFx(Ud0\L CO| 1cM \|@ :*iEÊL:LNPM8Op2-P+5-6: &ˢn{,BXBpBlB) @<1/L0XsB K/2*ܡ.GUO< ->>x#dP ? :f[ $M 4$ $$$p["0 T -6nsI%"X0"ڜB{bU9~0ZEDK S 0U 4XE/M.P3"-+N->jG<}*a8 vW?8D1 070 '0?@w xA2YZ͂ΜQ)?N~h27dx404x8a ^A .`np` T6DPE tIf* h *u:ԪA !cUc H@3e F~- wۯWN`vv,J%@>q@t ]@g W6eɰ' !P Y/>}WZ5u8cBCpy_VE¬7 X@ޡ (Cұ `.XP @h&4pfDd 4`G?P^jG09ꡈ0bWpcd Ewi{P׶pG/XPP sԈ<RL X9ÁIq<H-N?e@/02t#6PQx< ` 0P/  @@$Q]qr0n*A$P,]HqA1@@XG; wp7+% `jpU PY 0 5p4,R1,P" !G!-4gQ>т4`܀1?*,  :p @ 1S7]*5uM1'qj0zs8QgAmpP !PNBxQ 13m2,!N8!I#@ fzH0u,SW N'$ -8 J$(QzPshd('g+`XH8r8H`".g;u`='*4&q."N 18C:$Y \ I?ad3=A63)Q9.;c$X{ )TH+< 5 BZY,;6y'Yٕ1/aPRҐ y @>V-0`O(TW =S1) @ :,0 _ Ru `r$)i'Q.pp: ,P"50 ˳|W PE0p@% (鞣9g`  p p R0S- BO)Kip ZYȥ0 PP]0nS! 2<aZ:,`10 =G#:0!6506ݠ Т>/B*Y:j>:%  2)ޠ@`O4V2Dpi]jH$ 3pyP+@t QD:4QPp5pܘ^!0% ~ܐ0UD5 *P4@ ]0Z x꩟UWP3ISq)w'aکzp7SQ%-AbwC:s1SkSxR>A'!Ź\ڗE1/ٕ0w1?m!*Ly7v11> {V8*OcA9x1JW+?trUq(4)+q>,處 +zNDkÑJb, rB ٵgA#%oK^{; I2 *'Qۘun;r'E+>;G1O rOEkˑ: ;OjA]k@= 0O Q b ` 0 (p` ~0 k {F68@uL F@t6*M 6P0qr6@|t qw{;F0cP3n  @T  0 'FP^ lP2,` 8=sJ0tMô Pn7  JEpQs 0K? 0 σ97/nMC?, Sh u0, 9Iu`vM0< pJ9@^@ɺ%70  ;nl=u{O$`P@6/ w<̄@X~ @Y:RL -PK u0 Q@<3@ ő  <ʍc3O{ @ɻ  ƃ ` c0F0K $5 x$nD:_%V-ЁtSA*C h}M`>@jK RѬ'=96m Ȁ Α 0Pg Hp[j@>Y0D6T FpCF) w 0''?D@"q^ P@cd7 F  B@ '0(szj@goNp+P# > (0`ZeD 5 `QN? 0@ pf@+P?ǐFfN B`S/'Lwpw0p p Y D:/5Ǫ&v/1qH˃`tQ5 Ӻ8@ p׸C#%iTޚhz2To]:ztaQ!:ajMG3t1^imz)y!,2'H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0I͛8s̞@ Jѣ "]ʴӧP T*իXjHuׯ`ÊuٳhӪ*]KR˷ߛs㾕;]z*^̸d #PA˘#>Y ΠCwЂN`lZbb۸R; 'Z`vpB FtHlسkw:Y<|MTl&C:? `5AFG7eGRsh`E)UB'0e6 ǁf:0Pk]CGA\'!Ay0A a!=Ѐ,(d$@Ɛ PWACsNKQ֕X`7 k#d9A adp6L0E0qNX6AB&*R8ȠB(*j G馜jgէjQ IjZ ZjeJ$f٪v5(r 0YA` (kD tHakDpjB9D+ T zz0U+žiJJ,=$ Ybэ4 GtǺ"XmAjpC@Bo$On v /=cAUA'rBT@6okq[NžW[jA6wL.8\([0]5'L^~AcY[P|#PA\V r+YfCáG%BT,PN/Amf/M^ѡP@ V"@ E< V񑞪 pH'|] ,&ntbP\ EHR*r%! $ L l4&Jcemf"e]V!$\Fx11 , :!oLp3A $oˉPu-c,3sD +Wk>s51:lDv6{|4nq/+^wY€/&9x~C1-@fk:05՘Б۲ZvPqp YEꄼMC5R LEV#Ru$d'X, uP59WuL:n+6/x'lF4kYpf MOENdpTCp40﷦x2~r~=2A'o r59W:1sÑn#ap#ؐ5 \sR\p&N.@3&0PÇ=hE,PA( :c `(R 5 ;ߍ1exmV^h.aK#\* ~c/@c0 P@F&' [P/[',ЁUKǑADJ<+fsȖ UHu2Q  fV u P3 SwET5^d!Հ Pj`] 01 TGVx+G-"e1P A@NWHg+pW8:u$W@2`nwـ0?`]ED 0DR #NT43PY$_.;0&c4B  ĥVN22w#TZ,u2#aan,Dyq$T#1K50:@ 5gۀNΰ ` &shz/7 5@,R>{# v'/H+0 V lBp.PV$p6Adq'(؀ 6 $钇&4KTZ6  ݠHk-p3#z388sae%5"20,N =SF1PUbƔ70 ` I 򠙾`#PevY ;[۵ϨV©[-#`,QO2f((`y۰)F8p >01 "Uu5MnH+>;O[ ?C 5` OfC|i"M 2 e QHt;E&) 젙(T0c%q& 0> ~Bc) ⑺A3-3R&82fԿL"VP-0v` N0 ,/~pJSK쾍 qJ,^T#Q `X(r_ua1``L^\1/Cqu!p1JDuuwKvED|>#h2`ȇw~rmLhCHg,;Z!M'Ȫ JB\+ʘ>$A"2G,C@ָLKʧ4x[q=1z?>iGoAy͈a?J,Cm$ r P0U %$rq*'L茨wҼ#n ?04uכP9.`0K`V:-=wJq!twQ3P&BpeR؝#XE]/F![:IBփ'2؈ dm Q} S" `?1>sp{ѲA(.ׁz'},j-',R@ZL PhAX.^wLb Lp!;l*"2' Υe>\٪*E>"ABAbbJ,='s!+#%?1ɤ5/;c@g4_zB5 F"9qwcnxH }d#=0sԟXz<#3b! @"(בcRz)N3q,N{-p7A5:7ޙ -@80V]Cr4 B]`  *0pJ hhkr4砽΁Q+h,r 0rܠ 6 y/!0J$A DPB >QD-^ĘQF=~)FHL8P`).L0 ((A&CA 2^lV!>UTU^Ś`Ӌ&$`Y: Q"hڹ 70t" %b6'FXbƍ?jq `=',Ȁ 12 PX':"?Ɲ[n޽}= BL ^2xG P &v10&A _|!Glqۿ<f|Bj 8@ XZrA@b$8F$DOJ هD ǚF(j&`g{BBd\`=DQJ+IJ"#VPa h$ @XE,-3 2l,O?r*h6 Jc@4RI'Q(Ӆ,S0,TSOE Rj2=4Qzz.췿{{{|WwyW~~~`8@ЀD`@6Ё`%8A VЂK@!,q'H*\ȰC @Ë3jhĊ9Ir%S\$˗09JyCȘ8s20Ο@ *H_"ӔxJդ=\ʵׯ`ÊKٳhӪ]˶۷pqZ   "L8D܅w A$Ux௣=L A'.NXɖoYtLz=\*h06(CLQ RA fƍ` '+?2y\ В)a/x{Bg4??m91_I@62L'`؀ =5t Qe.vixPc- `e!LЂ_ PQO"b(58#z:ZP#% HAW$Q ecU^H+M )d@4`p €3M[ $ L 2"v"pL<=x#2 @!m9 2t'13>Y$pa3; 1 rO1B؄kr~ 109\Q&zºzD 1<唳 )3?N%ܡ1?Bw#L * ?0BvAh%ppB*%`E$Llp5QN(Kj ShYL3„;Q,p0e!( Ù*S @Sm_~0]2J'ЂxgpT_͓_cS3<g9݅YBfN>`B`x cíwQ`=3 -@}"nxصv@xc0ܱK9ċݺ/< ;4k 62(eL6hOp~,3#3O>=`d`#$ A2QTBPэ B3\17tHdE,:!|b>l ܁OP.  pn=hN Xk`+J@C, 4 i1cnLȈJ 8l@86PPjTl.p?2^'f1ZPo5H: B+%! A p } ߰E9pl,`*ɦ)2} : }Fc[Q@A N |%<0"O94ȅ=vAdܣ @.ыps4 `ȇ; $Q@k2w t"؇2jp 8vx ? EG6mK'9Z.HM.&boF£Myb5G8k 7A4s]#'/R7[ă83@S==$-ъN4d)G/HXȣ*R  M T; :X#(`8G/Px+ A |#f@0ы}L^y# @6#qA}$ ]B VXc/@f0o1`{LP~@'>E)9ΑtAeB&?l PЍq@d{ Y  @DEg p e5 GIK) NP8x7 pȰȠ\*@`  0z :0 @5B&(." Q8`:A 2P,p `QBP$4q %}ք7qR #0B DHH0S  ` HUY7q(p(,m34@ؠp@00 + / SDo}#x'# U~0ܐ`@ ( W{$v!/g+1w5 V= W6`'x2R!osu03Y*pu'B0"#O'}mJB+NBvwm'r$s%Gl3N9/QOX"@Z5`/pQ.pp,tV,0 &Y't*?qv! #`-zhQE6rڠpp,@@5P)*4:QyP4(ZC0O0$/jp#0a8 k{ :Pn+ 0P `TrQu1iM0 `_hȨXr0 #P0\v _$@{!". =t' :qq&4|=30Q͡!!P2i30 %<231|+sXfu&*{6)vY;>Db9!#ҟ?PXH$0kY*0qOAaJ4 3Xq.q63n2C.!Xü36!/QgkAqE+i:ZG8]:Am BXsu(7 ''+X(p 0 |1N r<7n) =5a$ l%49+C #v+0 -I>h#* @80{D(` $ I3jP  5=-p=CnDY0֘:)$bp4  U j6Gs^yx NF!j4 %U}C P0a;`*ͽ@էȠTT^6 9`:0#*2Zԅ ؄F/r @ HPt7X jJ50 Wq> x d|_1B%uL00 gy؛- 1sT'3?/p7K`P$`# م 0C *HpR0 Tà`  v3Z17 p25j)05 nHR:A/6O,q "!WQ5`IyX#  jQ+DL ']pFE'%( P @ zp){?Ye ?{7{ |z)݀>xY&$KDfjoWd5QF=~(@߾b \ o?~VpfoߵhV@A("FD~qOfF=рY78@N5kD$f:!@PA %`rd;0Lc #IRĹiNUZ0HGZ}q*gZ 脏ҽ}mQ{Œ={ \肅Kxx.@ ŧG:zIHu _o s#P>$E*!,P4I'(p!& "8Ç#Jd:ȱ# 'dHC'&4ɲƖ09I3ŕ A1D>H10  p`€ *T DV `@^&Ȱ*0a+&$kB /&(!]WʣĈYH"C`2LsE"PQF iKu!3A *p`ŠU@V!,'H*\ȰÇU@HŋBp0PQ C (S\9AT0cʜ&3 ɳ'O > JѣH^Y@&'.dXIJ &!f d]:&@v &`Ђ_Nyǐ1Vpɘ)7 +P`ly,D.m4ҙY/Yhp'$hH+0e % p-Ng@p0b U72OR&nĆc$8>\9 OPȂK 288 ҂c1g > O=32,9 A7`(挂M<󠂂L p>6@3 n <;O/Ʋ,2/@L03q -@@C2@$4Om 0 >L# 4p kB,A-?Zu 8A/ $.A$V (P700A> #/0.;zPҌ !s3 $whӋC8E~.88 iP0PCQР1 o PO/zv7(33J @ f155ZR^ B嶀2hL4N@INC>N`x_iY@C L@+hBGز3D  @ .$3A.O P $ӏH%>_ x:0-ycA 2`bj pa@gR%2V a[ 1'L0˟ py}>PR'& (&'r8sNB:yB8@@JuDC 8P;?v P -!HmBJjF7*r2ьJ(FqCʑ 7"ԟ$@ LFJ˹lD$i7/J@hZhRIQjqy #A8%g~JSP'G0ɪ Lznt ߘ[@wN 5鏔Rc!| _yDǐQ DJ*>}ʘw17PAJШ&q~[>`gaoBȂ ĵ--{6d lI1C--d Fc#Zj28 PA4 9Ѥ_b-x<T}F<Tpe߂6a|LLlj[;3MB B 4n!'P1U*`'ޕ"g N g2a!|*CZ)PEHGL80 pU]$Zhq  vx'8܋}#<0 3Y6vvD/f-85˗O $W8 "Gx{NJs1ЂXF9 pc X<l+ 2bZƆejt >l#?D|C<@8 1{F5NbZ8@IE~Ӂ߀" xJvo oa6ޒxyaW^ 20ݐAa#%!@F5uC#H<+ 3qѝ?0N &ЊO+oi6p?>"d8R zؐ1pa(0G4!AOF4 = I&BP 20>P:> (|ɠw͐ @>0RUzgV @Qx % CWwذgP( bVv`ːWP UHWȐYhxC}'` 2` 6W(( !es cu ; 0!& 3P 3P) 7@ +K:v ({DG!O ݠ50p~,P0 W ud ;) `<P%@ k)R]@50I0.ݤ  <5  ܠ%0 7`6 P@_&e%`@ؐ/p ` K)ư ƣb`GR $!P~0 9"&3\@_~6VlV  @r w* GP v3ZMVfP D"P <'>50(tp .2ˮq)m)` NH Ճ:P)P:0dײOީ   Sp`d؉ p1 +ڶM5p[tkd6s 0xL0`1 )ҴKnޑo9kT\U-p#ƪ AkI-89Vc/y̻:vHzYTJTjw+ˠ[+R+J[b˝tZ+ӟ\E+X+#U[^}ќ7վ,NE#J nѾn1#lqZ4.è`R4 0 ocuP 4,8,FR8C}P:0P+rj`g5a%jCo1VR1-Px!I08a 0O3 i ky,R ]@T`g7q%"1`R$ ې$m] ڀ V-0 ŀ 6G`  P 5&,0IγX)"-*"1 HdWc*0 ݀x 5 p0 Rwk;1V#nYWю* 1h1g6lf p >`7@E B X6t]38` (џcMd$;2K yĀl~ BLS"3} :#o(Pc"B77)00+`_xNcS͆V-;8l$PΠ:`- CzS {_qs=#PV f|7}# CshdْDD pٔԝM% %'w0p9 8p A72J z/P<5 O- e7|r  ƠōMK-1&0(h7 ` pФ4P0Ȅ'jfr`n ^ʛ͜5DZ<})4, KvuG3 2c*6v;~@ PJ3eG(8!*2`g+>!UMrj5v9T|iRQLuc J"9[+aUQ٤M ]EN}%\ugVAu>$lvT(Ξ]]<ʱNoA 2`pQpUTUV]^Q%r"6[x]-0 W@ƀ qULϪp!y5T0o`U*rOq%VzP&$Q%oi$@>$Lp/qcR-R:Pm`` 3@w0*(i`h` )bZ(/B + !* p G] ƌ  n C n + 0~ŗ>uN:`0Pm3SV nUP/p m> %m 9\j -XЎT\R0%LƆ j ({=p #bj*A$g\2m'0 Ӱ=0D ` ^P@h…>QD-^8Q J1b+H @Y HXx\/.Z ${cY aSA s# ^z5V]n zX8uY `QB&.8co ,14NQKE zŜYFX„mpfxY&yި\9'c7d':UA\)( #2˖nƞb#>pb8.C| @шxqxp3H$#N`IT> <}< XqYbg0iE@=8$ :0E q' cp5c @@s]5a!Xr(&s"L=B091tp8]b@&KP"Ay| X%@gF!A&@v@uP 丈zy:Pv H)Q hB0MBp@54D`dGPl'<m8`!,pcBߕ+Bv6y* = D.&H`ذy2H@ "8(D1>LhfH!<ч1|A^H~E9 PpqGՀN M R@myx"5p%pN0(FD3s Z6])- Jlk + x{*BlJS @`CZ2A mBL0R"\ZHJeaHJ95a4`  !JY΁DR: ʃ2 X2}L`t e05c i<攄%XH}+ cTWeZM7J%3_hKT ՂObC#ʉP!Q2L 9&ªrZUEc*eAI=a]*Y$3y@@F9l@4OR!ePEt,L|-xqAieAdžif`x6A:(Rzа\%l]q́8GZlEq)Ђ`cxe 1|Cj0&`xcTajkJsn$4@G9V@YCF?"b\CF=AA8. 0gۘq!X:ax 'h G` kl` <6 j +"0һR-1: R X` (hzЅBk3؈;cC x5pmP пosy؀w)PKf Ĉ T@Ѕ@w@xxpX3hЇ,xXaPa9x\^jH)8(;+?!@[: L wI)WPvpxXnw;*}ȏ|6| pf =; A< X$$yEB'F` !B"Fb+3옛m9Ii z/al@iD9kd<+@cXI;)EFmn GkF SGꪙG14Ǥ{GG?)z<ǁ$|4Hz2!,xz)P@A ZXh!D E  z8Ƀ #jD8pÆON( G≠G!'pI$N7 v!Hbˆb8r@̊1)` $8bp:rAT"JȌJjե)Bvi gDEa  &!gAOIZfA9=\3  + aR DiiV3Ptbm Pfy5Pb NoEG-D6&yjjZ*'0 *˯]wp W0jǑ@SA 1r#6'c;N1۬4[Jddh㧧) -Bv/)S]J32*b p=19_b2t'8Xsi5̀n:c []^@+MG 28D8QmڼOthyM0qxNЂ|0 M@/o'7G/Won+˽/ܫވ:n0ɺםA9 K<Y9y*BN` ԄxBX3@Ad$E&8Y`J>A|it8 y GF)@00(vBUmbaȅ# $x @0❙%)Ep 4ժ׎`%P:&L`ChbA2Y5>8bl&ƙa^ Mgy(A|և@)^">fQxҏ;LH}}9|&Led@R`{8A N(]ziދʒf"*W61⑽0jif * L7Fm@xҕY72.3Q8 z)=dygsd={cD0.z `9a gK9h|O Bhd !CLך&@qAA)I'LK'A'mzbEƄ(1MA#HRչe$|X1a,"Zͨ$e\5I js֪"M@@be@N],3vi_M됎,A0 4f[:8k[Po<{ dYda@#!" A X+c L) ɼmD` 3f4P &0̀ H O *C `&% :%\D*HNl4e+CMnK=吵pķ״#͐kO( s@4\IQ@l H (@ݠPbcBePF p ^8t@ +XP' @SaƦi$0#^&~LWC@0aM@105nHp"v4GFg#+LR=$!GT[@:!΁#, 344ycH:+K:Q4; C@FwPoG91lw2 `CtU"r| ̅Np ҝ N%&p`IԘ FŢvg&{ 1  بmkk%^*@p-tK ܦ*,A8?0c=jXV8 K:!,,& ٘C8@<(AWl`AIòܕ6n@p= +7'@\0k+K,g9Lkp [^ =05>낁/5k ̦g2tCĐAtn B ;"lF1F)1a]7Y# w̻ GTi -pE0N+x"vE!^cWn[jE<8A L@kCS>9 ,g9 N!̏T}@$ $r#bf\H^A[lLc7 t(w&#$ 0Ox輇s\IJ_&nQ"YGOXpaMnz6i}^{E4sNC})-@9~ƩL'Tzޓ_-P 8pzS~藀9~! 6' ZRd'g>'&Fd8? ؂>AP~0 J`!EёY6 `jdaD+3!@*g>%`60pSd>xQG6c2y.-b1kW b$K0bGnP#0 1kCdG)mKS0_e F@1NU5 %@0&*Pjx* 1U0v 0zXPQUQvTx + pc7e 1"4P 2؍HBQ%" &N!DWp/ Ue I` : xB i U.@S>4I>sE mBf4NR(b%la>"K^wQ0|rA`;QCV @9KN jY#)1  &Y+`0Ehf ֲaY-#fy- POa6* 5ڰ6c&qBR'QY ȐO0-/"F ` PqdPFxS]-i+0 >" qW17v٘ƙ}ЕݠB3nvIS! 4qb Op@'0iLfncw )K~'x Y,iP T{6E/' 4c7]RgU:0dA Vw?О0F O Km"@< #`\ @$ 02` Z' 0Ki  ְTBRٞe eAS%9!n8 XW=2`@S*$֑<IwuEp7;(! ,/8qrSX6 dr/0]`@;r:m~17h$wBש(wzEjJxQ+6r{BV5Jwek b:;J [!{+wtm7S,PoFXn&۳?j*˲+ u+,|0!z( ѳT{o"@JnvqJ>kSnq{ID'XU50 ~0cCs0K^iKkGmo >;DtP"YsJg?2h+| Pmĸp̅N2 $ o$ +eXd;M,;;wQKǗ/Y6p0zP*B01k[+5bs);> &N%`@r; q|4MAqː5x)h1K q : ! 3 5T-ЕJ \Ћ%.p 1Thj@"N $lB )F,p 5 2P8lah҃ CŎñFed`/T[1T` 4Kx<`<k*c7(IYHCe- 4Ewǐ+*_8rz9L<a,K!_gCasaʯ:LV,˲Lˏ1˿zL?w=Ǫ8K~Al#̽|#~{\;8ͼ1N4xl$+^{3\! 4qt&S@C9g<7":- 'S<7*B<όi\ 1&M( R4*p 氇8 @0œ +! 2hy|aS ,b-$RCV z YR `% 0 K] RO o k@ݑB=YD+#YM -|-5q( UXV7K0_@Źx| ` ʐͷFMU-I?#[B @# `@$uo֖}],@?s =>I4T,r=M+C . VD=r8كkcՙЂDE"0eaP @4plލ6 6A7l>b)Iω $UFu. ,~^@VE Q;/Pa$QBƙk)23R*4)i)0 Eg? 4mJG~-?6ct1 vd:p8P}SX@$ waU0T20y!l $K"ϡ, C<#Q#E*P0Dz$K.x|فX) ~4r_70 j;Ҫ2~Bo)vQ#~v J-t1'>ށ7^|?kqs 7t'}gJ>kg2'qϲB#^^Ø>/_($A r d@0~$JxC!ࡆC;6B!1@r#_=`!!!~俁@>ڑ=!( 2 f Äq5dT0 "&Hg9TA"@ALxnE 0BX E (8@SbP?Ydʕ-_T*Pzΐ` HLPG0n-L$xgiKŀ &l1c43?]tխcӂ)"!6~h *b3WI!)1߼)5^afr@'tɤ暻.B 'B /4*; PX xaglj|'&kF&gya ðI'2J)@ . ,(L&X- i@ ,&P)4PAJ4.T22@ϲAێT>e%TSOE5 i&`԰._IR-Xa (ւ^j^3KTYgTVur$eV}5]5I DLF]w߅׺͂Y;S\hń59qhk CذnWtW5^' `{I0_ǭ1TQNXa\^V3 bwgaLmobp R\y9,sI0Pv٥ (0Lr}&lz 7=p eU.Ğz IEDʬ_KU{'rsi `3m< Ni0 pD&hpÝkRm; qˏG>U%*vB؞@ 2E*>H* :=Z9iڤMBtDTx?6)s  J"4w$poߋ& 1B 74 p9d BP*#H$TXD aX$ @}vǘ  C H n4pU&D%_f>Iptت@ !A HP6 |x@(X6Bh=PnC%FJVrg0!#ALih+%!s,)|A @ܠ"F/of0E5F , A'MBfoYz B:dx 1MRI"'N|TDm" 09OSIe 8Yfj XBGE3L IG59@' al# "QPD `Apœ'Ei0p3'u`p&`I戾|g$hGqDe4OVj5\gb(:kOabj(mD Ht|PUBn"Q p(1pL\fЄ-e'A}$ AjB ɜWV2`mB # d9q%paWJK 5R Y0o'Ѓ tzm;ޥL+'ժ8ƘS_ET׾ z*ka3s/}~` 0{`ż8dJ ]w>y0ј$a82[oa'! L@xcA҂ FYG֔vB..uq c4ȇ=  A"teH*H j\&LCe)T.F0 N^UU4> ޻dc T9 hoRlgNKZR\ cP 8!2Όcdن~r\c:,Ҧ;l~z @w tc%j-2A_\h0 ܣ  lD4f׻ΆV& y8CDT6y3Q P,3:c{wΞ @|mbstceTqg8CHAABplG/ rs`ÖGoL # MP\G061r|$HAP@FzQl1`+/2b4cB 4g5€5˂v%Lrx>@4quB"( x(D56*AЃ \ҙ܃r%( Q {Y M@7Ad(u)Q|?ט>n (1kSy@;śj; &&ɘ9%/sǫA%s Pؒ`'l 8⧘1+ )4' qerA`@8 =DB0AA1  B$'`1Āb=lT8; bH)@\I0q~@`¸xTQ!*PpA9AIm Z%ۄ,!B(P[$1q"h "q`?aJ* P?OrM``Axl[<¬P c @ ZPP2 O0Nb е@KAx@Oa2?ҀXhPN7"u #A @@0MA@  Z\qB FNqhIMgD@(3 73ALtM4A%0,0 tzHX-31N3N@ T.)= *:5d%'tTKTQ8Ռ@3 !dW*cA tX:YŒ5TB 0d9`0@"` 78Qm(3pP9@d0 %`Q@F@X (88寝"yB81<~8)P `S Cc d:@(O dO5ʹB[`G *X&А@̌ %ǔxKx+p8@[`y 0Z@}2A '҈/$ 4^tUvZ -'x{ZsuBcYPWb` :BUP:B!zi \ {{W. ) Cjeo]nUUP\QG^a|o: A tXPBqKG@xs i a'P (`4>ɍc!F!BI#Z'"1 f@) @|Ft围 rOBЮ(ALguv#<'~ *R޾٤r^ Ud<' n fzwf-10p-mHA*feFZMNl{2a N#™Hђ(@=+F7|P[M@f A6< [!/@7֏Aljsl2}hjA*?k$HJT^Z${<  m{( n^`I$A 'Nn,@dHءc7 ?Xr$8xpBiHĂcFI&`1`@% +' v!aE<  D{tH@Qf%xY `##_Yw y^\RwuVs0kF|r)Ǜ~.&g,5'ew <W Z3y]L zvU E+d^{0{ PZdp- 2o ם0+NZ*ŏ`nzUU(}RjV?DW-0C׶Eqq#DA>%=w`Wa pi 2&P-P2]HЁ 0 NacL q`IofU)L}aQ.+/sm3rK  2/sVbIQm 40 ^ue#`"a5z:aWAA[Rghytv-A$0̓~4"'&+` @YWi%Q @<QA J&aE7& @^ APt%uFX 8 \!8@c_@(_UzR p3(nY+J0ZupaYdQufȐ480908n0Srg`Fgc ?8F(=q&iY9@h0mK@F 0IFO V =xL9:GYY J!8{(g11qA`9upS*8}Lag PT0$Mp&h6y73Ta[!@ 9]D2i2(P$NE|NiY u;HPPhi|XbQ(@tIg9Eٙy8!4'!aYhVb 2yIyDxG&(iYEcgu=f !?Yp -@P 4 P P4$"UT8U"Cn%v& =3"ڲ& ְQ5b==l1` " 0`0 _) @aa3߲*@ᅉ0 S=,`"!n^ .$"j'5!$\"1p؀"oQp4a>`4@pL A〳a860=!H(M@v"%: 0 cM4]x;D ',* YW!Np@PHۈY0]53@+Aa$CA?ɘ@,l 3yQ66NЍvDcB# L -AG(E`cA  ANS@L0pG`#Λ0שp (t2A jq @_kPhR-E~Qp)dWhak;\! UV bB Ҳwa<[n 1]chi A[`if \cK)2lo3A;#]<<DOtEo) 75@4e="5] ]P HIP88a+B p"*L00C0h9,, //_c Ϻ@^<OoCX2X>P=x&ӅL;YT@/#LBJ5AtN`@s 2!P<\(4tBLON e$A\@:u@LΆFVP #`6G j]RG܏ XJ ̍+ 4>BXYARZ8愆p+PAjr@$H` ,!@2bdЇ"@!G `h0X^2s> ˇC5v ] O|LO BW @@ŗTG"AJJ&D#Z>R.X@N1d 1×8@IiL0DŽBU wEA.)$B 3`{ׁ.UYvuL{4Ԧ&zmGYHomVjڡ#9C}Ģ_"؀'NE.҇ˎ00(81Vq ~^$9T\)iLIX+N|yG >;s%#K8ZH#4Gbgr>`J'0&#&lr){ix& ,h`A~9*l/KܫR %Q9/idFD';q9qM`2 m`!FA@ pX ؀ 0б ɨCQCHW,/ Л R@,{!O xgC :ʁ #xRBONA, t!^ȏ W/+wtwzZjr:J(A0I# T: _vc$3\->[`,/3:]"/TfjeD]ZVg}(-5KEJ/I9 *?3$ 'Pg<9u!=A4z6 cxw(+L# XOː䋤GA{|@ /~ӈNzSa| HsL:nO3A*_7)f3QP! ‡0q`! T0K#U3ph `"y#8xj 3g|.6%]_ *@^ť}~.a$cXq0"0$76}%m$az %cx_lHw1T2g5'`ƕNzAYG4D(AFoV,UQD57}X b1  t0:l-#r CI% p+O3Mu8ڧ8p.jyO308@!E /VBЖZ+f͘g :LV XTXAD0HnIk2Eu0g50h&guK{vuOh!0w"9$I%iB)IsX/1I,ْRf!,(,"'La(TA@`aNP`! !,(ix'H*\+JHŋ Ǐ(S$( +cʼq3sDXΟ@{JTP(8q  P(0D-'i՚]V[kE;jZJ$u@XM lܹyզ e ȺBN*TP \@/ T%`5AK; "N-3.' +(X31O @#"%5U㙬x9$M,L` 8hAS T) g[YpN>ƴ ԰@ g 2瀣 ͤc (">֘1hdB9ΐ`>W@,L4s M1:  < kCujk7   pL;OL:N8 )zĀN7N pL< !C283ݜ0/Т@) ,7J\! <(?\C9@@2U@5ӎ5C0@ /hYٸ֦s 5d!",eF7@ ~HS xC^5t{P`eL0 |gWTn Ν[]a&u` P$HtYAөxHY?jf4)ZHY5UB@ X*Ui Ƥ>~H@$`+P'd1 fZpTn '$}}ڛ*?h{+1X @ \G6 qr(@ՋX>M)(7E pes (E>/2)@vY@11l$'.!0G>Ax,#*F<_n(|@(mg% ېG@.W`XR*kȢ/tПUڐ3vP@;tQ(Ax, X2 l' 1 ! P Tit&4t Tl #ygQ1'M^;pa&A a`@1 @2 (.@s(cO/>c^VZajX@CdfXpFXшF:> FxB,E?(a5%b 6EH1a: t_)06O@8AguBRF )``.TUؚ`HR1`@ɶl A P.h=pq\8mjWrtA2v'Dpm#Af-5AX6(@PC80A,PHm|B^:;OwB''"t> c /@Qˣ<F {cۘFiAa .H?i 9N)E" - O ӍZxNb5Þ@p?!P <RħPvXD"r"}$᳗/ !'8a ZpCpB|Ρ,] kT=REZП*M a~NHPiabyu@) LZ$!PAVi?Svs5将@HLޣdFHpS^#Ӱ$VCD(hp# P@xyX`Ex'`YBs"f1\dBET"+~,`h!]ԠPp\/؆@Jp7Kavrx &`s#: Щg8N,AB;c0؝gG9< 6vЀSЌXᘆ5oL;A<!EtB-RY; % F@j(XZP 5=lhDgh.@#!K n TX0x43@TpZ4 sTBe8 ԁc: 1Dр 22%0+;;~K! w08 $P` B W%0H @<K;89qFY1'T`pl*pBM\>50 PD,PMAG280 jIjSG3Q1W=A0l\="a`I:q6<&C;VfCMa9WQlVSQRC36|w4?(<pшq;+0c6{F emt ?GphEQR=Z<1 E`B:Ra%17Xr8fF4^ RTg>Z 80vQ0P5J: >`V91M@Q'pyYH0*P dЩ# q 4 p1@ ֐3"@n @ zp8s  2gS"P@ ^0y`44Ds*̀ p|vOy0  qc 2@z6 * npAIsD !`4p L& AIfh aÓus6pyyP <3`0'!@bq#X!0 jK12TY^!<:MrlFTC(%P]eVC:tD\V9-= Q gDȆ|,T#Z` .3<~0]5B5ai^R! QCʑ]bgYj ^Bf;CaQ7!EL@1pF0-4P$ ' ؀ - ` 2% 8G-@ DK @9ezS:  B`NP ǐ ٹ pL8 p 갩   .P ~`5  *HRQE, y< W{KN) 5' #  z ^)P~ 0 PM uPZa'|-@/H@@ TU# P% q_)P<ҠN;8 `%R_! o Ô)@ /?UK zqM  !(W@ YN 5'`?K ,,R X,xfqfL0 %MD$-L׿,)L;%㠅 uKP`ó~XHX,X`6e,HFMdHω1L QJu'(0"`p@dR  ^d ^BdM#N /ڠE YTev}ڽxUSs";w G k'͉fvj!5pօc¬xNjdD-,, @71` @!I8 ЏQ` ''`& &aT`AЏJ  'q¬ZBNDPhaE! $ӷ( +-[@ɷ:ID(ELɓ&K.$@$4(<_M)ӜˤC锲;H DMIPONBæ&l*SMWPIT!ՐTZV }UXHOu] _ aM*c !,4Ce'HTpÇ#\Pŋf—G+ NHN 0!@WG̛ ]s z lBH8a)N+ 괪իXjʵׯ`Ê+ <@ UhL5! UBb-N4wC7!`…'B~N.u@``+0kOM۸sͻ Nc+RV|g͔t&~7d!<(w@5XHdgPIz .(Km\ZCXI7?6@I$C_ P@!TI5B%pŠp80v#~A xG;V?:dD3 4T$ AS8aR'g@&V%uLЀG=IU^wbd#}1dL}fA⤄Affq< :"Q6_@cL,z:JI*P_@ZUt띷B`&A* Aޤ ' j=5Cxs+K;f*t5u»@]"opIK!,2'(B @Ç#JHŋA@Ȑ )"ɓ(S\ɲ˗0cʜIKzɳ'A$w8NH*]ʴӧPoV ՓQxׯ`Ê Ub ۷pʕ @J @ BH8AL&C faOPhdװcZx0$OEl.j&OD KN=3y 6*|[` #"tSP1=H@~w@K41#y @w@{}`W!vaW TC 6>}=Z+a"P(P+LpB H&$K}s d@,yf8:( \8Erq{ІV9CD s6'pjQ|`LP|NPD\Bt`Ģa2  sp 94a|Id9H6lu G JN8 -v v%"p I!س{Y  lG:x  {Vl1Y 033%Zۢ@ ċb=@:A_2tsW CVKy6A!o~0);wuRYmz tX,"LaEpT0Ac4@-`[_'xO }eoY 9 m1ƍm⨧YdQEE@GY ,PUc;^5"N5N=A SH;t=_bsg/ǟu]"0`$pc@_M$A ‘&1, p,f1W6akdcoӑ`i 0sTAU!Q !GjĤm=BLIB 4$_,czv`ziO`8h‚L8@g>N! b"9.:J@"@9ajD G0HlC B,`@ވhg#Ä`'&p !%x!$L` -HLCDl@]<@v  |1'0]L-pL \PK\6=3'` z͸4nT (A jPMp̒ T@hpc5,Nd RND@> [0JԢRB"]6,` 8A5FcB]v*ptCDkmPp/I@ \B#_caN@QJTd6BR2=jlgK@!^YU' t YnH3@Y~ f1KD7tW6@xgj̸8G aHA h@Q(wX6@0m,` 6s @R \K;$@wm-XFrd48PK!4*D!HW^ݒ0v^6rlO QҌvN*8F;H\U Yh=`3 F: < B,A 0Q|B@ÌmpFe#[ɔH <'L`p@Ac|LO[˼ՠlEuS挢AXkA@ !@Ւ+. S("H 0x@[,z2AT0m͵zQէۈfX'lDw")@]G9s6f4J3ܲ-ĘMe{ jnw PA%OR0p\ n0D40xNs-/IbRJ9~iZ# Q$ yAE=$Hd,Ej,CX^[2Ϻ$`cu]\QB2}\ ; $mwwvҩ]( iIyJ$ w< Oy.n@ݴWU3J{#vL򨏟B\@``PBe)tGo{է %E;`NǐWP H4bb܅_~ a@ .Z`/ :! p0`i@ !@E;w~10[$x3!-pC"RQr'PJ(䇀$;Җ`p ڡFSV 0> Z-0B0DCTF;' 5N;`N5 >P51PK>p$ @ CHwh8T7(8 ]TDXI:c2OPN#Y?iX)PJQ'1N  N0Z {HHG8LC}qyU rMxF3p00@Rh>8b ^4wpuS40fA  @@H* HAAKF7->GGUx0#ag(8%@0\js33$DT!GA؏ 'wQ;q:xivx< ayq"t܃ :ixlnf>%Y~(AGWGz>Ix%&ɓP>ytjqDYAwaDW>MQ9a+ t3 Vg)+`mSQ t@|Y:y2:bI1C XLyw4& CDG}yw?3`q ?uisOr3`tw  ٕrwO$$a@cNyzYKavO4$XИx 0(I !($`ŖpL4 PinLH%s "PP"M P }B@a% 2 P Z%4 JPeN_PGõ ` @$5 >0@ %P" feA$M+pVCJfFHme6VM(=(B4 p^v q:nvVʠNspX` 2-`h Ц Ѕ @WS URϦh(pV(Jqک4: 1?0XX%! mc t!4Ͷ]N φ. 4$4N7*ғ@  +H vo*څK1` 0%1g#UXW z4ZMp+PڊZDfP~d-3` nq$*4!Ne D _gZL/pIp *`?S0WXJ!u3{a4 AQ&YK4n>,e&Db۶2a9qx%| 18w۷ Ir#nv8Ù88p9۹)I6 6~@1빪[|.)6g[[QJ1E, W-' v$b ? Gw 'P} Aoc{T@ 2b5+PM%C΀ Pڡ-y/V' -b3-::F נX9 " pLQ`7C$gP pt;CY#ІPth`n(- u$H_)j !|y45\s$P~X9q08J ?IvCL\s >7Z:XSC`?QY)O%Pz{b<*]0+PYdX: 0lΐNPZΠ 0:#ȃ SnL \: @- ZQ @:OՄ 0CK" CV* .yA,% 8&Z0?Є$ p {4QT8'0>@%vo ZqUrVtăqoBp ]\h:-%Ut@ 쩡D+SmAqLq4*BGw$mP4Fp9!W6777K+*R`nidw1\wω\-JqY c!6fw$:U]p]8=ցO9;"mɒ+)pEADyu'PD1z=|]sM1{1"(t-C؍]QExiHp3 l[dmA@uuM 4WV` 0) ` *-wH }|a) I&>X܆F pzw#d;Z uJr-ΏwSq TB٩("fJ100q;%9QN!3 Hj0U2@dJ ]m4Su  ]qNseH`WDY$BRA) XӀ 1aA4 10 P"@È 2P`91 F P agzm~2|Eq|0%QLb'X1[O/2PLDpUP%MHLP'"J`0ޠ'\;` Qhk4iђL x&$ c>qGM!Ln,p'9^" m+ī& QV:FYqhrEyb\Ksrf#m|j;#X9;ys2]V{j>Zn"v!m!mDٍ#gȆcG335Kݏzv BLPa BpĄ"0X8cȅ"DX0J-]SL5męSN=}TPA.,r 04ȈŐ-Z $ I͞-XZmݾW\u<*2˩N[ TVT 0A(8Yw=ZhҥMp1TLCb`z*'8DQ `Dž=\r͝?);_y 'ݶ9}ߴBIK [OPd+Wȡ0@$p(&ZhZJh&b  Xp *@̍ @ @&8 ' Ȭo1GwT� $1pD @ $ch76Tyq/@(2$'Ur 9ʲG7߄3N9q`OOd0#&`)4n&C "=8Q3MSO? H n!%pr>h" 0*6h0ヅP4 Tg6p<ÚIM-CC7GW *sRf ^{& rCZ$  ɓH7wӨ (Zպq!nj|7z\´- l LMc 1NSgd$)$nKDA0d'aZ;N)P:$:A fg Ѐ *1z!wD6 A!gqvP)TH#ꄍm@n=# Bqo]4Ғ"Md; n/3Lz4w矇--⽪ZjsWrJA7z'ߦX:{w/?~˯~'k,_h:@"XHODݭLSSY'"I 0A o'Da |L X28< 6 RFF} A )P-Ha@;pWmqS6z{XD&rC$Ybȣ&FGJvc@0PÐpG'1y,Ș ac$acVralc4f 9Ҕb$ HNbWB1 H?&AD$P X0PL`jTh'()9Nr!=( @1!DkJ2ܡ ,pJ@@ irL ҍ͘i]АX&)%,X2bIGV`B$B l!jT2҅`pXHx/80Fn! 7^%c <aޱd2Jկ v W`(B";2A BBZ#aF:ϐ``F#0#PЈs9;[D=;Pȉ$A $ MjB~j3 X ;'sXmx uj@7 0` _+ D\xm Ӆh!A#Nac V;^ W40@ ݸ?p)ׄFf̅f% T@ l+pGxp7x%>qWx5qwyE>r'GyUr/ye>s7yus?zЅ>tGGzҕt7Ozԥ>uWWzֵuwqOЂ!,/Q''H*\xHŋ:8bƏ C  @ɓI\/A2O 0Hv")kj@+GXxp{J"tcLJ0)(!@8b B!NC0~>AmF"^b25P,A698VT&UnU c-f%^;,K9ᢡ05QOo|N䨩:qSx1$P^:( b @ܭ$9c݋- %KR2E@`HbN6!G/j@exZJ(5@&D"A`@Amq lWX``,٘@<=شbʆL\Q@3Qt$h)E9f# k 0k"cCxZ'Edp 2HOpp=vDuRhAT+51C(p0Xz  SqBz 0c!Db ][*rEv (푁<[B qE$;%@8䡴G»h! p0! p3`OhH#2:3y^4 6Kx7-h@IN@$FL|JUx?R`S=l˛$\Ss%Jce՜%?Ht-yUS4 &siG[.o:'T| 2::wN49 g>D2@CˑC"U=x` 1XUmC 4/kC+JDҧR V,qMCw8=DqEld=Y@>gT}(Pvz} .>%;RYCA~A.YN@r4 $.0x@ ,/$ӏ0hB= 3$tJ] C%H̸%3nܜ; @EΣK4xOo cM>NPTi06L5$yqB383p $`C%wQB 03!4* @ċ<=܈c8t3,p?24]G73%r>\ =(`;(@h=+( w3(C i]t P%|CJ C' 0C (2V:f]pPZ'X* on=b +2p@Q_V` )j?0M# Z`:0G \`1@ b$+uAn/D EbXVK Ea~'`W'' a9(5(J`{ >:5N@*>!`ܩW >iPVՠF= c KG`[}ԘǷJz; F$-$ DZL@[&8GH !aCrjGLX|GVPegքtCD {%0 vIDP S) )&h¢>$e,M "8<8(vB `-0*q !34DZE ``RE;z! 1F hsRԥkns}8q` 08P~hG,J4| dF6v1xDceA0d0.i7F΀EGj0zP` %P,`Shw *ր 1pҐ ڐ2W0OԀ D- , k@FV6' 5xk'0 p * `Et9Rw "P4 mk5 VY#`/_7 @V@E'f50 9D:)gx ~ Nr/403Y3p8 @ CpP) l@ajrg ,.0@ P  `Mp7 ؠ#aLKR , ~p/ C0 d/ QB:C)fb!p+B$CWT)^%=cw9^R)q#  #[H$@yi"-*p3*k:֚ $!A"@z!;QcS7E"0r!ŹiV6:+e>=;?a8#!=Er@EhCR cAIFu˼6ד[0A0LA6hՔ A`&` ` pP@; иT>t(f& |F&hu q ]Х q!# ;H  %D g$Q5V=cH3p#0  *P Pǂr4q bP ~05( `Fv~  u P(&!0l-0u@, 6@|iqpİLh`{_00^0L s@r `M@p0TQ:9P MP6 s0<\ i ,<$p0s  @0s`'DAn E@}  3*@Q 0`@QPp#0npu  ]=csf,@ @Ћ@$%`F@A DUn #Uu)u`h adf8QĦѓ?=I8@^'ծ\FR7P`pIK{P-\`U=c, $`ʯ\؞ ]G>7Pl `u-/Im@@4M<]{=  c!L@=K`p^h n'` ؇0;KʄU0¾H  00 +i0T@R NQ `> l t @ 3|`; o 930b ,.R*P:@ӥbp, CE P'c:h61O~Gd->q+8ݍ$0DFL6_0nFʻ!C`btGBqA(d1A<_$[{`V>5v@adfdT#>DEʢCy=>@bE<#  kC !_^YeHAC 6OUd"/̛l@%@0Ϡ5ـ "I֓lӰ aw@ G1G=CC)a*`|b@XIw N-6t`!/4%~a 3080] &;%) ^S/ 6  P=-, MQ!PL DP 0@ 3@ 0 æ ܸ= PaK d8 ĺ6|tOlۨ\ɘ0 RM7'bH3wnXhN ۺ0%EcZ`] a9|ś0 P ޺A7 UcA*}LX`rM M2)m{cY:': Dbęyܩ mx=3vnC;usR`0p)A/g岎e1g]j @&9fb:DX%tW.>G^"`qlI,7 2``n&H9Z:`iiI,g&<)lFIZ   iَVqg I h4 @YraFP@&-FR8bJ 5&XfZ@dqbu~Qqq]&P TZj, "rA.d *K@j e^ @  @Rᇖ .`` @^ zx.fXJP Y`rxPZ^EPQfѧ mwZ am2$vbFph|bzB)A{AX FFƉ jAk|[y, $\HF^~8)h0>c`:v EV6s̪T)+gqo醫|g|Mt%?4 @Q chAJD@@vnEB`5j) 6 8aBD MOP䁜N H0  % *!7n*@?%J_9t@(|'00 mSH({ |Eʯ<؁0y+eC} W; !˃lNz5"<`KN@@qa9DiTt[C%K 6P~fLZF4э`)zeޘ@6яf#Ay HF}lI NpGCf17c#5#:OՠQuP f\z5p¢$V ! ;{'8@@$<`,tu[حEHA` N(#YRkQ̄D ᔔ3DYR@%08L`E )p !V BG"B`T*u'``Qq xؑ< Ў)e/'Q9eDPj%HQf*6@LeyB 6ډP:@Y/r BFg)P"LӦV)CJ? T/U L?Tji?zAT + pͤ" 6L I15ʉAxEh**\% Ҟ *"4@)Rfi odoma`F Q(p5`+5Y4pH,!t O~ 6AGB -Cēh:Nbvp2L4$J.C'AXЗ xUA}h, N'ALE%+`@B$d=]FB(mo_‚1^HlKB߻砇.褗nzzPl"#;>_#.fJD?A 2tPr#oi`KО<[/)r@(,nB40vJ+B: "lYVȨbb7'8`s8#?  lbp%d  :\)! PM'@a08(x q:%%AtG&` )ZV-cSJ͵ ./Z1M)!E:Qq IPdқma#!M7U6 ,E,=P) bV` hИB5%$1'LRtA Ҽ-s4aT,MK&"yH!,1'!,1'!,2'HP *\ȰÇ#JLxċ3jȱǏ CIIO\ "B0cʜI͛8UR0`@Ο@ JѣEwDʴӧPJ-&`b'^R9@@|]˶۷1*]$1@z LX(VXa%kL!7AъMl+b<ͺ)X }*Ia`g& >@U`NuO 8.pËSzy,p|BٸOBe H12A^h u T+<P{rVZ2 @$%by(POa@8L@ig@i1 FI3nQg T+XcZL؟ ӶZ> Zp@t "K\-Bbd@*wQ(;0xN׺72$*&z"*fKHVbN<ra&dMA `y.jzw&ͥs & [3 v+Rmw)ߍ/x; nuj|\<]e_eՌU| LJ.A0E9zAlW^J^c2s70BFv X.ȣ5@ jD 31 1)Kc4 g2A2T%Dm@= NlI6xVf@_~`e<[:#.@A)D|p.Ԯ``* b&M 5[9^1aC  K!.924P`=N 5UQ⮌`Y2qH` P/uv'd];ԫ@$@JĀpe8C -`' xDwQ2lXJqCqy y@P@dchP*hwh@0zh-0C+-8A 6|OeX(2 0F:\IbgPAʋ uL A7 D`9/YʡDS0 |аiw҉ [1$d,AD\l A$,E`PTF"}2< 0xTt* xxwPB :@g3 E=QT p?PX3'smb~vƐy+lTY R7HY>!F ` 07s R R6D8YK &0DTo IF,CA76T}uK;ԕ` &9 ^u n21BS ,5g(0Yuu9Y<%ȗc0N ~ 4q  H6%t v0o SPjp0 4)0#ڠZ)CkKB? !אZ5X/@U6q;sFư᧐׉Se2?%/g4Wb\36b*Ƃp"yUf-!w5Up;+D.a],à T]$Cu6[QtoE%xc]*0 ~-D4R1f- F[s]_4QP$J'-8Sos_/2\z_jsw)j+p%l:0A| ⒄7w0Mvw}@-\"騏5Օz**+8`⋠z3KjQe*dW2!VQ=@P2Q+/YKJ2WMjPT&=_b`*s8".J*՚G 0ŃS c MiJzC0 JsP,rPqK k`'pX+2?(  5:S۔0Od# I*@ x}:P # @E0kpM9P;; D $ 27,0C)@Q;XZTX@aD%qH3ɀ 7 CA'Sjt ainT` RZ>ps~PY(gs0h-#X%@ }0n;ϠSĢKJwK7x ;Co/N~q9>t` ,W'PSp86^+[/[>րpJN k9!P\jLPԐ:J%0 9\ /gBgSxW<`9f9M *@eE` ~C!<nBP?A ||{ @ ; '`>L@*Ae_|zsv*0Zq280@@8D+u~g7g`wKV&wZcU."pC"]t]bZ!=5 H^!%,ʤ$1w+(7"ꚛ¤qh-nNdM˂df^3SAe &+= 4 <,V[!⵸kR-\n͎d\+=`uJȚYX >;!Xy 1 Э0L¼b 2%.M 00-,*2l" 0 !vAVfl\^7m!fsanrrpu` 0'` !mq0{0q;`O !( P * Xf!_!X*Z-_qFQ0r S<P hpu0c0@ @ !a0@fؕ+V dbΑ0 @ ^0`V `ۀ!-2}@0,۟RO؈Q^0 p70u  p% 0$*.V17-$R)9>}#"1uݡv )= \}&30K@WQ`>cf:-A+~)ߍ!ӟ'Pr"S7݋'>PCf50` Ͻ>Tl}2F^6 ^=:M]"fV` &3]7Pܞ!72l@X2.)Z| @s0 p;F^U 0 !>($ `>tV+)Z=αC}(D0 c]e`^*efV `'PKҳJj>nA2@5H[_ᨎ9آW&!&eʏ_-'d(Hv "B^S\ب$-6A'C^Q[#BچWH2)H.47b)ZȁraRYAW8|/=1_vV`81f_'/TepXVAWwe\&* \&8;' w 3@"= FJ4#Auo 1aB ) ,@@L ߄tH,/l'0NwG%MDRJ-]SL5męSN$A0`@-@ ׽c%)0`:>  , : 1 a? 0L`C]( ڧ Xbƍ?Yd? (7 !.V+7ADn^`:Nj2F4$:`Qݱ0)ϝmm , p [\ $9n_,P@ 4^x͟G8x+`,yYlOx2n2.G0X *v#skct&b`!8c!o HFo1Gwdi^ 8jW:( hF5g_ufpq s@0RC&p' @8q5'?AjA+E4QEe% (p( "`I.\lj YH!n+bYJHO F* 9~ \X4RFPh g(@j4[mP`~ J&OAm\C{3@]HȜ u! \ 0&`ׅ3 hGF6Hǚ `te_9^̈́DE Z xag"K)~&='3 ^ 6@kPb1vJg Vm4^˄'0Tfoh΀0:[} &PGgJ lWh2 Y1"+< zO{WꙊPoww< ,  nL808*@W:a5'I0hy'$U>ЀD<bkbxh i! _g('Hz!P`z4L`uCyo`(B,XsF>{6\!jN2ݘ}@ ?rbyB,bC2ь!aX1)1Ђ r#TĢQ偑sܣ36ґ,}1;O@P'ĢKBrd*UʝHC &ӒN`I$B^f0aWR&rI-JB겘D09MiӚtIB[^HIRӜDg:չNvӝg<9OzӞg>O~ӟh@:PԠEhBP6ԡhD%:QVԢhF5QvԣiHE:RԤ'EiJURԥ/iLe:SԦ7iNuSԧ?jP:TըGEjRT6թOjT:UVժWjVUvի_kX:VլgEkZպVխok\:Wծwk^Wկl`;XְElbX6ֱld%;YVֲm$ٸ0 !,M',@+`A &!,0Wr'H)PC+V*|$@^e:_7'1d & m֘,@*{tҦM' DL;Y 6SEȈcL6 *T[AQt@ǑqЌ@tALAxUD@ L25dAE L`>En%HHOBM! +V0J9 L@La^B{]P@!Tp%EeQ)4+u:9כsn'-)s'&ʙ(-9І)z.Io izN )~=L yR .>I0;2 /O-hNQO@inaP ^83R;=VQ=~MjS0@SSd:6O^j,T fMHR*4d"X -x]CdmQ0@ 6QkOd-P h@溷MK pF0L6-]P@ N$vA0g)=0qK@,0ۄ'P e79t-Km2X) 2=D@.4M=hlѼ%wms\;w5>(៩U/UA (J6 O 8 Kx3,?l2k Th3 S\#H:Pv2`7 T D4V{O C%P4kYb4ż-' "+vP!K㞋 q`~Ɩ-Yd2Y ZUB254ă(D @"q8 7Fnqc9{_Φ1:j@hK K/!$FЀ.U0 4CpןM Y@,@:9,Gs$*AHpe9AlK,=xN|c !{6\` iE A!X/xf~LA ]'@0X`(t!"N{(AA 5G %6|7NmY8&Pdtb.>p=&} @Dcg# HH`ت76L&gBK5_1"4>>֋zBE&+7&ESLBF50p #7";hU@Zi3Z"rB#n!%^fXqD4`bK$5U1G$ Tۖ874'RK(@e-@^ _@ VBK1 [ ғ! u{i|h$=̔!^!;$FoQ†"#$q %ʼn*4 <`= , @`@$0U< m;B$V11, ";56 -Jp\g9H2 >c"D A0 7U:cj6-EHޠkA ( A@1K L&Ȝelsh pQ 0uQKC`6@ (o*zK}cIpZb)TGn9d&aFD V@bIr9%O!P'A$)/ ċb$h!/iA Ș:.}M,QwuÁt'=%7d 8<D[lon^O jUlю).G `@o C`C yGZ[R.H0XxG_ʱ)TO@2qN" /0&#k~ߤ xD݁?# 8WЀ# 0PAAxA "0Ty*:GZ07b |0q4Rr{L_ P#sp"*hÂC>GTixGHwUAuW@ 0,p0c [Ey?"U%27 >0#p !HI*&pqQ1RxqD03qoP'X&`#1x4X sZL'3QgYg :%!vM3A1Z14hэ[by^BSȎcCюǨ0!,0Lr'H*\Hp#J!ŋ!VDž?P"E*``qȁ؞m$hozMD1`8~z'r$Az:V +BW(>eW.K'P`BP:!C`A`?'xћ. aLr('~듿/q~ɗF] ( # %Hh2.M>,ۆ6@#B*h3 r]!P5 Bw8ygLPm'P@yT΍BIE3`ӆ## -Ă@@0ZJU;!ɫ]`&FN\H&$CS,D5q6"@ Xc n!@EhTQ:d  A0gsM>]}4cTr(H0}^/PR"/ 21,c'NA| |VAG-@BQQBܰ$z,H2ъ B*xTb4h+Cx@nWIe\S doX^BY[0I K tXbZpB KP{YpFDoo+@hGD,8%`a֐<|p6` bDL3 "WgU="yBJx$jv%db$ZqA+BD/ԙRAE7<!, 'H*\Ȱa! @ 0@ 8@j*H98 D D &0Ɖ @8\ʴS%"hɖV`ѣģ)( )1G U`A%TɓfQ-t߿V B#M4x'D0Ċ jR(5 i8s,J~L6װ%BФ~e˖ 0i ֞2`$@j+[4IPHPАիM7 6 ekTo*1A0"BO^4!*8 !i4q#Lhuq|v!u4!!^HT "HMT_rxGP|8#DD$1уuDAD!xc8R!JS6G!eQqlA6 { 0DM1! u0C'L*@0 GC A9"D F6" >E `@*! hA0|+ߍ@ Q '(5ȑ LbhOE' @jd#"0>0jfY~m@ keu!4Uv@APV分O.G"kukqutЁ 1RhIN7A: /G0"&7:@,,C&I&r,@́xE0ǑGAe 4Z*,HBOucgIƥ('@PDܪl8"(iHhxw[FsHSx)x" : *ogWG$ ,!hpB < =8 H!( `"4!MSO1$'e {Ri4ܐ"Sq@$&>Q^@"әǬZ0Ё0P `IIZ?%'P82LhD-e3i!k&$%kI`@#HRĉK)I"Zn jKD  )b!`q'[@  (&L"D`#i#GHr @LCLJVrd' GA"tBI V 0 "AhxILD' a/- CT$69IHV'!%`&:AwI|ZGVF)*Q҂*Qe3&':̊BSXVi(Ѧ6p1/Vʈd$'~\TL*|QOH8B8DvpPBPR<5E)QzuRюD@!fHȝp$# m'@\c*$(nF,hP{ʟN`F:"G1p@BT@CD#6jgB*E"M>Uv됄pZ@JIXPO'|߮@ΰb ̋r,#'E:m0 Rc"fYT(@( E 8H`jm1ZbP&a{-Ɓ $ac*8A@]b@J`i8gZЂgd%5D0!|Q*`t0h4h4{NCXpP th#Qb( α  @¨ypc2Z rР` 4LcL2eC@2;96E v@v00j"` @ H`l'a.}}@\0x3ߠ :[QH g zPcN -'u@u=NITjDb p-@Yɱ|!PG8nl&2 G 4 (@ Z0p%P) @ BPY ` (`\إyD0 X7 z}X^ OPp @T_]ArXQח# 0eB` fPdx@SH4`İ_LvhPSp , 5 z ?P ` >Q}2'/PLV"@q82 (3m!1 0=QQaJIIUk$RDВ\VHP&aib K~4\`m@% EE lPhW@!P6 +TmAIhuRY$-_#(0F xQDg} E~rX@ѓИEٕXNWI4OBMdLiLEћOIiz9ԹɩIiz 02y۹N)@YܹYzoI7} W)*։N  ꙟ`ڡo,(-:4ʢڟ1j=Z*Z69J=ʣ*J>j:ZGڤ JN:6zQZ=-jT:Pz`i7^*6az.ڥed:QiE^zZi/JtvZ?X}Doz9٨B |zij0nZz*2Jʤ. \9&zQGh* ZJO NQrZ *A5* ڭ 14zZjz犯ZZ⺧Uڟj:ɉ+<%ʟp* * HzY$ʧz "k$;2+ n#Z*/;ڪCYzʰ LM*AUkO] >*~A;f h۠Nc۶ K!YJ ۣYxkɯZ2kʱ;cǺx{劳5;ڱҊ{범dk+ '묲*Vˮ뱾ڦ K;"Js٪mk{KZjkk귄KK+\ʵ[닶Ka:;{ī Zۿl[ YRˬk3 +|ܫ:ԋ#̞+ ̠ |k+ \tZة*w]:J9˩8Q*|E;EKkW̼Z(˿:䊦ILj\g}z2blF{r̾pJgJp:9<\+#kz{ JuĖ츘>LǺQ[:Ú[Ë <[0ɸ K\˾#̺ ü(!, 'H*\ȰÇ   @P0Ǐ CNhBǑ0cʜy7L@@`ϊ4 ia@DЧPiTМ tj::,('UO-Sm΍$ *@+qFBcJȕؐ"pK$B-p5$@\@*8$D#:%8B ֓;cP DڕO7䍖d"L@b@pԤ@9iA)LpOΝZ(P/>0e0xu1qt0PY\@!, '@AA 8`paĈ'3AAHuLs`"bAC"E 4񢃖4<DKGJJ 83b lh'XkYX:ЈK!,2'HP*\ȰÇ#: )̨QǏ CIɓ(S\ɲK_ʜIE3ɳϟ@ :B*htӧPJJQU6ʵׯ`J0 d'h@@1`' [' La-h0w0o^#k̹3Ϲg[2 ]԰c˞1_x(mB`}i<?,S &.ґڄNy5J8B G$4`.C]Q DLx& @؁B)נ@A8Ѐ@s\ >A0\@0(P_L0ߋr 'JY8sȬ3=&%* L6ًNd@JBX3eA/xKl {2P9P;njGgw% Y'y9 ^%@rlW9"A骬:RX*무 k檫AyY6Zk챦M` ! L@JXY@G  q'p1`Ē%Ӫvxvg EN|M;Pf:{w%<*M @ PAPVR*P`'kܻFVhߊ$n:, sZN0 TG h7!F dX5m`G0l=R`!0R h@ .0b-@B["P..(A~i0A)땀l'(Y3pCuOpgR!1<@c d @6xM8FxOLLݠrl>B @@M@Yv5 V4M&?Hnϻ$@ J+u9- Ḣ( F&B _F׀"A PЀ :@( Vq:@ @lxc x#7B@@n& #H!H :lJZXi@B4o!`D@l!0P"KtFnp(JzH9yǩ*`2/q Tm S@b'`M }p<. F,L_(@.P \N#0d@>vzcB t@@qJ"Ą i"❧Awl,Bѳ$|Qd%Mj)wL!@ hN} ORuFGAr -x"&sNS@`pY`*Ӷ>:ȰR`H+ P(dHX` @#56s H*dC.{{\rv bcbaR>>[w; xHECE,`N_C8 X6Q_$K:gsYhomK161 @'#!mM[r袭 (YyX#(2,|1E F ޣ3)/Cv̾ʲ@~`-! _څ{@cR! KZǐ`qjCl1FU(INgHAYs+ߎ _?w:8ؕ^ f+ 4pW4kAE@`qe $:1i:K˘J Ȣ/ " 3s2Rɹw 4 -HccgR *Kw d0*emXxKx"=NLX6Q4)/RḦ́1%d4d"hO?y=Z~85F,? #`ߒ|cHpQS ֱ9}qYv98[!E0k#+б Ce 5ڟ3% MS``cA8ٔO}\BQ@&kLͷ0lbJ#N y;1 ,3/AqCeѰk00Y d 2 eX7!$}vmշ*q}ׅ) KVcD`"7 `6W= #`pݶ~5cxCW^56/Ӏ aPx 20N)S 9bҗ,16L70Wrǰ ` Bx ,H*Z @ 1ʠ\ `wa 2'N. $>P.#Wȅ(^8>@\"}I s(az PWqxQ61׀ 0ٰ ` 7x6HR(5B>TxH(VC0>*U%+P|:%"W$pmvQՖPyUjw)z1om,d؀o I! | ` /t`bW6},(gA(hw4SW?ʢ\u1Zg0P IXCBaS8-7' H@ 5Ht`Xeגn  {'0-)7p?EY 7 *3 > ,E d" C 3P q <@J/&x/P }  5I"qQpÖZ2Vpy  07C@`(N40!`ƈPV~C7t S܂\yyP7QE4 0z% =¹ŹX:@i'3Q(*!P5ӂo2 "50J{qhrw dMFr~o>4aTsp')Yؖ:+r@B;D[F{HJL۴NPR;T[V{XZ\۵^`b;d[f{hjl۶npr;t[v{xz|۷~;[{۸;[{۹;[{2+!,/Q9'H*\xHŋ:8bƏ C  @ɓI\{Jrr -~ύc4pwQ APXDX)Q @l3`@ CP@5PӍBG2 rf#A D 7-NУ0@BM;dB9~Wv_p^LMЏ@ PČ@2ćaSo =&BZ逦F]""gF!,h# 1p0A!, 4'HP @\ȰÇ#JHŋbȱǏ ) 6Acȓ(S m½b+cʜyRuz,c4 Ц DJA ;0ט:2!۷-Xݻ$KE x 0wÈ? FApULe=T%Pr'@pS!M۸s!eZ`q6E"0h`G2 `a",` ہ>0@rH&"ܼ@ Y %P@x"Py1!0qc!R@t&5@`Di0,ADH a&PI0! = #g( I EbE|Ct<6Ԉ&1#t89ߒQ Z6(dN@kNf9%D'08ftaܡy 87Diipi29Ab@ji9A PYP-R2T YI9yC2^: F $JA`9g A }18]A PK t ŀ-b5"M) R bt=tBNH8DXFg%@7кdà -%:pfC&;A`:Aи%2؀3@ ` VO@qw}3t(J{&0ՠZA0(P`AR-'xB)O a{2kHZ.p ,00YHGX`WM ؈Hh A;z†waCr^i-y2idc@!*2D>^Z8 A3~M O(L˔ 0F r E7:>o\H9"BVGH`v QψS% L AめC B &&C- C34qt!C.!7œ_6tS~d?rog @!/b pa6Tp`KI s0Չ@P1WȢ!xFsD$t" DDDRom  ) PT 9hnZGL`$A^>`@!5I' ; v, `g,@4@LW%` @yq]woa$rR%1Ѓ cW09(7Q$m--Lb+",L@t%F)eRN:0PAi=Q0!H8aD@@E.o@lLi2Z8OG1!30*yQ h Pq L @i@Y%ۚ0 oK&( 5B.gfOkF2aHfҶdtpu bOR7Dp٘@> vM !@jԟl.D  Y r kqt2AG (:w@& T@}Ԙ*ZwT)/L@ݚ& q!p9Lqt0Rx0V@Pu&Pad&P:;2&3HH"ƛR}@.& D^H$%ON  rJ`xHjyv&H"1H7|_&T 1 p -y.iR  "ိѝ =If. YQN4H@ D 廸&SAyJt :KP jܬ"4 @0& Bh(Qb5%B7 B'z{&*Vr\xʶPW`e!e+dm!$TېqizD?b Y_f@]WKsr Z?_pc2!d zDФCy5&ieD$!/G8ӹB%,D,!AEm $ @ՈT.|bU5 v=>I$ D$`8$C&0t'HrH9!P/!Ro(av|G_dT'o9k4M%{(@ h`k @auw#s "K##U5 qIVኚY![ 82o /w# ~@ 50`{`2NP#$ @q T %b @y 298x70$ ` Ȣ >=GAo L8 Q}%Pİ):0AO` @HjFQ !i2B':s%G2f9}T:0% H 2*x9@S'7 t, r]3/`ms! 2w)P 'q1a/ӑ9F @y j S#rwQw|pi`* vǪ"`;s+`m0+P1ڢ'`@B%"Ё?ЦEnpi {l&a7(Ov ~0~_$­97^W ;P!${Ka(ò.*23[k81<;۳@sA;HD{aHLP;:JV R{Z i[۵&^K`P;d˴f{HjKl۶Ap۳r;9[vkx3|~,[${ ۸;믖{뚤[wy[غ k8{ݗzۻk;iʻdּΛb5 \{ U۽!N;TkG蛾B{A[;[s{9ӿ8r3<5|6 1  .8AC @\ĖqHJċN\P}1T9WVj\ _aLcL^\aqh<kjZps\ur|Rz\}}|DȠ7Ȇ|EȌ<!ɔ|:ɚ|G,i5ʢK,bʮ쳱[<"eˋU˸L <aTllKM u ׌IB$T4t/W=hL7* vb'1 DM* t`^<@֜ЮS}'@XB43ۊߚP䃟7Tgnu߭{; AT@)\S)1!njR;,?WT͛ .$k ADBG>$>orᖏuN"ߺW UP!,/t7'H*\xHŋ3jDCIҠ%S\$˗0IByHcꄉa͊9w PCHEb@(P1"Uʵׯ {<KٳhӪ]˶۷pʝKݻx-IqN`t Fx1CP /B. ӊK8:@}0A#fX%Yd H a;&k®>Nv@6WX@J<,k¡- mۋ Эb[O7M}yQS>uAiBq8_ ?J&uRϾ w w}߀R 'tRࠀD!,a(BA0a *\Ȱ` jqaHɃ O\IgBSqA T,xO;t8Xaȟ 5<д ‰U0Y!; V TMX1#R Q[iR'x;z6} vmHB pD 9;& BѴy0c،Xf;f [`w55 VpYDfN=ֽ}c&لb6 s7YKLvÁ`:ot H~މDx蟂rVAx`2B)` )Y} w#>#O%H  EB`a]H f"@u _a:"Ce``֕i$d6hC_"!,a(+)0!A !:` $d'xx !,a((A0a&A'4( \XA!, 8'H*\ȰÇUP x`BEȱǏ CIɓ(S1q OdTI͛8s#ѣH*]ʔ!l5JիX;ʵׯ`IJ8a˜ ª]˶COkh`'&׭߿µW&E+^X)07&`B3 6̹s/T$1!1al̺k^s}_+pȓ+o@ˣK^B0`h&Mu7N<걝Kd Z0O~M-?ĉ0 'L҉ ]d/5 LE(!Cu$v l`8( @ T"x'!%Z7 @,$= ءAh"Xҷ>A`8#A%$j Ve0;L6Mx#'p:*w28QZL{ (= jpŒ"P t$Gg#x驻@ͨ3PEw2*Z"B:A ͺg^뱋E M n C"Hl"_')N I Bզ۶hЋ>$b~ x#j/@ 7 @+*$jdk{%++Rf+BZL4,T/0/ L>=BL@A]TW%]X'3B-8dO3a|S=}@/6c HXh3KT;ݒh3Cmh]muS+mu{KBnt#ekSzN{lAF9=H}J8#.ՅP1Bu܅[vS uA3LM ;Iev_h}ڷ/|}@/_N<+BVg5cHǓM{#w7ρq[f'` `>@5PP;2!ag`1yYfַ P %s۳4TdJQP+ -H.t0dрA(|b3Ɨ"H.0)i2',p>ťO y'5Hpq l,2G7Qӈ2ZT 6!1<%P0NB@f {^1Pq<>CN( 3x oHc* {\A@HRӔ̢'UQxr*3 0`GF4#hƃd# %U/ `V3gcԱ Tp) Hg"`A @b`h-'29X |8Qfc`#0(DL D-8R{-@ D E kJZ²u$iB TA-(A @h,xF? 5 $ *J݂`xBk_¦B:H{ڃ-Z%x 8 ;Qy"D(4ac $`Ē!LH1*cs0G<2P0``3pIr>  =1 㰇 s@`P $0pWuM7Cu0Tg Pb^u\  7]kv pB5? %PxN,Pʠ& հ"8 p[Ey@@@-p 50 Q8\,Xz/ (v  {- *|/P4QFGB :)W]\S >PF+ 0A'W3P( ,` P `S8_׳7.h/  P @ @ |UX+(q>B`H@M3p 4 P  ݰps 3s ְV p ̀/}5P'upUuW['4"_G \ . ]Ny РeV018N0]$P e0؀ p 5>sppW ?B@ `1P}D `E %vw@ ۀ!N `^5 @}굛V ` ΀ ΰP@g0`v0@ɐ ^P"PXP`԰אBSq?Q\q 0=| _w  &v2y5Yur @Y\ 䔭tpa ` ؐG21`  cXf ui)"%p)pv{p @  >_ r~p ҐMݤ0f e 0>$J`'@rl7ҧ/N@}N0I638\)`$)YXk X`` z(ŝ B3 ѐ =%Ǟ xp,0X` Ӑ %a)@c;6 0X3&1qSВY0>=cĪ :0 pP,P0]ys +pK% 0c ; 2Ojbh3a)ձ *@pX=O*r^WT r hv PN )x  pi S2S3#  怉. *"1%"'ӥ!5@lZà3(0z?p$iqf૒} ,  [N0i7_zs P5>-=5B*HB1reQjEB/PZ3 pq)H@+q(*Uk`Pzg0W4P %e^tcO.܀ Ԡ=[JąQ*ɐ0MA#uUZWQ@$p֐(@ HNhjY/Ї:@MtV+/j9V D PS{lISP5;]շp6, : Q  ^9-t3Y>3gY x' 1@PP 6hҐ$*Bxpc rx !hXW#ɰz0 H 5sX+\ۛ˼614&w#CP[ P 8P p [/@ 20M(P ۦm, P 8lTj ( p 7, 3"@jxԠm0Kz >3B 0 ' P BP4pZ50"` whS el%4 pv R>0P{ 53s = lGc 08 ?F L@p 0g  P px@Wf۫:%4` Ḩp iӬ{ } ѐY`_GrzcDp&sV pp=Amؖo<4  j l :(Pp|͈ P}=$0` *8u pp 0R@W' `4vYYpR֧),Űv2pr=)0p`QN𓝻l+`@ Mh٠(!`Íp  6 ٛM%0(@0=h P 2$R2 :֐%As|$ `m3_3 p 1RX!_u#% 0[~0 vHa8NRl-8_5>;~|tD@ 쵴o9$ ЋotePU!e= Ao&p0 sbg5_MJ(.B %FQ8 qPIKW!\&RJ|`Y}3k@SG41B;3ھS11/plOa? 3j-$9r@s#5Y>?m!,2'H*\ȰÇ#JHŋ3jȱǏ CbIɓ(S\ɲ˗0I͛8s̞@ Jѣ "]ʴӧP T*իXjHuׯ`ÊuٳhӪ0Dݻ>۾[q.] Xv  ̸ O 3A8`B @C4eE(!W&}- FM(I# }'$Ar1 5PA‘-hy'h@-F8.0h7!L"B+4pC %hFAL -" ѹ "R}#%i(X[Dr磵*٥mal 駗2#z"В] 4L)j,Ag6֪,f#|^-8_'L,+."v[e@K랽˗uA(Z3Fp @h-l ɦkgw\5^fL 02~ul3X3At@Ksm-̀&uNA7+DdNWUQ[K E - dÄTZS%J@ dxW,hGa$Bw4HIgGAA 2-t5.a4xA7<8i"T+n{wy9n`?]Nɷ'X^ :lBg^w_*;hj  \hl]@"=O"-d; D( .5`H.u@ !\R,HB,*@M R d/@%  @ X?J ]D&2dQ11Lzq@m9bS@w1cO!)1|n41>!@).4H&El퍀l#`Td_Ah(@:R&vYBԁȁcL ƒ2SDBv9wX"d%3{ xPT.fSwG1!99I2~_rZ0+aөZ `i_٩:6 86U'R˧BqtQ=3i0L)r#ԥ)#VO{22EI2"Bb!M_ҁ^42;?:R`BzS"8E L-#VtQiPj5 }w)[X{\}hbj)ݨLQ UVCN0DIJIBWTRVt[Jց|hv \ S0~KXuz*\"JVهh-B^{rd+-S;eV@.EkQ@=rMF#&$J | H#%1  Bc"~w(A"C[ij*D A`T*e!2D4 ^|?ؚg6̄..[,ULZ@ AE'ʲ~kS8.)UikͅI l;L@XA|'@I FM @CKh g  {*5be b=(O3 8F.{B}rwMj>&>τy OxШ [uNtog5i?5ir607b D9Ҧ J/,u4pMP, p-8Sp2* a2Y.t(A&P_k1PYFǦ8(gL'-gXPK7Y6>yЂ OHA&qeXP /L3<Q<_|Qtn?Hp!D\&KItzJ@Tqp6%݇|@@9\hU`纂 ~ ]!?ha`^tp Ȝ@6a` ~A,lAsO '@*\Xj.g^{ л@m AP0 v3+S`P܏dLp7Y BcsGpăӞ/p&`i@0\ύ\D00 $. v`& 2- 10w0 (5 HYeEY 'I >׷0xE4#xMB>>KY g `E| Az@IH I@T'! ^WJWh#j'+`" A){ &_4P,p1 CVn!p .A0> 7/#?00k2+ 5!b(w*=C  Qt @})k@{n! pH=p nTd\rO3A(O9rW-6  [4a B3dNH pS, @TB~B JG/,  pi(l@P*@ ~Trx&dv(0 bV@2'PM(>0 >rQQ0v* %XE@0 *o22`j$JHobX]\#hxZ"&0&0AaР(0` pvp0L4w10o(P#AL5؀i%0yE"-0( HB7(Rv@vx, ~H`P $h0w(qnng"2 ,ω|p}3wE@ CF~ yC qSB8@P}V4sH0LS6~x- {3: P9rQ<0 q!B9bt)RNpr 1yVyqG^%q WTZ=%М3Cw7)B Hx#`ո*ѐ Xq)Ѝ @RZAHE ʠAT NX27}RGCE0חxG y)+Tp@CT:a!)З'*sX-D RvPX:@)9٪׍`cD$0 +}Y< (` ЊPМ*t& א[}e]Eٜ(sW + w2jX:5PCLbrAWB1fx)p 0O0 +W $pF#aJq#,P0 7D 9ȃ  V: A)" 7|Q)%);Rz m CS s&Y>Ik^+Y\YB!oeb60Fu7;z(!0 pAHo&!{٠&3M&*"01 }#ox[c  a $/ЅY`Ϫ *pŒ 5 >`1c"@V $PT+p- t*<7)3ѐD 8#*@I[YNSP/3BGc? E0)Jǒ0,Ĕ%k*Pj2HJ7b+ &yd@ ;oċE[njh '<<0Y:"ȶc"p*Sm p-nD)O  З7p PH(L)$:@wQRDLCܠ ~x @[%#/GGCav#GL:  g + L% b*Ei$( 3רC)#B0vf_ R;D`rw0B`OTaO>ru4Q*fEU^LTC>[-Q0o!i6;\巜-^4{J( 773h~QR >f9PϜ'Y7W @--8^+_V;t'@V c bE3KRTR%L孾ZWN5:W.])ojQ"P1n"r !!@X)?nScc^I;fu]򨜨JW!ݨv8z>Qbƞ62AvW8r[m %s.\ vrudR<0)i=O1[>@Oa%%E;M*UNFFHɩ9nVVUAc_fBl)mr0s_vݡ|x_O_>?WCOr@S!o_?}?v_?ȟpjo_coޟM4OOoF_D_%tCa@ DPB >QD-^ĘQF=~R@%MDRJ-]!I5męSN=_TPEEhRM>UꄥS^ŚUVU~V؝^ɞEVڮlݾwYuūn^}`… S=X⼂?cɕ-_JfΝ[jZhIF:iխ]fZlɱi]vn޽\ZÍZrP7sխ~]vٹ{x-7^=CݿG _n~_?N@@MALA'B B +pCC qD*DJqEjE׊qFɪFqGG! rH#*H%JrI'}jI)srJ+eJ-?۲K,$ӣ1D33d5ۄ7S:dA%7"} _H. Q!@ x6M#'p=8k(}l8F $zX@PF ArĖƋFAɇX"M H @#1w\b<' 4N ~p@,0=# u$DŽ!d I@@B%+N HX6ˉ ƵP` :El ɂ>7k!,HR'l PU]f0򠉑 K9elNUhjGP׍4,ћӚP 9a 3'R #4 _\d}u@HAH" } Xh@.#isS 3r P*7 H"@A8«*D&dy 4UiᬙUBM >@Eesm霸5, "^49!$A89 $@ m2ِ,xM:GAd8V 5X?6YĿY*6]z'9;5#Cv!Mӛvܣ'V*Z.Ab1@'" ħN-2Db_p @lPqG(0ꚢoe)8O7THF8Sd B/ё<hv'mxT!&nL"4A'fnE(4(ixwXЁ,o H..X,?{Pc =HFi XqBX6 RBvc(ž ,wɝа!oqCH@d d@uAj}̀xev䎐ݍ/]FbIt7d5/b;d{12A W[509EED&D"',BMu =BAnD(C{NA6RA#xa;p*"(F_&%UkӚS7+#$!M2 #ȑiM`Bg ~JƎs- ACsM9t]tۼM^5@"H!(L)=nvzm>631BR$-c;I^?lb-Q{ >:OBo/E>DR?Pʯ J@!,}H'H*\ȰÇlpċ3jȱǏ CI@*$\ɲ˗0cʜ 4sɳϟJѣH*]ʴӧPJJիXjʵW6J٥"!t۷+O \[LÈ+^̸ǐ#KL˘3k̹ϠCM^:5כۖeͻ_Nȓ#]NvY(ޕw'}Oӫ_Ͼ.xw' HhF D .B >M@Q@li(+ġQ(Ѕn(Hb ؜-~by1HcF6Vcx̸Q0!A"v>川,w(IP0A'QBPЈ'DigGg6Hl'd TA l`0Mkrt=CCP@> Km|2NAL@ 3>Ӡ w8#LH2CH @St $l$ADl nbD D@ q@?\ ,lAĨaB FL/#20?@xH"p4NFO$+P{@ mPv;$5Mv ؎k- BR D$0@ ?WBN5:D p0A6@,Cw P Y.ЏT: zB+^_P1@vK)L72(AһCH9+2L #&Џ ؆@@~7 - U PxM YOGyPB +XR dfI"iDc 3ā!M @`C c _* n `Fa J0)V*@]Ihӧ5$y";F@V{T*W/u Mn<@Q0!3VSN@" 2l $Hb c"ޔxх" …b*q䘶"AT0. hGNe!LZn!$Ԏ D%s'Ƀ|]@1)cC@HQA6sN!qc+ۀ@MHcݔWNb^G/L8 .Ibj4CA 22 @9=92dEAs8Ljhj,P0P kTbX$F*-LZT%&`$٠%G .(N c QZF +` 8d1d˴ y9F0JL8A:/yW#?tG(^( GZ - bK\KTv䃸oA =L5&i :T,AT!(zU T#A {a< UvS|N9.{|GJ@&ѭ :Ej /+ڸI8"j <%]%pDCG4d Xvɇ& І<9@e6O[#?.#qnRh(äeȉÁO Ngz`XߕZ `Q#FtQGjީ)!,^& <'LA "PzaL@̝~6t@ 8!4W)!5jA< @Ì@*Sh@ ԕ֬&3Jh B +A3 'e38A*]!~ F@P S0 06tn鲫%30 /≘(d K62lJ:A!@tq$*L^ ć,Gn(D=Su!=5A @E8@0 ,ad7ж ^@B'p-wJ@9 |7o ٍTmr;S>MFrل3@8O]砧ę$LP,GJhTC)5{S{wCDm$ 6G/}C@|TĞ5AaC@:?tI}=JԝC hI?',,&P$8 I / h M@R  )RET" H!'l U`\pHb\n& H 5G|pr"~AnD(rC@`[cQ@4- i&`2xcǐ&r7 +z@%ѐ} HS 4 &Aӑ@%L [L! (aLT!W' DK* 21!B#0:ibi "&#II{I |I U\LWQTM`1 Y`  ze,95˟@DDT.h@mP"!4uE29y"|{hCGJ T!4Ph #b"tГCҞ !-Tu%A!U2!TF'INzq|HSJ֯@wt@D іxp^ DHI_׾ '"WrNCDXNLl'$wV.#ZAȈ c)k<'YJn)fc?L+Zц6!pٕ̠z8,[NvlÍ.â8!膤[4׷xvTBHdw?]ւֹ|B e@XB^^wfJ?J$R"PbC <B[8(6̣>D4$U{Gs@!hyp wM"KEݥ@`R`G %퉍P[r 3^ d{Ș'9$> xH34YrDH t m'o Hr,zЀ3MwyUB9[0 -†"\DrjC5H@Ҷq\A5D܀Y8֘8.\P(giO)bY$#,l7ڰ9So&n{t#w\.u]hxL#q:vi5^+ r/s0&FoYe4أ1gKN$5e;V)3,B^4iF^5QB0st̛k ^<2<>>4rICZ ps!jZB$Ҩt6۝J"Ԝט0".J(bY~Nt;]ƿQus͍쪣ģ3h(s $ PQC8eӗf̓€  50$8!W8y?Vw|Wc#zB0 P:`P `|HGR!: ~nV3!{u@fwW+=/0@ @M p'~sA%zI4P(+G;p]Q8PI[l CA vFPdPHSW8x'`u`kA'*+5vSaqF N<0 !@oqe~'?fx' 2P=S  7[| f" `Se#,` qSQ84$6!p@|0OQɰyE0!74V0` q0pp[p5 h f M1 -} 18@ 0%3zဣ_:4 7$); q2մN- ِ-SH{P !yXLYpX:Pt_t)@2 [8y%P\}`  'à Y%P$ 004"q d A[ 8lPW*n(o` ``3 ,2'BC̠8 hiyCbuE,!m=V0`  86?  x9 l9 16pKπP °]MN$ LtKtgbQPrq(o` zW>@n 5aq@Y P$`M dC:57h! 0\p`#=FF:pA;E-pK0*% LdT0 4Bp/PئёVWpə%p;W%PtE0 'PJ,58{q7q R3WS%І$AqEf7naRaЋѴ>Vfq 0 :d|)ːO0/ 0 7s@q ݀0yCL/IuɄP_ HF1n2=: Bpn'3Sp 0gi+L2!*`6SSB*eٖoP  QdMSp &" ?`08D78;PW:@Et;h.-jM6 9P6rWmzlsQєy K aȯyJaR_`?t<:Cc>~BazP0]o@) 6j &U׺8Id#)ћSг@ $@E+f 0LX %p! ;ls H `] D'R6' Հ[F ܰD+k2Uyꗷr|;/E)i*HH ":XP(4aFeI*6) 뺵 R,'pOxzcqӀ 51I*a0#2 P Ұ0PWyQ 0# !!YG9`LWF7\&}8q )>LRXpOS07#0/2!fX, / (0C1R)GXȌ D#8;0B@-XP)VFX א,鸎/PfAHɛjf, *lOAȥCAAu u+0[xt\=@ 53e<( ? +UHv[یXҝrT&X*2d 2>8Tϗg 0#b)Md P_7  wa >0.r=wa23>o# fju+,`T@ +m@AY*@ m kxf\Ҕ+%(1`! Mp2Cv74;M15((*%w2'\s3P-79m5?#IXdqɣ#P'&pmPN*7W,L0D4`T+>MFhPp spf-~2 2@1`2A1P|{RoxV.߽ 2>QD-^|hC HCDJ-]SL-5jSN:qN4IASSRM>UTU^ŚUV]~VXe͞EVZmݾW\uśW^}X`… FXbƍ?Ydʕ-_ƜYfΝ=ZhҥMFZj֭][lڵmƝ[n޽}\pōG\r͝?]tխ_Ǟ]vݽ^x͟G^zݿ_|ǟ_~0@$@D0AdA0B 'B /0C 7C?1DG$DOD1EWdE_1FgFo1GwG2H!$H#D2I%dI'2J)J+2K-K/3L1$L3D3M5dM7߄3N9礳N;3O=O?4PA%PCE4:QEeQG4RI'RK/4SM7SO?5TQG%TSOE5UUWeUW_u `]!,/ML'H*\xHŋ3jȱǏ CY!ĉI\ɲ˗0c(@̛8sɓg͞@ JT'ģ0,ʴӧPb4իXjʵW|K,TB@]˶-NG{ZKݻx@߿ LÈ+^̸ǐ#KL˘3k̹ϠCMH%XuB<в5BڶMO=kA}7HVZblu n-b/))=)TɻSSޑv ̄0};&k®%]Cc+t'PtA0Pt ؓ}!$,)w-l}(b0č@*32cL4PRPU;p$A0TN@P!2f9 APG 4Xf!PHG9Pl.HO\N>91d6 Ry2ǡ-IfjBP@5Pm@$L*aXP.sPI[8hz`)DՖ;@ P,HK8 !uF%|W4I fkPA';A Mɿ WPT"IcZR{Ne?9%AAlGS0!sN0@g$(nHw$jL/bPkT3m u x rN om.>dly%PWp@d@"P NpœDtZ8 G4[Iԣ8D Bzy+ O4X[J!=LH?G'R# JM$ !,@(l,'HAÇ#JH!LPǏ !hɓ\r*[ʜIQ 4ɳ`$CAϐ? =*E'Nʔ& X8b N0ٲ{fՐ*@+@ٳi >uVVSٿhrc':÷oR=wiDDWGFNhʥOvM $@imo j5 q=[͟_7.2zlt8NG̼yW~ IE`|0 `/OmXP5 c ,J?(RX!O 0֌S  `r'2UWj pq9δ =Ts _! ^ `$Q #D'Y%$pf'&M- Q5VfY#T9M-` 餒&:I%:ku @FdVA! FVA LhDUqL@&G2,F+Vk%9"Pt#P5@t;ѷLPN;su9d@jUX$:X9r9P1Ntcebֲ4ձIc!,a(3,)0!*\Ȱ!A ŋTȱcCCr0p(Heʗ` &L]ɓ 'DȩE N4PgM7d<(I%Ap X:uN'Rj1!$x"p'EUoBv`M(wNE;Mjxv+6IIL]7#x!ptJ5lW35|a!6$ AK@w7w ~ ]ls7a ٹ&`*3!MZ۩NT*$L'Œm pNPwA9hTarAu<5(z*%E"F'PvB 0(4h8Q #R>8P0r8LD*!,a((A0a&A'4( \XA!,a(+)0!A !:` $d'xx !,a((A0a&A'4( \XA!,^& L'LA "puD@7@G[:3. Lࢌu3I@HS\5`X 8Ap2kTU [ -Ў6> T pAT3A  B'Dt WOB} ͗H0w"0@N i.h[|" Б7(H!Y"9?0뛐pbmř@ Ԥ'Pd0=t:@41'ə\ Q}+:`A`@LS4 umpAFP%;(@+:7 Ă@.$ 0&Kɢ*@"4QCV Fn|ٞnEe%5s@KO<@U= T/hcY"]DgPN m;ϿBREp 4I`>*L *ݛ'\hጐqk jźA$bunT__hG' A'T3ت<\qI9Mu@j49M|[)ZtЭoNt\e/k T*o-@FŠTdΠ@#5 A x@l@ j1I;9A `)5H pa)A`5^K!  9>AbA!ϢÂPH /d~3+U0AსSJ ℿ  }&?(DNi@#r@`'g"@P2蠘x* @F73{**\e L<U[* 0XH& Kh0ka^ A! ӫ%̢o%Aڸ{N0 #-DfVML$'lKa&ܘcKo„@ :@OHDEsBXBIJ'roU̿16#@ $ۯ+qH'=e2L3?%TO2 'L҉yy 6JbϬvoc?1R]1{~0DEuhGM@P 48A QC'$cZ3Ż93{uOf%X "" 2`0@HZа'`u-UA  5 '\l A NŦc3`ı8>@\aȆecVHAy5CP~?&.V9zI[ O3$p^u\ۈ=g a#86bRCL8@~ *P P^ {cBZ54 ((E!j% d`(< M(ph`h(Qa߳DVHrn&Q"ۋ,]BYFyD  o(Πb F, h0YAHR؂ p^A10`&, @P^Uƴ~w,0,C% q(W>/do0Ls.-Fp `2  0#nu%A я@ 1&a D@WPX|K*T5p xE |`&K _)>z!:|Z_݀@wO=~A`8FH^ 1@2y\cF J@rpC 2\F)C@,q늯PDl;W`)  xX0T.{iڬ۳3L >9$@ S`1 FPvb>(j` X:nfsҸuS ~H"2xGPN",g[ Ib ?8*׸! S;N{_oޅ:9|S[\{k`V1 5  pB` A .E+q Q~~L P EȠEry6 Sb0J#CJY=ûb'J\<v4e$"@'!Vs5"Qn!6TV1 < R@uZH0IfU)4u(S!ZãWSA!=R@K|X;hW.I.{6daQ3*0 xŐQi1IWO'0Lp@cKNJ(hca8):^3LBo"u d51Bro& k?v4 0R-=JL_ADCE))9kIo!@YAF!3!`Wc6Iv)x0uւSaÎř7r._ {9~9SnɒI Am1g!,!&J.'H*D‡#J{w)9%Em+l00 @* 6K#@#XtJBt~5L +o)B.%4 /L %6-AEKGH*X;HPŕ .|@>K34uB-D Piڕ@<ԍ@Y3c3Jl oAH@!l'~rykC,D R]'=H/ W9AlHF@L 0h6sI )-y QÏ@d3#P*l G/{p|ZмB!<t@`3* -@׼+2BB-Ë<!4"7x`=3E<VBqaJ VH^0 b*TY7 8^r}B"rm$-H )g@E-W5`Y.uPbJH pX*B+o)A  @#@xDkE+р Ő;&d=A F\A@.?jB I§Aʂb\7'L`n0H!!r2&0AxMxa(R%psLH2!My7L@yφB4ALtȩ05JrH4 h RIV)2G8 z@#ͩHN`iAP3hX1@B!.` B Rb*0 pVc{$G>0,Rr42&^m%&<+Hbж:*(YYRʅ@b(@>N*MOpD'TzDR  kQ{ x pE^& EG tM#n!YA$ gZcX^Iw[jw< :u,yuxe}vBPĺa_愹& ==`s3(ZGr5XK;aja#>Ŀ::qXf ŵȪr(zſz2" k귝p*0 Bxb8FpbGZ=C&D+U!A e3 Z*l k l44x[ 2 R\Ϳ 8y !<@Nx[yۂХ"g<0Z]N@~+|qZ(# A Z4R!.LuYuW!wP8ڹ97ɕ XC?n%gL AW0jJ AA=KOe&P`xQUI bӄSY܌^HO}n{2:@Q@ ].u<颫 4F XԲϖ9ڣ,|X)QG'>b_'.w9@q R49y!GAx i-dY)'HR0EȀ( d4ˀ)jc J̴V1G>7þv7]9dMA lҫ F$d= JƜ6@K@ψ ! p387-+4z 5t FzW+@#eM!pudA'!f pF' GS{1{ anV \gQ,.p(|D}6% ܐ}BX [0tI ܴD2/8 ?P[7C#4\7 0 u׃lWz!=D%p=3@tP8(@BLR^X6{gAx!?l8! 0]o1 y aj~U 8 @ @@ q=g# OhAGϧ9xQ;cM@ '(wz׋Orgk } " Syf >|WW pt4 1rd!`/ ho~|}ю{ȃUqe P.~!=6^ɑbP IG9S^LlMrOWqbNp`~HNGrqٗ%SQH4Z@0UD*?f+ {͖=xfgxF81?'qlI 6r閏cKrGm$Ii jiqI+q9*!YIHa]"֜)krչٝ9Yy虞깞ٞ9Yyٟ:Zz ڠ:Zzڡ ":$Z&z(*,ڢ.02:4Z6z8:<ڣ>@B:DZFzHJLڤNPR:TZVzXZ\ڥ^`b:dZfzhjlڦnpr:tZvzxz|ڧ~:ZzIڨ:Zzک:Zzڪ:Zy!,/q)'H*\xHŋ3jtq"E CP(Shr˗$!<#̛8/b`3ϟ@ EУ7 MJG{Z8Jjjʵׯ`ÊKٳhӪ]˶۷a) ` !LλN8x OA(A0Zѯ]{y~+b/);@)*vY;6鷂a:0vRMք]'r#!*UB J`p ,#lyVbOSpJ.Mq@l0B ^g6= q#}Lz,ݔUB4  $ O7h ~H|7A9pCARfQp@ D@5c0._ m(MF `GAN,D>ٳ>A1/GBHg %T YHAb 9FW! "O ekCFel DM?PIR%!, (H)'H*T@ #JHqLPlj ~Ira(Q x2ˎ0N8͈d`Nz JѣH*]ʴӧPJJիXjʵׯ`ÊK֥ Mvr?&:@  ua@\څ @ag &$@ k@!, L'HP @\ȰÇ#JHE3jȱGa&`HɓMW ˗0*Nϝ%bL'2ϟ(g Q  SO~$0߰Ojcׯ o "{`lk~E,]!(ݫ-߿ Aa!ŐO6 !X'<̹ϠCMӨS^ͺװc˞= h<ܞ^dv@ԅ_8="{;ka&sV:0~nA#TnQ}65!Hp09#axRK0[47K4ЀY`Ag2(B~Fu2A'9Xcgp2$B^O-,R!P - D'Ђ^$v% UٸdA4 @5 y' 2eIF#3#B3LY LPQX0<AՂ)A5Y6\@`:?\Q (@O a ^ v¦F0$ØMS3.!L`K  0 pB0E$1A%@$NZhLL@=n0A950p$V+ L+ JF<'P:?n NMR5A78po2peMB2P&\˷P0 ,1е;S tO y5罊P^MO e YQ, S `A SSw r6CN)*T[P'{02P rY3An> @B^{6: |\뮯l Cn*({CO`e ZH ޮлނzܺ@ ?5: \p _ 26yAVw?TPAC0|Q@v Jy 6BdM*Dؔ,$›*#6\QG&QʜurXX̢sEp'!,a((A0a&A'4( \XA!,a(+)0!A !:` $d'xx !,a((A0a&A'4( \XA!,^& N'LA "wB+^laƐ#Kf  -  ӨR NZS]/10@bHMsУ#,|&pNm!x@ξ}M 2"8B` +ЀH @F-{ 6X|J@ 4LE"& Lb IBz897R8P@ 4d@yP 2q΁Q1] h Pa$@G&FPv]P1(@|<`P7#B L 2ApN˂ m(D@AtZ썄3ah#4:A gB!pэt@2L@+Ld@YG $Ed&Ԣ&jBSۥ @' Ֆk(A8ȠBf'N` tL&1)) OP~!@'@+`Vz՚9 +@!|0O2joVVn1̧<8,H 4ȁ&UKTuXgռe"P.h-Y\mل)Eh=Vْ^t PQ`jIS mP|?7OD y@%T ]PX`A%o]W|x3Dx> \dS<+@'l箼w9:WUYk+aT4Iuh9 p#u $}r1 ~F$4}܈ JP iG?4/Qz%])q: GPnU ?v  PTBI؇@&N"4&^$GY|`T@Qy(3 H'"8OȃBAu 80,'ELA'x@P<< `apFIJ20F!PT xH\ DA h@%+Z60D@3 @B B8 > xaTZEh[풂Hv$iI К<1HI}J45ƺ dRG.8S|{4)P r!0qS>x__*| ˜!ҁLP<{lDMrJQPE%d3/LNsRT=>u(h CpD[BqAV5mG v؂^1b] xY @Nw'),C@ETdUFak2Y2exKd)`uPE @:zS[RiN}6Xf"NY4`*-&U]i]o9 \ʺ0Z\31+ t_~?]ckɮz*{\-]ǥ VAs=\1p>]PP,IƓP9Haۘ). ߈\b՝1.̊(x &.w;n"YP  0W=I>4$Ar6rj.ׄ&IgMۋN`8k )58̇ZP/av* {:A5q ÆRΞ3*"A! 5 LQas@h!($-li~ELb+P ,Cr?jf  r;H> Ε{v)/԰BLAmDC T`lME 9$F xýJ҈4PH @J6}mlr>@^0%~80 km0G>Мg! BP .7 to4Pyh^Qc6e`"P @ > g|:֊a @Q@}Q#hX# \٥\`_IU%@ʘ=  B菮(OvHD_}fYR`l&DP 4x'0,mWSYaC pZ0 ;$[V=+p/Aae"&o= @N 7;)!~!x`Z p0P(P ^KݰSz$baOgh@}Z%3"xP>VoI  ?PɧP4"lB?6=@f*6b*"H96W$.Fu]HQgF# # $>\*.Br ud;PPUm 2f3i4@݀v5edkDem H(@dw16Sm p_t@z@":9\:ׂ r)I[ 3#E4=7 Qw'Q xNZ=Y6 57edb6[|CHæ3ojf9j@̡Cըn:x<ym fJzPjgJ:zGڪ<#z=z.Zګ:zzڬЊ:ZؚCܚzZix|꺮.׮j;6^zTگ P[t{I vڰYk[2t۱ "=$[bz*-,2K5K4{z:K7<۳n@6B;fSF+6HYL[5NOR4T[>sX3Z*ӵ^2`3d/f{撶j[-l۶p *r;Rv&xL·~"iBj8Ѹ/w;+cV{ѹ 13[Júuw1[{kۻg6ÛŋI{_ʻۼYЛK+f׋ٛ˽˱xkk;髾k=۾D+;cSK;ۿ8l<4Q >dZ| "<$\&|(*,´!,^&42'L0@*\0!#J'&jH"9iFzɗ;TÚ+V ,ZΞL0ƌ!JJիXjʵׯ`ÊKٳ]*TՊd{ՊxiRI~իI}'I''0_ XTzVLѥJMՂ,-؉ʈI˖*Â\#)[Y&p퇓jfx_ꤥ` #v 3gBG83E x36X`" ןISF9貂T^YGT d>,=Ԭ@ "zMO?HA@ L /BT $/O>LuU #5I[0 dH>@ Q@_e>  gETFS@TbMLeYF!,^&4/'LA*\0!#JH &jHbA C(S\2a dC_9!A8fѣH*]ʴӧPJJիX[^:G[/vӄN̞D)'Ngͺ ٶN[`81.V@8BM5A#& AY7h7x |tDd fm6&uҭ$GH H!B-+\w P tDJGڲL{c nEMdR5\BwRM 0 oUP@!,&0'H*\P #JHŋ bȱǏȓ(SIR˗0cʜIS"5sɳϟ@Yt&QH hJIUjʵׯ`ÊKٳhӪ]˶۷pʝKݻxzLz "O=*a:R V<ׯH&iiR'Βs\|Yӓ:O (,<0 [< 0>ٳ'L|xo<g'0[yN13 F1.h ,< D 9aW)xJ=V b>ls}:XP@ӑ0%Ĉ+4p3$ CALpCuҟ  :: 8L A P+LB,\0Ȁf7I0mtAu 2ML@V31 > +̙3 D^ 5:9d@zp2)d߆9NE%.+ ' @@ %@?ٱĀ=kQ@Y;X^ Xa 6 Wq1%PS^ C Ӌ B"%dRO|JLLO[tp(#y&,;L ؃O>h xVg0M"\i[*ZW(L pB p}U0A} uDK8Y@ LG#!ȾvSNp@@MzT@u{s[a B܈$6YFZ6vMyޢtNP@!,^&3'x'LA*\!#J@ &jHbA Ci(S\` dC_9!A8fѣH*]ʴӧPJJիXjʵׯ`Ak0!,^& :'L0@ "<' lPE\qa 0DD E D`P%!,^& :'LA " EO~`/A{ [:7`Z`9) ^sCc{@~Xs"rM]KXD_[lN 4x_`K% H| cH&@ p &BDB@ (5Sji!ShpIgE M j6@zA4;̂2@9t ?P/  +Bdžt:&jA~N,v{=J O?jWCP170PdL3HF < ,O#VT8& w퓑#PC) B1w{OP4# kDxEfxZ+32 ,Q0r 4@[chU"VG@W@QzaG(pv P{cjFB(di$94@@ϕ,8N8O\$D*pBD^PO(0v.ad 80@+CLQ r;l4^7Aef_ J -_m`N5`?;%Yn8rC@Z4Z 8TCD6-GA`B e,ySrDaiE O+JqR%^JP.nBP%o`%pROC֍D S9YXqDq^%&O02"$qI19!%4D8C&h#sGD&<.I60Ɍ2+XF`rzO)! 7EY)_T~S~B B;6= q#{LAx  d(<%&PCg#Q  bK+V$"6Q$B$=$PcFX@9L ƈ*@)JO4R`GAN,D>ٳ>@́$NjYB C8#R9P?iP5bTAw``>Y6@1N(7ɨ@ P'JK8\AH!,a((A0a&A'4( \XA!,0'(*\ȰÇ#JHŋ3jȱǏ CIr(S\ɲ˗0cʜ$͛8sɳϟ/mJѣH pXjʵWONp׳hӪ]˶"`OaM8ж߿|>y v1 fKLV"x`MӨS^=k^˞M[$5ȎP%+_μd (";N+Xν{OO%ӫ_,+t/}*~߀.'RX v`4(Vhfv ($h,.(cFa78.D_0n9#@ idd P NLj)PlJ͆;H@ |j ͪ[Z TaH@B)0 $|` hd~2u tBK/3A;F6U t@W)H+j4TI~ML"k 𒌱T{L@D]3LK@3h/5 (4s8b/$ @$eW@@R+rXYU#s=!6\@ ? \pP%ԌMxFq „ $, &PH['nGM9,[k^a 03C @L  ;A4} c'hA&qHFp!&" M@&b* BGAYP{%>S0l`威 (`ARPT0"1OxI`$0Ax@ $( VZ0!~G@v Tn\s2F0 hC4 ?j(x! cp ;. A@BiPbƮGE abPE$@4zLH$#9IUE@XHb`SYB,R "Dz@a K 45 k @0KV% bc Jg)tL@x5`9Pe P1$ '(` 8>#O r\ ʢoN)=pTF xq%r.qSj8@ x@ #֎ TX JS!c @Q F$j )X#.9@^f078C,^X-H)U>RB ,t &SX'' `ORS N1Uj@İRAƉNرHfFVKd&@אJ_w2_L C̥SH1"ZS,sDzZW|CvG2ĺJ Vd0]2x p*SU@ T$(D,n7Yu܂Ĕe-Ci`UA `#p+@` 3K. 3̰|+f0Fѩ[FOսZw;13:ukC҂IOW҂wCH, 3%fGH0 RB|H ! w¯(D &R<]@7r% |)dP]i_{Іuh# /pgc R$ +P2%0"lRU 0!@ ^0˓KU =",@jW.%լ!X!U z0G|1%P2#v rn6hs"P7P|uRG>#sFĂ@ %9+5j a 2B `pWqS!bDF ] Pp%po$%-Pl^",)tf6!bPPRA:MSAa7B#@W!gW`&# @ @pj4:8 ` Ux1h o )(!$&b3&{D F ,^{lh#nH Ȁ% ߐHV54`{`0 x  ֧  X`P0pR 0  p3 ,x!' ]2 @@U@z2wP wf0 P *5P +R~w PP6j$`] }TA eb P藁r'C|m 1]@ P Yp p P^ {"pO 2w 3P!P 0 $w` 0 ]w*yy3#xss+ 0s)  Y 0|f`y{W |'t{6t{q8  2R0 # pP~Pw#d; +`%I`؉y | cru-zy*I OF@p yS {'|闝I 0 )ٸ P *00 `0 @ <'И@ s#p 2; @6P CP(2xtP%P pP0 x0 j} P 3SUP ~Y.P`Hne9U ~nX Xkd5R* ;C@ 8% F@&$ :[ 0j2 `,U 3@jPP 7P:V1B ()-\$AP e2 pT%  p W kj/uЗT < ,@'0v;sV `) 0W!<9+Ё@ ZE,gB-d0 +`# K *Ocg}Q ' 9t8#eiZ AX V 27 ǹ-3Мpn '˜$@j"0'`͸'5 '"2ϠSӀ ` Ȁ ǰ @ m p %|/|EW A+j@"q L* -|6 m 6j#ѾlIC ; KzL:R],xF<,n)I0 "ҟ!ot: @Ч5`8rr:h0B0 } AևG՜p,] p@U v; I # { UO~?0,x jpP@֓@(@(CypܑP N}%) )ˢ<II pA zPp➱ wjd R}d: i[| d4ڨ0 P e=08I7 ` !. 7$ 5:֫{ᯊP; jtP-ʀ߄=BW.% QJ:px2 .,逋K PAh|r ,~: ,x3+< :>A!y @ @pZɆ*h$x0+ޭVD Ig{ [ ] :,E*젥 '0Pxp d="@3m~T*"} #@N!b!P{. 1p `s@ zȐC #b p]R߅%j. obR-m)3(0 J)GęH|{!3pP/b vtk,.t+ ĿT ڱʬ֕* VP$\ w \:`eʞ 0{ծeJ"-QW0R}"+?20'(^H Hj@ *55Rq0Z5 Qa J" BpJ bUC*@<$J ࡥF #R"R!ZāNpɒE ՉPdJO/^pEiC ^UPD Z?8p-ȪD*HXA$HH`$n)2!/b4ڵ 1Qm骀8uG  ` `i* ,R(8ANhaLriR@8q#C,*@:IQj )YЁECE"qYސAL9eQRE}( L"pLIJE BੀX9vI! P2*\ +.MJI," PD^ JjlX#W1⥌TPɬL:̱w:`HCH6 Y`+8!F_0GJZPĚ' BhNE^J!^ 8xKs*CĖ3 aq%6  YȀa'&Ie h}?^1fZ@DB:@dxK08dA 69Jۉ Fˏ,xI@qD|AI"@H%* F .Q(`\"(+*Lb0Q!ZS`$`pF `ث g= M}J*RaY4`NpWaHSPBA* C tph>3d1(+UeMC @p , 8ɝZ2 H0 V } ^`p#(Jp'`iP |#J }Ca8*8@qQ Yb`M,uPDA;p+x;e M 3%5PW{h3 ] g%e|ŧR ὤ:Z|X`CF:^V #B  hZY5E-:{ ,P[Xhu5p$$/fAT`IDW"H$ ,EB6; R}Ģ@BflE0/} I@'4zKq76sbqeæ1xt. wqtf>z5!E_b>m9l9| Gx$+"{jˆAwߋj~?AHFM{}.;髿3c=8Px?И%R#>c4?ORp@|ľ?3@3A>6w \)?CB;B{?GA,!A.s;982^*8) C-s ?0D&$,)$: dOܕw;UBPD?!A;dTEgFh81 kGED}FwL{c® 3:Q1!XXv P -2= a`$3/Jޛ5ʮʯ˰˱$˲4˳јI04H3@Xv}xyP' Ci;? `3l`8aṿo9$4M0G*F0 8w]G+$UpVЃ@@D !3Fq 3+ 8z龕8 'B06]A4DT_@ЉXT)v;8x0\ wYJslx`hF|c\qy(9a0cPcX hz+IP_8TȬmxG5&|+X< `Lx@6w5L YyPQyYx]@?Xl8\Pe}NȅHw(S Ձ(~X؆Y_ȇ]Pn^+ KLTg{cXD#et؀wWp}pMw@j, XzqN{HyЀc'X}Ԧ hXCk Ёwxy''{|('j@R}x`K `JKw8=DRwx v؆c@hX}+pPdphI8dH!X58_xc0n8xЅh~pץeڦuZaڇr8D Jxe( buEIDXX h\=Ⱥ0´;kj+`ߣ;@!nl|d?|qx2 oIǰu\=%{M{\>Cq,gs|nN=w]423⸝08nFg&@I09CFoGh=BK226yAa%ālHth; IwWG$!`^F;xyiP Bxp(;x0y\q @|8!XXe`k4@`Fhz`'q(I։ћk`2 :򊛘 땒ȃ6`kXDUPg\ Vwի\kxSv Itq`TH YQfT1Pw5D؆`hs8mg@xhIc` fBaXJH`|cP1 t0|sex0xyhx`TN/d' hy HPjVwpLv ^H) 9W dIp=ƅHt `IYYE8w0^ȇixhX\a0xPXhiH%=leS@\{[V y]@g'H (u(xnȷp'xӀr'uЇZH4rn8LǠtP[J hn3XE(|مp(nXlvkX5iPH{{!`8`Xi8,n(Ta,Ѐ3uϺ&0l!Ĉ'Rh"ƌ7r#Ȑ"G, ҂#Y @'P  q[+\e.hM=AbaBc#CEX(!IWe"zDXp,2s`guDs 1g@ @^` /\؀= C $P,߾|q8TAn2VP$زgӮm6ܺQwI˘f2 vN0̨̟%V ՎQ'LeKurG.ZyԭG%dZNZ3uLJ`5ja@0 BCiY 샋,A& 18#5x#F9e;0L M6PB ;@2@U`D !@wd(D4 ,>OU1/P@U)p6Q/" *P4ŸAN 0K:D ij "rS 5BM(CTZÎ"wq SM 5L?S0ZN.5H2{9<4 A<9= ##Ɏ;1LME>2{<3N4\Q%vC *c6D@;Ps6u:f7NDTQ>LӂJܓ;1" ;@Nxh;@{T 3 ~$H$qV4 !0ˢ?B,LG4L3 `x6 'd BP$ f(@9@r|!`i?B-R !r`5P C@2V.E2 2\0"BTa N×U `*`J@@)d2 V '* L&A !a5 OLS! R0U|M` bu0papS"*/ROC9nJx# ,N:$g"Ǘ:sG@F䞺>a2MTw(ԠUd,l@͉Ҕ(NsS)9CpdIZ".!TczJB)V} %y"RIJ_azV0)R1{8 y?-.<29qb]d #QTtpTHG wBh`>:?C A4 X[5x\[-F9t֛}t| \\ X@ ~CjG( ! N.F7tr`CA=QH5X RQ+ XsE' Q XP>Yu5ܑ N1hT'L 0wH-zgC@8aDp; X=";#$Y$RP&5A<(;4 $C8"zIUA;L> 6"h- Dp7Xd@ 6

4 CC;p[튐 94b;ȃNgg&9Y8'!kF208+ 4\O8$LT!BCgmG@' B 4ck, L" h |JT dB%j71"A'uDrJL~i @\78( 'QMyvGIDr1UL,D T(+B0)k#tF՗&eC,U2jF@@ăZP%(X/B0i0$P 6Di,:"  HvA 43kD,C!t80Z8R%L+P4`<C P@H<Ktxj>8Qa$ (gɅp0*|C@r:xh\Ҡd2` S28oZ\ I4؄[=,(6DAtl,C@Nr/DR/1Q @2C ALC1q$(4$q|RDHH2|C>~M73Lg8H4M@M`@}<2h>+Dt FAg@:   2C`QUE'oFp{ )t-aA7Ǵ] HBSDN\CIJ0D@d@ 5o[T^%u95v}v<3C/P.R#2S4 ;B +ÁHJp0>pCQh.Ԁ@ ?C%8[ X>#@*J7 D/D1@ՙ=|O%z:QL8+= P T X*@hܘ F&Z=DX7(k=@J 3bY? *Jf(Di։="ٺ{.ă%a lKLkМC4;gWiY^:wxWV/B`j-P:H+nF$sRqSiaQ{;΃L>=~ qCBJ;9äYU}ȇ }'K!/=8aD'E}Dp՗=ҋOGuȄYM)?qc+7C7ATE`k3Ǭ[擓} >U{3~C4}C~c߻>is藾L>Zob$#P9P0nO.Rk1RW` @P8p,o/C(LNج/%LA @T0`A @(P@ '&$PP%Ё+ fԸcGA9dI'QTeK/a H @ѴtZIp %0A{@$9-5+0j*AZ':&-08#?b3gBY8@yUH ,;|a<tiӧQVuk@(q,=] NWeC M/\8@]€ fV@. 0@,T^Z`QF ψUC` g!& @#ŖIG; D.@f2h0epA QI,1%fm8Es&A! ơ:)~! 1gl ogs9f4ax&hAvƙt+@0!F BP +yg8@I.ਇ LK1TS !LXyQI’x#HHHe|(\fWl!+kb xtVdH`2bXgXAXß;}cjNJ1TJGjʀzTn'tpM ThA !VigّYkM{G|Рdͨ`9ǝp pZf'097uަbYG7+!_EF+|h#zp&'NZLfB VE=,gj΀)?&ŀtׅoD`q1&eŀ@fqBƞb!;kxhV0`Hf}\@PE_xoZ`WfQ x<. h~h5g'(AP:s8% {p75sBdd@d%C+P( HF d퐇=K C:t14Q ݐAv@/% 2a;Tq9 Pg)0;N`o7˂VM:s2І{c*zB:`A6>(2z"LB > A0 ҂+jNzsX&D3G( E&QP!JP[ ^ l. yR;a0X $ Cp0l&U\r`@@\n0b;)E.pLcȠ$ddE*f T`4Yj؅zg܃b(ypEQ`O dȃ ;l Np@K0kpY>]C?1J@Q9E /;GdPj&@s#G<ʴN]LIA -d`wdHw@F瑙Dh |MBbxp!`t'Ix0;@1$QⅧ3\뎘%. {etW#"t2AȪhަ39Pz2)Y!A XzH&L(`:b TR1E4B0D hzyuˍu9S3|gyƱ0O$.<1bg9y Ul>9= RhڠFl2;.޲+la)=͠HM`.' D`DĐ)s5"X3&6 X$ bg_Ǧyf7q]#5-0>=bt/h%8P%#uM)ʀPO8i@S]vFK\VX.-݆Ϝ~RʠO#"@2WҾMXl& X{ăI|t!#@AI 5%082 `4'0fpm@J=x yyZџ~EE@\CvLF@q#0@[2a:apA,@Xf8NH>Jboh FaAZ >Xpj:ԯm0o+!*@N@:"/^!*b@̡D aCf@T'#n̜],7 4 !,=J@M4`Pq&*@ZAat`|@P$V`VXBE) !Ja (VO@ j1-] j! J΁ԁ~``*bG@"=d׎]Roagzp ]A@N12#=u2NI e,{JAb hPF`Nd`LMeD`Πta@ &e @^@APggN#+W#=r.GDҠƅu~30v)^}`$5h}N N{JN`9$҆ce   V1/j ?)2N@d1 1s 43  A9U,F!bA^ ",q I-b( 8hMIh6v g!8(H r0`Pɏk3Abt !X* Da  CHy\"f,& mp0J`k[xi(0%7*EހtN@Hfe0ҏTddpO=fV B ~1=g@-6%!^Nx h)2Bz Գy^%EM'RRD")yHna|LWLxȽ(b44T\^"=xĬGk@ }h RbJM85 ꪪwL:@%X2A=Cf r]z$s+1\ 2 >;LwSːY*v1B1Rl"f: S5 I!s 4\Hԅ6]w=&fpBH `*9`Bm2n1 8np,PĘە` 3FZlDIRq,Q M f!B^L5J򏓦960lSM9@p3[B]95':K@ z0Ё3:TVH; J-8bobmil3+9$5\pҁԷ.T%6Ea|EN/%gr6F<#) ,']  "B^Z/\ xG7Bk'hA"Uj{`y"B!v*`'h zCxl,sR1 tmׂ0{҉YH0 5*#|#Z݈ =yy')12vbP1Sp_z(ɓ0&4C 7U)0D>uPg=#A q5K8 c >ԐEIz_Tɨy p)B1&w#u(E)Ȓ~ PљP(Ii!}a y0t) P<A)/Ye%l!ǜ15p Pl)`wpJYPujYUv99$ l^$ qadZcQE i1@  /i 4蠎qP0{GI 硚'0'*/ȹP[0GO!LY 2[$㤙P`<! 4ީ<0 ѥ^ "1q|ڧ~:Zzکڃ Cm(0;ѷ$+ܙ:GCL:M1yb4Ⱥ{ 0yM!1.JkEu{kջ!#Y1rps lc;+Naf{3s*uO>,;(, ߇'GzС @l,p0'X B5'>`"a܃a 1@ʠ! ةI$uD'p U,c"a!,@(E<'HÇ#JHŋ3jȱǎNL `(S\ɲ˗0%|͛8s܉ϟ@ H *]ʴӂ OH@իXThRׯ`&*ٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCM1N^ͺװWk۸sޝ Nd»xGBGZ e`7R(e—c&{DshТA'4Q#02!ND$W34߁!  6F(Vh !B7* SVYU*h,B4Ѝ1 DH tB0R ! )p`6p h(+X@ L du@i(PVPm?*L A2P9AT@ALR!ZЉw!gALJ)?-5y'b98UFHY(T-5w' T @!T B*eAL bSl ܦ j`nֵbk]!,b()p`A`РFBC G p |׎-zwyM; xUXtde_ťMFrk8WdC-UG[<-'F҇/>UTSk}=+y0ora~TӴ~Q$?~L`M4(v94v24WrƳ;Y$8 { Q259yXZ6!%2H =0!:fd2v4z@CP CCcE 41 #[B*01CTJc(c b2(9r8Xp@M4]̱dE.]E!8M,eQQR" @%rՔ $@ & @*46HaXH~8>`#Q0B"2'u8')  D8#1$P6}7%/[ v s!.BCjFQ>;w;? 'jDSǓG7fTYE*F_@Y?Ca<)Ӊ恽 1ixSiI-KkFFA2<Iytnqrib419y-3!@!#  (İa%5X&:sq35fIFfn}g:y!PA8u:'y`84)0ؐ&3 CM'ęm8Q1OQ03a '9nH. Ib Xw @@ A1m'`kc7x%В0p0jpЀ0l :!q5czQ /`4% ND]`b (#Q! z s-3VefEk60;Am:5ءOa{ "`P$%)9w,0!{5 C^0pєzJՐH PQk4 ݐxW @1Sc@V! az" L b: UZ&0ʠ44}1w=;;xW&7Ptv4Pk+hu ԪJ@ +|dT$ b4ףXkEP6P_0tكqQ@`g3;,Q:0v[Y;{'` +poy-U"U2Ksʺz;|$,{.@ ]hE,`X<)US2h.u'@F-i\cZ0 j?APQ82$@Y" `dCNM6T0%q`9L`b%XM&'  jf!ʶ@ QDڝ?ɚ3M!G/$@F(QE+QZ1rx*o0GC]1G-0A#b2l_ѽ;LOhmDUQq܋" 1%%?k2b* <glL'%&XC*L!oSs49Aƃ|gKDh5LH/A<K,?ɟ K4>0+t38,1Aw++Oal\mQԼLcʕ%*ûth\F/LqA9':{}4ڼ~QHq^]jw#J}"x<sf@@B$@9V]R=tL;pߓIŠ(Fȵ $W'  8 &&E!0p V: ^6~ 1*A (`"%Hޟ!= 5`5 R} '0ٮW0 b2`X0j Q2P =D@3X>70eV%$++ p 0Úwdr"@|4! ]8g0`t '@ ,-tFj1M `@LBk‰&X b-N(0@FC ׄQ3Up\u+ 0L0 Ncr (IO83 x OiQv RMP4 eQFdZAᡅ$v   ]YKA%wQ*cѡ^LMPa 7/|,` BzET:F7.k 9!D!J6AŁ1a;C M݄#U8` Pm+   PX`vE &C;(΢hJi:(P&h! HO%H* W+V:&Рh,)OHϧ. h ^X'ZhR eA&(R載尃&8E% "/ҔM% PDb 1yRR6`÷D` 0 ;X k!2ˢZj*N Ap8 N" a@@Ho|,p7xxh XԊD"$!3ЪSbkZr ITt=L`-^, XZTa.O#6|m ڰ:Yx/15\mD'`ÁLMI%~X d`1s9Z'58& $P: F,b9@RV @r \p`'Ma& RZ-BpI@{4'@E }^&5.dATg?LX #Ypɛ)>1 `C\SY! 2Bَ  ԣ>nOEX fp9- @mٔHSia`UWb:)yE&90, mF @!j"T6+Pm9@8 #Da V68l:oA=K8 JĢpKDU`J)H&@ NOȴ)tH-KGOIA`'`i%:|/I?DNTZ _,@jaᴫhX@ KYAg$RR)% BJCfE\b A 7 J/ SLqtƋ;bx %""`QD6̥CpNl[  AN:{fjyH 2r(`4j,PWHjbɍ8PP)Ch5\BEyE,9Fj d,d˭-0ZE-T@8< AM : uX $u]A^LY"9$n|ȉ +Fą~;rpb9 ]O)O1K-0Ol$ Y-O 1B 6u b@p bVf@0G%ȝ^;|՞KXh^ Ҧt)`0 , HٝpE":dBiU0<@B/drzb'ΒAI!Jxnհ2AtPB)Z 1>wvA-K!0V X9; 0Rx>W@h&1 YhO0 @.'! + (ި3.8 :"B˘ᘒc@ P$ _b $gp08(A=2  xR`@E180} @ 8n(;?[أ x-zmvpJ}lHAt6ג]#3ؐ;a``Z T0W(?iئ߳u  Þ:Q*X U.+Wm Ep?DVZ@ЀI!&PۿQ#൨\C@~j*Y#^ڈ[#r|T# ڸE8r9ȥ2$[ HI/X9 Y (КX*@I@FT"9!1Ihi`t4A P  se) 8A>\`y٣P: h&;W8)˗7D РcƋ)' ì8 : ƈ@- 3Ă,7; :y" [!묀,!8 $ĉ3{aυ6`)L:"O X_R\\ئ|x! / (*p15(>7Ϛ\E 0Pa+X ꨵ,l;X;X @: 8! 3SQJ;т IB[ %O2 z94)Yk=| ()a Ѻ`>ۼ(,) /nnGW{`UP( ;6N35RL =3ǤCO8 H050 QE  4#(Xئ䤉Y# (Cdͫf $B Q  <,$O$@vQHTƊ ` gUb `95 4﫳,N D6e)OiTQ Iח2SV٣-ȣZ=)"-ւp"XbY(Y1S:9aZӐE>Z׋jEЌ̉TZ(\ۨ\uY0 lOaZ=5ٓPYcYz؜5VYYL eZE ׵ ۮݝ5\ Zu]<& '[[9g [[;|޶9H2ھݨ]E5Y]]`Z%YMN-WmT%.uNĝhE=ߕY}Vݪߟ]Z4Xy]S5x Ѝڙy<٩ ܺm]\fF==&`=!PEQ=&گ]^ٌZ.]fTWr`b=]h+)`u5&an]|VH\`ĞR.-bN׃P=_%]ll]9Vg`X%XW ȹ5['b Ɋ %b 萭inopq&r6sgh?&i,eW |I bHTɂ :bؐ F :3cHt .%Z)h,fi걉  %i&B#r8aW#Y %^ꪶjm@"!eP_XPg1UX` Nccrp؀j(PНd@k0WAp}5$n$' 큠@1K &5 ٞQr+kyP h}Ȗ ( pc~P X,  Kp( Hd yG73G4j9oN3 HnmX;aqّ*Ja F(8r.nЃ?(1zrbwv"(XLEVR7SG ;م3kGP hb>MKw8 'a&Òd<ȭ{ffz~pSs(TpqP aI&ފ ]^wvXxm@uV+ v"ڇ]PK5Jq%7ql-۩KЀ b0- $UD|Jr+]k0ȅ @PxHwXqt#Fpzޠczv.X*v^Vhyw 0]~DPb<􎐁"E!DI~~ ^F +ЁW]&b0SZF  H8!):amސpb͇o_!8xo%)LtE9fp]# uG ,h „ 2l!Ĉ'R$@FHP$Kb @*L"Ą&`q X1m 'ZĀ-XxBS Nxf/ޣ 4Fh,ڴjײm-ܸr(0/Tb#K(UNP1(0:a7-jR T1 mRstgӮm6ܺw^I!K (R@LrV= AF #28A ;a?[8Z# 5ݻ_ ܃x uv * \u `fɝy(ZRR%("S'7U1_uMgB6@g 9$EY`ib@T@Eմ!_F`OyԭHAy=evɵY5qo-ڵ\b 9(zDId8 f ħ86w!Voi ^z-C֢9s >3M":,۠$0?* ?]Pp26*grQe01s5(UP 4',ipӅ@'A%pA ,  $3w1;0:ǀÍX;2% {M@O^7 + ;/,;2M>#L~m3 O>LB-"2035m:FO`B"(BLЋ<.@LPAL^^ -ZOK*/c@SPv2`tQ)8OÓ e;)5d%)HcfW&6X5Bd *A:z!dUH 1;wbxy"'xDb "G;"Rb #少e@*Y(o B VA3c0@ -N8Y*hG  0 PJjH tJ$(C&#f\!*@ Xl:qeG|S2Cr1sD#Ї(J\~ Ɏhf V GE`\8#rh !God d@' #}W,`w l0T&!EX BPMXJ10L @d7e@ 0` Ѕ`h)h0ł >Z'ҍo 2`A*0Vj |խrD Uj u!;v#|ӸFb` Ѕ\ (4L;պЃh CABFET‚yB˄@DI Y*nsʟ)e71X z5{L`œr'09*߀@p9 A^N@f "l:mxӡNT؂NLX$@i(!$>" $/6؜>er D. 0ld<.T,tB2La'db M,L*  pP%C{X% ׍BE7$h ^`,@ ' C-3=X$G*N*dY,E D8 @hP-+ChMX4̣@ , =4pD4[ dO7~#L&h Mx H 0`M@XA_`M>D-$2C7"Ÿ@!j@8CZB38 uΉ8@B;<bS>.4q/cLLVŸ\ |PIX@P^$A:Z -JR^@`@A%@,CŃ:,Bq6\SH:P D`NŐ ^4$cJPBWN2qTKxLK9 , B 0o,r/h AChC06fe ,$>B 4=hr^!A  `/(CʅжDƠB&4.Jb|ph46 vɃ:h*^7sE 5X@M&C>@ L(@eLl87۫jad;Jh*ŠҨ o5bLDɍ$ ҄L@ T \@xt$۩ J >ş' @4 T 0{ځ@HDIA^%,"Qj6 A LlQ{|-lX)LB @=X?_B m&-NX C-Nl mlC "odCxF=Ӊan|f -mn.n޷E9An&:D@>]FJ^enr.{xƗLuěIIhݗD HG QMP ,.B\t@/.*vo}N fD&RFzIDׅREatn/I'9^C 'P b.D<ȿ/0؅ U#rO8| lF<(}QC,hbtL5x~pP6dCCx6rL> ,2 DeDJH0T"v0Z@@ 00#@AN T 4C"*iR1+{O$ԀkQH$V1%@z LA 2=Bd-C:pcFؕX@T$AA2g ?-Br+, 02N[O% 1LXM@*`\\@;M+LЂtMFC |A D"wrhT4C8d d xC  9P#N ^0,C: A T 'tS'u:\L94/ NC)@uXR ?C(A /l} ."rL ꟼb@!CO @Pee戭L@u/p>J>CȀ:>YM$oS)r#WA] < \AIE 8 `thtr!(Aj @8GFPk{ /Y >諅K(_֯JyTY-߳ōK&^叾'ܟ.oGۛ]/ޙоؾώņf^DohHP^ T.XaN dƏm9~^}HJ'<@X0H TB  ` 18 (P!b@ dTeK/aƔ9fM7qԹgO? @&81a" ƛp"ʇ%&5Y<8a%M@"ƈ {0@B!bKrZ +(o @䠟A=tiӧQV MD ) P0݄mTTpz /L7E'LXA@t5-Ƈ#\'-9X O 8vbB?!)X07ەZH& `$2 \TP ) 1 jcz̙`n p;ɸd g Jht `ޙ ^@0Ha*` ~A ~~쥎H4@0J ;35d0Pq 1R8gHZm" af Th 'TPamB *J!nkm ~Tr%`f'X@@ +&lg6ϪBZV!`( t JP nb)/-IK`e&PuC&)>awI MJJ V Pu :ֆG !xuJ%u oHJp MYYnO_xG<("MN3 C!4H ӮȞ,pĩl.& .Hn =X% ga{YPA$AgFQscg`A .Jf‡s YgTp&6HW9T@~jE%'Xt ^ h^?)J`T"bq C ob),_,h>?Z0 L'c*N tD3y/?C7q@Y/VN!>},7 p;*f r`#1S_N]c9:@ɽf,Tk܆l&,Bz0'C"kzl?N #,8q̥.w^ n(b2 Әм 8< chDp PVDr&$1D t ⁃\$Q1l>ń!M: U5!ۧBχ!+3R b˅Z*Ȱ&h/h#( \V( OJgLvd6$Mw#J i*bшu~H5dll+O!9LD}i \6jI kEH5m,}I@4Kzdg5cׂZ Y@tHM{}M8*GN(xD=+bo.NRT&=c ,qdqF^JqP7VAVUHq@1Dpb,H&aI0 )'W()(G6ʹh^%9M HD|.+La3zpdI@ "S + I`!I)5*=3L`p( 3L` < ~i20E"o0Ă@t +@;k*Dp u_30 n3^"@J $r_݁oTi V `a8(0  M%q0[go'r`aH`6z !g@~xoP(\G6zA+`y0U`aa5@"T"!+G)] e0pAP @;/Xw00}.F<MS 8PQt 0:%n>vU b< $isExՂx$A G.dANSq!tvH&m1GX 8h2!#g? AM`  3~'apD"q 0gvx~Ќe@HrIzx 3Q0 n=RQ v&8 Zw@! x L5DD`in'Shs jYip @QでJ~0#xs y ^2@TIP 'vpx9y>vn 0wyHplGe  zHV'p qQ`s!(Yַ_gfA(4`. U#FDY,`y&F@' )5 r#qA$@^o0`V6sd0Шu8z&B@ P;xiK(6!+3!5Q ?" ' fK ! VFv# 6PlpY djRDQYazP+ ф0|!U}ie3Q5Eu/I%vd' .Fa61>6V$9sX#O i$ydPim~ FW;e( Rs=#nuQpDWUyjWt]N@ @&T~R l\Yhi9;#*HCpZ  9Ӑ(Y'@:zg :d*Y~A %fԡ^0wYX)VE<^r"!Ӆ@zIτ`ꭡvؓ@WWP*%uQ `_1Z!GjPQuE12$AXJu+F:j1:X7Ka ⴾGt0a8aa *jg ᇴ9:cPB Xu3Y9V+&ap`a8 $vX!u*Ќ~ tj /PQ(eг&d|"I39熓&o&Cy 3qaKp piف { T*R_[iDkcuDare`iRW6 19+劇'v SK%sgѰ(B#u":bKQܡ .vWS+6[q:Gf>,GCS52l3BλϮa!! P4]yoF4ceՅ['Йf!A}PDϷbφE=m7"ͽcҲ8=-9!ӆHqӭ6q C}Da!L t,b3)[t>sq.sVMx{ڨ@ .4bkIÛv- mp j@-6#`˚q :p*tvX PN@Q4A>1Ua PA^L`Ա#NFgB&0,JMЦ=N8hDq@ 1 +*Z뺉 UٱG*K0-g1&e">'$AO^/` * kREbF q١+H1/Jq%UsT=~HZ.TO8p:=9,t#ƧxzEMajO1W7)q.rxoӠ1x_O~sύNO'g.p WP1 ̾'lq6?E!L_ GA%e+:=5 h v0pq ' V:R0^"` Jem!dB &D( B8X&h€="a ( BJ-]SL5męSN=e p+,X9b 5\ MR$H81ۄ0 _Lt D'0ۑ{I𥻃xX`… F8 :6m.,>D"#ܘeY.{F3:7AɊ][lڵ3|򄰇C0*Bu2Icf Nmխ_Ǟ=6NJ YR'kǟ_~gm[\Ɏ~; V0? /0C K>@ GVBI9礳NP` A%,RHk ` P FpaE*Xa&Gݜ # v" 5;WeU: ayaH O6IU>AQT%JUgvá|y.Zm[oq5P[sE7] HT]w߅7^y祷^{=,s&Z ו rmB@{y>4&Xَ}cD@&ʈG&Y!d bG[n#1>H ,<bd6Hd)(ۘfhx!4Bk7EZ#s$ HΌ :"_"2@b^2 nNʅ!& "q&8%,:iQ\"A !kd,Rk^|c-2o`&#Ikp= q3Mop1<r ;džSW<"\(Y:AD&vj[rxqpA(Bت!C?)!3;>@:H⇐YtH fb8HBN iNP@^ GV"#8>&' C]:x#2LXFXC+8|ŏT Q %:%a reV9D"4t"ݸAR3Bd +31'LPirRGh V؅lވ&p΁&d3 A.GC? \`8&7X92,'R!Xdm;V[C8!({y=ӌHx&:r`c:-N w1&P]:/f' aP :[CHMZ%8=UȈsXD+=an. $&1@ BhÞB^*yU%$ɴ=fcQ!}IxE䞸#t ius11>S`L7O<3!=,NT(Q&p^bA \Uςɨ'XƼ<Ug$8qT;$i-n78@om`{{=My=|Z%A$trZsyƾjǸ1Bd1#wM2oo%P=&c L`zQ ,OokU~7T:!{- TPSۖ/Gubn-u9~HKˍA 3&똓im\@8:a uR씭@F&Y{Dҩ(:]{nCR ^J<|}՚oX9!#°!CT|S-XGAsjɷ~'p@8@*2ԋ. G\c8Ӛ4ZzL\?xy4q#dobzmO$) H ưc%o;^йp 0>wb(/`6x <ÿc?Ӿ m 䚀3P Pڛ r/Ht8h6DB (j Au C M7~ Q"5FjᏊSص y#ʖ$:HlJ FI D0JbWL@W5 )P1Zb+P;Y&"% '/Q!BI۴zũr;RA} "M?3*3t;P`im(kkJd@4PHB0Ќ:,2!] )"@,,/'+ $E*B͈ ۠8G@i1qA !~bE%pZU8@0 'espZ71A|maXX79_8pdR8B27677/fPPp'ZHw<@[fhmU'mX yW{kȐ`>hVцsrAuh(b'oH1!gчq YsUux`m>@Ew#X@ 8jPJvP tQ2 |=B @}   P7ЅH3y{=&rX$ u1Iv7( @ cPLh|5G =nw>@p]! iQI0vZ[< 6#7Ej7K"W 5084 iP.IPUx-su|J63 ΈK&ɵx!peBSKmxZ~u!$m I,yVԠiB(m(T}Y)oB !up50$>r1 B&!u +1n"9i R iGVJmP  )o1g%n%zfgazWu*EAa0mc: mXW0AIy8ΙZ(<$ЯO#LU|[SFb(exȲUg"Xe-K%@A=XANf>z[eyW\$e;A[rMATj˶l(n{)t(k{w%W}aol[ckGn۸pkD[ayof'JyRxr&w۹%o~Y#DIJ```;JbUH!c**@+Ht; 07s&0p $[aZ.@ >BUB pU3y YppAA G  !, D9JWLxyyH%|Q-`;*25*p O?f @u R5sɠu1 !fd 5P Ay >W%(C@J,#a bPA A2:0 aP +p\1)2q2{ǁT\C61 G6 SK:aʘLk,?WPc  Y 0Jc7+ˠX˭ HG%{Epe4` L%λ!f & [yLV|Kt}0sC]͡yDžϫ;0B]G ]]"!u@`[EtCӌ$^6 %asJ0L-$4akǁZ\Y2P3z qֈXM׿Ԝ10 pu7Јt8'[(B$am0@LG0Ӏ0L)H|^9 9`U,7 H՟b}4\0 ?-1 Ĝ˒6 E-~: 51;t!:X1+v Q^3 +,2b`g~21AaB <@^Cʄ hbCnQH%MDRJ-]SL5mY cB ꦘ@o1"3l **$zLFN@xń8śW^} ' `뻁P`H%9^}A攚ڑNXj te*X)`( qIf &(Zh0UgFDTT ߜ&]$|[O\NZ' O@YF} |i9]3ir)ǒJ&7@"K@~Np7؅O@?xĎ& A)Uz ,X+ x‚ Oa $/9 r0ЇE  ЈG`奈HbY-bغ+j_#v,P0i8a t_E#H=n20d ^!]BY%" 9Ji0IƗᩁ( bwyʀLNO@dəIu` u)@a )ajP;c0j9N,X"E @@&s`r kYpD |*AADNLx#Ad $!HM{" $S;fXI\P`w@ D$ ,H+Yԇ!L;fCfCx@,C0 !6e| ֘SڬL I,DJ(6QD|*6f & dYA:t`xkhU5%|ŏpDD]!* HP*m"3P P"E)U6NAZȕ*$ @%0܇$mXc2T Ĵ7(q;M il\_IP ]6 hSRIc 0b^Tb1Y՞"&RD` @|BG"QB0jFYC:d [\v$ eWvO@&Pus[{b*wЬ#}-ot3RU5r'YU%RKj+Ȑ6f>A_"g"9$vFgFA|ɗg/ї0@% iLgFW ;VSUe zַf'hĵq=' _D#y=mJN Én vGD#%`, -Z 19(f&P@%vۤܞsH&ULxsU $%o|V_÷jjHqל/ gqJF3,pV 929K.,O'X.LQ(s(鵧"_QzdQ2—=Q9fMDhrTov9(aB@24*Of2 +`&fYJi^xf菰, Q-!XePW`MWĎL7HK?|8G|7+ !,X *'L0*\PNI(dHfFH<!,1'!,1'!,0'( ȰÇ#JHŋ3jȱǏ CIɓ% "T˗0cʜI͛8s2 @O< IѣH*s)ҡ:JիWX8'0!! .T@طpʝkQ!Y0 tOr%Hao̸E*. R€FxN0@n=^ͺ'o"+uŰ'H5Ж^μ9k*B65, @HVhSW'.n @ ~%^8 @5|51 H%xBhLO*EuK$09e1-2A@!{0P+(8,QЂ@["`PF)%F32y4]@LЋ>0Ktf1b~LHC@( i4$AcFvS&hV#P 0FH5@?y8L}vHL5q1A@1 @~J2> a%OȁTh0mAQ10ޡ0P#OGHVa>H3kNd@wV'di4G4nAlчT(AP0$]9~蒁, q'fQ(F !s`0,PK^5|X~񂅡B `B䇰t~p Xxl@.p"*+%_!@2S+t7#+KsS"@z50>qOqL\QwpFhƔsx8daAƎM3@E#8@ ` B0D:P6bSaTr0|_ `ZءwD&zQE`; G$ Z[$ \Z=؀ cAX0~! |#Mq #ĸXzGa "(8 Qgaz, y_w*&:~D n+le4 @`#$ e"h嗙 P&)NC&*2l ~Q#H6& yhPw0 0!M BbUc:F;Kh1 JWUqFeNe1tK2/I,k.)P~2`'P4SB@KXa I3un8#+4+.ڲP 2n%Q p"M6H aKU;N"{oǘ<j p_ alIxFQg 0^\ˁ<'G $@ ,0@K)3p3@- ,{!Q,Ò"8dL=Sb0fqBr53C +^Gt~ L|ACb=13)  a]ѕF|i1:\ecaeC0|-/f.=; HhJ"ƴQ7M8QqtpEL"d֮a+ǹ]3C24PM$Aì1dHSMU=j}Aߙ'8dFwt(5;J-_1@,a@}KLOaaU;AK ^oP HbDtAtW=@ ܶr1U $2s AQ4S%rzmp7h0P 0e`?q$' 0~\ aq''U7pA]Qy!AKZ =x5C`[SaQIO8(#`[rkM*abL=#h.>d8¤By~|q {̂ v1>TV@iщFmZď" rc,p'pGBq+Xഴq5`W+=![R$!GhYJ5[QN Q{!,Rxsצ|n Ns I#z& Q[#Q_a[PZP.v0R1 Di!nBVij=C ?%LH&Ҍ&" 0`R: 1 -r"y%. P\-?2c ۠z!>Y#J@^N f <~0Z·<( U00#1Ɇdave#|@ ab臬^qC*.o*O,% g  <@< ! b/l{e p`!>s]q P ]t?:@Mh7‚ ae-}` La N)PŹ d @@\ ,eu JR NBTr(!Lc y pѪdaPC"D\ B/0` ! tLEV…e"haWN'@ R 4&\&Á+g%d `c Z~X,AVЬ "脆 Xyc E0΢YX#DnU*ĕ dث QX`(8#'njEFPM q/0! KL&3 (+'8#!HH+;?>/pq3C jHjQdB&o#-R PHp++/+5hzP``J@AIF-˸ f-:d!c%ӑ&p?`lpR& D aJ  : f!z@J*aR@飒d 0XB,H HYW=s  3XoBP'D(TJ`\[c!é`f@@3AIJ!ހ-ڋ0(v 4n F<*RJ)%*`'@;L/Z8R.tkSie"A3L@ l&[N Si0YJwqFbe {g$!Y=I H&ljȝkAfg|if l!z.{%h C'8TX`a5PɌ,Rk ^*tJ. \2a Au;S{rgB6- b'" g=&q` B-iL ;` Cz6NB 4:8ReR( kM'VWM@0R@ ``raZ,$%P◻,ŠI& Y4Op IpgH QjjPE8 \,q*piDSB )4,F wDD2y0+8&dx6֕\^AL'?HcXqY KUDFXa֫#H.+xR8+` (OD WĦd:P z!hhA v4OP l)SV M@Y' %L=n )PHNYBpaa٬$"H'N D#`;4PJyL@%u UJkpk @FwhCxX(%h%F`X"F0aA%tP'vq䧅P0PzYG|mʄOK8\Uv^]E`8 PjЕz!'~0u 98]0fQMւ D'HR eB*hYoIgWpBĩIlj(V/  g Nn %N&'(K$ܮfdCEK{[P'V3M7$ qK(%p)?a ehسQ;Y 8 H}c>Pt 1amzap>k(€68(GQx4H-zHP4*80PX JRⳫ "XZ `xD Y G,DrY2xJ x»+Q?- (Tx(C3F8pT8ST0HHL#Qu5 x8R- 82H+g1T۲;)@ R3%Tp93?*Ђ 0$5ۀcP  8(GQCZ >[8%: ( zB(^nwjЇ#2| Pr}!]Ȥ5) JЁ NHȁi頟YX@89gۀW ؚ+*,c Xϛ8(3pGY`hJI!HZzY.pYP8N(ԸQ+JHCQT˞7(%9=(ť TC 8 }7:+XT8(b\  SH ЀLLP$WYX,dY(Y@$Ŕ P}a `0!aXQ: #+š ((ɑB T*8(Y 24s>!’ )Љ :  Qʥ(ڎʠ=Mѫ ! ;(p! Ы !9O `7J *1uQ  w7X `Y+M+ҜOH;$   4  gQ9:)Mӑ@x"07P`ba57Q-;ے04G5S8]X#Q+&p+ y"W%+h XAJ P A4 Jۙ`e4 (;@Z]J~;Ê 9U %+-i`d%XHR8S@H9͆(E8 hETHR*Z L횒6鑊8ԿIFT5.ք` ͫh)=~&DS0ȈpIϬp* i;h]8J& !]E G'Uh*ُ}@c`嗶^ :4s^ ޵hJJcIz^1uhUPpJҌ-De _ ^ 0vh-֭@  Eݵ[ۭM]v]~]i`Q$];` ߌ^] 'Np_;! ߃ab_"v8eN>^6^=8d %4['^Z``J8Խcpa^(^xY qa d24 ce_ ^~@ 6#IdLKd 9]NL6Za>b.5Uebfdc &`~ݍ ŽKd@l!O^{>R.XAU V֔لcZ>^OBP^0_f" ffTX ~8j狦l ffp 8g5>iwNUyت ed,]IDT*ʛ =, bLq ?DUfvdžȖɦʶl#j8:@%x4=Aه}|؇uy 툥.+i82`0L`H*Baa=MP`>yAq:&009&8aK,LO )ԨRRj*֬Zr+ذbǒ-k,Z @ , ZxÁX1>hQQPa…&1YT ,ЬB*:b! %R8U1@pӴgӮm6ܺw j+7S~;!0ӾpL͏/*)k'NX+Tx<>D  Eؽ? {aBAR +0fN sW +|VOҁ45P}4j"6>`Ѐ9#=W)N /P ,`>MXUPr L Qp#Y p  BQE' $q9'uSA]j"Di.ctZ`2sPي X0cN1^ :c"Fթz4PQ!O"s8>8@v+k Pjy@& 6G @ r V0ݨvT5N B$/kS*XT]l,bpDŨNXU _>1)Tn,tTX~)C2c|39GpX `0!/\. z Dܹ1D1*Ȃ X7*/;8A>`Sxl x|7yje':XL=@ 0^,TNmi>j DU`Yl}Xѽ&ڢM\3 "j -D%X 3LDLD,<[LDIELMN>$ȲvJCuaL{!N 69c= M@Ka`01'I,0! wb@M ꘀ <2Γ c(0*B$?MD1?&@? `@0td HpJ0'ɓ `Y* '`Ilc8@ B@U 2{0cP\|)#|&<"5g(IR+,+  L'xMl!͠؀4 2t/HtL@˅OJ@mlP %_N2l` պ\cy$(ɇLR @(H2)L XSi`US,q9.k13A|"Y Hc%N g c(1!Bd:UqT5ǴBqNaJG2 1c R-C$`2nN3>*I?%PL"p M7@0SbWQBx:! vc?.1Z);Q'xX^KXⰛ"h0SrP H)?A_Au=m~,e qObALs"Dd@bYXObWBcve XG}L x 1~pdh.HSN}{J @;$`  LACRC-5nj2"+H97Njm$NghFR\î!}JEAP "ޣ l VΨO~8 "NX`E^()SHZN#ϘPAY}W₤ LF':"^ hPp PqL'` Bd&$2ܡ'@DEFmL@Ke&pvQS`@% MqZ&ZM)2S-B@AA +]wC@S% |x3Ẉ>m GO!F$r١F 06L90E|OВLA\AMNHD|@DDpH 026ΛOB` @U;8 v腆iTq 5O$7SX@ iBE[9S,^D?\†O {*9(E,e@@ dBD1 jw񌪰C7j 2ș6Sp QECE=<DT C+C{N5d@NTgPLEla' E3@f0JpW߆ <lkHN ЃH l) O,/BHdSl4TB. ~B?PCȀ1C?8B C4NA9LAP@m A4V@.N8@P>tCw6]gA*TJ^ {' y8 j/\TII4@9tPM(@ z T-x@ )O> MZG,[: X|}ȀHƽE FZMj $C2 TX@C/I;P5Ӂ19=6OCŧJd:0QjP62TO>|+<($@BGl<2BH+x'k^@ $;6*=SU7PL(L.(z3HSW8ƅփsxv87h\c2Lo-j'ō%1$t?$SG9ʮma;Y+e05\yW:m N 4$0hHcy,~-,DDc#X@=N0=+@(4S _D;F@r PY:ӐN< ?Gz'.D-WJBƬl z l1D|Jg { z:hzɵgO]%4zpG @*@qHT^'Oq-դ#8ޕR<4oC{16$Oh#^H@ X󄔣B.em 8eHu 6ɍ|=<׏O%=¤rOŮVˆ.bH0f xz; Q'OLC܃6+("CY@<%}#qC%@)x^`X QnקWnD|8`@ SY✎*xg e8kMc "@0 STh2sAC`q5zLIOWWaC7]-bM |g= @q@A 80a0TX0aŠ&@bA 1PQ! Hh@ń$T =`EDUPZjUWfպkW_;lYgѦU˵`G!NKFTqW+ߝ2rPAȿX zc )pA1)`0yb3nVqmiӧQVukתۆ€2W7g zU=,ě& V;l:?|yZz/_nD j߰ {έ D -{O9( 1̐<={n곯*>; @ "@<@B!oG R! D(f#*,Tp J:$"hYfRd48RF@ j` !2.)0STꢄRLH.i"1TM95ȥ,X p8 R( O7KqzĂQ& tX(@ 0g;&h P|}B;d ΐft3 (V{ 4`^y(4NX $ Nxa Չuw&XJz56js"TAM ^t*+ELHbyvó1(!!8rMpD>Ul yԎb/@^/@/RQG/`<QQPd 0~V@,CC(b`T 00 mĐKF,/>&Pj+ ԂdL !l'MEMaѸE383B l8Ec)`eJ>\  Xjtr-qKqa艎L:w$\ՖQX@ V PG9Fi8c<;KA_ 02P"Pr ($+:u2x.P0G` jC(LNJQ*hQо!  %H015CNGIL`2xVt\A჏R PDY< L qe5Y'PX`x2`81FݷL"!0HjRj  XEMPL&i^Q,`⑯WBealR0` Z0(+6$c b R)QAoK@t PHCzŒbĐ"R*( hGF傾Y@ @ʢ$&P@KX`rU֮6@Vp@ĀݰD%j `f:  E`E!J1W(J AF.4\R$Qj̅U +\!L:$ `8%?Ƀ_>]bmα|0 5¤ގ|˪1-|WOpg&Q@*@\L! DIUzDє]4 @/ - (EJS F\^ݛʙֵTa2#uJP̮23)HvvC5ށդ M) VKjkq;Sp( 2OhÜT"~  E^w`bIU "X&!MCnOz3t 2)Zv`PZ *]OXLhBd/vJ@"&B__#6ԆYl)i|ކ+k覫+k櫯Hv 0XHn9ZJh U<04F#0AHI(W-r1la 6l'p )AwB(a@!pLW+gI )N[#$AXY6NvN:\IZ8wN'lu|߀.n'7G.Wn5c0 d ` BA`@0;g`t_#AsA>%@Aܸ 8F*мAjRCeI~|xI4> ‹ '7>A?O0" r,ùcT9Y5s+@0G Z̠7z GH(L W02%.I2Տo4 /?}]fW( !,a(b'HI \@ #2t("A-@1#Cx\8ȑ$'Dɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`b 5@mggn0>e&g/!,1'!,1'!,1'!,1'!,_`]H'H*\xC @8$8@jȱ# )aA =\2C0-N`5aF8s.t0 WJ#O ӡ<) ӫ,2ݺ׎ZvJٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL2Y>agV6LaSh!jM 9 .&tz Nȓ+_n=G='P)Fv]#PT܀И 0(48\bܘأkh Q@!,_"!p'H@*\Ç@3j8`G@p#DŽ$>l@ * :22B69q @= H(`QPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ M@R}!ȄudHw] F@=.՚ajR|P ( l1XeJb!,a@Axta<S8!!,P08'H*\h -ZL3 4`8#Ï CIɓ(S\ɲHtI愙q# 0$3\ʴӧPyӨԫUeZʕ!&`'Ί]۵۷pʝ˳VnR0jOx LvwM\5fw:j73+ެqf=jytIEԼZ|>LیbV4j+]7#uW896XN+ș?fM"g |-9k!|;*~Kg/I.ɍt23}o ڥX-e0L-TuiP"+=492RqA'-0CW Pt!p.5̍^U;ǒjgMs ^z,Fb2O fvx8DZ'-KY y]+U|VBޒ")ocJ1[ch@xVͪvOqEKZ!"WARdH2qҚHN K7Qmt# pkMM :tKZͮvz xKVDMz@װm[Qb5)@K#U`B`;XAB)3a'ΰb8RdʰGL ` ?;-g ("`ЂŘ@KNcb"M&m 概X2Hª.{+KǬ1P=!,`o"'H*\Ȱ!A! (1Ë3jHQCQI\ɲeAآ'0pɳgCAI'R Tѧ>MBDir j])UK+TJٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸,>Y,2LRA͜!,1'!,1'!,2'(0D*\ȰÇ#:B $jȱǏ CIɓ(Shp˗ )Z͛8sɳN| JѣH*U tӧPJJB NT Vsb:0($~ ˶۷p5b5;`< bֺLaw <1pɠd"8`ǘ3k ߄x.m=b 8K r ۸s;VA VНiCBPdU2У;U7a +xLLP>ξ{>&H~*G?}! <0A80AQEL@h{VhaGOX7r7'8$%`@0 Dp8(w-D̂37AAi ~'( " A7MRdA2LyA,)j]@{6! N5t $Q9Bur`őf3jҹm|1MiӠBMjeԨUj֮9Rl Z"c'Wc^_xA DܔΌ F l @QMn4h_@7&'{.Ҁ`, p4gR!&9iw x@!c\7h0 d2qlvٚ(TN)|㣮H,l9O0|?"}zy}}7@1bzګA @&SMўW.q#u.fIq oÎA% T t-,9땠ު We >al|Ǖ>Bl}_?JZ-Ʃ1iWSvfW7a;csm( f'EH؁+ HiR2Z9Q8Rv# re6@E_VGH478+sjY0V`Q17~UW} n*U/ͷkR(Lf WO6Pv.0R-R $d.]q 2u4i [Q ]p@op 5010a 5p 80!0 ;!^p 5NHT{,0@U([  1!0!8C0ݰSz%\?U%c\=F  %8PP(  P4Am81Z0O 1VhD# GC{@‰*a Q !`01ɋ"6/(^.5Ѱ :3m9(搏pB$y#d)Oy#UPM5a Ù  1 q =Fu觩:;5%Qr , p8 9 @$/BݩSqgSWm5[- a 0h;5&q;W5`w¥] aϸqP)`;pN%`:U6~qS" 9:077ɠyBH 8J6c|2R8ewSa<;8)O0f8AFA ZJ#*ȁ%0 0w8O))' I.K60 4ؖ4-U%XQ4+>!+/}RTqT+^'Yq/;JXuԽ'C9z¤7HveM1K=LIb 2#963hOvG3"Ft QJK *`;GDsqQ* K ǰ+. q kJ6΀ M X {p,L!G Qy/=|,! 1*F*83V<X| A[;&`Ng\ǐj/Z\BPr P,xV à hcAA$a0 ,AF̆W~0@ XՃH d&~Sv20Liß"\A;r aiW_G A͚` aQ%+>`S`Lڠ0CuBx[%S V vU.3aTҍzU` (. w@ JZr^QfT5{ ז2x ΀TpB:NO :+ɡO=2'ZQ~(mۋϭQ^ThɓM=}K3PhqСi]2T7s+A6.Ce#5 inpvs0A 00TF+0 QQoa`u-EB4#q ֝⧈J۽a21n3l  i9*-llMNwZ XyA{ @ʔH cT1]($@*[de QV?H!M^^ 2TT}p)ޮD~'srU@aso4")Z;Ѩ^jy@֪,2KV' =~+P 1LHڎq3n AݼkL!#+ ϓO̹dd=f r@tNQ6"_mX'Rкj^0Ъuyl8QG/Q-&5*@ H|DцWn 6O|ɺ @ěKQ N?":X aG VGe"$3. Q8\rNg+UVZ Z:?FQEɬ>0@E3 SBUj@W_ɺi$0Q2vm:U_bj tï_md =Jq(^lqjle#>:1`@ DPB )PXBC-^ĘQF=~RH%MloZ\|߯ܙ@gJ΀@adK*elA+v垁lAeʙ df!^h,@az?WdE@ @b,gK& eqbD#HHp6JǪ!tHl `PJE1$(:A&79脗:vB/=J[g T"yt#' 4RK/t$47@@F0@`y/Ԫ} " o26Xa-s , n&NQ'@0r~΢''a ވE7]u$e ja0 `C̒̂,y)jA ȁօ8b#@ S B~`?S!mDH jXlbo+QH::@ ;Rk2R1ڞ "DR!֜:kXk tC\"@PBPᡇ!À@&xT7պoH&ՄԆ)}Ep'#q/.g pOG}˻['LJ%(8aOw߇n&Ԟ!)x_)H)lf?}Ss"phy=| !%o~r7.rW~|ϙt` |_dF~E¶=-Ġǻ i"?Lvi* ЅbGAHQ@+HV @Bj2e ˴Mܧ $ EM8>4 .S0L06^@Kzo=y"nn@UPtS]&NH$o PsYA`%&%VЃUKUVAA@^BRTPy' "Z8h#F \V \8ЉczMU7aW@-H 4pSUUL)$RQ,V8&k}F'dvTYC0iT>dx)SĘ@HPSP̩P'ӱHz:TBUk.2"D'&6F+VkfvR@(.B!!v2nb;dbݼn/E{/d| ? Wlgw ,$ԫ&ǡj@Uq d̦3?*'d se8]a @)l B~h:Y@Y(p1Ȑ<L Ad\,#@LYO$6"LohqqZbj'|if >|1A?sٍQ`Q2DIP "jF؄70 d`CP02 P41 n$)sq>8A>]@@CPu $ \Ď6P ZA2P Ka`q۟ 9HYS.`@IȪ2aC,߉J@!,a ,'tS1 "WAx,a¼M X0Ə>NHaB@!,'H*\ȰÇ (ŋ3j1aŁ;IIS<ɲ˗UI͌' bɳρv8q`ΟHLJJxV *ʵ+*D J]v4hKwUӄ{'@~b{K[:TP)ka!Q!Xݼy+ĹIhzװc˞Mr Ӫ*8@bNn'ܽ (L%s=w6\u- l50^+0FAghI{ķw"2QEtY BaD}͵@2}BTh(L``XZH[ 0 Am1j< f@ Vǣ@(LOCP@#ա#Pi@@(h贑UI98@ srAhb#4JSU&|CatBC2D X m&$d0ި jF <7= ^{l'SG+Ev+k覫+k,{ < 2 g*r<ʘڠ;Av|},o 8M+0,4l8<5гCH P'zaЂATWm @5QUM@${;rQ" /.F6Q}woCtB Y>2s 9 ,=o7-‹F$ 4LB,#e0>14s 1*3) P 3 [Ap*CgT5>@Ʌ:@S8]PB /?1PcXAAm *L 1D`rCZ`Nt")$Hb d(ҧA~e"Q@⃉P\cZ(^ ޿1C*@ sT@ $ 7-H'k&L۲P)X67 X0#FB!LV3}N4eC6EQ&B7K/PCt&ldQ%A0ubQ% 2cHF^!KbJA)4zb,"X'Ԡ¨#?4#M @=P"Oke0(= @0HA^M9"q)ܿ!è;B ܳ\BC^IfH2 TB! @b e`k a2RCB $` _dcJ%r(!I /ߡ]DP˜\")+|8\)HWc<fTʦ#w.C~L㈨ *Q0dxg8APl&w(Lv1_YgDN#8߈h˔ug.#Aqܳz% !,_h P'8 <@ (Ha`E 8U w)"DD9@AS(@0B Oڴ9Q`@!,1'!, 1  *\ȰÇ#JH!3jQ! AvIdɃ&S ʕ0c(R͕wO(QJѣHHʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸7HJE DrByS lxև2@ |{6,oG! $~2GB \T=8F8 H5 Qs$#j ,bpom1AܗP %@兩o9Bw@r 6у! M=*у}.)5 -b禛 A ^He6R ĢZP\ $dКm wzUF5@qPP *P8Z=R"灦56RưSNjDO -(9sL |n9܃3$X+c.+l%< ,І;P0Bp LS <@h25 .(.ѭ3;5 TK&쳋 IJ1dsO7:ݜbK<0&u @XԃE*0`PxƳEȂPuB"A3f+ wָx\gQiL#d.1h`HM` d@ xzf$Ynw( PF6b Gκ(T9H0e.@;p\F α  f dX YP6 YFS *X B+ H=Kv0Z?yc  }k.;$P [VB a d Rmh '@䫐rs@g rBY'ga|mmb841EE F9ntO#xx4(6xcȣN88*z9//ԗgA. xDX#P^%H=) 3`\(qD6q V8Ye5vF((3!{LhǦ{#AP^@yh> cEV/G9L?C@ iP` |a|ؚCPAUͬ 4N`F<<#=Q FhLTtts 8,Zf>̼8T*J XC8頍lbSl.cO̓w ]2OIr<ǀ PGŇ q<#X[hya)c;DQ`X"BkחW;չ~$>!9>KdX혿+'d&FD'rdH٤y$g~xЗ  9$2Ay؁XQ(d0;#AZ(;Ղ))VRA)"p" l} B GB"31m$sJ=` /N5[ &{&Cׁ,O@'Hd7uec&0U{fM@$7YMS2H -\- ńx L:Ej *0ĊiWwSA;nrMh5phF6qXR>ʼnyԓ5,`6NucHN{PP)EN&[PW&G* jiE9Ryx#"$hm 袊"2(.:i6z)韝~ jjꪬ꫰Ƶ*무j뭸뮼:@T4BJ{Sɥ,L <+- TԴ"E;Pئ@Bvk9%h()nz0#y9й/l/K3A_A(M @Wlgw , ir$'t)#2-21r͌7,!,'(B*\ȰÇ#:@A.fȱLj?I$Šbh˄)RM!UbxȘfJaN;y]:bS 8 0 !T`դE)1@ evzꉦ Deb]-$;4E $)S lTq0@ #vpR pl%)lwAb"E&8ZB{OFЄ M:,L 6@}6 H0HЉ&:&z.sYMP n N:dw#xk9VBP =U$t&罵3\@%@0QhMMP z "sH@ O Au UC%#diP@ @GIN`,( GU<j4'Q *{Ewn+@ )0CD3at'| 'eED'{!(Y\PP׼d*4 % BT@|IEdU%:)ZR(TG` .EJBB9%E`x%qΑ;Lѩvy谓$QzSnNz@B9Tc;^?\+T=-R[R- |J" N6Uij|aP8P4WcB0XLe b7qz4G3@MU,bdtLfÁeZ͚Zဃ"q;T0̐CI,5cIK}%ݠ6BJSx` /?p 1ج f ٝs yr\ EX%)@1sx h\NLjY!2 E@ . Xr 2-p?@l oi_ A`S~"PHv7kbK0f ~!ذ@1`-_ 8#pD< `G0Ay,aXsB0&TH }b $C4oR.HV(  dњ%C9 B gfHh |lBad#sHrL R|!)jNd \p Z@:)Np<2 ҏ6i y" C8@O6A$%`%*% n$d% f*ZU2@V9QD3!H&%Aٕ²|2QB +.3̗#!,,5D0 j{B/ԡ |*#1Hwbla1涎E@dDu.*veOm  G{ \-;΅ 7r@1%%OZsRHGD0:9Td;F`5; 1@y FY&cEM 9W524!^{[ Gdtr1MhK! I*:ǂ1<ғ(M1g2$9 *Pp*,ۀ rM %V$4Yoq]00 ^: *T% "F!)7>)0 h s!p99',` + 2e  0}BFP_%` F8Dq?# M`$P26JҠ0 7= BB2B;)"P ( T :IYP_C wq H B0#€f{ x~AE1w OSK9  *I%y3$`62PB2sG.#(. 0*QHCԷ& apvPD8еx0N0 bր0b{5D{ 1 0BQ J[ M뵂rAlJ"*"L $ 9#T| !l<@156: b/ְkPP*~OYD =+0=H`Z-oʢ i!.)192qY)9$޺` ,/H@ `ZC0+۳'T8gf9;=Д>R 0 +Ƙ;Kc!0 [b+fMxڷ) Z}K;P #簣5\`j[ >pдpIO0Ѱ6K p+z@ z;åШ("IL:p(=w F5, 0?U0Ghak1#  ]p #x7q1 5@ ɼ  `@܀s1[k8|[PngQK7m: Ш""P'@AlՑ:\ѡD:VAIo?"y9QA<3^v!@!! K"+ =Q%p֐*/R!:By!`ъТ% !`? 4hs(rе4!s`+|xV7foM8QDIRcDQD JV,V ,4PJg{n~9,!4*lZt' T;FDb:TDn\18=xiSq9|AHaPP O٫w; ;y/=kA!;ڡl@m =dl qG#33(a?]<31S{3ƒt\"NrXF%3!=Qs:ϣG]-a%e=IK ބ#@e;̳Cm]H)*!#Au>U kFAA$ sAFp0Jlϣ) |B DY[4p5ćW>A\#41[vzo!!|$X̊޲aUž5lN<~  Y0 < (P 𰟄CF!::.) ʁ]owN/0tڧ E `P!Pv(S 61A0ͭQ ?m&򫶷&1%5DH&0P$$`vx`<\4wcPjʀ z 3)0 wS֨"@HSf bT%:p:R1 ذa0= ,  5/p[.< NAl%%g0/ӡ!H!0pJ;kt @S A (b9:׉8J>GIWsf< { pիmK/yTt[&P H`EY @ CU@ `6DP{pɏǰ=&ք#pob}BĄ`&D>qa a"zpM hQH%MDRJ pAM"H, O  @Q&`( x =(PPaC5 bЖuśw$Ճ&;8n !O 0DС.F*,jL@p@*0 m* la"\^޽}M*2A*P$PbRԵ+Nhq" YWlP*04߽\@G~COzߏ,R FHN=zn, @ VXAJ꫰:v!j:Hd!8 C1ŹZ;į(-JX ?jC-ѱc.$"HUʢ )UE)Eid`n0 '$23Ɯ.-,T`1 ζ$2KO`ܤj@ ! -&'*PUIH@.Aα: m>AԳ(A3Ӟ d J TJzx&Pi,rȄbHBjCt$Dy&(:MH@@(u @!&`arVPDb@h>1 jkY=M*icm ޻֙07ōDĐ-,"V&FHdGܳ3`jaBFZƞiI66B[ڐZTq+|8QI 裯 &ɟKg gK``,ҁzQD.&? zR)Vfe'+e]& *;8qo Й*\ZX > @TRe^/ N  (YJcDf>@^4Ǽ؛Ej+% րMq!D~D9%1HćM246&p@ΰH@q*I)AL',JRBne$h0H!D!$1iL'YC>a kK,x(`i 0Ŝ$FhLhC!U0/b2HB`$QH{T :qa TH!;$bHFK*IHad@j+rTmY&} 3B #/s#EBtaDC0IzQr:FI11#& .aG3f`,%v_J1|o{d_>W$ƞ$aĒT*pCʜ)9F,o$"(*޳LĄ@`%C3L,+ k YLz- !I/%@}V(7{SfCi] [QEI 0~5 ~M 2"ֱfS=ֲu+Tvֳ(BW[ud^3Hy-XvӞKI=fНᱶ-XSghۼq=p(̭ kb 3Vݷ:fI$RH^% l9\FQL/<;Pq1%4&>n2~\̕";R(?'p*"`wc YPtDH6ID nw+:OED@,$`pTऊ[" lMBIc!РSl!,4$sN@@ķro\ OB h/Q IQb\&e]ҹ\P}#+wW^r?rg;*uLI]hH'Jזon$ +2#ʵ;KtTS\JL+]-JVHoߴ۰dIF_Iq[ Ƣ$[hSx?w,[\|1oJ HEyj[X)㘧ۮTPR+@--k>`Dr2&_ PӲ[h*=yA@+ (: @SX@ m Ȁ3pqi A1)9Ych9˳rހ vx`BTu?8/4C8 q خ'^0١eXĘ((Yy69/h1zIß+Qa;Q9Bv8FP.'h !D@"uz nj H`kĦr !*6y!?RCp :* E6 +ܑKӺ'|!b5h0&cbƣQJV!;W=HSSiCw? .U dы2e @!55c67-S:U;J;9i}@x*CT7JpTT'ҺzJ% LEY(9I lC 7"NyHx>` `%`] b/ؿʓƾκѕBY¯J {`ܚy0S08HI֐y( B- |ZBaOSIPpI7*ʸQx ^Rx#rBy) pBi h*px ZN@!4kF (B1 حp0Pp t0{\Hs*@vk@*%W`50(Dv|pG|MʼnD|D2?Hx?Hy(YRA촟-[K;2ڱXǍ JЁmi?,P] SYyx$P x腐ɓMcp]l(\+(OYRJ @,H'PzkRTd{~P}H48Efn$2Dˆn ['} 㤊01y3F \Aז.D(n(WpR/)*SQ9QT(.Ŋ#,Q?Ҍ`%o ,rir;Q {7}9'JzX x=g x'Te,cr&1əi*V)5+$5ɰx-5Èʭ-E#ӢmmmmXᆩA扺f/irtPY.0aS6BQr;am2 )]TVG!}9_ QX€b'aEK ۼڎq^@Au6Q.qek +bϦ0[Ts 2 /xP288Y< hS Oȅ %Ŷ(, `D[ /c (%| s/]97o sOHb- :bgYeT^xa@eȅn{]8QpH{؇HxV<j˨^ 6{8 Ji/vDX %Z@ț ZX Y_Hͤhj w@gPwMuH6vp:nHxc2(J 0x7֊;XrU0f( iYT@ +h K%8py !]PaJv(~H2 P Jگ',{WQτscX#'ƽu Y:&^sp 5=Gnz> / nymx}#XPa@Dπ6&t >ӊ‡}8( pw}:wr:OP6` .E _+*Ƀ&-**` 2l!Ĉ'R81q L8!J@$ 2 0Ʌ>o*T0Ā VPE )* Z+ذ k,ڴjײ{-ܸr !,_ J'b )um!&ESLw1=;bh1E LI -s$L!, 'H*\ȰÇ@Hŋ3j8P"Ǐ CIr` J\ɲ˗"A M-.iBM2'PcBH1\4!B 5@PPEҐT"dB\{oBJVctmN@@=80a(r COT&0Z&8rA UMhy" Ot/i :9m%ԍo)'0}mVAA+1lDp`vO>>{ۿG/`v 1t8 հayde$@@ <s|! Q!@ ,QU GЃ >= #D7aP۩ ~8D#8iQ$b\E$0Б3L`##9: "Jx1F$ Y`A EI GF!T8 Db(,tBD $E" >F!T!PE nvA]d fE`I(ha_ݦQnq &Z @.rA$i"!%p}&ȊԤ@@@V8 A t P)P $R MPu-A$!<'?QI R{Sv隄nJ҉$z,X,,A l3A'46 f,2-A,hӂL(pF?MqP.KcV'{q:MN\/mhlp-t-v|߀ qv +iQ@ՐT偅mhN5WpwQR{X-xg68cp4< Q&'D$T ;w, V)X qP  gfBO<]>L3P>zԃP .@fLO @;3 a@x d'c.AJ1q<> O )" cfIDO G/d =Ru $:0HND8%N  YAǾ~0 (@A'I4p@1E [@X` CE H3Vc;s^{H(~ĉ0_|*F8<XuPb)H lxAȘ B B$'>'[v8.#  C $b(,'&bo(AB d`<8lD0`=z;$: 2 f \&`fh!H`F0a9 YBHYAgP#Q$CH'p4!vة1N("=6S #hAh0n{]Y@Rl'<30(A:bx0H62*:d)HFUGC䄃 C1c @p.|LG3 B dHAA а@ D bA^G:AYN, v cLj9-*`9d# 8HLbx$݂I!U Or`n(HwOlH 8w}3UЅLlL 8 S@?{ ~ Q7!֥O#Ľ>5iD 3LK!VcWnOp< x (f t ! ݐ|8}2qr*b67.>Q2Zff4@l1Bq"5tUl I-hB#(3FAS A+RBF,-gLDEgp$0~ @ɐEԫAZܙ[BbA B"a?DYȞڱӄt4B֝3s"F=Q\X FV6m\ 6ELy>VkFB`$@٫ED =F`Y;{'YF&c+eFł2*yB'! ѹj @g, =0P4TcY^pYXWvtbM".]'@dj6^%|S}rC6@A|/;Ysoe+g$y *Q-Z__Q'st~%Ht_ᓤ(m|Լ03O[Ͼ{'B<! ~~04i궿@p4YdH [8A(WG H (~7(66>p7`|C0wH?Ѐn0?gE 0zW*dh@k((Cphub Bk/HiwM1R!, 'H*\ȰÇ ŋ3jq (vIɓ(j˗0cʜNϝ j" Mѓ'QuE]Zӧ (Ujՠ s߄Ȫ]T kM0!&vr:H3 xQ&5e7!G6^@Q:LuW DbA-s?됷@ۺ Nȓ+_Zs  ?Nx-RPyu0!& } >pw~x噷zT % 8(X# BA'BؒPEr2#iBLtpVt5bOZk#ŸC{K6% D)"%ƃъ`d' h1YI/_|yi0Be){3.gZ` D'01˺i_8)I;H A&-:m^RWm5qD 8^\ dmhlp=]MZvVHō!&Et7b: WfA:7>Wn9[bL05Z{ KT', NUʮ.G0=gK "ti!dIx??4D@l6U^0A/qc !4 $ AT@70PHp5俐@03WB=AM  7 xOJ@< |  hс@!kĉ< f! 2(@ @3{L `8d]O y@kDia1!%LAΡA Hv I@{xq x1&#A1g8c3D|09CEc$IA p @7`!^"( R 73 4 1}DLjo ]0P=!ˀE쁋i 1  H6jb ()nB`qr8&ғ16@E##b? fBDn@T`KY$A 8`ʐT6i.'A{=()&`6uh~KlPW`h̡ F ʦ !yqAM4-FQ Ct҂AY!DEH( $;zH*>Y|!xl<;+nUjh tH.̼U"-T^ʥuit>L4S-jZ&qOyn`LN8aRN'`pJ8xŬ))ͲܪU ܿ Z}:a[ xN6_13۝J*l7Mp@bf*TLhwG 0tY a@GQ6 `Bq'4AEV' ,nT'h =0&DJ 3O(`l!pKa%Oa3 fݙ"A 180 Pqȡ0x3 ^p!p h@#}GN&7܏=0#>pA 3b9Aw$B?2~1f|h@m[("%fY@90 E0#rqHedQnOBBQ@$BTQVrB0@m!  P%B<, @RHN pQ5 *{" fQY7AdM[T@:dqY @q\VdF'YDhNkMlLdZ%QV*R\NxXBwME'5~' \瀡^x=fW${Cn65FwgU(De~EPwcvW%_Ip$Wnr@ \ LMx}Sav\r(`1z X @rRH* EaD)aX&rD r`-a8xB,DjzTo $/^\†0? Ԝ 4 iHMH(& $%b6$;#3 \m"Ŕ-8a9 `$=@|?XHӗgr rTu׻@e3Q~/ gH\mj<2&PMMO"w(QGI 6OPs#IܧP0I@m %W[; L4ܔԱ"\@E(b \D0yl[8H|/ޝa'D5NI x{ Het -Մ +U jp@~XCB>R|hu'T) 8,! ̐"`XkYn KQܿ ɇCT()lB ɩ}p .qOM1EQaA0BB:$kSᆪ9O0{`˧Q4$0!bSa'BBh8u$@5s71'Lla H ~r1Cԋ]qGhȍ8()TD嘎6bq꘎q8&H K :؈mh!¥>m(ZGu Z АE7;ً 2/Zgx(`D!=Fp;Ǒ26]zVMR&ɍwf~y1|39#96i3Pp! |<6Y*?vw1 ,=iH?x3T.wQ5?hqkWlY\]418)v:J?ac3'rZqKDc|i'6pf0@*^ag{JDKiPh+Xu461@?lF!-TqaEt"֙lOt!SATfFu]7nQ_Q12ȩTaɜ!L zwJx 1ĄgT'~`=w)x' *0&4!1g>fC ! u[0CP߀.q3"APa"pr>tA$D[3w&R%0p+C1| 7p :076Gx00(@ h0p E@2a{+0 x5P^0[q}Lހ z 0(  L72Dk\'!  `0N a [ڥ>"`q;5g:V^X؀vDsKϋ   'Q[iH V@0(m jU+k 0E66 I*V 3 8W p yg(  c3$6oa>UO^:@KB{T!F `^?<]+SZM9m0E<EYAQ: X LH Ң b Y)M43bmE @=$*`B' A1t1PHP a P5$oP&Ea~%|!X'!6@b0bA*p315_QIR[iOt Pf`ֺ1( 5FmD@ `i/Ya mS)CDГ 7H G!}Q_[ Ց͙@XsH-6C\'u(#1t- `U `w,L>tQ]ю)D*n^EmS+%˶ J=od!p!lyyw>v :sDqed $:o8d>n8uٜFl)({: 9ꅮ;]JE((N8fByZsPC>nq~=:uI}5sN8 ..(A$3^C0aK(cNi6$GuY#$ፁԛ>oDMN p|t ?:^l`~2ilFlO_1R|ovq4pRl)u­)ps(_N^q2*v'EW.F΍)Pɢh?Q4r.y#A"2!\qsfP'YO{kk}à^OH(wctI<:n}"l+ 6>CQK`7?[o1CzS~ 0oP@?_dH5`WT$L9`T! `m߂ 5]oK؋,w] Aӭ j o Nծ)&@+_/ R8`B;-BxSfL5mĩrN=}L B JLX7m  L a`YoQ <$  J{=`̈ P9 $Xbq.Q !B `!Ax:.!AdlXkӛC$Ll%bT r7u1M' $rOح i u" ANˉ9 / hЈt*3bŠ , a63Yxa&`jPdB&& "C)' Cj)qLC'` QETh&2j{H%Θ " ! dQ)-[PD@#(f;`LJ\.0TQ]b(h'|HFEda '?$t4Ye4*UN8@#l!O'. Xj&/|Z( @*ɲXo?@–8 iaErه+Q# ( `̙Za; N3`f &Κ2*8 Z:@cZB@ IJd@‡$طx DЁ( DaS  0 &~HZfB&&1+ L#``XǛ`Ey΂YOo3\!cj\p |bĉ4T$:2}?);/Z$xԯ_D ,\݌#L#T)>a$&']){ ^qLy̦@:kA(MguCUT#b=-π&0hӼn΃? 'B(h@n]TLF&,D Ƿēp<z.&ڀx:uiXp &Ѐa:!> F 1 `DP|3-&] `B04ogwE n2LJB:t`czA0p}#=aW&=de  |CVۙ8L#<`)FGQUņA nxHP>NH=LM`L @ XDJ," \GWZT 󙌊&k 3f7'Z !H9j evB:2&a Y_^CKRp# r*`ɃRi|? NPax X8'h#H瘇36DP"xʄ AA*Va Xa;@Xˀ*AvdF†9%D1I[< 4 %k˫5촍I~*N@H<&Q1lOJ \"EF{Y 2>12̩ԒB@U RRX)[5|@sL N,yv` '`;/ rN@*2FL  9C1Nܔvv'x=D 1L&) ;a u J- !Pf>8 ֘T&F 0c 2z$0&0`sJxf0!N #gJQ(bmA1wLH%*p)쬉b]Y@ Pŀ<rD`˘xC Lu{#pW3_@k}H*IDCXLGKĴ @,'0p'(k$&ÈIz|Dh9Qڼ1-3$;3{oˤJ'0vUS٤j@uswpruDpC2xYm$igLC!3MRƇX#19@oO{';;7Y?j?,yK8J lP!p Zډ{ d6 ,Y+)qp=pJtr :`H h؇#~xd6Z`bx %r ; 7(Դ΃ ن %|/D9DQiA C =?X0<>ØD:T̡ϰ1Qj>iMFG HJT7LÇ5:OLCDPAK!? + +jEW2P, Z0AL 8,_8+*(āZyic'ћiwEhT@‡ Xd3F@4t"g oP&1#5@pnc:C hpFL Q[hDHH zU+XsF3"ю[4Hi+y|<x]-7^Ay`F[|<qj 0ŎH$G€)6晬tǐXz& #*>DLqJ214Y6%hAX /2ɒ z z\0 `.A %WI\1hJ{s*RɇbsƹЀI0;s"0Q\(ITEg%y9[~q خ{ӋHpEAO"؜S,)ߩAp3P*-`H#K@dH[@,UGSe]!(z1r{"+ՠXxš!lT9{RP|VP<7U Xx?C}BV6]Xk0zۻ[Wq (,$MΘ\A kgpKXb 5 L}Y =zUzK xHZߡYYHxbĞ@llRbC ӥE2ڪ_haZ E1rb!k=e݉Sǜ'(HaQM֘=@!heЁy y%*\L5Hw+JMM}ݐ  \VEFyȇx蕮HŰ` Chv Q5A]eVx(轤b sP0W%-}ϞH0AZ< !|#aΝ:` _"̙ZxD=R̨W)T'ډ` `D&J`rgQbmzHp 2ZS X ᫶ !'nė3,-8Y`;0@TH|RFcszAxp- _P97o5͘PP\` L(ZX59 |p@ RN0.IM% P .nBGv ZΌe H0 a4U2ʙ'x'8e 惊0 A2-$88Ԙot2[ZPex?lx8"^џ `uH  Vp Džs $ ( gQ Kfᇈ~̸Rtڞ.x^~ bvFr !, 'HA  #JHq`COL bŏI! )($Qƒ0 4̘8z,ŝ9a J.7U+[su,J*LGrρ  5dIRà w (a i;! . a PL8c(C|p[>Mv x75)M~]r=&1!AA4.ș@-l#};t{B V$ֈF8&o9@Al0PCZAWQ t 4%EL(`Ap[-M0& `NXaA>4Dr%7@EBoHXh `WDhdE$B!w m`GPV9ep?i +X)fAB p Y`c9J'0: PR98IXF*餔 (U4V >hjꩨꪬ꫰*무j뭸뮼 %0dATJR$:I裲1klC"i-Bۣcv[[.޺.?;/,l' 7G,qU!pI-V01y 0  05P ׍p ?$i<AEOA c,lc 6<8 P41/L@9Z db@p TN&F7u * J+HGL0O>ݷ.;p{LAv&-0G)vN"XP#uHD,7@\P7G/Wogw/ʰd>G+-;& t1,A u0,T_Nb~&!,('H*\ȰÇI@Hŋ3<0Ə C#ɓ(S4`˗0+`2͛8=Nϔ:IǠ:*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸s}P߻8o}+6OqC|:u۾h`9ܧeznMռxnq=OycogZuvԁ5DUh@- :@A^%N&Q  ƥ!/^% $5Op\Xuc2,0CvB(%:H *@)dlh-2)昰 @fYv eRp'tZ aQ^h衈&袌6裐F*餔Vj'PhnL Z!¨y*injۨJ JA@!,B'H*\ȰA#JHŋ3jȱǏ C)H+cFrIr N\ɲ˗0cB͋n ϟ@ JѣH*]ʴӧPJJ!X)T]!+֭ ^PV&ŢMu턮jמ0!,"'H*\ȰCJHŋ3NǏ CL#ɓ(>!˗0cʜI󢅚8sT%ÄF6IhM8 hS 2}JAVVMQub@-:mXP붮ݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkݱ- 1P8{o޼O_IH#+4A:0A=`ѐh/p`inQnLF3 c@@R~ UPB%\ 7*3 X؎2>P8Ct^P ` ¦ܬ@$Cp3-T@?BT Ad@ 0q;pӍ  *CNP@& T8@}QrP #PFȷ屹T;Aq Pd # &B$Lc"4@ *Xp hh쑂Ȏ+0kA`@`^A XҢe$&D@/<*TMXVC*r~>F6 xBSAz5Ak68!=|E AG>+Y.Gg`ҏwInRSVR1$h 9DL )@@%  읐u V/ Y@adD.peGٍ'T 'HRu ZFiQJ Pkઋ)#hflh"T N=A ~oDK( c!q bIJ4cXDD5%PXA&<`߀A,Tb(oJPm1M4F K 2@0@rgdDi[.1‚#0LL@KHG ; &"b@N*p r)?F14dl@x& RK |47pb`S&6d ( `=6t& FI`"ШD.dBfxЩK!hBV`$Ѣ>FVH:&.Q>"4TG=dm@7qp91TۡKJ @@dt<N8Ar"i|z|{]Sg?\* %U83hFT^͌&?"l =zVG!`#2֑S1ևrfLs}*|2Jl* |Ě:(5ulAlTZ-."𧁒&i:_k!>[ Vmnh;psj06-\2ji@v7h{KDr#(YzQ:䩂 ܸ`r {WfT |"V>N @TOG @> חA2<5,^&[` QWc`ro^UqmMhydFPkAs\h `0ZPxmdcÀfC@Ns-fFa" ` F7f4Їx@&OkEH'Z|i>&ۤiԑf 'G*7/BIgŰ@ Z5]kr@ `=@~[1Q@Z ''J˴Ժ짎 @}[& #:@rvbc~ P }x{ $nf.r|,U &H; 'X!*W|r`c=qN%Ue|2{Qz @tJ (pEM\!Ҋ)A0A$ H:1?N-028F@Gn T@B@ad@ LPZ4 XX0' @>5 QQ@A 8 prB ꕀn*} PA [dga '(ođ ! -mp)BT`{VA Z 7ЛC)P-PB@ ? 0B@pL@@$ /8s՟zC,Lp:Veb$p3\D泵';7tuG#-q%}PG-TW} v5r^0a7[iS cM?T6i'@C vw(wP i5$^"2Bǵ, ql780q!,''H1 \ȰÇ#JH0rȱǏ C<ȓ(S+cʜ 8srɝ@ Q7Pxc 5Ԑ)N\ʵׯ$٫ ( x۷pʝQ x˗cپ LÈ+^̸ǐ#KL˘3k "^N4\^ChV+>z*֫'(l B&}j'b8X [@Zi?7}:Z$оh-BOСi6>E 2d ?~P !,"''H \P F04H3<nj'PbKt 0U!͛8sɳϟ@ JѣH*R >@#F NA'RԱM5jZ6%nQAoj<"+չ!,1'!,1'!,1'!, 'H<1p ((H"l11@C`ɓA!A1ie$IB 2'IT!Hhs GbRjep鄡IjI V6*@9[*9y84-E&:<0/A # $4p vG>S~M,R3&`i*(4ӔI ĚpELp% zqkĪJN L@@1}҂A'Jp>O!o&#GI{w=@oн kw8C0=5P3|uV~tVAfd%h@d!A$ @fsԀ@DPe"dEx0L{iL{'$P dPVTTP@DIؖ`V$@BlTd6*xg_" ; Зz:%OJi03H馜v閼 dcyv JnA*무j뭸뮼+k&BVڙ,QRiP<)0"@>{L0#AcG@u4 5jtJ tf&? `~X8 4Z h eqZ~( R*=19A ~( ¬d*cà*<w! /!A!Nq/eE\φ'В!AVDPPIbزa4$ 0rE GmHM@idƂgB@: Vi昂:5rC5K<+FFƊd@P9Hj( 56 G.Hz"])MkS q0PD&iRS5Y12q2)g$vsNhZ &A#vxP`dV,C 8$HI  AK,e|X0[B:ujy$؈"D @ cc}ihD0a$C. ׈@8,@%21~/`} yؐc%I*`$A2$4"3A|`" @(!ip xЪR qZh0(dbE H˲>e5F'@dDxi0$rd0Z%02& ۃ(C=IBԅ] ]]YA`ǫW:EX \`K@"%/q$ rb|vod`BPaX:LDDgq%t Pb3 B [; 1k%"AE1N`A0KԀjM!0}$$֑Ieo}0a*,Qw\c`YY ֒1EH%0J&_Hvr%Q/8[ Bu\FaĐ۰X7I'V}(\'lbȌ%D[vAh 1 PaUU\0hJ6Z;`Tb5 )jS%fdӉ SP]>lDXT(TS '\qD 3jLb@FQudBe2Z1 , !, 'HA  #JHq`COL bŏI! )($Qƒ0 4̘8z,ŝ9a J.7U+[su,J*LGrρ  5dIRà w (a i;! . a PL8c(C|p[>Mv x75)M~]r=&1!AA4.ș@-l#};t{B V$ֈF8&o9@Al0PCZAWQ t 4%EL(`Ap[-M0& `NXaA>4Dr%7@EBoHXh `WDhdE$B!w m`GPV9ep?i +X)fAB p Y`c9J'0: PR98IXF*餔 (U4V >hjꩨꪬ꫰*무j뭸뮼 %0dATJR$:I裲1klC"i-Bۣcv[[.޺.?;/,l' 7G,qU!pI-V01y 0  05P ׍3!#'X4q/D c,lc 6<8 P41/L@9Z dΛ>Cd P/j tP8Adbq[$и{$< tߺ¹K(3p)~(:ASO0"04A# [qR-WogwG8o觯(÷C  28R@1|La*HV4b~& !,('H*\ȰÇI@Hŋ  ĀǏ j IIN\YĄ,cʜq8s"Ο@yJѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺm^7AejJل' &(ق@ }7% 6(.Q@Jhq$vb@ +xab$J +P @@ M?Ta3ָ?"b، 5ByabH&8PAOf؂1PC5v˜-Qb`FƦ-L!, i'b*L8 0,\a +RP@V+TУG"v4o ,TOPnB 2dFb.(" UjT1T!,1'!,1'!,1'!,_I 'bBrIr N\ɲ˗0cB͋n ϟ@ JѣH*]ʴӧPJJ!X)T]!+֭ ^PV&ŢMu턮jמ0!,"'H*\ȰCJHŋ3NǏ CL#ɓ(>!˗0cʜI󢅚8sT%ÄF6IhM8 hS 2}JAVVMQub@-:mXP붮ݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËO^75&И`m})c6n+,/QP[A)Le1T*LPZہvT'@pByvᇧ0^ h(,0jw `m5XD3(d3;Bm#EPP$?N%A%eP+xeDNJ$e4[g>f Vn|YQ9i@ @tP P'E:ieeuI RcX RNPAV* Bs]&KdAN0yZo] K(AXP I )@!RB@* jpN?@J: * A84 @觿ڌ@^ 4{sknP1En C6d2pQ jW~3A E("s,ݲD:I]0,fu`wvdNj=qgu4$VZw^u7 ԏ$NW7"tb vSg@L9xn<^,\@!,"Q'H*\ȰA#JHŋ3jȱG>Iɓ(S˗0cʜi4sɳϟ@ sѣH*)j 7rLH@bիXl @ ZÊӑAdӪ]۷pゔK݃`'@˷h ^pPŠ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËO^) V@ `\7׷~ }yd` *`-LWAFDa 8 -0X`@#ND!BvH!BbU (x5CЇ'p P-(ю8j8APIb衆'dXBd&& 0BL(!ׅ0ÈEh!XV`)#3 C-lË3Ȓ"!9M:C& )-)/j. Ӱ3 !p6LT0@`O0 X1#>1l0EJ|%21XB2ƀ12@*X""L0 @ ,/:@B cCL@:ppF=;A.p1:0!*>y5$0TA)@# @]Pp39A"%⎻"88N? -LK;c88a0?w,4 ă>1XA,܏ (І;@ lN72 4F(<`XALCsI&%H /<.6J (0?w6dqˇL'pC 'n.[Y 89$ 4ԓO>pD8|'@@2Q~ S<'c {L b'X2Aʧ)\ PGT0p`9CgBD4-nPC %@pુ:Ǵ}(Ì-jԳF8k4& j?Y@ /LK 4Xh (RF1:C m< P=ͱ:W|#@=_$(6J- q Ng8:Rd>ѠG\@؀%@ Zyk8 a, 1JT* QϾN Fq]1maN E*^aG*A D(ڑ#@J`th4Ac pr0,bX=c(.`?` (?D RQ!PVE,7{#o5ZXaK*m(E"| `2v HYڳ^ +0 < e\OأmaATP2dl?\J,.w Ԋ. X3w $+qZ`L O OP,Yi L3eX` Hc? $l xJP{,BVH{ d5>$ h=A|4ޚ1.Pp lP:@g[3ٵkd/& lvbt5=l܏4@J*Mna||Kez}kt'> lrgHީ"d+qw7/`-mC? ЇNHOҗ;PԧN[XϺB\[R4v얁O>`bewhTgV8 ?~&<1SvL@^@4"ɟW &Ѝ A:zÄ YC>a lOJam%/(5 2; (˿ wf#~eW0?? \%_T$?L/'Ͽ8Xx ؀8Xx؁c!!vbg/@  p)X1vn@L79q nP bjEX `_`\MbS(M]n i`cЅtȅ`PFDQH8hXJG|36x8f hu !H`FiQ[6PMx_P[07P9Z$> `q @ewRF"AP!,Q'H*\ȰÇ 0Pċ3jȱǏ CR@ ,IʜI͛8s^<@ā!t Jѣ$O 0@HJJjL*' @կ`Ê )!P0BA1ʝK.'<8@ L;@w KLY'-"Pp/g)'lӨGx/n(%(2@Aڸ NБlX-JУ5A0'ZL @oMVKOlZ',H"}0,.OϿQtOAgEO6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dt' Zh#p+=wlBD'^ٙI-1XfTkA yBM`CTgGI)-x' jShRC{ f ZiT3 N`루P v0 ><3"$Hd@\02 laM }*%H j00@kT@U BP`pA>, mB=#3A Š҅#LlʪIԶԍ@7)ϴ"',@>,l~Ԩ>S(`m+xC+AP @D)@8rYD`*@(%Ħ9RFz(@4Iي*"&yX` B / R:FYA,g˅`>-HĤIJ?|<0`(|u:d:gH6J6Gh'6%dsrُP`lG,zi@F{|ɶf%dKmm&. pr[BĶ?9n'\VaHugz xKMz|K^P ZH6Y77ɣhAj`C*4a7>[oua ]C;Leu%񄀠 0gL8αwۜ1! ZFn9lbm=Bҹu ].+3b#HGDf3dF3jTX9WJ/jW4"6\JV?gB C F;ѐ'MJ[Ҙδ7N{ӠGMR9=5Q&QExꐙr&ӂ˜)%=yQmNlDr((@KȌ| ی.0GA/MoˤG5Gy \@)р6rV5 9gRLQ n)4ty[&~q0QvhI{&uM|e\j-h8 136pn^ܚxMZ2 31Hk_{݁_I^Ql%J;Q(Vվ *}$p"120G$8G B0S+@a z#::Kl DL홎ܿ!,Q'H*\ȰÇ (0ċ3jȱǏ C $cʜI͛8/8c 9 JQ0$Á@GJJH<=կ`Ê{N'=/2g\ >8DyS0-P "4 :u4ʵdwJTPi@@$"b=zdlGM퇔z#5 -nf &( T9=wym j. \ LVa |h&V킳& B}nTOd0@UeYاnёeF& E#Wސk[ħ2Ȑ |<$N'P@P-&I+$v pUSeYNu"w d+ V0ֺA&L#HokvWMfL.wD@*1NT @ 0 \-0Yyp>09d 0Q!ԁ;i8lznZpHRD&'chc VJT=Zw )(^p%5v,ƛڦ!C_ ML2?vG `Lgm zwC35dޡ_$/OpiC-*GZn-DAs61e!mZF^TS~8-=jA1"jt|AEԭ!kI뙿IA-GR䌈Zjњ6A8&kE]dxF(*15ڎs^hxs 1AB5<>Z2?RG_Q&Ta]ey8Uq HUu*!5%@еd Y4{Þf/!vQ< <,TgD.w^usrFQy9rHC:-0pX{ $-373{Ek'BO'NS a#e+3Q0 '0%TsROd/x~@9Zk@;w>q`b^tP ^fPrt"=܋)` "@ %;3 å/ $-Q;l P`fP J|Ķ ;W}7@9uA 0 Ŋ+  ^!WT#pI[" .<ݷdEN7cPsPp".*O!0-@&P""g{l0q)"{S;yX%b`*\@{d$HKs@KTt0lf@ӣqs)! [FKp="+: 91 %rheAP9а'`@@`X߻^R, lh`!I1c6ubɜq{%]0,fr0-ȑ0U P P%I,{nț`bȽ 1MGe  0pn=[%}L W&0{ mD3-0l['[P> 'K P ! F-О@ฤ )E9v" ]0ͷ; >H ؎ep@cIMPTL@*X ځ2p3M ^`uT ׌hc`k0jpM΂`3AhQ)qcގ\5bP*]e#}ccLctM`P;L F@|q`G[ hP$* x}uM0MK03\`c7 | -+ɔۇ}  Km*McFfpu`~Xs l lPT y9e}e X@R0`220lڞ ]eX @@؄@,^ ;(0$+o>&`4Cz~~lp^0@ !Q4P, ^P|  ݠN'A2czUGQ.D?Gߎ=_``Q0cͱɔ,7@e"\Pt<`nP~(VOc#.B@. XE@ŞLbΎ|0/r~R9}׼"q 0Bzu偆^u}')y3& LoE42'`0:[u@n QP =P,_ꎀKEf < @&PC#  h'!d&bB( &rH-‚ 'PPL0KCA""3!UXlAL(!H2.DKh188(T@!M ˖0/hFZj֭][lڵWc O~LX;5E% b9E, A1 ʉ@^qC$~`Kh 4H* C@r`ZkɢPPE{  #q2:T+J"THͬr@"H QŁ8B+&`K"Dz:HP"TAI $jG8A!(sL2"fZR8?4PA7׼ *`{qNӓE 7pOMa @:/44h@M4ϓ<`NU(L$= Y#o ,dp1PO60!вjd,T`d`P5{1 RbTrI M@+ˡ 0L1C^[DC &$Pgf_ R N h FfR}&Hg<&X!R&hAPG`d0 5 E$ đ &‘4&@3p̃dE=[=papBDTB7k 1h@MD!79Yb  NCbmsUD @ÄAh"NШp *X #& 1P$$D_g0‘z( F4H-yUC9 B%~| 6hB4[p4aM3NS0V<@[ o D8Bp6)`BT ' %BD{0P>n@2@/ȖXK+&48,PFhGV@PA("0mcyU 6|rS'/(F^h*И ! t"  8t#$0Hዞp#`! @VLRb XMEto C,<A B+BlOI'x&Gq claDv,UlD @R(q2-bB~ӟ#ĀH `@@ R T0EB0'#A!& ;gjQ]48C&=@ b(B 6Rmwl H-P\G4MHTh@2q`B$F?oCw%)!`Ц`7-b tiT|tAXLZ%a@ `&% AJ  pX3Z49A HHik)C@֙H$>Uİf $AKImȨ@AF6׹P @J"` ^ цJyyQb"vȬIbeB cs ƮIEL8 nL:1J(mk:wR9F/sUjbmT/qg5"Yal@YBVb' $-t_,/$%(d@BcE 1MBoiz *PCP: Å$ ;ӝ!Ά,gAB8Zgw(&tiK, "B_ް alhítqgsx-3!|4( v l1֗Dc#Jruw]** N@mEqFwk$?n" i_э4- iB54"9ӕ2PQ3اI\ $}Gv;ޔdz.b3Vߔ91]B_Ϣ@QkX[ŸLKLXiv{l$&iz*+B€}@(vP@K8i؆y%Q.\"AD X1 :!n#,!,!$AT \RrUrxyx|wMN@]H} h(+_P($8Á$ ܰBD"t2B$w L:8+\?(D@0 ropB k@zU|x|X._ja( 1d(8lpdp{xx/Nx 2FfdHd 4Dj=h4-D*YǕdɖL7 c'Ё n }H@Ѕ_"Ag'fȆ8~(\pm~ |NxPFC%٤D˳ ­H?0p780˽˾L˒xs8F2Tdyv s nxRʢ<H~p6D8u,]j*Z(47!BDK8 p8K0`N7HLyp}dk ؁v(XwtPA[H,Mbg) ֤0pL_qh8JG7N P P OP I%@%:HNN/%~p\d<!%RO`oousy!|<k05JXw,^Xq8nli5M8?D?{A=={BP0HQ%85=SASD@TBT@0(n83&+TU ȀXNv(tYO`XSbx`l`1.M0X\@ lm@@@poqp-E@8@T7?s5{|WsW?<4 @y:UU؅ G8X}؅z` n`HXsp^eh(bŚq h}Xu84r! 1Sd+ٟYՙ9xW%Z]ڗꫦ4(0k8ڮRm0xp\؇ZxQe1Xo(J`(TZ\lȀ5pB; 3 4"%e4=cKF/V @sM:7P52VE3#Ecc ![R8 Nc? 4<+d7;ܘDfXfr(,>.XkƊdv"9c[chA69`egK }[n>xo˛?TOk xB8娫:,(T'^du58g46Rh%aa9gv 93i&~辀?ft @ F9CЀ ) wDVj9t *`z^70 hZB*2_ke"\<@fNQ慮$qCŽ*@+w(8=0`+  "Fk3@1kƎy[V0i-WP NT-D&ߑ˻Q3„ꀦj`@k&=#&4hihImӷ`8(΍mt(RpEAְBxzِT'8(T\%NxH0N $HN .p8E쨧n,Jhi=^-"i*b)?$?2 RJ-$ 87'xoB$ k&QbqY2*n:H$oL!ƚm x@dMgp~(4ifC"XXBBH Yh H(R>oWWmR5Hf\EHl/³,RW(*7H8 DZRtYHu DBH-RT@ZPHIRYTm"Pv <@W (: fHUTWH(YxFhVG(ANt ghOAvT(DTHWVxN@0VGwvpwu#sw8pygXYH(B3uF2OW=\g3`%+mEdwY`T81HQ`yGsn I@3h0W 8IB vZYXO?mG2k?QN(s@D7QPH@8Rl"NHp? WP!/Y0@R Bp!+pQ02j9+/mGzZWWʨW%28W8R0o3|{W@ @OB=7{YWL@ Z(??@eNp&B$`„ (3Zy5 WL8d%yE NZ,L$JV7h Ew0L-: 0@!ҤJ2m)ԨRR D^!QF ӫ-1'HB%D;.L Q $5#`'b8Ԩ$$b4C Rb+RPTB (ZdRwT!# G@Q%CU<W000LzՃāN5)蹥HU`aBi\BIuH$2H|'fNܽ8 Tq2 ;@{gy' R̀#N .a :1  x(@ p@D*jaApE&fW 4KZT~O\,UI=Bd @@B)o}  LDqը@xUU@A B dXqD QSA-UxY$VP 9JfL`E 0@ @ XǏ \t N[-)?| uHHY  ^BAtĀLb-QŚGwM@$@E}1% ` rʀR՗8Y a|H!D @ ܁$48^ M9" lXxMa"\9SQ\hGX} aEXꠌ N LNaB%F!_D!L@Xb‰ae \<$a]sT< X LN\` M SB%K9!D"B@ģ4XT@ hU(JjNM@9QUKQ@ $ @MB$*IPdNLոQEX&-M%fZ)Pba& @8r^}#:Ve"ĉ@*dd$@rgB]@Nq-ABeiEB0|RP'BEep2-pefS&hl&,Etzs EU0gv~R$g *gRDɨ%rz'{*y6Br&RLgTЧ|Zg@v&vn'kw&ȽR'&.('S&vG~T'nBj'h(U" 񄃎U'(R\gsFguJhfBftҦH.'i~Z*2b):jj*jRh*N(~jjjj+鮎꨺k>&^k.kr*v6Vbzj+rkz|Vg6+֪+ke+*jnj9k.lj~(j"^:nN)+kzgbib,˶i~&tr>V}>,Ž,Bl*m-V,>тJi,)N՞-Bо)ѮtB)s*JF*VŌJj-&ֲiJkB,ifh>.6i".ʦl:Eʪjfަlٮ-jir*h,:,kҊ캬-IJn6.6'vn/֮ʭ "-©/ /&6of)ګRF/k6m:j)o/*~¯,2o/jl6,w/&pgp>0FNzoj ++/z 0rRp>* (0 pz쬒m16߭nO:+:q*>1tl0z~o-o.f*zʰ11,q}pv1+rq?2:/#ز֎po +Ţ1$9Jr(wr&2z-('{&or-rhzjqK"[)1 m-'3֖2njj0}fC_,+7Y -2F&67snC4s:s3b/<38s3+fo00)=3C3sr-;AC47FCsc͒s3qs oJ7 #195/7[JtUD4Rs ?V4R4:4P;"0IwIrR_uT42'tSwuFPS1VRhN4/AwY_ufqVS[VG\u]w3W+@#r 3v3 6 `uG6Ʋ++.}^(\IWve_ʴ ߱D/-e`Wo/6d6j6z4rhH2X6r#5>w #trWrm8wlw[x v/qw!;xb͎ګ|7{No(s 귀O.u7 rXG=OH; x-#xzgW6m\?1Ox[37Yu:6@)E8_Eu}9nyO59yy=t2y_o۸S8:엏y (9+hjyϹ#i߹y湟zzzp'z)+:(7:٣G:;t_qN:ozp/:::Ǻ:׺:纮::z !,{'H*\ȰÇ(8"3jȱǏ CIɅ'Zx˗0cʜIFlɳϟ@; JѣH68ҧPJ ETjU)`ÊٳhӪaڷpfAr˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^VK^d5AT hY7NJal 4xq/VN#SjKׂbp &8H~V1D QqaPgxLPLnAuB~݂Y`5`]PXMA @ P 8yxH] 4uvW9$=Ax"G )ڊLWP2!/(@0\Zzv@l-N@]xQ#Y$A8Yq\ 290) ̀NЂBi:ДZQ zn! F۬I qϝrWU)pqA,F_*ThAlvt!ˆknR"d 1 [ `NۿLI?p@fm/.Dw̑\"lP.@AȚ0,4l8<@-DmH'L7PG-uF ax9uR hZ__+i}[c O-Pbm[ԸvJNRrExXYx\?^v땹q#r$w/8H~ :w]z‚`+t“LXw #Oj=>! 8'W&d+AQ{ZmA}ȼ@ @݋~P!iF fg$|@j(L`@. `0 :h 嵒H SaUA `v__Al^C ,!hFD i& @+nh`B6L#hf8L 1# D%6.U\"r,> \9 =Fu$Q`x^Np%CZ@jU%GT= q wK Aίbvwk(270/x(&Ll!W"8/7L~ܖP]Wp0$IPeGn HDIsJ (EjЃ">DPl%|V" RO(l&9@U @Ҕj Kq4 #3.&S-zeTD71D@I0M$"eui3W%H?d}LT_Ld #AZ7c/&e1aC l0kzDbd63eR `H ɜu6KafqgF. |q}LIٚaB~l@**!q-Ѐd!5Y0RD@U6bg&w Y Ǚ.~h^%2  qSN^g/ $ؗ~B JPS@60# -QD6\+<N" w@_C$L@RK:$ , AL0 DH܉d6?@aA8d-R\ =ȶ&XHCQSP'OjLZz9RރԘKr TǹAd@=)P1 aݪ K\AZCPIdOoA Ta@*4)Ju N @alHH:!BQI0EIEw$.ܔ @_8qէ2`(%(`d腬R e80@ J",. 6 jEI!Nn@@%w'#hCrPG#Dt-(@:N5`Ŧ (3*9Z,@W&% BP$ʮKS`酎^K1A@J- A_4N?aC<0rjnV_PbE%,Ԑ(AA O>*0u%W ?xJw\"W?Qwd"rc*,HQ,x+--kxxd@`Pc80tg-=0 k?2@pK&  "p|Vc2%l``1(Zd'j,G?xO0rO+E" " -yrH-c4xFU:2w$p 6;A'5z._T'0"aY}b `oHPSj!,[,qtr!0%Q0?q!u4|'N˞Ąu񬑥WA bTyO @ 3K^[bDN WŮ]ӫ)>y &H*|H su߀ndY s6t H+@ a$hW&bS(0 0h$H#<:hcQTa `FT"HIsM R+([X$@0fH"Y&{7[f)tN0*Pi"%WwV0cZA{Y(tL@   1*l@00Pw4PA92Aw3A0v T" ULp+A៯U83 )0P2­uB:@7)v,(g( @0Ќ'P@kqS0@U @-qkl ;S0 pW tµXѻʬO^:T#  5+--vN$ s"BvJ1xWU0< z&_hA П%NB % sk El8Z?2@ H"!;%P (@! P@.PE# `H.s$uqv"0⚂ьg V,h ȔX~Z#F 쑏x?.Adہ-sL̄ɥ@$XA8I5q!+dp.)ecAx,eFFҁbeK2CG&8e"MdI rETfGAc b^ &kCԦF@w u%jBͿ>O '!$»ߑ ,6EZ$b <)ZfA`$#bH?GO}! XKJih`h3?&+Mt!$%)~]a 'x**IeZ1= =58\BEԄuقK$ ~I!G:jf#\"V&(I<bvNu@hJ +Ԩ&q'@<:LP3Ot f& "@e ԖU DT{6E@İQȒ; "Q~7XE`QU*rHL Tͅ!)L`'ĐB3lAK J +@gڭs@/fEtR-`ɅHd3 ʌ@FA A JM ( L\`)P+& 4Z! _&@ @v|1r/8 VXNC\#Ab @/r@ &fJ <,c! \U!H`H)Lpȝ4ހ<"!x,M Z#!$@pxFqCA"amFn`IV{^&0X"PY@EI:lh@0[7T!nS{06#) |p@:@ I"mi $ G!&b@ @@x@P]'Į!& F IP^뚁z%_ك$BtDEaTh`$r ]̋V[>xAWAa6^> <yF[,y|  \;ЄAG=E[ X qwPl!p*^(|@mm.IТ=r y@4B 5m" `&`w4 q-2w9^a '6n$|k2$,-87`v'cp!6ANF'p0j a!~BS~Q-h@zW(Ff7P,!@ sT!$q4C6 0ME&.al}]3n in#  nutLp !l&% 0 7Xzdp p}Q1n0%X (80_g  CvR @0j3z1R 񃀞`amjBXAc%F qQu` W"ƈ4@PSPNQej]sU{g1s),Bɂ'U$oP?H_k{a'@nE+u2Mti䑺LuQ@;ә,,A)XW) s/m!xY7,r!q5(1)-)c%Sft!'Iu#(rT9 zL-q7*,,P.P}=!2:,x` ;K%Ja|QrUR,yƟ:͢xfgGN=  1y 1`:bwH#S,E_4/yi?!PID 0¢'/P  & cX 0 2;& 0h''B!!+P~Pp*1$gM3_? Ly0zq ~2&Ơ.\ӫv1$ 0\(5!q 8# 0 \ 00` $VP;*&RCA ; "`P(:p8(k+1`,Tq``0 K#`eђ > *bq ?kP*k[: 2΀^'$ 0 uJ1 zֱ5 `10;Z ]a0_PP^f egUek[p;[6QS ꐰ z" K P[8 0۵r\ Er`(S0;[{曾<($aWv)^0p̐Y@" =Q`P5B5:a-u%aQ+ FZA.0(.  %@ j8. :.<;S!hDxB;`QKЀ `]P6@אk@ H0*Q<FJJh``#pza !:22p.'0g #rraP &DZJ@0 vp /Z v K &B;K+=au),2¨UH4k$kO@J"`Z0 LOlJa3HA%p03.aF B)q zUP p:hf( @EF 3>3Ql$s)Pʍ:[Q `F7Rm P͢Z8H`baԛ3 `Xb|ybsPz2w0e$ P`]@}'x"#0{2#տsBwܗ4PR:a3(G,)` INz_>Pr;m4 P'"`=6Ͱ! ! >cW@5XqY8=ڹq fpjv" s+Yx2k>biF" 3 Y)u>,9>=.3`⇞p 1~P@ ł=>$c A.4i @ x3*s # @%p!0p Ό;P@be.ܥ\R~`bN   x 0pIp0h[ D [Qk(cJNPP [n-P j :V6" ȡ(d _MwY^P 1;0x0luYM)qcCnK8?gu=И2 7> aY>Db b2K/'p7{ 'Sn0.(h$R1;Zsց 1z G^j5{/,'+@T/#jS  @`0 09PJ7v~c) p@ zLm}P\t, 5._0T/,/Ү&y2@Gɼc7[A>S1aB  P~&%"9M4D^)"!Q6&uqD ZLqΝ50ts⎠+erf dL@1@H)RPkBet"T+, B ;^ԩ5 vzaQbB%'j*`)4̊c%c2Rф!?aw8`NPV &p# fso{XT"R-6N5cEW'7mXFI1 ;:A!HdĎ'XO+T@`6NS`OX)H+ 89|?+\Ae'@pҰ;/WjXN #Q:!C 'K Hx$$R$k9Pb F)搚Rۉ5"Z`l"D 0NT+^;736"qռo(NdY!-&Ovb䀛Bb q;^EKXPXe B98h Bɰ! tILJ(oh.D &,IT*(,V@ ȑ',tXZf ahrz TD (PH\A a{'@&( >">58R9@aAOSV x?B"!LEDVʐ'}h5l̨NÀ&^Ȏ^8xkFQw5\ FHlwaowaX?c!(I'@S@,l` VUTd"L{ H S L9E06`vb&rWew62jJ,|p[B] B7{ɡ.^ܚPf+ay ҝy TpkI[A.bZP`8$#u: "A"'w%һeYB 8(܀hk;ײy&( P+̈́$ T(фڳ& RB it$0n'ObB8Yi"E*N9()2 Gb ČyOt`YbDl`J%%2Ő' q GXW) :(D!EX& oD&yGE%qQXh>*zEls(-ژ' v1N-*i`#肊Cb0xAPQ \:k5OaVUI "1  ĆIITzkaqs|?bCP0A(*@$ T*s'kjҊᔑsN~0aG(UK' Ta/҂q'$X T "O"To e!TE r`_710i0?A Dp0 "Vp+$2%?Q D+Zt'  Jf64MW2P@+\z 1շ'D@A3M7A),-tpDPO\!,1'!,1'!,1'!,2'H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0I͛8s̞@ Jѣ "]ʴӧP T*իXjHuׯ`ÊuٳhӪ@ٵ (ݻx[%6/ĺLpŷ} ;Wǐ90ʘ3aP R9 ^Lvزyb0߽ { (Bͼĝ'DQ`-qN;OM'{ m9H\P@ UpQ N@ r +C1 t@:DA"BA 5Hn3!3!#W!3%D N0%Z&P' !BA@'$D# t!D0 fK+쑀| 2+pz2A.㏄* ,@T0]tM@s8Ӌ C @L5N@8$Aq@7Q)nL w4]1ЮF+-2@ 4@bE:@@E!0 ^B}JCFTfA A^9=z&>P¶FR2\@B 0B.@{#1;ġ@``@uD M(!v p@O #CoG r;3Bl36Hr< 9 fA:w8ph`DR4[cO'^gɤ3#QtȮ ޑ<з03L@O@'p¸qb `(A83 'Jx "8vt2L!@!]>R:Aa T6R;@(KBa~I![p(ӝm]G&rX k q·06 BZAvK"<`xtĶ 4J&N Xz+lbfrA?yA&$Hd@wU UR&y4D]MlC _h@8Pi0bz@ 9 GZ %fxm$0B $bE@p`!B.5 @AR |P  N!"8Cn*P:$@( z49PˌU nɕR2!A$]2@dρzi\X(F(;in$ztz,] SD6!"I'pl,H0S! 3/#833I`Î~` )Tbҥ&$"XiK ҘE44-MqZ=E? jꖢ"i䌺>dDS! "hzMhw+Z:slՈ[EDoQ]ʂ{M`,=wXƺ6IJ+AyT@CkZb|Mlÿ,_ءhbyS!UD; 4pT`5;CL-Jd;RZA~jĂ 5 CC A ͸ d" \ EA ^X#DbqHNbMq@_MB0A8x@:-6JҠ4ɮ $7b ~?hp $@*8A"Y#Ev{.3?d*t'8KāQN@ ̡ EvcNū%  9N *8,u<]."74- r5  ƄJ%idn= L3hA4п b  b8 ( uG8#'-ݶXcA'PP j8}6("6xZ2+ <st'NJOMY3_z,z^0͝OG>`]$Pc `:75Du]' @3w37'!3&uQP췂3& gF@Mj`uL3I8X K1O5٦ pMmti94r#~Mr%(C5y!…7`~-,1{TB%q% ( (0>5'Q 2! 64}3qҀ'pfwsp?<T0i?Qŧ#Av%a6pQ J@1 cT:F%"9J? lUP xpo@/f͂I0p! fy'׸IsC ؊8Jq20!;X) 1xs&רj=@ F'K`$d|U*37C >aWV:tlF)Ve(]G> & 3d?!!&0Z|sa#` :2$ pM!   p~Y&W6 Hy[  SJ1 =( ݠ @K 0rAx*07*Z&'r}k5!:n# n-"%REB0JcXS gVT3!!$bK1 RIVJ0%pSxK *A%p&l뙰!P 0{P%E!1, rLmv X< r3.6%zۨ4&RP8R %P BH2G݀ p aM0уyؽ;P <+85Q4v6Fpg  #udDMh`q 94w y S!SϫQ>0, $#3 f:<>,-e"3՜ݔ $Ѕe\ǵbiOѝpl~L4Yl:Lj̣>lw{|Yb"e2"wfgA'sʨLLZmck:K񅮼 qA'"cS] ,1a$k S K̼qv_\,# H^L%a0M p|kb,Ϯ 5_p[qCoFp>_˼c@^3R\1Nq!-M.r4qrdV4]1x c0H$C350%,b&-"NZHb!ReVUqi}YVYE:WlX}nS  YA9aّ/Yֆ!6v}ٖ׃S-UjL|US`;^9"#P\Ax٘M+%Ro|a :n Y%Ȍ)V}mƿJ--|2LfRpm|Lܧ MSM@T:b8!;P8¶%65}l˖\Zt"ASq[$A^.9d] q"?T.2?#!M#Sٕ  U=sP\妡Z,S3.Q)1x'&봹 Bvٱ?jJAqq+"R0jv>F@s^Jpi@T}pL>aW FɥbsF`af6|5jaB8C`Bꘄ @; ^뎜'/jz Ѭ5*\-![LPu@ҸP0c" 0%bQV#AKך%WӞ(_Bw}0V`wz}4e>k R)=  ER*6)M%&M} >3 x)Y8 gA:qږ7p{`7QpyX| ` ")j+~"km I0%ry /}Y33`&fsK·q#"]0 P (UMnI8h %J@)`„ nz`B >QD-^ĘQƍPa0@@k&qN1o#2 $6'$8J3&ChT @ &<@pcTY(B# KCu v` q2Je\-z2# &$BOt7m" ABt@C$2т( |dkPgE&$ a!^zݿO/ȁ.^IPb` cS' H @ M ؠ!:a1:d4c"Xi+_[ $`Ec4@sȁA z21,F:"h`/rh4d )&h@9DOL7|s:N8  O:%' D61 ˵XHÓ ւ/RI'HH`4Rldz |yq&h! R@KXOhIhC3cR+9mOchZ˺ pcE % B?u ,@!U1d[؅ 8tƴB4*+~E4Jɷ,Xmcij`{c.@`)[Hi`i= G`Kfi#օM}ikrǜ ` Bep^Y+ T]?䁮)e]HgO H'pMR\!Ux@/x ty(&Z]z =eQ Xa~$*x!'a^yأ"'(4J%Jd{v*8Ϗȝ.y G4!)٤BPi秿D  &@H&Vi D1 S qB-"cH5]*ЀMKH88L E^h@88p_l8CVަMNJc݄vX# ЁEeй +U!ZiL`Xpus1w/ ~eijX3M a&`018@,ǤfM`LwINv="򎗱Sap_T@H@P@q m ҠTg!1hXꠂPAJ!ؐtE hNp#$p䃢`C fEaFDGz## vAb4e@h:qy_G)hhX1!' C~ΆKh>+|NEzҦ7C}~ NVtp)Sn T*lIEY< |sdcE,zt}kqk*w{W3keXn,4Br$< $(EBy,E݀{E-G\!EIYA(Ȏ'CX #$\_8 1(ĝGHOe4' KHɱL؇@`c(P[j!:"NЌҺy|Ŭ̦qL|/ dt΢BTS8d 0M=!p8KN鸌 >T8IDH? {DHX|UZ)PY @N Y eЏqX {PμϧOh CO$ vRI%"5K8yXrOjd(X`)fcXy@l\P&J@ %9SE>CJsR4,6^0j`(I S;m JƩ(q +_  `ZZZXk؀6PZ_:!#Ms=u:^u>0KDHhf@0O:~H}OTy8[ PЉ hm`łtb|d.v/b]/2[4V2ub ;vWvK5 @p*D] @ FPXxS@F2E1 X4@ ۣ dž7N,@XFtX$SW_ NvѰ( +0IM8-pfިeKuֈADпQ8v C!`<@IKް= há0%9DC R`3V>k8>F IS @M8IËCȥOȔ= $89J4d6B^$QTTiY.?˿1Ex^E=Գ.Ɗk8Y 8̿c" 뱎eΰ*;`wB'.X CYN\^`PQ48i  XL Re0iG5 jh @@7 S:MP$ ;] -dˏ>R`P>(0APl gL"E_dV"m.l0Ċ3@P^M\ 1O0XB NqpMG*D >bB YUtxp'A)S 0 U`fNHD`̵ R#cRU5PN"q7 (m3[pU47S:K3NޥH!5T=D)΂,`T&$K'2/_A 04@-lZ(@e*-ǎsy uwM+sꨥ cypC !s}Bd0#2I'pA—r_q*5 `#]$d` HEGFA fԹ`R.JD @Yg{8Qe&32% hl|&MQсڠ3 *9(D!Q' '2(fYK0A0@\iU0V07!+.`E%TFL "BP @'‚2F |rB8~@<{8k4TX0XRLP( W lL(0F!B!X d0$Y >p3eqOh%# 0MYaZS35:E|&q3э21Sv,5 9F, M"r ,$)גQKb*DG|WcR@h ()ԌcC2Z +HЬ)>@ *pJNcP >@Tx4X. $RuMWXd?L**@ jv⸞KU6W1H(T@}c J@Ԃ8Xu >%&6"2@}F6%PBo83pacz2[ )}")g,A(_8 }bSYb T@@'IFq,- "  /7MH@`P0:LePA2<;dbHSAH, U8 R›X@s«%/`4ZL t.LD/pb7(Vq&>R4RpBf86j  H@\:r󊘼$* Yb?@k<' xV JZ_uCN1TdY(F l\Pm]Q bSfJ߭6lmA,%|ePDL &$( @b!D N$ Q& OJ5Ǩ\2]C^TzY&\ T߄x6D`C6yRʾ-WKfV .Ld`49qZ$5{`i`z9?Na'`|ޠ#}Y4ȍ'*E0!p (JH/WRџ` $8 xW"W(d+k*ȣ ʻ2$o0=!p 1U@ LEs`PU=}fb]hAN]g X |Ґg19FG Ѐ\M`J_8镒O%ֲKR6% ^H``DF Y @k^|Hgu*@[ ? Oջpf lLחI0~A^PGbV d}D[j0Kd$ cx^$TUۑ@Eki 0'auu0f؉TQqQDPIh%6n'7~c"8#ck N3 lQMcv 9ƣ<#=r8#>bXPbeQcvOc>$B&d=ޣBcIAD F@R#XFCn$Gv02G$UE $$JJ H$G$F$M֤MFKAdI N$PdN<;$Q&RrP.S>%TF%u5TVU^%VHVn%WvWWXRR%Y%ZdNNF&e^&fef&gvAngh#hif8jBk&7&l&ml&n&N&o&ݦonplq&j"r6h2sF'gBtVdRufbbvv'arw_x]y[z'Z{gX§|Vҧ}gU~ShPP(%&(6(6F(V(enhW^(~h2z(hj剦(QFƨ($(ʍ(6Ǝ(eْ6i0&)FiN)^if(v~)()))Ʃf(i)Fi&(* &'<*FN*V^*fn*v~***꥞@@!,1'!,1'!,1'!,1'!,`0'H*\ȰÇ#@9 ^THɓ(S\ɲ˗0YV(P&@3ϟ@ JQWA*N ѫXjʵ+¤+ Rj׳hӪ]˶L+T DۻxK-MtLau)Xǐ#Kn 'kuO`Q@'>8?˖m֮)Z Tݹqɟ² u0{vw&1OsC8Nsϯ> {tZF;`4+©VkALz'h H!; $!:>Xt&Tpj"P*i( )@=!Cvz6muāep~1YBH† X@Dp{쳞y`DF@o.LTp43ˌ¨Lu@1K4p|#.PBno<T@A,o."AC"6o'^b]͍~k-yya`EJUt՛lOnzgF>FUvm0jታ/~9 bva:TbIW.d|!W 0}#dSѠ~~AaP@,q~N`ZCp_Q(D1_8FY"Q<XEp  ;?_F"*ъT̢4^\EB, d~C@v. G06RNmdQ9/ҦED"(cHp#$7CN3_ 2^#iHQ:dnX x%K/Sad8W6Xre8n|7eӝ79~:H)3] DOHCψ ħ.ӉnHk:,29I[;׹J4$T<12k#\[CP@N:A"(Au SQHv$NJystjFx:ua%<Ieԣl$[dz3/}X׹Uh_)WMhM` XrtcQBr_^θ֯DdY:^dULr eY#Ny}kP98ֵ$!lWvv*;[\reHܩviKX26Tmfx[v}Hu Ou,(]oWm}G CQd6m/oo~ܯb˶:x=p$- w ~k[(JbTof?,\>~ WQ 73٨jӸ_B"[p΢4v0k*ﭽol}+%LfQ MX6wYuuζ+ٓ}.L@!,p 3'L `A#X!@ $&P 4HK!,2'H*\ȰÇ#JHŋ3jȱǏ CbIɓ(S\ɲ˗0I͛8s̞@ Jѣ "]ʴӧP T*իXjHuׯ`ÊuٳhӪ>t f nUH.+wq KLa!cYH#1`81L X vM{-DO=Ā-@`>0AvȿL5Xɳk~3yP(lM]q":=w .R#D | 6}# p@b AL  W  0nɤ pAAo$ Ah 2@LPN45N L`;N0!!1 NF xeb!@bQ@+8(@©j" X{ x#訋@TZ &"Ax$IO@NPj\@ vjepi0N']+ @knp@5X6d+m]{L !{\h[Y]'v,* X50 3= @ (ldza@aȰjuEjoa̜X]uA6&`Qa*DOfb?#n]A]ԉ=$@nP*3deu_XkM^mu[ܶmsAzi6@ @o1+݌+d zD0 6Q{H PNitO00;V3C4L`Br@א.0R/G.UE@ 4 5`cKAbП ӷOTE@40;OLlrC˒k$1&@$@F4#.)!A"Hk( `k LtJ38-JVTvx \ rA೟ UIVZL)OOۈT`vdžX ɻ8oI[ȕ.i@g.D3Bל% q2 xpp90ۅHT+1FZ[X8lw\4P" ,}Cd+\.4 ,:e#W%҇"[@2 f4BS<gmA2n1.A- nv+2v|u[nz^H@ݘmA3dka;q=uYPjV֊Ĺ<[? f kdQm؇Xp) Ȃd \M7s7]$ÝMHC:'}#A>BbH'sjrj4 .8[ y 5ruY0HA΁#atG`1vm=V6kD+͗ Ix 5H$9|I[Nf8I-w\d0Q5_kܻ 2:D%'0⺛f4{AVyC(x0@S[ @~_CrQ $K960@ CrByq&C)7G];P )0 a K vBtWnET02j73}$3N 4y! )3 |qRSbB KvhT=,( qӀ'/4@+C<$iA 'vg!'Ԁ^H@ qbp$P.0fRu ,đ'3G:hdg'0X% 0!*QB1&Gl8Q 1XaZsA|1>!a! P#?{lw9}fD1$u`(#@|v ` 0px&a>8B hKPJָGWq,卜#B@@a訑|N6C(<93V/!$ ol$|i{b pHBtbD ?3b(PN$iI?27`&RXdmg-Pk&Bse@2hC0I +ߩkyt Iiw' [`;TU@b Y|0'Q({0BP *-`,lP8 H:FeZQYYy: 2uf(G-b$@7_taB3`E0,0z+bg$j8uHy҇F0t#bGk xP#Q?"E0#czhmw!UW婺1ΑO|h{Dȡx0|J\*=sc{'3"Y0}|jt6I2M'vD٨zirj|n^mO. o2Zz_6ъ3abns6mz\KCbpq¦mjanޥsaZ$rzjf!n_m";]JZpic7kֈ.SÔ'*wћ*MŲ62˒46:t4W@+B5DF{'lд2bW{8'_ y-' [q`5)w! 'k8;@wTA۹d? {7&_;K):qaœ!`뉻%rK[q[pY: !$Qifr*];Ϛbx`דM55 uIфDp"Lp[ zؿw=5$\q! @œ 1!28 <=LX(=W8!YHž0`9\1`b<a,`L! !TaK +;RLqP ^P -@U 2@'a0NS`m0~ P]:,!H\Aʨ\ʩʪʬʥ,0yAppPb\p},!P9,\mȡ0*! *,ZmRYYY))W3a` ]`ͽl`)`/ ܨQ !ؠW0<30-뻖}\;,F$ 2М- g 1 l\2mPWܴ**6#Rqi"Ap"onMbݲ>&r >s[sm'*mpWnJD)~Hkr1[;n)k%T~&\9vX[:`~Za>Df(g~Z極lYnrNWt^iuxUzC~.S$5^^冞~N钞K^tG{E>0TA.=^:^s>7d>5D3>3~^0,>R>*؞팶&>PR#/.ޜeN.jo j ^O_u o6#%X"r*,h0io2?bQ68^<?A?S>?cULNMRXZ\^`b?d_fhjlnpr?t_!,p'H*\ȰC @Ë3jȱǃ%Vɓ(Sn @˗0crd)͛*%<$Ο@:0УH*]T&MJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻxf=@TDRhT|47aˢ$Ł$/B/ @M:,`B$[&jd&?Fڑ@9DA&$!>($DqQulqP>^< X@ŗPNށ@mtTYA) tBh BFD'P BsT A6t0!F-1'B 11z'"$AWP`8h^C8 N_u6P|@aT"Nt@ɥHFN&em)QhGNT X@F)VJ (WL0L[uqLJ)VHݦa ꨤ6@`NTD*S ѬUɰ:+kSN@A9,SAF+Vkfv+k覫X@Ed )t<ՂN`@L> P'bV(,5LNQ ԉ0$#.:Qu|aV~N BirP*I@Ap6]c 2} 9 4MX>#H36A41<(KtY LOU}037[@#B0MoޝLA M-fAa7A1M)#}էݙU# 0 A$9D hTADM]m@-G|m,eA$;A N`I0A DY?Г@>#0Z)IXB5fe6[ @EX le%o ,q"AGˊ L.|0@,dduǒڰH `[N= PfpaE`(<PQx#ʨ@^!@7 @@,M'’p3(A&J_t ZGA荌0N,A H .-i0 uh(ܛHRR.Y BO(p?eC%-IOС ZpDD\B { ,["48B `P8@:t" NtXNX_b`Vڒy\g8 Aw#'>xxXwW\K!#bC? {(i!Q /)Lg*Ӛ6M)PD";`E?H77T*0 j< (Ap$+SXdBfE>%,8A pp i@1)D"'Yk6qyc@+x "RBL`H2*A PhÿS>m"Ё!/x 'NU[΢eHnʭU7 j$pA t)w! k"B !,X^`'L@@@ *\ȰÇ#J|X 'jȱcC+N"<\raIHв͍/oɳϟ@ JѣHzcJ>|*gʎDVݚuׯA4 ؍(0ݩ![haم{ $0 ,0Æ#^l& }*P- &:WPsPWI*jc LXpba+ k9q@ҷ !,.h'L PN\ȰÇ#JHŋ3jȱG&o(S\ɲ˗0>%@̛8sɓ 2T,гѣHftӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻxݛr BTb!RXB#% .8(3}X,GoBe$<^N1G V.TV:Ɗ 'lL`ȯ*P_K߄ 6-8<0{Oh̫yˇ ϿXM@fD`D-haCDLM N@:,4!h\m#P"S|Cͨ >e',Cbp`?L`SEi K h"I6"FcFG#+t2,;Y(P dࠀ #` j昽($h- |m*8A@9kSBP.)2KЀh,<%|B}d@$$A@I+UhJ2M LI*!@ 1N325d`IG=0%l9\DZT"LfBdԖ\!6fJJ0)%&]@e@PYkr7EDr B,!vFEw$81a|#*'!c5`.StWJ@E fR: VT @HBZU.P`#7 Sp! sT@!Pe-Rs%Xkϑl XX@A %eI^h1U0h`$y*Aѱ:Y#d%K;F"0Y  aw5!!U%V R2 N8A[ 䵜$&N薷n'[wmKUĐ?啎p`p*!y#n:#|d!@z^w/|^%6Aڑ@Ik&4Q#hjOZ0x "&\DT… +b!PD9ax(氉-ܒ 4@8.C0rFEI^!`JH3*T\3X @R s\,7! P&;u+H@ \`j5] <04 Qp@A0fɖhN dC}cCH1:[9A iNjV+zZ#hYjJ3@$ԇUNଌ:"oL!ۣ8re )0bKQ%9NMr+kA?,!,p 1' HQ\h!C #JHŋ3jȱǏ CF@ɓ<!,1'!,1'!,1'!,2'(BR@Ç#JHŋ3jȱǏ CIɓ(QDaʗ0cʜI͛8sIp< JѣH TӧPJJ5 Z$DWKٳh0Ы ʝKݻ\=VKÈ(O)HpXĘ3kY#9TP fY+Ů*@7Ġa xHh nM7 A_C/& a i`y9„u&ؚKq| h57=0!Lpk!9@Pye'gI*( &zBBt?030=HN NHpPbe"i'9##Q7ͣЖ )PntBqFQ&66!:ND35(k 'AsP@ $a2Lăx@ 8 И2@e73/j65Z)2Ap\+np #l%}*Qº䐤|ND3F $  &aɐ:#H%4l8* $@w! ,Aa <>*ޜ59 'tQSN`#郏uq Qɶ w ?0Fb$(*P/LP7[pMҀKABPoKnQ[\l8 !*Pl!42 6D)'a}%BI~o Q PO{X $K ҇o|JڰМk_ݐ @JC?} x, $oR[T*Ok DBɭkA>&D~`ap=sb@$0A(P.+9FoHHBH0yu?a Vw,)\ZF(bCp: HE dA Q@֊ &0 V)yPBV#-Z qyC=}L5(E! zf7aHE$!-x6 @hxSJ:_2 |(2(fM wr/^@Y@ \,Nn%..kHBP$MdkVU/A~*sK.`MzKaٍu1Κ3TJGZ'!T$؎ M@,UG G'8@&RIh6iL]L fT8g(lSHؘtQ` , IS E&BapCܠatU& QpMbV!B61 'nЁ:{Ed*ZC"ف|`$ \#H=,DXAeXA$u i,(4 N9,UAUef XAn& >v Ex }L n=16& pU:6!9 r= !FWzNh@@I @FlBhMQSY} a 20,! XԐ5- 4S H Bj8B(r` 9.Ӡ/i  %'5B#Rg @P"7&屵om cs]C`~gլT0:u٦.y2 ]Z@B DHN96K# b1Ky. ?aN I l~ZқŤN?@,U !_`M AS' 5K 0/;C7 0)46!ۈ# i*<1R[8 d0O'0`WX Fܜ9Џߚ: H7RNm(g`tD@l|7%1yܯ@@}a{3?r/v~pO"V}Hm"& (ueN9\r9PZ($ D9ZA6؇J-;ZLd#1CjBhw CݱoPXײBQ0c=4C'IM]$B.)P  V;xD-%wP,p !j@tDRT,C8=okh0 q a %%E0p),0 80xG'% QZ.yP ! PN@1]%Ey3` C؊ B ZCgC , 2$p/EE  "`=lP`xb< =@~ 0kOm2 CQ / zQ+} q2` h%+%z8$68cͰ /a ǐtqp 30> ? ^k'G`3}qV FL !ܣ'+y0P 6 UJԎ$5 %%' ` d G%֔tv ' q͠3CQJMq8)z5%Ԁ +c ?y"KuaQS 0rY1c~&%̰ a= rU'SǙ95:&Vp AArg 0`_G*@P^@'qb"AKI/BЖ‡9.yf6 б 0+EB-?â% 0M4PJ5K-&.W>AU(8 \@)@{;w@-[K:Oxtpr7zF$68"`'Py ?`0;3'J9ʣ`*C 1=Cqaڦ"Efzt% *_zr2uڧrY:l览/=,+aZyj =s-ک$[t/kxe䩪  *8 DaY @R C*q"VJp9:uY˺rhGaqfёuZBڮelaqt l^p`=h49p:p-0fL-EDAI <U3vv{l[/I"(&qqMU{&2 E@>[ QSrrz0;B;*@wj-$Ba1Dq3:@s?9ʢՃJ̣DTCd rp@+!fJAD:?9 ɮ;De+B0vpR+g@gIy 1')R+zzDQtsc䶝a[2:Z+ PnsWcI'E*D+U1@r;`V ]!] !:s\aARմqS";3-S^  c @v$`nRvZOˁ>nŢqzKI^r` R@QFNw' ep7 >K  q qAr0趽1!W.7nKEKeKIt{RI|t/\v5r a~r-!P]ո+@ {]>#A(`Tr-" h/{cJñԒ@~w1WK;ܕcU+/tKuk1d$70+?y+^;;ۄ>LV=ٓN!p !G3N@8uK iC BWl_!`D C(@",R(hƍ*0e‹ N89aeL5męSN=}TP{믟~B~ XӍ.ljPRM~qQ g^GG7d0$cY~ 'H(  ugI&зxH QV$b:By@6 Q/ :x@Ah7auHd였&  ᇀ8c94@zT0ډ s 8.!(T0ոF)П6|R0PpMV~beO#{;0<=,H)"X`peTƒ#P}P" '0 (H x ߽ep 3L0 nu4A_md\(?7Ѝ2Bo9ǐBQ'thzl$@mӎ;$D2$I$|IHp"jh\ 0X/# A=ci>s#`'! ^] h;ݬ@"B+ʧ{( R@:e1Ԩ\ՋAD ( E h3 Fln!R1̀AtR @Ts!5AHpBH `'sc (=KK$Aمk Wt): PQT6N8U*}^ge'|x\t`GU!,P&lR'H *\ȰÇ#JԊ<Ϯs**@LF+Nkm'j&r 3 A *Pn!/ND@:ίE  4'$PÀ <gB(Ds*y[sƒ6bou (!HH  $ fC5Gdll`0aU ]_@ X>@Pp4K\\O 4PD}09V7!'xT E BAQvYu~ @8W܀@O7Tuێv#晷 BUv]!,N&nR'H@ *\ȰÇ#JNH0PѧBj$իXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KLA @PQȼy#^(QbƫZ h0o@ȓ `aP捡?oxU1HշS H`+`PՀz:?jbSN&jةN@@LC`O>"{LT@2Ajx":GCLI'p Ji&!uh9A'@&$LrI'tg|B D !t4+<>=̐ifLЩ@ꨢJ@ $ =0ZC k9rinԱG N@ZHHB6+g18A .Ops nhQ` Ě@&P @ "̆5tAf ҽM6B2F3B%14qh91?2 )Tc?8.#(n5P@z^ !HA)"T^ |.gUNs|+T@'T)1'zW G~+dYڙ=-;!Pl B콂#dDG][`SԤ"0ˁA"g)$c xO@8\w@ תvu:K:Pͳ&-j` ' YwB$N;=Bn905{#U",|>?FYi5$ts$I@!,p'H*\ȰC @Ë3jQ#D-vII\ɲˁ)_ʜI3ě0ɳL vJѣF1 @ʴAB@8ўTjuf]ÊKٳhӪ]˶۷pʝKݻxڵ8TB'0 0㢏',PA$(_\ 80E/ @ Z H ČU@PH3#i3γ^!Hm.Κ녱g^1yN8A  ʐax'z@O) DA,8TA7h i@7CU0@(M7Tz6`O4PNGP|iBYM@P! S9A!T0%7*R>!PIG9ЃFm;E}VUOG>@̱L y }W@kdbMb'M9A'0A6bȓbs&P(I@^"*`Q%-iE+k&6F+Vkfv+k覫+]&X4J +OR: PI-Xp9jrQ0Zg;A-0P'ST#M}1#82*@'Hx(%{A\ @€<@X 5v R%B,LMQDAAA @mU$9L`O?@, T@ab4cи*!Lpc4y\`1A; T:. @ -[Aoƒӎo'7sH!WOgg?`z`L@ϛFaȧ@Wn<+X +Q!,a(XPA0a *\ȰÇ@0 3jQCٰɓ(5 0h2˗P,hMf3dNz JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ JA ctpbdA' DULhb˝TW:Cc`Q30ߺ/"(P-" P)qQ ?}:ltQUzIn!`VTrl=P"T@Q@!3ʈHSj*T el>s#kG}d1 .ÊLe}tF#uI %$1|C?Ɣ.0pBCt2I$p ur2 t*'tdqX:\ ԰-0<)>̓i錣c7J\x >z0A Ȯ+hJTL뱈 l.1*P^ɍ < 1hD Tf @@j`x0B mwmHN?B>$y`Ѐ uZh D%x)pS)YkG G<fO1O9W C䪼rdt@Q Yp ' < @HAa_mTֈ @' -/𐀋 - p-@nA D!,a(XP)0!*\ȰÇR8@3jQ!Cɓ(58ACʗ0xf̛'` O:IѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ L ɋ y1 ([|z `m-@$1΄)x:u  x Ā 훰?l`2 eL`sЍcP'2{8@B@,BBw@|!|]0:L 8|u|!B`;<TAG#C,Ep3"3AA'xT ?GM%%wu A'QpyHP3Ǵ3e-A,mݩWve7+ FAQOaodA/La20"`EPlP DAxgH=%M"U @#r(Ԟ{-L^U ǕVADfP&@+A!,a((A0a&A'4( \XA!,a(+)0!A !:` $d'xx !,a((A0a&A'4( \XA!,^& N'LA "Ȋ?(34Ѧ?e+ !.7,"F6 =AK [&CV7"$P `قZDawCG{ L 40 'ЇИy(;x= yАq7xE@HP_TxA&9yyAA:9@p}MԀ^H"uTQ@&H(XP )1 y 3h Uh"GP d^,,4"ArĠx2=  A 3Y0R@%\C@ 䈚7Lŀj (JD>D @N lf@D& *ĠB@K6U BP1 $f2$l@P~jp:4@뎋dÄM A7OʙM '|@@e*Ԁ̲W.C`ZD. %-9 DBQ$l(Ձ |D !ȽMyB 5P4R;LT`$ @P Y 7{Bny:oz0(I6 1$A^6droT@2=n2  @ |jQʈC0H;PH@v#Ai5B=UGㅝ7޹PNh xw;~Y;^@'0uK®5[җj&PM De )١2[ÈDmc҇ }Q@*?m_K.'Sh3zR$! Gn+``da} ްac!|h E$CCafh>![h /!'*F}(y_Fj `A= Ē%--sY dB Zй /&qB{Wʄrl3 Bi>t@ʡgc N(8PL@ $I0Nb̦@jz3ɜ (`GBh2W2s>1 C!J >HP2BІt8F` T ;cABD:0k$H=S ALIueaS2hr\ZPC@21-sHU䱂 Pyl*)XA `h*ʶ8h= W(N ')],3fWZ$ aIBէ)]lD @vcݮg5`8A#Mh=OyǍ!h"Z`kSQ G6N X *5$ !,^& S'L0@,A@8!DRPSBzǓ:TrG*`0 ^Ɯ) Qnl0$ !,^& L'LA "MP[n:"m۸K2&:^qŃUwOzMS9c)C }'@}&C 6CF(yUHVm ,Ѕ;P q7߄ G%WH-2$i xcxyhXL=pr4d}NiV,4(‡Q%&f~;ݖpD\pF;k$_ũTPlwY%|NI袠 ^Z#6ahjR@2:*R+⠷&KSk#$O9`tlIX, Ҋڣ `Ѫ׊Bf"}+0IU jQO)ofQ]ApH`Cz`mNs/0*E(/axc8  q (mF6 f+1|%_3qDq #61)`F:,YBtC9 ' ;]`p0G2q'`Fn@&O3cXRΆA?=stCٞFZ 8P- NhSD LDgÛG8 ga?hMV4m5p%`P)iэHkO,N'7=J)0Pe |wMGDSHF*_',f#E< 4O>*p{5p%` W07*"0ZP 2|EBGkx0(`9p Gp04 A@7G$'~ DCj1+pn0p,a3 `f"+Ahe2t*;"Dt0o"3#Vdh6a8sE `#82pׄvX(V,Bk# !Ce;~4o(/T%e_7.Vl! (5#~B6 uL5d&80 ai53 D%EAPk!;r8l 77kt8hbcq$[gG M88 q0 .Q&rQ3Ψ .D$P5Fk1m`s07‘I2>C<6ӓ!aeX Q&'CE8ÏV㡐E K)*n x )0Hs )Q!Y6H&wzsEl$h6GbD}IK/1RK2>i{bf C\26 j@9P2R)   Q9 wsm=' ? #&>@ fS_ٜ7YIP :p-1٘P j 6HTq_фOP7c1y)fRQjA>@ PO  T P?t)`C`0-Q. &&45͸ RN~9bc  0?j.CBhD"C:tKIwj-lh1q  `% n"p0:=k9DB$bw駦8n)>"Da ( $ !wS./ 3p:* P TF0 s~   #VwP`KcYp @y KMoSjpfeG ]d?P >p.Qj pOP@[P @N p €` ٗưM!Ү  gdp>C͈W+ 200ЀBT Q" ـ b6 3xW0 pʐ'P` Pz H0k> j}!t- `T 8P1@ 6p W00 ǀD j ːtQ0P d%0 Ȱ>  GPI0|0ڱGь E3[66ZP `>PE ր` k ؠg HpP/ @Z,pH[`8^ڋ-PP5Pp*{p>0JP/^I.pPa>dd` 0P080:!'P"B""Hf j}g ɀ VmO ʀ p  d3H+&jq1qx@ `H ЀE-` ) o \p% BT%`S(z CJ PJsh 9d2q :EBp 6F9 P> 5@p 0Yz)ŗ8!h6- S)0 p Qȧf ;l, >$DS :ˁ@k8u*)AʲAo   $P@0mg:`{ LmFKfNQ&8 f,` Yj"R tΐ S0 Ż| h I P  @0uB*K] P YA<s$ N9>0)Ԑ8 :7k! 0 kBR&0>ӯ*6#J =:1 GBORMQX:P@0\l^)P'@#W 0>m p 2Pt}7X^5xNB! 0k NPȀQ$0X -0 = !x^Q&Lt$9d/ `) 3@p) Ou0,`@mU@ [Wg`jZ/,> $ gP-;R ܰPZ7NEmP p 2Tʀa) `K C0. /8x!k#/,n }k  VWp0*2g7   ` S`C ɐ۾2T u},=00 .2v$zcf1` 6b P,(*؍ Ӗy|qG/zzp v > GA(;Ϡ  ^'@$wj@jiHh{80ygVZn-Q q>H(v9-p ?}Ռ'D.(;$JrsqY@*Iti23I2v9^NMCc17좨k-  54Oxii>/Çxçk 0TY kC5 Q35sl1# }SA 88xAH26n'w=B/mP8boF!swpSA虧#5X$c3cAu-qA8{T'Dxp *T D-^8 &Rd@PRJ-]XA4 "Ha& 4‡ LБ#@$X!P ʬYqaÇaEaуԢU\u ް3Rb+5 X,X Đ  N(X!Ä!,@8 -teD'W@dTHyL'(1ě;4[pE^Éf(hme+Ą>@@Ռ@S+"Bs0N LX@pI ( `؇V0v(Q2'dhHl1 jQ(30%V[kE lfa 2Xßa`XNvG\ @A|hu9Ŗx&HlЁi~2 `Jb@Z&[8riW8 A 7hCH jfXWaqaH\' M , Xh ~c(Jb% j).cJ.D] 0y9d'>ggvJ<@@83H0' P&)|8˰Wg ڕ2#j1 wYznpx~:m qȃ +ZaHAxItk=~~t"Cp.@-:X@ KRP!@*W'XANk X:$,$aT?ݯ<Π]\n 6p"@ Q}B X;Yxs ! ElK iÅ#@7$`Tȸ9p><$Af @vC(5  wB/50%p) :eKH 0>~F F0|`#f4_bܗ|أ P/|4 (j@L<ю|p p5HѮ ~"Ja ~ sC]fF5* ,n P !R}pd3` 2 !* 6"@jJ? PDNPcY&PjίGicMl|Kx&Y[ 1%"n K8JEBJhHVe~hT)5!h c \ G0jlZW{¥ĉhGiŭeX:+ 2 B1M(,&PD$+\ٸ(jm] b`MEXZUM:V6 B:f&]2 ҍcmհ h9CA^PM尟*+^uDƋVf8e('I]}w-D*"`Sr.ih!l*ˠ sTJ y;䊰wi-5){^'g(zg52-B``t eTGWƴ {2 OldW-I'٬g/5=(=sL/6Ù f5&, Ib*@)p1o]b*Z>*dtD2W p`Tl" N\ؙ"ץp6M :+겚wp5$-d i؃ 4 {<<@B l#A-BmdG̸T$E(BM+)h P"g=[Ke{[r_*PD$N!COA ׬`+`Jpo  V p/ ` `F76-#J*4{:~ Z@R£V7 fEpm)붹%us 'KW8(3 +P ؅圊x#AK Nyc@@ P&fJEw/D`m 9яw/(y6c2>aD庛O3ғ۹+=U7x`S,ݬy:M:GeJ-WC Ρ/dpw('`"PgJpkLhhOkyk'-YZժ)|n pz!nvj^)^r'0 b`?6 *SoW?S4sM˨)Y`I@P ?D@X-?Bxi넍i"I3.H32W`<N`Z<{P3X<86TQӮ;ծC$rxOM {QYO3Ϛ4˘x3Ϙ Ț(0)8"Spb<\{̲/n^ 3M!{@"nMҊ< 3Gћ<[ۅ8[ ÚHƆ FX9=b4.m/10# k_v_U+uS_z{7-9̕$@T-_[#_ (9FR Kۊ Q.a5acw^N54s:Ճ =>(aPڎ2.&9ژʑ4r_bΝN;H`᢫*r؇}U\%p"о NfE8i8@'8>2`;a8;gX}-hЇc@ & (_v|9BvdHNq;%,i,f- 0лA^Kx=~S Șu Q`P\ɾ)DZl -6>lay`apdH_yh^o@|vv?}yxWW`yqP}-,i8[Wj dƞp9 ForEX_# {y'dr@_`PkN|\ %m_@@?Pg8WJDHUmpH:f 蚢.׹ #1~@3Fh(/ȅvHky@VOӁ(dg\suR7^μ:ɦ^_`>W (YcЇh` ȱsXWŮֽ +Hx|H!hh3 YOm}w=Gs]H@ngFo`8[`&'~- KY XfDd$ U[Pr8 0A|-0c"044l3-{ pL:٨K8`YiIP[DV  rnetR_ăO2<@$K sŭ:0 >L8Ş3;@TB2\@7TS#؃Ak{ I]0Blh'Zg.@@ #@cYp# X,p0" M.21dA L>LS-3b s 3$!]Ȁ'H<8YP,!LCHAQ ^O@D bYB腍`юcL" JP|`c`2 X!5p TF%Y@` yI!`/ D#?:/Q`L:Ҩƣ:; ppk A2WiTM(F9t (P }\@ Fn`&[XG'fC@ &TQA:`lc!+q\D X@:!hJ s*%P<@3W6\yP00 l+Y Vi[#8é`)|1_ P^c %R ` p_,F KaxB P_<#_@1 pȠ;Bx%&ĐADpsc$.> 5G>:7ĠLIN9G!%ًx.`=;w`26̑ u(DAX#؈z_ xp#ذ4RЁPsF)?,K̐.d!@ x6JV0HHM}*nv d`3J Ѐ6)JJ=04lԓpƭ4o mh%`΀Zpb@n *f, \~da?*Q i<2l%%&&n{k F>Bqtc"HA2 EAء ;02 5c$7zP4 G7doPC7BAn 0rt x3|Ĵ Hv'aglÆY\pNj&>,\ȣ0:Q,>H21W"SX0G0['`` w` F }E4-(@4<,ȅ=p1Q5G5d>s97 R llcKp&:4Ds@j);j",=ܑi@!>~ w= _m(%IBG5!`4  wx?a ҡP4 BۢQpxrP/%XQÞ M"`40 %) qhةY~yT `8,PbyPx:x(i0Ji+}s@0 ஈlT $[ϰ^쾷E,x $ 11ܩI.PpWXIcg ABp.p /k*4!@])H@mIh*1 ZܖQLHK4 qE>> TXW5HOlYŶr@)=6GRPY ll d@ (' Z ZSXQ jD РrhȀ ddQĹ} KTKP+ːElQ|h}dRX!O|љr |6bOeKaaIl˶p nGA@ B"Obi"!!2r'#"RLH'L+,LHQ L``(.h_%_X%(% x̌n"b@t0B1BX0C0T_Pn THT]p.$+^)"AFɀ0LQ8M+"_4 +YB6,/iL< At4d=YHñ IHP]<`C ܈1C>C;/2?(B L>C< xX @f 097 '9NdbQ@]-͏,n@hp<C F@ 4]f\@an䍄/LXQĄ2P'L/(8ԓxX,B;P-)0HY.> $^((7@8(0H8 ̌=8Ch%W @ ;齀Ѐ;PCEB7$5@ 'A p?H%T-%=Lxp9\F&Ď8,$i,? >H)1T@, YT;  ; ~@ ^NkOW0C@0Çh :di?`{dJu2܃2VR*/Hp"\=@õ@K@@2?>B,@&0GUC)N@ W@,C6 @桢/R,%`i8Kk C=0쐀( ,k J0 0@(>4O@lLl-jA:C`h-06X@F4``@Ll!Ҧ)@Ld `d@x@Ce-Ī (AB2l ZD :n;hO bNAL9؟Ho!W]Ce&0 D tH d;Hdm4.l6Cg2@7%T|V90,((@&ԃ.-@ Ę &D3.++VԆC<ȃ<#T0ĥC?.EXIOă孂:=q\HW3|s?pONF,O5SwQ#*u55U7S'5 uUo^uTSXPsX4j5VxXuy).[R7 1]SͅW"Cl9C7ģ_? :,nbkc;cC6hI_@5<^:E96lI_ F*…F^:ԉktlT pEG=] 7tq`USsG7wSE@ ` haw|~V(|Oow-@, F<ː@ \pv#1͜^7υ5@q-I8h Js|pw#( DC:*@ t540GIHxI\HQ XC< A ҒЙ5C1==!so %@o:LnP ( _`Fdɫ$!NHA|I W3Ì%'@&C1<0:^Ho%@4AQ T' X&B }.؃2 /C"0hƚ$9,8rokdʀ ;83D74,5嶁3H;68$C(Ą.5T @ <\A@[8{8.xF9dg7,tA6 C̝Wd(} ?10H ?8\(0DdG6R%/A<3,T,^@A N2=Ȓ@>@|ɗE,:{ ;@ =8A 9}>PB?$B, D~C09 \i#xC~M l9t&N'~\0 @P1ż^,E/ )M Za]tu '`P81a 4i8b& >q0s*PXJ%AF:jUWfպkW_bE { ='d ˢn2@$/@  fּt2GzaĴq-"7Xׯaǖ=v @`7x`W'@S&1btC@*w+a!:JyQ\z(1ِm,0|`=a(MFMl,htw‡HXDF0P}l g|fhoFx)&@xǞuB!/ *& ?2p 8d &hA p L*ᡆ*L3 O1HadROA UT44) HN, (@J ` k63ItH(sgVZx%tD`(-N.PU&4YtHiW_;q喂]7lm:'f*^ ʬ{XchKZXI.5J#E3a9S dYy7 eJt磑NA 0phꭘ&0ܹ Z.&TphuBAB'̎[n&@ƒ7j Ad ('0%\EM > bӿ\.a3G]k֣>iiwf"@Sy tߙo~R2%pxЁiw^]Yaw;-!, 'H*\ȰÂ$,ŋ3jȱ#CHɓ(KKɲ˗( tF͛8sNk@ xУH < HPTpSX|ׯ`ÊKٳhӪ]˶۷pe0& 0$ #΅RtX02 'le!j (&Lrل+B DQ޴ItweJ$HP6{@ S>ڭ6@ 9PZ,HA  ij I- (,bh%3'[IXN# @W5@YS#"ӭr;A$54YsIL j7N[qX0",i#cM8pW q%_^0A CjUHl;R[ 0\1OpOQ0-!,1'!,1'!,1'H*\ȰÇ#JHŋ3jȱǏ AbIɓ(S\ɲ˗0I͛8s̞@ Jѣ "]ʴӧPNP*իXjHuׯ`ÊuٳhӪj[ݮKݦ](߿W+^^Ì#KLYc0uXoܽ2]颜I <_|AOp۸o4\by+5ͼ^X0x{\wPAݰٳ_Ͼɑ&0Bz/Q3X!߁&Hu#Il)(iv@ FH 6È},R\0dw-#`v 1$C*hkA'0:PePFt # &\ZEXtXvuiD9f@ 0\h)'P 0 U|9XvRg~j(xZ`A(硐F.vJPf+p* *@ M0@'P}@! 0(И d:DM@Z6Ћ"( ,Ъ[ g *GB2 D 5yN@ ĐX0}6L dL , @ ;A Ib, % F<`>;t| (L=ce_Op6C@*f$3a2&D "H*22dB X@xqkDZ7dІ*VL@BTUh @B.uD8$`@]bԐX,, p :gHF$ <@$*A3 s PH D2CAh0Jc5r d.\Z»pyB R 밃P8X@@to_`  U~ eB0jl DF4RZ3&0 *UԈfF B- (- 2e`8@yaيG4'z%rE# HTPd0'@YTiH zZ]@p` #HgH`QVwR|ѐu"gl, dpj+ā( µ+@o HDA͊)(mGZox“kØEnq7\ 0 @2 @ dj0 R$+BeS1d7vx"t rnA H3b_ ~Vp!2R36gjW g'%4 4N+}{ˑS*-s+N3 "trnhMT5c߇FU+O&.ya ϐYj #QgX>1]uxF-< rB\\E-b52B6 -l,W$`Ew^A7.P@"ASxr!ls-wc6'=$6Hrhvp *2NC@(@aP#B01 k/<2[0 rq?d+ h PNVb03`NV*&E\(#1 -'G4$Г1 CW&x/-0ѰD@ "P ؀ TϐEKv0AŃ @P! 0 2Ѝw 6Er8:Ip" 0MPcưMD">$0GI+@ 0P*H'PŢI"D` -`aG-C ` ~ hx05"TY>P]" Nuy=,108$ 4F/ 琂p0@ :&$P;_ 44[Iv XЙҞX ` 1D9mvL5 nqVۅ (pYpHT*@OCKF%ZK4-!$q~2L&$}džnQP#Q_H@9ev,qV-4 @A06"9?Fn.^D#YxcЍ@ 8NUT5@"ť 0D pC.00w}g{vp*!r0aQ=>!`%|V=O PAlLU)'|wy 5-0sA@ 0=2@DYv4-lrPH-P:?`Bk:rN:!͊J >D -AP* >~M:P jn3P - J' i:Z"{$j8-b u: j_ÆAFSr22 #0I!DMMv $Đ75@(JKx)C p'/08Cd  Ro0I XmZ v4U% d-ru&4&.v˜Y#AOKl;C-=I 0Oœ?+{ |M0 p%.00P#7] n8Ac SQ:$ņQs֫|K3ߗ pUY* $0-"1dK9"d5@ hT%îÍjrlp !TcwI1| 3 W0G|n*02~[j'JTڿ1X5)*LfS,E9n 7'ÑP.Ӎc63)\!uHv*1ҡ>ķW #qcqa1#/1(qW@&&yJ* +)!# &I5 p8"쌒l1C2pi(Cggϯ&\}z 0 $g1?*K&q&-p(p0Tv(AA=q!.]/*9*#U {q#ޙ!xSht+UT?ec'.[Hc/ҡ\7-YY*20]`;gձ*$0.0&`1 K O4,Cq~(=00SC  p3}Qz/rv"|Xb3A V7>POX7ړUY)M%ڭ-v &lV(a3a3?٬Ȅ(i+-EI= "( 3$Rz:~Mŀ3Oi2 N0J1=P]Ӎw0dӫ3 .$0 CZ "l o2 ^ ~2aC2p ;5K##W0 81 (א{P#dp .}!wCp&p2@9 TIMO*55PBT`]n#T-&x#`^Pcs!?(NM.xh*=|-"SN@N%2|y MU0 pc<+pMM"~sv @a.`;'Vb4fޒ2HCk! xI#<ʡFՎ$WBQl1T HUe3<0 0DcTxս2:B4@} PH)m' J$@#P  ace>UV;7&-B";}ߨD>\$ k`:<1XbU .>znj2-$ /\ĂiC&pw'N)!iaZ0}!bOp.,`Ϛ(Q L@8@ RH8 #^Jo!DiJ-]SL5męSN=}% 4(dRD`ʈDOLPaJFV8z1J'N\Tzg%RR\uśW^}1) O:AT (bJ1=2-X(q n%Wj֭][&`c'P(QAB0Y7_D9 gySqY W'#l渲^xa58#0mp -@ b#ZJ+쌃;ȃ0B '‹{ *&AHN^Ap "!JjHH)͒KRrD * H{A6R(0I%dI0tICg`i ( iFiax@ "cpSL!,09'%PCHdBq­0.& 4F@Za(P`,`lpqI'BHh%* AsSx<  C6[miQ "[K AT @ 'P !"yl&{EHfɢ$ 4f[ qǟbwN) \!%u&E  }汦ۖ'"i.X]l Ofg|I_Yd '@ິ ww1G@ Aak"H+h@D!tBई& 0g$NB-_Qp !iDi4`aW6G?}G<^x)@q@/^uL  @{L FVu@E * (҆0*%p$0 \2yOj Dχ?$@UHH1 \X%H/EBѩ F&ю@~%ăzNJV" i ]͈᪒i!x^@ST!$"&B!dz;*A`Ov;z kΤH L #.@̕Ҝ2JHf6S5L$#)IVA.E$#A(':,x>0 !KG"d`Y(hK9B2 „,k d3e,әh]J堄˰WB|x$8}p %P 0 {X NCH8;.C\dBd/PuA\JhF:VmY2 2g +F 4ZȄ":SN`-`.!,8Aqd)_@C9n7J*([w[f=cP]L7V0Ѳ ƭ:b]泣d`Lg p)i# @P #oeL(GMyۂ_FYoJWGβ> !h?gt[kiKp bu=!+Nzֿ[ѻ[O# Vk5X>!!Ax n,p7 YD+8"`' ,$ֳʂJ߱GBathL:p`ئh n!  ʂgi,yLJ Iy-Y;8K\=B_4+ hH 2!c8sX3kbڛ0(C3fS>?s¸CAeَhJm;eP( l$?XXsi` *:! := 09- .MFq$z9 4@1U* %g8I$o/-ȇ_ fE(8  P*z)9NOċxPui=PkZ j:Ӆ*o8S{ro`+B j9F#hh4H H xzEy$I*^L$@g-3ƽAfHX)]Z; +[  `;0Y ABL`C 0`Vb:'J\tX@B_1):*t99^ )6 23ۖ]0A)H TpfBH9ڛM  LBt+}X:=8% wgXæPke:/{JVPd3TVM2BcqB7KPIS=A'L0 6D9c7.g>S'ZD%,JX20(ч\0g wB5 /Ak~>髟)j哏=T"ÛM@;ؤ &}X#KB;]jyir?q(  #j &ֵnX+7h%x>=q2?7Őo[(DO-9\WT,8 AV$VOD`6d!KGE o7 P'(`QDڔ!~Wr=)MiȨD&tDł?RdR AːA$ H*Ml$/U2_o٤&Y*O1ȾbA$_9$eal&OBV}Jexly\Q^oAXmrArFКx>iKEҜmfX)y2QT4YM,<(Bh_m  ($S/e; `&B$&@m4(JSRJ4'b°?es6)N1RCĠIs*ԡUT;LjRN}1f&RC խr *Oʓh^=+ZzTZ&*qֹkXڦe+`Ґ’k @mG8[:+vYa"-P KR]eH¸ gf-%SKxDBvb A-n\ T{I1N*10r{5+@tUrŸ=/zAeYZ4N,ҷtk^ R, iUp 'pAI\5HB6dSmS z HEV2HW4 `@ZdlqM&$&>` iLBA8$ si(u5 ^!%&@ HC+ў:1srC,hELK%6'sK}a /|@ "D^HM&A@C8` ys}Аz͐ޯ8x-8&w>qMCkL Θ̀6@2 JAIoU)RU d \6@@B`gxGMq[v/Ns¦e:$-l6e 8vͶ,N ZMi#%Jtl@ƕ&+mWD 32`d5$Dlyٳ2,P`K+:!{l`vM/N t81%PH $ʯb No):xB(EڳrpO@ OS-.@8F^BՌZh\ 5 M_VBEN4(WN.L.|XLJdH90B@,,\B)5ƜHC4`C$OAF'qM=L d ##HENXQNhA'X_@(!D $F!TA^ Du!㌘d@1# X`7 ،V@%VT #@d%Xש\ ]XmŭL8%Zd\\IM¥^\<\`".`&fJa@a;l bNYPz Q&g 8 8Rg&_BkT,ʍkb@ l3٦oF LqғatjqF'7a@ 0 Štv'ޱ$@x'yb'zy'{z'|^{'}*M}gn~~ͧhT (ch(.(y((>h@NE^gt~(((((ƨ(֨(樎!, yLH \Ȑ B0@@Ë3jD  Lp@fH E(0@A\6KyT+"'c"J(ΰ7M`YNP@M70_Akpk[pĄ\5N#d 3iAPǂgj+` :dEpńw)jG>j`;P Mv^'h.:aS /\!jӞWW|7\}]Şew%T'!AT2$PL@5m f6QF@aMD7bbfGR'ք ~Ĩa#Hf<Bf(dP$ҐH&[TA.)T'gY;NT!$J[ 0`P<)J8lƉQR ]PxLyy~)7=o& YkBQ):-HFO"f*l@݈*P }R#PBʐ E'QN%} J,k~nJ9(=Dm=f-$0H Br>iLA0ȠC~8@ 1L/6.@bu' T X%<:ɐ¢ %q $u1f1CZ@,0,4l8<@-DmH'L7PG-TWmXg\w`-dmhlp-7PQ$ y Y9#SLT bӿ<=!A+[mgA7tӈNVL>PWfC!,1'!, '( *\ȰÇ#JHŋb0Ǐ CٰA(S\ɒb-cʜI" tɳ̛} J㰢H*]]"KJ%pի+bU#JtK′hӪ]˶۷pʝKݻx˷߿-B'@8<bB-;" 1pNj:-x< Dd~h! &P !*8cP iN OHq pu?J%{2ZM{H":C [` YCPXB4yL0M3P;"ALA+hҰc:3 6{)'@s;&B+0.L MYC$Њ:$9 @HX@뱂 4DBO Õ lp $NE26F$ _3PUKΐXXfQ> BtZ9`c@4u@(Pr3ICo$l, 4j!x0>|B YIsj r"H68k9Ɣ`j% mH4C#bk!^DB(|'>Q '5\ʀ尠K "FXtja)C@M9|b؜Q#xFǨ1D mrSƌ_h' GL($BBPIt1T$!9~h3+ŦEBY(Nr$h H@ &S81A 91cDŌ)D3MqN\F:`H!ڹ `bJȹ, *,gKו6Q@|X*"J6d4L`8lJM4)Ts64ݩP x$PPDb'OP KBH((";"R<>A;:X@T8*H` i8A&MG> {]L8fXdbSm^ N%E7qD"`|0%R `J p9+ǡهe}iKDM3xnpL P>@W bDQO΁Jl*":Xp0 PJ8!\Pg(\x2Ne(@ S%  @t&d@RCgąܕF`Dp̈< \H7tZT,H󢸵;+x+,8M5~֤yn 3yC U*A2!)Cm'4Oq{8" @H0#X,Ǚ Y@z$gkg!QF! fY(4KY.'dm"ܘ*w@E(eB\thLAlK+L_@#q1 !p/Z3suAzdaA b1%~y3Dͭ4DAL{!KXhtU9ǣtDc8z 6a( HAI|ѶAQBibm̠: iAqV+d0.=A0G^ $xv 1@ '!H@ 2nJP$$<,D A@B0Zӆ7WsFcFf )% %ւ(^$ym%41K6ӏp) 8׎iY@6#&qQ%u(G_bÐSxd6G6k& 4bTq' )] P8!`SVWH(xA +k`0 p^%` pRd%AUT B6/E'.T m*.SxKg))3Ԑg*2ZR~"` v0i~Vz^%?"@8#1GzU-)q 2u,bAaf5Ep8 X+3p]pW ):H:3(9u1 jcQw$de)Bx,"bJek%Ơ=G-yn G(d8)  v$dl0W]!3 P$2la; Ơ< E b?+&]W +04m `#Њ'6-0a?*r' P5YPjipV /vP <("\ a` jp›q0Y7sY@ZبG2 =@JgiXP.PB^"/34  A!$asM*&{'f . 99#q1B~6EЂ $Ҋ>@Ƶ.Cؘ 9NJ6OK!DsbY²3Ue 3i#+:%@*6xbPC{!p 3ڀ TP8k"qPSrZ "0 R摷Y$A\! =Փ6G s1x"e$'ɰ4WR0*n ^)G *} Q+ }1%дy ךT;7u*hP|c @LtG*B,@ bt !A3Y0БaYlᑷ7|XCz|ƺvaa I! -J K**H,2ULjPlyʳ;\9aL[+қcdDC T+L$p̳r<Xw6̑"0P0*d(1bV3SGK0BW/uD?H8%:{ X@|$P-PX0`E%Pw%~(&sF5#Zd1)2r.Ά01ŀ*pHuͼ3W3raX "Mp$hxP&4fr`( J u5 750A"e21pӆ5s;V%)d;,#$ cEN >' "ˀn! 絏hmıpDq E P)`\E6w|Dtc#nB$% 'wbbA F)#P B- så=0Q`Sfri|)V/Pқ @6=  -pMx=X!S0 =6:1PyS[ @ A0nsaQ0+G2=4[r1t1LI|,##~}10m>J5fDŨdH 4kz1,WR~^ia"9JidaI)-` #hiƣA *p,0N2~LE h 6 6"#̷wWBP\E+ e  xR4E@؀ؓ3 M:WP s# "V*߲#-R+@Tب0֑ۀ v!f\;v/Cц * %`^ nPS(2s%"Y b2 P*2:@ * 5$JK4@V>,lb0 ZQg0ʠł ͳbb4} :Aq2Jna4 ,V oRM+q 倧 2+[ 5p ! ހ"u HUVa)a>s8ͤ2ns %a @  gq$@" $ +!~80AH=* qSy;N ҷ p A $\%.P$!2mJqO- 28P "EiN+0rf\jX;liLkQ+ '@7yYA ߝcL Me=9@f2p%Zd%.PU.p%@%ȠC1A@  ]"b:ؤvʛdM?(8&JIM0Ʉ Y(6 &*0<(i@TK^-@MCC2͐Pcv @l(@N&T]+|kRjY8@+0(5hNh1A XR +`ńQY.0A0rC*l ^c@7 sDAt묥;Nxrg>O~ӟh@pEhBYP6TKU;P hFwDhԣ.RQ,iJBQC/] `:S7}MqP% h,X)_N8PQT.G;h,($2Q]< 3KCv|iO 21@2 $y@v6lpHQU*_%dƓxTfh|R + !@Egs@$R0``-w%˼S2W9"D!´D Ž/}&5"TpgLV H4g`BYǬr O;E /DCh(F<2*jAYet@|1Xϩ]c^X( Vil^c?F5AEdXvE d\A@@FW+̃u$ BS BeDpaPgT+4VVzD)_|IՍА-| @h&g Ĕ"Fe 9v[Kk(3|T9" L @x!(D+Q| h0uAv5S@ ^A硈d6BCs<ۤ"GֆpD,ٻM~y0S!6aA 0 pX, E-$j|J&-+rFʠ꺔 % M`+>0Li v:ZU'0h9= |J{ 4u?( wݛIpΦ_A jL@a]<_(%J4xw3 !,1'!,1'!,1'!,1'!,2'H*\ȰÇ#JHŋ3jȱǏ Aɓ(S\ɲ˗0c)͛8sɳ'L> JѣHMʴӧPJtիXj**ׯ`Ê+ٳhӪ]ە$۷p kvݻx߿9 LT 0UĐ#KNU˘3o{o5Mlhิװ[pD tdͻ#8.#UP˄_:K.}$2k{:-sO3*ʫ_ox$ xq! Ȗ~%L02X 6fnPB)ڃfPL hQzXbp0bB8XӌUP@P>Z` UI2) ve IA>0@ 0" ) PT9ҍЉ@re%$gn  @6i.?PH  4:I*d xAvP+Cp!TW9=M;Ikp@wN` u 9gN&Mp3E@]XZ O>`3A@N piM >&@{ (R@CL[Ic$#*,#i ؼ-$ 2TBPYRL$PAH@>L@w $Lp\LP$ } @:<*lqPl0034A*# Q 883>Dؓ21 T+..U_L4M33A=>,4A(@*pVPf9 L3 T@T0@,3A/;7BC»\@ T3}9aAp(@!|g/'\"a>^?y"oH_ A    Lt1L B,  S p $cu&p@cSA ;L>D`ebNkj@4xl DT~)31  0_4"81&0P3-vOĂI& LI 8I@X8AH O 2$ T)\ 3yH#Ї@Q JN уb R!@BL F) =W,bJcYp,b80X%|)?Y3GZ%eNd`pglc@N+sv@xaJ" %LYZJ%, B+6btL@f˔!Z̅T S!O2:Rb=(IEz'A'A y%XA[sn s'݂8Dnv|p cawBP/AHu0$R!IyDMDLE%~U *#S%<"7+h(-,$ +N 9ص2NHY<Z/J< >B;-!!DXa)Z2@1`30lC)x,$@N'cg.(#E+{6wdBz&SᆠY>#;!FjbTMVaGNvY׺ѷ@ ^wHk 'G P XQr~Ef8T~<' HT cEA|p>I+XE&nl>[i 8=H]0j*Xauoh5֎_QA h7|֏L!% f`A8N7qOyF(H++0Din6Ky͖jOIѷ3,H҉t)`eʄ2W!$ `:O6EXm@|}ҡAz}{mA D#C ϯ m^!/{OIWЯ:Sh 穆LuB^p{Xf}N3c5s"p %}wmEk p :`1S Hd"@&S@ >P/p!-Fh :0A[+-p3,"094pEѵ.R+.QP~nq 2 $`,% ,5!VrqmLo/Mb'0,V2cL"p DP155Tc` P!d[ns130  pD0A'p`@8@ %p4DaYH? }p1q1F 5;, 8ZC_'; S r PZ D343 "@Z3"WBA s Ű2_(F  23װ).@PseU:d pH9p$!P@a#+0+U$`b$!U?O: 00 0ӀB0? (!рy * 1 8ʘF!cB&:V"_ O(6`#'c P$  ` 0Y+ FbC!p 0A7քbw120V9k  @PIc %? )p   Pp'p pUFCatYd-S70~E6w5g i($ )#eq $00D )`+`-^CVzLJI3Z!A@4 x8.Ӊf?>%,?6g 4?b$IuI9Xʉb$1)0R5 k0U2R+70O6bj5  eN H[b =I :J>i4 қ{ÌxN p93UԐ $N ` dtZ5$B(0Kj-.+.(9!,YR"IH`O;E5 )xUv1Hq3#Fb41d::B kQ^:3Wf i cFZ3@ @`o߀b-XV) 1`H4rZDRf$\*7xP: DSSť>Y:DaqE""&I T0b1H @ Fq-&.)09mCX@4@aSO~55p50xp1TU9# 5dRch +-Sq+&6#.r ݅.Y h'(#0@GAm 1#o3K%~cҩ` ! (>΂~[05 K2&լ%p˫V}y#&x~0A%#+0&/v6&x:00$ vJ$A tcp"И!j0o+WN)&U aY1djʻ$#XlI) '*^r+_htJ"rRb") Ar+@, R%),]25@wy%]'`: |:FN,hU N,񂬁clGzHBp0\0=>o !`FcwMoBWG23q gr\Rwu,h|ɮ/x2kA I0MC"@l7X0 \ 0@غ#L]wsyM ?'  *Y&cOd 0 Cm6ّ mD ;mZ}8!OJIq-d $ 0Єã ~!,%QS}$9p c &HWtཀ-5"SPEeģc p SUI]1UPMM|% Prp]A]}]wMFѐ ր CTS@!.i_&tbJ"D-a"Q z4yL&cnck"d:2;TcOv&R%UyV4?BoSr>#0U!20Mg$ _2Ôh 9P#AFJ( p 4L9"-}C%QЬeb ye{z3EP)CWhLrb$` eEw-2 huq.E~@ܻ`P RYV\V4f#46SrcrW6?{c%Q^6&6R*0X>#Tk3*X.BUnx gwBTo^]'lLVhgoS/naH Ң)Gfg!'"&zZ5\ rኵ/&|Ct@ ! ,@bZ`6Ad ;A2A)P`:b:(@[ CO(=`ݽ^x+{,< Zb)1A_1ԶP0 ,HDY޳g AK!&`g4MIhfD!9+AٱǞcT<wGdf!`0A!ePj9a &!\X PTl)'nɂ. >~0#!3e,SHh 饄g&BN&g@RK/4Jl 8 ?;Ch Ȉ &@&a F5`(T JA\˅6@H,4g`Xdl X TWYM߅7^y絉S 4T&X J"`.!# FJueJx@&HNLO6!`t 0 A.'b}^&h] _Jw~AH 5*@$ ionxXh/GH(PY e9 &!L(F5 Xm.`{L Dc,e9Kq&B@ ` @"U Hw,O )ǘ;&*8u (O7 HH$2 , Jҏ&5Zӟ-kr ,L (rH#QKiĒY/1 r4 15"b rEJ+St#@"%+ZڕHDf7ԃz($ADH|:V4Yj6PvJکARKCHZ d%ձYgvF9Z&49:8z%lo)Rj,tg O [*N2.)%xebj?6%i@!0p٪U(5`o;^yv#2+|V;Ao\˦ pz8RJO:JzHPU@'HVBNy[GG/!p%?Ї - >|L#.bH@B\o'@ @ f!XA!$زڂL(9K-=lh؀24ZWf$xSf+&' v @H&&] E; ]LAxX5B l xB $|d0&>Vp&ʐn BcԔD  ̊%Y&p  z+I X !,2tiӸ.^pĠx Hnk 8zq8ھP"8&XBDY 88bÉ;fl<׳YM  0SqP YlZ):0՜;L\qFG!T0=L0<VG'PB C8T4 L@OŠ,N0@b9nEy$IdYT>U{>@Su?]FT(]MP-1GN?T8u ,?LB*3A>8a[G0*0#O7UGO$B2$z)YifQp(h`tFz@UFVQ=w~$VFp@Q:@'G'5 R ZH*骻. m*ciz/Eeb+ $ F|0 wnIdcozb R;T{1ȏ1LXđ,YtPI<35/juasK(;ے7+4 mg$*[ ,%4aD{0:5֦imH;6umә<]#]rٍew+89Mq}ks7~9k~#5y囋>:ܹH!'n:tTa;ޑ.V8;+<[KϷ:im]=)FT̚~Th*Ԑi+`qӀGdPi(F kbэ #$E9R$, HG@= # P9H k_˸{Jة'h! ;0َ\&pfU`^(F4PL,JԶ [%T*Ԏ t@"+`pb>L L:\mXKV( y8A Q7^I y.ISuU !!jN`>֐#Q 2!}>"&BbQM&vb' (("*)"+* (",*"-z+".-"/."0r/#1,#21ņ#40F#53V#6Zl#7v7~#88#99#::#;;#>#??O@!,1'!,1'!,1'!,1'!,1';tty-prompt-0.23.1/benchmarks/000077500000000000000000000000001403662044600160565ustar00rootroot00000000000000tty-prompt-0.23.1/benchmarks/multi_select.rb000066400000000000000000000007411403662044600210760ustar00rootroot00000000000000# frozen_string_literal: true require "benchmark/ips" require_relative "../lib/tty/test_prompt" prompt = TTY::TestPrompt.new prompt.on(:keypress) do |e| prompt.trigger(:keydown) if e.value == "j" end 100.times do prompt.input << " " << "j" end prompt.input << "\r" choices = (1..10_000).to_a Benchmark.ips do |bench| bench.config(time: 10, warmup: 4) bench.report("selecting") do prompt.input.rewind prompt.multi_select("Which number?", choices) end end tty-prompt-0.23.1/benchmarks/speed.rb000066400000000000000000000013141403662044600175020ustar00rootroot00000000000000# coding: utf-8 require 'benchmark/ips' require 'stringio' require_relative '../lib/tty-prompt' input = ::StringIO.new output = ::StringIO.new prompt = TTY::Prompt.new(input: input, output: output) Benchmark.ips do |r| r.report("Ruby #puts") do output.puts "What is your name?" end r.report("TTY::Prompt #ask") do prompt.ask("What is your name?") end end # Calculating ------------------------------------- # Ruby #puts 34601 i/100ms # TTY::Prompt #ask 12 i/100ms # ------------------------------------------------- # Ruby #puts 758640.5 (±14.9%) i/s - 3736908 in 5.028562s # TTY::Prompt #ask 63.1 (±7.9%) i/s - 324 in 5.176857s tty-prompt-0.23.1/examples/000077500000000000000000000000001403662044600155575ustar00rootroot00000000000000tty-prompt-0.23.1/examples/ask.rb000066400000000000000000000004471403662044600166670ustar00rootroot00000000000000# frozen_string_literal: true require "pastel" require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new pastel = Pastel.new notice = pastel.cyan.bold.detach prompt.ask("What is your name?", default: ENV["USER"], active_color: notice, help_color: notice) tty-prompt-0.23.1/examples/ask_blank.rb000066400000000000000000000002361403662044600200320ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new(prefix: ">") answer = prompt.ask puts "Answer: \"#{answer}\"" tty-prompt-0.23.1/examples/ask_multiline.rb000066400000000000000000000002301403662044600207370ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new prompt.ask("What\nis your\nname?", default: ENV["USER"]) tty-prompt-0.23.1/examples/ask_valid.rb000066400000000000000000000004611403662044600200420ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new prompt.ask("Folder name?") do |q| q.required(true) q.validate ->(v) { return !Dir.exist?(v) } q.messages[:valid?] = "Folder already exists?" q.messages[:required?] = "Folder name must not be empty" end tty-prompt-0.23.1/examples/collect.rb000066400000000000000000000006321403662044600175320ustar00rootroot00000000000000# frozen_string_literal: true require "json" require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new(prefix: "[?] ") result = prompt.collect do key(:name).ask("Name?") key(:age).ask("Age?", convert: :int) key(:address) do key(:street).ask("Street?", required: true) key(:city).ask("City?") key(:zip).ask("Zip?", validate: /\A\d{3}\Z/) end end puts JSON.pretty_generate(result) tty-prompt-0.23.1/examples/convert.rb000066400000000000000000000002631403662044600175650ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new answer = prompt.ask("Any digit:", convert: :float) puts "Digit: #{answer.inspect}" tty-prompt-0.23.1/examples/demo.rb000066400000000000000000000013151403662044600170300ustar00rootroot00000000000000# frozen_string_literal: true require "json" require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new envs = %w[local development test staging production] platforms = %w[Debian Ubuntu Fedora Windows macOS] puts TTY::Cursor.save result = prompt.collect do key(:username).ask("Username:") key(:password).mask("Password:") key(:env).select("Environment:", envs) key(:version).ask("Version (1-10)?", convert: :int, in: (1..10)) key(:verbose).yes?("Verbose?") key(:platforms).multi_select("Platforms?", platforms) key(:nodes).slider("Number of nodes?", max: 20, step: 1) end print TTY::Cursor.clear_screen_up print TTY::Cursor.restore + TTY::Cursor.show puts puts JSON.pretty_generate(result) puts tty-prompt-0.23.1/examples/echo.rb000066400000000000000000000003221403662044600170170ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new answer = prompt.ask("Password?", echo: false) do |q| q.validate(/^[^.]+\.[^.]+/) end puts "Password: #{answer}" tty-prompt-0.23.1/examples/enum_select.rb000066400000000000000000000003271403662044600204110ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new choices = %i[/bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny] prompt.enum_select("Select an editor", choices, default: 2) tty-prompt-0.23.1/examples/enum_select_disabled.rb000066400000000000000000000004631403662044600222410ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new choices = [ { name: "Emacs", disabled: "(not installed)" }, "Atom", "GNU nano", { name: "Notepad++", disabled: "(not installed)" }, "Sublime", "Vim" ] prompt.enum_select("Select an editor", choices) tty-prompt-0.23.1/examples/enum_select_paged.rb000066400000000000000000000003151403662044600215460ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new alfabet = ("A".."Z").to_a prompt.enum_select("Which letter?", alfabet, per_page: 4, cycle: true, default: 2) tty-prompt-0.23.1/examples/enum_select_wrapped.rb000066400000000000000000000017301403662044600221320ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new quotes = [ "There are certain queer times and occasions in this strange mixed affair we call life when a man takes this whole universe for a vast practical joke, though the wit thereof he but dimly discerns, and more than suspects that the joke is at nobody's expense but his own.", "Talk not to me of blasphemy, man;\n I'd strike the sun if it insulted me.", "There is a wisdom that is woe; but there is a woe that is madness. And there is a Catskill eagle in some souls that can alike dive down into the blackest gorges, and soar out of them again and become invisible in the sunny spaces. And even if he for ever flies within the gorge, that gorge is in the mountains; so that even in his lowest swoop the mountain eagle is still higher than other birds upon the plain, even though they soar." ] answer = prompt.enum_select("Chose your quote?", quotes) puts "Answer: #{answer}" tty-prompt-0.23.1/examples/expand.rb000066400000000000000000000007721403662044600173710ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" choices = [{ key: "y", name: "overwrite this file", value: :yes }, { key: "n", name: "do not overwrite this file", value: :no }, { key: "a", name: "overwrite this file and all later files", value: :all }, { key: "d", name: "show diff", value: :diff }, { key: "q", name: "quit; do not overwrite this file ", value: :quit }] prompt = TTY::Prompt.new prompt.expand("Overwrite Gemfile?", choices, default: 3) tty-prompt-0.23.1/examples/expand_auto.rb000066400000000000000000000007771403662044600204260ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" choices = [{ key: "y", name: "overwrite this file", value: :yes }, { key: "n", name: "do not overwrite this file", value: :no }, { key: "a", name: "overwrite this file and all later files", value: :all }, { key: "d", name: "show diff", value: :diff }, { key: "q", name: "quit; do not overwrite this file ", value: :quit }] prompt = TTY::Prompt.new prompt.expand("Overwrite Gemfile?", choices, auto_hint: true) tty-prompt-0.23.1/examples/in.rb000066400000000000000000000003231403662044600165100ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new prompt.ask("How do you like it on scale 1 - 10?", in: "1-10") do |q| q.messages[:range?] = "Sorry wrong one!" end tty-prompt-0.23.1/examples/inputs.rb000066400000000000000000000004301403662044600174230ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new prompt.ask("What is your name?", default: ENV["USER"]) prompt.yes?("Do you like Ruby?") prompt.mask("What is your secret?") prompt.select("Choose your destiny?", %w[Scorpion Kano Jax]) tty-prompt-0.23.1/examples/key_events.rb000066400000000000000000000004151403662044600202600ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new(interrupt: :exit) prompt.on(:keypress) do |event| puts "name: #{event.key.name}, value: #{event.value.dump}" end prompt.on(:keyescape) do exit end prompt.read_keypress tty-prompt-0.23.1/examples/keypress.rb000066400000000000000000000002671403662044600177560ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new answer = prompt.keypress("Press any key to continue") puts "Answer: #{answer.inspect}" tty-prompt-0.23.1/examples/mask.rb000066400000000000000000000004531403662044600170410ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" require "pastel" prompt = TTY::Prompt.new heart = prompt.decorate("#{prompt.symbols[:heart]} ", :magenta) res = prompt.mask("What is your secret?", mask: heart) do |q| q.validate(/[a-z\ ]{5,15}/) end puts "Secret: \"#{res}\"" tty-prompt-0.23.1/examples/menu.rb000066400000000000000000000020021403662044600170420ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" @prompt = TTY::Prompt.new(quiet: true) @cool = 0 def main_menu(_from = nil) name = @prompt.select("Cool Menu") do |menu| menu.enum "." menu.choice "Coolness Status", :status_menu menu.choice "Manage Coolness", :manage_menu menu.choice "Exit", "exit" end next_menu(name, :main_menu) end def next_menu(name, from) if name.is_a?(Symbol) send(name, from) else eval(name) end end def status_menu(from) name = @prompt.select("Coolness is at #{@cool}") do |menu| menu.enum "." menu.choice "Back", from menu.choice "Exit", "exit" end next_menu(name, :status_menu) end def manage_menu(_from) name = @prompt.select("Coolness is at #{@cool}.\nManage") do |menu| menu.enum "." menu.choice "Add coolness", :add_cool menu.choice "Back", :main_menu menu.choice "Exit", "exit" end next_menu(name, :manage_menu) end def add_cool(from) @cool += 1 next_menu(from, from) end main_menu tty-prompt-0.23.1/examples/multi_select.rb000066400000000000000000000003071403662044600205750ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new drinks = %w[vodka beer wine whisky bourbon] prompt.multi_select("Choose your favourite drink?", drinks) tty-prompt-0.23.1/examples/multi_select_disabled.rb000066400000000000000000000005231403662044600224240ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new drinks = [ "bourbon", { name: "sake", disabled: "(out of stock)" }, "vodka", { name: "beer", disabled: "(out of stock)" }, "wine", "whisky" ] answer = prompt.multi_select("Choose your favourite drink?", drinks) puts answer.inspect tty-prompt-0.23.1/examples/multi_select_disabled_paged.rb000066400000000000000000000006061403662044600235660ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new numbers = [ { name: "1", disabled: "out" }, "2", { name: "3", disabled: "out" }, "4", "5", { name: "6", disabled: "out" }, "7", "8", "9", { name: "10", disabled: "out" } ] answer = prompt.multi_select("Which letter?", numbers, per_page: 4, cycle: true) puts answer.inspect tty-prompt-0.23.1/examples/multi_select_paged.rb000066400000000000000000000002751403662044600217410ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new alfabet = ("A".."Z").to_a prompt.multi_select("Which letter?", alfabet, per_page: 7, max: 3) tty-prompt-0.23.1/examples/multi_select_wrapped.rb000066400000000000000000000017451403662044600223260ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new quotes = [ "There are certain queer times and occasions in this strange mixed affair we call life when a man takes this whole universe for a vast practical joke, though the wit thereof he but dimly discerns, and more than suspects that the joke is at nobody's expense but his own.", "Talk not to me of blasphemy, man; I'd strike the sun if it insulted me.", "There is a wisdom that is woe; but there is a woe that is madness. And there is a Catskill eagle in some souls that can alike dive down into the blackest gorges, and soar out of them again and become invisible in the sunny spaces. And even if he for ever flies within the gorge, that gorge is in the mountains; so that even in his lowest swoop the mountain eagle is still higher than other birds upon the plain, even though they soar." ] answer = prompt.multi_select("Choose your quote?", quotes, echo: false) puts "Answer: #{answer}" tty-prompt-0.23.1/examples/multiline.rb000066400000000000000000000002531403662044600201060ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new answer = prompt.multiline("Description:") puts "Answer: #{answer.inspect}" tty-prompt-0.23.1/examples/pause.rb000066400000000000000000000004161403662044600172220ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new answer = prompt.keypress("Press space or enter to continue, continuing automatically in :countdown ...", keys: %i[space return], timeout: 3) puts "Answer: #{answer.inspect}" tty-prompt-0.23.1/examples/select.rb000066400000000000000000000006221403662044600173630ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new warriors = %i[Scorpion Kano Jax Kitana Raiden] prompt.on(:keypress) do |event| prompt.trigger(:keydown) if event.value == "j" prompt.trigger(:keyup) if event.value == "k" end prompt.on(:keyescape) do |event| exit(1) end answer = prompt.select("Choose your destiny?", warriors) puts answer.inspect tty-prompt-0.23.1/examples/select_disabled.rb000066400000000000000000000004521403662044600212130ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new warriors = [ "Scorpion", "Kano", { name: "Goro", disabled: "(injury)" }, "Jax", "Kitana", "Raiden" ] answer = prompt.select("Choose your destiny?", warriors, enum: ")") puts answer.inspect tty-prompt-0.23.1/examples/select_disabled_paged.rb000066400000000000000000000006001403662044600223460ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new numbers = [ { name: "1", disabled: "out" }, "2", { name: "3", disabled: "out" }, "4", "5", { name: "6", disabled: "out" }, "7", "8", "9", { name: "10", disabled: "out" } ] answer = prompt.select("Which letter?", numbers, per_page: 4, cycle: true) puts answer.inspect tty-prompt-0.23.1/examples/select_enum.rb000066400000000000000000000002731403662044600204110ustar00rootroot00000000000000# frozen_string_litreal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new warriors = %w[Scorpion Kano Jax] prompt.select("Choose your destiny?", warriors, enum: ")") tty-prompt-0.23.1/examples/select_filtered.rb000066400000000000000000000003531403662044600212420ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new warriors = %w[Scorpion Kano Jax Kitana Raiden] answer = prompt.select("Choose your destiny?", warriors, filter: true) puts answer.inspect tty-prompt-0.23.1/examples/select_paginated.rb000066400000000000000000000003461403662044600214020ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new alfabet = ("A".."Z").to_a answer = prompt.select("Which letter?", alfabet, per_page: 7, cycle: true, default: 5) puts answer.inspect tty-prompt-0.23.1/examples/select_wrapped.rb000066400000000000000000000017231403662044600211100ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new quotes = [ "There are certain queer times and occasions in this strange mixed affair we call life when a man takes this whole universe for a vast practical joke, though the wit thereof he but dimly discerns, and more than suspects that the joke is at nobody's expense but his own.", "Talk not to me of blasphemy, man;\n I'd strike the sun if it insulted me.", "There is a wisdom that is woe; but there is a woe that is madness. And there is a Catskill eagle in some souls that can alike dive down into the blackest gorges, and soar out of them again and become invisible in the sunny spaces. And even if he for ever flies within the gorge, that gorge is in the mountains; so that even in his lowest swoop the mountain eagle is still higher than other birds upon the plain, even though they soar." ] answer = prompt.select("Chose your quote?", quotes) puts "Answer: #{answer}" tty-prompt-0.23.1/examples/slider.rb000066400000000000000000000002601403662044600173640ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new prompt.slider("Volume", max: 100, step: 5, default: 75, format: "|:slider| %d%%") tty-prompt-0.23.1/examples/validation.rb000066400000000000000000000002601403662044600202340ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new prompt.ask("What is your username?") do |q| q.validate(/\A[^.]+\.[^.]+\Z/) end tty-prompt-0.23.1/examples/yes_no.rb000066400000000000000000000002431403662044600173770ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../lib/tty-prompt" prompt = TTY::Prompt.new answer = prompt.yes?("Do you like Ruby?") puts "Answer: #{answer}" tty-prompt-0.23.1/lib/000077500000000000000000000000001403662044600145075ustar00rootroot00000000000000tty-prompt-0.23.1/lib/tty-prompt.rb000066400000000000000000000000361403662044600171720ustar00rootroot00000000000000require_relative "tty/prompt" tty-prompt-0.23.1/lib/tty/000077500000000000000000000000001403662044600153275ustar00rootroot00000000000000tty-prompt-0.23.1/lib/tty/prompt.rb000066400000000000000000000352421403662044600172030ustar00rootroot00000000000000# frozen_string_literal: true require "forwardable" require "pastel" require "tty-cursor" require "tty-reader" require "tty-screen" require_relative "prompt/answers_collector" require_relative "prompt/confirm_question" require_relative "prompt/errors" require_relative "prompt/expander" require_relative "prompt/enum_list" require_relative "prompt/keypress" require_relative "prompt/list" require_relative "prompt/multi_list" require_relative "prompt/multiline" require_relative "prompt/mask_question" require_relative "prompt/question" require_relative "prompt/slider" require_relative "prompt/statement" require_relative "prompt/suggestion" require_relative "prompt/symbols" require_relative "prompt/utils" require_relative "prompt/version" module TTY # A main entry for asking prompt questions. class Prompt extend Forwardable # @api private attr_reader :input # @api private attr_reader :output attr_reader :reader attr_reader :cursor # Prompt prefix # # @example # prompt = TTY::Prompt.new(prefix: [?]) # # @return [String] # # @api private attr_reader :prefix # Theme colors # # @api private attr_reader :active_color, :help_color, :error_color, :enabled_color # Quiet mode # # @api private attr_reader :quiet # The collection of display symbols # # @example # prompt = TTY::Prompt.new(symbols: {marker: ">"}) # # @return [Hash] # # @api private attr_reader :symbols def_delegators :@pastel, :strip def_delegators :@cursor, :clear_lines, :clear_line, :show, :hide def_delegators :@reader, :read_char, :read_keypress, :read_line, :read_multiline, :on, :subscribe, :unsubscribe, :trigger, :count_screen_lines def_delegators :@output, :print, :puts, :flush def self.messages { range?: "Value %{value} must be within the range %{in}", valid?: "Your answer is invalid (must match %{valid})", required?: "Value must be provided", convert?: "Cannot convert `%{value}` to '%{type}' type" } end # Initialize a Prompt # # @param [IO] :input # the input stream # @param [IO] :output # the output stream # @param [Hash] :env # the environment variables # @param [Hash] :symbols # the symbols displayed in prompts such as :marker, :cross # @param options [Boolean] :quiet # enable quiet mode, don't re-echo the question # @param [String] :prefix # the prompt prefix, by default empty # @param [Symbol] :interrupt # handling of Ctrl+C key out of :signal, :exit, :noop # @param [Boolean] :track_history # disable line history tracking, true by default # @param [Boolean] :enable_color # enable color support, true by default # @param [String,Proc] :active_color # the color used for selected option # @param [String,Proc] :help_color # the color used for help text # @param [String] :error_color # the color used for displaying error messages # # @api public def initialize(input: $stdin, output: $stdout, env: ENV, symbols: {}, prefix: "", interrupt: :error, track_history: true, quiet: false, enable_color: nil, active_color: :green, help_color: :bright_black, error_color: :red) @input = input @output = output @env = env @prefix = prefix @enabled_color = enable_color @active_color = active_color @help_color = help_color @error_color = error_color @interrupt = interrupt @track_history = track_history @symbols = Symbols.symbols.merge(symbols) @quiet = quiet @cursor = TTY::Cursor @pastel = enabled_color.nil? ? Pastel.new : Pastel.new(enabled: enabled_color) @reader = TTY::Reader.new( input: input, output: output, interrupt: interrupt, track_history: track_history, env: env ) end # Decorate a string with colors # # @param [String] :string # the string to color # @param [Array] :colors # collection of color symbols or callable object # # @api public def decorate(string, *colors) if Utils.blank?(string) || @enabled_color == false || colors.empty? return string end coloring = colors.first if coloring.respond_to?(:call) coloring.call(string) else @pastel.decorate(string, *colors) end end # Invoke a question type of prompt # # @example # prompt = TTY::Prompt.new # prompt.invoke_question(Question, "Your name? ") # # @return [String] # # @api public def invoke_question(object, message, **options, &block) options[:messages] = self.class.messages question = object.new(self, **options) question.(message, &block) end # Ask a question. # # @example # propmt = TTY::Prompt.new # prompt.ask("What is your name?") # # @param [String] message # the question to be asked # # @yieldparam [TTY::Prompt::Question] question # further configure the question # # @yield [question] # # @return [TTY::Prompt::Question] # # @api public def ask(message = "", **options, &block) invoke_question(Question, message, **options, &block) end # Ask a question with a keypress answer # # @see #ask # # @api public def keypress(message = "", **options, &block) invoke_question(Keypress, message, **options, &block) end # Ask a question with a multiline answer # # @example # prompt.multiline("Description?") # # @return [Array[String]] # # @api public def multiline(message = "", **options, &block) invoke_question(Multiline, message, **options, &block) end # Invoke a list type of prompt # # @example # prompt = TTY::Prompt.new # editors = %w(emacs nano vim) # prompt.invoke_select(EnumList, "Select editor: ", editors) # # @return [String] # # @api public def invoke_select(object, question, *args, &block) options = Utils.extract_options!(args) choices = if args.empty? && !block possible = options.dup options = {} possible elsif args.size == 1 && args[0].is_a?(Hash) Utils.extract_options!(args) else args.flatten end list = object.new(self, **options) list.(question, choices, &block) end # Ask masked question # # @example # propmt = TTY::Prompt.new # prompt.mask("What is your secret?") # # @return [TTY::Prompt::MaskQuestion] # # @api public def mask(message = "", **options, &block) invoke_question(MaskQuestion, message, **options, &block) end # Ask a question with a list of options # # @example # prompt = TTY::Prompt.new # prompt.select("What size?", %w(large medium small)) # # @example # prompt = TTY::Prompt.new # prompt.select("What size?") do |menu| # menu.choice :large # menu.choices %w(:medium :small) # end # # @param [String] question # the question to ask # # @param [Array[Object]] choices # the choices to select from # # @api public def select(question, *args, &block) invoke_select(List, question, *args, &block) end # Ask a question with multiple attributes activated # # @example # prompt = TTY::Prompt.new # choices = %w(Scorpion Jax Kitana Baraka Jade) # prompt.multi_select("Choose your destiny?", choices) # # @param [String] question # the question to ask # # @param [Array[Object]] choices # the choices to select from # # @return [String] # # @api public def multi_select(question, *args, &block) invoke_select(MultiList, question, *args, &block) end # Ask a question with indexed list # # @example # prompt = TTY::Prompt.new # editors = %w(emacs nano vim) # prompt.enum_select(EnumList, "Select editor: ", editors) # # @param [String] question # the question to ask # # @param [Array[Object]] choices # the choices to select from # # @return [String] # # @api public def enum_select(question, *args, &block) invoke_select(EnumList, question, *args, &block) end # A shortcut method to ask the user positive question and return # true for "yes" reply, false for "no". # # @example # prompt = TTY::Prompt.new # prompt.yes?("Are you human?") # # => Are you human? (Y/n) # # @return [Boolean] # # @api public def yes?(message, **options, &block) opts = { default: true }.merge(options) question = ConfirmQuestion.new(self, **opts) question.call(message, &block) end # A shortcut method to ask the user negative question and return # true for "no" reply. # # @example # prompt = TTY::Prompt.new # prompt.no?("Are you alien?") # => true # # => Are you human? (y/N) # # @return [Boolean] # # @api public def no?(message, **options, &block) opts = { default: false }.merge(options) question = ConfirmQuestion.new(self, **opts) !question.call(message, &block) end # Expand available options # # @example # prompt = TTY::Prompt.new # choices = [{ # key: "Y", # name: "Overwrite", # value: :yes # }, { # key: "n", # name: "Skip", # value: :no # }] # prompt.expand("Overwirte Gemfile?", choices) # # @return [Object] # the user specified value # # @api public def expand(message, *args, &block) invoke_select(Expander, message, *args, &block) end # Ask a question with a range slider # # @example # prompt = TTY::Prompt.new # prompt.slider("What size?", min: 32, max: 54, step: 2) # prompt.slider("What size?", [ 'xs', 's', 'm', 'l', 'xl' ]) # # @param [String] question # the question to ask # # @param [Array] choices # the choices to display # # @return [String] # # @api public def slider(question, choices = nil, **options, &block) slider = Slider.new(self, **options) slider.call(question, choices, &block) end # Print statement out. If the supplied message ends with a space or # tab character, a new line will not be appended. # # @example # say("Simple things.", color: :red) # # @param [String] message # # @return [String] # # @api public def say(message = "", **options) message = message.to_s return if message.empty? statement = Statement.new(self, **options) statement.call(message) end # Print statement(s) out in red green. # # @example # prompt.ok "Are you sure?" # prompt.ok "All is fine!", "This is fine too." # # @param [Array] messages # # @return [Array] messages # # @api public def ok(*args, **options) opts = { color: :green }.merge(options) args.each { |message| say(message, **opts) } end # Print statement(s) out in yellow color. # # @example # prompt.warn "This action can have dire consequences" # prompt.warn "Carefull young apprentice", "This is potentially dangerous" # # @param [Array] messages # # @return [Array] messages # # @api public def warn(*args, **options) opts = { color: :yellow }.merge(options) args.each { |message| say(message, **opts) } end # Print statement(s) out in red color. # # @example # prompt.error "Shutting down all systems!" # prompt.error "Nothing is fine!", "All is broken!" # # @param [Array] messages # # @return [Array] messages # # @api public def error(*args, **options) opts = { color: :red }.merge(options) args.each { |message| say(message, **opts) } end # Print debug information in terminal top right corner # # @example # prompt.debug "info1", "info2" # # @param [Array] messages # # @retrun [nil] # # @api public def debug(*messages) longest = messages.max_by(&:length).size width = TTY::Screen.width - longest print cursor.save messages.reverse_each do |msg| print cursor.column(width) + cursor.up + cursor.clear_line_after print msg end ensure print cursor.restore end # Takes the string provided by the user and compare it with other possible # matches to suggest an unambigous string # # @example # prompt.suggest("sta", ["status", "stage", "commit", "branch"]) # # => "status, stage" # # @param [String] message # # @param [Array] possibilities # # @param [Hash] options # @option options [String] :indent # The number of spaces for indentation # @option options [String] :single_text # The text for a single suggestion # @option options [String] :plural_text # The text for multiple suggestions # # @return [String] # # @api public def suggest(message, possibilities, **options) suggestion = Suggestion.new(**options) say(suggestion.suggest(message, possibilities)) end # Gathers more than one aswer # # @example # prompt.collect do # key(:name).ask("Name?") # end # # @return [Hash] # the collection of answers # # @api public def collect(**options, &block) collector = AnswersCollector.new(self, **options) collector.call(&block) end # Check if outputing to terminal # # @return [Boolean] # # @api public def tty? stdout.tty? end # Return standard in # # @api private def stdin $stdin end # Return standard out # # @api private def stdout $stdout end # Return standard error # # @api private def stderr $stderr end # Inspect this instance public attributes # # @return [String] # # @api public def inspect attributes = [ :prefix, :quiet, :enabled_color, :active_color, :error_color, :help_color, :input, :output, ] name = self.class.name "#<#{name}#{attributes.map { |attr| " #{attr}=#{send(attr).inspect}" }.join}>" end end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/000077500000000000000000000000001403662044600166505ustar00rootroot00000000000000tty-prompt-0.23.1/lib/tty/prompt/answers_collector.rb000066400000000000000000000032101403662044600227210ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt class AnswersCollector # Initialize answer collector # # @api public def initialize(prompt, **options) @prompt = prompt @answers = options.fetch(:answers) { {} } end # Start gathering answers # # @return [Hash] # the collection of all answers # # @api public def call(&block) instance_eval(&block) @answers end # Create answer entry # # @example # key(:name).ask("Name?") # # @api public def key(name, &block) @name = name if block answer = create_collector.call(&block) add_answer(answer) end self end # Change to collect all values for a key # # @example # key(:colors).values.ask("Color?") # # @api public def values(&block) @answers[@name] = Array(@answers[@name]) if block answer = create_collector.call(&block) add_answer(answer) end self end # @api public def create_collector self.class.new(@prompt) end # @api public def add_answer(answer) if @answers[@name].is_a?(Array) @answers[@name] << answer else @answers[@name] = answer end end private # @api private def method_missing(method, *args, **options, &block) answer = @prompt.public_send(method, *args, **options, &block) add_answer(answer) end end # AnswersCollector end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/block_paginator.rb000066400000000000000000000033671403662044600223440ustar00rootroot00000000000000# frozen_string_literal: true require_relative "paginator" module TTY class Prompt class BlockPaginator < Paginator # Paginate list of choices based on current active choice. # Move entire pages. # # @api public def paginate(list, active, per_page = nil, &block) default_size = (list.size <= DEFAULT_PAGE_SIZE ? list.size : DEFAULT_PAGE_SIZE) @per_page = @per_page || per_page || default_size check_page_size! # Don't paginate short lists if list.size <= @per_page @start_index = 0 @end_index = list.size - 1 if block return list.each_with_index(&block) else return list.each_with_index.to_enum end end unless active.nil? # User may input index out of range @last_index = active end page = (@last_index / @per_page.to_f).ceil pages = (list.size / @per_page.to_f).ceil if page == 0 @start_index = 0 @end_index = @start_index + @per_page - 1 elsif page > 0 && page < pages @start_index = (page - 1) * @per_page @end_index = @start_index + @per_page - 1 elsif page == pages @start_index = (page - 1) * @per_page @end_index = list.size - 1 else @end_index = list.size - 1 @start_index = @end_index - @per_page + 1 end sliced_list = list[@start_index..@end_index] page_range = (@start_index..@end_index) return sliced_list.zip(page_range).to_enum unless block_given? sliced_list.each_with_index do |item, index| block[item, @start_index + index] end end end # EnumPaginator end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/choice.rb000066400000000000000000000062531403662044600204350ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt # An immutable representation of a single choice option from select menu # # @api public class Choice # Create choice from value # # @examples # Choice.from(:foo) # # => # # Choice.from([:foo, 1]) # # => # # Choice.from({name: :foo, value: 1, key: "f"} # # => # # @param [Object] val # the value to be converted # # @raise [ArgumentError] # # @return [Choice] # # @api public def self.from(val) case val when Choice val when Array convert_array(val) when Hash convert_hash(val) else new(val, val) end end # Convert an array into choice # # @param [Array] # # @return [Choice] # # @api public def self.convert_array(val) name, value, options = *val if name.is_a?(Hash) convert_hash(name) elsif val.size == 1 new(name.to_s, name.to_s) else new(name.to_s, value, **(options || {})) end end # Convert a hash into choice # # @param [Hash] # # @return [Choice] # # @api public def self.convert_hash(val) if val.key?(:name) && val.key?(:value) new(val[:name].to_s, val[:value], **val) elsif val.key?(:name) new(val[:name].to_s, val[:name].to_s, **val) else new(val.keys.first.to_s, val.values.first) end end # The label name # # @api public attr_reader :name # The keyboard key to activate this choice # # @api public attr_reader :key # The text to display for disabled choice # # @api public attr_reader :disabled # Create a Choice instance # # @api public def initialize(name, value, **options) @name = name @value = value @key = options[:key] @disabled = options[:disabled].nil? ? false : options[:disabled] freeze end # Check if this choice is disabled # # @return [Boolean] # # @api public def disabled? !!@disabled end # Read value and evaluate # # @api public def value case @value when Proc @value.call else @value end end # Object equality comparison # # @return [Boolean] # # @api public def ==(other) return false unless other.is_a?(self.class) name == other.name && value == other.value && key == other.key end # Object string representation # # @return [String] # # @api public def to_s name.to_s end end # Choice end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/choices.rb000066400000000000000000000051401403662044600206120ustar00rootroot00000000000000# frozen_string_literal: true require "forwardable" require_relative "choice" module TTY class Prompt # A class responsible for storing a collection of choices # # @api private class Choices include Enumerable extend Forwardable def_delegators :choices, :length, :size, :to_ary, :empty?, :values_at, :index, :== # Convenience for creating choices # # @param [Array[Object]] choices # the choice objects # # @return [Choices] # the choices collection # # @api public def self.[](*choices) new(choices) end # Create Choices collection # # @param [Array[Choice]] choices # the choices to add to collection # # @api public def initialize(choices = []) @choices = choices.map do |choice| Choice.from(choice) end end # Scope of choices which are not disabled # # @api public def enabled reject(&:disabled?) end def enabled_indexes each_with_index.reduce([]) do |acc, (choice, idx)| acc << idx unless choice.disabled? acc end end # Iterate over all choices in the collection # # @yield [Choice] # # @api public def each(&block) return to_enum unless block_given? choices.each(&block) end # Add choice to collection # # @param [Object] choice # the choice to add # # @api public def <<(choice) choices << Choice.from(choice) end # Access choice by index # # @param [Integer] index # # @return [Choice] # # @api public def [](index) @choices[index] end # Pluck a choice by its name from collection # # @param [String] name # the label name for the choice # # @return [Choice] # # @api public def pluck(name) map { |choice| choice.public_send(name) } end # Find a matching choice # # @example # choices.find_by(:name, "small") # # @param [Symbol] attr # the attribute name # @param [Object] value # # @return [Choice] # # @api public def find_by(attr, value) find { |choice| choice.public_send(attr) == value } end protected # The actual collection choices # # @return [Array[Choice]] # # @api private attr_reader :choices end # Choices end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/confirm_question.rb000066400000000000000000000075571403662044600225770ustar00rootroot00000000000000# frozen_string_literal: true require_relative "question" require_relative "utils" module TTY class Prompt class ConfirmQuestion < Question # Create confirmation question # # @param [Hash] options # @option options [String] :suffix # @option options [String] :positive # @option options [String] :negative # # @api public def initialize(prompt, **options) super @suffix = options.fetch(:suffix) { UndefinedSetting } @positive = options.fetch(:positive) { UndefinedSetting } @negative = options.fetch(:negative) { UndefinedSetting } end def positive? @positive != UndefinedSetting end def negative? @negative != UndefinedSetting end def suffix? @suffix != UndefinedSetting end # Set question suffix # # @api public def suffix(value = (not_set = true)) return @negative if not_set @suffix = value end # Set value for matching positive choice # # @api public def positive(value = (not_set = true)) return @positive if not_set @positive = value end # Set value for matching negative choice # # @api public def negative(value = (not_set = true)) return @negative if not_set @negative = value end def call(message, &block) return if Utils.blank?(message) @message = message block.call(self) if block setup_defaults render end # Render confirmation question # # @return [String] # # @api private def render_question header = "#{@prefix}#{message} " if !@done header += @prompt.decorate("(#{@suffix})", @help_color) + " " else answer = conversion.call(@input) label = answer ? @positive : @negative header += @prompt.decorate(label, @active_color) end header << "\n" if @done header end protected # Decide how to handle input from user # # @api private def process_input(question) @input = read_input(question) if Utils.blank?(@input) @input = default ? positive : negative end @evaluator.call(@input) end # @api private def setup_defaults infer_default @convert = conversion return if suffix? && positive? if suffix? && (!positive? || !negative?) parts = @suffix.split("/") @positive = parts[0] @negative = parts[1] elsif !suffix? && positive? @suffix = create_suffix else create_default_labels end end # @api private def infer_default converted = Converters.convert(:bool, default.to_s) if converted == Const::Undefined raise InvalidArgument, "default needs to be `true` or `false`" else default(converted) end end # @api private def create_default_labels @suffix = default ? "Y/n" : "y/N" @positive = default ? "Yes" : "yes" @negative = default ? "no" : "No" @validation = /^(y(es)?|no?)$/i @messages[:valid?] = "Invalid input." end # @api private def create_suffix (default ? positive.capitalize : positive.downcase) + "/" + (default ? negative.downcase : negative.capitalize) end # Create custom conversion # # @api private def conversion ->(input) do positive_word = Regexp.escape(positive) positive_letter = Regexp.escape(positive[0]) pattern = Regexp.new("^(#{positive_word}|#{positive_letter})$", true) !input.match(pattern).nil? end end end # ConfirmQuestion end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/const.rb000066400000000000000000000004411403662044600203220ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt module Const Undefined = Object.new.tap do |obj| def obj.to_s "undefined" end def obj.inspect "undefined".inspect end end end # Const end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/converter_dsl.rb000066400000000000000000000007011403662044600220440ustar00rootroot00000000000000# frozen_string_literal: true require_relative "converter_registry" module TTY class Prompt module ConverterDSL def converter_registry @__converter_registry ||= ConverterRegistry.new end def converter(*names, &block) converter_registry.register(*names, &block) end def convert(name, input) converter_registry[name].call(input) end end # ConverterDSL end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/converter_registry.rb000066400000000000000000000030021403662044600231270ustar00rootroot00000000000000# frozen_string_literal: true require "forwardable" module TTY class Prompt # Immutable collection of converters for type transformation # # @api private class ConverterRegistry extend Forwardable def_delegators "@__registry", :keys # Create a registry of conversions # # @param [Hash] registry # # @api private def initialize(registry = {}) @__registry = registry.dup end # Check if conversion is available # # @param [String] name # # @return [Boolean] # # @api public def contain?(name) conv_name = name.to_s.downcase.to_sym @__registry.key?(conv_name) end # Register a conversion # # @param [Symbol] name # the converter name # # @api public def register(*names, &block) names.each do |name| if contain?(name) raise ConversionAlreadyDefined, "converter for #{name.inspect} is already registered" end @__registry[name] = block end end # Execute converter # # @api public def [](name) conv_name = name.to_s.downcase.to_sym @__registry.fetch(conv_name) do raise UnsupportedConversion, "converter #{conv_name.inspect} is not registered" end end alias fetch [] def inspect @_registry.inspect end end # ConverterRegistry end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/converters.rb000066400000000000000000000117451403662044600213770ustar00rootroot00000000000000# frozen_string_literal: true require_relative "const" require_relative "converter_dsl" module TTY class Prompt module Converters extend ConverterDSL TRUE_VALUES = /^(t(rue)?|y(es)?|on|1)$/i.freeze FALSE_VALUES = /^(f(alse)?|n(o)?|off|0)$/i.freeze SINGLE_DIGIT_MATCHER = /^(?\-?\d+(\.\d+)?)$/.freeze DIGIT_MATCHER = /^(?-?\d+(\.\d+)?) \s*(?(\.\s*){2,3}|-|,)\s* (?-?\d+(\.\d+)?)$ /x.freeze LETTER_MATCHER = /^(?\w) \s*(?(\.\s*){2,3}|-|,)\s* (?\w)$ /x.freeze converter(:boolean, :bool) do |input| case input.to_s when TRUE_VALUES then true when FALSE_VALUES then false else Const::Undefined end end converter(:string, :str) do |input| String(input).chomp end converter(:symbol, :sym) do |input| input.to_sym end converter(:char) do |input| String(input).chars.to_a[0] end converter(:date) do |input| begin require "date" unless defined?(::Date) ::Date.parse(input) rescue ArgumentError Const::Undefined end end converter(:datetime) do |input| begin require "date" unless defined?(::Date) ::DateTime.parse(input.to_s) rescue ArgumentError Const::Undefined end end converter(:time) do |input| begin require "time" ::Time.parse(input.to_s) rescue ArgumentError Const::Undefined end end converter(:integer, :int) do |input| begin Integer(input) rescue ArgumentError Const::Undefined end end converter(:float) do |input| begin Float(input) rescue TypeError, ArgumentError Const::Undefined end end # Convert string number to integer or float # # @return [Integer,Float,Const::Undefined] # # @api private def cast_to_num(num) ([convert(:int, num), convert(:float, num)] - [Const::Undefined]).first || Const::Undefined end module_function :cast_to_num converter(:range) do |input| if input.is_a?(::Range) input elsif match = input.to_s.match(SINGLE_DIGIT_MATCHER) digit = cast_to_num(match[:digit]) ::Range.new(digit, digit) elsif match = input.to_s.match(DIGIT_MATCHER) open = cast_to_num(match[:open]) close = cast_to_num(match[:close]) ::Range.new(open, close, match[:sep].gsub(/\s*/, "") == "...") elsif match = input.to_s.match(LETTER_MATCHER) ::Range.new(match[:open], match[:close], match[:sep].gsub(/\s*/, "") == "...") else Const::Undefined end end converter(:regexp) do |input| Regexp.new(input) end converter(:filepath, :file) do |input| ::File.expand_path(input) end converter(:pathname, :path) do |input| require "pathname" unless defined?(::Pathname) ::Pathname.new(input) end converter(:uri) do |input| require "uri" unless defined?(::URI) ::URI.parse(input) end converter(:list, :array) do |val| (val.respond_to?(:to_a) ? val : val.split(/(? 1 && second_index > 1 first_previous_char = first[first_index - 2] second_previous_char = second[second_index - 2] if first_char == second_previous_char && second_char == first_previous_char distances[first_index][second_index] = [ distances[first_index][second_index], distances[first_index - 2][second_index - 2] + 1 # transposition ].min end end end end distances[rows][cols] end end # Distance end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/enum_list.rb000066400000000000000000000257661403662044600212140ustar00rootroot00000000000000# frozen_string_literal: true require "English" require_relative "choices" require_relative "block_paginator" require_relative "paginator" module TTY class Prompt # A class reponsible for rendering enumerated list menu. # Used by {Prompt} to display static choice menu. # # @api private class EnumList PAGE_HELP = "(Press tab/right or left to reveal more choices)" # Checks type of default parameter to be integer INTEGER_MATCHER = /\A[-+]?\d+\Z/.freeze # Create instance of EnumList menu. # # @api public def initialize(prompt, **options) @prompt = prompt @prefix = options.fetch(:prefix) { @prompt.prefix } @enum = options.fetch(:enum) { ")" } @default = options.fetch(:default, nil) @active_color = options.fetch(:active_color) { @prompt.active_color } @help_color = options.fetch(:help_color) { @prompt.help_color } @error_color = options.fetch(:error_color) { @prompt.error_color } @cycle = options.fetch(:cycle, false) @quiet = options.fetch(:quiet) { @prompt.quiet } @symbols = @prompt.symbols.merge(options.fetch(:symbols, {})) @input = nil @done = false @first_render = true @failure = false @active = @default @choices = Choices.new @per_page = options[:per_page] @page_help = options[:page_help] || PAGE_HELP @paginator = BlockPaginator.new @page_active = @default end # Change symbols used by this prompt # # @param [Hash] new_symbols # the new symbols to use # # @api public def symbols(new_symbols = (not_set = true)) return @symbols if not_set @symbols.merge!(new_symbols) end # Set default option selected # # @api public def default(default) @default = default end # Check if default value is set # # @return [Boolean] # # @api public def default? !@default.to_s.empty? end # Set number of items per page # # @api public def per_page(value) @per_page = value end def page_size (@per_page || Paginator::DEFAULT_PAGE_SIZE) end # Check if list is paginated # # @return [Boolean] # # @api private def paginated? @choices.size > page_size end # @param [String] text # the help text to display per page # @api pbulic def page_help(text) @page_help = text end # Set selecting active index using number pad # # @api public def enum(value) @enum = value end # Set quiet mode # # @api public def quiet(value) @quiet = value end # Add a single choice # # @api public def choice(*value, &block) if block @choices << (value << block) else @choices << value end end # Add multiple choices # # @param [Array[Object]] values # the values to add as choices # # @api public def choices(values = (not_set = true)) if not_set @choices else values.each { |val| @choices << val } end end # Call the list menu by passing question and choices # # @param [String] question # # @param # @api public def call(question, possibilities, &block) choices(possibilities) @question = question block[self] if block setup_defaults @prompt.subscribe(self) do render end end def keypress(event) if %i[backspace delete].include?(event.key.name) return if @input.empty? @input.chop! mark_choice_as_active elsif event.value =~ /^\d+$/ @input += event.value mark_choice_as_active end end def keyreturn(*) @failure = false num = @input.to_i choice_disabled = choices[num - 1] && choices[num - 1].disabled? choice_in_range = num > 0 && num <= @choices.size if choice_in_range && !choice_disabled || @input.empty? @done = true else @input = "" @failure = true end end alias keyenter keyreturn def keyright(*) if (@page_active + page_size) <= @choices.size @page_active += page_size elsif @cycle @page_active = 1 end end alias keytab keyright def keyleft(*) if (@page_active - page_size) >= 0 @page_active -= page_size elsif @cycle @page_active = @choices.size - 1 end end private # Find active choice or set to default # # @return [nil] # # @api private def mark_choice_as_active next_active = @choices[@input.to_i - 1] if next_active && next_active.disabled? # noop elsif (@input.to_i > 0) && next_active @active = @input.to_i else @active = @default end @page_active = @active end # Validate default indexes to be within range # # @api private def validate_defaults msg = if @default.nil? || @default.to_s.empty? "default index must be an integer in range (1 - #{choices.size})" elsif @default.to_s !~ INTEGER_MATCHER validate_default_name elsif @default < 1 || @default > @choices.size "default index #{@default} out of range (1 - #{@choices.size})" elsif choices[@default - 1] && choices[@default - 1].disabled? "default index #{@default} matches disabled choice item" end raise(ConfigurationError, msg) if msg end # Validate default choice name # # @return [String] # # @api private def validate_default_name default_choice = choices.find_by(:name, @default.to_s) if default_choice.nil? "no choice found for the default name: #{@default.inspect}" elsif default_choice.disabled? "default name #{@default.inspect} matches disabled choice" end end # Setup default option and active selection # # @api private def setup_defaults if @default.to_s.empty? @default = (0..choices.length).find { |i| !choices[i].disabled? } + 1 end validate_defaults if default_choice = choices.find_by(:name, @default) @default = choices.index(default_choice) + 1 end mark_choice_as_active end # Render a selection list. # # By default the result is printed out. # # @return [Object] value # return the selected value # # @api private def render @input = "" until @done question = render_question @prompt.print(question) @prompt.print(render_error) if @failure if paginated? && !@done @prompt.print(render_page_help) end @prompt.read_keypress question_lines = question.split($INPUT_RECORD_SEPARATOR, -1) @prompt.print(refresh(question_lines_count(question_lines))) end @prompt.print(render_question) unless @quiet answer end # Count how many screen lines the question spans # # @return [Integer] # # @api private def question_lines_count(question_lines) question_lines.reduce(0) do |acc, line| acc + @prompt.count_screen_lines(line) end end # Find value for the choice selected # # @return [nil, Object] # # @api private def answer @choices[@active - 1].value end # Determine area of the screen to clear # # @param [Integer] lines # the lines to clear # # @return [String] # # @api private def refresh(lines) @prompt.clear_lines(lines) + @prompt.cursor.clear_screen_down end # Render question with the menu options # # @return [String] # # @api private def render_question header = ["#{@prefix}#{@question} #{render_header}\n"] unless @done header << render_menu header << render_footer end header.join end # Error message when incorrect index chosen # # @api private def error_message error = "Please enter a valid number" "\n" + @prompt.decorate(">>", @error_color) + " " + error end # Render error message and return cursor to position of input # # @return [String] # # @api private def render_error error = error_message.dup if !paginated? error << @prompt.cursor.prev_line error << @prompt.cursor.forward(render_footer.size) end error end # Render chosen option # # @return [String] # # @api private def render_header return "" unless @done return "" unless @active selected_item = @choices[@active - 1].name.to_s @prompt.decorate(selected_item, @active_color) end # Render footer for the indexed menu # # @return [String] # # @api private def render_footer " Choose 1-#{@choices.size} [#{@default}]: #{@input}" end # Pagination help message # # @return [String] # # @api private def page_help_message return "" unless paginated? "\n" + @prompt.decorate(@page_help, @help_color) end # Render page help # # @return [String] # # @api private def render_page_help help = page_help_message.dup if @failure help << @prompt.cursor.prev_line end help << @prompt.cursor.prev_line help << @prompt.cursor.forward(render_footer.size) end # Render menu with indexed choices to select from # # @return [String] # # @api private def render_menu output = [] @paginator.paginate(@choices, @page_active, @per_page) do |choice, index| num = (index + 1).to_s + @enum + " " selected = num.to_s + choice.name.to_s output << if index + 1 == @active && !choice.disabled? (" " * 2) + @prompt.decorate(selected, @active_color) elsif choice.disabled? @prompt.decorate(@symbols[:cross], :red) + " " + selected + " " + choice.disabled.to_s else (" " * 2) + selected end output << "\n" end output.join end end # EnumList end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/errors.rb000066400000000000000000000016131403662044600205120ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt Error = Class.new(StandardError) # Raised when wrong parameter is used to configure prompt ConfigurationError = Class.new(Error) # Raised when type conversion cannot be performed ConversionError = Class.new(Error) # Raised when the passed in validation argument is of wrong type ValidationCoercion = Class.new(Error) # Raised when the required argument is not supplied ArgumentRequired = Class.new(Error) # Raised when the argument validation fails ArgumentValidation = Class.new(Error) # Raised when the argument is not expected InvalidArgument = Class.new(Error) # Raised when overriding already defined conversion ConversionAlreadyDefined = Class.new(Error) # Raised when conversion type isn't registered UnsupportedConversion = Class.new(Error) end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/evaluator.rb000066400000000000000000000011701403662044600211760ustar00rootroot00000000000000# frozen_string_literal: true require_relative "result" module TTY class Prompt # Evaluates provided parameters and stops if any of them fails # @api private class Evaluator attr_reader :results def initialize(question, &block) @question = question @results = [] instance_eval(&block) if block end def call(initial) seed = Result::Success.new(@question, initial) results.reduce(seed, &:with) end def check(proc = nil, &block) results << (proc || block) end alias << check end # Evaluator end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/expander.rb000066400000000000000000000172251403662044600210120ustar00rootroot00000000000000# frozen_string_literal: true require_relative "choices" module TTY class Prompt # A class responsible for rendering expanding options # Used by {Prompt} to display key options question. # # @api private class Expander HELP_CHOICE = { key: "h", name: "print help", value: :help }.freeze # Names for delete keys DELETE_KEYS = %i[backspace delete].freeze # Create instance of Expander # # @api public def initialize(prompt, options = {}) @prompt = prompt @prefix = options.fetch(:prefix) { @prompt.prefix } @default = options.fetch(:default, 1) @auto_hint = options.fetch(:auto_hint, false) @active_color = options.fetch(:active_color) { @prompt.active_color } @help_color = options.fetch(:help_color) { @prompt.help_color } @quiet = options.fetch(:quiet) { @prompt.quiet } @choices = Choices.new @selected = nil @done = false @status = :collapsed @hint = nil @default_key = false end def expanded? @status == :expanded end def collapsed? @status == :collapsed end def expand @status = :expanded end # Respond to submit event # # @api public def keyenter(_) if @input.nil? || @input.empty? @input = @choices[@default - 1].key @default_key = true end selected = select_choice(@input) if selected && selected.key.to_s == "h" expand @selected = nil @input = "" elsif selected @done = true @selected = selected @hint = nil else @input = "" end end alias keyreturn keyenter # Respond to key press event # # @api public def keypress(event) if DELETE_KEYS.include?(event.key.name) @input.chop! unless @input.empty? elsif event.value =~ /^[^\e\n\r]/ @input += event.value end @selected = select_choice(@input) if @selected && !@default_key && collapsed? @hint = @selected.name end end # Select choice by given key # # @return [Choice] # # @api private def select_choice(key) @choices.find_by(:key, key) end # Set default value. # # @api public def default(value = (not_set = true)) return @default if not_set @default = value end # Set quiet mode. # # @api public def quiet(value) @quiet = value end # Add a single choice # # @api public def choice(value, &block) if block @choices << value.update(value: block) else @choices << value end end # Add multiple choices # # @param [Array[Object]] values # the values to add as choices # # @api public def choices(values) values.each { |val| choice(val) } end # Execute this prompt # # @api public def call(message, possibilities, &block) choices(possibilities) @message = message block.call(self) if block setup_defaults choice(HELP_CHOICE) @prompt.subscribe(self) do render end end private # Create possible keys with current choice highlighted # # @return [String] # # @api private def possible_keys keys = @choices.pluck(:key) default_key = keys[@default - 1] if @selected index = keys.index(@selected.key) keys[index] = @prompt.decorate(keys[index], @active_color) elsif @input.to_s.empty? && default_key keys[@default - 1] = @prompt.decorate(default_key, @active_color) end keys.join(",") end # @api private def render @input = "" until @done question = render_question @prompt.print(question) read_input @prompt.print(refresh(question.lines.count)) end @prompt.print(render_question) unless @quiet answer end # @api private def answer @selected.value end # Render message with options # # @return [String] # # @api private def render_header header = ["#{@prefix}#{@message} "] if @done selected_item = @selected.name.to_s header << @prompt.decorate(selected_item, @active_color) elsif collapsed? header << %[(enter "h" for help) ] header << "[#{possible_keys}] " header << @input end header.join end # Show hint for selected option key # # return [String] # # @api private def render_hint "\n" + @prompt.decorate(">> ", @active_color) + @hint + @prompt.cursor.prev_line + @prompt.cursor.forward(@prompt.strip(render_header).size) end # Render question with menu # # @return [String] # # @api private def render_question load_auto_hint if @auto_hint header = render_header header << render_hint if @hint header << "\n" if @done if !@done && expanded? header << render_menu header << render_footer end header end def load_auto_hint if @hint.nil? && collapsed? if @selected @hint = @selected.name else if @input.empty? @hint = @choices[@default - 1].name else @hint = "invalid option" end end end end def render_footer " Choice [#{@choices[@default - 1].key}]: #{@input}" end def read_input @prompt.read_keypress end # Refresh the current input # # @param [Integer] lines # # @return [String] # # @api private def refresh(lines) if (@hint && (!@selected || @done)) || (@auto_hint && collapsed?) @hint = nil @prompt.clear_lines(lines, :down) + @prompt.cursor.prev_line elsif expanded? @prompt.clear_lines(lines) else @prompt.clear_line end end # Render help menu # # @api private def render_menu output = ["\n"] @choices.each do |choice| chosen = %(#{choice.key} - #{choice.name}) if @selected && @selected.key == choice.key chosen = @prompt.decorate(chosen, @active_color) end output << " " + chosen + "\n" end output.join end def setup_defaults validate_choices end def validate_choices errors = [] keys = [] @choices.each do |choice| if choice.key.nil? errors << "Choice #{choice.name} is missing a :key attribute" next end if choice.key.length != 1 errors << "Choice key `#{choice.key}` is more than one character long." end if choice.key.to_s == "h" errors << "Choice key `#{choice.key}` is reserved for help menu." end if keys.include?(choice.key) errors << "Choice key `#{choice.key}` is a duplicate." end keys << choice.key if choice.key end errors.each { |err| raise ConfigurationError, err } end end # Expander end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/keypress.rb000066400000000000000000000045621403662044600210510ustar00rootroot00000000000000# frozen_string_literal: true require_relative "question" require_relative "timer" module TTY class Prompt class Keypress < Question # Create keypress question # # @param [Prompt] prompt # @param [Hash] options # # @api public def initialize(prompt, **options) super @echo = options.fetch(:echo) { false } @keys = options.fetch(:keys) { UndefinedSetting } @timeout = options.fetch(:timeout) { UndefinedSetting } @interval = options.fetch(:interval) { (@timeout != UndefinedSetting && @timeout < 1) ? @timeout : 1 } @decimals = (@interval.to_s.split(".")[1] || []).size @countdown = @timeout time = timeout? ? Float(@timeout) : nil @timer = Timer.new(time, Float(@interval)) @prompt.subscribe(self) end def countdown(value = (not_set = true)) return @countdown if not_set @countdown = value end # Check if any specific keys are set def any_key? @keys == UndefinedSetting end # Check if timeout is set def timeout? @timeout != UndefinedSetting end def keypress(event) if any_key? @done = true elsif @keys.is_a?(Array) && @keys.include?(event.key.name) @done = true else @done = false end end def render_question header = super if timeout? header.gsub!(/:countdown/, format("%.#{@decimals}f", countdown)) end header end def interval_handler(time) return if @done question = render_question line_size = question.size total_lines = @prompt.count_screen_lines(line_size) @prompt.print(refresh(question.lines.count, total_lines)) countdown(time) @prompt.print(render_question) end def process_input(question) @prompt.print(render_question) @timer.on_tick do |time| interval_handler(time) end @timer.while_remaining do |remaining| break if @done @input = @prompt.read_keypress(nonblock: true) end countdown(0) unless @done @evaluator.(@input) end def refresh(lines, lines_to_clear) @prompt.clear_lines(lines) end end # Keypress end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/list.rb000066400000000000000000000374161403662044600201630ustar00rootroot00000000000000# frozen_string_literal: true require "English" require_relative "choices" require_relative "paginator" require_relative "block_paginator" module TTY class Prompt # A class responsible for rendering select list menu # Used by {Prompt} to display interactive menu. # # @api private class List # Allowed keys for filter, along with backspace and canc. FILTER_KEYS_MATCHER = /\A([[:alnum:]]|[[:punct:]])\Z/.freeze # Checks type of default parameter to be integer INTEGER_MATCHER = /\A\d+\Z/.freeze # Create instance of TTY::Prompt::List menu. # # @param Hash options # the configuration options # @option options [Symbol] :default # the default active choice, defaults to 1 # @option options [Symbol] :color # the color for the selected item, defualts to :green # @option options [Symbol] :marker # the marker for the selected item # @option options [String] :enum # the delimiter for the item index # # @api public def initialize(prompt, **options) check_options_consistency(options) @prompt = prompt @prefix = options.fetch(:prefix) { @prompt.prefix } @enum = options.fetch(:enum) { nil } @default = Array(options[:default]) @choices = Choices.new @active_color = options.fetch(:active_color) { @prompt.active_color } @help_color = options.fetch(:help_color) { @prompt.help_color } @cycle = options.fetch(:cycle) { false } @filterable = options.fetch(:filter) { false } @symbols = @prompt.symbols.merge(options.fetch(:symbols, {})) @quiet = options.fetch(:quiet) { @prompt.quiet } @filter = [] @filter_cache = {} @help = options[:help] @show_help = options.fetch(:show_help) { :start } @first_render = true @done = false @per_page = options[:per_page] @paginator = Paginator.new @block_paginator = BlockPaginator.new @by_page = false @paging_changed = false end # Change symbols used by this prompt # # @param [Hash] new_symbols # the new symbols to use # # @api public def symbols(new_symbols = (not_set = true)) return @symbols if not_set @symbols.merge!(new_symbols) end # Set default option selected # # @api public def default(*default_values) @default = default_values end # Select paginator based on the current navigation key # # @return [Paginator] # # @api private def paginator @by_page ? @block_paginator : @paginator end # Synchronize paginators start positions # # @api private def sync_paginators if @by_page if @paginator.start_index @block_paginator.reset! @block_paginator.start_index = @paginator.start_index end else if @block_paginator.start_index @paginator.reset! @paginator.start_index = @block_paginator.start_index end end end # Set number of items per page # # @api public def per_page(value) @per_page = value end def page_size (@per_page || Paginator::DEFAULT_PAGE_SIZE) end # Check if list is paginated # # @return [Boolean] # # @api private def paginated? choices.size > page_size end # Provide help information # # @param [String] value # the new help text # # @return [String] # # @api public def help(value = (not_set = true)) return @help if !@help.nil? && not_set @help = (@help.nil? && !not_set) ? value : default_help end # Change when help is displayed # # @api public def show_help(value = (not_set = true)) return @show_ehlp if not_set @show_help = value end # Information about arrow keys # # @return [String] # # @api private def arrows_help up_down = @symbols[:arrow_up] + "/" + @symbols[:arrow_down] left_right = @symbols[:arrow_left] + "/" + @symbols[:arrow_right] arrows = [up_down] arrows << "/" if paginated? arrows << left_right if paginated? arrows.join end # Default help text # # Note that enumeration and filter are mutually exclusive # # @a public def default_help str = [] str << "(Press " str << "#{arrows_help} arrow" str << " or 1-#{choices.size} number" if enumerate? str << " to move" str << (filterable? ? "," : " and") str << " Enter to select" str << " and letters to filter" if filterable? str << ")" str.join end # Set selecting active index using number pad # # @api public def enum(value) @enum = value end # Set whether selected answers are echoed # # @api public def quiet(value) @quiet = value end # Add a single choice # # @api public def choice(*value, &block) @filter_cache = {} if block @choices << (value << block) else @choices << value end end # Add multiple choices, or return them. # # @param [Array[Object]] values # the values to add as choices; if not passed, the current # choices are displayed. # # @api public def choices(values = (not_set = true)) if not_set if !filterable? || @filter.empty? @choices else filter_value = @filter.join.downcase @filter_cache[filter_value] ||= @choices.enabled.select do |choice| choice.name.to_s.downcase.include?(filter_value) end end else @filter_cache = {} values.each { |val| @choices << val } end end # Call the list menu by passing question and choices # # @param [String] question # # @param # @api public def call(question, possibilities, &block) choices(possibilities) @question = question block.call(self) if block setup_defaults @prompt.subscribe(self) do render end end # Check if list is enumerated # # @return [Boolean] def enumerate? !@enum.nil? end def keynum(event) return unless enumerate? value = event.value.to_i return unless (1..choices.count).cover?(value) return if choices[value - 1].disabled? @active = value end def keyenter(*) @done = true unless choices.empty? end alias keyreturn keyenter alias keyspace keyenter def search_choice_in(searchable) searchable.find { |i| !choices[i - 1].disabled? } end def keyup(*) searchable = (@active - 1).downto(1).to_a prev_active = search_choice_in(searchable) if prev_active @active = prev_active elsif @cycle searchable = choices.length.downto(1).to_a prev_active = search_choice_in(searchable) @active = prev_active if prev_active end @paging_changed = @by_page @by_page = false end def keydown(*) searchable = ((@active + 1)..choices.length) next_active = search_choice_in(searchable) if next_active @active = next_active elsif @cycle searchable = (1..choices.length) next_active = search_choice_in(searchable) @active = next_active if next_active end @paging_changed = @by_page @by_page = false end alias keytab keydown # Moves all choices page by page keeping the current selected item # at the same level on each page. # # When the choice on a page is outside of next page range then # adjust it to the last item, otherwise leave unchanged. def keyright(*) choices_size = choices.size if (@active + page_size) <= choices_size searchable = ((@active + page_size)..choices_size) @active = search_choice_in(searchable) elsif @active <= choices_size # last page shorter current = @active % page_size remaining = choices_size % page_size if current.zero? || (remaining > 0 && current > remaining) searchable = choices_size.downto(0).to_a @active = search_choice_in(searchable) elsif @cycle searchable = ((current.zero? ? page_size : current)..choices_size) @active = search_choice_in(searchable) end end @paging_changed = !@by_page @by_page = true end alias keypage_down keyright def keyleft(*) if (@active - page_size) > 0 searchable = ((@active - page_size)..choices.size) @active = search_choice_in(searchable) elsif @cycle searchable = choices.size.downto(1).to_a @active = search_choice_in(searchable) end @paging_changed = !@by_page @by_page = true end alias keypage_up keyleft def keypress(event) return unless filterable? if event.value =~ FILTER_KEYS_MATCHER @filter << event.value @active = 1 end end def keydelete(*) return unless filterable? @filter.clear @active = 1 end def keybackspace(*) return unless filterable? @filter.pop @active = 1 end private def check_options_consistency(options) if options.key?(:enum) && options.key?(:filter) raise ConfigurationError, "Enumeration can't be used with filter" end end # Setup default option and active selection # # @return [Integer] # # @api private def setup_defaults validate_defaults if @default.empty? # no default, pick the first non-disabled choice @active = choices.index { |choice| !choice.disabled? } + 1 elsif @default.first.to_s =~ INTEGER_MATCHER @active = @default.first elsif default_choice = choices.find_by(:name, @default.first) @active = choices.index(default_choice) + 1 end end # Validate default indexes to be within range # # @raise [ConfigurationError] # raised when the default index is either non-integer, # out of range or clashes with disabled choice item. # # @api private def validate_defaults @default.each do |d| msg = if d.nil? || d.to_s.empty? "default index must be an integer in range (1 - #{choices.size})" elsif d.to_s !~ INTEGER_MATCHER validate_default_name(d) elsif d < 1 || d > choices.size "default index `#{d}` out of range (1 - #{choices.size})" elsif (dflt_choice = choices[d - 1]) && dflt_choice.disabled? "default index `#{d}` matches disabled choice" end raise(ConfigurationError, msg) if msg end end # Validate default choice name # # @param [String] name # the name to verify # # @return [String] # # @api private def validate_default_name(name) default_choice = choices.find_by(:name, name.to_s) if default_choice.nil? "no choice found for the default name: #{name.inspect}" elsif default_choice.disabled? "default name #{name.inspect} matches disabled choice" end end # Render a selection list. # # By default the result is printed out. # # @return [Object] value # return the selected value # # @api private def render @prompt.print(@prompt.hide) until @done question = render_question @prompt.print(question) @prompt.read_keypress # Split manually; if the second line is blank (when there are no # matching lines), it won't be included by using String#lines. question_lines = question.split($INPUT_RECORD_SEPARATOR, -1) @prompt.print(refresh(question_lines_count(question_lines))) end @prompt.print(render_question) unless @quiet answer ensure @prompt.print(@prompt.show) end # Count how many screen lines the question spans # # @return [Integer] # # @api private def question_lines_count(question_lines) question_lines.reduce(0) do |acc, line| acc + @prompt.count_screen_lines(line) end end # Find value for the choice selected # # @return [nil, Object] # # @api private def answer choices[@active - 1].value end # Clear screen lines # # @param [String] # # @api private def refresh(lines) @prompt.clear_lines(lines) end # Render question with instructions and menu # # @return [String] # # @api private def render_question header = ["#{@prefix}#{@question} #{render_header}\n"] @first_render = false unless @done header << render_menu end header.join end # Is filtering enabled? # # @return [Boolean] # # @api private def filterable? @filterable end # Header part showing the current filter # # @return String # # @api private def filter_help "(Filter: #{@filter.join.inspect})" end # Check if help is shown only on start # # @api private def help_start? @show_help =~ /start/i end # Check if help is always displayed # # @api private def help_always? @show_help =~ /always/i end # Render initial help and selected choice # # @return [String] # # @api private def render_header if @done selected_item = choices[@active - 1].name @prompt.decorate(selected_item.to_s, @active_color) elsif (@first_render && (help_start? || help_always?)) || (help_always? && !@filter.any?) @prompt.decorate(help, @help_color) elsif filterable? && @filter.any? @prompt.decorate(filter_help, @help_color) end end # Render menu with choices to select from # # @return [String] # # @api private def render_menu output = [] sync_paginators if @paging_changed paginator.paginate(choices, @active, @per_page) do |choice, index| num = enumerate? ? (index + 1).to_s + @enum + " " : "" message = if index + 1 == @active && !choice.disabled? selected = "#{@symbols[:marker]} #{num}#{choice.name}" @prompt.decorate(selected.to_s, @active_color) elsif choice.disabled? @prompt.decorate(@symbols[:cross], :red) + " #{num}#{choice.name} #{choice.disabled}" else " #{num}#{choice.name}" end end_index = paginated? ? paginator.end_index : choices.size - 1 newline = (index == end_index) ? "" : "\n" output << (message + newline) end output.join end end # List end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/mask_question.rb000066400000000000000000000044421403662044600220630ustar00rootroot00000000000000# frozen_string_literal: true require_relative "question" module TTY class Prompt class MaskQuestion < Question # Names for delete keys DELETE_KEYS = %i[backspace delete].freeze # Create masked question # # @param [Hash] options # @option options [String] :mask # # @api public def initialize(prompt, **options) super @mask = options.fetch(:mask) { @prompt.symbols[:dot] } @done_masked = false @failure = false end # Set character for masking the STDIN input # # @param [String] char # # @return [self] # # @api public def mask(char = (not_set = true)) return @mask if not_set @mask = char end def keyreturn(_event) @done_masked = true end def keyenter(_event) @done_masked = true end def keypress(event) if DELETE_KEYS.include?(event.key.name) @input.chop! unless @input.empty? elsif event.value =~ /^[^\e\n\r]/ @input += event.value end end # Render question and input replaced with masked character # # @api private def render_question header = ["#{@prefix}#{message} "] if echo? masked = @mask.to_s * @input.to_s.length if @done_masked && !@failure masked = @prompt.decorate(masked, @active_color) elsif @done_masked && @failure masked = @prompt.decorate(masked, @error_color) end header << masked end header << "\n" if @done header.join end def render_error(errors) @failure = !errors.empty? super end # Read input from user masked by character # # @private def read_input(question) @done_masked = false @failure = false @input = "" @prompt.print(question) until @done_masked @prompt.read_keypress question = render_question total_lines = @prompt.count_screen_lines(question) @prompt.print(@prompt.clear_lines(total_lines)) @prompt.print(render_question) end @prompt.puts @input end end # MaskQuestion end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/multi_list.rb000066400000000000000000000146531403662044600213730ustar00rootroot00000000000000# frozen_string_literal: true require_relative "list" require_relative "selected_choices" module TTY class Prompt # A class responsible for rendering multi select list menu. # Used by {Prompt} to display interactive choice menu. # # @api private class MultiList < List # Create instance of TTY::Prompt::MultiList menu. # # @param [Prompt] :prompt # @param [Hash] options # # @api public def initialize(prompt, **options) super @selected = SelectedChoices.new @help = options[:help] @echo = options.fetch(:echo, true) @min = options[:min] @max = options[:max] end # Set a minimum number of choices # # @api public def min(value) @min = value end # Set a maximum number of choices # # @api public def max(value) @max = value end # Callback fired when enter/return key is pressed # # @api private def keyenter(*) valid = true valid = @min <= @selected.size if @min valid = @selected.size <= @max if @max super if valid end alias keyreturn keyenter # Callback fired when space key is pressed # # @api private def keyspace(*) active_choice = choices[@active - 1] if @selected.include?(active_choice) @selected.delete_at(@active - 1) else return if @max && @selected.size >= @max @selected.insert(@active - 1, active_choice) end end # Selects all choices when Ctrl+A is pressed # # @api private def keyctrl_a(*) return if @max && @max < choices.size @selected = SelectedChoices.new(choices.enabled, choices.enabled_indexes) end # Revert currently selected choices when Ctrl+I is pressed # # @api private def keyctrl_r(*) return if @max && @max < choices.size indexes = choices.each_with_index.reduce([]) do |acc, (choice, idx)| acc << idx if !choice.disabled? && !@selected.include?(choice) acc end @selected = SelectedChoices.new(choices.enabled - @selected.to_a, indexes) end private # Setup default options and active selection # # @api private def setup_defaults validate_defaults # At this stage, @choices matches all the visible choices. default_indexes = @default.map do |d| if d.to_s =~ INTEGER_MATCHER d - 1 else choices.index(choices.find_by(:name, d.to_s)) end end @selected = SelectedChoices.new(@choices.values_at(*default_indexes), default_indexes) if @default.empty? # no default, pick the first non-disabled choice @active = choices.index { |choice| !choice.disabled? } + 1 elsif @default.last.to_s =~ INTEGER_MATCHER @active = @default.last elsif default_choice = choices.find_by(:name, @default.last.to_s) @active = choices.index(default_choice) + 1 end end # Generate selected items names # # @return [String] # # @api private def selected_names @selected.map(&:name).join(", ") end # Header part showing the minimum/maximum number of choices # # @return [String] # # @api private def minmax_help help = [] help << "min. #{@min}" if @min help << "max. #{@max}" if @max "(%s) " % [help.join(", ")] end # Build a default help text # # @return [String] # # @api private def default_help str = [] str << "(Press " str << "#{arrows_help} arrow" str << " or 1-#{choices.size} number" if enumerate? str << " to move, Space" str << "/Ctrl+A|R" if @max.nil? str << " to select" str << " (all|rev)" if @max.nil? str << (filterable? ? "," : " and") str << " Enter to finish" str << " and letters to filter" if filterable? str << ")" str.join end # Render initial help text and then currently selected choices # # @api private def render_header instructions = @prompt.decorate(help, @help_color) minmax_suffix = @min || @max ? minmax_help : "" print_selected = @selected.size.nonzero? && @echo if @done && @echo @prompt.decorate(selected_names, @active_color) elsif (@first_render && (help_start? || help_always?)) || (help_always? && !@filter.any? && !@done) minmax_suffix + (print_selected ? "#{selected_names} " : "") + instructions elsif filterable? && @filter.any? minmax_suffix + (print_selected ? "#{selected_names} " : "") + @prompt.decorate(filter_help, @help_color) else minmax_suffix + (print_selected ? selected_names : "") end end # All values for the choices selected # # @return [Array[nil,Object]] # # @api private def answer @selected.map(&:value) end # Render menu with choices to select from # # @return [String] # # @api private def render_menu output = [] sync_paginators if @paging_changed paginator.paginate(choices, @active, @per_page) do |choice, index| num = enumerate? ? (index + 1).to_s + @enum + " " : "" indicator = (index + 1 == @active) ? @symbols[:marker] : " " indicator += " " message = if @selected.include?(choice) && !choice.disabled? selected = @prompt.decorate(@symbols[:radio_on], @active_color) "#{selected} #{num}#{choice.name}" elsif choice.disabled? @prompt.decorate(@symbols[:cross], :red) + " #{num}#{choice.name} #{choice.disabled}" else "#{@symbols[:radio_off]} #{num}#{choice.name}" end end_index = paginated? ? paginator.end_index : choices.size - 1 newline = (index == end_index) ? "" : "\n" output << indicator + message + newline end output.join end end # MultiList end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/multiline.rb000066400000000000000000000032241403662044600212000ustar00rootroot00000000000000# frozen_string_literal: true require_relative "question" require_relative "symbols" module TTY class Prompt # A prompt responsible for multi line user input # # @api private class Multiline < Question HELP = "(Press Ctrl+D or Ctrl+Z to finish)".freeze def initialize(prompt, **options) super @help = options[:help] || self.class::HELP @first_render = true @lines_count = 0 end # Provide help information # # @return [String] # # @api public def help(value = (not_set = true)) return @help if not_set @help = value end def read_input @prompt.read_multiline end def keyreturn(*) @lines_count += 1 end alias keyenter keyreturn def render_question header = ["#{@prefix}#{message} "] if !echo? header elsif @done header << @prompt.decorate(@input.to_s, @active_color) elsif @first_render header << @prompt.decorate(help, @help_color) @first_render = false end header << "\n" header.join end def process_input(question) @prompt.print(question) @lines = read_input @input = "#{@lines.first.strip} ..." unless @lines.first.to_s.empty? if Utils.blank?(@input) && default? @input = default @lines = default end @evaluator.(@lines) end def refresh(lines, lines_to_clear) size = @lines_count + lines_to_clear + 1 @prompt.clear_lines(size) end end # Multiline end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/paginator.rb000066400000000000000000000063321403662044600211650ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt class Paginator DEFAULT_PAGE_SIZE = 6 # The 0-based index of the first item on this page attr_accessor :start_index # The 0-based index of the last item on this page attr_reader :end_index # The 0-based index of the active item on this page attr_reader :current_index # The 0-based index of the previously active item on this page attr_reader :last_index # Create a Paginator # # @api private def initialize(**options) @last_index = Array(options[:default]).flatten.first || 0 @per_page = options[:per_page] @start_index = Array(options[:default]).flatten.first end # Reset current page indexes # # @api private def reset! @start_index = nil @end_index = nil end # Check if page size is valid # # @raise [InvalidArgument] # # @api private def check_page_size! raise InvalidArgument, "per_page must be > 0" if @per_page < 1 end # Paginate collection given an active index # # @param [Array[Choice]] list # a collection of choice items # @param [Integer] active # current choice active index # @param [Integer] per_page # number of choice items per page # # @return [Enumerable] # the list between start and end index # # @api public def paginate(list, active, per_page = nil, &block) current_index = active - 1 default_size = (list.size <= DEFAULT_PAGE_SIZE ? list.size : DEFAULT_PAGE_SIZE) @per_page = @per_page || per_page || default_size check_page_size! @start_index ||= (current_index / @per_page) * @per_page @end_index ||= @start_index + @per_page - 1 # Don't paginate short lists if list.size <= @per_page @start_index = 0 @end_index = list.size - 1 if block return list.each_with_index(&block) else return list.each_with_index.to_enum end end step = (current_index - @last_index).abs if current_index > @last_index # going up if current_index >= @end_index && current_index < list.size - 1 last_page = list.size - @per_page @start_index = [@start_index + step, last_page].min end elsif current_index < @last_index # going down if current_index <= @start_index && current_index > 0 @start_index = [@start_index - step, 0].max end end # Cycle list if current_index.zero? @start_index = 0 elsif current_index == list.size - 1 @start_index = list.size - 1 - (@per_page - 1) end @end_index = @start_index + (@per_page - 1) @last_index = current_index sliced_list = list[@start_index..@end_index] page_range = (@start_index..@end_index) return sliced_list.zip(page_range).to_enum unless block_given? sliced_list.each_with_index do |item, index| block[item, @start_index + index] end end end # Paginator end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/question.rb000066400000000000000000000227711403662044600210550ustar00rootroot00000000000000# frozen_string_literal: true require_relative "converters" require_relative "evaluator" require_relative "question/modifier" require_relative "question/validation" require_relative "question/checks" require_relative "utils" module TTY # A class responsible for shell prompt interactions. class Prompt # A class responsible for gathering user input # # @api public class Question include Checks UndefinedSetting = Class.new do def to_s "undefined" end alias_method :inspect, :to_s end # Store question message # @api public attr_reader :message attr_reader :modifier attr_reader :validation # Initialize a Question # # @api public def initialize(prompt, **options) # Option deprecation if options[:validation] warn "[DEPRECATION] The `:validation` option is deprecated. Use `:validate` instead." options[:validate] = options[:validation] end @prompt = prompt @prefix = options.fetch(:prefix) { @prompt.prefix } @default = options.fetch(:default) { UndefinedSetting } @required = options.fetch(:required) { false } @echo = options.fetch(:echo) { true } @in = options.fetch(:in) { UndefinedSetting } @modifier = options.fetch(:modifier) { [] } @validation = options.fetch(:validate) { UndefinedSetting } @convert = options.fetch(:convert) { UndefinedSetting } @active_color = options.fetch(:active_color) { @prompt.active_color } @help_color = options.fetch(:help_color) { @prompt.help_color } @error_color = options.fetch(:error_color) { :red } @value = options.fetch(:value) { UndefinedSetting } @quiet = options.fetch(:quiet) { @prompt.quiet } @messages = Utils.deep_copy(options.fetch(:messages) { {} }) @done = false @first_render = true @input = nil @evaluator = Evaluator.new(self) @evaluator << CheckRequired @evaluator << CheckDefault @evaluator << CheckRange @evaluator << CheckValidation @evaluator << CheckModifier @evaluator << CheckConversion end # Stores all the error messages displayed to user # The currently supported messages are: # * :range? # * :required? # * :valid? attr_reader :messages # Retrieve message based on the key # # @param [Symbol] name # the name of message key # # @param [Hash] tokens # the tokens to evaluate # # @return [Array[String]] # # @api private def message_for(name, tokens = nil) template = @messages[name] if template && !template.match(/\%\{/).nil? [template % tokens] else [template || ""] end end # Call the question # # @param [String] message # # @return [self] # # @api public def call(message = "", &block) @message = message block.call(self) if block @prompt.subscribe(self) do render end end # Read answer and convert to type # # @api private def render @errors = [] until @done result = process_input(render_question) if result.failure? @errors = result.errors @prompt.print(render_error(result.errors)) else @done = true end question = render_question input_line = question + result.value.to_s total_lines = @prompt.count_screen_lines(input_line) @prompt.print(refresh(question.lines.count, total_lines)) end @prompt.print(render_question) unless @quiet result.value end # Render question # # @return [String] # # @api private def render_question header = [] if !Utils.blank?(@prefix) || !Utils.blank?(message) header << "#{@prefix}#{message} " end if !echo? header elsif @done header << @prompt.decorate(@input.to_s, @active_color) elsif default? && !Utils.blank?(@default) header << @prompt.decorate("(#{default})", @help_color) + " " end header << "\n" if @done header.join end # Decide how to handle input from user # # @api private def process_input(question) @input = read_input(question) if Utils.blank?(@input) @input = default? ? default : nil end @evaluator.(@input) end # Process input # # @api private def read_input(question) options = { echo: echo } if value? && @first_render options[:value] = @value @first_render = false end @prompt.read_line(question, **options).chomp end # Handle error condition # # @return [String] # # @api private def render_error(errors) errors.reduce([]) do |acc, err| acc << @prompt.decorate(">>", :red) + " " + err acc end.join("\n") end # Determine area of the screen to clear # # @param [Integer] lines # number of lines to clear # # @return [String] # # @api private def refresh(lines, lines_to_clear) output = [] if @done if @errors.count.zero? output << @prompt.cursor.up(lines) else lines += @errors.count lines_to_clear += @errors.count end else output << @prompt.cursor.up(lines) end output.join + @prompt.clear_lines(lines_to_clear) end # Convert value to expected type # # @param [Object] value # # @api private def convert_result(value) if convert? && !Utils.blank?(value) case @convert when Proc @convert.call(value) else Converters.convert(@convert, value) end else value end end # Specify answer conversion # # @api public def convert(value = (not_set = true), message = nil) messages[:convert?] = message if message if not_set @convert else @convert = value end end # Check if conversion is set # # @return [Boolean] # # @api public def convert? @convert != UndefinedSetting end # Set default value. # # @api public def default(value = (not_set = true)) return @default if not_set @default = value end # Check if default value is set # # @return [Boolean] # # @api public def default? @default != UndefinedSetting end # Ensure that passed argument is present or not # # @return [Boolean] # # @api public def required(value = (not_set = true), message = nil) messages[:required?] = message if message return @required if not_set @required = value end alias required? required # Set validation rule for an argument # # @param [Object] value # # @return [Question] # # @api public def validate(value = nil, message = nil, &block) messages[:valid?] = message if message @validation = (value || block) end # Prepopulate input with custom content # # @api public def value(val) return @value if val.nil? @value = val end # Check if custom value is present # # @api private def value? @value != UndefinedSetting end def validation? @validation != UndefinedSetting end # Modify string according to the rule given. # # @param [Symbol] rule # # @api public def modify(*rules) @modifier = rules end # Turn terminal echo on or off. This is used to secure the display so # that the entered characters are not echoed back to the screen. # # @api public def echo(value = nil) return @echo if value.nil? @echo = value end alias echo? echo # Turn raw mode on or off. This enables character-based input. # # @api public def raw(value = nil) return @raw if value.nil? @raw = value end alias raw? raw # Set expected range of values # # @param [String] value # # @api public def in(value = (not_set = true), message = nil) messages[:range?] = message if message if in? && !@in.is_a?(Range) @in = Converters.convert(:range, @in) end return @in if not_set @in = Converters.convert(:range, value) end # Check if range is set # # @return [Boolean] # # @api public def in? @in != UndefinedSetting end # Set quiet mode. # # @api public def quiet(value) @quiet = value end # @api public def to_s message.to_s end # String representation of this question # @api public def inspect "#<#{self.class.name} @message=#{message}, @input=#{@input}>" end end # Question end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/question/000077500000000000000000000000001403662044600205175ustar00rootroot00000000000000tty-prompt-0.23.1/lib/tty/prompt/question/checks.rb000066400000000000000000000055561403662044600223170ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../const" module TTY class Prompt class Question module Checks # Check if modifications are applicable class CheckModifier def self.call(question, value) if !question.modifier.nil? || question.modifier [Modifier.new(question.modifier).apply_to(value)] else [value] end end end # Check if value is within range class CheckRange def self.float?(value) !/[-+]?(\d*[.])?\d+/.match(value.to_s).nil? end def self.int?(value) !/^[-+]?\d+$/.match(value.to_s).nil? end def self.cast(value) if float?(value) value.to_f elsif int?(value) value.to_i else value end end def self.call(question, value) if !question.in? || (question.in? && question.in.include?(cast(value))) [value] else tokens = { value: value, in: question.in } [value, question.message_for(:range?, tokens)] end end end # Check if input requires validation class CheckValidation def self.call(question, value) if !question.validation? || (question.required? && value.nil?) || (question.validation? && Validation.new(question.validation).call(value)) [value] else tokens = { valid: question.validation.inspect, value: value } [value, question.message_for(:valid?, tokens)] end end end # Check if default value provided class CheckDefault def self.call(question, value) if value.nil? && question.default? [question.default] else [value] end end end # Check if input is required class CheckRequired def self.call(question, value) if question.required? && !question.default? && value.nil? [value, question.message_for(:required?)] else [value] end end end class CheckConversion def self.call(question, value) if question.convert? && !Utils.blank?(value) result = question.convert_result(value) if result == Const::Undefined tokens = { value: value, type: question.convert } [value, question.message_for(:convert?, tokens)] else [result] end else [value] end end end end # Checks end # Question end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/question/modifier.rb000066400000000000000000000055111403662044600226440ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt class Question # A class representing String modifications. class Modifier attr_reader :modifiers # Initialize a Modifier # # @api public def initialize(modifiers) @modifiers = modifiers end # Change supplied value according to the given string transformation. # Valid settings are: # # @param [String] value # the string to be modified # # @return [String] # # @api private def apply_to(value) modifiers.reduce(value) do |result, mod| result = Modifier.letter_case(mod, result) Modifier.whitespace(mod, result) end end # Changes letter casing in a string according to valid modifications. # For invalid modification option the string is preserved. # # @param [Symbol] mod # the modification to change the string # # @option mod [Symbol] :up change to upper case # @option mod [Symbol] :upcase change to upper case # @option mod [Symbol] :uppercase change to upper case # @option mod [Symbol] :down change to lower case # @option mod [Symbol] :downcase change to lower case # @option mod [Symbol] :capitalize change all words to start # with uppercase case letter # # @return [String] # # @api public def self.letter_case(mod, value) return value unless value.is_a?(String) case mod when :up, :upcase, :uppercase value.upcase when :down, :downcase, :lowercase value.downcase when :capitalize value.capitalize else value end end # Changes whitespace in a string according to valid modifications. # # @param [Symbol] mod # the modification to change the string # # @option mod [String] :trim, :strip # remove whitespace for the start and end # @option mod [String] :chomp remove record separator from the end # @option mod [String] :collapse remove any duplicate whitespace # @option mod [String] :remove remove all whitespace # # @api public def self.whitespace(mod, value) return value unless value.is_a?(String) case mod when :trim, :strip value.strip when :chomp value.chomp when :collapse value.gsub(/\s+/, " ") when :remove value.gsub(/\s+/, "") else value end end end # Modifier end # Question end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/question/validation.rb000066400000000000000000000035261403662044600232040ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt class Question # A class representing question validation. class Validation # Available validator names VALIDATORS = { email: /^[a-z0-9._%+-]+@([a-z0-9-]+\.)+[a-z]{2,6}$/i }.freeze attr_reader :pattern # Initialize a Validation # # @param [Object] pattern # # @return [undefined] # # @api private def initialize(pattern) @pattern = coerce(pattern) end # Convert validation into known type. # # @param [Object] pattern # # @raise [TTY::ValidationCoercion] # raised when failed to convert validation # # @api private def coerce(pattern) case pattern when String, Symbol, Proc pattern when Regexp Regexp.new(pattern.to_s) else raise ValidationCoercion, "Wrong type, got #{pattern.class}" end end # Test if the input passes the validation # # @example # Validation.new(/pattern/) # validation.call(input) # => true # # @param [Object] input # the input to validate # # @return [Boolean] # # @api public def call(input) if pattern.is_a?(String) || pattern.is_a?(Symbol) VALIDATORS.key?(pattern.to_sym) !VALIDATORS[pattern.to_sym].match(input.to_s).nil? elsif pattern.is_a?(Regexp) !pattern.match(input.to_s).nil? elsif pattern.is_a?(Proc) result = pattern.call(input.to_s) result.nil? ? false : result else false end end end # Validation end # Question end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/result.rb000066400000000000000000000016021403662044600205120ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt # Accumulates errors class Result attr_reader :question, :value, :errors def initialize(question, value, errors = []) @question = question @value = value @errors = errors end def with(condition = nil, &block) validator = (condition || block) (new_value, validation_error) = validator.call(question, value) accumulated_errors = errors + Array(validation_error) if accumulated_errors.empty? Success.new(question, new_value) else Failure.new(question, new_value, accumulated_errors) end end def success? is_a?(Success) end def failure? is_a?(Failure) end class Success < Result end class Failure < Result end end end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/selected_choices.rb000066400000000000000000000032211403662044600224600ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt # @api private class SelectedChoices include Enumerable attr_reader :size # Create selected choices # # @param [Array] selected # @param [Array] indexes # # @api public def initialize(selected = [], indexes = []) @selected = selected @indexes = indexes @size = @selected.size end # Clear selected choices # # @api public def clear @indexes.clear @selected.clear @size = 0 end # Iterate over selected choices # # @api public def each(&block) return to_enum unless block_given? @selected.each(&block) end # Insert choice at index # # @param [Integer] index # @param [Choice] choice # # @api public def insert(index, choice) insert_idx = find_index_by { |i| index < @indexes[i] } insert_idx ||= -1 @indexes.insert(insert_idx, index) @selected.insert(insert_idx, choice) @size += 1 self end # Delete choice at index # # @return [Choice] # the deleted choice # # @api public def delete_at(index) delete_idx = @indexes.each_index.find { |i| index == @indexes[i] } return nil unless delete_idx @indexes.delete_at(delete_idx) choice = @selected.delete_at(delete_idx) @size -= 1 choice end def find_index_by(&search) (0...@size).bsearch(&search) end end # SelectedChoices end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/slider.rb000066400000000000000000000156351403662044600204710ustar00rootroot00000000000000# frozen_string_literal: true module TTY # A class responsible for shell prompt interactions. class Prompt # A class responsible for gathering numeric input from range # # @api public class Slider HELP = "(Use %s arrow keys, press Enter to select)" FORMAT = ":slider %s" # Initailize a Slider # # @param [Prompt] prompt # the prompt # @param [Hash] options # the options to configure this slider # @option options [Integer] :min The minimum value # @option options [Integer] :max The maximum value # @option options [Integer] :step The step value # @option options [String] :format The display format # # @api public def initialize(prompt, **options) @prompt = prompt @prefix = options.fetch(:prefix) { @prompt.prefix } @choices = Choices.new @min = options.fetch(:min, 0) @max = options.fetch(:max, 10) @step = options.fetch(:step, 1) @default = options[:default] @active_color = options.fetch(:active_color) { @prompt.active_color } @help_color = options.fetch(:help_color) { @prompt.help_color } @format = options.fetch(:format) { FORMAT } @quiet = options.fetch(:quiet) { @prompt.quiet } @help = options[:help] @show_help = options.fetch(:show_help) { :start } @symbols = @prompt.symbols.merge(options.fetch(:symbols, {})) @first_render = true @done = false end # Change symbols used by this prompt # # @param [Hash] new_symbols # the new symbols to use # # @api public def symbols(new_symbols = (not_set = true)) return @symbols if not_set @symbols.merge!(new_symbols) end # Setup initial active position # # @return [Integer] # # @api private def initial if @default.nil? # no default - choose the middle option choices.size / 2 elsif default_choice = choices.find_by(:name, @default) # found a Choice by name - use it choices.index(default_choice) else # default is the index number @default - 1 end end # Default help text # # @api public def default_help arrows = @symbols[:arrow_left] + "/" + @symbols[:arrow_right] sprintf(HELP, arrows) end # Set help text # # @param [String] text # # @api private def help(text = (not_set = true)) return @help if !@help.nil? && not_set @help = (@help.nil? && not_set) ? default_help : text end # Change when help is displayed # # @api public def show_help(value = (not_set = true)) return @show_ehlp if not_set @show_help = value end # @api public def default(value) @default = value end # @api public def min(value) @min = value end # @api public def max(value) @max = value end # @api public def step(value) @step = value end # Add a single choice # # @api public def choice(*value, &block) if block @choices << (value << block) else @choices << value end end # Add multiple choices # # @param [Array[Object]] values # the values to add as choices # # @api public def choices(values = (not_set = true)) if not_set @choices else values.each { |val| @choices << val } end end # @api public def format(value) @format = value end # Set quiet mode. # # @api public def quiet(value) @quiet = value end # Call the slider by passing question # # @param [String] question # the question to ask # # @apu public def call(question, possibilities = nil, &block) @question = question choices(possibilities) if possibilities block.call(self) if block # set up a Choices collection for min, max, step # if no possibilities were supplied choices((@min..@max).step(@step).to_a) if @choices.empty? @active = initial @prompt.subscribe(self) do render end end def keyleft(*) @active -= 1 if @active > 0 end alias keydown keyleft def keyright(*) @active += 1 if (@active + 1) < choices.size end alias keyup keyright def keyreturn(*) @done = true end alias keyspace keyreturn alias keyenter keyreturn private # Check if help is shown only on start # # @api private def help_start? @show_help =~ /start/i end # Check if help is always displayed # # @api private def help_always? @show_help =~ /always/i end # Render an interactive range slider. # # @api private def render @prompt.print(@prompt.hide) until @done question = render_question @prompt.print(question) @prompt.read_keypress refresh(question.lines.count) end @prompt.print(render_question) unless @quiet answer ensure @prompt.print(@prompt.show) end # Clear screen # # @param [Integer] lines # the lines to clear # # @api private def refresh(lines) @prompt.print(@prompt.clear_lines(lines)) end # @return [Integer, String] # # @api private def answer choices[@active].value end # Render question with the slider # # @return [String] # # @api private def render_question header = ["#{@prefix}#{@question} "] if @done header << @prompt.decorate(choices[@active].to_s, @active_color) header << "\n" else header << render_slider end if @first_render && (help_start? || help_always?) || (help_always? && !@done) header << "\n" + @prompt.decorate(help, @help_color) @first_render = false end header.join end # Render slider representation # # @return [String] # # @api private def render_slider slider = (@symbols[:line] * @active) + @prompt.decorate(@symbols[:bullet], @active_color) + (@symbols[:line] * (choices.size - @active - 1)) value = choices[@active].name case @format when Proc @format.call(slider, value) else @format.gsub(":slider", slider) % [value] end end end # Slider end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/statement.rb000066400000000000000000000024531403662044600212050ustar00rootroot00000000000000# frozen_string_literal: true module TTY # A class responsible for shell prompt interactions. class Prompt # A class representing a statement output to prompt. class Statement # Flag to display newline # # @api public attr_reader :newline # Color used to display statement # # @api public attr_reader :color # Initialize a Statement # # @param [TTY::Prompt] prompt # # @param [Hash] options # # @option options [Symbol] :newline # force a newline break after the message # # @option options [Symbol] :color # change the message display to color # # @api public def initialize(prompt, newline: true, color: false) @prompt = prompt @newline = newline @color = color end # Output the message to the prompt # # @param [String] message # the message to be printed to stdout # # @api public def call(message) message = @prompt.decorate(message, *color) if color if newline && /( |\t)(\e\[\d+(;\d+)*m)?\Z/ !~ message @prompt.puts message else @prompt.print message @prompt.flush end end end # Statement end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/suggestion.rb000066400000000000000000000053061403662044600213700ustar00rootroot00000000000000# frozen_string_literal: true require_relative "distance" module TTY # A class responsible for terminal prompt interactions. class Prompt # A class representing a suggestion out of possible choices # # @api public class Suggestion DEFAULT_INDENT = 8 SINGLE_TEXT = "Did you mean this?" PLURAL_TEXT = "Did you mean one of these?" # Number of spaces # # @api public attr_reader :indent # Text for a single suggestion # # @api public attr_reader :single_text # Text for multiple suggestions # # @api public attr_reader :plural_text # Initialize a Suggestion # # @api public def initialize(**options) @indent = options.fetch(:indent) { DEFAULT_INDENT } @single_text = options.fetch(:single_text) { SINGLE_TEXT } @plural_text = options.fetch(:plural_text) { PLURAL_TEXT } @suggestions = [] @comparator = Distance.new end # Suggest matches out of possibile strings # # @param [String] message # # @param [Array[String]] possibilities # # @api public def suggest(message, possibilities) distances = measure_distances(message, possibilities) minimum_distance = distances.keys.min max_distance = distances.keys.max if minimum_distance < max_distance @suggestions = distances[minimum_distance].sort end evaluate end private # Measure distances between messag and possibilities # # @param [String] message # # @param [Array[String]] possibilities # # @return [Hash] # # @api private def measure_distances(message, possibilities) distances = Hash.new { |hash, key| hash[key] = [] } possibilities.each do |possibility| distances[@comparator.distance(message, possibility)] << possibility end distances end # Build up a suggestion string # # @param [Array[String]] suggestions # # @return [String] # # @api private def evaluate return @suggestions if @suggestions.empty? if @suggestions.one? build_single_suggestion else build_multiple_suggestions end end # @api private def build_single_suggestion single_text + "\n" + (" " * indent) + @suggestions.first end # @api private def build_multiple_suggestions plural_text + "\n" + @suggestions.map do |sugest| " " * indent + sugest end.join("\n") end end # Suggestion end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/symbols.rb000066400000000000000000000036671403662044600207010ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt # Cross platform common Unicode symbols. # # @api public module Symbols KEYS = { tick: "✓", cross: "✘", star: "★", square: "◼", square_empty: "◻", dot: "•", bullet: "●", bullet_empty: "○", marker: "‣", line: "─", pipe: "|", ellipsis: "…", radio_on: "⬢", radio_off: "⬡", checkbox_on: "☒", checkbox_off: "☐", circle: "◯", circle_on: "ⓧ", circle_off: "Ⓘ", arrow_up: "↑", arrow_down: "↓", arrow_up_down: "↕", arrow_left: "←", arrow_right: "→", arrow_left_right: "↔", heart: "♥", diamond: "♦", club: "♣", spade: "♠" }.freeze WIN_KEYS = { tick: "√", cross: "x", star: "*", square: "[█]", square_empty: "[ ]", dot: ".", bullet: "O", bullet_empty: "○", marker: ">", line: "-", pipe: "|", ellipsis: "...", radio_on: "(*)", radio_off: "( )", checkbox_on: "[×]", checkbox_off: "[ ]", circle: "( )", circle_on: "(x)", circle_off: "( )", arrow_up: "↑", arrow_down: "↓", arrow_up_down: "↕", arrow_left: "←", arrow_right: "→", arrow_left_right: "↔", heart: "♥", diamond: "♦", club: "♣", spade: "♠" }.freeze def symbols @symbols ||= windows? ? WIN_KEYS : KEYS end module_function :symbols # Check if Windowz # # @return [Boolean] # # @api public def windows? ::File::ALT_SEPARATOR == "\\" end module_function :windows? end # Symbols end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/test.rb000066400000000000000000000012741403662044600201600ustar00rootroot00000000000000# frozen_string_literal: true require "stringio" require_relative "../prompt" module TTY # Used for initializing test cases class Prompt module StringIOExtensions def wait_readable(*) true end def ioctl(*) 80 end end class Test < TTY::Prompt def initialize(**options) @input = StringIO.new @input.extend(StringIOExtensions) @output = StringIO.new options.merge!({ input: @input, output: @output, env: { "TTY_TEST" => true }, enable_color: options.fetch(:enable_color, true) }) super(**options) end end # Test end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/timer.rb000066400000000000000000000026201403662044600203150ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt class Timer attr_reader :duration attr_reader :total attr_reader :interval def initialize(duration, interval) @duration = duration @interval = interval @total = 0.0 @current = nil @events = [] end def start return if @current @current = time_now end def stop return unless @current @current = nil end def runtime time_now - @current end def on_tick(&block) @events << block end def while_remaining start remaining = duration if @duration while remaining >= 0.0 if runtime >= total tick = duration - @total @events.each { |block| block.(tick) } @total += @interval end yield(remaining) remaining = duration - runtime end else loop { yield } end ensure stop end if defined?(Process::CLOCK_MONOTONIC) # Object representing current time def time_now ::Process.clock_gettime(Process::CLOCK_MONOTONIC) end else # Object represeting current time def time_now ::Time.now end end end # Timer end # Prompt end # TTY tty-prompt-0.23.1/lib/tty/prompt/utils.rb000066400000000000000000000015061403662044600203370ustar00rootroot00000000000000# frozen_string_literal: true module TTY module Utils module_function BLANK_REGEX = /\A[[:space:]]*\z/o.freeze # Extract options hash from array argument # # @param [Array[Object]] args # # @api public def extract_options(args) options = args.last options.respond_to?(:to_hash) ? options.to_hash.dup : {} end def extract_options!(args) args.last.respond_to?(:to_hash) ? args.pop : {} end # Check if value is nil or an empty string # # @param [Object] value # the value to check # # @return [Boolean] # # @api public def blank?(value) value.nil? || BLANK_REGEX === value end # Deep copy object # # @api public def deep_copy(object) Marshal.load(Marshal.dump(object)) end end # Utils end # TTY tty-prompt-0.23.1/lib/tty/prompt/version.rb000066400000000000000000000001511403662044600206570ustar00rootroot00000000000000# frozen_string_literal: true module TTY class Prompt VERSION = "0.23.1" end # Prompt end # TTY tty-prompt-0.23.1/spec/000077500000000000000000000000001403662044600146735ustar00rootroot00000000000000tty-prompt-0.23.1/spec/spec_helper.rb000066400000000000000000000021221403662044600175060ustar00rootroot00000000000000# frozen_string_literal: true if ENV["COVERAGE"] == "true" require "simplecov" require "coveralls" SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter ]) SimpleCov.start do command_name "spec" add_filter "spec" end end require "tty/prompt" require "tty/prompt/test" RSpec.configure do |config| config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true expectations.max_formatted_output_length = nil end config.mock_with :rspec do |mocks| mocks.verify_partial_doubles = true end # Limits the available syntax to the non-monkey patched syntax that is recommended. config.disable_monkey_patching! # This setting enables warnings. It's recommended, but in some cases may # be too noisy due to issues in dependencies. config.warnings = true if config.files_to_run.one? config.default_formatter = "doc" end config.profile_examples = 2 config.order = :random Kernel.srand config.seed end tty-prompt-0.23.1/spec/unit/000077500000000000000000000000001403662044600156525ustar00rootroot00000000000000tty-prompt-0.23.1/spec/unit/ask_spec.rb000066400000000000000000000121741403662044600177740ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, "#ask" do subject(:prompt) { TTY::Prompt::Test.new } it "asks question" do prompt.ask("What is your name?") expect(prompt.output.string).to eq([ "What is your name? ", "\e[1A\e[2K\e[1G", "What is your name? \n" ].join) end it "asks an empty question " do prompt.input << "\r" prompt.input.rewind answer = prompt.ask expect(answer).to eq(nil) expect(prompt.output.string).to eql("\e[2K\e[1G\n\e[1A\e[2K\e[1G\n") end it "asks an empty question and returns nil if EOF is sent to stdin" do prompt.input << nil prompt.input.rewind answer = prompt.ask("") expect(answer).to eql(nil) expect(prompt.output.string).to eq("\e[1A\e[2K\e[1G\n") end it "asks an empty question with prepopulated value" do prompt.input << "\n" prompt.input.rewind answer = prompt.ask value: "yes" expect(answer).to eq("yes") expect(prompt.output.string).to eq([ "yes\e[2K\e[1G", "yes\n\e[1A\e[2K\e[1G", "\e[32myes\e[0m\n" ].join) end it "asks question with prepopulated value" do prompt = TTY::Prompt::Test.new prefix: "> " prompt.input << "\n" prompt.input.rewind answer = prompt.ask("Say?") do |q| q.value "yes" end expect(answer).to eq("yes") expect(prompt.output.string).to eq([ "> Say? yes\e[2K\e[1G", "> Say? yes\n\e[1A\e[2K\e[1G", "> Say? \e[32myes\e[0m\n" ].join) end it "asks a question with a prefix [?]" do prompt = TTY::Prompt::Test.new(prefix: "[?] ") prompt.input << "\r" prompt.input.rewind answer = prompt.ask "Are you Polish?" expect(answer).to eq(nil) expect(prompt.output.string).to eq([ "[?] Are you Polish? ", "\e[2K\e[1G[?] Are you Polish? \n", "\e[1A\e[2K\e[1G", "[?] Are you Polish? \n" ].join) end it "asks a question with block" do prompt.input << "" prompt.input.rewind answer = prompt.ask "What is your name?" do |q| q.default "Piotr" end expect(answer).to eq("Piotr") expect(prompt.output.string).to eq([ "What is your name? \e[90m(Piotr)\e[0m ", "\e[1A\e[2K\e[1G", "What is your name? \e[32mPiotr\e[0m\n" ].join) end it "changes question color" do prompt.input << "" prompt.input.rewind options = {default: "Piotr", help_color: :red, active_color: :cyan} answer = prompt.ask("What is your name?", **options) expect(answer).to eq("Piotr") expect(prompt.output.string).to eq([ "What is your name? \e[31m(Piotr)\e[0m ", "\e[1A\e[2K\e[1G", "What is your name? \e[36mPiotr\e[0m\n" ].join) end it "permits empty default parameter" do prompt.input << "\r" prompt.input.rewind answer = prompt.ask("What is your name?", default: "") expect(answer).to eq("") expect(prompt.output.string).to eq([ "What is your name? ", "\e[2K\e[1GWhat is your name? \n", "\e[1A\e[2K\e[1G", "What is your name? \n" ].join) end it "permits nil default parameter" do prompt.input << "\r" prompt.input.rewind answer = prompt.ask("What is your name?", default: nil) expect(answer).to eq(nil) expect(prompt.output.string).to eq([ "What is your name? ", "\e[2K\e[1GWhat is your name? \n", "\e[1A\e[2K\e[1G", "What is your name? \n" ].join) end it "sets quiet mode" do prompt.ask("What is your name?", quiet: true) expect(prompt.output.string).to eq([ "What is your name? ", "\e[1A\e[2K\e[1G" ].join) end it "sets quiet mode through DSL" do prompt.ask("What is your name?") do |q| q.quiet true end expect(prompt.output.string).to eq([ "What is your name? ", "\e[1A\e[2K\e[1G" ].join) end it "overwrites global settings" do active = ->(str) { Pastel.new(enabled: true).cyan(str) } help = Pastel.new.red.detach global_settings = {prefix: "[?] ", active_color: active, help_color: help} prompt = TTY::Prompt::Test.new(**global_settings) prompt.input << "Piotr\r" prompt.input.rewind prompt.ask("What is your name?") prompt.input << "Piotr\r" prompt.input.rewind local_settings = {prefix: ":-) ", active_color: :blue, help_color: :magenta} prompt.ask("What is your name?", **local_settings) expect(prompt.output.string).to eq([ "[?] What is your name? ", "\e[2K\e[1G[?] What is your name? P", "\e[2K\e[1G[?] What is your name? Pi", "\e[2K\e[1G[?] What is your name? Pio", "\e[2K\e[1G[?] What is your name? Piot", "\e[2K\e[1G[?] What is your name? Piotr", "\e[2K\e[1G[?] What is your name? Piotr\n", "\e[1A\e[2K\e[1G", "[?] What is your name? \e[36mPiotr\e[0m\n", ":-) What is your name? ", "\e[2K\e[1G:-) What is your name? P", "\e[2K\e[1G:-) What is your name? Pi", "\e[2K\e[1G:-) What is your name? Pio", "\e[2K\e[1G:-) What is your name? Piot", "\e[2K\e[1G:-) What is your name? Piotr", "\e[2K\e[1G:-) What is your name? Piotr\n", "\e[1A\e[2K\e[1G", ":-) What is your name? \e[34mPiotr\e[0m\n" ].join) end end tty-prompt-0.23.1/spec/unit/block_paginator_spec.rb000066400000000000000000000060751403662044600223570ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::BlockPaginator, "#paginate" do it "ignores per_page when equal items " do list = %w[a b c d] paginator = described_class.new(per_page: 4) expect(paginator.paginate(list, 1).to_a).to eq([ ["a", 0], ["b", 1], ["c", 2], ["d", 3] ]) end it "ignores per_page when less items " do list = %w[a b c d] paginator = described_class.new(per_page: 5) expect(paginator.paginate(list, 1).to_a).to eq([ ["a", 0], ["b", 1], ["c", 2], ["d", 3] ]) end it "paginates items matching per_page count" do list = %w[a b c d e f] paginator = described_class.new(per_page: 3) expect(paginator.paginate(list, 1).to_a).to eq([["a", 0], ["b", 1], ["c", 2]]) expect(paginator.paginate(list, 2).to_a).to eq([["a", 0], ["b", 1], ["c", 2]]) expect(paginator.paginate(list, 3).to_a).to eq([["a", 0], ["b", 1], ["c", 2]]) expect(paginator.paginate(list, 4).to_a).to eq([["d", 3], ["e", 4], ["f", 5]]) expect(paginator.paginate(list, 5).to_a).to eq([["d", 3], ["e", 4], ["f", 5]]) expect(paginator.paginate(list, 6).to_a).to eq([["d", 3], ["e", 4], ["f", 5]]) expect(paginator.paginate(list, 7).to_a).to eq([["d", 3], ["e", 4], ["f", 5]]) end it "paginates items not matching per_page count" do list = %w[a b c d e f g] paginator = described_class.new(per_page: 3) expect(paginator.paginate(list, 1).to_a).to eq([["a", 0], ["b", 1], ["c", 2]]) expect(paginator.paginate(list, 2).to_a).to eq([["a", 0], ["b", 1], ["c", 2]]) expect(paginator.paginate(list, 3).to_a).to eq([["a", 0], ["b", 1], ["c", 2]]) expect(paginator.paginate(list, 4).to_a).to eq([["d", 3], ["e", 4], ["f", 5]]) expect(paginator.paginate(list, 5).to_a).to eq([["d", 3], ["e", 4], ["f", 5]]) expect(paginator.paginate(list, 6).to_a).to eq([["d", 3], ["e", 4], ["f", 5]]) expect(paginator.paginate(list, 7).to_a).to eq([["g", 6]]) expect(paginator.paginate(list, 8).to_a).to eq([["g", 6]]) end it "finds both start and end index for current selection" do list = %w[a b c d e f g] paginator = described_class.new(per_page: 3, default: 0) paginator.paginate(list, 3) expect(paginator.start_index).to eq(0) expect(paginator.end_index).to eq(2) paginator.paginate(list, 4) expect(paginator.start_index).to eq(3) expect(paginator.end_index).to eq(5) paginator.paginate(list, 5) expect(paginator.start_index).to eq(3) expect(paginator.end_index).to eq(5) paginator.paginate(list, 7) expect(paginator.start_index).to eq(6) expect(paginator.end_index).to eq(6) end it "starts with default selection" do list = %w[a b c d e f g] paginator = described_class.new(per_page: 3, default: 3) expect(paginator.paginate(list, 4).to_a).to eq([["d", 3], ["e", 4], ["f", 5]]) end it "doesn't accept invalid pagination" do list = %w[a b c d e f g] paginator = described_class.new(per_page: 0) expect { paginator.paginate(list, 4) }.to raise_error(TTY::Prompt::InvalidArgument, /per_page must be > 0/) end end tty-prompt-0.23.1/spec/unit/choice/000077500000000000000000000000001403662044600171045ustar00rootroot00000000000000tty-prompt-0.23.1/spec/unit/choice/eql_spec.rb000066400000000000000000000014471403662044600212320ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Choice, "#==" do it "is true with the same name and value attributes" do expect(described_class.new(:large, 1)) .to eq(described_class.new(:large, 1)) end it "is false with different name attribute" do expect(described_class.new(:large, 1)) .not_to eq(described_class.new(:medium, 1)) end it "is false with different value attribute" do expect(described_class.new(:large, 1)) .not_to eq(described_class.new(:large, 2)) end it "is false with different key attribute" do expect(described_class.new(:large, 1, key: "j")) .not_to eq(described_class.new(:large, 2, key: "k")) end it "is false with non-choice object" do expect(described_class.new(:large, 1)).not_to eq(:other) end end tty-prompt-0.23.1/spec/unit/choice/from_spec.rb000066400000000000000000000110461403662044600214100ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Choice, "#from" do it "skips Choice instance" do choice = described_class.new(:large, 1) expect(described_class.from(choice)).to eq(choice) end it "creates choice from a string" do expected_choice = described_class.new("large", "large") choice = described_class.from("large") expect(choice).to eq(expected_choice) expect(choice.name).to eq("large") expect(choice.value).to eq("large") end it "creates choice from a symbol" do expected_choice = described_class.new(:large, :large) choice = described_class.from(:large) expect(choice).to eq(expected_choice) expect(choice.name).to eq(:large) expect(choice.value).to eq(:large) end it "creates choice from an array with name only and defaults value" do expected_choice = described_class.new("large", "large") choice = described_class.from([:large]) expect(choice).to eq(expected_choice) expect(choice.name).to eq("large") expect(choice.value).to eq("large") end it "creates choice from an array with name and a value" do expected_choice = described_class.new("large", 1) choice = described_class.from([:large, 1]) expect(choice).to eq(expected_choice) expect(choice.name).to eq("large") expect(choice.value).to eq(1) end it "creates choice from an array with name and false value" do expected_choice = described_class.new("large", false) choice = described_class.from([:large, false]) expect(choice).to eq(expected_choice) expect(choice.name).to eq("large") expect(choice.value).to eq(false) end it "creates choice from array with name and nil value" do expected_choice = described_class.new("none", nil) choice = described_class.from([:none, nil]) expect(choice).to eq(expected_choice) expect(choice.name).to eq("none") expect(choice.value).to eq(nil) end it "creates choice from a hash with a value" do expected_choice = described_class.new("large", 1) choice = described_class.from({large: 1}) expect(choice).to eq(expected_choice) expect(choice.name).to eq("large") expect(choice.value).to eq(1) end it "creates choice from a hash with a nil value" do expected_choice = described_class.new("large", nil) choice = described_class.from({large: nil}) expect(choice).to eq(expected_choice) expect(choice.name).to eq("large") expect(choice.value).to eq(nil) end it "creates choice from an array with key-value pair" do expected_choice = described_class.new("large", 1) choice = described_class.from([{"large" => 1}]) expect(choice).to eq(expected_choice) expect(choice.name).to eq("large") expect(choice.value).to eq(1) end it "creates choice from an array with a hash with name and value keys" do expected_choice = described_class.new("large", 1) choice = described_class.from([{name: "large", value: 1}]) expect(choice).to eq(expected_choice) expect(choice.name).to eq("large") expect(choice.value).to eq(1) end it "creates choice from an array with a hash without value key" do expected_choice = described_class.new("large", "large") choice = described_class.from([{name: "large"}]) expect(choice).to eq(expected_choice) expect(choice.name).to eq("large") expect(choice.value).to eq("large") end it "creates choice from a hash with name, value and key keys" do default = {key: "h", name: "Help", value: :help} expected_choice = described_class.new("Help", :help, key: "h") choice = described_class.from(default) expect(choice).to eq(expected_choice) expect(choice.name).to eq("Help") expect(choice.value).to eq(:help) expect(choice.disabled?).to eq(false) end it "creates disabled choice" do expected_choice = described_class.new("Disabled", :none, disabled: true) choice = described_class.from({ name: "Disabled", value: :none, disabled: "unavailable" }) expect(choice).to eq(expected_choice) expect(choice.name).to eq("Disabled") expect(choice.value).to eq(:none) expect(choice.disabled?).to eq(true) end it "creates choice from an arbitrary object that responds to to_s call" do stub_const("Size", Class.new do def to_s "large" end end) size = Size.new expected_choice = described_class.new(size, size) choice = described_class.from(size) expect(choice).to eq(expected_choice) expect(choice.name).to eq(size) expect(choice.value).to eq(size) expect(choice.disabled?).to eq(false) end end tty-prompt-0.23.1/spec/unit/choices/000077500000000000000000000000001403662044600172675ustar00rootroot00000000000000tty-prompt-0.23.1/spec/unit/choices/add_spec.rb000066400000000000000000000005301403662044600213540ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Choices, "#<<" do it "adds choice to collection" do choices = described_class.new expect(choices).to be_empty choice = TTY::Prompt::Choice.from([:label, 1]) choices << [:label, 1] expect(choices.size).to eq(1) expect(choices.to_ary).to eq([choice]) end end tty-prompt-0.23.1/spec/unit/choices/each_spec.rb000066400000000000000000000005501403662044600215260ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Choices, "#each" do it "iterates over collection" do choices = described_class[:large, :medium, :small] actual = [] choices.each do |choice| actual << choice.name end expect(actual).to eq(%i[large medium small]) expect(choices.each).to be_kind_of(Enumerator) end end tty-prompt-0.23.1/spec/unit/choices/enabled_spec.rb000066400000000000000000000006071403662044600222230ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Choices, "#enabled" do it "returns only choices which aren't disabled" do enabled_choice = TTY::Prompt::Choice.new :foo, :bar disabled_choice = TTY::Prompt::Choice.new :fizz, :bazz, disabled: true choices = described_class[enabled_choice, disabled_choice] expect(choices.enabled).to eq [enabled_choice] end end tty-prompt-0.23.1/spec/unit/choices/find_by_spec.rb000066400000000000000000000021011403662044600222320ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Choices, "#find_by" do let(:collection) { [ {name: "large", value: "L", key: "l"}, {name: "medium", value: "M", key: "m"}, {name: "small", value: "S", key: "s"} ] } it "finds no matching choice" do choices = described_class[*collection] expect(choices.find_by(:name, "unknown")).to eq(nil) end it "finds a matching choice by :name key" do choice = TTY::Prompt::Choice.from({name: "small", value: "S", key: "s"}) choices = described_class[*collection] expect(choices.find_by(:name, "small")).to eq(choice) end it "finds a matching choice by :value key" do choice = TTY::Prompt::Choice.from({name: "medium", value: "M", key: "m"}) choices = described_class[*collection] expect(choices.find_by(:value, "M")).to eq(choice) end it "finds a matching choice by :key key" do choice = TTY::Prompt::Choice.from({name: "large", value: "L", key: "l"}) choices = described_class[*collection] expect(choices.find_by(:key, "l")).to eq(choice) end end tty-prompt-0.23.1/spec/unit/choices/new_spec.rb000066400000000000000000000005011403662044600214130ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Choices, ".new" do it "creates choices collection" do choice1 = TTY::Prompt::Choice.from(:label1) choice2 = TTY::Prompt::Choice.from(:label2) collection = described_class[:label1, :label2] expect(collection).to eq([choice1, choice2]) end end tty-prompt-0.23.1/spec/unit/choices/pluck_spec.rb000066400000000000000000000004561403662044600217510ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Choices, "#pluck" do it "plucks choice by key name" do collection = [{name: "large"}, {name: "medium"}, {name: "small"}] choices = described_class[*collection] expect(choices.pluck(:name)).to eq(%w[large medium small]) end end tty-prompt-0.23.1/spec/unit/collect_spec.rb000066400000000000000000000044731403662044600206460ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, "#collect" do subject(:prompt) { TTY::Prompt::Test.new } def collect(&block) prompt = subject count = 0 result = prompt.collect do while prompt.yes?("continue?") instance_eval(&block) count += 1 end end result[:count] = count result end context "when receiving multiple answers" do let(:colors) { %w[red blue yellow] } before do subject.input << "y\r" + colors.join("\ry\r") + "\rn\r" subject.input.rewind end it "collects as a list if values method used in chain" do result = collect { key(:colors).values.ask("color:") } expect(result[:count]).to eq(3) expect(result[:colors]).to eq(colors) end it "collects as a list if values method used in chain with block" do result = collect do key(:colors).values { key(:name).ask("color:") } end expect(result[:count]).to eq(3) expect(result[:colors]).to eq(colors.map { |c| {name: c} }) end context "with multiple keys" do let(:colors) { ["red\rblue", "yellow\rgreen"] } let(:expected_pairs) do colors.map { |s| Hash[%i[hot cold].zip(s.split("\r"))] } end it "collects into the appropriate keys" do result = collect do key(:pairs).values do key(:hot).ask("color:") key(:cold).ask("color:") end end expect(result[:count]).to eq(2) expect(result[:pairs]).to eq(expected_pairs) end end it "overrides a non-array key on multiple answers" do result = collect { key(:colors).ask("color:") } expect(result[:colors]).to eq(colors.last) expect(result[:count]).to eq(3) end end it "collects more than one answer" do prompt.input << "Piotr\r30\rStreet\rCity\r123\r" prompt.input.rewind result = prompt.collect do key(:name).ask("Name?") key(:age).ask("Age?", convert: :int) key(:address) do key(:street).ask("Street?", required: true) key(:city).ask("City?") key(:zip).ask("Zip?", validate: /\A\d{3}\Z/) end end expect(result).to include({ name: "Piotr", age: 30, address: { street: "Street", city: "City", zip: "123" } }) end end tty-prompt-0.23.1/spec/unit/converter_registry_spec.rb000066400000000000000000000040221403662044600231460ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::ConverterRegistry do context "contain" do it "doesn't have conversion" do registry = described_class.new expect(registry.contain?(:unknown)).to eq(false) end it "contains conversion" do registry = described_class.new(foo: ->(val) { val }) expect(registry.contain?(:foo)).to eq(true) end it "checks conversion with object type" do registry = described_class.new(integer: ->(val) { val }) expect(registry.contain?(Integer)).to eq(true) end end context "register" do it "registers new conversion under single name" do registry = described_class.new registry.register(:foo) { |val| val } expect(registry.contain?(:foo)).to eq(true) end it "registers new conversion under multiple names" do registry = described_class.new registry.register(:foo, :fum) { |val| val } expect(registry.contain?(:foo)).to eq(true) expect(registry.contain?(:fum)).to eq(true) end it "fails to register conversion" do registry = described_class.new(foo: ->(val) { val }) expect { registry.register(:foo) { "value2" } }.to raise_error(TTY::Prompt::ConversionAlreadyDefined, "converter for :foo is already registered") end end context "fetch" do it "retrieves converter from the registry" do conversion = ->(val) { val } registry = described_class.new(foo: conversion) expect(registry[:foo]).to eq(conversion) expect(registry.fetch(:foo)).to eq(conversion) end it "retrieves uppcase named converter" do conversion = ->(val) { val } registry = described_class.new(foo: conversion) expect(registry["FOO"]).to eq(conversion) end it "fails to retrieve conversion" do registry = described_class.new expect { registry[:foo] }.to raise_error(TTY::Prompt::UnsupportedConversion, "converter :foo is not registered") end end end tty-prompt-0.23.1/spec/unit/converters/000077500000000000000000000000001403662044600200445ustar00rootroot00000000000000tty-prompt-0.23.1/spec/unit/converters/convert_array_spec.rb000066400000000000000000000013371403662044600242650ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "convert to array" do subject(:prompt) { TTY::Prompt::Test.new } it "converts answer to an array" do prompt.input << "a,b,c" prompt.input.rewind answer = prompt.ask("Tags?", convert: :list) expect(answer).to eq(%w[a b c]) end it "converts answer to an array of integers" do prompt.input << "1,2,3" prompt.input.rewind answer = prompt.ask("Numbers?", convert: :integers) expect(answer).to eq([1, 2, 3]) end it "converts answer to an array of booleans" do prompt.input << "t,f,t" prompt.input.rewind answer = prompt.ask("Numbers?", convert: :bool_list) expect(answer).to eq([true, false, true]) end end tty-prompt-0.23.1/spec/unit/converters/convert_bool_spec.rb000066400000000000000000000034331403662044600241010ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "convert bool" do subject(:prompt) { TTY::Prompt::Test.new } it "fails to convert boolean" do prompt.input << "x" prompt.input.rewind prompt.ask("Do you read books?", convert: :bool) expect(prompt.output.string).to eq([ "Do you read books? ", "\e[2K\e[1G", "Do you read books? x", "\e[31m>>\e[0m Cannot convert `x` to 'bool' type", "\e[1A\e[2K\e[1G", "Do you read books? ", "\e[2K\e[1G\e[1A\e[2K\e[1G", "Do you read books? \n" ].join) end it "handles default values" do prompt.input << "\n" prompt.input.rewind response = prompt.ask("Do you read books?", convert: :bool, default: true) expect(response).to eql(true) expect(prompt.output.string).to eq([ "Do you read books? \e[90m(true)\e[0m ", "\e[2K\e[1GDo you read books? \e[90m(true)\e[0m \n", "\e[1A\e[2K\e[1G", "Do you read books? \e[32mtrue\e[0m\n" ].join) end it "handles default values" do prompt.input << "\n" prompt.input.rewind response = prompt.ask("Do you read books?") { |q| q.default true q.convert :bool } expect(response).to eq(true) end it "converts negative boolean" do prompt.input << "No" prompt.input.rewind response = prompt.ask("Do you read books?", convert: :bool) expect(response).to eq(false) end it "converts positive boolean" do prompt.input << "Yes" prompt.input.rewind response = prompt.ask("Do you read books?", convert: :bool) expect(response).to eq(true) end it "converts single positive boolean" do prompt.input << "y" prompt.input.rewind response = prompt.ask("Do you read books?", convert: :bool) expect(response).to eq(true) end end tty-prompt-0.23.1/spec/unit/converters/convert_char_spec.rb000066400000000000000000000005051403662044600240600ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "convert char" do it "reads single character" do prompt = TTY::Prompt::Test.new prompt.input << "abcde" prompt.input.rewind response = prompt.ask("What is your favourite letter?", convert: :char) expect(response).to eq("a") end end tty-prompt-0.23.1/spec/unit/converters/convert_custom_spec.rb000066400000000000000000000006621403662044600244610ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "convert custom" do subject(:prompt) { TTY::Prompt::Test.new } it "converts response with custom conversion" do prompt.input << "one,two,three\n" prompt.input.rewind conversion = proc { |input| input.split(/,\s*/) } answer = prompt.ask("Ingredients? (comma sep list)", convert: conversion) expect(answer).to eq(%w[one two three]) end end tty-prompt-0.23.1/spec/unit/converters/convert_date_spec.rb000066400000000000000000000023041403662044600240570ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "convert date" do subject(:prompt) { TTY::Prompt::Test.new } it "fails to convert date" do prompt.input << "x" prompt.input.rewind prompt.ask("When were you born?", convert: :date) expect(prompt.output.string).to eq([ "When were you born? ", "\e[2K\e[1G", "When were you born? x", "\e[31m>>\e[0m Cannot convert `x` to 'date' type", "\e[1A\e[2K\e[1G", "When were you born? ", "\e[2K\e[1G\e[1A\e[2K\e[1G", "When were you born? \n" ].join) end it "converts date" do prompt.input << "20th April 1887" prompt.input.rewind response = prompt.ask("When were your born?", convert: :date) expect(response).to be_kind_of(Date) expect(response.day).to eq(20) expect(response.month).to eq(4) expect(response.year).to eq(1887) end it "converts datetime" do prompt.input << "20th April 1887" prompt.input.rewind response = prompt.ask("When were your born?", convert: :datetime) expect(response).to be_kind_of(DateTime) expect(response.day).to eq(20) expect(response.month).to eq(4) expect(response.year).to eq(1887) end end tty-prompt-0.23.1/spec/unit/converters/convert_file_spec.rb000066400000000000000000000007351403662044600240670ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "convert file" do it "converts to file" do ::File.write("test.txt", "foobar") prompt = TTY::Prompt::Test.new prompt.input << "test.txt" prompt.input.rewind answer = prompt.ask("Which file to open?", convert: :file) expect(::File.basename(answer)).to eq("test.txt") expect(::File.read(answer)).to eq("foobar") ::File.unlink("test.txt") unless Gem.win_platform? end end tty-prompt-0.23.1/spec/unit/converters/convert_hash_spec.rb000066400000000000000000000014271403662044600240720ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "convert to hash" do subject(:prompt) { TTY::Prompt::Test.new } it "converts answer to a hash" do prompt.input << "a:1 b:2 c:3" prompt.input.rewind answer = prompt.ask("Options?", convert: :map) expect(answer).to eq({a: "1", b: "2", c: "3"}) end it "converts answer to a hash of integer values" do prompt.input << "a:1 b:2 c:3" prompt.input.rewind answer = prompt.ask("Options?", convert: :int_map) expect(answer).to eq({a: 1, b: 2, c: 3}) end it "converts answer to a hash of boolean values" do prompt.input << "a:t b:f c:t" prompt.input.rewind answer = prompt.ask("Options?", convert: :bool_map) expect(answer).to eq({a: true, b: false, c: true}) end end tty-prompt-0.23.1/spec/unit/converters/convert_number_spec.rb000066400000000000000000000026741403662044600244440ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "convert numbers" do subject(:prompt) { TTY::Prompt::Test.new } it "fails to convert integer" do prompt.input << "x" prompt.input.rewind prompt.ask("What temperature?", convert: :int) expect(prompt.output.string).to eq([ "What temperature? ", "\e[2K\e[1G", "What temperature? x", "\e[31m>>\e[0m Cannot convert `x` to 'int' type", "\e[1A\e[2K\e[1G", "What temperature? ", "\e[2K\e[1G\e[1A\e[2K\e[1G", "What temperature? \n" ].join) end it "converts integer" do prompt.input << 35 prompt.input.rewind answer = prompt.ask("What temperature?", convert: :int) expect(answer).to be_a(Integer) expect(answer).to eq(35) end it "fails to convert float" do prompt.input << "x" prompt.input.rewind prompt.ask("How tall are you?", convert: :float) expect(prompt.output.string).to eq([ "How tall are you? ", "\e[2K\e[1G", "How tall are you? x", "\e[31m>>\e[0m Cannot convert `x` to 'float' type", "\e[1A\e[2K\e[1G", "How tall are you? ", "\e[2K\e[1G\e[1A\e[2K\e[1G", "How tall are you? \n" ].join) end it "converts float" do number = 6.666 prompt.input << number prompt.input.rewind answer = prompt.ask("How tall are you?", convert: :float) expect(answer).to be_a(Float) expect(answer).to eq(number) end end tty-prompt-0.23.1/spec/unit/converters/convert_path_spec.rb000066400000000000000000000005341403662044600241010ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "convert path" do subject(:prompt) { TTY::Prompt::Test.new } it "converts pathname" do prompt.input << "/foo/bar/baz" prompt.input.rewind answer = prompt.ask("File location?", convert: :path) expect(answer).to eql(::Pathname.new("/foo/bar/baz")) end end tty-prompt-0.23.1/spec/unit/converters/convert_range_spec.rb000066400000000000000000000014451403662044600242430ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "convert range" do subject(:prompt) { TTY::Prompt::Test.new } it "converts with valid range" do prompt.input << "20-30" prompt.input.rewind answer = prompt.ask("Which age group?", convert: :range) expect(answer).to be_a(Range) expect(answer).to eq(20..30) end it "fails to convert to range" do prompt.input << "x" prompt.input.rewind prompt.ask("Which age group?", convert: :range) expect(prompt.output.string).to eq([ "Which age group? ", "\e[2K\e[1G", "Which age group? x", "\e[31m>>\e[0m Cannot convert `x` to 'range' type", "\e[1A\e[2K\e[1G", "Which age group? ", "\e[2K\e[1G\e[1A\e[2K\e[1G", "Which age group? \n" ].join) end end tty-prompt-0.23.1/spec/unit/converters/convert_regex_spec.rb000066400000000000000000000005161403662044600242570ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "convert regexp" do it "converts regex" do prompt = TTY::Prompt::Test.new prompt.input << "[a-z]*" prompt.input.rewind answer = prompt.ask("Regex?", convert: :regexp) expect(answer).to be_a(Regexp) expect(answer).to eq(/[a-z]*/) end end tty-prompt-0.23.1/spec/unit/converters/convert_string_spec.rb000066400000000000000000000011251403662044600244500ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "convert string" do it "converts string" do prompt = TTY::Prompt::Test.new prompt.input << "Piotr" prompt.input.rewind answer = prompt.ask("What is your name?", convert: :string) expect(answer).to be_a(String) expect(answer).to eq("Piotr") end it "converts symbol" do prompt = TTY::Prompt::Test.new prompt.input << "Piotr" prompt.input.rewind answer = prompt.ask("What is your name?", convert: :symbol) expect(answer).to be_a(Symbol) expect(answer).to eq(:Piotr) end end tty-prompt-0.23.1/spec/unit/converters_spec.rb000066400000000000000000000143601403662044600214070ustar00rootroot00000000000000# frozen_string_literal: true require "pathname" require "date" require "time" require "uri" RSpec.describe TTY::Prompt::Converters do context ":boolean" do { "t" => true, "true" => true, "y" => true, "YES" => true, "1" => true, "f" => false, "FALSE" => false, "no" => false, "0" => false, "unknown" => TTY::Prompt::Const::Undefined }.each do |input, value| it "converts #{input.inspect} to #{value.inspect}" do expect(described_class.convert(:bool, input)).to eq(value) end end end context ":string" do { "" => "", "input\n" => "input" }.each do |input, value| it "converts #{input.inspect} to #{value.inspect}" do expect(described_class.convert(:str, input)).to eq(value) end end end context ":char" do { "" => nil, "input" => "i" }.each do |input, value| it "converts #{input.inspect} to #{value.inspect}" do expect(described_class.convert(:char, input)).to eq(value) end end end context ":date" do { "2020/05/21" => ::Date.parse("2020/05/21"), "unknown" => TTY::Prompt::Const::Undefined }.each do |input, value| it "converts #{input.inspect} to #{value.inspect}" do expect(described_class.convert(:date, input)).to eq(value) end end end context ":datetime" do { "2020/05/21 11:12:13" => ::DateTime.parse("2020/05/21 11:12:13"), "unknown" => TTY::Prompt::Const::Undefined }.each do |input, value| it "converts #{input.inspect} to #{value.inspect}" do expect(described_class.convert(:datetime, input)).to eq(value) end end end context ":time" do { "11:12:13" => ::Time.parse("11:12:13"), "unknown" => TTY::Prompt::Const::Undefined }.each do |input, value| it "converts #{input.inspect} to #{value.inspect}" do expect(described_class.convert(:time, input)).to eq(value) end end end context ":integer" do { "12" => 12, "unknown" => TTY::Prompt::Const::Undefined }.each do |input, value| it "converts #{input.inspect} to #{value.inspect}" do expect(described_class.convert(:int, input)).to eq(value) end end end context ":float" do { "12.3" => 12.3, "unknown" => TTY::Prompt::Const::Undefined }.each do |input, value| it "converts #{input.inspect} to #{value.inspect}" do expect(described_class.convert(:float, input)).to eq(value) end end end context ":range" do { 1..10 => 1..10, "1" => 1..1, "1.0" => 1.0..1.0, "1-10" => 1..10, "-5--1" => -5..-1, "1.2-5.0" => 1.2..5.0, "1 , 10" => 1..10, "1..10" => 1..10, "1...10" => 1...10, "1 . . . 10" => 1...10, "a..z" => "a".."z", "a . . . z" => "a"..."z", "unknown" => TTY::Prompt::Const::Undefined }.each do |input, value| it "converts #{input.inspect} to #{value.inspect}" do expect(described_class.convert(:range, input)).to eq(value) end end end context ":regexp" do { '\d+' => /\d+/, "unknown" => /unknown/ }.each do |input, value| it "converts #{input.inspect} to #{value.inspect}" do expect(described_class.convert(:regexp, input)).to eq(value) end end end context ":path" do { "/foo/bar/baz" => ::Pathname.new("/foo/bar/baz") }.each do |input, value| it "converts #{input.inspect} to #{value.inspect}" do expect(described_class.convert(:path, input)).to eq(value) end end end context ":uri" do { "http://foobar.com" => ::URI.parse("http://foobar.com") }.each do |input, value| it "converts #{input.inspect} to #{value.inspect}" do expect(described_class.convert(:uri, input)).to eq(value) end end end context ":array/:list" do { ",," => [], ",b,c" => %w[b c], "a,b,c" => %w[a b c], "a , b , c" => %w[a b c], "a, , c" => %w[a c], "a, b\\, c" => ["a", "b, c"], %w[a b c] => %w[a b c] }.each do |input, obj| it "converts #{input.inspect} to #{obj.inspect}" do expect(described_class.convert(:array, input)).to eq(obj) end end { [:int_list, "1,2,3"] => [1, 2, 3], [:int_array, "1,2,3"] => [1, 2, 3], [:ints, "1,2,3"] => [1, 2, 3], [:integers, "1,2,3"] => [1, 2, 3], [:float_list, "1,2,3"] => [1.0, 2.0, 3.0], [:floats, "1,2,3"] => [1.0, 2.0, 3.0], [:bool_list, "t,t,f"] => [true, true, false], [:bools, "t,t,f"] => [true, true, false], [:booleans, "t,t,f"] => [true, true, false], [:symbols, "a,b,c"] => %i[a b c], [:sym_list, "a,b,c"] => %i[a b c], [:regexps, "a,b,c"] => [/a/, /b/, /c/] }.each do |(type, input), obj| it "converts #{input.inspect} to #{obj.inspect}" do expect(described_class.convert(type, input)).to eq(obj) end end end context ":hash/:map" do { "" => {}, "a=1" => {a: "1"}, "a=1&b=2" => {a: "1", b: "2"}, "a=&b=2" => {a: "", b: "2"}, "a=1&b=2&a=3" => {a: %w[1 3], b: "2"}, "a:1 b:2" => {a: "1", b: "2"}, "a:1 b:2 a:3" => {a: %w[1 3], b: "2"}, %w[a:1 b:2 c:3] => {a: "1", b: "2", c: "3"}, %w[a=1 b=2 c=3] => {a: "1", b: "2", c: "3"} }.each do |input, obj| it "converts #{input.inspect} to #{obj.inspect}" do expect(described_class.convert(:hash, input)).to eq(obj) end end { [:int_map, "a:1 b:2 c:3"] => {a: 1, b: 2, c: 3}, [:integer_hash, "a:1 b:2 c:3"] => {a: 1, b: 2, c: 3}, [:str_int_map, "a:1 b:2 c:3"] => {"a" => 1, "b" => 2, "c" => 3}, [:string_integer_hash, "a:1 b:2 c:3"] => {"a" => 1, "b" => 2, "c" => 3}, [:float_map, "a:1 b:2 c:3"] => {a: 1.0, b: 2.0, c: 3.0}, [:bool_map, "a:t b:f c:t"] => {a: true, b: false, c: true}, [:boolean_hash, "a:t b:f c:t"] => {a: true, b: false, c: true}, [:symbol_map, "a:t b:f c:t"] => {a: :t, b: :f, c: :t}, [:regexp_map, "a:t b:f c:t"] => {a: /t/, b: /f/, c: /t/} }.each do |(type, input), obj| it "converts #{input.inspect} to #{obj.inspect}" do expect(described_class.convert(type, input)).to eq(obj) end end end end tty-prompt-0.23.1/spec/unit/decorate_spec.rb000066400000000000000000000022401403662044600207750ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, "#decorate" do it "doesn't decorate empty string" do prompt = described_class.new(enable_color: true) expect(prompt.decorate(" \n ")).to eq(" \n ") end it "doesn't decorate when disabled" do prompt = described_class.new(enable_color: false) expect(prompt.decorate("string", :green)).to eq("string") end it "doesn't decorate without additional arguments" do prompt = described_class.new(enable_color: true) expect(prompt.decorate("string")).to eq("string") end it "decorates with a callable object" do prompt = described_class.new callable = Pastel.new(enabled: true).green.on_red.detach expect(prompt.decorate("string", callable)).to eq("\e[32;41mstring\e[0m") end it "decorates with a proc" do prompt = described_class.new proc_obj = ->(str) { Pastel.new(enabled: true).green(str) } expect(prompt.decorate("string", proc_obj)).to eq("\e[32mstring\e[0m") end it "decorates string with named colors" do prompt = described_class.new(enable_color: true) expect(prompt.decorate("string", :green, :on_red)).to eq("\e[32;41mstring\e[0m") end end tty-prompt-0.23.1/spec/unit/distance/000077500000000000000000000000001403662044600174445ustar00rootroot00000000000000tty-prompt-0.23.1/spec/unit/distance/distance_spec.rb000066400000000000000000000025571403662044600226060ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Distance, ".distance" do let(:object) { described_class.new } subject(:distance) { object.distance(*strings) } context "when nil" do let(:strings) { [nil, nil] } it { is_expected.to eql(0) } end context "when empty" do let(:strings) { ["", ""] } it { is_expected.to eql(0) } end context "with one non empty" do let(:strings) { ["abc", ""] } it { is_expected.to eql(3) } end context "when single char" do let(:strings) { %w[a abc] } it { is_expected.to eql(2) } end context "when similar" do let(:strings) { %w[abc abc] } it { is_expected.to eql(0) } end context "when similar" do let(:strings) { %w[abc acb] } it { is_expected.to eql(1) } end context "when end similar" do let(:strings) { %w[saturday sunday] } it { is_expected.to eql(3) } end context "when contain similar" do let(:strings) { %w[which witch] } it { is_expected.to eql(2) } end context "when prefix" do let(:strings) { %w[sta status] } it { is_expected.to eql(3) } end context "when similar" do let(:strings) { %w[smellyfish jellyfish] } it { is_expected.to eql(2) } end context "when unicode" do let(:strings) { %w[マラソン五輪代表 ララソン五輪代表] } it { is_expected.to eql(1) } end end tty-prompt-0.23.1/spec/unit/enum_select_spec.rb000066400000000000000000000440061403662044600215200ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt do let(:symbols) { TTY::Prompt::Symbols.symbols } subject(:prompt) { TTY::Prompt::Test.new } def output_helper(prompt, choices, active, options = {}) enum = options.fetch(:enum, ")") input = options[:input] error = options[:error] default = options.fetch(:default, 1) out = [] out << prompt << " \n" out << choices.map.with_index do |c, i| name = c.is_a?(Hash) ? c[:name] : c disabled = c.is_a?(Hash) ? c[:disabled] : false num = (i + 1).to_s + enum if disabled "\e[31m#{symbols[:cross]}\e[0m #{num} #{name} #{disabled}" elsif name == active " \e[32m#{num} #{name}\e[0m" else " #{num} #{name}" end end.join("\n") out << "\n" choice = " Choose 1-#{choices.count} [#{default}]: " choice += input.to_s if input out << choice if error out << "\n" out << "\e[31m>>\e[0m #{error}" out << "\e[A\e[1G\e[#{choice.size}C" end out << "\e[2K\e[1G\e[1A" * (choices.count + 1) out << "\e[2K\e[1G\e[J" out.join end def exit_message(prompt, choice) "#{prompt} \e[32m#{choice}\e[0m\n" end it "raises configuration error when default is higher than number of choices" do choices = %w[/bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny] expect { prompt.enum_select("Select an editor?", choices, default: 100) }.to raise_error(TTY::Prompt::ConfigurationError, /default index 100 out of range \(1 - 3\)/) end it "selects default choice by name" do choices = %w[/bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny] prompt.input << "\n" prompt.input.rewind answer = prompt.enum_select("Select an editor?", choices, default: "/usr/bin/vim.basic") expect(answer).to eq("/usr/bin/vim.basic") end it "raises when default choice name is not found" do choices = %w[/bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny] expect { prompt.enum_select("Select an editor?", choices, default: "unknown") }.to raise_error(TTY::Prompt::ConfigurationError, "no choice found for the default name: \"unknown\"") end it "raises when default name matches a disabled choice" do choices = [{name: "/bin/nano", disabled: "unavailable"}, "/usr/bin/vim.basic", "/usr/bin/vim.tiny"] expect { prompt.enum_select("Select an editor?", choices, default: "/bin/nano") }.to raise_error(TTY::Prompt::ConfigurationError, "default name \"/bin/nano\" matches disabled choice") end it "selects default option when return pressed immediately" do choices = %w[/bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny] prompt.input << "\n" prompt.input.rewind answer = prompt.enum_select("Select an editor?", choices) expect(answer).to eq("/bin/nano") expected_output = [ output_helper("Select an editor?", choices, "/bin/nano"), exit_message("Select an editor?", "/bin/nano") ].join expect(prompt.output.string).to eq(expected_output) end it "selects option by index from the list" do choices = %w[/bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny] prompt.input << "3\n" prompt.input.rewind answer = prompt.enum_select("Select an editor?", choices, default: 2) expect(answer).to eq("/usr/bin/vim.tiny") expected_output = [ output_helper("Select an editor?", choices, "/usr/bin/vim.basic", default: 2), output_helper("Select an editor?", choices, "/usr/bin/vim.tiny", default: 2, input: "3"), exit_message("Select an editor?", "/usr/bin/vim.tiny") ].join expect(prompt.output.string).to eq(expected_output) end it "selects option through DSL" do choices = %w[/bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny] prompt.input << "1\n" prompt.input.rewind answer = prompt.enum_select("Select an editor?") do |menu| menu.default 2 menu.enum "." menu.choice "/bin/nano" menu.choice "/usr/bin/vim.basic" menu.choice "/usr/bin/vim.tiny" end expect(answer).to eq("/bin/nano") expected_output = [ output_helper("Select an editor?", choices, "/usr/bin/vim.basic", default: 2, enum: "."), output_helper("Select an editor?", choices, "/bin/nano", default: 2, enum: ".", input: 1), exit_message("Select an editor?", "/bin/nano") ].join expect(prompt.output.string).to eq(expected_output) end it "sets choice value to nil through DSL" do choices = %w[none /bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny] prompt.input << "\n" prompt.input.rewind answer = prompt.enum_select("Select an editor?") do |menu| menu.default 1 menu.choice "none", nil menu.choice "/bin/nano" menu.choice "/usr/bin/vim.basic" menu.choice "/usr/bin/vim.tiny" end expect(answer).to eq(nil) expected_output = [ output_helper("Select an editor?", choices, "none", default: 1), exit_message("Select an editor?", "none") ].join expect(prompt.output.string).to eq(expected_output) end it "selects option through DSL with key and value" do choices = %w[nano vim emacs] prompt.input << "\n" prompt.input.rewind answer = prompt.enum_select("Select an editor?") do |menu| menu.default 2 menu.choice :nano, "/bin/nano" menu.choice :vim, "/usr/bin/vim" menu.choice :emacs, "/usr/bin/emacs" end expect(answer).to eq("/usr/bin/vim") expected_output = [ output_helper("Select an editor?", choices, "vim", default: 2), exit_message("Select an editor?", "vim") ].join expect(prompt.output.string).to eq(expected_output) end it "changes colors for selection, hint and error" do choices = %w[/bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny] prompt.input << "\n" prompt.input.rewind options = {active_color: :red, help_color: :blue, error_color: :green} answer = prompt.enum_select("Select an editor?", choices, options) expect(answer).to eq("/bin/nano") expected_output = [ "Select an editor? \n", " \e[31m1) /bin/nano\e[0m\n", " 2) /usr/bin/vim.basic\n", " 3) /usr/bin/vim.tiny\n", " Choose 1-3 [1]: ", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "Select an editor? \e[31m/bin/nano\e[0m\n" ].join expect(prompt.output.string).to eq(expected_output) end it "changes global symbols" do prompt = TTY::Prompt::Test.new(symbols: {cross: "x"}) choices = ["A", {name: "B", disabled: "(out)"}, "C"] prompt.input << "\n" prompt.input.rewind answer = prompt.enum_select("What letter?", choices) expect(answer).to eq("A") expected_output = [ "What letter? \n", " \e[32m1) A\e[0m\n", "\e[31mx\e[0m 2) B (out)\n", " 3) C\n", " Choose 1-3 [1]: ", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \e[32mA\e[0m\n" ].join expect(prompt.output.string).to eq(expected_output) end it "changes global symbols through DSL" do choices = ["A", {name: "B", disabled: "(out)"}, "C"] prompt.input << "\n" prompt.input.rewind answer = prompt.enum_select("What letter?") do |menu| menu.symbols cross: "x" menu.choices choices end expect(answer).to eq("A") expected_output = [ "What letter? \n", " \e[32m1) A\e[0m\n", "\e[31mx\e[0m 2) B (out)\n", " 3) C\n", " Choose 1-3 [1]: ", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \e[32mA\e[0m\n" ].join expect(prompt.output.string).to eq(expected_output) end it "sets quiet mode" do choices = %w[/bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny] prompt = TTY::Prompt::Test.new(quiet: true) prompt.input << "\n" prompt.input.rewind answer = prompt.enum_select("Select an editor?", choices) expect(answer).to eq("/bin/nano") expected_output = output_helper("Select an editor?", choices, "/bin/nano") expect(prompt.output.string).to eq(expected_output) end it "sets quiet mode through DSL" do choices = %w[/bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny] prompt.input << "1\n" prompt.input.rewind answer = prompt.enum_select("Select an editor?") do |menu| menu.quiet true menu.default 2 menu.enum "." menu.choice "/bin/nano" menu.choice "/usr/bin/vim.basic" menu.choice "/usr/bin/vim.tiny" end expect(answer).to eq("/bin/nano") expected_output = output_helper("Select an editor?", choices, "/usr/bin/vim.basic", default: 2, enum: ".") + output_helper("Select an editor?", choices, "/bin/nano", default: 2, enum: ".", input: 1) expect(prompt.output.string).to eq(expected_output) end it "displays error with unrecognized input" do choices = %w[/bin/nano /usr/bin/vim.basic /usr/bin/vim.tiny] prompt.input << "11\n2\n" prompt.input.rewind answer = prompt.enum_select("Select an editor?", choices) expect(answer).to eq("/usr/bin/vim.basic") expected_output = [ output_helper("Select an editor?", choices, "/bin/nano"), output_helper("Select an editor?", choices, "/bin/nano", input: "1"), output_helper("Select an editor?", choices, "/bin/nano", input: "11"), output_helper("Select an editor?", choices, "/bin/nano", error: "Please enter a valid number", input: ""), output_helper("Select an editor?", choices, "/usr/bin/vim.basic", error: "Please enter a valid number", input: "2"), exit_message("Select an editor?", "/usr/bin/vim.basic") ].join expect(prompt.output.string).to eq(expected_output) end it "paginates long selections" do choices = %w[A B C D E F G H] prompt.input << "\n" prompt.input.rewind answer = prompt.enum_select("What letter?", choices, per_page: 3, default: 4) expect(answer).to eq("D") expect(prompt.output.string).to eq([ "What letter? \n", " \e[32m4) D\e[0m\n", " 5) E\n", " 6) F\n", " Choose 1-8 [4]: ", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \e[32mD\e[0m\n" ].join) end it "doesn't paginate short selections" do choices = %i[A B C D] prompt.input << "\r" prompt.input.rewind answer = prompt.enum_select("What letter?", choices, per_page: 4, default: 1) expect(answer).to eq(:A) expected_output = output_helper("What letter?", choices, :A) + exit_message("What letter?", :A) expect(prompt.output.string).to eq(expected_output) end it "shows pages matching input" do choices = %w[A B C D E F G H] prompt.input << "11\n\b\n" prompt.input.rewind value = prompt.enum_select("What letter?", choices, per_page: 3) expect(value).to eq("A") expect(prompt.output.string).to eq([ "What letter? \n", " \e[32m1) A\e[0m\n", " 2) B\n", " 3) C\n", " Choose 1-8 [1]: ", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \n", " \e[32m1) A\e[0m\n", " 2) B\n", " 3) C\n", " Choose 1-8 [1]: 1", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[19C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \n", " \e[32m1) A\e[0m\n", " 2) B\n", " 3) C\n", " Choose 1-8 [1]: 11", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[20C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \n", " \e[32m1) A\e[0m\n", " 2) B\n", " 3) C\n", " Choose 1-8 [1]: \n", "\e[31m>>\e[0m Please enter a valid number", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \n", " \e[32m1) A\e[0m\n", " 2) B\n", " 3) C\n", " Choose 1-8 [1]: \n", "\e[31m>>\e[0m Please enter a valid number", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \e[32mA\e[0m\n" ].join) end it "switches through pages with tab key" do choices = %w[A B C D E F G H] prompt.input << "\t\n" prompt.input.rewind value = prompt.enum_select("What letter?") do |menu| menu.default 4 menu.per_page 3 menu.choices choices end expect(value).to eq("D") expect(prompt.output.string).to eq([ "What letter? \n", " \e[32m4) D\e[0m\n", " 5) E\n", " 6) F\n", " Choose 1-8 [4]: ", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \n", " 7) G\n", " 8) H\n", " Choose 1-8 [4]: ", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G\e[J", "What letter? \e[32mD\e[0m\n" ].join) end it "doesn't cycle around by default" do choices = %w[A B C D E F] prompt.input << "\t" << "\t" << "\n" prompt.input.rewind value = prompt.enum_select("What letter?") do |menu| menu.default 1 menu.per_page 3 menu.choices choices end expect(value).to eq("A") expect(prompt.output.string).to eq([ "What letter? \n", " \e[32m1) A\e[0m\n", " 2) B\n", " 3) C\n", " Choose 1-6 [1]: ", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \n", " 4) D\n", " 5) E\n", " 6) F\n", " Choose 1-6 [1]: ", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \n", " 4) D\n", " 5) E\n", " 6) F\n", " Choose 1-6 [1]: ", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \e[32mA\e[0m\n" ].join) end it "cycles around when configured to do so" do choices = %w[A B C D E F] prompt.input << "\t" << "\t" << "\n" prompt.input.rewind value = prompt.enum_select("What letter?", cycle: true) do |menu| menu.default 1 menu.per_page 3 menu.choices choices end expect(value).to eq("A") expect(prompt.output.string).to eq([ "What letter? \n", " \e[32m1) A\e[0m\n", " 2) B\n", " 3) C\n", " Choose 1-6 [1]: ", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \n", " 4) D\n", " 5) E\n", " 6) F\n", " Choose 1-6 [1]: ", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \n", " \e[32m1) A\e[0m\n", " 2) B\n", " 3) C\n", " Choose 1-6 [1]: ", "\n\e[90m(Press tab/right or left to reveal more choices)\e[0m", "\e[A\e[1G\e[18C", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G\e[J", "What letter? \e[32mA\e[0m\n" ].join) end context "with :disabled choice" do it "fails when active item is also disabled" do choices = [{name: "A", disabled: true}, "B", "C", "D", "E"] expect { prompt.enum_select("What letter?", choices, default: 1) }.to raise_error(TTY::Prompt::ConfigurationError, /default index 1 matches disabled choice item/) end it "finds first non-disabled index" do choices = [{name: "A", disabled: true}, {name: "B", disabled: true}, "C", "D"] prompt.input << "\n" prompt.input.rewind answer = prompt.enum_select("What letter?", choices) expect(answer).to eq("C") end it "doesn't allow to choose disabled choice and defaults" do choices = ["A", {name: "B", disabled: "(out)"}, "C", "D", "E", "F"] prompt.input << "2" << "\n" << "3" << "\n" prompt.input.rewind answer = prompt.enum_select("What letter?", choices) expect(answer).to eq("C") expected_output = [ output_helper("What letter?", choices, "A"), output_helper("What letter?", choices, "A", input: "2"), output_helper("What letter?", choices, "A", input: "", error: "Please enter a valid number"), output_helper("What letter?", choices, "C", input: "3", error: "Please enter a valid number"), exit_message("What letter?", "C") ].join expect(prompt.output.string).to eq(expected_output) end it "omits disabled choice when navigating with numbers" do choices = [ {name: "A"}, {name: "B", disabled: "(out)"}, {name: "C", disabled: "(out)"}, {name: "D"}, {name: "E"} ] prompt.on(:keypress) { |e| prompt.trigger(:keydelete) if e.value == "B" } prompt.input << "2" << "\u007F" << "3" << "\u007F" << "4" << "\n" prompt.input.rewind answer = prompt.enum_select("What letter?", choices) expect(answer).to eq("D") expected_output = [ output_helper("What letter?", choices, "A"), output_helper("What letter?", choices, "A", input: "2"), output_helper("What letter?", choices, "A", input: ""), output_helper("What letter?", choices, "A", input: "3"), output_helper("What letter?", choices, "A", input: ""), output_helper("What letter?", choices, "D", input: "4"), exit_message("What letter?", "D") ].join expect(prompt.output.string).to eq(expected_output) end end end tty-prompt-0.23.1/spec/unit/error_spec.rb000066400000000000000000000014401403662044600203410ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, "#error" do subject(:prompt) { TTY::Prompt::Test.new } it "displays one message" do prompt.error "Nothing is fine!" expect(prompt.output.string).to eql("\e[31mNothing is fine!\e[0m\n") end it "displays many messages" do prompt.error "Nothing is fine!", "All is broken!" expect(prompt.output.string) .to eq("\e[31mNothing is fine!\e[0m\n\e[31mAll is broken!\e[0m\n") end it "displays message with option" do prompt.error "Nothing is fine!", newline: false expect(prompt.output.string).to eql("\e[31mNothing is fine!\e[0m") end it "changes default red color to cyan" do prompt.error("All is fine", color: :cyan) expect(prompt.output.string).to eq("\e[36mAll is fine\e[0m\n") end end tty-prompt-0.23.1/spec/unit/evaluator_spec.rb000066400000000000000000000030401403662044600212100ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Evaluator do it "checks chained validation procs" do question = double(:question) evaluator = TTY::Prompt::Evaluator.new(question) evaluator.check { |_quest, value| if value < 21 [value, ["#{value} is not bigger than 21"]] else value end } evaluator.check { |_quest, value| if value < 42 [value, ["#{value} is not bigger than 42"]] else value end } answer = evaluator.call(2) expect(answer.errors.count).to eq(2) expect(answer.value).to eq(2) expect(answer.success?).to eq(false) expect(answer.failure?).to eq(true) end it "checks chained validation objects" do question = double(:question) evaluator = TTY::Prompt::Evaluator.new(question) LessThan21 = Class.new do def self.call(_quest, value) if value < 21 [value, ["#{value} is not bigger than 21"]] else value end end end LessThan42 = Class.new do def self.call(_quest, value) if value < 42 [value, ["#{value} is not bigger than 42"]] else value end end end evaluator.check(LessThan21) evaluator.check(LessThan42) answer = evaluator.call(2) expect(answer.errors).to match_array([ "2 is not bigger than 21", "2 is not bigger than 42" ]) expect(answer.value).to eq(2) expect(answer.success?).to eq(false) expect(answer.failure?).to eq(true) end end tty-prompt-0.23.1/spec/unit/expand_spec.rb000066400000000000000000000217661403662044600205040ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, "#expand" do subject(:prompt) { TTY::Prompt::Test.new } let(:choices) { [{ key: "y", name: "Overwrite", value: :yes }, { key: "n", name: "Skip", value: :no }, { key: "a", name: "Overwrite all", value: :all }, { key: "d", name: "Show diff", value: :diff }, { key: "q", name: "Quit", value: :quit }] } it "expands default option" do prompt.input << "\n" prompt.input.rewind result = prompt.expand("Overwrite Gemfile?", choices) expect(result).to eq(:yes) expected_output = [ "Overwrite Gemfile? (enter \"h\" for help) [\e[32my\e[0m,n,a,d,q,h] ", "\e[2K\e[1G", "Overwrite Gemfile? \e[32mOverwrite\e[0m\n" ].join expect(prompt.output.string).to eq(expected_output) end it "changes default option" do prompt.input << "\n" prompt.input.rewind result = prompt.expand("Overwrite Gemfile?", choices, default: 3) expect(result).to eq(:all) expected_output = [ "Overwrite Gemfile? (enter \"h\" for help) [y,n,\e[32ma\e[0m,d,q,h] ", "\e[2K\e[1G", "Overwrite Gemfile? \e[32mOverwrite all\e[0m\n" ].join expect(prompt.output.string).to eq(expected_output) end it "sets quiet mode" do prompt.input << "\n" prompt.input.rewind result = prompt.expand("Overwrite Gemfile?", choices, quiet: true) expect(result).to eq(:yes) expected_output = [ "Overwrite Gemfile? (enter \"h\" for help) [\e[32my\e[0m,n,a,d,q,h] ", "\e[2K\e[1G" ].join expect(prompt.output.string).to eq(expected_output) end it "sets quiet mode through DSL" do prompt.input << "\n" prompt.input.rewind result = prompt.expand("Overwrite Gemfile?") do |q| q.choice key: "y", name: "Overwrite", value: :yes q.choice key: "n", name: "Skip", value: :no q.choice key: "a", name: "Overwrite all", value: :all q.choice key: "d", name: "Show diff", value: :diff q.choice key: "q", name: "Quit", value: :quit q.quiet true end expect(result).to eq(:yes) expected_output = [ "Overwrite Gemfile? (enter \"h\" for help) [\e[32my\e[0m,n,a,d,q,h] ", "\e[2K\e[1G" ].join expect(prompt.output.string).to eq(expected_output) end it "expands chosen option with extra information" do prompt.input << "a\n" prompt.input.rewind result = prompt.expand("Overwrite Gemfile?", choices) expect(result).to eq(:all) expected_output = [ "Overwrite Gemfile? (enter \"h\" for help) [\e[32my\e[0m,n,a,d,q,h] ", "\e[2K\e[1G", "Overwrite Gemfile? (enter \"h\" for help) [y,n,\e[32ma\e[0m,d,q,h] ", "a\n", "\e[32m>> \e[0mOverwrite all", "\e[A\e[1G\e[55C", "\e[2K\e[1G", "\e[1B", "\e[2K\e[1G", "\e[A\e[1G", "Overwrite Gemfile? \e[32mOverwrite all\e[0m\n" ].join expect(prompt.output.string).to eq(expected_output) end it "expands help option and then defaults" do prompt.input << "h\nd\n" prompt.input.rewind result = prompt.expand("Overwrite Gemfile?", choices) expect(result).to eq(:diff) expected_output = [ "Overwrite Gemfile? (enter \"h\" for help) [\e[32my\e[0m,n,a,d,q,h] ", "\e[2K\e[1G", "Overwrite Gemfile? (enter \"h\" for help) [y,n,a,d,q,\e[32mh\e[0m] h\n", "\e[32m>> \e[0mprint help", "\e[A\e[1G\e[55C", "\e[2K\e[1G", "\e[1B", "\e[2K\e[1G", "\e[A\e[1G", "Overwrite Gemfile? \n", " y - Overwrite\n", " n - Skip\n", " a - Overwrite all\n", " d - Show diff\n", " q - Quit\n", " h - print help\n", " Choice [y]: ", "\e[2K\e[1G\e[1A" * 7, "\e[2K\e[1G", "Overwrite Gemfile? \n", " y - Overwrite\n", " n - Skip\n", " a - Overwrite all\n", " \e[32md - Show diff\e[0m\n", " q - Quit\n", " h - print help\n", " Choice [y]: d", "\e[2K\e[1G\e[1A" * 7, "\e[2K\e[1G", "Overwrite Gemfile? \e[32mShow diff\e[0m\n" ].join expect(prompt.output.string).to eq(expected_output) end it "automatically expands hint" do prompt.input << "d\n" prompt.input.rewind result = prompt.expand("Overwrite Gemfile?", choices, auto_hint: true) expect(result).to eq(:diff) expected_output = [ "Overwrite Gemfile? (enter \"h\" for help) [\e[32my\e[0m,n,a,d,q,h] ", "\n\e[32m>> \e[0mOverwrite", "\e[A\e[1G\e[54C", "\e[2K\e[1G", "\e[1B", "\e[2K\e[1G", "\e[A\e[1G", "Overwrite Gemfile? (enter \"h\" for help) [y,n,a,\e[32md\e[0m,q,h] ", "d\n", "\e[32m>> \e[0mShow diff", "\e[A\e[1G\e[55C", "\e[2K\e[1G", "\e[1B", "\e[2K\e[1G", "\e[A\e[1G", "Overwrite Gemfile? \e[32mShow diff\e[0m\n", "\e[32m>> \e[0mShow diff", "\e[A\e[1G\e[28C\n" ].join expect(prompt.output.string).to eq(expected_output) end it "informs about invalid input when automatically expanding hint" do prompt.on(:keypress) { |e| prompt.trigger(:keybackspace) if e.value == "w" } prompt.input << "y" << "y" << "\u007F" << "\r" prompt.input.rewind result = prompt.expand("Overwrite Gemfile?", choices, defualt: 1, auto_hint: true) expect(result).to eq(:yes) expected_output = [ "Overwrite Gemfile? (enter \"h\" for help) [\e[32my\e[0m,n,a,d,q,h] ", "\n\e[32m>> \e[0mOverwrite", "\e[A\e[1G\e[54C", "\e[2K\e[1G", "\e[1B", "\e[2K\e[1G", "\e[A\e[1G", "Overwrite Gemfile? (enter \"h\" for help) [\e[32my\e[0m,n,a,d,q,h] ", "y\n", "\e[32m>> \e[0mOverwrite", "\e[A\e[1G\e[55C", "\e[2K\e[1G", "\e[1B", "\e[2K\e[1G", "\e[A\e[1G", "Overwrite Gemfile? (enter \"h\" for help) [y,n,a,d,q,h] ", "yy\n", "\e[32m>> \e[0minvalid option", "\e[A\e[1G\e[56C", "\e[2K\e[1G", "\e[1B", "\e[2K\e[1G", "\e[A\e[1G", "Overwrite Gemfile? (enter \"h\" for help) [\e[32my\e[0m,n,a,d,q,h] ", "y\n", "\e[32m>> \e[0mOverwrite", "\e[A\e[1G\e[55C", "\e[2K\e[1G", "\e[1B", "\e[2K\e[1G", "\e[A\e[1G", "Overwrite Gemfile? \e[32mOverwrite\e[0m\n", "\e[32m>> \e[0mOverwrite", "\e[A\e[1G\e[28C\n" ].join expect(prompt.output.string).to eq(expected_output) end it "specifies options through DSL" do prompt.input << "\n" prompt.input.rewind result = prompt.expand("Overwrite Gemfile?") do |q| q.default 4 q.choice key: "y", name: "Overwrite", value: :yes q.choice key: "n", name: "Skip", value: :no q.choice key: "a", name: "Overwrite all", value: :all q.choice key: "d", name: "Show diff", value: :diff q.choice key: "q", name: "Quit", value: :quit end expect(result).to eq(:diff) expected_output = [ "Overwrite Gemfile? (enter \"h\" for help) [y,n,a,\e[32md\e[0m,q,h] ", "\e[2K\e[1G", "Overwrite Gemfile? \e[32mShow diff\e[0m\n" ].join expect(prompt.output.string).to eq(expected_output) end it "specifies options through DSL and executes value" do prompt.input << "\n" prompt.input.rewind result = prompt.expand("Overwrite Gemfile?") do |q| q.choice key: "y", name: "Overwrite" do :ok end q.choice key: "n", name: "Skip", value: :no q.choice key: "a", name: "Overwrite all", value: :all q.choice key: "d", name: "Show diff", value: :diff q.choice key: "q", name: "Quit", value: :quit end expect(result).to eq(:ok) expected_output = [ "Overwrite Gemfile? (enter \"h\" for help) [\e[32my\e[0m,n,a,d,q,h] ", "\e[2K\e[1G", "Overwrite Gemfile? \e[32mOverwrite\e[0m\n" ].join expect(prompt.output.string).to eq(expected_output) end it "fails to expand due to lack of key attribute" do choices = [{name: "Overwrite", value: :yes}] expect { prompt.expand("Overwrite Gemfile?", choices) }.to raise_error(TTY::Prompt::ConfigurationError, /Choice Overwrite is missing a :key attribute/) end it "fails to expand due to wrong key length" do choices = [{key: "long", name: "Overwrite", value: :yes}] expect { prompt.expand("Overwrite Gemfile?", choices) }.to raise_error(TTY::Prompt::ConfigurationError, /Choice key `long` is more than one character long/) end it "fails to expand due to reserve key" do choices = [{key: "h", name: "Overwrite", value: :yes}] expect { prompt.expand("Overwrite Gemfile?", choices) }.to raise_error(TTY::Prompt::ConfigurationError, /Choice key `h` is reserved for help menu/) end it "fails to expand due to duplicate key" do choices = [{key: "y", name: "Overwrite", value: :yes}, {key: "y", name: "Change", value: :yes}] expect { prompt.expand("Overwrite Gemfile?", choices) }.to raise_error(TTY::Prompt::ConfigurationError, /Choice key `y` is a duplicate/) end end tty-prompt-0.23.1/spec/unit/inspect_spec.rb000066400000000000000000000007161403662044600206620ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, "#inspect" do it "inspects instance attributes" do prompt = TTY::Prompt::Test.new expect(prompt.inspect).to eq([ "#" ].join(" ")) end end tty-prompt-0.23.1/spec/unit/keypress_spec.rb000066400000000000000000000030071403662044600210560ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "#keypress" do subject(:prompt) { TTY::Prompt::Test.new } it "receives line feed with echo on" do prompt.input << "\n" prompt.input.rewind answer = prompt.keypress("Press key:", echo: true) expect(answer).to eq("\n") expect(prompt.output.string).to eq([ "Press key: ", "\e[2K\e[1G\e[1A\e[2K\e[1G", "Press key: \n\n" ].join) end it "asks for a keypress with echo on" do prompt.input << "abcd" prompt.input.rewind answer = prompt.keypress("Press key:", echo: true) expect(answer).to eq("a") expect(prompt.output.string).to eq([ "Press key: ", "\e[2K\e[1G", "Press key: \e[32ma\e[0m\n" ].join) end it "asks for a keypress with echo off" do prompt.input << "abcd" prompt.input.rewind answer = prompt.keypress("Press key:") expect(answer).to eq("a") expect(prompt.output.string).to eq([ "Press key: ", "\e[2K\e[1G", "Press key: \n" ].join) end it "interrupts input" do prompt = TTY::Prompt::Test.new(interrupt: :exit) prompt.input << "\x03" prompt.input.rewind expect { prompt.keypress("Press key:") }.to raise_error(SystemExit) end it "timeouts when no key provided" do prompt = TTY::Prompt::Test.new(interrupt: :exit) prompt.keypress("Press any key or continue in :countdown", timeout: 0.01) expect(prompt.output.string).to include("Press any key or continue in 0.00") end end tty-prompt-0.23.1/spec/unit/mask_spec.rb000066400000000000000000000074171403662044600201550ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, "#mask" do let(:symbols) { TTY::Prompt::Symbols.symbols } subject(:prompt) { TTY::Prompt::Test.new } it "masks output by default" do prompt.input << "pass\r" prompt.input.rewind answer = prompt.mask("What is your password?") expect(answer).to eql("pass") expect(prompt.output.string).to eq([ "What is your password? ", "\e[2K\e[1G", "What is your password? #{symbols[:dot]}", "\e[2K\e[1G", "What is your password? #{symbols[:dot] * 2}", "\e[2K\e[1G", "What is your password? #{symbols[:dot] * 3}", "\e[2K\e[1G", "What is your password? #{symbols[:dot] * 4}", "\e[2K\e[1G", "What is your password? \e[32m#{symbols[:dot] * 4}\e[0m\n", "\e[1A\e[2K\e[1G", "What is your password? \e[32m#{symbols[:dot] * 4}\e[0m\n" ].join) end it "masks output with custom character" do prompt.input << "pass\r" prompt.input.rewind answer = prompt.mask("What is your password?") { |q| q.mask("*") } expect(answer).to eql("pass") expect(prompt.output.string).to eq([ "What is your password? ", "\e[2K\e[1G", "What is your password? *", "\e[2K\e[1G", "What is your password? **", "\e[2K\e[1G", "What is your password? ***", "\e[2K\e[1G", "What is your password? ****", "\e[2K\e[1G", "What is your password? \e[32m****\e[0m\n", "\e[1A\e[2K\e[1G", "What is your password? \e[32m****\e[0m\n" ].join) end it "masks with unicode character" do prompt.input << "lov\n" prompt.input.rewind answer = prompt.mask("What is your password?", mask: "\u2665") expect(answer).to eql("lov") expect(prompt.output.string).to eq([ "What is your password? ", "\e[2K\e[1G", "What is your password? ♥", "\e[2K\e[1G", "What is your password? ♥♥", "\e[2K\e[1G", "What is your password? ♥♥♥", "\e[2K\e[1G", "What is your password? \e[32m♥♥♥\e[0m\n", "\e[1A\e[2K\e[1G", "What is your password? \e[32m♥♥♥\e[0m\n" ].join) end it "ignores mask if echo is off" do prompt.input << "pass\n" prompt.input.rewind answer = prompt.mask("What is your password?") do |q| q.echo false q.mask "*" end expect(answer).to eql("pass") expect(prompt.output.string).to eq([ "What is your password? ", "\e[2K\e[1G", "What is your password? ", "\e[2K\e[1G", "What is your password? ", "\e[2K\e[1G", "What is your password? ", "\e[2K\e[1G", "What is your password? ", "\e[2K\e[1G", "What is your password? \n", "\e[1A\e[2K\e[1G", "What is your password? \n" ].join) end it "validates input" do prompt = TTY::Prompt::Test.new(symbols: {dot: "*"}) prompt.input << "no\nyes\n" prompt.input.rewind answer = prompt.mask("What is your password?") do |q| q.echo true q.validate(/[a-z]{3,4}/) q.messages[:valid?] = "Not valid" end expect(answer).to eq("yes") expect(prompt.output.string).to eq([ "What is your password? ", "\e[2K\e[1G", "What is your password? *", "\e[2K\e[1G", "What is your password? **", "\e[2K\e[1G", "What is your password? \e[32m**\e[0m\n", "\e[31m>>\e[0m Not valid", "\e[1A\e[2K\e[1G", "What is your password? \e[31m**\e[0m", "\e[2K\e[1G", "What is your password? *", "\e[2K\e[1G", "What is your password? **", "\e[2K\e[1G", "What is your password? ***", "\e[2K\e[1G", "What is your password? \e[32m***\e[0m\n", "\e[2K\e[1G", "\e[1A\e[2K\e[1G", "What is your password? \e[32m***\e[0m\n" ].join) end end tty-prompt-0.23.1/spec/unit/multi_select_spec.rb000066400000000000000000001076561403662044600217210ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt do let(:symbols) { TTY::Prompt::Symbols.symbols } let(:up_down) { "#{symbols[:arrow_up]}/#{symbols[:arrow_down]}" } let(:left_right) { "#{symbols[:arrow_left]}/#{symbols[:arrow_right]}" } let(:left_key) { "\e[D" } let(:right_key) { "\e[C" } subject(:prompt) { TTY::Prompt::Test.new } def output_helper(prompt, choices, active, selected, options = {}) hint = options[:hint] init = options.fetch(:init, false) enum = options[:enum] out = [] out << "\e[?25l" if init out << prompt << " " out << "(min. #{options[:min]}) " if options[:min] out << "(max. #{options[:max]}) " if options[:max] out << selected.join(", ") out << " " if (init || hint) && !selected.empty? out << "\e[90m(#{hint})\e[0m" if hint out << "\n" out << choices.map.with_index do |choice, i| name = choice.is_a?(Hash) ? choice[:name] : choice disabled = choice.is_a?(Hash) ? choice[:disabled] : false num = (i + 1).to_s + enum if enum prefix = name == active ? "#{symbols[:marker]} " : " " prefix += if disabled "\e[31m#{symbols[:cross]}\e[0m #{num}#{name} #{disabled}" elsif selected.include?(name) "\e[32m#{symbols[:radio_on]}\e[0m #{num}#{name}" else "#{symbols[:radio_off]} #{num}#{name}" end prefix end.join("\n") out << "\e[2K\e[1G\e[1A" * choices.count out << "\e[2K\e[1G" out.join end def exit_message(prompt, choices) out = [] out << "#{prompt} " out << "\e[32m#{choices.join(', ')}\e[0m" unless choices.empty? out << "\n\e[?25h" out.join end # Ensure a wide prompt on CI before { allow(TTY::Screen).to receive(:width).and_return(200) } it "selects nothing when return pressed immediately" do choices = %i[vodka beer wine whisky bourbon] prompt.input << "\r" prompt.input.rewind expect(prompt.multi_select("Select drinks?", choices)).to eq([]) expected_output = output_helper("Select drinks?", choices, :vodka, [], init: true, hint: "Press #{up_down} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") + exit_message("Select drinks?", []) expect(prompt.output.string).to eq(expected_output) end it "selects item when space pressed" do choices = %w[vodka beer wine whisky bourbon] prompt.input << " \r" prompt.input.rewind expect(prompt.multi_select("Select drinks?", choices)).to eq(["vodka"]) expected_output = output_helper("Select drinks?", choices, "vodka", [], init: true, hint: "Press #{up_down} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") + output_helper("Select drinks?", choices, "vodka", ["vodka"]) + exit_message("Select drinks?", %w[vodka]) expect(prompt.output.string).to eq(expected_output) end it "selects item when space pressed but doesn't echo item if echo: false" do choices = %w[vodka beer wine whisky bourbon] prompt.input << " \r" prompt.input.rewind expect(prompt.multi_select("Select drinks?", choices, echo: false)).to eq(["vodka"]) expected_output = [ "\e[?25lSelect drinks? \e[90m(Press #{up_down} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish)\e[0m\n", "#{symbols[:marker]} #{symbols[:radio_off]} vodka\n", " #{symbols[:radio_off]} beer\n", " #{symbols[:radio_off]} wine\n", " #{symbols[:radio_off]} whisky\n", " #{symbols[:radio_off]} bourbon", "\e[2K\e[1G\e[1A" * 5, "\e[2K\e[1G", "Select drinks? \n", "#{symbols[:marker]} \e[32m#{symbols[:radio_on]}\e[0m vodka\n", " #{symbols[:radio_off]} beer\n", " #{symbols[:radio_off]} wine\n", " #{symbols[:radio_off]} whisky\n", " #{symbols[:radio_off]} bourbon", "\e[2K\e[1G\e[1A" * 5, "\e[2K\e[1G", "Select drinks? \n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end it "sets choice custom values" do choices = {vodka: 1, beer: 2, wine: 3, whisky: 4, bourbon: 5} prompt.input << " \r" prompt.input.rewind expect(prompt.multi_select("Select drinks?", choices)).to eq([1]) expected_output = output_helper("Select drinks?", choices.keys, :vodka, [], init: true, hint: "Press #{up_down} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") + output_helper("Select drinks?", choices.keys, :vodka, [:vodka]) + exit_message("Select drinks?", %w[vodka]) expect(prompt.output.string).to eq(expected_output) end it "sets choice name and value through DSL" do prompt.input << " \r" prompt.input.rewind value = prompt.multi_select("Select drinks?") do |menu| menu.symbols marker: ">", radio_off: "-", radio_on: "=" menu.enum ")" menu.choice :vodka, {score: 1} menu.choice :beer, 2 menu.choice :wine, 3 menu.choices whisky: 4, bourbon: 5 end expect(value).to eq([{score: 1}]) expect(prompt.output.string).to eq([ "\e[?25lSelect drinks? \e[90m(Press #{up_down} arrow or 1-5 number to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish)\e[0m\n", "> - 1) vodka\n", " - 2) beer\n", " - 3) wine\n", " - 4) whisky\n", " - 5) bourbon", "\e[2K\e[1G\e[1A" * 5, "\e[2K\e[1G", "Select drinks? vodka\n", "> \e[32m=\e[0m 1) vodka\n", " - 2) beer\n", " - 3) wine\n", " - 4) whisky\n", " - 5) bourbon", "\e[2K\e[1G\e[1A" * 5, "\e[2K\e[1G", "Select drinks? \e[32mvodka\e[0m\n\e[?25h" ].join) end it "sets default options through DSL syntax" do prompt.input << "\r" prompt.input.rewind value = prompt.multi_select("Select drinks?") do |menu| menu.default 2, 5 menu.choice :vodka, {score: 10} menu.choice :beer, {score: 20} menu.choice :wine, {score: 30} menu.choice :whisky, {score: 40} menu.choice :bourbon, {score: 50} end expect(value).to match_array([{score: 20}, {score: 50}]) expect(prompt.output.string).to eq([ "\e[?25lSelect drinks? beer, bourbon \e[90m(Press #{up_down} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish)\e[0m\n", " #{symbols[:radio_off]} vodka\n", " \e[32m#{symbols[:radio_on]}\e[0m beer\n", " #{symbols[:radio_off]} wine\n", " #{symbols[:radio_off]} whisky\n", "#{symbols[:marker]} \e[32m#{symbols[:radio_on]}\e[0m bourbon", "\e[2K\e[1G\e[1A" * 5, "\e[2K\e[1G", "Select drinks? \e[32mbeer, bourbon\e[0m\n\e[?25h" ].join) end it "sets choice value to nil through DSL" do choices = [ {name: "none", value: nil}, {name: "vodka", value: 1}, {name: "beer", value: 1}, {name: "wine", value: 1} ] prompt.input << "\r" prompt.input.rewind value = prompt.multi_select("Select drinks?", default: 1) do |menu| menu.choice :none, nil menu.choice :vodka, {score: 10} menu.choice :beer, {score: 20} menu.choice :wine, {score: 30} end expect(value).to match_array([nil]) expected_output = output_helper("Select drinks?", choices, "none", %w[none], init: true, hint: "Press #{up_down} arrow to move, Space/Ctrl+A|R to select " \ "(all|rev) and Enter to finish") + exit_message("Select drinks?", %w[none]) expect(prompt.output.string).to eq(expected_output) end it "sets default options through hash syntax" do prompt.input << "\r" prompt.input.rewind value = prompt.multi_select("Select drinks?", default: [2, 5]) do |menu| menu.choice :vodka, {score: 10} menu.choice :beer, {score: 20} menu.choice :wine, {score: 30} menu.choice :whisky, {score: 40} menu.choice :bourbon, {score: 50} end expect(value).to match_array([{score: 20}, {score: 50}]) end it "sets default choices using names" do prompt.input << "\r" prompt.input.rewind value = prompt.multi_select("Select drinks?", default: [:beer, "whisky"]) do |menu| menu.choice :vodka, {score: 10} menu.choice :beer, {score: 20} menu.choice :wine, {score: 30} menu.choice :whisky, {score: 40} menu.choice :bourbon, {score: 50} end expect(value).to match_array([{score: 20}, {score: 40}]) end it "raises error for defaults out of range" do prompt.input << "\r" prompt.input.rewind expect { prompt.multi_select("Select drinks?", default: [2, 6]) do |menu| menu.choice :vodka, {score: 10} menu.choice :beer, {score: 20} menu.choice :wine, {score: 30} menu.choice :whisky, {score: 40} menu.choice :bourbon, {score: 50} end }.to raise_error(TTY::Prompt::ConfigurationError, /default index `6` out of range \(1 - 5\)/) end it "sets prompt prefix" do prompt = TTY::Prompt::Test.new(prefix: "[?] ") choices = %w[vodka beer wine whisky bourbon] prompt.input << "\r" prompt.input.rewind expect(prompt.multi_select("Select drinks?", choices)).to eq([]) expect(prompt.output.string).to eq([ "\e[?25l[?] Select drinks? \e[90m(Press #{up_down} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish)\e[0m\n", "#{symbols[:marker]} #{symbols[:radio_off]} vodka\n", " #{symbols[:radio_off]} beer\n", " #{symbols[:radio_off]} wine\n", " #{symbols[:radio_off]} whisky\n", " #{symbols[:radio_off]} bourbon", "\e[2K\e[1G\e[1A" * 5, "\e[2K\e[1G", "[?] Select drinks? \n\e[?25h" ].join) end it "changes selected item color & marker" do choices = %w[vodka beer wine whisky bourbon] prompt.input << "\r" prompt.input.rewind options = {default: [1], active_color: :blue, symbols: {marker: ">"}} expect(prompt.multi_select("Select drinks?", choices, options)).to eq(["vodka"]) expect(prompt.output.string).to eq([ "\e[?25lSelect drinks? vodka \e[90m(Press #{up_down} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish)\e[0m\n", "> \e[34m#{symbols[:radio_on]}\e[0m vodka\n", " #{symbols[:radio_off]} beer\n", " #{symbols[:radio_off]} wine\n", " #{symbols[:radio_off]} whisky\n", " #{symbols[:radio_off]} bourbon", "\e[2K\e[1G\e[1A" * 5, "\e[2K\e[1G", "Select drinks? \e[34mvodka\e[0m\n\e[?25h" ].join) end it "changes help text and color" do choices = %w[vodka beer wine whisky bourbon] prompt.input << "\r" prompt.input.rewind options = {help: "(Bash keyboard)", help_color: :cyan} answer = prompt.multi_select("Select drinks?", choices, options) expect(answer).to eq([]) expected_output = [ "\e[?25lSelect drinks? \e[36m(Bash keyboard)\e[0m\n", "#{symbols[:marker]} #{symbols[:radio_off]} vodka\n", " #{symbols[:radio_off]} beer\n", " #{symbols[:radio_off]} wine\n", " #{symbols[:radio_off]} whisky\n", " #{symbols[:radio_off]} bourbon", "\e[2K\e[1G\e[1A" * 5, "\e[2K\e[1G", "Select drinks? \n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end it "sets prompt to quiet mode" do prompt = TTY::Prompt::Test.new(quiet: true) choices = %w[vodka beer wine whisky bourbon] prompt.input << "\r" prompt.input.rewind expect(prompt.multi_select("Select drinks?", choices)).to eq([]) expected_output = output_helper("Select drinks?", choices, "vodka", [], init: true, hint: "Press #{up_down} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") + "\e[?25h" expect(prompt.output.string).to eq(expected_output) end it "changes to always show help" do choices = %w[vodka beer wine whisky bourbon] prompt.on(:keypress) { |e| prompt.trigger(:keydown) if e.value == "j" } prompt.input << "j" << "j" << " " << "\r" prompt.input.rewind answer = prompt.multi_select("Select drinks?", choices, show_help: :always) expect(answer).to eq(%w[wine]) expected_output = output_helper("Select drinks?", choices, "vodka", [], init: true, hint: "Press #{up_down} arrow to move, Space/Ctrl+A|R to select " \ "(all|rev) and Enter to finish") + output_helper("Select drinks?", choices, "beer", [], hint: "Press #{up_down} arrow to move, Space/Ctrl+A|R to select " \ "(all|rev) and Enter to finish") + output_helper("Select drinks?", choices, "wine", [], hint: "Press #{up_down} arrow to move, Space/Ctrl+A|R to select " \ "(all|rev) and Enter to finish") + output_helper("Select drinks?", choices, "wine", %w[wine], hint: "Press #{up_down} arrow to move, Space/Ctrl+A|R to select " \ "(all|rev) and Enter to finish") + exit_message("Select drinks?", %w[wine]) expect(prompt.output.string).to eq(expected_output) end it "changes to never show help" do choices = %w[vodka beer wine whisky bourbon] prompt.on(:keypress) { |e| prompt.trigger(:keydown) if e.value == "j" } prompt.input << "j" << "j" << " " << "\r" prompt.input.rewind answer = prompt.multi_select("Select drinks?", choices, show_help: :never) expect(answer).to eq(%w[wine]) expected_output = output_helper("Select drinks?", choices, "vodka", [], init: true) + output_helper("Select drinks?", choices, "beer", []) + output_helper("Select drinks?", choices, "wine", []) + output_helper("Select drinks?", choices, "wine", %w[wine]) + exit_message("Select drinks?", %w[wine]) expect(prompt.output.string).to eq(expected_output) end context "when paginated" do it "paginates long selections" do choices = %w[A B C D E F G H] prompt.input << "\r" prompt.input.rewind answer = prompt.multi_select("What letter?", choices, per_page: 3, default: 4) expect(answer).to eq(["D"]) expected_output = output_helper("What letter?", choices[3..5], "D", %w[D], init: true, hint: "Press #{up_down}/#{left_right} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") + exit_message("What letter?", %w[D]) expect(prompt.output.string).to eq(expected_output) end it "paginates choices as hash object" do choices = {A: 1, B: 2, C: 3, D: 4, E: 5, F: 6, G: 7, H: 8} prompt.input << "\r" prompt.input.rewind answer = prompt.multi_select("What letter?", choices, default: 4, per_page: 3) expect(answer).to eq([4]) expected_output = output_helper("What letter?", choices.keys[3..5], :D, [:D], init: true, hint: "Press #{up_down}/#{left_right} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") + exit_message("What letter?", %w[D]) expect(prompt.output.string).to eq(expected_output) end it "paginates long selections through DSL" do choices = %w[A B C D E F G H] prompt.input << "\r" prompt.input.rewind answer = prompt.multi_select("What letter?") do |menu| menu.per_page 3 menu.default 4 menu.choices choices end expect(answer).to eq(["D"]) expected_output = output_helper("What letter?", choices[3..5], "D", %w[D], init: true, hint: "Press #{up_down}/#{left_right} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") + exit_message("What letter?", %w[D]) expect(prompt.output.string).to eq(expected_output) end it "doesn't paginate short selections" do choices = %w[A B C D] prompt.input << "\r" prompt.input.rewind value = prompt.multi_select("What letter?", choices, per_page: 4, default: 1) expect(value).to eq(["A"]) expected_output = output_helper("What letter?", choices, "A", %w[A], init: true, hint: "Press #{up_down} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") + exit_message("What letter?", %w[A]) expect(prompt.output.string).to eq(expected_output) end it "navigates evenly paged output with right arrow until end of selection" do choices = ("1".."12").to_a prompt.on(:keypress) { |e| prompt.trigger(:keyright) if e.value == "l" } prompt.input << "l" << "l" << "l" << " " << "\r" prompt.input.rewind answer = prompt.multi_select("What number?", choices, per_page: 4) expect(answer).to eq(["9"]) expected_output = output_helper("What number?", choices[0..3], "1", [], init: true, hint: "Press #{up_down}/#{left_right} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") + output_helper("What number?", choices[4..7], "5", []) + output_helper("What number?", choices[8..11], "9", []) + output_helper("What number?", choices[8..11], "9", []) + output_helper("What number?", choices[8..11], "9", ["9"]) + exit_message("What number?", %w[9]) expect(prompt.output.string).to eq(expected_output) end it "navigates unevenly paged output with right arrow until the end of selection" do choices = ("1".."10").to_a prompt.on(:keypress) { |e| prompt.trigger(:keyright) if e.value == "l" } prompt.input << "l" << "l" << "l" << " " << "\r" prompt.input.rewind answer = prompt.multi_select("What number?", choices, default: 4, per_page: 4) expect(answer).to eq(%w[4 10]) expected_output = output_helper("What number?", choices[3..6], "4", ["4"], init: true, hint: "Press #{up_down}/#{left_right} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") + output_helper("What number?", choices[4..7], "8", ["4"]) + output_helper("What number?", choices[8..9], "10", ["4"]) + output_helper("What number?", choices[8..9], "10", ["4"]) + output_helper("What number?", choices[8..9], "10", %w[4 10]) + exit_message("What number?", %w[4 10]) expect(prompt.output.string).to eq(expected_output) end it "navigates left and right" do choices = ("1".."10").to_a prompt.on(:keypress) { |e| prompt.trigger(:keyright) if e.value == "l" prompt.trigger(:keyleft) if e.value == "h" } prompt.input << "l" << "l" << "h" << " " << "\r" prompt.input.rewind answer = prompt.multi_select("What number?", choices, default: 2, per_page: 4) expect(answer).to eq(%w[2 6]) expected_output = output_helper("What number?", choices[0..3], "2", ["2"], init: true, hint: "Press #{up_down}/#{left_right} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") + output_helper("What number?", choices[4..7], "6", ["2"]) + output_helper("What number?", choices[8..9], "10", ["2"]) + output_helper("What number?", choices[4..7], "6", ["2"]) + output_helper("What number?", choices[4..7], "6", %w[2 6]) + exit_message("What number?", %w[2 6]) expect(prompt.output.string).to eq(expected_output) end it "combines up/down navigation with left/right" do choices = ("1".."11").to_a prompt.on(:keypress) { |e| prompt.trigger(:keyup) if e.value == "k" prompt.trigger(:keydown) if e.value == "j" prompt.trigger(:keyright) if e.value == "l" prompt.trigger(:keyleft) if e.value == "h" } prompt.input << "j" << "l" << "k" << "k" << "h" << " " << "\r" prompt.input.rewind answer = prompt.multi_select("What number?", choices, default: 2, per_page: 4) expect(answer).to eq(%w[1 2]) expected_output = output_helper("What number?", choices[0..3], "2", ["2"], init: true, hint: "Press #{up_down}/#{left_right} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") + output_helper("What number?", choices[0..3], "3", ["2"]) + output_helper("What number?", choices[4..7], "7", ["2"]) + output_helper("What number?", choices[4..7], "6", ["2"]) + output_helper("What number?", choices[3..6], "5", ["2"]) + output_helper("What number?", choices[0..3], "1", ["2"]) + output_helper("What number?", choices[0..3], "1", %w[1 2]) + exit_message("What number?", %w[1 2]) expect(prompt.output.string).to eq(expected_output) end it "selects all paged choices with ctrl+a" do choices = ("1".."12").to_a prompt.on(:keypress) { |e| prompt.trigger(:keyctrl_a) if e.value == "a" } prompt.input << "a" << " " << "\r" prompt.input.rewind answer = prompt.multi_select("What number?", choices, per_page: 4) expect(answer).to eq(choices - %w[1]) expected_output = output_helper("What number?", choices[0..3], "1", [], init: true, hint: "Press #{up_down}/#{left_right} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") + output_helper("What number?", choices[0..3], "1", choices) + output_helper("What number?", choices[0..3], "1", choices - %w[1]) + exit_message("What number?", choices - %w[1]) expect(prompt.output.string).to eq(expected_output) end it "reverts selection accross pages with Ctrl+r" do choices = ("1".."12").to_a prompt.on(:keypress) { |e| prompt.trigger(:keyctrl_r) if e.value == "r" prompt.trigger(:keydown) if e.value == "j" } prompt.input << " " << "j" << " " << "j" << " " << "r" << "\r" prompt.input.rewind answer = prompt.multi_select("What number?", choices, per_page: 4) expect(answer).to eq(("4".."12").to_a) expected_output = output_helper("What number?", choices[0..3], "1", [], init: true, hint: "Press #{up_down}/#{left_right} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") + output_helper("What number?", choices[0..3], "1", ["1"]) + output_helper("What number?", choices[0..3], "2", ["1"]) + output_helper("What number?", choices[0..3], "2", %w[1 2]) + output_helper("What number?", choices[0..3], "3", %w[1 2]) + output_helper("What number?", choices[0..3], "3", %w[1 2 3]) + output_helper("What number?", choices[0..3], "3", ("4".."12").to_a) + exit_message("What number?", ("4".."12").to_a) expect(prompt.output.string).to eq(expected_output) end end context "with :cycle" do it "doesn't cycle by default" do choices = %w[A B C] prompt.on(:keypress) { |e| prompt.trigger(:keydown) if e.value == "j" } prompt.input << "j" << "j" << "j" << " " << "\r" prompt.input.rewind value = prompt.multi_select("What letter?", choices) expect(value).to eq(["C"]) expected_output = output_helper("What letter?", choices, "A", [], init: true, hint: "Press #{up_down} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") + output_helper("What letter?", choices, "B", []) + output_helper("What letter?", choices, "C", []) + output_helper("What letter?", choices, "C", []) + output_helper("What letter?", choices, "C", ["C"]) + exit_message("What letter?", %w[C]) expect(prompt.output.string).to eq(expected_output) end it "cycles when configured to do so" do choices = %w[A B C] prompt.on(:keypress) { |e| prompt.trigger(:keydown) if e.value == "j" } prompt.input << "j" << "j" << "j" << " " << "\r" prompt.input.rewind value = prompt.multi_select("What letter?", choices, cycle: true) expect(value).to eq(["A"]) expected_output = output_helper("What letter?", choices, "A", [], init: true, hint: "Press #{up_down} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") + output_helper("What letter?", choices, "B", []) + output_helper("What letter?", choices, "C", []) + output_helper("What letter?", choices, "A", []) + output_helper("What letter?", choices, "A", ["A"]) + exit_message("What letter?", %w[A]) expect(prompt.output.string).to eq(expected_output) end it "cycles choices using left/right arrows" do choices = ("1".."10").to_a prompt.on(:keypress) { |e| prompt.trigger(:keyright) if e.value == "l" prompt.trigger(:keyleft) if e.value == "h" } prompt.input << "l" << "l" << "l" << "h" << " " << "\r" prompt.input.rewind answer = prompt.multi_select("What number?", choices, default: 2, per_page: 4, cycle: true) expect(answer).to eq(%w[2 10]) expected_output = output_helper("What number?", choices[0..3], "2", %w[2], init: true, hint: "Press #{up_down}/#{left_right} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") + output_helper("What number?", choices[4..7], "6", %w[2]) + output_helper("What number?", choices[8..9], "10", %w[2]) + output_helper("What number?", choices[0..3], "2", %w[2]) + output_helper("What number?", choices[8..9], "10", %w[2]) + output_helper("What number?", choices[8..9], "10", %w[2 10]) + exit_message("What number?", %w[2 10]) expect(prompt.output.string).to eq(expected_output) end it "cycles filtered choices left and right" do numbers = ("1".."10").to_a choices = numbers.map { |n| "a#{n}" } + numbers.map { |n| "b#{n}" } prompt.input << "b" << right_key << right_key << right_key prompt.input << left_key << left_key << left_key << " \r" prompt.input.rewind answer = prompt.multi_select("What room?", choices, default: 2, per_page: 4, filter: true, cycle: true) expect(answer).to eq(%w[a2 b2]) expected_output = output_helper("What room?", choices[0..3], "a2", %w[a2], init: true, hint: "Press #{up_down}/#{left_right} arrow to move, Space/Ctrl+A|R to select (all|rev), Enter to finish and letters to filter") + output_helper("What room?", choices[10..13], "b1", %w[a2], hint: "Filter: \"b\"") + output_helper("What room?", choices[14..17], "b5", %w[a2], hint: "Filter: \"b\"") + output_helper("What room?", choices[18..20], "b9", %w[a2], hint: "Filter: \"b\"") + output_helper("What room?", choices[10..13], "b1", %w[a2], hint: "Filter: \"b\"") + output_helper("What room?", choices[18..20], "b10", %w[a2], hint: "Filter: \"b\"") + output_helper("What room?", choices[14..17], "b6", %w[a2], hint: "Filter: \"b\"") + output_helper("What room?", choices[10..13], "b2", %w[a2], hint: "Filter: \"b\"") + output_helper("What room?", choices[10..13], "b2", %w[a2 b2], hint: "Filter: \"b\"") + exit_message("What room?", %w[a2 b2]) expect(prompt.output.string).to eq(expected_output) end end context "with filter" do it "doesn't lose the selection when switching between filters" do choices = %w[Tiny Medium Large Huge] prompt.on(:keypress) { |e| prompt.trigger(:keydelete) if e.value == "\r" } prompt.input << " " # select `Tiny` prompt.input << "a" << " " # match and select `Large` prompt.input << "\u007F" # backspace (shows all) prompt.input << "\r" prompt.input.rewind answer = prompt.multi_select("What size?", choices, filter: true, show_help: :always) expect(answer).to eql(%w[Tiny Large]) expected_output = output_helper("What size?", %w[Tiny Medium Large Huge], "Tiny", %w[], init: true, hint: "Press #{up_down} arrow to move, Space/Ctrl+A|R to select (all|rev), Enter to finish and letters to filter") + output_helper("What size?", %w[Tiny Medium Large Huge], "Tiny", %w[Tiny], hint: "Press #{up_down} arrow to move, Space/Ctrl+A|R to select (all|rev), Enter to finish and letters to filter") + output_helper("What size?", %w[Large], "Large", %w[Tiny], hint: "Filter: \"a\"") + output_helper("What size?", %w[Large], "Large", %w[Tiny Large], hint: "Filter: \"a\"") + output_helper("What size?", %w[Tiny Medium Large Huge], "Tiny", %w[Tiny Large], hint: "Press #{up_down} arrow to move, Space/Ctrl+A|R to select (all|rev), Enter to finish and letters to filter") + exit_message("What size?", %w[Tiny Large]) expect(prompt.output.string).to eql(expected_output) end end context "with :disabled" do it "fails when default item is also disabled" do choices = [ {name: "vodka", disabled: true}, "beer", "wine", "whisky", "bourbon" ] expect { prompt.multi_select("Select drinks?", choices, default: 1) }.to raise_error(TTY::Prompt::ConfigurationError, "default index `1` matches disabled choice") end it "adjusts active index to match first non-disabled choice" do choices = [ {name: "vodka", disabled: true}, "beer", "wine", "whisky", "bourbon" ] prompt.input << " " << "\r" prompt.input.rewind answer = prompt.multi_select("Select drinks?", choices) expect(answer).to eq(["beer"]) end it "omits disabled choice when nagivating menu" do choices = [ {name: "A"}, {name: "B", disabled: "(out)"}, {name: "C", disabled: "(out)"}, {name: "D"}, {name: "E"} ] prompt.on(:keypress) { |e| prompt.trigger(:keydown) if e.value == "j" } prompt.input << "j" << " " << "j" << " " << "\r" prompt.input.rewind answer = prompt.multi_select("What letter?", choices) expect(answer).to eq(%w[D E]) expected_output = output_helper("What letter?", choices, "A", [], init: true, hint: "Press #{up_down} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") + output_helper("What letter?", choices, "D", []) + output_helper("What letter?", choices, "D", %w[D]) + output_helper("What letter?", choices, "E", %w[D]) + output_helper("What letter?", choices, "E", %w[D E]) + exit_message("What letter?", %w[D E]) expect(prompt.output.string).to eq(expected_output) end it "omits disabled choice when number key is pressed" do choices = [ {name: "vodka", value: 1}, {name: "beer", value: 1, disabled: true}, {name: "wine", value: 1}, {name: "whisky", value: 1, disabled: true}, {name: "bourbon", value: 1} ] prompt.input << "2" << " \r" prompt.input.rewind answer = prompt.multi_select("Select drinks?") do |menu| menu.enum ")" menu.choice :vodka, 1 menu.choice :beer, 2, disabled: true menu.choice :wine, 3 menu.choice :whisky, 4, disabled: true menu.choice :bourbon, 5 end expect(answer).to eq([1]) expected_output = output_helper("Select drinks?", choices, "vodka", [], init: true, enum: ") ", hint: "Press #{up_down} arrow or 1-5 number to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") + output_helper("Select drinks?", choices, "vodka", [], enum: ") ") + output_helper("Select drinks?", choices, "vodka", %w[vodka], enum: ") ") + exit_message("Select drinks?", %w[vodka]) expect(prompt.output.string).to eq(expected_output) end it "selects all non-disabled choices when ctrl+a is pressed" do choices = [ {name: "A"}, {name: "B", disabled: "(out)"}, {name: "C", disabled: "(out)"}, {name: "D"}, {name: "E"} ] prompt.on(:keypress) { |e| prompt.trigger(:keyctrl_a) if e.value == "a" prompt.trigger(:keyctrl_r) if e.value == "r" } prompt.input << "a" << "r" << "a" << "\r" prompt.input.rewind answer = prompt.multi_select("What letter?", choices) expect(answer).to eq(%w[A D E]) expected_output = output_helper("What letter?", choices, "A", [], init: true, hint: "Press #{up_down} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") + output_helper("What letter?", choices, "A", %w[A D E]) + output_helper("What letter?", choices, "A", %w[]) + output_helper("What letter?", choices, "A", %w[A D E]) + exit_message("What letter?", %w[A D E]) expect(prompt.output.string).to eq(expected_output) end end context "with :min" do it "requires number of choices" do choices = %w[A B C] prompt.on(:keypress) { |e| prompt.trigger(:keydown) if e.value == "j" } prompt.input << " " << "\r" << "j" << " " << "\r" prompt.input.rewind value = prompt.multi_select("What letter?", choices, min: 2, per_page: 100) expect(value).to eq(%w[A B]) expected_output = output_helper("What letter?", choices, "A", [], init: true, min: 2, hint: "Press #{up_down} arrow to move, Space/Ctrl+A|R to select (all|rev) and Enter to finish") + output_helper("What letter?", choices, "A", %w[A], min: 2) + output_helper("What letter?", choices, "A", %w[A], min: 2) + output_helper("What letter?", choices, "B", %w[A], min: 2) + output_helper("What letter?", choices, "B", %w[A B], min: 2) + exit_message("What letter?", %w[A B]) expect(prompt.output.string).to eq(expected_output) end end context "with :max" do it "limits number of choices" do choices = %w[A B C] prompt.on(:keypress) { |e| prompt.trigger(:keyup) if e.value == "k" prompt.trigger(:keydown) if e.value == "j" } prompt.input << " " << "j" << " " << "j" << " " << "k" << " " << "j" << " " << "\r" prompt.input.rewind value = prompt.multi_select("What letter?", choices, max: 2, per_page: 100) expect(value).to eq(%w[A C]) expected_output = output_helper("What letter?", choices, "A", [], init: true, max: 2, hint: "Press #{up_down} arrow to move, Space to select and Enter to finish") + output_helper("What letter?", choices, "A", %w[A], max: 2) + output_helper("What letter?", choices, "B", %w[A], max: 2) + output_helper("What letter?", choices, "B", %w[A B], max: 2) + output_helper("What letter?", choices, "C", %w[A B], max: 2) + output_helper("What letter?", choices, "C", %w[A B], max: 2) + output_helper("What letter?", choices, "B", %w[A B], max: 2) + output_helper("What letter?", choices, "B", %w[A], max: 2) + output_helper("What letter?", choices, "C", %w[A], max: 2) + output_helper("What letter?", choices, "C", %w[A C], max: 2) + exit_message("What letter?", %w[A C]) expect(prompt.output.string).to eq(expected_output) end it "disables Ctrl+a/Ctrl+r selection when :max option is specified" do choices = %w[A B C D E F G] prompt.on(:keypress) { |e| prompt.trigger(:keyctrl_a) if e.value == "a" prompt.trigger(:keyctrl_r) if e.value == "r" prompt.trigger(:keydown) if e.value == "j" } prompt.input << "a" << "j" << " " << "r" << "j" << " " << "\r" prompt.input.rewind value = prompt.multi_select("What letter?", choices, max: 2, per_page: 100) expect(value).to eq(%w[B C]) expected_output = output_helper("What letter?", choices, "A", [], init: true, max: 2, hint: "Press #{up_down} arrow to move, Space to select and Enter to finish") + output_helper("What letter?", choices, "A", %w[], max: 2) + output_helper("What letter?", choices, "B", %w[], max: 2) + output_helper("What letter?", choices, "B", %w[B], max: 2) + output_helper("What letter?", choices, "B", %w[B], max: 2) + output_helper("What letter?", choices, "C", %w[B], max: 2) + output_helper("What letter?", choices, "C", %w[B C], max: 2) + exit_message("What letter?", %w[B C]) expect(prompt.output.string).to eq(expected_output) end end end tty-prompt-0.23.1/spec/unit/multiline_spec.rb000066400000000000000000000046631403662044600212240ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "#multiline" do subject(:prompt) { TTY::Prompt::Test.new } it "reads no lines" do prompt.input << "\C-d" prompt.input.rewind answer = prompt.multiline("Description?") expect(answer).to eq([]) expect(prompt.output.string).to eq([ "Description? \e[90m(Press Ctrl+D or Ctrl+Z to finish)\e[0m\n", "\e[2K\e[1G\e[1A\e[2K\e[1G", "Description? \n" ].join) end it "uses defualt when no input" do prompt.input << "\C-d" prompt.input.rewind answer = prompt.multiline("Description?", default: "A super sweet prompt") expect(answer).to eq("A super sweet prompt") expect(prompt.output.string).to eq([ "Description? \e[90m(Press Ctrl+D or Ctrl+Z to finish)\e[0m\n", "\e[2K\e[1G\e[1A\e[2K\e[1G", "Description? \e[32mA super sweet prompt\e[0m\n" ].join) end it "changes help text" do prompt.input << "\C-d" prompt.input.rewind answer = prompt.multiline("Description?") do |q| q.default "A super sweet prompt" q.help "(Press thy ctrl-d to end)" end expect(answer).to eq("A super sweet prompt") expect(prompt.output.string).to eq([ "Description? \e[90m(Press thy ctrl-d to end)\e[0m\n", "\e[2K\e[1G\e[1A\e[2K\e[1G", "Description? \e[32mA super sweet prompt\e[0m\n" ].join) end it "sets quiet mode" do prompt = TTY::Prompt::Test.new(quiet: true) prompt.input << "\C-d" prompt.input.rewind answer = prompt.multiline("Description?", default: "A super sweet prompt") expect(answer).to eq("A super sweet prompt") expect(prompt.output.string).to eq([ "Description? \e[90m(Press Ctrl+D or Ctrl+Z to finish)\e[0m\n", "\e[2K\e[1G\e[1A\e[2K\e[1G" ].join) end it "reads multiple lines with empty lines" do prompt.input << "aa\n\nbb\n\n\ncc\C-d" prompt.input.rewind answer = prompt.multiline("Description?") expect(answer).to eq(%W[aa\n bb\n cc]) expect(prompt.output.string).to eq([ "Description? \e[90m(Press Ctrl+D or Ctrl+Z to finish)\e[0m\n", "\e[2K\e[1Ga", "\e[2K\e[1Gaa", "\e[2K\e[1Gaa\n", "\e[2K\e[1G\n", "\e[2K\e[1Gb", "\e[2K\e[1Gbb", "\e[2K\e[1Gbb\n", "\e[2K\e[1G\n", "\e[2K\e[1G\n", "\e[2K\e[1Gc", "\e[2K\e[1Gcc", "\e[2K\e[1G\e[1A" * 6, "\e[2K\e[1G", "Description? \e[32maa ...\e[0m\n" ].join) end end tty-prompt-0.23.1/spec/unit/new_spec.rb000066400000000000000000000007771403662044600200150ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, "#new" do let(:env) { {"TTY_TEST" => true} } it "sets prefix" do prompt = described_class.new(prefix: "[?]", env: env) expect(prompt.prefix).to eq("[?]") end it "sets input stream" do prompt = described_class.new(input: :stream1, env: env) expect(prompt.input).to eq(:stream1) end it "sets output stream" do prompt = described_class.new(output: :stream2, env: env) expect(prompt.output).to eq(:stream2) end end tty-prompt-0.23.1/spec/unit/ok_spec.rb000066400000000000000000000011161403662044600176210ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, "#ok" do subject(:prompt) { TTY::Prompt::Test.new } it "prints text in green" do prompt.ok("All is fine") expect(prompt.output.string).to eq("\e[32mAll is fine\e[0m\n") end it "prints multiple lines in green" do prompt.ok("All is fine", "All is good") expect(prompt.output.string) .to eq("\e[32mAll is fine\e[0m\n\e[32mAll is good\e[0m\n") end it "changes color to cyan" do prompt.ok("All is fine", color: :cyan) expect(prompt.output.string).to eq("\e[36mAll is fine\e[0m\n") end end tty-prompt-0.23.1/spec/unit/paginator_spec.rb000066400000000000000000000065121403662044600212010ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Paginator, "#paginate" do it "ignores per_page when equal items " do list = %w[a b c d] paginator = described_class.new(per_page: 4) expect(paginator.paginate(list, 1).to_a).to eq([ ["a", 0], ["b", 1], ["c", 2], ["d", 3] ]) end it "ignores per_page when less items " do list = %w[a b c d] paginator = described_class.new(per_page: 5) expect(paginator.paginate(list, 1).to_a).to eq([ ["a", 0], ["b", 1], ["c", 2], ["d", 3] ]) end it "paginates items matching per_page count" do list = %w[a b c d e f] paginator = described_class.new(per_page: 3) expect(paginator.paginate(list, 1).to_a).to eq([["a", 0], ["b", 1], ["c", 2]]) expect(paginator.paginate(list, 2).to_a).to eq([["a", 0], ["b", 1], ["c", 2]]) expect(paginator.paginate(list, 3).to_a).to eq([["b", 1], ["c", 2], ["d", 3]]) expect(paginator.paginate(list, 4).to_a).to eq([["c", 2], ["d", 3], ["e", 4]]) expect(paginator.paginate(list, 5).to_a).to eq([["d", 3], ["e", 4], ["f", 5]]) expect(paginator.paginate(list, 6).to_a).to eq([["d", 3], ["e", 4], ["f", 5]]) expect(paginator.paginate(list, 7).to_a).to eq([["d", 3], ["e", 4], ["f", 5]]) end it "paginates items not matching per_page count" do list = %w[a b c d e f g] paginator = described_class.new(per_page: 3) expect(paginator.paginate(list, 1).to_a).to eq([["a", 0], ["b", 1], ["c", 2]]) expect(paginator.paginate(list, 2).to_a).to eq([["a", 0], ["b", 1], ["c", 2]]) expect(paginator.paginate(list, 3).to_a).to eq([["b", 1], ["c", 2], ["d", 3]]) expect(paginator.paginate(list, 4).to_a).to eq([["c", 2], ["d", 3], ["e", 4]]) expect(paginator.paginate(list, 5).to_a).to eq([["d", 3], ["e", 4], ["f", 5]]) expect(paginator.paginate(list, 6).to_a).to eq([["e", 4], ["f", 5], ["g", 6]]) expect(paginator.paginate(list, 7).to_a).to eq([["e", 4], ["f", 5], ["g", 6]]) expect(paginator.paginate(list, 8).to_a).to eq([["e", 4], ["f", 5], ["g", 6]]) end it "finds both start and end index for current selection" do list = %w[a b c d e f g] paginator = described_class.new(per_page: 3, default: 0) paginator.paginate(list, 2) expect(paginator.start_index).to eq(0) expect(paginator.end_index).to eq(2) paginator.paginate(list, 3) expect(paginator.start_index).to eq(1) expect(paginator.end_index).to eq(3) paginator.paginate(list, 4) expect(paginator.start_index).to eq(2) expect(paginator.end_index).to eq(4) paginator.paginate(list, 5) expect(paginator.start_index).to eq(3) expect(paginator.end_index).to eq(5) paginator.paginate(list, 7) expect(paginator.start_index).to eq(4) expect(paginator.end_index).to eq(6) paginator.paginate(list, 8) expect(paginator.start_index).to eq(4) expect(paginator.end_index).to eq(6) end it "starts with default selection" do list = %w[a b c d e f g] paginator = described_class.new(per_page: 3, default: 3) expect(paginator.paginate(list, 4).to_a).to eq([["d", 3], ["e", 4], ["f", 5]]) end it "doesn't accept invalid pagination" do list = %w[a b c d e f g] paginator = described_class.new(per_page: 0) expect { paginator.paginate(list, 4) }.to raise_error(TTY::Prompt::InvalidArgument, /per_page must be > 0/) end end tty-prompt-0.23.1/spec/unit/question/000077500000000000000000000000001403662044600175215ustar00rootroot00000000000000tty-prompt-0.23.1/spec/unit/question/checks_spec.rb000066400000000000000000000075331403662044600223300ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question do subject(:prompt) { TTY::Prompt::Test.new } it "passes range check" do question = described_class.new(prompt) question.in 1..10 result = TTY::Prompt::Question::Checks::CheckRange.call(question, 2) expect(result).to eq([2]) end it "fails range check" do question = described_class.new(prompt, messages: TTY::Prompt.messages) question.in 1..10 result = TTY::Prompt::Question::Checks::CheckRange.call(question, 11) expect(result).to eq([11, ["Value 11 must be within the range 1..10"]]) end it "fails range check" do question = described_class.new(prompt) question.in 1..10, "Outside of range!" result = TTY::Prompt::Question::Checks::CheckRange.call(question, 11) expect(result).to eq([11, ["Outside of range!"]]) end it "passes validation check" do question = described_class.new(prompt) question.validate(/\A\d{5}\Z/) result = TTY::Prompt::Question::Checks::CheckValidation.call(question, "12345") expect(result).to eq(["12345"]) end it "fails validation check" do question = described_class.new(prompt, messages: TTY::Prompt.messages) question.validate(/\A\d{5}\Z/) result = TTY::Prompt::Question::Checks::CheckValidation.call(question, "123") expect(result).to eq(["123", ["Your answer is invalid (must match /\\A\\d{5}\\Z/)"]]) end it "fails validation check with inlined custom message" do question = described_class.new(prompt) question.validate(/\A\w+@\w+\.\w+\Z/, "Invalid email address: %{value}") result = TTY::Prompt::Question::Checks::CheckValidation.call(question, "piotr@com") expect(result).to eq(["piotr@com", ["Invalid email address: piotr@com"]]) end it "fails validation check with custom message" do question = described_class.new(prompt) question.validate(/\A\w+@\w+\.\w+\Z/) question.messages[:valid?] = "Invalid email address: %{value}" result = TTY::Prompt::Question::Checks::CheckValidation.call(question, "piotr@com") expect(result).to eq(["piotr@com", ["Invalid email address: piotr@com"]]) end it "passes required check" do question = described_class.new(prompt) question.required true result = TTY::Prompt::Question::Checks::CheckRequired.call(question, "Piotr") expect(result).to eq(["Piotr"]) end it "fails required check" do question = described_class.new(prompt, messages: TTY::Prompt.messages) question.required true result = TTY::Prompt::Question::Checks::CheckRequired.call(question, nil) expect(result).to eq([nil, ["Value must be provided"]]) end it "fails required check with custom message" do question = described_class.new(prompt) question.required true, "Required input" result = TTY::Prompt::Question::Checks::CheckRequired.call(question, nil) expect(result).to eq([nil, ["Required input"]]) end it "passes convert check" do question = described_class.new(prompt) question.convert :bool result = TTY::Prompt::Question::Checks::CheckConversion.call(question, "t") expect(result).to eq([true]) end it "fails convert check" do question = described_class.new(prompt, messages: TTY::Prompt.messages) question.convert :bool result = TTY::Prompt::Question::Checks::CheckConversion.call(question, "x") expect(result).to eq(["x", ["Cannot convert `x` to 'bool' type"]]) end it "fails convert check with custom message" do question = described_class.new(prompt) question.convert :bool, "Wrong conversion value of `%{value}` for %{type}" result = TTY::Prompt::Question::Checks::CheckConversion.call(question, "x") expect(result).to eq(["x", ["Wrong conversion value of `x` for bool"]]) end end tty-prompt-0.23.1/spec/unit/question/default_spec.rb000066400000000000000000000016351403662044600225110ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "#default" do subject(:prompt) { TTY::Prompt::Test.new } it "uses default value" do name = "Anonymous" prompt.input << "\n" prompt.input.rewind answer = prompt.ask("What is your name?", default: name) expect(answer).to eq(name) expect(prompt.output.string).to eq([ "What is your name? \e[90m(Anonymous)\e[0m ", "\e[2K\e[1GWhat is your name? \e[90m(Anonymous)\e[0m \n", "\e[1A\e[2K\e[1G", "What is your name? \e[32mAnonymous\e[0m\n" ].join) end it "uses default value in block" do name = "Anonymous" answer = prompt.ask("What is your name?") { |q| q.default(name) } expect(answer).to eq(name) expect(prompt.output.string).to eq([ "What is your name? \e[90m(Anonymous)\e[0m ", "\e[1A\e[2K\e[1G", "What is your name? \e[32mAnonymous\e[0m\n" ].join) end end tty-prompt-0.23.1/spec/unit/question/echo_spec.rb000066400000000000000000000022711403662044600220000ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "#echo" do subject(:prompt) { TTY::Prompt::Test.new } it "asks with echo on" do prompt.input << "password" prompt.input.rewind answer = prompt.ask("What is your password?") { |q| q.echo(true) } expect(answer).to eql("password") expect(prompt.output.string).to eq([ "What is your password? ", "\e[2K\e[1GWhat is your password? p", "\e[2K\e[1GWhat is your password? pa", "\e[2K\e[1GWhat is your password? pas", "\e[2K\e[1GWhat is your password? pass", "\e[2K\e[1GWhat is your password? passw", "\e[2K\e[1GWhat is your password? passwo", "\e[2K\e[1GWhat is your password? passwor", "\e[2K\e[1GWhat is your password? password", "\e[1A\e[2K\e[1G", "What is your password? \e[32mpassword\e[0m\n" ].join) end it "asks with echo off" do prompt.input << "password" prompt.input.rewind answer = prompt.ask("What is your password?", echo: false) expect(answer).to eql("password") expect(prompt.output.string).to eq([ "What is your password? ", "\e[1A\e[2K\e[1G", "What is your password? \n" ].join) end end tty-prompt-0.23.1/spec/unit/question/in_spec.rb000066400000000000000000000063671403662044600215020ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "#in" do subject(:prompt) { TTY::Prompt::Test.new } it "reads range from option" do prompt.input << "8" prompt.input.rewind answer = prompt.ask("How do you like it on scale 1-10?", in: "1-10") expect(answer).to eq("8") end it "reads number within string range" do prompt.input << "8" prompt.input.rewind answer = prompt.ask("How do you like it on scale 1-10?") do |q| q.in("1-10") end expect(answer).to eq("8") expect(prompt.output.string).to eq([ "How do you like it on scale 1-10? ", "\e[2K\e[1GHow do you like it on scale 1-10? 8", "\e[1A\e[2K\e[1G", "How do you like it on scale 1-10? \e[32m8\e[0m\n" ].join) end it "reads number within digit range" do prompt.input << "8.1" prompt.input.rewind answer = prompt.ask("How do you like it on scale 1-10?") do |q| q.in(1.0..11.5) end expect(answer).to eq("8.1") expect(prompt.output.string).to eq([ "How do you like it on scale 1-10? ", "\e[2K\e[1GHow do you like it on scale 1-10? 8", "\e[2K\e[1GHow do you like it on scale 1-10? 8.", "\e[2K\e[1GHow do you like it on scale 1-10? 8.1", "\e[1A\e[2K\e[1G", "How do you like it on scale 1-10? \e[32m8.1\e[0m\n" ].join) end it "reads letters within range" do prompt.input << "E" prompt.input.rewind answer = prompt.ask("Your favourite vitamin? (A-K)") do |q| q.in("A-K") end expect(answer).to eq("E") expect(prompt.output.string).to eq([ "Your favourite vitamin? (A-K) ", "\e[2K\e[1GYour favourite vitamin? (A-K) E", "\e[1A\e[2K\e[1G", "Your favourite vitamin? (A-K) \e[32mE\e[0m\n" ].join) end it "provides default error message when wrong input" do prompt.input << "A\n2\n" prompt.input.rewind answer = prompt.ask("How spicy on scale? (1-5)", in: "1-5") expect(answer).to eq("2") expect(prompt.output.string).to eq([ "How spicy on scale? (1-5) ", "\e[2K\e[1GHow spicy on scale? (1-5) A", "\e[2K\e[1GHow spicy on scale? (1-5) A\n", "\e[31m>>\e[0m Value A must be within the range 1..5\e[1A", "\e[2K\e[1G", "How spicy on scale? (1-5) ", "\e[2K\e[1GHow spicy on scale? (1-5) 2", "\e[2K\e[1GHow spicy on scale? (1-5) 2\n", "\e[2K\e[1G", "\e[1A\e[2K\e[1G", "How spicy on scale? (1-5) \e[32m2\e[0m\n" ].join) end it "overwrites default error message when wrong input" do prompt.input << "A\n2\n" prompt.input.rewind answer = prompt.ask("How spicy on scale? (1-5)") do |q| q.in "1-5" q.messages[:range?] = "Ohh dear what is this %{value} doing in %{in}?" end expect(answer).to eq("2") expect(prompt.output.string).to eq([ "How spicy on scale? (1-5) ", "\e[2K\e[1GHow spicy on scale? (1-5) A", "\e[2K\e[1GHow spicy on scale? (1-5) A\n", "\e[31m>>\e[0m Ohh dear what is this A doing in 1..5?\e[1A", "\e[2K\e[1G", "How spicy on scale? (1-5) ", "\e[2K\e[1GHow spicy on scale? (1-5) 2", "\e[2K\e[1GHow spicy on scale? (1-5) 2\n", "\e[2K\e[1G", "\e[1A\e[2K\e[1G", "How spicy on scale? (1-5) \e[32m2\e[0m\n" ].join) end end tty-prompt-0.23.1/spec/unit/question/initialize_spec.rb000066400000000000000000000005151403662044600232220ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "#initialize" do subject(:question) { described_class.new(TTY::Prompt::Test.new) } it { expect(question.echo).to eq(true) } it { expect(question.modifier).to eq([]) } it { expect(question.validation).to eq(TTY::Prompt::Question::UndefinedSetting) } end tty-prompt-0.23.1/spec/unit/question/modifier/000077500000000000000000000000001403662044600213175ustar00rootroot00000000000000tty-prompt-0.23.1/spec/unit/question/modifier/apply_to_spec.rb000066400000000000000000000013621403662044600245070ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question::Modifier, "#apply_to" do let(:string) { "text to be modified" } it "doesn't apply modifiers" do modifier = described_class.new([]) expect(modifier.apply_to(string)).to eq(string) end it "combines whitespace & letter case modifications" do modifiers = %i[collapse capitalize] modifier = described_class.new(modifiers) modified = modifier.apply_to(string) expect(modified).to eq("Text to be modified") end it "combines letter case & whitespace modifications" do modifiers = %i[up collapse] modifier = described_class.new(modifiers) modified = modifier.apply_to(string) expect(modified).to eq("TEXT TO BE MODIFIED") end end tty-prompt-0.23.1/spec/unit/question/modifier/letter_case_spec.rb000066400000000000000000000021151403662044600251470ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question::Modifier, "#letter_case" do context "string" do let(:string) { "text to modify" } it "changes to uppercase" do modified = described_class.letter_case(:up, string) expect(modified).to eq("TEXT TO MODIFY") end it "changes to lower case" do modified = described_class.letter_case(:down, string) expect(modified).to eq("text to modify") end it "capitalizes text" do modified = described_class.letter_case(:capitalize, string) expect(modified).to eq("Text to modify") end end context "nil (empty user input)" do let(:string) { nil } example "up returns nil" do modified = described_class.letter_case(:up, string) expect(modified).to be_nil end example "down returns nil" do modified = described_class.letter_case(:down, string) expect(modified).to be_nil end example "capitalize returns nil" do modified = described_class.letter_case(:capitalize, string) expect(modified).to be_nil end end end tty-prompt-0.23.1/spec/unit/question/modifier/whitespace_spec.rb000066400000000000000000000026251403662044600250170ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question::Modifier, "#whitespace" do context "string with whitespaces" do let(:string) { " text\t \n to\t modify\r\n" } it "trims whitespace" do modified = described_class.whitespace(:trim, string) expect(modified).to eq("text\t \n to\t modify") end it "chomps whitespace" do modified = described_class.whitespace(:chomp, string) expect(modified).to eq(" text\t \n to\t modify") end it "collapses text" do modified = described_class.whitespace(:collapse, string) expect(modified).to eq(" text to modify ") end it "removes whitespace" do modified = described_class.whitespace(:remove, string) expect(modified).to eq("texttomodify") end end context "nil (empty user input)" do let(:string) { nil } example "trim returns nil" do modified = described_class.whitespace(:trim, string) expect(modified).to be_nil end example "chomp returns nil" do modified = described_class.whitespace(:chomp, string) expect(modified).to be_nil end example "collapse returns nil" do modified = described_class.whitespace(:collapse, string) expect(modified).to be_nil end example "remove returns nil" do modified = described_class.whitespace(:remove, string) expect(modified).to be_nil end end end tty-prompt-0.23.1/spec/unit/question/modify_spec.rb000066400000000000000000000024331403662044600223510ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "#modify" do subject(:prompt) { TTY::Prompt::Test.new } it "preserves answer for unkown modification" do prompt.input << "piotr" prompt.input.rewind answer = prompt.ask("What is your name?") { |q| q.modify(:none) } expect(answer).to eq("piotr") end it "converts to upper case" do prompt.input << "piotr" prompt.input.rewind answer = prompt.ask("What is your name?") { |q| q.modify(:upcase) } expect(answer).to eq("PIOTR") end it "trims whitespace" do prompt.input << " Some white\t space\t \there! \n" prompt.input.rewind answer = prompt.ask("Enter some text: ") { |q| q.modify(:trim) } expect(answer).to eq("Some white\t space\t \there!") end it "collapses whitespace" do prompt.input << " Some white\t space\t \there! \n" prompt.input.rewind answer = prompt.ask("Enter some text: ") { |q| q.modify(:collapse) } expect(answer).to eq(" Some white space here! ") end it "strips and collapses whitespace" do prompt.input << " Some white\t space\t \there! \n" prompt.input.rewind answer = prompt.ask("Enter some text: ") { |q| q.modify(:strip, :collapse) } expect(answer).to eq("Some white space here!") end end tty-prompt-0.23.1/spec/unit/question/required_spec.rb000066400000000000000000000055261403662044600227100ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "#required" do subject(:prompt) { TTY::Prompt::Test.new } it "requires value to be present" do prompt.input << "Piotr" prompt.input.rewind prompt.ask("What is your name?") { |q| q.required(true) } expect(prompt.output.string).to eq([ "What is your name? ", "\e[2K\e[1GWhat is your name? P", "\e[2K\e[1GWhat is your name? Pi", "\e[2K\e[1GWhat is your name? Pio", "\e[2K\e[1GWhat is your name? Piot", "\e[2K\e[1GWhat is your name? Piotr", "\e[1A\e[2K\e[1G", "What is your name? \e[32mPiotr\e[0m\n" ].join) end it "requires value to be present with option" do prompt.input << " \nPiotr" prompt.input.rewind prompt.ask("What is your name?", required: true) expect(prompt.output.string).to eq([ "What is your name? ", "\e[2K\e[1GWhat is your name? ", "\e[2K\e[1GWhat is your name? ", "\e[2K\e[1GWhat is your name? \n", "\e[31m>>\e[0m Value must be provided\e[1A", "\e[2K\e[1G", "What is your name? ", "\e[2K\e[1GWhat is your name? P", "\e[2K\e[1GWhat is your name? Pi", "\e[2K\e[1GWhat is your name? Pio", "\e[2K\e[1GWhat is your name? Piot", "\e[2K\e[1GWhat is your name? Piotr", "\e[2K\e[1G", "\e[1A\e[2K\e[1G", "What is your name? \e[32mPiotr\e[0m\n" ].join) end it "doesn't require value to be present" do prompt.input << "" prompt.input.rewind answer = prompt.ask("What is your name?") { |q| q.required(false) } expect(answer).to be_nil end it "uses required in validation check" do prompt.input << " \nexists\ntest\n" prompt.input.rewind answer = prompt.ask("File name?") do |q| q.required(true) q.validate { |v| v !~ /exists/ } q.messages[:required?] = "File name must not be empty!" q.messages[:valid?] = "File already exists!" end expect(answer).to eq("test") expect(prompt.output.string).to eq([ "File name? ", "\e[2K\e[1GFile name? ", "\e[2K\e[1GFile name? ", "\e[2K\e[1GFile name? \n", "\e[31m>>\e[0m File name must not be empty!", "\e[1A\e[2K\e[1G", "File name? ", "\e[2K\e[1GFile name? e", "\e[2K\e[1GFile name? ex", "\e[2K\e[1GFile name? exi", "\e[2K\e[1GFile name? exis", "\e[2K\e[1GFile name? exist", "\e[2K\e[1GFile name? exists", "\e[2K\e[1GFile name? exists\n", "\e[31m>>\e[0m File already exists!", "\e[1A\e[2K\e[1G", "File name? ", "\e[2K\e[1GFile name? t", "\e[2K\e[1GFile name? te", "\e[2K\e[1GFile name? tes", "\e[2K\e[1GFile name? test", "\e[2K\e[1GFile name? test\n", "\e[2K\e[1G", "\e[1A\e[2K\e[1G", "File name? \e[32mtest\e[0m\n" ].join) expect(answer).to eq("test") end end tty-prompt-0.23.1/spec/unit/question/validate_spec.rb000066400000000000000000000072661403662044600226640ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "#validate" do subject(:prompt) { TTY::Prompt::Test.new } it "validates input with regex" do prompt.input << "p.m" prompt.input.rewind answer = prompt.ask("What is your username?") do |q| q.validate(/^[^.]+\.[^.]+/) end expect(answer).to eq("p.m") expect(prompt.output.string).to eq([ "What is your username? ", "\e[2K\e[1GWhat is your username? p", "\e[2K\e[1GWhat is your username? p.", "\e[2K\e[1GWhat is your username? p.m", "\e[1A\e[2K\e[1G", "What is your username? \e[32mp.m\e[0m\n" ].join) end it "validates input with proc" do prompt.input << "piotr.murach" prompt.input.rewind answer = prompt.ask("What is your username?") do |q| q.validate { |input| input =~ /^[^.]+\.[^.]+/ } end expect(answer).to eq("piotr.murach") end it "understands custom validation like :email" do prompt.input << "piotr@example.com" prompt.input.rewind answer = prompt.ask("What is your email?") do |q| q.validate :email end expect(answer).to eq("piotr@example.com") end it "deprecates :validation option" do prompt.input << "piotr@example.com" prompt.input.rewind expect do prompt.ask("What is your email?", validation: :email) end.to output("[DEPRECATION] The `:validation` option is deprecated. Use `:validate` instead.\n").to_stderr end it "provides default error message for wrong input" do prompt.input << "wrong\np@m.com\n" prompt.input.rewind answer = prompt.ask("What is your email?") do |q| q.validate :email end expect(answer).to eq("p@m.com") expect(prompt.output.string).to eq([ "What is your email? ", "\e[2K\e[1GWhat is your email? w", "\e[2K\e[1GWhat is your email? wr", "\e[2K\e[1GWhat is your email? wro", "\e[2K\e[1GWhat is your email? wron", "\e[2K\e[1GWhat is your email? wrong", "\e[2K\e[1GWhat is your email? wrong\n", "\e[31m>>\e[0m Your answer is invalid (must match :email)\e[1A", "\e[2K\e[1G", "What is your email? ", "\e[2K\e[1GWhat is your email? p", "\e[2K\e[1GWhat is your email? p@", "\e[2K\e[1GWhat is your email? p@m", "\e[2K\e[1GWhat is your email? p@m.", "\e[2K\e[1GWhat is your email? p@m.c", "\e[2K\e[1GWhat is your email? p@m.co", "\e[2K\e[1GWhat is your email? p@m.com", "\e[2K\e[1GWhat is your email? p@m.com\n", "\e[2K\e[1G", "\e[1A\e[2K\e[1G", "What is your email? \e[32mp@m.com\e[0m\n" ].join) end it "provides custom error message for wrong input" do prompt.input << "wrong\np@m.com" prompt.input.rewind answer = prompt.ask("What is your email?", validate: :email) do |q| q.messages[:valid?] = "Not an email!" end expect(answer).to eq("p@m.com") expect(prompt.output.string).to eq([ "What is your email? ", "\e[2K\e[1GWhat is your email? w", "\e[2K\e[1GWhat is your email? wr", "\e[2K\e[1GWhat is your email? wro", "\e[2K\e[1GWhat is your email? wron", "\e[2K\e[1GWhat is your email? wrong", "\e[2K\e[1GWhat is your email? wrong\n", "\e[31m>>\e[0m Not an email!\e[1A", "\e[2K\e[1G", "What is your email? ", "\e[2K\e[1GWhat is your email? p", "\e[2K\e[1GWhat is your email? p@", "\e[2K\e[1GWhat is your email? p@m", "\e[2K\e[1GWhat is your email? p@m.", "\e[2K\e[1GWhat is your email? p@m.c", "\e[2K\e[1GWhat is your email? p@m.co", "\e[2K\e[1GWhat is your email? p@m.com", "\e[2K\e[1G", "\e[1A\e[2K\e[1G", "What is your email? \e[32mp@m.com\e[0m\n" ].join) end end tty-prompt-0.23.1/spec/unit/question/validation/000077500000000000000000000000001403662044600216535ustar00rootroot00000000000000tty-prompt-0.23.1/spec/unit/question/validation/call_spec.rb000066400000000000000000000016341403662044600241310ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question::Validation, "#call" do let(:pattern) { /^[^.]+\.[^.]+/ } it "validates nil input" do validation = described_class.new(pattern) expect(validation.(nil)).to eq(false) end it "validates successfully when the value matches pattern" do validation = described_class.new(pattern) expect(validation.("piotr.murach")).to eq(true) end it "validates with a proc" do pat = proc { |input| !pattern.match(input).nil? } validation = described_class.new(pat) expect(validation.call("piotr.murach")).to eq(true) end it "validates with custom name" do validation = described_class.new(:email) expect(validation.call("piotr@example.com")).to eq(true) end it "fails validation when not maching pattern" do validation = described_class.new(pattern) expect(validation.("piotrmurach")).to eq(false) end end tty-prompt-0.23.1/spec/unit/question/validation/coerce_spec.rb000066400000000000000000000014461403662044600244570ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question::Validation, "#coerce" do let(:instance) { described_class.new } it "coerces lambda into proc" do pattern = -> { "^[^\.]+\.[^\.]+" } validation = described_class.new(pattern) expect(validation.pattern).to be_a(Proc) end it "doesn't coerce symbols" do pattern = :email validation = described_class.new(pattern) expect(validation.pattern).to eq(:email) end it "coerces into regex" do pattern = /^[^.]+\.[^.]+/ validation = described_class.new(pattern) expect(validation.pattern).to be_a(Regexp) end it "fails to coerce pattern into validation" do pattern = Object.new expect { described_class.new(pattern) }.to raise_error(TTY::Prompt::ValidationCoercion) end end tty-prompt-0.23.1/spec/unit/quiet_spec.rb000066400000000000000000000010311403662044600203330ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Question, "#default" do subject(:prompt) { TTY::Prompt::Test.new(quiet: true) } it "sets quiet mode" do name = "Anonymous" prompt.input << "\n" prompt.input.rewind answer = prompt.ask("What is your name?", default: name) expect(answer).to eq(name) expect(prompt.output.string).to eq([ "What is your name? \e[90m(Anonymous)\e[0m ", "\e[2K\e[1GWhat is your name? \e[90m(Anonymous)\e[0m \n", "\e[1A\e[2K\e[1G" ].join) end end tty-prompt-0.23.1/spec/unit/result_spec.rb000066400000000000000000000020701403662044600205260ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Result do it "checks value to be invalid" do question = double(:question) result = TTY::Prompt::Result.new(question, nil) answer = result.with { |_quest, value| if value.nil? [value, ["`#{value}` provided cannot be empty"]] else value end } expect(answer).to be_a(TTY::Prompt::Result::Failure) expect(answer.success?).to eq(false) expect(answer.errors).to eq(["`` provided cannot be empty"]) end it "checks value to be valid" do question = double(:question) result = TTY::Prompt::Result.new(question, "Piotr") CheckRequired = Class.new do def self.call(_quest, value) if value.nil? [value, ["`#{value}` provided cannot be empty"]] else value end end end answer = result.with(CheckRequired) expect(answer).to be_a(TTY::Prompt::Result::Success) expect(answer.success?).to eq(true) expect(answer.value).to eq("Piotr") expect(answer.errors).to eq([]) end end tty-prompt-0.23.1/spec/unit/say_spec.rb000066400000000000000000000034261403662044600200120ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, "#say" do subject(:prompt) { TTY::Prompt::Test.new } it "prints an empty message" do prompt.say("") expect(prompt.output.string).to eq("") end context "with new line" do it "prints a message with newline" do prompt.say("Hell yeah!\n") expect(prompt.output.string).to eq("Hell yeah!\n") end it "prints a message with implicit newline" do prompt.say("Hell yeah!\n") expect(prompt.output.string).to eq("Hell yeah!\n") end it "prints a message with newline within text" do prompt.say("Hell\n yeah!") expect(prompt.output.string).to eq("Hell\n yeah!\n") end it "prints a message with newline within text and blank space" do prompt.say("Hell\n yeah! ") expect(prompt.output.string).to eq("Hell\n yeah! ") end it "prints a message without newline" do prompt.say("Hell yeah!", newline: false) expect(prompt.output.string).to eq("Hell yeah!") end end context "with tab or space" do it "prints " do prompt.say("Hell yeah!\t") expect(prompt.output.string).to eq("Hell yeah!\t") end end context "with color" do it "prints message with ansi color" do prompt.say("Hell yeah!", color: :green) expect(prompt.output.string).to eq("\e[32mHell yeah!\e[0m\n") end it "prints message with ansi color without newline" do prompt.say("Hell yeah! ", color: :green) expect(prompt.output.string).to eq("\e[32mHell yeah! \e[0m") end end context "without color" do it "prints message without ansi" do prompt = TTY::Prompt::Test.new(enable_color: false) prompt.say("Hell yeah!", color: :green) expect(prompt.output.string).to eq("Hell yeah!\n") end end end tty-prompt-0.23.1/spec/unit/select_spec.rb000066400000000000000000001136371403662044600205030ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, "#select" do let(:symbols) { TTY::Prompt::Symbols.symbols } let(:up_down) { "#{symbols[:arrow_up]}/#{symbols[:arrow_down]}" } let(:left_right) { "#{symbols[:arrow_left]}/#{symbols[:arrow_right]}" } let(:left_key) { "\e[D" } let(:right_key) { "\e[C" } subject(:prompt) { TTY::Prompt::Test.new } def output_helper(prompt, choices, active, options = {}) hint = options[:hint] init = options.fetch(:init, false) enum = options[:enum] out = [] out << "\e[?25l" if init out << prompt << " " out << "\e[90m(#{hint})\e[0m" if hint out << "\n" out << choices.map.with_index do |c, i| name = c.is_a?(Hash) ? c[:name] : c disabled = c.is_a?(Hash) ? c[:disabled] : false num = (i + 1).to_s + enum if enum if disabled "\e[31m#{symbols[:cross]}\e[0m #{num}#{name} #{disabled}" elsif name == active "\e[32m#{symbols[:marker]} #{num}#{name}\e[0m" else " #{num}#{name}" end end.join("\n") out << "\e[2K\e[1G\e[1A" * choices.count out << "\e[2K\e[1G" out << "\e[1A\e[2K\e[1G" if choices.empty? out.join end def exit_message(prompt, choice) "#{prompt} \e[32m#{choice}\e[0m\n\e[?25h" end # Ensure a wide prompt on CI before { allow(TTY::Screen).to receive(:width).and_return(200) } it "selects by default first option" do choices = %i[Large Medium Small] prompt.input << "\r" prompt.input.rewind expect(prompt.select("What size?", choices)).to eq(:Large) expected_output = output_helper("What size?", choices, :Large, init: true, hint: "Press #{up_down} arrow to move and Enter to select") + exit_message("What size?", "Large") expect(prompt.output.string).to eq(expected_output) end it "allows navigation using events without errors" do choices = %w[Large Medium Small] prompt.input << "j" << "\r" prompt.input.rewind prompt.on(:keypress) do |event| prompt.trigger(:keydown) if event.value == "j" end expect { prompt.select("What size?", choices) }.not_to output.to_stderr expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[90m(Press #{up_down} arrow to move and Enter to select)\e[0m\n", "\e[32m#{symbols[:marker]} Large\e[0m\n", " Medium\n", " Small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \n", " Large\n", "\e[32m#{symbols[:marker]} Medium\e[0m\n", " Small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[32mMedium\e[0m\n\e[?25h" ].join) end it "sets choice name and value" do choices = {large: 1, medium: 2, small: 3} prompt.input << " " prompt.input.rewind expect(prompt.select("What size?", choices, default: 1)).to eq(1) expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[90m(Press #{up_down} arrow to move and Enter to select)\e[0m\n", "\e[32m#{symbols[:marker]} large\e[0m\n", " medium\n", " small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[32mlarge\e[0m\n\e[?25h" ].join) end it "sets choice value to nil" do choices = {none: nil, large: 1, medium: 2, small: 3} prompt.input << " " prompt.input.rewind expect(prompt.select("What size?", choices, default: 1)).to eq(nil) expect(prompt.output.string).to eq([ output_helper("What size?", choices.keys, :none, init: true, hint: "Press #{up_down} arrow to move and Enter to select"), exit_message("What size?", "none") ].join) end it "sets choice name through DSL" do prompt.input << " " prompt.input.rewind value = prompt.select("What size?") do |menu| menu.symbols marker: ">" menu.choice "Large" menu.choice "Medium" menu.choice "Small" end expect(value).to eq("Large") expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[90m(Press #{up_down} arrow to move and Enter to select)\e[0m\n", "\e[32m> Large\e[0m\n", " Medium\n", " Small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[32mLarge\e[0m\n\e[?25h" ].join) end it "sets choice name & value through DSL" do prompt = TTY::Prompt::Test.new(symbols: {marker: ">"}) prompt.input << " " prompt.input.rewind value = prompt.select("What size?") do |menu| menu.choice :large, 1 menu.choice :medium, 2 menu.choice :small, 3 end expect(value).to eq(1) expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[90m(Press #{up_down} arrow to move and Enter to select)\e[0m\n", "\e[32m> large\e[0m\n", " medium\n", " small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[32mlarge\e[0m\n\e[?25h" ].join) end it "sets choices and single choice through DSL" do prompt.input << " " prompt.input.rewind value = prompt.select("What size?") do |menu| menu.choice "Large" menu.choices %w[Medium Small] end expect(value).to eq("Large") expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[90m(Press #{up_down} arrow to move and Enter to select)\e[0m\n", "\e[32m#{symbols[:marker]} Large\e[0m\n", " Medium\n", " Small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[32mLarge\e[0m\n\e[?25h" ].join) end it "sets choice name & value through DSL" do prompt.input << " " prompt.input.rewind value = prompt.select("What size?") do |menu| menu.default 2 menu.enum "." menu.choice :large, 1 menu.choice :medium, 2 menu.choice :small, 3 end expect(value).to eq(2) expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[90m(Press #{up_down} arrow or 1-3 number to move and Enter to select)\e[0m\n", " 1. large\n", "\e[32m#{symbols[:marker]} 2. medium\e[0m\n", " 3. small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[32mmedium\e[0m\n\e[?25h" ].join) end it "sets choice value to proc and executes it" do prompt.input << " " prompt.input.rewind value = prompt.select("What size?", default: 2, enum: ")") do |menu| menu.choice :large, 1 menu.choice :medium do "Good choice!" end menu.choice :small, 3 end expect(value).to eq("Good choice!") expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[90m(Press #{up_down} arrow or 1-3 number to move and Enter to select)\e[0m\n", " 1) large\n", "\e[32m#{symbols[:marker]} 2) medium\e[0m\n", " 3) small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[32mmedium\e[0m\n\e[?25h" ].join) end it "sets default option through hash syntax" do choices = %w[Large Medium Small] prompt.input << " " prompt.input.rewind expect(prompt.select("What size?", choices, default: 2, enum: ".")).to eq("Medium") expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[90m(Press #{up_down} arrow or 1-3 number to move and Enter to select)\e[0m\n", " 1. Large\n", "\e[32m#{symbols[:marker]} 2. Medium\e[0m\n", " 3. Small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[32mMedium\e[0m\n\e[?25h" ].join) end it "changes selected item color & marker" do choices = %w[Large Medium Small] prompt = TTY::Prompt::Test.new(symbols: {marker: ">"}) prompt.input << " " prompt.input.rewind options = {active_color: :blue, help_color: :red, symbols: {marker: ">"}} value = prompt.select("What size?", choices, **options) expect(value).to eq("Large") expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[31m(Press #{up_down} arrow to move and Enter to select)\e[0m\n", "\e[34m> Large\e[0m\n", " Medium\n", " Small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[34mLarge\e[0m\n\e[?25h" ].join) end it "changes help text" do choices = %w[Large Medium Small] prompt.input << " " prompt.input.rewind value = prompt.select("What size?", choices, help: "(Bash keyboard)") expect(value).to eq("Large") expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[90m(Bash keyboard)\e[0m\n", "\e[32m#{symbols[:marker]} Large\e[0m\n", " Medium\n", " Small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[32mLarge\e[0m\n\e[?25h" ].join) end it "changes help text through DSL" do choices = %w[Large Medium Small] prompt.input << " " prompt.input.rewind value = prompt.select("What size?") do |menu| menu.help "(Bash keyboard)" menu.choices choices end expect(value).to eq("Large") expect(prompt.output.string).to eq([ "\e[?25lWhat size? \e[90m(Bash keyboard)\e[0m\n", "\e[32m#{symbols[:marker]} Large\e[0m\n", " Medium\n", " Small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What size? \e[32mLarge\e[0m\n\e[?25h" ].join) end it "configures quiet mode" do choices = {large: 1, medium: 2, small: 3} prompt.input << " " prompt.input.rewind expect(prompt.select("What size?", choices, default: 1, quiet: true)).to eq(1) expected_output = output_helper("What size?", choices.keys, :large, init: true, hint: "Press #{up_down} arrow to move and Enter to select") + "\e[?25h" expect(prompt.output.string).to eq(expected_output) end it "sets quiet mode through DSL" do prompt.input << " " prompt.input.rewind expect(prompt.select("What size?") do |q| q.choice "Large" q.choice "Medium" q.choice "Small" q.quiet true end).to eq("Large") expected_output = output_helper("What size?", %w[Large Medium Small], "Large", init: true, hint: "Press #{up_down} arrow to move and Enter to select") + "\e[?25h" expect(prompt.output.string).to eq(expected_output) end it "sets prompt prefix" do prompt = TTY::Prompt::Test.new(prefix: "[?] ") choices = %w[Large Medium Small] prompt.input << "\r" prompt.input.rewind expect(prompt.select("What size?", choices)).to eq("Large") expect(prompt.output.string).to eq([ "\e[?25l[?] What size? \e[90m(Press #{up_down} arrow to move and Enter to select)\e[0m\n", "\e[32m#{symbols[:marker]} Large\e[0m\n", " Medium\n", " Small", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "[?] What size? \e[32mLarge\e[0m\n\e[?25h" ].join) end it "changes to always show help" do choices = {large: 1, medium: 2, small: 3} prompt.on(:keypress) { |e| prompt.trigger(:keydown) if e.value == "j" } prompt.input << "j" << "j" << " " prompt.input.rewind expect(prompt.select("What size?", choices, default: 1, show_help: :always)).to eq(3) expected_output = output_helper("What size?", choices.keys, :large, init: true, hint: "Press #{up_down} arrow to move and Enter to select") + output_helper("What size?", choices.keys, :medium, hint: "Press #{up_down} arrow to move and Enter to select") + output_helper("What size?", choices.keys, :small, hint: "Press #{up_down} arrow to move and Enter to select") + exit_message("What size?", "small") expect(prompt.output.string).to eq(expected_output) end it "changes to never show help" do choices = {large: 1, medium: 2, small: 3} prompt.on(:keypress) { |e| prompt.trigger(:keydown) if e.value == "j" } prompt.input << "j" << "j" << " " prompt.input.rewind answer = prompt.select("What size?", choices, default: 1) do |q| q.show_help :never end expect(answer).to eq(3) expected_output = output_helper("What size?", choices.keys, :large, init: true) + output_helper("What size?", choices.keys, :medium) + output_helper("What size?", choices.keys, :small) + exit_message("What size?", "small") expect(prompt.output.string).to eq(expected_output) end context "when paginated" do it "paginates long selections" do choices = %w[A B C D E F G H] prompt.input << "\r" prompt.input.rewind answer = prompt.select("What letter?", choices, per_page: 3, default: 4) expect(answer).to eq("D") expected_output = [ "\e[?25lWhat letter? \e[90m(Press #{up_down}/#{left_right} arrow to move and Enter to select)\e[0m\n", "\e[32m#{symbols[:marker]} D\e[0m\n", " E\n", " F", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What letter? \e[32mD\e[0m\n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end it "paginates choices as hash object" do prompt = TTY::Prompt::Test.new choices = {A: 1, B: 2, C: 3, D: 4, E: 5, F: 6, G: 7, H: 8} prompt.input << "\r" prompt.input.rewind answer = prompt.select("What letter?", choices, per_page: 3, default: 4) expect(answer).to eq(4) expected_output = [ "\e[?25lWhat letter? \e[90m(Press #{up_down}/#{left_right} arrow to move and Enter to select)\e[0m\n", "\e[32m#{symbols[:marker]} D\e[0m\n", " E\n", " F", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What letter? \e[32mD\e[0m\n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end it "paginates long selections through DSL" do prompt = TTY::Prompt::Test.new choices = %w[A B C D E F G H] prompt.input << "\r" prompt.input.rewind answer = prompt.select("What letter?", choices) do |menu| menu.per_page 3 menu.default 4 end expect(answer).to eq("D") expected_output = [ "\e[?25lWhat letter? \e[90m(Press #{up_down}/#{left_right} arrow to move and Enter to select)\e[0m\n", "\e[32m#{symbols[:marker]} D\e[0m\n", " E\n", " F", "\e[2K\e[1G\e[1A" * 3, "\e[2K\e[1G", "What letter? \e[32mD\e[0m\n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end it "navigates evenly paged output with right arrow until end of selection" do choices = ("1".."12").to_a prompt.on(:keypress) { |e| prompt.trigger(:keyright) if e.value == "l" } prompt.input << "l" << "l" << "l" << "\r" prompt.input.rewind answer = prompt.select("What number?", choices, per_page: 4) expect(answer).to eq("9") expected_output = [ output_helper("What number?", choices[0..3], "1", init: true, hint: "Press #{up_down}/#{left_right} arrow to move and Enter to select"), output_helper("What number?", choices[4..7], "5"), output_helper("What number?", choices[8..11], "9"), output_helper("What number?", choices[8..11], "9"), "What number? \e[32m9\e[0m\n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end it "navigates unevenly paged output with right arrow until the end of selection" do choices = ("1".."10").to_a prompt.on(:keypress) { |e| prompt.trigger(:keyright) if e.value == "l" } prompt.input << "l" << "l" << "l" << "\r" prompt.input.rewind answer = prompt.select("What number?", choices, default: 4, per_page: 4) expect(answer).to eq("10") expected_output = [ output_helper("What number?", choices[3..6], "4", init: true, hint: "Press #{up_down}/#{left_right} arrow to move and Enter to select"), output_helper("What number?", choices[4..7], "8"), output_helper("What number?", choices[8..9], "10"), output_helper("What number?", choices[8..9], "10"), "What number? \e[32m10\e[0m\n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end it "navigates left and right" do choices = ("1".."10").to_a prompt.on(:keypress) { |e| prompt.trigger(:keyright) if e.value == "l" prompt.trigger(:keyleft) if e.value == "h" } prompt.input << "l" << "l" << "h" << "\r" prompt.input.rewind answer = prompt.select("What number?", choices, default: 2, per_page: 4) expect(answer).to eq("6") expected_output = [ output_helper("What number?", choices[0..3], "2", init: true, hint: "Press #{up_down}/#{left_right} arrow to move and Enter to select"), output_helper("What number?", choices[4..7], "6"), output_helper("What number?", choices[8..9], "10"), output_helper("What number?", choices[4..7], "6"), "What number? \e[32m6\e[0m\n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end it "combines up/down navigation with left/right" do choices = ("1".."11").to_a prompt.on(:keypress) { |e| prompt.trigger(:keyup) if e.value == "k" prompt.trigger(:keydown) if e.value == "j" prompt.trigger(:keyright) if e.value == "l" prompt.trigger(:keyleft) if e.value == "h" } prompt.input << "j" << "l" << "k" << "k" << "h" << "\r" prompt.input.rewind answer = prompt.select("What number?", choices, default: 2, per_page: 4) expect(answer).to eq("1") expected_output = [ output_helper("What number?", choices[0..3], "2", init: true, hint: "Press #{up_down}/#{left_right} arrow to move and Enter to select"), output_helper("What number?", choices[0..3], "3"), output_helper("What number?", choices[4..7], "7"), output_helper("What number?", choices[4..7], "6"), output_helper("What number?", choices[3..6], "5"), output_helper("What number?", choices[0..3], "1"), "What number? \e[32m1\e[0m\n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end it "navigates pages up/down with disabled items" do prompt.on(:keypress) { |e| prompt.trigger(:keyup) if e.value == "k" prompt.trigger(:keydown) if e.value == "j" } choices = [ "1", {name: "2", disabled: "out"}, "3", {name: "4", disabled: "out"}, "5", {name: "6", disabled: "out"}, {name: "7", disabled: "out"}, "8", "9", {name: "10", disabled: "out"} ] prompt.input << "j" << "j" << "j" << "j" << "\r" prompt.input.rewind answer = prompt.select("What number?", choices, per_page: 4) expect(answer).to eq("9") expected_output = [ output_helper("What number?", choices[0..3], "1", init: true, hint: "Press #{up_down}/#{left_right} arrow to move and Enter to select"), output_helper("What number?", choices[0..3], "3"), output_helper("What number?", choices[2..5], "5"), output_helper("What number?", choices[5..8], "8"), output_helper("What number?", choices[6..9], "9"), "What number? \e[32m9\e[0m\n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end it "navigates pages left/right with disabled items" do prompt.on(:keypress) { |e| prompt.trigger(:keyright) if e.value == "l" prompt.trigger(:keyleft) if e.value == "h" } choices = [ {name: "1", disabled: "out"}, "2", {name: "3", disabled: "out"}, "4", "5", {name: "6", disabled: "out"}, "7", "8", "9", {name: "10", disabled: "out"} ] prompt.input << "l" << "l" << "l" << "h" << "h" << "h" << "\r" prompt.input.rewind answer = prompt.select("What number?", choices, per_page: 4) expect(answer).to eq("2") expected_output = [ output_helper("What number?", choices[0..3], "2", init: true, hint: "Press #{up_down}/#{left_right} arrow to move and Enter to select"), output_helper("What number?", choices[4..7], "7"), output_helper("What number?", choices[8..9], "9"), output_helper("What number?", choices[8..9], "9"), output_helper("What number?", choices[4..7], "5"), output_helper("What number?", choices[0..3], "2"), output_helper("What number?", choices[0..3], "2"), "What number? \e[32m2\e[0m\n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end end context "with :cycle option" do it "doesn't cycle by default" do choices = %w[A B C] prompt.on(:keypress) { |e| prompt.trigger(:keydown) if e.value == "j" } prompt.input << "j" << "j" << "j" << "\r" prompt.input.rewind value = prompt.select("What letter?", choices) expect(value).to eq("C") expected_output = [ output_helper("What letter?", choices, "A", init: true, hint: "Press #{up_down} arrow to move and Enter to select"), output_helper("What letter?", choices, "B"), output_helper("What letter?", choices, "C"), output_helper("What letter?", choices, "C"), "What letter? \e[32mC\e[0m\n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end it "cycles around when configured to do so" do choices = %w[A B C] prompt.on(:keypress) { |e| prompt.trigger(:keydown) if e.value == "j" } prompt.input << "j" << "j" << "j" << "\r" prompt.input.rewind answer = prompt.select("What letter?", choices, cycle: true) expect(answer).to eq("A") expected_output = [ output_helper("What letter?", choices, "A", init: true, hint: "Press #{up_down} arrow to move and Enter to select"), output_helper("What letter?", choices, "B"), output_helper("What letter?", choices, "C"), output_helper("What letter?", choices, "A"), "What letter? \e[32mA\e[0m\n\e[?25h" ].join expect(prompt.output.string).to eq(expected_output) end it "cycles around disabled items" do choices = [ {name: "A", disabled: "(out)"}, {name: "B"}, {name: "C", disabled: "(out)"}, {name: "D"}, {name: "E", disabled: "(out)"} ] prompt.on(:keypress) { |e| prompt.trigger(:keydown) if e.value == "j" } prompt.input << "j" << "j" << "j" << "\r" prompt.input.rewind value = prompt.select("What letter?", choices, cycle: true, default: 2) expect(value).to eq("D") expected_output = output_helper("What letter?", choices, "B", init: true, hint: "Press #{up_down} arrow to move and Enter to select") + output_helper("What letter?", choices, "D") + output_helper("What letter?", choices, "B") + output_helper("What letter?", choices, "D") + "What letter? \e[32mD\e[0m\n\e[?25h" expect(prompt.output.string).to eq(expected_output) end it "cycles choices using left/right arrows" do choices = ("1".."10").to_a prompt.on(:keypress) { |e| prompt.trigger(:keyright) if e.value == "l" prompt.trigger(:keyleft) if e.value == "h" } prompt.input << "l" << "l" << "l" << "h" << "\r" prompt.input.rewind answer = prompt.select("What number?", choices, default: 2, per_page: 4, cycle: true) expect(answer).to eq("10") expected_output = [ output_helper("What number?", choices[0..3], "2", init: true, hint: "Press #{up_down}/#{left_right} arrow to move and Enter to select"), output_helper("What number?", choices[4..7], "6"), output_helper("What number?", choices[8..9], "10"), output_helper("What number?", choices[0..3], "2"), output_helper("What number?", choices[8..9], "10"), exit_message("What number?", "10") ].join expect(prompt.output.string).to eq(expected_output) end it "cycles pages left/right with disabled items" do prompt.on(:keypress) { |e| prompt.trigger(:keyright) if e.value == "l" prompt.trigger(:keyleft) if e.value == "h" } choices = [ {name: "1", disabled: "out"}, "2", {name: "3", disabled: "out"}, "4", "5", {name: "6", disabled: "out"}, "7", "8", "9", {name: "10", disabled: "out"} ] prompt.input << "l" << "l" << "l" << "h" << "h" << "h" << "\r" prompt.input.rewind answer = prompt.select("What number?", choices, per_page: 4, cycle: true) expect(answer).to eq("2") expected_output = [ output_helper("What number?", choices[0..3], "2", init: true, hint: "Press #{up_down}/#{left_right} arrow to move and Enter to select"), output_helper("What number?", choices[4..7], "7"), output_helper("What number?", choices[8..9], "9"), output_helper("What number?", choices[0..3], "2"), output_helper("What number?", choices[8..9], "9"), output_helper("What number?", choices[4..7], "5"), output_helper("What number?", choices[0..3], "2"), exit_message("What number?", "2") ].join expect(prompt.output.string).to eq(expected_output) end it "cycles filtered choices left and right" do numbers = ("1".."10").to_a choices = numbers.map { |n| "a#{n}" } + numbers.map { |n| "b#{n}" } prompt.input << "b" << right_key << right_key << right_key prompt.input << left_key << left_key << left_key << "\r" prompt.input.rewind answer = prompt.select("What room?", choices, default: 2, per_page: 4, filter: true, cycle: true) expect(answer).to eq("b2") expected_output = output_helper("What room?", choices[0..3], "a2", init: true, hint: "Press #{up_down}/#{left_right} arrow to move, Enter to select and letters to filter") + output_helper("What room?", choices[10..13], "b1", hint: "Filter: \"b\"") + output_helper("What room?", choices[14..17], "b5", hint: "Filter: \"b\"") + output_helper("What room?", choices[18..20], "b9", hint: "Filter: \"b\"") + output_helper("What room?", choices[10..13], "b1", hint: "Filter: \"b\"") + output_helper("What room?", choices[18..20], "b10", hint: "Filter: \"b\"") + output_helper("What room?", choices[14..17], "b6", hint: "Filter: \"b\"") + output_helper("What room?", choices[10..13], "b2", hint: "Filter: \"b\"") + exit_message("What room?", "b2") expect(prompt.output.string).to eq(expected_output) end end it "selects default choice by name" do choices = %w[Large Medium Small] prompt.input << "\r" prompt.input.rewind answer = prompt.select("What size?", choices, default: "Small") expect(answer).to eq("Small") end it "raises when default choice doesn't match any choices" do choices = %w[Large Medium Small] expect { prompt.select("What size?", choices) do |menu| menu.default "Unknown" end }.to raise_error(TTY::Prompt::ConfigurationError, "no choice found for the default name: \"Unknown\"") end it "raises when default choice matches disabled choice" do choices = [{name: "Large", disabled: true}, "Medium", "Small"] expect { prompt.select("What size?", choices) do |menu| menu.default "Large" end }.to raise_error(TTY::Prompt::ConfigurationError, "default name \"Large\" matches disabled choice") end it "verifies default index format" do choices = %w[Large Medium Small] prompt.input << "\r" prompt.input.rewind expect { prompt.select("What size?", choices, default: "") }.to raise_error(TTY::Prompt::ConfigurationError, /in range \(1 - 3\)/) end it "doesn't paginate short selections" do choices = %w[A B C D] prompt.input << "\r" prompt.input.rewind value = prompt.select("What letter?", choices, per_page: 4, default: 1) expect(value).to eq("A") expect(prompt.output.string).to eq([ "\e[?25lWhat letter? \e[90m(Press #{up_down} arrow to move and Enter to select)\e[0m\n", "\e[32m#{symbols[:marker]} A\e[0m\n", " B\n", " C\n", " D", "\e[2K\e[1G\e[1A" * 4, "\e[2K\e[1G", "What letter? \e[32mA\e[0m\n\e[?25h" ].join) end it "verifies default index range" do choices = %w[Large Medium Small] prompt.input << "\r" prompt.input.rewind expect { prompt.select("What size?", choices, default: 10) }.to raise_error(TTY::Prompt::ConfigurationError, /`10` out of range \(1 - 3\)/) end context "with filter" do it "doesn't allow mixing enumeration and filter" do expect { prompt.select("What size?", [], enum: ".", filter: true) }.to raise_error(TTY::Prompt::ConfigurationError, "Enumeration can't be used with filter") end it "filters and chooses a uniquely matching entry, ignoring case" do prompt.input << "U" << "g" << "\r" prompt.input.rewind answer = prompt.select("What size?", %w[Small Medium Large Huge], filter: true, show_help: :always) expect(answer).to eql("Huge") actual_prompt_output = prompt.output.string expected_prompt_output = output_helper("What size?", %w[Small Medium Large Huge], "Small", init: true, hint: "Press #{up_down} arrow to move, Enter to select and letters to filter") + output_helper("What size?", %w[Medium Huge], "Medium", hint: "Filter: \"U\"") + output_helper("What size?", %w[Huge], "Huge", hint: "Filter: \"Ug\"") + exit_message("What size?", "Huge") expect(actual_prompt_output).to eql(expected_prompt_output) end it "filters and chooses the first of multiple matching entries" do prompt.input << "g" << "\r" prompt.input.rewind answer = prompt.select("What size?", %i[Small Medium Large Huge], filter: true) expect(answer).to eql(:Large) actual_prompt_output = prompt.output.string expected_prompt_output = output_helper("What size?", %w[Small Medium Large Huge], "Small", init: true, hint: "Press #{up_down} arrow to move, Enter to select and letters to filter") + output_helper("What size?", %w[Large Huge], "Large", hint: "Filter: \"g\"") + exit_message("What size?", "Large") expect(actual_prompt_output).to eql(expected_prompt_output) end it "filters based on alphanumeric and punctuation characters" do prompt.input << "p" << "*" << "2" << "\r" prompt.input.rewind answer = prompt.select("What email?", %w[p*1@mail.com p*2@mail.com p*3@mail.com], filter: true) expect(answer).to eql("p*2@mail.com") actual_prompt_output = prompt.output.string expected_prompt_output = output_helper("What email?", %w[p*1@mail.com p*2@mail.com p*3@mail.com], "p*1@mail.com", init: true, hint: "Press #{up_down} arrow to move, Enter to select and letters to filter") + output_helper("What email?", %w[p*1@mail.com p*2@mail.com p*3@mail.com], "p*1@mail.com", hint: "Filter: \"p\"") + output_helper("What email?", %w[p*1@mail.com p*2@mail.com p*3@mail.com], "p*1@mail.com", hint: "Filter: \"p*\"") + output_helper("What email?", %w[p*2@mail.com], "p*2@mail.com", hint: "Filter: \"p*2\"") + exit_message("What email?", "p*2@mail.com") expect(actual_prompt_output).to eql(expected_prompt_output) end # This test can't be done in an exact way, at least, with the current framework it "doesn't exit when there are no matching entries" do prompt.on(:keypress) { |e| prompt.trigger(:keybackspace) if e.value == "a" } prompt.input << "z" << "\r" # shows no entry, blocking exit prompt.input << "a" << "\r" # triggers Backspace before `a` (see above) prompt.input.rewind answer = prompt.select("What size?", %w[Tiny Medium Large Huge], filter: true) expect(answer).to eql("Large") actual_prompt_output = prompt.output.string expected_prompt_output = output_helper("What size?", %w[Tiny Medium Large Huge], "Tiny", init: true, hint: "Press #{up_down} arrow to move, Enter to select and letters to filter") + output_helper("What size?", %w[], "", hint: "Filter: \"z\"") + output_helper("What size?", %w[], "", hint: "Filter: \"z\"") + output_helper("What size?", %w[Large], "Large", hint: "Filter: \"a\"") + exit_message("What size?", "Large") expect(actual_prompt_output).to eql(expected_prompt_output) end it "cancels a selection" do prompt.on(:keypress) { |e| prompt.trigger(:keydelete) if e.value == "S" } prompt.input << "Hu" prompt.input << "S" # triggers Canc before `S` (see above) prompt.input << "\r" prompt.input.rewind answer = prompt.select("What size?", %w[Small Medium Large Huge], filter: true) expect(answer).to eql("Small") expected_prompt_output = output_helper("What size?", %w[Small Medium Large Huge], "Small", init: true, hint: "Press #{up_down} arrow to move, Enter to select and letters to filter") + output_helper("What size?", %w[Huge], "Huge", hint: "Filter: \"H\"") + output_helper("What size?", %w[Huge], "Huge", hint: "Filter: \"Hu\"") + output_helper("What size?", %w[Small], "Small", hint: "Filter: \"S\"") + exit_message("What size?", "Small") expect(prompt.output.string).to eql(expected_prompt_output) end it "navigates left and right with filtered items" do numbers = ("1".."10").to_a choices = numbers.map { |n| "a#{n}" } + numbers.map { |n| "b#{n}" } prompt.input << "b" << right_key << right_key << right_key prompt.input << left_key << left_key << left_key << "\r" prompt.input.rewind answer = prompt.select("What room?", choices, default: 2, per_page: 4, filter: true) expect(answer).to eq("b1") expected_output = output_helper("What room?", choices[0..3], "a2", init: true, hint: "Press #{up_down}/#{left_right} arrow to move, Enter to select and letters to filter") + output_helper("What room?", choices[10..13], "b1", hint: "Filter: \"b\"") + output_helper("What room?", choices[14..17], "b5", hint: "Filter: \"b\"") + output_helper("What room?", choices[18..20], "b9", hint: "Filter: \"b\"") + output_helper("What room?", choices[18..20], "b9", hint: "Filter: \"b\"") + output_helper("What room?", choices[14..17], "b5", hint: "Filter: \"b\"") + output_helper("What room?", choices[10..13], "b1", hint: "Filter: \"b\"") + output_helper("What room?", choices[10..13], "b1", hint: "Filter: \"b\"") + exit_message("What room?", "b1") expect(prompt.output.string).to eq(expected_output) end end context "with :disabled choice" do it "omits disabled choice when navigating menu" do choices = ["Small", "Medium", {name: "Large", disabled: "(out of stock)"}, "Huge"] prompt.input << "j" << "j" << "\r" prompt.input.rewind prompt.on(:keypress) { |e| prompt.trigger(:keydown) if e.value == "j" } answer = prompt.select("What size?", choices) expect(answer).to eq("Huge") expected_output = output_helper("What size?", choices, "Small", init: true, hint: "Press #{up_down} arrow to move and Enter to select") + output_helper("What size?", choices, "Medium") + output_helper("What size?", choices, "Huge") + "What size? \e[32mHuge\e[0m\n\e[?25h" expect(prompt.output.string).to eq(expected_output) end it "doesn't show disabled choice when filtering choices" do choices = ["A", "B", {name: "C", disabled: "(unavailable)"}, "D"] prompt.on(:keypress) { |e| prompt.trigger(:keybackspace) if e.value == "a" } prompt.input << "c" << "\r" # nothing matches prompt.input << "a" << "\r" # backtracks & chooses default option prompt.input.rewind answer = prompt.select("What letter?", choices, filter: true) expect(answer).to eq("A") expected_output = output_helper("What letter?", choices, "A", init: true, hint: "Press #{up_down} arrow to move, Enter to select and letters to filter") + output_helper("What letter?", [], "", hint: "Filter: \"c\"") + output_helper("What letter?", [], "", hint: "Filter: \"c\"") + output_helper("What letter?", ["A"], "A", hint: "Filter: \"a\"") + exit_message("What letter?", "A") expect(prompt.output.string).to eq(expected_output) end it "omits disabled choice when number key is pressed" do choices = ["Small", {name: "Medium", disabled: "(out of stock)"}, "Large"] prompt.input << "2" << "\r" << "\r" prompt.input.rewind answer = prompt.select("What size?") do |menu| menu.enum ")" menu.choice "Small", 1 menu.choice "Medium", 2, disabled: "(out of stock)" menu.choice "Large", 3 end expect(answer).to eq(1) expected_output = output_helper("What size?", choices, "Small", init: true, enum: ") ", hint: "Press #{up_down} arrow or 1-3 number to move and Enter to select") + output_helper("What size?", choices, "Small", enum: ") ") + "What size? \e[32mSmall\e[0m\n\e[?25h" expect(prompt.output.string).to eq(expected_output) end it "sets active to be first non-disabled choice" do choices = [ {name: "Small", disabled: "(out of stock)"}, "Medium", "Large", "Huge" ] prompt.input << "\r" prompt.input.rewind answer = prompt.select("What size?", choices) expect(answer).to eq("Medium") end it "prevents setting default to disabled choice" do choices = [ {name: "Small", disabled: "(out of stock)"}, "Medium", "Large", "Huge" ] prompt.input << "\r" prompt.input.rewind expect { prompt.select("What size?", choices, default: 1) }.to raise_error(TTY::Prompt::ConfigurationError, "default index `1` matches disabled choice") end end end tty-prompt-0.23.1/spec/unit/selected_choices_spec.rb000066400000000000000000000033671403662044600225070ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::SelectedChoices do it "inserts choices by the index order" do choices = %w[A B C D E F] selected = described_class.new expect(selected.to_a).to eq([]) expect(selected.size).to eq(0) selected.insert(5, "F") selected.insert(1, "B") selected.insert(3, "D") selected.insert(0, "A") selected.insert(4, "E") selected.insert(2, "C") expect(selected.to_a).to eq(choices) expect(selected.size).to eq(6) expect(selected.delete_at(3)).to eq("D") end it "initializes with selected choices" do choices = %w[A B C D E F] selected = described_class.new(choices, (0...choices.size).to_a) expect(selected.to_a).to eq(choices) expect(selected.size).to eq(6) choice = selected.delete_at(3) expect(choice).to eq("D") expect(selected.to_a).to eq(%w[A B C E F]) expect(selected.size).to eq(5) end it "inserts and deletes choices" do selected = described_class.new selected.insert(5, "F") selected.insert(1, "B") selected.insert(3, "D") selected.insert(0, "A") expect(selected.to_a).to eq(%w[A B D F]) expect(selected.size).to eq(4) choice = selected.delete_at(3) expect(choice).to eq("D") expect(selected.to_a).to eq(%w[A B F]) expect(selected.size).to eq(3) selected.insert(4, "E") choice = selected.delete_at(-999) expect(choice).to eq(nil) expect(selected.to_a).to eq(%w[A B E F]) expect(selected.size).to eq(4) end it "clears choices" do selected = described_class.new(%w[B D F]) expect(selected.to_a).to eq(%w[B D F]) expect(selected.size).to eq(3) selected.clear expect(selected.to_a).to eq([]) expect(selected.size).to eq(0) end end tty-prompt-0.23.1/spec/unit/slider_spec.rb000066400000000000000000000312171403662044600204770ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, "#slider" do subject(:prompt) { TTY::Prompt::Test.new } let(:symbols) { TTY::Prompt::Symbols.symbols } let(:left_right) { "#{symbols[:arrow_left]}/#{symbols[:arrow_right]}" } def output_helper(prompt, choices, active, init: false, hint: false) index = choices.index(active) out = [] out << "\e[?25l" if init out << prompt << " " out << symbols[:line] * index out << "\e[32m#{symbols[:bullet]}\e[0m" out << symbols[:line] * (choices.size - index - 1) out << " " << active out << "\n\e[90m(#{hint})\e[0m" if hint out << "\e[2K\e[1G" out << "\e[1A\e[2K\e[1G" if hint out.join end def exit_message(prompt, choice) "#{prompt} \e[32m#{choice}\e[0m\n\e[?25h" end it "specifies ranges & step" do choices = (32..54).step(2).to_a prompt.input << "\r" prompt.input.rewind expect(prompt.slider("What size?", min: 32, max: 54, step: 2)).to eq(44) expect(prompt.output.string).to eq([ output_helper("What size?", choices, 44, init: true, hint: "Use #{left_right} arrow keys, press Enter to select"), exit_message("What size?", 44) ].join) end it "specifies default value" do choices = (32..54).step(2).to_a prompt.input << "\r" prompt.input.rewind expect(prompt.slider("What size?", min: 32, max: 54, step: 2, default: 38)).to eq(38) expect(prompt.output.string).to eq([ output_helper("What size?", choices, 38, init: true, hint: "Use #{left_right} arrow keys, press Enter to select"), exit_message("What size?", 38) ].join) end it "specifies range through DSL" do prompt.input << "\r" prompt.input.rewind value = prompt.slider("What size?") do |range| range.help "(Move with arrows)" range.default 6 range.min 0 range.max 20 range.step 2 range.format "|:slider| %d%%" end expect(value).to eq(6) expect(prompt.output.string).to eq([ "\e[?25lWhat size? ", symbols[:pipe] + symbols[:line] * 3, "\e[32m#{symbols[:bullet]}\e[0m", "#{symbols[:line] * 7 + symbols[:pipe]} 6%", "\n\e[90m(Move with arrows)\e[0m", "\e[2K\e[1G\e[1A\e[2K\e[1G", "What size? \e[32m6\e[0m\n\e[?25h" ].join) end it "formats via proc" do prompt.input << "\r" prompt.input.rewind value = prompt.slider("What size?") do |range| range.default 6 range.max 20 range.step 2 range.format ->(slider, value) { "|#{slider}| %d%%" % value } end expect(value).to eq(6) expect(prompt.output.string).to eq([ "\e[?25lWhat size? ", symbols[:pipe] + symbols[:line] * 3, "\e[32m#{symbols[:bullet]}\e[0m", "#{symbols[:line] * 7 + symbols[:pipe]} 6%", "\n\e[90m(Use #{left_right} arrow keys, press Enter to select)\e[0m", "\e[2K\e[1G\e[1A\e[2K\e[1G", "What size? \e[32m6\e[0m\n\e[?25h" ].join) end it "changes display colors" do prompt.input << "\r" prompt.input.rewind options = {active_color: :red, help_color: :cyan} expect(prompt.slider("What size?", **options)).to eq(5) expect(prompt.output.string).to eq([ "\e[?25lWhat size? ", symbols[:line] * 5, "\e[31m#{symbols[:bullet]}\e[0m", "#{symbols[:line] * 5} 5", "\n\e[36m(Use #{left_right} arrow keys, press Enter to select)\e[0m", "\e[2K\e[1G\e[1A\e[2K\e[1G", "What size? \e[31m5\e[0m\n\e[?25h" ].join) end it "doesn't allow values outside of range" do choices = (0..10).to_a prompt.input << "l\r" prompt.input.rewind prompt.on(:keypress) do |event| if event.value = "l" prompt.trigger(:keyright) end end res = prompt.slider("What size?", min: 0, max: 10, step: 1, default: 10) expect(res).to eq(10) expect(prompt.output.string).to eq([ output_helper("What size?", choices, 10, init: true, hint: "Use #{left_right} arrow keys, press Enter to select"), output_helper("What size?", choices, 10), exit_message("What size?", 10) ].join) end it "changes all display symbols" do prompt = TTY::Prompt::Test.new(symbols: { bullet: "x", line: "_" }) prompt.input << "\r" prompt.input.rewind expect(prompt.slider("What size?", min: 32, max: 54, step: 2)).to eq(44) expect(prompt.output.string).to eq([ "\e[?25lWhat size? ", "_" * 6, "\e[32mx\e[0m", "#{'_' * 5} 44", "\n\e[90m(Use #{left_right} arrow keys, press Enter to select)\e[0m", "\e[2K\e[1G\e[1A\e[2K\e[1G", "What size? \e[32m44\e[0m\n\e[?25h" ].join) end it "changes all display symbols per instance" do prompt.input << "\r" prompt.input.rewind answer = prompt.slider("What size?", min: 32, max: 54, step: 2) do |range| range.symbols bullet: "x", line: "_" end expect(answer).to eq(44) expect(prompt.output.string).to eq([ "\e[?25lWhat size? ", "_" * 6, "\e[32mx\e[0m", "#{'_' * 5} 44", "\n\e[90m(Use #{left_right} arrow keys, press Enter to select)\e[0m", "\e[2K\e[1G\e[1A\e[2K\e[1G", "What size? \e[32m44\e[0m\n\e[?25h" ].join) end it "sets quiet mode" do choices = (32..54).step(2).to_a prompt.input << "\r" prompt.input.rewind expect(prompt.slider("What size?", min: 32, max: 54, step: 2, quiet: true)).to eq(44) expect(prompt.output.string).to eq([ output_helper("What size?", choices, 44, init: true, hint: "Use #{left_right} arrow keys, press Enter to select"), "\e[?25h" ].join) end it "specifies quiet mode through DSL" do prompt.input << "\r" prompt.input.rewind value = prompt.slider("What size?") do |slider| slider.quiet true slider.default 6 slider.min 0 slider.max 20 slider.step 2 slider.format "|:slider| %d%%" end expect(value).to eq(6) expect(prompt.output.string).to eq([ "\e[?25lWhat size? ", symbols[:pipe] + symbols[:line] * 3, "\e[32m#{symbols[:bullet]}\e[0m", "#{symbols[:line] * 7 + symbols[:pipe]} 6%", "\n\e[90m(Use #{left_right} arrow keys, press Enter to select)\e[0m", "\e[2K\e[1G\e[1A\e[2K\e[1G", "\e[?25h" ].join) end it "changes to always show help" do choices = (0..10).to_a prompt.on(:keypress) do |event| prompt.trigger(:keyright) if event.value == "l" end prompt.input << "l" << "l" << "\r" prompt.input.rewind res = prompt.slider("What size?", min: 0, max: 10, step: 1, default: 0, show_help: :always) expect(res).to eq(2) expected_output = [ output_helper("What size?", choices, 0, init: true, hint: "Use #{left_right} arrow keys, press Enter to select"), output_helper("What size?", choices, 1, hint: "Use #{left_right} arrow keys, press Enter to select"), output_helper("What size?", choices, 2, hint: "Use #{left_right} arrow keys, press Enter to select"), exit_message("What size?", 2) ].join expect(prompt.output.string).to eq(expected_output) end it "changes to never show help" do choices = (0..10).to_a prompt.on(:keypress) do |event| prompt.trigger(:keyright) if event.value == "l" end prompt.input << "l" << "l" << "\r" prompt.input.rewind res = prompt.slider("What size?", min: 0, max: 10, step: 1) do |range| range.default 0 range.show_help :never end expect(res).to eq(2) expected_output = [ output_helper("What size?", choices, 0, init: true), output_helper("What size?", choices, 1), output_helper("What size?", choices, 2), exit_message("What size?", 2) ].join expect(prompt.output.string).to eq(expected_output) end it "specifies choices instead of calculated range" do choices = %w[a b c d e f g] prompt.on(:keypress) do |event| prompt.trigger(:keyright) if event.value == "l" end prompt.input << "l" << "l" << "\r" prompt.input.rewind res = prompt.slider("What letter?", choices) do |range| range.default "b" end expect(res).to eq("d") expected_output = [ output_helper("What letter?", choices, "b", init: true, hint: "Use #{left_right} arrow keys, press Enter to select"), output_helper("What letter?", choices, "c"), output_helper("What letter?", choices, "d"), exit_message("What letter?", "d") ].join expect(prompt.output.string).to eq(expected_output) end it "specifies choices through DSL" do choices = %w[a b c d e f g] prompt.on(:keypress) do |event| prompt.trigger(:keyleft) if event.value == "l" end prompt.input << "l" << "l" << "\r" prompt.input.rewind res = prompt.slider("What letter?") do |range| range.default "c" range.choices choices end expect(res).to eq("a") expected_output = [ output_helper("What letter?", choices, "c", init: true, hint: "Use #{left_right} arrow keys, press Enter to select"), output_helper("What letter?", choices, "b"), output_helper("What letter?", choices, "a"), exit_message("What letter?", "a") ].join expect(prompt.output.string).to eq(expected_output) end it "specifies choices through DSL" do choices = %w[a b c d e f g] prompt.on(:keypress) do |event| prompt.trigger(:keyleft) if event.value == "l" end prompt.input << "l" << "l" << "\r" prompt.input.rewind res = prompt.slider("What letter?") do |range| range.default "c" range.choice "a" range.choice "b" range.choice "c" range.choice "d" range.choice "e" range.choice "f" range.choice "g" end expect(res).to eq("a") expected_output = [ output_helper("What letter?", choices, "c", init: true, hint: "Use #{left_right} arrow keys, press Enter to select"), output_helper("What letter?", choices, "b"), output_helper("What letter?", choices, "a"), exit_message("What letter?", "a") ].join expect(prompt.output.string).to eq(expected_output) end it "mixes choices as values and via DSL and keeps ordering" do choices = %w[a b c d e f g] prompt.on(:keypress) do |event| prompt.trigger(:keyleft) if event.value == "l" end prompt.input << "l" << "l" << "\r" prompt.input.rewind res = prompt.slider("What letter?", %w[a b c d]) do |range| range.default "c" range.choice "e" range.choice "f" range.choice "g" end expect(res).to eq("a") expected_output = [ output_helper("What letter?", choices, "c", init: true, hint: "Use #{left_right} arrow keys, press Enter to select"), output_helper("What letter?", choices, "b"), output_helper("What letter?", choices, "a"), exit_message("What letter?", "a") ].join expect(prompt.output.string).to eq(expected_output) end it "sets default choice by name" do prompt.input << "\r" prompt.input.rewind res = prompt.slider("What letter?") do |range| range.default "a" range.choice "a", 1 range.choice "b", 2 range.choice "c", 3 end expect(res).to eq(1) end it "sets default choice by index number" do prompt.input << "\r" prompt.input.rewind res = prompt.slider("What letter?") do |range| range.default 3 range.choice "a", 1 range.choice "b", 2 range.choice "c", 3 end expect(res).to eq(3) end it "sets choice value to proc and executes it" do prompt.input << "\r" prompt.input.rewind res = prompt.slider("What letter?") do |range| range.choice "a", 1 range.choice "b" do "NOT THE BEEEEEEEES!" end range.choice "c", 3 end expect(res).to eq("NOT THE BEEEEEEEES!") end end tty-prompt-0.23.1/spec/unit/statement/000077500000000000000000000000001403662044600176565ustar00rootroot00000000000000tty-prompt-0.23.1/spec/unit/statement/initialize_spec.rb000066400000000000000000000006571403662044600233660ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Statement, ".new" do it "forces newline after the prompt message" do prompt = TTY::Prompt::Test.new statement = described_class.new(prompt) expect(statement.newline).to eq(true) end it "displays prompt message in color" do prompt = TTY::Prompt::Test.new statement = described_class.new(prompt) expect(statement.color).to eq(false) end end tty-prompt-0.23.1/spec/unit/subscribe_spec.rb000066400000000000000000000011331403662044600211700ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, "#subscribe" do it "subscribes to key events only for the current prompt" do prompt = TTY::Prompt::Test.new uuid = "14c3b412-e0c5-4ff5-9cd8-25ec3f18c702" prompt.input << "3\n#{uuid}\n" prompt.input.rewind keys = [] prompt.on(:keypress) do |event| keys << :enter if event.key.name == :enter end letter = prompt.enum_select("Select something", ("A".."Z").to_a) id = prompt.ask("Request ID?") expect(letter).to eq("C") expect(id).to eq(uuid) expect(keys).to eq(%i[enter enter]) end end tty-prompt-0.23.1/spec/unit/suggest_spec.rb000066400000000000000000000016461403662044600207010ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, "#suggest" do let(:possible) { %w[status stage stash commit branch blame] } subject(:prompt) { TTY::Prompt::Test.new } it "suggests few matches" do prompt.suggest("sta", possible) expect(prompt.output.string) .to eql("Did you mean one of these?\n stage\n stash\n") end it "suggests a single match for one character" do prompt.suggest("b", possible) expect(prompt.output.string).to eql("Did you mean this?\n blame\n") end it "suggests a single match for two characters" do prompt.suggest("co", possible) expect(prompt.output.string).to eql("Did you mean this?\n commit\n") end it "suggests with different text and indentation" do prompt.suggest("b", possible, indent: 4, single_text: "Perhaps you meant?") expect(prompt.output.string).to eql("Perhaps you meant?\n blame\n") end end tty-prompt-0.23.1/spec/unit/timer_spec.rb000066400000000000000000000011431403662044600203300ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt::Timer do it "times out loop execution" do timer = TTY::Prompt::Timer.new(0.03, 0.01) yielded = [] timer.while_remaining do |remaining| expect(remaining).to be_within(0.1).of(timer.duration - yielded.size * 0.01) yielded << remaining sleep(0.01) end end it "registers a tick event" do timer = TTY::Prompt::Timer.new(0.03, 0.01) yielded = [] timer.on_tick do |time| yielded << time end timer.while_remaining do # busy work end expect(yielded.size).to be >= 2 end end tty-prompt-0.23.1/spec/unit/utils_spec.rb000066400000000000000000000012371403662044600203540ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Utils do context "#blank?" do { nil => true, "" => true, "\n\t\s" => true, " " => true, "foo" => false, :foo => false }.each do |value, result| it "detects blank of #{value.inspect} as #{result}" do expect(described_class.blank?(value)).to eq(result) end end end context "#deep_copy" do [ "", ["foo", {bar: "baz"}, :fum, 11] ].each do |obj| it "copies #{obj.inspect}" do copy = described_class.deep_copy(obj) expect(obj).to eq(copy) expect(obj).not_to equal(copy) end end end end tty-prompt-0.23.1/spec/unit/warn_spec.rb000066400000000000000000000014761403662044600201700ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, "#warn" do subject(:prompt) { TTY::Prompt::Test.new } it "displays one message" do prompt.warn "Careful young apprentice!" expect(prompt.output.string).to eql "\e[33mCareful young apprentice!\e[0m\n" end it "displays many messages" do prompt.warn "Careful there!", "It's dangerous!" expect(prompt.output.string) .to eq("\e[33mCareful there!\e[0m\n\e[33mIt's dangerous!\e[0m\n") end it "displays message with option" do prompt.warn "Careful young apprentice!", newline: false expect(prompt.output.string).to eql "\e[33mCareful young apprentice!\e[0m" end it "changes default yellow color to cyan" do prompt.warn("All is fine", color: :cyan) expect(prompt.output.string).to eq("\e[36mAll is fine\e[0m\n") end end tty-prompt-0.23.1/spec/unit/yes_no_spec.rb000066400000000000000000000270451403662044600205150ustar00rootroot00000000000000# frozen_string_literal: true RSpec.describe TTY::Prompt, "confirmation" do subject(:prompt) { TTY::Prompt::Test.new } context "#yes?" do it "agrees with question" do prompt.input << "yes" prompt.input.rewind expect(prompt.yes?("Are you a human?")).to eq(true) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(Y/n)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m y", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m ye", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m yes", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mYes\e[0m\n" ].join) end it "disagrees with question" do prompt.input << "no" prompt.input.rewind expect(prompt.yes?("Are you a human?")).to eq(false) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(Y/n)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m n", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m no", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mno\e[0m\n" ].join) end it "warns about invalid entry when using defaults" do prompt.input << "test" prompt.input.rewind prompt.yes?("Are you a human?") expect(prompt.output.string).to eq([ "Are you a human? \e[90m(Y/n)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m t", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m te", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m tes", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m test", "\e[31m>>\e[0m Invalid input.\e[1A", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m ", "\e[2K\e[1G\e[1A\e[2K\e[1G", "Are you a human? \e[32mYes\e[0m\n" ].join) end it "assumes default true" do prompt.input << "\r" prompt.input.rewind expect(prompt.yes?("Are you a human?")).to eq(true) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(Y/n)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m \n", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mYes\e[0m\n" ].join) end it "changes default" do prompt.input << "\n" prompt.input.rewind expect(prompt.yes?("Are you a human?", default: false)).to eq(false) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(y/N)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m \n", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mNo\e[0m\n" ].join) end it "infers default value from a word" do prompt.input << "\n" prompt.input.rewind expect(prompt.yes?("Are you a human?", default: "no")).to eq(false) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(y/N)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m \n", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mNo\e[0m\n" ].join) end it "fails to infer default value from a word" do prompt.input << "\n" prompt.input.rewind expect { prompt.yes?("Are you a human?", default: "unknown") }.to raise_error(TTY::Prompt::InvalidArgument, "default needs to be `true` or `false`") end it "defaults suffix and converter" do prompt.input << "Nope\n" prompt.input.rewind result = prompt.yes?("Are you a human?") do |q| q.positive "Yup" q.negative "nope" end expect(result).to eq(false) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(Yup/nope)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(Yup/nope)\e[0m N", "\e[2K\e[1GAre you a human? \e[90m(Yup/nope)\e[0m No", "\e[2K\e[1GAre you a human? \e[90m(Yup/nope)\e[0m Nop", "\e[2K\e[1GAre you a human? \e[90m(Yup/nope)\e[0m Nope", "\e[2K\e[1GAre you a human? \e[90m(Yup/nope)\e[0m Nope\n", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mnope\e[0m\n" ].join) end it "defaults positive and negative" do prompt.input << "Nope\n" prompt.input.rewind result = prompt.yes?("Are you a human?") do |q| q.suffix "Yup/nope" end expect(result).to eq(false) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(Yup/nope)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(Yup/nope)\e[0m N", "\e[2K\e[1GAre you a human? \e[90m(Yup/nope)\e[0m No", "\e[2K\e[1GAre you a human? \e[90m(Yup/nope)\e[0m Nop", "\e[2K\e[1GAre you a human? \e[90m(Yup/nope)\e[0m Nope", "\e[2K\e[1GAre you a human? \e[90m(Yup/nope)\e[0m Nope\n", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mnope\e[0m\n" ].join) end it "accepts regex conflicting characters as suffix" do prompt.input << "]\n" prompt.input.rewind result = prompt.yes?("Are you a human? [ as yes and ] as no") do |q| q.suffix "[/]" end expect(result).to eq(false) expect(prompt.output.string).to eq([ "Are you a human? [ as yes and ] as no \e[90m([/])\e[0m ", "\e[2K\e[1GAre you a human? [ as yes and ] as no \e[90m([/])\e[0m ]", "\e[2K\e[1GAre you a human? [ as yes and ] as no \e[90m([/])\e[0m ]\n", "\e[1A\e[2K\e[1G", "Are you a human? [ as yes and ] as no \e[32m]\e[0m\n" ].join) end it "customizes question through options" do prompt.input << "\r" prompt.input.rewind result = prompt.yes?("Are you a human?", suffix: "Agree/Disagree", positive: "Agree", negative: "Disagree") expect(result).to eq(true) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(Agree/Disagree)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m \n", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mAgree\e[0m\n" ].join) end it "customizes question through DSL" do prompt.input << "disagree\r" prompt.input.rewind conversion = proc { |input| !input.match(/^agree$/i).nil? } result = prompt.yes?("Are you a human?") do |q| q.suffix "Agree/Disagree" q.positive "Agree" q.negative "Disagree" q.convert conversion end expect(result).to eq(false) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(Agree/Disagree)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m d", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m di", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m dis", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m disa", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m disag", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m disagr", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m disagre", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m disagree", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m disagree\n", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mDisagree\e[0m\n" ].join) end it "obeys quiet mode" do prompt.input << "\r" prompt.input.rewind expect(prompt.yes?("Are you a human?", quiet: true)).to eq(true) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(Y/n)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m \n", "\e[1A\e[2K\e[1G" ].join) end end context "#no?" do it "agrees with question" do prompt.input << "no" prompt.input.rewind expect(prompt.no?("Are you a human?")).to eq(true) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(y/N)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m n", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m no", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mNo\e[0m\n" ].join) end it "disagrees with question" do prompt.input << "yes" prompt.input.rewind expect(prompt.no?("Are you a human?")).to eq(false) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(y/N)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m y", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m ye", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m yes", "\e[1A\e[2K\e[1G", "Are you a human? \e[32myes\e[0m\n" ].join) end it "warns about invalid entry when using defaults" do prompt.input << "test" prompt.input.rewind prompt.no?("Are you a human?") expect(prompt.output.string).to eq([ "Are you a human? \e[90m(y/N)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m t", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m te", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m tes", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m test", "\e[31m>>\e[0m Invalid input.\e[1A", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m ", "\e[2K\e[1G\e[1A\e[2K\e[1G", "Are you a human? \e[32mNo\e[0m\n" ].join) end it "assumes default false" do prompt.input << "\r" prompt.input.rewind expect(prompt.no?("Are you a human?")).to eq(true) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(y/N)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(y/N)\e[0m \n", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mNo\e[0m\n" ].join) end it "changes default" do prompt.input << "\r" prompt.input.rewind expect(prompt.no?("Are you a human?", default: true)).to eq(false) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(Y/n)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(Y/n)\e[0m \n", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mYes\e[0m\n" ].join) end it "defaults suffix and converter" do prompt.input << "Yup\n" prompt.input.rewind result = prompt.no?("Are you a human?") do |q| q.positive "yup" q.negative "Nope" end expect(result).to eq(false) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(yup/Nope)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(yup/Nope)\e[0m Y", "\e[2K\e[1GAre you a human? \e[90m(yup/Nope)\e[0m Yu", "\e[2K\e[1GAre you a human? \e[90m(yup/Nope)\e[0m Yup", "\e[2K\e[1GAre you a human? \e[90m(yup/Nope)\e[0m Yup\n", "\e[1A\e[2K\e[1G", "Are you a human? \e[32myup\e[0m\n" ].join) end it "customizes question through DSL" do prompt.input << "agree\r" prompt.input.rewind conversion = proc { |input| !input.match(/^agree$/i).nil? } result = prompt.no?("Are you a human?") do |q| q.suffix "Agree/Disagree" q.positive "Agree" q.negative "Disagree" q.convert conversion end expect(result).to eq(false) expect(prompt.output.string).to eq([ "Are you a human? \e[90m(Agree/Disagree)\e[0m ", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m a", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m ag", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m agr", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m agre", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m agree", "\e[2K\e[1GAre you a human? \e[90m(Agree/Disagree)\e[0m agree\n", "\e[1A\e[2K\e[1G", "Are you a human? \e[32mAgree\e[0m\n" ].join) end end end tty-prompt-0.23.1/tasks/000077500000000000000000000000001403662044600150665ustar00rootroot00000000000000tty-prompt-0.23.1/tasks/console.rake000066400000000000000000000003331403662044600173730ustar00rootroot00000000000000# encoding: utf-8 desc 'Load gem inside irb console' task :console do require 'irb' require 'irb/completion' require File.join(__FILE__, '../../lib/tty-prompt') ARGV.clear IRB.start end task c: %w[ console ] tty-prompt-0.23.1/tasks/coverage.rake000066400000000000000000000003221403662044600175220ustar00rootroot00000000000000# encoding: utf-8 desc 'Measure code coverage' task :coverage do begin original, ENV['COVERAGE'] = ENV['COVERAGE'], 'true' Rake::Task['spec'].invoke ensure ENV['COVERAGE'] = original end end tty-prompt-0.23.1/tasks/spec.rake000066400000000000000000000012551403662044600166670ustar00rootroot00000000000000# encoding: utf-8 begin require 'rspec/core/rake_task' desc 'Run all specs' RSpec::Core::RakeTask.new(:spec) do |task| task.pattern = 'spec/{unit,integration}{,/*/**}/*_spec.rb' end namespace :spec do desc 'Run unit specs' RSpec::Core::RakeTask.new(:unit) do |task| task.pattern = 'spec/unit{,/*/**}/*_spec.rb' end desc 'Run integration specs' RSpec::Core::RakeTask.new(:integration) do |task| task.pattern = 'spec/integration{,/*/**}/*_spec.rb' end end rescue LoadError %w[spec spec:unit spec:integration].each do |name| task name do $stderr.puts "In order to run #{name}, do `gem install rspec`" end end end tty-prompt-0.23.1/tty-prompt.gemspec000066400000000000000000000026121403662044600174460ustar00rootroot00000000000000require_relative "lib/tty/prompt/version" Gem::Specification.new do |spec| spec.name = "tty-prompt" spec.version = TTY::Prompt::VERSION spec.authors = ["Piotr Murach"] spec.email = ["piotr@piotrmurach.com"] spec.summary = %q{A beautiful and powerful interactive command line prompt.} spec.description = %q{A beautiful and powerful interactive command line prompt with a robust API for getting and validating complex inputs.} spec.homepage = "https://ttytoolkit.org" spec.license = "MIT" if spec.respond_to?(:metadata=) spec.metadata = { "allowed_push_host" => "https://rubygems.org", "bug_tracker_uri" => "https://github.com/piotrmurach/tty-prompt/issues", "changelog_uri" => "https://github.com/piotrmurach/tty-prompt/blob/master/CHANGELOG.md", "documentation_uri" => "https://www.rubydoc.info/gems/tty-prompt", "homepage_uri" => spec.homepage, "source_code_uri" => "https://github.com/piotrmurach/tty-prompt" } end spec.files = Dir["lib/**/*"] spec.extra_rdoc_files = Dir["README.md", "CHANGELOG.md", "LICENSE.txt"] spec.require_paths = ["lib"] spec.required_ruby_version = ">= 2.0.0" spec.add_dependency "pastel", "~> 0.8" spec.add_dependency "tty-reader", "~> 0.8" spec.add_development_dependency "rake" spec.add_development_dependency "rspec", ">= 3.0" end