././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1704229718.3424404 toot-0.41.1/0000755000175000017500000000000014545075526012775 5ustar00ihabunekihabunek././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1670174215.0 toot-0.41.1/.coveragerc0000644000175000017500000000005314343153007015077 0ustar00ihabunekihabunek[run] source=./toot command_line=-m pytest ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700988522.0 toot-0.41.1/.flake80000644000175000017500000000014314530603152014130 0ustar00ihabunekihabunek[flake8] exclude=build,tests,tmp,venv,toot/tui/scroll.py ignore=E128,W503,W504 max-line-length=120 ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1704229718.32944 toot-0.41.1/.github/0000755000175000017500000000000014545075526014335 5ustar00ihabunekihabunek././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1704229718.33144 toot-0.41.1/.github/workflows/0000755000175000017500000000000014545075526016372 5ustar00ihabunekihabunek././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700988522.0 toot-0.41.1/.github/workflows/test.yml0000644000175000017500000000133214530603152020055 0ustar00ihabunekihabunekname: Run tests on: [push, pull_request] jobs: test: runs-on: ubuntu-22.04 strategy: matrix: python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -e ".[test,richtext]" - name: Run tests run: | pytest - name: Validate minimum required version run: | vermin toot - name: Check style run: | flake8 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/.gitignore0000644000175000017500000000027214543235771014764 0ustar00ihabunekihabunek*.egg-info/ *.pyc .pypirc /.cache/ /.coverage /.env /.envrc /.pytest_cache/ /book /build/ /bundle/ /dist/ /htmlcov/ /pyrightconfig.json /tmp/ /toot-*.pyz /toot-*.tar.gz /venv/ debug.log ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704229682.0 toot-0.41.1/CHANGELOG.md0000644000175000017500000003726514545075462014622 0ustar00ihabunekihabunekChangelog --------- **0.41.1 (2024-01-02)** * Fix a crash in settings parsing code **0.41.0 (2024-01-02)** * Honour user's default visibility set in Mastodon preferences instead of always defaulting to public visibility (thanks Lexi Winter) * TUI: Add editing toots (thanks Lexi Winter) * TUI: Fix a bug which made pallette config in settings not work * TUI: Show edit datetime in status detail (thanks Lexi Winter) **0.40.2 (2023-12-28)** * Reinstate `toot post --using` option. * Add shell completion for instances. **0.40.1 (2023-12-28)** * Add `toot --as` option to replace `toot post --using`. This now works for all commands. **0.40.0 (2023-12-27)** This release includes a rather extensive change to use the Click library (https://click.palletsprojects.com/) for creating the command line interface. This allows for some new features like nested commands, setting parameters via environment variables, and shell completion. Backward compatibility should be mostly preserved, except for cases noted below. Please report any issues. * BREAKING: Remove deprecated `--disable-https` option for `login` and `login_cli`, pass the base URL instead * BREAKING: Options `--debug` and `--color` must be specified after `toot` but before the command * BREAKING: Option `--quiet` has been removed. Redirect output instead. * Add passing parameters via environment variables, see: https://toot.bezdomni.net/environment_variables.html * Add shell completion, see: https://toot.bezdomni.net/shell_completion.html * Add `tags info`, `tags featured`, `tags feature`, and `tags unfeature` commands * Add `tags followed`, `tags follow`, and `tags unfollow` sub-commands, deprecate `tags_followed`, `tags_follow`, and `tags tags_unfollow` * Add `lists accounts`, `lists add`, `lists create`, `lists delete`, `lists list`, `lists remove` subcommands, deprecate `lists`, `lists_accounts`, `lists_add`, `lists_create`, `lists_delete`, `lists_remove` commands. * Add `--json` option to tags and lists commands * Add `toot --width` option for setting your prefered terminal width * Add `--media-viewer` and `--colors` options to `toot tui`. These were previously accessible only via settings. * TUI: Fix issue where UI did not render until first input (thanks Urwid devs) **0.39.0 (2023-11-23)** * Add `--json` option to many commands, this makes them print the JSON data returned by the server instead of human-readable data. Useful for scripting. * TUI: Make media viewer configurable in settings, see: https://toot.bezdomni.net/settings.html#tui-view-images * TUI: Add rich text rendering (thanks Dan Schwarz) **0.38.2 (2023-11-16)** * Fix compatibility with Pleroma (#399, thanks Sandra Snan) * Fix language documentation (thanks Sandra Snan) **0.38.1 (2023-07-25)** * Fix relative datetimes option in TUI **0.38.0 (2023-07-25)** * Add `toot muted` and `toot blocked` commands (thanks Florian Obser) * Add settings file, allows setting common options, defining defaults for command arguments, and the TUI palette * TUI: Remap shortcuts so they don't override HJKL used for navigation (thanks Dan Schwarz) **0.37.0 (2023-06-28)** * **BREAKING:** Require Python 3.7+ * Add `timeline --account` option to show the account timeline (thanks Dan Schwarz) * Add `toot status` command to show a single status * TUI: Add personal timeline (thanks Dan Schwarz) * TUI: Highlight followed accounts in status details (thanks Dan Schwarz) * TUI: Restructured goto menu (thanks Dan Schwarz) * TUI: Fix boosting boosted statuses (thanks Dan Schwarz) * TUI: Add support for list timelines (thanks Dan Schwarz) **0.36.0 (2023-03-09)** * Move docs from toot.readthedocs.io to toot.bezdomni.net * Add specifying media thumbnails to `toot post` (#301) * Add creating polls to `toot post` * Handle custom instance domains (e.g. when server is located at `social.vivaldi.net`, but uses the `vivaldi.net` mastodon domain. (#217) * TUI: Inherit post visibility when replying (thanks @rogarb) * TUI: Add conversations timeline (thanks @rogarb) * TUI: Add shortcut to copy toot contents (thanks Dan Schwarz) **0.35.0 (2023-03-01)** * Save toot contents when using --editor so it's recoverable if posting fails (#311) * TUI: Add voting on polls (thanks Dan Schwarz) * TUI: Add following/blocking/muting accounts (thanks Dan Schwarz) * TUI: Add notifications timeline (thanks Dan Schwarz) **0.34.1 (2023-02-20)** * TUI: Fix bug where TUI would break on older Mastodon instances (#309) **0.34.0 (2023-02-03)** * Fix Python version detection which would fail in some cases (thanks K) * Fix toot --help not working (thanks Norman Walsh) * TUI: Add option to save status JSON data from source window (thanks Dan Schwarz) * TUI: Add `--relative-datetimes` option to show relative datetimes (thanks Dan Schwarz) * TUI: Don't focus newly created post (#188, thanks Dan Schwarz) * TUI: Add ability to scroll long status messages (#166, thanks Dan Schwarz) * TUI: Add action to view account details (thanks Dan Schwarz) **0.33.1 (2023-01-03)** * TUI: Fix crash when viewing toot in browser **0.33.0 (2023-01-02)** * Add CONTRIBUTING.md containing a contribution guide * Add `env` command which prints local env to include in issues * Add TOOT_POST_VISIBILITY environment to control default post visibility (thanks Lim Ding Wen) * Add `tags_followed`, `tags_follow`, and `tags_unfollow` commands (thanks Daniel Schwarz) * Add `tags_bookmarks` command (thanks Giuseppe Bilotta) * TUI: Show an error if attemptint to boost a private status (thanks Lim Ding Wen) * TUI: Hide polls, cards and media attachments for sensitive posts (thanks Daniel Schwarz) * TUI: Add bookmarking and bookmark timeline (thanks Daniel Schwarz) * TUI: Show status visiblity (thanks Lim Ding Wen) * TUI: Reply to original account instead of boosting account (thanks Lim Ding Wen) * TUI: Refresh screen after exiting browser, required for text browsers (thanks Daniel Schwarz) * TUI: Highlight followed tags (thanks Daniel Schwarz) **0.32.1 (2022-12-12)** * Fix packaging issue, missing toot.utils module **0.32.0 (2022-12-12)** * TUI: Press N to translate status, if available on your instance (thanks Daniel Schwarz) * Fix: `post --language` option now accepts two-letter country code instead of 3-letter. This was changed by mastodon at some point. * Fix: Failing to find accounts using qualified usernames (#254) **0.31.0 (2022-12-07)** * **BREAKING:** Require Python 3.6+ * Add `post --scheduled-in` option for easier scheduling * Fix posting toots to Pleroma * Improved testing **0.30.1 (2022-11-30)** * Remove usage of depreacted `text_url` status field. Fixes posting media without text. **0.30.0 (2022-11-29)** * Display polls in `timeline` (thanks Daniel Schwarz) * TUI: Add [,] shortcut to reload timeline (thanks Daniel Schwarz) * TUI: Add [Z] shortcut to zoom status - allows scrolling (thanks @PeterFidelman) * Internals: add integration tests against a local mastodon instance **0.29.0 (2022-11-21)** * Add `bookmark` and `unbookmark` commands * Add `following` and `followers` commands (thanks @Oblomov) * TUI: Show media attachments in links list (thanks @PeterFidelman) * Fix tests so that they don't depend on the local timezone **0.28.1 (2022-11-12)** * Fix account search to be case insensitive (thanks @TheJokersThief) * Fix account search to use v2 endpoint, since v1 endpoint was removed on some instances (thanks @kaja47) * Add '.toot' extension to temporary files when composing toot in an editor (thanks @larsks) * Display localized datetimes in timeline (thanks @mmmmmmbeer) * Don't use # for comments when composing toot in an editor, since that made it impossible to post lines starting with #. * TUI: Fix crash when poll does not have an expiry date **0.28.0 (2021-08-28)** * **BREAKING**: Removed `toot curses`, deprecated since 2019-09-03 * Add `--scheduled-at` option to `toot post`, allows scheduling toots * Add `--description` option to `toot post`, for adding descriptions to media attachments (thanks @ansuz) * Add `--mentions` option to `toot notifications` to show only mentions (thanks @alexwennerberg) * Add `--content-type` option to `toot post` to allow specifying mime type, used on Pleroma (thanks Sandra Snan) * Allow post IDs to be strings as used on Pleroma (thanks Sandra Snan) * TUI: Allow posts longer than 500 characters if so configured on the server (thanks Sandra Snan) * Allow piping the password to login_cli for testing purposes (thanks @NinjaTrappeur) * Disable paging timeline when output is piped (thanks @stacyharper) **0.27.0 (2020-06-15)** * TUI: Fix access to public and tag timelines when on private mastodon instances (#168) * Add `--reverse` option to `toot notifications` (#151) * Fix `toot timeline` to respect `--instance` option * TUI: Add opton to pin/save tag timelines (#163, thanks @dlax) * TUI: Fixed crash on empty timeline (#138, thanks ecs) **0.26.0 (2020-04-15)** * Fix datetime parsing on Python 3.5 (#162) * TUI: Display status links and open them (#154, thanks @dlax) * TUI: Fix visibility descriptions (#153, thanks @finnoleary) * **IMPORTANT:** Starting from this release, new releases will not be uploaded to the APT package repository at `bezdomni.net`. Please use the official Debian or Ubuntu repos or choose another [installation option](https://toot.bezdomni.net/installation.html). **0.25.2 (2020-01-23)** * Revert adding changelog and readme to sourceballs (#149) * TUI: Fall back to username when display_name is unset (thanks @dlax) * Note: 0.25.1 was skipped due to error when releasing **0.25.0 (2020-01-21)** * TUI: Show character count when composing (#121) * Include changelog and license in sourceballs (#133) * Fix searching by hashtag which include the '#' (#134) * Upgrade search to v2 (#135) * Fix compatibility with Python < 3.6 (don't use fstrings) **0.24.0 (2019-09-18)** * On Windows store config files under %APPDATA% * CLI: Don't use ANSI colors if not supported by terminal or when not in a tty * TUI: Implement deleting own status messages * TUI: Improve rendering of reblogged statuses (thanks @dlax) * TUI: Set urwid encoding to UTF-8 (thanks @bearzk) **0.23.1 (2019-09-04)** * Fix a date parsing bug in Python versions <3.7 (#114) **0.23.0 (2019-09-03)** * Add `toot tui`, new and improved TUI implemented written with the help of the [urwid](http://urwid.org/) library * Deprecate `toot curses`. It will show a deprecation notice when started. To be removed in a future release * Add `--editor` option to `toot post` to allow composing toots in an editor (#90) * Fix config file permissions, set them to 0600 when creating the initial config file (#109) * Add user agent string to all requests, fixes interaction with instances protected by Cloudflare (#106) **0.22.0 (2019-08-01)** * **BREAKING:** Dropped support for Python 3.3 * Add `toot notifications` to show notifications (thanks @dlax) * Add posting and replying to curses interface (thanks @Skehmatics) * Add `--language` option to `toot post` * Enable attaching upto 4 files via `--media` option on `toot post` **0.21.0 (2019-02-15)** * **BREAKING:** in `toot timeline` short argument for selecting a list is no longer `-i`, this has been changed to select the instance, so that it is the same as on other commands, please use the long form `--list` instead * Add `toot reblogged_by` to show who reblogged a status (#88) * Add `toot thread` to show a status with its replies (#87) * Better handling of wide characters (eastern scripts, emojis) (#84) * Improved `timeline`, nicer visuals, and it will now ask to show next batch of toots, unless given the `--once` option * Add public/local/tag timelines to `timeline` and `curses` * Support for boosting and favouriting in `toot curses`, press `f`/`b` (#88, #93) **0.20.0 (2019-02-01)** * Enable interaction with instances using http instead of https (#56) * Enable proxy usage via environment variables (#47) * Make `toot post` prompt for input if no text is given (#82) * Add post-related commands: `favourite`, `unfavourite`, `reblog`, `unreblog`, `pin` & `unpin` (#75) **0.19.0 (2018-06-27)** * Add support for replying to a toot (#6) * Add `toot delete` command for deleting a toot (#54) * Add global `--quiet` flag to silence output (#46) * Make `toot login` provide browser login, and `toot login_cli` log in via console. This makes it clear what's the preferred option. * Use Idempotency-Key header to prevent multiple toots being posted if request is retried * Fix a bug where all media would be marked as sensitive **0.18.0 (2018-06-12)** * Add support for public, tag and list timelines in `toot timeline` (#52) * Add `--sensitive` and `--spoiler-text` options to `toot post` (#63) * Curses app improvements (respect sensitive content, require keypress to show, add help modal, misc improvements) **0.17.1 (2018-01-15)** * Create config folder if it does not exist (#40) * Fix packaging to include `toot.ui` package (#41) **0.17.0 (2018-01-15)** * Changed configuration file format to allow switching between multiple logged in accounts (#32) * Respect XDG_CONFIG_HOME environment variable to locate config home (#12) * Dynamically calculate left window width, supports narrower windows (#27) * Redraw windows when terminal size changes (#25) * Support scrolling the status list * Fetch next batch of statuses when bottom is reached * Support up/down arrows (#30) * Misc visual improvements **0.16.2 (2018-01-02)** * No changes, pushed to fix a packaging issue **0.16.1 (2017-12-30)** * Fix bug with app registration **0.16.0 (2017-12-30)** * **BREAKING:** Dropped support for Python 2, because it's a pain to support and caused bugs with handling unicode. * Remove hacky `login_2fa` command, use `login_browser` instead * Add `instance` command * Allow `post`ing media without text (#24) **0.15.1 (2017-12-12)** * Fix crash when toot's URL is None (#33), thanks @veer66 **0.15.0 (2017-09-09)** * Fix Windows compatibility (#18) **0.14.0 (2017-09-07)** * Add `--debug` option to enable debug logging instead of using the `TOOT_DEBUG` environment variable. * Fix: don't read requirements.txt from setup.py, this fails when packaging deb and potentially in some other cases (see #18) **0.13.0 (2017-08-26)** * Allow passing `--instance` and `--email` to login command * Add `login_browser` command for proper two factor authentication through the browser (#19, #23) **0.12.0 (2017-05-08)** * Add option to disable ANSI color in output (#15) * Return nonzero error code on error (#14) * Change license to GPLv3 **0.11.0 (2017-05-07)** * Fix error when running toot from crontab (#11) * Minor tweaks **0.10.0 (2017-04-26)** * Add commands: `block`, `unblock`, `mute`, `unmute` * Internal improvements **0.9.1 (2017-04-24)** * Fix conflict with curses package name **0.9.0 (2017-04-21)** * Add `whois` command * Add experimental `curses` app for viewing the timeline **0.8.0 (2017-04-19)** * **BREAKING:** Renamed command `2fa` to `login_2fa` * It is now possible to pipe text into `toot post` **0.7.0 (2017-04-18)** * **WARNING:** Due to changes in configuration format, after upgrading to this version, you will be required to log in to your Mastodon instance again. * Experimental 2FA support (#3) * Do not create a new application for each login **0.6.0 (2017-04-17)** * Add `whoami` command * Migrate from `optparse` to `argparse` **0.5.0 (2017-04-16)** * Add `search`, `follow` and `unfollow` commands * Migrate from `optparse` to `argparse` **0.4.0 (2017-04-15)** * Add `upload` command to post media * Add `--visibility` and `--media` options to `post` command **0.3.0 (2017-04-13)** * Add: view timeline * Require an explicit login **0.2.1 (2017-04-13)** * Fix invalid requirements in setup.py **0.2.0 (2017-04-12)** * Bugfixes **0.1.0 (2017-04-12)** * Initial release ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700988522.0 toot-0.41.1/CONTRIBUTING.md0000644000175000017500000001076214530603152015216 0ustar00ihabunekihabunekToot contribution guide ======================= Firstly, thank you for contributing to toot! Relevant links which will be referenced below: * [toot documentation](https://toot.bezdomni.net/) * [toot-discuss mailing list](https://lists.sr.ht/~ihabunek/toot-discuss) used for discussion as well as accepting patches * [toot project on github](https://github.com/ihabunek/toot) here you can report issues and submit pull requests * #toot IRC channel on [libera.chat](https://libera.chat) ## Code of conduct Please be kind and patient. Toot is governed by one human with a full time job. ## I have a question First, check if your question is addressed in the documentation or the mailing list. If not, feel free to send an email to the mailing list. You may want to subscribe to the mailing list to receive replies. Alternatively, you can ask your question on the IRC channel and ping me (ihabunek). You may have to wait for a response, please be patient. Please don't open Github issues for questions. ## I want to contribute ### Reporting a bug First check you're using the [latest version](https://github.com/ihabunek/toot/releases/) of toot and verify the bug is present in this version. Search Github issues to check the bug hasn't already been reported. To report a bug open an [issue on Github](https://github.com/ihabunek/toot/issues) or send an email to the [mailing list](https://lists.sr.ht/~ihabunek/toot-discuss). * Run `toot env` and include its contents in the bug report. * Explain the behavior you would expect and the actual behavior. * Please provide as much context as possible and describe the reproduction steps that someone else can follow to recreate the issue on their own. ### Suggesting enhancements This includes suggesting new features or changes to existing ones. Search Github issues to check the enhancement has not already been requested. If it hasn't, [open a new issue](https://github.com/ihabunek/toot/issues). Your request will be reviewed to see if it's a good fit for toot. Implementing requested features depends on the available time and energy of the maintainer and other contributors. Be patient. ### Contributing code When contributing to toot, please only submit code that you have authored or code whose license allows it to be included in toot. You agree that the code you submit will be published under the [toot license](LICENSE). #### Setting up a dev environment Check out toot (or a fork) and install it into a virtual environment. ``` git clone git@github.com:ihabunek/toot.git cd toot python3 -m venv _env source _env/bin/activate pip install --editable . pip install -r requirements-dev.txt pip install -r requirements-test.txt ``` While the virtual env is active, you can run `./_env/bin/toot` to execute the one you checked out. This allows you to make changes and test them. #### Crafting good commits Please put some effort into breaking your contribution up into a series of well formed commits. If you're unsure what this means, there is a good guide available at https://cbea.ms/git-commit/. Rules for commits: * each commit should ideally contain only one change * don't bundle multiple unrelated changes into a single commit * write descriptive and well formatted commit messages Rules for commit messages: * separate subject from body with a blank line * limit the subject line to 50 characters * capitalize the subject line * do not end the subject line with a period * use the imperative mood in the subject line * wrap the body at 72 characters * use the body to explain what and why vs. how For a more detailed explanation with examples see the guide at https://cbea.ms/git-commit/ If you use vim to write your commit messages, it will already enforce some of these rules for you. #### Run tests before submitting You can run code and sytle tests by running: ``` make test ``` This runs three tools: * `pytest` runs the test suite * `flake8` checks code formatting * `vermin` checks that minimum python version Please ensure all three commands succeed before submitting your patches. #### Submitting patches To submit your code either open [a pull request](https://github.com/ihabunek/toot/pulls) on Github, or send patch(es) to [the mailing list](https://lists.sr.ht/~ihabunek/toot-discuss). If sending to the mailing list, patches should be sent using `git send-email`. If you're unsure how to do this, there is a good guide at https://git-send-email.io/. --- Parts of this guide were taken from the following sources: * https://contributing.md/ * https://cbea.ms/git-commit/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1657012909.0 toot-0.41.1/LICENSE0000644000175000017500000010451314261001255013765 0ustar00ihabunekihabunek GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700218850.0 toot-0.41.1/MANIFEST.in0000644000175000017500000000003114525643742014524 0ustar00ihabunekihabunekrecursive-include tests *././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/Makefile0000644000175000017500000000177414543235771014444 0ustar00ihabunekihabunek.PHONY: clean publish test docs dist : python setup.py sdist --formats=gztar,zip python setup.py bdist_wheel --python-tag=py3 publish : twine upload dist/*.tar.gz dist/*.whl test: pytest -v flake8 vermin toot coverage: coverage erase coverage run coverage html --omit "toot/tui/*" coverage report clean : find . -name "*pyc" | xargs rm -rf $1 rm -rf build dist MANIFEST htmlcov bundle toot*.tar.gz toot*.pyz changelog: ./scripts/generate_changelog > CHANGELOG.md cp CHANGELOG.md docs/changelog.md docs: changelog mdbook build docs-serve: mdbook serve --port 8000 docs-deploy: docs rsync --archive --compress --delete --stats book/ bezdomni:web/toot .PHONY: bundle bundle: mkdir bundle cp toot/__main__.py bundle pip install . --target=bundle rm -rf bundle/*.dist-info find bundle/ -type d -name "__pycache__" -exec rm -rf {} + python -m zipapp \ --python "/usr/bin/env python3" \ --output toot-`git describe`.pyz bundle \ --compress echo "Bundle created: toot-`git describe`.pyz" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1704229718.3424404 toot-0.41.1/PKG-INFO0000644000175000017500000000161214545075526014072 0ustar00ihabunekihabunekMetadata-Version: 2.1 Name: toot Version: 0.41.1 Summary: Mastodon CLI client Home-page: https://github.com/ihabunek/toot/ Author: Ivan Habunek Author-email: ivan@habunek.com License: GPLv3 Project-URL: Documentation, https://toot.bezdomni.net/ Project-URL: Issue tracker, https://github.com/ihabunek/toot/issues/ Keywords: mastodon toot Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console :: Curses Classifier: Environment :: Console Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3) Classifier: Programming Language :: Python :: 3 Requires-Python: >=3.7 Provides-Extra: richtext Provides-Extra: dev Provides-Extra: test License-File: LICENSE Toot is a CLI and TUI tool for interacting with Mastodon instances from the command line. Allows posting text and media to the timeline, searching, following, muting and blocking accounts and other actions. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1688731561.0 toot-0.41.1/README.rst0000644000175000017500000000333714451777651014476 0ustar00ihabunekihabunek============================ Toot - a Mastodon CLI client ============================ .. image:: https://raw.githubusercontent.com/ihabunek/toot/master/trumpet.png Toot is a CLI and TUI tool for interacting with Mastodon instances from the command line. .. image:: https://img.shields.io/badge/author-%40ihabunek-blue.svg?maxAge=3600&style=flat-square :target: https://mastodon.social/@ihabunek .. image:: https://img.shields.io/github/license/ihabunek/toot.svg?maxAge=3600&style=flat-square :target: https://opensource.org/licenses/GPL-3.0 .. image:: https://img.shields.io/pypi/v/toot.svg?maxAge=3600&style=flat-square :target: https://pypi.python.org/pypi/toot Resources --------- * Homepage: https://github.com/ihabunek/toot * Issues: https://github.com/ihabunek/toot/issues * Documentation: https://toot.bezdomni.net/ * Mailing list for discussion, support and patches: https://lists.sr.ht/~ihabunek/toot-discuss * Informal discussion: #toot IRC channel on `libera.chat `_ Features -------- * Posting, replying, deleting statuses * Support for media uploads, spoiler text, sensitive content * Search by account or hash tag * Following, muting and blocking accounts * Simple switching between authenticated in Mastodon accounts Terminal User Interface ----------------------- toot includes a terminal user interface (TUI). Run it with ``toot tui``. .. image :: https://raw.githubusercontent.com/ihabunek/toot/master/docs/images/tui_list.png .. image :: https://raw.githubusercontent.com/ihabunek/toot/master/docs/images/tui_compose.png License ------- Copyright Ivan Habunek and contributors. Licensed under `GPLv3 `_, see `LICENSE `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1688731561.0 toot-0.41.1/book.css0000644000175000017500000000056514451777651014453 0ustar00ihabunekihabunek/* Overrides for the docs theme */ table { width: 100% } table th { text-align: left } code { white-space: pre } h2, h3 { margin-top: 2.5rem; } h4, h5 { margin-top: 2rem; } td.code { font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace !important; font-size: 0.875em; width: 20%; white-space: nowrap; } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1688731561.0 toot-0.41.1/book.toml0000644000175000017500000000031714451777651014631 0ustar00ihabunekihabunek[book] authors = ["Ivan Habunek"] language = "en" multilingual = false src = "docs" title = "toot" [output.html] additional-css = ["book.css"] [preprocessor.toc] command = "mdbook-toc" renderer = ["html"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704229637.0 toot-0.41.1/changelog.yaml0000644000175000017500000004267214545075405015617 0ustar00ihabunekihabunek0.41.1: date: 2024-01-02 changes: - "Fix a crash in settings parsing code" 0.41.0: date: 2024-01-02 changes: - "Honour user's default visibility set in Mastodon preferences instead of always defaulting to public visibility (thanks Lexi Winter)" - "TUI: Add editing toots (thanks Lexi Winter)" - "TUI: Fix a bug which made pallette config in settings not work" - "TUI: Show edit datetime in status detail (thanks Lexi Winter)" 0.40.2: date: 2023-12-28 changes: - "Reinstate `toot post --using` option." - "Add shell completion for instances." 0.40.1: date: 2023-12-28 changes: - "Add `toot --as` option to replace `toot post --using`. This now works for all commands." 0.40.0: date: 2023-12-27 description: | This release includes a rather extensive change to use the Click library (https://click.palletsprojects.com/) for creating the command line interface. This allows for some new features like nested commands, setting parameters via environment variables, and shell completion. Backward compatibility should be mostly preserved, except for cases noted below. Please report any issues. changes: - "BREAKING: Remove deprecated `--disable-https` option for `login` and `login_cli`, pass the base URL instead" - "BREAKING: Options `--debug` and `--color` must be specified after `toot` but before the command" - "BREAKING: Option `--quiet` has been removed. Redirect output instead." - "Add passing parameters via environment variables, see: https://toot.bezdomni.net/environment_variables.html" - "Add shell completion, see: https://toot.bezdomni.net/shell_completion.html" - "Add `tags info`, `tags featured`, `tags feature`, and `tags unfeature` commands" - "Add `tags followed`, `tags follow`, and `tags unfollow` sub-commands, deprecate `tags_followed`, `tags_follow`, and `tags tags_unfollow`" - "Add `lists accounts`, `lists add`, `lists create`, `lists delete`, `lists list`, `lists remove` subcommands, deprecate `lists`, `lists_accounts`, `lists_add`, `lists_create`, `lists_delete`, `lists_remove` commands." - "Add `--json` option to tags and lists commands" - "Add `toot --width` option for setting your prefered terminal width" - "Add `--media-viewer` and `--colors` options to `toot tui`. These were previously accessible only via settings." - "TUI: Fix issue where UI did not render until first input (thanks Urwid devs)" 0.39.0: date: 2023-11-23 changes: - "Add `--json` option to many commands, this makes them print the JSON data returned by the server instead of human-readable data. Useful for scripting." - "TUI: Make media viewer configurable in settings, see: https://toot.bezdomni.net/settings.html#tui-view-images" - "TUI: Add rich text rendering (thanks Dan Schwarz)" 0.38.2: date: 2023-11-16 changes: - "Fix compatibility with Pleroma (#399, thanks Sandra Snan)" - "Fix language documentation (thanks Sandra Snan)" 0.38.1: date: 2023-07-25 changes: - "Fix relative datetimes option in TUI" 0.38.0: date: 2023-07-25 changes: - "Add `toot muted` and `toot blocked` commands (thanks Florian Obser)" - "Add settings file, allows setting common options, defining defaults for command arguments, and the TUI palette" - "TUI: Remap shortcuts so they don't override HJKL used for navigation (thanks Dan Schwarz)" 0.37.0: date: 2023-06-28 changes: - "**BREAKING:** Require Python 3.7+" - "Add `timeline --account` option to show the account timeline (thanks Dan Schwarz)" - "Add `toot status` command to show a single status" - "TUI: Add personal timeline (thanks Dan Schwarz)" - "TUI: Highlight followed accounts in status details (thanks Dan Schwarz)" - "TUI: Restructured goto menu (thanks Dan Schwarz)" - "TUI: Fix boosting boosted statuses (thanks Dan Schwarz)" - "TUI: Add support for list timelines (thanks Dan Schwarz)" 0.36.0: date: 2023-03-09 changes: - "Move docs from toot.readthedocs.io to toot.bezdomni.net" - "Add specifying media thumbnails to `toot post` (#301)" - "Add creating polls to `toot post`" - "Handle custom instance domains (e.g. when server is located at `social.vivaldi.net`, but uses the `vivaldi.net` mastodon domain. (#217)" - "TUI: Inherit post visibility when replying (thanks @rogarb)" - "TUI: Add conversations timeline (thanks @rogarb)" - "TUI: Add shortcut to copy toot contents (thanks Dan Schwarz)" 0.35.0: date: 2023-03-01 changes: - "Save toot contents when using --editor so it's recoverable if posting fails (#311)" - "TUI: Add voting on polls (thanks Dan Schwarz)" - "TUI: Add following/blocking/muting accounts (thanks Dan Schwarz)" - "TUI: Add notifications timeline (thanks Dan Schwarz)" 0.34.1: date: 2023-02-20 changes: - "TUI: Fix bug where TUI would break on older Mastodon instances (#309)" 0.34.0: date: 2023-02-03 changes: - "Fix Python version detection which would fail in some cases (thanks K)" - "Fix toot --help not working (thanks Norman Walsh)" - "TUI: Add option to save status JSON data from source window (thanks Dan Schwarz)" - "TUI: Add `--relative-datetimes` option to show relative datetimes (thanks Dan Schwarz)" - "TUI: Don't focus newly created post (#188, thanks Dan Schwarz)" - "TUI: Add ability to scroll long status messages (#166, thanks Dan Schwarz)" - "TUI: Add action to view account details (thanks Dan Schwarz)" 0.33.1: date: 2023-01-03 changes: - "TUI: Fix crash when viewing toot in browser" 0.33.0: date: 2023-01-02 changes: - "Add CONTRIBUTING.md containing a contribution guide" - "Add `env` command which prints local env to include in issues" - "Add TOOT_POST_VISIBILITY environment to control default post visibility (thanks Lim Ding Wen)" - "Add `tags_followed`, `tags_follow`, and `tags_unfollow` commands (thanks Daniel Schwarz)" - "Add `tags_bookmarks` command (thanks Giuseppe Bilotta)" - "TUI: Show an error if attemptint to boost a private status (thanks Lim Ding Wen)" - "TUI: Hide polls, cards and media attachments for sensitive posts (thanks Daniel Schwarz)" - "TUI: Add bookmarking and bookmark timeline (thanks Daniel Schwarz)" - "TUI: Show status visiblity (thanks Lim Ding Wen)" - "TUI: Reply to original account instead of boosting account (thanks Lim Ding Wen)" - "TUI: Refresh screen after exiting browser, required for text browsers (thanks Daniel Schwarz)" - "TUI: Highlight followed tags (thanks Daniel Schwarz)" 0.32.1: date: 2022-12-12 changes: - "Fix packaging issue, missing toot.utils module" 0.32.0: date: 2022-12-12 changes: - "TUI: Press N to translate status, if available on your instance (thanks Daniel Schwarz)" - "Fix: `post --language` option now accepts two-letter country code instead of 3-letter. This was changed by mastodon at some point." - "Fix: Failing to find accounts using qualified usernames (#254)" 0.31.0: date: 2022-12-07 changes: - "**BREAKING:** Require Python 3.6+" - "Add `post --scheduled-in` option for easier scheduling" - "Fix posting toots to Pleroma" - "Improved testing" 0.30.1: date: 2022-11-30 changes: - "Remove usage of depreacted `text_url` status field. Fixes posting media without text." 0.30.0: date: 2022-11-29 changes: - "Display polls in `timeline` (thanks Daniel Schwarz)" - "TUI: Add [,] shortcut to reload timeline (thanks Daniel Schwarz)" - "TUI: Add [Z] shortcut to zoom status - allows scrolling (thanks @PeterFidelman)" - "Internals: add integration tests against a local mastodon instance" 0.29.0: date: 2022-11-21 changes: - "Add `bookmark` and `unbookmark` commands" - "Add `following` and `followers` commands (thanks @Oblomov)" - "TUI: Show media attachments in links list (thanks @PeterFidelman)" - "Fix tests so that they don't depend on the local timezone" 0.28.1: date: 2022-11-12 changes: - "Fix account search to be case insensitive (thanks @TheJokersThief)" - "Fix account search to use v2 endpoint, since v1 endpoint was removed on some instances (thanks @kaja47)" - "Add '.toot' extension to temporary files when composing toot in an editor (thanks @larsks)" - "Display localized datetimes in timeline (thanks @mmmmmmbeer)" - "Don't use # for comments when composing toot in an editor, since that made it impossible to post lines starting with #." - "TUI: Fix crash when poll does not have an expiry date" 0.28.0: date: 2021-08-28 changes: - "**BREAKING**: Removed `toot curses`, deprecated since 2019-09-03" - "Add `--scheduled-at` option to `toot post`, allows scheduling toots" - "Add `--description` option to `toot post`, for adding descriptions to media attachments (thanks @ansuz)" - "Add `--mentions` option to `toot notifications` to show only mentions (thanks @alexwennerberg)" - "Add `--content-type` option to `toot post` to allow specifying mime type, used on Pleroma (thanks Sandra Snan)" - "Allow post IDs to be strings as used on Pleroma (thanks Sandra Snan)" - "TUI: Allow posts longer than 500 characters if so configured on the server (thanks Sandra Snan)" - "Allow piping the password to login_cli for testing purposes (thanks @NinjaTrappeur)" - "Disable paging timeline when output is piped (thanks @stacyharper)" 0.27.0: date: 2020-06-15 changes: - "TUI: Fix access to public and tag timelines when on private mastodon instances (#168)" - "Add `--reverse` option to `toot notifications` (#151)" - "Fix `toot timeline` to respect `--instance` option" - "TUI: Add opton to pin/save tag timelines (#163, thanks @dlax)" - "TUI: Fixed crash on empty timeline (#138, thanks ecs)" 0.26.0: date: 2020-04-15 changes: - "Fix datetime parsing on Python 3.5 (#162)" - "TUI: Display status links and open them (#154, thanks @dlax)" - "TUI: Fix visibility descriptions (#153, thanks @finnoleary)" - "**IMPORTANT:** Starting from this release, new releases will not be uploaded to the APT package repository at `bezdomni.net`. Please use the official Debian or Ubuntu repos or choose another [installation option](https://toot.bezdomni.net/installation.html)." 0.25.2: date: 2020-01-23 changes: - "Revert adding changelog and readme to sourceballs (#149)" - "TUI: Fall back to username when display_name is unset (thanks @dlax)" - "Note: 0.25.1 was skipped due to error when releasing" 0.25.0: date: 2020-01-21 changes: - "TUI: Show character count when composing (#121)" - "Include changelog and license in sourceballs (#133)" - "Fix searching by hashtag which include the '#' (#134)" - "Upgrade search to v2 (#135)" - "Fix compatibility with Python < 3.6 (don't use fstrings)" 0.24.0: date: 2019-09-18 changes: - "On Windows store config files under %APPDATA%" - "CLI: Don't use ANSI colors if not supported by terminal or when not in a tty" - "TUI: Implement deleting own status messages" - "TUI: Improve rendering of reblogged statuses (thanks @dlax)" - "TUI: Set urwid encoding to UTF-8 (thanks @bearzk)" 0.23.1: date: 2019-09-04 changes: - "Fix a date parsing bug in Python versions <3.7 (#114)" 0.23.0: date: 2019-09-03 changes: - "Add `toot tui`, new and improved TUI implemented written with the help of the [urwid](http://urwid.org/) library" - "Deprecate `toot curses`. It will show a deprecation notice when started. To be removed in a future release" - "Add `--editor` option to `toot post` to allow composing toots in an editor (#90)" - "Fix config file permissions, set them to 0600 when creating the initial config file (#109)" - "Add user agent string to all requests, fixes interaction with instances protected by Cloudflare (#106)" 0.22.0: date: 2019-08-01 changes: - "**BREAKING:** Dropped support for Python 3.3" - "Add `toot notifications` to show notifications (thanks @dlax)" - "Add posting and replying to curses interface (thanks @Skehmatics)" - "Add `--language` option to `toot post`" - "Enable attaching upto 4 files via `--media` option on `toot post`" 0.21.0: date: 2019-02-15 changes: - "**BREAKING:** in `toot timeline` short argument for selecting a list is no longer `-i`, this has been changed to select the instance, so that it is the same as on other commands, please use the long form `--list` instead" - "Add `toot reblogged_by` to show who reblogged a status (#88)" - "Add `toot thread` to show a status with its replies (#87)" - "Better handling of wide characters (eastern scripts, emojis) (#84)" - "Improved `timeline`, nicer visuals, and it will now ask to show next batch of toots, unless given the `--once` option" - "Add public/local/tag timelines to `timeline` and `curses`" - "Support for boosting and favouriting in `toot curses`, press `f`/`b` (#88, #93)" 0.20.0: date: 2019-02-01 changes: - "Enable interaction with instances using http instead of https (#56)" - "Enable proxy usage via environment variables (#47)" - "Make `toot post` prompt for input if no text is given (#82)" - "Add post-related commands: `favourite`, `unfavourite`, `reblog`, `unreblog`, `pin` & `unpin` (#75)" 0.19.0: date: 2018-06-27 changes: - "Add support for replying to a toot (#6)" - "Add `toot delete` command for deleting a toot (#54)" - "Add global `--quiet` flag to silence output (#46)" - "Make `toot login` provide browser login, and `toot login_cli` log in via console. This makes it clear what's the preferred option." - "Use Idempotency-Key header to prevent multiple toots being posted if request is retried" - "Fix a bug where all media would be marked as sensitive" 0.18.0: date: 2018-06-12 changes: - "Add support for public, tag and list timelines in `toot timeline` (#52)" - "Add `--sensitive` and `--spoiler-text` options to `toot post` (#63)" - "Curses app improvements (respect sensitive content, require keypress to show, add help modal, misc improvements)" 0.17.1: date: 2018-01-15 changes: - "Create config folder if it does not exist (#40)" - "Fix packaging to include `toot.ui` package (#41)" 0.17.0: date: 2018-01-15 changes: - "Changed configuration file format to allow switching between multiple logged in accounts (#32)" - "Respect XDG_CONFIG_HOME environment variable to locate config home (#12)" - "Dynamically calculate left window width, supports narrower windows (#27)" - "Redraw windows when terminal size changes (#25)" - "Support scrolling the status list" - "Fetch next batch of statuses when bottom is reached" - "Support up/down arrows (#30)" - "Misc visual improvements" 0.16.2: date: 2018-01-02 changes: - "No changes, pushed to fix a packaging issue" 0.16.1: date: 2017-12-30 changes: - "Fix bug with app registration" 0.16.0: date: 2017-12-30 changes: - "**BREAKING:** Dropped support for Python 2, because it's a pain to support and caused bugs with handling unicode." - "Remove hacky `login_2fa` command, use `login_browser` instead" - "Add `instance` command" - "Allow `post`ing media without text (#24)" 0.15.1: date: 2017-12-12 changes: - "Fix crash when toot's URL is None (#33), thanks @veer66" 0.15.0: date: 2017-09-09 changes: - "Fix Windows compatibility (#18)" 0.14.0: date: 2017-09-07 changes: - "Add `--debug` option to enable debug logging instead of using the `TOOT_DEBUG` environment variable." - "Fix: don't read requirements.txt from setup.py, this fails when packaging deb and potentially in some other cases (see #18)" 0.13.0: date: 2017-08-26 changes: - "Allow passing `--instance` and `--email` to login command" - "Add `login_browser` command for proper two factor authentication through the browser (#19, #23)" 0.12.0: date: 2017-05-08 changes: - "Add option to disable ANSI color in output (#15)" - "Return nonzero error code on error (#14)" - "Change license to GPLv3" 0.11.0: date: 2017-05-07 changes: - "Fix error when running toot from crontab (#11)" - "Minor tweaks" 0.10.0: date: 2017-04-26 changes: - "Add commands: `block`, `unblock`, `mute`, `unmute`" - "Internal improvements" 0.9.1: date: 2017-04-24 changes: - "Fix conflict with curses package name" 0.9.0: date: 2017-04-21 changes: - "Add `whois` command" - "Add experimental `curses` app for viewing the timeline" 0.8.0: date: 2017-04-19 changes: - "**BREAKING:** Renamed command `2fa` to `login_2fa`" - "It is now possible to pipe text into `toot post`" 0.7.0: date: 2017-04-18 changes: - "**WARNING:** Due to changes in configuration format, after upgrading to this version, you will be required to log in to your Mastodon instance again." - "Experimental 2FA support (#3)" - "Do not create a new application for each login" 0.6.0: date: 2017-04-17 changes: - "Add `whoami` command" - "Migrate from `optparse` to `argparse`" 0.5.0: date: 2017-04-16 changes: - "Add `search`, `follow` and `unfollow` commands" - "Migrate from `optparse` to `argparse`" 0.4.0: date: 2017-04-15 changes: - "Add `upload` command to post media" - "Add `--visibility` and `--media` options to `post` command" 0.3.0: date: 2017-04-13 changes: - "Add: view timeline" - "Require an explicit login" 0.2.1: date: 2017-04-13 changes: - "Fix invalid requirements in setup.py" 0.2.0: date: 2017-04-12 changes: - "Bugfixes" 0.1.0: date: 2017-04-12 changes: - "Initial release" ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1704229718.33344 toot-0.41.1/docs/0000755000175000017500000000000014545075526013725 5ustar00ihabunekihabunek././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704229065.0 toot-0.41.1/docs/SUMMARY.md0000644000175000017500000000067314545074311015401 0ustar00ihabunekihabunek# Summary [Introduction](introduction.md) - [Installation](installation.md) - [Usage](usage.md) - [Advanced](advanced.md) - [Settings](settings.md) - [Shell completion](shell_completion.md) - [Environment variables](environment_variables.md) - [TUI](tui.md) - [Contributing](contributing.md) - [Documentation](documentation.md) - [Release procedure](release.md) - [Changelog](changelog.md) [License](license.md) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700988522.0 toot-0.41.1/docs/advanced.md0000644000175000017500000000200714530603152015775 0ustar00ihabunekihabunekAdvanced usage ============== Disabling HTTPS --------------- You may pass the `--disable-https` flag to use unencrypted HTTP instead of HTTPS for a given instance. This is inherently insecure and should be used only when connecting to local development instances. ```sh toot login --disable-https --instance localhost:8080 ``` Using proxies ------------- You can configure proxies by setting the `HTTPS_PROXY` or `HTTP_PROXY` environment variables. This will cause all http(s) requests to be proxied through the specified server. For example: ```sh export HTTPS_PROXY="http://1.2.3.4:5678" toot login --instance mastodon.social ``` **NB:** This feature is provided by [requests](http://docs.python-requests.org/en/master/user/advanced/#proxies>) and setting the environment variable will affect other programs using this library. This environment can be set for a single call to toot by prefixing the command with the environment variable: ``` HTTPS_PROXY="http://1.2.3.4:5678" toot login --instance mastodon.social ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704229682.0 toot-0.41.1/docs/changelog.md0000644000175000017500000003726514545075462016212 0ustar00ihabunekihabunekChangelog --------- **0.41.1 (2024-01-02)** * Fix a crash in settings parsing code **0.41.0 (2024-01-02)** * Honour user's default visibility set in Mastodon preferences instead of always defaulting to public visibility (thanks Lexi Winter) * TUI: Add editing toots (thanks Lexi Winter) * TUI: Fix a bug which made pallette config in settings not work * TUI: Show edit datetime in status detail (thanks Lexi Winter) **0.40.2 (2023-12-28)** * Reinstate `toot post --using` option. * Add shell completion for instances. **0.40.1 (2023-12-28)** * Add `toot --as` option to replace `toot post --using`. This now works for all commands. **0.40.0 (2023-12-27)** This release includes a rather extensive change to use the Click library (https://click.palletsprojects.com/) for creating the command line interface. This allows for some new features like nested commands, setting parameters via environment variables, and shell completion. Backward compatibility should be mostly preserved, except for cases noted below. Please report any issues. * BREAKING: Remove deprecated `--disable-https` option for `login` and `login_cli`, pass the base URL instead * BREAKING: Options `--debug` and `--color` must be specified after `toot` but before the command * BREAKING: Option `--quiet` has been removed. Redirect output instead. * Add passing parameters via environment variables, see: https://toot.bezdomni.net/environment_variables.html * Add shell completion, see: https://toot.bezdomni.net/shell_completion.html * Add `tags info`, `tags featured`, `tags feature`, and `tags unfeature` commands * Add `tags followed`, `tags follow`, and `tags unfollow` sub-commands, deprecate `tags_followed`, `tags_follow`, and `tags tags_unfollow` * Add `lists accounts`, `lists add`, `lists create`, `lists delete`, `lists list`, `lists remove` subcommands, deprecate `lists`, `lists_accounts`, `lists_add`, `lists_create`, `lists_delete`, `lists_remove` commands. * Add `--json` option to tags and lists commands * Add `toot --width` option for setting your prefered terminal width * Add `--media-viewer` and `--colors` options to `toot tui`. These were previously accessible only via settings. * TUI: Fix issue where UI did not render until first input (thanks Urwid devs) **0.39.0 (2023-11-23)** * Add `--json` option to many commands, this makes them print the JSON data returned by the server instead of human-readable data. Useful for scripting. * TUI: Make media viewer configurable in settings, see: https://toot.bezdomni.net/settings.html#tui-view-images * TUI: Add rich text rendering (thanks Dan Schwarz) **0.38.2 (2023-11-16)** * Fix compatibility with Pleroma (#399, thanks Sandra Snan) * Fix language documentation (thanks Sandra Snan) **0.38.1 (2023-07-25)** * Fix relative datetimes option in TUI **0.38.0 (2023-07-25)** * Add `toot muted` and `toot blocked` commands (thanks Florian Obser) * Add settings file, allows setting common options, defining defaults for command arguments, and the TUI palette * TUI: Remap shortcuts so they don't override HJKL used for navigation (thanks Dan Schwarz) **0.37.0 (2023-06-28)** * **BREAKING:** Require Python 3.7+ * Add `timeline --account` option to show the account timeline (thanks Dan Schwarz) * Add `toot status` command to show a single status * TUI: Add personal timeline (thanks Dan Schwarz) * TUI: Highlight followed accounts in status details (thanks Dan Schwarz) * TUI: Restructured goto menu (thanks Dan Schwarz) * TUI: Fix boosting boosted statuses (thanks Dan Schwarz) * TUI: Add support for list timelines (thanks Dan Schwarz) **0.36.0 (2023-03-09)** * Move docs from toot.readthedocs.io to toot.bezdomni.net * Add specifying media thumbnails to `toot post` (#301) * Add creating polls to `toot post` * Handle custom instance domains (e.g. when server is located at `social.vivaldi.net`, but uses the `vivaldi.net` mastodon domain. (#217) * TUI: Inherit post visibility when replying (thanks @rogarb) * TUI: Add conversations timeline (thanks @rogarb) * TUI: Add shortcut to copy toot contents (thanks Dan Schwarz) **0.35.0 (2023-03-01)** * Save toot contents when using --editor so it's recoverable if posting fails (#311) * TUI: Add voting on polls (thanks Dan Schwarz) * TUI: Add following/blocking/muting accounts (thanks Dan Schwarz) * TUI: Add notifications timeline (thanks Dan Schwarz) **0.34.1 (2023-02-20)** * TUI: Fix bug where TUI would break on older Mastodon instances (#309) **0.34.0 (2023-02-03)** * Fix Python version detection which would fail in some cases (thanks K) * Fix toot --help not working (thanks Norman Walsh) * TUI: Add option to save status JSON data from source window (thanks Dan Schwarz) * TUI: Add `--relative-datetimes` option to show relative datetimes (thanks Dan Schwarz) * TUI: Don't focus newly created post (#188, thanks Dan Schwarz) * TUI: Add ability to scroll long status messages (#166, thanks Dan Schwarz) * TUI: Add action to view account details (thanks Dan Schwarz) **0.33.1 (2023-01-03)** * TUI: Fix crash when viewing toot in browser **0.33.0 (2023-01-02)** * Add CONTRIBUTING.md containing a contribution guide * Add `env` command which prints local env to include in issues * Add TOOT_POST_VISIBILITY environment to control default post visibility (thanks Lim Ding Wen) * Add `tags_followed`, `tags_follow`, and `tags_unfollow` commands (thanks Daniel Schwarz) * Add `tags_bookmarks` command (thanks Giuseppe Bilotta) * TUI: Show an error if attemptint to boost a private status (thanks Lim Ding Wen) * TUI: Hide polls, cards and media attachments for sensitive posts (thanks Daniel Schwarz) * TUI: Add bookmarking and bookmark timeline (thanks Daniel Schwarz) * TUI: Show status visiblity (thanks Lim Ding Wen) * TUI: Reply to original account instead of boosting account (thanks Lim Ding Wen) * TUI: Refresh screen after exiting browser, required for text browsers (thanks Daniel Schwarz) * TUI: Highlight followed tags (thanks Daniel Schwarz) **0.32.1 (2022-12-12)** * Fix packaging issue, missing toot.utils module **0.32.0 (2022-12-12)** * TUI: Press N to translate status, if available on your instance (thanks Daniel Schwarz) * Fix: `post --language` option now accepts two-letter country code instead of 3-letter. This was changed by mastodon at some point. * Fix: Failing to find accounts using qualified usernames (#254) **0.31.0 (2022-12-07)** * **BREAKING:** Require Python 3.6+ * Add `post --scheduled-in` option for easier scheduling * Fix posting toots to Pleroma * Improved testing **0.30.1 (2022-11-30)** * Remove usage of depreacted `text_url` status field. Fixes posting media without text. **0.30.0 (2022-11-29)** * Display polls in `timeline` (thanks Daniel Schwarz) * TUI: Add [,] shortcut to reload timeline (thanks Daniel Schwarz) * TUI: Add [Z] shortcut to zoom status - allows scrolling (thanks @PeterFidelman) * Internals: add integration tests against a local mastodon instance **0.29.0 (2022-11-21)** * Add `bookmark` and `unbookmark` commands * Add `following` and `followers` commands (thanks @Oblomov) * TUI: Show media attachments in links list (thanks @PeterFidelman) * Fix tests so that they don't depend on the local timezone **0.28.1 (2022-11-12)** * Fix account search to be case insensitive (thanks @TheJokersThief) * Fix account search to use v2 endpoint, since v1 endpoint was removed on some instances (thanks @kaja47) * Add '.toot' extension to temporary files when composing toot in an editor (thanks @larsks) * Display localized datetimes in timeline (thanks @mmmmmmbeer) * Don't use # for comments when composing toot in an editor, since that made it impossible to post lines starting with #. * TUI: Fix crash when poll does not have an expiry date **0.28.0 (2021-08-28)** * **BREAKING**: Removed `toot curses`, deprecated since 2019-09-03 * Add `--scheduled-at` option to `toot post`, allows scheduling toots * Add `--description` option to `toot post`, for adding descriptions to media attachments (thanks @ansuz) * Add `--mentions` option to `toot notifications` to show only mentions (thanks @alexwennerberg) * Add `--content-type` option to `toot post` to allow specifying mime type, used on Pleroma (thanks Sandra Snan) * Allow post IDs to be strings as used on Pleroma (thanks Sandra Snan) * TUI: Allow posts longer than 500 characters if so configured on the server (thanks Sandra Snan) * Allow piping the password to login_cli for testing purposes (thanks @NinjaTrappeur) * Disable paging timeline when output is piped (thanks @stacyharper) **0.27.0 (2020-06-15)** * TUI: Fix access to public and tag timelines when on private mastodon instances (#168) * Add `--reverse` option to `toot notifications` (#151) * Fix `toot timeline` to respect `--instance` option * TUI: Add opton to pin/save tag timelines (#163, thanks @dlax) * TUI: Fixed crash on empty timeline (#138, thanks ecs) **0.26.0 (2020-04-15)** * Fix datetime parsing on Python 3.5 (#162) * TUI: Display status links and open them (#154, thanks @dlax) * TUI: Fix visibility descriptions (#153, thanks @finnoleary) * **IMPORTANT:** Starting from this release, new releases will not be uploaded to the APT package repository at `bezdomni.net`. Please use the official Debian or Ubuntu repos or choose another [installation option](https://toot.bezdomni.net/installation.html). **0.25.2 (2020-01-23)** * Revert adding changelog and readme to sourceballs (#149) * TUI: Fall back to username when display_name is unset (thanks @dlax) * Note: 0.25.1 was skipped due to error when releasing **0.25.0 (2020-01-21)** * TUI: Show character count when composing (#121) * Include changelog and license in sourceballs (#133) * Fix searching by hashtag which include the '#' (#134) * Upgrade search to v2 (#135) * Fix compatibility with Python < 3.6 (don't use fstrings) **0.24.0 (2019-09-18)** * On Windows store config files under %APPDATA% * CLI: Don't use ANSI colors if not supported by terminal or when not in a tty * TUI: Implement deleting own status messages * TUI: Improve rendering of reblogged statuses (thanks @dlax) * TUI: Set urwid encoding to UTF-8 (thanks @bearzk) **0.23.1 (2019-09-04)** * Fix a date parsing bug in Python versions <3.7 (#114) **0.23.0 (2019-09-03)** * Add `toot tui`, new and improved TUI implemented written with the help of the [urwid](http://urwid.org/) library * Deprecate `toot curses`. It will show a deprecation notice when started. To be removed in a future release * Add `--editor` option to `toot post` to allow composing toots in an editor (#90) * Fix config file permissions, set them to 0600 when creating the initial config file (#109) * Add user agent string to all requests, fixes interaction with instances protected by Cloudflare (#106) **0.22.0 (2019-08-01)** * **BREAKING:** Dropped support for Python 3.3 * Add `toot notifications` to show notifications (thanks @dlax) * Add posting and replying to curses interface (thanks @Skehmatics) * Add `--language` option to `toot post` * Enable attaching upto 4 files via `--media` option on `toot post` **0.21.0 (2019-02-15)** * **BREAKING:** in `toot timeline` short argument for selecting a list is no longer `-i`, this has been changed to select the instance, so that it is the same as on other commands, please use the long form `--list` instead * Add `toot reblogged_by` to show who reblogged a status (#88) * Add `toot thread` to show a status with its replies (#87) * Better handling of wide characters (eastern scripts, emojis) (#84) * Improved `timeline`, nicer visuals, and it will now ask to show next batch of toots, unless given the `--once` option * Add public/local/tag timelines to `timeline` and `curses` * Support for boosting and favouriting in `toot curses`, press `f`/`b` (#88, #93) **0.20.0 (2019-02-01)** * Enable interaction with instances using http instead of https (#56) * Enable proxy usage via environment variables (#47) * Make `toot post` prompt for input if no text is given (#82) * Add post-related commands: `favourite`, `unfavourite`, `reblog`, `unreblog`, `pin` & `unpin` (#75) **0.19.0 (2018-06-27)** * Add support for replying to a toot (#6) * Add `toot delete` command for deleting a toot (#54) * Add global `--quiet` flag to silence output (#46) * Make `toot login` provide browser login, and `toot login_cli` log in via console. This makes it clear what's the preferred option. * Use Idempotency-Key header to prevent multiple toots being posted if request is retried * Fix a bug where all media would be marked as sensitive **0.18.0 (2018-06-12)** * Add support for public, tag and list timelines in `toot timeline` (#52) * Add `--sensitive` and `--spoiler-text` options to `toot post` (#63) * Curses app improvements (respect sensitive content, require keypress to show, add help modal, misc improvements) **0.17.1 (2018-01-15)** * Create config folder if it does not exist (#40) * Fix packaging to include `toot.ui` package (#41) **0.17.0 (2018-01-15)** * Changed configuration file format to allow switching between multiple logged in accounts (#32) * Respect XDG_CONFIG_HOME environment variable to locate config home (#12) * Dynamically calculate left window width, supports narrower windows (#27) * Redraw windows when terminal size changes (#25) * Support scrolling the status list * Fetch next batch of statuses when bottom is reached * Support up/down arrows (#30) * Misc visual improvements **0.16.2 (2018-01-02)** * No changes, pushed to fix a packaging issue **0.16.1 (2017-12-30)** * Fix bug with app registration **0.16.0 (2017-12-30)** * **BREAKING:** Dropped support for Python 2, because it's a pain to support and caused bugs with handling unicode. * Remove hacky `login_2fa` command, use `login_browser` instead * Add `instance` command * Allow `post`ing media without text (#24) **0.15.1 (2017-12-12)** * Fix crash when toot's URL is None (#33), thanks @veer66 **0.15.0 (2017-09-09)** * Fix Windows compatibility (#18) **0.14.0 (2017-09-07)** * Add `--debug` option to enable debug logging instead of using the `TOOT_DEBUG` environment variable. * Fix: don't read requirements.txt from setup.py, this fails when packaging deb and potentially in some other cases (see #18) **0.13.0 (2017-08-26)** * Allow passing `--instance` and `--email` to login command * Add `login_browser` command for proper two factor authentication through the browser (#19, #23) **0.12.0 (2017-05-08)** * Add option to disable ANSI color in output (#15) * Return nonzero error code on error (#14) * Change license to GPLv3 **0.11.0 (2017-05-07)** * Fix error when running toot from crontab (#11) * Minor tweaks **0.10.0 (2017-04-26)** * Add commands: `block`, `unblock`, `mute`, `unmute` * Internal improvements **0.9.1 (2017-04-24)** * Fix conflict with curses package name **0.9.0 (2017-04-21)** * Add `whois` command * Add experimental `curses` app for viewing the timeline **0.8.0 (2017-04-19)** * **BREAKING:** Renamed command `2fa` to `login_2fa` * It is now possible to pipe text into `toot post` **0.7.0 (2017-04-18)** * **WARNING:** Due to changes in configuration format, after upgrading to this version, you will be required to log in to your Mastodon instance again. * Experimental 2FA support (#3) * Do not create a new application for each login **0.6.0 (2017-04-17)** * Add `whoami` command * Migrate from `optparse` to `argparse` **0.5.0 (2017-04-16)** * Add `search`, `follow` and `unfollow` commands * Migrate from `optparse` to `argparse` **0.4.0 (2017-04-15)** * Add `upload` command to post media * Add `--visibility` and `--media` options to `post` command **0.3.0 (2017-04-13)** * Add: view timeline * Require an explicit login **0.2.1 (2017-04-13)** * Fix invalid requirements in setup.py **0.2.0 (2017-04-12)** * Bugfixes **0.1.0 (2017-04-12)** * Initial release ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1688731561.0 toot-0.41.1/docs/contributing.md0000644000175000017500000001132614451777651016765 0ustar00ihabunekihabunekToot contribution guide ======================= Firstly, thank you for contributing to toot! Relevant links which will be referenced below: * [toot documentation](https://toot.bezdomni.net/) * [toot-discuss mailing list](https://lists.sr.ht/~ihabunek/toot-discuss) used for discussion as well as accepting patches * [toot project on github](https://github.com/ihabunek/toot) here you can report issues and submit pull requests * #toot IRC channel on [libera.chat](https://libera.chat) ## Code of conduct Please be kind and patient. Toot is maintained by one human with a full time job. ## I have a question First, check if your question is addressed in the documentation or the mailing list. If not, feel free to send an email to the mailing list. You may want to subscribe to the mailing list to receive replies. Alternatively, you can ask your question on the IRC channel and ping me (ihabunek). You may have to wait for a response, please be patient. Please don't open Github issues for questions. ## I want to contribute ### Reporting a bug First check you're using the [latest version](https://github.com/ihabunek/toot/releases/) of toot and verify the bug is present in this version. Search [Github issues](https://github.com/ihabunek/toot/issues) to check the bug hasn't already been reported. To report a bug open an [issue on Github](https://github.com/ihabunek/toot/issues) or send an email to the [mailing list](https://lists.sr.ht/~ihabunek/toot-discuss). * Run `toot env` and include its contents in the bug report. * Explain the behavior you would expect and the actual behavior. * Please provide as much context as possible and describe the reproduction steps that someone else can follow to recreate the issue on their own. ### Suggesting enhancements This includes suggesting new features or changes to existing ones. Search Github issues to check the enhancement has not already been requested. If it hasn't, [open a new issue](https://github.com/ihabunek/toot/issues). Your request will be reviewed to see if it's a good fit for toot. Implementing requested features depends on the available time and energy of the maintainer and other contributors. ### Contributing code When contributing to toot, please only submit code that you have authored or code whose license allows it to be included in toot. You agree that the code you submit will be published under the [toot license](LICENSE). #### Setting up a dev environment Check out toot (or a fork) and install it into a virtual environment. ```bash git clone git@github.com:ihabunek/toot.git cd toot python3 -m venv _env # On Linux/Mac source _env/bin/activate # On Windows _env\bin\activate.bat pip install --editable . pip install -r requirements-dev.txt pip install -r requirements-test.txt ``` While the virtual env is active, running `toot` will execute the one you checked out. This allows you to make changes and test them. #### Crafting good commits Please put some effort into breaking your contribution up into a series of well formed commits. If you're unsure what this means, there is a good guide available at [https://cbea.ms/git-commit/](https://cbea.ms/git-commit/). Rules for commits: * each commit should ideally contain only one change * don't bundle multiple unrelated changes into a single commit * write descriptive and well formatted commit messages Rules for commit messages: * separate subject from body with a blank line * limit the subject line to 50 characters * capitalize the subject line * do not end the subject line with a period * use the imperative mood in the subject line * wrap the body at 72 characters * use the body to explain what and why vs. how For a more detailed explanation with examples see the guide at [https://cbea.ms/git-commit/](https://cbea.ms/git-commit/) If you use vim to write your commit messages, it will already enforce some of these rules for you. #### Run tests before submitting You can run code and sytle tests by running: ``` make test ``` This runs three tools: * `pytest` runs the test suite * `flake8` checks code formatting * `vermin` checks that minimum python version Please ensure all three commands succeed before submitting your patches. #### Submitting patches To submit your code either open [a pull request](https://github.com/ihabunek/toot/pulls) on Github, or send patch(es) to [the mailing list](https://lists.sr.ht/~ihabunek/toot-discuss). If sending to the mailing list, patches should be sent using `git send-email`. If you're unsure how to do this, there is a good guide at [https://git-send-email.io/](https://git-send-email.io/). --- Parts of this guide were taken from the following sources: * [https://contributing.md/](https://contributing.md/) * [https://cbea.ms/git-commit/](https://cbea.ms/git-commit/) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1688731561.0 toot-0.41.1/docs/documentation.md0000644000175000017500000000134714451777651017131 0ustar00ihabunekihabunekDocumentation ============= Documentation is generated using [mdBook](https://rust-lang.github.io/mdBook/). Documentation is written in markdown and located in the `docs` directory. Additional plugins: - [mdbook-toc](https://github.com/badboy/mdbook-toc) Install prerequisites --------------------- You'll need a moderately recent version of Rust (1.60) at the time of writing. Check out [mdbook installation docs](https://rust-lang.github.io/mdBook/guide/installation.html) for details. Install by building from source: ``` cargo install mdbook mdbook-toc ``` Generate -------- HTML documentation is generated from sources by running: ``` mdbook build ``` To run a local server which will rebuild on change: ``` mdbook serve ``` ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1704229718.33344 toot-0.41.1/docs/images/0000755000175000017500000000000014545075526015172 5ustar00ihabunekihabunek././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1688731561.0 toot-0.41.1/docs/images/auth.png0000644000175000017500000006327114451777651016656 0ustar00ihabunekihabunekPNG  IHDRY IDATxw[՝7Uo#MŞ;6&@J:)7n웅!XzBƀx<3^4]ܑƲ)fFc09sEIw(i"~$JAhhP !0 gO.h)iG\x35@?Ij]} *) |Ɍݿ,C=M#"H1tp{|S݌J ДQwT7h<wV,21/ RDH` ΀o[8M%W]ۍ[SݔyhӈFjw!" %[C)83DDDDDDS""""""DQVbJDDDDDDY+ekMe1ϩJݫURva@DDDDDg~ tz(ڵP3M!""""H€7>#OlZė?q}'-{uI+ӥOrMh#eV€d?qWCg.\xbl~ǸeE^W:K;Su]:_z7@5t:ʹov3Ug{hz5ݽN8 VOF ,ZN' ڵtG|x6_F|Ͷx(s A(DKy]֞9r3xR,_R|䚍p}w$~b 6',|kWW;;U·r }j+eFZ:jǚ;?DDDDDDɌRZĺsidAmt{ m}8}{$,+~x&\vvkJ¢y3l ^Htڙܲi#zmhi?RzV/o7@\@Lon}~ot{qh+j*? .7#m%""""֕KœcߞyW|5WBQ!e}vG4W~aԲm\ .BsK7`OY`iK` pm8->wÅeF|K~2ѿ/x-xE(s/yͻ2V"""""h֋/nmjZĂٳ WENhCo\ڌl( IVysQ}ar֖h<h@{g?7wFpdmDET `ρ#9u1{ȦZ.Ax=~UJ(rxD鼾@={%""""tJs@QGkp bl_&SY^(qw B$:s ;i*sfUEcC sӪ=DDDDDD%iq9-CC'1cTک;?yQ Ѩig•·r5A@Wkf,׭^R謖0`5a:Esk7 aמXb֮^}RzH$8*jyM|3+!~' {Y- ;1 Et:p^i*ut JHpiYi>AId[j V> -gonԢ8 rt鳡kg=j\2R.v*u}ύؒڸJF_w2GDDDDD4 U8҅ޘ| ^x}[7]48ƜhwcFnp_͟^#=^_>jGz75A"̮QEsk7~NtżV;pzPV(*0HS~?w$#r3Uowje)Fo= wAMelqb(B*TVm=㫱P v=]xbTWw ]9bӬ$,ZNgB)$Mu32.=4-0X%""""V""""""J X(+1`%"""""ĀV""""""J X(+1`%"""""ĀY 0Mp _.d5ZG@DDDDQ0;v-6?c46LuS&Ew\{,?s.p^Ǹ S"""""JӨmo?SOqE A^krMhTܢON$gYj2IRhOihlBDDDDkԀuy$ f m̅/g~[6]4Lt{~[Ko֥l?,~^h4#'KP7 <ƆXt DQtT yln~YhF̰F2;9ǀfLF#n r}a\v֮='?,֮~ww%湫.]onv;RV[6mĪsV)q8Ԟy3/@C}t: ZzwFAٿsWÆ+Byi>>wF8g64*U_~^}_ݟ_LF=\\p"#,8ҍW/>Lyyi>.ް˗ԣ$f#\n:ؒXfb;`4O} ,D~nNu綍zLRq9x!r A(DKy]֞rFM~ekaBARw/S} Ew}z8]^OAsk'"""4`5tX-cLJG!I֬酌5b{ȁAC}:b|oבcn}~ot{qh+j*? .7e;C8,hS;|ݯ_r$K!8o||[#m,JZ_:Ѳ03JG<yߎB Xص8 zͩy30n5~'bTMoCK[ fV zyt/q4ُnü9p8=عrLyuTKD*j9A;7Ⴕ{_Tj͛~gfUwbFd9&=𳯣}v;taQŒb\5U_~]Iۯ@ń< lڈ  """"IUK@RbţuЉ]-GMe1Z{3Ҁ~;/Z*ac'7#{mX筚7y}Me1WA_߹O8ȳ~y\sٹxg!Xۛȓ[ȓ[ }zør;z_xr.-6 7_2/q1oN5iws4 ]WQT 8ZqEhn7~lv7Wp5}"#%b·.]ʲ!xif`%#bOнDDDD9Iaܿl{y.[<翾")J< @mJį|.&/['ۆ{js36"""" VBUK;> XWdW NO:m  ?/sg$ݝ7_xy TAx=~UJ(4&CC}%rLztXq%> 2Ҿ:+*>_ =s:0涌d ,iK=:+o ~"""". /?9&=ض/fa[v`n5,f#wYb6c˵T*.siY6trΙUk.; 5(*̅Nt>eSҮdR9.C** ] $I0u0&ܝWv Xm[ֈ0`]rxuӅ"v=u6s&f6:;oшt&jXr]y*|+WCtXᾦhz33q0P&C%qH^'g2}E$ah4*uic?ķXb.h9-0|Ob3M/ so~LXp͊y2`]qs[31| +;Zo7x%AO~^z=v>vOFT!j5=S:%qH\$PMv\Zn;:Hm;bێjXhb--}?:DDDD!q7ͨ*AeY.|)?QPG5+ 8QP`ѨLvy/ұXj &Bo0y[s\RTu mSZEevvg\V@dzm\Ee%=&n"Qs1(?gId2;VvhɾܾƸx8ijG8,(gh5T!r=}6w עq̸W- ؕ`tTe|lUouų;۞DDDD4u⮶#[ּ`݈w;sHg٢ǯdnQCq9a4 gj .SSv$_FgV_2cn7>DO 3~Y3˰vL_&TW7nL DDDD4bYL7=}64t'-. i{Nlv=n|TߎƆTgz{ [ُkšmPU;om?ՓivWv)__p) 8PVr}J:ۿʲv%ѿgVSD7_9ݍ__Gpnŧ[/vt̟S ^Ri:مm;b_} ,?Ve>qJ軟W]F8/bZ^ހp'6uex;pե<&I縤2۱|lY1OT7FԎUPhEA-|^%:R|]@KTOus)Lng}9wڠ2q;tu+N& X҆]-p3B `D=h{5NDDDDdeJPhEoO8S (jyA}0vy4-%""""hԀUP`?~SML#a~5 02P[B(X=L_oףM \0x64\z;tMXLA[TݢG|8`QBJ.ug.Ώ/WƱ_Bm ` 3#/B_C t~("a?0WP֊Sr)лp0N*e3aЊ]lG7Ԗ i8l@ΈYIw]yڂ$Q_A7rf6 (%*xZ 1g|tؿ:>~8ohz5`E.63E p(- ď+BʋkpO?u䄜*H4AfaAS#>k焥 V=~\0T{?i!Uۥ&e$ j!J(bDZ+^%ڟ.KӮGثA%A 0jsvADȭF}2?_)J9e gb@OR‡BW@[qe:՛: "Rۡ$ raY@ۣw3Rz{0(lrpc( ]9;˛F~^,rp\kVXwº#W@5]0xa-Zs ]/y@0U}ڢWf>e~.zSt%Q|a?B.ڟ)Sin.vuO~ԺlPR:unk0TiЦɈ-r( 2aPvy4yAFA4Њ_npd{dPOEQN-}RϩiY:7wϖ-N!g+ Z_KuR==ohI%[/-BsBT]qK!! *SpFք%(WPÐBJ/KZ^'WPfJ׊j2&x@PJȩw#TEդoUP4owAY! >7T끡Fv5\MԗN6bTU]QO_vD~n~8t ]վ Yvq( 9Uh~:{)n@9vհ5ǽNic_YN1u,Y.Gi0HN:a ; ^T\Ӎև+ub<ӿ0~DDDD4%WЂ^߅^9ގ^ F-Cvk.zӡPP.TT L!(gzjtPIP|A?3 c3t?""""ޒk9K훿BsK4n"D &ڻTPJ&zh:&PD$h!٦C] oO^^YV.z$:Z3c6;DM2ϛRSM--ZPq<J&E~HƲЁ/b歧yTKa$ENADDDD4-8wۊvvY8>>]/fND=P H:k̅ByVm%&vUBRIrm>QcPa`` WwCewU W9~H718 TA_Cy֘6 * mڏw$޷ .Xi$`q/؇ e@[8QhD_0> % #1Rtl:?M]vԹAbhnpBW"*{ߧ:7jnj`!W4:Qvy/N=V= `BPIp4~ ~ yl[j&/K]|=yKitn-,@T^_! jKbx[ Ih0zP~e7N=Q)$ P!TA[BСRFJB.'Gh:0Aϗ./<_ ] Ma/}Lw^33<0z06x;t>J RRcK-^%TPtP C+Rrg= 5^_)m=| """ɓR/&\V[H :Tb1 V<Caf lσ2Tgj aFȩ so bW2B_iգ*)+ g|Z/AW懶$[ ǡ II1R?.E%z uЗ:aDe]!A{G׭EÕ_nqGC6$ף C]:+Abw%I[, WG*ai"ʮG)? n)@AA}[ 6R_D*y v>_6}B jHa&"""F¢e$z-4 @VX:!(%xZ\(g?*:ZS@;yțt&փ PKp3ok3YDDDDD4fY1%NfN6,шMDDDDDDQVbJDDDDDDY+e%DDDDDDQVʾU,NvOuK4͘JC*/T7 Lu+>{lD>@rx;tI_[ڟ.uRzq8횮Wؐwݯ!x0gC ;Qa`}?w1T\gM1}+M Xŀ"'4TTDfPhE9a@SR'B+3ѿki/EQVFėZ7¢N;(0މ%xmg>\PXE\dyGws]ޅPXcmmx|JQ@5xUk S VO {{_ѝK *dB}un(@LQq];T{ \ zuBCZ~eLunJXe%ú3oǗu/jQj^ʑ! R?j {ͰZ  ZrX9BN>z\PC],pyDfl;vz(D%Еiӣ|(`GLRMgZști57mY[@1-#o=uFt>[B]yڂ$Q_sX$kgAf2Tö{ B 9Kx!iG7Ԗ i8l@N\?L|LEr ᰀsX4˅mxY:cbUh5"gwccf=V x Q65`E.,3E p- ď+BʋkpO+\9 9UX5zh 0r8ӃF^u< K> ܭz07`+*,7J/CثK MyUSIB ЕQrQ;߉µVJ?]0(]W }J/rU9!T\)@`P c%i ^-x;pS?NDsTv I&?%vX9hE=a"T[ЕT;tOvC (iCPJWP^{+~B-.r }F89KE*n*&/[ O } e~tRS&t_&^+bN}Z :U8l9}bKqK (-PdWv!@í4ֺ1o;~WhsA8eGuEf v6WxR+}'ty3ѯ@׋G+ouG.;r,T^CYn8آӦGCzADZ+N=%s ]/y@T}ڢW2 O٥ ]?a4A_؏KgJT¨p0Ӛ<Њ0zPb@gJL ZaҠMWZ GV5>h u(,{JCu_jy ak̇;+e t<[ v‡ )TE?F't}p74Ƒ};?Y!CDէ:I=`]X7tDDD4L.2_~v.<1n"s7\'=nsLjCG2˂yS$}RΦ$XiSB$Xw+X_ւh ș{n ;A!~=hf5wcz>_H@s%WC9TA#g,Y]t5ycRHN{#`UöW^Q4(%SciINg"o з &Tc`L3ArӍ{^/j{r4*qHYnJiл0fW[~S~LӁ7Uc6ȁmsIS($̟ݭBs?Jx֍wwFc ; U~[Ֆ,Ut}2W)ZBJmQOTB IHU$h8]$K?L$QHx\|`}o:5_ A) B"Y CwŶ'R>&I=bn<7F?4-qߣMi)M~bPHXl*vNDЮJe}zlq9Wб6tt LV&Dde^UYS;6#AB5usJ\3',TD)H~Ϟ&?A%iH#_:~RH@e0չaitP퍮բŒ `O#$(CAgqT C:b`riO[t(PK8p ^V1sS'҇rp>4ֹv9[p [}XCҀՠ?3tZ55x;pݷK-鹧F9~(n!Y_mMRPMq"Pr`uJ)RT؁sWz:Ar}UdIJ/cs0uA%9vW?HCU̚0>&L83r73OLb"_2m +kNX[ xUq܇;ȶ5u^tau"Ju$$뉈($WЂ^߅^9ގ^ FrhnmV={T&9,04B.Ā*S(KE`PJ /q'5r{ ߑj$ KU&O p7mF:uP0e懘3js(=j<5U"̂C[EX!EVi@LKQӣc,J9|wkcՠDMgs, ҹ 4&|u_^'ٳxE%SRPJ%g:&PD$;hL*WxZ0ջanp'>3:V6=z7dc.ũ0:Z3c6:،W&/U }/C"c7T!hSCo{콙ƙŬ=ZHM^jKA{)z*˙/S, (\31@+EqUqH<0s/a,2"_dW_ۙ*{Ԋ /xpyxKq^X5H' @cSaQLDDDo衳ۊ6vgZӕb.:eDtDYc.γl{,qd\ 57Z:̖bOl![ cξ?XЈ*p# D-9~H7l}(01B[-?g@~A)1Y(^[&/6@?}ؐ?/yؓ/2AQ|0Ԗ 7T!9 TA_Cy֘Ͼ6}G-죯,f}M< n#ړm̭KWum+nujNvawX*GQˊԹAgnpBW"+*{ߧ:7jnj`!W4:D `BPIp4~ ~ yl[j&/K]|A;yK!ukf.TP[B{n'& XA8D9CSmjsA J]+mTPꇃޛkz+:~J}J[h*2bNÿn̼|]ZJ ?\MFZz^33<0z06x;t>J S[WxrG M~Ͱ,LjMt!Aϗ./<_ ] Ma/ǔK{)/'Ƌ+z=-բ1¨-P/a ^ׇE\xՒ_?U/_݁sX4˅zJXL!,A«秝]Ć9ut_3Io &-}lՁ#pLgk%p.+V^L(m|t-by jn%؞),e~Trϱ<!Sa&rîFe(\k),ӪG %TRV@Fz_fmIaC91/pcVuC't~]ڋK@Սu/!hSuˆ9.<* 2?BneT2~aT2 l7E~y5YRa\'ޏr16\0"Uߧ=T[ |0ϗWÕ$n-?$ j[y8_n IDATo.' XS/]b@-ࠀiCukp%a* hI.ܛ Yks |ȲԲ`1ܐTZQ P2K/V0uDUI6HZ*J(d[V|!zA ?#ANE+djQRPT5ĢDݷ_C\˺/wppPwn\{%l߹;Z`rZ|6t]g׿h +X0 ~B>oR2kYvϭKW]m}%K_)}5_[O븯` _~X+C:u0ncٖu9X.Zq&Z s;B?$ aМhz9^3SoD˛VC-Nt-b[G9s $ImJbQ,Qh"S H6'jBj=!uwu;0 n+!m '$łV\s(QRAP3^;$[jtUR'8rj7OtmCw4#g<AHOBRy#;d#JΟUaHļY|"qN}i]W>gvbxSpr"3,d#.OZL$ՎZaCz6műy +|AkYHW{x}z Y~ u~jDl/ijS\;%Y'_) /k5Ԣ$ւlƔL*gȲ fJB'Nj៉ው;Z hų1|mn;`pUNY`l_XèґΎ׏Y! ĵ/sH߼ɐI`TϤBOx{j #xyOt=skYpp 1T.ǂS(FQjauٳpE9,Z0!+#.'7+]z, =ڭaYu4 Qx;DXTCCT_y M!'+=y+2ry>ٷr^Tq/}?}]b,%s0N[[L[{wL`ue|:[tSUY=w\F"/!%SLw]I[{7즬$o@}/0MmߴnP^ϥ_Džg{"d tBiaA\ne6gsj )v T32?ElڲE)Ϧ7}FC4 L>m6 |fǗϷߺłt$ *M?_43Ӹ隅/|?᠍d{ B,eWωc=%s_{1vM5ۺ IQ2P&\;۱cx5tG3jN%ֱ碎F:V8_^s?Ӱ[c?"#An{tTȩaSgcuݳ~9ejxԜ81(:r Ztx[Q2JQ'dѽe ux._>%^3jn^ +h$!d[I s0ܝZw")bRb\ *M?2֊fiYik\uoa?So@Rx[v i)(]qՎsAgx]~e[rz >Fo~UHҋ:u'}A,E[FRmاcБSFՠΤ{BM"߀uʞO"Fk#m<N-U|戒|xxtdIⱇnfZ%Ξޏ}d49,So8[Z#JFԣr%r}cкVspJpH{m:dvf?\d.ħ붅l\ ~ ~`Sc3$>/Z<\>t?x9>cSYxk~/0:Y--{Tekxg>w2߼2OHvrZa\s?a pE"G:d{kpx?[%6A0}?%R;(9cS>S{qn ^RmV.ARpʑ=QGTA ~/V0kzEkh"g4zPg̜?` g"s6ܸIz'Cɶ_$wʵGge>Y5g_>ٙt`̀94\u,Wϼ6W̑/?h=y𞯢:9ԅ^V OT.~Cv^[:?LTw.ΏGRm(߽<;_FUä́%~!VJzL<3H6m7fHo!%~2`e3'd> XkW47]۬C{V^n&Jq:!>cƫ(MUx/?$Ibnm^?;ޗu{p:LPo[YKk'ZAe_c78].4UAQϢy>;o_OCL}?!,!=h~zl"G}Yf ^_c4IF} K<нaR Z`ts/rd׳M,0}RF{ O% EhyP2JBY%Z" $elzWpmm@ ,|Mg{B_<'ax%H -kmS]͝3^XL$`8؂aHO ӑyY|EWn跽̌2Sp<+.+3E;ogt*VQ3'~ 4 |r_'ǖpɒLEވ,l;?XkρH>៽$RŅi = %j^rJ.?e9lZ̀t_RHZJ'%-I6CIxĊVVt̚^ Obad^~jU6ަa ˩2F+U G!v ٞdIpwRV3W ~9\g~K$zL3xCͩD}ʵ|GmonnYGYEϝܚ `3)NW4ۺaXT6KȀrr3 G .#-USx`3fr{g^m5S)*jsܲ5\xlsH|~F9>4Dg6<+OVc8:gD%XǞ/~>y̹jyCJ;q!fy]SS;B^h\n]=ÆͻH8%6i4nOPNt}?%Z}:b.EIN4`x&n6Iw LI`xApu`Hd%Z2Ƙ.X{W gKꍔx@w4/u"s$#Mv?d5wKJ#[7ܙ=sop?/>AĂ=% )p;32K u#Jl 5rĥ9o0v$[:(л;?WbLl닯̈)Iت.0gv f:;f* $WyL?)ߏ:b(8m< : '[3!DNQEghb^"yN$aՒ%-n_#k> yLf 9eje;.>mPazϖ5c)Gg_4M%%D}t=oq/W~ pS;#]g~6XHLe ʊ`l6L&T I+Vmb#aX&_O;2vzw_Y4*T@>gh]wřN- x~y3Yl}>L895Uٺ}e%yͿ}i.)kT`_i/qmk R27;ޞq JhԜJRjn׾IR`DVp'Ƹw4`r-}d[r9kmI%Z,8w9OFI3tnwfhx#eg+eN<֡LVu /G`ԚvPs˽R:d6RˬloZI|_W2${8 \IDATjN%+^@Cd}ypl3IWc)7 _~$YC(AR,ȩy8P.(Z֝`XF?zh9ٌhj/V]ٌ:u4"I2rzަ-5h"3Z%>[%B+i)(e\:1$K:JyoަmxgN XL@Ca?22/@3w zbNe۸?ijX=;w9g̠f8;odݗ(%H y.Y2ήaԱΦm{X nY)+>و"Evkӧ1cZ:{xf 2~lI@fٽ,8 L>6_knn13f86o#O53#5d&E[t//{SpY1pnzSQ*0 _>@ @ %"@ @ %b@ @ Ix9s&"`@ 3g, W\q-K@ @0H>(V@ @  =+WD$.]ҥK3gX@ @0x^;<=@ @ r$IncXU @ C$IAXŒ`@ @  KD*@ aX@ @0,@ @ %lNr<=;IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1688731561.0 toot-0.41.1/docs/images/trumpet.png0000644000175000017500000004014614451777651017411 0ustar00ihabunekihabunekPNG  IHDRaMsBIT|d IDATx]y\M/=MDQh 043 1#3_b%ƌk'„$l%JiӾ;-}^W<,<] (?-=P`tP ,@ݻ:BFW@w055Eee%ۧѣGcҥ=PF^^Fhm۶EBB.ڶm۴#VYqm|ᇼ}B111y&,X 35˗Ƴ… ׯ1-^˖-t zÑsssoUDJJJ fВܻwM^@UU555ӧ]HJJ7S6:u;w 99={7QSSGBOOfffĉQQQ@UU>KFFFBCC o:ZnІPVV[n֭[VbYmhԩ,(>44?lll0`]l… hР]\\7?yrZdd$Bh1&W0x`ڵkpvvf~߾}ܹ֭*<|EEE3g@ǏO>,G&LCCC| JJJ`T[dt)]ɵsk3ƎK}۷O#Ҍ3裏>۷oӒ%KM6H-XGGGe2z||SXĢnzzۖkGر#Ν;dzrªU`ccUVa믿->7m4ŋGii)b gΜaK!uuu,ZZZ:TVVB$L?/)TTT0m4 G!!!***۷/={{aٸuѹsg =zsxJJJŸ{.z@,Zhiiaʔ)066ƺuPRRUUU )) Æ ޽{yR@CC#TTT@YYRaeeDbBMM ***{Byy9{ܶ&;_qq1ZZZPUUetttPUUTWW׫}6BCC@NByy9ÇEEE0a݋.]`Ĉ OOOȑ#ڵ+V\CWW7BCCiHJJę[UTT ]]: iLHHȑ#̨fBHH"-ݧL"s)qq@l_yyL'ÇSUU۷֯_O)uN@ ,*..PZ~=%''~^ Dz ݼyFɮ۽{w200SNQbb"3O$&&gffF/^?^JHYYYl2rww'GGGK#SSS T***(#sN#hD,f̘Aiiil_rr2 OOOŋtyޱ322H~#}* `c~,GDo>0a]pm0***;w) bbb( f̘AnnnrO>+W4ۛ<$j.L2yCQO8)ڂ j*DDD0 N&}a߾}066f3MII @JJ QYY)3TÇx }}}Cii)z KKK\t :::PUUEDDB!k\vMnK@lVAnn.]B1}t|(**իQ^^K.>۶mCv񰲲ĉh׮zj_~R+Ǭv=-%%:t&SlXCmSҥKŶmpuVl޼MzwyyyPUU쐒Ç#::ׯ={66l}ȑ8y$e˖֭[ؼy3dHN4 %%%R2>L6 SNeAeeef'LII<.cǎz*Zj*,Xk֬iб***BMM jjjC@e=z -- HIIAnn.1j(TVV",, HNNL8JJJ077Gǎo ":u EEE077gG\878y$m{fc_0ftĬYXm0R`nn[ǣGx)zWWW3--- Z/Ʒ~[5)ƢErJaӦMRKvt)Fc555TWWCGGXt'CCC#kkj.k,׏)--sμ^}>|8 T#???255%]]]0`JQ]gAGӦM#???:~8]v ŋM2DtQbڵk+^w&g\II(7op̤VWWg}F={RCJatYKvSSSΦlrvv&^,:yd1??)8(ԩjՊ233uaٲeR!==K2z~~>u֍׷O>lLpF6sذa{Ak׮em ,I& mݺɯ?̾deeEZ۷oi۶m<"''iӦMR|N0zffԒ](Ǐ'4k,ڳgՀYǺuV7oeeeHKyVVƵk׎rssY$Z;88Paa!1ީS'zKwg@hΝez5%JKKNa۶mVN***OdaaAnh"2e %&& 4`6>|et###vԱcet&L|?&"¯JZl|999?t҅ƌCaaa믿/ݻwYN$}ۉZnbd1ɓ~ GMD|/҈}ƌV "OxkPMMMZ&MDt=K$9yݝxsZS ''wiӦѐ!CMQ>}ؘо}H Pxx8mۖ]F#Gd/IKKOw988ЬYk׮2qIE]$I-wED(+kkkޯSZZTk׮[>uג>iҤzctΎ&^ΝYY Dp"ѨQaΜ94f*))!]]]:}4YoNIĤ$Z>MvvvGjj*/ۼ ,%И$^Z_99s&x]y9p0yd/ /eۆ!@Qj*nݺaҥ-9Dp!?~^ahhkkkƍHLLDff&QQQ#;;/UUU.mڴ6mD_}988QYYGo۶mƆllľf̘A2S5VVmڴZEEEvI2s"4rss8>>LLL*7?$z-:'%%msYf+** 9{eL⧛ wf֛pY={of阳s=+QTT˗/`8JSAGG߳ fI022իf|||`hhCCC֙xVZIoQFڪU+<$fffO ???ykQoݺ5.\???̘1700y}򐓓.]666011۹`pvv Ny[YY kՉٳgۛm_t ٳgѡC%BfJԩlmmٶ@ н{w@rKSxƌ@@zzz֩S'8;;3X֭[m۶zb-|rmdd͛7>_d- ---!&&"aaa={6q8cС-uZj&(11455 g9uT3򧧧GgΜr)7ݏ?đC#Gy󉈚,3g8do}}}^ /׮MZ"@1coFNNNJ6664ydfߴiuܙ]Ɔ IMMm+++kXZZj׮듆<Ƥ}u҅rrrܸ54$y>71rhF/--}AֆG'" 5~@P߾}ݻT^^N^^^< gצ =zDAAAAR t1r {dd$>>>D$n۷/;v޽K}͘1RSS]>>Q9bcc[XXoFDD};.M4nݺՠqڪU+X+Wl媉TDG,NH,n̙3Xl6Ad``@EdkkK'NKRJJ ݻw/"Ξ=Kl?}9[pV/D\C\gϞ=˗ŋY8rCH2dLzVVYZZN$ %EFY}z"^]].Z>>>rqqRUU_TRRBĉ2u%%%Z|y 6DDwa(77|}}jjjԷo_9s&͜9O.ttthz/^$@@^^^`tI9ˬPF'2O?DM+Vx-Shܹ|rDT^^N CCC&VRAAD"JOOBϗ{SSS)))m;wWŋLyMf͒Zs3ӧ*8rqqɓ'P D$%Ν;Xؘx SNpţK^YWNC$^ 0A^[Ft6KeJ}Z9ZaǏ>|"())!!!w;wЭ[zգG?~xpUDDDSNEff&BBBwQ8wNSSSXZZwޘ0aB!<΂!bbbн{w8# @"BiiL=z >>^^^HOOÇyv킕ӧO1a>d>+Wʕ+l;88ڵCnn.k_yfhiiaذa ##]GG~)RSS~z\rڵk;_~,vj9*hݺ5-[KKKL4 ĉ'SV> j0TSuC[%v]oQQQhȶSNaɒ% ý{rJ ###|Xp!JJJ .`ܸqETMM ^EE???aHHH -,,vׯ3g=˃3@DXn pիjjj#33QQQPRR·~/q^p=zwLǞI&{u\f?SQQá'OJBUU1bW^طoKٱ7nl\\xyy9kccSWRXt)BCCw'N[i ͺv-碢"rttC6IH5jwel`` -ZNLzz:8p<==ISS%8MtAAٳdճG  =F>CڰaYYYX4hq&W}ժU)ښ}xQ;̙ۤ3ѣ^-xk3:̥obdtWWW^т;2il[__yyy1vtt4D"FD$-(?>yxx<K\ް&bt555h׮UVV6HF ҫW/*..&c͛71$KP(k]hjhypydggܹse?~쌅 ɓ'HHH#Gx}Æ ֭[@/%jՕ-]L=tPiժsssCfyƄ ၴos4TSSVc;v,f͚76p*^vHKK (++%ۛ0aDfWUUΝ;/8zx222h޼yĉ8GrӿoRWWWțО={ɉH(iӆidSN%A{S޽;YG2B/w GRF)/]DDݻw'wwwС9993KݩS'ee]^UUغu++\hqFJ*`?C iVSȖ-[x/ŋM4fڴiWMM mٲΟ?O= ۷/9UEDdffF{%#iLW">x`6ڴqF:CCCz*djjJ:tjj8Kʞ^^^ĄԦd?4&&&qB}rJvν{(""֯_OÆ . ښРA(""-ZӧEDDәSLcϏrrr¢E] >Q^^=xm߾\\\(//55W$vZ*..*HJDtfgպsŗ_~ݻw 'OĈ#󝹺"** gϞ&K9$ nHMMo+2! ###hkk믿fV_xݻw#33۶mP(lPqW^Mɓ'cΝ&n477GPPPݩS'L6ouD"`ooׯC]]8|0ر#lmmwΝ ~^^^駟"Byy9.\HR:K"&&.$$,XǏGPP@,6l> --JLL+T,hFݻ,A(˗/YW#OOf H?c?\-=cڱckoӦ -Y^xѠ&&&駟~b,Q|xDtR*nݺQAAҝÑ#G2WҜR^^ k ^cMjOҒV\ɼСCdnnN8yر$jftڴicUj$t]3}ڵ<+gh":t:tX .ΉcNE8=\E"=z˴!SSSݕ o57)55IWWW*RSbtx.kjjŋij… u*ttthռF||}߿O9]79F'"i-F!iiiʕ+)==rssVSSCM>>>u&faY(##nʮ]_6Ƃ^*9;ڶmKRcty=_zD"͝;Wn5UUUUf/,,l1fGJJJ51ӕ$M:Q䑖P(d?^\J_Pzz:ڵM0IYx'q~z,]G"##aaa۷㫯BNNY2,X ,@vЫW/ cǎ9 8::bȐ! ڼy3PSSe˖͍ij!//FFF޽;qYV޽;헞aϞ=22E LLL`ff~ Rae..];,VJwPVVƽ{BSL nݒRr⫯^G۶maff* : EQQ~Wڵ /^qAbݖ-[_XE&^zEG&@y,#Am۶/迳7[>nܸlΙ[pnnnnD>>>_Q3:g 7񄖖XwҧOzO4bא477,G\BN񨥥Evvv4}tZnbe߲eёvEcǎј1cёƎDN]dhhHNNNԳgOliر4}J xgHlC紣FFFqF""t7JCI__"""x2886{Qqq1@,^hԩv!5++-W\ctIq)1C]#3gZ8 4$XDTwII ӫW˗^/-Y|||>oС~z^fw9/>>F/_-߿Ovb㺺4uf 6ׯ{O]|ڷoO_Stt4"ZxqvvvS*,,ʫ. ,..&&ct"b*++رcGoX/R!t}]}{Dbf,@'={V1.99Y*9x>رwԉlج~I^_Uoonxz%СChr}:M4Iqvvvt);Vĉ~:>gϞ'pvvficcc| xpͨgpaXh>s^Bll,`bb{4Gرc<^EEǎCBB.]Zo"O>nnnlDz̙ NQPP %6;;pwwGbbzsEff&OZFFF(..Y\KK - UUU!++ {쁧'yyy:qh7͛7idii٠YNҒFݬ^IoM6M>]ȑ#>Ґ!CxIH6oL b2޽{I$Qee%u!22ؼy3*++q}5U{쁖LLL0`?Fb8}4>cX]vqH\~/^DBB"dff"77oPUUEϞ=1n8L6 ::: "ܻwgFDD}}}z gΜam۶w?k(((@LL bbb{rrr0w\;_w}MMM^gϞM6g}@Ücl|077w}x=TTTS_ryYea")))e",appp!CЫW/blؼy3ݻ}"--Mn~{@C{nL8AEK.'h8D"ݽ{e5k⋫6uT9ަDqq1=~|||̖W`& FDIt1qqq342zPqq1IHD" f^$uO8AzbhffF_|ϣWJ744ٳgK9?/t;v֭[k|ι2)Mcׯ^z*hiiA[[O>eKjHJJBǎKJedd`ٲePRRT!!!ppp`\PUU'|OOO|'L7aXjؽ{7W@ #Fd[BWW'O_Ç_~./^+B!QVVb,sex{шB^^tuu1d[[[\z :vk׮LetYHOOǞ={PTT_~ҽUBBaa![ңRvѢȄׅKd;_WϦ06.ȿK|P8#~?1۝9Y]KeZۄLm6~+Kxl49nu߾~VC딨_w|=&^ |*O^+c0_~wuNlڊyy4Ӑp;/kk~`f, 1[9p]Ѽsg'&م䲳v3sB gyYg}*fVnJ;.!N8O9x^sx^s/c^W~zKy'I>c>:<^g<'IKrrtofRsFOxu/}7^[Vp,Mm8[^fmvP[q4nNyt4:)ڜn2тUgu3(+i~Q[ٙǭH]cկU>` Ge;>SR\90*Pr<}`Q=Ʌ b_|VXC;8 yml]HY)JR3q%/Z^ɛ!>7m#@o^65I qQ!kVWCa]* XM'֗a*|p#u&k9"KH@ֈ9l< RBm A!v_dp 0diAz[m>׽zg 15ڗX_g0X磞9#(dhҌXn;:WǁE!.o?d/⎧rǬ'.3-8Rh]I"~Rz% tiŵ6!p5A6B*5mSM`?v;a}]sF !5ʙ3+ kXTfj Xvs%u*o j~ r𜁫>˱+лy5 ,\ﭐ#̹䡀xDXL1¥ {ԒVi-'"PJ){cw㢄mPd[r8MY;2 |ޭ8rNY<1, H{ekB <l)eTO`De9`<LJwl m` F-ۣJWh/"?dݠ"$x`?fxItVfnd0ET,QgjSĒ@F kR'yyCƐiJ)0 * 5B]lݬvlFX8tIHZg0N _P(hJ l6=Rt0%|9.edat/@aYcTh]('=:U0DNvSՖ>!@1PsEAںpp/N"hO"D;gXuU!Z<vq @ :LQM 46GWΆhgUBRe(xOMp`W2K_"&rUC,\Z]Cib[; /&W3w-T@UϔV]٢ƕ؛JxS;(yL vJ@;,W619@lAE5BZdv,(See}|B D&Ls%ic]ED+pZP׹|g c\>AնcC9_gU UET(>P5?c;KOD_0tW^3AHS愵vJnOŨƢ0#B;az;2άƺс޹uP`(8|]$m_yKc/HX,E ,4X` 웇ߡEz>.kX"!'N!{dPH]a5%Ba [qvTirte@p#PY Oj(ATF#V{jRn e v-s%_#&ZO-DMKtNVJ]NYvt#e7^P͡T-PH B8ђ-5aՉ6#`gDԿeXd(-+oǐV(kTEBEF>,>|> }O2Qka.(_w:a^#2r$DFPf)NgPa8k/plBu{&;1(n?y4e` fBZA$1XۭҀ9p5Sh4'"hO ZS0c (,+Նj@QEU"9O0BR:a=]P >4qL[p.Q ;y7b)'Ӫm{C ݊~JpC[KhgJ}1%_6h:siR #O#X, vZ^c{&M FF#0!nT%n:L0-Z۹BiE jYc #=|(IC\ OU w<xn{ƛ ((%^5Pdi4QQ[MjV(} [.7塾H@_)Ӏܰgy t<+8uv7h7:/_߽.wnWiĻHA>vV|Avu˰'d^^s!%d}#Jm"pD, Nny6U i1c`p\jQkȤruDdQ5,kfGvDDڃM5x^$ IH@ғl_]kAdzڐ{|˒w_/>C5𻟧jQ5/GP?|§=F ׬D H}s2aD +yθ5N=s$=hը2 D%mIKNvxD!86-$t~73b:M$ᡱU?Cvy6b$fEjjtԒvPS^mBp J'X3 Tyb X:e!D~}(y3~7:~7:~7:~a>ԆlXFhEm^" c'.=iW.4̳m>6FԾC8!4 vϲ,.KnZd<j?b#ه<߰WL0;l5{F3m=l Bthj f)i TWb ,vu,hu,!ԭPl~u4%1VwԊ}@er6g?X̄[;φY@HNZVۮ[,`:N 6Ei8AVMZ蝭f[}# ydSGn32U Ѭ=ʴı&ʽpN쵿w`o A;UH)E'uϨ1Fa{,2/X`o~6K{,t4fq #G3[U%ڿ)}kY+oJog"xƱzf)qw\I͓So zLhcs3sigO_m{x}^d4I\SuMԧUCө/5yxQ6 76$SF)? CQ MT վݧ9{S"k b&"xa$ -'W! zlj ق>#%TcvqJm{[uȚGf,w(#c8<٪ea->iZ׮a[[k(MuM.ЭӉuq7CFC{RZg 23ƞRD@||ҩʭqE*"&h:aeL'(nI4lp"%qG}:r<O(VK(FP>+B!%bUWXmz^) b$߆k_2$ܚunA2NEz⏶Gc WS24e+o4^ WiwGhWQD%u--ۖuQ@)NB_O@  #m];$^;N^IaX᧏mzEiPm\1;nu 3 \XVk}r2IŶdmzϠ(&ͤ2 %DZ79+ͨ>7͒$'u+@ bO~ʶ'ep%Ss~xA2hccF7 \ONϝ{R1/}^|᨟_\v[E(_Y`K3ˋ#L\T 0NUu,߾쁫]fYNɫ|qxʱ$M 3IbEz}G6bvųbaI|an`K&NM:qHʇ~7gZPmr~3F94fskEiSF9r7P$е_.^C=v㔪΢W~q&{*?[0kn[6u3rDIyc/if?֚P .B @ NĄs۴RfB(by6ݬ.WԇK[>oj G~S$cRߵ0 "M䏶Oi 1B\߾_;荓*wvr(OO={\VEg[c7Myym/Ĭ/yͪ5DI;М`KsUo[$IF+: eyD8 H|g[+f rNe݁ؠP ys/H_]:/j]sH{=$״jWIr&QHnCޔG߮jwT7ms.B @ NS(ӨPNkۃ5Q98gBs3:$ H,N,~v]4BkM[ f?O-߹b?ط3qpRͤ'H !h?,W&wbtl47)%9CRK)NJ2qTk7r*UΤ2)۝XleTu|Ny\!@ ']Y tE0+<:*iiʊ,.;U;VxIʂ$>CIO1sGpKd8N]t$y}7 Np D=N̺zMQY 6w$6Ƞsr&QHٮEOGb$ Cq +K[>e]iq8qb : D_MoCenw|}G&ZO+¬)EZ'R{reyKOx˾ à8AڶsG\޻hSW(˘8Y=p5rTc~޹_5ӂrDv|J'c\(X:@ @k"p¢w荓ϝ1N|:8?/f8oolHZ -tFH5Mt\QQ(5[4/+˶Z(USW/6N+߶Γ]!EaIk0е',+ܶQkfBܫ= t$dRNϋմ)džU 9gefq?%? YKrKg%m! 잕gYfIϿkO Sq+1]uS ͬΌ4M)=߶u%=tFޟo9<<꒎/&fjlӴR;B @q$[.,-5a4v lVnM{/|VJT哇VMv뻞ޞ6~Qkя\?*:V?w?i%vn=t$y&^x˺d5\5>EY^s;n+@]{zf0O:+*[znjSI~ÃKF[ 3096x?f3W_4ʠ!vSU_5 j፯893UL n`c7MaX~}e?={V/[|YuSayu.;#Jj+Y\|&~=1/<꒎+nE54ٷ }U!@ ReJKK{3ZQ)@ oh5>q@ qFpE+tp p@ 88ۂ:lHDY%@  @ x@ 8d@ ~(osţ{&7?R@ " @ @!.zicJNIE7_ aṼFKy?m6軿J18M1g@ q <ݞxCi%sV:'6\mwsuz7FsH>ԜΚ9[Lyՠ̲.Y38&ܱoo˲vM2gXw_h*&w8>+d#g],Bitaw{u|FuBP%V{SG=Y9OA@HK%o|>Lcҩ"& q;LPk X-Pe6}cDSxK3^8y#D 1@3y=PqId5qٝ~Ӽ AX&lz&i`[Z<՟}yӦvm9ǟsqw?RmMoe2j ul6 lB@w5 9F?*;˷lg>CLi Dq:۳P|>CYX*!>9^a8X&M^BmJSAcyȳՒ6GYx!L3xiΥeb{0ӗ&HڔeB+:51B- a_iJȑw4`8$kygM͹DYxi.Cm2|2 ~.sۻ&]^)ź*;g(|C/p':>v4;u*c:7DXc];X-q)F0Oogvl=C/6>i>6 ˺Vu˛9b*}b󬌲]\%f_%5l9H9af$ < 6k0R""i! X^Pdo0ŖC:]aChmIYd,a ܿʥʟP@ Nqz9j*A]'Pjk䪫ի񃩘:=ós1o*$F|zxe#Lmu[?ղrI}>!i7NM[^-xe3q=|¹i$*OF,Q"4L3(]-.7[Hn9%s+41"x@/ȐL0RZnԺ6#։~@M$2]*Ur1~}$Hx&a0@d@0{diLl1J"&0_׍XJ_2Z]=5Qdag+5hG9^<'xv-C:!%"Lf&OЈ0I[x:Ie[謓*;ǺhDNJjWIEW Tf>DSK?)I?@RK^հc&XZ3k _tGD4&~tV/2$N8RRKk iۻqs-Icƛs<DŽ>}Pi.wk8+<4FȔ^T9HVW!4"mwfnUP˲LqLt5nc:+nmRZNh{}]kD4Ib=@0) 4hd,.?2j~ş4"Y;dJ `g5_oL*JCq!* pY!wµA7})ǫU;cL'hi`8v6~GI9I2V9Iy(p>2^PΤ2-ǺU|FO_ KiD|N0]`^=1JLK?))~ q: < ^p+pEH ?};FHx6I}^&iM,5weAc6to&ݲ׈<˗vbҟ&mY9ޤ>c~]6`#PO(Tx|x4qji O8^UB+3[^쿀PE= Ӛm) +<,ަ$0\$8VI%aV'beb6۵iJDl󸠱,d퉶{t$DB\)yXvu"[,pt"GʜLD IH@yG~2O)r&I:õ:]AijLyʧ(qJduh9 m6;XK?)'@ [/Nko}AK[^9>0w7ب>-+w_ֿ(|Zvr[+Jypk9?&B&y(=!u#n1 0 Su9X7E44Nh2#nU'*}$H_5ca|]K=!cק8-b1ź'cy(T ҟV m28ny#+\)y8v(A.qBL~L7BT8)at2 }dN!I~KrFg u0@JWI 6U,ۥEL$X:̈8Gd߽#q7Vl9S& 7i5 ,9'+[nH] kL}]QjsTR*;o.Y7~ʹX2"SW)K*;Gx{Ӧa|LW<+vu i6FdB{rB%X7r} bԀk&((Ci ~e5u:K1^Zs:_ytS/;RiigeM֖{mi5.a[RiY9v k̜ѳg8AXqǢ9 gϪ蟧<4,K&8/m( S&;|^`2)uqgEb1MV4=_Jz(8į,2rmU; qZMR+& PeY2 Vlt_^:zmB7Y79%OZ7 Pcy(ҨL{r|k (2VqT$P#urqqj&4qtsi@^1˔Q!Bħl3|E0 2s^ _eHP,I+ y=&B'f{{ iH9d!im^ .x?sɉeʽI划g"u:ciXvC&qZ9 dkz5;~Pbq dW_RO?@B/|A&\|gߟ-gZvy|]FkReLJ%_oaGaV] Ee-dI̛d"ϺiIgl$ ޖw ES$~x֊ )UZ'8Ň`|'ڰlr IDATClBLn`{hvK6i8*l>|{EUziNSe[&+sB0u~\oSj?!^86&|MgI=L*TӻQ6?c1]f-Jj]dg\MyMIv[mP 6çJ$u;LL&Q: dtPT$69@צ4߮-f&to:{<~ 2wd Tv6kyo:(6}PK9͚!nS4eXsH;i趦ַ2ug%mBYgg)X>QY^3&xFdy<~\9"i͟~4F֖ji佷;fL2艱}To,_g]X%Eg~ߵWX:_q}h z}fw3&&oi &2̜j{n6`l{^vdGGW/V{R盞zٽ[^6~x^/<3uJ\nz@t,ԝpݕ S=4 ;ǖk&%w}Ԟ nΚZ[U$*=g&\)Ye%E1Le/6G 3P`1#_ahQw` 5.5>8 6#)EkcHk Z?r%u[ޓ!9vd?2Zkt'H*v2IM)̣`y,'ES Y\Ŵ[V \56|oQ 6k'K`7 w}`=jUUA`UJ3ndo n./ DT"u"`rsyOoy33_oLV9cY희re 2vc)WC%Z;>Jox: ܩKg\%f_%5l9}?F$qRegR@ # ,9NtPŀM/5_m Y-?]yKZa橞y,w y.o%ص1td6rW},p?UR~wWζxgLTu U7Jw.oˑ(Ճ\PSϾKιu}-;"\ ?^G`X:,}궖y9{+]#r>"z-w=^}(6g>\)Ye=r!醻Z ҙb);L"Bcތ5 鮃G(harUTtڞ7qQ{@w9s7EAUs\v#o=+0L}&-Q/ |-3U+,W |/8  #'̉Q~`n|)[ )g- 7j] DȇHMi+gZӮWPJ,#K d:>L<,ٗtXsӔI T64 :YG mFHaϖ! OFJE.j?|a-lv&aP4BtP&#p&"kt.oD2Oy+[HnNGyr7%_?=M[~gi^+.0]}{{$ɫm %ؼ=`zת"(rz+rr%K],Qx󬌲U-`wdPuw9>RwD Đ֗aLpFjά5;rj7wm@cO`aOGk+c@|wu՟]Zc(_xM 厛;n(O-3ޤ>'} U}P[NǬ_:G }!rO[8rv3l󸠱,d퉶{t Uź ԫ %aj0^%Eh#d,:F&N`c=4miGxޫ!e d҉k2Rv&U:Ru&M3ދuRe@ GsX:ɾ-e {;ly >"ԍ lT` |i|Zvr[+Jypk9?&ĒB&Ya|-DNKh\;RY"WK D3!|d؜ ^hDlT*'^%._ky&L36ΥH DZ>Qz1 GDH쨏PK%/6֮}=O)̣@cgYs**y8v^Ѩk٨'ڔ$@'d߽#wLq7Vl9S܎pB, {ڇrэe͜]{7XI.τ5&[ZGk\4Յ >%/R'dJԹY' u:~"C^rB@&=M  {[M62p9:CS&^eXyhL||Q;c#nhɼw>ǺU .6;2܏.'*)R꛸Buv&YQYɌȥOLf!ԻXEV-+!VG٫*g:I#JJhjȢàޕf#q9 ǗܺMQEUs1.x8nw_Mr E4qV!W~L"biK'.: Zg29 L=4],(>b⺭$־`5k}& `sokقhFnӮFLYbtFOWl4ؾK*-+5fN rʥഓe?{<>a\: %5GLcLEe(@h$넆Њkh2 PqZMR+& ֩k゚l<.}iN:WQRֲ.MVPe֍`d U2g"X~d4 Bgey=̘~d6koB8obgO bUf:X_zѬFknXe}{0fS|Cà||zȔ~"ƔjLb2K,&@n|/.T]z 1צU؆oyIW.7dN־uSagsi1+0YA?l]0[?\~{gz^="V%_æYGNUoGl4׉o9KT6\ry^&\)9ELYNj\CvƀY}eo~1n9n?3r);L"B_YJ5;G][^/ZkiW> -T|8RozsQrl~v$ m~FnK:Y&..sȳ{>ygRډK9go ;ܲri*ٯ۶#f{dMW^b9yC_e u}PKba8↖ַ3Gsw;ؼҾ:d sjMVLe({V'i3)>B+q>2Xm<ʪc x^\Ch˾MY7rV5ZeD"CZaT > Ѿ&6Ǔ}iᡃ:4#+-V}W&nd*]ޥt1V_b{wN8X>j#4LusK`Oi;l^z?sx^~=3&&UD{|BsV{ؿ^ᐊW;o>Uii{@b'iݤ<'B>r$j&鿹#}f-8jelyXymjNg{-jPQfiطƷe9֔횜#F[e)ڝֿ.TMIԋz=;},fL`ك37qLG;qRB:\&mdLc@ ę &>}g 'x;OZkevޚ1v-<˴T| GL\\f㥶gyy, 4yEӗTY[]Nc4L%ݵ;v| fZ/}Ye)]gdcd9:IPsI[V'FmǪ3a ' Ƶ*0B&.? 26PnĺwL&i!]Ui|_7@ x!wk,-ߺ+/ӧemn]|,=fftVgՏRU,'`È&eIS4G\i4 nnn3v.TY^3&xFd́49}ЊAc&M*NMt21]_؂cWgiW)3i3|4NXʻĴk dWӽ=t˷lg>CVtwk2cZ¤=cM%ޤx( R%j*ucg%mBIe"J3e4:HE!Dݦ;0@ Kg@ 3Ap޾osޚw%ḡhccEôoRITĒݞ]%s/CޔeyK궼'Pqd'TZh`FY;ÍZ4Cq:0!h Te c=UK殌λTTCkn8-wq D=Rn&9Y-k`w([xhZSWPէ4y]țpniXh\ }}jgq@%>eO*&o%;% 0@e:TB^c 8Hѽ!MXo*8 pJ ˄:+4dL<p_ v&#mр[8- XbNgR刔c C8UB@ ( neۍcRaOγRwhD~'O[2Ikba塀+ "K{/)DnkDk;SW1~] {M{oRPYoލ[iiO(ISdIuvN}µ:]AijLyd't"{҉!}P&DTlWdWƲm'ڪ'4P w21`Zk4j%ZٿT0#"ԒpT划$ESD9@ /G+_xd߹/2:basZ"!\ l4譙#>7eiy΃[HSJ&YI %WpxvԺepl߷t:%u0j F B'$pB>mcMg1B2h$4ґBvƫU% !)vFВya7A%65`/ǐK޾<^@ SN[?qўu&ZgIBh 4]ׯi-#&-vB Zs:_ytS/;Riig zͭlis ZOef>fTʂbcFϭ=̐BRƇ$T|9+U{D@RT%J2 mm',3K1\8KyU㵺b "|+zs0᷻>$J6jńu^LFz}vC^vO [odFT0fȇjrD$B0r0x^&]0qGx ܙ(:뱿=IÖdC/AgL`kz982꒾GZ[jQt [D4\/.3<)a#uHXˁr|#^)z00\\sto^qNK:l '\KrI>pM:?_ڵY{Ur]VG:wDz NR$0a)4uŅG#6_''M^,zMeGzDho([ -U=q]W^jX?Du|ަ# y_mZI;P.VgJ:J0S:Wfw/K{I*,-|u1aY$ I$LluqEō&:reKZ(Sgz"-+0\ !"Lڰ#4hnG5aY]M66үEڷoo[ kD^^upy˗#B8W|tY"pBrS[B],"^kPK#b,h(8( !98fc[Yl=12aAUJ/9@O?2w"2u(+J{6Gvm")>?M~*t9Ua "BXHBcvw<>$v=i1zsR];f'ĨiBijn}COwՏ_+-d^ cwl" I汿f(Ynl0Vy,V{fvѧ_V<0YO+ 7!`֘!kiA5-oQ1ӨGẄ0m׫w| 䣐>vaEe/m6ai"imZj,B(!6t >{gB.yɉ2H;.5f#f-{99uBk/?%~Kؗm}#_:!uŢI~O{dsLwO}+, ![dkqBd2wSloB? ,=`y>ʝ>!g[2,wS4w9gj-l I≼$K&.xř(*{?I88f(RS z։湍KbP\{D-xdӁjٴu/|?޵55Nx<$1rH[W>xJDƌHrr9`>[;폿g_ȯꒊowFuMuբe[mxblkԝgsJۗV@A%˶>~cϗ{ORc`sknգkeB`OQNLDV+QR#[p!B۹7SG ^`(6,mP:nkudeť%YypB_Xu^^SGۥ]G.}94efi-XsG[dzl3v(v}o܇!qKq\p. u3W&k48)kG/>I +h9*|cvo0x/}dބrbWi:{K䁟r+OMJK`zֱYݎ)D9&1ݎ;gΙ1'k0hf> o}mVt4W*/q17oB͵lNN+$M^YsEry 즲Bߘ^fԶ4e&L\f5\ z۹Ȇ%ǿbR/O(MLKS‡N%;Gq=U֡w:W~`۲v! KklV\t+bwqʥ^ w&xḱ#9gs[]X1lpLEUsO*)G `3?&4G,lM|Mw*~{ۺ3?>ñ}sU`7oke$L,F$IǎEJeGlψ  &X9ԕ77rcOl5qBNifE!  |VzkOh5'N\k!pkNhܗwX 4}JO|h*WUPSW12}=F_ ~;; io> N]һl!|.:鑧c3_}բw>u( J8_l‹I3#[pڇ26bnbݫo/n3/7ﴊ]I5Oa H+0!D݃OzcoHng$xrvRFc{oD2Bj@yD!L[㰙6'$|S݋ ֢'QsRp r2#}uyǒg֫u%<4E6Z66@qz{No|~Y|LH^APȟ1iXdx#O$e/?~ #_=W?ADovKׯ}JRȏuড়&uXܙ#v,3o oݧ&h?_d$N_+MIa~qja?մW,Y_oV׵Qi ?6zū2{C ג\xҺb/ڬ*9 îgi#;m(8IDDZF„nق[L" ȗ;+.x8,){]No<؜63[l'|O#M %> +|i?"mLv7NJm6l_ PeMO1l}doR%ܰf՝>_?ew_{ 3>܇PH1W6mĦ?DQ(폾w*(z}[[i mxb'Zde>yTbr9 A m-S w/wLVbb;Y^^ѣG!kCl|٬C`߾;>+~ n# _dG(N=$x~~~0 {H`._oq 0(-JxU7p`<׺݃J&, OgDUs57_S$(R2FحcN:g, +{3z;n1fUMQ$V+ JRGֶ̝ܲIX6˘tPQRVZ|.1-we]oY3x#?/.~ìS-~I*0V\AWN}7nQȭ8*}}HV?"u%Yn,:%vBMW |^Uw~_x_0tΓ #Loa/?J~勯wt<"ݗ/zw_y_u;W|tY"pBrS[B],"^kPK#b,h(8( !98fc[Yl=12aAUJ/9@O?2w"2u(+J{6Gvm7{ W>v9y5T'VHGO Iȉ+0 4IX <{bP7Y7jΏ@b4!4Eg>['Z;Ne /Vgd׋aMR$3GgMkRcB N@^WNKbArbL*RkgfT,v> kOQTfKSJ?uR%~2Ѣ.]*o2,wS4w9gj-l I≼$K&.xř(*{?I8t noO'w|򯾳ꒊowFuMR|CVy*uٜ& ykϽ^H8bXܘIL[;폿gR/})hAĞ IDATDG-*8kd RV0QlOY~5+۴tJЩVvzȽc=˱B*<]6$v攴gO0ۂNKΜ<|cB|"ִ^a8s6U{A\#@!žxSQ̣B%eBQT^@om5٩3%Ň&[I0+uΟ-:uǍ8V/ԙ[m&_m# g'Vc{e7#zM_E_#:UU?){L_mїQ;I])RPyĕX7kjڏ.y\k kf߯?v~(:鑧c3_}բw>Ҽ^AGw5f(K_J?흆dpa0 ^GE2;B^Q!S{+6f^vj{AUZ'7r-Z3h#aI&2}=F%Z71Yx֔Y[M۾;aտX}k0Y*^{ۉNa4qĕOuT^ȱ#6y yZ徑)WyS4ER0F3jh7j6 4ñ 0\^b|] sbPb73.fzq~>2Dt͆sCOz殿.횔E݇s}kT)s;5fMpL*(Vw\'O'g_ưU6u;Џ=?2mfxbO,Cànp%-zU]'No)3z)! !$ lG K勪.=ߐF_o]ޱm] O(oͬo$2}4|O\_xȉ+*MghCKhO/VXZ;kZżrޔvBWO\M~ŢIa!!>nu]+MM*,3k|l?44B|YTZ{O~2-}h MA&җҏFdXI_{wPy ^H}l. 9:em7/;?St^fߐµ$W פ]7_%a=myNPp" K ܦ+.<DoðOV<0ɼzՊ/w*<7{N)nXfKN/r>䫝?ew_{ 3>܇PHOMM۾;Q/~7k]zy^R]&]'Ofß??:տ@U4zKG=bߟYs˟){Vۄ9lc:Be;54%݉C^ieZj s A02`?& {tt':uFD8aب/9 # `yb|D" ږ_02<? en"0*[Y 86llٷlg^:{t+@QXꅶ;%$R:ynj%/5o 2 [FCW|tY"pBrS[B],"^kPK#b,h(8( !98fc[Yl=Gṋ|!dPKrPbƏ 춲̝Lʊxcd_oXʵY @ REgtǠHjh ܓɪ:'cH|B&q1tF{_ةE|j2Ih0v&)`m#;uAdd"SvSK6Xy{N~f/8p㳟۔Җ:#}GB8g /9+Z92A>yN@dq'3[[ŋ$k5)CkpyÎ,]'CGL B[StY;)`)Lʮ;6ilstޢFPU+≼3xByB)W *{?I}lk${뇿6/B'-&Ǯ/ʏ鬒K@$5J܇b/ I4BH#@#הl'IjԔ!#* o_hUFig׮Sϼo؜>zj̛asΖ3i}$R.a+_㒒#$&gnj1XX@cn9,7 D\[9rorG]:ԝ̮ 4T] `JYbIuj-vV1+W'0 9oIP$ C s_a,V?[tʛq­&_hѩ3o=KUGf\Lyў%L\reϛ>1ES[$ 1ívaƍy5uuAc+׋[BY+1O(^Hks_4V}w\z<)VVd 7B$I53:QZ}~"uY>|K4Vs@pם\y/^&-/``v3z~d2Y`,0ZUuF%JϨ9zDT|M@HXbU_G8wpQtJЩVvzȽcd&zIcR٢SWv(n5qBNifE!fbR IG>+=5''.ӵյ@ֶ/;h*!T6uMcdzQo?Erǥ"!d5v]D[W氙;8|EU?A˰^dm5­96;,IdzQoH&.L[- e~IVCQ|aXqc|M]]X|JPZ+Y08M/2nhƌ6g<'!HaP7-.ueQN@7s>+|6H:\o57,u /p.(bvz%XW7hJxBi}lf}s/ok聾= I_5Geg"s2+^u̕O ē}t~kf]VG:wDz NR$0a)4uŅGF'?`Q&%%X?%ל`Xs|Q.e tۇXf:RQD  {C6llٷlʇ1'ʿ{~Eaҫ!s#V+Q|YuW?aT,7K <7 X{{Y/ >EŌ'[-YgwSɰ'2,s蹠{oC 6_]V垣{">!4tr!rY.jvbc& @ ޝ~i #a%iMm'׈$\ ~ 7# ^_H;{y̭6gk^J֑yUaszgZo:n'G.pώ~]Bh"~Y"OO[m38K1Ef;(uzfi?nGeOX[ V?"ugSK}C"p{e>&MW |cc_5C<=&itۤrBcMj5]=߆)xZ+E;l!1~+Xw?,GotpP5ǪRgk+ 4!`zյm.F-[T^^p$)^&xGu*!:[*>%$yRߨ!RϟU?"v̂Bc.,> XpܚˇBU=O(*Ez>G_o`n+܉(ԡ8+\=FoEo*!ᐑ~VmB1cwjm{"kA0pw~&ISc3_f%J3B"( _ Ay$v=2zsR];f'ĨiBijn}CO͡w5_+-^ cwԓ" I豿Fg-" !h!$SJ$R2*Owؙ^ba#'x[%U9xpFVk35v9s&uhmN6O!QXccM!C#&!)v-[!"2/<(Lh <>M;>-m3(.Exǯ%9)K;sʹV&]LO+P6w-<ݜk::`7޸ 1iܕxɵ@Tl܌0@F5.iqDRAce+42N6e~‡b>yԗGf4Va,u;6ilstޢFPfpB<^UuO(8O( $qUe';._d:M"c's,+'Blk${뇿6/yc?9zr𕳭(;97~!{c%{x húY IDATO/WN qII&m|/qgՖMsEՖM[6^J[՗!5{i̕o?.1pw#";s/ I4Bș{TS۲)$QS;FZ,~u>E4J>vr(? Vas!2oކ6;wr&4VR*e):Q*Ug['OQ&v_V23 h0S4G9:Q)=9x-&3d_`8&)|- ƪk >.`I yl6gElp$>E1qNxeMr &P{G'ʃ^*#(#Hʠk,!b+K!-roL޾߂K*`Qh |>νQ++,ZJ[Y>)1?X0{(f4TSC"^ ?jq؂C1Ii +umf&c_lor)Ƨ(<*'b,L( GiP?[#n! 7t%o^v~(2w{)3U\!kI@/<7|XP[⩞'Qk 7OaMuNWя; 5%X$vjEη/ ,&/ʧ _.n#G~GLZEf3{Y Z E`UG9 $$Y J{(soi #?z%{~[?35d܈zc&/38Fh>Ѵ8D {k<*-x̏8FzqDžMǚDb5 *SA>JkSa <Zc_lojˮ/'"n#ˋgt3vbc e2NIC}ښMv7qIII0C }5gn"0*Gs ~lѼ @<7T#p ;Hoϯ_8 ZMP0aY`|:'2ꯟۭr'D7"n5c{Y/ >EŌ'[-YgwSɰ'2,sffc/2`@( .<.`bvozGou/]J_"+J٩o- 6GϺo՛jG~$+ۍ;RĎe+zZQ;WM:Ib[ǻXӷ?d/ngn5TnCuTN}jKHY{QC?ck zD 'E!|#nj]X|l+G5#fU\>2yBI%U9(IS$a5^Adk1 $>!FMBHS[tv:xՈݯQTp"Xa5h$x4bILFFc[fe׻N>fT,ZЂYc燬U˧+״޺F{gf3>ueTZ£+fDGͶ;tuQH];İx2}6ΰgfBJUǟ{ӽmߝX|zX]nie ' hW c e~Nk;6ilstޢFPU+ϸKwve̡>si[k"q8꟎MX.^k ۢGFDsȈQR8-j&߃gݾ+IRk % :?91LAR! \U56nQ'xdg j]B Q iƖ:YR)&?"i4q=e/b=z\ Ek_r;!!ddԕ/L.%B_H֝ڋxUMm]˔;˳ljI0"cWgd `Ġ!j\d3 :!TSנm>y[vNXdyD9붽s[_=WyEiƙ;<9SjvTzҥWǯ}ՌWpз R^ùv24׬L60PR!gp]d@#zzDIٯJ*`_^ITI2 Uqn.9),BIEGUƫC"ݢuKS2)LR!Uh*Ȭi&VNwOhDNJDoƤ[!$q]%^d?&vg7E4TԲ3(T[aëfyr˜_~ܙKfƌD3rɴ |.\K,)5ܿK;n> sڈN̟5z"H47F14hmid2C맾(Mg Ow JZg݃D<|f(JgJӺ_+bJoi$[3tjs3yϔ喤V)=*dvz2fmFǙ131zm߹ E?1wZͳ"Gi^h!F Zm]ýnkJ$Ι:hWNY%U4sVN =sĘ㢱$)#}AzA<===/oC}5xt|;l@Ьka?_$x?By=N-(.~WVQ[VQ rp&@<4W.=`=bP^ G;5KQ(P׽ Jn@xUŹO׼yM'tT,(~vѥƧ˦,߰L+'4Z v6F*◽ω;R)q\SYn 2iVl\"Vg{.Nzw58Z*dVd YvێJr~FNVs,}C]Qˍ,Үt[?j)K@ ZJ"$_u[H{)`@)[ &c;(UF|±ٱv}]f]ô U?I;Na8#c"4sb,xx0l`4{Ҙ`jmG(ugx ^BnB&A՗}=wpnYRs KjyU Kנa%)J2w}xD^ЙqTB5&?J"v $+6TNEN)ߓL32v{.26OwoGY9wS _&ԄѥRz tg$x*B̫P-^uɮAfRPMQZ21]^chj}b}E>MD=*v[=UJ Oިm~]J*5 *泫k!^v"46!CWŒ9A 8|Vĸ;[:_c^4!.|xOO{jaf&3Ti{$Z%rS~\nĠ-[LU晍UlbL_0LhwԪA}wVƿu:w'⌞E:u5i1œ狪kIs ):ϯt3u<;yzAs#'~ʸol,Mr ˶=~Y^}>"Gܯsfҙώ8rP5gD>c8n&Y!TB3njM +35sdb~SѤB>x[*Q!Dy˭*xF;ΠPd[B܎xmsց޸7 Dl c1=,3B{W'x؄u﵆Y-zdDTAQ?7u+.z{fY3Wn>a]*1WMB$"?,bY!_V!xiɘS lzږ񳶋b~;r ~0`_9Y9+'>_Evf YPFL$|!ߎs?m-^$vCxʥP!SJe=׾ $ ܑlx4ݓX0)QgwE$͙Idr;%$"H (p&.9# !UTJs}td E%d͙,'ڬRN> jgcfH3 H5u\]KUhFے:clkkmZysv W> 4'rR57W Ỷ~>iP$ٹ~d|*%unm]=V3ɲ1O6G_1z.U" E>1C h.|C;KBeG}جs&x!SiLuneו;x#|B !$+_\"Kt3[{;ǵ񪊛 \3{)w4+g'z}3SaDn:5["_o&ӿz>Ð@"j?@_n;<빂6bJ*XfnάGs+\X,k MH{%ו[O g^=Wo9?7J(# !TR@ 2H NYYoa4c茌}vލO5ə-RS02-ԓ.ż:~fF ]W]2΍˕Tfe!J 8c"c|3ުO3Rd=zIT6L@b^R(TTq44s$2v6fάrvIoi&d0~3ٷ';υ#M}߹Ǚ +ܝ^2Ч4ks== ٯJ*`9L($]ow>li&t+2cϽ nC^O1dR !BΫ.$Z UlAYwLB3\[UތI3BI[㜻K֧?~z s岟P]RΠPn=FJ 2DnoEƖؙ>KɣRs&ƌҲ+>|褎.psW/_ JMy=,'U^m/#;8G8['x嶝afM0 |33?k rijW6V31A\D"aLQ3Ԟ;:~?zz|3FHhgwâ~L4yEeK89z{[P\s1B@qx?‹ eep ޗB7N9R~t—YHv0uh4ٿ-qiDZ=b3l/x{W.Ɯ:1mʤ/]]prt>$ 's&Jn@xUŹO׼yM'tT,(~vѥF MYaV׷OhmT/{wZR%vLk'2qeӬhD']䦄|w%ϯPg?S'_|+J%rŧ>>\<-P(߬ȝ|;L8l6j˄dia-jCK316!_DZh`nKW+ !vwD9ԲWK b2(2$z?ԽgL{+@A9q' v!S2i^yON[6?xHĈSgŒ9x0m:z¢W?iIW3Fe~444ܸv0 { U6liopŮNߵm [l=z-?l>̹W\'0B<0|;nK1ߎ˲q)ƥe/ޤOGT*';0;>OgEb!D"ܯ.ŲA,yL" fCcuۼnMdg_ /R-Y|aG>ڱBmYJңTd'i % :?91Lpkb;xЀg)穩/_O\.BH.WDꚚB\N}=Bhvn蓒Ok+/W/ڶБ[d1`jDU*! W,Y:p/`py[w"3^sqҕk1thpn/! qT&Sw&OUdEVҒidbp5`_|P'=i;j0yvBkH-_Ot֪A ؑvvuKt> _54:y%(4cK<ċTEDזL.s5+ X~I팛K2C+zdߩ}rI#7^h'?L&U#d29D" q32vlt?tMϫT*gMm۬վ}Hw_I3ZPZ uN:wꘓYE $x<=Uĭ=0O&B2O1J,Qb~]^BsGWh/q2E!u!)ͮO:032v{uF֩ȟ ˍ5g~|7cZrJڎ{>Zꢙ͗dRT}Q!dyz!&&-/T}CM}Ҷcbb"4i'Y9݄'leUH$eٸmdbi?[17PZVmĎh^\8x;d*iF2 MBb>G!LBf7/żZ)/e;OR񪊵_.޻8SryvD5:#Tϛ9=eӿ32bYzƿNR(d?]|ӷBP(ں7ܼi 0q8P;[@վ){id2w{WRl\] bggg뒒-s}}͐-SbDnY~7: Gq>OlyĔ4it,k_54ꂔpn]Lħ25+ )TTms~tqII|CgOPnƋwōs6؜fG{"Q*}TfB$Hw0!Փ)iivBRh:kپ&P( ,m Lwnt6ѿC&ŐR*MPu8) 4 &Xؘ4c+DѼ5ι`;߾d}ZËt~vû_DCuI-;Be)6Lm u"!ĠIz$k++PN*!"&NرKWK_  u gCOUJS3 Iz~U%UoXhSSs#%hN}w쨑[6;v@ |Ͼ-U?Gjs1̞1[X(-)*% `zdY%-Y8ĸ]سGk E6=&Ktݐfjb©We(d#kjko`sLb|; z_w7IV-b_G;^S~|mmeemeŲz9L|ֻyE GW?TʧwmZDS|K&ݚISk߼@ Ɵ{KUErKRn Pc2I ;=E3]CNy_uMss3 F]ǣO~r+<|bb͠$lڲ `>eb\l1D̎4;􌌟6m0?}s^"Dh9M_rB(/ߧjr/[9黶mwb~>K[";7W*%5u=ºM[O4aXc&UY7~ۂ F]fa;;9 :<$̝{~8a\䬙Y~vkՒ Ŏ}c _5Լ@ӌ- uSL~E*"hz"k^z ,^$vCxʥP!SJe=׾ $ ܑlx4ݓ H #V;(L,ӧձ$=}BH&)F^= U̯k+[^5+^\YWL4*?ovY"@[3x!SiLw6_Wfhb9 fbԕ/L.ytS_ԻڋxUDZ{gjV.Nvx"cWgd "5ə-RS02-ԓ.ż:~fF ]W]2΍˕Tfe!J 8c"@[xMnC^O1dR !BΫ.$Z UlAYwLB3@oSތI3BI[㜻K֧?~z{xOgg7E4TԲ3(T[aë&v{.2yg݃D<|f(JgJӺ_+bJoi$[3tjsmqƛ 2R.mV-IR*C=zU$5ԫd"\Ͱv-QzzzB_|z! Y.hy?_$x H:=/ ns/\H[h4ٿ- ]-d*ݣg{BU?]5jt>RٍG34efZ9_>"WK!6RE|NiJ2`rSĕyOb!:vȀ~!˗,B)cѥ@o BקxP6Rdr\;WG-7tHQBne,R0j)lp|M֝cn#Bb=l1_{W VgFu>ϸ{ IDATg5`v~6i) ]693k+{ޔ_v8.E;; ^I6U$sJSWĶNW˳>4 :vp><82i+̄n.=P!dF 8u9q,]Id miDb3n-*.)(,zd}@L :?g!\~SOOO*67噛k昚ΏYT݋!mbƍ]V^~,9ͤ6Fq.sgl/ S,:~8>!xd٘O.!cG̬kgc.*Z 8('۱{oz ҝ[:!<]njڶkϵ7uU#7e% :c3iT]$#3xHo/O##.fͯ6]2rl޽ƍeiiࡳct!t*+N]u9m ɴ075>ósr?~w୻ BM^"TmajM +35sd&7 *C]!Dy˭*xF;ΠPd[B܎xm=k;ٝ:7`2PȻo0_næv9K;!H#'XO0~޾;rPMkxʝ{Z<fظ4^&&ظxX7mlZБ%;vra _54:=%(4cKCp8/RA_[2-֣Wլ0`Y%)3nS.R~,}V%YwOd{uFƣlҔf\n粞ުK|z=c굛 whW<2גr3>JZp!a:v1t[iT*ZaKM#k`g{Ck7ؒ:cGV. kjkbw7W+K vn_n=7[s~Y)i Bܢi݃D<|f(JgJӺ_+bJoi$[3tjs3yϔ]nImRsB&a^= h޶_AaQEnn+|bɢd2Dh9M_rB(/ߧpr]۶D;qxO8Yr2uΖZi)&kd*+rMo21f\D"edfūVia1"c+b\RRBbg'G@PWӿ066A*ߺsgFWٲXEl6Bk`=x F.6b 1F;u ԟ-^WM60^tx]\.zӃ.H\.p .N: = ?fϾ En\As 3c856C?| <$x3hg>p^d+nkog7zİؖJ55s'E(:9:_w#S=íH87)W3N7wn<()7l6 ] 14e/sNTJ.iԾcB&{-޶F>kzĐc"ff=WtqoyRdr\1e4m'S&N8sۊŒinj E_[[􆓣.]Hp8j9B(2ȡseԕĀQKeQ5tSAeQI-f{p~1V"sNVC(dҼx]ô/SfS'tzȧw޶F|7ÿ_|Cn]R2fw E~\JDx.];RJ{zwnL~0ԦdNiv ;Xܩ#!~^Uqy'\!R.݇8bU=-||WU\S@ Z*h4;'?LmY:_+KJv ʍ ]$ { U6lio4K6[S.xp~A9_tŸ607Vϥ[JңTd'i % :?91L٩!-;'J{8:8degKeVfV^~1݃Jja{vo|ݧՂxh׶-pB nW(94%V.>tт۟]zg{@'xdg &HCc sċTEDזL.s5+ X~I팛K2C+zdߩ}rI#7^h'4ewsӒe*U⣤Ξ76fe?m32vltʛUL*Ug!{{MYWTVuSdn]qΟ[hLF(H$"P(Z%s9y}E  g4M %RGTei%Fxܜܜ|Zm -#asDwKzT:Gu{s 2]~xrg^!S2ɋ[GBd Sk:#cWgd- l`G va*50{r#f>$,7͘cyF*2>}?>!8lm>}ZBVKiꅷ"H451Qf0or6~^;d*iѭ2soAY9!uH\"dy-ϼ⦶.eى^L6Ԥa۫3&"c|z^~% :qG>OV(u HxY=r_07?|?1!;~篨U] brҊ{)I !6EhsZڼB)5y"Zd`mLw!Ĥ?,8eβŞ^W<_Ѧj+M5cEfWB\ы[`I/~ϚibeG<BHOoc ΘwW;&MPJUu c !|׫/@VL&eXKPf 쏭~ ȣ_ nniizjiKqN7Yr劗W|\lEUݳgoxG̳'Vt3fDykC)'rDz؞q`)~ф}'v}KdK7-eMǜ]@r!Kw^>w^n(Y#=KBR'Xi ['gx.aS ֔xsk\.72"tH|UX_{uX,>Cv~n닎W_w'J$Di=,gxlH(PϷs=cb+V^՗j~??kKo3ã8Y|\o|e]"#jm;8uk^f'yg _\߃e%.-wv6|}M ]V^۫l <>\3x(=)G-X9ۃj䭗=xM_74lԵ69$H4`JΞC'].M0 6gɜW6AVPZe1لu;[v6ȤbJ5_iSEi4Xv4 ^mC+~jyª ,pgz!F. IDATÓ_t3"VJ&.a~7_scՅEvg/z#,O9/_"fgm}&ELKgy<cO~s|]//=-NJnj ̒?ɛ{;*Ɖ1I^߰Њ{5ku=uÄwr\=Tf^.4SSJ/[;k#,! y#fSm~ j'&2xGy6F$aWvrGp3m\^V^_k?dӢv~o?]Q[ivը\ -دr4M|L<~RYiD@jy Dn1p<iYi͟Md`I,o{=߯nf3]]S;5|s)7'쓭[={w~8z5>.ȩz7_R< x~Ht}] Yޥvltn>ek)zV.B DZsʛ(8.e3%xI+>Y-#; ƂI>|?6f/n{ݿ[cY]+ƿ[l4og&K^|y݉W__W'růůNcyMO=/__{xcaBx𨮺OȤ 5g86IU_ju;(".e3!jֻWCq^=cΫ.u;&. R yn#ϥ'͛ɧ8f3}ˡ8NdԶ>bp^{ 3MB|&L x9'OwtB*.RՄj0JHb$Չݎ@[ !zo'_hm!_9/z;cQo޺W8UuWG-\mԪ;<'?dHbwT-8dTwua6YnmTdSlij2Bq}FGE9;BHEU5ˍ:QUO_|UawW;Ck2V>|g/_܈qmzv\C{Ƨ=cP+/Bu$Oײhgyn#N446o>|o{ ύ4ŸKB7\J$oHĄǹEVt0룃Y!!/Ҏmyk\BM>t"}z7ߊhDX}>-jɱ5BW*%~QӣU] 4/+.Weys^ڠH/QYfa<uӷ|y띿xd#:ZWwk[R=m9#6j646 r8=[ 2eH8|DͽOLHxa+;0 ^VKHJY-EꗷħN;DsBIzBL^+S, _%lhKiasޖܘ6+T-Ք@1I ;ng-8>Փ&iϏ}Gk1O~}sŔ ΘwW;&MPJUu͐U>\r_y6X|߂?l8r~0l%X$::`$nuוw8|,v!,Hߒ9$zI.Uk@5\Vt7,#^!/!$xƢЄ Vڤl/لu;[v6 zrﲌ:FFD׳;iC?|nU՗nôgcSdD?_|\so_̽;Q"~w`'~ýf=<<)|CcF]kYnSC‰dAiOKY;+hzR"Ayncyya[a YMXngcL%/^CCfbeLJU0ѴzBHC))܏?93UV~ïU"Y^'KgD$ L\f?8n4W  _uBJeAVڨ4"Yݏ}]wn_Qg)ɶ&1.~o2FXɡ{ߗy*/ ]E ~,Hu;6:Fpz-% ?z,qvEQ.|gn??z?yϬ[|@zfˑ_?/h3*x𨮺OȤ 5g86IU_ju;(".e3!jֻWCq^=cΫ.u;&. R yn#ϥ'͛ɧ8f32w^v_n\pa¬ߞ+r;_j Ͽgq\ I{0eo:G-Y(0 /l#b˛?`'Þb9I~Aۏ?x9˒䋣mpЯ|`ٍ ~ֽ!2?jjSUfdvtUg;kCf.j1by|g/_܈^ˍO{ƠV4 ,7÷%0M:BtlجtʼnNģFmϴךѹC[ Zy'ҦK>"vgآhPT<^HCSSC)={?S}=2a||գz\7C!N37(urLp?}!+`˨Ѫ 4=i/_V4]l: 漴A+^_xÓ/bmi0QF[wH6lv^*/8lX%s^i`شzEXicwCim~Ťgxo9/q6BH&C] 2߮tr8٫^M/eSMSd;,!%rjڨ1M@db|jz}'*落VPt)2Kdslȫ>;6e̴#o"vgآhPT<^n+7y/&IԯCnet| !Fm6 } !kYLzF%Jns[] ˼CbT5Ν՝7Բh662m]E-\;9Ow߸FJ7X s42jz#MO˗M۫β<9/m Ν="J 6,3w~bn brҊ{)I !6EhsZڼB)5ypU(ژBBI|kܔ9˂{z ;RC=6oKYnmjJ ҤMd8V+&,ظG;ɵ[u&_"]q|HŤo)m*>d^KZ=PM-׫ e6ˈBR'Xi ['gxo9p(MBȽ2A;.o?_\L <@( <@? kS Z:0r%[/{X9|Ol&XQZTp"YP|Ҁ)9{ >wH6lv^*/8lX%s^i`شzEXicwCim~Ťgxo9/q6BH&C] 2߮tr8٫^M/eSMSd;,!%rjڨ1M@db|jz}'*落VPt)2Kdslȫ>;6e̴#oŤ7jTbYPȬ<.ǠQ;>$FZZyN- fyn#^5ctk(TJ|R?G?.G:lNy|Yt,˃@ٓ/b^28xG'5+&',8I)bZ46g/oOM;( Rw < X ) tηM,8~-9{n<_Ѧj+MOfN(~o1 wBwt+fN(x<.fN(ڸ IDAT.Y W_Y q(A 5V~17_74lԵ69$H4`JΞC'].M0 6gɜW6AVPZe1لu;[v6ƶK\RzWrªsz:wiKbلԧiKH\6jnL^_J;)&y67asyٜ5;+O~%M43-[l"3w4g` LZnMΆ%t3Go h{[*U:umO;9.Gr3P|8!D#o{o<"sDTEv]oW}Av£/xxRV62xGy6 w$hjbYJ0ε(:*GSTΗ':+[;knF$ (۝;fKr8o۬4xGy6ƼspR?]_sXwivhfHg!pSd+o*⸔͔@G$l,ft"n(#3X 8m+qnL^J Iڰ[X^sc#4]% KV"R6Bfk}53漺]w?cRp!p62Ϡ䋣mpЯ|۟qKv6iH{C8ZUg{e~FڹÃY]<]}C6y)$&y}G␙Cf.ZLyXGvn#ˍO{ƠV4 ,7@ukEsmd p[3x1O ~Gr+SH1j{YH\cb5*,(dVr{EkFcШXjq|IG<^n/jɱ5BW*%~QӣU]A6'hzҼ_h^uAyiV uQYfa tfVKHJY-EꗷħN;DsBIzBL^[YKі=y[rcЯhSTSI&}Uv'`h" DZZItt4!d=bI+Ep6YҍCb-&}KYnS1g%ܿ%sH]jj^n(YF\0%X"?J-U>9n{ˑΆCWoB]"1-v)}|b P <@(oދ;KOY\qԂՁ=(Fz/}bS6ƚֲܦ/ɂӞLٳvHWE fRa}a,Jæݷ;(JJk,&=n{ˑov6WmZWVedA^;{Ջ/՞92dV2qbD&Ƨ_.,:.?+8~{ayyٜ5;-o3)jfZ:Md/#٨,^+m !8W#oVWS=L8z'DžHUnnL3%1q?pY-œ702j6gzz: &xxRl"3w4g`L '8e69mYJ0ε(:*GSTΗ':+[;knF$ (۝;fKs^cvX!@lgmq!3X-ڼC#;^nO(Ƨ=cP+/BukEsmd p[3x1O ~Gr+SH1j{YH\cb5*,(dVr{EkFcШXjq|IG<^n/jɱ5BW*%~QӣU]A6'hzҼ_h^uAyiV uQYfa 6箘^J$DRBj(ڜV%>56' lvJM!2,cJ26;bZ^>eβŞ^vR~EH"4{?xGl8J ! 6NrVw])xɗbnk1[r9(-CdRVTSjEwC2"!/!$xƢЄ Vڤl/لu;[v6 zr |oK[SP <@POA 5V~17_74lԵ69$H4`JΞC'].M0 6gɜW6AVPZe1لu;[v6ƶK\RzWrªsz:wiKbلԧiKH\6jnL^_J;)&y67asyٜ5;+O~%M43-[l"3w4g` LZnMΆ%t3Go h{[*U:umO;9.Gr3P|8!D#o{o<"sDTEv]oW}Av£/xxRV62xGy6 g4vGC۬FM%Zhi~h*KdMʂ̭5E7M#jf_W3K%asy9zmVMv<c^p˹E, һ4}ݎ-Q;\4^K $׳ryJ8)27Qq\fJ #V6}nZF:7wf`,6ǃGuՕ8xE&U/%$mحh,9sMЄW Aq) !V޵rs^]owI1)vdVWxmugPyы6_8nWxP|[8%;4 ~ֽ!2?jjV,.>!@lgmq!3X-ڼC#;^nƧ=cP+q:V{6x^BBmұIc'^:=V_kFo1hY杞J.Q'o,bBR62xF'R#oQ) $V, $zOf1X2+"ϵK#ou1hT,Q8wVwޤS˂Dfup?}!+`˨Ѫ 4=i/_V4]l: 漴A+:w(جW 0QF E3| K+DRJ$%جYh[Sk }f"9V$kc !&]qS, _%lhKiaۼ-e1lWZ)$bJ*0w4X$::`$nuוw8|,v!,Hߒ9$zI.Uk@5\Vt7,#.rBg, MH`Mʖl}MXngá7 !.{x>|q1wH1?w.hxnA 5V~17_74lԵ69$H4`JΞC'].M0 6gɜW6AVPZe1Zxo9/q6BH&C] 2߮tr8٫^M/eSMSd;,!%rjڨ1M@db|jz}'*落VPt)2Kdslȫ>;6e̴#oᲺA9o`dlvtdOMDfh"3xSc74v+mrn5=`kQtU/Ot4W* 2v4HQ;}]Ť7jTbYPȬ<.ǠQ;>$FZZyN- fyn#^ރO jG-\;9Ow߸FJ7X s42jz#MO˗M۫β<9/m Ν="J 6,3w~b brҊ{)I !6EhsZڼB)5ypU(ژBBIk͎YKі=y[rcЯhSTSI&}Uv'`h" DZZItt4!d=bI+Ep6YҍCb-&}KYnS1g%ܿ%sH]jj^n(YF\0%X"?J-U>9n{ˑΆCWoB]"1-v)}|b P <O<Hy# <@ kS Z:0r%[/{X9|Ol&XQZTp"YP|Ҁ)9{ >wH6lv^*/8lX%s^i`شzEXicwCim~Ťgxo9/q6BH&C] 2߮tr8٫^M/eSMSd;,!%rjx5["S >QTI|'$憢#lN9/_"fg{E^Dޱ)fyMd&2lyWW˭ٰ}c.~ !o_JC{ibB|'DžHUn&׃Ԑ4SSJ/'h䭔@y翰_ch5jݮ/NxOJFfh"3X Fv!vը\ -دr4M|L<~RYiD@jy D>lN9/1VOJݎw4g` Y-#; 0xuNJ[<"H6V4ל9&MW}IhҫՅf̈́YZ_ 9y9${Ob2\+n<tH6\d 3(_-pŒMGRy^V^pQv`FmOWM^J I^Qu8d␙KSm!֑{rӞ1 ˍ8FK`P+=Gtw䄥R")%BlVF,-y>aSjgP1ָ)s/6~w4{xmޖܘ6+T-Ք@1I ;ngGqVMYqwkJ;MDtXIRT|E $o${zZW+l9 y !3&O&eKU}AO&rdP՛{ew |˽] M m@wXBJմQscO-UI1뭴Sd˗^W}/wlʦiGby<c^Udr+m"u6,yvĘ˅9z}fHCbWiЩnxq!f{g)L/MyÞ03/\G[v+|^!,ZLh@ap32Wl-SXy달-D!W_ :{V?޶uxҼ;ߠ¥gsc򋉈2oඊZ-=r9文>OO+˙.@㉜s4.;&=[Iuu5-{<{s`{,ͶUT&hϡro=qTn\Rɋ.rdQ7W7 &i9gsT6g/9rHkzDp"7^-k CP <@PWCLӗ+6OBSbnZ6޹QጶҺ5Y'o{aM3OYx6! ;5$R>dҌNp]|TI؜9Gxy~Um^y&u2j=?jM[Lg7OolxՏWO^^ӰA{RDzm?o:c~m#<~_*,D +^l 8E'"}_*Y|OIכvxr[tGߑ$IDd8kʍwlߨRkf5~oTWb+,g}HL/&]H"{6#DOdF<r"Rb/èG)DAIݘ,?W0*Re֘lѬŨ82FNJY|.JCD!OI:;iV,gMSݱ픘؁8,3Wf %35Љ5ZXμ]Zz4C'Tկhm wN(hv}5#gy#gk%Ɗ,ߜO7U߼닭{F|C~ä%II}[8K8|]<pS;)r暆 [v;W^Q9{v8j/nt#iBjv,ߜ*xK~5VưaQ<ZBDdI>yL9 ;7b!¼5㙝O#[E./>|9BG R^,27=aJg.^:V!B,Y왝z#˙р2gd؈[4,\;~UY5Z(%&CLtԭ~mkywAgKBIg4e7ymu+:C[zn#esh{-]}V3O]r7 ({?|9h\vLzj"Z &{yP?Yԛm.MўC;%zr6?m=[M ]%oo4Lrrߩ$l^r䜣/hmODo |˽Z <o.x&{A˙/[W2m5>}=z{Um"=smukOQJg.vmB"vk:MIJ)l|^Kɤ7/* m6%9{ɑs-47PSLHoeռ{~,𱽯dP1nT8m:!:?Mu?a($yfۂ~߼×uGfy[TY,?Wkٶ1pOD~GTVTӟ[퓮7U'5U]莾#;H>q֔lپQjި4n?V" 9{Y4m}ޑR/-6_Lm6$E|C'lGj-+ ;p%x/OW;D$xryمVu:]f,>g^0VrgC]Ag*i|^QSJ1gY~4xaT0ʬ1JYQpzef- \VXC&uvb9ӬGYά՛.c)1yq~Yf4XJf,Pkء2kz#yCϻ:J%iN_5/:Q"jG2WF|6?wϖ.K;mUY9#\Voy}[cA-"IK4pE!&q5}x8֧6ewdG뇿S5 NvrJ1ɷ|Cq132^1*Uݚ'bWF҄xDb׮~ @DQ&% xlǫ3 vB ΍i/&">˼5n*VhuϷF˙[ <=,gn>P~'rl%DĹ67jW=\PY=wKf˙Ql~z=p J%/ȑKD\Uh* QSI؜9G#_۞‰{xSr]*  <@( <@_ M3M_d<5k9{O}{䋹&km#D<{h{wG3JV5>{噈ב0yXc{_6nc;1kJ $MJ؎W5,{g,_xfW? +;a(>~|O8;YӒ3|sF\,ը[^ZBDOk Eʓ%h<1J+47rf< Txzgv<lJ"7gdڿqw5+8Kazhx \D6)xw~<߲[ 0ggv,gJD wb#nќpU[_dh! 2VS񶝯œ5}.= %Ӑ_LD|ėykUԭ o9=3oϡ5<tyzZY^TxP <x-gl]ɴyjrMsMGUx g֭yRt(ćڔJ%GV2V׈+xy:l[OΞ^yw|5+,D1VW+x-6FNQh_jjSz}梪]~2􄜽梪wdI'Κr-7ԚYfJD!g/˙-c;R*E<% m&owHeE!3|YWˆ+x!w!")%C^.ZB'<7LeEw=8i? :UI2z䭞RJԍ9;m^6Qqˆ(d+f-F镙5B<z&7>vY ߱o˨mYx  {ykϟ1}l,Ţ)s!o3ptEHn+œŞY7) ( .D cp4OҙwpOYvb/F kQJL\}%[x I|#b,tvnLC~1_qVQB3t|6osh{-]}V3O]r7 Q;mm{#DĹ67jW=\PY=wKf˙Ql~z=p J%JDTqU {ZN;KiMNDܯ1wmmMxSr]U&b ޿7c1 ߄CP < M3M_d<5k9{O}k&km#D<{h{wG3J}Fx"++`m(DwKe5O5)>zsQծ_?zB^sQu;$gMΖUjͬJ3y]_l%LӖױ)"|ޒi߶kIR7tr~ֲY,f+a?]!/e-!xГ^tu֛m融?t{4F[IݟYuitѪyF=VO)% JƜOd6\/è8Ka?YcF!+Z"f9* <%M)f{g)L/MyÞ03/\G[v+|^!,ZLh@ap'e+4 ޔk'֯j"kF d՗)ZΞՏ|`-472/vKBIg4e7ymu+:C[znSF8=Pie9%wokE,f+IQ]]MD~d/g3zvI>sh{d6[On*6U.'Fcnh{WL Cy$k<*mna IDATN]mrU8Whν)A˙/[W2m5>}}5iz{Um"=smukOQJg.vmB"vk:MIJ)l|^Kɤ7/* m6%9{ɑ6U?|%{g| *cK|LJH $R ˕'ls xUs ~w$V/Tu\IDYХF"u*ū\ѿBުU3 y~Um^y&u2j=?jM[Lg7OolךiV?~r\=-f{yMQH}KIͶyg/^~$|jsZ N&b߀4Pug .Lϫ+>`_t\Ya kI Tw^ jHjH-G~G޵㥜uF x/K2 -6FNQh_jjSz}梪]~2􄜽梪wdI'Κr-7ԚYfJD!g/˙-c;R*E<% m&owHeE!3|jsZ8P7QUHF- qb$&jQxyylf!,xryEKu:][t,>MD{6Pg-??V}'K3xȣX0ꑷzJ)QPR7x"͖I}7~_*oKWՕąƹ06l߻Q2zyHa"|4=\0?1']OW՗S*t& Iw8wJ!"L`^MȳZȧ ɰ:7ky; Ӈ81Ṿ !]<;i ovb 1Fn3/~yW㟼^$UZ]Sg5>JDb"]H|ވoneI>6xbǾ-"7gdl3 |jbRP~&/o_Jj>kxbD [%!:;E51vReVr1D&yvOER!"F*Yf.ӉQHDDjvD$I48=H`NHew =i[8Oi$n2'UАNŦJV2aÚd˥bTMDC {}CϬ u_W.ćq)`|XHkmw h1H$I8s8FaeVph{Q𸉈b3} 1s«;Ro>s|Xg6TFsWo՛o^Xp3jV7&5WspE!&q5}x8֧6ewdG뇿S5 NvrJ1ɷ|Cq13r.f"JBG?l y6Kd޷pZ4Z;{$RCD$1#%P|xlMOB^H"12Pel8ѽ dNRށ!"6ED܄x#Ԩa?Kb$IlA"刈w45zU>֒LFYUJW qb$ 2^su]fKݘ8G?g䙫C!EPRa \Rխy"pCG4:.pz D$MJB/a]3'cayg6lǯ g9ktu5{ZrFo2ܵmN9g UצD}LJ+Rms7*"+֖ȲLdl[Q?DOֱ&Iz"ݟIDɈZ_Q}%z TB=,Ę!RO_(SZ/m/tR U]Y][u+c0ZK(B`-!"$h<1J+47rfeM"{<}|$`VY9#gsͣ˞aT:oj\9ҧ(U 2sd=W|1w*u}gt\0;Nt m~|]4aY;o>Ȩ%Ce898U"R""0cj>k{^-WS%VxӪP1tҔJO\'ϟ?c^YL^4en< {./S:syebabYΔx",g[x՜j3|R29L'$2_ qީLL*(}w"Ҏ(cZ["`&'{GiJ8%spIJ\>oϡ5<tyzZYwjȻ:`vD寱zdǕy &-£/*Fy3"VX.k$fZ{?9ԽϦ~Y?|}}?oYL|`scTj|޾#;XU8}{ZIDx"޾D,wtESͽ_~r׳FK,nv_'(Ê,44h=xE+E+QĘz e{S UTp_rnM):R2wnCzXG4,Ed5R<#fL4C*Wb8(9؀;īR̡;W) ρ|1UdTsBb/.XSR &ؚwX6VGXx[Ouѻߪ˴Jm.Uvא$RFۆ5BI+9=cuu5F4^E+J#IDR60jZxa>3?Dt3kM!%07 vj8)j_% .czXI}ik1a3Вb}j-Nx<ȄN)5ԜW\T+V1QD([Ȼl8P\ͥ N&i/^.]Tb*] ^g 4Z6޹QጶҺ5Y'o{aM3OYx6! ;5$R>dҌNp]|TI؜mriK-v3퟿=|{&_2<Q&2)ꚆEH: =a4TE1ū{UkV]]Ah$ge(47PSLHoeռ{~,𱽯dP1nT8m:!:?Mu?a($yfۂ~߼×uGfy[TY,7YXvIx<ǔ6}^7p2G.V*ʫU;b=#^gؗz#gdXZsU=%U8seh) <+RYSMJoO\TOƃ\Tuݢ; I""YSneFZ3{Lz`[(e9Ӵu{GJ很'd|1D ܳ($rFx"˷Mx?'*]~Q^o~G輲GᛍQKjh% ~C?NyA'(X-x!wg)%C^.ZBJ\[t,>MD{6Pg-??V}K3x(3*yF=VO)% JƜOd6d΢Q,%oۣԚʺO}/kw(6/=j}^QlStW_O )SRr"Vԫ6Z(Uɰ&QISͥ)1tlo.9jX͕T2qgŨAMq5&.W}i)X%ynC{fZGXxPc 4:wۤ䙭r1" }dQfΝb[u(d rs0N$Ryd2ʴJIc~|O8;YӒ3|sFdHB^C~Fxc wZƟj+Rqi♣Qy$33F !MJPi, }wU` EgZ/m/tR U]W *ѤHm)Od5\YX[BhR# $J1DJ!Խ ٗzzE ˼էc_x(1JQ([4|`)ܷgne {"}DW<.$h *4}zwO-?Iff~mՙ2L&|sFqQc aHЂ~}wbznFbT֝Bҧ(9sjcf,nZ|fWxO#_%X+kKLX,p1.Q6*+2jI|'y\9Ka,")s!o3pƚ #-~H> s{fgrD40LYxț)笥E<;3WxhͼiU3G-t_XM=Aú̗iCy$w ЇNlždD==i|6aAW] Z͒!ʮ\J/WU*ʴ+D NQN #ldt~$*և`[ˆ;~%_sy)UB_ QcD Dq&\^IIIWWWzaµW5o}5ZXRb2-AgOvnOx|$= %<"2e7ymu+:C[zn#esh{-]}V3O]r7 ߫$|Y*?*&ES,97*B٬eeKjnٝ=J3و((\ss%:_ZFѾh1gfy!FFͫK)&V':Ey3"Vs IDATX.w92 itC*I$dD M1oep!s|My;=)sҹӞl|ʏy3ڼ6/]=ԇ; nAZ]0M ;RJ:7YV7̬ u%)u_֮zx$mv_%x?b}08~(bC>oߑRJo~Q=-$N.DisJoӖ3Tiuaw_kKB~GE͐(\sĘz e{S UTp hU5pqpۙWz"=ΞyUfR6 *kH)f{|mÚ|!faIeTkD^4rMbs~\GRRuOeod{䫍F'C:eꄗRk01ƕudTkuv^?j›j}JCZ1޹^_yيoq)+%B~#;~qbڐJ4ּq9ĤvX}*$<^vس;h[_,X_}jF$'*9mqaՌgZϙ?F"5 \&vZ}z"RL|̯d)I|،$o̢ɻٮH <#,JUiT:<5&ͩ/+_`noS36G^]֩bT\ͻX;M{,2OaEz xAI*>,7S~f7q n$QU*f cO#yzGTl]O@OM]ɰkc%Ų`'hX5ZQi96#Q`5:V?᪗7 xZ4}ٺiԬ!=/֛ثjHh+[x֏wqTgfwJZUK\$ &`LH @H/$@Mp&SBc{bumަXiٕecǏvϞ33;;ֱgܨ2f=ջ_>ZezHt9e6Jؔ,UK*&Uζ}tNXTg%~UDmiNx9fS(%GP¹xWCBfY9#xN%6nO[NjD: OQT4i2s]mO8/k@ ]YGlW>?쒗37irh$YVn>hk*^/ZxSoT~/&.d-|/n[ L4ߩn蹰#&//w`JY:u W~xJc-[rjKԮ\c߶꣍2|=vNNu9fr|cL8S>g|!WtJN'j_!n;vs^\w Hh-'* QwBPrclab*`uմgrQCWc瞽A;o@IrnȼF:~ KNNNz͞s/|}=e þ"H|>?0zc2ݱvRi`HU=sp! O=&(Zӧ?3\֧B^;Dc]'sf>ߙGX vGooX Jfn>̅gtjS$%;MHooҹWS/*Ţgw 5KYx{Sx}2s=j.[>?MNވt~Ն2A S@ U+UHVVU U h3ܞg"B5m5mZdV=nl~{tjl&,n[:t5XۯHH00!3wWVl7,QYQ  ^ΖEnZM-~Gkt?^2sڔprS?c"q/42f%~tKdJK"v S9&ݘ)Y<&cbL$د]ֈkV{>k佶%ؠ2w5軎uoeD- ϐ3R3)%'p\dPc[ۮ <3jf_Um2KԲ-ˊ0^}gFMޛ$jWU6꒙b/G@)S}eZ-*uǏA4F{:\t# eAxT27j51F6Z{PV.heZ3*qGiz{iF*W}swt)9@Ir/PGK2揥ZKI%-Zwʳ#ת)rvpΝf`u7hs,c4;㩶, ODRtAs}VHuTJd.v&ճyVfKS. +319kڋӔ6}m2JK18 :4wFmZEp}ukyK=2]Hfb:.uk1Qy@3N6g?YsɞGE)Z;S=]Goewө B@ނix6ac~+ ~-8d4wKF_;[MeW[[j`3< >iC{ yZS}4D-[o;r5gndd9&X\bW*յ5HeSS3O{N |rI!=F#@QrMMm 5=yфrظKV"vi@ ;t,Y oT_Q! ?=& Huǎ$t.Z}%>6Ue}uݱcΡ·sMM@i`,#?vl[oo| xs* sֆ{sv6'jɫ9]Z;oZ2[a/\3ew?*()j=%,A-$u 5z:%;L|#.Z?9ꎟ>X0yY[og:r{Uc>`W׹h܁Sn,ޔ%g*w+ wTLcs,say{I:O2v`+BFeeĢTEpw-*ZBg-sHOI"p$Ԣ0p}a SC0~Ju!]#H{ZsK!rִ&Mi89eu,{B+E(Y5Dn9}#)S (Á+b7e{[Q 萑4̑f^h{/3%ԨԌt&BN Ý8AMuBfd@"KNͬFT(ti B9SXv=D\9DdžF3~H :xv,]6{EKW[fyS|5p@M7G2iԩ1st@pD]&\s i[[YQYC}'=Z.WzK n{ ];~OpZ*w[$M^N}U=]Z)[rWOmgO\?q1F~R%ޔ%@ԧQW~jF}}n2k+l޷C]&2vɷiMwU/6tD] .h|/ձlJR8,k]ex .|+^AhLG</T\xbC5ժښp^~`XyQoxX z/h|pAyG䄯}?u龇 Jdױ2ѠD&־&cNnpzZ GϿ^1<>p Mc1;s~?lQNĩTbݞX`nma`r4-PԀw@3ڻǼ= 0#?5oˆ)ΆJ9O뾏b)L cg;Οh9i2BgNX*W u,Q)Yݒ$jhf T,N7C]VƢAU> #r&@`wN<;FP TF@ UM?6N}9cs9p^"C䵸ԩdXRRt_HI!J%d%)Pn(eGn)?o3Ȋٺ-Jr @]|1~R//|ڀZ @8!3-? stDzOM>n.fȰR6xtJݮ`qqXyOdow)+>7䭭1$>_ɤ=xSBa%Ռ_nB/}XU8y6*ϱ~GsBn'/* #,?K iB =1Lo+2o"LY#[_xt[oͮ*Z9hvfo:%w8%ԙ{WXxr)S6ԧ8+)M:v<+lgSih>/&y{(g$"LvL}I g[2AŅ yV{RkեȉxNjcn m`u|nj[hKՔ(yBI`\;:RsAq60q/Gn[ɻͯUiQ̮ڱjYa<-ۍ留K:23ȸ*hnF R- l/|;M)8m/Q*.XOuuݱaО8d_v1Nr͙:"5cV$'G[yRf _8Y[zVYcYʓ-ѣէ$w $o SƍgzMx-^" (…]f9Q/OӜRI}BFAZ-P`"`P=Dk%nv N"~NG%x9es(lM?M|h_|#o|3n/6NsTZG|!WFgYGS"IDATQ vOx`QqLx'p ;Es.sˤ?%uTn> =()6N+Tc~V>gwm&FEt0vy+wm;#T=Dj+ )39keZUVKsYzJg-$m2 V&v=X<5*017uD{M .0ȉ2bly2st}FY!p?i~)7{eDĜݔoQ&4{2#2~cRGH5N:ym۱%4mE)XWM47/nO>isܔ65!<5c׳ y5o^kXLU}y S5*99/Sp@?Cyʾt9ݦJx`Tt^9{6:oXtvuMu׌f9:; oX +oL r.\\'Ir4ݼ {$57Hs+v J ;Zۍw[[b/f6(u7YcCn?>=ox`CcmXAM&?ݽ;eѨ}2NV47Y.i6}s@*l}7v:iK*yUϔ) xD ?b>Kfdɼt2r42?9k4^xUUav)Y)AЎcƌA_ W;[U͇B- r!38R=$  ,ϣC$^ahY&ːU!f%ӌ QU#a 2 h~Z>ti^1 W\ <:psBW! !'N*;Is|:hۻ١&zA"Q65΋HF#zr"x ^t^L! rS^{PH[U>A ? rHgAA'AɆI_ݶh¢ǧzWȷ߸{\>ܤ   >R%ݷIKz^[5Ye0œ8kIkE|(+4|k0pբOAA.F mgW?9)Ӡx/}sRݓ/"{nuJstQ$V5%@}ok S//UfQ%gs˾rFޚ#:YSgϞm۽hbE9f?yG5/z˼0L*;V͝?[ՙKy4$uM~+ Օ2)3_p r ֫CsۜnAA@zvחzI]C'Tn\0nO}hd^Pڌ=muN"oWv~?=ljH"ivyoZ}U`Q˽.GlGjz5)9 'd%f߸8ۤx0oVr"+7Ο_;Y"ݔ R+q_]YoFe}y/q4|Έsݫv!(jtFAAwM2|G<%?_Qb?fV틊>8,884Ҵ;4JKFʽ;v;gpת;yRB1_w=myVgM!d6Y5GkR ypdjzt݂ 3m<8sG$I2$6o{g(xG5kx3* T?u㟛J&PrORP]a9Y1l_|gf<`ERo[Xtײ0?oV&]L`y㬝WH8R|j˹`R7zAVizr0(OA>} E~t5`?d,'c_@2cMosBN.fz2T+ɟ7h}լYexlx{8[Ox = э  xj~ZB@UVHc+ԶjڼdqZήttm APmR`XJHOj:ۤ F)w>kH^Q |p8cOf)Пnv4(tG`חv[S'Ab~tp&<]'ӷ;X^^Q ַ?ʉt+ JӓO˒?n9;xGtMVu7*u+ggUJDBqhF[< u$6ޏr0,MyMgnAA{_"/V~AݝwJi~/`Q$wBCZ.@($I]zMƉD:7(QSg2.ib?ozr9rz#뗏m%~,G*!Oyqk.ubmlb҄XJ`yqg;z55e{BJox=77Eo0{s]1yoJSP2C(S.hNQ#kMzA|^M3kZm>nAAxx>G잷iU1'N̛7l:ަ/-iV@?Z]iPɃ\py׏ID0336l^׷O?qɔOu>~9Ej8tEyF~9&-/yv[;{:AA4ڶE,֔ Mq-Hwfur1+W*Y4(p"!__ZQb#F<*&ba87V;A{ՎG[ zOBсG,բW$$zT2o3i#stQꙦ{C)x 9ӝEٚ~` ezohaI]C堶;:Ow # /Ե_>{G>{=?z|_Bɧ<AA\H{㺻5y?\'Hy}w}ʼ67+rʗ~5?avw9p+AdWqO$pUg=n>8g5)n]ԖꄴmaY}Ҩ52`X'jjg3;*P?X]X["&;:Tr+Կyc?%:02GAIJKGx볆H1r$_=ݔy-p]< ۔ ) ?+ JKJƏ0m T+V5zT{C3*Oys:61}f/~oL<'~t# K69fU0œw?¸߶4W'J:4q VOG=koz ̼?^\aYo?cZ?%p=ߜj'/Ȥʰ܋;^,5 x䮥%D-i?ϫ:ұ2}j}RZfD^YgTqo9]^UJO^&j5k8Z*;^W>k֍HLwwe'UwOm?ߜd58XޚV;~IGޮ;*T;c<}{_L G]'ڟ AA !3F=7pu%w,)!`5(_A|"x   |iyp< JdRjR&Q%a   "_CAAAE{$    _P!  |Iݢ@AA<AAAx    <AAA   AAA   AAAA   CAAA   CAAAP!   (AAAx   (AAAx    <AAA    <AAA   ATEIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1688731561.0 toot-0.41.1/docs/images/tui_list.png0000644000175000017500000060027714451777651017554 0ustar00ihabunekihabunekPNG  IHDR1$zTXtRaw profile type exifxڭW\e1<Á{mT(=O2Ǥ&떻˥jz˖-~gKnnO|_h뮭}{xtf\(og|=;7 KœBu$K;dQ,)} Ds(b}=^q@E+ ?~~cݽ{>w7r%R}oGO|pO4* ?չE6ٜZ.XDv^aq9;{-_Fz#;1-x9k|+tμ_^o~<н*LR> i.Cӟ|iy}sn~$6ܹ?j+<'>W|v?z.\LHdאJ[ıǔ$&JrzԹN ﳱ@ (Fjh s~Z(dWJ^jڪ0jr+z6z깗^{[-aŪ5gःC=34,69*5viv߶ R:SO;Z[nkw7Z-s'N*n/bBUDE:3 bN%]j:&]ަu~:lf8eqS7up0=d(H>lhUr(]rS@}Sii -"8,pi˻ eyx6s Y$BЌWʸQDX(.-s3h,t7B'(CrʃOֹvnWA!bgJcFy u_TalK5B_3'' PE¢jCx aIXxF:ꛔ8*Bf`|QEj.^ wQ)VP[5H-Rk"J_AECVN_Bq9l9]iAԩhCWX3*ck!zzSّR(7X3x&H &"hAz 7WQGnÝ)z15I!2md+[]؄>hDwU484ԍP򭗭\ٍ1*`7RцC3"눐6QDA='(PqR'{Qwt.:`Z#hL2de.m(SSk04b|C!e#l"5}RUAݠktCsl"B&dJ%xxh7Hlly&HԂ'Dp% e943b()!jj-cx,xX wTAkjWT+iJrB? VUNCVn<>Pv1 ?ěDBvJ #Ԝ_2(=7In(Pn#W"q#10ˢ=_Ui=1ꊨRt9'^ ?%b&=aOH]ț T9>C2'aCSPǭuّu N^hŇa3 x¡ 6Z$t&R8V! 捾?℃zTD G+Ƞf0q7˂! E:Xa^/*I3`5Hq|➞ +^{`=;puZh"`M HDxRHqvB_!@!Mysd7W3> ORTr&"*p>hȅF4yENC(v(DG*-쌝 bN`hކλvH!XQobӈmGƀf*՞&ʡQv|^]T ҉hB.ҎlWaFH䦐!*rX~ae!OSȨ(F"~j4/҄Oƃ4QEr)l7[6fZnj!P?\qGEC  O*" WӋ)]Z^g[HT2z[5qcjP'E'!>Bۣ U ji u6MR^8+?G]rgNȁ -s#Nyӯ#5bzU9W\3XKP0KМ!{Es n\* [U)Έ->рM=!\n8HZl<' 5N!g͟9PhoCP}1imNpjOq$t%7G<㜐]|}@TB3HI&9b)Í=}9MΈ;7OVI^1ި;M RBm@଄ 4pA5[र/4.5E `akE25%l`Tgi|hW~ ڋ'B0Wf{+J5]Zp* ( $X~7wbV(Jrb{o"<}!=\13Bx&4+̽e1PTDJDArY/ x!~R(DfO*dMvg Q5KܣզQ1% zpsʃ(o ַGHymV0D]#§ڰsVtաnț0K*p@ӰyѤ]tӻUl%@[8``@h‘#  ?dm<|jiDJU7ȫcBe磸gIIQ""D7 vǎ9|u5U-$~xX `Xu @4ܨDBi@$]M$#6?H>_l3 `6zd,FFG*(݅.4BhlIߛx|dƓ^g`'YmQC<'O*h@62Mڀxilp(i8ኒ1ȸNw4ވmJIsyKQlKfp!9.xJyUʧqX+/ A]T Ǣ<ڎ/Z6]9M CZSrekTe:Pk5rr}g%UZa]k%jJdm"&5~;Q3' ѐCաN(ꖺ)^{YJNr:>QQ[/f*P|36)+, B1pOH_{@H'$tIR(Kɐu,f-XPLZ8 a)"ȳ S]Z&2GQN.`ZW0V J0i| `VZD\Nu67G>Ӓ''_ ׆WAOR᭨E7 ܀i5D6j(k>.@ڇP9YOʪ '+kk0-i{В6ZSZFhK$8j&.$2}.gH> X*4 |K'xM/ޥXy;ET':(NU,$ìH.⃈[ㄙFrY'LB şm *ڣl6 5Q#CGR:Dlۥ-'eP!-{9Z PZS o?lҸFU t,u}HNjd'ǬQz܆4AigC&98jXoUI{v 44VKcsYsluU*VH^z/ԡ&ʚl2d_,1Ӥ([fZ5xkLů*I$h!m8,jT L RkoTj6@B8%Ѫ ;}?薴xCs7PQ Z'YD`mW4d!3B v5|3`Y عq !}phNۘDy7VؒYU\{ЙtD p?FqI,a<5Z6/u\idZk!fhK@,_C!g G|䳤YcW qG4@LU!b cOo[B5-iޛLKs(B4|u /o -ZKAC ?1? 'm -Uo;(-`U͒SEB'!-&JӐ5Vky7kH_ry!|Zc#]%mYJo /5$NX2y3~q5()La莥v2pTWӇy$,{"-""n "O*GtS`hRܽԈ^h,iNGLf9~SFm=(#1#QFFc6hU$mY`nlɲ1?ܔhG[WP]Hbh\W=i|#vєR[ZpCU.Ďvl|v(vDMyw!Q>`߃k9J=#v!fu{lYmu*Z#<{QFNCg-kPLtʹbq)ݟ'TǞ5*6MڪTkBkR̐@j/W1e0i4>ZXS DڠxX%WǘZ#5:cdD!L_ڀAdݱ*(m3  ;狵 U+%C=`_󭌏`څa$ cda:F1s0*8{>8 BbHrgE5ͼ&3;ܵo!DV- 炱{}Q+h wDUhՎ=*B{5Ls,̠Gb}#p, e"`:&g5j.K+7 h'{9mLCE82M_QQvK\eC,A4:(avhDVC7=FJ`7-noVv!䋤d][A^h!ͪU);UY; ?O>;sZB!1Vxcbh Kz?wh{1 j)Q  )$14GJ#N`Gq!%EWI^; (6\6)gK\ p p\8}`>>%Z4q/j>u]Vc5Au7HdBX$)6h)2,b0T5y=nMpPrIwo( ex~Jlsi*G A}bA}q_  mo )OVRma3yhZSFUg[ғ&!TvH ]ώY<|P Jƛ|F?駟mϽްym&e7"h71@ִf,n@AF54˅N$:JI+Q[ng+{M PBڱ4)ؖ <s!"|C{77@HAz#q(ֵH`#0N^4'ȉ٣X2\9Zgf4N4= ls6>=xokɝ}e\c+QkVLAEw*8ehi09"AIR*أ&rM *[Vp5z [Dh3`jAS-u!+4t !-vjns B@T Yhx%lUp&p8"h8C67="T,.ZLWM/Wmn@!U縴"om6tg Tq"RXEࠢ) }tQzP{K-0%H@aIԏ֌apxV-a"qf(=f-/%i .@8CKGp\䐴8@HKq1$b\/B%cZ̠5d$7M۴&]o?Q"GIWBc֍ozqq fs%hlE:CVR|{,չmsj ;0Ǣ z .]! zRmhcADpL恳Crf85wԃxTJ~ YpKL-bhuf?@5kT]0"ڮ;'5/Mm_(U9Fi?/+2L s1.P).|"KO5cAVBvtf=5mDk䈴bJ,H8>b,'PS;˜>Xn\K瞯IP^ρ2+fTΔZ< ZjFua(NB(͡XZhR6w`/Q"qioحMY J6R뉳sӂQ~0U , j+ޑ^3G6\uQP"j2 ׿w*-}/nK_sҗܿ<m=R96 pHYs.#.#x?vtIME 6k IDATxw|uSvn6=z |wީ<~;OlX.EBMf{cæB|;3ϻ|>|>L@ @ ~(@ O+zS(Q?8ECzlϲj/{jشIȵrIt |C/:Y! Wpse&@ ĩK1_?${^yw_Iڧ7 dz 8JOԋp۟yۤ_A'թ:IxKQ#@ H_&_͈8GNټy˿(?@ įBIlmd|bcbKױ-:Tj?}>cn=eͅ%smVL(t] 34T?OQv vX-Vk ŷ_8)/yd}묑uk{~۪fY`]蕷/,;*JΗ}{UzaNۻIWd܂t=M7U',>uхiPmX2=Ɂ5?6%dohHI1[˝˿>uR"-gZuMjO(d&ͤ<2%DZ7>+͠jǾٰzQT@ qҎ#~Jm@C{8qL^>1|@m 2{ܯ\hr*. 7vE{ɐ[r9S & 'Г)Hn]5"lOZxfKJT>?z:ݤsQIgR%Cgt%Ti<ՎƄuI@ 'iώI%Ǯ =F?$~yEPZyy9٤S,5/?%Q_i7 klqy&җ;U4g]hz ._營yjoj~I7Ls{٣{_O25U0ӵcn:p%7uWGgp\:=m~/~hE)e7ifě' |&Wzƅ/H4+. >^_!@ }vftj|`Es`p)> O~ q] 8cpL3&ɹFExˋq\6EibRtjwT)Pw|U'^2nߚ8jQ/qŚ4TVN(I{ 1qh:7)9F( 3 qSkWMedO=/+Iy#;*Ie۩:~@ @3`Cr#<{,݂g>ڿ눋f'o'/X^4hS0Cu9OzNb c ɱ0j$ϮkFYj k]<%ܱY7_) <^$t ٛ*+Iy#kQb+@ xl.BǦ5W- t|g,e}_޶5)Hs:_BQ9e=tHׯlT_=w/hhxj$SlcKiq86qlZ/DVM5\.NwKޟPuKƚ %Œ0N4x ΁5 N +sL6,RUsjum,C`UeSapf>|(MaL*GA6Q@ 赴w]z^'y+]ds^TBQ5IܿaXYnV4 L)]4V{  ɶimU)]mTbs*Oҿ-T`\E~o?>~Ŷ!y+f swJzݘWe5t8Ǩ(.i)cW+|7g(8B:kӆ9NKHh +F 3O~klNΦ'f8Vcf^a:˦9c2ii#{xJSeأ|KگJLZEI%6#@ Hn$(=.*)5bԶMhZnUqe5WWR+_sQ<035wD*MZDZ]io{dZ9C|_?N"wukUk+|[ʪ=\yG3 \ j$T5P}#Q7w?Ǥw5?R['G 7d|a9~Wd5nx^-G o1'pP}`b0{O2fZDCW\67i۪{ K/ [[grT5ۼݞ (+nAѕT'zWsb,N%Wpu/(RSzߎ.;@ H &IRIIn_pG_Ϸ5 K/puD%E<.oG @ g%$JDbڶЕOlq,ƒv@ I[ V2 322BP8hP8A˨ YEvv/F{]-? oVӉ@ vįjc}2:\!U̇y'>mߥ| 0 (krr0 {G$]u+rb%̩s2U5_F ,v xq Wl]*y؂yX~Ϛ_Kkym􁫟YcW搩.Z2X:rpJvg-3erL4je)P늦](PkV9+~D]d*2&ɋAdhڽ'Jglσ\6Kx#Yæ]tDtoŻIțԽq-tʵ IId/sE}#IN%2 R6(ařDQ$T(l6Q\?8]@1o{YCVl8:`ޞƂP{ޞW8yQþ I`^05q6YfWjԺWmTuX@"W]|/MZ|}Y6vM̑MXgڨOt:nQmi|u6/H$?"|#1caH?VnJ2E5%Ib83(bDhm~$,p@d@ gs\O!wcb$ RJ)ĿK '/ҥeMݿy=\,=|6GyRQjͱ'i'cÈ$%QRQ_Iِ0|͕{V=zYQ\,r&w>ǂvB-6;Wd-* m[Gcj7Y$Et 9FTJͷd mP-_f5ݬ}Fdoz[Cii ڔ yz`7 :^cʔ5 L}{{1qk05iS|4V䰘Sea\YyJcU9b"*-Kdc& ϟ n=eb9i§$oXT Gnly[]]yQLugy ɤ}CCۦ{ B dNJnjTi7ua Mo)A 6.W!$PC*2In{K/$I4Ҫmz(T8>޾, yq,C(^8 ,9t6-x(caLʿnS:F0BʾEd/)gsuD fvd2/ltLY7I2&cegVZY>BYI(Eeݔ2\Ҷz>-ɼu-RX]A_OZ&[L"|g\wZ$d.j[UfΫhy7[&5vlt Q/5H51efhWңHb,Nnܹ K6g#;Cfx|Z8n-zkdNlE3.hׯĕ F0PZ9k(L%J¤}-SZs.oso=~\bݮ B։ !Ф*ŭZks(b gݔqt@dol aЦ)1ߕV+؃tq 4"IG4An$&I$B=#7(! XU_F̩5t$@`Jc1\ Գ^W>I]A7v#E|b9|L"ND"F#Eb I݀abYb8򫽐mfvWISaRMs  x@8ϕ2ƜJQ 6-͟fRs<ί1K HM?2iʓX:WOlS}Qq`k-8sIv&RD@iƩr-*I4v#Md ~ ӄj+}m4bq(b+DPgĢ jjZWH8~҈$K'8`!m vԗ̣B #+E%>|z[|^/3+L=^<Ů[DtÁ1K6JJ?e/=e̜F*)Q)ˌ0XVA$1EA`c̮y,$KO*<!h*\SiWr~nPCDo#2+QW@tA\bR/1RIQ.Qʾ~HJ {ϼ"Z#p[|<Ny s۷ttߺjuIk7}dv^H ^1ًg^q&K;.DNYS,i8o'B#2; @wP RcVz*P b!WAXNO:!E8JI%>s*1<6`m[cjDZz 4QviT0$d6ڿ'V\Ov|@ 62-v̝9MP`By8;*PDL70 Z h*z+>Bn7+xT+x~0SVVaBfc&m߾ ߾g7@ vCݠ1ڶhao5o(>05<器.-+w߽uWdm]#ۇs%$!dj7}Di)9U403IWHIcUv2rj+6q*[w@<7O-Ƅt N9$A4i^ SFH]UePB:l7{) qA|gY)J6}+D ByD#d*m3f/L: Qat>/HMA Z}ԚRvx+&Mnv)xPob ԫbځ֯S/>KWx}anQ.eg|D?ٍ@ ĉ]+vWq0vh.aOs/LaXfɔsUk,ߔb!yV;RZFS6)o_|]J/s>ʪ i7!̙O뙫P #RZ)U9KM*bB8u AfF` ׅ8)%^:qSJ; $Wt 0DO8/iD0]@W5KE>GSSXkzp2џbyՎXƅSUz,k7TFM cڕϖɾbضVل[aLQVL7x-ɊM\g$'pnV,֩>>B85gQ "ڂH@7%߾#g7@ MLZ?v~W/QZ#5(!WCBbF^p h-8݄lS:0_r'^3rBᳺݫSvzAWv) 6ږJgl$ Ygd 5tZS+Ȕ Ŕe jw}%`tiXe( K"Ƈ KK {.pHgȺaL#|1OxlO\$A˻mT QLR#& _sE8"<`P3i}]{kU5܄F` |YuV_f`$eÃt2\C + Fw/5 ` kfȷ|}dzÖzOhPm҈<Ȳ`Nzn ., BiO`bT>W4PnI Sx.\3R&ۋ7V8y$߾F Ir%wwj%Iܽaot#<ܶˀB%?\ث0 rȳOnþ(]Pmz1/ xj1kq$@谾쌋cTn:K(nQvJ;JpS筇3˓I`&T!BZssPV?88zvh^aɾUdkkZm$o_2ٯl]yMs7b F#4ҤA( ((I@-[o O UxRۺڞ{.m/ڠoV8y$߾F %I*))A@IIxLieshb= G7/Z,\x)֓?ч [LVM֖0Ӫp{~pI\l7vφg]D8 Q N37U<00Kֲpu>ϨǣuCgdf7 c&ȸ=Pn6@h)F޳/"@ i@u2E!hcEn劷M J 'pc:o0)mWMJA?(JV/8g_D@@ g&1:P]q/aX =i`M\؎S#iF/"@ i@ _M?u!J|~6@ gh@ @ i@ @ $$}\>׃R@E&ctZ|rwiШipKL(Y;ZW< G8҄+|>8*y؂yX~Ϛ_Kkym􁫟YcW搩.Z2X:rpJvg-3erL4j{m:6ϔ~*` ee-c(D6̜_y1l]_I8^:5tL.ȸpI_ Rep͓i"wF'ziwϿOS?:/W-/*yc]ro>b>n9:K n[8{E3b#/\uzky [me*U-|lÁ)a nuW>J>לzCQ9:oyl:ָ7a21e-udAtH$*we$As!Ysv>OCtRon~΅v7zyūKnkq'@ g+_<h8-s|9kԊZ6Xj '/jط$ \Rf9N8+w BZW4Jb{vE9=cK((!w7{fdsg֟!QM8覎EF_ݜ6\gC=qB$fS'hE V}E0}ӽMuΡX]pn6P@ /IڅܝdK y)b\եҲ&p߼.>xZ\G[ (X(Sv1>IR.4ޟgc5WYl5fsŅU.,m>6ŧLcE9]F=Q~_|y؝ /iwmIVCG9ܿhm_oQl9 Pls*u]]վ[Zo Vkj'`7 3Jd:2Z:eͦARTX:FGsݖ~S'Fm^Djy #6Af<&0!jJ"d.tbFiJ?-G(ko2|Cu.q}YÛVf8["s:oC$KGŸS[uyW_Z PTs/j^uRڍ9tڍ:BM4>um9粚j/\Vse5_vR\}g[+wj;g.ň(s9;?x?si<+OgIG]j=^G ȥa⚬ mZXR*7ϓYJWV#fLҥ|& ~2Ά?]zAgBQeiw?51efhWeգHb\Knܹ K6g#;Cfx,C}-zkZkK9슧rtSW/1_ G0 ZK<):ܱg D#Jp׃M|M30s.vQ`H8vYn9WopPy9Ӵc#`b3}0*%?e'/ x #1Ge@h Eغ32wkϑAg?wny{"_})GmݴRTf`ma&fbl/w# CiPJf/1J5iLC\U?Sm+XҸc+bΩ&^?TRjy5oj9#wԜQs9ݔk>iI}NƩ ,ͫ/mTv^rc#92X[a?ݢ#DV1\eIVи"D y4ܛDsơ"@ Bty.(@\pV0BRjB-P&>1d%dI@QyUJu]\$c8P6lI"Ba18^"qs2.Uc JLC3w 1i_iCTqE{h H\tE,NK#?J}h,`@*W)&\1FS-f6`39jzWi$ #^8N:b=jo{vIXs7g}@O]~{Baq#/*-(J5pm|.J pL$}o:yEF:]pv̻N߾#UUS+OZ|I}>r}i#?Psϭ ӷMԿm4 Y{ox1*:wK"С /ğ)qň"p!RpȪLeeI#y}7$3X_c8(j Rꢋ^Hs^|]8@XsƝB6v `X4,Q{Roo>D֝HT|#2Cۿ.źK#^z {n*>3E'IS+^V܂"wh=Rɸ<=}|mOnb`nSMo;["o?t mh}jx[S3Xȣd掞XhTݴܡC[lg"KNI*5Ǜg)6:?]Pwe׽MDξ׉RbTN'08N !@0nevu+Beg)#u%%e7= e(6if퇟bm5BEʜLr0^< :@)paݦКHTmDpYrc}5m ĘVy1T(U\ݽgᵵ 5HY~etZ׏Gu:A.[l:Nڅli쩺Lq #*@ #g^iG?~Mw~5GmϱaOso.!S'ߣ}R&kj5qB:6uL?ϔZ?FUͧ@{+}Ņܬ|w{ᬗ Wiv 4A;tNJ &`+8[8 ΦA1.z(SXEJicF.~f75d^1P s̩Te 410]~|q廕iM?bA3M;g-s"6bZAv#5uBc^͈?3״c2G{= Wm\_|H_@4;RR^)z:W׳뷄*SW>LQJ m+3DÁ==[+-f/Ͷn,.nݬu{.ڴ<)?=і,@ yS`˾z)D!^7oxGcN0n &foWckHjLD|w xQ9 vN}ڇݺ=k{};@]D͇R|?Ygd 5tZS+,76rH/غ3ğfc8)>uV_f`$eÃtPK |a+E)M$6syuHӨP|f&@kU5܄F` |ٔ?b֢63l3Mb) %aI0z)}IXaތ^aI#; /5 ` ~7p,NAO8_-YN< kKs'Ikx$eer2o7wJ5_2gj]G*43|j&7o*w-0n%DQ{3|᯿ҒCJRGK-/=PX7RnSRiS >B#>2xp9'A`p1 zu˅3nJig*`< Gz!BZ㤸@2> 4Ѽ2:Ó}Y#Z4[N%@M{Rv[%5C_oݱ'X>/ tO}wWN$*~>WzNۻ?ݿŪQn۞43n ŗ?yMLtI_ ^~ssefg*jƻppy펉+o'㤧>evBآ$c^}n%f2.ns|@dgr8sm?{MwHMW0]׿qt] w{OoвKL}SehtZs ӱp ߸@ ނITRR8IIxLieshb}5 MV28]jpk[[nظz-ϛ+gAbO!"$aAリVa<*W⳷W}H@ qX!qS5sz"γf~E/=mߥ?\!U̇y? I;ZW< G8҄+|>8*y؂yX~Ϛ_Kkym􁫟YcW搩.Z2X:rpJRKf5: ̔9Hb݇73TMo?L(Tyե?y:jp=3[X:8)!M.6Ҏ1AM^"Gmf{x{2q@DAJpk_aA ˖vyc1.7x;F0|ܒ?r 흇u1tCpl f\Np1^4~\o::c+L?y*e5m_ IDAT854lM.}gSY>9NW/+yUlc&WMYk%9W.\IڜӘ3 X{#f>LRF4&,>H},^FnHI+@r]@1o{YCVl8:`ޞƂP{ޞW8yQþ I`^05q6YfWjԺWmTuX@"W]|/MZ|sX;ONVN| P]*`Dhm~$,p@sk>qMWGW7M9 @ ⧐v!wc$ Rʕ)ĿK '/ҥeMݿy=\,=|6G׹RQjͱ'$cÈ$%QR3ϩ`t ==vM86o ZuFkBa&ގJcU9b"*-\GCۦ{ B dCV\֪Ρ@Θm5x$C*1.@ͪ]vt29,Tzwf5}Fdozd-n1Y³d!Tb˗xAB#ՙ1B%aҿM%ޤfxf.֦LHTʚF CC>DL#QR1aAR'!ҿkL } AncwI@ ˖v]0\m5ܚآdFmQ#%3!WXd6hRw8 ƐL^x n>IMj*Q R|NKJc&HsЮcUȄgx UjaU#TBRۑv}YݤjZAhnZWw QV6OtZasҭޖ8 ZVoxzBr,pyY@m'}Em=F:+4y1@:!Hiu[ie<f%HTvs{ 㢔6VWA숺\ 4*/$g@ J9O$HJx|Z?J}v|c*6ڎ;րƔ9h@`]U"8ys'&,}0{ [YvM jċF+㮚#/\EoTPɗj/kg~ Wh-J˸(ЭJ>-ͣC%]mPSI7FeӌxY&I$B'znJ\<`RrTRࠞ*HFJY[õ4}Q2/5p!(# 3H"1"xX'2@L#eG1RWҝv+qt@dol aЦ)1ߕbY$h,=vKNNd2(I+XX`{fUu_7 "g@ G)Tjv}E`RiMt)-=W36b=-7ϼ"$8߰obRλ|S䎚+ \IMs2N$VF}mAgl2afo^=%i7܈p1 h\ *GnWK yGQ%4w(XPv^^"*ޯzZ ԟ" HQ^w7wgwذYn3ٙsr̜wN{=DXlei,bqQ.Y/=D{]*[o"yK.=Kc>`B,CqQevKp1 8G[3~>oˢI dx'᷈_<J:@ * 0xĤ"A(G ΏXQ2ToJ ;`τ_O/_+՞4<sy0bFԙ~Y.ݷ6xԈd~S7 Y“aꀉ%g.q噞:!gmXB9RBк^PM_U(2$Q*E3ȩ2mHLaHX H9k8O sȍ"B݊&m,[Ag6Qh@ Ҷ==n:кw \t[Z#6 ȴFX$^YK6xJ=\d|6~nU}S '.O49QgX=`i=] J@(~8ԓ ĭ!.a\,V ~'Fpn<(8 $yʕ)_x}˂q91Rۖ]딂 +7F D\ctw!"q\F3@ Dz͐8wMKEҐ*Ua5粺! Òy+j wE_.EjCbD.*hMy:3L=g,*x0mi!mڐ)uh?UQB yGi3$$BwY8ojY(N*tK&47m3OdXZrVL.HҖhpxY <2$!yF"E$FH{f6aDoQү,o\?u@ hZ?g35ErH]P4U_˴ CMh @I_.sԁbY 6mk_n--v+DRe17κoo9ƤN7gYqhKј%=pWJ*j{2 ~+Ag0dMY:.VαLZδ^ne(0G&g~{;O#[ERy Er5鴔XǦ69XOH 24UoTݵjDE~`Hn]a\sʼ6N{]&KjK _5r8̈́@֋]%r9&k/P L !a*[h[Tl)s,fީd8.@{](6ZR6yn#ǀ넲ęM F"^bnǍ<]u(e*{{MѶ-(qS~UwY a"&ֻ x\:)[|R'iޯ3B (C q G ./w:]qNKԜR_^[pp4&6RcBִ-QD/9 ǖ/!!Zqя2QhW&G[-żR&nNh\"*`\ho@ vJ$P?Ƿ;8H+hRe(oޫ Dk0"@ Pha|SL; -ILa},_譒""@ Ph@\K8TN[K!@ nY@ @ !@ vDZJ=ЁAΫ %b3J܎$BKPe'-%~ލc$|s{S_m"= =f;cU'R韼oX}Odrݓlk'*zo9\=-g6"QhMא/Trm]KxRuB9wtֈ/{jjcT_ژ{BMR5:[&ۈvA20>6sE+Y7R?ȿԉ ,K֏{k\m;1p@1 'YTWSXr+>NU|;ƏnTYvDvp\}8 F))NO[FFM.SH9C_0&?[8s>IE 3snu[je*-c(]k$k[6Q kyJ~G~ikJ}S*dqU$U.s2|hd,?}S?W\g ho,eb<_.g8X2᷷tt8d 6f +9/ 8Fanrh7xu{:q?~UlQi{Y' !~Ub*8eh5 iuCUۑ97"K#1\]: \tKp1nմ=N3!c'YI>BnvQtR?&X 5CS5HSI$9hjpmco0qB-kBhj:Uٮ<~Mh7XQg 0S`h^Qc8vV]D?ټχaNU>dְ=9۹~$xdɬd-jkrdwmIO<0;wZ)37;m d5{w6%59bu6).:Yݫ[4r 7My5d?ajXf~&省sV3n8x$t6$Pprؠf;=Mf\&ϝdQg= 掯k~K_\u~_w_[3;28X(M㩩 -~tϙ&O(ޱ]0?k%o<;vD^b {tHztɍ}{܂?|^{8mX {]+= g&N)<,e%xu߬ݙ^EBϜF{=0ςGn,9~>G#֍NdWKrvgW}\+EK[_^{SPBEReOH&k:}[OԮ81YnGs<B(/ \2Ww[_L}0!24eα|Ƛ+4-sI~$pv3B8ܜ -RZNydv]"!c&[װcU*WrAD!a\'yhو I֦-qXO[Ć8+GUdB] ά k*% ͔p+IM[ df7 A-dkoXO%jt t$K8.^Ko 3?E?٢"q\@+mnfSٳqh=UV#~|bn|lɬ7iI*ݿh{l`X_{KWpgJ sf4f@ANH鵿T).d+ k$1oҋ˿X<ɥs=E9 dʆgߔ0mC猻eָge0sf?ꗇ˒^_?/B*sƉ>=SOV4#IbsY*q _pk?w2yެ]Do[ߊE·^Z\tj`wz'lQ{2F8}:Ps$ r9,N鞙4}r~2-{~`Y)i>{ysv_-K[Xac/OQ>lDڿݱ`ҘЮ_tFc_tY{0=5a /MABr~Wo(da{'.bT/$7Iw2I 8mkcoשּRgXjNh~>F'y׫5J 팽QɃ7Doы0=h9E׿wҢ8UVw8 ^|۵O(|^;$(ҷgjɪDCh@f,~T~ܴaX˻mgwxI2-h!ɀboWUǍxf&KLYs-z7ܹ{e ȿ=ڸa[*~y>=ROU@ @Uknphnkna ?W|P  }Ͼ-xs؂g$T} =x]n_|ǰ쉒TcBF ςQX'#$3ߓ_؛?o1#}hLXj?|#֍NdWk %>tZڼ^ D"4(k@Y:n^f IDATs75~ݺ >FH̞rWm mdM9_ވ:Gl\Hg!UU@E$]~ޜc…boY u`#D!T`8$E7)ְABz q"}Ji$.e,,L: ` Y">. PMbpved)sjw<`oǺ,J}j)p-gCE8 p?o\y8pt038L?TYmE'n FvbBgCPS㏞3L8d#Sr E38cNU~Ĵyxd޿?Zco~%uͶ=EO?#BC8//<^ݫ[)(eREsg[zqeڋR7Ur`Y5 ֡.5#G99͛(n).֝G6.}CR If麺v#__#CYKDUΫߔ;}|$?s޳:)G ΏX?1,Coo"m=~Z?>Փ(rC}ړg=b.ϷTF̙ވ:^ bo8lBоY9ǢsW3[uM[w\i>j( !#Ig2&P"]rسC#`1h"8 c[<26'mh/W*E3ȩ2mHL %$,MU(2 m(3BQ:-_ 5*yi>^iQ[rǴoC'h~P۪ZV8t2Z4%-%8?yǟꙕ3)Qjۄvt!R %gzbeicB 67F#pMMg>5I޶ X4XyI''JkBRGV ӴN}ϱó?vkf1ɨ۹/KJX)8E{Q&,]|o}6ʘ]u2gy=yk]g~YHڕ"ރnKΓe@D]e긼o_i3pm7emUBkO6^>)'~:tme=tqɼ.ҥ v<9Qg",}/:OsE%qKX=a+ȵC9MP>$nvFpT1UU"wRByi܂s;.oé:lFG)!2P@_qwB_޹B+aDy??4i7~x`QF3]~G.q+_4Di EO&bQi;^/] x\b 9Z@`h&ڀ?VJ)f[T;gZnuR!}w:9ɨ;_Jss v[.tNϗmmVS+Dr)-UZHx}@y:Z&N]s_鴆 *L^_ZunxQBtĚAP` KMF &Yyͨ?V"˰*E Z%EM.X#CB2$qÀU.놷/XP\[D/&d嫈HL&qƄ>hZ/,odFsKY;/M7$v y|DNo  .Xr.b_}NGE44l5M{ðómuIFۯܳF\xr?ql]̙Lle%1p Lq1W_vL"&<`pLNWFTYF`X>tAyeceMcGmU=3[?JĢK!|bUeiw^ѝN(*"tr<:P96&:pAOv+^EFJϙ"Z$W.su(p*xĎeڄӎ&g\aD24 Be:pP,+ۿ;YVޒ}bH$Us;Mv^{bBkCRK769{LmS9ǢsW~T0N7ُ 9c(T2[ov֖t PGZ֏I}%mao$~2-Ml Wpd@`=5UoHxa | ld=_ DZZIֵZ*婐3$&1`1(Pw8E*I_?>_ED-TB-PSu Er} iY !c@)z20 J: U4"%%2~΀YLO*qfQ&O=cVzoyxqoxrPvfAok _{ί߮*`PZ P*elsQ G ʦCsMR oѬ rOUqniUY?t ²U?9&\7a+od8h2uEIA( pE> ?_bL$bC>tzL]k^p-J^s.D,i;y</sh=yXe])c[o=#L?Vu( % 3x|כlaXJt]˭>ciP~Kea(Mv`> ]6!_Ĝ;sW!uq):)7շU" v͢h8]dg1^~zPzRRϱyNNJ}vPvu)6go|l~VZ+P#Eӧ @\;!c^lʍ~n:/?^3{fCe5u@ Wh'(ՉHG%^oC;u/DZoBQV*F"@\!B(2=rEJ '=B&q7)d_$^SD@ W2qux G @\,6 o|,kR\،@ ppݎ@ @ ];C^@ @ . +"Rn^G<1d)>.?ij劦ߣ n4T@.?N@tN(U7s(!LU%{V[l8Dv!_:۾uIoXN6q\ {O^q/Wm=a&CJ-FmD{;Og~_@%S2飇־{W+=wLY7R? qҼaFjն0.@$U0p_xg@ߩv;(q44ӑ$DnD ҅vn 'ӆLɹ}_>b>(?K ]K)?Y}t93<x̕JCJ*P.J᷽XrMiCVD33!ㇶ1 ǘGʭdļ[iisViyv DjZ3đd)ax[x|%f~|؈܈@ ĥ 7u}=dYCe|Ώ9]*e|ZmCzQs |Ӗ9lFS?6T%*z,o=Fk9 O8ck}l>mKEO|ϕ!:bX"#/ÈIr,CE}EtU1cؼglu\}YFF5 i%B!*HPIq?~Ub*8u:i*7Ja|8x+[ªy T$ ~mgo O>!0m#$xI(X`)$&?ԺV8ޟ1,$!ai~Lm;r-,RFq(4&fSjGڄZvm<Eup9 v *uyr 껋^w/.3-fV |Mf^wAn2~?eƎ^"q+ \pӄ8͇_lZaO0 Fm=o =}᧽wvmbT֘g}mIЫ{9`k6m3J@t< p:xo.ko 5R蒂 P>dn# N5;;z,Gs<}D׭H>ސ9_iJ^n$GHWb&e^#Y+]HȘB5ln)skPC\on q$n $Zɳ.Džq9WG9q&[*WD%WC'nV7m5bFSdD -bCǕ*NLY.9WiZ*{wIvg09#qI?nb3"g/^~z*iVL޸5N'YroZtI`<Mߢr'[ܖ}:Y$ (<|%-VW|  r*{)IN/q {L:lpIO.3yܠOVn.l葑8zxMy GJnZϞ}ĜݗcKfOM3cԤ1=3i䜇ee/Z\ַbZz؝uF qqB;c@x`Doы0=ho\9|p\rTt\Kavq\olOտ}z4Ðd ("z@\N(QrI$rLa&]TKϕmFO 0ws}M^܎ӟ6cۋ~<"2{]O6hRAb7Q|A{#E ~|MR4wliNYO$=]ĔSC;EY@}d(ز61Dq`^ibc;:lY u`EK֋i2v֛HFRƲKҘD P\Aǥ)\yF"i[-lh`59I~-+Oc<~jyv55`-d#V >!F0E3ʫ[J|0Eg@ !p| #3 }XJ:;?z{bP~OTmh_ DR{|GC9CBiܾSb(V{TvtGˆ9QgN ItR#M$dVAw\i>j( or&`k6 N{IBz9B5}VːDժ 0[#1m%"]rسC#`1h"8 c[<26'm?#7F Kw+oQe4 DQ箈Yп5mr)8kCVx@ĉҚP15$ba:#msS{E^5mcLkM"n?2U#wn:dХ&g~{#(]{mW\EozKX=-8"_=): .+c}pWR[6.J""v$s4ʁ3L<|ژѥQB yGi3$$BwY8ojY(N*tK&47m3OdXZrVL.HҖhpxY <2$!yF"E$FH{f6aDoQүL4疲-tP"p?p0K/[}l;2i9zMQdTy:GXOH UG>ji)MU" v͢v?0$Q.09e^]'ـC.%L5ܥj9kfp Ů9˵(r&npȭ-*YEC[93IiOx.ru)%uRL*^?{PqnkU+}냵mt[Zo%D}6;`羂kl5@ 8ӧr~gHک)YMyZۡ<pGcbC u~=&dMEJTʈ Bxl ڕ#_4}P "q% @.@\v#섌q+ڕ*qQVKc1o ˿Xhh\"*`\ho@ vJ$P?Ƿ;8H+hRe(oޫ `D @8?Sz!i!a%i 1EURUђU @K8TN[K!qoloD W"qF!@  >=S;tF !uTfW0B OjrܜGvu+Fs6{2ɺ/{b+Rh/oZ_&38wƨR|u܃.DZJt{y\:wE_!{;ֹvB׸ƞC e*ٳrVߴ{ i>O-ݺ7,U'dn~{aSI}GgQK ⽧v86J媍',$`(_S;hhoy 'mKSc])DnS%d ǽ5RK͖A΀K;OsPiHɞrCJsE\R7kwX7A7Hu _tkWm09-3nHMk8W~L;A"._z/6v%`M~%i4!;\FWf͹({ѹ(u2KrqEݿ@XJ++Kv?:L {}>J]wM!c2>mǏ.S2>Ƕ!֨9[> AiK6PeI=Rc#w5'v~ 5}' s混tU,9S"[_BqF_vm)yuq證@\QtHKX Vy͐^,nU+Ӣ+A3@\0sYjB\VTT̵>g|'GVݖZT N?\r1|\ Z+)~F=Hc*`әWAƌaՕqE9O'kw\UPNGKa&5Y׺7]1v ?;NIsyvZԺV`p9 v *5ǂ8ΟfSQG؄*o7fXE`f"lzch\v$^LۜՏ &L:IAqVRDnޣ(<;EY;O,(kKrv^S_j=}᧽wvmbT֘BB{ui ^t7kw柑W]t׾ o']g/d/ڰ ph4ق#!;k58&͎GJy=PZ8a勫7vݮP<L%}~DSv(x`vv4Rfnvۘf0Ez% q}OŮUDo\g$=7Y0w|]?|L\C_k3R 'Kkn##zB:|R?F&ÿ>aщ*/b {bK!bxdɬd-jϫ/۶)q͓'篓kZxwX.E9_pӄ]cЫL;O{FnD{/xE|u$Kqu{C(tI^{7 ^HlmTфIrMkiq=FT#¹v W$SwL MEsi%+4tFauV.fdi>E&oT|  r*{uJV{te|Mz0"6hy\i?"Ą'[ܖ}:Y$ (<|%8 `\iK#%۵C!qU_W%jJTU*ǁYv`\udikPMYJ^KoJl\xtLʼFVR.1 -kE"DWG"=ح~d}\:gCzo"[/-.:Uj;[iI'<,.{~Ѳ 9Qū7y?k2=Fu8F5H$ j4k'?8]:c/_&̙1jҘmfziӯ~y8,ɨ%ohrޱ`Ҙ`_tFc_AY.yoْc叽?Fk>/{_r}~wGh2wEcP(xo.7+bؒYogmZ{Z%aO[&ϛ56XyuNOMX¢=y_o<6yJߓ<:WT5Nv(8,y5IF=Ti;~As>x~M}ٻ?LwWUuUꪝdTc̡|ֲ6|D.Yc9;7zgW8}b$+[0 ,Qo{~\^t_v?ak6qa id_,*  2/T&d0R.} ٩uf싣cf>Ae E9˼Vw~mcͲb2J,!2Ӎy,Bmw ОLf@)E h'ONUӄgK ^қfPFrm oe<6컅,BLqdIzO[`*)Da8p ?gz$'HMU3`m7vnQ{t 8O}eg4w0tOy1 w{3 +֝l͞ wx O{!k|~wQF9DQʭqou-څk]1a߾Ͻ$L7 [E)Hos4DcyI7H$ $j]s_ٓ i,+cVS)qY-fC;&sN=H#Z@lodo{p$ B"VY6*BAɢ ~'߿_?‚ӏ+&%Iο1gV._3(X\& UHrѫG%Gٓ´JM?^|gQ8w -`حմeS;hf&5EPZ1,IUhaہu~ y{O잵zS&˝pw?wF/3׸{`ՂU ։{js%io4 78',zu@_/- dNdI&200T3N qH:H`-m9DH"d:BPv>3BFHTP"t"erCv X.̓80@ f' Pٸ.$c8P.l!:,'zռ 8c(K17K';im,Ou"_ïm9@ygT⩟_n1n_{fG'los_왕*%V_=w(YV$Y'"؆NgL/SdEްak c*-Z8L[_n-S CMUNeoK'p5i;hcx /'hH2cz١#gV\5b6{=>e^_z߼"ضh{_or+X6*BA/fNt;gVrl$L ʞT(Er,ӫf8oQr04sr;J5m0WXu߈ƘϤHZϦHZ*W_Daow̱͒[Mŧymp߆KN3zfÜ˫~_ wę (&l2飱dxR!;@pere5e>ҫ_O3,TJ>ڛ/ zsa\.7 ܓN Mj*cѫ2[r=.جTkGAn)-Cׁc*n.e`Q>nRL&zĵ)Bb6 X7O=^>oFӚ6^߿cG'+oްGϙvg_޳Xln㩮@(b%M)&xi~핻صa%R̙rhlbrP9XҠN',rg`1czgj/)+8j zɎ^pˆ^\Wi MkTFM''U$Pd͠Iv!b zZC$ownkU0p[v1;k~GY ]It9+ݏ{HY/$ugd@\Zhf5P:T/E ِ1/HT@⥊ Aj sd_c6= dCr\Xxű QX*z25FFpihm)K"2i@ eyQ5˲5p.]fԎ~.c1vs%tRa(0'v{\oq]\Vrٜ{:7򪜯Ո1wTT PxcGu;fߨu;̬݇ظʆ7A4wtΜ>\̴8jWeO*WQlzY{@WvZiǞZl1z ޡH PP2Aaɂ?WT/Wj7u`bidBkg5mbeL[ao)2X@D'T]7>rj׳z[I͕Nz.7]a(`SrӗT9ַzH+:⁞PW3͸6?_de{'/}V]Y^wM_T%|[*yuDW60I˼8Ol@ t,"V!8%zQd܋d^{+ei]$I2d>HE6qFGTi[CmAt ';I\2N&$ .Lf'5 i<'1 @Zx>:|r̶E1Q a+CFSv;ԕSgVցji^i|?<нͯy=}rM:w-ްv58? {n]= 壅`nL&=A-VYrݵ~}Y_`/w KJ{-+>79/?q˶Go7/i[MxJM=Vz[;qmehZ)[uMŏK;)fAt=nm+-;IbrP9ol=/r[8ԫ}eO*wݵ Q b= KܶwP֫ѐc^k"djX(BAZZw8lo~# m 䡬W9ϓW9K2BW[݃<>? մeS2+Ћnf`?}?Yï<6wC>#Mܙ/ϻ$X IDAT~>Bg0e_|G$a|!Kb V#lahJyxؖޗʲ駥ڹkm-O…aK^Yi1Yw~6N^!Stm7G{_ylSlϾ@ӿ]R]hg7;b7_} c>֨kO4w>/~z:qkTh7V.mVW?~wv}Wqd Pe'gvJN[*QL*Tcys||oߥ2=٥>:{|ӧy^zyb-%__M?-lr.k<=ko^ 98y(UNs1U#咣LvG|{]klVS7yۇWYoA{Z+e?i˦,˳gFĥN2,`4+Tq5LCF\(+nӏ+a^M? `6 |za  cG(b[E.@\H]O ROT@FC;ԔEܻxeς(nhا3/B&5QBq)q“m"qO%MFݪ]VS?=uyZ49Ҍ@  X dC .2a75d\m[̫5D.y"@ vWh@ b f!@  @ qQA|l*k W9NN_7OuCpJ\o۩v?\#]g!Itdeodۿwԡ9+jlrLۼޗdYRDeOk-!_Sش<ʾRh_Q,o? 2Rn+*qMd2B;iB/J,\ /@$QV$OVe^\@w@ )-p,S~yGSao(Wme?Ct/,sΞVxUy:C%3ș/MΊEk)՚K?Ͻ y6Xnxx~15<ʾRCD[ _H?Z /ӧ u|4lcΕT>MHE.Uz\9wj|I @ >Ю'1ȳu+Z5{Ys]w_C ]}[{YyWָ m(ӗl`3$ݔ8sm^wz=3{j ˩/. %\ANde_!V-T2}1FȄ^4LK}g=L.ô"m.T @ >.}%1S:s~e@˖jt'}:@>2G[.>9b_cAkuJ޿zѽ6+ZIH K{ ZZ M\k7%Bl(O*`Q4c8ؖD-sQd9zԒkg|8䈱~F ڡ]>,}_tt05;jdeC,3ɱtp$Hq(YMh~d{m=/Ȳa/F EĎe ?eXBdHm;" CGmW1áz ګleaLb\ c0HYPy-2^]%ݥ7p\lsBȋmp3aUIq `2^ZHD,V {e@05X ۂ,$+-ELGijYrkrJIe-Rk-@ qvϿ+L,zvُ,|&E:&x6Ezٔ>7lmIJI/_)6! z4|I;~g? c oz!XP%d, B^Jo!|FyZTh: RTUc);Y' "6@1C"%ëh,…ZRYTXsΕtֿݝ K(;Hidh&VWS/qx#P[EV7bP@P(@ b777Poq~ף&.&^dD99M`әDh/ZX۰+Pߝ佃QU>;y6WV#Yނ 2 H&06Xrilg63K?]yE^-m KDcmӹ4AV!5tBD9YqUaz:vS7غTt<7 -X,#eA.h1$%+:"- {n, qJĈJZ@ S.eO;==w`牚t vQ&Nǡ f_]Wxo)噤̝(KT Ww$ҙf/m8E<+Z<%cSC:/pbk-y _|0E^3Z>IjSQ2>ں r6.Wd@XЀ$ӯM6ڗD+"Bmw&6޿ɸ0l3ؗFc$P4r.ރsq9-:IdϯAvz48'SzKFLV@icmzs2 y(/}Q@ rj2mͿ `I\wȩ]m%5Wn8u қ@o-67}Mjh}w~[zlx'DL3MOW#Yɓ\WP>oUܕu5_eқ\~ttRzY:otԟ;6 ȗQ!f0NXEmbѣ :Wb!7Nlqq?lLOkK: "i=\)u^ѕg f.LR62/^{+ei]$I# ߦzS&"KbφY9 z ګlQ섩rSrEHE6qF';a@l1 l!~Љq.@1աG@ )ڕ]ChE}3Kls?ï<6wC>#M'o@I5\}~>Bg0e_|G$W!Kb V#la(8ey&i 1^ӱ4Z *2MX?۶En)qۯz&sn,Ě{rgf?TpOC>׆޷; `4 @\`,Ϟ=92}&Snqc\Udn"--8)NrHX 2b,+nS"@ AK~#>~ˢ^5uUFќ,ah,ܼ>6Hub7:Bz!d]?NR`ty@ B; DD @ C @ !@ MD|l*k W9NN_7OuCpJ\o۩v#>\#]g!Itdeodۿwԡ9+jlrLۼޗdYRDeOk-!_Sش<ʾRh_%_ tڛ*qMd2B;iB/J,Fm;.h^B;DY钀w>Yyqs]A@ dh~ϳLw~罧M٣_yy̳Lϱpp 3Wm3 \2#gj49+yu+jҧWk/?6jzI`7>rHVw$+J o~8&YEJi{:se8ծg$ebz-gE52qD!@ 5D:yu_}nE˾f:k0wx(6/[~k 6> _clgkTxNYgcO~d9<ŷD+(Yɓ+5dtw_-y2 c&Ǖ ڀô"m.T @ >.}%1S:s~e@˖jt'}*K%\jo|䬈zM_z7| @Gw8IKcZH K[nY#%f-6xG3:dWg_4{",a27% # d\ @.rp.ǚM\X#G0R.} ٩uf싣cf>Ae8fY1CO%R[HhQGg%Uph7ނ*[dYX|- 8%yT^ dnIwM39O-* v2/>?i_;jݗˍ"s]@ qvq{;>ă#`zò]:UKϵx]gq-'`ɐY7e {hzjR:S]}{V-X[`(v=Z=YIx _IGn=؛3u_GqXΆvtyd*۳v1(k,"bcXUl\&XARDKEo"C5$`?o8 p@(iv¶ 8-. a z l4сg $+[PrAƒ|$xG/*-ۢiiOF@L=BZ X IDAT fk"o63%$Iz1I-tFٹҍ~ ?FȲpNj !DŐ8cTΫHD (y(2ˢifq1BVy@ ĥʹuFtCgssWv09+u5 ܵ2p2?V6gqxo)噤̝(KT Ww$ҙo|za8etITlJ̠Oڔazj֖r1u+2 ch_8m/NBڐo3ٮ%\H7.L% tCN]0,GN:7D<ނ*[)H L=e70 [T v"/P^.@ EeL[ao)2X@D'b]7>rj׳z[I͕Nzξ](`S.pӗT9ַzAVVudku=&Jgql:rN䂾y,qOJ*ӗ\6ojyxkn6#2 I]>b1mQs?zBDPє[#vɁ-n>Nb盍iuTAd05=eΫ#L̅I&Xřx#ݫut,! ٙ^1IbdT|y*dS}I1+GAoA{-0UnJXǛֱ<j0Iĵ,ϱTHYof@lח 57HzHI pYc@H!>e?SN4ύs0=j-@LЮBC/훹_b,~屹Z lu=Ƕh ~r_0 k_I_Yog] [UeN䂾p)IDmV>f2XxulQ -ua P~RP]'N\LKrcVBAlc80^:bӅce& ZILCm(Ia*rRqWn~+.NH\TC;YeN*}P!q\EM>sAJYoA{-bZciԵ:%TzCe.O>*mA RX_!!MXF5~},ឲ|ү ow^&4xi!dY={6regbMN.ǸȔ E[>^[pRd\&/]&eXW y/6uݚ%*dܣw$pnO" >vˢ^5oYҍX9ݣY=:>X>Bs D`rno@ b@ㄲ7I}=òhEӌ4bW@ !.e =nJuC .gɺ`-~ (kn*@ vGڥXyzV|@ @}@ B;@ @| )soe8q'0qt׺$D6^W5X.FDG*/z<)Y^OKG^~Ol1) }茳Vm̼ w˻/Gnu5nxY=ˤlm?(Y{l)ulΊe76;%YT&QYSsZf|z6FlA{'OPdh* e\_8^P+ỵ r/\Ŕi$[F*< S䨥8sa,ng@bO;aIԖJƱ<=zSWSφcYr%Os[7!vh'C]8y^Tԣ7{+62^:y9YaM|< m%7>rf F~Z&}z&Osoîw ֹ^p#_yLde{'OTźf~&\g)1mcΕT>MHE.Uz\_{L6=hIVy/<8*UB\C5{M81 |Ls٫ƫtf Fm}UdĤ^0}C30?cv_cYķͅ`LEô4XɄ,est3zH\al_+$`⌑עq Pio@%E&_eI$”Μb%}x(5~`;O}[|&Y1oy =3$ L"&SЋaȉ$j"ƂNde_exjhȫ q彗ϽJ䋱DךK1eŖ>9b_cAkuJ޿~`MVg%KbIY9Nⱌ`ɉp!Nv<9YbkZOFD!]ZucyDc$ ؗ;\ 0lKq( 2z=j^Xy׀m k[mx[7ʒ תg\YjP΅ {C9 ^ԕe$$qKn}md9*]%Z&$\H.5 =U.|+rЪ&P 澲E uN1*I b" υ5EQrPٓk$%'G7\æɜ%³~zp Rj#e{'\lC iVNU+(Rh -hoű,1 l|Ʌ)wReuG)]"Vboޗh/KGs_Q_l|&9.tÏf ,XqGW%Qs<ʾbCFCWۼ'H<`|>+ka)ALxzˢ=/9FmٺIt=m* v JPOY[G,mNt&c`]7R+d}zd]䁷]yU$g*["=7J:Rd)z-i Jv f%E8cm*` f7NB٭̀6rL3nLH^P* xC9 ̲kKXך Ol3kdφ@h9\׆F堂')'Wq`kW wz;Frh,CdQe+|/(羲EuN1RvI (Y H!E^QAeO*۫d庽<M2>v*%&s zd(Fp܆ԫfTs^A1r{TPoA{s})%_-+>e?L~\.AAѥy._JC_4ZP:ӊ~h;vr3 d i<[T̅bц3>8+b>ɡ}H7<sP+^}YR5 +3^ jg)u ( ʹlQj©Rv!:Qc3}Z1IR64|RʞTPL$Yn/&djJɲy_w +2^m5҄X6^4+{ iۣzY{ U1@{2mA? uSʸtC;8{ͽlfC,Vkn `,,=kL2;<~MA^JgqkԮTKނ0' 1oMJ{'w<R u sM#ʲL98&h]9/=RDK0K 3@gj3: :"[Jpd3T3N qH dô Ƞ~6i֕f=:\;tA-*\:uAҗ@#JW@c wmƧ͵Oл.$+C^[eGD.{ ~g9zqlL# M\@VRxr9a{Ռc(yö8&<ՕK^~j &"%܈h(*e;s'>0SW` -( cI.GUٹ=/t~{?)C>z`{dOZ/t:`\F_ϟUzk%R{!m~lW,8N!OU( !J s]"bH1hdZǛMazz`s˓gQU0]f:!I|N9-*Ι7&18t $(-a5f!L~Q1/`aL $㤌2/;wRkUD{4U+&,IwZc:j.nO;==w5`牚t vQ&NSMbe?~qʋLRg~Y5P:#`IV3^{_ڝ9 2k+nncab(Q S6}iTm]U /KdB'\!(Kf60Cq RF z27Y=d$n\(Jc:Id)B/"ιFsEDHd^lbCcmP5]h{A9-RSYSɃ %e|yJcBG {NF08NIF13 '&[GQL 8PW \Vu`2ՠ#RhWi7l-eP HBkGNzVo+r]f̮ zk HnKoVC vZw@Oҙf\sܯF'|ު+kܓJJ:}vHr'|;i]+:hXLW!M-b%)Ћ"Ct:+D\le^BiM,"ukٶ(=j! se(hRN@W^'JY.BZ$33Ib,(93MKꃄ^dg$ox$o`uTAd05 S妄xɬ-a#ɞh$5fAcH kT΅b^T(Hev\ٌ"tyn ЄALu?-'io$^K-W)m5)Xr+[+JձTvq*흘d庽6pL993SօAk[P2NJe7mAt 2ܵHA?+Ye5{ Qn Gz ڋi[C?{E=3;{f %]6, >+>QlQ}lCTt =^$&l1a fP~?#;{ιww"_Pܬuۥn+oNADr'<+Pjn];_m4`=E(cCQ*"o="+{iԁ(M:PN.G@wMYtW(95I@@ 7q6tpm8#O.]ï(!"x%bw-z8vhy^9Y@ -@qs`@ R;@ @G{$t zr@ n+Kxy"i~S|Xw`mկJU?iqKN(8,:?u:W 1-\(eEl?}aSoj5E\ 2VJԔl*QnUZZ+rgu]!|M?$֩U/xH/S52ԊXgH?OuԔMYϦ*Mz]! !01XT]͊Y,n @Q0Hb7GpGs5}Ha㋮Lp8\Y=QìXp/v,_$ \O7Z{[N2hI|\ 6V ! )5s^9@8ªwievWn uъ(/fܥP)xyM< ]LTUW6B$M7XE+O(o)eȓE~M>ÀENENɈMm:qS7rw ߜ%/.+JF 0.VTy-\Pv2x\GHݣbCxj<0h\O"""|悫Z mcG;mg 󖶐Ml**HƕV m$CApEbZڰRVي55vu|Zry-8x0p^53 hSTm  'jXt@bYb-`,.aY/_Qtv,9RpVTA O(m7pZ-NK %JRGBP-o)Bz>>TYM).[ #n3^H<}DNLZ ( 7d=¦!nzS|vI(=&>S<)!йEbI+;."J=eG#kgUZy /G@ٸ*J|N-E::g(t5JbQOC0ѫH =px$gfo8+>;`vI1߄rj<@ONbY$X$@ ĽaN9j.]s"gCޯ^\n;ߵlumOi*$.2~|}NKIC$+1tq/-(ހr#-Gu%J2F8Ѵ+;>A%#Թ]F>_k=\['Jk3I'2ί0."\LܭUܤOJ8fjK^txp%?E*S-$Q&4X&xn1 fGzfY(@ Ľ@qUĊ>mpVW)Qe'J$UGwRvvOSVcI?gUjM%9/W]<7ڱlo@#:k\s$Cpy_Vt`8%vJ YƯR>kyBgVkĚ+%=-p@\3E wW JG)ukendQ:9>u@ ԮMGѵ{ueɴQ"EFV]:&TaXxRWSp,{BzNHqQ%![߫C٪.,j'ogHg=u|Y;OAx.˄sRUwmૼtRDi0|I}lf%btDŕ!lbb?fr. MGTE2qUov V{pbBᬜQ& ܞz2J[@ظ7^S|gE꺵 lo,jUb9'5=G $O}\kƒ"f*@:Z@&*A@ ۓE$y^i0eKOQuK{w}nǥ %f.?뇦˯0,/K'Iͽ>Yo]IĤ oo2ugjޖWPWz㴚+\6:ߛNb])rbX/Jj24W-_:?Oe$޸!=#(h=v+"#4PX/J SUz=|D3~EekY6psl0#.%%"cES{_->mU5n:%~〳R`͓Sǯ?TIX^8)p B<>c n 3{Ü%J$5ORwUwi5!\Uj5l|kD{J- װ5TӻNݭz?dA v!G v̡DqզêXeEӧ`yWRZOԔJGCPO7v2y3@ =sS)a rG(s7` BοC_m4`@ =(Cܷ`gE'# d~&Cݢ::ww|;"@ Pj@{k9|t~0GEMp%bw-E@ R;zR>i}5n@ K]مB}@ EA @ J V&E o7xJz<Π-! O p_ܴ䍿Q!FdD,\ٳ&ằpTA,o;"!?c"oʷ],7bt>sj8?-s  B\H_4lPi6\>疋,ڀ׏Zqn^1yQºS,kF-՝j_kQ C7'rO-muq6gif<>bö e)uf7.&Ưz~ѝ󰁝͞ źމOcu>8iwkuʝ'Z-x:/߸Ⱦ,e;uhԡo:x^c[=ߵN*POHw22(ߺ1SBߊR&U;f}h_7no5?U>b/Fۺ}se-sV"qz2L%\/*g+r}` }%1;掾[M߂R$uǹn#xjKx[y2`e"z|.q([/~݄b~0ufW{4H,-R$ KG.:|Ӊ՝k/cdzNg.a-u8ݓn[y{3jX,蚚οImZ6 5n DgF+LWw~pH~Se?7HSuzq7mzbkY(E]"Fus3\񻻇P ϜuWGgFso&JEIgwSp> vMY4>q^%)"xn͑PilagLɭ9r_؋@ N.$Al%Wd:!Irkۑ}* |a 9x6|r{4szlg*CkcvߴIj[!ȲS掾A6BʤRs" a/rf/ߒ̲,'$>c\/5gߥO3_[]i:I?pz*QXArOw/|q> Ey/Ps/d?̮WtE H2Y~n߮'C%#iőw8gg51R?_ܹt8b8g7,i?I{Kߔ%\^Z3Z=G>5R[װxz f$@)~oKٺUċόiUek&yƏqc=)<5uh|L+叽g8r~w/lk7Xn:Sµ/nA4|Sksdol\?~秵3#GĈSޡ?W}!:$||J) *l6g<:luG_@ėfЫ +}G( }z s ؞q3cvjM;gAs@ /gڝ1x~ph\2+oHtr'ԪoEp~V4K9b</  .Bk+`jǬ|\ @q0.n`$3ϸ:{Ը=,qš K K\~Ez.G[@TUM=ץ[J%sC^(%bK|6۰\~\2rQkK=樶/!,N!ԭ?hkrzFAʿ$}8F۝Nچ,= _%btkmw0 ºSbB/m\p2`+h/''wxgh3tfd VqoxY=9 9+7,NCM9ZG/LU:X&NqpAzr1>Aߏ1$ą=6qfME|ޢw-i0[M ~e?2ֽf&5Of#4ϟvO]*uسk;IŞYtvŪ=荍 xx:_u|L9UgǾ2c'_AJ/q&ߩύ  >Me{TCG'LKIXWfk̒Bm2>|$ȅo?'}~ %TV3]noۄȗg5֘>ҭ_Tb@9rRG@IE,2ߤqϏl:w(~Ԏ1?|ko s_1EL'58csJY^#Vqalz(DOI͞5~[M}z s YKfĸݓ7l=L\3=3Kf`@?ߗ]vHuWXr(=OnrTD)t75]2d:vL~grPd|2(6z\v :fȎoNU (ހsd_)oH*o8B4zHA?h~`qsݖk%QMK|vXpv}mߨ8-@Z &yӑ>=8-3voâ1! JDv V.XJBӴ 2|58rcPAIfvO}n$]Dm7dd<.?\1f7sZ;Qm_\}&N=SxO͞=[ˑҍ6OB1bZ,.\X֐ТGS?`?s HzϧEFZ7j3j^9n@+;`+ࣟ ?p02 RSN ƇJc8e' 8W￙'j˲M$(v}ʤ&zЙU]R.V!A`c/`l$ȹ Ziu>7Aw/7%-5Z-G0,.FT%tR]o9I@0bpsO5GO,/3ч +(VטE*}!:#7|4NgLכ̵2_n eC}v7.]0#} ~ˬ76J7fXsᆵ IDAT):_n!Y2sOz~/ߦ\哘R 0HTPnHRNɭ=@Ϝ~ku{[76zUgdt #=Nş]0Hf7d\W2~8ڹ}ն|M,>Qh/6Ku>1w X6tb!u^ՑYEo<6o=+^Yݦ 5p_]wW/\HΝ2 +w!lE`rVRaXUݍ$ܖh/m9#=g { VՑg>TgןWws K'vG饭tIb봐N~ ^ 8րu0%^~d&=ɬG,&Gˌ"1 1.sšj7ܖeXfOƫR7k Wtu2  ez <5gM‡@aɊMfcU]q(ݒ/vIOo\/STe{s]rNJQf(삻S/ٷ֞$3G5~MzD4uᙪ&K/OU5峽c87:L{ ~z az]"FdU;0^ 4]o܁OzmB>,`dj;'KJ.!)0N=NG+n5ü|ejIRrSk|<,{ԸeކkuY&@>|_ @'+OK]s mCzc&rJcs7^m3H ,hNSE Vp0|j,- uj'.T {pvCV+ؑ?-T;틣۾cГg=)+l?p?e .JR%͖ 5:iLې^_w2y/SV F܎6`?g5|pڬ#Oe,gvO~_1pzAdbsFgrIRۘ^ 8Ѩjl6'e¥2̒ts,lp2{wkj0MLnoMiw`\gn1TTR,M*LrOfYJ);3dޔԮwħ|aYٞx~E-ok`ѱ{iZp9G1fm6.y M-gv_{Un{jGR'fUC[YŮi(?ꔨN?LE6`\ߠn׿_1l0=d "̼Ik8 oJCI Z>;w00>Io)t#{=i4~SeYDh=Vٴ6/Dz,+&(Yc6.6+ˢNtF^K+O^\ ~\({O{Sg'kh}v`o8k;s["CLre%L*:K2 Y Dv!A]ffmᖣOQTQ!\I?r*7Đd=,Z~h?o=>/YFCP}5*YyMW5 yK/u;'Sb{um7[<7!.lӎ@c9{zJˬnwJg?u,9-w+p ПyR";rlGzrOD.D?r]E;/}p߄'^1Ug*Od1ߪoBT' ^m9,yg.{{nKM8^?*:O65= 01fޔnQTzPʤPi,s.b|@#)<:Nēū; /r%-vmEn`-(^Gf.?<Ή68ZLQ骝==3AËV&ŪRbUo#wT@\rjܟ*IG*P'zӋ'مF"5^N@3*QJcHxKMߝ?{2*^yt1NTl5ƌ h/'+-nQ13U,̿z|3ՓSSH:i/w_p vU>t+1"FMz^{{ebv1>k@ e}{$푴k(XbhEPp3م1:m0;'q襤$$y2x{ ]g.N)*(jK\ Mc9|k&E%ՑaVaz1+ n,r]~QV)=[Zcj=wvޣlD|/\UsvN}xo_y,%ZC{IY}C55 *]d[0_Fgbֻ{2gFlk]뿽M9|tmp;$$h,E`o;}\ڄnBqG]e‡N0?TKOeu' Nū;OzmP'\ 6kd6he`E)57N=01!W+lޓط(Xqpltbۇz|{79]clV10gGO*k~,QSo+9xM8xf/\nG'v򉊭Q@>3+BȱTywmvgjr|V߯_.I6ehYGmuf{x\zA"Uz f{%XU (䒘HIQeM1#?~gJ٬'G:~Bм R:jHׂ*RvxƼm׉:ԇDEhٌ۵Z{0/*: jL ECw թ/?kƢyʍro>}_5KoPqiIm})f{ ^JrE<;sUa 96*t-^m(1EBA|^, 1}VO8{(s_.JsNe-S/󹥙rZ)-,1Wu2gf 21+GE?˜PQdt_gH0M2gqqˊ>3O.mجl mJZ(H!SmP,-'9hY:=h٦ް^_ĕ:J3K~ߞMmߒ$cly$X[Hz^³S .%s&{94ZBM-W2d3ֶhLiK3g7WXiɣ˞jl]n&tl.0+GR2g>U[f5]zfjcoUJW'8~BY#N68U_zv\&6Yef'SS8߬2z](Y|x1sK_$zjԻ +~Z{e!z^xzg:]o%;k2ֆ~׫ύ# ̒BIfOpTGr=CDz:t,W&iTg.- ySOf_R!]^oSOkn;oڃ~Quz닢X/`ccэє~z gO/,:ܛИEU啵WIcפ/x1,ù߁ s_ΕuGο$g>6CR/g;^})^Sbױ-So@.Z̴aNR+ku}='_۞%3G0/0ڵkzva<<̜stݞny@\I9<.O>/ُu~[RSp@:dAI/Q„\1K6v݉!xҾW}2qv-cK-W#&^n5M.Impޓ?I_~?r}fw{V _ޜbqע] n5}j{F[[#H; @?wƢJKG9U2\|1Q^wMjޖ@ GJ!ݶ?ϙiQh>5Tp8S z sM&!!.UQ;AvҒYQm^QO%\gߝY^v>" wc J.söÙr<9 =! -Vǥ<"R;A!X1Ӎ9"? wJ`B~l[:@SWo2/CER;G@ q/A.@ @ !xdم_z_V{#+rdܳ%Mʫ܀ Hu?Ν=kW!)O_LJe/EELٳ%$D&mUE{?W+D6[wb$ڒvJU?iqKN(8yGU߾xZSI[+Qyײ6^>iD몾t>7$$}&[w$ڊKWg{ )Z yEu 4ݠZ9+?+}6crHug8Ɓ qj3U^3%0hXS tB Zr!y\,(;lNX6g.YNav/G?cŒGlvkԡSS[mZB0G?՝ݏuSY۾1=e熘i?G,\Nڄ@-uc鏲6QFnn^ixΐ~&2a_|/+!B֊c`\qqY!jVrgu]!l 'Rq|n݉c&6 ZO-VqPikW9?` Rm(Cp!YYw9E ]c͓PS^ݨV;CB\^WVg$||=1 dY%鲱#z|ˏkҧ?2$*"aYw0*4Dٜ5eHݠ[SJkPY*7^0XIMzKUh=>;ג#3;+=41eE:Oq(,dPMQ@8JE>]fG ;u!kI7M0ۘ%5fUg Ԏ# "'ua\!lb\+ĝAq0l 6fOחYSYltm"_1Xc\&ϵu^ڬ O}l|~;|ɳzXwxδڜs?\%^~v+3}l\.Z-3\nOkb-~|<ڍ>wXԵSbϮef̞D[_כmt?_8_v[9`Y:7X#c5F'Erqζ5sY/'=/]dn>6 X$WdmGd8O/ 4]lC`d˓V6m0WI`eUulK?~olu>.ϥ f?]09Q1ú/ם{O@u[l0GVCD?@0[sa!4DKf?e3sk&2}x`*ݭ]N^iu9O(m7pZ-NK XQi=xH1lʲmOqٮl>b/=dC,WyHt}a/%ހp˞8Y۾}36cunNJ $!=( QQ*qxx~Xmn5[tW?{gD>wfIi-k }E\U}WW\p"* b-(k}IMO.ɴi<<%ITht{#r2$p0(&]SsH.?G ..FpXa|Z巹(:VyTKkc+|f1'V5ȏ4O(NQԣ    Pri -\#eiF.XUhnQO-*BbT4msEU,%5{OQ+Mj@\AqimyҘܓ]{cH0v~-DR9o.*I@̈́24uhlH$ASiݮ5 ̼G~͵mF\vPceQTFވdK>V7|Nx dL\X[N%lS I.DDL4˯zz[oƘ0F;X Bl5J:"&X)h?޳ S,Y8euWuW79BY3 ;99I`mW+s3ZA//R#_vkN=#oKk؉hw^=i=kU<7W=%{j: RjRHҮ*?`Mv{xmv'M%eu 2IZ[tE xݶ$}zq{~aXj;F&:,\:,\x@ >GpM?F<˧myl_>-/\L:8H7^$#20 Oh $1yF 'ͳ֬56 J75#uLz\" J_yl5~2>k3}_wQg}r2& Y\ZK]8]nR5kY5#ԃ<+OoB_U)['Uqrhth`JR82Yݮ|>REDgyW%oس,[Q`6gw/άji6{׺]`^46.2sg*cԫ CSwh(TR嚖# 0̜3qOX_/EzM2D $ D63;mI"|MB^Gc]+Bօ%FF~=@ɶm]Ah=u!&QnQ'$M^- c҉UIY 2S j Q@A_s-pYc.*JPG}?^mn<,O^QT1 D0rpV@GDfpcouJ8G(`q)!4MЦM,uWajHy@2ͱ9k%m*RqDB6 KgNyF{&@* ՝lFj|, V{>}KNy#f%4,ǡ#s.{q7x@귛 ;]4e CN~2(PM\<,7յdüYcNݣ3E=fBӪSL2cFC<T MF-45{(*v5 }CM~~>^rM[=okZL*^}MUw}J#JoW{:ϣy.>!;4uЇת U`,ecieRxXn#cr3UT[/pDMj̩#jkOt~YUpdAi/XOvɹtZ^ ?z2" uӸOTڬe o}='y6|a%DŽf>e\ESʢCޡHA/${>"=NKi2ڳ>\,6?B53Lrb -HJ y6ȭ[ Bw%-26~3a#P/u/n]H-\2 ,Ү,6~e6Li4]bMأN9${5)WZXo)2Hyr@ Pt31 ysu{ME0Ju[;~v鍥oG6/Y j/hPvwh[dҔ J.UϷk^~& }MMK]bu}䑻J[΂Ϝܠ,;n?O">tr|/ *0u8]^2ЅmЩeZhrw}6z#9.o Nn[pm ,yw&Y!~#ӸwY.WZze*d'sUT[2L^_I~BB~qbϛXVQ_kia,uX$nXvovqkӒ>߃k5P=kyoݿi_7kA z&q/6JW7uD~蚼aa{Ξ0wN̛=`*pk^sHeHɝyMN +S^x͡_T?d/}WH$Gd c4`-:HI2LvߵaH Ǐt_c49 k'W⩣[3x|i|[wgC(k~ജq;ɹKv}5d^2owV26`Y0d ͞8!:{EYZO-s#xlUy܀W|2'Cc?57"oXBS="98#z@kY+$k]̸N?ٺ%@ q&R;OcM3C"WM<ڀ%/'{kp7eSu(u "B<_ʯÈ Yf2d~84ыn;WK%}an$Gܟޤײ"Gչ~|XKm[Y'k H0I H%DrF8I;S.R $Xiۡc8ueY w;lo9ݢO3wH'Ӛ*h6l5%-f/@z]"UyfiB0yQCx('7ODS9-҄qCf 0)Qӱ.: qfx%j⡧?lҟʇ>| vlx ph5sk,vq߶ wPF7o#g.3FYu3SHwU+R,/զ 1,j~{VJB `S FV.{⽢Q=+~y3xҋ`IYg_o?ƪX$ãwW? '>Y˗Z6j굪{V\6ze^Wi݆ߖ.Zmw]y@MyD фvaa<3W, uߚ8_ ) 0Fy VIKcrxlNpT }, 4)R Fn7QLĘfZB:j)j0'0 L7%&kJnXF4-y}*f\\\Z;}& >̇^_ O?0e@I2dktv0_: Wr\ʤG~(93n[v3ݳ<R<93-QDU]g£݌Z<ݑ_tӲ3*`GKv{#L ڻ_ $|[gGslǿu~/tX]1Kյ4+Qȥ{epꒀcmϯ>+rgP[rlwig_?,SJB#"=q>S;cNnI„pXyR_-en@7jbK)d*EƲD"re:z&l&3 \O<24@ q]-ޣJ/`Qmn֜.Rf Hx:9$!ؚԑ@FEU,%5{OQ+Mjkjִ .W*dA4:]0͍G  1mr-Б gvE'$ IDAT ݋_{_wp#/OU55{^}o7,(_oqimyҘܓ]{{CKk2#ɋ'{j۱20}Q}iEur4VE(F9lvomvC;Ӌ@.tOܜ|"tٷs ֱ04uhlH$ASi*9aך۞&̼G~͵mF\vPceQTFވ$q> dL-bY{oA; Z̾M8&$1H.zAƘ0F; Bl5J:"&X)h/޳ !IBfn`vrIY]jrB$s֗Zpʊfv='>}_Oaϐzt*S%8Gm{can~ܚDN?^s2]H#ߨ{Lr8W$AHb5ՈTy֚C&{!Vi&c8`{]@VX\R^A+ͱO?\@'{B fy̪~fIH_jQ O{q8*e넳*Np kٙG_[5XQr֬ݺf֌TS^?x?="9jwEW-ת&-):zf,.}vK._X9]^B-ZN=8+h78jCƐ|=Q+ UTewcȀm* 3LS5E;K^uL)DžBgy=B")ͽ=}.I%wvKcxYF%RcbX%۶BviV&օFEܞ4Ir:z&؎K'FC8"VN@&RfL̛X5`ʒBH 1SQeMI2UWV[3M:yPMxv* 9*eWX,^:+[\Z3(M,j ͤDK_~k}in ,¯73ZPYcYpvkYk0#&ı9 fN͝l5G bU^0 UL* WX{3 Yu8Sqe:$JƔ1e&$n[尹kLYIg]Z!2Ca">Q(QL[/1YcM嵜חo ['s1stCQ#w"ޓG)C&\N%翗MBi,1j}j9h`"FL2OnB4! O Ƿ++P2`, Tu4!Ve&)7t752:,O 2:~[ɥW_ :,V 98}dM|+W8G(`  !t<6 Dq}"Mg9Κ@\arHmb ]ul0Mۛ\ցɉFmgp3&VR}0l*)͝9zP w{V/,2~ȐA& S iFT,CG*N2nԠ_:K~C !ӧrM[N-ayOhy=I~|5ۦO/5rX#r|l׬aVK}Lk/[}pyNE6C֝{=pT c Y'kCg @ZrEK d6hl௑[>OZdmXgTG^(%^ܺ^d:@X6$]m1ͱY6& llh AıG˝rHjR f߮Sd4!6u/&3o}Ygw!$ X 0 㯔[c@kghXvzd>)QV jOD$ÔLWwB@jW0 ;@Qde@ɝ!rW<9|x-^?G.cmU|%^S7QToU\lҗW5/=vUA>V1]{wt'vGQ#.Ϭ+w>~_,vez#iAjޑ_K ѻ=y*k-]-Y=Z}ǍsR8Q^ɺ_{:|ꕷYvEw4Hqo}% +K /wm m%ODrrC gL^RV`mF> ƲQ ,ƤP &fl[\U(2ΈXg)w*rvL )<ߴț} ѳDv 6JWƶ@C D쪋B@ (C z3 MA KǞuR;@ vsAG?<@ (C [8]ލ[Dq@ -h@ @@ @ 7hA&"R4ˡ3)A.aҗ㬿}].fU}{= 8R;aj!c,?:yiƠB\"۶NVqCf 0)QӱT@7*톖 !*-;EJ:aZ"+m;tl[I΃xm[DfBA7BƄ"AMv Ca&1uq<YbP-a9 SZ* Bss=MZf {qǣ6c8hG;5Cܢ8:9 Ho9ݢE3@ w0\I9"6V7T׃^\ժD!hƯVO":\%xyi7 jN$G$r$ B.84`ltҏsV5iG[[5Ͷ7_2%hl=$S7v}uAgfRl,)@h eK|6eE7o6*HtGk6gJ.~:)!eĺPTA7k֙j "%aFVlJQFc{AYx/tq@z|YCIRܼ_ny4!XNFI6{UqFny_|I5t?7P*>Lx?yoz@ qrgL"I=o2eY"WM\_>xOzެû\xso:ot(1k1W=2dJ8H]ZCәd~JH"YWʐⴔa{z>렚ecqLIlSa`DD>2ͱhGx0 701ͲsVnS %Ұ{EpfO@CȐzeCZO`"6yQBaߩW j#&L ~YZn[Ҹ^E=RJ! g]GT!N!I`091~5#\Lߜ, R\˜f -Jc$QR;^Jt!PwOF8 d}n܁3@ J#nkH;ܹLW1 ڶ`\01M. x]Қ6SȠUu@Zd,J!/WnܡgXFir ڬ5.eg^FZayH= 9/ 򤀿ZliBKc=96{įW<%x@ R;p|إw.oPH*']E}"P M#>Ic%r DNصI@93o@sq[m/?XYU2Q%G7"sBI[2J:Rk/$:m A(hYR06qqMHt!%bڧF`B2  ?%*LO^T(MUws[K6],Ur !z+{rD?]w4_d~{O2GC$;hG۝T̯y@2ͱ9k%m*R}_R=+7[۽u4N;ʕڴ[1yr@ ~5Ff'C͢neHa(szu8t]-ťÄfyryHr$ڰf_2uWILf ﯫPģ>%㛻hsbuX4l]c@[HH(`Xj{-ߙ"U%mY: aMǹEMBiw.%\@ ΋.9w!\x_"G[,w+Cf4nD7F˔zO/m::; Jv}i= dXNϛ=pU4l,:4aoIQ},Dz;dǙ_Mu SR3!s\iߥpD?Kf#گ̆)KI{ܡCҼWre0v"' 9RC*:I^9,%iUO%,Tt…}Dح iOoyq6\ϟ"xw(g/y"qs>.q.$H7ۈ 6JWF @ @APj87訤Kl"u1Ƭ{ @ @qW @ R;,M@ q&4@ v@ @qh)0cw W7 yKU⌦.3/eBKd͝ЃE뇗{qrS_+E R^27IO1.? |ke]?V'Ъ^ehNƻ޵g7|-n{=ƞ۴>k' 4`!QxlU%kZ{ Wi^Ua&hNzX4TV#%}?pTL93~_ab> qasWRAV8y83o)KiCF{{ε"!%w 4:^}ze*ث9ߗ*̾y]"bZ?¥u8*9og?wUVfߩ?3y.dLa]$pHW5Onѹo u'(5 Ux.5V(*ÍrtN fޔ9íNUDŽ9^O87{Y=OzʙH~\wh*Nˉw;dWQC*cp7ylU*cz nC ~ZWtБ5z?7V% |ei,}_ih:ߎXD贪1#2 i_( 꺄6>.QC<\6kVe_O6@cESoЩPLm::ERT x?ïÈKYH"9hhL?\wb7 qB6lZ_V5Q̜JK'+.kM5 m%8dV߼e4!h꾲 IDAT搙 {Jt,[+RIJ+ 0\YbGQFGݾ_DԮ}#嚄# :Ng;[/~K x;{%։D뵕8o)ޱe//ة(9hr$ B$ziB, RWsB -S(;] ?u)؞mJ@/loS d焲%8R:eqY+]D(eKb,lD+,m^l4"0/'e{&mYl{Sr.lJXI a>uҊ@\IHz0Qj}"Q󀚬RF HIƖNZQ^k֏ujGL kj6 Ӛ(= ~{$Sʆ- "U8H,HB1NOd6{UqFjw].&}C*`Z0 V hwVxn{Ortk_pG3R_xloG^x+!-}C~ H rϊ6sgAˍ]4exd e;(`O<9Im|bnxGX*s{W\Zo2ϙw#ch-{_ˮi-Y",;a dU,j3c؂e\t/1ǣtKO,5M7][- IuU?\׸COT"~凋>Y+8> O.ܺln V=r5+_aY6h+Θ2ݏ,4ql h:_fu?§S.mo6Xj˗?_xZް~Ͻ+.<__/?^q_9]$8UCϿ񕐣D'V#~Qw%3 K$ ^\d`SDy{遣|})DGCYuwCgt68q;^_uώє!SA҂ r$Ur +MeHqZ:u6rfݑp'O^ϯ^<◞HJ6+]p}[ԍvJBAY>^4KAf#1Yvq{*w*%k@9En$,M4`,Q=7ł;4]DpuDr;a"6yQBaߩW j#&L pT }, 4)'ye(` *ԮsQH3 b %i]`Y`YېT7F"%(J-Bq@:=H?Έ\~$ _Y Q ?{j=^߯XIB.}+[$D/.>yX]~ѵWLL*c-ڝfEy_uv(7[],W$0}jOz~Խk,Eta8>;HĮvT'I};wQAH*(hC{dG"B]U.]c/.w MBOuXuX 6:1pi#fvIWէ%rr񂜋RG0F#<0iJi?>kMgk+hPȱ^SԎtt\?hFfW 1>RZ4GT2_1GL$MڦDJk3]bd #qSt$D(=qj{~᯻uv?Z0YUM[RV,pdf5kfz'ttťOnAu۔j*Hvdֿޚ>jH3=9..V$g (Yxݙ.yzgVNL$ esL3zfw3U]oWuպƌ2e=%D"N f`R%bAep6yj_8޳g9|Gʰ g/o+|"zJg`R? ᢊ2\2>wmPa9#ͻ^jkT@1&NKL y.`o3)SsZ۔9W:i2on\+*-7w$) %AEqpD;֗c[|@j+<Ʃn&Htn4_2F{ɢ.2POtHO@`X,9L RɽMiE)1wc~x# pH}ui-JQR@Y~euk]zŔģtLYq ?:w\JnN^oMRtȉf;u⨩ܚ rH|.-mr4jT1""H"~?yg&NO>PYf#'w=ʆ!SF?wb„(6)Lj_,FEr^r{ɋ ֛5XЦ*͛Pخתw\hu}*;R&-'sve~WKoJo +|"z r˦bNvOxn//cҍO2PϚRŬ{3{Ϫg6p0fǝ8K m>Š3g鈣r+4qa{37\ǢcW`9ce<Һ@Ύ=5xl(|; }m[Eqq}!uXVѾΜsPp6 *(PDXJ`McFڭK =kfߢda)ʄG,1Ӌd\1Uyً;EZ: W"*m2&zL3]vk>k=MoYH'<6E)v2Lu/qr.Չ8d&;壜؆{~MNo"@Y%? >W{BTdӶC/6Z}v>p#'\-KrV|;hzwy\!y-;_xjV#R}~Q~+*ۺU߼~틞ՍPGO=_gЩ}_]eD%֩.U&LY#lӵ}'NrKm>]:fVۇ'X7;'mdSg].=X?=SxwXpӲ:j_a_9uIs&?4?^u'(WXo#'Xស\&6&mu}*;R&-'sve~WKoJo +|"Fx9r$rbpVo*Sy19K:t<`c4s&o 2/0]Ϥ)gW/ŏG_ٸ/}6W]6[.lQǖ鷷?ۆP[9uG@3N>1&y9=ҿe-ד?` kucQhCV8,C| ް\+ipډr;!PCw t@@ v@ lЄLďRG BJ rE/1Fya!nNr݈sClfD녨=B.}߿ع__y@  ˔3[_Lrۏ؎!Ui,,M_3{5Y^\-)gԒIW*Q*`=ĶxKz5Vhstv-+H()dao_rňwG):e>me%b"hΚ}2LsЎ4F}!gj9vm/$yXcOS<3 0,r@ Gh7U8~^ŵxK0k#gYt/nSE XN{ e 0f}'}hoRڝS>aR~ k7$ m;^qO_JGHFKV{Ceϼ3GN#trLc>T0|ZUYgӑGIVUjfa*`ܾCDю6D8\?VA?}"`3 /0BTG~@  ֽX:k^-ZTcE*sW ~[\8tU͇6q)WW'DMGE)On_졽bniṟ֯5qr„c"! ܗtMiiSDhN^ܥ3Lvu4 yRRA7),y=kW7`85q]J?ؘXQz BHM}lvWAK&Ɨ@. eSۑ#J}Nӑ3L tM)Hqru:^[ҙ׏^x׉VS ~1 FnϿ/}Ue Pw[֗da{SJh:ng{&U}2yu% q Uy5e^GEF; .af0h_k 0gY`ž^ wJQEj<;0hoM`W\f O4D1QE]b8}K'm9" 5 #{Hs 1%!XJ$͉Kā2>|(K+[oJ{-Ҍr} x :$8Y <m_dn 5U O-JQ fcwT Tۺ/Y]c KDk7kn'mn{_DG~+ 'O.0yo^`3]𑓟.qq'^u{@Q=]nz @ g.K#ghػ9NOR*UhZ3źti}U 4?`֖[Jg..{te͇6Uf:zIlޭU[vS0v.D}Aڒ7)??jw6׾W֪Cұ^w`>$ %!ߑX>p MX^a K?l9us`<KY%L[jQOE(pj,l5e\\Ɂe".J~׍54.@p)H5ˀ&NK'/,XɞF"a!a_3xL?659<}kvmUWvacqP8<;z8ntɉX\ǚ,!qB; \q?מZP$EoyA:I溎KWyȦwD,p ~<|&iaґ#wq[qK7T:RJ7 !DfH z&\:sy"ˤ@BQt֊4;넂 8ؙ !ZFa*zYhz/O;֧7NwZXXBꣴWĝ逮kXo sQoxOq`(V5pݪZt?8á]ټӮOupYe@UAW?DL$DBJCny65k((k99qr].E?2 H7 5 ڎ|S6źTѠQ{E VY$4`C! !p)L0~J]f\QrÊP3R$aĹKܥ#E6yG>eS!2F?z՛E,E$?`odeQ2Gpd1ODZ0{ÍoqHGV<?3 R D,"6uOp 7@ sZ7t !>~* U|&d| 8 rFM|SC-U[$21qX ehțp{3ڎ`"TL5f}u;Nt'^: R:@(J`uN4;qyaై-P &W'xn6rvn4݄ ޏ" M@BbvHx/ų %ǀɑ!!zMiE)1G~x# pѝ9;1 ҷ(EJeIhM.uC ,V[lvU+=#U7`6ubYU yP(as'#5QDB;?k>1qXu~YvvlDU>a͇6[Xt,q4VUyx@L$е;>ٚj>k] KOç];q$h987M_kqLKmfg8sQ\anHF@U3'ܣ%e ÁJ5 6:&9)X,! QvEr<ٷC(YnXb2!<Ka"gLA^N:Ha)JmLW]ڥϚoOi֯jD0I&϶MiQJk S]K܄Ejub,#٢;rb 96;qe\ݗwR;];])*I?cO'_Xy7-ӪZEN}z?߾_ m}[-;ϝ1Sx&@\`<Ϗ991 IZةey%]:~ڂxg01܊9FZjMBđgҔ`GK! $!gGE"Jjaf5q '@ @ (C &ξ&R3/00ų:V5,D"sا_@  2lRL!c\GN\I١&*@ vڅozy_e!"@ m_;@ @@ @ 1BhYA[A9ƨ IPssbn胍Lhx@ _h')Kg, ~[Sͷ9NmCԕ/XX e7Y }Θ+d{qEQSK&]gG>y." XFZǶd"tĦwQ|#fwϦ^ \\{S^@FhG>ya\;uݶbCv} }PU\获T /:B|J}EF8\TptP cwbۇ*1lޭ,i:+U'o~CЖ/st$ ۛdaoTm7T&=3{d:2_]oZmFk ;tL@Djxo kOt{Eo/O,ڊ@ U{=䵱t<ڹ[6fcƢT->9&p䫚m%SZ:NF/ :k7] ~[)S~]ݮy.^ l4rlJĶUKII,lo&AS $4mܜ+jw0'bHx[b;O,Tk||'Cnn3Lrwn0 @ oc~)S'X~{ki?8tUJCn :=ޒ ;zW>Pw[ӗda{SJh:ng{&U}2y}uU3NF; .af0h_k 0gY`ž^ wJQEj<;0hoM`W\f O4D1QE]b8}K'm9" 5 #{Hs 1%!XJ$͉Kā2>|(K+[oJ{-Ҍr} x :$8Y <m_dn 5U O-JQ fcwT Tۺ/Y]@ qvbr {WS>)AJ \kưX.v}!Ә& rxK7qeL>ЦLG/)Sͻjs eȱo>H[r9]=V8N )KBNYK-ac] u I .ccX\<XJUJMVt]DB 6ZSE1X&~XXCp t ЛadP xjtb59(Ia-֫5@ qv~k=8G :Id׾HP$@:r,]#p5bro{K'lUFw+KGl|ܭmu-yPhJ)Yޔ3/]2% Eqй[+ҤN 66 V ҬbLB)c}Q~tgEjh%>J{Ep@:*iI9s' "B1n7-JQf9EnQh@ <{lri6q1*t,,tKDBaS)7!~[S?*8=|fIt5GH9yGjfS!2F?x_Pz3 ,JQ,FP@ }hW:}?s*cL_\2>wmPa9N){ifKSƘ8Y,U2e4MSJNIymGv0TRqΩ2%R:@(J`uN4;c[|M?Olh ˻ laoQSHѮIFp"._"ɑ!!zMiE)1G~x# pѝ9;1 ҷ(EJeIhM.uC]@ sW% XPeR!XooNX>[E}.GN}[H,݉Պ'.3G,}Eux`}|fgc5)S vM$k;3f"97tYE׾ڗ3r˧Mkښ+`CND:7Z7k^C(XtJX#Db#k̴OF6j ¤RNk=KDh7B-R\FEڱL6 D)6H#j%'xI crW"aUr"Z-͊&F:GjFfDtͱHXo]gsr6KrX?Kl @ba΋ 0A"6v*|IXu/j 6 GO @ Mh[6K_p∷w~O_*_pǤd"ڝ5ܘ8S,U̺?czyj;; jY{c}Cx-XX:z8*׼BSim< lo&Szel}Ip5R~ g̃F~?iq3oY;Dj+ ɳcZE:sW=ZBY0(_`Cca*5rR k.^$ޯɻs}^>%(£N/qK;xTe/i騃֛^a(Զh1t%]4fF#dl۔۱&0եMȹ[V'0-n('ឰ>i:1PV @`<Ϗ991 IZةey%]:~ڂxg01܊9FZjMBđgҔ`GKGK~#$!gGʕRsXYFb =$Ed@   /ԌKs7Lv񬎥DU }2@ @‡ C` ;}c̚YbI ;i<;$C^E @aCV8,C@ znY@  IDAT@ !18LTF⁥"EeF !}\$WtWS\'rijP,͛PR}^{~#{hB.I'5{]9mݪ-fݹbW/-7GxLfԢ3Rx-Œ圍wp2BE׷vbtr R5|1J];p_v猹^MVWtK5dҕ }v Xl?#,^hĬڜa,lJ&JGlJ{,Yŗ\1bvwlQʟ7t/N>Vp'/aV z&J>ya\;uݶK/$ysHZɠye?ӂJ@R!}knO ej:V YO>W;s]'Z8{LPȥ?Ǟ~dsgI3o9fgX b6ju{~9?}u>&\^ ϫo?tYc_|#t1#Tp*bp/I cwbۇ*1lޭ,i:+U'o~CЖ/st$ ۛdaoTm7T&=3{d:1*WVD) .|\+UVUjfH>rF;[~. VP91,>`(K ࡻitRӱGgÓ#֙hTQ4Jl87kJ~3aXqa'2Lʜy97k?Zҡ,6X;@˜aퟏ:5!j{V=c?n=^eMM.R +׼5ݰoWߚ8dtɔֿ^=uOXYajztgO&\ik>FmYtwZض<H7ޠђg*\̽#w<ѐGrb/R1>M~|;O,֯TT[j0p$7 uHT㵭K=Lxԉ#_z4Sӱ7m$q~Ӧ^2 J5}<22^-:#+ö!\2$M<~DU 1_E"4u_|fzw$~H:9[þ#C'_48yx뿠Á3{P-s= "\ЅE֋a;y D71+oX&,{V{೗y]>_졽bnir֯T,qyKj s4QؿtKY>e"XŲa B2,2si1Hn-r(9YNrL@9qu0-V3kcE&[-`Hf;¶U3e| Y]:6}jHҸF?ed" )!a[br4jJ!iSUֳ7g.a[adڤiVE)ˌ᠛єDJ<՞5ŀ[ ?JrOԵ^p+guUǛ~!*R q sw.8~8⓷58~̱j|wUDdBee:}kφnݻμy~sJ_/5UX'oi_~ ^V :U>\>wCk85 xL68]wVmZq_ώE٘)ka#nzfTBlk_9ќEpɸ.txIz?zWK7[_Z\hy~#}Wܹl4/t 𛮝}‰bäXt59$}aO yݪ_}gm6<[a9.eKoO Xϖ]~cMn[4crFZLFZ\Z[vs|J!vwrO^SKguǛkAmRXpJ?`)(ej O(U*5 ]w>)i<!;.Әڎtٕnt& :h(@T]=l\\Nh'Vtub<zM8f(Y XQ/t;/EekC`^/,0aaG7g6oY*m4Ysɷ3h[X_km2R8IMFuH:7"N1pajyyfEL*;VS j)o8%9K:yHiFyˬ\h҂xlys1j]mEsU;.uCumLP;TSmB‰є~ertn0V^VS@HTJ tΒ@cIbsN YUY.̚h|// (^\4ءI), z*Uᙷ}zŕy?5e# 7?xeri/Iɬwŵ քtRew</gW)dT8h5ʧ^\Xl&N_o9dna5WL[kZ:^4=VӒ868gݟ8j ~[&%X1iNWrÿ\=wƘGhcs=sPT)!PCr #'f;R]7qc3Lk}aO YBA [2}e~;fOhCpЛTIQKs#~% ,thX,jkSo͚:w|㶃`мC^~{m ?\ ·P6)Wޔu$r~.{V?0 lNEn! Mio_O~NY6ȡeDBDvߓ8"*"N7)SM9{ߋLGGlj= ڪ{;9F/ h(9>pޠsϪg/y3~|wݮ/l}I7̑zgR[շ*cZץY _tO17T߲/ cJ.BH'0h_k 0gY`>h/;%T h("Or} Q)0HԌBX˰!"ρ2>(K*9}RZBǬ_fFۜuy״K,p4E9F"#j̿ñC0hi `">wi{ܾ͠Wx<մ_}A{.qBRW3E&1$gY`O3/<}P\5@k[$G5};Jڤ}ɨ &FUOzX۝oX9Lc|0Nk-3 =2C~3LU6֪c );e"Ǿ m}ڛRrt;2I}!ƾsLOC_g\g5rRvK>{C"ZT;aǺ)d5 $b \btN#x#rEI PpL-7±Eu@Xh'l,tJbPV)!Jd.c[ m b9Bޠ&H-mTjJaov8,>8-XbMN.XR [$W1$ Y? \83 <ϋEn/iilW-]u٤|Z)#PuC-be9f4tJ% i&&Fмc5-v/JY楋nEPThwZž(X,0&npaƲܴmw;)[NJ~t]3v=mUmypSS µ ,9vWTיHj0&9;|sZˢm2?U攥𵟉t }!\10So sқ޾V~NY6hckO_EoyA:I溎KWyȦwD,p ~T Mґ#wq[qK7T:RJ7 !Df_L)~G )ה/_#ScBF]@9 !꺛5Lb5H/h\J͑$u4i44o.:֛'\]~LWK"BJ#%7&O+ٔ뜒(qFaa)KMiԣPԶќf^)=%7-JUG %E;o(uK}6 vm -/z/c?U^?&|WI$ ۫_l9sIk_Rr<{m7e-KKƗ~* U|&>\2>wـrFM|SC-U[$2kF eh(ݧJNIymGv0cHj̢ o-$[sL4yKS"͎bD/ El7M?Olh yNR%hNFkJ@r,Dȸ3yQ(,IZJ(h(/ymF,=9P!dhx/PI!LEs/*)1w `b. lY܉aE)0Pth^jİu= RK5ď+;>-sT%Kr ;9xI F&dCxySv i-M a) %@B6.6q%7:;<ҝ~gfνs=9瞳sk29& " bvܟ>so"U.of`abU鹒ox^}?>VQf'w]^˽@ݼ`O~eYմ*.ܺifՕ%Q)?љy;s+C8AO|uգ6܂5B'O(ƒ_ðPgHRyn+y3PgIuȲwPg_ޚ{8T[vzx^o2ǟz~0iOX|Ҙ;hVtM.۽Sڸ $i;x[_T mQ }a|Gq"+M J9υ.+]LYw|/IQ'ConCZ>=wPk+6kOngaX޿z[KT}l,L^zȳƆOLAk+;y! |˾7b,OqLU IDATd*%KE;єi;'p.myQF丁ԉB-I"ެ (Vye\ju^ľ,vӡ:Ô$ ٯQ^E! aB+qa2d̎yʹLCve$O.%ly폿tJ Nj.4c>ClcɎo=rOO Q >o3[p$<ҜV,1TLRxqr|8~c>f^mGlflLfxܭϿ>;/\?_MwϽ/޼la]K#gr47~ޕ?iWj>M$I~uyss=  G^wpˎCf[) -y&;Br:g:uǯ޶镟i{wˁʙT(ЁPtE`8Ǯ} &`<)߿9ȿwx_y ZA ehG7|0XZ&*y,/_6{(Mϯn<'y[T[H#ݭ\ȽɲK~1E$`!Z!>_OQ:p5Գ/o{m("@w+4k @ @ C @ C|̼"m0T -OG@pJRO] p4۝Bh]@ >i'/4q_WǯnA7׭yVYǥ]xo-tƭ0WoCJ-yc{y]䬚|cQDO{[ 177Pyso,1Z w;'ݠO6}TmY'e $%|`CQ62+LZQbT&Po#蘒cEv"&ؗBg+4=f;@ Rͽp?,S9kP.Ϲ<Կdo,}t;-f |zP3>rfϫFv"ϵԿYH y_qOΦuEuky#M!el@ Vp'}*6.&.Scr}<,[<|`׉3I.Wc4̳%dֿ,$H$͊VntuT&k/+;921ıc@ )wA @\ id_˒(1WbX0vt‚ֲD<(NO_vř;1i'i-3B_ #O%/D1w,0KY_ &gܻ~k8$ oEsueRdƷ֟v:P>?UCjgZxEьcKBVTD1 2/֌c@]̚gGk4ɐv!ڙ棤oS)Սg ôx,C> dE1eAȷNYٲ"˜Uٗ zߑ9ЊMiPKB3 H~ 7oDAO{4-ܢ1cmh,kz ڗW:_,/oPz'rfiQ[W3YAB[y۲m$;i52}4AK* NLn]WK"YR`z4FYŬ 'gW9^h+_z'Ѫl"0(Qm,7D\Iy77(Gׯce09gam5H;f ~@ qMI;yIyN~R`= ѓ3+gkN+ r\e5숙d{'o˸Oh6exZ>oԺ"d5'?$Soscz"b}lW۰fm#跱"Ѭ̍vSN2nZ_FJ~U94Tufz4GT2?vI\!DYd=7%]'% dp @r.GO a\ޝX ّQ#'2 |1,&bK .ek$A( f@ڍ7oJ,2$j,C;ř[RZ@6ybDyZ™<[z`C}<(2g@ kGک7nlt .JiWw4&,t5yنt"=^=؎+/1Ԯzqԁ+*fΗ lbL#Ymk?f):r|פ2N_蠫iMez?eg3{IDi(l%:% 0@e\$׻# D |t }@0U+,(Ըyf\eqZ\AA1"MI:խ4{X!,b59%ͤ2)ՍO c yѽ @ $~[.(O'IZD6&IZ <,- :O$I'BSm3¦# L@V= pi_o!٘ײry-7"F^YEŹN6eѸK[䘼lCVڍ.#:itϮ+c7d9j=f}UCrg1NHٽ .@]5 t5I]urVp< 2JazHW˃DvqYJ~s߸hn@#iNgջ ,2W W\8߼*G(QIF骇Ky1 (O}M<.hQyVy/rDDے`ѐ]#Ʃ f#1fc2jcm<١L B׶0)KG\̂izc)a0Y43Ij%$F<#٣6NIFb̍Rf>#3vW9qø quXQ]Z F!%TFAeHGX*G7V'ڴūVd*'&\:%  6\VDfO9}u1O:1ٮEw @ W+]LYw|/IQ'߲tԭ~h}?TK[GgTu˿_ٷ_{2xv;; Úrx3v!Kb5+la<(IJrc Y׵7}{ľ3_b!}cWk |5*:9o=g>-0WW2f]1O@d7KlKw%Z\LjWc{#"B'90o։Do丁ԉB-I"ެ 0W)Dqeюg4ei )h7He=SKy" P~ƫRvج"e/0aJRmFׯgsuQrCJ\5cq%V=ܝ=2̆{yI:Ŷ8DqH!dY:u*J2&CvQ_Ǹ)O 7.o,8)6n|&jeH(zۯM?ܚ$g(E@ B .;B+FOnYh1խYR>RNv^ڟL @ @ i,|->(sZ`uAH"Ch 0@ ZFLcd^HZvڊWJo( O.IuiPV@ H!Vڥhu_r@ @}@ @ @ .hB&e5h b8my:rxSRzRzGnć ̼D3z@ qI;F?y9?~=pv jnö:.:{Cl,3nz˿mRT:mQ,%p{f,XEe&gՔJ'|rԮW6UټP5wH2.&R~O|mG Uk]{S[X/leVL["MK<5_zZoˡO7@EiӺmY?p9iȱv {ym*J乼eldD'j=6KCR 6.W5 'ۿ3Y燽 @ RHx'Zt,ć٫FE \*WY 9%;7|}n2Z3DdY9޼BŬU}'[˖<C[_[y6[ḿW \:_$:mX?$㦅xei>a! `\W՛3OC]K{iG|Lh,C}odB%L1>>sS2uQbM~~)+r `HFe݉0)i5q(3i"vJ/ȐRFD^Z?9`~ƫ cqJrx.2SY%թ5L d3G)F,iz̳;O=dݗÍ"3.y60LeTJQ)*E~Jv*~?C"(ZW-r`XKWsј  Ds|e҉h{%c;>|PݮS*fRS;_.|l,SNx51? -ožKkvL[`9_:_<^TFkRt3͸L>pls2 B68$`]&X^Dzwhq/5b`U7t IDATHJ reV`7όLo*fb HzTJr xҚV?oVI"Q|QIt"UOY:RNo$.X!Fe.*~Ezhf8-n[L}YBY8>JJH@!J̳!V{C+qxAc(cDZ*ܱQ:k#QUJ˙ADTJQ)*E~J kW}Yk埇L\L\ZH'`,Q&)Meׯ:KEq_׈~E֊ڞcev?s_3 kV5"/ϝj-50 uxūE%ެ3ϊ:Q_b}tv$ A1@H;a0ֺLb' :T|A7If•9ov 0Bs4RfY/Bx.~F;ƈD (1Fu7RDy8%b\HbG竑t:JQ)*Exe_;w* UhLv&Gͬt<aS֮zvqw')噄dR(KE 4-+p_DڋJ']w>yGL,{ 1L]qҕ9p xVՒŮz|$u!^hf,}זAX_P'ӧ[FlBn%ڴC򨫤7A2.D%ZuyW#8P b0WIdrDl4lڙ.ziA#j@_:9yߖ>@ qE3 j6~MOQ:3$ qwVb|3>rjKZsq՜5'w]1$PZ#hM;U[7n mC[@.ڛM0ql*wŲr6R_HZod=dKꖔ.mܝ[T=7J&_qhkf6|9!٪3NMhJYAEͳΛ}#&B'ږ!7NM7;7Uk`j'm0r)- kґ#")`cFGX)hJX.L͌gffZ y6HS&$-sHČyU(z0nCt],dT³Qi QP9)MՉ6mـ ׻NÀ42 $JTs}EhDc]ӄNLk@ U#j*z~t,z7uO6RVw΀J[ʾͼ>ړaּޖÒ0v!Kb5+la<(IJrc7y ux:DuFitSoHLL,RVǧ/H6./f]1O@d7KlKw%Z\LjWc{#"B'90o։Do丁ԉB-I"ެ 0W)Dqeюg4ei )h?lG!~ry:/YsJox#b\ju^ľ,vӡf5LI^*wmވl.. YnZ &cv,ĪGf^^pO99S?0.I;@ W>,SNE@\^7dT>5ۼ!'e@0ƍTo=d@ e_o[$ @ D^He:?Bh-kQm4 KX[ÇU*cT3ٽ@ $ /G{QL.ɢYdH S\QfB @ C\ˈi4)%. *@ I;Jm2Q@ @@ I;@ @\nЄL2TAi*ԓj{?^ `<=Kj~÷qt26&|3ZHT^V(/[ZFoD NO^9iAi㾮_ b[7׭yVYǥ]xo-tƭ0WoCJ-yc{y]䬚|cQDO{[ 177I܅$TՏ7~=*W ƺ8s/b(_ KH؅K8ˬ2EhES=@yVYh+XNg+4=f_+ 45N m([PȲRA/b0af<[Ly.pH**?Q2m=cEXB(^P/@ W{Yr֪w=Ǔ!W9wg~X enULZOӡ)4wGyh׮z@乖7 _2 ?i᳴n3o}Л)IJrc~FΜG5l}%ZQ8d\?P~ͳ%dֿ,$H$͊VntuT&Įr 0SF.WcL_ueRa(*SYU)bks;U F ײ`H8Ý-\0@\i &,5 hL޿zZoˡO7}]-ި{ _礹"ǞJ2 >5DŽل*IJrc 3?D\fx(fqب".*i~qD ݯ nr*2iLc8jk8yDczZMth$Ŷ:{n!Sp/ʊy9~_{\`a޴ܰFC_@@\i $=ײ$!Jc̕Xqo:̼v,@=<ʧӗCgquxIZKxiÈ;eIebelN3nlHLΪw?k9o o˼~XhXi>Jvؙ>uhw1@E$n@S (i[Ç'vqcHLK<o? vk yv<[ )-3ƃaDL]d3Y|; J7/ |C3C'uƀv[dUw'[uֳ(T}3|(Bؗ2bd=M5ώg-ynT#'#Ǎ#B+Xi+'XK>5_k/+49f 1^AϦYdb;4[xA "l(<-?{֍ѸxcjL5yLk:/@ WpwbRxoSoÌ>JPTqJk,D)krf,O\tgk[(6 ;F :QZw<K4JktL U+\%;>j/ tX>f vo,7DiχX~UYBh[Jqmvज1h;g!zXL[umYqB4D>%mKo$Zumv 8V 7n);n 8VPzv4BLٸ;h4 X0LNYX?E9}M2` /"*JoDl8-j+j&+rӣ5.f78h]A?r9渁3iznTu (N$MMcF=Kb4._v$(&k*)f0 /_X@\9eAR:64EzG/f>,ć٫FE \*WY 9%;b7|}n2Z3DdY9޼BŬU}'[˖<C[_[y6[ḿW,r%"]'% dp l, 3}W0>Nizr1E"FJ~U9;4'L ,C;řUCRZ@)w':=Väev$rILLG%w`SsBƩ (t<[*pqx̙:mgY? ɴG[^4ӣ>Bz4K l"l !&1HY8g#vRLz1$.sK[?o: " ~rB[\9Nө(UVD 6͓$9abu,wy$ }\$>7wWĜ%gxA?xP'%eA < JzO71!"`V늴En\qј  Ds|e҉[͏{|M>W_Jc]@onש3WT\) 쩝/lyxZ>o q)'w#=1qnRf*yfǴUQU} J'&Xb'uZivK3͸L>p,!3E,:*\#v[%KxSm pt[2Uf5 3R˲LY9&G~b_դ _ZSe$p)\ՍO fo}E I|;`JȠfUn7όL"i0 :-x' "w}+b.j% 'kV xAww@ iϸL2x)>$i z<$i-l7l~Ď?&MY!n"x(U<{b {z۾oot6浬o^MŬUw[I\IZ>PZS*I?qګ*쇄N?KiT;p| ~jI`F\@Њ曝9'b;$)2fc$ wIV$CAdˋfƌ%Tڷݑxʭ?ZgGmCdV+[@DUu%ӧf?Lē^/O& pϜjE 'p |>mß8翆衍yVO/_g{{I%@ U_֚_!!҉0`T{ lJ`SzkYN}ҠkZ+j{}=}z}2njV)XV7ZQ3;Ӌpv>wzZC:9SA9\-z#'9N!j)M^ڽ13z뼈H0LH1.(t\X2]0hҺ{Sh)#\}]p9LY>-c6o说 ^DkY\?2 э˲zx^F >N{,'?B,ߗBii-C@ H3ylGx5Q3+%}bS֮zvqw')噄4NJ4z.U ѲNtu|=Bi SWqtew"U&g$pl2at.vߵi1G_T"C~EY% Pc`u L5U! PlPIjѦaԅڙ.ziL:~Zo- I»ݖhӞSsM-Y2aUy~)GhYE l UFfr,!'xl)% JQ*`4<_*4ULr|OH*Y9q&-I2 y>ZS'֛'OPWx@ uiW3W7lzҙ( $Q+뜱S;_Қ9캎&@[T l2,?V[7n mC[@.fڛM0ql*wŲr6RQɐ{r)eӗ-)]۸;zK-ln*1Ll̼pՁa@LpYe@H:F8-NIV"9j4ϊ:oEh[6]2pY @GX)hJX.L͌gffZ  qb&.]<'Zv7x,,MA%bTjf#/㴄(iD&ܛ|ô"4.iB'&۵gUk`j'm0fi:Ѧ-^ "P6^?1z)qJ"- kґ#")`c[_V&{)h^LQgd|,ܑ2y CZ%IIW]lvSϿzW差xnٙl|iv8`)"LXNN~/~/`\̠"qHńu:??y-ҡ7ST`S-ou (Ժ_냯=<a{m9, ~%rKy6ذi)kxb9o6ϺOqLd*%KE;єi;'p.ƥYEBi7o)i`LY{Vj[pG[S!5:7 Sr&%XbE}ތS?0.z@"B'90oel.. YnZ &0sVGf" N/jILf]耹WrO,F*[E2t\^K\ʛKC*Xf$J^/,tmɿK-e\ .'~^DpxbDd2:6G{.34_Ao}NYc@ ĕyeyԩ(*B!OeYh7n SIhJ 5ۼ!k~ɏj íO{R@ "/$J3*YW-] h9;7 PT&'^JTd KukJeB$!@ i@\j%k}2G/E]d!+[d,2$N)ZЕ@ qv{^+,& )QJr]Ҡ+@ I;*FLu>>#/F@ q  @ v@ ̠ "T hc1NI_I*&|3ZHT@ 'T6j୮zsݚmu\:uxkہYJg Sq8tڢYJ8&:̞eY*~MΪ)7NЩ]lA;-Fk}mG!y4H玱8)!._ iG8ˬ2EhES=@.ork@$bI;6@ oܻ~htTT7tl)sRS IDATS8qѝׯ97/~j\œXH_|z+fWmc}/i.dl,q38AN_!>jt}]F?i=ߖw~{j+ ɰ̞f>DkY9ޱXVFKz"hKv0B&n| d,d\H{7LoLgn?7@ K!⁞kqUWC[\RIyavi^ttwJ^yy5u~ B@H cزQ$k(>#@ ֜TuL{hZ\|.QK؉H(ƪNIJ~6覅ोYp.Xj+O^v"uZL@12B0Ms>*mIcW%2w{߭Ul/nV%)b) Oy7"l6MX'2~Ǜ3ɐ [l2tt2^ki1b1ϘFRHlE98{̓ ڿ~j\=%O~;? ZfDб=$E[< X?9413SA^l|,y1rh)kJJ07h|2+u 52xÃp&S@( $ acj dnzA {R5Q.LcM~rp#ds3oƕ 둼kW t-} "gKo,K?ܜv-ɖ< ҇Lm )GeC߯~)xo8m mm|Lt7C):pg2GTկ]6;-'Z6PGU\e3  ȧJLL"$Rɶ+?u:3ܶasw& \uPbW<﷐qղ ׽qbjy"4-Y?tl,rVOêKmt̸e'ΓDŽ4D!eZfcZ#~ӯ3qIY7d @J{Yr G)Jh!.ޔ? N[ZuG$H-s&& 0v^sl6nK٥ Cd5ʞp1(Hta&nT]ox#r,Ψ94,ɨ Ыu[}fx9UnDUZ-:d[V}4(w3  ȧGֹ=\6> A1Fiv ԥi_|"4pd5||?|:e-~uÃGwNkZi:ENl]͖/oU˗;8g20GrG:O|uz$Ӌctc熙RϾ'H`|Bsopr6$!JU&J4B~+ƍG"GDf4)U&hPEH @U ˲.mnε(gpH$Q:~/@Z)DyMNU$+3~D~--EWyRwY, }NSbReg(bjt'`QFZ@FW18d9{e:~:LsQ>/](KqRVJ#P Z2ޕV_׬d1[`DQ/4UKىQ:eUƩ3  u\/󸉋d O)hvd"KX:W^EV*ijM=^y^qO[exZl/oL;$)=:R?"}1\>N!n`uf>@i %f J(Cc)R1  ׍kh]I%|&t5Oe?dص%r7|X綳#tTK>[om4^" o<%qZ,;UQd ˥C篐D|uɝ8gS^N4nVE2w)_3t\!VwYW,j=JiRIh5NU@N[\#UJ2χ4p[8SDHinڤ6l k NXOvJb[5^19[^wQcS9۶jDUߨHoJQfULN{y,`,}x1XpO?#92&>t{W&)p!  >isD W/ f:m/9&5S#7Jp\XHZ4`! Fe:mŰI;?k|&1E TF>eiʬdY/T]Q{JMR`pHW=>!liB+   (O![3Jͳ<).IV,2>!}Mk0"  (O? O =Vx([.7J2SB xքYEAAP!'+T~eCaAA߲AAAAi   |L%F>}~B2j={nG+}Tϖf|Adzs`֍cEϞ~-VGEp38Ζ\xu] [1}g-zˇ>EMSk_G`[rg*:pbsPlx/۲~6 &묕n2,PׇDNl547qɐvWt/ɚYQ8`qۊ`=mS BUoWe&p,[s  u)>#8%k^~ P*A7>7yw}KC¹Sg, ZGOz>wml]"]{^~~6_ہo?ZYt^~NIJ~6f9*q+O=6qCܠ1ɸse3.9O3ٹ$P9KQEWMVF)K[" rJom)f$@:Ե/jh]qzz`7D.zo@۲`-ݺcһ"]z>zrѳ:k^S( Z‰ρRS^hWQ$ex'bY?'w>J'w>mY8Mn|CPeV,-EЈLjU3/ Ȟ|qȳ,yLJAAA> i?k璌^)rA>(>!x][~ kaq*:=4kf-.>E%jCMU~ U+I¸{K7=:[HS:P5EwxGN´~6ϙxd$%^iK}&cV%)b) Oy7"l6MX'2~Ǜ&/e67dynIQ>&?rpC&(M"Ӈ66SRƐIPF5T2+qSӇTb~kjD KlYRֹ0ko%cupN$H|b>b1$ ^=sp5Ms{^jx'hY'y`J'S.1fb6(TjfaȡJ24n s)R.N1[#_Wl?<7`28)ۼ0[zOo6@ƬK@{,-e<[u}4) :"xIV17i\Yɻ&zE@g҇K-mQz4OYϲtAiגlS܀)}ȮɤNݖrT\6ǫ둂ݖ;>&$J,>}S:ZRwF*3~DUmBa}uz!au$_5EY۬_<# |]`̈́O/1-B"Řl+X=S:Ym:wg"=_E,V*qE#f~ 7_- }{_.fY?ު'eZyY(v%Zmt8]7<_O[KӬOb XaV΍<97LA 1Wm( Y>r&tq^}}Q DH 7A)C@#V/ RI#hap*Cv)G㐥,vM'@)m #5R!12|GX켹v[5^3@0K2j`CƲi0j]ib63^diQVpV!u_J/7*w3  *z`um|:BPhq~(ujZz{&o" 1{f>)pηY_dk]ѝmZNW[<3s8{=tćWE.w͟_ͪy#Ӌ%ig^<7̔z?AJ{C T!% ت2Qu@T˅X@5nNw8]1=K3GN*|Tr"A*eYU67Z38$UqUяTM\-VKY&*G?"}4ȝL=gAAUc;`I.\&unA_Yx;PY3HB>"umOY93ܣH|ݿ~~wI8o 5x2}hߍtVoUD(dB&=kvg1t "8o3_˛1YbSu❠e 9WpF!1 c2&»m^r\V"|()eZa:>rȃFёR!cս4=[w3q5޿71-_H5BZYy\*!eRϗ[5^8Q?Q3>'2/UiA#`~9VH[.  5ާ/mCO2cqȹXYbe} zgͮ7 )0 fgTyi73eZkվoVko[o%Ou2&̕١{jD,d <XG+蟾8غkgjh]9xtgJ*竉n}n"-}Z M,wU"sV^#H7R&.44[s^ƀג}[R>a4 Ј(dS h\if&mV@]e€>*ڽ4øD,[5^2GlS67dvcY=`7e]c_~ؘ?c[̊7^:3C4G@Fj r*y~L}~VE)AAF5 _> Gߚz'oڲPJww[//<8Awg*T?}i>{$vkD,dCU/Y+XrIÿO}4Bҡq+dN^.c*pՉMêHNZb=bw6c.[E G ;mQ5mQ|)iKrkJI&uns gjY(I1m`nڤ6l k NXOvJb[5^19[^wQcS9۶jDUߨHoJQfULN{y,`,}x1XpO?#92&>t{W&)p!  >isD W/ f:m/9&5S#7Jp\XHZ4`! Fe:mŰI;?k|&1E TF>eiʬdY/T]Q{JMR`pHW=>!liB+   (O![3Jͳ<).IV,2>!}Mk0"  (O? O =V|NS IDATx([.7J2SB xքYEAAP!'+T~eCaAA߲AAAAi   |L͍|SiPg|YS=[/|6ycse[c&AA&]L٫7fH1\_yzfٲ n[иKy+/]/rбݧvijULsCLE#]NxNe[φdrS`M%?nj+ԥwE|\H"K"eVT,{\6L/zD"9h ( dMXnUU&/k*ϗ%dAAdR~G$pKַ=~X!*]%F=]oI]8wyj[՛%YK]ڼV$k˵O߯ ;бYs~#~ObY?މX[>gr_%0e}㉳&=sn)|/-],1>pyq%B}6k}7hrCgHY]dﯚ|ˉL" L;֖b&HC]wWw 6.Mgm? 4 - rѭ[H^q>-+u =˘V}{5(hZ!>녶EEZ֏w"qrsrޖ(Qigt6 dOYZ8Y   ׷ʟ5UsId McgX~Ӑ@Ȯ-?|5s5K"ft5vb*R-j U+I¸{K7=:gHnZx^.@{2CT$u_ BF-ﶸ#ȝDw5ytY?L:d3>ac8u1-e }NߚeTC!74~;M"Ӈ6_--ou0[_Z$Ust=4u.[s.%y* KWl6MX'2~Ǜ=uezy:;Ymr}O(_7yʨy:}ؑ=^AYcYsKD62ϳ>~Y-i DN[b56҉wDA<.B&A&Xba1),|bEs/s'GVų_4k*oJ;o #_p彩ufUSM/1N9x_0? #uWc󬏾_U7|>kY|&Qc;L" L~(0ff'!bL_i)G C3gp"+ո"3}Td 篖nloUd{g,;Bd6oc:tFU[ic{dzV}_gB3 ^2!y.>TEχJP򯋫"᭴9lTPx*{ª Ơ5lc8c|>S>Wymoֺ;C'5-Z۴h" 'f 2xtg簻q{ݹЉh.w͟_EaNq79 ̵ 4UaBl^L KCv`j;*BFM-S9 BZYCI[9ҤwzT࣒@ *b 4*ƒWHSꗪHh0=n)x!hAܜkQHVRWf.qpÑﲈix< 3eܐ %iFUL" \aiG»eb])!5q؎# 5$4IU7<74c9k6g{/߰׏U/s[" OF-xѪjyPgN5I%g^hJ$~2Kb"퐤H)b`U #Q lf9ZbܢqBMUȋׯeUJ% GeX>jxW&[}^*  G}ѹ(kJweU%Wy_"F'3< 5>IAA+,Z}M\'CV)MK9MPz?޳rѳ+:p'̗SexZRR5 V͆hgLɝx}4)~gF ԭ][*W8wvT)E1mZ)I2 AiʪƲ5;mɝNq{].- rU*;I rb&AA ޖ HԶveb%KpE8vѝ5+qyc*˜b1Sqx'hYe! ($:dPٱ{gK?#ugp7?|'\6y-?b18$' 12O48$1924&UPxblKQ82IZ+뽲 F\ٯ_+nX/8߯jDLzIj2 w^_ML2nty v1]|u.ʖśl-mt2  remxI`,P9/lo]U7W$&ܺfgTyi73eZkվoVkTo%Ou2&̕١{jD,d <XG+蟾8غkgjh]9xtg:((;Y kyk*!)1 E<]ucHbKSOMemRX'ACq_'1E;J33i J۔9 lX'x*׊6Ϙ)" ẆPEjZ\עp1 f:'(dU 2+ GAŗ0`)d:Jv/0.$k+gQ2)Y1Rc><mK2H`@.P%%ejv1I3.ٱ 5J?#}^` vvHRzt!kI&pG4(UQ[y6ҏw"DA]CJ.ݲpY/?1זbמW+ F˭aOΎ ӻ3uPK4UinkAE}G;xJX֏w"u*׬,_ç>g!BrP8aҸMqBa"rr29_AxQ&U.k‘ji v=dRi*6prB6oMf{ݥjߨHoJQfULN{yj٪$;)h$p!6wzdsKmܿ6oX 6 Ú5]n2&>t{W&)p!vT1G˷>,-O>b-8eT;mIs՘+<ȝ#eL1pՉMêHNZb=5~$  ȤBh6w\LIk&W9D軾L 3|XW?21E TFL)ϠLϖ& a.AAAi כq AA{   (AAAv   J;AAA?wp0$6yc F>}g؂Ag9Oli/  ן3Wo̺b̹?x 5o|/=̾?eq-GݴqVL_Y^cOzQ:ևG,k1[5˶\5U3yٖ'Xgd< *ڕUM%Yss2+@L=.)mbbրR'kiw-dMX  u)>#8%k^~ P*A7>7yw}KC¹Sg, ZGOz>wml]"]{^~~6_ہo?ZYt^~NIJ~6dr'Tg^H te3.9O3ٹ$P9{e q.6eR79DAzc]om?FJ xIEZWz`7wd.zv侞?9c}@lmY0nrzcһ"]z>z==:냗. kzOo`RENx 'OFe`jD,gC?XHO> V;aLN[A#=RAAdϚ$cW \l&J3f,i {dזH|q}X܁ʹp͚Y%jv:~ ;SSISkD,gC?ԥK u|-OW(f"wdXа1:tߘ2>oM2ܐI,X'&:FnhbO(i4cj@o^KKN\fA[Ჳ߂Y񯍛H}lAAw.1b1Ϙl.IsM})}3uA`\=|Y8? ZFLicg,hC햯T6>IJk2ؔ?8dBlpHeVגl-egͽhTpߔv.ȇ FFx) s烡|N*+ާRΰJ㦰“/Gym3 $ !}S:Z]S-b[6O٦ b-AK"P#R&BI͹za! L}^xodCULo^ 8֙1@AzoLL"$Rɶ+?u:3ܶasw& \uPbW<﷐qղ ׽qbj%CG߷yӡ3,ny*@]SA *P GB-e5r8{ݨ)M!deĤ.4VzM.A~M&aK:ЫuL}f8ְq8kfm!qbѷ̸Eǂlx?{ ܐrM%4i$XQrMhYQbmdC5#7:;on]=@A& Fu.;/i5-=׽U÷@O옽f3ϔoUx[ۿ/cxЉ}M6-Z‰EjybTɤ厼K5:uLaB 5Mc<"W2Zlt APs7rj8p,bV11xn)鍒 !H`|¨QHh#MJ|G >*9ZK7@Ԫ--9gp"ve._yV$ػh~7;Zղ~U-_6ɤ>WɃ2+za*Z Vre@':LsQ>/](kxESeUEݢձiDm-SfYq<[62G^x W[ƻ2+Ul8 | ]d |>PH^BHPz?޳rѳ+:p'̗SŌexZl&ɋ1yuN!n`uf>*rޚ+Z0Ld[Mٜsܐvڍug}.;2yy]6e!IiC0l`ahStZ^a\RIlWΕ eRb,Jnj \͝;Vퟬ1]p|hoi,n=Kngz 'mg7}gX*ZE0ۚڭzE}GOzbjmj6#G`?ux$˗]竈xW-Q&U78t=}1cU~Y׮D5&{S QU TۜRHVSy^ k3 c@PE2w)\LҡmVgjisqVSKr>ѽ,Ա\IygK' UK˥[c&AA&]L٫7fH1\_yzfٲ n[иKy+/]/rбݧvUn_G`[rg*:pbsPlx/rlTYvsnk ]ϟLU+hR 4R~^57) dq҆ zDՕvL>Y{kZ5KšֿZ\b_B&AA&U-}Gp͟K׼d}?׏U o|o$Y $pޅsZY/Сt]|غaE\Km;~8?GD-㝈elT,(6.XS?w$gT_5MqM!~3Jue3.9O3ٹ$P9{k eiPt~6 q.ϧՖIAAIvGRDItk_>к*]No峉\[U̝wkAS6 pާ5΅Y{k.Y2$9O ƅIdý<+=Pη:apI|-/E77sSN&y#nKt# Sٗ:LYKMS8ߪ$eV 6EI9OL56N6i4 f;+J$Ust=XZ8-)#GG뷾~t̊m)*aGc;  rmIJ49|CvXb:\>,#eA"y4:8@40f{-Nok\S;4muyEUKNвN6fM?c1EYr%-mk$ Z=2߭ugGׯ碬mgͽhnYMis~- #wU7AY[mѡ~J"8:~ҡ3RYg#yY(q[b)V5E1ΰ)xo8m mm|sHiB@ 1@Кm2t5jsi?m%ɌWl?<7`28)ۼ0[cd#!x07lLw̍Yw׿.nb  d-SNtDbnӸ҉W)n>ddX'nKH9*eAAQis3E3g FH1&ۊ4}{ϔN#|jsۆݙH38sA@j\򈙾B*ozǍW7|r_WiY֏˦L}mLΌu5b{33A)C@#٩Wth2!(p,Q)RBytTוPVխA5Tb0dl0 nl;8LNtZ_V[+Nl:vbxHlllcc f?J !.P)tuNSy]!e25((nn.: VWR;$>޸>\wSq+Rqj"~t",hYRz66% "Ña-֊b6&5(@Х~wajz]d T]#ty;`^=+s?hY3ưO"H={),p|qѨ 1te f  שX~7\+X\&@\Sfs.@Cۧ- z2.cs5Y/@rRvm%5-nE)RUzdU\U|UTp]9jczesz3'oq$+BAAR$9xق"edH6>weHv0Y}QSO؄BnU*7z+U@۝k,}9Hu{D1-;Kf<4E[&zBZQ\yT)=&`+ ,+؆&@Y53@Ukު ZE`$h+&?0~VcRu9JT&Q#eA3os&Kػ3x噟/IE{D6_AASi׾=/lB*oiWD(.FKXr:՗3zqUn3o0" ,;Kf4aja!GZ5PWV^zRuvXvdS f:]!QQ&o]^IQ JVݗQ%x{UH^c/0cY;Lɸ]/+R)kҸA|alko[5xE?#[ztIcmT)6(5Zzτ/+i!ʿ꺎bG[k+Nw&>p8W wAf7jJFNvEWJGD8J ETfSJ1. H&{<^ꝲV5wDAd'1k'pnT%\wǏo{xۚc^lV0 :.pks?.ޗ=؜S|^,6c8X6ΆqDeg-k}ՕT̳qn7}sNک =@ťRʓ-$QvU.ϼlXS7ũau>kv ([+[(9LڮZ {9aC}k.we}osxn6>M~D(N%_}O}kQBlIQKݲ^dBxs@P1MuIqb|6=_Tlg(d:\Rf<{VdMW[#H{d\իp/IAA$ۗPvރ4_뺶_\ XJ߁w+wZ]Uz烈'ngGĩ]v3nV]S[毙싪,&zwl~JMm`x,g82τFT3Ϫ"w~\&v:Xʯ_j(o}fo- HV$h6_/K6=$2pɺJTC4n)ŤF)e,~)!̦x+E@1MO:|ҥ>REOq[bQ9S҅.{xe.Ʌ Tތ&?( !vXڙFro i51>Oqb0u)iʮIi:w5ʚ s24ޕZGoqMk\UwT3f&AAóӧOD 5?Rp$B~i†abf8I$4&x8KXhKLa׿cA,ۍ|VEsK%) w-W f kBXg]#J}69me  (k+֯9LRW]a!tգ4ɪ)%VH@AAi 70BڿA ĶV&HFJ3WKgl8AAP! Z*u>>#^~GAݗAAAP!   \\Klk.e;JR#;@zW-j;_7 kEk@u?rzL_\|&3  rť昶lCpmcΜzٍXk̔[N8o>:RÌm K|!ttɝ/fEvhMUoQKf̎%[3cf-@U >gszsc[˄go}Kc?ܫ^':qew^R&~[bGXFIk+4 Y9W{zul_+./"  WT-ЛEeު_ y,_oߖEpk. ($q|_e3 ?0\} wj' J*Zqb! l.诼5UȧZB.Y>>0y_c!1C;,f-? ^똉tӬiFKԹ ;uM(1elGT;K穲; 7 ـ+9KǶʥ)&H%Gwoevfxi cf3+0y.E[TE6iar._ˢɋd5G}Vȧ!HZxrWjJ֭LiXqsNx錽癖8=co"ojNh;ɪME][=Yhsh[+yoτ^StsjY_`Rsǜ.~X╤uByv*s!7=D* A끻;)cjJ;k@۵)$3~AfQ&|!ì>2FICa>lMŸSw72VH| [+9ڊ'h7Fix 'Y"8&i7f{e&P|-s+N;)B'KAN];([9crb2؜xׁc|L{[<+i2k7@Jj[*3fѽ~~4s"uymfexǴ|ɘL]g{w*rף}ᇻ5B YrG{@]ZʏKrBϳMjU+s'8mV,8&15'DYr.ۼ S֛"l<CqJYȖ@AȚ1o$|A٣N)eK{LF(V7(AN~^!(jj5(?*׵5vї >mp_#ov}yΊ9+UE<Ӗ/7sFI M?We̒GMyj Q'Fy c\YojԺ>̔ˏ L(_r&JTAjYkIB  lB:QQfmMe˱$d1cJiHڵִ̗HU1 *WsVtȟ.{!V)RMÙw2tq1yϜ-p AAKiGgg "l|Sߕ"@UcG>G36el,?z+|d3V}Mţ>s}/ݑ=?M5Tp.9zͦb ]Lx";e퇬$  %3<Ѷp]o~|vOBe+rI`.XLWvtf_ޘ~sd朲b)7xlDzq6#*8kY̥K郞bl9s_;_,̌U_kv ([+[(9LڮZ {9aC}k.we}S>{-`4G [@;6=S9F u%kFR,.vrfxE"] 9G@C!P653'YSv|Syi{ݪeWńˎ)_dV\("i[pI)(Y9~5^[췺n)f8"]ew!]#۽V(R"5#S<%o=2uSY"(1&3IoQgSJ)yKQ2#*gJYe[%:[ߛ^e>K;H8-&F)R &η8=M5)M玸FYza.Q搓ڻUSTc 27i# ξ5`$  ȕ}xu}3GR2nつu@W/M",Z2ۚuOzcT^km)wL  ceϪtnbɿ$Ķ.<1t? ^=xM_vkD&-AApm1]"3Oj4JU0+,duߢzT&Y9Ԋ]^   (F[75HqtA6Dúɨ@q&j} G  J;QKTg$u  71   J;AAAZ 2Z_vD&A_CȖNZdo2]7}6J~?& lbsL[!86cgNRe5[fJB-?[a}vMz?qaⶅy%:Ηt]3q[za* }Ƿ%3fnj-%gbsL]PpWH v~J'&~vQ Mpt(]䝗(dߖ3$fh(m)>)"qH<4 Y9W{? 4P?Ϟx XOY;sGNG ~駺\{ʯHB1醳'vu'Ifl]RFsn'Oܔ|$eI7qVwW>p̻<=tC3ecǓ[?b;w ۾򫩁|6ng]%2ܥ^ 2݅v= ǔ>t˵r'x57Ox$dq)3asj1sN$5d$[_s=´,֮}[  Ș ~R6"dB+ŧv|?Vge'gen]묶;>0y_c!1C;,f-? ^똉tӬiF&KԹKuMh1ecd;j˚T$ EWCZq}${[FZ.ORV-zeW+jJt?#ظ. lqL+diߗ.0 #ZP6llsC9W{>[xd@ŶV(;V.nTlk-WD;ӬOdBݕ+[D,D玸;e LOqzV\+_,iBHS35Ȇ "gς{fvJre>uW>;fAAn>F_BF][D*gކRa)-…ڐJnz2n7vz?csZ XSk+#Ώw<}6 o LKѣ[| \NB~} 3}hsxĪ@@K }nҤcʰX-<Ӣk=S3y; &U=m؜7@枍͡֍&;%uOJInebOn\3͏?RfJ㌈Q־?4^Q1iX|<"ƙ!W3-=ϴOT $Vmz( ѭ~bo>7In9081 Lxdk\͵qAAnJFf"'VXL36"u{/4FޖFkZ PRʌqtt烈ş4\|G]{^.d81-`;MiUϷ߶pe+bc˺A*PMT6CAcH Q1`o CBSUyI-QJ(p+9T~/ōu_+W+)ڝS@@zpsN-zg2\rƭ9j<1> Qy3*OFPx otւݞT{nV𛵠aIF |A6Ɩvx%\dK wuD53  gEYw?|nwX\&@>Sfs.@Cۧ- z2.cs5YweHvESk4UѭьM(nY!Verîg\}L;cTY ۱hkkPX_ё$Fd.?`m+5r  r3|uWw$ù }^m=uo[sl P0 g+=k@,+M WXX{ׇ{˼0nw{%{06%KcX6w<}6Yg.ml_2pxG IY|r񾞏P)6}3WoPV-Z G. zHIV(Tuaک =֓ťRΟ[+?~WV*zQJ5seOhj&hm<LLKJьGq#?`u=֠pO3/7Mqi2{Y=䜰!5;։Ej5kFR,.vrfxqD akᴽnUbeǔB/Dqy~lkfNAAD5/,r&_뺶_\ XJ߁w+wZ]Uz烈'nFĩ]v3nV]S[毙싪,&zwl~J f,;>F3 *} 'FN~l JC/mGtǝ}/4kd)T."XzRFX_7d]*ojGJ1}CFķ{Q`k%DJ Q搓ڻUST3Bro i51>O QyjzdCaM"'.߰ Yc[;2SBM3\!fȟ✷(#gYiјF6)&We.θ}tLZ#a)e oI:_!DXػ#N'ri8K;𦔅])BAdL 8YuM,L@  A\[~mL!gMzXAAAi !l (  C>ۨ%\}AAAnp-   J;AAAZ 2k mͥlZ_vD]vZoJ?E^vK&!su h[~[Z/kd&AAӖmNbؙSᅯ8x3scP<:m}߷uGG\jmrz/:%]Lhܮ;8iBs/*bɌ1d7{3[^%4p̻<=tC3k5(ڛ'5FK%2ܥ^vD栻p~Y6Srt+c,Vޖ˝th(YZL\^DRIN 5  Y\JFL@˲/ oǿU ᷞ.ec,@&Թjl_|jןWouZ{B.qZ&/zVuj[&)z֚ohc҂}昺=w.P%A׋ɝg¨4excF,fA~V>!wkoa:Nu$A]ZK#N5AAM׺ csU˕||@%ˇ&/zk,$ sh?BӬy3nG'$v :wa8f<~ qzs+:>_[؀+9KǶʥ)&H%Gwol>g\E-i߂,Tb5(h2?U[Ox(lW{669L5s|rǷ(mJFdۻ(Z[]xd!VDSkN[xI.ë+C>O|vv7LVozY/vi RK6dJug~"sG% P<9|o L>I !ʦ>vW߸2 [@Y5@gӟ  W$-f"3oC갔OVBBmHc I!vc36Ǭ5`lN1evfxcy>{g0vW`\\5~w7Enejo{66^Jg=ϴxo*DIdզ"@\wN)术B={66ZwqvgBu?*%jYܸ>Bk0fV*W b vpa&Ɍ_jz0]&b[]^}dgZ89y癖sf+?ק?us-%4oQ@!z@IB}7 ^M~[xs>i1HABf;([9crb2؜xׁc|L{[<+i2k7@J*e[*Oѽ~~4s"uymexǴl8};MiU/ZC/A9K@3xo+ *?wBy ܳ =6%JW 읞y]!e25((nk.: VWR;$>޸>\wSq+Rqj"~t",hYRz66SH Gxʇ [+qBZۘܚ,o9CmޅWMvA6TRt8Gy%\dK wud7v> Qyƽ?WFNLBuL+؛B'KA~F|NPkQ~5U>kk6w/D!?} B![X}G/326g :QsVx| -_01-ť ?o ڠ)DE-qeӿR0S~/?$0bKW*QIdg'mjbOS!&7@Z4 >DEV5b-|Ȫ: +g,)!iV2_ZV"UH6x5sVtȟ.{!V)RMÙw2tq1yϜ-p AAKiGggs"l|Sߕ"@MDTcG>G36el[ q _0U_Se@zDuߋww$zi81-0NVW!j(ISrVM ©W+YZ;W Mjfz;U} ZYh;9ڊ=_弹՘)kt] ABMF(˂Sg~T3'皑/Mwg3?_l-5l\ƿ" \'|l_vwψT@(UY{PL]c"|mhc[|L?qc'Ŭwt3ӲyL7ja!GZ5PWV^zRuvXv˟th?A~ݴ[g~__%s`spBrb{aM|WnX9JY ؜>2wUCzno@'{e OQ!EAzE@yNL0l@LX4st_u]G1Iȵs'Fr&>p8wFO8 2W{W2j]!FUJp.9zͦb ]Lx";e82 ֡ IDAT r Ob-\:uO2J>W+X쵏S75Ƕg?Vg^k@,+;M WXX{ׇ{ tߗۻK=_Gsʒϋf,;qֲƙKۗ Q]I|}B@;hRPTytd4ʮw!]#۽V(R"5#S<%o=2uSYP1MO:|ҥ>REOq[bQ9S҅.{xe.Ʌ Tތ&?( !vXڙFro i51>Oqb0u)iʮIi:w5ʚ s24ޕZGoqMk\UwT3f&AAóӧOD 5?RIҺ+&l-nmz mMBúhr'ύcz_*5pϏTv;AA1gUQ:7_b}rem`nBN&ԯeJ}V5g[a  An`bژ.C'5|^\/u ݷ(IVuN)1b|G  J;M RDmu1Q5LAAvrJHAAnbp%AAAv   ȵd"#ۚK$|툾~>dlMBvi&Z)PO\^0,-L" \qig9-z3)qv2Vg-3%xfu-0o&0cq¿_urKtѸ]wp-w=Z0Eh_TĒc{ɖϔźV;}`ߟ<ͼoIz{+SD]#@#.W_[J]DoK˨7>)"qm==+j_Z/v{J$xd3sCUt+`p44lcwu{+x)X5US t;  r=0zL mb&R96BoC,.ԆT*0z2n7vz?csZ XSk+\lΏw<ﳷycw&ϥhyo!hуknTtD՞6llNE{i5ޞU& iU9r}t9sJmll( l=zV)TI<`,jTڰ."&ҟ]3s)7=8_7f>"UZ8&+kS3IfRӃ.og'2#l<"ƙ!W3-=ϴO3X.Ȥ >k)}j"| BuLcWj›/5K)EC  rE3{+g,VNLs:pߗOi#rroՃGve5 SfRZxKe^˸b:woOf.SD>|⣮=?l 0x}):*Z92$'(.r! w7埨(A !)p*<ۤ(]%wz otւݞԜZTQA0Z]jH(LzpsN-zg2\rƭH =7dZ $W+f] KPN="'<I|)Np2l(YpL. k9ocRskN| ]y_7EPyJ5Bܳr-1֑5c {H$ԳGR͗^: 2C7 Q1`o -AAzvcݏv :A1V+Wc DT=ܵodi7l{b}y̴؜6pxGGsV4Y*m/|xǴ<3H|Л1TkJKH.z X_|j Q'Fy c\YojԺ>̔ˏ L(_r&JTAjYkIB  lB:QQfmMe˱$d1cJiHڵִ̗HU1 *WsVtȟ.{!V)RMÙw2tq1yϜ-<3egvwj%*MH11qIvKKr%wr%9.۱^` & u+V++G;31A qEvOa6kCH%HZF|%HZJ$Y7< 4crl)I@ ke6%$@)Aw[N|4X~k2"G'YNd?zH98(F(rJF~Cp0I(D`oϖZzO?LX;ey~ rCqꨬ_v(ә;bxg8^5aܿyR+qّXe(OF\ޑ?h"2"sY=?(|H2R?kb{VwH\O vBijix?ӓ@ v(JMM rKzrW{wbՉpcٻ hRL]iʵc 0>p%`XߛѦ̑_ ! JۍuŒJ:vE96SOdO'^Wy# dKvꄐFcP7@ @qT'Wy5>¥zqQܠ"Y%i8&Ip6@ Ph@\Ťz]x(_ {5^( @ (C b$%~!jw@ /0h%@ @@ @ Ph@\%zpYr)Qs@ .w4:øEwAPWg6O5XV﨨SN}oH]5ͅU!\WϹl}l穏_U9O嚋F_r5FR'(r|i׬Fg֢4:&i6ۨ%gRiİ0Y|n M+^9<٥ ajXv޹>bB=@ bD[~G?ӖO凟>DН>a[I]V5廉=wm瑭(ਜX=閭MO9q7Ȩ|uzy;(aZȹ')܉/$B}>~eGh(Is֬nHjVeY:ˎʉuڕOJv _l0B!S4lq~̀)k=9!5@  bߊ,bAJgWbTã5Knxg`lEk}$k*]uF ^ةȒOĘޑԬ,K/Cj0yC=:Odk>}lpBp%B-a! ,Zeq^d51OlJ2`aK$p1N&uPK䞷Gzɹԯs,sРuc׊bl}j30QSm,tlҘDG}T*Tp__bN16E?[vkm!g͎h 2>b zP~ v^7g$hL%[( >:S6}nHc8Zhl&r`6EI$QS9mq=@ ⢅v`3ۙ9bdؓ>W@H'O)D @Mvr[|ĕ(cܲ!S>mHjL]]R|79GO%H$ ٧"4. {I Yn 0ov;_vgӢq6NK7 TOPJi(vۥXIJ[ޭ.@kcRl.ИSۥ 6)"- 1"dk{Lۥto~Dkٲt+[Jn#z+Hy߹4Huw$>+@Ab?=n 6=4Aޠ,km_ٳفӒ"eb3Vv/m. s Z6g۬B)A])\o4R(\2q\QtoN #9@ ?+? 2G4Z (q7~k9|wj_=;#Vkĕ'#!E23fr!ϾW~9-9ޖ= W9kV!_UY3vm"tMO'FGi}zH\Ӗ%^>""' 2GH,uqS֩1 FE+|]V!JeR4.a2٥ ] ҘEvIdth,$ҩ>3K)mח١]5 ~0=k-.Jz9-2I+z(5Qb @1Eؠ%덷}ۍca#2BP7(;J䘑jBrk9P0qJ.Z˳,( Snd8@K-RKxh&g퐼/#9@ 7h 5պ=>VX'Vyk\0knT<}}瑭5X{˧,-L^Ȼڛ\Uwo[)n<;_ή(?9 4'*2(Rvz@pA, "+iU>@a1HK(wfM:%1 8-nD P@لthn< G尷8ԁ6sY>e@LNNS,Ve*S@A?@ 7dbɳr{GRZ= f9kt";$tƚ_ ;2;7~Mfqtv?S6ΔLf(yc{̰c~HL-i},4f.9E;(טLJ ;"tKmκRt^uk2I*F(g*?I%{#6>H[p>Z崗S/ڢT ^C/tZCuD qvsTN_Q11SdI:3J>yC'=2I:$Pz-%B]f.TC[v~`#^N7^3Hc%=wS#YEg̻ߣζ%wVOu.8P2qQI’ ;$ߴ`o悜p>lN] 2X43:h`np QL77L5qE8)cM\c3%HAJ*F (I1A9Qi䒵}V b?u" Exp/3Oofz7'6L%(KIm#urɺ>E c ū=E`4Ό]6Mih0Hw3O~X/ 5TOhe)AĚ޼i/z:^:Vx/O_C1OpJ -)oC}~6+De>DFM9@ )RSS1JS5k2jb/S/'@14wKAшǕaO3oJG2G;\@ "'$r*x7`Jvj@dNi_ R(pe2dr~r@ !W*bT$Ē8-$ηvIej\]Н/\9I@ @q(\w$G.Nv.P.9gy*ѡj@  Y")Z}!L at[!@ >oЪ-@ @@ @ .7hetHqTj7䊯w%[ ȣӞ[OTLQ]_վ#dYhU;R#i4+zDitq(;1oG'ou5XV﨨SN}oH]5ͅU!\WϹl}l穏_U9O嚋F_r5FR'(r|i׬Fg֢4:&i~`t.G]o[*Wkvok)ԭ xwԬnHjVy̼ufDI݂k_:Ss>e)Ӝ[/y.vk}?q`UYLTnD_<{pdo{|a QgK@@gBYIDaqϞGmk;?V)bTo~3db+<_*0>\ߟ~F?pO=%Z>Z]tHlٹP:rf|wJ"! 6;vBFp)kWnj^S_G-xe'I@쳸%;~U5l|"Jawo?^R;ig7c$bbbܵGrb6=ĕ"j*O t ok"瞸S[4vs'$Y#Y];^e/;*'-3˧l*L=_~ $(XxHQo0]=T~>'\n%ǬVb,b֬sբv5kt*^/uuW#jdوɓɓ9Ex+VhmվG}iIE B;kMR 3e+1_w*H>j 2g1kp1J>5h.Rw}?8feB; TQ.Lsm' d e++Csv;&דܫ[ٲ2eX{LؼyAI_4U쒵Z279b:|(s-wdAˁrZ498AJ~U#HLTW{V??;.2sh0w~b>t0}Aw8d,Zקu aa+*0(ڥ#:q6 jk@2V*r+VZ]ѻozۭ#wkn +BɁ783hIF >ڪ=srYW}؊EJ{t]Î?{ Xc05k2VIcj͇fM&sl uϽ6ǎqoٷ<خs?֔Y;H0S#7o/F3u@bÇ0z{>\p#i#*21 י ͕2\T8Hq(}^c)u)a(Q5sˮ?p#9@0vWMCN'PzsT<;Z*e.b S=0kfͤ[fg8&Q/ϷTy3ey:|;mζ'+8>jj{ɊAyF)Z 4wT"%eSr-kmۥ|~ߙІ](Y[͎z,S-kl<D,;_vEӢvz78^u 1<۫nm\k+tlrjY x۬+Iu-)׹otQ:df*@ <|i{9fT|sf`'w?h[q.U֛&F ,8IhݸVzeiU5듔gj8Z'+ɋbJ麻7^{|@`1߱M\^ohn"u}d9{dO'FGi}ZW/[5{q_k/ y~,ě0]s> Kni0`\§eM eMyhKBLvls=oȘ"`B\M)~Hm4] @-DaR7ަ}l7MئÇMB, Q"GL R0!uf@#CGClL"7Fd hV, =s$1'svho7ۣ%rĞ镺^4_OΟ( 7gbS97~"$vu{)gy]qcPtAL1^ b5,3p^O|8<3eN O>$FkH${s<Ŵ!񺉥{H&1YSOXdr=LnͺS"+Z#Gٳt0x5V*r_4C4$DH&/Ί>E)zt7V\W}#2u*q?>|׮^'L@jnγ1E)| uޛnNiz3gĨ*24 y&4=j:ZɦsfMt~p8h{zs\xo:<y셶quv暻Zg_wVXg1ze;:sܯ;X<:};RH~bG6~|R:c{vO-|2INl{!koΚs4atܔ7nz}bKl9_+b7CouNX:Jw'<% thgNF еE)JC K5fQLY N cVt\(XNDp9ၛ`qaE,̵22e?0ĬSb2\EnN-:Pf.˧,()x'QH].SbMX $a8v@άYsYM$֙뙖f2j?c+Rc ),Ӵdb`aj"1.9ڔCݢaBp_MGw^HUT0g/botHC; '05I(0 im; i=\^dIرϒ._|GU|ǃ~xr',Gc[PIokk7Yޜ5=cW\"%"1o!:++Kbo(n}, Wr73LU"עMć)4LH1Ah hB.WbJ-S 1@KL%"<84'cAoSAM/xɺA7*rGhE#Aݢ:E'Dyu9k^\>F e.ùHM,/<<%V3!`і౬Ywf>=8oAm8޴npԩDuunLfCSݢ|xu>vwܯ*^^ud`{ޓ{&|붲LR9kVUDȣ53TZ6 bN3f_,bMuZd$Cuҙ4AZDcFS<)=fԕd‹3SI{=V]|0BQijb;1bMLA%:Z$A)Fw(SF(8v~e]:x: ZPH$%ؤ~6YlWyV^_KQeO:;Jin24:_)Cө٢ْ=Z&rZtY 8?ؐ}g BW+*.ܘjrEf _d:y/p f3s+A$YF"]ظQjn?cz=lЂvxiiL&1Lz3K}/'QB|1}> A5_p!W QYk>;'Q3?;qVOKłp0ׄsGvYH]ufGbt>w#Y]` VDwHlF錓W=T1uo\e-4U"%"WxLi8T'Đu)-1@}i P=ImfꛝK-iПJr oah'?#p̊ Bi0;2$t!9W"O΁Wk0< o3(Jӵ3}uUr~00T_~o˳_Խ:\.bdP$IIzbLTi)(?C/*dZZxsDATȋ YDu-a1dxw1?7ݼՆ<fBK*}99T ,YIDATr3G@0/{RTeuQY ;S[LcKKAQ窘,>"OTU'5ez) VyOO^Py $5+V ò\~}#s`{5WԯbcYcLO^Љm뭅f$ْ]O[Y7зA4 2EIz=l0hL &Da8q<2}gM{1Kc<"}v1n(n Z˥Lx j$Y \o}U?7 Gc8ts̽0 j]?ͧeƵ{%=\H:d2J:]$=okœnpŮ[ vmOX# fKx,2O6y/--xY^Wgx} C},zM/1c{vY9Bv%  =gDv'gvon}s%wyxGe|^evv5||Pa.VE*vI x>5;Ut%a߫ikk踿5ygcawKv̜e;mtL5wQwնU cMejQ~XbǢ@魽2N2yoeLFgW}Vz1X7kbdD]nd%~B+K "~T耹6"c6}%+IvH:m#urɺ>E c ū=Eܜ[ĺޭp`^yq|P٫msZSn߆BmV|62sq%3seW] m`ͱ HhuWEhȲ+-ҙUf`Ѣޛoy9uVg G%_Djdin;De>Yx] 8N&bl{wz]u I'J_zj`PY MBs:;-==U@زٳdeez//5K,xOٹ"%{jyEכ.(׺_DcԵRvw<9Kzкݎg.ZLIo0\a&fqcZO{]!1]gMc۳;8h4=(`SR~&`H$J^y966͜%d8djl̦O*~FLQH1S1JS5kڂpcٻ hD8?7+ lY "J&:irp4Jv1+q z)rlxi-Wy# dKvꄐFc P38?D9`Jfo@ &<{@qeAل뽤Itv)AEJ,ӒqL*|;mWW8'zdrl@ Ph@\>bLC 6G2k$9Ep>vqC@ }G\82=bhmG*B;".IDM\~!!@ .X*&#kH&MȇWr!WR&O~@ @ Wd,kG;T~MUNn}U|9΁~g #@ DVhd!qF;D"ZTEV}"%9UdVfC _ m{??| 3PǖWCNM?:gwwuc`_KwyNFKƞt%09erOO89{ۯ*qbI~!6IOٜrUtQĻ<}'}DZ?’LI{{Tn^8j o?'sj[U~s!nmyZtYH1Vˎ(ّP`߷Yp\@E v_0 ίﺱ8Yɔؙ}bә>{ˊxÜ_|c ޲UNo|lIeNͯ+퓞xpKixx^ҒOYu:WOuM9]h5߻M^pZt\?=8,Okkg\3(ը9.xpѶO~e/}~v_33uaIE @Ĵю;Vpsݐ3٫]ƻo=Uw.ZSQ~Fc_0%86ƾloW-W:No|PrqiwPw/GeYYV]iQ 'ĆS=3vTyQ)7ͪ٥m}L'յRF4Gìq 3r~"##j)ʎ"@\}hW8vo]N=Κ:X4ŕ^;ߴ8fs]b|owӛeT+cƔDI>|zcSŘ]9w;cI%g>}'_]Z]4tx70? Jr ʙ*sdn='[`55tS9˪U9Ga5t> _7-:fhˮ`_W~iѥc؂,4{u醶Po9uƖnYX xqgɊ,;k'L7zoO۟gU/{Üw϶?.\;\Mh5HlN]WO)7'9qg 'n]@ qvúzB+%YCV~vfúZD٨,X4qG?ͤcIepQloMrtZ{c_[8ؑK2'ΚG $8Vhm=ؓ92[2eV#}5wN+ I>;АX*rUg+|ڧ76ˊw_G-(#^ ;X&.V{cǽ@rTu?㞩o7tY㝙 W/kHߘ~3G D‘;:8ASb~ i'#j$Qγ*t>0hkO~f5߹FK}hWV*?#ݟ.QE b)zdw'e wX8g[ݲX ۗ p3 ݾ=XQ1nq=Ȯ{~I:uѨKm9$R^ɒT~5 h)f;_;?ᓣ}L-_fq 3rEUx7egu=yɮe3TWm o?rbM?' h7 5CކRsN!p,wB#In5׍2g# I?GRѵ,+jh 5uG7::ڑgّhuX30C6=)ŅV]>k._}]>멍MW>@ q1YC"q2t萛hSOd|%x,M/ /ĺ(^Z:(뾽fB:Px ôa1PvYx^-{AT~g,/oTX Go}ucC1>rue--=Dkoئ :W/I53kj><Ѿg] _֟"c]N‡IuݾD~Iu6zE }X>w1xoOƾ46F%Ii juWf9 m!FL閊%?QsHz,@ ė";'VcLƋhB)_v;p\KDJdzXR0hI)؏bo )NjqG3/uxⲢ逄~OhIݻr\sw4szYu: bjƋ fC- +Y9_;_7bhum ;-JHz,@ ė7˼icL tIU|rTJ\8M>B). Il9ϛrE99]p׊$練*{JN-& 8 ylnSf^ì~ͼBcŞ5h*;׍:%@u)c(91 N)gvʷ`weUԙV)1s:3P%y療`i:M8k/2郏|#>qwgbh'oW;ɉlYݢKGKycbkxQXp@Xkҟc|eQ!HKϟ2Kz 6ԍJy#@ W#a.K/}ru㦏q_!޽ƴUqNK[a&6 4 1i@%3:I|%D{1㒽11̌$1 8R.@)Zz|q"a[j{ZY|ݜ)p5w2:Ha+QqctJY5yh0㝱AKP|S&?H{T!* p{=of*K2LmiA_-u!DAN٫CssRo_>nN])J]Q9|rݣ\_i~zj[\6wNJ~Eʟ^-9 Ņ}Eǂ8?dkrFbtAD\1/;JtXOe7wڜU.{eYxboZ"邜F!oA=NN.1׬3o\ctM/[4~)Rn2qK& {qSqyUK.reu/7`|236P,bW| !^4?WݳmC j$i_]ū#s'anUsmSH~{^Jdadt@^֯/^6&L􌧵UOIgvbu.:g'bw<&4HpNm>0ǡFKDpV#7vݪL~mWQmhǞbcs[i`lw Bȗ^y$zug#\3eU-ow>"/;_=/}}kE?^'Y_g y=|w{#|{>7 KœBmKb1W{wNs|fwM|b Bz?ݻy+P?LSk? _xą6ޜY. 90 b97{R=q lwbZx-rc-ݷ`y _߽B*tC1gyb]Q129ͻpH_g?c,??%[9m/wa1!_C*bl!`G?ǔ&s,N ア@ (kH s!~Z6bhT+Ҋ^FM5RkmU5ZjV[kz,[j̺{Jn1҃cgyYg6YyUW[;I]wsvC(|ʩ;Kt-v;׫?{-<ڟ^~\"N|bxtϼ<'$Ex-9gy b7W<\_*JzRʦ>]me现ǜW;墷vkl#܊5_\77} ө7UHwݭL{̵W=ʹYa3On[ay\sF/k©;QZv3RfW-c[kv=XhotfPN1 ź[213mq#9٢`mݪW |COn9kAtHSjf>]}O\DOx1 (vI|(&6ci|givu5dʾ|Η%<Ap%K;sHy#B  hYkΝ-O_T-5,V6KN$ Ng옩"&)gXY ]413vNԆq-g5钻K-ݱ)!jV*)Bj^w*k4 8FҔ <6,ޔ]ȇQ%HgNֈəױ~ݰ~ݎ7|/wtt''U[HϏ,93|uVޔ@S׬ .5z$;H&Uǽx{t b귵^0y*yBN*6?S-9J-nWY"٢P  S'IZcQeT^,71r 3BFYp%P5Ho}/p׸"Z$"|fVa.'=e$'v\فpVUN̴:xKtPYl@+s*I/dɝ#DHxP4ţ, [l BY9cZ#1yB$ި9'Myq0sؿIY Nm ;qCV-l{GB$k*3D7j Tb]x\=Pch MCÉe-,ck`l 1O IUYEnGGf"j 7stR0`u)&g3qTHPnzI-8?hk}EN ךA)8Í(-/3QE\N'>O#Z_(=o(ڥy!դ{&IsFjb 6"߱BpZNB+ p)$m ]6r(ڋS+ Hлfp]$-.Nňwh (ŗK/ m*/tfGzђ5kSHIQ''%|s|.U(&r̆3{l-K 3O# ːƫ],Yot9,8zD Zj ΓWR&NXX8 uV?r2lɪk?"/  !BX\SC>u5Yb'*n@0;/uYe {A 9U!ƀO!*`k ~` R˄]^!Zch(.pao3jAϘfzHĦ5fQZ[j|M-'Ajkd(czR[@8|e<&4z+FX%R $OI&CBtyV\B[Dz_S}hۉjqSD@+*@w"z*XD{Bg(|HnPa#mg>[,C[Q3an\`ΤCj5BM5J@Ġ˨.j\'?ɁJta: |sTcZ^*CΑC84Y a9%ȹ"(7]XY;APG&ވ q̅Ja%\>"DdPWT WMČn (jlE쪶x05=1xu~dB(A6+ ږFեb"# +Ay{jNvߕ Q r,UXPdqhFx*$%@欱Pؼ!^^Eyi&UW  !@9CHdM"KQ >e ,n)"V N$=Py #c!_>DxHC &b3NY9Yh=5N&4;ҦZ&&NR]d]"yM6%G-e7G[5HiMD`j(b #4c}MuFL~GzTG.@m8װ4R Rijs%ie+~KcZNkA6WaYy#~@ "00 ؋h=$Qh;bG(sZF,>L7~xn?8 ]."*jRt~DS{fN"7Ea0@$b}WVدU嵩0;"T|uA OUnT܏=TRu=3|Sh\U@/Fbf9AǢ*:RTzjжQ ):^`#;dԓf@kڳn QD x5agp{y#zKZt #v1 iAL/6pS6@ :%`Ԁ`!7#(m5ɣTTG={vHA/ʝK&׳:R?dR(= įlLΤ6y[xFN!$_{LBt$ ʠZ!Z@MJy чF^0_F6L5!g`1)p14gRѮ6Hqhd!Qnq⍈}F xuX`&5L!60Q='h{H-E 2=1EC:1uaDC]J&2;9Qz5XTu/mPMR;Tu(Pq 2@ wN[Tj /]Y 9oUs6 lj'8x j&!#z ݗXom"7mr !YӶQ!óJ|BӶ.M U.).Җ౺7˥}I=z"JTe42%xF$X HDSS`oe_s9ad"r1*Ͻ^m 3)/؃<(16R:kzoU-BHR-R Cr$ֆ(u(?dW_ _Wd>ǥ폡V':t?  q#H ͅnaZBHa vB Ϊ%j4"hng&SN šv] +ljNQɎ{dy4NH9=!/d QG[U! (*:__@;Pd:lX!Ą\rN䤷;")k+@v\EExgKr`b3xۀNSPE`jJI|o4$0#̲\zJ0E26yS [Vrn0 &?0 f@ 9jGo18S61JʾqKO~+&B%-ot 5+:"11@yj'$_4#5YMd@yD\܀BMѿ=}mwI b-6Ogs44 |X4=0+5!+bq!KLN5W)uwAp{D-:6,3dhy8£ Pk&_qCxHS"|-8Ee ֤B9GujJ=a1 R(iY7.Q119$%U_ @dH5ljSi: Y1+'.*O ll7@.4G(VW .,ڏy {J@<]+5V&vqz^OVi 6СppymФD l . HFQ4~Fu#wiDى!A&صsɚ"#4dP"!~6SڽjUNiMKA9Talͭ~A?=46⿀%=1Wh:K.P^ӞfEB; ACovZ"(::6po8r^PsF1*j+W)LsWM*@Uz `PKH7xq$ZsqDnF&m4sRӶ;qwy!(zFCpN!DH}VQ_Y8H 4Q&*aBpYxFh2N )cğIj7֠+|kF=Q_-n2@?ڢImi*QWZJ= u ÃW)y:LpG$%2MB\3$'(ᢺ vdFې^9\k)>ZX x P ֬1KLk A!3/T(=zpzENXA^:_ 51b @gbLLz~[}RxۂxH6-5 .PvU2 d͙h2ghhgN&*FMm46Ԯ=hg8`ix3 *F(3-rl|Q~&VbĦ4"pi-h P]x_/]s3( 4 Y+^MjE%i&45fEM0vNzjı*\x5!߬q-QˠIi'w㚫"IH\~ ԼCq S *L;C5H1 9 IMn(f[&24#̕)y<8 `j}b@x6. D&Q ~ǦP5"JG^@4ikVdwM!2Lh#Cz/m]j_H C'bj1oYv@Hy۶'&}p.f<w r$<CֵC;8T5ytQR[Jg hl4>=RIJH'$H|6ú R=MKM:%='(=`cuXƏqķݠy70ؑY4>}$^P`1c%$E G)?<i"7⡩ 5 f{۠}z*UqCv԰0Y+|?UmTHH̴9_֍ $:E ⩣'*ַ1V[0_NqT VwnYJPU"DTTꝽw$Z~|r4]D IPQo6599>m:͇LpmJ 1~K B1 F:U^s#ӿ1y@NbޓШ4 }@k@24cޘ (OW4TЂrSp,~=G<4B-64Q/"T8@0(j7^B;;՘*eyOYs<Zz"CF"59Ԫ"<Ţ`C|C("{lyAatyܲ 4j9ۧcN:!I8F(6OMQn5ILjψf4.i dփ-Ix?hpBgE{Msfn}j:ffDšYDPϫVdCxMt ߞ,DMjUzjeқ(2J$՜;5v5i\(C'ܦ(R-RD? giKDK8;5:ӻ`N]DfQztfAIgִZ28c }XŇA\ wᅈ.$ùhOo ʛL.] Mr0ʮP# M=~bU]V_vިPֱT, |!STW l昴|mcIz_YU/~'B?˿z QA(G3w26irT[uPcjMS,$EPć pC$x.xp[ߞ'|M`GGݧQ7NacAG2kTiW m>i;Bj[SB?-%Q`9v~ɁcPa۩ѩe 0)>tF|PUبCT\GV]C>k$L(HMgOWlFMOd U 86gn:Ut%XI'ǐ#et4bטV x2aQqˠx¶I?^r64utj3k&ԓ;"h~ cmz`eՉ6C$2bzCD (c: N N  A rZ ij(vήǣs͚dGxamLT77w:%  󣙮F8JY+8d_wr$ϽrT[s\~YGtCF];IgO):࣮fLi{X[wnpAآ2khkF d.TI|jFCR9g5Dކ 0dY~i.#_rt8St@{ʈj@qTcbO\=PGntYYQ83]8Ier'[^+siM\|uRw:ig)];bA%>P$>"Y][#.LE$eYhXwxhFPj%w.}:$7hx>QL7:vefĤ,v"CruJf2iS}bSV?(5UG ,rA;(!IHٸh QAx֦s=dARaCUX@Eir<"pt~)M ϢA>!_["OdU^HZ7yH}0wmy7@W (*s !">B&zx4? I @LUڮ&t5#kTIu 8:;r]}.?.?QC&Ԭ9AF=0G}l!UwPj~~ҾLMMK?2KKK?2skKT%cfMymō!cm u5iSSG=jw*ƥ Y"{JNwm5>Kc!ӭKŠb㓤Y)EBB-:g}h>ӊ| `ǮP$RJEGkk[*FS.fiK'X<Zؕ C+Jx߯bg+ z8Ea\UO9oSk9SN}\]? 49D~[[٣i;\ۢ>Lս>;A:T1 ӡ9`{-xs̢;fpcvL>Ӗrhd-` VYh7@:TK>{ 2O2|#S>$Ct.鳊h膺GG( EmߣzePLR~>}pL(қATUYXXA# :E,$e3jIR}lɱ50rҀ@MZ rAbhl-765%pUx6M}Ǵ]HSק_7dЉy譟j>+_u0!h6DJUԘ}^*RebJ[K{D]}z Jӆ4ݨ010Avk;!B E ߆הZH(|}db ;u %B-"\W3E@XmllQ}N4kZa\CrOd vU#t}0Q߰EG3Աz*`7Wm#G!iEc8پyk9)G|iǩ@?Fy: -9>nAF54Ďax-^4|yGIE .Zœ&V حL}Gy)%Z99ho_MGt]ﳭˢAַ%0}S#Q{}E{-:SүFGtɻCX{ac^RyEВWww[o{L-A\% ݪnv5;+{xpkK5%pw _ƻqkww#u pHYs.#.#x?vtIME 3k8 IDATxw|TU{oMLz!B QQV^Pwuum+]ς EDTHo2Lϼ`2χ=sn# AAA@#AA{D/   [qf$9'QR|;uKO.yrd$MiWYoqr MzrN  HQը0+⸧$Fxn(X$dɳ^u^gj~JMМ34e{c]N6̛U/WJ׻ۃH7\6b愫3\8*i@A9II?cז((W68:NrO|]Pʝ^n̛cKkG[j?}!  rZA(>wC ZZ\}  IG9go N/s"\69;+I = x5}'$!yM7o50 >l_dƬSTe6ji;ƺt[϶dܲ}5->9}֘/?)Cr\12+f9h`50Mu,?pǞv/yt>f6]=-eDQ$.{ȴ^z#/pEE2MA_oFOQxyI˗7<;olmS  ȩ HxA  r@6k@wJy۲ }Y|J_}5F{(6rP!Mme CRۢ(=6<#ZwWKz[;uhZEW7˒]?uӨwVVtOT-RGO\?bˡ׾>4ip%Pp ٪ieJrI;`oMǗԞo z@]ž͑6~,/V5`8@@K+`xM`mi)F zaIFǿ1O\?⫍uDOyc]  ȯX ({<u̟}͌OR`[~DE> E&y ;/BD2zy] ; z aS A|90;m؛pxa]3,7޾+/  Wxu8Aqg)z[i˴i9iI+Hx ?ݭ_ujYUҦ:Gj ;$!Ks:R O~ƠjEp_vWyʡ{cc䍭ʋ  Zvmtv%PFoa) M1@jlɧ-@VFm$(3^`&%x⤴ʕi#wQ/|ɷ@1R*:SO( 3~ʽQ)0G*tPEF!hÿa_kn O~k켱]yeA"I  2 m׷iK9G?< ¬0˿Ǔ$ rK~jl}[0Hh,V:?ӊ:;+]d,;'tTVR_5 8f@vV8#]JIX`DA y&}#'eYLɽ}vѷw "٪UIM Ahlױ\8틡ŋJ*[xaپ'{ͽܣ+uc}'_Lo?Va+s?Whz&U pn[~jn#}JI!%mV(&vZ%Nqj3CF ֫z$Hy Ĕ~ySAs+AĨNxh7L9ɦMQps詵)֒?M"ԫL5,^3GDȱ dqe1LYx˴k1sZ=-y{4.0T)U7zY|Ғ{ ƼQ:]jhEl8dc}+ǝP9E=.Qڷj۔.I>8yjۄyYiS@QiCPLs+Xˉ He]-X ȩHBW%p7xnxEIVxE*;v>fnyS?O1,eڎ tc3(HpYFI/ l]?'xpW6]߭8r ~;&rДiwv4QloK<"k?zvU(IOcE*EI8oH(HebSg+ ~dF͑;pՑ22^rGag|Ҹϱn67OȾ_5tߟ+Дwʘm$X}˴_9ᛃ4٪IlSc2dPҽO3niӗ}"X8,m:/s3[4)"v;iI}ⲽ\~E9i3f% j]͒Kg|uT%؟m?ٳK}.C_i[,}HMn|ĩڬα*$$pPh$-^SqjIb093VWj寔VӮI Sj+ܻL;E!ބ ʗ密ػEȸYucU6^#6*A8`Co}"yw7p8P$K8-wniһ_YMvI2*Ma9T8y(LwlݘYxlXףޔ76)yv:ڒg>:cv6yygܪ'uidνqٖ@yм&h2mrzc-wnP6 Y+Hh%h|Rڑna*%Eeg 2$mR',,L՜n϶ ה] #eR^ִ|g3 QZvZiSy]Nf^R'#ڪz ܢ>'$fUܻFMU.5T)8%|؜Abun5ϑ@6H'_jG ރ$5!{ٖb& Ga}½Dgi6ܳb 6hGO|!;okZέr&]W6 @>5&yjkO+my[ooZQIpå-?zFo)"fҦi`Xt5,{M[-쭔c@_+I"APc9ضd}.!іd}'fR'&ڃ]ɾֵu}j^rl`yO9Sӱ;ZRd@{Oڻ_')8ΫӪ Ԥ$N`CA JR TkuyJ#f$G*^=J(Nfڎ\ o )Mx?(Aɘ9|gDF[fh>Mq{+;/iH$l1ozS N+ފgy7v8%s"v Lu 0j:J-dCVbmSr&g1 /r ßď۱ A|2"mfehk=0t  n A%+V:ɻ&B !Χ9Rabv&i GX\[CoRrE]vPPaGRpmeebZ+dڼfqAK,AKJm+@ rj 4pQufJ׶ˊkYžq꽰"(:=+oqJVR[5mq 5J @X5EH|tn|~Y4ej&M(sŪ7c]‘'Om;K`<%*3{8rImռv& @]m nm|_ft9ި;?f.|Jd*oqJGש_ c9۷t٢Io ΍/Z2m|\hgZiX&Oh (m^sQe+[t H5Cl`'׵j@&rڃ:yXmk]sG'e_W<46sE*3Eٖ!pm#>~CC$-z_5VVd Ϋ3뤕&N2m4Uı.V "OjԺܠ.;82""q9bZ2j >@vAIؐAPȽ+(Rygyއ^]6|"/OS HP11= RǼ1o?<YCIlAO\4)R)}/s/㑜i. r8m^{'~bV'=7s;AJR,myjȻTí'Nd6|-pQYkqY3 o_SOa'&?\ښ#̕W*2ms 9~7851fu#:N ӦIl|UF @&z ~k5iZ2lytsdUhIȑwKqu1Ά5Xm\5[y]un6jt3间rF rB]w ޽cݲ+ߋsl$N a3;vb]"}/\ EVhiJ}7#M:Q 7}ږь IDATgO|0yՓ3Z{   }A!O->ƹE$&o?~i[]Nߢ1?2AAAμ(AQ9ťҵO+FOAAw9kx{=A{Bg!   ȩt>AAL Hb/DAA#Ht    àŜku^>B#W]ICAkH}I FP-[vo}jeЌlYpvM_uu))ۧvZ4>o:k 7[w%$i_ fNk7!_؞Tj.M.|Z*Sy44mc%^mV "C5,[y (QdQ%sݻX}$KbV種2vf>qlk7 U@8&U2m{׿}y3A#cBXoM2V _>`.|H `䐙Q[-}k-߰,b߳jw)rLG6dqhL_i& s%~[YS(2c Tj[x6O¦x["PSB (^pE er7~6G;jײ__Ԋ1 r${}=qso%muWm9 ^C9j svI[vq޸{VNR7uS;rfk-_QKk/$>^.B]%ChrIٌ [膥ɡ5TCtJD ǣ@?5l|w[9>Z? #I(}.Zc |m aolʝc ^]wsazs-U V}=펭7A:kr,vۓG,p,>cJy?{ױl:!qj6+D;sJ< D&m^Kvu(̉ZlZn5jb]^ mv+TOQ M7duX#­*SD, xoU}K9Wk[kN!^@s%al~ʱ:1Ԩh̓#ݻLֱ.Dyn] @`6 *SK&9cƥ͚pyYF$…[T升صp u]kQ⁲:Ehjο*:9/.N*.8pݕ~ұ4MK(,}YǢk/B$(+fM->է+, M+{h~Q6'(~'l2(V%7wNklYJ38}uҼO^Zx TN&-I/u] ,s?<$ SU(o[ks|kDu|kX% 8$ fS@Q;/䔪ZF[qa]5N}n{aAO|'pwzDJdD 1&'t͑Z8;haOU{rb< Zm>q:Ok9_@`ѽnY"Kԫ1ö|xx{isŘ.o^h5MOͿ&~~3UR2.ki_ogQASzBēW G_v$C %>R±.EBe y] b6 wx$AI`Я 6_m9Pww9h϶ӱ%VTmYi@Q\Q$Ym6 ټ"$ZT$-&Oo}y{HbBolZ S]#O8:/J-.ivq+&^Wڍ_vLbufLYܲ|v?W3}naؠF"ÌzAj&=UIN+MFJ Ei >6Νh yFjOiy{İUc'-\<%BADj!o7E:l'#F1И^7~}ݮ{ *9?jvޓ䓷9IhسQ˚E]fӁ_!_N"OͿ2Ӫ&$It !Hx"1fyu=HYy&urur8L˽dM@'2]ts%$@BFh_ D7@*% Lw~-Yi@Ѫصw;v 9מIsF$,_iw pQ 3ɧfăy0$NK7HZfQ#y ljr4 fJ+YZI$%*S2Ng1Q&*;< $ s1c\\.ؾ;DQD|U$ ]"AH2붐"ο:B y~z (TZs?+Qv(hM*\%p ޾?~>jzbbZU؏ɾ+-΍ʦt$_ZaZ\ꎓj@]%"kRؒ$i*Ɩ(Z:e-7:&y8I}7RRA0D]A? uRA&Gc6RHGH9|UF*_v`wXz 9sC]Eq{oD]Wh2uNSѝ:J L?ɗR R;bF"ʫ،4e|uYe-S**kN`̧x%=U^(!ƒj"V LWCA>:8捙=|_u&ZgQ}mu2sсZrFE^RpxO'9 @u}QƐs*m/_l6рzV`L)]w$)帅UoZY{ްԁv-O7 iœML8aϚu}gog2“6չ"2&-0mKPEJ+!*zm\&-md] ›FUtRa҈@JJ#|_ƻw-% &J'&:={ G#?`~.IayOЊ =J7ڤiV`ڕ*o2Yd 8 !Bzv&[Ԑg5fty#J#4 },}Z7b`qu5驮c3Kfw7$R 0($ҒYNZI $uJ9;wwًhs, P?$'Xj 6p_W=Q/3\6+u-7v~afM3WT33&ǛuL.nJKV%+S8Z>.R?LyiJ}ny? r6$i'PJUܻ:|-}@,Ie ~+`ϺiPuSn~>UbQ"bөc/:N}gs Oy̹T;|Жn\uؐTmBɆOSmړ/lA6[NI^mvP>~N" ]jP)ZLԕZ~I|&9/mYwP׶.Yݱ͔~Y$m?Y!MZXAu+MRxuTw0f3g8BM*_YؒCMjj[hwdWYhM:k.o_P`ZisDصoĮcWnMqfW{ݮuZ%益0Z'C;zgؙ╵OIO1nd0vv {m&$XF=u=W_X_}k߿jv@j TְrU> O芓.8p=ᆻ-3Aqjﳯ_kA B$P،MRt|k@Ϝ'>p}]=ڦV$1/W/ej#O~]>|M3+AܷA~="l1t@HC7r<"lW1ί$  $Tp’$0AA0 AA|~,jD?    Ml}AI#  `@   $rz5PZ̹6\'AA䬤s R?iArek>nOF q-kn_Ynt!n5%~nR獹XgMaC}D%&Q v׵śؐ/N}aslO*5 &Rjt_>k-zBv<)zoG8*wn[HBNyZKڬ D k7X GAANdtk*6|߾?\HІ[S̼cB]DpcDI#\tjl]#ple[o5mU#7ON0c>9'Dc.H4a6/Y{evRGcgCgAYbMt*m-_Axs'kaSiwq@A9-=߾8wS6p|ٺO嫶{O!u{V9uV$-8o=+_')E񌛺)9|[Ֆ稥5Ul\{p ?rsbк?#OmɃk>EAl&ĶY&sݭm:B7,M5h䯡ze_՘0uU"AANǀ^ID!sct`kk{;U;Gooҟ_ Ӌ'@gMnR*%yU@l$d Ywg؞>fcp!SrKh,3|%P%"G[U-P&rߝU {Uo1N nʗ #= ߶6c:+#ݻLֱ.DyAe4ȫ O.5w$TLڼf7Q:7';)ش.5:vt(<ӦjƺhH4bѪIZ̿PZhIۡprN 5lQ%2.urXH_mUH9=vAA~[ H)1n譩;x55…SХT5c\z#dR/yۓ]2)E)Ԛ+]fZ=PomS約u6-[U#UYff6y7$bwvILg{V%piso\hPRJ++`?:ݱܰ4cIk @HUg)M\wGzK7Z| U): '4q}uCm 0Ne$uiyR+٬N:=~wŰT 闶aaYJj_ ! cJ}rhJAIQu_D[?''ƣVSϸ6k%|:%Hɳp {yɇ:F,T~;]R@!6_m9Pww9h϶Ӵ5{[VڽjTwmzv%0=J)ԩLn$'ɒ~I*9nV*0:J3/)@h"INJ6[W%B!&OoSnpP"AA' Q.u-Ajά5GИ^7 AuV'7Y[zOOx8'aG.kzݶwM~ʆ|+^;ъD|ò`GVkMvՋs 5>ٽ/ױYqݯ "$<vp"K@ |e=ER$V@Fu@[pu%)h3I "Ȓ@Hc1c۬Ni9C;:-?NAAN !n y/–JP~V}QКU8yJ‘ֽ}|:xR ee:^# 0Q5O^- 99D/?_{=5 ~=RӶ !tPg ֫O4, #}}me9B@gϟ"KI:@MW;ADٺOZ˷+_;b1<) %L_2.RC[Ip`C>wSE]b\c ֙B Uo4o|RB'$NiԢ|e:&KWrnYsTmvP>~N"Yݱ͔~Y$m?Y!MZXA=o&)a:Ҋl»xY^A`H.y Z{{NL^ VX! 20Ҝ6 !qYm3f%}um  QXX^@>H$H< >ѳK!ބ ʗgF>zr_1AuA/eId-z=)2  `@ bp   șOV˴AA H7s+87jAAN8AAA HAAANJ97&DW gEٯKC7nzf%νp,:̭ӄܬy3a"IU/{o:$BlxF҂9j.i?|DZo4[ 6ݾrWݛ5uȬ[MI>ۥԢyc.YSؐiߺC?/$1Nb5pjw]_ 6R/pir(Foo,ֲV oRi@tB9hk-jV2Xi`>AN9ӧK;k|:#d['2c Tj[x6O¦x/GG [g'nq/VlEǿʓ?oݹꥷFW o|p:g{}=qso%muWm9 ^C9j svI[vq޸{VNR7uS;rfk-_QKk/$<[/s!ܡuGwQT?3}d$ ! HP XO!', H% 5-پ3;3?&l@f'لܝ{=;6*3&/HFK׆ťEwm`F01pW7]R櫫 q+5v;vG5P$eiT_[ ;ܼn_x ,LVՅmO |htL"ҿ+l+w[I0)De/+n{tڰkkk,ՅL,Wmw6\/c;NfvMR$S:*}|i/+N!wmpD48E U!v~AVAW͓Q@g!]> | A*SpfE*!6%[i9){TOjz[hh0#&*1ц[6iYz;pck1)U?iTD%=U\ઔ0B5c{]jIfqG7$ѻ)jheFҶ>/]gL -Y$91G'vSUk`狚GL˿!!S2?ȡ3m7 x/ .T^|[2x/|qŹ%7E*ƝXԮ*hݴ5_r4i܀j 754+}{uy1Kys Zx3IIq}+nrԩs +-K~N.tzr *~ظ/^v7nבC|a֐/r/39qt0c"I*H@Fz҂ƩCCTvptn@'>|kgNVQmnݞK_{{B:vH7a(Tך{Yd2lUPvX,8,Իsf?bXۏlڊ%\T wXB4WB-%;")=T"p1:n]Mqb;k ŧOk:dJ]ROL l6Xbz ]eҵtqHUTu'kuȊ)٫wҠ޸d%7qܥEU[v:wԹKw8Dʳd{oP(d0~H|vcs]JԃӆOwJj|˂eڠ?]Ԡ@IY͒ 6삷>|>|łܡ9J=mk}?a֛ť5e#.&W<0/"|t|>X䔫M0/<ERK_NT)XO_{Kt.3 N~~9+7vOd@}b>5Ow۹X_V2iCw-x;85!][$1ٴy׍Ecnd/WWC̘ZpG_,yf$&3&]W"؟GB T0UfHw D՗5% {fT1hug.]\@cc*j) 5tNӿ|<ʨ/t0]R޹h) eȵdHo^3k_OEAEX^5VE1rGP7 {f麎L{†*^*tb4x]( :jJHVw@Hvh2,3J&ʋ׋D\fޡ^Wr*oᬒtc֋!#]6RkExT=Jw$xH0Bʥ$C<ؕ#bs= vˬiCڮ J?N& #gʚ6qϼ%p_`u$].:.4E仪l}E#B4>\/Gd邯~i2 & >ć&zmƶ^ K Ac*y-vwS|;b/痏ѣ#炷`02wh(~oQTV}$Iܽ48Һutfoٮӧbd,3&UPv-HnZ١dV9JnQ^oΈD;R~{*=%n`n}Xjwi9&YjsZCmaӦNm] 0bHf>A^]|q-8e2쁰kw{eg] uK嶙ƊtC+8[M\ҍ5an요(n$ IcU3A$Ut1RW{ mpon~c%]&mEy)l2qKDgU[EH]@^8EB!=u"qH UsG$zwZOjkev׈C{X}Rɢ@#Z1xNٜ]NDGj EUIqqQz% R!Xoi2߬T''4 Iĭh.O0eIO')*o!@}=h.(jxh7-/.=pC(3 X"¾ /ufv7Bgoc&n<~]q;ª` 3&jsZpa*_n/Z0gc|إbѡ_^ۮcE5ܧa1Ҿ*>AOPw [k?'Dv{l;.Yhs65m &{?)#s>KfOQʈҫ;ܚE(&8S_vJ 5$%fHg-F >$uflhc̷a1ӫ]p"١J*~6+Tz1)(|f d$" 0e+`2Y%Q@ஒ dC{XѮiZxTԤji[j|pH)kpHU$nl)v!P]5[.AiZ6ؐ`602Il.6ZJ)D{o ؽ)1S' YΎ}, eo߻kn7'BBF`x%:cCQiMĨʊ%wש-)J' N02߬mq41>0 MIQLݻ}v*{}wc0o=/,ᩩbAxEB:{M~P]e3MS'7}}̼3zy?B2{xUPwR7{@z~xt,HɃ59T(חIb0^]6Ke~SX1[ KB '$lrQuS_D?dYszYynDnD}Xfq$2TEO)nFF׉V-B;ȤkdnX)ʫj/b!DZ[4fP/<2 x"qNzP-WԈc,2.bSdPc* Gmup/E3S2~ۃАWðs8>++|d yk¼]'=q3^?9@ɉQgr @Bp,qW*7wdQzub*fŒW<7o~(Wzjބ,p<{Y v^yvV[wپZmϣ]v{<=PM^D̨DTs Mj V2yصl,W|쇖*B  =aZ{m /ṍK8$S<•}S(<<<<<<>o8םǘxIdV~YE]d ܆ οt6EGEorfxxoDzigr Jx=}e㓖:'D&ڜyE/3fxxo aHe!xLf۫~wHxxxxxxxxڃpx^mF&Sq1:^<7~ ϭ19e5xnWѮO 36LyKML۝mvV.Y(|YѺ0_SڋӆO?_MdGtyF&6+DNO? IDATeۮmoC~og C"t:Cl%_yE $D}aqԎ6WƐ>a*"f5 2 J\]>%݄o֞qk!5P,`BɘE9v- P$ݪ;DaUd$AeL5a!{oȌKx]7m-}GuKl R>J+3o嚦hzIhC,ժ8{%>2st0IBWu礗aO\3;1a[ VG{U #F ~n#Z%>/~{7w3{< 5Z*w$h vH85 ~pH/%qqߋW0U2Mdt{qW|+ChѷZu:!lQ ;"7WCV~J&@tulm'>)]V%="K'!9!dRo.-+ic{ v #E侉<4lw8v븑GMs+?p!BƲlvὖ,k_5녳  b V!׷Z|Ghd<~ϝ:(,{}~1Qa<{-Ѿ+VmЧiÿa 5nV- mg ~pHr~_l% X*F>yTC?5w(tjՖ(tI&4 aqi'Xb%g|M[V["$ ϴy~!&"siZ"wt\媐B;?[^I䉎{kؐ.vm[wPk>uU &PYfvO8 AAǢn^V=QSͧB5-Dx44|PhCӭTJ,Tz=uaIE4**.[gpUJ{Gfڱ= df{tM"P[&P:{x CgNsaZUMם6l9ıoxf=)1.7/3{7ۘ^L7zZ{bM 9|QSV<3>=iw8{׿r:Xk/c"59$7a(Tך>/O]#ϜzgDxo|q6=>)kJ!3Z7mͼvUKx$=cPxoOvhF5wcaI5%G-qLXq#{S)z'Eyx<5csDc9R.*gD2%rt}e=[缃df$PMLK kdBIՖS]aźC\o;!=rȊWkurWԅ<ƉDذzqu3JWSB ׋@7^^XD%K 9 ]26a~tAϢe &Ǖ) Q4E1[S] M(R:nvZ\+\"cTA:k"F.Q1=U.)Ȉq:(`xڋN9Ǘ/9_/ێ$K>Y:lNᳯ} U,z|T"^u~F&/|~9O  ,]`^~gX,|{n"0{xh@KCY~xFW*s3&ݒc_*zI1L?p;Eh{~YptԹK7|҃ӆg'-xó-ڜ 6]Om x"=wd?bup/9Rhq|E '-Y<{qn˞X+ԉ9g4Z{PޫSt;~WĨ]NIsVhPT]r0{f:E3I@ ro9&xz]|Kye~A+l+eg_ZӏnU+P4g/߽<߇v, ;e&ƍxzI-}eW<ćxW-{g]C{o9g66`]hODߵ&U_jJY$U n] {0$P}XlƘsj C >$7Ͼ21]2Lws2sZMr 9T\I7GP0Ҿ }eGP7 1t]pjڋ6 TiUѤIBQSEZECaQ6T^^$4kB*$WS5Unz1`K 1HQu*" + N`WJ Tl`:epر. Aj"(:ag&@xڃ1gc>_i\;m#Z'@".~f?dV E/-XCtG鰎>\/;\O.ꇝ&]@tl?ǡi+_Jr..I)1.DE/j\6 8N[`>l#^ەnIRG Ig]K.$*H/[29-/ߣ{™eO=G7N=Og_]gOcNЪjUHK"P2^rcn/+98M,iPˬ*O_\lw;lN6SXRm e⌊Ԛ-v_ kMQ/-wxev# ?o$svH|%$af'Vۏvsri&yEU0o 66.C"t>V)o&H䡲P3gG6KUfb7Ve2l}=v~Ӫtc{v]se4Ӣ&.[/WӼyjkMLK5Hz( U):)U,=O r҉@:Pc]߯PP8yhj^i -EnHC$Q`0<"<сIIQ7=4 Ghkbeݮ*0(C4BxڅmwR\\xu CBxI_`("I3-r~9sKN\tv'2ɹPڥS[wW*dRڜ_Vص4/ 6ڍk#rðzSpr۴ }9b$0yҊ:&#bGN^#%>k "RHVZ7n;zIJ hgFQiP(ת:'8bp<ϊ"m@$uM]h*$C:lOK@sYs2%Mr mY*oޕlD2U1mB$EJD*SlaC2In]kwM& {u/B{KE  "P ))@0;$ba9v[~OT66Z/3ϔօvsHRG="SfKʰTp8CQz{ [mI- t )Qu6ӰiI\Bvm&ArK02,xDm<ikJdFf069-EE$>~!`FTGC|sZ&=_.J0LT@Kf\$q#6hЀ7FPEqO7PE0gpqDw?ZjX0v5bI$JG^=W Ƞ~,!C r~yJNBArx8HlNalTך}Nj`eA*8qY͑v9=\"団Cm0W0r *۷t;)1?}x(z)tވPMvKTZQKs^us*;IU6Yaq{$=7A?~&7>6BS3_Vglx wzHhfա!KϞ9eXb+*&% Kk"VZfHB.޶)@@tAqtK mHϔ`hTPb{2dmL$W*HXk?qec/o9- D2%B#0D &Ƥen.>;:*Y%3&C҆Fu:^ISH>,%rqůtEh4}͘9uVy4ArZ,tHkwFAEbY&Q$Iu1]|4Ef҅Ul jVE풼nҀeB. =>'IY+T»sy~d(]ްWscvbƵ4ls7I~bSRH"[_^]#-17ǽo7e\^.zyF{lXXXR|rhYOɽvK`/%=`0^h\/l(~)t^wR@ r/5)z]+ۉ3y`iܭrT''F1$o͵\+_vųi,[?2̯WU+O{ӍRefO8{.k׮|7*iڋp.ݺV`S8^=BkXMCKx`([rsiO.X} /g#XmOއxmyP^9S/*\٩[^{{xbݼ6ڌL*~ﵹtGQߏMs]jU='nysxx ;<<<<\n KxUآ得]KZ/WVy]vt ㅙ:K״ׂ+!ṡ8Jdz[vւ<,,F^E3%Uen:;$<a*"f5 2 J\]>)򱤛wBڳ|>q#dfd@= %cR[pEϷ4$B{Z IDAT( b{qW|+ChѷZu:!lQ ;"7WCV~?:OsVLNoṙI+ $Keȧ>J{'&4,.vp[.6iҳ{ ,.-q+QL6v~;Yx|+jKDҐ!BS%\vt`YxBҬ\P5\{=bEr)U]|[}MnI$U;rq]BQr/>rG 6F*R](rxW{hc5?8fdf${(JgK押S?ݐ|ǹb-NFl:rUH}-DPUPU$DGԽ5PYlHO@;5*FUJ3T'YņcQu BHM`VZNh ឨ)SAh<>̈ItLVe|Zo*:q0$ZLJUOQIO3*%̽#jPMxXpWWw$38ܣnru542sӎ N'⇍1A X0gܸ3_GMYC& z"ue*. Igߓr2W|!S2?ȡ3m7HO.I3&m ޔ<<<<<7ljGT wX}WB4Wf>Dc]!-ZwERESzD2%#.clu =$3&j:dJ]ROLЊ=%D5LvIJ*ӹk! Wb/Sם'CY"+*@fީE:k $ tX5X/{Y}Iqe|CTN4{kc UB=T1+wp(bd=wuȌiդ-Y'L١ #v!*Rks,积å}q=)0ndӋ;I-}e"\BCOηڜϾ+~ڿ?ܩs$F]񆢒v7qܥEU[v:wԹKw8˛s%01D1ERŠ培w sK՗f8w09mc@N~y[|[9+Uz_ε=bRf\I.9aіK\{WBw8uYHx+ uH8 I3[>Hufڋ6 TiUѤIBQSEZECaQ6T^^$4kB*$Wyn g4^ <H _#h/ ƣUb]%EBW.%@%@|uc]fuO@vEP1uL5qeL"}Ҋ:ȾPԪ)zy~:ǜ\0t o} ;kDm&6[$rxxxxxxnC"t>V-$y,Tnj4l~ n,e q/ҳ{U0UK/4(̅5 wٲy.84W[kJdj]ʨE3.oGo0JׁEOdyrMo@^N B@Bd~-Eܵ*@@-7GMXm)opD]%"!XqLJBIP8  @ .DvUoqD&r.\,1m/ێ8SQպՒd (,N:C%REvN00]N6X ﷵqHM𤫡.mք!K] u>8bp<ϊ"m@$uM]h*$ V{>u.baso6i9 -iKhRydV_&nR$.2xHV&:Ub btIjwZ떸k$>A彺O!=Lp"gsM@@KtwH&Da@E&q ˌIPnron$ Vǜ3e#3νtvnl[ty@"r ժ̜Lc`w&T쮋ye>߆7-UäOn&Rn& \T=C4N=G5zn-sm撮!e0*T?xm,1)s@Z$Pq AP\^2,xD]:hLB >N}3~W҃"$ z(+xS/p!iXe&&ne Z'?ć*l[$*upy~} 1 ,8iVE&%Xtr^+Hoa(%K"WfuUl0)(-TzYQ1d$|$eH4҃=l-.w^ UHOPFtWnS8}Tm<&{̷ 74xn)م*o 2.TzJ@EZ]fW~\ aC 9 f? cެl?RZ^6qLM bazj|~]{hLs_T,zՄ*><{n-sC^{ ҆*m uP̙eZaد݆uh]d:%E5f̈ Nש##4׮@ &*$*u0&/(sgNnyg82nzM;P"sǾc1^y- 6䝢fOH/wx85`*lfl%ʻ %\̟oj7 KB '$lrQuSvDdMe>70/9FF篈mM"IUJBidq.i[{(~_-B;ȤkdnX)ʫHj/b!DZ[ ԋ:LHhc`˕+:<5"5̤ 6=Q%y!-ߺ`~q4Lc(2ɺ?3g=Ky^x6MÙe7C?_xZټ?1xկ7a; .Ϟg?n/F=$=>'|k {ա!>w?a.q8liw8{5ksL(Ock׮xz8 Q AFn65#dȫϻZqI7{mbByxxxxxnwm<M|lyE+;BPxxxxxxxxxx:'8 !q<"^<3%Um E@XZTV+m ᗲ ~ _Q.v[*o, i'gCoe8TD)sTH%=]|ڱ•|+8$BiH3ɽ󎓪:s{پtvҤ `AQcAcbOcI4F71H( vPRwYVNwnevX`?6g^)]7oSo>$1 bh<֬R\O:L$<&ox- B0 8?*ֿT9e>cQ[(^ KSoAk`KS݋cXQټӗS4[WyUꬹ%snY|m?<9wԌY: DyRJ ֣},`* Zuj8.XpS۱ry^[ؐ x @ S@RXͳ4jf)K]l/!_ݨ i&EtU/ ٨yK{((fw@HC]WQCz,aHd[_J;6gI)䏹$m)G#DBk c blYZTT3j onL`CB H]~RO žxkIOݾi-9o x*.GrG8YHpj>ezX J5fG$f{lē l=,ks+q6vMYu/hE,2fթISH٦&6R$À!}Iطd)7\poQQTKMݰHwߊUe'Լ\qbT@zNd~y߽ɖ`0dZ ^Cjg@_W8lPPY^3N(." 7tKdW:F`xdkOG1 _;;IE q$`2Z'hX܌:;h69&!U&5.RҸUo²yH@z = Zo=ժ 0^쳽a3!6F;(8+S\tlbؗWT*O/r2a"RI9{ WĵŔĀ$}z{oqLH7gM_@ 1|"!#]Gv%ȕ*{^,6K씢utsc` MFͻ x&j^j<i$ghDm-[6@9UCYn ~#LcT2Rqo5kz.qT06LaT}GY)GualPKxw{Eh0 DbԤLai/;T ؀,֬2Ow7su;͎֫e`2 f0*دZ٩@:5%CuzET*ޡT6:I`^jU6y%"}^_.$cC2:Ņ>@ 'zhJ툙7)#HRcThIPC61"f,G?vM7?}[0Ҩt$f)ۓ XuB3S qie2NN8{X֓,;4pH3eB(AaI u-ڠmz_@ g@2l ]c EeT}иxA9G\T26O-I[0nNk9sO*݈Y7jO{ͳA#F .땏R!;1dUݒ>^RaJ.H+CZU>5XtrQc5RT]s\&&H|̡ZvP0hP i\1k 4u/XK dO4q8)jHSB$l :Wfіɝ jUi )˔>UXXH:;aw5+9U6tʌc‰[2|DveN5Ky+WXYHGYmVG|Tf$Ml"IhIIExHgZ{F}9M]Z>}z@bb(<%Mvwחz5#HrJrŸ~s5H( >_:I7c5Ni%Wj.]<;a#p}k l$ cuH쳼Yڗ 6{21`᏷+;Yz$w(|;M+kVul:쐘 "ꬹ7H3amȽM0fR6&k]e}&7v&^I߮:Ъ}mNr( A  6|p?.r΅]52Q9Laear:R @@H5k׹Ѝ'hAvQ<@ bcD'Y@ g ԥ@ @@ @@]vL 6_Z7?BTJr{]3{|o{21G3F\+@ g5 av IDATæ/q awcSjM2hM}2CVgsAԘ*Vբ(H/ Sצ2TXglܓR4~no.W?lxL> y\H]#Լ@fܔ9qfygoQ|g=}Ůw?oq]|p܉f~(p#"ǟB!'p: r!EEoڶՕ4. C@2@WY*7ڇvx8oK0kdi?h٣,ci{q +*7t6~H k 9uXΚ[26ejJ1.-ɓsG1 Mٜ'Dhzol='_p>%1:gHg,[9\}D N*%˥ k~R˰}rw@8J<7F鎾̲}/i {[zo 8ajC@! ly,Ym5JNɔ]7Sk:{"ݍ:{ɋoQkA]Ur :jv)ݍJ;tu;??8R$Oi:js&BK"@[m|""[8xfjٱy@:k_0g|mCOʍRA{W/6+NDnA $aosr(TSaOKKWLVhLpzSj̎_Iٜ'E?Z{ Y;ds M!eSaaw(5=_\HV I49 ;e|P<,{NU,o0O7=0LҐLsaYBkP蜫]{ lPh#B'X萾$ܹ`)sGIps* EF ~'զL4Y&wM,!ܛlTRNgs+q6s8#u]0wNYfQ:s9ʑC8LТu%HlΞ>ovCO7ޟ.sZԴ쭬nWڵߗ$r>fd/w?~ dMmϾޱf&-*^`Щ=Oxgˇ.wgX?l`68wU[񻮛(&{W!S̲a`\=Z3 t$v&0ue@RUo²yH@z = Zo<:+ڪqBWs.rQʦUζON 7߹xrX 㨈 ;#>+,lKoj5 ժ Ֆ:[dw5h b@t5e0~yZ&V9Cqjя S<_#QXL,%R$>y2yB퓬W 5+]il|b[fwlDt7z"s]~ՈaX^m)7t,J $jxji(~O*{># yֆƎ{~eG/t[2qdr3ue]elIݭ8II V=+ߺfL.^Yw<|y۟khoknj'F^3n]C@>#{J GHn=/^z%vJQ:9r1{0a<KmUJo{x43Y"dk嶖-UV\Vs 6fM2 bJG{IUnN= ؐNQp"a "-;bJ8 L&\Ά-kjsYذ H1 Ve`emv{B~('BMФp 7&*q` ++sȍ#J*BY\X& ;&-,`|f0Az;ժh[aa#DƸܝra4hr M_0iͶ\]G|*ϯؙ VG1lfv:ZrwNPRS&\qw ,''ot@~hsg[tlF$J@㰛74u@ `8>{f sLͯQB x6!#U%z'OؾqR2yVFr6@x2ܜ@{]=}hooCΚ2tMj4.G|}i$bΑS&1O-I[0nNkes$U );Gk;V"x2RΚ5 r)P6VȥʉFmzu#5E(`\`N <  'Hos`E|ZĠ Hܝ&"A1$k>)}C鈂Kݾ`݆ޮu "X6[,d.3~܋ƏHoe_9o"A#9}2[;,hs Qens[A>/()xn[w{̓{>txnz`Uy9"WsMIS#8P0I94/137]?r@ jL%qW*y j(waK1?rt@\|ߓyw|) êQG;^9fǚ83da jOe+_*t2 oK(>J5o'HMo^L,ܡ.%!gyk_*HgdnCumy6Յ*'1\ g]׺,yB'NYNO:cF{=y ;kV#Vq.ڇ \ $6@  @  @  IƬپ[I+cB%@ ^1BTݍV{/@5λZPģ{>4f˗?{# tƜPGEb[<3{R K>@6lp &s;jv6GǤȔC.v Wi"֚9e9/S!OMojQ?<&gr{ʘDž1B 4kVyMl@~2!ٍ>دG%@ $ Tqsʮ}hNJQ /A֜=z2ǰyC/a㧈[WyUꬹ%snY|m?<9wԌY: D%+u7WxM1λs^C.Fe,)ZWV?kJЊ&\^ՐUDjG`Ϡ@SY:S{v4pqڎÅ(ۨg+Z}@J@ $-*}j`C]=uO^D-mغ65d#WU/SS<{zSj̎_Iٜ^2'vE?Z<ɳ dqo1 ss|a }m3|RhNҧ͠h{@;,b86KC NX,ޡkH!c8տ-FgjgYdG%aNerr½F* U~Wٳx``( ɴ<*􁽆D¼< Y, AްMMlܥHLW K@ɉf\eVmOњ+ t$v&6~%XT钛8.RҸUo²yH@zu{͸~ԼnY%)Ԗ#0$zౄLsBpB,8{%4gs 4^,k2L_^P2)дi0 %ڬ .(2eYVv^ᢲ!]`jU Anf/٫dlnXĻL{H@S;.v]ٹKYaa ]ZkM쳼g*I_ x#} "$ :+GоNRrӞ}\)E讂;!<Kz$I~CW'ON'9CMc;ONMv=iܦN p8:v`ݿZƨ1g; }G4,鹆ל|cͻaJl@p^Z&xJtRҊƆ 04cc6:ia0\ VN"1N1ilà}]0dcG^nbEh0+1lƒd@³tXȰE1\H5̦{,\Sg.mCl7'wѸ28&NŚva z8r {,jfؠL8#aK&Ԝ=~N/Y)pJR8o 89?Ul;~G?ZPH'賊 ocZr9[ӥ9r*.Vm;9yo,%Ln9[y#$IJ0,1u ǜ˕Ot}ĸ贉ְoɷDneMˑ0F8`s;XJ/q1BLf6d H7R8o 8n! qw>yLBgSF__&0)ns-7jGSg79=,QMRCL`J7z=cH~lCz{ar,%obg[͹\ Ɔd_LJ6?'#]jP(r:7"FR߄J_ IDATdKZvPL"%K ;pF]o;Wt`XJ!so qФes|21<4%-)yo) H,ɵ5'^7z\c?Q)g>bk %yɥQmKV B,24$9M]>})d%@ \>|8L9:7In*uҹwXK9:vlu|_o0m3}ZHY$AKɁ quQP |תߘx |o~ͫ5TٴuOx[C ;_Qbȸm86$ gY]H6<_ja"^J#ph j锅 +1`b8Pm R+GYzV]߮乔J7AHݟ2F%x:M<7'D?@ `@@ LDSX%m\pogP@@  8k$g٢d|O7@ ( A g+8aв}@ D@ $@ $@ @ yaB]Y}?V8&J|q 7GUrGXvQ# _}< y9.|NX_]_*ߗ' r틯zkN,[rv%:n޶{|!JtޝւR&my7t9eϞ!)&]1g3TjѭҒFoˇk K*,&>06̈́\9{uRO쵌}^دG%WKRifεS.)*>T QNeΌϽ~Lqqs$) ob,MSvC;V<%b~4U 4մo#?\T6o%lꬡG/Uc:knɜxzz͕[<'玚=b2KGA9lZ1D_! Au?Z0qi7{+ ٷsoZT|QR[7x}w xxgOQ:gQw=ò#Y[^TΝ,XQ]NGs:yr@sly,Ym5JNɔ%Z. 6/n O^Դ"X G ϗlԼ=_QO n$UڡӮ(!^T$yxLӁ/XJ@?:+NEqP-& ›[-:;6ؐ"x$H} 8;6x",ra6&&º!t$aosw()TSaOK<SoEZKNކ`Q3NV1;R{\2Z1ýN7KAh>l%K(֚=?/ ܪ6ӫ.0G5-f@asvu1/ AmP mةVUYu/hE,2fc:\ԩ0;o I0z BzeWhLX<\l3l^萾$iLX.B7٨V%X<`wo% CiH幰,P5$-nWQ̢MeApm؜mOnb.E2d0RH_ F%xho?~]Pe~zY]]5T5tLL_.]8~PQ7xᵏiM0nؽ?]i[Y'ݪ?l`68wUK}.rr#QqfsyKY^>N㯧T_etpcz&<[` ͞>k.β_z}k]`ʯCǤd9E'_vɅ+Vm9y96?KWBF?[T2S{|KGol;Ng3/MںA\|ťC/vv'.vJ-[okM_L?o .r>?v3Z]7tߝW4?V2yz3A9yň!yn+%M*+=[曌ZP_p3rNΓ֍^?f/.q5WlVRrK@r劫 ֪=Z3~ۗxC5;]69&!U&T4{՛l;:9d)6Gj}XB&9w%:w1wq@;0hwl& K-MӸP͚>mQ; mWs.r:6Xq.kԦpqLH7gQ >wjQ&|l/ARьyB,>*s\֑5{len! kUX.* VfJE;̴Th>2p.jڕLQDֹո6d:>{7җl3}۞{Zj!ǏH>Zr?;yMB˻g>̣nY ٭_lt R?~h_@ ݿTQ㩥+F?[R*;mhtͿB p?R?s:,UԻ=3F~޿t);+/ju ?^s4(ų~[^oz%[Iyf+ݞ֝6g n}w,}ZpѬ|n]5{WX)K>X)iɛuud2c<_m??ZDokM_kl&~ǰE>޶䒻[xDV3RI뗿yߝWRWYq/?sɥϽ3rMoUJ&%8pL_7zE"u?^-z7ߴyQB^sԷ1|"!#]GvuԔItSnSЗ+;(pUP6`G9dԼgbO7sJԫ[~Γ'GV͙s:rΚh;e4R< .G6ұB?""s 9.ƚw˻Ô~('BML餥9 `iXǒmt`1~)]$u)3 cLpMq f%FǎuCĜ# jVnh\ӳa%f0*:U:@0ʸt cKc'-Lg|Z=Pq脇ʥuL_Pw+#ԟj7Ÿ=_\sT ǟ_0,(Ev],3aE,Ѡ$H^2ð c.tOyf9.y?g^ڵ#A?;w60)i#Sֶ̚6l <2_OwWu9>|'TQgb'@EUO6O~X|ԩJ~W@zB&z^klM_c3s&}B0۸eo$FJ*DAx7;~w{+%MM]9o=jWYw )W:T;h%'nXr+DLS@*޻"pXJ'`˅^%k B?fzɪxIbiWҗyHF_gu;lm?SQֶ$pp#Px+[Yư왲j䰼CGm8P@bgICS\.Y C2aAqX:$y潉<.{%đxM_,+|&, 1v;sܢ&f[4j%A^(,'{$:NړI׍&H'CXq|ʜ0@z{56?g⍔'xVp6tJIs8̾Y舡yg>|J O°|2GNby}}Nh`8>{f OxTPQAWcLsC <{`Uސx?|ƒ`{=vؾ^m.Y_H IcsZIfRm({N.le}Aye6=c)5Z:sid9{B\㘐~d:kV7%%g:'85ϸG6(NlwHX 5'0DhAKV $;I[ "P֟͛]V5Q9w]jn1u}2ԁ[two]7fׄgA9uy9ElscO4'=A>>>Yr7{fE;E'vK99}0ul)Ս kXy༑UgW?:('̶::Ϛ1#S"z݀<V2jmۏY J }VDptcZKN9|+PxTR:GNe֪S gL#9C3$҉+4ķ# CѾ~b܈tDkطi"L㲎HJ#EW E{f0BLO,8N!FVR3^2Exa) 7~1ɊUWXg7.?ntQhmc9p ='Q+$ tiU( M.2ޣEENR*CIV5fooY(G2օ+tujI{nDFTO}y20@z{o&~8odh^u&~# /?Ouִrn*} \nn! qw>yLBgSF_k`s?ySrDRZ`~,)Nk3ҍ^pO9 bchYjQ͌`lHΤ4@iS1~2RQ6;l#Z8VHi|>.QJ2J/jQA麷L.fK*too#^tg󊎞ۤ-̽Â8{N>vֱf֝&_Y_kh3K,TZ}RA6fSGk[FO=6)2;r-u ǚ:f8g|&<چdJLrF:N_5dxҪEQ;&eS`=aS&#BzOWL7?7Xu:_ mڶ'wϽC}Ӳ<l)*y2}?fT׵JyZe.}&-,,{dHg<㣨~fgf{l&!$B iDQGDA_EyT( *H{6lo3~ل0;&q0;ws=s;HH:R*&=+UGusL֙oi@P1ʩE?cG:M.ȃ%-dW9e7(g€'u'UGu١ܿ=víhrQU)z-{@8*h_:TcfiZ(AH\ "cwXY ?HW&sX KdG^cMUnAU\⍜4((Wd :T1/!b@X +5ֿcM˯-u/_A]mH׆!* JDUh0fJyq fe~I#{uҿWJdK &mFY온:/yfūYnWmKDrx=&-o|z]+s}y}g[2Svy9|維A?g^]fuv*[5f2WtXFl_::I̔]U;p~R?\ n9y+ WWemԺ'^>UaCP>Qclxtr{ ̊  I.,-QIZ;;5KC8RyT! mr9tb.P] qG pMh 1 %!նR^EJ;-˯cHl/:kTԥ6K'v쎑SWйVfѷ[Z6 rZ{v~};g<=A" RҾvس/回{2͗f@fNC_qלGZ4o􂯷ZBre~+<赛yGNk %+\4o܆ ,VW>qEw&HGMf{u}c]<3q3Nnk4-\:}.KzAo[Nׁ-xղYj%3Mf7/K.\|BϽ x-Ox0L\TR SJ{_4I,ݬb˗b}siTw5KMIyɳחg8~;Cܯ\0{@MwJugK &mF]v#oݛO 3m, q IDATImKN:=&…K^q(L{oPp10“1{mCl->r͊'zhB2eTppel t$o2}H\լʠ@!a``9q%000000ЁB|lӉkMb? Q/ 0000000!/gd2z``/,}fs|ђ[ߜhqHڐ2]ICuq=x a```````````̐-1sqBK__JF "n~e 7.M9~e4$>/ukFWj'?#U~t ;o\bA6Yl{=]~wQŽ>Ufu G3x:'+3Nl O?)mfο?o+븅?lС"a5\?~|;A4H7Nux|HBl)bQf mp~ѽ98 !8ּmEPkAngY U)rM mg&-  s&Cy<6 M̞y}ƌHl]i5j!}kSoy.hr=?a]{.KVlj}.Qg!i=ým3kbDx9pҪE'Ć'|Oޡ3<щ!*ufޏ;^؍:-_Mk:W uNUcdWN}nxanM ?EPפN~#pH=xD:0WפgӏoKč|s:2Shui*s/IԹnzQ1"36:".j8qɹu:&YkU.+uzjg20Z-tڅr6~?(G.3Ae5`4[qۿWV$/>ԓtHVc)oY!lx#'w `;'\i1_]|3R__B&?{_Ꞛ?rHOOCe9t Cڴ6kR[9/'!C#Uڣf+*mTWAxBob1%+<"+4-̯uNiߋ> [տD۶2S̔ڸul c'y`+P[_k!E᳊}k0&ld6=(.&,48_^&d.2Yt=s}?]qw^I#{p%/>uT,tԡ[  gSXYd3ԭU$%p؃'\)zꝵYYK~Wʺ$qƚKReewTs@=ſ=fqEy4>y\ٯ#wLhk1&3 /VWp'˘!< ,yAoe5ٹ{2w͔k*:P쵌Zh}/|!=/xqǧ+6TvOO|~=1O/.Dr|y[WmP[u=vQA\øo{*$~܆ߗr ʣ"Zn_k7B'_u>([v\}u5\xw+.k3yNe紉-ϑi:&y0iܟ}חf*m2db}I6jCG>=xnsciҁbG5'|`&f…8(HL߆`\jVW͊DidWrTq 4P_ebJo~Ŭl6R[ b A^-ǗgxQדmg+|Cw$B04h>u*A'SP\ϳ=u(5F{)-=%lz—7/ ^^\euԸ_NB Q ҳ\D2Д}jI ^F1~!AJhUunlRJ{w~?~둓W[+G\Xq}*v#`._pМ[EcF$^n8PBW;@)9w1}@XDVN"IGY,`Ca9ڠ5YOu7٭6{#={e>F}5-f?r걔.Q'.nϽJ4ϯ bu2tr#2S@Gvs\kT1CsCd X _qH\Vwlհ#d5Ѕ;P…A88lbK|Vʝ\[tVZf(eFEm,0`B}Jff> <ђ] ,V;G'T&vzkAYJY2OMޥK*Ь2j' ?k̪~[yebN6D4v9j;ڮ|b =9v{Q誼Ro94jEL M"Am9ڠiuM֓[]U׶=`į??Рą+d=taÚcG$O ghh_6S H#\]J?[aG c]|IW;f_obE>#2SЬR0ה%J@ R}՝ȹ GPR򂟂}} FMӂY,.ۇi3~<{+sTsȮtUi(e,(ʫY|\ff{$[vo$տ~nzD,e%CfkzV[oi(wۧ& *f1vxϣ)w5"!F)jlu<0@ei^a}Qvˋ: R ܂2Z1RsRzD~YTFVkU.-U[]vzZdť:xJdDX7rZvЂv[̐/srSiTz،&]qs&v@ߪ̾RvMtY|뻷Hf:P&=>Yڨs?ܝj({##\Sj%k0+ ,6Rdb-~Z8)Ū !V QU ġrN^^u踼G$ _mx@32y [*a ^{HN~Ytd;5ӳb;quE`8SRufC'DWJ'JZ6De*j?2L[WN˝YEfeCY&kBu$ չr-A}#_άܒuo͓ZKSC|cCz٪eNl;~#zV/]8)4?,XŻ=zzA,29)oX@(hk6cv\V{ZI:s7j|H@E${B?uj]IqaX6mVd<"j^7uv* k^e.%YV٢9m_=pjKXϷ>UaCP>Qc"y\]Cn! WWemԺ';5KC8RyT! mrjՊ5w԰ $M[eGW~%_PqHUUB>u\\f8 ^u6~=SȌYAu!ɥe%|^rrFv~iB©g;SpO))ӕZa000000000000 00/O`F0<߾tAoxꍗr9̃X콇lKeJܓOv}ZXٷ`cWa2L21\_k/ctԢ!1V;^8f3VsϦf_}9D?erSoZ괙BܙH/a+:]OxB]xl1ӑYZMq]k\cNfn' *܉yb;2sjDH9dOcRe]'5PgNTѯp9a^o`a.#[*_8j.=Z,`%he] 8c%o GW;S _՞IxԢmk WwFWÑ;]&pOSWw`W meUPNy:G G'v =T?r(k;ITf>1c~b`^ϭ.?ҫ{4AGOn7yo!wZZQn"e~KI+ +u)]|Vb!_}-;gREEh^?!6:jsMI^A'c=Pϟ|ⱇT򌜒6WX5$(ʚ1eQy&,K2F;vA_)3vECsPU]iLsĘ'gZ,wJa9ƭY>{k3 mf' Pȫ-:9vB<]3Ha^{ϊ%b+oyR( _xfKM1=Yt)-CԎ ,4w_<>㴙b J5]7c2.Mz9o#c#",+lN B!]۹qe.2k!`wc7a_-^K0K-^`*;븸ΞyJga3e `-<{U#5eE;j.IEZ uoh٤Hʾ ?y;i^Ck,98gSr!g]q߫/=0p8(df2*T~_Ѭ~y#VL&;tS殙2w͡Wyxr1 A&Y T<1/~G5 aGM|ŏn>61`\J~{OϾ]>~#Rs.9{ڧ?^3`,^庍K!*R3vD>j,b!t /.a~=xŦ=nbI=2xx#L]@TDЬ+0n[y/DpkWνOWVVQ^ݡԖsRƔk0 ۾Q1_H;wNC%٘AmtHi.I];$J"jÞYp 4 M0~YPC\V l"xr)WXy\)6+ \Q*C~X+ +̆nvGHo)Baѕ/9bh j fpaꨨjsnȯd-]2mZsBp2h>ћjf?1umwЅKE 0===h:?SmWfL-A}ndgj<β}ώa p }f}S;^O/P}V}Kz=wMj;"9s[reڮljWdwۤ2=戰@6wBAq%hr'iF~5Fe4YX/C-/%H#2Z";%JIj{Z3{r4 A"T""|(:ad[1Q(^_<p~9ШA|H&`֙i@Yo#t%GcLjWx^_p_7+]mQR]ƕ[.MfpX}^Qv""$f^QZsѐɥRSѢ 7Dmi,VuL[S;`qpۀc7p8t\*RCEPTvsP 0< wRW۬\!];$䈡~ Xh]!uO萞U~_Dp CY,`=ć)EB>UdFvqqnH:o/1GEϱ17 6\ sZ!ǯ4ƽQ\ӿwWg9y5b튌j7Ie[i7u5y׽gI+.m0~}yN[<8ZQEc3*k^eN?(nFlQhr|甗a2MTdM}]5J1f^]m&<MGH\AJB X\bϑAvɺɱ[l! R)[8J ʺBKX\">uؒg&ҔG$kCTH%"m*4uv㹔5_||'Z%_pRhXpPǑ9v:r>L-ՎS(;k_87i yܵo1t@O$p=: RxeєIGNÚBA X+ +8]"ObӳGӗwj^e`QA]"4v?Ԗr10*baou R[uesD[PSk=$=1. tTn!|I2r2:u ﱊڞg;DMEYaOߞuF7zLZ-Mu w=7]vKf.4/#_ܶպ[y q̋yrfa(hnNf1{jԗd5ZeB8<}M=&.aqx{>.K?΍O*fU(̨1CT}V^bOk<.ܮ!7[}HTëTë6jݓܚ!J<[6^ea9uoՊ5w԰ $M;˔-R@0K␪oS-|<fgp "q'm܄;BKqxKTyRK.[QE;r*)~z2>D۝/,ܓIo42s7o<ĮX:<rR1r s2ߺ9G/[eJ]Î䌑_rR=GSRKV|h޸ Xǯ}5mXxR.p%OzoyjǓe⢒?^75hyo?_h+uњ[En=j,imر4eݮZW)mY?OOjñi9}c];<3q3N=uk R[uesD5 93՞ =:bpf/|9-tN"&=͗%IՖ f7N_Iv3]s}hѼ r\t=tԉiC0rGjՓ{96D`V7a<P9[XyJ2RGvfI.C̒yZį<&%αX>B7]x9-g7+tma?cmZ3O?BYE VF C&Km$sĢςd "Vi~AgQ&\9  OJۿo@d̿sƊ|/zд3$:&~nWWѽ0-@暲'rq?cn3m4bPaCʸE;"ZtQbeߚ~2? *=bp{!1Vֻ8f3VsϦf]}9D?erSoZ괙BܙH/a:]OxB]xlSˌ hAe9i6}YZMq]61r8,Oj[ j tQ1b/^Z,D@~1&eZUyBYsQZwAUٿtNVΫ>/uG  ,U^v^]5N1CTyBA8ҕu5H⌺3re2|,_u((b^zi퇹l}DIJ\*?.Ok:2ȝ2e1t.kk ƭY>{k3 mf'LMSnw%b+oP;|1۽EYPVQCqѪ] &*dv/mIE_݆hyFTbC6єԍ_o۾ČC˪vƋEkz=H}c1*Rh3qFe͘2tJiyEo$^e>31V*V ;HUDw~;VT۸+^f Ew+8߂9cN;bp׷^9h+{F-=V. LJ]K4m^mcѦ=nyIM^#'B52.e7+_ԶA-u^^:g#jEFN/sw4PXק̗r|ʗz×Gm7XMXiY_>X~UX߲&\XE;;6s> w踁#ꃔɥf˺ZCy8GjD] IDAThGP%HkR*KO~Rk%)3O97=afA߿]:,HePO.{!4j%Vx/y޻?:~Toe5ٹ{2w͔kR}wt5mfMQ+Xuh*R't'fhYuw s=[3,8g&P#DG ڕs/e/xӕkUԯWg ^\8özbM;~?ի{4ͦ#h}/|!k"]?_"K3u)PU/6< Ymu}j8v:m@Xd4Z-vŜ^lFnV(lZ*tw7 yrvr|ʗz×Gm/$;Aܲ[ꍀ/jVf]f$iG_;Y[#tL4`Ӹv?'tUTVY-fj5維!]vk魳Ygv\vB/\ <]AZy`ugTrpOV7>Ig[JYP(sN+:%'@ fC 7p;Y|ν qQ&r ,(!.mD@XDu rRfm]w7"S.6]I;o!(H,  aKMJqY_8lJ]*.u\C`(ۯ2k1s9fE1Gg],^Y@ѕ׻CBY hYQqz?2QC/jf?1iDhՁ2DDj&J3&Ǩ5aV hU-M=9y5y}^XxݢƔq@^pY´~<@ 0h9tlך:ӑ*",PP\ 7r6UGYG/@ܝ)3YEuMkt4I.8ڪJ˫`ߡ ˗> AxD E^K cg_%eis{e-ΑW(lXJSu-;((eؾhcFlpfFP{}4 mPKEzsa/ {>ZwxUll%M/$H i6Q嵾/v_6b C$H}{cf`vM~#Ǘ|UkpW~Y'w|rUS{Ƃ=N!$S/eYVy#6OB ~Fῦf}0% O`S9Kμy]jz^ @Mui,"#ƽ:¼ͤ*2gEXp+`ge^w ֳ¼ߐZq{qؾ'ac2 UTZxEUBjR⨨n:UNjƎHWE* &>9VNS/LQ +p\'-C/r p^k*񟗖%w_nAZgMtlIC^;:!VKq7m\{Vᔍt: ɘz\S :- .s1w.>iԳinT*>r ºd_:GRFͩ] rӯM8 $ôF˰FDa}(ް)WcՈؙ͆k+%m= :p6>h[ĝ3MbȊrq1z7=\ѯ8OZܡEUS ÅVQVRT!{i֬۱fݎD -y_WrN+?6=ד[.hFƤ?-\:W1.uH}rTr"39'3gL]aC0oW>QkPS'*{D0<~^"#j[ݒY\N@rÊ1\hBץhI<HJI2sgl= XÌfY7+jVpkHJKRZ=៻%3@;!s6HeF`,lOXX/3.EGj wζ|$ڧʎ @KFmkӝH*K"{!kp{Zro߶||{nlEuȬ~MlMz5אp,`Ơ(r?wDVL9jX f0>ZCny/9Bћ2$Or-ۏvvUEYd=yީq]5{f syg`֘^K.D~љ z#+%G%%Kr y e hPF*wӃӦ$Lz_Kk^ 'rn,{ 0{RU! uLi3'aL.L?m;+58.Ht⩝8VrCRhӷ0fw<4.ܵ,p 9kYxgEn5V|zVe?O{" |>e׏`V|໦ d 4EhoOէv o4I+ |D[~>Z_a=tuahz𱔠 FõM@4͇5ݒVD?٨l,]Xx |PAG_mĥ  󡶴N]fC qknSlS'pSftYl9Je=ZyGD(gɹqKnY;‚Eh?aW0fLzuA?wΞzi(HkDAbLk ~ӵ (V(&1ikUB8`1*]=X\PA>OcZ %u_ohYQطWgEuSZίMY.|S>QWҨ#yFO}0߿[+n‘sv]?}f]nsߩv F>puJlߛS] ΕԮYcS,,({/?}kl}§!ܗ{ڜo7_0{lYECm*>e2⳵uL&?SѳVne ?_#x p nl7* Tr7WoYڹkY87hNN{s ,Ka(_zL4V"\ D&y0m#:b gkXw\}l>\UG8}lv"Oc ?_!hX^iM]KlRۡnѨ"<܄@'>2@#`w4$iͩM3wj_?(o4uVI?8Fy敯! [>鑻mr./Zٝ9"(@ W.f+#Xm_AA z4i@ @@ @@@̤Y3OqqO=؎x)/A.\*\7}d,ԡA dO@֏I“q˭)R8aIYԎٚݧLPgf/jzƒ:nq㝯s<' W+$Gdl˃<;$}5$)}1QW){nr4W)ɽGϭ>XR3`'\80{Eig6+?+8IJo(eBs%=NӶ_t]| pGݳXei,ڷ>kHL?QgncLZ9}t;FqFV{":(Khd@ a b0elGK:ds#tNc-+}u& xļMU [P9&:K $eHXѲ_˒rՃSF7"Ґo@pN$S*hshq0h[UvaM9H ?aY1&ۣ#FHCz%)l9Ffc(.v㞼Š*Q>b!}Ys0~&jW}̼)-^puLz[6l`p\,`z=oONϷxѓ3Fuی2a -?q:w~}E|+ ~湯7!ӾjE֠Яw¢y߲hHKN{e6_<b];_>[=7̹- =mDNltC7sgatk_ʚfMfЎ:&1Cv詑VEmDPERgIcT j76Rq1#Ĕ3cN i+ԩS'%H㓳~Q;j9k9R$o'4h[#+>M4qNSgH5,'T_n5=s^{vɂ}{v"WR=W/gЫz=bN B '͚6wj ^>h ]QH}e~O\ÄQw~Nڽ?YOX`+e ?m;`+ c)i {۪틮>px>GWλI3l!}EB3*{͘uS ͼCzP&uیw?+UO݂aplN@ .>! $.7;֨I").}]_Xt89kzru\ws;)W=/_*gn\_q (pPif Sg+دJ֗y))(90Eo<ohr5@Q_? _0,vJxP S,`q,Tkh79|*}fE]uО! z2- y~Brw\ ଐ)YVH!9>@{6LxiN ƒ7_gCFAD f [¿ ±{Ԥ!.غhu] i7w>li#n-ARV[ 8.R[|>Sٷs8i|^e 0vĀ3M-V>imvP4foIa4{O8]^5p@ K$gO9~Ayڰ?/-':Cz^|;V L;YPRplN@ p4b%G~؍RB-WC3,k2Uȡt IDAT[^sk.O<睒x/U)3^[졤O)o;1Uqo3湈BڙB˪T=z/5$ z)˲bn!x1&ϴ3 55_,\xkYY;\jz^ @Mui,"#ƽ:¼ͤ*2gEXp+`ge^w ֳ¼ߐZq{s2Yyi[r5{3d`ZNRb]C?Yj4~g}Y+qA8> -*mlq> IߺXi@+yr VHOJJn_XᏝ$*!DX & uRr~tk;u %2eGK(Ͻa/bi?q&'Xd͠5|BQUnP4G!~ND/R{9+CPsX> ignN_/6)&m'j. .D{+Dz~s NPG;ƛ5za }GIW"ECZi21:g\N1>C9KO|!BY]cd\z7>Ʃ#[RmqNqlIMwSNaЪfڽ9cG jX]r~ON~i.Q+]n"CT"NyD*=mp|@ a$Ss׾aj\^"cRen/"' zv7UJGn\8PXW"K>)Ԯ׆sܦM' S\G!2`8$iآo7 z:7l՘r5b/vfڦJqI[OB erf0 :,qLp68{9{p\v酞FO$kXnGJǖ<+}9ryJk_xc[PթT=TFhsiQp:rȌⲺ&s9K\\"dد(Tdqt%D"2mV m=~oJ<ݫ6V+eֱs}^i K;ZN噈S RDw)"=N**U$Ue>,)ͼ'y Oq(;:1 zn,-M\JEZH0R O3 SgoD< ,F4KtqYk/G dQJ S+ "zq- b),΄Q,m1roMWrz!|k\~W4=ݧW[{<9Av͖_FQimz$uܠTaJ[PDRT(r:sPXTb>vĀAGx%R$Hs[`q6%$ӿ3o>\&9r՛:/ln!Tpl˿L\\Vd A dY$QEim;DL׳`ںV!u>xdˎ!]J@t !2qE$?A fv5U֝G) N|dߕ[<ӓ@ @ zܜ#go{sdqYU;@@ WVG9[hoDL!DAF@  H@  H+;b@ OZאd&,2_kG^󺪎o+;sڸAVb~{sISqc{Nym`Y~rF?y<3YeHu\6;g-EI?&沓6a:q˭)R8aIYԎٚݧLPgf/__q_'"SI4YfW堆r *J=Ԗxʎvq@ ">qۚik}ɔ>[|{٨ԫ=}k79ޣV , Q){W.3JUbYD72p%=NӶdt]| pGݳXei,ڷ>kHL0uu֦κ[g(ڍOS"E-uQѨ#@\X82a";َZuKޣFZ0VL8B !x\(K#n3ZCMnͭ3' M ux|)T ԕ-:sbQxtc,!1o|D婓yPvag힓(z4@ه6jٯoCHJ;ܼ3XLVU]AS5?zB2 &7ZìMd:h¥L/zA J7"IQ> ͸,Z fzWY>S/5jt-" I9ў:)O[]Jg?hB?,5xR(Vײ@ \ dh1p$BݏKve6y!| )7@JVvcQi:7"f))E҈>cT|)Iby\(sȳW b57S#^W$Hk7⋤ϒ,uW+>MfЎ:&1Cgߐ*iIz· X[g%"a3Ka@gŚڍ4Fy y>Ke$B(Q,T3੓ZOm"Yh/n?8J X1,%!M‚mb $Q0Rh)ڍ3~BWw\:k80 Y Lr453!@  HD҈c0vbT`Qif*z-/b9M5{M\ux|mէvNM:en0Up ")^`Q*fq,Ƕ h|&1A31E=᪐+zp-VS#X 8a|X y:K ^OC;Lh찈w@ q$@0hֽ[K~HK(yl-{>$\Bxgdޖ w~)˼Nsl 4 \p`͓SAŅī&xhIz;M;KғC?69 N M P0Eh㒆L\ ZGn\8PXWB9sK0U=oi8Kl|vV^[hnT~Pe.e 6''AGۂ4¥ .! Q$VSgFT'$]7δ%m@B{#K$&yW/\mWF%T[{ :NS]g:X\7Rv`/D|&ZWx"Y}J{p\;@E&Z)`:#@` K >c3I,Rh"ĒhHEM]^7u|nMi/< ,F4KP@ ʧu ר9)Y~PP*`hRdۛ+ͼo䚘agvxW.8w\xkGى&$?6W X#r@72&5;ԹiK-զbϸ^w&'|F^>,'QۻרǶu?pB|q{׵s W Qc,x5/g5jS' _*)e XQ$ ?pV a@ fp9M{ wT?>P=៻%3@;m:1S&"IYZWC`v|&q'`Nu IdSI") ~<֓6Ìf ƛlJ@ F@1I{8pX,Ve?O_=.*W_6ak*9Ps"Kn7Wwu>e~nIXY#nNl&-+T$燊#[:`>8v9@JڈKڅ;C~N]Y qwS'}f*zZh)YW.~Xv3>~:".Me f @  }\Ur(Fv@ @ '@Z@ nMeA @ ( A g3ob|,ڷM>A\I]Ѵ 5B.z#9Gξя!r߲ٽ Nw#~AT}[Lܩ<# 3+SnJF ?{dqpֲd̴~Vqkh+րD$7a0\,w4W`lB=i&3{yTr:o[7hJULoo.p*.}lQ)>@Ѿ ,ԏCnڈY'u>.ʐ%>4m*9vvZp3.NdMe'm|:_chS)Q>P<ٍikdM+_v?^Qie-"BZe)(Dze~yk&FaؕE=%ȅ^릏̞< ui ʔgM86ЫzJ1<kײ_M s;\YW^qpnozLt܃'yԬ?Iî$<9<4s%Ix^@aY}',"b4}Z* Q SoI_Iz-`oKy^℁c& ˚CgR;gkv3B|{mz+|RWLx7":W *HTM-jU&o9BR%/&G+oߓs0𵼲]ʓ嫿 i;_Ҵ23<$OvEiuQ,l=),Yp],V'Z)Wfme(=>`@R'n[3M`/rGc|?zRx&GsR{S;e *e`1 }"Wt6|fSɱ#J,;Y&CnIz*mt]| pGݳXei,ڷ>kHL0un f֙FqFV{":(Kh^#XMEW4ˋ쀸Sy{N!Zr~ lq/zX1a";ZRvx>y:NPBkem,J0s Fjvmn1 o?eho[K2_o9| nE1$mc.,sE/W&(09S7&TR-urh)RiGwF @3ܪʴ #h!GZOX$DFqvLMEV=ᰜPFYD"i=g!1u?E L*kdz1\ IDATy\nE!)'޼3S'iKGM'/C ŊZsG7oݕkG?xל{ykGr[aT:⑻fOV)-&_s:LYqWϞ6"Jjj~:@ jE֠Яw¢y{fl=0wkfݎ7MM6mo!O!i3|H_eﳟ uQ6y S& Zh/lؼwj[/.۰y_yJ׾;Ȭ~w,veunܓOgoץMx}{rWKt1?uP{N_=a) G+?y`Ŝx7W5̜ÿ|\nrԣ\z킙c~ӂ3*WKzOuh].![C{ĸs%' OkַWgl qxLf }׭34(i1(Yo42Ut]mVڸ@g:/%veʶ@by$$)YٍG s\vfYw˲}.()>RhH}!PDS$O×{㪦Q -OFy{_$aveߎ7W|J;*O6dd)5(Q.z2S'%HbTsj$eS#-$9mEnz]nU3PLLw~2lܜ9MU_&,&*)y%MۣE*(V;ZK mb:8tH脅ZiX\NL1bA!=cT"B.ڜVbs>Jl]3aw}O E&}"l/9^;5X =rޔ ?ʆ>cGf%}k%b-=]T`+h0o㮺Oi- '?7F?rm'[nD"ữpz~+D=bo=|xWɛNw{hŜrF)gn_t] Si^{v_l> PH}eũTV>r\T0& B\ظ v{'*dHѨ#vY6=e@&uیw?L3#+z%+by_©)=WB !%1gn۟[{= ٚI?t'lkh6߿`!Kۜp.iɆ7[Kktᄐ~8S\ͳv,r@b?n+RaeʱK_m)=yf|4NΚ^w\g`@O<"Yեrei:7>9"< n)sW]{7&۳l\`2*Z_! _|*}f\?/=ٜ0ҁjbMY<lgeNץͲZOF!U(k}mzI"3dlCG(#dc?ǰL4 $A3E* 0DHJ!@h7uc_ݕre#`IJNJƏ0Oא[iW1Dk!}OlqG̚6{2[$E9mfl'.8 *5)F 8Z]g*bq`/οϒT"z>w~zhǍ?sk'N+#344 .k4E?~OZ183$onVr=]̘um׾u,WMӱi;2ek ֟=4xr)4E-o^RgJr6-*F>uiXvn#]f}gᩗP.iq>QݩlC4qLfk@2*ÖDiÄ;gI|\nGcF x⁅oy+UbOu7V@lp O*$ $E isw7hM{b5o"Ϝ & uRrݣdJd[˞' P"Y14_ 2"[C9M XdAu|BQU^y.G! i(%Xj52灖?RVѹm\s4~8i%N-7a-٫߆Cι=^\|VVD!qlDH?.i";\v&?z}oϟ9&{Jʥמ/{ネi{NP #n/ϐ\nG z`ç\]?{lM}ˑEX±d8uSeCˠTʭ6ou LgbuM9_! \I;ܵ/t4G%giȘTp;[KnpGⓆL=SK#7.}@(+!CS__s6qS:&(T<4ϊILSFl6\T0p6#h[Bp6t=N2@%()T:THgY7L֙v㸤H\hO7GKnN6X)ic.ز1Hta{r; p< D$Io:\hjݵfݎ5v$_xlZ$4[JEۤSe f{*qv̻)qQg+[L6i{N`$D"p)S/&ZOݺO,Wh1~US\)uVS!Z.]&qîۮS ]hxSTJ^p崱"_(Su?{E=3;Kv7eH P)`"JA𡢠C (TzzKBfذ !dD'f{=޹Vmmi{:g؀/νY~f :J(8g1u)ΘA+ھr"@l7twHl3W JZ3]2w=ͺ:6GSt†*܀:b6/L %|@@ܭKSa7.Ȍ  ";SI @^AEAq~?s]a 1e: lc{@Vn)oӥ zkٝ_vlLL߮ƌk0:7>H]p +U;Q;$y"T(UJ|wvUQI @s.ʹ!K(sܹB'~F9MfwW>6#}C:]{9t?j  'I{Mzue;vK7Q<Y.gO_:({琁O'JaD 1VҁW:pNPq}z>N?, fe⤄ؐq8+ߛ;.6tѫpڊZt:.#IJU[pX|4 e^?h:ՔWXl[qb}QԄj7.+j-88b>#aˡw>޼]kWf:<<>zp\YICQ8(k"ۃ_޳~&Iu#gŤNEOOj]? HVk}&u[sUJZ梛II L5mAQ@:dW$5kS_>";fFU-jUD&apkP>PM4ixzbhbhMz?fqs2@O9 /#lѳ̪6※ˀ:>0׳շZY )GVb\_(:h&}1L\7,v8,$Dgb -;v2tk|rU'0ߎF̞6 v?b!>)vuo}5 {iQTvl 9u}_\:Kb~OΙ6IC2IIYWL/e Ll (޺Sp\Z[E(RW 5mIe2ao_/'}WeG ~1;:~ѫGMyV+c$:_A[쇅G)`Wp29 WSsZ(:, enWД*2v_ xod]EڊZvpv5h5ƣѿ-\xտ$ImWG G/wu<@b֝u|ac22~zl:ݰ;BQVZpq&*/f,}yw­erjh a O,6Ij\pcn c*R ]ϔx2k`;m&dsg.o1(: eEGZmǶ9)qzm~ɶ=g|Q$-S 7o/ m ᰓ/ W@0idĂWSsK^A;tѹUR|\е0ˏeʁXS0 W5h%bA|\d #C._,}d9bv+jL90000000<.o_jL˖|Ί׻q`*6bj c^# , C8$ ]@D+j``xHݛ[<By+&jקֿ=?ϔOr9LQ0XQim|6)SwӤ@?Y 5UYgwܻ _,Ha6 νgv=_xh<t7Va6h23 IbSgtȐij٠km,U<=MnjWj1G.U7ܥoߕ\Ws/O q jR䘊+0C8\[5$IX#~܃A&lؑ}0T$ ;w:6^<.v:,Mko+':~ρN8!}H;3z%<ʬO%ɐbgt8$OƌxAQI 2l.Hz'cZu:y{G폙t@/j SuGH8'z􂫻9.K-f&bfV&v4 -Z6WjjVVEGg'~y7o`z5튐$Ɋz 2<xx9b,iڢհ4:]sHm7T Te~> +wS׀$ S߿nTj % <$ &Bّs[$T}5~TrA\K'I Z̠-0(hʬ ͦ;I+2ly-127Ԛ:zx1?a+%ԙ".+15CWtH45M5u\S󮰦Ĩn\_{{b/mM 䧝ٸ3j#?Y|"Drt!rVmjQI]Wm!UNz4ڞusDop繘 1V.K o!,}$n:q8XXϺ_/Fa,MsnV",Ǫjhت4'u)IlE5so(s\򉲿heQjpMuKzTͫ8j2 {͓fq {ְ.9r̢EJ4˪-L!u|X,QYgcYO#NEYS'>=fDWieUG.#=cÂ| F󩔴?0h}g>;_3F",_kjF>wҔ yV^ٚ~/(F+:>ECn(lc3ߘ7[G{UZ1Q&`:,>jŒiƼMSGօ}:a)K- o_?leǯlqb#꒴|<]f\Od8K u+hʰSjRu ^i cFm[ Z@?c+tM׆LbE=J IDATVE?2W^ Bd+$cĜ=n-}7+tRt9Ę\s|ꚴ+ @C^r&_@TPB>*mH aj<点Ṛ-:DuPCylbX$X[sT幙tT ,`p@ȼ~s ]yU{mNa^$AzWYI+ RCGc GU;c+tVSśfqQޓ* %wVrCAvsY5° )(1op bUso罹gd|Ϸ+,?sޟ4yI{t5,浥8ޟuAZ ܱUժ3>t+Y2 8 *}Z']| y3G}aN_tH.yO~8o2͹ߴ=cWZS^d['\9)_X1jxo@YM_y7_t*:t=kU;|ICbbsǵ{ſ4vNZfjSuĞ9}.~cE6z*]&O_n7BP@#GBG>Z[G2>><>X3Gv-q_l ]*[(ClnKM/2s[s-I"_[ y2n7ZgUѽxNh"qb ֟e jiP!К ܂/hS?X97:"!OT_[5`rռ~;RWO xVg W3@9"y)dR'xyԫ־#f}^\^Y,mr5,ՙ|ٶK2~ӥZaum$Oٽ7GjjiL  tbmuX;R|:%t /}BogUVG%I?7vFkgpU,{p!.7kZtQ./ e vh|sa@G ڲS' M_t#?+]^#tuE[V3;1H^*I?]voPatoKMI’[__,a "~u_fФ0bjxl*U;tףPD~9BXc&  8@&˞GMXh8X0rZ.1StBQ7*2mzH9' @ vI-3_i4sވյCXu3a-6#Nq=FJWa>3ohHGQV@ $ mRfmk< |֗4k3=\DB>5u )3ۆS> :In9% 4;a `4, Apt$J"yZ>aR'%]::z5K/>nv3~)ҥ)slSc6 K~q'!2_uбOAG,i 9%UsHYS$E w==ff , ,VmgǫcS߽tlhgEXo3 ںphAwoON(Ԑs~RlNx L~U97z{:/Lnɾ'+F:!p1 hoY)L:V_̯:h[bMX@cZ3JZdz`s6M`,WU\㺚R 9̨77=8 Lӛl%Eն!yg &븣 s'L}4!eQpК9BmJQܩp̕*t敕[=ù]:]l+c ܡ{Z:]G:#Nm\~w΋#j՝.@:ĺЮOAG,iS eq/?oũN]%٢H}R [WXڤ+s @WWN?Io1.^=#MUa[6ff"f;ʹoxw/ J!iWY5u≤G=ҘOR{A^{Au6{$U),q"(ih&7X\AIq?Oc[yL5Su"0s65jq=,>s#E3<Z;lN%JHI"{ë>!qx<!IΐSU :p$IUx*]zz^PX=Sr)tmw7ΘN\K#2S}d܉ص Z]9q{'TSkeںGƙ/" ^Y):RHmqe]&qH*2j괵$"a:fqi+4ԙc@jዸY)27"UIw_\$*i_τNѬkmjsD;6VԉeRiѹ3=ܝiƌY,D$\}uZX?o3ddU6 6O֦G:Xhd[V6 y\$[q1#W$*@o󮰺 *iT?wb΅}Ya eB;W(Icyذ9 ޺M.ȽZ\H?a}~~'ۤWNq\c#T3[1WXU1(8qtW &6.kJDg%8zY>YO<=/%r*i9#"B |\}f;$,g8 ]#J\K$BeY?75y RsZt,J2qRBlHPjЮpV!~/>7cs[/lچn^ y=$:pr |gAZ_IJU[pX|q\ұJ:ܖmX=8.r$[i+GԶ筘5%ᛟMϗ,Y8f̹<'*<2cdP:r.tSkuvRX;m $I&_HTx@v:Vd[qb}QԄz6W@5aACE~/+u#gŤNEOOj]? HVk}cnRW72PM4ixzbhbhMz?fqs2@O9 /#lѳ̪Vqe@szTH[Njk抔#P1/TS4spy Z.ՌX];MH3Ur%2tk|rU'0£OΙ6IC2IIYWLY7up>7Ǭ_ϻȝ]s?m=*׮a#~˕K{)]rx٢Pu4MOKm?~׻َ/L]×Ykn!XZF[Λ1J .8m7=J8)L'_X˺Q52_m Bu/&zĢW/zu6}A߯[|s ~ӥcWmݰs[aʍlP5h vϛ# `w_vԹ4q~f3cb7%[t:I5_4u`]pShAGuvRX;mlTJ1rKZY߱zD$֝u|ac22~zl:mGBBBnÓMṚ}&![-G\ʢ.r3%!Dd%XK0rm{|cm0mǮzTm O&SL5ܼ)KD?A]d/ Kz8$ \Ѕ |=) VrpTw|٪-m-}00]TeʁQrFVna9S~$|Q=cL]@5=x![Jz8$ S1Sk)e@2_6P5h/qH^|nYIL9{OmYʋ#[1i@Jx(,պ s' yvtܟqOr9;uY-< Ӊ߬ډ6AQPSUuvGͽk 9~f܋TgRCkx6["gA[yfIҽ">CLWs ]j[f2 i28f̾zVI6brڱyˮK#N+K-ӣB0ł9x.Z}Jł[,#gNi-*_;i_?qˡ((\VWʺϛZ m3SG繇}Gr{n^O~)^i5uvJ%—$W Jm9 40vda1^/ [{tIs7_Uܘ;HtwYd:!}H;3z%<ʬO%ɐbgtM8$OƌxAQI 2l.Hz'cZFnqTۏ#{L:5|s)ˋSO:#O$H=z1~'֗fKU:rD afomWyL:]ܢesYiezY~t_=FzM {5Rg2aaV$Ʉ +Ugt0̲~@1ՠicW.]kz9{X|VG_zipOwF rEwԖj] I4:ãa`–'ʺTIVYQx*]:w|Co:62}Ґ;ڙI*3,{d%n}Dw׍ZMUD׽ǁ$?[܄Y(;2qnc*]B@4 {I$ uABs .J=A'Z2+bqΩ@3c} pq̍Fõ5Ū:rD' cdP"~5:S?ԥ_}Q7{'lY.+ Ǯy',~ ,k(#Nvq \&n2'3_]l6kϵ+lF܍;×p֛M,8A$|Ccog9㣤R`ܴOņ08Yv~S&IMMqKGFMWԼ.1xikJ&?ŘQ9D+Tyl'5}St!rVmjQI]WǞ,5$\GGKޭ/; j9"cDop繘 1V.K o!,}$n:q8XXϺ_/ƲXRuUݬ@FbܢaҜTץ$<7׳dνUXmp-'RXE5ץ.9NS5∫ *T@U6O%/,(i([^1:f(,ds3)FciDgIe?u:1/OכrKu*5#hf5aQ+L4mgGKS{(YyekSܱaA>TJM#z&$MY1,>jħd7ر,|rnLTw=yB<>}QUֵ6Rmvl֔//Du7c\&vsjچK׳ B>)~JZWY y"\q=HK u+HGW55pzUN6fl^Hg,6pNtIȺbҪ۵-sU/D&yzI=LIC:ڕY2RX([⥩.j>yξa\{O-XSK9"+xP]VuepQhhKȢՒ ]G%5VQ !L4\05 IDATZ,Pw\*yO~8o2͹, 7r(Gǀ{\qݒTkrwb4a!ݽߘ;ZuP6cV~;M-+1ܰ'ǯ\:}uYRK#q☁nkFG ;:RsiC>eEehZԖc.PTfݼ޺aж6[mr@К:u^aYKN x]&vZhk$}W64h!=|Z8$=#eRq\l(1ޭ8$ʐ([qiLjɀYιq?$Py/fdٌ^GiY,-oU9"csH\lȐ8`غ7riA0 Hmfo\8b1a^ϖkg\$A:aWj1pfiB}[ RSn:FT}GLP{6!?I j/ʽ'Fc9\H-c;YD82 G zt@(Q2{j*H Pȱ{{f䈡3ugǮXw>oWp'[:Fˎ}<'OFfݧGDh6kq]65ŢYn`en]N xu1ɅG b M엙Sj]-SVQ{#=w֔K>dD̜;,B/K:,Ո{*-z`rռ~;RWŘ}W~ȩ{5ʒqb ϙ9%CEDu8xdr1  |]-P[%m |rntD\N:GB沍 8}Ґ9/&jUjK]'ޜ9e0o:Sj-PCm9vR9ؚɥⶶѴQb^ǟmkA'ʍ,z3Fc{9Nݠ֯#Z6.k4zSn|ogFZqH8|qiy5-(Ʉ2u$i|sa@G ڲS' Mf/y֑ڕ_ڢ- NmwJOݹ=7zn19նd8{>P9%&J9,cPƳPgURmܡշE@ 3xi‚0i8aF!@o4Y CB_P0!)ښvؕ W3 S~ٛR)J#+vp\Dc_盍w\ rr5c,FƮ] }Idrjg٤Hȏq}B;$~;!do 6eAk>aFP|_DQ է{ 3ۨO=88<6PJ߮\DRZ^p8RƉs3^t/"NAumM{\ؕʑ,۵vmw7Θw8bqa6_K ~;$ME|$S2o ۻ&?M#6UteikKID<qEu4IׯWBi93x Z.q"@l78.]k7-9sM?#߲V}#CIT FN#Gd~l4ZO 4'Xluup' @q\H1T5rynfE0sLecUe6VfTBo#oLߋ淕WpG)b&A$l98f~32$܂r:O3sJB|mOph&t~/>7d6gov\M'5|W^rS{3=[כ*mnCd@fϨQ<-( e5ʭGeɯt5aA>t,V+,&=}]7+y5 -*P_WPQP\9_~aܤou mA_*;aki9n=xcv9ekW̖Ď$ٱ׿Z1.6f uD ]&kwtA7o@?z,!}nnjL-W$劤@Mu*JZpB{@'iYO| 2w0m7`5WptoݦN^ANiod.v8|,uuQm-@cD^]z;q&_'v]]y^^nZGpxUN:6WnF f:qEZd`q TԶAB_W ENt_)Ϗ);LvIJ:pm2öΙ|g^}?jxպ /X1XUEYJj5f>OZvkEe=IBeu=b}Yp)7:BMVYMhMa1!ݽVkksWン'b$iAxfe:pK`(L ?y-}>&FsO%v=QVtDe/mvl۞310<,֚f蒏6љd{y`-?us%s'Ó aAgRjy}ssa``GoOS\Zm]ăտY0!V/?:m,2h2:q]203x30.{_1%`4/hXg Co0+nNg\8$ Ph2й\[Xޱ]WǯZ6S,5%m|#5? z-{b4*6bj C'SVQ[VQ۱]z2z0000000KYqHrW ݇2EVL;##2gɾ-_yq$߮H߽'/Iקֿ=?ϔaWݭk-m3;"kjq52J m 8ٮ\Ws/O q jR/>g`tzFv{yv[#Xu+f+er ]_OԖCł[,84I cGM$ƓךǮZG1Ou;cރ:"R=.rDVqPIs@ +!湷m~WWWf} -L_?;{D%L $ &1(~2flpJZyf[uA;<%3g#u駫Rm?3^2%},/N=yȏ<,"qNWws\qRō? :1]Ima2ۮ Au\a9EJ-^ ~ ~ ]H[i?$]G 0_3fe-Vo0ʚ*@30Ӫ~quT6m7T Te~> +wS׀$ S߿nTj % <$ &Bّs[$T}5~TrA\K'I Zt[`tQ :ڕYMwNmWd[(cfe{z+A>osL!: n.D`j(uzKʣnLKarI^A8FwX.u~lMMqCFMWԼ.1WK[S5ig6.ƌZ& rV6_c<ܨ+:Eũ'ifZfA_TF8Fա-cfrJG n}i=L"K\=L`WwYj(؞x3aX'uyoZR}֥}14cnJUw*ai&}:vD/O U{{Ą̝1|;hu!惋7|ngfvb"5-{0>.fwei]_1$Kf[vRBDߔ XY9vDyFgݙ_e͛5jJiMml}pj~c"5k]uy3GVTr٥S"[k~.Xζ'=rJ[XzR3\*;NkǫݵEYP][O)IP-6oA'3O{Ͻ׫5s%b+G[ʺ+JgNZUo}(K-A=7>*9]tKa~_רv^-9c3f,UpDN3sg-^W=:w϶=lw_xiŌ#|ȠaؓaN*bL__\4/}؜x"(ͤϾ-X> 03 fwzƄ<:ñ+4-g`c׉p؟^n2^y_6/wJSA IDAT׫,*t@Yxz^Q+FEAW7_U]`s{uY kZuO(7vȀ^Q죯 MzLH=ln3M9t\x/]=N9g“'T\9PS(&M|0yz<ƨ/vUL߳[޿9jfjXklyï$p;lUOh=xZ܈9n5cϷdY!ʂVوvDDXDCة1,8L(HEC0+J`MϬ2 )qEpI2+u;\)M2VSo,i^ye fGp|YH埁YU<^wjHGƬ(@oX T $ZX- 4VQ,kBګ" l\=&w3#̍ğ}U$F]*Zh7lZ8{?K ID fMN QV^OAFQ{#'2fO>|pso|nQtH6Sk&m2[ۥR "T5u Unٟz2v}&s) LփG/-vC%WD`7佥bCsJp>ݜ1)zӸd\?:@kߒem6竗~}{GQԯWQ+4XXR T  I%"27=q6U^uD~ރxa,UalҊ:ȼZDf}U6r:-!z_xoQ*G|H&ȹrzO% Y.YWYc\^K/voE_+wo5 gv:Ϙ>c1ڑhK YGk /+Bz}?V^;bNf]A)*yn 2ʐ!5牜xUYQ(ۢQ%G׺ Q0zO s[2 \^wJp0a)-ܞ 0ēN  jjKf#djpG : NFeI'Nzv:ap1OPn;P A@|Ы9 2P4w`BWo|54XzŅ64{v6vyvcUgi{ "xyl9.E]/We)M~=jn92䂜?0$$H)QxO_){Z ) GV]:Fi\60^*E5b(H5~וͺV7}?KϺ١QG_a:"^AXS5(_Ry尰yBy2|}6W`7{PUNŠx}tEٜF瞼|E^k1@{ &dy_¸DžҀM+[-m2+nI`$XUq;nOwܵÿ6d-iv5YlvE-$REa e{V]O"]%J@]Gw*'ѥur4V@M}A F_bq1%<9L>MM9S9-"W9R85͘EyMma88<;,5- @7Y~>x18zG Z*\~)`DB2DJB4jZݪ,X`hܽo'zk jZ9}1':&RQ+d݌e7U|y;/Mvghz|?"ܶn9- {dU&YWў ʺeYGinxq'ݎ~rH) CAq]4(MxFvѤph cozLKyGyB)X Qũ33rhȡI9MJʗ~mK#Qm&$5GucXfȾ*y=$t m%Ncoziy]h o>k-}uTQ$Жi%$ Qߌf6N~& aK p_4jyӺ[-n\? Hm5ypwƿ;X[^!“'0rh2|㢷fsٺ:nCڲK?9۪}5dou5^e aEX(-1 jO˛~.I粭aD5F_,sKCT8RFv^me9o{wֳW$X e.'Ԣ~DP{P4Tk:.߉9XnK;SHZPЌЊA#t!p't]TwBɤE%5YW?~qwRvx]+_\*X|aE\+>r2ӳh0Z^xgLVб_05_ލүw㸘wprl“'}]+)~h=Z.W}FA:b?uQ%.}Un̟6I>^rOW Xl~E[#~5ϟ~OE!IE?n:I7j'q8\ϽӏO~ bíʞ쬮ݥ{BG^qҚb,BCUWXjPS ɿxP"ƣiYiW:I:-{ٳ'F0bFxй=p=Fp}FR|>D}Mu(Ob7|pc .sgۗ7<==^/{6lcDF3Zna_0`S+3* |Gb'vswsx$'N&fBd˅b?I16w$ ?{1ZLj̒- }, 10KnrON'MּʭN{;Q+ qb I0e$$ 0`$$ 0`Z`0` 0`IH0` 0`fS;1seB ID$Fsfq݋ig}K.cW@R|3KFVgm??|Wf.zS+!zrڒ[75$'N\[J.+8gkwB[=H䴙+OJ8M(ڍ8mbfۦY9jL5y篥nr;U@9 /oKm?7|:n G 2k+N=w)XR;p+*pZ&Цɛ_|΀AwҿqcЛ k/<9JH:f nnp9lb=6ɗ]Ʈ: *ڕKvv\  K>' 86]OtdC̽߹~zW-J)?aKm?EP+\[E<1n\@`܀ޓ98K0nrCueYG 3<42/V z䭲#W'.3;N3 uyg;t@.%8qxl}_W}(d֮"@XwR6R_C_FfJAG1y#{ V'>l5+.e.._1@Dum=,>eн6b.*>Mp°O#Sݨ3ՖU1C^>ICgd&.k@Ԁ5y /.eN~}]척=է40jҬ9R7AXsmIb.4S˙0Io5p#[~tDRH{ !HI5|V. #XP\_s U5zCߤ(F ];# XBbҖ58f7I TWn76^3TWp)ft?eY[ڢc߿貛CF܀Hn*P] y""vC(R2dfA[.*#pE7աZ$_Ȼs-}E☻Wt-D6z҉{ OVjx>_Fv-IDіi5Pu_r@[;ТFQXR}R  D#s؆LᢔY]Aop5pte#u(KEV+m$/J9Wߩzb-*+P7oc).ۢUɲtrH=GrC4ifj.)WTw\AX1m΋ONO. tIz/~h\b!6uM;z-KHwlu|̑_zvTH֚_3<ܲBmvgjZ溟M#/xɡy Z?(GZ܂߶z:YM4|p#Bl.5mJWBouW]kPյ >[GL#`9bl\tK շmU>T,28a `#+ǎ=o֨ٺluܫmP[,(M7 ԓ_QG^9n+@DB5{Ց֎yFM?_)smO`NG ]륯zh=>蕣}#j{+g.|n[o}۾%Y YiPO6J)4_\vs[ 8f@ \q^ "ybu9@s k .p~I _Yi;[E|I(B*/ȞoH)'2fz_YS^j1ZsXяae7~'pP 4ȒUw43O Y$l~lo64kꂢNZz]n|>eR75+(ڥ/Di&M~og.zo,d3sg-^aGO?pgۊJj<`mb9竗K_{lw_˦#QK9cn_`XZqpW6@G5SkVx[8o䀾q64HMIr:v3xt8OK? 1M;u̐4 ͭ~ /r_zx##s΄'OrPM`NkxQ_ $gsV԰ל_IvتOESeuITd* 0-AS{IŪ+̸r}iq#֌=reWJHNE4dz<`OnlH 72 M2(!n 6<\$;Y%r 4XOe r ᲄk~llѬm@iyȬ*n;\ ]l[$#scVw7,*v_Bq;i5!TUn6PIn=9cRz'=8{|y3GDj* eP2/^s) LփG/-vC%Wh֔ tܜO~a&}keBx dϕV@բ#+q~&֤I A^i@aI&P Hd:0@&P`nqIn ٜ[ 8_H=x5v__Uݰ'-S N$u>V_or&mP[Q}g_`?__oEG^9n{O91{{;hvZ5SkUM0hէ[^7 @- x5Szh=>H}#j;C# `^WsKk -ɾ)5z~ٿmVcE2LEҀ! E1*2S{k77x`+}CWԮvtW[涃?8aܢ򬣕Ng,v\;mzm9fݭgjcMP8+ IDATSk<[%mACfV2<Бƫ"̊@ m,=݈%M88"@)[FMxXh8u\*\*SEPTKPmAWL!w!PWmU\2!S;&68bp{0at R|E&7[PQQ9448աCei Xh{^)"E(ʊҐaVbY؍ժNW%Z{Kؓ~,=Jw{d.7 mvb#4SΖp846JsBΰR(X,*휑3\n9͝[l˳ƣBTdԋduQM/'(r $[鰎rD.5Fvc734k[{J[{ӑFwngi6{o_4FЍ;Q 9yeb.[Z.W;RK!jQoJ{p; 8{л ͣ͹3F,[0qm=}%7 JiӁpFH1kOlQFbBָV,t Fg>9yı+?p1࣯wJZ.~[4S`;5jE܂ M1HAZ^'i+{&r:uS|k avGw_W;5l` [)CTiwSB0q4 }VKJ@ }faSwjK*E]Mp]}5{q=a e{V]O"mɹ}I ,Hz Xk% t\$t.]U8&ת)ou"(hkS҂9X,.'xQ/Õy*XP:*'W2:AP4cV5y"9NB.Ʊ&韚yDF=u~}G^qTSrA nG#mڢL@xd*ݤ7*$"aC]YWp,(Bt 7"љgKlfq~NkCL!g\%UGnHUc 41]U7FTr|OV̆Fb,M&r> m`a56΁8u\ p:NH]\4W#jִ:셚f{ P% 8jyN6 #7? F"nWդ NH~he;av2 6ތTXR4@q[\\V{,JltMYߪ̛9RL eL&:"(Xh~W8.{ ,~&EQP\mP[ꨮI勎9nz[+ p4׷Ú:f:c}G.\LK?:]ðA:G"'\O/W$(q@X X[{SRuTP!'@,3ir1Q|uG<8NtEc;*/$ 8Y O?j4S˙Dp҈Ĕq[5aV"UG=xj}z-:^V41q)5 N?>tNYIF[#qs$nT ?{bn%P!,. 湍ϑAȵ[l! *@8N+cd}!+Y\U^-,#BgSEqQS98R̩k%x3G-fi6\ ,B=VZSt Ybr?ۆ'Qܿ{3G ILV_\1#4?,XkxȉiDi< Nَa_il}G{b=ydw::2P$K'ǤԌ~ o>}FMZxPЎ~i Jư:]CQiM@u Qj\,(*.94qpq KFTc<ߗ9j}+M>ug1 V o6-v_Cw3W~面#jۥחK;X{vNi٫<3k}fM垾F-]\28]f:c}GrqzƆ +꧷__W#Dz%DѨ{4 Qߌ<6~+ aK<EJ~>}ݍG=&n@u]y'kK65ͥ8<Lj1C[-(4S˙f2T:e@k|Hj3jvC>1י?o ,Ϭ-ꜳ b E8fAM"iyS"6i\UC^!hUc=ŝznyiȜJGR p,уT T+֣BY6^bW[30@PaAAUS9e|p<'`-H L"qOjhpPЌЊA#t!p't]TwBd_^4=( Kk׫ML%.{%j%+k)yg{v1/14-/3K|rqyzZu;>|3Kf`Pw_!JBx4-kÖC?w) f~i6:[|) "fݳH(k?&??7̓ e~o0W9Jv)`Osrɹy}մrprrh'Isn~mP[z~|;yQsD.5_jwѦ'<:{\&.9Ǎ-% z2٥Sju5tD|'V1"j/‚6}Ǿ^IC/4i G na߮}y*zqs>Dm MxNrh߇BxP0`p{+lj]fDA7DR|yF$>|\йZ&!a?8K70Ns*u(0膐E[8Naaн3f.ZWsX "!^?v|}5\=#+Hxfpjs=e'=Yݥ^4%n6SK>' 86S]d_ikh侔Qw#]'{E&E?>oTuzATb[h rum.KmWw4F~NVӋ޲,MoFnWh1>{kC-r2o3' =;?>*<ٍ:SmX3dzC@=tF־Y(;iV D \w`- RA'Iu.[qfQ}J3ө+q-u:ؖ/.rH3S:sY[foGSWs˷m 95ǀtwd~4aVBbҖ5%O8f7I TWn76WWp)ft?eY[ڢc߿貛CF܀HnP] y">vC(R2dfA[.*#pE7աGRuٯW]zNz=u["lʡ<w!V!<}a[̓-jpf]w\YE s !CbF6 E)/^V(I0_*s$nGzSZTWn2.LqOե˕C9rی Uiʪ-IzڎX\QqbSsĠyFM?_)smO`Xb"5-{0>.fwei81}Ґ~ŏ >^33Sh:r *~۞R.~nٴ}H=ŏ:.Qo,ɏ;Yd~޳uqNƅ,k[[wߴ(RRNgwZZ;zĄ̝1|MWݵEYP][O]j]r O|Is&{SBS4h9Ԡ۰ݡԲ+]QKf_QKmGԲ7:ͺ?Dj־dfгKEJvXA*yna_^\Vz'_˦}~َyQj7ʙ 9}#ݿqmOMHZK n*X MLZ 5䏞0eo3B kD$O=0苛ڂ =F>4񉜣 6+?m'TKM3a\Des0w _rxi,-RJOPp=5ӫ 5Y%)׼x~P0o<|rxr>z{񕜒[beC ۥ+(ڥ3}ir(k^0gcsưlܾ KYvEjkŮ9~EzI PsD-+j~}f]矕r1 A/\/o.{|lR<⊉LkXSkqR/ wI ÉWL_ڂ+7B4=u>Ďj I ͭ~D"+[A]7?i:Lx+'j eؤO6Zgٮ@⯊{v7gVmL K}͙-|n]4HM3*C 7";)Sc7ݔ(h;9>&|`[NM ih}fE qXU"a18΢H6.I\&6pnU8I}*U?U %+o,!p@1q4k3p@wZ2۫N-#u<[ 0ܘ ]WkPz=j}MU{U'q{fpĠ3:aЪOOE;>y:kpwnv:]q^{nΏ'3!|NʐDAWKdX e=x2Ə?ڷ䍼6mNVk)>Z}v̍fU@ʠeU5z)M'?䗏I'*m0Za6l')nquy.5 5HM/C w=}DÇNx _z+ P[/vEu>.׫MR#jYQ7Rs) LփG/-vC%WDWWo$gr6l={K{ņfv̍Ϥ>ngIU@l(5GӲ}httdPAQ jniM-fO=6Uxr|L(S3qmҀ! E1*2S{k77x`+}CWԮvtW[涃?8aܢ򬣕Ng,v\;y]Jp䴙2~wsbPEHϫ6綠!3 Rs١ЍWE-XL{T{K;`qpE؁S<1 pUU|"(ڂ 0C< dBWEPWmU\2!S;&68b)h\cv론WsJɷdd(+6Jyܘm;P A@ֵ]'XzVEUI9; \+"[YR/xyvcjnAEEn`p=-B M`HHR$(7tۥQ@!gj j RfJsvcN-k]ԏ|ͺ?xu7ޥ e5*ue䢩t.#+rfDʓWp#!{<Ͳ-=i[C]މS.Ry尰yBy2|}6W`7{PU_m1(l:vQ6'[oًs9k/ m5;H28CUP60Z1D(7eڃ%'L&ظe#?*tNpw‰dqpظŖ:*˕NwiFE),0`p!-V J$A.3Pķ΅#R t&X̂y$lyPR.iU絼2ϻdEʠ6 WkԊ`L\[On9>ywI']Vr֠/ 5|N-슺.G:|f?SjsǹmoIĢ;e kCAUjFH IDATZfGq VZdW'\H=mV?epXqRtgٷDQ7Pvk*B{IHiEʰ\=jqt9.]KsΠɵjJcJxf0ڔ`!(a:'q,lrġpeni)9Nɕ iƬ(k,>n smb y6zw`%G7\`Dje/},64]~dcը)M_韚yDFW]9" Ajh9w+ꚻom8:unXS:M<t[pTSrA ^Dmܼ7Z[)HȪMz=A"6ѕu˲,D~*Oy6fH=_9|W6N3qOת?<'׿zrY~S߬r^4 z9_S9}؄'KrE[;V?XBl@S8q9R/:u\^#ugp_m\ZZj5<~C0⨽"\R ȰA |_3n\;!Jzj_n/gVGum_(PX\]\V28!epc[grpq8I}).`,֭'X$Ajb9wf/oXJYQmK>5 uellߤ( nEQ  fhsJt (鑦$z SI(:J<6TkHdƐٕ,.Nx8xjA]g 7 =̜:(Fی:\bN];ně9_-m1LMaI/⟢k6d甦3fN'1j֔_^.4(K$ 9$q{u_~䘔1:a`s]^04?|g={?4a+b m-}D洉b4G[r13ĐĞj3BÂU<^ {.94qpq ey;V*X,DQ v96^ݎiDna\7y܀!C^P\qz'DO<6iru5SZVvE]}+jPK?:~mt8F[n>%DvߊEOY֥^z1!Cz2=IOؖ7S- G'ߗٮ-y ' G9~3^i.|=F<⬌=߸mA+G/洙 :A˫! b E8fAM"iyS"6i\UC^!h?{wEy?33lH ID@nDʡx RmzU۪=Uն~*z x!"܂΄${߻sذYlJ?vgv>3nخqx 9YҾZ$pm6 c"$h<2k"2ʫ30 g9)mP97:3p mRVV`H>ҋ T 04څJSg!F9VXx緊,:qm`{,LF]skoSC#C1f֊YaOFNzw_~%=_5uVt8W>4qG~6ͽvϼW}O$G&tѠ}Ey_zw-M&CGO~2DR|o/K<~S;=ݕ~ˤs{*׿t}w^RѕGN~c5o/o$8PU+ݝ|>GNzoUms\kq9oc2L(;gG_9oV<[[R9P«zMݷY&vW;+oabm}ks t!e [BE熬iL־Z-|NCfu 6BT]n^:2W|÷1 S*[[)y !B'?0k7~۫0!A߃śn Bz7gL{P BiP3n!B{T6b;$ñڹH tq~O B!vHk9mq( B!B!B! Bg=@iކx>]4j媷7˺} L!:FUڡS؇huPm)X Jxaߺݟvfy_˪Qcacۏo]!I;M^?lZײ62:tS󉜾%*ICUQL/Fɕ};uޕg& j ʚ˂(D6;M_x&I {Ȕ\,!cFyeU:k~ [sU&K_nSWK 1,t:O-9x-*/qH^ɤ|YO߰k^9jy6֫"%ٿWHڵće7 |?|t1?93B!네%рSL?L]l~'l X$ '\n IJg/R1skY!g|ۉ]Ib'Iҗ\bYl<)=|"oc[-ˬ%S{Cި~OX*4;qYG! Iݔ\$BG;/^ǟ0fSXX# C̡JpAAYݛ' h-c2f;-B y׹1+1Z 4o( ʴ+IE7ͷhrncT[>%G!~eOK+>IDGԢIZɦ幱eb[.eOn0ܘs(3l꜠blӼJ{\PƵ,qdby=BЖJQ$9}ګʇ}(S]gd> :\f營H}Er 3Z-v/ʐy?:/2ut\|ӘQC$Iڼ7?e.÷~}1$o[jKز=_XMZXnRao?=OS󌜦%;ہ_qƒ{rߺ.Gjm(Jvy|ܶXqYngxB;af_f^F(D0Km_-2dzkBqTbf6ty߷ؼ2Ƿߠ){i %rgo\<5E+XPyuKK"N8B*9{ KZx_\'^ba&|@{1wp{@`.$gXJo ggEPw3tfK& wӐ)Lo9QMg F!浆ծݐ?B2 &g?|W޺ЩνYW=?˿ܼ*ׯZspIxv/!A$XWg~7܍Ř|?M*n7uPQ|UD)=n{Cm G* „${hO[XgM,[JQm%~N?KĮUw|]U]zִ׍ϱhJίЎ1Q;ۖadE$A|*xDs"CG@ىhLPґ+NLVmK쯪NMc9„Tߟ$G^`,95 X<"Sb1B ^uJvx;2Z{M]hx~"|lLʥd~WLޱ̢DD=%; jm][9UVJf-"?ھIQ  G=&siI]'f D^Ht)5/ <d"G ĝ4mh++3pWq3X_g#Ik;Oys&̞^~]}% n{Ecߞ^dE ^OQ1<(b<5i[䫊Z~@=n|jVtj GB$$3Vv-}ЦZP Ef^ Dy&5SW mI#&237O9i""kW-@ I-"<>xve׌޼5i .DBO=کf'M;vq%UGO<  B]䣡Sd-,9]_S:.jXyGo:DjKeRnA Dcyy} nܓwbS8x@o|7;*7Dz&u_g_o̿x &$W<+n~$"V%KwՋ%I\yvRs}|xg=n?BDk|~N_.pm4BnJ) *T6uiOJmg[&zc}Zd}!YFyuC`p>Iihʹ\Ÿi6) ++0$EbEv tpBrʱNoY2tLnoZ&&?uf/QS{-;]1o-]o4h_xj_n޿ᗞyvK]C%ş|~= gx4QZ[&)%0/<ފM]"|ݷ{Drzg!6-Ʒn/!B[İað凔I +oA !t82h}[w&(l"B?S*[[)y !BaBwx B!LHEԌB! Bq~ B]08!B! B!BJE6dO`S B]:FUڡS؇huPm-ZS{%l<Ұo]OҘsG^!{/,*wānԘsX[WHaҔ;`æu-kˋX(չoFC&ͷ+WinG׫۫Q3 \ҙ#񷜸RW[V\TD) 2dIiJ9B![ ɘ[ʵXAٌ[oKb-Ao vG9g9&yt(*=d.~^u1#}EJ&}zf;Ec?aÐ]Ą|*d?jSy:Uv6Ʋ@–AB!'/DNcR3'Vo0ZtV_ăAg+4~ncF$psպ%$%+Ki[g J;dm'v}$=$I_. spYl<)=ֹoF>e( }?e4dj2!+'R٫5{ v|oxݐ IDATݼkV%ƚ x\Sx2׾> ?!B}1! $QJ\`8~WAj-yaw3+.yKYlOo) )yV@rSY J4f{2u 19j~1u(O❧Vlq?Ma2"G=C\;:7O;Oɻj_+ [d:޵wZC#xscVb AiPi>$W ,&o^o<ƬJ)|fK5 C8\3.EۗVK}RE8術Msc-Ķ]&xaʹ1+PfV9AئyȑjkY{B!tqeR;A*CVߖ\5FSF퉅%\<]l4Lt2L*cI۪ei";5Wm9m\;pM~i +=%(5F9|M[Bc}ciBba R 1n:Py6DA \Ciy=ڤ@Ȥy\P޼*'R6 6@YCi_9 }@^ASZ)e-fQRS&)q`ݴ +'(ȍDT|H&=-U:FZbeFj |{B!tt+WjO[XgM,[JQm%~.|tb*C;&{Z*7 $_pcKS_XЗo܂oH=EYw_l,Tm\YWaz Zfѯ;2K9- 7˟8zΘ7||5<4-MAT -'NP r4q TSF}@$HD$96sV% Jkԫ5#J|HLNDB $}x81 b.#B$9cWͩ \<"Sb1B ^Jvx;2Z{M]hKtD9ߙKVcE4Df"&9FЩ 1Ɇ %ITrB"ĨW#Bɞhҹ۴$ޮLh3d"z/_$:zYJ߅DCDB2#eVicN6q8ΫL:Su=b9A!%ϸ[mڵ. {[@]okLgZK^Q3nz?uUV;b)9o;s43&y #vV2ڙ[R2 Eǟ}tY/>Q.@%(J;'eR2K$XgH;΁ZnY,Yb.q ){soh'N;9ޮPZ S_p!:~C8Y׼Fk \ <9rGz_m.=WJvWSRs9FP{)ӻn/LyCEkuO:_ySJ&OjڒNR x6pTn^In媯Z=zl JNq&*/?g2J1"%AR@J)|J" (T@" mRڦɌWcdj p:{R [rO"Ѽ B.m64BoNfzh$|bnxXnXO/y[˧T_7P:%&#B] I^$J(@[wx,IU/Z4'y&ZsuJ_i7˟A?hDjv :{="F6ec!ˉԫl͊?tkGH"n:Kmt(w gw߲|JL#d]BnSglw خqۮqx 9YҾZ$pm6 c"${yeB!dBRŒh)p [j?z^&Z 6xr6lmҁnZdw)`95{ViLĮ$$ba.N8k7e:߅kWmtJ{YxkqM8_,#Bń$nJ.D!*}jW8j;j ?Wk ]_uɣ\<_:4f{/[LVhLPϳҗRP1ۛ~nōlQoFmէh c >9"ޮ1$սyyHMPZvh2& m߾"t0{ LcL+!RoDPd1y|~. 6fMPJ3["qdP_7Xzq)ھ^쳝Jt|dO-AlZkQ&2Y&S΍Y12ö ,6ͫE Uk\G&#B, T"974oO,L. `aZ,gRM*,V-gi|q#z۠QLykdV}msŚr[>}{gnmP@c~ TG^ eqYnTLb#B-/{S;A0Zn.H,a<ϲ^/d=ttw͕ΤBȟ&U9:!Bo%$aWQ27%rgJ7WJf.L h;MƜAIξ6tm*6hԙ%DPw_ȎmU[[[|ޣXXu9TMA qOp! hR>F!rAyH\.$mf AWV>P+ͫrzI!NjEb2J#M|GI!Nyvhur(Lk 7BcS g!$Bp@dVil"h]k9sY;4$B!u\>maݞ5`n V*5Fі=Xlr‰] Yx"iii"Pڿw]8b(?Ѱtiy.h bJpo<wrb%)ꞲS'I̊H|sCmJC$ WkF(@",  AII:+X+-qbAp]&wG!Poxۄ ɑ? jNMgOB 6sjPߑѪx;l@k;yD9ߙKVcYGD"oy.!F5o2ND ަ%vex@Y. {"ԼRO.$B&)3JwҴq^8f֙3$B!.ugܭ6dZT-ւb}.5bx&3Q%G7*lnL1yșH<[qIJ->Y,!0dVc头BA`KbܴDxh"1Ukt"L;JSg!F9VXx緊,:qmB!. İað凔I +oA !t82h}[K. o&8ѱ8!$N{EU7]?R B!;0!A߃śn BaBХ/Ҡf4B!&$]_SB!tPB!&$!B|vL?ii75;tYlBلD>d4ECΆm+ܧn䗠ԚJfc-(a㑆}jw%Ɯ; xaaU#&wƜ-ߺB 뗦c6kY_^BOSg(5pu*9j:ˉLboIJ;d|r&v| kKɕ};uޕwyYј9os (5QJ- mRwRo|PqObٙ\yS<1Vq/p㬫fO/Ƿ !, ,Py|O >g?;nYXPt嫷^l$Ijs {BHHX 8K^+XZk:[7~zB`AZX:pU떐t=1skY!g|ۉ]Ib'Iҗ\,|qր+n$lu+'R٫5{ vdޠ[ؖ($l2kai~^%$7onm+v7ex2roWcͪXk pXgg5 |ohi"Ibg;qB})HxKAzO]யܺQ./zf=u̒NP+4xȓIBTt w;}mq?MߒX.^4f>G;/h9zK,| +rD]c9Tɵ#H>({$аeL@],}Ex#8_a0:7f%F $ %AVC2޿ bͣ\@m̚ꡔbgDXȠ8o g76ڠG$yXnxh8.o^ :s(((7wl1~>c~ TG^ eqYnTLb#B-/{S{-7pTxgsL$0yr[Xgu:{;JSgR!odV {.:A]i6Yc)\ɵr?ߗe5weeW |xMɵy9]j+;<~̋4 ?~(OH2 +k~<[uSǗdXB]~2>*JF߃Rx|J7񮿸N8L h;MA,ǡ:cFS{]6INyϰ\j4jgRcԘs g(pî^8vwג 4dm<=grTmY-_\u IWcrLgK/8gcZKXXuWg`!Β6K<(0$|B&k ׫][-!s@υd@xMfPl[g @̡W!*B Պ %dܔiēmʿUa[x @adz^@n(%2ԩ#g'FOu@d0c?}MBkmO!\&ԜUw/]%̫GٳL?>b1?}"n#*WDq?3|p{0}Ah+hf+oq<}GF!t'$rvu{Ă]~%(ZF[`IG'v}2dirЩ @>4}V-(ztYSnrZo?+pq8?&1oŻoIh⬫0{שkXr`{d1Zuӗku~aG6$XPZ  {Nw@Q H Fq'g(YH8H)y,涢lX *YMP\+(lL^Tm䓧G@dr"$무ct"Iv tu@wo_-B+uըҁO4'zphHP?;A:{>|]UVOx!5@|[& IZiescѨE_cdBsBB\5&awNJGd U,9&"S Ywo|GFaﰩ u~H}=2O=r)g8&r= LV2j$Qm{K)y%'5WmI>sGLIf4p {%k@yN oknSWFxCi0PG @D %>B%% HZԂMIiΫr'3^JݒE2*õnJ-0ni[>lF*7ͳ$YS:urÒHaҺaA`=Z~DXz^fRo-zSY~ӿ@idOJ.6[p瞼‘ؕ}E g,.hfW< Vżvzw&QwO|.ý&vΌ1'[FͤV+O̘:jƽno[gBR9}dBmBW<+n~$"V%KwՋ%I\ݘ\K>My.GXrK:Ѱ= bAw{>>NvSJQPjwO EtM+r7PF!]]>jAr8}{ H"lVTyqEGITlDyROd\ 5_n6oFr֣o.qػR% j ʚ˂(D6;M7>G!tɹuJ9]o6O_"]W2j9?ٰ|K]&aIxӮihߕN &دe}w ɘ[yIU{;le tV`""`yH-h|RO,AcA+( ]no3~Yf==;w={58f~m{dȓ0|?8f[&,GM1vK J` =y`/7VIOro>9zNјکzu/$nSAܞ1ɹ L5~fw|4v$O E.PdoytPE,QPIf$e5ѲeW]b M!ą'{<{{̯;0=+>NMNrV{eMqmrۂ~c#kT-:s >I iw3qo}3k,X0k38˪/n?Օ3Yv9N ۳۰yQY77xG$mW`Ȝ>|*~~۰w^e)V˫K aT;+ZvS NM ]-n۴#J,R|p(:eg}([eỿ0MxeW `g2J'ߙw-D`_C8$$)CƘ- N6Zߴjb͕t@?DI 謅s"$u6>pT@n묅?sYU# iY84'eI>Yx;c*raw޷n+ی0mecthqk2#hp1F6?82 MOG%lSAoRC!&o#=[2%b#'#}&Et#BTiv jٳڙ4@1U2YP ^?V2:,AZ lR-Z]eHY "aq"8` 0ʒ۬.d.*FÁrpB ~@ .,^Ol I0LMf=Ud(Gk~+>?6eSva$OLp2;&IZD/K\J>}dK$asF4[_|8|1+N=vmN7Hfw*5/Hd.ѸMkv ߓbˌ:(i;Ut+K Jyb86nLu}뜃%W%e_!Њ2O(|Z` B4j'("Pv4 B>wCL$>+{dNB8q"S\kꓞd-Mz-O z5 &ߟH3|{^(/}ܰNp| qoך;^{ܤDc/l" Ƃ H7#r)KmImx5;+kT>vI}dsԜ& ի.pY?O}zj 0 ed! i KwX;,/Z+o9։Jf#+WHSF( s:A-$*,N>٪lS&!ѤR ^ehcqrbIw_rb 1FuOY퍳^k ?s>VO=ecf/~ۨ9߲N2؇iL=K:+kx(bac/GM:6Y(e5G5:3ac@Qž1'I(/0Xz/v_^#\v͗ӪEhoďI)/##灞wӮ/]p)ҙ@ĸ=# b =ZKAG?W^K N5 KBOr p6~n랏 m,ƂCktO8g-%?:y%ο}d[f$Nf,M޲I䝕<}ճ]g۔>oѴTTTU9=k3 a4!㦳># "pEe@L!PpZ $1D&@"աv5E&[V)AۥN4i T]ЏHx+*0Zi!ucv(ѬL3ピqLB11A!0&2I|p&TFQeI|6G-5«}}&B'gut?@\L-p,s"=67/+[|z,w IIGpIWX7|@?A7P5|0n+'/cڷ-ronR./^4xr 귾m;YOI{ٹ۬7wSo~Xym'6ȳ%aߍycqt'v!~gOcU}Bitc.t&&hv CR'9.jYJz]h*dw,Noܡ-O!q^p^h|,,·SErFbi/pde!00;l<,~8<Ѥ+\#RM[dqvr5pAAx0:y)!wU6K ~>;XrS:Vz!A^]?=KV_%tϥ FxmCS>۴JAzss cwNe7|-=yq񁓊m-`*ٷ=l8jbʑ/>~U?&(yֳu0뺇.Jsԯ$ {E\dVTR @  5@| Am@  qlr @  +@B}k@ A $@ 9hn=@ @#$){fwX"ѨOKn}@ _ Qif/-9q_[S7S-i/ϳɶkvߧ"xµ e}^V9:k${kwE6胿X`g儛x ҅jѧ@ XLgph?)_{C^|8s1 |O֢ 8wt ]8rR=C߷` =y`/7VIOro>9zNјtbGֽMnA IDAT\fZ?b V?m;ɜvt╉@5⍋ \Utj҇LNoqۦ8w1499nT0G@ Q$ExAƂC6W" CH`%sN7fYt7Yx0An6:kaOmvVHkwvӘ +pݰ'dY:anU0Xo|ی0mecthqk2#hp1F6?82 MOG%lSAoRC!&o#=[2%b#'#}&Et#BTiv jٳڙC1̽㯺C`S| tU<2sQ㖒Q ]ӒG͹Q:kQy£^<Ƈ/ÓY'/Jb5G<5̺q1p%z(ƭ ~fo=E1Rs+"& 0U':.Lu 0Rq-bηZkm8 IʛcOFƭ0vUOH=`TdX"vL/sĐNʲy/S"Ѕ,Je.FQWYR=Ep[ IET 竜mLctq2nuAlՏbW _uvs%U}nv&o\QwuguOᕳ':-y,KJ8S/03bX>|RG__״sͫl`$5;i9ڝuj'Ȕ(- |ͺϛ[mk<Қn1Rufٙ~(y-|Ը]1`%CGY&~??ed+~O@5T"^TX8s3o{ TTb(Y9hJ4=vT'xd te l>4@1U2YP ^?>y) dQYE9PVRA #^\EL2P$,#NdLP QBƭp'+B T*xCy뚲ƎvD;8x Ë!"Rf6Ts[78hHϠcm@EiA(ܳ&ǂ5^RdiABcmps;ؼT  '\&o8{&IZD/K\I>}dK$asF4[_|8|1+2;və*rԜIL#NEI<JkGb HDbWJcƱq`C[,aL_?e()e OhE';KH@&^XEYΓ&An(w ć{eBc YnOϼf1gE Xg3b8Py-m6Ǝ.fpeor)o.%wߟ,p^=]P_%kF Z#} I`FR"\jvV_|ȖӋ97Lo6@" y\Y H?oZѺ|1d5Ip%BYEkvkP2#vzD8%a0g2HI@."䓭:ƫzX^\|.O}׏/$|^>~׿@ˡڅYYC  +3c[M:6Y(e5G~hYJcH*h6%iȳxO1█8=OG^rݟ &%N"CTQg ėqRɨH0j@_x֙qM  1>HL9?E)LBz L%wwZHJ4k3 ePdLL|2ILƱ QTE$do-͑FKMj_dIYh!p]UiV~~oVpm>s_ _궗Wn ܺϏov[E4vY͆>?N_qo~׷rC"L7a7wӹ5Ο3i=P7\>eVUˮ05;;\L=o5$q3 ]3/<{zr;}"jv"N*9LG'0~ކ4]Da۪}͊,yqW>%wvbӊL0˫??sxIWGTƷΙRjm%zatRBmV}v.u6t@w^D+K>ڿ=zwo>t֊UH>f1~~ቿ1;t>Wk4ԡAqon_|ݯܐ)>1xA?޸UhKͿvja4hئu/f@|FIEPDÖz0[>uEbʑ/>~U?[ۭ<ٺ uCC_=]}_@ C3JWIf-.@5//L @ H Gn @ A@\$۴\B~@\(_օ@ $H!B A\0x @ A ęG|As@ $oPJ~[<4@ G}$<3eKяpd}m[V2~Ɵ _E#pLVQp{%P0rʄEؼZg$ ʧqvm7̜\R=htKB~UhsZTLȺAthnOh7#fܠ19WIϬ]கێ{"x *bgMZ6 )h-c(h @ .Tn֕_?xjnoރ= q{W%Sa.{?)5;kN|Yp]ї:?|@WART' D< {5or,X0k38˪/n?Օ3Yv9N ۳۰yQY77xG$mW`Ȝ>|*~~۰w9dYJ_M+%d$Gܯl7չԤЕM {?q<@\<‡$+Ͼf 5*o1?QQ|W9Psp$w;۽*H⁎L"Kl4^{8 ֘Ovaw\"2Zm6WEϾ?_j,{'sMc-KiΪIy K.\g+nݻvC ,/f:+=gOhyeSmږS#DN.H\Lגw.s'q-|N;Wte) zNLi^^2 iűzKM,cvƱ7\KbT&SʿYp)\P)k.h~R+GqZ*[bη+{W@ *X4JicR3kwW08`m;>?>cI?gCZƿ,/H!?#3L/})ARw~:.4n{߸W҉>v֨\E2!yΪI_t=_͹Q:kQy£^%a&~,~w$iDi׏}ȥ~lZ;p%zTp tȇĜA6+"& 0U'Z^,R"a"F*%]02iD8 IeMձQ=0nu) 9z D^!\m~"i).d.5T&0iu TfQJ2GnA0ժ5G~6[&1 8 U-ɘ@ .niYH1iՑF;b&t~iؑ^_PKpq!g$*~eͻ?`b}bVZ3>xIiw19 Dqh9KD4s}~?\ uۏI~U"^TX8s3o{<17zrx&~h?k>`-5^g5-&%]D?cGuR׋A@W)&Ld\%)j%c![A (v J/N.٢U& D('2(؀Bj$-0(' us⒱ɷo9c0ZŅ6 ҏ|?gd$OLp2;&IZD/K\J>}dK$asF4[_|8|1+N=vmN7Hfw*5/H%\2iQssCRb3gSj:`e Ug(+/DIYwCL$>+{dNB8q$‘DczDjݾM.dtZLx?ikgORV^]ϑ-HͿ]krx>s!l0 * ߌȥD.J'?U㾶JcxL$G^H2Umag|;wHiKpE|x::1BɌ`dqJ%a^'e$\@EE'[uW;y$$t_rYJ+?p bq@ .f]A݌aXzD_{IB1Y?ږ~svi~oSU={6vėMomԜocG]Z40ge %X 0xeӿ@Gݦ< cf6c@ۏX|۰1(aOd>PÄEM'G`R\ veF!JVךQFϽV!MO'Jt ө g=@T&vU&!W ي;ۋbmfϴJ[‚q>=DÌe=nj,mw2wﯯ(+(tZ҇3|j.)/qjxyQ8ol>&bEir;qyZ9?xҙ( dI3 kcTL^xe:鵤YZspɰ$t._zݰ Whmg(^rn`q4TZ{9o9 w.+ 5vm#2&'grY)׎~@ygq3{>CFIYDSuqY 2 &F(8-NɄV" P;牚"-R^ IDAT+ R'V*.G$<|^qJv-:1;hfA8&Ș eP$>8c*2IZ>w#hվ>쳂:C:@ .T*mn^ffL`nߙv;ŏ]E aCݰMn?cadKE*9$q3 ]3/<{zr;}"jv6*9LG'0~ކ4]Da۪}ϵmVdyϛO7?,ȶVYsnO+xӘʖ, Vnk?Dc{>;!K䐁IZD^i;{?+whS}HWW*!*TrG1FѸX)?vY:5Lhe>L0˫$O4 j93Y\\C \br ^3Nr^JpC*%xοՆnbB;]}V5y6VC)+K??vO"žr}>۴@%omߟyO}Ehq~FIEPDÖz0[6c 1PSY ટn<ٺ uCgau# Ӫ)8W".N2lqyy)`Jf@ lA Z A| 6@ @Ii@   DH!5@ H E kF 4@ @|eV̔\T޻m@qc ph}q>Qi^ Ї@ $*~쥅#'6k߲2U ﴗd޵M;SZ<{L?ò>Yc/ g#[Ol^(rz}m]<ϳmI˧\1Xc_xs@Oh7G#gT8rJK <  %eP%(;oiRV9<ա ldo|@ )(5f(""b>xRYOb{izXKQ>tMEJ(J@ ^cqgq0L 1[6UQ˲L]>eO6P]EH ɔC7>Ň!O:7TJo-cz7Mǰڅ#g/3(S&,:d[%oV>mn`9Ec \:GjbGֽM\<߽Zs{r@91ɹ L5~fw|4v$ [^(TKm>+lYIDsMlUCCKG{'ð*;U#750)fR%'o63o8)oH;tO洛^e)}7WsVTBS>d: vBW~6-ā!q%QG8 Ziwwz0 sjıAXUig ,ǥR)tc@8 mH,D[ą*H⁎L"KlD@Bf)֎׋:]6bVY1A;:O_4s YY:"W(LHVj4YX;_{'㍒$qޠXNf<,pjcì67XƌwWcFr^@g;^BAHR٤IiY{䗮,6 r52XrWhoŅ4MRL2D_j3aHFzTa*M'fR6џ>4^{8 ֘;f(]]99l.;FO}ǓvɏY8tO涛Z:U18A\rVܺw`sY,^tW{V;Y/~J[/ 9C&զmyL:5bNt䂔ϴMp-~W[2yR 7Mв39/` 5w* 3R@ : 0yy$ئEWǎ-57wj^^P| 2 R΂NႪLYԈguA3|*]9ڌRMwU]o7H^+"$YM|xU$ʭm.x$GM<;+?MӽeLFaj:h`js{zaCcǏgw1 >\EZ;:zURuETYyi"ljhik`RLv3w::Yws{#ZMUr,V F#ܜaJ}K >+R`.1^I'cĮ[fsOeO%eWc;&~@5HgYk, /_6Ƈcd>%O$ VN~E.ug -!'Hl<+#>_اC>$ Y1!N:b" 0Rq-JhmIkm$(IL"(+ouuƎq#NQFE%b2GH .lc NK ]t!v2mHH2R9w ҇VaD0<"s2)aеƩ`\nN pEV ww^c8V2V#l4ZͰa:2\ Hc$F ]n  GeYizĨ#u#vMV0 LɈܞEPdYNbVQc-(@$ Kjðp(q?6rv~&I2݀ 7r_QnoVIsKЗ@ B1s5ywZ3k<;՘o X"8qԜlw}~?\ u2P7RQ$zQI`ͼqkDDߓ3Ck_ng&_kɨ=>?wxh1I,4%tY;Rt^ 22HM6YX` *t,(HQ+ V FR@YK)PTzvr2Ih$,fG@8y0@mVLV#ii@98O?WZet8hx3$d2=d278?b14ETw/{Xp#U$MQeݞV:.Rp|pw*ð<ϛFGd/t. ä鲊jZLJ2 V / Ch,VKF+ Ve}  7 N#I0QgK$<׳%OYIiDhѮtG?>_̊S.A]2~۪M97EF"k 'MP6:@¢ 0 X#YTg=Te Uih$f D8 'K8H'6k*0 {"hvjiWI Z{:YVņ&b 3(Yrv~E$r}EAFY qoך;^{ܤDc/l" Ƃ HR"\jvV_|ȖӋywleب,Ab.iϓ_ ɰWmr)Ma PVhoud#FVjh8$P2uZ)I(TY|UxLBI8*gAнW &nay^3=I$轥wQOǿ+7@Dh:_26sҲҒ/ϩd2 W(Yb@zF|^>7;=:r`1,ꬬaDplw7۔g|w&r`Q<o6%iȳxO1LXto٤}{I,U`WFoduh/`w'Jt ө g=c |Rs~RW\y.d+l/΋;r">*m h􀻰xeӃ%{Bs궉`X;w'Ojyn0A̴77i464Gf˲,rFwqUg3W8VQTQ.8Et:Sʲ>nxR6Qd-~-јUP,V$ FU7շ4m:ʣ*r6:XN 2e;3yEw]%gey@'is9/ZvRk%Wo~6p6G#m[z.n+xbOU䔱%-k,k');-E=s׷=e&SG,bfuirf( XdTAi$ɪAQx<0lumWeKch J.c("%BbMCF:o{}"k(3eQNS 9G ּ(X1>59"3JKWos#5B$uJ׺x4]ⱄ4M꼉xr¬kPiM^X~m8;K9˲ rϚy0eYvw#A48Siql&38HsÞdY6x4q1HΦٍpLQ0L^h2$bbQti |z.͑eYuzLOyd~hTGգ'mv8DJ{+FՔ[R MwBk˴0:u{0~v :w.upZYB&2êYSIˍXu,O9K)cS"M+ﲥ*kok{$bC=a-{gQ3mT(l143ɖ=__ZT(C!!H~+ؖek=4aVTL%Kb~=4 ~߶zgwNU9dtԉ|.tb]kZkVw^??Wk]J iMz#:T2^tV_Sb~# +$UUѦuC@U C^{!Bhf%$+o}Znϓ^ Wϒ}_Le- 1VxL;luEnq:3{↥&:)9l}O4=ޘ<`;€MǪminMgUa|朒{ ږgr2E1Tm!1PjkO m*9WϜ4[39hYw[B-a,co7FE3cR5qֻ1V{䔆G"9.e}~<!B}.0޷jfGOPB<Ntic- ж)x|w:kʾCʿX]sktFQq_;Wvާ~X\"YͮTGK'<Æ%e9{K3c"Ajf1?ؗ|(fM&,֥')̓@КH4;R,R*1u֥7<&s2aR2L@)<*&Bj2 pbqD'X*K-Qc 3/yKO 7; IDAT5دf,AkM&Uߦ(.33;B!9 3-_3~%(V3 6O`MG^[koxh{ 6l-|T~orZZo'F''lI )=HE8/?/9|N)|k?nɬc;C7^uCUY8gLJ89O5lI溌E9iT  =W*ON*ňdmPEHFcS4vNQk<*s[UR6b!Gtb%(1)Gs^amrh QNNTB)f6|E~-e#Ԏ u ҄I#qO0!д$w|:uvtTӜOG{%!Os~V"y;Y}1Xakzo*_/WS^ff}=/\C0y)kjykOH&䥛_W$O@rtCH،=xvUƲ8Z/ "o{úr_,7sRVs{*0 *RL\[(9U##,kXH[%)]zEL19E_p!%$-0X~ cr+\,f'FjF P09OLld`j~Z'?*%WǧY s&Rq"y&+2tӑrZ4RNgﳳnswSxK(|bxҢ$*st*(y@!ư6~uJeT6+R{:}~ B!T~ۂ jZc}GͮYz-S=sZD.>A/vqб5TK=_ƠOZc{Wt|pv.GOpY|쫿8k gK^U v|k?vOj}ɑ-?ya<|GrXzuvVk^r7Zn+gyk"2{g&SG,bfuirf( XdTAi$ɪAQx-sEf,2m*G{kHꐕ2*u1si?B!g#!񷬥+HK5M=%^BǗi`t_姥<`#At]F=MY/(ԵLdpoz5U=Uw򶧼KG( @KTRO#1v B\UiSuGT̞6Fw; !B … 1 CҚgm y۝>nbm2εG0WӺP7[rCB}p+2%m@hK(!B>DE7B!0!A/?`b,!B>RRWB!tPB!&$!B!LHY'(6s1@!"g毠XC62󱊥@g/X" n;s®Y%Nsz^W٫n^xĺv^Xx6@`y{ʛfG&:trSPw-eN)뽸/=3B6#XhʠY+>G!B3+!Yyk?>v{| ~3br-mIM'涎J }W.|ߙ]F] -_W$kZWޡc;G/nXnICmEWis߼o?f!1#9e]%EcSEiboK%=YlkM ?38!BhF&$^VHGITkw\۹ϙ=~L<0{s:xxhιFRҎ{'WnwFXi;ASHR^I|bSKSљ w\,X{@dz/K8dtNr+gwI2vIQn>1Vs :_Ql1C!BuIA|j/T<-¥-R17Yb!4K-޲οTJms3ѡkp?>޶tfEHVѸ5Xֱ*zXgcJINi}T);\yy)B!JH| hv-( Voƻb F|PZ[\$ v|Pٷo-.sZj(:ft5u|W_|3VvWd->[O*t%O8E'B$~r2_ LHY Ks/ C:M!d[F 3.kp@Ze`ui&+uuÖu %MHeNTRx2*u?y~;k8_Xe sIE66JRjP /y,P7N bUY<|P2Fv9L EA^܋{q/r9δ;{?|L햠Xh3<5u{Iouڿ@⡡-ذK_S;ih1}'VS{Pp_!^rL}#9}]dј'穆-##\Q3'JA%ITe-Hɨ`l*Ω2'Ae6*c8 C*8KPcR83)" A II6|E~r u ҄I#t,]R4OGp/Ž^[}9$w|:uvtTӜOG{%!OsN"y;Y}1Xakz/(^13z);p~UJNtltXT[{B2Y$/oUKHᩁlWe,u.giI1+KX(iU>c= "ĥRC*HYZH*{|uu#ƒ_jFi+[ǓE IV!(\|S@1ɜGU/CY)P7WMTxOZXĽ^܋{qtlWӒ?Οu;jv[nZ&ri ~k=Z O2*'-\|EiPWWd-Xy/#`sF#^z OQ9Ik,JLa`SIr47"Y*qnQb"cJઑWlg .tubU"h!Bh u[6yQheVP9,'Hzۼ仿s9Jw|2ؼ 䓊4ֹ}g- ~պvzo6}g Wmsp>9Ҷw=O1'>\]HN+/]_߲vyKh|Z\$bELЬ].A3,WJSp*n'~!B3Xp!F]yHZ4\⬭!Q#o͗XmYƹ6HFcZFyKncBN{EW]ۻ| !BaB'B!&$} B8 BaBЧ@JR jB!.ʂB!„!B! B3eox71!BW޴`VkF:>VTL%Kb~=4 ~߶zgwNU9dtԉ|.tb]kZc;{ X׎+ f,ooZyDNx*=S4JWn'F*7Rn ?NRPmZG.њ1 AQ0W.| qܬFN;~9Z$bx4/|W# ">jhJ( p$^cF_LMf,+X<BW~B~. |<@>*%rm, g[ے/Nmo;luEnq:3{↥RӼo?f!5[424su_M\pWIJeNPU$h!k]"c9ʶtP3~9' $E2y?O4n Eu^Aی@,I 9y[xneEmޞ|br!^gYGGµ69*Jwg/f5EMx.!{}[!Q$RyrmߏO9'g#fO`[MW9|H^qĞ7_wG?hԗ+9ϕ_}wj)v63:Ӽ_9#kƫ> MvMRs8dtNr+gwI2vIQn>1Vs :_Ql1Clt(v9vqbi$S45)ш,J}^uAH\,,:/NMBQ(fsqBQS!HNΧF{p*~79Ɠ* 9Vo5Xjiܔ5uvEȥjUl-\-3;.KɵDѸ5X[zS s 3{ t _4R7=BUsJ-#h3h[>Q8ƨx6+SɃꒇ@mTz\|>sdo͔ܢeqvwm Kg@hM;1 -B)X }Iލ_$4>u.)]v7352 NpA/Kr">64H:q( DZX)aٱF8;,LV;qJK% "]`Ho͎`Q'Yڻ#w.m36N{mv|PL8<ɻzg{jmFG]S[w~*\q{tg_D4 x}!Q@[ۃ'"orvkf_$=rHBaF'$N `KS>i&R\ߓ JBI hͿe$grmG)Kk:DIA]99aESA)RS&UW4?y5~{WF8_8c,c%٨WJR~y^ fWo}y `5&@ЪoSkΙh42,#(=٘f%I2F>I(]W<4M;Nx4LL1Aqy}UUSN;LHfX_U.(L6U)(4=wL6;Bљ]g Umք^#CCG[ak1. wӪ7zc>1<9aT%OdXM?xAE*7ly3)"=?hXxr@1TkBr40=Z?OgLJ89O5lI溌E9iT  =W*ON*ňdmPEHFcS4vNQkT涪l8 C*8KPcR83)" L+u6|E~-e#Ԏ u ҄I#t_g$I`YvfR8EQuh$&ޮ$yᡉsIn%l&g4fCT*]KBup7 IDATfN% `Ps A˖.hxn!_IBB䲛OGNg/y{$i5JU#>zbS4/ 7lM:%|rh害AƆXuɦWUfɧ 2Y׺xaHy3ݴ֕CҴ*W ?eU0୔ RV%:P]b]"mejR͔QiZ$1*ITI-K7H 'bV_iMg EqU/PdY"˲:o (240U|ao%#EV(E̩NEQIEC{o~;6G|7~`TlOZ$YrsyN%Oh &s1o#V)c f@Qxԩ ǟ4YVFCa.}Mi$>̩zò}:rTYhn'"L0Q; `2@duC+?!Yo\MK><Nvhs[Qi-f4n Q蚖G{YAhm# s͍@&Vo.蜵4-lT7/kP@y8C$5yoDEd9f(3쫘>!&\quKUxҏxHZ+g)PdӜԣ.$x@],c8(DYrnJ{pH++\ s2VB' @RD6&Q$Tfh 'a0Fdo',eh0/"+XBwzeIZ.s,ˈfY$B] ɜշ4m:ʣ*r6:XN 2e;3yEw g+ݕ -`O*9e70/xZWo~6Z}ɑ-?ya<|K)h/YoY;|lgBj|Xgr@>1R~쫿T},Fusb߁7fx-p]#1Avٺ4Hmҫ@jE9O $RErnSy6:ALҶ[A%G@qD1xcAb Lr?TyoFŽ>5qֲ(k^c 9"3JKWos#5B$uJ׺x4}h70 P,,˥ nr ii4U+kĚm[;r"h@Ӵ k.B4Tl1l֡z>V f ~R}\.ņƆ(˲.3 !߲b/#=ڷW?,=4l{WU ]{_܃ nѹwᮃT^Pdk tv6kz?ix@=gv>{YJ%S"MϿVbE|&ֵž_'_ꨙ6*bNUTӘoJ=l; M:$bbݏʓ<`m#D=[ x?ym±*IT1Ig[.0k@zMʤ=]Bd1ۉnB~z7(jy姥}=\vIY<[K\,VZ-BA:ⅾ~hEPuw/0 k)f3n9767o=IUhWED+pB򐴦h2Y[CFv/X۲sm&ǴGo=7!BLl 1w[#Z B!„O09q B!LHq@!„O B]68!B! B!Bf ʪnc(B!Hci+(֐ t|B~%:}I=8F?M(_vٯv/v/u" }fؾ*^u'ֵ|«ۛVޤ7;2ѡ;Jk敛#=ʍS~a *ڴ`]5c( daHk\!Bͬdm?\@k{my|"TKYQl%_4 :*/ x\33ZڿHb4zCvF{_ܰtCݢ5ڊRӼo?f!5[424su_ML^REiboK%=YlkM ?38!BhF&$^VHGITkw\۹ϙ=~L<0{s:xxhιFRҎ{'WnwFXi;AS,P^I|bSKSљ w\,X{@?h4^\l5?*Qv?/= ssu2B!fbB 7iR&X21Fqў殹b;kba+0:|cR4gbvY ,1:|CGޭmWogn" CK׼/>PS_yz˚:F;"R*mDYxۖҙZh\,)zUÅN%) A\z'836Q~H7l}eO1̙f/{ }h*8VlKsoz8 c3[Ff;xgж<|8qc9Ƙce*y6B](5ֵ ZcSgN쭙r՜[,߻QSps,co7FE3cR5qֻ1V{䔆G"9.e}~<ŝ!BͬķjfGOP茲P`?Kn[*Jk`%Hް`i7@ []>>mFG]S[w~uŇ9mm~D>[O*4luwo?8E'B$~r2_ LHY Ks/ C:M!d[F 3.kp@Ze`ui&+uuÖu %MHeNTRx2*u?y~;k8_Xe sIE66JRjP /y,P7N bUY<|P2Fv9L E!B}z&hiw~ -A:fyJkZM7>^#CCG[ak1. wӪ7zc>1<9aT%O1<"X{C6K }kvr4,yaH5e/jIR`zP 0xRv:U(F$k *@2*As쉦iP hs1됊#:1Ƙ# 9yJ6|y4(''HhPCRR:_a5y>'H`݂4a$!Bh$$I.tR1Osz>}ﱿ<YtGOxf\bᆭRGdT^^K܅UT):Er#aMS 6'$SFHO&]5}3SO7خXg]!]mtӒbXWQJӪ|n/_%zAVEJK kx/ TerJ4p64Əw>)?C$(|bxҢ$*st*(y@!ư6~uJeT6+R{#B3,7%?';wCou(x\|7_tMKף=Z O2%>x2ж՛Kk,:g-MK.[U͋nZ*5p/HLnԥ띓VR4Ʉ9Gm]IB917"Y*qnQ"cJઑW9"WlWeʭ24A#_p!$$sVԶ+F+k`91DmԻ'5.gey@'i쫿8k gK^U v|k?~FPѾHۖ<Ű S\K4K>r^{!)?L'F/ Lkz1u"&h.[f WI2PzH4E[S>7Ai$ɪAQxtNlimP@Pz ePù$Ӽ\>Ui^[GQqOz afb,i*!(1"ƧBeqȌERۜboMIRFŵ.>f.MG!l$$õ~{Ѿa鱦?dݫPb-oWi=?A:8QOEL]͚OZnƪ;Pd1xbϙ^kFbgSUEOpfJ5F}]ҩJv~Vig-=zpPZsCsC@y` w404zsrvG_:M8V%)*&qFyMB\ѷ)B€>򶧼KG( @KTRO#1v B\UiSuGT̞6Fw; !B … 1 CҚgm y۝>nbm2εG0WӺP7[rCB}p+2%m@hK(!B>DE7B!0!A/?`b,!B>RRWB!tPB!&$!B!LHY'(6s1@!"g毠XC62 L%Kb~=4 ~߶zgwNU9dtԉ|.tb]kZc;{ X׎+ f,ooZyDNx*=S4JWn'FjgB6#XhʠY+>G!B3+!Yyk?>v{| ~3br-mIM'涎J d+m̮g̮+صw{-^б#7,Ph$䡶4OAY&k\xW3ӊg*}O4N{[ڵ.{8sދnYdnSl PB _M 7pIpoĘ`7l{{٦cJF+K <ؙ<|*k hgבs_sۙף# \'\1dN+a<ԚwΞ?=gΞS8uI `Up'7,')٨ٳxs[š@YGE3(=, / `˶1Ƿl6˕8楶Ct7СF17 HEn\iT/# ?ɽ2 >  ע DJ ySh-Aw#kNl_s65 4_jBИWH盌%LcNo8eŖ^8gcΪ#Ё{Ð7qSM?zGte]k@|uMɍXoWX%m CIjI4mPrvD2C\;,퇻٤Yw`un$& e~2 A@ae.mm?l4Or>mR kd@eJ\\֍VK@B'>NVePچH2׳d.7\rnE)7(l1l2=*d\aY2~_D  WK,j'Re|;Z#to&`o0 uGVS1|_Z~0^r B!xGRgS9Vc)Y5E(n9@k9F9أm m~$ 5g׬n?d8nN]j(y8*غB=+,lҖedHRr3ө9}퇍2ڏ49a:2 XJ6hl %q,  ȵ%Hҋ'S2R´J77Sɠ~G4cF!A> <N?=JoxD(5F9#w3[u;L=@M'w4v7x={ܼӻ9&6x/%:jqZsӑ&gɵ2/#lè`G\B&f.majy؀ h1;#MJ1}xBczQ>JjGdOʛh, |3&VM 2= aJQQEh$ Dµj$m6 G sY;" sAAs\9G=Tjj=>XLmqBOT"in8uĬ{nz|jk7gSR]i|gpjz֘9ݧ7Nqua3JN_*ˬEY-`g4|DHf8"d C)@} EHV"yxhlExhZ^ajԚi#'D`H DR%6+ӣZ:Ӊ4h[! \+ ѷ<pU\Xd UõLxڗOo~SFAo{:Z8;x*\:Tda]zYDl6ad(ژ6pAҧ7$7RoJ::Z,$` 7(%6e@C&p{ԜP"G qC`2%eFiE4mbi+#3W1m4<%{( rAA+'HJ=6'c֜R@! s֒=v-o'uJ%_GtR&k:ȡ8 IDAT)KLFt4JF1t oH`.՘-m{AI<{M&dL_Vn$(11tK1yuR>$!FNagBQa✴|]T pcAARdĬoYsdkN_2".AO?6ptރc ')|sk:E)39*]E˰ 7hn}uWZ,yaшQtݢy7wCtM"btRcx|TsR&&T D@ KA;@-7 s +1r߃+Eь[6Eim)c YQS?p |eIŹeZc5x.Otv7?~˷զ |]nQjcB<9?a=Z)Hwb?lv _sߧtɼNJ;SF標e32K7ܖ=s&>+fy[_W UQq=&0iR$yd:(_ͅxDR!I #TBۄ*#ƴˌc[2|PeJ6M[O&u\\gixLoZfxhȠ(\bndPnO?NחzNd6hyi}Gq79}G t# |5IftJw5gQ~l#S= W(yK+Rs_H\?_Apg=I D5}~?}fwPsܕ/$3^!fQ \[܈*31\d.t[צYy͓) | Vi8)-S97A17 Jyhc$0ktšXvp%a3VgXgz 8q  _ b" +3fB.87:Nq2[R.zvoxv!ĝΧE 2WIb^ BAAAP '   A17~@AAA W'c}   8AAA$  |'r)N 9iWk۶|:+;Den;ښj˗[Ъ`MlÉW bfeU#>g*Y"3r- J;b=E(Zp֕x} 8Jlw9eL4TwxCվz$1;F!-#Q2`ms 6y~(hDn)3=&H1q  g܉T:pf*!=<m8tv;\,J\o$qV=TBˁ'~0y_Լ# *nSqDn09 e2X/ ;KMW(ҏ< Yz &ha U]L\PF8㸎k3PuwsQڎCR}sAbGPYɮ]{e hneCe~ŢƎU=l|8ZZ0L7$м&pnw{; (,T2V`? /);R'cǾr-a0h>3,P]ͮ]m6`.~ ,.v_`8rزc w޺8׾/\*~MOgW7B\<+i;;y9[p9S Q~rrZ=7U>9uJ[4ʽB߃$  &lS|K*i\>ck^j8Aw?sH^4gbgjo=}AieRL?}m\EtH,S۾600 a2Q&Q&E'nίٝZpȗ,!VnN:ע w:ϑf35z45zthd?䚵_ZTWWJ%rIC рV铅A{◮Sh-Aw#kNl_s65/ɳ|2Z1ETDXr4_Xlix6:rN Dg{(CzeO7UnuN"p^f9XF!3fy(мiRIMA(sqTL8KaCr#F`h{H 8zEϷ [>۫L ,(׸vE3_h4kLmX.H97"MJHNYw`unD@`3eZ |'#xaZT"Lw@V)}*6ǽa鑛h"!XTF58|¼&YP~&MJ5;Nܼsɲҡ)3 g[򄐖;E =cxnBTf^~7y={N4!(PphׯbTUZoK>}C,is_y)~,\ts _ n] 9x9X4cDNa8r8Hdk+mF5;Chϛo#9.EA!ܗ_UZ ˕oVI\PP?`Yª /wP< B{;@k&pnμ!.wضlV^h9}YWm"93YI֪jh\.|9~eh'UFJJ GO ˺i#cÃjk۶v^d~`ܿp!/'vޟͿM-gJ_;RG@ eV+ 'O kp)7:ͤ` ZwfG82^kvt{?*]W~ huJ37AcA_? lt] Sg>?::L>Jtrv;*w$csv R2׻Y[>KoYgWgGsZzyZshM שk^0OG[7cZuv9F9أm m~m5g׬n?dೖQquFfnx(FO;ck^VJ.溻,lҖedHRr3JnD_ac㪌#MN:͟k^͉h }ͫ95RYaa[c}G?XF B\ zxed,j͡ឹ7vw ۖ(g|-u*vOy՜^՝?7(QUc}ZL*\Q*(*!ڊTJ$.WA|'F7n=2}sgx%^'IzC;:]Ee{oRq:ȒPI*|i5@$p͛;rO_b1  [1~ (/49]Y037E?KJmfKmi#GF=i4X()\A m.HHEE8=gdJ"ӧGͷihJjGdJG,"G 5odaJ .rAX;NcE|r46@в.r2!}K^X"IZ|eyV@B&f.m ֨]-ywTd@8MKL("Mu|D΂nal#[@$UAB۷ xCEiӈ 8/ ucAׯ#85i|kPlIRw nHHA  0?޹NOl  Nt4Ol BVYGG*U ,7px2ڥWe?$I⑜^x}aZ˴g맒ecǨɓ:I גJ"_  jtz^`#k>׋)ׂ rE}JmtKbQT@@Ӯ Ɗ"KJȂ+*RAB|[o:;i֖IoDj.+Z[d=Pgƌ{{'NO'.m0tc6F7nО=ʦ.'~}~~jrf R;r}>=hRcTq)+)m_S6"zNl1h#񅛞|'6Z7?mԟq{6"_ FW?1sx{VJ=xr[}cds_˃Hĺ^ߊU^noթMyѪo*&HMf(+13Ÿ3wV:@ .|Y-`hhDb+!DB!U{E5ƿ@)LYtʪh^bghk,oVIycF"6 Vhnhz:B\ՠjjlNt Z.5?|nPSs#ǰP$) u *Ny/\8<.H…U%J3. ,E[[ۤlnEXT)tɓV]D}<,l6ad(ژ6pAR2A&7sVǮړ)xP̀Vq?q/|eɲi55WnV%,+!(dx` ZM1Lozo '֭oCeM$Vl{CIAo5vЧ@9\,dfwvAg]oRQ2ɚN8D}kLFt=0 HcnKviñZ-;a#S$=A*4ƲFTMѲ]7C75yh3q3}Q[)E S cJPbb}.$2#I'(1YYZX#E^y㊴lAt9la2T(+]_Q(@|"$}{M2fsm e|wMŋ#K5^_h&M pa vDW%H,p׭MIFR:NPWGP$Eͮefg MUյ+A#H|EW"jܹ՗QNSY1nф NgbdDXi2iZK*6r g|u|!-kn5:0S Eޠ?doUu#oθyMwquPRI2"YY Or7CD[[`0_H,Zv^gAo2|DZAl6su|IŹ.1 :[v0;kG!=?cԼ +~ă H1MmLLɻގOu֣+@O;,/2iy]G4CI9jo禬bku%j:+@R=Q:=oz{5lMgv'9jffٌye+msFTCz^ySn9!1#K:ڤVPi"*GS]Scr#t*,Nڭ@5U#cY ^i^vwwFDȒ;}> Be%ee5k?:Hڢr8?zL$+?zL.A*)K:Bt|dNqwZ_ V?_)+vJ !q3 ǏQ󃶊$/[kYN͢ O)cN:AIfQs/1nkO_ }q, +{Uw Z9>Zc?96k#?tXg@I{%:7?>m|)'|']eEzIDATt2 oM)>D5}]q nWnx?^PM33=YZ Ӹvt2:jµլ΍2 ÅI'O%ֵii^vJ-02)}9`FJ#/t$D)GVdm7ڢ5w%\^y5Pf;udka ސ!(M H$՞3[XF29ˮCu'OZ{ RICr}=5/5 $-͚rjKWjU뒥gwͷ#:S[~}TU}==;)L:oT M_z[z͍=={N+/%7tM\?;>|_$zqgl[ht`?ɩSb(Dh4H[%o|;V%[E i_|=(5}`p͍A 8m?<0.ͤh˕$mL(=˦yQE#ȢWdiBs0}n嶮S{jEח5JYuK G+/qh3u6_uhe#tqn2lTޥ^OȜ=:=уW:xh٬L3XwuD{|/+oE+`tn,ȰyX ݷ>`BIבtTlW=6Dwpܡx=yu+֟?ZɱkX\pۛ*S 8m '9*H1tY9BAI7ZOd(:a{/;HUswNE௫NzQ[unAnULwÌ?u4ºvp t) nݧ}- *+֝x! 8ΑIבtTl`=GwsmՎZj5 RoZҡǤS|cI+i#AA\$[&g(CMHzM;󺌻fiN8VymfO+Uno}Q1{0Sٞ7)0ó/xW~~g Οyq֠_?֜l-]o[9^c ].#o4M}LX[hqX7tSGc s\㲜eQ.:ۧW E" Wx[ɚxгN[h,3roNm8عuQKhIIIQg{?UHV:-S=rӈ9~y_;f/7b@LHE׈Jn+\h}R=vklC8<ٶ|;..KHm9BAdɌQi$\F.{pn<'KKu*Q,+=7tn5+opTlwƓ$Q(eJX~:b]Ӝ߼;KDIpSei rE.#ꖀxђi9{8XJ<"X{t޿r-kUȹcY6L^WEǮYV֪dc -퉮=.O?4l-UfUz>]cᙆn+quDvlK%@s3Z].#wPi*sN:Q}Ճ>s~kNf("4o!pv?G \d__;tTG+=~GDV~ ];@+sEHm$I[v?܎,Y63oQCQ֠ ?4@_R2rgeNz%2W~2MF]i&䑶W"ef,g#f♇ǓW.S ?,yw*O3;]4⯫NwUoqSɷOoEd{dPcMkdM[*_[>bQ# ;oA\dM{y3uc )Ualjc3I(52HZ#*볁?wtmS=?gLFI|_c/|_#؋{qҮR  G?ziG*ܵݹ0-#СתH3ÉMa8 P[{Z#͞(APe8,*Ȱ~$.o8pJNZ:â۩S]TjGMMn,gg@In h_2|SVˉOz0Gi#skķ7U6yB- GKJT?{lJœ6<*5/|tz뱖ԫI"D)z7˵^t*?2᳽O'.΍N0kJ 3@$X*ͤz T`R f0_Bti'IС?0xeBm9BAdh%1:7$zC5j;N(AQ}%M uCQV8YR$=׵G/ЭS;*d2r̷7U+_锣1#ra}][PL:.HXNx{s? NRRd{i2Ú E$ŕ7oH؋/;Bnbvv}.q) ;g>p|p9W$ b4 ''_YsNчh't4f1i1,֮RAA#Huξ^# }D?~JShBV KJL c ̙,M|g\Fm|SnEr]5G$AD{yQ L:ePX'4{"6 @rq%!AUsಛVGy{Sۛ*_;M% Nn?2 J` Fl^\B{ 1I't\MJs  $Mëhj،Z횢L2t"vj9N;V"ۧ{\p/fR{GI$LCCnt\QtfhtRT#q>1$kݧ۾5;wO[\%/jŪ,'ʒgQ˪z'rӈíFE۫+ggV5+o9 # "âL\( p輧si9מOtJ>8a):Psa/VoלuA P L_o'G:_w7Ci%4%cmnF 3uCǻg( Uqi&մW29Tϸ6!yY>[fZ4&ej Yιav0ÚC1Vyp e-#[ܚրBN3iPd{hAQwӤa/?Uۣg/*{{hgYO?hƃMJ>U6KJܧ7zTro D;[z*w6'Eę'Z|A¨m~ݿ8U|*Noq;ToWy/.9+76V?LI3t'_=[ޤHzu3XO%#AA.X;7p+D12xźıkWmR$_~ -)   O!! B@! YN@AA+D   ȕ$i      W    UGHAAAA    AAA$   AAAAA    AAA$   AAAAA    AAA$   AAAAA    AAA$   AAA    AAAA*RCIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1688731561.0 toot-0.41.1/docs/installation.md0000644000175000017500000000116214451777651016754 0ustar00ihabunekihabunekInstallation ============ toot is packaged for various platforms. If possible use your OS's package manager to install toot. [![Packaging status](https://repology.org/badge/vertical-allrepos/toot.svg)](https://repology.org/project/toot/versions) ## Python Package Index Install from PyPI using pip, preferably into a virtual environment. pip install toot ## Homebrew For Mac OSX users, toot is available [in homebrew](https://formulae.brew.sh/formula/toot#default). brew install toot ## From source You can get the latest source distribution [from Github](https://github.com/ihabunek/toot/releases/latest/). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1688731561.0 toot-0.41.1/docs/introduction.md0000644000175000017500000000313014451777651016771 0ustar00ihabunekihabunektoot - Mastodon CLI client ========================== ![Toot trumpet logo](./trumpet.png) Toot is a CLI and TUI tool for interacting with Mastodon (and other compatible) instances from the command line. [![](https://img.shields.io/badge/author-%40ihabunek-blue.svg?maxAge=3600&style=flat-square)](https://mastodon.social/@ihabunek) [![](https://img.shields.io/github/license/ihabunek/toot.svg?maxAge=3600&style=flat-square)](https://opensource.org/licenses/GPL-3.0) [![](https://img.shields.io/pypi/v/toot.svg?maxAge=3600&style=flat-square)](https://pypi.python.org/pypi/toot) Resources --------- * [Documentation](https://toot.bezdomni.net/) * [Source code on GitHub](https://github.com/ihabunek/toot) * [Issues on GitHub](https://github.com/ihabunek/toot/issues) * [Mailing list on Sourcehut](https://lists.sr.ht/~ihabunek/toot-discuss) for discussion, support and patches * Informal discussion on the #toot IRC channel on [libera.chat](https://libera.chat/) Command line client ------------------- * Posting, replying, deleting, favouriting, reblogging & pinning statuses * Support for media uploads, spoiler text, sensitive content * Search by account or hash tag * Following, muting and blocking accounts * Simple switching between multiple Mastodon accounts Terminal User Interface ----------------------- toot includes a terminal user interface. Run it with `toot tui`. ![](images/tui_list.png) ![](images/tui_poll.png) ![](images/tui_compose.png) License ------- Copyright Ivan Habunek and contributors. Licensed under the [GPLv3](http://www.gnu.org/licenses/gpl-3.0.html) license. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1688731561.0 toot-0.41.1/docs/license.md0000644000175000017500000010414414451777651015701 0ustar00ihabunekihabunek### GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. ### Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. ### TERMS AND CONDITIONS #### 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. #### 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. #### 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. #### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. #### 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. #### 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: - a) The work must carry prominent notices stating that you modified it, and giving a relevant date. - b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". - c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. - d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. #### 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: - a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. - b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. - c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. - d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. - e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. #### 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: - a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or - b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or - c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or - d) Limiting the use for publicity purposes of names of licensors or authors of the material; or - e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or - f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. #### 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. #### 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. #### 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. #### 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. #### 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. #### 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. #### 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. #### 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. #### 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. #### 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS ### How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands \`show w' and \`show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700894327.0 toot-0.41.1/docs/release.md0000644000175000017500000000231214530313167015653 0ustar00ihabunekihabunekRelease procedure ================= This document is a checklist for creating a toot release. Currently the process is pretty manual and would benefit from automatization. Bump & tag version ------------------ * Update the version number in `setup.py` * Update the version number in `toot/__init__.py` * Update `changelog.yaml` with the release notes & date * Run `make changelog` to generate a human readable changelog * Commit the changes * Run `./scripts/tag_version ` to tag a release in git * Run `git push --follow-tags` to upload changes and tag to GitHub Publishing to PyPI ------------------ * `make dist` to create source and wheel distributions * `make publish` to push them to PyPI GitHub release -------------- * [Create a release](https://github.com/ihabunek/toot/releases/) for the newly pushed tag, paste changelog since last tag in the description * Upload the assets generated in previous two steps to the release: * source dist (.zip and .tar.gz) * wheel distribution (.whl) TODO: this can be automated: https://developer.github.com/v3/repos/releases/ Update documentation -------------------- To regenerate HTML docs and deploy to toot.bezdomni.net: ``` make docs-deploy ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702478547.0 toot-0.41.1/docs/settings.md0000644000175000017500000000563414536341323016106 0ustar00ihabunekihabunek# Settings Toot can be configured via a [TOML](https://toml.io/en/) settings file. > Introduced in toot 0.37.0 > **Warning:** Settings are experimental and things may change without warning. Toot will look for the settings file at: * `~/.config/toot/settings.toml` (Linux & co.) * `%APPDATA%\toot\settings.toml` (Windows) Toot will respect the `XDG_CONFIG_HOME` environement variable if it's set and look for the settings file in `$XDG_CONFIG_HOME/toot` instead of `~/.config/toot`. ## Common options The `[common]` section includes common options which are applied to all commands. ```toml [common] # Whether to use ANSI color in output color = true # Enable debug logging, shows HTTP requests debug = true # Redirect debug log to the given file debug_file = "/tmp/toot.log" # Log request and response bodies in the debug log verbose = false # Do not write to output quiet = false ``` ## Overriding command defaults Defaults for command arguments can be override by specifying a `[commands.]` section. For example, to override `toot post`. ```toml [commands.post] editor = "vim" sensitive = true visibility = "unlisted" scheduled_in = "30 minutes" ``` ## TUI view images > Introduced in toot 0.39.0 You can view images in a toot using an external program by setting the `tui.media_viewer` option to your desired image viewer. When a toot is focused, pressing `m` will launch the specified executable giving one or more URLs as arguments. This works well with image viewers like `feh` which accept URLs as arguments. ```toml [tui] media_viewer = "feh" ``` ## TUI color palette TUI uses Urwid which provides several color modes. See [Urwid documentation](https://urwid.org/manual/displayattributes.html) for more details. By default, TUI operates in 16-color mode which can be changed by setting the `color` setting in the `[tui]` section to one of the following values: * `1` (monochrome) * `16` (default) * `88` * `256` * `16777216` (24 bit) TUI defines a list of colors which can be customized, currently they can be seen [in the source code](https://github.com/ihabunek/toot/blob/master/toot/tui/constants.py). They can be overriden in the `[tui.palette]` section. Each color is defined as a list of upto 5 values: * foreground color (16 color mode) * background color (16 color mode) * monochrome color (monochrome mode) * foreground color (high-color mode) * background color (high-color mode) Any colors which are not used by your desired color mode can be skipped or set to an empty string. For example, to change the button colors in 16 color mode: ```toml [tui.palette] button = ["dark red,bold", ""] button_focused = ["light gray", "green"] ``` In monochrome mode: ```toml [tui] colors = 1 [tui.palette] button = ["", "", "bold"] button_focused = ["", "", "italics"] ``` In 256 color mode: ```toml [tui] colors = 256 [tui.palette] button = ["", "", "", "#aaa", "#bbb"] button_focused = ["", "", "", "#aaa", "#bbb"] ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1688731561.0 toot-0.41.1/docs/trumpet.png0000644000175000017500000004014614451777651016144 0ustar00ihabunekihabunekPNG  IHDRaMsBIT|d IDATx]y\M/=MDQh 043 1#3_b%ƌk'„$l%JiӾ;-}^W<,<] (?-=P`tP ,@ݻ:BFW@w055Eee%ۧѣGcҥ=PF^^Fhm۶EBB.ڶm۴#VYqm|ᇼ}B111y&,X 35˗Ƴ… ׯ1-^˖-t zÑsssoUDJJJ fВܻwM^@UU555ӧ]HJJ7S6:u;w 99={7QSSGBOOfffĉQQQ@UU>KFFFBCC o:ZnІPVV[n֭[VbYmhԩ,(>44?lll0`]l… hР]\\7?yrZdd$Bh1&W0x`ڵkpvvf~߾}ܹ֭*<|EEE3g@ǏO>,G&LCCC| JJJ`T[dt)]ɵsk3ƎK}۷O#Ҍ3裏>۷oӒ%KM6H-XGGGe2z||SXĢnzzۖkGر#Ν;dzrªU`ccUVa믿->7m4ŋGii)b gΜaK!uuu,ZZZ:TVVB$L?/)TTT0m4 G!!!***۷/={{aٸuѹsg =zsxJJJŸ{.z@,Zhiiaʔ)066ƺuPRRUUU )) Æ ޽{yR@CC#TTT@YYRaeeDbBMM ***{Byy9{ܶ&;_qq1ZZZPUUetttPUUTWW׫}6BCC@NByy9ÇEEE0a݋.]`Ĉ OOOȑ#ڵ+V\CWW7BCCiHJJę[UTT ]]: iLHHȑ#̨fBHH"-ݧL"s)qq@l_yyL'ÇSUU۷֯_O)uN@ ,*..PZ~=%''~^ Dz ݼyFɮ۽{w200SNQbb"3O$&&gffF/^?^JHYYYl2rww'GGGK#SSS T***(#sN#hD,f̘Aiiil_rr2 OOOŋtyޱ322H~#}* `c~,GDo>0a]pm0***;w) bbb( f̘AnnnrO>+W4ۛ<$j.L2yCQO8)ڂ j*DDD0 N&}a߾}066f3MII @JJ QYY)3TÇx }}}Cii)z KKK\t :::PUUEDDB!k\vMnK@lVAnn.]B1}t|(**իQ^^K.>۶mCv񰲲ĉh׮zj_~R+Ǭv=-%%:t&SlXCmSҥKŶmpuVl޼MzwyyyPUU쐒Ç#::ׯ={66l}ȑ8y$e˖֭[ؼy3dHN4 %%%R2>L6 SNeAeeef'LII<.cǎz*Zj*,Xk֬iб***BMM jjjC@e=z -- HIIAnn.1j(TVV",, HNNL8JJJ077Gǎo ":u EEE077gG\878y$m{fc_0ftĬYXm0R`nn[ǣGx)zWWW3--- Z/Ʒ~[5)ƢErJaӦMRKvt)Fc555TWWCGGXt'CCC#kkj.k,׏)--sμ^}>|8 T#???255%]]]0`JQ]gAGӦM#???:~8]v ŋM2DtQbڵk+^w&g\II(7op̤VWWg}F={RCJatYKvSSSΦlrvv&^,:yd1??)8(ԩjՊ233uaٲeR!==K2z~~>u֍׷O>lLpF6sذa{Ak׮em ,I& mݺɯ?̾deeEZ۷oi۶m<"''iӦMR|N0zffԒ](Ǐ'4k,ڳgՀYǺuV7oeeeHKyVVƵk׎rssY$Z;88Paa!1ީS'zKwg@hΝez5%JKKNa۶mVN***OdaaAnh"2e %&& 4`6>|et###vԱcet&L|?&"¯JZl|999?t҅ƌCaaa믿/ݻwYN$}ۉZnbd1ɓ~ GMD|/҈}ƌV "OxkPMMMZ&MDt=K$9yݝxsZS ''wiӦѐ!CMQ>}ؘо}H Pxx8mۖ]F#Gd/IKKOw988ЬYk׮2qIE]$I-wED(+kkkޯSZZTk׮[>uג>iҤzctΎ&^ΝYY Dp"ѨQaΜ94f*))!]]]:}4YoNIĤ$Z>MvvvGjj*/ۼ ,%И$^Z_99s&x]y9p0yd/ /eۆ!@Qj*nݺaҥ-9Dp!?~^ahhkkkƍHLLDff&QQQ#;;/UUU.mڴ6mD_}988QYYGo۶mƆllľf̘A2S5VVmڴZEEEvI2s"4rss8>>LLL*7?$z-:'%%msYf+** 9{eL⧛ wf֛pY={of阳s=+QTT˗/`8JSAGG߳ fI022իf|||`hhCCC֙xVZIoQFڪU+<$fffO ???ykQoݺ5.\???̘1700y}򐓓.]666011۹`pvv Ny[YY kՉٳgۛm_t ٳgѡC%BfJԩlmmٶ@ н{w@rKSxƌ@@zzz֩S'8;;3X֭[m۶zb-|rmdd͛7>_d- ---!&&"aaa={6q8cС-uZj&(11455 g9uT3򧧧GgΜr)7ݏ?đC#Gy󉈚,3g8do}}}^ /׮MZ"@1coFNNNJ6664ydfߴiuܙ]Ɔ IMMm+++kXZZj׮듆<Ƥ}u҅rrrܸ54$y>71rhF/--}AֆG'" 5~@P߾}ݻT^^N^^^< gצ =zDAAAAR t1r {dd$>>>D$n۷/;v޽K}͘1RSS]>>Q9bcc[XXoFDD};.M4nݺՠqڪU+X+Wl媉TDG,NH,n̙3Xl6Ad``@EdkkK'NKRJJ ݻw/"Ξ=Kl?}9[pV/D\C\gϞ=˗ŋY8rCH2dLzVVYZZN$ %EFY}z"^]].Z>>>rqqRUU_TRRBĉ2u%%%Z|y 6DDwa(77|}}jjjԷo_9s&͜9O.ttthz/^$@@^^^`tI9ˬPF'2O?DM+Vx-Shܹ|rDT^^N CCC&VRAAD"JOOBϗ{SSS)))m;wWŋLyMf͒Zs3ӧ*8rqqɓ'P D$%Ν;Xؘx SNpţK^YWNC$^ 0A^[Ft6KeJ}Z9ZaǏ>|"())!!!w;wЭ[zգG?~xpUDDDSNEff&BBBwQ8wNSSSXZZwޘ0aB!<΂!bbbн{w8# @"BiiL=z >>^^^HOOÇyv킕ӧO1a>d>+Wʕ+l;88ڵCnn.k_yfhiiaذa ##]GG~)RSS~z\rڵk;_~,vj9*hݺ5-[KKKL4 ĉ'SV> j0TSuC[%v]oQQQhȶSNaɒ% ý{rJ ###|Xp!JJJ .`ܸqETMM ^EE???aHHH -,,vׯ3g=˃3@DXn pիjjj#33QQQPRR·~/q^p=zwLǞI&{u\f?SQQá'OJBUU1bW^طoKٱ7nl\\xyy9kccSWRXt)BCCw'N[i ͺv-碢"rttC6IH5jwel`` -ZNLzz:8p<==ISS%8MtAAٳdճG  =F>CڰaYYYX4hq&W}ժU)ښ}xQ;̙ۤ3ѣ^-xk3:̥obdtWWW^т;2il[__yyy1vtt4D"FD$-(?>yxx<K\ް&bt555h׮UVV6HF ҫW/*..&c͛71$KP(k]hjhypydggܹse?~쌅 ɓ'HHH#Gx}Æ ֭[@/%jՕ-]L=tPiժsssCfyƄ ၴos4TSSVc;v,f͚76p*^vHKK (++%ۛ0aDfWUUΝ;/8zx222h޼yĉ8GrӿoRWWWțО={ɉH(iӆidSN%A{S޽;YG2B/w GRF)/]DDݻw'wwwС9993KݩS'ee]^UUغu++\hqFJ*`?C iVSȖ-[x/ŋM4fڴiWMM mٲΟ?O= ۷/9UEDdffF{%#iLW">x`6ڴqF:CCCz*djjJ:tjj8Kʞ^^^ĄԦd?4&&&qB}rJvν{(""֯_OÆ . ښРA(""-ZӧEDDәSLcϏrrr¢E] >Q^^=xm߾\\\(//55W$vZ*..*HJDtfgպsŗ_~ݻw 'OĈ#󝹺"** gϞ&K9$ nHMMo+2! ###hkk믿fV_xݻw#33۶mP(lPqW^Mɓ'cΝ&n477GPPPݩS'L6ouD"`ooׯC]]8|0ر#lmmwΝ ~^^^駟"Byy9.\HR:K"&&.$$,XǏGPP@,6l> --JLL+T,hFݻ,A(˗/YW#OOf H?c?\-=cڱckoӦ -Y^xѠ&&&駟~b,Q|xDtR*nݺQAAҝÑ#G2WҜR^^ k ^cMjOҒV\ɼСCdnnN8yر$jftڴicUj$t]3}ڵ<+gh":t:tX .ΉcNE8=\E"=z˴!SSSݕ o57)55IWWW*RSbtx.kjjŋij… u*ttthռF||}߿O9]79F'"i-F!iiiʕ+)==rssVSSCM>>>u&faY(##nʮ]_6Ƃ^*9;ڶmKRcty=_zD"͝;Wn5UUUUf/,,l1fGJJJ51ӕ$M:Q䑖P(d?^\J_Pzz:ڵM0IYx'q~z,]G"##aaa۷㫯BNNY2,X ,@vЫW/ cǎ9 8::bȐ! ڼy3PSSe˖͍ij!//FFF޽;qYV޽;헞aϞ=22E LLL`ff~ Rae..];,VJwPVVƽ{BSL nݒRr⫯^G۶maff* : EQQ~Wڵ /^qAbݖ-[_XE&^zEG&@y,#Am۶/迳7[>nܸlΙ[pnnnnD>>>_Q3:g 7񄖖XwҧOzO4bא477,G\BN񨥥Evvv4}tZnbe߲eёvEcǎј1cёƎDN]dhhHNNNԳgOliر4}J xgHlC紣FFFqF""t7JCI__"""x2886{Qqq1@,^hԩv!5++-W\ctIq)1C]#3gZ8 4$XDTwII ӫW˗^/-Y|||>oС~z^fw9/>>F/_-߿Ovb㺺4uf 6ׯ{O]|ڷoO_Stt4"ZxqvvvS*,,ʫ. ,..&&ct"b*++رcGoX/R!t}]}{Dbf,@'={V1.99Y*9x>رwԉlج~I^_Uoonxz%СChr}:M4Iqvvvt);Vĉ~:>gϞ'pvvficcc| xpͨgpaXh>s^Bll,`bb{4Gرc<^EEǎCBB.]Zo"O>nnnlDz̙ NQPP %6;;pwwGbbzsEff&OZFFF(..Y\KK - UUU!++ {쁧'yyy:qh7͛7idii٠YNҒFݬ^IoM6M>]ȑ#>Ґ!CxIH6oL b2޽{I$Qee%u!22ؼy3*++q}5U{쁖LLL0`?Fb8}4>cX]vqH\~/^DBB"dff"77oPUUEϞ=1n8L6 ::: "ܻwgFDD}}}z gΜam۶w?k(((@LL bbb{rrr0w\;_w}MMM^gϞM6g}@Ücl|077w}x=TTTS_ryYea")))e",appp!CЫW/blؼy3ݻ}"--Mn~{@C{nL8AEK.'h8D"ݽ{e5k⋫6uT9ަDqq1=~|||̖W`& FDIt1qqq342zPqq1IHD" f^$uO8AzbhffF_|ϣWJ744ٳgK9?/t;v֭[k|ι2)Mcׯ^z*hiiA[[O>eKjHJJBǎKJedd`ٲePRRT!!!ppp`\PUU'|OOO|'L7aXjؽ{7W@ #Fd[BWW'O_Ç_~./^+B!QVVb,sex{шB^^tuu1d[[[\z :vk׮LetYHOOǞ={PTT_~ҽUBBaa![ңRvѢE -h` shows the documentation for the given command. Below is an overview of some common scenarios. Authentication -------------- Before tooting, you need to log into a Mastodon instance. toot login You will be redirected to your Mastodon instance to log in and authorize toot to access your account, and will be given an **authorization code** in return which you need to enter to log in. The application and user access tokens will be saved in the configuration file located at `~/.config/toot/config.json`. ### Using multiple accounts It's possible to be logged into multiple accounts at the same time. Just repeat the login process for another instance. You can see all logged in accounts by running `toot auth`. The currently active account will have an **ACTIVE** flag next to it. To switch accounts, use `toot activate`. Alternatively, most commands accept a `--using` option which can be used to specify the account you wish to use just that one time. Finally you can logout from an account by using `toot logout`. This will remove the stored access tokens for that account. Post a status ------------- The simplest action is posting a status. ```sh toot post "hello there" ``` You can also pipe in the status text: ```sh echo "Text to post" | toot post cat post.txt | toot post toot post < post.txt ``` If no status text is given, you will be prompted to enter some: ```sh $ toot post Write or paste your toot. Press Ctrl-D to post it. ``` Finally, you can launch your favourite editor: ```sh toot post --editor vim ``` Define your editor preference in the `EDITOR` environment variable, then you don't need to specify it explicitly: ```sh export EDITOR=vim toot post --editor ``` ### Attachments You can attach media to your status. Mastodon supports images, video and audio files. For details on supported formats see [Mastodon docs on attachments](https://docs.joinmastodon.org/user/posting/#attachments). It is encouraged to add a plain-text description to the attached media for accessibility purposes by adding a `--description` option. To attach an image: ```sh toot post "hello media" --media path/to/image.png --description "Cool image" ``` You can attach upto 4 attachments by giving multiple `--media` and `--description` options: ```sh toot post "hello media" \ --media path/to/image1.png --description "First image" \ --media path/to/image2.png --description "Second image" \ --media path/to/image3.png --description "Third image" \ --media path/to/image4.png --description "Fourth image" ``` The order of options is not relevant, except that the first given media will be matched to the first given description and so on. If the media is sensitive, mark it as such and people will need to click to show it. This affects all attachments. ```sh toot post "naughty pics ahoy" --media nsfw.png --sensitive ``` View timeline ------------- View what's on your home timeline: ```sh toot timeline ``` Timeline takes various options: ```sh toot timeline --public # public timeline toot timeline --public --local # public timeline, only this instance toot timeline --tag photo # posts tagged with #photo toot timeline --count 5 # fetch 5 toots (max 20) toot timeline --once # don't prompt to fetch more toots ``` Add `--help` to see all the options. Status actions -------------- The timeline lists the status ID at the bottom of each toot. Using that status you can do various actions to it, e.g.: ```sh toot favourite 123456 toot reblog 123456 ``` If it's your own status you can also delete pin or delete it: ```sh toot pin 123456 toot delete 123456 ``` Account actions --------------- Find a user by their name or account name: ```sh toot search "name surname" toot search @someone toot search someone@someplace.social ``` Once found, follow them: ```sh toot follow someone@someplace.social ``` If you get bored of them: ```sh toot mute someone@someplace.social toot block someone@someplace.social toot unfollow someone@someplace.social ``` ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1704229718.33444 toot-0.41.1/scripts/0000755000175000017500000000000014545075526014464 5ustar00ihabunekihabunek././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/scripts/generate_changelog0000755000175000017500000000165614543235771020221 0ustar00ihabunekihabunek#!/usr/bin/env python3 """ Generates a more user-readable changelog from changelog.yaml. """ import textwrap import yaml with open("changelog.yaml", "r") as f: data = yaml.safe_load(f) print("Changelog") print("---------") print() print("") print() for version in data.keys(): date = data[version]["date"] changes = data[version]["changes"] print(f"**{version} ({date})**") print() if "description" in data[version]: description = data[version]["description"].strip() for line in textwrap.wrap(description, 80): print(line) print() for c in changes: lines = textwrap.wrap(c, 78) initial = True for line in lines: if initial: print("* " + line) initial = False else: print(" " + line) print() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/scripts/tag_version0000755000175000017500000000360514543235771016734 0ustar00ihabunekihabunek#!/usr/bin/env python3 """ Creates an annotated git tag for a given version number. The tag will include the version number and changes for given version. Usage: tag_version [version] """ import subprocess import sys import textwrap import yaml import toot from datetime import date from os import path from pkg_resources import get_distribution path = path.join(path.dirname(path.dirname(path.abspath(__file__))), "changelog.yaml") with open(path, "r") as f: changelog = yaml.safe_load(f) if len(sys.argv) != 2: print("Wrong argument count", file=sys.stderr) sys.exit(1) version = sys.argv[1] changelog_item = changelog.get(version) if not changelog_item: print(f"Version `{version}` not found in changelog.", file=sys.stderr) sys.exit(1) if toot.__version__ != version: print(f"toot.__version__ is `{toot.__version__}`, expected {version}.", file=sys.stderr) sys.exit(1) dist_version = get_distribution('toot').version if dist_version != version: print(f"Version in setup.py is `{dist_version}`, expected {version}.", file=sys.stderr) sys.exit(1) release_date = changelog_item["date"] description = changelog_item.get("description") changes = changelog_item["changes"] if not isinstance(release_date, date): print(f"Release date not set for version `{version}` in the changelog.", file=sys.stderr) sys.exit(1) commit_message = f"toot {version}\n\n" if description: lines = textwrap.wrap(description.strip(), 72) commit_message += "\n".join(lines) + "\n\n" for c in changes: lines = textwrap.wrap(c, 70) initial = True for line in lines: lead = " *" if initial else " " initial = False commit_message += f"{lead} {line}\n" proc = subprocess.run(["git", "tag", "-a", version, "-m", commit_message]) if proc.returncode != 0: sys.exit(1) print() print(commit_message) print() print(f"Version {version} tagged \\o/") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1704229718.3424404 toot-0.41.1/setup.cfg0000644000175000017500000000004614545075526014616 0ustar00ihabunekihabunek[egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704229643.0 toot-0.41.1/setup.py0000644000175000017500000000355414545075413014511 0ustar00ihabunekihabunek#!/usr/bin/env python from setuptools import setup long_description = """ Toot is a CLI and TUI tool for interacting with Mastodon instances from the command line. Allows posting text and media to the timeline, searching, following, muting and blocking accounts and other actions. """ setup( name='toot', version='0.41.1', description='Mastodon CLI client', long_description=long_description.strip(), author='Ivan Habunek', author_email='ivan@habunek.com', url='https://github.com/ihabunek/toot/', project_urls={ 'Documentation': 'https://toot.bezdomni.net/', 'Issue tracker': 'https://github.com/ihabunek/toot/issues/', }, keywords='mastodon toot', license='GPLv3', classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Console :: Curses', 'Environment :: Console', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'Programming Language :: Python :: 3', ], packages=['toot', 'toot.cli', 'toot.tui', 'toot.tui.richtext', 'toot.utils'], python_requires=">=3.7", install_requires=[ "click~=8.1", "requests>=2.13,<3.0", "beautifulsoup4>=4.5.0,<5.0", "wcwidth>=0.1.7", "urwid>=2.0.0,<3.0", "tomlkit>=0.10.0,<1.0" ], extras_require={ # Required to display rich text in the TUI "richtext": [ "urwidgets>=0.1,<0.2" ], "dev": [ "coverage", "pyyaml", "twine", "wheel", ], "test": [ "flake8", "psycopg2-binary", "pytest", "pytest-xdist[psutil]", "setuptools", "vermin", "typing-extensions", ], }, entry_points={ 'console_scripts': [ 'toot=toot.cli:cli', ], } ) ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1704229718.33444 toot-0.41.1/tests/0000755000175000017500000000000014545075526014137 5ustar00ihabunekihabunek././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1657012909.0 toot-0.41.1/tests/__init__.py0000644000175000017500000000000014261001255016215 0ustar00ihabunekihabunek././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1704229718.3354402 toot-0.41.1/tests/assets/0000755000175000017500000000000014545075526015441 5ustar00ihabunekihabunek././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1688731561.0 toot-0.41.1/tests/assets/small.webm0000644000175000017500000070011714451777651017437 0ustar00ihabunekihabunekEߣBBBBBwebmBBSgMt@-MSIfSMSTkS,MSSkSIfA*ױB@MLavf53.17.0WALavf53.17.0s-g0Xk_ykD@Tk4ׁsŁ"undV_VP8#ツU0@tׁsŁ"engA_VORBIS@pbdcP8Qvorbisqvorbis*Xiph.Org libVorbis I 20100325 (Everywhere)encoder=Lavc53.23.0vorbis+BCV1L ŀАU`$)fI)(yHI)0c1c1c 4d( Ij9g'r9iN8 Q9 &cnkn)% Y@H!RH!b!b!r!r * 2 L2餓N:騣:(B -JL1Vc]|s9s9s BCV BdB!R)r 2ȀАU GI˱$O,Q53ESTMUUUUu]Wvevuv}Y[}Y[؅]aaaa}}} 4d #9)"9d ")Ifjihm˲,˲ iiiiiiifYeYeYeYeYeYeYeYeYeYeYeYeY@h*@@qq$ER$r, Y@R,r4Gs4s@BDFHJLNPRT@CuA gqM*0@ WɨCuzάϳn{vv=w;;/SouGO}|??s?x+o_nO_u+k_~ 77? ?u?go?Y?'ÿw>~s˲_;$ovz'~~;o_o~BykW ?s_ߴ?=]6~|{?}Rק?ט p?a~}3o[_}k]0m;/KDxߌgѫB' CXkm 6?BI~&RgA%[K?ԥv]?gC]̞޺+Fttd8mW :@!P2ֵQ $XD2@]xtR;*90SWmm 3y8':`(,ܘL"#$@.F = F$ i df'f3pqe`Yޗ!] >m2~Yȸ8S߀XTcҵzYZ=5QCqVҘEw0OЎg^bgW!휳*܆E<碓=]fBr7bjq<8,IK6RzbǡZ`+0&HN @%Z#!ŹT=hY7<ƬAA۞){dOdP}j;R/˱\;Ô!?uzN%8(JBFp]qtYt`0E 7@P,&wBipurE PHM r_lhkqWvńHtpt&像j]> Ժ|jM-$< FMߜ =\S}WKn{paGk%OiYR%CtwF-Kn臅a<PԷQ i98&蟴q m~wbltYxxHGd,S+ i.怉r!IZՠ^ifzW^T]@uے3bcΰ%eDW~X6Sv%(?E-҉7%AsZs pBs3Q!K0jYÚh2 KHq!ʖ3#Kn_F-8t&^pb_vEd~`MNdU )rWG&b+ZaIVh|YXkG*NlP:x# 8?@/9f#)C3)eG]zSxVռt%S|;pQQ&+x(¥TccX*ENjg:"d~)GG7VICwj/0#6LRC*IE¶nL:8̕)΂:'㭥dN짏uvpܡ"fK?Q K{}G6%3һ +Ӧ4 asd[Z_MiEHyS QEf͂YlN*BP&nU4܋-/\`}H+Qdc~A\kJ\ݲ>P_vy7Zzc? :r\(~.c%;ҠAu8 ^)8Eܓ)?_ G/--.~S*tOh#d|ԏv婷ݻI#i¤iӵG@G6!xfKb`zr ' rY]f"v7n|zYIdk94c.HZv2W? dQ$ VSeJ5EŴ͓>Cp"+p[ȞzLn72d"@`F4R3Nf5`X-Pbܝ8oQh q -ޯ=R N'Ϩ[PAU;zF3] sܮC&" }mP6^W>_ɘL`)v4\̃)?b.yg=[G(/4`hFcxj_ =&Phw6lC8~7G N"W, Ge,=kBvѼYy/ Ud:../tьވT} i_2ɼe_~/'S z)9'#8*idbI9reM{)@guM2VKHw|1RP3;Zhx+/ʈ`u$OX8^ f@> d{&_plH`G떔i2jNxDvWWO|(=?ĮNUӗڅv˚?bKBh 7"'uc| |kc&(QA]Od0<BoF!! !JHuGC\5)ڌLViki+7!6F v 1z5 *O8vaX3$":}`$ӹV"œִÑEA 3n4kFI<ɦ=7]%$8_ oU9i<zv+p],s5'U3uQjuYv^!6[W _'`'~śihb_ cKffY⤗J !(amF& ] jaDwsiVi F,yepրM$$̭#]WB%(r]{zkt5j H+nc㡿%(rF擝ڵNl& GN,:P~H=J@ 2 rg2+R%%/|d9Xi3q4.1. )UedV5 R0*ñ69dnѮg :ƁbMA׽ UzO1P2m &5:XclP%.go26M>>-PdzOt sA|~Z^paX|*/r1BKE. x!:b-dK(W~wz<'^FCRϴ:@A$.3~8i"R_-["ʋ1OViQ/31D΃2{q^M=1ىcv[)6C(1]5wy8B9mczW qyݨvSyn*GT)ԍ޼܇9SZu{rY\l{j]]vTj5) =~z)Tơ^ޘ/qs[X_t3RDD1 GZr@Yc;P3]HJ'ql@g:lCq~d_cp̊ @iEUxu$}8ɺ)-DΆN8.M 2< IРIgլw\KuW!lHuFdh=p<+9z >kq_朧0U̕h/i[,t]8ؙh#L(K}#YX6Mʀ3@Yqqe_|{ ;5-z"aY|/=%hSod#:$ N&oI>dUy* X|¸~~R"A;~"9PĬ62Ee>8G"%uf u}{07<@S5= KY,CDm1~w椴BlKcwVғN$xju^ U>jxUY֊k} kof҈ 1$C]:z9'bw8-aFcEW|!N-VP6UaԥGr|]{^CSљoEvUKn^KWXW?{ظ:,c[X>ӊd0>#Z7F: >ԧ!滺[dȷC3ET/j̖F 7|.\&jY>ѴÓ:xe(BjPFa ˺&U [Vo$q кK^~]|IF:8uPuqm/u"6tԋJbp H$j+P _E/+vrk }"H9VV>:Xms͝ *]]m"h ,Mtͼ*goA=<„PtPˆqo3r=Wu,vO j0I?s\;8wop2Ȱ=h G=jʑ=H)(lsa/'n(2Ev }vQ}CmPA s/+QL%$(`3EGJq=G60t3@7fg), i{}*,%PV"un<֘;% U~wU -W6g“4s!;Elbd'w޸1kː읿$9ᛤh)5P?/&imY#2u; %g%6{@w~fN:*-@9`3Ws.% E̮ç1)b bNQ>HYO]::эwUC^SB҃3f;4Я>)wNJ$@Mc$/ f!bLMsRo쥌 O1ibޟ!$@|!YӬԀ#@+Eڟ85MI?x Gc$[[zb"Tj%h/-V S8*[s%@Hl,bW`]!8VVl~L݅I4DWSZT,9 a9_>F7W2HkNKv˕{C͟iU\eDˇ3Li] IN1Ҹ O_?IJ(]E _ε8xhEZhg|ǒYS*:q65y@lC4Xj&eːZ32KbT'Nt\-K2nQJ-Pg#"=Jq6mX9e +؟|AÃ74}X~hBTQi:Br`g,"XW֟;'$x!jSLz[fiw:Q;VCqEX"8`K1ě.s-Of&nn%Ph/Q?__y>ª^ؽJ E3>\2&p bZ'edA%bX Z(&J=< 9>%7J(t.PՋ{P`M2 ( ) |d83:@ pl3;jVt-"KmX%Ts25+KL* YOck6V [k,]Y _R@!ݪABRx&xrԋD ǧd'u&uRW_48b7]N2۶zGXʚv- «~iieuG_hbgpw*nSt|f(T\7f*qE &u=H>؂W,,!ǖ:٣rgqذ4Śk{Uں|QIO\#ǖ] 37w)erE4zʤLIO`_9Opq(QP/%mv /3cs flK^杝ѨGbؾP{|wonA;E'HP_.LB'-OߏZGztE`x=%g0= N;(|Zq2Ӳ=zI wKL$k}МhkZK6ei*ݥv+t51X;ԯ$Y3 5 hZ6( <`3T ss\@3 7mbv#`F`rJ+ivFS{.aq|'#r\+PZ ۫k}pۉn[KSq.޲CbAJ8^v~vD rT*tlA]42^"Ez X3g+N E\ȍl+T23]2eHv#W>G[D@Ƴ }=7(m0 M *ogNn nXs0,Ь{ %jۈRuT,ھ1MZV,I cVOFQŖT?"Nˮ@Exʃ%Ꝺ^q- 4 FChAP.B:C0*]㾃L`] gZ#/y/H$yc@F(.ÙV*d`2n]&ZrD|o ݡv|T)C#0bu^?PvG0HF$+T#Tsf 58M3 N4^fd4hXg/_B7,q1HY $.( W.ӌִUl?멭9>L~V0 2ի BYG4-"ټ5oV1}Pk:@صIIO܅;Go 2g\Lv$|iqj?$.0LAh+Y2w^Ƚw?CW[Dttzp'"A}i'~4 =`[*AD1, B-Ws+c+āϗC Ovv*9tim tIʷ/<:g8;Ё9#VJd[Y4-0k[Wo~~K}A~q)IܲUV͊Nlq\06B)j1"PdS[;1%@6ܩeW>{GkVjc*-!#+ˤQ&zot{-A9. ܆~G*iƐH+)"2?@ ٥cW4rN iE-n0h`~s*֑j-&Ϝ*sm;?ME ]HT^V7 a=)Ovrqu/Ki4 bvݚ:Nϑ UG(0UGqVjػ%)۟0D5E&ae68i k5>e[P צ\XTσ Pq?823}ϱNY9ӆ:7zwUk_D@U>폼\njƎA%8! ϲ7+;%C7,j\Ԁuob*9t5 P(T%%FPJ^ψ0ΛQÜj&Ӄjd60 M˨w 㼉|Tn:*N|{ ) +Cʫ ?&l&4P·.).F̫*rJl>\*Z&\g=~AXK9f#8 WAϲ#P6q2GuNCN x5*x -w l4"'o oy  ,V ÅAz7@Eco{!4jnbzT4KM+W5F}ۉ{ ¤\*$_'xFw/bm[xv4 :[w[{,JJͷ;izf4#VQBVVv,a%Lܮ=b-JGZ~+yMpK*azNEr'~Rm@+qDՔ6j)h^srL64wcWJ >~l{5Ēݜ$]mh;EQѬ~yź9x*SJCV)I=:P'퇱(5 AFT)p?TWL(C@c:7/SIC}`[Tde5t:DE3\&!ZqC{aur±'sOH&{_\92Eo4 R-Jĥ\D$ vĂ~p8^wuܲF8`tbX7<(I&0өu︰PW]pR? !WقזvjŅqt@6"9&;Ehl㌔BG^3Z+A8avB}K{|KrfF-l#-ГG"; \R&%aDb)ߦ?,E]{soa [n0->r"SBZRk4OKt ȴ;TUk%Z~7"I<1k8GNTf <m]~b.PHLYwk.bIrgLɌlDY5) Қd5{\vG݆OT1Y'Ǻn59D;)fY] Gqɷ/e>W19S Kf*.S&_$8Ӟ%YStSj,zUd+o$5iv>]]έ2彀a9 h9S'_y4Y'#2Eup[hU:GY ]@j'r[}i2$ӋDMd ?V (:P>H=J+f-7+2VE0[_ Sδ?ЇYuhuNz,e)'7Dlr Y*3 P =~yJĀk;׺{ =S.o?m' vK)JTK5߀w~2'eRs]wAKn[a8_k9k\ [LJ-J->vAekg6O7O˜m7Z[WՕ3T_~ ~9CW{.ktCaSx$) 9N~ [i^Ecx~QaG2'jr_7MCF=4R?JԨH ^$烍NYtaM_^Ohny\Xل|D`$ V|p%)<傘v(W(պY$4m6ĢK睼*ݥuzs !t#V,9iyNiX wje}xH c u̐eI!H1>@ /K? h.9!/'-u &$R-#zS3INN69cix*"VS/ b; -\ x?d]x){KÈ29r`T bvNjÕi6wړ@IC{}ϝ}z\x)㢓)VV E%dO_;(u!')3u)B  u; C]viVy.[U#&ky'+o .v @_WkR>-G kNGٴ0 %^ 2hi7ɻJeTVƨ84^H[i(x%6З_Yb8]Lclg3 p7!հk*4 w=h0]7@荥^, vz6&"[lhU -B$ٯn"y6GXm|7єe2B\6r+.ws6UZ. :re*_,O0ڼ̬d͕R -J ;摓Y%Xe\/hU("ND `G[li޸ĞFPd8~ jzP E)iH:8yo= kV{t%qcvHx%t}*U[{;NО>(-d'31Q`oǠsE #SW+df%,?VtIꜣ@Tg2&¶M7 M&,rŽɇfwFPhL}IR2~JX'X<B~< y2x.-&pRX=o`5Io@)zR(+0RLD%׆ѭGi 4K!R0*Q g'~SOf]WԗӤ_t+ F'H4 6=^sohap.}4,S7z2`Z\-^T"VDMl\ecb)PvnvjLF|6[WnRps8Ne =UU Wَs%ve&7޻;9|g~;ia%m{a?̾u>3im~JXMv{>ש;> xiD 5{ Hڙ_ :¡`s\=g;7=/F958ŘDWSQ e&d5wcb|^_w]Ox 2g62Mʵ q[T3]_α Ec5'oP xCȁzA;4wz!2w`[lsL6Y0Rb'O6Oyq8>C'oOHaؓ.'V=%'h?1 eb؀Gܜ E@#@%3:.*>AP%Jާ}ncsYA2?Y06C6=;Xi,]DvA5](ܧ='جդU+#g.''b?{FDů$&v&87Uu.FbڮACڵl\1ZTYZ.~MNJ@{v_D e<~JƳ<U_9eH/]RMq<|+1rn}TԒ$&LiE*^w4hv3t1;yuM\uG0W1]'ն(E=1?wr+`mb^u,j~zZfh{zFmEl{ 4!]' Eur65 UBRѦQtujVYJ se3IAU/}D$ڕ\V$eT.4|:(($3~G*'x?ËO}CxG3~A ~J&@yvgJ#) yzoFvNԋ:^)V}WlXweEmS$&zz〴whfUyȌ`R~`"m<%%6UJ|~sh}'h2*7s6gm эgJ{$UDv­ۉ{HuG,*pmC߯% ӣ>c' ʁ4.!;F&t7A7WI"6scŐ$塤qk4b (B#eH,m03Vr]<#ȒoTc h z=($`K~1.'f]ecɮyn3ϭkBlœW{_bҢhTł9a)*- PIpU\H`ᆶ[m:[uWqdQ 4,&O>{*_۶iW l{,^ΘK:ʘ]-J8X35u"in`%XXҺj+f=f|mH#\4#l';c 8ՊB243K荀_`i4LTCT.G1ǘ({Ehw,' !N#m>. vhf_{JET|\Q/3IdxvhΩ^2_?1y2ޡ}8:Uyd1^O„=)1saqM[a{ ~jB%zZQmTYkd&I\ BHV]{_4,v%WEk~7yBu6eahljj5F34 RsWF`|@+F>& =aXuRhiZ,y`*";G ZTӲ7aej?Hf㰿V%a^ycS/Ɍ!N%H[eY>. K کnN4}Ă{|vܤ6f&L`g R؂9e4D\G#pu W kݳg8.iǣ{q^8 ?k`lP#݄i` ۱˶?rץTծK6zuYIMs[\QW .zZuvDFl쵵jVH@.iτs۰o/Ӊak# * F'?BNzu˓F6 ! P'Y^IF>>N n%뉝5zRB U<٥օ?jbI"}2 +ciސ]tبkCidDkYJA8B0w _>JEF#Ƹ#ϋu7}D4%cQg^oOwUu>dwCID@:PH2>71="NlL5 `q=FspFA:-*> c{L5B&[Sy qȹMbLD0?tacM%D@D3Q'#U@vKWa d`jpObB/Q$2K8׭[7SA!S.Q>sFaZxPI"teJ<}Lܢafjr߸顟d* TR/eд]8Ω [(C"y"U{>u"+,4VGe9u雿!NmRRIۺ$IFbGp%zZEsCNiAR1kl0ZjlDE4UsfjIgE!M t?|D.O h&UU]D~ٿOFl & V!ԇ`orx|GV<.;ñVpz7q\H(f?QHUWk&Ŭ"r8q^U*o.:GVef(>ch뼎ej9%JnM*~w,,PQ9#4C 픫ːD)O0eWEݷyȶvo9~a ؔa1:积M@W&S e( B4)g xh+a y@;RNǦ]P;rvK:I=ΓFY*q(86i*敗:pyY2w)h+TRd+VNārq.~Me|-<vƮԀ4[-ª"<O=sG8#5j̚35Ӧ>^6a#_ mOՍ{Ŋ?L gaZ9gjfR'fBԚ`p|{C!ί}G'[˺fT/% - ۖ _(S\g4~)bͧKp)"I$X<Qԭt _$"67U@ppؖn/`BjÕUo`, PStŸ&QʓzCw;Ъ޶g韦|,6fvXfƚCyo΅ǿY:4|0ߙlP8)͸SKܜnJ+@$TOC7;o3'\#PG=T!ώhPЂB[ Y{<9oPe`ˡۆyM8 )4ѽWd dpܜżK y:2ob'^I%I|M_{n]= QS_鍡UK4cQZ;LF^s;ԔqX)TLSvTdsB zUh2]|'9AmyȀvF:qs":Rtog ٱ(F ˪l$ ހri !:X$Y |.Z ~glno}<w9B,AAEfF"WlJNiRa"/ǒJ!&yKJ7p9|w̋OgV/YRٌ#ZP4d@l51Q5˹Ko:8?FT"G~ì|/.Mst~,xHNՆPg`q.\DLؿT t;xVq0ZEqm`JHl\EfTCDBIl5h Gg_&e#~|p /S#ec)ﵮJ69f0(3F89M8r+rUBm\:8.w cJi.+P??r A :+{/z>noc몖;R7s$FSCZ(r':&#ʎ)'M@D1? fő; qO$6sbF\\,f`HtahJ5;gstÜ2,%Lw"7޾*&;Cua+]JkDS&ƚ2'$ZfGj>C|XuCR[q &  ,bYGΟ)v(V?Sve!%H}^23-t[mb{ʛt&XOx}H1S^j1i -jH yY!GtYjf%#@ 4aK ֻr#Bl=QOYۦ9;h>I6GL))A-5r-˱D{$r7D9."v?MI Q)wr) HP1<`%VnsQ0I1]6`D7-6A8acߑ:!a]qXY}GoySu)'"HGa;zrb2Lk2ֲw,^/ߚ=@_ECZ=Uei?%;I5f7@ L?Ny+z2g+g9ʡr{ bHQ4#:X`Z;u %ݩViwЂϭv:A߬ao ON`1. 9Ŝc+_aT>(\ Frڦ( ΁>$W}hǹ>dT<+?!pbӚ'z2etQ:,+ P:n]?/gH6n}GЈ]驛qt^!]1c#aM(pnm;Kbz& -s_ZeĢQ{ooTi!/UH"n/.aw\{ۂu@|U|Q:ݷ0=|a6sa r$hTJt"_"2 ,""O:n2jiУYKh}={W|l1Y:A,c+-7]>w'ygWl}hu%m)P@#EL B`e|/b?XHNZx:|Ө d.'NCaEŇ7YWM˲i\Ry~(qt脹LZ GYd68?iDhUTrx-_?h35Ĵq\]9 Y Ut]`tͶrMu9NbqL{k}ue3I !Ryc1(Cm=80&Ȝ@d)QU #"`[CQV[`Wp9R@pLcyُ #ս .~du'9}.ҡnS`TĤuC/by9?tz6瀶rTʚw)4g4nd5ÌF/_̵? DqԹI:JN7I*a$A}fFJ 0 4tnBA"E&.ٿyrJN{^NG\C-4} ˌaJ({21a0}%T;tJ_9ނE4&>w|}^aiz)K/`kvNM_8= hX/@Af_6 0 ނd[%g _|ҵyw!DBѫbS]{V!X/+o mUaQȤA$1=6s5Lﱋ'[n橰pէmRuߖK|ԑ%ޒ/ǔ:9ډS5Ţ-U>{G)nG_bA 6Fdf5pm^iO-_e[7-÷],I:XoRn[IV |oCy [is)H2YZ&`c-xɉWv?^*nK!B 6p%oF̊mr "Ëx-2Sء=nC|s=]iCd<AQ9T!n@S`?QJFK<ߴ~z1D0Vic#̐ Pm-ߑ"1p eASӮM)")>$//J vo'E ΀<#wSkԡLwnoQH\=KP^k f6F0ۅ螦 f47?2j9zt3I[s6Kyj3) 64MAk[ cAEN$q)Xg.%n2<)ʸH,m1^sC٢'hl S4A xXPE}%TyZ: LJ7Y~D0IIG2{F"~Th_乄)GT˗9$٭RQza8m'gng4?h݌1ğ(AJy [=u4#5dJ k Y '/בQЉDA7y? YۘF\]]*n}XҸ+'?N;tkSN*CZ Pc^9_(vk ^q2f[%r]Qq3Ho.|}J[=%ol \N5Ҡ iM!G\ل'ӨUP.1z6j> EM@X %` j?/ȈΨ o1HulN}L!r0FX/+o mUaQȤASc,ȵHghm95/7=G 5M}C6{ ^۱ 8n ~pGM棢;#QO_2uvvaVڎ8(-p?$[2^U&gv.ZFZψ^78!HcI;5c5}rY=dǪ,cP~^]?-;]Nǔ_lx6n, qEdلS GTʊ5sKm>. Ü/ٳF6+mx(_4zYzC߯<+H&_@MxUz}Pj<-g6V}Z2/ԟu ҂U1l}[ɪKwH&E)!IὛyuPeQm3BqFGy@PMΆX/+o mUaQȤAQ~ M>8t.*y&mHE ,AA}~tD"O$2PT'y_;52^kf3NY; r%iD|L}r0xl Xm(GCsQUat@F"TfI{?&u&gWv %ʇ UUǚcHqELѠK,~8CFL[Ol:,FeCњxɛ;;G1BVu$!W<`bK+,m]y*"-Ў]-N{ٴ۲ t `Ҧ%B8 q|y r1`ս^!֮B.1G@3TZ?Mn{;3Dh>,ǐc>&og% &C,_pd,/.!ehGH:_FvZ<+j6fhp,>EWS@+7WA k9'AutgɼRL6'.3)V4e5G _$Fښ>;]IF*ǻvɁA2 PH}d?Bj+y.L,4 53U/ ؒdK=SA_bg̩#]%ˇZVph$|tXu^4A} XҍAayl'0)'C#r˸`3nLEnAHH-/LZ= {At$a|7Y jB5 xuryF/NWƺj_B~5BAd%HZՊ}.'Z@S;Ҹk 3!؀]iZ m0?S `pq].'ktX oG ݑq~6 K t7}c{@_Zf_aG2[Xi3Y^B{(Ē@@AK |{ȮΖUF^)awx00 i?, Dusb!z-.JtTH}z#_{<&lzCeFy ,ZE+}4wuH[d'+ٍhxH;}W&|s',~S.ͫqX&5bSf1d'gDsojldKܑrW˖vC[pWqWWg- KcDx61o1FpU!DE.l]>2ۦ: X/+o mUaQȤAM mE/߽1&#:!k?#lF!|4iܘM(a@;toj>p\SAnzQ/>¼ /LTǾϛ1=<əUo* iWV)]@(eb&F̧v^y 1qx*}X])oOL43Pt[nVdy2Lh$R0@Ј Tk9/lfYsZ655tC=|RAݿP$Ew(Ih+ ddqVܿi e}4uCyrhhd_ĖЍfs2"߀";pϞq2;?^X7j2 3C@R>8.c:#R`:PuL[irF"NNy%I<uct>,axckneS3RG?%4MQaT~mPdHd0{13tѻ@)sη͈(aŷ}[)68@7i :c L8!K:LIvGԬ/LKǿs)?U3l*C8/pDf;Bf1e?rZdacc7ks$"/6( ܿs,egեxbiiqJxC AJd̒"i}` V_ (>7xB k4 C+ۺ/ *:keX腂S1[A/D 0[:cHa(}>oX/+o mUaQȤASy#ꍍ.tMm']֞|l7P{%w9 U /OmU#/A"S_CAH -=>*y7FDRg!N\OblKS_7* N? f&rssCfj[~kXG#. 5"Mm][Q/pcuSiܭJXoIߖalF{L '/~gkz/%9/\D0LFjl)Rʃ񡎨'.2i!m@ }:=^6|!G K!3# zBX Lk{:ط!پzI)suQ/ عr˗".K7,/H[P1wH85<mxTwP+ eוq4Qg$)}m%B}鞭)Db&FG~hbtսtuHwN7wb--t%2aPGe`&}T!j.Ӭ (.XQlS2Hq+9L[<}}@=Er$>&$rZ|.'ͯTCBs&hĈTa:.rk+KU y>Vtk\B u8a/sυ3ׅ.؀AV9"ჶH2ٕ]=V- cΡفۻ|śg/ 4xLO@)kB׍|`8h\>lUM/ MݫDzS_R==lzxEQT};i0t?⥔VR Y\N=RPhҀfB_3{vV|̼!`~)5 4H4of=<,Մ ]bWm*)/[3dG)_=6<.궦\D)..me VlкJNۏ!^#_:(R}UT&³`…s4#*1cO.̻_?}O#d;V)LVp~1s=q!ai0UJ0'r<ޝ?ďWX64\ӥ#Z3K󹆉y~y%{~Ͳ)qҫB&cFqi&BKjs,^  ̓d cW7;smmy0LU&!)W.W fHf 3t, @j!]ߙQ!E0Mn,\w ]&֤hC-zڛncBNaN(#~,yqpx,R˞.tUDzYYx#݃9`Ca:D.]#+.]r_?&7$NZM*ʆ21xC=_TB+} B4orc*t[GWgIfNb esa(8 Y"C,AxdfkiT3R 9%Gv<)Y_pc2aTD+7(ţ)K Hj?H7ؓ[$ty1EQ|5N dv1x s7[#zuS&#-OS*~#FikO$٧AvȢŖyK4N؂wŊ~z~gA$}ndBi6~Ni{^ϭ:YVX/x! (>4y{#Wox_]4.3)m Ͼ0ǹvi O*x>\D/ Ɍ8e /4_8/7ȇIaiwSIg0PG8a+sS؃)ӸcD\unїsM!pܹxg<Ј+!v5.ialmCҁN8%-ĵbBjk"5Y%}yf{)ujwG#yZH9"@5Q82uH{5npGiHb5n-`Q!fg^lrݫㇼKǥ%VCޜkuRLJ,5|.{/>>Рm>sqf.?GoI|gbWV5p0~5Ekg Qhxزs¥ USߓ(–>AEd ePdWzO+(KDx4# Z~TdșhӪֈE yT f_ִ a+J{#%GD F^ qGOA G̔(W:+辚ĪٝjP8\Wd3(ߴmGbmlH!iћk 0F'QP{X `G7iEG"L߇,| 0|6xFzƢvxh5Y5+c_;J@y|/6]vέbtM8mLϥ^ RҕiDK_q$`d:AyizBplqW}'= Wn+;@APytvUWxSL;4E,EDx` i9b+}[+\]֘ ,V ,'[9[ ꠻`컗ʑ7 pGtvbH[bғCnk9'%z;?<@OT4/gOhIhjn9Ō#]=-[So_ZLXH?7!^lѸ:#ʔ^xwT SuwӃB/n;H2?SEpg-qБoxOp2(٨Jno#EX9m9{B~>fq AFZ$}J~0tقISeJ h.5kŭրvF./XIfAigi;|U x9\.?aXNi"( Сѵ~A^[/W2n5ܫE=1(AY3*i[Plԑq֠ &vA<(o?oe~|!>hPea[piiV.YZ+u|&?Ј b$;C }GwKՃ$6fx@ l`.R@Ҵ/) ׯ. v+E 5hli _DjgK *O} 奟Cʔ9ʼKVfWRX[u ʼnއv6w-r88h+foGjςa"q|,@ w`<'8)=AŲ(2a1¢5.3S"V3m"{5Z8q 4y)k ujCoX6'c9u-H؞6}V.:꽁{#Bŝ։=s^dmvc2JUFH;鼎z)VN% c!޲kw~W94o5S\IAgUO*$iGG\+jϮ,;ڑv]]Wtj^C u)A8SOTIwO.{6CsH/Qp(d·B̀`%ATh8?C-Y.{̈ Q5*"3M0 6ثLkDǡ,*%?Nϟˌ@~A:\⫖4+vK/&&,+C.*QҷgiDpܱw=i_og).,WܽhZtv 6Eqրx C\)a^ 2 ܔ9Lo[;ñ3Ecuxܽ?ym󞇱s5A1+%TpDnqrZJm>:@؁Q l/XO Ë%S?>,_6CqdJA)Ycz0` uyg5mtc kAx aY]uWXhH*Be:0(<Hk9)QΛ@⶚dD kwWuAiaEa7Vw`w Y!VɉA_πLi5N-&*/x A78 hCN@[ jqa}UR%`U@j=j`g"=v+oK"%I  up\뮳j^ha?o2kf sJrҢ16nf5:茷 TJ ߻f&@idw%ՙ_03a hbNoi{ghh`2+ N kǃv@ lr-.QTo$ W[(uH)Ri l/6jU+ H\+vUXvRUXXWwᯟ`* 通*0EXuIYEx"s -ex@AZ~ Ŷn@҅%Cc#At0ϯViҰ3zށ|cŞ:Ь KY= |z, JcUUAZFIN8I=Z+C[V?Z gںt?%0hQ mKh lZ&K x#wv0njZ|NAH9;>(d P7Tt#fXE+ft2lRR4uZe5a#r>:@x=J =.TѲeC/%j·y3AP~xHZQ\ *bVdSf B3Fe1 b YZ/[݃Jb=|u{k`YOFͮ.Ye`SV%C-f L5Zjg#4wU6@s`ܹ[ 7;.͙X( @` gZcF3};EٜQ*y{x[) L <ҹ1q3}r}dAp|Y09.sw_ pb[FN&sXо7/eꈠV?_ @ l/8Z׽Y3e"7tSf&)$? rE祋 `@`Uȃ!.㾏a.Hp.[GM~Kæsf|J4lϲ9 `AQ^ } jl <l|em_q|eq<. &}!.4{LV oz%iQLƕNkdxSʻ %[>!@= Z: XP~CuݎH"bXڠ*“M{1{DX4{ǐ:_K`Q!A:f^,:"O|2'@[ZY Եcڽj: b7*bT?#D3~G'bJL0y3W[Y*p58L&gC <7e!cIёq":T1P:NūVkAx(.!k<_泥lPsT9)૱=**Sr[9¶7aYJ*MK길# $ZAA#H+6 Kge8]5xģ@46s!|!fozUd 9v9Of;1 g_ GM@uOPK?.waڏЭ1 &@A{M5 * pd,|<ܲ.;zPd܂ A]:5Uofw}][Zo~S̷t=7c}'m|;CM3 ";~&P6>/ނ % ;[!ܦߴ'|NcՇnvZ-7n,3$?É$/?QwO9?.ɗUނA6$a>fȍLsZz7d/Ӫ;dtKD/#u~lbWwK̽2/]a*xٷQArK5jE@ .xpXSxBij%ڙ* xJK]>J\tSmҲ/wTgy$4{ zV?q&S,x|`&PӟZ}VFSzy1a6HGHվqpWCcڳYE)6XRw݊B#+yZ?t7|'5 cp#F$\Uu͚h 8CI~~\7ֆ i8}uvNJ_4($Akm @ڬf,^ݍ~aZiT[(B{WxSLvWϟRMC`@+8΁ܾ7YBH>Qf l`/*=,Lu;p& 6@qO.XB7QhAU^)iH典l-xCK*,"{#g2޼Ujd ACex\z,HJ4wzF`ˇx#2ew['Ia>[ j pel#1{\> uT8 aΘ d P0ZQ_]yF7x0'gK6\:hr jxK?nUZ~:xc3a'HIu7~]9/}TP_S2P(_OV<`mDP T{:qwǤk.m9*GBm$3{wCXwKM})yقM(Eu: Yυp]۹!8|o(ȧ+l?퓒S7fF!'1mrpB:=NѼpV.%22Lx0ϷQysK9Ӿp h!@j#HcN*wBr jD<"Ťbg~ȁjrNbe90h4M̅IFxrФ"ɔ9c?N/dGW9C_v|ڈC@粪PJnw뭘$KjŽqdck1‘?M[`0d?'߾k&;cS2S_lSCjpȤE)V&}!< N~0Tyk?(׃Hp@b7orͥ]G, !)4_@WZ/BKsUը s܈.,pX4* |[c3 _d0[e]- ,iyɌ ʸJXD NQdYq)'0!i\m:j^4! AYTF$DWƆr0pGstBO=ay[vcETo<+p;!R x! ݍu>ZHTyx3:rch}i+49YP-څsmH2cʲeO3ey‚ w/Sˁ~k߭+Mb3mlHȄ@oE爐4 ]GcʣMr)]>i@ARπIe/@t=ZQ2%3z>#:_]idTɶҠ@#6а֧m!S.+ }wzmGCe-vFjY<iזN\_0P&b->>k#}ywc?kt9bR#[Z^OD&3ARXIL b q`7!/ ;P yu/%^c'<D@FqG|ca¾2V*0q ke 8؄)WPW,(J>?_AY䀞̪zK/ĵH61sNKq&HԡטSz5t'Gx$yQ@A~\qmK5ryi;PO1!ъڅ$%2 Yb: fKLdBYCؿU͸gh( eƇ-,c&,2{`PsUU#, p$~)e?kë0툐$!p[ q F܍zdgf5̾1WcX@d&ˢxEnF7q{  >Et}4WYÙ0_ ;[ @ l"<W"Ҵr,iG @rsۓ#:bIۃ%d8l HFZQv40[&Boԋh,p=M^*`@#C7T 1<(oA\^ T! A-wR#O:W84*YE)+VmSa$K -!!h9X¯x|SxbsLiO?>f1!`u' NÎ0h'n[7,lNT7pq!DŽ^)|'bulmW}zr>%5u*NlRMh@x`ֹžU,aE_M:Ϸ \@<&=_NՋ(qL?q3=XPp8JD$\,h[̇ l/6ק`2[#CwB&.+"3z@#b@ߚhuJЦB& PgMa06qTu6XPAU^Z&} hx 5/BEpؚl87m4L:"u1]Zgʛ 3;VmzY$s?Ss16?z~sww bK]TZ*K,k&>yA*J RY۪Ra0`(ZLb32zKZ o;;R.cY!0VP5 6K=ߏsG`~u]2A>ӂ(Z-O@W* YbmG$xoq<&7.hA_$^Y)h$p-xm8!82ewlĥiUqnU5xd!S *ў=.MK9 5]~ʶ]]q%ă^=zٍln[$y@Ԩ[aC]zOzCC:JDVeи@3#84ג⵻F0: VA"4[2ANs=7=UeLf\ɍvphu ;Z ->"wea@iuP_m X~r?:&9~B mJ Q],T7X}؁+1lx/*_2߷la8t9}b@m"+>y(۞&؜7oί[h?aj!E"sAf9̂\z3j 0~uu3ؚ.{y8G)ziH{F)uHYF*^ٓy3Tt@N 4j<fTϠ=&v9?[!S)Ɵ}VrmA @BY0@V# ݐ9iP"3?\=@g:c{p NHב2!@uڹR+f`bJӐb@9u0]M`3ƶu8fl@n!ԊM`)^'C#V Ҡw@+ށLl`/*. 9i_ߌa \iTs| k0Ll GɡS޾Z@AaOD5jxP͢q0YuZ#X#e{ y{y!ј=_+=*&R/mOҴ?M*a[Gd(k2IQ>d~w!`#Z+R[rqyf[4ԭO94U!F*Md>,y'l0W}Lrx!1(5;A$< K@UV)='. V-V. tS\o'`h ?Jd$% ) ]0]Atd^(>j:ڇ5H/cbTף;nEx,Hb۶ ӎ]*$c:o:j>Wz%5H:8%gq@Ɵ8pQHPօjV`Ѣ/Pm\ 𝶼O|5"cW:.³ YkD};~ hX;=oA,h'Nxi$yO='2̑esbj{Czx Ҁ:l =2V"p+_C3ńg(8pOz'u\1roKsk &O <d;S6 }-;bSC~yP2<7%(q2h ܁ml`/*. H!yx)^v_- OoJfKܚ~5]F[$E @A~y%8ecC?;4#ds'cw>l<>-ſGcfXLu`Zl-Z?p,.O@ޟ&\آ1NMo@Ξ:jDOlmuGv(rMxh{V(^d C]o\l(ն C5D@W^uq`J.KX@kS0xZemBN )5bz*p⋃@sz;\E^c4 z  p`8T)uթLPFRE}' l/6] nIԖj1(g ԼE3`љJX eLlAjp> +1aiLJd4 kArO3uޏDo1BiHx%(T˛ xut1pA) &'?^גS9doт]!yk[Rbn0V6^T` ,6fY a[W*8ݝ:HVԲ_| .\y+:ܮN>5ZKta 0xdDPİ* KAc-W `eui/V+Q8$$+ )a9u}t'vCLrFPwK|3K; Amnj~i( AuE܀mS헯DMT#釈} م^~9ц3m=SD&@;v@ |*SE8X/ŀF~1QLqzsײ;ӿǧmyD*A- ZͬSX'z}r;Br?x"T#jTw8|!w!nk 1u-Z@R0Ŗ*;'geΰf9xp>xh;`^) ׷0 WrJy5<s5djb LSH cZދ$bs֐@"J1 lx/*<%a^]qm-eQpH"ȏ9~Ⱥ90vPBy}* ħQ<=(A?TgQ^^ Al~Ժ~ =_N]K&ujM =5oĈB b|@[Jc89|z{]i[5 icԭ;AX>.kBY&0 l# mCRZ};;4/V+A=>tN6(*zYGcRTdWY lv0Q>)նtGks|ҋn =A wQas8?<܊$KsG|PKǐhB^8tHl% j6c^4} ӫAwπ~J.gQZ"DJ#%YLc $k? "_#%gCY\@KmcNόB{v`.mYM~BAePnxcժM_S@>Ag†T:lR!B} w5DQXH Z*_P t {.ΏVp'2<C1{Q!@;b/i5b\YXqP [€yH (^)2(/OLt 8,TDb@k *R7Y0bP[O9{l`/*< Y;wP4@Y<@-KM2ܣh3}R<MON_SEK}1ffAy䀾̪:ϳ(0"^"fF}%`*DT9Q^U7˔P[[Ođ#ba<2ȩ ̺6Vl60x椉ǠGO?'qw 2 BoӼn9 F!eBX`C2$#jəZHz_*E uT"4(Ў 79:<@3\tyۃH+-ZK7[#LMa8nr< 0rbn$Pi2XNױ .7jQP DЙFتTK @Rw5- hej1)قI O]=a0ӑe0ӯ^;SEl 2_KqeGYN^9ACe`$+oQ Yhihq[[ 0v7I &TsgnɘD~A}ENb-y."5a{: 9"<: @} JV؄xC`PwMu Tp|4vM Hrl8<zRxU;bsHW`^DآQB C)b'W~Qyņ&"◺ސ[a D3D@G+#S&2]ҡIp~)ҾO A+LQues!BKAb1,Asr#+jRsVM]dnR8 IV<]n'`N"E$@mK?Q+Z($_@I@I!TPca$}⠏d' Ar~%b3GL5A/)hSӲG?L0G 4lGblOe:"CRqk>0SNAnhk5 Ȫd:^Q*|?cF>V, YMw3 <˫;4Vʾpя`  3̈, )& ppQ("!{v6Q<%,Wk%2s j"d-vZ 9ݳ$8G؍truCjܛ`@|uX6 p[d@ L>hWS(X11@BB.5PϕPo: dDA*@lI@e%"93ز. +Y:԰bo) Ȳm"))n1Hݐ+*NU;N`\3|Vxg/fAl$%܀h}ӞK'+ SA*}QBKA)ML(^Ƌ4IwTI~b$;w AU Nڥ9 CfYy rF}~9솑CDps7} { ܴGwL[Y_4d8i+#("Nh13,SUt. 3Z/Y0 R)^^G~}~6JZ>vnvDY#0jP(S?v9b,!\)=:u #Lٛo ;DF5 L{hs̅y)q%h`8 /sŧQTiR80/iHR=>08. =!0˭]8/,J47|.Ъ E_[h:JWd:zkw?ωRZ`h@݀jI|An֨Di@EⓖzR *'%-=!"0љWg "kE!%-@$c+gCz ʮH? c? qLAg>,O8fKfZiٵD2|= ZJc̲7՟UCcGQɔDd4 ;W'F}B yzg1r) T}f@n0 ]1 DŽ+W+Hmh seiש@X6t7D8-BXZH($iv <&-a!^>C" cAAc9 YC<^[ %,o%5I50H| HMHX#N?72%;FNMf(m9>2fIoX ?#[oL&@4ݘ?*d&''|7'CWhЧl6}P+@H E\0%˷kR. A;;hBwv3-y6AWzOP 6(><AXO>xĊG?U,bgBUVlLL{1A&@?nu` ZmeTiH4=@ ԥ@2NŰ-{d&$%`o|IHRQE(XIȻ}j̸XK5wCÁWQ lS[~p hSrUI=T" iFm|D>ȎN%0wsڌ3xćV0Q_1zi|m^<]R)uGUgg}( D _bWG.2lL'-館[ 2VcBd~+47e0>N`TF>tRl!H$I n*%F39lQTjyUq+jxk1zXpO8+'GhyFZyNqayC-v'BO4Lt.g[*$;=O nq4kAr=rAo!s].!qTr6/zi7vc2VcuO~6WH.~dMD[e xqdR9;5L \[e(J~Z> חQXB^UT5X*kCݖ)"xV g/ Iji g8PCpoNB7 X6fni oiİȿ|ŹF7,dlnklW}[V\byea?uHF{| yh40".qc^U˧k9 ܂ foz4XONn׊Lp \o%ɰrgsɠ#s(% ?vƃ6jR^-G"XN*+֚yRuG9R"uSP P,e'л%oܱ24 80|A|S$~b'~dzGZ<%\kD[HEVrEez3 *0 9tadق2d *¤TS\=tyKPŦAО +ߦDsUjz?}jH Acd$jcJBPV}Z*+ aT:j8f# *Zf+ BaKUVj v5QS3<4u>8ײ]tnt6ub>+r8nio#CxOҊ"vfM68P`ZW.qxQ*%h-~/)!n&>BoLF3JJJ-΂D݁xQ* l̴׻P>=%/uE:5sv0bG4c1'-y|ejqwxB!OSq%mh~zT;dq2t%]' 'W%&qd)TW3pVl VHuA@5ߒڒ f־Koѹg X[ rUF6/tϯ3+ηi'vIs +Rm@"(pBc4)[N9=JDF{I+`L|4qiXF ~N{T;Cv]V#@w~64;9Lְ |?Evߝa4zMBM^'ЃY0477_0]D|!+u쉡piYevK|D9UNt&OtAl>#;N!h rL+y3 .pvYh˂#uA yjDnZ@e?BDkϬՇLE9_)Eph!ߛYʋ |w,1'qi9 RA?bu= UƜ31ڢsb>G-WaC Qps/,->imjp4M-"FDSB9X$jPI²+:#ڑBlk ~Vj9 Ig}wRc\f8N]?&ZHO鶑ۀ:6Y`!uG^ i똖\h&zӠ%&z>@&,`B 3:ܟJF_&JpYSS[?[*]Mz. Ӷ|rH% YLdߑ[X.7^?.WRkr. b*S wYhWS0ć..ĽqdWƺ[3wds)~Ѻ/ܯ%  tTKU% -|O S_&2vIrW읩>Lo1 Ѓ7d,y5)CEa"%*у/(䦨cL-ɞ͸-Rna 5 1XF^fi$o=T%Jnzb.*f4h0X.#1h3 -4Q<}䡮ϲcA]y1wW{|T$Z7dJp^#!eN )L;_`d'A0#Ae5P6$@1I,eD\ɳw]ixe׭.{d!M 6!GىaBZx1Hq J5ؿ;Gen:?XsqAl:@1a׹Y~ #wqǔF6XY _C.Yu])}:3 +Eؼ>% >L3)ͺ 2<  ϳpAE X*I@s҉ V~ ppgiet2آ@ҍhk A_nt$ɻs(Q2 +@C4E3 i'iguz ]a-yۜWݿגZA6(vdoJPӕ ǃqey{x~XRS>VKߌMi) }ZtCߴ`T~@,tuLWr `$&"m)2/WV4ϭ)cyEjvCς.^е9=(iвXk+iQ큫F;rRBR}|vka_B8֨Nf40Pן4}#ɣB"9`,%П~ #=eX"=,ILꚊiЗE o/\rܹxzq˩Rmtutܶ-Bdf$RR/V6`à˖>5yL!bL82@:t0a c*X+HǼުvǸ6ffIE"xc04Ju+ڧMf/8ri^^mѝBC -M4]%!J<$2cR\FBuf*OҾ;(jJ/mkx 4)<"{tŨ#G(>ݼsŎf(} d|A~s57zؖyqRGj/FI^&";9 ÖrI'$-ͧaP|nH*i5)St-5AfѪU.<|NHպk1- r9*s"LAQ^Ɨks-j0 O{ڵʠ*}ܘ.R°۸Idf㍕  ͪaz+ ;lZoz%1>՚zNidJgS ix*n2\!-i#s8+=4#ន@}t˦Z.lH5Xpr#}::&XC~[bՕvFM ٦9=y uJ`}H+ZBʜάTQPKIX5ppIrpb 7\{;Gf͓,@AP KMbt㥈:grъ>R/'jT@ )]Gp(p8yCX ;`9ɩjo<ehNYlA>JߥyLH& D3Bou:Vlf=nr֜PG/f p[UB?WG\wS!@fFp32&!*W\KVŐ=$`#Z]1ڶ7XB!@p- &0R4TR6TqsaT݅2+w`LWSeBQ$@-dNo>VߔF暿?lFԊ# T }Y g.i٫pN}-_۬*&zfݽЄ :W6m^@nUPpڹM6ţ@Ki)m)fU_Bȡ\j-_ #CF][ ]#G=CЊyFԯaw~F+MK7Bl k}؋ ~RpX&.iʽ^|;tQp/'AJoxQ T*-JAo˕{4a8[2LLooxiEĬׂ\$@f,11&6gDKIm*cA&+FWAB#wqdS%)QJ G+ Fۭ⿶m ƚJt; B)PcInuwlR^8ݳD ½Ɵl?Adk>N8y]41R=Z'1[dxdG,on-WS9XHwn w(gxKǿQ2'౱b / Mͨ?2A w 2:uCقͅMdK9F| 4 TV >j5w&aX]}pxTQ,ǟU3’ظR꯾ܯ@+5:2tP#0ݿ/qǤ!2h^y.ܤtlX nbǦNF3Yng68b 7B3 j'?2ݲ5ȩaTztR48nM%xH) o'>+W|Q`-ka.Gx39OԌQ"*&(t&۩aTl1\mIP8py I W-d.ܮXa&ЅUP q Os!߮LTx$O[ཪ?mJ}xۀAU^̩|MvC$B-owݰr[l00=ThFfܻ[nF` hez^k}XϴNlH=ΏpRo~ BԕdAVNҮqP:7nZHyx?瓳~P]: ɽo3N  QTsʸVC8` 91E)8qFyw* ĜD9?AVϻ_j,6~hdS(aUf+}qqA|\6<0qKOBQYJSi(zmp62՜.--7M`*krs1K֐ȎM|u.{eEf{mgpwl*B7{\#gŒ>E>h-k1Iϊ'c; `26 5m)=a.0l *"ml`|A3VG_p>q_cK~GoO=gB1>fݯЭTՈ'vc%ckuzZ,<Zޏ\-3u=JMusd)=_xɧFNYFRfAS$.fFuHRUFMz"[u<$8aWk#e_3Zb94{;%q/ ]PMiўp+dQ:U&QRTbخc)w 3yD1VI sp3d=ɀ'\c@:'%^ߢI>i^2H:>,Vwڂ6,0 E/-fx.8y)@bdq&_97 F9@:@:d} =%s^$-2'ai@Gl\X-A[9 TgB!78 :tg=pwAԀXc"!T&v@`lz, *MOUgʼ9$1oѶMԎoLU@VI,M]ٔ>%Wwrh@FVAZ= ]W9{QBN )Cc-3wl<fQ=L}Ax3@Dme9,`%.aZР-WŃ(prU%=t`G4\3m4Cd9T قf շ,b|/rBr;?,1RYy ?4#jͽ5THSG{7G}5߱jTcO5cP̓lY 8ݽڭ}H4ְ{_&cn-K iQ-ALOpOP (K)::ӊ[}mbՂܫz3v+[ZtL<$?taד S7&u)̀.&4ḣ~18ZWZuLKGO惴ݻO# 679 ȜsmpU76S i9Kq^jjf)xŋ@~j$˙b=o5DQ%i?\mm?=(թT7nΫw{oEb 'Bpx<}Eyzv:aܩslWvd9}n֎ #@Ad#'ɟ":_/5<1)6%Onlm4c`W N7i,'>ީ_glQk #c)K%PoUP%K~\ZI s~}W_Ey44X`I-1`PY}cge}rۭ(O?F(Q5~>*8Bh$a7|t_nWzqS sTIҳ &l_gj h`ڀ'g}[îmL!i%OLuTa2 r~jR-㿲1L!w|=W?O^ xLwZ eU ޗ+jo%}"O)%aoނ{唔@s=osO~^ox:ݽߍ;%4쯒<7ۛLfA/Wj܂})ۏܳ}_m1+>4?c߲: \HnS[o^61#SBLx B7!<!.]{%D(Q{htv0\b۵ĺF?Peۜ5pN 8CdrdET9DŽ@ԕ:?Y86QInGܬ)hމ7Lp?vyqĔ2HUmy4U|z_' o@ݻ8t&oJ/*"vz"Iϑ`w|G'4 U8τYz X,+-3KpCMTeBc~q}$luG;ϨyHj4=3?Pi顊^ʣ̶a%\'IrP˹.1#vY6*@o 4˩%}{oדK*r"/tG:̭`)S*3PI2nԍg*yt#/u'@YMA yF|˃?;\SJ ^">}a7>K`A:Q[Dg10v sDo4M}5g j_UJ6p wE'$F1|y4<1nxceŕCA&(U&;@v]>_V :K=jn}zD"8&o޼- 0$~v3ާ1H֎+;kv{:=FןcHȻ>䣸\i}xXQԺ6\)ͯǢyw<6Xby2MF檓I^/Qsy;bҢ,( ;πH2'bRaE`70xޖpN䠆HJkoGc14 )s bYCF*v9gҊ~g +L._a,T`"e:gs*y\BKܬ It6DXSCOd<|L80qpi-A#QU4emHS3F"7? p}S`YFX57z_iۦ-Pvӥ;/וi^hK㕓/r"=̨vHtfXZ ,p:P'>OM61SnрS%7V{@265٫<0r=[,bœgg=U6\q666!5We zZo,2e)&uOcӶpi LAe7j64?a 5Xjٝ϶gdcOΑc^0֘}uVG_Ed [ & "vG8Am x7hnFɮQYM{鬜=RXN8_ۿ4] 쮅+YOӋ7`pQ72#sd-Rf\ 7|:PZJ-g*|`zNe^X6޿Jq2]y*mG>u#-/P`&Sxbʂl1aƜFݰk,bÎޥ!R|W>q2Iظu%1d*Z%)H8se\43KZc@? (b? nP,"u؞ȱLV>) 17J'f|ªu=A ɥT 1m47lzoa@lK ZdKwCdݡ1&vAfuѻIA UZ.t }0(6R(o7G41IyΆh4*Ac^AmBldf\g0G:;69 d R٤K Ղ,FAEZ=1e&ͩ2Fm,8Iȷ^ߏ-􃖎2"" RuV%O-:47‘RslӷE [`G@סzEM S 7ՙd:&_,193"QV ddh JBԵBQ^GY*.H F0w Bcq% &=-tty[!c^nv7ʡS\-zDwWʶPm-/&ypἩ4Gu+/ZD(4LUʈ*N(G|ڰmdwpoL(N=歝ؿdmi Bb eɈߗkMWFRQ X hnoAQ^svDgUn@6TndМ A-IƐ-?`1AP4: xpp5g="鶕86ʘe'~4岈MsH]#òNu(QRY"{±ƈ(UBfwf:yT~y|9' dV: bV2@(|m}Q|;SRRTÊ] Iȍ F) #p2`BQixhg. ga%5#uPE47mO}({SˉJ(sfUiJ:i85W8Wꛧyׄnֽh5EXBr;Ɗ}(W8 nH\tdN9@6@Yeȿ1)40x|L|+LQi'T=hGƷ&ScD㤄uew=]05}3.^Ʒ,^ ̯UtI:,˂x(# f`^ is ;6@֋U/ng'w1tbDBHHrs%6kbC-PMfe#0W92^^y.FSt*El7))0}#Ǚ[/6p(&7cƈAYɀ^̓:f:d#(yn4D| ZJx8ڣ<0}!VRNBeLv,p*MADmrBܦ`ſ@D~ 4pݶb>M.ʜ_dh M"}@TpB+󮭫gYiUڭ5A{rْ> 2 ,]Y~;ޜr:/ o'ph,(3^Xz !P4;y2mOZMxH#ꝻύTÇeS|PH ]XAR߀>̓og:DrR,KV\~^&sO̳@=iJ:ݺ %c˄jߺzu(%˦AS"vMçdtssqV<\%T8@`Xџ~`193FbtڡOӲѾI̓s@0b|-bawF0EpuA@(ɶGa E(V\="̃9$*̂ ˅]NgXYw9P`Aف1+* D D P(XbUĬ$b-=- K<:=g6N\ђa.We-Iv>=~ZbpWPk5gVa;fیKLrօ51]av"~\;ՇJd:&4ߝcޞKSHlue&Sb.7WS'K "vxP̓>hhH.B׈ǣW}wѹq d#hWgIe<͎ؓ1r,Pjlx;sx2KlM-* (Bҵ#hTFILs~L+"|V'lmR ']'jQcc=#+3ùGIJxb45+(CLB[w|):Z'*RXſ}֊Ou=BiAGAR+2~O Hws 5ڢP|>s%xMd3E0PPܖ/gϼ m;D8Ph\?zo_[C[e=;m %(Ѵ@Cڥ;$D]_6- #ޯ3c<Mw$)XX9Mr!1猯l[cx; @q F?&\BU5! hoY"O҅F!Pf ʈ]hUcU=)qcW,aHM&ɜAq;v,!)W&d o%/L@J  dLΗ^>phV9AL콬pU"@@rɺVI L< b\WPdAO  \SB]dI9HӨ~Zlc5{f~{Gy"Ork-NtzB>"Fn:d#yki@籏q+n!wGD6gQɞ2d`4zA诅*O~п6u?:הvaW8N`@V|1 ӆgt&Y'g >0ڲ*o#?KjC Zw𻾆]h[-ĕe|4͞7c̸s&~GjdCx-O\cFMLMԝ4*AJI\M:$8[#Q\ZA9S<Ĝv;*`ah3JU4GC-6{GeC|#^} DE bӾ5 M" k܋EWŨ|Wkl6>8.K8R '3"["#M$F5 G1;C1Rʀem{0_{jZ> ZBQ T]6pxyzP&llRmĢ)kzfҒsbxA<) 1 }IAF$mUJHS4CP]6Hע7 3W\ܩ;}NaJ,Ċ mЄ/ Ҽ">đdik;|q3l72&*F9R񟣺ࢉUELƮڳ~{vui,:>!׉N2Dd/R" Mz)ʢn.עԯerm0L|(S5'cNCI|f{lѴMv`g+D՞!M~쪔vΞϧ7GG|}pL"*8Qʑ %)ފEA鴰,Z!J,VZRX/Aq~{Ck%Q|`;o+EdO40xi Woc-Um6eH@+@ۍ_FO}:5P>C)9%x$݋WTaċZ5T"S hw_3sZ9_(ұ9S[ZFh9g=~ׂp3!N' U.~ÎN(\c1@uү-BK3DpuR#wI͢&lAY_&S2%`emE'hxh'W]U 1yiUX:uP$S ␬[_+p}q0An`=^9&v ubP8L{%bM#v877$T>R,BAh64\鼇&K%_l zJy?qr@As(? j iѕÅ#iYXX(w+Gmr -B ^JOQʫ}H(Sg"/W (ќV("p" =k}9A!l4HG%*2]l- QN$TWtNW1 oa*ިumW[ ]ۘ+wLWuCA? ;``}. bHB.NO0^IW #$c@F"4vb}N5HlYt7_!pHihj+xC9zּRq=?/o6qfN@5$얾 V=ҕb^|S[s௷)M ZtfAKt30S{C]dЄ {m& L& ٠44p3Z͂>I#cn-(͖ݘ27' K Fa?3_Cbpb)3}PS'" 9B1"_Ά&刌r`GtJ^rne0dZ9E+{ T!}W9ԟl. "S-9?]=u %W0*]9\&[klT0"֚Xs+UF54p[Jm3H)5&W]VAP"X`?U1UH"Jny3+~ * / zM1c}Np7٥6DO#Z?z~bJJ]:9jݕ$=y‰[1b*D*f}_?5E꣥6W+%턱ϙEbE,H{zJx"ހ;O[:@ {0UQq\P2ѻ}J0BgfpQZsOJ5#o;V>_oY70^MRmX쟌t \ّѥliH ’ƴpEr];Jj8~ُg{W6 ffePF+6:Y ?Ճ[#CQ& ]=2Gޑ*.-UF{uYYK$C~``r :E У96k S-Զ4)a^ֶf[ӵ }ίdNVz)\v4^TMc|4^=M./\gU-G_BZ3_r㊊z B:lR3UISn PRQZc.VđIh=ߐSgQW`0#B̮0K3zh:[G<Ψ;:/zDV$֬id_(ˡ'z aKՊ`Zr^9p'fK@‚ՀLw0etñk_S0T5"utS;WN6s: ,B؀\wb W>/N>D\<xtZTC_3-Tk|>d*5qPEM܂ۀ pYKvk{,Ƿ/Bys;.y[̗7#:q$ #M}R[8g5R=;$eiYdق݀ K( mFyܦ|sW7{lRn.۵?fAz&C_ :JzO˞y{FA 鹷jG MF~(ӣ͖gk5ҝmrV(ܒ+Ϟ6|1ImV@0Y{|5o@KqM1&_y6?l_ &?ᾃW.Z;?ue]BB>$jgIGuD.` ;}8fAws&;Icj@iвty?+r'm%xW@ϲji/)[ؑ"+3I-z'+T#,&Q /V7I4dY5B:tL( <|I[)._9"%J*mёG42B+ (/XT&OĮbxix{CM -%61)<&jH68.40{5Q"lZFp h-h e{b)l?yY4>sӿ>ȗhY@0OثoxBr|'SjDr#=NBQMO-Ehx*͞}?,b1RN.dL9ﱾ,իre9>Kzf28 w"6\,t7 Z:ulAsIrT"XAğHϮZ,S {K57;]@CKt቞1l.!0ybaEYwY:>ˡu7vz؄qb}LNe0RiψƤ-e[hb^b-(-EU`˱Hks֗W ?7Ҹ@.RvLAaS ASgH]lhoٯ2]Dhfz_|)E\Lĺ%7SW3z0 A^2ԇ.JɆD7ld(Ielwݯ:Eg7;Q`3&gAqgI= Hۄ W1O yVLGId,FSTU:~h_T{{ UB&1% ^?_C1ߊG``WqhƫˊxhW9ϲM!>,ItM؈d_Z()Y#r)N[==ۢlDuJ'd-V1Kb>1wF@.?$KSQBUJE|+)dPfoV ݸdC%&Y5 X9sH5WA/ N%!ԗu";3&-*#3 \YW!Eų˸R퉦+Nդ +m| CʘUl/{FUY>󒀥l 2GRArm)OԈJ8hLAQ{*ID.9Pї,Sj<q- 0bWO;!OoEWbfѠdi Ɯم qFN*S14:O$H9Z` 3'b%۫/fgƎpW@kS tCr#R2ף |c`|D.m<5 eLvVPAM3[rxɡZ71~ |bC{OƫI1|k)VP-~ץɣp*m9i8=urh;Gsx=ej[M_TDeRm],GŬ]O7q uL85Wj%.$جHtVŀW-C>R/34G+<)z"[wӐI$AG>\M%H!kIZL>ۡZ3Cs,ܓ +awc}~1;n޸It $7熗<Z؟Li#߉%<)G &.Q"ΏyItHx܂(, +h׻sΗ<_t߬o7Fr'4O>~rzp+E LYl݂+l 4U$ؕ?>ν&|r{g߻Õ#wG!n((&`+;ۈq#cykWQ|'ڂ-4KTc?|s>NkFagGWٻ9!|[H;lґٙC|ݑB4!Fx[b(JF(išm5>3^% 傭m69-nBi9ϽOY~NM6r_\獆wcҿ!V8a(!y߰K\>@GQI rDAkYV&+L.sd6[eso%G>rba0qfcߢ&fe |e&w `]5 33pbq@!bb'o PqnEqԥ俇{E7畠Z%I&> >=.tS/{~vYN|`>1sucmR֛xV[2ם&=O Zlw<6Q.ؔ?(\jQ ';7# a-T,`/KԦq wIoCv^Um =-|.Xx.~aJ28ՙYڃ/bTЯ5iVYugX@dGˆ >?bJuKj.1@$F jRb$$?7.~}LbK9a|8<7:3 ~+\n捫΀_nm63kAv:߈YXmš⑥ᘜ7ԦGAs/6zgE5jeNd;o.AU -U\# @J f%0:k57e" C!f,VH v}ez2xhXmb=YT;,YYֳgrܴ/Vk!lwgrK#ݠsCˬ|E[cC8HL8k&R &W;lF7f2z0-Xl{8}km͇JG=βO(˔\ܪd^\$(V`GsSU^+]Ԫרѥڱ]%H(7\FEp}[K޼L8j˻W(!Aid&C*\q=e~lv4xH*X>cSUY$*4c l.KGr"Bᡵ@ePRZ`%[> ;c^^ U̓tJoCBo[JjX(':$PnOT?]S;jM,3kU-_ͥh{@a]ؘ-ӿQ;CXdhcw+ ݔNw2MԳ^iC2 eڅq\…j;}ݣ JN _d vI|嬼^^% Hw29˜LNKʯMؼ'瓆A[?B?C`,ӠoHG\ʪn͑YTߺ?7xW@4|璩2Y-C~h{rB߈ox&o\la iƊpoF*䑬SðoJnG9%&z@e㐕~S`[\_p1H~yI͎o^WdnObE; (9's%Ak8O?Z;^YNF,}DRnt L(FJmd%'V$lu٠槨9vNcQ qCv/Ǵmiͳ#&B?xr{.vlޫ)=٭x $޶\[CF[ t%B1yKǜNE 8VIS7/~oA%rXc򼆧が#]:3g:lT դD%j|8`@ ];p:Bx1/c3}4.U Cu㑖cIOymUI RnI$ _7B$7g|9PA1LtFĒh) R<ᨿá{gB fo2?w ᣊpr}Gbmn]j1[~6j] yPLV"g1 pǨP`'lM-kJNd%ڱwrQU0?t-\ͣRO=w8 |o`f>T(,\m=GHv%+NJ5#Ɋ‹WtRz$+r:},1q8G:S?? R ]}3RĎKQz#2B ʠ٢"XNK&I/7[$J &NߴujAfc%յdɵCERŨ57㔉m!9YU>c+L2%ۭbsZ46A~+N_2WVƮe:ċow۩GA:y4y#u5{tyw`^w%GX cNH] 3-˵#{ \2 exìrcNՋT,*^_PrqBn-4tߑpˮ`iPT# tWMl,^PK4js@wMX9Ruս No>(^˞ᨃx.J2 B28@xlf`>3ȿ_ ghu316iZ'栁X)uzٚ!gL</;;_vQCm'ncs6!㭼|wɯ̅xvS*g 2TuݚoBK)fy9V˿OGO.Oim9~hA]@xӪ$SENXSN9lLJsn.SBDZWt<Ď"ſZbEW5%m2yoH5»à%D~Lf/LMPJE\6*tTȟ+ UpXx%M;Oa.R ;  {t.oT/g%a kqe&%f'҄_kpbC7& %sDP=ފ =Ѵ5[\i|m|[̐VUQw ǤAfy~@4CJEI1V,boAMA?V*a&. zfX@~3m4;70x+V߆xo/[S [?v{Z:+ OsùKP AwL<Ѥ5I ۱92$UIY.zOOf(U}>A@r#P';㭆oj{ h,2Bѝ W*:T~2zJ5xew.R k GEvd<%wm=^Js脊٤Gn.ouƝvA`tc\HDuڵBw N G@K4]Q ; #^ }L:gFn0^cb Ztj{]̝G&F03RY1bv m3.i;8~z2{hx&',"Ŵxߴ47>ftOoXx.q7] / 幮׀!wh_y  PvtаŞG+b}Rc"ؼ{s39׶ѽYqGj{`#ݰXpIF[!Ty9L@Ձ(ހ Kjа:q (zeȕYMOHe|}9 q* 0<-h02'/<57>LT3ul 1C I|5%VV+5À'&>JZ7Lg/*|uABK,A4-xWPՒs`lA` ["j?=HpRd1hZo탦 d2:د0L*(RYhG)pYá̅G1X&>A6`V?Z~OAog82 oOs9ϑf7o롒q4/;FոjۭR1(?c[i͋+Sf0>\i 8x |n`jgw-\E?pd$%jq$㝇Q ͧg];#+B4":?T㷱D֦ؒ8y0,s/fjfvߜ۶:o _Aq*2E"o$G%` .JjW8:"jzՕWa<&QC(5@ۯB_4{_UXC؆̭k~b>W;h.Lt^ϭ)0)M)R Vp> C\vݕ_GfAf&?B |d~|Kn} bu,R+-9Jw'Y}Aπbx럄2k5I?v)"oZSl/}Dw hmF[,7 WI+騵d53#{eca.;]eAl.İrS/ S.c㨩ƘE'SD)HURXB0@#Ёd"0no_ H q轋!e#wYhAqȦ, !2:gY 'aI.>P.8i.:0Bĵ_A*DFVUe{3dyٖaVGFBŅrn>JU.ng'l&i[#2i}< >'zRq~ Vz7X 'HQ\ gz 4/ 5> !5qV#lX.'1)WaG iU9&AAa#a0[K5^Ɓo{VpF}GK:ɚЖ ˾pIpCdq'ӐƢ/kàMNK!%@AOy]'. - ÂtEbc" GcC#A6Fƣ#gKۂۀ<K9߉;϶uJi*NXkt[~5:^v7$ O?s?yjyTn.ʫKLea[^Zq7ݔz)J؂݀<w ӡ/˻c^}olɻ E=gV0\Ʋ_j_3'(GIn۫h܂Dw T.O6NhzM WpJ8jN&_?$:їu[|wjn]P&DA:P_PaGۻOG~2Rsڝ+"kV 4 6+%{9yiٓN Hk)|x?Ԑ"PÆwӔp ?5^@@KKu1mci]M @Y؞:tp©OS87O3B, w#cř+RnvwXX5?㾈uS  >F$%nƚ!/w{1~Zv r<ͲhRuʭ/mM2Bik-Ub,,AYP7YmlߣMBb &wDkbox8gGJ%u]ŴeH@f/3s&7rWߩos8JQ(l{!5(ia>f]'~14O6a{uK{Ošۺh壻)b\h w'ltGP qXY/򈜐'5ׄ*Q>C˿#oNε7"\ftS 4!L-\T2r(0#%?v1YǨASb6,J]H%f,FZ lą.`RPPeAL dƊ=-cvFhA74+ {ꢵsriPKVjtZdjmR(jʀu>LPY})036gՕ\4o7ܤ2ƚesY~mM0$.7;F(C d LON/iFא+J'IX? %"f"Ko-ꥹɨf!'|Cra+iQm\KEY7=ݴ3a<(A-yɢ{ѩ@>,TdqvY:,6x;PqEB{Tv @g$GZxݹ\{{^r7>kfgc 3p,UR4HaXz#'CT东)j !ӌ(+ڵY5t}4F(1pj5 a+)&S//Ia^OضfYSV@Z3 _R1T݊́V[[r(890D5K*H% aNyRb=>)" Մ |Vx`"qEgC7s[H"g]JʫDJLռ ݔ l| hſ$[lI6f/+G)PbRiHĞ4"VHO>a@k)ť=a"\tݘd 01!Bl_Rd7cM1`vaB-^/z]l*8SYw (Jխӟ %Y(ZF֋4=M 5D)B <,]?H?6+KT1L?K Vw臔Emj/9UkOY*/Gu=c~馐Y&&w5b% }YB Zvj߈ v|яoc64}$Yl'-%aTŇS.'<8l^y]3x5@7Qޓ ,K=c誚 }h(Vx-+L<)6@)iMf6\7GO(<fE2. g35]|-quWԫ ^P(CmE<^O@'PPt;Y|wiXt4Ċ˷[)8ETb9 pX5g7-Z]j Zh-wpTo+Z-Dx.17PAq0c.IsOc^@(N J[ )0@Mݓf(CCi7]}\CQN7nMz$ԥPu̙^WQ_W2egI]ޮ'nbb>?Y ˷߁';l&KyJyAc "DhqLO sohdFxƩ֪]Iiuubsd  uw 4;ZT/fC:.-`@.%{7Ҳ,b7*tCPh]Zxc8 GabJK, c7ړx3I3F-|=~Ѥ8'-u \~"P!㼑ƻrrvg |[YdY&b%;b $W AN 2?PRo\^2~twfpaw:woԅYRQ@A\jͦszUKOiٴUzT jACq>F)s :5Mh-IbVUS)а,{X>@pGlf?أH`E5>svqDkRIZbA{^)y3e ̥$;3"%#wňLΐ#YfWL@" F^X1 AcSO)&>йRr Եr X/+oOY` g-A0/1lVAU ,~2?t鼃b`ea0?Lذ3-@;O$t$$N8@g)LHZ'5pɵ;r- SYB2O|݊:9 L֐v#MNHZ?x\N?U4~ϻqy}#k}69(d:frd 9jIh̿v%cw8" Ŏ]tz݁R,f4^yH9yX$\Kf&~%z5Яb٨R]Q lTwO)*EW:@X3̖5[Y3+zB }'A ?30cdyMñۚݼ*ţXˁ K9C\өqOs4q]kpѹ@9LI0c}H3ԑeM /Q# x}O50C30dzM5x9$pIAW W",~B"wӾwsYcML#܂>otV4-q TT9yEh"ZwϺI:NH \cdlr׃E׸ĒKڑ3A+0I MfA%&*wRe䚠Nb0 ߎO?2y];?`u&{]ߜgM* 1qsJ0<7Ov;6> ^$V,!M~]b"!ģZ׋DgdrSgnyl A `ZIܕ<` iayH^¸[nblc ?ƺOg=Cp _1L~G7:0ӬMS='&Z,f~;( 0=ӃqW7rm׬׋ ÌР(]c$KT)srnp͌_cC $j2r ôlgfLZƘno=|roM6RKl [ Ót- &'mco4ij}$% (> ( vV"*HM4v;Xˮ7!i(-h[=`LCvk\1rrAlv.(/֜K:8: FDV'ڂΑ懃ϟ~' DJHF 1z]dii殉 AE\vE5)?#YgBsgĊ:>r;GMRk^C}K?p, %su:z:<ֲ/&77٠;(׏}>~ND+YO҅gfUO~b 4DMw+8c` @7kIs<:T#U\X {|<{>ԡs s| P(A& Jcoq8i]5=Ը,IM %lWZ"FlCiH3c5v,eU9)=8rǾdԱ>^Ԉ-4E2y~  .Bjnobn2"W<u(/u`w >.1K~4\?H]=v\,1 /" Jİ@l\pR"n-&g%;ђZNX1. JuǣB-9ʻ6JW.\r!Pu^,5zaADeǸNFֽto՜Basa -+=#yyB.O򅔓ɬ!9o?K5θEx/v)-=sӰn8nl4dӏ ?MLH;n߼K"df}׋M7͹tDeʬM/hmŽ (PA>(cHD1.6reGU"p\~Kؖ䏚цOGQxf M8M ҟ2LOlvX՚t@Ήq %(B \`l^׺ʘŨJ '7ޣӐ6*9C8G9eMq>[O~OPG@UȻVczXىxVi4S]ڤqQA=)E[^^kFZ=/6@tYBOFk\vT"l<⇴5j+em )Qz^J&Y78ƚ%t*pq͉{AmsC̶pIBfkyҞ񧍨9pT!ӯ j;"k i6-BD9*,Gjk"+ږ";J̯XIK+NB"bH[l~_/#&IѪhs֠ٹ^MI;MaJ3q.٤YaE~%^hyX- LTvQƲK>^n҃R[j,B9 f663oƔ0,mZfs"EEep?4墳JÔ] )c%_ҝ5R1k8JjAO~S-Xi{ŴhyF7L;SFP ?)9-Ξ x<&`))nJgtv)2Iɏ֎F .`aA<3. ~J,H4=k *>Ms^{kC]=5j8A)$Uc@Mh+̴.i-ī5O_~;臘%Zh'ӦtWVq?4Ydw+bء~$vSEwϒ|N uq+!摜tYggcީ?'E~]%N QNb]Q2NzPSV YN u-CgYYƃ=V> ``M> դ XDP"ԯh~DD&4 z߽309yL(:"Q 7!BD8n`OBrbJ{TvٔgeW9} ZRK#J/5؍rv,B Of0Ϯ,.nvDp'X(߭"e5^M[*pGaD#eM`DJ )2mp smd!8*jCItH qZ%nw/)Er)2'V(`/epv)yy= H;|PRPN66 UAbw/cD=fj2$Pi͢:%s"ז֫|UF ;?uKH t}H\0=ŤK\a(̫싛%CDim`888Ӂé'QNvw} ,QlWEХR=-ӿt;1Ww{d\|793>9l`?L,=}9;(z,WQU-]ۼ{DGsk72:2 ( !Açh/ѐ eSr  Qu-2PI \TJar^bin_2 <-kꎛ"HF oAQI@#$̴c]hk5l9mǞzug {nq jAƴiEim e!=2n8.%4NNJ)?k刟/u ƓdJ7S5 v<"+;=s Sc`m1_ 9͑y 0`fA&`܉,~YZ?npFwQ |\7y;`M2^4ngݦV+OwO`;#MwxD{Pc9ڠdcoqT[txR#B_oa̯4yC TOn "+P9x{F ϢAY Hw"I>]Hqi6,E׀֊2FC@1]Rnj %2{!(@" JjB^fW(7GokWYvb}C'Y-^h_OÈU9|Iv|O/1HiffM2gʭj{?D>a~8zfVJc:0!Dt~-}_"^@ap)ca_?Bt>*7!âeX Πec+8=U F}FDmU"h=pC q&`'04Q`BȨg] FtU]溋o tz ,uO:V3{d]%*O)3wv`7(-hɋvi.8ٻ>ca #+oG 91R0wo@N?h!%"6<~L cjmuP4}xrd:MP‰%8Ó>y仳2JE7 ʟ1m ~g:uɗ%>"֐`EiIbD9=~T|_m~Akz䴛Y[j+;ftv6 B)D{'y'yl˸חP{PkNWo D1.yQ/^ @R4R^B$- Ew|ܻflZ.C@l[ItJstUabQp8c4Cy_lӥRX,ι lsq oFN7cs%0{Pԙs;dbT%StJچ[_ +6kuX{#:BssC"‡ B#w[)rmRTh,,Skn h(83)02xl@'*H$(5*0E6C戋~HPG|º WN' k]USu|]۪WL>ViC\p&\9(T16#Bfg)F׆d"`AZ ~2?BgclW.76~R )rOn P7uKpdldՓBD67j}࿑D>Sb gnkZ.o(%JtE6R5~48|QVQbGh L ~zΡ6Ov|Zp]LޝhD@ h@4|4߃)+m9i% h?r FI@&Q+lToDlȄ$0)9hH /^ҮiqI (<{0y`ƾOA$*mwJlT{ P ! $ϺƝBBYd}zGɲ %H.z8SdXdEX|s9`C.Ai7<EiLlIRq?<{=ֽ-%ٗʺ: 9=nn R-[ၙZK.4:_gB}񜴓BeXzB6#I⟆ZxM%"rs GfTzԝ:$OQܪTMa_"uJsSSFĦ1(fBM+7>༸" @1H[1gw\2uŬ^Q|Y(q$^9kq&$_n(_ /B! ~7@eamEqVq"OGE3|'%z&Ư."q08CgBg?ʝqL/@J(y;Brs{IȡfMrK,[X^&7UqͤZ,K_|EL}fu7b?[ m*?nݭd(JNwhKN@a', L8e.>WQtHZ}D=-VEϸě /=)`tU2I(Ul`(Ĵ0QY 6Ely–`r>j3Qeyt?-UA)7! e+?##A]{G6j88hi\ox챘V%[$U%hͼgWQǬFV@pS6gd gR4 T.MfpQo3X#D\*0w[W7DCD &˧-Xi2˙Ëd!Tvtr?TxY`9ZakL7a;:<\Z~ySs.YDif63HXˉpH^ r:;Zr#qst FnMi|p^tբ`oTPzo|֨< ` Jm p#'ɡ3%υnw L q2ӕ r`)< C^璏-UB'=yblO ;ק'֞tu*.Z'e+i&v?Ocz-[bh$!煰q$Ui#>rC?*|!n;a4qp,*F ]kɘ9'pK' 1)^ 9 c^(RbJp/ym~//"TRg ld{3gESzܹ#_t|FbȃbOk{v6!a(0U#OUNK\^Fߐ8o:^ 4=q !G`>1Vt &=+o۫iSKm3.Y-l"EeM#֮ya s0;sTlWvؠD;EI/j^j>7-02p9هpNh gEv)Ԣ]id:NbR3ɲ>9 ښ1mV9=̓k%!ΨET=׊#䮜 `/v,ksN9ɶ=c-uC>}=%>S L6]9I]kM"{&؜5PKjkA59Xm#Q/=Lb# 7'IC kyH=)yuĀ;X"@xOl!B]n$_g$t{)M%`=h썭_ pR9Iu}Rȴ4K{9Wt`A] ,> DJF\PK4}+L-s:D9 6R5epw'=uO[N̊KX;J)j6&\ԗI*9Ta{풗ʵsHd!$9}VH[% !`Aa A^̊XM fښUe Ȳn./9- i7p 0; ȂeR X- ib/vRS#SQh$ qp{ 6Qyleh.jmy:Rȅ!I9}]K\9= y4 z>O1NYG4lDŁY(ތhDq`@P 6M -51ݸsN_= }L@U6>=o)rW~ Q!cLL=C@5N%n^]MAB I%   ق`)D$1fp2D }Fg"P!euYkƑF/#, \"H9?Ĉ@. BP 0tA|KgO7ܘ~;4oT[WdHK>wNER Xg#|GFuj>Dʳ8Vh?c+dݶ\m{.zmn;8xMűvʇԮרf3Wh98[D 08H+,D2=z)^_8Do֮*WLثSƖ[.Z nRdAq JV?Vu =tT0/UMA!:>+I1$b[I䨎fol$z8 1Goӆ{H=O$jt5jeqt+ O * Y*hh"h}’1M,V[ʸ]]מJ`4 4LqWewCB{7 >'9N.67ol_=ϻ]P@8J l1?ȉC`eL| HR/3ޛLHr ډvŮ`{mXcg*ڐGAi W̲Q<䴇Z V:ǰ2Aש&c, 4PJϐvKsӋ6huLh573 NjPmAU@s#+*[ņ@f4>3l0dy+cTƓ:q_bѰg9m"A@|$ 3} aЉ _|B kQ)~Gsc4f^$KPvoHQVQʠQr_F̬<|C8$2,s'syf./T %<7>-I rìT1xr5pfIšP*Ga-욶 O5sk?5 . qmM(+8!:9Eϱq3Ƌa}OAQ5F}!,A ~ȅ1\Qpϣ}~4y=bد=ld4!b2hS*(ѽR3Y˴q*yd{j]]8!]!̠: 0pBEX/sS=\n濧V8xEGxXQ7 ToO~Ek@ ԃ$?^.\"@ZQ͆u~31Oxzt`j(t@JH7~.K_ׂ{ bRn\2!2Mx$ R|@0t[L D?/ .b 0j3ZtVo%Wߘtzw&zA}RI:f%̑(^F3n1;M 3>^ oQe|6`Ar l̀e j-̓ BA8Šy"s_}rX BvEO<]Z~&tfl>R8Iٮ)MS͊!"[ٗvޫ֭9Xd}A!HtcKԳ`]{Fo6[2ڙCtݣpLNP}`C! 8g#êpMzlr-ΝA@qzy&N7,B *$‚#seM5IȕZQLػ& 9s,1cE"J3eN.! oab-83`ͤ؉Ӏban6uCjr=!3bzNp?Hc- "5/ 5Uݥ`f쇶'޳=rK$ZNP~58QVģۖ\*TH-̊|QO bR=>.d.1Ơȝ0` OqlqRO}ݳbZK4ft!ZQ$=lAXtG5Ghn 6M_#%_4-9  Ⓒу6*НK=!Z:?dMmDŌI(҃)&Z_ӱNy%p}%8yXAo ^6]WYgmRrQLSJljy:diD{# @k~ЉA RpiD0'@8 㕘`?L>d5Ot|QՒ}~hwMh9ͨl6(͵Qd;u}+摤Eť!`/z+ # P%1}trsկ޷w? V"@yy]4 ˧ž3&To)~%X!$fX!>{o68 4X;:"jޑэShδ*nD> բc#Eց 1@\eL-20< ="3|/lȓQy_i,I0)^PDfX .ab\$r*uMLˁ*d8?l?\Ejh ;ޓ~W`9Ar:of$:9xޅu^ksbq ?ZCTዙ<>n0Ha "Q纻o48$Po$$ J[=(coϊ^% >&?ӺlXyS;o|s*g/4\wTo٘~ b!ױ/L!ֈAK竗 69}(rCԝs~no ?s޳|}.]2÷U!xӮkLg@d8jDrY_5bQYp" ~U1O BX$n Zϗn%8e@d[Au͉Ӿ#3f$6r^t~EiʳjP-0G~]}S̶VUE}wh#L #0ac(Z$D'U$W&TyƄSQSyX4dUߙ׵i ̒CMPy`jE2oz ;X(,,߬DWIh/ghݮ.0s>t<-HO".E0o6T*!cBX> %y~o%6lߌ#}HR ;hãYp\e4;0J( YԴKkRv W"Vw9Qe1/,/@)޴ vLt8` 4߈DT'%w܁d,S;ٍT#0^6FǺ薡H3#Y g+Y|QasdvK鱹=0:49Yߒ6ɓdK7x_OnR! oHpC-EubABOYP܅)39uSQP8tvXWoeHrVB*פU=eFfXC2Hƪ1,Qz7ҽ4OrID|TDKEzWTHO~3"xg}2Na%fAJ|lEplhaNzѺZV[Wlg^6*<'ݡG.#'4}ˊi@eT._S)%]\9!lm7A;ciDAo ׀RX.|7CDsGL3grIe⻓q${@i?Oҏ/<:4Qt;,L@nOXz~ٹ,tHMy1KyѸJXˮrhi,V&r2ݎb vcw=e 4wͨo$ڼ>W|B4SfRݼ/cU{EtŖUE-.6~,/,h E{O3-kxtд?Ra MW*'/D>|'~ϒC;؁8;3hd23Mȸ+OX:?m{_p0!xxE~/m//RҩI3/xsAcVo5tpGS%Z d\ };*B7B@diC82hvBv]J$ 9Rǂڦ;%|' \MB[M_/\Ԙkl0\:=kعΤXRG+fb%qVͯ`l@PΙ 5sÑ&kw"@ʍSbxEz45!5vxgXtw(^s}An ~EPVS$H߯byZ0RDP*BQaԕwEn} CBAzK(thc3^O jF輅 н:򟬩rmgTo.qg栅1"#Pϼ-!pYʮ t#q U\eKmal[48]M\ݿC(|J16j?,t]M#؞6O񰂛6(O; H d <>.颔[cd5{ /C(Z߾0! v(eEC#C'B% (@.T 3XK}~؀3d+>[őB < HS wK_zG&=Rh2E ڃAh h 0T;dr $;/b7VvK~GYO<qSI*VmȌE7shu<(JCB&$D-0\ι"<80 ^I݅MLW|nV*i_F $ʁ+/P~vT1%or; fӡ'f-OOb!錖Kb$Uc Fdҳy-/A뮚X~:1ٷsÃ*VJfD0TF L´UGsi n2G[PEW!(N{y9{ЕߔWY; ` Y76[ظOa *-.n *dsH'WqLHOKNɅv"Pcr xfKE&㟍/sP.bm#DŽ@ P`~%ATW cYo 6 ;B9l魧tf0ё]@Qȁ*kbXQv a>)sǾAc< wGb]bR&!7LAy ,%T`.F'!Ej6\}Z_C*CO5QQ/0GڣwxkG|ʋC6B v' A/hƾgލ |`z6В `][N~F8~3E_=6\ !kk`vӷKctY|ZѯoafJwX@ BDΌ(/\ $/zYW'$!ok/X*} S4EZ [t~Lx%ғ{{űh4X؎f k]yԬ*WOfڿYH $2SF"#&nD7!@̚\B8 31*@F[Smn?+ 6XCvo}~]f :Ty_ g{'XW`~ʪVoţb/:Jb>^K7CfGriY}#wuyGq/v\fm O3H"sA۬k[y*L ,V[]_9>)M +B) 57䙄 }фSo|\⦒3879@fI*J#1[ 'U%a3 IџhGQǐl1Vub| -30c葻UMmޭ^a2mZxD`Ŀ*Ba1Oq^WX+s b[em1A8S=KU\:ڠkliqɒT (X"ò]]2 NHj&>wa)7g 3Vw<^#6]<͕aL?)Kӝ,V+>?Qjr,Az A~%|!)~\,B̈́%IGDCo Գ ksC,S6_n@^dy!%A7 δ.p{IZ' kMXjԹ?0NZDQ^g!cW|nP-KʓC˅!@M`˙F"A$ U2z|N2أ$ NQ/fiX;2+o흮n E%/B9!:pnXae7z'ӥ S;Pr[F>baΦ +|lrTFseup4joi6BIO T1@{#r-g &Ov"2pEtB#] =ۃyr,"[ "0z*d\-xCб$bɯ|2؈#HO^(8|ˆqIF&a`\Q#+WE[Q⤰] ūiTXITԿې!ɸu•sfe'܋. ɯki:\2S<n!qbW٥J%geHGIo4$g%6i S~znmT^b՟=~F-CZ ۋUFˎ;I1`C\dֱ:œ-%wE%A(JNCJh#lHo0/v(kAɨM48?Bj WKl Y=4 d< Onp.6krvXZn]IҶQ՝k:n!^: 뚻@yAhʂ'N ,mޯZ!6 _u /6?<\Uބ<`IS(  $ŃBvs2t0qh*Nw/k}hʀf"U9긊L=7gȕ}6gu/SwIƍ$ mEDyN܂O6|o|*xȫF-D'mjEq/iD;\ȞQt6k&Di2tWUKdo1CɅp#7 ˼`FǤLm X 1X2`1USui:ˑKbso{Tk2P0:!RRӕ(eʓINSpޠfsbA#1J w0KH}}k5GmU ; Uކ.:XsOt̊2Iׅ PͅB/yu޶x2yNX Wc>Qs-c9bE! Z %~[ALxu.ܻhKh{Ka.Z kQݲ>4S< Pގ vW8pC` "oAu l%D˦A$+r~QBK@j )0Ɣ9QccމTtM* 쥒v9 Cʇ`>j!08D@Pll0ָC7Nj*6[wZWY/_KlE|@^q{)] xOJ8:$9)?ܖD`(Bb3+rUm}0:YfBpĔb ܾvݗ`~&cl6?O 8h.ACF_F]=:G`Eyj1 Q“wxdapnW.ٷ6Z#\+^ Aف u<޿Vw""A pn7no'N?J$]h"y5(϶NW~Wj.m2 Fɖ;G[1qzJZ=*8&.)(\4xj[4 3] 2P;L?+]@g*ci\g!>=xdRfD/DX#Ye -4N4/|o~_NA,݉nyZ<﷥\5]q>c}d`ryyi"7r" H~+dT'\Q'Y 5I6Q|$PP4Jo |m[{n&\Ĩ97^5Ytc↻ipA ̒ S$%9ؘڠ7Q8B\MX{+٢'h1@Hꆵy@s3+v[x4ʄm SBv_$WL{x;@5<(ݲs~͎:ߡM[y+ ls]v@A0 6`X9\2i@"<@g~*!FN-| #Ra02 ӞȋZKDVǤ8Z?+Fr* |5-'yIf82 8y^ßA<Z G+0̀0iSV* bM0LV?eNމB ,#< @)b0!Bx͙(!Ƃ8uv͡ZX-x?ކ8;OJBnz`C^a|3 #(]Qጒ}٭i\!+dYzr%wlEO7hxgӺ׷{o5m۽nH[F lyZQ@$4Ǎ;c(=ԮP bΖNJ6SVzѨ@`NwR .7e ,Ǜrmm **Y0mRc @Ք)N?~0ASJm(;s8#/sjAi ׀zL{`̋ƺdƐLI0N4l_W5͞b/rtb{M7T[#PjڹIN%Vcj#EpFQ-t1#B KW"lZ;-:戬aO?佗Db)M}&UpLC]&ڹrƧ@fqr-SO#\:X qˆ4sooA5 !\.[c"8_ *X` Ho:,B&9ٍ#4S-j6(5hp  ƴԤ̔7*ԧܐSpֲ ,3$A "D.K(Q%!a#X7ӑ*dl vR&Bb]$60l7R 9O~ ^ ն"0_>3i{w9jybE>?LbH$Y 䐥w)A\`LPaֆW7%,[MͤpnL-%P J@&2 ;U$g5G*r_KEII*[gwAu`"J:+ԧi:W`klS_}HYti֓<$Aee4srv:2WeRC3FpAa&!U]*H&Csb$P#dtmwUpu>_7 ^j*'T|?\R5 5l>-W]-Ȯp(ag $We0h-}k\w(H!S|rl|P6΄E\Af 쀾J.cG3)Ԍ=3f9<9R9!@GRkRyaJ) %0lx쉓 A篟 ,Oz,| WjgV3Y*"@wwۊTMH#FџmBkU]ݙɝ/#(`ցTa@%n.nWk41$7VLq(hRDsp~+qi-/C!f~i-33-ՎRПEA4 NBNw=A`|o"2|}>zl ĵmh})[0jB9,KS:KS%Lrm݂7fRlW]41(5:L#!&w4ZJ\)ii぀~Ԑ1B?;d߬sх CgZb{ocDE-]-2KKXhd+B9|-v6/MfvoVg29:;̽N:P#]wpN~Ҳ+qOa z쀏-*v 8Ãs,nR3x5Z#j/ qwQzJ%AHEeL|1ʩ'/ڠl(\P:3hN 9[_Cߤx qZ2Hrd)L:[=9 uu}P ߱7>sΩ ip? 롓+d}ȘEՔq[gnB|]swnfC_+4B1IfU4ԥ Ƙ7c|3 J櫍U*ea()*U*>$:EO{/ps{' TIXUf(&iI|/eAۍG2(fL}m'$x JJ"z+n$ 0; =rAc|%~/#[뢂ECinݹ̞#ehc:?#gapZrhٌ3A;+ ?d4>>,o-̫AC_nL %$&V Q5mo¥6 {W|(xdܾ1j#@w l<r0ɑp| WzUvV^'q<'j/S}qdA\oەpWw#<gM(xVcf`3miesl!TD;O`0D6Mts/Gx*aHG <_05 p'lxnpkhf2>3 +DQmk8o)Hk{ + t:↦AZ v]@(_=1'ZFA q$ D Ա"vfD+~k X4yLyPimqz庎Vavpe'Ô;Pb!8ׇH D1͗j!8ZcLV"*j$X^S s\osopjH`l( e(S,DpgH>S|ĬJ (1P"ilpVd PlrJ9`aO| Zn&MW)T_#kEy{Jp%]1ne% YlisBo᭒< : ֓`)M*b9 Ͽ|(cAwH(qeÌNJ}k')[.CO:!&2%X@F <)yrf[4`Tvpqr󋷾(*'cr Ђ[@<^"M˻  Ve`,Lb>ur?階!^vXM93Pؚ 7KA^ƽ8j8z S'Y>0D~O-:CUyAc A>60O["bEi}$u3nO`Y/PGT=ׅ@ZcHpPfC+#+aB(qODSнFp(:UIl{C1];30uOG s~`»bm[_:Ž~c.%ީAmm}J$:57 X%BL%>) /A8juaݺR ǤFc辚NH8m9nD;Q!bā! lf$5:/O4 oEsa"nz#=|C0j:h8cIAk W~}x<5iJ&g/Z^9'( Da89@m`_K [{scO-04X* Չ=DehA[1g2QyF^R#~7IaG+ȸ>uǢ|KE୫vww؆@'[3GsZu8+?:Zc2Z.Oe1_ʯ< g_ىÝ$KT49(qʟ~{K82}>GէXXZP2tk'35޹p;o8tso0g`rTFc`aaA _!,8/C\w;ѝ&鴈ɮ]h 0~ӻjP(F]@C'bXDtL7 sP X%ߌ)yI[eO "L?]rpTq6'z4UQ biџ@<cn$֐(G+&2k;"8 71멚7\ۿMww+81=kb00r=\m-aରBNީܚ_"{UFgSMREx 5Ǔ֮͞ J ?|{2@.xhũ{Gl)YLh,CdCEozPITVpƄ/ө_!->*&\Y^eAi1b '5%o'7rL글H{ ⭸wVD(#< (."+ZB٠ c V#' ~[ "F=<:회Û"G[vƗ=E܆ *:sdd$Rl2iT tVfN_Ŷ̙cEӲ@'Uy84$E쇝f['CR8]^c{HFᇬ֎y )Wj!.%' lqC8U +-B<3 9c* sp["\Ӿ4 >XD%6ΠBo/lxlt tD"SӞ#X,]oI?,~KՉcz}Q6i ;$r$Ԥʪ{ H#IC-qoM.mʢI'f\C"%5.SjxghQ] O{CAeX6> S2YV`}''>vPDG%HIWV%•/*>'&}/s_5:n7$`E;Vm,eK@1ҁx3mtrUJb!G\"TU{F%`x, ,ַT{S<9LFH)g1D5~nǰz 8(isK{M`מ΄8=KRYV&'R&FUQ~frs[X*`B)GůOJNfQ>k"~Vqtdfp ˊsY& I($'Ԝuݬ3Q[ G&>/629,F7vaXZ "Z`\w نwt膽$ӟ*%]txYc3-tXhQT FCrj)q6G=EPRxZaĿ2nb╋z\)zs(@oXP7K56޹ % B+SonJ5/yf*[LUaU6FP)i;{^F[YXD8`A5/j]]ɵ ͊?-a,$2%IXs(ި1&oxU4Tj?.|~)P=@OX N66Ps ij JF3dw!qR> +S_t*Fh`uNhf??Ov^@EJBgxpB,Vɟ`t1ûs|RoKȆyQmcqF XZ΍Oh< x`S#]bf HAi [~]!sXpGٔUNĦH2{be1$(mGx*F!S(Z[h0\%>hx}2``>Ee߯V Z;p1ʃD#ՃK`z[IqD@Qx~&/adCRǂ]q:-g+adヺ p0+UsSė?;pk5VoŨoQhN,qn578w]0D)n$}Xm ,gMAH5w?`R19è;4KPEV^S'Pl`F/z)'yoocM7K.ͨg `V>U5#$@퓬`:0<{auԢ[qwQX#,ܽb6 Ʌ"zJOO4 |S>1NR4/M!O?r뚀A|񱥞N:ܮl\1sTQ";KɳSL @PoZXLtE(R XXED-!P T^]d G`A{qr1t>3hdűN6[?"kduy &?E4 ^/{Q1K?NyKf/PU|C`S!uu{1pD ̼t#:2|T`&1Ȼͻl:T".-\v֜&sƃk&|f5j ÔH3iI12 KvM:qc^Bm* yz0R.3dV{ !u3du\/SԪw -^̔0ܣUÜSj'\b|3ȁ/6wEg.({$aUukC:hh:VSh[v$%OAܦX3މdMtȠUEH7|2!uKGz}UMT4/xn]ێ-w#1pguJ~yffI|D /'-`^[Vrϣ6ҏiXRA] "+@}8+/tk7Yb^ ( 푬 <l#@KL f&"s-|n *3v@&TKKzo`O k=rՐiYZl]:wWl)AՋ߃Ρ3@[-[ɻ7fxH8wl̂3GOf`䎈|Kml$~,3CmK>,1htU5^ &c4=Lk"+rAϋ2skcyM" CXyH &D?qG %38F/`!+ WAUGӓ2/_ OX]BW˜6ؖ * w^D.i]k宜YhkF/DpDSlzvvd `AQ ^PaM3Tjrȱ5mlHyxV?5):Yoj W.!K$'8aAdՆ0Lxm%8GNZR6WA=LA͑Ɍ[ʑ֯umI82.obnks\ &f5CKkwb!&D,>ﵪ^(nHoT' LUE#hjUձxn@4qwzKjw`0c; ހ1D*BPNy!LJ9)1>n^ҪSg0le~^oS1AU Aõ&CMmJ:05ȂwCNR HA o7mV fL+Qo {h/Z96>+(ZrDMAv > 9^̇ak+zI\V2b0[OR3,6<b#p "tx4+C͢@-617Z80JGm1M;D 8쩈 TB轂"FyL3|Fn6n􌣁dizOycխS Q"9x8miw#R]q0|ƶmV`†~ p}t1/! × %љ_QX8",_M׊aO -p@&2jpË' $&N E.fA eT`&ӻ\3ѯaDD 0DL|$1@0?W#$`o܎4o[q8MET|~GqZdKS$캫5Ȣ<8 EAH-e{ET$K`Uq,5)< ;)9:%CHߗ=, 2ē6U25pޣCr{zО*֜M7CFiO2!G`66>"ɠ?$/c|0' ۳%\:N୎-Ȗ<ʤ]T?UNnA AWY WqpS+VoUsB3(IV 1.# 96.Qp7@1&O@K1T6f| d Q6C@Oįy[*ow1Y7*^Q `f(L0S!x}ҒNDi){[6 =r5;$آWC{hH{jp!iU'O Ɖg@!ҫI+hA]<4r{ 7> 1mU{@ALx5 pDŜDZi@t.} 6uÑ6JF '!l fsTXZW_i;xJc~㦫 ,=,,z-܀γUzQWuB問nsPA<eM':os 5RuD zC -nq˜93%Y'7Hغ;'o)\3{qO;nIފ4ɌN $e>*P, tRv8Q<% BapbAOGfJJòC(>#$9Ip tL .n4M򗥘4C :k(Zl[ZK ʼ]}_'Z}{4BʿWgM& ~*j1٫!cw22g\=OPshc3" _R"+vÚ94Phu ժE )Sj8z9oVv$5%DAXbd2#լYS>dg؇& ΰ2_SIm' J?hVsw/<   Go:J⃨~ [F*i)b@m[hq8̡5Ҿ^zhB<wY)7^+ ~_:K =˪sAObs1{XV^39m{e89], [m|UgÚUB*c| TmFUm6_匬"ө̼- 8*r< c窪x"FOq&I [slf}dFIbH[;uf<8AI}&B~1`tx}IrB.]g{pNLсν"@΂5h ӜDȲi$*µ^+򱪷$Yv^S1'3!YayOCL>1&hDII\So\$uΣ3Ŷզfwkge'[+w9$ ӀM-?1㼠l-fgKqw$ p~9\|f}:Y7LllYp:z'a! Nb{҉"/P}3E֞4UZ KSH? ._bGN <5Ajnj91ef*E(6dH۱:yJD_PޢsJjzM>Yrn)I.b#lQʖՁ <| xs~bB![-X,i^#<ӱ*EK z Ͼw>=aa%,!+8WТA@~'ry Qk9{'95Ʌ4c ]SZ ~'|3V~/),:S=\42#N۽g`ʂ^cn8X8wG:ՊTQב<]MעHB, xUsS?ߡ\@ʂs`~q36'4=vnF|sz|ɛkt@w![z/7zqٴ+A%Yc=RŸ=6 ]`6&qRTrX"S,jrOɩoVT*j!ȏFf;Ċ?⦈VG~(A7@ޯV?rk5e).7AFQL$;$:9@`qq(v<*Az , q[̱XLЍ61\0$_Jkԑ^Bm&VD^$iQ^#Ay AUc&w4 rtF<5&6 8 %UBK fV9]ruQ "ueE8ࣆ&4Ѥ]g 7JȬ"ufABv*c/NY5{]1]X =ҰUI>Qh>K?/{E2s|&hF9b   kΓ]j Ԍbˮt~dI(). FLCF;o>)Pz#^xZ.p0Ah;;q^Y!nz3c,`[|&a 4. 1u"u^X P~4X(ŀ3 HX/+o mUaQȤAv W6Tc.URZ #"4 TDLc&a~[~J;dR :70)'oR#xK-KU:{~>ܶ@_h إ$D9W&qk$#KêgDcuAiaw= /ʏZBKk+BbT B~BϠ=.fls{n$T9j 2WK.B#a拃حT!y'`;D CCc̘b\ib]@ w+ Lyܟd #Uwlb &៌vY Q0 i$@!UaQƃ'V]8YaOJ`A lu򞫶k⯴mHc|L;CX"@MEgJP@#/#.0 .QYxuX$^wF |@ʁjWP~6bbh|I8 ;A ^e"S6`1ƢUP>4T[B9?y (Sʨ Z50Rϼ۪r p7W,CPq-X`$Tf-)n\j'iFX_# T<_ {virzvf1'itPmA.0ڨbքHR~4kC 7e |p6}9Le oUl1@2#N%R^"keV7(EB xQW;Ai~:Q3 ]`,0[A Uc_/z:jmU3 u{pO- bhfԫiƟ=hPKBw~Y S#g ߽ZH@E3ЫCBQ!y. B5[4MXfL`J@!߈05t}Th1 p< _ T!Gҁ \{I"J5¹3un޸|2 ˖Mmjm@d-ةOY6KF3]kNsa̹=2tMMuډ?w1 B}[2￰8B1bzEЖ RA҈zHw< |F]QɥA`=A)l|'Pc§d,P>ąmύ(q_[b1>j; `"_#i&㿭=cL{ZhF`_5@}F?Sex6Cmlʑ_#""c؞Gdv7RPN {잽'?sM( +\n_c#Q`EIȆPA/:bE҈ɓ*@/>oy l-BnRVqQ>V  W:um%>gpgC$*"JnHM ^<%g~*Ӥi~e6n-a|ے%O (.xpF-諉nC;ɭN4x϶#GG_I WK|b+giN;i0o^a ܅ׇ}rEГe|枓;ۿ9Hk3ТjOD5ٗ;,z@SXH7>I2W4sƫ.5)9OVWz4TOf9@l{B qx9.jygڋ[zX13 wh/4C.QGM8k5֨(NPt* X-xhW1P<1 ]}s3t h9¦Fm05wMU`z)C"ΛW!O'Z ds3#Q,XWgaٜ0RA( "jU, 0PB ߝ&!3E[Jm( \jp_8(_lE]F"B`~c$Sը6JF(:e;B;i݌Z]OIb/PPd"RYK4U,@c5 "9|f\=G qJl#/ jO+3~1(wY!D1ec-MbA 5:[=xH}mf޵$Op5e{E,\Q݄y]RBa0A@ZJ_lMm9S @FY6\ T50'l{=-j@"NpSiKˍ?]*aQ6hG`Ch D@r V,ri2[De3O> srOE @$gg L9v{hj 108l1oEmOv+@+.mWa Dd@,pr, ep "l~F[E8e"l0lI'gA7DA ExZb>ULSDNϨ3ٰ@u³HFo kͰ.C`0yl% =N}וv.^ۢN3Ъ[dUU;hm9^piC6GZ XqџΕ++|$؀2h|_ fv*@0~R̊& X_@^^P+m nř cÇ~R_Zk@I~t"VLzj 5E֏`w.3s, p[ !`fjlqh;NAZ=H)HGDF Dw`xbV4lZ@ 1 |n?6>s YXjzҖVA輅h5&QkM~>9v`M:ރ[/Hp|XMn#ܡ)CNe2Ⱥ/顲$;@צVTVO藄ݚ /7 A ׀ETx >ԴEҺHoyX!\!`cN 25-I1pa9@x@ 0)o 857l摁o@/fz#0 E T1O-\oPe3O->Mx/d65+aN0djs;æ|(u* 7@8zEi|8H%VV>Z!+0!xFbrZ!N|VaKh%tO9(j=E.  ! G`4XBS8 ꡝ8A %(q^R"$ÏB}ƣ~LvP5eI &.TJ#0CPH)A9L5h Q,iJ*. @ v̩gy7){C_?&7&<\nsYGq);@Vx|%z+wvKIn(``ڻ=RSC(}UIDWw3?Zco0T4 4@-$xO8=U@o9Rmၕp h8sS=77ЧJG*1-iAN^=2Lʀ@1AMgA" q l-1UWR\*-qܔ7LsADRBtdz&1 2tc 3h,ڢcɏYXV 2ԅ$ۼ.Ԗ[j$ޮXXmHi7.TrOB1z 0h)iʘO ' ’QZ/zOP@]x0WSlc )k8lKÞ`K|>~e *9bϞ7މ0mqxZ`a֛o||#Jة 8:IgtAz ̆K]i] RG)Oa&(w#;CMLxfw&o` \тPP< ZTcU $xzm+Py2. o"( #֟hr9͗@Iww>(h'/ Kώ jYoL姇cW[Kh{_5\;hmle<7c/6t~2' gب IEcLP:影09!@@s:vnd` Ts_F ~B  R~tFC*`>l)y@P&}TdFdT!ݑA ĴGlao}|պgjQ[%slл'}+Dp./p_֧f+ A8n:Y +6Í vbV8)1B5 [H"cZ: S X8p#0oRGt7ћcAY,!. SCu>A4M?ݘQ/  M5p|B ;FJpyR2#w.dqOQvir<:_ΓEm?i:Ox gx>d]q<]#9_%G΄0^P}`A;$s3Q<4DW6-#,}}ѕ.2Њ߭J ^~n^TԨ0=L~@`^C"<_. eǾpfي(LF Zάq,\,F8a8%BXo 1! k'|e8A^11 l'`Y~ e-6ͤ>D1kUX+`࠵T.Irułv.INvޒT!JP3쀰|Kp4Yi>zO3IBLJ ֆ'AR֏L *WO[ly8_VTXmRd\nU ]9 Kش2  cÕS 6f4U"Qrqzax-&}mi6]tp?AkȂWޒWzɩspsH)A: bQ۲wpJ]ꞖzA^AUP?X@j1Rz^|:^TDN/^+ڡBШo*$ݵ* v[ 0y0ďve.1)]GSzKΛIߓ۷=Oeˤ\ daQBw݋ᡉ-]\%?ϯ RyOT;!40g48CHRY"m ż"&M)ϕۦ@K">|"8#tӪz~;`WL4JN3 e1pS:ͤ!D|\dX*Ƌ auK0 <6Wmd̗lUl;(|ےM3ˋMuGnJYWF imHʕhyڐyǼrX\CtKrX 4qHI3XY21dΟj+WN4ZkbAm#Ŕa\iX(:jV@ foA$Ufzq !&0ԍs(oFĴ<Lu0\}"/g%Asl(le, W%HuPW6 өeF[*ԕ( -mJ Б RǛ96jҸ sӘJ}\}iYMhws}\Б7U!ن?GsvK*~}R lb> *Ժwֻ7< l]2֊Uua$ mj3z)! ]2{ ܁#,̒~2F3Mę!nvhxȎ,i%^J~y_ReDDwRp(Rt&H;At l2\ȎfsH*xȓ 9^m @<0w6i\4|532eAZm!ə7##Qdc ʔn H}ۓ '<}RMMT=pOjYTtJ~WZ&e Ք+ܾ!; x>R6~G[^ц6[sX~+lu9ҏbWrZKb>7Sr(L: qȏ\"̙h3gR9RwUqC)sAfM?Hއ.V}Gw'>L*-g>)iRk琊e'x0kAן&2 Q)4%vVw^hkUw43FD1G.:b@j벯%Jswǻ;;d;6U깊y]Or8CE~5јcձm%wֱkvCmNX8;ndhz,g t9@pS6c5ZP# J;G0h{m~'^g*bXQ(eЁsΑl2s G1#h3 B 9뿋Je,B_./}HӘB zmE(W * AE vg7%lKIvUU‘GhE 6ߪ5idK5ލ=/| wMr7cehj{ߦ#<d6JZ0fB^l}[Nn80C0.C1wyⳎ){[+xA% k w+.fNYDZKs' q[ UXmR5;دu\l`X_ѿ3pwr9ӿq2 -ь\H&x#q؍I(lK(;5+-:#rQ㰂Zi18<ޓ ^an yH0V0d]3zK%9A8>U6} zʟߓ(|ņ0$߿Q%Ukȫ*tq$f>S4իXE|݂t +\ko9ORl lKDLPMlS)جdz c|+rb'?Mo_S2nU1"u^<}J_Mn,mSk;GPIg}{v.^ߚB l8/蝫b:^H:_cRx<%E*TE Qg<?lZe]>tM !AeqCfCXQpxt|l-]QR! vdy+BSثd݀뀸mFhsz LQ.rdxĚx,DՑm7H?r=JASR|FSM=K LGHhUx MupAu%)GSQqݭ &A<^>kt'l%}^d;&n:~E vq/LmM{h+j(׾fy[AzP1څ?[؜Kˌ}H9d3>F4y6} Y; :Ӻ6S[NYfB{~UEhO)u 8i>h Rz39% YQ`AUɀ^Ez@-eu ;h}N0.M*t4jp`x1 j"G ""ѴRv/\")([^F>kp=-־ۅ3RH5XZG1ܥ\9JӸ2eqoe>fx{벍̂NxW{Sa trl:#-Iv\t#H>\vwU0Ԁ66rM '!%E cV-\[Ih\p#, ]d<.BET[ ?{,t-YF. XafKu 圦 &:"|_^юMg+.d5+E!_㿚U-ل~ SB'K|4Gg_ 2Kcl+UaaATuç [zbw|90IpC]Y:_ai I40s8%3YtQK$4l(^R$Гw,`L,gUR?D RwQfj/CDh4f >ճ߫uӦ_FG'~7Sܠ%9MaaNfQֶCv[򦫤5s<IN3C8P|,ģݝSEd1I!̈ר .DHz湐ˡ4zA\̝&m 3 ק%jwGZj$iyszOxJCvU\#{yF8:7\^3+fkɓ.:wb^?7@^ oto᪔=nl&&ǔYV݃o", hbD21x능6n)`I)ظRS>=7\h K(02 (^W +02o僪xKH{T&i5<6Akb?TЪ]@M?Q'~2f[Et03//؂d~?;.0l<)ǎ˕eavBG3r0YB+ ZoWwZ҃ dd8h$vbR'٦iz Qg;{QUYިVsi*}؈9ahaUы58pȱHi,׎VLRf$* Q]sA4K}vвnc +#O+=ed f^єF) eYzñdb\H;ۇ?wgj=J~JDVS/z)+ʙ..[)W]֏SD׀0_39KPC//_;ifY:&fCڲ*Se~"b\hjI8XhyϕkB"ujex3[V!V6{Ig/Uteܾ>)IjXЍXQ?讣DcɢÎNQA2Q  z}dgf;Ezo0S(>엄t[뢗 rT5SIj7w7H(?[G9r̓Gkl豕[sΦkRkvI󠘈Cipp޴a>⢳m!HcI3X;n׀;|,Ee7ٙ%NNf=A1> 3AP߀xA|A-hMսbٗpkD7q3w1% Aij^ h,l~v{^]^ɇ\T%o\&]xlh_{Ctf;Ag"QgUHϰu [ ]G:}_yNpcxI[,ɳ#/>+c: yRiQq 7r4,;I8bk0_LĽF_Iѣl"(,jE;ԻFxnu<Z2p!mETC+AMTIW#]fŏ3 ʚ1;SP E7"ߍS0x 7ae~ĈۡW"ƎIx;(jU 9c:n| f^B8,lމAR3%0#H*6QѯkΪ)x}+ԜEwy !MY D1bQLY+^>H+\ieqL9,94di # b[N8F0kUhf1/_^f(p}[qCNS7Bn\NTX@mA l^eC7?fm|}2sT u"hc>cVq"RR@}2Z{\Z=p"{.܄%pT۔&Zm_/Rivu sj;3?Dt35~Y` yfGp%_S.1e_) {@")# -5i#HvF;ń/>ÜFm*:)- `A?syAK ~xPi*a `?: }nT4M/"`Д܅ y}k 櫷5lDK)X}}L d E~>֮N<εg9QsQ@#ri?7fMKf5:8.D./e77~5&t &O=v]Fo(BhP;"~F:7EcCp@WMK!vۧ{ orKh!Eu07`-tFvg=?*`#v GMzȨY@OG񥗥zN  ]~v?k=9ɷYڃ'Hj֑ |f\.SϬwRNASd>M f˨B`Ο{VϹ?GiɅkCXEq`lA}%RC@"`EA>; Al2M%n`ϊ3|^4 =x^GmAT$IHB_d/M0etHPAM?Z4{xahΒߚV#s%J=3/fZvOT>-HҠp +2$l~?.pt5Ed8*SIg`nrH{bq@-7 #%HC0Co`aOn͠NC$N{>ATֶ)2@HϾAO4D?as) R`e$c+lm(K@&Կ]e"yq,/' Γ+c<2_]X.F{lycn.LmyRh卹zdf O{y@E`0E[ǣ=G3Knǐ  ,*f<}C4B^z\'=.RyKUƔqPwq=7"WLzLt v?9J-hHR"HsRAM<Q l<V~ V,/#YۡL ̂.I]Ng$LET],P̱5Cjl_0R#9bl/~1)VMa.3&ϘQ ?/ؔ l+lTW=l%;aB?>DtXo1drD/*6jp2,>3^(-5?+8=0Xp=K9}LK8'p]9}/ Y kZM0d_bO+PIF 6LawpBt4(N"EUoN؂F$NjFL}AOI>TI?z $<2zqb3d›d`h4 H B7zdDړg⽵@5*xjO}>AYXhl256J@}iP-sۤ.!N[bbhֿ81SwlIa&}5jCm+/CJUj} Y5SDf)3G?+DnZm;]t}>*l^UD*Z\ÐDѸwqfHjoS1bP Hn #^h u") [ B]l$৤CAhbS5aQKt 7 [៳>ō!zi; z$42 YtBrW9} 9Td'Z}Gra*#E Y [PY!Hh0H1TOJ+a!mO ggY3} DMs>iՐ"C|ٜ+&qA]h|籵'lJJVѓY)OC]ݰ߄uZϝUmB\ijUibF[%F/RD%?!}?KWMeŲpeL*b>E\(E[ {^AQG^#03>)wݭWwjVV&$Bt2AiU,L!&ɾe OY+!A="MBY)_J݋OFo Ąe"'d 5_ NB*D_$g{ X iYXe\pT__ * \G*&qEeP 8:D_sХco|V>Ⱥ=vv{Iѹ{k ;U^!`D0}ǘʑ#ѕ`"%aQY84E` } {p ^e|")^WY+ ]Kae"6|>- n1.֕!Io1*Q[AO_Th3b8leȀz[9fWrO:~WTR'gHVdw\6OP7jmq["Msc ##sb[Uo۵a$8}B>%`֑N.ƶr<(srq8HJ_')_Z/ /|X -b zt5jn1FjN(̶۴o&(9%9>H̉+6nN걘B/Ԝ6-+yyQ`ړ~FRZTAc?VBkX1;g,TAQ,8$}?o΀ dmo!OS`v!Wt%qdzy5sg1D3sۆǐS6t Ivd`5Ze3Eg"koOM{Ԭ{>1&YF<U!6pp~]=et!h[@(*Nn|0`NpXh^nԠg(Z;R5ҖII,c{<0Vǻ;0P) Ȋyϫ:D$Z&ۭi# ,)߻qM.SsaefR oIβg|^B<~c+aepJpNMEQ=hF)bm89[2v8ˏ\DIdJ@E^@DyYw1ǼNoͽ+`AY3Jq3l a!?NK`V9 `%[!gqgP7+s; d+*W*4^"\j]YgUw$+b*wܻBNv֥IQvWئԹ`iݼf[]o@[a2c2h34E* 胶]A4v9Tmj+aIg~ܡz q$ G3*(/WX ,a `EgG{_N$; Kc}4FSS0taMU_A$8<#Po)?J[:`^}\:?bW5}1ϹEXF400i[fH~he<&Z~> ’c-D}x?:{|xZézwK u|L%r.;n{>o^NnT*r6>2ʢ<{4Lh_ipW=(+dJGf'v !-A Jz {z֩Н8pʻJfOinVo^h@'ϵ0WwX͑b# 7ͻaԞ|OZ |S'{)~кFFUyŋI?sgȠ jEEV3AQ l0/|n?lgnKY2Q=O9B8*2@2<ĔxHR?u؁&"A5]zglf៟eYy7Ba CɲߜԩcSXֺ` O| 2|!K(x|:/qku -WR~GbHdȉ:ҍR"h^R#'Z՝0j=I1?)>bf RbYfP[{GeviDq xbR| *%}[}B6Lϴca9?yֲR =NQ1R~zE1GM A2ɇ2@MLv<*8x8\‚%_7"DR0㗚ePV^ P7_}auF:Lνނ\-Y%noOZc;mou=ݔ?pYtRvTjOi3&_׵z&; |#-\݂l!{Âb4~)olS۳ao7+.QOLg[ڲЮ!@ ٘Dl ނ|wm*7r}n>WKzM^֕)xfYFb/\)q @ lScQ0 `M }!bmݲ_K=4K1 {[)[:kB$<`Q7@:/[P 67 "n$4 .|gb'K)&Rv`TںDQ췘7}C YЩ2h(Xg25 !±S3uUʽK+FAɀ$KvXw7FLaDZ=""F8 xMy0y5=LLO) vʞ=pdIdx|#dM>o? դYڮM^g {hky3DZqܼ352I R%v]"\%:y fQ{CGr3jh\r.۵֘Wr YkSQ{0nW vSwٻDD,b\n'2#`.<1 w?$)P@J}H|h ⑆cL /4l8{z[dk`أW1Wa:A1 lP J5}=(r"خ JgBlTTn3^$&7+!fko.W; r%B J#֪N'vnGB.i-#Nx3?Ԡ0gx%.0}7ٽg4u4qxw֌ ~ G[ɥ1k3PV|EQf%tR2EKRTAxC\GrEդ_}p7"ϝ]mG(5W[$HN,(l0hTuQ _ )iم#:K"M﬙\l% *l0 y7W&]]v+W;Xm N\B[<: 270 .8?@ϩoϝ>F֯U,{n3>9#'Ob険 W7E1$P.N.Ӗe+0A:<θA\ \6Z2^H7K/cz &#u${l]Ҋ[;eh9^ii%tA$x`g=h/)!HQ( ZC0q*fzQ 'p0e0k8/5˪.3whb0a$E7A]4` x:m+7f;A0TfJ @=#xӡ@àN΋09A&m;TL_JFƢ܎x)Oqe]gezu4W=sB)Tr-ȿ#PZ8qM7ouV?j @s5:T@>L, iIxV료r>Agrߍ浨͝kyusw5 ؘDd즴(E&ӷeW E"u~Pje*)^⭚1 (LBټTFʁG P{D?S]sT#ِ RO.a3\`=ARI  y Kc9$/֟$1=y_T^BuVk={B%E٦5~O}P B`XRB(n Dł.BeR,D*Ƒ?smCSyFuʁXn+y&|=V|0Bt .>ca"cV҄Q#2PDr3NYF:elvIvƨ;M3!;I[Ge# 6i=nIaT"w9N$jfŁAӪ]vlQrJ`F'DDP3Z9 <` ޕ$p f@j_*ӓAQ&oP'(}zyzddNj_+] lϻIwy7huEƘ <8qZg r+1N`e.̂4|E3cqP+2#q՟.Pҭ;NLl> afwulRhȖ_19%ƹ 4.TrO}h@ :PRJ)c|c1ids09]+Tx.A 鱙tKHO|,dY7c8:TE6K6Wq] ȵNtМ^OVA7fzM@&пz[\Ѫ< xs{~ΚIܠyß^Pv,dBQTO=C7Yi49^)5#řzx.[}m=HTd>LUrT|GZѭ_B䄋iu϶ceKh0PφKzeJΈP2vC[ vƛNA,=GU p{>Alc.0WJ 4ՎH$V"3^U15sRţdOڝNl-NPA&QM)ԛ,s#.عX.H \n:I"y౶\:o9T}Z mh78Yun=0N2jf.mǵvW[yO1iM<>TMn?(YB|,u:Fr@]Wh䍒E@=.ës _=v22R2i""fbq߄G:Q6bbE ֭bKQ q*lxlgϷ S+9LF$I/+N<fdI1BsequGC $o1Qi422 {qӷ#z^SAzq_{p߻ ]l..՚=RUκR[cRuJbsz.FzW#76M J61p1A,ǖrZyA{aOޘ:YZpIt*DhкrfJs^j=B8,ii'vaNQATI~@L/eGB_!(x8ץA "TH6.KzӔk)TԆl7%lF5ћW@MW׫T֑K*"E-c!Ӳ&!M8-.zPfu;0{}vVݺ8\MKj?y7$FR\K l(yC檃 z ):J~(#ݾWHZĝV4.1>Z4P)B2?2ƍSKcd m;MڡTd0o5|E? [MANp<]A. A`_Nn֧eꫫMCYO;(kE!OR5%>s؅L6E;R5gAl &$]8\Y {%+{({9S/',Nsr"3t'5iP$!`圂Yaf Bj:^ S<29UaҼb=Q1~wជ@()doh;F&-ũ耸c4l*I#ɑ7֒p,+l-:BiZ HS_ @m%W3Ŧl WU ibƨ{@@c>hA[t/cK*6}b Btdxz(;ݽ\tB ]M@ !c,5-PၔQRiIivֈb r?;rAlap/O8(Ķ$jpUXv u")N$솓ަp$( LXJ802*~FRZO"HDa~E *cW;|u ])TC옐{]& -{S腲A1Gk[z "K*GEԢeHW>@|_ 4D^oy9Y&^em%Njf2k ״sow)tcn[9,nΏI2ӄžlX&&ʊgGj8\JɕJ- Nl(7|B+luQv.Ƞϳ A ک\ b!,I-iA^΋xsE$MQ1ʋ-ШI5]f$y(x (b-*`\5:g8``ɿF},?&=I݄@bƜ) +O3R>`Ly59+MR+ nQ?Y8.*ծە\<M׉\p# wy&H{Lg";N<7͛5= Nc@93Wkh(%;*{SB,=TtMnۏ 󄧡r0|ȧS)l^IH45q{D7#} xAV>&j?Muqc7 W̵V VQq٦RRUlWe#Sދg6-ъ#yj*yoV~->Dg}wj~.pT"Vu-aZ|v^wtJ5fDr=Ҥ7gK.s`A*ZcdކF\"95 Zp9&I1uXBPl6ux7Wl'KsXZ3Lv-vmaGc( 7]"Ϙ{] NL~vȷW%6嗹1TO (Ay :;u@܁Q l/Xq8lP!;HURgBVb]ڋVYd-AN@ kPU1Rd@YsZwe,Vv8feހyB"ss& )0 1R`E״   -uWzCZrTDB~Džuuxw.r* "2fCMC XX 82AΧ_35#Eͧgy8 54{^[y]JAvO̍]/ g7~M[#ujכiySS[feX ov.?h2^YvVcp AYɀ ZSš3lf97Nh_ni> Axj o`Q<-ڎF_DQ")HJ3e"H|%Ή^*BnCuW,$DQ FWˡȩkYNoGlR7z:J6<2)fЂgw놶ZŃ;Ys( Ki@5vcʆXK^n첊~6bg5Ή,w:1I*n32Ԛtil>20^%9`6e#Q;=>ޙ&K]囡nU><[L]hkؔ}StS57\^k؍s3ET?n\\"B6o0%<W*NXMUK=lЭJ:ƅ(:D ƛV4f)*iZ__~X.ul6ǂ4V`H;B9_UdMC1a NON.2i^Iˌ+7C9usg{MdߓʓXe;]nQ8̰_HXR3м}WI'|a=}X\Qe*G1Cu^v5L!qf14HFCPyV͘Y w5 e ?f<͍!U˟.gvHU_4h(h<@qzL!鮱df)n.[݄:UѢ"YPdL/ ;OΫQ]>BۂiSj65ņL1mGŝ67׆ҋu}=wn8Q"H:ToA#5‰4Q /*2xN.8D23Y0 ID> (RnK-6~hu U]b+oV*eJ[V/nߠ¹. eITYI {L$~`j\u๩s%0A tl| 3\whEH6/=] ݏKxRiWTFǦTu{`n; 0aB(nrVbĪXz[eiqOHGfÃ+pq&1ogI_lere*!cˎclr,nd9SvRZ;4FAGzcD'=YX9C:wnzz h'v(;1gwGocvyo76ʽCQ3`xbҧ|J6l'`uktK D+^5"'KzARH as!a{ҩICŁq lDjM{o`eYvHI3yM4d+!̉BLk^ ɧҺ!mAsj$-DӜ*\$mhw1!õO A>y,#đ`IK_~2yP Ļ:@$DD 9*ek1-^'AE(֋*Q":Sh@1 Ԯ_eǾl5қV؟f&`i=|LݭQ%bűW*pJ;;Axh`W9ҥ:|g)| 8yJZ+nj/ *ΦO˶y>t D>'V@R_`,-'ms5ds6+d#"Sqz%>MGw o&~bm{ECb43wm̓lJП"ԯ>7p|*^J-@#M?u 6VXGpRPB21@-TI"5t,4[hY %IgI0~Ľ%hZjn/&[S4c0Fkw:Իߧ*ԓ฿mm; *LRɭ ڤrM~_z& )a[SB Ϥ+-,B{m޾X~WN=9!˝ Ŕ}l"nZwR=x)^>7 /oZ ]̢Dn0Tx rygfY$M̭L-M/6cm-a؋"+]/ݮrm颸$&rC]s׸ $) U WO/免]/hB >@\7a]3a[yAj~!Qgt#=RxIS"RqC+3vZ<ΊN`6X-dfR3}cmJl?pI}85|1P\d6wMpTx3z#mM\}ۼ8dFJpbck>k{km 4vMY$_@wi AAgo*R>9Q:;`kqsN TUV6 fIy|cW/` eXeׯy~3n,w7rG-6;*N:gJD^uos\2~orIbP ~JO5S.w~_C0!lfe90YC-!VIq- y8xQR0΁c;;QT ߪ3G+B("} MK<<˺eP}7S2|9ѣZ/eTWxH¢`/nAf*U_L <,kTѻnpVWlS0d>So[)e]*d0hdb]=p?(WuHEJ8RpWf\"ӶP ^iK 3gp픐+Sntk(A'&Omoɬv9DzĸYVbFWlsV^9 pȠ1:3AeIDуxΤD7K#V61ބ?7W,pͺHRi L?:B5C@G#mgE6!X` (. 0OJ͵{`ׅy%O~V&0I'pCq[2YBx'pܦol)o!DŽ➥Br=~dWx m~VZSK_j&bBt! OM~  Q6i#Aia7W q,8 DpNAOs1 l Uz4A*b9KOBb:PtWl$3k?TŎ Vf\x͕1;#frI$)akp'MԂ PPw8˱",=mSx%RA~A[6Ϳgs [i}{/3oq]a,:v4{nS:T$gjqUhc 㓱 Qɻ)$pUoЇ'i1])QPEd! 1 /hNNUFL{MR -dwv] w[:B2p0rΒcy_j@rc.Ճa3P|TG>Ov !@໧&x|IyIC*Ņc%tvVEۅy~wݺPo"8f%Φ{2$;Tr=}et/j ?yn+$Zi 9p1?HCV~& EJoxZ458#2oW(95gp/fH,.l` ?yq_ےSjaF$jS`)T9%.yQѰÿ q(5|i,ryBv+[M Qo'hQC9FlK.D r_WI;(f[6TQO!} pTKYBT0~h@09U-__DUof' (l-w{\"mw 7wAIubW~}@fWu2Wa^$t}xD.7(4܍WH9J;PŒuLx9֜M2GK8yէSNwNƢ#lpmo3QSt͉]̔D~,I:qWG SJt{Մ< :B8ɮYL[~]S2`_FW7+qآE跦#xm4~8PCƃU/C为#"Ox)YgDqֻ:k`=sʰ*?AzF J"eoHiyn+:0R jŪPzNhPXN4J,CW(Jo(w@  x:`~:U~< XIYXҮ#辥a|nI !%XAK}qzL'y2w}_1#itl% jS,Ǣel[}:'sM FF29CVYn1!ٿ3acC>gNH<dje+_2:` p*fZr)Q@42uMEVH#82/]* $㈰q=w͇*D"Q$ l5bΥܪ*OXR$"X~[TOS`.x܃ʕ$aI<a*fN-oalA[;ѮUu\Bxx6%\Pb.Oo -@ ۯlL祑QnPf/%6ٮ¢ə&r0.E0O.+(ټ=VJ1_,_>xAd%_D Ο9/ZCH|]KǾsHtfA~ɀ>EXJOk)ʂ|Oz VOW=HR]e@52^'+X.g!@m6Ey_8JȁpPeZE=[9ܡ~Ax*6!Ms'o#:2=ut`<TDw,KawYô, dD{ԅϪ.uFdK-nۍ:Ok!:i]f-UZE| #F5GItW*'%0/fUEpxToR BE`_qAXf0KU:G1}~`ߐAgU+4F-XJdDYisTKP*᎑f}U""A¼] OՅp Ƨ =@B0ʉ$ysv[jg0Uܧ2@Rq4єfoK4D ݻu+yk`.UNݢa"׶қ`@B35)*=D8J"dN|4Mh5(ު3G_xFU_"$p^%J;*49:yt82;m R`tfr:q׆)&X̎}=k.4k# pf % C? pp/=ع\#{V7rӸ,S|cJWvDhAebUUCWu]Yk毎ZS} m"j'ָցmEh-^gK `妇LA7 ߃ KkQ?9Pm}q7\mޗMYDqR[o-ͳ0/Wwj4ݾ/&M '7G#?˜留ض0qQrp?JϬX9uӅ\ݤjzS1g0çŧm?\W ʁB0Ӓu@GLc0>z(]Kfwњ8ZUj! dWS`QGEf7SOdU2%#hMm#*:{ RR\{n`8Br4 G`'hlO<+L|y3CфBP? ]7픠D[֗Sr;&/}=$\t6مC=[ђkj.M9ƣ6{ YÏcoC^Vzl8Tsڊ۰=F-J]1JsbBsB3S(4W0c{3eZf4ʃD3?QuzTfjtF%6Au UPP?Ϩ(6QZ&*HH=V>Ԭ4PcBQ mR;{;%G郂9iuXw vwk WÎ r 7]!y'c}0J *q$[҄$$SK/$9j^fD8Z-3@`)3_TP$uf>s:L'N!tiAAx5S:I wBco%3N~Cc ע4W(, 鰎D-Ֆe*;G82@>(%/Se=bq]Cā$ x%&(˼lWs$E;ŕ?Lq;V4/E\==1P힖:۶_p]nA\ ^"Vo]/&su $ZIkM 2efh8%86 I"CFeQO1Zjǜn]&?ͫfWq9@T#E8%}EnԵ0 +pf7(dDS\PT-Vg^Օtد*i'RZ3.Ac8d`\lUљf )$&a3v6KT6%OQoJHIS% x:FLU놤PWE2h",6D+mA_;~Iv-ޏn OmW p03y.@u]@k`"g/,D;( l1mmܹu.(݌O(fyEDŽ%Rձ09{&7J{c^b峛aFiuPEAI=M]Ta%`mFcɭw s UENPEU! =K^Wmu4wB㰕I=%+_yZtF:G5:U'VY; \ |aV죕٬i$^`kdkA~xQt7d A/ ]Eq |;\R8Hw2M߷y9roݶ& ߃*m,RQ13}Ny#߁70t]NiBׄkG1#)&nPpTFY\P _<\]0S5u{guP'2t[o5+C))0: {Թ+Dy ΧN*qX^Rﰏh]\B"X}67u ;xyΣW;Jr*2 5iәdSa{vБ)Mf/pyٮ?*9uˤ}K=~E01x) $$ (yTNNv~0ſ- `6|vB6/n\0Z_(PJ& k&u[ C(S99؉9I+/=hsaSCO$=mmUA]I 8.I*"f*dFO6q&=/D . o!ƛun}BMwF"N>.+kI=V͛ ܢkg1poHڱ`IZS^vb1K1UaFo'|@'΢"Bez9l_DBz_tӲDsjnX[8{]6~"(ӃЈW'RaYvs ''G<ڳNqZт߹dɎ,Fr&H.;ہXj%itIs}i>աE- 51t8b ψ )A {[sN~# A@Ah_~ =O+lƥG+C5bHs/`aRup7Ox,*ҋNGƿyR\}yMBE}% ,x1I! s%XAqfȜVI=T.o47{čGXl)Ith*_OX_b6]Yqޜ3&Zz_zd}^q=km^tB"ia̯8j2,{1ѷAq{>Ol!bl} wCo'Yd%#?z(v)zAv$Ĉ;r;dlsAʡoW^ĺKȋXqkUD.]QfW{|ŖhtH nK9i_𧻽D$$B I 7o|_Rl̇ v[JS}Rq~D(X0} ]ӽSAymWKm[|{㉁M1//B\5H&_/9o9Γp(bLS9}d`J8p` ޻e6A1'#52dnzzCb)M8CkI1"@sC p VsZBq^Tf ׃ 8Oc˰QU]E FHa,R7I LAj~% 2."s\〭\0T_g$_#(Y45d#4zofuv)A^m@*}(6ft#`*`}i}uފ4jhW%BraH%Apan-\Nɗl-uc?+\-rؖP 2o0W;d}v[|L뭂l0G|"L\L!VLLl3]QؗHQ;C#9$-!8BgƝsF5Ò (5_W\a|ΰJ3Lj hH;"QחL\^Bh؆q#B% oѝGI}DV3&q7rb;8M4?2kt2MLtFXR7#q͔#b{-?#DgNތ6ˌi_{QkuC@O`Uy*ֱ lZÔܵbgTIRsZΛ{@'-uD +o4#2'3I4dWv _sVZ,$`wjkcʎ#Ƿrޠ@nlR7Y0w+pf=X&'OJ (K>C^FQJ衩_C% ls#4ø27Į{U; gt.72W<:ccҹ\ჴ[GE+U~r'-lLI!yTU$o-"qfsv}qKw pH4%Ec/8CD@ON|-ytZ"ũոq5D/TV=p9׿,--}Μ|@oaPJ@r ^q?3aʓf)L>M#TO=cJ'tsRb/PKskʩ?oh ˬAF%Jk ١8]uE; })Ab^F0Hb[pCW=:?/AIR[;>dB=¼ Ze /oφƁz2+1EQL90 t/hs=_ *҇'f'E)8?[JڶՒ~ 'r11z@7`}z7'!ٴ.'\Y6n V(,(xh,؉@-AiA) +(T kb|#1ZSbNSqb&N:BCsdƧ1$oDo+# *F=0 >ސɳP8^@/Ag>&@ZR@-{g9a=n be Fjq u4hƳ{U 2 5A !$Q!R"j'a'vH0%ǘ2tOૡEޭ6';cy-by6V _4 KGZLÈUI b2| iJb.jTڟ6] ̺Xrsi51wY"n@@0$p73B?GL׀DPcH0kx. m^n-1<>[lzom Hq) Tpb'֗]( \6>=F}pe삩զZiQ>\xfsӽyo^Q+;\&ͯw L3w )a┴&Hd.588c ICd# 8K9ٺj j2p(`v|?[ŵ8,_B7+L2,\ZpH_e+>9mۇwTo] _G7UY{2RqQd76QX-5|2.4w%R08ѝC{paeuJ-RmEJWw'#~0KRB,smo`!u#P#radH^7q˘i[zd;Or5%jp͍Tq[:1n $9 TRJKO^Vuflvșګ{6D_'`mMbBKG,B͜&wJBqsyHB!e J۳z~ioރz-sԁư$n&E@Kqzv|"hI\>ӗ\RZv=$_HnPZCO3i^1a ;pF1(֠ng9Ŵ X?``_=,p<l ⑘5vJX~8=LK0=_-l~(#'ϣW%T G-U2򽅾 p09],1=flHn I+uW8IС[Ɨ{8gb+IH>2G$l}#l;yP_\Z#i$3H'= R v{F){^0=/eMrš,kヒ"?rn#;8= o)Z\.s5oRU'HihRB۵',lgXcASugUʚQDMQG+6ph{#{fioR2TyZ8G] :r°eqc$WeSx U%2%SՋλn] EwˢYjte (q["u~?@ڗnP5)RO&y Ĥ- OVv_w.`lej/gU?UkJS-bXK4gԘm0NiJBǗU_v*d}Q]NЈҍ*&$oڦg 5ѕ?l6jֺ2N%%, d}9mWMfUdP<]&!3ЇyH2Q뚺FWD|\}?o::zp<3]X]bBF2jSGT?ϓ՝c sڤ|u辨Ԟ.kXp^Om%>Emu//#Zcno9vU KajC yuk~'2OuIe7_yv Ѓ Zʁ'Wd$G=wHNRsVjÁ9]|D|+^oj;WG-|DS\̕&ZZ4vGFdYSr.mgvɜscEG%6@( jV0Xg/%DSŚY6i XPOE]![ϮTA\ɀ jK}Oz6nIXJBrUhĎӱ']:tE&nj$=8})k+#j"-kÂ2/Qp*kxa+tU1=k_>΍x'ʘg!9_"~A>9+m\A.qyO#l~7ߔjܚt#YlJ Ri)7GӋTQ]Ruj8u-R3A_߀I'eũp:> bɵ I٬)[bTG+6 \Ag̘/  6XGjZ],ޗ'uj<" x MƄ.{nk- |54FtWdUXp0IXlONUΦ *!aZ]+ѽؚ@ՅJCnNQpe*i:>ٛ?mFA߯O D G_b܌o0=1>|,/ADoQKGʢœO/vZÚ@5K\hEB% .m.+elΙtZ~aL{u =NaYb94.:0SD/}P.<] фJ\;ϣX'P(U;\3Om4~xލ\aѵwxK_ ]x[s}R2m$4v} nZ}pΒgFFt ?x+ q3X&dXdqD'0*L›FQ)Pybķ*BT_EY"4(I&C0Z~StV78}=!LFȒKx9i62Zl^XQqu#\hEoJÝ;SFa b?ܳR!}U<%+,uB)`TA zmIVm 0s+/=KPS(V*Asd3TbqUp*/>7+CX&[3+5c{<`A o.DZ cl;ĺoj–Kv$i7y5{)t|GzoRKl(G,*j(%h6g5C(I'X1]H2A 1y \#˯mG+9;`$jB-W( I.*?u/q)6fMpV7=qV\ vJK~qQAI=Ɲ= 퇠ZmL`Lɪw5\ 9ۆcm?Qɡ\v䦦i@a}/;->kƫ0f 1C+pZY4^C;8{.|U7 *W+,",_MdR8RTy 9aFMr+\'ÔKew'E9i9E 4rfwzJ#-jkZ)jݡ زEO%M`CVs3bDǕusBո< 0 BL95nsu0'Y|y(@Pzbh6A!7s*T'GǵJ/boՔ Ad > "$H6ȈQm 7aP6oe 9DpS!bM* @/W0a@@~,Mn#j$U .9f*Kk[岄e%@vbq̕Մ\zD&߮]e}ӊ\I@RVp_]&4AdkF)r AˁE.3=)&c aų SWXOARo aq$icK0ShU&@3(;avZŸ0sPLݍyKXs{3p6g\+*i'%!7[2z?x{-)i^:jpLNڙ?c͈apNjM$׌ 070:H)/ O_[m)!8rBu|:]oiFgG{4jމ#ۈ52-@'2ɠ8RLUVMf,Cԉifd9zFq%:m ?Y1^D75UEQZ`Ŵ"__mrYɮ|B㪹L.[{\J#PPOwt>ev4*~C6m=% <-8 *{oxAeI x;ܝ!Aƣ9~~T^MHEķ!!Pes|/c&Ak^ C8p/wgͪJjx>ܺS79;~q1֢ɨ" ~h,o#(t|DTH?觤7^lԢ?/d:KqK +.~>d-<$(heT`0"`?æX]vjIA% #,8b*/3 G![r5ʅC6=6ZpwF*]0Uak{#`)A`_>lRcBxWmUzC2;MuJ {v`.@"x/XtBY5= 0չ ?U߬> S1"<|WmIc{@uGou_]kYݦJ4pjb.]n%(TJ+aHɖFag) lP ]W9Z{ 3{qՆP5Sݏ5[ 6ʙZ{p@'=ߚ!U9Q$8rUJYzax@(co qmCՈe>iO^娄f,!(~!agݦlEZ`&AkV{ms~ vuK *mp Ykcˍ'8J&P'DPYzab?Xk~uޠ/>& j̓*Ka K4塚ɒ)Ke)ӻ "VC0D+: rNe1t2-|69a2 8 eN1'T0)`Rܲ5p8mfބT^xW 0q4j\f.c1l!ݹk}n@ e_l[dϱ!.}JPigc+w=Zk 6YhڿN!`u#2xly~ f;݊XxP"V#Ff $v5/̂Θ#^ʆQck o#rGfW4mDK鏉9.U+Fp!F!S*Tc1$7gyUITRAUΛHj]ШKjs8%3Zl $MˆKV{,x~:8"qŷA#u@Ћ TDF|bbz;}MnZdi-ޑ] G26Ssc#+}PfKc\^+Ȯ[dk0_zjFG`<).b<нAlH7ȇzIN yINq.O.GkԫuZ(&R]j\+L pvq[J ߀sfnz~" RKN<:lEA e% YS@8ZÂ̶WPm`9 r:(:[@  rCKgVh{/B沘’$ZTYއIҳ ginGrB,L,<.Z]9Dx 5H GfEyORy>CKj`#^ R|͊I# GMqAd~ H 3Pϙlқ]К* 5ќ)6ɂbUm񙦠G7- mTyuR`^*`J&6/owH|̭`ztTn9cN tQ%xh͂=!b78#'M @). G4g\xx9 vE>.5o#OB ҉OYrĦ krW螏*ty<THHk=e~֚䅄}띠6,ZTƾ%_aK ,$ƯZ:).\F̶D~,0Ag+̨0o`LZd/K^iT|]}=ZsޡI d 4PPۦc.JKG _b '_@XgĻWcګ%b}NS#xwq^&p-q'PxA[116⠟$DPfAf@T F5$5 ̓UV+y3ƒ^ S7qV?<ta×+l\@c'pH`laR6C{5tيh0;Ѩ";-ɼGC{sNnnj(qFI˶ۤ9f :"(-wbfJ=XF p- wWw%U٭6wN1JO̧ߺxҫw$S0$οK"T(LƟ&<)Vp{}m'ߠT.I*فKCRK7{AҁB yIІQ3c϶+Y_7~仈A9a䑥^Ȭ)Cr\ᐁm;`El&S #"qoڛ6vl9LP XlEDlյU)f.7{c ?a͢JN%o϶(r/ +dߎ y*3QA>=8 ,#i.XG09p$A\ԡj\[nx-IS?Mf[;1SR:{ҹ:=͞k6_dοLG!DPгVo5ƴܕiZpuujm s[W#hOϋ-re,| "Q届0]9|OJRS-4\x.mxÇ hrɘA[V^ 8-P#1.}˳VyAhN鍗h۔rSj0; }:|??g.. fnkRHjZ.nĜ0](w~/ /GPpH\$b|qp]6>SKwp p *8pZi:)tb{Űt{TJF/zu0@wd`c"Jfo֥'뫲۲lH ;o)#WPI\Ò8q=yڷ73vܾO_M!货}`Gցc% XpdTُrO@ 9 .G pC[&{8fTnb]Q@UgJ^^JiܰA#g/ȃ\L9^FMC>XH=o6Ta,kΠ=eL/i8 WeCY?HvCE;(z˽0ҖҒyYFJŴa /A,<Ypfps3!N檉7(D\s刯11,o6t6 ymsp)fKhrݸ~<Kbl8@Y.iS{.-e|G݈sw4I)<7qMj-([~f-~<#n5me .J5ʏoP\] ( ^?8Y[pm2l\!v4UX%\p6a/#%B*0QB}>I9|Qa= N*?C"CD_/ ێ@r]osދ=`[jh([жuZ1oJU(Vl2{==k8Zǂ+5_^k\|g+|f9 η3 SA*\/Ns8.tD ~`m>n:";ݿ{K]1Y6##L$㍔6WS_jPc FĂ~!MZ{Rs}CiÈ2n,f"X7R8My5#sqia*gS2.Hv/Nlt 4yi_Z"Xb 3V*\iA}J)xn2\i@:ꆺtE 7;TF_3|Nh4gT3A@[{g? r?v94O)GnOsNvHN$\l;ՏO|VrUg}sZuILzcpzϭg&.1: |n1K2ߦ:V%vYewɬw(۞GV8&.Oy\ad$ڪy_^8WRan9&թi!.ߠ Lv"y9*Ѯo<$ B\gnuOp3XѺYn*<;3)WXCK'rZ]fM\ k"g%oM܌ڮ/vХ(h]ǭAٹtC.p6ZX!F+RAfk|ߑW99|TyS^~()s=Qw ,kv 1i'UH/{[亂^0^z$-jo҄ 9 W*~FC_ ]<~ ~k2ٗyk_w<,\֝/<3-hWdrZ.6LQ%&[ m)J1-S9Ywc~Sz:5ֵuϥRCfRueYƱ&,dV4lz^3Aa CwxH)YT!6ArI m5K]dрːYr."5bYԛuy>:X>c_컚'f= vw-Uf"fH5$q0ooch}+4*3',,tdgbϴ$_;yЎ85;g6l(m9!Y煻ɝ&˿FWDn& Fg9L[fg}i0d06#YyyqB.Y/>tihq8YT)0T;Vnu(ês~(23j-!BI¦K|[SColA@Aq  J ѥj+7aI,iłL Ihϲ:MD0QIҖ2i WOJӈUE^&(?QJ=or3aq ZNL%3'$ [CI ipy@:OJXƠ9+CY;jDTaHhO:mr}1-L+q V~B+Bd {vP<}\ZhqP i2[^p]kWߡp /gqٶ@ʷ`bGQm؞k:*>ljrySYM (-:GXD,n2w`i''am& C7, [2^6 5fƭ ʜr{A]JSU*I|chc|62ʶ,fv򠷫7o 4\{Gz}K݋s 3l:EuQX\7JeKME( GRX. xf0Yot O)}{&V(P2`,"V :aaHLWdk;X;Y{E׻js/O9%\1a2M_+:r((Q狘r &xʷ $ě pYa3QyE%~1_εy/K6a,LwEfqk@%& ;mD?qAPQ 60|N##DZR9U!;<*ȕjC )Vu .?W|!0҄[ +S|6e:IhҪk P]}:SNLjTî:3Aҽ7 FŚbIPzxv&M҉wa/.B|epCZô2OGyڎdhxGrqӿ?rJ${qc}E+ ֘6zՠu#1Wz vf'F7 ii Ny+|֧7gTwZ9W$Jk>3bX`7 ~xgAg̑Vjouj| {Ң,}#ȭVgzF iѮfr{Mk)$\`22vl .̮5].(O7 *(^H<^(s2+P@q Dxw*{AJ@]rL9Xl%|nuAĒZAGAn|Xh[ ԩ}n9NNk!F :wHa_)A1Q*Ukg:+CA`cPEï)wXI$a 6vC2~jSc%-N^Cq$ x+Ţی}ݶrjBƻN$ۇNȪ_:C!*2IM-WpA}WqQ;B];VϓEs/(d*!dT!@b7|4EN(XuPw"K:D M;uׅIMtYG}t@i;P l] E,:or 6zwLu.qhRZdDAMsÁ6WG Fq91gJЫ9*y=g.B(.3! {ޟM`=u')Ȳ )Y۞XU Ay B d`B=| [adMAK 5.g;%141aے4xwXlV3][ܩA4#Mʭk[(U3 $&PGJB_$׹rT-E@ܚXW|Lm}͑f˲nd Ylv18T?8_jAE7h_@o FߝƮV+Z/I-8RfZ?D,(G n\K1~%,9ֺsҞ#ٗ]9TP'j:@X")rXoTsX4u#rjUj0SAjր"@`\)h ܻx +D]\$\UAIGG%rdGfF6+xPTUʕym6NᠫEp|*Nn>nߢ8(,lѓehgم440t;!{B6ֺcZU;S 6\ƎkiKo#V؟FPYNI͋! Wlm2IF9\ %djkDp\V%6FW6Th~+ųl_H̒i<-5OI#"Z(`֬w%BՁ#pMA#f m:qz'yu%z浱`\BBQ=m|3J O1-,ԕDy_| Ejno0iB#jgَb}JWaa"%ّsD\9t386pIa0!M퍫&,m|K#$~\$gmꂰwϴ'_uR$sK3dgzF9A+k~}c"2dҕUDOtڟ6Qja*2XUG'+Nb.  t\q剗;;P!V6KO&ɲq,fbO⫎r@ˊ|/J|*s.*bq9Hَx3>+bI(ˠ;tr4O`:"J^""8"4ò'}~SIt؇h:.Od;_z^8?:Bcs |D)0'$j>^AqZ*[t0TUt0H4$L0nIV=WT&3#603+15D+_![-锇ۈ -򾜵`&B JYs”åO&/'XYи[,׺4YŖ%"AYJvwm#14(Ag뀞܀`D*1)TQi,~4=cbYO5$nE#V%C7DEc'Nv;d@{{X+|6oNjvNP,fʰ[}r(er=f$ [gXX4ULG/43t6]W-t  gz8~6B%6N1H+N*]b24v~}-5u:6xrF'LxK-pg0)gd%_ spu ~^R:,೷HuI K/*`xۏ G*@yjAjnߑʢ 5IL'f>P(jyjp'䰤1 ke%RC $[P w7U*H_r2*Dit:4~ѪzU#j׻o2X_^-G}Gd%&j6EhGA\] AI (R4-*F`Qt)V`\ J޸yLO/(_@l@13̎Epدv>λ dԓq0 ӟ #dǁPjEB Q'l!3d 28JK(P^Wq_s}M/~nag&Qw'!8+ژ`?tz*m"ŏ { SKm> bN8;4K|AN06^ZYL"P`st_vB 0v*<(xЪgxſ U1o Zآ%xLv#z 6^O'Xѡvz_3(m F=nw--0Dj%p%aԞ; im@8fI@<ak(es[S\w4(fOʷedGѣ Ԩ.Jl=6o4=s$-$ tFN[(6 h#K2 ͫ}CLZY0Uf:5X@wH oA]3kG$\{W`ZQxXN~Iq-2ǀ'"T^:T:폅( zKH@M0 VS䭣aIHy&'L<+!ר] ݩl3[7=2't zMX8yOx9S8ȿuE2F;8RkԆ":~~ W8[jwڽmHuǐ;~)ه- Da1m>j(.-`=Qm.<\U6JTx_2Vzڨ >^6#D,T5 OqZ0\VO|k{PiK;J:Oť(V'8x<3 EKCjO"2=RTUsrsove  ßhxBn9GLrd{hF}ih2 QPv66 ‹Y\'L,=NQ{ufsgR'Rqm ny]SDQߖv]A5{lupG_ZfhPi~9 A_^乚>y!T#?3;"#1k՗>,2%'# 1ؼ)m]I&TxYLы54VϫaDq J^zh8>97k"δJ{,v܆|7eѻwD1 ߑ!Aw8{H&T1^nC;1Mx9|.?^4h |X̬|o<f9ΏPAXk 8єq59nq]DMpZ@[%,M.UXhgpnR;֐LXA܁+  IPwI)dYZnj7XZ$0afH2HjCӟ#/$djn(ʤcV>+A]#_PI`[xʼn@*贊 Hǽɘl(˛gŝIy~\[o-@B~yVK%X[MK ։A(z0(ݢ0@հW0qq:bdS'@=6U;BrR/S˩ÄuΕ ΄#([Wyi %ni}B@*sJ fR2e&[þK{&0 IT+"_BUie^{I_^h,^Q,JPijaQi@15ppYj(ѵ\S%棽WmݗԂAj+ y~i9/2L"]*U44O0M=zY xT 1 BKC^8&hfs (1%I;VJ姹Gۭ}e9v'_wRg 28nA{BFgKK/q*lf6M"qqwlS%ޮp[06\rj =,=>`we(CN/GL!Fy lr9ͭxlFj;aq8`X2>bA`@e=;$"0# UyHu ylC5'DTVZ-d^K3i`G6*ݖ ;i $l@d#ܝtF\w㎷G>-r!J Vd1yT\veFAA.]'G&':% !29jePi̓-WȒ/vg"jF%HoZmiԼK?/b2^ `@#x,=,! PgR:8f_d?qm"Ɠ/f0B-#L#']gu!Qm> [K\Z_o+bxҧM@L AWSV97\owOWUNώUX2uPZ_ڄ\HP^*IWݩБ'\5#˒{l[m(ڢuY,bSh{6 u2cpdk-o@p6P UOv^n?ӤJ| =\x'^Fcp,XB,ll>r>%͌3mIVA`Vsgퟆ=Wc _s6u(Z4.7hƱ׸" ;v252 o % N"R 2ҖYHml-1\FL:J~縏&.K}Nlh[X!$r3tɉ_jepTW@ tFpUΗkBnxP{(lf^ n-T_Ks0sl23y` /X~Mw@kA`k>QJ$HQ_B}EPDXk3W%xcESe'l\Mkvp3S[kh>+ =xQNOv\kY>z AubֺĐ-gbEeC@Q/Lua0B%.loRsFKU:S_I9yIX@C KDl"XŰC=[CTFD?lV3ZKQ;Cxb_KXDNOX;/=Cā9P0>O% Q H B݁n .cKxje0ㄌF|!R~}b~{ KPpP ʒ3\|.{I!h$m}%~ wS{boʊTNC(t;Fco)r7)HTW9Fq(9>HsΎ2 (fJx lm1K(3 C#us&wkhF^3__FI(`!!/&(;` O܎RChntU[yJLӒR 9":cw2/'NH.\Ԕ:ڽ70Rm=q$N|B'SxHgn. u| LTz'uԌBWRCwg9p57E?CW}EU_,悢+kwO,rZ [tH;\aBO^{vGr@)5GLwpT~<|.wu1:tv>d+a7uk-/J+pg5ܵHV`J}[XFy&Nox׸>Kޛ!Eܭ˰JōnZf2{Cu 7Rp+us.\S-%nx 2(f5z3g؞wx8#ڒ+kN\sS϶£Gwf[z$0V[#ӊޫ#`8~97A>qd n'k| |œq!Iz!pWط QwطR7ݏs)Q`Xp/u7+6yv}8TsI:^+G+|!Z/sR {\|F8Y.Vet^&[>>^nU|E\̚G BZ=CD(qAeO탸HV7@(ѐngXja3C4u*MU+K}nazbe֙(EM?j"ucMp[ MA(R䈌X9ss)_x5r#Ss`y\z^2.V'^v(*vQ1WJ9J:Egg@&i4K<-ccYgSU3R12/Qaqh]u|.8YU_Us;h+AI>a=|auX~nyO~g7RN;1 `WzY?,ODFuTx\=R:cOƤ |Z _"3o)]oI9%0UlH5mn&Fit!&*ݭ&(47Xs:A̕h}:[B #IR2QWboU ZJDq9' *t_r@>Wk7ň(l礂1z԰< X% itcUdCQYS @B Vqeпy($ l @. +t7my}Q5C1[^K_YY%= `} #?Z"Ԝ b5.<77VNXBe.^"3J.^Ko`q/.r;Ьm]DJ!B#Mc%Cb({FJ/a3P,0V,|ҋ 3gP p <LJyD4G<04*]{#>͡ @v&2w? L-*ɲH RL{S<CЯ݈i@Ad|LGs :Ae ctfPde#1z)nbGXA-xJH2{P&Y ZY1_[6߶yB)A<]):|v%|;?.ٸCNܭakytJ}5NdQٱ8TfI,DRh2T` }ɲ{v{=jeclqrPS_\!H9&pBP3VwM!b=3@;CވO;]aBB1GC=G [Sp'FPqp@x _%^B} | y1^i;Z@~M:"YGNw_!ӍI@pGtTTu['Y#iW|r:4oF{|// EG?|.q7wϷVưQ ~*Nɽ#^JZA:c˴m"E{v4M~LOёL! : w) ;4Xa!'$EVzZʋ<}B,3c&vA3ʜYp~]\R6iՓ^$0#/! + E2Qb@AjO~XSk,>g1nߟPB|lJU1G)kLK+y;F+^/2[> .SeM&wp/M V0L= ~|3`?O|$ez)drm Φm;ϟvo9:l@k7&FfH);._sY?lYn sf7;Q\hb7Vx$e0xh%-&t*J0A2޼)ҞoRάDwC sdQ O [ Cp`+a;5ȟ=bތF^0c#8! ߼0M SNk-Bfq' xlְh1ˈLX'qjKfH G_.)#fQ(}^$hFu#Rޱ"EU&C`<뷭vP콳NEt ߀ sP-&tvXٱ,'lPV=LJrA9Ş r0& w9?Ven>쾑:Em4{gwҷsnc_LJɊ‚;,?s3ET60Z4E/78Fm⢓9ƇشZ*"tE LEdz<(17nʈQ:_4vO0@ y#17g؛}'bMaA\~s0㣖jR: 0o$ 2:`Bp@z<;/^.L 2W7j|t&ʡCu26& Ԑo 9k 1g@t"--ơQ|+P&s7}ɒ|(RStD@uLEnpj>w1-cYr@Ɔ&^PcsfbNooykY ($2bL n/ -*؊}׻DTS 7WBz1 t fC]?*SHչ `v0[f < C3='HW?Z9P̛ $*vGz&NCVVbjj_ڝXr:)i:&(KXip) o9771D.z s;<LH.W,wXt,I VmLu6aH6VM7oz?O‚3q@s>1z쓚lB?NÐQ9Xі 95 \# 7PMoNZ6#p_؋E9rXS[y2}\LMi&4=PӃ Aڮ?M ϛgG ޿U1PԃsAbs˃XK%jr MY|{;{kz9¤}I h@!Q- )R96+=%nͱW= z3Uc-7ae0XKyP{MSc5P}Pg{[nL[ԔUK~:oPU|!yG?d`l~ 5!en=sK^~3ЇZ}AY't[6 ӟ*rSrz蒌ܐZ>_j3*wQ~ÈT8)`:UN;3Q(J? ڂ,5Xmн|m֗NN>sf]Φn+^ E\e+?RX Eoۂ/tHy{};~j4*v.ɯy,Cb57:adi>mț}A7'1ď\]~\pX!##[ x8 [R5_iK\98$DȨ`%XҪb"E1OK&9Q n/1vzնm~+/׀OҫM-rnMM.w.#i <~FߔoU>N4uey4r@5; pHYs.#.#x?vtIME MctEXtCommentCreated with GIMPW=IDATX102 s$ R=/'iiiiiiiiiiiiiiiiiiiiiiiiiiii,RubIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1669912846.0 toot-0.41.1/tests/assets/test2.png0000644000175000017500000000113214342154416017174 0ustar00ihabunekihabunekPNG  IHDR2<W~iCCPICC profile(}=H@_SA;qP,:jP :\!4iHR\ׂUg]\AIEJ_Rhq?{ܽZf(jLvE atS3d2u_<ܟGə 3L7,uMKObEI!>'1ď\]~\pX!##[ x8 [R5_iK\98$DȨ`%XҪb"E1OK&9Q n/1vzնm~+/׀OҫM-rnMM.w.#i <~FߔoU>N4uey4r@5; pHYs.#.#x?vtIME 2 @9tEXtCommentCreated with GIMPWCIDATh zl,UГc, s-IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1669912846.0 toot-0.41.1/tests/assets/test3.png0000644000175000017500000000114014342154416017174 0ustar00ihabunekihabunekPNG  IHDR2FP1iCCPICC profile(}=H@_SA;qP,:jP :\!4iHR\ׂUg]\AIEJ_Rhq?{ܽZf(jLvE atS3d2u_<ܟGə 3L7,uMKObEI!>'1ď\]~\pX!##[ x8 [R5_iK\98$DȨ`%XҪb"E1OK&9Q n/1vzնm~+/׀OҫM-rnMM.w.#i <~FߔoU>N4uey4r@5; pHYs.#.#x?vtIME :n tEXtCommentCreated with GIMPWIIDATh zl,UГW{9IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1669912846.0 toot-0.41.1/tests/assets/test4.png0000644000175000017500000000114614342154416017203 0ustar00ihabunekihabunekPNG  IHDR2PaiCCPICC profile(}=H@_SA;qP,:jP :\!4iHR\ׂUg]\AIEJ_Rhq?{ܽZf(jLvE atS3d2u_<ܟGə 3L7,uMKObEI!>'1ď\]~\pX!##[ x8 [R5_iK\98$DȨ`%XҪb"E1OK&9Q n/1vzնm~+/׀OҫM-rnMM.w.#i <~FߔoU>N4uey4r@5; pHYs.#.#x?vtIME WtEXtCommentCreated with GIMPWOIDATh zlUГKAa_IENDB`././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1704229718.33644 toot-0.41.1/tests/integration/0000755000175000017500000000000014545075526016462 5ustar00ihabunekihabunek././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1688731561.0 toot-0.41.1/tests/integration/__init__.py0000644000175000017500000000000014451777651020565 0ustar00ihabunekihabunek././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704226064.0 toot-0.41.1/tests/integration/conftest.py0000644000175000017500000000772114545066420020661 0ustar00ihabunekihabunek""" This module contains integration tests meant to run against a test Mastodon instance. You can set up a test instance locally by following this guide: https://docs.joinmastodon.org/dev/setup/ To enable integration tests, export the following environment variables to match your test server and database: ``` export TOOT_TEST_BASE_URL="localhost:3000" ``` """ import json import os import pytest import re import typing as t import uuid from click.testing import CliRunner, Result from pathlib import Path from toot import api, App, User from toot.cli import Context, TootObj def pytest_configure(config): import toot.settings toot.settings.DISABLE_SETTINGS = True # Type alias for run commands Run = t.Callable[..., Result] # Mastodon database name, used to confirm user registration without having to click the link TOOT_TEST_BASE_URL = os.getenv("TOOT_TEST_BASE_URL") # Toot logo used for testing image upload TRUMPET = str(Path(__file__).parent.parent.parent / "trumpet.png") ASSETS_DIR = str(Path(__file__).parent.parent / "assets") def create_app(base_url): instance = api.get_instance(base_url).json() response = api.create_app(base_url) return App(instance["uri"], base_url, response["client_id"], response["client_secret"]) def register_account(app: App): username = str(uuid.uuid4())[-10:] email = f"{username}@example.com" response = api.register_account(app, username, email, "password", "en") return User(app.instance, username, response["access_token"]) # ------------------------------------------------------------------------------ # Fixtures # ------------------------------------------------------------------------------ # Host name of a test instance to run integration tests against # DO NOT USE PUBLIC INSTANCES!!! @pytest.fixture(scope="session") def base_url(): if not TOOT_TEST_BASE_URL: pytest.skip("Skipping integration tests, TOOT_TEST_BASE_URL not set") return TOOT_TEST_BASE_URL @pytest.fixture(scope="session") def app(base_url): return create_app(base_url) @pytest.fixture(scope="session") def user(app): return register_account(app) @pytest.fixture(scope="session") def friend(app): return register_account(app) @pytest.fixture(scope="session") def user_id(app, user): return api.find_account(app, user, user.username)["id"] @pytest.fixture(scope="session") def friend_id(app, user, friend): return api.find_account(app, user, friend.username)["id"] @pytest.fixture(scope="session", autouse=True) def testing_env(): os.environ["TOOT_TESTING"] = "true" @pytest.fixture(scope="session") def runner(): return CliRunner(mix_stderr=False) @pytest.fixture def run(app, user, runner): def _run(command, *params, input=None) -> Result: obj = TootObj(test_ctx=Context(app, user)) return runner.invoke(command, params, obj=obj, input=input) return _run @pytest.fixture def run_as(app, runner): def _run_as(user, command, *params, input=None) -> Result: obj = TootObj(test_ctx=Context(app, user)) return runner.invoke(command, params, obj=obj, input=input) return _run_as @pytest.fixture def run_json(app, user, runner): def _run_json(command, *params): obj = TootObj(test_ctx=Context(app, user)) result = runner.invoke(command, params, obj=obj) assert result.exit_code == 0 return json.loads(result.stdout) return _run_json @pytest.fixture def run_anon(runner): def _run(command, *params) -> Result: obj = TootObj(test_ctx=Context(None, None)) return runner.invoke(command, params, obj=obj) return _run # ------------------------------------------------------------------------------ # Utils # ------------------------------------------------------------------------------ def posted_status_id(out): pattern = re.compile(r"Toot posted: http://([^/]+)/([^/]+)/(.+)") match = re.search(pattern, out) assert match _, _, status_id = match.groups() return status_id ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/tests/integration/test_accounts.py0000644000175000017500000002107214543235771021712 0ustar00ihabunekihabunekimport json from toot import App, User, api, cli from toot.entities import Account, Relationship, from_dict def test_whoami(user: User, run): result = run(cli.read.whoami) assert result.exit_code == 0 # TODO: test other fields once updating account is supported out = result.stdout.strip() assert f"@{user.username}" in out def test_whoami_json(user: User, run): result = run(cli.read.whoami, "--json") assert result.exit_code == 0 account = from_dict(Account, json.loads(result.stdout)) assert account.username == user.username def test_whois(app: App, friend: User, run): variants = [ friend.username, f"@{friend.username}", f"{friend.username}@{app.instance}", f"@{friend.username}@{app.instance}", ] for username in variants: result = run(cli.read.whois, username) assert result.exit_code == 0 assert f"@{friend.username}" in result.stdout def test_following(app: App, user: User, friend: User, friend_id, run): # Make sure we're not initally following friend api.unfollow(app, user, friend_id) result = run(cli.accounts.following, user.username) assert result.exit_code == 0 assert result.stdout.strip() == "" result = run(cli.accounts.follow, friend.username) assert result.exit_code == 0 assert result.stdout.strip() == f"✓ You are now following {friend.username}" result = run(cli.accounts.following, user.username) assert result.exit_code == 0 assert friend.username in result.stdout.strip() # If no account is given defaults to logged in user result = run(cli.accounts.following) assert result.exit_code == 0 assert friend.username in result.stdout.strip() result = run(cli.accounts.unfollow, friend.username) assert result.exit_code == 0 assert result.stdout.strip() == f"✓ You are no longer following {friend.username}" result = run(cli.accounts.following, user.username) assert result.exit_code == 0 assert result.stdout.strip() == "" def test_following_case_insensitive(user: User, friend: User, run): assert friend.username != friend.username.upper() result = run(cli.accounts.follow, friend.username.upper()) assert result.exit_code == 0 out = result.stdout.strip() assert out == f"✓ You are now following {friend.username.upper()}" def test_following_not_found(run): result = run(cli.accounts.follow, "bananaman") assert result.exit_code == 1 assert result.stderr.strip() == "Error: Account not found" result = run(cli.accounts.unfollow, "bananaman") assert result.exit_code == 1 assert result.stderr.strip() == "Error: Account not found" def test_following_json(app: App, user: User, friend: User, user_id, friend_id, run_json): # Make sure we're not initally following friend api.unfollow(app, user, friend_id) result = run_json(cli.accounts.following, user.username, "--json") assert result == [] result = run_json(cli.accounts.followers, friend.username, "--json") assert result == [] result = run_json(cli.accounts.follow, friend.username, "--json") relationship = from_dict(Relationship, result) assert relationship.id == friend_id assert relationship.following is True [result] = run_json(cli.accounts.following, user.username, "--json") relationship = from_dict(Relationship, result) assert relationship.id == friend_id # If no account is given defaults to logged in user [result] = run_json(cli.accounts.following, user.username, "--json") relationship = from_dict(Relationship, result) assert relationship.id == friend_id [result] = run_json(cli.accounts.followers, friend.username, "--json") assert result["id"] == user_id result = run_json(cli.accounts.unfollow, friend.username, "--json") assert result["id"] == friend_id assert result["following"] is False result = run_json(cli.accounts.following, user.username, "--json") assert result == [] result = run_json(cli.accounts.followers, friend.username, "--json") assert result == [] def test_mute(app, user, friend, friend_id, run): # Make sure we're not initially muting friend api.unmute(app, user, friend_id) result = run(cli.accounts.muted) assert result.exit_code == 0 out = result.stdout.strip() assert out == "No accounts muted" result = run(cli.accounts.mute, friend.username) assert result.exit_code == 0 out = result.stdout.strip() assert out == f"✓ You have muted {friend.username}" result = run(cli.accounts.muted) assert result.exit_code == 0 out = result.stdout.strip() assert friend.username in out result = run(cli.accounts.unmute, friend.username) assert result.exit_code == 0 out = result.stdout.strip() assert out == f"✓ {friend.username} is no longer muted" result = run(cli.accounts.muted) assert result.exit_code == 0 out = result.stdout.strip() assert out == "No accounts muted" def test_mute_case_insensitive(friend: User, run): result = run(cli.accounts.mute, friend.username.upper()) assert result.exit_code == 0 out = result.stdout.strip() assert out == f"✓ You have muted {friend.username.upper()}" def test_mute_not_found(run): result = run(cli.accounts.mute, "doesnotexistperson") assert result.exit_code == 1 assert result.stderr.strip() == "Error: Account not found" result = run(cli.accounts.unmute, "doesnotexistperson") assert result.exit_code == 1 assert result.stderr.strip() == "Error: Account not found" def test_mute_json(app: App, user: User, friend: User, run_json, friend_id): # Make sure we're not initially muting friend api.unmute(app, user, friend_id) result = run_json(cli.accounts.muted, "--json") assert result == [] result = run_json(cli.accounts.mute, friend.username, "--json") relationship = from_dict(Relationship, result) assert relationship.id == friend_id assert relationship.muting is True [result] = run_json(cli.accounts.muted, "--json") account = from_dict(Account, result) assert account.id == friend_id result = run_json(cli.accounts.unmute, friend.username, "--json") relationship = from_dict(Relationship, result) assert relationship.id == friend_id assert relationship.muting is False result = run_json(cli.accounts.muted, "--json") assert result == [] def test_block(app, user, friend, friend_id, run): # Make sure we're not initially blocking friend api.unblock(app, user, friend_id) result = run(cli.accounts.blocked) assert result.exit_code == 0 out = result.stdout.strip() assert out == "No accounts blocked" result = run(cli.accounts.block, friend.username) assert result.exit_code == 0 out = result.stdout.strip() assert out == f"✓ You are now blocking {friend.username}" result = run(cli.accounts.blocked) assert result.exit_code == 0 out = result.stdout.strip() assert friend.username in out result = run(cli.accounts.unblock, friend.username) assert result.exit_code == 0 out = result.stdout.strip() assert out == f"✓ {friend.username} is no longer blocked" result = run(cli.accounts.blocked) assert result.exit_code == 0 out = result.stdout.strip() assert out == "No accounts blocked" def test_block_case_insensitive(friend: User, run): result = run(cli.accounts.block, friend.username.upper()) assert result.exit_code == 0 out = result.stdout.strip() assert out == f"✓ You are now blocking {friend.username.upper()}" def test_block_not_found(run): result = run(cli.accounts.block, "doesnotexistperson") assert result.exit_code == 1 assert result.stderr.strip() == "Error: Account not found" def test_block_json(app: App, user: User, friend: User, run_json, friend_id): # Make sure we're not initially blocking friend api.unblock(app, user, friend_id) result = run_json(cli.accounts.blocked, "--json") assert result == [] result = run_json(cli.accounts.block, friend.username, "--json") relationship = from_dict(Relationship, result) assert relationship.id == friend_id assert relationship.blocking is True [result] = run_json(cli.accounts.blocked, "--json") account = from_dict(Account, result) assert account.id == friend_id result = run_json(cli.accounts.unblock, friend.username, "--json") relationship = from_dict(Relationship, result) assert relationship.id == friend_id assert relationship.blocking is False result = run_json(cli.accounts.blocked, "--json") assert result == [] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704107353.0 toot-0.41.1/tests/integration/test_auth.py0000644000175000017500000001504714544516531021036 0ustar00ihabunekihabunekfrom typing import Any, Dict from unittest import mock from unittest.mock import MagicMock from toot import User, cli from tests.integration.conftest import Run # TODO: figure out how to test login EMPTY_CONFIG: Dict[Any, Any] = { "apps": {}, "users": {}, "active_user": None } SAMPLE_CONFIG = { "active_user": "frank@foo.social", "apps": { "foo.social": { "base_url": "http://foo.social", "client_id": "123", "client_secret": "123", "instance": "foo.social" }, "bar.social": { "base_url": "http://bar.social", "client_id": "123", "client_secret": "123", "instance": "bar.social" }, }, "users": { "frank@foo.social": { "access_token": "123", "instance": "foo.social", "username": "frank" }, "frank@bar.social": { "access_token": "123", "instance": "bar.social", "username": "frank" }, } } def test_env(run: Run): result = run(cli.auth.env) assert result.exit_code == 0 assert "toot" in result.stdout assert "Python" in result.stdout @mock.patch("toot.config.load_config") def test_auth_empty(load_config: MagicMock, run: Run): load_config.return_value = EMPTY_CONFIG result = run(cli.auth.auth) assert result.exit_code == 0 assert result.stdout.strip() == "You are not logged in to any accounts" @mock.patch("toot.config.load_config") def test_auth_full(load_config: MagicMock, run: Run): load_config.return_value = SAMPLE_CONFIG result = run(cli.auth.auth) assert result.exit_code == 0 assert result.stdout.strip().startswith("Authenticated accounts:") assert "frank@foo.social" in result.stdout assert "frank@bar.social" in result.stdout # Saving config is mocked so we don't mess up our local config # TODO: could this be implemented using an auto-use fixture so we have it always # mocked? @mock.patch("toot.config.load_app") @mock.patch("toot.config.save_app") @mock.patch("toot.config.save_user") def test_login_cli( save_user: MagicMock, save_app: MagicMock, load_app: MagicMock, user: User, run: Run, ): load_app.return_value = None result = run( cli.auth.login_cli, "--instance", "http://localhost:3000", "--email", f"{user.username}@example.com", "--password", "password", ) assert result.exit_code == 0 assert "✓ Successfully logged in." in result.stdout save_app.assert_called_once() (app,) = save_app.call_args.args assert app.instance == "localhost:3000" assert app.base_url == "http://localhost:3000" assert app.client_id assert app.client_secret save_user.assert_called_once() (new_user,) = save_user.call_args.args assert new_user.instance == "localhost:3000" assert new_user.username == user.username # access token will be different since this is a new login assert new_user.access_token and new_user.access_token != user.access_token assert save_user.call_args.kwargs == {"activate": True} @mock.patch("toot.config.load_app") @mock.patch("toot.config.save_app") @mock.patch("toot.config.save_user") def test_login_cli_wrong_password( save_user: MagicMock, save_app: MagicMock, load_app: MagicMock, user: User, run: Run, ): load_app.return_value = None result = run( cli.auth.login_cli, "--instance", "http://localhost:3000", "--email", f"{user.username}@example.com", "--password", "wrong password", ) assert result.exit_code == 1 assert result.stderr.strip() == "Error: Login failed" save_app.assert_called_once() (app,) = save_app.call_args.args assert app.instance == "localhost:3000" assert app.base_url == "http://localhost:3000" assert app.client_id assert app.client_secret save_user.assert_not_called() @mock.patch("toot.config.load_config") @mock.patch("toot.config.delete_user") def test_logout(delete_user: MagicMock, load_config: MagicMock, run: Run): load_config.return_value = SAMPLE_CONFIG result = run(cli.auth.logout, "frank@foo.social") assert result.exit_code == 0 assert result.stdout.strip() == "✓ Account frank@foo.social logged out" delete_user.assert_called_once_with(User("foo.social", "frank", "123")) @mock.patch("toot.config.load_config") def test_logout_not_logged_in(load_config: MagicMock, run: Run): load_config.return_value = EMPTY_CONFIG result = run(cli.auth.logout) assert result.exit_code == 1 assert result.stderr.strip() == "Error: You're not logged into any accounts" @mock.patch("toot.config.load_config") def test_logout_account_not_specified(load_config: MagicMock, run: Run): load_config.return_value = SAMPLE_CONFIG result = run(cli.auth.logout) assert result.exit_code == 1 assert result.stderr.startswith("Error: Specify account to log out") @mock.patch("toot.config.load_config") def test_logout_account_does_not_exist(load_config: MagicMock, run: Run): load_config.return_value = SAMPLE_CONFIG result = run(cli.auth.logout, "banana") assert result.exit_code == 1 assert result.stderr.startswith("Error: Account not found") @mock.patch("toot.config.load_config") @mock.patch("toot.config.activate_user") def test_activate(activate_user: MagicMock, load_config: MagicMock, run: Run): load_config.return_value = SAMPLE_CONFIG result = run(cli.auth.activate, "frank@foo.social") assert result.exit_code == 0 assert result.stdout.strip() == "✓ Account frank@foo.social activated" activate_user.assert_called_once_with(User("foo.social", "frank", "123")) @mock.patch("toot.config.load_config") def test_activate_not_logged_in(load_config: MagicMock, run: Run): load_config.return_value = EMPTY_CONFIG result = run(cli.auth.activate) assert result.exit_code == 1 assert result.stderr.strip() == "Error: You're not logged into any accounts" @mock.patch("toot.config.load_config") def test_activate_account_not_given(load_config: MagicMock, run: Run): load_config.return_value = SAMPLE_CONFIG result = run(cli.auth.activate) assert result.exit_code == 1 assert result.stderr.startswith("Error: Specify account to activate") @mock.patch("toot.config.load_config") def test_activate_invalid_Account(load_config: MagicMock, run: Run): load_config.return_value = SAMPLE_CONFIG result = run(cli.auth.activate, "banana") assert result.exit_code == 1 assert result.stderr.startswith("Error: Account not found") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/tests/integration/test_lists.py0000644000175000017500000001220614543235771021230 0ustar00ihabunekihabunekfrom uuid import uuid4 from toot import cli from tests.integration.conftest import register_account def test_lists_empty(run): result = run(cli.lists.list) assert result.exit_code == 0 assert result.stdout.strip() == "You have no lists defined." def test_lists_empty_json(run_json): lists = run_json(cli.lists.list, "--json") assert lists == [] def test_list_create_delete(run): result = run(cli.lists.create, "banana") assert result.exit_code == 0 assert result.stdout.strip() == '✓ List "banana" created.' result = run(cli.lists.list) assert result.exit_code == 0 assert "banana" in result.stdout result = run(cli.lists.create, "mango") assert result.exit_code == 0 assert result.stdout.strip() == '✓ List "mango" created.' result = run(cli.lists.list) assert result.exit_code == 0 assert "banana" in result.stdout assert "mango" in result.stdout result = run(cli.lists.delete, "banana") assert result.exit_code == 0 assert result.stdout.strip() == '✓ List "banana" deleted.' result = run(cli.lists.list) assert result.exit_code == 0 assert "banana" not in result.stdout assert "mango" in result.stdout result = run(cli.lists.delete, "mango") assert result.exit_code == 0 assert result.stdout.strip() == '✓ List "mango" deleted.' result = run(cli.lists.list) assert result.exit_code == 0 assert result.stdout.strip() == "You have no lists defined." result = run(cli.lists.delete, "mango") assert result.exit_code == 1 assert result.stderr.strip() == "Error: List not found" def test_list_create_delete_json(run, run_json): result = run_json(cli.lists.list, "--json") assert result == [] list = run_json(cli.lists.create, "banana", "--json") assert list["title"] == "banana" [list] = run_json(cli.lists.list, "--json") assert list["title"] == "banana" list = run_json(cli.lists.create, "mango", "--json") assert list["title"] == "mango" lists = run_json(cli.lists.list, "--json") [list1, list2] = sorted(lists, key=lambda l: l["title"]) assert list1["title"] == "banana" assert list2["title"] == "mango" result = run_json(cli.lists.delete, "banana", "--json") assert result == {} [list] = run_json(cli.lists.list, "--json") assert list["title"] == "mango" result = run_json(cli.lists.delete, "mango", "--json") assert result == {} result = run_json(cli.lists.list, "--json") assert result == [] result = run(cli.lists.delete, "mango", "--json") assert result.exit_code == 1 assert result.stderr.strip() == "Error: List not found" def test_list_add_remove(run, app): list_name = str(uuid4()) acc = register_account(app) run(cli.lists.create, list_name) result = run(cli.lists.add, list_name, acc.username) assert result.exit_code == 1 assert result.stderr.strip() == f"Error: You must follow @{acc.username} before adding this account to a list." run(cli.accounts.follow, acc.username) result = run(cli.lists.add, list_name, acc.username) assert result.exit_code == 0 assert result.stdout.strip() == f'✓ Added account "{acc.username}"' result = run(cli.lists.accounts, list_name) assert result.exit_code == 0 assert acc.username in result.stdout # Account doesn't exist result = run(cli.lists.add, list_name, "does_not_exist") assert result.exit_code == 1 assert result.stderr.strip() == "Error: Account not found" # List doesn't exist result = run(cli.lists.add, "does_not_exist", acc.username) assert result.exit_code == 1 assert result.stderr.strip() == "Error: List not found" result = run(cli.lists.remove, list_name, acc.username) assert result.exit_code == 0 assert result.stdout.strip() == f'✓ Removed account "{acc.username}"' result = run(cli.lists.accounts, list_name) assert result.exit_code == 0 assert result.stdout.strip() == "This list has no accounts." def test_list_add_remove_json(run, run_json, app): list_name = str(uuid4()) acc = register_account(app) run(cli.lists.create, list_name) result = run(cli.lists.add, list_name, acc.username, "--json") assert result.exit_code == 1 assert result.stderr.strip() == f"Error: You must follow @{acc.username} before adding this account to a list." run(cli.accounts.follow, acc.username) result = run_json(cli.lists.add, list_name, acc.username, "--json") assert result == {} [account] = run_json(cli.lists.accounts, list_name, "--json") assert account["username"] == acc.username # Account doesn't exist result = run(cli.lists.add, list_name, "does_not_exist", "--json") assert result.exit_code == 1 assert result.stderr.strip() == "Error: Account not found" # List doesn't exist result = run(cli.lists.add, "does_not_exist", acc.username, "--json") assert result.exit_code == 1 assert result.stderr.strip() == "Error: List not found" result = run_json(cli.lists.remove, list_name, acc.username, "--json") assert result == {} result = run_json(cli.lists.accounts, list_name, "--json") assert result == [] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/tests/integration/test_post.py0000644000175000017500000002662214543235771021066 0ustar00ihabunekihabunekimport json import re import uuid from datetime import datetime, timedelta, timezone from os import path from tests.integration.conftest import ASSETS_DIR, posted_status_id from toot import CLIENT_NAME, CLIENT_WEBSITE, api, cli from toot.utils import get_text from unittest import mock def test_post(app, user, run): text = "i wish i was a #lumberjack" result = run(cli.post.post, text) assert result.exit_code == 0 status_id = posted_status_id(result.stdout) status = api.fetch_status(app, user, status_id).json() assert text == get_text(status["content"]) assert status["visibility"] == "public" assert status["sensitive"] is False assert status["spoiler_text"] == "" assert status["poll"] is None # Pleroma doesn't return the application if status["application"]: assert status["application"]["name"] == CLIENT_NAME assert status["application"]["website"] == CLIENT_WEBSITE def test_post_no_text(run): result = run(cli.post.post) assert result.exit_code == 1 assert result.stderr.strip() == "Error: You must specify either text or media to post." def test_post_json(run): content = "i wish i was a #lumberjack" result = run(cli.post.post, content, "--json") assert result.exit_code == 0 status = json.loads(result.stdout) assert get_text(status["content"]) == content assert status["visibility"] == "public" assert status["sensitive"] is False assert status["spoiler_text"] == "" assert status["poll"] is None def test_post_visibility(app, user, run): for visibility in ["public", "unlisted", "private", "direct"]: result = run(cli.post.post, "foo", "--visibility", visibility) assert result.exit_code == 0 status_id = posted_status_id(result.stdout) status = api.fetch_status(app, user, status_id).json() assert status["visibility"] == visibility def test_post_scheduled_at(app, user, run): text = str(uuid.uuid4()) scheduled_at = datetime.now(timezone.utc).replace(microsecond=0) + timedelta(minutes=10) result = run(cli.post.post, text, "--scheduled-at", scheduled_at.isoformat()) assert result.exit_code == 0 assert "Toot scheduled for" in result.stdout statuses = api.scheduled_statuses(app, user) [status] = [s for s in statuses if s["params"]["text"] == text] assert datetime.strptime(status["scheduled_at"], "%Y-%m-%dT%H:%M:%S.%f%z") == scheduled_at def test_post_scheduled_at_error(run): result = run(cli.post.post, "foo", "--scheduled-at", "banana") assert result.exit_code == 1 # Stupid error returned by mastodon assert result.stderr.strip() == "Error: Record invalid" def test_post_scheduled_in(app, user, run): text = str(uuid.uuid4()) variants = [ ("1 day", timedelta(days=1)), ("1 day 6 hours", timedelta(days=1, hours=6)), ("1 day 6 hours 13 minutes", timedelta(days=1, hours=6, minutes=13)), ("1 day 6 hours 13 minutes 51 second", timedelta(days=1, hours=6, minutes=13, seconds=51)), ("2d", timedelta(days=2)), ("2d6h", timedelta(days=2, hours=6)), ("2d6h13m", timedelta(days=2, hours=6, minutes=13)), ("2d6h13m51s", timedelta(days=2, hours=6, minutes=13, seconds=51)), ] datetimes = [] for scheduled_in, delta in variants: result = run(cli.post.post, text, "--scheduled-in", scheduled_in) assert result.exit_code == 0 dttm = datetime.utcnow() + delta assert result.stdout.startswith(f"Toot scheduled for: {str(dttm)[:16]}") datetimes.append(dttm) scheduled = api.scheduled_statuses(app, user) scheduled = [s for s in scheduled if s["params"]["text"] == text] scheduled = sorted(scheduled, key=lambda s: s["scheduled_at"]) assert len(scheduled) == 8 for expected, status in zip(datetimes, scheduled): actual = datetime.strptime(status["scheduled_at"], "%Y-%m-%dT%H:%M:%S.%fZ") delta = expected - actual assert delta.total_seconds() < 5 def test_post_scheduled_in_invalid_duration(run): result = run(cli.post.post, "foo", "--scheduled-in", "banana") assert result.exit_code == 2 assert "Invalid duration: banana" in result.stderr def test_post_scheduled_in_empty_duration(run): result = run(cli.post.post, "foo", "--scheduled-in", "0m") assert result.exit_code == 2 assert "Empty duration" in result.stderr def test_post_poll(app, user, run): text = str(uuid.uuid4()) result = run( cli.post.post, text, "--poll-option", "foo", "--poll-option", "bar", "--poll-option", "baz", "--poll-option", "qux", ) assert result.exit_code == 0 status_id = posted_status_id(result.stdout) status = api.fetch_status(app, user, status_id).json() assert status["poll"]["expired"] is False assert status["poll"]["multiple"] is False assert status["poll"]["options"] == [ {"title": "foo", "votes_count": 0}, {"title": "bar", "votes_count": 0}, {"title": "baz", "votes_count": 0}, {"title": "qux", "votes_count": 0} ] # Test expires_at is 24h by default actual = datetime.strptime(status["poll"]["expires_at"], "%Y-%m-%dT%H:%M:%S.%f%z") expected = datetime.now(timezone.utc) + timedelta(days=1) delta = actual - expected assert delta.total_seconds() < 5 def test_post_poll_multiple(app, user, run): text = str(uuid.uuid4()) result = run( cli.post.post, text, "--poll-option", "foo", "--poll-option", "bar", "--poll-multiple" ) assert result.exit_code == 0 status_id = posted_status_id(result.stdout) status = api.fetch_status(app, user, status_id).json() assert status["poll"]["multiple"] is True def test_post_poll_expires_in(app, user, run): text = str(uuid.uuid4()) result = run( cli.post.post, text, "--poll-option", "foo", "--poll-option", "bar", "--poll-expires-in", "8h", ) assert result.exit_code == 0 status_id = posted_status_id(result.stdout) status = api.fetch_status(app, user, status_id).json() actual = datetime.strptime(status["poll"]["expires_at"], "%Y-%m-%dT%H:%M:%S.%f%z") expected = datetime.now(timezone.utc) + timedelta(hours=8) delta = actual - expected assert delta.total_seconds() < 5 def test_post_poll_hide_totals(app, user, run): text = str(uuid.uuid4()) result = run( cli.post.post, text, "--poll-option", "foo", "--poll-option", "bar", "--poll-hide-totals" ) assert result.exit_code == 0 status_id = posted_status_id(result.stdout) status = api.fetch_status(app, user, status_id).json() # votes_count is None when totals are hidden assert status["poll"]["options"] == [ {"title": "foo", "votes_count": None}, {"title": "bar", "votes_count": None}, ] def test_post_language(app, user, run): result = run(cli.post.post, "test", "--language", "hr") assert result.exit_code == 0 status_id = posted_status_id(result.stdout) status = api.fetch_status(app, user, status_id).json() assert status["language"] == "hr" result = run(cli.post.post, "test", "--language", "zh") assert result.exit_code == 0 status_id = posted_status_id(result.stdout) status = api.fetch_status(app, user, status_id).json() assert status["language"] == "zh" def test_post_language_error(run): result = run(cli.post.post, "test", "--language", "banana") assert result.exit_code == 2 assert "Language should be a two letter abbreviation." in result.stderr def test_media_thumbnail(app, user, run): video_path = path.join(ASSETS_DIR, "small.webm") thumbnail_path = path.join(ASSETS_DIR, "test1.png") result = run( cli.post.post, "--media", video_path, "--thumbnail", thumbnail_path, "--description", "foo", "some text" ) assert result.exit_code == 0 status_id = posted_status_id(result.stdout) status = api.fetch_status(app, user, status_id).json() [media] = status["media_attachments"] assert media["description"] == "foo" assert media["type"] == "video" assert media["url"].endswith(".mp4") assert media["preview_url"].endswith(".png") # Video properties assert int(media["meta"]["original"]["duration"]) == 5 assert media["meta"]["original"]["height"] == 320 assert media["meta"]["original"]["width"] == 560 # Thumbnail properties assert media["meta"]["small"]["height"] == 50 assert media["meta"]["small"]["width"] == 50 def test_media_attachments(app, user, run): path1 = path.join(ASSETS_DIR, "test1.png") path2 = path.join(ASSETS_DIR, "test2.png") path3 = path.join(ASSETS_DIR, "test3.png") path4 = path.join(ASSETS_DIR, "test4.png") result = run( cli.post.post, "--media", path1, "--media", path2, "--media", path3, "--media", path4, "--description", "Test 1", "--description", "Test 2", "--description", "Test 3", "--description", "Test 4", "some text" ) assert result.exit_code == 0 status_id = posted_status_id(result.stdout) status = api.fetch_status(app, user, status_id).json() [a1, a2, a3, a4] = status["media_attachments"] # Pleroma doesn't send metadata if "meta" in a1: assert a1["meta"]["original"]["size"] == "50x50" assert a2["meta"]["original"]["size"] == "50x60" assert a3["meta"]["original"]["size"] == "50x70" assert a4["meta"]["original"]["size"] == "50x80" assert a1["description"] == "Test 1" assert a2["description"] == "Test 2" assert a3["description"] == "Test 3" assert a4["description"] == "Test 4" def test_too_many_media(run): m = path.join(ASSETS_DIR, "test1.png") result = run(cli.post.post, "-m", m, "-m", m, "-m", m, "-m", m, "-m", m) assert result.exit_code == 1 assert result.stderr.strip() == "Error: Cannot attach more than 4 files." @mock.patch("toot.utils.multiline_input") @mock.patch("sys.stdin.read") def test_media_attachment_without_text(mock_read, mock_ml, app, user, run): # No status from stdin or readline mock_read.return_value = "" mock_ml.return_value = "" media_path = path.join(ASSETS_DIR, "test1.png") result = run(cli.post.post, "--media", media_path) assert result.exit_code == 0 status_id = posted_status_id(result.stdout) status = api.fetch_status(app, user, status_id).json() assert status["content"] == "" [attachment] = status["media_attachments"] assert not attachment["description"] # Pleroma doesn't send metadata if "meta" in attachment: assert attachment["meta"]["original"]["size"] == "50x50" def test_reply_thread(app, user, friend, run): status = api.post_status(app, friend, "This is the status").json() result = run(cli.post.post, "--reply-to", status["id"], "This is the reply") assert result.exit_code == 0 status_id = posted_status_id(result.stdout) reply = api.fetch_status(app, user, status_id).json() assert reply["in_reply_to_id"] == status["id"] result = run(cli.read.thread, status["id"]) assert result.exit_code == 0 [s1, s2] = [s.strip() for s in re.split(r"─+", result.stdout) if s.strip()] assert "This is the status" in s1 assert "This is the reply" in s2 assert friend.username in s1 assert user.username in s2 assert status["id"] in s1 assert reply["id"] in s2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/tests/integration/test_read.py0000644000175000017500000001353514543235771021013 0ustar00ihabunekihabunekimport json import re from tests.integration.conftest import TOOT_TEST_BASE_URL from toot import api, cli from toot.entities import Account, Status, from_dict, from_dict_list from uuid import uuid4 def test_instance_default(app, run): result = run(cli.read.instance) assert result.exit_code == 0 assert "Mastodon" in result.stdout assert app.instance in result.stdout assert "running Mastodon" in result.stdout def test_instance_with_url(app, run): result = run(cli.read.instance, TOOT_TEST_BASE_URL) assert result.exit_code == 0 assert "Mastodon" in result.stdout assert app.instance in result.stdout assert "running Mastodon" in result.stdout def test_instance_json(app, run): result = run(cli.read.instance, "--json") assert result.exit_code == 0 data = json.loads(result.stdout) assert data["title"] is not None assert data["description"] is not None assert data["version"] is not None def test_instance_anon(app, run_anon, base_url): result = run_anon(cli.read.instance, base_url) assert result.exit_code == 0 assert "Mastodon" in result.stdout assert app.instance in result.stdout assert "running Mastodon" in result.stdout # Need to specify the instance name when running anon result = run_anon(cli.read.instance) assert result.exit_code == 1 assert result.stderr.strip() == "Error: INSTANCE argument not given and not logged in" def test_whoami(user, run): result = run(cli.read.whoami) assert result.exit_code == 0 assert f"@{user.username}" in result.stdout def test_whoami_json(user, run): result = run(cli.read.whoami, "--json") assert result.exit_code == 0 data = json.loads(result.stdout) account = from_dict(Account, data) assert account.username == user.username assert account.acct == user.username def test_whois(app, friend, run): variants = [ friend.username, f"@{friend.username}", f"{friend.username}@{app.instance}", f"@{friend.username}@{app.instance}", ] for username in variants: result = run(cli.read.whois, username) assert result.exit_code == 0 assert f"@{friend.username}" in result.stdout def test_whois_json(app, friend, run): result = run(cli.read.whois, friend.username, "--json") assert result.exit_code == 0 data = json.loads(result.stdout) account = from_dict(Account, data) assert account.username == friend.username assert account.acct == friend.username def test_search_account(friend, run): result = run(cli.read.search, friend.username) assert result.exit_code == 0 assert result.stdout.strip() == f"Accounts:\n* @{friend.username}" def test_search_account_json(friend, run): result = run(cli.read.search, friend.username, "--json") assert result.exit_code == 0 data = json.loads(result.stdout) [account] = from_dict_list(Account, data["accounts"]) assert account.acct == friend.username def test_search_hashtag(app, user, run): api.post_status(app, user, "#hashtag_x") api.post_status(app, user, "#hashtag_y") api.post_status(app, user, "#hashtag_z") result = run(cli.read.search, "#hashtag") assert result.exit_code == 0 assert result.stdout.strip() == "Hashtags:\n#hashtag_x, #hashtag_y, #hashtag_z" def test_search_hashtag_json(app, user, run): api.post_status(app, user, "#hashtag_x") api.post_status(app, user, "#hashtag_y") api.post_status(app, user, "#hashtag_z") result = run(cli.read.search, "#hashtag", "--json") assert result.exit_code == 0 data = json.loads(result.stdout) [h1, h2, h3] = sorted(data["hashtags"], key=lambda h: h["name"]) assert h1["name"] == "hashtag_x" assert h2["name"] == "hashtag_y" assert h3["name"] == "hashtag_z" def test_status(app, user, run): uuid = str(uuid4()) status_id = api.post_status(app, user, uuid).json()["id"] result = run(cli.read.status, status_id) assert result.exit_code == 0 out = result.stdout.strip() assert uuid in out assert user.username in out assert status_id in out def test_status_json(app, user, run): uuid = str(uuid4()) status_id = api.post_status(app, user, uuid).json()["id"] result = run(cli.read.status, status_id, "--json") assert result.exit_code == 0 status = from_dict(Status, json.loads(result.stdout)) assert status.id == status_id assert status.account.acct == user.username assert uuid in status.content def test_thread(app, user, run): uuid1 = str(uuid4()) uuid2 = str(uuid4()) uuid3 = str(uuid4()) s1 = api.post_status(app, user, uuid1).json() s2 = api.post_status(app, user, uuid2, in_reply_to_id=s1["id"]).json() s3 = api.post_status(app, user, uuid3, in_reply_to_id=s2["id"]).json() for status in [s1, s2, s3]: result = run(cli.read.thread, status["id"]) assert result.exit_code == 0 bits = re.split(r"─+", result.stdout.strip()) bits = [b for b in bits if b] assert len(bits) == 3 assert s1["id"] in bits[0] assert s2["id"] in bits[1] assert s3["id"] in bits[2] assert uuid1 in bits[0] assert uuid2 in bits[1] assert uuid3 in bits[2] def test_thread_json(app, user, run): uuid1 = str(uuid4()) uuid2 = str(uuid4()) uuid3 = str(uuid4()) s1 = api.post_status(app, user, uuid1).json() s2 = api.post_status(app, user, uuid2, in_reply_to_id=s1["id"]).json() s3 = api.post_status(app, user, uuid3, in_reply_to_id=s2["id"]).json() result = run(cli.read.thread, s2["id"], "--json") assert result.exit_code == 0 result = json.loads(result.stdout) [ancestor] = [from_dict(Status, s) for s in result["ancestors"]] [descendent] = [from_dict(Status, s) for s in result["descendants"]] assert ancestor.id == s1["id"] assert descendent.id == s3["id"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704107622.0 toot-0.41.1/tests/integration/test_status.py0000644000175000017500000001374514544517146021426 0ustar00ihabunekihabunekimport json import pytest from tests.utils import run_with_retries from toot import api, cli from toot.exceptions import NotFoundError def test_delete(app, user, run): status = api.post_status(app, user, "foo").json() result = run(cli.statuses.delete, status["id"]) assert result.exit_code == 0 assert result.stdout.strip() == "✓ Status deleted" with pytest.raises(NotFoundError): api.fetch_status(app, user, status["id"]) def test_delete_json(app, user, run): status = api.post_status(app, user, "foo").json() result = run(cli.statuses.delete, status["id"], "--json") assert result.exit_code == 0 out = result.stdout result = json.loads(out) assert result["id"] == status["id"] with pytest.raises(NotFoundError): api.fetch_status(app, user, status["id"]) def test_favourite(app, user, run): status = api.post_status(app, user, "foo").json() assert not status["favourited"] result = run(cli.statuses.favourite, status["id"]) assert result.exit_code == 0 assert result.stdout.strip() == "✓ Status favourited" status = api.fetch_status(app, user, status["id"]).json() assert status["favourited"] result = run(cli.statuses.unfavourite, status["id"]) assert result.exit_code == 0 assert result.stdout.strip() == "✓ Status unfavourited" def test_favourited(): nonlocal status status = api.fetch_status(app, user, status["id"]).json() assert not status["favourited"] run_with_retries(test_favourited) def test_favourite_json(app, user, run): status = api.post_status(app, user, "foo").json() assert not status["favourited"] result = run(cli.statuses.favourite, status["id"], "--json") assert result.exit_code == 0 result = json.loads(result.stdout) assert result["id"] == status["id"] assert result["favourited"] is True result = run(cli.statuses.unfavourite, status["id"], "--json") assert result.exit_code == 0 result = json.loads(result.stdout) assert result["id"] == status["id"] assert result["favourited"] is False def test_reblog(app, user, run): status = api.post_status(app, user, "foo").json() assert not status["reblogged"] result = run(cli.statuses.reblogged_by, status["id"]) assert result.exit_code == 0 assert result.stdout.strip() == "This status is not reblogged by anyone" result = run(cli.statuses.reblog, status["id"]) assert result.exit_code == 0 assert result.stdout.strip() == "✓ Status reblogged" status = api.fetch_status(app, user, status["id"]).json() assert status["reblogged"] result = run(cli.statuses.reblogged_by, status["id"]) assert result.exit_code == 0 assert user.username in result.stdout result = run(cli.statuses.unreblog, status["id"]) assert result.exit_code == 0 assert result.stdout.strip() == "✓ Status unreblogged" status = api.fetch_status(app, user, status["id"]).json() assert not status["reblogged"] def test_reblog_json(app, user, run): status = api.post_status(app, user, "foo").json() assert not status["reblogged"] result = run(cli.statuses.reblog, status["id"], "--json") assert result.exit_code == 0 result = json.loads(result.stdout) assert result["reblogged"] is True assert result["reblog"]["id"] == status["id"] result = run(cli.statuses.reblogged_by, status["id"], "--json") assert result.exit_code == 0 [reblog] = json.loads(result.stdout) assert reblog["acct"] == user.username result = run(cli.statuses.unreblog, status["id"], "--json") assert result.exit_code == 0 result = json.loads(result.stdout) assert result["reblogged"] is False assert result["reblog"] is None def test_pin(app, user, run): status = api.post_status(app, user, "foo").json() assert not status["pinned"] result = run(cli.statuses.pin, status["id"]) assert result.exit_code == 0 assert result.stdout.strip() == "✓ Status pinned" status = api.fetch_status(app, user, status["id"]).json() assert status["pinned"] result = run(cli.statuses.unpin, status["id"]) assert result.exit_code == 0 assert result.stdout.strip() == "✓ Status unpinned" status = api.fetch_status(app, user, status["id"]).json() assert not status["pinned"] def test_pin_json(app, user, run): status = api.post_status(app, user, "foo").json() assert not status["pinned"] result = run(cli.statuses.pin, status["id"], "--json") assert result.exit_code == 0 result = json.loads(result.stdout) assert result["pinned"] is True assert result["id"] == status["id"] result = run(cli.statuses.unpin, status["id"], "--json") assert result.exit_code == 0 result = json.loads(result.stdout) assert result["pinned"] is False assert result["id"] == status["id"] def test_bookmark(app, user, run): status = api.post_status(app, user, "foo").json() assert not status["bookmarked"] result = run(cli.statuses.bookmark, status["id"]) assert result.exit_code == 0 assert result.stdout.strip() == "✓ Status bookmarked" status = api.fetch_status(app, user, status["id"]).json() assert status["bookmarked"] result = run(cli.statuses.unbookmark, status["id"]) assert result.exit_code == 0 assert result.stdout.strip() == "✓ Status unbookmarked" status = api.fetch_status(app, user, status["id"]).json() assert not status["bookmarked"] def test_bookmark_json(app, user, run): status = api.post_status(app, user, "foo").json() assert not status["bookmarked"] result = run(cli.statuses.bookmark, status["id"], "--json") assert result.exit_code == 0 result = json.loads(result.stdout) assert result["id"] == status["id"] assert result["bookmarked"] is True result = run(cli.statuses.unbookmark, status["id"], "--json") assert result.exit_code == 0 result = json.loads(result.stdout) assert result["id"] == status["id"] assert result["bookmarked"] is False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/tests/integration/test_tags.py0000644000175000017500000001232614543235771021033 0ustar00ihabunekihabunekimport re from typing import List from toot import api, cli from toot.entities import FeaturedTag, Tag, from_dict, from_dict_list def test_tags(run): result = run(cli.tags.tags, "followed") assert result.exit_code == 0 assert result.stdout.strip() == "You're not following any hashtags" result = run(cli.tags.tags, "follow", "foo") assert result.exit_code == 0 assert result.stdout.strip() == "✓ You are now following #foo" result = run(cli.tags.tags, "followed") assert result.exit_code == 0 assert _find_tags(result.stdout) == ["#foo"] result = run(cli.tags.tags, "follow", "bar") assert result.exit_code == 0 assert result.stdout.strip() == "✓ You are now following #bar" result = run(cli.tags.tags, "followed") assert result.exit_code == 0 assert _find_tags(result.stdout) == ["#bar", "#foo"] result = run(cli.tags.tags, "unfollow", "foo") assert result.exit_code == 0 assert result.stdout.strip() == "✓ You are no longer following #foo" result = run(cli.tags.tags, "followed") assert result.exit_code == 0 assert _find_tags(result.stdout) == ["#bar"] result = run(cli.tags.tags, "unfollow", "bar") assert result.exit_code == 0 assert result.stdout.strip() == "✓ You are no longer following #bar" result = run(cli.tags.tags, "followed") assert result.exit_code == 0 assert result.stdout.strip() == "You're not following any hashtags" def test_tags_json(run_json): result = run_json(cli.tags.tags, "followed", "--json") assert result == [] result = run_json(cli.tags.tags, "follow", "foo", "--json") tag = from_dict(Tag, result) assert tag.name == "foo" assert tag.following is True result = run_json(cli.tags.tags, "followed", "--json") [tag] = from_dict_list(Tag, result) assert tag.name == "foo" assert tag.following is True result = run_json(cli.tags.tags, "follow", "bar", "--json") tag = from_dict(Tag, result) assert tag.name == "bar" assert tag.following is True result = run_json(cli.tags.tags, "followed", "--json") tags = from_dict_list(Tag, result) [bar, foo] = sorted(tags, key=lambda t: t.name) assert foo.name == "foo" assert foo.following is True assert bar.name == "bar" assert bar.following is True result = run_json(cli.tags.tags, "unfollow", "foo", "--json") tag = from_dict(Tag, result) assert tag.name == "foo" assert tag.following is False result = run_json(cli.tags.tags, "unfollow", "bar", "--json") tag = from_dict(Tag, result) assert tag.name == "bar" assert tag.following is False result = run_json(cli.tags.tags, "followed", "--json") assert result == [] def test_tags_featured(run, app, user): result = run(cli.tags.tags, "featured") assert result.exit_code == 0 assert result.stdout.strip() == "You don't have any featured hashtags" result = run(cli.tags.tags, "feature", "foo") assert result.exit_code == 0 assert result.stdout.strip() == "✓ Tag #foo is now featured" result = run(cli.tags.tags, "featured") assert result.exit_code == 0 assert _find_tags(result.stdout) == ["#foo"] result = run(cli.tags.tags, "feature", "bar") assert result.exit_code == 0 assert result.stdout.strip() == "✓ Tag #bar is now featured" result = run(cli.tags.tags, "featured") assert result.exit_code == 0 assert _find_tags(result.stdout) == ["#bar", "#foo"] # Unfeature by Name result = run(cli.tags.tags, "unfeature", "foo") assert result.exit_code == 0 assert result.stdout.strip() == "✓ Tag #foo is no longer featured" result = run(cli.tags.tags, "featured") assert result.exit_code == 0 assert _find_tags(result.stdout) == ["#bar"] # Unfeature by ID tag = api.find_featured_tag(app, user, "bar") assert tag is not None result = run(cli.tags.tags, "unfeature", tag["id"]) assert result.exit_code == 0 assert result.stdout.strip() == "✓ Tag #bar is no longer featured" result = run(cli.tags.tags, "featured") assert result.exit_code == 0 assert result.stdout.strip() == "You don't have any featured hashtags" def test_tags_featured_json(run_json): result = run_json(cli.tags.tags, "featured", "--json") assert result == [] result = run_json(cli.tags.tags, "feature", "foo", "--json") tag = from_dict(FeaturedTag, result) assert tag.name == "foo" result = run_json(cli.tags.tags, "featured", "--json") [tag] = from_dict_list(FeaturedTag, result) assert tag.name == "foo" result = run_json(cli.tags.tags, "feature", "bar", "--json") tag = from_dict(FeaturedTag, result) assert tag.name == "bar" result = run_json(cli.tags.tags, "featured", "--json") tags = from_dict_list(FeaturedTag, result) [bar, foo] = sorted(tags, key=lambda t: t.name) assert foo.name == "foo" assert bar.name == "bar" result = run_json(cli.tags.tags, "unfeature", "foo", "--json") assert result == {} result = run_json(cli.tags.tags, "unfeature", "bar", "--json") assert result == {} result = run_json(cli.tags.tags, "featured", "--json") assert result == [] def _find_tags(txt: str) -> List[str]: return sorted(re.findall(r"#\w+", txt)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704107427.0 toot-0.41.1/tests/integration/test_timelines.py0000644000175000017500000001525114544516643022067 0ustar00ihabunekihabunekimport pytest from uuid import uuid4 from tests.utils import run_with_retries from toot import api, cli from toot.entities import from_dict, Status from tests.integration.conftest import TOOT_TEST_BASE_URL, register_account # TODO: If fixture is not overriden here, tests fail, not sure why, figure it out @pytest.fixture(scope="module") def user(app): return register_account(app) @pytest.fixture(scope="module") def other_user(app): return register_account(app) @pytest.fixture(scope="module") def friend_user(app, user): friend = register_account(app) friend_account = api.find_account(app, user, friend.username) api.follow(app, user, friend_account["id"]) return friend @pytest.fixture(scope="module") def friend_list(app, user, friend_user): friend_account = api.find_account(app, user, friend_user.username) list = api.create_list(app, user, str(uuid4())).json() api.add_accounts_to_list(app, user, list["id"], account_ids=[friend_account["id"]]) return list def test_timelines(app, user, other_user, friend_user, friend_list, run): status1 = _post_status(app, user, "#foo") status2 = _post_status(app, other_user, "#bar") status3 = _post_status(app, friend_user, "#foo #bar") # Home timeline def test_home(): result = run(cli.timelines.timeline) assert result.exit_code == 0 assert status1.id in result.stdout assert status2.id not in result.stdout assert status3.id in result.stdout run_with_retries(test_home) # Public timeline result = run(cli.timelines.timeline, "--public") assert result.exit_code == 0 assert status1.id in result.stdout assert status2.id in result.stdout assert status3.id in result.stdout # Anon public timeline result = run(cli.timelines.timeline, "--instance", TOOT_TEST_BASE_URL, "--public") assert result.exit_code == 0 assert status1.id in result.stdout assert status2.id in result.stdout assert status3.id in result.stdout # Tag timeline result = run(cli.timelines.timeline, "--tag", "foo") assert result.exit_code == 0 assert status1.id in result.stdout assert status2.id not in result.stdout assert status3.id in result.stdout result = run(cli.timelines.timeline, "--tag", "bar") assert result.exit_code == 0 assert status1.id not in result.stdout assert status2.id in result.stdout assert status3.id in result.stdout # Anon tag timeline result = run(cli.timelines.timeline, "--instance", TOOT_TEST_BASE_URL, "--tag", "foo") assert result.exit_code == 0 assert status1.id in result.stdout assert status2.id not in result.stdout assert status3.id in result.stdout # List timeline (by list name) result = run(cli.timelines.timeline, "--list", friend_list["title"]) assert result.exit_code == 0 assert status1.id not in result.stdout assert status2.id not in result.stdout assert status3.id in result.stdout # List timeline (by list ID) result = run(cli.timelines.timeline, "--list", friend_list["id"]) assert result.exit_code == 0 assert status1.id not in result.stdout assert status2.id not in result.stdout assert status3.id in result.stdout # Account timeline result = run(cli.timelines.timeline, "--account", friend_user.username) assert result.exit_code == 0 assert status1.id not in result.stdout assert status2.id not in result.stdout assert status3.id in result.stdout result = run(cli.timelines.timeline, "--account", other_user.username) assert result.exit_code == 0 assert status1.id not in result.stdout assert status2.id in result.stdout assert status3.id not in result.stdout def test_empty_timeline(app, run_as): user = register_account(app) result = run_as(user, cli.timelines.timeline) assert result.exit_code == 0 assert result.stdout.strip() == "─" * 80 def test_timeline_cant_combine_timelines(run): result = run(cli.timelines.timeline, "--tag", "foo", "--account", "bar") assert result.exit_code == 1 assert result.stderr.strip() == "Error: Only one of --public, --tag, --account, or --list can be used at one time." def test_timeline_local_needs_public_or_tag(run): result = run(cli.timelines.timeline, "--local") assert result.exit_code == 1 assert result.stderr.strip() == "Error: The --local option is only valid alongside --public or --tag." def test_timeline_instance_needs_public_or_tag(run): result = run(cli.timelines.timeline, "--instance", TOOT_TEST_BASE_URL) assert result.exit_code == 1 assert result.stderr.strip() == "Error: The --instance option is only valid alongside --public or --tag." def test_bookmarks(app, user, run): status1 = _post_status(app, user) status2 = _post_status(app, user) api.bookmark(app, user, status1.id) api.bookmark(app, user, status2.id) result = run(cli.timelines.bookmarks) assert result.exit_code == 0 assert status1.id in result.stdout assert status2.id in result.stdout assert result.stdout.find(status1.id) > result.stdout.find(status2.id) result = run(cli.timelines.bookmarks, "--reverse") assert result.exit_code == 0 assert status1.id in result.stdout assert status2.id in result.stdout assert result.stdout.find(status1.id) < result.stdout.find(status2.id) def test_notifications(app, user, other_user, run): result = run(cli.timelines.notifications) assert result.exit_code == 0 assert result.stdout.strip() == "You have no notifications" text = f"Paging doctor @{user.username}" status = _post_status(app, other_user, text) def test_notifications(): result = run(cli.timelines.notifications) assert result.exit_code == 0 assert f"@{other_user.username} mentioned you" in result.stdout assert status.id in result.stdout assert text in result.stdout run_with_retries(test_notifications) result = run(cli.timelines.notifications, "--mentions") assert result.exit_code == 0 assert f"@{other_user.username} mentioned you" in result.stdout assert status.id in result.stdout assert text in result.stdout def test_notifications_follow(app, user, friend_user, run_as): result = run_as(friend_user, cli.timelines.notifications) assert result.exit_code == 0 assert f"@{user.username} now follows you" in result.stdout result = run_as(friend_user, cli.timelines.notifications, "--mentions") assert result.exit_code == 0 assert "now follows you" not in result.stdout def _post_status(app, user, text=None) -> Status: text = text or str(uuid4()) response = api.post_status(app, user, text) return from_dict(Status, response.json()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/tests/integration/test_update_account.py0000644000175000017500000001177414543235771023101 0ustar00ihabunekihabunekfrom uuid import uuid4 from tests.integration.conftest import TRUMPET from toot import api, cli from toot.entities import Account, from_dict from toot.utils import get_text def test_update_account_no_options(run): result = run(cli.accounts.update_account) assert result.exit_code == 1 assert result.stderr.strip() == "Error: Please specify at least one option to update the account" def test_update_account_display_name(run, app, user): name = str(uuid4())[:10] result = run(cli.accounts.update_account, "--display-name", name) assert result.exit_code == 0 assert result.stdout.strip() == "✓ Account updated" account = api.verify_credentials(app, user).json() assert account["display_name"] == name def test_update_account_json(run_json, app, user): name = str(uuid4())[:10] out = run_json(cli.accounts.update_account, "--display-name", name, "--json") account = from_dict(Account, out) assert account.acct == user.username assert account.display_name == name def test_update_account_note(run, app, user): note = ("It's 106 miles to Chicago, we got a full tank of gas, half a pack " "of cigarettes, it's dark... and we're wearing sunglasses.") result = run(cli.accounts.update_account, "--note", note) assert result.exit_code == 0 assert result.stdout.strip() == "✓ Account updated" account = api.verify_credentials(app, user).json() assert get_text(account["note"]) == note def test_update_account_language(run, app, user): result = run(cli.accounts.update_account, "--language", "hr") assert result.exit_code == 0 assert result.stdout.strip() == "✓ Account updated" account = api.verify_credentials(app, user).json() assert account["source"]["language"] == "hr" def test_update_account_privacy(run, app, user): result = run(cli.accounts.update_account, "--privacy", "private") assert result.exit_code == 0 assert result.stdout.strip() == "✓ Account updated" account = api.verify_credentials(app, user).json() assert account["source"]["privacy"] == "private" def test_update_account_avatar(run, app, user): account = api.verify_credentials(app, user).json() old_value = account["avatar"] result = run(cli.accounts.update_account, "--avatar", TRUMPET) assert result.exit_code == 0 assert result.stdout.strip() == "✓ Account updated" account = api.verify_credentials(app, user).json() assert account["avatar"] != old_value def test_update_account_header(run, app, user): account = api.verify_credentials(app, user).json() old_value = account["header"] result = run(cli.accounts.update_account, "--header", TRUMPET) assert result.exit_code == 0 assert result.stdout.strip() == "✓ Account updated" account = api.verify_credentials(app, user).json() assert account["header"] != old_value def test_update_account_locked(run, app, user): result = run(cli.accounts.update_account, "--locked") assert result.exit_code == 0 assert result.stdout.strip() == "✓ Account updated" account = api.verify_credentials(app, user).json() assert account["locked"] is True result = run(cli.accounts.update_account, "--no-locked") assert result.exit_code == 0 assert result.stdout.strip() == "✓ Account updated" account = api.verify_credentials(app, user).json() assert account["locked"] is False def test_update_account_bot(run, app, user): result = run(cli.accounts.update_account, "--bot") assert result.exit_code == 0 assert result.stdout.strip() == "✓ Account updated" account = api.verify_credentials(app, user).json() assert account["bot"] is True result = run(cli.accounts.update_account, "--no-bot") assert result.exit_code == 0 assert result.stdout.strip() == "✓ Account updated" account = api.verify_credentials(app, user).json() assert account["bot"] is False def test_update_account_discoverable(run, app, user): result = run(cli.accounts.update_account, "--discoverable") assert result.exit_code == 0 assert result.stdout.strip() == "✓ Account updated" account = api.verify_credentials(app, user).json() assert account["discoverable"] is True result = run(cli.accounts.update_account, "--no-discoverable") assert result.exit_code == 0 assert result.stdout.strip() == "✓ Account updated" account = api.verify_credentials(app, user).json() assert account["discoverable"] is False def test_update_account_sensitive(run, app, user): result = run(cli.accounts.update_account, "--sensitive") assert result.exit_code == 0 assert result.stdout.strip() == "✓ Account updated" account = api.verify_credentials(app, user).json() assert account["source"]["sensitive"] is True result = run(cli.accounts.update_account, "--no-sensitive") assert result.exit_code == 0 assert result.stdout.strip() == "✓ Account updated" account = api.verify_credentials(app, user).json() assert account["source"]["sensitive"] is False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/tests/test_config.py0000644000175000017500000001160514543235771017016 0ustar00ihabunekihabunekimport os import pytest from toot import User, App, config @pytest.fixture def sample_config(): return { 'apps': { 'foo.social': { 'base_url': 'https://foo.social', 'client_id': 'abc', 'client_secret': 'def', 'instance': 'foo.social' }, 'bar.social': { 'base_url': 'https://bar.social', 'client_id': 'ghi', 'client_secret': 'jkl', 'instance': 'bar.social' }, }, 'users': { 'foo@bar.social': { 'access_token': 'mno', 'instance': 'bar.social', 'username': 'ihabunek' } }, 'active_user': 'foo@bar.social', } def test_extract_active_user_app(sample_config): user, app = config.extract_user_app(sample_config, sample_config['active_user']) assert isinstance(user, User) assert user.instance == 'bar.social' assert user.username == 'ihabunek' assert user.access_token == 'mno' assert isinstance(app, App) assert app.instance == 'bar.social' assert app.base_url == 'https://bar.social' assert app.client_id == 'ghi' assert app.client_secret == 'jkl' def test_extract_active_when_no_active_user(sample_config): # When there is no active user assert config.extract_user_app(sample_config, None) == (None, None) # When active user does not exist for whatever reason assert config.extract_user_app(sample_config, 'does-not-exist') == (None, None) # When active app does not exist for whatever reason sample_config['users']['foo@bar.social']['instance'] = 'does-not-exist' assert config.extract_user_app(sample_config, 'foo@bar.social') == (None, None) def test_save_app(sample_config): pytest.skip("TODO: fix mocking") app = App('xxx.yyy', 2, 3, 4) app2 = App('moo.foo', 5, 6, 7) app_count = len(sample_config['apps']) assert 'xxx.yyy' not in sample_config['apps'] assert 'moo.foo' not in sample_config['apps'] # Sets config.save_app.__wrapped__(sample_config, app) assert len(sample_config['apps']) == app_count + 1 assert 'xxx.yyy' in sample_config['apps'] assert sample_config['apps']['xxx.yyy']['instance'] == 'xxx.yyy' assert sample_config['apps']['xxx.yyy']['base_url'] == 2 assert sample_config['apps']['xxx.yyy']['client_id'] == 3 assert sample_config['apps']['xxx.yyy']['client_secret'] == 4 # Overwrites config.save_app.__wrapped__(sample_config, app2) assert len(sample_config['apps']) == app_count + 2 assert 'xxx.yyy' in sample_config['apps'] assert 'moo.foo' in sample_config['apps'] assert sample_config['apps']['xxx.yyy']['instance'] == 'xxx.yyy' assert sample_config['apps']['xxx.yyy']['base_url'] == 2 assert sample_config['apps']['xxx.yyy']['client_id'] == 3 assert sample_config['apps']['xxx.yyy']['client_secret'] == 4 assert sample_config['apps']['moo.foo']['instance'] == 'moo.foo' assert sample_config['apps']['moo.foo']['base_url'] == 5 assert sample_config['apps']['moo.foo']['client_id'] == 6 assert sample_config['apps']['moo.foo']['client_secret'] == 7 # Idempotent config.save_app.__wrapped__(sample_config, app2) assert len(sample_config['apps']) == app_count + 2 assert 'xxx.yyy' in sample_config['apps'] assert 'moo.foo' in sample_config['apps'] assert sample_config['apps']['xxx.yyy']['instance'] == 'xxx.yyy' assert sample_config['apps']['xxx.yyy']['base_url'] == 2 assert sample_config['apps']['xxx.yyy']['client_id'] == 3 assert sample_config['apps']['xxx.yyy']['client_secret'] == 4 assert sample_config['apps']['moo.foo']['instance'] == 'moo.foo' assert sample_config['apps']['moo.foo']['base_url'] == 5 assert sample_config['apps']['moo.foo']['client_id'] == 6 assert sample_config['apps']['moo.foo']['client_secret'] == 7 def test_delete_app(sample_config): pytest.skip("TODO: fix mocking") app = App('foo.social', 2, 3, 4) app_count = len(sample_config['apps']) assert 'foo.social' in sample_config['apps'] config.delete_app.__wrapped__(sample_config, app) assert 'foo.social' not in sample_config['apps'] assert len(sample_config['apps']) == app_count - 1 # Idempotent config.delete_app.__wrapped__(sample_config, app) assert 'foo.social' not in sample_config['apps'] assert len(sample_config['apps']) == app_count - 1 def test_get_config_file_path(): fn = config.get_config_file_path os.unsetenv('XDG_CONFIG_HOME') os.environ.pop('XDG_CONFIG_HOME', None) assert fn() == os.path.expanduser('~/.config/toot/config.json') os.environ['XDG_CONFIG_HOME'] = '/foo/bar/config' assert fn() == '/foo/bar/config/toot/config.json' os.environ['XDG_CONFIG_HOME'] = '~/foo/config' assert fn() == os.path.expanduser('~/foo/config/toot/config.json') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/tests/test_utils.py0000644000175000017500000001723514543235771016716 0ustar00ihabunekihabunekimport click import pytest from toot.cli.validators import validate_duration from toot.wcstring import wc_wrap, trunc, pad, fit_text from toot.utils import urlencode_url def test_pad(): # guitar symbol will occupy two cells, so padded text should be 1 # character shorter text = 'Frank Zappa 🎸' # Negative values are basically ignored assert pad(text, -100) is text # Padding to length smaller than text length does nothing assert pad(text, 11) is text assert pad(text, 12) is text assert pad(text, 13) is text assert pad(text, 14) is text assert pad(text, 15) == 'Frank Zappa 🎸 ' assert pad(text, 16) == 'Frank Zappa 🎸 ' assert pad(text, 17) == 'Frank Zappa 🎸 ' assert pad(text, 18) == 'Frank Zappa 🎸 ' assert pad(text, 19) == 'Frank Zappa 🎸 ' assert pad(text, 20) == 'Frank Zappa 🎸 ' def test_trunc(): text = 'Frank Zappa 🎸' assert trunc(text, 1) == '…' assert trunc(text, 2) == 'F…' assert trunc(text, 3) == 'Fr…' assert trunc(text, 4) == 'Fra…' assert trunc(text, 5) == 'Fran…' assert trunc(text, 6) == 'Frank…' assert trunc(text, 7) == 'Frank…' assert trunc(text, 8) == 'Frank Z…' assert trunc(text, 9) == 'Frank Za…' assert trunc(text, 10) == 'Frank Zap…' assert trunc(text, 11) == 'Frank Zapp…' assert trunc(text, 12) == 'Frank Zappa…' assert trunc(text, 13) == 'Frank Zappa…' # Truncating to length larger than text length does nothing assert trunc(text, 14) is text assert trunc(text, 15) is text assert trunc(text, 16) is text assert trunc(text, 17) is text assert trunc(text, 18) is text assert trunc(text, 19) is text assert trunc(text, 20) is text def test_fit_text(): text = 'Frank Zappa 🎸' assert fit_text(text, 1) == '…' assert fit_text(text, 2) == 'F…' assert fit_text(text, 3) == 'Fr…' assert fit_text(text, 4) == 'Fra…' assert fit_text(text, 5) == 'Fran…' assert fit_text(text, 6) == 'Frank…' assert fit_text(text, 7) == 'Frank…' assert fit_text(text, 8) == 'Frank Z…' assert fit_text(text, 9) == 'Frank Za…' assert fit_text(text, 10) == 'Frank Zap…' assert fit_text(text, 11) == 'Frank Zapp…' assert fit_text(text, 12) == 'Frank Zappa…' assert fit_text(text, 13) == 'Frank Zappa…' assert fit_text(text, 14) == 'Frank Zappa 🎸' assert fit_text(text, 15) == 'Frank Zappa 🎸 ' assert fit_text(text, 16) == 'Frank Zappa 🎸 ' assert fit_text(text, 17) == 'Frank Zappa 🎸 ' assert fit_text(text, 18) == 'Frank Zappa 🎸 ' assert fit_text(text, 19) == 'Frank Zappa 🎸 ' assert fit_text(text, 20) == 'Frank Zappa 🎸 ' def test_wc_wrap_plain_text(): lorem = ( "Eius voluptas eos praesentium et tempore. Quaerat nihil voluptatem " "excepturi reiciendis sapiente voluptate natus. Tenetur occaecati " "velit dicta dolores. Illo reiciendis nulla ea. Facilis nostrum non " "qui inventore sit." ) assert list(wc_wrap(lorem, 50)) == [ #01234567890123456789012345678901234567890123456789 # noqa "Eius voluptas eos praesentium et tempore. Quaerat", "nihil voluptatem excepturi reiciendis sapiente", "voluptate natus. Tenetur occaecati velit dicta", "dolores. Illo reiciendis nulla ea. Facilis nostrum", "non qui inventore sit.", ] def test_wc_wrap_plain_text_wrap_on_any_whitespace(): lorem = ( "Eius\t\tvoluptas\teos\tpraesentium\tet\ttempore.\tQuaerat\tnihil\tvoluptatem\t" "excepturi\nreiciendis\n\nsapiente\nvoluptate\nnatus.\nTenetur\noccaecati\n" "velit\rdicta\rdolores.\rIllo\rreiciendis\rnulla\r\r\rea.\rFacilis\rnostrum\rnon\r" "qui\u2003inventore\u2003\u2003sit." # em space ) assert list(wc_wrap(lorem, 50)) == [ #01234567890123456789012345678901234567890123456789 # noqa "Eius voluptas eos praesentium et tempore. Quaerat", "nihil voluptatem excepturi reiciendis sapiente", "voluptate natus. Tenetur occaecati velit dicta", "dolores. Illo reiciendis nulla ea. Facilis nostrum", "non qui inventore sit.", ] def test_wc_wrap_text_with_wide_chars(): lorem = ( "☕☕☕☕☕ voluptas eos praesentium et 🎸🎸🎸🎸🎸. Quaerat nihil " "voluptatem excepturi reiciendis sapiente voluptate natus." ) assert list(wc_wrap(lorem, 50)) == [ #01234567890123456789012345678901234567890123456789 # noqa "☕☕☕☕☕ voluptas eos praesentium et 🎸🎸🎸🎸🎸.", "Quaerat nihil voluptatem excepturi reiciendis", "sapiente voluptate natus.", ] def test_wc_wrap_hard_wrap(): lorem = ( "☕☕☕☕☕voluptaseospraesentiumet🎸🎸🎸🎸🎸.Quaeratnihil" "voluptatemexcepturireiciendissapientevoluptatenatus." ) assert list(wc_wrap(lorem, 50)) == [ #01234567890123456789012345678901234567890123456789 # noqa "☕☕☕☕☕voluptaseospraesentiumet🎸🎸🎸🎸🎸.Quaer", "atnihilvoluptatemexcepturireiciendissapientevolupt", "atenatus.", ] def test_wc_wrap_indented(): lorem = ( " Eius voluptas eos praesentium et tempore. Quaerat nihil voluptatem " " excepturi reiciendis sapiente voluptate natus. Tenetur occaecati " " velit dicta dolores. Illo reiciendis nulla ea. Facilis nostrum non " " qui inventore sit." ) assert list(wc_wrap(lorem, 50)) == [ #01234567890123456789012345678901234567890123456789 # noqa "Eius voluptas eos praesentium et tempore. Quaerat", "nihil voluptatem excepturi reiciendis sapiente", "voluptate natus. Tenetur occaecati velit dicta", "dolores. Illo reiciendis nulla ea. Facilis nostrum", "non qui inventore sit.", ] def test_duration(): def duration(value): return validate_duration(None, None, value) # Long hand assert duration("1 second") == 1 assert duration("1 seconds") == 1 assert duration("100 second") == 100 assert duration("100 seconds") == 100 assert duration("5 minutes") == 5 * 60 assert duration("5 minutes 10 seconds") == 5 * 60 + 10 assert duration("1 hour 5 minutes") == 3600 + 5 * 60 assert duration("1 hour 5 minutes 1 second") == 3600 + 5 * 60 + 1 assert duration("5 days") == 5 * 86400 assert duration("5 days 3 minutes") == 5 * 86400 + 3 * 60 assert duration("5 days 10 hours 3 minutes 1 second") == 5 * 86400 + 10 * 3600 + 3 * 60 + 1 # Short hand assert duration("1s") == 1 assert duration("100s") == 100 assert duration("5m") == 5 * 60 assert duration("5m10s") == 5 * 60 + 10 assert duration("5m 10s") == 5 * 60 + 10 assert duration("1h5m1s") == 3600 + 5 * 60 + 1 assert duration("1h 5m 1s") == 3600 + 5 * 60 + 1 assert duration("5d") == 5 * 86400 assert duration("5d3m") == 5 * 86400 + 3 * 60 assert duration("5d 3m") == 5 * 86400 + 3 * 60 assert duration("5d 10h 3m 1s") == 5 * 86400 + 10 * 3600 + 3 * 60 + 1 assert duration("5d10h3m1s") == 5 * 86400 + 10 * 3600 + 3 * 60 + 1 with pytest.raises(click.BadParameter): duration("") with pytest.raises(click.BadParameter): duration("100") # Wrong order with pytest.raises(click.BadParameter): duration("1m1d") with pytest.raises(click.BadParameter): duration("banana") def test_urlencode_url(): assert urlencode_url("https://www.example.com") == "https://www.example.com" assert urlencode_url("https://www.example.com/url%20with%20spaces") == "https://www.example.com/url%20with%20spaces" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1657012909.0 toot-0.41.1/tests/test_version.py0000644000175000017500000000036114261001255017214 0ustar00ihabunekihabunekimport toot from pkg_resources import get_distribution def test_version(): """Version specified in __version__ should be the same as the one specified in setup.py.""" assert toot.__version__ == get_distribution('toot').version ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1704229718.33644 toot-0.41.1/tests/tui/0000755000175000017500000000000014545075526014740 5ustar00ihabunekihabunek././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700988522.0 toot-0.41.1/tests/tui/test_rich_text.py0000644000175000017500000000232214530603152020323 0ustar00ihabunekihabunekfrom urwid import Divider, Filler, Pile from toot.tui.richtext import url_to_widget from urwidgets import Hyperlink, TextEmbed from toot.tui.richtext.richtext import html_to_widgets def test_url_to_widget(): url = "http://foo.bar" embed_widget = url_to_widget(url) assert isinstance(embed_widget, TextEmbed) [(filler, length)] = embed_widget.embedded assert length == len(url) assert isinstance(filler, Filler) link_widget = filler.base_widget assert isinstance(link_widget, Hyperlink) assert link_widget.attrib == "link" assert link_widget.text == url assert link_widget.uri == url def test_html_to_widgets(): html = """

foo

foo bar baz

""".strip() [foo, divider, bar] = html_to_widgets(html) assert isinstance(foo, Pile) assert isinstance(divider, Divider) assert isinstance(bar, Pile) [(foo_embed, _)] = foo.contents assert foo_embed.embedded == [] assert foo_embed.attrib == [] assert foo_embed.text == "foo" [(bar_embed, _)] = bar.contents assert bar_embed.embedded == [] assert bar_embed.attrib == [(None, 4), ("b", 3), (None, 1), ("i", 3)] assert bar_embed.text == "foo bar baz" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704107353.0 toot-0.41.1/tests/utils.py0000644000175000017500000000207114544516531015644 0ustar00ihabunekihabunek""" Helpers for testing. """ import time from typing import Any, Callable class MockResponse: def __init__(self, response_data={}, ok=True, is_redirect=False): self.response_data = response_data self.content = response_data self.ok = ok self.is_redirect = is_redirect def raise_for_status(self): pass def json(self): return self.response_data def retval(val): return lambda *args, **kwargs: val def run_with_retries(fn: Callable[..., Any]): """ Run the the given function repeatedly until it finishes without raising an AssertionError. Sleep a bit between attempts. If the function doesn't succeed in the given number of tries raises the AssertionError. Used for tests which should eventually succeed. """ # Wait upto 6 seconds with incrementally longer sleeps delays = [0.1, 0.2, 0.3, 0.4, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5] for delay in delays: try: return fn() except AssertionError: time.sleep(delay) fn() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1704229718.3384402 toot-0.41.1/toot/0000755000175000017500000000000014545075526013762 5ustar00ihabunekihabunek././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704229648.0 toot-0.41.1/toot/__init__.py0000644000175000017500000000214414545075420016065 0ustar00ihabunekihabunekimport os import sys from os.path import join, expanduser from typing import NamedTuple __version__ = '0.41.1' class App(NamedTuple): instance: str base_url: str client_id: str client_secret: str class User(NamedTuple): instance: str username: str access_token: str DEFAULT_INSTANCE = 'https://mastodon.social' CLIENT_NAME = 'toot - a Mastodon CLI client' CLIENT_WEBSITE = 'https://github.com/ihabunek/toot' TOOT_CONFIG_DIR_NAME = "toot" def get_config_dir(): """Returns the path to toot config directory""" # On Windows, store the config in roaming appdata if sys.platform == "win32" and "APPDATA" in os.environ: return join(os.getenv("APPDATA"), TOOT_CONFIG_DIR_NAME) # Respect XDG_CONFIG_HOME env variable if set # https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html if "XDG_CONFIG_HOME" in os.environ: config_home = expanduser(os.environ["XDG_CONFIG_HOME"]) return join(config_home, TOOT_CONFIG_DIR_NAME) # Default to ~/.config/toot/ return join(expanduser("~"), ".config", TOOT_CONFIG_DIR_NAME) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/toot/__main__.py0000644000175000017500000000004014543235771016044 0ustar00ihabunekihabunekfrom toot.cli import cli cli() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704228888.0 toot-0.41.1/toot/api.py0000644000175000017500000005000414545074030015071 0ustar00ihabunekihabunekimport mimetypes import re import uuid from os import path from requests import Response from typing import BinaryIO, List, Optional from urllib.parse import urlparse, urlencode, quote from toot import App, User, http, CLIENT_NAME, CLIENT_WEBSITE from toot.exceptions import ConsoleError from toot.utils import drop_empty_values, str_bool, str_bool_nullable SCOPES = 'read write follow' def find_account(app, user, account_name): if not account_name: raise ConsoleError("Empty account name given") normalized_name = account_name.lstrip("@").lower() # Strip @ from accounts on the local instance. The `acct` # field in account object contains the qualified name for users of other # instances, but only the username for users of the local instance. This is # required in order to match the account name below. if "@" in normalized_name: [username, instance] = normalized_name.split("@", maxsplit=1) if instance == app.instance: normalized_name = username response = search(app, user, account_name, type="accounts", resolve=True) for account in response.json()["accounts"]: if account["acct"].lower() == normalized_name: return account raise ConsoleError("Account not found") def _account_action(app, user, account, action) -> Response: url = f"/api/v1/accounts/{account}/{action}" return http.post(app, user, url) def _status_action(app, user, status_id, action, data=None) -> Response: url = f"/api/v1/statuses/{status_id}/{action}" return http.post(app, user, url, data=data) def _tag_action(app, user, tag_name, action) -> Response: url = f"/api/v1/tags/{tag_name}/{action}" return http.post(app, user, url) def create_app(base_url): url = f"{base_url}/api/v1/apps" json = { 'client_name': CLIENT_NAME, 'redirect_uris': 'urn:ietf:wg:oauth:2.0:oob', 'scopes': SCOPES, 'website': CLIENT_WEBSITE, } return http.anon_post(url, json=json).json() def get_muted_accounts(app, user): return http.get(app, user, "/api/v1/mutes").json() def get_blocked_accounts(app, user): return http.get(app, user, "/api/v1/blocks").json() def register_account(app, username, email, password, locale="en", agreement=True): """ Register an account https://docs.joinmastodon.org/methods/accounts/#create """ token = fetch_app_token(app)["access_token"] url = f"{app.base_url}/api/v1/accounts" headers = {"Authorization": f"Bearer {token}"} json = { "username": username, "email": email, "password": password, "agreement": agreement, "locale": locale } return http.anon_post(url, json=json, headers=headers).json() def update_account( app, user, display_name=None, note=None, avatar=None, header=None, bot=None, discoverable=None, locked=None, privacy=None, sensitive=None, language=None ): """ Update account credentials https://docs.joinmastodon.org/methods/accounts/#update_credentials """ files = drop_empty_values({"avatar": avatar, "header": header}) data = drop_empty_values({ "bot": str_bool_nullable(bot), "discoverable": str_bool_nullable(discoverable), "display_name": display_name, "locked": str_bool_nullable(locked), "note": note, "source[language]": language, "source[privacy]": privacy, "source[sensitive]": str_bool_nullable(sensitive), }) return http.patch(app, user, "/api/v1/accounts/update_credentials", files=files, data=data) def fetch_app_token(app): json = { "client_id": app.client_id, "client_secret": app.client_secret, "grant_type": "client_credentials", "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", "scope": "read write" } return http.anon_post(f"{app.base_url}/oauth/token", json=json).json() def login(app: App, username: str, password: str): url = app.base_url + '/oauth/token' data = { 'grant_type': 'password', 'client_id': app.client_id, 'client_secret': app.client_secret, 'username': username, 'password': password, 'scope': SCOPES, } return http.anon_post(url, data=data).json() def get_browser_login_url(app: App) -> str: """Returns the URL for manual log in via browser""" return "{}/oauth/authorize/?{}".format(app.base_url, urlencode({ "response_type": "code", "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", "scope": SCOPES, "client_id": app.client_id, })) def request_access_token(app: App, authorization_code: str): url = app.base_url + '/oauth/token' data = { 'grant_type': 'authorization_code', 'client_id': app.client_id, 'client_secret': app.client_secret, 'code': authorization_code, 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob', } return http.anon_post(url, data=data, allow_redirects=False).json() def post_status( app, user, status, visibility=None, media_ids=None, sensitive=False, spoiler_text=None, in_reply_to_id=None, language=None, scheduled_at=None, content_type=None, poll_options=None, poll_expires_in=None, poll_multiple=None, poll_hide_totals=None, ) -> Response: """ Publish a new status. https://docs.joinmastodon.org/methods/statuses/#create """ # Idempotency key assures the same status is not posted multiple times # if the request is retried. headers = {"Idempotency-Key": uuid.uuid4().hex} # Strip keys for which value is None # Sending null values doesn't bother Mastodon, but it breaks Pleroma data = drop_empty_values({ 'status': status, 'media_ids': media_ids, 'visibility': visibility, 'sensitive': sensitive, 'in_reply_to_id': in_reply_to_id, 'language': language, 'scheduled_at': scheduled_at, 'content_type': content_type, 'spoiler_text': spoiler_text, }) if poll_options: data["poll"] = { "options": poll_options, "expires_in": poll_expires_in, "multiple": poll_multiple, "hide_totals": poll_hide_totals, } return http.post(app, user, '/api/v1/statuses', json=data, headers=headers) def edit_status( app, user, id, status, visibility='public', media_ids=None, sensitive=False, spoiler_text=None, in_reply_to_id=None, language=None, content_type=None, poll_options=None, poll_expires_in=None, poll_multiple=None, poll_hide_totals=None, ) -> Response: """ Edit an existing status https://docs.joinmastodon.org/methods/statuses/#edit """ # Strip keys for which value is None # Sending null values doesn't bother Mastodon, but it breaks Pleroma data = drop_empty_values({ 'status': status, 'media_ids': media_ids, 'visibility': visibility, 'sensitive': sensitive, 'in_reply_to_id': in_reply_to_id, 'language': language, 'content_type': content_type, 'spoiler_text': spoiler_text, }) if poll_options: data["poll"] = { "options": poll_options, "expires_in": poll_expires_in, "multiple": poll_multiple, "hide_totals": poll_hide_totals, } return http.put(app, user, f"/api/v1/statuses/{id}", json=data) def fetch_status(app, user, id): """ Fetch a single status https://docs.joinmastodon.org/methods/statuses/#get """ return http.get(app, user, f"/api/v1/statuses/{id}") def fetch_status_source(app, user, id): """ Fetch the source (original text) for a single status. This only works on local toots. https://docs.joinmastodon.org/methods/statuses/#source """ return http.get(app, user, f"/api/v1/statuses/{id}/source") def scheduled_statuses(app, user): """ List scheduled statuses https://docs.joinmastodon.org/methods/scheduled_statuses/#get """ return http.get(app, user, "/api/v1/scheduled_statuses").json() def delete_status(app, user, status_id): """ Deletes a status with given ID. https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md#deleting-a-status """ return http.delete(app, user, f"/api/v1/statuses/{status_id}") def favourite(app, user, status_id): return _status_action(app, user, status_id, 'favourite') def unfavourite(app, user, status_id): return _status_action(app, user, status_id, 'unfavourite') def reblog(app, user, status_id, visibility="public"): return _status_action(app, user, status_id, 'reblog', data={"visibility": visibility}) def unreblog(app, user, status_id): return _status_action(app, user, status_id, 'unreblog') def pin(app, user, status_id): return _status_action(app, user, status_id, 'pin') def unpin(app, user, status_id): return _status_action(app, user, status_id, 'unpin') def bookmark(app, user, status_id): return _status_action(app, user, status_id, 'bookmark') def unbookmark(app, user, status_id): return _status_action(app, user, status_id, 'unbookmark') def translate(app, user, status_id): return _status_action(app, user, status_id, 'translate') def context(app, user, status_id) -> Response: url = f"/api/v1/statuses/{status_id}/context" return http.get(app, user, url) def reblogged_by(app, user, status_id) -> Response: url = f"/api/v1/statuses/{status_id}/reblogged_by" return http.get(app, user, url) def get_timeline_generator( app: Optional[App], user: Optional[User], account: Optional[str] = None, list_id: Optional[str] = None, tag: Optional[str] = None, local: bool = False, public: bool = False, limit: int = 20, # TODO ): if public: return public_timeline_generator(app, user, local=local, limit=limit) elif tag: return tag_timeline_generator(app, user, tag, local=local, limit=limit) elif account: return account_timeline_generator(app, user, account, limit=limit) elif list_id: return timeline_list_generator(app, user, list_id, limit=limit) else: return home_timeline_generator(app, user, limit=limit) def _get_next_path(headers): """Given timeline response headers, returns the path to the next batch""" links = headers.get('Link', '') matches = re.match('<([^>]+)>; rel="next"', links) if matches: parsed = urlparse(matches.group(1)) return "?".join([parsed.path, parsed.query]) def _get_next_url(headers) -> Optional[str]: """Given timeline response headers, returns the url to the next batch""" links = headers.get('Link', '') match = re.match('<([^>]+)>; rel="next"', links) if match: return match.group(1) def _timeline_generator(app, user, path, params=None): while path: response = http.get(app, user, path, params) yield response.json() path = _get_next_path(response.headers) def _notification_timeline_generator(app, user, path, params=None): while path: response = http.get(app, user, path, params) notification = response.json() yield [n["status"] for n in notification if n["status"]] path = _get_next_path(response.headers) def _conversation_timeline_generator(app, user, path, params=None): while path: response = http.get(app, user, path, params) conversation = response.json() yield [c["last_status"] for c in conversation if c["last_status"]] path = _get_next_path(response.headers) def home_timeline_generator(app, user, limit=20): path = "/api/v1/timelines/home" params = {"limit": limit} return _timeline_generator(app, user, path, params) def public_timeline_generator(app, user, local=False, limit=20): path = '/api/v1/timelines/public' params = {'local': str_bool(local), 'limit': limit} return _timeline_generator(app, user, path, params) def tag_timeline_generator(app, user, hashtag, local=False, limit=20): path = f"/api/v1/timelines/tag/{quote(hashtag)}" params = {'local': str_bool(local), 'limit': limit} return _timeline_generator(app, user, path, params) def bookmark_timeline_generator(app, user, limit=20): path = '/api/v1/bookmarks' params = {'limit': limit} return _timeline_generator(app, user, path, params) def notification_timeline_generator(app, user, limit=20): # exclude all but mentions and statuses exclude_types = ["follow", "favourite", "reblog", "poll", "follow_request"] params = {"exclude_types[]": exclude_types, "limit": limit} return _notification_timeline_generator(app, user, "/api/v1/notifications", params) def conversation_timeline_generator(app, user, limit=20): path = "/api/v1/conversations" params = {"limit": limit} return _conversation_timeline_generator(app, user, path, params) def account_timeline_generator(app, user, account_name: str, replies=False, reblogs=False, limit=20): account = find_account(app, user, account_name) path = f"/api/v1/accounts/{account['id']}/statuses" params = {"limit": limit, "exclude_replies": not replies, "exclude_reblogs": not reblogs} return _timeline_generator(app, user, path, params) def timeline_list_generator(app, user, list_id, limit=20): path = f"/api/v1/timelines/list/{list_id}" return _timeline_generator(app, user, path, {'limit': limit}) def _anon_timeline_generator(url, params=None): while url: response = http.anon_get(url, params) yield response.json() url = _get_next_url(response.headers) def anon_public_timeline_generator(base_url, local=False, limit=20): query = urlencode({"local": str_bool(local), "limit": limit}) url = f"{base_url}/api/v1/timelines/public?{query}" return _anon_timeline_generator(url) def anon_tag_timeline_generator(base_url, hashtag, local=False, limit=20): query = urlencode({"local": str_bool(local), "limit": limit}) url = f"{base_url}/api/v1/timelines/tag/{quote(hashtag)}?{query}" return _anon_timeline_generator(url) def get_media(app: App, user: User, id: str): return http.get(app, user, f"/api/v1/media/{id}").json() def upload_media( app: App, user: User, media: BinaryIO, description: Optional[str] = None, thumbnail: Optional[BinaryIO] = None, ): data = drop_empty_values({"description": description}) # NB: Documentation says that "file" should provide a mime-type which we # don't do currently, but it works. files = drop_empty_values({ "file": media, "thumbnail": _add_mime_type(thumbnail) }) return http.post(app, user, "/api/v2/media", data=data, files=files) def _add_mime_type(file): if file is None: return None # TODO: mimetypes uses the file extension to guess the mime type which is # not always good enough (e.g. files without extension). python-magic could # be used instead but it requires adding it as a dependency. mime_type = mimetypes.guess_type(file.name) if not mime_type: raise ConsoleError(f"Unable guess mime type of '{file.name}'. " "Ensure the file has the desired extension.") filename = path.basename(file.name) return (filename, file, mime_type) def search(app, user, query, resolve=False, type=None): """ Perform a search. https://docs.joinmastodon.org/methods/search/#v2 """ params = drop_empty_values({ "q": query, "resolve": str_bool(resolve), "type": type }) return http.get(app, user, "/api/v2/search", params) def follow(app, user, account): return _account_action(app, user, account, 'follow') def unfollow(app, user, account): return _account_action(app, user, account, 'unfollow') def follow_tag(app, user, tag_name) -> Response: return _tag_action(app, user, tag_name, 'follow') def unfollow_tag(app, user, tag_name) -> Response: return _tag_action(app, user, tag_name, 'unfollow') def _get_response_list(app, user, path): items = [] while path: response = http.get(app, user, path) items += response.json() path = _get_next_path(response.headers) return items def following(app, user, account): path = f"/api/v1/accounts/{account}/following" return _get_response_list(app, user, path) def followers(app, user, account): path = f"/api/v1/accounts/{account}/followers" return _get_response_list(app, user, path) def followed_tags(app, user): path = '/api/v1/followed_tags' return _get_response_list(app, user, path) def featured_tags(app, user): return http.get(app, user, "/api/v1/featured_tags") def feature_tag(app, user, tag: str) -> Response: return http.post(app, user, "/api/v1/featured_tags", data={"name": tag}) def unfeature_tag(app, user, tag_id: str) -> Response: return http.delete(app, user, f"/api/v1/featured_tags/{tag_id}") def find_tag(app, user, tag) -> Optional[dict]: """Find a hashtag by tag name or ID""" tag = tag.lstrip("#") results = search(app, user, tag, type="hashtags").json() return next( ( t for t in results["hashtags"] if t["name"].lower() == tag.lstrip("#").lower() or t["id"] == tag ), None ) def find_featured_tag(app, user, tag) -> Optional[dict]: """Find a featured tag by tag name or ID""" return next( ( t for t in featured_tags(app, user).json() if t["name"].lower() == tag.lstrip("#").lower() or t["id"] == tag ), None ) def whois(app, user, account): return http.get(app, user, f'/api/v1/accounts/{account}').json() def vote(app, user, poll_id, choices: List[int]): url = f"/api/v1/polls/{poll_id}/votes" json = {'choices': choices} return http.post(app, user, url, json=json).json() def get_relationship(app, user, account): params = {"id[]": account} return http.get(app, user, '/api/v1/accounts/relationships', params).json()[0] def mute(app, user, account): return _account_action(app, user, account, 'mute') def unmute(app, user, account): return _account_action(app, user, account, 'unmute') def muted(app, user): return _get_response_list(app, user, "/api/v1/mutes") def block(app, user, account): return _account_action(app, user, account, 'block') def unblock(app, user, account): return _account_action(app, user, account, 'unblock') def blocked(app, user): return _get_response_list(app, user, "/api/v1/blocks") def verify_credentials(app, user) -> Response: return http.get(app, user, '/api/v1/accounts/verify_credentials') def get_notifications(app, user, types=[], exclude_types=[], limit=20): params = {"types[]": types, "exclude_types[]": exclude_types, "limit": limit} return http.get(app, user, '/api/v1/notifications', params).json() def clear_notifications(app, user): http.post(app, user, '/api/v1/notifications/clear') def get_instance(base_url: str) -> Response: url = f"{base_url}/api/v1/instance" return http.anon_get(url) def get_preferences(app, user) -> Response: return http.get(app, user, '/api/v1/preferences') def get_lists(app, user): return http.get(app, user, "/api/v1/lists").json() def get_list_accounts(app, user, list_id): path = f"/api/v1/lists/{list_id}/accounts" return _get_response_list(app, user, path) def create_list(app, user, title, replies_policy="none"): url = "/api/v1/lists" json = {'title': title} if replies_policy: json['replies_policy'] = replies_policy return http.post(app, user, url, json=json) def delete_list(app, user, id): return http.delete(app, user, f"/api/v1/lists/{id}") def add_accounts_to_list(app, user, list_id, account_ids): url = f"/api/v1/lists/{list_id}/accounts" json = {'account_ids': account_ids} return http.post(app, user, url, json=json) def remove_accounts_from_list(app, user, list_id, account_ids): url = f"/api/v1/lists/{list_id}/accounts" json = {'account_ids': account_ids} return http.delete(app, user, url, json=json) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/toot/auth.py0000644000175000017500000000435314543235771015300 0ustar00ihabunekihabunekfrom toot import api, config, User, App from toot.entities import from_dict, Instance from toot.exceptions import ApiError, ConsoleError from urllib.parse import urlparse def find_instance(base_url: str) -> Instance: try: instance = api.get_instance(base_url).json() return from_dict(Instance, instance) except Exception: raise ConsoleError(f"Instance not found at {base_url}") def register_app(domain: str, base_url: str) -> App: try: response = api.create_app(base_url) except ApiError: raise ConsoleError("Registration failed.") app = App(domain, base_url, response['client_id'], response['client_secret']) config.save_app(app) return app def get_or_create_app(base_url: str) -> App: instance = find_instance(base_url) domain = _get_instance_domain(instance) return config.load_app(domain) or register_app(domain, base_url) def create_user(app: App, access_token: str) -> User: # Username is not yet known at this point, so fetch it from Mastodon user = User(app.instance, None, access_token) creds = api.verify_credentials(app, user).json() user = User(app.instance, creds["username"], access_token) config.save_user(user, activate=True) return user def login_username_password(app: App, email: str, password: str) -> User: try: response = api.login(app, email, password) except Exception: raise ConsoleError("Login failed") return create_user(app, response["access_token"]) def login_auth_code(app: App, authorization_code: str) -> User: try: response = api.request_access_token(app, authorization_code) except Exception: raise ConsoleError("Login failed") return create_user(app, response["access_token"]) def _get_instance_domain(instance: Instance) -> str: """Extracts the instance domain name. Pleroma and its forks return an actual URI here, rather than a domain name like Mastodon. This is contrary to the spec.¯ in that case, parse out the domain and return it. TODO: when updating to v2 instance endpoint, this field has been renamed to `domain` """ if instance.uri.startswith("http"): return urlparse(instance.uri).netloc return instance.uri ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1704229718.3404403 toot-0.41.1/toot/cli/0000755000175000017500000000000014545075526014531 5ustar00ihabunekihabunek././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704229574.0 toot-0.41.1/toot/cli/__init__.py0000644000175000017500000001244614545075306016645 0ustar00ihabunekihabunekimport click import logging import os import sys import typing as t from click.shell_completion import CompletionItem from click.types import StringParamType from functools import wraps from toot import App, User, config, __version__ from toot.output import print_warning from toot.settings import get_settings if t.TYPE_CHECKING: import typing_extensions as te P = te.ParamSpec("P") R = t.TypeVar("R") T = t.TypeVar("T") PRIVACY_CHOICES = ["public", "unlisted", "private"] VISIBILITY_CHOICES = ["public", "unlisted", "private", "direct"] TUI_COLORS = { "1": 1, "16": 16, "88": 88, "256": 256, "16777216": 16777216, "24bit": 16777216, } TUI_COLORS_CHOICES = list(TUI_COLORS.keys()) TUI_COLORS_VALUES = list(TUI_COLORS.values()) DURATION_EXAMPLES = """e.g. "1 day", "2 hours 30 minutes", "5 minutes 30 seconds" or any combination of above. Shorthand: "1d", "2h30m", "5m30s\"""" def get_default_visibility() -> str: return os.getenv("TOOT_POST_VISIBILITY", "public") def get_default_map(): settings = get_settings() common = settings.get("common", {}) commands = settings.get("commands", {}) # TODO: remove in version 1.0 tui_old = settings.get("tui", {}).copy() if "palette" in tui_old: del tui_old["palette"] if tui_old: # TODO: don't show the warning for [toot.palette] print_warning("Settings section [tui] has been deprecated in favour of [commands.tui].") tui_new = commands.get("tui", {}) commands["tui"] = {**tui_old, **tui_new} return {**common, **commands} # Tweak the Click context # https://click.palletsprojects.com/en/8.1.x/api/#context CONTEXT = dict( # Enable using environment variables to set options auto_envvar_prefix="TOOT", # Add shorthand -h for invoking help help_option_names=["-h", "--help"], # Always show default values for options show_default=True, # Load command defaults from settings default_map=get_default_map(), ) class Context(t.NamedTuple): app: t.Optional[App] user: t.Optional[User] = None color: bool = False debug: bool = False class TootObj(t.NamedTuple): """Data to add to Click context""" color: bool = True debug: bool = False as_user: t.Optional[str] = None # Pass a context for testing purposes test_ctx: t.Optional[Context] = None class AccountParamType(StringParamType): """Custom type to add shell completion for account names""" name = "account" def shell_complete(self, ctx, param, incomplete: str): users = config.load_config()["users"].keys() return [ CompletionItem(u) for u in users if u.lower().startswith(incomplete.lower()) ] class InstanceParamType(StringParamType): """Custom type to add shell completion for instance domains""" name = "instance" def shell_complete(self, ctx, param, incomplete: str): apps = config.load_config()["apps"] return [ CompletionItem(i) for i in apps.keys() if i.lower().startswith(incomplete.lower()) ] def pass_context(f: "t.Callable[te.Concatenate[Context, P], R]") -> "t.Callable[P, R]": """Pass the toot Context as first argument.""" @wraps(f) def wrapped(*args: "P.args", **kwargs: "P.kwargs") -> R: return f(get_context(), *args, **kwargs) return wrapped def get_context() -> Context: click_context = click.get_current_context() obj: TootObj = click_context.obj # This is used to pass a context for testing, not used in normal usage if obj.test_ctx: return obj.test_ctx if obj.as_user: user, app = config.get_user_app(obj.as_user) if not user or not app: raise click.ClickException(f"Account '{obj.as_user}' not found. Run `toot auth` to see available accounts.") else: user, app = config.get_active_user_app() if not user or not app: raise click.ClickException("This command requires you to be logged in.") return Context(app, user, obj.color, obj.debug) json_option = click.option( "--json", is_flag=True, default=False, help="Print data as JSON rather than human readable text" ) @click.group(context_settings=CONTEXT) @click.option("-w", "--max-width", type=int, default=80, help="Maximum width for content rendered by toot") @click.option("--debug/--no-debug", default=False, help="Log debug info to stderr") @click.option("--color/--no-color", default=sys.stdout.isatty(), help="Use ANSI color in output") @click.option("--as", "as_user", type=AccountParamType(), help="The account to use, overrides the active account.") @click.version_option(__version__, message="%(prog)s v%(version)s") @click.pass_context def cli(ctx: click.Context, max_width: int, color: bool, debug: bool, as_user: str): """Toot is a Mastodon CLI""" ctx.obj = TootObj(color, debug, as_user) ctx.color = color ctx.max_content_width = max_width if debug: logging.basicConfig(level=logging.DEBUG) from toot.cli import accounts # noqa from toot.cli import auth # noqa from toot.cli import lists # noqa from toot.cli import post # noqa from toot.cli import read # noqa from toot.cli import statuses # noqa from toot.cli import tags # noqa from toot.cli import timelines # noqa from toot.cli import tui # noqa ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/toot/cli/accounts.py0000644000175000017500000001547314543235771016732 0ustar00ihabunekihabunekimport click import json as pyjson from typing import BinaryIO, Optional from toot import api from toot.cli import PRIVACY_CHOICES, cli, json_option, Context, pass_context from toot.cli.validators import validate_language from toot.output import print_acct_list @cli.command(name="update_account") @click.option("--display-name", help="The display name to use for the profile.") @click.option("--note", help="The account bio.") @click.option( "--avatar", type=click.File(mode="rb"), help="Path to the avatar image to set.", ) @click.option( "--header", type=click.File(mode="rb"), help="Path to the header image to set.", ) @click.option( "--bot/--no-bot", default=None, help="Whether the account has a bot flag.", ) @click.option( "--discoverable/--no-discoverable", default=None, help="Whether the account should be shown in the profile directory.", ) @click.option( "--locked/--no-locked", default=None, help="Whether manual approval of follow requests is required.", ) @click.option( "--privacy", type=click.Choice(PRIVACY_CHOICES), help="Default post privacy for authored statuses.", ) @click.option( "--sensitive/--no-sensitive", default=None, help="Whether to mark authored statuses as sensitive by default.", ) @click.option( "--language", callback=validate_language, help="Default language to use for authored statuses (ISO 639-1).", ) @json_option @pass_context def update_account( ctx: Context, display_name: Optional[str], note: Optional[str], avatar: Optional[BinaryIO], header: Optional[BinaryIO], bot: Optional[bool], discoverable: Optional[bool], locked: Optional[bool], privacy: Optional[bool], sensitive: Optional[bool], language: Optional[bool], json: bool, ): """Update your account details""" options = [ avatar, bot, discoverable, display_name, header, language, locked, note, privacy, sensitive, ] if all(option is None for option in options): raise click.ClickException("Please specify at least one option to update the account") response = api.update_account( ctx.app, ctx.user, avatar=avatar, bot=bot, discoverable=discoverable, display_name=display_name, header=header, language=language, locked=locked, note=note, privacy=privacy, sensitive=sensitive, ) if json: click.echo(response.text) else: click.secho("✓ Account updated", fg="green") @cli.command() @click.argument("account") @json_option @pass_context def follow(ctx: Context, account: str, json: bool): """Follow an account""" found_account = api.find_account(ctx.app, ctx.user, account) response = api.follow(ctx.app, ctx.user, found_account["id"]) if json: click.echo(response.text) else: click.secho(f"✓ You are now following {account}", fg="green") @cli.command() @click.argument("account") @json_option @pass_context def unfollow(ctx: Context, account: str, json: bool): """Unfollow an account""" found_account = api.find_account(ctx.app, ctx.user, account) response = api.unfollow(ctx.app, ctx.user, found_account["id"]) if json: click.echo(response.text) else: click.secho(f"✓ You are no longer following {account}", fg="green") @cli.command() @click.argument("account", required=False) @json_option @pass_context def following(ctx: Context, account: Optional[str], json: bool): """List accounts followed by an account. If no account is given list accounts followed by you. """ account = account or ctx.user.username found_account = api.find_account(ctx.app, ctx.user, account) accounts = api.following(ctx.app, ctx.user, found_account["id"]) if json: click.echo(pyjson.dumps(accounts)) else: print_acct_list(accounts) @cli.command() @click.argument("account", required=False) @json_option @pass_context def followers(ctx: Context, account: Optional[str], json: bool): """List accounts following an account. If no account given list accounts following you.""" account = account or ctx.user.username found_account = api.find_account(ctx.app, ctx.user, account) accounts = api.followers(ctx.app, ctx.user, found_account["id"]) if json: click.echo(pyjson.dumps(accounts)) else: print_acct_list(accounts) @cli.command() @click.argument("account") @json_option @pass_context def mute(ctx: Context, account: str, json: bool): """Mute an account""" found_account = api.find_account(ctx.app, ctx.user, account) response = api.mute(ctx.app, ctx.user, found_account["id"]) if json: click.echo(response.text) else: click.secho(f"✓ You have muted {account}", fg="green") @cli.command() @click.argument("account") @json_option @pass_context def unmute(ctx: Context, account: str, json: bool): """Unmute an account""" found_account = api.find_account(ctx.app, ctx.user, account) response = api.unmute(ctx.app, ctx.user, found_account["id"]) if json: click.echo(response.text) else: click.secho(f"✓ {account} is no longer muted", fg="green") @cli.command() @json_option @pass_context def muted(ctx: Context, json: bool): """List muted accounts""" response = api.muted(ctx.app, ctx.user) if json: click.echo(pyjson.dumps(response)) else: if len(response) > 0: click.echo("Muted accounts:") print_acct_list(response) else: click.echo("No accounts muted") @cli.command() @click.argument("account") @json_option @pass_context def block(ctx: Context, account: str, json: bool): """Block an account""" found_account = api.find_account(ctx.app, ctx.user, account) response = api.block(ctx.app, ctx.user, found_account["id"]) if json: click.echo(response.text) else: click.secho(f"✓ You are now blocking {account}", fg="green") @cli.command() @click.argument("account") @json_option @pass_context def unblock(ctx: Context, account: str, json: bool): """Unblock an account""" found_account = api.find_account(ctx.app, ctx.user, account) response = api.unblock(ctx.app, ctx.user, found_account["id"]) if json: click.echo(response.text) else: click.secho(f"✓ {account} is no longer blocked", fg="green") @cli.command() @json_option @pass_context def blocked(ctx: Context, json: bool): """List blocked accounts""" response = api.blocked(ctx.app, ctx.user) if json: click.echo(pyjson.dumps(response)) else: if len(response) > 0: click.echo("Blocked accounts:") print_acct_list(response) else: click.echo("No accounts blocked") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703762179.0 toot-0.41.1/toot/cli/auth.py0000644000175000017500000001072714543254403016042 0ustar00ihabunekihabunekimport click import platform import sys import webbrowser from toot import api, config, __version__ from toot.auth import get_or_create_app, login_auth_code, login_username_password from toot.cli import AccountParamType, cli from toot.cli.validators import validate_instance instance_option = click.option( "--instance", "-i", "base_url", prompt="Enter instance URL", default="https://mastodon.social", callback=validate_instance, help="""Domain or base URL of the instance to log into, e.g. 'mastodon.social' or 'https://mastodon.social'""", ) @cli.command() def auth(): """Show logged in accounts and instances""" config_data = config.load_config() if not config_data["users"]: click.echo("You are not logged in to any accounts") return active_user = config_data["active_user"] click.echo("Authenticated accounts:") for uid, u in config_data["users"].items(): active_label = "ACTIVE" if active_user == uid else "" uid = click.style(uid, fg="green") active_label = click.style(active_label, fg="yellow") click.echo(f"* {uid} {active_label}") path = config.get_config_file_path() path = click.style(path, "blue") click.echo(f"\nAuth tokens are stored in: {path}") @cli.command() def env(): """Print environment information for inclusion in bug reports.""" click.echo(f"toot {__version__}") click.echo(f"Python {sys.version}") click.echo(platform.platform()) @cli.command(name="login_cli") @instance_option @click.option("--email", "-e", help="Email address to log in with", prompt=True) @click.option("--password", "-p", hidden=True, prompt=True, hide_input=True) def login_cli(base_url: str, email: str, password: str): """ Log into an instance from the console (not recommended) Does NOT support two factor authentication, may not work on instances other than Mastodon, mostly useful for scripting. """ app = get_or_create_app(base_url) login_username_password(app, email, password) click.secho("✓ Successfully logged in.", fg="green") click.echo("Access token saved to config at: ", nl=False) click.secho(config.get_config_file_path(), fg="green") LOGIN_EXPLANATION = """This authentication method requires you to log into your Mastodon instance in your browser, where you will be asked to authorize toot to access your account. When you do, you will be given an authorization code which you need to paste here.""".replace("\n", " ") @cli.command() @instance_option def login(base_url: str): """Log into an instance using your browser (recommended)""" app = get_or_create_app(base_url) url = api.get_browser_login_url(app) click.echo(click.wrap_text(LOGIN_EXPLANATION)) click.echo("\nLogin URL:") click.echo(url) yesno = click.prompt("Open link in default browser? [Y/n]", default="Y", show_default=False) if not yesno or yesno.lower() == 'y': webbrowser.open(url) authorization_code = "" while not authorization_code: authorization_code = click.prompt("Authorization code") login_auth_code(app, authorization_code) click.echo() click.secho("✓ Successfully logged in.", fg="green") @cli.command() @click.argument("account", type=AccountParamType(), required=False) def logout(account: str): """Log out of ACCOUNT, delete stored access keys""" accounts = _get_accounts_list() if not account: raise click.ClickException(f"Specify account to log out:\n{accounts}") user = config.load_user(account) if not user: raise click.ClickException(f"Account not found. Logged in accounts:\n{accounts}") config.delete_user(user) click.secho(f"✓ Account {account} logged out", fg="green") @cli.command() @click.argument("account", type=AccountParamType(), required=False) def activate(account: str): """Switch to logged in ACCOUNT.""" accounts = _get_accounts_list() if not account: raise click.ClickException(f"Specify account to activate:\n{accounts}") user = config.load_user(account) if not user: raise click.ClickException(f"Account not found. Logged in accounts:\n{accounts}") config.activate_user(user) click.secho(f"✓ Account {account} activated", fg="green") def _get_accounts_list() -> str: accounts = config.load_config()["users"].keys() if not accounts: raise click.ClickException("You're not logged into any accounts") return "\n".join([f"* {acct}" for acct in accounts]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/toot/cli/lists.py0000644000175000017500000002055314543235771016244 0ustar00ihabunekihabunekimport click import json as pyjson from toot import api, config from toot.cli import Context, cli, pass_context, json_option from toot.output import print_list_accounts, print_lists, print_warning @cli.group(invoke_without_command=True) @click.pass_context def lists(ctx: click.Context): """Display and manage lists""" if ctx.invoked_subcommand is None: print_warning("`toot lists` is deprecated in favour of `toot lists list`.\n" + "Run `toot lists -h` to see other list-related commands.") user, app = config.get_active_user_app() if not user or not app: raise click.ClickException("This command requires you to be logged in.") lists = api.get_lists(app, user) if lists: print_lists(lists) else: click.echo("You have no lists defined.") @lists.command() @json_option @pass_context def list(ctx: Context, json: bool): """List all your lists""" lists = api.get_lists(ctx.app, ctx.user) if json: click.echo(pyjson.dumps(lists)) else: if lists: print_lists(lists) else: click.echo("You have no lists defined.") @lists.command() @click.argument("title", required=False) @click.option("--id", help="List ID if not title is given") @json_option @pass_context def accounts(ctx: Context, title: str, id: str, json: bool): """List the accounts in a list""" list_id = _get_list_id(ctx, title, id) response = api.get_list_accounts(ctx.app, ctx.user, list_id) if json: click.echo(pyjson.dumps(response)) else: print_list_accounts(response) @lists.command() @click.argument("title") @click.option( "--replies-policy", type=click.Choice(["followed", "list", "none"]), default="none", help="Replies policy" ) @json_option @pass_context def create(ctx: Context, title: str, replies_policy: str, json: bool): """Create a list""" response = api.create_list(ctx.app, ctx.user, title=title, replies_policy=replies_policy) if json: print(response.text) else: click.secho(f"✓ List \"{title}\" created.", fg="green") @lists.command() @click.argument("title", required=False) @click.option("--id", help="List ID if not title is given") @json_option @pass_context def delete(ctx: Context, title: str, id: str, json: bool): """Delete a list""" list_id = _get_list_id(ctx, title, id) response = api.delete_list(ctx.app, ctx.user, list_id) if json: click.echo(response.text) else: click.secho(f"✓ List \"{title if title else id}\" deleted.", fg="green") @lists.command() @click.argument("title", required=False) @click.argument("account") @click.option("--id", help="List ID if not title is given") @json_option @pass_context def add(ctx: Context, title: str, account: str, id: str, json: bool): """Add an account to a list""" list_id = _get_list_id(ctx, title, id) found_account = api.find_account(ctx.app, ctx.user, account) try: response = api.add_accounts_to_list(ctx.app, ctx.user, list_id, [found_account["id"]]) if json: click.echo(response.text) else: click.secho(f"✓ Added account \"{account}\"", fg="green") except Exception: # TODO: this is slow, improve # if we failed to add the account, try to give a # more specific error message than "record not found" my_accounts = api.followers(ctx.app, ctx.user, found_account["id"]) found = False if my_accounts: for my_account in my_accounts: if my_account["id"] == found_account["id"]: found = True break if found is False: raise click.ClickException(f"You must follow @{account} before adding this account to a list.") raise @lists.command() @click.argument("title", required=False) @click.argument("account") @click.option("--id", help="List ID if not title is given") @json_option @pass_context def remove(ctx: Context, title: str, account: str, id: str, json: bool): """Remove an account from a list""" list_id = _get_list_id(ctx, title, id) found_account = api.find_account(ctx.app, ctx.user, account) response = api.remove_accounts_from_list(ctx.app, ctx.user, list_id, [found_account["id"]]) if json: click.echo(response.text) else: click.secho(f"✓ Removed account \"{account}\"", fg="green") # -- Deprecated commands ------------------------------------------------------- @cli.command(name="list_accounts", hidden=True) @click.argument("title", required=False) @click.option("--id", help="List ID if not title is given") @pass_context def list_accounts(ctx: Context, title: str, id: str): """List the accounts in a list""" print_warning("`toot list_accounts` is deprecated in favour of `toot lists accounts`") list_id = _get_list_id(ctx, title, id) response = api.get_list_accounts(ctx.app, ctx.user, list_id) print_list_accounts(response) @cli.command(name="list_create", hidden=True) @click.argument("title") @click.option( "--replies-policy", type=click.Choice(["followed", "list", "none"]), default="none", help="Replies policy" ) @pass_context def list_create(ctx: Context, title: str, replies_policy: str): """Create a list""" print_warning("`toot list_create` is deprecated in favour of `toot lists create`") api.create_list(ctx.app, ctx.user, title=title, replies_policy=replies_policy) click.secho(f"✓ List \"{title}\" created.", fg="green") @cli.command(name="list_delete", hidden=True) @click.argument("title", required=False) @click.option("--id", help="List ID if not title is given") @pass_context def list_delete(ctx: Context, title: str, id: str): """Delete a list""" print_warning("`toot list_delete` is deprecated in favour of `toot lists delete`") list_id = _get_list_id(ctx, title, id) api.delete_list(ctx.app, ctx.user, list_id) click.secho(f"✓ List \"{title if title else id}\" deleted.", fg="green") @cli.command(name="list_add", hidden=True) @click.argument("title", required=False) @click.argument("account") @click.option("--id", help="List ID if not title is given") @pass_context def list_add(ctx: Context, title: str, account: str, id: str): """Add an account to a list""" print_warning("`toot list_add` is deprecated in favour of `toot lists add`") list_id = _get_list_id(ctx, title, id) found_account = api.find_account(ctx.app, ctx.user, account) try: api.add_accounts_to_list(ctx.app, ctx.user, list_id, [found_account["id"]]) except Exception: # if we failed to add the account, try to give a # more specific error message than "record not found" my_accounts = api.followers(ctx.app, ctx.user, found_account["id"]) found = False if my_accounts: for my_account in my_accounts: if my_account["id"] == found_account["id"]: found = True break if found is False: raise click.ClickException(f"You must follow @{account} before adding this account to a list.") raise click.secho(f"✓ Added account \"{account}\"", fg="green") @cli.command(name="list_remove", hidden=True) @click.argument("title", required=False) @click.argument("account") @click.option("--id", help="List ID if not title is given") @pass_context def list_remove(ctx: Context, title: str, account: str, id: str): """Remove an account from a list""" print_warning("`toot list_remove` is deprecated in favour of `toot lists remove`") list_id = _get_list_id(ctx, title, id) found_account = api.find_account(ctx.app, ctx.user, account) api.remove_accounts_from_list(ctx.app, ctx.user, list_id, [found_account["id"]]) click.secho(f"✓ Removed account \"{account}\"", fg="green") def _get_list_id(ctx: Context, title, list_id): if not list_id and not title: raise click.ClickException("Please specify list title or ID") lists = api.get_lists(ctx.app, ctx.user) matched_ids = [ list["id"] for list in lists if list["title"].lower() == title.lower() or list["id"] == list_id ] if not matched_ids: raise click.ClickException("List not found") if len(matched_ids) > 1: raise click.ClickException("Found multiple lists with the same title, please specify the ID instead") return matched_ids[0] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704228932.0 toot-0.41.1/toot/cli/post.py0000644000175000017500000002115714545074104016065 0ustar00ihabunekihabunekimport click import os import sys from datetime import datetime, timedelta, timezone from time import sleep, time from typing import BinaryIO, Optional, Tuple from toot import api, config from toot.cli import AccountParamType, cli, json_option, pass_context, Context from toot.cli import DURATION_EXAMPLES, VISIBILITY_CHOICES from toot.cli.validators import validate_duration, validate_language from toot.entities import MediaAttachment, from_dict from toot.utils import EOF_KEY, delete_tmp_status_file, editor_input, multiline_input from toot.utils.datetime import parse_datetime @cli.command() @click.argument("text", required=False) @click.option( "--media", "-m", help="""Path to media file to attach, can be used multiple times to attach multiple files.""", type=click.File(mode="rb"), multiple=True ) @click.option( "--description", "-d", "descriptions", help="""Plain-text description of the media for accessibility purposes, one per attached media""", multiple=True, ) @click.option( "--thumbnail", "thumbnails", help="Path to an image file to serve as media thumbnail, one per attached media", type=click.File(mode="rb"), multiple=True ) @click.option( "--visibility", "-v", help="Post visibility", type=click.Choice(VISIBILITY_CHOICES), ) @click.option( "--sensitive", "-s", help="Mark status and attached media as sensitive", default=False, is_flag=True, ) @click.option( "--spoiler-text", "-p", help="Text to be shown as a warning or subject before the actual content.", ) @click.option( "--reply-to", "-r", help="ID of the status being replied to, if status is a reply.", ) @click.option( "--language", "-l", help="ISO 639-1 language code of the toot, to skip automatic detection.", callback=validate_language, ) @click.option( "--editor", "-e", is_flag=False, flag_value=os.getenv("EDITOR"), help="""Specify an editor to compose your toot. When used without a value it will use the editor defined in the $EDITOR environment variable.""", ) @click.option( "--scheduled-at", help="""ISO 8601 Datetime at which to schedule a status. Must be at least 5 minutes in the future.""", ) @click.option( "--scheduled-in", help=f"""Schedule the toot to be posted after a given amount of time, {DURATION_EXAMPLES}. Must be at least 5 minutes.""", callback=validate_duration, ) @click.option( "--content-type", "-t", help="MIME type for the status text (not supported on all instances)", ) @click.option( "--poll-option", help="Possible answer to the poll, can be given multiple times.", multiple=True, ) @click.option( "--poll-expires-in", help=f"Duration that the poll should be open, {DURATION_EXAMPLES}", callback=validate_duration, default="24h", ) @click.option( "--poll-multiple", help="Allow multiple answers to be selected.", is_flag=True, default=False, ) @click.option( "--poll-hide-totals", help="Hide vote counts until the poll ends.", is_flag=True, default=False, ) @click.option( "-u", "--using", type=AccountParamType(), help="The account to use, overrides the active account.", ) @json_option @pass_context def post( ctx: Context, text: Optional[str], media: Tuple[str], descriptions: Tuple[str], thumbnails: Tuple[str], visibility: Optional[str], sensitive: bool, spoiler_text: Optional[str], reply_to: Optional[str], language: Optional[str], editor: Optional[str], scheduled_at: Optional[str], scheduled_in: Optional[int], content_type: Optional[str], poll_option: Tuple[str], poll_expires_in: int, poll_multiple: bool, poll_hide_totals: bool, json: bool, using: str ): """Post a new status""" if len(media) > 4: raise click.ClickException("Cannot attach more than 4 files.") if using: user, app = config.get_user_app(using) if not user or not app: raise click.ClickException(f"Account '{using}' not found. Run `toot auth` to see available accounts.") else: user, app = ctx.user, ctx.app media_ids = _upload_media(ctx.app, ctx.user, media, descriptions, thumbnails) status_text = _get_status_text(text, editor, media) scheduled_at = _get_scheduled_at(scheduled_at, scheduled_in) if not status_text and not media_ids: raise click.ClickException("You must specify either text or media to post.") response = api.post_status( app, user, status_text, visibility=visibility, media_ids=media_ids, sensitive=sensitive, spoiler_text=spoiler_text, in_reply_to_id=reply_to, language=language, scheduled_at=scheduled_at, content_type=content_type, poll_options=poll_option, poll_expires_in=poll_expires_in, poll_multiple=poll_multiple, poll_hide_totals=poll_hide_totals, ) if json: click.echo(response.text) else: status = response.json() if "scheduled_at" in status: scheduled_at = parse_datetime(status["scheduled_at"]) scheduled_at = datetime.strftime(scheduled_at, "%Y-%m-%d %H:%M:%S%z") click.echo(f"Toot scheduled for: {scheduled_at}") else: click.echo(f"Toot posted: {status['url']}") delete_tmp_status_file() @cli.command() @click.argument("file", type=click.File(mode="rb")) @click.option( "--description", "-d", help="Plain-text description of the media for accessibility purposes" ) @json_option @pass_context def upload( ctx: Context, file: BinaryIO, description: Optional[str], json: bool, ): """Upload an image or video file This is probably not very useful, see `toot post --media` instead. """ response = _do_upload(ctx.app, ctx.user, file, description, None) if json: click.echo(response.text) else: media = from_dict(MediaAttachment, response.json()) click.echo() click.echo(f"Successfully uploaded media ID {media.id}, type '{media.type}'") click.echo(f"URL: {media.url}") click.echo(f"Preview URL: {media.preview_url}") def _get_status_text(text, editor, media): isatty = sys.stdin.isatty() if not text and not isatty: text = sys.stdin.read().rstrip() if isatty: if editor: text = editor_input(editor, text) elif not text and not media: click.echo(f"Write or paste your toot. Press {EOF_KEY} to post it.") text = multiline_input() return text def _get_scheduled_at(scheduled_at, scheduled_in): if scheduled_at: return scheduled_at if scheduled_in: scheduled_at = datetime.now(timezone.utc) + timedelta(seconds=scheduled_in) return scheduled_at.replace(microsecond=0).isoformat() return None def _upload_media(app, user, media, descriptions, thumbnails): # Match media to corresponding descriptions and thumbnail media = media or [] descriptions = descriptions or [] thumbnails = thumbnails or [] uploaded_media = [] for idx, file in enumerate(media): description = descriptions[idx].strip() if idx < len(descriptions) else None thumbnail = thumbnails[idx] if idx < len(thumbnails) else None result = _do_upload(app, user, file, description, thumbnail).json() uploaded_media.append(result) _wait_until_all_processed(app, user, uploaded_media) return [m["id"] for m in uploaded_media] def _do_upload(app, user, file, description, thumbnail): return api.upload_media(app, user, file, description=description, thumbnail=thumbnail) def _wait_until_all_processed(app, user, uploaded_media): """ Media is uploaded asynchronously, and cannot be attached until the server has finished processing it. This function waits for that to happen. Once media is processed, it will have the URL populated. """ if all(m["url"] for m in uploaded_media): return # Timeout after waiting 1 minute start_time = time() timeout = 60 click.echo("Waiting for media to finish processing...") for media in uploaded_media: _wait_until_processed(app, user, media, start_time, timeout) def _wait_until_processed(app, user, media, start_time, timeout): if media["url"]: return media = api.get_media(app, user, media["id"]) while not media["url"]: sleep(1) if time() > start_time + timeout: raise click.ClickException(f"Media not processed by server after {timeout} seconds. Aborting.") media = api.get_media(app, user, media["id"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704109151.0 toot-0.41.1/toot/cli/read.py0000644000175000017500000000700214544522137016007 0ustar00ihabunekihabunekimport click import json as pyjson from itertools import chain from typing import Optional from toot import api from toot.cli.validators import validate_instance from toot.entities import Instance, Status, from_dict, Account from toot.exceptions import ApiError, ConsoleError from toot.output import print_account, print_instance, print_search_results, print_status, print_timeline from toot.cli import InstanceParamType, cli, get_context, json_option, pass_context, Context @cli.command() @json_option @pass_context def whoami(ctx: Context, json: bool): """Display logged in user details""" response = api.verify_credentials(ctx.app, ctx.user) if json: click.echo(response.text) else: account = from_dict(Account, response.json()) print_account(account) @cli.command() @click.argument("account") @json_option @pass_context def whois(ctx: Context, account: str, json: bool): """Display account details""" account_dict = api.find_account(ctx.app, ctx.user, account) # Here it's not possible to avoid parsing json since it's needed to find the account. if json: click.echo(pyjson.dumps(account_dict)) else: account_obj = from_dict(Account, account_dict) print_account(account_obj) @cli.command() @click.argument("instance", type=InstanceParamType(), callback=validate_instance, required=False) @json_option def instance(instance: Optional[str], json: bool): """Display instance details INSTANCE can be a domain or base URL of the instance to display. e.g. 'mastodon.social' or 'https://mastodon.social'. If not given will display details for the currently logged in instance. """ if not instance: context = get_context() if not context.app: raise click.ClickException("INSTANCE argument not given and not logged in") instance = context.app.base_url try: response = api.get_instance(instance) except ApiError: raise ConsoleError( f"Instance not found at {instance}.\n" + "The given domain probably does not host a Mastodon instance." ) if json: click.echo(response.text) else: print_instance(from_dict(Instance, response.json())) @cli.command() @click.argument("query") @click.option("-r", "--resolve", is_flag=True, help="Resolve non-local accounts") @json_option @pass_context def search(ctx: Context, query: str, resolve: bool, json: bool): """Search for users or hashtags""" response = api.search(ctx.app, ctx.user, query, resolve) if json: click.echo(response.text) else: print_search_results(response.json()) @cli.command() @click.argument("status_id") @json_option @pass_context def status(ctx: Context, status_id: str, json: bool): """Show a single status""" response = api.fetch_status(ctx.app, ctx.user, status_id) if json: click.echo(response.text) else: status = from_dict(Status, response.json()) print_status(status) @cli.command() @click.argument("status_id") @json_option @pass_context def thread(ctx: Context, status_id: str, json: bool): """Show thread for a toot.""" context_response = api.context(ctx.app, ctx.user, status_id) if json: click.echo(context_response.text) else: toot = api.fetch_status(ctx.app, ctx.user, status_id).json() context = context_response.json() statuses = chain(context["ancestors"], [toot], context["descendants"]) print_timeline(from_dict(Status, s) for s in statuses) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/toot/cli/statuses.py0000644000175000017500000000746114543235771016764 0ustar00ihabunekihabunekimport click from toot import api from toot.cli import cli, json_option, Context, pass_context from toot.cli import VISIBILITY_CHOICES from toot.output import print_table @cli.command() @click.argument("status_id") @json_option @pass_context def delete(ctx: Context, status_id: str, json: bool): """Delete a status""" response = api.delete_status(ctx.app, ctx.user, status_id) if json: click.echo(response.text) else: click.secho("✓ Status deleted", fg="green") @cli.command() @click.argument("status_id") @json_option @pass_context def favourite(ctx: Context, status_id: str, json: bool): """Favourite a status""" response = api.favourite(ctx.app, ctx.user, status_id) if json: click.echo(response.text) else: click.secho("✓ Status favourited", fg="green") @cli.command() @click.argument("status_id") @json_option @pass_context def unfavourite(ctx: Context, status_id: str, json: bool): """Unfavourite a status""" response = api.unfavourite(ctx.app, ctx.user, status_id) if json: click.echo(response.text) else: click.secho("✓ Status unfavourited", fg="green") @cli.command() @click.argument("status_id") @click.option( "--visibility", "-v", help="Post visibility", type=click.Choice(VISIBILITY_CHOICES), default="public", ) @json_option @pass_context def reblog(ctx: Context, status_id: str, visibility: str, json: bool): """Reblog (boost) a status""" response = api.reblog(ctx.app, ctx.user, status_id, visibility=visibility) if json: click.echo(response.text) else: click.secho("✓ Status reblogged", fg="green") @cli.command() @click.argument("status_id") @json_option @pass_context def unreblog(ctx: Context, status_id: str, json: bool): """Unreblog (unboost) a status""" response = api.unreblog(ctx.app, ctx.user, status_id) if json: click.echo(response.text) else: click.secho("✓ Status unreblogged", fg="green") @cli.command() @click.argument("status_id") @json_option @pass_context def pin(ctx: Context, status_id: str, json: bool): """Pin a status""" response = api.pin(ctx.app, ctx.user, status_id) if json: click.echo(response.text) else: click.secho("✓ Status pinned", fg="green") @cli.command() @click.argument("status_id") @json_option @pass_context def unpin(ctx: Context, status_id: str, json: bool): """Unpin a status""" response = api.unpin(ctx.app, ctx.user, status_id) if json: click.echo(response.text) else: click.secho("✓ Status unpinned", fg="green") @cli.command() @click.argument("status_id") @json_option @pass_context def bookmark(ctx: Context, status_id: str, json: bool): """Bookmark a status""" response = api.bookmark(ctx.app, ctx.user, status_id) if json: click.echo(response.text) else: click.secho("✓ Status bookmarked", fg="green") @cli.command() @click.argument("status_id") @json_option @pass_context def unbookmark(ctx: Context, status_id: str, json: bool): """Unbookmark a status""" response = api.unbookmark(ctx.app, ctx.user, status_id) if json: click.echo(response.text) else: click.secho("✓ Status unbookmarked", fg="green") @cli.command(name="reblogged_by") @click.argument("status_id") @json_option @pass_context def reblogged_by(ctx: Context, status_id: str, json: bool): """Show accounts that reblogged a status""" response = api.reblogged_by(ctx.app, ctx.user, status_id) if json: click.echo(response.text) else: rows = [[a["acct"], a["display_name"]] for a in response.json()] if rows: headers = ["Account", "Display name"] print_table(headers, rows) else: click.echo("This status is not reblogged by anyone") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/toot/cli/tags.py0000644000175000017500000001057314543235771016045 0ustar00ihabunekihabunekimport click import json as pyjson from toot import api from toot.cli import cli, pass_context, json_option, Context from toot.entities import Tag, from_dict from toot.output import print_tag_list, print_warning @cli.group() def tags(): """List, follow, and unfollow tags""" @tags.command() @click.argument("tag") @json_option @pass_context def info(ctx: Context, tag, json: bool): """Show a hashtag and its associated information""" tag = api.find_tag(ctx.app, ctx.user, tag) if not tag: raise click.ClickException("Tag not found") if json: click.echo(pyjson.dumps(tag)) else: tag = from_dict(Tag, tag) click.secho(f"#{tag.name}", fg="yellow") click.secho(tag.url, italic=True) if tag.following: click.echo("Followed") else: click.echo("Not followed") @tags.command() @json_option @pass_context def followed(ctx: Context, json: bool): """List followed tags""" tags = api.followed_tags(ctx.app, ctx.user) if json: click.echo(pyjson.dumps(tags)) else: if tags: print_tag_list(tags) else: click.echo("You're not following any hashtags") @tags.command() @click.argument("tag") @json_option @pass_context def follow(ctx: Context, tag: str, json: bool): """Follow a hashtag""" tag = tag.lstrip("#") response = api.follow_tag(ctx.app, ctx.user, tag) if json: click.echo(response.text) else: click.secho(f"✓ You are now following #{tag}", fg="green") @tags.command() @click.argument("tag") @json_option @pass_context def unfollow(ctx: Context, tag: str, json: bool): """Unfollow a hashtag""" tag = tag.lstrip("#") response = api.unfollow_tag(ctx.app, ctx.user, tag) if json: click.echo(response.text) else: click.secho(f"✓ You are no longer following #{tag}", fg="green") @tags.command() @json_option @pass_context def featured(ctx: Context, json: bool): """List hashtags featured on your profile.""" response = api.featured_tags(ctx.app, ctx.user) if json: click.echo(response.text) else: tags = response.json() if tags: print_tag_list(tags) else: click.echo("You don't have any featured hashtags") @tags.command() @click.argument("tag") @json_option @pass_context def feature(ctx: Context, tag: str, json: bool): """Feature a hashtag on your profile""" tag = tag.lstrip("#") response = api.feature_tag(ctx.app, ctx.user, tag) if json: click.echo(response.text) else: click.secho(f"✓ Tag #{tag} is now featured", fg="green") @tags.command() @click.argument("tag") @json_option @pass_context def unfeature(ctx: Context, tag: str, json: bool): """Unfollow a hashtag TAG can either be a tag name like "#foo" or "foo" or a tag ID. """ featured_tag = api.find_featured_tag(ctx.app, ctx.user, tag) # TODO: should this be idempotent? if not featured_tag: raise click.ClickException(f"Tag {tag} is not featured") response = api.unfeature_tag(ctx.app, ctx.user, featured_tag["id"]) if json: click.echo(response.text) else: click.secho(f"✓ Tag #{featured_tag['name']} is no longer featured", fg="green") # -- Deprecated commands ------------------------------------------------------- @cli.command(name="tags_followed", hidden=True) @pass_context def tags_followed(ctx: Context): """List hashtags you follow""" print_warning("`toot tags_followed` is deprecated in favour of `toot tags followed`") response = api.followed_tags(ctx.app, ctx.user) print_tag_list(response) @cli.command(name="tags_follow", hidden=True) @click.argument("tag") @pass_context def tags_follow(ctx: Context, tag: str): """Follow a hashtag""" print_warning("`toot tags_follow` is deprecated in favour of `toot tags follow`") tag = tag.lstrip("#") api.follow_tag(ctx.app, ctx.user, tag) click.secho(f"✓ You are now following #{tag}", fg="green") @cli.command(name="tags_unfollow", hidden=True) @click.argument("tag") @pass_context def tags_unfollow(ctx: Context, tag: str): """Unfollow a hashtag""" print_warning("`toot tags_unfollow` is deprecated in favour of `toot tags unfollow`") tag = tag.lstrip("#") api.unfollow_tag(ctx.app, ctx.user, tag) click.secho(f"✓ You are no longer following #{tag}", fg="green") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703786428.0 toot-0.41.1/toot/cli/timelines.py0000644000175000017500000001230314543333674017072 0ustar00ihabunekihabunekimport sys import click from toot import api from toot.cli import InstanceParamType, cli, get_context, pass_context, Context from typing import Optional from toot.cli.validators import validate_instance from toot.entities import Notification, Status, from_dict from toot.output import print_notifications, print_timeline @cli.command() @click.option( "--instance", "-i", type=InstanceParamType(), callback=validate_instance, help="""Domain or base URL of the instance from which to read, e.g. 'mastodon.social' or 'https://mastodon.social'""", ) @click.option("--account", "-a", help="Show account timeline") @click.option("--list", help="Show list timeline") @click.option("--tag", "-t", help="Show hashtag timeline") @click.option("--public", "-p", is_flag=True, help="Show public timeline") @click.option( "--local", "-l", is_flag=True, help="Show only statuses from the local instance (public and tag timelines only)" ) @click.option( "--reverse", "-r", is_flag=True, help="Reverse the order of the shown timeline (new posts at the bottom)" ) @click.option( "--once", "-1", is_flag=True, help="Only show the first toots, do not prompt to continue" ) @click.option( "--count", "-c", type=int, default=10, help="Number of posts per page (max 20)" ) def timeline( instance: Optional[str], account: Optional[str], list: Optional[str], tag: Optional[str], public: bool, local: bool, reverse: bool, once: bool, count: int, ): """Show recent items in a timeline By default shows the home timeline. """ if len([arg for arg in [tag, list, public, account] if arg]) > 1: raise click.ClickException("Only one of --public, --tag, --account, or --list can be used at one time.") if local and not (public or tag): raise click.ClickException("The --local option is only valid alongside --public or --tag.") if instance and not (public or tag): raise click.ClickException("The --instance option is only valid alongside --public or --tag.") if public and instance: generator = api.anon_public_timeline_generator(instance, local, count) elif tag and instance: generator = api.anon_tag_timeline_generator(instance, tag, local, count) else: ctx = get_context() list_id = _get_list_id(ctx, list) """Show recent statuses in a timeline""" generator = api.get_timeline_generator( ctx.app, ctx.user, account=account, list_id=list_id, tag=tag, public=public, local=local, limit=count, ) _show_timeline(generator, reverse, once) @cli.command() @click.option( "--reverse", "-r", is_flag=True, help="Reverse the order of the shown timeline (new posts at the bottom)" ) @click.option( "--once", "-1", is_flag=True, help="Only show the first toots, do not prompt to continue" ) @click.option( "--count", "-c", type=int, default=10, help="Number of posts per page (max 20)" ) @pass_context def bookmarks( ctx: Context, reverse: bool, once: bool, count: int, ): """Show recent statuses in a timeline""" generator = api.bookmark_timeline_generator(ctx.app, ctx.user, limit=count) _show_timeline(generator, reverse, once) @cli.command() @click.option("--clear", help="Dismiss all notifications and exit") @click.option( "--reverse", "-r", is_flag=True, help="Reverse the order of the shown notifications (newest on top)" ) @click.option( "--mentions", "-m", is_flag=True, help="Show only mentions" ) @pass_context def notifications( ctx: Context, clear: bool, reverse: bool, mentions: int, ): """Show notifications""" if clear: api.clear_notifications(ctx.app, ctx.user) click.secho("✓ Notifications cleared", fg="green") return exclude = [] if mentions: # Filter everything except mentions # https://docs.joinmastodon.org/methods/notifications/ exclude = ["follow", "favourite", "reblog", "poll", "follow_request"] notifications = api.get_notifications(ctx.app, ctx.user, exclude_types=exclude) if not notifications: click.echo("You have no notifications") return if reverse: notifications = reversed(notifications) notifications = [from_dict(Notification, n) for n in notifications] print_notifications(notifications) def _show_timeline(generator, reverse, once): while True: try: items = next(generator) except StopIteration: click.echo("That's all folks.") return if reverse: items = reversed(items) statuses = [from_dict(Status, item) for item in items] print_timeline(statuses) if once or not sys.stdout.isatty(): break char = input("\nContinue? [Y/n] ") if char.lower() == "n": break def _get_list_id(ctx: Context, value: Optional[str]) -> Optional[str]: if not value: return None lists = api.get_lists(ctx.app, ctx.user) for list in lists: if list["id"] == value or list["title"] == value: return list["id"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704047802.0 toot-0.41.1/toot/cli/tui.py0000644000175000017500000000266314544332272015704 0ustar00ihabunekihabunekimport click from typing import Optional from toot.cli import TUI_COLORS, VISIBILITY_CHOICES, Context, cli, pass_context from toot.cli.validators import validate_tui_colors from toot.tui.app import TUI, TuiOptions COLOR_OPTIONS = ", ".join(TUI_COLORS.keys()) @cli.command() @click.option( "-r", "--relative-datetimes", is_flag=True, help="Show relative datetimes in status list" ) @click.option( "-m", "--media-viewer", help="Program to invoke with media URLs to display the media files, such as 'feh'" ) @click.option( "-c", "--colors", callback=validate_tui_colors, help=f"""Number of colors to use, one of {COLOR_OPTIONS}, defaults to 16 if using --color, and 1 if using --no-color.""" ) @click.option( "-v", "--default-visibility", type=click.Choice(VISIBILITY_CHOICES), help="Default visibility when posting new toots; overrides the server-side preference" ) @pass_context def tui( ctx: Context, colors: Optional[int], media_viewer: Optional[str], relative_datetimes: bool, default_visibility: Optional[str] ): """Launches the toot terminal user interface""" if colors is None: colors = 16 if ctx.color else 1 options = TuiOptions( colors=colors, media_viewer=media_viewer, relative_datetimes=relative_datetimes, default_visibility=default_visibility ) tui = TUI.create(ctx.app, ctx.user, options) tui.run() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/toot/cli/validators.py0000644000175000017500000000361014543235771017251 0ustar00ihabunekihabunekimport click import re from click import Context from typing import Optional from toot.cli import TUI_COLORS def validate_language(ctx: Context, param: str, value: Optional[str]): if value is None: return None value = value.strip().lower() if re.match(r"^[a-z]{2}$", value): return value raise click.BadParameter("Language should be a two letter abbreviation.") def validate_duration(ctx: Context, param: str, value: Optional[str]) -> Optional[int]: if value is None: return None match = re.match(r"""^ (([0-9]+)\s*(days|day|d))?\s* (([0-9]+)\s*(hours|hour|h))?\s* (([0-9]+)\s*(minutes|minute|m))?\s* (([0-9]+)\s*(seconds|second|s))?\s* $""", value, re.X) if not match: raise click.BadParameter(f"Invalid duration: {value}") days = match.group(2) hours = match.group(5) minutes = match.group(8) seconds = match.group(11) days = int(match.group(2) or 0) * 60 * 60 * 24 hours = int(match.group(5) or 0) * 60 * 60 minutes = int(match.group(8) or 0) * 60 seconds = int(match.group(11) or 0) duration = days + hours + minutes + seconds if duration == 0: raise click.BadParameter("Empty duration") return duration def validate_instance(ctx: click.Context, param: str, value: Optional[str]): """ Instance can be given either as a base URL or the domain name. Return the base URL. """ if not value: return None value = value.rstrip("/") return value if value.startswith("http") else f"https://{value}" def validate_tui_colors(ctx, param, value) -> Optional[int]: if value is None: return None if value in TUI_COLORS.values(): return value if value in TUI_COLORS.keys(): return TUI_COLORS[value] raise click.BadParameter(f"Invalid value: {value}. Expected one of: {', '.join(TUI_COLORS)}") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703761271.0 toot-0.41.1/toot/config.py0000644000175000017500000000663114543252567015607 0ustar00ihabunekihabunekimport json import os from contextlib import contextmanager from os.path import dirname, join from typing import Optional from toot import User, App, get_config_dir from toot.exceptions import ConsoleError TOOT_CONFIG_FILE_NAME = "config.json" def get_config_file_path(): """Returns the path to toot config file.""" return join(get_config_dir(), TOOT_CONFIG_FILE_NAME) def user_id(user): return "{}@{}".format(user.username, user.instance) def make_config(path): """Creates an empty toot configuration file.""" config = { "apps": {}, "users": {}, "active_user": None, } # Ensure dir exists os.makedirs(dirname(path), exist_ok=True) # Create file with 600 permissions since it contains secrets fd = os.open(path, os.O_CREAT | os.O_WRONLY, 0o600) with os.fdopen(fd, 'w') as f: json.dump(config, f, indent=True) def load_config(): # Just to prevent accidentally running tests on production if os.environ.get("TOOT_TESTING"): raise Exception("Tests should not access the config file!") path = get_config_file_path() if not os.path.exists(path): make_config(path) with open(path) as f: return json.load(f) def save_config(config): path = get_config_file_path() with open(path, "w") as f: return json.dump(config, f, indent=True, sort_keys=True) def extract_user_app(config, user_id): if user_id not in config['users']: return None, None user_data = config['users'][user_id] instance = user_data['instance'] if instance not in config['apps']: return None, None app_data = config['apps'][instance] return User(**user_data), App(**app_data) def get_active_user_app(): """Returns (User, App) of active user or (None, None) if no user is active.""" config = load_config() if config['active_user']: return extract_user_app(config, config['active_user']) return None, None def get_user_app(user_id): """Returns (User, App) for given user ID or (None, None) if user is not logged in.""" return extract_user_app(load_config(), user_id) def load_app(instance: str) -> Optional[App]: config = load_config() if instance in config['apps']: return App(**config['apps'][instance]) def load_user(user_id, throw=False): config = load_config() if user_id in config['users']: return User(**config['users'][user_id]) if throw: raise ConsoleError("User '{}' not found".format(user_id)) def get_user_list(): config = load_config() return config['users'] @contextmanager def edit_config(): config = load_config() yield config save_config(config) def save_app(app: App): with edit_config() as config: config['apps'][app.instance] = app._asdict() def delete_app(config, app): with edit_config() as config: config['apps'].pop(app.instance, None) def save_user(user: User, activate=True): with edit_config() as config: config['users'][user_id(user)] = user._asdict() if activate: config['active_user'] = user_id(user) def delete_user(user: User): with edit_config() as config: config['users'].pop(user_id(user), None) if config['active_user'] == user_id(user): config['active_user'] = None def activate_user(user: User): with edit_config() as config: config['active_user'] = user_id(user) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704109151.0 toot-0.41.1/toot/entities.py0000644000175000017500000003030414544522137016152 0ustar00ihabunekihabunek""" Dataclasses which represent entities returned by the Mastodon API. Data classes my have an optional static method named `__toot_prepare__` which is used when constructing the data class using `from_dict`. The method will be called with the dict and may modify it and return a modified dict. This is used to implement any pre-processing which may be required, e.g. to support different versions of the Mastodon API. """ import dataclasses from dataclasses import dataclass, is_dataclass from datetime import date, datetime from functools import lru_cache from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar, Union from typing import get_type_hints from toot.typing_compat import get_args, get_origin from toot.utils import get_text from toot.utils.datetime import parse_datetime @dataclass class AccountField: """ https://docs.joinmastodon.org/entities/Account/#Field """ name: str value: str verified_at: Optional[datetime] @dataclass class CustomEmoji: """ https://docs.joinmastodon.org/entities/CustomEmoji/ """ shortcode: str url: str static_url: str visible_in_picker: bool category: str @dataclass class Account: """ https://docs.joinmastodon.org/entities/Account/ """ id: str username: str acct: str url: str display_name: str note: str avatar: str avatar_static: str header: str header_static: str locked: bool fields: List[AccountField] emojis: List[CustomEmoji] bot: bool group: bool discoverable: Optional[bool] noindex: Optional[bool] moved: Optional["Account"] suspended: Optional[bool] limited: Optional[bool] created_at: datetime last_status_at: Optional[date] statuses_count: int followers_count: int following_count: int source: Optional[dict] @staticmethod def __toot_prepare__(obj: Dict) -> Dict: # Pleroma has not yet converted last_status_at from datetime to date # so trim it here so it doesn't break when converting to date. # See: https://git.pleroma.social/pleroma/pleroma/-/issues/1470 last_status_at = obj.get("last_status_at") if last_status_at: obj.update(last_status_at=obj["last_status_at"][:10]) return obj @property def note_plaintext(self) -> str: return get_text(self.note) @dataclass class Application: """ https://docs.joinmastodon.org/entities/Status/#application """ name: str website: Optional[str] @dataclass class MediaAttachment: """ https://docs.joinmastodon.org/entities/MediaAttachment/ """ id: str type: str url: str preview_url: str remote_url: Optional[str] meta: dict description: str blurhash: str @dataclass class StatusMention: """ https://docs.joinmastodon.org/entities/Status/#Mention """ id: str username: str url: str acct: str @dataclass class StatusTag: """ https://docs.joinmastodon.org/entities/Status/#Tag """ name: str url: str @dataclass class PollOption: """ https://docs.joinmastodon.org/entities/Poll/#Option """ title: str votes_count: Optional[int] @dataclass class Poll: """ https://docs.joinmastodon.org/entities/Poll/ """ id: str expires_at: Optional[datetime] expired: bool multiple: bool votes_count: int voters_count: Optional[int] options: List[PollOption] emojis: List[CustomEmoji] voted: Optional[bool] own_votes: Optional[List[int]] @dataclass class PreviewCard: """ https://docs.joinmastodon.org/entities/PreviewCard/ """ url: str title: str description: str type: str author_name: str author_url: str provider_name: str provider_url: str html: str width: int height: int image: Optional[str] embed_url: str blurhash: Optional[str] @dataclass class FilterKeyword: """ https://docs.joinmastodon.org/entities/FilterKeyword/ """ id: str keyword: str whole_word: str @dataclass class FilterStatus: """ https://docs.joinmastodon.org/entities/FilterStatus/ """ id: str status_id: str @dataclass class Filter: """ https://docs.joinmastodon.org/entities/Filter/ """ id: str title: str context: List[str] expires_at: Optional[datetime] filter_action: str keywords: List[FilterKeyword] statuses: List[FilterStatus] @dataclass class FilterResult: """ https://docs.joinmastodon.org/entities/FilterResult/ """ filter: Filter keyword_matches: Optional[List[str]] status_matches: Optional[str] @dataclass class Status: """ https://docs.joinmastodon.org/entities/Status/ """ id: str uri: str created_at: datetime account: Account content: str visibility: str sensitive: bool spoiler_text: str media_attachments: List[MediaAttachment] application: Optional[Application] mentions: List[StatusMention] tags: List[StatusTag] emojis: List[CustomEmoji] reblogs_count: int favourites_count: int replies_count: int url: Optional[str] in_reply_to_id: Optional[str] in_reply_to_account_id: Optional[str] reblog: Optional["Status"] poll: Optional[Poll] card: Optional[PreviewCard] language: Optional[str] text: Optional[str] edited_at: Optional[datetime] favourited: Optional[bool] reblogged: Optional[bool] muted: Optional[bool] bookmarked: Optional[bool] pinned: Optional[bool] filtered: Optional[List[FilterResult]] @property def original(self) -> "Status": return self.reblog or self @staticmethod def __toot_prepare__(obj: Dict) -> Dict: # Pleroma has a bug where created_at is set to an empty string. # To avoid marking created_at as optional, which would require work # because we count on it always existing, set it to current datetime. # Possible underlying issue: # https://git.pleroma.social/pleroma/pleroma/-/issues/2851 if not obj["created_at"]: obj["created_at"] = datetime.now().astimezone().isoformat() return obj @dataclass class Report: """ https://docs.joinmastodon.org/entities/Report/ """ id: str action_taken: bool action_taken_at: Optional[datetime] category: str comment: str forwarded: bool created_at: datetime status_ids: Optional[List[str]] rule_ids: Optional[List[str]] target_account: Account @dataclass class Notification: """ https://docs.joinmastodon.org/entities/Notification/ """ id: str type: str created_at: datetime account: Account status: Optional[Status] report: Optional[Report] @dataclass class InstanceUrls: streaming_api: str @dataclass class InstanceStats: user_count: int status_count: int domain_count: int @dataclass class InstanceConfigurationStatuses: max_characters: int max_media_attachments: int characters_reserved_per_url: int @dataclass class InstanceConfigurationMediaAttachments: supported_mime_types: List[str] image_size_limit: int image_matrix_limit: int video_size_limit: int video_frame_rate_limit: int video_matrix_limit: int @dataclass class InstanceConfigurationPolls: max_options: int max_characters_per_option: int min_expiration: int max_expiration: int @dataclass class InstanceConfiguration: """ https://docs.joinmastodon.org/entities/V1_Instance/#configuration """ statuses: InstanceConfigurationStatuses media_attachments: InstanceConfigurationMediaAttachments polls: InstanceConfigurationPolls @dataclass class Rule: """ https://docs.joinmastodon.org/entities/Rule/ """ id: str text: str @dataclass class Instance: """ https://docs.joinmastodon.org/entities/V1_Instance/ """ uri: str title: str short_description: str description: str email: str version: str urls: InstanceUrls stats: InstanceStats thumbnail: Optional[str] languages: List[str] registrations: bool approval_required: bool invites_enabled: bool configuration: InstanceConfiguration contact_account: Optional[Account] rules: List[Rule] @dataclass class Relationship: """ Represents the relationship between accounts, such as following / blocking / muting / etc. https://docs.joinmastodon.org/entities/Relationship/ """ id: str following: bool showing_reblogs: bool notifying: bool languages: List[str] followed_by: bool blocking: bool blocked_by: bool muting: bool muting_notifications: bool requested: bool domain_blocking: bool endorsed: bool note: str @dataclass class TagHistory: """ Usage statistics for given days (typically the past week). https://docs.joinmastodon.org/entities/Tag/#history """ day: str uses: str accounts: str @dataclass class Tag: """ Represents a hashtag used within the content of a status. https://docs.joinmastodon.org/entities/Tag/ """ name: str url: str history: List[TagHistory] following: Optional[bool] @dataclass class FeaturedTag: """ Represents a hashtag that is featured on a profile. https://docs.joinmastodon.org/entities/FeaturedTag/ """ id: str name: str url: str statuses_count: int last_status_at: datetime # Generic data class instance T = TypeVar("T") class ConversionError(Exception): """Raised when conversion fails from JSON value to data class field.""" def __init__( self, data_class: Type, field_name: str, field_type: Type, field_value: Optional[str] ): super().__init__( f"Failed converting field `{data_class.__name__}.{field_name}` " + f"of type `{field_type.__name__}` from value {field_value!r}" ) def from_dict(cls: Type[T], data: Dict) -> T: """Convert a nested dict into an instance of `cls`.""" # Apply __toot_prepare__ if it exists prepare = getattr(cls, '__toot_prepare__', None) if prepare: data = prepare(data) def _fields(): for name, type, default in get_fields(cls): value = data.get(name, default) converted = _convert_with_error_handling(cls, name, type, value) yield name, converted return cls(**dict(_fields())) @lru_cache(maxsize=100) def get_fields(cls: Type) -> List[Tuple[str, Type, Any]]: hints = get_type_hints(cls) return [ ( field.name, _prune_optional(hints[field.name]), _get_default_value(field) ) for field in dataclasses.fields(cls) ] def from_dict_list(cls: Type[T], data: List[Dict]) -> List[T]: return [from_dict(cls, x) for x in data] def _get_default_value(field): if field.default is not dataclasses.MISSING: return field.default if field.default_factory is not dataclasses.MISSING: return field.default_factory() return None def _convert_with_error_handling( data_class: Type, field_name: str, field_type: Type, field_value: Optional[str] ): try: return _convert(field_type, field_value) except ConversionError: raise except Exception: raise ConversionError(data_class, field_name, field_type, field_value) def _convert(field_type, value): if value is None: return None if field_type in [str, int, bool, dict]: return value if field_type == datetime: return parse_datetime(value) if field_type == date: return date.fromisoformat(value) if get_origin(field_type) == list: (inner_type,) = get_args(field_type) return [_convert(inner_type, x) for x in value] if is_dataclass(field_type): return from_dict(field_type, value) raise ValueError(f"Not implemented for type '{field_type}'") def _prune_optional(field_type: Type) -> Type: """For `Optional[]` returns the encapsulated ``.""" if get_origin(field_type) == Union: args = get_args(field_type) if len(args) == 2 and args[1] == type(None): # noqa return args[0] return field_type ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/toot/exceptions.py0000644000175000017500000000062314543235771016514 0ustar00ihabunekihabunekfrom click import ClickException class ApiError(ClickException): """Raised when an API request fails for whatever reason.""" class NotFoundError(ApiError): """Raised when an API requests returns a 404.""" class AuthenticationError(ApiError): """Raised when login fails.""" class ConsoleError(ClickException): """Raised when an error occurs which needs to be show to the user.""" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704224572.0 toot-0.41.1/toot/http.py0000644000175000017500000000762114545063474015320 0ustar00ihabunekihabunekfrom requests import Request, Session from requests.exceptions import RequestException from toot import __version__ from toot.exceptions import NotFoundError, ApiError from toot.logging import log_request, log_request_exception, log_response def send_request(request, allow_redirects=True): # Set a user agent string # Required for accessing instances using Cloudfront DDOS protection. request.headers["User-Agent"] = "toot/{}".format(__version__) log_request(request) try: with Session() as session: prepared = session.prepare_request(request) settings = session.merge_environment_settings(prepared.url, {}, None, None, None) response = session.send(prepared, allow_redirects=allow_redirects, **settings) except RequestException as ex: log_request_exception(request, ex) raise ApiError(f"Request failed: {str(ex)}") log_response(response) return response def _get_error_message(response): """Attempt to extract an error message from response body""" try: data = response.json() if "error_description" in data: return data['error_description'] if "error" in data: return data['error'] except Exception: pass return f"Unknown error: {response.status_code} {response.reason}" def process_response(response): if not response.ok: error = _get_error_message(response) if response.status_code == 404: raise NotFoundError(error) raise ApiError(error) return response def get(app, user, path, params=None, headers=None): url = app.base_url + path headers = headers or {} headers["Authorization"] = f"Bearer {user.access_token}" request = Request('GET', url, headers, params=params) response = send_request(request) return process_response(response) def anon_get(url, params=None): request = Request('GET', url, None, params=params) response = send_request(request) return process_response(response) def post(app, user, path, headers=None, files=None, data=None, json=None, allow_redirects=True): url = app.base_url + path headers = headers or {} headers["Authorization"] = f"Bearer {user.access_token}" return anon_post(url, headers=headers, files=files, data=data, json=json, allow_redirects=allow_redirects) def anon_put(url, headers=None, files=None, data=None, json=None, allow_redirects=True): request = Request(method="PUT", url=url, headers=headers, files=files, data=data, json=json) response = send_request(request, allow_redirects) return process_response(response) def put(app, user, path, headers=None, files=None, data=None, json=None, allow_redirects=True): url = app.base_url + path headers = headers or {} headers["Authorization"] = f"Bearer {user.access_token}" return anon_put(url, headers=headers, files=files, data=data, json=json, allow_redirects=allow_redirects) def patch(app, user, path, headers=None, files=None, data=None, json=None): url = app.base_url + path headers = headers or {} headers["Authorization"] = f"Bearer {user.access_token}" request = Request('PATCH', url, headers=headers, files=files, data=data, json=json) response = send_request(request) return process_response(response) def delete(app, user, path, data=None, json=None, headers=None): url = app.base_url + path headers = headers or {} headers["Authorization"] = f"Bearer {user.access_token}" request = Request('DELETE', url, headers=headers, data=data, json=json) response = send_request(request) return process_response(response) def anon_post(url, headers=None, files=None, data=None, json=None, allow_redirects=True): request = Request(method="POST", url=url, headers=headers, files=files, data=data, json=json) response = send_request(request, allow_redirects) return process_response(response) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1702020194.0 toot-0.41.1/toot/logging.py0000644000175000017500000000331514534542142015753 0ustar00ihabunekihabunekimport json import sys from logging import getLogger from requests import Request, RequestException, Response from urllib.parse import urlencode logger = getLogger("toot") VERBOSE = "--verbose" in sys.argv def censor_secrets(headers): def _censor(k, v): if k == "Authorization": return (k, "***CENSORED***") return k, v return {_censor(k, v) for k, v in headers.items()} def truncate(line): if not VERBOSE and len(line) > 100: return line[:100] + "…" return line def log_request(request: Request): logger.debug(f" --> {request.method} {_url(request)}") if VERBOSE and request.headers: headers = censor_secrets(request.headers) logger.debug(f" --> HEADERS: {headers}") if VERBOSE and request.data: data = truncate(request.data) logger.debug(f" --> DATA: {data}") if VERBOSE and request.json: data = truncate(json.dumps(request.json)) logger.debug(f" --> JSON: {data}") if VERBOSE and request.files: logger.debug(f" --> FILES: {request.files}") def log_response(response: Response): method = response.request.method url = response.request.url elapsed = response.elapsed.microseconds // 1000 logger.debug(f" <-- {method} {url} HTTP {response.status_code} {elapsed}ms") if VERBOSE and response.content: content = truncate(response.content.decode()) logger.debug(f" <-- {content}") def log_request_exception(request: Request, ex: RequestException): logger.debug(f" <-- {request.method} {_url(request)} Exception: {ex}") def _url(request): url = request.url if request.params: url += f"?{urlencode(request.params)}" return url ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704109151.0 toot-0.41.1/toot/output.py0000644000175000017500000002236514544522137015676 0ustar00ihabunekihabunekimport click import re import textwrap import shutil from toot.entities import Account, Instance, Notification, Poll, Status from toot.utils import get_text, html_to_paragraphs from toot.wcstring import wc_wrap from typing import Any, Generator, Iterable, List from wcwidth import wcswidth DEFAULT_WIDTH = 80 def get_max_width() -> int: return click.get_current_context().max_content_width or DEFAULT_WIDTH def get_terminal_width() -> int: return shutil.get_terminal_size().columns def get_width() -> int: return min(get_terminal_width(), get_max_width()) def print_warning(text: str): click.secho(f"Warning: {text}", fg="yellow", err=True) def print_instance(instance: Instance): width = get_width() click.echo(instance_to_text(instance, width)) def instance_to_text(instance: Instance, width: int) -> str: return "\n".join(instance_lines(instance, width)) def instance_lines(instance: Instance, width: int) -> Generator[str, None, None]: yield f"{green(instance.title)}" yield f"{blue(instance.uri)}" yield f"running Mastodon {instance.version}" yield "" if instance.description: for paragraph in re.split(r"[\r\n]+", instance.description.strip()): paragraph = get_text(paragraph) yield textwrap.fill(paragraph, width=width) yield "" if instance.rules: yield "Rules:" for ordinal, rule in enumerate(instance.rules): ordinal = f"{ordinal + 1}." lines = textwrap.wrap(rule.text, width - len(ordinal)) first = True for line in lines: if first: yield f"{ordinal} {line}" first = False else: yield f"{' ' * len(ordinal)} {line}" yield "" contact = instance.contact_account if contact: yield f"Contact: {contact.display_name} @{contact.acct}" def print_account(account: Account) -> None: width = get_width() click.echo(account_to_text(account, width)) def account_to_text(account: Account, width: int) -> str: return "\n".join(account_lines(account, width)) def account_lines(account: Account, width: int) -> Generator[str, None, None]: acct = f"@{account.acct}" since = account.created_at.strftime("%Y-%m-%d") yield f"{green(acct)} {account.display_name}" if account.note: yield "" yield from html_lines(account.note, width) yield "" yield f"ID: {green(account.id)}" yield f"Since: {green(since)}" yield "" yield f"Followers: {yellow(account.followers_count)}" yield f"Following: {yellow(account.following_count)}" yield f"Statuses: {yellow(account.statuses_count)}" if account.fields: for field in account.fields: name = field.name.title() yield f'\n{yellow(name)}:' yield from html_lines(field.value, width) if field.verified_at: yield green("✓ Verified") yield "" yield account.url def print_acct_list(accounts): for account in accounts: acct = green(f"@{account['acct']}") click.echo(f"* {acct} {account['display_name']}") def print_tag_list(tags): for tag in tags: click.echo(f"* {format_tag_name(tag)}\t{tag['url']}") def print_lists(lists): headers = ["ID", "Title", "Replies"] data = [[lst["id"], lst["title"], lst["replies_policy"]] for lst in lists] print_table(headers, data) def print_table(headers: List[str], data: List[List[str]]): widths = [[len(cell) for cell in row] for row in data + [headers]] widths = [max(width) for width in zip(*widths)] def print_row(row): for idx, cell in enumerate(row): width = widths[idx] click.echo(cell.ljust(width), nl=False) click.echo(" ", nl=False) click.echo() underlines = ["-" * width for width in widths] print_row(headers) print_row(underlines) for row in data: print_row(row) def print_list_accounts(accounts): if accounts: click.echo("Accounts in list:\n") print_acct_list(accounts) else: click.echo("This list has no accounts.") def print_search_results(results): accounts = results["accounts"] hashtags = results["hashtags"] if accounts: click.echo("\nAccounts:") print_acct_list(accounts) if hashtags: click.echo("\nHashtags:") click.echo(", ".join([format_tag_name(tag) for tag in hashtags])) if not accounts and not hashtags: click.echo("Nothing found") def print_status(status: Status) -> None: width = get_width() click.echo(status_to_text(status, width)) def status_to_text(status: Status, width: int) -> str: return "\n".join(status_lines(status)) def status_lines(status: Status) -> Generator[str, None, None]: width = get_width() status_id = status.id in_reply_to_id = status.in_reply_to_id reblogged_by = status.account if status.reblog else None status = status.original time = status.created_at.strftime('%Y-%m-%d %H:%M %Z') username = "@" + status.account.acct spacing = width - wcswidth(username) - wcswidth(time) - 2 display_name = status.account.display_name if display_name: author = f"{green(display_name)} {blue(username)}" spacing -= wcswidth(display_name) + 1 else: author = blue(username) spaces = " " * spacing yield f"{author} {spaces} {yellow(time)}" yield "" yield from html_lines(status.content, width) if status.media_attachments: yield "" yield "Media:" for attachment in status.media_attachments: url = attachment.url for line in wc_wrap(url, width): yield line if status.poll: yield from poll_lines(status.poll) reblogged_by_acct = f"@{reblogged_by.acct}" if reblogged_by else None yield "" reply = f"↲ In reply to {yellow(in_reply_to_id)} " if in_reply_to_id else "" boost = f"↻ {blue(reblogged_by_acct)} boosted " if reblogged_by else "" yield f"ID {yellow(status_id)} {reply} {boost}" def html_lines(html: str, width: int) -> Generator[str, None, None]: first = True for paragraph in html_to_paragraphs(html): if not first: yield "" for line in paragraph: for subline in wc_wrap(line, width): yield subline first = False def poll_lines(poll: Poll) -> Generator[str, None, None]: for idx, option in enumerate(poll.options): perc = (round(100 * option.votes_count / poll.votes_count) if poll.votes_count and option.votes_count is not None else 0) if poll.voted and poll.own_votes and idx in poll.own_votes: voted_for = yellow(" ✓") else: voted_for = "" yield f"{option.title} - {perc}% {voted_for}" poll_footer = f'Poll · {poll.votes_count} votes' if poll.expired: poll_footer += " · Closed" if poll.expires_at: expires_at = poll.expires_at.strftime("%Y-%m-%d %H:%M") poll_footer += f" · Closes on {expires_at}" yield "" yield poll_footer def print_timeline(items: Iterable[Status]): print_divider() for item in items: print_status(item) print_divider() def print_notification(notification: Notification): print_notification_header(notification) if notification.status: print_divider(char="-") print_status(notification.status) def print_notifications(notifications: List[Notification]): for notification in notifications: if notification.type not in ['pleroma:emoji_reaction']: print_divider() print_notification(notification) print_divider() def print_notification_header(notification: Notification): account_name = format_account_name(notification.account) if (notification.type == "follow"): click.echo(f"{account_name} now follows you") elif (notification.type == "mention"): click.echo(f"{account_name} mentioned you") elif (notification.type == "reblog"): click.echo(f"{account_name} reblogged your status") elif (notification.type == "favourite"): click.echo(f"{account_name} favourited your status") elif (notification.type == "update"): click.echo(f"{account_name} edited a post") else: click.secho(f"Unknown notification type: '{notification.type}'", err=True, fg="yellow") click.secho("Please report an issue to toot.", err=True, fg="yellow") def print_divider(char: str = "─"): click.echo(char * get_width()) def format_tag_name(tag): return green(f"#{tag['name']}") def format_account_name(account: Account) -> str: acct = blue(f"@{account.acct}") if account.display_name: return f"{green(account.display_name)} {acct}" else: return acct # Shorthand functions for coloring output def blue(text: Any) -> str: return click.style(text, fg="blue") def bold(text: Any) -> str: return click.style(text, bold=True) def cyan(text: Any) -> str: return click.style(text, fg="cyan") def dim(text: Any) -> str: return click.style(text, dim=True) def green(text: Any) -> str: return click.style(text, fg="green") def yellow(text: Any) -> str: return click.style(text, fg="yellow") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/toot/settings.py0000644000175000017500000000261114543235771016172 0ustar00ihabunekihabunekfrom functools import lru_cache from os.path import exists, join from tomlkit import parse from toot import get_config_dir from typing import Optional, Type, TypeVar DISABLE_SETTINGS = False TOOT_SETTINGS_FILE_NAME = "settings.toml" def get_settings_path(): return join(get_config_dir(), TOOT_SETTINGS_FILE_NAME) def _load_settings() -> dict: # Used for testing without config file if DISABLE_SETTINGS: return {} path = get_settings_path() if not exists(path): return {} with open(path) as f: return parse(f.read()) @lru_cache(maxsize=None) def get_settings(): return _load_settings() T = TypeVar("T") def get_setting(key: str, type: Type[T], default: Optional[T] = None) -> Optional[T]: """ Get a setting value. The key should be a dot-separated string, e.g. "commands.post.editor" which will correspond to the "editor" setting inside the `[commands.post]` section. """ settings = get_settings() return _get_setting(settings, key.split("."), type, default) def _get_setting(dct, keys, type: Type, default=None): if len(keys) == 0: if isinstance(dct, type): return dct else: # TODO: warn? cast? both? return default key = keys[0] if isinstance(dct, dict) and key in dct: return _get_setting(dct[key], keys[1:], type, default) return default ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1704229718.3414402 toot-0.41.1/toot/tui/0000755000175000017500000000000014545075526014563 5ustar00ihabunekihabunek././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1669711187.0 toot-0.41.1/toot/tui/NOTES.md0000644000175000017500000000307214341342523015763 0ustar00ihabunekihabunekInteresting urwid implementations: * https://github.com/CanonicalLtd/subiquity/blob/master/subiquitycore/core.py#L280 * https://github.com/TomasTomecek/sen/blob/master/sen/tui/ui.py * https://github.com/rndusr/stig/tree/master/stig/tui Check out: * https://github.com/rr-/urwid_readline - better edit box? * https://github.com/prompt-toolkit/python-prompt-toolkit TODO/Ideas: * pack left column in timeline view * allow scrolling of toot contents if they don't fit the screen, perhaps using pageup/pagedown * consider adding semi-automated error reporting when viewing an exception, something along the lines of "press T to submit a ticket", which would link to a pre-filled issue submit page. * show new toots, some ideas: * R to reload/refresh timeline * streaming new toots? not sold on the idea * go up on first toot to fetch any newer ones, and prepend them? * Switch timeline to top/bottom layout for narrow views. * Think about how to show media * download media and use local image viewer? * convert to ascii art? * interaction with clipboard - how to copy a status to clipboard? * Show **notifications** * Status source * shortcut to copy source * syntax highlighting? * reblog * show author in status list, not person who reblogged * "v" should open the reblogged status, status.url is empty for the reblog * overlays * stack overlays instead of having one? * current bug: press U G Q Q - second Q closes the app instead of closing the overlay Questions: * is it possible to make a span a urwid.Text selectable? e.g. for urls and hashtags ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1657012909.0 toot-0.41.1/toot/tui/__init__.py0000644000175000017500000000044714261001255016660 0ustar00ihabunekihabunekfrom urwid.command_map import command_map from urwid.command_map import CURSOR_UP, CURSOR_DOWN, CURSOR_LEFT, CURSOR_RIGHT # Add movement using h/j/k/l to default command map command_map._command.update({ 'k': CURSOR_UP, 'j': CURSOR_DOWN, 'h': CURSOR_LEFT, 'l': CURSOR_RIGHT, }) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704224572.0 toot-0.41.1/toot/tui/app.py0000644000175000017500000007750714545063474015734 0ustar00ihabunekihabunekimport logging import subprocess import urwid from concurrent.futures import ThreadPoolExecutor from typing import NamedTuple, Optional from datetime import datetime, timezone from toot import api, config, __version__, settings from toot import App, User from toot.cli import get_default_visibility from toot.exceptions import ApiError from toot.utils.datetime import parse_datetime from .compose import StatusComposer from .constants import PALETTE from .entities import Status from .overlays import ExceptionStackTrace, GotoMenu, Help, StatusSource, StatusLinks, StatusZoom from .overlays import StatusDeleteConfirmation, Account from .poll import Poll from .timeline import Timeline from .utils import get_max_toot_chars, parse_content_links, copy_to_clipboard from .widgets import ModalBox logger = logging.getLogger(__name__) urwid.set_encoding('UTF-8') DEFAULT_MAX_TOOT_CHARS = 500 class TuiOptions(NamedTuple): colors: int media_viewer: Optional[str] relative_datetimes: bool default_visibility: Optional[bool] class Header(urwid.WidgetWrap): def __init__(self, app, user): self.app = app self.user = user self.text = urwid.Text("") self.cols = urwid.Columns([ ("pack", urwid.Text(('header_bold', 'toot'))), ("pack", urwid.Text(('header', ' | {}@{}'.format(user.username, app.instance)))), ("pack", self.text), ]) widget = urwid.AttrMap(self.cols, 'header') widget = urwid.Padding(widget) self._wrapped_widget = widget def clear_text(self, text): self.text.set_text("") def set_text(self, text): self.text.set_text(" | " + text) class Footer(urwid.Pile): def __init__(self): self.status = urwid.Text("") self.message = urwid.Text("") return super().__init__([ urwid.AttrMap(self.status, "footer_status"), urwid.AttrMap(self.message, "footer_message"), ]) def set_status(self, text): self.status.set_text(text) def clear_status(self, text): self.status.set_text("") def set_message(self, text): self.message.set_text(text) def set_error_message(self, text): self.message.set_text(("footer_message_error", text)) def clear_message(self): self.message.set_text("") class TUI(urwid.Frame): """Main TUI frame.""" loop: urwid.MainLoop screen: urwid.BaseScreen @staticmethod def create(app: App, user: User, args: TuiOptions): """Factory method, sets up TUI and an event loop.""" screen = urwid.raw_display.Screen() screen.set_terminal_properties(args.colors) tui = TUI(app, user, screen, args) palette = PALETTE.copy() overrides = settings.get_setting("tui.palette", dict, {}) for name, styles in overrides.items(): palette.append(tuple([name] + styles)) loop = urwid.MainLoop( tui, palette=palette, event_loop=urwid.AsyncioEventLoop(), unhandled_input=tui.unhandled_input, screen=screen, ) tui.loop = loop return tui def __init__(self, app, user, screen, options: TuiOptions): self.app = app self.user = user self.config = config.load_config() self.options = options self.loop = None # late init, set in `create` self.screen = screen self.executor = ThreadPoolExecutor(max_workers=1) self.timeline_generator = api.home_timeline_generator(app, user, limit=40) # Show intro screen while toots are being loaded self.body = self.build_intro() self.header = Header(app, user) self.footer = Footer() self.footer.set_status("Loading...") # Default max status length, updated on startup self.max_toot_chars = DEFAULT_MAX_TOOT_CHARS self.timeline = None self.overlay = None self.exception = None self.can_translate = False self.account = None self.followed_accounts = [] self.preferences = {} super().__init__(self.body, header=self.header, footer=self.footer) def run(self): self.loop.set_alarm_in(0, lambda *args: self.async_load_instance()) self.loop.set_alarm_in(0, lambda *args: self.async_load_preferences()) self.loop.set_alarm_in(0, lambda *args: self.async_load_timeline( is_initial=True, timeline_name="home")) self.loop.set_alarm_in(0, lambda *args: self.async_load_followed_accounts()) self.loop.run() self.executor.shutdown(wait=False) def build_intro(self): font = urwid.font.Thin6x6Font() # NB: Padding with width="clip" will convert the fixed BigText widget # to a flow widget so it can be used in a Pile. big_text = "Toot {}".format(__version__) big_text = urwid.BigText(("intro_bigtext", big_text), font) big_text = urwid.Padding(big_text, align="center", width="clip") intro = urwid.Pile([ big_text, urwid.Divider(), urwid.Text([ "Maintained by ", ("intro_smalltext", "@ihabunek"), " and contributors" ], align="center"), urwid.Divider(), urwid.Text(("intro_smalltext", "Loading toots..."), align="center"), ]) return urwid.Filler(intro) def run_in_thread(self, fn, done_callback=None, error_callback=None): """Runs `fn` asynchronously in a separate thread. On completion calls `done_callback` if `fn` exited cleanly, or `error_callback` if an exception was caught. Callback methods are invoked in the main thread, not the thread in which `fn` is executed. """ def _default_error_callback(ex): self.exception = ex self.footer.set_error_message("An exception occurred, press X to view") _error_callback = error_callback or _default_error_callback def _done(future): try: result = future.result() if done_callback: # Use alarm to invoke callback in main thread self.loop.set_alarm_in(0, lambda *args: done_callback(result)) except Exception as ex: exception = ex logger.exception(exception) self.loop.set_alarm_in(0, lambda *args: _error_callback(exception)) # TODO: replace by `self.loop.event_loop.run_in_executor` at some point # Added in https://github.com/urwid/urwid/issues/575 # Not yet released at the time of this comment future = self.loop.event_loop._loop.run_in_executor(self.executor, fn) future.add_done_callback(_done) return future def connect_default_timeline_signals(self, timeline): urwid.connect_signal(timeline, "focus", self.refresh_footer) def build_timeline(self, name, statuses, local): def _close(*args): raise urwid.ExitMainLoop() def _next(*args): self.async_load_timeline(is_initial=False) def _toggle_save(timeline, status): if not timeline.name.startswith("#"): return hashtag = timeline.name[1:] assert isinstance(local, bool), local timelines = self.config.setdefault("timelines", {}) if hashtag in timelines: del timelines[hashtag] self.footer.set_message("#{} unpinned".format(hashtag)) else: timelines[hashtag] = {"local": local} self.footer.set_message("#{} pinned".format(hashtag)) self.loop.set_alarm_in(5, lambda *args: self.footer.clear_message()) config.save_config(self.config) timeline = Timeline(self, name, statuses) self.connect_default_timeline_signals(timeline) urwid.connect_signal(timeline, "next", _next) urwid.connect_signal(timeline, "close", _close) urwid.connect_signal(timeline, "save", _toggle_save) return timeline def make_status(self, status_data): is_mine = self.user.username == status_data["account"]["acct"] return Status(status_data, is_mine, self.app.instance) def show_thread(self, status): def _close(*args): """When thread is closed, go back to the main timeline.""" self.body = self.timeline self.body.refresh_status_details() self.refresh_footer(self.timeline) # This is pretty fast, so it's probably ok to block while context is # loaded, can be made async later if needed context = api.context(self.app, self.user, status.original.id).json() ancestors = [self.make_status(s) for s in context["ancestors"]] descendants = [self.make_status(s) for s in context["descendants"]] statuses = ancestors + [status] + descendants focus = len(ancestors) timeline = Timeline(self, "thread", statuses, focus=focus, is_thread=True) self.connect_default_timeline_signals(timeline) urwid.connect_signal(timeline, "close", _close) self.body = timeline timeline.refresh_status_details() self.refresh_footer(timeline) def async_load_timeline(self, is_initial, timeline_name=None, local=None): """Asynchronously load a list of statuses.""" def _load_statuses(): self.footer.set_message("Loading statuses...") try: data = next(self.timeline_generator) except StopIteration: return [] finally: self.footer.clear_message() return [self.make_status(s) for s in data] def _done_initial(statuses): """Process initial batch of statuses, construct a Timeline.""" self.timeline = self.build_timeline(timeline_name, statuses, local) self.timeline.refresh_status_details() # Draw first status self.refresh_footer(self.timeline) self.body = self.timeline def _done_next(statuses): """Process sequential batch of statuses, adds statuses to the existing timeline.""" self.timeline.append_statuses(statuses) return self.run_in_thread(_load_statuses, done_callback=_done_initial if is_initial else _done_next) def async_load_instance(self): """ Attempt to update max_toot_chars from instance data. Does not work on vanilla Mastodon, works on Pleroma. See: https://github.com/tootsuite/mastodon/issues/4915 Also attempt to update translation flag from instance data. Translation is only present on Mastodon 4+ servers where the administrator has enabled this feature. See: https://github.com/mastodon/mastodon/issues/19328 """ def _load_instance(): return api.get_instance(self.app.base_url).json() def _done(instance): self.max_toot_chars = get_max_toot_chars(instance, DEFAULT_MAX_TOOT_CHARS) logger.info(f"Max toot chars set to: {self.max_toot_chars}") if "translation" in instance: # instance is advertising translation service self.can_translate = instance["translation"]["enabled"] elif "version" in instance: # fallback check: # get the major version number of the server # this works for Mastodon and Pleroma version strings # Mastodon versions < 4 do not have translation service # Revisit this logic if Pleroma implements translation ch = instance["version"][0] self.can_translate = int(ch) > 3 if ch.isnumeric() else False return self.run_in_thread(_load_instance, done_callback=_done) def async_load_preferences(self): """ Attempt to update user preferences from instance. https://docs.joinmastodon.org/methods/preferences/ """ def _load_preferences(): return api.get_preferences(self.app, self.user).json() def _done(preferences): self.preferences = preferences return self.run_in_thread(_load_preferences, done_callback=_done) def async_load_followed_accounts(self): def _load_accounts(): try: acct = f'@{self.user.username}@{self.user.instance}' self.account = api.find_account(self.app, self.user, acct) return api.following(self.app, self.user, self.account["id"]) except ApiError: # not supported by all Mastodon servers so fail silently if necessary return [] def _done_accounts(accounts): self.followed_accounts = {a["acct"] for a in accounts} self.run_in_thread(_load_accounts, done_callback=_done_accounts) def refresh_footer(self, timeline): """Show status details in footer.""" status, index, count = timeline.get_focused_status_with_counts() self.footer.set_status([ ("footer_status_bold", "[{}] ".format(timeline.name)), ] + ([status.id, " - status ", str(index + 1), " of ", str(count)] if status else ["no focused status"])) def show_status_source(self, status): self.open_overlay( widget=StatusSource(status), title="Status source", ) def clear_screen(self): self.screen.clear() def show_links(self, status): links = parse_content_links(status.original.data["content"]) if status else [] post_attachments = status.original.data["media_attachments"] or [] reblog_attachments = (status.data["reblog"]["media_attachments"] if status.data["reblog"] else None) or [] for a in post_attachments + reblog_attachments: url = a["remote_url"] or a["url"] links.append((url, a["description"] if a["description"] else url)) def _clear(*args): self.clear_screen() if links: links = list(set(links)) # deduplicate links links = sorted(links, key=lambda link: link[0]) # sort alphabetically by URL sl_widget = StatusLinks(links) urwid.connect_signal(sl_widget, "clear-screen", _clear) self.open_overlay( widget=sl_widget, title="Status links", options={"height": len(links) + 2}, ) def show_status_zoom(self, status_details): self.open_overlay( widget=StatusZoom(status_details), title="Status zoom", ) def show_exception(self, exception): self.open_overlay( widget=ExceptionStackTrace(exception), title="Unhandled Exception", ) def show_compose(self, in_reply_to=None): def _close(*args): self.close_overlay() def _post(timeline, *args): self.post_status(*args) # If the user specified --default-visibility, use that; otherwise, # try to use the server-side default visibility. If that fails, fall # back to get_default_visibility(). visibility = (self.options.default_visibility or self.preferences.get('posting:default:visibility', get_default_visibility())) composer = StatusComposer(self.max_toot_chars, self.user.username, visibility, in_reply_to) urwid.connect_signal(composer, "close", _close) urwid.connect_signal(composer, "post", _post) self.open_overlay(composer, title="Compose status") def async_edit(self, status): def _fetch_source(): return api.fetch_status_source(self.app, self.user, status.id).json() def _done(source): self.close_overlay() self.show_edit(status, source) please_wait = ModalBox("Loading status...") self.open_overlay(please_wait) self.run_in_thread(_fetch_source, done_callback=_done) def show_edit(self, status, source): def _close(*args): self.close_overlay() def _edit(timeline, *args): self.edit_status(status, *args) composer = StatusComposer(self.max_toot_chars, self.user.username, visibility=None, edit=status, source=source) urwid.connect_signal(composer, "close", _close) urwid.connect_signal(composer, "post", _edit) self.open_overlay(composer, title="Edit status") def show_goto_menu(self): user_timelines = self.config.get("timelines", {}) user_lists = api.get_lists(self.app, self.user) or [] menu = GotoMenu(user_timelines, user_lists) urwid.connect_signal(menu, "home_timeline", lambda x: self.goto_home_timeline()) urwid.connect_signal(menu, "public_timeline", lambda x, local: self.goto_public_timeline(local)) urwid.connect_signal(menu, "bookmark_timeline", lambda x, local: self.goto_bookmarks()) urwid.connect_signal(menu, "notification_timeline", lambda x, local: self.goto_notifications()) urwid.connect_signal(menu, "conversation_timeline", lambda x, local: self.goto_conversations()) urwid.connect_signal(menu, "personal_timeline", lambda x, local: self.goto_personal_timeline()) urwid.connect_signal(menu, "hashtag_timeline", lambda x, tag, local: self.goto_tag_timeline(tag, local=local)) urwid.connect_signal(menu, "list_timeline", lambda x, list_item: self.goto_list_timeline(list_item)) self.open_overlay(menu, title="Go to", options=dict( align="center", width=("relative", 60), valign="middle", height=18 + len(user_timelines) + len(user_lists), )) def show_help(self): self.open_overlay(Help(), title="Help") def show_poll(self, status): self.open_overlay( widget=Poll(self.app, self.user, status), title="Poll", ) def goto_home_timeline(self): self.timeline_generator = api.home_timeline_generator( self.app, self.user, limit=40) promise = self.async_load_timeline(is_initial=True, timeline_name="home") promise.add_done_callback(lambda *args: self.close_overlay()) def goto_public_timeline(self, local): self.timeline_generator = api.public_timeline_generator( self.app, self.user, local=local, limit=40) timeline_name = "local public" if local else "global public" promise = self.async_load_timeline(is_initial=True, timeline_name=timeline_name) promise.add_done_callback(lambda *args: self.close_overlay()) def goto_bookmarks(self): self.timeline_generator = api.bookmark_timeline_generator( self.app, self.user, limit=40) promise = self.async_load_timeline(is_initial=True, timeline_name="bookmarks") promise.add_done_callback(lambda *args: self.close_overlay()) def goto_notifications(self): self.timeline_generator = api.notification_timeline_generator( self.app, self.user, limit=40) promise = self.async_load_timeline(is_initial=True, timeline_name="notifications") promise.add_done_callback(lambda *args: self.close_overlay()) def goto_conversations(self): self.timeline_generator = api.conversation_timeline_generator( self.app, self.user, limit=40 ) promise = self.async_load_timeline( is_initial=True, timeline_name="conversations" ) promise.add_done_callback(lambda *args: self.close_overlay()) def goto_tag_timeline(self, tag, local): self.timeline_generator = api.tag_timeline_generator( self.app, self.user, tag, local=local, limit=40) promise = self.async_load_timeline( is_initial=True, timeline_name="#{}".format(tag), local=local, ) promise.add_done_callback(lambda *args: self.close_overlay()) def goto_personal_timeline(self): account_name = f"{self.user.username}@{self.user.instance}" self.timeline_generator = api.account_timeline_generator( self.app, self.user, account_name, reblogs=True, limit=40) promise = self.async_load_timeline(is_initial=True, timeline_name=f"personal {account_name}") promise.add_done_callback(lambda *args: self.close_overlay()) def goto_list_timeline(self, list_item): self.timeline_generator = api.timeline_list_generator( self.app, self.user, list_item['id'], limit=40) promise = self.async_load_timeline( is_initial=True, timeline_name=f"\N{clipboard}{list_item['title']}") promise.add_done_callback(lambda *args: self.close_overlay()) def show_media(self, status): urls = [m["url"] for m in status.original.data["media_attachments"]] if not urls: return media_viewer = self.options.media_viewer if media_viewer: try: subprocess.run([media_viewer] + urls) except FileNotFoundError: self.footer.set_error_message(f"Media viewer not found: '{media_viewer}'") except Exception as ex: self.exception = ex self.footer.set_error_message("Failed invoking media viewer. Press X to see exception.") else: self.footer.set_error_message("Media viewer not configured") def show_context_menu(self, status): # TODO: show context menu pass def show_delete_confirmation(self, status): def _delete(widget): promise = self.async_delete_status(self.timeline, status) promise.add_done_callback(lambda *args: self.close_overlay()) def _close(widget): self.close_overlay() widget = StatusDeleteConfirmation(status) urwid.connect_signal(widget, "close", _close) urwid.connect_signal(widget, "delete", _delete) self.open_overlay(widget, title="Delete status?", options=dict( align="center", width=30, valign="middle", height=4, )) def post_status(self, content, warning, visibility, in_reply_to_id): data = api.post_status( self.app, self.user, content, spoiler_text=warning, visibility=visibility, in_reply_to_id=in_reply_to_id ).json() status = self.make_status(data) # TODO: fetch new items from the timeline? self.footer.set_message("Status posted {} \\o/".format(status.id)) self.close_overlay() def edit_status(self, status, content, warning, visibility, in_reply_to_id): # We don't support editing polls (yet), so to avoid losing the poll # data from the original toot, copy it to the edit request. poll_args = {} poll = status.original.data.get('poll', None) if poll is not None: poll_args['poll_options'] = [o['title'] for o in poll['options']] poll_args['poll_multiple'] = poll['multiple'] # Convert absolute expiry time into seconds from now. expires_at = parse_datetime(poll['expires_at']) expires_in = int((expires_at - datetime.now(timezone.utc)).total_seconds()) poll_args['poll_expires_in'] = expires_in if 'hide_totals' in poll: poll_args['poll_hide_totals'] = poll['hide_totals'] data = api.edit_status( self.app, self.user, status.id, content, spoiler_text=warning, visibility=visibility, **poll_args ).json() new_status = self.make_status(data) self.footer.set_message("Status edited {} \\o/".format(status.id)) self.close_overlay() if self.timeline is not None: self.timeline.update_status(new_status) def show_account(self, account_id): account = api.whois(self.app, self.user, account_id) relationship = api.get_relationship(self.app, self.user, account_id) self.open_overlay( widget=Account(self.app, self.user, account, relationship), title="Account", ) def async_toggle_favourite(self, timeline, status): def _favourite(): api.favourite(self.app, self.user, status.id) def _unfavourite(): api.unfavourite(self.app, self.user, status.id) def _done(loop): # Create a new Status with flipped favourited flag new_data = status.data new_data["favourited"] = not status.favourited new_status = self.make_status(new_data) timeline.update_status(new_status) self.run_in_thread( _unfavourite if status.favourited else _favourite, done_callback=_done ) def async_toggle_reblog(self, timeline, status): def _reblog(): api.reblog(self.app, self.user, status.original.id, visibility=get_default_visibility()) def _unreblog(): api.unreblog(self.app, self.user, status.original.id) def _done(loop): # Create a new Status with flipped reblogged flag new_data = status.data new_status = self.make_status(new_data) new_status.original.reblogged = not status.original.reblogged timeline.update_status(new_status) # Check if status is rebloggable no_reblog_because_private = status.visibility == "private" and not status.is_mine no_reblog_because_direct = status.visibility == "direct" if no_reblog_because_private or no_reblog_because_direct: self.footer.set_error_message("You may not reblog this {} status".format(status.visibility)) return self.run_in_thread( _unreblog if status.original.reblogged else _reblog, done_callback=_done ) def async_translate(self, timeline, status): def _translate(): self.footer.set_message("Translating status {}".format(status.original.id)) try: response = api.translate(self.app, self.user, status.original.id) if response["content"]: self.footer.set_message("Status translated") else: self.footer.set_error_message("Server returned empty translation") response = None except Exception: response = None self.footer.set_error_message("Translate server error") self.loop.set_alarm_in(3, lambda *args: self.footer.clear_message()) return response def _done(response): if response is not None: status.original.translation = response["content"] status.original.translated_from = response["detected_source_language"] status.original.show_translation = True timeline.update_status(status) # If already translated, toggle showing translation if status.original.translation: status.original.show_translation = not status.original.show_translation timeline.update_status(status) else: self.run_in_thread(_translate, done_callback=_done) def async_toggle_bookmark(self, timeline, status): def _bookmark(): api.bookmark(self.app, self.user, status.id) def _unbookmark(): api.unbookmark(self.app, self.user, status.id) def _done(loop): # Create a new Status with flipped bookmarked flag new_data = status.data new_data["bookmarked"] = not status.bookmarked new_status = self.make_status(new_data) timeline.update_status(new_status) self.run_in_thread( _unbookmark if status.bookmarked else _bookmark, done_callback=_done ) def async_delete_status(self, timeline, status): def _delete(): api.delete_status(self.app, self.user, status.id) def _done(loop): timeline.remove_status(status) return self.run_in_thread(_delete, done_callback=_done) def copy_status(self, status): # TODO: copy a better version of status content # including URLs copy_to_clipboard(self.screen, status.original.data["content"]) self.footer.set_message(f"Status {status.original.id} copied") # --- Overlay handling ----------------------------------------------------- default_overlay_options = dict( align="center", width=("relative", 80), valign="middle", height=("relative", 80), ) def open_overlay(self, widget, options={}, title=""): top_widget = urwid.LineBox(widget, title=title) bottom_widget = self.body _options = self.default_overlay_options.copy() _options.update(options) self.overlay = urwid.Overlay( top_widget, bottom_widget, **_options ) self.body = self.overlay def close_overlay(self): self.body = self.overlay.bottom_w self.overlay = None if self.timeline: self.timeline.refresh_status_details() def refresh_timeline(self): # No point in refreshing the bookmarks timeline # and we don't have a good way to refresh a # list timeline yet (no reference to list ID kept) if (not self.timeline or self.timeline.name == 'bookmarks' or self.timeline.name.startswith("\N{clipboard}")): return if self.timeline.name.startswith("#"): self.timeline_generator = api.tag_timeline_generator( self.app, self.user, self.timeline.name[1:], limit=40) elif self.timeline.name.startswith("\N{clipboard}"): self.timeline_generator = api.tag_timeline_generator( self.app, self.user, self.timeline.name[1:], limit=40) else: if self.timeline.name.endswith("public"): self.timeline_generator = api.public_timeline_generator( self.app, self.user, local=self.timeline.name.startswith("local"), limit=40) elif self.timeline.name == "notifications": self.timeline_generator = api.notification_timeline_generator( self.app, self.user, limit=40) elif self.timeline.name == "conversations": self.timeline_generator = api.conversation_timeline_generator( self.app, self.user, limit=40) else: # default to home timeline self.timeline_generator = api.home_timeline_generator( self.app, self.user, limit=40) self.async_load_timeline(is_initial=True, timeline_name=self.timeline.name) # --- Keys ----------------------------------------------------------------- def unhandled_input(self, key): # TODO: this should not be in unhandled input if key in ('x', 'X'): if self.exception: self.show_exception(self.exception) elif key in ('g', 'G'): if not self.overlay: self.show_goto_menu() elif key == '?': if not self.overlay: self.show_help() elif key == ',': if not self.overlay: self.refresh_timeline() elif key == 'esc': if self.overlay: self.close_overlay() elif self.timeline.name != "home": # similar to goto_home_timeline() but without handling overlay (absent here) self.timeline_generator = api.home_timeline_generator( self.app, self.user, limit=40) self.async_load_timeline(is_initial=True, timeline_name="home") elif key in ('q', 'Q'): if self.overlay: self.close_overlay() else: raise urwid.ExitMainLoop() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704224572.0 toot-0.41.1/toot/tui/compose.py0000644000175000017500000001375314545063474016612 0ustar00ihabunekihabunekimport urwid import logging from .constants import VISIBILITY_OPTIONS from .widgets import Button, EditBox logger = logging.getLogger(__name__) class StatusComposer(urwid.Frame): """ UI for composing or editing a status message. To edit a status, provide the original status in 'edit', and optionally provide the status source (from the /status/:id/source API endpoint) in 'source'; this should have at least a 'text' member, and optionally 'spoiler_text'. If source is not provided, the formatted HTML will be presented to the user for editing. """ signals = ["close", "post"] def __init__(self, max_chars, username, visibility, in_reply_to=None, edit=None, source=None): self.in_reply_to = in_reply_to self.max_chars = max_chars self.username = username self.edit = edit self.cw_edit = None self.cw_add_button = Button("Add content warning", on_press=self.add_content_warning) self.cw_remove_button = Button("Remove content warning", on_press=self.remove_content_warning) if edit: if source is None: text = edit.data["content"] else: text = source.get("text", edit.data["content"]) if 'spoiler_text' in source: self.cw_edit = EditBox(multiline=True, allow_tab=True, edit_text=source['spoiler_text']) self.visibility = edit.data["visibility"] else: # not edit text = self.get_initial_text(in_reply_to) self.visibility = ( in_reply_to.visibility if in_reply_to else visibility ) self.content_edit = EditBox( edit_text=text, edit_pos=len(text), multiline=True, allow_tab=True) urwid.connect_signal(self.content_edit.edit, "change", self.text_changed) self.char_count = urwid.Text(["0/{}".format(max_chars)]) self.visibility_button = Button("Visibility: {}".format(self.visibility), on_press=self.choose_visibility) self.post_button = Button("Edit" if edit else "Post", on_press=self.post) self.cancel_button = Button("Cancel", on_press=self.close) contents = list(self.generate_list_items()) self.walker = urwid.SimpleListWalker(contents) self.listbox = urwid.ListBox(self.walker) return super().__init__(self.listbox) def get_initial_text(self, in_reply_to): if not in_reply_to: return "" text = '' if in_reply_to.is_mine else '@{} '.format(in_reply_to.original.account) mentions = ['@{}'.format(m["acct"]) for m in in_reply_to.mentions if m["acct"] != self.username] if mentions: text += '\n\n{}'.format(' '.join(mentions)) return text def text_changed(self, edit, text): count = self.max_chars - len(text) text = "{}/{}".format(count, self.max_chars) color = "warning" if count < 0 else "" self.char_count.set_text((color, text)) def generate_list_items(self): if self.in_reply_to: yield urwid.Text(("dim", "Replying to {}".format(self.in_reply_to.original.account))) yield urwid.AttrWrap(urwid.Divider("-"), "dim") yield urwid.Text("Status message") yield self.content_edit yield self.char_count yield urwid.Divider() if self.cw_edit: yield urwid.Text("Content warning") yield self.cw_edit yield urwid.Divider() yield self.cw_remove_button else: yield self.cw_add_button yield self.visibility_button yield self.post_button yield self.cancel_button def refresh(self): self.walker = urwid.SimpleListWalker(list(self.generate_list_items())) self.listbox.body = self.walker def choose_visibility(self, *args): list_items = [urwid.Text("Choose status visibility:")] for visibility, caption, description in VISIBILITY_OPTIONS: text = "{} - {}".format(caption, description) button = Button(text, on_press=self.set_visibility, user_data=visibility) list_items.append(button) self.walker = urwid.SimpleListWalker(list_items) self.listbox.body = self.walker # Initially focus currently chosen visibility focus_map = {v[0]: n + 1 for n, v in enumerate(VISIBILITY_OPTIONS)} focus = focus_map.get(self.visibility, 1) self.walker.set_focus(focus) def set_visibility(self, widget, visibility): self.visibility = visibility self.visibility_button.set_label("Visibility: {}".format(self.visibility)) self.refresh() self.walker.set_focus(7 if self.cw_edit else 4) def add_content_warning(self, button): self.cw_edit = EditBox(multiline=True, allow_tab=True) self.refresh() self.walker.set_focus(4) def remove_content_warning(self, button): self.cw_edit = None self.refresh() self.walker.set_focus(3) def set_error_message(self, msg): self.footer = urwid.Text(("footer_message_error", msg)) def clear_error_message(self): self.footer = None def post(self, button): self.clear_error_message() # Don't lstrip content to avoid removing intentional leading whitespace # However, do strip both sides to check if there is any content there content = self.content_edit.edit_text.rstrip() content = None if not content.strip() else content warning = self.cw_edit.edit_text.rstrip() if self.cw_edit else "" warning = None if not warning.strip() else warning if not content: self.set_error_message("Cannot post an empty message") return in_reply_to_id = self.in_reply_to.id if self.in_reply_to else None self._emit("post", content, warning, self.visibility, in_reply_to_id) def close(self, button): self._emit("close") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700988522.0 toot-0.41.1/toot/tui/constants.py0000644000175000017500000000633314530603152017140 0ustar00ihabunekihabunek# Color definitions are tuples of: # - name # - foreground (normal mode) # - background (normal mode) # - foreground (monochrome mode) # - foreground (high color mode) # - background (high color mode) # # See: # http://urwid.org/tutorial/index.html#display-attributes # http://urwid.org/manual/displayattributes.html#using-display-attributes PALETTE = [ # Components ('button', 'white', 'black'), ('button_focused', 'light gray', 'dark magenta', 'bold,underline'), ('card_author', 'yellow', ''), ('card_title', 'dark green', ''), ('columns_divider', 'white', 'dark blue'), ('content_warning', 'white', 'dark magenta'), ('editbox', 'white', 'black'), ('editbox_focused', 'white', 'dark magenta'), ('footer_message', 'dark green', ''), ('footer_message_error', 'light red', ''), ('footer_status', 'white', 'dark blue'), ('footer_status_bold', 'white, bold', 'dark blue'), ('header', 'white', 'dark blue'), ('header_bold', 'white,bold', 'dark blue', 'bold'), ('intro_bigtext', 'yellow', ''), ('intro_smalltext', 'light blue', ''), ('poll_bar', 'white', 'dark blue'), ('status_detail_account', 'dark green', ''), ('status_detail_bookmarked', 'light red', ''), ('status_detail_timestamp', 'light blue', ''), ('status_list_account', 'dark green', ''), ('status_list_selected', 'white,bold', 'dark green', 'bold,underline'), ('status_list_timestamp', 'light blue', ''), # Functional ('account', 'dark green', ''), ('hashtag', 'light cyan,bold', '', 'bold'), ('hashtag_followed', 'yellow,bold', '', 'bold'), ('link', ',italics', '', ',italics'), ('link_focused', ',italics', 'dark magenta', "underline,italics"), ('shortcut', 'light blue', ''), ('shortcut_highlight', 'white,bold', '', 'bold'), ('warning', 'light red', ''), # Visiblity ('visibility_public', 'dark gray', ''), ('visibility_unlisted', 'white', ''), ('visibility_private', 'dark cyan', ''), ('visibility_direct', 'yellow', ''), # Styles ('bold', ',bold', ''), ('dim', 'dark gray', ''), ('highlight', 'yellow', ''), ('success', 'dark green', ''), # HTML tag styling ('a', ',italics', '', 'italics'), # em tag is mapped to i ('i', ',italics', '', 'italics'), # strong tag is mapped to b ('b', ',bold', '', 'bold'), # special case for bold + italic nested tags ('bi', ',bold,italics', '', ',bold,italics'), ('u', ',underline', '', ',underline'), ('del', ',strikethrough', '', ',strikethrough'), ('code', 'light gray, standout', '', ',standout'), ('pre', 'light gray, standout', '', ',standout'), ('blockquote', 'light gray', '', ''), ('h1', ',bold', '', ',bold'), ('h2', ',bold', '', ',bold'), ('h3', ',bold', '', ',bold'), ('h4', ',bold', '', ',bold'), ('h5', ',bold', '', ',bold'), ('h6', ',bold', '', ',bold'), ('class_mention_hashtag', 'light cyan', '', ''), ('class_hashtag', 'light cyan', '', ''), ] VISIBILITY_OPTIONS = [ ("public", "Public", "Post to public timelines"), ("unlisted", "Unlisted", "Do not post to public timelines"), ("private", "Private", "Post to followers only"), ("direct", "Direct", "Post to mentioned users only"), ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704042944.0 toot-0.41.1/toot/tui/entities.py0000644000175000017500000000602114544320700016742 0ustar00ihabunekihabunekfrom collections import namedtuple from toot.utils.datetime import parse_datetime Author = namedtuple("Author", ["account", "display_name", "username"]) class Status: """ A wrapper around the Status entity data fetched from Mastodon. https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md#status Attributes ---------- reblog : Status or None The reblogged status if it exists. original : Status If a reblog, the reblogged status, otherwise self. """ def __init__(self, data, is_mine, default_instance): """ Parameters ---------- data : dict Status data as received from Mastodon. https://docs.joinmastodon.org/api/entities/#status is_mine : bool Whether the status was created by the logged in user. default_instance : str The domain of the instance into which the user is logged in. Used to create fully qualified account names for users on the same instance. Mastodon only populates the name, not the domain. """ self.data = data self.is_mine = is_mine self.default_instance = default_instance # This can be toggled by the user self.show_sensitive = False # Set when status is translated self.show_translation = False self.translation = None self.translated_from = None # TODO: clean up self.id = self.data["id"] self.account = self._get_account() self.created_at = parse_datetime(data["created_at"]) if data["edited_at"]: self.edited_at = parse_datetime(data["edited_at"]) else: self.edited_at = None self.author = self._get_author() self.favourited = data.get("favourited", False) self.reblogged = data.get("reblogged", False) self.bookmarked = data.get("bookmarked", False) self.in_reply_to = data.get("in_reply_to_id") self.url = data.get("url") self.mentions = data.get("mentions") self.reblog = self._get_reblog() self.visibility = data.get("visibility") @property def original(self): return self.reblog or self def _get_reblog(self): reblog = self.data.get("reblog") if not reblog: return None reblog_is_mine = self.is_mine and ( self.data["account"]["acct"] == reblog["account"]["acct"] ) return Status(reblog, reblog_is_mine, self.default_instance) def _get_author(self): acct = self.data['account']['acct'] acct = acct if "@" in acct else "{}@{}".format(acct, self.default_instance) return Author(acct, self.data['account']['display_name'], self.data['account']['username']) def _get_account(self): acct = self.data['account']['acct'] return acct if "@" in acct else "{}@{}".format(acct, self.default_instance) def __repr__(self): return "".format(self.id, self.account) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700988522.0 toot-0.41.1/toot/tui/overlays.py0000644000175000017500000003303314530603152016765 0ustar00ihabunekihabunekimport json import traceback import urwid import webbrowser from toot import __version__ from toot import api from toot.tui.utils import highlight_keys from toot.tui.widgets import Button, EditBox, SelectableText from toot.tui.richtext import html_to_widgets class StatusSource(urwid.Padding): """Shows status data, as returned by the server, as formatted JSON.""" def __init__(self, status): self.source = json.dumps(status.data, indent=4) self.filename_edit = EditBox(caption="Filename: ", edit_text=f"status-{status.id}.json") self.status_text = urwid.Text("") walker = urwid.SimpleFocusListWalker([ self.filename_edit, Button("Save", on_press=self.save_json), urwid.Divider("─"), urwid.Divider(" "), urwid.Text(self.source) ]) frame = urwid.Frame( body=urwid.ListBox(walker), footer=self.status_text ) super().__init__(frame) def save_json(self, button): filename = self.filename_edit.get_edit_text() if filename: with open(filename, "w") as f: f.write(self.source) self.status_text.set_text(("footer_message", f"Saved to {filename}")) class StatusZoom(urwid.ListBox): """Opens status in scrollable popup window""" def __init__(self, status_details): ll = list(filter(lambda x: getattr(x, "rows", None), status_details.widget_list)) walker = urwid.SimpleFocusListWalker(ll) super().__init__(walker) class StatusLinks(urwid.ListBox): """Shows status links.""" signals = ["clear-screen"] def __init__(self, links): def widget(url, title): return Button(title or url, on_press=lambda btn: self.browse(url)) walker = urwid.SimpleFocusListWalker( [widget(url, title) for url, title in links] ) super().__init__(walker) def browse(self, url): webbrowser.open(url) # force a screen refresh; necessary with console browsers self._emit("clear-screen") class ExceptionStackTrace(urwid.ListBox): """Shows an exception stack trace.""" def __init__(self, ex): lines = traceback.format_exception(type(ex), value=ex, tb=ex.__traceback__) walker = urwid.SimpleFocusListWalker([ urwid.Text(line) for line in lines ]) super().__init__(walker) class StatusDeleteConfirmation(urwid.ListBox): signals = ["delete", "close"] def __init__(self, status): def _delete(_): self._emit("delete") def _close(_): self._emit("close") walker = urwid.SimpleFocusListWalker([ Button("Yes, delete", on_press=_delete), Button("No, cancel", on_press=_close), ]) super().__init__(walker) class GotoMenu(urwid.ListBox): signals = [ "home_timeline", "public_timeline", "hashtag_timeline", "bookmark_timeline", "notification_timeline", "conversation_timeline", "personal_timeline", "list_timeline", ] def __init__(self, user_timelines, user_lists): self.hash_edit = EditBox(caption="Hashtag: ") self.message_widget = urwid.Text("") actions = list(self.generate_actions(user_timelines, user_lists)) walker = urwid.SimpleFocusListWalker(actions) super().__init__(walker) def get_hashtag(self): return self.hash_edit.edit_text.strip().lstrip("#") def generate_actions(self, user_timelines, user_lists): def _home(button): self._emit("home_timeline") def _local_public(button): self._emit("public_timeline", True) def _global_public(button): self._emit("public_timeline", False) def _personal(button): self._emit("personal_timeline", False) def _bookmarks(button): self._emit("bookmark_timeline", False) def _notifications(button): self._emit("notification_timeline", False) def _conversations(button): self._emit("conversation_timeline", False) def _hashtag(local): self.message_widget.set_text("") hashtag = self.get_hashtag() if hashtag: self._emit("hashtag_timeline", hashtag, local) else: self.message_widget.set_text(("warning", "Hashtag name required")) def mk_on_press_user_hashtag(tag, local): def on_press(btn): self._emit("hashtag_timeline", tag, local) return on_press def mk_on_press_user_list(list_item): def on_press(btn): self._emit("list_timeline", list_item) return on_press yield Button("Home timeline", on_press=_home) yield Button("Local public timeline", on_press=_local_public) yield Button("Global public timeline", on_press=_global_public) yield Button("Personal timeline", on_press=_personal) yield Button("Bookmarks", on_press=_bookmarks) yield Button("Notifications", on_press=_notifications) yield Button("Conversations", on_press=_conversations) if len(user_timelines): yield urwid.Divider() yield urwid.Text(("bold", "Shortcuts:")) # show all hashtag shortcuts for tag, cfg in sorted(user_timelines.items()): is_local = cfg["local"] yield Button(f"#{tag}" + (" (local)" if is_local else ""), on_press=mk_on_press_user_hashtag(tag, is_local)) for list_item in user_lists: yield Button(f"\N{clipboard}{list_item['title']}", on_press=mk_on_press_user_list(list_item)) yield urwid.Divider() yield self.hash_edit yield Button("Local hashtag timeline", on_press=lambda x: _hashtag(True)) yield Button("Public hashtag timeline", on_press=lambda x: _hashtag(False)) yield urwid.Divider() yield self.message_widget class Help(urwid.Padding): def __init__(self): actions = list(self.generate_contents()) walker = urwid.SimpleListWalker(actions) listbox = urwid.ListBox(walker) super().__init__(listbox, left=1, right=1) def generate_contents(self): def h(text): return highlight_keys(text, "shortcut") yield urwid.Text(("bold", "toot {}".format(__version__))) yield urwid.Divider() yield urwid.Text(("bold", "General usage")) yield urwid.Divider() yield urwid.Text(h(" [Arrow keys] or [H/J/K/L] to move around and scroll content")) yield urwid.Text(h(" [PageUp] and [PageDown] to scroll content")) yield urwid.Text(h(" [Enter] or [Space] to activate buttons and menu options")) yield urwid.Text(h(" [Esc] or [Q] to go back, close overlays, such as menus and this help text")) yield urwid.Divider() yield urwid.Text(("bold", "General keys")) yield urwid.Divider() yield urwid.Text(h(" [Q] - quit toot")) yield urwid.Text(h(" [G] - go to - switch timelines")) yield urwid.Text(h(" [E] - save/unsave (pin) current timeline")) yield urwid.Text(h(" [,] - refresh current timeline")) yield urwid.Text(h(" [?] - show this help")) yield urwid.Divider() yield urwid.Text(("bold", "Status keys")) yield urwid.Divider() yield urwid.Text("These commands are applied to the currently focused status.") yield urwid.Divider() yield urwid.Text(h(" [B] - Boost/unboost status")) yield urwid.Text(h(" [C] - Compose new status")) yield urwid.Text(h(" [F] - Favourite/unfavourite status")) yield urwid.Text(h(" [K] - Bookmark/unbookmark status")) yield urwid.Text(h(" [N] - Translate status if possible (toggle)")) yield urwid.Text(h(" [R] - Reply to current status")) yield urwid.Text(h(" [S] - Show text marked as sensitive")) yield urwid.Text(h(" [T] - Show status thread (replies)")) yield urwid.Text(h(" [L] - Show the status links")) yield urwid.Text(h(" [U] - Show the status data in JSON as received from the server")) yield urwid.Text(h(" [V] - Open status in default browser")) yield urwid.Text(h(" [Y] - Copy status to clipboard")) yield urwid.Text(h(" [Z] - Open status in scrollable popup window")) yield urwid.Divider() yield urwid.Text(("bold", "Links")) yield urwid.Divider() yield link("Documentation: ", "https://toot.bezdomni.net/") yield link("Project home: ", "https://github.com/ihabunek/toot/") class Account(urwid.ListBox): """Shows account data and provides various actions""" def __init__(self, app, user, account, relationship): self.app = app self.user = user self.account = account self.relationship = relationship self.last_action = None self.setup_listbox() def setup_listbox(self): actions = list(self.generate_contents(self.account, self.relationship, self.last_action)) walker = urwid.SimpleListWalker(actions) super().__init__(walker) def generate_contents(self, account, relationship=None, last_action=None): if self.last_action and not self.last_action.startswith("Confirm"): yield Button(f"Confirm {self.last_action}", on_press=take_action, user_data=self) yield Button("Cancel", on_press=cancel_action, user_data=self) else: if self.user.username == account["acct"]: yield urwid.Text(("dim", "This is your account")) else: if relationship['requested']: yield urwid.Text(("dim", "< Follow request is pending >")) else: yield Button("Unfollow" if relationship['following'] else "Follow", on_press=confirm_action, user_data=self) yield Button("Unmute" if relationship['muting'] else "Mute", on_press=confirm_action, user_data=self) yield Button("Unblock" if relationship['blocking'] else "Block", on_press=confirm_action, user_data=self) yield urwid.Divider("─") yield urwid.Divider() yield urwid.Text([("account", f"@{account['acct']}"), f" {account['display_name']}"]) if account["note"]: yield urwid.Divider() widgetlist = html_to_widgets(account["note"]) for line in widgetlist: yield (line) yield urwid.Divider() yield urwid.Text(["ID: ", ("highlight", f"{account['id']}")]) yield urwid.Text(["Since: ", ("highlight", f"{account['created_at'][:10]}")]) yield urwid.Divider() if account["bot"]: yield urwid.Text([("highlight", "Bot \N{robot face}")]) yield urwid.Divider() if account["locked"]: yield urwid.Text([("warning", "Locked \N{lock}")]) yield urwid.Divider() if "suspended" in account and account["suspended"]: yield urwid.Text([("warning", "Suspended \N{cross mark}")]) yield urwid.Divider() if relationship["followed_by"]: yield urwid.Text(("highlight", "Follows you \N{busts in silhouette}")) yield urwid.Divider() if relationship["blocked_by"]: yield urwid.Text(("warning", "Blocks you \N{no entry}")) yield urwid.Divider() yield urwid.Text(["Followers: ", ("highlight", f"{account['followers_count']}")]) yield urwid.Text(["Following: ", ("highlight", f"{account['following_count']}")]) yield urwid.Text(["Statuses: ", ("highlight", f"{account['statuses_count']}")]) if account["fields"]: for field in account["fields"]: name = field["name"].title() yield urwid.Divider() yield urwid.Text([("bold", f"{name.rstrip(':')}"), ":"]) widgetlist = html_to_widgets(field["value"]) for line in widgetlist: yield (line) if field["verified_at"]: yield urwid.Text(("success", "✓ Verified")) yield urwid.Divider() yield link("", account["url"]) def take_action(button: Button, self: Account): action = button.get_label() if action == "Confirm Follow": self.relationship = api.follow(self.app, self.user, self.account["id"]).json() elif action == "Confirm Unfollow": self.relationship = api.unfollow(self.app, self.user, self.account["id"]).json() elif action == "Confirm Mute": self.relationship = api.mute(self.app, self.user, self.account["id"]).json() elif action == "Confirm Unmute": self.relationship = api.unmute(self.app, self.user, self.account["id"]).json() elif action == "Confirm Block": self.relationship = api.block(self.app, self.user, self.account["id"]).json() elif action == "Confirm Unblock": self.relationship = api.unblock(self.app, self.user, self.account["id"]).json() self.last_action = None self.setup_listbox() def confirm_action(button: Button, self: Account): self.last_action = button.get_label() self.setup_listbox() def cancel_action(button: Button, self: Account): self.last_action = None self.setup_listbox() def link(text, url): attr_map = {"link": "link_focused"} text = SelectableText([text, ("link", url)]) urwid.connect_signal(text, "click", lambda t: webbrowser.open(url)) return urwid.AttrMap(text, "", attr_map) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700988522.0 toot-0.41.1/toot/tui/poll.py0000644000175000017500000000656214530603152016076 0ustar00ihabunekihabunekimport urwid from toot import api from toot.exceptions import ApiError from toot.utils.datetime import parse_datetime from .widgets import Button, CheckBox, RadioButton from .richtext import html_to_widgets class Poll(urwid.ListBox): """View and vote on a poll""" def __init__(self, app, user, status): self.status = status self.app = app self.user = user self.poll = status.original.data.get("poll") self.button_group = [] self.api_exception = None self.setup_listbox() def setup_listbox(self): actions = list(self.generate_contents(self.status)) walker = urwid.SimpleListWalker(actions) super().__init__(walker) def build_linebox(self, contents): contents = urwid.Pile(list(contents)) contents = urwid.Padding(contents, left=1, right=1) return urwid.LineBox(contents) def vote(self, button_widget): poll = self.status.original.data.get("poll") choices = [] for idx, button in enumerate(self.button_group): if button.get_state(): choices.append(idx) if len(choices): try: response = api.vote(self.app, self.user, poll["id"], choices=choices) self.status.original.data["poll"] = response self.api_exception = None self.poll["voted"] = True self.poll["own_votes"] = choices except ApiError as exception: self.api_exception = exception finally: self.setup_listbox() def generate_poll_detail(self): poll = self.poll self.button_group = [] # button group for idx, option in enumerate(poll["options"]): voted_for = ( poll["voted"] and poll["own_votes"] and idx in poll["own_votes"] ) if poll["voted"] or poll["expired"]: prefix = " ✓ " if voted_for else " " yield urwid.Text(("dim", prefix + f'{option["title"]}')) else: if poll["multiple"]: checkbox = CheckBox(f'{option["title"]}') self.button_group.append(checkbox) yield checkbox else: yield RadioButton(self.button_group, f'{option["title"]}') yield urwid.Divider() poll_detail = "Poll · {} votes".format(poll["votes_count"]) if poll["expired"]: poll_detail += " · Closed" if poll["expires_at"]: expires_at = parse_datetime(poll["expires_at"]).strftime( "%Y-%m-%d %H:%M" ) poll_detail += " · Closes on {}".format(expires_at) yield urwid.Text(("dim", poll_detail)) def generate_contents(self, status): yield urwid.Divider() widgetlist = html_to_widgets(status.data["content"]) for line in widgetlist: yield (line) yield urwid.Divider() yield self.build_linebox(self.generate_poll_detail()) yield urwid.Divider() if self.poll["voted"]: yield urwid.Text(("grey", "< Already Voted >")) elif not self.poll["expired"]: yield Button("Vote", on_press=self.vote) if self.api_exception: yield urwid.Divider() yield urwid.Text("warning", str(self.api_exception)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1704229718.3414402 toot-0.41.1/toot/tui/richtext/0000755000175000017500000000000014545075526016415 5ustar00ihabunekihabunek././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700988522.0 toot-0.41.1/toot/tui/richtext/__init__.py0000644000175000017500000000077114530603152020515 0ustar00ihabunekihabunekimport urwid from toot.tui.utils import highlight_hashtags from toot.utils import format_content from typing import List try: from .richtext import html_to_widgets, url_to_widget except ImportError: # Fallback if urwidgets are not available def html_to_widgets(html: str) -> List[urwid.Widget]: return [ urwid.Text(highlight_hashtags(line)) for line in format_content(html) ] def url_to_widget(url: str): return urwid.Text(("link", url)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700988522.0 toot-0.41.1/toot/tui/richtext/richtext.py0000644000175000017500000003205214530603152020605 0ustar00ihabunekihabunekimport re import urwid import unicodedata from bs4.element import NavigableString, Tag from toot.tui.constants import PALETTE from toot.utils import parse_html, urlencode_url from typing import List, Tuple from urwid.util import decompose_tagmarkup from urwidgets import Hyperlink, TextEmbed STYLE_NAMES = [p[0] for p in PALETTE] # NOTE: update this list if Mastodon starts supporting more block tags BLOCK_TAGS = ["p", "pre", "li", "blockquote", "h1", "h2", "h3", "h4", "h5", "h6"] def html_to_widgets(html, recovery_attempt=False) -> List[urwid.Widget]: """Convert html to urwid widgets""" widgets: List[urwid.Widget] = [] html = unicodedata.normalize("NFKC", html) soup = parse_html(html) first_tag = True for e in soup.body or soup: if isinstance(e, NavigableString): if first_tag and not recovery_attempt: # if our first "tag" is a navigable string # the HTML is out of spec, doesn't start with a tag, # we see this in content from Pixelfed servers. # attempt a fix by wrapping the HTML with

return html_to_widgets(f"

{html}

", recovery_attempt=True) else: continue else: name = e.name # if our HTML starts with a tag, but not a block tag # the HTML is out of spec. Attempt a fix by wrapping the # HTML with

if (first_tag and not recovery_attempt and name not in BLOCK_TAGS): return html_to_widgets(f"

{html}

", recovery_attempt=True) markup = render(name, e) first_tag = False if not isinstance(markup, urwid.Widget): # plaintext, so create a padded text widget txt = text_to_widget("", markup) markup = urwid.Padding( txt, align="left", width=("relative", 100), min_width=None, ) widgets.append(markup) # separate top level widgets with a blank line widgets.append(urwid.Divider(" ")) return widgets[:-1] # but suppress the last blank line def url_to_widget(url: str): widget = len(url), urwid.Filler(Hyperlink(url, "link", url)) return TextEmbed(widget) def inline_tag_to_text(tag) -> Tuple: """Convert html tag to plain text with tag as attributes recursively""" markups = process_inline_tag_children(tag) if not markups: return (tag.name, "") return (tag.name, markups) def process_inline_tag_children(tag) -> List: """Recursively retrieve all children and convert to a list of markup text""" markups = [] for child in tag.children: if isinstance(child, Tag): markup = render(child.name, child) markups.append(markup) else: markups.append(child) return markups URL_PATTERN = re.compile(r"(^.+)\x03(.+$)") def text_to_widget(attr, markup) -> urwid.Widget: markup_list = [] for run in markup: if isinstance(run, tuple): txt, attr_list = decompose_tagmarkup(run) # find anchor titles with an ETX separator followed by href match = URL_PATTERN.match(txt) if match: label, url = match.groups() anchor_attr = get_best_anchor_attr(attr_list) markup_list.append(( len(label), urwid.Filler(Hyperlink(url, anchor_attr, label)), )) else: markup_list.append(run) else: markup_list.append(run) return TextEmbed(markup_list) def process_block_tag_children(tag) -> List[urwid.Widget]: """Recursively retrieve all children and convert to a list of widgets any inline tags containing text will be converted to Text widgets""" pre_widget_markups = [] post_widget_markups = [] child_widgets = [] found_nested_widget = False for child in tag.children: if isinstance(child, Tag): # child is a nested tag; process using custom method # or default to inline_tag_to_text result = render(child.name, child) if isinstance(result, urwid.Widget): found_nested_widget = True child_widgets.append(result) else: if not found_nested_widget: pre_widget_markups.append(result) else: post_widget_markups.append(result) else: # child is text; append to the appropriate markup list if not found_nested_widget: pre_widget_markups.append(child) else: post_widget_markups.append(child) widget_list = [] if len(pre_widget_markups): widget_list.append(text_to_widget(tag.name, pre_widget_markups)) if len(child_widgets): widget_list += child_widgets if len(post_widget_markups): widget_list.append(text_to_widget(tag.name, post_widget_markups)) return widget_list def get_urwid_attr_name(tag) -> str: """Get the class name and translate to a name suitable for use as an urwid text attribute name""" if "class" in tag.attrs: clss = tag.attrs["class"] if len(clss) > 0: style_name = "class_" + "_".join(clss) # return the class name, only if we # find it as a defined palette name if style_name in STYLE_NAMES: return style_name # fallback to returning the tag name return tag.name def basic_block_tag_handler(tag) -> urwid.Widget: """default for block tags that need no special treatment""" return urwid.Pile(process_block_tag_children(tag)) def get_best_anchor_attr(attrib_list) -> str: if not attrib_list: return "" flat_al = list(flatten(attrib_list)) for a in flat_al[0]: # ref: https://docs.joinmastodon.org/spec/activitypub/ # these are the class names (translated to attrib names) # that we can support for display try: if a[0] in ["class_hashtag", "class_mention_hashtag", "class_mention"]: return a[0] except KeyError: continue return "a" def render(attr: str, content: str): if attr in ["a"]: return render_anchor(content) if attr in ["blockquote"]: return render_blockquote(content) if attr in ["br"]: return render_br(content) if attr in ["em"]: return render_em(content) if attr in ["ol"]: return render_ol(content) if attr in ["pre"]: return render_pre(content) if attr in ["span"]: return render_span(content) if attr in ["b", "strong"]: return render_strong(content) if attr in ["ul"]: return render_ul(content) # Glitch-soc and Pleroma allow

...

in content # Mastodon (PR #23913) does not; header tags are converted to

if attr in ["p", "div", "li", "h1", "h2", "h3", "h4", "h5", "h6"]: return basic_block_tag_handler(content) # Fall back to inline_tag_to_text handler return inline_tag_to_text(content) def render_anchor(tag) -> Tuple: """anchor tag handler""" markups = process_inline_tag_children(tag) if not markups: return (tag.name, "") href = tag.attrs["href"] title, attrib_list = decompose_tagmarkup(markups) if not attrib_list: attrib_list = [tag] if href: # urlencode the path and query portions of the URL href = urlencode_url(href) # use ASCII ETX (end of record) as a # delimiter between the title and the HREF title += f"\x03{href}" attr = get_best_anchor_attr(attrib_list) if attr == "a": # didn't find an attribute to use # in the child markup, so let's # try the anchor tag's own attributes attr = get_urwid_attr_name(tag) # hashtag anchors have a class of "mention hashtag" # or "hashtag" # we'll return style "class_mention_hashtag" # or "class_hashtag" # in that case; see corresponding palette entry # in constants.py controlling hashtag highlighting return (attr, title) def render_blockquote(tag) -> urwid.Widget: widget_list = process_block_tag_children(tag) blockquote_widget = urwid.LineBox( urwid.Padding( urwid.Pile(widget_list), align="left", width=("relative", 100), min_width=None, left=1, right=1, ), tlcorner="", tline="", lline="│", trcorner="", blcorner="", rline="", bline="", brcorner="", ) return urwid.Pile([urwid.AttrMap(blockquote_widget, "blockquote")]) def render_br(tag) -> Tuple: return ("br", "\n") def render_em(tag) -> Tuple: # to simplify the number of palette entries # translate EM to I (italic) markups = process_inline_tag_children(tag) if not markups: return ("i", "") # special case processing for bold and italic for parent in tag.parents: if parent.name == "b" or parent.name == "strong": return ("bi", markups) return ("i", markups) def render_ol(tag) -> urwid.Widget: """ordered list tag handler""" widgets = [] list_item_num = 1 increment = -1 if tag.has_attr("reversed") else 1 # get ol start= attribute if present if tag.has_attr("start") and len(tag.attrs["start"]) > 0: try: list_item_num = int(tag.attrs["start"]) except ValueError: pass for li in tag.find_all("li", recursive=False): markup = render("li", li) # li value= attribute will change the item number # it also overrides any ol start= attribute if li.has_attr("value") and len(li.attrs["value"]) > 0: try: list_item_num = int(li.attrs["value"]) except ValueError: pass if not isinstance(markup, urwid.Widget): txt = text_to_widget("li", [str(list_item_num), ". ", markup]) # 1. foo, 2. bar, etc. widgets.append(txt) else: txt = text_to_widget("li", [str(list_item_num), ". "]) columns = urwid.Columns( [txt, ("weight", 9999, markup)], dividechars=1, min_width=3 ) widgets.append(columns) list_item_num += increment return urwid.Pile(widgets) def render_pre(tag) -> urwid.Widget: #
 tag spec says that text should not wrap,
    # but horizontal screen space is at a premium
    # and we have no horizontal scroll bar, so allow
    # wrapping.

    widget_list = [urwid.Divider(" ")]
    widget_list += process_block_tag_children(tag)

    pre_widget = urwid.Padding(
        urwid.Pile(widget_list),
        align="left",
        width=("relative", 100),
        min_width=None,
        left=1,
        right=1,
    )
    return urwid.Pile([urwid.AttrMap(pre_widget, "pre")])


def render_span(tag) -> Tuple:
    markups = process_inline_tag_children(tag)

    if not markups:
        return (tag.name, "")

    # span inherits its parent's class definition
    # unless it has a specific class definition
    # of its own

    if "class" in tag.attrs:
        # uncomment the following code to hide all HTML marked
        # invisible (generally, the http:// prefix of URLs)
        # could be a user preference, it's only advisable if
        # the terminal supports OCS 8 hyperlinks (and that's not
        # automatically detectable)

        # if "invisible" in tag.attrs["class"]:
        #     return (tag.name, "")

        style_name = get_urwid_attr_name(tag)

        if style_name != "span":
            # unique class name matches an entry in our palette
            return (style_name, markups)

    if tag.parent:
        return (get_urwid_attr_name(tag.parent), markups)
    else:
        # fallback
        return ("span", markups)


def render_strong(tag) -> Tuple:
    # to simplify the number of palette entries
    # translate STRONG to B (bold)
    markups = process_inline_tag_children(tag)
    if not markups:
        return ("b", "")

    # special case processing for bold and italic
    for parent in tag.parents:
        if parent.name == "i" or parent.name == "em":
            return ("bi", markups)

    return ("b", markups)


def render_ul(tag) -> urwid.Widget:
    """unordered list tag handler"""

    widgets = []

    for li in tag.find_all("li", recursive=False):
        markup = render("li", li)

        if not isinstance(markup, urwid.Widget):
            txt = text_to_widget("li", ["\N{bullet} ", markup])
            # * foo, * bar, etc.
            widgets.append(txt)
        else:
            txt = text_to_widget("li", ["\N{bullet} "])
            columns = urwid.Columns(
                [txt, ("weight", 9999, markup)], dividechars=1, min_width=3
            )
            widgets.append(columns)

    return urwid.Pile(widgets)


def flatten(data):
    if isinstance(data, tuple):
        for x in data:
            yield from flatten(x)
    else:
        yield data
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1688731561.0
toot-0.41.1/toot/tui/scroll.py0000644000175000017500000003745314451777651016453 0ustar00ihabunekihabunek# scroll.py
#
# Copied from the stig project by rndusr@github
# https://github.com/rndusr/stig
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details
# http://www.gnu.org/licenses/gpl-3.0.txt

import urwid
from urwid.widget import BOX, FIXED, FLOW

# Scroll actions
SCROLL_LINE_UP        = 'line up'
SCROLL_LINE_DOWN      = 'line down'
SCROLL_PAGE_UP        = 'page up'
SCROLL_PAGE_DOWN      = 'page down'
SCROLL_TO_TOP         = 'to top'
SCROLL_TO_END         = 'to end'

# Scrollbar positions
SCROLLBAR_LEFT  = 'left'
SCROLLBAR_RIGHT = 'right'

class Scrollable(urwid.WidgetDecoration):
    def sizing(self):
        return frozenset([BOX,])

    def selectable(self):
        return True

    def __init__(self, widget):
        """Box widget that makes a fixed or flow widget vertically scrollable

        TODO: Focusable widgets are handled, including switching focus, but
        possibly not intuitively, depending on the arrangement of widgets.  When
        switching focus to a widget that is outside of the visible part of the
        original widget, the canvas scrolls up/down to the focused widget.  It
        would be better to scroll until the next focusable widget is in sight
        first.  But for that to work we must somehow obtain a list of focusable
        rows in the original canvas.
        """
        if not any(s in widget.sizing() for s in (FIXED, FLOW)):
            raise ValueError('Not a fixed or flow widget: %r' % widget)
        self._trim_top = 0
        self._scroll_action = None
        self._forward_keypress = None
        self._old_cursor_coords = None
        self._rows_max_cached = 0
        self.__super.__init__(widget)

    def render(self, size, focus=False):
        maxcol, maxrow = size

        # Render complete original widget
        ow = self._original_widget
        ow_size = self._get_original_widget_size(size)
        canv_full = ow.render(ow_size, focus)

        # Make full canvas editable
        canv = urwid.CompositeCanvas(canv_full)
        canv_cols, canv_rows = canv.cols(), canv.rows()

        if canv_cols <= maxcol:
            pad_width = maxcol - canv_cols
            if pad_width > 0:
                # Canvas is narrower than available horizontal space
                canv.pad_trim_left_right(0, pad_width)

        if canv_rows <= maxrow:
            fill_height = maxrow - canv_rows
            if fill_height > 0:
                # Canvas is lower than available vertical space
                canv.pad_trim_top_bottom(0, fill_height)

        if canv_cols <= maxcol and canv_rows <= maxrow:
            # Canvas is small enough to fit without trimming
            return canv

        self._adjust_trim_top(canv, size)

        # Trim canvas if necessary
        trim_top = self._trim_top
        trim_end = canv_rows - maxrow - trim_top
        trim_right = canv_cols - maxcol
        if trim_top > 0:
            canv.trim(trim_top)
        if trim_end > 0:
            canv.trim_end(trim_end)
        if trim_right > 0:
            canv.pad_trim_left_right(0, -trim_right)

        # Disable cursor display if cursor is outside of visible canvas parts
        if canv.cursor is not None:
            curscol, cursrow = canv.cursor
            if cursrow >= maxrow or cursrow < 0:
                canv.cursor = None

        # Figure out whether we should forward keypresses to original widget
        if canv.cursor is not None:
            # Trimmed canvas contains the cursor, e.g. in an Edit widget
            self._forward_keypress = True
        else:
            if canv_full.cursor is not None:
                # Full canvas contains the cursor, but scrolled out of view
                self._forward_keypress = False
            else:
                # Original widget does not have a cursor, but may be selectable

                # FIXME: Using ow.selectable() is bad because the original
                # widget may be selectable because it's a container widget with
                # a key-grabbing widget that is scrolled out of view.
                # ow.selectable() returns True anyway because it doesn't know
                # how we trimmed our canvas.
                #
                # To fix this, we need to resolve ow.focus and somehow
                # ask canv whether it contains bits of the focused widget.  I
                # can't see a way to do that.
                if ow.selectable():
                    self._forward_keypress = True
                else:
                    self._forward_keypress = False

        return canv

    def keypress(self, size, key):
        # Maybe offer key to original widget
        if self._forward_keypress:
            ow = self._original_widget
            ow_size = self._get_original_widget_size(size)

            # Remember previous cursor position if possible
            if hasattr(ow, 'get_cursor_coords'):
                self._old_cursor_coords = ow.get_cursor_coords(ow_size)

            key = ow.keypress(ow_size, key)
            if key is None:
                return None

        # Handle up/down, page up/down, etc
        command_map = self._command_map
        if command_map[key] == urwid.CURSOR_UP:
            self._scroll_action = SCROLL_LINE_UP
        elif command_map[key] == urwid.CURSOR_DOWN:
            self._scroll_action = SCROLL_LINE_DOWN

        elif command_map[key] == urwid.CURSOR_PAGE_UP:
            self._scroll_action = SCROLL_PAGE_UP
        elif command_map[key] == urwid.CURSOR_PAGE_DOWN:
            self._scroll_action = SCROLL_PAGE_DOWN

        elif command_map[key] == urwid.CURSOR_MAX_LEFT:   # 'home'
            self._scroll_action = SCROLL_TO_TOP
        elif command_map[key] == urwid.CURSOR_MAX_RIGHT:  # 'end'
            self._scroll_action = SCROLL_TO_END

        else:
            return key

        self._invalidate()

    def mouse_event(self, size, event, button, col, row, focus):
        ow = self._original_widget
        if hasattr(ow, 'mouse_event'):
            ow_size = self._get_original_widget_size(size)
            row += self._trim_top
            return ow.mouse_event(ow_size, event, button, col, row, focus)
        else:
            return False

    def _adjust_trim_top(self, canv, size):
        """Adjust self._trim_top according to self._scroll_action"""
        action = self._scroll_action
        self._scroll_action = None

        maxcol, maxrow = size
        trim_top = self._trim_top
        canv_rows = canv.rows()

        if trim_top < 0:
            # Negative trim_top values use bottom of canvas as reference
            trim_top = canv_rows - maxrow + trim_top + 1

        if canv_rows <= maxrow:
            self._trim_top = 0  # Reset scroll position
            return

        def ensure_bounds(new_trim_top):
            return max(0, min(canv_rows - maxrow, new_trim_top))

        if action == SCROLL_LINE_UP:
            self._trim_top = ensure_bounds(trim_top - 1)
        elif action == SCROLL_LINE_DOWN:
            self._trim_top = ensure_bounds(trim_top + 1)

        elif action == SCROLL_PAGE_UP:
            self._trim_top = ensure_bounds(trim_top - maxrow + 1)
        elif action == SCROLL_PAGE_DOWN:
            self._trim_top = ensure_bounds(trim_top + maxrow - 1)

        elif action == SCROLL_TO_TOP:
            self._trim_top = 0
        elif action == SCROLL_TO_END:
            self._trim_top = canv_rows - maxrow

        else:
            self._trim_top = ensure_bounds(trim_top)

        # If the cursor was moved by the most recent keypress, adjust trim_top
        # so that the new cursor position is within the displayed canvas part.
        # But don't do this if the cursor is at the top/bottom edge so we can still scroll out
        if self._old_cursor_coords is not None and self._old_cursor_coords != canv.cursor:
            self._old_cursor_coords = None
            curscol, cursrow = canv.cursor
            if cursrow < self._trim_top:
                self._trim_top = cursrow
            elif cursrow >= self._trim_top + maxrow:
                self._trim_top = max(0, cursrow - maxrow + 1)

    def _get_original_widget_size(self, size):
        ow = self._original_widget
        sizing = ow.sizing()
        if FIXED in sizing:
            return ()
        elif FLOW in sizing:
            return (size[0],)

    def get_scrollpos(self, size=None, focus=False):
        """Current scrolling position

        Lower limit is 0, upper limit is the maximum number of rows with the
        given maxcol minus maxrow.

        NOTE: The returned value may be too low or too high if the position has
        changed but the widget wasn't rendered yet.
        """
        return self._trim_top

    def set_scrollpos(self, position):
        """Set scrolling position

        If `position` is positive it is interpreted as lines from the top.
        If `position` is negative it is interpreted as lines from the bottom.

        Values that are too high or too low values are automatically adjusted
        during rendering.
        """
        self._trim_top = int(position)
        self._invalidate()

    def rows_max(self, size=None, focus=False):
        """Return the number of rows for `size`

        If `size` is not given, the currently rendered number of rows is returned.
        """
        if size is not None:
            ow = self._original_widget
            ow_size = self._get_original_widget_size(size)
            sizing = ow.sizing()
            if FIXED in sizing:
                self._rows_max_cached = ow.pack(ow_size, focus)[1]
            elif FLOW in sizing:
                self._rows_max_cached = ow.rows(ow_size, focus)
            else:
                raise RuntimeError('Not a flow/box widget: %r' % self._original_widget)
        return self._rows_max_cached


class ScrollBar(urwid.WidgetDecoration):
    def sizing(self):
        return frozenset((BOX,))

    def selectable(self):
        return True

    def __init__(self, widget, thumb_char=u'\u2588', trough_char=' ',
                 side=SCROLLBAR_RIGHT, width=1):
        """Box widget that adds a scrollbar to `widget`

        `widget` must be a box widget with the following methods:
          - `get_scrollpos` takes the arguments `size` and `focus` and returns
            the index of the first visible row.
          - `set_scrollpos` (optional; needed for mouse click support) takes the
            index of the first visible row.
          - `rows_max` takes `size` and `focus` and returns the total number of
            rows `widget` can render.

        `thumb_char` is the character used for the scrollbar handle.
        `trough_char` is used for the space above and below the handle.
        `side` must be 'left' or 'right'.
        `width` specifies the number of columns the scrollbar uses.
        """
        if BOX not in widget.sizing():
            raise ValueError('Not a box widget: %r' % widget)
        self.__super.__init__(widget)
        self._thumb_char = thumb_char
        self._trough_char = trough_char
        self.scrollbar_side = side
        self.scrollbar_width = max(1, width)
        self._original_widget_size = (0, 0)

    def render(self, size, focus=False):
        maxcol, maxrow = size

        sb_width = self._scrollbar_width
        ow_size = (max(0, maxcol - sb_width), maxrow)
        sb_width = maxcol - ow_size[0]

        ow = self._original_widget
        ow_base = self.scrolling_base_widget
        ow_rows_max = ow_base.rows_max(size, focus)
        if ow_rows_max <= maxrow:
            # Canvas fits without scrolling - no scrollbar needed
            self._original_widget_size = size
            return ow.render(size, focus)
        ow_rows_max = ow_base.rows_max(ow_size, focus)

        ow_canv = ow.render(ow_size, focus)
        self._original_widget_size = ow_size

        pos = ow_base.get_scrollpos(ow_size, focus)
        posmax = ow_rows_max - maxrow

        # Thumb shrinks/grows according to the ratio of
        #  / 
        thumb_weight = min(1, maxrow / max(1, ow_rows_max))
        thumb_height = max(1, round(thumb_weight * maxrow))

        # Thumb may only touch top/bottom if the first/last row is visible
        top_weight = float(pos) / max(1, posmax)
        top_height = int((maxrow - thumb_height) * top_weight)
        if top_height == 0 and top_weight > 0:
            top_height = 1

        # Bottom part is remaining space
        bottom_height = maxrow - thumb_height - top_height
        assert thumb_height + top_height + bottom_height == maxrow

        # Create scrollbar canvas
        # Creating SolidCanvases of correct height may result in "cviews do not
        # fill gaps in shard_tail!" or "cviews overflow gaps in shard_tail!"
        # exceptions. Stacking the same SolidCanvas is a workaround.
        # https://github.com/urwid/urwid/issues/226#issuecomment-437176837
        top = urwid.SolidCanvas(self._trough_char, sb_width, 1)
        thumb = urwid.SolidCanvas(self._thumb_char, sb_width, 1)
        bottom = urwid.SolidCanvas(self._trough_char, sb_width, 1)
        sb_canv = urwid.CanvasCombine(
            [(top, None, False)] * top_height +
            [(thumb, None, False)] * thumb_height +
            [(bottom, None, False)] * bottom_height,
        )

        combinelist = [(ow_canv, None, True, ow_size[0]),
                       (sb_canv, None, False, sb_width)]
        if self._scrollbar_side != SCROLLBAR_LEFT:
            return urwid.CanvasJoin(combinelist)
        else:
            return urwid.CanvasJoin(reversed(combinelist))

    @property
    def scrollbar_width(self):
        """Columns the scrollbar uses"""
        return max(1, self._scrollbar_width)

    @scrollbar_width.setter
    def scrollbar_width(self, width):
        self._scrollbar_width = max(1, int(width))
        self._invalidate()

    @property
    def scrollbar_side(self):
        """Where to display the scrollbar; must be 'left' or 'right'"""
        return self._scrollbar_side

    @scrollbar_side.setter
    def scrollbar_side(self, side):
        if side not in (SCROLLBAR_LEFT, SCROLLBAR_RIGHT):
            raise ValueError('scrollbar_side must be "left" or "right", not %r' % side)
        self._scrollbar_side = side
        self._invalidate()

    @property
    def scrolling_base_widget(self):
        """Nearest `original_widget` that is compatible with the scrolling API"""
        def orig_iter(w):
            while hasattr(w, 'original_widget'):
                w = w.original_widget
                yield w
            yield w

        def is_scrolling_widget(w):
            return hasattr(w, 'get_scrollpos') and hasattr(w, 'rows_max')

        for w in orig_iter(self):
            if is_scrolling_widget(w):
                return w
        raise ValueError('Not compatible to be wrapped by ScrollBar: %r' % w)

    def keypress(self, size, key):
        return self._original_widget.keypress(self._original_widget_size, key)

    def mouse_event(self, size, event, button, col, row, focus):
        ow = self._original_widget
        ow_size = self._original_widget_size
        handled = False
        if hasattr(ow, 'mouse_event'):
            handled = ow.mouse_event(ow_size, event, button, col, row, focus)

        if not handled and hasattr(ow, 'set_scrollpos'):
            if button == 4:    # scroll wheel up
                pos = ow.get_scrollpos(ow_size)
                ow.set_scrollpos(pos - 1)
                return True
            elif button == 5:  # scroll wheel down
                pos = ow.get_scrollpos(ow_size)
                ow.set_scrollpos(pos + 1)
                return True

        return False
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704224572.0
toot-0.41.1/toot/tui/timeline.py0000644000175000017500000004150514545063474016747 0ustar00ihabunekihabunekimport logging
import urwid
import webbrowser

from typing import List, Optional

from toot.tui import app
from toot.tui.richtext import html_to_widgets, url_to_widget
from toot.utils.datetime import parse_datetime, time_ago
from toot.utils.language import language_name

from toot.entities import Status
from toot.tui.scroll import Scrollable, ScrollBar
from toot.tui.utils import highlight_keys
from toot.tui.widgets import SelectableText, SelectableColumns

logger = logging.getLogger("toot")


class Timeline(urwid.Columns):
    """
    Displays a list of statuses to the left, and status details on the right.
    """
    signals = [
        "close",  # Close thread
        "focus",  # Focus changed
        "next",   # Fetch more statuses
        "save",   # Save current timeline
    ]

    def __init__(
        self,
        tui: "app.TUI",
        name: str,
        statuses: List[Status],
        focus: int = 0,
        is_thread: bool = False
    ):
        self.tui = tui
        self.name = name
        self.is_thread = is_thread
        self.statuses = statuses
        self.status_list = self.build_status_list(statuses, focus=focus)

        try:
            focused_status = statuses[focus]
        except IndexError:
            focused_status = None

        self.status_details = StatusDetails(self, focused_status)
        status_widget = self.wrap_status_details(self.status_details)

        super().__init__([
            ("weight", 40, self.status_list),
            ("weight", 0, urwid.AttrWrap(urwid.SolidFill("│"), "columns_divider")),
            ("weight", 60, status_widget),
        ])

    def wrap_status_details(self, status_details: "StatusDetails") -> urwid.Widget:
        """Wrap StatusDetails widget with a scrollbar and footer."""
        self.status_detail_scrollable = Scrollable(urwid.Padding(status_details, right=1))
        return urwid.Padding(
            urwid.Frame(
                body=ScrollBar(
                    self.status_detail_scrollable,
                    thumb_char="\u2588",
                    trough_char="\u2591",
                ),
                footer=self.get_option_text(status_details.status),
            ),
            left=1
        )

    def build_status_list(self, statuses, focus):
        items = [self.build_list_item(status) for status in statuses]
        walker = urwid.SimpleFocusListWalker(items)
        walker.set_focus(focus)
        urwid.connect_signal(walker, "modified", self.modified)
        return urwid.ListBox(walker)

    def build_list_item(self, status):
        item = StatusListItem(status, self.tui.options.relative_datetimes)
        urwid.connect_signal(item, "click", lambda *args:
            self.tui.show_context_menu(status))
        return urwid.AttrMap(item, None, focus_map={
            "status_list_account": "status_list_selected",
            "status_list_timestamp": "status_list_selected",
            "highligh": "status_list_selected",
            "dim": "status_list_selected",
            None: "status_list_selected",
        })

    def get_option_text(self, status: Optional[Status]) -> Optional[urwid.Text]:
        if not status:
            return None

        poll = status.original.data.get("poll")
        show_media = status.original.data["media_attachments"] and self.tui.options.media_viewer

        options = [
            "[A]ccount" if not status.is_mine else "",
            "[B]oost",
            "[D]elete" if status.is_mine else "",
            "[E]dit" if status.is_mine else "",
            "B[o]okmark",
            "[F]avourite",
            "[V]iew",
            "[T]hread" if not self.is_thread else "",
            "L[i]nks",
            "[M]edia" if show_media else "",
            "[R]eply",
            "[P]oll" if poll and not poll["expired"] else "",
            "So[u]rce",
            "[Z]oom",
            "Tra[n]slate" if self.tui.can_translate else "",
            "Cop[y]",
            "Help([?])",
        ]
        options = "\n" + " ".join(o for o in options if o)
        options = highlight_keys(options, "shortcut_highlight", "shortcut")
        return urwid.Text(options)

    def get_focused_status(self):
        try:
            return self.statuses[self.status_list.body.focus]
        except TypeError:
            return None

    def get_focused_status_with_counts(self):
        """Returns a tuple of:
            * focused status
            * focused status' index in the status list
            * length of the status list
        """
        return (
            self.get_focused_status(),
            self.status_list.body.focus,
            len(self.statuses),
        )

    def modified(self):
        """Called when the list focus switches to a new status"""
        status, index, count = self.get_focused_status_with_counts()
        self.draw_status_details(status)
        self._emit("focus")

    def refresh_status_details(self):
        """Redraws the details of the focused status."""
        status = self.get_focused_status()
        pos = self.status_detail_scrollable.get_scrollpos()
        self.draw_status_details(status)
        self.status_detail_scrollable.set_scrollpos(pos)

    def draw_status_details(self, status):
        self.status_details = StatusDetails(self, status)
        widget = self.wrap_status_details(self.status_details)
        self.contents[2] = widget, ("weight", 60, False)

    def keypress(self, size, key):
        status = self.get_focused_status()
        command = self._command_map[key]

        if not status:
            return super().keypress(size, key)

        # If down is pressed on last status in list emit a signal to load more.
        # TODO: Consider pre-loading statuses earlier
        if command in [urwid.CURSOR_DOWN, urwid.CURSOR_PAGE_DOWN] \
                and self.status_list.body.focus:
            index = self.status_list.body.focus + 1
            count = len(self.statuses)
            if index >= count:
                self._emit("next")

        if key in ("a", "A"):
            account_id = status.original.data["account"]["id"]
            self.tui.show_account(account_id)
            return

        if key in ("b", "B"):
            self.tui.async_toggle_reblog(self, status)
            return

        if key in ("c", "C"):
            self.tui.show_compose()
            return

        if key in ("d", "D"):
            if status.is_mine:
                self.tui.show_delete_confirmation(status)
            return

        if key in ("e", "E"):
            if status.is_mine:
                self.tui.async_edit(status)
            return

        if key in ("f", "F"):
            self.tui.async_toggle_favourite(self, status)
            return

        if key in ("m", "M"):
            self.tui.show_media(status)
            return

        if key in ("q", "Q"):
            self._emit("close")
            return

        if key == "esc" and self.is_thread:
            self._emit("close")
            return

        if key in ("r", "R"):
            self.tui.show_compose(status)
            return

        if key in ("s", "S"):
            status.original.show_sensitive = True
            self.refresh_status_details()
            return

        if key in ("o", "O"):
            self.tui.async_toggle_bookmark(self, status)
            return

        if key in ("i", "I"):
            self.tui.show_links(status)
            return

        if key in ("n", "N"):
            if self.tui.can_translate:
                self.tui.async_translate(self, status)
            return

        if key in ("t", "T"):
            self.tui.show_thread(status)
            return

        if key in ("u", "U"):
            self.tui.show_status_source(status)
            return

        if key in ("v", "V"):
            if status.original.url:
                webbrowser.open(status.original.url)
                # force a screen refresh; necessary with console browsers
                self.tui.clear_screen()
            return

        if key in ("e", "E"):
            self._emit("save", status)
            return

        if key in ("z", "Z"):
            self.tui.show_status_zoom(self.status_details)
            return

        if key in ("p", "P"):
            poll = status.original.data.get("poll")
            if poll and not poll["expired"]:
                self.tui.show_poll(status)
            return

        if key in ("y", "Y"):
            self.tui.copy_status(status)
            return

        return super().keypress(size, key)

    def append_status(self, status):
        self.statuses.append(status)
        self.status_list.body.append(self.build_list_item(status))

    def prepend_status(self, status):
        self.statuses.insert(0, status)
        self.status_list.body.insert(0, self.build_list_item(status))

    def append_statuses(self, statuses):
        for status in statuses:
            self.append_status(status)

    def get_status_index(self, id):
        # TODO: This is suboptimal, consider a better way
        for n, status in enumerate(self.statuses):
            if status.id == id:
                return n
        raise ValueError("Status with ID {} not found".format(id))

    def focus_status(self, status):
        index = self.get_status_index(status.id)
        self.status_list.body.set_focus(index)

    def update_status(self, status):
        """Overwrite status in list with the new instance and redraw."""
        index = self.get_status_index(status.id)
        assert self.statuses[index].id == status.id  # Sanity check

        # Update internal status list
        self.statuses[index] = status

        # Redraw list item
        self.status_list.body[index] = self.build_list_item(status)

        # Redraw status details if status is focused
        if index == self.status_list.body.focus:
            self.draw_status_details(status)

    def remove_status(self, status):
        index = self.get_status_index(status.id)
        assert self.statuses[index].id == status.id  # Sanity check

        del self.statuses[index]
        del self.status_list.body[index]
        self.refresh_status_details()


class StatusDetails(urwid.Pile):
    def __init__(self, timeline: Timeline, status: Optional[Status]):
        self.status = status
        self.followed_accounts = timeline.tui.followed_accounts

        reblogged_by = status.author if status and status.reblog else None
        widget_list = list(self.content_generator(status.original, reblogged_by)
            if status else ())
        return super().__init__(widget_list)

    def content_generator(self, status, reblogged_by):
        if reblogged_by:
            text = "♺ {} boosted".format(reblogged_by.display_name or reblogged_by.username)
            yield ("pack", urwid.Text(("dim", text)))
            yield ("pack", urwid.AttrMap(urwid.Divider("-"), "dim"))

        if status.author.display_name:
            yield ("pack", urwid.Text(("bold", status.author.display_name)))

        account_color = "highlight" if status.author.account in self.followed_accounts else "account"
        yield ("pack", urwid.Text((account_color, status.author.account)))
        yield ("pack", urwid.Divider())

        if status.data["spoiler_text"]:
            yield ("pack", urwid.Text(status.data["spoiler_text"]))
            yield ("pack", urwid.Divider())

        # Show content warning
        if status.data["spoiler_text"] and not status.show_sensitive:
            yield ("pack", urwid.Text(("content_warning", "Marked as sensitive. Press S to view.")))
        else:
            content = status.original.translation if status.original.show_translation else status.data["content"]
            widgetlist = html_to_widgets(content)

            for line in widgetlist:
                yield (line)

            media = status.data["media_attachments"]
            if media:
                for m in media:
                    yield ("pack", urwid.AttrMap(urwid.Divider("-"), "dim"))
                    yield ("pack", urwid.Text([("bold", "Media attachment"), " (", m["type"], ")"]))
                    if m["description"]:
                        yield ("pack", urwid.Text(m["description"]))
                    yield ("pack", url_to_widget(m["url"]))

            poll = status.original.data.get("poll")
            if poll:
                yield ("pack", urwid.Divider())
                yield ("pack", self.build_linebox(self.poll_generator(poll)))

            card = status.data.get("card")
            if card:
                yield ("pack", urwid.Divider())
                yield ("pack", self.build_linebox(self.card_generator(card)))

        application = status.data.get("application") or {}
        application = application.get("name")

        yield ("pack", urwid.AttrWrap(urwid.Divider("-"), "dim"))

        translated_from = (
            language_name(status.original.translated_from)
            if status.original.show_translation and status.original.translated_from
            else None
        )

        visibility_colors = {
            "public": "visibility_public",
            "unlisted": "visibility_unlisted",
            "private": "visibility_private",
            "direct": "visibility_direct"
        }

        visibility = status.visibility.title()
        visibility_color = visibility_colors.get(status.visibility, "dim")

        yield ("pack", urwid.Text([
            ("status_detail_timestamp", f"{status.created_at.strftime('%Y-%m-%d %H:%M')} "),
            ("status_detail_timestamp",
             f"(edited {status.edited_at.strftime('%Y-%m-%d %H:%M')}) " if status.edited_at else ""),
            ("status_detail_bookmarked" if status.bookmarked else "dim", "b "),
            ("dim", f"⤶ {status.data['replies_count']} "),
            ("highlight" if status.reblogged else "dim", f"♺ {status.data['reblogs_count']} "),
            ("highlight" if status.favourited else "dim", f"★ {status.data['favourites_count']}"),
            (visibility_color, f" · {visibility}"),
            ("highlight", f" · Translated from {translated_from} " if translated_from else ""),
            ("dim", f" · {application}" if application else ""),
        ]))

        # Push things to bottom
        yield ("weight", 1, urwid.BoxAdapter(urwid.SolidFill(" "), 1))

    def build_linebox(self, contents):
        contents = urwid.Pile(list(contents))
        contents = urwid.Padding(contents, left=1, right=1)
        return urwid.LineBox(contents)

    def card_generator(self, card):
        yield urwid.Text(("card_title", card["title"].strip()))
        if card.get("author_name"):
            yield urwid.Text(["by ", ("card_author", card["author_name"].strip())])
        yield urwid.Text("")
        if card["description"]:
            yield urwid.Text(card["description"].strip())
            yield urwid.Text("")
        yield url_to_widget(card["url"])

    def poll_generator(self, poll):
        for idx, option in enumerate(poll["options"]):
            perc = (round(100 * option["votes_count"] / poll["votes_count"])
                if poll["votes_count"] else 0)

            if poll["voted"] and poll["own_votes"] and idx in poll["own_votes"]:
                voted_for = " ✓"
            else:
                voted_for = ""

            yield urwid.Text(option["title"] + voted_for)
            yield urwid.ProgressBar("", "poll_bar", perc)

        status = "Poll · {} votes".format(poll["votes_count"])

        if poll["expired"]:
            status += " · Closed"

        if poll["expires_at"]:
            expires_at = parse_datetime(poll["expires_at"]).strftime("%Y-%m-%d %H:%M")
            status += " · Closes on {}".format(expires_at)

        yield urwid.Text(("dim", status))


class StatusListItem(SelectableColumns):
    def __init__(self, status, relative_datetimes):
        edited_at = status.original.edited_at

        # TODO: hacky implementation to avoid creating conflicts for existing
        # pull reuqests, refactor when merged.
        created_at = (
            time_ago(status.created_at).ljust(3, " ")
            if relative_datetimes
            else status.created_at.strftime("%Y-%m-%d %H:%M")
        )

        edited_flag = "*" if edited_at else " "
        favourited = ("highlight", "★") if status.original.favourited else " "
        reblogged = ("highlight", "♺") if status.original.reblogged else " "
        is_reblog = ("dim", "♺") if status.reblog else " "
        is_reply = ("dim", "⤶ ") if status.original.in_reply_to else "  "

        return super().__init__([
            ("pack", SelectableText(("status_list_timestamp", created_at), wrap="clip")),
            ("pack", urwid.Text(("status_list_timestamp", edited_flag))),
            ("pack", urwid.Text(" ")),
            ("pack", urwid.Text(favourited)),
            ("pack", urwid.Text(" ")),
            ("pack", urwid.Text(reblogged)),
            ("pack", urwid.Text(" ")),
            urwid.Text(("status_list_account", status.original.account), wrap="clip"),
            ("pack", urwid.Text(is_reply)),
            ("pack", urwid.Text(is_reblog)),
            ("pack", urwid.Text(" ")),
        ])
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700988522.0
toot-0.41.1/toot/tui/utils.py0000644000175000017500000000602014530603152016255 0ustar00ihabunekihabunekimport base64
import re
import urwid

from functools import reduce
from html.parser import HTMLParser
from typing import List

HASHTAG_PATTERN = re.compile(r'(?>> highlight_keys("[P]rint [V]iew", "blue")
    >>> [('blue', 'P'), 'rint ', ('blue', 'V'), 'iew']
    """
    def _gen():
        highlighted = False
        for part in re.split("\\[|\\]", text):
            if part:
                if highlighted:
                    yield (high_attr, part) if high_attr else part
                else:
                    yield (low_attr, part) if low_attr else part
            highlighted = not highlighted
    return list(_gen())


def highlight_hashtags(line):
    hline = []

    for p in re.split(HASHTAG_PATTERN, line):
        if p.startswith("#"):
            hline.append(("hashtag", p))
        else:
            hline.append(p)

    return hline


class LinkParser(HTMLParser):
    def reset(self):
        super().reset()
        self.links = []

    def handle_starttag(self, tag, attrs):
        if tag == "a":
            href, title = None, None
            for name, value in attrs:
                if name == "href":
                    href = value
                if name == "title":
                    title = value
            if href:
                self.links.append((href, title))


def parse_content_links(content):
    """Parse  tags from status's `content` and return them as a list of
    (href, title), where `title` may be None.
    """
    parser = LinkParser()
    parser.feed(content)
    return parser.links[:]


def copy_to_clipboard(screen: urwid.raw_display.Screen, text: str):
    """ copy text to clipboard using OSC 52
    This escape sequence is documented
    here https://iterm2.com/documentation-escape-codes.html
    It has wide support - XTerm, Windows Terminal,
    Kitty, iTerm2, others. Some terminals may require a setting to be
    enabled in order to use OSC 52 clipboard functions.
    """

    text_bytes = text.encode("utf-8")
    b64_bytes = base64.b64encode(text_bytes)
    b64_text = b64_bytes.decode("utf-8")

    screen.write(f"\033]52;c;{b64_text}\a")
    screen.flush()


def get_max_toot_chars(instance, default=500):
    # Mastodon
    # https://docs.joinmastodon.org/entities/Instance/#max_characters
    max_toot_chars = deep_get(instance, ["configuration", "statuses", "max_characters"])
    if isinstance(max_toot_chars, int):
        return max_toot_chars

    # Pleroma
    max_toot_chars = instance.get("max_toot_chars")
    if isinstance(max_toot_chars, int):
        return max_toot_chars

    return default


def deep_get(adict: dict, path: List[str], default=None):
    return reduce(
        lambda d, key: d.get(key, default) if isinstance(d, dict) else default,
        path,
        adict
    )
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704224572.0
toot-0.41.1/toot/tui/widgets.py0000644000175000017500000000437614545063474016614 0ustar00ihabunekihabunekimport urwid
from wcwidth import wcswidth


class Clickable:
    """
    Add a `click` signal which is sent when the item is activated or clicked.

    TODO: make it work on widgets which have other signals.
    """
    signals = ["click"]

    def keypress(self, size, key):
        if self._command_map[key] == urwid.ACTIVATE:
            self._emit('click')
            return

        return key

    def mouse_event(self, size, event, button, x, y, focus):
        if button == 1:
            self._emit('click')


class SelectableText(Clickable, urwid.Text):
    _selectable = True


class SelectableColumns(Clickable, urwid.Columns):
    _selectable = True


class EditBox(urwid.AttrWrap):
    """Styled edit box."""
    def __init__(self, *args, **kwargs):
        self.edit = urwid.Edit(*args, **kwargs)
        return super().__init__(self.edit, "editbox", "editbox_focused")


class Button(urwid.AttrWrap):
    """Styled button."""
    def __init__(self, *args, **kwargs):
        button = urwid.Button(*args, **kwargs)
        padding = urwid.Padding(button, width=wcswidth(args[0]) + 4)
        return super().__init__(padding, "button", "button_focused")

    def set_label(self, *args, **kwargs):
        self.original_widget.original_widget.set_label(*args, **kwargs)
        self.original_widget.width = wcswidth(args[0]) + 4


class CheckBox(urwid.AttrWrap):
    """Styled checkbox."""
    def __init__(self, *args, **kwargs):
        self.button = urwid.CheckBox(*args, **kwargs)
        padding = urwid.Padding(self.button, width=len(args[0]) + 4)
        return super().__init__(padding, "button", "button_focused")

    def get_state(self):
        """Return the state of the checkbox."""
        return self.button._state


class RadioButton(urwid.AttrWrap):
    """Styled radiobutton."""
    def __init__(self, *args, **kwargs):
        button = urwid.RadioButton(*args, **kwargs)
        padding = urwid.Padding(button, width=len(args[1]) + 4)
        return super().__init__(padding, "button", "button_focused")


class ModalBox(urwid.Frame):
    def __init__(self, message):
        text = urwid.Text(message)
        filler = urwid.Filler(text, valign='top', top=1, bottom=1)
        padding = urwid.Padding(filler, left=1, right=1)
        return super().__init__(padding)
././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1700988522.0
toot-0.41.1/toot/typing_compat.py0000644000175000017500000001267014530603152017201 0ustar00ihabunekihabunek# Taken from https://github.com/rossmacarthur/typing-compat/
# TODO: Remove once the minimum python version is increased to 3.8
#
# Licensed under the 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.
#
# flake8: noqa

import collections
import typing


__all__ = ['get_args', 'get_origin']
__title__ = 'typing-compat'
__version__ = '0.1.0'
__url__ = 'https://github.com/rossmacarthur/typing-compat'
__author__ = 'Ross MacArthur'
__author_email__ = 'ross@macarthur.io'
__description__ = 'Python typing compatibility library'


try:
    # Python >=3.8 should have these functions already
    from typing import get_args as _get_args      # novermin
    from typing import get_origin as _get_origin  # novermin
except ImportError:
    if hasattr(typing, '_GenericAlias'):  # Python 3.7

        def _get_origin(tp):
            """Copied from the Python 3.8 typing module"""
            if isinstance(tp, typing._GenericAlias):
                return tp.__origin__
            if tp is typing.Generic:
                return typing.Generic
            return None

        def _get_args(tp):
            """Copied from the Python 3.8 typing module"""
            if isinstance(tp, typing._GenericAlias):
                res = tp.__args__
                if (
                    get_origin(tp) is collections.abc.Callable
                    and res[0] is not Ellipsis
                ):
                    res = (list(res[:-1]), res[-1])
                return res
            return ()

    else:  # Python <3.7

        def _resolve_via_mro(tp):
            if hasattr(tp, '__mro__'):
                for t in tp.__mro__:
                    if t.__module__ in ('builtins', '__builtin__') and t is not object:
                        return t
            return tp

        def _get_origin(tp):
            """Emulate the behaviour of Python 3.8 typing module"""
            if isinstance(tp, typing._ClassVar):
                return typing.ClassVar
            elif isinstance(tp, typing._Union):
                return typing.Union
            elif isinstance(tp, typing.GenericMeta):
                if hasattr(tp, '_gorg'):
                    return _resolve_via_mro(tp._gorg)
                else:
                    while tp.__origin__ is not None:
                        tp = tp.__origin__
                    return _resolve_via_mro(tp)
            elif hasattr(typing, '_Literal') and isinstance(tp, typing._Literal):  # novermin
                return typing.Literal  # novermin

        def _normalize_arg(args):
            if isinstance(args, tuple) and len(args) > 1:
                base, rest = args[0], tuple(_normalize_arg(arg) for arg in args[1:])
                if isinstance(base, typing.CallableMeta):
                    return typing.Callable[list(rest[:-1]), rest[-1]]
                elif isinstance(base, (typing.GenericMeta, typing._Union)):
                    return base[rest]
            return args

        def _get_args(tp):
            """Emulate the behaviour of Python 3.8 typing module"""
            if isinstance(tp, typing._ClassVar):
                return (tp.__type__,)
            elif hasattr(tp, '_subs_tree'):
                tree = tp._subs_tree()
                if isinstance(tree, tuple) and len(tree) > 1:
                    if isinstance(tree[0], typing.CallableMeta) and len(tree) == 2:
                        return ([], _normalize_arg(tree[1]))
                    return tuple(_normalize_arg(arg) for arg in tree[1:])
            return ()


def get_origin(tp):
    """
    Get the unsubscripted version of a type.

    This supports generic types, Callable, Tuple, Union, Literal, Final and
    ClassVar. Returns None for unsupported types.

    Examples:

        get_origin(Literal[42]) is Literal
        get_origin(int) is None
        get_origin(ClassVar[int]) is ClassVar
        get_origin(Generic) is Generic
        get_origin(Generic[T]) is Generic
        get_origin(Union[T, int]) is Union
        get_origin(List[Tuple[T, T]][int]) == list
    """
    return _get_origin(tp)


def get_args(tp):
    """
    Get type arguments with all substitutions performed.

    For unions, basic simplifications used by Union constructor are performed.

    Examples:

        get_args(Dict[str, int]) == (str, int)
        get_args(int) == ()
        get_args(Union[int, Union[T, int], str][int]) == (int, str)
        get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int])
        get_args(Callable[[], T][int]) == ([], int)
    """
    return _get_args(tp)
././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1704229718.3414402
toot-0.41.1/toot/utils/0000755000175000017500000000000014545075526015122 5ustar00ihabunekihabunek././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0
toot-0.41.1/toot/utils/__init__.py0000644000175000017500000001052414543235771017233 0ustar00ihabunekihabunekimport os
import re
import socket
import subprocess
import tempfile
import unicodedata
import warnings

from bs4 import BeautifulSoup
from typing import Any, Dict, List

import click

from toot.exceptions import ConsoleError
from urllib.parse import urlparse, urlencode, quote, unquote


def str_bool(b):
    """Convert boolean to string, in the way expected by the API."""
    return "true" if b else "false"


def str_bool_nullable(b):
    """Similar to str_bool, but leave None as None"""
    return None if b is None else str_bool(b)


def parse_html(html: str) -> BeautifulSoup:
    # Ignore warnings made by BeautifulSoup, if passed something that looks like
    # a file (e.g. a dot which matches current dict), it will warn that the file
    # should be opened instead of passing a filename.
    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        return BeautifulSoup(html.replace("'", "'"), "html.parser")


def get_text(html):
    """Converts html to text, strips all tags."""
    text = parse_html(html).get_text()
    return unicodedata.normalize("NFKC", text)


def html_to_paragraphs(html: str) -> List[List[str]]:
    """Attempt to convert html to plain text while keeping line breaks.
    Returns a list of paragraphs, each being a list of lines.
    """
    paragraphs = re.split("]*>", html)

    # Convert 
s to line breaks and remove empty paragraphs paragraphs = [re.split("
", p) for p in paragraphs if p] # Convert each line in each paragraph to plain text: return [[get_text(line) for line in p] for p in paragraphs] def format_content(content): """Given a Status contents in HTML, converts it into lines of plain text. Returns a generator yielding lines of content. """ paragraphs = html_to_paragraphs(content) first = True for paragraph in paragraphs: if not first: yield "" for line in paragraph: yield line first = False def domain_exists(name): try: socket.gethostbyname(name) return True except OSError: return False def assert_domain_exists(domain): if not domain_exists(domain): raise ConsoleError("Domain {} not found".format(domain)) EOF_KEY = "Ctrl-Z" if os.name == 'nt' else "Ctrl-D" def multiline_input(): """Lets user input multiple lines of text, terminated by EOF.""" lines = [] while True: try: lines.append(input()) except EOFError: break return "\n".join(lines).strip() EDITOR_DIVIDER = "------------------------ >8 ------------------------" EDITOR_INPUT_INSTRUCTIONS = f""" {EDITOR_DIVIDER} Do not modify or remove the line above. Enter your toot above it. Everything below it will be ignored. """ def editor_input(editor: str, initial_text: str) -> str: """Lets user input text using an editor.""" tmp_path = _tmp_status_path() initial_text = (initial_text or "") + EDITOR_INPUT_INSTRUCTIONS if not _use_existing_tmp_file(tmp_path): with open(tmp_path, "w") as f: f.write(initial_text) f.flush() subprocess.run([editor, tmp_path]) with open(tmp_path) as f: return f.read().split(EDITOR_DIVIDER)[0].strip() def delete_tmp_status_file() -> None: try: os.unlink(_tmp_status_path()) except FileNotFoundError: pass def _tmp_status_path() -> str: tmp_dir = tempfile.gettempdir() return f"{tmp_dir}/.status.toot" def _use_existing_tmp_file(tmp_path: str) -> bool: if os.path.exists(tmp_path): click.echo(f"Found draft status at: {tmp_path}") choice = click.Choice(["O", "D"], case_sensitive=False) char = click.prompt("Open or Delete?", type=choice, default="O") return char == "O" return False def drop_empty_values(data: Dict[Any, Any]) -> Dict[Any, Any]: """Remove keys whose values are null""" return {k: v for k, v in data.items() if v is not None} def urlencode_url(url: str) -> str: parsed_url = urlparse(url) # unencode before encoding, to prevent double-urlencoding encoded_path = quote(unquote(parsed_url.path), safe="-._~()'!*:@,;+&=/") encoded_query = urlencode({k: quote(unquote(v), safe="-._~()'!*:@,;?/") for k, v in parsed_url.params}) encoded_url = parsed_url._replace(path=encoded_path, params=encoded_query).geturl() return encoded_url ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1701940743.0 toot-0.41.1/toot/utils/datetime.py0000644000175000017500000000230414534307007017255 0ustar00ihabunekihabunekimport math import os from datetime import datetime, timezone def parse_datetime(value): """Returns an aware datetime in local timezone""" dttm = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%f%z") # When running tests return datetime in UTC so that tests don't depend on # the local timezone if "PYTEST_CURRENT_TEST" in os.environ: return dttm.astimezone(timezone.utc) return dttm.astimezone() SECOND = 1 MINUTE = SECOND * 60 HOUR = MINUTE * 60 DAY = HOUR * 24 WEEK = DAY * 7 def time_ago(value: datetime) -> str: now = datetime.now().astimezone() delta = now.timestamp() - value.timestamp() if delta < 1: return "now" if delta < 8 * DAY: if delta < MINUTE: return f"{math.floor(delta / SECOND)}".rjust(2, " ") + "s" if delta < HOUR: return f"{math.floor(delta / MINUTE)}".rjust(2, " ") + "m" if delta < DAY: return f"{math.floor(delta / HOUR)}".rjust(2, " ") + "h" return f"{math.floor(delta / DAY)}".rjust(2, " ") + "d" if delta < 53 * WEEK: # not exactly correct but good enough as a boundary return f"{math.floor(delta / WEEK)}".rjust(2, " ") + "w" return ">1y" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1688731561.0 toot-0.41.1/toot/utils/language.py0000644000175000017500000000725514451777651017274 0ustar00ihabunekihabunek# Languages mapped by their ISO 639-1 code # https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes LANGUAGES = { "ab": "Abkhazian", "aa": "Afar", "af": "Afrikaans", "ak": "Akan", "sq": "Albanian", "am": "Amharic", "ar": "Arabic", "an": "Aragonese", "hy": "Armenian", "as": "Assamese", "av": "Avaric", "ae": "Avestan", "ay": "Aymara", "az": "Azerbaijani", "bm": "Bambara", "ba": "Bashkir", "eu": "Basque", "be": "Belarusian", "bn": "Bengali", "bi": "Bislama", "bs": "Bosnian", "br": "Breton", "bg": "Bulgarian", "my": "Burmese", "ca": "Catalan", "ch": "Chamorro", "ce": "Chechen", "ny": "Chichewa", "zh": "Chinese", "cu": "Old Slavonic", "cv": "Chuvash", "kw": "Cornish", "co": "Corsican", "cr": "Cree", "hr": "Croatian", "cs": "Czech", "da": "Danish", "dv": "Divehi", "nl": "Dutch", "en": "English", "eo": "Estonian", "ee": "Ewe", "fo": "Faroese", "fj": "Fijian", "fi": "Finnish", "fr": "French", "fy": "Western Frisian", "ff": "Fulah", "gd": "Gaelic", "gl": "Galician", "lg": "Ganda", "ka": "Georgian", "de": "German", "el": "Greek", "kl": "Kalaallisut", "gn": "Guarani", "gu": "Gujarati", "ht": "Haitian", "ha": "Hausa", "he": "Hebrew", "hz": "Herero", "hi": "Hiri Motu", "hu": "Hungarian", "is": "Icelandic", "io": "Ido", "ig": "Igbo", "id": "Indonesian", "ia": "Inupiaq", "ga": "Irish", "it": "Italian", "ja": "Japanese", "jv": "Javanese", "kn": "Kannada", "kr": "Kanuri", "ks": "Kashmiri", "kk": "Kazakh", "km": "Central Khmer", "ki": "Kikuyu", "rw": "Kirghiz", "kv": "Komi", "kg": "Kongo", "ko": "Korean", "kj": "Kuanyama", "ku": "Kurdish", "lo": "Lao", "la": "Latvian", "li": "Limburgan", "ln": "Lingala", "lt": "Lithuanian", "lu": "Luba-Katanga", "lb": "Luxembourgish", "mk": "Macedonian", "mg": "Malagasy", "ms": "Malay", "ml": "Malayalam", "mt": "Maltese", "gv": "Manx", "mi": "Maori", "mr": "Marathi", "mh": "Marshallese", "mn": "Mongolian", "na": "Nauru", "nv": "Navajo", "nd": "North Ndebele", "nr": "South Ndebele", "ng": "Nepali", "no": "Norwegian", "nb": "Norwegian Bokmål", "nn": "Norwegian Nynorsk", "ii": "Sichuan Yi", "oc": "Occitan", "oj": "Ojibwa", "or": "Oriya", "om": "Oromo", "os": "Ossetian", "pi": "Pali", "ps": "Pashto", "fa": "Persian", "pl": "Polish", "pt": "Portuguese", "pa": "Punjabi", "qu": "Quechua", "ro": "Romanian", "rm": "Romansh", "rn": "Rundi", "ru": "Russian", "se": "Samoan", "sg": "Sango", "sa": "Sardinian", "sr": "Serbian", "sn": "Shona", "sd": "Sindhi", "si": "Sinhala", "sk": "Slovak", "sl": "Slovenian", "so": "Somali", "st": "Southern Sotho", "es": "Spanish", "su": "Sundanese", "sw": "Swahili", "ss": "Swati", "sv": "Swedish", "tl": "Tagalog", "ty": "Tahitian", "tg": "Tajik", "ta": "Tamil", "tt": "Tatar", "te": "Telugu", "th": "Thai", "bo": "Tibetan", "ti": "Tigrinya", "to": "Tonga", "ts": "Tsonga", "tn": "Tswana", "tr": "Turkish", "tk": "Turkmen", "tw": "Uighur", "uk": "Ukrainian", "ur": "Uzbek", "ve": "Venda", "vi": "Vietnamese", "vo": "Walloon", "cy": "Welsh", "wo": "Wolof", "xh": "Xhosa", "yi": "Yiddish", "yo": "Yoruba", "za": "Zhuang", "zu": "Zulu", } def language_name(code): return LANGUAGES.get(code, code) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1703754745.0 toot-0.41.1/toot/wcstring.py0000644000175000017500000000637114543235771016201 0ustar00ihabunekihabunek""" Utilities for dealing with string containing wide characters. """ import re from typing import Generator, List from wcwidth import wcwidth, wcswidth def _wc_hard_wrap(line: str, length: int) -> Generator[str, None, None]: """ Wrap text to length characters, breaking when target length is reached, taking into account character width. Used to wrap lines which cannot be wrapped on whitespace. """ chars = [] chars_len = 0 for char in line: char_len = wcwidth(char) if chars_len + char_len > length: yield "".join(chars) chars: List[str] = [] chars_len = 0 chars.append(char) chars_len += char_len if chars: yield "".join(chars) def wc_wrap(text: str, length: int) -> Generator[str, None, None]: """ Wrap text to given length, breaking on whitespace and taking into account character width. Meant for use on a single line or paragraph. Will destroy spacing between words and paragraphs and any indentation. """ line_words: List[str] = [] line_len = 0 words = re.split(r"\s+", text.strip()) for word in words: word_len = wcswidth(word) if line_words and line_len + word_len > length: line = " ".join(line_words) if line_len <= length: yield line else: yield from _wc_hard_wrap(line, length) line_words = [] line_len = 0 line_words.append(word) line_len += word_len + 1 # add 1 to account for space between words if line_words: line = " ".join(line_words) if line_len <= length: yield line else: yield from _wc_hard_wrap(line, length) def trunc(text: str, length: int) -> str: """ Truncates text to given length, taking into account wide characters. If truncated, the last char is replaced by an ellipsis. """ if length < 1: raise ValueError("length should be 1 or larger") # Remove whitespace first so no unnecessary truncation is done. text = text.strip() text_length = wcswidth(text) if text_length <= length: return text # We cannot just remove n characters from the end since we don't know how # wide these characters are and how it will affect text length. # Use wcwidth to determine how many characters need to be truncated. chars_to_truncate = 0 trunc_length = 0 for char in reversed(text): chars_to_truncate += 1 trunc_length += wcwidth(char) if text_length - trunc_length <= length: break # Additional char to make room for ellipsis n = chars_to_truncate + 1 return text[:-n].strip() + '…' def pad(text: str, length: int) -> str: """Pads text to given length, taking into account wide characters.""" text_length = wcswidth(text) if text_length < length: return text + ' ' * (length - text_length) return text def fit_text(text: str, length: int) -> str: """Makes text fit the given length by padding or truncating it.""" text_length = wcswidth(text) if text_length > length: return trunc(text, length) if text_length < length: return pad(text, length) return text ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1704229718.3384402 toot-0.41.1/toot.egg-info/0000755000175000017500000000000014545075526015454 5ustar00ihabunekihabunek././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704229718.0 toot-0.41.1/toot.egg-info/PKG-INFO0000644000175000017500000000161214545075526016551 0ustar00ihabunekihabunekMetadata-Version: 2.1 Name: toot Version: 0.41.1 Summary: Mastodon CLI client Home-page: https://github.com/ihabunek/toot/ Author: Ivan Habunek Author-email: ivan@habunek.com License: GPLv3 Project-URL: Documentation, https://toot.bezdomni.net/ Project-URL: Issue tracker, https://github.com/ihabunek/toot/issues/ Keywords: mastodon toot Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console :: Curses Classifier: Environment :: Console Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3) Classifier: Programming Language :: Python :: 3 Requires-Python: >=3.7 Provides-Extra: richtext Provides-Extra: dev Provides-Extra: test License-File: LICENSE Toot is a CLI and TUI tool for interacting with Mastodon instances from the command line. Allows posting text and media to the timeline, searching, following, muting and blocking accounts and other actions. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704229718.0 toot-0.41.1/toot.egg-info/SOURCES.txt0000644000175000017500000000412514545075526017342 0ustar00ihabunekihabunek.coveragerc .flake8 .gitignore CHANGELOG.md CONTRIBUTING.md LICENSE MANIFEST.in Makefile README.rst book.css book.toml changelog.yaml setup.py trumpet.png .github/workflows/test.yml docs/SUMMARY.md docs/advanced.md docs/changelog.md docs/contributing.md docs/documentation.md docs/installation.md docs/introduction.md docs/license.md docs/release.md docs/settings.md docs/trumpet.png docs/tui.md docs/usage.md docs/images/auth.png docs/images/trumpet.png docs/images/tui_compose.png docs/images/tui_list.png docs/images/tui_poll.png scripts/generate_changelog scripts/tag_version tests/__init__.py tests/test_config.py tests/test_utils.py tests/test_version.py tests/utils.py tests/assets/small.webm tests/assets/test1.png tests/assets/test2.png tests/assets/test3.png tests/assets/test4.png tests/integration/__init__.py tests/integration/conftest.py tests/integration/test_accounts.py tests/integration/test_auth.py tests/integration/test_lists.py tests/integration/test_post.py tests/integration/test_read.py tests/integration/test_status.py tests/integration/test_tags.py tests/integration/test_timelines.py tests/integration/test_update_account.py tests/tui/test_rich_text.py toot/__init__.py toot/__main__.py toot/api.py toot/auth.py toot/config.py toot/entities.py toot/exceptions.py toot/http.py toot/logging.py toot/output.py toot/settings.py toot/typing_compat.py toot/wcstring.py toot.egg-info/PKG-INFO toot.egg-info/SOURCES.txt toot.egg-info/dependency_links.txt toot.egg-info/entry_points.txt toot.egg-info/requires.txt toot.egg-info/top_level.txt toot/cli/__init__.py toot/cli/accounts.py toot/cli/auth.py toot/cli/lists.py toot/cli/post.py toot/cli/read.py toot/cli/statuses.py toot/cli/tags.py toot/cli/timelines.py toot/cli/tui.py toot/cli/validators.py toot/tui/NOTES.md toot/tui/__init__.py toot/tui/app.py toot/tui/compose.py toot/tui/constants.py toot/tui/entities.py toot/tui/overlays.py toot/tui/poll.py toot/tui/scroll.py toot/tui/timeline.py toot/tui/utils.py toot/tui/widgets.py toot/tui/richtext/__init__.py toot/tui/richtext/richtext.py toot/utils/__init__.py toot/utils/datetime.py toot/utils/language.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704229718.0 toot-0.41.1/toot.egg-info/dependency_links.txt0000644000175000017500000000000114545075526021522 0ustar00ihabunekihabunek ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704229718.0 toot-0.41.1/toot.egg-info/entry_points.txt0000644000175000017500000000004614545075526020752 0ustar00ihabunekihabunek[console_scripts] toot = toot.cli:cli ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704229718.0 toot-0.41.1/toot.egg-info/requires.txt0000644000175000017500000000042214545075526020052 0ustar00ihabunekihabunekclick~=8.1 requests<3.0,>=2.13 beautifulsoup4<5.0,>=4.5.0 wcwidth>=0.1.7 urwid<3.0,>=2.0.0 tomlkit<1.0,>=0.10.0 [dev] coverage pyyaml twine wheel [richtext] urwidgets<0.2,>=0.1 [test] flake8 psycopg2-binary pytest pytest-xdist[psutil] setuptools vermin typing-extensions ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704229718.0 toot-0.41.1/toot.egg-info/top_level.txt0000644000175000017500000000000514545075526020201 0ustar00ihabunekihabunektoot ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1657012909.0 toot-0.41.1/trumpet.png0000644000175000017500000004014614261001255015167 0ustar00ihabunekihabunekPNG  IHDRaMsBIT|d IDATx]y\M/=MDQh 043 1#3_b%ƌk'„$l%JiӾ;-}^W<,<] (?-=P`tP ,@ݻ:BFW@w055Eee%ۧѣGcҥ=PF^^Fhm۶EBB.ڶm۴#VYqm|ᇼ}B111y&,X 35˗Ƴ… ׯ1-^˖-t zÑsssoUDJJJ fВܻwM^@UU555ӧ]HJJ7S6:u;w 99={7QSSGBOOfffĉQQQ@UU>KFFFBCC o:ZnІPVV[n֭[VbYmhԩ,(>44?lll0`]l… hР]\\7?yrZdd$Bh1&W0x`ڵkpvvf~߾}ܹ֭*<|EEE3g@ǏO>,G&LCCC| JJJ`T[dt)]ɵsk3ƎK}۷O#Ҍ3裏>۷oӒ%KM6H-XGGGe2z||SXĢnzzۖkGر#Ν;dzrªU`ccUVa믿->7m4ŋGii)b gΜaK!uuu,ZZZ:TVVB$L?/)TTT0m4 G!!!***۷/={{aٸuѹsg =zsxJJJŸ{.z@,Zhiiaʔ)066ƺuPRRUUU )) Æ ޽{yR@CC#TTT@YYRaeeDbBMM ***{Byy9{ܶ&;_qq1ZZZPUUetttPUUTWW׫}6BCC@NByy9ÇEEE0a݋.]`Ĉ OOOȑ#ڵ+V\CWW7BCCiHJJę[UTT ]]: iLHHȑ#̨fBHH"-ݧL"s)qq@l_yyL'ÇSUU۷֯_O)uN@ ,*..PZ~=%''~^ Dz ݼyFɮ۽{w200SNQbb"3O$&&gffF/^?^JHYYYl2rww'GGGK#SSS T***(#sN#hD,f̘Aiiil_rr2 OOOŋtyޱ322H~#}* `c~,GDo>0a]pm0***;w) bbb( f̘AnnnrO>+W4ۛ<$j.L2yCQO8)ڂ j*DDD0 N&}a߾}066f3MII @JJ QYY)3TÇx }}}Cii)z KKK\t :::PUUEDDB!k\vMnK@lVAnn.]B1}t|(**իQ^^K.>۶mCv񰲲ĉh׮zj_~R+Ǭv=-%%:t&SlXCmSҥKŶmpuVl޼MzwyyyPUU쐒Ç#::ׯ={66l}ȑ8y$e˖֭[ؼy3dHN4 %%%R2>L6 SNeAeeef'LII<.cǎz*Zj*,Xk֬iб***BMM jjjC@e=z -- HIIAnn.1j(TVV",, HNNL8JJJ077Gǎo ":u EEE077gG\878y$m{fc_0ftĬYXm0R`nn[ǣGx)zWWW3--- Z/Ʒ~[5)ƢErJaӦMRKvt)Fc555TWWCGGXt'CCC#kkj.k,׏)--sμ^}>|8 T#???255%]]]0`JQ]gAGӦM#???:~8]v ŋM2DtQbڵk+^w&g\II(7op̤VWWg}F={RCJatYKvSSSΦlrvv&^,:yd1??)8(ԩjՊ233uaٲeR!==K2z~~>u֍׷O>lLpF6sذa{Ak׮em ,I& mݺɯ?̾deeEZ۷oi۶m<"''iӦMR|N0zffԒ](Ǐ'4k,ڳgՀYǺuV7oeeeHKyVVƵk׎rssY$Z;88Paa!1ީS'zKwg@hΝez5%JKKNa۶mVN***OdaaAnh"2e %&& 4`6>|et###vԱcet&L|?&"¯JZl|999?t҅ƌCaaa믿/ݻwYN$}ۉZnbd1ɓ~ GMD|/҈}ƌV "OxkPMMMZ&MDt=K$9yݝxsZS ''wiӦѐ!CMQ>}ؘо}H Pxx8mۖ]F#Gd/IKKOw988ЬYk׮2qIE]$I-wED(+kkkޯSZZTk׮[>uג>iҤzctΎ&^ΝYY Dp"ѨQaΜ94f*))!]]]:}4YoNIĤ$Z>MvvvGjj*/ۼ ,%И$^Z_99s&x]y9p0yd/ /eۆ!@Qj*nݺaҥ-9Dp!?~^ahhkkkƍHLLDff&QQQ#;;/UUU.mڴ6mD_}988QYYGo۶mƆllľf̘A2S5VVmڴZEEEvI2s"4rss8>>LLL*7?$z-:'%%msYf+** 9{eL⧛ wf֛pY={of阳s=+QTT˗/`8JSAGG߳ fI022իf|||`hhCCC֙xVZIoQFڪU+<$fffO ???ykQoݺ5.\???̘1700y}򐓓.]666011۹`pvv Ny[YY kՉٳgۛm_t ٳgѡC%BfJԩlmmٶ@ н{w@rKSxƌ@@zzz֩S'8;;3X֭[m۶zb-|rmdd͛7>_d- ---!&&"aaa={6q8cС-uZj&(11455 g9uT3򧧧GgΜr)7ݏ?đC#Gy󉈚,3g8do}}}^ /׮MZ"@1coFNNNJ6664ydfߴiuܙ]Ɔ IMMm+++kXZZj׮듆<Ƥ}u҅rrrܸ54$y>71rhF/--}AֆG'" 5~@P߾}ݻT^^N^^^< gצ =zDAAAAR t1r {dd$>>>D$n۷/;v޽K}͘1RSS]>>Q9bcc[XXoFDD};.M4nݺՠqڪU+X+Wl媉TDG,NH,n̙3Xl6Ad``@EdkkK'NKRJJ ݻw/"Ξ=Kl?}9[pV/D\C\gϞ=˗ŋY8rCH2dLzVVYZZN$ %EFY}z"^]].Z>>>rqqRUU_TRRBĉ2u%%%Z|y 6DDwa(77|}}jjjԷo_9s&͜9O.ttthz/^$@@^^^`tI9ˬPF'2O?DM+Vx-Shܹ|rDT^^N CCC&VRAAD"JOOBϗ{SSS)))m;wWŋLyMf͒Zs3ӧ*8rqqɓ'P D$%Ν;Xؘx SNpţK^YWNC$^ 0A^[Ft6KeJ}Z9ZaǏ>|"())!!!w;wЭ[zգG?~xpUDDDSNEff&BBBwQ8wNSSSXZZwޘ0aB!<΂!bbbн{w8# @"BiiL=z >>^^^HOOÇyv킕ӧO1a>d>+Wʕ+l;88ڵCnn.k_yfhiiaذa ##]GG~)RSS~z\rڵk;_~,vj9*hݺ5-[KKKL4 ĉ'SV> j0TSuC[%v]oQQQhȶSNaɒ% ý{rJ ###|Xp!JJJ .`ܸqETMM ^EE???aHHH -,,vׯ3g=˃3@DXn pիjjj#33QQQPRR·~/q^p=zwLǞI&{u\f?SQQá'OJBUU1bW^طoKٱ7nl\\xyy9kccSWRXt)BCCw'N[i ͺv-碢"rttC6IH5jwel`` -ZNLzz:8p<==ISS%8MtAAٳdճG  =F>CڰaYYYX4hq&W}ժU)ښ}xQ;̙ۤ3ѣ^-xk3:̥obdtWWW^т;2il[__yyy1vtt4D"FD$-(?>yxx<K\ް&bt555h׮UVV6HF ҫW/*..&c͛71$KP(k]hjhypydggܹse?~쌅 ɓ'HHH#Gx}Æ ֭[@/%jՕ-]L=tPiժsssCfyƄ ၴos4TSSVc;v,f͚76p*^vHKK (++%ۛ0aDfWUUΝ;/8zx222h޼yĉ8GrӿoRWWWțО={ɉH(iӆidSN%A{S޽;YG2B/w GRF)/]DDݻw'wwwС9993KݩS'ee]^UUغu++\hqFJ*`?C iVSȖ-[x/ŋM4fڴiWMM mٲΟ?O= ۷/9UEDdffF{%#iLW">x`6ڴqF:CCCz*djjJ:tjj8Kʞ^^^ĄԦd?4&&&qB}rJvν{(""֯_OÆ . ښРA(""-ZӧEDDәSLcϏrrr¢E] >Q^^=xm߾\\\(//55W$vZ*..*HJDtfgպsŗ_~ݻw 'OĈ#󝹺"** gϞ&K9$ nHMMo+2! ###hkk믿fV_xݻw#33۶mP(lPqW^Mɓ'cΝ&n477GPPPݩS'L6ouD"`ooׯC]]8|0ر#lmmwΝ ~^^^駟"Byy9.\HR:K"&&.$$,XǏGPP@,6l> --JLL+T,hFݻ,A(˗/YW#OOf H?c?\-=cڱckoӦ -Y^xѠ&&&駟~b,Q|xDtR*nݺQAAҝÑ#G2WҜR^^ k ^cMjOҒV\ɼСCdnnN8yر$jftڴicUj$t]3}ڵ<+gh":t:tX .ΉcNE8=\E"=z˴!SSSݕ o57)55IWWW*RSbtx.kjjŋij… u*ttthռF||}߿O9]79F'"i-F!iiiʕ+)==rssVSSCM>>>u&faY(##nʮ]_6Ƃ^*9;ڶmKRcty=_zD"͝;Wn5UUUUf/,,l1fGJJJ51ӕ$M:Q䑖P(d?^\J_Pzz:ڵM0IYx'q~z,]G"##aaa۷㫯BNNY2,X ,@vЫW/ cǎ9 8::bȐ! ڼy3PSSe˖͍ij!//FFF޽;qYV޽;헞aϞ=22E LLL`ff~ Rae..];,VJwPVVƽ{BSL nݒRr⫯^G۶maff* : EQQ~Wڵ /^qAbݖ-[_XE&^zEG&@y,#Am۶/迳7[>nܸlΙ[pnnnnD>>>_Q3:g 7񄖖XwҧOzO4bא477,G\BN񨥥Evvv4}tZnbe߲eёvEcǎј1cёƎDN]dhhHNNNԳgOliر4}J xgHlC紣FFFqF""t7JCI__"""x2886{Qqq1@,^hԩv!5++-W\ctIq)1C]#3gZ8 4$XDTwII ӫW˗^/-Y|||>oС~z^fw9/>>F/_-߿Ovb㺺4uf 6ׯ{O]|ڷoO_Stt4"ZxqvvvS*,,ʫ. ,..&&ct"b*++رcGoX/R!t}]}{Dbf,@'={V1.99Y*9x>رwԉlج~I^_Uoonxz%СChr}:M4Iqvvvt);Vĉ~:>gϞ'pvvficcc| xpͨgpaXh>s^Bll,`bb{4Gرc<^EEǎCBB.]Zo"O>nnnlDz̙ NQPP %6;;pwwGbbzsEff&OZFFF(..Y\KK - UUU!++ {쁧'yyy:qh7͛7idii٠YNҒFݬ^IoM6M>]ȑ#>Ґ!CxIH6oL b2޽{I$Qee%u!22ؼy3*++q}5U{쁖LLL0`?Fb8}4>cX]vqH\~/^DBB"dff"77oPUUEϞ=1n8L6 ::: "ܻwgFDD}}}z gΜam۶w?k(((@LL bbb{rrr0w\;_w}MMM^gϞM6g}@Ücl|077w}x=TTTS_ryYea")))e",appp!CЫW/blؼy3ݻ}"--Mn~{@C{nL8AEK.'h8D"ݽ{e5k⋫6uT9ަDqq1=~|||̖W`& FDIt1qqq342zPqq1IHD" f^$uO8AzbhffF_|ϣWJ744ٳgK9?/t;v֭[k|ι2)Mcׯ^z*hiiA[[O>eKjHJJBǎKJedd`ٲePRRT!!!ppp`\PUU'|OOO|'L7aXjؽ{7W@ #Fd[BWW'O_Ç_~./^+B!QVVb,sex{шB^^tuu1d[[[\z :vk׮LetYHOOǞ={PTT_~ҽUBBaa![ңRvѢE