pax_global_header00006660000000000000000000000064152607373430014524gustar00rootroot0000000000000052 comment=69907e4ed532e02014570ef0586d4418e3aac280 urwid-urwid-69907e4/000077500000000000000000000000001526073734300142775ustar00rootroot00000000000000urwid-urwid-69907e4/.devcontainer/000077500000000000000000000000001526073734300170365ustar00rootroot00000000000000urwid-urwid-69907e4/.devcontainer/dev.dockerfile000066400000000000000000000022401526073734300216430ustar00rootroot00000000000000FROM ubuntu:26.04 RUN apt-get update -y \ && apt-get upgrade -y \ && apt-get install -y \ python3-dev \ python3-gi \ python3-gi-cairo \ gobject-introspection \ libgirepository-2.0-dev \ libcairo2-dev \ curl \ sudo \ xvfb \ xterm \ xdotool \ imagemagick \ fonts-dejavu-core \ fonts-noto-core \ fonts-noto-cjk \ fonts-noto-color-emoji \ && apt-get clean # Install uv globally to /usr/local/bin RUN curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR=/usr/local/bin sh # Set uv to use temporary venv location outside workspace ENV UV_PROJECT_ENVIRONMENT=/home/ubuntu/.venv-urwid # Without this, the container's only locale is the non-UTF-8 "C", which # breaks docs/tools/screenshots.sh: xterm mis-decodes the multi-byte # UTF-8 the doc examples display (CJK, Georgian, box-drawing, emoji). ENV LANG=C.UTF-8 ENV LC_ALL=C.UTF-8 # Xvfb (used by docs/tools/compile_pngs.sh) needs this directory to # exist with these permissions before it can create its socket; a # non-root container user can't create it itself. RUN mkdir -p /tmp/.X11-unix && chmod 1777 /tmp/.X11-unix urwid-urwid-69907e4/.devcontainer/devcontainer.json000066400000000000000000000020551526073734300224140ustar00rootroot00000000000000{ "build": { "dockerfile": "dev.dockerfile" }, "features": { "ghcr.io/devcontainers/features/github-cli": {} }, "customizations": { "jetbrains": { "settings": { "com.intellij:app:Vcs-Log-App-Settings.show_changes_from_parents": true } }, "vscode": { "settings": { "python.terminal.activateEnvInCurrentTerminal": true, "python.defaultInterpreterPath": "/home/ubuntu/.venv-urwid/bin/python", "python.testing.unittestEnabled": true }, "extensions": [ "ms-python.python", "ms-python.debugpy", "ms-python.vscode-pylance", "charliermarsh.ruff", "ms-azuretools.vscode-docker", "GitHub.vscode-github-actions" ] } }, "name": "urwid-dev", "containerUser": "ubuntu", "init": true, "postCreateCommand": "uv venv /home/ubuntu/.venv-urwid && uv sync --all-groups", "postStartCommand": "uv sync --all-groups --all-extras", "mounts": [ "source=${localEnv:HOME}/.ssh,target=/home/ubuntu/.ssh,type=bind,readonly" ] } urwid-urwid-69907e4/.dockerignore000077700000000000000000000000001526073734300207362.gitignoreustar00rootroot00000000000000urwid-urwid-69907e4/.editorconfig000066400000000000000000000005161526073734300167560ustar00rootroot00000000000000root = true [*] charset = utf-8 end_of_line = lf indent_style = space insert_final_newline = true trim_trailing_whitespace = true [*.{py,pyi,pyx,pxd}] indent_size = 4 max_line_length = 120 [*.{yml,yaml,json,ini,toml}] indent_size = 2 max_line_length = 120 [*.{rst,md}] indent_size = 2 [{Makefile,Makefile.in}] indent_style = tab urwid-urwid-69907e4/.github/000077500000000000000000000000001526073734300156375ustar00rootroot00000000000000urwid-urwid-69907e4/.github/CODE_OF_CONDUCT.md000066400000000000000000000125611526073734300204430ustar00rootroot00000000000000 # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD]. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations urwid-urwid-69907e4/.github/ISSUE_TEMPLATE/000077500000000000000000000000001526073734300200225ustar00rootroot00000000000000urwid-urwid-69907e4/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000005621526073734300225170ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: "[BUG]" labels: bug assignees: '' --- ##### Description: [...] ##### Affected versions (if applicable) - [ ] `master` branch (specify commit) - [ ] Latest stable version from `pypi` - [ ] Other (specify source) ##### Steps to reproduce (if applicable) [...] ##### Expected/actual outcome [...] urwid-urwid-69907e4/.github/ISSUE_TEMPLATE/docs.md000066400000000000000000000004161526073734300212750ustar00rootroot00000000000000--- name: Documentation issue about: Documentation related issue or request title: "[docs]" labels: docs assignees: '' --- ##### Description: [...] ##### Affected versions (if applicable) - [ ] `master` branch (specify commit) - [ ] Latest stable version from `pypi` urwid-urwid-69907e4/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011401526073734300235430ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: "[Feature]" labels: Feature assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here.urwid-urwid-69907e4/.github/ISSUE_TEMPLATE/question.md000066400000000000000000000004321526073734300222120ustar00rootroot00000000000000--- name: Question about: Project related question title: "[Question]" labels: question assignees: '' --- ##### Description: [...] ##### Affected versions (if applicable) - [ ] `master` branch (specify commit) - [ ] Latest stable version from `pypi` - [ ] Other (specify source) urwid-urwid-69907e4/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000010461526073734300214410ustar00rootroot00000000000000##### Checklist - [ ] I've ensured that similar functionality has not already been implemented - [ ] I've ensured that similar functionality has not earlier been proposed and declined - [ ] I've branched off the `master` branch - [ ] I've merged fresh upstream into my branch recently - [ ] I've ran `tox` successfully in local environment - [ ] I've included docstrings and/or documentation and/or examples for my code (if this is a new feature) ##### Description: *(P. S. If this pull requests fixes an existing issue, please specify which one.)* urwid-urwid-69907e4/.github/dependabot.yml000066400000000000000000000011251526073734300204660ustar00rootroot00000000000000# To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "github-actions" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" - package-ecosystem: pip directory: "/" schedule: interval: weeklyurwid-urwid-69907e4/.github/labeler.yml000066400000000000000000000021261526073734300177710ustar00rootroot00000000000000"CI/CD": - all: - changed-files: - any-glob-to-any-file: - ".github/workflows/**" - "tox.ini" - ".yamllint.yml" "Maintenance": - all: - changed-files: - any-glob-to-any-file: - ".github/*" - ".github/ISSUE_TEMPLATE/*" "tests": - all: - changed-files: - any-glob-to-any-file: - "tests/**" "docs": - any: - changed-files: - any-glob-to-any-file: - "docs/**" - "examples/**" - "README.rst" "Widget": - any: - changed-files: - any-glob-to-any-file: - "urwid/widget/**" - "urwid/numedit.py" - "urwid/vterm.py" "Display": - any: - changed-files: - any-glob-to-any-file: - "urwid/display/**" "Event loop": - any: - changed-files: - any-glob-to-any-file: - "urwid/event_loop/**" "Canvas": - any: - changed-files: - any-glob-to-any-file: - "urwid/canvas.py" urwid-urwid-69907e4/.github/release.yml000066400000000000000000000012141526073734300200000ustar00rootroot00000000000000changelog: exclude: labels: - "CI/CD" - "Maintenance" - "in progress" - "tests" authors: - "dependabot[bot]" - "dependabot" categories: - title: Breaking Changes âš  labels: - "Breaking" - title: New features 🗹 labels: - "Feature" - title: Deprecations âš¡ labels: - "Deprecation" - title: Bug fixes 🕷 labels: - "bug" - "regression" - title: Documentation 🕮 labels: - "docs" - title: Refactoring 🛠 labels: - "refactoring" - title: Other Changes labels: - "*"urwid-urwid-69907e4/.github/workflows/000077500000000000000000000000001526073734300176745ustar00rootroot00000000000000urwid-urwid-69907e4/.github/workflows/documentation.yml000066400000000000000000000022611526073734300232710ustar00rootroot00000000000000name: Documentation on: push: branches-ignore: - 'dependabot/**' - 'gh-pages' tags: - "[0-9]+.[0-9]+.[0-9]+" # Canonical versioned tags - "[0-9]+.[0-9]+.[0-9]+.post[0-9]+" # Canonical rebuild tags pull_request: branches-ignore: - 'dependabot/**' - 'gh-pages' paths: - "docs/**" - "README.rst" - "urwid/**" - "pyproject.toml" - ".github/workflows/documentation.yml" jobs: Build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: fetch-depth: 0 # need for setuptools_scm - name: Install uv uses: astral-sh/setup-uv@v10.2.0 - name: Create virtual environment run: uv venv - name: Install python dependencies run: uv pip install -U --group pyproject.toml:documentation - name: Install main package with all extras run: uv pip install -U -e ".[curses,serial,tornado,trio,twisted,zmq]" - name: Build documentation run: uv run --no-project sphinx-build docs build/documentation - uses: actions/upload-artifact@v7 with: path: build/documentation name: documentation urwid-urwid-69907e4/.github/workflows/isolated_mypy.yml000066400000000000000000000014441526073734300233040ustar00rootroot00000000000000name: Isolated Mypy check on: pull_request: paths: - "mypy-requirements.txt" - ".github/workflows/isolated_mypy.yml" concurrency: group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}" cancel-in-progress: true jobs: Mypy: name: Check with mypy runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: Install uv uses: astral-sh/setup-uv@v10.2.0 - name: Create virtual environment run: uv venv - name: Install package with all extras run: uv pip install -e ".[curses,serial,tornado,trio,twisted,zmq]" - name: Install dependencies run: uv pip install --upgrade -r mypy-requirements.txt - name: Type check with mypy run: uv run --no-project mypy urwid urwid-urwid-69907e4/.github/workflows/isolated_pylint.yml000066400000000000000000000015051526073734300236230ustar00rootroot00000000000000name: Isolated PyLint check on: pull_request: paths: - "pylint-requirements.txt" - ".github/workflows/isolated_pylint.yml" concurrency: group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}" cancel-in-progress: true jobs: PyLint: name: Check with pylint runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: Install uv uses: astral-sh/setup-uv@v10.2.0 - name: Create virtual environment run: uv venv - name: Install package with all extras run: uv pip install -e ".[curses,serial,tornado,trio,twisted,zmq]" - name: Install dependencies run: uv pip install --upgrade -r pylint-requirements.txt - name: Lint with pylint run: uv run --no-project pylint --output-format=github urwid urwid-urwid-69907e4/.github/workflows/labels.yml000066400000000000000000000003471526073734300216650ustar00rootroot00000000000000name: Set PR labels on: pull_request_target: {} jobs: triage: permissions: contents: read pull-requests: write runs-on: ubuntu-latest steps: - uses: actions/labeler@v7 with: dot: trueurwid-urwid-69907e4/.github/workflows/pythonpackage.yml000066400000000000000000000133631526073734300232620ustar00rootroot00000000000000name: Python package on: push: branches-ignore: - 'dependabot/**' - 'gh-pages' tags: - "[0-9]+.[0-9]+.[0-9]+" # Canonical versioned tags - "[0-9]+.[0-9]+.[0-9]+.post[0-9]+" # Canonical rebuild tags pull_request: paths: - "urwid/**" - "pyproject.toml" - "MANIFEST.in" - "README.rst" - "COPYING" - "classifiers.txt" - "requirements.txt" - "tests/**" - "examples/**" - "pytest_requirements.txt" - ".github/workflows/pythonpackage.yml" concurrency: group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}" cancel-in-progress: true jobs: Ruff-format: name: Validate formatting runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: Check code style with ruff uses: astral-sh/ruff-action@v4.1.0 with: args: "format --check" Ruff: name: Check with Ruff runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: Lint with ruff uses: astral-sh/ruff-action@v4.1.0 with: args: "check --output-format github" PyLint: name: Check with pylint runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: Install uv uses: astral-sh/setup-uv@v10.2.0 - name: Create virtual environment run: uv venv - name: Install package with all extras run: uv pip install -e ".[curses,serial,tornado,trio,twisted,zmq]" - name: Install dependencies run: uv pip install --upgrade -r pylint-requirements.txt - name: Lint with pylint run: uv run --no-project pylint --output-format=github urwid Mypy: name: Check with mypy runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: Install uv uses: astral-sh/setup-uv@v10.2.0 - name: Create virtual environment run: uv venv - name: Install package with all extras run: uv pip install -e ".[curses,serial,tornado,trio,twisted,zmq]" - name: Install dependencies run: uv pip install --upgrade -r mypy-requirements.txt - name: Type check with mypy run: uv run --no-project mypy urwid Test: needs: [ Ruff-format, Ruff, PyLint, Mypy ] runs-on: ${{ matrix.os }} strategy: max-parallel: 6 matrix: os: [ "ubuntu-latest", "windows-latest" ] # , "macos-latest" # enable macOS only if OS specific branch will be added python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13", "3.14" ] steps: - uses: actions/checkout@v7 with: fetch-depth: 0 # need for setuptools_scm - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} allow-prereleases: true - name: Install uv uses: astral-sh/setup-uv@v10.2.0 - name: Create virtual environment run: uv venv --python-preference only-system --python ${{ matrix.python-version }} - name: Install dependencies run: uv pip install -r pytest_requirements.txt - name: Install package with all extras run: uv pip install -e ".[curses,serial,tornado,trio,twisted,zmq]" - name: Test run: uv run --no-project python -m pytest --cov-report=term --cov-report=xml - name: Coveralls Parallel uses: coverallsapp/github-action@v2 with: flag-name: run-${{ matrix.python-version }}-${{ matrix.os }} parallel: true file: coverage.xml UploadCoverage: name: Upload coverage to Coveralls needs: [ Test ] if: ${{ always() }} runs-on: ubuntu-latest steps: - name: Coveralls Finished uses: coverallsapp/github-action@v2 with: parallel-finished: true build: name: Build distribution needs: [ Test, Ruff-format, Ruff, PyLint ] runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: fetch-depth: 0 # need for setuptools_scm - name: Install uv uses: astral-sh/setup-uv@v10.2.0 - name: Build dist run: uv build - uses: actions/upload-artifact@v7 with: path: dist/*.tar.gz name: built-sdist retention-days: 3 - uses: actions/upload-artifact@v7 with: path: dist/*.whl name: built-bdist retention-days: 3 Metadata: name: Validate metadata runs-on: ubuntu-latest needs: [ build ] steps: - uses: actions/checkout@v7 - name: Install uv uses: astral-sh/setup-uv@v10.2.0 - uses: actions/download-artifact@v8 with: # unpacks default artifact into dist/ # if `name: wheels` is omitted, the action will create extra parent dir pattern: built-* merge-multiple: true path: dist - name: Validate metadata run: uvx twine check dist/* upload_pypi: needs: [ build, Metadata ] # upload to PyPI on every tag if: github.event_name == 'push' && github.ref_type == 'tag' && github.repository == 'urwid/urwid' # alternatively, to publish when a GitHub Release is created, use the following rule: # if: github.event_name == 'release' && github.event.action == 'published' runs-on: ubuntu-latest environment: name: pypi url: https://pypi.org/p/urwid permissions: id-token: write # IMPORTANT: this permission is mandatory for trusted publishing steps: - uses: actions/download-artifact@v8 with: pattern: built-* merge-multiple: true path: dist - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 urwid-urwid-69907e4/.github/workflows/yamllint.yml000066400000000000000000000012731526073734300222530ustar00rootroot00000000000000name: Lint YAML on: pull_request: paths: - "**.yml" - "**.yaml" - "yamllint-requirements.txt" concurrency: group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}" cancel-in-progress: true jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: Install uv uses: astral-sh/setup-uv@v10.2.0 - name: Lint YAML files run: uv run --no-project --with-requirements yamllint-requirements.txt yamllint . -f github actionlint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - name: Check workflow files uses: reviewdog/action-actionlint@v1 urwid-urwid-69907e4/.gitignore000066400000000000000000000175601526073734300163000ustar00rootroot00000000000000### NotepadPP template # Notepad++ backups # *.bak ### VisualStudioCode template .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json !.vscode/*.code-snippets # Local History for Visual Studio Code .history/ # Built Visual Studio Code Extensions *.vsix ### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf # AWS User-specific .idea/**/aws.xml # Generated files .idea/**/contentModel.xml # Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml .idea/**/libraries # Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. # .idea/artifacts # .idea/compiler.xml # .idea/jarRepositories.xml # .idea/modules.xml # .idea/*.iml # .idea/modules # *.iml # *.ipr # CMake cmake-build-*/ # Mongo Explorer plugin .idea/**/mongoSettings.xml # File-based project format *.iws # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # SonarLint plugin .idea/sonarlint/ # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Editor-based Rest Client .idea/httpRequests # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser ### Linux template *~ # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* # KDE directory preferences .directory # Linux trash folder which might appear on any partition or disk .Trash-* # .nfs files are created when an open file is removed but is still being accessed .nfs* ### VirtualEnv template # Virtualenv # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ .Python [Bb]in [Ii]nclude [Ll]ib [Ll]ib64 [Ll]ocal [Ss]cripts pyvenv.cfg .venv pip-selfcheck.json ### Emacs template # -*- mode: gitignore; -*- *~ \#*\# /.emacs.desktop /.emacs.desktop.lock *.elc auto-save-list tramp .\#* # Org-mode .org-id-locations *_archive # flymake-mode *_flymake.* # eshell files /eshell/history /eshell/lastdir # elpa packages /elpa/ # reftex files *.rel # AUCTeX auto folder /auto/ # cask packages .cask/ dist/ # Flycheck flycheck_*.el # server auth directory /server/ # projectiles files .projectile # directory configuration .dir-locals.el # network security /network-security.data ### Vim template # Swap [._]*.s[a-v][a-z] !*.svg # comment out if you don't need vector files [._]*.sw[a-p] [._]s[a-rt-v][a-z] [._]ss[a-gi-z] [._]sw[a-p] # Session Session.vim Sessionx.vim # Temporary .netrwhist *~ # Auto-generated tag files tags # Persistent undo [._]*.un~ ### Windows template # Windows thumbnail cache files Thumbs.db Thumbs.db:encryptable ehthumbs.db ehthumbs_vista.db # Dump file *.stackdump # Folder config file [Dd]esktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msix *.msm *.msp # Windows shortcuts *.lnk ### macOS template # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### Python template # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/#use-with-ide .pdm.toml # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ ### SublimeText template # Cache files for Sublime Text *.tmlanguage.cache *.tmPreferences.cache *.stTheme.cache # Workspace files are user-specific *.sublime-workspace # Project files should be checked into the repository, unless a significant # proportion of contributors will probably not be using Sublime Text # *.sublime-project # SFTP configuration file sftp-config.json sftp-config-alt*.json # Package control specific files Package Control.last-run Package Control.ca-list Package Control.ca-bundle Package Control.system-ca-bundle Package Control.cache/ Package Control.ca-certs/ Package Control.merged-ca-bundle Package Control.user-ca-bundle oscrypto-ca-bundle.crt bh_unicode_properties.cache # Sublime-github package stores a github token in this file # https://packagecontrol.io/packages/sublime-github GitHub.sublime-settings ### GPG template secring.* # Generated version /urwid/version.py # Built packages /wheelhouse ### Documentation build /buildurwid-urwid-69907e4/.pre-commit-config.yaml000066400000000000000000000017041526073734300205620ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace - id: mixed-line-ending args: [ --fix=lf ] - id: check-yaml - id: check-toml - id: check-json - id: check-merge-conflict - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.16.9 hooks: - id: ruff-check args: [ --show-fixes, --fix ] - id: ruff-format - repo: https://github.com/tox-dev/pyproject-fmt rev: v2.26.0 hooks: - id: pyproject-fmt - repo: https://github.com/adrienverge/yamllint.git rev: v1.38.0 hooks: - id: yamllint - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 # Use the ref you want to point at hooks: - id: python-check-blanket-noqa - id: python-check-blanket-type-ignore - id: rst-directive-colons - id: rst-inline-touching-normal urwid-urwid-69907e4/.yamllint.yml000066400000000000000000000013541526073734300167340ustar00rootroot00000000000000--- yaml-files: - '*.yaml' - '*.yml' - '.yamllint' rules: line-length: max: 120 allow-non-breakable-inline-mappings: true level: warning indentation: indent-sequences: consistent brackets: max-spaces-inside: 1 level: warning document-end: disable document-start: disable truthy: disable braces: enable colons: level: warning commas: level: warning comments: level: warning comments-indentation: level: warning empty-lines: enable empty-values: disable float-values: disable hyphens: enable key-duplicates: enable key-ordering: disable new-line-at-end-of-file: disable new-lines: enable octal-values: disable quoted-strings: disable trailing-spaces: enable urwid-urwid-69907e4/COPYING000066400000000000000000000635021526073734300153400ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser 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 Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "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 LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY 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 LIBRARY (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 LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! urwid-urwid-69907e4/MANIFEST.in000066400000000000000000000002511526073734300160330ustar00rootroot00000000000000include COPYING CHANGELOG README.rst include requirements.txt classifiers.txt include urwid/display/_web.js urwid/display/_web.css prune tests prune docs prune examples urwid-urwid-69907e4/README.rst000066400000000000000000000266261526073734300160020ustar00rootroot00000000000000Urwid ===== |pypi| |docs| |gitter| |ci| |pre-commit| |coveralls| About ===== Urwid is a console user interface library for Python on Linux, OSX, Cygwin or other unix-like OS and partially supports Windows OS (see below). It includes many features useful for text console application developers including: - Applications resize quickly and smoothly - Automatic, programmable text alignment and wrapping - Simple markup for setting text attributes within blocks of text - Powerful list box with programmable content for scrolling all widget types - Your choice of event loops: Twisted, Glib (deprecated), Tornado, asyncio, trio, ZeroMQ or select-based loop - Pre-built widgets include edit boxes, buttons, check boxes and radio buttons - Display modules include raw, curses, and experimental LCD and web displays - Support for UTF-8, simple 8-bit and CJK encodings - 24-bit (true color), 256 color, and 88 color mode support - Compatible with Python 3.9+ and PyPy Home Page: http://urwid.org/ Installation ============ To install using pip .. code:: bash pip install urwid For advanced functionality extra requirements need to be installed. Example for ZeroMQ event loop and LCD display: .. code:: bash pip install urwid[serial,zmq] Alternatively if you are on Debian or Ubuntu .. code:: bash apt-get install python3-urwid Windows support notes ===================== Windows support is limited to the Windows 10+ due to ANSI support requirement. * Not supported: 1. Terminal widget and all related render API (TermCanvas, TermCharset, TermModes, TermScroller) 2. Any file descriptors except sockets (Windows OS limitation) 3. ZMQEventLoop. * Special requirements: 1. Extra libraries required for curses display support: .. code-block:: bash pip install urwid[curses] * CursesDisplay incorrectly handles mouse input in case of fast actions. * Only UTF-8 mode is supported. Testing ======= To run tests locally, install & run `tox`. You must have appropriate Python versions installed to run `tox` for each of them. To test code in all Python versions: .. code:: bash tox # Test all versions specified in tox.ini tox -e py39 # Test Python 3.9 only tox -e py313t # Test Python 3.13 with free-threading tox -e pypy3 # Test PyPy3 tox -e py39,py310,pypy3 # Test specific versions Testing different Python implementations ---------------------------------------- **CPython standard (default)** Tests all optional extras including C extensions (glib, zmq, twisted). .. code:: bash tox -e py313 **Free-threaded CPython (3.13+)** Tests with compatible extras (tornado, trio, twisted, serial). Excludes C extensions without free-threading wheel support. .. code:: bash tox -e py313t # Python 3.13 free-threaded (PEP 703) tox -e py314t # Python 3.14 free-threaded tox -e py315t # Python 3.15 free-threaded **PyPy3** Tests with compatible extras (tornado, trio, serial). Excludes problematic C extensions. .. code:: bash tox -e pypy3 **GraalPy** Tests with compatible extras (tornado, serial). Excludes extras that need C extensions GraalPy doesn't support (glib, zmq) or that have known compatibility issues (trio, twisted). GraalPy also ships no ``_curses`` module, so curses-dependent tests are expected to fail, and the Terminal widget (``urwid.vterm``) is unsupported since its ``fcntl``/``pty`` usage doesn't work on GraalPy, so its tests are skipped rather than run. .. code:: bash tox -e graalpy311 # GraalPy 3.11 tox -e graalpy312 # GraalPy 3.12 Supported Python versions ========================= Urwid supports: - CPython 3.9, 3.10, 3.11, 3.12, 3.13, 3.14, 3.15 - CPython 3.13+ with free-threading (PEP 703 ``--disable-gil``) - PyPy 3.x - GraalPy 3.11, 3.12 (experimental) Python implementation notes =========================== **CPython (standard Python)** Full support for all optional features. All extras (``curses``, ``glib``, ``serial``, ``tornado``, ``trio``, ``twisted``, ``zmq``) can be installed. **CPython with Free-Threading (3.13, 3.14, 3.15+)** Urwid core and pure-Python event loops are fully supported. Some extras require C extensions that may not have free-threading wheels available: - ✅ Supported: core, tornado, trio, twisted (24.10.0+), serial, lcd - â�Œ Unsupported: glib, zmq (require C extensions with free-threading wheel support) For free-threading Python, install with compatible extras: .. code:: bash python3.13 -m pip install 'urwid[tornado,trio,serial]' **PyPy** Urwid core is fully supported. Some extras may have compatibility issues: - ✅ Supported: core, tornado, trio, serial, lcd - âš ï¸� Limited: twisted (may have issues, test before using) - â�Œ Unsupported: glib, zmq (C extensions with limited PyPy support) **GraalPy (experimental)** Urwid core and the ``select``/``asyncio`` event loops are supported. Some extras, display modules, and the Terminal widget are not: - ✅ Supported: core, tornado, serial, lcd - â�Œ Unsupported: curses (GraalPy ships no ``_curses`` module), glib, zmq, twisted, trio (no C extension support, or known compatibility issues), Terminal widget/``urwid.vterm`` (like on Windows, ``fcntl``/``pty`` usage doesn't work on GraalPy) Authors ======= Creator ------- `wardi `_ Maintainers ----------- `and3rson `_, `tonycpsu `_, `ulidtko `_, `penguinolog `_ Contributors ------------ `1in7billion `_, `abadger `_, `agrenott `_, `akorb `_, `alethiophile `_, `aleufroy `_, `alobbs `_, `amjltc295 `_, `and-semakin `_, `andrewshadura `_, `andy-z `_, `anttin2020 `_, `Apteryks `_, `Arfrever `_, `AutoAwesome `_, `belak `_, `berney `_, `bk2204 `_, `BkPHcgQL3V `_, `bwesterb `_, `carlos-jenkins `_, `Certseeds `_, `Chipsterjulien `_, `chrisspen `_, `cltrudeau `_, `Codeberg-AsGithubAlternative-buhtz `_, `cortesi `_, `d0c-s4vage `_, `derdon `_, `dholth `_, `dimays `_, `dlo `_, `dnaeon `_, `doddo `_, `douglas-larocca `_, `drestebon `_, `dsotr `_, `dwf `_, `EdwardBetts `_, `elenril `_, `EnricoBilla `_, `extempore `_, `fabiand `_, `floppym `_, `flowblok `_, `fmoreau `_, `goncalopp `_, `Gordin `_, `GregIngelmo `_, `grzaks `_, `gurupras `_, `HarveyHunt `_, `Hoolean `_, `hukka `_, `hydratim `_, `ids1024 `_, `imrek `_, `isovector `_, `itaisod `_, `ixxra `_, `jeblair `_, `johndeaton `_, `jonblack `_, `jspricke `_, `kedder `_, `Kelketek `_, `KennethNielsen `_, `kesipyc `_, `kkrolczyk `_, `Kwpolska `_, `Lahorde `_, `laike9m `_, `larsks `_, `lfam `_, `lgbaldoni `_, `lighth7015 `_, `livibetter `_, `Lothiraldan `_, `Mad-ness `_, `madebr `_, `magniff `_, `marlox-ouda `_, `mattymo `_, `mdtrooper `_, `mgk `_, `mimi1vx `_, `mobyte0 `_, `MonAaraj `_, `MonthlyPython `_, `mountainstorm `_, `mselee `_, `mwhudson `_, `naquad `_, `nchavez324 `_, `neumond `_, `nolash `_, `ntamas `_, `nyov `_, `ocarneiro `_, `okayzed `_, `pquentin `_, `rbanffy `_, `ReddyKilowatt `_, `regebro `_, `renegarcia `_, `rianhunter `_, `roburban `_, `RRMoelker `_, `rwarren `_, `scopatz `_, `seanhussey `_, `seonon `_, `shadedKE `_, `sithglan `_, `Sjc1000 `_, `sporkexec `_, `squrky `_, `ssbr `_, `techdragon `_, `thehunmonkgroup `_, `thisch `_, `thornycrackers `_, `TomasTomecek `_, `tompickering `_, `tony `_, `ttanner `_, `tu500 `_, `uSpike `_, `vega0 `_, `vit1251 `_, `waveform80 `_, `Wesmania `_, `xandfury `_, `xndcn `_, `zhongshangwu `_, `zrax `_ .. |pypi| image:: https://img.shields.io/pypi/v/urwid :alt: current version on PyPi :target: https://pypi.python.org/pypi/urwid .. |docs| image:: https://github.com/urwid/urwid/actions/workflows/documentation.yml/badge.svg?branch=master :alt: Documentation Status :target: https://urwid.org .. |gitter| image:: https://img.shields.io/gitter/room/urwid/community :alt: Gitter :target: https://gitter.im/urwid/community .. |ci| image:: https://github.com/urwid/urwid/actions/workflows/pythonpackage.yml/badge.svg?branch=master :target: https://github.com/urwid/urwid/actions :alt: CI status .. |pre-commit| image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit :target: https://github.com/pre-commit/pre-commit :alt: pre-commit .. |coveralls| image:: https://coveralls.io/repos/github/urwid/urwid/badge.svg :alt: test coverage :target: https://coveralls.io/github/urwid/urwid urwid-urwid-69907e4/SECURITY.md000066400000000000000000000001301526073734300160620ustar00rootroot00000000000000# Security Report vulnerabilities at https://github.com/urwid/urwid/security/advisoriesurwid-urwid-69907e4/classifiers.txt000066400000000000000000000017761526073734300173620ustar00rootroot00000000000000Development Status :: 5 - Production/Stable Environment :: Console Environment :: Console :: Curses Intended Audience :: Developers Operating System :: POSIX Operating System :: Unix Operating System :: MacOS :: MacOS X Operating System :: Microsoft :: Windows Topic :: Software Development :: Libraries :: Python Modules Topic :: Software Development :: User Interfaces Topic :: Software Development :: Widget Sets Programming Language :: Python :: 3 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 Programming Language :: Python :: 3.13 Programming Language :: Python :: 3.14 Programming Language :: Python :: 3.15 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Programming Language :: Python :: Implementation :: GraalPy Programming Language :: Python :: Free Threading :: 2 - Beta Typing :: Typed urwid-urwid-69907e4/conftest.py000066400000000000000000000046561526073734300165110ustar00rootroot00000000000000"""Pytest configuration: skip collecting modules that cannot be imported on this platform.""" from __future__ import annotations import importlib.util import sys import typing if typing.TYPE_CHECKING: from pathlib import Path IS_WINDOWS = sys.platform == "win32" IS_GRAALPY = sys.implementation.name == "graalpy" # Modules that import only on one platform. Urwid picks between them at runtime # (see `urwid/display/raw.py` and the `sys.platform` guards in `urwid/__init__.py`), # so `--doctest-modules` has to make the same choice instead of importing everything. _PLATFORM_ONLY: typing.Final[dict[str, bool]] = { "_posix_raw_display.py": not IS_WINDOWS, "vterm.py": not IS_WINDOWS, "test_vterm.py": not IS_WINDOWS, "_win32.py": IS_WINDOWS, "_win32_raw_display.py": IS_WINDOWS, # ZMQEventLoop crashes the interpreter on Windows, so urwid never imports it there. "zmq_loop.py": not IS_WINDOWS, } # Modules importing an optional backend at top level. Urwid keeps working without the # backend installed, and their doctests have to be skipped on the same condition. _OPTIONAL_BACKEND: typing.Final[dict[str, str]] = { "curses.py": "_curses", "glib_loop.py": "gi", "tornado_loop.py": "tornado", "trio_loop.py": "trio", "twisted_loop.py": "twisted", "zmq_loop.py": "zmq", } # GraalPy ships a pure-Python `curses` package (so `find_spec("curses")` succeeds) but no compiled # `_curses` extension backing it, so importing `curses` itself raises # ModuleNotFoundError for `_curses` rather than failing to resolve up front. Treated as always # unavailable there rather than probed with `find_spec`, since a partial/shim module on a # non-CPython interpreter is not reliable evidence either way. _UNAVAILABLE_ON_GRAALPY: typing.Final[frozenset[str]] = frozenset({"_curses"}) def _backend_available(name: str) -> bool: if IS_GRAALPY and name in _UNAVAILABLE_ON_GRAALPY: return False try: return importlib.util.find_spec(name) is not None except (ImportError, ValueError): return False def pytest_ignore_collect(collection_path: Path) -> bool | None: """Skip collecting a platform-only or optional-backend test module that cannot run here.""" name = collection_path.name if not _PLATFORM_ONLY.get(name, True): return True backend = _OPTIONAL_BACKEND.get(name) if backend is not None and not _backend_available(backend): return True return None urwid-urwid-69907e4/docs/000077500000000000000000000000001526073734300152275ustar00rootroot00000000000000urwid-urwid-69907e4/docs/Makefile000066400000000000000000000010311526073734300166620ustar00rootroot00000000000000 PYTHON_SOURCE=$(shell echo */*.py.xdotool) TARGETS=${PYTHON_SOURCE:.py.xdotool=1.png} TOOLS=tools/compile_pngs.sh tools/screenshots.sh EXTERNAL_PROGRAMS=Xvfb xterm xdotool import .PHONY: all clean ${EXTERNAL_PROGRAMS} all: ${TARGETS} ${TARGETS}: %1.png: %.py %.py.xdotool ${TOOLS} ${EXTERNAL_PROGRAMS} tools/compile_pngs.sh $< ${EXTERNAL_PROGRAMS}: @which $@ > /dev/null || (echo "Need to install $@"; exit 1) apt-get-dependencies: sudo apt-get install xvfb xterm xdotool imagemagick fonts-dejavu-core clean: rm -f ${TARGETS} urwid-urwid-69907e4/docs/changelog.rst000066400000000000000000003213151526073734300177150ustar00rootroot00000000000000 Changelog --------- Urwid 4.2.5 ============ 2026-10-05 Deprecations âš¡ +++++++++++++++ * Deprecate non-integer index in deque insert(): ``MonitoredDeque.insert()`` and ``MonitoredFocusDeque.insert()`` warn on an index without ``__index__`` (a float, a ``Decimal``, a numeric string); from version 5.0 such an index raises ``TypeError``, as ``collections.deque.insert()`` does by @penguinolog in https://github.com/urwid/urwid/pull/1399 * ⚡🕷 Deprecate the unused ``_sigcont_handler`` (removal in 5.0); also a bug fix, see "Major bug fixes" below: Do terminal signal work in the input loop, not in handlers by @penguinolog in https://github.com/urwid/urwid/pull/1369 Security fixes 🛡 +++++++++++++++++ * Replace C1 control characters (U+0080-U+009F) in raw display output, so displayed text cannot inject terminal escape sequences such as OSC 52 clipboard writes by @penguinolog in https://github.com/urwid/urwid/pull/1367 * Stop crafted terminal input (a paste with about 1000 ESC bytes, malformed mouse reports, an ESC before a terminal report) from crashing the input parser and ending the main loop by @penguinolog in https://github.com/urwid/urwid/pull/1368 * Read terminal input at most 64 KiB per wake-up, so a fast producer cannot grow memory without bound; also fix ``ValueError`` from ``select()`` on descriptors numbered 1024 or higher by @penguinolog in https://github.com/urwid/urwid/pull/1373 * Limit the memory and time ``ANSIText`` spends on untrusted output (cursor moves, padding, long CSI and OSC sequences); out-of-range SGR colours are ignored instead of raising by @penguinolog in https://github.com/urwid/urwid/pull/1382 * Lay out long text in linear time instead of quadratic time (80000 characters without spaces: 5.4 s -> 65 ms); also fix wrong attribute runs in markup that mixes ``str`` and ``bytes`` by @penguinolog in https://github.com/urwid/urwid/pull/1386 * Stop a masked ``Edit`` from exposing its text in ``repr()`` and in ``ValueError``; refuse control characters as input; stop ``IntEdit`` from accepting more digits than ``int()`` can convert by @penguinolog in https://github.com/urwid/urwid/pull/1387 * Handle very deep and cyclic trees: no ``RecursionError`` on deep trees, ``TreeWidgetError`` instead of an endless loop on a cycle, faster sibling lookup (up to 98% less time), and cached nodes of removed keys are freed by @penguinolog in https://github.com/urwid/urwid/pull/1389 * End an OSC string at an ESC that does not start the terminator, so the text after it can no longer end up in the window title by @penguinolog in https://github.com/urwid/urwid/pull/1392 * Keep zero-width control characters and escape sequences out of drawn text, so untrusted text no longer overflows its column and overwrites the next widget by @penguinolog in https://github.com/urwid/urwid/pull/1398 Major bug fixes 🕷 ++++++++++++++++++ * âš¡ Do terminal signal work in the input loop, not in the signal handlers: suspend (SIGTSTP) and resize (SIGWINCH) no longer change the screen and terminal mode at an arbitrary point of a running draw; also deprecates the unused ``_sigcont_handler``, see "Deprecations" by @penguinolog in https://github.com/urwid/urwid/pull/1369 * Fix ``ZMQEventLoop.watch_file()`` with a ZMQ socket (raised ``KeyError``), and remove a watch by either the object or its descriptor number by @penguinolog in https://github.com/urwid/urwid/pull/1371 * Stop ``draw_screen`` from spinning at full CPU forever after the terminal can no longer be written to (hangup, broken pipe) by @penguinolog in https://github.com/urwid/urwid/pull/1375 * Restore the asyncio loop's exception handler after ``run()``, so an unrelated task exception no longer stops the application's own loop by @penguinolog in https://github.com/urwid/urwid/pull/1376 * Fix the Windows console reader: characters outside the BMP no longer end the reader and lose all later input, failed reads no longer spin, and stopping the screen no longer waits up to 5 s by @penguinolog in https://github.com/urwid/urwid/pull/1379 * Fix ``watch_pipe``: no second read task on a pipe that is still readable, no double close of a descriptor that may belong to another file, and no full-CPU spin at end of file (the callback gets ``b""`` once and the watch is removed) by @penguinolog in https://github.com/urwid/urwid/pull/1380 * Fix ``ListBox`` repeating items when a wrapping walker is shorter than the available height (issue #464) by @charan-rathore in https://github.com/urwid/urwid/pull/1381 * Fix ``ListBox`` hangs and slowdowns with many 0-height widgets (up to 98% less time), scrolling keys that never ended on wrapping bodies, ``ListBoxError`` from page up, page down and end, and ``ListBox`` objects kept alive by their walker by @penguinolog in https://github.com/urwid/urwid/pull/1383 * Free widgets that were dropped from the canvas cache: wrappers rebuilt around a cached child (for example ``GridFlow`` display widgets on each focus change) were never freed by @penguinolog in https://github.com/urwid/urwid/pull/1393 Corner case fixes 🕸 ++++++++++++++++++++ These problems need an unusual setup, an error path, or input from outside the normal range of values. * Skip a ready descriptor whose watch was removed or replaced by an earlier callback in the same pass, in ``SelectEventLoop`` and ``ZMQEventLoop`` by @penguinolog in https://github.com/urwid/urwid/pull/1370 * Clean up ``MainLoop`` when its event loop or ``start()`` fails: remove the idle callback and watches, and restore the terminal mode and screen buffer by @penguinolog in https://github.com/urwid/urwid/pull/1374 * Free a dropped raw display screen at once, without waiting for the cyclic garbage collector by @penguinolog in https://github.com/urwid/urwid/pull/1377 * Stop the gpm helper with a timeout (kill it if it does not exit) and tell the event loop when it starts or stops by @penguinolog in https://github.com/urwid/urwid/pull/1378 * Reject infinite, NaN and over-large widths and weights in ``Columns``, and fix overflow errors from widely different weights by @penguinolog in https://github.com/urwid/urwid/pull/1384 * Reject infinite, NaN and over-large sizes in ``Pile``, and fix overflow errors from large or widely different weights by @penguinolog in https://github.com/urwid/urwid/pull/1385 * Draw ``ProgressBar`` correctly when ``current`` or ``done`` is negative, zero, NaN, infinite or too large for a float by @penguinolog in https://github.com/urwid/urwid/pull/1388 * Keep ``BarGraph`` and ``GraphVScale`` rendering when ``top`` is zero or NaN, or a value, line or label is NaN, infinite or too large by @penguinolog in https://github.com/urwid/urwid/pull/1390 * Free signal classes created at run time, and keep a weak-argument handler disconnected when a callback interrupts ``connect`` or ``disconnect_by_key`` by @penguinolog in https://github.com/urwid/urwid/pull/1391 * Handle contents changed by a widget callback while a container or a focus list is working (``GridFlow``, ``Pile``, ``Columns``, ``MonitoredFocusList``, ``MonitoredFocusDeque``, ``RadioButton``), instead of continuing with stale indexes by @penguinolog in https://github.com/urwid/urwid/pull/1397 * ``TrioEventLoop``: an exception from an idle callback now ends ``run()``, and ``ExitMainLoop`` from one stops the loop, as with the other event loops; Trio used to log it and stop running idle callbacks by @penguinolog in https://github.com/urwid/urwid/pull/1399 Documentation 🕮 ++++++++++++++++ * Document that MainLoop.draw_screen belongs to the loop thread by @penguinolog in https://github.com/urwid/urwid/pull/1372 New Contributors ++++++++++++++++ * @charan-rathore made their first contribution in https://github.com/urwid/urwid/pull/1381 **Full Changelog**: https://github.com/urwid/urwid/compare/4.2.4...4.2.5 Urwid 4.2.4 ============ 2026-10-01 Bug fixes 🕷 ++++++++++++ * Fix Text.pack() wrapping a line after a tab stop by @penguinolog in https://github.com/urwid/urwid/pull/1364 * Fix ListBox focus restore on a shrunk body by @penguinolog in https://github.com/urwid/urwid/pull/1365 **Full Changelog**: https://github.com/urwid/urwid/compare/4.2.3...4.2.4 Urwid 4.2.3 ============ 2026-09-30 Bug fixes 🕷 ++++++++++++ * Fix Pile, Columns and GridFlow selectable flag caching by @penguinolog in https://github.com/urwid/urwid/pull/1362 **Full Changelog**: https://github.com/urwid/urwid/compare/4.2.2...4.2.3 Urwid 4.2.2 ============ 2026-09-30 Bug fixes 🕷 ++++++++++++ * Fix Edit wrapping on full len by @penguinolog in https://github.com/urwid/urwid/pull/1356 Refactoring 🛠 ++++++++++++++ * Simplify `AttrMap` setters to accept parameters like constructor does by @penguinolog in https://github.com/urwid/urwid/pull/1357 * Type palette names as Hashable and the optional fields as Optional by @penguinolog in https://github.com/urwid/urwid/pull/1358 * Fix attributes defined outside of init by @penguinolog in https://github.com/urwid/urwid/pull/1359 **Full Changelog**: https://github.com/urwid/urwid/compare/4.2.1...4.2.2 Urwid 4.2.1 ============ 2026-09-29 Bug fixes 🕷 ++++++++++++ * Fix: Empty `Text` render fail and 0 is valid tab size by @penguinolog in https://github.com/urwid/urwid/pull/1354 **Full Changelog**: https://github.com/urwid/urwid/compare/4.2.0...4.2.1 Urwid 4.2.0 ============ 2026-09-29 New features 🗹 +++++++++++++++ * Handle tabs by StandardTextLayout by @penguinolog in https://github.com/urwid/urwid/pull/1349 * HTML fragment can use all colours and styles like web display by @penguinolog in https://github.com/urwid/urwid/pull/1350 Deprecations âš¡ +++++++++++++++ * Deprecate monkeypatch of displays by HtmlGenerator by @penguinolog in https://github.com/urwid/urwid/pull/1352 * Deprecate GLibEventLoop by @penguinolog in https://github.com/urwid/urwid/pull/1340 Documentation 🕮 ++++++++++++++++ * Add minimal docstrings to all classes by @penguinolog in https://github.com/urwid/urwid/pull/1346 * Add minimal docstrings to all public methods by @penguinolog in https://github.com/urwid/urwid/pull/1347 * Add last docstrings to class initialisers by @penguinolog in https://github.com/urwid/urwid/pull/1348 **Full Changelog**: https://github.com/urwid/urwid/compare/4.1.7...4.2.0 Urwid 4.1.7 ============ 2026-09-23 Bug fixes 🕷 ++++++++++++ * Make Text's line translation cache safe for concurrent readers by @penguinolog in https://github.com/urwid/urwid/pull/1338 **Full Changelog**: https://github.com/urwid/urwid/compare/4.1.6...4.1.7 Urwid 4.1.6 ============ 2026-09-23 New features 🗹 +++++++++++++++ * Implement synchronised output by @penguinolog in https://github.com/urwid/urwid/pull/1333 Bug fixes 🕷 ++++++++++++ * Preserve focus when deleting negative-step slices by @jakezwang in https://github.com/urwid/urwid/pull/1329 Documentation 🕮 ++++++++++++++++ * Fix screenshot generation and re-generate screenshots by @penguinolog in https://github.com/urwid/urwid/pull/1326 * Switch screenshots generation to 2x size and no antialiasing by @penguinolog in https://github.com/urwid/urwid/pull/1327 Refactoring 🛠 ++++++++++++++ * Optimise signals emit path by @penguinolog in https://github.com/urwid/urwid/pull/1331 New Contributors ++++++++++++++++ * @jakezwang made their first contribution in https://github.com/urwid/urwid/pull/1329 **Full Changelog**: https://github.com/urwid/urwid/compare/4.1.5...4.1.6 Urwid 4.1.5 ============ 2026-09-18 New features 🗹 +++++++++++++++ * Support async callbacks set via MainLoop if backend support by @penguinolog in https://github.com/urwid/urwid/pull/1314 * Support async callbacks in ZMQEventLoop, make ZMQEventLoop async by @penguinolog in https://github.com/urwid/urwid/pull/1315 * Add full palette and AttrSpec support for WEB display by @penguinolog in https://github.com/urwid/urwid/pull/1316 * Add capital letters to thin 4x3 font by @penguinolog in https://github.com/urwid/urwid/pull/1323 Deprecations âš¡ +++++++++++++++ * Announce removal of old display import aliases by @penguinolog in https://github.com/urwid/urwid/pull/1312 Bug fixes 🕷 ++++++++++++ * Fix screen redraw on window resize for curses display by @penguinolog in https://github.com/urwid/urwid/pull/1317 * Implement nested popups support by @penguinolog in https://github.com/urwid/urwid/pull/1320 Refactoring 🛠 ++++++++++++++ * Optimise code and extend test coverage for Canvas classes by @penguinolog in https://github.com/urwid/urwid/pull/1313 * Optimise BarGraph logic by @penguinolog in https://github.com/urwid/urwid/pull/1321 **Full Changelog**: https://github.com/urwid/urwid/compare/4.1.4...4.1.5 Urwid 4.1.4 ============ 2026-09-17 New features 🗹 +++++++++++++++ * Support async callbacks in AsyncioEventLoop by @penguinolog in https://github.com/urwid/urwid/pull/1299 * Support async callbacks in TornadoEventLoop by @penguinolog in https://github.com/urwid/urwid/pull/1300 Deprecations âš¡ +++++++++++++++ * Clarify deprecation warnings by @penguinolog in https://github.com/urwid/urwid/pull/1310 Bug fixes 🕷 ++++++++++++ * Fix: close file descriptors on stop / exception by @penguinolog in https://github.com/urwid/urwid/pull/1297 * Fix `Frame.contents.iter()` / `len(Frame.contents)` by @penguinolog in https://github.com/urwid/urwid/pull/1303 * Fix "CanvasError: Canvas text is wider than the maxcol specified" by @penguinolog in https://github.com/urwid/urwid/pull/1302 * Fix overlay top widget setting via contents by @penguinolog in https://github.com/urwid/urwid/pull/1307 Documentation 🕮 ++++++++++++++++ * Strip redundant type explanation from docstrings by @penguinolog in https://github.com/urwid/urwid/pull/1301 Refactoring 🛠 ++++++++++++++ * Refactoring: get rid of oneliner / one-use local helpers by @penguinolog in https://github.com/urwid/urwid/pull/1298 **Full Changelog**: https://github.com/urwid/urwid/compare/4.1.3...4.1.4 Urwid 4.1.3 ============ 2026-09-11 Bug fixes 🕷 ++++++++++++ * Fix: Overlay top widget or bottom widget replace should trigger invalidation by @penguinolog in https://github.com/urwid/urwid/pull/1294 Documentation 🕮 ++++++++++++++++ * Documentation: force theme override to fix readability on wide screen by @penguinolog in https://github.com/urwid/urwid/pull/1288 Refactoring 🛠 ++++++++++++++ * Speed up the widget render hot paths by @penguinolog in https://github.com/urwid/urwid/pull/1290 **Full Changelog**: https://github.com/urwid/urwid/compare/4.1.2...4.1.3 Urwid 4.1.2 ============ 2026-09-07 Bug fixes 🕷 ++++++++++++ * Restore terminal palette if was modified by @penguinolog in https://github.com/urwid/urwid/pull/1278 * Handle issue with shard calculation inconsistency with shard_tail by @penguinolog in https://github.com/urwid/urwid/pull/1280 Documentation 🕮 ++++++++++++++++ * Documentation: fix typo in widget classes doc by @penguinolog in https://github.com/urwid/urwid/pull/1277 * Fix docstring for "draw_string" by @penguinolog in https://github.com/urwid/urwid/pull/1279 * Documentation: fix docstrings to follow PEP257 by @penguinolog in https://github.com/urwid/urwid/pull/1285 * Documentation: mass-add `raises` to docstrings by @penguinolog in https://github.com/urwid/urwid/pull/1286 **Full Changelog**: https://github.com/urwid/urwid/compare/4.1.1...4.1.2 Urwid 4.1.1 ============ 2026-09-03 Bug fixes 🕷 ++++++++++++ * Fix `SIGTSTP` handler for POSIX RAW display and explain behaviour by @penguinolog in https://github.com/urwid/urwid/pull/1271 * Fix regression in treetools: handle wrapped widgets inside TreeWidget by @penguinolog in https://github.com/urwid/urwid/pull/1274 * Stop CanvasCache._deps growing without bound by @afonsojanu in https://github.com/urwid/urwid/pull/1255 New Contributors ++++++++++++++++ * @afonsojanu made their first contribution in https://github.com/urwid/urwid/pull/1255 **Full Changelog**: https://github.com/urwid/urwid/compare/4.1.0...4.1.1 Urwid 4.1.0 ============ 2026-09-02 New features 🗹 +++++++++++++++ * Feat: detect terminal properties not only for basic xterm by @penguinolog in https://github.com/urwid/urwid/pull/1258 * feat: extend faint text support to vterm and displays by @penguinolog in https://github.com/urwid/urwid/pull/1263 * Feat: implement deque support for ListBox and `ANSIText` widget by @penguinolog in https://github.com/urwid/urwid/pull/1265 `ANSIText` is a `Text` like widget containing parsed `ANSI` text including color/style information and basic flags as `bel`/`leds`/`title`. Bug fixes 🕷 ++++++++++++ * Count the real ellipsis width in trimmed layout segments by @dylanpulver in https://github.com/urwid/urwid/pull/1261 Documentation 🕮 ++++++++++++++++ * Typing: fix ListBox typing protocol by @penguinolog in https://github.com/urwid/urwid/pull/1253 Other Changes +++++++++++++ * Tests: add missing tests for common use-cases by @penguinolog in https://github.com/urwid/urwid/pull/1257 * Tests: extend canvas and event loop tests coverage by @penguinolog in https://github.com/urwid/urwid/pull/1259 * Tests: fix windows side by @penguinolog in https://github.com/urwid/urwid/pull/1260 New Contributors ++++++++++++++++ * @dylanpulver made their first contribution in https://github.com/urwid/urwid/pull/1261 **Full Changelog**: https://github.com/urwid/urwid/compare/4.0.13...4.1.0 Urwid 4.0.13 ============ 2026-08-25 New features 🗹 +++++++++++++++ * Display attributes: add faint text support by @ChrisJr404 in https://github.com/urwid/urwid/pull/1249 Bug fixes 🕷 ++++++++++++ * Fix sizing related issues by @penguinolog in https://github.com/urwid/urwid/pull/1250 New Contributors ++++++++++++++++ * @ChrisJr404 made their first contribution in https://github.com/urwid/urwid/pull/1249 **Full Changelog**: https://github.com/urwid/urwid/compare/4.0.12...4.0.13 Urwid 4.0.12 ============ 2026-08-25 Bug fixes 🕷 ++++++++++++ * Text layout: do not crash when a wide character is clipped on both sides by @luantaraschi in https://github.com/urwid/urwid/pull/1241 * Fix web display init by @penguinolog in https://github.com/urwid/urwid/pull/1243 **Full Changelog**: https://github.com/urwid/urwid/compare/4.0.11...4.0.12 Urwid 4.0.11 ============ 2026-08-19 Bug fixes 🕷 ++++++++++++ * Raw display: do not crash on a last row of a single wide grapheme by @luantaraschi in https://github.com/urwid/urwid/pull/1229 * ScrollBar: decide the scrollbar hitbox before the event is passed on by @luantaraschi in https://github.com/urwid/urwid/pull/1234 * GridFlow: keep the focus a callback set during keypress or mouse event by @luantaraschi in https://github.com/urwid/urwid/pull/1230 Documentation 🕮 ++++++++++++++++ * Typing: annotate ListBox by @penguinolog in https://github.com/urwid/urwid/pull/1236 Refactoring 🛠 ++++++++++++++ * Refactoring: stop with monkey-patching `Padding` in `GridFlow` by @penguinolog in https://github.com/urwid/urwid/pull/1231 New Contributors ++++++++++++++++ * @luantaraschi made their first contribution in https://github.com/urwid/urwid/pull/1229 **Full Changelog**: https://github.com/urwid/urwid/compare/4.0.10...4.0.11 Urwid 4.0.10 ============ 2026-08-17 Bug fixes 🕷 ++++++++++++ * Web display: handle invalid input with "window resize" by @penguinolog in https://github.com/urwid/urwid/pull/1220 Documentation 🕮 ++++++++++++++++ * Typing: annotate bar_graph by @penguinolog in https://github.com/urwid/urwid/pull/1221 * typing: handle internal mypy warnings by @penguinolog in https://github.com/urwid/urwid/pull/1224 * typing: fix monitored list errors by @penguinolog in https://github.com/urwid/urwid/pull/1225 * typing: fix part of listbox errors by @penguinolog in https://github.com/urwid/urwid/pull/1226 Other Changes +++++++++++++ * typing: early guard `ListWalker` API in `ListBox` calls by @penguinolog in https://github.com/urwid/urwid/pull/1227 **Full Changelog**: https://github.com/urwid/urwid/compare/4.0.9...4.0.10 Urwid 4.0.9 =========== 2026-08-14 Documentation 🕮 ++++++++++++++++ * Typing: fix `WidgetContainerListContentsMixin` typing by @penguinolog in https://github.com/urwid/urwid/pull/1210 * Typing: simplify internal annotations by @penguinolog in https://github.com/urwid/urwid/pull/1211 * Typing: explicit annotate enum literals for widget constants by @penguinolog in https://github.com/urwid/urwid/pull/1218 Refactoring 🛠 ++++++++++++++ * Refactoring: get rid of monkeypatching in monitored_list by @penguinolog in https://github.com/urwid/urwid/pull/1213 **Full Changelog**: https://github.com/urwid/urwid/compare/4.0.8...4.0.9 Urwid 4.0.8 =========== 2026-07-26 Bug fixes 🕷 ++++++++++++ * Fix MRO for SimpleFocusListWalker: wrong order causes delayed invalidate by @penguinolog in https://github.com/urwid/urwid/pull/1204 **Full Changelog**: https://github.com/urwid/urwid/compare/4.0.7...4.0.8 Urwid 4.0.7 =========== 2026-07-24 Bug fixes 🕷 ++++++++++++ * Fix encoding handling by web display by @penguinolog in https://github.com/urwid/urwid/pull/1201 Documentation 🕮 ++++++++++++++++ * Typing: annotate `MainLoop` and `signals` by @penguinolog in https://github.com/urwid/urwid/pull/1199 * Typing: fix `ListBox` expected widget API by @penguinolog in https://github.com/urwid/urwid/pull/1200 * Typing: partial annotation for `treetools` by @penguinolog in https://github.com/urwid/urwid/pull/1197 **Full Changelog**: https://github.com/urwid/urwid/compare/4.0.6...4.0.7 Urwid 4.0.6 =========== 2026-07-23 Implemented protocol base classes `AbstractWidget` and internal `AbstractBoxWidget`, `AbstractFlowWidget` and `AbstractFixedWidget` for protocol based typing. All classes implementing `AbstractWidget` are considered valid widget (as all required APIs available). Container widgets as `Columns`, `Pile`, `GridFlow`, `Frame`, `Overlay` and `Padding` now declare expected APIs from widgets depends on sizing parameter in `__init__`. Bug fixes 🕷 ++++++++++++ * Fix web display broken by token_urlsafe session ids by @2018302345 in https://github.com/urwid/urwid/pull/1195 Documentation 🕮 ++++++++++++++++ * Typing: fix scrollable module annotations by @penguinolog in https://github.com/urwid/urwid/pull/1194 * Typing: introduce `AbstractWidget` abstract base class/protocol by @penguinolog in https://github.com/urwid/urwid/pull/1196 New Contributors ++++++++++++++++ * @2018302345 made their first contribution in https://github.com/urwid/urwid/pull/1195 **Full Changelog**: https://github.com/urwid/urwid/compare/4.0.5...4.0.6 Urwid 4.0.5 =========== 2026-07-20 New features 🗹 +++++++++++++++ * Copy signal handlers before emission by @Sanjays2402 in https://github.com/urwid/urwid/pull/1187 * Implement `_scrollbar_layout` and left click handling for `Scrollable` by @penguinolog in https://github.com/urwid/urwid/pull/1192 Bug fixes 🕷 ++++++++++++ * Fix rle_product for zero-len elements by @penguinolog in https://github.com/urwid/urwid/pull/1191 Documentation 🕮 ++++++++++++++++ * typing: fix `Filler` typing annotations by @penguinolog in https://github.com/urwid/urwid/pull/1183 * typing: extend widget_decoration module typing coverage by @penguinolog in https://github.com/urwid/urwid/pull/1184 * Typing: annotate and fix attr_map by @penguinolog in https://github.com/urwid/urwid/pull/1186 Refactoring 🛠 ++++++++++++++ * Migrate `Edit` widget to use `WidgetWrap` as base class and update methods by @penguinolog in https://github.com/urwid/urwid/pull/1180 New Contributors ++++++++++++++++ * @Sanjays2402 made their first contribution in https://github.com/urwid/urwid/pull/1187 **Full Changelog**: https://github.com/urwid/urwid/compare/4.0.4...4.0.5 Urwid 4.0.4 =========== 2026-07-13 New features 🗹 +++++++++++++++ * urwid.Colmns: re-calculate column_widths only if pack widgets changed by @penguinolog in https://github.com/urwid/urwid/pull/1178 Bug fixes 🕷 ++++++++++++ * Fix IndexError in decompose_tagmarkup when a nested sublist is empty by @gaoflow in https://github.com/urwid/urwid/pull/1172 Documentation 🕮 ++++++++++++++++ * Typing: partially annotate examples and fix annotations by @penguinolog in https://github.com/urwid/urwid/pull/1173 * urwid.WidgetContainerMixin: return use protocol base class by @penguinolog in https://github.com/urwid/urwid/pull/1181 New Contributors ++++++++++++++++ * @gaoflow made their first contribution in https://github.com/urwid/urwid/pull/1172 **Full Changelog**: https://github.com/urwid/urwid/compare/4.0.3...4.0.4 Urwid 4.0.3 =========== 2026-06-25 Deprecations âš¡ +++++++++++++++ * Deprecate `content_delta` as unused by @penguinolog in https://github.com/urwid/urwid/pull/1151 Bug fixes 🕷 ++++++++++++ * Fix `shard_body_row`: falsy content_iter raises ValueError by @penguinolog in https://github.com/urwid/urwid/pull/1162 Documentation 🕮 ++++++++++++++++ * Mass typing annotations fixes. **Full Changelog**: https://github.com/urwid/urwid/compare/4.0.2...4.0.3 Urwid 4.0.2 =========== 2026-06-02 New features 🗹 +++++++++++++++ * Initial typing support for urwid package by @penguinolog in https://github.com/urwid/urwid/pull/1144 Bug fixes 🕷 ++++++++++++ * Fix ListBoxError in _keypress_page_down when snapping to a widget above the top by @neutralinsomniac in https://github.com/urwid/urwid/pull/1133 * Fix lazy_import of display modules if __package__ is not defined by @penguinolog in https://github.com/urwid/urwid/pull/1136 Documentation 🕮 ++++++++++++++++ * Adjust typing for `urwid.display` and `urwid.event_loop` by @penguinolog in https://github.com/urwid/urwid/pull/1138 * Typing: extend typing information for `urwid.display` by @penguinolog in https://github.com/urwid/urwid/pull/1139 * Typing: adjust event loop types by @penguinolog in https://github.com/urwid/urwid/pull/1140 * Typing: adjust types for vterm, numedit and graphics by @penguinolog in https://github.com/urwid/urwid/pull/1141 * Typing: adjust types for `urwid.widget` by @penguinolog in https://github.com/urwid/urwid/pull/1142 Refactoring 🛠 ++++++++++++++ * Feat: refactor `urwid.widget.scrollable` by @penguinolog in https://github.com/urwid/urwid/pull/1143 Other Changes +++++++++++++ * Web display: tighten security by using more complex identifiers by @penguinolog in https://github.com/urwid/urwid/pull/1137 New Contributors ++++++++++++++++ * @neutralinsomniac made their first contribution in https://github.com/urwid/urwid/pull/1133 **Full Changelog**: https://github.com/urwid/urwid/compare/4.0.1...4.0.2 Urwid 4.0.1 =========== 2026-05-26 Bug fixes 🕷 ++++++++++++ * fix three return type annotations by @evgunter in https://github.com/urwid/urwid/pull/1121 * Web display: use secure random session identifiers by @penguinolog in https://github.com/urwid/urwid/pull/1128 Other Changes +++++++++++++ * Add SECURITY.md by @jrwe in https://github.com/urwid/urwid/pull/1123 New Contributors ++++++++++++++++ * @evgunter made their first contribution in https://github.com/urwid/urwid/pull/1121 * @jrwe made their first contribution in https://github.com/urwid/urwid/pull/1123 **Full Changelog**: https://github.com/urwid/urwid/compare/4.0.0...4.0.1 Urwid 4.0.0 =========== 2026-03-30 Breaking Changes âš  ++++++++++++++++++ * Deprecation: Remove deprecated protected methods for version 4 by @penguinolog in https://github.com/urwid/urwid/pull/1078 * Deprecation: Remove deprecated public methods announced to be removed in v4 by @penguinolog in https://github.com/urwid/urwid/pull/1085 * Deprecation: Remove deprecated `Filler.get_body` and `Filler.set_body` by @penguinolog in https://github.com/urwid/urwid/pull/1117 Deprecations âš¡ +++++++++++++++ * Deprecation: announce deadline for deprecated APIs by @penguinolog in https://github.com/urwid/urwid/pull/1118 Bug fixes 🕷 ++++++++++++ * stdin handling for closed state by @wilkpio in https://github.com/urwid/urwid/pull/1112 New Contributors ++++++++++++++++ * @wilkpio made their first contribution in https://github.com/urwid/urwid/pull/1112 **Full Changelog**: https://github.com/urwid/urwid/compare/3.0.5...4.0.0 Urwid 3.0.5 =========== 2026-02-02 Deprecations âš¡ +++++++++++++++ * Un-deprecate `user_arg` for signals by @penguinolog in https://github.com/urwid/urwid/pull/1092 Bug fixes 🕷 ++++++++++++ * Fix terminal scroll TypeError by @penguinolog in https://github.com/urwid/urwid/pull/1098 * Bugfix some kinds of emojis with grapheme boundry support by @jquast in https://github.com/urwid/urwid/pull/1100 * Fix: Text.pack line splitting not match Text.render by @penguinolog in https://github.com/urwid/urwid/pull/1099 New Contributors ++++++++++++++++ * @jquast made their first contribution in https://github.com/urwid/urwid/pull/1100 **Full Changelog**: https://github.com/urwid/urwid/compare/3.0.4...3.0.5 Urwid 3.0.4 =========== 2025-12-01 Deprecations âš¡ +++++++++++++++ * Fix CheckBox DeprecationWarning: Use user_args instead of user_arg by @sawdmn in https://github.com/urwid/urwid/pull/1079 Other Changes +++++++++++++ * Refactoring: use official python way for lazy imports by @penguinolog in https://github.com/urwid/urwid/pull/1077 New Contributors ++++++++++++++++ * @sawdmn made their first contribution in https://github.com/urwid/urwid/pull/1079 **Full Changelog**: https://github.com/urwid/urwid/compare/3.0.3...3.0.4 Urwid 3.0.3 =========== 2025-09-15 Bug fixes 🕷 ++++++++++++ * Python 3.14: fix buffer overflow on terminal size request by @meeuw in https://github.com/urwid/urwid/pull/1027 * Add guard against empty list on focus complete. by @gryf in https://github.com/urwid/urwid/pull/1047 Documentation 🕮 ++++++++++++++++ * Add extra code type annotations by @penguinolog in https://github.com/urwid/urwid/pull/1037 * Mark Windows support limitation: win10+ only by @penguinolog in https://github.com/urwid/urwid/pull/1038 Other Changes +++++++++++++ * Maintenance: fix devcontainer to support actual requirements without pinning by @penguinolog in https://github.com/urwid/urwid/pull/1035 New Contributors ++++++++++++++++ * @meeuw made their first contribution in https://github.com/urwid/urwid/pull/1027 * @gryf made their first contribution in https://github.com/urwid/urwid/pull/1047 Urwid 3.0.2 =========== 2025-05-07 Bug fixes 🕷 ++++++++++++ * Bugfix: Corner case: Scrollbar render for only 1 row height by @penguinolog in https://github.com/urwid/urwid/pull/1021 Urwid 3.0.1 =========== 2025-05-07 Bug fixes 🕷 ++++++++++++ * Packaging: drop setup.py and adjust requirements by @penguinolog in https://github.com/urwid/urwid/pull/1018 Not updated `setup.py` lead to wrong metadata and mark of release 3.0.0 as Python 3.7 compatible. Since urwid is distribluted in the pre-packaged format (wheel), missing `setup.py` will not affect old toolchain users except special cases (manually enforced sdist usage). * Python 3.14 compatibility: adjust `AsyncioEventLoop` by @penguinolog in https://github.com/urwid/urwid/pull/1019 Urwid 3.0.0 =========== 2025-05-06 Breaking Changes âš  ++++++++++++++++++ * Drop deprecated `__super` property by @penguinolog in https://github.com/urwid/urwid/pull/956 * Drop deprecated `FlowWidget`, `BoxWidget` and `FixedWidget` widgets by @penguinolog in https://github.com/urwid/urwid/pull/955 * Remove deprecated protected getter methods from the `Canvas` and `AttrSpec` by @penguinolog in https://github.com/urwid/urwid/pull/958 * Remove support for the `bytes` based fonts by @penguinolog in https://github.com/urwid/urwid/pull/961 * Remove deprecated `TermScroller` by @penguinolog in https://github.com/urwid/urwid/pull/960 * Remove deprecated protected getter methods from the decoration widgets by @penguinolog in https://github.com/urwid/urwid/pull/959 * Remove deprecated protected getter methods from the container widgets by @penguinolog in https://github.com/urwid/urwid/pull/957 * Remove deprecated protected setters in the decoration widgets by @penguinolog in https://github.com/urwid/urwid/pull/968 * Remove deprecated protected methods from the container widgets by @penguinolog in https://github.com/urwid/urwid/pull/970 * Remove deprecated protected method `_set_done` from the `ProgressBar` by @penguinolog in https://github.com/urwid/urwid/pull/971 New features 🗹 +++++++++++++++ * API Extension: make `CommandMap` `MutableMapping` by @penguinolog in https://github.com/urwid/urwid/pull/969 * Make sizing computation much faster for nested containers by @ogayot in https://github.com/urwid/urwid/pull/991 * `MetaSignals` subclass `ABCMeta` by @penguinolog in https://github.com/urwid/urwid/pull/962 Deprecations âš¡ +++++++++++++++ * `MetaSuper` should be the last base for classes by @penguinolog in https://github.com/urwid/urwid/pull/972 * Announce deprecated API removal versions by @penguinolog in https://github.com/urwid/urwid/pull/999 Bug fixes 🕷 ++++++++++++ * Fix handling of WEIGHT selectable items in the `Pile` by @penguinolog in https://github.com/urwid/urwid/pull/1006 Refactoring 🛠 ++++++++++++++ * Refactoring: micro optimizations of iterable items reconstruction by @penguinolog in https://github.com/urwid/urwid/pull/1009 New Contributors ++++++++++++++++ * @ogayot made their first contribution in https://github.com/urwid/urwid/pull/991 Urwid 2.6.16 ============ 2024-10-15 New features 🗹 +++++++++++++++ * Feature: Add focus reporting support by @aclindsa in https://github.com/urwid/urwid/pull/929 * Add 16-color support by @luca-script in https://github.com/urwid/urwid/pull/939 Bug fixes 🕷 ++++++++++++ * Disable mouse tracking and discard input when exiting the main loop by @george-prekas-deshaw in https://github.com/urwid/urwid/pull/932 * Windows: use Unicode input directly by @penguinolog in https://github.com/urwid/urwid/pull/943 Refactoring 🛠 ++++++++++++++ * Type annotations: fix typing issues by @penguinolog in https://github.com/urwid/urwid/pull/941 New Contributors ++++++++++++++++ * @george-prekas-deshaw made their first contribution in https://github.com/urwid/urwid/pull/932 * @aclindsa made their first contribution in https://github.com/urwid/urwid/pull/929 * @luca-script made their first contribution in https://github.com/urwid/urwid/pull/939 Urwid 2.6.15 ============ 2024-07-03 Bug fixes 🕷 ++++++++++++ * Fix `ListBox.rows_max` calculation for empty container by @penguinolog in https://github.com/urwid/urwid/pull/910 Urwid 2.6.14 ============ 2024-06-12 Bug fixes 🕷 ++++++++++++ * GridFlow fixes for empty container by @penguinolog in https://github.com/urwid/urwid/pull/901 * Fix ScrollBar mouse_event handling with ListBox by @skimmmer in https://github.com/urwid/urwid/pull/905 Other Changes +++++++++++++ * Test requirements: exceptiongroups -> exceptiongroup by @penguinolog in https://github.com/urwid/urwid/pull/897 New Contributors ++++++++++++++++ * @skimmmer made their first contribution in https://github.com/urwid/urwid/pull/905 Urwid 2.6.13 ============ 2024-06-07 Bug fixes 🕷 ++++++++++++ * Adopt `ExceptionGroup` handling without an external library in python 3.11+ by @penguinolog in https://github.com/urwid/urwid/pull/894 * Fix browse example by @penguinolog in https://github.com/urwid/urwid/pull/895 Other Changes +++++++++++++ * Maintenance: update ruff and fix warnings by @penguinolog in https://github.com/urwid/urwid/pull/891 Urwid 2.6.12 ============ 2024-05-14 Bug fixes 🕷 ++++++++++++ * Prevent a possible infinite loop in WidgetDecoration.base_widget by @rsekman in https://github.com/urwid/urwid/pull/880 * ScrollBar will check wrapped widgets for SupportsScroll (Fixes #878) by @rsekman in https://github.com/urwid/urwid/pull/879 New Contributors ++++++++++++++++ * @rsekman made their first contribution in https://github.com/urwid/urwid/pull/880 Urwid 2.6.11 ============ 2024-04-22 Bug fixes 🕷 ++++++++++++ * Fix `Widget.rows` annotation by @penguinolog in https://github.com/urwid/urwid/pull/874 Documentation 🕮 ++++++++++++++++ * Do not use deprecated positioning in the code and examples by @penguinolog in https://github.com/urwid/urwid/pull/869 * Docs: partial update of screenshots by @penguinolog in https://github.com/urwid/urwid/pull/873 Urwid 2.6.10 ============ 2024-03-25 New features 🗹 +++++++++++++++ * `ScrollBar`: fully support `__length_hint__` if not `Sized` by @penguinolog in https://github.com/urwid/urwid/pull/863 Other Changes +++++++++++++ * Support PEP424 API as marker for limited size of ListBox body by @penguinolog in https://github.com/urwid/urwid/pull/861 Urwid 2.6.9 =========== 2024-03-13 New features 🗹 +++++++++++++++ * Support relative scroll for `ListBox` by @penguinolog in https://github.com/urwid/urwid/pull/858 Absolute scrolling calculation is resource-hungry and can cause serious issues on the long lists. This change also rework calculation allowing to use `ScrollBar` with `TreeList` (users should prevent infinite load cycle on lazy-load self). Bug fixes 🕷 ++++++++++++ * Support `` key reading for sgrmouse by @penguinolog in https://github.com/urwid/urwid/pull/859 Historically key reading was not implemented due to `` for buttons 1-3 is handled by the most GUI terminal emulators itself. * Fix regression in `TreeWidget`: original widget can be overridden by @penguinolog in https://github.com/urwid/urwid/pull/860 Urwid 2.6.8 =========== 2024-03-04 Bug fixes 🕷 ++++++++++++ * Fix regression: Overlay not accepted relative positioning by @penguinolog in https://github.com/urwid/urwid/pull/854 Urwid 2.6.7 =========== 2024-02-28 Bug fixes 🕷 ++++++++++++ * Fix `MainLoop.watch_pipe` regression for the callback outcome not `False` by @penguinolog in https://github.com/urwid/urwid/pull/848 Refactoring 🛠 ++++++++++++++ * Refactor: fix static check warning for not using `min` in `GridFlow.generate_display_widget` by @penguinolog in https://github.com/urwid/urwid/pull/849 Urwid 2.6.6 =========== 2024-02-27 Bug fixes 🕷 ++++++++++++ * Fix Columns sizing and pack behavior by @penguinolog in https://github.com/urwid/urwid/pull/846 Other Changes +++++++++++++ * Extend `__repr__` and `rich` repr for the sized containers by @penguinolog in https://github.com/urwid/urwid/pull/844 Urwid 2.6.5 =========== 2024-02-26 Bug fixes 🕷 ++++++++++++ * Allow `wcwidth` to select unicode version by @penguinolog in https://github.com/urwid/urwid/pull/840 * `TreeWidget`: do not use deprecated API in `update_expanded_icon` by @penguinolog in https://github.com/urwid/urwid/pull/832 Refactoring 🛠 ++++++++++++++ * Refactoring: move `monitored_list` module to the `widgets` package by @penguinolog in https://github.com/urwid/urwid/pull/833 * Refactoring: move `listbox` & `treetools` modules to the `widgets` by @penguinolog in https://github.com/urwid/urwid/pull/834 * Use dataclass for symbols constants by @penguinolog in https://github.com/urwid/urwid/pull/842 * Special case: in case of `Columns`/`Pile` empty - use fallback sizing by @penguinolog in https://github.com/urwid/urwid/pull/843 Other Changes +++++++++++++ * Tests: Extend Tree tests: basic keys + nested behavior by @penguinolog in https://github.com/urwid/urwid/pull/831 Urwid 2.6.4 =========== 2024-02-21 Bug fixes 🕷 ++++++++++++ * Fix regression from 2.6.1: `ListBox` used for tree implementation. by @penguinolog in https://github.com/urwid/urwid/pull/829 Urwid 2.6.3 =========== 2024-02-21 Bug fixes 🕷 ++++++++++++ * Fix regression from 2.6.2: weight can be `float` by @penguinolog in https://github.com/urwid/urwid/pull/827 Urwid 2.6.2 =========== 2024-02-20 New features 🗹 +++++++++++++++ * Feature: support `Widget` instance as `Frame` focus part in constructor by @penguinolog in https://github.com/urwid/urwid/pull/820 * Feature: `EventLoop.run_in_executor` should accept `**kwargs` by @penguinolog in https://github.com/urwid/urwid/pull/822 * Feature: extend validation for `Columns` and `Pile` by @penguinolog in https://github.com/urwid/urwid/pull/825 Bug fixes 🕷 ++++++++++++ * Fix: nonstandard display typing issues by @penguinolog in https://github.com/urwid/urwid/pull/818 * Fix: Text pack for `layout` without `pack` by @penguinolog in https://github.com/urwid/urwid/pull/819 * Fix: `ListBox` render crash if empty elements in tail by @penguinolog in https://github.com/urwid/urwid/pull/824 Urwid 2.6.1 =========== 2024-02-16 Bug fixes 🕷 ++++++++++++ * Fix a scenario with ellipsis wrap not fit in screen columns by @penguinolog in https://github.com/urwid/urwid/pull/813 Urwid 2.6.0 =========== 2024-02-16 Compiled C extension is not used anymore. +++++++++++++++++++++++++++++++++++++++++ It became a blocker for the future correct Unicode support and caused pain for some of end users building package separately. Bug fixes 🕷 ++++++++++++ * Fix regression in the `LineBox._w`: should be a property by @penguinolog in https://github.com/urwid/urwid/pull/804 * Fix ellipsis encoding in the text layout by @penguinolog in https://github.com/urwid/urwid/pull/809 * Fix ListBox `MAX_LEFT`/`MAX_RIGHT` report for `keypress` as unhandled by @penguinolog in https://github.com/urwid/urwid/pull/810 Other Changes +++++++++++++ * remove .DS_Store by @penguinolog in https://github.com/urwid/urwid/pull/808 Urwid 2.5.3 =========== 2024-02-12 Bug fixes 🕷 ++++++++++++ * Fix render regression: khal Padding width > size by @penguinolog in https://github.com/urwid/urwid/pull/798 Other Changes +++++++++++++ * Update unicode table to the version 15.1.0 by @penguinolog in https://github.com/urwid/urwid/pull/744 Urwid 2.5.2 =========== 2024-02-09 Bug fixes 🕷 ++++++++++++ * Fix Windows last line: use ICH * INSERT COLS by @penguinolog in https://github.com/urwid/urwid/pull/792 Refactoring 🛠 ++++++++++++++ * Typing: annotate text_layout and extend `Text`/`Edit` by @penguinolog in https://github.com/urwid/urwid/pull/793 * Fix incorrect `TextCanvas` typing by @penguinolog in https://github.com/urwid/urwid/pull/794 Other Changes +++++++++++++ * RAW UTF-8 terminal: SI/SO/IBMPC_ON/IBMPC_OFF skip by @penguinolog in https://github.com/urwid/urwid/pull/787 * Unicode: use "target encoding" while transcoding for output by @penguinolog in https://github.com/urwid/urwid/pull/782 Urwid 2.5.1 =========== 2024-02-01 New features 🗹 +++++++++++++++ * Columns special case: FIXED pack with not enough info by @penguinolog in https://github.com/urwid/urwid/pull/779 Bug fixes 🕷 ++++++++++++ * Windows and WSL: SI/SO/IBMPC_ON/IBMPC_OFF skip by @penguinolog in https://github.com/urwid/urwid/pull/785 Documentation 🕮 ++++++++++++++++ * Documentation: add Gitter badge to the README.rst by @penguinolog in https://github.com/urwid/urwid/pull/776 Refactoring 🛠 ++++++++++++++ * Extend Command enum and update `Columns` & `Pile` by @penguinolog in https://github.com/urwid/urwid/pull/778 * Fix double `Widget` inheritance in the `LineBox` by @penguinolog in https://github.com/urwid/urwid/pull/780 * Optimization: RAW display block read by @penguinolog in https://github.com/urwid/urwid/pull/783 * Typing: correct annotations for `Canvas.content` by @penguinolog in https://github.com/urwid/urwid/pull/784 Other Changes +++++++++++++ * Typing: Make `WidgetWrap` and `WidgetDecoration` `Generic` by @penguinolog in https://github.com/urwid/urwid/pull/777 Urwid 2.5.0 =========== 2024-01-29 New features 🗹 +++++++++++++++ * Long awaited `Scrollable` and `ScrollBar` made by @rndusr with fixes from @markqvist was added to the urwid. * Add support `ScrollBar` to the `ListBox` widget. While scrolling is done by the `ListBox` itself (as before), the `ScrollBar` can display progress. * Added common decoration symbols to the widget constants (`BOX_SYMBOLS`, `BAR_SYMBOLS`, `SHADE_SYMBOLS`) and exposed in several widgets via `Symbols` enum on the class level: `Divider`, `LineBox` and `ScrollBar`. Documentation 🕮 ++++++++++++++++ * Update examples: reduce amount of deprecated parameters by @penguinolog in https://github.com/urwid/urwid/pull/769 Urwid 2.4.6 =========== 2024-01-22 Bug fixes 🕷 ++++++++++++ * Special case: Columns PACK item not fit as FIXED and support FLOW by @penguinolog in https://github.com/urwid/urwid/pull/763 Urwid 2.4.5 =========== 2024-01-22 Bug fixes 🕷 ++++++++++++ * Special case: allow not subclassed incomplete widgets in `Columns`/`Pile` by @penguinolog in https://github.com/urwid/urwid/pull/757 * Fix: columns rendered with a non-first Selectable widget should not skip other items by @penguinolog in https://github.com/urwid/urwid/pull/761 Documentation 🕮 ++++++++++++++++ * Documentation: get rid of deprecated code, typing by @penguinolog in https://github.com/urwid/urwid/pull/755 Refactoring 🛠 ++++++++++++++ * Deduplicate code in `frame` module by @penguinolog in https://github.com/urwid/urwid/pull/759 * Extend typing annotations by @penguinolog in https://github.com/urwid/urwid/pull/760 Urwid 2.4.4 =========== 2024-01-18 Bug fixes 🕷 ++++++++++++ * Fix regression: Columns render incorrect BOX/FLOW widgets height by @penguinolog in https://github.com/urwid/urwid/pull/754 Urwid 2.4.3 =========== 2024-01-17 New features 🗹 +++++++++++++++ * Feature: `Overlay` typing and proper repr by @penguinolog in https://github.com/urwid/urwid/pull/741 * Feature: support proper `repr` and `str` for `Canvas` by @penguinolog in https://github.com/urwid/urwid/pull/740 * Feature: Support FIXED and FLOW operations for `Overlay` depends on options by @penguinolog in https://github.com/urwid/urwid/pull/743 * Feature: `Columns` accept focus widget as "focus_column" by @penguinolog in https://github.com/urwid/urwid/pull/747 * Feature: `Filler` can act as FLOW widget by @penguinolog in https://github.com/urwid/urwid/pull/749 * Feature: allow explicit focus in `GridFlow` constructor by @penguinolog in https://github.com/urwid/urwid/pull/752 Bug fixes 🕷 ++++++++++++ * `Columns` support correct BOX render only if ALL BOX by @penguinolog in https://github.com/urwid/urwid/pull/746 * `LineBox`: make side only if side elements present, fix pack by @penguinolog in https://github.com/urwid/urwid/pull/748 * Update source package manifest to include `_web.js` and `_web.css` by @Hook25 in https://github.com/urwid/urwid/pull/750 * Fix `ListBox.contents`: it should return final entity by @penguinolog in https://github.com/urwid/urwid/pull/751 Documentation 🕮 ++++++++++++++++ * original artwork for widgets diagram by @wardi in https://github.com/urwid/urwid/pull/739 New Contributors ++++++++++++++++ * @Hook25 made their first contribution in https://github.com/urwid/urwid/pull/750 Urwid 2.4.2 =========== 2024-01-11 New features 🗹 +++++++++++++++ * Feature: support FIXED-only widgets and FIXED rendering for Columns by @penguinolog in https://github.com/urwid/urwid/pull/731 * Feature: Support FIXED-only widgets and FIXED rendering for PILE by @penguinolog in https://github.com/urwid/urwid/pull/733 * FIX Padding: support FIXED render mode by @penguinolog in https://github.com/urwid/urwid/pull/734, https://github.com/urwid/urwid/pull/735, https://github.com/urwid/urwid/pull/737 * Feature: support FIXED render type for GridFLow by @penguinolog in https://github.com/urwid/urwid/pull/738 Now it possible to quick check incomplete widgets prototypes without size counting like: >>> import urwid >>> grid = urwid.GridFlow( ...: (urwid.Button(tag, align=urwid.CENTER) for tag in ("OK", "Cancel", "Help")), ...: cell_width=10, ...: h_sep=1, ...: v_sep=1, ...: align=urwid.CENTER, ...: ) >>> body = urwid.Pile( ...: ( ...: (urwid.Text("Window content text here and it should not touch line", align=urwid.CENTER)), ...: (urwid.PACK, grid), ...: ) ...: ) >>> widget = urwid.LineBox( ...: urwid.Pile( ...: ( ...: urwid.Text("Some window", align=urwid.CENTER), ...: urwid.Divider("─"), ...: urwid.Padding(body, width=urwid.PACK, left=1, right=1), ...: ) ...: ) ...: ) >>> print(b"\n".join(widget.render(()).text).decode("utf-8")) ┌───────────────────────────────────────────────────────â”� │ Some window │ │───────────────────────────────────────────────────────│ │ Window content text here and it should not touch line │ │ < OK > < Cancel > < Help > │ └───────────────────────────────────────────────────────┘ >>> widget.pack(()) (57, 6) Bug fixes 🕷 ++++++++++++ * BUG: Columns with GIVEN width == 0 should not count in rows by @penguinolog in https://github.com/urwid/urwid/pull/736 Refactoring 🛠 ++++++++++++++ * Remove unused deprecated "test_suite" directive from setup.py by @penguinolog in https://github.com/urwid/urwid/pull/729 Urwid 2.4.1 =========== 2024-01-03 Bug fixes 🕷 ++++++++++++ * Fix Regressions in 2.4.0 by @penguinolog in https://github.com/urwid/urwid/pull/727 Urwid 2.4.0 =========== 2024-01-03 New features 🗹 +++++++++++++++ * Basic **Windows OS** support in RAW and Curses display. * * Fully functional RAW display support. Thanks to @mhils (initial RAW display) * * Limited Curses support due to windows-curses limitation (mouse support limited). Thanks to @asmith-kepler (windows-curses monkeypatch) * * UTF-8 only * * Force `SelectorEventLoop` for asyncio under Windows if event loop is not set by @penguinolog in https://github.com/urwid/urwid/pull/715 * Feature: switch from `select.select` to `selectors` by @penguinolog in https://github.com/urwid/urwid/pull/691 * Feature: support FIXED render mode for Text by @penguinolog in https://github.com/urwid/urwid/pull/610 * Feature: extend functional keys handling with key modifiers by @penguinolog in https://github.com/urwid/urwid/pull/705 * Add `run_in_executor` helper to the event loop by @penguinolog in https://github.com/urwid/urwid/pull/712 * Feature: Add internal logging for behavioral debug by @penguinolog in https://github.com/urwid/urwid/pull/708 * * Feature: Use module path + class name for loggers and init once by @penguinolog in https://github.com/urwid/urwid/pull/720 * Feature: Implement `keypress` and `mouse_event` default handlers by @penguinolog in https://github.com/urwid/urwid/pull/721 * * Not subclassing `Widget` base class during custom widget implementation will produce `DeprecationWarning` Deprecations âš¡ +++++++++++++++ * Refactor: join display modules in package by @penguinolog in https://github.com/urwid/urwid/pull/655 * * Backward-compatible. Main display modules import will not produce `DeprecationWarning`. Documentation 🕮 ++++++++++++++++ * Fix #186 - `disconnect_by_key` should be exposed and use correct name by @penguinolog in https://github.com/urwid/urwid/pull/688 * Extend input_test example for better debug capabilities by @penguinolog in https://github.com/urwid/urwid/pull/713 * * Support alternative event loops and debug logging. Refactoring 🛠 ++++++++++++++ * Refactoring: remove dead code from Raw display by @penguinolog in https://github.com/urwid/urwid/pull/707 Urwid 2.3.4 =========== 2023-12-05 New features 🗹 +++++++++++++++ * Replace deprecated use of MultiError with exceptiongroup by @DRMacIver in https://github.com/urwid/urwid/pull/679 * Declare extension use Py_LIMITED_API explicit also in setup.py by @penguinolog in https://github.com/urwid/urwid/pull/686 Bug fixes 🕷 ++++++++++++ * use Hashable for signal identifier types by @ju1ius in https://github.com/urwid/urwid/pull/669 * Fix #674 : old versions of setuptools_scm support by @penguinolog in https://github.com/urwid/urwid/pull/675 * Fix WSL support: filter-out SI/SO in case of WSL by @penguinolog in https://github.com/urwid/urwid/pull/656 Documentation 🕮 ++++++++++++++++ * fixed typo by @vindolin in https://github.com/urwid/urwid/pull/676 Refactoring 🛠 ++++++++++++++ * Maintenance: apply `refurb` fixes and tighten ruff by @penguinolog in https://github.com/urwid/urwid/pull/671 * Fix exception re-raise in trio event loop by @penguinolog in https://github.com/urwid/urwid/pull/683 Other Changes +++++++++++++ * Maintenance: Tests: Use explicit encoding for tests by @penguinolog in https://github.com/urwid/urwid/pull/685 New Contributors ++++++++++++++++ * @ju1ius made their first contribution in https://github.com/urwid/urwid/pull/669 * @vindolin made their first contribution in https://github.com/urwid/urwid/pull/676 * @DRMacIver made their first contribution in https://github.com/urwid/urwid/pull/679 Urwid 2.2.3 =========== 2023-10-19 New features 🗹 +++++++++++++++ * Expose `widget` and `event_loop` packages by @penguinolog in https://github.com/urwid/urwid/pull/646 * Introduce optional dependencies for package by @penguinolog in https://github.com/urwid/urwid/pull/650 Deprecations âš¡ +++++++++++++++ * Refactoring numedit: PEP8 arguments, allow negative, type casts by @penguinolog in https://github.com/urwid/urwid/pull/636 USE PEP8 compliant arguments and deprecate old one Allow cast IntEdit to int and FloatEdit to float Allow negative values without changing default behavior Bug fixes 🕷 ++++++++++++ * Fix import from deprecated internal module by @penguinolog in https://github.com/urwid/urwid/pull/645 * Fix deprecated `_set_focus` method usage by @penguinolog in https://github.com/urwid/urwid/pull/662 Documentation 🕮 ++++++++++++++++ * Documentation: Sphinx can build gh-pages ready by @penguinolog in https://github.com/urwid/urwid/pull/643 * Documentation: generate changelog from GH releases by @penguinolog in https://github.com/urwid/urwid/pull/648 * Documentation: Use static default text in BigText demo by @penguinolog in https://github.com/urwid/urwid/pull/651 * Documentation Remove unneeded closing tags in TOC by @penguinolog in https://github.com/urwid/urwid/pull/652 * Fix tutorial: rename `attr` to `urwid_attr` by @penguinolog in https://github.com/urwid/urwid/pull/653 * Documentation: TrioEventLoop is missed by @penguinolog in https://github.com/urwid/urwid/pull/642 Refactoring 🛠 ++++++++++++++ * Refactoring: Force automated import sorting for all code by @penguinolog in https://github.com/urwid/urwid/pull/637 * Refactoring: Force automated black formatting by @penguinolog in https://github.com/urwid/urwid/pull/638 * Refactoring: Force `ruff` static checker for project by @penguinolog in https://github.com/urwid/urwid/pull/639 * Refactor: fixup usage of contextlib.suppress() by @ulidtko in https://github.com/urwid/urwid/pull/640 Urwid 2.2.2 =========== 2023-09-25 New features 🗹 +++++++++++++++ * Feature: Support pack() for CheckBox/RadioButton/Button by @penguinolog in https://github.com/urwid/urwid/pull/621 Deprecations âš¡ +++++++++++++++ * Mark `AttrWrap` as `PendingDeprecation` by @penguinolog in https://github.com/urwid/urwid/pull/619 Bug fixes 🕷 ++++++++++++ * Fix font in case Font.data is `str` by @penguinolog in https://github.com/urwid/urwid/pull/618 Documentation 🕮 ++++++++++++++++ * Enforce examples code-style by @penguinolog in https://github.com/urwid/urwid/pull/620 * Documentation: do not use `FlowWidget` as base class in examples by @penguinolog in https://github.com/urwid/urwid/pull/623 * README: suggest python3-urwid for debian/ubuntu by @chronitis in https://github.com/urwid/urwid/pull/444 Refactoring 🛠 ++++++++++++++ * Packaging: stop tests distribution as part of package by @penguinolog in https://github.com/urwid/urwid/pull/622 New Contributors ++++++++++++++++ * @chronitis made their first contribution in https://github.com/urwid/urwid/pull/444 Urwid 2.2.1 =========== 2023-09-22 Bug fixes 🕷 ++++++++++++ * Fix: deep TextEnum was improperly resolved by @penguinolog in https://github.com/urwid/urwid/pull/609 Documentation 🕮 ++++++++++++++++ * Documentation: mention correct python versions by @penguinolog in https://github.com/urwid/urwid/pull/608 * Documentation: add stripped changelog for 2.2.0 by @penguinolog in https://github.com/urwid/urwid/pull/612 Refactoring 🛠 ++++++++++++++ * Refactoring: use `super()` calls if possible by @penguinolog in https://github.com/urwid/urwid/pull/611 * Typing: Extend wimp typing annotations by @penguinolog in https://github.com/urwid/urwid/pull/604 Urwid 2.2.0 =========== 2023-09-21 Compatibility +++++++++++++ * Fix #583: python 3.12 compatibility by @penguinolog in https://github.com/urwid/urwid/pull/598 * Python 37+ migration, Python < 3.7 support dropped by @penguinolog in https://github.com/urwid/urwid/pull/522 * make tests compatible with Python 3.11 by @dotlambda in https://github.com/urwid/urwid/pull/517 * Deprecate legacy property creation by @penguinolog in https://github.com/urwid/urwid/pull/533 * Deprecate `__super` hack by @penguinolog in https://github.com/urwid/urwid/pull/538 * [BREAKING CHANGE] Fixes: #90 Remove idle emulation from asyncio event loop by @penguinolog in https://github.com/urwid/urwid/pull/541 New features 🗹 +++++++++++++++ * ZMQ event loop by @waveform80 in https://github.com/urwid/urwid/pull/362 * Add two fonts based on Unicode 13 2x3 TRS-80/Teletext mosaic characters by @rbanffy in https://github.com/urwid/urwid/pull/434 * Adds 256 color and truecolor support to vterm. Fixes #457 by @danschwarz in https://github.com/urwid/urwid/pull/559 * Vterm now emits 'resize' signal upon terminal resize by @danschwarz in https://github.com/urwid/urwid/pull/584 * vterm.py: Adds support for bracketed paste mode. Fixes #452 by @danschwarz in https://github.com/urwid/urwid/pull/594 * Pass SelectableIcon `align` and `wrap` arguments to parent by @penguinolog in https://github.com/urwid/urwid/pull/599 Bug fixes 🕷 ++++++++++++ * fix: restore normal screen on ctrl-z by @proskur1n in https://github.com/urwid/urwid/pull/477 * Reconnect the 'modified' signal after setting a new ListBox.body by @exquo in https://github.com/urwid/urwid/pull/474 * Allow signal handling interoperability with raw display by @AnonymouX47 in https://github.com/urwid/urwid/pull/557 * Fix alternate/normal screen buffer switch for raw display by @AnonymouX47 in https://github.com/urwid/urwid/pull/556 * Fix text layout for `align="center", wrap="clip"` when `maxcol` == `line_width - 1` by @AnonymouX47 in https://github.com/urwid/urwid/pull/543 * Fix assertion failure when string contains SO but not SI by @mfncooper in https://github.com/urwid/urwid/pull/489 * Fix empty markup handling by @penguinolog in https://github.com/urwid/urwid/pull/536 * Resolve #499 and add tests by @djyotta in https://github.com/urwid/urwid/pull/500 * vterm: Fixed OSC 0,2 to set widget title properly (decode bytestring) by @danschwarz in https://github.com/urwid/urwid/pull/561 * vterm: Fixed a crash bug with DECALN command by @danschwarz in https://github.com/urwid/urwid/pull/560 * Fix #443 : export ELLIPSIS constant by @penguinolog in https://github.com/urwid/urwid/pull/527 * Fix: #445 - add `__len__` to listbox with validation if body `Sized` by @penguinolog in https://github.com/urwid/urwid/pull/534 * Fix old_str_util.decode_one : support bytes and str as arguments by @penguinolog in https://github.com/urwid/urwid/pull/531 * Use `locale.getpreferredencoding(False)` if possible (most systems) by @penguinolog in https://github.com/urwid/urwid/pull/528 * Fix `TextCanvas` `CanvasError("Attribute extends beyond text...")` by @penguinolog in https://github.com/urwid/urwid/pull/555 * Fix merging attributes while decomposing tag markup by @mandre in https://github.com/urwid/urwid/pull/507 * fix: use trio.lowlevel instead of trio.hazmat with Trio >= 0.15 by @ntamas in https://github.com/urwid/urwid/pull/439 * Fix TypeError in signals module on weak object finalize by GC by @rvtpro in https://github.com/urwid/urwid/pull/503 * Include _resize_pipe_rd in fd_list for _wait_for_input_ready for raw_display by @inducer in https://github.com/urwid/urwid/pull/453 * container: fix duplicate text by @vapier in https://github.com/urwid/urwid/pull/490 * Provide 80x24 fallback for ansi and vt100 by @roadriverrail in https://github.com/urwid/urwid/pull/465 Refactoring 🛠 ++++++++++++++ * Use == instead of "is" for equality testing by @naglis in https://github.com/urwid/urwid/pull/431 * Split event loop in several modules by @penguinolog in https://github.com/urwid/urwid/pull/537 * Drop some compat for python < 2.6 by @dlax in https://github.com/urwid/urwid/pull/409 * Annotate types in simple cases and use isinstance (& protocol) based type checking by @penguinolog in https://github.com/urwid/urwid/pull/529 * Add type annotations and optimize `urwid.font` by @penguinolog in https://github.com/urwid/urwid/pull/540 * Related #583: Cleanup C helper from python2-only code by @penguinolog in https://github.com/urwid/urwid/pull/597 * Optimize `vterm`: adopt data types and add annotations. Fix tests by @penguinolog in https://github.com/urwid/urwid/pull/547 * Split widget and introduce base enums by @penguinolog in https://github.com/urwid/urwid/pull/595 Documentation 🕮 ++++++++++++++++ * Use non deprecated template by @jspricke in https://github.com/urwid/urwid/pull/424 * Mention asyncio event loop compatibility in readme by @johtso in https://github.com/urwid/urwid/pull/463 * Fix documentation of TrioEventLoop.run_async() by @ntamas in https://github.com/urwid/urwid/pull/438 * Fix column label typo in tour example by @devfull in https://github.com/urwid/urwid/pull/473 * Update index.rst by @adbenitez in https://github.com/urwid/urwid/pull/504 * fix typo by @doctorcolossus in https://github.com/urwid/urwid/pull/493 * Update README.rst by @yhh2021 in https://github.com/urwid/urwid/pull/481 * docs: fix simple typo, incompatable -> incompatible by @timgates42 in https://github.com/urwid/urwid/pull/446 * Fixed twisted example: use `implementer` decorator instead of deprecated `implements`. by @penguinolog in https://github.com/urwid/urwid/pull/591 * examples/terminal.py can run against older versions of Urwid again by @danschwarz in https://github.com/urwid/urwid/pull/596 * fix: update links to examples by @geier in https://github.com/urwid/urwid/pull/577 Other Changes +++++++++++++ * Test fixes by @penguinolog in https://github.com/urwid/urwid/pull/524 * Fix input handling and extra type annotations by @penguinolog in https://github.com/urwid/urwid/pull/530 * Fix regression: `Pile()` focus_item can be Widget -> need to set property `focus` in constructor by @penguinolog in https://github.com/urwid/urwid/pull/535 * Fix incorrect type cast in vterm (`apply_mapping` should return `bytes`) by @penguinolog in https://github.com/urwid/urwid/pull/545 * Return original code to the deprecated getters and setters by @penguinolog in https://github.com/urwid/urwid/pull/549 * Fix CheckBox default state validation and initialization by @penguinolog in https://github.com/urwid/urwid/pull/553 New Contributors ++++++++++++++++ * @johtso made their first contribution in https://github.com/urwid/urwid/pull/463 * @devfull made their first contribution in https://github.com/urwid/urwid/pull/473 * @adbenitez made their first contribution in https://github.com/urwid/urwid/pull/504 * @doctorcolossus made their first contribution in https://github.com/urwid/urwid/pull/493 * @yhh2021 made their first contribution in https://github.com/urwid/urwid/pull/481 * @dotlambda made their first contribution in https://github.com/urwid/urwid/pull/517 * @rvtpro made their first contribution in https://github.com/urwid/urwid/pull/503 * @vapier made their first contribution in https://github.com/urwid/urwid/pull/490 * @proskur1n made their first contribution in https://github.com/urwid/urwid/pull/477 * @naglis made their first contribution in https://github.com/urwid/urwid/pull/431 * @dlax made their first contribution in https://github.com/urwid/urwid/pull/409 * @mandre made their first contribution in https://github.com/urwid/urwid/pull/507 * @timgates42 made their first contribution in https://github.com/urwid/urwid/pull/446 * @djyotta made their first contribution in https://github.com/urwid/urwid/pull/500 * @penguinolog made their first contribution in https://github.com/urwid/urwid/pull/523 * @exquo made their first contribution in https://github.com/urwid/urwid/pull/474 * @roadriverrail made their first contribution in https://github.com/urwid/urwid/pull/465 * @rbanffy made their first contribution in https://github.com/urwid/urwid/pull/434 * @mfncooper made their first contribution in https://github.com/urwid/urwid/pull/489 * @AnonymouX47 made their first contribution in https://github.com/urwid/urwid/pull/543 * @danschwarz made their first contribution in https://github.com/urwid/urwid/pull/559 * @dependabot made their first contribution in https://github.com/urwid/urwid/pull/570 Urwid 2.1.2 =========== 2020-09-26 * Add pack method to LineBox. Fixes: #346 (by Miguel de Dios) * Add a test to check the linebox.pack is good. (by Miguel de Dios) * Add bin/release.sh script to partially automate releases. (by Tony Cebzanov) * Add workaround for #386 (by Tony Cebzanov) * Fix curses_display python3 ord() (by Ya-Liang Chang (Allen)) * Fix bumping to dev version in release.sh script (by Tony Cebzanov) * Fix focus_end on a collapsed tree (by Anonymous Maarten) * Fix crash with "ellipsis" clipping for py2 tour.py works with py2 now Typo in tour.py (by akorb) * Ignore resetting to invalid locale (Closes: #377) (by Jochen Sprickerhof) * Use ord2 for python2/3 compatibility (by Ya-Liang Chang (Allen)) Urwid 2.1.1 =========== 2020-07-26 * Add TrioEventLoop.run_async(), removed nursery constructor arg (#392) (by Tamás Nepusz) * Add py38 to Travis tests (by Andrey Semakin) * Add popular IDEs folders to .gitignore (by Andrey Semakin) * Add wrap_around kwarg to SimpleListWalkers (by Krzysztof Królczyk) * Change documentation on Terminal (by James Johnson) * Remove debug documentation change test (by James Johnson) * Remove support for py34 (by Andrey Semakin) * Remove invalid escape sequence (by Andrey Lebedev) * Fix GridFlow keypress handling when v_sep is 0 (by Aurelien Grenotton) * Fix Terminal in ListBox (#382) (by James Johnson) * Fix Crash on `fg`, SIGCONT (after Ctrl-Z, SIGSTOP, SIGTSTP) (by goncalopp) * Fix 256-color mode on some terminals. Addresses #404. (by Tony Cebzanov) * vterm: reduce __init__ boilerplate (by max ulidtko) * vterm: errno 5 is not EOF. (by max ulidtko) * Terminal: use UTF-8 by default. (by max ulidtko) * Instance of Terminal has no __super attr -- thanks pylint! (by max ulidtko) * Do not call wait_readable with a closed fd in TrioEventLoop (by Michael Hudson-Doyle) * Make options a static method where applicable (by Philip Matura) * Set up Travis to run py38, speed up build (by Andrey Semakin) * Use comparison with a string instead of "is" test with a literal (by Andrej Shadura) Urwid 2.1.0 =========== 2019-11-13 * Add support for Python 3.7 and 3.8, drop support for Python 3.3 * Add 24-bit (true color) support. (by Tony Cebzanov) * Add TrioEventLoop (by Tamas Nepusz) * Add support for input encoding in Terminal widget (by Tamas Nepusz) * Add ability to specify LineBox title attribute (by Tom Pickering) * Add custom checkbox symbol (by Krzysztof Królczyk) * Add installation instruction to README (by Patryk NiedźwiedziÅ„ski) * Remove PollingListWalker class (by Heiko Noordhof) * Change SelectableIcon default cursor_position to 0. (by Werner Beroux) * Extended numerical editing: integers and floats (by hootnot) * Re-raise coroutine exceptions in AsyncioEventLoop properly (by nocarryr) * Fixed locale issue (by Andrew Dunai) * Gate SIGWINCH behind GLib 2.54+ (by Nick Chavez) * Remove method Text._calc_line_translation() (by rndusr) * Fix colon in HalfBlock5x4Font (by Alex Ozer) * Don't use deprecated inspect.getargspec() with python3 (by rndusr) * Fix issue "Non-integer division in bargraph when using set_bar_width(1)" (by Carlos Jenkins) * Fix misleading indentation in Screen._stop() (by Akos Kiss) * Fix crash on click-Esc & Esc-click (by Maxim Ivanov) * Use 'TimerHandle.cancelled()' if available (by Mohamed Seleem) * Break rather than raising exception on shard calculation bug. (by Tony Cebzanov) * Increase _idle_emulation_delay. (by Tony Cebzanov) * Fix EOF detection for the Terminal widget on Python 3 (by Tamas Nepusz) * Fix the asyncio example, and make the raw Screen work without real files (by Eevee) * Unbreak python ./examples/treesample HOME END keys. (by Dimitri John Ledkov) * Urwid.util: Fix bug in rle_append_beginning_modify (by BkPHcgQL3V) * Fix AttributeError on mouse click (by mbarkhau) * Fix ProgressBar smoothing on Python 3.x (by Tamas Nepusz) * Fix asyncio event loop test on py3.4 (by Maxim Ivanov) * Handle case where MainLoop._topmost_widget does not implement mouse_event (by Rasmus Bondesson) * Implement `ellipsis` wrapping mode for StandardTextLayout (by Philip Matura) * Fix .pack call in Columns.column_widths (by Philip Matura) * Use ._selectable member for Edit widget (by Philip Matura) * Fix use of ignore_focus, for widgets inheriting from Text (by Philip Matura) * Remove some special handling for TreeListBox (by Philip Matura) * Make Columns and Pile selectable when any child widget is (by Philip Matura) * Implement get_cursor_coords for Frame widget (by Philip Matura) * Fix Frame mouse_event when footer is trimmed (by Philip Matura) * Fix Python 3.8 SyntaxWarning: 'str' object is not callable (by Anders Kaseorg) * README: Use SVG build status badge (by Olle Jonsson) Urwid 2.0.1 =========== 2018-01-21 * #275: Late fix for proper exception reraising from within main loop (by Andrew Dunai & Adam Sampson) Urwid 2.0.0 =========== 2018-01-17 * Full Python 2.x/3.x support (by Andrew Dunai) * Proper handling & customization of OS signals by GLib event loop (by Federico T) * vterm: Fix handling of NUL characters (by aszlig) * Add 256-color support for fbterm (by Benjamin Yates) * Italics support (by Ian D. Scott) * Store envron's TERM value as a Screen attribute (by Benjamin Yates) * Replaced hashbangs to use proper Python binary (by Douglas La Rocca) * Post-change signal for Edit, CheckBox and RadioButton widgets (by Toshio Kuratomi) * ListBox.body update (by Random User) * SimpleListWalker is now default when setting ListBox.body (by Random User) * #246, #234: SelectEventLoop alarm improvements (by Dave Jones) * #211: Title align & borderless sides for LineBox (by Toshio Kuratomi) * Support for 'home' and 'end' keys in ListBox (by Random User) * Various code cleanups (by Jordan Speicher, Marin Atanasov Nikolov) * CI fixes (by Marlox, Ian Ward, Anatoly Techtonik, Tony Cebzanov & OndÅ™ej Súkup) * Example fixes (by Kenneth Nielsen) * Documentation fixes (by anatoly techtonik, Marcin Kurczewski, mobyte0, Christian Geier & xndcn) * Code cleanup & typo fixes (by Jakub Wilk & Boris Feld) * Integration of tox for easier Python cross-version testing (by Andrew Dunai) * Test fixes (by Michael Hudson-Doyle, Mike Gilbert & Andrew Dunai) * Correct error messages in Decoration (by Marcin Kurczewski) * #141: Fix for StandardTextLayout.calculate_text_segments (by Grzegorz Aksamit) * #221: Fix for raw display should release file descriptors (by Alain Leufroy) * #261: Fix issues with unicode characters in ProgressBar (by Andrew Dunai) * Fix for 'page up' and 'page down' in ListBox when having focusable children (by Random User) * Fixes for examples compatibility with Python 3 (by Lars Kellogg-Stedman) * Fix default screen size on raw display (by Andreas Klöckner) * Fix underlining for padded text (by Random User) * Fix for terminal widget crash with Python 3 (by Sjc1000) * Fix for string formatting error (by Jakub Wilk) * Fix for iterator in WidgetContainerListContentsMixin (by Marlox) * Fix for missing `modified` signal in SimpleFocusListWalker (by Michael Hansen) * Dropped Python 3.2 support * Test coverage is now collected Urwid 1.3.1 =========== 2015-11-01 * Fix for screen not getting reset on exception regression (by Rian Hunter) * AttrSpec objects are now comparable (by Random User) * MonitoredList now has a clear method if list has a clear method (by neumond) * Fix for BarGraph hlines sort order (by Heiko Noordhof) * Fix for final output not appearing on exit with some terminals now that extra newline was removed (by Jared Winborne) * Fix for a resizing bug in raw_display (by Esteban null) Urwid 1.3.0 =========== 2014-10-17 * New AsyncioEventLoop for Python 3.4, Python 3.x with asyncio package or Python 2 with trollius package (by Alex Munroe, Jonas Wielicki, with earlier work by Kelketek Rritaa) * Screen classes now call back to MainLoop using event loop alarms instead of passing timeout values to MainLoop (by Alex Munroe) * Add support for bright backgrounds on linux console (by Russell Warren) * Allow custom sorting of MonitoredList (by Tony Cebzanov) * Fix support for negative indexes with MonitoredFocusList (by Heiko Noordhof) * Documentation fixes (by Ismail, Matthew Mosesohn) * SelectableIcon using cursor_position=0 by default instead of 1. Urwid 1.2.2 =========== 2014-10-05 * Fix for a serious raw_display performance regression (by Anton Khirnov) * Fix for high color palette detection (by extempo) * Small changes to enable windows support (by Jeanpierre Devin) Urwid 1.2.1 =========== 2014-04-04 * Fix false failures of event loop tests * Remove extra newline generated on exit of raw_display * Documentation fixes (by Paul Ivanov) Urwid 1.2.0 =========== 2014-02-09 * Add support for PyPy, drop support for Python 2.4, 2.5 * Signals now support using weakly referenced arguments to help avoid leaking objects when a signal consumer is no longer referenced (by Matthijs Kooijman) * Add TornadoEventLoop class (by Alexander Glyzov) * Update GlibEventLoop to use python-gi for Python3 compatibility (by Israel Garcia) * Automate testing with Python 2.6, 2.7, 3.2, 3.3 and PyPy using travis-ci * New container method get_focus_widgets() (by Matthijs Kooijman) * Add support for double and triple click mouse events (by Igor KotrasiÅ„ski) * Allow disabling and re-enabling of mouse tracking (by Jim Garrison) * Create section in docs for example program screenshots generated as images like the tutorial examples * Add suggested basic color combination images to manual * Fall back to 80x24 if screen size detection fails * Fix screen.stop(), screen.start() disabling mouse events * Fix to make GridFlow v_sep argument behave as documented * Fix for registering high palette entries in the form "hX" where X > 15 so that basic colors are applied in 88-color mode * Fix for raw_display clear-right escape not working with standout attribute on some terminals * Fix for Terminal widget select loop: retry when interrupted Urwid 1.1.2 =========== 2013-12-30 * Move to urwid.org and use sphinx docs for generating whole site, move changelog to docs/changelog.rst * Fix encoding exceptions when unicode used on non-UTF-8 terminal * Fix for suspend and resume applications with ^Z * Fix for tmux and screen missing colors on right bug * Fix Pile zero-weighted items and mouse_event when empty * Fix Terminal select() not retrying when interrupted by signal * Fix for Padding.align and width change not invalidating Urwid 1.1.1 =========== 2012-11-15 * Fix for Pile not changing focus on mouse events * Fix for Overlay.get_cursor_coords() Urwid 1.1.0 =========== 2012-10-23 * New common container API: focus, focus_position, contents, options(), get_focus_path(), set_focus_path(), __getitem__, __iter__(), __reversed__() implemented across all included container widgets A full description doesn't fit here, see the Container Widgets section in the manual for details * New Sphinx-based documentation now included in source: Tutorial rewritten, manual revised and new reference based on updated docstrings (by Marco Giusti, Patrick Totzke) * New list walker SimpleFocusListWalker like SimpleListWalker but updates focus position as items are inserted or removed * New decoration widget WidgetDisable to disable interaction with the widgets it wraps * SelectableIcon selectable text widget used by button widgets is now documented (available since 0.9.9) * Columns widget now tries to keep column in focus visible, hiding columns on the left when necessary * Padding widget now defaults to ('relative', 100) instead of 'pack' so that left and right parameters are more useful and more child widgets are supported * New list walker "API Version 2" that is simpler for many list walker uses; "API Version 1" will still continue to be supported * List walkers may now allow iteration from the absolute top or bottom of the list if they provide a positions() method * raw_display now erases to the end of the line with EL escape sequence to improve copy+paste behavior for some terminals * Filler now has top and bottom parameters like Padding's left and right parameters and accepts 'pack' instead of None as a height value for widgets that calculate their own number of rows * Pile and Columns now accepts 'pack' instead of 'flow' for widgets that calculate their own number of rows or columns * Pile and Columns now accept 'given' instead of 'fixed' for cases where the number of rows or columns are specified by the container options * Pile and Columns widgets now accept any iterable to their __init__() methods * Widget now has a default focus_position property that raises an IndexError when read to be consistent with new common container API * GridFlow now supports multiple cell widths within the same widget * BoxWidget, FlowWidget and FixedWidget are deprecated, instead use the sizing() function or _sizing attribute to specify the supported sizing modes for your custom widgets * Some new shift+arrow and numpad input sequences from RXVT and xterm are now recognized * Fix for alarms when used with a screen event loop (e.g. curses_display) * Fix for raw_display when terminal width is 1 column * Fixes for a Columns.get_cursor_coords() regression and a SelectableIcon.get_cursor_coords() bug * Fixes for incorrect handling of box columns in a number of Columns methods when that column is selectable * Fix for Terminal widget input handling with Python 3 Urwid 1.0.3 =========== 2012-11-15 * Fix for alarms when used with a screen event loop (e.g. curses_display) * Fix for Overlay.get_cursor_coords() Urwid 1.0.2 =========== 2012-07-13 * Fix for bug when entering Unicode text into Edit widget with bytes caption * Fix a regression when not running in UTF-8 mode * Fix for a MainLoop.remove_watch_pipe() bug * Fix for a bug when packing empty Edit widgets * Fix for a ListBox "contents too long" error with very large Edit widgets * Prevent ListBoxes from selecting 0-height selectable widgets when moving up or down * Fix a number of bugs caused by 0-height widgets in a ListBox Urwid 1.0.1 =========== 2011-11-28 * Fix for Terminal widget in BSD/OSX * Fix for a Filler mouse_event() position bug * Fix support for mouse positions up to x=255, y=255 * Fixes for a number of string encoding issues under Python 3 * Fix for a LineBox border __init__() parameters * Fix input of UTF-8 in tour.py example by converting captions to unicode * Fix tutorial examples' use of TextCanvas and switch to using unicode literals * Prevent raw_display from calling tcseattr() or tcgetattr() on non-ttys * Disable curses_display external event loop support: screen resizing and gpm events are not properly supported * Mark PollingListWalker as deprecated Urwid 1.0.0 =========== 2011-09-22 * New support for Python 3.2 from the same 2.x code base, requires distribute instead of setuptools (by Kirk McDonald, Wendell, Marien Zwart) everything except TwistedEventLoop and GLibEventLoop is supported * New experimental Terminal widget with xterm emulation and terminal.py example program (by aszlig) * Edit widget now supports a mask (for passwords), has an insert_text_result() method for full-field validation and normalizes input text to Unicode or bytes based on the caption type used * New TreeWidget, TreeNode, ParentNode, TreeWalker and TreeListBox classes for lazy expanding/collapsing tree views factored out of browse.py example program, with new treesample.py example program (by Rob Lanphier) * MainLoop now calls draw_screen() just before going idle, so extra calls to draw_screen() in user code may now be removed * New MainLoop.watch_pipe() method for subprocess or threaded communication with the process/thread updating the UI, and new subproc.py example demonstrating its use * New PopUpLauncher and PopUpTarget widgets and MainLoop option for creating pop-ups and drop-downs, and new pop_up.py example program * New twisted_serve_ssh.py example (by Ali Afshar) that serves multiple displays over ssh from the same application using Twisted and the TwistedEventLoop * ListBox now includes a get_cursor_coords() method, allowing nested ListBox widgets * Columns widget contents may now be marked to always be treated as flow widgets for mixing flow and box widgets more easily * New lcd_display module with support for CF635 USB LCD panel and lcd_cf635.py example program with menus, slider controls and a custom font * Shared command_map instance is now stored as Widget._command_map class attribute and may be overridden in subclasses or individual widgets for more control over special keystrokes * Overlay widget parameters may now be adjusted after creation with set_overlay_parameters() method * New WidgetPlaceholder widget useful for swapping widgets without having to manipulate a container widget's contents * LineBox widgets may now include title text * ProgressBar text content and alignment may now be overridden * Use reactor.stop() in TwistedEventLoop and document that Twisted's reactor is not designed to be stopped then restarted * curses_display now supports AttrSpec and external event loops (Twisted or GLib) just like raw_display * raw_display and curses_display now support the IBMPC character set (currently only used by Terminal widget) * Fix for a gpm_mev bug preventing user input when on the console * Fix for leaks of None objects in str_util extension * Fix for WidgetWrap and AttrMap not working with fixed widgets * Fix for a lock up when attempting to wrap text containing wide characters into a single character column Urwid 0.9.9.2 ============= 2011-07-13 * Fix for an Overlay get_cursor_coords(), and Text top-widget bug * Fix for a Padding rows() bug when used with width=PACK * Fix for a bug with large flow widgets used in an Overlay * Fix for a gpm_mev bug * Fix for Pile and GraphVScale when rendered with no contents * Fix for a Python 2.3 incompatibility (0.9.9 is the last release to claim support Python 2.3) Urwid 0.9.9.1 ============= 2010-01-25 * Fix for ListBox snapping to selectable widgets taller than the ListBox itself * raw_display switching to alternate buffer now works properly with Terminal.app * Fix for BoxAdapter backwards incompatibility introduced in 0.9.9 * Fix for a doctest failure under powerpc * Fix for systems with gpm_mev installed but not running gpm Urwid 0.9.9 =========== 2009-11-15 * New support for 256 and 88 color terminals with raw_display and html_fragment display modules * New palette_test example program to demonstrate high color modes * New AttrSpec class for specifying specific colors instead of using attributes defined in the screen's palette * New MainLoop class ties together widgets, user input, screen display and one of a number of new event loops, removing the need for tedious, error-prone boilerplate code * New GLibEventLoop allows running Urwid applications with GLib (makes D-Bus integration easier) * New TwistedEventLoop allows running Urwid with a Twisted reactor * Added new docstrings and doctests to many widget classes * New AttrMap widget supports mapping any attribute to any other attribute, replaces AttrWrap widget * New WidgetDecoration base class for AttrMap, BoxAdapter, Padding, Filler and LineBox widgets creates a common method for accessing and updating their contained widgets * New left and right values may be specified in Padding widgets * New command_map for specifying which keys cause actions such as clicking Button widgets and scrolling ListBox widgets * New tty_signal_keys() method of raw_display.Screen and curses_display.Screen allows changing or disabling the keys used to send signals to the application * Added helpful __repr__ for many widget classes * Updated all example programs to use MainLoop class * Updated tutorial with MainLoop usage and improved examples * Renamed WidgetWrap.w to _w, indicating its intended use as a way to implement a widget with other widgets, not necessarily as a container for other widgets * Replaced all tabs with 4 spaces, code is now more aerodynamic (and PEP 8 compliant) * Added saving of stdin and stdout in raw_display module allowing the originals to be redirected * Updated BigText widget's HalfBlock5x4Font * Fixed graph example CPU usage when animation is stopped * Fixed a memory leak related to objects listening for signals * Fixed a Popen3 deprecation warning Urwid 0.9.8.4 ============= 2009-03-13 * Fixed incompatibilities with Python 2.6 (by Friedrich Weber) * Fixed a SimpleListWalker with emptied list bug (found by Walter Mundt) * Fixed a curses_display stop()/start() bug (found by Christian Scharkus) * Fixed an is_wide_character() segfault on bad input data bug (by Andrew Psaltis) * Fixed a CanvasCache with render() used in both a widget and its superclass bug (found by Andrew Psaltis) * Fixed a ListBox.ends_visible() on empty list bug (found by Marc Hartstein) * Fixed a tutorial example bug (found by Kurtis D. Rader) * Fixed an Overlay.keypress() bug (found by Andreas Klöckner) * Fixed setuptools configuration (by Andreas Klöckner) Urwid 0.9.8.3 ============= 2008-07-14 * Fixed a canvas cache memory leak affecting 0.9.8, 0.9.8.1 and 0.9.8.2 (found by John Goodfellow) * Fixed a canvas fill_attr() bug (found by Joern Koerner) Urwid 0.9.8.2 ============= 2008-05-19 * Fixed incompatibilities with Python 2.3 * Fixed Pile cursor pref_col bug, WidgetWrap rows caching bug, Button mouse_event with no callback bug, Filler body bug triggered by the tutorial and a LineBox lline parameter typo. Urwid 0.9.8.1 ============= 2007-06-21 * Fixed a Filler render() bug, a raw_display start()/stop() bug and a number of problems triggered by very small terminal window sizes. Urwid 0.9.8 =========== 2007-03-23 * Rendering is now significantly faster. * New Widget base class for all widgets. It includes automatic caching of rows() and render() methods. It also adds a new __super attribute for accessing methods in superclasses. Widgets must now call self._invalidate() to notify the cache when their content has changed. To disable caching in a widget set the class variable no_cache to a list that includes the string "render". * Canvas classes have been reorganized: Canvas has been renamed to TextCanvas and Canvas is now the base class for all canvases. New canvas classes include BlankCanvas, SolidCanvas and CompositeCanvas. * External event loops may now be used with the raw_display module. The new methods get_input_descriptors() and get_input_nonblocking() should be used instead of get_input() to allow input processing without blocking. * The Columns, Pile and ListBox widgets now choose their first selectable child widget as the focus widget by default. * New ListWalker base class for list walker classes. * New Signals class that will be used to improve the existing event callbacks. Currently it is used for ListWalker objects to notify their ListBox when their content has changed. * SimpleListWalker now behaves as a list and supports all list operations. This class now detects when changes are made to the list and notifies the ListBox object. New code should use this class to wrap lists of widgets before passing them to the ListBox constructor. * New PollingListWalker class is now the default list walker that is used when passing a simple list to the ListBox constructor. This class is intended for backwards compatibility only. When this class is used the ListBox object is unable to cache its render() method. * The curses_display module can now draw in the lower-right corner of the screen. * All display modules now have start() and stop() methods that may be used instead of calling run_wrapper(). * The raw_display module now uses an alternate buffer so that the original screen can be restored on exit. The old behaviour is available by setting the alternate_buffer parameter of start() or run_wrapper() to False. * Many internal string processing functions have been rewritten in C to improve their performance. * Compatible with Python >= 2.2. Python 2.1 is no longer supported. Urwid 0.9.7.2 ============= 2007-01-03 * Improved performance in UTF-8 mode when ASCII text is used. * Fixed a UTF-8 input bug. * Added a clear() function to the display modules to force the screen to be repainted on the next draw_screen() call. Urwid 0.9.7.1 ============= 2006-10-03 * Fixed bugs in Padding and Overlay widgets introduced in 0.9.7. Urwid 0.9.7 =========== 2006-10-01 * Added initial support for fixed widgets - widgets that have a fixed size on screen. Fixed widgets expect a size parameter equal to (). Fixed widgets must implement the pack(..) function to return their size. * New BigText class that draws text with fonts made of grids of character cells. BigText is a fixed widget and doesn't do any alignment or wrapping. It is intended for banners and number readouts that need to stand out on the screen. Fonts: Thin3x3Font, Thin4x3Font, Thin6x6Font (full ascii) UTF-8 only fonts: HalfBlock5x4Font, HalfBlock6x5Font, HalfBlockHeavy6x5Font, HalfBlock7x7Font (full ascii) New function get_all_fonts() may be used to get a list of the available fonts. * New example program bigtext.py demonstrates use of BigText. * Padding class now has a clipping mode that pads or clips fixed widgets to make them behave as flow widgets. * Overlay class can now accept a fixed widget as the widget to display "on top". * New Canvas functions: pad_trim() and pad_trim_left_right(). * Fixed a bug in Filler.get_cursor_coords() that causes a crash if the contained widget's get_cursor_coords() function returns None. * Fixed a bug in Text.pack() that caused an infinite loop when the text contained a newline. This function is not currently used by Urwid. * Edit.__init__() now calls set_edit_text() to initialize its text. * Overlay.calculate_padding_filler() and Padding.padding_values() now include focus parameters. Urwid 0.9.6 =========== 2006-08-22 * Fixed Unicode conversion and locale issues when using Urwid with Python < 2.4. The graph.py example program should now work properly with older versions of Python. * The docgen_tutorial.py script can now write out the tutorial example programs as individual files. * Updated reference documentation table of contents to show which widgets are flow and/or box widgets. * Columns.set_focus(..) will now accept an integer or a widget as its parameter. * Added detection for rxvt's HOME and END escape sequences. * Added support for setuptools (improved distutils). Urwid 0.9.5 =========== 2006-06-14 * Some Unicode characters are now converted to use the G1 alternate character set with DEC special and line drawing characters. These Unicode characters should now "just work" in almost all terminals and encodings. When Urwid is run with the UTF-8 encoding the characters are left as UTF-8 and not converted. The characters converted are: \u00A3 (£), \u00B0 (°), \u00B1 (±), \u00B7 (·), \u03C0 (Ï€), \u2260 (≠), \u2264 (≤), \u2265 (≥), \u23ba (⎺), \u23bb (⎻), \u23bc (⎼), \u23bd (⎽), \u2500 (─), \u2502 (│), \u250c (┌), \u2510 (â”�), \u2514 (â””), \u2518 (┘), \u251c (├), \u2524 (┤), \u252c (┬), \u2534 (â”´), \u253c (┼), \u2592 (â–’), \u25c6 (â—†) * New SolidFill class for filling an area with a single character. * New LineBox class for wrapping widgets in a box made of line- drawing characters. May be used as a box widget or a flow widget. * New example program graph.py demonstrates use of BarGraph, LineBox, ProgressBar and SolidFill. * Pile class may now be used as a box widget and contain a mix of box and flow widgets. * Columns class may now contain a mix of box and flow widgets. The box widgets will take their height from the maximum height of the flow widgets. * Improved the smoothness of resizing with raw_display module. The module will now try to stop updating the screen when a resize event occurs during the update. * The Edit and IntEdit classes now use their set_edit_text() and set_edit_pos() functions when handling keypresses, so those functions may be overridden to catch text modification. * The set_state() functions in the CheckBox and RadioButton classes now have a do_callback parameter that determines if the callback function registered will be called. * Fixed a newly introduced incompatibility with python < 2.3. * Fixed a missing symbol in curses_display when python is linked against libcurses. * Fixed mouse handling bugs in the Frame and Overlay classes. * Fixed a Padding bug when the left or right has no padding. Urwid 0.9.4 =========== 2006-05-30 * Enabled mouse handling across the Urwid library. Added a new mouse_event() method to the Widget interface definition and to the following widgets: Edit, CheckBox, RadioButton, Button, GridFlow, Padding, Filler, Overlay, Frame, Pile, Columns, BoxAdapter and ListBox. Updated example programs browse.py, calc.py, dialog.py, edit.py and tour.py to support mouse input. * Released the files used to generate the reference and tutorial documentation: docgen_reference.py, docgen_tutorial.py and tmpl_tutorial.html. The "docgen" scripts write the documentation to stdout. docgen_tutorial.py requires the Templayer HTML templating library to run: http://excess.org/templayer/ * Improved Widget and List Walker interface documentation. * Fixed a bug in the handling of invalid UTF-8 data. All invalid characters are now replaced with '?' characters when displayed. Urwid 0.9.3 =========== 2006-05-14 * Improved mouse reporting. The raw_display module now detects gpm mouse events by reading /usr/bin/mev output. The curses_display module already supports gpm directly. Mouse drag events are now reported by raw_display in terminals that provide button event tracking and on the console with gpm. Note that gpm may report coordinates off the screen if the user drags the mouse off the edge. Button release events now report which button was released if that information is available, currently only on the console with gpm. * Added display of raw keycodes to the input_test.py example program. * Fixed a text layout bug affecting clipped text with blank lines, and another related to wrapped text starting with a space character. * Fixed a Frame.keypress() bug that caused it to call keypress on unselectable widgets. Urwid 0.9.2 =========== 2006-03-18 * Preliminary mouse support was added to the raw_display and curses_display modules. A new Screen.set_mouse_tracking() method was added to enable mouse tracking. Mouse events are returned alongside keystrokes from the Screen.get_input() method. The widget interface does not yet include mouse handling. This will be addressed in the next release. * A new convenience function is_mouse_event() was added to help in separating mouse events from keystrokes. * Added a new example program input_test.py. This program displays the keyboard and mouse input it receives. It may be run as a CGI script or from the command line. On the command line it defaults to using the curses_display module, use input_test.py raw to use the raw_display module instead. * Fixed an Edit.render() bug that caused it to render the cursor in a different location than that reported by Edit.get_cursor_coords() in some circumstances. * Fixed a bug preventing use of UTF-8 characters with Divider widgets. Urwid 0.9.1 =========== 2006-03-06 * BarGraph and ProgressBar can now display data more accurately by using the UTF-8 vertical and horizontal eighth characters. This behavior will be enabled when the UTF-8 encoding is detected and "smoothed" attributes are passed to the BarGraph or ProgressBar constructors. * New get_encoding_mode() function to determine how Urwid will treat raw string data. * New raw_display.signal_init() and raw_display.signal_restore() methods that may be overridden by threaded applications that need to call signal.signal() from their main thread. * Fixed a bug that prevented the use of UTF-8 strings in text markup. * Removed some forgotten asserts that broke 8-bit and CJK input. Urwid 0.9.0 =========== 2006-02-18 * New support for UTF-8 encoding including input, display and editing of narrow and wide (CJK) characters. Preliminary combining (zero-width) character support is included, but full support will require terminal behavior detection. Right-to-Left input and display are not implemented. * New raw_display module that handles console display without relying on external libraries. This module was written as a work around for the lack of UTF-8 support in the standard version of ncurses. Eliminates "dead corner" in the bottom right of the screen. Avoids use of bold text in xterm and gnome-terminal for improved text legibility. * Fixed Overlay bug related to UTF-8 handling. * Fixed Edit.move_cursor_to_coords(..) bug related to wide characters in UTF-8 encoding. Urwid 0.9.0-pre3 ================ 2006-02-13 * Fixed Canvas attribute padding bug related to -pre1 changes. Urwid 0.9.0-pre2 ================ 2006-02-10 * Replaced the custom align and wrap modes in example program calc.py with a new layout class. * Fixed Overlay class call to Canvas.overlay() broken by -pre1 changes. * Fixed Padding bug related to Canvas -pre1 changes. Urwid 0.9.0-pre1 ================ 2006-02-08 * New support for UTF-8 encoding. Unicode strings may be used and will be converted to the current encoding when output. Regular strings in the current encoding may still be used. PLEASE NOTE: There are issues related to displaying UTF-8 characters with the curses_display module that have not yet been resolved. * New set_encoding() function replaces util.set_double_byte_encoding(). * New supports_unicode() function to query if unicode strings with characters outside the ascii range may be used with the current encoding. * New TextLayout and StandardTextLayout classes to perform text wrapping and alignment. Text widgets now have a layout parameter to allow use of custom TextLayout objects. * New layout structure replaces line translation structure. Layout structure now allows arbitrary reordering/positioning of text segments, inclusion of UTF-8 characters and insertion of text not found in the original text string. * Removed util.register_align_mode() and util.register_wrap_mode(). Their functionality has been replaced by the new layout classes. Urwid 0.8.10 ============ 2005-11-27 * Expanded tutorial to cover advanced ListBox usage, custom widget classes and the Pile, BoxAdapter, Columns, GridFlow and Overlay classes. * Added escape sequence for "shift tab" to curses_display. * Added ListBox.set_focus_valign() to allow positioning of the focus widget within the ListBox. * Added WidgetWrap class for extending existing widgets without inheriting their complete namespace. * Fixed web_display/mozilla breakage from 0.8.9. Fixed crash on invalid locale setting. Fixed ListBox slide-back bug. Fixed improper space trimming in calculate_alignment(). Fixed browse.py example program rows bug. Fixed sum definition, use of long ints for python2.1. Fixed warnings with python2.1. Fixed Padding.get_pref_col() bug. Fixed Overlay splitting CJK characters bug. Urwid 0.8.9 =========== 2005-10-21 * New Overlay class for drawing widgets that obscure parts of other widgets. May be used for drop down menus, combo boxes, overlapping "windows", caption text etc. * New BarGraph, GraphVScale and ProgressBar classes for graphical display of data in Urwid applications. * New method for configuring keyboard input timeouts and delays: curses_display.Screen.set_input_timeouts(). * Fixed a ListBox.set_focus() bug. Urwid 0.8.8 =========== 2005-06-13 * New web_display module that emulates a console display within a web browser window. Application must be run as a CGI script under Apache. Supports font/window resizing, keepalive for long-lived connections, limiting maximum concurrent connections, polling and connected update methods. Tested with Mozilla Firefox and Internet Explorer. * New BoxAdapter class for using box widgets in places that usually expect flow widgets. * New curses_display input handling with better ESC key detection and broader escape code support. * Shortened resize timeout on gradual resize to improve responsiveness. Urwid 0.8.7 =========== 2005-05-21 * New widget classes: Button, RadioButton, CheckBox. * New layout widget classes: Padding, GridFlow. * New dialog.py example program that behaves like dialog(1) command. * Pile widgets now support selectable items, focus changing with up and down keys and setting the cursor position. * Frame widgets now support selectable items in the header and footer. * Columns widgets now support fixed width and relative width columns, a minimum width for all columns, selectable items within columns containing flow widgets (already supported for box widgets), focus changing with left and right keys and setting the cursor position. * Filler widgets may now wrap box widgets and have more alignment options. * Updated tour.py example program to show new widget types and features. * Avoid hogging cpu on gradual window resize and fix for slow resize with cygwin's broken curses implementation. * Fixed minor CJK problem and curs_set() crash under MacOSX and Cygwin. * Fixed crash when deleting cells in calc.py example program. Urwid 0.8.6 =========== 2005-01-03 * Improved support for CJK double-byte encodings: BIG5, UHC, GBK, GB2312, CN-GB, EUC-KR, EUC-CN, EUC-JP (JISX 0208 only) and EUC-TW (CNS 11643 plain 1 only) * Added support for ncurses' use_default_colors() function to curses_display module (Python >= 2.4). register_palette() and register_palette_entry() now accept "default" as foreground and/or background. If the terminal's default attributes cannot be detected black on light gray will be used to accommodate terminals with always-black cursors. "default" is now the default for text with no attributes. This means that areas with no attributes will change from light grey on black (curses default) to black on light gray or the terminal's default. * Modified examples to not use black as background of Edit widgets. * Fixed curses_display curs_set() call so that cursor is hidden when widget in focus has no cursor position. Urwid 0.8.5 =========== 2004-12-15 * New tutorial covering basic operation of: curses_display.Screen, Canvas, Text, FlowWidget, Filler, BoxWidget, AttrWrap, Edit, ListBox and Frame classes * New widget class: Filler * New ListBox functions: get_focus(), set_focus() * Debian packages for Python 2.4. * Fixed curses_display bug affecting text with no attributes. Urwid 0.8.4 =========== 2004-11-20 * Improved support for Cyrillic and other simple 8-bit encodings. * Added new functions to simplify taking screenshots: html_fragment.screenshot_init() and html_fragment.screenshot_collect() * Improved urwid/curses_display.py input debugging * Fixed cursor in screenshots of CJK text. Fixed "end" key in Edit boxes with CJK text. Urwid 0.8.3 =========== 2004-11-15 * Added support for CJK double-byte encodings. Word wrapping mode "space" will wrap on edges of double width characters. Wrapping and clipping will not split double width characters. curses_display.Screen.get_input() may now return double width characters. Text and Edit classes will work with a mix of regular and double width characters. * Use new method Edit.set_edit_text() instead of Edit.update_text(). * Minor improvements to edit.py example program. Urwid 0.8.2 =========== 2004-11-08 * Re-released under GNU Lesser General Public License. Urwid 0.8.1 =========== 2004-10-29 * Added support for monochrome terminals. see curses_display.Screen.register_palette_entry() and example programs. set TERM=xterm-mono to test programs in monochrome mode. * Added unit testing code test_urwid.py to the examples. * Can now run urwid/curses_display.py to test your terminal's input and colour rendering. * Fixed an OSX browse.py compatibility issue. Added some OSX keycodes. Urwid 0.8.0 =========== 2004-10-17 * Initial Release urwid-urwid-69907e4/docs/conf.py000066400000000000000000000253341526073734300165350ustar00rootroot00000000000000# # Urwid documentation build configuration file, created by # sphinx-quickstart on Wed Nov 30 20:10:17 2011. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. """Sphinx configuration for the Urwid documentation.""" from __future__ import annotations import ast import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.todo", "sphinx.ext.coverage", "sphinx.ext.githubpages", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["tools/templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "Urwid" copyright = "2014, Ian Ward et al" # noqa: A001 # sphinx magic # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. FILE_PATH = os.path.dirname(__file__) VERSION_MODULE = os.path.abspath(os.path.join(FILE_PATH, "../urwid/version.py")) VERSION_VARS = {} with open(VERSION_MODULE, encoding="utf-8") as f: parsed = ast.parse(f.read()) for node in parsed.body: if not isinstance(node, (ast.Assign, ast.AnnAssign)): # Not variable define continue if node.value is None: # Not a proper assign continue try: value = ast.literal_eval(node.value) except ValueError: continue if isinstance(node, ast.Assign): VERSION_VARS.update( {tgt.id: value for tgt in node.targets if isinstance(tgt, ast.Name) and isinstance(tgt.ctx, ast.Store)} ) else: VERSION_VARS[node.target.id] = value # The short X.Y version. version = ".".join(str(x) for x in VERSION_VARS["__version_tuple__"][:2]) # The full version, including alpha/beta/rc tags. release = VERSION_VARS["__version__"] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # Generate CNAME html_baseurl = "https://urwid.org" # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = "default" html_style = None # make readthedocs really use the default theme # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = { "sidebarbgcolor": "#263193", "sidebarbtncolor": "#263193", "footerbgcolor": "#181035", "relbarbgcolor": "#181035", "sidebarlinkcolor": "#aabee8", "linkcolor": "#263193", "visitedlinkcolor": "#263193", "headtextcolor": "#181035", "headlinkcolor": "#181035", "collapsiblesidebar": False, } # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". html_title = f"Urwid {release}" # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = "urwid-logo.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["tools/static"] # Custom stylesheet, loaded after the theme's own CSS so it can patch layout # and admonition styling issues in the "classic" theme (see the comments in # the file itself for details). html_css_files = ["urwid_theme.css"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. html_sidebars = {"index": ["indexsidebar.html"]} # Additional templates that should be rendered to pages, maps page names to # template names. html_additional_pages = { "index": "indexcontent.html", } # If false, no module index is generated. html_domain_indices = False # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = "Urwiddoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ("index", "Urwid.tex", "Urwid Documentation", "Ian Ward", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [("index", "urwid", "Urwid Documentation", ["Ian Ward"], 1)] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ("index", "Urwid", "Urwid Documentation", "Ian Ward", "Urwid", "One line description of project.", "Miscellaneous"), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # -- Options for Epub output --------------------------------------------------- # Bibliographic Dublin Core info. epub_title = "Urwid" epub_author = "Ian Ward" epub_publisher = "Ian Ward" epub_copyright = "2014, Ian Ward et al" # The language of the text. It defaults to the language option # or en if the language is not set. # epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. # epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. # epub_identifier = '' # A unique identification for the text. # epub_uid = '' # A tuple containing the cover image and cover page html template filenames. # epub_cover = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. # epub_pre_files = [] # HTML files shat should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. # epub_post_files = [] # A list of files that should not be packed into the epub file. # epub_exclude_files = [] # The depth of the table of contents in toc.ncx. # epub_tocdepth = 3 # Allow duplicate toc entries. # epub_tocdup = True autoclass_content = "both" autodoc_member_order = "alphabetical" autodoc_default_options = {"members": True} urwid-urwid-69907e4/docs/examples/000077500000000000000000000000001526073734300170455ustar00rootroot00000000000000urwid-urwid-69907e4/docs/examples/bigtext.py000077700000000000000000000000001526073734300253442../../examples/bigtext.pyustar00rootroot00000000000000urwid-urwid-69907e4/docs/examples/bigtext.py.xdotool000066400000000000000000000002371526073734300225560ustar00rootroot00000000000000windowsize --usehints $TERMWINDOWID 39 21 key --window $TERMWINDOWID Down Down Down Down Down Down Down Down Return key --window $TERMWINDOWID Up Up Up Return urwid-urwid-69907e4/docs/examples/bigtext1.png000066400000000000000000000175541526073734300213160ustar00rootroot00000000000000‰PNG  IHDRp é]¿ cHRMz&€„ú€èu0ê`:˜pœºQ<PLTEÍÿÿÿåååîÿÿÿxÒ`äbKGDøoéÇ pHYs‡‡�åñeÇIDATxÚí� –«,°…sfнî À \üGàÍüÇt[¥Š*%&vzï³N'Ѝ|áQ@‰�Aпïå·rø%ÿþþz7�õüùS¾Å"8‘ nÜ?ªÝ}½Ü|ß_ó·#´€‹‡ïgΘ¯'v\1íã×ÊáBw·�°zăàŽú%àæ\w0"À�÷óí§Ššïöçû?ª°þ}•À-Ãé¨9`üóý-*ßD†O›Ó=�ÿ¾Æ{ù§Xºæäðù~S¥N÷Aû³ó^ n¼…%8]ç/Áqø¿´ýÓñU§Ã©R?fp|9â§ã‹ËÍÈh›âþü¼W‚ûù3eAf¨ŽW­ê?Ê9¼ýñ,#ò tø¿ùrc>ù¢sÍàŒô�YŽÃçûåâû ýÙy·ÀÙ‡‚–÷Pà8ÿÕÀÑIuF”TáôãTÁéSpž�ûzÎ[M¿Ì±û¹é:.^ëë‘1ü·.… æ± ~¥Ý)áyxîK�)/âS]&·óý÷Ç›à(ŸÖÀ¥ðïï+Àeç]K?}±Õx#cmZ#Ž_—ú0_EpYxüÉëà²Öe/¸ü¼"2gµ1GvƒûÚ®’ã²ðïï�¢j—Ÿ÷à¾þ�÷ÅGÖë8ã¯Õ,þ.p_p…ô·7^àŽƒû'MóGÁÉÀ•Ì‘ø TÍ>Û°™9"Àå‰ápÑDeæƒ8Ò8�Äå�¬€S­j2PÓöã[ü™Ýž‡ÏvÉŽSçSà¾p_\vÞ-pG[Õ�f7•rÞ墜²ç;þþÖ9gÙåásŸH‚K�àt¡EtÕ5þ–¢U=RT�ŽÇ¥>t \ÞÉ—÷©ÂEJ¾ÿ((K·>Uª²Žã,ú]ŸÕ$².ýRv\vÞ-p‹Û|…Õ Úºûµð–÷ñœE_m‡ÍàßÇõj9·õ8®ñ}\¯Æ•ªÜ;*w‚ ‚ [éwu\n$€3J�SB‡pFœQ·%í¯ö�Fî�NÅGµ‡÷ëÆ¹^¤…?Û#ÿ˜}Í LÀãœ1>Àãœ1>Àãœ1>Àãœ1>Àãœ1>Àãœ1>ÀãÿåàSzeŽÛÒ¯Íqw >Àãœ1>Àãœ1>Àãœ1>Àãœ1>Àãœ1>Àãœ1¾ÙÍk-ÞÞ㎺a] Îìæp×øÇYo¨Y®w$ýgL?ÀÓpÆôœ1ýgL?ÀÓpÆôœ1ýgL?ÀÓpÆôœ1ýgL?ÀÓÿ8£~“GæÕÇßvÎàŒ 8cΘ€3&àŒ 8cΘ€3&àŒ 8cΘ€3&àsÁqsZsÏ’aGÎ/�/ÄW²œ¿tŸÖø×Üo—uÛ$€3 àŒ8£Î(€3 àŒ8£Î(€3 àŒ8£Î(€3 àŒú$p/Õ'�CŽ3 àŒ8£Î(€3 àŒ8£Î(€3 àŒ8£Î(€3 àŒ8£Î(€3 àŒ8£Î(€3 àŒ8£Î(€3 àŒ8£Î(€3 àŒz-¸ç/Ýð»ïƒôkÀý7üß»ïƒpFœQgÔ¸Á-¿•Ã/ÐàœëÄöóÇ}ÀM©W·(î}ùM&H�÷�juc .¾œ9cþZpÅ´�_+‡W=Fàø+À)p?ß~ª¨ùn¾Ta ®+�[†ÓQsÀøÇ¹X÷I:|ÚìÆÈã¿n¼—!N±¦ão î'‘Ýœ®ó—à8|HÛ]äÙÀépjmÆ�_ŽøMá·÷ó5fÊ‚ÌP�S­ê@9‡·»˜“²ŒÈ'ÐáÃ|¹1¿wt®ùôNÇß\¢¥Á=8Î5pñ[–%yN?NÜôÿÖà( Ý3c8¬€Ká‚y,ƒ]ÚÍZ„çà:u&:þÀñæ8ʧ5p)ܹ÷ó}¬¥K`†UpÜ ]\>·+àæã?\·\%ÇeáÎmÕß®N�ëøÈz'Âcüõ¢zs;nNØxÿwÜÀ¦{ÅÉÀ•Ì‘ø TÍ>½™9"ÀÅão.Ÿ\ «V• Z±ýtâ8�=Kás·¬hÇÉóÝ�fà¸Ï»\”Sá¢Ë¥sβË%Âç¾–—:ÁéB7÷L÷Y—wòãÈÂE'¿{Æ~h÷T–n)|ªTeÇYÔ ð7×@æ‘£‚*C\ÿÍWÕß×m„Ñß×rnëù—ÀåuÿIýp¯ÑóÀÕíܼdÀpFœQgÀpFœQgÀpFœQgÀpFœQgÀpFœQgÀpFœQgÀpFœQgÀpFœQgÀpFœQgTׇwß˯Ò®ï{w;p~rο×=I=&nã î¾Iß¿âÆêà^sý-=~î#8nî½×ßÒÃMå4ÜA=ÀÒ¸ø­~fGUËÏ Î­U5Ó§¨{æF…�óMëò„'ž7Þ_º~Ûë縞ÒO76Ýo÷Sxºñì¸Þ\O­~áúñÎß ®wóOJ·âÇû ¼M�%iÜ&ÒÓqý|lkpã—ùw™ïO^ßß\H·àûxgâæUJä>Ï%»¥æœ%ÁþA5Ù‚©·çE™³bÜÝôþ]b Ž®wp^üÔ.moƒkYÅ9u_epM¯wœ*#.m/ÀQ-Mg¸¨q¨�k|½óàú^UÚÞ—ì–×€k{½àB.¬Ôq*‰M«¹àZW«ÇÀ©žÃIp-«é=àÞ9¢£ûª²¸J@\|E’òí«ÀÕÌ‘t½÷´ªjt„ M]ÇùÀÁчlD.*ªpA_ïMàäxµª>kU}쳪Îl©õmwc….ç°Ôivïç²ëÇî´“Íþ¼í¨ó 0ÞX¡“Oˆ<ÿpî½à ƒÎ(€3 àŒ8£Î(€3ê¶àÞ½°Þ¯]xïÝ`àî%€»œ§ùé ÷¦žþ¥�ëñæâKo¢ô+­Üráð]àj3¢o—�P›· npù %Z,ø<+• øâ°MÚúà:[eûpe@¯——ÍŶ~GNEc,yL¾ pkðÜúö‡�Ë‘”“ÿ>p¾þ3NŒˆgCÓ½òSà.™Q?Ž_ð¢¶ó#Ó;tÚ‚Ó.a Žö÷ù$IšÄi™K©«€›‰%pnQùò­M—�¨8:†'§Ó4áLòzp:'Ñ+ûžê=bi[Ø=Óû^/¼²‚ n^sdšã\€£#ß®[¾™NÈYÑÑ[°™#œïóÉù ¸ AñÜÝÝÁ üþ±‹ÀñLoNøÉ•Á5U;p\4Å „»çY=¸¨ùÑ;™øÀI?9îšg‡Z‚£—ñuzû4¸äÖ=ež¾\T¥ŸÜoÇ͆Ún .TÀ…•:®½ÚƒÔ;«Ïƒ#O¤dàÞ\1½ \¢Å8ú ¸¾bŽÈãš�;ÐåÚUÇųòvspä'Ü�ä~.f?¹›‚ëžüfÍé³kÔªzÑ"°52º½êµªýÎñþ´£5¸ý£#òµ¦98þ¸¿êšØq¢:KϺipä@÷s'Ÿz÷õU�‹oi•ïsm1Ä~ëÉšµàw à>Ü®Y.€[‚»³àîw€ëß,€8€8€»Ÿv�óáÁ§¿-ÁÝÚ?N¦ßWGã%<›ŸH9õ1-I³OÁ­‚kã—ì müãVÀñß‹e•ÒLØyp+«¼4§Ç-‡6Á8Y¸yø—‚çÅ”8ý9Múø~—ÖÁµróÊÀumüãVÁ½b—<ª·ž2çvI}¸eÑl1Y³ÎO9lrË©ƒ#LAÎöÏÿ 8QÖÕf.¬.�× [7�ýãªåH‚ ±ÌFkÎ9Éd/¸\¯7´uóª§ËsÊ Ü¸'Èu”sp‹3¬i}«–þqºqx8¡ÁùÜ”Ë{9¸¼… Vf\;ÿ8mŽ<¯­ã\ë8ÏEµgŸ°“àRÏö5àZûÇUÁQ Rãèè�9¯Xä£$­À5róŠ›­ýãêà¸ïêR–òó¶.«5pyŽÔÛâ¹€W€kî×ï�ßÏF€+Áeœ+àîíæu=¸àÖöþipë]Óu«d Ü­ýãZ€;£5pw&¤àààà®9¸»÷�‚PR|Üö1q¾—ÃKN®lŸŽ ÎíÆ•Î¿n½¯z6[§Ö•{¨''9 ¨˜B.8‘b±Ÿ�?�×N€jºî&·¿z¾¸é½ ¾�Ó < 8Oky Ž̽×ùÕ úsw†›¯³n}Ç!pý)ÇÂy}‚àJ»L⯚fª"�8‹Å€†øýåªàŒ~sepª9àfPœibë«ëº%8ùò]ÚL%ý8‘n6ÆTÇ :8‘úáI“8OrÙÌÏwf]¹y©fà¢}2ίú¹rŠušãéBóåoRòCôy ÕÜUÌq'Áñür¬õ£�GuœÛwf]¹8'[WQ7-ZO ®’½êuÜipO Nç8:ŸëÊÀ KkœÏÁ•;«µVµ8Ú™Õqsµ·Ü`rÿZv¹´µšRHÛa œïËäÊv\\‰ämÀIÓÞeàÕiep ¸ÖsX·Ñs�pfRÚ‰6ÜvGñžë¸À­6_}Jd\-Žš864tñ]p–}ŒÂ·­¢ºÝåJËtÉ@j+‡ œ:_wn]9=—ú@i\N¥�òH/Á9®Ä| \¹o€Û¡œB­'÷9w¹TQ¥Î«È ¢�{Ôo.�çàDÍ$íážk÷Ôs’ï'ìÕ Hò£JËÍ})é±Ùp]¹+çv�¯€ûýn^6€3‚ógÁÝ}–ë­ZwgÀÀÀÀÀ5÷[z�F—<§) Š„)9ž?SxŸf¹Ä‚û4¸§f€œKþ&qÊf ܵþq.›ØwGýãĸ�OÃk}/@ÁÑö ¸ÀÏn.ÁÅ›Á µ½Óé&¯VáAè²^?û°U&²DxîíDNLñÒûæè„§–A;ëGcxµímp\vœšR˜`zÞ^€‹ãékàt¼ \üowÚ?®{ê9†|{\tåZ‚ó Kpý*8�Sƒûþqý¼ð™‚¤<½|\¿Žª¹¸/ôaÚ9{ñP`pîMþq}\ضÁeŽœÁ»Cànï7Õó Ü¢10�ËR€éøÇéxÏcu\`‹ÃÅ‘ù‘ƒ#3C˜ÂLY‚s¸Þû¹üºàÎûÇ1¸d†ÇFÇApιUpº¨ÎÖp3pƒ¬ -þqÙlâóỷ Úpûë¸\­¨úkÁ5ð�sN~[9(�“MáÞÆ!¬�Ë[U7¹±7wÖ?Ž2Ù‡‡V’àœÌAMÀùâñÍÀ�ô�KžÇ3Ü¥à„×ë•àhçpÐÍ‹3Ç;�áDã0§Ò3ˆ==‡ p¡ Ž—Ž\ï9\í—¬ÞÌx?¸)«Å§?¸}Õ¸ Àùà.ö�ã ã\-À\ ¿ÁõÑ‘28/ŽwnÜæèȵþq +€í×÷äD8zÜGC®:×S–Subª#Ì D¼à8'²Ù]÷ÿ8�/»\ð"'G úêp/ÈF€�0|§:³n'yŸ\¼ Ç;n—7ÖeZww7¯)ßÜqp¢pÞ ÜÝg¹Â]ÁÝY·žW½³àààà>\fW—ÍìV=‡Á¥.|‰» ÏlàR‡+¤á#‘Т‡ÐéÅãhŠïçá(útt'‡ÎåÓ…n'¸µ¾ê��Ø 0ƒGûwƒ,½Ö Î'bÅmΧ¹?1äžB8Ÿþƃ¯'¶œðdVpC\Ÿ�•‰16—¬æ"È1DFr<ЦnY€ ê©üpÛn^pùqÁ-�¨�“nWªH†:8*²snÓA"Ke5~ÓË“¼œ˜…°€sÇÁõ;À¥úÍ+Ÿ¹8šÄØ·íæõp…÷³Jpù‘—ç}¸ ÿ8>ï\\;YÞe=·ι]àRÁdpžJ¼7í S�連Œ¦‰œr­H•èp<³7;ÓŒAt©zZ�šsVœ\ ¦Iç6Áñ�®sÖøv:ºÌ¼N½ÓMº.xúàul58åP(ÁM—4ר?nŽÏ?Û޳N\\ê1�ãCr™Ð�Áµ­jcÿ8]TÅñ œÛ×—Á-V,$`Nóy�ר?n¿¿uÜz$Ÿ¸dŽÔÀ©Ê‹ _QÒ"mÀ½Ç?NÔ�élä¬CY4�£\'[ÕÌ~`;-¤6#6Ù¢ðþµàÚúÇ)øéÄùuþb—+&Óq"s­Žs*ƒËšã‹GGÆ:ª¡œîr9qþ¸B'Ÿ’D &sMç¬T|ÅÃn/ר?NwòÕùUUðPGæ¤bä³ànêæ5S¼œWàš�¸%*²~>÷áÉš{úǹÆà \B®­î–Š5dKpµîr}¸ç îz}¸(¸@XÁ¹›êÝ`àî%€8€¸‹Á飆1VpÛ·>¸l²àö��Qà^íwƒËAÀ] Nzü–gnÜÆ:o··k�7€[‚s{Öy¸8šHV×y¸¸AÍà¿´�øÅà:•¦¿î8ä8¸au¹2€Ëµs�7€[€cÓt4ZÕ�àv¬óp Å£¨¯º3Àí\ç àn&€8€8€8€8€8€»ƒÞ àþ"¸]OÖdϯÜs~xkˆs§ÏÅ2hñ¸mp.Ë¿®E~¾=¸8 ØÙÁ�Gv+ÛŸn.›éçÅó«Ë§SËNùÑ-¶?\÷t'À]ävgpü ó�–!¸ýà†øÈ}ÜÀ+ÉmRîG×hBçÆàÒú¼Š@œ£GíÕvŽfÃþ¸§S«,Àñº!ù6EÒ~tyŽü`p‰`ç¢|›"9åGÇÛ·.ó£kTÏýpEu8éÕÔÈÑú€N‚ë´-×Mútpr}¨àÒ:8À'Á N¬Þø'Àu܇Ð÷¾\îGÇÛ Žqtb3¸Ì�nøøÆ�×ÔâÊÝ NúÑ oŽˆVðÉ…¬nŽÖ-Àå~tÒƒó“Á‰`#¸Ì�NØ…Ÿ î¦Ë Üç‚»çúq¿Ü�pp¿Üÿ(ùZ“àZ²1IEND®B`‚urwid-urwid-69907e4/docs/examples/bigtext2.png000066400000000000000000000201641526073734300213060ustar00rootroot00000000000000‰PNG  IHDRp é]¿ cHRMz&€„ú€èu0ê`:˜pœºQ<PLTEÍÿÿÿîåååÍÿÿÿ*,Ö6bKGDaf¸} pHYs‡‡�åñeÌIDATxÚí� ’«*†3;H¾ª³€kqWW€l`nÕÙÿV¾Q覵Åh2ýž:“ bä ? tðvS©T*•jƒ¾ówùøC>üñ¸ŸM ~ƒ?ò·˜G2ÄÀ�ÇGµ»¯×€ó÷}÷ï¶| \8}%8qÁ|-8’±½à²yßN/º:¸åŒ•n·�Л€ó¥ncB·ÜÏ»Ÿ&ÊßíÏû/h°¾÷¸y<œå#Æ?�i|#?§{ÿÝÇ{ù §C*Þrb¼¿ßبÃ}ÀñäºG‚oaŽ·ùspÿÃ÷��{�‡Fy|ñàðã€OO>Î#ƒ0D„ãéu�÷ó6(‚È��ÏzÕ/(9¾‡o<)ˆxÿå?n,'w¸–ÿ,H7Zä0Þß/ÞÞO®»NÞ9|ZÜ��ÃòWå‘~ ‹‡/§Ž_‚€ÃøÜý–¹n1ÿ´Ä®çÆÛ¸ðY÷[Âð«.Ææ¡Þãá˜ñ4>wgWŠç“ôЖÑpzüzà0¸Êi \Œ<Ž—\·–x#­ªáFÆÖ4†œ_Ç0÷,¸$>|åepIï²\z]’Z²Ú˜#«ÁÝ×€+”¸$þñX¨ªBpéu_îþµ ÜÏ,·q$>¤¯WÕ$ý*p÷¸LþÛ€?GÁm÷EMó[ÆIÀåÌ‘ð ÍïmØÄ!àÒÌ`<é¢sAlé¶‚ FâüF*àX¯ Ô¾=È�ØíiüÃØ9;Ž]��»ß2àî\rÝ%p[{ÕÎ&7‚�r:ä‚’2‹Ç;~Y<ÉÉýö¥€Yº¹ø©Q¥mÑG|ҒжôÎì¸äºKàf·ù mj´t÷µø–÷ñ)8‰îm§ÅàßÇñj¹¶uÛ®ñ}¯Æ�ªÜ�»J¥R©T*Õ¥ô^— IÁ Åæ)U›¥à„RpB)¸%qµ/˜¹:ÿeüÕnÜÛÍs½H3¶[úâ}ÍT‰p¡ô‘YîºiSWÔÜ_m^âTÍüÕfà´‰ËjæÏ–ÖQíòšù³Í·­®¿D3·¬y¯ Í\N+À½ÙBï‹4óg+‚S|L3¶ÄùÂw Ž+ñW£�þdÈŸx?ûV/&î¯öHG÷h�(8•J¥R©Tª ©S‰¤à„RpB)8¡œP N('”‚JÁ ¥à„RpB)8¡œP N('”‚JÁ ¥à„RpB)8¡œP N('”‚JÁ ¥à„RpB)8¡œP N('”‚JÁ õ›Áý󿉜P N('”‚JÁ ¥à„RpB)8¡œP N('”‚JÁ ¥à„RpB)8¡œP N('”‚JÁ ¥à„RpB)8¡œP N('”‚JÁ ¥à„úÍàvé7ƒÓ'”‚JÁ ¥à„RpB)8¡œP N('”‚JÁ ¥à„RpB5÷ÏcAôƒJÇÓë´¸þšó·^?nSz×\r¡T¥øÙ ¬ÿ²k N˜'Ì€‚f@Á 3 à„Pp (8aœ0 N˜'Ì€‚f@Á 3ð¹à¶¸9ÕܳhÜ–ëÓó3é™$×Ïݧ4½‚kîÝ%�ÁIÁ ¥à„RpB)8¡œP N('”‚JÁ ¥à„RpB)8¡œP N¨O÷R}8-qB)8¡œP N('”‚JÁ ¥à„RpB)8¡œP N('”‚JÁ ¥à„RpB)8¡œP N('”‚JÁ ¥à„RpB)8¡œP N('”‚êµàž*‘œP N('”‚ª®wówùøÔ;熓 ¬ÑmÊ}þ³àH†¸ñø¨V7ö‘àÂé+Á‰ æÛ‚Ëæ}|[8½@HÁáÛm„¾Up ÜÏ»Ÿ&ÊßíÏû¬Þ 9póx8ËGŒœ mCÀâ§à0&ÿ ã½ôátHÕ°Ë9ÜO&‡98ÞæÏÁa|ÃCà9dÀñxèmÆ?ø]²eà~Þ†ÂE2p¬Wí¡ä`x%))ˆxßû�Ëû×òßa8ÿZbà"-îÉÀaù+� ï’‚HɳxørŠà6µ€ƒ< Ï„a_ã óP‡x5‹OÁ ìJñükIƒ à œ–ÀÅxç>ÜÏû±•Î�‰ªà°7è‡,¸$Þ÷pî’½ƒܰ\¡Ä%ñÎ-TÕ÷7ô»À xf¹�#ñ!}½ª^Opãý+¸íàz4Ý æH.gŽ„o hŽÐxoô&æwASdŒO ®£Y¯ Ô~:òô¼Ì¤ñ~X–µãØõ.¦[œ1LÀa£œ¹ ¤ÌâÉ�‹—œù�‹Äû±ÁñƒÎæ”÷Œ÷™—òâHâÉ x†qèðd–n.~jTi‡EÔeÁ_CmkZ¶C„”“‚ª¸¡mf¸Æm÷ï×·m»¸ß)'ÔÍ©DRpB)8¡œP N('”‚JÁ ¥à„RpB)8¡œP N('”‚JÁ ¥à„RpB)8¡œP N('”‚JÁ ¥à„RpB)8¡œP N('”‚JÁ ¥à„RpB)8¡œP N¨®3gßË[i×u�»8;m{­{¢ºMÜÆ\}“¶{Å�•Á½æó—tû¹㬹 ¸s?I77ÕS£à6ê†ÀÜ&ý€ ï:c=;hZ~nÐ÷ÐÔL¯¤íñ� žg›¶åO¸n¸¿øùm?o38Áu�¸±éÖ0ÜMññÆ“óB|{pôú™Ïw~.¸Îù¯nÅŽ÷e0 �di éé¼ÎŸÛÜøÆ/þþèçÛK€3ñlîŒÜ<Ë =f±f·”/YœÁ/”“½ 8’ r{–T_Ãá¦÷�àà#æààó®Î’¯ÚÅð2¸–Mœc÷•×ôóöƒcuÄÅð ´Òp…ƒ:‡¸ÆŸ·\×ñª áEpÑny ¸¶Ÿ×œIÀ™JDzش™[®u³º 9ìײ™^îÌ>V¥Õ•ÂêK²”†�W2Gâç�Ó«²Ù44yg=84€�ƒÚ‰TU3à ÿ¼“ÀÑù8èUmÒ«Ú0feƒÙ\ïÛîÆ2C.,aqÐìÎç’ëÆá´£Ý¾;èÄ,H̃†7–ä"‹_œ;œJ '”‚JÁ ¥à„RpB)8¡. îì�õÞvã½³Á(8w-)¸ÀYXŸ6ôhé:¸o.<ô&ˆ‡Pqßà—>ùáæØÌàµÀ% X°‡óÁõ.} D‹=šý´Rž€ÍNÛÄÐ{€ëáj…ðàò€^.­›³0FNAc*zNVp5x®þ0p)’|öÏg;â?ãÈŒx25Ý1?5î�õ àð/,œžŸ¡Ów�0spp¼KIâ"N˘Ë]œ'Á¹YãßÓ§6Ž bàà\œŽË„žäñàxI‚Gö=ÙsÄb˜œ8<ãó^g¼’‚3n2n^sdZãœ�ƒ3O7ÌŸLÇNÄ¢èà)X�Ì‘Îvéâ|œá píîêàz|þØAàp¥7Güäòàšª8¬šäÂÃs¯nXÕìè� |2ਟwÌo‡Z‚ƒ‡ñ <¼\tëž O—¯ªÔOîÝÀa·Á­Á™8SiãÚ«=¸ž=³z?8ðDŠîuÁeËÑià"-ÄÑÀus„ž× ܆!ת6.\ÃÍÁ�qC¢Ç ¸p–ñ~r7<ñÉšÓëШWµ¤G@kĺµ²ãÎáñx 5¸õ³#ô±¦)8üp¼êšØq¤9‹¿uãàÀ�.ÇA>Œî«6žÒJŸçÚbŠýÒ‹5µà³¥à>ܪU.7we)8§àÞ\w²œ‚Sp NÁ]O«ÀÙ…xcãß–à.íGóïƒË€ƒù\Í�¤{™¶¤Y'ãªàÚøÇ¥{}ÿ¸ 8|c;²­R\ Û®²ËKCp|Þ²oÓÔÀ¹ÀÂùé_ˆö›)püuZô±Ý*ÕÁµróJÀ müãªà ß±‹žUgM…s¹¦¾ܼj¶X¬©ƒ³S ›ÜrÊà“!¿6gUY®‘\?œnª™¤ª.€‹á%p°Ì±Ü‚\×Ú?®΄ 8ß98ÂÀD’mÁµò� ýGkÿ¸98pp,F»d–Œìgµ¶ «Áñ"¸Ò?:ÞÆþq%pXe§6Ž€1 àbçð"pC}]µ˜ÕG™# ¸.p¤@¹\2Í‘%pÕ�Ìš�s1º©\ô£Æ…MSl8ì\ \0€CU¯±{1¸Æþq5pØmNelÚ#È{!Z 8ø^ ŽØ»mýãJàɸõë÷aµßüp¤®³`ÎT·Îk­›ÆþqÅzDÁ™Pgƒ5çe²\ªWƒëÛºy•óe1çnÊ)¸ÙjªocÕÒ?Žw/ 88›šréÈ!—öÐË,€kçÇÍ‘ç±m‚3cg±ªvè¶\Ù¾\kÿ¸"8èLìü`Î2é,I+p�ܼB°µ\Ž]],RÖ‡y]-�KK$“ß¼\sÿ¸n…ìB|2\ˆÎ㬀»¶›×ñàŒ«ýÕàêCÓºURwiÿ¸àö¨îÊÒi§àœ‚Sp NÁ)8×ÜÕGgA +6„mÈœíèô’£;ÛÇóŒs«qÅë×ÀÕǪ{‹e¿k_¹ûå$rÈÀGrLŽãùÊÚp}©B—Ýäªé‹×+�›ž b»°œ€Ë€~YËRp˜a½úG+ð×ÕÎN\}>nšZÛnص¯Ü�dØ£±±òâBGg‰Âz+^c¸n—c¡ßŸ`¸Ü¡ ‹87V²bóC^�-�3)¸â ’ɼ[W]s(çñUà ãkÁ‘ReÀë‹V×|sÆa“÷ À9ú›û©© ‹ ¸‰×ð$Ç“‹ýæV€síV€ãN:]ÜñÀ§‰¨wõuÕ¸R€„U,Ô‡×à/W'ô›ËƒcÝ)çAa¡ ½/oëæàèÃw!kúp$ßh<Œ¹´w$÷ýqžà²™^oϾr#8ðRMÀûd\_µ¾q mšÃåBÊ”<—gØžÁ´îó�롇SéR¢pÕkÞ íÙW. .n�÷öÂ:g‰gfθ,8“1î Üì®Pj¨ÿÛ6p½Èo. .Z#–ƒ8ôüؾ�OâË@êºÂÁýn^ 4ñ±âM—)‚Û·¯\®�3¤U'ÖJ—v1û&ø¼šbéÊ–¸�àp}9´úÁ΃6Î-‚Û³¯\œ£½+i›f½'W(^å6n7¸'ÇK\¢nǾrpÄÒZgSpùÁj©Wm&mœoöV€ëEî_ó!·Vc!ljàl—'—·ãº­àr$/Žšö.� M˃›À¥‘CÜÂÈ�Âñ¤¸9l¸å6Ò=7¶q-Zn¾Ú˜É°Z˜5qhhðê;ãLÇ™wKUuyÈ·é¢#€ØWö 8v½aß¾r|>.Ž�â¼Ë!”‘Ž‚s؈Ù¸| ^·0;%zOs:r±ª ƒWR@Éw«ß\œNÁ‘–‰ÚöîqäD'†_ŽØ«�èÿ�–óc)ê±Ùp_¹#×VÍWÀ½¿›—LFÁ ÁٽாÊuªjà®,§àœ‚Sp NÁ)8§àšƒ{—‘ƒ�Ù%‹yŠ“"fÊŽÅ×ßÅU.²á>Lî± 碿IX²©�;Ö?Î% ûn«™²qz­ë¨,8WÀüíæ\¸°\_ªÐ+�nÒacq#„.™àµÞ‡­°�EâSo'CJbLŸ7ܵ Ú^ÿ8˜Ã+…—ÁaÝqlIa‚i1<æÓkàxº\ø/·Û?nxò5†4¼.¸rÍÁÙ„9¸® Ž—ÔÍàÞÀ?®óŸ1HÌÓËfÀuKà ™ ÇLg¦ƒÞ‹"€s'ùÇuYpfœY—8rë6�»¼ÜÔÎ3p³Î@.uHiŽä»oàÇÓ=·µq-2æG Ì bn3eÎ%à:k}ýu-Àí÷�CpÑ Ù �Ž�àœsUp¼ªzk¸¸ž¶„ÿ¸d5ñùÜfŽ„ m¸õmœWªªöXp ü㜣/ÛvŠàhW¸¶s05pi¯Šà&7öàöúÇAûpÓÎAœ£%¨ 8›=¿¸�þqÑ`{�;ñz=ì7ºya�Ãt é|.-‚X3rXgòàpëÈúÈáhÿ¸hõ&®ÀëÁME-üú#‚[1VÍ€3 œÝî`ÿ8,`!ÝÆ½Ñ¢LÀð¬ÏŽäÁYr¾sp‹³#ÇúǤÏì¶ \×�áèq ¹â|\E޵‰±�t8YàA‘tÆaID³» îþq$~ìZpÆ’’d€êŠ3À!—™vÄð�ÚÌ8?Orž\øL·Ü*o¬ÃTwu7¯©Ü(¸íàH弸«¯r™«‚»².½®ze)8§àœ‚Sp NÁ}0¸Ä®Î›Ù­F½‹CøwŸDÈÀÅ—‰ÓG$£&d„O¡Ãƒ ÆÙÛùé(xupG§Îé¯ ÝJpµ±jŸÌØ0=ÇWƒOÌ=Ö ÎFbÙ0gãÚ™r�¡œ�ÃÉÇ�#!Ç'<‘ƒ\_×%sedŽÍÅ ³µp ¡‰N´ñ�[fà ûU~ܲ›W\zÞFpó3Jà¨Û«’¦ ª¬/m|aÂPd±®†w|{’W€#«pn;¸n¸Ø¾Yæ3W‹Ëà–ݼ^.ó|V .]'-‚ ŸÖGÃ�ÑñšÊw`Bptå§ ¸¤ K¬™9¸ÏÇůžõ2PÎ`5h+8».¬)«ØúÛ28ãØ±þqOôµ�…Ñuà°ë ‚ãÝIì†Ãª™{n—Ép\‡ÎJŽ5qIÅeålM·äà"²a~Ú¼s€“âôÊ[ÿÁ¿¹ ¸\ÏJv§MÁYÃÁE3ƒÕÔ£Á³!ä0Ë_ Órûjn좪v[µµ„9¸¸h]g»ìë5ÀQ×ÂÜÜ`«Œ5¸KÀAF+àFLzV´Ì‹À‘ º¸¤�›ƒ›ÐÂVx¹�càbÎéÖAYp0(càp`ðp½[÷”�›ìÆñÏZpiÖp~‘„àŒcàl—v ¸xjÜð|ÖÀ¥`H‰ë“^º Îl‡ö¸/7®�^ÿ5ptä�´R%p¡hâˆ!Ž 8¸Y÷*—²Ü0ãBÀÍ.X7½®cU´m[�[9r€êº\Æ™u«ÔYÇl8ÚÖ‘ñ|ÑÀ qÒ ÝêRp¼ l .øÁ_Þñ ÎÀ,Eˆ¢¿¸©ƒ v÷k#6ޏ0X‡>&Å„{Á­›!øI /yO8ˆ^Õì!ÁL‡\øµÅãup8Ü�ªã�:†#8cZštn6OÒøÁ=qʼdãŽq ƒüèèÙÓ:ž€c™°]Kíwu7¯(Ó–Qü~*¸u{ Àå‚+À]Ý?îTÕÀ]Y§�s :Œ‚û4pKÏ£àܵ¤àÜËÁ5hýpÉ2Ç1=…ŽæÞHÉS \õ›i5rèÝ‹ýãºt µ μ Ü)þq)÷1a&åô“$–±epÉ´®[h Z�#¡}þq)â ¸®¢n4O#wŽÜÄ}¨Äp<'Ÿn—›×‚»†Ü28��nnÖ°°~ �ÃùØe¸øÛrC$‚£5y¶�wKÜ Îµõ�£-e~•27fseËD›°gÚL«\8ÎÁ‘µ‡1:>-3^¿ î$ÿ¸twˆBwLÀuà™Šî\ðð2Ã×Oã BX4Éš€£¾À&ì­„ç‡U²MàHó�ÃUÁ°Ô³ï!WU) ˸stÎÎc�”ë2à,sP”ƒ#fBKÿ¸i‡Ã�”ȼÛy\ÌfÃñ;žÇ<3gàÀíÿÕà–üãÄõd(Rg ÍjŽyf¶÷ŒléG¼Ÿû°8ãÀ9‡ýî•ÀõnÜ&ÿ¸X�£ÝXíXKp‡VÕç1þqÄûi]‰£ ŠU5‚¸¸á p¤�{18[íU%àr@ZûÇ�Ó&pnÁ¡àV˜#y;Î2èep'ùÇaoBì·j¯ê`ò¹Èì8»ʦ;\cÿ8_{î*ïU±ðù�Ïm C,¬´¤ŽC1ËÁ¡gÝÜâ�ë$ÿ8ˆ‡±jL_罡ù–€3I›•u5 Î, òÏñ� ÇÑ?®Ïr۹ʕõxÛ¶üS÷Fn^ îàl‰Pçvpê}Å ›kîÊÚÎä×ýŒÛRS#¸6Rp NÁ)8w=)8÷AàL˜±$°ÜÕýãŽ,M œÝî ÿ¸>7(·ièÉzRà×ôGpGùÇ]\ÇÀô{Õ9¸ô<'·ÅÍëÀ�â·ñW³ðƒ|ðÛ²°ÀoqÊO i¢ÇRÓå:…×€sMýã誮M ¸bˆ¯7¾8;×ÀjÀüGpaw/²zÕÜ9þqì ¡×Oþߨ:*”4ÿ@®°ùìeÀÁE�¯ùCк£À 9pY©ªnñ�#_ÈàÝ+ž3hEp–esœs¸W‰ )ì ÀE;£¥^—ULƒë¸Cx†4u}ˆ¿ì°Yp&>ÓBÝ´�.î­„²ôº'�[ð�Ãëúê:ÐúNÛ¹8çV�‹ÁY¨ñ ÜtÈL-¦k š&þq̵"6¢ÀábÌ œw¦_ Ru°5–¬<8ºL“6Î-‚ÛâG\+|Ñëñvø˜1ºŽ=Ó�º.XxÁ}l98æPHÁMANh®±Ü ^ß[‹N\ØêÑ€ãCt™à�Á±½jcÿ8^UÉùœ[×åÁÍv,`Žóy�ר?®Ÿß:†nÑ'.š#%p¬ñ×´…ð‘6àÎñ�#mg¼8ë@�à ÔÑ^5±ÐN3±Ï�E²)¼}-¸¶þqÌ~:rýž]?;ä ÙtX„À\Ë�Ã’Šà’îøàÙ‘±�jèLJ\Ž\.3ȇ,A†Á\ã%+V_òc·—ƒkìÇùìú¬)¸1�#ñ R6ñ^puóò�g¸¦SçnΪ¬õ×Þ¼XsMÿ8×\†‹IÀå“•Á]R¡…l ®ƒC®÷ôàŽ×g� R)8wQ� FÁ)¸kIÁ)8§àÇÏêÇTƒ‚[¾õÞ%‹ n¸ñîeÐÞ\ JÁ)¸cÁQ�ßüj¢‚«‚[ØçMÁÍÁ­ÚçMÁÍÁ¹5û¼)¸8XHî«û¼)¸¸ž­à¿´ƒxcp€ŠË_ n8-q2p}u»2—jå>o n]LãÙÚ«®·bŸ77S8 ƪ+S)¸•û¼)¸‹IÁ)8§àœ‚Spîo8 þ½˜.î_ÿòßÙ œ‚Sp ®5¸ï?ÜÊè�‡¿ ΃Aü3�yü/Qß¯àÆ³jáÏ÷ï˜Ëï=àÆ3ÿTŸnª›ÿ~Oàþû;¯«Ô"„‘üŸJøsÁýùÉ«\Zµ„W7!ù÷‡Ú»ÿŠ\ Ü÷¸¿5pßӉߡ͇0cìôôûƒó\þõD&;®Î‹àx¯  ¾ÑßîïØ þGpp�¿¡ˆ¥a‚ã›L‰ü`pã› 8,EiÀóãáK"†Ü8h×¾�_“êúàªê pX‰YøÓÁ}ï÷b¼ßîûÑÜãÁß à�à �û=à~†\jv\Üwà .ŒÈXU:ƒÊ¿Ü#ÎŽˆÁùêé-ßï�7GÂìHœÈ,ÏŽLÿ'ý™�CÃ7ŒU!üéàþÙ n²ÿ~㟿-—çg€ý}þ÷wÖ7œ+÷‰àV­r)¸9¸Qܵ¤àœ‚càÔÍk£Îv|[dzÁ(8w-)¸Owé竞 ¨®¶×yúŒ@¦§ààøjpñãóáO÷ä»-1p[ú àNy¾jöÌLÊ_Ž?´ñóÁ�ò|Uh¨†?œkú|Õxíø,›\¡¼0¸•Ïåê�kù|U¼v�û¬}¸!ÉÜÏW}>ãc¾ðÑ\ŸnÔ@(¶y¾*/º™ þZp ÏW¥„³|ppÑ4y¾ê3~°s<üaàz·nËóU ¨~àáGv.,ƒžÏ¸�ŒË¿~¸¡9¸"†ùõÞ\Hãç«~¸sž¯‡á. ›«}¸¶ÏWųqä�Ÿé¼2¸Sž¯Ê0ã˜ìÁ5~¾j¬Ñ<Õ7WS/ î¢� UpŸ îšÏW}pW–‚Sp î-Àýn:*�Ó.WëIEND®B`‚urwid-urwid-69907e4/docs/examples/bigtext3.png000066400000000000000000000202741526073734300213110ustar00rootroot00000000000000‰PNG  IHDRp é]¿ cHRMz&€„ú€èu0ê`:˜pœºQ<PLTEÍÿÿÿåååîÍÿÿÿ½€bKGDaf¸} pHYs‡‡�åñe IDATxÚí� ’ã(³…Ý;(Ç�Ùh¿³…7P1ÑûßÊ-Id’‰I)l«ªÎ™˜¶èÁW<H£Û ‚ ‚èÏ}ý-ÿ”›ßïï&PÀ¯ò�˜'2¤ÀMÇ'µ{®×€[žûcùvä†p!ùNpæ‚ùZp"cgÁeó>}-$/º:¸íŒ•O<î¡on)uO¸3྾}5QËÓ~}ÿC ÖŸûGÜ:žR-Ó?÷»h|#?çgžþû˜žåOHNgé–“ã—ç��:=O®ûLpÓ#¬Áé6 ŽãÿÄðGÈÇGœŽ§FyúXÀñ툟>_ÜnAFaŠÇÓë>Ü××Px¨2C•^õª¨äpø#üÅ“‚ÈÐñ–ÛMå䃮µÜ€ ÒM9Ž_ž—ˆŸƒŽ'×ÝgïþZÜM�ãòWGÕQÞPÅÓ§N_B€ãøÜÇ-sÝbþe‰ÝÏM·qá^·„០¸/˜‡:øÇŒ§ñ)¸u¥˜^œOm™ §Ç¯Žƒਜ–ÀÅøûýà’ëÖòO_¬U5<ÈÔšæÀˆô5pq ó‘—ć?y\Ò»ì—^Wä@–¬6æÈnp{ÀJ\¿oTU#¸ôº/÷ñç¸NYnãD|8¿^U“ów�û(€Ëä¿ ¸é>wÜišß2æH.gŽ„¿@Ñ‘ñ‹ ›˜#\šŽ]TbŽ0ˆ#�ÃQpÁH\?HœêU£�÷»ø$v{_ 윧®§À}Ü2à>4¸äº[àŽöª7ž1L„åtÈE%eÏO|¿ë’³r‰øeL$ÁÅAp¼Ñêt54¾ËÑ«©ªGçãâº.äËçTñ"'·?T ”¥›‹ŸUÙÆq½çÀ'-‰lK?”—\w Üê1_¡Cmƶž¾ßò9^"€³è£í”£\ãçx¾Z®mÝN€küÏWãFÕ î�;AAÐ¥ô½.À¥æ)¡Ã8£Î(€Û’öWûC3÷F§âߣŒ¿ÚM‚ûvó\/ÒÊŸí–~,¾fP"^(½g–»nhêŠZû«­K”ÑÊ_mM\V+¶´Ž¢sÈkå϶jÜŽºü­Ü²Ö½š¹œv€ûf ½/ÒÊŸ­ø”Vþl‰ò‡¿œVâ¯&; þÉÐ’ðãÝ�z1iµ{:ºgkà ‚ ‚.$™pFœQgÀpFœQgÀpFœQgÀpFœQgÀpFœQgÀpFœQgÀpFœQgÀpFœQgÀpFœQgÀpFœQgÀpFœQgÀpFœQgÀpFœQgÀpFœQgÀpFœQgÀpFœQgÔm€L û%ßïï~�wÉš€8›Î(€3 àŒ8£Î(€3 àŒ8£Î(€3 àŒ8£Î(€3 àŒ8£Î(€3 àŒ8£Î(€3 àŒ8£Î(€3 àŒ8£ÌàÞýàßU\âö’ßJ¿u�­óŽÆïMôy·Î8€8€8€8€8€8€8€8€¸]àîÚ›AkF·î4} ÜÞóvÏ[ÿb{KâÑó�†[Ÿpppppppp÷ËÁíͤu¸Ä¥Ú{ÞÙ뿺Ämåàààààààààžnoúû†~ ¸­tg3Ò T«ë–pppppppp÷ËÁíi1¸£%ïh ÚŠ¿d½îÖõŽ>ÀÀÀÀÀÀÀÀÀ\Kp?M{ ‚UgÀpFœQgÀpFœQgÀpFœQgÀu{@&œQgÀpFÕÀ�ný-ÿ�ιîMPöè6ç>ÿˆYp"C Üt|R«û‘àBò�àÌóÛ‚Ëæ}úZH^ püõ!€ã¯§À}}ûj¢–§ýú>Rƒ5º.nO©–ˆéçBÛ§¨ø9ØM'OÿuÓ³Œ!9�Õ°Ëy¸¯LvkpºÍ_ƒãø1†»À³Ë€ÓñÔÛL 8¾ñ»dÿªÀ}} …‡Š 3TàT¯:RÉápJRRù:~\n7•÷Ž®µÜ€þ†!ýµ¤ÀEZÜC�ãòW¾%Q’WñôÇ)‚{²©Ýå¡{$ Ç ¸/˜‡:ØÅìU| ®SWŠé¯%8n€£rZã�û྾O­tLÄPǽÁØeÁ%ñKïQç.Ù;Áu{ÀJ\ïÜFUý>àºñ¸ŽS–Û8ίWÕë)nz~€;ndÓ½`Ž$àræHø Í¿½‰9"À]ЙÁãS‚+�áhÕ«F5†NüF]fÒøeX–µãÔõ.¦[œ1LÀq£œ¹¨¤¬âÅ�K—œõ�KÄ/c- .‚ã�ÞÍ)îŸ3.ä;ƑċA~÷ãÐî¡,Ý\üܨÊ6Ž‹¨Ë‚¿†Ú,Ö´l‡.)'€3ª¸®mf¸Æm÷ï7¶m»¸ß)€3êæ “Î(€3 àŒ8£Î(€3 àŒ8£Î(€3 àŒ8£Î(€3 àŒ8£Î(€3 àŒ8£Î(€3 àŒ8£Î(€3 àŒ8£Î(€3 àŒ8£Î(€3 àŒ8£"¸¡÷³|+Mà†ap—ççMc¯õLR·™Ûô€»Ò¯x°2¸×ÜK·¯çè�ï/î½÷ßÒÍÍõ´¸ƒº10€;¤/páÛÐû…5-_¸ôÔÔÌŸ¢íY:Nç›¶åO¸nx¾xÿ¶÷; ÎGpåŸl~4s||ð$]ˆon ^?sÿðäï7¸åOJ�â§çê9L�ei é9ݰ¤m nú²ü]–ç“÷÷—×ÇGðCx2ñð*'ò˜çšÝRKÉ’àzþƒj²'r!Ï‹ ²Åp¸éó38ºÅÝï*à¼øS»Þײ‰sê¹òàšÞï<8UG\ ¯ÀQ+MWxRçP×ø~çÁ ƒ®ªÞí–×€k{¿àú\_iãT›6s;ÀµnV��S#‡“àZ6Ó{À½sFG�Ueu•€¸úŠ,¥ág�+™#ñ~ïéUÕ욺�ó 86€{G²yRUÍ€ëõýÞNÎÇQ¯ê“^Õ‡1«Ìæzßv–rq ‹ƒf÷Np.y°aN;Ùí/aG�˜×…yÐðÁ2ƒ|BäùçÞ 2àŒ8£Î(€3 àŒ8£. îÝë}Û�÷Þ àîZ¸'€ó´>ÝË£q¤ÿÔÁõôpá¥7A:ÄŠû¿ôÍ7§f¯.¡‚#¥Û7ºô…-öh^¦•ò|vÚ&†¾¸‘®V?\ÐëÀ¥usÖïÈ)h:K¦IÃWƒçêá.E’ÏþûÀùAøÏ81#žLMÊOM€{ÊŠúpü‚NSÆwè´§] ú58:>¤‹$q§e Ìå®n!Á¹Uã?Ê·6= œ@¥ÀQ^œŽË„ ÉçƒÓ%‰^Ù÷Pï‹a‘°{Ä÷½®^xe7e¼Ï¸yeÌ‘y�sŽR¾ \·~3�JÈEÑÑ[°™#œÒÅù¸^ƒâµ»«ƒùýcOÇ+½ 8á'—×TíÀqÕ/îguãªæ'ïdâ“'ýä¸çüv¨%8z_§Ã§ÁE·î¹ð ùª*ýä¾8î6T¸5¸¾®¯´qíÕܨÞY}y"E÷ºà²åèmà"-Æ1À sD¦kîÀ�kW®ÊáæàÈ?N¸!Éã\HÕ/~r×=øÍšógרWõ¢G`kd1t5jUÇ�ããñ@kpûgGäkMSpügàñªkbljæ,þÖMƒ#ºpœù4ºÒXµ¸ð–Vù>×Sì—^¬©Í¿[÷ÁíZå¸5¸+ àྸáÍ8€8€¸ëi8¿ßûøoKp—ö�“ùw„ÁeÀÑ| ¯æGRN}Ì[ÒìSïªàÚøÇ¥{cÿ¸ 8þâ±­R\ ;®²ËKCpzÞrlÓÔÀ¹ÀÂ-Ó¿½l¦$ÀéÏyÑÇ»T×ÊÍ+×µñ�«‚ëõŽ]2Uœï*œÛ5õàÖU³ÅbMœŸKØì–SG˜zñksU•�àùÇ�Ý»ÀÍ5STÕ p1¼Ž–9N‚Ûð� àZûÇ•Àõ!néœàB`"ɶàZùÇ…þ£µÜ4¸�£]²:Mìgµ· »Áé"¸Ó?Ž:ÞÆþq%p\eç6N€é7ÀÅÎáEàºúºêØ82«ŸeŽ$à†hÀ‰år§-àØÙWÝɬ8£›úÇ•ÁQ?Ú»°iЇ�«� p¨ê5v/ר?®Ž»Í¹ŒÍ{-^ˆÞŽþ¯'ìݶþq%pNdÜ/ë÷aµ1ø+àD]WÁ\_Ý:¯8¶nûÇë‘ׇ:¬9ç$“½àR½ÜØÖÍ«œ/Ï9'pÓ‘^‚[]¡¦ú6V-ýãtçðpBƒó©)—ŽRpiA½Ì¸vþqÚy<·�cpýÔÆy®ªû„�G¶¯×Ú?®Žz€>vŽŽ~0ç‹t–¤¸Fn^!ØÚ?® ŽÇ®.)¿„u]-�KK¤‹ß¼\sÿ¸a‡üF|2\ˆÎ㬀»¶›×óÁõ®vôWƒ«MëVI Ü¥ýãZ€;£¸+ Òpppp×ÜÕGŽgA(+>„}Èœäô’“;ÛÇt½s»qÅë×ÀÕǪg‹åxj_¹›úå$r¨ÀõNäXçôÊÚ pc©B—Ýäªç¯W7¿Äa9�——e-/Áq†yôº¼ZAî.pË}*àêóqóÔÚpÝ©}ån"à +//DIpr–(¬·ò5�N9.ûœ—;t`ç¦JVl~ÄgïKàú\q©Ï|ÛW]s(çñUà(ã{Á‰RÕ“×—¬®ùæLÃ߀sò7÷sSx¯î!Ž'1úÍíçÙ'ì§�t†¸ãÁrN¼@½s¨¯«Æ•ª$¬b1 1|¹"8£ß\œêN¸šÐûê¶n N¾|—‚±¦'òÍÆÃ”ëP@G'r?>hçA.›éõÎì+7�#/Õ\°O¦õU¿4N¡Ms¼\èCy¢’ç2àzµg°,€çüãÈz)©< J¯z­´3ûÊeÁEÃmX¼½¸Îyá™™€ë]\Ÿ1îzxÙ]¡ÔHÿ·càF“ß\\´F¼GpdúؾMoâË@†ÂÁón^ 5ñ±âÍ—)‚;·¯\®�ëE«.¬•!íböûàóÚKW¶Ä�ÇëË¡ÕvµqnÜ™}åràœì]EÛ´ê=5¸Bñ*·q§Á=48]âè5p'ö•Ë€–Ö8Ÿ‚ËVK½j#pt0iã–fo¸Ñäþµrik5æ�Â} œòäòvÜp\ŽäeÀIÓÞ%àµiyp+¸4r¨ƒÛ9H8 )mŽn»�£óÛ¸ž-Zm¾ú˜É°Z˜5qlhèê»â,Ç™o[Uu{È·é’#€ØWŽ 8u½îܾrz>.Ž�⼜Ê!•‘A‚s܈ù¸| Þ·1;B%…zOs:r©ªJƒWQ@Å÷¨ß\œNÁ‰–IÚ÷îqä$'†_NØ«3�èÿF�–[ÆRÒc³á¾rÏ\sØ5\÷ýݼlêÎΟwõU®·ªîÊ8€8€8€8€8€kzš]òœ§8)ÒÏÙñüㇸÊ%6ܧÉ=µä\ô7 K65pÏõ�sɾ;ê'æ…|œ^* ŽÂp=ÿvs .\Ø n,Uè�N7iƒp°�¸ B—LðúŇ­°�%âSo§^”Äx^|ß]ðÔ6hgýãh¯ÞÇuÇ©%…¦çð \˜O¯�Óç%àÂÿVp§ý㺇^cHÛà‚+ל_AXƒªàtI= îøÇ ËÆg ’òôòpÃ8jæÂ±~è烋E6çÞä7dÁõàúmp‰#gïÝ!p—÷�›ÛynÕ˜À¥) À‰|� üãôy�cm\χ "ó#Gf†07„™²çpƒ÷Kýu-À�÷�cpÑ 9 Ž�ŽƒàœsUpºª.Öp3p£l -þqÉjâãqÌ ÚpûÛ¸\©ªúç‚kàçœü8¶sP'»Â½�C_—öª nvcoî¬0²í$Á9Y‚š€óÙôÍÀ�ô�‹žÇ ÜSÁ ¯×g‚£ƒãA7/.p|Þ�':‡%—žAì9l€ëóàxëÈúÈáÙþqÑêM\�÷ƒ›‹ZøõG·c¬š×+pþ¸'ûÇq çÜ-À\O~ƒõÙ‘<8/Ò;W·9;ò\ÿ8aƒŒ™Àv�r"œ<îƒ!Wœ�¨È©61¶‘Ž' Pâ¼ÞqId³» îþqâ<¾í^p½%©wj(΂\fØ Ãwn3 à–y’÷ùÇ…ÛðyÇÁíòÆzš*à®îæ5—€;NT΋�»ú*WUpWÖ¥×U¯,€8€›õ7ˆÒý½˜.îßåã¿wƒ8€8€[3©w€ûüG‚›Be}ÞïwJðõµŸDØÀÅW§�DFû�=…N/&˜fSü°LGѧ£+89u.]èv‚›²ô¿9»÷ÿ A�_�B€ù”àèønpSÂ$eîÄœ�IJaÎǵ?1å'B#8ÿ ‰�ƒûw‚ð¹ N„Ty¬à> à†d®Ḻ¹˜aµAŽ!ò$Çmzã–¸^ý*¿n®›ÿ~Îàþû›ÔÕ¸4ÝApë%pÒíJUɾ ŽªìRÚôÂD/‘ź¾éíIv€ûç« �XXÁÝ�ƒv€‹í›W>s%p´ˆ± n~Þ¿žû‹Ýi–^îóŸ*¸t�´.d|^ D§kªÞ�‰ÁÉ•Ÿ]à>¿Àý­€KÚ°û¬ ¸ONz ]é.Ó5æëç·À…5ÅÞѪ·þ¾ ®wêXÜ’ƒ—Nr¶ã²àbc¾E‡ÏÏ»îNb7üÏÂóþ÷ ¸L†³àvVrª‰K*®*g{Ú87—ŒÿÒºÁEd¥ªªJ$˜¨|Οºõÿd°ÿ,OP—ëYÅî´)8ßkpÑÌP5µ¸éKÜ]Znp\þRp|®¶_¨æR‰ü§nÇ©uP?HKXƒ‹‹ÖEp~È~^A‹ç®Í 2™—šZ7$à(£p¦žzU´úÖàêUUÖ­BUUmÜÜ=c¹Í)CM­´q \̹Ü:( Že  �ûÜhãî›àþÚÀÍvãôÏ^piÖp=ý"‰ÁõN�óC˜Üç½î~ߨªuç‘—‚%î3饫àúCàØ~#·186€ àþy¸¥�Û'GI+UŠ&�âÈ@ƒ[u¯ÁMÝZÙŽ;îŸnuÁ¸ùs/8«²mÛ \9ˆ±jÜf'ÀeÌ‘U·*Í‘mpʆ“m�Ï %ˆ“nìV—‚ÓmàAp÷8;’7e\ô‚•^õ®Á±¬ÎQqä°.Ø!Ú¯Md¸wÚÀ¥Á: ðùT>ñ,¸ev$NdægG¦F(ô‚Iïú—¢!^¹œø“Pú{<^Ç3Àù :ݨs8‚ëiL+O]ƒ[Í“÷¿-p“™ÇEª.2ù»c�RýÃdzàT&üÐRgÁ-3ÀËsþ÷7™~·¸¾-9.¢<øý©àöí1h— îwd•ë½àÞ ¸Iܵô6pnCp¯HHéÞ êÛ€{·ãà·u,|7€¸kéÖ| õ{À%CÈÇ4‰¬½‘’·¸ê_¦îÈ/kFâ–ã.ã“+¸á(¸þUàj¿å“_´ 0£GÇ€Óܧ3gÎ?IRÛ— Aëà†�¶ 8*å{'¸qÜPQ· ZŸc·½ Z\šî¸éÞ]%^€Ó9ùyàįô·ÁíÜíÊà¶·A»8G¯�nn~!àiý*þ<§ôÜe¸øÛò^.HGkñn¡Å-ñ,¸¤ K¬™5¸‘ÓóærŸÙRæwñHÀMçœPYà21Ðæê�6ó*—'ŽkpbíaŠŽoËŒ×/ƒÛÚ?îÁ{QÑÆ!ûÀq×§»“twˆBw,À ä™Êî\ôò²^¯ŸÆ„°<Ø'k Nú÷ao%NVÉ�ˆà"²n�lÝ9Ð�y—¾ù3Ýd”ößèè}g9s\·q ÃkÇí�€ó«Å&å† 8¯íà„™°$-€ã|§àø\½­mçx«È°�éX2G4¸˜)Î6‡ã?êxœòÌ\�#·ÿWƒ“[- 4ÚäEî¶Q×S›Õ œòÌlî3èjà’6n N×Äxëx<ÓA$�C œsÜï^ Üè6Á=�ëèÖÑn¬‚£v¬%¸§VÕGÜxjÜ×=5p]r“C%N‚(VÕâ:àº'�mÜ‹Áùj¯j—r\§g,à܆9"Áí0GòvœWÐËàvލºî—1GÆ´(«íq½ª£}È×püe ²ãü8*›î©àÂ>qb¯ËœØ53k¯Ú±“u齫ºWåN`Éo|oƒbq¥½pŠy Ž=ëÖà6‡\;gG¦6жœÖ%ïAy×Qµa²sŽ«­²€)™ØWnÅMòÕ =;È÷\Ÿ´YpTW³àú­AþÎi%Ç{nÁE$�ÍA~8ÎûÊ�Yn'W¹²oÇ–*ா À½œ/*à<îêûÇE}Å ›kîÊ:®Ï¯ûõîHMý�àÚààà®'€¸®³#^Ž€»ºÜ3K“ç‚{‡ܘ› ´�;4ôÔ'.¤ÈÿîÐo¹Ž‚¡Sþq×7(p‡~vùx“À‰tGܼ~¸·øÇí'üÕ<ý Ÿü¶<-ð{žòæ$áœè±4Ðt¹£Ná5à\Sÿ8¹*¦_�ó+bËçM/ÎÐÎ5´°Gÿ3¸°»—X½jî=þqê"¯ŸüSë¨TÒ–r…ÍGh/ .z|­_‚6< \—‘•ªêÿ8ñé÷ŠÇ ZœWÙÜçïUâÃþà¢�ÑÒ?ޝ+Šªz£œ[@ßV®ñ—> ®�/ÆôT7ýÀÇc�‹{+ñ…¼¼î›ÀmøÇñu—êÚÉú.Û¹8çv�‹“Áyªñ Ü|¨Ÿ[L×`4Müã”kElD€ãŘ¸Å™fúèÅ�j Ý¨¹dåÁÉ­`š´qnÜÿ8áZ±½‘§£ÛLÑpê�nÒuÁÓïc«Á)‡B n¾…HÐ\cÿ¸8¾þl8.:pa«Çž¢Ë„î žÛ«6ö�ÓUU¤�àÜ&¸!nµc!sšÏkì¸ÆþqpüþÖ)t‹>qÑ)�S�¾¢-¤ˆ´÷ÿ8ÑvÆ«‘³ÑŽJ�ìUû�í´>ö¡³H6…÷¯×Ö?NÀ'®?ªëg‡\!›Ž‹™k9p\R\Ò?yvdj£úÇé!—×_�Ë ò)K”a2×tÉŠÕWüØíåàûÇéA¾º¾j nÊãÈ<ƒ”=ù,¸‹ºy-Ÿ Î+pM§Î¸5ª²~¹öáÅškúǹÆà2\ú\þ´2¸K*´�-Á•bxÈõCÀ=pÏ×Ï+8wQ½ Àܵpp÷dp:Õ8�ÕÜö£�.Y,¸}à¦4 ÜË }op)(€¸ç‚“¿ùÕD€«‚ÛØç àÖàvíópkpnÏ>o·G ÉcuŸ7€Ë�Õ þK;ˆo ®#Pqù à�C‰³�«Û•\ª�û¼Ü »˜ÆÔèUw‚Û±ÏÀ­RÑXuçY·sŸ7€»˜àààààî z7€û�àvý²&ùýÀ=¶¶A é¶Á¹¤üºåù€›Rv•ðÏWý½*ƒÚ„�øÑ­Â—דÜÁ® ®ú›|€;nä�‚d˜”úÑ5Zй0¸ú¾#œ£ŸÚ«p ŽVÃ~%8�gv€ãŽ"2L'i?º´Dþ^p\ŠÒ0�䔇n\âGר�ûà¤WS#Gë_®Ó~trß$€Û.îƒpGÀ�NìÞøËÀéà>p©‡,¸C®*¸Ä�nüñ�CCpÒ�nüñæÈ�Ùµh.õ£“œW—øÑñîH?ÜE·A¸Ÿ îšûÇ}pWÀÀÀÀÀ\spGF£{ñûUß ¨®îæõHÞÃÐæýªé›äVáŸN„Jûñýp;ßç°—¦;.—2sæ¯GïÉø-àö½³æ àR¯œñg‚sM߯¯í¸µÌÊ ƒ«¯«2¸Ñ¹�à(:|Ž.éNx–ýÁ¾?¸.ÉÜ ."ëÖÉÖ�ƒX.̽_õñˆ¾ôîg�›Ô ŠmÞ¯ª‹næ‚¿\º(�¼_UÎ^ðûƒ£Œf_¯º—¸´†feßÜè6Áy¿ª5v:üÃÀ �Ë2¸îñ¨�ëôm\þó§�뚃ë(¢[_ï{‚Ë9®KŠÔw÷ž÷«ÆaDxŠÂæj?\Û÷«rê1þÖ)ÃíÒàöÍŽ|Qkù~U…™Çd?\ã÷«ÆJeaRåÊààæp/ÿ8#¸+ àྸÿlK R\‘øIEND®B`‚urwid-urwid-69907e4/docs/examples/browse.py000077500000000000000000000003551526073734300207260ustar00rootroot00000000000000#!/usr/bin/env python """Run the browse.py example on a fixed directory to take its documentation screenshots.""" from __future__ import annotations import os import real_browse os.chdir("/usr/share/doc/python3") real_browse.main() urwid-urwid-69907e4/docs/examples/browse.py.xdotool000066400000000000000000000001641526073734300224100ustar00rootroot00000000000000windowsize --usehints $TERMWINDOWID 79 19 key --window $TERMWINDOWID Down plus Down space Down space Down Down Down urwid-urwid-69907e4/docs/examples/browse1.png000066400000000000000000000166501526073734300211450ustar00rootroot00000000000000‰PNG  IHDRð`ö[€I cHRMz&€„ú€èu0ê`:˜pœºQ<PLTEåååÍÍîÿÿÿÿÿÿÿÿrD¤'bKGDaf¸} pHYs‡‡�åñeIDATxÚí�]r³¸¶@¥®ä*èº38ÕÈCàÏ*7€þÁ;ÀÞk�óÅ„,ãÕ€þ6Æ( ?˜ÿsý9Pˆ§b’‡‚x�‚x â�(êÅë~+@Ö¿KlÏ92pÓùN¡x�IÝB¼ÖÐ*^7käA¼¦P+ž-åz¶•¢O¼�.+â5Ä™µÚÿAeÕõAUÚñ@âÍÃ=wnWBN<“ßÙõ†;·k 5Ù'ж¼ �£u–4â� 濃ñqøŽþ¨ÄBsÊá„x�€x â�úîñú/éc'šSŽþ ¿‹÷¿'ÛA‘xÿë§}=Úü‹Ä!‚#¨H¼/.à ¡èoE¼$ Ô�¢Z-â] 9ñú¯ÿ Ž�.ýþoá†n}¨L„åÿ¯!Ås•…I¶T^°rµÚ¾+¹ÇËÄû bҜ҂â}Mr�÷wÊB¼R>Äk ÁKí׋âq©m‰Åâ,ˆ7Ô¾†ëâýÇ)¯N¬Õî µÚf�lÇóâõˆw=äïñ†Jøñ®„p;ÞñfÍ)ÿq�×òíxe�õ‘x_Ôj[BR<ß.ÏgÙ.ïRKÒCHÞãùX¼ƒ+{*¾þËE”>Z°²µÚj Wñ@âq½"²íxS ¾¤œ�ì蔩ÒÇP4Ë ®â�áO*0¯ âÁ©œö¸�G Þ5A<ñ@=â=x¾JgxòJ[œô¸�G ÞUQ+Þìazˆ×ˆ‚ œŠ†ZmñÈxëÞ ^Ëho|ä{çžß™ávÚ"{¬`G4Šg†'‡Fñ â5Œ"ñL?‰6]jÿçÀq‹•=F°+òµÚù=â]ÄAÄÄûwúO,‰E¼ �xp*kµˆwDA<ñàd7§L£xb…„P+^ïºÌ‚x�A½æÐ5,j¯+Äë¯4Ôj™ìsAD@<ñàdäkµˆwID@<ñ@ăS¡9D@<ñ@”Åë2P3õ6§ ^Õ¨¯ô+,#^ÕÔ+4âÁ©h¨ÕæCßg ^‹(où•¹)q9ÎÀ *w…(Ïdqò¦7vA<ÃTŒŠÑ'Þ0»±›ÞLË}6ß¶�oÓY©âÃÛÈ×j‰W¾qogâIþ„^ñ¬“¬ëgâqƒW5z”yÂ=œ­s7sˆ×úÄ •�g¯!×j]ûIÂp©mÍâeÑßû© ¥G¼¶P)^�xÍS‡x†æ”vPÛœ/µv,çðàÓÏÚYh@®µâ™¤OÌöîÙRqÙø¾3ß— 5¢pXTè‚íü � Ñ&â1H F4Ôj™ìsAD@<ñàdäkµ™xÆ!}XàhDP&\ Î0 âÁ©ˆ7§À5A<ñ@yñ6öôw¯ŽE‰©¹§ áæ”X»)Ùšx³g‹"žR*o-eW¶>#žRš¯HÀ`=Õ4%^vjC<•è©Õ"ޥРžÀnìž|¹óCÞÃŒqµ·»µ!Å”ÞçÓåS8bþqOÛ™$b‰ñŒÄG¨psxÜrïE“zœ/¦Ø>ÅKæù|û¥üÓr ÞÙÈi?=qœ¾hc¼°l|ø Û›d ·¯Eøí!»Þ¯ŸÒ“î1Ï?Ý1Æ;×AºVë/“ÓY(®óËQ¼™œcÃ,ŠÎOÒ éGñ’¹9Vô ÂûÈOo pŠÉÜÓdÄéMß'¡Ç||;.µu"]«õÞÌÄ‹bÙ4eóñ*FZ¼2ÔDT±k÷ âÕ â�úÄjã&ÄkùZ-â]ÍâÄk…âu¡ÁØ¥D¼QÖœòD<šSAº9åQrÅ)¤ôé ñ¬Xáá}¤Å �Š*Ãàâ%aÆŠ¨ít™UŒºAýCñº2¾±'*D¾V;+‘uožÜ»q[W=ˆ"(ïQÐ0ËDŸ†Ð'ÞÃ7âØµ�x­¶,�u¯�ŠD]¢ ´‰ñ@ÄNE_s \ÄD‘oÞö¬KìA#^Ç•Ê�oNA¼KR­x³DˆWòâÍA¼Ë€xp*:kµˆ×<òâùðcaÈ{ú(ø>™uæKœÇÅ‚˜uÙä ¨Mâ%qòü¬ë<´I‰ó¸xÃ+âÕ‹|­Ö‡%ƒõ˜8�±÷'@_Ò<.Þ/ÔÆ©ÞP òµÚ¨Mˆ`BĨوãb"7âÕŠ6ñÒn!3 ñšB±xÅ xÄk …â…¨ÚC€‹¤¤ñ@¾V».g¼Q,ž ›¹ÇkÄôˆ—6§LEË£G!^#hkN‰âÙ±p±çb&Þô7Ч«Ãž#-ž¯D¡Ò.³øŸ~&^ïºÌ‚x±-êC‘xé ë»Ñ† ¥x]!^�xõ ¡V ñ@ÄQNFºV ñ@ÄAD@<8šS@ÄDÑ'Þ‹£ë’0eÏ8E˜3%hmNA¼ÆQ'^—Ž7~雨,ƒ•Tý¦=ñ6¦B< â�ŠÂZ-â]yñB¼»ÞGŠšnó“xyÅ#ã³ý’é�aÑ}“æ;ÿ<âëI¢@Þl3žÊОxÛò¸ g¢èà#ÞPX«E¼+ /ÞÉññbþÆô1ÅÄ[÷hú.N�ýQ Þ¹ññBþÝdî\<—1â�‚üôƳâãÅøearYVžÎÏóíËw(ÒµÚ³ãã%ÌŠxƒ—D\<{n˜²°Ôå±ZÊ|{NxgpAñâym±<}ÿFÅ^E^¼³Â”%AUl¾\”‡+í‘(¨ÕZ)ñÖÎx\iD“xçÄÇÛ|�Ç ï ˆwR|¼Ä;&â:ññ7§D§k9âŠxsÊÉññúÐÓf}»a¶}Xœ„N> öG^¼sãã…üCCq‘Ž.³3$Ч—Ê#ãã…¸x6-ϸ 8µÚEaʨWâí´ ¼†"ñ´Ädzô•�ˆ¼xjâãqkwâµZë^µÄÇëåx ÒâÁEA<ñ@ăS‘oN�K‚x â�(õ‹ßèðªáæ”Îu$3úýxä8ãf!ÔÄ쟈Wij™xq¹.^VhÄ« qñâ?_¢|:…qÿæûA|¤¾üQñ¦`=±,Éï§âeë¯*ÄkµˆwMŠ×™ Õ¸¾ë»nY¼0Égz²‡0e6Yª¨AöŽ[1 “U:Þ/ èƒxêP!^÷'ñ¬û&¶Ï"^‹—4§¸HM™�Ý“ž Ä«=íx¡»á¡x®ƒ¢ˆŸX6§„ˆPñT"ޜһF^_Û{ñÒàœKâÅðv íx®�ñ´¢A¼.Ž·ë\7XÖÌÒeãñB—lè“ áÉúXÙp©U�´x6Š7­�]®ýCñ|Ù³‡î%�-â©E¼V ×ñ@ÄQN†hQ â�ˆ‚ ˆ€xp*4§€ˆ" ˆ¢_¼gÑ VâC1F@#µ4§ ^c ˆPŸxsÅX-ú9ÄkŠzjµˆ× Ä OKô±Sb n¿bX.ãß%�†·~z�›8”?±1æÇ½žˆg¢p!zÏïë4ï†í>Î]/ÙϤû‘cLoâäéCî'gs÷1Î��k7MNœ¦�u6�s—L_´~"PÜ¿÷'ÀäËÙäs@ ÒµZoIÇÂO=K·ÇÙbÉ„m›_éü™¯øŠ¶ç>PòâÙ©¶5ˆ4D@¢bI¼4T…@�-¾¢M>T!)^ˆÇÄ1oþ0ør".èA¾r‘FxK¼0㻳å'Øäs@ jµ>€�sgÞ¼Ôñ–Ïx±z Ð ž‹»c]‰ìîñ¦Í‹÷x=·y‘½�$£ô!/^Öl’ˆ÷JsÊ”0„‘²Iöù�G¼9¥÷ ÁÖǹërñ\²]oÚ?ö\ä¾u\ju!.žÉ»¾|\»$Ô§I/«i°í—,¾Æ¾ÚBËý¯pÔÌ2ƒk�x â�ˆ§bâ©GN¼Ãâã¥�Š�ùƒ.Å;*>^ȸ˜<š�ï˜øxÓÙsJœ~¨B�x;ÇÇëÜDq‰ ñt"y�wL|¼t¿éÎ-žBNï´øxˆWòâí¯‹••ér;Õ.º¡v"x„aùKínññˆòMhoßøxÙ¥vZ�xúP ÞÎññæâ1sW!ÂÍ)ÄÇK›SÜ nñô!ߎ·w|¼(ž�æ0!žFä»Ìv��—v™!žVä†E/$`ç›A’â¹74ï^ĈÇ-Ø‘�V¶K¢A<éc(šìWñ@ÄQNEîqpiD@<E�xÄÇ»Ú7@|¼‹ F<âã]‹jijÄÇkýâ%IŠ%âãUÈ™µZâãA@N¼Ããã…ʯ"(Þññ’|@%-ÆÇóÓ$íX1eœW«=?>^¸–ƒ>ÄijÇÇÇëLÏäFÝ´ñÔÓ`|<‹xŠ‘lN98>žE<Åhï¸øxˆ§ž6ãã!žzÚŒ�GsŠb¤ÛñŽ‹�G²jäÄ;!>ž_¶=è¤Áøx ÐŒÜd”]ĈǠ¥+C|<8©É>ÄÇ»8jf™Áµ@<ñ@ăS!Zˆ€x â�(zÄÛ³=™� ŠÑÖœ²G|¼˜ñÔ¢F¼ãã¥Y"žRªÏn��—f‰xªÑ/^’¤X2ˆWmÆÇKS žJšŽ�g'ñÜç ÿ³=è ñøx&¼ ù#ž>šŠ�ç§7v~îî(vo,£ì!=¯öÐøxˆ§—+ÄÇ›>ñÒv|<ÄSKÛññO”ñÔ¡A¼ããã!žZÚŽ�‡xji4>Í)z‘nÇÛ?>žE¼h:>^Úe†xi2>^>Hñ4¡ ZÔ6 ˜zDP ÞÞá¢,ƒ?+¢¡øx<Ÿ¶š‹�×ê± ÔÌ2ƒk�x â�ˆ§B´(ñ@ÄQôˆ·_#\Ùq¶Ø‘ÖñlG!´5§ìñT£F¼Ýããm¯§gWˆjij¯ÇÇÛxþ¶;üýâ%IŠ¥¿^+O€¶ããm<ÛÞ¡¹øx!ž¿ôûw~Ú·�“jˆ'@sññB¼(^h ÛžÏZš‰�—LêqA}Âþþä:mïúbz%œ‹ô¼Ú�ããÍÅóûâå)àt‹�‡xuÑL|¼íâ…iĤ™øx›Å‹ÂO aÊv��·Y<_ñDÐ Þ®ññ¶‹gOÍÄÇC¼ºh&>ÞKâq�'ˆt;ÞÎññ^¹Çë|ɉ‡<�ÅÇKÄšúIJýÇM±VëE=1š‰�·Y¼Þ„†dÄ;Ñ¢ˆ)vEDP ×¹+ÓP|<¨ƒæâãA¨™e×ñ@ÄN…hQ â�ˆ¢èo¯ödz┣­9e¯øxˆ§5âíχTA<ÝT#žÝñjB¿xI’bÉ ^}4ñj Áøx.Ÿi^…íA%MÆÇ â1äJ=ÍÄÇK/µœð4#=¯vçøx©xœð4ÓX|¼D^ȇ.³*h&>^Ø‘AªQ-ŠñKWñ@âq#veˆ�'C|<AÍ,3¸ˆ‚€½ùø‘.\‡�ŸŸŸðv§ü¾(ççý÷ÏýöûúK¶Þ/~Þ]’ñýçðfZéîIVCÒaŸ>K÷øs¶í¿­<‘i‡­ù¿uÜLÌ';böý}ù§rß–#æw ÇíQI² ƒ(?þíÓ¯°ÍË1¿ï·�ÓóèØ-]ᄅ{óT¼iѽæ?ĦÏÙ´ÿ¶ò”;lÍÿ­ãfÞ/”ëö²x÷å‚ä�¶‰²U¼­ù½qÇ/¦_ïþ�ïÇú^ˆö»»ô¿™Ý²¼Ö>gëþ[ʰíù¿uÜ^ó·óïïËÿ9I÷°ŸéùÝ·ÅÿN>‹<Q¶+µ-Õ7‹©‹B…ÿâ̸°$žñ§’›ÿ\¼w÷ßRž,ßÏ¥üï‹ù¿uÜÌâÅ3ØËâ-¦-Ïø×ÏüQ<³�x/äŸîz3ë¨o¼6†ZÆïšé�_ñã7O7oînJ7ÅóùLéÃ>aŸø¼õøDˆéÌ1 ßdø¡Ç—ù/{f¼¼-ýuñ^ØÿIyÅØ�ºëͬó®x±üŸæós‹xÓ÷+�[–.ÍdI<§Šmª}Dñ~‚0cºhUÈïã§Ì'ÏïEþØPyA¼ßÿöÖÅûü£x›öRž˜þöRþé®7³ÎâÝÎ[ƒß¿/ßf2î÷ýwºÙ¥wéÄóÙ¹|ÆÓÞ?S¾Æ„ü}º-°Iñ|õc[þé®7³NUâ…¥ì†m.Þ˜n¼Ü~'ùÅ3çw’>/fç®Óßf�gµÍåÚÜ—kµ÷x€ŒoØö9¯îÿ¬<�»oÇÛ–zHnf�2Ÿ$¿âùæ‘PþÛíóþ°1ßéûýIœ‘–_Íþâ�ßOX¼oóX<ó*»2âÝÙê=ñ¶ï¿Q<“‹wOϤÛ+‡‰·ø=òßK¼o“½>ïg£xßiõ¥ÌwÄ›’›YGµx¹q¯Š÷‘5ÿ>ïç;¤ÜY¼ðe]‡¶q?Ü}I¼é†ê–öGnýœ­ûo)OFúyiþFN¼¬çe›xÆWJÓ>é2[|yÖœâ×?ϧÿÄûžeWØ·Åã÷lø¡CºpÌKâ½¼ÿ+â}šâóBþÊÛñ|tÔÇÇÍ(ÏùPŠ÷=Ö’äïl‡ Lz¥�×j£xîÕåäóßÚ³;ÿRAß©ž\ÞÖÅ+nâ·|Ζý7”gžþ¾˜ÿmetÊÁâÝãqxQ¼Åû½Èã#íÁÊnö’êç·[ñmÒ.3—E!^¹Ý-GÓ4›Åûœêps!òñoþ›º+–)Äù C?ü½ÔBåðöX¼µý7”göµRñòüoóò%»Þ’ò=;lù8¼™3Ezã‡EÅr}n�çkç›Çã…¡e-3 ÷í“~Ç—gâ™t{ØÍ·ÝýüdùoËG“ðX—ûô?¸ñgŠeñâá�ly@}ζý7”gþƒ¼T¾LjçÊÿŽxK²í‡ÞŠØT�­³Ñÿ:k]û¬wíå ¬7w¿„”xvãßšî¨ýµy>÷5oÃ`ºC°Ý¾éŽÚ_ËqPÀ®×Úc&•¬ u¨‚ÿòYAvädZIEND®B`‚urwid-urwid-69907e4/docs/examples/browse2.png000066400000000000000000000160771526073734300211510ustar00rootroot00000000000000‰PNG  IHDRð`ö[€I cHRMz&€„ú€èu0ê`:˜pœºQ<PLTEåååÍÍÍîÿÿÿÿÿÿÿÿ­g\bKGDaˆë pHYs‡‡�åñe”IDATxÚí�KrœÊ@«®ÂC„àx+P„Wà/@Í5Òþ—ð¨?ÐMË™Y}ν ˆÏ1�EU¶s ¿™ÿ¾ûï€-NÅ?ß âA â�ˆ¢¨o¸@>~œ·ØŸsdà›X®w Å\éâõ†Vñ†U%âu…Zñ|+×µ10Š>ñ&†j³¯#ÎŒjý‡x�ÏùargvkúÕŽ�ã¶xCxô åÁ&‚â…àa²gþiÆ'®ŠwÆføn$Ä+¤óëñqrÌWÛÆBTë¸Ý𿼍vG<·ÖާK[^Â/CÄë1ñü¢Ðà“Úñ›âùöV ùö3økiö÷_þ‹þ†x·nµñ:@N¼Ë‡Ù¨v|.³„¸áʇx!Y�r[¼ñzEƒx®ÏÕâUK䩈ׂâ Îm��#â=BÕ)¥xÍxœVl£_†T§t�t=^úÝŒûuT[½HC<ãȉW ûã‘%ÊX¢cÏ+³>8¿YT~ɯsõø¸öª�F–‘ëìÓ†®;¡,ô â� Äkª‡GžÜ 9ñ\ýd7:žÜ©Î>Y´íq譽̠sA<ñàTÈ" ˆ€x Šñ¨?~´U§ìŠ7ðN£+Ôˆ×6Xͦý]W˜ÏsîýâEÀ>Š¢ZÄ{$$ÛãUùð†Ð¢=7ibÛ�6¢ý!(^�¯¯H€7ã#âuˆ„xM>¼x« iʆ6mÙª»#˜F¬!è:Þ2Ýç,�˜T§ô„d ä&^š¾%âu‰ñ¸ÕöÈ âMò®¤); ý1úáĨö Ä#ªíÙ~µU><Ä{DÅ‹iÉïªN¹"Þª:eä¯+¤ëñÚ˜uOvÄ+«S&R^¼—æ—Ø?ïVuJ, §b§:%‹÷tùPÕã…ùËôX�óçåéN=·Z ìˆW'«^™­ç?åüy1xHiÌÒ+³\�¢nïûGó­¶iU6xª ,¸Ô: â5®/5ÈÕ)Ò;û€hŽj¯wöáöhÄD0ôŒ×€x¦ÑÕ"^ÇØLƒx BzÆ•Å“Þø^NÅ?ª@¼¾A<ñ@â .ÿÞg™ïüõù L¨N‘ÞŒÄëóâ­<Ú/•C<دaÇ£}ñ@êN âõ�æ¨vnú^MD¼n0$ÞàüäÎàœ ã“[ÃÜ¢ÝÏã£+æO¥ý¸xùIåÚùq½p*†Ä›Í)ÄË£Q¼YÅ$Ö2ÿŠxMA�@Ý‘_‰w¹* ®ˆj§ëÕ�GÇe®O¦± Ýèü¸Šj�›W1�!žz£Ú•xc-^1cúrÂMñÂtÄŠxQ’òŠ7æÑM±w[¼¼^@Ý‘�iÊòúV¼dŒÕñ,¡îÈßoŒ1âYÄNT»oÌÕ(c9ñ,`Y¼ð˜‡x–Qwä�ˆ7"žyÔù›Õ)óVûq_¼,Ý@uŠB¬T§¬êñò­Ö�yò¸Ï5åVâyÄ“ÀŠxñÕ– ï¾W¼;ócìºY¦eþômQn)—æûpÅä•™$êŽüºYTx™Å“.öx±À¥Xl °)�$ÐÕž×ÙñNñ&ïtojвß1¾™�ØB½QíIâ…ØNæáÅA<ñàT¨ND@<Å–xW^q îJµ¯Æa±:ñ: 3ñFR2ñ@ugñúFsT»Î�A<ó/6U-Ùý2-7uS矲«E»ÈbP¼”øÎÅŽdA¼ÔùǧrcH çWË�Ô�Š]ñbž¼ÐY;õULݳ‹K÷Å1õŸ Ë…¼z ‹Þ¨vW¼qS<•«Å‹ŸËå@ÖÄ+SSÌW±qð[â•©)Šò¹_7h@�x«4eiKý�â ùa°JÞ Pw.ŽŠ7UržF¼ÁÕâ\¨À^T{¯xÅó`ʪÂÝVœÄóÃUñ|+�yŠ0,ÞT/|ŸxtàÖƒñÖ·Ïå•Å^uJHUVåÁC<X«NÙznkÄKYôR~¼²�[­¬‰×¾úZ‹74ó—à£üJ*u{û¸¨;ûÍ¢š—ýkñÆœ‘q‹å}Pzç@uTKgŸŽA<ñ@ă“ÑÕVâ¹€ôvÁW�x ‚ñ OÜ_eÄ+œôvÀ÷€x BªN‘Þ�Õ†!^× ˆ€x BŸÏxÆüT’ªS¤7¤ñúưxW¼B<õô,(Æð3âYFsTûß/ÄëKâ]6sþ=ÿü™/Ô?‹é?ÿ,£¦Oç7¦ƒ ‰Dû³˜”Å Ãñg4mœ?dñêé ÍÏxµx“méÒ¶L¨¦ÏWÁŸóä?cQ ˜þñÔ 9ªmÅ *Å W/HõsùÉ‚ñVÓAvÄ[n«?ç{èô1ЧGÁ¢d­xy:(@í­ö¿¥诼¥?Ó¯|©û‹xF1$^Š&þüœ'DñÂtÄ3…�¨v,ÂØyâYÆ�x?Ç­g¼¿Ïjoµˆ×7vÄKÒlT§Ì“R�b;Õ)s-ð˜Þ\„ yúmñ~"žìˆ_y�I¬â•Y!^.W¼2S…æ[mÛ,jöèO¼z函ø®6¬Ä£‘€&4Gµ_ßÙñÔ€x ‰÷³¨’i,=ã}Á®JïD4Gµ_,^ŒE@$hñ@„ôŒ÷ 9÷þ 3>ˆ§Â× €ˆ" ˆbB<¾m 'ì|ÝÀ€x=aG<´ë Câyé�‚/ñàT4GµíWÃ#^GØ/}‡ž _ù¿ê,bO¼)¶�ÍãË{@ÝܽÕáÓtµC<Ãè�j÷Ÿñ‡xæ±*^¼ê�iÔ�Á_Ë3Ýå6úy0,Ïvˆ×êÎà®xƒC¼°ÕFñœãVÛÅóˆ×êÎ â=êÎàUñœË?`kÕ)ÙG=ž}¬‰7EµCxg‹xæQw÷›E¹Ð8€F–ÑÕÞîìƒxfA<ñ@ugñzGoT‹x]cY<0 â� ˆ" œŠíê0 â�ˆ¢è��a¥:ñ:üx¼¾° â� êNâõ�樶ÉåwJ"žE,‰7¤N>wÀX ø4l`I¼Ð·'g°¨Å£ï�EÔ�²�ÄŒãPtæ^n°E¯îÁQåb ½Qíúñ:šxÁµé_ŠGJ “¨;c«4eˆ×%êÎØ-ñæ`#tìNóÏ –¢ZÄëËâùñÌ£îŒÝoªQF<ë¨;c·ªS–œ=—!Õ)F±V�Rf âM¿ÏÖÄ‹¯Æ²h¼23ŒºS¶Ù,ªl 0/li$`ÍQ-�}:ñ@ÄQNFoT‹x]ƒx â� ˆ" œ Õ) â�ˆ¢X�æž]`¯:ñºñ@{âAG œŠæ¨¶ýjxè{â ±ËÅôŸS—‹üUñ¡ïMá5cX<çBÚ²¢³� Ã8.½p u§èÊ­6æX®pÃXvò^z;Æá ½°�Þ¨ö˜xqJ/ÿ€bz¯êä=gnôÒû·Q'Þ*MY¦oÈI‘kñxÄ3€ºstX¼Á5âÅlÜš±Õ¶âÅ纕x©žÒ�x~ã/ô‡þÈÑ¥xd�Ò�mñ\S�’öÊKïìb½:eþ╦âx\*–¹ÕjÆšx!x¨¢Ú!¾*+†Q­Ô�¢ÝfQ­xK2ä±n$àÇÔVT¢9ª½¿=¯ÉÌ€x BGây¢Xƒt Á„-ôFµ•x.°¿-ŽmÑ�x` #âAŸ ˆ€xp*U§€%D@<Å‚x‡^ˆ .}¥^¬ÖãEšJìT§ ^W˜o59 ‡xª±#Þ7ÅÅ œŠæ¨ö`~<ijˆñbJŠ©�»|L$0MHÑD-ÞÆrž§>q,Š?ä>=Už¼]ñŠåÔíîâîLlÜjcWž1$d ùïÂå+æÉÛ‹jórKÿ[FoT»#žÏYŠÉM‰r¸^ñ`W<_NnJ”Ãz¹JZ�FÝ™ØHSÖŠ®[9;czš ûä·–C–÷W戧Ãâùqõ WŽŽG<Ũo#MÙ ñboij@?â¥üxQ7ç¹ÓêE­xwßjS~<ij@GâùV¨>Ö â�‹§oŸ Ó¯xT«¦gñ¤w®Ð¯x ÄDA<8•üºÐâ�ˆ¢˜OaL‡±ûuâYÆ®xhgÃâyé�„ñàT4GµmÓ÷Õ¶{é�„OcH¼˜/³ùM_î{QÍ�MãS)bE˜oÎ{7†©[wlúó‘ùeßbÞÓ nÜOzà*öÄóñC1m\‰Çž Ô‰w3?^‘«"öìN%æ=òÒ»°#^ RÊ Ä3ˆÁ¨6fˆÙ¡·ZƒX¯NÒø žIL‰7"^7ØÏQ�bsÕ)ùVëçíwËW«´õ,T +Çœx®x'æ—ñÁåñ*Û»—Þ ¸�FñnäÇKߢç|¼ÆUâÑH@3š£Z:ût â�ˆ¢ œŒÞ¨–4e]ƒx ‚ñ OD@<8ªS@ÄDéI¼ª%Þ@¥ôW�‚x&0#Þá$=í ÄSIâ­öÐKï ì‚xp*š£Ú¦é;âõ„ñR²ð“óãůˆ¿ˆó—-»–¾"ÞKï ¬è@¼œÒbˆ‰òÒ~ÞG/½3°‹zñê~´~,r§¤ñ˧<#åÍC<�è�jïÏWGr ¨Æºx{),ò'ÄSŒ:ñÖiÊî/õüF<Åô'^Ê›‡x1ÕÞ-^Ê›‡xéX<�x@<Á”xÃNuJU0åÍC<�ªNñóöº:?Þ®x1oÞHg\•˜/çÃóQÁæ•Y,•¾c*Ç·Ò»;¨;3-�S>¼1^ëÚFóžäÄŒ>O’ÞhÐÕÒÙ§cD@<ñàdôFµˆ×5ˆ" ⾚oÒ[�Ã�···ôñ‹Ö÷ú Ûùüqùõñ~^¨¦ÇÑç�Pdþü<}XFR¹�bUSÑiÂ<½*·ÿwŽ-l{2ËG×ÿ©ãæòzêaµ‹ÕþÇí_¶ûcw;òúÞÓqÛÛ’jÆ$Ê[üxuŽy9¯ïõÓÇéúŒ콜ö4|¸*Þ2†õ‰8ôw-l{ÚŽ®ÿSÇÍ}B¼´]ïw‹÷±½!õŒI§c¢ïèú>qçË3Nÿ@óçùD4â¥å>BùËÊÞ«uÝú;G—?²=‰ù„_ÿ§ŽÛưþ¸Þÿ¸ýÏ‹t»Ûñ\^ßÃqÛüwòܬcå¸RÇJ}ÇÃb)ĦPé_œ›G¶ÄsñRò?_ï³ËÙžj½Ï[ëÿØ\ÿ§Ž›û„xù v·x›eÛ+þc‹çþQ<÷5âݱþrÑwwÕâÍ÷Æe\¦,Ÿâ„·8{yxûžá–róò…iY¼¸ž¥|Z&-ןþÞíxEˆåÊ1 Ë ™NôØýx«‚‰R¼²üôÿk¼â•ë?ŠäÚÖ¯;Ñy¹|b?>>®ˆ³W�wtywL¼U=ž:ñ>ŒˆWEùB¼··rýŸosz¹µð0¼Ïm‰sôïÜ»üõí©W½½þ];Ò ¿]³üï·Ú(ÞÍõ•x!š¸%ÞeBy¯\Ö·QþRìm™¹˜×·xNôGˆ&Ûg´ø!Èçâlà»–¿²=õ2ïÛëw÷ˆ÷¼sEÚº¯oÞ?añ^ݾxî-»2â}¤+ÕçÄ;¾üAñ\-ÞGy%=\|›x›û¹±þ¯ïÕUÃ}ñÞŠ÷ú£ _ÚõÞñÖ‡äÝÝFµxµq÷Š÷£ªþÝïí5•übñÒΆÚ.œ¸�-ñ–ª÷ò}äÑ¿stù#ÛSQþ½rýNN¼êÍË1ñ\ J6Ë^ye¶9¸V�§_/–ÿ1‰÷ºªî°ŸoNuàD§r鸻Ļ{ù{Ä{vÍßKëŸ&¼/¤}¾rÜœRñ‚­x¯s”PT ¿V $aªz¸UT›Å ð¦¸þ£ov×;•„ˆ/Õ‹ÛÛmñš‡ø#çÈò¶g]þcsýï7Z§|³xù8Ü)Þæ…ý£YÇ�ò Võ°ÿV„Ÿ¯a«+_™…U4âµóÃx°üƒî°xÏK ·¢nÿ÷ü9ܱ\#ÎsjúŸ¥6‚Ã÷}ñn-`{V»UŠW¯ÿ}½}Å¢ïÅö];lu;¼•3My›Eåíz>Ò/Fç‡Û㥦m”™…{�E_óàšx®œŸ‹uwooÕú�¶eÉçÑ<ÇrÏñ„»x¥Ø/Þ�·q@÷þαålÏú„ܵ}+¾G¼°ýŸowKŽ�裈uÕ8Úý_{­kïõ®}û·«»ïBJ<ð€-÷]Ëk9ò<­yÓ} ~øÚrßµ¼–ã €/½×~O§2€H]ðLðÃYv6ƒÌBIEND®B`‚urwid-urwid-69907e4/docs/examples/edit.py000077500000000000000000000004231526073734300203460ustar00rootroot00000000000000#!/usr/bin/env python """Run the edit.py example on a fixed file to take its documentation screenshots.""" from __future__ import annotations import os import sys import real_edit real_edit.EditDisplay(os.path.join(os.path.dirname(sys.argv[0]), "edit_text.txt")).main() urwid-urwid-69907e4/docs/examples/edit.py.xdotool000066400000000000000000000002011526073734300220240ustar00rootroot00000000000000windowsize --usehints $TERMWINDOWID 39 20 key --window $TERMWINDOWID Return Return a d d i n g space s o m e space t e x t space urwid-urwid-69907e4/docs/examples/edit1.png000066400000000000000000000442471526073734300205740ustar00rootroot00000000000000‰PNG  IHDRp€“ãÒí cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿÿÿ ½§“ pHYs‡‡�åñeHIDATxÚíÝy°-Ç]Øñ>wyûÓ{O»tž¬Í–Œe�mðJy)Àeˉ!¬&©â�� ) I•Š@©‚„Jà“?’–¤CHâ² \Æ6²dí‹¥óô$½åêmw;÷žüqžFóféùuϯ{zæ|?år=�;§§»§§Ó==sFÆÑd2qý ƒ·b\bäx<î:äh©ë 0+å�æÃPû°u<»ÎýJ’•oæ—%ïÄåEšo•¹²dµeEy×)ÙÆù­<*‘.áië]�@­%ŸH] u‡>Dk¿ù·œNù�0h”ˆÐÕ¸Ò> ×ò„(̼šÂUVèôó{™§ï:»®xg»Nê›L&ãñØé$•Ë5�°ø)¹1ìygÛ¾«ÓU ÉŠ,Ì@Öþ³í�º�ö£˜~Ý‘Ív'Ù—eu_�pGæ•CÕX’öí2Ÿ‚üjT.KßµM8�ö ñØK>}§ë\§Ä³—%¨u…â(HºõÔF¨Þ5#Ü{¹ýë¤2}K_麯rû—óèm+Ëg„Úx¤$‰ ¨!ÒO* E¡–ë®®" Ÿ;]Ä™`×éÑÒA˜xúÓ›1C©zÎ ƒŒrnÛÝÊ](æ?týÈëDÞ-¾U8ËÿöØW#•V:Ï¿ë0@XWYû ‘¾©©Ï–×1u×sñ­ÈË“oˆmÎØa#Ë›ü®¡<ýÆOZ濜᠉[úîºÐ¾Ò\„=Z�”÷ò�jU©}�R�¸±¶ÆºP!WZÚg8ô5�={é_m×åYò¹J¹|F¨í�h]À3ab^L}lsùœ2/•Û(Ö†ðd:wSêX¦¡Êÿ™H¶ë@þv»zúå‘ea¿Z•“ßQ ½(NŸ„[#b¹Sº6žÈ~ýO|+’òä/‹êN¡ c)Bãçmòß¾5Ô‰V«–\S+«T±?ÊO¶­×›.‰<™í´zÀµý7ÞæhY3ŽlyR7Ú%‹JWó2½|Íš`ßÞ²PŠgqìU¾=ÕáâÉlƒº?¹®ïµÌ¤¥|æTÞk¨Ä´Y/ÝaéŒo×ôî~þÖ¬)M`H %L?ŸÉÜCú×ô�²×r O›Å¿¡à}þvÒŠ÷Pëº×B+÷X†®„uékÝTo¼‚¶LÉ×Ùë³nªS^ÿu¡ÈÔÏ:xT¦Ê·´OÍ¥6ë§W*ÄTõõ�…ôå [XêùÏg¾rÖÍXÃv"§@P–©¦Ê )�°��[ÛUëj·n¾ÎiVÊ’û| ä¾ˆðTW¬“òÞå# ׯzq²%“RN-|vÈ>"o߆ËëcÕó?Œ ÆÐBßò(LhåÛUùß!´låa¡üÑ©–®¨å*«œ*œâ´^Ñ5f¸²Ôš}0-ì÷-éK>”´¿º«~•'‹ä­ÊòÜ‚¤ŠäYòÓxMæqÁš¤&%Å™”“L¯‹?KµÔ}Ør.ªe�9�¶ò²'(ýºRF¯ÔBÓ/„Àl³Ê% FV×ögZÚ«‹I–Øï´ =ôšõÆùä9ïËC×)ßLX––ê¢iãrÐôõ·«7½;IqQkûãÛßzîV_Î#�BéÆÔc½ÎF+¿ï:�¨>ŸiI?_.{üÒ¯ü$ñûyöŠªX7žNÊÛ8ôT9ç]ǦÀãþˆS'b™ ±¤æ±Æ¢1ó1ëPž%{àZ2ì—~èá�z�'&õG¨“+ߨà”éºå)�,sP!Y‘¨»— +Áu¿ò'=z´²7¦p:¡§m�\´;ËúùDú?¡¯Þ*Þ”Ôk•kYOËZÙÐt÷Õ¢’:sìËU„ °…;rJ_«i¹>Z·™ðn·Ó µ1ýÂYæ}ßTÒ&=Ê›mYy?«n_»°—Ñub@Xiñ%x-"yoÓ^_©u™ï: ¢¼uUÃQG¨‘/TnPùü\ ìWå–œ»ÄòÈ£Êð4Ü3Çöü”ïG„~A¸²´'é\&-Þ�<¼„!?Tíüt󸑇öç…Óc~mr[¾)žÿ‡î1­<:–q—½B\—"+–¥ƒ{¨qø+§,o–˜šTˆ½(I8Àu]Å*y°¡-ä—Šq&èʱ­ý|€PåE¨kþã¨ëL½û)¿;Ä…?yÌ—šÝS]ÞÚ_,6¦_yÖ(æ?ŸùÆ%è»h¹�Êî ·E¼ �pÂã*Ê›åOŠDîÈEZ”ÔÉ2<õu’õ Šv».\ý/°*û)IÞ<ø]먇ySßiå­<Áã‘x‡éÇä4ë�Ÿ¥ˆ¶ÔÀUå5Ÿz—böûgá2{1[Š4B-Op…{€oNØR=nõż8P¹Ù&¿ºô[”ÔæêÕƒp H¾äX%çsSÞ÷‡Ê­[ª¦Òñ•Ó÷¸˜pªÅÌ[2 ~皤¼AKúj²²Þì}µ÷jŒÊé«ùÏ™ªKÉ¢$d:‡Èvᓞ– :Q ¨vT*-u�†€€ €‚‘ë˜ò lD€ =¦|P@@@�èÅ…§eíñÄ8ïK°VCg)Á"‡+`‚•™È«ˆ�_ѪîŻǫµîín)¼Ô^¨w ºÍI^W9–CÖ»úi)\û)¿‰FýÝ4•ïñÑ}Ÿ~8�5Ÿf¶#ÁïÇ-�²âµr$š½M±ü#hußM_ÐÆÞáö«¢0T!^,Gœ×È®DS¾…TF{_ROøœ^_¼€õ“ ×V[þD�wæû3"ü¼èŠÐ¯ �]E@µÿ²Ç`ã�Á©$^þ]­®Ë \Ö÷ÖØò7¥uÕ#ÔÆÞ¿ïó½R³˜«{T~˜ ‰hžò-ßç×HZ qzô÷”Sü °DôëXô¥VýôëX ïšjÝ¢¤NT†ð–¹r]k'ÜØ)e-!nùô=àõ=ÿ‹#Áúw:ws„�¼Ë5k óþ3‹©¦jí~"ͨG�¸“Ò ÖO‚YÒ-Wûã[¹8œãré/þH6cððêÕ>¸ÉÇÑÔÎaÝ¡³ÖŠür:ZV§{Û•æ¡qn?ñ9ÿd3–TYÁ7VØõ-Tý ’è±™¹”›‹bÞT’Jöâ#Š‹¨)Hˆ±c¯˜)×Ê,c˜~hdê\„·ð4ž‡qÞÃâݲ+£©âyÂÉf7˜€-,¬wn{T̺²w¾ë¾×!t]¨u³¦…»§ùϳ÷î:´2Ê¥Hê½ Š¯x•>ø™± ¯tÝZ"\p8ÕFjíÁï–ŠJúÂmR«1dj_=Øa êâ&Nƒ8Ò©çºkÐÆo-Hê=mÓa ¦ò‚è±SН½•–K”Â_¬Xºhõcy'—ýu]ƒôîš=…ÁÜÃKv&|u‹:Å€*Œ”�Lùªÿ ‡�úŽB߀©[`Ür§]Õ jÑÊ¿0 &PÉ•‹,¹ÛÒ²ŠêRcæ xNùÆ¡JnÝéþZ� gŽkâÑîÔ†«Ë :ßÝ.ÿH„ä®gûÙ ÝÇÆøùÅAr^å;g„*éò²7QÐÚ2…££2±ÜÓú�–ÿ W3Ñf2ìRŸmV)]c/º‰&ÛøŸç”oL�KF½O§7jÑJMþp‘÷Eº¤¿kSÿq–³†Ëd‰gÏIù—•C<¶›ÿSöþÔ6YUÄÏ/RŠ«|]¥ð’a¡Â›§‚^5›XG³Gõ.ÿÂ{¨AKî‹!®) ó(é_ÐrØë6�@DS¾–KxZ•\á ´î_W’ßm$˜%`®îæ™”Ï,Ä·$Ü.›6™L&…§ë Ÿ¨k\•þúÉ,“…|æG�É^šô½þûžÿÁÚÈC,¢,ß�-ÿjHùOXdÒßC5Wލ*©wIùû[�[†®)’Wå/~ýzvÉ^üRŽSÿá9�Ù~‚ö§Aß_Ÿþ‹™‹¬~”]œkÍ6¡ï*ª¥ƒŽ·¬ñ.`ÊÍWø=!®p¯~äéÌÿÑÇú@þ‡¤Ð�(>~Úxù]J¾•ßKÝ•1ÍiÁ�hÃÆIŽ”Ñ>1$TH% * ¨(   €€ €* ¨(   ÀóÆ/ê”P|Ÿ\e²åßÏêËéWn‡ªeûä½N°ëþMI}é+½¦[žxRåõ+B¥–å*w‚’ßH í§[mÚ§å]ù +毤ô(ÿŒP¥ ?jÑ‹FÓ‰ò…‘Êu}öjò˜¿�®[-´Ÿ¸¶Ï WB=Õ÷: —î¡:Èÿòk_úñÈ* #\ú™^ÚOçœÚgþê­§‘#¨¾×IˆüWÔdïZ˜« ù�ÙŒÒ< ‹\�öh? êû&ѧ)ßEëR(¯|~2\¿ÓfÔÛ÷ùÕ6ù—´ŸÔê'\~<ÖEûV:é÷]çõÓ›)ßÎkj‘Ë›Bh'ÿ_¶ŸÔê§óü´<ûBç¿óúI\‡õS1BÍr#YKR™uÉþÆ-½EË¿ä¬Kg=ja¥¢ð@T.©u*oå6Ù #S³¼¨2?•©’y{vÚ^Ò~\óï*\úqÚ[›\É7Î7WÝü»¶§ô]÷øŠü”,מ¤>ƒÖ�Sþ…®xl¦.++A„bÝWÊ}�_ôRÌ¿¥A¸¶.ñpI™üLðÛ…e=Ž¤Ã•§ï—£ÚžÛ·§ü·i?!ž ‰ÐÞÔ³äº^,~{sJß#qùWä=¹d{�oµ¬×ü˽:BÍŸÞÙÙ(YV.¬ëÊ3\~51ÏŒ¥›�ÿòÆ’ô+— �Ë+Wy¡-¹�*Óºi�Ê,Ó^EíG˜ÿº?¹rÝöãZÿ…o¹‚{×Þ\É–åꤱ‰†no-ÏÇ@ýa]úå GÉ“oÉÖ�ÝRaÇu @ÊŰçµåUž}cKfå_kì«U^~³ßÙwí¢Sy'¯¨ÜÒi,(¬ÏÆü{Ú~Tò/o?!ê'h{s¥r>:•]½>�Ò÷(oèþ¼M{P¯ïL6ZjÜ_"ëb,™‰–ÿ@÷Òâd¾ý^úþ4^›ü‡Ž1¡¶kþ]/àho‰ä_¥=´ï¼ëÁ)}�ZJ¶~VùZæñy¯{L$ÿÊÛrwögy=Ö;´'Oß/ÿ»·¢G7ÿå³nú©•·R¸öº½E;^}黪ŸF¯Ô ¿Ùöá :ÿ ¨ïUèÊ×;ñùO*ý¾ç'BþC�äIÜ[:õ#$z±CÝZä¾Ä0­ü»jss{q¸®t—�Â'�«p9¸ðæÑÞ\ÉKêî·/õBÒ/vð[6Ý‹ü´ #B¹t‹r­�öé{×g~ú(Γ*í!ôö¡ë!t~Ú´O×�œò/loƒé]ï.ªŸõYP“] ÔyþS®½º¹÷^O(馟o?)Œž{­“öæ—I¡ «vƒÞ-V I…Ó*ôÄÏGÿjã}à6Û×UŸbmv’ŸÆåìAË[ù‰}2ÜõñyþƒÖ¿J}f§qÝr¶tòßIýØmo®œÒ®öȇ¥Ðímý¡S}†®ŸpõÙöåø®“uò‹£É•¿é±SÅü{ägòÊ“õå¿Z¶QÞIÕ{ˆ ûršHQÌ„ö ܾñ9¼–·�Bç?túrqÚ›w®äéWôÆ>7h{s­ŸB:’ åV­‚üØIØçÿC×�Çöv¢jÝÅfœ…ÁíK4ÿò–wz¥@¾&WªÜ¯k~äÛû] :åÇ)ÿ~3 AÛOÐú÷KßUèöæJž~cg]y•´ý»Ö�Ó©G{®Ì�=“ò{–qêGž¹+^=˜Úܺq9ÆqòïÚæ\¿¨Ù[îÚç'µ{¥A ›Zþ#l/O6N{óÈX›-uGx…uMßï(„ÈOcà�_?~»°s¸‡ê1û!ß¾“¾,ñûÛ�Šïtç{¡(¶gÐÞ…®–|úòqj:RË�Ä’išPœ� ¤ïù�£|û½Ãöêt -�_ùöNRí­wÚ·Ÿºû\]åGWjùÉŒ*£QyÊÛ>¼k¹½Jí„Îßn“tþ”tjõOûÑ•Z{KMüöÓ¸Ì'r~R«A~Õiû@‹ Bä`:ïãR«ÚOP�··ÔDn?�õŸZ{N-?£”3@_$ýêAú‚€ €* ¨(   €€ €‚¶¿6ƒn•Ö£ëõUš?¹8ù€çPûªÛ#§Ÿïü÷"·ÃËR¸:�j_Uv ]õªÃèÍ{ñn³ç?Tøáj¿yüB\´ÌôNþ×ûø&÷¾çè;ê0ÑŸzÀeA×Y,N+Ø mQR蹚tÒ�Óo2÷e×÷úI-ÿ©å§œ·ò�fýÀ u„ó—{û˜~ßó“š¾×OjùO-?&±H–`ý`®8B-¯.±¯7qýE6§í=~îm2™mmòô +D„Ï·¸æßµ°åíuëÓ5}×í]µÉ�¤ ÝÚœ_ºùwU. dÍ”JûwêT~Ê;ÿƒ�–_˜Ž\?ýgèóqð*~µPƒ–€êú¤„ÓöÞ�aÄù�^{ú’F/ÿ¥_¿]ÔGyN´ÊÛ¾1xhÓ8 ?hº~ê¾ë}¾¨çßU„öߦ~„7ö�u)7^0uÛ?tr>.ˆÑ¤i$jÿ\ÒírJÇ£�hÜØƒðl©¼n5‚¡ƒ$ÿöúÖ³V}º6ž–ù‘ð»: Q?Þí¡§çKã,—ë/]ËÛ¿Výxl\W:Éœ_´úéð|\—ï¡F¨8{)4‘–¿Õž~ÀX/¾üHÒo̪ý’О¾kýtUŸ�òÓ(Áúqm}?_Ê»–\õü¨´·ºG’&¯¨ÜØ2y®~"·d®X”¤X}�ÎI§ôË­\—wú~ÝœJ†ëþ¤RŸZ÷Î…ùQ/oWíMë('~¾¸–·M~µ·~ÕOjçã"µÊ·ÍÍ$ãWX6^úA§‚–·ï¢ÕOOÏ—Îó:òõ½~HÛ€Ú~öOQè]ô=ý¾ç'5¡ç?z�ÿ8ùY¨@’ÚñBÙÕ©u–ïd×P–ÛNm‚…gºRë}íq^WÑæ`ûÈ£ÿ‰Ó´ÖÓµ§¸ÐB:S¾ù鈻€ÔžÄpM§Ã@e%½býw2ÐUyì@ƒÖ¿Vûϱeÿ£[?mjOñ1EE\Ø ]¨u Øä÷êëRðc¿Æì×Ñ-,…o,£âNãì«}öz—~û¼ñ½byûŸëÖãÑm§ ±ì´Ícؽ:BÍ·ÈÆ{à•²f]ùE¿4å»ÎöRØiWé×­e·Ÿð®�C´,¯Vú¡ó£¥Ãò:µãx¾tu¼,\ïµÏ�½ÿqʤßñ¯rO›ÿ°²Ž—Sý ÎÈ2±`ÊØþµÖöF|õ”Ô|o¹°�Ïb{äßïi³r®ºšÿtÊ�×ô�ê3N{�Ÿ/AóïÊ2þn,—Sþ=ê'h{�ÿ)Ÿ¸GƒæÇ¸ÍÐçã"hþ�ñÊ#9«];n§s£1·ÝÖ LßûŽŽkþ�¶¯¼TRLßâDËüøUf´ò¥öàw¾„Ë¿‡Ê€ÑXíëSþ'Åö 9(·Õ%—òó]¸S§úAÁ¨±îTdû¢wǵ¿9GNíÁû|Y©Õ�G@• ާ ¹µÄäÿØLäí{7›_¸SÒuvÐ1�ö@³±£~ìzÝö”ô±™Ð�=H&7úÕ&ÊËú•èÒm 8ì"×�÷KûêÖo¶ÏRßûÏžªøµSµÒÌðBl_Ö¯9ýBn™{Yp®í¡ýù2l©Õ�G~"¡_ýgOUüj™î*Sùöu_ïi›è{þ¡Ëc…Ž}ûE“Zýxä'fè"h^å …úµ TPP@@@+]g R{ÜÍðH¤§" òÓÇÝ¢þá-ô¯Í°}¿¶Gd¢7%™^>’N» —%yý'X-q,lÁ%R{ÓÛw»=â³½Ë7ÿ95t–œê?Áj‰ca ÞÈþ“Ô®?aÍö}ß�¸¼(Érl8TPÿhϵå°ý°·G|ÅU¾•‰# õ?ñ`‘íSÞ�¸bQ’Ç1Þ$/ÌÔÉè­îÃÆÛ ¿ ä] Z¼w-œü©¬Xá"ï×X¨Æ#Û¦=ÇCÜëöÓ˜7¶_äíÁ’iq`ü~·üKË�ÊÖ¸£p¿îÛ&ŸêÅlü\˜ûŽ<Ò¯ü“bý×íZ¥¼¦Eû�ðkó~?6ÌöCÝqø?‡Z¹3ÂÁ_ÝŽŒµ>]Ó·,ÂRi•é7V‘¼¼öEdÝJ­7gûn·G4±ß”T×Q†ÞW„ÝuB^Ÿö+ öúôN¿œŽî‘ònrÂö“r3KíÆÛw»=Bó ¨u§:(§[ƒ æ?5�ž Ô#XÒbyÆRk*©Ýºcûn·GL¼zp±Ä¼u·8œ®¢�*R±}·Û#ê¢XÌP.W“ɤn™=°˜¨ Á²ŠA%ä„N?MåGk²"S�D@],‘‚u.B~¸ï`nÅä&¯¸²îõ? •“À�dƒíÙ�(®ò>«^wUÞ—w—'›ÿÈïšèêÆjèQ]Ê£Æ8/vßå€*|€½C‰d#P–¢Õ>µUê‘~¹¼’/º¾’ÂcɶLS…kÀfûao�˜^½‡šŸ°>æŸMT–ÿ¤•¿º\µß…wþeɵþUך¥ôH߯¼òÊZŸödŸžÈS¾u/©¾#ƾ±·pÝ“wþeɵþ[6Ü ‰„éW–·ñMIòl[’R¹ sÚ#€ÁqþÐ^ìwù0HTPP@@@TPP@@@TPP°Ò> ‰Îg€ VŒã�[(cÊ+¦jÐÉ -N¡ €€ €‚V«|+W3If‰ó_´o_޳Ѐ�*ã“äª}mpá‹N7~Kƒ ½€hZ�P-«™„ßšo?�íI•¿@Rüï¡VŽÿ²Ã^aGŒ; н(©.:Z0CR@úÚ¾z0N´sº-ÊŸÿu<�¦u«¥°ä9Bµ<÷¢~÷´œ`å"&:ÔvÊ7B` ±Ð ]}zS£R@²”jã ²°Aˆ—0Œ_¡[4,ÚÔ|ÜÒ�a•·NcU n<ï¡N&“,¼(¹R�ö«ˆOóMI‡D…o>ª{Ñ� ñ €öú´Ê€dPP@@@TPP@@@TPP@@@Au@å÷DpÂTPP@@@TPP@@@TPP@@@TPP@@@TPP@@@TPP@@@£ÉdÒuè=F¨(   €€ €* ¨(   €€ €* ¨(   €€ €* ¨(X‰¼¿ñxÜu‘ ?Ÿ…1B@AìêW‘t1û…Î1B@A7#Ô9®(´ÇŒÁ]ŽP縺à‡Y.$…* ¨(   €€ €* ¨(   €€ €* ¨(   €€ €‚裂½Ø#Ôñxœý@DþßàŠþIaÊTPP°Òuzf~“&¿’«ü  "»#˜µ®ò'ÒÁÕAyÉ‹ sŒP�•.,Ú�hÑÊ @#T)†§ *�´…šz�€ ¤ˆ w¸‡V¹,/ò÷í*ÿmj%•=¬}øR¸GX¹‹Êd j ’ʨ¼‹Ywk³ñ–§Sýû•W² ¿� ÃS WPã��®­®§–÷þ®‰—Çx )X2H9AÓW/¦åÈ/€ê6&”½3´€Úy�¨ÌIå`«,?DËr^øÏòö–â7f¯qnÕ«}wAÓ/T¦_yíƒûòñòhúeP÷P%swñ:V­ÈdÖHJ(c‹¦.6K¶~@/ * ¦©óˆ^©Û~¼n ¨>[—~˃bÈ6î"Íö ½¡Mù¦)ÂÄ)z„×GƒD@ ¥°LwNñÅ¿„ÒÈ�Bèö sƒšò®–Œ™ùjO'….y]�µ)ÚUòøfü,Íÿ¢ßL¡€‹Ì£þƒ¶Z@5 ǘÊI?,,Ú00ƒšòEï:è6ïvh|¢×’~Ë«.F™Ê¨¡Äyk>Í �»nâÞñ,Zs}EFáÉÑ'3À)ßtÔõ˜í'¥ó³……{Æê‘¯nGZ) ßAQΆ½°–ô-™——·nK‹pí@ ¡†R·ä8Ü›’õËáºûò[ì¡Î5W~«¾ååµdXxtX˜ ɨ“÷¡çßRK‡‚<^z9ú$…* ¨(   €€ €›AZXT §¡  öc3 Rìjþ­l•oh!ú$…)_PP@@@TPP@@@TPP@@@Tðã…Ÿþ*~ó¨ÄAMŒƒ Tb„ }Y‡ËPfxˆ¦@F¨…h:`„U Œ€ À×I@* ¨(   €€ €* ¨(   €€ €* ¨(   €€ €‚¯º ½Ø#ÔñxœýðSþߎ찥s|éO�¦|P@@@T¬t�ÀYyí‰ÖbõBÊòd³/ê.›—çǾ‡Åü@ŒPÑ3•‘£ýòÎ6kD-.-'ËBV eŒPÑ'•AÅ3O6… e)éx<¶Œ8Œ]a„ŠÞ¨›VU !“ÉÄ/�Ñ7Ð2€ ¨‚®Ï<òEÛ;ñHS¾Î$+bêF�#�Âù}U~%ÜòœÔ0h˳ÏúèÄjcRI¹ðyˆÞ­°»ò^ê–çØ3Ó»ÈT®‡Â 2y¢Gz|YÔôç¯EÛ)ßŬµÅÁñž8O�åט҈°óËnžþB‰þ|[�0kä*tuYÞÞÀC�v*õ“¯ÿ|jùÕIí÷¢RÌE£Ráœ;ʯL¡¥zÅnçS»-3?ÿ‡Ó»Êú[Ne\ØW4k½ÿ«îiÉS@Ñê?…Àç ¢ÅlÌ�hPó‡Êé°õñ'òpzLÞÇ7•ùö!kIqɺå9Ô“½ÇsÊ7Z s8ön×òÅ®«Â­à!NKKÕ©  q’��ãñMD]þ�Ô´Ïk>)ûŽº.ëðù�P+¯=ý¾Ø/Ù´Xï R~ˆÖRï㛈Æ×ô`1·[Aë?Ùƒ.cÉÙ;Ï)—ˆßCÅBȯþpýEBDamNš1†�€ŠE‘‘¬pKtbj�ÏM¼ÔÃÃs¨h�}G(MÜ"׈²/r}v‚€êŒYA _$K¾‰=h�)_ý]ß �U¾(`„ €* ¨(   €€ €ÍçPåï=àŪæô mRÖM±w1µó«ìÐ#ÈÐ…Òzž­_Í&šò:Ĥj©}XäEIêøù6í—#½œìã8#fý lHõ:óý:³àJsQR_¤Ó¦Õ_�ÔrdY¸PMí::eTT…ná^[Dn��·ÿN¦pµŸ¥Â.]¯Xã¼ Ù)ñ˜/.°Ë/.©”Ô;W�¿ØÁRØ2¶ âÔ:íÍC]_ÜIfð|éÝTLùö%ë’TQañdã{-Ú7�~­Pp­èêoýWž)ùû‰ç¿�¥ɀKNÐöóê”o¾;vJQÒ�×màô�rçSšRoUþ®}ÂAå½Ç)Ïú¶¬´4˜ú·w,êO"%rͺPO­sõösŵüV‡§p,öÙÇò ·Ì¡¥?½æ]?P1€ú¯.v’õ¿P/FO°=;Õ‚ùïê°/50´ÇfR¾¬ ç#Šíçی˥h å~&é_ÈNRX+ë�ÎG´Ô—åf#É]ÎèJ_&rg»�Ç UýUqÕõu»mDÃÐý¯ÍSpE@­œŒ�º¡:a8 HSu@¾³ÞUÝ« ý¾@:Š5ô°ð� ‘0 +í“ðPŽ£’@Î|/ Y¶{¨ê¬.A"% ï*F¨©=xʯ‹Ò׳U¾¤)õ€ÊðÐ Ý,J’#”z!õ*½0b@{ŒPP@@@TPP@@@TP\Vþ¡âºÍ„[fË·ú+Æ»|³siþV&ûbÆã±ýhæ»×ü–uŸ»¦/Ù×€ó‡ëWHÎt{ÜMªø@#ÔEa¸`ÙÅ<œdæ[Î1åÏë¾.K¹¼1ýº4…ûÊÊ+̼¤N:ä1údÀŠ¡ª¡:]A7ÊF¢–GrÑ*^È7*«Æ|–*ë6_F×¼°~¼«Å¯þ-%­KG’~öõyE…;ÊuéÛä“pùZ3Yý˜¦c]ØXXK�íHVE@Í® …t98u£FpªH®Í£ ×dó‰×•"?P«L¡}Þæ»ö€Z¥–gÌ#3�_ŒäìƒiË–!ŽB„hš±·êºí�¾ôHÅ”¯ülÌ÷höÞ­Íl•ü»Y\wçÔ5.²°‡öÆÈZ •ùr€—ת¼f?i£ÜÀì¥p�+…ôÛO±Ú£©nu¢Cø¸F‡¥„®¡–Çš•±!?6-w@uãTK÷$dH.½³ äã�è+¾7ö*s¢U?ù’fGJ¾úÆ{ô&?j-ÓiäºPHqb9æØÝ^(�\¨…›|Ù‡å[€y…ÞÞeÔ�ÛÂÛ*ö‰èÊð –»›•Y-ÄàÊš±—Ú¨^ÔKêÇTÍ7ºîÈRœÆUHm Ø8ùao�IÉÓ±2Hx³Vx/FR(]º{á*úkÉ”âeyˆYk6¦«uQŸM{Jæ?ýv*é+ókPÍ•‘µü-˽ä|¹â×O]"*9n) HNUT5-×®Sˆ~A´îqšÆ ,,xÎ'˜µ7¢ � Ë÷P%±ªñ™ë …Fù¾Ã¯ÑOäó�_º,_3¥«}ý¨W‘¤ø�{t½à(l/,N››ú®Û¸^ 6NÝ[.ãtÅÿ9Ôº1A"kG÷ât£qÎrý¢,m*‡[½eOGkŒØÉmÂ8“œù2:¥Fd:áP+×þïDzì(?±)ù–ëx(òÕ€zý¸né·0ؘ~¨ÜieöbÆ Ép_òØ¥p{a–ÿZ¾µî@Ùå€*™±lœ• 4örêý³9XùWòϨTö¤�]y‡Ë(„÷#Í•£XÉl¡ývrãN½oèÚ•d…QËüT'yšê�ÖéÜÌ?Ãfr×»ÄT ‚SÿÀ†äF—Ó%pгºn •=ÿåD,!¹°ž¹q)¯÷ªf§RËû}ùÁrÍd>…ÞÜRåUâmn�ÖÕI]Õ=r*Ï¿=ÍÆŠ’TK~ùz›ºR|þG�½’¹@�\~l¦ñÙƒºï;�Sízm\Xë”ɺAja)¦¤\ò"øu‘×èæ¿"YG# 0òî^¸½GfÚä_¸±<ñÊöì´/áõe Kaùö…sª�[¢)úÅÿ×fœæ‘T®ŽýºÑº/6v4uã×–¥°PžSs�3‘QˆÓd¬Ç¢×bj £å¿Z,OFy¬› ‡(ˆ…åP�zy#›@nü0ÎÌUü{N�Uätc¸åÔYû²+ŽÕؾqûüô‰eÆÈãiö'š<¦ €dU¿zP"tÔñX tŠ5µ•á2“ZIѨp›¼òO–/6Þ.nfߘF…Áq  ‚Ê÷ÀCÂŒˆ�hŠAb„ €F¨(   €€ €* ¨(   €€ €* ¨(   €€ €* ¨(   €€ €* ¨(   €€ €* ¨(   €€ €* ¨(   €€ €* ¨(   €€ €* ¨(   €€ €* ¨(   `¥ë ¤b<gÿžL&]g']³‹vNLF×\»|õ5]çB@5æ•h:�£ùÈÚ¡™™¾¼ñÙ³—>½µsrþÉÞ•[®9pÿ¡=o�F!ö8�f³YÝ_whówu÷¡¿™moc6§fçû?vÃï‘“üáècú-3�µ@.ì€~! ^–Tçµµ3yêÌÏ>ö•õÿô kwÞ½úƒ;fŒÙœ>{âÜÇ——¿ê‡÷­Þ¡»Ç}ûö=zt:�ž={vgg'û|¶³³õÉOL￾ö΃‡®Þ¸þÎó'6³ÝÓëf÷¡¯ ¨ðãz-¿Íw~)Ó2çs}Ì?â  &gf¦'Ïÿ†1;Ço]ùÈß?xÏ×îËÿug÷ÂäܯÜzìÇV–Ž)îtsssiiigg'M×.mnþâ�]}òù«_ó–éÖú™'þrgãüüOg7ÌÑQå… Q¦. úM:…ng}iÇuaV/l~q}ûqcÌþKïzßAcÌK/LýãçÞúŽ}ßô-Œ1Óݳg/}úºCß¡›í“'Oæ§|�ýicÛœß27Ýt“°ø £6\Ûa]{P¯(ù×�Ʋ’�[æÜ£Ã‰<1žßK"w…� Š€šo.ãñXÒ^ëfrÆã±Ê$Oèôãfu}ûÉÂ'_ùÒÖ‰¯N_ºsúê6ÓGuó6›Í²h:›Í˜œyòåõ[^ÿºK«Ûçþì›/ŸÌo|ò¢�FGî¾»²€•/”ZÒ%Y¶±×¡°¿kÜÌãj²í0ù•DœœhŽ2ê(ÌÚYk¶ÌG˜;IžN–Bþß-ù^ÕîþûmïÝãñå;ïÞ“}²½s¦}ö*MwwÿâÉNmLo¹ëµÛÛÛ_ØÙ»uýñ›sõÒ¶yñ’¹êî»�Þ{oþ‹u7™|9_.feËLÿ® ƒ©",¸¶U{æ!Ê/J…N¿Í~�Ì3ÙœòÆùãøà gO=òí×Ì–/¯æÝ³:ºë {ó[�ÌÀé‹ö艃×^ûšÛn8ýüÉÓÏ¿`Œùâþáòtû†§¾dŒÙÙ5Oœ5»33þðß1˯6Ë’�¬à•uRù¹åÊuúQ84i?(<¾ƒŸ·Mý´rÒíÞ±Z½ØA>øð{"%túu;êäù™ÑîÎò?¹ïãÿhù™¿Ý|Ý]»Ö#³wÅ3o‡ºêª«²ÿ\^^Þ·÷r¨>uaã3?wø†ë¯¹é†�µµ7YùŽ·ÜñÞ»n>|ðàçïÿÁ³7ÜfŒyþ‚¹¸m–VWo|ßû+Ó·L$tÂéP†Îge6²ÛÑt5g`™�%­#;®4çè—â5©.¹âÎþTy�ŸXF*…ô+oÅy§o¿K§>ê­µ³½ò¹ß[~î¡Ñ™çGÛ[ø'{Þô�Û§“s›Ÿ½´ùÐÆÎóÆ\ñxè±Õ»¯Úÿn¿]mnnæcÍ–õnlOÿô‘çŽÞtã±ë¯{ù¥S·/oýÜ/ý—“/�ßxýÿ÷~iw÷Ï¿ç_ýüŸìþÁﮚ�ñý÷/8PYiÁë*­>Ñ~ÊÔ ‚-{ºî¦§«%�3º±ypûÿ)߯¦Yw=.lyù-³Ë?lLÜ4 _Ê15T·¼:}Çw�þüŒ¦Û[ßó3æè�Ƙ½+ãëV¾{¶ïÔúƧž»ô'3³;ßvïÒÑëöýhõ.¿]moo—?ÜœîüÉWž=tíµÇ®¿níÅS;k§Ÿ9ÿÒÉ—Nc&'_üÓ¿øüëßô–Ç^XûäÁ›N}ðÜ~pÏǾçÃåÊL–ðÚ(þbQ§ìYøÕ‡VvÒ`ü.è+ç®*·a>&è»|ã¯ÎpJ?Û¸/óÛdý]ed•kÆצw½}ë£?7�¦¯Ú=¿2:°4ZÍ>ØÜ];¿;5£}FÉîlö¿¾ðð3§Ïî?x`íÅS/>7Y2&ÿ4êtg:2æñÉäÔÚËËKKßöþw8tPkï°˜¼¢ëŒhÒšø�Y-ö1°„6®¡:}é:šÜW$z(¿ºå æ eàæ²N×¹+P�÷Ää3_|ÀóÁûîùö·¿)Vñcèä�HË:€Aj¼IìqùîÇoÖ·Û”Ñ/ý¨��"x,4(ß@U?+Ê]§¼÷œMŸÍ^>½ù·ÓÙº1æÜö“f¶{lÿ;÷.Ýè÷jüy4]{ìÑç¶/ÍïÇΖ–wßó-¯¹ûuÏ>þÄÚÅ‹g.œ¿ñèå×Îfæož=eŒÙÜÞþÜC_ÙÜÞ~óíÇ¿óoV¬œÈ,ƒÔhwO³€h¡ßåë÷Ø[¡ø’*zªâQ´ªð²±qx×2£�®[c>*'ï/Ff:›>snû™sÓ§³/ìœ8¶|ëhäsÛ{¶³sêÇòôc�œØ^ÏV O_sû�7¿ug:�íì^}èÐókg÷¬¬\}èpö­íéôó�|س=�þÕï�¿°Ïê?ûð{öï¹¼0êüùó�ª%´Ê1bü=…÷ ż˜ðˆ¦-/j%WÿºG��5æü§|ËA®ÜªÊ§�Ór€rú�÷Ÿ„éçgù»…UHÞUäk×켸¶õøöîycÌÅé‰C+ã½Ë×üñ~zº³™môâÙ¿xbòÛ++{Ž_û¡7ÞñÃËË{ë’--½¼v¦MÝ~Ç�÷ó³ëëfw÷¶OþáMŸýÌo|ã™[ï<}áü¥­Íë�ý܃�¿tiÿžÕù‘÷¿î¦ëC”3Л’ì{ì6š.ŽÊsS8énB+è¬2Í —jã2°rÝÒhϹíóÛìßàõ×~dɬnÏ.\¹ùÐÊñ�é#ûW_oIöè½÷¾ýWÿÛl:]=sðâ/Ÿ8yèÈɃ_÷§¿3ÝÜxfûÒůÇôøm»³ÙS'žöì¹Ñµ7¾ùØ�ú¾�ì]u›Ã°_ue:‰(•3( ƶÈoxH§ŒŠù—¼ÜMEзY¡wÚ®òÍbŒeÞµÍUyeú~é×¥\7±ÜžðÜ™ÙöîcÌÈ,ß¼ÿ]{—�c^\yÁ˜G²m–——ž¾ø¿wf—gq­?°w;P·§f³Ý=«W_=søÒ/-í½ñkßµçÏ~s{sã™íKwÜuñï~tíÂ…/>öø¥��Ñl÷ “'¿m|¯<šæ/5:1Ø+ß\9Û¡5d¤cµ“Ï´û­%Ž¿ÐŒ{([1-V!ÍYò¨ô2áÒoqvêº;ÏÓ{´¼kvŒ13³szëˇVŽïζ÷Ø1Æln­>òê­í•׿åš;_÷âæŒÙ5Æ\˜>·3ÚwìÀýY=ý+‡¼æŽ›¿ëŠ„ÍÖ��ßYÚ{Ü,¿ý«?ñ¦ë_Ý^ß·ó?{áÆëÏ>´nŒy×þé5×ü½üÓÈL¯ºøó«oyæç|áS_zrëÒ­ßú–·ýÔGÍúÿÛÚ=ò™;Ÿù…_¸neïþ¥cÌs[ïýõ_;pü–NŠßrÖ/¢Ç¹úî•uyÑônœÇ/õãMIA%xnŒVn=²rÇÙ퇳OV—c6vNoÏæoT˜={éÓ{—�­ïœžopîÜôÐÁKG®Ú¾îª»î¾ù}G�¾£pp—§�.ÏN�ö}“¹qòÌÖú½?ð�7þÀ·Î¶žÛ½ôðæžï{þ�ëæÕý+¯¼5bjfû® ò¨L–9û· ·B#ê] ÷.È�j¢v¶xîüolìžÉ>Ù¿|ýÖîËóÕ¿e«Û7ÝvìÝÆŒF£Ñltt´ç-…§höoüÁ�¥Ç–�½¶»¹yê¥}×ßÝüêîÙOoîy÷Ó}ÓÓ?ùS7¯^þ‰ÓÍÝ�çí}ç'~¿ë:€>a„š¨¥Õ{n>ü½“ó¿¹¹»6ÿd}çź�¯Z½ãÆÃß`ÌÈ,­Ü:Zº¶°ÁÈìì�þÕlvnzò?›Ùte´2}éˆÙ9»¾çCë{>xäž—o¾åvsòcÌölwb¶ïþÑ뺠g¨‰�–V÷¼é5Go=qáW/n=T¿áò5>xÍ��FË–Ôf³��Õw³<]¾ey÷Åý›´¹ôæÍýß¼;:bŒÙsôèá÷½çüÿíÝÙìÄÎÆí?ò#׿û›º®è¦|ûasúì¹Í¿ÞÙ=»³»µ4Z]Y>z`õ®ý«¯[Rú¥ñÝ çŸûÎ�~uº~÷Oÿ›ëÞùή‹ ýC@Åeç¾ø…Ùž=GÞpO×€^s¦}*,8Ÿ×TPP@@@Ï¡¶öù§.ÿã¾Û[m`1Ñ? ÅŠ1¹Ã)1€C.)°fÇš·�uûVy{hñ;^ußåH¡;É�PíÚmÑÊ ø© ÃóχVéÒ¶bLÕ±Y�ítîÆdùõªCí‹Ó×þxI» ¢ %è›,pbpöŸ=�¬è9* 4î¡–×øXæ� ­ü¼rÑPáî&Iê "©–º¯´)¯¤ò+·Ÿo&/T8/a²ß i¼}ãÚ„Jù©¬ÏºJ¶T~œþqµ¡~þ)ÑÁ65�¯w½y¡\uÅï*?–…Å�1fvìŠÿ)r:^–Ïë:ÓòWcmË&4l©�ïH^‹ª}pùù§Šçð}·×6Çò–å½tqË9±Ÿ]uqá–�åµ\)—+¿îëòL.¬Ù1�eJ’Išá°F1?–ó7)£³—¯u G§îÞj›ú tŽ„îÐ�)ßü¡µDͼDúna€·<‘‚ø±\Ç CÝø²®·-|ž}]+¦Î «½òó¬É)<�ó7§ã•�©’í�ð|§|-J–\]‘¾ºòv~+·.?�Ýb/ª=¦r/¨_±*�»äžÓù»8R;ßÑÑ_ìP¸î¶�rz [ûˆX7µØ’ÇÄþ"pªáÊÙÝ kP=Nø,¦ϰ8*gS-'‚w(�Ï ¿ÞÓ€my•˜Šîtñjå*_`ðî»]; ³8ùÿYwú€¶Ö#T×Óµ¼� Åé&e賃³ÐÛ’€Ž„ fÌý]ð ¨~Kûûy]y»Ê|c~y¨P|®Q…ü�źO Ÿ÷âìëP›ó�º]<#ÔÊ;@’ÛB–${¶E+o·*Ÿ€ ýš$×f¨IÏߤԽ_ëÈûz4Z½%~€ÕȘ3 :~¯ ’|Þ¸—6#0ï{WZ¯FòØ‹S~$¯‡ðÖ£W�ðëâNoD2õÍ@øæ^Ë6’,5~W‘_ìö�™ú×öæ7h|ǯ]Ëþѵ¡Z– £¦$¥ÓVÊ™‘7yšý}QFj,ïNÒšì­;XòÕ¹’#[xíQãÆý]\whÂ2§UÖá*0ýC³ÀjF¨€²E˜ã�/Vù €€ €* ¨(`Q ¡ àÿ:*±F'IEND®B`‚urwid-urwid-69907e4/docs/examples/edit2.png000066400000000000000000000210311526073734300205570ustar00rootroot00000000000000‰PNG  IHDRp€î¯X‰ cHRMz&€„ú€èu0ê`:˜pœºQ<PLTEåååîÍÍÿÿÿÿÿÎí›bKGDøoéÇ pHYs‡‡�åñe!tIDATxÚí�~´,Çåá ížàé ö³÷?ÓV!! A�:¶ùí¾ÏtBøÊ]—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹\.—Ëår¹(fÝíÇãé#ÜíÇãéÃÁ|×Öeù@HPÝ ü(}×ç‹bù¦ï“¡jvZŠ”rIÿ]Tƒ‹2+)‡Åñ¨Î‡íd„-pˆ%†L1BÇQ° kdn/¨ô%ÕÈ?ÃO¶Ô�\de }²9# 3Ž"`Äœ‡ÍT€‚~Qƒm”à–-ÚÀ-Qæ3Tß%¸ ÁeSqi‚«¹ pߌâ’ÿO'\ËŒ<ºª®Ë|‚v\çBi¶f¬„’u7¼ %½¨ÀYIšà.Ò1p Iƒ•«)HÈ…�Êè6Lƒû>¿„7·ù¼ßÏ… ŒÌ¨N(@(Õë›èeµ™± Qµ‘ƒ+Á�ü< (p°† %zÊ'4Ámã—­ÛǪªîµ·ò ù¤ ”>C±~çY6 |®Mu(²Îºütñ"oHçY” •ö˜›qÁ’ ²“‘§ V§½Õ™­1/­·lÓRË^zɨHñð¸%{¹%\ÀÔÀ—ÎW�ƒjàR†óU%@�о ®|JpŠ �ËbÖ¸Õ_ˆ,²žC¥6)²i­W·PÛ8Û¼"lX°Ì5ÁÉ68‚r‡àÊñRâðüVJ³�©Z‚4¾°ã¥m\*�YàÇ¥›ÕØ òY˜·u‹C Í †­(ÆNG$¸ìudösOt\¤ó‡ÀáUS%tù©ÁñŠœ²V Gx‰Ø¾†oÃ&8¬¸4ñ€­2Ä9pXå*p:÷ŽŽ«~3ƒ[zàÂRê[úž§P‡¥W—%Ù;mµqc[2`P»v\Çg—¸äpà5Ov¹|±ïA‚ÝÎë,dVõrKî£Ù¯ž«nõ™�ã0 l\jæÌª*CQ:ù»ÕÆõÁéhé N-iÜ•²JƒOerJº#‹¬OúŽ ÜÖÁ¡'À-ׂSUü¢4ˆ¶?Šª3|Z,Ap¶É Œ�,KúAƒíwšìŽ(èb8b�£ã�áH=¸�à²7¥f)p!Õ«R3!Íá¨W�9ßÌyšn0pòÛ€‡~CÎ |;àäÀ6,l`¬ÀjôiæP pÛÁÖ*P> w™fb Ø1C·’#pr®šÆÇ¢‰Ë'!¦!x¸cy®´TS.ôÁé)ͧqʅ͉žšád˜ìâ…ÑS. .XàÒ"P\M\f©îVÈ1¼Ñ‡SUŒyð�5FW¥('÷øY�Ó“|Ä“G8e�®‡بmÇ–Þr¼¨3ÌG.bËÎÁá8—ó``ž ãÊRyä1Tî�é…‹éUG×®€CÞù<¢5Á® Š^=àRQA�ðÆW#ä7ê0„ˆ@)p¥Èëu+ë²~§Àaž4úé<З9p` 5.6®=í_H¾jt£t›­ çõî\¾@a¦r+›þ�à^+Hî¨äPÀår¹.’œù²•QJÎŽW‹Ï¥¿ÛŠ*N‹f,¾´"ÆÎ÷ m¯,:¨V˜&æ€ç¸�NÕàÊ2‘Úš ï‡®\›:E>Ï‹ 8»»Ðk©PÒò¯¾¾m�Òþ„½íŽríÀ£b‰º Ž/øé%íplÙk]ò õTF9šLðe=زõcà*3A�«ÖÖ&£ª"ËMÊϼæÅÓ¯Á…önܲÔëmûયQ”`PŽZÑ8Z)€p¼Æh`À’µÁéÇe§R+´%ámúƒàtU<å{UâtU Öì‹@mE^€ %ÉP�Kw«ZƒßwðrÓÝN�à¬6ޝ†ê¿6[KP>7pë ¥V™Û—/žÝ§JŒŽÙ«Á){z«FˆjÑ'óÂÓ…(+Š \ÉRÈûD8¸ ¥.Š6!æÄ |Uྵÿý08Ö‹B¹nÅ�ïò€·‚Á²P�£I\Ü:Ù’Æ6m„†Ê5u?°'«ªlË{³_ ܲ7Ñ‚Z¥4Œ�“Å´7oA¸R"å¨ r3o OÇ|ÿ3– Qe6ª.¯#ܲ?Ž[òÀ�_:æD±¡™9hÜ'pÐ8¸ffô·2�CàŒó*­­€˜9PfØ2µÝÁ…p|-¿îU�Œn­§Ø–gM‘d–›à*û pMû[€_9\PP-OÊ5�ùN?0P \nÑÙÍ’•†ž·2Ê:‹`�‹�øF¾­óU n–è4²PàDU…‚Û†#pbšÎÌ(ˆ^6•¨Ip]aµÜi R02¯J\¹¬ \UAË_\l�[òl–’߯±Ã·‚š«#¹[n‰¸¥aX龨åÛ"[]Umpœ³—÷)î‚c$bL;[êþ¾›Ëœl½eü\Ð-½‚Ó·5ŠÔ]Q&mpl¸TƒÛ6ä(pA‚Þ5JœêÄÌ^MæD‚«‚«¥G}þ8¸4ZaÃ4`¡"^)Ñ»…(“«ÀÅ&8Õu!¸z%šp|Ø¢!ÁUç«ÑŒW¥�„2&Á­°qp û† ÜÚ­‚ް™Ýzø]ûfU�,{,ó¢±l§6N69ò Gk.ª"]Àñ\ê«€¯ÁÁ"«*_…Sà À�¦\d~D^ Ú%N|˜àø�7sZñmpU�ë’{þ±Û)ý¹%�CXàø&Csu„Ö\R,�³ªÜJ ÄbInzõܲ ò$¸oŸEˆê{ÝÆåŽ¿5sXòÏu#%¼û³À�“)ÿšàÒO±>&À©øuÆ•ýcàB¹·’–,Ô÷\ ±<7D=UáÈÀ±˜fÎ¥µØÀòÊ‹uW+Ú¬… )¶à¢ï6zeXÚç«6N}×m”c˜Qì6nÁƽ€ë{Š™® ;Á¦.ßcˆÀÈœ¨CºnŠ^'¯g4âkp•ýƒà~™à´—Ëår¹\®gIÍ«]:/ümÈC•GÇMpyvÚ“ã?* .}up]á¢B,‹Ø.òËçlêÀѪôÝÁ5DàÊêFœøø»j€ã7Œœ¥FUåë–yéJ„ÅÁõÛ¸ œ·q‰Ã²Wó½sPªÆq=pË‘íz¿X½p¾ãàzRàð–…¸'"ïbüiÕ798P7“©®|¾:ÒíÀ,‡‚÷™ªÀUå¶÷ms‹G¾Å¨áGº¹Oß\VÞ�â€Øs Ö0ÎÁåý0ÍíNâ‰E唃ë�Ûvòçß:¸¤üz¹E“ïUU[Ÿ\VL?ô“›BKã�+#ª·ppK¢"vg‹ÁÉBÛ*éÐú§ƒK‚»p¹\.—Ëår¹\¿I‡o ªuxñs¸+ìï¤wÖÔDÚí¹hãב{ùP·3Âî ²£öÛé…C÷áBgU»w¾ N¿F¤Nü¶1½aa/Gí·Ó;vÿ²zÈÚ.8e¢ ôbç^ã1ûÍôÞ÷í¥cœ7î©fp`ž—ßK¸�Œˆµ¼¸cö›é$~%8ýsþVÆìšÇ‹™õûðf}•É’ýVxãxˆbé¿~\ñc\´ÏWàì4ÕS�jpÍîx$²ß oMÍîs$FÀÅÖyê„è¶\}ا·½³XR©; ùÛR}~äÇ¢í û‹§\¯Ò)'bº/¡òƒÛ‰%[ÍøÛz|.  _°›ÅÛ÷ù†9¬Î—wEõ¬åb#P>b~ Ð÷rAxºÌ>ž¯.`#Á�,`¤ÃýbÇ1xä°ðÕfÊõE`Xt°GÊà2 ã<ûŒ²ð³szVdoŠCûü|0ìãùb_Œ¤«tèDNèé=*îK7¦ÿ>KO‡�ä^¹üŒ(ì�ð“«óZzúЧ-±äñ²Ãö´Aÿ1û¿Ø{éP~’N¦#üªÓ¥x­ðìÊ1 \€ú<¶ëâSgˆuŒ;à„ãÚ>Ågö÷Ò1Á±ó »uº¯^�K÷M-Cú¼ .ÖŠ Ï—†30€žq^o¥Sj� ÓÑvUºœž9�­ccbbç‡Á-pÜqXºö{é4Á©t*{-p­ðœ:™†�8 .˜àxôW‚cœt[Úʯ ? YnU‚úSd(‚<K®8Å+mûžý^:ÀRª2KDz'ó»äíðdœ<¯{Â:C¥Ã«ÎÛÇe‰'ÓàÐo 6ÄEúo¤Kñtø&8ÞPRƒ öy]šø0téâ퉊Ç;+ÞH:ì6É"=žù¿ÔùÅxUx��©2¹�”«óe&�ÎÉõz8Ù`#sLˆÅì8ïÍõùê{+:iÀš€¨tÈž¾h4þ„Fú¥HãÕB£!–ÒÉÎÓ\Ÿ-¬æp¬ÁŠü|òª:n¤Ûþl¥³ÈïX#t:̼tœ@‚^ÝÈEš½—>ßS+ �ÓÇít§Ói©²�3↭ž?eˆu7m�ï{hj‚ãé��‘«ì³‚â@ø]Ç¡ÑK½WK!Ú9¢6ÌJÆí÷ÒÙÍ'Ôñ[évýÁõ'ñ¹s¾›!XÌ`­øGí÷ÒéåSšèûՖ꽪wgëó#>î¥ÓKÿ‹#¡kûa7]ÿŒËår¹\®¿¢Éߣ÷f Gf�TkÿXßYÏîœ?�¼ê…>Êt"ö êäøý9à 1YîgÎ�oĺȨš«C1Ïœ·µ¦;ñR"ƒßå² P†hòéœo�­3?ÖÛ„8ü½µÂÚIL»£çÇc+éÙÞÆ2„këiÑ]//é„ðŽÝÈà÷ ê{ Ênc¿‹nï“ûöh_^>6üVdw— |–a„îÖÊòþEÜb/žò.Ç…Se·±_®xaïSûöh_žéïÒ.5Å÷Uà-<~§§Ù†ÄÇÞWïo¹¯‡Ëæ�Çns­�X¹ ׇø0Æ[pYð—µ9"PIKöSØÜ[8PÆ…ß]pPÎä›=À‰„ø1f¿X²\¼úó"püf�'Î'±Û‰»ÃR8×ì×|¾8}Ç7ïƒkeè-ûÔëÁá^`ć½ÝHm‘û#àb”U¿wÁUOQøkà@�ƒ�6NXhuwï N˜›'âŸ÷¬¥)œkî GèÃŽˆ¹*¯®U_eÏ÷|805ÀA\´â3h!Ê6.ÿ€�À€ñÄ>T:S Gà5Ÿg%§Dlêà’w„ ¸W=æá׈u¼š^õ ”ß+¼Ï.«j{¿š+«lÞ’à.Úžý{Õçݾ ¸È†#xÌñ5Em\ààÚûÕ\Y¥WM`ý¼6×Mò!µqåÇl鼃s¹\.—Ëår¹\.—ë!Ú~ é:.7)7)7)7)7)7)7)7)7)7)7)7)7)7)7)7)7)7)7)7)7)7)7)7)çr¹\.—Ëår¹\.—Ëå:/|Nëî‘àîMþ¹ž;¸éäZYÁÁ=ܽ<ÕïÛxªß·;ðT¿owà©~ßîÀSý¾Ý�§ú}»Oõûvžê÷í<ÕïÛxªß·;ðT¿owà©~ßîÀóÛõÔ+w»ß·;ðT¿owà©~ßîÀSýî9ð}öG<Œpìñôoîh†fõûÀmÿ°3øŽ¥V¸ß.ÄA$®Êç�,Ùƒ~¿^on8�A¿_¯Qp!¦7Q­ÿ[#ĈïeÍ/ŠrLÀW»ä—¹`| #³{¼ó~?p�7éÅ#¾# $ a{ý�SWâGú^Ž_à÷Ýà‚½}*¿u-_[IÂ÷³¦—TAUù+HùËÓøkçbäïu}:¸F  üfpt _¼w\ùï¿ß´À ²d†ÁâðèíV�þ™~áásÀå÷ž!¸éÈ9p“o'} 8jܱ�‹'Áa/ñÛ;‡<þ pø~ÖÓàfßëúvà–f\ÐÆ»¿\[/7·úòdp»m\Ž„ÖpäO€ ºªæ÷³2p �ãb(xäïuý­àŒ^UTÕÅžr­}�œZ±©V$»¿Ü¢Çq‘Æs-7(Ÿä³? À—ú};¸Ó³Ó©›ý¾Ý7kÚÁMšvp“¦Üêv¿owà�ùízê•»ÝïÛxªß·;ðT¿owà©~ßîÀSý¾Ý�§ú}»Oõûvžê÷í<ÕïÛxªß·;ðT¿owà©~ßîÀSý¾Ý�§ú}½áâ‡OýpW?¶ë¯€»úNÃßwq~uÕÁMÚsp“öܤ=7iÏÁMÚsp“öܤ=7iÏÁMÚsp“öܤ=7iÏÁMÚ»œkR^â¦ýrp“~9¸I¿ܤ_nÒ¯ƒ«~UH¿Ó„¥>NOÐiÚ{•ßÇóùbpò_y\†ú}/=TÂÁ!q‡/€ý“jñ;ÿ={/óûx>_ n£!Îg4êÕàहê¶(>Í*pkÌ_.µ=X"òóâv좟ÿR�oƒ‹�õs0"vH~^8hƒ‚Rµû<²4Àé òpõ3 ÄãÌðyqô8Œp D\lÛ{cpÊq¬> àñpͪ Íç�<\zĈ6�§?çÀEðR[üµàjƒ‚GµÁ­.?fÑvŸ .\€zÊÏŒ'ý̃kUÕ`m¶|685AŠ?.>œÊÌIpÍ™ƒqü p¡ôt�ƒŽôÁ•ü"ýp�ºÀ8 N­Ž´À…\Sá±ã8-ý¸²¸12åZc=NFĵx¬Ü/Wžó&Ú¸ÒûöÀÉnºŽ=3>îÛ{ ¸ã¯vðgü>î—ƒ›ôËÁMúåà&ýrp“~9¸I¿œë'å%nÚ/7é—ƒ›ôËÁMúåà&ýrp“~9¸I¿ܤ_nÒ/7é—ƒ›ôËÁMúåà&ýºœ?”j6§þ´I‹×rkx÷ Áý¿owà©~ßîÀSý¾Ý�§ú}»Oõûvžê÷í<ÕïÛxªß·;ðT¿owà©~ßîÀSý¾Ý�§ú}»Oõûvžê÷íü1¿]O½r·û}»Oõûvžê÷í<ÕïÛxªß·;ðT¿owà©~ßîÀSý¾Ý�§ú}»Oõûvžê÷í<ÕïõiŸ�”ƒ{,¸[“®çn6ù»«Ü‰ÊêàžÎår¹\.—Ëår¹\.—Ëår¹\¿\gÖ¡¯~`ÆÙ¬D|×÷îä­�ôz#£ƒà‚µØ?.Ä—àMw´‹é”ô.´Óì·2\ .ìûu!¸òâÛkÁEuí�‚=X‡yI�®k ½ùz!E,ú§åÿqpò 6”®èeAë�ÁT¡µÑ~_®Î¬ýÊ ž—}pa ›‰ÔTQ›µ¾‡ÍΗù:^~˜½vº×ÛéwT½U Ûªò.œ‹ÀA¤×q[/ïÙ— û·ÖÀíwasö8z›“î,΂CDXUñõQÁ…\õƒŒT/ÕGé·ÎçâÚYæßAzj¾@Mû7î»Éïøcmœ NµˆQ–LþzÏU¸#àÀHYøË Ïe– ¶ôpñ¸ïF±J�ù‘Ê œ‘îqp I€úÍhgÀÕéƒ ²-hözxeeyWàöG¸SàXFvÀéƒ/⸿ò-p!^ .¨�\{àt÷ð*p¥] ƒàX]­òŽàÈžÈøÛ‚›iãzáâ¥ààRp:±�Îaªªþ 8»�Û7¦wžßÞ*lÃà–g·qÊ;؇մµ\Uƒ±ñ™%N¥3ò–ôzÊ5Ðç‡sV6ì53Ôp¤NÏ�µc‘_¢1p KTãü‹À…ˆ¯WÇ)×�0ˆÓ4å » ·¦t¯—JbédήŽà?¡NM¹4¸2ɇÝkJåñbp�×€›P8oBäè¬wcï­­vV€¯pMyv™!¾¨pÂÊ(�_.ùuö‡D*þŽ9yª“îõàâuà^¡«~Ðõp¯¸År¥{×Ü>»Ü{¸7çr¹\.—«¯K÷�©åÆáÁB+ØÉ9Úøö²aj�jg=_-‡|»²ó›ø÷w`{Ùp:CàªE×äI\ •£hš˜÷û,8¸ÜÐ6¨P�¨cg—í÷7¾Î7\SÇö�ip¸þ½‚[Ú1&2þ"pÃHÆ›8°}ï�ŸÉæßœN¼±²<ŒDÇבp©{pÿN¢é¿Ap~\7~j0T ÛêÔ’¼aÿÐþ1ì>CÞùG= �³ožÚ)8®»54 n$ù /œ²OÛ²÷V¬Å|;�÷dÁ•MQA&ß\ªî+[ñÙéEß4jY•{˜[ékpµýƒà°zæô88c�ÇBÐï4&Á™ñ¹[r#bs‹C”ù¯·fȈßkûzëølùÉq\ퟜìöã[àx Àín±}¸…öE¼}�Àµgø9r�•€¨¿ïÅ OXO0̾wGÀzèMû‡Á‰Él©ªßucæcÜnüíä(¸n™·Á-'Á¡C,{©�ƒ�!óÒ(1ãm\¿Ä©Œ±¶XmÚËÁaÛ�m\wÀv5®ÓÆAU•ŒaÇ9pãUÕÊõVAìÀHÑ=?Ñ9¨±euÄ™Le¿ GšÛ ªM-Ø7ˆít¸`„ߎŒƒ«âW£* ì*ÓÛð(Cƒàpâ“GòyKÓZMaÜõ3µÌëÞÖp”ßCà*ûeÊÕÜeï�£¹cÇåÞÁwý\µZ�| µ•,Üî8Ž©Æ¤Š´/î¸�ç«9FŽGV~ œ¿š„Ã2.ıWÿ( Z HºQ±20¡W-+Íùu V?è.¸³û×^nÒ¯ëÀz{ÿØé»]Óàpì\çü:®·ƒî’Í=�Ñuy ƒ�Ì/Ñ_*$.—Ëår¹þ˜Â¹‘ùŸû9äÝ®¿Ê¿ÏÑ¥þ.Ÿ_IýTWýã.œrä?ÿ[õßüÇÏÛYÿÚ7óSàd:mpsþ\®o甿¸¶% nèË� ë?æ$ì ]'£§R7ÃÁÉè7‚K-ÝúÏ÷¢MÉ}~ý[¿|¦à%Ügþ}²ß×ì€C;Éìç%n¶ÄmpÙÏÕH2¤ü½ Üzú�Þ¿p¹/©Á.æóësÀ3Nµé_Ìð¿’ñ;ÃmpÙÏ—ƒ[i¥"÷Ïmзϭ4²CìKÞÇÇ1pª ||®Ÿ%Óù‚Z%¹Ù9|~åÚƒà_Õ«¦ê÷ÁKŽG%á㣮K y­™þÏú·Ê°È<�3óLv>ª ðCàRuø'Ú* 5<ÊÕ ƒÛò,ÀýcçÎý‡þü8¬�ªz¸Æ—Ï/J?·¥pì~~\éÿÝ Nt¸_Ø‚|í€Ó�à àî-q8ù¢6÷uªï }jµq?.5sdÁýÛ·ãà>dOú�õà]Á2�áû£39 NOÎiü–»�ü¶Á•áÈç‹Á­žÒÌA�ÇöÀ‘[3àþ«Çqè ´ \ò¯e§9þªÀýû8/5å*‰}PA?øÔêKv&jÊuÜÿª�kéU?8L· NÍÝÊÔð_ºÊßËÀ%ƒŸ8½)��‡“g=»·ÛÆ.¸ÿHne.ÿ…s?ôb®Z­g?×R Á�L¥‡Àý^½ð®ˆƒ›”ƒ›”ƒ›”ƒ›Ôï÷B9¸Iýâ™�‰ýÓõIEND®B`‚urwid-urwid-69907e4/docs/examples/edit_text.txt000066400000000000000000000072771526073734300216140ustar00rootroot00000000000000替洼渎æº�潺瀚ç�¯çƒ«è™«è°ƒè¾¾é€¯é�˜é†‹é•¿é—«é˜šé¡ºé©¼é«“ 공곽껫ë�“뇽늙등ë—�뛴룸많맹뫘볶 ტექსტების დáƒ�მუშáƒ�ვებáƒ�სáƒ� დáƒ� მრáƒ�ვáƒ�ლენáƒ�ვáƒ�ნ ყველáƒ� áƒ�დáƒ�მიáƒ�ნი იბáƒ�დებáƒ� თáƒ�ვისუფáƒ�ლი Control Characters:  Tabs: | â•”â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•— â•‘ • “smart quotesâ€� â•‘ â•‘ ╭─────────╮ â•‘ â•‘ • euro: │ € 14.95 │ â•‘ â•‘ ╰─────────╯ â•‘ ╚â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•�â•� Math: ∮ Eâ‹…da = Q, n → ∞, ∑ f(i) = âˆ� g(i), ∀x∈â„�: ⌈x⌉ = −⌊−x⌋, α ∧ ¬β = ¬(¬α ∨ β) ♔♕♖♗♘♙♚♛♜â™�♞♟♠♡♢♣♤♥♦♧♨♩♪♫♬ A😀😊ðŸ‘�BðŸ��🚀C中文D한글Eã�‚ã�„ã�† café☕naïveðŸ�·résuméðŸ�¾Ã…ngström 12345👋ðŸ�»67890👋ðŸ�¿ABCDE👨ðŸ�¾â€�ðŸ�³XYZ test🇺🇸flag🇯🇵test🇰🇷flag🇨🇳end 👨â€�👩â€�👧 👨â€�👩â€�👧â€�👦 👨â€�👨â€�👦 👩â€�👩â€�👧â€�👧 👨â€�👦 👩â€�👧 👩ðŸ�»â€�💻 👨ðŸ�¾â€�ðŸ�³ 👩ðŸ�½â€�🔬 👨ðŸ�¿â€�🚀 👩ðŸ�¼â€�🎨 👨ðŸ�»â€�âš•ï¸� 👩ðŸ�¾â€�🔧 👨â€�â�¤ï¸�â€�👨 👩â€�â�¤ï¸�â€�💋â€�👩 👨ðŸ�»â€�ðŸ¤�â€�👨ðŸ�¿ 👩ðŸ�¼â€�ðŸ¤�â€�👩ðŸ�¾ 🇨🇦 🇺🇸 🇬🇧 🇯🇵 🇰🇷 🇫🇷 🇩🇪 🇮🇹 🇪🇸 🇧🇷 🇮🇳 🇨🇳 🇷🇺 👋ðŸ�»ðŸ‘‹ðŸ�¼ðŸ‘‹ðŸ�½ðŸ‘‹ðŸ�¾ðŸ‘‹ðŸ�¿ ðŸ‘�ðŸ�»ðŸ‘�ðŸ�¼ðŸ‘�ðŸ�½ðŸ‘�ðŸ�¾ðŸ‘�ðŸ�¿ ✋ðŸ�»âœ‹ðŸ�¼âœ‹ðŸ�½âœ‹ðŸ�¾âœ‹ðŸ�¿ café naïve résumé Ã…ngström ñoño façade coöperate â�¤ï¸� ☀ï¸� â­� ☕ ðŸ�· ðŸ�¾ 🎉 vs â�¤ ☀ ★ â™  ♣ ♥ ♦ 人人生而自由,在尊严和æ�ƒåˆ©ä¸Šä¸€å¾‹å¹³ç­‰ã€‚ ã�™ã�¹ã�¦ã�®äººé–“ã�¯ã€� 生ã�¾ã‚Œã�ªã�Œã‚‰ã�«ã�—ã�¦è‡ªç”±ã�§ã�‚りã€� ã�‹ã�¤ã€�尊厳ã�¨æ¨©åˆ©ã�¨ã�«ã�¤ã�„ã�¦å¹³ç­‰ã�§ã�‚る。 모든 ì�¸ê°„ì�€ 태어날 때부터 ìž�유로우며 ê·¸ ì¡´ì—„ê³¼ 권리ì—� 있어 ë�™ë“±í•˜ë‹¤. يولد جميع الناس أحرارًا متساوين Ù�ÙŠ الكرامة والحقوق وقد وهبوا عقلاً وضميرًا כל בני ×”×�ד×� נולדו בני חורין ושווי×� בערכ×� ובזכויותיה×� सभी मनà¥�षà¥�यों को गौरव और अधिकारों के मामले में जनà¥�मजात सà¥�वतनà¥�तà¥�रता और समानता पà¥�रापà¥�त है। ABCDEFG 12345 Hello世界ã�“ã‚“ã�«ã�¡ã�¯ì•ˆë…•하세요مرحبا क ख ग घ ङ च छ ज à¤� ञ ट ठ ड ढ ण त थ द ध न कà¥�ष तà¥�र जà¥�ञ शà¥�र दà¥�ध कà¥�क तà¥�त दà¥�व दà¥�य à¸� ข ฃ ค ฅ ฆ ง จ ฉ ช ซ ฌ à¸� ฎ à¸� à¸� ฑ ฒ ณ à¸�็ à¸�ี้ à¸�ื้อ à¸�ุ๊ à¸�ู้ เà¸�๋ à¹�à¸�่ โà¸�้ ใà¸�๊ ไà¸�๋ ─â”�│┃┄┅┆┇┈┉┊┋┌â”�┎â”�â”�┑┒┓└┕┖┗┘┙┚┛├â”�┞┟┠┡┢┣┤┥┦┧┨┩┪┫ â–€â–�▂▃▄▅▆▇█▉▊▋▌â–�â–Žâ–�â–�░▒▓▔▕▖▗▘▙▚▛▜â–�â–žâ–Ÿ ■□▢▣▤▥▦▧▨▩▪▫▬▭▮▯▰▱▲△▴▵▶▷▸▹►▻▼▽▾▿◀â—�◂◃◄◅◆◇ â†�↑→↓↔↕↖↗↘↙↚↛↜â†�↞↟↠↡↢↣↤↥↦↧↨↩↪↫↬↭↮↯↰↱↲↳↴↵ ♈♉♊♋♌â™�♎â™�â™�♑♒♓ ðŸŒ�🌎ðŸŒ�ðŸŒ�🌑🌒🌓🌔🌕🌖🌗🌘🌙🌚🌛🌜ðŸŒ�🌞🌟🌠 🎀ðŸŽ�🎂🎃🎄🎅🎆🎇🎈🎉🎊🎋🎌ðŸŽ�🎎ðŸŽ�ðŸŽ�🎑🎒🎓 ðŸ�€ðŸ��ðŸ�‚ðŸ�ƒðŸ�„ðŸ�…ðŸ�†ðŸ�‡ðŸ�ˆðŸ�‰ðŸ�ŠðŸ�‹ðŸ�ŒðŸ��ðŸ�ŽðŸ��ðŸ��ðŸ�‘ðŸ�’ðŸ�“ ðŸ�€ðŸ��ðŸ�‚ðŸ�ƒðŸ�„ðŸ�…ðŸ�†ðŸ�‡ðŸ�ˆðŸ�‰ðŸ�ŠðŸ�‹ðŸ�ŒðŸ��ðŸ�ŽðŸ��ðŸ��ðŸ�‘ðŸ�’ðŸ�“ urwid-urwid-69907e4/docs/examples/graph.py000077700000000000000000000000001526073734300244322../../examples/graph.pyustar00rootroot00000000000000urwid-urwid-69907e4/docs/examples/graph.py.xdotool000066400000000000000000000001331526073734300222040ustar00rootroot00000000000000windowsize --usehints $TERMWINDOWID 79 24 key --window $TERMWINDOWID Down Down Down Return urwid-urwid-69907e4/docs/examples/graph1.png000066400000000000000000000161551526073734300207450ustar00rootroot00000000000000‰PNG  IHDRðXØ„§ cHRMz&€„ú€èu0ê`:˜pœºQ<PLTEÍÍ\\ÿåååÍÿÿÿîÿÿÿµ¹´¹bKGDaˆë pHYs‡‡�åñeÂIDATxÚíÝmr£¼†á9;8g ¸Ê 05;HV0Éø‘ý/á„Oƒ@€�ÔÝÐ÷Sï;;¶Ó±¯µìhþü!D#ÿûïþ÷û#£è¨^£ÏQ½FŸãŸ?ÿù½ðç÷#£ä¨^£ÏQ½FŸ£ö¹žÑç¨^£ÏQ½FŸ#sGís=£ÏQ½FŸ£zŒ>GæxŒ4ŒŽFõ}ŽÚçzFŸ£zŒ>Gõ}ŽÌñi.�ê0úµÏõŒ>Gõ}Žê0ú™ã1Ò\0:Õ `ô9b"! …vÚ…’ke÷rŠv¡äZQÉîu<íBɵ²{9E»Pr­�¨d÷§S´ %× ÍQ ðˆJèj‰Jh.ˆJ€GTBWKTBsATò ¼[IH‚<Š×ºÚ›vÁä©á½Ò\ÜÚÇgr…4Ž€G¤³oiŽ÷Ð.™\!�£—š‹‡vÉä QÉ:<æx$Sh.ˆJ€GTBWKTBsAT<¢ºZ¢š ¢à•lÀ›oS<’"-¼w÷ÇÓþL——h+É•ðç\ÛorÄÓ~A¼$¼äß•æâ ðT"oi9Es<1ï÷Bþu<š c‡·ø–ðÜEÞmáCI¿3]í"O04gð€§wð˜ãÙˆx4Æ<à©Ä¼�ýñ´_/q�ýñl%öê}tùîòq’Äü°?ž±„GŠ Àc9å ðTâ]­�x�Çrб¬Áûüë/ÙðN’5xõaîëã³?ÞG=›‡ÁMxÌñld Þw-ìWß Þ§Í0ÍÅI²¯9·þ¢«á}„çÚß+?€GÞÍxŸßsxŸÍ5ÚÌÞ€ÇÏFVà}7ð¾Ûy^omÏ`h.N’mx__ ¿¼ïïæ¬ûeƒ#ðN’8¼Ñ÷pÜ›Né>[i]“ñuxÌñldÞwƒ«ùû×^wéóc˜Noa Í…±ì€÷Õ/ÙMX�WS~o <òRÞ†7^Çká}~�s<y÷Tû1\Û ´�æÂXv5ËðÚã³{+÷ûëû[¿·ÞY²Þ×Ç2¼öÝ‹ïö¦g�ÇÏF¶á Èx_=¼¯OÃðh.Œe¼gϾÊxß_Ÿ>5¼“$¯{çbô^ídy ¯=Ý~}ê÷tµgÉ&¼�ç§S·̚\sHæyÚîh.Î’xí§SÚ‚~~Ì>�Ò­ãÕùy1[ðFŸ@ž~ïùÁäú{xÌñld^û äç/ûèwàÑ\ ð€§’5xWú-3ºZcYƒW‡ß«%Y<à©Ä<öÇ37ðØ/M~¥ˆ$ÕãK'æ‡ýñ%¼Éó¯ èÀÏÃrJÎx*Þ›ðèj�x¯Ác9%Q$àU÷ñ-§—¬x™#/8ì³{j+{s¼cQ‡WÙÝö7~o�®6sÔá#_¾ªCWMáÝ%?ÆBs‘9ð6>�ÒݪËì÷Š{ßÕŽOµ?à �w…hÃk€ui>$Pêº Ü'ð†O Ø€ÇïXàm|ô}‚P Ö»ðh.xÀS‰¼Ý¿ev&xÌñŽEÞîœÍE¢x*1ÏP¶á±?Þ¡ïExì�—&àM¢ èèÏ>_ì�—(à]úˆÇrJ¢x*Þ›ðèj�x¯Ác9%Q$àí|çÂÐ*ð²GÞÎýñ6àU¢‡ÏMxÌñŽE^5œ�×áU²çmš‹Ì€—f¼ê.z*^æhÃ{a<[ð˜ã‹¼Tû㙀Gs‘(fàõs¸úï÷þ!�wÙ䇷s¼'¼âùË?²¶à1Ç;x#OÕSWÕyk1Î÷Ã3�æ"Q´á ûã ðîÏ]*à]6êðªAÞÞ|[2[ð˜ã‹:¼ç[§€Gs‘(à�7›oSvÀ»R,Àû9#<æxÇb^x#ðh.EÞ²DàyJ~x;÷Ç[ZN± �9Þ±¨Ãë÷ǛûÇFðh.EÞ¾ýñúfbüo™�ß¹¨‚ËÀ;w´á=ߣ áûáYƒÇïXàíߦÌPh.2xÀS‰¼Ýûã ]mæHÀ;ch.2xÀS ðÞ…Çþx‡¼á±?^šd€7‰6 £?Oø|±?^¢d€wé#Ë)‰<à©xo£«=à½�å”D‘€Çþxd xiöÇ+$ÿñF>�’;êðªá„¼OøÄMs‘9ðÒì�×ÿ“ñÀ»F´áíÞïþ3|FÞ<æxÇ"/Õþx¢íÍEæ˜�WõÂèþxÀ»RòÃK¶?ž-xÌñŽEÞÈSµ<[s<à‹6¼öÇÞ•¢¯ämmSfk9…9Þ±¨ÃÛ¿?žä�æ"w ÀÛ¿?ÞýG,ÀË ðvî�g⟔¢«M»ð‰ðh.rGÞ²Ä x²Ÿí^æä‡—h<Ù]mî¨ÃÛ»?ž¬;š‹Ü€—f¼"¸ ¼sGÞîýñ¬ÁcŽw,ðØ¦ŒÌ<à©Dûã‘Y$à�14™<à©xïÂc¼CÞ‹ðØ/M2À›DÐÑŸ'|¾Ø/Q2À»ô�å”DðT¼7áÑÕ ð^ƒÇrJ¢HÀc<2‹¼4ûãUì�w¥¨Ã«†ò:¼çíTáÑ\$м4ûãÕ÷²ô ÝÀ;mx»÷Çnls¼c€Çþxd3ðª~BÈþx.’ûã‘…¨Àyª^ØÏÔnQÀ;mxû÷Ç« –S.uxÕ oc›2cð˜ã‹:¼ödzpª¥¹Hðöî�gb+Zà%Šx;÷Ç£«½Rì»Ͽ‰x4‰¢oY"ð<%?¼DûãµßÄ<æxÇ¢oïþxwºÚKE^ÂýñÄÜ/w´áíÞïGÔ]mîÀc›22ð€§ xì�Gf‘€wÆÐ\dð€§འ�ýñx/Âó¾?ÞŸDÉïR‰>ï^»ZÃð&Ïÿ‰Ãr ð€g&ÀS†çµ«ž<÷Ë)Àð<ÂcŽPÖpµ<š ðb>{ð,ÃcŽ×ñ¦ð†Ëûws¼Á ÞSí»Úî�Óµ¹Ox÷1�ù÷‰>î°p\ü¥êþÚ_Þù¸ÍÅ•à��4£ãUÿ–Ùt�·Xú`Ýöã>õÛ³?ÏË;x…7¬¯ Ó¬æ ÚŸÉ9ñ}x÷'°áFÅð)„ð˜ã�Þñï“õq š àÏP€§ �9ð$áÑ\xÀó/ð€—^,á½µ�8„w‰ç?VÿðÄÑÕšƒw‰ç¿`9e9ÀËàE¼¼Ù„GW ¼Ùîj�¼ ^$Žá}Ç/%Ë&<æxÞà}�­ýýþþ—ãù¥¹ˆÄ-¼¿ßÿþެ՗r<¿À‹Ä1¼ZÛpñ»ù/}èj#ñ ïŸ*<š ¯ðjt£³«ô©xÀkÒ6éíÑÕF¼þòtΗ*4‘8†7;»æXP^$À]“áù¥«�xÏ+r<¿4‘8†Ìñþ•çxÀ^“fY/Ãûµtµ‘oÈï¥ô‡<š‹HÜÂû;[>ù=èýý÷ރżH€÷¼Fsⱜ<àÏ!<ºZàIÂc9xÀžCxÌñ€' �æxÀžCxÌñ€' �æxÀžCxÌñ€' �æxÀÞ*¼›ösþj{à1Ç^ê¬Á£¹8¼à²õôõxá1Çž$<š àxá1Çž$<š àxá1Çž$<š àxá1Çž$<š àxáð²Â‹%|mhá]*Qxtµæà]úˆÇr ð€<‡ðèj�' �å”óÁûýé‹é+³‡A÷«7·rø%œM8åë_žWx·æÙY µ¯±4ýŠ <æxç�^ÿ ¼…û«À£¹¸¼ƒ×á­�ðÎ�9ð$áÑ\\^ù¸µ7,Û)ßpy¸9îJxýín×»)ãmí7)7Úià9…ר5 ÊJï¯7!#xÏû · Çrvÿ·á1Ç»¼²h]·TÙßs8p£CÖ^w¿þ¡ê õw¾u÷¿Õ_/g÷ÍÅåàÕŽ^Ï[ÌáMîyßg oô�çc÷ð"ðý-Güž…W޾ëßc¡”7à1Ç» ¼ây«¡K°�æâêðwKðš€7½?ð€7…×ÏǬÂcŽwUx�•9^p¿—Oµ«¡¹ð€§¯Œ,§ì�·¶œ’s¼«Âë|Gð†…ábtÏz�xº ]@.úäbÍ…/xuãÓlYÍÀp“b|Åpíc¶ þ åü-3à/¯~�¿=Z݆éÚ (x…ûGŸÀ[û�@PÉ;ð˜ã]ž¡Ð\\ Þ[¿ì<à�®öŒðΚ àxÀ àð²Â‹%|”T`¬½ÐÖêÑÖ’+QR]­µÚZ= GŒØeë±µœbí…¶Vð€< xR]­µÚZ=^à‰/§X{¡­Õ“^ðiwà/¼rõ=4kð˜ãÙ¨ç8¼úY^¹¥x4Æê9 ¯ÿÄð6¼Üž+x ¬ÁcŽg£ž4ð|]Í…±zŽÃn^ýïF4¯|÷Ñ÷Íý7àÝFðJ5°áw(Šûß ÃcŽg£ž”ðÊz²î�ßÿ®½­<š cõ¤„÷_9À{Äa;m€ç ^Ù�K§W>žãÒ·ä]]­ox·²,wÁKýKC4Þà�þ˜ï�ƒ§Õ\ÏF=Éá…ûãEámî �s<õ¤‡÷Ø /õ4�æÂ¼ç>x¯ÃKü®ðÜö)+ãË)Zð˜ãÙ¨ç0¼áߟxÎñnåê:^·€,{ª¥¹0VÏaxÃ{c£®v¼?^lÿ»[÷uàï=xý'�ûsh°?^tÿ»Ôîèj½Á‹p†W� s<õì‡ùT”±Ð\Db­žWàÙ˜Ímþ<À[еz^�÷c¢Ýüyèj—b­ž—à� 4‘X«xÀ^šŸ‡ýñ–b­7ðb)‚¤*ÄÚ m­žâ¢‰ÂÛêj¯úB[«gô”_zGÐÝË)W}¡­Õ<àOÞVW{ÕÚZ=^àí^N¹ê m­àxð˜ãÙ¨Ç <š cõxÀ“€ÇÏF=^àÑ\«xÀž<æx6êñ�æÂX=Àð$à1dzQ�x4Æêð€'�9ž�z¼À£¹0Vð€< xÌñlÔãÍ…±z€<àIÀcŽg£/ðh.ŒÕ<àOs<õx�Gsa¬àx"ð‚xYáÅÞûª/´µz´•äJ]­�zÂ#ÅÂrÊêð€'�®ÖF=^౜b¬àxð˜ãÙ¨Ç <š cõxÀ“€ÇÏF=^àÑ\«xÀž<æx6êñ�æÂX=Àð$à1dzQ�x4Æêð€'�9ž�z¼À£¹0Vð€< xÌñlÔãÍ…±z€<àIÀcŽg£/ðh.ŒÕ<àOs<õx�Gsa¬àx"ð‚xYáÅ¢]-¹V¢ð¶ºZBÞ̱åBÞ ðˆJŽ­ãòfŽ-§òf€GTrìÓ)„¼š ¢à•ÐÕ•Ð\•�¨„®–¨„您äux%! ² oiާ]0¹F–à­5„$ ðˆJ¢ðæx„$Ko­¹ $Y€GT…Ç�ä ÍQ ðˆJèj‰Jh.ˆJ€GTBWKTBsAT<¢’8<¶)#3ƒ·•íO[òz¢ûãõ£v�䚉.§�ä ðˆJ¢ëxý¨] ¹f¢Ë)À#9<¢’è§S˜ã‘œ¡¹ *Q ]-Q ÍQ ðˆJèj‰Jh.ˆJ€GTBWKTBsAT<¢ºZ¢š ¢à•ÐÕ•Ð\•�¨„®–¨„您xD%tµD%4D%À#*‰î�Ç6e$g6÷Ç#$kb]-#cÎQ½FŸ£zŒ>ÇhWËȘuT/€Ñç¨^£ÏQû\ÏèsT/€Ñç¨^£Ï‘9#Í££Q½FŸ£ö¹žÑç¨^£ÏQ½FŸ#sGís=£ÏQ½FŸ£zŒ>GæxŒ4ŒŽFõ}ŽÚçzFŸ£zŒ>Gõ}ŽÌñi.�ê0ú ÑÈÿ¡#t‚ö'IEND®B`‚urwid-urwid-69907e4/docs/examples/graph2.png000066400000000000000000000166001526073734300207410ustar00rootroot00000000000000‰PNG  IHDRðXØ„§ cHRMz&€„ú€èu0ê`:˜pœºQ<PLTEÍÍ\\ÿåååîÍÿÿÿÿÿÿ,;¦ÍbKGDaˆë pHYs‡‡�åñeÕIDATxÚíÝ ’›º…áÜÜ;Ììª €ê)t&�Šç?„Û¼A À<´—пêœ(vÛîíðhË4ýë!ùßÿó¿Ÿ?ƒŽæ0¦9šÀ˜æøë×~nüú¹ÁÈr4/€1ÍѼÆ4Gëc=cš£yŒiŽæ0¦92Çc¤¹`Lh4/€1ÍÑúXϘæh^cš£yŒiŽÌñi.Í `Ls´>Ö3¦9šÀ˜æh^cš#s;źPr¯Ð\“�˜„®–˜„悘xÄ$tµÄ$4Ä$ŸÀ{¼9!Ïì³®öa]0¹GJxŸ4�§õþ™Ü!•#à‘ÐY†77Ç{Z—Lî�ÊÑGÍÅÓºdr‡�˜ds7/§Xo�T<à™$9xk]­õI%©ÀÛ¼œb½AR‰)¼Ç%½ ðbH(x�òg¿Ü&Ús<�¤�æB,áཪÿ�Gª$�9žFR�Gs!–pðÙ£‚ö¨¯1QÎújx¯“?¸^ ïùª µßµýöåpêw¦«�!Áá•ÿ—ÐÍ¡·lwÏÝåÑ\Ä�pð²çÓ�—½šÛçÎõ€C«çtºÁ˜�z¬¥«�!©À£¹‹=¼*ÀK-áM÷tð˜ãiÄÞéß•æ"†x& on9%$<æx€÷sãúu<𠱇7û‘ð’Kxx�™“NýÎtµ1$¼€¡¹ˆ!ÀžI’ƒÇO#©À£¹ ð€g’ôà­\Ïzƒ¤’dàq}<­ø¶ÞW¤ñùáúxbq÷7€ÇrJ ðL’<ºZ�¤�å±,Á+òá&ßR ð"ÉÊnxÅ@^ ¯Èc€ÇO#»áUôz�êðh.Är^Ý`Íó<Ëì{ àÅ’#ðêO/š‡Æ�9žFŽÃË[xy! �æB,Á›“ØÁËòBà¬àE?¼•O.†ðj§yaß[ÐÕÆ’Ýð²ÚÛWÕÑvóÞ¯“b äªdžX:oÖOðëão9©ìñ‚/§o9ÀžI’ƒª«ÞrR�|9xËY‚w§O.€'–%x\ï@€·œÝð Í2ÍE$Y€ÇõñŽxËÙ �ëã­xËY€ÇõñŽxË9¯¨O}ï.’—eÝ%Ëš©bȹ ð"‰ÞÆëã}5ïƶ‘WdE}ÉÆV¢<æxùÞÀOÑ]¯ù9Ç^ûÀ¢¼7¯„ÜåÑ\D’½ðºëã¹ðÚûÚ}b‘�/’ì†W f‚Å<¼þIᎵtµ‘d7¼öúxx­H-x4b9¯>˜ºðŠÌ�—í.€IŽÀûšƒ×=Us<�œ/Ÿ™ã… ÍE$ùÞœDàÍxËñÃÛz}¼~9¥nÛ‡þl�®6’솗 |5øŠ]1†—‡<�…æ"’,ÀÛv}¼ö#³Ÿ;ò¶«j¿ºx}öÂë¯�ל$Pîêš“ò¼î¬ xÌñ4²oûeʄΑ¢¹ˆ$ÀžI–àmþ)³˜à1ÇÓȼ͉ͅX€<“œO(ëðœÏ&ÉÀó%srÖ?,ð–“yb è¼Y?݆¤«ÕH*{<–SÄ<à™$9xtµIË)bY‚·ñ“ ¡U<àE“%x¯�·¯ºû\…ÇO#»áÝy^ö¸MsIà�s}¼"z(^$Ù ïƒëãiÁcާ‘xg]OÍ…XÃkçpå ïy»í�¼•øám¼>^/ëøgDV s<�|oà©èu�·ãôzxðh.IJ^w}¼^Þ_E ðV²^ÑÉÛ^–L s<�ì†×$<š ±€7¼Øìð2enw<àMsÞWŒð˜ãiä|xîƒ4àÑ\ˆå#xó�7à-Çoãõñæ–S”á1ÇÓÈnxíõñ¦ðrgß(�æB, ð¶]¯m&†¿ËløÉEáÜðÊì…×FëÂs®‡§�9žFàm¿L™Ph." ð€g’%x›¯�'ºÚH²/ÆÐ\DàÏ$éÁs<›$Ï—ÌÉYÿ°À[NæÉw“w“ïHâóÓmHºZ�øöxÃc9%†x&I]­FR�ÇrŠX–àýù7„WÞšÍ%•À‹$KðJPÿ~\5û;ÇW·\�÷ó¸ßÁ² �9žFÖà½K9?ú&ðþtàex�Ô4‘d^ulýAWÂ{»ÇÚr7¶T©6à.x‘d¼r�6�Wíæþl�P½H¸]]m$Y€÷nÍük˜ ù|ÔQl?h."É:¼ßÿê]Ö<¼¶÷xÿk�¨Ó9ð€çƯÖòîö{ã)]7oëáµ™¶±Jð˜ãidÞ»ÂUÛZênµ¬1þyÿ›Ì锿xÀÓÈxÿ¾ßSxßog¯êrÿÕ·þx+Ù Ï]ÇkáÕ‡æá#µ–S˜ãidï¡ö»»wžÂ2Í…X65óðÚ=™ïív!wxÀ‹%[àµÝÂÐ6xAOm¡«�$ëðºä�ðÜÝáÑ\Ä’ ðþ¼Û¿»[y¼°çò/’øáumB÷YíhÙÏ]J »Ã£«�%«ð¾û³SÜ�Ìf»Ú?Îi+aÝÑ\Ä’xõÙ)õgb¾'g§4ëxm3Ñ/¥ŒOÀ |æ<ð"ɼÁÈãóñþ >šÁsN9VƒÇO# ð¾ûÚ»9Y?4‘xÀ3ɼ�?e&ºÚH²¯ ?W»3À[ð€g’ôà9žM’�çKæä¬سà�uá@kh ðF±¾Àâ� 3Îúé6dl]mðn}EÐh—S€G€Y€Gs!–ÃðŠvB˜Í¦s:ào;¼©–1¼Ü…×þ%ËÉÃcާ‘�à < IŠÆ[�±ü#sàü+Th.nï«_%iàåõÿE69(³œ¼óà�¼1¼êAξQ s<�ì†W‰ÚOãPKs!–ðZP¼lÚ]Üå/x_[áÑÕïzxùôM À£¹ËGðæ%xgÂ[ùäÂo™À�®V,»áe�'^>Ú7ætµÀû ÞÊÙ)-Ϧ™è—Rš/ôÖñ€'–½ðúÏh]x_NSÔ]í à­œú®š àxûàmü)3©ÐÕÞ^Œ¡¹ð€¼h¯�¼8â…çKæä¬B€·Þ­â…·ÖÕ/8¼Ñ¿Ä9¶œ<àx1åØ:ð€w&¼ÍË)Àð€SŽ��<à� �æB,ÀðBÀcާ‘TàÑ\ˆxÀ^xÌñ4’ <š ±xÀ �9žFR�Gs!àx!à1ÇÓH*ðh.Ä<à/<æxIÍ…X€<à…€ÇO#©À£¹ ð€¼ð˜ãi$x4bð€žàïRx¾¸Ï^px·Š]­FÜ=Å Âr ð€<౎§”T౜"–ð¯c_ðvAÓ‚ÇO#À{½Æ/ä�ÕÝÍ…XÀóù¸j xÀ„ÇO#©À£¹Ëùð¯ŠÔëù¨>êoRÞ[ßñª`>ªïþì¾^ýå9xð€·^+®�¸ç ¼¦šöyWÁcާ‘ëིf×õ¨áUû²Ÿ¿T¸²IW[þYÞß?ïTx4b9Þ�š Pó¿ oxÏ,¼îyÀ^xõa÷zxÌñ4Þ£ù¶aáÑ\ˆåtxÕ<îå‡÷h¿-ð€w.¼úe}ð^/“C-]­XÂÃ{ÚÎñ€§‘ÓáÕ{´'ð€Þ35sð^†Ë)Ìñ4^¹@\ßó �æB,gÃk?�íUyŽºÚæàú|}ð‘ð€·^ó§^öj’cx�ö$ºZàEš àxÀ£«UJ*ðh.Ä<à/<'ÀÞ¥ð|qŸ ¼àðfsÖûõºm¼ðèj5âî)n�唹XCð$’<ºZ�¤�å±x‘ÀÛ·qÖ^·Ø¹Ñ?†ÇO#Ã넞m4ÿ&xùҿܶ×Íh.n ¯Ôq <ÿ+xõæ/®�÷µ¸Ë; s<�lƒWÌlþmðÚgnzÝàù^7£¹¸¼Áæh7Q}5¯7}Þ|åçqýýíãr?<÷uëÇfí²ê׿×Þ�àe# ù^Ñ‚(²’ÊH§iåu[xÐí£~no}ÝUxÌñ4²Þ¨ÛìöCí¾¯hÁåƒóé!¸g´üºÍ딲ë]\^½v{{ãëf47�W8«ÝÆ)œÝU¿/tÞrõ„µ×íºÚö/ý1}ð…õ×ÞMàî¦(Ú­3…7¸á>åkíu‹îÛüÑMâÚ;·½î*<æxY‡7y Å^+ѯî;–_·óÜÃßÞøºÍÅ]àÍmï¼V´›ëè¯yÙúËywLïnÛãO,kð&s¼2CxY¶v¨Í¶ÎñÆÐ¾zóð²OæxtµbY…çtŸSxù ¼í]­»ˆ\í»Ûtµ©Á›ž�ò¼âƒu¼1´æµ=ðŠ×ñ€'–-ðFŸ0ÌÁË–àå_>x³Ÿ\ Ÿ>º½íuWá1ÇÓÈ&xÎgªmW›7x ^>ÒÚðºÝÂqyúKÑüµ½½ñu3š‹;ÁlðáþªýÈl¼À›Í�X·þº½¾¬ûpö«¿½ñu�wSxÝúZ7ͪ> ý÷ÃË{`݃²î,„3à1ÇÓÈÇðv&Ôëf4ó±†<àI$9xÌñ4’ <š ±xÀ;áu×á9ðÎx]/<_®* x›á�b]×Ñ÷ãÞßmHºZ�øöxÖux?,§Ïäýx&ï‡u<à™¼–S€gò~€¼QÞoÿ­3ßg§o˜÷{hí÷ûý÷¢÷Cs¼A~¿ÿþX+o]ô~€¼AJgCxÕ—¼ºZà òû¯)<š ±ƒW¢]Cj�'+xusq¾=ºZà�ãÀ«–S.hli.€7ÎûïäèzÅ‚ ð€7ÎÞ% ytµÀg/ì2Í…Xìæx_¿Îñ€'³®öoõ!Úxƒ¸{¼êÖù»<š à�ó{²|ò³Óûýwß‹-¾àoxÌñ4Þ_§™z¨¥¹K0xÎùxÍ=W¼ào” 3“åæx /Ôû¡¹žÉûðLÞ]-ðLÞÍðLÞð€gò~¸>ðLÞ×Ç»²ž7Ô-ã…—jW+ oöŠ †åàxÀK{x6ð’_Nð€—"<æxÀ �æxÀ^‚ð˜ã/$<š àx Âc޼�ðh.€<à%�9ðB£¹ð€— <æxÀ �æxÀ^‚ð˜ã/$<š àx Âc޼�ðh.€<à%�9ðB£¹ð€—"<'ÀÞ¥ð|qŸ­¶¡ïZ�µ’«â…GW«Q�»§¸AXN‰¡àx!àÑÕjÔ“ <–SÄêð€s<�zR�Gs!Vð€¼ð˜ãiÔ“ <š ±z€<à…€ÇO£žTàÑ\ˆÕ<à©Á{¬žÌ'–çxÌñ4êIÍ…X=KðœÛêië^ õ$�9žF=©À£¹«xÀ^xÌñ4êIÍ…X=ÀðBÀcާQO*ðh.Äêð€s<�zR�Gs!Vð€¼ ðœx—ÂóÅ}•»nhµz¬µ\/<ºZ�zfö¾Ûêa9%¦z€<à…€GW«QO*ðXN«g¼×èÚðîM_š½y¼ºÂY…óúü+À‹µž£ðÕf�¡µ¯²4þJxÌñ4êÙϽÿx3Ï7�Gs!VÏqxŸÃ[ ¼ÛÕ“<æxõ¤�æB¬žÓཞ�ú�Í”¯»Ý=Ô•tðÚÇ=º/7SÆG7>ëo²ÖN/¦zN„WÃx4ÍFµ¹‡Ï7!xýóºÇ¹ãkòüÝð˜ãiÔs¼WVíº-¨WûÌnÇ• vY=¼æyíK•7Êïühžÿ(¿þš<ÿSx4bõ‡×î�žý#+4x£g>†ÏüƳ-çùÀ‹¼žëà=ÛGøõ/å…÷|×¾g¶Ò�ÐÕÆTÏqxYÿ¨®KЃGs!VÏéðÚ]àÞø#ŽÏáù>"^”õœ¯��©ÂcާQÏùðž s<çyjCsS=ÀÞ}à½<Ë)[à--§œ�9žF=çÃk|ðº…álðÌr�x¼ ã]@ÎÚälÍELõ‡×~Döì»Ú®è’ ïèî}ŽÀ>kkédƒç9Ï^Üõœ¯üŒ¿Þ[=ºéZ'høJí½.¼¥“œJöÀcާQÏQxB¡¹ˆ©ž-ðvý°ð´6´Z=ÉÁcާQÏx±„æ"¦z€<à�çxÀ»ž/î«ÜuC«Õc­åªxáÑÕjÔ3³ÇðÝVË)1Õ<à/<ºZ�zR�ÇrŠX=—ÃsÎvð΂·üš<æxõ‡×žÍ¤ �æB¬žÃðÚ3†×á]àÅTÏax¬ˆà1ÇÓ¨çx¾Í…X=ÇáueíÏFT[¾9õ}õúwÀÞx�¼ŠÒ£i6ªÍ�m¸þ]`xÌñ4ê9Þ«œì¹×Çó_ÿ®~l x4bõœ ï9¼³ƒ÷ôÿÀö¹^Lõœ¯=–Žï|öãÜ%*§wtµ1Õs>¼G=y[‡wö Ñ\ÄTÏðL¯�çƒgÕ\O£žÓá¹×ÇóÂ[½2À5ð˜ãiÔs>¼çVxgOóh.bªç(¼þ:xŸÃ;ùS àÅTÏ%ðºË”½üË)Vð˜ãiÔs^÷û'ú9Þ㵸Ž×, ‡=ÔÒ\ˆÕs^÷ÙØ «^Ïwý»]-ðÁkÏ@n�¡Îõñ¼×¿3ZÇcާQÏ ð<=ûtx4bõxÀ �9žF=©À£¹«xÀ»¼ðáúx1Õ“ <_ÜW¹ë†V«ÇZËUñ£«Õ¨gf�á»­–SbªxÀ^xtµõ¤�å±z€<à…€ÇO£žTàÑ\ˆÕ<à/<æxõ¤�æB¬àx!à1ÇÓ¨'x4bõxrð^‘e<æxõ¤�æB¬žx±x1Ô“<æxõ¤�æB¬àx!à1ÇÓ¨'x4bõxÀ �9žF=©À£¹«xÀ^xÌñ4êIÍ…X=Àð‚Às<à] Ï÷ÙwÝÐjõX+¹*^xtµõ¸{Š„å”êð€]­F=©Àc9E¬àx!à1ÇÓ¨'x4bõxÀ �9žF=©À£¹«xÀ^xÌñ4êIÍ…X=ÀðBÀcާQO*ðh.Äêð€s<�zR�Gs!Vð€¼ð˜ãiÔ“ <š ±z€<à…€ÇO£žTàÑ\ˆÕ<à/<æxõ¤�æB¬àxAà9ð.…ç‹ûì»nhµz|[ï¬×�¶~/<ºZ�zÜ=Š౜C=ÀðÎy?¬ãÅPO*ðXN«ç¼BQ'ð"©g;¼Â½3NxÌñ4êÙ ¯˜ÜÛÀÓòGsI=ÀóÞlÔ6´Z=›áeyýÿ$‘ÁcާQÏ2¼bðÀ\^[)ÍE$õ,Âl†ŠWùG °¨TMýfA†MÑÔ¼HêY‚—E¯.‚®6’zà�—)#Wfõúx„\_WËÈxåh^cš£yŒiŽÞ®–‘ñÒѼÆ4GóÓ­�õŒiŽæ0¦9šÀ˜æÈ�‘æ‚1¡Ñ¼Æ4Gëc=cš£yŒiŽæ0¦92Çc¤¹`Lh4/€1ÍÑúXϘæh^cš£yŒiŽÌñi.Í `Ls´>Ö3¦9šÀ˜æh^cš#s–kL^òRo <Áoê¶ÀkV Ç6Ëß1ðn™CàÁ›zTà5ë <àÞÚÖÀˈéëéGÿß¼my4,hÚªÈgí 'àÂrK6f×Ão¬x—ŸüœÀk&DY?¬.¨—¶®ÞŒ€Š_o6ÜUêï¢x—t)7üЧãœUÿ£Vb´ôᡘÜEù˜¶o¼Š ýèÍV–¼‰ZàÓ'¯›¼rj¥\0ÇdàU–Ï‘·o¾2à�T�á%Ïûm¢ ÅD¯D}±�Ÿ­Êa>6s*\V5©¼©‡^kÜî�o3of*ð®=µƒnÓ�WŽ™¼¼ÁTàMw3ä(Ã~ŠÆkÞÂÃoœ3Q£Ç^±‘'ðàMÍ ¼ß}˜Û^s�í£}ñë›ÿ.«íÀœÀëÏC~çÀ›Óxq“�7\GQI™jggPk4⣵IÙ®±¼‹aÆ:�0äU>ªÞ`eà'*ç[�}$F“» |³öíW‘·óWâÁ{*ø7¯Þ˹¬—6ê©è*õ‚nÔ²Ëácfàuµ—ßuµåo£øu/ ¼a§7../ ¡t¨:¹ž ¨`1ÌöGð:>bï¼8ûÌLM‡-�à�»ÚûÀ± ¼Ñ¸êü´ÙÐÚ†k¿§'~S¯N౫~=L¯;%xox£õ¥³²Ú°#ûy}^-…+\VÃäþ é._=ž7ê�w묬¶Ià}~^Qd�æûyÆ#˜OêÚßЯ€•ÀûlÞåYÇǸA,#.Û磒öýó"%Þ+8¯…Ù¯µ+ÁK¯Ëçï·G½!Y„#¢öéÇ�éòŠzK_o°Å;çý<&2^Û‚À«ÄGx—Çq"}Ž7вé×)µö£ À®œ¿ßúk,�çu^ ókùû­þžË ]ß'Ðåú É1¡}Wk?®<¦‡‡áy�W¾Þ´ÑÌ÷sx3lxŸ_{µõÀK+�W,_TÑ xí—Ìó8¯…µhŒM¶ÏNE@”¥Ú�W´o^ú Ëzí7r[àÍøh(mx£À©ÛdðeKª-<›¿¾|‰÷Îká–�W>9Uòáº)6¯ý�Á7û�¥‰Öš^îñn� oK౫óZ¸å.í!OæÍôž�W¾|žÔy- ;-òQ•5ø†ÀK6 _>ðtZ,°Qà�Ÿ <æ;¯…Aº•?êÊ,ðläÍ÷ž�·÷¯˜ÙÎka¹Å’™ºÇ.íÂöOxý‡Æ”mÎÃKž <æ;¯…ÑÖ\xµ5øÅoÎòo <�µ³ls¥EúliÐeµ§oöi)¼€Ëj˜ïsž²M–n~àŧq4æÚw­öY uÝDÐÕ6¹†ñq�FŸGóýL~D ž$ð²�óE.¼þÉÞ?g¦\VÃôÇÞE#kkð-�×/`<¼ ðºÓŠÀNn^W[~ùúNÍÅ16„ÕõI%ðjklŠé¼e€•»Ëã“�÷Y6Oï³xõ<­~=Œ²-ù‰W×àrRÿí×Ú ëG<<Ñ z¨^Y¯òúº¸þhÔ¬÷3±4ÆîxŸÙ¬•…i‘‰öù2Ó�×:Q ^¹²[L÷¿—»^¸ÂuÁÊTY¿‚ ;Íy<Í ¼Õë¿þ¸Ï.m«ßLà‘Û`—6_e†ŒWî`]{åÀ󋘩Õi‘�jÖ™Z�ê1(ðo‹À;åkÛaO§Åõ£å‡Ú¬#ð¸ÙF�—t"4wiÇùðú�g#o¾0„ºì(GФ¨Ó ¼S«�À;¼­/q„À;9’7S-ðŠ‘Ÿfú �¢ªQÓdXàÕ¬À›šwQà�òáÁ�w·õß�bŽè»é®�Wé'�êmë_x¿Ô¤}uqåtßí›™üB»å�W=ˆ’¯ÞÃúUíêwSA×=xý÷£˜#¼pE¹!ðZ;!—Õö]»žïöÍlxã¼é-M¾ÚúVþ?xIó¥�·ùúß�š‹cløŠ‡QÑ÷Þú0»A:br†Sµ}»žïöÝL^}Jeu:ëO-�¢ Ñ"’Áú®lY¯ö’ãúã÷OÛúMÌQ^öEÓƒå<õù—_`°ßQ¬bã‰{pljƒÀËFå›DåöQš(­cdÁêÚ8vWÖ«¼èÚ`1jbýwóÍÚ„›^a�'Xÿ„Æ<žÂ¬ïÊ“IÂVÖîlÇõ¯yRoÅwÌá9è¾d�w‡á[Cà‡!ð€ÃxÀa<à0p8Œç»ÒÂY À�ls-mx»ŠwK)' <`s[^ívx­{6N„šÀ¶7ç~wSÓ‡*ÆÃAûn¢¦À¶·AàUƒ®’Z£MÁéš›igÓ¨]óî­É“�7ý²°¹]/*Qܦ¿5½¿ƒ¶T˜}ïÃÍ/þU‚ÖôËßP‘xÀ o*ñfvZ”£ƒöQúE“EàómxÅX󬓭oïOx9OxÑ.p~õ?£0ÃìÒ <à16¼�ã->�ìDà‡ñ˜ÀkxÀc<þ<¼b´ÀcŸKËÒkio:-eïx=Þ껥Ä�×Uë <à&ÝjÝýð‚æaÁñÔ½?8àõlxëïx´1™¸÷¼žmnñðpÏ÷WËîD’‡!ð€ÃxÀa<à0p8 �†ÀCà‡ñüWZLÜyNn'ÁL›\K[ù+�íÛ¡ZÅ­ÏÕuÁúµÚû…ò 6¼b},WÐ…÷Ç [ìýI±�[ÏêBÅ÷ÃnH\»}òõÇÕÉ�ëËàxnùê­.Tlx×&•?ÄØMï§•Â6{RìCౡ-þ¦EÞfyàÍ.Í[›õg=çÔ±ºpÑ…ƒ÷ ¼©YƒÉÑŸ¸hMÿгñÚ*ß_ýÉh} � [(V« þ.í¸ôué8g®ì5¶þˆEøG}¬â/­úõ•c»ìß7�G]ýÈóͨPÚ4ð¢¦ó-»âÀr\XàÊŒ-¼pl«•õ�ܶ[xáJ+ð˜cò^Ûõ†™Ç�Á⺫^>LN§�¸© ʆxM› óß/ñÎÁ3ʧñèþé%äÒö�Šiûd†|yk�“c1ãÑýÓKÈ¥í«Õ’-¸¼^˜ßÒ‚ßÊú颾E#W8'Í(ðÆ£û§—�KÛ×Ê�²ùÛ�wš(¸Ô9jF 4Ý?½„\Ú¾ZïÒ.©3^—�o×[ê-£Ä�îŸ^B.mߪ×å‰6ž?)� Ÿª¿V%ð’åÃð4ÜÙÝRm‹-Ý$ËÆ7ã*h_>nxe‚¥›dÙøêVxy½ ð²ñÙâ+Ù¶UâÕ¶ØÒM°l|=žn ¼¢Ñµ-¶d¯³ºƒ[­×¥3—3¤ŒÛ¬m±¥�•�oäQxeÝÊc>zs× JŽÙU§÷ÿíxãCwñ„lJ¿Ã:£dT¦º¼å[^å„| +=B•k^°¥—4xtàÁSLȦô;¸ 굯¶˜[GÛòátJ¿:U/QÎp§ÀKžE²)ý>çüzaÞ�6Ãa�dË›Œ§Î)Y”íxÁA·t†Æ´(ð’zÞ¦ñ‚€Êª�×Úý|ÂÀËâ�×ÚÿœxIý^t .™¡µ¿ùn�wí¤È÷qxCŸÂV�—¼{à…Ý¢é ‹/«Ë7˜ ¼òõ­íZ>8ð¢~Ž›E½ ›^¿I˜Íÿ°ÀúîxÉ‚á`¡zµÀ+N?©Þ‚]Ú‡Þ·2qâÀi”^^omà¯o•< NS›^áôl�÷Y&Px³wi³ÀËëß;ð¾—‰ÞŠ]Ú ¼ß¸û^6auà5—·Vus»ÀkÖŸèÛ(šT�9Þ(¼lÂÝo‹·q^6aƒÀ‹6xÙ„Í/nŸ ?Sàõ§?oàMöÒVº' ¼½´oxí^Úb®§¼öY)Á¨ô¤¾hy»ÞôY)Oxñù&ÙÐ8øÆ×V<ú´”àBв—vò<¼êÐÝOKÉN��Í>¯¨Wßuî²ö]´üàõ­“íÊRD—M¬=ïã&²]ÙàBв—¶[”xaàÝë^¾+›�75Zx^9½6ü”�7Þ!ðòƒÃ[x}„FË[­ ¼ôDä8ðº�—�Øœ%æèɸnxߺl`³N‹ ðÒ‘ãÀëÞ){ž^±›¼Ÿ2ðÒ‘ãÀëZ�7>ñ8o›×@à¥'"W«xY"[ˆç'ÑðC/¸´,h‘N}XàU.-릯uiY½þƒ.-릯zv\ÔK[\«[Ž6ü¢æ›^j\ZÖMÞìK˺²~tÌF[zñ¥eÝtàM\Z^^óÀ«\ZÖMÞ’KËÓÓáÇÃû#>l7 —Û� ¼ðæÝTàMÝ< _@v©Ú½oÐM^ãæÝt½èæá…ã]¥ÞZ§þâ‰aðÔMÞ’›¤·ÈËOžé²ÔgñÄ0øÙMÞÌ›ÄFõ·¼ðæÝTà-½y@Þ`<2ËÛx&hÚ.°x¬ ³lG‹‡x¯JàÁ|›ž7ÌÃmw#”}Ø%å¡¶¼0ŒGû|õ¼›:36&ï^ØËç]yCc8º¿.jÃ÷ö÷E1¶1ÃÝ^É sý¸(Æ6f¸ÏëßÇ?ÅØÆ K0½¼íü¼(Æ6f¸×kÙ¿ÅØÆ {¿æ»xë <�w¯×²�Éî��÷eeàm“�+ª¬ ¼GgàæË[xÍüŠ&Þ1𾬠¼§ËÀ•�÷~øt�·¬íóÞ²¶ïxËÚ>à-kûü�·¬­ÀÛšÀÛàõßçåÎ"ðnžý!^Fà­«"ðÞ�³?Ä‘oÈ®¢g"j�š7g»Y’5e/Æyð2®þwÒ:èøÈÊ�dÍ“ú·H¤ìÅ8^ÆõÏ$­ƒŽ�¡ôåÿ?j Hê-F¯­¹¼ÆÒgJ¨ìU8^ÆõÏÿIZ×;"²öÅòúçÙ¸U™˜$VÙ‹q¼ŒëŸÿLZY�Ÿù¢òãÑ?×eh’Xe/Æyð2®þoÒ:èø¨Ì_–è‹ä/¡Òx­2ªþ*zbÛ�7n¾GॉöûaÔbnàE³„õoѼ4aúôº´˜x?ÒÀ«VÙ ›µ¼öògi^šh¿F-æ^T±x>Œk×›£xi¢ý~µ˜xéäÑŒQù¼þMÚ�—&Úï‡Q‹�W4¨•ÈÛG�×+ï’]Ù¹(á<ã�2ñ¶ÛñmöÒA7~Ì6ئÊ_æ� ƒ÷饂nü8˜VÖ o×ÈÊfÊêç ˜\Þ�é1¥ÙK;ÝøqHS»´ÿTJ&Ï“mÆ®Ø(\¤ÙK;ÝøqœHípfL�×åãóú‹5{i‡ ?fhÓÅóDûz̆ƒå­Sä[°wúb�—7™xeíìñ��—7Yxù\Aàeãç-oEâ- ¼¼Éæ�7]µmaàåM&/Œ­^±…�—7¹%ðÊeUƒå­t ¼ñ`íY9œÍØÿ·{Tà��Ö�‡—^4pÝ´›W¥¡xÁ–XµI­t+ðjõûg{^D7^ך!¯—ìÚÞø~Z�—ï‹. ¼Jírž^z0myàåówåtò¬6,ðò§¯8hת¿ ðVÄx›^qŒp²è„cÞW0]Ýx}µ¿6Ì»G^ÒKñr�—õ:DÇòÆ &—7Ñk2Ç“ÞD7È´‡^ß-1|³ÀKz5‚áé…,#ðÞ)ðòcukïGÐ`)�·]àýÌ ¼ÅÚá�› ¼ÿ?¼pàÅ_'ðÂzå.mZ¼xÓ¯b“Þ­oãêá�—Ÿw×®¿ØsìÒ¶‡7 ¼.¿JÔ :Þï'›ÞREàµK ¼î�o|²ÀÛBû,ãnÑi)ý .›ÞBêþ§¥4öb§¥d§ÛÕOKÉŠOÞ½Žá=è´”¢`¼Õ#OKé*�÷¨cx÷9-¥±ðlø�WX"^1*¯¨¹ÒßÕÍ®ab;ðþž(-lÓóðâͪÑÄvàµï&Ãûñ#KªAÎY^QïÅá²ü:¯n*ðþ™¬”ŒÊ�ž­:ïg>"ŸØ¼ŸÓÅË ÍËoxõÐ)OΛü;µ‰—�9\^_1¾à�º-òQñpÜñé ÒKDzk½ÊKËÆ�Ùì•êÁÂÆû²+/-_N–wD—–�³ÙƒÊq/m²/šX¸¼¢Þ ÒKDzNƒòÒ²ñc6{­t:¹,]Z¶êÚ²ñ•ÅÅ^½¨ó.-‹ï:ó†�7¾œ,ï4ˆ.-?f³×‹×¯˜>½¼Õ‚]Ö¿¢¬ ÷DŽj®Ó ¼àæ]ØõÚ(--ÛB\x]0QµŽá¥Wû…óóð~üH‡k7/¼–WÔ»A+ð‚›tÙžg;ðâMȸühêš=ÛVà7èÂÐjWO‡Ë©[^× ¼àæ]y ®HÉü]¼�ÌŸÅm<Ó �¾ÛÓ}¬ ¤'°*Pž þZ[ô&<“u tŸJk¬MœM7ÑÖxÏ@à ¼{WZã}ï‘“ç‚]Ø—²ú¼Þ�믵Ýé"Ïa‹]̤ÞÎ�·v—òÇàÖxèß »‹Õ] ;Kè½^ýµVw!<™Ÿ?'Ñ-¯¶ëûù÷ßö!º{Ï¿±—Ï»©.‰çwïñ+©m±¥�•�¯ç]%ð¦Úýx»ˆ/Ÿ0xÁXžÕÇG¾ ×¼bÚﯟwå&VíèZ¿ƒ:¿^Ü8Ý´“wûèS®»5ð’]Û½ß Óf^r/ ¼λJ@Çà’Z›c3/:ˆ'ðö!ðŽf2ð>>ŽxC§Â}¯¨ÙdÜûƒ8¨¾“¢Ï«ï?Ý/d*ð>²cwµÀ{ÕÄ˪è>­ÖÌzùüe}�·'�w03/yr€ÀË&lxå]»ùʺÿ?¬¼½ßKlx/ÜkqSàMŸ•² ð:y·£Ë–ÙuMཹ©ÓRŠ )ÂÓR^?ñ²óгÓJŠö ÏÃ+&‡ç鱃!ðúÁä±pï”—V ¼�ìùÇGž|S[~¯¢ ¼ôDä8ðºVàe‰Ã+.å`'•-»vàý ¾—]Z–ô¼öÏ“K-F3¼é¥eÝtà-¹´¬˜Ü ¼ç0xQ§Díj³½ß 3ÄJd‰vi“LM t¯žxy'B7xKo�NN¦ ¼=ÝxùÝäÌO1y¼8�¼¿¥w?ÑG ¼¬‰3“£Ö{¿d€-ºrBÞÃÿv Ô=l—IEND®B`‚urwid-urwid-69907e4/docs/examples/palette_test2.png000066400000000000000000001536761526073734300223540ustar00rootroot00000000000000‰PNG  IHDRð@W×]D cHRMz&€„ú€èu0ê`:˜pœºQ<èPLTE‡‡¯¯ÿÿׇ_ÿÿ‡åååÍÍÍÍÍîÍÍÍÿÿÿÿÿ\\ÿÿÿÿÿ_ÿ‡ÿ¯ÿ×ÿ_ÿ×_ׇׯ×××ÿ×ÿÿÿ‡ÿ_×_¯‡¯¯¯×¯ÿ¯¯ÿ‡×_¯‡_‡‡‡¯‡×‡ÿ‡×ÿ¯×_‡___‡_¯_×_ÿ_×ׯ¯‡‡___‡¯×ÿ×ׯ¯‡____‡_¯_×_ÿ_ÿ__ÿ‡_ÿ¯_ÿ×_ÿÿÿ¯×‡¯_‡‡_‡‡‡¯‡×‡ÿ‡ÿ_‡ÿ‡‡ÿ¯‡ÿׇÿÿ_×ÿÿ‡×_¯¯_¯‡¯¯¯×¯ÿ¯ÿ_¯ÿ‡¯ÿ¯¯ÿׯÿÿ‡×ÿ_¯ÿÿ_××_ׇׯ×××ÿ×ÿ_×ÿ‡×ÿ¯×ÿ××ÿÿ¯×ÿ‡¯ÿ_‡ÿÿ_ÿ‡ÿ¯ÿ×ÿÿ_ÿÿ¯××ÿ¯¯ÿ‡‡ÿ__ÿÿ×_ÿׇÿׯÿ××ÿ×ÿׯÿ¯‡ÿ‡_ÿ__×_‡×_¯×_××ÿ¯_ÿ¯‡ÿ¯¯ÿ¯×ÿ¯ÿׇÿ¯_ÿ‡_×__¯_‡¯_¯¯_ׯÿ‡_ÿ‡‡ÿ‡¯ÿ‡×ÿ‡ÿ×_ÿ¯_ׇ_¯__‡_‡‡_¯‡_ׇÿ__ÿ_‡ÿ_¯ÿ_×ÿ_ÿ×_ׯ_¯‡_‡____‡__¯__×_×_¯¯_‡‡__‡‡_‡¯_‡×_‡×‡‡×¯‡×××_‡¯__¯‡_¯¯_¯×_¯×‡¯×¯¯×ׇ¯××__ׇ_ׯ_××_×ׇ×ׯ××ׯ¯×‡‡×ÆÆÆÐÐÐÚÚÚäääîîîׯ‡×¯¯×¯×¯‡×„„„”””žžž¨¨¨²²²¼¼¼‡¯¯×‡‡×‡¯×‡×DDDNNNXXXbbblllvvv€€€¯‡¯‡‡‡‡¯‡&&&000:::¯‡‡¯¯‡¯¯¯wÝ�°bKGD ³k=€ pHYs‡‡�åñe€IDATxÚíýwÜuÍ]ú?ßçN¢¡$¿HQ$H ¨;bA@¥JAª䨀bÅr4< "" QŠkE8бQP¬±rÔ×½fÍ̷ΚY³¯½vù|^ÉÞ{ÖZ³¯rïûýL[s?ó ‚ ‚ ‚ ‚ ‚ ‚ ȇˆºŽu½Ó’£"Aî Åš!túÁ£ Ñ<’y¬/ƒ È5älàÑàÛ5N�|«{ÀŒr£Ù^ó�ܣͷDðöÔxr£9xÍ÷x‚œ#oöjA®&HËKÊ�úZ;æ\Úz÷ t%Öpî€Üè a£:f8ä†�·þ•ï¯)„}¬gœ÷“W‡àu忪üLðþ‚ÜJðV]lÃϼä v½?EoÁ¾z}2§Ÿñ|”×7¾ çzöÃ_A�Iž9¿Þ3àÙ³ÁÛ956Á ¾‚_C_ßü饄cxâun9WlÌJÄĶŸ­wv}l:å~­Pj€‡ 7š³�×:v8x~›A�Ëxe#tÛÏÙO_°Þö4ƒ tç)ß3‚ 7‘³ƒÇ�ñ.:?x¦‘ðäFÓÞÓ9ÌÓ�×ü‚íÑ>ÿûëÿ)Ãë‚ÜAzÀËë�o¼žK¹òl‚W ';�÷b¨Õ¤l¿ÇøCÇÀC�;HõJŠ/˜Ï,TÖ­ÇL¢wú_u}û»Ð×»þB<¹ÍØÿ&xñ,çØ,­7SñLð~Î4ª�r N𞉾ýgž‰¾‚ 7ó·{¼ÚéõßÜýz.B²ž6åM œ&¢³ý+AA¹ž‚ È�ähoAAAAAARžE¹“<Aî&A�» ÀCänð¹›Tðž<‰¯j�;yžœõ«!’‹ùü�ð'Úóm?Ysôw£)àµÃ�³åçTJøÃ¾ÇL}þ.‰Æ7Ôøv/óg¹™<ä"rOàm¿÷³ÜL2x[¿à�ÿX±çéàû3õù;ú›üÎò_€w@r ¹#ðê߀wþ¬àmþ‚ã 2�¹ÏßÑßýØwÖó _îuí9 x˪ž°kÄÅòJjhF0¶¯‡xýåë§ÿ?‰¿rÑ91xú�ß|¤ÔOòÁõöó¬Ÿíç»Þæ÷««ný<ø¸w'�×ñÛjƒ·þ¯dE2·¨l¯×‡ÄõùµùóÇGàj2ûù³‡º>?æóò¤}½ü‚üÙÿ<»ßsÏ÷«.êýyû�é2^<å¤À+c²böÖ|b„�éÙ¿ž5ŸØúõ?¦Oâ7@.:³Ÿ?÷Hëó#>õú'ÑõüýœgïóÝžý~åE�?ϳø´wä4à=»ôjcðä;8ày_Iý› ZàõþTÈEdòóç´ÏžÝþü8Ÿ×'­ëàÉïо_ûÙÞÐoÉ9x œ¶MøÔW’ÀÎwRþK|}|®%'Ïùiàûõ\¿¾­Ä»ÞÅ/ú`£+3€‡Ÿ“viï<|Ô;s›à™^rÑ97xâóqõàaÒb '�¿xÈ`ž:3yý¹MðŽþ­"c9¨K;xýƒ÷,Fò:sšuxâÀCsšÏŸ>p™àõ|ý=àásß•ÓÜi!_�BgG$ôa3H«Ÿ8xø“¿¾œäó§Žl}~œëŸ´®·ïÍÏw^¾0ú~]qß§÷wˆ<{1à­üëw€—_ýëEúrnðÄ磎åõ‚÷äÙ ðôç=ïIôõí÷÷lóË!<'Ù-E¿’30=£ðDØïY{¹� KýëE:sŠÏ?ôÄ#CŸ?/­Ï[.ÆÿAßüü=qßРü}zÖŸöÎ< xϺŸ#œ|“�ëõ×”žJð0kuu9)xæóe®qþ‹Û#³Þ8#(�÷Ûú�O«GlôóàÃÞŸù�ä¹€Ïиƒ<ä"rŸ?€w¹Àµ ¹ËøùÃäþýÿ.-r÷Á ÿýà!¼»›<Aî&A�» ÀCÍsTž«ò9arhôà!�€‡ý�xÈcà!‡F ò˜xÈ¡ÑH€‡‡·…ÆÀ³#d¢Ìj<÷9ì•újÏUßÔsƒ/¨/vÞO}ÃÞÏ`¾ßÖùæ÷‡´2ÿïÒ>«ßa¼�gç06æ@ÖCâ]–O¡ÆŠ½®ÇƒO°ÏLcØ¢|w¯GDžõÀ“â4çQœ£öj‡k~¨ùý!­ €×ËØFóËž(s#ÿ.gsZ¹š,ŸBƒE}ÉŽûŸ`;i¡`2“u^D¿ƒnfŠŽµÛ²«õôûõƒçL»xúyÿUß8ÀëÈ#�×Xvr*ðŽþ›Šœ$˧ÐíŽ>·¼ }�î9ÂüFì=Íû4À“_ƾŸ÷]…ïo.òÁk½#æqÀ{Ò¨áý“yÞ{´Îû›Éò)|TðÌ©Ü!ÔïŒ:�=§çù9�ày×»øß_$+âåÁ ª<„eùž K«�¨ïã ã¾ó5ƒó:óXcxòïá<„eùzÃS£à‰¥|w&-ð�C³| ðÔ²”.ðÔj“ ðn¦Kû4òúsðšoðî9˧Ð�wÆéýO°ž8pà=Gþ× óëðžmƒg¼{Îò)´àñÇõxð Þž]•r.ðüIcû[h�wßàõä1o-‹Á“÷ÒîZ–rô_Tä4ICów¹<=G_ ‚÷\#�¨ñ\²pRÀyÿçnA÷ܼ|¡ûþæ°çÞî<"x­¥Çb·¼Öû¼›IúrkÖòªÒsô:ž7ráïs•ž�çj7Á—�‚·ùýèº[à=÷9Í/‡ðœf{(µÁÝÖvwz?<çúöû¼›Éú9”àÕA¶çêó&jN–Õ–Ó½��÷Þ—Ÿ+kß/ú–ý÷ç?ƒÞ½ÞõäQÀs¶@v«DEs¨}«-rÅY?‡õ/*Y¢O°ó×}-ª‘AÓ>’¢´ÆÈ¬7Ï}N<ó~Á7Í!çl¼ÒÈi¶xG��ÉDÛÚ’mœ3ü]w€‡šüAxÈ9ryÿjrW)ŸDÖÏ4à=²Dþd.r‹Á¿K‹ý�ô[x� úßOrhôòð0ê?xÈ¡ÑÈCÀCî&94ú ð�Ç ÀCänð¹›<Aî&A�» ÀCänrywZlmµ‚ ²3§¹—ÖÙÍdh·{ à!ròœ¼h;¼°ÒÖyìÿ„ ÈcäDûá±§ñ�ã·D9aN^]ï¿iÑzOA�“åÿ¦Åð6¿-€‡ ÈésxÞ[˜Ò¢u~)`fA�±œàߥÏ9jç0ÜÑGþ#>˜ËEd0àm‰×9iaïúwi×cGÿþ¹¢œ<Ók®:9xGÿ湺\x^ØÅOü³�Gÿâ¹¾\@—à!ržœ¼ŽI Þâx‚€‡ ÈÝä<à5ßà!ržœž9 ð9O޹µLÞK»kYÊÑ¿8A®/'oz·¼'áû<AvåDÛCÍí‡ç\î¾!?{ô/A�ëË)À›ßñعÞQœ<ú‡ Èõå4[¼#‚\A‚ w“ËûWËA)øwi¹›<Aî&A�» ÀCänð¹›<Aî&A�» ÀCänð¹›\þ�;¯ ȳØN錽—VmR¢Îº þ•Æöv(Î;´ÞŸç;Í“'ÎçÉ»êèo¹†8›ðnç‰9kc>�ö:¸?ž{ÅÑ¿)ä˜ì$ˆ·ž÷ï‹umxmí^¶8ß@ì€ äþ²ç�$HÞ“gE¯�‘¬ÖøWz‚úú߸ð¯9ú7…€‡œ0î¿i±4¾Çð~ðºß¹étý³ž=ïƒ� ²F}üÄgOÙ|k¼­ªÎih�_ ˜Ù¸î~ñ köF�ñq@ÌÇÊÿwi�ÏËxêØSË~ù°�éþ£>øˆ_uÂ?>{ô‰úïÀCâ¨OBðq»ÿ¨NÇ;³/!ç0â�Ÿû�Ö'<ýô,ðòâè_/²+OF>å^ëþé Às ìxÛôBKó+úw¹ý#ƒàý[Eö¦ÝslNà!�ÔÏËéÀ Û„OÀº¾’üˆ·Îã~½Ùê{z=ToÄà!­¬™“uiŸXîÁÖÇŸĦÞ“að¼ÿLë–�XößàÝU:ZxîÑÖUø< :ÍÞ³,åÙÍO¥»¸DÔxˆ—Í1<¿Jã*|‘�1¼'yžAk¿Ÿ{TÔD— 26K‹1Ý�Þ[[³ r·9ɽ´OüÝMâ«;n}ÅÖ7ðÑ9xvožg›[0v€÷¤ Øö–U�ÿ»CäÊâlêf³CÝx#—è’÷} Aî.§¯\¢ö_ ßj<¶ä¾7@äNãý#>^zû#?*xß ‚ HÎQà9ÿä…,µf%¼·lº[ÂcfAî-î¿Këe¼•“ø²Ö¿¸²žsØ^îþ @òýÁ\.‚Ü]ÀÛºäÉxæûgií9çý¼÷ö†‚ÜS¼üoIm\w0x�øKEä2s:ð¤w=“¡Pcàyïç¾µè6?Þ¯A�KÍÉÀ“Z€‡ ÈååTàiïL§ò‰uà!rÖœ¼`–Õ�‡x‚“Ӏ笪ëXWð9kN²Ï£íÑÆðÌI€‡ H_Nq§…Û”{´e)ö^Ú]ËRñwŠ È…æàù§fÁëïI<£ÀC�{Ïi¶‡rÆìúo-ÛœåˆßA•í%Áy€‡ w™cÀÛœÖݺÓÕæŒìé/ÑóÍ!rÃ9Éï‚ ×€‡ ÈÝ䀵 Aä˜àߥEänð¹›<Aî&A�»É3ÏC¹“<Aî&A�» ÀCänð¹›<Aî&A�» ÀCänð¹›<Aî&A�» ÀCänð¹›<Aî&A�» ÀCänð¹›<Aî&A�» ÀCänð¹›<Aî&A�» ÀCänð¹›<Aî&A�» ÀCänð¹›<Aî&A�» ÀCänòÌ�:sF¿Á}aýþŸcýù_ïÂ3úó¼þ…gôçyƒ+ËèϧðÀÈÑ ¼¹<€ðr4ho.à¼� À› Àxo Gƒðæò]xßh—I½×ízŸÑïŸYñð~]¦ô^wmà=ü\]m\§NÓšóƒ÷ðE» ê½îhð¾Ï.sz¯xà<€ðxgÈÀWýù¼¡·x:à<€à<€×Àxàuà<€ðN  öô¡4,\OéëtýSƒG‹= u˜ZÑT±gjPÆO«7T_oQ—x£ß`ŒùdzœåÏ÷*¸^TÐ_o¼$#Æô#«uÏׇÝOâ-8ý~L’oHöýå—"ïàxIžAªZ÷zâz·ª\Ü�Zo8úó‰˜@ºiVÏ1ðÚÞ¥ç<aãýFž‡µ´˜HåT½>ôN�hê‹7Pej¾ÿèϧƒ§ÚqMðlg÷ôàYÁH4ÉV<û>êùÄàYÁH4É,i~ŸÒ‚§ßÏOwÀ�¾ÒÉÀ³à�h‚µû¬“à™‹æÁ³âïu†\<Ç?[�Dƒ1ýy-6 VÔÁíϾoð¬?x?J‰åy'}/<"Û¤³à9ÖÐ;y½~›ðëíO·¼|ðÔñлm𜖞¸àÜàx\ðÔñØ;÷ýZàE_f/xdGÛðÔñ~ïX‹Ð¿ðÔàÙ&�Oßö.¬¯Ge€Gv�-¯íÝáà•ï?/öÎO¼ß ÀÓßß©Á+ï�×ðîÁ+?O^ûðÄûŸ¼òõ"ðÞÝxe’B÷qÏÕœ<ñ†�ûr'O½Ÿž¸`<ûý�<öþgÏ|Á“‚ÇÞþ$àå&¡áîL౯÷Hà‰/à”¨`¶Þ@—ö¬àH6ÀëíÒê÷›ÏùþN ^qg¼vi¼‚ÙxÝ]Zž~ÿǯ`¶ÞD—ö Á{:�{x~_t?xþÉÇO33 ^óýÉöW·Àó¾«S‚§•zlð^/ÎèÏã‚§åš�œ7;xM°Nž½*_xy©ñå‚·9K{áà ÌÒÞ$xíYÚ믽*Å9$õ]xÛ«R.<½‰*qøø½� žîÊÊ‹À“§ÛàEâùo0úý?ßt=8ñ,íÀ:<ó~äwuGÁó¿�ÑŸÿõLW–AÏÒίëõ›â�þ<¯oº²  x–öõ‡ÄsÁs¾ÞIÀÓ]YNx¤�òÀ‹o©­oä¨ßÿ àÉ…ÈX-ð”ˆN 1½ðÊg/åGùàý(uþÜà·–5×áµ›xêŒzÿ3ÝZÖ\‡7ñ xöýÔ×Se¼ç{—ŸláqpkYs^£‰§ÞMd¯½U0§/¸µ¬¹o«‰ÇÞμ�zÿ“ƒÜZÖ\‡·ÕÄãïçµáÄyY>xÁ½J¼ Å÷øà¹›üè-ð¶6pÈ“m¾“�çnðü-ð›<ûý¼ÍÜ/øÏÞoIq�à™I‰;ÏLJ\9xfšB”¼A½ËÏLJÜ8xô†º�wYàÑ Xï Á£ê6žÅíJ¦iGþÒ¸“â‘¿VåJÀ£é6ÞuƒG?F·ñn@Ãà©úÃà©úÃà©úxü¤OÕ÷Àãg xª>À;ofþäZz/€ððT—VÕÀ#Ò«Rr—VÕwº°Ôœ´Põ=ðt7VtiU}<Ê#|n—VÕ·à¡Kû˜™ýùKãn}úq˜´¸í¸‹RhTÁ#!¡ÃÓ‹R r¬LDá,­^”’@£`™ŠÃÓ‹Rh¤ñX™äž^”’@#3‰�áéE)/’ò¼ �;÷ã꺔£1x��³€gº¶×Ù1=€w1™ýùžÌ]€'VáíO¬Â ÁãâIðÄ*< m­Ã«ð<ðÊ_<± oxbÀ»vð]ÚŽià�Žáé^ðT}€wæÌþüOàÝxo ð:ÿü¯.³??À“¹yðÌÞø­erσ®Ý¥•Cxã·–É!<]<3„g £­.­Âíeà]nÌÞNðì,ièšàÙ‰1ðì"; 5Á“[?cy ð¬n་‹ÞÐn)xb©1m,KqÀÚ-Å�äÐå¾k/xb·”àvK¹Zð0K{ÓÉxéÝî¸oº,Á# žÙþN—%x¤ÁëÛ¯€G<½^.‘iðÌ~xõðÀ# Þ‹òÕ/x�žÙŸßoÍÑš¼ÓGm %yÍ2O5ßøÚ;NÞGà©æÛ›ðæ#O`ཀྵYdLd|Ó<9>§[s¼ÍçƒgE#[x×v<¾å¼ÑdÞx2o2™7�Ì��Ì‹&sôŸÿÑ`ÞuçÔŸ€ðÞgöç?,€wæ<€wÍ™ýù� à�9à]sfþ£ÁxgÀxלٟÿh°Þ™ðÞ5göç?,€wæ<€wÍ™ýù� à�8o6™?™7ŸÌ[Læ-'ó&óV“9úósïyñ•gôçxàÝqŽ à à±+:À3È·P§õóã&y–¾| ž>}9ýyo¼ôÒOuiÇÁ£<â71ö(�øõ�À7Þ[z-¿~ð(�ðyåmðL—–òˆß…‚§0—Ø3®m“ª¿¾LÇʃ…îŒàÑ‹uÏï"Åýy¯*“NÙ]”òVR>¼·ºbðô%{ÀÛºŽèŒ-xbÞðÄ*<€½w ^ùóag/¼Ëoôç½ðÂ1;j�áéÞæ^_füæ1p!xºÇNZàqÝ¢I‹x| ¶\Ÿ‚mŽáoó9SÁyx—·y@V§¾B¥W ðøÍµÁ›7Öá‘>Qà™ A/*£?ï €7—?™7ŸÌ[Læ-'ó&óV“9úó3�Y…úëŸÏ;ü#>×€ð΀w�ýyÀx™Ý½d´><€à¼ÇŽšrxäúS_j8GƒððÞ£gλÁúgõà][ÀCöçh°Þ`ÀCöçh°žÊ[Oæ'NæÙɼÍdÞv2o7™·ŸÌ;LæèÏr[xà!w€ðr7xà!w€ðr7¹YðÒÒ§)ðê¼Û~_ûÁK^yÜ\'xY3qÀ¹"/�•åðž^ß*o�÷ôz]–GÚà=½ZÅ�-ðž^>y¸ €gp#€‡œ#×ÞâÙŠZݽFêF€¯üP½‹Šx‹gµ|=ç."¯ˆ– o 8sÀ€·p–MË—¿­,ºäáòÃÊ]/y³ü°rç�×o±,ƒ–ë7xÈ£äJÀK|UðØSÖMˆW_3ï x7Q´â1ðf+iɶ*œ.;à%ËVÑm ¸ü2/Y¶Š–h«ÀQ>ƒ—p[‰KÖuŠÇ¼Ëà%ë¬x ðg+jI7a\èÀCNškÏ�Î0/‹¦eËüm�G<ödËxTÁcOìÅÛ´Á£ ÞÛÚb žhàðØS?xTÁs}Û�*x9€‡œ'×Þ[à½u'x?1ï'v‚Ç›{¼g;Áãͽ=àñæÞðxsox¼¹·<ÞÜxȹðœ2ä³à <~¢tr=ð–©1¾< �õŽ�èYð®,©1¾€‡œ<—É ƒ§ê÷€Ç¯~VÕï�_üìÛ¨úàñ‹ßæmUýðøÅoûvª¾?iÊo¯ê{àñ³ºøª~x¢xôç¹­\!x: žêÒúàñæœ|AyÄo/x¼Ø 1öÄémð,|ütxºÁÇËàQáËe~z<Ó¥§�þü!·•ËO÷hÍ2”§/H4ýÌžÂ+úÕ²™ÔXÁó¥<ËåÓe¾8oíÒêE)9Væä1ñÜE)okä[1Ô‹õÖ.­^”òvR¾·Sâñ²»(åí…|T&1äÞRv¥¼ƒ”/Ã[ýùCn+÷ éO¶øÎ^ص½ðÈŽé<äBsà‰Uxn—¶ ž¿ �×O¬Âë¯tàxb^¯ÔOà‰Ux<2ëðJýžX…ç�Ç:¼ºlVáõ€Ç:Àf^x¬|ôç¹­\xÚ»x]ž^8f×.›ÞȤ�kæOZ0øðø�½“<®Y4iá”MÏGΘž^!�6ÇðjrÚ\xb· [ˆìƒ×Ú-¥ö|rê\x䀷Fxk<Òà™ýï¢r�4x[ûáåÒ iðÌöwªœK+x¤ÁÛÚ/VðHƒ§÷ÃË%’³´¹KK<³^ýÄ­´+x¤Áó÷Ã3§Ñ¥ENŸ«OõW;v<–àÙ„o|mžjó%ðž5‹Š©Y¦ÒæKàÙUÅä”I— xr‘�˜‰åÄÉâÛVðì¢b" ÞLàí*xvQ1Érió1ß¾‚ç/´3kŒ¥xÃC!WÞ\~âdž�ÌÛLæm'óv“yûɼÃdŽþü!·€ðr7xà!w€ðr7xà!w€ðr7xà!w“ÃÁ{ÉdÞq2ï4™Ÿ4™Ÿ<™Ÿ2™wžÌ»Læ]'sôç¹®<€ð�» Àx¹›<€ð�» Àx¹›\-xé~ù)ðê¼3‚÷ðûÞ^úóxÈ\®¼„›ñ./�TFÀKo Ë#à=½¾QNoßïéi]fGXÑgìéiUäXÙgìéé†r©~<òp«ÇRýx7à±ÂÑŸ_äºr)à-ž)Ô´näXðÊ™;fœ*zà-žÔr¿è�·ð•QË×KßJ¹~¼…¯LZ>­€Ó§—r!,?ÏŠpõz§\Ëų�¼Z¿‚·ð•A˧ðjý ÞÂW-Ÿ¬õ2˜ /ñ€—tâÕ×Ì;{ÊÍ;+/a–I˸‰b ó.ƒ—(«Â9åúļËà%ʘxùe¯>1ï2x‰2{Reæ]/Q‹Gùê ^¢Œ�æ5òxõ‰y—ÁK”iãÞU2žK�bð^¢N»à9йFàeÑ´l™¿-ð¨‚ÇžLY <² ñª¸¼ <ò€kÃ'xdA ½[_ˆžš�XCP€G<�œûzyqôç¹®\xQ�ö%�à½cÞ;v‚÷NxïÔ oîqØ~R'x¼¹·<ÞÜÛoîí�7÷ö€Ç›{y„\$xë�Æ­¶ôJzÀ3²N®^ÐÀ“/Jׂµìü²OC§¼Û¯¯+ƒ'Acƒv�àIÐd'·< ²S-?€‡ çxðHFˆÖžªß¿ú�TýðøÅïô“TýaðTýaðTýaðT}<~Ò‚§ê{àñ³™£ÿ‚!—€ðr7xà!w€ðr7xà!w“#Á#¢æÉåü„uõ§À[6xgïá÷½¼ôyxH;iüh x$#IëOÕgsºeÇ_” ô¿´Ý¿ú§©úÃà©úÃà©úÃà©úÃà©úxü¤OÕ÷Àãg xª>ÀCfrxåÑOuið¤o<Æ¢^ûæ~ÇúÏ µÀ#⓲ò…€o'x”�v‚Għ0XyxÍÝRH-KqÀK�©ÕµõÁkî–BjYŠIß(O]X}ðÄn)x”—¥8à5wK!,KA¢œ<2;Ô!Y~7<Ò«ðÖ«í­dv÷)x䀷Æ]—§n-[áÒÛ}Vß¶öÃ# Þæ~x¼œá²Û{ºÛß™r†‹ß/–Ï«¢9�î¥% žÞ/—ˆõeËùz#ÅÏâUïô­eò|†Kï~'¼s¶‡Êç�þ †\VΞꯪÍDkÏOl PwdZ}²XÀTÑÔS,àñæ3M•õé¼ã±œ©à�ºvy½µLn %yŠ@·¬¶†�<% >ð33xæYµÞª‰a¾|k™ÜJ4òl›Oø³3xF4³ã±*¾{õðè¿`ÈeÿˆÏF~Úd~úd~Æd~æd~Öd~ödÞ}2GÿC.+à<änðÀCî&à<änðÀCî&à<änðÀCî&Óà½p2?g2ï1™Ÿ;™Ÿ7šÌÏŸÌ/˜Ì{NæNæÇLfÔw�Ì;Læ­&ó¢É Àxà<€ðÀxà<€ðÀxàu�—þ:ÆÐåó ËÎ7­[μ»ïá§Ý^úmí/Õß^ª¿¼T?x©¾>Ô^ªoËGv)à‘ÏàWÎ÷�G~à=­?^úæÔÿ|‹1YöÏ?xúŒ¾¶–|ÆžžWEs ÞÓó-æêùÐ6j�WÏǼ9Þ‘=óæxG–¿˜7Ç;²úż9Þuƒçá6žÄ­øF¶|4`G�·x—ÿrÊ#¢K»x¦P+Èåú ðÏ"Ôrýx _«aUá›).Š`ù�+æÅ��·¼È%ƒ¡¹¸‹`ù¡øV„«_ìØóE°ü°rÖ"�Ÿ/‚凕3‡<òÎÁòCÕ�"ùù"Ø»‹½á h.€ì‚BX~¨ºQ`ý}VðË2hùt@ ÞbY-×o¨Á[,Ë åú1€µ|4`�—øài݈«¯™wx´¼ ÒâQ/a€÷´fØÈcÞ1ðØSnÞ/×'æ]/ã&Š À|þ4à¥çµ”+à–çŸoÏ3ï2xɲ å³ô ìyæ]/Y‹'Î3ï2xÉ2+yç™w¼d™0ÎÑo=ϼ{÷º5|sGåúO}ðg+j‰2mÜ»ê/q¶¢–tÆ…Þ½ó.ƒ—tÆyÞqð_+bI3aÙß}�á­ ¼<}Ì�bðÜ!= Åà½GxZ4ð t€ò¼hàQöL÷oÅaÒ�Ý�G<öÄ^üüŸoÏ‹yÀiøäyÑÀ+à±'¼ú$xÄak�WŸD�¸n-ðê“hà×�¶þÍ 6†·6ðÈ‚fÁ#ÖàQO#×Uð4r]àQO#ð|ð^€÷ÂNð~NÞÏé/êѾG'x?7ïçv‚÷óð~Þ™Àãͽx??�7÷Zàý‚<ÞÜk�÷žx¼¹×ïàñæ^ ¼€Ç›{{Àãͽ=àñæÞðxsox¼¹ðzÁãnµÌλà­�X=”gÏ{à­o.xòE9¿ ^»lÁ#¿�g[z�ž†ŽüÞú‚G�®¬-[ðˆ å‰1»÷|Oç¼�ØP�˜£ Õò[^Xðˆ åñ;"Õò[^XðHü3�uÀŽÈ¶ü<ð$h|ÈNµüðˆ åñ="5Æ€Gl(��èQU/½ À#6”ÇGôÈ- ØùÁS‡ÁSõù$E?ÿª>›£Ð-;v žÿ¹ªþ0xªþÙÁÓåQðTýaðT}<~Ò‚§ê{àñ³ðÂî-õ�çvo)ÙocÒBÕ·à™.­8½ žéÒRñë]Z ž7„gÀ£µéŒáÉ-Õv^.ëâÊó�!<ª“´ÎÓÚó¦Gk–¡<}AºéWÎûCxë¼,À«çi2‚+³‹ùywQJ²Œüe*ò¼»(%YFj�êdE=ï.JYþŸÄ$ÙódÅàÕó‚ZÞú É]Z½(å]¥| <±lÅ]”òR¾` o9è.Jy+)Ÿ ÞÚ¯u¥¼HÊð2 [v ð¬ncàYÝà‘ÏŠ6žm <àYÑ"ð̲9„ðN^s·”ðÄn)Ûà•û-ØUÌç-xû¥à‘œ¶%r»¶�žXjì(vKqÀÚ-Å�¤o”§.ìyxC»¥8à5wK!µ,Åoh·¼Ön)ë j�×Ü-¥<Ân)1x´^§{¡ ™�ê_gYöÎç›]”bî�%ï|†K‚·Æ]—§÷Ã# žÙÿ®Už°ŽŠ\<³^«œ!ãû{æóªì‡G¼2.'‹¢o[Î×)~¡�Hn÷É�ðRÆ,ÚïÎOí‡G<?¼w÷Ïg¸ôîwDŽqòô iðüýðÌéÜ¥% ^—{+`28™šÌ/�ÌOæÇNæhàÀxà<€ðÀxà<€ðÀx¯ ¼4ÇØ´n9ß°ìá<À»!ðþ<÷ƒ—>OûÁKõ÷ƒ—êï/Õß^ª¯õƒ—êÛò€m׋ÂR>¸K�$x.~àQˆ_kOë«âzÀU,�Wü |ÆÒyYÖç[à¥úír ¼t}TLZॠ1sõlh›å�©g;y#y¤¾îä�ä‘Zêä�ÄVèä�ÆÀ3¼Ñxš·õÙrh5Á£{oñ,.¯)k€·x¦P+Èåú ð¾jÆ·r¶.r«à-z­¨Õó‚7F;PËÕ»¨(ÊE°üP}«Â©2/ÁòC.ëÏãœ.‚å‡ê](?]ËÕ;-^9ÀOÁòã�Þß§OÁòã�„w–?ªà-|)Иx¦PÎÐòã�„wÍ.íbY-×Þy*ðË h¹¾ðMƒ(º´‹e´\=¯ž=¸ƒÀK˜ ð¤w^#��—0 ÀKÖâ1ï"ðmU<ÊWWðf<ö”›w Àå¹€—ðʤeÌD±Š'ÊÌ»^ÆŒ~±[NÅõ óî—pá–çRdOùôz󮀗1£Ø»róîý8h˳^:½^ļ+àeÌxï¯ô[/bÞð2o<å]>ϼ+àeÞ„q¨ ¼ÄYF-ó&ÀkŽá%Î2j™7’¶5ÀK|eÄ2g<Ýcx‰¯±¤™O‚wßcxk/ÏóÀ£¼xHO4ðBðØS½ly!xº@=†·6ð(ƒ¦û·â°(‹eÐtÿÖ+ð²h$z¯:  hàQ­9l—9Ôà1Кcx™C ^V�ìX�¯>‰9ºÙC¢ xdu³àsQ€—Ustë/«æèÖ^VLëÖ UðÌ�ÀsÁ{¯¼÷êï½ðÞ»¼¨Gû>�à='ï9�àý¢¼_¤ÀûEx¿¸¼_€÷K:Áû%xïÛ Þûà½_'xï€÷þ�à½ÞL‚÷�“à}Ð$x¿t¼ž�7÷^/xÄ" «â•Nn^éÄê¡<1¦‚·ž— <ÓÒë/,à ¼ ¥gÁëlà­/,xÊ;ž,[ð¬wëïó}•‡Ë žã]éÄê¡<<íÿ<‰óË žöŽ×·ç-xÚ;^¿<�¯og0,xdç&JÄ�^žƒ[‰-[ðˆ å‰!»Úò+¹GðH¦<~þ½U}>I€'N¿�ª¯'%6ÁSõ‡ÁSõ‡ÁSõ‡ÁSõ‡ÁSõðøI žªï�ÇÏðTýaðTýaðTýaðTýðDñ—ªú=àñò«úÃà©úxü,ÀS¿0Û¥5)�øà•Ç<^ôÀ{ŸFÖLZ¨ú6òÀ£|4/½Ü^zY]Óå-ðÒK:ê/½\a“-;ê/½ä]Yʇ¨¼ôÒíÒRxé¥Û‹•çðÒËžI U/½ô»´¢ç€—^ú]ZÊ#~ ðÒËž.­ªï€Gy„]Úç­]Z=„gÀ£Ò…“¹K+{´TÛy¹H¬‹+ÇøCxTa£užÖ.S1=Z³ åé bM¿µ¼^àá­çyY€WËþÞZ¿Uæ]Z3„WôóW©×.­Â+ú“UF¯è'')ˆ�ç]Z3„Wô À«2úCxE¿¼zÞÂ+úyà‰U+þ^ѯ5†·ô‡ðŠ~1x«‰þ^ѯo O/Jù±B>ª-;€·<-> ©kºà‰–Üðˆ·äœòx²ë:‰–Ü8x¢%çÀ·…­º>ðÈmÕ}€wÞ�4rƒà‘Fn<Òȹ¯cðH#7Ù1½{O¯ÂO®Âë/âVðÌ*¼rÞ�ô:<± ÏíÒzà¥r¼ ÏÎÏ_…Ç–Ÿè²Ï_…ç/CñÀóWá1ñÊïÓÏ_…çORxàù«ð¸rmðüUx\9¼t0^…Ç•£æ:<WN�W~Ÿ <W® ž¿ �+×O¬Âx�à‰E'Ôà ÀÓ=Úx––jxÕ;v¾o–V6ð¨wÒB‚×9fçÍÒšÛ,œõvQٽ͢1IáÍÒšÛ,Ì �OÎÒšÛ,Þo <9Kkn³xÿ-ðä,­¹Íâ6Àû<~›Å¶À#3Kkn³ø &xì÷ÉÀãS°¿txü6‹ÞŸ’/žÉ ƒ§êw€GÄæ©úïÃn©èOÕOÕOÕOÕOÕwÀËÐÉÇïËf7X†ÁSõ‡ÁSõ‡ÁSõ‡ÁSõ;À#â-@U�¢e)¤fið(X–’ K}ÙxÔ»/�z—¥8à‰ÝRž�¶À3ûáÕ#õÆ ±Àn�,ËÓ¹KKzÞzÞ_w§o=Ëp9»Ý…ûáñr†Mo÷Y}ÛÚ�4x[Ûá©ýðHƒ§ÖÝmí‡G<½^._¸’Ï×)ì­dþ­ez?<ÒàÙíðÔífü|†K‰¶ä|ðÄù —mɆàÕ 2fÞ4ì¹à•UÉ+x¤Á³Ûá©ÛÍê7ð<ÒàÙíðšà‘Ïì‡W¿À{žÝÊ‚g6ÃKEÊཷ^TœtcLj ˆï]ÁãýÔ÷©�;2­>Iâû¼Ï ž¹AVúÆ×Þñ ž“Á³;ßøÚr25™�žÌÇLæc'óq“ù“yÓÉ<€ðÀxà<€ðÀxà<€ðÞU€—ï¡>À» ðþ<öƒ—>ûÁKõ÷ƒ—êï/Õß^ª¯õƒ—êÛò€m׋ÂRÞ ]ªð¤wÄ‹3à‘Ä/½ùxO/×åtÄU,�V>Ì-»Š¥ú�µä*–.�Ë^‹®béúsõtD›£;TOwêFòP}Ý©ÉCµÔ©‰C¬Ð© �gu£!ð´nùÙrh›àÍ€Gmðf›ÞÝ1x‹gp¤øsÀ[øR-º‚\®ÿ!²¸(‚净;A;PË™;fœ*Šr,?dݸpªÌ‹E°ü�y«¦éò‡ób,?dîäñÓE°ü�¹³ä•üt,?Ü8yÒ?~º– nœ<å;_Ë7Nž)”ó´üPpã¾ùØ1ðËVÐj}á�Çbo±l­Ö¾iÿD—v±¬€–ë‡àÕ“°üP5£¶wwÝ¥M˜ ð¤wa#�y�—h«âQ¾º‚—0 ÀK´1ñòË ^«‚ÇžrsŽü2óŽ�ÇžX{Žxq=ȼcà±'·L¥‘Ǽcà±§¨y·ŠÇ¼cà¹=Xb –FóŽ�Çž>R¿^õ[ÅcÞ1ðØÓGéו¿ ^⬂Ǟ>Z¿–ç™w <öô1úµ^⬂Ǟ>V¿öÀK|UðØÓÇéײ̼+àeΚà‰1¼Ä׊XÒ¬)^=wÏàQ ž9æ�G1xNq}!x!xÊO‹ëDÏ@XŸDOBG@ÞŸ­O¢�· �=.xºH¢¼�Àûå�àýò¼�ìï#ð>j¼�žïc&ÁûØIð>n¼_1 oî¼^ðˆE@Ç_ä“>xDk'Ö×Þúþú¡.t²!¸ ^»¼ ^ÐÀ[_Œƒ'Ë=à­¿��øç|xDk'Öžß�ìùmðx}Û½Ý�×߯og0¶ÁãõÅ�^'x¼¾-[ðïh9F¹ã[ã�Gl(OÍPˆ¾tèþÀ#™ðÄéQõͬÄxª>›’Zv xTš‚¦G+W�àÕ L�–µé>ÚOœ7=ZÖ„ûO‚'Váuui%xbž·¯ü>|ðÄ*<Ó¥ÝO¬ÂëêÒJðÄ*¼ž.-›Ý0«ðv¬Ã«ðzÖá•ßgO¬ÂÛìÒZðÄ*¼ëðüUx¬e·žX…ç�W~^€÷<·�·1†WÃÀÓ=Ú�YZÕÀcïßœ´�àiïfie�¢I ·lx4xÚ»pYJžön`YйÍâWì�ëMZ¼ç=�d:À+—‘žÓ%¹þ¸ r25™�žÌÇLæc'óq“ù“yÓÉ<€ðÀxà<€ðÀxà<€ðÀxà<€ðÀxæã'ó+'ó²É|Âd~Õd>q2ÿÇd~õd~Íd~íd>i2Ÿ<™O™Ì¯›Ì¯ŸÌ›Là<€ðÀxà<€ðÀxà<€ð.*déþè)ð꼂÷ðûÜ^úóÜ^ª¿¼T?x©þ~ðR}}¨¼Tß–l{¸^–ò^èRýýà¥úOzG¼èkØ)üÒôƒ÷ôj¿ÜÇZúz²<^ª®Ì€÷´þ xT^yG¼8IðjiÈ;²ú y·<š�x¥Ñ&t³àQ¼ÌÙ†w!x 3£›>rÇà-|EÀ‘ä�ˆ(‚å¼|¹òn9PËwœ<^.‚凨»rš‹`ù!⎑NjE°üÐÉ]&¯–:ÀËõx‹_^ NÇô#Þ¢—ׂ«ÀYþ¨‚·ðåµàªp¿t¨€–Bïš]ÚÅ2šòÎ;XÀ[,ã-8ë�_.€å‡‚™!�¨' `ù�qFMïD—vá,›–«KÜ8yµp§à%ÌxÒ;n å«+x ³¼D/¿¬à%¼ð’mU¹^â« ^PfÞ1ðLÖ‚'Æð_±ÌY<1†—øZKšUÓ(ŸàÝ÷ÞÚÀ ÁãÇ$|¢�‚§|z omà…ཌŸeÑÀ£È;¯lÀ#<�P4ðºÁ«O¢�× ^} ¼nðê“hàõ�' ¢�×^y½¼ ¼9ðh<šOC§ÅÛ/+FÍÎl UðÞÄ…xOöh5xß Þ¯ Àû•�àE=Ú—)ð^€÷ �à}BÞ¯êïWàõöh?1¯o O¸×Õ£Õàýê¼_3 Þ¯�ï“&ÁûäIð>e¼_7 Þ¯Ÿ�7÷^/xÄb�ë¯tb=èdCÐoýâ²�´ô<ðÖúíÞúÂo­€'Ëxk}=f'xë ¼õzA©–]~á�·ÖW#vùP9³¼ðÀ[/Vv¥#+`ôÀ[/6Cv¼+Ûo½X–Ø!^ðÀ[ÏË’>Ôo½X–Ê!Yî�–—µXâ�g½[/V|é��¢®l-—Ü#x$3 žªÏç$ºÀSõÙœ„mÙyà©úlNB´äBðT};IAmðT}6'Á`ãe’à©úlNBH.xª¾®x¢øÉª¾.�—?EÕ¢õ€§ê{àñ³žY…WêG]Z žY…Wê{e žY…W®ïϬÂ+õûÀ3«ðJýxé » ¯Ôï[‡gVá•úxì¼» ¯œïϬÂ+õûÀ«ðL—v<S­|?>xbž�Ð¥õÀÓ÷Ž…cxdfiÝíö,­ÏÞHA­õw/ÓàUïX};fçÍÒª«ß7K«x¬¾W&3K«xìú<9K«x¬~Çžmà±úážOèÖ�Ì,­jà±úþ?ï4ðØùð„n;ÀÓÞ5×å9àqÝä öý4Àã7V4'-î<’OÕOÕ»¥¢ �¼u*ìN 5„·ž¹µL ám�gn-SCx[à™[ËÔÞxæÖ25„·Þ¯Ñ]Z5„7žÂÏá��g‡ðÆÀ ‡ðú»´rÏÜqÑÏ ááÖ²>ðäÞwãàɽPìäD¡;„W†òúÁCxe(σÎO á•¡¼~ðÄÞ'ò®¬.ûà‰!Ÿî¥%½ o=û2!\õNï‡GzÞzV WøSå ™]”"šzTýÓûá‘^…·žM;^Vûá‘^…·žµûßùûá‘^…·ž »´j?<Ò«ðÖ³a—–o‡÷k2\vQJ¸¯þ8 x¤Wá­'Ãuxâ|½qB/J Ë[Ë>…ô*¼õ\o—–ðÖDx¤ÁÓÛáåùà‘¯o?¼{ϧ‹Í��­¡’fЏ_©.Xo-{™ÝjÕŒL«ïe¶ìm µj¦‰“k’ËŽÇvk¨U3-œ\“\v<¶[C­šIàÄ5T^{[C­šÖe±ã±ÝjÕ,Oîxl·†Z5 ÁãÍ?ok¨³<µã±Ý*y/<æç½­¡VËBðäævk¨ä]?xr|®'Ëážœ©`ÆéRžj¾½ oÞ1òä�ûo.¿r2/›Ì'LæWMæ'óLæWOæ×Læ×Næ“&óÉ“ù”ÉüºÉüúɼÉdÀxà<€ðÀxà<€ðÀxï¢ðÀxà<€ðn ¼­|êd~Ãd~ãd~Ód~ód~Ëd~ëd>m2¿m2¿}2¿c2¿s2ÿçd~×d~÷dÞx2à<€ðÀxà]yÀxà<€ðÞ½€—îGžï¡þxõ§À{¨?ÞCý)ðê7Ë[à=\/ ¢ÜÞÃõ¢ Êà=\/ ¢ÜÞÃõ¢ Êà=\/ ¢œÏ7,{8/ nyÀ¶‡ëEa)ï….Õß^ª¿¼TàIïˆ}ûÀ#…_-÷�—¾ž8Eà¥êòÍ€GåÜ>ð¨¼î�$xõu'x$Á«¥NðH€Ç �àÑ$x4 )ðJy'xÔ/k¶å]^æÌ/–#!x 3Ë›:pÇà-zEÀ‘â�(‚å¼|9ç.—‹`ùÁ/WpŠE°ü`Á£á_*ÁòCx¹>oñk£EWʹ>oñKµà\à˜~ÄÀ[ôR-8œ<“Å+„å<†]Á/* å<;Þb™Zõα–\ðv¢\Ë]àQy]˳yõd,?0ÎH–”€¢K»h–IË—kÞÈñNÁK˜ ð¤wÂÀü²‚—ð ÀK¶UñD™y‚—qÅõ ó®žhð%ëVñ˜w½à%ëVñ˜w½à%ëVñ˜wmð¤wÅ@æ]<åÝò\ÀKœ5Á3Ç9x‰³&xÍ1¼ÄY¼ßÑ/ñÕ/8żOŒá%¾*xì)OŒá%¾¸pËs)²§ß-_3ï2xI3\~)À»ï1¼µ�‚'މ“¢�‚§|õI4ð|ð‚ã<òÁãÐÙã¢�× ^} ¼nðê“hàuƒWŸD¯)Ñžªog)¨ žªÏ'%jSN9xª>Ÿ”Г.xª¾$®ðRÙ®ÂO®Â‹Àã“<¹ �šËP<ðä*<¼ü‰ À“«ðÆÁ“«ðÆÁ“«ðzÁKí*¼MðH¯Ã“«ð:À+¿Ïž\…7ž\…7žY…W¾¿>ðÄ*ÍO÷h–¥¸=Ú<öûdàéí(xºG; ž¼�b<í�7iÑ�·çü±;Š–¥ˆ^Àx4Þox'ï·_8xcxª¾ÞïxwoëÖ2Ù¥U7Ò’Ì£�[ËÔ�´¤ólYÞZ¦n¤ÝO7Cx›à™[ËÔ�´[à™[ËÔ�´[à™[ËÔ�´[à}šîÒªiÇÀ3Cxƒà™!¼ñ.­º‘v <;„7^8„× žÂ3w\ÐV—VáyÐ< žÙþ©YðØ}e$—¥ô�ÇV“?YÑ�ÝWFrYJxlF–dW–9×�ÝFFíe)>x¶ GCàÙ&Åà‘ÏŠFCàYÑh<àYш:—¥èûÊhpYŠÂc·Wt,K±s´.xÔÏ™‘ÏNÁš–ÀëOì–BjYJxÍÝR:Áã-?±[Êx^K�øn)Ûà©…È$wKÙžØ-exb·”�àQ^–2ž™¥�-L¾HðäBãAðèh<ŠZzà‰¥Æ„e)<ÚÏì‡ÇËõÆ ±kÀYÖçÙ�j}YÖçã™�j}¯¨÷Ã# ^pk™¹·vÝ�Ìε~»ü[ðÈìP¯wÁSûá‘Ù9 ÖwÁãÛá}Z½±Â®³‹Á«§ë�Þ4lk–6wiÉìPë»àÉ[ËÊ�‚³5=à‘^…·Ö}Ù|Ì�ðÖˆrÔ¥% ž^wWßÏ�4xfû;]¾{ðŒpºèïxü©<Þ_µ›ˆÖž)»[C%Í4qæ‚ o¾É6œÎ\�Á3‹Œ“f8qMÁð7»[C%͈뉲ØñØÙ*i‚'w}2Ÿ1™WLæ 'ó™“yîd~ÏdžLæÅ“ù¿&ó{'óû&óY“y£É<€ðÀ»Õ<€ðÀxà¼[ ÀxàÝ=xé~æýà¥úûÁKõ÷ƒ—êï/ÕׇúÁKõãòxéú¸¼^º>.o�—®�Ë[à¥ëãr:ƒ—®ß.7,{8—Sýýà¥úûÁKõ÷ƒ—êï/ÕxÒ;âE}º Ið4µÔôŽ"ýj±é]^=×ô.¯žkzGQ¹¾nzGQ¹–šÞQPf…¦w–·À#œ.o�G ¸°¼<š�šàeÍ¢r>‚—9‹Êë�¼„Y\^½»cð¿"àÈá/—‹`ùA�Æù#§XË ´ŠŸö/‹`ùA�ÖÐ�x‹_hx¹>oñËÍ+çú ¼E/ šöÏrH¼…¯ œÀN—ëﳂ·XVAÓ¸ùØ1ðË*h7ƒ�o±¬‚&pk– `ù¡ <*¯ `ù¡Û;Ñ¥]8[M«¿ßßç–×C¢K»hVHË×VPþ¬Z(€å‡ª)ÝÈðNÁKx Òts®Š'ÊÌ; ÞËm{Žxq=ȼ“à‰æœ8–¬[ÅcÞIðÚÞ™w¼W„à%ëVñ˜w<ÝàÓú­â1ï$xºÁçðWÁKœ ðtƒÏ8x‰3žnð5Çðg‰^7xõI4ðúÀÑÀ민^^ˆÞx4 M‚G“àÑ$x[Ð1ñ\ð²b$:¯¶�G<�š.<Ó£Õà½@�÷‚¼—w‚÷ò¼OïïÓð>£¼ÏÀ{E'x¯À{ÃNðÞ0ï3'Á{î$x¿g¼'“àuׯ{ /odÏï÷M‚÷Y“àñæï�^ÓO·á–¥˜ÞÆ^ O·Àx¤¦n àÍ‚÷{n¼ß«Àû½ïfâá�ÝZf‡ðô�ò ©[ËìÞØ­ev¯ ž¹µÌá5Á3·–Ù!¼±[Ëì^¼ÏÔ]Z;„7Ö¥µCxà¹Cxc]Z;„7Ö¥U7ÒŽ�g‡ðÆÀ ‡ðú»´rÏÞq±Õ¥•Cx¸µ¬<³ýÓ xru1y3MðäÞwãà™Y óššàÉ{ɨ±,ÅÏìŽ2žÙ¥YðÌî(ƒà™Y‰MðH€'…k�g–¥Ø!¼àÉ­Ÿ†–¥Ø!¼ÒvóÊ>xfCà9S°Ñ³ žÕ àM€×Ü-¥3ƒg…k‚§v<Ö[Cm¯ÃKÞUð¬pmðÖÆ]ÏŠ6žŸ+�93ážÝ*ù&ïDYƒ§šo;KðTó-×.3ðTõ�TsÎ/ß/xsyùd>}2Ÿ1™WLæ 'ó™“yîd~ÏdžLæÅ“ù¿&ó{'óû&óY“y£É<€ðÀ»Õ<€ðÀxà¼[ Àxà<€ðÀ»µ<€ðÞÝ€‡w2p2Ÿ7™?4™7›Ìèçà!Sxà!w€ðr7xà!w€ðr7‰ K÷“ëCýà¥úúP?x©¾>Ô^ª¯õƒ—êëC¶A–êëCö|ò‡óÍ“Ëù½Ð¥úûÁKõ÷ƒ—ê<äÌiz‚WÏ5½ Á«g›ÞE౓MïöƒG“àÑxåüNðh¼¬›>�—5Û:‚—9ãe瀇œ;E°Wð=÷ vO¾fÞ1ðØãM€'Æð_±Ì™( ñêëÑÏ+ÀC¦"xÝàÕ'ÑÀëOœ ¼.ðʹå…hàÍ�G“àÑ$x4 ùà•Ãì¼ ž†Ž$€ªÏkÁËŠ9ÝW}à!ÅíÑn�÷Š¼Ïžï÷O‚÷9“àý�Ið>w¼î1¼¼Ï›ïM‚÷fxoð� ‰�XÄ�žhÙå<^_ é厬ÀЂÇë‹!½Nðx}1¤WÊü¤�×CznÙ‚Çë‹!;Ê_~Þ/ ÑÕÖ‹å|ÅzÈo=€Çë÷€§½ãõ=ð‚oé•<äÌÑÿR·ž,+ðT}ð j”GøxåR žéÒRás'-T}§÷Jy„/ƒÇ[ðLï•ò_mÖ²Ï[r—�sº´¼ò˜…³=ÜúÚ/lÙy“< ]V�ü.­Ÿ£5]]B—9&î^óß½}úD¥‹ëáyàÕ¹Ù‡§ª£;„ç�Ç–ªpÝ!<<»Te)»Cxxu©Š(»Cxxb© É1<Ù£¥Ú΋ÆôHŽáùCx\¼Œ¡?†Ç{´fÊÓâ€Ã3CxE¿ZxÈ%dÀC.;w Þç\xp¼ÏxâÆÂk‚gn-³CxMð>[wiíÞX—Öá €çá�uiíÞX—VÝH;žÂ/Âãƒxí.­³w\hør\Ü!¼æ²w2p2Ÿ7™?4™7›Ìèçà!Sxà!w€ðr7xà!w€ðr7xà!w€ðËçOæ &ó…“yådŽaèrò“ù¢ÉüáÉü‘É|ñd¾d2t2l2o=™SÀ{Ô<€ðÞÝà<€ðî&à<€w7xàõ…ˆ¦À{¨?ÞCý!ð®·åÇþý^z"ÈÒýåûÁKõ÷ƒ—êï/Õß^ªß´n9A–êÇÐåó ËÎ7O.ç÷B—êï/Õ¿ðÒ�—·ÀK×GÅmðR…ýà¥úo;MïBð깦w!xõlÓ;jžm�G“àÑ$x´^9ß^ÖM{× ^æMˆÀËš‰ÎW Þ¢Y!-opõùAÙ‚·pVLË×Ë’&�ƒ·èVˆË×÷ƒ·XV@ËõÞfŠ`ùA�ÖÐ�x‹^¸�Ö]~ƒ ÞÂW®Ùº«Ÿ§ Þb[®;Þb[n; Þb[®Ýºãç `ùÁ‚W^á�èÒ.š Ò¬wìt-ÀòCÁ¬§ŠÀSÏ^²,‹–mãþñ²/YÆ…[žlåÉ/Ù–…ËÖõƒ—(Ë eëÞv˜w¼¶wÅDæ�oû幀—xàýáxõ‰y'ÁÃK¼ ð¶‡ïx‰/Þ—l€'Æð_1xº‡[_3ïBð2o<1†—øªà±§¬›¯¾>õçñœàå&œnÊ™¦]^MÉÆ[zMðr“n³i€—A3M;€G4ðºÁ«O¢�מ8)x]à•sË ÑÀ›�&Á£Iðȯfç]ð(Ï9î‚ç@çxCà}þ$x_0 ÞN‚÷J€7·G»Þgà}Ñ$xx¼?2 ÞO‚÷%“àýÑIðþØ$xo€÷Ö7 ^‡w©�€çº&O¤>ožõnB�BðïR—ÄÀ[bÁ#oƒöÀãõ½ ‰¡�×÷f0¶Àãõý ^¶àñúÁ F*àñúÞ †<ï‚G hx¼¾Þz2ðŽ×ïÏ)—\#x$Ó¿ú TýðøÕ_¨ê;àñ“æÀ+U}<~V—û÷{éù õûsÀ³Ežªï€g‹xŸ1 ÞM‚÷‡'Áû# ¼?2ÞO‚÷%gï�¼0fÏÞq±Õ¥•CxæŽ ÚêÒÊ!xf–¼Π@Yv‡ð\ðˆ‚e)vÏ�5ðìŒ �g1#R“MðœÖÛ½�GÛ]Û&x´ÝÒk‚§ £Qðëð:Òå© v€À£ô<Ú¯Ü:Kr–6wii¼r+-±¾l>öů¼r+m™�êû{E éUxëÅ¢o[ù3à‘Þuà­o<{ìðÍ–S©€'VWÒ´€²\ÆðäL#M—$�e OÎT0Òd›¸±[CÙux«nì •×vk(»oÕ�,Þ}‘ÝÊ®Ã[q#>§‘ vk(»/ù&ïJÙn e×á­¶ŸÓ¨›è­¡ì:¼ä] žj¾©Í yòžj¾åÆ©•*,OõW;v<¾jðæò“ùÂɼr2�ýû½ô|Æd¾h2x2d2_<™/™Ì�Ì›Ì[OæÔŸG€ð5à<€w7x༻ ÀxàÝMÀxïnðÀxw€ðn ¼/�Ì—MæË'óÇ'ó“9†¡ËɧOæOLæONæOMæOOæ+'óg&ó’ÉœûóðÞ¡xïœxïÐ<€wÎ<€whÀ;gÀ;4à�3G‚GDwÞÃÏ ðt"ÈÒýåûÁKõ÷ƒ—êï/Õß^ªß´n9A–êÇÐåó ËÎ7O.ç÷B—êß xé§iØ/]—·ÀK×GÅmðR…ýà¥úo;Mï(Ò¯›ÞQ¤_=ÐôŽýX¹émë×ôŽšúµÀ#žÕ¯œï/ó¦¼ë/áf¼»bðÍVÒ¨Dz§Åãà-šÒòõ_”-x gÅ´|½,¹äáòCõ®K¼‚Y~¨Þißž“"X~P Uü´©XË4�ÉÓk¹–Šh¿rH” iù¡§ñóÉ+xå‡"œÆÏ�ÇÁ[8«¦ ßjý¯´ç `ùÁ/×Þ‰.íâ™BMëF€çþ¼�¼dY�=­Þ-Ï!xɲ,Z¶�ûÇ˼dny.Eöä‚—lËÂeëúÁK”eвuo;Ì; žhΉcɺU<æ�O4çä±l_/Y&Àí9qL”™w<ÑÚ3ÇxÉ2ž‚MSà%¾x¢5§�Õ×Ì»¼Ì92ï"ð’nB¼úúÜŸ·S‚§¡# `k omàQöM÷o·ÆðÖ}¹#oé5ÁËMº¾¦�/ƒæÂðüˆž ‡Î(x.xåu~!4 <<ºüB4ð\ðÊkW<ÑÀsÁ+¯µx<òÁ+¯Ýó¢�çƒGv,Ï�bð^¢Nß x_: Þ—M‚÷å“àýñIð¾à�ÆíÑn�÷éxb¼?9 ÞŸšïOO‚÷•“àý™Ið¢íKî <Ç»ÔÇ ÀÛönÀÓ®YðÖúxÖ»õz·lÁs\K}\â]Þ\8÷ø¥Å‚G,x²lÁãõ=ð„wx¼þðx}.7°?÷ø¥Å“à‰e(é­MÁµK«‡ðT+ÎbX¸Cx¼b¢Wv‡ð$xÅD5|·q‡ðÔp�Ä�˜»´zO‚WLôÎ;=Ú ]¯jèŽáùCxT'+h�§½ðl‹ONZhðˆweu¹<µ e?÷ÞŸºqð^rÃà…Cx|¯Ý¥•CxöŽ‹&xfÏÜqÑÏ á™;.Úà™!2^,cxr¦‚™–×RO.²ûòÚ|“ê³<»¨˜{WJÄWêѯàÙEÅÂ;óŒá-±[C}º[$³9½²[Cý ·Hv)òŸÈà™•ÆNÑ!ðOþÉ<-œW$ÙÉ]ç+xf¥±S4–1<½5ÔWºE ÆøÜ­¡’n$º°Ñ­eÎÖP«ndZ}·Þ\¾l2_>™?>™¯˜Ì¹ÿÀ/-Ÿ>™?1™?9™?5™?=™¯œÌŸ™ÌK&sîÏÀx‡à¼sà¼CðÞ9ðÞ¡xïœxïÐ<€wÎ<€whÀ;gÀ;4à�3›à½j2_5™?;™¯žÌ×Læk's C—“—OæÏMæë&óç'ó&óõ“y·ÉýùÑxï¢ðÞ)ðÞEà¼S་Àx§ Àx€ðN™Ç�ˆÞ€m¿/€§A–î‡ß^ª¿¼T?x©þ~ðRý¦uËù²T?†.ŸoXöp¾yr9ôçGçD९¼tyëÀxéú°ÌŠ>c邨ÈË>cé‚P9v:´ÍòV�°ÓG@ŽNÓ;Šø«¥¦wñWËMï(à�›ÞÑ6Mï¨É_ <’àþÊù>ðHãG×Þ¢™!Mz§OsðÍVÒ¨DzçŠWË´*œ,×÷¯à-|ÓòyYrË…°üPA‹Ä«õ+x _ 4-^>Pë¼çÁòƒ�óGN±– h–?rÊ…°üPD³ü‘,-å"Z~(ÀYþ(oÑLgù£¼E3 œå�¾Þž/€å¼\]zwÕ]ÚdY^¶./YVÁcO«wËs^²Œ‹¶<ÿY·œK ¼dmy.Eö¤ÊÌ»^¶.¯>1ï xÙº¼útôäè0ï$x/·í9âÅõ óN‚§¼[ž�2óN‚§¼[žÅI^²L€§¼[žCð’e<åÝò‚—øà)ï–篷ç™wxI7!^}}ôçGg< Àk < I[cxk�8h¼ë—¢�G_-dûꯎ¡Ë/D�8h�þ,{!x´h �˜‹O4ð|ðH�åeþ4x€Gj,O”EÏOêW‹Ë ÑÀóÁ£ÖXžhàùàQ0–§Á£<²cy.xƒ§‡ôn¼WM‚÷U“àýÙIð¾z¼¯™ïkÞhÜíx/Àûs“à}Ý$x~¼¿0 Þ×O‚÷nxïvãà­]öØ»t¶<ëÝúþxÞm‚§¼Ïz·~¿�à9Þ¥>.ÀsbÁ#–Fo}aÁãõ7½sÀãõï6Áãõ}ïØI<^?ð.� Àãõ=ïäy¼Ò‰ÕCy|D/]tôçGg<’aS+täMÏ– ¾JÕï�_üUVÕOÕOÕwÀã'-xª¾? ðd^®~Ãà©úÃà©úÃà©úà‰ÓAÕï�ŸÿzU(ËÞ*<¼¯SÏTÖáéUx>xì`ù<%ðô*<<Ö²SàéUx>xþ²» ï^Àct ¼Wu€§½k®ËsÀ“cv¶¨ìÞfÑ1vÇgiÍmòû~ýYZs›Å×¶À#LZ°¸ <;†gÖß½\ƒ§—Ûת¥§ÀÓK�ƒI o–Ö4ðìÁ>O <½ÔؾnƒŒÑ±×mðt�6œ¥½rðHf³E§Ëª~x²Å§êƒ§êƒ§êû“NËîkØì ÀŒúý ƒ§êƒ§êƒ§êw€W.#=§[™ëOÕOÕ¿Sð^õÈà}Õ•�÷5ï±r—àýy€÷XééÒª!< Ü«¶º´þïÂ6À3CxöŽ‹­.­Â3w\ÐV—Vá™;.Úà™!wOßi!ê.­ÂÓwZ´»´vOßiÑÏÂÓwZ´»´vOßiÑîÒªi p¾ÛO/³³][ýL<1GkÆìâÉ 3„gï©%o²Bƒ'7ƒ�MÉ’ß…¥&xlµ1©éY’ËR�;„çßZÖO®.&oÃ}v‡ðü[ËšàÉ)Xòf0x$À“«‹É›Á 3yÁÀ“«‹É›Áh‚g5£;�-,nvm]𶺲 ºÔ—m�G]]Û<êjéÅà)èˆ(˜¥ À£ha–¶¦-‹Î¬íiðÌvxµ‚-ý9:²xz;<µm�×ÚïÏe¸à©íðô~x´žÞ/—Vðh <³^ýž‚G[àÙíðxdv¨ï/Ëïv+àéfWË4qò‚2†'g*iú€.ðì ³Â;[fþ©­¡¸qZ@·¬¶†âÆé’&ðk¾f�7çq²ˆ�ÝØ­¡,x+qªœ^Ù­¡,x+qnÙn eÁ[�Ó~Ý×­àÙUŪ˜›s²TÀ3™ûg…wjó½5”o%ïë#ðTUm Z{·Þ\¾j2v2_=™¯™Ì×NæèÈÑyùdþÜd¾n2~2a2_?™w›ÌÑŸ€ð.:à�2à]tÀ;eÀ»è<€wÊ<€wÑxï”xï¢ðÞ)ðÞEà¼Sæ™o˜Ì7Næ›&ó'óÍ“ùK“ù–ÉÌ‚ú“yådf?À/˜Ì§Næã'óÒɼ×d^8™£xà<€ðÀxà<€ðÀxàÝxDÔ´n9ðÎÞÃï{ <µ�Á0xõ§À{¨oË�^º?x©þ~ðRýýà¥úûÁKõ›Ö-ç#ÈRýº|þhÀ ¼ôãí/Õï/]`EŸ±tAOÙg,��Šü€ÏX:3Wχ¶YÞØ‘z>´ÍòæîÆÚfy/Õ?<’àiüj¹éñ¢ÏaÓ;âE_æwÄ‹Cà‘ÏůIð ~åüÑ€íoÑL‘V‰[<ŠÁ[4óI+Þ¹§ aù!sÆ…“eq¾–ªgU¸F¹–ªg¹X¿Þ7ÛóE°üP=‹Åcç‹`ù¡z¦Å#ï|!ìkå?ŒæˆGÞ~{³üP½Ó¾µÀ[,+ åúçoñ+.;^¹–àòõÞ�BX~p€Ë—«âr ˆ–àòå ðÏ"àrýx‹gp¹¾ð^‚4Ýœ«â‰2óN‚'½æ—¼„™OzÇ ¤|u/a&À“Þ…�<æ�Ozç5òx‰/žÖMˆW_ Ø x€GÁXž�ð(lùIð4t$dÍ=öB4ð({¦û·qY4ð蛥l²èž <Êž5‡íÄyÑÀ£x¬.:/xòßÊÆðHl-/xÃÖ^Í4íÎÅà‰c¢,x!xâ˜8)x!xü˜„O4ðBðÌ1<ŠÁ3Ç<ð(O»ð¾a¼oœï›&Áë® Þ7O‚÷—&Áû–Ið¾v¼¯˜ï•ƒ÷‚¼(ð^€÷©xŸÚ ÞÇà}|'x/ À{i'xï€÷^�à½0ï…·^êÃæŽ¬õŽ�÷À[»ü¡wüü6xÊ»Mð†¼sÀSÞðäy žõnýy=ïðïR6wdÅy žã]êãv‚縖ú¸Ä»¼¹`Ás¼K}\c|�±Ä-»ü‚Çë{Ð ïðx} Ü6x¼¾€Ž¿È'ðx}]/� ÀãõnµÌÎ Ø8x$Ãç("ðøùoTõÙÅJšOœÿ&Up�ž^…7 ž^…×^*{«ð6À#½O¯ÂÛ¯üþxzÞ(xzÞ}€§{´p!xlŒ®F8ÇàsÀÓÞ…ëòÈÌÒšÛ,þ¢®Qvo³ð')ܲiàUçl ¼¿ä€Ço£ø– ð¾Å�ßfñµ-ðÈÌÒšÛ,¾¢ ûyx| ö•€'î£þ1<¯�·=†gÁÓ÷Ž…cxdfiMoc ¯†�'�Pc /OÜ;F·;†G2Ãà©úN‹Nƒ'Ï«úÃà©úÃà©úÃà©úx:>†'g/X†ÁSõ‡ÁSõ;À#â-@Uÿì੯? žª? žª? žªß^¹Œôœ.ñu'�à©úox߸Þ7ÞxéÆÀû €w(x/x»Á3Cx¸oØêÒª!<²]Øxá³kwiåž½ã¢Y6CxæŽ Þ7ë.­Â3w\�íâÊ.­Â3w\hð¾Ewi垹ザº´rÏÜqÑÏ á™;.u2?™—Næ½&óÂÉ Àxà<€ðÀxà<€ðÀxà<€ðÀxà¼KÎ3[|ëd¾m2ÿ÷dþòdþÊdþêdþÚdþúdþÆd¾p2GÿAn+à<änðÀCî&à<änðÀCî&3àÀ;!x¿Ï)ðêO�÷P ¼‡ú¶MÄÊE°ü�5ãÂ5ÊE°ü�5«Äér=RÁ[ôZ�«×·½[ÎÁòCÖÌ’W°ó…°ü�9³ä‘w¾`–2w–¼x‹e+hµ~?x‹n…¸\à!gN'xɲ¼l] ^²,/[WÁ«OÌ;{Z}[ž�2óŽ�Çžlù/gû*xɶ { ¼[ž x‰² žÛƒ-²…¼DYÍíÁ’wžyÇÀs}k€—(«ày¾µÀK¶eá²u9wFÀ£VÞkñí�µ·ÁÓ/8|=àQØÅí�š“Ûàé²ì‚G¢E§À“-½£ÿ‚ ·•.ðä*¼�•Í*¼ž.-ëÛšUxÞ:¼òópðÒfžéÒFॲY…gº´<ÒëðÄ*<Ó¥%c%ѬÂ3°mƒç¯Âc╯ð�ÇM?xºGkgi|xbŒN�çŒÝñYZÙÀ£hÒÂ-ÛõNZHðì,,™I g�žmà9À5ʶ�§�k—mOgÀc?O{®Ë À㺙.,ÀCΓMðH¦<ÙâSõ�]ÀCÆr&ðÒö¯vŠ}¬¥ ^¹Kµt¹{`s©þ~ðRýÝà¥ê— ^úþ&ÀKo²7§oáKµè*qy{3YZÊE°ü{WO³r,?DÜqòX¹–ú½[./‚å‡ðrý Þ—ׂsÀ«?Oo±Ì­¼Å2Úx o«qõûëoᬘ–ëË’&à!Ã9xɲ¼l‰ã¼dY^¶Žœ2ó®{Z½[ž x‰º^ðr뮂—(ë/·î*x‰²~ðêón¼DÙ~ð’m<öÔ^²Œ ·<3ØÊÀC&rJð(Oç ð(OeÑÀ£†wõ…¸N4ðúÀ׉^xâ:ÑÀë¯\¶¼ ¼9ðh< Ý–x¼,š’�·ô2—Ó‚÷êIð¾}¼¿9 ^w/¯o /¯k ¯Þî1¼žmxß5 Þ<äqó8à¥>¬èºŽ€'GìšÞ¹àÉ; žlzàÕ:¯,‚xu„Ž(ÐɆ ^¡cÀ[ë êˆ�ÇÏ{à­çmÄÁcåð̈^*๮É©Ï ð�á̃G2fVb xþ*<½LExÈxNžîÑÚYZ·ì4ð¨FLR¸e§�Çê7'-$xÕ;Vß+ñA=§�Ç®Àû[xÕ;Vß�Ì,­jà±ú.xâ¼ÓÀcçÀÓÞy“-ðt�5»xÈD¦Á#™aðT}¿E×OÕ7-¸-ðT}Ó‚sË « À“çU}¬®žªï€—¡ó»´ª>ÀC5— Þ·O‚÷7'Áûޏ.ð¶Æð¶ÀûÛ“à}çxß¹ÞÀ^xßð�ËÈ)º´êFZ Ü«·º´jO÷íͲÂÛïoê.­ÂÛï;t—V ám�÷·t—V á��g‡ðÆÀ³Cxcà…Cx�à™!ðÄqÑÀë¯^^ˆÞx]À5À£Ið¶ ÓâiðrN7åLÓîºÀÓðm�W`ËÒ±†_xºAð"èzÁ£8W¼`hN�'‰ò±à½f¼×N‚×7†ƒ×7†ƒ×5†×o÷ž×£ÝÞî1<Õ£Ý Þçßxt*ðì‰cÀû¾AðþñéÀ{±ïÅ—^ê³æŽkË;¼Ôg À“ A¼Ôg-°­]~×A¼Ôg%1fWÊÒAM—Ë¢ø÷TýìY-µËß«êWß(§ËâÀßWõ{ÀãW¾ªß¿úÀ«ßÏ>ðjý}àÕúxå¤ ^­¿¼Zxµþu‚Wx¹`ÊxåÑŸ´ µõGxå1 '[v´¶þ(¯<Š.l)ÓÚÚ£¼ò(&)t9OZ¨ú¥ëšKN™÷pðÊã \³ì�Çz¬ßë•+‹xŸºÆòÖ«ðÒË +¯®W_xT&%HxG’>Š&-HÍJ�‚ok � t¢EGf–6� läAG[àQ�®Q4iA²«Ïöh©¶órqý�¿Æ–�m®6íH”^[8=Zªí2^cŒn+Ì.jö]ã.m{�n#›ƒtÙš”ØŠTìÅÃxà<€ðÀ{ðh¿wi o¿wÿ°NKìòî•Y‰}Þ}_�–Øå]ÃÛïÀxïìàQc�®¼p„®¼æ(]xÔ¥ë�Z£tà©;€ðÞƒ÷ö{W'-öyW'-öyW'-öyW'-öyW'-öyðÀ;;xÙ¥œ?†·¼‰ìRÎÃxà<€wQàÝzfAýþÉü“ÉüÓÉü³É|édŽþóÿç“ù'ó/&ó“ù—“ùW“ýóx“x༻ ÀxàÝMÀxïnðÀxw“†erÂqðêO�÷P ¼‡úMë–óÖ=ÔŸï¡>Àxcx“Ù•íXv�—ª_xéûí/]ß:pÉàÕÍ<ä�^ðj}yd¼TA–h¼T!.·Àã»·ðc½àñúâ(À»„Á¾[î¹×/ÿyVð¾pxõóPÁ[lSÀ�€·Ø¦€oá̚Ƹ³äqðÍVÒêõÒ;-Þ•€güϸ^¾^|ñÞUáty<Íßð¤~ï¸0ïºÁË­» ^¢­¼úļ›/Ѷ¼dY {rÁK–UðØÓêÝò|5à9Å@<<¯Ü^Æ­€Çv€—m£ Ü×¥Þ…�¼¸³ªxï""x}à‰ã¢�×^9¼¼ ¼9ðh<òÁ+‡õy ž†Ž$€W3†§Ác ¿=àñ–^xJ6R n‚§›rAgv/�íÏñ.õq/¼Ôxcƒvƒà¥ñ¹ Ú8x©ñVû®ÿb¼ÔgÍ×ï~@vZ ¼@<†›4M6ðÞ¥DÿKÝ|NBþ#A¤þÈðj}<«!À;,x¤ÿÙGÙ²£µõGxå1 çµìʤ…¯„xd^Œ�GæÅxd^õ¤ÅxòEx¶ÅwA“Å1r—£´½ûçq—µ¼â˜×ÒÛòî_°.«×ÒsÊ<¿×^¡,¯pð.!vž^餈–ßòlWá9à•úþ:<¹ ϯԷe» ϯ\ïƒgVá•ë½.­O¬Âóº´þú¼ O–ïàéI º´xR¼À»xQ¹ <wYŒƒ§xïRöhÅ �ï»ð‚YX1iAö|Ø£­/X}[{´¢ Û^õŽ]¿=–gxMZ\3x‘wàÑÆ¤…ž�¤ˆ¼óÁk–÷€×ÃÛo}ð.$$ã€'[tú¼ªïON8-»üBÕwÀãÐÙ²ªï€—¡ó»´ª~› '+TYÕ¿QðBïÎ^èÝ#�G“à�àž.ð¾{¼ïŸ¯Qî÷Ïž(ÇÞ�¼Ø»Çï_N‚÷¯ÞEÆ ám‚÷ݺK«n¤Ï á ‚g†ðÁ³Cxcà…Cx|ïÊÁkx÷àý ^Ã;¼Ø*‚·ukÙxá€wHœûÊÈ›�µ][ ›‘%Ù•%6”ƒ'o¤à]WbCw¶ìÜWF²+Kl(Ï]–b‡ðØíÎÂcRËRì­k™É‹‹/Ú Ë;oYŠ?g/<¦`²‚=‡Þ9ËR~`ëy{Y {¹ ž³,Åi̼ Ixj!²Z–² žZˆ¬fi·Á ‘e¹ <µy<¶°¸ÙÒsÁëëÊ^xÄ\Ó̵¸Ëà�m å·ôðÈ�¼ÉÚ<òggý®­ðþ¥¯¹[ÊÊ›ï_¼ËL½±B,¸[#n¥%Ö—-gë�bÁÝq+-™YÛ<Òà·–ý·\oœ�·Ê’nyM¬/[Î×'ô¢Ñԣꟺµ¬Ü8áÞ9æ¬Ë»ô[Ëd‰—ûn-“%ï€Ïìxç—Ꞻµlh;<ÿÖ2e›:«Á³·–¼Ë�»5TÒL'®©º[C%ÌH,4¶ëò¾ÿûWðÌ¢âd±Áº°ìn •,“Àéuye Ïn µZ¦…“”1<9SÁDÓdñbÆðþ¹³yÇm<»y€Ò-äÎÛ<  ÷/zÁ“›âk¡wîæÖ¶ˆ;ó7€w!ùîÉ|ÿdþÉdþédþÙd¾t2GÿùûËPúã Ú …ÙE;kÜ¥eªýÀp¤jÿr8¦53úçð&ðÀxw€ðz&'ZàÑ~ïÒÞ~ïÒÞ~ïÒÞ~ïÒÞ~ïÞ¹ð^tù…hà¹à•îx¢�ç‚W»çE�"ï®i �wew�ÇZzîëÞ1<Òò Žáiè\ñbï xÖÀMðD{ÎoÚ �ç¾ðž™ï¯¼×N‚÷C“àýëIðþÍ$x¯ºðáèŽÀ{�ÍÑ­¼áN ÞºrðÖ.y7xëõjNƒ.¼Ôg À“ Aú/ŒÓ£­ÀÕ¦‰Òkë§G[ASëPä2¼Tvz´8Ù…Õ#x˧G[�S-:ï|c�òÜ-ÉNn=~]àýp7xë�ûo÷‚'�£çxØo­ÿïeÑ]ˆç€·ú/ÌU‚÷¯¼W]9xƷлû/öîþÀ ‡ð:Á ‡ð®<3„· Þku—VÝH;žÂÏ á�wiÕžîUWžñîžÀû¶Ððî?D ¿Ób ¼Ë»µÌÎÑv�G<1CáGnùè¿0Î}e$—¥P0‰!Ác÷•‘\–Ò›ƒ%Ùu�À#»mŒä²Š'1xz™�íÚêgºð̲×»¼`^÷^°Ï+»³´Á:¼î1<wêéÏúËR,xÍI ³,EywñàõA—ú²xtÄÀcå£ÿÂlƒ×îÚnƒ·9K»ž]^˜¼ ^£¥�Ç7»¶1ÕÜbï~¸Ê–�#Ç»-ðˆGÊ;òýànp–6ƒ�×â.ƒGàQc1ð*sâ•öð8ðÈoM³œ»´ä€·F”…€¥|ô_˜zcÅk¹xk¼¢\—Wo¬ø!.Þ¯¨÷Ã# žÞOË­´+xdv¨¼¢ºµ¬Ü8¡¥á*k‘,o‚§n-“%q€\ðÔ­e²´±žsk™,ýûðγž»ã‘ݯ÷Ö2Í›=+ÀS·–É?òŸ.<¹È®�–×eðÌ¢ât½,êÍ.f ÏÙŠˆmP„S¬·–y[C±Í qZÄú¡<¹È®4âˆë‘l–óîÖPÉ2-œ{ÞÛjµL'/¸˜1¼V›XðÞ¹›(Ü6ÁÓ›(ÞÜù›ÝBî‚ÍÁ3›˜ö\è]°y€æ-âîR¶xÈNæè¿0¯�ÌMæ_OæßLæU“9úϯ¯ãúÃmð6º­­lÒmÄNRŒå?xÆ ä?FÆuÆ´‰þóx�€ðÀxïjÀ£ýÞýp�¦ØåÝ¿­Ó»¼ûwušb—wi o¿wi o¿wÿ±NSìòà�;ïÀ#v;À Gè:ÁkŽÒu€G�AºðºNð“]à5&%Àx'ÌWñöxW'-öyW'-öyW'-öyW'-öyW'-öyW'-öyð΀wàMd—rþÞ~ð&²K9 o?xÑžï‘ðÀ»!ð^7™ÿ<™ÿ2™£A}Íd^=™o�Ì7Læ(Èrh2ÿu2ÿm2ÿ}2ÿc2?2™ÿo2£Þà<€ðzðæðŽ ÀxCxsxÇ༡¼¹¼cðÞPÞ\–=üušï¡þxõ§À{¨ß´n9ÿX�õ༡t©–>ýà¥ë»Ê®bé|¬;ï*–Î_xé§ÖrxéúW;Å>ðR…ýà¥úwÞ€…–0þî;Á[ê6Ê[à-×Ë’<´Þrq£¼ÞúÕô�‹oá,0­þÂbð¾VÃêõÿy»\Ë™³˜ðjý+ÏoÙÕ2?lÁœ´Põ½±¼ÿâ¶üÒÓªúÞXžnðñòìïϯ<2ðrÁ”ðÊ#/줅ª_„Ë¥TÌ6wAxå‘�— ß Ï?d½¡ÉTØÖã®sÒ‚mDœ8Yj‚G 3¢�àQ²Œh< À£2)±<Ò³îž>}h—ÖÂ#Õ…uÏ›-[wÒS6=ZÖ¤k Þ•ó¦GËšpj¸N¡Sü«hN�¶BWÀ«ê²Ó£­ ت†z•ŠÓ£­Ððª†zŒÏéÑVè xUC}þ± ëÍ£€WÏÍ€×ãÝYÀûæôðÄù1ðÖßçÿðnxgzìÎLZpð´w³´²�Gñ¤…³NÏ6ð(ž´pʳ¿¿°GkgiÝrØ£µ³´n9ìÑÚYZŸžîÑÀÝ x�âÝxÞ,îxo¾x£ÞÓà©?ÿÕ3²¢”ÙyUÀâí�è¦ÀkNZx๸]0x¯Ûïugï¿L‚×(Ÿø÷wà}+À{tðú¼»"ð¨<×» Ïá��áõwiý!¼NðÂ!¼3�g†ð p¯i–ÍžîÕ[]Zu#-Ù.l<3„g€û€ð~$¾µÌ‚ç{waà‰!¼×ñ®k ðÄìðÄ … 5Á3.pä–gÎ}eäOVøÏÎ}eÔ3Y¡ÁcS°LV(ðH€ÇV“?Y¡ŸéFÀ Öáu�á9ËRØËmð2f´¼Íe)³´[ËR¶À3ËRbï.¼r¿E ]>oÁž¥UàÐQ0K«À  #+ÏþþúÁ#·kÛ¹-½~ð$tyar?xtÃëðØÓ¥ïþkåM‚×éÝ«ÒÍ€G ðjû/�Zà5¥dðˆ�¦]k7ûFÿ¼OéUxëGÁ¿•LŸÏpIðÖt”3\¼5òVZ2³¶ x䀷F”…€¥<ûû«7Vˆ›ÇÖȲ>Ÿî¥%³s@­o×ÝéÓõÆŠàÞ1VV·ÒR�Ìε¾,ßÇ­e²Ôž½µŒ†Àûïµþ—E¿¬ÁÓ·–‘,«³<}kå ÀS·–ñß®={àÉEv¥ñF|X�´ˆ¯«àñæ#­«,·†äÉ"¿†�—[C òdQo&p²1x>tù…hàu�W.[^ˆÞx[йâýŒ HÍyÁûŸ“à�4ð<ðv�á©-ÀC�«Ì!à‰»àÕ:¯¼ Þz}A­UÞÑ;úA�šÇOÝlm')6ÀSõõ¤„-+ðTýêY!®Y~FÕïOLbýŒ HÍ!àõ´ìʤ…žnÉy-»2iá€W\[_´Ê¼í±<~øè?`AjÎÓ¥õ‡ðH¯CÑëTÊ:<�ÔÓäû_y–ÖÂ#uÄ-›-kÂEåg˜xGÿ#Rð‚ÜMΞY…çâQ¸ϬÂSƒxªLzžY…§íZe³ ¯gëýŒ HÍùÀ³ ¸&t|–V5ð䈞)“™¥U <9¢·Q¶ <ꟴxrQyô¿�jÌõn»e—_¨ú«oª%ç´ìò UõŒwaÛeU¿¼Ò¹xra9¼ŽÙÙ&xÿ+® ¼ÿ­€k”w�‡1<¹Èœ£K«†ðÆÀ³CxcàÙ!¼1ðÂ!<€‡ W—³�'†ðÊP^?xbïñ®lT–à‰!¼ÿÍ»®í²;GÛÆðäÒr xå~‹®YZgÎ"OUxe3KëÌY䩉í²¯ºgà!È¥å à‘^…—‡óxS�ªz?<Ò«ðòpÉYZ]Îà‘^…—‡óHÎÊúå ™¯ŽèÅetiäÒrðxóíÖÆña={AÞñØn µbFb¡1¿¦p—ÀÓ‹Š×Ƭ Ërk(A^X^KA.,—±Å{#ÿk2ÿ{2ÏLæè?`AjÀC�» Àxr7xAî&à!ÈÝà<¹›<€‡ w“«ÿ 9 êë&sôÏ� HÀC�» Àxr7xAî&à!ÈÝà<¹›Üx�ÝXºTK×<¹òÜ xþ¿|Ëy öÛ+¤å‡ˆ»€¼£~Aús#à™ùV�ÇžT™y×�=<¹ÚÜxäƒ×þ73D�Þç�þùéÏm�÷?'Á{ÀC�[ÎM‚'Fìv€'Gì,xüüÑ??‚ ý¹~ðHÆNRl€§êÛI ž8ôÏ� Hn¼ž–]™´pÀó[vµÌýó#ÒŸë/Â3ëPô:•²ÏÂÓ]X÷üÑ??‚ ýxAî&7žY…çâÅëðÌ*~âY¸æ”»]Åœ=rfÅ¥wÒ�·¶þ[uIŸž»-EÖ¨ÅåwªšJâÆËõç¶‚ûÌÍN’eqªØ[Ä]¢I³áŒ›ö_dq�uw‡ÛEθY·FÜO|±IÜ5긛½Í­—>—êë‡ku ,‹û)ÞUWWo×ñv³í‹¸öG®Í‘J;nÕ¡.ÕsP=HÑΪnª|«5€oËò®ÕWU;­Š qé2—›mmŽèÜÅ.E½9rNq±!ûÓ2¤†èÔNï_Å¥âq^‹÷—¹+‹óiÙÉü}J\þ=®,nìƒ�ª¢²Øår¹Ë#š¦K$¾‚B—+­ŸíoÄ¥›@©Ë¥òwqc%3méÅ2äN¶ïdý”:ó¦!íJ�ü´b¶M\©“/óõ)q-Sû¥ qˆ{~Õ�Ÿˆ›1»É nÕÁ'Ä⇸&@â×ûŽb°ÄŠ®ÎŽ²î¨¦œÍŠ|YÓÆ?�¸ZPËŽÓæ”âÊålVܾm8 âצ¸yü™Žw“.ƹ-ƽ¥xºZM–‡·†«r\/Ó�¿°Ç·)®“ù©Äß™ü!.F2TâÉ cŸµyÜ›Këõ·mŹ8Âo¾0—a|ÎG]œÈOQœÊï—jŒ?‹§³¹‚ò H¦ a1î- ¶’¸Ðÿl[7¬—Ã}Nî×ÅaÆ84óS·5ù=¦Ž‹.Å“Íʼnuâ½É°W«Í’8/ &>öq+�†jöù½ÌOQœÎïûÅÕ§·’8žUvÔ0¿ ^XM›™€ÌÏb(Eðгñn§Ï£,ΟAÜ,Þ­&N„k= ßÊâBíî Åõµ·/|¬FêÕn›¸1½ãÄ¥@•³Š#óÓê üž8ÿBgOý®{~©±âBYœk¼ÚÏo7îu!q:ˆ¿Øâ7âÔ†_WªãTsd}Ü›jŽÈb¿*NfG‰ eqkš#{Åy%ÎÆ»•Äùw¶÷¦À]*Ú33³`¥i1¥o.UÛ<‘ù1â¼ý¼ÛÕŽ3ñgóx·XžB—Kˆ«Ä½%qbÇMââþN&õ ç7«Zü�ù|�8gôñ»\ìäËýÊqoY\<Ü&N&�ÏÄUâïL~÷ˆ{�z%þÊ´")Ä!.$ίÊëuÄ­ ÎZ—К¯àJâJˆø¸·‚8Ä!® ‡8Ä5Áâæ”{?gå“â.¥q_·V⇸ϋKãÕø²ü‹syܰmŽgâËò˜ âyLm_æâøé0&b¶»Šóz@qUqΗę�‡ÙvqŸו#V7Ûî" î âæÏ_+‹‹Û]Šâ Ï_+ŠKÛ]Šíâìó×*ͱݥØ#NêÕÅ™€¾k°_\5¾L¡#n�8ÝÉGœ÷ËAâ׈CâšqˆC\ qˆkÄ!qM€8Ä!® ‡8Ä5⇸&@â׈CâšqˆC\ qˆkÄ!®Qq�‰o¨9qÆÅßoq'qˆk]Ü—ÿ6ՅÞ¤|qîjPÜðW•qyŠ“× $§y Ä!qVœûuâb‰÷‰�Åøoü ó¿]}¾ÕIœyz½|˜DzŽZÞ//gÓüdzùfqû§—J³ÀXqµùV'qf¾„4·ŽžÕŠSsx©tìòÝâÆä÷ˆSgN¾Tç[õâ ÏÓNuf>T{©ZqC:¾ô˜Ž7×qc±·‹3WœªãÔät… Ît>òÓ#âç®$ÎÌŸª&Bû¤¸ýS!«Ô^§Ëe§Þ›æ¢š‰sfP;õ^ÞåÓ í˜—Ëž8sq•ùV�®âZ×ç}g\m¾ÕÙÍÁŠ‹wÛ1Ó€¶zÆ-Öq!”/U;Å^{âöÕq]õ®ê;qÖ•Äéj®x©JabÁSˆÛ{W�ê7ˆë6‰sóºñ âŽhÇ©žCQ\X#.èírí¹¸/5Gé9ؾª7u\y¾U{©æv\ΙÜÏÉsK— Ò~çSÃŽé« þŒ•�¾«êi>c�«].³"MZž&TÌ©­»\¶ÁóŽ甸ê|«³vœž;‹²½w{ Ö:ù]kâz^¬Y]#�ë¹}ˆ»¾¸s=·qŸ¼º¤¸o»:™¸F9La^Åý6‡8Ä5⇸&@â׈CâšqˆC\ qˆkÄ!qM€8Ä!® ‡8Ä5⇸&@ܧÅÀGù±ì¯•-GÏEIEND®B`‚urwid-urwid-69907e4/docs/examples/real_browse.py000077700000000000000000000000001526073734300260352../../examples/browse.pyustar00rootroot00000000000000urwid-urwid-69907e4/docs/examples/real_edit.py000077700000000000000000000000001526073734300251052../../examples/edit.pyustar00rootroot00000000000000urwid-urwid-69907e4/docs/examples/subproc.py000077700000000000000000000000001526073734300253622../../examples/subproc.pyustar00rootroot00000000000000urwid-urwid-69907e4/docs/examples/subproc.py.xdotool000066400000000000000000000001361526073734300225630ustar00rootroot00000000000000windowsize --usehints $TERMWINDOWID 39 18 key --window $TERMWINDOWID a n y t h i n g question urwid-urwid-69907e4/docs/examples/subproc1.png000066400000000000000000000104531526073734300213140ustar00rootroot00000000000000‰PNG  IHDRp@(Ò cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñe£IDATxÚíÝ[–£¸EQ2GôBýošúQ6$=°ñ GÖšuƒ’¼~Ü?y“®“ªå:__?ÝAýcÊ„ÕùÛþþtÿôÿbà„2pB}à9\jÏ4; sÙüÕ§u>­z§Ï8Âyü*½:ïžÏêÝ[~úSþÑ�û¸lþ½hÊÀ ÕžÃ}¿¸Þÿ•¶-oi+«·+�Þqö~r,óݾ®ŸmrQ"‡å¢þÜ–ú§uw£!�V6UƒúÛðeè¸A;?í|´ûëö?j¶?ûó~{¤y:ƒçkY{„{dàðü¥tØÕ¾¼oQ>œRùPêÖ�K¤m[[Ÿª½u:”%ªi¸}=þºþxz¢ùK'·_˜¢Ùø»õïaëý=v*¬õdù*õÖ‰Tü ¤m Úº_lþ8ˆn½íóÖ<ùÅúmRÎwÑÿûƒ£6Ãíëñ ÑrПjþŽãiç#œïnÿÃöíxýÉéPªKô|�ŸÃ¥mòdzÏLwåv8”µ›ÏêË/¬_‘·sEŠíëÔÅfÅs»ù°+Ágû?\ù'¿zÄ:+\nÿµnÿyžéVPÿ%i2ù©êÿ¾‡ÛGã¯ça4/gç/Þ~}îâñ�êŽl—{Ï�ßwýy|®æ¤àWß:̧NàNm.§-åáŒ�5‹Û¼þ§+G˜÷‹ /ŸŸ¡W·m<·!¼éÙyÃ}¸´m[Î9ÿÞ<�µ5u–Ÿô¶Î÷^RÇ»›eižµßû\„ã«Ï�òü¢è¥ý½y<·sõ·ì5<ÂåÃß7¾ì˜'ËÚ¶mõ%µ¸É¸zqJÄrÕ¿ÁúÕ,Õ{µ¿ïÚþìøÛú³Å­õ¿qàz÷…ª·Zº{yâ>Ûkñ|S¸'gý)ê•óþŽž¥trû×f¤OΓÏÎøâm‘òþÌø”r_�×ûôh2Z÷Ý»I¶œæo&ŽÆ;Ý_ug¾Y>Ùßz>æÛOÛŸϽaÞßyæø|�­½¤æòÎêãtôÎ֩μv“‹Nu«¯ï¤o¼ºi=+yµýt>æÛOÚÏöv¬ÿÒì­¿çüéTr¼p�÷Ü„»¨ê¯¾CøãªkÛæR×Ïà ¿±}I=�L¿ApÑðìGëV½¡~õM}®ÎK*}kû’jæíøã³¤+Í.>ã{©úwLNÐ= åm¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('Ô×Ow 'mÛ¶mù§»¡‹}ÝŸÙ›Ïy~ÓÂÚÜ{ Õ£IÁbû@Uï8Ç ‚ «êG.i°¿Ô ê÷ÇÛ¬OãÍçý�ê×õó9˜¯?ù™À¥wg³~›•Á„ì|(G«Ûõi¼}¿~]1Þ¾PŠ[w½< Nÿ»îö7¬ß¬7ŸÃùú*K}�<[—¶t˜À¼U3Ÿ¶}}ÚØ·¯Ö×õnåëËú‡g¥S¿éϤÿešúMµp¼Eÿ«ñ–[OûÔ?e:Ÿ£ùú� 9ïI�‘Ÿªï…t˜ŸºýaFªz·ÐEýJS?ؾˆ^½¿º?�ú“ñ>Ö×ý©ÛOûÛ©ßoÝÿá|ýÆÀMäê{�çy‰‘t¢QZÙY®Û\4¯ r¥ýÂ|Æó5XŸ%¦æ‘ðœ3O[f—œÅúh“‘,Ÿ{žIR¾¿fôë7O¶?kñˆúüxÃþ¾Z<_ã·ÿ­þÑý›K[°>}?®Ÿµÿ‡¤«ø�pÕÕÇã0åíñZ]þê“Ô}’SѾӇªýâQ¢¼� .kgùM£zã½Í«�«-T<\ûEGÚ¶~w�ó#uÐzÞÿñ|vG÷u\ÈåÿT—!';P4y¢ýÂØª“òÉ�$�«×NW]ðbû¶_),Îàv{"Æã ë}_µí‡ý}}>óµpÑ0;Ú4WQ“Á­µ?%圣'uT÷ÌßpTÿ8Ò4Ú~ñõ�ý9Õß'ëwûíãûpןM]{«¸¾�v{°xM^qûÇü°^³Ócý¼¥ÁöÓþWy;ÖOYŠúM½¢?Aû•þú?›ÏÁ|… çm¿ú¼¶ìñ¬°�—ãø£›BÁülÇW¡òtü·ÒÖ/³·Žïå‚W€<¬Wõ§i?îï3ã­û?h\4<Ú^�ýFõÒ]‰õª““²ÃÎòdý´^Ô¾àÊÆÕQñ¸¿èœý…ù nÁuûß›‚Þ§E.¿ŠáüìáøÓdýù!uë_Ñß—Ÿƒ“Wé¯Ô_™Ïûzî�†·ÝŠ¿žF›Žç§~ èDÇ£íóÍÖ¼N/ôwq¼OoŽï|ýå×Óm¸Ã ÃÒUϳ×϶Oá?·µã{Ö›­ºäàÑÜç|±þ‹GØçKÍûXþÛ-Ñ\ƒ”gý§Ox.{I=¼/¾%óxï¸éüã†vΓz)èoê�Võãíûûkú[ÕïýAÆ[ö¿êOÓ~±¿ÝÀd>Çóõ'·u÷Ûˆe‹Y(ï4¦`unêõ;íÅ×èhמ—í�IÉíêfý¬Þòør÷�¢I½¿¦¿Ýù‹ç{²º¿Ãnû¶2ŸãùŠ·¿ožª.´‡….…w©†¯rÍéÜæEÿzÐμ~¤W¿ó>Ä`ûºÍB¢—¨Ü«¾ÑF¶»}Ûßb¹¹�?ëÿh¾þ¼í\^ üƒŸ‡Ó'3pB8¡ œPN('”�ÊÀ eà„2pB8¡ œP·�˜oÿGïþǯ©?Þ/¯Ô?n0k¿õ?®0üpˉO ?<±°<þxFÿóIý¯m ÚÏæsa~A_‡¤é'‚ž3¯Ÿ†Ôí¿¿”’Gåêï §ö;ñòìó¯ÑG3Ëúåx¦ýŒùøÏõï Õó™ÎxŸÛîþ—Òÿ}²£zýly^?þ¤m¥ý¶u�¤EÃþï½Må*TãåzÕx¦ýݦGš{órü½ç£Ýÿt~A·-ú}´ÚKŸŸÕ|F9/µ?»÷Ùþ¦P?2ÙY®?0=Y?«}Ϭ¿�1Ôû«3;l<ü½·Õ¿h(ÿ†ópýÊò¬ýÙõSñÂñ\òóF>�)ê|žµ9»�1¼J]üªáòrܾhù)L�ÿlÈwc_úöÚëâù|ãü>aégó?ÿ÷ÍÚïœ?¾ñeî¼?çÆ»Ôh ¾WÝ98ûŠ·t)ׯ}鯳E÷÷Î>á",Ïr'ýog$÷×FßÏ/äx®ùÎø£oñL¬¿¢Žëo�×¢¼Ô¾ù½³ývTêNiu—í-¸æÎÊ^wÚßjyÚŸx¼£ùœÌ/hé÷áÞÞ³áï‘Må÷ù¹ÚÕy ö7OÕßz¹ÓŸTm9êô©ß{cÝwïOy·qØÃÙ™iõµë•úÅ7jŸöR·}J }��ö÷èÏB£åfÂÛTo¾æõ«í¹<~áz÷y®:À�î#­ìáÑ~z_+¸é4øAÊ×Ç7ù½µ“÷áâþ´y›ÍÕhù®Rëß+kêoâo�í·xf»õ³ökgßéûl¨ßI×>¡�ùÎ MÎî‚×?‡cnÂ=¿~ŠžÔ7îoåøvv¾~è¨7¿wxä=]8óË“gKßëoKÔã¹r|ûþ^®Ñ~ZäÄëi/ÿ¬Ãî…W¼¥·µÒuí{�ú”s•¥§xù�NÞ^8Ãioé]ý»Î]�×~£·k§oÂMë§Îúº}ý&~*6ÛÿUþþYzç+^°¿cÿfýmúÌKp‹·>ŽMæ³?¿¬ãçáÞÕ‰YýÎÿqŨ}®¯î3ç=n‹ã«>[ö²Óßùü—[œ�ÏÙü¢¾¾?ÜULÏ•7á¦õsó^â°}næ³Z_ï/ׇ×kuöwxcaØß¦ÿk»;1ŸãùeùûpBù%¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ eà„2pB8¡ œPN('”�ÊÀ õ€ !¢'.ÛnIEND®B`‚urwid-urwid-69907e4/docs/examples/subproc2.png000066400000000000000000000133261526073734300213170ustar00rootroot00000000000000‰PNG  IHDRp@(Ò cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñeNIDATxÚíÝ[z£:…Q¥¿ÌBóšæÑ¾éRº``S±ÿõpNc@Ê •Ÿ�Wwß…ÀAŠÀAŠÀAê÷ê´biíEü=¿!–/ðcÅ™|ìáØ•öއãñü½÷ÌqCÎ;‡uœ4@ŠÀAª=†{}¸ÞÿCH!†âcív¦Ñûœ»Ÿ‡äͼ֯Û�Ù:©h"™ÍYý¹Mõ êîZ›4šÙ¼T5h´†‡ùíø´ãѾ_·ÿÖúÓþè´{¸g²Ÿ_ŒÙ¦<&C}‚[mòs±ÇK±Û¾ÝD am~¬Þ­Ó¡8h¢Z!—¯·¿n<<ÖøÅ�Ë/ Ñlû/²x–zÛÓÅ×ïà-61u—.¶/îDCoùÔ V9?Z‹äKWý¿¿8Zg¸|½ý1”¨§�þTã—oO;æxwûo®ßnÏUìc¸&¿�‘éÎ ùÇe³ø¬ýaó óW¤°­‘bùºuc³ÆS»ø°+Æ[ûÔþë±q�_ðçžç�½´Ñþ.q2ø±êÿÂ{—·¶¿‡Ñ¸l?{ùõ±³·ÿ*ç\øu°abÍAÁ_Üû(X�K1Ä4±q fq›·ï]¹…éq”ìéí#´wyÇNØÃ=O×?h˜ÆÒãì5ÙÓxén–Ù@Îúo‡1/ÄÑôlý­ï÷w™g©)ûïÎnÿi2�ÂêW[ÅEÆÕ³ÕN,UýÌ_ ÁR{{û{Öòlÿ‰ìÀõ® U_µt/î¿q�m_ò¤á�VAø Ÿ8¢æØý#µªWö¬Ì“?МòE‡7¹¨Çw .ý ¡®?ö8 ö1 ÈE}¸M݇Ìohë�=·Êî3+ËöLÎ*Ô‡#oNÝ�ášE·<YsR޼yôϪ?vî´ŠúpÀye_‡K[?‘}Œþìà\º® HŠ;~`Ÿ_oë|Ó°ç2ý÷Շú[àêlÝ®3¼¿úôúpxÛ%w‹¤³}åÓÚ­Vý±íH"—Q¼Ô‡ƒ'÷ëpM½³4üS“y×ׇ‹{®#âD¿¡©?v?èO�£>Û°ž›»úppä_Uý±ç~´�›ÿY OõáàÇ5¥„õáªsmîɼØ'ÞÇ. õá¾××á¨÷Í.úH¥>Ü·º`wB.ˆÚŸñ™Bç¸Bç 0!ÅeH8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8H8Hý^Ý�žB!]Ý ì÷þ“½ñóó� sSï…XoM4&ÛªöòÁÈ0¬jßúu‰ƒ÷‹mcFûýímæÇñâóþZí×í Æs0^?é�Àų³YÿÀ›™Æ€<^xmP²f·óãxù~ûu‹öòÍE{ín —7¨Óÿîwûk¶ß¬7žÃñú-›r$ÍæÅ³L¡ùóãã…ÇòÕüº½[sÙü²ýì§Òi¿éϤÿešö›ÖÌí-ú_mo¹ô´¿Fûù¦LÇs4^ñ¤!¥Ç†Äç–ä?ª×DÌÆ§^?‘ª½Û¿‹_è¢ýJÓ¾±|½úýêþtÚŸlïs~ÝŸzýi;íw··îÿp¼þbà&RõÿÞëiÞÄHܰR\y³T¯sÐ8ìÙÈ•õÆ3ÛžßÁ<û(16¯˜Çœ)?l™�ró­7˜lÉò±ç–$¥ûgF¿ýæåÉò[-îQßß^³¿{Û�×?{�Ûÿ«tçb0æÇ×KæüÙú$•À`î᪳�ç1`LáùY]îþêƒÔÇ ÇbýNªõ÷剬qZ;Ëoµ7~·yëãÖZÌÎý¬=mÛ~÷ç{jcíyÿÇãÙݺß|"•ÿ«NC6v Xå�õ¶­:(ŸìHâ¶öÚáªÛ¼X¾íW40G0Ü~ãí5Û{�µëû»<ãµpÒ0ÛÛ4gQ“�[[“˜R²~¨£v·ü[íç[GË/~ žÙŸMý}³ýâJc}û:ÜñGSÇ^*®¯£Ý^,>Ó�OÜþ>ßl¯yÓ¼ýh|ÅÁòÓþWyËÛ7YŠö›öŠþë¯ôwÐÿÙxÆËÿ$ÏÆîzzm?{Ø#Ûmß0[bíè;¾Ž†êíÛèØhg¼ÓÂ*[ßB¯ §¹÷þü)õ žø~+û·­ãuÑ^o~.û~äœ.l©<¹µé{ûùe‰z{ŽÜ¾Çûín£½[dÃçiO_+ÛÃíøÄ[úZ+·~¯S^ŽU–~ÄËßtò¶ã§½¤wt]ç®ù¹ßèëÇÚæ‹pÓöcg~½~ý%~,{ü«¬ÏüÄ3Þ/ï߬¿Mÿ�q1.ñÖû±ÉxöÇW+¿î¬NÌÚïüáŠÑúq8¿ºÎœq[ܾ:á³iãýì ;ý��¹ÄÖñœ�¯Ôïëæ®bx޼7m?5ß%×OÍxVóë÷KõîõX�÷˾Xö·éÿÚÛmÏñøjQR&x/OéâtKÅlNFÚ¾g©�ú-Ê0ïÑ7gÇùæ=ä§ÞÖ�?ÊÚÃ=Ÿü®þ1z®¨�˦83@÷‡hìGÈŠçØRáV¯¥]<>^ŒÅ‹£b"øVÖîUµ¬| mÛ¹d[QX«·öÐ`Yˆ¨Áb_9ü“�øáÆ ^8�¸º¤pûŽO@œ„o 5Üê_p©ËQ™mqá!„^àb°*L¡!@xÇÏó[…öÏpÄæ;‡T/RL êA ¯åqõî‹ô¿i(þLBþ¿jfÈ¿šÆ¨)ÎR!Eà Eà Eà Eà Eà Eà Eà Eà Eà Eà E}8H9­��æ·É�×ýp—ׇkÿºh:fúì¿‹ <Ö‡‹�Wâ1Ó!¤”ÒìNyHx8i0JH¤æOÎî˜NÏ;JIÜå<®•ž†?©øµ_¯W,±¿>\,+´Ø;m½®B}8H}O}8 rº`³y<„•%è�bo»êÃÍ> 7N·Ç¸õá õõáîŸî‘ß‚Ë9­wÂu‘Dâ<ø¢úpéöço¢>¤~ÓP—ÿÚ; O¾¨>Üõ=€Ûúp±}ÿ·§ó{HNí4¸¬wìýp±jW2¿i¨oÀMÍ-¹å_:øŽÚú‹ÿ}Ó�Ûˆ›Ô‡ƒ”óT|2))))))))))êÃAÊE}8»´Äûõàìú_ü2yà >\{¿ÚÑ÷¿5÷ëá2Ö ˜sGîQ"OÿÛÓV{m} \ÃÃICzV…Èo-Þ[îÙAóÄAàRõÿÐdÓtZ\ Wø-‹m­×‡+ŽÃ÷ׇË<�¸îÅt¶N�š&tW£>¤Õ‡;:’DÜ£­×i—óSî蕟íQ½Ü7õáNËÛkŠØ9à¥>\•·ÝõàÚ'¡9AuÁI}¸s÷o„ÍõáNÎÛëE\ÍÁ7 u>ö×ƒëæ—=Ýå<Ô‡“ìßàƒƒúpÑøón;ëÁµ�0D¾ÃwÁA}¸îßúÚ}?\uòCÞøÈúpæö�7¨)g©øRRRRRRRRRRÔ‡ƒ”‹úpmÚ÷Ô‡kowâ·ÉõáÚÛíNºŽÄ9à >ܽ*D(î÷ÜQ®édÝ>.äá¤á~Cx*÷Eïׇ[hWñ¸Öîç árê'õŸ)Ö‡+–8¦>\]õÍé²µ0zrÔ‡ƒ”Ÿúp?M­ØÁ]ÎG}8ë+ì¯7øÃ ¸ŠŸúpø >êÃÝ?}¬ùÜ#Û>Þä¥>Üíqåò#<Ÿ¹qzÖ>®â¤>\ÇV¨´š¢¦>êÃ�á¶á9ü¦a}8øå¡>\õ~ˆË/BÌG}¸Çÿòz_oׇ»IÃöqWõᎹ®³9�¼y`Þwÿª>V?ÒúÛÉ—÷¡\|ðÓnoÈÝsǯù=D n>PRÏRñɤ¤¤¤¤¤¤¤¤¤¨)õá‚u¿Ê†»EÊ›Y:÷;ñËäÁ=p—Ö‡{t šˆÕÝOÝéé/K½>®ózjËÏOãYï-¦¥éçt}±Ë�<%耓“†z'µ©\爇k<ñ¸úÓî€çž[¤Îƒß²ØÖz}¸â8ü˜úpEc¯b:³é-Mº«�J=ÔGåÝ—9?æ/Íži8:œ–zå§>ÜñØŸ9ä¡>œ±ƒ{«\ùÖÑÜåâbêÃþ�š²ÿ6ýµ|Ô‡;E¾‡Ž�êI>\_ÎÜÁz]äýfÎC}¸×YñY© ln|n}¸Ÿ¨üîob§æJ]v�7­Lo/\ÇQ}¸£ñªGêÃu:°±\Ó³X¾ÉŸ êõKl¬W]ýkî¿lº‡ëX{¸êÂéëï?¦j‘jê°èí�^o7›¶ºŸÍ?º{Ø�úp�òq?¾�ƒ�ƒ�ƒ�ƒ�ƒ�ƒ�ƒ�ƒ�ƒ�ƒõá å¡>œñ÷%wÕ‡[˜�«x©W7öÔ‡ÛZ?:NêÃ5�Èï©·P?WqzÒ°·>Ü–úqPò¸½ÏA“0·>°>ÜŽúq8�‹úpœF~OõáØ}õáŒznûêÃñ´_êõ�%âs9ªÇAþ7¸¾>œ‰ë"ŸÊC}8|‘/ª~ÓP—ÿšMÏÖ‡'ŽêÃ�oà >\SÏ-ì«·2WqP®ó÷t×§›«³ù¸ŽùMCýÍfj¾ë,ÿæAÚuz*Óû|¿õé¦Eêä»E}8H9¸‹n7ÆÎü³àd^h›ï&à¢�»�spóà>¿ö3P=B° Ü®û�™Ÿ÷.�Êã“Õ!xüšö×G¯ÔÿçàÿXe`-ýy�i§#âÏ€ãù%áöÇõÆåÿR©ñJ¶±ö,å¼”ÏkÆ?�¯¬Tñ¸ ŽØùâ©ç\QÔà.Ì`•Žˆp7.Çà®T.ä¹½~ÿüÂZós·òAÂÑ$iÍ�_.·(UØ …Fp$HHÊ IâÑ:êƒÕµ…¡e�Â+�Fûqõeî‡UÙ×'òï‚ã§šà€KÜ­É-µoÜE\»^p"ž,øµ)éÀU§ŽÁ]rëêà>j{Ãà Þ¥êäu<ªJºUqÝ8¸½Ê˜§êžàò¿gƒËðaàꦒŽ€Kf¨ÄpÅòqwD×Óù¦‘} @RîèŽ|>Ü'‚»†”íð/W€ûâàx<è×àMp,ß"¨÷áÔN]˜ÐǵǒáLÎòí“qœ íJ¸êxéŠTc²/ 6"éÈø¸üföGn˜ÒÅ®À}~‘Œáh\ òÜÇå"ïr:ø–ÇKý&â].Mp|�_§#â›ÏláÀW™Åº ïøO\UÇÿ5ÍøëàÚMÊùñ\.—Ëår¹\.—ë ni'º§õ�ªÈìOy'|¡3çä¤>[Ä’ê·ÇßFÀ›Õ­êp–«Æ¸¯\À¯\Nd�“ }öüÑÚ¢/ù�Á©ÐÀð#oGá‚|Á{¨Êšõ6UÓ>¸Ë�ÀÉ„�Ô_4~ÁßmpªÛÓ„=úŽfhîN}|¸�“Á}üËà>÷ùà‹­®z%?jï³å�ö)êÌÏìWŽ/‚ƒ)˜´ëíÆDôódp]ö>[nUÜO/׃_drh�âøRÀi݈sÀÕÓÆg‚»5.ð™ï¨4ë©ócp²q@¿=œ’{úšAœ:'·ŸÕl^)¨2%¹d�ƒÞAùjw ;r Æï RÙZåC7éç‚\.—FA�üÌFí‚û⩼B à°ú¬¾ãLƒ`�T†#àø©Ü�½˜5×ýÑ`ˆ± ŽüQ͓¾ N­âÄÄô…ÞvA›à&츋支ÄYàdãPÜÏ>¿4pÍÆÈ^fÀÉéö {MpÄ•â¾àô;®ö\xpŸwWìªu¤oÌNÙp?8£oÒa�Žl·*æIà>Úà¾> B?ܱ½!p¬òühuG쎾Ž>‘V?„§c”Vê"óJÎîÔØÃr_ŽüÑò£B:ÀÖâ=à´ðÇ¥Œd?äÿÒÁÁÏ�À/M¾?ûèö3ë³7à�Fú_½CŠ¥p•»Ù%ßí%»0Öúý+µ ø_õ�m¿ŸYŸ½n´òÛ¹ »!J�T~½×ŽÄ+ |•»štO�ŸZÒU�lîÏÈʹ\.—Ëõ÷\SrpƒÚ²Ü ܤܤܤܤܤܤܤܤܤܤܤܤܤܤܤܤܤܤܤܤܤܤܤܤܤܤܤܤܤܢܠüŽ›”ƒ›”ƒ›Ô™àâVÿubNÓã¨ôdÇ÷ý»kÀ–ƒ»Ê÷mÁ<7K3Çw�«WÔzT BßÇw¡ƒ ý‡óìà\è·×t×ÿüÿo¯û~Š�”3ÿ·’3œny†Ïp;oɤÛqÎ ¾ÚØJøRüïj-åüõÔîþYÒ# ±ó{¼*~Ig\õýûóÿÿ;ÀÝì‰ÏD ²±‚ï'RFp#Ä LÀmÈUœ�Ìî&.-*?s¾X|LgÜ•Ö~ËýÀ(ßT ÿÜîßöÏ´ßIoÈ©�ýÿ©„ç)\�ÅýVaç¯÷éíξ¥—ˆ}ñ¨ÞŽ' ñªø�Î àò_Ð998¸|ÁÕ)Á£ G \0켎ÇfŽÁ¥®úôÜí±ÜŸÕý ­Àý 8È<ý¤%Ò.'‹á pâ<¼@M*88œÃUàh~ÏroxÜ&o…m;ܦÞÚ�‡­Á÷�îV™–‡Tµ4Dk(8Y«Kpü|Up´ÿ:àÔ;n\à\6Q.ƒûO÷ƒ_µ:îÿDRl‚ƒLС,—”:ŽJ{TÙY .Ä÷wMw \¸3¸ðà�“ÞÙ©”PëŽ�ãý¼6¸º—Á»¸‡wG®àð‹#‡|€gH—ð3Ð~š|Ts‡™=g¼õH;_÷ãÊ£š‚0ÓÕ> \ȯåàßíAWzÜ06R†\¤Ò†ÞG‰ŸÚên"�´@ü<ZÓb_r% W�+C®³Àí ¾óx5”ÊΧ òSF”3.²�8Yà¯HŒUÆÇÓ4K¨®Áá ܺ´áÕ?"7©;‚K/7Ár¦îNÖñÿ–îNÖÍÿ–î[ÇýÃrp“rp‹rpƒò;nRnRnR-pg˜è+à”,Ý5µ\Ì:‡á&åà&‹ýœ:î¾�*q.˜É…ƒ›Ì…ƒ›ÌÅ*¸3)Ê7ÐÔ�Œ¿õäÕs¼ù»Ùþs¾L’môxy`î"Q¸Cpèö†¾I ïÚ· &޶ž×:=àêÉê¯&ßÑp׬Úþs4Ãs?¶­Ïéîs;]àÊä &¤�£S%épÅ¡€O´�?o€»ú»‘ç§#'ýã¨û9Ø´Þ18Ì7¤Ëýòèñ|P‡„À¥º¾`s�,—˜1婢KÔ6—Hºýà0ß�r¼å¬s7p{aTpPVÕ Œ†—.Ì¥�^—Ìp91üàâ~inOwœìí pðÜ \Ü8¸ €Ît—Ó�g5¶1Ý:Ô<\Nwœâ!ÀÁÝùޫܡD~jpºÿÜ0¸´ ®vÃ�pùë}ë¸ \äO@ÎðŸ{¸ðtp[óˆ´üçJjÿ8Ñ¡épV7u ž½wwdËXâ=”¸E ®îOý8.qp{ºÓà¢ñ¨‚¿],c4Ãn'8¨4µV56Àá8K€ÀJØrñ+_†\4Ýap�nl ¹6rçW�ÈIàÀŸÍ‡#{ \c��äó­Àúq›ðgWùåñÖîÔé»qÂk¥¾:ÁÎA#“‹¦ï)7›íupÛ�À¥ž—‘OÓ:¸Ug.3zU™¿”η˜ƒdœèª£Ÿ¦Õ¿)7)·(7¨-ÿspƒrp“rp‹j�+Ÿg �ôtùÑó:}�¥Üï9w”Z¥epgÓTì5Á‘7ÄSÊ묱ƒ;ÈÃÓÀñ“ç�’ƒ,ÇX«êàP&8Û¯ÍZNÄ“¿('¦¹?¼Ó†ëhç"è«v™/ O^ÁÓŸˆ×n[˜– å%ö´éÈp0yB?“ .– r1ˆ@"]/.J{ô³ ™äi�£“A©NLéªÀ‘ù�â=ƒî[=}'ãÅP¦ù" �v/�›µ„v"O®çþtU¾Øôaâ“å4K?(ù­*³§�3ZU \s]¸: S8IÃP{‘¤Ñ.Õ^:¸Àì›à¨½1pº{ý¬üX<È\â饿“ðò¸H¢Ê—§¸hˆ‚%žî¸ºˆLw—g€£¬â,p0c|wpÛ&Ò=\_ތNj´i�cµs}¼vÒáùH¶¶ypv?nÔƒÒ¥À‡áï –º¸áuáX<’`l‚KåxUÇ… €ãùÐ[žåÿ´Gub]8�&x*8‘¯ pá®àfÖ…£ñöÇ¡'ãQ¸´Ž£å¡•¤ÌW£;[àªüŸÔ!w•—ð“® 'ãmÐa!H¿‡­„/ý»ýx Žç ÂWàŠŸÏ_€Ž­È¿X·ØKsਛT5äR*ÿò8“Ö•%¸ñÅ?·Òma�I•Ž‘/m}¹°IqÌËò'‡RdlLì2{c�ªÈàѺpòŽØÊ‚vâR±†€Jpo³‹Av .rp%zÐòWò#óupƒƒü¦•å?)7©sÀ%ÞJþój—w\³®üe•×jUMUotÿqYåçÚåà–äà&åà5×�s9¸Y9¸EÙàîÜi‹åÝÅø@nnèwÒ€ñ¨;âà ™àÄ+ìûéIàVù½¸iOˆŒrp£™f«êàš™¸Úg307� _,AX>¥�~j ÌáÓþz\ÌñÔû¨–Wø:õ͹t»#8æO¦�‹d}7u¯ž ŠN  ÿœ— ˆuëö¹œDìÑÉ w>8PãQ5÷5 8�EÖw+Ëa�€p§Ü^§ÂqDÆ[U˜ÜøòjQÙ4ÒéÊ:û)ûÁÉÊà�­*ÌWF\¢à’ž„.1;8î§§gÿùàÐ- Ï¢;W‰ûzp׆#p• ƒgº›U¶Oÿlp»N¡nêWf†ïncöžn“­ßÁ-ncàÐnœUËCÛqp}ý¸pÛ6ö¨Ç�;�#®Ï'ò†~j$ܸtb§+Þhø©½.¸ðàêº=1ÿ3ìÃåÔŒæ§VüçÖ»#Åb îÝ‘ÄÁ¥@6XWÎîÇñtñ‡€À¥Ap�/ËÎW�>œæwÖþm�!—¸.Å z£•?:²ï*Ìàçã†{št÷ª^ùOƒþgýà,prNG?^Àqû¸N?:î?—ØñNpSïÕÅ „ÿYN3ÈIjþˆÉõâhãÅ/)ιðùU9D:rþu÷£#Ó‡‰û×i³Ñ|z._‡Á C«U¥¾|Æ]δKpŠ¡&8a_©“´1qÍæ]YþzÁ¥0VÏYàj7ª^p"uá"!ÁUö;Á W æ"qý� .»§­‚Û5ŽøÁéàDU± ®Ï�îÁ•Ú=a9àªFàœ°¯Íø�Ëö páhµNÙ­ªnÛÚ�jåZ°®8Ö¼¸D�TÖ ‡À û“î`üQ¥ *{íõçu*¸ðÒàÖŸ[gùŸí�ÕœÙp­îÈ8­ŽkuGZàŽÖŸëÖq?.ê¶?8â@Žnõ£ŠÙà6<Íûq2T�ëÞ�cöJ>xÿé4p–ÿøµE\Ÿ : à¥U ­Œ!W4üÛê! bî`¼u�ÌŽW%ª{šô“;íQeƒ|¼&!�uáÈh3,•o¬è©|p¢Š}°Ï�.’}Wˆx.΀k¿Vzu�õ}BnRo .MÕIgë Á=Û�®5YóÒz¶ÝÛ‚{¶Ü’ܤÜ Þ¸;ò\9¸I9¸E­€{ìÀçF 9#aµ;âà&õ¢àî�­wë¿ ¸]n4áÝZÕWÇ|pe �•ÂKÿ3Å›ÔQ ©�X¦ XþqãÃï$ÜÉïñÛ„Þ&çRµ>|bPc?Ò29Â'êÀ�ìDL.Ý~ì,×…ƒã˜\]‘?mû* û¹Æ3�­*—èA\¢þ& ¸Äë§â£Ã§šá8ý›…O<¼ÌÆ!óÍ4ÜiUŸ .»C‘”ý”फCí2¶;OíÃq¡rm(àªüQphÇòß[S8âw¸m£ç©ý.p+Ç*mp§NU4¶ % ž N®ïFíÃé­ýæ”ü1pe›¥»¨–›W{}¸epb}7j¿:)ùk‚;gi ¸öúpëà[ß�~RP&¸$À¥FG¾‡s«¹Æþª÷žîÌ7,�ýU©ÿØVÿ›íŽT6yüx9ÙwjK^XæÏÇÝßp‹ø¬·#xõJe!õ~\êëÇÁ£J;Èïóõêr8ñð2¤œ ^ �óˆ{8ðS“C®ÊYÙrÕÝ9äBÿ6ËN¢¨nõYü ìÐÖ7œn×18¹>~Ö^ÞÆ ¿îŽb§øÁ‘A>+š°³ázr,%~äIw$¬ƒ­dUÞú¨½7“ƒ›ÔÀ¥1o t"¸Á!ÍÛ‚³ZÕ£‚š|ÃúgÀ¹²Ü’ܤÜ Æ»#®›ܤÜ¢lp¿?s�ã}ÿiÇ£ágó)Õݯ´»#¿¿¿ ú»àþçvÍÌp†riÎJoræyÜ÷ÿ9ùþ¹'¸’�ðÏ·«w{Nœ,X­*dÄÁéÁ‚ >¾÷<íU^ùþ}ý+Ÿ eÁº1‡ãç!î-`Ðì}“ªµ�¬âåðJzÿå8;³ǹ>µQüüÊÛþú"˜à¾ ¸=§ß˜A(ñonv3Ñ  WÎ+à{=àöðuzœ|?(ýï„óó³Á�à~oíDøÎ¹Çï¿{=xýBsŒ�†cçóMúSþ,ö¾w{·Ó$œÂËô:üÐüò`SNg2?¿Dæ>õ{®uÇ•üçBNÔM¤Î�pß-pнNpá÷·Nï\ù¤ÿ�Gb^çƒÃÂþžcQqþz£üh਽ÛÝ„áìxßà¤KõË yZSº“xÅÝMW¸ïRù¼7¸â—÷pß¿¸_^ûWà~eëðû[6 gǛǜ¶oõ[íNÆWXi‡©§UÅFATß øå= ÜO£ŽSb²G•è÷ç组³ãÍ�+~ypž©ÀÁzsªÚ#‡SÁ]{ª=à0œo\‡ëÍõ‚#cUú¸j݇8yþ› d8Z|‹¡€Œ7Û‘0-¨ÅÙ1éœ=oG°ãËÀý`8üŠˆ�‡£çË]z®n¬ p,=.iàÀ?üæ¤^ËK´ }­*}TÙéWDüQ Ðh¨ö‚ ´+Ò+%S‡\‘¹¨ƒžñLp»Úo€sÁ~8:Ègþ©]äƒÃô¿ pQäÿ8î?'ÁÅâV§€[›¬} ôÉÁMjÜ�Òù{š'Û„¿¦io¥ïß¿}ù›×¤Ü’ܤÜ Ü[iRnRnQ6¸c—úÓãórTÒ=Óê‰:êŽ88+ƒÁ'šmêÜh¹«tï nÖü:¸ƒ<,ƒ»³Ö®‹ƒT£Uup-™àÐMªLòT¯æ³�ÀÓIðSoü‰÷Öá§&&‘ªt‰}þËôò“t\µ€¤ËÀlôøVüãÍßùàÔõÜjpÚ*éØOM¬+W¥Ëì‹èpž-Z AÐuã’?ÔxTÑ_Œù‘÷'\¶%â2=Ôï ç7[· —Õ�ûšŠt‹}¼þb “ǵ¿Ûž«Tüá0|bëÌ «§U…ŒÅ6¸ÀÁñ•h´XÔ…\Dº|=�œ«Ò-ö #ÿ¾;8áåv›ê$.™2—Óá4\8=>½,]ÅgS~÷쎜NTqÃàôø0ÃÜ Ž„ ¸ÊŠo·Y�;X·mœ?nœ¬íIôÒZ-ƒëëÇ�N™1gÅߌt p4ücÀ%SìSj:ªÐ¡ì8;~ÓAFt¨ù›Ñ8±Oi¸°.¼%¸º;‘D?«îŽ´À�vG*p[é’¨é {øÀ'+ƒÝàFw$qp)ðõÜê~퀒uÛöPâQk€cé¢}ù¨Ò´–nn?¾ëÌ�Nn·)üÈJÏÆHÊ�‹ £h­uådº¬5Ñ¡7Õt988Χ’Þ0¸]¸ÈÁmðª òS±S­ùŽƒ|™å¨‚“ñ«uçªt¸âhˆáÍȸF«º ;¿³ V1‘.$Ü\òátpi²¶͹LãAé �N6wRéAé–ôB8×[I¾é½W¾e"J·d œ î¯ÈÁ-ÉÁMÊÁ ê>ý¸? 7)·¨7¯³4ô•ðXøóR¶�vw�îÈ>§ñ_ü†spg�3Ë{Îz…{opâû,8´tlÈ(XGyûÀ�§¦éQp»Ü ¸V«êàê×öSc/õÎ>I÷©Ú-ìÈŸ­«é2O˜kH$¿•_ßFæ§òË+!Õ·.=àÚ~jlª<á´á1¸#: ާKÁÅÖþ¯ÆæA pr®§é?×wà§F�çi+:Á/¯ Ót|ZÑjUEºt|kíÿZ¦ C$Dªü”ë/¦1ƒñøö´ªrþT/Xþ .ì1¸¦?�f_µw°ÿk$¶»À¥”Z7ŽƒKf8¸HphÌ-¬—t{‘Ó°ìÖAy~†Áí'{»\×ÓÁÉifNñ“;\5“> ÎjŠ{ULÚew ×»ò�ëÛÿÝ»„i™ \ÜTp}ý¸–ŸZ N½ãÖÁUþq}û¿®‚»'€Sª›@Óë¸:y@øÇõíÿ*Ý»¬ü˜à’ 4.L�›ð§“ø��íÍ‚ùy8<«wG cÌÂËîȸÞý_KÛÁº#U~ZàÔÆa¸;b<ªd=6(PjÄŸ®²/Π=ÚqUûq�/ NÜ•_^`v§ÁY~j¬Õ#WZVæqãvŽüéJÆõt%8ð{S‡\쀑éˆáÔÅEAýà*?7Þê•Õ±²32zäO'À)¿mcà¬ý_Ñ®Ø÷µUpÅŸN¹ßž=YÓ¸¢Ã¢�Áåàf“ O—Î-äƒÀ�žîd¿¶‡�³[Õéd¿¶¿î]åà–äà&åàõÝ‘÷”ƒ›”ƒ[T8X¢›îäq¬2 ZZõǵBZ›“�îÈ·¶°´ƒ#j�û©ÒÏ“ÀÅ­ý}\ãMòP~†ÁÙ»ªL¯ªÿpý–ÀíÒwI’·œƒC™­ªƒkêu]ÙågŽ�ýQ³á”Sô‡‹âM6Ë|„¹�€ñ+6é¶%Ž2¹EóÇI<Õ¯®¶bssT \Ÿ?\ù®MΔxø¹UáŽÀ)“8 �óôS¨àj€C€¸ÿ)ßXLì·JÊÉfSb¯?iÅd[Ç>·-¨ó¼V«º•ýS£°Ãæi ¿:yϵ[UøòûKéûj5ÀõùÃÙà�3ý7 .Q¿b'ÉéFžF?8Ò�“›Œµö$7Hšrë. åpÄ(à¶¥¸ouƒÓÜÀ¤g€nŸ©1r ûª>ÜVnâ—G Žƒ›wë:n[†ûÖ08b‡»TXö÷Ó¡ÕªÞÀüN=ªw—Þ ÜÏ,¸I¸#p)ày-ÜÔ£J޳GUµÏtp³þp&¸”;ªoîw¼;¦ýáûq<ôåFÁÉ|hÝšïJ�~Ü®t|ÀýpdøÀÀ•¬êg틊wë�¬gõã¸]\¶SÀ%Œ>Õ�Ã?ò¦×bÈeî�* :ê‡ß[C.ò)GDÒßMG@Ò§é³|ª²ÁÁ¾ªr�oî�j�ëô‡Ãï­A>) äK7œÈG™ýÑAþ”Æß ¯ëA^IU²ÁÁÍ%N÷Ó¹ßj:·°O:\ï~«wò‡{¸ŒVu\½û­ÞÉîá: Ü_“ƒ[’ƒ›”ƒÔ¹ý¸?$7)·¨qpFÿ)nu7­»Ç«ÅøÑ'�HFÂ\w¤\tp´väôé5Ýü÷ÀÕÍå®Ì¨«àâöÖàìVõÞàÐçÏ‚KZLzÜ—ÞÈÁ�n¤;¢�+Çmp´ŽSº#ì»;òT'ˆ!p‰ƒK�ígÀŽ#¸$BÅ1pI‚K/ ®wÿÓÚ,Jÿ·DãÑPìÑ«‡\˜ ê_Wù±=÷Õfk�/³›û®õË‚E9ÈO¢ÎJ±.ŠðÅ�íàÞk²æ5Úƒ›ܤÞ\z¡‰š¢7w²_Ýz~Âàdͳt²_ݲÞÜ«ÉÁ-ÉÁMÊÁ ê�º#¯%7)·(7,ïŽLÊÁMÊÁ-ÉÁ Ê[ÕI9¸I9¸E9¸ay«:)·$7)7(oU'åàåà&åà†åÝ‘I9¸%9¸Ay«:)7)·(7,oU'åà&åà–äà&åàåÝ‘I9¸I9¸Eƒ+?28ë‡AÅNŸÅ£PCË¥‘åÚŽSWCtGö5¶í†spg�3óuÎØdœí¾à0Úñï�Œ Ì®âµX#ôƒ;V#+n0=­ªƒ[‹házn"e0?ɇ ØOÏq‰¹.Û EYæÌLwt�9üŠ?P×âa9¶^p»!¶ž›Žþ„>u€“›êìámpõÚ CëÌEü.ÀñxXŽÑÍ~jp¸Ž\YÎ"*ápÝ8¸PqSŒ%~_F–Ï0÷]µÒ\g."2c™µjW±®Þ¡¿Uåàôp¸n܆·ú¸¾}W­t'֙㵶/’¼œ .ÙÙɵ NÅDKD—þ�O} #݉¥„4pÚ’E×ÿè«ù fÀÉë Àé 'ŸN¯Z»ÁE¹�̃ÁY�CYþ,&\˜Þ>ÔJw\Ü:Áå|àÆúqüÙÛê~§Þqëàdºƒà }"8YÝˆŽ²^Ç…é}W­tGÁ¥NpI†W5.L�›ÝwÕJ÷_‡gõîtŒYxÙYwÔ�uëzº#ôx5D›îŽ�*Ùÿ: U(\ÂOußU+Ý \2Ö™ƒë ÚmkÅ«÷W]•f4ZÕ²ï)&(†<ÜNᆱfº4Ùæ:s¼4NÛÖˆWçG\È›ÆÁÕª°V‡-rT^�ëÜwÕLW‚3×™CC©DiŃ^æ‹Y Ç­êãÔ¨„bg žƒ›Œ÷Bà’Q‹<ÊÎ\¼wÖúp³vFã·ªÒYëÃÍÚ�÷2àÞMnInRnP/Ôy/9¸I9¸EÝ\ï次–¦Q×�îÎ\gwD}›{7 ¤îȈuÞ‚0À¥ëà´‚vgþ\´_‚Η¹+Ük‚ënUœúÁEœ�…wñé–PdŸøn›û••G�Lnô•yí÷ÆýÛÈ45±[ÎÇjj¡½lœô«ÛÓ·mú£is-†_YW¦ ce€]Sî߆é%v1°[¨�›ûÀÎúÕñt†Àm××¥1”9Júiø•ᣎӃ`ýãó{ÃïùöÂô’xüàü¦߬}`'ýê0_�­*—ÈA\ùw.Q²JÑ&¦#-X/¸ÄóÅ® x4‘ aÊ„û88^�¤¸0ÔÎ*XGÊŠª*‰Y¹hÆy‘„ ä˺2G®%}–ÏŽŸ$mÙY¦ølÛ©àdݽ•[‰¤÷4p±TºÃà6Rà¸à ÷(pï²À�C•ÞÕ»æ½àêÚ~\I/vè};"ÁA½t¸Êß»,½g�+õð^¼ypÉ—;_Àí�º Ψæ”;s ·=NÏg¸–Àeð pUáTp´“ópa£ÿJ]κ#!b—œgŒ†;·5ÀõtGZàŽº#˜>䫳;R H;–j?.îWFâ7ÀEñ¨Æò¨&š^…—öS}¼€ÛóeƒK]~u˜¯!p¬À¹²×†\쀈/ZK'ýÞÊ÷2äŠ84K1DÛªJòdèW¹y‘üÄ:Ÿ}à"WöGUùùŠE äÃ�•¾,7f9•ôjpQäã D®×¯.ß¡·UµDƒ‡HéÑ?Á´ƒ›4ý†à¶;�KSexpwÛ,ôÑþq�w'Ãî÷ 9¸%9¸I9¸A­wGþ¨ܤÜ¢œP,þF÷λ#ªܼÆDÎR×`ÒÁÕj‚óVÕ–ƒëy��s)°O>8¸DºþfŸR9äu\[Ä -‰å2¼Um‰ùïE×-æ†Æ}ÂüQmÉÁÍ ÝЊ7v‚3Zðà­ê.7)ê¿Gø§*Gý÷ò‘Ä?U98ê¿·K‚½;²«øï¥ìG‡n„ºÛ£ƒÛÅü÷²ÿTYW|çK<o‰÷VuRnRnQnXÞªNÊÁMÊÁ-ÉÁMÊÁ Ê»#“rp“rp‹rpÃòîȤܤÜ’Ü ¼U�”ƒ›”ƒ[”ƒ–·ª“rpKrp“rpƒòVuRnQnRnXÞ™”ƒ[’ƒ”·ª“rp“rp‹rpÃòVuRnRnInRnPÞ™”ƒ›”ƒ[Ôú:Àókiße n²|Ù±ý©LvGö½oòŽSkÜcÀuMpA®ó0 ƒ;Ñd-7¨•VÕÁ…5p°'ny'÷ͪ·fá§õx�ÿ»Þ÷´ì}à –_†'aŸmCâ§'�Û`×§² /?ì|W6JÔJ;„¯ö=…Oا•Ø£ë¿û§–ør 1-€Ãm;Ë6MQ„ÊÛOéûœšñÒms!ÜöIì{ŠvpŸÔ‘ñV÷Ýùˆ|Ø~�Ѫrpu(º[QYŸM‚Ó¬—ðr£1´뺌׾ú†e_îöDp¼rk:¶ÞShÇÃϼ<™ÜNßâMgåÃ0Ñ«3Á‰ªª\o î8öfàd%_eXn×Ùˆ‡7 \Ùˆ¬æÖÎØÎ´SçôãøÖnœK78%Þ ¸²qäÛ�ãÕ•úˆ(v´xò³zTYtž^×£ÊÂOéTpa\pp‰‡’Ý‘p´;¢�»|2pZ§uGèñáÁãéݹOj.¶Ï©¯t€ïÇ%8¶ Õq°+C/÷O­÷�}8¨lå>©eÈ] �„/H •”V•vpÌýS«}`§^Wœ®Ú'ù¢à\�ò¿³™�XG+ /k}Åþ©rØ!pk²æÄ—ônR\š¬ÅÚúà–^»iöÂl«úfÝ?õHÜÙrpKrp“rpƒúCÝ‘såà&åàåà†åÝ‘I9¸I9¸%9¸Ay«:)7)·(7,oU'åà–äà&åàå­ê¤Ü¢ܤܰ¼;2)·$7(oU'åà&åàåà†å­ê¤ܤÜ’ܤÜ ¼;2)7)·¨ùÁñ¿Öuðc„…åÍÆsiÄÈ¿:¼ýûÀeÍfÌþ-p±œ™Þ¸IšÇ?9\ÃŒ~ – Ð7Ü rwpÇêÊÂ88š¾ƒë?µÿn9”VÕÁõ�2ÁÁOÕaaöSp\·@®ËF~¢Í—+ã¿Ú».Zá7ñ ו¢ÙÒ×™#?5ÏY€_ÒG}}:±.œ ŸóU¯G_åºr‘,°iàèOØS8¹(Y›Sâù/+è¥z-’²¶€XípÕšúºp §¬G�ø�¦SŽp¸.ך&²Š¬¿ËP�uÝà†¤ëÂÅ-ŽA_ÄVj�ëÌÁrÕB,°@�ˆW¯ ‡áqý¸x>7’/BÀôe«Šë²Øcp�ƒãÏ_µºE(�@ÜJAjpzÃlSàJs˜¨áb ¬$£f\.O&ëh¨ã°VO¼ �vFÁ™Ë›‰%�DUÄVÜ9W·ª%Ãê7ޝlƒà >|8±RÆÛºU\Ât´:îÿó)6Á%–Ïý/ dDö:¤�QpIÔØ*¯2Þ\×w-÷¸Ð': NGŸ0¥;c^vGzÁm¸M+ôAw„ü]�npX%'^û —¾®Z‚[9Éäup ?óºn{(ëQ-ë¼1û´#XƱLÐèà°ƒÃ|‰õè6Ñ¡ï‡ë¬aD^ •»…¯Ë¦Ž}¶î¸}ì¥�c)z"q£�€6äÚêø´ÑàùâC,Ì—1ä2Õl‡%¥²3Àiƒ|(`οì9ܦ¯ÛÆëi RÊ ?ÑüèàJ¾ |P”'ð`u«º.Y/Ý/Í?$7›l8 \šÌüö–à@'€ui´µ? \iUç,Þpÿ¸¿&·$7)7¨sûqHnRnQ÷g�©ÄÀaKÚ‹ÊrÖדÏÝ9c¢Þ \\w”ë|uüÉàæõS�c7�à¸q½á»â�ƒKYrpE]à°Uup…Iê°ZƒÛp¢LØÂ;|b‘Ô‰ðŠ_Ä#á–£Xæøyþ&ÓçþwÕ«íâÆuŠÿ§¸g”r¨ÁU~qyÿÒÈ�(~e"ž'ßבÉv>F‘¾ð¿«&S 8fGɧßð£à4äQ•~q·É²Dö-Åœè–?Pö]eÛp·•‚ô }œ¶C71#‹Ó…çø¿1£©æÀº#ôÊp‰[‘àŠ+C\}”ƒ«Ó€Ù&>’ÍyÓ�.ù züpœCNº>�’hà¨{Tªñ;ŠªŽŸ—S‘4,\—WÒóI ±éÃD-P£±JŒüI[ÕmcgÉ#»µÀÞMŒ¨â*pb^Ò›ÇóC/ð¶Žs àb¢ –[ÝWZO ØGwŒcéIpdb¹nÑÿ�Ï3ûœƒÖ�Óï8¨_lpí;Nög*pdÇ7–Þ,¸5ÿ7\ޝ€3ê¸*£ôÐâUàx5WƒÛÏWnZÚ£ªå§Îð+þ€:‡‡€ àÂÉàÎð+þ€‡àêîH)”ÑQ〓ݙ)â?ÇÂÓlmà¤ÝÎîtìkØ©&Nv8…_™åO‡÷•8Sõãé¸&’^Ò;èÇ•G•åsëõ“Ç%Ö�K8æ ²r½à7ºTñÈ�h)çÉЈ>ª¶Oçdÿ· œà€àj¿¸’ñ$ëÒz™þtØê nö;­Î£ÙÄÀIÿ»ûú¿™àr|lUƒkHnRnQnX¹U}v6ÞOnRnInRnPÞ™”ƒ›”ƒ[” .–wÕ‹¶.iÚT`ý½�Þ¸KÚ\þí „üZ)©™qpfBÉâ‹€‹Ú[³N'�Ã×Kn;7§ypíŒ�ƒ;IÜ®ƒëV~›pzÐÁõ©—gúó;g2Õ‡îfy2¤Z§Mz…ì¯öó:qæºn£þnåð¦Ú%! w›=Þ{òÕ9´‚9ü�]\yuo€ã“*ªß[Ês‰Ì‰ÐðrÒ…¯ÀU‡38a—¦ÏÁEÈP•i˜„?´»«€Ë~ddn–·ªñ?ƒéºÈ. Ä/Kñð8=!>ý^=b™46MGìÒô# ·[MJþÅô ”4‡?´[æU7œŽ-p)Ðpj=Upåß&>éñ \õ�NuÈq)±‘ÿMÉ{™‡=°+ÁeV±,w¦' §4¿·=>_ HºgYÇGÁ±,ÖœªÓxQGËaÛݵNTqO·m4\78˜Á6Á »\LX™ß "W¢ “‰d9!¾ià„{–u|œ¨­-ß—#pqàÚvI«: Nwyx8áºÑ ê[·«€ËaË:q�GNPp +�jú£ÂڣʧÀ%¥ŽkÙÝõ±_| ׉;^ \¸¸ÐGZñ|'Ù͹.bZwGà¼ìŽÐã£à´‘U8™Þþس´í–î€KýÏvp›žp'Õ|\í‡MÇ�w€KZ>ðty¤Rïàn•?Þ�].Ò¡O·7ÄšŽAØe¹ àÄç蓚¡éÑ3)Tù(\á+o$¬üÓxôQ=´»‹€Ãó0Ã:8â°'nè}hˆuœ±®Û¨¿-'×™ÛÊÒWò×¶‹­jø‹Òê±N9¸IýapIïr ê‚ÛªÊy0þíßW ù³àVåà–äà&åàõ‡»#krp“rp‹rpÃòîȤܤÜ’Ü ¼U�”ƒ›”ƒ[”ƒ–·ª“rpKrp“rpƒòVuRnQnRnXÞ™”ƒ[’ƒ”·ª“rp“rp‹rpÃòVuRnRnIÿÕb5õ”;Ý‚IEND®B`‚urwid-urwid-69907e4/docs/examples/tour2.png000066400000000000000000000212261526073734300206310ustar00rootroot00000000000000‰PNG  IHDRp QÄBÓ cHRMz&€„ú€èu0ê`:˜pœºQ<PLTEÍÿÿÿåååÍÍîÿÿÿ…ÇrbKGDaf¸} pHYs‡‡�åñe!îIDATxÚí� –£*†;;èlAÏÝ€ºƒô æœÙÿVn'RPÅK(ÐhæÿçL'ñÅ¥@*ðõAA[ºÝï÷ïž Ö§v»‡ïΧµ\ÜÖË‚‹ýÚøkó\÷Ì7»ûv¿3.·ç¿ Ïó|põEM•Îlïn“â68ß¶Á‚5o²àܶS�K$Ý .r€g>þ¾¹ p÷8¸51±çcÁù¶ð,Xs5—¸×>€Óƒ»}¯{¨z¤Ãît¹®W¦¹ßÝq&ª?ß¾m­y[SùbÇñ,¹‹–Ûï÷×)Áy¬à¿[¿Y•q£ëç–ËÇ;_.¬ÊÀYûœ¬ð(Ù\¼îI‚ûv^èçJ-Á}‚ûŽ€ˆñ(÷uσ³Ç—¸a©Ð;$* ÉÎãuÔ—¨kÃTzž;¨¶~ÍeàlUö}“@>œÜ•G\òà^.×Õ¾ypwï»+ç�çüéDÜMº¸`×6¸»ñ®÷¦W ŽÎ»�¼‚[5b@Ñ­ê}oÜZeèÁEµ'8óÿÝàÌx¸ÐUòžq1 #ç8—òvs$®¤9âÙÍ"kG�å\й½ÜÍ‚{M(ó à/Y [\Ü·'Ï£†q> Nä㟟"oÃEu¦cÂoלÈÎveŒÉ¯WÙű༮кÁlwM‘ Oög�ÈF>þù)pæÉì68ù<Ž]0®‰€»}3ÃloÜë丯ûÝ¿ÊyçÛßîê7ï¼ûý+ NvòÃ|¼ó“÷¬ã 7ÆÚºÉ^ñ·¸ Žÿפð¯ƒË»”þçAAA]P–Ö1<­¬WÅFÜ3á;9g;ã£E"«òôäÓz «ê.ªÑîÛðÛ”“¥Àù{@ÊÒ«ˆGË‹?äß-@ Ü?öt”¾�oz¤–zšê.M�§Ý¸ûNàüŒ·TÎ>hü¦÷ypѰ'Ezü?�Ðì.ú8¸¸¯Îà¾>Üm¾ÇOË� ÉצwËţݼ:ófâáÜöFp4ÄBƒV”zÞ™ø½uW”Þ-V%ãôL=øÍ‡Áñ!Žï¸X3¢¸pظ'¸—s¡WsP¥�oƒó�ƒ�Û³cCþËš,A;tÎnoÁhž+hdH²)= ΋ Èã¸Í‘×aò r•­‹¡�¼Ä¤üMAî÷{¦ [qfµém‚û–¹°û 5€³ÌÖeá—L�Rõ�càä®\Uz_4j�G£.F38ö&'¥1ü(¸hç Lßù�.hœ"=w�E™o¢8ß9¸ð³Ûw \Ö9Ù»œ?Ü®H/ Ž…Rì .~Å…‘ —÷uÛœK7ZÇQþéM�„I¸\¢mR�ï9¤ÃªØ1o÷•÷ýEzpÛéU�•çW®9’nèÇÀñ;2Õ‘ù$ Ê+uÏøH.ݨÛHÏ–û¾�fnÖöÁÙZ¼\¬üuw=¿r³ïâàèçF—æ??û*Ž3+K¯"�µ¿8~…¸”JÀáfwsµ;s©¯õíµ¯¢µ£OlËãÌÊÒ+ŽGs¿�»‹ ÂÕIî×{%àØy.ƒowU³æ‰å-i«:'÷Ï(Õ•ƒ ‚ O¤ÀUj6ÿ®R§À5 વ^sW-€S àšp•‚WU à”¸F\µàU•¸&œRW)xU¥®Q§ÀU Í¥®IW)xU¥N)€kÀU ^U)€S àš´ î§³Þ�À½ \qsààÞ ŽTnžgn’÷±8]>ór0¸ÂæÈV�V»Å,»çsfpži�½À©ò¸«€[Õ ný�°¾#¸d>ƒ«ñª·¸Çò:ä·2ø¬Ò3þ°[>¿[Öü–GŒÝÀýbú=ä·¯’<%i'óyœ_gEÛ-{�#uGG>Á%¹ugŽ|Ðûù'öE퇬“Wµ²{æl˵[>´÷u÷~8S÷ìny¸UÍà~‹³<Øz©e»JÝòyPnWGY‹òÈ\pmàD>Ö9 ®›Wµšm½³ßççCЮ nÙµŽ ó1ÕÜ…oUówp^>?ÚåÁÍû5GÂ|Þ®SsäõÉÊTÞ»ôÂ|Þu«v·¬ÞŽnp,ŸÇ{¼*©¸WäU{¯WAæ’ë•�}B|Y¯Z©wçs(¸SÖÜ5À‘0®Z­r¯ ppo· �…J\¥®¯À)p�¸j!\_)€S àšp•‚WU à”¸F\µàU•¸&œRW)xU¥®Q§ÀU Í¥®IW)xU¥N)€kÀU ^U)€S àš„ÀB€;fó8€»8Ò.¿¬¡#bG¶æó˜³¿#>\§ùã"¡ù�ÁÙÉ..ãQò°¿_ž+ ´•ÏúËëן.8€>F ¨÷šè àVuç_»€{]¿ï×uþ¸cÀ-á»�g'03ÑÐk7páC3Ùi Y~~ Ù¨h¾:Cî1óOöGêçܤ»Üª¸§³0Óhüp³©÷׳9BÅ9œÛhfØY ò8{™;p%ùôRÿ:îppôÕ]ÜÏ[ÀÍ«W°óÉ®ï<ÀrŽ£cÁÙùä®ËåªuÜà‚žÃ'�{ì Î뫊É÷Ž×ñ鈙\k_pìéÈLŸØ|r×g�Ú¼ì Î=�{¸YYg7ŸÜõnU÷ |×:Ž=–½{šOîb^µVïÎçPp˜ ຠãªÕÂl^p—·ê4�…÷áàN®pp9\µN®_À¢ªpœ[ôAUü! À9>K|᪣Á­º87æðVp×óªÉ¨8€Ûç@Ùé…Æ<€KÉ]WnpÈE@Žt=pvµ$º}áUëÀÙ�WŽ/¡÷p«. .p[àqq{ƒ»®W%p‰¸8€Û· Ž+ÄÃ\)8ÓW%h‰¸¸ýÁ]¬9bŸŽXp..n¸ü%Ga]¶ËÅáÆ­š#·^zÔÉ_ãââ‹Vïeçå¼j­à> àªu9¯ p÷o‚[upWÀUêtÍ‘«à”¸F\µNÖ¹ŽN)€kÀU ^U)€S àpÕ‚WU àšpJ\¥àU•¸FœRW-4G”¸&\¥àU•8¥®QW-xU¥N)€k&Þ¸cÀaµK€¸K€#}ÂÌÓѬãúªG€s³ø_Ü’14S jp›ù,Ñ$¯ ®pùÉfpuëìnÕË„î®æÚ Øžë«\]�ÖõM33Ztgë:Z”Ö>|Øe0¾¸×�ãÝú\û€{D&5XÁ¹¥læýÀ‘º‚{­oº;8 �&¥¹ Ö‰4ÌôAû!ëèU Ü °81‡œÛûË”^mÈ9 }>¬'Û@œ�·¸U×Çß}¸Z®78Sмqo·Osd_p?få¼·Þª{‚›÷Çš$Ÿq«ÒG´%בhËg8M>bÞ¸OhŽÈ•bó=Ê6pl=Õ lÁ-çš »ƒ£ÙX´¾êÂá^\_ö]®yö6ï naóƹ.«w‘N~¨üî~ùäM8¸ºA=¸nƒB×7¿Üòºv÷Gê?®šmþ1®ºz«ý�íµÚåÆu²?¸u®Ñë�Ûлó¸·�[ušÀB€ûpp§ ×8€¸œ>Ü9£•Îîbñq§güœ.Á-fáÆ²§M{Û{pëƒÌ÷ƒ»œWõÇf®œ[t�Ç ãã~v}¬tApKîA ¬\îÍ džœXôÇÆã±ø¸S žÜO0 ù�$¸�Gò¯.hlj0C/4b^ö¶÷:àllÀÕ‚³ryp«÷ØËÎËyÕ‚p{�[]ÆÞöܧƒ{l‚ãË#ãés 4Öíú!¸…"šÎ�³-`×W5ŸÖ0¯Yô½ö¶÷2àD ^ïäï×W½¦WÍÉ‹õ8€û p$€«Öż*ÀíÎÀ�ܪӀ»š®R§kŽ\E§À5 àªu²æÈupJ\“®RðªJœR×(€«¼ªR×$€S à*¯ªÀ5 à”¸j¡9¢À5 à*¯ªÀ)p�¸jÁ«*pJ\“Ÿx¯6ðÝù\'po[íààjÀ‘ŠÀ­³ÿ“ä§ÍéÓ#ÿ–•_Ë‘ULgk'¶7«ÆH*ËÖ¤ÉÍà¼óSÓ Õ.õz ¸Z|â|àÜZ„åÙέÓaÖXY•yc±•6ÃÂ{ÓÿlÁ-‡€[Un¡ßvÿËà a�­W¸þùGpg×8Êåi «ûŸ>£\G€#½a\5ê.4®ú¦Õ.yŸàv4üÝù\7p«X¨ÀU áúJœR×(€«Âõ•8¥®IW)xU¥N)€kÀU ^U)€kÀ)p•‚WU àpJ\µÐQ àšp•‚WU à”¸F\µàU•8¥®I,Ü+°ð݆¿;_˜Í àî”ù§ôQóÇçZñ“¨Ô5×ü[.6%ÏnàrùG¦{¸«ƒ+ø½h»´ùnÕåÁ‰)G§˜-žíà ò¿086sýÖý�ùã~?<ûcû—ÿÃM«!ì©Ì¿\~Þ1SŸ·í ùãÖiõèÇÿ,;M‡gOeþ)éÀ-Âp ަÇ8hþ8 Žòu¯”Ÿ´çŸš²Yý4h Ãå4b’æ�cà~üÿì–Uç¿?¸×7I;ùtûέdÁñ|W�lâ fOeþ)õ»Ußî9«ÝÃβ#À±©ªÎ n±uñ|0¸µ3ùš×÷ýÀµyÕ¸§ÛZN.fÏyÁ-î CÁ-r§çÙóÖ[•½gàæÃÀ‰ÆK†à9UÕáàJ»\9»ÜaóÇÍ|Š6ñúà�ŽoŽTƒ[^×ÄQóÇ=,¸5_÷êòãönãé„›ÛvgfèÎóǹ ÿlK|}].þEÖå*çM|wÐüqkÏ`qùÒ«èä3{Žëäc4€;æ� …ùãªÁaþ¸«�«3üÝù\7p«X¨ÀU áúJœR×(€«Âõ•8¥®IW)xU¥N)€kÀU ^U)€kÀ)p•‚WU àpJ\µÐQ àšp•‚WU à”¸F\µàU•8¥®I§ÀU Í¥N)€kÀU Í¥N)€kÀU ^U)€S àpÕ‚WU àšpJ\¥àU•¸FœRW-4G”¸&\¥àU•8¥®QW-xU¥N)€kÀ)p•BsD)€S à•7Îó<±×@ñ­9�søn[¥ùÔÛ£Tº9ò*À%3Ràˆ™â™dJÝ\‚&À…IlÏd¯ëñ6]—%`òYw›Sƒ|B{Ì«Û~pÆ@*ðì}N�›Í�üubàfÈúy´ùšDe>69a·³¯ªÁ¹[õÉbz]8ÃzÉü–~pŸÅÑæRr ×OtÞì.#›Žbò4ž/;nðí™gw~7%½ê3éœ-€ûO»Ç<¸A‚÷_PÕÙ+ŒÎõò ÁÉÿüüS�[o‹É~­ôÙ=NŽ¿ÚÃ\>¸×æ¤Ç_wp=ÁÍîâj'«$›Ç8/rRpî(g½«óE�æÙݤpt<Ýw�s c Ü”ÈG¦g_û‚K{Õѹ§:pƒã×nð¼¥Íg KÍ:Íkƒ›Øžˆ‘ó4�Yp“Lwðª9éI¸ ™=äV%W®5r~¶@ëÀ ypóIÁMâÏ&8Ñ$ð>¯‡SÓ=Ý)G-aïpóÉüm•Ù\ª9ÂÀÙÚÚ'!ÁM¯ÄÆÁ5ˆ‡™κœk¨³KwLݪ¦áìÀÉ|xx`ént̨a4®u6÷Ù'¼¯ßåš]º£<]xÕ‘�óó™ýüÍëQ·ª¨ÇEo™"ë7çT9Ø$¸X'réú¹±/hrµd˜�èä3»óªGªæúIpJ�Ü´KÝs”ÞÎs—ÑÛ£•¼'½—ÑÛÁ]U×$€S à*u‚æÈ5pJ\£âàxO?ØUÐüŠw¥Æ óSáö;öiaçš#Ÿn<¸¡¤‡™— ÚÙæ ·oÙ±S�n\òÌ ƒ7Î?ܦ� ¸Jå½*À¥†t˜×4άj¥'Ú“ˆqCƒ÷¤Ý>g»0¯H|å’ˆ»³ ño“ÜîRšzTÏèò4&Ž ¾ØÏn3c‘ˆW“gÀyc:©¸;›�¿ÝŽ,cI.DÂË 9£h숹¤x51²,°˜†ûì8”K„ÝøñuÞ‘AÜ�ãáo'pò¦=;íö¡ÓÍ›òª4îé7JæˆAãÌ ÄbÒJÀM^AqwÖÞ`{8^–]À¹ÑpwÅÉíœvå�æa^¥àü8‡p»Ã�&Ø ÜªpîÆ%âÕN ÎéÞœ­l=ƒlT“vD†yƒ‹„qÉíuàÆ¹ ¸´W À±q¿n/nž»Üª5à\Ü›GŸ½°«° 2>®êVõì’Û+ÁM×qÜè(¸1 Cñâã>œß±FÛíÎàX¼š× âãjš#1pÒ*nËrûý24«¸G·êÌM\¼šuÓ0Ì1p¬û`Œ‚Ûƒø·`»µ£¨7ˆyp6®ÌTÂäÅ�Sm—LÜA¼›,Hd=…E ºppnûèìôí˜]Ö²·‹Ò�|¸ša{H(¯&;ùpc\"îÎç¶›ÀÁx'?¨%gi—Rõƒ5É~Þ›cµvyx”Én8]vC7poÖ:©Ü»ƒµÏ>7XSc໣O2¸&œRW)D+)pJ\£ràþûÛ–¶ë‹µõÊZût½ì0J4G­(¸ÿþþýûçƒå@Ylq—åÝàÖ�Op…ä6 Η" Î�Z’k²C.ºçù·ô’ËÛV .ºÇ=èÝ׎”Tàª+¿�ÀU¹¸¤W¸VpÏÊŒÞü1uÛïnÝ><�úëHúa]ñŸ’³°®­2zqztžgãáÄ“šz;Ê~ÍXîïëMnÝþûáÏ¬Ò öqØP¸Y”-Qà�|¨·£.BbÛ«>©üÇ?ç`ß<ÁùfA6ãÆ|q[àü8=ß9Ø7áQ•þ¼uI•6GØ%¸þ5W¢÷÷ï¸ì|qåà¼�v�� 6ØQ\Õ•ƒû·V|ƒ­ëœÁ2¬‹FßùkAs¡@Ñs`ߦoð�/®ÜènUÂg›a&nÎû�S�cé­šg¸ýµ},Ö÷2y—Ëë«Zƒy˜XÐÕ¡¾Ò”ÇOgqz漑'³&h,µcô®Ø mƒì�øŸäH�ÿ?¬‘3xk¾¸Bpî7q£äH�ÿI^2Õv”qKzÕ]¥{fx*;N©7€›Þ˶‡‚««|ÏkG«'¬ç·ãppŸ"€kÀ)p•zO;îpJ\£Ë$dÆw3GdN –­ïøÄÎØ¶¦sG/Õ¸ )Á™šÈq’¯WšãTZÔ§ùÀå 1¤¼*Àå 1¤Á‰õL6X4IpÞã÷“n;F2‹a; ŽBÂô›ü”Û?xûÇoæÓÅ›ÑälP„ƒóãÛ¼Á“á¹Ü(=ôÏ‚óÓ·ßC"™ß–½[ù(]ÐL®gê¦998~7¾¾ê+ŠãuôĆéâ^•¥/ÓKæÏò›²öŠi@RùT+çUyÑøh¹'ëzï,lƒ Ò—i¦öÛˆ’M{·óÙ nqÄpÜ$°Ø £Éw*qp±8œxþ2•m{·óQ(ÎŒ\–ƒ5`;¸dþ pÉãGCåIp~ô³­¬û€Kç—>¾+¸|;ÎT²ù[5'¯Ö\:ÿ8¸ôñGƒ› ê8©±+¸©Üt’:Nn8ú¿Ñ‰�ëÇ�i›#éüEòöÜ™Mƒ3Θ3 bU1dÀ>8*ªhñ¹ô§pÂä,8ÓNÚ;%ÀMû‚{z)~éûó¶E»HƒÙÎÀ¹.×(Î'ç¦ïÒ‹ç?¸t§m{·òQ*ÓŽ3ó­±vu޽À@Wz3¹/š‡°ÈÀH�ÂN~*ÿ�ç³eïf>u:ç`Mï0°Tz§Lï³ÀMñ&N÷ôúäs"p½ãçRé].>nK½ãçRé!>îM¸&œRW©6G®!€S àw’_r¸ŽQY©OôÓ¦ÒÍ‘±œ0Çþ±%�‹€k½ÞDË|?pÁÆFp¥§gÀMe)$S–àÚK—©Ã£¸UW™^Ϋ\®pCr\•žÉ{ñn<Ô`Ê%=Š¥a¸ßwæGüë¯ÑŸÿþ ûíú0°!´Ñ U¬�º7æŸcÇË'ú�Ývèƒ?BwéåËW΋wƒ:pîGýëËvú�?pÜz�2àü1ßn:OÚ¸ôÊž×¥oUøð‡á6çYKƒû}»ÎE·Nà·N<÷¤Ç.96vm‡“GÞ0³á½„WõíóíN¬ÎLhË«:p¾ß…Qr¸ì¤VMNT„1p…óÏñqû8wÔÈÒëއøßœÛèÀ™Y™ùãÄÁq;¬=ÑoÐ]qqp“+�±1æ C$4à6ªœÿn�¹dGpÒ>ßî·�Ûª<í­”g.Aî¯ï´Ó¨¹áòœsàÔ pÛí¸$¸�yÖFé­pë¯;‚ÛXAî�àòÕÜ&¸?‘:Nä®�ŽÅIEÁÉø½ãoÕarà¨É §žÎmH€iŽx—ÅñÙÊ;î¯×‘ÅŸªç}Û7Æ`íÖ Àî‡c¸¿¯¦°h‘iÔBp“»šM,nôoUÏîÔ:°Ã(Ë­çwQž]8rë3?η~¸EQ¯ÊoÕÁ6ô$8ëb]®H˜?z^5°;H‡¥0±rç•çuòŸ0(aq\ôkOƒûõG€þþ�ÍxȼgØÉŸ„�\ðà?´;º¬MOÛÉ?³z‡�)pJ]ÜÔþˆµ£.®wüœÚŽa¸V´Rïø9­.î,¸&œRW© 6GÎ!€S à‚«íÖ¤W­¤>¬î�¿Tsà¶Á�™§r¹”¶2H?â-<¯Ð>í÷²?8¥ÁÚnÀU*íU.«<¸Q/ä¡0ô�Æ‘õ7Ïü<=8¯*¢‘æÓ€£p¨‚°(1€œç­�ªu%àÂhk ¸œWeAÙ=ÁÉõQõà‚ЇÂ[õ0p&�’°(Qð$¸aìÎ ª™*ê¸ÄþN·êdÚMÝÁ �À 'GíYjË¥ 3-'òÛjŽÄÀ•Ôq�}±æH ¸ÜÓëög÷iÇMÒ°õ¸¸Ñݪ“(oØòý©vœ«Ò=üvÊ–}^<\�°ër±×°KB]æíf±wæ]*W'œů¹.‘ rö‰›eøW™}öøŽ].×Éç™Â¯TÞBñ@?n8?�×tþÄÓe�Öi"éØŒ&º6¥�yû˜&ýÆN~òŒ‚T¯¬Âòœ²|×X>€«._Ê«6'|Qí2¸&œRW©Ùà*pJœREà¢� >½=·¡p#ÀEŠ_.†[·Nï¶ý­*7‰�÷*>ÀépJeÀ�|@šoõ¡{¾Ã>¹öÖWÍÇÙ]Mp|ð" .oæB\¨ƒ<˜Üñöük* n´Ãd‰[5o–^_uòãì®|É5€›Üï8»ùxòDŸEœÝtéz2 Î�zçÁùaS¹õUýp1€àfyÀ%ÀùaSlÄ] 8ß­N· .,|r}Uÿà"çþ³àjš#1p~œ[g7%Ò¿†TàìÖX¼Å­…ë«N2ÎîSÁ¹¨n¿`ùx3·F>Ã…q‰´Ïç:ëSlG2ÞÌbôÖW âì>”À)pJœR§À)pJ\£®R¸â”8¥N)€S à”8¥N)€S à”8¥N)€S à”8¥N)€S à”8¥N)€S à”8¥N)€S à”8¥N)€S à”8¥®QW)\qJœR§À)pJœR§À)pJœR§À)pJœR§À)pJœR§À)pJœR§À)pJœR§À)pJœRîý 0îÓ~'IEND®B`‚urwid-urwid-69907e4/docs/index.rst000066400000000000000000000002751526073734300170740ustar00rootroot00000000000000============================= Urwid |release| Documentation ============================= .. toctree:: examples/index tutorial/index manual/index reference/index changelog urwid-urwid-69907e4/docs/manual/000077500000000000000000000000001526073734300165045ustar00rootroot00000000000000urwid-urwid-69907e4/docs/manual/bright_combinations.py000077500000000000000000000030631526073734300231070ustar00rootroot00000000000000#!/usr/bin/env python """Display readable foreground colours on bright backgrounds, for the manual.""" from __future__ import annotations import sys import urwid RED_FGS = ( "black", "light gray", "white", "light cyan", "light red", "light green", "yellow", "light magenta", ) GREEN_FGS = ( "black", "light gray", "dark blue", "white", "light cyan", "light red", "light green", "yellow", ) BROWN_FGS = ( "black", "dark blue", "dark gray", "white", "light blue", "light cyan", "light red", "light green", "yellow", ) MAGENTA_FGS = ( "black", "light gray", "dark blue", "white", "light cyan", "light red", "light green", "yellow", "light magenta", ) BG_FGS = [ ("dark red", RED_FGS), ("dark green", GREEN_FGS), ("brown", BROWN_FGS), ("dark magenta", MAGENTA_FGS), ] body = urwid.SimpleFocusListWalker([]) for bg, fgs in BG_FGS: spec = urwid.AttrSpec(fgs[0], bg) body.extend( ( urwid.AttrMap(urwid.Divider(), spec), urwid.AttrMap( urwid.GridFlow( (urwid.AttrMap(urwid.Text(f"'{fg}' on '{bg}'"), urwid.AttrSpec(fg, bg)) for fg in fgs), 35, 0, 0, urwid.LEFT, ), spec, ), urwid.AttrMap(urwid.Divider(), spec), ) ) try: urwid.MainLoop(urwid.ListBox(body)).run() except KeyboardInterrupt: sys.exit(0) urwid-urwid-69907e4/docs/manual/bright_combinations.py.xdotool000066400000000000000000000000521526073734300245660ustar00rootroot00000000000000windowsize --usehints $TERMWINDOWID 70 26 urwid-urwid-69907e4/docs/manual/bright_combinations1.png000066400000000000000000000441161526073734300233250ustar00rootroot00000000000000‰PNG  IHDR`@–+" cHRMz&€„ú€èu0ê`:˜pœºQ<0PLTEÍåååÿÿÿÿÿÿÿÿÿÿÿÍîÍÍ\\ÿÍÍÿÿÿG…¯îbKGDºÙ pHYs‡‡�åñeG‹IDATxÚí›M‹T9Ç[G—ÖRDzçF„ZÉ€¸¨� ˆ›^È€¸jAÜ52 .{%ø2#=ßÀ™aÚùú äYø6Š‹ú Oåæœ““ÜäVRuoÕ­öÿŸ§êÖMNNòÔùõÍIRnl@AAAAAAAAAAAAAAAAAAA­TƒÕåÐ`@ŸÌï;Ö6Çß"}¶¤Ã›››§ù£®àÒå "¯l›9tè„yÏæÐ‰™.»Å€Éí¯m>½Ñ 0‡3[:»Å€Éí/[0Þ'ÏêÛfᑦøÏÀ4úõÛÆü5xè0¾Ì ¾0V&!€Iˆ�9D¤ðe ÌÕÏZ0V&8†>™§À¡êºq(6Y¢vƒtý¡ ö\Ý�¨û“òü:4Xý>ÌF¸Jâ@M_‡)€žµò°yŸvÃÁ›ÜìÓJù«×WžØ�ø í¥ü4?�ov³ccthàr ™1 0'âÙ�~’²�ªQ®\^½æ´гVÀœÊ7sº¨M5‚ÃÞ̤ ¹\^Ýs‚áð�á;3cœhh—Æù`âþ© Ñ̉æe5ûsÀÔöaôå ,«¥õf0¹Ëjå‡ËÃ}}Y²z ³Úˆxw Ãq Œ80ÆQ¯€Ñ‹ZÙSÀœÖÖ\¯rB%~&• ûQ`Ä�1Ž– /[ÉV>ŸéÄ5Úp# Œ>8ã øqZ±ìz„“Iw4ÀK Hëà À¸£�„…:8ã øýËÑ@GR‡‡0:ü Éácwøx"&<|”îO¨v«—Œ>ªS¦ÓÊZ>RëÓ�ÀÈácÂÂ>ž� e¸§U;hõšˆh=¢÷í À@ù:=ãà§“¹ÈªG­D³rÉDýf#eÐÁÕá¡OÔãAAAAP¨á9þtdؖϘ§ïí�`n}7�.òG]Á¥ËD^Ù<6sèÈ9}7ß>ÇïbÀäö×¶¾»¸Ñ 0ße¶tv‹“Û_¶fS‹ì7̆C#LñŸñ�†«L¬À� À$`â0 U±œL#4R#[¾AoTMöµ~ÖýSìÇ85#ù®z]Ô~6ìi Ü®{`)'àÐr»À¯­’£s•Ç 1ƒ×€Ê7t}¦"Ä”ŽlÜB`luÕJˆº(öµŠ=×ýSìgú¡B¬‚V7 ò ¶£vSÉþ1O#5 Ò¡Ç»œÛ&¦ç�#µéÌÖ©€Ù *«»s¶üœgHåçø td¸ú}˜�p•Ä�š¾¾£z֦ЄH€!bÈ>­ë¿«€á‘TwmùEÏ�Ê/òè»Q7û0•0çÔã&¸iæ\$ÿáú¡êäˆ73iÃsºŸÚheJsQèYó­à;mH×%ëGÊ÷wÞ̤ ¹\^sä\0”¨Ôš" Mª^\‰Ûà�ô£Û­V>0œ ¤€QÖRÓLÍ_¢^Fð§RÁ‰Ê)!òÚu¡î�92䬈܆)Ïæ;Îy†ü8`FÑGÛ²€©’V?ð0n¹´ËæÈ0êgÝ�1Y¬­nö3ÊÂ×^Jk`<ó /�†ýh`<óåsîH#0çRí2�9— Ì9¿Ýê5/0¼¿Ò03�¹è·ëLC³O¶T`Î…«$¯Ÿµ†µÝs1\%yýwÌÚªmZVÇÛå-«i¯Fû ÷a¼~Ö}Y­�iÊar—ÕìÇSÛ‡ñúïzY ¼Ú±ãËû0CÞˆS`Ä�±ýô½¨•�1ÌEmÍwþm - oÄ)0âÀG+f¨Ïè   $¦-u4p$f @Ù'û[ºìz„“É�Úª'r4`Ë/rë¦)'!0»»Žˆ{Î~ƒîÃvb�›Ý㎃q‘_io@�~¨`Ö ¨|#ÏüÀX10æ¯�þÂo»ËÅ7:KQ½i{,6‰3ån^Ýݶå·=C*¿ÍO c;«Û‡ÙWI�éë8J›¿çê/ø8µd{ckw|#>éÙ§˜²¯×ßUýØ»{¶üžgHå÷ø t|w±}+ô±��€ŠB`ndzý$¡ù�ýú<¸ryõ˜{(m,À°­FA´Kå"l›JõÃ˜È ä ¹ÜϪþ’7xZ’7¹Læ·cD0l)`¸žú’3yæÇTµsÛk·ùÀÈSã^0ÚÞ¼åSë'Q/íù‰µ<�¨\÷¿àN¯ÕÒ€9F9‰¦<˜ãœëÌæøîîî"Àp{&Ly:Æd«·7(ÇLS%µ±æsl'Ǻs|7†²Ü°ÝñÝÅ€‘ö³€Qý¯0¼”ÖÀxæ˜ÝÝ`JJÃv±^s€‘ö ˜{Áˆl?­ãc’æö±…€¹� Ìm¿Ýê4'0÷"9ÌF½]*€ÙÀÜËæÞFcå:¶,€À˜}´V�¹®’j½˜æz˜{á*ÉûÑ:0.8ÑeµÝx½kž·¬¦½LmF_ÖvYM{(þ´±&–Ãä.«¥½Sۇїö–Ռ酶æ©\@ÒO‰PÙÀðFœ#ŒG€Ñ‹W·wOOëæ¸Þ°«Ì"íviÙÀp{FÛëÀ¨­xÿª7ÑãH9h¨§UÒ±ÿhÀ³wãY•캃“Æ µúá�{Gj•Äg=»ê¶»Pr4�8þTGÇC`ü£Ï~#±*›C2í8tÔõ%€8æ’æD=>Þ� ½qÈáãÊäs\M9÷Lõñ(0Ó¼ãïEÚíº�>Orø˜æx­@íðQþoÜÓãYT+ŽÊ:©ô o'@}€É€1Zm¢°>ºWøðRûuQ‹ÿšä`«4gl-Çì™ð€ÉÔñݲF©=AAAAAAAAAAAAAAAAAAõJ{T  À@E0P‘ T$ À@E0P‘ T$ À@E0P‘ T$ À@E0P‘ T$ À@E0P‘ T$ À@E0P‘ T$ À@E0P‘ T$ À@E0P‘ T$ À@E0P‘ T$Ìàduy0Ч@ó÷k»ˆ¿%êáæææþ¨+¸t9ƒÈ+[‚ 0NšOùÀ<89Ëkh×.0¹ý/ª‡gö:æafKg×.0¹ýGUÆû¤õ�³çÐHS9“ÐAÆ€9ÉeÆ ÀD`Ò0i`)|Ès5¶AÀØz×^*NRýÉ ö7½ ´?ñcí¨¾j°ç®Îÿj€1Ù¯-Þ¬^gÒýîñ€íÕùo ˜�¨º7Ë%K‚-7Á´AÀPnÏ";r,þ *Äœ?ågàê÷tÿ'µÿSbJ7mÜB`¨~ZS¡ð�êùU Ø&”;ú'ìÇùÝÛóýî±½ô/Äûý—Iíà †>™¿ëÕuïAl²z@íéú0칺;Yó'IýIzÙkÊ÷òWI¨éë!г–@>4ïÓÀî9Xb“›}Z)õúÊû¿›á&èwZ¹IO!o¼‹) ŒÑƒ�Ë5d¦$ÀœŒg?úIBSÎɘ?ÌIÝ7¿Rþ»W˜3@ÏZîLPö°˜3 @mª<ôf¦d¿7ýWÊ¡ü�^ ‡ ß™™âdÄ·KÔù`¢þÄî�šÍÓE]û '$)`‚„EZVO„„÷¦€Š¿=߯™yÎ4ôK‰‘wí˜6§pÀD0ùÀÒ‰kMr4�¨:x?ðïÕjm5ÀØõ'�îh€�<�Ö€qG u4ð0f3ݯÜׯ»�t#3ê$å4Áác 9|LÔ>ž �I>Ê0Nºëê€Ùó�‘ÃGuÊtFY«C@j}¦9|LX¸ÃÇ3!0©ÃGöwm˜>hM~ç;ó§Ñú%üw ]˜y`ú “+›rÊtfÆ£ýL2Yí¸ÚQ�€$Îú¦ÍÍæÐ'ê7»Žæ¬qµ£ó`°˜0ÇÍ­ï<š³ÆÕŽz ´0P‘ T$ À@E0P‘ T$˜ßÇu‹°lü”ŠÇô©åô»j Ïó§Gö|Æ<µç½`~Êwíó{¢ž˜ñ8zZ� Lv¿ õ'×§�S%•~à`Ür)h—Ì£a!0ÔŸ\ϯ^ÒêÀ=U-º&»ß /¥50ç#výæéœÀ<�÷� Ln¿zJ ®ÝsþQ#0çSí2�9_Ìù½ÚµgÀ<õWIO—L¢ßecvÆ– Ìyæ|O�ái�öLTàfîÃü®W:¡r—Õ³úUËj¹××®€yD[©MËêx»¼e5íÕh©}¯?¾®ÚHÓ�ICô Ìôm�}˜YýÊÆ {Uýñµ;`ÂÀ«;¾,°3¤ < Dp2cûãëÊ€Ñ[ô¿‡�‹ üÎöOe‹>ö”‘£�D@3ûÝ VQrO×eã’\µE{ÊÈÑ@" êhàQLühÀ¿§ëê€q‡€OÃÀÅùÔoìŸFº�ÃÇD@3ûÕ‡�Ò½êwùÀðJäÔ+Òó{‹ÿj�ôhØ£Ì:õA*€�Š` "¨H*€�Š$À<�F—ø£¶àÒeÈï9]¶Dýºµµu•?ê .Ís2oçíùjI˜Ç—ö:æqfKg×.0¹ý§ôëÕ½V�ù5³…³k˜Üþ%Àì94ÀGî@³çÐH“ÁŒ/–ÀD`Ò0Õ�1ÙïžýpÉ˼_²Å#©‘-ߣ7ª&û½ZÀ‡õRqI÷Ç~ŒS3’ÇÕë’ös‰;µ#åÛk²ÿ¹¿`‹†É~mñÖU4ó~ÕoyHM+­%5 ûZÀ~µÍÈO½_nÇîÉï¯ÕëªöcíÂ~éêüw LEˆ)Ù¸…ÀŒ8@öõ˜êɾ0±çzÙQìgú¡B¬‚vø¹ú=¶ŸÕ±ÀT„˜Ò-¿:0.'ûZÀ¶8 ì8è—ÚýÊ~ÈoëVàg+ÑïUí¿5`$ôÛI€5}=¦úa6Á¼¤€!bÈ>­ëWÀðHª»K¶üRàg¤ë/Ñ“è’?ÞÖ䯒8`ÓׯHßüWS{µª¤'¯[‰©ÅÆwïר¤Çí~åˆßðCÕú½ê�³esIèµä[ ÇÚ>�®KÖ�”ïÇÞÌT·sõÞ+å~%€¹*�ôÍ•D^�À\�gIºM9ì×s§€©õ«ûhIi`8AI£¬¥¦)`5‰zÁcN¥ü†Ÿ8‘/ótQ×.�±‰K#0®•ù+¿OTk~õâòWN¡ü†ØEúåk?�yÌ9Ï‚À<–e�Ý&ìÜ820¿JŽBîýçÕÚc²L[Ý0�G…ÀP6.×K=†²áE�ùu«îWõßK`F£v¦$ö£�¹±ûV€á¥´æjÄn�€áý•–€¹TÌ¥½ÚµÀ\u­æj!0ª_¾ö ˜Ç£¸} ù�¹äoÃ\úÖ�¹êoÃ\í 0¹Ëj LS“»¬f?˜ä>Œ7Yæ_ꘌeµ¾,¼¬¦½í7µSë—Gß0ÞW/a—/ ¼qÇáÑÆÛÂÀ� Dp2cºkÀx!� ±«Œ÷$˜´ oü) ‚“�î×^;Æ®G8yÜ«­z"G¶ü·nÜ8 Ž¢õ#çÇ&~4àßÏX¥Í/».á$Ò p êgI{T¼µ¥®¡äh Pu4ðkLüh Úo‡ÀìùÀÈá£Bâ’onoùÚzì’ãD=>^ �I>ʰ/¹kûÀìùÀÈá£:eÒ‘)J΀®FO˜åð1Pwøx5&uøèõ»ÇgIÝSSÖO Vð£ÛÿÎwÕ¿«]åxÌ<01]Ê|´�–½Üqu¥«­>â×m< ÀŒFY(Œ–½Ìqu¦Dû�Œ§˜Çy(,=z™ãêLAŽ»r-w<ø—�P‘ T$ À@E0P‘ T$I€ùm{{ûÔ\ºLù#H—­@Ovvvî¨kKNóÊz Ìo7ö:æ·Lή]`rûõäN¬p60OîìeÉÙµ LnÿsI€Ùsh$€™;b ˜©s#f`B˜ö3€ñ`"0i˜ˆêÀ˜ìwÏ~¸a‚dÞoØâm)ª7‘´-¸¡½R»½ZÀ~³n\½TÜðürwäÿ·êucñþ˾ø*n6xÓ4·*€yRe¿öÆZû³õSƒ'aO¸ý�ªgÑÓÓÑ“êuGû¹ÃƒÙñÇS]�ÿUSbJ·m¼B`¨~ZcàzºR»zÀ¶9 \Ï";òûû#ÿ¼Û-ôŸ-ûÅ ;ј�*`i`v8 `ßU¿c—]´þª3®¼dÇõ2º:ÿ�# /Ö>ü9@Ó×o8¯¥ð7ó~£2Ú¦§€×.�}j5ÖWã‰ÿð3_ÿ¥b`Ìß/ýeßq5–Šot–¢zÓöIl{bÊ 0ܼº»cËï~vtýzÙkÊËÊæ†Îk©vƒïõKÚÒuÉúm5’ß¼™iáþKÅ�x²³PQÌ�xÖ£Ÿ$4å°_‘~õ+å¿e¥�±‰K®çrJL¼k,`5¿‰zÉoœRùŒyû/Uõ—¾ÇÓ’¼É•`2ÿ»‹Ö (×S_rcfž;1»'*“Ú¹ã]¿m`~“…Få§<˜'’£P¿~Êsð€¡¬X®7æ·íB`2û/Ö4›4q¨rÎ40UÒ[ çód§�\ï|ãÀðRZs£…þ‹µ$`x)­�¹±;8ÀÜØ«]æF!0™ý+À$ Ì�' s§oëšôîu Ì fÎþ‹õd'X%€1;g­sÇ߆¹Ós` –Õª•».º¬¦½í?µSÔ±÷ôˆ.«íFì�Xó¼e5íÕ(`’û0r¯¯«ÆûÊeì†sC·äzõ˜ºëÂÀО"8˜«ÿr1Ó mÉS¹€¤Ÿ¡²�¡ < Dp2cÇÃ×c×!œ4º£þâ“Gþ}­] 9h¨§UÒo!0Û-ô_,u@ˆxWµ¸‰LŽêi•ô$&~4àß︳‰%³ç#‡�ê”I@þIóîš ˜>&ëùðñFLêð±¨ÿbÉ´ãÐQ×'|”Ø—4'êùðñNLêðÑÏÎNÓ¬e5ÿ|ÖO Vý;Û¥ôߣ×ö`(f¦z¥ ¥˜3é³ê»Ö²úßY}”*ÝYÊ”3K Àl‡¿W)¬ïZKê¿ÅM²è@ú€n0AŽ[\ßµ–Ô¢TéI/ÐÅ¿|„Š` "¨H*€�Š` "¨H*€�Š` "¨H*€�Š` "¨H*€�Š` "¨H*€�Š` "¨H*€�Š` "¨H*€�Š` "¨H*€�Š` "mìCP� T$ À@E0P‘ T$ À@E0P‘ T$ À@E0P‘ T$ À@E0P‘ T$ À@E0P‘ T$ À@E0P‘ T$ À@E0P‘ T$ À@E0P‘ T$ À@E0P‘0ƒªË‹Á€>z1˜¯‡¸·õÒ?ãÙeãgT<¦O ª/­Ëóâó)˜?Ìòªíò�Éõ»,ýóL>µÌ?‰ò¸]>0¹~[Q ˜ýTˆLPf?j3C&å/§_“iߘˆLZ&" Ì Š-_òÁ\ 0Ö"ì [ïÚK…ïwê`0{W&íz Ì?ãgœÍNoÆc Ì?*ªÆê}âwú4kâÞúsíü~¥œú#{W&íVŒQUƱ$b¨Üc ‚ÀRýànÏ ü¾*Õ½†Ûõ˜ñxl!bþW¡3ÀŒÕS†ÌökÀŒ…´gž{²?�Ç‘~ÅõGö ˜ˆÝJíÃH)–ôÉüý¿¨®û/bÓË j7HÔ‹ßæÓlog¸é‡T»¾h›*`ö)ZÕÝ3[þÌÙ¹húÀ£qbJ³ìÿ›~ÄŸëoŸ8­>¤Úu¬40Fv’σØÞ�óC<_QÀü ì0?ô{sO?Iþ±SÃ3úìq�&�±Í `ž){̳•lîù;½EÒ‹�»33ÊÜ. û}1Ð÷æ?¬0t#†>ûàÎΆ•¤ß$0ÏüþèÞüïY?�y¡“ {óðÍó%Œ?Ñ|óÀ¼t L•ì6µë‹Ü´2'0ãqô´`N`ªd·©]ÇJ3„SÒ Í)ií€á¥´æYÄîÛ懮�ùáÅZólN`žÍö[̳Ö˜¢sÒÜÀ˜»µ晿Jz¶$`ÌŽ]/€ –Õ´·¢—ÕÍû0?Ì\V{ö/h‹·ïÀðtD{& ˜™û0ÿèVÔﳬeµgÿmñö ˜½¿ ¼�»ýv�in× 0´��¥€Óû?¼q·¿Ø>Œg?«]ÇÒÀÈýÀ‚b“Ò}:"pG�äh Œ xöëŒ>ø'&z4ðÛ?“£�qÂï~0r4àÙ÷wxH˜{kÊÝác 9|Lå0j‹WÙ¯ 0îðñYLüð‘NÇcwøø,êW¶hâ9ŒÚâUö½‚2` "¨H*€�Š` "¨H*€�Š` "¨H*’Ì«7u‹XY›zózÕßAž†?ò§?†íú[+`^½æ»v€qþrìò�ÉõÛ–þøQßÍÆ·Ÿå7˜\¿K€Ik05nL²¬Y®0Æ€‰À¤`"RÀ¼zóº‚ãÍÃéÍ›7˜W:ªlGõdÏEûµÀ¾²þL« ¢º•rögí]™´[ 06®S8†?þQ}þc8Òeh+mA-°ØjnøuåÆ÷Pìkýõ˜7FU¼l„_½©Bg x£Ÿ2dçêßH„)ʯ½.ÈüÍë7oü‡Cõì�ì0ÜnÕÀ8Rö50NØ¡5äv�ߪzh}*Õ}­¿c5�Í« ˜}ŠVu÷Ú–¿Þ×vò$¨ê÷,±éåEÿMà‡%t¸þö‰ÓêCªÝ’d'ži§Ñs�ïòãþpoh9Øÿ#6ý˜O?²}¤¿^ÉFž$4¿¼¦ÏÞ“Aó:({ÕÌë8P ˜×ÊÞóz¥›{.€?ª?ÿàòG0?ÆóÌ�Ê>Ò_¯ä€‘DdßKHª™âµj$,b_=‰"=°}˜ÀÝ›ÿ½î0üØ %*µ†¦HC“ª×~Lgõþz¥0¯$G©Ê_©G ˜Wœû­o˜WoÊ€ûivú:ÒÜÀTÉnS»eÉ$Ÿ~à`Ür)h70‘þz¥:0¼”Ö@¼V-jf{´ûV€y] ßv1¿eÀ¼~Õ`~ü£˜Síæ&ì¯WÊ浿 ó:Ì«7±f~`ÌŽ]€1;h˦Ö_¯T_VÓž‰&¹SÝ+û(0ËjÏþmñ®˜?h˵iYo—½¬öìëýõJ`h/  ³{íÃö‰°ÎLs»e©xµcÇ—Åöa<ûYíV,õ‹;u4ð*&~4@õdŸ «,¢RÀÈÑ€gßW`†úL€.±�^w4�FŽ<ûõÆ>¾�I>VbûTXåð1•è-^eß`þp‡�?FÊM<‡ñOØ~}€)Pt‡®ëÿB}€�Š40¯SÄó h`Þ¼‰¢ñf•‰´,ÍÌ«7xÀ|»Â¿|„Š` "¨H*€�Š` "¨HÌ¿ÿûßÿÞòGmÁ¥¡¨\µ[Líxi]ŽF£Ÿø£®àÒ®µ¬~2e€ù÷í~'Àü›(�Ûå“ëwQýùÓ~'Àü™ÙÒÚå÷“ëw! 0û.d `übà ̾ Y¿¸ÀožÝã ÀÄC`L €I ÀDTÆd±ûöÃ[Èyk‹½ô–Ê¥�y§Oìà­×Õ¿¶¹k'o=ÓN+ÖÞ•I»Õc²_[<úÉ0bÞ²Å#?¨#[¾OoTMöûµÀþÖûýKùÔÙh$ö®,å·5SbJ«K˜ÿ‰á[~·D°]X²'J ûû÷„ uoËT»SbJ«K[½ÏQý“êÉ~¿X±çz¿êÎ8"T¨{[–öÛ�ôÆ�¿Jªž.öeÿ´Ã)éŒ1ˆ¯–ƒýcÓ�øû×|zËö¶Wëóßüi«Mù«¤êéb_öO;˜’L¡ �CÄ>П£æz¡ãOóé'¶·½N?¤Úu¤ `ÞJ ½–i`j¦â•mfóVÙ;`Þ–ä9m*ÌOHÏšou ÿÔö�tÝ `~Rö˜Ÿ–º¹—†ƒŸæ­¶ `ˆD¶æ/QÏýнùßÛ>à J e-5MÀÔü%êÙÝ›ÿý`"0º¤ÀÄ€ ³ãÀ¾˜*Ùmj×½æÆd£¶º`ªd·©]G0³µ0v)`š§¤ˆææí¿ë ï¯tÌO˜0fÇn�ùs·�xŸ ³c×'`æXVÿë ª«`YíÙÿK[¼}&wY­�i lÁ²Ú³ÿ“¶xW …š�yóv?°æ£zÿ—7îöÛ‡ñìgµë^¼ËÀüó“¶æ�;cµÓ 0Íí:’Æ HÒú?·7/gDo�µ;ø—ÏÞÊÑ@ì)#G)`ähÀ³_=0öP`ÄgµUOähÀ–ÿÄ­s�à(!V-�6eß`ö}`äðQ�2½UÆî‘Hø_,‘>¦rµÅ«ìW̾Œ>*$~òÍí-_Bû@ºä8žÃ¨-^eß`jJå#ߺ²~ÑPü³‡µ€™G&¦·½ý%ÿjõSæ0úÖ€Iä®ß¼F£,FKL,–¨`þmé4ý™‡ÂÁ|Àà_>Be0P‘ T$ À@E0P‘ T$毭­­Ÿù£¶àÒù-óTâú{{{û&Ô\º,-»¿„ 0ý¼ß*0e¶°vùþsý¶¥¿oîw Ìß™¬]~¹~ç’³ïB—&ãñq�Ùw¡JãÏá?Ï®ÇÀø0ñPÀD`Ò0Õ�1Ùï¾ýð³À¼ÿl‹·¼àN+­%5 ûZ`ÿ²ÍÈ�®øy_—³?kïʤÝj�1Ù¯-Þ¾i1ï7mñ¶Lª7 ÙÜÐ^©Ý~-°[7®Þ‡”³_kïÊR~ÛU0!¦´ºÄ€¡ÐR9Ù×KÅäG;øyßk¦üyÀÁ¾ßΕ¦"Ä”V—:0T?­¡�R=]©Ý~-°Û ×ûã`·ž_˜„ßv¥7îüURõt±/û'NI™ÚŸ«JK”- õE_ü:Ä_uo{³¾ÿ*YÚ·/•T=]ìËþIS×”ö÷oVF–¬›~»@SôSõB‡ø­îmïÖ÷ßùÓÖÜÊæg ¨ßÔÍ$òjæçx¾¢€ùYÙ;`~^õæ^˜›@ÏZs“ïõËÍ#ûõv7Óõ ˜›ÊÞss)›{i`lâÒŒkE��åÅ5?‰zí¯zZ™)¨‡ÀØÄ% ×s9%&Þ5 ÄÍf b~ÿÞ®þwÀ˜dýúCÙpËÀTÉnS»åiA`(+–ëÍV�©’ݦv- ÀÌ€QjiJRö¡ææç¿07÷k×v�¹ù7€©V\?ÿµ`ýÚÙÍ¿·ûLƲZ_ÚXV{öÑo߀)XV«VîÚ²ڳÿ›¶xWŒ ý¾lÈ)`~öšJ€�AÀ4·[ž¶½�ËÜM_„Åõ.°ÆÐ]Û¦¹]ËÒÀØCNbÝѬ~–´OÅvïk+Œ;H#Gž}€±‡œ´º£PòhÀ¿¯µ $G)`ähÀ³_!0û>0rø¨N™~VÖ2EHrøHS9ŒÚâUöýfßFÕ)ÓMe­¥ùMwMVS9ŒÚâUö«¦¦y~£û-hÖ/æþÅCÿ`怉éç•OýÔÍ�þYõë­`9ì7¯íð÷*…õë­`‚"9nqýz ÿò*€�Š` "¨H*€�Š` "%�y¹³³ó‹º.¬v¼@+–æå/‘Š `^&ÊãvùÀäú…V $0û.ĨH*€�Š$À¼Üá·ýiš[ÕÀ¼¬²_{c­ñ\Ùú©ÁË€ 楴ÝÙ©ûsíL�åž0½^ÀìT ¥�±õSŽEþ©~Ú–Pñý¹v¦Çrû0 Œù;·‘ó�yi£ûÒ«R¢zÓöelú:^šO¿Ôü¥ÚA½’†f"ør`~‰ç+ ˜¨¿T;¨WrÀ¼Ü±Á£Dd?†`2ÿû%Y†- Ì/^¢Tó`ÖB*’:Kšf�¿ìSrš¦JN_îÔ=Í Œø0k!Iãc’æ——ícý˜µ�æåN°JcvØÚ†ü˜µP÷ôˆ.«Íª8º_R°¬Þ�ù0k!ý*cz¡#*�ôS$ÔüÀ4·ƒz¥:0zß»Rq w4�fÇ„þÌZ(̾üÆNN!Õaa˜—.iŽç0j‹w¿îÀ¬…¼fÕƒ�ú/ÉÛ‡Yõ` þKŸVÿ²êÁ@ý—÷óš%üËG¨H*€�Š` "¨H*€�Š` "¨H*€�Š` "¨H*€�Š` "¨H*€�Š` "¨H*€�Š` "¨H*€�Š` "¨H*€�Š` "¨H*ÒÆ‚ ` "¨H*€�Š` "¨H*€�Š` "¨H*€�Š` "¨H*€�Š` "¨H*€�Š` "¨H*€�Š` "¨H*€�Š` "¨H*€�Š` "¨H*€�Šä€œª.ïúèÝ`þ^bmñ·D½ßÜÜ<Ëu—.gyeK�æÝ)ó)˜w§fy íº&w<¥zvÒ 0ï3[:»n�ÉO¥0Þ'­ÞSkØ)0‡F˜âÈ­˜ZCÓ’B`|˜S\`¬LD&-‘æ‘—�|0W'kè�­wí¥âÕŸªbÌþ¦7ƒ�öGVæ¿Sž�mg œ“”<»%c²_úÂÏšïܼŸµÅ›RToSµ°Õ\TÐ{ÛœüÕûgÜ ù}_½Î:/濳ž�m7±¯�W†=/0æ� ÌÀÆyb ‚ 8 ÜžEväXü0¦œ¿ wçˆ8¥D÷ä_€»eS}ã¦tÓÆ-†ê«N+ÞS=¿jÚä€rAÿä�ý8¿“‰j`ªgÕ8¤¼bW Ç�Q¡0Ð'ÅwÕuò.6Y½£vƒtý»*Ò칺;ú3Ï�ê‰âú�Гè”ÛÓ”$þ:—¿Jâ@M_ï)€~˜9�ïÍû4°Klr³O+å¯^_yb?â×·7Ï�ê‰Âv<c¯ø+SŠ¢¤ 4“Ô$ÀœŠP?IÞÙGÕ©ˆ?F÷ë¦.åÿÝÀóݽÀœ•zÖ ˜³AÙûF`Î6µ©FðÞ›™”OA<]MÔÔïy�¿ÓK9Ã).{7pwœb„’§Á©80ίóWFû5o}FþzÏÆ� iY=Þ=‰z?!6=þjÀh¿æ­{`ÞÙÂ}ÀäóN%CuÕo#0­�yÏ9È‚À¼—ÜHÜ*3Ì{αfÃþÊ”æÝ ]`Øß,`t¿vZ²ÙmŒøë\‹ó~ÓeµgÞs€y¿™ Û¹<Æf·áxÅ_™ÒÀpþ �9ñ� ûÓWæŒî· ñ×¹†óˆE�?*àÊ\€a»YÀˆ¿25sªe`Nes*’ÃLêI¯Î�ºÕ‚Àð}°úígs6˜Àž�jÀœí˜SÑ`~`<˜÷ñ `~`ÎFWIË&XVÓÞŠ^á4ïÜj^V³?L|F÷«û�³Š¦`YíÕLî²Zùáòè> ÛyÓØ$Ìb9Ì@g/´Q6ð6î& Cþ0“(0ª_¾Nü}˜Sz|ÝËÛqa.gµ5׫\�P‰o“eÃ~0“(0¼ÑG㨆¯W‘40¼èx'{ð|¤ךäh ±óªŽÞ…Àx[*n•$ýª«ó3%Ùõ'�µ á¯Þ td5ó¾w4�°PGïC`¼-·JâœiS]ÃñºqIç02Cp äðïÔ¤vXè$‡� `Üáã©ïðqR³s‡�§´îÐmôu,˜÷êÑÖÆÊ ´:|¤Ög�‘ÃÇ„…;|<ão&×ìÜáãÙÈx7ÝF_�úý#ð¾þîwÖ­Ÿû÷Ý�s˜y`ú¨SË:�.ÔÙ§¼g“¹H¿Æ9Ÿz ̲vþK5kG=Q¿ÙAôç|ê10ï½|À„9nný²03Ç9Ÿz ÔG¨H*€�Š` "¨H*’Ì×qÝ",?§â1}jA9ý®Zà üéð-Ÿ1OíyïD˜¯Ïù®`¾&ÊÓvÝ“;žP.è»ÙÀøö9~»&w\h†•ZCy\ˆÃõâJÜz¤0Ú¯7®•ó•r œ?ÑtLs¿²› Ì×q˜4Í£î�ù0䬈Üzœ†ìfÃþºQ˜¯ã9�‘副\`fôër˜q õ#þ”I*ýÀÀ¸åRÐ.˜Ã<`ØÎå1¶ßñ×�êÀp^ ¿øçªEWÀÌèW€�ƒ))Œø[P]Ãùˆ¸2`†Ã`JJ3.yJRSD,pÍSRDÙÀ<Ïæy$‡Qã`ž·Ì…�À\HµËæB&0"9̤žôêܨåóÜ_%=_0^¿«ÆÄk©À\ˆ®’z ?îiÏDnæ>Ìצàä.«gôë#Ó’švbÀ´‘Ã| ­Ô¦eu¼]Þ²šöj´¿è> ÛyÓØ$ÌRr m¤©ÀMÀÐÆ<c"Tï!˜æ~uóUoØq¿ ˜ç2®V€ ¯vìø²À>Ì�6ð4€Q`x£�6ì¸_LÕJüu#õ‹;µEÿ5 \ôhà+Û?÷¶êCÉÑ@bÚÊë×[%ñÙÓX]½3­çÊnA…�wIn°UJŽÓ–:øãm©¸UŸ= ÕÕùwGK™’Ô!àó0pñÃG>õ{�ªIÀäõëöaœ�;||®ýs‡n£o¥�ù0Ô�Š4”-šD=>^�ñ'5;wøxÁòÀ–̪ ÖQ+üÝ/€YG˜yô¼•ÓèõÓ…NO£gi��igçýÔéÎÿL­10_[ü‡Që¤Ã>`Öh0P‘ T$ À@E0P‘ T$æãh4ºÌµ—.C~Ïé²%êÓÖÖÖ5þ¨+¸4Ïɼ�·ç«%`>^žtÌÇ̖ή[`rÇÃútmÒ*0Ÿ2[8»n�É�'fâÐHS¹Sk80‡F˜Œö˜ZÃÖ€ñ`âÏ`˜ˆLZ&¢:0&û�Ø—ÍwnÞ/Ûâ‘�ÔÈ–Oè�ªÉ~R Ðǰ^*.ëþØ�qjFò±z]v^Ì—=;Ûnb^¯ {1`LöK_ü5óÝ›÷k¶xËCjZi-©Ù×ôÉ6#?õ~¹»'¿Ÿª×5çÅüwͳ£þlÿµqÊpÛ¦úÆMéÈÆ-fIJ¯�TOöõ‰=× —uì§Bcú¡‚Ö50ÕuÜnâ÷/ã»¶€©¾ySºeãWÆ¢ ´f‹ÊŽƒ~©Ý'öC~+X·To[[ŽÝ݇ã»ù�‘ˆ^æ˜ðûȾ>Rý0›`^VÀ0<£ÉÇØäfŸV�õ«HóHª»Ë¶ü²6³O¶ãq£ØxÅßÜòWI°éëÒ7ÿdj¯U•ôDø´•˜Z,W“O±I�Û}âˆ_ÏÞesYX j¥úQÛÒuÉú‘òýÑ›™”OA<]MÔÔïb{9 `®I }óO2ƒÈ«˜kñêvUŸ¼™I™ñä÷'Ÿ#ã\`/' Œüõ^Ž£¬¥¦ ˜š¿D½Œ€Ÿ#ï�TFû5o]#Å×’À¸Væ¯üZ<@5?‰zqÉOˆ-ï�TFûåþû ÌGÎy†ü8`¼”Ç£úk†ýÍ­ó‰rÆ{^9`>qn5ö7ŸÆd™—9.-Ã~f£û³Ó’ÍnÃñŠ¿¹5'0”e. ̧­<`ØÎå1®/‡Ùê 0£Q;SûÑWæŒî¯ ñ7·V ç#:à×¼Þh™¿LI `Äß|jÞ_i ˜Ë™À\Žä0“úxun4Ÿ˜’&“Å�¹– ̵H3©�35žLµÌÇQÜ>ÐüÀ\Ž®’¾]`®EWI+&wY­�iÊar—ÕìÇ߇a;o›Ä�i3‡ÉXVëËÂËjÚ«Ñ~£û0lçMc“80må0zQë6Â\.ë–¼qÇáÑÆÛÂÀ�Ì$ oôÑ8«áEÆ+þæ–·â6Ä\ üp»'ÁÔ `xãO�†7údÃÐöŽs+±‘˜) Œ]�pò8©­z"G¶ü2·nšr£„hýÈùñ€ñ¶TÜ*‰ÏžFêŽWìæ–]—p9Q«Ax–4¡bÙ¢�HŽ;¯êhàSŒ·¥âVIŸ¶êý†ã»ùäý¦×æ£zô_¶±Š>N$ü£Ð>ÐG—'êùðñrŒwø8©Ù¹ÃÇË‘ñŽÜFßœò�ù¤¦€k6fQ`¤OncÍ“>&€q‡�×B`¼ÃÇIÍÎ>^‹Œs+1ž<5ÿ<ë‹^ñ�nW1„¾ðu€™G&¦Ë™§¼‹e”‹+wœméÚB§½ë>¾`2wÔGK�ÖüãlM í¬¯ýø€ù˜‡Âª0¹ãlMAŽÛ;u;>üËG¨H*€�Š` "¨H*€�Š$Àü·½½}‹?j .]¦ü¤ËV Ï;;;wÕµ%§ye=�æ¿[“N�ù/Óƒ³ë˜Üñ|¾+œ Ì结,9»n�ÉO–˜‰C#ÌÜ[10µ† 3ƒÒS`j ;Æ€ `nÙÏÆ€‰À¤`"ªc²ß‰ýpË|׿ý–-Þö�¢zÛ‚Ú+µ›ÔôŸuãê¥â–ç—»#ÿÿU¯[΋ùï–ggÛùý»qËð‹€©¾~ƒiš[•À|®²_{c­ýÙú©Á瀰ÏÜþnÕŒ³èé�éèsõºë¼˜ÿîzvwõ¸È¿IÆw<»¥S}Ó¦tÛÆ+†ê«Ð™ ˜êö–�çh›Êõ,²#¿ÿ±?ò_Áë˜jGÄ-5¿·Ø>a„DÌN°40;PÎ÷_ÕïP¤ïÓÆ•j`ªw];ù`Ä®}`$’·8ü¾m_ÿQàüðrÿ3ï·*£mz xíÙ§Vc}åñ?‘ø÷ìÍs§z¢°�§Ö?�[üŠ�1Ѳô�ùliùìU)Q½iû96‰}6å&Òܼº»kËïj3ûDa;ŠÇ%þiJúïL* �¹ ~’ÐüÇ~õI€a;Œß‡Îa:ØüK#µ·âÀp=—Sbâ]c@Ôü&êe$ü„ØöH5`´_¯ÿ�©þÒ'<-É›\ &ó¿»1"äip7 ×S_î ±ã=�jÀh¿æ Àü'9�ÊKy0ÿq®5öW¨¥ó™rÆKy0Ÿ]ÒÔ ûkW-CÙ¥\o-ÌÛyÀ°�Ëc‚þUûy€1Ùd·ü¸Ëüng²ÒXó<`>ïäÃv.�±ã �íª¿Àp>¢®Ì˜íí`JJ#þJµ$`8ÑWæÌÎN0%%€íªå))¸.Ì­L`nEr˜I}Ü:7*R€I˜»Ÿæn&0w#9̤žôêܨE­0·¢«¤Î�ùì/¦“À˜x¶ ÌÝè*©·À,«U+w]tYM{5Út†í¼ilf®†0p_~tYm7^ïÆšç-«i¯F߇a;o›Ä�é:‡ñB.`î‹¿¥[r½zL Ýua`hO†7úÔÆá$2nñW,Ù‰Ÿ~¬î9DRø×®• mà)`&Q`x£�6ìx\ ˜»4ºÏc×!œ4NÔꃿòÄÑ€_kHŽê·yïâãm©¸UÒÛMý»£�ù0Õ!â]Õâ&º³*G õ;¼÷?ñ�ñ¶TÜ*‰ÏžvÔÕùwGOI˜ÿÔ#ÿ–�QêðQš«%��ÃÇd=>Þ �ñ'5;wøx+2îm·ÑW&™‚v:êúyGf‚(Ÿ]Òœ¨çÃÇ»!0Þáã¤fçïjÿÜ¡ÛèkQÍ?ŸõÏý“ƒŽÔÉxzúãÚ ÀÌ€ÑjæÖŒÓÝYõËVWãé slEw;9�ž¥`fí¤Ï¹ÓÞ™:O‹ÿš¤í�­å`‚·¸~Ùêh<}}Àpn»dá_>BE0P‘ T$ À@E0P‘ T$æËîîî}þ¨-¸t•òG”.ƒ:—æËýÉR�ù’éÑÙu Lîx ‰fâÐH3o„jíV ÌÜã�&q`|H À˜"S¤:0&û�Ø÷ÍwkÞïÛâ]…Ô—]óŸ¹çb²¯ &‘vÒ<Ъ¶öºâ¾‡êgb:øR½î·?¨I ÀT߬)ݵñ �ÙÝu¸ïì't¶»0@Üì>w,]Ü×ã�öŒé‡ æÝÆ5IoÜù«$Èôõ…¥š¿óê/ø µd{ck÷eŸôìSLÙ×ë¿ìª~ìÝ}[~¿ýñ@�Êæ¾J7”±­ �‚ h—ÊEØ6 ”ê‡1¡™©õñ@�J#¥÷ó€Ñöæ-7@µ~õÒžŸ»Þ©µñ@�j˜/œëÌÙ…Ê†Û 0ž·ÖÆ5jq`¾ì¢¬2lÇv¡r�‘ö3€Yt`, where ``screen`` is your screen instance or :attr:`MainLoop.screen`. .. seealso:: :meth:`register_palette() reference `, .. _using-display-attributes: Using Display Attributes ======================== Once you have defined a palette you may use its display attribute names anywhere that expects a display attribute. When no display attribute is defined ``None`` is used as a default display attribute. ``None`` will typically be rendered with the terminal's default foreground and background colors. You can also specify an exact foreground and background using an :class:`AttrSpec` instance instead of a display attribute name. Using :class:`AttrSpec` instances in your code may be trickier than using your screen's palette because you must know which mode (number of colors) the screen is in. .. _text-markup: Text Markup ----------- A :class:`Text` widget can specify which display attributes each part of the text will use with the format defined in :class:`Text class reference `. Some examples: :: Text(u"a simple string with default attribute") The string and space around will use the ``None`` default display attribute which usually appears in the terminal's default foreground and background. :: Text(('attr1', u"a string in display attribute attr1")) The string will appear with foreground and backgrounds specified in the display module's palette for ``'attr1'``, but the space around (before/after) the text will appear with the default display attribute. :: Text([u"a simple string ", ('attr1', u"ending with attr1")]) The first three words have the default display attribute and the last three words have display attribute ``'attr1'``. :: Text([('attr1', u"start in attr1 "), ('attr2', u"end in attr2")]) The first three words have display attribute ``'attr1'`` and the last three words have display attribute ``'attr2'``. :: Text(('attr1', [u"nesting example ", ('attr2', u"inside"), u" outside"])) When markup is nested only the innermost attribute applies. Here ``"inside"`` has attribute ``'attr2'`` and all the rest of the text has attribute ``'attr1'``. Assigning Display Attributes with AttrMap ----------------------------------------- If you want a whole widget to be assigned a display attribute, or if you want to change one or more display attributes to other display attributes, you can wrap your widget in an :class:`AttrMap` widget. :class:`Text` widgets have no way to specify a display attribute for the whitespace around the text caused by alignment and wrapping so :class:`AttrMap` may be used. Some examples: :: AttrMap(Text(u"hello"), 'attr1') The whole :class:`Text` widget will have display attribute ``'attr1'`` including whitespace around the ``"hello"`` text. :: AttrMap(Text(('attr1', u"hello")), 'attr2') The ``u"hello"`` text will appear with display attribute ``'attr1'`` and all surrounding whitespace will appear with display attribute ``'attr2'``. :: AttrMap(Text([('attr1', u"hello"), u" world"]), {'attr1': 'attr2'}) The :class:`AttrMap` widget will apply display attribute ``'attr2'`` to all parts of the :class:`Text` widget that are using ``'attr1'``. The result is the ``"hello"`` text appearing with display attribute ``'attr2'`` and all other text and whitespace appearing in the default display attribute. :class:`AttrMap` can also change display attributes differently when they are in focus. This can be used to "highlight" one or more widgets to make your interface more user friendly. To use this feature set the ``focus_map`` parameter when creating the :class:`AttrMap` widget. .. _foreground-background: Foreground and Background Settings ================================== .. list-table:: :header-rows: 1 :widths: 23 15 10 10 15 * - Supported by Terminal - xterm / gnome-term - rxvt - linux console - others * - :ref:`16 standard foreground colors <16-standard-foreground>` - YES - YES - YES - very widely supported * - :ref:`8 standard background colors <8-standard-background>` - YES - YES - YES - very widely supported * - :ref:`default foreground/background ` - YES - YES - YES - widely supported * - :ref:`bold, underline, standout ` - YES - YES - standout - widely supported * - :ref:`italics ` - YES - YES - NO - widely supported * - :ref:`blink ` - YES/NO - NO - NO - some support * - :ref:`strikethrough ` - YES - NO - NO - some supported * - :ref:`faint ` - YES - NO - NO - some support * - :ref:`"bright" background colors ` - YES - urxvt - - some support * - :ref:`256-color foreground/background <256-foreground-background>` - YES - - - some support * - :ref:`88-color foreground/background <88-foreground-background>` - w/palette setting - urxvt - - limited support * - :ref:`RGB palette setting ` - YES - - - limited support .. _16-standard-foreground: 16 Standard Foreground Colors ----------------------------- * ``'black'`` * ``'dark red'`` * ``'dark green'`` * ``'brown'`` * ``'dark blue'`` * ``'dark magenta'`` * ``'dark cyan'`` * ``'light gray'`` * ``'dark gray'`` * ``'light red'`` * ``'light green'`` * ``'yellow'`` * ``'light blue'`` * ``'light magenta'`` * ``'light cyan'`` * ``'white'`` .. _8-standard-background: 8 Standard Background Colors ---------------------------- * ``'black'`` * ``'dark red'`` * ``'dark green'`` * ``'brown'`` * ``'dark blue'`` * ``'dark magenta'`` * ``'dark cyan'`` * ``'light gray'`` .. _default-foreground-background: Default Foreground and Background --------------------------------- * ``'default'`` (or simply ``''``) ``'default'`` may be specified as a foreground or background to use a terminal's default color. For terminals with transparent backgrounds ``'default'`` is the only way to show the transparent background. There is no way to tell what the default colors are, so it is best to use default foregrounds and backgrounds together (not with other colors) to ensure good contrast. .. _bold-underline-standout: Bold, Underline, Standout ------------------------- * ``'bold'`` * ``'underline'`` * ``'standout'`` * ``'blink'`` * ``'italics'`` * ``'strikethrough'`` * ``'faint'`` These settings may be tagged on to foreground colors using commas, eg: ``'light gray,underline,bold,strikethrough'`` For monochrome mode combinations of these are the only values that may be used. Many terminals will turn foreground colors into their bright versions when you use bold, eg: ``'dark blue,bold'`` might look the same as ``'light blue'``. Some terminals also will display bright colors in a bold font even if you don't specify bold. To inhibit this you can try setting ``bright_is_bold=False`` with :meth:`BaseScreen.set_terminal_properties`, but it is not always supported. ``'standout'`` is usually displayed as the foreground and background colors reversed. .. _bright-background: "Bright" Background Colors -------------------------- .. warning:: Terminal support for bright background colors is spotty, and they generally should be avoided. If you are in a high-color mode you might have better luck using the high-color versions ``'h8'``, ``'h9'``, ``'h10'``, ..., ``'h15'``. * ``'dark gray'`` * ``'light red'`` * ``'light green'`` * ``'yellow'`` * ``'light blue'`` * ``'light magenta'`` * ``'light cyan'`` * ``'white'`` .. _high-colors: .. _24-bit-foreground-background: 24-Bit Foreground and Background Colors ------------------------------------------ In 24-bit color mode, any hex color code of the form #rrggbb can be used to specify a precise RGB value for foreground and background colors. Support for 24-bit color mode varies widely among terminal programs. Furthermore, terminal multiplexers such as tmux and screen can sometimes interfere with the operation of 24-bit color mode unless properly configured. .. seealso:: The palette_test.py_ example program .. _palette_test.py: https://github.com/urwid/urwid/blob/master/examples/palette_test.py .. _256-foreground-background: 256-Color Foreground and Background Colors ------------------------------------------ In 256-color mode you have the 16 basic colors, a 6 * 6 * 6 color cube and a gray scale with 24 entries (white and black not included). The color cube is weighted towards the brighter colors, with RGB points at ``0``, ``0x5f``, ``0x87``, ``0xaf``, ``0xd7`` and ``0xff``. The hex characters ``'0'``, ``'6'``, ``'8'``, ``'a'``, ``'d'`` and ``'f'`` are used as short-forms for these values. High colors may be specified by their index ``'h0'``, ..., ``'h255'`` or with the shortcuts for the color cube ``'#000'``, ``'#006'``, ``'#008'``, ..., ``'#fff'`` or gray scale entries ``'g0'`` (black from color cube) , ``'g3'``, ``'g7'``, ... ``'g100'`` (white from color cube). .. seealso:: The palette_test.py_ example program .. _palette_test.py: https://github.com/urwid/urwid/blob/master/examples/palette_test.py .. _88-foreground-background: 88-Color Foreground and Background Colors ----------------------------------------- In 88-color mode you have the 16 basic colors, a 4 * 4 * 4 color cube and a gray scale with 8 entries (white and black not included). The color cube is weighted towards the brighter colors, with RGB points at ``0``, ``0x8b``, ``0xcd``, and ``0xff``. The hex characters ``'0'``, ``'8'``, ``'c'`` and ``'f'`` are used as short-forms for these values. High colors may be specified by their index ``'h0'``, ..., ``'h87'`` or with the shortcuts for the color cube ``'#000'``, ``'#008'``, ``'#00c'``, ..., ``'#fff'`` or gray scale entries ``'g0'`` (black from color cube), ``'g19'``, ``'g35'``, ... ``'g100'`` (white from color cube). .. seealso:: The palette_test.py_ example program .. _palette_test.py: https://github.com/urwid/urwid/blob/master/examples/palette_test.py .. _rgb-palette-setting: RGB Palette Setting ------------------- A few terminals have the ability to customize the terminal palette's RGB values with :meth:`raw_display.Screen.modify_terminal_palette`. There is no automatic way to tell if this is supported by a user's terminal, so this feature shouldn't be relied on. :meth:`raw_display.Screen.reset_default_terminal_palette` is used to reset the palette in the ``palette_test.py`` example program when switching modes. Recommended Combinations ======================== Neutral Backgrounds ------------------- .. image:: safe_combinations1.png :scale: 50% Choose colors that are fairly neutral with medium contrast for most of your application. It is good to use one background as a default for text, another for edit boxes and a third for selected edit boxes. Foreground colors shown here in bold text will appear as bold text on many terminals. Bold fonts are often more difficult to read so those foreground colours should be used sparingly. Bright Backgrounds ------------------ .. image:: bright_combinations1.png :scale: 50% Use bright colors to draw attention to small areas with important information. They are good for buttons and selected widgets (other than edit boxes). urwid-urwid-69907e4/docs/manual/displaymodules.rst000066400000000000000000000126021526073734300222750ustar00rootroot00000000000000.. _display-modules: ******************* Display Modules ******************* .. currentmodule:: urwid Urwid's display modules provide a layer of abstraction for drawing to the screen and reading user input. The display module you choose will depend on how you plan to use Urwid. .. image:: images/display_modules.png Typically you will select a display module by passing it to your :class:`MainLoop` constructor, eg: :: loop = MainLoop(widget, ..., screen=urwid.display.curses.Screen()) If you don't specify a display module, the default main loop will use :class:`display.raw.Screen` by default :: # These are the same loop = MainLoop(widget, ...) loop = MainLoop(widget, ..., screen=urwid.display.raw.Screen()) Raw and Curses Display Modules ============================== Urwid has two display modules for displaying to terminals or the console. The :class:`display.raw.Screen` module is a pure-python display module with no external dependencies. It sends and interprets terminal escape sequences directly. This is the default display module used by :class:`MainLoop`. The :class:`display.curses.Screen` module uses the curses or ncurses library provided by the operating system. The library does some optimization of screen updates and uses termcap to adjust to the user's terminal. The (n)curses library will disable colors if it detects a monochrome terminal, so a separate set of attributes should be given for monochrome mode when registering a palette with :class:`display.curses.Screen` High colors will not be used by the :class:`display.curses.Screen` module. See :ref:`setting-a-palette` below. This table summarizes the differences between the two modules: ============================== =========== ============== .. raw display curses display ============================== =========== ============== optimized C code no YES compatible with any terminal no YES [1]_ UTF-8 support YES YES [2]_ bright foreground without bold YES [3]_ no 88-/256-/24-bit color support YES no mouse dragging support YES no external event loop support YES no ============================== =========== ============== .. [1] if the termcap entry exists and TERM environment variable is set correctly .. [2] if python is linked against the wide version of ncurses .. [3] when using xterm or gnome-terminal Other Display Modules ===================== CGI Web Display Module ``display.web`` -------------------------------------- The :mod:`urwid.display.web` module lets you run your application as a CGI script under Apache instead of running it in a terminal. This module is a proof of concept. There are security and responsiveness issues that need to be resolved before this module is recommended for production use. The tour.py_ and calc.py_ example programs demonstrate use of this module. .. _tour.py: https://github.com/urwid/urwid/blob/master/examples/tour.py .. _calc.py: https://github.com/urwid/urwid/blob/master/examples/calc.py Screenshot Display Module ``html_fragment`` ------------------------------------------- Screenshots of Urwid interfaces can be rendered in plain HTML. The :class:`display.html_fragment.HtmlGenerator` display module lets you do this by simulating user input and capturing the screen as fragments of HTML each time :meth:`display.html_fragment.HtmlGenerator.draw_screen` is called. These fragments may be included in HTML documents. They will be rendered properly by any browser that uses a monospaced font for text that appears in ``
`` tags. HTML screenshots have text that is searchable and selectable in
a web browser, and they will shrink and grow when a user changes their
browser's text size.

The `example screenshots`_ are generated with this display module.

.. _`example screenshots`: http://urwid.org/examples/index.html


LCD Display Module ``display.lcd``
----------------------------------

Almost any device that displays characters in a grid can be used as a
screen.  The :mod:`display.lcd` module has some base classes for simple
LCD character display devices and a complete implementation of a
:class:`display.lcd.CF635Screen` for Crystal Fontz 635 USB displays with
6 buttons.

The lcd_cf635.py_ example program demonstrates use of this module.

.. _lcd_cf635.py: https://github.com/urwid/urwid/blob/master/examples/lcd_cf635.py

.. seealso:: `Urwid on a Crystalfontz 635 LCD `_


.. _setting-a-palette:

Setting a Palette
=================

The :class:`MainLoop` constructor takes a *palette* parameter that it passes
to the :meth:`register_palette() ` method of your display module.

A palette is a list of :ref:`display attribute ` names and foreground
and background settings. Display modules may be run in monochrome, normal or
high color modes and you can set different foregrounds and backgrounds for each
mode as part of your palette. eg:

::

    loop = MainLoop(widget, palette=[
        ('headings', 'white,underline', 'black', 'bold,underline'), # bold text in monochrome mode
        ('body_text', 'dark cyan', 'light gray'),
        ('buttons', 'yellow', 'dark green', 'standout'),
        ('section_text', 'body_text'), # alias to body_text
        ])

The :ref:`display-attributes` section of this manual describes all the options
available.
urwid-urwid-69907e4/docs/manual/encodings.rst000066400000000000000000000063261526073734300212160ustar00rootroot00000000000000.. vim: set fileencoding=utf-8:

.. _text-encodings:

***********************
  Encodings Supported
***********************

.. currentmodule:: urwid

Urwid has a single global setting for text encoding that is set on start-up
based on the configured locale. You may change that setting with the
:meth:`set_encoding` method. eg.

::

    urwid.set_encoding("UTF-8")

There are two distinct modes of handling encodings with Urwid: Unicode or
Pass-through. The mode corresponds to using Unicode strings or normal strings
in your widgets.

::

    txt_a = urwid.Text(u"El Niño")
    txt_b = urwid.Text("El Niño")

``txt_a`` will be automatically encoded when it is displayed (Unicode mode).

``txt_b`` is **assumed** to be in the encoding the user is expecting and passed
through as-is (Pass-through mode). If the encodings are different then the
user will see "mojibake" (garbage) on their screen.

The only time it makes sense to use pass-through mode is if you're handling an
encoding that does not round-trip to Unicode properly, or if you're absolutely
sure you know what you're doing.

Unicode Support
===============

Urwid has a basic understanding of character widths so that the text layout
code can properly wrap and display most text. There is currently no support for
right-to-left text.

You should be able to use any valid Unicode characters that are present in the
global encoding setting in your widgets, with the addition of some common DEC
graphic characters:

::

    \u00A3 (£), \u00B0 (°), \u00B1 (±), \u00B7 (·), \u03C0 (π),
    \u2260 (≠), \u2264 (≤), \u2265 (≥), \u23ba (⎺), \u23bb (⎻),
    \u23bc (⎼), \u23bd (⎽), \u2500 (─), \u2502 (│), \u250c (┌),
    \u2510 (�), \u2514 (└), \u2518 (┘), \u251c (├), \u2524 (┤),
    \u252c (┬), \u2534 (┴), \u253c (┼), \u2592 (▒), \u25c6 (◆)

If you use these characters with a non-UTF-8 encoding they will be sent using
the alternate character set sequences supported by some terminals.

Pass-through Support
====================

Supported encodings for pass-through mode:

* UTF-8 (narrow and wide characters)
* ISO-8859-*
* EUC-JP (JISX 0208 only)
* EUC-KR
* EUC-CN (aka CN-GB)
* EUC-TW (CNS 11643 plain 1 only)
* GB2312
* GBK
* BIG5
* UHC

In pass-through mode Urwid must still calculate character widths. For UTF-8
mode the widths are specified in the Unicode standard. For ISO-8859-* all
bytes are assumed to be 1 column wide characters. For the remaining supported
encodings any byte with the high-bit set is considered to be half of a 2-column
wide character.

The additional plains in EUC are not currently supported.

Future Work
===========

Text encoding should be a per-screen (display module) setting, not a global
setting. It should be possible to simultaneously support different encodings on
different screens with Urwid. Making this work involves possibly changing the
function signature of many widget methods, because encoding needs to be
specified along with size and focus.

Device-specific encodings should also be possible for Unicode mode. The LCD
display module in development drives a device with a non-standard mapping of
Unicode code points to 8-bit values, but it should still be possible to use a
Unicode text to display the characters it supports.
urwid-urwid-69907e4/docs/manual/images/000077500000000000000000000000001526073734300177515ustar00rootroot00000000000000urwid-urwid-69907e4/docs/manual/images/display_modules.png000066400000000000000000000374641526073734300236720ustar00rootroot00000000000000‰PNG


IHDRH"n–[sBIT|dˆtEXtSoftwarewww.inkscape.org›î< IDATxœíÝyxL×ãÇñ÷d‘ˆ 	b‰¥¨%%Öˆ’±V©½ª¨¢ø•ÒjµßZºXJ«ÕM«EK•¶øª-ö­jß÷5DD"d‘åþþÈ×ÔT,µÔr?¯çéódæž9÷Ì�3÷~î9gÔÈcË0Ë픳³³»b†ãýn�<0¹
ø|«B‹EçƒÇØížÔoïs.\|0­‘ûª]>×Û.k±³³üC.gçûØ"y:ÈŸ™•™yÛåu>x<ý“ó¨<®rêv """ò�P11‡ݹ¿æÌ™c3××®]»Ûš#”LJ���ýOÓ§§Î™3çÚ§s·k×î–käñ¢ó�Àõý@#"æsûDä±§  ""bb
"""&¦  ""bb
"""&¦  ""bb
"""&¦  ""bb
"""&¦  ""bb
"""&¦  ""bb
"""&¦  ""bb
"""&¦  ""bb
"""&¦  ""bb
"""&¦  ""bb
"""&¦  ""bb
"""&¦  ""bb
"""&¦  ""bb
"""&¦  ""bb
"""&¦  ""bb
"""&¦  ""bb
"""&¦  ""bb
"""&¦  ""bb
"""&¦  ""bb
"""&¦  ""bb¦
ééÄFFZÇ�9Cú•+wUgRìyR““ï¶iò�1ƒs§Na‰çb¸|éÒ]Õy)1‘äøø{Ñ<ÓKMN&)öü]דráâbÈÌÈàüéÓw]gld$ééw]Ï£.áìYÒRSï[ýÿä;y1!áž|¶�+S�ÈÇXßúøÝÆ�8¹gÏ]Õùý A¬�ùóÝ6M2W._¦Ÿo%kÈû¢W/¶,XpWuÎÿ|¿�}/šgzüö+ßöï×õ¬š>�©o¾	ÀùˆÖ¬~×u®[‡è°ãw]Ï£n\çNìY½*Çm).°lÊ仪ÿËWz³é÷ßoYnl§Ž¬Y�¯ú¼rWû{ÐmÚÈáÍ›ïKÝ÷¥ÖGÄë?L£ø“O>èfÈ# ë¨Q¸yy=èfÈ}äîíÍ»ÿ�÷ ›a
ÉññÌ|ÿ=š¼Üó¾îçì‰ìY½Ši§Ïàèät_÷u¿m
ÅÎÞž'ýýïyÝÿjXóóÏxù”àäž=Þ¼™n£Ç�ÃúÙ¿q>"÷"EhÞ§EË•`öØ�z¾3ž%Jsò$‹¿ý†¶ƒ“ßÓ‹3G�²sù2Z¾zó»‚­‹²ñ¿ÿÅÝ»U…ØlÛ³z
&wÞ¼ÄFF²èë¯8{òyòç§n›vTkÒ„ðýû8ðÇ-ÿ$ëû…<ùÝh5pÞÞ×íëB\,+~ø�ð½{qtr¢zÓ¦ÔmÛ€u¿Ì"¯»Õ7¶–_?û7r»æ¥F³fw{h:§ìgå�?{ú4ž>>´8÷"E˜õá4ïÛ—ü=ع|9i)—¨Óú9bÂÃùsöoø5`õOÓñ©\™FÝ_"tâDŽnÛŠ½ƒ¥ýüh=p�=Õ³lò÷Ù²…|4{¥EË•²O‹¿™ÈùÓäuw§~ÇNøݴ͇6mdÕ´qvÍK£îÝm¶Ù¼™2ÕªãæUˆK‰‰,üêKN8€SîÜTiLƒ.]Hˆ‰aɤo©ÝòYVL�@³>}ð©Tùº}]¹|™S§plDz23©X·.!=^ÆÞÞžÝ+W’xîA�;[Ëï]³šó§#	îÚõN?’‡Br\¿OøŒ?	À®+8ºm+ÿó.�}²³ØÙQ½iS6Ïÿ�íKBÁb¡^ûTilSßæóÙ<>îÞExnàëäõð¸éþ333	�ø5Ƕoç‰êÕùßÌi—.±cÉ*ÔàðæÍ¬šþ#Éññ¸ñ¦yŸ>+ÿ$›çÿN.çÜœ‹8ÅÁ
ð©\™gûÈñBsêÀ~ÖÍšÉÙ'ÉW° !/õ ŒŸ¿Œü� Î/P¸tikùÙ�! M[k?~Ôe\¹Âœqc9µ?ëÖ¥I¯ÞØÙÙ1ÿó	¤¥¤0uÈ[´}óMN:DÜ™3ärvfË‚¸{¡Ã;ï¾oK¿ÿ§Ü¹i3øM
•,y[ûŽ	ç—‘bgoÏOÇQÆÏ�Êõê³rÚ�ÔhÖœ•?þ€wÙ²„¼ÔƒS§¶k†aP¹~}‚»uÇÎ.{À<õâEþûÉÇÄ„‡ãH~//,X¨Ù¢gŽe[èbÊ׮ͪéÓppp¤ýÛïp%5•ùŸOàrJ
Í_éC¹Zµ¬í:ºmëfÍ$9>ž*
ÒðÅ®X,⢢X1u
þ­[³ô»ïÈH¿B³Þ}(ãçljݻٷn-;;Ò†¤R¤Lšõ¾w#ÿêÔÀ®åËøºoÒÓÒîÚ
ç*š\ÎÎ4ëÓÿV­˜ýÑG¬ù9{ÚÀÉÅ…ÇYËf¤§óãÛo“ï'®GÑÑ­[Ö¤1^>>´8ˆ’•}IŽ�`ÅÔ©¤$&]Svûÿø€„³Ñü>á3æ>�M›Q®f-|>�­‹ѨûK4îÙÓúåÝ®-a»vÒ´woJU©Ê°&�­k@ÆthO.ggZ½6�š-ž!åÂ…›¶ùøŽŒíÔßÀÔjÑ‚iÿù�Íö-ræÈ~xû-N:Dó>}hÐ¥éWÒHŽ�cÁŸ3{ìžîÐ�•*1¬Iãç&S“/�’œLp×®4|±+æÎåבP Ha¦yÓ¦�Íúðìý¼<
°jú4ÎÍ>–¡“¾aþ„ψ‹Š`Ñį­Çó·1£™ÿùžnß‘€Öm˜ôÚ¶/Yb­ëÈ–-ì[»–Æ/¿Ìå‹—Þ¼YYY7Ýÿ÷ƒ²cÙ2šôê…½£#‹¿ùÚºíRRK¿ÿÈ,#ŸkEÙ5ynÐ<éïOJRvÚÿÇL|µ).дW/ŽlÙ̽r¾³=sô(%*V¦Õk)S­>׊³'Ndï#>žÐIßZ˞ܻ—ÅßLijD‰tLfsÆŽÅû(�ÏwfÑįY3cåkׯÁÑ¿F!ø5
ÁÙ%'víâ§aC	ß·�†/¾HØÎ]Œ}¾‹¾þІ]ºàèìÌÇ/<Ûûvus£\­Z889á×(ŸÊ•Iü€±±,øòæ�O�Ö­)Z¶¶~–Ô‹)P¸0…J–¢PÉRø5
¡lõ÷ê#ÀÔÀS
òÜëoX7èÒ…¬¬,¢£q-àÆöÐPv¯\EàóÏãÄî•+iöJö­[KÓW^aïšÕuî̾µkiÒóæÃJ‹¾þŠNC‡S³E’Οç—?̱lÔñã<Û€õnàZ†aÐ狯ÈåìLÅ€ºìY³šMóæÙܱ”ôõ¥¤¯/H:žà®]Ù8o.
^x�šÍ[0eðÞ¼™'ýýÙº7//›¤ø¸˜;þcZ¾Úß:ZóO†²2®\¡ß×É“??¡ßN¤DÅŠøáàèHåzÙk<nø“³'Â:ïwììì¨P—°�;XóóÚ¾ù1'OP%8˜
unk¿K¾û–Æ=^&ðùì�½£#û֮ɱlÔñãÔlñ•êÕ·	&WÛßó“O)X¬•ëÕçØöí¬øa*�‡�°)—ßÓ‹öCÞ&59™Ä˜ZôëÇŒáÃè<â=|*U¦D…Šlœ;—]ºpêÀ~¢Ž' M›Û>Ž+;;;*Õ«ÏÞ5k(Tª4'÷ì!ðùÎì]³†ºmÛrtÛVÿ4ƒ´”æ}:ž/wí¡`±btJÆ’IßXGМ]óðò'ã±³³ãIÿ:¼ZÅ—½kÖP588Ç}§\¸À꟦óÍþƒx-J¥§ëqdË–ËÆEGa±³Ã¯Qž%JP¾vm›í¥«V¥Íƒ(þdz•/KÜ™3x-jS.à¹6†AbL®Üð­ȶŋhÙMzödXÓ&tyÿr9;³â‡©u~�\ÎÎwuŒ&õ:v¤A—.D;ʾuk	îÚ•'ýë`ïèˆ_ˆí(­—�Ï@V–ÁGÚñéÓävuåÉ:tö*Hr|EØÎ×Õq))‰�_èL\Ô
—*EfF&ñÑÑÖíMzö$tÒ·4èÒ…?ü@àó�›ÄSAAì^µŠRUªPºjUüBBØ<>E½)V¾<®
¾W®ðA«–Ö×¥§¥‘Û5¯õqÑrå¬AÌÎÎŽ•*}üØ
ƒÀ¹S§pvuµ¹XûT®Ì郯+[²²/
º¼Èk5«ãS©µžiIó¾ýpÊ�€b׬-Êëá�»·7ÑaǯÛ/fÊ[ƒñðö&¯‡‘‡ãñ¿þQ¢b%Š?Y��ÿý/uZ·æÏÙ¿1fuÎáóQåýÄÖ¿óº{p1áæ¿šñò)yMywòzx�ÛÕ€\ÎÎ8çÉÃÅ„„Û
7’×£ MHŽ‹cÜ�I:žB%}ȸ’NÂÿ®A1'Ãñ,ácýܳߓí5¬@á"8æÊõ¿º³ëõºfT'Ÿ‡‡õ×B§bÆðaü2räÿ¶¤§¥q)1ñ¯¶]óÞòº»s1!áŽßëíú׃€Ý5Û™™™L8€�Ö®£D…ŠŒéØž«wEžx‚Ü®®,›ü=¥«V%ŸGAÜ‹aåS)Z®Ü-;Cžüù¹”ø×A¼””tòõ;v¢n»öÛ¶�E_ÅGÛóɆM¤üíç�ñþ[ ˜=vÕ›6³&Úõ³ã÷O?µnoÔý%^«Q�ýöpdëVÞ˜>ã¦íTåuwçbbÎ�×ÞÑ�ôô¿~²™zñ¢Ív;{{›ÇÅŸ¬Àø�›9{âæÎáƒg[òÕº»S¢R%F¯\�ã~÷x™à®Ý8¼y¿OøŒÏºwçýÐ%9–…ì¾rñ_F€”›ô•Êõê3qßÂ÷ícå�?0¼YS&Ëž¦º’šJfF†uÿRb.×›«–~ÿù<�‚ØÙÛóɆM7@—“mûÎ¥„Ä�õUyòç'íÒ¥ë>Ÿé1v/Œx�=«Wñë¨Q$ÅÆÒ}ô˜÷�’”D·×Õ1yðëôÿî{ëHÖÄÿëǵšôêÅÒï&‘•™‰Oe_ëú¨Ç…ÅîŸÍ>[,–›>¾ìlÏ1‹¿™ˆ—O	>\ºÈ^+ôQÇ@ö…ýBl¬Mù‹		(ü×MEŽm¼A»óº»3àû)9�’FD`gwó÷k\»¨åz ?ÌÊÈ ãÊÒ/gÏ	F:Ⱦ5¶‰Ø7(ˆyãÇ[O¾�AÌýøc|ƒ‚nYÕàF¬š>Ã00ƒUÓ§åXÎ0¢Ã°ÿߊÌFÝ_">꯻³³aaì[·Ⱦ«Ø½j%Ur¸ëHKIåÊåË@öb°åS¦Øl÷ðöæ© |üÂó<÷.ùòÝò=<Šj=Ó’%“¾µÎ‹e¤§[ÿöò)É¡
ÙókÉqql]´ð¦uE?Ža.]šýþ§y2¾AAäqs£@‘"¬ž>ß ¸)BéªU™÷éxëë232¬óëG¶n!|ß>Â÷íãÄžÝÖnN
/N‘2O°æçì~)1‘ÍóççXöB\,prq¡Ö3-ñ	!áš›-’x.{}ц¹sprq±%¸*-%…+©ÙŸóùˆˆëú»«ÖD‡�`öØ14~LGs’}“–h3
ö \NI±^ƒ233Yòí_ë6JU©‚SVü0€{öX×­Ý	ÿV­™÷Ùxëy²×‘ÜŽÏS�
nYÇwßel§Ž¨îG.ggªshãÆëÊeeeñ~Ë8çq%w^WÎ�Š ûGY·—zê)–~÷3?xŸèãÇi?ä�W‚·ì?€ÑíÛ²wuö]ª_ãÆìZ¾Ü¦L“ž=ù°u+B^zù–íT=ójbÂÃù¿§|ñ*éC|t4¯ÿ8�'ýýéøî»|Ú­+˧LÁ)�•ž~ú¦uÍûìSö¬^…gñD;ÆÓmÛQÚÏ;;;Oÿ‰oôÇ)·X²“z¿¯'òDõ¼Œkw��ˆ;s†Þ>¿é~¿Ü“ƒ7òU|)P¨O5hxòßèO\Ôܼ
uì�G¼G77⢣p-P€ÓòNà Î�Š ö³ÏRë™–×ÕÒãe†7k«~UpÊ�ÿg[åXæ÷	ŸÑiè°ë¶=ê|ƒ‚عl¹õ{äÄ¢¯¾¤B�:Ö2§üÀW}ûüï7îÄGEñL¿Wi5p êòÓ°wIIN&êØ1z~òéuCóײX,¼úí$ÆuîÄŠ©SÈÊ̤rýúÖõµÎž8ÉØN)X¬(WR/“‘‘ÎàŸþÁó
bL‡öX,ÎGœfÐ�?Z‡‡¯Õiè0>éÒŸÊ•qpÌ…_Hc›í޹rѰKVNŸ†«Öÿì >Â\ eÿ¼QÇ;{{F¯Ìùßø74íÙ‹-š1 º޹œ¨Õ²%{ÖdŸÃüÓÏ|ó}ùuô(Êת�«V8¹ä¹£}u:ŒÉo¢oåŠx/N|t4Oú×aÐ?Þòµõ:tdË‚ùô(]’JO×ãÍ÷î߯±ÆœoYð~JŽ�øåOîTBLù<WfÏžmû\»v9Ž=Ù;8¤ÏˆŽq¸—óÐéii$œ=‹[¡B6ûéiiÿ^+|[õ¤&'s!.Ž|äΛ÷ºíIçÏ‘•™E~//›Å{	11d¤¥áîí}Û+î“ãâÈåâb3'˜“‹		\JJ­P!kÙˆCÞ¬)?†G�šœLVV–Íz‡¿ËÌÈ áìY
)‚ýߦD ûgFcÚ·cÒá#wµ> “G¦ýø#¹þºPe®íÚµ»î
h±XøùàïR““¹˜˜ˆ[¡B9^l“bÏã’7ßmÿN<++‹Ä˜Ü‹¹i¹ÌÌLÏžÅbgG�Â…­ßçÉo¼N77ž6œ„³gÉçé™ãçwÕ¥¤$2Ò¯X2ûw_õéM>OOº~8ê¶Ú§þÉùàaì÷SFzzvŸðö¾nð߽܀æ¯ô¡^‡Žw¼¿«çÆüžž7½žÜ9õƒ‡â÷Hw³ðãv(tãEb×Êïyó0ævÚéèä”ã]ÉÅ„ÖÌø‰…_E¯ñŸÝV{uŽNNxùøäøü톀Üyóæ®ºÑçv»Ÿûµn7Œº(€k�ë焯ºY{¯²wp°YXx•a,™ô-+ü‘Öƒ^l	Þ©[~þ7¸ÀÞˆ��Ý-C€½½ýMG€ÛêÇ7
ƒ°qÞ<¶…†òÙ–m·¬GîGÇ¿‹�½žçÜ©p
.ÂÁ?ÿ$íR
þ­Ÿ»«ýÝèÜø <Aàn|ùJ/Žmß~ÝóÍz¿B³WúÜuýÅÊ—'¸k·»®'33ƒË—.Ñû³ÏË@èQð˨‘lüïÜ랯޴ÝFÝý?ý›ßÓÓú�Ý
Ã0¸˜�@ëAƒxº}‡»®ÏLÃàµÕrÜ6àû)èfÈC ]>×Ýù)ˆ•aº	rX,råÊuÛåÕO‹åA7ARš11S1ûÝyx(ˆ˜P»ví.?è6ˆÈÃAA@DDÄÄDDDLLA@DDÄÄDDDLLA@DDÄÄDDDLLA@DDÄÄDDDLLA@DDÄÄDDDLLA@DDÄÄDDDLLA@DDÄÄDDDLLA@DDÄÄDDDLLA@DDÄÄDDDLLA@DDÄÄDDDLLA@DDÄÄDDDLLA@DDÄÄDDDLLA@DDÄÄDDDLLA@DDÄÄDDDLLA@DDÄÄDDDLLA@DDÄÄDDDLLA@DDÄÄDDDLLA@DDÄÄDDDLLA@DDÄÄDDDLLA@DDÄÄDDDLLA@DDÄÄD®±}ûv¶oß@ll,sæÌ±nûõ×_IHHxPM“»ÎÒ¥Kïy½©©©„‡‡“–––ãö¬¬,"""8{öì=߷Ƚ   r�ß~û�Y³fpâÄ	Þxã
ë¶þýûù š&wiëÖ­Œ3æ®ê:t(ãÇ� ::šÖ­[S @‚ƒƒ)X° Mš4!,,ÌZþ‹/¾ÀÛÛ›š5kR»vmÜÜÜxõÕWIII`Ô¨Q<óÌ3wÕ&‘»¥  rµjÕâÔ©Sºòùý÷ßiÕª©©©4lØGGGΞ=KXX‰‰‰¼ðÂ=z€1cÆðÉ'Ÿ0þ|bbb8uêaaaØÛÛ“˜˜ø€ß‰È_ä®ÅÆÆâççÇÂ…yê©§(^¼¸Í�—a|óÍ7øúúâåå…ŸŸ 00�3gÎXË~õÕWŒ7€;vкuk¾üòKÊ•+Ç Aƒ8sæÍ›7ÇËËoooÚ´ic}mXX­[·¦xñâøúúZïìæÏŸ�¯¯/žžž”.]šÉ“'ßò}íß¿ŸfÍšÙ<·eËüýý)^¼8¯¿þ:W®\`ýúõtîÜ™qãÆQ¦L†
FXXMš4¡X±b/^œÞ½{séÒ%k]Õ«WgîܹԨQƒêÕ«3bľøâ›ý½öÚkLŸ>ý–m}ÔDEEÑ¢EŠ/N±bÅèÖ­›ÍÅ100�Ÿþ™ÚµkS¼xqLzz:§OŸ¦yóæÖ×¾ôÒK\¸pÁúÚˆˆÚ·oOñâÅñööfРA6û5j%K–¤B…
„††ZŸOMMeÈ�!”+WŽÒ¥K3hÐ ._¾lÝFVVO<ñ¿þú+‰‰‰L›6
777ìííâ	VºIDATéÚµ+Íš5#55•Q£F1~üxj×®m­ÃÃÃÏ?ÿooï{{@Eî‚‚€ÜµŒŒvïÞÍÒ¥KÙ²eË—/g̘18p€iÓ¦1nÜ8f̘Á¹sç˜;w®õä¹ÿ~›¹ÕèèhëðûÅ‹	

%66–;vðá‡2räHŠ-Jxx82È>‰7jÔˆgŸ}–S§N1oÞ<̶mÛÈÊÊ¢{÷îŒ7Ž˜˜vîÜI�:unù¾RRR¬�åªéÓ§³dÉöî݈
¬ÃÄ.\`Μ9†Áž={2dÎÎÎŒ=šS§N±oß>=z´µ®={ö0}út.\ÈÚµkiРŸ|ò	YYYœ?žÉ“'Ó¨Q£;ýhZ>œ“'Orøða,Æ
³nß·oßÿ=Ë–-cÏž=¬]»–	&�+W.Þ{ï=Nž<É¡C‡HOOç½÷Þ²ûbóæÍñññáÈ‘#„‡‡Ó©S'k½›7o¦P¡B;vŒ#Fн{w233xýõ×	gûöí8p€ððp>øàëk¯Ž@vð«[·....9¾¿Ý»wséÒ¥Çò³“Ç�‚€Ü3ï½÷¹sç¦B…
øûû³k×.&OžÌ»ï¾K•*U(]ºômß¹ºº2bÄòæÍ‹««+öööÖ€ƒƒƒõnkéÒ¥äÍ›—†
�ƒƒÍ›7gþüùØÙÙ±}ûvΟ?�››•*Uº£÷8dÈ
(@�øÏþÃÌ™3­Û¼¼¼2d®®®¸ººR´hQŠ/΂øõ×_)T¨ëÖ­³©oذa)R„¼yó„«««õ.uÚ´i4nÜø±¼{ôòò¢lÙ²,^¼˜™3gR°`ÁëŽÍ;#››îîî¼óÎ;Öc]¨P!Ê”)âE‹˜5kžžž¬]»€M›6ËØ±cqqq!W®\6wä¥K—¦gÏž8::Ò©S'’““‰ŒŒ$##ƒ)S¦ðÚk¯OLL]ºtá÷ß·¾vþüù<ûì³$''ãááqÃ÷—œœŒ½½½5ð¼üòËT­Z•ªU«²bÅŠ»>†"÷Š‚€Ü3žžžÖ¿]\\¬Ãà‘‘‘øøøÜq�vvuÓ‘#GR©R%Z·n�··7}ô�=|îÜ9úöíký/22’‚bggGhh([·n¥L™2Ô¬Y“�7ÞQ{
.lý»D‰DEEY*TȦìÚµk©R¥
kÖ¬!99™¬¬,âããoX@Ÿ>}˜4ißÿ=½{÷¾£v>ì¶mÛFÅŠY¾|9IIIdeegSæFÇzóæÍTªT‰•+W’””„aÖãI±bŰ··Ïq¿^^^6�sçÎMJJ
ÑÑѤ§§óÁXûÏäÉ“­�166–°°0k¨ðòò²™ÒÊi?™™™ÄÄÄXŸ1b¿üò©©©Z# ‡ÝyüùøøFHHÈuÛrçÎm3o�«««õ±Åb±)ïææÆçŸÎçŸÎúõëiÔ¨Ï<ó¥K—ÆÃÃ%K–äØ†Úµk³páB.]ºÄÈ‘#éÛ·/{öìùÇï%22’êÕ«pòäIŠ+vöNš4‰Áƒ[y0uêTë�ë�^Ó­[7†ÊŒ3¸rå
Mš4ùÇm|L™2…W^y…÷߀9sæð믿ڔ‰ŒŒ´Ž"]{¬¿ÿþ{^}õU†
À¬Y³¬wî>>>DDD�žžŽ££ãm·ÇÛÛggg&L˜À“O>yÝö…Ò¼ysk(mÖ¬íÛ·'**꺛ŒŒ|}})Z´(³fͲ®Q(Q¢NNN·Ý.‘ƒFä¾ëׯ£G�fóæÍ¤¦¦²sçN"""ðóócÒ¤I$$$°|ùr,XpÓºæÌ™ÃñãÇÉÈÈÀÎÎ{{{rçÎMãÆ�ÉÈÈ`ذa$&&’˜˜ÈŠ+صkÉÉÉ̘1ƒ„„ÃÀÞÞþ†s»·2vìX"##‰ŒŒdäÈ‘tíÚõ†e=<úè#Μ9ÃéÓ§5j”õX{xx°zõjÎ�;ÇÑ£G­‹K›…œqqqÄÇdzråÊ[¶ÇÞÞž¾}ûÒ§OŽ?Njj*ûöícÞ¼y€í´@óæÍ			¡qãÆ,[¶Œ3gΰmÛ6úõëÇܹs±··ç믿æ½÷Þã“O>áСCœ={–µk×rþüùë Èƒôxžeä_•+W.‚‚‚lž«\¹²õN©cÇŽŒ=š7Þxƒ
*0xð`ëjì	&pôèQüýýY´h|ðeË–²ïþkÕªeSodd$�:uâ‰'žàõ×_çǤL™2899±fÍΜ9C@@µk׿Ûo¿%W®\X,/^L�:u¨R¥
à§Ÿ~Êñ½”.]š'žx€|ùòáïïoÝV·n]Þzë-zôèAHHÍ›7gàÀ�@öÅéêHÁUÇÇÑÑ‘:uê0tèPF�MÍš5­Ûs¼;|ñÅIOO§G�·<ö�ªÁƒS¨P!êÖ­Ë Aƒøàƒ°)Ó·o_ºwïNãÆ�iݺ5ýû÷àí·ßÆÍÍ�€€ÌÈ‘#­Ÿ“��K–,!%%…ºuëÀúõë�ìáúªU«Úìãé§Ÿ¶†Â±cÇÒ¬Y3:wîLÅŠyõÕWIMM%55•
6Ø,ü³X,Ì™3‡.]ºðî»ïR§N^}õU¼½½iÞ¼9­ZµbÉ’%lذ�-ZȘ1c?~¼õ×.>>>øúúÞ‡#,rû,€1çÂÅݹÚåseöìٶϵk—ã­ˆÅb1ÃøWÚ%7÷î»ïrüøñë†Êï”Åbyäú�»»;6l B…
º)¬Y³†Ÿ~ú‰©S§>è¦Ü•Út]x<åt]Б‡Dxx8ýû÷gÓ¦Müù矺9ò?
4 Aƒº"÷�‚€ÈC¢`Á‚¼ùæ›T¨PÁæf´páÂ;þ¥‰ˆü3
"	WWWêׯÿ ›ñP¨[·îƒn‚ˆih± ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰9´Ëçú Û!÷Iûöíÿþ”q£²‹åþ6F	êº.˜‰€aÜðÚ "�¸za×ùàñ¤~ �s?ÐÔ€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚€ˆˆˆ‰)ˆˆˆ˜˜‚ÀCløðá|úé§dßûöí#==ý_ÛßÏ?ÿÌ+¯¼r×õœ;wŽÒ¥K߃ÉÃhÏž=Ô­[÷¶Ë·k׎¥K—ÞÇÉ¿á£�>bäÈ‘7Ü~ðàA._¾|ÇõOœ8‘·Þzë†Û'OžŒŸŸ%K–äÌ™3”)Sƺ­mÛ¶,[¶ìŽ÷ý0PxˆÅÅÅ‘˜˜ø@öýôÓOý¯í¯|ùò4lØð®ëÉÌÌ$<<üî$¥´´4"##o»|tt4)))÷±EòoHHH >>þ†Û›6mʱcÇî¸þ¤¤$bccsÜ–’’B¿~ýøòË/9|ø0ùòå£[·nÖí�CsxÐ
x…‡‡³nÝ:.\¸@Íš5ñ÷÷²/2‹/&""‚Ê•+d}ÍêÕ«yê©§Ø·o‡"00�J•*Y·ïÝ»—õë×P±bE4h`Ýö矲{÷nŠ+F‹-ptṯ]†a°råJŽ9BÅŠozá4ƒåË—sìØ1Ê–-KãÆ�±X,lذNŸ>ÍŽ;èÑ£...6íÉÈÈ`Ù²exzzR³fMŠ-Ša¬^½šÃ‡S¾|y5jd}ÍòåË©Q£;vìàØ±ctëÖ�5kÖHhh(/^¤cÇŽ¸¸¸°`Á¢££iÙ²%ÅŠ `Á‚deepùòe–/_N£F�X¼x1.\ U«V,X€+W®ðçŸrðàAòæÍKÓ¦M)T¨Ð­?Xy .\H£F�È�;7çÎ�cãÆ�<óÌ3888pöìYŽ?ÎÓO?
@bb"Ë—/'!!�
P®\9›ºâââX²d	´jՊܹsßtßk×®åðáÃR¡B.^¼Èºuë¨W¯Ë–-ÃÙÙ™–-[rþüy–-[FJJ
AAAÖ}oݺOOOJ•*À‚¨V­ÅŠÃ0,X@‹-ppp`ëÖ­lÙ²ªV­J�:u¬mÙµk›7oÆËË‹gžy'''¶oߎ»»;‰‰‰lÚ´‰N�:áááqoþcâèÑ£¬^½šR¥JѤI¶lÙBjj*«W¯&,,???òçÏÏ–-[¨U«¡¡¡dddбcGìííùý÷߉��§uëÖ· ++‹��Æ�Gll,¯¼ò
¯½öšõuýû÷§sçÎÌœ9“cÇŽÀÖ­[X²d	!!!œžþyÞyçâââxûí·éÔ©“uÛðáÃéÒ¥ãÆ�#""‚´´4›×�ÈÈHêׯςˆŠŠ¢C‡têÔ‰
6pöìY¨R¥
+W®$,,Œ:uêXË/^¼˜�?þȾ µnÝš¯¿þÚÚæàààÀ´iÓhß¾=QQQœ={–O>ùÄÚ–ÿüç?tëÖ�¸¸8æÍ›‡¿¿?©©©|òÉ'tïÞ�wß}—Ó§OsñâÅ{rÜ›6mbðàÁDFFÒ¯_?ëTÁ‘#GHKKcïÞ½lÞ¼™sçÎqüøq:wîL�=8tè“&M¢C‡¼ðÂlÚ´‰uëÖQ³fÍ[N'¤¦¦²k×.Ã`óæÍ>|˜øøxÚ´i“cù¨¨(ªU«Æ¼yóˆ��çù矷ö‘‡�!ÙÒÓÓ
OOOcÑ¢E×m›={¶Q¦L#==Ý0ÃˆŠŠ2œ���}ûö†a+V4†j-?pà@càÀ�†aÆ€Œ\WçÉ“'
gggãÔ©S†aFff¦Q¡BcÊ”)†aF¿~ýŒaƆa_ýµÑ¨Q#ëkSSS�Â…û÷ï¿®Þ?þøÃððð0’““
Ã0ŒäädÃÝÝÝXµj•a†Ñ°aC㥗^ºé±È—/Ÿµ]†a“'O6êׯo}œ––f+VÌØ¹s§a†áããc|øá‡ÖíÇ�7cïÞ½Ö68::_|ñ…µL�:uŒY³f†aß|ó�ѺukÃ0ãôéÓ`¬_¿ÞZ¶B…
Æ’%Krlë;ï¼c<Ø0ŒìÏÅb±Üô½™
`Ìž=Ûæ?Ã0Èé¿ûy>xóÍ7�×_Ý0ÃhÞ¼¹Ñ¶m[cĈ†ad÷ɹsç†a!!!ÆW_}e}Ýš5kŒ'žxÂ0ÃØ²e‹M¿ÊÊÊ2j×®m|÷Ýw9î3  À¦¯Ïœ9Ó¨P¡‚a†qäÈ0¶nÝjÝÞ½{w£oß¾ÖÇ3fÌ0|||Ã0ŒuëÖåÊ•3Ã0&Nœh´mÛÖ¨^½ºa†ñé§ŸZ÷Ó±cGcäȑ׵eïÞ½†§§§‘””d}®eË–ÆÄ‰­¯kÑ¢Å�à]zXúÁ�xë­·???#++Ë0ÃX¹r¥Q¦LëöâÅ‹[û„aƶmÛ;;;#<<Ü0ȉ‰1cúôéÖ2מSF�}ÃsbXX˜áââb}|æÌÃÎÎÎú¸N�:Æÿû_Ã0£W¯^ÖsÑÕ׿ɓÇHKK»ã÷~¯åÔ45p�'N���@³fͮ۶mÛ66lˆƒCö!+R¤¾¾¾ìÞ½›Ê•+Ø,b*Uª6l M›6<ûì³:tˆ–-[Ò¾}{
.Ì®]»([¶,%J”ÀÎÎŽF�±sçNzôèa³ÿ�7gsn°™‚¸ÚÖ€€\]]puu¥nݺìÚµË:�P¯^½tl6lØ@RR’Íþ³²²8pà~~~9Öéä䄯¯¯µ
W§®=FQQQ9îÏÎÎÎf8õÚ²çÎ�ã?ÿù›6m"==�äädþÑû‘_pp0C†!==�]»v±páBÀÛo¿ÍæÍ›™3g�}ççââ¶mۀ쩠°°0ëÈ•‡‡‡µ_Y,5jÄöíÛéÕ«WŽû½v®qãÆtîÜÙ:§ëäädÓ'·mÛÆ¨Q£lÊwéÒ…„„üýý‰ŒŒ$22’U«VÑ·o_^~ùeâââXµjÏ?ÿ<;v¤k×®lÙ²…-Zо}{ÜÝÝÙ¼y3ŽŽŽ0ÀZDD°>þ§ßK3	°No–*Uê–k˜¼¼¼ðññ±þíââB­Zµ¬Ûovþ¹S6l H‘"6çÉ+W®pêÔ)Ê–-{O÷u/)\ÃÞÞþÚDlÃÎÎÎ:‡}•aØÙý5»’+W.ëß‹ÅZO`` 'Nœ`ñâÅÌ™3‡aÆqàÀììì®ÛWVV–M�×ÖW³fM›€Ð·o_›Õ«7«÷ê¼äí²X,T«VÍfeß¾}­ó¥ÎÎÎ6¯¹𮲳³³y.§czí6{{{›ý_}Oýû÷§páÂlݺ•yõµvvväÊ•‹zõê±|ùr6mÚÄŒ3

bÙ²eüñÇ|ÿý÷<÷Üs;vŒ…ò믿òþûïsèÐ!Š-Jß¾}möéééiýûïß!ùË�ί7ò÷ó�Åb±yîvêø§,Íš5³®u�ìódÑ¢Eïé~î5­¸FÉ’%)T¨óçÏ¿n[�:uXµjW®\²“üþýû©^½ú-ëÍÊÊÂÃî]»²`ÁÜÝÝÙ¹s'Õ«W',,Œ“'OÙë–/_n“Z¯
dûöíÔ¨QƒÚµk[ÿsww¿®¬¿¿?6làÂ…@öŠÛ
6ØÜùÜŠ‹‹111×í¿Zµj6û¿º€ïßtäÈÈ“'�½C~...Ô®]›áÇŒÅb¡^½zŒ1‚àà` ûDZ¿~}Ž=jÓÏjÖ¬i
†ñññìØ±Èþn-[¶ì¦}{ùòåÖ¿CCC©T©Ò
úûûj}¼páBÊ–-Kþüù¬ëtž|òIœ��	f̘1/^œ"EŠXÛT¸pazõêŲe˸téGŽ¡^½z=z”bÅŠÙ¼·’%KÞùAëÔ-00�}ûö]×w¯]Œý0ÒˆÀ5ìíí™û,Ï?ÿ<Õ«WgàÀ�téÒ…Å‹S£F
š4iBrr2kÖ¬!44ô_ÿÝ~ûöíyã�7ضmû÷ïÿW÷-w'88ØæÂÌo¿ýÆøñã­e>ûì3š7oÎÞ½{)W®œ;wŽ•+WÙ¿0yç�w¨Zµ*Û·oÇÉɉ_|ñ†ûŒŠŠ¢}ûö-Z”3fX%ædèСÒ¡C<==ùí·ßlÊóÖ[oY§‚ƒƒéÚµ+ýû÷·–iÓ¦
®®®”*UŠ­[·Rºti|}}É�;7ï¼óþþþ´mÛÈžŠxõÕW­Ó
rgZµjÅË/¿L­ZµèÓ§ùòå{ íxÿý÷iܸ1!!!T¯^�³gϲ}ûö‡þvµ?dff²~ýzN�:…‡‡O?ý´õºð0È©(ˆ<æå€Ü;ê9÷­11S11S11S11S11S11S11S11S11S11S11S11S11S11S11S11S11S11S11S11S11S11S11s°X,º"ò�Ðù@@ýÀLðù´çÌ™c<¨ÆˆÈƒ¥ó�€ú�Ùhj@DDÄÄDDDLLA@DDÄÄþ±æ�T´�ÑÂIEND®B`‚urwid-urwid-69907e4/docs/manual/images/introduction.png000066400000000000000000000617451526073734300232150ustar00rootroot00000000000000‰PNG


IHDR°ØŠž¶©sBIT|dˆ	pHYs
×
×B(›xtEXtSoftwarewww.inkscape.org›î< IDATxœìÝwTWÀáßR¤ Ø»Ø(Š ¢bïŠ=jì%±7Œ%Š5–Ø1Qc¢Iì]ÁÞAA”"bEQAÀBïì~ð¹JlˆÀ²zŸs<Ç��½÷Ývß½eîH‚ ‚ dÔd2‘ÃAå!‘HPQt‚ ‚�"�	‚ JI$0AA)‰&‚ (%‘ÀA¥$˜ ‚ ”DA”’H`‚ ‚R	LAPJ"�	‚ JI$0AA)‰&‚ (%‘ÀA¥$˜ ‚ ”DA”’H`‚ ‚R	LAPJ"�	‚ JI$0AA)‰&‚ (%‘ÀA¥$˜ ‚ ”Ô€ $‰D¢è”šL&St‚ðA"�	_½(¯CŠA)™ØuWt‚ðQ¢QAPJ"�	‚ JI$0AA)‰&‚ (%‘ÀA¥$˜ ‚ ”DA”’H`‚ ‚R	LþãÈ/.øø¿³="ê9‡ÏzòüUÜG_õŸÀÛŸUgÀ�Pn…>ü¬×·N$0Aø�±ó×0ð§ÅÄ'&çØ¾~ça~p^ɽ°Ç}}pèCöž¸ðYunw=Ã᳞Ÿ« |ËD„÷¨Y©<'/ùÈK¥2N_¾†©qÉwö}O–T*ܦICVΓc©TöNBü\z½Löá²3³²HIMû¢z¡¨k!
Â{tkm�Ûy/úth€—ÿM,jWçVH˜|Ÿ­‡O±úŸ}èéòàq³GdTß.¹à…ëÙËüõË4†ÍZFéR%8qчäÔ4¬jWgë²Y¨«çþ£wÞÛŸ5ÿî#úE,‰É),sú�N¶¬þgûOz�œšJ£’l˜?…Ц&Œ[°	—®!ABk;+~�öcþ$AP0Ñ„÷hikÉõà{ò–ÍÁ3—èѦ©üù„¤d6îvãøŸËpß¶†]«æ°tÓN¤YR223HÏÈäúÍ{xíþ�€Ãq7ì	ž~A¹Ž#úE,#g/gêоxï]Ïæ_~bÌüÕ<�Šaÿ)ö¿À¡ßqíà&l-j3a¡™™‡<ÄcÛZ»Ž�˯Ã#E‚H`‚ðÅŠÑÒÆ’“—|ÈÈÌÂóÚ
ZÙZÉŸ×ÓÑÆc»a�#ùkß1v»@RJ*Ré»·éÔ¢1ZšhijP«Jy^Æ&ä:ŽÀ»÷)UÜ€æ�̰17£JyS|ƒîrùz›Û`\ÒU9¶ÃçÆmÒÒ3h߬úºÚhkiÒÑÁÏk¹Oœ‚ D„pü7¢»�?M¬êæèö{—@óï'°vëÒÒ3èØÜï&°·ÇÍTTT޻χ¤¤¦a¨¯›c›�®É©iêëÉ·ëéh£ª¢"¿‡—q‰âòçtµµä‰M¾"�	Â84² àv(ÛÜÎÐý­îC/ÿ`ÔÕÔØ³f.c8¢ª’ýQJÏç$aVµ"AwȧîÇ%&át‡:Õ+S»Z%Îy]—ï{ÞÛ�ÊåË ©Q€K×nÈŸóð
IJvõ|�MMLâ„PWW£¥­%ç½ýi²dzŽçš6¬Ï”%¿3cÅ&ŠS'äác´µ4yŸ˜çúöŸºˆoÐùãš•+°Ôi#út¦å Étr°åÄÅ«ŒØ�êË2¼WGŸõ¤ÃÈéÔ¨TŽSž¾üýVœ¾AwøÞi™YÄ&$2¼WÇ<Ç&E‘�‰Û†_+‰DòÙwdö¼…yͪhijðìùKbã©U¥·C©\¾º:¤¤¦qÙÿ&ÚššØš×&èÞ*šš�%•ó2–ZU*pçA8%
õ1*aðÎã×î‡GõüeŽmzºÚÔ«QȾ¶,ìI$õkT¡‚©‰|ŸŒŒL¼o‘–‘A£zfèëjðƒóJ[Ô¡FåòX׫…ºšêg»îˆï¡¨’H$"�	_·¼$°¯ÁÎ+±³¬Ëàîíò\†H`BQ&‘HĘ |�ì,ëR­¢©¢Ã„%ÆÀá+ô%-/AP¢&‚ (%‘ÀA¥$˜ ‚ ”DA”’H`‚ ‚R	LAPJ"�	‚ JI$0AA)‰¥¤„¯šD"QtJM|7E•D"+q_7ñ,_/Ñ…(‚ (%‘ÀA¥$˜ ‚ ”DA”’H`‚ ‚R	LAPJ"�	E–§§§¢C¾�8‡BA	L(rŽ=еµ5“'O&..NÑá˜yóæ1oÞsçÎÅ××E‡%ä‘��¾¾¾Ì�;—ùóç‹D&ä;‘À…{_âêܹ³¢ÃòIçÎ�E"
„H`‚ˆÄõm‰LÈo"�	…N$®o›HdB~	L(4"q	o‰LøR"�	N$.ácD"òJ$0¡ÀˆÄ%|‘È„Ï%˜�ïD⾄HdBn‰&䑸„ü$™ð)"�	_L$.¡ ‰D&|ˆH`Bž‰Ä%&‘È„ÿ	Løl"q	Š$™ðšH`ÂEFFrøðaùc‘¸„¢äS‰ìðáÃDFF*0B¡ I™L&StBÔ¦Mž?΂X°`sçÎIë………±uëV.\¸@‹-4h•+WVpdÊíèѣ̟?gggœ��)UªgΜQpdBA�H$"�	ï·~ýzœœœHKK£B…
¬[·N$®|’ššŠ¹¹9÷îÝ F�¢©©©àȾG�eüøñ„‡‡£¡¡ÁŠ+3fŒ¢Ãò™D"]ˆÂ»¢££Y¼x1)))H¥R

éÔ©“¢Ãújhjjâää„¶¶6ÚÚÚ899‰ä•�:uê„¡¡!R©”””/^Ltt´¢Ã
€š¢Šž´´4–,Y³gÏˆŠŠ"66–'OžP¾|yE‡öÕ9r$ÿþû¯üÿBþyòä	
4 U«V˜˜˜PºtiÒÒÒ–PD¢ (ˆ��666
ŽD”�A”’A”–H`‚ ‚R	LAPJ"�	‚ J高FÍŠ•+ñòò&0ПĄE‡ôMÐÕÓÃÜÜ;;[œ¦NÅØØX¡ñDGG³rå
¼½½ð÷$!!Q¡ñ|-ôôt±´4ÇÖÖŽ©S�ŠÄy^µr>>^øù/ÎsnèëëbeiŽ��SŠÀyÌJ?q×®]L˜0	Û–]1·v š™†%ŒÖ7!öe¡·ôuÇû¼..kèׯŸBbÙµk'M cW[ìÌ©oQ�RF†
‰åkó<&–¡xºrÜÍ›µk\zž'OOG[Ú¶¨‹µU5LŒ‹²‰Š‰Ã×/”Ón²÷°7«×¬SØyÌJ?�~×®]8ý4“Éó7QÍÌBÑá|ÓBo°zî(Vüº¤Ð?»víbÆL'ÖnšL}‹j…Z÷·æF@(G­fé’
9Ï?ÏrbÏæñX[V-Ôº¿6¾þ÷é;|¿,.üó˜_”:�EGGS§N=¦/Û&’Wz;€eÓThÝÑÑÑÔ­W‡MÛ¦‹äUHn„2jà2nêy®_¯6Gv9‰ä•O|ýïÓ¥ß
nÝRÊîD¥¾lÅʕضì*’WRÍÌÛ–]Y±re¡Õ¹rå
:vµÉ«Õ·¨FÇ®¶¬\¹¢Ðê\µr}mEòÊGÖ–Uéãh˪B<�ùMi˜——7æÖŠCøsk¼¼¼­>oo/ìÌ­>!›½ƒ9ÞÞ^…VŸ��m[Ô-´ú¾m[ÔÅǧðÎc~SÚè/Z_EP53ý­>ÿ@ÑúR€úÕð÷,´úüü±¶ç9¿Y[UïÏc~SÚiô‰		�mþàáî ¢ªŠžA	jÔ¶BCSK¾�×…#4²o�šºúg×/ø:ÅKabZñóƒÿ�k—Ï •fѨiûÛãc_pãÚ%jÖ³ÆÈ¤l®Ë{ùüO…R¯�ýG÷ó¹x‹F9ŽÉ—2,aT¨—1$$$~r¶all">—oÒ®“­|Ûý�§Ü¹FÇ®M�H$<~EÄÓçT«QŽ»·a×´þ;eù_»K‰’úT¬\&Ï1gef¡ª¦úÎv�s~˜Õ­Œ±Iñ<—ý¥²²¤¨¨HäÇäCJêe
ññ‰¹šmø*6‰SçÞû\G;TT>þ¾ò"33µÿœÏW±I¸{Ó½s£|¯/?™(õeJÛû”Ëç\Ùúû.�9Äùc{øÇe.?ôlHÀUwù>;7-#=-%OåÛ¿™ ë—ó%Ö¿VÏbÉôÁDE<ʱý쑬tþ�;7®~Vy¡·Ù³ùÓýÚë—N%!îÕg•­ŒÔÕT?j1ѱòmëVîaêØµÜ

“oûçÏ£\8{�Ga‘ìøçä{Ëú÷¯cxzäý«ÿõ{Ìž¶ñ½Ï-™ÿ/Á7ä¹ìüЫãtRR”÷Ö#ãùþ‡uì>èõÎ?©TšïõMŸ·ƒSçßý{xð0Š‘“þÈ÷ú„œ”¶–õ­›3nÖjùãÓ®ÛX»`ëvz¢«oÈo»^7éß}	ñÉ|×íg:4ŸDk»±<
‹”—éãu‡F?âØÆ‰Es¶ÐÎ~¸pbk¶¹óÛ./ÒRßüÂÏÊÌÈÑu’™™ùNWʃ{Alÿc	k¶¹óÇ�k´èØ—µ³[‚·º0tÂ|Vo=ÏÌe[¹qíÒgó¢ I3s®]ÍN`ÏûѬ…%
m̸FbB2¾Þ·hШêH¥222²¿P9o¡R•2¸ûnä¯í³¹ü€ôôÆ�XΜEÃ9幎ªÕËò™LÆ�€PÖ.ßÍI�µx\ûƒ®=š2m‚eLK1uÖì›[°hù�¹ŽýVP×|náqõÎ\þ�qSú°Áåê´é`ƒëþ‹@öXŸ§Gm;ذláV^¾Là¸û.^ßÄËñü¾z_vìi¼}éçëÇ¿®�ªª
û�/¥d)å]Ý"=#Çï—çø·pùú÷²gÛž‹ò}ÿÝíÁ÷}šqáR0ë6� äša¿Ó¶¥9?NÉþñ™‘™Åù‹7¹éµ’§Áðâe"GO]gìˆvØ6¬ŽËÒ¡tlc™ëø&Ìø›ÌÌ,‚~@̳'X4røPPصìŠ×ù#\q?Jã]äO?¼U7ïdw9V7³ä¦ÿnúyaní€AñRhhjåx]nø]9O)£2\<½ŸCÛבœϽ`?’ã¨ß°›×ÌaÓÊ™¤$%0rê’Ï*»¨°lP“Ga‘$'§âqÞŸf--QWW£¡�×|nã㌽û³Z½=ƒèìØUU*U)#ïZ
CMM•–màØÛ�ÿwyœó£Œi)íóà�u‡HJLÁÏ÷.ÉI©yнNý*lÛ?ŸËÙôÛ!<ÝHˆÏþ‘Ò«_KÜ^D&“qôÐ%Z¶µFGW‹+žAôìÛMÍbèèjѽws._¼‘§ú•�ªª
Cú9äø×¦Eö„œïz4á¬Gñ	)\ö¹‹ž®&Væ•9~Æ�reK²yûy–¯s#--ƒ³7ÈÌÌ [GkôtµPWWżnEž¿ÈûD¥—‚6 %êèп—=ç.|ç	R©ŒÞÝP¯v7ª�»ç-LK§uózèe�¯ÿ}’’ÓhÙ´.¦oaüô-$§¤ã²tè—>¥òM%0™LFxØÝwÆ•ê5°gùæ“”¯T·]ÓÇ–èÈìnÃo®P×ÒÑ#++“¬¬,ù¶´Ô¦�hÏö�‹ILˆÃ®eWd2Èd¨ª©Ó¼]O.ž>ˆûÉ}4o߉ʇy“V]¹|Þ�ûw1*]}ÃòçÒÓRÑÕ3Ìñz½ìþîÔ”$tõÞüb.i”s†\ŽØ{ÆÉ’“âÑÒÑCG×]Š—0æÇi¿"AB¯!“˜º`#êêêløÕ‰™?tFWnQSWÅʺ^oððAõ-ªÐÌÁ¿kwñõ¹EÓ÷$°¤¤TßÃ*]&ûœ¨¨HPSS“ÿ*¦®†šzöØiB|zúÚèè o ƒ±I	,û!ÏÇíέ‡´°Í�=ÐÖÑÄÞÁB^–…U
4µ4ðó½Ë¡½îôú®�=.gXü͘­ž¾oÿl{ûoâukók¡ª¢‚c'ëÿlfŸo}=-:µµb¿›7Ûö\dØ€–ÄÆ%c¨¯�¡�†:Tª`ŒËÒadfe§r¦%åå«©©"•æý3�œ’F‰âoþ¦^�©%'§a ¯�ªªÊ;Ï”6y36­­­�Déé™Ì�Þ››& ªªÂˆ	iÖi®R~Fóâ›J`®»6�”‹íÿ[1¯�;º“;A¾ô2‰å[NQʤ,wo^àN�/éiÙ¿œ}=¨T­NŽi÷÷ïò"&’_6¸ÒkðD4µ²ú32³h[uîÏÕ‹'¸â~”–�ú~4¾jµ,HJˆãà¶uït5–¯TƒÈ'a<{ú0»üô4‚®{R©Zª™Yä˜]týM7Ÿ†–6¯þ?n'•fÉ»OßV©Z¢"ÃiÝuíº¦Ž¥wƒ¯£¥­Ëæ5³ÑÐÒfè„¬ÙæÎƒ»AïTMšÕgËGhl_O>�ºiK®xñ"&Žšfï^Qß²Ýߌox_¾™½Ý¢2™Œ[ÿŸôàs%˜¤Äì­fu*ñ8<š¾ß·¡ÿàv4´1#Ð?mMÔÔTå¿ês븛Í[Zá²i*ßí@rRjޤӻ+þýë/žÇac—}±o­Ú•¸pöš|Ÿg¯cV§2Ú:DG½àé“yk@UU•¬ÌüŸ­W”íß‚û.ávâzg_jbQ¯áOžóãÐ6ü8´
Væ•	ºþÞ	oSSS%+ëóŽWý:9vÚOþøø?ÌëTĬfYŸ<çNÈS ;9�u¼nöße@ÐC^¼Ìnù¹{S±¼Å
u?}úÚ¬Y<„ÀK˹êJTÌ×9†ù__õ,ÄëWÎ2{lw²23xñˆbÅ4˜¹l«<ɼVÛ¢1?�îÆ½`?ÒRSHMIÂܺ9·® ©¥�Ó°6Ô¬ÛŸ‹'˜»zOŽ×Ö¨ÝUU~ûeúÅKòà^ú†%Iˆ{‰‘IY*V5CM­šÚ”­ðé1íZvåèÞMŒûyMŽí%�Mé3t
ÓGv¤IË®\¿r–V�ûS£ŽUkÕçÒéƒLÙÓŠ„…ÜÄÀ°�ìÛ±xú`b‰zúè½×|5míˆûÉ}LÐŒ�[qõÒ)z™„DE…êµ­X1{6ÍÚLÃ&mÞiá)‹&ÍÌùÅùo–®~3Ó³rUS¢Ÿ½Ä¦ÉûWy˜½`ßuû™!O	
y‚ÆÿgóI$&ýô}ºÌ²aM´µ5ÑÔ,F1
uºölÎáýth>�f-¬8{ò*§õE"‘P®‚1W<ƒøuÑ6~š=ð�ú–ÿ²�?×’?îìØ”6í1ä»,[¸•¸W‰D<�!öÕ›.¬î½XþË6ÆLì%oN�9€¡ß-àAhéé<|Á΃hß©1Ó'þFÓ–<}‚iÙ7×SV¨hÂè¡KY±n"¥ßju(“´ôºÌ{g»ËÒ¡Ô¯SûÚŒ˜¸‘&65)Y"»•:b`Kv𤾽-›ÕåÐÑ«¬þeð'ëª\ј9‹÷ ¡¡NûV9[ðqñÉïÄñ÷ocX6o]û/ãFð#^¼LàYt,k—Ũ”>ó¦÷¦iGgú8Úqò\ß÷iJ›š¸{ÞB_OûÎ4³3c¿›7·:ÐȪ½†¬Â±£5~7ÂèÞ©¥�¿�;1(íb¾‰„C^Q|>:ò±¼PEU…â%Œ)]®rŽñ¯[�ÞÔ¬ÛUU522Ò¹ä‹4+‹šu¢©¥�·û1ŽØÂèé+xò0„Zõ¬Ñ3ȾÈôÉÃtõ0,iLFF:·ü¯ QQ¡Ž…-�†`XÂH~¡µË¢	˜Õ·¡M×ï�õ^°e+VEG×€øØ—Ä<{LÕZÙË#=
¦xIŠg'¤ð°»<}B¥ju(S®²¼™TJÈí’“â©R£>/b"¨\=ûK9òI�BoQ¹F=$	ªjj”4*Ã� _ªÖ2G]½2™Œ»A¾Äǽ¤¶¹mŽ©ôI‰qÜ
ðÁ xIª›Y~´ »�I¡uaH$îGúôŽÿçãLÝúUÐÑ}“ÈïÝ	GO_›2¦ÙÇ8>.‰'áQÔ®—ÝÕœœ”Š�W0eËQ¬˜:ZÚ”*e@ø£(uIˆO¢\,ª
  tªª*Èd2®_½CllÖ6µsL¥
%11…ÆöõrÄv# ””äœãd¦e�(_Ñ„ØW	øzß´¬uêWÁÇ+˜ÖµPSÏîÎjÖ`»Ý~¡\ù7]Þ±±‰\¿z}},Ô@]ýÍïU?ß;¼z•ÛãGQT­QMÍbÄDÇr# k›Úè¿g*ùÛªšt/Ôó,{¹÷“û%&¥rÍÿþ{Ÿ³¬_ýì¯wBž¢§«EÙ2oºé¥R—®Ü&>!…fvfÜ÷íÇÉ)ix\¾E�ª¦T­lòÉ8¬­ª¡£­AÌóx®øÞ£TI=¬-«¡®þæBè[wŸpûÞSêÕ®@�ªÙ?/\
fꜭÚæD@ÐC[×Àø­»_Å&áé}c#}YUû¬©ô’}”²ËQ©W£×Ó×gÝ®+zï¯×	lÁºyz}TD8w‚®²mÃ/ü¶ëò;-¿¯QìËÆ÷kLB||¡Ô§¯¯ÇÙ+ërﯮm¦2tTêYTc϶3„Þ{Ìß»�5†›�÷9yì
÷n‡³iÛ¬B«÷yL,­�'>¾pV]10ÐãžïêoöÞ_¯˜Ÿû²|-7*&ŽÖ“‰‹S¾›+õjôææ–„Þ~ÿ’1ùŨt9,mZäùõÏ£žpéô!¦ÌßðM$/Ⱦ¥Š¹yî§)KKsn„Z}oûuíxî?äç-hjcÝŸN…ƒ·×MÂF±xÕ˜B­÷F@(––…·ˆ²•¥9¾~Š9ÏEAiCºvh˜ïåúú…bUˆç1¿)í˜��-�¾î4lÒ¦Àê¨ZË\Þ•—u,í¨ci—�}�¾îØÙÙ~zÇ|bkk‡§{ |:{aªU»3ç
)ôzß6bt7…Ôë鈭máýmÛØØqúÂM:·kPhu%f5hø' IDATÊ2ozï|/÷ô…›ØØ(ïw”Òv!ŠZ=↖ßqC˯ƒ¸¡¥ãⲆÕsGxW¢ði¡·X=w..k
õÃ`llÌÚ5.LµZa]‰ß’¡Lµšµk\
ý<¯^³Ž¾Ã×áû�IBîùúß§ïðu¬^³N)“×kJÛ…Я_?&LˆmË®˜[;PÍÌ¢@'voľŒ!ôv�¾îxŸwÃÅe�üœ¦×uŽ8�Ž]m±w0§¾E5…Lìø=�‰åF@(žî�wófí…žç.ýÆÓÇÑ–¶-êbmUí›�Øñ¹¢bâðõåô…›ì=ìÍê5ëró“Òv!¾-::š+WâååM` ¡Þ�ê[¦«§‡¹¹%vv¶8M�ªð_rÑÑѬ\¹oo/üýõ~U_3==],-ͱµµcêT§"qžW­\���~þ�J}?«Â¤¯¯‹•¥966vL)çñK)õ4zAáÛ¥Ôc`‚ ·M$0AA)‰&‚ (%‘ÀA¥$˜ ‚ ”DA”’H`®è„",.Nñ7
,
1|«bcc‰��Uhßúw”H`àéé‰���Rÿ�Ì›7�yóæ):Œ¯VëÖ­ñôôTXýžžž´nÝZaõëÖ¬YÚ5k>½c	ÇÆÆF¡ƒŠ&Ø{xzzÒ»woöíÛG…
ŽP=z{{{…Åðºîױߖ
*°oß>z÷îýÍ&1‘Àþãíä¥È/'¡h›?>sçÎUtÌ�;—ùóç+:AAìíí¿é$&Ø[Dòrãu‹§sçÎ
ŽäM¢öíú–“˜H`ÿ'’—�[E¥õõšh…	ßjû¦ؾ}û‘¼„Ü+J­¯×D+L€o3‰}³	,66–ñãÇÓ¿‘¼„\+j­¯×D+L€o/‰}³	ÌÍÍ�˜˜vïÞM‹-Dò>©(¶¾^­0áµo)‰}³	ÌÕÕ©TŠD"áüùóòîDAø�¢ÚúzM´Â„×¾•$öÍ&°¨¨(zôèÁßÿÍ�;wèÝ»·¢Cа¢ÜúzM´Â„·}ILMÑ(Ê×zB…‚QÔ[_¯½n…åD+ž·“Ø×8ÎÿͶÀ!·”¡õõšh…	ÿõ5·ÄD„OP–Ö×kb,Lø¯¯5‰)]KLLäÞ½{¸»»³sçNV¯^ÍæÍ›9qâ���ÄÄÄ “ɦ ä?~ŒL&SªÖ×ko·Âd2�?VpDBQð¾$¦ì“×$€¬¨~á¿zõŠãÇ�sð�+���<{�T*¥”qJ–*M	£Òè‘’œÈ«ç‘¼|þŒçÑÏHJLÀÈÄ„Š+Ñ¥SG»aff¦è·ShÂÂÂØºu+.\ E‹4ˆÊ•++82å1bÄ®_¿Nll,...téÒEÑ!}–#GŽ0aÂ

iРýõ—¢Cúj(ûçëõÂ
-Z´àüùóܹsCCCE‡õÙ$IћĆ«›&Àß‹FÍhФ=-zN¤D)´uõ?YFFF:¯žGùøÞWΰî÷vhjhн»#Ý»agg‡ŠŠÒ5>s­L™2ìܹ“{÷îÉôéÓ•r144$  œ��ÉÊÊÂÑÑQÑaåÊáÇ™?>ááá<|ø�V­Z):¤¯Š²¾ìííiÑ¢»wïF"‘àææÆ AƒVž™oq777Ì-¬hаÇÏùдÓ0¶¹ÉÔ…›qhß›r•ªç*y¨«øLyÌ5gèÄE¬ßw�qÎyú
†Ž�‘±	óæÏ'))©€ß•bhjjâää„¶¶6ÚÚÚ899¡©©©è°”Љ‰	”*U
G”{666”*U



àÍ{ò‡²¾öíÛÇùóç‘H$H¥R\]]Rž)¼ÑÛÛ›)S§ù,šïFÍÂÚ¾-**ªZgäãìû{·¼˜7o.#†GM­È5F¿Øë)³_Ó maÙ¶m3gÎdÖ¬YŒ3FÑáäÉúõëY¼x1K–,aàÀ�Š竣̟¯ØØXÜÜÜpuu%**J)߃D"Q\		á§é3ðòò¦×P'Zuþ®À×=¸{ƒ÷"’å¿.¥{÷î…ZAóññPªÖCQñøñc444066Vt(_$::š´´4Ê—/¯èP¾:âó¥X
I`2™ŒÙsœY¿~=�ûþHç>£ÐÐÔ*´úß'àª;;7.¢T	öíÝMÙ²e� ‚ðq…žÀé×á/˜²`ÅKJ½¹!“É8²g#'÷ÿÉ¡ƒį*A„"L"‘Þ$ް°0¬Ù’¥^çÕ{ŠTò‚ìƒÑõ»ÑŸú+;uaÛöíŠIAøˆBi�yxxЧïwt8‰ö=†h]ùáqØ=–ÍD¿ïz³lé’¯zʽ ‚2*”.Ä;v2qò&ÎÝ@=«&VO~Kˆ{ÅŠÙè`ZŠC|•³AÈ�ääd"""ˆˆˆ 22’/^P²dILMM155¥L™2hkk+:ÌoJ�'°Ë—/ãØ½'óÖ¤l…jRGAÊÊÌ`éôA4´¨Å†õ¿QY‰$Ÿ¢*šòëoèk?NŸò©ãøµŸ¢²*Ppp0‡ÂÍõwï…�––Ži™R˜–.AÙ2%(Y\‡ç/‰xöŠˆg/‰ˆ|ކF1ÌjÕ k·tëæHíÚµý6¾jšÀÂÃÃidcËè™.˜[7Ë÷òKrb<3Gu`æt'~üñ‡<—#‘H8ä•�‘ÝíLò5�Ý�:”/e)›ª&Ýs•À¾Öã“›÷_�®]»Æž=»8|è ©©ÉtëÐ�n­h`^…Åu?ùú—¯¹ø€ÃÇ®ãzâÚÚº8vïAŸ>ßѰaÃBxß–K`ÉÉÉØØÚa×¶/zÏײ!òñf�éÊÁûhÞ¼yžÊ	,w¾æ/èO	L1	ìöíÛ̘îD` ûØÓ­CZVý¢2e2×pø˜/;\ÁÜÜŠ¥ËVP«V­|Š:¥¤¤pñâEˆŒŒ$2â	Oˆ��ÇÄÄ„2¦å(S¦,¦¦¦Ô¯_KKKE‡\0	L&“ѳWoRe:Œš¶<ßÊU´À«üöË8®úxçiÑN‘Àrçkþ‚þ‘À
7�EDD0wîlޏ¹2}bÆk‹††z¾×“žžÉúͧYºöŽÝ»3oÞBJ—.�ïõ|®èèhŽ=Š›ëA<<.bi^•j•M0-­�ié☖.޾ž6Q1±DD¾""*–ˆgq\ó@bR:]»u£kWGZ´hA±bÅ
=þYÌwÉҥ܉ó꽟Ü÷N�/OàyôSÊWªIûC0*].¿CÊæ�šãøý:uîJ`€êêùÿ‡.BáX»v
¿,ZȨÁ-¹ç»}½‚[L¡X15&�îÈÐ,YãJ½ºµqž;�ñã'X�óðáCæÌžÅÑ£Gi×Ê’¾],ù×¥7ú¹Ÿ„rï~$®Ç¯±pžßxÊô37n|¡¯	™¯óÃ###Y±b%æüŽªÚÇ¿à}/²tÆKÓ¢c_R’qÖ†èÈ¢{ï¢N½G¢­_Šõë7(:Aò --�¡C±kû&ü=³èç>š¼Þf ¯ÍRç~\;¿ˆ·üÎðaCHOO/”ºž?ÎäÉidm…Y	·7°û¯qôëÙä³’@�ªe˜6¾žÇ�¹|rW.ºR³F5þùç¤Ri½ƒwåk›ã<—Ö]¾§¤±é'÷½rá-:ô¡ÏÐ)4vèÌ(§¥ÔkД ¿7‹J¦¦$áï}žÈ'aòm�Ãî‘Ëu¯3ÄǾ >ö%W/�âV 7²ÿ¼'C¸â~”ˆðûòmñ±/ˆŽ|Ì‹˜H¼=Žç(ÿSŒžÃÂE‹ˆ��ÏõkAP¼gϞѡ)éÉḙMÙ2%GÅòF\:îLbì}ZµlNttt�×¹k×NjÕ¬NVò}n]YÁ¬ÉŽhiæO·_�ªe8ðïDvÿ5šÍ›VÒÀÊœäKÙŸ’oc`wîÜ¡i3Ö¶ŽÞ'÷?íº�½[VÒsðDš¶îŽ®~Ϊ……Üdî„^X4r &ê)*×dôôÌÛ�ŒôT233iݹ•ªÕaùì‘ÔkЄG÷ocXˆ9«v¡ªªÆÞ-+9í¶K®^:I¯Á“éÒwçŽîäøþ-H¥R*V5Ã×ó“æ®ÇÚ¾m®Þëo‹ÆccQ�_~Y”ëã#ÆÀrçkãù1Vpc`~~~8vë¨ÁÍ™=µè,Ú½`ùAþÞéÉa×£˜››ç{ù2™Œ9s~fïîmÚ6™:µ
~Qç¿¶�ÇyÉvîÚ‹ƒƒC�Õ“¯c`Ó~š�ã÷ãs•¼ÚvˆL&ãôá­l^=›êµ-iÕ¹?­»ô`ã¯?ÑäÚ÷BFz.‹&�œ˜Ýê©mјÁc�³ëÖŽ~#§Ó¦ëd2Îã{páø^ê5°çÀ6þØï‹aIcü0‹Ñ½Ñ´uöM	?¼ÇÖwÐÔÒf÷æå\¹p$×	¬ïÈLÚŠ±cÇ`júéÖ¦ ŠóèÑ#:ul�ËÒïéÝ­±¢ÃÉÁyZjU/Cûvmðöñ¥bÅŠùVvRR¿ïOBìc|Î,¤¸¡N¾•ý1#¶¤VuS¾З9Îóùᇬ®|I`—/_&àFk¦}ÞžíÑÎq1QO¹vù4;7-%>îÝú�&,ä&Sæg�5©Ó`ê‚?䯫Z³>�}çåû÷nàqrW.àyT!·üÐÒÖAUU
—Eo”J
 ¶¹
šZÙý¾¥ŒËòèþí\ÇmdR–V�û3gŽ3›7‹[µBQ•œœŒc·ÎLŸÐ©È%¯×ú86æiä+»uæ²—O¾¬è‘””DófM°kXžU›~BM­poUeo[¯“óè:`‘‘Ì›· @êÉ—6oþBúûé“7Þ¶`òw7—ŠUÍ02)K‡CÑÑÑç´Ûvº÷‹L&%9)A¾ÿÃÐ[˜˜V ˜FöL‰	º?–Òe+É÷ÕÖÕçö�«è–`ø¤…9ê-ilÊås®h½ÕRÌËâ=Mdl.\ Za‚PÉd2úkS&�î¨èp>jòèŽÝzÂàAß³wß�/ZqE&“1ðûþ44/�ËÒAùåç©P®çÿL£Ös¨U«6ß}÷]¾×ñÅ“8âââðññƶùçý�”0*Ãæ5³ydOظ|Þ�jµÌ‘¨¨PÇ¢1^ÿoUŽzÎÂ)ý�J³r”¡ª¦Nm®_9O™òU(]®2ë~™DÈ­jÖmÈ«Ñ<�Š lÅê,˜Ò�ÌÌŒ�Æû2Ç„�ÑÑ5 AãV¸¹¹}Öû¡p,Z¸€çQøý×¢¿€8ÀƕÈ|z�E¿¬µ2ož3/Ÿ?dÝÒ!ùØ(Q\×S˜4qׯ_Ï÷ò¿¸vìØ1ÌÚ£^Lã³^÷ƒÓRþX>�ñý›¢¦®NFz:MZv¥ÿ¨ÙÏOû•_g
çò97444<Ö]ƒwÊ3c+�àÇ^�(¦¡E]ËÆ4jÚ€‰sÖ±rî”4*ƒT*eÀ³ÐÕ3|§Œ·yž=Ä…ã{Y¾åÔ'ßCûö8¸�Ì¿>^™TÊãG!—./ïâ>L&“ñ<&ŽRFïüjMNNE&•¡£›»iÒ©©é$Ä'Ù­ò%
PQùºÖLJL!99í½Ï×E]ýëX´ÚÏÏ�M›Öà±uõ¼wŸeffñòU"ÆFo¾{†Ç £­�Q)} ûo0ôÁ3ÔÔT©\1ïwð.VL�ƒÿN¼Ù:wéš§Õ.8Àö­[ð9³ð‹Þw~ªS«<®A�î]ñ½æŸ¯w9ÿâYˆ=zö¦œYSZvÊ[óP&“ñòù3K¡ªúî‡'1!ö“I )1-m]TTrž4™LFRBÜ;³?$++“?–OgÌŒ•ŸÜ75%‰‘Ý̉ˆxŠžÞÇ'¯äfâö�‹9íº•â%Œ‰}C�:
˜™
.øiöÀ<Çó¶üœ…غ•ýk3üû_TNð�ÇØ´žEâ“mòm=¯dᬾԮYŽƒG|;m3†:¤¥g÷î,œÕ—½›æ¹Î
[ÎpøÔ}N�>÷Y¯KLL¤FõªÙ9…Uò\A™>/
ùó¯-ùRÞßÐ2--�³gNÓ°Iîfï}(ˆ’FeÞ›¼€\%/ÈîÒûoòz]~n“Àý;�të—»•¦–u,m9yòd®Ëÿ�,Æßû<.Û/²vÇE6»Þ@SK›ÖÍýⲿUý·c‚ÓÇXw½LLÔ+ùc;ûz„ì èÁ.|nþM|\o:RСš)3²ƒ€�4kaÁŒ¹Cä�?'y„?|ƹS¾iÞ�:uŠˆ§Ò/oë–~Lff÷B#¨]³žÞw>a#¶Nå¶ÏjøÿÆ®¿&2~úNœõÏs#µäaXçÎ}^[¾üWZ;Ô)’Éàç)Ž9âÊíÛ¹Ÿ0÷)_”ÀΞ=K¥êµÑ7TÌ�¡F�ò1³ÜhФ=þ¢:³23pÛý�ðdvóZM]�‘S–PÍÌ™TŠTš…Û®�ÌãȤ�¬_:•”äDþZý3ÞîÇX0ù;¦
kÇi×ì_‹G÷þ‰çÙ7±=º›õK§p/ø:Kga€¦Ì›Ø›»7¯Ù-Y—E²·OêC°¿×½7Eºx!€ã®—�ì–V?ÇÙtt˜ÄìiIˆOæ’{îçýØñÏIÜ^|çõºzÚ˜[U'1!Y¾íôqoÆ[F÷vÓøkƒ+YYRb_%ðà%Üy
ÀÕ+Á8�w!3#ë�2‹²è¨W8�[K‡æ?r9Og_`ûïŸÇpY±©TÆÌÉ¿sÉ=€…s6ñ4†)cV+2ì¤R)Ӛʒ9}PUÍÿÑ^ñ½Gcë8/Ùì)ݱkTSþ¼Mƒê,žÓŸ_VÌsjjª,™Ó‡Ó�rÝ"�ŒŒä·u.,œÙ+Ïõ4}=-fNêÆŒéNùVæ�aW·#Xٵ˯X”’µ};N�8AVVÞ¿¬†ÞBUU�
Ur®T­oX’¶Ý"QQáì‘�\>ïÊøÙ.üôËfÂîÝäÄ�ì¦øÝ kìý{ýFNçûgñ×ꟉŽ|L¹JÕqÝõfÙ«sGwR¼¤1ɉñÌŸÔ—¶ÝòËz7jÖ³fíÂñìû{5jjj,Þp„Ž=‡³zþX²²2óüÞéIx¡!�ÉÊ’2vø¯Lúé;v\@B|›7ºbm[›ÆuèÚ«9­Û7àqx]²~í~fOÛˆÇ9?�èÀ¡}î,[¸•Á#:³`Ù=ìÉ¿ݸ
m̘>i¯^Æ3e̺öhŠZƒÈ
©TÆð‹0)S‚¿w9S·~UõžGzz]{6e÷¶Óx]ºÁ†µûyE“f挛Ò“Ò%˜½°èÜqb×®]èhA·ŽÖRþÉs´oe@àÍG4mlöÎ>Í›ÔæzÀ2¾àL�.6¨«¦³ÿþ\í?wî†p by£<×YFkÃÍ›xzz~zç\ø¢vï^ˆüš¬o•a	#TÕ‹ñìÙ³<—‘œœøÉI0MZueöŠ�hëèñâù3Kñ<:Rþ¼CûÞT¯m‰y£æ”)[‰èÈǘ7lFìËhž†‡"•fqéìaZvú
-mÖl÷ ~Ãf¼ˆ‰@WÏP>TW¿8¡·ñ½|šZõ¬ùë°ÿ»w•…ŠŠ==޹^æÞ�pVý>‰I?õCS³ZZèéjÉÇϲ2¥ÄÇ'‘ŸLZZ:ñqI\¿€ë~z÷k��]êYTcÈÈÎÚëÀˆ1ÝÐÒÒÀ±í4:w·§YÅßnâsܾÆÃû8´j@däÙÕ%--�k>·)^BŸ_]&0m‚;þ9ɪõ“QQ‘P¼¸ªªª”(©¯èðåþÞ²‰©cÚXùçaeLIMË !1å‹êrÛ�M|úÚÚ””vïÞÍô	]¾¨¾ÂP¬˜SÇt`ýïëò¥¼/úfŠˆŒ¤D)“|	D™•2*MDDeË–ÍÓë«Ö¨GBÜKãcsŒ×eeeráø^ìZváÙ“‡¬Y0™T†™y#232�ÉÞ¬ûX±Ú›»¿ÓÐ"++‰Š
-:ôÅãä~jÕ³¦\Åj˜˜V$3#ƒípÕƒu¬([¡š|
ÉÇ¡©¥Í·í¬_:Ûæ™0ç7ÔÕÿv	ùE"‘°ýÀ|þÞt„©c×’’’Æ‚e£èÔÍþ�}+U)“cRÂÏ &ŒZA‡.vÄDÇbV§’ü¹šf‰�K’×Ѧƒ
ógýI‹ÖÊwóÂȈ稩«²mË	ù¶Öf¨ªf·"­Ö$+3³:•16)®¨0?êÕ«W\õ½ŽëÖ‘R~tLjò…o-êVâöÝ'Ô¨Z&Ç~·î>¡rEã\ÝócÚµ²`èø?ˆ‹‹ÃÀàÝد�>}ËúUå³"‹:ÇN�˜½ø'233QSû²Ç_Ô‹ŠŒ xÉ'°ÐÛÞ‘óDäãl\þ�Ãîå©N·]yøÿÕ4
Ãѽòà^ÐG÷)aT†ˆˆˆ<ס­«�Y}ï\Ÿcû©CÿòïïóQW/ƶ
‹phß›ßv_fìÌÕ/iLÖ'®ihÕ¹žç\ñ8}€V�³—éºâ~”‡!Álv
äçåÛ±¶oKæÿ»	¯]>C]«&,ÞàÆ_‡ö÷æNPѨÿÉI©\8{�¹‹Gâéÿ'?ÍÈš_wg?)‘ð±a-m
RSÒ‘Ie”+oŒ·×Mùs>^7©^3{m¹Ç�¢ømÕ^æ.Ɍɿ‘ô…¿¾›i9#’“S™»x.›¦â²i*6vu¨Z=ûGÙ¢9[hÕ®q±‰ìÙqÈÛéèÑ£8Ø×CGûó.éÉ­Ónжśõ
»wnÄ’Õ‡IOÓÅ.•ÊøeåA:´þò¸Ž¶Í›ÔåĉÝïðá8vTž¿iéâT¯bÊÅ‹ïŽ;®<§¿””Ò32ÐÖýpÖ�xüŸ‹'p06ûqø}æŒëA×~?R¾r�<Õ{Õó¥ËU¢RµÏ›1•W¾ž§(i\†*5ê}pŸâ%M¾(�L™¿�©CÛð":‚Ú�¹to÷cŒ�¹
õbT®^—Û�W	ò»Ìý;�øyŸ§~ÃOOÕ51­HI£Òø{ŸgÌôìK*U«ÃËçQ\ó:ƒD"ÉNœ2éi¤¦$ã²p<½†L"öE4YYT¬R4ï"û¶
käsêÖóÍ4mMN½Â£‡Ïhæ`‰ûY?6Ê»(QBŸ³§®Rß*{âΣ‡ÏX»<;¹ÅÇ%qâ膌쌚º*Ã~ì¸Ë14ÔEK[“-Ýp^<‚¬,)SÆ®aÔ¸îÞ‘[70ÿç¿øuíøB<_¦vÝÊØ7·`üÈåÞ	Ÿ+ÁÜð¡ß vœ=yO�N^táé“h¾ëú3¶vu)^RŸˆ'1ìü÷ý+~,üð¡ýtë`Q`åŸ<ÀäÑ�ä�§OìÆÝеžÉ°-QWWeûÞKèëi±ú—ÁùRg·¸>ðÁU,²²²8zä(sÎÌRM¥[KÜÜÓ²eË/*'Ï	,""#ãÜßUôix(ÎãzÒkðD:ô&ßž•™Á­WIKIÆÌ¼:º$&Äò<êiŽ$õôQÅßPF>	ãyÔSj›ÛäXÂ*)1Ž`ÿ+”«XÓ
onžž–JÈmâ^=§RÕÚò瞆‡b`XŠ»ÁרV˃â¥Hˆ{ÅíW?š´ÞfX²ô'°’Ʀ¬Ùz�×.qçæ5jÕkÄà±så3<{�•Gðñ8NõÚ–,ÝtŒÀ«´uHé²oýïã£fõ
ÍìzËW®ÁOK¶àçum]}f,ù›‹§’˜KÓ6Ý1,aD ïEŠÓ`é¦ãè–ü¢÷V�4µ4˜àÔ÷½Ï5hd&o	mø{:Î\ç’‡?Í[ZÒ»_+�è„d3DE¾ nýª8öÊN|‰„²å�X½~26vÙ‡6vuÙ²sÇ�x‘””Êo›¢¾E5„>¥u;k†ýЀŸçãïMGˆ}•€añ¢{
_×ͨTåÍ2hü3“=;Îàãu“ºõ«2Ñ©/**žE¾ÀåO'´u4©^³‹W�%äîcZ·oļ%#¹{ûR©L¡}§¦¦ræì96,]“¯å—2`Ö””66ÄÊüÍÝØ%	›]Fã}í§Î’’’Îl§´lZ—bÅògܸK»LsžJFFÆ{o¢DÉzT©¤\C9ÛX0àÇ/_G6Ï2_ºt‰ñ“§3o݇§�_<}�¶0nÖjf�ëAÿ‘3hÓu€üù”äDæŒë�††ú†%¹åÏÜ5{Ñ3(Î=òç!?ô
K’™‘ÁðnõYõÏ9VÏKZJ2%ŒJó2æ*ªª,úýêÅ4غ~!žgS¿a3|=OÒ¥Ïô2‰ÑÌ�Л*5롦^Œ«O2najÏ\ IDATÖjl:±pjâ^’••E³¶=¨UÏšù“úbÓ¬÷nù‘’”ÈÈ©‹iìÐùƒïóì‘�$FÞàŸ¿?|�ž¸�Jî|Í·ùq;•¼_ÈìïïÏ ï{ä¹4Ÿ£R¼zö3غ}ÿ{Wæ8vìë]plwΩé—}î²mÏû»èúõl‘“×™íÔCƒÂY¡þ¿S0­=š„„¤<—ñE·S‰‰‰ÉÕ/ó1‘8�ï2)o-Îplï_–0föŠíÙ³‰­ëðóòíX4rÀóìa:öŽïåST©Q_~£ÌÊ5ê0fÆ*¦iÍùc»©ZËœ#{þàßc·ÐÖÕ'&ʉ{Zc߯‘èÈÇ´í6�®ÿ¿@y÷æåx{ÇÖ!»; š™%£¦.‘—7|ÒBZuîOrb<ƒ:¼;Mö¿Š—â~`L.�œ ù-""‚r¦E·§àK”3-IDDÄ{Xdd$åLß�Tc ¯M­êÙß—®'®‘•%¥GçìKEŠf¯R@·^Ë=]-TT$$&&¢«›÷É.yN`Å‹')!î“û½ŒyƤ¹¿c\¦Îã{PDz1Uke„Þ¹y�gO²`rvÿnrRÏž> Uçþغ–޽†sáøžKUYÙ¶’ÿ¿¾u3îß½�Šªu,ËÇäŒLÊR±ª�î߯¦YRS’زvCoñäafõÉ˨Z3»«03#ƒGîPß:»I[WŸêu¬>ùã_Q¢xÑœ™%ß‚ˆˆÊ–ÉýŠ;ʤlÃQDFFPî=ï»®YyêšeO0zñ’´ô&½5~7¤Ÿº:šÄÅ'‡¾ž>×Bh`QåíÝy\MéÀñOQ*ÒNÝ’l„‰²dÍ.TÊ6ÃØwJ–²Œ�ìŒ}7c7вd'D‹-;‘Ò­´¯Ê-÷÷G”¦(íæwÞ¯WÜ{ÎyÎs“ûœóœïóý¢'ÒÀÿq‘ñ´oÝ ÛÔðm¿ˆCchnR‘vÆwÞçÜ�wý_cXK„�¾½áé1†µtø¹¡A®}×U!44”:uòŸ8âß
<€‰D"¢#óž«Sߘ֟ŠHZ
Ǫ¹£Yµç§¼…24iaNW묔ÿ22�‘MÍ:²Ùy/ŸÞãÙC_eÍ—¦¥eEý$ÆÇPIY9yyÿ5 &ÄG£¬¢ÆÉƒ[8ïö76ƒ'Ñ£ïHn\:ÁËÇ÷2÷“¯�ñl¨\¹rÈÉÉ“”‡VÕŒè«üDúEG†Q­ZÁBèAá…†Ši=ÔüG&ÒV!444×mâ�`L�¾?R×>‹¹ï¹ÿÇALuÜ‹ªŠº:ê¿‰!Úr÷A åË˲d�\æÐwèjž<¡I㌜²•¿¶L {'cVorç¦÷3$’tÚµª��¾;ö]¤K‡Æ,p>†ew–Ìù%Gtu2î,3€8Œ^$e[H›ý†ÙQQY…-+¦ÐȤ-wo_FE]Ýêu¸qÉ
×a÷åÊ•§}×>lužAËö=³-ôõ<œ”÷IÄÇFs÷Öe~nnÎO
Myð˜×/2œïû\#!.ƒÚ
xýâ!f,1ïÞ
-|®ŸãÇœ¹ede1jbÆU�cH¥RÂB	xú s{XH q1‘9Ž‹�
GW¨	&”qÈÛÌ;‚ÿ‘¶â�à\·½ŸŒ’báÖh†„Fsö˜ÇöÚÓ®U}üqíÔ|Οƒ·ßK"£p;ë‡ï½\_ɾÍ8²k*cì¶g¶¡©®Ì}Ϭ_6w?¦O²bí’¡œ=æHõjš¹žWI©ïßn¹I�ïÀ”••‘"%å}r¾Ë~”+Wž©ó6a7´#—O¦»í0_§Š¶
Š…{Øš‹‚bEÊçÉô£JLH&,4ŠÚ†ÕJ»+eNdD,jê•‹%`Y&F§mþ—ÖÄÄ&ef�ÿ’r%Åb[GVP:UU9{5÷µ¯Ú:ºˆÃ
VÓ Jæâl}-4>EΖ+'‹ºZ%¢bðòyŽD’ŽE¿Œïb©‚ÞF‘1ëÕ¤qVáI£,4z=«6ºaÙÝ„‘C:åz^qX4ÚÚù�dÏM¡b=µµEÄD…££W#×ím»ØÐ¶‹M¶÷Dúµ8t)0óõ§µŒžîLzš$Ç—´T*E¿æO6hšùÞ¢�X©)ï‘“—Ï–�¾cÏ�t°@Rb|¶Œuê³~¿g¶LƒÇÍ`ΪÙΩ¢¦É²m§HNŒÏ±Æ­ÏЩÄF½Ëñ9c"˼*óþmKñpÙ‹ªºq1‘ԩׄ©ó7åZ-?Úÿ€Ó1n^¸òeA˜8ŠI£WòâY0Uªª“”øž‘ã¬ùmd�¼þŠðÐ(Ž¼È»~nÃóÊ=Þ'§ÐÅ¢E�Û(*Í�†qÕw+zÕŠ®öÒ÷HNNa�ó!fý1´DΧ¤T‘ä÷ò½ÿß×pýÖÓáîNö68L´,Ò¾…Gıi§ógìo+)9%¥Ü/`uttyóüiaº‡¢Bö;¸Ü–ÈÊÊШAuV/Î^áY]µRŽ6¬{˜òîÅÎ_~Àa—›45ŸAÈã­(TÈ~ñü6$�ìYL¾W¡.ÏtttˆŽ,xÀÏäääs^ÇÿZÏÊ9#é=hB®ÇTPP̽|ЬìW˧|OY•ÜhÇÇFgF.~)ò�¸H°ƒÛ—ã{ý<ëÿ¾Æúýžì<ñ€ŠÊ•Ù½N(­Ï >séÐÙŸG{ñð\Ïßÿ,`ÝÊC<¸÷²Àíú?À÷váÊ<¸»^'â]l¡Úø¯GqÆ­ä*ˆtõ	�þ®c–ÿñ+±�{²ýõààÿ8ˆK׿½ãWˆÃbéêåºM$šw0]auhcĵ›�Q¨ ÏOutyöB̰ñ›(_>çÒsÀ2Ü=î`eaʆ忓˜”BBBö©Â´´t¢¢ãÐÒ*\òáB
`5kÔ øÕ³BuàkTÔµ0Â�öÝúKûѺ“ò²MLNŒ'))¡ÐWŸ}ü˜Î‰ƒ›™•¿wŸa€•ƒlçråBFiø÷ïS9hqæþ	ñÉ™¯ÎÙIdD,ã‡;—Èç‰ô‡ÍÅÃeÏG8-:˜ùúÇ4¬~u&6.‰Ä¤¦8î¡qº÷]‚ß½Œ× ·‘Lœ±‹µ›OaÒa&=ú/åÑÓ`Þ§|`Ö‚æôÑŒ/ÞÝëçñúÅCìæo¡YÛn¬œ;:3’ò܉¿èn;Œ_G;²iÙ4"ÃÅH>¤2o¢-u�L˜»ú *ª,™>¤È&µû~/06©›ãý¶Œil\©TÊØaË
cúX´`PŸyˆC"HˆObÏvw¢£ãùs»U´ÕY8g'•”•6²'uëé3qZbcd3‡n=[²ýo'>¤J°¿€ÀWb8î «E6lŸÆ�«p9r™æ­Œ0m^›~í1ïl²ù{Ñ7ÐæˆûR:woÎôÉE“}»°NŸ¼É_»Î°xåX¶ýåHHpÛ6º¢o ÍÓÇo¸u##ïçÛàwœqó°n5æÍÜÆ•‹~,Y5Ž!Ã-˜6q=w|žò1ý#×®dn”HÒ2_O´ë‡šFe,]"ŸKGGqØ÷݉\¹ñ˜µ›Oeû‰ŠNÀ¸‘wxû)Qóéów‰�KBU¥"#&m!2*�“¦3À¦ý—ö.–Ĥ6ï:Ç«7ï8´c
ujé0aú.*Èá0ÑÃZ"9
àÔ¹;¸œòæÔá™ìÜ0†¥k\yðèÍ7û)‹ûêrýúõyø$�÷)_Ÿ>]¹p0–ÿží½È—;ÑÕQ§[ÇŸñ:—u²vÉPfL¶Ê|x#ukgÌ.9Úõ&ñí_ܹ²ßKËha’=¸~Ù0æ8Øfcj\Ÿ‹Kñ½´Œ�Ç[±Ÿ3KþMïg˜˜4Íñþ÷*ÔÖ­[7žÜ÷Î,¬øÿÈïúYlz[å½c>%'%P^N>×+›ÏÌÌ{2wõ!*VR!*"5�*DFd¬ñ<œÞ¿N@G¯-Û÷dæ²=™Çõè3]ýÚ42iC
C#‚ŸqßçéééüÔÈ”¨ˆPš·íNЫ'„¼)øt\qJHHBAáëQWoƒßq×÷³£«§E¿��¨[O�S·�‘•aü”¾èhceÛ–·AáÈÊÊPYµ"òòr¨ªVÂã”U´Õ©YG�wá1ô°nÍ¥ó>ÄÅfü�›4¯G§nͨY[—vŒ	
GAAÅ
TRVBIIUue|n=æúÕ{Xôj…û¥²Qô±}Ç&pYˆºFeBC"ÑÔR%LœqÇm; 'þÉÈÞàrä
–6m‘‘•Åí¸'íûS³¶.],ZЩ›).G¯|ó<ªjÊ”“•EC³dBÛE"Ñwß�EÇ$‘íçƒ$
U•ŠôèÒ„#®^üuäÚ“”œÊW/l{5'4<–ºuDÔ©©Í?'ow·³ím¨]S›ÁýÚò*0ÔÕ*!'W
ueÔT+òVÅÑ·�••%øáf5¨þ­n~ºËý…ºº:MŒæüå¹n/«Nœ¹‡¥•MÞ;æ¡PAÊÊʘµjÍÝ[—0ëPôsÇe�Dò�;·/sìÀö¼wΧš†
IJŒ'>6*[¦“l¥UÄoX;éééÔoÜɇH?~$9)�øØhôke%ßÕ¯‘u·òyJ@A±"’©DG„!•Jq;¼5s›I«Î¤åcý[i0jT‹‡r¼ÿèÁ+bcPRR ª¶F¶„_–=ÑÕ«‚ò§ˆ+yÒÒ>æh+4$’÷É©ìÞê–ù^÷^­Hü”W±^ƒ¬ %ùlÙÈ?›9÷7vn9ÁŽM'˜6a=¶:°Ðyô7/LJBhhSF¯"))ÓõIMù€ì§¨;Û~æX˜Oaþ²Q?r™Í»gŸ„D’FÝzY_²uëUϬ‘ö¥ôôœ¿Ë’R½zuž¿|‹T*Í÷ïØ¦g3&Žêžë¶a¿š³Àù}­ZrõÆcömžÀ[q²²2vÉz¶§'ÒÈ,›¢]E•*Z¶¢¢B¶Ð)“---™1sééi®Üëáº99y:YþŠªzÖ•ÈÉC[ð¹~Ž=~¡ŠNÙ[ëã{ý,¶6ÖEÞî”y›˜ö{¢"B12nÅ3¼®¸3vÆJ*((f”Vy�QZåÕ³øy] �±Ö¿ŽgߦE$&ÄñòÉ]R’“¾Y³Í¸¹9:z5X1{{Äë²;ï“èÙ¯xŠV}£LšÖŸßúÏ§Ï€Ž¨«WÆÍåéi錛ÚUÕJXÙ¶ÅnÜZ†ŽìÁ�«�““£u»Æ¼yýõ…÷êê•yþ4ˆ“ǯÑú5×Ã~üZ,,[qdÿ´ªªQ®|Ψ×/©©WæÂYoŒ×"â]×3b¬/žSYµÕª—lHûî­nTþ"YkçîÍ©gTƒWïáuÝŸÏ‚9sò&õŒ²î(ûþÒ	ûñk™5/«ȨñÖ,ž»‹È^<æÙ“7Ì^ø;òòrÔ6¬ÆÜ[iÙº.G.SþSL5�ÊD¼‹eïöS…ZÞð=¬{ÛâzÊ—æMó—ÙáÌ…{DÅdüaXK‡�}Z###Ão¿´gÉêã\qûȨÏ5y´£¦leæk‚ÞF²iç9y­æ­8ê«çÑÔ¨ÌÓ!ì;t•¶fõéÖg1û£RY‰ÇÏÞ2nø×KÑœ8í—çT›��¸�õú‡i¾>{iڵߓþI[…¾ÓÖÖÆÐ°.÷¼¯ª�Æfx]qÏ|�”Gpà4«dŸûMLˆÅÛó,¡ÁYQ]q1‘„‹³„ÆF½Ëöúéon^vËv@B\Þž<ºç•Y‘8¿$Rñö<‹•UÑ=ÿúLCK‡5{/Ñ´e'^¿xH݆&l>ê�™’«ÏÐ)´êhÉí«§3×­}Àúü6™¾Ã¦òì¡5ë6bÚ¢mtê5(Ûz½ýÑ­^YÙr,ÚäŠQ“Öøû]§©Y'&ÏÍ»Œyi:²'›wϤR%E‚ƒÂ™æ4ˆÓWסúiMÊŠ
“é;°#¾·ŸÐ QMö»,¤²JEªh«3tDÖ—é—¯71dÌD[_…¢¨XW�þT¯ëþØèÀœEÃhÙ¦­ÛgÕœúòõ°Q½hظ6ï¢6²'c'Ùâ{û	R¤¸œY޼|É-"Ÿì0 ÛàõÙï£{Ñw`'Î�¹�‚¢<ÇÏ:Óæ‹ÏÓ®ƒ122`iÛ6ó½	vý˜:s ýÐÐTáà‰ET«žqQ´÷ð<´´Ôxñ,ˆùËF1Ñ>£´�ªj%–­�ÀÛàwH$9§X‹ƒµuo\OßÉ×¾ƒû·¥YÓÚßÜgä�Ž,˜Õ³fYÓð«
ÁÑΆÛ~/Q®¤È�³©¬¬ˆ–Feì'd+|ùºQ}Ìêϳ—bôµ¸à:—˜Ø$|ï°cݺuüz
3×Ó~XZæý3Ábf-<’ë´eYâs7/ŸF�S$í¸œÊ—ÜÜܘ6s˶{hê ·YUÆÏZÃU�£,ü3c¡òE÷„pý‚+ÓnǰAölø¯+î4lÚoÏ3X‡ÍàIx¸ìåùã;Ltʈs;¼�À—�˜è´Ž�K§üú9µëýŒ·çY~9óîýxöÐgÇá40nIðëç(WVeÞÚÃÙj‹}‹Ëþ?‰yˆ«Ëñ|í/”SÉŸÿr¹�¼”v9•””Ø{–‡÷X½ij±œã[
SN2¦oE:U¹âæ”9÷#{ù*Œv½ó6$4_ß«;´£oϺŒÖ¹zW0í-1tøT†Zè¶ddd
ЫW/4T•ñ<_ðÿX
›¶âí›—ÄFg”%ñ¼àJ›Î½3·?{èËév±îï«Lp\Ê�çØ¿uYžÏÇ|oœgø”EŒ˜º˜™Ëö øiÁô®usé7Ì»ù[X³ï²åÊqÁýà7Ûú,1>·›p^þß«=$øÿõ‹•‡ÿ:�ÃìÁ¥Ý•‘‘‘ÁÊÚŠ#.·J»+Eâï£×±îÝ;ß7+V®aÁ
“RйgãîáGdt*C†É{ç|*š²¡ÀêU+èÓïÌÌ{(ßž¬l9š·íŽ×wÌÌ{N�:F™Û_<¨I«ÌŒUEúèÔ!èÕ·Ó¨X
‡ÓXKj6¤y»ît±Lzš„€§÷)_^ŽÛ×Îð.4˜�ïfËŒÿ5ÇÿZGŸ>¶~÷çÊ*�¥Ý…B›6m­Ìš3~D—ÌèÀÑ»ˆ86î8��oþ¦Dš4iB·nLžµ��ëGcï¾_Dd<gîcËÖÝ…^¼ü¥"ÀZ´hAÓ&Æx¸ì¡GZw´æð® •Òªcöyßòòò$%d_瑃²ŠQ¡¤QbåËuiÖÇÑ©×@ün^àòéÃx]rgùŽÓ #ƒÕÀ±èêg…§*VÌ»ü{DØ[.�:ÈÓ'�ôAñ©]»6ýú÷gñjWV-TÚÝ)°ùÎ.2ƒï:îÏ�›iݪk7ŸfÊØ¢O0^I:¶C×òÛÐtíúõ€•‚(ÒTÕÎË—âºCfZ£ïÕàç„¿ÆÃum:e�î«×°¯žûó& #_ÝÝ[—xŸœHõZõPVQãMÀ¤?’ž&É(IOOc€VD†‹i×µ¿ŒœAä»deËadÜ’;·.¡S­&:Õj²q™}føù·Þ±œ	ãÇ:‹²@ (sçÎgß!Oƒ~Ì*éÏB9ìr'§9ß}¬’’®'ÜqÞpšs—ïCï¾ßøé{ÐЪÁ¼yó‹¼í"»¨[·.ôgǪLš»)ïþEFV–æí,xþÐ�j5³mé×b”ý2ÇôB£Ši	sV@A±"MZtÀíÐVFX£¨T‰†&­ù�šB¹rå4Æ‘µóÇ!'W�˜¨pFO[ÀØ+Y9gclM‘WP¢~ãf¹&êý’·çYž>ðÂåÐŽoî'J�––S¦LÅiñöo_ÚÝùn³ÆÎ~yïœ}}}9F[kNrÀÄøë‹ ‹ÛâÕ®xùáuË»XÖIâ—$	íÍ;R»q;zšXdí–¶7OX0¥/Î{`llüÝÇQˆù#D!–^bi+lâ—’““ihT�åóúÐDzôËÛä×�c7˜½Ä…‡�ž ¤”¿:‹_ãææÆˆáÃX·t0lÌŠ¨‡ù“š*a´Ý.üŸFpÒí4ººE_±¾È¢¿$''Ç	×ã\=µoÏÂeè(+âc£qžõ,Ðà%JÖç©´	Ó÷r÷ÁëÒîN¾øÞ
`Šã_œ8é^èÁ2¢Ã/^º‚ã¢ã8-:\b	ºÃ#â0·ZL²DÏë^Å2x}V,åZ555qs;ÁŽUÓóŒ,ëÒÓ$¬š3œß‡ýFß¾e§´‹@ ø¶†
²uÛNzY“Y9¸¬
{‹ÍokÙ¾c
6Ìû€|222ÂÛÇOïP:Û,ÅÿqP‘µýoR©”Ç®cÒÁ‰®ÝûpøÈ±"ˆ¿¥Øê�7lØ�m[7³Âq(ñ±Å_¯¦¸ìX=‹Õª²`þ¥Ý�@ð�¬¬¬5j<½¯!5µl&¨NM•Ð{ðÆŒ™P,™}455¹xé
–6CéÔ{)¿OÜöÝÅ?órõÆcšušËºí×Ùà(óþ˜_"ù0‹üØ¿-wvfóÖÌXº/G`FY–&‘°m¥Qâ—\»z™Šs/é�_¥�…¼¸å3°ÿgùyö_V\ßEƒ
Dü„£»'•©õaQÑ	ôû}zÕ°wßßÅ~¾øøx–-[¶­[`ÓÛ^ÍhÓò'Êç‘ç37QÑ	¸{Üáàñ[¼|É’¥Îôëׯz�;™âÀö8€�Ý4Æ9®§±iÛ¼(eq1‘¬tú�ŸêTgßÞ=(**–v—A!H¥Rœœfqôð~N°§žañ=—ɯGOƒ±úu5¿Æ,(Ñ‹“��víÚ‰«Ë1Þ¼	¢G—¦ôìÚ˜:5uÐi ©‘}M¬D’NhxoÅQøÜ	ÀõÌ]|ï¾ ƒy{llû1pàÀ"+ê›_%6€øøøÐÛÆ‹þc±°^ìç+¨×/²ÂqcÇŒdÎìÙ¥Ý�@P„Ø�ÝÔÉìÚ0
‹Î¥�uâ´£¦îdý†Môïß¿ÔúŒ««+çIDATqó†'jJ±ÿÍœ[WN•äé¿*$è%+œ†qtÇ._º ^Á˜©©)Þ>~¼‹«„¡©뷞͵ªvQJM•°òOwM§’(ÑÄ×ïn™¼¨Y³&ÆÆÆèè蔹Áë³½ûÒåË—±³w@òQ–�cfS¿qÉ/6Œ�Žàèî•x_;ͬ™3™0a<òòò%Þ�@P:=z„ã¬é<|xŸÅN‹žÐð5iié:~“ÙKŽbllÊ’¥Ë©W¯^‘µÿÿ¬Ä§ÿM*•räÈfÎrD¤oÈ€QNT«QüÞSÞ'sòà&<\v3bÄgÍDEE¥ØÏ+ʦ7nàä8ÿ‡Xti‚Uwcºuü™J¾»­Ä¤Î^¼Ç‰3w9}î�5dñ’嘙•l6ŒÿºRÀ>“H$lÚ´™ÅK–PU¤O³®4kÓ
Ýêù+
ž‰	±Ü¹y‘;^ç¸ç}+++/Zˆžž^‘�C üØÄb1'Ožä„ë1nzݦu‹ú4m¬�®Ž"muDÚjèŠÔÑPS&2:qhâ°ÄaÑ„„Æàw?ˆ·ŸÐʬ–V¶ôêÕ‘èÇ/®Y•™ì³´´4®]»Æñ㮸žpE¶œ<&­»bÚºuê7ù®:cR©”pñ|oœçÎM^>¹O{óô±íMÏž=QWW/ÆO"~t‰‰‰xxxàïï�8$±ø-b±˜ÐÐp¢¢cÐPWC$ÒF$!é¡#Ò£qãÆtéÒ¥ÐëFy+sؿݽ{WW\\NðòÅs•Ь¢�ºfUT5´QQ¯ŠŠšÉI‰ÄG¿#6:Œ˜ÈwDE„�º†�;wÆÖ¦7]ºtÖs	ÁD™Àþ-::±Xœí'44UÕÊŸ®‚²~ªV­JùòEZ-F eÄ7€	�@¥°L ‚¢"`�@ ø!	˜@ ~HÂ&‚’0€	�à‡$`�@ ø!	˜@ ~HÂ&‚’0€	�à‡$`�@ ø!	˜@ ~HÂ&‚’ dòÁç•ÚÆÞD¶IEND®B`‚urwid-urwid-69907e4/docs/manual/images/urwid_widgets.svgz000066400000000000000000000246641526073734300235600ustar00rootroot00000000000000‹í}ûoãFšàïù+xnÑ�e²ž¤»[‹éÉö @‚l²;9,[¢mmdÑ�èn;¸?þª(‰bU}ŇDµHÊLÚ¦(²ê{¿ëý¿>?,œ/ñj=O–®ü±wåÄËi2›/ï>\ýǯŸÜàÊY§Ñr-’eüáj™\ýëä»÷ÿËu�¿®â(�gÎ×yzïü¸üc=�cçÍ}š>Þ\_ýúu<ß^'«»ë·ŽëN¾ûîýúËÝwŽãˆ÷.×7³é‡«íŸV‹ìÆÙô:^Äñ2]_ûcÿújûtûT¾}þ%ž&Ér�}s¹þ¾pójv›ß-Wóg7ùa^{è!WÜá®_–iôìª_k„¾Š<Ï»Ÿíï¬w×ÍZôQü?¿}wa¼NžVÓøV|//ãôú‡_È?t½ñ,�³ƒ§òVÈËè!^?FÓx}½»ž}ÿë|–Þüz�1C(»xÏïîÓWœ�±dWç³Wbõ87wíVs“S‰7Æ›�v¯(~D¸³BˆXýò,™Êµ}¸zZ‰µü.þ§ë±xÑŸÙ�ôÕgÇÏ�É*uoç‹xó„ëûä!¾žGËëâõiòx­<ów4~\Þ�Ïxž=
„øáËîÉøôý,¾]Ë»6ð�	€�ì3ñéC´ú#^m>/±ÂÇ(½ßݼyŒ¼BÂпÚ_W:æ|ä9n0ftDoäúž³ýãÏÂÍÛW
<,ÜÕÓBÀíK¼Lf³wët•üß|ïe?Û?ÝŒânüÇôÝX®�"«ôf)¤Há¡é*Z®õ?š›F‹ø� y2’ÿy{å\o¡}½yBCØÿý)møÊÓ…¾ðnC_¿ö9´»>ÆS)€s ìX=}‘J½Ï®Œ}yüýY€Û¹q(bc?ð½Püîƒ7¾lnôÅÅ?xÏŸIVýÐí²\Aws!w0Gc�pDÄí˜zcp¬}U¢q¿oî¡Ô€&j•&:–&±O®Ód h28XüeµJ¾¢ŸâåìxÈÏ:î…(“eê®çÆ7>z|~gƒì”Þ˜!º»´˜/ãÿIæË›UòTX^†ÒŸö¸Ѐ#Á sL‘ã¢1ò8
B*àëùÌó1b»;!…Å­žÏ=8SÇõÇœPŠë‚àÈc‚±äEL	ñ‚3_Üà‹‹,vù(¯Ÿ`…`
¸}ˆÒÕüù�x‚?òF’ÄÄ/t,–r¢ýŸ2ªj	ÕúÓF6õtŒˆM;täRD$·ílXÍë ‡•I}M(¢ÐãÕ²ÖS)ÅÇxûcûB¤ûÙ�ô(ͰU3úcª4VŽFؚї^4pàk¸( zá —M#2<œËéwYn¿Ûq3¿ËŽ®×1PåüSúß¼Œâm¤ñЋg�n¾k¤ŸÃþh²Ïº
ûc"^ºÖ#îûP‚kÚ
vá†	.Ûô§Œ_¶ûOyAk\$_8è…k��Ðûmæû—­†f	¾¿–5Ùoùd9ÆìË<þú]„ÏÑzgn?Fwñ4Y$«Wßßf?Û>'«Y¼Ú}IJå£ä1šÎÓ—M9ÒwÅSóÏ=øóõ}4K¾~¸Bú‡&Ƀü'!ö¨çëŸOˆ/
cÔøP¼‡xìÄýÃY2}’KîÓrž®1<_Z­ä
‹è%ûÎþ1ðu¾+wwÕ:,0ö·½cWºrc�Û;ž÷^�þ‘ØH¡]ß'_ïžæ³X,ú6ZäØË¿“}è~þœ<«7d_\Iœ§«§ÝEy!M±ð²¦‚–ŒµÉÏ·Ï*~K¾¢ð5ß3Á·^F�îÝ"ù-”ïê{{ˆžçó?c±.ºÙ3Œ·«§‰[
Žì&á†Zo�Ïw¥ïk»!ùü?BJìž‘-D—0LEÑò7ñ÷§Uòð�UìöKœ¦óåÝ:çà
ã>¿È¯½!e$äÀÜ]|Q/Êz1ññŽ´Ñšì×E”Æo¼‘‹…ŒFŒó]ÔÆxùöY?É9†¨Zš_ûõ[°¤ñsjh\½¬ÐvÃsÕ
e•�óeºJfOÂêH–yÙãn3rY”û{Nl@�?&<Ä{ûDð½OÇæíÔ–z”ìOùÑÍR’æbsåk¦ŒòK‚Êòà\½Ûòú�¿ÝÉ Üæ«·ÑÃ|ñróQ(ÍtGÎ
%àü"€½_ßÃâ&+iZUz¼ú"æûT\ZîùÜ™eo[àÈPŽŠ…¹X%r÷RzH
{-!¹Åõ]Âw„€»Û¸dÕWq×”B”ÒÕl•+¡b
ûÚ`3Ñ
jn¾�Ã)‹n-xC¬Þ8Ì~I…â#± ›ÏOij$	ñ¯vW³?BE§7dwM¯;‹„	·ZE/J)\Mno×qzãmƒ¼›Ï3�?_ÈGleÿ»Ù|ý(xùf¾”Ëx§Ç­ßm´‡û9šþq—LÝDSaÁ=I×E‚¼˜CØÚg!-\ÛYd«ôÇQ0.Þ'­HFÆC~14~0í(”SƒvJ¨g¯‰VC¦ôÞƒ·¸m(B
°(8ðF/wHÀÕqÖZ÷Xt(P
¿aQ7ù$XÄÙ]È<ù=Eör¥[t!Áƒ‘ÁY‚¼ºcj’É7¦ë‹�<“† ±ÝNY¾ŠÕ¢XµŠˆü»Ê©0§IÒ#¡Á™I…:&ëñ‰‹0	H=ù£Àeò1yn"]JØëö6Ž£è•½*¬–·iµ„ìÕjiÁj		²�ãâã�[-ÐkY-¡R>b³ZæÏYjSÁ²%ò§‰(ñ‘ ù£>¥ÜL>õý`Tì«TBYµãf$¡ ©ÃÄ	ˆ½=¦}À€<!öÒaÆ > Uè<_	ó°�Ll4Ž’µý1C±£?'‹Y[šã�»£wóÒÇx:¿�O#»¼ù‹œ0;?'ËÄù(ÞªašÃ67à
Ÿ¤cð›[l‡)ŸX ¢ÁeòÃüË|¯,zQ«Úìß…c�ÅN!
ՒѧÕâÍ÷…bæ·»�æ³ÍJ¶Ç‚/•›eùíÛ²zW&Lé¬ãrD¼`#Æoh$›UËr¤È£<•Ìb™ÁYá4-²§Ñ¯ܵQŸ]€ÙNôk0ë-uXµn£åîlо1Š @Ac4à†Bî¡1Ú¯‘qWþÏw=7PALlqðKß`V"ü!%•ÅvŒ¤2$rˆJ>øzN�zÕÀÞ*åC�<à%ÊÀÝÛ”Y"Ó¬äC‘AÉÅU—Ûä¦á8ùì­J> Ö–|¸SÁǘ’e´ô`ô¶k¼–ߣy®µˆ¼Ö»[jUØ¢]Ön‚hGª]F¯,ñ«½£Ì0Q�ÿþe{
aU¥¾zÉÊ�½MФÔZ@ux*=ˆ'Ê´_µþ+H9†©¯}f’^zù«€,ðùpæØV‘i)™ÜA‘fˆ(!.kûEgˆ¤ev÷Χð7Ô‰è�õæ¹N¤kÎËÅJ~ŠüŒFÎa–1²3Hˆ—©óu=>Ƴœ±Z™í�¡Y§l„�]M’•3]Ìå*œ4q$á8é}|’�”£¬Èø‰¾Dó…T¨i1r¢—Ó¬ÇçUò‘XÏb�d‹æKç4ÁK©¨ŠÒt5"9^;o2³g=:ÍrH%å#m"ìYS¿ŒGŽ49ÞZìñ뻞ç^}V|_óÜ«ð?ŠJ°vëÅä^}Æšæ^ré	¨ñ\A±Ãƒ ¢ç^ÿm6o^ºÆ´«Øgðo­´«�—v=£šA\Ͼùf¨aÏ‘J§lÐpG»¶î»È½ÜÃW“ÿXËâEÁ�™
•Ƕ–�@l¿,¬•ìT¹hé$�RòF‹Ö�y9,°°¶:äÛO‘J@çå	= Œ‰½�q2¤L‚ØŽ‘IЂ‰
@&£õ|êüs³‰#[WO¬´úÙ¨Ê8ª˜¥¯*o¶¢¡JÉÁ˜dÓèÙï¼J½«qûY|™,ï�}ÂË’}ñCÀ¸"åjTÐ’<°¤¤¼™!0÷¯3Q+%¬Ø“|`ï`¯uÁ.vk´xO�6å çâ �P&ðí fr£€F^¶/e˜ÈG ¤;á!ÔÎMmª$X²–õ¿,ÒwÎGA>ÉÒù—»ô�5gÓ+Já%¾¿N)¢½�
5 -+TJà¢P$ëP؆¸ÖNœNÇ'Î)A™Í4Û½"#¨iP{dUCÞkJð[¥}#'H¸YæZŒÈÒE¼‘51”�Jp•°«Éç­óïÑlžl¹6wPAd¡ àÄF#ȯF^Õͨg ÌÖ’9<#�Q0½	í¿‰‘÷_Î;½�§|LlUˆ…¬g
T1ÂZe2E`2Tñ¤»­]Ÿ
m
"æt$ £qD[E�÷•qÆBÐÈ:žš³²bÔóȘ4è_�jHlºJ/2³Ô£U¨*쇰ý¦ÉÎÒHQg*1òùõ*awÄÜìa/ô!“¶­’nA i×£±ý²12]·[F;në’EÚÀ90TJà¢XZ#¤-�­2“¡vˆ°�¡‚1÷8Œä½ï!>Ö9²Ð)%G×銇@Õ¹àŠ´5M~«¶Ý/ؤš›´.¢kèkÀ.ðNK^Àóê°Ë!8a†¸+õ/'ÀNk㤖«�“AÉ.—É.¡Ž—]ŒN”Ȯݚân‰`-ŽO �–ìUÁ¯04Ÿ€@²*…ѵ¡ã9¤ã-hSÖt±8”B'A8	t}^‰ÿÙ:ÐÔ]ÐNëÊ® e_À‰Q\eÕ'„'V}ïUʼnþÙÅéH5ôI½�â!
Ðv9ú„ÛôIÒlûVœ´¡ãC@ÇÃ+ÒÖt�ì
ÍMؤu]C_“°¸ÓR€ïñ„½–ýøþñ	¥‡Å»°‡Žçìéúܺ¢â]Øt¼%Þ/¢kèk'ÞexO-ëø=N|Ã�‡´ìºšÝï´d¯
Nê~YvÁ@²*…ѵ ã±èxÚ”5
'¨OüüxŒ?^QúA±zXŸÀ‹èúÚÑ'6€ñt*?#CÇ[õI84_¢Oཪ8¹x?R
}‚[ðã±1צm‡ê“Â’�îck¼‹/Ïh�wÁ{Uñ¤ëý‹ãHÕñ.ŒÛÐñÐñ6´]N¼Ë÷6Z×,0,�waÒ†O?^Qñ.LLo‹wÁ‹èúÚ‰wÙ^ÄÓ©üxbâÄ®O†ƒ,Ó'•1HÒ6Nú'»¬±ú
}Bhv1nÊcõ¢OÊpR–�'mÔA¨²'Gäã	TiÉÇPvYóñ5dWÛu�ýãk²"ÞEXv1jËc�ÇÅ»«Ÿ?\¬¾$ÞU«'FmdkvTi©"NlõÂ5pòZY"»Jë…Iu�ªƒ,•]Ô÷'Öd•>i£’@u�%1È#õ	TiÑ'ƒ‹Õ—è“êX=9Y$�ê -údˆ8±é“:8¹x?¾Dv•ë“6ê 	TY*»Žé?!aýüÉàb�%ñ®ê$1j#/ŽO¬±úªxWØ‚Ž§FÍcy¬þBâ]e8)‹wÑ6ê )TYŽ“#â]ªƒ´Ä»(»¬ñ®jÙEOVIÍ:H»>ñý¡áÄ®OÀ½*8y­ƒ„�T­Ohu�ªƒ´¡í‚ôI	NJõIu�ªƒ,ÇÉ1úªƒ´é“áÉ.»>©–]m×Aö‘OBŸ”Æ»(jaf5j­+j!ÞE1«‡ã]ð"º†¾vâ]6€ðdÔF¶fwAu�–x×qb‹wÕÁÉ«o—]¥ñ.ÚF$…ê Ke×1ñ.JoóOŸØýp¯*ž^u<¤þ	iCÇÁß´]�R‚“Rÿ¤�š;
ÕÜ•ãäÿ„:ÞæŸOvÙý“jÙeÔá5‘]ʼn⃘!MZ£Tvù~>£oŒ°®èø6)ñ¶úc�¨ãíLjVêxß#pq|©:¶â·02€èáöR´]Pl…[b+åEœœ-øŒ¾1Àº¢ã�U >ØžnñOÀEt
}-ù'€ðÔv{zùÄ&»ªø·àŸøF+z¹ì:ˆOzˆ«ìâa9NÚÐ'P{z™ìÊ×t�ì2ÛӭǾ‘p`qá’cßཪxºxÿÄÊ'~¼ßB{:1g“–óÉ‘~<­?‚fˆ:Þ6‚¦†Ž7ZÖ/�O‚Ãò'~G]ˆ‡@¶0¸¢Ãùd@8©²»ÚÈýú`î׎“#ý(÷k‹­MÇ—ÅVªuükî·DÇ—Ç ƒ6t¼‘S¬ÒñGÅ Í<£=V?4_«¯Öñmç81âÂVÿ„â�É®ÿÞ«Š“‹�—äOÊãÂmä”g,ÍŸ\D\¸'e5w¨…qñâ!€-\Ž“#jîPƒ±¥t€#2m>#¼WO§[*ž\léqb‹Õ×ÁIËúd�“#2éðÆÌYýx¯
NÚŽAîqÒ`ôßùÄZYƒOŽý7§Õñ.DÛ°»Œ£uKÌåˉwÁ¤:Þ…Úè?APÿ‰M¼ÕŠwåÝÝû_ò&ïM·4/Ä=îÃþ)�o•Moö†·ÝºÕÔnµ™KÈSoL<¦tô
\q¥l×QÊÂqHièûívM¨Š¥½kYeØ+¶Œ:QìöiÎþXÌÅ?7ÄhÞ]˜EëûhµŠ^”¶_y5¹½]Çé�÷�æ™hÄSl§þVƒ:ÿsìJ¿Ù¤…–ÀNr)X ôœgýé^ÖŸŽN@é0Ð…ÜyfÞB0Pa­÷÷ƒ8±·Ã4žï"““u}w e¶ßú_œÒ-4Yæ‘Pæ½ÕǨ–
éæ˜	˜Dhvؘ	hG¹+r±tîÙO(òM„un¡vŒt;cÞ`|wdî@Ç´BšòO½ñ}þAvþ9=mö'˜§½'žR{ô^§f}_ð€xðœ¤FK´TrʵØ0åÚ6�OÓäÓJìwÀ¡;©„|ìûúôÕ"©¸ƒ%·!¹ä šÜÇÑ,^9o’G¹µhñvà¤úžo) ̯�2î ‰ÇGIBÀÒÀ5¹M’ôrÈ�9›7È…5ÈGòÀÈò0À4�M>'³—FCHû‚ ÄA\{†ù?�QM±ßhªyÍ£�ê‹»û¥÷‡…I¤¨Q^(@úàdàåûw|Á+t䊋Ž£ p\Ÿî®x#7àΟ֎¦þsö]&³8}yŒ×Âû‘?
¢*·ÙÏY"âj�Å…�¿Úa–žÓ]?\zXÂ,ö#Åš‡Y4J8/¹öI~`‰»èÇ�wq„¢×ÈKw#/ÖóÅ�WyqODª0h�³JÎÛƒjòI 1ò™ÙéE�„X='hÀT“¹A�#MÛ$YÍïæÂ‡Þ:V"*3E+çö¶RX_´JožV‹7ßÿº(º[Ä?.~»ûøa>Û¬dûw,Ä­róߟҟß*ÀÈŒbi–a̱4Œ·ºJؾ�AQ8ëX½V<üè}{¼tÅ!²¬¸·ø©í-œõÀYNq¹®i1FRRàtFwN5áWµ´ÈIq€Â›M©qæwô¦È©/1€½Ìºw)€=Å}ã´Y^å™ÞHÇp§ZkÝ
;ª†¸[–ÀE¦ñn9â¡·îKÍž4¬ð0\Í ƒ�✯Ó�ɳ%ŒÝ:ÚèŠû“•5s5é¢N9,
(Ø¡u�³ßEÕt"sÿåý·…NTë´[tÒ[#v¿…Ì`'ÒLÏv�ÕÌbGšÁ^4øÝ¢
Ré`”`'Y‰¹¥�å±TÝ1¥=¬ªrR¿ªœ  ;ä›_M¦\Õ)!ël”‡Y20eì°¹²Ó³1VuçÛÔ]h…eP^Ö]‡Ç˜ú�¯Ñ‡¬‰	(¡5LåqÏ©ÁÕýØÐ<óqhô`9*½=Ô‘ý§e—ÄÂ1Rµ‡FÆëS„V׈&úUæ#oD<‚ÉæìübOê0²È} ãaõ”R°À£óÁÒÆ­j]©é£´Q�8áPíëIãÝIiQ^4fZ¯¶”àåûwBª[#Ï‘™nÀÅU¸ÔRð†UWy¡¥e7�'7œóÜôM¶£Q®F$ÌFˆd@ôDF®øÃ
»ƒŠT�KÝS�¹-H±Qƒ	tÀ¸dÐêÎém2¦Ùˆ(*%p)Ú�cí3ЂüKš®~Ž›VÑ@Ly{ÇQÔY¦™42·L)…Ú‡R²ÉÿޤŒ#ÞÈ%À˜Y˜›†{\cÌó%Á_†xÌCª5gôõ¬t*Ò×�×À�–ìUá5¢Ï—yÍV¹¯H+j¶¢•xãðjòæë<½w"!æÂ‹×NÕšü )”­‰øW“Çd-ùûʼnÂŒgö^¹žøƒ 9	Ÿ‡Í&ä«Çoj P@8µÕÒª!¼‚Uå³ÜªrÛbUõEYÁujý
âØzùhC8pQ£½AZâXZ9¶³¬^Q•£ÊbãôW¸2W¢´Y†Rº
wU\Ì}——õX6;´ÜFtîVæ�=„eϲNš¥ã(åƒìtûÒO+�40í
ä23´z-ïãx^uqb-M»Klî ȃ\;±}}CÁ±¼¡#wP‰Wæ öÏÆévH™Ó:êÁR6ôNš$zÑì.0ýµ˜óaB½CPϬ2ÙÝa³Sµ!‚Àn•�¹Ëý˜æ9¶™ûˆ‘]à2Ù‘öªâ¸v&«W»2µ:feè+ ^ÇX£OÓ"­;_
²4á4»Va©šx6®XG9tQ¬éÓ1í»Œòo” ^²/×D�½MÞÏHÕ”¢ºÈG{›,Ô]<+�ô!ÿˆf³ùònø©OéXî%A­ÈyÁSµ+—Ô#T6[÷HAåê‰Ó"55jê¥L+Žçu½©·ÓMv5”㞀iÑ®Êø�	ºß¢9$ÈÙ�#˜ú§5¥X®{«%kqbÔÁH#æ”�\rÊ;Æw€¬Ò�{
ȵ\’*\&?‰'Ú{±[=¨ý€Ý^*+Ù™cOâÌœÏ×úè‹\>´ü4“Ê>F[ ¦“>÷òžU7#kæÊqÂòœÍŒ®˜°ge¦Ck­	o`/•±ù] ðã 7Zð ¨hp©“l*¾ÃR–kY]é|¾M@•&™:Íê;Œ}E™Œì96€;B$gf–d[wóÇŸŽï;ò™ðì3*9}ó«K)d�Þ=ÁY_§6°N)3(ך¹97[–öÒ"–¹æ’qÔÁ"×òÓçvLj“�à°!2Ý¢ãÚ]˜ÐÞ@¼ˆ$´t°Ëy�"Œ¾Il4¬J.	ü1ÅH·ÌwF†{nWp(*¾F¶AB&`TÐLþ_û"À–ìÏ!¯–g¶5y5oíÕ�ßp×�­*û«Q³]“êöêÚ¯&殇ÃûŒÂdfT·ªxßíš]Íý5f“éF>œù¿¹ð±¿¼±pËë˛ˀ“j¿¼±hsç•r _%ý.!Þˆ  ª_vøö´›?ÄGvó�QŸ
 Âï¼¥—!nt†Íà0Óv7?ÍÎNÊ÷¿éy•1TÇ¥lDÙæ‚˅Јfå#»PWX
1ƒ%åp:ÓC³ˆŸÚFžR¡æŸéfd—ÂÃ
:`÷*1$ª|,�B	,.s­ÆWÊB°@¿áË,dž/ôï‹ù:µÎÜëM�-,Ü@Qx`ûSHÀ³ŠzÛþÄ<è|
š%›vÐDYóÂ[¹(ËdÈvì	Š_LIyLT¿È
h֌ܰ•ÚÎv&È€+kÃæùÚ•µ½:)�€\Fim¨�Ÿ³‘˜�rÔ)!êcÑÅÕÖîÁe3Ô+ëä*e��œb_@''$
L“¿&‹§‡åú Ã¹òu¬ø:Y÷¤/]ÁŒ”7Â#ÛŒæ6¸ì(—ú-rq`cí9
�ôÔGºÿ=å„ñ1õ=-UmuÄö’H;~º#Ø9¼�D©OZôôh—³øýaG/2÷e·.§íT“o¤lOK¶”Ñ1
=n*P!z9÷´®÷Ù†-h+¤G¬+ÒÖ4�mƒ”{;ŽÇõ}4B�Äî	­»ï¥§ÃŒŽXÑ-y@É)©2º[Úµöyh‡Ö=*ª+°žWLæn•­¥/²¦¢HÃ
/$„Îø¶õcý`I¤œÑ(‘€,	™M}eI`‡%¦áž%‘W=ã{Ë’Ö´oŸ
HdYàY /r
ò=—C�ê+¹k,Íbeé‚(@Õ"¤*ð×ùîQ€1985Í=íìܤ¦/%'Ê}sØpUN‘ay«ÄðV!¨è9Ñ¿ŽÖã…ë´¬äQ¤JÁÏc:ÚªàÆçˆ|ˆC/a
‚4$:H/ºÇÛêoLfÁ‡cŽiè¸âJ…ç4òƈß!¬ÄƒÀ
©–ÛÛ8ޢލ–Þ…~õcÃP ¡PóÏ84ïÑ•*†^øîЭ{l$i™Õò¿Û
IºÌáûÙ±á
%bõ�ìˆìd'| ;9TLö£Lì¼ñWî£1Á/Sû±ì+åµ5söÌ…½ÆCPÑà2ùœ¤iòÐKŽŠcʅ̨tL±rêVµcÚiÄÕŽÌa¥ÉtcJ¡aGæ:�¸¾ÂWç8.ä¶·ÐéÁ†’„N	Ð÷/‘í”SP4�e©f½%»Ë²38Ñ"\Ü7
‹g-2fØH¨cÞAç� MPí$®Ÿ·	%ˆ§É*‹Ÿ9ÿÜìäE	gE¸"yºzˆìÖ0']‹ÖôãJ§D1
@&€�ÄãVôÐñ¼ÇfsßóæÄ(o¦^ÜÆüC(oFj	·à•»²è��ý±˜‹nˆ©'¶fÑú>Z­¢%&¯&··ë8½ñÎk?ïpR'ƒýUœ„Ö\‚ ›!‰B¸!V ¨hp™ÈÞ*§¬¹¥<â…ò$¯”<îN´‘l€Àå"3›¯øÚÑBÈ]àÆ°¦ÍaÎê–L¬r‡vX"=Ч†5û1yn"]ºE:ŒœÈjဃÚ8…v¡RuàVK`TÙK¨à$ðlVK`4Û­–.“OóçxÖkÁ"G—‚%“ŒwÙ‚¡þªCž¬@¸�.'nHí“,ÙNuÉþy½þ"þùÿ÷]fÛ0?urwid-urwid-69907e4/docs/manual/images/urwid_widgets_1.png000066400000000000000000001454301526073734300235660ustar00rootroot00000000000000‰PNG


IHDR@h~›0zTXtRaw profile type exifxÚÝ™Ync»†ß¹Š,�S±Èåp²ƒ,?_ñHvÛm·só–ä3‘¬á(·ÿõÏãþÁOÎ1»,ZK+Åó“[n±ó¡úç§ß×àó}½?|L¯£_Ž»R_#ïvÉs™¯åyïã¯Þï¡óI~yP�¯ã뉖Ÿ÷X¿=è5P²E>¬×ƒÚëA)>'ÂëýY–/­ê¯KûyÝÿ„�?g/ç}Þ¾ÿŸ•è-aœãN!y^SÊÏ’ýe—:Â}­ñ9|?óÒ{&ä§8}ü4ftö“Š.ú’•�Oß²U^Kuß³•ãë’ô-ÈåãýÇã.ÈÏY¹¡ÿµ~>ËäËñÓýyfô-ú7øgÕs×Ì*z.„º¼õ^âýÄuƒ!lèê˜Z!;…ª¼Ûoã·RÕ“RX~úÁï-DÒuB+ôp¾ï3L¦˜ãvQùãŒé¬Ic‹óf2Ûo8QSK‹<Æ4Ÿ´§ø1—p‡m~º;Zeä¸4¸å�ÝŸÞp޵BKRžüÆhÁf–9{å22Î+¨rüþýþcyMdP,ÊÖ"�ÀŽçCÂ'¤›èÄ…ÂûÓƒA×너¡…É„DÈZHJð£†@ +	êL�Šƒ‘¸˜dÌ)rC'14·h¸—F‰vÌÈ„¤’”Ü´ÔIVÎBýh®ÔP—$YDЍTiÒK*¹H)E‹�bפ٩hQÕªM{M5W©¥j­µÕÞbK€¦´Ò´ÕÖZïŒÙyrçîν�8ÒÈCÜ(CGmôIùÌrÊÑSO;ý#k¯´þöûY¯¬Å›)»P?²ÆQÕ÷#‚Á‰XÎHXt9�qµPÐÑræk€_,s–3ß"]!‘IŠålËÌ;D9á�;ŸŒZæþVÞœæ/y‹ÿm朥î3÷{Þ~ÊÚ2š7cOZP}¢û8¿k_ÀÐÈq	&­§´žWËñ(ßB#\©N�t
¤pæ¬gîQØAê©KyŒÌ±Î;3îÊ£M)uÕ=W›®­À´õLúê›’M©–f�	½®³ÃÔ
ö•Õ×gL¯‹¦>³�¤æu$ï¶°t\ûn¯ñשc0>­®§ùçq“…ͰDÏ3ÏÆžO$MËY®Qá©hàÖ¦IDs˜ýŒÐ:%Û-Ç÷}&�?—¹pV[àõË÷åÔœcô"L²Äg’?M‘€~?îÞ'Z#ê[š–DõÄ~v_=nJ&,J ï®DúܳFj•ÈD^)ûÓ*NÊ>Ÿ‘6•ñÄaÑ=o
2F�Öæo™ù¼Þ}ÞÀâJƒnÈ´¦Sµ[|ôÉ�Ž\õ¾Ù…ç%{OǼxPÜv(†³i¨$ÅB8Zg‘
§J>’$K¢èŽÊ‰:«¡÷Mµ•CWÄч¬°¹¬F?ëî`E
û¬Òoú+…°ã?°_.üEäÍ]…÷êõñ<õ×g~}¢GnÄD‹Î;&3ó°š`i„¶µÒ
?Τ
…UüÞ´	Â\±¦¦(H¹ÎLÜdÁÚ�—ÌÁÊ1<>ºñETd˜ã1ÈÛþ�õ]|�Mr�X5
	sËÜÁ>wìv;ÍŽ•|–Ó0ðH͸‹NS0d�,�¡ˆ%
H’¬¦A{œ[./�6‚¬¹­�Vd´_D"û²!)yl4c×iL'±{A³¥˜ý,7/𑹄Ê§ÃbHæA& 8p~Ÿ}"«&UaÀš`±óÍJ.Q=&1óFÞö…ªíSPTtRHt¼�‚–ÂîHXÆ@(ˆ	øtÁÝÒB^¤äá	—åjvȯqpÓ
:‚¹#às«¦ý3Šýmr@€:£ýÒ9�îMHµ¬+ SXµFYæ°ˆrf*sçõQ˜ë%¶ tòM‹½Mó-ÇZ¡iÀÜÁIR›§JbZsL⨑xXq5ÉoÛ4,ð‹d͙ы6AÙ+ã˔¡‹ˆëÌåûÇC¾eô^†U2.8 ´¡ |�ÆsñÔ¦°rØâ¬ÔϧA.š5€+Ÿ2�J¿ÇR@&Ôòä–¡ä×�â4bƒÅ
µoSŽUÝÆVÀ¦à„@ìPÔ¼†Iî³à4€z³<ù`¬‚¼8q›¥aÉg´ºàO~pA1W&’NoÊn!çPÀ�¼AÙPÌWL€f´k6dE¨<2Sz¤K#ó~ÚÛƒ"–æ2r±±~¼»ïþú;lB^HOjÇ{„3(5l0Oëhã¢`±®Bx�éiÚ#ÆÔh nz¥@I†Añ8Íi…ˆIƒ0H÷Fˆ
=f[¿-¯d¿�vI±|Q�·M¸1jv«^rA DÎTœï½!ü\ž&íPl¼<™6ÒÙ^ܶYÑŽÉ  ¿ç€§©¸Eê�ÙÓ@‰1ånûhÕ÷èüûÝ,Ä3��P¯t‘pAÆwÒÚ`j›
°ÔÑêAc<$6Ñ~~<.uV
´ãOƒ™(Ð4m/("Ù!Îèç°}¢Ìa@ð¨Í¬î›¿nÛù=º÷‡¿ûþóƒ¬v=ˆ#XºaÍ”½xŠ˜âEäƒøÙ‚ðDÌ)ÜA)KHd ÀùÄÀs#_¥¼mS屿½0â�¾»ÿO;míñòk0|+Ö„¾&¨ðµ(·X%¤�5·Ä`tX“È�ß5o Õ6GÅÙ~'"…Å<ÂìŠDÖd F�Æa°<Þ£±/ËV¨~¾»ò#-1wS`z€ÃÛ†gÍBa@6*�J ��¥b›W�ïßóÙÒ»‹±ÊÙ‹lØ<¢1{Pÿì¾Enä¢�.>(‘¤b*'RÓ
šг@¢‡�ŔὮwã<nÖÃÁ“ +ÿ„5æI‡ÕD8NèSLǦ…4ö1¤“lßÓH¯çGƒÐbpv²ïq¸ÕF_ˆ=ºšvÈP,³lÐyâ�ù)ûn %
s#ôßqŽgqeG—
©©�*B+Nñ0-æ:ÅJqJ"ÇÓ`Š8Že”gŒœgê±€€)ØnEe¦Å+¿ŽÛH%Ì Ï–!Œÿ¦ˆqø˜Ô_*L�eâ•Ð�d˜ ¤¹Y4–€“u;LÔÇB¦™v¦±aˆÞpkÙdü˜	*)âsWw«á7âÇæ�ÖE!FóÁä,J¹…¶Z7}|̤+#ÄúíB‹!©P¡ÈÄèAÓ§”j�"66+Ù&IÅùÚÄŠâUŽmÒ?L™?Õ×NÃ÷	�X¶Á¼G£·¼šÝBæÂ�Û¾]ÂSê4im©”Ú–¼Ó–y¤¯$ˆà® i0„+‡ür�놓~ÈUèyÙ¾&ÑLòÊtŠ¥�*íæžÖ©zAE dõ`6o€iö}/)`šÃj…X’eº‰u+ŠÚ®K¯UJ( 'ÐÙ˜<ÕáÃNE~W^($´Ä §ÌB×á2G±–pŒø@4ê⾚ƒ™xþÁ£õzHÍE*@Ùþ6f³ïÌšúµIsî·ý2Z656û¨�ðØfjk
›Á$ÇØþ›í¾l–Œëßb¶…Õ%Û®¯X–£Ȇ@ûæR£m©•íZ£f§m³ðÒî÷Ø¡“V<ðÚ³ú7ÃâÈi&LS›ì†Cl't–0(`ÔéÇ«¢Ë£}�ÕÛŽ[
™XL¡ø&’<*É»_�€aôN1€1Mx…ƒ¤®
ß·9dªÚ ¦�á;”A=S#»æ~•4
ä8Ñ ¤ o®j”n{ÎÊ©Ó:ÈÁ¶æ³y2Mçp™íø3I´1:5Ö2Avëtìt°í+ªuäÒš¸lyÙßõPžÈ£‚’7ȵÿX&­^Í/^IŽŒ„3�íY2i5e¡Å¥QeØUÈXˆJÑ(�mR¶˜ÉRÛº¤ÞyÈÕu{ÙŒCÀ<&ó18�éøÇâE¿û˜ôª9W0K‚ãÔµ‡Ûfëz”€1ß5äþG:ëÿìA°ñjîß‹wUûÍOö…iCCPICC profilexœ}‘=HÃP…O[¥Eªv�â�¡:Yq¬U(B…P+´ê`òÒ?hÒ�¤¸8
®«.κ:¸
‚àˆ³ƒ“¢‹”x_Rhã…Çû8ïžÃ{÷þf•©fOP5ËȤ’B.¿*_áCˆbBb¦>'ŠixÖ×=uSÝÅy–wߟկLøâÓ
‹xƒxfÓÒ9ïGXYRˆÏ‰Ç
º ñ#×e—ß8—öó̈‘ÍÌGˆ…RË]ÌʆJiTXtXML:com.adobe.xmp

 
  
   
    
     
     
     
    
   
  
 

                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                           
IÁøbKGDùC»	pHYs
×
×B(›xtIMEè⡘R IDATxÚì�wxTE‡ßM'	¡½÷Þ{HQì°
l
¢¨(ŠX‘¦€Ò{/Ò{OB„P’�Ýï�9›½Ùl HKà¼Ï³Ùì�[æÎ�;3¿™3gl€EQEQEQ”{@Š¢(Š¢(Š¢ÜÚÇKÓ@QEQEQ”{@Š¢(Š¢(Š¢¨REQEQEQ¤(Š¢(Š¢(Š¢HQEQEQE�¢(Š¢(Š¢(Š
 EQEQEQ@Š¢(Š¢(Š¢(*€EQEQEQT)Š¢(Š¢(Š¢¨REQEQEQ¤(Š¢(Š¢(Š
 EQEQEQ@Š¢(Š¢(Š¢(Ÿ»øÞŠ!ÀvàJ:ŽgN p8“ÆcŠY2À½)Š¢(Š¢Ü�84	î(¶=Øq£'!õ…‡íg�CÀ\ù¾�ÌZy€S7ñ¼#�@àࢇðO�àW`©‡ðn@=à'`%ð*ð±lÿ%�q˜úè+¨(Š¢(Š¢d4nö s@&˧:0H�ï·ñÞÁŒ�DÜäó.–ïFœÛN¤žMT4°U¶�‘xþ¦ÙQQEQEQ2–¸lùl>&HXóTޱa˜” 4Æ;PÈøzØçÆ<Ì~•óøË5s_GZ8Pca��àk �×-¼�\g).ÛÑ‹ÏËW¹fAŒÙ]ZñÁÌ�ºÞç›KÒóFñ•8—¹Î´UEQEQ”'€<±I¾/¸m/LÅÌ:ìb��˜ù;žŒÕ9ü•ã¿qÛï7ÙžËÃ9Úɱäš'åójîe­ˆ–Z˜¹>VË�E©ˆ¤Æn"
 ·Ä³“‡k=+÷z3º¶¨p•¸aæ�öËy'‹xŠ&¥ÉžSŽ"�pIÏ3ÀnÂò`¶üÿœœÛùi!Ûƒ�±rýÃÀI×pÔdNQEQEIøÜ¦k”E„†uþOjdÞ”}kÇ,éòžGSºˆø™tÅeb÷�ÄÑ“·Œ§AÀ‡–íßaFržÅŒÄÍ!zEÐy6V¼�öÀ �¤�“OQ38EQEQ%p³¥~/hÎOKÌHÌù¸�XÛöˤqn�c´üö4çÇž†øõ•†únâÇIÜu Hîè ‘ÜÏ‹¸+‰™žçÿ¤F'Lc,âÇÉ{xž/ô„%ÜzþxbD¯bL‡{wn{8�iâÀ˜Ãù¥’¯ìúº)Š¢(Šr/0eüH&}÷1“¾û˜Es¦}úÔ‹KTäIzuIÝÈé¯ß¾ãç1ïßðuâã8zpÏ=)€Ü½ÀåÀŒ‚\ÆÌEñä»ð‰ˆŠýÒ ?%Çù’Ü‘À�ç�˜9>�b̲ÒJYù^tƒ÷éœTרQ#`®õqœ£]�ݾÓríò½ÆCØiÀSî*.bg—‡°�¶ĘÄ�ÁŒ(õsû< ¢¦LÓÄü,‚ï(fä©­äEQEQ”{†IãGs??þÝ´ŠmÀÚeóîH\2e
¢q뎩†_Œ=O̹¨¾Î‰£x÷åÎâùÜŠ9@—Ýþ_‚q:pcÖõ5f¤À)
æ‹hXˆ™«rVÓí1.£­ÞÏfȹ^¸Ôî€AúG¶ý{�¸’kݨO±Ó—‰Z~̈�“Ur­FÀ$<ÏÿI�|ò�šï’6VrIÚy]Šô°­¨|—$¹ù›•8ÌÜ¡´òfÞO?àù\¦`Ìí"PEQE¹hz_gŠ–2͵Üy1{ê·Ôlàòñµo×föîØD–¬¡T«Ûÿ€Lœ�Š`ß®-T«Û›ÍÌ^øwÓJ²…†‘·`±dç?zpv{"…Š™¾êm–“=Gù
™~ôMkQ²\U|}ý)\¼\²c÷ü»‘»·R²|µñ¾t1–µËæâp8¨Z»÷l£XéJ‡˜&á…Øsl^³˜Øóç(_µ.ù
`ûÆåÄÇ]fÍÒ9T­Ý_?ÿtùln×¼ŒH`ŸˆkÃ}Œˆ‰êÀý˜‘ˆ·€!©ˆfò}S̈G`‹‘%xöø–,¯ˆèËyîi±EÄ5–ÿ—XÂ/`Ìá!˜ùGQÀ¶4œÛiž—#•ð°Tî-câ—š òt�Y$µsÿ”¿Nñû­S]„U8ÐCÄ­M‹CEQEQî%._ºÈþÝ[Ë[(iÛŸ“ÆòFÏ9¼'S¾ÿ„OµâÒÅX‚C²1ù»ü9ék6¬ZÀÈ·ž'08$ÅyîÙÆwŸ¾ó³÷_íÊ/cMŸvlÌY>ÐooÎ�9ÍǃŸr]{â×¼ÝçaŽÜÍgïôbÝ
×ÈTB|¯=Û–?'�eßÎͼ÷Ê#|2äŽÞÀ‘»x¹G3–/üƒÃûw2èùv¬^l|œÚ·ƒ„øxölßÀžíHHˆO·ÏÄç6^Ë)Nbå;ã¹m-)Gnü0žÝ®Æ~Œ—´Ò¸¾h‚1µK��@5ŒW²i7I5ÂŒ]Äx³²TDZ&i™ÿf̨¿ÜÂB0£6îì’ô¬GJgM<ì¿OÄgIï´äRç>×2ksˆøÛ¼
ATã€BQEQ宿ýWÃÇ×—èÓá”(S™¾o}	ÀÅçùõ›y}Ø�TªÙ‡ÃA¿n�™=e?Þ�Wßÿ–O·&w¾Â|5üU^~çk²fOÙw_±z¾øð%âãØ¹u-ª5`Ïö
8ìv¶m\N±2•	ÈÄùsg’޹t1–‰ã†óƈ	”«R‡Ýžl~Т9S°Ûùý|vo_Ïk϶M
ÿáó!4nÓ‰Gž@ÙJµùñËw©Ý¸-÷wz–ÍkÓ­çéþÙÜ® WE�ø®c^'B'ÔÃþžF9R›ïsÎM\¥Æiœ¿‹q2p#é±3ÊSã®{•
wfí"«hºSEPõÆx„³òž‹ýL¾? ¹»ë¼·ßî\ÁxzË…q-žZþÈnù}D¾«{Ø×?•x90ë¥åù(Š¢(Тܼòî׌úéFŒŸGް¼|$£0Gî6â¡Jl6Uj5áÀc$–· ϼ<”a¯=N‹v]©X½�Çóg
ÍEž|…Ù¹u-[×/£zÝæä/\‚{¶±uý2�Ç�8²‡ÃAé
5̵½¼(_µ^Rø�=Û¨Pµ~Òïâ"¢œìÚ¶Žý»¶2úý>Œ~¿«—Ì&üÄaâã2Ô³¹Ù#@�À–ßA˜µdÊHCøq\£‰s¶Ž˜5h~ÄÌi‡ñ<¶�”‹~îÀx‡›�IÈŽÍé‚Yxsé5â·
³VMOŒéܘQ¡,˜‘“ìÀ#i¼Wçz@-Dd|åaŸå˜ùLÅ®S…cFOVáZ¨;ÆK^	’�&-–4ì!i73RsfT¬)=±½ŽñÔ×_Òz*f~Qnyf�ȵ¿�ýO›1fŒ0£wñÀ‰ËF̺G«E,Å8B(…q¥©Å¡¢(Š¢(÷™ƒ	
ÎBPñ,t~²?}º6àlt$^^ÞØíÉ›d‰‰W°y¹úá·o\ApHVŽØ}ÕkT¨Þ�­ë—±uýRú½õ%bÏÉïeô|mdŠým^^ØívìGÒœ	»ÝŽL7Â××�ؘ³®Æn||2S6‡ÃAµºÍ(VÊ5£¥]—çðöñÉPÏæf�ùJÜù¹OÝ?I£y†Ûþ=1žÖÅŒFLbFT6{8ÿ"Ìú;_Kƒz¢4ì­q�4\�^˜µlBDdÌ’|7<{P»VA³ÄCøY\s~Ò:ÿÇÉ` Æll<ÆÌïIÓÎ|ìvÌã"fNcÖ*ŒÂÌ«²aUµT•}cÖÿ™|¼,é±Ö²¿CDÑlŒG½�"ÒJ1˜µ›—ãbæÝ/Ï¿‹…Š¢(Т܋ìܺ†Ð\yÈœ%Š”Ä××�›Wñs%�M«Q¢Le–/øƒMk1zÂR"NaöÔoS=oÅê
X³ô/ÎFG’·`1*VoÈâ¹S‰Š8A©r)
vò(†¯¯;·¬I^Û7.O
/W¥.[Ö-%æ¬ñ
·jÑL¯¸œJW¨aœ'”«–ô)Q¦
^^Þ„æÌÃ…ó1Øí‰éþyؤQ{§'§Æ,ˆº›k{
óŠ`F].cF~"þã½”kG`æÝ¤Çñ;_Œ™`.´ë¥>fäl$Ƽðjéšßr­ÿáZ’¦¹1.¶á2QTEQE¹pL_žjàCõóP²lUƒ2qêñq—xä™�4iÓ	€ùNà»OS«Ñ}ìÞ¶Ž�¬¡¼ÿå¢"O2ð™6¼9r"%ÊVáøá½¼öÜýùt
ÅJ§œ16†n­KÓ¨õÃôyc4v{"ÝZ—¦T¹j¼5j‘§ŽÑ«s¦,9
À¼?ñýè·hТ=;·®#köœäÉ_˜^¯�ÂápðÉÛϱnùßä/\’�¬¡ܳ�ÁÿBñ2•9yôï½ò(þ™¨X£!gN‡s6:’w?ÿ€÷_éJ̹(Âòä¹þœ9ëMOøöuøAíâH/H¹9ø{qþ˜Ñ²F˜ÅX—k2)Š¢(Š¢Ü:ô秊8d¢BhÎÜä)P4Å>'Žìgß®-dÉJùªuñöñåÔñC\¾t‘ÂÅË&íwìÐ^l6’Ü[»³çß
dÏ‘‡aféÌ»·Bî|…HHˆgÏ¿(W¹NÒ1‡öíàèÁÝ/S_?âã.%¹³8yì 8`³Ñ³Sm¾Ÿµ=ÉCB|;6¯&*ò$Ùr„Q¶Rmü\þ±¢"Npêø!J•¯��¯¯
 å–3ãÑmf¾NÌœ¡R˜…c;j)Š¢(Š¢ÜZ”‘™óûxŠ”¬À¹3§™óûx¼¼}x듉é&~7Cùhþ½«X¼<è|ÀݯaÜ…+Š¢(Š¢(Jª$$$0c—Øí‰”­\‡v]ž¿ëîQG€îN‚0‹½Fòßæñ(Š¢(Š¢(©s׎¥wtHI�*|EQEQ%%^šŠ¢(Š¢(Š¢Ü+¨	œ¢(Š¢(Š¢\M‚;®aþó³S8EQEQE¹}
på£&pŠ¢(Š¢(Š¢¨REQEQEQ¤(Š¢(Š¢(Š¢HQEQEQE�¢(Š¢(Š¢(Š
 EQEQEQ@Š¢(Š¢(Š¢(·€^È=E‚º
»|3tüÃê¶¿!?÷á+§kþ¸‹¹Ñü¡å‡–WcúÊpÍw1íë†iý¢Ü²òã6 ùïÎrCùCBÕÆ‹¢(Z~(Š¢(×/Â5îŒ8¾ás¨	œ6^EÑòCQEQîTiãEQ-?EQE�¢�EQ´üPEQ@Š6^EÑòCQEQTiãEINøÊéÏÝÈñû§h“ÎŒþé7v‡ƒßM¤Gÿ÷‰:{NXÑòCI3v»�OßëÃÓÕàÜ™Ó8v&~7‚÷û÷àÜÙ(�Ç?¼�ÎMŠòÛO£q8®íðÉýœî×tÿ­Üæ¯ýè1ìµ~íúfWl¶ÿî±.1ÑβU;(]2?¹seÕÆKÆà~ë�±“þà­Ñ?$Û¡|ñBÌ?ïkžÌT+WŠ‚yÂÈäïoò…ÝÎêÍÿR¢P~r…fÓÏÀ|úÕL^ü3¹B©V¥8
ë–å�ûjR¦dþ.+Òkâ©üÐ|�’˜sÑ,ý{:«ÿ…ÞHéŠ5¨T½ªÕÇËë¿ôåÙ(U®ay
âïŸ)ÍGý1i,?Œ~+Ù¶Rj1ô«éØl^ÿéœÊ�c­_ò„f¥z…R4©]�fuª’;ghªuÈõÖ9Š¢¨J^”ÈǧC»c·;ødÌŸÄÇ%ðR¯vd
ð£@¾7|þ}NÒù‰ÌžòFRã%>.‘ˆ£ñä/~ç*?×OÏGÚ‘G*$__ŸT…±ûf›ÍF“ÚU“m;xô$Ïί£ÞÖ†â]BÿÞmÉ›';»÷gèÈßù~Â?,™ý~šE‹§²ÂÓöø¸DÎ Wé„tW~h¾NΕ„~øü]V,ü“¦÷w!$K6¶mX�@ ¥+ÔÀÏ?àº;Øl6Uk7¹ÖN)·I/ÿ]{“=4?À–âœv»]_è;P¿„åÈξÃÇxsÔ8¾ø9ㆤ\‰"ë�kå‘kío³iš+ž9~
vGò2 _�Ž„ŸŽæ‘WÞcÙ„Ñw4~‰v;5~ž…?~BÖÌÁ)Âg-^żeëøüÍ>7|�]ŽP®xá»SµlR™–M*“˜hç—)‹¹w…žlM–�@ââ˜ý÷&O_Á¾'hצ&=iBႹX´|;<ò!=ŸnÍotÅËËÆ¨132l2?Œy‘re
òîGSˆˆºH�fƒÌƒ™8�¶­ª©øIß,Åm CëÆ”/Q$ÅÎgbÎóóŒyÌZ´’†5*Ñ´N5B‚“5&ú}0šU›¶3绑œ;ˈï&s�VO
�Ѣ^
MùL×N�¨T¾0‰v;!™ùèóYœæófüÌÊE³¨T£!Õê4%08Äc‹·i›N)Îí~ÎÌY²ë}›±Ö/Û4á‰�Cy÷‹ø~Øëøû%Õ!�Á·Sf2nò,þ÷¥‹`Ûž<ðÜëôîö}ïÈK–:'G¶,׬§ââX°r½Ç÷ÛY�ÌF—¶ÍÿÛlÖnÙÁô¯†âë£K3ÞMŒ?‰®ÿkAXŽä�XW9v*òŽÇÏËf£KÛ¦øûznË^¸x™Èè³7|�ó.Ò¬ÇËœZ1-]=ŸÛ2ÈápðÝ/éÐãü|½yø�ºL›¹šî/|Æé¨Ô.ÃO¶b̸¹¬Û´�]{Žóá¨ßiÛ²:mZTåÄ©hx¹ç}|:´;yrßÙ^Q?×&¬nû�Óº¯ÝnçóŸ~烯'�7W2z<1±S=æTd4W®\àùÎ÷ó^Ÿ'ÕÇÝBLÌENœŠ&Þ,äÍœ¦cŽ[ÊŠçu•ÇS)Cã~Z@ûÇF`·;xø�ºÌž·žÖ¿ËŽÝG“�{ü„…tþ3²gËL•ŠEøpÔ¼3|2‰v;‡ŽDСÛpÖnÜK·Î�ÌäǤߖáëëí±<ütÄbžô¾>Þ´kV�Ù‹VÒkÈ'DŸ�I5_;~œ6‡'^Î…‹—xäÍ9sî<~¾w£É? �A™Ù³c÷íÀápàíãK`Pæ¤4ýûÏ_þúã8ìvê5kǺåóy»ogŽÜ“æ2è÷Ÿ>gÂ×�#W^?zccôeÌ Ô¨Pšö-²në.ö9î¦cm4«S
€�;\ydö]4®U%Å2ói©§¿ÎœŸêûídòì…ô|{$‘ÑgèØ¦	Þ3N»ÝN|B‚>À»€î¶¢ÿS]’>¾©˜ûoÝ}€ï§Íá�…+¸xér’Pš»l-v»u>ÅßË×%säD8›wîKq®#'ÂÙ´coÒïõÛw³mÏ�¤ß«6ÿKxÔÊ—(‚�·+N;÷æ‡isY»uWŠó&$\á�…+øuÖBNDD±fËNNFºæO^Ž‹ç¯%«ù~Ú¶ï=˜´}ņíÌYº†9K×pö|lºx6·¥æ<ÃÈ/þàÑuøvt/ü}iÚ -ڿúMûhÓ¼*½Ÿ¹�­eبidË„—ÍÆ[:’)À�&õ˳gï	¦þ¹–G:Ô§z•â*~20Íz¼œôÿàç»òb·DDŸeÚ¼%Ô©T†Ñoö%$8ˆzÕ*ðH¿wR=O½jØwä83¯¡}ˆT.SB÷. rÃIÿ—,–‹Çô¡@¾$¦ÁœÈZV<õTò²ÂSyú#¿øƒ¦
Ê0ntO2g¢UÓ*4¾ÿM&ý¾œw^ï’tü±çøþ‹Þ4o\‰s1Y·i?+Öî&6ö2—ã¸t9�Æõ+ÐçÙ¶úsþ™üRÄqçêKŒ™0�öÍë1jpüý|©_½"{¿Å¦�{iV§šÇ|}–o§üI­
¥øüí—	ºgL¬‚C²Ò£×Œ|ë=ÿÍÛ=J›ö=È_ؤMÌÙ(¦MCÙ*õyqð(2SµV½ð KþžF×g^óg£#X2oe*ա	
¡Bµz¼Óï�ûÿ3gJ’	\þBÅ©Z§©¾¼é///J)È…¸xNŸIéÄ lñÂ)�‡Ù‹VñpëÆØífþ³‚²ÅQºhÁ”uꩨ³1W}¿›Ôªbö;sž·{?IÛ&uR˜ozyyáç¥>ªîÆM�Íû_ýLÇÖ�Ù°}77‰¿¾NHp ¯ò-¹sd§r™âŒÿí/¾˜0�•“¾¤h�<|þËtr…f¥r™ämâcá§yuøVNú€gÞAXh6æŽûˆD»�î†ò÷øØm;ÿú‘ìYC˜ö÷R^6†‡Z6d⬅øúú˜)IØw~é]N�Ž¢yÝê|;e/]fÐó�ñ@³zœŒŒ¢cŸ·)�'%
à“ï§ðò�xâ¡6ìØ‡ÃÁ†í¦£¡RéâMîîJq:†ÈÓ1üúû*~ý}U²°ð3¼V Þü(�žü€±ŸÒ+ç‰è³1D�9OÇ6M“Þ¯{¨‘T³A+F|?�…³&2ç÷Xðç¯:ŽªušrîlçÏDÑì¾Nd
 ,_A²åcëúå<òÔ«Ø®‘VçcÎr16†Š5$�,åÎWˆÌÙB=îÿÇ„/’þ¼Ï»*€Ò)NÏlžæˆe
¤ó}Mùô‡)9Á•Ä+lÚ±�!}ž 0S@І´ÔSi}¿C‚©^¡Ô
9‡RÒ?öŒ··){îkT›Ï¿˜,üÂ¥Ëýú¾û`ÍêTÅápÐüñWøfòL^}ª3
«Wdéú­T.Sœeë·Ò¶Qm–­ßJÑyX¶~kŠóÔ(_ŠQœˆˆââåËäΑ�ˆ¨3ÄÄ^dßác„Q$ž�Šï|ñ##ö¤C«†<ðÂर…«7²}ÏvÎý	o//އGRµý³Iá�›D�
¥5¨7÷7®C——Þá±v-y¦ÓýŒ?…7zvKWÏÆç6•@´mQ‰M*%*T0WR!é²5<}‡Ã‘®
?7‡ÔæØ,F^Z1Ü“8ç”-]€'zM§öõhÿ¿ZîE
‰‰v.\¸üß‹¦ëØ×Ï×??W‘i5ðóõáõ—:ЮM
¦L_Á³ý¾¢~�2Lø¦Ù²'+?rÕæµ«Ð¨fåd×(�'ì*qM½!w¯P p	zôz“zÍÚ1tÀãü:îcÊU©}íyIf-ƒl¶ÔEÓg¿,Mu~‘’>H´ÛÙ¶çAþ~©:©W­ÃÆN`ËνÄ'\!Àß—:UÊ¥9�¸×Si}¿}}½ñÑ9?w=¿Œxƒr%L}æçáyï=tŒ+‰‰Ô«Z>©|oR«J’ÉZƒê™ü×"º?Ø’è˜ó~á1~šñ7ÍëV%<êUË•LqN__jW*˲õ[¹xù2
ªWäTd+7mg÷�#4¬^1Å1‘Ñg9u:š†5\a
kTb�˜„nß}�ÚUÊ%™jæËIá|¹“ö]·m¹²g£Ïû£“êèKqq�$kHpº|6·åíË™#9s„ÀÓÝ[hÜI^I´'µd¶ï<»Má™n�	

aبi4mX�ÚÕÍÃõö1‰/vü*~î.²dðb4æ IDAT&$8�U›¶óLçÿ‘9(�£'#ˆ:sþªÇ9_Æ„+‰šˆw!WMU\¼±‡

àìÙXbÎ_"4{fNœŠ&ât!™]“�S++Ó–*ušR¼tEN;ľ›x¨[ï$sµÝz3fØË|þÁK-U�%ó¦áë@ýæ¦é9f
£~ó˜5ùk>{¯/ÅJWdãªô…Ì€õ˲µ›Ù²{?/[G©¢ܳ™üý=Ιó÷óå¡–�ø`ÌOÄÄ^ä³7û¤ê�--õÔµÞo§ù·¢”(”�@Vnú—¦µ«p%1‘VoJ2CË�#;òäâÓ£O·øûQ´@^ÆNžI—¶©›Ý6¨^‘o&Ïà«!/}.†‘ã§p"â4_
y9Åþ¡YC(�'KÖn¡ó}Mp8,]·5iPÝ*åùpì¯;IþÜ9Y¶~k2q5+–fÃö=ôÊ5oÖnwàåeÃ×ׇÌA™ˆ>Cö¬!÷–²Ùl<ѵòç`úÌÕ|óã|ŠÌEç‡ê“%KþµŽ_¦®äÓ¡Ý)]"6›�w^ïB·ç¿dÒïËyöñ4ª[Ž‘ï=Æw¿,äòåõT?�1ÿLú¿|ñBt{ ÞÞÞôíÑ‘�ÌAÌ\¸‚…0æ�Wè?üË«ž«N•òé݃_ÿœÏå¸xÊ¥bZ§d>þb6™ƒ|)Q,7/õü�ªG‰byx¬s#.Ç%ðóäÅDFÆðd·fÜ×¢*Ã>�žt|je…§í6›�î]š�;,“§-ççÉKhÓ¼
Ý»4¡BÙBiŽsŽÐîoUƒ™s×{!ŽVM+óX§FÄæõX>ú¿äË�“Ù‹VñÓŒyÌFû
È"C=åk///zwë@¡¼¹™±`ßÿ>›–õkR$|¼½)Q8?^^^ÉLóî²çÈE÷^o°mãJÌœHÑ’åé5èj5h™dØô¾Îd
céüé,úk*Õê6£é}�(\¼Lš®áååEÇ}	ÊŠ…3)P¸¯¼3†/‡÷×—2ñã´¹äËA�Šeùz/Õ¬|ÍFWÍŠeˆOH $8�ªåJ]5�\«žJËû­(N2øóNß'xrÐpÚ4¬Å†í»É’™g:¶MÚ§aõŠ|÷Û_Ô¯^!é÷‡ßL �S¶¤¶U‰"\Ž�§BÉ¢øûù’'g(>Þ^Η‡\¡ž×Ô{¯ï“¼0dK×maçþÄfË’V¹Lqž|¸
µ;½@ÅRÅH´Û)U¤`ÒÔàºÑå¥w©Ó¹'ÍêT#&ö[w`ñÏŸâíåE»¦õèÐçmJÎÏ çº&3Ÿ»SØ0ÖÓÿÙ¨Ü=å®ÓUñauÛßЄƒð•Óoyþ°ÛØl)çF$\¹‚
>>Þ(é3hù¡ùãjL_ž.ò‡Ãa—9©^òÛYæ¸~;¿JÚi_7ìŽ×/v»ÝÔ6¯d.åívíØl$r`Ì}°%Ÿÿã]²æÍw¶ü¸ECøÊé©®Üô/•J#HÌ �ÄÅ'°iç^jW*›´íÀÑ“lÞ¹—ìYC¨W¥<¾–¼xêt4GNDP³¢Yï-úl»¥Nå²WÍcÛö 80EĤs÷Á#ذQ²HW7n§FÅ2IBfÿ‘lÙµ�²Åš5„Èè³”µ,`z<<’/–3;åÚ>Îì±Ã¨TºXÒ;³f󎞊${–ÌÔªT6ÙY§ÏœcÏ¡cT,U4idéò7¢]‡ÎÀÓÆK†Çé¥Ë]TNÑòC¹Øl^XÛî�mèfäúÿ”îê½½¼’­Í“ô¬›‡:ÇKÝV+nÔMÅ™†¿Ÿo2ñP´@Ц2O-wŽìäÎáZã0{Ö�TÏm¥BÉ¢É~—*’Òµ{]q¾à¤XÁ¼+è²\È™Ý5Z4qöB
çËÍå¸x†;‘’…ò'»†·—WŠóYÉ‘-9,£Jwm!jãEQ-?EQ%UlØ7e6qññ”-^˜_Hµ:# H/Š¢hù¡(Š¢(©Ò¥mÓ«:^Èh蘭6^EÑòCQEQîTiãEQ-?EQåžAMà´ñ¢(Š–Š¢(Êu"ÞÈ”È
»ÁVE¹™„¯œÞ¨.?'‡Õm?MSEQEQ”›„#é�¢(Š¢(Š¢(ÊÝ.€t�¢(Š¢(Š¢(÷*€EQEQEQ¤(Š¢(Š¢(Š¢HQEQEQE�¢(Š¢(Š¢(Š
 EQEQEQ”tÁÍXUÝhßÝØ4(š?ÍŠæ%ƒå�[�æ¿œ?|î‘LªÜÙ—[ó‡æÍš?4(7=8Ú½+[¶¶ŒñÚ;¢§èúù#{§>‡šÀ)Š¢(Š¢(ŠrÏ HQEQEQ@Š¢(Š¢(Š¢(*€EQ%}Pø’ß6àm 
|ä²ìÿ2ÐÈ|·„=<xïÕ,a% ?ÐÄÖxIþxÐVxKâÕèn	+|ø÷/ZÂò#�,@=`°%,DÂò€_	ó“s–
#€`KÚ¼	Ô²É9r[ÎÛh
À’–°'€NÒfÔ°„už‘ÿ_šYš¯j6UE�¢(Š¢ÜI#þù=hl¢€+ÀOÒøxX
\N“D4´Þ–‰À¿ÀT Õ€¯%`-0È~•s¬Æå€rŽ­˜	ÿËEt5%ì'Çõ¡å-翜Öí�¾r�q@Và°]â÷¾„}Tvʹs_IØ °8#×ýYÒ¦ÐX	\–sOqÖBDÕrÀ.÷3UâPøFâ°FÒ£€ˆÓ‰²MQ%]a“‚ùFÜmÜèñJúåf<[Íš?4hþ¸•çÖó€®@uà°„ùK€-@gÌèÄf˱Ó� ð°À6È#‚âmàKؘÑ� YžČv‰°qÒ+BÃ3ªâ¤:0˜	”šŠ(¬QÒ¨)	 §¾©" ª§%,PÒf¾Ü_Mà€„y‹€r\KGN¦Šj.âq®%l4P3Š5øÁöÐ3*õ§„ß²ü¡^àîÒÆ©ñ—îÝ`«¸;”?Œ¸Ò.·ZyI¡ê‰%˜^¸´à$pk}®ûbzÿë5¼¤BIÈ ù©Pøþ67`2I¥¼Ä²­$¦wòðH‡öÀIL�îà;LO²¿ÄéZzØÞ؆é=MÞò�x‡òóÝ*€ŠH^Gò×Î4¾Û^’žž~+w>´æ½D¸X©(è.VrG1#!Oz(›œeAkybp3
âþž­ÄŒ€âÝÂ~!”8ë63SÜ"T¬ÂâCŒIÜJ·°îÀ�"d~sk.¨¯œßJYÌh×p9¿•œ"â&‘ÜtŒ™Ü	^ÍÝÛ&À)/+\¥R¤ÜÕèÝ�~Ãî–?½Ôž’5ú²aÑ0B³g¾iYµno
�ÌüéoþçsÌ_¼•1ßÍcúÏý=†÷|uµ«— {—F7×3g/pöÜŠÊuGЭ6�óËçUà=Ëï€ëy.$·G¾Lð_i(•hF%¿‡Šìv&·•'EˆÜ	j[ȱ“/à`�ï‘ÊöFÒJ+ƒ1ó	þ+¥¡¤$§3¦7¿1f>Äiè^�û0&PNºIcZIø`æÙ,zzxžïbL´z`æYycbÖNı•WD„Ôªº…=Žéì
#e'ß}˜Ñ‘‹À#na5$ïí^p+<бyɃé-÷ñŠ[X ˆ—å"r¼Ý:RÞ–°çqÍ•rO›nr
+oŠÀi+q³ÒcbW…äó�œï’Ö÷iö¼9$&&2vìXMˆÄ;Måä©3)„Ýã�6& Àï¦^ëò原Œ¾¡sÌŸƒ¶-«¦y:†˜ó—n8®³æm ïëßßñçs«Ð)˜_fIAëüŠé�«o)¨ûâš°Y×p~Ì$Ë�©\§¸ÖƒqMx
’cž”
ÌHÒ³˜	²ï[O)X�Iܜ⬂œ×š#ÚbzûKÕY¡´“8¿f¹''m,Hù�T’Îÿ{cz‡b&¯6Áôö½*û9é"û
ÁL¶u*àŠR±>/qËâöœ[KÜò¹UŽ]å9„¦Ó2¤¤ÜÏ(’O.Î+ù¡�%WHãÄÓÄÛ àiÌdæ@Ëö¬˜ÑžgÝz΋j'ùò5iÐØ0=¢#åZYÜ®SSÈÕ®r_5$o•õ–MòPK¹fAÙ^x]ò�3ž/X.…¥‘WJ>]åøLÜ[””÷»~*áK$]öcÌŸœïV·²ÂùnW”cjKPK~W°¼Ëƒ0fKÎgSNÞãÇäËŸJ|J9¢M†ÿÄÀédØMòQ޾òlš3¤³Åù|:Jý>y—'ár&Ðè< bd2�Ùò\?–ã{È9óHX>Ì(z7ŒyÛ'@i	Ë"çyQʲW,�~ö¡ÄçAà!g›I:×&Ë»_3ÒåäKÌWc̈ñ»n'A¶áš+…œ£¤œs
®¹Rȵ”¸“k;[lõ$îJM¶”�e¤¬î$ù~¼[�£üG†Ê[o½Åþýû512Ï=Þ‚!;&}|}½©\¾0>Þ^\ºÏŒÙ눽p9iÿÍÛ±n“ëoßy”±?ÌgÊŒU\¼”ÜeÿÁpÆþ0Ÿ¿m¹ê‚À«ÖíáÈ1cëp8øã¯uœ7Â,>þ
üµ‡ÃA–�@JË›tœÃá`΂M|ûÓB
OqÞ§Î0îç…L›¹†ó±—˜1{‰‰ö¤ðc'¢øqâ~øu1§"Ì`wÌùKlØr€Ság™1{slºkPj<Žr_)•È[Rp'`&lf–Bufþ,fâébçzÓ;»c^”M*±�˜!vŒÍ··›O¤ñº
c*Q	ˆÄØLo’k\‘†æKÀ2ù~Z®×	cÃ}Hz?“í;€9þ�[‹Hci$
²œËÙë8\®³@*ó|ûé=Ûë ËýŽÆØj·ÀL@u6¨•Jê´ôj:…Ü©´7`Ì#œ#Ó¤¾ã*âòNbÃØÝo•o§9X3ilÑ3N¶×&HCr‰Q±
3ªsÑ’—¤Ç5BDQ7·B6IŸ89f³¼7Á˜Q_àË1ÁÒèZ†™ÐÍÃ}�”ÆÁ2i¼¹�v]’<´G®yøð­Äµ�¥‘%
—ygŽH~Ž´äç„{ ž	•çµøTžÙ†Tö-"/`L ÖÊög0=úH¹ÑßònŸ”´<‚™×qT~Ÿ’ÆåT\óLœÝ´UåÙdÇŒ$,H%>“DÐ/–<Ն佸Jê´‘wì1é¬xJʇG0sjÞ”g'�"y0žàŠI]Óã`˜¼gäCa‚”µGEଔ²6PÄ )—æ‰à™ eÁD©�þ‘:hˆì€1§](û”´‰’?FH^úDÞéG%’²9›îKRo¼+e_wúÏJùø¨¤Aé@yR:B%7�ôª"çè$ç(â€t¤Œ•4Œ–¸…‹è•8?+eÔÌ<¢o¥£eŠÔç›ä^¿‘zIóó
pæÌ~þùg"""èÙ³§&HÆápоÛÇœ‹¹H¦?¦Ï^˯|ÀÞý'iÞþ½¤}‡}:ƒ¶�?d÷ÞŒûi!U
L�Y´ì_*7ìÏÒ•;ùhôŸ¼9trª×œ6s
Ÿ}ý[¶¦}·ŒûéV®ÝÍÀ!°Ùl¬Y¿—o»¦:öèù%OöþŠw¥MÇ¡ìÜs<)ìàáª4Àï®aæ¼
<Ðõ#Úwû˜¸xÓܘ9wµšbÙª�,X²�*
°mÇ.^ŠcÿÁpΜ»Àêõ{ذùÀ�}·éø~Ò0A*‡vÒ›ÚãÉÆÉTŒMòón=¶¥®Ò›ëÖ‚äfUS¤G/Ó«`étÚ=ÿNr“¡­Ò{[¸ßÒ`þ—éB.Ó¨ÆÒ�ñD)9>³T
s¤Ò›k‰ûK/¢³G°¾ôþcé
\€ËÌÊG*!o©ô¬“T¿—ÆACµå3c(
9k/ô„›œ7ÒrŽÂã0É/^Ò£Û3Jc½·g-÷´[žÅs"ê�û+,ŸL˜I¼_YŽùã¶µTÞNZÒwµï…±w·"ù¢¯äçÈc¼EÔÖ²ˆ£o$Ï{K|ëJ<“¼çÎ[skî.ÇÔqä#acä�±ÿþFê# é¥ì¸Yç˜$⣯EÀ¤Æk"„‡‰8™iy^¥£yW÷Êÿ÷[D6’?†[~ay?ý,ïd7·gû�ô�_�êÒ©²/ñœ¡êútrå.Î÷�;wvHz9BBB³fͺëï9ƒ¼ûGô”T?^^6GpP€#KH #KH ãñG;ìQ“€#|÷·GôG챟eJæsŒõ¬£r…ÂŽQôp8¢§8lúÂáïïë8¸ùˤó5oTÁñá[�:ÑSµ«—p}󑤰®8Ê”Ìç1sì¨T¾�Ã=Å1â½nŽÿ«åhT¯¬Ã=ÅñÆ«½žnåpDOqÌø¥¿£VµGôdžEÃÞÞ^ލýãŽè)ލýãÞÞ^ŽÏ‡?épDOq¼ðdKÇ#ê%]c̈§€ãÂñŸW"'9òæÎæ˜öÓ«Iá¯õ{ÐÑᵎè)ŽŸ¾êíø_ëjWM»k}n†vñ¹C™¦,ƤÄ9‰øiÌ]áq?°4�ç*æ¡]ÒmÛÙ¶Izr�ã�gð<<„1W©eÙ6Ãí|ÈyÒb^´[„Hl—¥á]DÂÀŒÚœ´3_gÿJã»°%ì°E4]1eÝŽÜqéõ�æŒón¹ïp·ý+Þ�¼pIz½q�ðäQaǘ'õÄŒÈý&®)ûz´KÊóp
à‹¸LJ­Ý[g,yÖeô¢Ž”U{µ�¬(÷¸Jt8¨Y³&9rä`òäÉôêÕuü�q˜5é5*•7³üüR6¹ƒý™úÃËTjПÖÍ*Óï…¶¬Û´ŸÀL~îr²u&–wÅáp°yÛ!FwùmiÞ¨·xM©_»4»öžàtÔy.ÙÎàW¢ó“£¸x)Ž…K¶ÑÿÅv)ŽÙ´í U*!{63+%{¶`ªVtM“Ü´õ O>Ö4éw“å\
Ô£‘œ?Ë´YkøcÎ:ŽŸˆæØ‰ètõl.�¶?å·ÓcU}ŒÍuKEõ¤!NÊyNfc̃¬žoþ˜H½ƒË]»kÄÉz�ó¤˜ÑgÓz¼'–azèŸò#¦'Ú¡3Œÿw0æNZbF”
I\#e{c‰g"Æ¡ÁSÒ÷ÇØeÇYîÅ9�7+Æô£åÊrõ¥a(
Ù¯E–Fê˜ùïȳ¾„ËÌy?VÖ`Ì[œ`ìÖ‡¹åÿKr­-BÑ]|Û%íœ^›H£ûiЖ°ìë'áó$Mç¹�ë„4l§XǾ©ä'kcù7I‹ïÜ‰Èè,û¿%ñ]yËŒyÚ‰ó1i®ÂŒRmǘEþ‰1
r`zÛ+IúÌð 2<±
cò·TηZänI¯5òIm†é˜–O$oU“†ïL\"
מ"P»“Ò%/˜Ñ¬ÏäØù¤tÍ‹ÜS3y>UD¤5”ýÿ”û”wã	¹‡a¸Ü˜OÁÌypæç{�]’–•0&kþ©ì÷„ˆ��˜\çB‘DlÏ'ùjöE@/’wg¹§E"”~•kþ#ùdðuÆû"ÆÜ­©\?Š¢(÷[¶fðûYö×»”/S�ÞŒ1Aͪʼn8mª‚ÚÕK$}ŠÉ�Íf£z•bÌ_äZUÃú¿'š5¬À°OgP­rQ||¼iÖ¨ïŸJÅr…È–5(ÅþÕ«c뿇‰ˆ4qˆ<Ãæm‡’ÂÕ+ËŒÙë¸t9‡ÃÁ¤ßW$…)”‹Ü¹²²wÿÉdq¯T¾0ò…sç;¸³¡fvæ&ÜK¨4öÏ[¶ùKüâoð¼	S£ÛM(ftƺÌLoñQÌ„~gØS˜UÉ�ó{<Å7¯ˆ+«+?ùÄÞäg{½çðÁ‰ËD̨L^�îë®ɽF^G|¼1ßObzÜ­yñ×·¶K.Rïé•´¾9¥ñ{áz:”$_ŸNe÷ݸª
3áü¿ŒÕ‡Ü¡r"½¢å*·<Ü+æ`»víâÁd×®]÷ÄýÞ
ëyçè̺…èZ©ˆ5ÃâÚ™ðÝßèOµÆy­ßƒ<ѵ	1ç/Q¥ÑÞy­�ujÀ—ãæ1è½_iÚ°,X²�GŸùŒ2%óQ³jqöì?Iþ¼¡|ùñS$$$R¶ÎK*�“b…Ã;êÙëÏ`!Tåæã@Ûݶ;Ðà›ùrkFÑü¡hþPT©Rt}hÉŠT«\”à äË^.^þ/õj•&úl,ûž¢nM—¯£Ç£�8Kõ*få”#ÇN³vã>âã¯P¦d>ªXæá=ÅòÕ»(V$Œ’Åò°÷À)jT)æ1.W®$²|õ.*•/œ4â³tåNJ•ÈKXNc}žÃGO'lKWî$<ò,
ê”!úL,YBÉ—ÇXç'&ÚÙ¹ç8Ù³³rín^0ž“»¾I:6úL,+×î&úL,E
å¢N�’øø¸BîÚ{œè3±Éî_�r5²aF¹ÜG*2aF7bµ£hWÑü¡¨ºÉá`Ô¨Q8p É	BõêÕ¹ÿþûU¥st7s9.�±ßϧ^­Rlßy”‘_ÎäþVÕøð­GoOþ¸	È%£qÏfZ—n²øQEIïƬOã4b·aÌ€kaÌP?Áå¢Ì@-0F‘ÜKãSÀÃ˜Ž¤wH¾�qG	3W´‰%¬%.�“Ï‘|ÁæÚ˜¹–6ŒS–î–°2ç+þ˜G_´„åÅ͂̕qpbÙ‘°üo—àr¤â'ç,‹™s:×ââ6Œ#ž:˜Ž´‘s¢Ï€ IDAT`'ý0sÚ0ó	KZžÀ¬ä…™[ÃÖã…Œ£ëü¹æx^”Z¹!°víZ¾üòKNŸ>ͨQ£œâ@QîÞ^^;Å�áS™ý÷Fz=ÝšÞx$CÝCF@Þ¸ŒÙñ¯rN0ñŽÃ¬'•CÎát’²\DWC)û§bÖ�“ãú‰Ðò–ó_Ä8ÄX�Y,¹¯\cœ¼ûÇ0¦ÐÕ$ˆqÊsDâàtò1ã
u¦#-ã-Õ†qÂÑã粜{²”[-DT-ÇÌ›Ü*qÏŠYŒõ‰?G2¿b–(,÷±F³é�ñÕW_Q¢„q6Z»vmÚ¶m«‰¢ÜQ|}½ùøÝnÌšôSx™çŸh�—WÆæÍ®˜Tfõ¤À®ˆkµö[AI©lºmÏ�ñØôÓužïqLà*Œs;¦n¦§n%f5ðÜŽŒéés6¨×“¼‡îV�^LX˽úKú9;51^¿Ú¥³÷ª•4¾œ.ÔÇb<Æýq¯QÓ›ÝáÞ§š8)é!ë0ž»b“uº?÷Áx¾Ü‚ñà×ãÐÉtŒãŠVo¢,ac0Þ:+bF•~±„½�Ý1ð‘%ìÏÐqŒgÂ~–°6R¬Ät@v²„UÇx뜉q­ß—“›bRoLÆŒ>Õ�äì Ú(ñèˆñét’(i3_î¯&.÷ûÞO†;世"ŽœL1Ô\Ä£u¡íÑ/še€¡ï‡N^ÚJ'ÍŸ~Ëòǽ2è½÷Þã‹/¾`åÊ•+V쮿_5�S®š?n‚	ÜítÊm“דùV ÆS›§°Ùî6Ó»dÅG
ÕDçñ'¹÷³$!KÊ5dœû§F^Œ©ÄOi<—“§0n¾�rý#/ZÐ�ÜWF¤¤Üc©ˆ¿¸Žt°¦‡sÝOyÓ'
éuµ¼b½v#`ŸEõ"¹Ç¹«�˹&–Ã-~A¨‰£¢x"VÇy׬k?]^ô�›øAÂŽbFB¸…½‚ñ¹ÚMü �ú'1ó1?v<-Âɽsffé¤\„{½œÞÓ=EÄMr?`F“NˆðúÁ-l¸Ôuq_å&0hÐ råÊuOˆE¹m
ö6ÿfý•¥˜ž4'Gq­¯RÉí˜b–Ji¹úÅp
µ?Š«gé)ü×bz»œ½âÍ0k­‘°Op-þÚÓû7³vŠÓ¤¬¡\o¥Äm©‡û™,ã̈„/fg‰|<B¾�éiœ-ÇmÆeÒ0
¨+ÿŸòplOLOÜ¹NˆTò“¥rž‹k­—â’så¼ï`ÞHË9^Áeû>Bž—ó98Ý‰ìÆ˜©|‚9›!
žš˜^ÑùòÜIå¯J>š‡YǹæÑ‡òìWK©G*Ç?†Y×g)¦§×éÆä ð­äÉy»{çÚB%ž¥D°Ýï–'—ȱN˹3›rlÙ^³>Òl¹FmÙ^³¾UF.;œçÐÏÝû¹yÌGÊëe“°Lná3¤L?Š1?¶2FÊ�Ó˜µ�ÜËì�—èUÝž”÷üfôÈÊ}"öy(Sjȵ¶`Ös¯óNK}õ¹[XNŒYÚrï} fg™|¼-aÞ–í;pÍ•rò›œó8)M�GKÙ%q³òšÔaQ$Ÿ„Ô‰{¥Žl{«Ëåîä&å�ÛQê'cÖ/ŽÛ!€²Û¤—ÜG‘e%,ÁCåa­˜¬î$¼ÜP7KÏÛ÷˜Ñ ç{åÿ–":²Èï߀ÆrÌKeø.3…Řa}¤ÇГª‰ÚÇÒ¨ýU®"½lžF×þµ\³¿4þÓ"€Ü·‡Hº——ߟJÏÒ¨vNÐ-'½wéYÕ°¤å
ùdÞ•gŒˆ=é1Ýàöb-‚f.fR°§’µâîj\¿YòÍvÇúJC'Hö›�Ë$ò$®EoÛáZ„ÔÙ;ìd,ð€¥—· ¥§ô9ùÿ\¢6ÆŒpºÇ½’t
ÜMHQn4�
—N/y'¾µ„õÅ,<í/¢b.³‰Ž˜9DÙDŒ¬Á5³f�¯"böâšsZNþ¥þ:…1•CʨpŒùZUÙ¯´¥.< dEDìÔ“0?)^‘:â(ð�„Ù¤l)eã6ÌHnõŸ·Ô_VK�¤lñ–³ï-a½0óy2£0£Sδy3‡(»ÔUë%ŽÎ²'3ǧ«Ü“³.#÷\EêÅpRŽt©Rî¤dðºåV ŽÀ—–ßqÍÛ9—ŠP@
ßLzÈR@Vï:¤±ØR
u,BÇéÝ&R³3¤òœ+=i{-û×K£úDF	¬=Žõo±:dùÝCîË&ÎËçÿÝNòv /¹—|Û÷÷å¹­À˜^KYM3>´+
D Î‘žâqd*ûq9¦pÒDÖ<4Ü"€œ¦2I£åj(�48°Œîüi@•-
%çb¹d$lŽ<Ïc*€@I´Á˜b…É﬑Q]D†s®¤¿”¯àm©móDd8G[¬æk?ŠÈ”rü9·rçOK1䜼(ï|€t¶XÅY‹Èí&ÎHUH:m¬âÌ)2*ãmqг¼Ræ·�rô¤Eœ…`F¥º‰@±Š3?Ì(ú@6‘qfÃŒ@&õ׌S'ßaFÖ=‰³÷0£ÝÞ*€@Jzª[nÇ Ãn=@,¢âžçý �ɼÒ(MÖkäÆ釹mÏ'…÷)ù<èv›|ü0s’R빺Brzî÷˜_®s3qŸCïV‘xÉ÷!Œ]÷©’	í3“�ÀBŒYEg©ÐO\#ÝSKwFH�æqŒé\•ë8ÞSþ=ì–§"D¨œ¸J<‘G0.ç#îçŠ÷�>¸Ì1KcLYE1̱t”€1‘-jùmõ–Gr×Ö9ÜÞýVne8n�LNʹ…YMÙÜæ|ŽËœía·°ß-�}Ü–á2I&';-i›I>õÉ�èä±üCr·ß¡ne�Õ”-§[ÚXMÙ
ºÅõ)Ëÿî#ðojU%=r;Ð:ŒG›g¥nKJÛgOü‚™;1L
àãר¿‹ô>•—©s"haÌhËnÌ(M3‰ÇfLOþdL�{ ôr-”󟘹KžØ!½Šm¥2š!½‡;0s>n�Ú�ÝØFòQ*w~ÂŒ¸}*~Lïezf!f­°3"6ÁÃ~á˜^Üd¿´'
“XŒYåÎë8ö¦çµ¯ˆ•nn�œ×1æš½,�™
ò¬àr„àld¬”|÷7f"ñ+׸þLOöÉ�v-¶EQEQÒ·r`¥ë.b¤Æs"4Rã+ÌÌîÒ€ýãà;	ßBòQŽ“¢³'30½‚
0î<�fJÝ1æMOcÖDpΕyc>qfT¢œ‡¸]ÁÕDâ¶@„Õ3˜˜&©ÜÓ—¸¼÷¬À5Ä;Ù¯ÔÒä	‰S5Ì\ë$Ù-Sç5–JCÝOD]zçŒéŠsÎ7á0
×HaŒi‡7Æ,d¢›�ú?{ç^UÑÄá÷¦A€ ô¡H“Þ»,€‚Ÿv,`AQQAEAЍ ‚DÁ‚ˆ‚TéR¤÷¤÷à~̜ܓ›Š„dÞçɓܳ§Ýs6»ûÛ™�Ùëqþöús	(áÌîþT‡’Ø"äÐ@ëĵˆkãWÙ;ú¬‡pÇ› ÷[[…Ë�.Ñz/âZÓXÅØ*Ý>q;q›ó~é½ß®Ïa‰nßLb×<Ã0Ã0ã<¸Úò%Ç'ˆò„ íM«Ï=öªÿ³w›šó¼ì@ÜoüV?Ãê˜qùû—´’(Í
N¯Ž<@ÆUÜþ„¤’/2o—³-ÈâTøŒÄj†a†a\]"›Ôa2.‘BÆ,@†½[Ãê˜qåö/Fê£Æ%iÂì†ñ‚À†Õ1Ãú㪪d—³’ö@7dáÜ‘äb‘Ès^tGò2¤·!‘º‚)<¡‡’8Bk$8Djo€.gýˆpý�“sGX3ÒVý0R_ý(Œ"Éè¿¦mk]2§kÿg�9‘@o‡…~@ÛòPí#Ü!³Û÷Ü™Ä�nš"áõAò¹Ó+Ô^Õûú‰×£–Br¥CÜ<é*Ë‹ä$ŠF‚©¼ì*ˬeù‘°Ó=äŠÐs–FÂU÷ABï;Ïæ &’üµ/‰CfwnоÑÉ—çp?Ð	Ïß�Ä!³oE¡mn#WYcà9k?{·Æ•X¿®&ô‚vNÁ	1½8[¢ÈÕÚ‰¤-�Ä«ÁT$˜¡’ÐÎÁ‰fÜ”!IPè��ÄÉ

@F꬟!�pP¡0Ç%z#Ñ}:ˆ_M ih'$Úd„üw¸A[$Œ}´
¡=HŠ€úHôÈ|HΡÝ**@Ò0ìF¢‡fGÖ¡:‚(¿WI»°Œ@ª…h$ç]íã¦!IDQ�´	Åðg¸KÔüB 9ó$ò§cùTŸ�ÓÎ'0Qô’ܧ}ÑZW¿ø°H¯ÝDïÛÉ·Ô‰všIƺGH”Ì�Èä_a$²e]k?{·FZ@í�Ù¦ûµÑ™«áÚ'ÖÕQ´Ef®&q²¶Ç<PVíú ³háA¨
2—7Šä•yŽÄ3…y�.�äpÏHRù°þÝ™Ís:¤[‘YÈ*€2"¡�'¨*®èA$^G×s9hû�·Û�®$NÖy—>ïöú|@fìj¹ö)JâlÞÿe¦Ïª2K
23û22»Y͵o5}·Îssf~ï׎·‹~¯¬H˜i‡¼:hæz±úù6}_E­M0d\uõ#’Û«¿¸µ- ¹ºöè€ÝÍX$}b­p3XË×!ÑDÝt¦ª x>¨¬=’¯í½'7Í�€=_«�qSEÛ°a*€BƒúÇ�ðøËT@áê—¶ Ö®-$NðšA'†ú«0s·q¡HÊ„¡úl*ÝÏ7*žv¨@t3I_°Éy榒Èu𒵆½[#- '´qoÌÔmI¡m€
GèTÑNÇméÙ„ÌH¹PILUõo¸Ð^@·Ö2ÖCÍB’X6’Ø%Àé@Öêß÷´£Èªûƒäg¹CÿþD;‡›õ»NPQÖÉÑSC�}Éér·Ì8�ç¸LEÝ�Úùþ¬Âí.íp×çw¿~?ç9Íví3H˯„Ì\Ä­¢ºk q’™½®~Ïæºý9döña$§Ïß*hª#3¶5T`–ÐÎô,Q¡Û£Û[è€ãyÊë¹<¹±¬ƒ²NÎêGÊžãÝ·ƒGÙµZö¦GYn$qõÇe‘H¹_<ÊB´½X‚÷”YHNº�²1H‚c¯I¯�H²c¯É˜.ú=jy”Ý£e·y”5Ö²§<ÊJkYO�²H^´‘eéµ�äQæCòž-ãìës¬ý0ìÝ©^eÖÁfGd6ͱÆ,AÌäa:¨uDI&íÐ:ªp)~âZp—
‰¿\h£kŸøi;¨œ
	Çõ.‰ý°¦é€z$2ó÷¼Šª¾A(D¿Ÿ3s÷gw�{ÍÕal"`½:›r¬!w	Èù¬@Q*:ªxsNF\õ"�
úû¿À”M¦u:ç‡T8~è@îä§ãôÝ»À¹Ðë.q�\2j�œ40©mí‚	 㪪a*8fhÜ®�Óɨ-$ö(±òÌF¬#E‚ʺ"“\{�JAeí‘$Æq:Qã¦9°MÛÞ{ƒÊªêµ–�tk¬–Í%q¢kGŒlÕïìÚ�A'‚fè�ÛrêÚþ7‹¹»Í›©÷›ÃCŒÍA¬C±bl±–UõckËQk?{·Æ•X¿.G (íœòkçtYœ‰
Š{ëËà 2³4[;£åÀ>IÉш€�ób×¹Áä°‘Ä^�Áw: �þüCÀÚãf22‹VR;̆zÝÉAûeCföN»®y6Ö»þÑOr~�œcö»þ±LeUñ8ñÁ^®ÏÔq	Ь‹iƒ¸ä»*a)í,ñ|<§쪠:p®÷L‰ w±CŸAÏpŸ¾CÃ0®zh»[_ÛŠ�®²ŽÚþ5R!4‚€U¢�Š•æÈdÖ×&¡ê#“[�‰³QÖ•A&°Ú¨À�¸JƒLB}‚¸}·E&ծѲh=Ï“ˆ»÷³®	—-{[ï§%2ɆÞïgZÞDÛ³Ç]ßñ}T
´ïyÝUö:báj€x	r•=®çj‚¸ã�t=›ÖzÍëÐ(Ö¬Úzï-õ�Òïæ´çýô»ß�XÖòY5#5ªìs#�›Ñ:èvÖþäÖkâòät<Žo´ã‚ÐÄ5ð
¶
 °näFXN#}Œ€›ÁpÄ-
 ÌÚ)¸g̼¬0µt¿Oõót`%�è:n¸?]
þ;,@wh熫Ìa�«ƒ­“Ì},#àß}#â+î0I]3×}FG]�l¸>ïyÚ‰_Ž˜s�#
™™žµ�çn=€_õïçkW(2ó»È¥�óê ÑãX€îACƒ¬óZ©·pÕM€�€›¬I¸lmGJ�ÃH»õ£âj–K?gÑI�;7àÝ¢™¥ÓöùYÖ–.‘1Q…�cm¹Ùu�ˆåÙYSã°ò6b�×6§»«ìIÄ+!½ös¹ÊnEÖeSÑö“K€Ô%°ž©b
w‰g=Ó=A}G^Ä*Õ‰N·Ã%Î2#|íô=.q¡íê‚Ôv=›ñÚׯè=»×�×>;RûI·8{ƒ¤ë™¬ý0ìÝWDýºÔ(RïŸKÐ\‡�¸cù\ƒô*æi‡Òø,¨:2ûõð²¦Ã@ó´QŸ¥3Q‘AÄ•aŠvN3’‡!–g}JOÝÔq›˜¡�‚#€ré ~ŠÞÛÙÐA¼£Ô��J�¸-üŒX‚f“Øgü-ë°®”îCZ/Æpïx‰|4ø\'ŽúIßÿB»‘ôÑçÛ=HE«(œ‚,ˆne(Õ \ˆeóÝdÊûiyr–=ÇÝöÁ v©W
=#¯ó„ê5o@"‚EZý°A�µV?{·Fê@9/ð¼98ÿg¡$õívðˆ„v6òqî58çK—€¥Þ‹N/”äž÷p$rÜ•ÖAù7I÷LaF’ú¬?§?.Áx¾DÙÿ|ªÀ\£9Ïò°�Ä‘Áª!®‘7§¯	XiãRèÅ%SOèDÊ@�ظÝê‡
‚¬ý°úaØ»5RŸ2Î�L—h ¥½/@X^‰”#€À8hê9ö™ë!€|*<îÚã.¥�57ýÏäCÜ’®Úž
‰Bö6�¼`nÔY?EÀU=ÿo®ÏYש;¬»hK Ô~²¶%Ôê‡aí‡aõÃ0.¼~…Ø3¸â848JÉóÞ�¸x]Í
Ë`ý1Œ‹¡â&ùSÐö=ú;Y1‡À‚oG¬|ŠX•ª!îu¨8™¡ÿ»?“8Hȇ	„¢wÜànUÑä¸~fEÜv÷ QgÿµÈ”Œˆ«æ	ÁVÃ0ø,çIR»H8׫�£ö*� âþv0™r’@y³Š•›�¤˜w#®r«ÍHÄJs;ºø3�s
ж¶-’ã%\ÏÙYÓ˜™@DÆz�/õó�ˆ•k!²æo²¨~¨½BÃ8'­t"ýêB  H�îÒKôA¢æ9kK�Iœª¡9’ÂÂY3ØW'$œ¤¯K—ZwšŠ‘ÄÞ5õs?$raWýübÕu§e¸I«9¼¢Ÿ—!'¦¹Ž¹Eï·Ž~ˆD7uRn,Gò,NzFÏ �7@"î"Ìc²N×½v¸µÞo}ýü>²þÚ‰@¸IÜþ‡ë˜Ûô~êç!HpÃøOðéÀØw縘ãsè?ÚÕDfà8¶:-ˆ&ßpÃêGr\£b ÁYö™«âÃ>=‰VÉL�¤ÈuÎmHÔÁ_�è”è=|HÒÐøqHT²ÁH0¶í\]³"ž|:ˆŠ#`åüDÑ:ØøMYý0¬ý8ç9b¤<8Œ\*ç*_†¸Ç:ÿ‹[õœç:¦ �@E[ub£ ë˜õHÞäŽÙ†Xp�cŽ 9ÝÇ,×û8Û1k	¸Û:Çä'°ypŠ€°×1kÛã³]E sÌQ$HÓ¹Ž9I ]ÅQ$ØT… còˆÌ»I=bÿûÆÕþü§k€â® ‡qÞ¥ƒùXg€ÒL¹ÎaXý8›šzŽ}æ&ó¿ýÐß5øI‡Den›ê 
Afls•9�SþGâ+î6.“
£\Èíæz-bi
CÂ2�ÕŽ?�"2!.u+�¤“Ë„/¶úaXûaXý0Œ+XE�‡Ê¤�~0ç�-=I]úB_~/‚¯ó-�¼û…yœ3�5@ÖˆYuÕ	 ÄÕl d9�P÷É	 ÜˆUæ7à{î4—ŽÙHÂÍŽAç‰DBþ?®máiÄš´q¯qrÎø÷˜éz®ò°8¡ök aà#­~PI)�Ñõü÷¤$<º;Bà3HŽœH$7˜;‰òˆ•/qÿ©ì*kã°�	¸ñ€·xZÿ~„Àz1ç]½ª÷õ?$o�C)$`F:dRíIWY^ĵ*Zû¥—]e™µ,?b™èáê##ôœ¥KAùê|ˆûTMÄêØ—€•ÄÊyƒö¥=	äO¸±¢†ÝTˆ;ÜŠ¸H�¬—kä*kLòAk¬1ìÝ©^ÍDO%q’4gp�ñ—ýñg½ÁÕéLÑmïb¿ûIzGÄ<¯ÇMwuNQÈŒê(dqó/Ú™ÔḆÓuð¥À¯üü�ˆK ÑŸ@’о‡ø¹Îæê˜•5d¤6´’¤ÑÜîD\KÎfÝ�ÐÁä……ɤíV0’Ù~¡dàÊ�òv¥Ô�ÏT”¢BaŽKôÖvÞ§ƒøÕ&ã:!¹Æ"´ÏÙám7¦hB{€’ZV_û®|@QÄ•ÒYÛQV?—A¬v[\‚(¿WOßï2ÖÄh­§mô½OC’ˆ¢ýÞ—¨M ™O¿Ÿ“oj€ŠeÇuèSëÒ^@l8‘ßÒþ͇ä¤[KÀÝê	dýK:Œq*Ê@ÖlD\™*è³)íš,؉¸nF\ÜëZÿbØ»5Òªr,3!Ú�‡	 ‡IœÐÒÙ×IXêCfj›yœ{pÐà'™ùš­×‰@%æÑsùµ“qi£[€ÜÖ¤�füÜ.p]×nïê°¬²FÌ:¨ËsŽL:Àl‘Ly-�´×•jëG&Ä=°¿¸µãh?±‡Äk@Ü
Gh_ÔØ£o«Âäî ²®È„Þ|d²ÍM{`©
“þAeÍwǯUȸ©¢âa˜
 ·ŽEÖK¼§Â)ƒ«,‡
­wõwö !½\ïc·
6‡PdÒo¨>›ÊA÷ó�Ч&%Ý}â÷ÚGßTÖ‰ˆ8‡@`ë_{·Fš@¯"Ö�µ1­$€
kc9™‰Š@¢²¸CÔ&çc¿Ùcpó(�h$ ³€íUmtm¿±ôx	 µƒš€D�y=ä̘UDÂßZd�˜uPV?ŒË[?nÐ};x”]«eoz”åF|ìQ‰»øÅ£,±-Á{¡ö,Ä« £l²=‹GÙ@dA}Q�².ú=jy”Ý£e·y”5Ö²§<ÊJkYO�²ÈÂv¯ÄÜ鑨¢“<Ê|È¢ùeœ}»µ†½[ã?«_—#Pu$zÈÍHXÇÃ�ÂFÄWú9$ŒãcÈLV^×>HÅÉ!x?t¿|É{2èÌy§\‡!~Ôí‘»ÉÉtdîó¹Ïe†a\Â�I¶™*€‚'Ä^G¬@÷Htëð*â•p3P$¨ìY9ÕLÚ9ܧÂ)²ÈMsÄ
u”Àº-‡ªHĵ$¦bå¹±*=í!FžÐïñlPYG3Iš 7Y5™̘̳iGR×ÍW�É¿zon:!“�I¼=ú¬›[õ4#µªìs_V;˜jÚÙìW±Psdádd�Í}º}²àôFd½M1�ó?†Xiê#¾Õ±*VÖ n÷kg†XkV»Ž½±�¸1ÒÎ)	Ùx’ßc�²`Pzý»Þ3…i}Æfq¬í°úaõãRž£²–%±ä»C„w$°–å=Ä«À±J´Ñ6<+â‚=—ÀÚ¡úˆËYNk¬*ƒ¸Œ]‹¬�C\¬A&ÞvשhÚC`mh´
ª;Tlí&àu¡ýä³*¶ km °Î§¯Š»¥$^Kû	’$7qËëá*ë�¬¯
EÖ¾~â*{É?‰Þ˜àz6­UàdÓ>ØY+…Þó.Äsã.k¥@;ìQaÔHŸE>k?{·Æ•X¿.‡\+ÄŸúú“_·;Ñaªkã>Y|êÌ`¥Cf“^'—ß‹ŠÚ€÷tuDYµ3éäjœÓ‘8ÒNyd�'Ú¹µAfË"‘è:}‘Eµµø‡ß׿;X+”›¤~âÖY#f”Õ«—îÍW³\ú9‹KdTQ‘QÂÕþÿ©ýB¬–Õp‰Œ‰Úæç@r»ÜìºÎΚšG\eo«ÈGÖÔtw•=©"#=âúæ ý=­ IDATg·ºDÆÀ qV—Àz¦.AâÌ�I<·8Ë«‚¬	b™Úáê3#“�í´Ït‹³Äòô�àµ]Ïf<`!F﹕ë{GÜŽÄÙ$]Ïdí‡aïÖH3ȰŒaõÃê‡Õ«†ÕÃÞ­ñŸ×¯0{†aXGg†aFZÁ�aÿ%>{†a\6�b†	 Ã0Ã0Òˆúñ›þI�ø|67f\Zþë°ÍéÎñ9'Ûua’†=¡¤lfõgH>4öÙh†Dš3Ã0Ã0#�	 w^‰“”zqPç"ifêsñ’!%(‹D®;ù/Ž­¬Ç†a†a†q™¹\.pí���Ë€±H. ¦HR¶.H¾…=®Ï›�� ­‘�¦7#¡E�÷VѰ¥ Éâéß�\Ó‘°¢G‘p¡_é5
 ¡« ­ýH¨ÐÖz¿Û‘П'‚®û<0Äõ9É
TÉþ�n/´DBˆ.LæÙÔ@B–ŽFr�$žÛ¢Û7!!V[ëù6!!¿Íîo†a†aÈå²E«èð©°IHzIÜ6I<ê|^¬ÇDò�N]]笇äyøø(©ÛÝûAµ6 ù¦û�äªÃTüÔA’ä�$ÄË‹ä}ؘŒH¬$hæ©MÀ�ï!$Éê< I³wƒä7zR¿ÿGHÒU�\HÃTÔ-ÞBr,|‚äx±ªk†Aaàdòíc^C,ô1À»@N×þÏ K‘Ú>»“k?܆¸JwG¬õm´$1hCWYSàiýûdÒË¡’ܧ}Ù=®²RH¡tÈš;G]^$'Q4’�çeWYf-Ë�LŠõ �#(BÏY(ô2¹žÍ+@M$O^_$�C'ij"=’S¯„«ì~í—B´?«ê*»Uû;´Ÿkä*kL ߟaÆÅåÈ”±jtÖiãŽæq5Î;\Ÿó‡\
»{ÿê,>N£ÛÙãœõT” 
¿;AÛ($Ãw
ýY­bç}í4‹%ó]2"Ù­qÝË÷ûý†dw„Ø\—@{Bÿ^Ž$îsDÕý{DP§ñ¥Š¥BWYÝH©s©³í0Œ”¨cŸ!“C¨P˜ãê7z¿hÿr¿¶óQ®ÿ
7hÿã‚¶ˆ÷A´
¡=&Ùêk?“(Š$T­©eeõs ;bÉoéêÓâ´_Ê€xD8kY£µol£l’DH´ÿDû´á®~óëc †Î
òOõù€$9�O`íê[ÚOù/�µÚW£}Ô"½v½o§ßn�LfA<%ö¨à™L܉xWFªÖ½”ý‹‘:±þŸ}Ë¥@QÚÐ?¢3C‹	dß>—Zt.·ú#hêý{GÐìSrh¹ÎœuqýdÖ™ÁÚMÑŽÊM¸6ø÷é,[0\3oá®ûrP°Íµ>Äšå ëƒžáÓú�æò¹.š2ìÝWzˬúë€Û=Q¦íæ`m·+;VÛÛ8í/ÜÖòuˆ«´›®¼ž*k,UaÒ?¨¬°øÚÕ79TQñ0LûwОXÄUü=íOÝýRZïêï쮲Ú×õWaVÔUЏ‡ÕgS9è~¾Qñ´ƒ¤knêÄßjíÝtf¨}éR÷/†	 øRÐ�®ÙªhÄÍ@5ä\3Wç@'€lúù�•˜Œ¸ ÂÄédjŒ»	÷o´“ºÎã;-E1 �Í’F°´ruz_	 wÀ*®û뙌rßÓD "âŠPß�q• ¿ý¤êŸËUÇnÐ};x”]«eoz”åâ��=Ê"‘õª¿x”… ¢%x粚…¬!õŠ:8BÀú,,Î	·°ðµ<ÊîѲÛ<ÊkÙSe¥µ¬§GYÄ-}¤GYz`/²v6ân¾Œ³çù2d˜2RµŠDÜ¿~ÖNa®
„©.�ô*buéwh²¾æÄÌï©[U LUÑá BÚXOA#8ë|~~ÐûøQÿþ‡wèì7�;]ŸŸÐ{ã:¥TàLVÑRÁCÕÐ{š¬×*’Œš¦÷5YK…vžM©@V?.æaÚ·ÌÐAwdPù8`&b‰	*ÌF¬#E‚ʺ"k=÷jŸá¦=´&.¨­Yϳ
q)»7¨¬ª^k	²ÞÔM¬–Í%°&Õ-F¶ê÷ø6¨,ƒN&ÎÐwŸêÚþ7�µRn16Sï7‡‡›ƒX‡b=ÄØb-«ê!ÆÖ –£&€R†S§Nù‡jÈ0®"ä�ó|²˜3˜tÜÏÎE‰]@¬;Ñg9&‰Ì|/ùI  óyÜÃù쓇€ï:ˆeª—
p
@F¯½t"+DÅÁG®²ŽÖ²¼§“RŽU¢
²–%«Š‘¹¬íõ—³*bÖ¸Úß2:ð¿VÅOœ¶Ï ®Ì;µ}®¤û]£eшÕè[»‘ –¢ÈzÖk­]}Ë/HЂH�às»s¢c¡:ñ×ÃUÖCûªPdÝë'®²Ç·ëHdÒq‚ëÙ´F\õ²!ëk�µRè=ïBÖøÜE`­”3ñ·ñRh¤Ï"Ÿ	 ‹çõ×_÷ç̙ӿvíZ@†q•	 ÔFMþ]"Ô”"Úc¦Ó¸†	 #-Õ�fˆ«™ã’œÅ%2ª¨È(ášûSE†cm©áUd8Ö–›]ס"ÃYSóˆ«ìmáÈššî®²'Ud¤G¬-nqv«Kdgu	¬gê$ΑQ�€µÅgyU�5A¢Óíp‰³ÌˆUª�
·8‹@Ö3½@ xAm׳�XˆÑ{nåúÃO/qöI×3™º@öîÝë/^¼¸ð7mÚÔ�a˜2l€kXý0¬~iµ~¤n¿ýö„uu™3göÿôÓO&€ã"ÛŽ0{†a†aWà(Íï§ZµjdÏž�Q£Fñøã�#úÀ0Œ‹Á§*Ûw‘³8¾K|ŸÙ€H2Ôó%+$à4âÒ°+…ï)=â’pèŽÉޏ]“Iïó˜~×½WJÛ›ïörÔÃê‡aõÃHƒõ#­ˆ�•+WÒ²eKV®\™6§>ö¿o\Êö'ä*¹ÑŸ¸ð$ ßðý^z	î©
µîB˜�÷º�Î2„¿CÒˆC†a†a†a¤WŠ
OFé§;Ç1^¤÷(+tçÿ÷ÿoÏåDa’ÍÜ0Œóçy�ö,„¤I*ÝÔEòÇÔæß4©�„<É)VìûWÕë5%°hÿ\„}¯ð+¨Ý6Ã0@ÉPøÕõù9yp&"Y¨§"Ñyêèö,H¸Î©HŽw˜èVHDš�A.ñ±	s:�¤‰âVéïšH´žÉHîƒO]ûÔB¢ôüˆ$~Ësß­4t&Ð͵ýn×½÷Æ;΋HîŠ?Iœ�ü/ ,çÛ0‰äUI éæåªz2Ç
×öäNÄJÜê¯[¨¬N�cÿÞˆ¥·1Qì“ó¸F?$Z˜Ã �½rÃ0ø²P‰gWÃôݾ™�}xT··Dr0ÔCò.€Ì~vÓ�Ê
À>$*ˆåg.Ð	»é&½ë^*ëù«"9œ0 =‘LÙ7 ‰P۟種À"áIÃ�W�põUÖ:.¿ÞC
^ו¯×�açæ$¡¥ƒµ<9ú"a{Ÿ:¸Ú¨öZv'‰sŠE"8OµkG¬QÑÿÿç=&R>Òí׫°ÉˆX¿;¸ö)‰„1Îçj¯ºÕtÒåýìÜW-$ig	×9nDòÔ<’f÷öb
¸à4bͺ±b-Ö‘±(gÃn‚$î;œ@,Enþr[õ4Œó¢&°$™²%*ÎF(’å€kÂ$ø	\ò•kß9Únî'`ÉIYDÿþN'=Vëÿv9×~U·¹7kô½ÖK®}Ê·èýlEòÎLE&‹vjû2UÛŽ:I´ø¸MÏñ?`°qÍ{7•¾ûÂȺIGàù€×«_Œ~owîg´ý�D,rn·Åôù…"¹|*»ÊÚh9Ț͆®²¦*ˆAòµt•Õ@ÖŠúô�Üã*+…äJ§uâIWY^àÑZ—ÜnÑ™µ,¿Ö•Ü­#ôœ¥�‚@—Pö!“s5‘ A}ƒú™NÚ‡¥G&Ý‚ú~ ­ÖýnZ�nÒ¿ŸE :4F¼>Œ‰ÏçcÞ¼y¼ÿþûìÞ½›~ýú9ø.Gìý²D£ƒ„®Ï'�‰— ÛB {tù
HÒ·–É\ëàyÜÏI÷äŠ^s7’<®5IÝhÎF¾ ¿6m/ ÛÜlêëÀÖYõ4Œs’KEDrîgñú?í8n¾G"/žRpq‡­¯íCy`Òvb îWÏã|QˆE·¥ë»�@,ôze´'ù|‡Uð¬Qá’¼r…ësk쎫£e#€o—ᥩôýoÔAü ¤¿„Lн¥ïþâÒÜL'¶UasI ú5bA»xSëÀi�ßèçbˆ«¶ãZ=¥çI§Bù&-ûCÅé}wߨ°ñ#®ÒôžhÙû*dÿ@\¸ãT@…LÖÐ}h}C&ñ¶Ûô>ÞD,�ïèý®ÐkfW!ܱÞôÒÿ‰Àg*àîVA_IëÑVýŽ5´ž¿­eg7ío´.>DÍ€Ï]m’Ûr0YËCuÀép§èÑAqWÙ—zOèàÛ}\AmkÑk6v•Å¥âú‘IüýµMv½	Sq1XÅc… cǪPŒz^è1cµ
¿;¨¬«¾çù$
´Ñ^ç/zOÕ‡Í*¼F•UQÑ4Lû9÷ÚÑXÄ3à=`�ÁU–CÅô»ú;»«,ƒŠ¹þÈD_QWY¨Š¬¡úl*ÝÏ7*�v –!7uò`µ
K7]€*Ò_ºÔõ#­ðúë¯ûsæÌé_»vmšø¾6v0.Gÿt9P�‘ëŒ,à­¡Ûïu5ÖéHìRRVíV:ÉêêÐnÖY§W	¸�ĶŒ³çq1tœ:uÊ?tèÐ4ó}mì`¤t5ó—w­Ô»œˆ\@†Õqº)™²›I:ï%€@f«ëÀo²q
2ƒîHŸÑ²ÀÉ Û¸B}¬âÊ@k‘5æg†a†aFšÁ�a†a†a&€Ã0Ã0Ã0L†a†a†a˜2Ã0Ã0Ã0d†a†a†aÈ0Ã0Ã0Ã�a†a†a†	 Ã0Ã0Ã0@†a†a†a&€Ã0Ã0Ã0L†a†a†aÈ0Ã0Ã0Ã�a†a†a†	 Ã0Ã0Ã0@†a†a†a&€Ã0Ã0Ã0L†a†a†a˜2Ã0Ã0Ã0d†a†a†aÈ0Ã0Ã0Ã�a†a†a&€Ã0Ã0Ã0L¥r€,pLQ=&ì
ÿn9ô>³^À1E®’ïf†a†a„ÿˆªÛ�žÀÀBà'à= 5z…>‡—ôY´½€c~Ñcr§ð½ô†’)ïê%SÞNËkéççô>ï¾€{w‰¾Û¹ðéMx†a†a—Jû¤¤*ÌÕsú�ƒÀ_úÛÙ¶hlè¬ü¦çmäQVÒõ,?JæøEZ^þ*@õôºíÿÓ0Ã0ÃHi”’.pkO5à3 0�¸ˆFÜÅ^²URɼÈü“Âç�ª¿ë{”9Û¶'SžUŸù^Ÿƒõ>ÇX�7Ã0Ã0Ò2)%€|:ÈŽ@¬
÷›ÜJØôJœÇ9ù=®“¸Èt�÷ä»Àcò¨`KŽ}@pæ,û¤C¬6¹/ày;¨�GYÄ¢6(ä
*¯«×™NÀºwTïóøY®Y�ä]î¼SQ{¡u(§Ç{ý7„ë=—ºÀgk†a†a¤qR®­žg5ÿníÆgÀ~dá}_¬û�_]ƒó¯Upø]?k€›<ÎWJÏ÷bmZœÖcvwzãv�k¯‚͹ÎJ ªÇ1cô:9=Ên–§\çÙ©ñ|÷G€Èz7Û€ñ@=gðwyW·wtm{BïÓ˽ïaÄ‚åâ�ù@9’w�Ë|#àæ8JÅÓ^`r2"°/°Ëõ,ö=ô»:ôiù	½g秉î“Ißëñ º°xÊþ�
Ã0Ã0Œ³iŸ”Zh^M�Ôÿ…’q“‚¸o}lu•_ëà¹4ðhá&$ЂC¨ž¯ªú¨�zJÏ|êq/÷5€á*Äî*"ë}Š\ûfÒë[Þ^ÔAyO`bIªC 0ÁÙˆf!k¥jSt»cñ Bå8â÷¥ëXÇ-nªk[z½Ïˆ ë<ôQQõ’
´æ*bvzÜ—YŸT˜ŒUqs·Š²,ˆ¥� kÏF\$Ðãâ�[ôše€–ºïd Fßë}oëô÷½ÞÝ=�KŸmIûŸ6Ã0Ã0Ω‚Rà?èyîø—ÇGÀMÎËš’�Ä–‡Bˆu`YÐö²,¯••@\¶‘غâX€«Ðqó9I­*à¡¢ŠÀõˆ]0éÎ󙼬çîîÚö�n«î9«\åш¥k�Н YõÙí!©KÚ×ós·;tÛ—Aç�@ÜýÀ¼ s½ªÛ_ôøŽhÙõ®mg‚ªïgS2ÏÑÜàÃ0Ã0Œ³jŸ”0:‚a“GY%ÄÒâþy;™óôÄ; ÀQÄjÌ&Ä
Pš¤®b .Y}ƒ¶­F –”ÇSñ,t@,0碣Ô»"îvÁœ8Ïg:U»ÔWð§~ž®‚Î)^ë’£­
¦Á$¶¶¼�÷z¡û]åîóŸÔwŒOÅWÐË£ÜÙvÛˆõý*¸¼êîûŸ6Ã0Ã0ÎFJ¹ÀíÕß^ID÷m[…·E`ñY®QYËR	�àAȬí‚*nÜ,UñÌ,àÑdÍ_Ûœ 
¹ÎãY”ÖßS.ò™ÎÓ{¯�X;N¨úƒ€›átýÝY#Õà®í|÷¹e»õY^´½˜Š�•Ç,LæÝG©Àz*�äGÖl�g�õb]€-ˆåð{àwdM’a†a†a\´±>õ(GÀ]*7ÞV‡]Él¯�XD|ˆÅçgd-ÎiàF 2ÞÌ’O½]{E…;ì±Í±ÚœO×B*Pâ.ò™Æ«Øi¢ß+âª6صÏl½V}@^ë’#ßy<£`”±ÀøÏóÝ9õ¡É[ýN$#œ“£;b¡ë„¸>¤âg4ð<)’Ü0Ã0Ã0ä)€jƒ.Á}B\žª#ÝÜ”WäEr!¬KÎŽKp¯[T(äH�ÁøT@
¸©Ms•AÜá –°ŠÈšž¥çqîçùŒ‚¿[q‚§“T^×ø‰@ ƒ‹å8’vâ^y+²6é^$ B5R6¹¯a†a†‘ŠH©5@Ÿ!á‹Ûª Ii‘v-âv,~Buœ¥Iù‰X×_‚gºB×I�sMÕßõõç(ýÍÍt$/Ò­ú<Îgý�û»Wð(ËŒXm‚Y©×¨íQÖÐcÛZÄ’U3™÷àÅIýyŽýü*þ^Ò{ݮﵰý[†a†a—Zí^Óó�";%®wJþ¥HUíId�IräB\¤Üä@Ö¹Œ¿Ït°Î_G‚\Ì3˜�Xyj�—·x5Î-šÎÅ7ú\Ÿ@"¹y	MÌý݃Äá®ó⽦ëé-'ðæYêD6×çÍA"ÕMºdî˯¼]
Ã0Ã0H98€÷�Eò�!A~DÂS¯ÓAk,�¤�/ðÜã§ï�O�Ö-€»�À	’9n%i¬²P¾ 
…Ì*ž]‚g:ÉUÓX‚¸ï­P1T[ûç.ÜÉÔDEÆ�}f"Áb/PíÞQá:èM Ð=H’Ùâ$¶&ME"èÝ‹¸ÙM@,5ÍUø>NÒHl/M�ÎH‚ÕokMnµwèµ¹ÄôbÄê7		qÉÝäG‚-|	ÌQ±TTëBI`"ɯ#3Ã0Ã0 å×KÔUp”@.¿bg#	0}AÇ8y€Š&sÎ,*~ÜçÛˆ¸™
ÕÏnääú\ÅÃ×qGT¤ãäjëQ–W˾Úî•Èáa$:žûžªà¸^r_7™}kùn�gÞy€žG‚8ר¢ÏuÜY¾ÛsH¢ÒƒH †+’øÖcÿŒÀ»A×ñ«Àû•¤Ãk�ÜRÛ‘µF~Y9‘5PÇ=Î3‚¦`†a†a¤Qís©Œ‡¨ ©áü"¨�‹‚*rŸc?·r(Ь
»Œ؇XŸê#ÖŽtWhEG\Î
^Ä9êè3ïsŽ:ëzÿåµÒ«Hj€¬9‹¶ÿeÃ0Ã0ã¿@ÿ%^ÈHÒ%³mª>ó:öˆÃ0Ã0Œ+U…Ù30.�‘HD·ˆkadÍPI`²&É0Ã0Ã0®\”
¿“Y€.�‘àÎú›3úùRÆÍÑ0Ã0Ã0.©öI�(Y#’ÇÞñ%##’s'£=
Ã0Ã0Ã�a†a†aÆ&€Bì†a†a†‘V0d†a†a†	 Ã0Ã0Ã0@†a†a†a&€Ã0Ã0Ã0L†a†a†a˜2Ã0Ã0Ã0d†a†a†aÈ0Ã0Ã0Ã�a†a†aÆ9	»ØŒ�µÓo�1õÒªV.ŸÕãRÕ��³ÆZýHÅäªÕê¢ê‡ïh«©_¶¶>{
†aü˜È0Ã0Ã0@†a†a†a&€Ã0Ã0Ã0L†a†a†a˜2Ã0Ã0Ã0d†a†a†aÈ0Ã0Ã0ãv)O~âø1Æ~1(p±Ðp¢²d£BµäÊ[0E®á÷û™?s"ëVýÅ®¸­dŠŠ¦p±2T«{™2g¹¤ïû¯†0þ›a4l~;w<ø¼Õ¦I|üIþ˜ü=[Ö¯bßވΚ�’e«P©ÆuD¤KI¯=¸ç3,ž7�v�u9v! IDAT¥n“VIêÖ·#pêt<
›Ýž¨Îîܾ™)FÆm÷tÄÂ’yÓX8÷wíßKæ,1(R‚ªu®'s–˜‹¾Ï·×äTüIz~8žlÙs§‰zqìø	}16ásˆ/„ÜÙ³R»r9
çK}Ï`ß�C3>ásDXÙ³Fs]ÍJäÉc
EG�� ÷À>‡‡…’=&Ц
ËS¤PNŽŸˆçšj�X6«/™2ž½=Ù´eŸ|95ÙòÜ9³ðèýMþõ=oܼ‹ýŽ�/O6rdÏl/Ñ0#u
 £ŒÞ'Év_Hï}1�|…Š_Ôù÷ìÚAï—Ú³zùB9¯Ï‡ß/yóê4¾…g_ÿð’>¼#‡°+n+‡쳚ô/Y»r1}º>ÌÎí›’¼Ã»y‰ÛîíxI¯`ßnvÅmåØÑÃIë©ÏÇÊeøsÖoœ}:íp�Ÿ ÏðQI­ÐPzu~„»on’ª¾ïž=¿oDx8ó¿JîìÙ¬Áp·¿GNн×7I¶‡„øX>û]®)ž€ôéÂ/H x�Ó¡|ÙB%€:¿úc~˜Ã;¯·ã¹'n²—h†‘�›¾ŸN"{μôïþ8‹æNáû¯†Ò¡K_Ö­\Âæõ+9tp?£2S¼TE
½&áØ?gOâT|<å*×aÍò…lÛ¼–æ·=Àûo=ÍêåÉ“¿O¾<€e*qäðA–/žÍ®¸m?v„%ó§FÙJuX8g2~¿Ÿêõš±ré|¶o^ϱ#‡È’-¥+Ô${®¼œ¸�‚EKQ¶R­T[WF÷ïFΘ,ô1†ï'ͤkÿáÜqc#vü³‡¥«×“3&+ùsçà÷9)Q¸•Ë”àÔéÓL�·˜�[ãÈž5šZË’3&`>ÏÄ™ó9zì×ըȆ­qìÙ€kK%_®,[³�-;þ¡Dáœ9s†™—Ò´vòåÊÁ�ÃG˜>	q»÷Q(ON®«Y‰°ÐЄsoÞ¾“ùKW±ïà!òçÎA•²%ÉžUêå?{ö3éJ¶íÜE®ìÙ¨\¦ùsçHô}ÿüîC"ÂùÿÅž,X¶Ša£ÇÓµC;)[¾šÕ·pàà²FGQ¥lIbæM8ö—óðûý4¨V�?-#n×ÞT'ƒY8µùóÆÐîÑ÷˜øûÞ}ÿ'>ìÿáaô|í®$Bh朕,[±…ZÕJ�1Cz–þ½™¼y²R©|fÿÚ€ISÿâ•·F‘-k&Æ�z€ŒÒ±{Ï!¦Ì\ÆŽ¸ý+š›U $ÄÇ¡Ãǘ´hZ‰°°PV¯ÛÁß+·’3GfBBBØ'“eËVlfÜøù.˜ƒ
å
[g`†‘ZСý{	�àä‰ãráðˆ„²WŸ¼•£GA|üIî|¸mî{€þÝçðÁýT¬Þ�Es§™!ª5`ÑÜ)<þR?J•¯@æ,1Ôlpc`�¸k=»ÜGúÈ)QŽKæR±zCbK–§k‡–øBB		åô©xBð÷ï¥g—û		¥DÙʬük^Âùßò}¢Áéšåyêîú=|€ÛîëÄ]¿hµëü9{qÛ6Aç7?J–19óÒäæ»ö[ù×<ÞéúyÆrúT<;·o¦å]�säðAÞ~á^BCÃ8ã?ƒÿÌ2dŒâíÇS°HIÖ­\LÏ.÷‘=W^BCìLŠ” ç‡?“!cTÂ5¦ÿúöéÂéÓ§ð…„ðL·!ÔiÜ’ªuo STöü³�e‹fQ®rþ^2‡];·‘!cÕë5“�QT4{ví`åÒùä+TœEJ$q©Ô£“Ç•hÛð–�-{nº<Òü~BÃÂ8�Ïçãþ§^ç¦Ûö|vÓ&ŽapÏg9y⸈²3g¨^¯/¼õ1¾�Ô·´/OŽl”(R€V�ëðý¤™;~‚ý3}þž~û}JÅbß�CÄíÞK—‡ï$îÜöÔk¬Þ°%á™2D2èÕŽ4«W�øøS´ìЕ?—¯ WLV"Ó§cã¶8ú½ôwÞØˆ�¿�À?üF­JeY°t'ãã‰-�—­q»¹ÿÅžìÙ��gÎø)W¢(ßz�èLùä»	téó!!!>BCC‰�?Eû[›ñö³3}þÚtìˆeçd|<õ«•gtÿn‰¾ïî}ˆãÄÉxÂÃ¥©>}æÍzŸÏGX˜œ;$ÄG�§¢ý­Rï}ámçÕ+2eî"òçΑêÐˆˆãØqé?""äy�8O«vïp`Ó2GEòêÛ£xã™À

¡Z¥bÌž¿šV7V㻑ÏQ£Š´í›¶ì’gš°mÒ´¥Ô¿ñ5öí?’ðîkT)Τq¯•)’_OeÜøù¼öBžép#M[¿ÉÖí{˜4öú¾ÿ³æ­`ÄWÓñÕ4¸û:†
|Ô:Ã0ŒËÌe)uëÔ–ŽwÕcùâÙdÏ•—»é’Pö|�ጞ¶…QS7Ó}àÆŒè�ÿÌ™DçˆÛ¶‘§º¤ÓkƒÙ¸v¹v`a\S®*³~ÿ�¡½;3´wg>z÷¥DÇ?v”��P^ì5‚Öíž"*:+oðc¦oeÔ”�´ïø§OÅóíȉŽ;sæ4…bKÑoä\‡ƒû÷0ê㾉öÙ¼a%?û6O½òžÌ.þ6ÎjÖy°q�¼Ã¼cÄÏ�£>Lx‡ß}60ÑþÛ7¯£xéJ¼Úïkê4º…ÂÅÊ0ð‹|3}+_OÞ@Ó[ÚqôÈ!~õAâÁÑÎí4¿µ=½>š@Þ‚±lÙ°š_¾û$Ñ>;·oä¥Þ#¹íÞŽøÏœá�ÉßëÀ3‚Ú�o`ê/â3}¢žj7º™ðˆt4¿µ=¾�~õ!�Ú5àáÖUøî³�	¢xú¯ß1yüW¤�Ìȳ¯ȈŸÿæ¥ÞŸ‘>}™x÷+FOÝÂè)›y±×Hü~?£?éëùÜÜÏÐÞÏsòÄqºöùœÏ'®¦l¥ZÌ�>�ß'ŒJ•ueÜä™ú|,=†|@Á¼¹ˆÉXC±bÝ&V¯ÈèþÝhZ§*o¼?’Õ¶P«RYf~ù϶oËá£ÇèôÖ ŽŸ8Éè	SøsùjrÅdeÜà7yù±vlÜçyíKWñd»VŒîß�b…òÓé­÷سÿ ¯=q/ë'}ÅÝ77aéêõ¼ûÉh¾øa!!>Žýˆ-SG3gô`Z5©Àç?N`ìûo²eÚh–ü0œw¶LrÍëèLÃ{žféêõ/”�§Úµ–Ûçctÿnl™ö
›§ŒfX�Μ9ã§ß§£“œcÇ®=éö4½:?’êÛ’¦­ß¤l­g™>kòÅðæËÿóÜoëö=¼ÝOÚçÁ}dúøî	V™sq挟û:¼Ï¾ýGÜçAmÉ]mê2gÁz”öbøÀÇÈŸ7†}¿£åݽٴe¯v¾�uÊ0°çý4®_€§kÁ¢i½yí…6Ö†aü\6PëvO‘1S³§ŒgíÊÅôëö8]û|À®�ÛèßýqÖ®XÄáCqAÛ·÷ŸD¾o½·#
›ß.ƒ’?~àôéSìŠÛJî|…Ù´n³¦üÈ¡û�à¡gÞJt�½ðù
K6KÿœÉ˜X¿ê¯„5 ;·oNrïmï†lÙsÓø¦»XúçLV-[�¨¼L…šÔ¿þ6âOž`àO·m#‡î¿äA®v"ÒE&Ç·ò¯y,ž?�£‡R¬TZ·{*aÿÈ™èøÊ ÂÂÅ•åÐ�}L;‚5/dÓúœ8~ÌófÎÃMÿ{ŸÏG�zÍùîó÷’¼Ã:�[Q©f#¢¢³1fÄÖü½(¡ìºæ·3qìfOù‰ö_gÖï?Ð@Ýß®ou/å*×áÇѲxîTâ¶mä³!=8vìw=ü"-˜@µz7PGUÕ:MƒÕ­˜8n$ëV,æÈ‘C	Bçè‘C‰,UëVýÅñcGgìƒûÅ`vʼn¥cÙŸШÅ©®®ôý80À/Q¤ï½òT¢òìY£é÷Òãø|>f/q}Ï-M)^8?îlIÿOǰÿàaÖnÞÆµü4ªY‰šÊP³Bú�ø†õ[v$¹výjåyþAy¦ÿìÙŸ°Ï„ésùí�ìÙ/ïë�?—P£Bi–®^O�¶¨T¦MkWá–ëjP³Bi¾Ÿ4“¶_£\ÉXÕ¨Ä-�j'¹æÝAhHc'ÍdźMtî=”A¯vÄçó±zÓV>û‹W®åÐá£	÷uâd<é"n^/>r7Ô­–&Ú’.�Z�9ßþ8—‹Öq÷#ïñÓ×]’ŠÙEë9uê4¹rDóX{ùÿk}c5ú
Îk¬ß¸“m;öðõw0jì,vþ#ýÕ”ËéÞ²eÍÄ>Eƒ›º1eÆrê×.M×çn H¡œd‰×Û¼y²™ë›aFZ@
nhC�"%(Y®*];´dáœÉ=|�=»âÜó¢³fç>OddFúuï â&ñ‚ïâ¥*&ü](¶TÂbó9SÇÓò®Ç¹ã¡¨ÙðFž¾çº$×�Ì�)Aüü1ù¾ü°'…‹•¡]‡®ìÞ¹�/?ìÉ�EæÎ}œ>}
€��ÐDå¹u�GhXàqž	²^I)\¬4Gà¯?gR±zC:÷ÆÔ_¾aÀëO$Ù¿P±Ò	â`Ü—ƒùîó÷¨Xã:zæm–̟Ƥ¿LRoΜ9
~?ø|ɾÃu*É÷Z»b1öéBLμÜÞþYÀÇàžÏÈ}xÔGç\!!¡)^FWú»@‘’©²®íþ…óå&WöläÉ‘-Aè8”¿&6ѶP}F§ôù�9ãÇ�?¡,½
…#ÇŽ'säèqÏkW,UÜõì×(Y¤ éÓ‰+o½ª×’%*¯t¸‡ZË0aú<æýµ‚nï}ÊÔ¹‹Õÿ5îiy=±òòã”Ù,X¶Šw†ͨ	S˜?fh¢kÞ}sSrÆd¡D‘Ü×¥'?M�Mÿ—Ÿ`áò5tí7Œü¹sйýíÄŸ:ÍóïMø®éüT(U,Í´%ín¯Gé’ù©U­$õo|�	“qààÑW8‡ôéåù;~’Ó§Ï¡ÃÇÏëλ÷ù|”-U�ðpiC®oTžÜ9“]6ý“eÓ–]:|Œè̬Á7ø‚¸l.p+þšËÜé÷ÅûDD¤#]dþ^<¿ßOÞ±ÔmÜ’m›×&{Ÿ/p»9rççúV÷ðÕ°ÞôëÞ�?gýÆòE³½�
Z±|Ñ,Ä”­L•ZMؼ~E²×5¼«—ÿɯߋûM©k«YÍI*×lDéò5è×í1†õ{™¿Ì`ÝÊ%Þ•Õçý+×lD©k«±qíßžÇ>¸Ÿo?{�¥ÿ`Î4™é½æßaÃæmU8‹«KýÚ$pwj×€‘ƒß`ÎÔñü9{KLH�]©†ˆòù3'2qÜHvlÝÀŒßÆräð–é÷(X¤$µ¯»™m›Öœõ^Š”(K†L™9yâ8‹^ÃÝ�¾Ì­÷t¤Hñ²äÊ[(UÖ•2Å
S±tqòæŒI"~¤n$ÞV§ÊµŒ7‘…ËWÓÄ7œ9ã'G¶,ÄÌKƒj˜4ëO}>–ú|ÀÎ=ûÎ:ð±4•(R€è¨Œ<÷Àí<÷ÀíÔ©\ŽÂùÅZýÃïpMÑBô{éqFôzQ¬6ÅB7qÆ<²g�æ­§ä›ÝÈ•‰»öpàð‘D×ücáR~š:›�FK}�ÊIXh(³‰•©t±Â´hX“5·$߸ûÒNš·™sV2nü|ÞyOÂb§OAÆé’ìW£Jq2GErðÐ1îôïþ‰o¾Ÿ}^×(R('…
äÀï÷“#{fºwiË«�o£nÍR-œ€•k¶ñxçaDgÎÀ£÷7aãæ]<ðä�„s”*)‘é¦ÌXÆäiK,J†aÆåå²Y€†ôz.áï¨è¬<ül/BCèۤ_
ëÍßKæpWÓâT¨Öà¼ÏyßÝ		eÂwŸ0}â·	k3|>_º ähzK;¦LÍÄq#™8nd²×
		åŸí›xá¡æd�ÉE›ûŸ±š“øBBèüæGéÝ™y3~aü7ÃÿÍ0BÃÂ)^ºÒY�o~Û¬ù{Ãú½ÌÇ^¥\åÚžûåÈ•�)FñÅâY¸X®oyÏÝkýëÛðŇ=Ö¥5t¹¿9unìçƒmË•…»‘`5´àæÿ=Â�£?bhïÎ	ûÿa	
›µåÛX4wÊyýdŠÊÂS]2¨G'÷|–!½žK˜qîôÚûV±€WkÇŠu›˜÷×Jš=ôÙ²dfЫ‰çúºÕ¸ëæ&|ñÃo¼1x$UË]CÉ"Yµa3a¡gƒ^éHû—z1èó±¼ÿŸ„gï¬ÓôùXV®ßœ(ú£ÿ»€1§1~êœDe÷µ¾�èLÙµwÂ5}íÝ„¿sÆd¡ßKb½½Åuúb,¿ÎœO‰¦w'¹´Î#ORÄd‹âýw� §[P³Dgdè»óÀ“Cøø‹)d�‰âºzeûÓ¼DQü¼'à||ùÑS´½¿Ý{}Ãë½Ç$¼Ã7_þÇOÄs{ûþ9z‚/?êHÛ–5Yú÷f¾ýq.ƒ‡O¤Ã×ÓúÆêþ+?ÿ¶ˆŸ[Ä{½ÚóÄC7Ø4øÜcPÀ¯¿ÿcgíô'WvúT<+þšïðúÈCîü…	wE�;}*žÅ󦑞Òåk°r©S²\ÂÃ#Xù×2ÉuvíÜÆ–õ+Ù¿wÑÙ²S°h)rä’™¶“'޳zùBBCC"Å9;z˜Åó¦“#‹^ÃÚKãšk«±{çvjU‘аpFý¾�egqäðÊU®KTtVþÙ±…vl!&Gnò(
À²…2›_êÚª„†…_õ¤U­\¾‹9þlõÃͶMkضyG$[L.Š–¼6á9Ü¿‡ÍëW‘1*3EŠ—MtÜþ=ÿ°lÑl
Å–"*:+[7®IØïÏY¿ñæsw{My^ï[–-œ…Ïç£|Õz		V7¯_ÉÁý{ÉW(–¬1¹8~ìkW,!<"‚’e«$ºÖêå9yâ8aááž{ó†Ulß¼Ž#‡�%kvJW¨Id†L‰ö‰Û¶‘
k–qâø1ò,F±Rðù|ÄÇŸdñÜ©¤Ï�‘kÊUeÕRY£Tª|5BCÃX¾x6þ3þ„ÿ	×ÁÕ˲ww™¢²P¬Tbr乪êÇÎYc“­ñ§N3ÿ/±ÌV,]œÈôIgôÿÙ³Ÿµ›¶’5:ŠR±‰­_~¿ŸËV±q[1Y¢©\¶Ñ™áÏ�?Á¾ƒ‡9zü8Ù³FS¶Å}ÄŸ:ÍÏõ¢r™¬Û¼��»÷R0o®$aªOÆÇ3éJ¶Æí&Cd:JÅ¢XAisö<Ìâ•kÙ¹{Ù³FS©tq²FË:®#ÇŽ³dÅZ¶ý³›Œ‘é©T¦DB~Ÿã'N²P×&„„†“%šÂùrœŸ8Ï”¹‹Èœ1UÊ•dÁR‰,V½BiBCB˜µh9øýT-wMBô¸ÿŠ\µZ]Týðï�|ýˆ?ÍsWº&«BÈElá\¤Ó�×gÎø™>K,Ãuk–"44„£ÇNpêÔÖoÜIé’ù¹¡M¦ÌXÎË϶NÖ†a†a†a¤	íc Ã0þßÞ�ÇÊUÕÿ¾öõu“²•V©,‘6PDhYÄ–¥ÈR*CL)[‰†‚5¡ˆ©Äh!VÖ¤%Ò¨P)[C 
X%RÂ"•‚K‚�*Kñ�s^Þí0¯ó^çÝ9gæ~?ɤwæÌܹ½sîïÜß»ËO’$©2L€$I’$™I’$I’	�$I’$™I’$I’	�$I’$™I’$IR+uÁ<¬#ÔÙºì*±HÆã‡$µ]dsçÃþaÿ�ÜI–ñCR[ð8I’$I&@’$I’d$I’$I&@’$I’d$I’$I&@’$I’d$I’$I&@’$I’d$I’$I&@’$I’d$I’$ÉH’$I’L€$I’$ÉHj©€ƒxÿp`GW›$I’z}�øóêܾ1�yL®ÄüVâýS€âô$àÇÀCÀ™þ¼YôÉ>æo+I-�?Ý®%¶3°w?m#ã ùN�¶àÝ:ƒèðøÚæ~ÜßÄÏMrÕK’$U�	�rµ˜š�s
ÉÎ7€}âôeÀjÂ霋�iñõ;�jæ¹�pi? ËU,I’d$åb!ð^œ¾	8x">ø<á(Î=1:ؘ“›G€å®FI’$™©ÌNÆÆDgF!º;þû*ð>°0›pdç®Ø¶p$ðœ«R’$I&@ÊÙ(àû„£>ÿ®cË;¿õ^¿Ólüx�pªÜÕ5óšâê”$I’	�r²0§ð|
ᇻG·ÚÏŒÉÎÀSÀ&Âénk€×€§còtOÍ÷ô'Scò4‡pMÐ&I’¤j°�R[G¸†gráÑœœH8Ú3x8¾9°8
X_Hœ6‡jþ, œ:÷&ðpk|Ïð8ÿ¿ŽÓ#ü	$I’ªÅ:@*󷵨?ÍÃGç>ª?¾B8�·×gâd¼¥cƒ¤ÄñÇH&@²Èþ1ô†¿'ÜÄ¥øáÔ^¹íK2’;0²ÈþÑ‘ýc
á´Û¥ÀUv·}I&@rFöÙ?:½ü�pÛþQv·}Iéã�7A�$©<ã	7ty˜æê�¤L² ÄŸWçö
û‡ýÃþaÿ¨zÿø9p>°/áî“cínû’ÒÇ ¹#û‡ìCïdà·ôÝõí»À�ìnû’ÒÆO�“”«�øð)C»Šç~º¦m:á¯ëûŸ,¼Þ
C¹uϺ€Ïgû×´[x|¤ÐvLͲB¨�¥Î0˜WØÙ_lƸj$) ÆÕq(»]ªªÿׇÇçËâëo+�½
Iâø:À¯�qqz0Ø\â²ÎŽß³3p%pG¡íRBÁÞ/ÆGï)P‹	Åx—’¦ÙÀÇý鳉ßÍÚ“â^S	w…ÛäO#Iiy
ÜÖ5ªãPv{;÷�*ôcG¹ó˜
¬‹‰Å3„#@½N>¬'êupsL,ZqÝEq‡¹x˜Ÿ¯¬ó™û„:¸€pT`50Âþ1dóH­äØ )ÓøcÔX£:e·›©Ê;¸—Ä„âÊ:m7Ƕy5¯‹ÉÑËÀ-^/»F µñßÙ…dé*à$àF©~—ÇeûÇÐÎ#uüµ�cƒ$ ¶Õ¨ŽCÙí&@ªêîé„Ó׎ªÓ¶x—¾SáŠn#Üzx\×Éà1¶¼Žg_`;ÂÑ«§€™ñõÑÀ¹1éÙø)áz k€+hÿ»…å?RÇ_ë96H2j;ã	§Ø¬%\hÝêvw`TÕÜ	qÛ8ø[^<>•ð—õ9À¶<
íà`!áK+Œ"œÖ6+ï™ÜRçõ„knˆIй´ÿÍr‰©ão®ñÝH’	�Al«Õq(»ÝHUÝÁ]	œ§¿\§»�'
;”·Çm`à¯1‘F8"st’ŸÕÀ7ûiëuKáÿÓëËÀÅqú.`{ÂÅùØ?†d©ã¯u€$™µ�FuÊnwFUÝÁ=¸·ð|á¯è‡î¬ö“BÛÎÀóÀ1ɸ°Ð6xŽpZYÎÞ!‘ê}ôÞø`}ÜÆŸ–×ì�މSOó‘¥SIDAT!ºø°§ý£éy¤Ž¿Örl�dÔ–æÇ�ª^Ý„ÛÝŽiQ»	Жv`pwÉìè¶n‚œÐ0Âém#ë´�!Ô5*û­ý£ùy¤Ž¿9Çw I•�?Öj¬Q‡²Û;ÝtÂßu+¡VÇ@}
¸3N	x”pÊM„#RÙ6þW§ímà�š×ÞÞwµ
‰ë�—
Ïß#Ô]z{€íÍÖ	jßs¯g;I�›%þ|î¬Tîþ¹íK2jcÖjmÔ]˜¾	8¨�]§'OÇé/Šb“¥G	½÷—-cËS7d$ûǶÆÿÔãGîãŸÛ¾¤¬Æ'¯˜ñÀ‰„#Ó´WѬ˜ÝM¸›ÖŒBÛÝñßW	×Jì©ý·þáÂò#û™÷¥„#˜Ij6þ§?rÿ$)Ï,(ñçÛ�u€Ê›G½#@£€géûkâu„ÛÇB8t^œî^ ÜJøBà[uæ_{haœÇ7ý¶è²äÿS�¹�nû’²ŸL€¶Î:@å'@�s
�‰„B–3€3õ]Š	ÐãÀLÂÝãVÆ×'뀯ŽÍ�¯ ³�×€¯¾«Û8`$ûÇ6ÆÿÔãGîãŸÛ¾¤,Ç'O�kl0¯¬—è«ãPv{§[G(9¹ðèN#œV1)&3Ç÷/G
LΉ¯o%œÚ¶€ðWÈ7	·¾µðžë�½
ßå6�ÇŽ²�Î{T!þ§?rÿ$)ë��2?ßîuš]þÜÛËìC5ufì�Êîc)ãg+Æ·Ü—Ïø!)Û±¥ì¨�ëàÅòçÞn$[ujK]‡§ìñ-÷å3~Hªlí[g¨–?÷v ùÛªSûXê:–ºOÙã[îËgü�TÙh>°Gáy7°ˆ¾:�ÚSkvùso7’¿­:µ�¥ŒŸ­ßr_>㇤,Ç–V�ÜœUx>•p×™MlO­ÙåϽ]’ÚU£:6×/Úß#ë|{€íÍÆÏ²ãoîË'IùfA%Þ:@Ö’±CJ1¾”ÿqùŒ’Úsl±PùËo ™IiÆ—²ã¿Ëgü�dÔ/ëYH&@RŠñ¥ìøïò?$™}ˆu€¬$ )ÅøRvüwùŒ’L€ê²�u€d$¥_ÊŽÿ.ŸñC’	ЇXÈ:@2’RŒ/eÇ\>ã‡$ z¬d ™I)Æ—²ã?.ŸñCRû�-íP¨Q�‡²Û­T®‰ÀƒxÿùÀžƒxÿG�‹âô8àxà`·©íU½ÎŽu€$i[³ ’?ßéut¬ÔÜf ëIR–	t~ë
mt:p(ᔵuÀŽ…èwÀI„£9?‹¯Oþ|8x¬Ÿhb|Ï�ÀŒîÀ¨#ú˜u€¬$IŽ?ÃZôEã��×�i¶gמ£ÀfàÀËÀ	…¶Ûb"´0þF§k€W€ç	ç°×»¶hðÙ˜ýÛ µ½²ã[³ó¯úòIRžYP>ßéut¬´íóèïÐjàà8`áö¬ÄÄç¸Âûž$ÜÅèZ`9áF½�ýèÿ¸1YšdH;¤VŒ/Ͳ�ñCRŽ-¹×²½³ëÕK€Æ/Äé.àÁšhiœž<§§·æÑE¸À·˜oí:xmpFmÛǬd ItüiÅ)p€y…`·ØPص=m{f.Âí}ìÜO8¥íIàŸ5ïßx¸øN|íAÂ9ì+	×	íZó¹oÇù­î.ÃÛ½Jí¬ìøÖìü«¾|’”oTòçS×ñ±}ëíeö�f籡0_=ãúië&œÖÖ_r¿=¡6�ÒÇ©ì>–2~æðý$^ÆIÙŽ-Ö²P®	�L€dkFÊø™Ã÷“xý?$U6ëìXHîœJiúXªø™Ë÷§^ÆI•M€À:;Ö’;§Rš>–"~æôý©×ŸñCR% ñÀz`-án]¶çÕn$[ujK?sùþÔëÏø!©²	�ux¬$wN¥},UüÌåûS¯?㇤J&@Öá±�Ü9•Rô±”ñ3‡ï'ñú3~Hªl4Ø£ð¼XD_�ÛÓ¶›ÉßV�ÚÇRÆÏ¾ŸÄëÏø!©²	�ª½ƒkÿ°Hö1ùÛJÊ&þsH’$Iª
 I’$I&@’$I’d$I’$I&@’$I’d$I’$IYè¢ùx;KÙ?$?$I.’$I’”Kîã)p’$I’*ÃH’$I’	�$I’$™I’$I’	�$I’$™I’$I’	�$I’$™I’$I’	�$I’$™I’$I’	�$I’$™I’$I2’$I’$ I’$Ijs]À®I’$IUðÉq¿ÉŠ_»:IEND®B`‚urwid-urwid-69907e4/docs/manual/images/urwid_widgets_1.xcf000066400000000000000000001763541526073734300235730ustar00rootroot00000000000000gimp xcf v011@h–B³üB³ü#icc-profile-nameICC profilegimp-image-grid¬(style solid)
(fgcolor (color-rgba 0 0 0 1))
(bgcolor (color-rgba 1 1 1 1))
(xspacing 10)
(yspacing 10)
(spacing-unit inches)
(xoffset 0)
(yoffset 0)
(offset-unit inches)
gimp-image-metadata


  8 8 8
  2023:09:22 17:04:32
  190
  360
  832
  1
  3
  GIMP 2.10.34
  248/7
  248/7
  1
  8 8 8
  6
  110
  256
  328
  4081
  1
  6
  3
  2.0
  Linux
  1695395073461433
  2.10.34
  image/png
  1
  GIMP 2.10
  2023:09:22T17:04:32+02:00
  2023:09:22T17:04:32+02:00
  gimp:docid:gimp:4d9cf2f4-e0af-4cff-946b-e1f53b18658a
  saved
  /
  xmp.iid:7a837885-513b-4668-86d8-b616f06caf41
  Gimp 2.10 (Linux)
  2023-09-22T12:04:57+02:00
  saved
  /
  xmp.iid:aea740ab-0b1b-445e-af3e-89472a139341
  Gimp 2.10 (Linux)
  2023-09-22T17:04:33+02:00
  xmp.iid:adaf6b4d-981e-4479-b4cc-9261ec7fbf78
  xmp.did:e82c3808-93ba-4862-b41e-41821c4b8d2a

ÂEaö…"qT
Buttons etc.ÿ!?€	"
 
"2%$ÿÿÿÿ#ÿÿÿÿ>gimp-text-layer"(markup "Buttons etc.")
(font "Sans-serif")
(font-size 14.000000000000002)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0 0 0))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)

T19TQˆ@ø¹ÿÿýîÁS
ýz3ýz3#÷¹ÿÒ¹Üþú3	ýPýgýPýg#ì¹ÿ_Ñÿaìú(óù!Nåÿý‹Nåÿæ‹
‘æ÷ÎWáÊQÖøÎ:tÜùæ¦Å”ç÷¹ÿ_&äü=ìú(óù!{èÿÔ¸d{èÿÔ¸d²ÿçÂøüUáüöÆâÿÞCý÷Áàïù²ÿä¹ÿðú~ìú(óù!¯ÿgã¯ÿg.ûñdþËáÿ� ùù!QþÔå.ûå¹ÿÒ¸Òþè0ìú(óù!¯ÿgã¯ÿgVþÈ%úô
áü<êú(
Äþõ�$þWþÿê¹ÿ_§ÿ—ìú)÷ù!¯ÿgâ¯ÿgWþÈ%úô	áû/èú(ZÌþî%åXþí¸¸¹ÿ_«ÿ æýDCüù!«ÿpâ«ÿp+úñdþÌáû/èú(ºÿdÄ/ûç¹ÿÒ¸ÐþüMµÿÚ™æÿù!‡ÿ츆‡ÿ츆ªÿçÁøý[áû/èú([÷É¿øü?Ä®ÿí¼¹ÿÿþ÷ÊU#½øè†£ù!»ùð{»ùð{ŠãùÓ\áû/èú(5Ê÷úØjü‡Þú@���gýz3ýPýgûÈBNåÿø‹’äúápðøó+{èÿÔ¸d¸ÿëÁìeðlþ‡¯ÿg1ûó"ÿõ§¯ÿgWþɸõ{¯ÿgXþÈÅ
«ÿp8üò !Æò>‡ÿ츆Ëÿë½ã‡!òñï°»ùð{¨ïû×LÜÚc*
Editÿ!?€	"
 
š2%$ÿÿÿÿ#ÿÿÿÿ¼gimp-text-layer (markup "Edit")
(font "Sans-serif")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0.91372549533843994 0.7764706015586853 0.68627452850341797))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)
":000|øPþÁÇë-þ¹ÿþ÷ôPþÁ„¤z3ø¹ÿÒ¸¸²üNþÁýPýgü¹ÿ_ò'ÁöÏxüÁáû/Nåÿú‹¹ÿ_êÑÿêÅöþÁáû/{èÿÔ¸d¹ÿíÁ7üò!tþÁáû/¯ÿgç¹ÿÒ¸¸‹XþÆ7ûÁáû/¯ÿgü¹ÿ_ïXþÅ4ûÁáû/¯ÿgü¹ÿ_ï9üðmþÁáû/«ÿpâ¹ÿÒ¸¸²ÔÿéÃóþÁáû/‡ÿ츆¹ÿë÷+ÆøÌ?îÁáû/»ùð{�Textÿ!?€	"
 
2%$ÿÿÿÿ#ÿÿÿÿHgimp-text-layer,(markup "Text")
(font "Sans-serif Bold")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 1 1 1))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)
®ÊâDDD‘ÿ¶ÿþÏòz3ƒ¸ºüýÁ¸•ýPýgé÷ù!”ç÷ÈB{þлþ•OåÿÇ‹÷ù!²ÿäÂøó+	ÑþmTýá{èÿÔ¸d÷ù!.ûålþ‡>úëáýT¯ÿgù÷ù!Wþÿó§™ÿÿ®¯ÿgø÷ù!Xþí¸ó{´ÿÿïÿgç÷ù!/ûç
[ýÞÏþm«ÿpÈ÷ù!®ÿí¼Æò>äýS=ûí ‡ÿ츆÷ù!‡Þúï°�ÿ¸¡ÿ¯»ùð{‘7
SolidFillÿ!?€	"
 
ª2%$ÿÿÿÿ#ÿÿÿÿgimp-text-layerú(text "SolidFill")
(font "Sans-serif Bold")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0 0 0))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)
727JLLLíøáû/Çë-üPþÁëÇë-áû/áû/*´ñòÎøáû/„¤úPþÁ¹ÿéó„¤áû/áû/
àþØÈòÂüáû/ôNþÁ¹ÿи¸¯Ñáû/áû/:üß	
‘æ÷ÎWáû/áû/'ÁöÏxüÁ¹ÿWîáû/áû/áû/&ùøbâ²ÿçÂøüUáû/áû/ÑÿêÅöþÁ¹ÿW˜áû/áû/áû/ŽþþÖR.ûñdþËáû/áû/7üò!tþÁ¹ÿи¸†áû/áû/áû/SÑþþ†VþÈ%úô
áû/áû/XþÆ7ûÁ¹ÿò¹áû/áû/áû/ÝgüõWþÈ%úô	áû/áû/XþÅ4ûÁ¹ÿWÍáû/áû/áû/)4ñù"+úñdþÌáû/áû/9üðmþÁ¹ÿWÍáû/áû/áû/[þèÁÞþŪÿçÁøý[áû/áû/ÔÿéÃóþÁ¹ÿWÍáû/áû/áû/,³èú眊ãùÓ\áû/áû/+ÆøÌ?îÁ¹ÿWôáû/áû/áû/2Dividerÿ!?€	"
 
22%$ÿÿÿÿ#ÿÿÿÿÂgimp-text-layer¦(markup "Divider")
(font "Sans-serif Bold")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0.68627452850341797 0.7764706015586853 0.91372549533843994))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)
 W2 s2 ‹èèèÒüÇë-üÇë-üPþÁò¹ÿÿüèª8„¤ü„¤üPþÁ÷¹ÿÒºÝþú]üNþÁ˜¹ÿ_týïáû1ÍþYaþÅáû/'ÁöÏxüÁ”ç÷ÈBáÇ@Ñô¹ÿ_×þbáû/tþ©±þkáû/ÑÿêÅöþÁ²ÿäÂøó+áûóâ°¹ÿ_Ñ®ÿ…áû/÷íñôáû/7üò!tþÁ.ûålþ‡áþ–¹ÿ_â°ÿƒáû/¸ýPXþ®áû/XþÆ7ûÁWþÿû§áü8Ú¹ÿ_Ýþ\áû/]þ ¨ýSáû/XþÅ4ûÁXþí¸û{áû/ѹÿ_…þëáû/îÜåèáû/9üðmþÁ/ûç
áû/ѹÿÒ¼åþõTáû/¢üþ—áû/ÔÿéÃóþÁ®ÿí¼Æò>áû/ѹÿþùÞ�+áû/Gýü=áû/+ÆøÌ?îÁ‡Þúï°áû/ý@hurwid_widgets_1.pngÿ!?€	"
 
%$ÿÿÿÿ#ÿÿÿÿ#}@h#¹ü¼üÈüÔüà@h&9*O.50©0Ë2S3)4™4Í6#6Z7l@­H¸NLTY>\Á_Èb¥f&kôqýtw}Ö„ð‰½ŒíŽ=ŽS�’”˜™¨žyŸ½¡Û¡ë¢÷«�±W¹6¿EÃ÷Ê$ÎlÒhÔÔÔ Ô0Õ<Ø@Ý‘á¨áÎãçäÈæüê˜ì¦ì¶ìÆìÖíâñ>õöö÷÷�÷¢ø˜ú®ûÞûðüü/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿþùãÿ/ÿÿþóëÿ/ÿ/ÿ/ÿ/ÿ/ÿ/î/î/î/î/î/î/î/îîþçÊî/îîþáÚî/î/î/î/î/î/ª/ª/ª/ª/ª/ª/ª/ªªþ§¡ª/ªªþ£Ÿª/ª/ª/ª/ª/ª@ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ4ý"0ÿÿÿ˜ýˆh ý¸ÿÿÿÿÿþûˆý¸ÿÿÿýÿðû3vöÿSýÿÿýÿðý›ÿ—ü'û*þÿýÿðýjÿŒý#«ñÿþ×Gþ{ìÿþõšý¬ðüNÕüÿýÿðüÒÿA÷Jæ“j[‡íÿBømÿÓ†Zj™Áý¸ÿû†ÿê„ÿýÿù�û¹óítþý%ôÊýÂû#ÿõ¸ÿ1üÞÿÿÿþÏV
ø·ú°ý(õ¸ÿ ÿZÿýÿðû?Çÿxï#ŽÀÝèè÷ÿQÿö¬Zõ¸ÿÊÿÿýÿðæ÷õ@û÷´’‚€Ñÿ&M¾ÿÿüº+õ¸ÿéûÿýÿðøÓÿ>Öö+ý¹ÿ&í#tÍÿì¸ÿÌÿÿýÿðùéÿ2
þÈüèÿ&ðµÿW¸ÿ£ÿRÿýÿð÷tÿð÷÷ùzÿÿ&ð¦ÿK¸ÿ9þÔÿýÿú û«ÖÿþPæªÿÅfPžòÒÿ&é¬gFX”þà
¸ÿû•ÿàtÿÿÿüõÔ¡,þ·ÿ÷Ü5¢ÿ&¨ðÿþ»(ý¸ÿüfîÿÿý =û#=5ýÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ/ÿÿ'éÿ(éÿ)éÿ*éÿ+éÿ,éÿ-éÿ.éÿ/éÿ0é
ÿ1éÿ2éÿ3é
ÿ4é	ÿ5éÿ6éî'Æî(Æî)Æî*Æî+Æî,Æî-Æî.Æî/Æî0Æ
î1Æî2Æî3Æ
î4Æ	î5Æî6Æª'¯ª(¯ª)¯ª*¯ª+¯ª,¯ª-¯ª.¯ª/¯ª0¯
ª1¯ª2¯ª3¯
ª4¯	ª5¯ª6¯?ÿàý0	ý!0ý�—
ý�˜>ù]˜:tÿ\	ý°ÿý»ÿCü'ÿÿ›ùÏÿ/tÿ\	ý°ÿý{ÿ‚üfÿåÚøýí
	ý°ÿý'ý;ÿÂû¦ñ}ÿýNÿ¯
ù
°ÿý"ÿþétüõùûåµ<ÿZøŽÿolðV÷fêÿûË-°ÿ÷¤÷ÿò©
î½ü[o«®õ»ÿA%ÿv÷™øÍÿ/tÿ\êsÿäyb¬êÓÿÆÿ¶elÈ¿þÊÿÿä{ÿ�eÿ7ÀÙýítÿ\ðíð‘ÿÿYÿ¦ÅÿÊÿ~ð;ÿÀ¤ó‚ÿLÿ¯øtÿ\gÿƒ÷÷ÿºÿ)ùMÿÊòðõøã¸DÿXŒÿnøtÿ\ŒÿLøÑÿÞùùÿÊÿ
ñºÿ?#ÿyû—Ëÿ.øtÿ\§ÿ6ù·ÿôéùÿÊAÿ
úzÿcÿ:úÈ×üìøtÿ\ŽÿJøÎÿ×üù!ÿÊ(ÿ
ú:ÿ¾¢õûŠÿaÿ®øtÿ\lÿz÷ôÿ¯ÿ>ùbÿÊúÿ	ûô÷å¼ûLÿÜÿn÷tÿ\õæðÿÿ@ÿÃ
ÛÿÊû“KZ•§
üºÿÿ|ûýÿÿ.ýtÿ\ö€ÿ×aJ”òÚÿ÷ ÿà— ê“þÈûÿÿò
üzÿÿ=üÐÿìýtÿ\÷v÷ÿÿã<°ÿøtÌòÅyÿ­ý?)$ý4
ý<ÿŒ6ÿüÀÿ<6ø©¯rYzØÿ«7øuâÿÿþâw9ý Èÿé/é/é/é/é/é/é0é0é0é0é/é/é/é/é/é'éÆ/Æ/Æ/Æ/Æ/Æ/Æ0Æ0Æ0Æ0Æ/Æ/Æ/Æ/Æ/Æ'Æ¯/¯/¯/¯/¯/¯/¯0¯0¯0¯0¯/¯/¯/¯/¯/¯'¯?ÿý<ý>ÿ’<ý>ÿ’4þý>ÿ’û)%ò�ëÿÿï……óÿùðûE™øÿþën$òÏÿ±hc²ÿ°B˜ÿÅxö"£ÿ¾{Yp¬•$þÿyø™ÿG>ÿ’üôáÿ$þçø"ÿ›>ÿ’üäç	)ÿÖpøqþÄ>ÿ’ú‡ÿìšK	&ÿü÷úÔ>ÿ’øeÕÿÿö $ÿ°	ý>ÿ’ù3†ßÿË#þéý8ÿ›	üåÿ!#þÿŠùÿÏþüØÿ#åÙÿ·bH`‰âlÏý¼†€%$ò˜`Bd¯ÿ¸$ýŽ÷ÿýûº6÷(²æýÿJÁøÿýû¡&û#?)û(@/çÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿg/ÿÿ³/ÿÿ³/ÿÿu/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ'ÿéÆ¯?ÿÀÿéÿéÿéÿéÿéÿéÿéÿéÿéÿÿùéÿéÿþóêÿéÿéÿéÿéÿéÿÆîÆîÆîÆîÆîÆîÆîÆîÆîÿçÆîÆîþáÍîÆîÆîÆîÆîÆî¯ª¯ª¯ª¯ª¯ª¯ª¯ª¯ª¯ªÿ§¯ª¯ªÿ£ª¯ª¯ª¯ª¯ª¯ª?ÿÀÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿ'ÿé&ÿé%ÿé$ÿé#ÿé"ÿé!ÿé ÿéÿéÿ éÿ!éÿ"éÿ#éÿ$éÿ%éÿ&é'îÆ&îÆ%îÆ$îÆ#îÆ"îÆ!îÆ îÆîÆî Æî!Æî"Æî#Æî$Æî%Æî&Æ'ª¯&ª¯%ª¯$ª¯#ª¯"ª¯!ª¯ ª¯ª¯ª ¯ª!¯ª"¯ª#¯ª$¯ª%¯ª&¯?ÿÀÿé¯é¯é¯é¯é¯é¯é¯é¯é¯ý¬©®¯é¯é¯é¯é¯é¯é¯é¯Æ/Æ/Æ/Æ/Æ/Æ/Æ/Æ/ÆÆýÿÅ$ÆÆÿÅ'Æ/Æ/Æ/Æ/Æ/ÆÆ¯é¯é¯é¯é¯é¯é¯é¯é¯éýåáèé¯éÿèé¯é¯é¯é¯é¯é¯é?ÿÀÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿ
¯ÿ®ò¯
ÆÿÄòÆ
éÿçòé?ÿÀÿ¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯éÆ/Æ/Æ/Æ/Æ/Æ/Æ/Æ/Æ/Æ/Æ/Æ/Æ/Æ/Æ/ÆÆé¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯?ÿÀÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿ¦éþçãXé¦ÆþÄÁXÆ¦¯þ­ªX¯?ÿÀÿ'é'é'é'é'é'é'é'é'é'é'é'é'é'é'é'é'Æ'Æ'Æ'Æ'Æ'Æ'Æ'Æ'Æ'Æ'Æ'Æ'Æ'Æ'Æ'Æ'¯'¯'¯'¯'¯'¯'¯'¯'¯'¯'¯'¯'¯'¯'¯'¯?ÿÀ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿQ.¯.¯.¯.¯.¯.¯.¯¯ûYWWn�¯þœ¯¯úK]C
y¯þ¥Z¯¯ú�¯¯[.¯ô‹fu¨¯Ÿ{®¯¯„•¯ô’�¯¯‚”¯¯~¤©z¯¯ë�¯§8E¯¯aG¦«$`¯|Ÿ¯ñq-¯ª8¯£§š¯¯a¯ì(§¯¥€¯®[¯˜x�¯ñŸª�3®v(¯š¯¦¯í„¤�<1¯ˆª¯¯O9¯¯sT¯ò�Sg.ŒHV¯š¯Œ¯í�¯¯“ŸŠª¯¯L:¯¯G6¯òKS%•\^„¯š¯�¯ì�¯¯j¬§
€¯¬_¯sž d¯òy¯Š«¯š¯§
¯ë'1n¯¯j>%©–
Н”Œ¯ò¥A¯­
1¯¯š¯¯h¯ü„ƒƒ–¯ù•q€¬¯—�¯þ“”¯ö‰›¯¯�—¯¯ª„¯.¯.¯.¯.¯.¯‘.é.é.é.é.é.é.ééÿvtýäÍ&ééþ]týäÉ
ééþ»éþÉ
éñ»ˆšßéå¡çéÏ£éé˹éö˸ééµ¼éé°Òéþ»éìÉ
éé…	^+%Ûé¹émÇé^téö¯#éé1éæÈéæXéÉ
éÞ¦éè,uéX{é0LŠé!²éöä	ÏÅ2Û·é鿝ÚÚàéÉ
é¹áéémHé•>Ù«MÎ
äé÷A’ˆoW¡zZééþ»éíÉ
é»áééiJéјYá�Cé÷~UK­”d<—ééþ»éíÉ
éà§éå'zéé%(–é;(€é÷»âÐ!ÓéQ.Æ.Æ.Æ.Æ.Æ.Æ.ÆÆûdcc}²Æþ±ÆÆúUiL‰ÆþºfÆÆúŸÆÆf4Æôžt„¾Æ³‹ÅÆÆ•¨Æô¥£ÆÆ“§ÆÆ�º¿ŠÆÆëŸÆ½?NÆÆnQ#!¼Á(mÆ�´Æñ3ÆÀ@Ƹ½®ÆÆmÆì-½Æ»�ÆÅ"fƬˆ’Æñ³À’:Ć.Æ®Ƽ	Æí•º¢D8ÆšÁÆÆY@ÆÆ‚_Æò!’^t4žRbƮƞÆíŸÆÆ¦´œÁÆÆVBÆÆP=ÆòU^*¨hj–ƮƠÆìŸÆÆxý‘ÆÂkÆ‚³$qÆò‰ Æœ"ÁÆ®ƽÆë,7}ÆÆxG*¿ªœÆ§žÆòºJÆÄ7ÆÆ®ÆÆvÆü•””©Æù¨€�ÂÆ«¢Æþ¦§Æö›¯ÆÆŸ«ÆÆÁ–Æ.Æ.Æ.Æ.Æ.Æ‘.Æ.Æ.Æ.Æ.Æ.Æ.ÆÆÿdcý®!ÆÆþOcý«	ÆÆþŸÆþ«	ÆñŸtƒ½ÆÃ‰ÄÆ°ŠÆÆ¬�Æö¬œÆÆš ÆÆ–³ÆþŸÆì«	ÆÆqP$ ºÆ�Æ\©ÆPcÆö•ÆÆ*ÆÄªÆæ	KÆ«	ƽ	�ÆÅ%cÆKiÆ(@uÆ—ÆöÁ°¨+º›ÆÆæ•ºº¾Æ«	Æ�¿ÆÆ\=Æ5¹‘A¯	ÂÆ÷7|t_J‰gLÆÆþŸÆí«	ÆŸ¿ÆÆY?Ʋ�K¿y9Æ÷kH@“~U3€ÆÆþŸÆí«	ƾŽÆÃ!hÆÆ "Æ2"mÆ÷ŸÀ±³ÆQ.é.é.é.é.é.é.ééûvtt“ÑéþÐ#ééúd{Z¢éþÛxééú»ééy=éô¹ˆ›àéÓ¤èé靯éôÃÀéé­Äéé¨Úá£ééë»éßJ\éé‚	_)'Ýä0€é¥Ôéñ–<éâKéÙÞÍéé�éì5ßéÜ	ªéè(yéË ¬éñÓâ¬Eç�6éÍéÝ
éí¯Ú¾OBéµ	ãééiLéé™oéò'¬o‰=º`séÍéº	éí»ééÄÔ¸ãééeNéé^Héòeo1Æz}#°éÍé¼éì»éé�åÞ
«éä$~éšÒ*„éò¢%é¸(	äéÍéß
éë4A%“ééŽ	S1áȸéĺéòÛWéæAééÍéé‹é¯ÿÇéùÆ—ªäéÉ¿éÄéö¶Ïéé»Éééã°é.é.é.é.é.é‘.¯.¯.¯.¯.¯.¯.¯¯ÿYWý¬š¯¯þFWý¬—¯¯þ�¯þ—¯ñ�ft§¯¬y®¯œz¯¯˜‹¯ö˜Š¯¯ˆ�¯¯„ž¯þ�¯ì—¯¯dG ¥¯‹¯R–¯GW¯ö„¯¯%¯­
–¯æ

B¯—¯§}¯®!X¯B]¯$9h¯†¯ö«œ”&¥‰¯¯æ„¤¤¨¯—¯‹©¯¯R6¯p/£€:š¬¯÷1nfTAy[C¯¯þ�¯í—¯�©¯¯O8¯�rC©k2¯÷_@8‚oK-q¯¯þ�¯í—¯¨~¯¬\¯¯q¯-`¯÷�ªœŸ¯?ÿÐ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ@.¯.¯.¯.¯.¯.¯.¯¯þeR¯þª’¯¯þ[D¯þ�¯æ‰g�[D¯¯¦po¡€£¯¯¤th”¯¯dxxª¯æI&D¯ª"K$‡¯‘8Iw¯D	RR¥¯鄯®D¯i¯¯ˆ‡¯1V¯¯s¯�¯鬯¯ED¯GI¯¯¬‡¯	3AA;ª�¯𪯯CD¯L@¯¯§‡¯f‰ý­�¯ð�¯¬D¯w¡¯n‡¯0D¯û®¯£	­¯æD+D¯®<#1‹¯– N=-¯¯3%<£¯û�r–𔝖÷Ÿ¯¯ª€m~¨¯ýœ„«¯¯ûf‡x/1"¯¯ûa75Y§"¯.¯.¯.¯‘.é.é.é.é.é.é.éýæÙéþ�Véþæ¾éýètàéþ’AéþÙéáè äééÀ‰³’Aééäš�ϲÐééៈ½ééž  Ùéáæ×é—	[05Aéè;#g* šéÐ%BgˆétmmÏéäæ×é–éé8A饿éΚéZZéé³äÙéóæ×ÔÔéévAéyGéô šé%;WWVÉÙéóæ×ÖÑéésAé;éûšé$t¶ýáÙéëæ×é’éè5Aé¹Í鬚éZAéûçéæ×é@.Æ.Æ.Æ.Æ.Æ.Æ.ÆÆþr\ÆþÀ¥ÆÆþfMÆþ£Ææ›t fMÆÆ¼~¶�¸ÆÆºƒv¨ÆÆqˆˆÀÆæ	S!+MÆÀ"'U)™Æ¤@S†ÆM
\\»Æ镯ÄMÆw ÆÆš™Æ7bÆÆ‚Æ£ÆéÂÆÆNMÆQRÆÆÃ	™Æ
:JJCÀ£ÆðÁÆÆLMÆVHÆÆ½™Æ	t›ýÄ£Æð’ÆÃMƇ¶Æ|™Æ6MÆûÄÆ¸
ÄÆæM1MÆÅD(8�Æ©$YE3ÆÆ9*D¹Æû¢�ª¯¨Æôª©ª´ÆÆÀ�{�¾Æý°•ÁÆÆûs™ˆ58"ÆÆûn>B"ééû‚IFwß"é.é.é.é‘.¯.¯.¯.¯.¯.¯.¯ý­£¯þvA¯þ­�¯ý®W¨¯þn1¯þ£¯á®x«¯¯�g‡n1¯¯«tlœ†œ¯¯©wfޝ¯wxx£¯á­¡¯qE$(1¯®-N t¯œ1Nf¯WRRœ¯ä­¡¯q¯¯*1¯|­¯št¯DC¯¯‡¬£¯ó­¡ŸŸ¯¯Y1¯[6¯ût¯-Aü—£¯ó­¡¡�¯¯V1¯_-¯ût¯W‰ý©£¯ë­¡¯n¯®(1¯‹š¯�t¯C1¯û®¯­
¡¯?ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/î/î/î/î/î/î/î/î/ª/ª/ª/ª/ª/ª/ª/ªÿÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ0ÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿÿHÿ		þÿÿÿÿøˆˆ¼Ëˆˆ	þÁ'ÿÿÿÿîp�$|<o)U1ÞŒwð$}{eb
f7„:ÿÿÿîp�`­B‘~j£
Ò1ÎV7ïd¼GM'ÙdiÚô•KÐÿÿÿïp�¾N8>Ô«ÕhÁ'þÁ'ôE¡ª?óoÿÿÿûp�è|x÷}÷*Á'þäôk�ˆeÚ^ÿÿÿïp�—N<Ì=Î	ª6þ™Tô!ÄÎÚ^ÿÿÿîp�$§¸µ¥Ö&rœNÀ¿ï'°»º<ÅÆ{Ú^ÿÿÿ
þþ
þÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿû
ÿÿÿÿû#Å]‹ÿÿÿÿþþ
ø#ÅþÿÿÿÿÿöiÂÂã ¾/å#£¼Ä;#ÅZ†š¡ÊËSCºÉÊŽ?¯ÿÿÿ÷ÓÂ*ûGå–Z#Å]‹Ÿ§ÛFò¬@Éÿÿÿ÷U•wuñþâ	ê#Å]‹Ÿ^¼/F·cˆ&ë°ÿÿÿø6°”VèþÅ#ê#Å]‹ŸyÔFÒ~jÓÿÿÿøÕfXß
èåi·?>##Å]‹ŸÑLx¾FòuNÚ£‰ÿÿÿùjxtæ1�ˆb.EŸ]†lF¢e‹BÿÿÿþŸIþF¢ÿÿÿÿþ^+þ)`ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿü…yþ#fü�ÿÿÿüwÍþ;­üÚÿÿÿØ]�a88#PVŒfiÍoŒJ;­U~ÚÿÿÿØl4bÄ1¾‘di4\ÍáÍ'V5‘ƒ;î…R×#ÚÿÿÿØKoÝÑ àHo×%áÍ[wÒ;àwqÚ-ÿÿÿ×
ÔgCÐ8s~M£ÓiDÆEáÍC¿[Gð;ÁW•ÚYÿÿÿ×L�÷8Ù¬B?ª	ðEáÍ‘XLó;÷�NÚÿÿÿ×	ɶÆÉ7µß÷ÊÂDáÍ7Ѹ¥é;ÊËÆž	Úÿÿÿÿ
þþþÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ
ÿÿÿÿÿ
ÿèÿÿÿÿÿ
ÿèþÿþþþÿ7éÿ8éÿ9éÿ:éÿ;éÿ<éÿ=éÿÿ>éî7Æî8Æî9Æî:Æî;Æî<Æî=Æÿî>ƪ7¯ª8¯ª9¯ª:¯ª;¯ª<¯ª=¯ÿª>¯ÿ?ÿÂþ
þ-þ­;
þ@¨,ë'Ó—x-}2K.ugŽÑx<ÃI#EIIl0†A‡tX0€P.@huHx/¿f8yž@£d¡ÇUˆœd»8Åj}¿Ãujõ¡B\9N×¼ÂM•›cænaÛ¹sEÇ'±x­;ÒA8LÀ¡iß@¨Ò'À
½·¸'üÄÆ Â÷©ç°^ŒÁÁYõ|òµ°Ðo®:èðjø°Œá©È¸$¡¸®.øcpÀ)…Âö6²C¦ÞöÕus®:èñèÿ,�U›A+¨÷°»¸
Û;Âö6²Ž\°<öÕus®:èõèà_ˆé°þèúÜVÂö2´owÌöÕus®:èôæÚ^ŠÉ	÷2AK™’D¿Â÷ÚK"ØMtÇöÕus®:èñ¼`3Ú^Šk­=6_÷–…-[Ž6aø2sD€`öj	:9Wtñgfm/E-Š�EþT4÷lR70þK/÷1·px‹]üOtåSvfC)SK·³·x%‹dnoæc,ƒeW|[�b5:"QÆkIÎå
Ü_ñ3Î�YŸ’NÄ·�•8‹Ñ\w²æÔ­LÀ¶t[Ók4aÆ'Ç”aÀ88ŽmåÁ&¢|vR��YÝe·px‹áêÔ6\¾Ë
Joܽ/	ß	ÇxÿIå~jÉ·•J�YÒE·px‹]Û
æÔLœ¼,ÔgDÎ;U–^’Óâ:Ó ÂØ�YÍ‹·Y‡‹]Û
<æÔLœ¼,I 
ö;ÙÊ+b¿¬ÃMâò\ˆÃ�YJÎê·º¿;‹]Û
òíÔLœ¼,ȶÇÇ:†Äþþþ)¢ÿþ�W%þ
ü
¹³;þOþ3þ[þžJ3þ­;þþÿ-é/ééþçèéþäåé/é/é/é/é/é'éÆ/ÆÆþÄÅÆÂÆ/Æ/Æ/Æ/Æ/Æ'Ư/¯¯®¯þ«¬¯/¯/¯/¯/¯/¯'¯ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ'ÿÿ'ÿÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ(þ'aÿÿÿÿþ	?þ&þC¥ÿÿÿÿþ àþˆpü[|V¥ÿÿÿÿþ àîˆp&u�PwI0=`t÷dc
…RÍ¥ÿÿÿÿþ àîˆpÊG3MC½Fyžgân5ûÒXYÊþw¥ÿÿÿÿþ àáˆpÙ[Ÿf88Ùg®
  	a—88¶9W¥ÿÿÿÿñó¡g ŠÔ€ËŠxög�7¨¨1�«xû0�¥ÿÿÿÿëè?
ã9ãwng�þ9¬ûÏÆ¬¥ÿÿÿÿìIéÃîˆÕ«¸t�¼°¾g�ù„«È#ÿÿÿÿýýþ
ýÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿÿþÿÿÿÿøál6²ÿÿÿÿöÍ6²þ	
ÿÿÿÿìsmŒº>šÙŽð°q6ǻˤùt¼¹ª‡ÿÿÿÿë5²ÅÅ	‚]áÍ6ô¸-ùx[ŒÿÿÿÿëೄFÅáÍ6¸‡`ù=¦·ÆœŒÿÿÿÿì®’p@—ÒáÏ6²†bùÉq¢Œÿÿÿÿìkù,î“áÖO)6²†bùÂYMË¢Œÿÿÿÿì{d0p)oRYC1ù$†ƒ5QFÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿûsÿÿÿÿúÕgÿÿÿÿáKŒX${…66;noaéxQZwI#ÿÿÿÿáÉ€7\BG=¶JtàfjÖ7Þ8%&Â0ÑO�¬Kÿÿÿÿþ,¼ç*e•št§°8Õ&‚dÞKÿÿÿÿþU—æ|žRnºtˆ�\Õ&¨DÅ(Kÿÿÿÿý
Ö	æÊ…ºtËÐÚ&Â^ŠØKÿÿÿÿáeɬ­d̤¹t´ÍÎyŒ¿�& ¿ÅQKÿÿÿÿùtþ
ÿ
ÿÿÿÿtÿÿÿÿ
ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ&鯝ÿ?ÿÄùQ8vÿ‰þ[.	
þù‰_jæÿèþšN	þ~j
þ¬<àvfŠ_]PòxY^�`è6…3šN5}*뺰xyEi _'Ó—x	ÞÛ`pç_È &ë8)j4eÀô¥H½]šN�‘@¯Q뚊8M¸D‚“V·ÉA¾f8õH›½_È æìLoßú!7±šNÙ:8\­î~jª^89Ú–Õ}¬<âk{�_È æÔfCÔ3õÕšNèxÿgú~jÖ„x÷hû;¬<Þ'¿Ý_È ßQ˜ù3ûG]ŽšNÃ"îgy‚c,Ô6Õ•K߉ËϺ_È ¿Ž˶ÅË2ì±À»$šN<³³»�ì ¼¿1¡»²µ
Ð5]°?¿¿þ
ÿÿþ
þ
ëþþ$úG4™Oþ;­þÿÿú†bÿþþþ;­üŸÈÑ_Ù\ÀŠߛŹ*Ùΰ'”LCºÁ›¸˜ÅÎ=:õ¹8;­ü¬ÔØÙ°>ç`NŸ†b™Ỏg¾|@¥/£;­ûbÝ
ØA©c‰ç	ã†b™OÒ:²¾0Øš·°ä;­û\ÜØ"Ä€jç1É‚d™OÞW“¾*Ú„`(ë;­û\ÜÙÉoPÝçª@«vO¯B™O¤ŠDÍ@¾*Úy”:£é;­û.nÙ	b~ç5’HK}L'Qˆ'_m
o‘2tVþç=ÿ‰0üIT";ûévUÞ:û¿¬;:û�›M:û�›M:û�›Mûéÿéÿéÿéÿéÿéÿéÿéÿ$é
ׯîÆîÆîÆîÆîÆîÆîÆî$Æ
ׯª¯ª¯ª¯ª¯ª¯ª¯ª¯ª$¯
×ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿ$üu©©R×ÿÿÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿü!HH)ÿÿÿÿüxLj¦ÿÿÿÿý&„<þxˆÿÿÿÿúkŶZ	üx©Hiÿÿÿÿü¨ÑoüxLj¢ÿÿÿÿûlƱV	þxˆÿÿÿÿû(„Ò¡üxˆÿÿÿÿý=&üxéÐÏÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿý˜Žý�˜ÿÿÿÿý(ÆýÎÿ6ÿÿÿÿþ(ÀþÉþÔÿÿÿÿþ(ÀþÉþúÿÿÿÿþ(ÀþÉþ7Úÿÿÿÿþ(ÀþÉþÿÿÿÿÿþ(ÀþÉÿ®ÿÿÿÿþ(ÀþÉÿ'éÿ(éÿ)éÿ*éÿ+éÿ,éÿ-éÿ.éî'Æî(Æî)Æî*Æî+Æî,Æî-Æî.ƪ'¯ª(¯ª)¯ª*¯ª+¯ª,¯ª-¯ª.¯ÿ?ÿÉú0þÖ	úLœ_‰0ä³PXfšÊx8ªÀx-zB+A\|ýwbä˽+
Ûp±8‚£8@ÊK™š]ãnZØú|ΧIã³!½+
ÛLœ_‰”Rà]­¾)ü4‘Ü]ãv�¼,ÛLœ_‰º2Ö]‹­;ûwÊ«8ã—|ŒZ[Û5«H˜px#Ë]‹­;ûPÀ¾cã 1лžÛ³¿W¹¿H§¾ÃF]‹­;þF þÿ¬?ÿ?ÿÀùHd7‰ÿ‰ûHH>	ùà¹q¨‡èÿèú
ùˆ’ÔWÿZÔè)‚HxGj‡/èFA
óH»mYV)
oÿ×ê J»QH»F}™)Õ]=Cèp¬
öHS²hÛZuÍ2ШhòëMš¤c88Ú€hêè‘•
ùˆ�À_\ŠÁ(lÕ§ÿøè<¬Ðˆxý©CêèÆ\
óí‚jŸN?úeì�
>è<¬|iX”éè½c
ó.â8¯Ú×5Ô(Цí¹Õ\è<¬Ÿ»±º
—ǰoè	µk
ýÐмN�ÂÇjµY8Í	é¯é¯é¯é¯é¯é¯é¯é¯Æ/Æ/Æ/Æ/Æ/Æ/Æ/ÆÆ¯é¯é¯é¯é¯é¯é¯é¯éÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿÿÿÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿð ‹7ŽrE	Šÿÿÿÿñ£Ÿ{ÈЏaÚ
pÿÿÿÿô×�ëÅ–í0ÿÿÿÿýHÿjý.ÿ„ÿÿÿÿûÆÃÔ	ü°Äãÿÿÿÿñ‰»­›oÏ“´UÿÿÿÿðIæÞY4ì&Ís#ìÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿð ‹7ŽrE	Šÿÿÿÿñ£Ÿ{ÈЏaÚ
pÿÿÿÿô×�ëÅ–í0ÿÿÿÿýHÿjý.ÿ„ÿÿÿûÆÃÔ	ü°Äã*ÿÿñ‰»­›oÏ“´U'ÿÿðIæÞY4ì&Ís#ì'ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿÿÿ-ÿÿÿÿð ‹7ŽrE	Šÿÿÿÿñ£Ÿ{ÈЏaÚ
pÿÿÿÿô×�ëÅ–í0ÿÿÿÿýHÿjý.ÿ„ÿÿÿÿûÆÃÔ	ü°Äãÿÿÿÿñ‰»­›oÏ“´UÿÿÿÿðIæÞY4ì&Ís#ìÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿð ‹7ŽrE	ŠI¯ö®­®®­®¯¯®®¬¯IÆöÅÄÅÅÄÅÆÆÅŬÆIéöçæèèççééèè¬éÿ?ÿ€„$cT�3UcuAFqgP8~X^)‡Jm�;{�-…‡*Œ}9�ÌHçVÞ3í'?è"ì9,ì+ãPè>
ÔiÝUÁƒÌoª�¸‰‘µ £wˆ¯¡èDü–¯Ý\ü|ÀËvübÓ¸�üI⨪à4éœÁ#ê“Ô
ãŽã
Õ�ì"ûžý	ð¸üÞÑýÆæý¬õý’þ"ýxÿ:ý^ÿTýDÿnü–Çì!ü|Îî2übÙèGüJåÜ_ü5ìÑyü#ìÉ“üäÄ­ô
ÖÃÃÃÃÖ€€àyÉ?ê_Û
+í.GèéB2í(ÞY!ì;ÌsãR¸�	ÓkŸ§À……¾©Ÿk8¹�äN¡§
Õg‡¾Â�mÒ¬›Sâ’´=ëxÉ)í0^Û
èEFçÜ]1íÀ€„$cT�3UcuAFqgP8~X^)‡Jm�;{�-…‡*Œ}9�ÌHçVÞ3í'?è"ì9,ì+ãPè>
ÔiÝUÁƒÌoª�¸‰‘µ £wˆ¯¡èDü–¯Ý\ü|ÀËvübÓ¸�üI⨪à4éœÁ#ê“Ô
ãŽã
Õ�ì"ûžý	ð¸üÞÑýÆæý¬õý’þ"ýxÿ:ý^ÿTýDÿnü–Çì!ü|Îî2übÙèGüJåÜ_ü5ìÑyü#ìÉ“üäÄ­ô
ÖÃÃÃÃÖ€€àyÉ?ê_Û
+í.GèéB2í(ÞY!ì;ÌsãR¸�	ÓkŸ§À……¾©Ÿk8¹�äN¡§
Õg‡¾Â�mÒ¬›Sâ’´=ëxÉ)í0^Û
èEFçÜ]1íÀ€„$cT�3UcuAFqgP8~X^)‡Jm�;{�-…‡*Œ}9�ÌHçVÞ3í'?è"ì9,ì+ãPè>
ÔiÝUÁƒÌoª�¸‰‘µ £wˆ¯¡èDü–¯Ý\ü|ÀËvübÓ¸�üI⨪à4éœÁ#ê“Ô
ãŽã
Õ�ì"ûžý	ð¸üÞÑýÆæý¬õý’þ"ýxÿ:ý^ÿTýDÿnü–Çì!ü|Îî2übÙèGüJåÜ_ü5ìÑyü#ìÉ“üäÄ­ô
ÖÃÃÃÃÖ€€àyÉ?ê_Û
+í.GèéB2í(ÞY!ì;ÌsãR¸�	ÓkŸ§À……¾©Ÿk8¹�äN¡§
Õg‡¾Â�mÒ¬›Sâ’´=ëxÉ)í0^Û
èEFçÜ]1íÀÀ$cT�3UcuAFqgP8~X^)‡Jm�;{�-…‡*Œ}9�¯é¯é¯é¯é¯é¯é¯é¯éqÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ@Æ/Æ/Æ/Æ/Æ/Æ/Æ/ÆÆqÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ@é¯é¯é¯é¯é¯é¯é¯é¯qÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ@ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿæpHˆ&aV€5SesCDtÿÿÿæ»]ÜlÏEèRà0í*<é ë=ÿÿÿü˜í3ü«£çHü’±Ú`üxÃÈzÿÿÿý*ÿˆýú¢ýî¼üÛÔÿÿÿü¬Äåü’Èí#üxÐí5ü^ÛçKÿÿÿæÒ	�·RâuÌ;ë [Þ)í1Céÿÿÿå(Ëw ë;µ‘âQ�ª	ÓkƒÁÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿæpHˆ&aV€5SesCDtÿÿÿæ»]ÜlÏEèRà0í*<é ë=ÿÿÿü˜í3ü«£çHü’±Ú`üxÃÈzÿÿÿý*ÿˆýú¢ýî¼üÛÔÿÿÿü¬Äåü’Èí#üxÐí5ü^ÛçKÿÿÿæÒ	�·RâuÌ;ë [Þ)í1Céÿÿÿå(Ëw ë;µ‘âQ�ª	ÓkƒÁÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿæpHˆ&aV€5SesCDtÿÿÿÿæ»]ÜlÏEèRà0í*<é ë=ÿÿÿÿü˜í3ü«£çHü’±Ú`üxÃÈzÿÿÿÿý*ÿˆýú¢ýî¼üÛÔÿÿÿÿü¬Äåü’Èí#üxÐí5ü^ÛçKÿÿÿÿæÒ	�·RâuÌ;ë [Þ)í1Céÿÿÿÿå(Ëw ë;µ‘âQ�ª	ÓkƒÁÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿûQÈÈ–Èÿÿÿÿû00$0ÿÿÿÿæpHˆ&aV€5SesCDtÿÿÿÿ@éýçâãéýæãäéýèççéçè¨é@+ÿþü˜=ÿý‚Ü;ÿüî%F;ÿûW´9ÿúþ�k³;ÿþ×'=ÿþ×'=ÿþ×'=ÿþ×';ÿúþ�k³:ÿûW´:ÿüî%F<ÿý‚Ü<ÿþü˜ÿ@@ÆýÄÀÁÆýÄÁÂÆþÄÅÆÄŨÆ@+ÿþü˜=ÿý‚Ü;ÿüî%F;ÿûW´9ÿúþ�k³;ÿþ×'=ÿþ×'=ÿþ×'=ÿþ×';ÿúþ�k³:ÿûW´:ÿüî%F<ÿý‚Ü<ÿþü˜ÿ@@¯ý­©«¯ý­««¯®¯­®¨¯@+ÿþü˜=ÿý‚Ü;ÿüî%F;ÿûW´9ÿúþ�k³;ÿþ×'=ÿþ×'=ÿþ×'=ÿþ×';ÿúþ�k³:ÿûW´:ÿüî%F<ÿý‚Ü<ÿþü˜ÿ@ÿÿö’ÈÈTOÈÈ—Èö‘ÈÈUNÈÈ™
Èö�ÈÈWLÈÈš	ÈöŽÈÈYKÈÈœÈêŒÈÈZIÈÈ�ÈÈ#0000$0ö"0000$0ö"0000%0ö"0000%0ô!0000%00€'é'é'é'é'é'é'é'éX&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿY'Æ'Æ'Æ'Æ'Æ'Æ'Æ'ÆX&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿY'¯'¯'¯'¯'¯'¯'¯'¯X&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿY'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿõÈ‹ÈÈ\GÈÈŸÈö‰ÈÈ]FÈÈ Èû‡ÈÈ_ÿÿõ0!0000&0ö!0000&0û 00ÿÿ>ÿÿ>ÿÿéþ»éíÉ
éé�	R0ßéécÒéy½é÷ç=éé%'éééþ¯Þéþâ±éüÇ—©äéùѳééׯèéø¼ÈééÂÃéé.é.é.é.é.é‘.ÿ.ÿ.ÿ.ÿ.ÿ.ÿ.ÿÿÿ�ýùìÿþÆD
ÿÿþfýúôÿþ¾)
ÿÿþÍÿñ¯øö¯óÿÿÏÊÿÿÓ˜©õÿûܘ¹½)ÿúÆáÿþ°æÿþÍÿèþéÿ^\ÿÚÊÿ‡qE'ñÿ¾ZC:)ÿútrÿÏ‚ÿã`þéÿô)™0�ÿÞßÿÿD‡ÿ8†ÿÿ[)ÿú¸/ÿŒS?ÿïÀïïõþéÿÿÔKÿÿ©_÷1\þÊÿÿŸ)ÿúôëIÄÿþÍÿôþéÿÿ—)õÿ§ÇøÚþ	Çÿÿœ)ÿû>¤ûVÿþÍÿóþéÿÐÕUaÿÝÇÿøýÿ=‚ÿÿW)ÿû�,Lÿ™ÿþÍÿèþéó&žÿñ"£ÿ”Pq?˜ÿÆR;E)ÿûÄ�ÿÜÿþÀóÿü¿úèÅÿøâËÿÿá«¥Îÿûâ¦ÉïÊÿûû¿îÿÿ.ÿ.ÿ.ÿ.ÿ.ÿ@ÆþŸÆí«	ÆÆ{F(½ÆÆT³Æf¡Æ÷Ä4ÆÆ!ÆÆÆþ•½ÆþÀ—Æü©€�ÁÆù²˜ÆÆ¶•ÅÆø ªÆÆ¥¥ÆÆ.Æ.Æ.Æ.Æ.Æ‘.î.î.î.î.î.î.îîÿxwýèÜîþ¹?
îîþ_wýéwäîþ±&
îîþ¿îñ£çæ£ãîîÁ½îîÅŽžåîûÍŽ­°&îú¹Òîí¤×îþ¿îèíÙîXVî˽î~i@$áî±T?6&îúljîÁyîãZíÙîä&�-…îÏÐîî?~î4}îîU&îú¬,îƒM;îï³ßßåíÙîîÆFîîžY÷.Ví½îî”&îúäÛD·îþ¿îôíÙîî�&åøËíºîî’&îû:™êPîþ¿îóíÙîÂÇO[îκîøìî9yîîQ&îûx)Gî�îþ¿îèíÙã#“îá ˜îŠ
Ki;Žî¹
M7@&îû·…îÍîþ³ãîü²éÙ¸îøÓ½îîÒ šÀîûÓ›¼ß½îûê²Þîî.î.î.î.î.î@¯þ�¯í—¯¯l>$§¯¯Jž¯[ޝ÷®.¯¯¯¯¯þ„§¯þª…¯ü–q«¯ù�‡¯¯¡„®¯ø�–¯¯‘’¯¯.¯.¯.¯.¯.¯‘.ª.ª.ª.ª.ª.ª.ªªÿVUý¦�ªþ„-
ªªþDUý§U£ªþ
ªªþ‰ªñu¥¤u¢ªªŠ‡ªª�eq£ªû“e{~ªú„–ª©u™ªþ‰ªè©›ª?=ª‘
‡ªZK.¡ª<-'ªúMLªŠWªã

@©›ª£f _ª”•ªª-Zª%Yªª=ªú{ª]7*ªï€ŸŸ£©›ªª�2ªªq?÷!=©‡ªªjªú£�1ƒªþ‰ªô©›ªªe£ªo…ø‘©…ªªhªû)m§9ªþ‰ªó©›ª‹Ž9Aª“…ªø©ª)Wªª:ªûV3ªfªþ‰ªè©›¢iª¡mªc	5K*eª„	7'.ªûƒ_ª“ªþ€¢ªü§›ƒªø—‡ªª–rn‰ªû—o†Ÿ‡ªû§Ÿªª.ª.ª.ª.ª.ª@/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿáæ×é 	T+>Aééh30'ŸéÕ-"eW)éé\*OÉéöé¯äé鯗ÁÔ¿éôÎÃÚºé鿝’¤ÛéýÕ¯Ýééú�°¨L+çééû‘LEoØé.é.é.é‘.ÿ.ÿ.ÿ.ÿ.ÿ.ÿ.ÿÿþºPÿþi¡ÿþîÞÿÿþØ›ÿþT“ÿþ¢MÿÞÿù°þçÁÿÿþ¶šÝT“ÿÿå›­ò¯üÿÿã Ÿçÿño5¯¶ÿÞÿ¿'ÿ°7ÿÿ]o&+“ÿÞH`Aõÿ²c[ÒçL$wƒÿàÿ|kÿ°7ÿÍíÿí“ÿiYÿÿ–õü®ÿÿxSÿ¢Mÿ÷õ9®ÿ°7ÿœ4ÿì5“ÿ8šÿÿ×õÝ\__D(ÿ¢Mÿ÷«îÿ°7ÿž1ÿó2“ÿ?�ÿÿÊõÛ¸ÇüÎÿ¢Mÿç,4ÿÿ°7ÿÒéÿê“ÿ~=öþrõü“ÿüýÿ¿7ÿßwÿÿ°7ÿÿjg'0“ÿð6(`ùÿ»>tNjÿú$CfÿúÁèÿÿìÍÿôÁªçÕäÿÿüÓå«ÿûí³¡Âûÿþ×Åÿÿú÷�ËŸ,xÿÿúútKT—ýÿ.ÿ.ÿ.ÿQáĶƈG$57ÆÆY+)!‡Æµ&VJ#ÆÆN$D«ÆöÆ”ÂÆÆ¨€¤´¢Æô¯¥ºžÆÆÄ”|‹ºÆýµ•¼ÆÆú{–�@$ÄÆÆû{@:^·Æ.Æ.Æ.Æ‘.î.î.î.î.î.î.îîþ®Kîþb–îþÞÏîîþÊ‘îþN‰îþ—HîÞîè¤íØ´îîíª�ÎN‰îîÖ‘¡â£ëîîÔ•”Øîáh1£ªîÞî²$î¤3îîWh#(‰îÏCZ=åî¦\UÄØG"ozîàîtdî¤3î¿Ýî݉îbSîîŒåë¢îîpMî—Hî÷å5¢î¤3î’1îì1‰î4�îîÉåÎVYY?%î—Hî÷ Þî¤3î“.îó/‰î;„îî½å̬ºüÀî—Hîç)1îî¤3îÄÙîÚ‰îv9æíjåë‰îüìî²3îßoîî¤3îîc`$-‰îà2%Zèî¯:lIcîé"?_îú´ÙîîÜ¿îô´ŸØÇÕîîëÅÖ îûݧ–µêîþɸîîú熽”)pîîúélFN�ìî.î.î.îQá­¡¯x? /1¯¯N&$w¯ "LA¯¯E <—¯ö¯ƒ¬¯¯•q‘Ÿ�¯ôš’¤Œ¯¯­ƒn{¥¯ý „¦¯¯úl„~9 ®¯¯ûm93S¢¯.¯.¯.¯‘.ª.ª.ª.ª.ª.ª.ªªþ|5ªþFkªþŸ”ªªþ�gªþ8bªþl3ªÞª¦u©š�ªª©yg“8bªª™gs¡u¨ªª—kjšª¡J#uyªÞªªu%ªª>Jbª”0@+£ªwB=Œš3OWªàªSGªu%ª‰žªžbªF;ªªd£¨tªªP7ªl3ª÷£&tªu%ªh#ªì#bª%gªª�£“=??-ªl3ª÷rŸªu%ªi!ªó!bª*^ªª‡£’{…ü‰ªl3ªç#ªªu%ªŒ›ªœbªT)¤©L£¨bªü©ª%ªßOªªu%ªªG
E bª $@¦ª})M4Gª§-Dªú�›ªª�‰ªô�qšŽ˜ªª¨�™rªûžwk�§ªþ�ƒªªú¥`‡jPªªú§M28e©ª.ª.ª.ªQ/ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿÿÿíÄ´Àkè¾²ÁHU¿ÁŽï#£¼Ä;vÁÂq
کȸÿÿÿöÌèfyúÚ¦Iï–ZÇÑÿ,�ÿÿÿí~²³ç2è ȹ]6´X”þâ	ôh‚Šbà_ÿÿÿø7­Û8è÷-¬oÎuuþÅ#ôI�§CÚ^ÿÿÿì,Ì6~ð8èJC+™`ÁuLÙ$ïi·?>#Û\aÜÚ^ÿÿÿîQ–XXt7�˜V]‚ð1�ˆspm/ÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ
ÿÿü‰ i
ÿÿÿÿ
ûèçü{7±
þ}kÿÿÿÿØ]�aFóxbOòxY
_=‹l7±SlOº°xÿÿÿØl4bÄ í8-%ì8)ñ•@	è7ï‡QÖ'è>ª™‹8L¹ÿÿÿØKoÝèçóè7äsuè>ª}k©_ÿÿÿð
ÔgCÐ8èçÌêè7ÅS™çDª}kÕ…ÿÿÿðL�÷8Ö
ßÌêè7ù™R½)Œªfz�dÿÿÿð	ɶÆÉ7pÀ�~¿�Ìêè7ÌÊÅ 
SÓÀ¥ª¼¿2¡ÿÿÿÿþþÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿþ
üÿÿÿÿþÆ"
ü¿)èÿÿÿÿþüÆ"ÿú¿)ýÿÿÿ×&ºtl¹˜ÅÎ<ŸÇ½Ô"Qµ¸”Þ“¿?¿)à9¾½Ë ÿÿÿ×(Àxp¿{A¤`‰&ÿ"уPæ\¿)è;ñ
½ÿÿÿ×(Àxp¿/צ?Û"Fá°°Ä™æ¿)è;³ŒÿÿÿëÏ™p¿)Ù‰Zò"%¼ñæ¿)è;­‹ÿÿÿØÏWXßp¿)Ù;ÌD�ô"Áu.I=æ¿)è;­‹ÿÿÿØ!†u@8_l,�QcNš~!s_tVEÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿ0ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ì:æÂ°
ˆº»¢™GÙ“ÅÇ(+ìU­;’AžJàZbƒ+숭;Lª·Î‚žJà1¶+슩=Ó‹ˆžJà0¸+ìŠv”@ÖHW׈žJà0¸+ìE
U	-�{:DO%p\:þrþI@.þ,©þ|l!÷yE`�vߢ@OŒR-}2|l8}'K.sTd‘r÷E�”‡z1VßßÏ{6Ye·G²s|l‚¥HÂW¡ÊW)˜m0Z8ûÚ•Ž$úé7±ê»+!É|lÕ=¬¡t¦‚xú	jÁ¿
úí`Œèßç|låÒ¡G
qŶù¨BúÓÑæ—QI¥|l´4fˆ¡G¹1?÷»²¶³¶®°Ý„Zlʬ¥)´½¹0|l8¼½°!¡GÁ³¯§òþ
þþ¾ýÿÿú
B‰þ9þ	žþ
ÿü	(þ]‹
þß	û�g�2ÿü]‹ÿõß	�g.­û:¬¸¢ç]µÌË€\ÀŠß	yÌͱgÙù(ÂX{ç]á	Á)Ù°>ß	ÉÙg·5ù[î°°¸Âç] zqA©c‰ß	a„–g¡Mü]Ö	ç]»•S"Ä€jß	DŸ±gÀ*ö]˜�3?O\iç]ëiWÝÉoPÝß	Ü[dègÜö.>”ˆ1”Xè.Hp…	b~o‚tD3M‹!þtþŽÚ?ÿÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ&ÿ'ÿ	À?ÿÿÿÿÿý(ØZýZÝÿÿÿÿýPKýKP	ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿþvþvÿ=ÿÿÿÿþ8�þž7ÿÚÿÿÿÿþ¯3þ4®ÿÚÿÿÿÿþèþäÿÚÿÿÿÿþÝþÞÿÚÿÿÿÿþ
åþåÿÚÿÿÿÿþßþÝÿÚÿÿÿÿþ‘MþN�ÿÚÿÿÿÿþ¶þ·ÿÿÿÿþ<þ<ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿÿÿü	þþ
þ
˜?ÿ?ÿÀýH;û?Jd$ü5HFúü™•ÙQ
ûk}Z û¿¦Š¾Ÿú“U¦BÅ&L»b�Z	lqq}OKv¿Aòj'EÆ¥x6Лx:|&L.vfÅ&}¬e3n³ÑnaÝ}ª>°†PÖ-¿vJˆ°äT”ªz8¹k8…£HÄT¤ÅUŠ™Å×éåPpâ*¹Ÿ}ª>
Øk~¿¦ˆ³–äT”“U¦B×@©¤f
ÝÅ&ž‡ÒcBà#M™}ª>,ÀI¤¿A½IäZ”“U¦BäϤDèÅ&
åaˆþ#ÔÃ}ª>Ø”Z¿AØ5Ñ¢”|d�Q·1i…¤DèÅ&z–϶¿Õ"rÏ̲}ª>TÅÀž
¿ÜÐÉ€dÒè”.¾¿!;¿¿:½¾¯ ¤Dèÿþÿ
þ
	ÿ�?ÿÿÿÿÿñ£Ÿ{ÈЏaÚ
pÿÿÿÿô×�ëÅ–í0ÿÿÿÿýHÿjý.ÿ„ÿÿÿÿûÆÃÔ	ü°Äãÿÿÿÿñ‰»­›oÏ“´UÿÿÿÿðIæÞY4ì&Ís#ìÿÿÿÿÿÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿð ‹7ŽrE	Š'ÿÿñ£Ÿ{ÈЏaÚ
p'ÿÿô×�ëÅ–í0ÿÿÿýHÿjý.ÿ„ÿÿÿÿûÆÃÔ	ü°Äãÿÿÿÿñ‰»­›oÏ“´UÿÿÿÿðIæÞY4ì&Ís#ìÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿð ‹7ŽrE	Šÿÿÿÿñ£Ÿ{ÈЏaÚ
pÿÿÿÿô×�ëÅ–í0ÿÿÿÿýHÿjý.ÿ„ÿÿÿÿûÆÃÔ	ü°Äãÿÿÿÿñ‰»­›oÏ“´UÿÿÿÿðIæÞY4ì&Ís#ìÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿÿÿ¼ÌHçVÞ3í'?è"ì9,ì+ãPè>
ÔiÝUÁƒÌoª�¸‰‘µ £wˆ¯¡èDü–¯Ý\ü|ÀËvübÓ¸�üI⨪à4éœÁ#ê“Ô
ãŽã
Õ�ì"ûžý	ð¸üÞÑýÆæý¬õý’þ"ýxÿ:ý^ÿTýDÿnü–Çì!ü|Îî2übÙèGüJåÜ_ü5ìÑyü#ìÉ“üäÄ­ô
ÖÃÃÃÃÖ€€àyÉ?ê_Û
+í.GèéB2í(ÞY!ì;ÌsãR¸�	ÓkŸ§À……¾©Ÿk8¹�äN¡§
Õg‡¾Â�mÒ¬›Sâ’´=ëxÉ)í0^Û
èEFçÜ]1íÀ€„$cT�3UcuAFqgP8~X^)‡Jm�;{�-…‡*Œ}9�ÌHçVÞ3í'?è"ì9,ì+ãPè>
ÔiÝUÁƒÌoª�¸‰‘µ £wˆ¯¡èDü–¯Ý\ü|ÀËvübÓ¸�üI⨪à4éœÁ#ê“Ô
ãŽã
Õ�ì"ûžý	ð¸üÞÑýÆæý¬õý’þ"ýxÿ:ý^ÿTýDÿnü–Çì!ü|Îî2übÙèGüJåÜ_ü5ìÑyü#ìÉ“üäÄ­ô
ÖÃÃÃÃÖ€€àyÉ?ê_Û
+í.GèéB2í(ÞY!ì;ÌsãR¸�	ÓkŸ§À……¾©Ÿk8¹�äN¡§
Õg‡¾Â�mÒ¬›Sâ’´=ëxÉ)í0^Û
èEFçÜ]1íÀ€„$cT�3UcuAFqgP8~X^)‡Jm�;{�-…‡*Œ}9�ÌHçVÞ3í'?è"ì9,ì+ãPè>
ÔiÝUÁƒÌoª�¸‰‘µ £wˆ¯¡èDü–¯Ý\ü|ÀËvübÓ¸�üI⨪à4éœÁ#ê“Ô
ãŽã
Õ�ì"ûžý	ð¸üÞÑýÆæý¬õý’þ"ýxÿ:ý^ÿTýDÿnü–Çì!ü|Îî2übÙèGüJåÜ_ü5ìÑyü#ìÉ“üäÄ­ô
ÖÃÃÃÃÖ€€àyÉ?ê_Û
+í.GèéB2í(ÞY!ì;ÌsãR¸�	ÓkŸ§À……¾©Ÿk8¹�äN¡§
Õg‡¾Â�mÒ¬›Sâ’´=ëxÉ)í0^Û
èEFçÜ]1í@?ÿqÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ@qÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ@qÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ@æ»]ÜlÏEèRà0í*<é ë=ÿÿÿÿü˜í3ü«£çHü’±Ú`üxÃÈzÿÿÿÿý*ÿˆýú¢ýî¼üÛÔÿÿÿÿü¬Äåü’Èí#üxÐí5ü^ÛçKÿÿÿÿæÒ	�·RâuÌ;ë [Þ)í1Céÿÿÿÿå(Ëw ë;µ‘âQ�ª	ÓkƒÁÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿæpHˆ&aV€5SesCDtÿÿÿæ»]ÜlÏEèRà0í*<é ë=ÿÿÿü˜í3ü«£çHü’±Ú`üxÃÈzÿÿÿý*ÿˆýú¢ýî¼üÛÔÿÿÿü¬Äåü’Èí#üxÐí5ü^ÛçKÿÿÿæÒ	�·RâuÌ;ë [Þ)í1Céÿÿÿå(Ëw ë;µ‘âQ�ª	ÓkƒÁÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿæpHˆ&aV€5SesCDtÿÿÿæ»]ÜlÏEèRà0í*<é ë=ÿÿÿü˜í3ü«£çHü’±Ú`üxÃÈzÿÿÿý*ÿˆýú¢ýî¼üÛÔÿÿÿü¬Äåü’Èí#üxÐí5ü^ÛçKÿÿÿæÒ	�·RâuÌ;ë [Þ)í1Céÿÿÿå(Ëw ë;µ‘âQ�ª	ÓkƒÁÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ/ÿÿ@+ÿþü˜=ÿý‚Ü;ÿüî%F;ÿûW´9ÿúþ�k³;ÿþ×'=ÿþ×'=ÿþ×'=ÿþ×';ÿúþ�k³:ÿûW´:ÿüî%F<ÿý‚Ü<ÿþü˜ÿ@@+ÿþü˜=ÿý‚Ü;ÿüî%F;ÿûW´9ÿúþ�k³;ÿþ×'=ÿþ×'=ÿþ×'=ÿþ×';ÿúþ�k³:ÿûW´:ÿüî%F<ÿý‚Ü<ÿþü˜ÿ@@+ÿþü˜=ÿý‚Ü;ÿüî%F;ÿûW´9ÿúþ�k³;ÿþ×'=ÿþ×'=ÿþ×'=ÿþ×';ÿúþ�k³:ÿûW´:ÿüî%F<ÿý‚Ü<ÿþü˜ÿ@ÿ@&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿY@&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿY@&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿY&ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿ/¯/¯/¯/¯/¯/¯/¯/¯¯ÿ¨Wý[h˜¯ù®yVAMm£¯¯ÿ¡þ”¯øœ&<¯¯ñ¡�¡1E¯¯Œnal‘¯é|x¥€e¯1†¬¡va¯§x|¨vu¬„¯å¡€‘+I¯¯
Z¯¯1	•„¯ù”)1˜¯ÿ¡Ý¤¯¯€ª®�®¯nyu¬¯uGGJ¯”+yƒ¥‡¯×¡R]$3¯¯k:
�¯l¯¯{—¯V
¯””¯¯­Z¯Ü¡’¯„¥’hqš¯‘¯¯š_¯¯�¯”	¯ÿr¯ë¡ix4¬h*¥Vš¯’¯ôH>|d¯”
¯ÿG¯ÿ¡ñ
s¯¥0š¯’¯ý¨Jú-¯”
¯þ�¯ÿ¬ƒý„’ª¯õ¬|�«ƒƒª¯‡ƒ¨¯öš}m|𝝩ƒ†¯ÿ¦/¯/¯/¯/¯/¯/¯/Æ/Æ/Æ/Æ/Æ/Æ/Æ/ÆÆÿ¾cýfv¬ÆùʼnbJW{¸ÆÆÿ¶þ§Æø±+DÆÆñ¶ ¶7NÆÆž|n{¤Æé�ˆ»�rÆ7—¶†nƽˆŒ¾†…ÖÆå¶�¥0SÆÆfÆÆ7
¨–Æù¨	/7¬Æÿ¶ÝºÆÆ�ÁÅ’ÄÆ|‰„ÃÆ„PPTƨ1‰”»™Æ×¶]i(:ÆÆyB²Æ{ÆÆ‹«Æaƨ¨ÆÆÄfÆÜ¶¥Æ–º¥v¯Æ¤ÆÆ®kÆÆ¢ƨ
Æÿ�Æë¶wˆ;Ãu/ºa¯Æ¥ÆôRGŒqƨÆÿPÆÿ¶ñ‚ƺ6¯Æ¥Æý¾Tú2ƨÆþŸÆÿ”ý–¥ÁÆõÂŒ’Á””ÁƘ”¾Æö¯�{�¯ÆÆ¿”—Æÿ¼/Æ/Æ/Æ/Æ/Æ/Æ/é/é/é/é/é/é/é/ééÿàtýy‹Ëéùè¡sWf‘ÙééÿÖþÄéøÐ2OééñÖ¼ÖA\é麒‚�Áé饠ܪ†éA!²äÖ�‚éÞ ¤à�œå°éåÖªÂ9béé
xééAưéùÅ
7AËéÿÖÝÚééªãè¬çé!’¢›åé›^^céÅ:¡¯Ü´é×Ön{0Eéé�NÑé�éé¤ÉéréÅÅééæxéÜÖÃé°ÛË–ÎéÁééÍ~éé¾éÅéÿ˜éëÖŒ E	åŠ8ÛrÎéÃéô`S¤…éÅéÿ^éÿÖñšéÛ@ÎéÃéýàcú;éÅéþ»éÿä¯ý°Ããéõ䤬䯯ã鳯àéöΦ‘¥Îéé᯲éÿÝ/é/é/é/é/é/éÿÿ>ÿÿ÷Wˆ²Î´“R"ÿÿõ+ÉÿÿòÒòÿÿàD!ÿÿô0ùÿ¢!vôx!ÿÿüÆÿsþ=þü(
þ
ÿÿüVÿÚõ£ð!�èÿº¨ïÿþÛMô)ð™4ÏýÿçZÿýŒÿ|ï®ÿ¸æ�qq@é•kZ„ëÿLô,ÿÎï¢`~ìÿ`ÿý²ÿYü®ÿßþ
ýðÔü,ÿÿ}û&öåÿýÔÿ:ÿ2 ÿ‡ý®ÿg÷­þ,ÿí
ü—ÿSÿýÂÿJÿPÿÿØý®ÿ7ò‹¿Üèèöÿ&,ÿÀü]ÿxÿýœÿlýÿØý®ÿ"ñ8øù¶’ƒ€Ìÿ0,ÿªüCÿ“ÿývÿ§ýÿØý®ÿ"ýÌú1ø¯ÿ0,ÿ¾üZÿzÿüòý/ýÿØý®ÿ"ýúÒöáÿ0,ÿèüŽÿXÿüqÿØ6ýÿØý®ÿ"ýðûöpÿÿ0,ÿÿkûñë
ÿô•þü¯ƒ`kŽèÿ¸ý®ÿ"è ÿÊiN™ôÏÿ0,ÿ×ôŠHfáÿlÿý=ÇöÿüúÇOý®ÿ"þ±ÿíà<˜ÿ0,ÿ¤Eçÿÿõiÿû">!ý=ø,ÿ¤"2ÿÿ3ý,ÿ¤ÿÿ3ý,ÿ¤ÿÿ3ý,ÿ¤ÿÿ3ý(ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>¯ý`V<¯ýUI-¯ëjaq›¯¯©xyªxlŸ¯¯UIœl�*¯êx¯›(”¯U$
T)¯ê­¬b%¯›G�-=¯Upn)¯é3¯›�¯q¯U1¯ª£(¯é nZ¯›š¯n¯UI¯­¡(¯éG¢<¯›C‰(A¯UI¯­¡(¯ë0¯›-–¯UI¯­¡(¯év‰§ƒ‡¯›¬€u¢¯¯™ƒ–¯¯ƒƒ¬.¯ý›<¯ý +/7¯Æýmb<Æý`S-Æëxm¯ÆÆ¿ˆ‰Áˆz³ÆÆ`S°z£*ÆêˆÆ¯.§Æ`(`)ÆêÄÃo*ƯQ¢3EÆ`})Æé:	Ư²Æ€Æ`7ÆÀ¸(Æé$}f
Ư¯Æ}Æ`SÆÄ¶(ÆéP·D
ƯL›.IÆ`SÆÄ¶(Æë6
Ư2ªÆ`SÆÄ¶(Æé†›½”˜Æ¯Â�„·ÆÆ­”©ÆÆ””Â.Æý¯<Æýµ157Æéý€s<éýqb-éëŽ�–Ïééá ¢ã �ÓééqbÏ�À*éê éÏ6Äéq0p)éêæåƒ1éÏ_¾<Qéq•“ )ééEéÏÑé—éqAéâÙ(éé+“xéÏÎé“ éqbéæ×(éé^ØOéÏZ¶6Véqbéæ×(éë@éÏ;Èéqbéæ×(éé�¶Þ¯³éÏ䪛Øéé̯Çé鯯ä.éýÏ<éýÕ:>7éý0	ý"0&ý0ýzÿV	ý¸ÿý�—
ý�˜>ù]˜:tÿ\ýzÿV	ý¸ÿý»ÿCü'ÿÿ›ùÏÿ/tÿ\ýzÿV	ýý{ÿ‚üfÿåÚøýí
ùzÿVü'ý;ÿÂû¦ñ}ÿýNÿ¯
÷zÿVbßÿÿØ5ý¬ðùNÕüÿÿétüõùûåµ<ÿZøŽÿolðVözÿ·ïŒtÎÿíý¸ÿø†ÿê„[o«®õ»ÿA%ÿv÷™øÍÿ/tÿ\ùszÿô"ý¤ÿo÷¸ÿ1üÞÿá{ÿ�eÿ7ÀÙýítÿ\ðzÿ�ý\ÿ”ø¸ÿ ÿZð;ÿÀ¤ó‚ÿLÿ¯ôtÿ\gÿzÿbý!ÿ«ø¸ÿÊÿðõøã¸DÿXŒÿnôtÿ\ŒÿzÿVýÿ®ø¸ÿéûñºÿ?#ÿyû—Ëÿ.ôtÿ\§ÿzÿVýÿ®ø¸ÿÌÿúzÿcÿ:úÈ×üìôtÿ\ŽÿzÿVýÿ®ø¸ÿ£ÿRú:ÿ¾¢õûŠÿaÿ®ôtÿ\lÿzÿVýÿ®÷¸ÿ9þÔÿ	ûô÷å¼ûLÿÜÿnôtÿ\õzÿVýÿ®ý¸ÿø•ÿàtKZ•§
üºÿÿ|ûýÿÿ.ýtÿ\ú€zÿVýÿ®ý¸ÿþfîÿþò
üzÿÿ=üÐÿìýtÿ\û?)äÿ¯/¯/¯/¯/¯/¯/¯/¯/¯¯ùªoRAPw©¯¯ø‹[¯¯á¨*’­œjs¯�x†¡n„§hau¤¯¯Ÿx„£qr§	¯¯üuŸ¯êuPy
‘¯|¥¯¯àU¯¯bGG]¯u<~Œ™’®©FE¯|a‰]¯¯à[¬¯:
$¯u	«¯¯¢R-.¯|®¯R6¯¯ó{~¯¯o$¯u(¯ðR1sD+¯|®¯O8¯¯ó®*
K�L$¯u*¯ð'dœ$+¯|[‚`¯¯þŸ6úI¯u*¯ðq/+¯| §¯	¯ö”yn€¢¯¯¡ƒŽ¯òŸs’ŸƒŽ¯|#¥yz©	¯¯ý|#
¯¯ý‰+F
¯/¯/¯/¯'¯Æ/Æ/Æ/Æ/Æ/Æ/Æ/Æ/ÆÆùÁ~\J[‡¿ÆÆøžgÆÆá¾/¥Ä°x‚Ʋˆ—¶}•½�un„ºÆÆ´ˆ•¹�½	ÆÆü…³Æê„[‰¥ÆŒ!»ÆÆà`#ÆÆoPPiÆ„D�ž­¥Å¿ONÆŒm›iÆÆàgÂÆA)Æ„
ÁÆÆ·\24ÆŒÅÆ\=ÆÆó‹�ÆÆ~)Æ„.Æð]8‚M1ÆŒÄÆY?ÆÆóÄ/U’V)Æ„/Æð,q°)1ÆŒg”mÆÆþ³=úSÆ„/Æð€51ÆŒ$½Æ	Æö§‰}�·ÆÆ¶”¡Æò³‚¥´”¡ÆŒ(»‰Š¿	ÆÆýŒ(
ÆÆý›1O
Æ/Æ/Æ/Æ'Æé/é/é/é/é/é/é/é/ééùã”mWkŸáééø¹zééáà 8ÃæÏ�šéÑ ²Ö“¯Þ©Š‚›ÚééÔ ¯Ù–˜Þ	ééüœÓéê›#k¡
Âé¤&Üééàq)ééƒ^^{é›P¨ºÌÃèá][é¤�·#{ééàzäéM0é›äééØm;	=é¤èémGééó¤¨éé”0é›6éðnB™Z:é¤çéiJééóç8d¬e0é›8éð4…Ï0:é¤z®€ééþÓHúbé›8éð—>:é¤#+Þé	éöÄ¡“ªØééÖ¯½éòÓšÃÔ¯½é¤/Ü¢£á	ééý¤/
ééý¶:]
é/é/é/é'éý!0<ý°ÿýý°ÿý>ÿ’ý°ÿý>ÿ’ù
°ÿý"
þý>ÿ’û)÷fêÿûË-°ÿ÷¤÷ÿò©
î½ò�ëÿÿï……óÿùðûE™øÿþënëÿäyb¬êÓÿÆÿ¶elÈ¿þÊñÏÿ±hc²ÿ°B˜ÿÅxö"£ÿ¾{Yp¬•þíð‘ÿÿYÿ¦ÅÿÊý~ÿyø™ÿG>ÿ’üôáÿÿƒ÷÷ÿºÿ)÷MÿÊòçø"ÿ›>ÿ’üäç	ÿLøÑÿÞùøÿÊÿÖpøqþÄ>ÿ’ú‡ÿìšK	
ÿ6ù·ÿôéøÿÊAÿü÷úÔ>ÿ’øeÕÿÿö ÿJøÎÿ×üø!ÿÊ(ÿ°	ý>ÿ’ù3†ßÿË
ÿz÷ôÿ¯ÿ>÷bÿÊúéý8ÿ›	üåÿ!
þæðÿÿ@ÿÃ
ÛÿÊý“ÿŠùÿÏþüØÿ
÷ÿ×aJ”òÚÿ÷ ÿà— ê“þÈäÙÿ·bH`‰âlÏý¼†€%$ò˜`Bd¯ÿ¸÷v÷ÿÿã<°ÿøtÌòÅyÿ­ýŽ÷ÿýûº6÷(²æýÿJÁøÿýû¡ý4
ý<ÿŒû#?)û(@/ÿüÀÿ<6ø©¯rYzØÿ«7øuâÿÿþâw9ý ïÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ'ÿ¯ýEr¯ü®[W˜¯õ˜WZ®¯€NAOl¡¯ý�¯ý6i¯ý3Q¯õR2¯\u¯ýš¯ç6i’j›¯¯u®¯®s¯u¬Ÿrޝõ�kd�®¯‰man”¯÷š¯¯œqeƒ¯é6+s¯ª‚¯„
ª¯M�­¯ÿCë�¯b¯¯š¯p7®
¯Ë6}T8¯¯GB¯FE¯¯h]¥¯qs¡z¥¯«®y¯¯š£|¥,l
¯ð6P¯Š¯¯‰	¤…¯ö•HT¯C>¯íf8
¥¯š€++@
¯ù6i¯�¯÷;®¯¯¬¯ù^¯E;¯ñ‹jj£¯š‚Lÿi
¯ù6i¯�¯ÿ\ÿV¯ÝQw|/-¯wh–r¤_1¥O£¯š£
j¨Ÿm€
¯ù6i¯�¯û›—¯ÿúˆ¯¯Në� 0£¯š¯xp
¯ù‘ƒž¯§ƒˆ¯ý‰ƒ‡¯û‘wl|¦¯è—voŠ®¯ªz‚ªƒƒ¬¯„ƒª¯¯¤~lxœŒ¯ÆýN�ÆüÅfc¬Æõ¬cfÄÆ�YIYz¶Æý²Æý=wÆý:\Æõ]9Æh…Æý¯Æç=w¥x¯ÆÆ„ÄÆÅ‚Æ„³�¡Æõ yq’ÄÆ›{m|§Æ÷¯ÆÆ°€r”Æé=1‚ÆÁ
“Æ–ÀÆ"W£ÄÆÿKë²ÆoÆÆ¯Æ>Ä
ÆË=�_?ÆÆQKÆONÆÆuiºÆ€	‚¶ŠºÆ�ÁĉÆÆ¯¸	Œ»2{
Æð=[ÆœÆÆ›
º
—Æö¨R`ÆLGÆít@»Æ¯‘11H
Æù=wÆ Æ÷CÅÆÆÃÆùj#ÆNCÆñ�xx¸Æ¯“Vÿw
Æù=wÆ ÆÿhÿbÆÝ \†�63Ɔvª�ºl7ºY¸Æ¯¹x¾´{‘
Æù=wÆ Æû¯«Æÿ!ú
šÆÆXë²µ
6¸Æ¯Æˆ

Æù¤”³Æ½”šÆý›”™Æû¥‡{Œ¼Æè«†~œÅÆÁŠ”Á””ÃÆ–”ÁÆÆº�{ˆ±ŒÆéý[˜éüèytËéõËtxçéªhVi�Öéý"ÑéýGŒéýEléõnCézœéýÎéçGŒÃŽÏéé›çéè™é›äÓ˜½éõ¼�…¬çé·‘�’Äé÷ÎééÏ—†¯ééG:™éã­é°
âé(fÀæéÿYëÑéƒééÎé•Iç
éËG¦oJéé_Xé][ééŠ{Ûé—šÖ£Ûé©äç¢ééÎÙ¤Ü:�
éðGké¸éé¶Ú±éöÆ` péZSéíˆKÜéΫ::U
éùGŒé¼é÷$Oèééåéù})é\Oéñ¹�ŽÙéέeÿŒ
éùGŒé¼éÿzÿséÝ%lž¥?<鞋ȘÚAÛiÙéÎÙ�àÔ‘«
éùGŒé¼éûÏÉéÿ&úµéégëÑÕ@ÙéÎé •
éùÁ¯ÒéÞ¯µéý·¯´éûŸ�¤ÝéèÉ�”¸èé㣮㯯åé°¯ãééÚ¨� ÐŒéÿ¯é¯é¯é¯é¯é¯é¯é¯é¯éÿàtývŽÉé¯éÿÖþ�é¯éëÖªÀO	æÍ ¶Ò�¶é奅™Óéÿã¯éïÖÃé Ö�)xÒ&û éä3¯éçÖs}%é�Yª¾U:ÄsᎯéÿÖí&¹é�èéé˜éݽZ¯éôÖ˜¸Ïèéé�Céö”éÛÀb¯éüÖÃéý�Eéö[6·k㢯éüÖÃéý�EéþØ2û©éé[¯éü䯯àéýÓ¯Àéûè´”§Úéÿà¯éÿ)¯éÿu¯é¯é¯é¯éÆ/Æ/Æ/Æ/Æ/Æ/Æ/Æ/ÆÆÿ¾cýdx«!ÆÆÿ¶þx ÆÆë¶�£DÄ®ˆ›³{›ÆÃ�q‚³ÆÿÁÆÆï¶¥Æˆ¶y#f³!û
ˆÆÁ+ÆÆç¶bjÆyK�¢H2¦b¿xÆÆÿ¶í!žÆyÅÆÆ�Ƽ¡MÆÆô¶�œ°ÅÆÆy9Æö~ƺ£SÆÆü¶¥Æýy:ÆöN.›[Á‰ÆÆü¶¥Æýy:Æþ·+û�ÆÆNÆÆü””¾Æý³”£ÆûÅ™~ŽºÆÿ¾ÆÆÿ#ÆÆÿcÆ/Æ/Æ/ÆÆé¯é¯é¯é¯é¯é¯é¯é¯é¯ÿ¨WýYj—¯é¯ÿ¡þj¯é¯ë¡€�<­šx‰žl‰¯¬|dsŸ¯ÿªé¯ï¡’¯x¡kZžû	x¯«&é¯ç¡V^¯kC€�@,“V©jé¯ÿ¡í‹¯k®¯¯r¯¦ŽDé¯ô¡rŠœ®¯¯k2¯öo¯¥�Ié¯ü¡’¯ýk3¯öE(‰Pªyé¯ü¡’¯ýk3¯þ¢&û¯¯Eé¯ü¬ƒƒ¨¯ýŸƒ�¯û®‡o~¤¯ÿ¨é¯ÿé¯ÿXé¯é¯é¯é¯ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿ#éÿØtýzŽÐ8éÿÅþÑé휒Գ Îé½ ÆÄˆÎéÚœ„£åéäË–…¡ÕééÓžƒšÍéÅÍÓ4méé¶��’Æéû¡ à¥�éÈ �é[:«²-Úéˆbé«?éŹ¿-r鿇ééAéܰ’�é[sªÏ"à\hé-ÐèȳéO¯éÑ©éÅï%àéèªäçœéé(—¥éÈZéé"�é[EééÓ6:'-ée:€Ïéˆ2uÄéÅwz)Uéé„J
âé¡éðOéé�é[vééÕ]eÛuéâŸV @éæ«^*éÅÓé 	å³�ŠÞéÑéö}`�é[xéÕ&lÙÙ¡•é”ÇåÁ響äÕÚŘ�;ézFÚeÞéÓéø5'“é[xééºné(‚éK`éÅñ¥éÑ@ÞéÓéöÏÅÙ¶éƯÍéêã®’œÅééÙ©’¦ØééÞ¯”ŸÑééá¯ý±Ääéõ⡯㯯æé¯¯äéúJR<ç9éû8*Dƒá;é#Æÿ·cýgx±8Æÿ¨þ²Æí…|´˜ˆ¯Æ¡ˆ¨¦t¯Æº…qŠÃÆä¬q‰µÆÆ³†oƒ®Æ¨®³,\ÆÆ›{m|¨Æû‰ˆ¾�xÆÈzÆN2‘—&ºÆtSÆ‘6ƨ�¢&aÆÄsÆÆ7ÆÜ–|zÆNb�°l¾NYÆ&±Åª˜ÆD•Ʋ�ƨï¾ÆÅ�ÂÄ…ÆÆ"€�ÆÈLÆÆzÆN;ÆÆ³.1!&ÆV2m¯Æt+c¦Æ¨eg#HÆÆq?ÀƉÆðCÆÆzÆNdÆÆµOVÛcÆÀ‡I6ÆÄ‘P$ƨ³ÆˆØyu½Æ²Æö	jRzÆNfÆÕ!\¹¹‰Æ~©Ã¤Ƈ¢Áµ
º¨�†2Æg<ºU½Æ³Æø-!}ÆNfÆéž]Æ"nÆ@Rƨñ�Ʋ6½Æ³Æö°¨¹›Æ¨”®ÆêÁ”|…¨ÆÆ¸�|�·ÆÆ½”~‡²ÆÆ¿”ý—§ÂÆõÀ‰”Á””ÄÆ•”ÁÆú?F3Ä9Æû/$9o¿;Æ#¯ÿ¢Wý[jœ8¯ÿ”þ�¯íunŸ‡xš¯Žx•“fš¯¤udz¬¯ä˜qdy ¯¯Ÿwbtš¯”šŸ'R¯¯‰lan•¯ûyx¨|j¯Èl¯E,€†"¤¯fI¯€/¯”‹�"V¯­f¯¯1¯Ü„nl¯EV€œ_¨EN¯"œ®–‡¯<„¯�¯”隷®€¬®u¯¯q|¯ÈC¯¯l¯E4¯¯Ÿ(+"¯L,`›¯f&X“¯”Y[@¯¯d8
ª¯y¯ð;¯¯l¯EY¯¯ FLÛX¯ªwA0¯­€G ¯”Ÿ¯x¬‡kh§¯�¯ö^Hl¯EZ¯ÕQ££yp¯o–¬‘¯w�« ¤”rv-¯[5¤K§¯Ÿ¯ø(n¯EZ¯éŒR¯a¯8H¯”ñ|¯�
0§¯Ÿ¯öœ”£‰¯•ƒš¯êª‚nu”¯¯£n}¢¯¯§ƒow�¯¯©ƒý…”¬¯õªyƒªƒƒ­¯„ƒ«¯ú8>-®9¯û* 3b©;¯ÿéÿéÿéÿéÿéÿéÿéÿéÿéÿÿõ÷„˜Þÿÿ÷óÿéÿÿêù×ÿ÷''óÿéÿïêÎêGeÿý¯¯ûÿÿ×éÿïêºÔ>kÿöñÿ}éÿÿêõïÿöñã–éÿïêx‡4Kÿöñ¯ÿéÿïêÕÿÁðöñ·üéÿï꙯L
ûöñïbéÿÿêõ¨ÿöñÿ¦	éÿÿú¿óÁÕøÿÿý¿¿üÿõñÏéÿýo?béÿý±I.éÿéÿéÿéÿÆîÆîÆîÆîÆîÆîÆîÆîÆîÿåw÷{ŽÏîîçãîÆîÿÚùÉîç$$ãîÆîïÚÀÚB^î죣êîî¶�ÆîïÚ®Æ:dîæáîuÆîÿÚõßîæáÔŒÆîïÚp~1Fîæá£îÆîïÚÇî´àæá«
ëÆîïÚ�£G	êæáß
[ÆîÿÚõ�îæáî›Æîÿé²ó´Ççîîì²²ëîåáÁÆîýh;[Æîý¥D+ÆîÆîÆîÆî¯ª¯ª¯ª¯ª¯ª¯ª¯ª¯ª¯ªÿ£U÷Xe”ªª¥¢ª¯ªÿœù�ª¥¢ª¯ªïœ‰œ/Cª©uu§ªª‚e¯ªïœ|�)Gª¤¡ªS¯ªÿœõŸª¤¡—d¯ªïœPZ#2ª¤¡uª¯ªïœŽª� ¤¡z	¨¯ªïœfu3§¤¡Ÿ	A¯ªÿœõ
pª¤¡ªo¯ªÿ§ó�Ž¥ªª©¨ª£¡Š¯ªýJ*A¯ªýv1¯ª¯ª¯ª¯ªÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿÿÿàÿ—ÿýßÏê$ÿÿÀÿ/ÿýT‹ ÿßÑܯÉòÇǃÇÇÒ몑µýÿò¯°ûÿÙ¯ÅÇ:_¯Ïÿü9Qÿë¨ÿÿ»8ôÿPñ¿LÿcÿüÅQÿæ¨ÿý–õ[~ÿíA�ÿÞDqÏâÿýqQÿè¨ÿÛ>?(=ÿÿÁ?ýÿÿT‹!ÿýdQÿø¨ÿÝjoó‡ÿÿ  ðÿÿT‹ ÿü�Qÿæ¨ÿþðí¬©ÿ×hSÿÿ}J¼äÿüQUÿæ¨ÿÿÅ„÷.¦ü;•ÿÏ“ÿüùA{ÿýê¿Åÿìö½Ÿ¬Ûÿì¿Æÿÿê¿ÇÿÿýÍ¿äÿü*â;ÿý@qâ=ÿîÿÑwÿ�îýÐÁÚ$îÿ³ÿ,îýN‚ îßÃÍ£¼âººzººÄÛŸ‡©ìî⣤êîË£¸º6Y£Áîü5Lîë�îî¯4äîKwá²Gÿ\îü¸Lîæ�îìŒåUvîÝ=xîÏ?iÁÓîýiLîè�îÌ:;%9îî´;ìîîN‚!îý]Lîø�îÎchó~îî•àîîN‚ îü„Lîæ�îíwàÝ¡žîÉ
aMîîuE¯ÕîüLOîæ�îî¸{ç+›ë7‹îÁ‰îüè=sîýÚ²¸îìæ°”¡Ìîܲ¹îîÚ²ºîî쿲Õîü'Ó;îýÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ0ÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿñQ‚1¡Q‚1¡ÿÿÿÿòÉBÐ:ÉBÐ:ÿÿÿÿü)Ø‘Œü)Ø‘Œÿÿÿÿý�åý�åÿÿÿÿüÝÏAüÝÏAÿÿÿÿû¨i+×û¨i+×ÿÿÿÿòR½|’R½|’ÿÿÿÿñhx
hx
ÿÿÿÿñQ‚1¡Q‚1¡ÿÿÿÿòÉBÐ:ÉBÐ:ÿÿÿÿü)Ø‘Œü)Ø‘Œÿÿÿÿý�åý�åÿÿÿÿüÝÏAüÝÏAÿÿÿÿû¨i+×û¨i+×ÿÿÿÿòR½|’R½|’ÿÿÿÿñhx
hx
?ÿÈáQ‚1¡Q‚1¡Q‚1¡Q‚1¡ âÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:"ü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œ$ý�åý�åý�åý�å#üÝÏAüÝÏAüÝÏAüÝÏA#û¨i+×û¨i+×û¨i+×û¨i+×!âR½|’R½|’R½|’R½|’!áhx
hx
hx
hx
 áQ‚1¡Q‚1¡Q‚1¡Q‚1¡ âÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:"ü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œ$ý�åý�åý�åý�å#üÝÏAüÝÏAüÝÏAüÝÏA#û¨i+×û¨i+×û¨i+×û¨i+×!âR½|’R½|’R½|’R½|’!áhx
hx
hx
hx
 ÉQ‚1¡Q‚1¡Q‚1¡Q‚1¡Q‚1¡Q‚1¡Q‚1¡ÊÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:
ü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œý�åý�åý�åý�åý�åý�åý�åüÝÏAüÝÏAüÝÏAüÝÏAüÝÏAüÝÏAüÝÏAû¨i+×û¨i+×û¨i+×û¨i+×û¨i+×û¨i+×û¨i+×	ÊR½|’R½|’R½|’R½|’R½|’R½|’R½|’	Éhx
hx
hx
hx
hx
hx
hx
ÉQ‚1¡Q‚1¡Q‚1¡Q‚1¡Q‚1¡Q‚1¡Q‚1¡ÊÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:
ü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œý�åý�åý�åý�åý�åý�åý�åüÝÏAüÝÏAüÝÏAüÝÏAüÝÏAüÝÏAüÝÏAû¨i+×û¨i+×û¨i+×û¨i+×û¨i+×û¨i+×û¨i+×	ÊR½|’R½|’R½|’R½|’R½|’R½|’R½|’	Èhx
hx
hx
hx
hx
hx
hx
ÿ'ÿÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿþ	þÿÿÿÿþA†	þ±6	ù)¾”ÁaÿÿÿÿþT«þü±6õ)¾O�|ÿÿÿÿþT«쑹¼œ±¡ÈÈH€¹¹f)¾þ{|ÿÿÿÿþT«ì�5±–Ò>žÄ
)¾þ{|ÿÿÿÿþT«ìR«¶Ñw±LÏ’ʰ°ÙP)¾þ{|ÿÿÿÿþT«ñÔ—|±gÞ	prþ)¾þ{|ÿÿÿÿþTÈXåÛB\Ü|±ÈG†¬ÖZ,`)¾X¨©Xÿÿÿÿÿ*€é2�w<>X7‹ck›k	_	€ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿþ	þÿÿÿÿþA†	þ±6	ø)¾IÄ×�(ÿÿÿÿþT«þü±6ô)¾=f³ÿÿÿÿþT«쑹¼œ±¡ÈÈH€¹¹f)¾þêÿÿÿÿþT«ì�5±–Ò>žÄ
)¾þœ‚ÿÿÿÿþT«ìR«¶Ñw±LÏ’ʰ°ÙP)¾ý{­ÿÿÿÿþT«ñÔ—|±gÞ	prù)¾s±ÿÿÿÿþTÈXåÛB\Ü|±ÈG†¬ÖZ,`)¾dè^XXÿÿÿÿÿ*€é2�w<>X7‹ck›k	_M€ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ&?ÿ@ÿ	>ÿ¿ÿ>ÿ?ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿþ9ü6/÷A'$FÿÿÿÿìÌ=5ÒÛ"W¹'Ù|”ÿÿÿÿí-ÖÓ6LÁÝqž.×ÿÿÿÿýyã‡ýžãbüÁâ?ÿÿÿÿýYþDý~û"ý£êÿÿÿÿîÝKÕ
-ØX¿LÃs›ÿÿÿÿí¬dŽËA¤iÛ$ÆEÿÿÿÿëV¹Ï:{•Ü p(Ù
ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ/?ÿ’ü7c.ÿ0HûF$ÿW+ìU¯vµ¥« ˆˆ~n¬.Ts+÷ã«4ùºSn
¸.ðç	«Ï«TŒO›9…B.ì
ÆW+
6ÎG	fB%¢$“†*ýºhþ` ú«š$fW)ý¸lû dùPw°Qn)þfúØï4ݽӧ	¸P¥´ 0ýþ
þÙ?ÿ'ÿ'ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿùQ‚1¡ÿÿÿÿúÉBÐ:ÿÿÿÿü)Ø‘Œÿÿÿÿý�åÿÿÿÿüÝÏAÿÿÿÿû¨i+×ÿÿÿÿúR½|’ÿÿÿÿùhx
ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿÿÿÿ>ÿÿùQ‚1¡(ÿÿúÉBÐ:)ÿÿü)Ø‘Œ*ÿÿý�å*ÿÿüÝÏA*ÿÿû¨i+×)ÿÿúR½|’)ÿÿùhx
(ÿÿùQ‚1¡0ÿÿúÉBÐ:1ÿÿü)Ø‘Œ2ÿÿý�å2ÿÿüÝÏA2ÿÿû¨i+×1ÿÿúR½|’1ÿÿùhx
?ÿØùQ‚1¡8úÉBÐ::ü)Ø‘Œ<ý�å;üÝÏA;û¨i+×9úR½|’9ùhx
 ùQ‚1¡ùQ‚1¡ úÉBÐ:úÉBÐ:"ü)Ø‘Œü)Ø‘Œ$ý�åý�å#üÝÏAüÝÏA#û¨i+×û¨i+×!úR½|’úR½|’!ùhx
ùhx
(ùQ‚1¡éQ‚1¡Q‚1¡Q‚1¡úÉBÐ:	êÉBÐ:ÉBÐ:ÉBÐ:ü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œý�åý�åý�åý�åüÝÏAüÝÏAüÝÏAüÝÏAû¨i+×
û¨i+×û¨i+×û¨i+×úR½|’	êR½|’R½|’R½|’ùhx
éhx
hx
hx
ñQ‚1¡Q‚1¡ùQ‚1¡ùQ‚1¡òÉBÐ:ÉBÐ:	úÉBÐ:úÉBÐ:
ü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œý�åý�åý�åý�åüÝÏAüÝÏAüÝÏAüÝÏAû¨i+×û¨i+×
û¨i+×û¨i+×	òR½|’R½|’	úR½|’úR½|’	ñhx
hx
ùhx
ùhx
ùQ‚1¡ùQ‚1¡ùQ‚1¡úÉBÐ:	úÉBÐ:úÉBÐ:ü)Ø‘Œü)Ø‘Œü)Ø‘Œý�åý�åý�åüÝÏAüÝÏAüÝÏAû¨i+×
û¨i+×û¨i+×úR½|’	úR½|’úR½|’ùhx
ùhx
ùhx
ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ'ñQ‚1¡Q‚1¡ÿÿ'òÉBÐ:ÉBÐ:ÿÿ(ü)Ø‘Œü)Ø‘Œ	ÿÿ)ý�åý�å	ÿÿ(üÝÏAüÝÏA	ÿÿ(û¨i+×û¨i+×ÿÿ'òR½|’R½|’ÿÿ'ñhx
hx
ÿÿùQ‚1¡ÿÿúÉBÐ:ÿÿ ü)Ø‘Œÿÿ!ý�åÿÿ üÝÏAÿÿ û¨i+×ÿÿúR½|’ÿÿùhx
ÿÿùQ‚1¡ÿÿúÉBÐ:ÿÿ ü)Ø‘Œÿÿ!ý�åÿÿ üÝÏAÿÿ û¨i+×ÿÿúR½|’ÿÿùhx
ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿ


ÿÿÿÿ×Q‚1¡Q‚1¡Q‚1¡Q‚1¡Q‚1¡ÿÿÿ×ÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:ÿÿÿü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œÿÿÿÿý�åý�åý�åý�åý�åÿÿÿÿüÝÏAüÝÏAüÝÏAüÝÏAüÝÏAÿÿÿÿû¨i+×û¨i+×û¨i+×û¨i+×ø¨i+×ÿÿÿ×R½|’R½|’R½|’R½|’R½|’ÿÿÿ×hx
hx
hx
hx
hx
ÿÿÿ×Q‚1¡Q‚1¡Q‚1¡Q‚1¡Q‚1¡ÿÿÿ×ÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:ÿÿÿü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œÿÿÿÿý�åý�åý�åý�åý�åÿÿÿÿüÝÏAüÝÏAüÝÏAüÝÏAüÝÏAÿÿÿÿû¨i+×û¨i+×û¨i+×û¨i+×ø¨i+×ÿÿÿ×R½|’R½|’R½|’R½|’R½|’ÿÿÿ×hx
hx
hx
hx
hx
ÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿÿÿ.ÿÿ0ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>?ÿ


‚Q‚1¡Q‚1¡Q‚1¡Q‚1¡Q‚1¡Q‚1¡Q‚1¡Q‚1¡ÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:ü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œý�åý�åý�åý�åý�åý�åý�åý�åüÝÏAüÝÏAüÝÏAüÝÏAüÝÏAüÝÏAüÝÏAüÝÏAû¨i+×û¨i+×û¨i+×û¨i+×û¨i+×û¨i+×û¨i+×€¨i+×R½|’R½|’R½|’R½|’R½|’R½|’R½|’R½|’hx
hx
hx
hx
hx
hx
hx
hx
Q‚1¡Q‚1¡Q‚1¡Q‚1¡Q‚1¡Q‚1¡Q‚1¡Q‚1¡ÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:ÉBÐ:ü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œý�åý�åý�åý�åý�åý�åý�åý�åüÝÏAüÝÏAüÝÏAüÝÏAüÝÏAüÝÏAüÝÏAüÝÏAû¨i+×û¨i+×û¨i+×û¨i+×û¨i+×û¨i+×û¨i+×€†¨i+×R½|’R½|’R½|’R½|’R½|’R½|’R½|’R½|’hx
hx
hx
hx
hx
hx
hx
hx
Á?ÿÀ?ÿ


ÿÿÿÿþ0	þi 	ùqKe.ÿÿÿÿþT«	þ±6	ø)¾BŽsº˜ÿÿÿÿþT«ì<…w"±N~V
g`)¾þ
îÿÿÿÿþT«æY;KÖ±ÇOަÑU\Æ)¾U›ŠÿÿÿÿþT«æ9l¸]±jßi�88½0)¾%‡ÄyÿÿÿÿþT«ò¸yJœ|±JÍ–§xý,)¾þãÿÿÿÿþT«åÞÀ|±‘ÔA¤)¾
àÿÿÿÿþTòØå2š¿Î­|±«ÄÎN‰Á¬È)¾~Í¿Í`ÿÿÿÿ
þþýþÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ&ÿ'ÿÀ?ÿ


€ÿ¿ÿ>ÿ	??ÿÀ?ÿ


ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ/ÿð?ÿ


	À?ÿ


'ÿÿùQ‚1¡0ÿÿúÉBÐ:1ÿÿü)Ø‘Œ2ÿÿý�å2ÿÿüÝÏA2ÿÿû¨i+×1ÿÿúR½|’1ÿÿùhx
0ÿÿùQ‚1¡(ÿÿúÉBÐ:)ÿÿü)Ø‘Œ*ÿÿý�å*ÿÿüÝÏA*ÿÿû¨i+×)ÿÿúR½|’)ÿÿùhx
(ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÀ?ÿ


ùQ‚1¡ùQ‚1¡ùQ‚1¡úÉBÐ:	úÉBÐ:úÉBÐ:ü)Ø‘Œü)Ø‘Œü)Ø‘Œý�åý�åý�åüÝÏAüÝÏAüÝÏAû¨i+×
û¨i+×û¨i+×úR½|’	úR½|’úR½|’ùhx
ùhx
ùhx
ñQ‚1¡Q‚1¡éQ‚1¡Q‚1¡Q‚1¡òÉBÐ:ÉBÐ:	êÉBÐ:ÉBÐ:ÉBÐ:ü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œü)Ø‘Œý�åý�åý�åý�åý�åüÝÏAüÝÏAüÝÏAüÝÏAüÝÏAû¨i+×û¨i+×
û¨i+×û¨i+×û¨i+×òR½|’R½|’	êR½|’R½|’R½|’ñhx
hx
éhx
hx
hx
Ñ?ÿÀ?ÿ


ùQ‚1¡ÿÿúÉBÐ:ÿÿ ü)Ø‘Œÿÿ!ý�åÿÿ üÝÏAÿÿ û¨i+×ÿÿúR½|’ÿÿùhx
ÿÿ'ñQ‚1¡Q‚1¡ÿÿ'òÉBÐ:ÉBÐ:ÿÿ(ü)Ø‘Œü)Ø‘Œ	ÿÿ)ý�åý�å	ÿÿ(üÝÏAüÝÏA	ÿÿ(û¨i+×û¨i+×ÿÿ'òR½|’R½|’ÿÿ'ñhx
hx
ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿà?ÿ


	À?ÿ


	À?ÿ


	À?ÿ


>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>@ÿ ´ÐZh-4urwid-urwid-69907e4/docs/manual/images/urwid_widgets_2.png000066400000000000000000002114531526073734300235660ustar00rootroot00000000000000‰PNG


IHDR@ßXÎzÄzTXtRaw profile type exifxÚmP[!üç=/]<ŽûhÒôøEÑdÝt‡tDàú~Þðj`RдY.9£C‹®.µ3¡vîp)#»ä�䮨SíHCËiæÇ…©ºJ7#;Fa_E#²=ŒâY”ÖQÓç0*ÃH8
4j|s±íþ…ýÂ¥º¶ýÜëæÓ;“¿#Ì—� ³ˆFÒ–‚TäÌ’¸©ÚYevâù7§	øS«YšÎ­=…iCCPICC profilexœ}‘=HÃP…OS¥¢U;XqÈP�ì¢"޵
E¨j…VL^úM’GÁµààÏbÕÁÅYWWAüqvpRt‘ïK
-b½ðxçÝsxï>@¨—™fuÅM·ÍT".f²«bà>„1€>„efs’”DÇúº§nª»(ÏêÜ÷gõ«9‹>‘8ÆÓ&Þ žÙ´
ÎûÄ!V”Uâsâ	“.HüÈuÅã7Ηž2Ó©yâ±Xhc¥�YÑÔˆ§‰#ª¦S¾�ñXå¼ÅY+WYóžü…Áœ¾²ÌuZ£H`K� BA%”a#J»NŠ…�Ç;øG\¿D.…\%0r, 
²ëÿƒß³µòS“^R0t¿8ÎÇØ5Çù>vœÆ	à®ô–¿Rf?I¯µ´È0¸
\\·4e¸Ü†ŸÙ”]ÉOKÈç�÷3ú¦,0tô®yskžãôHÓ¬’7ÀÁ!0^ ìõïîiŸÛ¿=Íùý[³r�Y4¼
xiTXtXML:com.adobe.xmp

 
  
   
    
     
    
   
  
 

                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                                                                                                    
                           
J}óbKGDÿÿÿ ½§“	pHYs
×
×B(›xtIMEè-òóÚ� IDATxÚìÝw\UåÀñÏe/‘%8.p ŽTÜ«,ýYd9*Ó†™#-M+-GYj®²´,µ´Ô4sÏÔܸâVP�½×½¿?Îá²Ýñû~½x]¸gÞsžó|ï³4€!„B!„xJH$„B!„x*b¹B!„Bˆ§…@B!„B	€„B!„B !„B!„�H!„B!$B!„B	€„B!„B !„B!„�H!„B!$B!„B	€„B!„B !„B!„@B!„B!�B!„B<áŒä”	µ3àT9ý|Õ[à�y‡Ûx«¯g%y!„¢ŒÑÉ%x¬4÷»±î~w¢R?y/¸
„—Ô×§•1`d”²<¨SFR`|QÂòÊÀõ÷ÉÀµÖù¨|„½/àüžÇuõµÊ#þü†êýÊ´’¿!„¢¤hu@”\…ÇÀßÏé~ ݃*po³Î&`pì)¼Wk�çW ¢ŒŸk,ð*`ÌTÙ‚:¸×'€yE–›«�S0ì	¼WÃ�@?`±d3B!„å˃€ÍÕtCàe`ðpT-`ŠÂÚªÁQY ö¨i£u)çš©JmKXÞ0PjQP&g”š@!„B!ÊMJ3¯5:¬þôPד�·Ø^ƒÒgÄþ.ŽY¥	™ímÖËÛ·í]~& .Å›¨Y5î¯*.¸]=ªàtçí~÷y—úÚ®„e퀃j�TÚò‚ûÈŒo¹·H‡îÜ]@ Ú]~®¼ûïðÒ¹¹šŽk= ý	!„Bˆ'4*Í*àwµàøm	Ë�€€$”¾C7Qú�¼YÊþŒ€1ê:	(ìãÔ×N%H¿©ë]U×Þ/a¿­Ôõ&«¿ŸRÏ)¨@à6
8
¤Qú¹¤ß
ìËXÝWgõïsêß	À¾ë¨Ÿ·(`šº,\
"¢Púà
ª¨û]ªžg€úw0�Œ¼‹ ­´ÈE
øv«�³Ü.Z žKÍ"ë¢47KTÏ3	Ø|›à·ªzü$”þGQÀÀG=Æô¶qþ.�¶bÔëùV‘õ6_«¿ÿTà^%rE–MEæÕB!„(óu�-
¥æC£%y…ÊçQ¾qßQdýlµ oIáÚŽê²áE‚Ÿ<çš ®óy‘ubQš—ï”°�3¥?”üäY ¾6z×j€ú:¶H0‘Nþ0ÔJØ.J
(Ú…Ò/«æ]—úZp ƒ¶(50é(µ[ûKX^pÛ;ù|“(\Ó“UàóõJÍTÑ!×£Qj󊪃Òÿls‘à§`Ú2G ãN$ªA­Y)ËeØl!„B°báþZ0�¿LcçæÄݼñØÎ%6&’!½ýJ]¾iåþ˜;é¾�“�•IXÈE	€J9^^S¯¼>/uÕWcàÃ~šªË½Ô×
(5HWP¾µ¿+”~*Á(ýgŠÚ«¾Ö*aÙ­&%5F©¡Y‹Ò/BÝÿJu¹Û¸VµÔ ¥¤aÃ÷Ýâ¼ÏR|°�”ff)\;v+;Õ×vêke”Z¬‚ÁÍ”&d5Ь»óöŸŒ*aÙŠO0f‡Òtñ%ϧt¼„÷òÒ–Q)i«Y‘´u;a(5{Õ45¥Ÿ˜¡dõB!„Èó×Âé¤$%`bbÊ™øàµÖÞ»õ±œ‹¹¹%íº¼Zêò´”d’cïû8aÁ|9¢×qu¸*(û$y5A�Pš–•$C-CþÈ_×ïðx‘¥,�,²^A1·¸f[Õs½ Qq(µ•Pú£XÜçu2U÷u�’gNS¯aIç�RÊ>ójÅî´°~Zý\
Qú(ìÿS0B]¦i(µD·ãz‹ëœ…RCW�£ú_ÊþJÚO^Úê€ÒÌîviëNôAi69øDý‰Ei®9‰Ò'»B!ÄS¤Ã½ð¨Ý€Ê.ÕÙø÷¯<Óú9ýòËçOréì	*ÚØÓØ¯3¦fæ$ÄFsùü)ûuB£QÆ®:s"[{'\ªyÚXÈE´Ú\ª{*ßãž>¶;'\«+ß1Ÿ8´“ZuallŠ[�º…¶½xæ8Á©U¯q±óNOKáðÞ-èt:5ïHÈÅÓxÖñÁÊZ©ÃHMIää¡]¤$'R¯‘®Õ”ïÁƒŽï#+3ƒC{6ШyŒMLËä½yÔ5@y£³],!GþB%ý¼]$ðq¾“`T}-møh§"ë݉×ÔàçW”&VÔBðòk€îWÞ<;Ž¥,7Sƒ’‡9©ªN
vQúµCi6P`�£jà×e°3”fqÙw°ÿ¼û^ÒˆoÆ%%×os/]oqŒ‰·I[oÞÅu¹©:ÕÕôü‹úþXŠ7=B!ÄS.#=�+NáäR]ÿÞº¿æñùà—¸vå+~›Éèwž#=-+k[–/˜Îº¿~àØ�íÌ÷>VÖÅörñ4¾Szxdge2éã×ùsÞdR’˜<úM
�HŒ¿É´±ù½=Ö-û™ñÃ^!4äßO‘ýù5SÙY™|ú^WÖý5�ËçNòÕÈ>Ìœ0ˆë×”©CƒÏ3¢_GöíX˵+çó~wîÚÀÕËgÉÎÊâbÐ1.#;;«ÌÞ“GYdƒ2@ÀÞ?§¾¶£ðHo¥ITþ5ÕÂð­æÏIBéâ‰Ò|ªhíÁ3êëÝLЙ׿gi	Ëž)e›¼`~ǹ¤uQú#Ô¥ÕÞXtà�RÃÓ¥9^j‘ÏuP]v©À6w"X
¬R¼Žo	ÁyŠy«ASÑ¢’j¦­;mÜz§÷*¥ÏÚ”.¯ Lú*„Bˆ§Ü¤�ßÀÈØ˜¸›QÔôjÈðqsHKMfé/“ùlÊ"|ži‹N§ãÃ¾íØ¸b>¯ôÿ��'ýÊèw»PÙÕ�Ÿ¦~̈‰?ccW©Øþ4iÍ�“?";+“s�‡©ß¸5ƒŽ¡Ój9}|ž^
13·$91¿ø›ž–²ùSù|úêúú¡ÓjõÚ¹yZm.3~ÛÀ… £|ú^Wýòߘ@»ç{ÒçÝÑxû4gÑœ/iÞ®+Ýz¾ÇÉûè;øó2oU
PW”¾Ž(#ŠýQ`ÙuÙs(Ã5—ÄœÂÍÊæ 4¥›RÊg(øÞ"”&eYÇe41(>Ú×­ä
cÝ¡ÈûN%#O¨úÚä.޳H}Káù{ŒÈÐaÑ#€@™ÄÖ‹ÂÍßòìAé“õæ]@yçþY‘@\£~æ’|¯¦…ÉEîqc”áÁ‹:�RcÕèy‹´ey‡÷ª¤¡¼A©ˤôæ‡B!„xÊŒüògf-þ�é·âàä·j-LXÈ%xðm¡|4|›µ'øâi¥@éR�#¾aʧýéÜýu4i]âþmìqvuã\àa�_'ª¸Õ$øâi�î-q»ˆÐ+èt:êÔWºØk¨×¨¥~yðÅÓÔoÔJÿw
5ˆÊsþô®œdö¤aÌž4Œƒ»7q�ì¬Ì'êÞ<è ”!‰Qƒ7”~Ž@Js¡1E¶Ñ¢ŒÂve²Êj!:¥¿O# Js££ê6ÓÕmuÿËPú˸ Œúµ“üæHßo¨ÇuAmÎ¥G]5;tŸq�Zh¦œ·£ð)JÓ>‡¶Ù|€2Òsj!;Š’G.Ë3¥¯I”Á–©÷ë-”ÚŽ}Àò‡œ>N£Ô´xÜ"¸Éëä‰R;tä÷½e8é®êõùEMýÔû’HñþO?ªiaJ-Øn”¦o/ k>”Â#±å¥­C(Ã?«7NM[¾êþžG©É¥	_JSG”9‘t(#>¯¦½?Ôk…2�T/5�úC²{!„B˜[XaiUËéõö(†½Þš„¸Ñj›››ƒÆ ÿ»Ý ãû±²¶!4øÂ-�Q¿I�î%ðè>7‡Ô”Dõï½þtF±õ5hµZ´:�þ]­V‹ÚÝccR’ò;ÎÎÊ*Ô”M§ÓÑØ¯#žjß&€î½bhdôDÝ›]d¬`û©…EKµ@ü=JÓ¥�oºJ/o”Ú¡·Q&¤Ü„Ò±¼Ê =ÈB™sæG”fb?©ëÏGùæþr�uÔ‚îfõ¼Vª…íÚ(Šö¿ËÏxD-€kP†ãÞ„Rõ¥7Ú¬L	À«êqÝæ89(#Œ-VøKÕß[«Ÿ÷YJ áAÒpò†½.ê ù}~¸³þ?yz¿©Ý
õÞTUÿN¡pÍ(´f£ô7z¥/Ô»äχTtî ójÚZ£Þë?¤­×�mä×ú ¦O”ÑýZ£ôM¯¦å«(M*G«it»Ôz«�ö8Éî…BQÔ¹ÀCØ;:S¡¢-UÝkallÂÙ“Êw¯¹9Ùœ8¸“š^
Ø·}-'ídö’=DG†²ñï_KÝoƒ&­9´g	q1¸Tó¤A“6ìÚò7±ÑÔ®[¼1‹KUOŒ�M8wê�>ð
:¾O¿¼®¯§Žì!)A)®عžÜœü¢]�úM•Áê6ÖÿÔôòÅÀÀûJΤ&'¡Õæ–ùû¡Q¹š2tN¦(ý{ìÔÂl%Ïõ“Çe8h{uý�[V(µI(µ5÷s‡¬Ô *G
àrâ51SÙ¹(ýZ²ÊY¾`…R“r±”ùN|¨#}(}²W5mÙ£ô#½Çki�R»i‡Rt•Âs	!„¢|Ó­(½úË­œ©åÝË
D߸NVf:}|Bûç•ùÛÖ-aÁwciÖö.œ>‚µ�=“æ¬!6&’O<Ï3–QÓÛ—ðk—øt`7&|·Ï:>ÅŽ“–’Dß.uhÛå†}>­6—¾]êP»ncÆÍRŠC17®3¤WVìVf�Ùºf1¿ÍGëÎþœ<‚�]%œ«¸1ä³Yèt:fŽÈ‘}ÿRÅ­Ö6ö„\<ÍØiRë!‘aÁ|5ò5LÍÌiд
ñ7£Hˆ‹áËþ`ÒÈ×IJŒÅÉ¥G}‹U›~áýýœ¸ÏØEW !n mhZ	eÞ&+”¦x1r™„Bñ¸ 3'Щ_ÇÛWªŒsU�bëD„^áòùST´±§^#?�Œ¹~•Œô4Üjxë×»~õ
úá­‹ºxævÎ88¹|!+k*»º���ÅÅ3Ǩ۰…~›«—Ïr�^
126!+3]?œ5@äõÐé@£apÏæü¶!H?CvV&gO$6&['¼}šcj–?vTlt7¯R»^SŒŒ�%â8ƒÒìî8J?!/à=”þ:Ÿ¢ŒÈ&„BñØ 'Ùæâ^«>‰ñ7ÙüÏB�7sY™9¿IúO˜CÀKäÏ•‹2õ`”A4„B!Ä=ÊÎÎfÍ’9hµ¹x7lA÷Þï—»Ï(5@âIe£þDPþúD	!„¢l+·5@e�Ô‰§Y‚ú#„B!Ä3�K „B!„xZH8!„B!îŽN.Ác�aîùÞI8!„B!]\4v=5’>$}•ÆÉÏ_Ò‡ä-ÿˆ
X-ùG9&ù‡ä3ÿx$z¼î+}ÈD¨’ùI_Bî¯äBHþ!ùÇ]Š
X-‰õ1pòó¿ï}H8É|„¤/!÷Wò!$ÿ�üC<5$’ÌGHúr%ÿBòÉ?„@’ùHæ#$}	É?„’Hþ!$’ÌG2!Á��üC!ù‡äB ‡#üÚezµ÷`åâÙètº;ZG§Ó²lÁt&�êGbB¬d>O€¢÷L«ÕòÝWÃx÷å¦$Æß,ö·<Ü„Ü_É?ÊŠË×ÂñhߋًW–úœeçù"ù‡äB<Ðhí_óð÷sÂßω·þ×�)cÞå¿M+HOKyÄKCíº�ñkßSSsÉ|ʈ‚é#ïçÓ�ÝÑjsïúžIá¸üúoÓ
üýœx£‹7aÁÅ–kµ¹œ>¾ŸøØè;zÿv鱤㤥$óÁëíð÷sbʘwÑjµ’M`öâ•\ºöÀ�µbÓúã‡E<¶Ïœ«Õ²ÿøi¢cã%c¸Ãÿç�KA÷]&¸—gÚ]¼™ðQV.žMØÕKüQÖÓïÃØi÷>ƒ±±säô±}ü0én„_¥Ï»£ÐhîaÈî;Ù¦È:�†FÍÛKæSF½øúPìì�0154ÅîÙ­
œü”_ÙY™ìÛ¾O¯F„_à\àa\ªzZ'2,„©c0~ÖRlíoûþí¤&Å|1¨Ðq®^$:"Tò�2hpŸîT®dÇåká|÷Ûß,[¿�µ?OÆÑÞö�ì?3+›µÛ÷ÑÈË“Áa<‡GU—û~LÝ‹�°HŒ™ÊÒYãØç{ÝM™à^Ë#¿bï’Ù�õürµZžyå}v,š‰M«bË7ì:ÀÖ½Gøá‹a÷}œóÁ¡Ô­áVþ v]zà^³Ͼø:£ßíÊþëy±ÏûXZYÆá}ÿr!èW/Ÿ¥QóötìÖ›jîµHNŠgëš?عŸ¦mhÜ¢VÖ…ö»u´Z-³¿þ� ˜±`3*Ú1ûë©ääJÛçz°yÕïÝÇË­êàÞ£+ÎŽöÄ'%³xõÖíØO‡�èݵ#ï|6…—ŸkË}{Ü['™O1žïI5�Ú…Þ+éžÝIaùhÀvön[CDX0ÍÚt¡c×Þ8:WÕïÏÉ¥�ºöfãÊ…œ=u˜o~Z�‘‘±?eTDX0gN`Èg3”{»}-m:ûclb
@xè–-˜NjR,£ßy€1ÓþÄ¥ªG‰ï7nѱät0W™»Á¾r5|·âhÀvüÚwÅÀÀ�NGЉø¶è@\Ì�BçW0ÿн|ˆöÍÑ»[Gj»WC«Õòá×ÊmäÛ½ùuÅzž£×èÓ­æf’<=º´£^Mw´Z-,-˜³t-Q±ñ8ÚÛ’��Ãö€£¬Ù¾—Ë×ÂéТ¯<׎ÚÕØì4}?žÄ[¯<ϘAob Ñðó²µLûu³Ç
§[{?‚Ã"8pâ3>Âö€£¬Ý¾ÿÎm05ÉÏ7â“’ùcÍV6ì MS:´hŒµ•E¡ó‹Œæß}‡9t�³—¯ÞuZ¹ÎôˈMJå¹wFðç´1tn̬ٔlåsnÛKpX]Ú4£w׎TuvÔﻚ‹½»vbáÊ�>u–Õ?}ƒ±ÑÓ9õß�–	š¶êL×ocïè|ÛgLIe­V‹…eÖ.�C|l¶öŽädg«yÙ¯]¦Q‹´{îz{ÄÞqš”àçÉ4}á_¼þ¿Î89þò"'7—ë7bûùh4ôîÚSã’ËD©iÄÄ%Ü÷q’SÓèØo7ö¯*S÷ç¡öÒææ’›«ü“(‡JKMfûú¥8:W¡Y›.lYµˆiŸ$)!­VË?‹`ÉÏ_ãàè‚™™gO -%©PFv»uJ³}ý2fNL…жøÕ®Äì?V3mþ2´Z-Z�޹KVóͼ¥Ô¨îŠ…™ƒÆÏ 4"Z2Ÿ2H§Ó±mýR¾ó6†FÆ´ìØ�€�™9aI	qúõvl\ÎŒñƒ‰�‹¡ýó¯b``(ÁOvæäAŒMͨáåCc¿Žœ;yˆðÐ+úåq17ÈÉQÒ~·^ïóÖ°¯°³w*õýRÓ�¡’LMÌhд5§ï!îféi)ìݶ¿v]õùVž¼ü«±³Ž.mš±hÕ~>�¸„üügÃñÍT²³ÁÉÞ–1³°lÃö2Ó7¤¼äI)i܈‰ÅÙÞ»ŠÖèt:þ\÷/ý?›ŠV«£{Ç–lÛw„^ÃÇs1$Œf
½éßãy~[¹™“g/qéêu~X¼’nZÐɯ‰~¿OžÁÌÔ¯tôkÌ¡“ç¸^èôÃâøúç%¸8:`afÆ„ÙIJI+öÐ_º~;Uœï)­Üˆ‰Ó§é÷{uã«aoádo‡N§céúm¼=æ[Œ�éÞ±%w0dÂÌBû^¾qƒÇÏ &.žWŸo�¡�Œyt«2�Gíú¬þc6ËæOC«ÕÞñ3¦Pþ�’DlÌ
lì�±®¨Ü«×ýÉÔÏú£ÓjiÙ±;GömcÊðîw•&¥üñäzó¥çõNoý�±QÉe�ÀÁü¶j3kwì'-=C(mÙ{­VyvDDÇòï¾#úmB#¢8yîr±}…FDqâl~3Ì£A8}1¿™÷�“gˆR›Õ֫鎑aþ9�»r�ßWmápàùâ_:gç°vÇ~–nØADt,‡N�#2&¿O]Ff›vä·U›	º¢ÿ1¥Yêæ=‡Ø¼ç	É)e#-?Œ�îÚòÇìäÈþmD…óÖ°¯03· º§ÓlÁH�8+XÛðûã¹NNN»·®Â˧ÿ˜�¥•5õ·dâ‡}ôûNˆ‹¾í:¥Iˆ�døß3øc’S|8yö2GÏ‘’–NzF&k¶í¡•¯73>Š•…9~�êÑçÉ’ù<`ÿm^¡/œV©^ƒF-:Ü}á'!–UKæÒ²“?ÃÆÎÂØÄ”MZ1nè«\:wßfJs‡äøXÞ:ží»>²<	~îMFz»¶üC
/+WA«ÕbljFЉ¸Õð ~ã–„‡^æÐ®õ´yÖŸš^
õÛ—ô~^SÊÒÒAnnÕ=ê`hl̵ËçpptáúÕK$ÆÇâY§驅αº§ûŒÇØXÉ:m¬+0þ‡ß	�¾‰�u~‚Ïõ¥I½:ÄÄ%Ð}à§ü±fk™¨*ùGÇ~#ô¿{¸:ñÃøquràf|"s—¬¢•¯7³Æ~€•…9í›5â¥AcXõïn>yïuÞyµ+›wdö¢¿©¨Þ¯¡oôÀÌÔD)À¦gðÏ–]øxÕ Je¥6ÅÌÔ˜'‚ðV›oDÇ%°jënZøx1û‹áX[YÒ²qýbÏ
/ÏêlY0ýžÓJËÆõ¹Îú]‡ð¶

½jè?§§–Ì;ScZ5iÀ«CÇqâÜ%Ú7ó 6>™ñCߦkûeªõBY’W&høL[ÒR’¹|ö$ç��ž–BNvÖ-Ÿ1�[tÔïgD¿üß�\=øpü88¹’“UKæâíÛŠÆÎÂÜŠ·šÙÝUš”òGù7ÿï�Lúé^íÒŽcA˜¶à/6ý2k+>›ù+•ìhèUƒ…+7ñã’Õü5�ªÎüðçjímhèU£Ðþ®GÝäã©s	øk¾˜Ž“½-[æK®VË›£¿áß…ÓÑêtôÿt
ç6-ÂÎÆšUÿîaÄ”¹¼ül–mØ�±±Væú/�{}ô%7nÆÒɯ	¿®Ø@ZzcÞƒ;¶$2&–W‡�§ª³#5ݪ2ó·Œx«'o½üuj”Øä®\@ë–ÍÅÒÚßfíèÞë=š´ê¬Ï€ƒ/±çßUœ:²‡È°²2SÕ‚H.ÉI	¤¥$Ñ ik,,+PÙµ:líó¿U»ƒuJccïÌKn‰€–fxVwåBp(¹¹Zâ“’‰�O¦ç±T)n®•±·­ ™Ï¶vÉ�úßûûòž ÄøX’ãcÙ¿}5û·¯.´,>6¿jÙÂÊšÚõ›Hðó¹ÀµËgéÿÁxŒŒMpr®Š{ͺìÚ²’NÝzë¿D¹·J6v•hؤ5§ŽîÅ·y{Μ{ÿ
|1€i£RÓ­Š~ßBÂ8{ùã?è�‰±U��¨[Ó�•[vÑ»[',ÍÍHHJ&)%�ÖMPÁRiöV½„gEÐÅ`V=„´ŸHl|2«·ïgõöý…–Å,ÁÚÊ‚&õkKðs›2A7%°4³°Äµº'¡ÁÐææÞñ3”>@ö•œ‰�‰d˪El^µˆêuHJŒ#9>–Ž/ôÄÌÜ?H¸Ë4)å�'ÛKƒÇbh¨Ô¾¾Ð¶9ß�ý ÐòÔô¾ùùO|=šŽ-¡ÓéèÔ$¿,_ÏÇïô¢M“ì9HC¯ì=H×¶ÍÙ{4�ªÎì=XlMëÕ&":–ˆèXÒ22¨ì`Gtl²2Sùeúh~™>Z¿ÌÄÔ’ÐàÔªÛèž÷«t```@£æXüÓ×¼Ðã-ön_M�¾``Xx}�˜õtùx
½k²Lí”þçÚ­Œœús¡õ²³s5wÓêtJþñ€äõ¨í^•áßÌáÅN­©åVõÖj”tV°M{\b:��FƒN§cßÑ@R3³=ýFOÿE¿ž¥©	‚CiT·V‰Ï ƒ"ÏŠ1±ô}Hi%/ïëÔÜ—¶Ï4,´¬ªs~³OccÃG–÷=©��16ίi)ø?§ÏÈïPÕ½6s¾NëN/RÕ­V‘çƒæ®Ò¤”?ž|Nÿœº5Ý”gY	ÿ�—®^''7—–�êéË�í›ùꛬµnÒ€å›vòæKÏ—”ÌØAo°xÍ¿tòkDTl|¡<)ÿ߈æ>Þì=HZF­›4àFL,'‚¸J›&
Šm—À�›q´iš¿¬MSŽœVšÂ]¡¹o]}SZW§J¸¹VÖ¯{äôyíl6i¶úL‡ôÌL£b
Õx—û¨4	q1$ÄFâQ»/6v•ÈÎÎ"äÒýr«
±°²&èÄþ×k–ˆŽ#9>ö®Ö)Ienr«o‚*XameÁÁA¤¥g`iaNDÔMbã“%ó)‹ßÜÙ:PÁÖ3sž}ñ
ÌÌ•obµ¹¹�¥¯…?÷'-%‰ÿ6­ÀÒÚžv]zè¿ÔHNŠçÀ8ut/5½}Ñh4ú~\¹9ÙE™’ß¿îµê‘’œÈºå¿3šu|ŠÝß3q	DÆ&з¶•ìlÈÊÎæÌ¥â¹JRJ×Âoà\ɞ؄$N�»ŒmEk¬­,%ÿxÀrr•o0³²²•~4¶<DjzV愆Gu3žî[ah`Àù+ט±à/^ïÖ;›
Ì^ô­›øÐ¸^m’RÒX±é?ì­-éÑ¥�>¨‰OJfÃØ{ô¾Þ5©¨>+œb@¯ÿQÁÒ‚°ÈèBÏŠ˜”Vò
Ù9ùµF¶6ØÛVÀÂÜŒ7^|V_S”›«•9ˆÊÀ3F«ö{ÎÎÊÂÆ®¶NDœØDjz£»N“ü<ù¬,Ì©x‹¼ßÀÀm‘ô”“›«ïÚªq}FN™Ë΃'hé[�gx1lÒì>|Š
½KíSÔºI}ôv�ˆŒ‰eïÑ@·òúÿ:•x@¡Z¡\m~¾cbbDJjz¡mRÕ¾J:­Ž¾ÞthžÿEeÿ—»PÙÁŽŒ¬,	€¬+Úbcï̆ÐææLb�ÀÅÖÞ‰V�^dÃòŸùþ«áxÖiÀñÿÚÇ�¬SRátÅmÎÍÑÞ–;µâçå9eŽÒ~{Ï!É|Ê(k{^éûó¦�"#=�ú�[‘™™Fà‘½|4aö•œ%øy‚„\>Ëͨpüû¡×[#ôß~¦$'réÌ	vm^Éó/÷ê‚�~„¦¿ÍƧIk}?²’ÞoجÝ¿’“+uê7eóÊù´êÜ'}ÿ!G”�Pl+ZãloÂÈÍÕAl|b±}¥ff1jÊ\^y¾ǃ.Å´Q±03•üãøgË.Ž�&*6ŽßVnÆÙÞߺµ°­X�¡}{0bÊ\>úúê×ö`ÕÖݘ™óR§VddfñÃÿ0èµ±07gíö½Ì\¸œŸ¿ü˜³—C�ºÉ�¾þŒx«—>
&&§pâÌ%VnÞE¿—ŸÇ©À³bøWßÓ Ž'ÿ8^ø9õ€ÒJÞ¥³ýMë&>Ô¨^…öÍ}ù ï+Œš6�´ôZ5®OZf&{�2gÂG8W²ªó’�/ÄÆ®R~AðYÿB51êSÉÉU¿î®-ÿpúXq±Ql^ù6öÎÔªëK…ж|Ò·Ë=§É
EF”ü£ü©YÝSSNœ¡Cs_rrsùïà	}3´ÊvTuvä»E+ÖWéæQÕ…yË×Ó»kéÝZ7iÀ/Ë×ðÓ„Ä%&1cá
"¢oòÓ„ÅÖ··±¦ª³#»Ÿ¢×íÑétì9¨ïäç[�Éó–rýFU*WbïÑÀB5—Ï4¨Ã± ‹Œz§wþ—ZŒ��¨`iN\Bv6ÖeæÚ?Òaaœ\:fÕ=½8yxMü:3tÌLì+WÓG ¯öN¯wGLVF:#'ÎÅ«a³BQêíÖ)¨)GîìB0¬ß«|üvOÎ\!-=“Ï÷S–=¦ªhÉ|J§Ñhèü¿×ø|ÆRìœØºf1çN¦ug¬*T”àç	sü ò%†O“Ö…š~XZYÓªó‹ÜŒ
×××ómA¿¡¸qý*GöoÓ‘]Úûwô¿flL³6ÊðÙÍÚtÁPm
S�üoõ]�˜1f(^žÕÙsø$�ýš0sÌPªU.\àôpubêèA\½~ƒëQ7ùvÔ@^}¡½äÈÜeë˜2o	»�d`Ÿî¬˜ý%U]Ðh4ôz¡K¦�ÅÄØˆ¿7í¤]3_Vþ8	¯nlÙsˆ¶íãÃþ=©Q½
®N|òÞl?x‚•[w±=à¨Zpð)”­­,y±s+£nræRï÷*£ßíEpXéYÌ�8’f
½xZiá[�	Cûqõú
¶í?‚©‰1�†×þ×™¥3>ÇÉÁŽÅk¶røÔ9ü;·¦b«§>/Ù±a	ÿ,þNÿ“žzo#NÝÍ3fݲ¹,™7…“‡vѽÏ@¾œ½—ª´Ô¸ç4¹jÛžÇZ£'å�GÃÜÌ”‰Ãßâí1S4a~½†`nj€W»ê×iÓ¤çƒCiÕ¤~¡¿[—Д-O½šîddeQÓ­
¦&Æ8W²ÇÈÐ7WgímJÜæ«áoóÉôy™øû�À¼À—v
½jðö+ÏÓ¼ç ^ð	“~úƒÚîÕô5Pcõåf|"-z
æóï0lÒl:ôûPj²»whI�aãyoÜ®†ß(åH@G±†©wnu@T™­s¿ÛÂiNN.Fª÷;ÍËŒc槃y½{ç§2óÑØõ¼¯è¯,§�’Ýù¿ƒ?àäç_nÓÇÝÞß¼ùWœbó‚8ØV|lç^^ò�¨€Õå²MWYJ+’È—cå5ÿx†¨€Õ¥.8qŸ:žúAYòdfesâÜ%šûxëß‹ää¹KØÙXÓÒ·ž~ôH€7ã�ˆæ™uˆKHâ|H-zß²¯Øé‹ÁXY˜ã^Eiq!$
j¹ç÷a8DÓ^ú@æJh§Î_Æ»†ö6ÖÄÄ%èGÀ�Š!5-§JvÔíÚŸ�ó¦àSÇPšÏ:y–°1ØU¬@3ïBó£ÝŒOäâÕë4¨í¡¯Yº�¼ƒû‰]]¹í!y/…—QSçbnfBåJödçäð׆í8ÛÛз®|óòÔ�àGHáE!ù‡¸ÏtPJÙÑÔĸPðàQÕ�ª%7ݯì`Ge‡üÉáíl¬KÝwAõkyú»¶{µâçØ¨ð€ežÕ\ð¬æ¢ÿ»’]~mѲ�;ps­LFfS]F­êU
ÃÐÀ Øþ
r°­X¦¾ì1’ÌG-öj4tnÕ”-{òÇš-ØV´¦Çsíx©sk<ªºHæ#$ø‘û+…!„ä⩤AÃüÉÌÊ»†Ó?„�Á“;Ì~¹€î5óÑh4¼Ð¶9/´m.™��àGîï]300`öJþ!Ê|Zò|�üCÜ­Þ];Ürà…'.–ÌG2!Á��üC!ù‡O‹rIæ#$}	É?„’Hþ!ÄmÓ¾d>’ù	~äþJþ!„�üCÜu42ñ„’©£…B!„OEìc ×@!„Bñ´�H!„B!�B!„BH$„B!„	!„B!„@B!„B!�B!„B„¤!éCÄcÎyPú�ÿñG¯*aT%ì¡ãAÍ�.é£|ò÷s’ç‹xèÏ—‡Å­�›õ8Ò‡]ÏûÞ‡4�{‚ƒŸ*\—!„(sÁ�BQ–É%xòT箄˅BHð#„BHT¾¹q"äB!$øB!î�4�“àG< ¹¹¹Ì›7O.„�àG!„�HÜ/wB$ø)ã&OžÌ¸qã¸råŠ\!Á�B!�¸ŸàÇ™H¹eX||<‹/&::šÁƒËü!„e”ô*ã<¦27äB”qƒ
âÒ¥Kû¬~�Í›73~üx>ÀÇŒ±±1“'OÀ××—iӦѩS'ý6›6mbâĉ:t€‘#GbjjÊ7ß|€��³fÍ¢C‡úm6lØÀ×_Í�øè£�°´´dÒ¤I4hЀï¿ÿžöíÛë·Y¿~=“'O&  €áÇcmmÍW_}@½zõ˜3gmÛ¶Õo³víZ¦M›Æ¾}û6l¶¶¶Lœ8€ºuëòÓO?ѦMý6«W¯fæÌ™ìÝ»€¡C‡R©R%Æ�€——¿þú+­ZµÒo³jÕ*¾ÿþ{vïÞ
À�!CprrbܸqÔ©S‡вeKý6+W®dΜ9ìܹSÿÌüì³Ïˆˆ(¿-[¾üv%Ú"ô�ùÈŸZM‡slçìí*<°c8r‘qß,gÛê/îyÛv2wÁVVÿ1ªÄåƒ?žOó&5y³wÛû:×ø„TSq¯îøXï�e"§ûp]'ó=ž\Á‰[~'?ÿ‡=Ð}OTWÞçiÐétÌš5‹àà`–/_Î�!ChÒ¤	ݺu+ןûA¥™ç¥|?ê<@’>D‰Ôy€ÚóÅÐÐ�èèh”ÿ+++ÜÝÝ9}ú´~�zõêqíÚ5’““¨R¥
�æ¶Û„††’””¤ßÆÀÀ€ÐÐPý6–º�««+†††úm,--ñôô,´Mݺu	»å65jÔàÔ©S…¶¹~ý:‰‰‰úmŒŒŒ¸víZ©Ûx{{®ßÆÅÅcccý6Ô¬Yó¶Û˜˜˜põêUý6µjÕâäÉ“…¶‰ˆˆ !!A¿MåÊ•Ñjµóùòðÿ·˜ÈСïöíˆse[ý{cG¼Ì×3W1êƒîXZ˜>°Ù¹÷CFÍçìÁY÷¼�—#Ø{àïöíXâòWûϤmKo†èr_çúÇò=ü½öë–~rïåe ûŠ]¤è	~DÙ	FŒÁùóçÙ¾};&L�‹"žêàGˆ²ÀÞÞ{{ûBï5lذð³ÖÓ³Äínµ�‡‡G±mìììÊÄ6îîîŶ±µµ}$Û]§èßnnnO]Ø¿3�|
_«†õÜ024 =#‹­;NÑ©]}¬,Í8yú*Ù9¹4õUÒeй0ö:�­�Ýžk„…y~Ðt%$Ší»q¯îˆ‘¡a©çpàÈE\�í¨VÅ�NǺÍGy¦q
œ�lÉÊÊaóöt¾	­-¨åéRðÛ¶ì8Éõˆ8:¶©Wl¿7âÙ´í8v6Vtn߀»ƒø_—Æ*½k®GIJcw:�Ž.�RÙц¤ätŽ�
æFTk6ÁÔÔˆç;ù>–{#}€Ê \–àG!Á�B”#:�ÿ¾ÓHLJÃÜÌ„Õ3hä¯\ºI'ÿ¯ôëNùn
]{MæÂ¥æ/ÞA£¶Ÿ�”œ(5>
ÛŒbOÀ9¾�½Ž/¾Y^ê1W­?Ä÷?oàTÐ5üûNgþâÿ8|�O&,A£Ñpèè%F�ÿS¿]¿Ásx{èOœ9Æó¯~ù‹ùóO†\‹Æ·íhþYwˆõ[�ñâëßâßw™YÙ¬ßrŒf�ư÷À9¶ï>�o›Ñœ>JZz&WB¢ˆOLåàÑ‹;üØî…Ô•ÁàÇ‘h¹B	~„â	Ö¶Ûx}�ˆ·gXøÃ BËçN—¦>å—EÛùiá¿|>òešúzr-š	Sÿæü¡ïp«V	€Îþ_1wÁV>ýð%Æ|µ”1#üùì#Þøñ	)%žC§v
ød‚ØìØsš—»=ÃŽ=§ùbTvì	¢S»úŶ9~*„¥+÷}q>v¶VŒõ
޵ÞÕ/ŸöÃ::¶©ÇÒ_‡ðÓÂÙ¹÷¹¹ZÞñ?~ûþÝžà³/—2qê߬\4’ž/µàﵘ2þõÇzo$*#4è¨Áe*#C!Á�B<á6üõ)>õª`bR¼ÈmiaÊß¿�À§õ(ºtlȇƒ”Á“Žœ¸‚…¹	¦æ÷1Š�OáÌù0t:'O_eöԷ󃜶õ9~ªäÚ”VÍëpþR7c“Ù±;ˆ±#_¦×Û³HKÏdÇîÓŒú {±mNœÁ·�;v¶VØÙZѨA~S¾�!¼ýFþ@í[×Õÿ~-,†È¨Vm8ÄÚÍG�ˆãzD\™º7•‘à§&—pà¦\Œ'PÁAbcc™0aÂS1‚�àG!Dé*X™cSѲPy¡¨½ÎSÑÚ‚à«Q¤¥gbanŠV«ÅÂÜ”÷ßz¶Ðºöj@¢ÑhÈÍÍ@"·”Á$ò‚¬f�k°õ¿“œ<}•f�kвYm6m;Á±SÁ´kU·Ø6Eö_ô¦¦Æ$§¤ëÿNNÉÐÿ®ÕêÐhà�žm¨hm‘¿�IÙ
9¤�?â~ï¡FÃáÇ™3g7oÞdÖ¬Yy#Ø!Á�Bˆ�
ºÆØIËØ»éKêyUcèè…<Ó¨Ñ7•Qöš7©©ÿñt¯ŒF£¡‰¯'Ûvæ�Xð÷’tlSŸ)ß­¡qCŒŒ騶>§þMƒºÕ±µ±,¶~_OÏ\#:F9‡˜›Iœ<}U¿¼mKoÖlµeŒÇ�¿ÜIÙÄÀÀ \Ï´{ÿY7ôÐq�g×¾3´lV‡¸„®„ÜÀï™Úúeaá±DE'ÐD;ôúM¿LVV^µ\ñ-Ð',<–}ÏãéîD-Og.ßП]TNN.ûžÇ§ž›¾ÆgOÀ9j×tÁ©REbã’¹v³Ð°Ý{Γ@ë^ÄŧPÑÚWge(öÜ\-ç.†cgkEÀá|0z!‘çÑoŸBÀáÄŧà^Ý‘Mkad”?\÷ùKáÄŧúüw\þxóIô˜‚ŸÚ\ÀŽûï&PÙ‘››Ëüùó8pàSñy%’àG !�ä/=}22³™÷Û6Z6«Mй0fÌYO·ç3yÜk�¦ü!¡>ÝÁ�([

ŸšàGHð#„�üE<¥å®GÄ2aêߘ›™0äÝ.¼×¯Óõ$zÄÁOÎcK¼\!„N„xH"""ä"”ñü%22R.üÊØØ�i_ö}¢?ƒ@�ˆZjsA‚!„?B)­¶øÄŒY™d¤§ÊýBÈóE	€žÌàÇ›³üy8=bñqÑLøðö�Q‡¿Ñöž�1ý‹÷¸~õÒ=oÿõ¨7ˆ¹q½Øû;7¯`ÍÒ¹÷tÿã’øcí¿’„ò|B ÇüT Y.†�‡Ó–›“]ê²´Ôdì*3õ—M…ÞÏÎÎ*¶î/«�{¯¤õ²23nyÌ{ñÕ�«qt®Vè3é´Z*’Tèþge—|ì\­­V—ø%¥ðÏÖ=’@„ò|â.H 	~„<œÊ¬¬ÌÏ�ÄÕËgÐh [Ï÷hÖæyΜàïßg‘““ƒ©™9CÇÌbʧo1õ×M¤¥&3ÖX¯]ÆÄÔ”¬Ì†ŽùžªîµÒ«¿¬:ÆùÀÃ,ýu*èt¤¥¥àæéÍбßðã×ENV&Õ<½y÷£I·=ϽÛVsõòYú˺¿æ±}ýŸÌ^²—áW™?s,ŸÏXÂÄ�z2tÌ÷8¹TcëšÅü»ö4
®Žø¸ÙpòÜe&þø;¹Z-žÕ\9qö»þø�NÇÌßþæpà9^hÛœ~þÏ1wé·„Òï“É´oæKÿ—»H¢BÈóE	€öÌÁ›³X‘"CÈÃé;~ð?²³2˜4g5I	q|úÞ4kó6Šw^lˆ¥Ö$;+­NKð…ÓÌ_{3sK>âODè\ªy2èÓé*YãÔÏÞ"òzÎUÜoyžu}[°fé\úKà‘=XW´'&*œSGöàݰ9ÙÙ€ŽÌŒtÖ/ÿ…™¿oÇÝ8Іô5šýÇ?|úÞë4óñâ÷U[8tê,GN_àjø
–7�NÇ‹ƒÇâß¹5ƒ_{‰+¡,šú™$!„<_„�H‚!§'ݱ€m4mõÖ6v88ºpõò¼¶À²B±mNÝC«N/`kïDu¯R‚–昙[àQ«W.âRÍ“½ÛVshÏf²23ˆŠ%èøþÛ@v•ÉÉÊ$>6ŠÔÔ$Ú=÷
�G÷xt/þ¯)´îÙS©S¯	ž¦1T%œvÏ4$Wm
wâì%šÖ¯@ûæ¾ü²b=ëwp1$Œ~ŸLàf|"{�RÇ£š$!„<_„�èîœ<¼‹º
[`lbZê:çN¢Š[-*T´-üÔå–¤ÞrÿÁa‘äææRӭФ6!§»äè\�ؘHýßq±ÑØWr!9)K«·q­^ƒáWÐjs‰¾Qòu62ÎÿŸ× ÓéHMIdíÒ¹Ìø};†Ì÷9%ô*I½Æ­øgñlêÔkBƒ¦mXöëTB.žÆ³vƒBë98º’¢¿ÿ‘1q8ÚÛàä`GL|Nö¶\½ž?I`u'œ+Ù1øµ—
íëjø
´:­$!ž¢òÇ�>_¤ü!DéžúAöü»Šô´[1G÷o#>6ꮃ€�³i÷AIiB‚Ÿ{ЬílYõ;Çì`ÅÂ88:ß¶ Ьuþ]³˜më–ðÓÔ�166As‡v›’ž–ÊùÓGؾ~)ç�Üñ¹ú4mÃÖ5‹hд
ÎUܹr!�êž^h
g³~îæ$ÆF²hõVÖlßÇ®Ã'õËz<×–�¾ù‘å›v2åFýyûwnÍÒõÛùgë.„„²|ÓN¢bã©æìÄÍøD¶î=Ì•ÐI0B”óòÇÝ<_¤ü!ÄSÅD…|¾Ð{ÁÉHOãÔáÝ$'ÆóbŸAXZå7£	¹DÈ¥ b£‰»©|Û±[�«zá8žÇ8~ô‰)ùWvN.‡ϳûð)²³s$e		~€jîµù|úΟ>Bå*î|>})NÎÕhѾ›~=s+ž{éM\«×ä«9«ÑææðÜKý¨PÑ—ªJ¶_@¥ÊUiÙáúí}›µ§º§&¦f|4a.ÿ­'-5™áã~¤¦w#Úuy»J¥žk½F-éñæp¼e³  IDAT}šðÚ€OèÖë=ýòNÿ{�ZR¨JæM&!9…à°fþ-}ë0àÕ®ŒÒ�ôŒL^ëÖ/Oìm¬ÙðË’RS™·|)iéX[Z`` aÁ×£¹~ƒà0	€„ÈñxÿFù#øB ™éÝKjJ¢~»ÜœlÎ&âðJ*g‡<1÷(22Rªxl4€N}½W:�N÷pNN£au@Ô=mûã×’šš„¥UE¢#®1þ»åÓ·Km¼<ƒ[�º´¾'Ó¿x�±ÓÿT†Ñó6FFÆ›˜|¿öÝx¹ï0¦|Úÿ7†R¯ž}_¨‹_C/Ü\+³-à?~1Ÿ:ž,ݰƒ«×#135aëÞ#Ì3„º5Üøõï�¤¥¥3¼ß+å9ùùó0¯ÿ}¦
ݽÞC	~Ê6'?ÿ2™>²³³Ø¿c-®Õjðß:nFG0òËyOÄýºB\BZ�ŽÙ‹WÑÿå.tïà'éC”;þ~N$}ÜÏóï~Ê�ºüQ»^ú½à…wÃTvuãXÀ6†ñ#žu|رa)©×q4Íx¤å�‡Y6y@å!JÍ;Êe øØ(BCÎóíü-|7qA'àÓ´
ÙY™ô~gEÚåGE„’œϤ9«˜üI¿BËóš½ådg3ê�ÞxyV§¦[6î>ˆOO^ëÖQù†&,33ÖîØOÝn’ÄD¹~Ê2CCCÒR’Ø¿c->MÛàóLÛºÿ©cÞF››[è½·†M¤²«Û}ß›
Vü»ï9¹¹L9€ZîUå†
ñ”—?òädgÓû�QT÷ô¢Š[MîÞˆgúwkMUܤü!Ä](—ÐÙ“ñ¬ã£ÿ»VÝFœ=©d@¦fæ¸×ª_l›+çOâV£®þo·šù¿kÐáA0¸afj‚—guª9;²ë�Ò~ýΖ¬ÛŽg5Ò3³ˆŒ¾)©KHðóòÂ+ï<´ýò͇vÿ«T®Äˆ·zÊMâ>¹ººþÄ—?
215£º§2ª¥£s5NÚEUÂ8¹s9£žÀò‡‹‹Ëcoª(žâ²ByüPµë5%äb�þïËçOQ§~S}áH­Z-Ä˧—ÎæÏéŒò»	YX“„)™·<æœ%k˜ñé ¾þè]¼<ªñ�Z¥		~„Ü!Ämܸqã‰/ÜŽiT%ì‰-<®{$”Ó '�«2cÜ{T°¶#"ô
õ·¾å6¶öNÔôöe‡=124&##
s#êrCro{ÌÆukñõÏRÅ©Gƒ.bb,S,IáW
¿BjJ"û¶­á9ÿ~·]72,˜ èÜýu´Ú\}¡`ßö5¸TóÄ£„o`ïDvvÆÆ&¥þ]Ôê¹£˜6ØŸ¨ØxÖíØÏ€žÝîøXyóHâ),›˜ÝrG¢± MÊB”Õ(##ƒÈÈHÜÝO$‚³³3fffå¸#&þÌõ«—HINà½�§èßÿdòo…Öüé¬+ÚðîG_‰‘‘1ó¾‰#GÌIç³�¯áâèÀo“?Ño[¯–¿Ý€¯†¿ÃÁ“ÿgï¾ãkºÿ8Ž¿nnöÞ‘D!V{ÄV­R«KÑV‡¢ƒÖ¯­ÖlQEQJUÍji­ZµWì;vB"!‘=îýýqn®Œ;D|ž�‡G¸÷œsOÎùúžïû~¿ç{N�ž™Eß7:«tA¿Ø¬ÍÓ{NÇÓ:þ�[fF:'�î¡FH³».·oÇ„4nûPŸqüÐ.ÊûaimûPë‡Ø�•Z˜[Xæ?—®^#=#SZù�V/þ
ï
÷µ¬‘ÚsK%€,øsKÚwíÀÉ£{Q?têÛ­.ÓÿÞ‡ÚØ€÷:3{Õñ"ï§¥À“n³.lÿ ß¯"+;‡~ov’PŒìÜ@pýæú�lȱƒaT¬�…¥õo?òâLLLïù Ý¢\:wK+[ý,`¹R““¸qœ àrK GmLÿásý¹}ýý/qvõÈ·¾QTñ·&¨„¶?ŒÍdŒ(ùŠõëÅ™3gR½zuæÍ›Wè½yóæQ½zufΜYlŸ_¶\E}×s®‚ÿö«Tc¥Aóç̱„m\ΜŸ¾Â$-ŽàŠJ¥àë��•Ò¨Ê}J;€�µ•þcFF*Ѽ^Mìm¬©¬»ÑÝÅ	O7n%§Ÿ˜ïOJZ:¥ùø?Ö —–ÊöõKï¹Üúåóú3þ™÷×c~ÌxØÆå¤¦$êùÙ¾ÿ(+·ìz¨m^�½Îã¦?·TVf;6.£~h{Ö.��VwA_·l.Ñ‘çõ�ÔèÈó¨ÕƘ™[’šœÄ‰Ã»9º;KçO&6:€Œô4þýkkþ™EjÊí"Îã
æLƆpûV‚>§¦$³ì�i¬[>�C»7‘ž–ÊÒù“Ù²v1YY™løw'²lÚçXß:‰�µeþ²¶Ÿ~û“¨˜8ýk³—®E£Q×o&òïæ]¤¦¥³ã@8»gòü¥\¼¢LŸÀÏ–±q÷Aý:©iéüýßV¾™ô;“æ-!µ˜ë”ÒfÓª…ääܽ‡×–•$'%>Ôöwo^ÉÑ}ÛzÿNÝË•K…^¿Íßs&<ôvG
|CN~1{ØöǬŸ†�ž–ŠoEej|oß,­lôëç^_JjûÃØL‹£¯<
D”|ÅÒ´wï^ÈáÇIMMeË–-…¦Cܺu+|òÉ'Ì�;—ñãÇS·nݧz0Ú¾Ò‹È3‡èÝØ‹Jeë¶—oãøÙüóó·nX›V
kËñÎ�:‚«»7¶öÊqOM¹Í­„˜™[pñìqjÔiJGÝ3\4šNÙƒ‡WyÒR’qu÷ÂÔÌœ×ßSzèÒR“IŒ�ÃÜÒŠ+—ÏQ¥F=ý·¼É·9{âvÎ…fã)JîƒèœÜHJ¼É­„xùú£Õh¸|á4å*TæÅîïh—‚ʲ§/D’ià™P‰·“	?}žà*þ\ŽŽÍ7[Ϲȫ˜ããáÀÉó‘<ÁÁ¸8Øá­{ýyyá4N®¨ÕJUupçFªÔÂÕݛӿç….oóZŸÁ,ž=žþ_O&6ú;6,¡f½PÜ<}055#(¸!6¶ö¬\ô+_ïKLÔ¦�È ‘¿úÌ䤵êÈ•Kgþiw~ü}½~[UjÔÃÜÒ
K+ŒML
nˆ¹…™iü>é[ÚµjÍ[­ƒ111f✿y§Ë„Ÿ>ÏŽƒÇhD�/¾gÊ7¨ê_žÉó—òf‡Ö©‰¹~“E«7Ó®I]Ê•u'''‡†ÁA8ØÙvðfÿMÿ�8q‘7ídò�ÌX´’ŒÌ,ºµkFdt™ÙÙXÊu
€kW/¡ÉÉÁÃ[y–’V«åâÙ㸗õåtø>*U¡ÛÛ1ÖõèåÖC¦æXYÙ`lbŠ�ƒ3/t~Gÿü§gÂq÷*OĉCøW	Ö÷
eeeqâ �†ÊÕëêËëÝ¿ÔIáæõkxxû‘••ÉňãøWQž9uùü)¼|ý©Ý¨
ffwêŽø8®\>‡ežgÇ€2;؉#»)W¡2	ñqxùúë÷!)1ž¸kQT¨T€¸˜(Μ8@ĉƒ˜šYP®Be),%@ÛWzqáL8!�ÛÙcø°ÃªŸTûCÂ�xîгªŒ“=-¸Þsƒ‡Ñ³S9ÀÜJ¸ÁèÁ=©X“¨‹§©Tµ¯õÌù3áÌ7˜2žå¨Tµ¾ƒ˜4²?ãgoàzìUÆü¯Uƒ²üRW.Eðõ�àíÀÈ�¯3{•ò0¹i£âéS—2eùý§!L˜³	•‘ýö#ŽÎ®$ÄÇñëø/=c•ÁSó::œ};ÖÒ÷X¹xW.ä÷Ã9βù?óùw¿1kL?êín5™¤äTl­-9}!R¡Y¶a3­¤q­ªÌøk%¯Æ°ë¯©ddfñÎ×?àéæLNކœœ&~Õ�ˆ‹Q$¥¤²óÐqªúû>w(êRD¾é¥«ÕnLø�”ñ,Ç]ÞæäÑ=df¤“xó:®î^\¿¦4LLLqvõÀÜÂÿ*µôë7nÙ‘Æ­:¡ÑäðÞ+µÑjµ…Î}³v]9ut/ÉI‰¤§¦så"îe}161Á¿J°~œ±±‰~Û)É·ÈÉÎdÂÇݰ´(<¬T¥R1¼oŒŒTÄÆ'ðߎ}Tõ/oðw61VãUÆ…¬ìjUñàŸÿ¶ñr‹†ØZ[Ñ08ˆ¿Vo"=#“äÔ4’SÓ°07£}³zR¡èüðÕ;¨��1R«I¹�Ä��“�ÍÐþ]ª�_`u¼ýó¿^LY†‰©Ã?펣s²23¸zùm:õ¤MÇ·˜ñã`>üâ<¼üø¦ß+7ÄÃÛ�™ã¿ä«æááíÇúåó¸•p#µš9“‡òÅ÷³póð¹ë>fge1æË^LþcÇìà»Ï{0yávÊxø0ö«·™¶h7kþ™…wùJ„¶ëÆšf±cÃ2üƒjqþÔQÔÆÆú°4ydj„4cÅŸ¿p1â“lÃÖÞ‰ß&áö­xœ]=™7u$_ý0Ÿ˜+ÉÎÊâø¡�ØÚ;K*!œÜ¨Õ U‘ï?Ê=¥O¢ý!áGHêÖ­KXX3gÎäÇ$44”¡C‡æ[føðáÄÄÄ0hÐ úôéóÔ„9éTáD±„Ÿ'íY:þûv¬#¸^sº½=­FÃ]CèÚë3ÒRn3äÇú§bìÛ¾†Æ­:Ññõ¾deeòf늷�™™Î—cçʳ["N" ¨6ï~:Ь¬L¢#ÏÅÙ“‡ò5’
©R³>¿Oþ€SG÷Q­V#.Ÿ?IøþíT«Ó/¢0#€	·8s!Šu¿�à�¯~ÐogÞòuÌ17þÛ±�S•ýÛ´û A}ùß{¯Ðã‹ï¹{�¶�Cذóz¼ò\VPfædfܪQ­Næþ<œØ˜HZ¾ø:§�íçàî�T®~�7O¥Qjd¤­­VƒJuçþ�¬Ì¿ÛŽv�߯Ë7+;®_»rÏ{8<¹Š�»«ÁðàéæŒ‘‘´¼Ü]Ù}䤌P‘;eSVvÑ�‡S.ã`gíä^m߂Դt÷y�Åk·2xÜ²²³™?îk쬭žë‹Ú•KgIKMfèO‹5èMΟ>Š�_e’o'ÒgàhÜ<òß�w1âff2€!};ÜvZj2}>�³›Ž®ì[GÇ×ûÒ¾ë»hµZ¢£”!™»·®Î×cmˆµ­=¦fܼq�ð;m×�ð;Hò‹Ç¿rp¡å×-›Ë˜™k°°´få¢_9°s=a›–ób÷÷m×�¤ÄxzµW¦PNŒ�#êâi†Oþ€¥ó'³?l�[uÂÜÂ’Wz�Ð3¢¤O¨#áG<‹Šõ >}úpôèQÞzë­Bï½õÖ[=zô©5¾G|ú*éi…ÂÏœ¥ÿ±týö‡Úæ­ä”BÝÌOSI>þ¹NÙ�_ òÌ•‘žÞ¸tîPx¼t®ó§Ãõã£MLL)[Îßàrkêÿîêd愯˜4¢Û×-!9)‘ógÂ﹟–ÖØÙ;q!âÆ&&ÔiÔ–ðý;?°ƒ6µ+ä»8:AÕ€;ßîW¼sÿ…¨ýð¶ ŠwÕÛöeÛþ£ô<šžƒG£Õh¸óÜWPÞå+såÎÿ)¿@¢£ÎsîäÊûW%(¸!ý6ŽjµϲdçàBJòíú¼ÓÇàíW‰¶¯ô¢RÕ:ùî¿(¸=+²³²”	¸ÊÝ:O]¸Ll¼r?Ñ–½Gh^O)›Aþ¾œ8wI‚s9;Ø“”’ªÿw‡æ
qwqd@�WÐã>|íeímQ©Œx³C+–L�Ÿ·'{ŸxîË̉#»©˜ÿ9,'ŽìÀÞɵPø8&_ÿ ý¿‹z‹�£Înyê”Ëʹ[ý'£½Áºes‰¹r‘÷Q§T­Õˆð;8sâ ]{Jø¥N)Xž“o'êë!€
yž3“·>´µwÂѹ LÊrýÚFîÉèÁ=9}l?I‰ò|º’ oû#¯ÿ–Î)t¯ëý†Ÿ§Õþ�ð#$ÁÜܼÐd¾¾¾Ou².½>ÁÄÔsÒ	⸾ççæ­$o§<Ô6OŸ�dâì¿KÔ	.©Ç?W¥ªu¸xF™IK«Ñu^ÿ ·Ü!V¯§NBzZ*‘NÞ¸�iâÍëœ:º—A#¥Gß!˜˜šr¿M¨V»1KP͆T«ÝˆC{6‘{™eó/W=°B¾Ññˆú¿W©X޳—®pðÄ�ÆuƒšAÔ­Èܱ_2wì—,øq�kWÃÒÂŒŒ¬¬ç¶‚òðò#;3SßT©TøW	ÆÕÝ•‘Õë4!êâƒÓÌÖiÆÉ#»ù¦_'Î�>r_ŸW¹Z7âbñÙk|Ó¯>y†µêð&?|ý†~@ëo2ú“L÷õ=·àëMïÿ�᥾$.>�6�Bèñrk>þn
¯ôû–ËWcõË7®]�Ã'ÏÒ©ß7ì?všWÛ7çÔùËt0”žƒGÓí“aô6�W?Aï/Çw#�Ðz5Ÿû‹Z¥ªu¸�ç9,çOÑ™b\T�R­.yž½qâ á*Ã)wÙ‚©>�·?‰{YßB÷\¥z�&„mXŽ•µn>$܈åȾ­T¯Ó$ßrÖ6öh´ÒÓ”P|!âXž2{ç2ñ×cˆ¿®|qR¥f}ìœùrì\¾;—¯~˜¯ŸÑÔÌœœì,ÄÓm”të&)·8ü<­ö‡„ñ,+•÷…m\�©™!�Û²õ¿¿‰ºp†}‡så"a—Óµ×§l^½ˆª•2;Ï‘ðp¦þ±3S\í±·Un0�Ž‹gÚÂ圾I—6M9}!’aý{°yÏaþZ½	{[kÞéò¾Þüòç
Ÿül�ÿ“À“’±·½3�r«µhRƒôŒýL“®Nöü=iX¾íMüª©ié¤edâd¯LÕ>sÔçÄ'&¡Vaoc-
J/¡JeÄäQ02R“ž–JÅÊ5ɹËòå*bïè¨�o��“…Je„é=꡼*TªÎœŸ‡cemË™qrq¿¯õ«ÕåûÏ{ðæJˆ.P�£û·á¤›ö8¯–/½ÁÈÏ^£r�º?´S3嬯­;1idN…ï#>.Gç2›˜bkï„�_ ã¾~—Ê5êw-ŠÊÕëQ·I;ªÕnÌÏߊP-Ú½Ò[
ÍShT¨TS35§Ã÷±ì�©˜˜šaï誟°Å,î 3N+±í	?â™o_ZÝχ¥ÕÓc‡U*ËvÅ>ðz'ïâ¿es8bÃ?éNB|,ãçldýòydffðòkòQ·ºlšó#fª,ZôüŒùã¾&''‡îŸçãž]x»s;zM‡æ
iݨ6_Œ›Á•kq¬œ>š}á§™<	“¿îOâí>6�u³Æ±÷è)~]´’YßñÄN [ƒNçñIJ ½×9Ôjµ;†»Wy\Ü<å¦ò„q”-§Üß“�•Ååó'ñË3ôãê峨;¹ò¿÷Ú3ea˜ò-رýTªZ‡Ôä$â¯_ÃËWwíê%e›ƒ3éi)Ù»Ww/lìQãè\†KçNRƳœþ92ådgq*|?•kÔÅÇ(šôK»±²´ÀÃUyŽÃ±ˆø—óÂÌT	5ŽŸ!;'Gb¼Ü]ÈÊÊæÒÕk\ˆŠfÇ�pF}ú®þ3".Fq,â"U*–Ó?7H£ÑrâÜE,ÍÍñóö(1•‡[ƒNO¤|h49D?H¥j!%æw—‡ÜÞ�•sÖþ]žHù(Ê¥s'ÈÉÎÖ×Z­¶Ðý~'Q1°*Ýg¯Ç^ÅŠ¾z›þC&ãR¦,ÏÇÃË3sÎ?@@�ò%DR¢òm½»Wy4šŽ•ŠòþU¹•pƒ²å*rýÚ}ýR”³'ãî勵�=7o\#ùö-¼}ˆ�¾Œ™¹¥~ºØèËD^8C`µnÄ]¥\…;Cõ¢.F`ecËèÁ=7k�þõÄø8ÂìÀÕÝÿ ZúÙ0c¢.�’œD…ÀO¥ŒtjàöXÊGQ×?µZ}Ï)ζ�ñ¨í�¾Ýê1~ÎFT*ŸõlÁ×ãæ“““ÃðOºÓ¥çǼ׹C÷-±í�û	?*Çn÷l›ùœ¢ÇÔþ¢Èº£TöÕaò¨df¤“™‘NµÚ�9{âG÷o§û;ƒ°$S20%“°ÇhR�Š>JûiHu}#õÔùËÌó?T*o¼Ô’±3°bSvÖV,X¹Qi¬§¥~æ‚§GZǼ[YÛaemw§Â51Ñ7d2ÒÓ˜9á+<½ý8¼g3ÍÚvÕ/—;ÔÅÒÚ6ßMóÎ$fnaE½fííGîlH‰7¯gcïˆÚØ„ àw¿åò�}+8«Wí €BŸ±yÏaö†ŸD«Ñ²mÿQ¾ÿ,ÿ=Xþ¾^øûz¸H¨Šœ1ìy`d¤–ðóŒ…+gÍSß�¼á ·ž)8ÙIî´Ós§ŽÀÞÑ…ˆã±¶uÀ¥Œòÿ;÷þåÚr§ÖÖÞQ?m¿‘‘šê!MïÔvúm$'%’�•™ï³-¬l03· bå;½ˆŽÎeò…¥‚3ɹyøè_³ÖõÄÇE³dÞdœ\Ý9¸{­:¼™o{'Wš´é\èø¸{•î:%::ºD¶?ò
?Fõ�¦xú(_ôUiŠ=	”ɺXbÛ�³ç'&FîsOO©@Æ&&xxùñß²¹TªBåê!Þ·•ȧ©\Á—*œÐ­��£ÁX­Î÷­€‘ÚH÷õ’r+Izæ�‹WFfå½i¬\áã鯙Ò(zÌÌ-èðêûÄÅDѺc�|AéqXüûx4šüßvîù	.nž�ÜømT+K3rrrüÞë¨�Œä„>C$ü<áça´ïò.—Î� AèKúàò¸l^ý—~†¸\
[v¤jpÃGÞ¶£‹;-;¼AB|,í»ö)²ûyãæötp·öGÁP®ÉÉÎ÷Ì([u:$–ØöÇãöö´Î‘¥6�rÃú’y“8bþU‚™6fAÕ‚	R�Ä„;76©S�ï¦Ïçrt,99¶ì9LUÿò¨�Œ®âϯ‹VòBÓº,X±Aÿ¼�.m›2yÞ^kßG{ÎGFcafŠoÙ2\¹v�´ô,Ìͤt#ïò•ð._©X¶ýÞ 1ÅÖø551¡^uyîFI�‘ž†V«ÅÜÂ’”ä[˜[Xz€eVV&ÙY™XXZ?ÒùÏÌÊ"#3+ß}>Å%áÖmìl¬Ñ¢%5-ý‰|æ³~@¹g,w†·Ç­ÃkÛ~«tÃî ªü§.Áí�JÕB
=s¬Z�&ÌŸþ±Ñ—q͹¾=aÔóï\"ÛrÏ�(mJí×ϵ´ÄÇ/�Àj!˜[XQ¿QsÞlY]~‚«TÄX­ÆÆÊ’1ßcà˜i�2›wº¾€»‹2¬aüÿú¢Ñjø~Æ4¬UowåÛŠºÕy³C+>ÿa:/ô̸Y‘“£ÁÅÑž.m›ÒgÈ8꺧Eé ßü—>ë–Íå¿¥ÊD‡}Ä¥³…§�>°s=óùî‘ÏÿÆ]õË|åï»r,¢ø†¬tîÿ-ñ‰·Ðähxí³‘dßã>	?¢4òôô,1í�:�ZÓ¨åËú÷+V	F­6ÆÒʆ÷Žá÷1ý˜>et‰mWøñðð�‚*žšR9	B¡‹3)TáÆ<Øàè¸x\ìHIK竉3iÓ(„—[4,Q'°4L‚ áçÙô¤&AÈ•�•…±nÖ¼¼r²³Pç=++“œì,Ì-î<T«Ñ�šz[?d2w˜£‘‘šQƒÞäµw?Çǯ2jccý·­»·®âÒÁuLøÚ}ÿB�–ôŒŒ|E]³mÛöeì ÷þó\ªT,G—6Móïsv&ÆjƒÛLNMÃÚÒâ®ËefeajbBó·>eÑOCqq´gÂìÅ”-ãB·v¡O<ü¨»Iý!ŠTZ'Ax\×—’ÒþxØð#“ ˆNk\ÚÇ
?ç._á“ï§`¬Vób³ú´k"Eæ9#áçéÚ¾n	;7ÿË�¸hŒTFÔnØJ#ñÖÿþfã¿ Õj	¨Z›|�ÊȈÉ#û“”x�&‡Š•kòZŸÁ,[ð3G÷oÃÔ̵±	ƒ¿ÿ�µK~''GC‡Wß`õß¿q5R¹Oãµ>_P#¤ÎÄsƒÛ¤gdÒ²÷@6Ï�€©‰	IÉ©´ygÛÿ˜’/ŒlØy€©,ÇÒÂ*†öë™oR‹KW¯±vû^v:ÆÊͻֿ¦&Æüøûbbââ155áÓž]¨Y¹"?Íù‡³—¯p>*šæŽ×j IDAT†ÁA|ðj~øí/¢bâ06VÓ÷õŽ4¨Y…›‰I™4‹KW®akmEjz†þó^lVŸ/'Ì,¶$=?BÏõ¥$´?dØ›(ÍJu²&™Êœ|¨ðФNušÔ©^rÃ�sŽ”`	?¥ZŽ&‡‹g�óóŸ;161á›�^¡Q«Ž¸¹ûð÷œ‰ŒŸ½33sÆ|Ù‹SÇöáâV–¸˜(FM[žo;þ]À”…;òõådçä›ìÂÂʆ±3×p=ö*£¿x‹—Bü¸É
ÌÍLi\«*«·î¡S«Æ,]¿�vMêê‰	­WSÿ¬¦mûŽòǪ�ïç9+å<ËЮIÝ|=@æ̡uÃÚ´kR—«±×$ü~�”÷ÒÏxU»akîÜH�¾Cppvc`ïVÔoö"­_~[{'Z¼øŸ¼Jp½æ´éØo¿BŸS«~\Ü<1Öf`rãh¾÷{vjË—fÒ©UcþX¹�_G*´�«±7˜8ûo®Ý¸‰‘‘ЏøÄ{þ~«¶îæ|T4­ÞÀ¡“gIÏPf}jQ/X?SàÊ-»qwqdͶ=œ¹ÉÍÄ$¶í;§½”©àƒ*úâæä ß¶™©	ihµÚB7`Ký#„\_$ü	@¥ˆ
·©ÌI	?BÂO)=&ßßk„4ÃÕÝ‹øë×ô¯ßˆ½Š«»2Ôlàˆ$ÆÇ±iõŸLÙŸoÆ/¤ó[óB—w8¸k#>{•©‹vøe{eµ‘¤§ÜÂÑÎ&ßû•Ê{“““Ã’uÛ±³±Æ·¬{¡mLš·„—š7 Eý`v>Áÿ~üµÐ2êãÞ}<Ü1 ·Áíå�ÁÍÇÃ�Ïzw£Z@þç·xº9s=ž�HÏÈ$!)YÿÞ­älm$ü!×	?B”æ$áGHø)].Fcݲ¹XÙØsdïVzõ†¹…%Y™¬Z<g7O¶­[Â÷ÓÿåÊ¥³\:w¿@ì�\±´²!#=�-kX½.vŽÎXÙØ›V-¤rS6ØDpL
L¸Ð³c[>3•)ß|lp_-ÍÍØì4–fÌøë_ƒËT«äÇ¿›vâáêL�À
¼Ú¾9C&΢ï/ceiÁ‰ˆ‹¼Ñ¡U¡õ^kßœá?Ïa@�Î8ØÙpøäYzvjC›F!Œ�¹kKVo݃Z}grψ‹QTõ÷•úG¹¾Hø¢´ [’䔄!á§	m×�ÌÌn\8ɘ_W뇽}7mÛ×/%òü)FÏX‰ƒ“*•Š+—#8²w~�ÕéóÙhŒ��IOOeå¢_)ãYŽA#f`d¤& jmý,EMZ¿BuOömYŽ¿oY¾|ÿ
Êyanfªß—Ƶ«aceI»&u
îëÀÞÝøcåFÖnßÇ wºsàø*–óÂD¨Ú5ÁÂÌ”}᧨àãI׶ͨX‘Åk6“šžA³�4Vš›«MãÊ{³hÍ“nÓ°–òÜ—¦!Õq´·aÑêÍ´¨L ŸVºè®ÚDŸn/Iý#„\_$ü‘G©™ûy
?÷3Õ䣞“il%ü<¸'1
ö–µ‹‰ºx†·ú~óÔÏÿ¥«×˜¶p9}ÊÒ§Û‹%þü¤¥gðÕÄߘøåGO%üÈ4Øânž§i°ŸÇð#Ó`‹®tLƒmÇ-*qZz~„„ŸRÆÓÛKëqþ7ï9LEŸ²ôîÜî™8væfO-ü!žßð#ijà™@vÜ"�SQ:/ÐÒø�ðó<ó¯R«Äœÿ·Ÿ‘à#õ�r}‘ð#D)@~ž
º¡BHãDê!¤~‘ð#„ ‡eO"•8-áçP\÷(‰§ëqN­,�©„xœ¢££ŸÚgKø¹?111RP… á@œ‘ð#„�ð#õ�…¸¹=�‘~Jþ9ÀHÂ�4>„�ð#¤þBÂ�€J Gnʰ7!„„©„¸+OOO©_Jxøñðð�‚*$ÝOø	à*Jçý$Òøxöåää0cÆ9~¤þâ)»víšÔ/%8ü<és$Ä3€œˆ—ð#J¼Ñ£Góí·ßrþüy9~¤þBê	?B”ÔÿÏBøñ'BÂ�(ј7oqqqôíÛ—uëÖÉA‘ƉÔ?B”pò˜!$•H~ijàÃ?äìÙ³ìÙ³‡Õ«WÓ¾}{90~¤þ¢„’G4!¨Ä’ð#ž…‹hHHÎÎÎ,Z´ˆ�>úH.¬~¤þB!$	i|”N*•ŠÏ>ûŒÓ§O³qãF†
&EÂ�Ô?B!D	e$‡@BHø‘úG!„�$¤ñ!„„©„B	@BBHø‘úG!„x6É=@Òø�H«Õ2qâD.\¸@||<Æ
£víÚ¼øâ‹rp$üHý#„B”0Ò$�ñˆT*ûöícêԩܸqƒ‰'¢R©äÀHø‘úG!„(�¤Hâ1øå—_8tègÏž¥^½zò 	?RÿñEGG³lÙ2ÆŽ@pp0cÆŒ¡uëÖúeÖ®]ËСCÙ·oƒ
ÂÄĄѣGP³fMÆ�GË–-õë¬Y³†ádzwï^ˆ™™ßÿ=Õ«Wgâĉ4oÞ\¿ÎªU«øî»ïؽ{7Ÿ~ú)VVVŒ5
€jÕª1iÒ$BCCõë¬\¹’Ñ£G³k×.>þøclmm9r$AAAL�:•¦M›ê×Y±bãÆ�#,,€àààÀðáèR¥
¿üòMš4ѯ³lÙ2&L˜ÀŽ;èׯ...:€ÀÀ@fΜI£F�ôë,]º”I“&±mÛ6>úè#ÜÜÜøöÛo¨T©³fÍ¢aÆúuþùç¦N�Ê–-[åÙy111RPÅS£´ºŸK[\Ͼ=yJHÂOÉ?€DI@⹿¾QdÝ!÷•ÒÆ‡xòÔjõs~ž	?Rÿ!„�J�4>„�ð#õ�B!HHãC	?Rÿ!„€„4>„�ð#õ�BñŒz,“ Èa,õe)Bʇ�ò!¤|ˆg¨|q׺C*!„B!Äs€dœB!„â¹!H!„B!H!„B!$	!„B!„ !„B!„�$„B!„€„B!„B�B!„BHB!„B	@B!„B!H!„B!$	!„B!$	!„B!„ !„B!„�$„B!„€„B!„B�B!„BHB!„B	@B!„B!H!„B!$	!„B!„ !„B!„ !„B!„�$
2jåŸãcà¥;°N% ª!„Bñ´i‹! ¼	ü¬‹�€ïS
yæºýz…ß_­;ÿjù¯.„B!Ùçq @`¿n›ZàpÈÔý;øå	ÿ’ÍtŸýÓs€¢tûìiབྷóœ¯�EÚÛ@`ý ÷tûÚWþ¿!„BHzœCà‚€CºFñ]cÛ¨‚ÒQØ´{ÆÚ5ÀxãÚç­ºŸM
¼×Èb‹x?X|ɺ×>Óƒ#òH!„BÀïa”{ˆóæ¥;o�Ê\wü©„B!J¯Ç1®£n;°Ñ›ÛXþL˜r‡b% •³2°|°pB†–ª['˜ D¹F£ôZ侟˜çO‹<
è¿tŸ©ÍóçÐÁÀç—Ñ­¿Ø@Lªß1y¶µ›;÷ÏTؤåYþ,жˆ�™û#€«ºå÷Ýãûé–‹(ðº=JïÏxà]Ý2Á–Y£{½Mž×&ëö£†�@ýpK·N°QwÌŠWØ€2ÄN\G®XQ·¼¡!“ŽÀy>G«dXnqžò‘Zàüç†Æ²À?ºã�÷üG¢B!„¥(û<®‰è~ÎF¹ßçAÌC™4á$ÊÄ	É@à”‰Zºð’·Ñî‰2œîº.lTD’ö)p¥[v£.(õA¹7iUží\Ðýôš¢Ü¯”€r/ÓûÀ
]#øß�|;
÷Yê^­(¿é@o”á+Q†	jò¬ÓX§�SQzbÜPîWY£û�þ4ðcQ†..£ô^ÜÍyàŠî8¹ëÂÜéñÙ¦( ƒ;¤û»Z·�ÙÀNûQ°üÌЩãÀ, xUw®õlÙê›—®lFé}yeÆ8;!Ø¥7јì@é=z˜¦6ÿÓ-û·n;¡ôpíɳ�Ü0¶Lw,—ê~ÇkºÏhÍó=ÓŸB!DéMA�a+tÛyý×k¤[ïx�²J¯Œ¡ós'T˜Vàõ
Üé=Ê«wŸÁ’ü½F¹¼uÛ:Yàõ¢&Aø]÷úe”™Õr©Pz€´@û<¯«�p”ž‰*‚A”.´XøŒX”ž‹1_·îky^�Òó‘»¿×tç2Wîô`åeh„Üpw垯\ÆÀjÝò{€†ë^Vàu”­.åµP÷z§¯«u*SôrÝm„rº÷¶qÌdšx!„BˆR–}W/wx×¥\ï]ÝÏQ(÷•äÒCt�Ó>E„¶!^;‡ÒÓa�r/ÑýJE~UP$JR ö¼›qB˜¥—‡
óv(½3QzžòJBúå	Ô7ðtáèAä6òóNtÐTXr÷w;ùïjZ`Ý»yGöÆ Ì—+¥—Î�Þ@Š.ˆå•L1°¼—.ÀmGé¹É+¥ÑÃC
IÖ�{3ݾ¤‘:B!„¢ty\Cànê~Ú?àz¹Ï�Ùià½s(CµÊ£|»Ÿ“ç½+y>3¯�º0æ†2÷ýò>BéÑðæÎð6Û<Ïm…±_èö+Wn¯�ð‰�ut?QîÊëaf_Û¢ûÙL÷Ó垟ó,³è
TÓ}F³ëÞMn¸Ûkà½C(Ãáò2GéÅ:^ 0å]§ Êy‚‰¡cæšç˜Ý�(½SQzî£UÜIÑ<!„B	@D ôø=àz¹Ã¸bŠx?eÈ™ùg•K.bùÜ{…ä¡—!º†¿
eÕZ”¡Z9(CÖj“Ú½$ßç~åÞ_Òè\ĶÒu© ëqŽ. «@™x –n¶@ ôü„£QÌ." ”ûŒ¡8ïåNR�wh ³î˜5›ž¡ß1÷˜5B¹¯ª¨cæøÇåm”{ƒú¡·¨;ÿ3Q†æ¥J5!„BQz<®!pgt??àz¹¡Æ­ˆ÷Ýt
ð¸b<?ë‚@}”aiý�ot�ßÈbüÜÜÐ×eˆ]Q†?ÆÏÜš'à4Ó“íyÞÏ×¨‰ÒK´e˜Úýþ>E
?,8Uuî4Ún÷T†>cü=ŽÙ+pLP&•ðÕýÞSQî#ú\‚„B!„ Bæ¡LIÜ…ÂÓ(”wšì³ºŸµ‹h{ ÜWô(Ñrg¥3t�eƶ3r¥¾�ßåQœÒýlöÏwÞÔåÞ£ø<ïk�0]�
-°Î½äΪWÃÀ{•)Ü‹–�2̱|a'ô1³»�ÿ‚û³
¥'¨†n½NÈDB!„€ˆE™”@,*"мHþûYæê~~Náç}«Û¿¹�¸o‘w	Y9(Cœ4Â?âáÌy¿VéÂÝ;(Ãð14Õôã@ítán›�e¶£»ûàPîylà\)b�I(“Œ-ð{¡ÌÞVД‰)ê¢]3ÄŒü³ÐÝíüÛbxÀL”¡t©ÈDB!„¥Êãl\ÿ‚r#|`°eHÕ5]ƒ6å¾�syÖÙÈ�gílA™^9徘.(V�ðˆûuå~–ºÏÛ§kà.ÐíË2”ÙÈ–sP&Xxè�2@�b:ö)(3Ü­Ö…ŽŸuû–Š2éB}”¡\Þ<Ø„w“{P¹»„›Ü!q~(3¤í¼Ïmï×…ßîºãÀAݺÍP¢zå~²ê(ÏzU·¹ûue¢ƒuçT£_
uen¾îõ”É)ºêÂÑ$©"„B!JícÞ^(p@×øÕæùsø�Â÷|˜ L�œ‘gÙ`	†ofϤð³yrýª[¿Z�×Qf÷ŠÖm[´Õ½g§ky÷õÊ0°iºç
w¯ç6×D÷ÞÞ«ŒÒ£)°7tÛ4¿Ïϸ_óò|†k¡8Y÷þ®"¶aè9@¹ëNÕ§ÜÏ8ŽòŒ¦Ó~(½EãtË%ê‚K/]hÒßXÇå±YŽY
ÊMËX¾¡îÇå9Îåu�i?Êð·¼ÛIF™RÛXª!„BˆÒ—}´Å´q5ÊвFºÐ ºÇòÆ(=Eux°Y×/]èqJ'Ãeâ�†º¼ú.X–ºãúÛxWW6ß½Ë2&ºpÛ¥÷Ñì!?Ëå™L¡(=@–R7!„BH¢8
-ö(ÃÓtÁT!„BˆÇ€dˆ�xÚÂP†4îG™’:å>Ÿ2À(”{–„B!„x|)H�xЦ¢Ü›“{ÿM6ʃu{È¡B!„Å‘}$‰’ÀåÞ'39B!„B�B!„BD±•�Ø]ˤ|Ü…k¥¬gzÿåú"í�â,ÆÅøûiïã=•®’[¸„”!„~„´?J[û£¸�ö>‚�ª$!)\BʇB	?BÚ¥¯ýQHkà§¶@ð1„TR¸¤q+•�”!„~„´?ž¥T0ôh��”÷O‰
AR¸„”!„~„´?Joûãq ‚áG“çB�Qž?%"IáR>„BHøÒþ(Ýí�Ç5
vÞð£r€lÝöM3À\÷Ó4Ïçæ$ý09�FÃO#ðî+u¸•pC
×sÔ¸Õj5ü9ëGF}Þ“[‰ñO­,HùâÙ®;„x­–gýIÏÏGŸxËà2ç._¥|hw&Ïû­öÞ“‹i4Œü‰:¯¼Ë�„[~ž÷2öíiÓ¾=æð£MKIfÿÎõê½Û×™F^qpr5X­l�¨X� šõ©Û¤^å*ÊI(á22³Øsä8›w"ìà1ìm¬¨V©MêT§YÝš¨T†˜¨€ZUðvwÃÂÌì¾?oÆ_+øvòœ|¯Õ­À²_¾bCY$üÜ¿ä¤DöïÜÀþ°õDœ:‚w9j7lE½¦mqt.S"÷YÚ%;éƒPjr’ö—qÿS‡mX¢ªÚ�&­;qþÌ1Vü5ƒÚ
[U﨤p•þÆm^^닽£+dž1eT®]½Äkï~ŽJ¥"¸^h¾oL¤òyþÊÇËoôÃÑÉ
S33ž¡Ç†‰§¤`Ýq/1QûU†N\˜/å­£]Êpõò9þžýWþÉèé+.+J†Ô´t¾Ÿ>Ÿ™¯¡¼§�Z7A¥RqàØiªø~rËOh½à{”±¢ßë÷ÆË¸99`ff*᧺•pƒI#?æðž�Ômö/¼Ò‹èÈóÌýyëWÌç£gQƳœ„Ÿç¨ýñ¸�`×ÖÕFa–¨zöÆKÝú 66F«Õ’xó:VÖ¶dgeq`×Fvl\ÎÕËç®ßœfmºà]>Àà†5
ß}Ñ“«—Ï1î·5ØØ9 ÑäðÕ‡�¸}+�~[�…¥5“¿û{G†¾Èæµsæø‚ë†Ò¦cÎ�g×–UÄFGÒºÃ4kÛcSêiÂðÝd<Ý\èܦ)s–®%ì@8­ÕáíÎíqwu`ïÑ“üóßVö>AåŠå­Lûfõ±±²”ÂõšµíŒoÅ Z¿ü_¼Ûž�›VòòkèÏãñû?k-6vŽ…ÖÍÊÌPÊφåDG] n“¶´hÿ*®î^Rù”’òѼ]·Bõ�F£aòwŸàæáMËö¯²úŸß9ytßÿ²Œ›×¯±/l=gŽäÒ¹“ץŋ¯âí _ïAë†']ÖÄ£É=Ϲu‡�ƒ3'�îeëÿpâðÊU¬Lp½Pê7kOB|Îú‘”¤x¾x§
_�[@�<_ÒåÖQ�K+V,œJB|,N®w½†;¸“QƒzЮKoÞúð+T*#Vü9�?gŽãão'Ó ôE9YÅdͶ=Ìü{
½:µáË÷ßÄÞÖZ_6²²sôÿä»Éx{¸ñjû–üþÏjö=É’©ßñùØ©ì>|œµ³Æãì`GBÒmæ/_Ǫ-»hR§:Íë×ÂÖÚð5¿[»æ”÷.T&]¿²²Ù¸ëË7îàÜå«4¯L—6Í(ïÍÌE+=}kg�# ¼7‘ѱ´}{ kgýˆ�g".FÑöíA|Ó¯'ƒ¿n%'ý~¨Z-Û7,çðž�ô0’ö]ÞÖ·OkÖeòÈ�Yõ÷,Þî?Œ?fŽeբ߸á·5øøUàBÄ1¾|ÿe^éÑ�n½?%;+³ÈkCQתï¦.)´_q1Q¯]^åüÙù[Þ\¸‚¿&
#¤Z ûÃOÑýãaô«3Ÿöî&í�Gô؆œegej·¬ùÛÈÎÉ�F-_Fml¬ÿf%÷[3­VËú0öË^h5¶èÀþ°
ý¸;Q#yVüñ3c‡¼‡�ƒVÖv,™÷ï½R‹5KfSÖ§7ãc™9ákN…ïÏ׸ýsåFú›€ƒ�
UÊ3yþ2Æýö'�†¨˜XzþžC'"èúB(ff,[¿cc)\�ÚhÉÉ!'Gù=ŒŒŒî«Û°r!?|õ6jc¶èÀ®-«™0ì#’oJø)eåÃ�M«1~h_n^'´]WŒŒÔ¤¦ÜfãÊ…¸º—¥n“¶ü·t.ㆼŸ¯L
®Š«‡7{¶ÿÇí¤jÕo¡듇ôëŸ>v€šu›Ü×µÁеªPýRĵ+ðÖ:Ú4
`çÁchµZ´Z-»�œ ´^Mi<Ž}}\;%å67®Gcïà„�ƒ³Á…’ãYúÇ4*×lDÿ¯'baiMpÝP¾ú°#ÛÖ/å�÷?òŽôûr<5ê4áêås~¿&&¦øz".eÊâëÄè/z`ue=Ôj£_'&>‘Iß|LÓ�ÜNNåÈÉsì?Erj™Y¤gdѨVUÞíö–æfÜNIÅÜÌT
×CÚúßíÞÂþ�ˆ½z�ÞFbnauÏKsËOÖ�ðõDLLͨV»ßöëÊÙS‡õ—„Ÿg»|l^»Xß -ëS�àúÍõïÝNˆçí~C©Ú^?¤ÅÇ/�gý‡±‰Òȱ±µgΔ¡Üˆ»Šµ­ýÕ
ÑW.RµVÃ'VÖD15x23ÈÊH§j­F¼Ôí]ÌÌ-IM¹�©™9U´>*•ªPoMÜÍD–®ÛFýê�Lþæcl­­hX«*¯}2Üàç/^»Y?®‚OYš×/<œ.>1‰i,¥QÍÊLüº?Ö–„Ö
¦ã‡_±tý6ÞîÒï2N:ÁÍêqâìE:´lDzF'Ï]¦Q­j<~ϲöTð-#'ý¤¥&“œ”ˆµ­=¶F–˜™[R®Beömÿ�”ä$ÊU¨Œ»—/»·¬¦YÛ.h5vn^‰O…Êx—¯tÏkCͺ¡¯U†z‹ºv]�»A%?oê׬Š�;x§«²�èY¹B¡ÞFi<ݺƫ2�µá°·ã¹�O‹ºana¥»¸xãàìFø�0^{g*£‡ï”²wrÇÓÛO)HöŽXÛØáR¦,N.î8:+�ªÜîð\îNöT,W+Ksü|<9s!’œ
>eèÿVg¦Ì[¾ðS¼×½ƒÁÊM
×ýû÷ÏiXÙ:Q³n3:t�Ú�Z¡R©î€n%(ågçÆeìܸ,ß{	ñ×%ü”’ò±â�Ÿõï5`D¾dimK@ÕÚùÆó_ˆ8ÎöõK9º;1QÉÌH ''ç�놜ì¬'VÖDñ)ãáCç·ú³dÞN…ï£C÷÷ò•£{éðZ_œ\܉¿ÃKç²vé\|ÊW"éÖÍ{^Ã\Üôx¹�¾Þ°µ¶¤vÕ€"ïJLºMRr*�ëTÓu÷ñ,ƒ“ƒ�Áåþc…þï#ô*"Ý">á6Ý^h�•…†½=Ýpsv ì@8ŸöîNýšAlÞ}�þoufËžC¼ÓõE²³sX´fÝÛ7'ìèAZ4©Š£ƒµœôÇQX°´²¡ùÝY<ç'â¢#ÉÎÉæÜÉÃô0sKâb¢îëÚ`èZUÐÝ®]f¦¦toß‚OFMæô…HÔj#.FÅðý ÷h¢i<�dai­µ´²áö­n'%¾YTûh3—&'‡ô´”B¯«ML0Ñ�ßÏ
aV6vú»k¨Â
nÏÄD�©É�.rcµQž÷Œù¸WWÚ6©Ëò
;8z*ukòˈAØÛXKáz†gqº�Œ­+@5뵤FHÓ|ï¹=†û2$ü”“l/òž@µ‰	Æy†ŸÆ_¿Æ¨A=¨X¹C'þ‰ƒ“+ëV,`úØ�T7¼HwYÅüÿÁÄ„®½>¦n“¶ìذœ©£X£.ƒFü‚µ�ý=×ϽÀË7€©ßLã–/ãUÎÿž×°Üû^s%ݺ‰V«½kCH<+Kìm­‰‰‹'>!	wW'æÿø
ié´{g�Ák¾ñ}cÏûe®Ñ]Îßö“îý­ü=Ú?jµ�kWgãΜ<{‰KWc(ïåAVv6§/\æjúY.^ŽcÈ ®÷5d\ä	5–ÖØÚ;qýÚ’oââæ©/#=•KçNbãिW½j­†ü1cgO%;+3sªÔ¬ÿ@׆‚תB�¸Àµ«½Ó9¬XÇÀ±ÓõËÔ­ˆ“ƒ
»ÃÌÄs3êU¯,í�€T&¦fªF-:hü2ÊhØzZ¾ôºþ?iVfÆ&¦Ø;ºààìÆñÃ{HOKÁÂҚث‘$܈¥Q‹©
ö˜[X’|û©)·±±s þF,·â±´*ümLQ™ì"ü!~±´ôÔj#ý|¨TÞ;†N™CäÕXì+YKáz‚윱qpÂÜÂ’Ö/¿‰¹…¥>ßÏs$ü”>‰7¯“Cù€Ø;º�••Éų'WRÐ-β&Š_FzFj5>~�x—¯„��s¦%öj$Ö•ìõãñs{üîþ…[¶î:–y_×°ËçOó׬ñ´xñ
lìY2w2Õk7& ¨–œ˜bbjbL§VMøfòlþÝÆ;]^ÄØøá§Î·³±ÆÖڒ݇�Ó§ûKØXYG|Âí‡Þ¦‹£=nÎì9|œ”´t¬--ˆ¼Kì�:´h„ÚȈÊÊ‘ž‘ÅÞ£'175£Œ‹#99Ì­Ôl
;@P ·œðdbjF“Ö™5ñköl]Ã]z£V+“ Ú³…¸èHZw졟x©¬OkÔåäÑ=¤¦Ü&(¸îeË=ÖkCÞk׎gÉÌÊæÄÙ‹ù–qur k»ælÜy###Ú4®‹—»›´?JXB¥RÑ´Mçœ=ÛÖýöÓ7?¼ߊUH¼y�Sáû5u	6vtîÑ�ic>cÊwŸR> *ÛÖ-ÅÄÌœF-;eddD`µ:ìÚ´œeL%(¸!»·®&1>Æ`*ºqûpߘ\ºC¿?Ѧq–æfüóßVÜ�ìq´·•Âõ„ÙÚ;Ñ¥GfŒûœô´TªÖjDFF*áûwðé°©ù¾Õ‘ðóœ”	;ì�ÜYµxšœ¢£.p+!¾Ä–5ñèVÿý;öŽ.ú7nÝ©PÏLÌ•Kü4¢!�Û`fnÉÖÿþQ&2°W8N®ÊÐÇ¿uá¤à½f[ÿ[±ƒ»¸ËÚfcï䎕š÷¼†ef¤³dþ^~ýC,,,Ù±q‹~ŸÀ Ó±´¶‘XT*�Z7aÓîƒ|;y‡NDP#°"7o%‡¯—ûmÏÍÉ�—[6bú¢U|;�_œÈ¥ÓèØk˜p…û@éë²æÇÉœº|ƒ—o°n׿T37axïNԯ퀂›ßw¾Þí*š9{»ê–¬þa2S—þùNuÊ€.m01ÐãàéKì;vž¶MêÑ¿Km« ¥¡Nûf
ømÛAêת‰©S> @÷šì=t�/FtD]MEdv)ÐÑ5àÛ¹¿Ë6B=¼{=ÖvÎÿr�Zv.¶ª£‹;¹¹9Ò¹<ÎnïýÝ`hbÁꙣX½ý—nø2¢OÜ]é=q–\<;kKª™“�“KÇò3—°2´?$¼X· ”ôŠ}±´E!Pðü/ïùÿ�:SO4nßCÑïÿNoëƒ^±åÖ_H”Q>Þžwª.?9<š˜|°òQXX€D"æB|Êå#Öë`¥õG›œŠöÀ�àPNšÇØ�=øfx¿rÑ�RIÚ…ïcèeõâmôÂù6Ð�ÛOOY—wDù”¾º”‹œâG Ä� ’¶?ü‚pîš;�œBSC�žm›	ñó¾Ë{l¼A’—PÁs¤öR˜hÜŠÆ­?¢|�@ˆÑþ(�giéì=vŽ®­š0¸{{¬-ÍEû£œ
 ÿA<Fá�[Qùˆò!!~å­ýÑ¢�+W÷­í�
"€øQó²[\áK/	 ÷*‚DãV ʇ@ „øˆö‡h”¥ú_ñóòqIñ^Äyç™Å¢p	Dù�?Ñþí��!€JA%�/é³(\Qùˆò!BüDûC´?>(b[s�@ �@ð)P(Ö�@ ŸB	�@ !€�@ �@ �@ �@ H �@ „�@ !€�@ � ,y¡Š}„*7Q>¢|Dùˆò!åCPYÊÇû@ôŠ-—wÆ£‰I¹µ­"üF�&&Tæòñ1±$K"*¬ýƹHôû¿—´b½ŠQ	1iâ!Ê‡àƒ—�ÂÂÒµq%I…*[&M<*ܳ`ÒÄã�òG´?²}*\à!~ÞZü�@ TT„„øâG �@�@ˆ!~�@ $„ø�@P!)(Èçä¡¿…Büñ#AågÿÖ_Ùµa)1O
$„øâG ‚ÊKZj2NìãÙÓÖ-Ÿ.�@ âGˆ�@ |*œ9²“¨Ç¡o¿°°�ü¼ÜWWÖ-ŸFtD!�Þx{�H (oܸ|¢ÄðÁ¾$%Ä|°ï�Ž#"<€{·xö4á�¯½}íAg6¡–ä/ËËÏ'":NnO„°ˆh�ñÄ'0DˆŸ¸éÌ‚ÕïßG¹  �‰³ß9�…«·]ªk#câyU´?ņ}ÿqøœ×{ý�Obã™ðÃJ<½ýÙ|àx¥ªž¥¥ãu;°Ô×gçärþúíׯ	
}Ä¡3žÜ¹VìÞfç=#™YÙ:ãÉ®‰J[ ¨@øÝ¼HR›ïôäÑ~˜4@v|ç¿Ì_a~oaa!vŽntî3m]}ºGá'º]¬@‚rÍ©CÛ^þ ȯtÞ�‹Ý»áõ"æúy®]üŸkçHˆ}òʸ;Ö/!,¤Hìlÿ}ª‘²ã€ûá¸öÅèYËi>äkb’dçî†>â¯ÊWô¼Œüddf›ðôý¿($<òÝÄst|"·ƒîc]Õ¬T׿t�Cg¯ÈŽÛ5v£~­šïõwæäæ@£:Nì>zN®Ñ^щŒ‰ç×­ûK}}rjWo{mœƒg<¹âðR™Ì¢×„Ù�º—î#9sÕ[.þ”ekD¥ý‰àéíOzf'.ßx­Ð>tÆ“Cg<åÂÓ3³äÞ×ý‚8tÆ“ˆè¸rkwAA!¹¹yen÷‡$åY"û¶¬äÒ©äËÂSŸ=åðîux�?B~¾ô7߸|œ¸˜Hlû
߸qù‘�îs`ÛoÜõ“v€¤$'òï®5\¿t\–^b|4×.ãÚ…ÿضf‚}yö4�þþ•K§¼véø?É:O�Ø,›·ãwóÑ‘áoõ[%	=}A×¾#©¢­ÏÀQS©ß´½@AYNFz*ñ1‘ÄÇHECfFšì—Ù+7Ò竹üsò’,<.1™•[ö1xòv9#{!ï_ÍeÈä…l?\4¬Ÿ’–Á/›÷â1q6ûŽ_�ûŽ›þÁ|½ð7¾ZðÞÏGãbŸ2÷·Íü²y/�ÇL'-#Sîš¿�¢O‡²ãGQ±Ìÿ}æ/áè…«H_V3V¬g÷çèýå\¯ÝNbr
ÏÒÒÙqäûN\`øô%ø†p+àÁa�HxúŒU[ö3ðÛÙqø´L´ü¶íûN\à³)?%÷=‘‰±Ñ³–ÓkÂlæüúW‰£RJŠŠ´hà¿g=+]=—ˆ—O€ìžË/éÜô.¶ýÓg©\¾u‡ì윢N‘àÐb£³ÏÒÒ¨ëh‹KMéó¿ãÈÌM8»õv¯œË÷?o °°u5Uzµk†¢‚x¥~*,߸[ÖA3eé«…oJZz±‘ì�GÎàæ1†¾_ÍeìœäÈÎýsêW}Ë¥Ýý'ͧNÏQô˜0«Xüm÷‡äÀ¶ß±¬nϣР–~?€¤„fŒëŠ�‘)Q�B™=Ñ€ªÕk¢¥­K-·¦˜[ÚPÕÚ]j¹5ÅĬ±Q�˜5¡'Æf–„…øËF‹âc"ùsñ·D<ÁÚΙeß�dë?`ae‡·×iþÛÿ×+í»~ù�ÂIINdÇÚ%\:%mCìÞ´U5ñ0
$¨h\>u€K'ÿ`Ýòé¬Y6€K§àuî0& 7'›™ã{pþØþúuÁþE=W;Ö/aÍÒÉx�?ÂâiÃØ¹þ'iÃ0"Œù“úsõüQÖ-ŸÆ¡~�Äøh|®¼‘Ý®
K	
ö£  Ÿéc:èãÅã° ®ž?Bjr)ɉ„ß Âç¦yå®Í/(àªoÍë»HÅ”{�wrÛù”ÄÀi/o<:´`Ý�“ùeó^Y£tâ�«¨ëdÇ–Ÿfö˜#ç¥îc
jÕdï¯óY·`2'.ß ð�4?~Ù¼—ÂBؾb6÷ÃåϳWmbÆØ!Ìübßÿ¼ž¬ì2³²Ù°÷6–æ^³
5ù—Œÿ½PìªW•��³‚6�ê²òû‰lþç8W}¥£7îáu;€­Kg`l Ç¼ß6££¥É�îíèש[—~�›³=aÑDÅIÅø¼ß6£«­ÅÆES¹áÌΣg¼ΡӞ¬šõ�[4dùÆ]¨«ª²ðÛQZ½�VîuXºaW‰÷Ò®zUî>xT©ê�ðÈh&-þ�ã—oÐyô4Y�ú_ÿç³)‹8vñ:�FM“‰Å}'.0hòÎ_¿Íw?­–¥óëßûñº {f|ûùyùÅë,ošÕsA"‘P×ÉŽä”TžÄ&ˆÊ¼ö˜7|Éx>JÒÑšë~AÅDvI$%§púÊ-¼|Èɕ֩:Zšôj×L&Ì¬ÙÆù¿WqþïUÄ&<åÌ•[4tuÄþ¥z¥<Ù
°jæWìZ9·Øµïbwy Y»^4jÕ•¡ãg~�¬Ìtn^>IËŽ}iÚ¶'}?ŸDNv‰qQ˜[Ö@CC{çz›Yb^µUtô±w®‡�±9×/� m·Á4nÕ�A£§‘œGJr"F¦Ué÷ù·4k×#SKÜ·£QË.ô4ž;·.¿Ò>×ú-ð»y‰;·<é6`�·½ÈÊL'#-#3ñÐ
$¨h¸ÔoÎ�[—ÉÏË%==…¬Ìtòrs¹së2.õ[ÈÅ
¸íEU+;ÆO_Á¬åÛÉÉ–Vòùxž>ļ•{5i!µëUÖçŽï¡“Çç�0›)7àyö_

òqoÞ	[Wzýk»ZolïÓ„X�HèÔ{½‡~Mç>#1³¬�…•};4gÎж¨«©Ê]“œ’F

ôuµ°­fAbrŠ?oHÓºÎÔs¶ÇPOwnÜ	&áé3<Šäν0Öî>B~~>ÿ>w'SVVbÍΙ´è¢ãe
‹7|Õ·šêjŒèÝY–þ‘sW0ÒÓaÿÉ‹ì;qeeÎ^õÀÜØ�ží𡬤ˆ‚‚Dή°ÈhªšÉDTaa!-¸b «Íàní8~©H íÙ�*šïÕ¯Û�¯mŒäåçsÍï.Ÿõl�–†:£úvåÄKi
îÞC=º´lÈ�;ÁhkirÍ÷.S—­åïC'¹x³d×Ъ¦Æ<ŠŠ­TuHlâS¶/ŸÅ‚oFÒ¹ECŽ_º.@ûÿc×/s˜÷åp†ôhÇ¡3ÒÆÅÖƒ'Ù¼d:s'g@—Ö²t>÷èÄÖC§8ëåM=g{Ù3û2‰OŸa]ÕTvlSÍ‚„äg¢2¯Ä$$ÑcüL6ì=Šw@ÿž“Ö)ë÷aüü•\¸áKŸ¯çñêyÞ�!ž²��ŸpîÚí;|ƒ`f¨�¹±ÊÊJ4®ëŒ×;Œœ”•ÝÒ:Ñ Ræ½±™¥ì³®�1	±QDE„Ê…›˜U#ê
ÜÍþ÷:#SK™G‹‰…•,\³Š6¦Ï�5«è�žšüÿ¶•îܺH£–])(ÈÇçêYœë6î; $n�àcaçìÆƒ`_‚îܤ¦s=$

ùß <Ä›š.rqC¼±u¬SÔðp�~މ|ˆ�±9’ç®'ֶβ]ün\äÁ]_ÎÛ#­ØôŒHŒ‹.µ½Æætè9”y_÷AG߈�£¦âP»d`D`Tìš*šê¤fd�‘™…†ºQñ	TÑÒâç
ÑÕ®"û¬ª¢BNN.99¹h¨©ÑÔ­H¼jªKGh¾Zð+3ÆfDŸÎüõÏ1ž>“ºXæ ð¼Œ(*õûdåäRÝÂT–VS·ZT33&=3ëµ/{C}ⓞafd@~~¾,m%Eòó‹FdnQ	…RPXðÊt_xaI$YZy/¥¥§­õ<\Q~üÒuÎ]óaú˜AèëhS³ÓÐbn_ñIÉèVª:ÄÑÆ
ee\mØ{ü�[4DII	�çeÂÍÙž¹¿n&¿ €'q	˜I󵎃­,�æõ]˜µj‰É)l;|šñƒz–ø}ÚZróÒ¢ãÑÖÔ•y%àÐiO:·h(—÷ùì;q�ã—¡¤¨È‰Ë7ØòÓF*1�[þÁ´¨ï˜þÝPRT”=wr":9Enî �UÕÿw1Žò`weÆ÷úyš´îNZj2ÉIñT­n‡[ãvœ;¶›V�ú‘�•IXÈk»“‘žJFFšìZ=CÒÓ‹:5ë5nÇMÏ4iÝ�ÌŒ4"…`ëX‡�@ŸRÛWþ6�Âxö4+Gœê4fÏ_?3hÌÛ/a]XXÈ‘Ý눉zDjJ»7-ÇÖ¡Î'9HŒ	>ŠŠJT«áÈ¡�«qmÐ×-øwç¬le‚æ¶NuäþÙ̲I	1²	Š!w‹*™ÚnÍhÓu ß/ÝÊ÷K·2ûç™VEUMƒÜÜœRÙܱ×0Vþ}ž�#§°ù·¹X�¡Z�ÌeàQQVƦš÷Â¥Ëa„„ãd[]ˆŸwÀÜÄC}ž¦¤R×ÑŽÚöÖ(+)‘—ŸOLÂSjÙ× 5=ƒÏ-2Ðʽ[ž ;'—­OÊÂûvlÁ
ÿ`Lô¨çl�™‘´WöÿÃÙÆŠðˆ(,ÍŒ)((àº_É©iì|öÅLé³^·1ššÚ̞Ћ¹_õ¡ßçߢ¤¬Œ¶®>Õm�™ó¥Gö¬Ç¼š
ššÚÌùÒƒ³GwRǽ%‰³'z0ÿ›þ3Åw²O¢ €�ƒ+fU­‘H$¸6hAÄÃj¹½ý�D"á~�Çÿù‹”ä$Žì^‡Dòi>ób¨S‘÷˜y�Kýæl[³€©7 ‘HXŠO¬D"¡[¿ÑÌß#SR’“04± m·Aü±ä[ÂBüÑ74%Øÿ&SnÀÑÅ�-¿ÏcûÚÅ4nÕ×7²5øÎ
.œÜ�•�#¡A~4­çŠ%4«_›U[ösÓ?˜‰ƒ{»nÚèAŒø~)훳ÿä¶-Ÿ%ÄÏ £­)7Ϧ†¥zR·¤ßç|ÅöÃgX¹y*ÊJŒî×
›jæ|?v0C&/DKC�ÁÝÛ¡ô|´ç»ÏûñÇŽƒô?“ÁÝÚÉV\³01bîÄá,Z»�GOb01ÔgÉä1¨©ªPÛ¾Æ+mØ­-«¶ì§çsÿø?ç}Ëú=G�Œ¦çV4­W[×ÉÆ
�/çP×Ñ–ù_}@“ºÎ\¿Ä ï0e䪙c¨«Àü¯>gÓ¾ÿè?i>Ý[7aH÷vÔ´¶D»JÑhCƒÚîÖ–Ù«6Ñuìº·nB×–�Šý†3W½ÙòÓŒJUêiWaøŒ%8ÛVçܵÛl[&m¼ëÙ‘ÁSÒÐÅ‘ÓWn±zþ$†öhÏç3~¢•{®ùÝ•KkP×¶¸ôÅìñŸ½òûFôéB·qß“ž‘Éùë·ùzXÙ” bÓ¸®3Þ÷¨çl/s´±B·Š&MCYéÿoÄêjk±dòXòòóY°úoö;OŸŽòîÜŽ6VfÓÇÈ»tôîЂÞŠ¿¸õtª0gÂ0Ùñp�޲ÏÍêÕ¦ÙK‚å‹¿óJÛjÙY“—ŸOdL³Æ-–Îw#úËo^"u0Ò×eÝ�“‹Å732`Ñ·£ðºH]';õt*MhmaÊöå3ÑPW#8ì1ÓÆ–5ö¾Ôƒ¾�ZâÊ”Qdn=ƒ»·£u£ºø=`ÜÀîóB	ñóQѪ¢‹�S]ÙñËó|^ôš¼@"‘àÖ¸m±4NÜJZj2	qQ„û1{ŹóN®�Š]£¬¢Š½s=²³2IN”ß»@IEEne•ªÕ튾nMJ¼ÿºÚ2·™~�[²çØyFôîŒM5sjZW£¦u5¹kî‡ãJë†u…ø©€¬_0‰äõ^ÄÕÌMd�ï�IýZö¸»8Tªû¯¡®&s'­nQ¼#ÃPO‡¶�ÝJ†/æé:HçTy†°aïQviƒîóyVî.\¸îËŸº¶j${Ö{¶m*—fjzëöap·¶âÁ¨ ˜›rò¯åxÝ$1ù-H=&éŰ^8wÕG+º:°è»Ñ²y‚/:#wkËMÿ`róòÙõóôuµ‰OJ&'/�ˆè8,ÍŒX2y^·x–šÎÂoG¡¥!íLxúŒ”´ŒrkwKwWÜœìÙGº�Ì‹¹L¥±[ðqé;ütô1µ¨þÉÞ	Pøüi)<èU>WòhbByµí}‰“&ì7z41áËF™”�Œ´îß½�¦¶¶uÞêZ¿›—¸zþˆ\XÕêötë?¦RŠÏ÷)~$úýßKùˆõ:(ÞF•“&¦|‡=æiJ*î.Žb/Ÿ
V>J;ê&‘H(‹²•–‘ÉOëw°pÒ¨×ÆÝyäwCÑ·cKê8Ú»_eYW¾»Mšxð.ùSÚ‚�×¾Wí"F€*°øHÑÐÒÆÕ½e©®}±øÂ§pÿÅÈ�@P25ª‰› ø hi¨ÿ¿âƒŸÏ÷vÑÕ%Ä�@ˆ�@ !€¢ñ-âG � ²!\àDãûƒóÜWS (‘çsQ>�@ �?•cäGLB¬|ùkì�ûbQ>%ò¾:>Ä"BØ
¹”Ïw¥@]8·7!~ʳø�@4ÒEû¤4”‡‘yÂ~�È_!~�@ í“O¥ý© 2OØ/ù+Ä�@ ¢}ò©  2OØ/ù+Ä�@ ¢}ò© $2OØ/ù+Ä�@ øÔ¨h1ˆ…#¢}"�?‘¿BüA©©H+š4ñ¨p+"
Á&Ú'å‘yÂ~�È_!~�@ í!€Dæ	ûBü�@ í!€Þ�¸è>,ñÜ�Ë'Ê]æåçåâ}õL‰çîú]#-%¹R>
2Ê…¯*‚}IJˆù`ßFDx÷nñìiÂ_{ûÚ9‚ÎlB-É_.<'W^T„EDsèŒ'>�!Büoɳ´t¼n–úúìœ\Î_¿ýÚ8A¡�8tÆ“;÷ÂäÂsóò)((”gfesèŒ'ÿ]¸&2F ñ#Pq"†äw½Äsk–M-uæÍX±žÈ˜ø÷nonnŽï-ñÜ¡^ªÂç}fûO^,§…¢{ÊG£üÔ¡m¯äWª4ýn\äèÞ
¯1×Ïsíâø\;GBì“WÆÝ±~	a!Ebgûï³P͈”/Û¸‹ú}ÆaÕzé™Yò":ôýs\ˆ�àmß%1ñüºu©¯ONMcáêm¯�sðŒ'W|dÇ—oÝ¡éÀ/©Ùñ3ž¾\,þ”ekDÆ!~>5TP�/7ºó È—œliƒ/æÉC23Ò°u¬Kýfeq23Òð»y‰ÌŒ4¹´
	ô½J\tÑ‘ád¤§Ê2Ï ó>×ý‚d=p	OŸáxï€{x¿eozäÃûäæd.UHKI&>ö	*ªjôö�¼]·½Hˆ�*1­�@RŸ=%>&Rîš`ÿ›d¤§bI¦y¹PdorjZ¹*5+“ —ó5>&RvÏ23Òˆyò€Ác§Ë]ì³DÑõ˜ ¿ë¤¥&;ÿ È—”ä$Ùñ½@oîÜ"$Ðû�FvZuꇙe
òrs	ô½Ê­+§INŠ'-5™�oé_ 7fyá(“G—
1Ò×`¸G'®ï[ƒN-ùûliF“ºÎBüï@T\"^>r#2©éÜô¦°P>üé³T.ߺCvvNQ§Hp¨\¼°ˆhž¥¥P×Ñ—šÒçßÅÁ†Ó›WгmS¹4ÕÕTéÕ®Š

"C�?ŸšRPPä§#ÈÎÊ$!6ŠécºÈÜÇ~ž3ŽÜœ¼¯žáø?à}õ³'zpçæ%~žû…,�´Ôd¦�îÄÕóGÙ¸r&‹¦|FØ=,‰àä¾µŒ›÷oúÒç빦3W/ãÏ%ßùð¾,�5K§°mÍÎÛÍÂ)ƒ9²g�´ñìË“úãsõ,kç�Àó¿-äææò02š‡Ob¸â@Rrjùi‡1qeò]—OàÒÉX·|ºlðÒ©x�;À‚ɃÈÍÉfæøœ?¶‡¿~�C°ÿ
Y:;Ö/aÍÒÉx�?ÂâiÃØ¹þ'©ÀŠcþ¤þ\=”u˧qhÇŸD„ß#1>šŸ+oäF·kÃRBƒý((Ègú˜Îúxñ8,ˆ«ç��šœDJr"á÷ˆð9†iÞÃb×›è•û†‘?‚ŠHxd4“ÿÎñË7èë]n
ƒÔ ¬Ì¾Ï¥~sï^GûCHOOA"‘�—›Ë�[—ñ2Q.nÀm/ªZÙ1~ú
¾èÛ�Ž:zž>Äê=W‘((°kã2¢‡pîø:y|N£–](,,dꨎôôîÍ;qÿ®½‡~ýVö>Mˆ‰„N½GÈ•e+;úvhNϺF²âGPQ‰M|Šç®ßQQVfÕ–ý¿t�½;ó×þÿ8õ×
4ÔÕøûÐI�¹ÌÔQÙzð$›—LÇÌÈ€}'.°v—´£ås�Nl=tЦnµ9ëåM=g{ôuµÅ
BüôfÔ~.(ã£éÿù·ü¶ðîx_Æ¥^³bqCƒ|±¶«€�ƒKQx°5j;ÚÚ`LAaDÅ%2zÖ2Ù¹êUMßÍ^·fìÚ°”€º�©]¯�Ü%4Ø�´”§š˜“žöL÷^ 7n�Ú ¨¨$³=üA ÕjÔ,²ÉΙè©�ð»q‘œäH. uÔÕÖ"-#³Ü„ó~((³ï´svãA°/AwnRÓ¹‚üoâ�ÍKùà�­cÙ±�ƒôsLäCŒÍ‘<a±¶u–	 ¿yp×—óÇöH~‰qÑ¥¶×ÀØœ=‡2ïë>èè1pÔTj7@ƒŒˆ*žâGP‘q´±BEYWGö¿@ç
QRR’�Ô¸9Û3÷×Íäð$.3#ê8ØÊÒi^ß…Y«6‘˜œÂ¶Ã§?¨§¸¹�@ˆ!€Þ—úÍ9ºwJJJXXÙa`dƹÿöеïÈbq­íkó(4ˆöµ	)r]sªÓˆ;·hiKç4¬W—‘]ÜiêV[.-
u5Ò“ËMA(«y?/£¨¨DµŽÚ¹š®}G"QPàß�k°²q”	šØ:Õá¦ç)Ùqxˆ?-;öÅ̲I	1äç硨¨DÈ]9q[ÝΙæíå7c¿HnnN©lîØk{
#øÎ
6ÿ6—�Öc¨VPl…7!~‚ϽðrsóPVVÂ?$œzÎö˜ê““›KfV6êjªø= �KM05Ô'6ñ)&zø‡�vK$umÃÊÍ{y[®ææ	�?@™‘�•‰um©‹’kƒl\5‹)Ö‹Û¶Û þ\ò
šu $Ðç%Õ‚S‡¶±zÎg¤'Eafd€ŠŠ2JŠŠéÑž¡ÓÓºa’SÓÑש˜þÝh^ß…Ek¶áælÏÔQßZ´�=º‹ê¶ÎèèòÛ‚¯˜¶xS±xõš´çÇo’��ÉãÐ`4µ¤.ªjê4kÛ‹Ùz¡¥­KzÚ3ôM±$‚‰Í™ÿÇšÔõG§Š&!ᬜù%�ê83ræRròréש¶Õ,>Z!(Ëy?%Ýûmk0uá$	‹§eÄ×?‹WÛ­»7.gãÊÙ$ÆG�D"k¸të7šYã{bdjAJr†&²òõÇ’o	ñGßД`ÿ›L]¸Gw¶ü>�íkÓ¸UWl\ßÈÖà;7¸pr?V6Ž„ùÑ´ž+–DЬ~mVmÙÏMÿ`&îU캓—op檙YÌ^µ	7';†öì Ä�@ðŽèiWaøŒ%8ÛVçܵÛl[6€a=;2xÊBº8rúÊ-VÏŸÀÐíù|ÆO´r¯Ã5¿»ri
êÚ—£˜=þ³W~_dL<¿þý7ýƒ‰ˆ‰ç¦0?M+2âÄÓÛŸºNvhª«qâò
:5w/1^vN.Ç/IW¾íÕNê	“_P@DtƆr�¹×ý‚ˆŽO¤ž³=–fÆåÎîܼ|"câ06ÐCó¥¹pea·@ˆŸr/€¦-Þ„¦fš·÷ FMÔÔ5¨Û°5N®
poÞ	‡ÚîÜõ½J÷ce‹
¨©k°îçŸÈ�¸�±�.�FMŵ¦
ãõdhÏœ½êƒC
+Õq`ÜÀôl׌°ˆ·wqê1ðš·÷@"‘ ohÊ’µG°¶¯ýÜM¾™ûºúF,ÿë$Þ^giß}¹¹9™V`À¨)têý9yy¹ìÛ²’ÖnvÒèlÏ¡?qùÖR32èÓ¡%vVœÜ¸Œ€û1Ðùx¾æe=ïçiÓuŽ.©°xíÌ,­eç§/Ù€ŠªË6žàöµsšX ¤¤Lé<œ.}GѲS_ÒSS8lÆf–˜W³añšÃÜõ»ÆÓ„X>ÿj>UtôøuûEÂï oT²eÖ](È—N‚î?b2z†Æ¨¨ª“——G|l$ã{ÐÒZêzÓ·cKZ7¬Ë½ð”Ÿ»ã<‰K@SCUem¬¨¢©�Çó—ˆ�ž4¿Ó22‰O*ûQÀŠ$~â¢#ÈHO¡ºmézä“ãˆ�}‚�S]QÛW2¬-LÙ¾|&êj‡=fÚ˜Á(+)ðÅ ôíÔ¿àP¦Œ€’¢4|p÷v´nTß ŒØ�GObeéii¨cb¨Ç€.mä¾'éY*É)ièjk¡«­…G»f²gY¢ ‘5h£bŠ­8'([²²²ˆŽŽÆÚÚZ.<<<333ÔÔÞßË7îfåÌ/©aiÆ”¥k^)$RÒÒY°úoæLÀÝ={9Væ&DÅ%Ò¹ECfŒ,‹ÿÏ©Kääæ~0!QZ»Ó22é8r*5ª™›€]õª¬ýá»·²»,ó§$rs²IJˆÅļš\xlÔcô
MPVQâ§Ð4Ì/ùóAP5ë¢ù0ZÚr�]}ùyÚºú4jÕ€šµêË2ï—ÅÓ±«^•Ë·îЮI}¹^-
õbË�˜êcj¨ÿÖöêêÉÙåàRT!(**ÉÙ¯¨¨„{óŽÅÒ8¸ý$	‘ï“q—^“ºÊÎ)(Hhé^|”AC]
w‡�V?ƼŸÿE«Š®Üý}yž€Ãó‘D�Žö¸5n[,�“·’–šLB\¡Á~Ì^±Ck£b×(«¨bï\�ì¬L’åG¿”TT002“W­n'û\Ë­I‰•‹�®¶Ìm¦_ç–ì9vž½;cSÍœjæ&T37)fCÀýpü‚Ciݰìçmä'Ð÷*á÷J-€Âïàyö_!€*!êj8ÙV ºEñŽC=Ú6v+nfd ›¤ë ]šÞ;0„
{�2°Ktµ‹–«wwqàÂu_®øеU#´4ÔiâV«Xš©é¬Ûs„ÁÝÚŠŒùHlذ�+V0xð`æÍ›'wîï¿ÿfçÎ�L™2…1cƼ6�à°ÇÄ$$á^ÛA6�,=3‹€�pÔv@á¹è}IÉ)x† ©®FýÚÒ¶�Ž–¦lÅÂÔˆ3›FC]�ܼ|šœÈОí±01¢¡«#§®Ü,ÕïÿÐvki¨se÷²ø�FOÃ'07gû7²û}åOi9}x;‡v®¦EûÞ5EîÜ…ã{¹tú½O }�Ï„øùÀâGÓ° \ä�Ryͼ‰Ÿyö8Š]Ú`PVãiÛm0‚nÓ¥±3­jô¯Êǘ÷ó!hÞÞƒûwoãêÞ’qS–¾ÝKÃÿ&WÏ‘«ZÝžnýÇ”ºryÓ•ý¹:ÑÈÕIˆŸ7àÉ£ûd¤§2I	1<{š [�¤¨ÇèOb)(”V´y¹¹D>ºOuÛ¢ûýðA U­ìQz>j'øtÑTWc¸GGÜ]åÂÛ5®G»ÆõþÿN´*Z,œ4JÜÈ�Àõëי꼘·E[WŸž�í+ŒÏåÇœ÷ó¾ÑÐÒ.uÅáÚ ®
ZTŠÊ¥²ŠŸÛ×Ά†–6;Ö-fÞÊ=HØðËLâc"Ð30!âaóVîAUM�ë—à�…•¡Á~T­n‡¢’¿ÌÇ�¿@WßˆÄøh–Ïû®ˆ¤‡ÕÄMøÄ9tÚ“Î-Ê­˜_PÀ¾8¾qJŠŠœ¸|ƒý'/0mô Ó¸åL‹ú.ŒéßMævù:7çå›vS¿–vÕ«V»?Ÿ¾„»1¢Og™;· ôdefp/à驨kjaeㄪš:y¹¹„Üõ¦¦s=•¤�tñ±OPUU'6êšZXXÙ½²}�±�.ZêDDÇ¡  ÁÂĈÔô’ž¥bU‚WJY‹Ÿ�M¹@}ÂVE²ÿcÏûù+ÄÏÛ   (›¶|ÖhîzcimOàm/Vm»ÀÚåÓð½q�úMÛsåÌ¿ü¹ç*‰„Í¿Ï#-%‰DB›®ƒ8{t'}†}ùÿvÓ¶Û $‰(ÜA¦aÆxzzÊ\¬Z·n]ÌÅê‡~ ::úµ.V—oÝốòÞ�÷ŸÄ¨™Ò­7
KóWÚ2 K–nØIÓ�_ÒÒÝ•ïÇyeÜMû�qûî}þ^öý;ýþ²¶ûàŸ¥›Oú�“—oÐ¥e£2ÉŸÒbïìÆ’µGd.VµÝšs±ÚÃ
ž&Æ~¸ÌŒTã¥sו””06«Fàm/voZNízÍÙüë<�›A݆­ùoï}¯bmçLRBF¦Œ›²´ÄöÉÞãç±45bH�öLYºee%¶/ŸÅ¾ÉÊÎfB	6}ñó1ó§LÐ�í¿SÍÚ�úMÛóóܱ|þÕrs,^Î<ïÀjÙY£ª"U¼FNÁÌÈ€ÓÇc¤¯KvN.î8Èã¨8zµoF+w霑�Gϲïøyºµn¨¾]ÞÉÞA¾\>}€_ÿÈ©·¡¢ªF«NýJŒõˆªŠqXšH™óßÂÝЇŒîÛ•Íð$6ž‹7ïð02šá±0‘Î5
{ÌÂ5ÛPVRbó’éïýžÿ·o#§o—5WÿôÙÙYLž÷ö„ÐeôºµjÌ—Ÿyˆ7¨?oM\pÙ¹�½¼JŸ�£+A~×ÉH“_¡¦s=îú^ÅÄÜ
S‹ê2acëPßÒg m×AÌß�Ͼä‰},^sXn� ’0f̆Jttñ…�†
ÆôéÓ_;ɾq]g¼îQÏÙ^æhc…nM6.š&[`ãuèjk±dòXòòóY°úoö;OŸŽÅ=¶<ÉIÏl[6K¶�Ui)K»_ ¨ @U#2²²Ë,Þ•ö=>£U§~$%Ä;תszýê£,‚ g`‚�clëʶì8õï6ÆMYŠ�ƒ+ÁMÛsx÷:ê6l-m�X×dÂŒ_øfHtÓîb©U|´®EW¶wïDZ‹×èЬ�L9Ô¨ÆÏ3&0ø»äžÛ;×CQ±({4ëD~^®lÞÏÐí±«nY&€G‚Òòòaá!þtê=‚jÖl[³H~?È—ZnM¨jeGÌ“‡²ð°Ùç*:zÔ¨éÂÖ?¤†}-tôÅÍ­˜4�4‚7CMM­Ø
c@‰aÿKïöÍñýR>‰ÁÊÜc=<Ú7gP·v›¶ˆVîÒ­7tµݯk‰iü¶íÙ99èëhsÍ÷.˧�/çÆ�`¾ÿy=½;´`ö*évCz´“Û˜÷m(+»¯ú²ãðì¬,|ð��‡‘,ŸöE™åÏû@YEµØ
c@‰a“{·°¶—Îkµu¬K�ÿ
Ù9Û—:�쬉	8�S£â‹ü4¨U“¯þÆu¿ Ü]ÈÎÍã¦0Áa�q~¾xLy?+ÊÜÎóÌ!ŒÍª¡¢ªÆ©-ó	¼íEnn_ê��ˆæÛ%¢£­Åú'Ë]›’–Á¡3ž„œÜ†ºš*G±ëèY¹ž�÷̓ _”UT12­Ê­+§9}x;©)Oq¨Ý€/tçÌ©#(+)qé¦ÓÇ÷¯íÞº	Ý[7áªïÝ2½ÏvNuå&Œ7hÖáù¼ŸÏ+ÜöefËA¯Xñf.�Š>²$Ñ/›Å>23Óùyî84«è�ƒ£‹;

ŠØ9ÕaéÌ‘™sÿ®Ã&ÌFIY÷æ�X0y0–Õí	¸í…•MÑÄöΟóýÝùñ�¢Vb½Š› „íÇÜÄ�“-Çëv ‰ÉÏhÑ@ÚМ8¤ÃzuàÜUm¬hø|ášEß�ÆÜXºšà/ŽÁÝÚrÓ?˜Ü¼|vý<}]mâ“’ÉÉË#":K3cì¬,Øÿ›ü$îªÏ;Fž>#%-£\Ú]¿–tÅÚˆè8:4k€£�•Ì†ÒØ-(BUMƒÜœ¢Ñ4[Ç:<

ÂÚ®a!w°w*Z�!ìy‡ %<|p—ºŽƒKÊJØXš³n÷aÆ
ìAvn.n?Hm{ë2qý.�s~>ººàMNv)wOðä�{VÝdÞN IDATΣ °€¤äTŒ
t±¶4cå÷e#@/ó$63#}ÔÕ¤Ã`N¶V\¾uçƒÚóä!ªj²mõ–¬ÿ
Í*¨ÄÞ¢ŽA}:´@[Kó�Ýî>4bÞ�?ïSü”/ö
KO}FjJ2_L]&;7aÆ/DG†“”ÃèoÊÂ?ÿj>�BƒxšKï¡_‘šR4*l`lŽ…•µÝšŠB(Šñb;ƒ—©¢©AÏçËA¿ –]ѨEƒÚRq`¨§Cç
åâ©«©Òʽëöaá¤QèéT)q)u€Sž7QUQƾÞÚne%E×q¦qç÷j·j¹5eÛê…ÄF=¢sï´ï1”Õ?M¦~“öÜð<Éà1ÓdqCƒýØõËW¤'<¦®£z:U^™n‹®,Û¸‹M‹§“—ŸÏçÓ—°dò‡ß¸¹"ˆŸ�"€Ìˆ¢ºI>?Üç§
;ñh×L®7áUääæÉÄ€†š¹¹yeb³D"AßÈŒM«fÓ³Mcú7´*Æêò°ß�@ˆŸÒð¿û†«KªZcVµ¸û„•�£läG[WÚÛyÓóÿîZÃÐñ³D!e‚–†ú/‘>¸{;a÷'ˆKýæÌÿm/�BƒPÓÐĽyG\ê7'Àç
=O@M]Cwä�Ô2MõÔq|½Ûä�îíhêVUeTQæÈÚ%Ø|àÕ•+Šø‘¶�ËUr0'=Îlù몦L_±ŽÕ;ýÿÂÉȀЈ(òò¥»È…>ÂÔH¿ÌlŸ»r7½Û7ãÆÅ£xLœ-F@U–ý~„øù´ÄÏ{¡WÑaä7?Ò°EgQ¯åIl<Ë7í7B ”	šZ:8¹6BM]ºO”šºõ›¶—?UHÅ�§4u«-?yùùÄ%&Ëý½XÂ\W[KN$Õu²C[KCˆŸç”é�%¨ õsŒŒ‰§Š–ƒº¶ÅH_—#ç¼°©fNpxD‰.pƺÔu´eëÁ“ôhÓ„ƒ§/3¦·2±=';åXoú¹[еöHÚ~þùX[šqÝï.ù(*דÙ9¹qn©ߪ™_’“—‡ºêëí°®jÊòéãQRTü +æýˆÊKˆ� t8�@ Ú•Wü¼wÔ¬]/šµëõ^3ïM'l©ª(£ªR´£²ŠæÿŸ)–Öö|1mù{-|êjª¨óÿïV«¤¨ˆŽ–æ»ežjaÉ
W1ïGT^Bü¥F¸À	Ñ~¨¼â>à*p3óT4е̯ԅOIµ}ëâK€W¶y?±Q�yø ø&²………ܸ|òƒ~·ÏÕ³äåæ’Ÿ—‹÷Õ3o|]Jr"Ag6qïjÑʆ9¹¹<|CA�¼h=sÕ›CgëWL/uºk—M%1.êµq6ÿ>�¬Ìtrss¸p|ï+ãe¤§ò˼¢IˆÙ=ùgÏÙñé+·h5t³~ÙH«¡“Ø~ø´ÜõkvþKXd´?Ay©wž»À	�?•OüÀXAˆŸ�#~ âÍû)(ÈçqØ=ªÛ:pÿîmªÛ:¡¬¢Jtd8zÆØ;»‘cë,'6îß½�­£«\Z………Üõ½Š±Y5òrsÐ50F]Cº\dVf:ïR³V}$

$'Ås/À›{�ÞÆÇ`ïìöúr¥ªFŸaE+÷%'Ær÷6U´u±¯U�ȇ÷		ô&$Ð;í,Œ‰ÃÂÄ�v�ëÒµ÷/íø
%EEâ“’é0r
ý:µBUE™v�ë±íÐ)!~‚r„p�û40iâ!ìñó	ŠŸ÷.€„øùxâ§"ÎûQPPdÉôaü±ë
I	±ÌÛ•éKþ½y'VÌÿïç¦çI¢#òÙ3¹éyŠÝ›–S§AKï^+K'õÙS~ün 5k5 6ê!QaLüþœ\qdÏz|<©nëÌŽõK˜0ã2ÓSÉHOá~ ±Oý¿(3#�³ÇòÇî+x{�æàŽÕ¸7ëH`B4ÚºúD<¼GVf>Ç0®¦‰žN¹ë
õtdŸÕTUÈÈ̦°°°\å…?�àS£"‰\“&N”Á&ÄÏ'!€„øùxâ§"Ïûq¨Ý€`ÿ›ÄþæA�/wnyâR¯y±¸¡÷ü©n#u—³q(ÚïéA�/ÖvRÑ#‘Hd.uá!$%İôû‘,™>œsÿí"';ë�ìíÜg$‘ï3a@c6ÿ6—¬Ìt,x‚ÿ¿ˆ˜½j:Zš|3¼¯?�@ ñ#ÄÏÇj[¿¯âç㈠Bï÷ãÒ %'nEAA3ËèêqáÄ>º[,nu[g‡ßÃÊÆ‘ðY¸S�FûߤiÛžä|çy|'4µt˜¶h#ŠJÊri©©i�››óÖöêšòåÌ•äåæ²fÙB.l£Q+{òó__çý¾™Œ¬l~™1Aˆ�@ „øâ§¢ !~>žø©è™X�žúŒZnM¥‚¨~s¶ü>Ÿi‹6‹Û®Û ~_ô
õ›¶'$À[^ǽgŽìäç¹cyš��‘)**j(*)Ó¶ë@MF÷V¤¥Iñ#�?Bü$ÄOy?i¥lôþìK¢"ÂiÓuÚºúouíѽëIIN”ëè1k»Z¥*?Væ&,œ4ê�¾û›a}Êì	ñ#�?Ÿºø©PHˆŸ�‚VZÈ~s-û¾Qå½|XXÙaaeWªk��Q)Ê�?�@P6xzûS×ÉMu5N\¾A§æî%ÆËÎÉåø¥ëôj×Lî\^~>…… ¬¤Àu¿ ¢ã©çl�¥™±¸ÉBüñóQ¨F
ñó1ŠEQu	ñ#>,ùùù¬[·N܈
Âò�»‰Mx
À”¥k^/%-�«ÿ.ž›—OÇ‘S:u‘\ø?§.½r/9�h?ñó		 !~>žMÏI•—?ÿK\°²x«	ï‘%K–0wî\BCCÅÍøˆ‡=æÂ
_22‹öŒKÏÌâº_ÿ×Þ�‡EY-pÿ‹,"¨¸!Šˆ;¦¹d¥––æVšz3õšš[¥]5µí¶¹g7¯–Z©uK³rÉ,×rESqÅ7Â�}Ùfî3L²hšˆ¿Ïóð$ó¾óÎyÏ{ óã=ç¼½ Gl|"wî#héæß¿e\Jç¹ûóÑ’ïhÕ4ç0ë�ü©S½ª.‚ú
?…ÑÏUøQøù‹2ÝñeZæXDäÆâââøòË/‰ŠŠbĈ¬_¿^•RÈ"¢cúÆLjûT¥r…r\¾CßÎíYðívì! Vu¦.ü†YFàë])ßc=ÅäÙŸÑ­}kbâqvr¤J>à?Æ™?.ñ|÷Ž„ž¿ Ê¿�©ÿ¢T$ÃOvRø¹£2ÝñŸVE–ø4Œøsœ2$‹ó©à—�Á£·~ƒŠ€áÇj^Ö÷îݬ]»–Î�;«b
Ѫ�;x¢m†÷ýsÅÐ,£‘ïÖmá—Ϧcgk˺í{ø~ýÆ¿Ð7ßcì;r‚¶Í2¤wìlÍ£Ç¯ÄÆçØ'ËhäÍ�1÷ÍW8ýÇEUü}L}—âÞ/+Æáç¾NžE#üè!wüW†L8Z¬ÂOaÉÈÌ"hû�žº§ç´?„̬¬;VbòÕ-_øå(Žž>˜ÿ
�rÝ𹿙L&š7oÎÈ‘#)W®cƌѳ�î�íûÓ¼¡Ž×ކžçò•XOšÎ€	Søæ§Í¤^K¿á1ž}²I)WiÝgãg|J\BRž}¾Zµ�Jå=	=Ç�c¡Ä&$|�¿IÉc4f±~Õ—
@
?
?%íÙC…Åž8Š+I
?ÃóãßgÇþ#ëa±k·ìæ›5›î¸Q1ñ|ôå÷Ö¿èÞ®)¾æÈ©³–kënâ¶=ì:x”Öoà�K‘|þýÏúá+"cÇŽeÔ¨QxzzòöÛoÓ¥KUL!kÙ$€à�“9^ó¯éƒ»ki>{øèIö=EðÑS9þ{ðøibâ­ßÇÆ'r1ò
ᗣؾïpžrÌûfÏwï˜ãµs.s6ü²õ{£ÑdîÌ$$±mï!22Ì?OñIɇœ2=EFf{tÊñàÛ¨˜xŸ<›ãøGN�åZZ:;÷!öº²ffe|ô¿ý~€´ôü¯G—G[±jãvk]ˆÈ_ëùØCü°~“>\Èüe?²rãvìíléÛ¥Ï�ŸùË~dÚÂ¥|öÝÚãã¯V0ãóe,Y¹žÝ�åYäàÑMx}D^ÑŸþݧ¦weõ|âo—{cP0ϼô—¢bðòÏ€V·S"¢cuq%‡ä¤x¶¬ûŽ„¸hæÏ˜ ¤ð£ðS¬PD8ß~>óž}¾#×hÀœHUøù›‚CNYB@Q1qlÚÌSÃ'±9(˜.C_cËžƒÖ@óä�	lܹ�ž£ßä‡õÛÈÊÊâløeÂ/G±sÑñ‰„]Ф÷+o³æ· ^›9Ÿ�¿ZÀæÝûy~üÞüx'Ïå�3¸qç>Ú4m`ý~àkS™þÙ2¦¶”ç'L -=�§G¿ÉÐ7g±uï!ž2�Ã'ÏŸDL|"!¡çع?„ÌÌL&~¸�ó#xkÎ"Æ|ð_¾^³‘.Ã&Z‡Æ
˜0•ÿžÍæ]ûé9úMvî?ÀÚßv±vË.Ÿ8C÷‘¯óû¡ãyÊkogK-Ÿ*8~Zÿ§¹E•½Ê±þ‹ty´•½ÊÑ®eSFþ£;ÞýËyвq=úw{€÷Ǿ@å
ž,šbîDöëÒžúµkPÞÃ�¥³Þ a]_Ò33	¿•ç3ë×®Á›#Ÿ·~—pÛÃc¨_—¿4€reÝørÚD붯¦OƳŒ›.®ä0Æx.‡›ÿèvêh0ÁAKd=Ü‹ (ü¨L­^£ïÙFg®R�c”"]áçèÑ‘Ÿ/ã¥þ=˜²àfŒ‘u|éÐê8—ÿÄ#ÍóíÏ¿2v`/:>ÔœðËQ}sOwlË#Í—`}ÿŒÏ—ñ\×ÇéÚ®&“‰ŽƒÇ1¢Ÿy³��­µs½ôŒb“pwsÌ«7edd²xªùá¹}ƼÃÑÓçñ­Z‰øÄdfNŽOe/ÖÝÉwë¶ðîËÿ¤¶Oz>Þ–VMr;%õ›wígÇ7sxã?_ðëîýÖår‡>û6ªG³úuYóë.Z7m@·mèÖ¡
ç/FàY¶+7n§E#ÿ<å®^¥"§Ã.äùL¹¹ü~f\K;Ó-×2Öõk×ø3„4ð³�2<ѶEŽýœx¤yc滆÷^œc››‹3õjU·~¿aÇ^JÙS§º÷-—×ÝÍÅúû©”½½µ,Íúé‚J&“‰ÚþMq-ãÉÎÍ«xâéRR§Þó¤ð£2Ý
W"/²ù§oè3x\¡~®ÉÔãv�º½Ÿ¢rÒÚYhR¯{›ï~ì:xŒ—<€w¥
\ŠŒÎwxØÖ=‡Ø{ä„u®Lù²î\ŠŒÎÑ�É--=ûëæþì:xŒFþ5­ß7
¨Í®Gñ­Z	wW|*›—Lmì_‹y߬ºéù8Š¿o5ë÷�uØuð¨55ñ¯m=§°Kæ;F¿ý~€O–®¦¦weÃ
ç8”*ŵ´t¤htHfÏžÍÙ³g‰‰‰áí·ß¦Y³fšTL¸8;å	>7Ò頻0¹«]û¾ÈŰPïÝVè}¤ûÉ=§ð£2ݵt†À¹‘HG~î’Æ~µ8yöŸˆ u`}zuz„%Ó&²dÚD¾žõ:Õ,�ÅÞ.ÿ¿¹–vÆÎÎÎúPÄÔµ~À¡ãg¬á)>)™Wóžfõëàì蘣ÙùÕäÄuCî?�#ˆyËóéÒy{Ô@¦¼:”F~µn¸Dãù‹ÔòÑ‹J‡dÏž=Ì�;—èèhfÏž�!¿‹/""×VøQø)Ž
{\Yâ¨ËIl(OT¾ßÂ@ÿî�3fÊ\kÕŒõ;ö0ah?ž}òQ&ÎZÀÁã¡ü¶ûŸîÀƒ�êññ—?€ÁÀÓ�?DŸ'Ûñòûs8v:ŒÊ<Ù}ðŸ0þ/?·eãz:y†–�hPÇ—ÌÌL^yÿ¿˜0‘‘•I#¿š¤^K£ŒKiÆN�K³úuùeÛ>|mmš5à£Åß³÷È	Föëž#\µnRŸ¦àS¥"»eÌ ^7-KÓ€:Ìüâ[ü|«±/׊U×;q6ŒÆ~õ¸"â“O>aÿþý„††òàƒê@""w™ós^îäÏM¦È •÷eø1xôævËVÔ‚Æíœãí–ÉàÑSìrƒGï;!jºsq
s\9¢©M(†bòȤ[
?–¡Þ韪oúûcï‘9¤^#hmàäè`}=##“-{ñ@ƒº¸»ºX_O½–FHè9|½+ãénž¼çð	.GEX¿.U+–çJl÷2?͟¾#'y´Ecœ�­ûÇÄ'rò\84ðãÔùpjT©hÝv)’‹Wr¬|ôMëÕÆ`0�’z�?.Eâ_Ó“ÉÄÞ#'IHN¡yC?Â.FÒ°®oŽsعÿ?ý¶‹)¯½§íÈ«U�i%å™8ï½÷sæÌ!((ˆš5kÿ·ù.×=kƒážõþîÏSQ*ov™ïäúDûÐÃLów1,”)òße;‹dù{´òºÓöa*ô;@ºó£2J²�»ÛÈ‹H|9[âÂOaÉ=7§´“#�µn–g?{{;k˜çu'G‡<ÇÈ=1¸¼‡ûMËàç[�æ
üÈÌʲ>¨AßüÛƒgY:?ò`ž×=Ýݬ“«®›ôàSÙ+Oø
´ïË>gÿš>ÖNÁõåÏ`ןCL|£û?­ÿÃ1'N¤|ùò%"üˆÈ½a2™X³l>—ÂHJŒeÙç3¨åטf­+quQ¨s€~T¦ÂRCà*s‰šœQø)^èÕù¦B-UÊžƼp_”µk»VÖåy¥è°µµeذaª¹k¡Ç÷óË_�Ëšeó)©S-)üÙ2i6n>ªñÕ9_lÎGáç;¯66ùÞ™¹Ÿ7�JÞæÑu	lõX‰¬‡B	@
?EºLöúu‘SuÎS•
?"""R¤¸¸ºóH§^”)[Ža㦕Øz¸ës€~Š|™ÊC1êíß¡ª\ÀƒXÒøs¾“uÜõÿÍï5…¹—žð2eÊ–£b•ê%¶
d85¥bßFPûµQûµQû�bÐ>Lj """""RR˜lT"""""RR(‰ˆˆˆˆˆ�ˆˆˆˆˆˆ�ˆˆˆˆˆˆ�ˆˆˆˆˆˆ�ˆˆˆˆˆˆ�ˆˆˆˆˆˆ�ˆˆˆˆˆˆ�ˆˆˆˆˆˆ�ˆˆˆˆˆˆ�ˆˆˆˆˆˆ�ˆˆˆˆˆ(‰ˆˆˆˆˆ(‰ˆˆˆˆˆ(‰ˆˆˆˆˆ(‰ˆˆˆˆˆ(‰ˆˆˆˆˆ(‰ˆˆˆˆˆ(‰ˆˆˆˆˆ(‰ˆˆˆˆˆ(‰ˆˆˆˆˆ(‰ˆˆˆˆˆ�ˆˆˆˆˆˆ�ˆˆˆˆˆˆ�ˆˆˆˆˆˆ�ˆˆˆˆˆˆ�ˆˆˆˆˆHq	@®À›À! Hó�‡Ã=>׿@ðÖ_¼–ŸG�Là›ÔáÀ|œÏv;à�”¿�ÏÌ60O«¹ŠˆˆˆˆÜÈØ
üh”Ü€ÀPàWËëEU°%À´ìsm+4²„”Ö€m®íU€šÀ ¶ˆœo[Kà›¬QÊkÐ8
tÊÎ@= ?°ÞŠªd`¿%ÌÔʵíÀXc9ßò¹¶g‡¦@°Çòý¿ÕüDDDDDŠ^2ZÎpàG HŽÿº`¾RT�m˜‡³µÊµ­-p˜ƒùîσ¹¶?ŒyxÜV57‘¢€L˜ç´€ùNÉí|öó–`$¿/ðç0³N˜ïš¼<‚ù.J*°›?‡š9cž{t̲-ØhyoAÚn9ׇs�CÌw‡v[>»íuÛí0‹K³l‡Ï*k	Q—,�j�%8Þˆð_àr®ýo4gÈì-ï9Œ 簽ɖkb¼g9g“¥n,׿_˜çz]µ¼~Är
Üô#%"""%�AUP´˜
à/X:Ýû€',¡äf쀥–÷˜r}e�å
@ß[:ÚÙûì±tÐ�€-ùÄyˈë>óNAó\ŸD ôº€VóbïYþZKˆËU�k¹^Ëï3K[öÉ}Àò|Mi`o>û§ß泿-æ;qùÕ·øôºÜÉ7¨Ïì4íÇ1#õã$"""%ÉÊ HÛ•A‘.ª‰¢•}
"Ù3,ðìŽûQ`Ð�¼µt¢/c¾TÎÒ©o†yÕ¸‡s ,Ìw;|r%ì1–ã„,ôòÀKYï
@6˜ïŒ¤µ-¯µ³¼¿£åû‰˜ïŠdÏzÆR¾Yñ™ÿ²ìwó¥R@e`¦åÜsšq–×b¾ÃT
¨d	'ùí?�?çµ\,ïie	^™–ÏÍv£Eì,×5èj9Ž3æù_Ó1ß})QHµP2P¶j˜‡«-N\×*\$¶YBÀCq¼ì´™¼Ãõl0KËóyïlËg- æ;=Æë:úoaöçaùþ!Ëñ²‡®Í±”¿ëM>3{ík€_®Ï3«sÌÃØ®uóÙe>ûÿŠyˆaå|ÎÉÏê¦ÞBÊ�‡-aSDDDD¤H ‚~êÀ€>–Žuuà;ÌÌ´tÐí0¯¤v�?çÅü•Íä]EΨ„[:ä¹ýl	wµ
ðü¶YŽÙÖ²|v¼eûAÌËe·Å|×+÷üŸüØYêé,æUôr'Ô
ùìïcÙÿL>ûoÌgÿ:˜ï°ýa	6Y×…Ó㘇ôU»…ó7b¾ûäŠyÎÕ'˜¾h‚Æ¾ŠˆˆˆH`s—�Ã<,ìqò>CçV¥Ü'õµ×r.­1Ïÿ	Ä|7Æx]9÷cNVðB€˜»‘^ocC³'~ IDAT_[K@±µ|ÙX¾®-·zmöXÕ Ìö~¶Á@ýH‰ˆˆˆHI@`¾ó�½`�óˆÓ˜h~ÇÍÎcžãÓ Ÿí�,ŸwºÏ%ó]žü9fûuÛ�–@ÔÐòùüùüŸ¿:_òÞ­2X‚cîýÃ0?\Õ7Ÿý;ä³ÿià
æ¡z†|õÊu7k˜‡ÕMúbžå,$ï|/‘b€æ[¾ž°tÌ,áàÌ«˜9b^r9ÃÒ¹þŸ¥£¼ø‡¥cîŒy„OɹÔô��–ã,ż �à‰yQ�Q˜ççüR€õe´{ÌCÁ21/"p½–rŒåÖžÿcVa^”à[Ìw—ì1/j0�¼Ka1Ï*…y®UöƒV+bž£Ô-Ÿý—XêåGK rçÏ¡t]0SlÝ{â,ïkeÙ7[)à7àEÌ}u²Üö˜~¨¤$""""÷»‚Xa%ù/‹œýOΈÚóçòη²öË7ø\'KÀ(Œe°ÉU&æe¿swø=,ÁË„y¸œW®í7Z{·¾¶LþË`gï}²sã嫳ÈùÜ$GÌsŒr/ƒíf¹–ùÃ|¤'¹Ÿ³OAÝ‚ùY@?a^Ú:ÓÒa>ŽyùꦘW‚Ë–�yèÔPÌ$c^,à€åX[nñsS�'�·1¯:—†yéë_1Q›w*m�ås±œSîám	˜
æ%££oá˜)–2ˆÀ¼ÂÛ~ÌÃÒÖæ³2æ;9Ÿ‘–ý÷YBÒ^KI¸nÿ,ÌA}ó"	±–º:‹ùîÓÓ˜šÈv
ó�¹í–²e‡ä4Ëç.°œ[ª¥,{€Ñ˜ï¼‰ˆˆˆˆÜß)HUPlØb^©.óp4É•}€Š¦Às˜—¯¶Ç¼ìøbÌw6Q8\ˆˆˆˆˆ(I¡ø‘Ï·ÒrÔ"""""
@ÅJ`pó⑘@h ªQ‘€4ODDDDDDJ QQQQQQQQQQ ‘âÊîN°2(Ò¤j,¾z´ò2¨}ÈÝj""""…Mw€DDDDDDHDDDDDDHDDDDDDHDDDDDDHDDDDDD¨ �F>z÷%^èù	qÑy¾—’íNÛÇ­ì¯6'"""RtØäÁbÙ¶a%»¶üŒÉ˜…_ÃhÔì!¶ÁÆæÞe­ÕËæ³øã7(íæI-ÿFÔoÒ’mŸÀ»zmµ‚Br¿¶Qºm™,žó;7ÿH».}p+S–#Á;qrv¡a³‡î‹“íÚwå+r1ì4ß-úˆMk–2åÓÕ”õ¬ –p—…ö!""""
@·,!.š¿o¡M‡nyå]llmé=hi×R1ÌÏJÌHO#x×fvþú¡Çà×ð:uïO�€@>~ÿ¼*W£Cç>¬ýþŽÚ߬ìÚÄöM«¸vš¦-ÛñHÇg¨æ[÷¶ËøH§§©Q»>F£çÒ®¬þf.q1‘”õ¬@ÔåpöìØÀÉ�`Ο>FÓ¥}—>T«Q£ÑxÃòÙÙÙ«ñöq³koýA±µ#êò–/þˆã‡÷ÐîÉgéÐ¥/ŽNÎù3#=Í\®�«¸~–m;Ѿs*TòVc¹‡
lÜ‘ƒ£Î¥]9uìçNÃd2akk‡siWL&¿¬X´‰ƒH½šB‡§ú’”‡�})ë16¯ý–Yo� .ö
�>Ñ6üø?¦MˆÉh¤uû®ìݱ‘·^~–ðs§þvY¯&'s%wÏJ¸•ñ0¿–’Ħ5ßP¡RUZ´íĺK˜ñú0ãcoR>[µ bÐ>nåÚÇEG²hÎÛ”q÷ÄÉÙ•ÏgObÓOK1™LyŽg2™Ø¸æ¦Oú'¶vö´nß• ßÖòáÛ#sSDDDD
_�ÝrqsgÀÈ×™õæp&½Ø�]ûñD�TµÌ±Iˆ‹æÇå©Û cÞšCi7ŒFcŽc$ÅÅðÏQoÑòÑÎâ¢Yñõ<ê5iÃèɳqrv¡i‹G™4¼;[7¬àC'ÜVÇhoý·W_^yk弪àSÓŸ™Ÿ¯ÃÎÞ|GÇÕÍ�ÅsÞ":ê".nîù–OŠGû¸•kðüÈ×ñ«ßŒö]ú0aXWÖ¯úŠ]úRÊÁ1ÇñãcXñõ–¤¸Ú=ÑËúWÿì‰ïÙ]g7ê6hf
	ñ1$ÅÅÐþÉÞ8:•¶—j”-çÅá};è;ø_ncò|×¾#ð,_‰˜+—Y·b	¿¬X‚�¯îå9{*„mVphï6.‡Ÿ#=-€¬¬,ëûs—OŠGû¸•kïZÖ“
«PÖÓ¿ú�=°›«)‰yPBœ¹\;7­d禕9¶ÅÅ\QC).À»zmŒ|ƒÖí»òÁø�|óÙš<ˆ	óP¡›…[{{ìì®+’é/>ì6sHö ïu™ûÁË<Ô¡~
à½õ§v½Æ¼5{)e=+°~õÿøtÚ«7/Ÿùös%â–®}VFFŽán&“	[ûüçeŸK“;иùÃ9¶yi�ˆˆˆÈ=U`s€’∋‰²vb«×ô§rµš¤¦$“™™‰‹«;Î.n	ÞIjJ²¥i$%9ñ†Çt÷(OÙr^„ØÍµTó_å#/þA\t$
[ßÑcV&ééÄÇ^!>æ2¾uàîQžŒŒtÎ…Uë(@÷kû¸Õk59‘ˆ‹a€y>ÐÉ£ûqqqù´[Þr•-‡kYO�œy¼Ûstí3”®}†Ò¥×`ê5j¡Æ """rØíŒ˜+LÒ™&-ÛQ˯!ÎsúØzö…siWœœ]hߥ?,™Í‡ÿE@ãœ=ÊÃ{ҤţùÓµLYžî?ŠySÇ2çý1øÖmÀÖõ+°wp¤M‡î†|'¡ßÈ–u?p$8ˆØ˜H~ù~îž•¨Ð{ûR¸{Vâ§åŸcÌÊâRøYâbÔ:
ÐýÚ>ÜÊ”½¥kŸž–¼©ãx¸cOŽì"&âz¾:
G§<ŸáæîÉ3ýG3Æ8®¥^¥A`ÒÒ®rxïvƼ=—ò–yg""""Rø
ì�G¹
®Á``eP¤®ÒMôhåuWë¿ D­Ô…*dáxèwöî&�Þªh)™HDîü2¨ÀYU„ˆˆˆH>4H¤˜…QQø … Qø‘ÛdŠ]n0Å.W[HD~DDDJତ$"
?"""%…zVž�ˆ(üˆˆˆ”& 0ª*€DDáGDD¤$¸fðèmR5ÜŸô T…) –à£ðsÓ … QøQ… ¹×.]ºÄ”)S¤k×®,[¶Œ´´4UŒÂ�ˆˆˆ{6OÑÒÙE-©~ï$%%ñâ‹/bkkËŽ;øä“OX¸p!K–,Ád2ýå{ضm›*RáGDDDÔT%¥¤ø½uôèQ¶lÙB·nÝprr¢J•*ôïߟ¹sç’’’BXX^^^ÜÜÜ>|8ááá�F^{í5Ž;ÆÃ?L@@IIIªP… uÀï_qqq”)Sëkžžž$''“’’b}íÛo¿%22’2eÊ0{öl¦N�J½zõغu+G�ÅÕÕUªð#"""7aŠ]®áj%=©~o•-[–„„’““­¯ÅÄÄàââBéÒ¥­¯ùûûãääD£F�غu+F£R¬ð#"""÷C(ÒÃS‹XRüÞ
 mÛ¶¬^½šÔÔT.]ºÄW_}ň#rÔÿ‰'HMMåðáômÛlll°³³ãêÕ«ªH…¹ƒ£*(AHð{ËÕÕ•ùóç“‘‘Áƒ>È/¼ÀàÁƒ0`ß?‹½{÷ÆËË‹ØØXÆŒƒ��
ÎÎÎŒ1‚‘#GÒµkW²²²T¡
?"""r4$®h¥TÓ¦USö"/^dñâÅ,_¾œ*UªðÜsÏѳgO	£yóæDEEáêêJß¾}™x”óÂÁÁƒÁ@ùŠUINŒ'ìÌ	L¦¼IÆ`óç©—óªLrbùîwVìŒ`eP$žê{Gua4Y½z5�:u¢Q£F4ˆÐÐâ³â\Dt,ÿYòퟅþÿz—U·‘–~ïﺔ«™©ß"""""EH�
�3�¬Y³†O>ù„Ë—/Ó´iS&MšDíÚµïZá	ÙÄò/>$6:’VíºÐö±ÔôkÈðñÓXöÅ,B‚wÒoغõ–ïqjù7bÈØ÷X0k)É	tèÒ�Z?FB|t¾û×®×ÿ†0¤{3ÒÓRòlŸùÆJ9”¦mÇžôøJž;Pµü1hô[,œý!Á;IOKaÊüµø5hö·ë",,Œ±cÇòý÷ßS¯^=BBBÈÌ,�óä«©Œ›:—�ê±vátâ“õö‡$&_¥÷Žy“ªwÅ
\³è/�}«ûå§‚_aáw~~>Þå	YxWê.)9•{�Of
£m«zú�'"""%žó@/ÃÃd2™8wî:tÈÑwvvÆßßÿïÎ``eP¤®ÒMôhå…ÉdâÔ©S<ôÐC,^¼˜6mÚàêêš#œþòË/,\¸�_ý•wÞy‡W^yå–êÿÛ€)2hå 8ä$½F¿É†ÅR˧
Ë~ÚħKW³vátb“è8p,1‰æ0ºþóéx–-ÃSC&ðÝœw¨Qµ2߯û�O—®æø9ó0³éã†1 G'Â#¢¬û9::X�SÚ¡�µiÆ#Rµby¢bâèš28Ç1jûVâË·2{Þ3/ ñé¬6èq23³n¸-Ëhä§uûX¸d#ñ	)Œx¡OwmÉØÉ‹˜÷ù&êÕ­Äî�Squq*˜_½¢}ˆˆˆˆ&S�
�ËÈÈ 99™ˆˆÒÓÓ	´†£ÑÈÚµkéÞ½;nnn|ôÑGªú\ŒÆ,^y¾½uuº­¼8´wÛ-¿ß××—>ø€¡C‡R¥JFŽIXXû÷ïçÅ_dÒ¤IÄÇÇßRø¹ŸÄ'%ãæâLigGëke˸‘’šÊÕkiÖ׿¼>š‹Û¿§±Î»Ž‡Ožaêü¯YðÞxÿø9þ5ªÞôóæÿ{‡×.&9%•í{PÁ³,{W|Æ™_—ññìç÷æÂ/FÿeÙýjWáø©DDÅó{ð)bb‰º’@Ðîã´~Лœù!øÐYÞø`)˾x•KÇçÓ°^•<Û–~>–KÇrnÛðc'/â?S³ò˜öÑJ~ßÊÔ·úS¯n%¶þô6Gwý§ÀÂ�ˆˆˆHQU`Cà®ï€'$$пÆ�����µþÃ?°bÅ
lllT󹨨ØòÑ—ép;;;L¿~ý

e̘1|ýõ×Lœ8‘°°0<<ó
Ëœ¢ëçí9Á¯ûö´~
{6½OOç[*»£ƒ=]:5cÖÜÕ´¬Cƒz>Ìúïjìlm¨^­Bžý+Wò 6>™°ð+\‰NäðѰ|·EE%䨿]¥ujVàtðÇc¾Å»œAÿh‡�Á€�­
WSÓôC""""R�¨8wÀÿލˆp=Õ˜ðó¡9þ}³ýî„““Û¶m£[·nâííMŸ>}hÚ´)óæÍãÝwߥL™2En¢‹³3_IFf�þù*c>˜C¿®�Ó§s»|@È­aÝšLú^˜4�†][çݎǺ֠uóºÔhô"Í;L&*æê-½Ï`0ЬqM¶ï
%À¿>Þ幓È�5Å¥´cžý›6ôå�‰}yvÐL*ù
µÎõɽ­r½a9¶6®Éô?ϘI_P¦ÚsI×ö­oé=÷ãCN¯D'Ògð,^ügGzukuo~yh)zL6ÈËË‹wÞy'ßm¶¶¶<õÔS<õÔSŶ&S¯&ó¯>Áð	өߤ%*z³hÍAkÈ‘{'Ëhäñ�c9~™†~5éñx[Ú·
,Ðð“•e¤yûqì?üçµÞ´òuÚ?ܰàÎÃò¡g"lìKŸžñD‡¦ºÀ""""·Á®(Þh4¼k3ëWý�#û¶[Ÿõc4Ù³}ëV}ÅÕä$Ú=Ù›GŸìEB\4cv$%1†R¥©S?ß:õ9zp7•ªÖ`àÈ7ð¬P‰¨ˆðû=Üéiz
|…ò^Uˆ‹‰bÜ�Î$ÅÅP§~ O>=�æudÉÜw¹ôÇ)ÞÙ�ÊÕêð¯w?å�±ÿ°ÞÊÈHç¥~mò/÷ùìݱ�õ«¾"9)�ÎÏ¢MûnØÚÙ©¥Þ[6yûÃþnçÎ�­­
Á[fÝÝó(„Ï)îŠôŠgNfÞÔqôø2Ë~=m}Ðééã™?sƒ_ú7“§/æ×Ÿ—³wÇFëûƽÿ9ó–qùâyÜ=*ðîœïˆ�½Â‘ýA9Ž?ö�|¹î(Î.n¬^ú)&“‘²žølÅ^–ýz†¾ô6Ÿü6±W"0ò
*W«Ã»sW1wÙvJ»ºå)o~ÇËq>'±ð£7úêûLž¾˜ï¿œÃñ#{ÕJï�ûqØ›ˆˆˆˆ”ðtåò\ÜÜ©^«ß§y	7w*TöÆÍÝߺ
¸~“	ìíKQµzmJ»¸âäTšêµëáà茻GyÒÓRs¿Z�:888Q£v!ûƒ0f‰‹ŽdñÜwÞ»c´#&⮦$ÝRyó;^Žó‰¸HLÄïÕ‚��¸pî8Îx½………áííÍñãÇsü[~DDDD€î‚‚ꀗ¯X•äÄxÂΜȱ@9¯Ê$'Æuù‰	qœ=y„ÊÞ¾d/v}XÊ^ò8÷Ý€çCIO»ÆùÓÇhÒ[N„ìcïö
Lÿl-Ó?[Gi7OL&ó1mlmI¿–zÃòæw¼ë•óªŒW_>ùîwVìŒ`eP$žê«Vªð#""""¤HO.©é×�áã§±ì‹Y„ï´Îªå߈!cßcÁ¬I¤$'СK?hý	ñÑ·uü™o¡”CiÚvìIï�¯`0ØP»^ü>À�îÍHOK±îëàèDçg1oÆ||ý2æ½[:Þõjù7bÐè·X8û
B‚w’ž–”ùkñkÐL-UáGDDDD
@�-ƒ};ÂÂÂhÓ¦
6lÀÙÙÙúoÿœ…»GË`ßéÒÔ…){ì°°0š7oNTT{÷î¥I“&¬Y³†�˜˜ÈóÏ?Oÿþý‰ŠŠºåú§˜-ƒ­ðS€¿<´¶ˆˆˆ=¦†——ƒƒÁÀ¾}ûÈÊÊbÕªU<ùä“´iÓ†�ššªj¿‹–,YBFFÍš5#88˜Ñ£G3sæLV¬XÁ—_~ÉO?ý¤JRø)±
|Ü’%KèׯvvvìÙ³‡Ñ£G³~ýzÊ•+GÏž=)[¶,Í›7¿¿;Ä×=ç()Uª���ØY–ÍÇÃÃ5jààà@“&M

å�PËWø)‘
t„›uÀË•+gí€ßî�9¹�jóç%õöö&66–óçÏÃ�¨]»vöÐ6……‘§Àïݨ^®\98@Û¶mÕ/$���üç?ÿáå—_&>>žAƒѹsg®\¹¢ð£ð#"""¢¤xÑåããCxxxŽ×lmméÙ³'={ö¼é¾¹ß§ð#""""ÅÕ=Yî–w�V�+J²W�»[õO1[Ná§Û‡V�‘¢Çd£:�’BáGDDDD€DáGDDDD€¤h	ÃÛÛ›ãÇ�«2~DDDDän uÀEáGDDDDJLQø )4éééÔ«W777†n]Þ:""‚š5kâææÆã�?Ί+0�„……áåå…Á`À`0°oß>…QR¼èøöÛo‰ŒŒ¤L™2Ìž=£ÑHÅŠ9sæ			̘1ƒq¯^}±sIDATãÆåxöÏ’%KÈÈÈ Y³f
?""""¢¤xÑáïï�““�5bëÖ­�F._¾Ìk¯½†ŸŸ�7æìÙ³$&&PªT)±³³+ç¯ð#""""…€Jzü^;qâ©©©>|˜¶mÛbccî]»X³f
AAAìÙ³‡
*äl6Åc$¤Â�ˆˆˆˆz*ÉðûAïÞ½ñòò"66–1cÆ`ccCóæÍiݺ55jÔ yóæDEE»óVø‘[Qà·]z÷î�««+}ûöeòäÉy:àIIIªõ»ÀÇLJÈÈÈ|·U­Z•°`Á‚<Û®Ѝð#""""Å�0Yþûw™L&ÓÝ)œÁÀÊ H]¥›èÑÊ‹»YÿwØ6L‘A+~Šã/�ÞÑ>DDDD
“IcϤÈRø‘Û¥$
?""""¢$¢ð#""""
@"
?""""¢$¢ð#""""
@"
?""""¢$¢ð#""""
@"
?""""¢$
?"""""
@¢ð#""""¢$
?"""""
@¢ð#""""
@"
?""""¢$¢ð#""""
@"
?""""R4“å¿—IÕXìÛjr—Ú‡ˆˆˆHa2©ƒ*"""""%&iœˆˆˆˆˆ”
@"""""¢$"""""¢$"""""¢$"""""¢$"""""¢$"""""¢$"""""¢$"""""¢$"""""¢$"""""¢$"""""
@""""""
@""""""Eœ0©DDDDD¤$ø? 5è�‚IEND®B`‚urwid-urwid-69907e4/docs/manual/images/urwid_widgets_2.xcf000066400000000000000000006232021526073734300235610ustar00rootroot00000000000000gimp xcf v011@–B³üB³üÓW
Unnamed #12ÏDOÜC´�DOÜC´�DOÜC´�DOäC³DOäC³DOäC³Unnamed #11È?C´P?C´P?C´P?C³4?C³4?C³4DOádC´zoDOádC´tßDOádC´oNDOä-C³7§DOä-C³7§DOä-C³7§Unnamed #10·>àC³°>àC³°>àC³°Unnamed #9A	Cx°Cý@Cx°Cý@Cx°Cý@C‡@Cý@C‡@Cý@C‡@Cý@Cx Cý8Cx Cý8Cx Cý8Unnamed #8=Cx€Cñ@Cx€Cñ@Cx€Cñ@C‡8Cñ8C‡8Cñ8C‡8Cñ8Unnamed #7;Cx€Cñ@Cx€Cñ@Cx€Cñ@C‡(Cñ@C‡(Cñ@C‡(Cñ@Unnamed #6/	C‡ÈC÷ÈC‡ÈC÷ÈC‡ÈC÷ÈC‡¨DôC‡¨DôC‡¨DôCi�DäCi�DäCi�DäUnnamed #5-C‰ÀCí�C‰ÀCí�C‰ÀCí�C‹PCî@C‹PCî@C‹PCî@Unnamed #4'	C‡ÈCñhC‡ÈCñhC‡ÈCñhC‡°DC‡°DC‡°DCŠ(CøPCŠ(CøPCŠ(CøPUnnamed #3!	C~DÜC~DÜC~DÜC‡˜DÜC‡˜DÜC‡˜DÜC‡¨Cý¨C‡¨Cý¨C‡¨Cý¨CŠ0Cö@CŠ0Cö@CŠ0Cö@Unnamed #2Cw`CåØCw`CåØCw`CåØC‡ÀCåàC‡ÀCåàC‡ÀCåàUnnamed #1	C‡ÀCÚ0C‡ÀCÚ0C‡ÀCÚ0C‡¸CýÈC‡¸CýÈC‡¸CýÈC�hCèàC�hCèàC�hCèàUnnamedCPDäCPDäCPDäC€CûøC€CûøC€CûøËgimp-image-grid¬(style solid)
(fgcolor (color-rgba 0 0 0 1))
(bgcolor (color-rgba 1 1 1 1))
(xspacing 10)
(yspacing 10)
(spacing-unit inches)
(xoffset 0)
(yoffset 0)
(offset-unit inches)
gimp-image-metadataã

  8 8 8
  540
  832
  1
  2
  90/1
  90/1
  1
  1

1v?R#Ã(-‚25µ:Ç?TDaI1RV¯\Na�5Overlayÿ!?€	"
 
ºâ%$ÿÿÿÿ#ÿÿÿÿgimp-text-layerø(text "Overlay")
(font "Sans-serif Bold")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0 0 0))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)

M5
i5
�$$$õüáû/ù&£çûí±7üáû/÷/íÿè¾Þþ÷Hüáû/Ƕÿ¸
—þÕÍþYaþÅ”ç÷ÈBáÇ@Ñôáû/B´äøÒVÉþrqþÄõü>Îøû/tþ©±þk²ÿäÂøó+áûóâ°
áû/\÷ÍËþø+fþÄÁþg-úøÎäþM÷íñô.ûålþ‡áþ–áû/
ºÿaíø"÷ñ-úøòäþM¸ýPXþ®Wþÿá§áü8áû/bÒ÷þÿÿg˜þrlþ¦ôü>ñøû.]þ ¨ýSXþí¸¦{áû/áû/8üê4¬ÿg6ûüýIµÿ·	—þÔîÜåè/ûç
áû/áû/bÿÆÙÿgÉôóß-ìÿç½ÞþöGÚ¢üþ—®ÿí¼Æò>áû/áû/@üýÉåðÿgfþÿ‡ù&£çüí²7üGýü=á‡Þúï°áû/áû/‡ñô Cûgøú,0üdýÃ.ûfÄûûK/üúÓUm=	GridFlowÿ!?€	"
 
2â%$ÿÿÿÿ#ÿÿÿÿgimp-text-layerù(text "GridFlow")
(font "Sans-serif Bold")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0 0 0))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)
”=°=ÈÄÄÄüÇë-üPþÁüáû/ø‚Ô÷ñÒ„ü„¤úPþÁ¹ÿúóáû/øÖÿúÉÈï�ðNþÁ¹ÿи¸¯áû/ß¡ÿÚ&áÇ@Ñôáû/'ÁöÏxüÁ¹ÿWäáû/
‘æ÷ÎW·þWDýýFTþ²ñýLéáûóâ°
áû/ÑÿêÅöþÁ¹ÿWŒáû/²ÿçÂøüUsÿ–�ùü€•þm,ú÷ª¸¸²áþ–áû/7üò!tþÁ¹ÿи¸†áû/.ûñdþË4ûѽÈкÒú++ú÷ìÿÿ÷áü8áû/XþÆ7ûÁ¹ÿŹáû/VþÈ%úôáø'ð�–ì'øÛõü:ø÷áû/áû/XþÅ4ûÁ¹ÿWÇáû/WþÈ%úô	¢ý{ûU]û~þ™¸ÿ¹
ø÷áû/áû/9üðmþÁ¹ÿWÇáû/+úñdþÌ]þÅù")úËþT0ïÿæ»Îþ÷áû/áû/ÔÿéÃóþÁ¹ÿWéáû/ªÿçÁøý[øûÝçüõá*§êüóÒ”áû/áû/+ÆøÌ?îÁ¹ÿWéáû/ŠãùÓ\Îÿ§¸ÿÄ23ListBoxÿ!?€	"
 
ªâ%$ÿÿÿÿ#ÿÿÿÿgimp-text-layerø(text "ListBox")
(font "Sans-serif Bold")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0 0 0))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)
-3I3aüüüÔüÇë-'ý¹ÿ_ü„¤óz3¹ÿÿýîÁSý¹ÿ_òPýg¹ÿÒ¹Üþú3ý¹ÿ_óáû/tÜùæ¦Nåÿዹÿ_Ñÿa
‘æ÷ÎW{þлþ•¹ÿ_Ñáû/Cý÷Áàï{èÿÔ¸d¹ÿ_&äü=²ÿçÂøüU	ÑþmTýá¹ÿ_íáû/QþÔ¯ÿg¹ÿîú~.ûñdþË>úëáýTý¹ÿ_×áû/
Äþõ�$¯ÿg¹ÿÒ¸Òþè0VþÈ%úô
™ÿÿ®ý¹ÿ_×áû/ZÌþî%¯ÿg¹ÿ_§ÿ—WþÈ%úô	´ÿÿÃý¹ÿ_×áû/ºÿd«ÿp¹ÿ_«ÿ +úñdþÌ[ýÞÏþmý¹ÿÞÌÒXáû/[÷É¿øü?‡ÿ츆¹ÿÒ¸ÐþüMªÿçÁøý[äýS=ûí ¹ÿÔoáû/5Ê÷úØj»ùð{¹ÿÿþ÷ÊUŠãùÓ\�ÿ¸¡ÿ¯ÿ,Frameÿ!?€	"
 
"â%$ÿÿÿÿ#ÿÿÿÿgimp-text-layerö(text "Frame")
(font "Sans-serif Bold")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0 0 0))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)
W,s,‹pppÝÿ¹ÿþó$ù¹ÿи¸¯$ý¹ÿWØáÇ@ÑôB´äøÒVáÐiØöÌ?“äô¨”ç÷ÈB¹ÿW®áûóâ°\÷ÍËþø+áÿûÆðÿÿðÅûþ{²ÿäÂøó+¹ÿи¸†áþ–
ºÿaáÿ€Pýü?�ÿ¬.ûålþ‡¹ÿà¹áü8bÒ÷þÿÿgáü>*úðeÿ²Wþÿû§¹ÿWááû/8üê4¬ÿgáû0(úècÿ²Xþí¸û{¹ÿWØáû/bÿÆÙÿgáû/(úècÿ²/ûç
¹ÿWØáû/@üýÉåðÿgáû/(úècÿ²®ÿí¼Æò>¹ÿWÜáû/‡ñô Cûgáû/(úècÿ²‡Þúï°ÜPileÿ!?€	"
 
šâ%$ÿÿÿÿ#ÿÿÿÿgimp-text-layerõ(text "Pile")
(font "Sans-serif Bold")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0 0 0))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)
:RôôôløÇë-áû/ï¹ÿÿü褄¤áû/÷¹ÿÒ¼íÿÉüáû/¢¹ÿ_"ùù!áû/áû/”ç÷ÈB¹ÿ_)úùáû/áû/²ÿäÂøó+¹ÿÒÀóÿ¸áû/áû/.ûålþ‡¹ÿÿýä–áû/áû/Wþÿû§¹ÿ_õáû/áû/Xþí¸û{¹ÿ_ìáû/áû//ûç
¹ÿ_ìáû/áû/®ÿí¼Æò>¹ÿ_ðáû/áû/‡Þúï°|=Columnsÿ!?€	"
 
â%$ÿÿÿÿ#ÿÿÿÿgimp-text-layerø(text "Columns")
(font "Sans-serif Bold")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0 0 0))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)
!n=!Š=!¢ÄÄÄüáû/(ùœæøÞŸ$üáû/'ø ãÿçÂèö!üáû/&¿¦ÿ»4
‘æ÷ÎWáû/ìú(óù!áÐiØöÌ?“äô¨áÊQÖøÎ:tÜùæ¦ñü;ȲÿçÂøüUáû/ìú(óù!áÿûÆðÿÿðÅûþ{áüöÆâÿÞCý÷Áàï,ú÷È.ûñdþËáû/ìú(óù!áÿ€Pýü?�ÿ¬áÿ� ùù!QþÔ-ú÷
ÈVþÈ%úô
áû/ìú(óù!áü>*úðeÿ²áü<êú(
Äþõ�$öû4€ëWþÈ%úô	áû/ìú)÷ù!áû0(úècÿ²áû/èú(ZÌþî%¾ÿ«+úñdþÌáû/æýDCüù!áû/(úècÿ²áû/èú(ºÿd:ôþã½ÒôªÿçÁøý[áû/µÿÚ™æÿù!áû/(úècÿ²áû/èú([÷É¿øü?6µðüé©
ŠãùÓ\áû/#½øè†£ù!áû/(úècÿ²áû/èú(5Ê÷úØj17LineBoxÿ!?€	"
 
22%$ÿÿÿÿ#ÿÿÿÿJgimp-text-layerø(text "LineBox")
(font "Sans-serif Bold")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0 0 0))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)
%ß7%û7&LLLäüÇë-+ý¹ÿ_ü„¤ø¹ÿÿýîÁSý¹ÿ_÷¹ÿÒ¹Üþú3ý¹ÿ_Íáû/áÊQÖøÎ:”ç÷ÈB¹ÿ_Ñÿa
‘æ÷ÎW{þлþ•¹ÿ_Íáû/áüöÆâÿÞ²ÿäÂøó+¹ÿ_&äü=²ÿçÂøüU	ÑþmTýá¹ÿ_éáû/áÿ� ùù!.ûålþ‡¹ÿîú~.ûñdþË>úëáýTý¹ÿ_ñáû/áü<êú(Wþÿç§¹ÿÒ¸Òþè0VþÈ%úô
™ÿÿ®ý¹ÿ_ðáû/áû/èú(Xþí¸ç{¹ÿ_§ÿ—WþÈ%úô	´ÿÿÃý¹ÿ_Óáû/áû/èú(/ûç
¹ÿ_«ÿ +úñdþÌ[ýÞÏþmý¹ÿÞÌÎXáû/áû/èú(®ÿí¼Æò>¹ÿÒ¸ÐþüMªÿçÁøý[äýS=ûí ¹ÿÐoáû/áû/èú(‡Þúï°¹ÿÿþ÷ÊUŠãùÓ\�ÿ¸¡ÿ¯RBoxAdapterÿ!?€	"
 
ª2%$ÿÿÿÿ#ÿÿÿÿIgimp-text-layerû(text "BoxAdapter")
(font "Sans-serif Bold")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0 0 0))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)
*&R*J-vR*j,Ó(üPþÁù¹ÿÿýîÁSü¤ÿýMüPþÁõz¹ÿÒ¹Üþú3ûíõþ¦üNþÁÝPý¹ÿ_Ñÿa
‘æ÷ÎW{þлþ•Zþ�ãî¢'ÁöÏxüÁB´äøÒVááFÖö´Nåÿ¹ÿ_&äü=²ÿçÂøüU	ÑþmTýá²ýLžþ\ÑÿêÅöþÁ\÷ÍËþø+áþñÂìÿ´{èÿ¹ÿ€•ú~.ûñdþË>úëáýTôîQý´7üò!tþÁ
ºÿaáþn0úø¯ÿ¹ÿÒ¸Òþè0VþÈ%úô
™ÿÿ®iþ°ðõXþÆ7ûÁbÒ÷þÿÿgáû7äü7¯ÿ¹ÿ_§ÿ—WþÈ%úô	´ÿÿÃÀÿ€¼þkXþÅ4ûÁ8üê4¬ÿgáü9åû7¯ÿ¹ÿ_«ÿ +úñdþÌ[ýÞÏþm#ùýÕÔÔáþÂ9üðmþÁbÿÆÙÿgáþu0úö«ÿ¹ÿÒ¸ÐþüMªÿçÁøý[äýS=ûí wþÆ$ùù%ÔÿéÃóþÁ@üýÉåðÿgáÿùÂëÿ®‡ÿ¹ÿÿþ÷ÊUŠãùÓ\�ÿ¸¡ÿ²ÐþrßÇþz+ÆøÌ?îÁ‡ñô Cûgáúyß÷¯»3üáû/;üáû/;üáû/ˆhhhYÿ3ÿgÿÇ‹”ç÷ÈBáÇ@ÑôÔ¸d²ÿäÂøó+áûóâ°g.ûålþ‡áþ–gWþÿû§áü8úgXþí¸û{áû/ñp/ûç
áû/ñ츆®ÿí¼Æò>áû/ñùð{‡Þúï°áû/\)
:AttrMapÿ!?€	"
 
"2%$ÿÿÿÿ#ÿÿÿÿHgimp-text-layerø(text "AttrMap")
(font "Sans-serif Bold")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0 0 0))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)
/ž:/º:/Òˆˆˆ%ü¤ÿýMýz3ýz3û¹ÿýLû øÿèûíõþ¦ýPýgýPýgû¹ÿÿ™ûmþÿèöZþ�ãîNåÿý‹Nåÿ“‹áÇ@Ñô
¹ûßá¿èôèB´äøÒVááFÖö´²ýLžþ\{èÿÔ¸d{èÿÔ¸dáûóâ°¹ùžü;÷�óè\÷ÍËþø+áþñÂìÿ´ôîQý´¯ÿgɯÿgáþ–¹ú\ûˆjüN÷è
ºÿaáþn0úøiþ°ðõ¯ÿgÕ¯ÿgáü8¹ú1ÞÓ¼ã÷èbÒ÷þÿÿgáû7äü7Àÿùþk¯ÿgɯÿgáû/¹ú,–úGö—÷è8üê4¬ÿgáü9åû7#ùýÕÔÔáþ«ÿpš«ÿpáû/¹ú,Iý×üE÷èbÿÆÙÿgáþu0úöwþÆ$ùù%‡ÿ츆‡ÿ츆áû/¹ú,
ëÿä	÷è@üýÉåðÿgáÿùÂëÿ®ÎþrÍÇþz»ùð{»ùð{áû/¹ú,©ÿ™÷è‡ñô Cûgáúyß÷¯1üáû/5üáû/5üáû/x#Fillerÿ!?€	"
 
š2%$ÿÿÿÿ#ÿÿÿÿGgimp-text-layer÷(text "Filler")
(font "Sans-serif Bold")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0 0 0))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)
4&#4B#4Z¼¼¼”ôÇë-áû/áû/þ¹ÿòó„¤áû/áû/ø¹ÿи¸¯øáû/áû/ü¹ÿWááû/áû/áû/”ç÷ÈBáÇ@Ñô¹ÿWÀáû/áû/áû/²ÿäÂøó+áûóâ°¹ÿи¸†áû/áû/áû/.ûålþ‡áþ–¹ÿð¹áû/áû/áû/Wþÿû§áü8ü¹ÿWñáû/áû/áû/Xþí¸û{áû/ü¹ÿWèáû/áû/áû//ûç
áû/ü¹ÿWèáû/áû/áû/®ÿí¼Æò>áû/ü¹ÿWèáû/áû/áû/‡Þúï°áû/²>
ScrollBarÿ!?€	"
 
–%$ÿÿÿÿ#ÿÿÿÿ•�gimp-text-layers(markup "ScrollBar")
(font "Sans-serif Bold")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0 0 0))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)
8N>8j>8‚ØØØøáû/áû/ù*´ñòÎðáû/áû/¹ÿÿýîÁSø
àþØÈòÂïáû/áû/¹ÿÒ¹Üþú3½:üß	’äúápáÇ@Ñô
‘æ÷ÎWáû/áû/¹ÿ_ÑÿaB´äøÒVáÇ@Ñô&ùøb¢¸ÿëÁìeáûóâ°²ÿçÂøüUáû/áû/¹ÿ_&äü=\÷ÍËþø+áûóâ°ŽþþÖR1ûó"áþ–.ûñdþËáû/áû/¹ÿðú~
ºÿaáþ–õSÑþþ†WþÉÔáü8VþÈ%úô
áû/áû/¹ÿÒ¸Òþè0bÒ÷þÿÿgáü8÷güõXþÈÔáû/WþÈ%úô	áû/áû/¹ÿ_§ÿ—8üê4¬ÿgáû/Å)4ñù"8üò !áû/+úñdþÌáû/áû/¹ÿ_«ÿ bÿÆÙÿgáû/Å[þèÁÞþÅËÿë½ã‡áû/ªÿçÁøý[áû/áû/¹ÿÒ¸ÐþüM@üýÉåðÿgáû/í,³èú眨ïû×Láû/ÛŠãùÓ\áû/áû/¹ÿÿþ÷ÊU‡ñô Cûgáû/99Paddingÿ!?€	"
 
2%$ÿÿÿÿ#ÿÿÿÿFgimp-text-layerø(text "Padding")
(font "Sans-serif Bold")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0 0 0))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)
<ã9<ÿ9=tttúüPþÁøPþÁÇë-ø¹ÿÿüè¤
üPþÁøPþÁ„¤÷¹ÿÒ¼íÿÉüNþÁüNþÁ€è¹ÿ_"ùù!B´äøÒV'ÁöÏxüÁ'ÁöÏxüÁáû/áÊQÖøÎ:'Â÷ÎOøÁ¹ÿ_)úù\÷ÍËþø+ÑÿêÅöþÁÑÿêÅöþÁáû/áüöÆâÿÞÎÿæÄöþÁ¹ÿÒÀóÿ¸
ºÿa7üò!tþÁ7üò!tþÁáû/áÿ� ùù!6ûñtþÁ¹ÿÿýä–bÒ÷þÿÿgXþÆ7ûÁXþÆ7ûÁáû/áü<êú(XþÈ6ûÁ¹ÿ_Ì8üê4¬ÿgXþÅ4ûÁXþÅ4ûÁáû/áû/èú(XþÈ3ûÁ¹ÿ_ÌbÿÆÙÿg9üðmþÁ9üðmþÁáû/áû/èú(8üñhþÁ¹ÿ_Ì@üýÉåðÿgÔÿéÃóþÁÔÿéÃóþÁáû/áû/èú(ÔÿåÂõþÁ¹ÿ_Їñô Cûg+ÆøÌ?îÁ+ÆøÌ?îÁáû/áû/èú(,ÈùÌ{þ¿0ÿ
ý{ÿª0øߨÁÆûüM0ù’áûöËXsCScrollableÿ!?€	"
 
š–%$ÿÿÿÿ#ÿÿÿÿ¨gimp-text-layerû(text "Scrollable")
(font "Sans-serif Bold")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0 0 0))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)
AvCAšDUCAºD2øáû/áû/üáû/üáû/ù*´ñòÎøáû/áû/üáû/üáû/ø
àþØÈòÂøáû/áû/üáû.üáû/»:üß	’äúápáÇ@Ñô
‘æ÷ÎWáû/áû/B´äøÒVáúhÖö´áû/”ç÷&ùøb€„¸ÿëÁìeáûóâ°²ÿçÂøüUáû/áû/\÷ÍËþø+áþòÄñÿ´áû/²ÿäÂŽþþÖR1ûó"áþ–.ûñdþËáû/áû/
ºÿaáþn9ûøáû/.ûåSÑþþ†WþÉÒáü8VþÈ%úô
áû/áû/bÒ÷þÿÿgáû7åü7áû/Wþÿ÷güõXþÈ€Äáû/WþÈ%úô	áû/áû/8üê4¬ÿgáü9æû7áû/Xþí¸¸)4ñù"8üò !áû/+úñdþÌáû/áû/bÿÆÙÿgáþu:û÷áû//ûç[þèÁÞþÅËÿë½ã‡áû/ªÿçÁøý[áû/áû/@üýÉåðÿgáÿùÄðÿ±áû/®ÿí¼,³èú眨ïû×Láû/ÖŠãùÓ\áû/áû/‡ñô CûgáÖXãø²áû/‡Þú@;;;èÈBøó+lþ‡ÿÿ§¸¸{
Æò>ï°!
<Opt ...ÿ!?€	"
 
rÁ%$ÿÿÿÿ#ÿÿÿÿ²¥gimp-text-layer‰(markup "Opt\n_\nSc\nroll\nbar")
(font "Sans-serif Bold")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0 0 0))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)
G<G*<GB°°°(ú7Â÷ìœøì¿9ñ®­ùwmþP§øAýÿþÜ>Úÿÿù‡ü9Žû2AýšÐÕùwmþP§øAýZž÷
ùwí¿8ñ¯Aý˜ÏÖø†í:ÄøîŸAýÿþÝ?¯ûìýAüSýAüSåÿ÷ÿÿJú�ïøáL
úDûbeS
üNýˆú‹ëþ¹õ	ªüþÚAgüj
ø\ñÖø	õC€äºhühó1ËòúÎ5Žìþ¹aúAüSAüS
úAüSAüS
úAüSAüSïAýÿÿg’îô®AüSAüSïAý¤	jüU/ô™AüSAüSïAüW–øÙÃBüSAüSïAüSjüS-ô™AüSAüSïAüS•ðö±AüSAüSRýAüSýAüSýAüSÛAýÿþÜ>3ûþð”AýÿÿgAýšÐÕ^û@Aý¤	ðAýZž÷
5ÐúÿÿZAüWðAý˜ÏÖ”ù*iý[AüSðAýÿþÝ?;Þþÿÿ[AüS?…Scroll Widgetsÿ!?€	"
 
p%$ÿÿÿÿ#ÿÿÿÿ´˜gimp-text-layer|(markup "Scroll Widgets")
(font "Sans-serif Bold")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0 0 0))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)
KØ…LQèQô…L,N²Q¯ÀÀÀ÷
~ÑôñÒ™=ø6û6ûýAü¦þÎÿþû/ø6û6ûüñßö|þÛN	)\´-ø6û6ûü¿ú)ü´üD%ø6û6ûý�þcý¯ü8	ç^ÁïóÉdHýo@Ä÷È
~Ö÷ð»Hø6û6ûôDý¢dþÌI
ý§þÿóýEHý¬÷ÿÿÊ	Àÿôüj6û6ûðòÛ‹úÿûÕŸPÛfþç^4™?Hýý”qþÛDwúò6û6ûúÁú%Bóo¨ØýþÆÂû=üHý½üÃû3ô�þf6û6ûû„þ_÷©þ¢æßýHýzüäÞôEýŠ6û6ûûFý�¼øôèæßýHýoüäÞôEýŠ6û6ûûóØïø÷ñÂû;ýHýoüÄû2ô�þf6û6ûáÃù_û™™I#@ÅÿÀfþå[
4˜?HýoírþÙBtùò6û6û	ú†þÔ÷¯ÿùð:ªþÿúýEHýoþ	Áÿôüj6û6û	òHýÿÌc³æûöÖ“"ôdÇóøÎbHýoù×øñ½Jø6û6û	üôÿ’@ÀÀÀüÄÿ’ùÖõ6ûý_þS$ûóýÍùùÈ6ûý_þSý>ü{ûFû°÷ýXþŒý_þSý>ü{û„ñCúPý—ýMý_þSý>ü{ñÀ¿êŽÒö6û÷“æöÊL_þS÷”çöÊL_þSõaÅòöÏj{ÿç÷tÑöñ„µÉøÉ6ûÉÿøûªþSËÿ÷ú©þS¤þÿûþ˜{ÿÂ÷‰þÿÿûGyöTþ�6ûtþÑ<ŒýþSuþÐ;‰ýþS`ýÊC:Éü<>ü{çÕîNô=ûL“ýN6ûÄø(÷±þSÅø'÷¯þS¾óø(úˆ>ü{çÁÒÆ
ðŠÎö6ûäÖøkþSäÖìjþSäþüüýýþÿÿ¦>ü{è6ßÝ—‹¾Æ÷Ê6ûäÖøkþSäÕújþSçÿú«>ü{üBxNûƒôcþŽø6ûÄø(÷±þSÅø&÷®þSÃêý<ü}ÿûGûÕýOË6ûtþÑ;‹ýþSvþÎ9…ýþSdýÃFL«C+ú·¿h*	ûôÿöù6ûÊÿøûªþSÍÿ÷úªþO þÿúýHÞÿý÷ðÿûÇÿËý6û÷”ç÷ÍP_þS÷—è÷ËMuü@÷UºíøÝ¯Pö9Âôþ÷6™Ôö"üÂ÷6÷‹\( þ¼6ÿ²ÿþð37ù&Ÿåúéª-Zrrr'û÷Ó~ÿúk
0Š_ãi1§çÓ3ÛÎOðçÿÿþ�óÉkB!<Opt ... #1ÿ!?€	"
 
Â%$ÿÿÿÿ#ÿÿÿÿ«�gimp-text-layere(markup "Opt\n_\nSc\nroll\nbar")
(font "Sans-serif Bold")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0 0 0))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)
TŒ<T¨<TÀ°°°(ú7Â÷ìœøì¿9ñ®­ùwmþP§øAýÿþÜ>Úÿÿù‡ü9Žû2AýšÐÕùwmþP§øAýZž÷
ùwí¿8ñ¯Aý˜ÏÖø†í:ÄøîŸAýÿþÝ?¯ûìýAüSýAüSåÿ÷ÿÿJú�ïøáL
úDûbeS
üNýˆú‹ëþ¹õ	ªüþÚAgüj
ø\ñÖø	õC€äºhühó1ËòúÎ5Žìþ¹aúAüSAüS
úAüSAüS
úAüSAüSïAýÿÿg’îô®AüSAüSïAý¤	jüU/ô™AüSAüSïAüW–øÙÃBüSAüSïAüSjüS-ô™AüSAüSïAüS•ðö±AüSAüSRýAüSýAüSýAüSÛAýÿþÜ>3ûþð”AýÿÿgAýšÐÕ^û@Aý¤	ðAýZž÷
5ÐúÿÿZAüWðAý˜ÏÖ”ù*iý[AüSðAýÿþÝ?;Þþÿÿ[AüS?:Scrollable_ ...ÿ!?€	"
 
/Ô%$ÿÿÿÿ#ÿÿÿÿªƒgimp-text-layerg(markup "Scrollable_\ncompatible")
(font "Sans-serif Bold")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0 0 0))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)
YB:Y^:YvpppQúAüSAüSýAüSýAüSú�ïøáLúAüSAüSýAüSýAüSúDûbeSúAüSAüSýAüSýAüSüNýˆÓ‹ëþ¹Aýÿÿg’îô®AüSAüS3ûþð”AýÿþÜ>AüS�íôŸ
Ì	ªüþÚAgüjAý¤	jüU/ô™AüSAüS^û@AýšÐÕAüSiûH.øwø\ñÖøÞAüW–øÙÃBüSAüS5ÐúÿÿZAýZž÷
AüS–ÿÿ¨ÎC€äºhühAüSjüS-ô™AüSAüS”ù*iý[Aý˜ÏÖAüSjúHÌ1ËòúÎ5Žìþ¹AüS•ðö±AüSAüS;Þþÿÿ[AýÿþÝ?AüS�ìþÿorþ÷ÿÿœúAüSAüSýAüS,úAüSAüSýAüS(ýùwýAüSýAüS‰‹ëþ¹’îô®AýÿþðÒõä:AýÿþÜ>3ûþð”	Úÿÿù^üSAýÿþÜ>AüS�íôŸ
güjjüU/ô™Aý•8û¼ð AýšÐÕ^û@ùwAüSAýšÐÕAüSiûH.øw–øÑ–øÙÃCüVø‚ä«AýZž÷
5ÐúÿÿZùwAüSAýZž÷
AüS–ÿ‹¨hühjüS-ô™AüSøå«Aý˜ÏÖ”ù*iý[ø†AüSAý˜ÏÖAüSjúHŽìþ¹•ðö±AüSøå«AýÿþÝ?;Þþÿÿ[¯ûìGüSAýÿþÝ?AüS�ìþÿoýAüS6ýAüSZTOriginal_widgetÿ!?€	"
 
©Ù%$ÿÿÿÿ#ÿÿÿÿ©Ÿgimp-text-layerƒ(markup "Original_widget")
(font "Sans-serif Bold")
(font-size 14)
(font-size-unit pixels)
(antialias yes)
(hint-style full)
(language "en-us")
(base-direction ltr)
(color (color-rgb 0 0 0))
(justify left)
(box-mode dynamic)
(box-unit pixels)
(hinting yes)
^ýT_!a„T_A`ðLýAüSýAüSýAüS
ýAüS÷Þ7Â÷ìœýAüSýAüSýAüS
ýAüSöÞì¿9ñ®ýAüSÔÞmþP§øAýÿÿgAüS
§øÿÿ¯AüSAýÿþæ@3ûþð”AüSÏŽë
�æ¦ØAüS
§øÿÿ‡ü9Žû2Aý¤	AüSküU6ø¯AüSAýœí¨ù^û@AüSÅMú;Öð6Þ›AüSküS5ømþP§øAüWAüS“øä¯AüSAüVÛ²5ÐúÿÿZAüSÅõ�ओùZAüS“øäí¿8ñ¯AüSAüSküQ6ø¯AüSAüSÚ²”ù*iý[AüSÅÄó®aôøAüSküQ3ø:ÄøîŸAüSAüS¨øÿÿ¯AüSAüSÚ²;Þþÿÿ[AüSð…ÿr'úÐAüS¨øÿÿý-ñ˜9ú÷þò¯þ÷ÿÿPðððÿ¯ÿ¯ÿ¯
Éùw¯
§øÿÿ¯�íôŸÚÿÿù¯küU6ø¯iûH.øwùw¯“ø䯖ÿÒ¨ùw¯küQ6ø¯júHø†¯¨øÿÿ¯�ìþÿo¯ûìý-ñ˜
ú÷þò¯*@urwid_widgets_2.pngÿ!?€	"
 
%$ÿÿÿÿ#ÿÿÿÿb¤@bà&R&^&j&v@f˜pyRƒ„†Ìˆ(‹UŒÿ�!Žw��ãš$¢/¬¨µ¦¾UéÏ
Ú¡ßÀä"êÀòÎúí¬Æq|a¨#*Q.Ý0û6ÀDÒJŒN"OtS”Xê]8^ÿekænHsvt€x„z|â}Ü�ƒ�‡iŠc�Žš�Ÿ¨¼ªÊ·ç½IÇEÍÖ›æÒö$¾ÆêdÄÀ„5å!A#%#Ü@>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ%ÿÿÿ
#ÿÿÿ"ÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿýÿÿÿÿÿ	ÿÿÿ	ÿÿÿ"ÿÿÿþÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ.¯þéÿ-¯éÿÿ,¯éÿÿ+¯éÿÿ*¯éÿÿ)¯éÿÿ(¯é@>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ%ÿÿÿ
#ÿÿÿ"ÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿýÿÿÿÿÿ	ÿÿÿ	ÿÿÿ"ÿÿÿþÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æ@>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ%ÿÿÿ
#ÿÿÿ"ÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿýÿÿÿÿÿ	ÿÿÿ	ÿÿÿ"ÿÿÿþÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ.éþ¯ÿ-é¯ÿÿ,é¯ÿÿ+é¯ÿÿ*é¯ÿÿ)é¯ÿÿ(é¯@ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ˜ü‡gF!%ÿÿÿÿþµ3#ÿÿôÿð/Q³ÿÿo"ÿÿýÿðüRþö$ý	ý(úÿýÿðýžÿ®ù­óÿÿä^ýzäÿþ×DøŸñÿÿãhÿýÿðý-ÿá÷%êû˜_mÎÿxìÄÿÏxZyÅqÝÿ³duÛÿÿýÿð÷
ÿþ
¶ýCöÌôpÿ®ú�ÿœýÝÿýÿðùéÿ,,ÿ±øZÿcâúýæùþhÿýÿðøùÿWÿ¶pù�ÿŒ
ÿ×ýÿ×þ+ÿýÿðøÿðyÿú÷ûž+ÿ»ý(ÿ¾þÿýÿðøbÿÊ`ÿxýÿÔýÿÕþ)ÿýÿð÷éÿ]7ÿ¸	ýåùýéøþ_ÿýÿðüKÙÿ²üÈÿRø	
zÿŸúŠÿ…ýÔÿôÿú  §Çéÿû™à+òýšZIg™ð4ÐÿÁhIc°oæÿŸSeÐÿÿÿÿûðÒ´�%ý°üÿýò©ý	“ùÿþäKø¶þÿÿ÷~ÿ"û,>#û*="ú7)ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ/ÿ?áÿ5ÿÿ5ÿÿ5ÿÿ%ÿÿÿ
ÿ
ÿ	ÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÓÿ¯.éÿíÿ¯.éÿíÿ¯.éÿí	ÿ¯.éÿí
ÿ¯.éÿíÿ¯.éÿíÿ¯.éÿí
ÿÿ¯.éÿíÿ.éÿíÿ-éÿíÿ,éÿíÿ+éÿíÿ*éÿíÿ)éÿíÿ(éÿíÿ'éÿíÿ?áÿ5ÿÿ5ÿÿ5ÿÿ%ÿÿÿ
ÿ
ÿ	ÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÓÿ6ÆÿÍî5ÆÿÍî4ÆÿÍ	î3ÆÿÍ
î2ÆÿÍî1ÆÿÍî0ÆÿÍ
î/ÆÿÍî.ÆÿÍî-ÆÿÍî,ÆÿÍî+ÆÿÍî*ÆÿÍî)ÆÿÍî(ÆÿÍî'ÆÿÍî?áÿ5ÿÿ5ÿÿ5ÿÿ%ÿÿÿ
ÿ
ÿ	ÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÓÿé.¯ÿ®ªé.¯ÿ®ªé.¯ÿ®	ªé.¯ÿ®
ªé.¯ÿ®ªé.¯ÿ®ªé.¯ÿ®
ªÿé.¯ÿ®ª.¯ÿ®ª-¯ÿ®ª,¯ÿ®ª+¯ÿ®ª*¯ÿ®ª)¯ÿ®ª(¯ÿ®ª'¯ÿ®ª?ÿáþ'/5þþÔü5ý¶ÿþÔü5ý¶ÿþ%ý
+ü%ý¶ÿ
þ
ý
!	õ<ð‡<Ñüÿ(uÓÿïö‘
ðûÿñððÄÆìø‹ìÿÿé€öð¦$Æúÿõsÿ•á@ÿÄò¬uˆÏ·zZl¾ÿºxØÿ…xxbÔü÷ÅÿÄjmÌÿ·öÿÑå¬p¨úÿXøý/@ÿÿdÿý¢ÿBý¶ÿþÔüð_ÿ¹	ÆÿOÿÿrýHÿËùÿ’@ÿÔý?ÿtý¶ÿþÔüýÄÿ4÷Fÿ´ÿèý÷ðùÿ¸@ÿ¥÷]±Ðæèëÿ”ý¶ÿþÔüýê÷ø
ÿÚÿ¾ýÅÿùÿÔ@ÿ�ö¾ÿØ›‰€•ÿžý¶ÿùÔüÿàùðöÿ²ý¾ÿ
ùÿº@ÿ�ý^ÿšýAÿžý¶ÿþÔüþìöøþÜÿ²ý¾ÿ
ùÿ•@ÿ�ý�ÿ@ýwÿžý°ÿ#þÔüýÇÿ+ø=ÿ·ÿ²ý¾ÿ
ùÿ7@ÿ�ýƒÿ‚üëÿžý’ÿWþÔüñhÿ¦¹ÿXÿ²ý¾ÿ
ÿ¢ý@ÿ�ö5üð�ImåÅÿžöIÿ枀iÔüñÐÿ²Y]¾ÿÃÿ²ý¾ÿ
ÿý@ÿ�÷añÿÿõ’,ÿž÷mÖóÿÒÔüø¡üÿÿû–	ýÿ²ý¾ÿ
ü3/ü2/Óÿ?Ùÿ	ÿÿÿÿÿÿ	ÿÿÿÿÿÿ	ÿÿÿÿÿ	ÿÿÿÿ	ÿÿÿ
ÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿ ÿ
ÿÿ+ÿÿÿ6ÿ7ÿ:ÿçÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯?Ùÿ	ÿÿÿÿÿÿ	ÿÿÿÿÿÿ	ÿÿÿÿÿ	ÿÿÿÿ	ÿÿÿ
ÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿ ÿ
ÿÿ+ÿÿÿ6ÿ7ÿ:ÿÏÿîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æ?Ùÿ	ÿÿÿÿÿÿ	ÿÿÿÿÿÿ	ÿÿÿÿÿ	ÿÿÿÿ	ÿÿÿ
ÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿ ÿ
ÿÿ+ÿÿÿ6ÿ7ÿ:ÿÏÿªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'é?ÿÙý0	*ý˜ƒü˜˜ù|˜¨ÿ(	äüíýü[ÿÿgø	ùõ¨ÿ(	äý¯ÿNüšøì¦øBÿ»	äýoÿŽüÚÀ®æý‚ÿ{	ù!ääþ

ýý/ÿÎúÿ�pÿ&øÂÿ;�ð%÷�óÿö·ää÷Áüÿéˆ/ðŒù«óÿÿåaéíýYÿB2ÿeøõ¨ÿ(÷§ÿÍmkÆÐíäöèÿ›^và½ÿ–ø"éû™`lÌÿð¯ÿM™ù	ñ¥Aÿ»ñ¨ÿ(6ÿË	¿ÿäý�ÿtüêÿ–ý²þFþÈðoÿŒØÄ¶ä€ÿ{ø¨ÿ(›ÿOý?ÿäýîíø�ÿ–(ÿµÿVð.ÿÌÿ„xÿ$Àÿ:ø¨ÿ(ÀÿøþäÿÆøOÿ–Sÿ¹pÿ�ðíüWÿE:ÿc÷õø¨ÿ(Ûÿùëä(ÿµø:ÿ–uÿû÷ò®ÿK—úö£?ÿºø¨ÿ(ÂÿøýäÿÌøUÿ–\ÿ|ûnÿŠÖÇû¾â~ÿzø¨ÿ( ÿFý6ÿäýãöø–ÿ–3ÿ¼û.ÿÝÿˆû€ÿÜÿ:ñ¨ÿ(Aÿ¿±ÿäðtÿš3õÿ–ÅÿVûìÿÿHûBÿÿôý¨ÿ(÷´ÿ»US²ÝðãöËÿÏ�«õ“ÿ”ø(ñþœ[Ig˜ü®ÿü
ü
úÿºý¨ÿ(÷žüÿÿÏää÷	‘×î¹YDÿyý®üÿÿó ý9
ýpÿXû+>$+ÿüæò6øÕ�k\…ëÿw7ùžíÿÿü×U:ý Ïÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ'ÿ?ÿ<ÿ<ÿ<ÿÿ0ÿÿÿ+ÿýÿÿ+ÿÿÿÿÿ+ÿÿÿ1ÿÿÿ-ÿÿÿ/ÿÿ/ÿ	ÿ+ÿÿÿÿ+ÿÿ+ÿÿ8ÿîÿ¯é¯ é¯!é¯"é¯#é¯$é¯%é¯&é¯'é¯(é¯)é¯*é¯+é¯,é¯-é¯.é?ÿ<ÿ<ÿ<ÿÿ0ÿÿÿ+ÿýÿÿ+ÿÿÿÿÿ+ÿÿÿ1ÿÿÿ-ÿÿÿ/ÿÿ/ÿ	ÿ+ÿÿÿÿ+ÿÿ+ÿÿ8ÿîÿÆ?ÿ<ÿ<ÿ<ÿÿ0ÿÿÿ+ÿýÿÿ+ÿÿÿÿÿ+ÿÿÿ1ÿÿÿ-ÿÿÿ/ÿÿ/ÿ	ÿ+ÿÿÿÿ+ÿÿ+ÿÿ8ÿîÿé¯é ¯é!¯é"¯é#¯é$¯é%¯é&¯é'¯é(¯é)¯é*¯é+¯é,¯é-¯é.¯?ÿý<ýrÿ^<ýrÿ^<ýrÿ^ü	$&0üµ÷ÿõðû"±þÿþßF+ù|Z´ÿ©xö
Ôÿ§p[w¿f+ùörÿ^ý,ÿ´ÿ+ùÿgrÿ^ýý¼1ùÿ�rÿ^ú»ÿÞˆ<-ù÷¢rÿ^ø|çÿÿì„/ýrÿ^úC™ìÿ™/ýlÿg	ý"ýí+ù	Nÿ›þýöá+ìï8ñõ¯�€Xä‡ZCpÇÿ„+þª÷BÅìÿÿ8Îýÿýñ…8û.@)	îÿ?Àÿéÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯?ÀÿÆÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/ÆÿÆ?Àÿ¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé?ÿÀÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿ?ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8¯'é¯(é¯)é¯*é¯+é¯,é¯-é¯.é¯.éÿí¯.éþíÿ
¯.éýíÿÿ¯.éÿíÿ¯.éÿíÿ
¯.éÿíÿ	¯.éÿíÿ¯.éÿíÿ?ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8?ÆÿÍ=ÆþÍî<ÆýÍîî;ÆÿÍî:ÆÿÍî9ÆÿÍî8ÆÿÍî7ÆÿÍî?ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8é'¯é(¯é)¯é*¯é+¯é,¯é-¯é.¯é.¯ÿ®é.¯þ®ª
é.¯ý®ªªé.¯ÿ®ªé.¯ÿ®ª
é.¯ÿ®ª	é.¯ÿ®ªé.¯ÿ®ª?ÿÀÿéÿíÿéÿíÿéÿíÿéÿíÿéÿíÿéÿíÿéÿíÿéÿíÿéÿéÿéÿéÿéÿéÿéÿéÿéÆÿÍîÆÿÍîÆÿÍîÆÿÍîÆÿÍîÆÿÍîÆÿÍîÆÿÍîÆîÆîÆîÆîÆîÆîÆîÆîÆ¯ÿ®ª¯ÿ®ª¯ÿ®ª¯ÿ®ª¯ÿ®ª¯ÿ®ª¯ÿ®ª¯ÿ®ª¯ª¯ª¯ª¯ª¯ª¯ª¯ª¯ª¯?ÿÀÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿéÆ¯?ÿÀÿé¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯Æ/Æ/Æ/Æ/Æ/Æ/Æ/Æ/Æ/Æ/Æ/Æ/Æ/Æ/Æ/ÆÆ¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é?ÿÀÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿ'¯é&¯é%¯é$¯é#¯é"¯é!¯é ¯é¯é¯ é¯!é¯"é¯#é¯$é¯%é¯&éÆ'é¯&é¯%é¯$é¯#é¯"é¯!é¯ é¯é¯é ¯é!¯é"¯é#¯é$¯é%¯é&¯?ÿÀÿéÿíÿéÿíÿéÿíÿéÿíÿéÿíÿéÿíÿéÿíÿéÿíÿéÿíÿ
éÿíÿéÿíÿéÿíÿ
éÿíÿ	éÿíÿéÿíÿéÿíÿÆÿÍîÆÿÍîÆÿÍîÆÿÍîÆÿÍîÆÿÍîÆÿÍîÆÿÍîÆÿÍî
ÆÿÍîÆÿÍîÆÿÍî
ÆÿÍî	ÆÿÍîÆÿÍîÆÿÍî¯ÿ®ª¯ÿ®ª¯ÿ®ª¯ÿ®ª¯ÿ®ª¯ÿ®ª¯ÿ®ª¯ÿ®ª¯ÿ®ª
¯ÿ®ª¯ÿ®ª¯ÿ®ª
¯ÿ®ª	¯ÿ®ª¯ÿ®ª¯ÿ®ª?ÿÀ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿQ.¯.¯.¯.¯.¯.¯.¯¯ûYWWn�¯þœ¯¯úK]C
y¯þ¥Z¯¯ú�¯¯[.¯ô‹fu¨¯Ÿ{®¯¯„•¯ô’�¯¯‚”¯¯~¤©z¯¯ë�¯§8E¯¯aG¦«$`¯|Ÿ¯ñq-¯ª8¯£§š¯¯a¯ì(§¯¥€¯®[¯˜x�¯ñŸª�3®v(¯š¯¦¯í„¤�<1¯ˆª¯¯O9¯¯sT¯ò�Sg.ŒHV¯š¯Œ¯í�¯¯“ŸŠª¯¯L:¯¯G6¯òKS%•\^„¯š¯�¯ì�¯¯j¬§
€¯¬_¯sž d¯òy¯Š«¯š¯§
¯ë'1n¯¯j>%©–
Н”Œ¯ò¥A¯­
1¯¯š¯¯h¯ü„ƒƒ–¯ù•q€¬¯—�¯þ“”¯ö‰›¯¯�—¯¯ª„¯.¯.¯.¯.¯.¯‘.é.é.é.é.é.é.ééÿvtýäÍ&ééþ]týäÉ
ééþ»éþÉ
éñ»ˆšßéå¡çéÏ£éé˹éö˸ééµ¼éé°Òéþ»éìÉ
éé…	^+%Ûé¹émÇé^téö¯#éé1éæÈéæXéÉ
éÞ¦éè,uéX{é0LŠé!²éöä	ÏÅ2Û·é鿝ÚÚàéÉ
é¹áéémHé•>Ù«MÎ
äé÷A’ˆoW¡zZééþ»éíÉ
é»áééiJéјYá�Cé÷~UK­”d<—ééþ»éíÉ
éà§éå'zéé%(–é;(€é÷»âÐ!ÓéQ.Æ.Æ.Æ.Æ.Æ.Æ.ÆÆûdcc}²Æþ±ÆÆúUiL‰ÆþºfÆÆúŸÆÆf4Æôžt„¾Æ³‹ÅÆÆ•¨Æô¥£ÆÆ“§ÆÆ�º¿ŠÆÆëŸÆ½?NÆÆnQ#!¼Á(mÆ�´Æñ3ÆÀ@Ƹ½®ÆÆmÆì-½Æ»�ÆÅ"fƬˆ’Æñ³À’:Ć.Æ®Ƽ	Æí•º¢D8ÆšÁÆÆY@ÆÆ‚_Æò!’^t4žRbƮƞÆíŸÆÆ¦´œÁÆÆVBÆÆP=ÆòU^*¨hj–ƮƠÆìŸÆÆxý‘ÆÂkÆ‚³$qÆò‰ Æœ"ÁÆ®ƽÆë,7}ÆÆxG*¿ªœÆ§žÆòºJÆÄ7ÆÆ®ÆÆvÆü•””©Æù¨€�ÂÆ«¢Æþ¦§Æö›¯ÆÆŸ«ÆÆÁ–Æ.Æ.Æ.Æ.Æ.Æ‘.Æ.Æ.Æ.Æ.Æ.Æ.ÆÆÿdcý®!ÆÆþOcý«	ÆÆþŸÆþ«	ÆñŸtƒ½ÆÃ‰ÄÆ°ŠÆÆ¬�Æö¬œÆÆš ÆÆ–³ÆþŸÆì«	ÆÆqP$ ºÆ�Æ\©ÆPcÆö•ÆÆ*ÆÄªÆæ	KÆ«	ƽ	�ÆÅ%cÆKiÆ(@uÆ—ÆöÁ°¨+º›ÆÆæ•ºº¾Æ«	Æ�¿ÆÆ\=Æ5¹‘A¯	ÂÆ÷7|t_J‰gLÆÆþŸÆí«	ÆŸ¿ÆÆY?Ʋ�K¿y9Æ÷kH@“~U3€ÆÆþŸÆí«	ƾŽÆÃ!hÆÆ "Æ2"mÆ÷ŸÀ±³ÆQ.é.é.é.é.é.é.ééûvtt“ÑéþÐ#ééúd{Z¢éþÛxééú»ééy=éô¹ˆ›àéÓ¤èé靯éôÃÀéé­Äéé¨Úá£ééë»éßJ\éé‚	_)'Ýä0€é¥Ôéñ–<éâKéÙÞÍéé�éì5ßéÜ	ªéè(yéË ¬éñÓâ¬Eç�6éÍéÝ
éí¯Ú¾OBéµ	ãééiLéé™oéò'¬o‰=º`séÍéº	éí»ééÄÔ¸ãééeNéé^Héòeo1Æz}#°éÍé¼éì»éé�åÞ
«éä$~éšÒ*„éò¢%é¸(	äéÍéß
éë4A%“ééŽ	S1áȸéĺéòÛWéæAééÍéé‹é¯ÿÇéùÆ—ªäéÉ¿éÄéö¶Ïéé»Éééã°é.é.é.é.é.é‘.¯.¯.¯.¯.¯.¯.¯¯ÿYWý¬š¯¯þFWý¬—¯¯þ�¯þ—¯ñ�ft§¯¬y®¯œz¯¯˜‹¯ö˜Š¯¯ˆ�¯¯„ž¯þ�¯ì—¯¯dG ¥¯‹¯R–¯GW¯ö„¯¯%¯­
–¯æ

B¯—¯§}¯®!X¯B]¯$9h¯†¯ö«œ”&¥‰¯¯æ„¤¤¨¯—¯‹©¯¯R6¯p/£€:š¬¯÷1nfTAy[C¯¯þ�¯í—¯�©¯¯O8¯�rC©k2¯÷_@8‚oK-q¯¯þ�¯í—¯¨~¯¬\¯¯q¯-`¯÷�ªœŸ¯?ÿÐ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ@.¯.¯.¯.¯.¯.¯.¯¯þeR¯þª’¯¯þ[D¯þ�¯æ‰g�[D¯¯¦po¡€£¯¯¤th”¯¯dxxª¯æI&D¯ª"K$‡¯‘8Iw¯D	RR¥¯鄯®D¯i¯¯ˆ‡¯1V¯¯s¯�¯鬯¯ED¯GI¯¯¬‡¯	3AA;ª�¯𪯯CD¯L@¯¯§‡¯f‰ý­�¯ð�¯¬D¯w¡¯n‡¯0D¯û®¯£	­¯æD+D¯®<#1‹¯– N=-¯¯3%<£¯û�r–𔝖÷Ÿ¯¯ª€m~¨¯ýœ„«¯¯ûf‡x/1"¯¯ûa75Y§"¯.¯.¯.¯‘.é.é.é.é.é.é.éýæÙéþ�Véþæ¾éýètàéþ’AéþÙéáè äééÀ‰³’Aééäš�ϲÐééៈ½ééž  Ùéáæ×é—	[05Aéè;#g* šéÐ%BgˆétmmÏéäæ×é–éé8A饿éΚéZZéé³äÙéóæ×ÔÔéévAéyGéô šé%;WWVÉÙéóæ×ÖÑéésAé;éûšé$t¶ýáÙéëæ×é’éè5Aé¹Í鬚éZAéûçéæ×é@.Æ.Æ.Æ.Æ.Æ.Æ.ÆÆþr\ÆþÀ¥ÆÆþfMÆþ£Ææ›t fMÆÆ¼~¶�¸ÆÆºƒv¨ÆÆqˆˆÀÆæ	S!+MÆÀ"'U)™Æ¤@S†ÆM
\\»Æ镯ÄMÆw ÆÆš™Æ7bÆÆ‚Æ£ÆéÂÆÆNMÆQRÆÆÃ	™Æ
:JJCÀ£ÆðÁÆÆLMÆVHÆÆ½™Æ	t›ýÄ£Æð’ÆÃMƇ¶Æ|™Æ6MÆûÄÆ¸
ÄÆæM1MÆÅD(8�Æ©$YE3ÆÆ9*D¹Æû¢�ª¯¨Æôª©ª´ÆÆÀ�{�¾Æý°•ÁÆÆûs™ˆ58"ÆÆûn>B"ééû‚IFwß"é.é.é.é‘.¯.¯.¯.¯.¯.¯.¯ý­£¯þvA¯þ­�¯ý®W¨¯þn1¯þ£¯á®x«¯¯�g‡n1¯¯«tlœ†œ¯¯©wfޝ¯wxx£¯á­¡¯qE$(1¯®-N t¯œ1Nf¯WRRœ¯ä­¡¯q¯¯*1¯|­¯št¯DC¯¯‡¬£¯ó­¡ŸŸ¯¯Y1¯[6¯ût¯-Aü—£¯ó­¡¡�¯¯V1¯_-¯ût¯W‰ý©£¯ë­¡¯n¯®(1¯‹š¯�t¯C1¯û®¯­
¡¯?ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿÿÿÿ'¯éÿÿ&¯éÿÿ%¯	éÿÿ$¯
éÿÿ#¯éÿÿ"¯éÿÿ!¯
éÿÿ ¯éÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ0ÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯þš‘¯û�§€¢ÿÿÿÿ
¯õC8C-¬¯5U–ÿÿÿÿ¯ôž%¯¯?€¯�¯¯ÿÿÿÿ¯ôtH¯¯eV¯®¯¯ÿÿÿÿ¯ø›'¯¯A~¯¯ÿÿÿÿÿ
¯ù=ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ0ÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆþ¯¤ÆûŸ½�·ÿÿÿÿ
ÆõK@L2ÂÆ<`©ÿÿÿÿÆô³*ÆÆG‘ÆŸÆÆÿÿÿÿÆôƒRÆÆraÆÅÆÆÿÿÿÿÆø¯,ÆÆJŽÆÆÿÿÿÿÿ
ÆùEDR,ÁÆÆÿÿÿÿþ÷ÞÿþÞ÷ÿÿÆþ¦™Æÿ™Æÿÿÿÿýš&€ÿý€&šÿÿÆÿÿÿÿûÈ;
?û?
;ÈÿÿÆÿÿÿÿûÈ;
?û?
;ÈÿÿÆÿÿÿÿýš&€ÿý€&šÿÿÆÿÿÿÿþ÷ÞÿþÞ÷ÿÿÆþ7¾ÿÿÆþ—Äÿÿ'é¯ÿÿ&é¯ÿÿ%é	¯ÿÿ$é
¯ÿÿ#é¯ÿÿ"é¯ÿÿ!é
¯ÿÿ é¯ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ0ÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéþÎÁéû»ßªØÿÿÿÿ
éõYKZ;äéFqÇÿÿÿÿéôÒ1ééT«é	»ééÿÿÿÿéôš`éé†réèééÿÿÿÿéøÏ4ééW§ééÿÿÿÿÿ
éùQOa4äééÿÿÿÿþ÷ÞÿþÞ÷ÿÿéþÄ´éÿ´éÿÿÿÿýš&€ÿý€&šÿÿéÿÿÿÿûÈ;
?û?
;ÈÿÿéÿÿÿÿûÈ;
?û?
;Èÿÿéÿÿÿÿýš&€ÿý€&šÿÿéÿÿÿÿþ÷ÞÿþÞ÷ÿÿéþAàÿÿéþ±æÿÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ/ÿ&éÿíÿ%éÿíÿ$éÿíÿ#éÿíÿ"éÿíÿ!éÿíÿ éÿíÿéÿíÿ?.¯þÍÿ-¯ýÍéÿ,¯üÎééÿ+¯ÿÏéÿÿ*¯ÿÏéÿÿ)¯ÿÐéÿÿ(¯ÿÐéÿÿ'¯ÿÑéÿÿ&¯ÿÒéÿÿ%¯ÿÒéÿÿ$¯ÿÓ	éÿÿ#¯ÿÓ
éÿÿ!¯þ°Ôéÿÿ ¯þ°Õéÿÿ¯þ°Õ
éÿÿ¯þ°Öéÿÿ¯þ°Öéÿÿ¯þ°×éÿÿ¯þ°×éÿÿ¯þ°Øéÿÿ¯þ±Øéÿÿ¯þ±Ùéÿÿ¯þ±Ùéÿÿ¯þ±Úéÿÿ¯þ±Úéÿÿ¯þ²Ûéÿÿ¯þ²Ûéÿÿ¯þ²Üéÿÿ¯þ²Üéÿÿ¯þ²Üéÿÿ¯þ³Ýéÿÿ¯þ³Ýéÿÿþª¯þ†¢¯þ³ÞéþÇÄéÿÿþA¡¯úR‘¯¯³Þéþ„zéÿÿæ�¬¯¯ŽŽŸ�¯•§¯‘Ö¬Úéé䳪äéé„zéÿÿæ"�¯8éÿÚÓÿÞéÿÿýš&€ÿþ€&¯ýž€ÁéÿŠ^ÿ¤éÿÿûÈ;
?þ?¯þ¶â*éÿÿûÈ;
?ú?¯¯·â+éÿÿýš&€ÿø€&¯·â鉗éþÚ½éÿÿþ÷ÞÿøÞ÷·ãééw‡éþ²Léÿÿ&ÆÿÍî%ÆÿÍî$ÆÿÍî#ÆÿÍî"ÆÿÍî!ÆÿÍî ÆÿÍîÆÿÍîÿ?/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿþ�ÀÆþ—·Æþ©¦ÆÿÿþJ¶Æþ\¤ÆþqgÆÿÿæ£ÂÆÆ¡¡³²Æ¨½ÆŸ¾’ºÆÆÂ˜�ÁÆÆqgÆÿÿæ&²Æ@DJ(iÆ>šÆ@[!žÆz^e(¢ÆqgÆÿÿï&²¬1ÆÆ=iÆ>次{_Æú©m_ÆqgÆÿÿæ&²„UÆÆbiÆ>𯯯”DÆ…7UN8ÆqgÆÿÿæ&²³+ÆÆ7iÆ>𯯯”DÆ*¯Æp7ÆqgÆÿÿæ&²ÆU69/oÆ>𯯯”DÆUG^27ÆqgÆÿÿæžÁÆÆ±µ?¡Æ¥»Æ™ÆÆº¦ÆÄ”޹¢Æ±¯Æÿÿþ÷ÞÿþÞ÷ÆüamE6Æÿº³ÿ½Æÿÿýš&€ÿþ€&Æý³ŒªÆÿuPÿ‹ÆÿÿûÈ;
?þ?/ÆÿÿûÈ;
?þ?/Æÿÿýš&€ÿþ€&Æþt€Æþº¡Æÿÿþ÷ÞÿþÞ÷ÆþesÆþ—@Æÿÿ&¯ÿ®ª%¯ÿ®ª$¯ÿ®ª#¯ÿ®ª"¯ÿ®ª!¯ÿ®ª ¯ÿ®ª¯ÿ®ªÿ?.éþËÿ-éý˯ÿ,éüʯ¯ÿ+éÿɯÿÿ*éÿɯÿÿ)éÿȯÿÿ(éÿȯÿÿ'éÿǯÿÿ&éÿƯÿÿ%éÿƯÿÿ$éÿÅ	¯ÿÿ#éÿÅ
¯ÿÿ!éþèįÿÿ éþèÃ¯ÿÿéþèÃ
¯ÿÿéþè¯ÿÿéþè¯ÿÿéþèÁ¯ÿÿéþèÁ¯ÿÿéþèÀ¯ÿÿéþçÀ¯ÿÿéþ翯ÿÿéþ翯ÿÿéþ羯ÿÿéþ羯ÿÿéþ潯ÿÿéþ潯ÿÿéþ漯ÿÿéþ漯ÿÿéþ漯ÿÿéþ廯ÿÿéþ廯ÿÿþ©âéþ²Øéþ庯þ–“¯ÿÿþW×éúmÁéé庯þd[¯ÿÿæÀäéé½½ÓÑéÆÞé·²�¤¯¯¬‡€«¯¯d[¯ÿÿæ-ÑéKPW/{éIµä8PŒ¯lSY$�¯d[¯ÿÿï-ÑË:ééH{éI±¹
�¯lT¯ú–aT¯d[¯ÿÿæ-Ñ›eéés{éH�¯¯¯‚<¯u1KE1¯d[¯ÿÿæ-ÑÒ3ééA{ã:ˆ¯¯¯‚<¯%š¯c1¯d[¯ÿÿæ-Ñéd?D7¸7ˆ¯¯¯‚<¯K>S-1¯d[¯ÿÿæºäééÐÕH•¯‘¥¯‡¯¯¤“¯®ƒ~£�¯œš¯ÿÿþ÷ÞÿþÞ÷éür€O2¯ÿ¤Ÿÿ§¯ÿÿýš&€ÿþ€&éýÒ �¯ÿhGÿ{¯ÿÿûÈ;
?þ?éþâ¶*¯ÿÿûÈ;
?ú?ééá¶+¯ÿÿýš&€ÿø€&éᶯgq¯þ¤Ž¯ÿÿþ÷ÞÿøÞ÷ᵯ¯Yf¯þ†9¯ÿÿÿÿÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿÿ'ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿ'ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ¯þ©¯ÿy
¯ÿšÿÿÿÿ¯þ¡A¯ÿ3
¯ý
;Èÿÿÿÿ¯ß‹¡¯¯¥–�|¬‘¯¯­‚œ“©¯�¯¦–ž‚­¯¯£~�
;Èÿÿÿÿ¯ß 'K.V¯‚3[�"¯š"O2‘¯¯…6K ®¯K]Lšÿÿÿÿ¯àjY¯®«‚¯¯�"¯d_¯ª‘¯¯…¯¯8ˆ¯¯ª�ÿÿÿÿ¯à@|¯¯1‹‚:¯¯�"¯A€¯¯"‘¯¯…:¯¯Nq¯J;Rÿ¯âh[¯®ª‚=¯¯�"¯j[¯§‘¯¯…:¯¯NqŸ!¯îÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æÿÿ'ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿ'ÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&ÆÿÿÿÿÆþ¿�Æÿ‰
ÆÿšÿÿÿÿÆþ¶JÆÿ9
Æý
;ÈÿÿÿÿÆß�¶ÆÆ»ª²�Â¤ÆÆÄ”±¦¿ÆŸÆ¼©³”ÄÆÆ¹ŽŸ
;ÈÿÿÿÿÆßµ,U4aÆ”:g²'Ư'Y9¥ÆÆ—=U$ÄÆUiVšÿÿÿÿÆàxdÆÄÁ”ÆÆ²'ÆqlÆÁ¥ÆÆ—ÆÆ@šÆÆÁ£ÿÿÿÿÆàH�ÆÆ7�”AÆÆ²'ÆI‘ÆÆ'¥ÆÆ—AÆÆY€ÆTC\ÿÆâufÆÄÀ”EÆÆ²'Æxfƽ¥ÆÆ—BÆÆY€´%ƪÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éÿÿ'ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿÿ-ÿÿÿ'ÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿéþá©éÿ¡
éÿšÿÿÿÿéþ×WéÿD
éý
;Èÿÿÿÿéß¹×ééÜÈÑ¥äÁééæ®ÐÄáé»éÝÇÒ®æééÙ§»
;ÈÿÿÿÿéßÕ4e=ré®%EzÑ.éÎ.iCÂéé±$Hd*çée{ešÿÿÿÿéàŽvéçä®ééÑ.é„éãÂéé±ééKµééãÀÿÿÿÿéàU¥ééA¹®MééÑ.éV«éé.Âéé±Mééh—écOmÿéâŠyéçâ®QééÑ.éŽyéßÂéé±Nééh—Ô,éÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ'ÿÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ'ÿ¯/é¯0é
¯1é¯2é¯3é
¯4é	¯5é¯6éÿ?ÿþü˜=ÿýƒÜ;ÿüî%F;ÿûW´9ÿúþ�k³;ÿþ×'=ÿþ×'=ÿþ×'=ÿþ×';ÿúþ�k³:ÿûW´:ÿüî%F<ÿýƒÜ<ÿþü˜+ÿ?&¯ÿÌé%¯ÿÌé$¯ÿÌé#¯ÿÌé"¯ÿÌé!¯ÿÌé ¯ÿÌé¯ÿÌé¯ÿÌé¯ÿÌ é¯ÿÌ!é¯ÿÌ"é¯ÿÌ#é¯ÿÌ$é¯ÿÌ%é¯ÿÌ&é¯ÿÌ'é¯þƒ¥
¯ø�§¯ÌééÉÁ$é¯þ/�
¯ýE�Ìéþ‹téþÎ_é¯þ/�¯žë¯¡’¯®�¯‘ÆééÁ»‹téé٫ݾéøÅÊéé¤,¶Ëéû'®¯/�¯Þ{E¯T®‹:¯.ÊéWIW/té˜1nD#éâE`]Zé~{¨éû6‰¯/�¯à¦¯+>�]hÌ5ÊÕ0ééLté:Êé¡#éŸqééFÎÆ9éû*f¯/�¯á(�/sa/¯é5Ê£]éé{téééÍ#é`2HHE¬Æ9éû/f¯/�¯çV3[¡/éé5ÊÒ1ééOtéCÄéš#éšiéþÐ2éÆÿ?ÿþü˜=ÿýƒÜ;ÿüî%F;ÿûW´9ÿúþ�k³;ÿþ×'=ÿþ×'=ÿþ×'=ÿþ×';ÿúþ�k³:ÿûW´:ÿüî%F<ÿýƒÜ<ÿþü˜+ÿ?CÆþ”º
Æþ’½Æþ«¤'Æþ6£
ÆþN²ÆþvcÆþ¯QÆþ6£Æ³ëƶ¥ÆÄ¢Æ¥ÁÆÆ¤ŸvcÆÆ¹‘¼¢Æø¨¬ÆÆŒ%›¬Æû,ÅÆ6£ÆÞ‹NÆ`ÅžAÆ-¬ÆJ>J(cÆ�*]9ÆÀ;ROLÆki�Æû=›Æ6£Æà¼Æ1F¢ivÆ-¬µ(ÆÆ@cÆ1¬Æ‰Ƈ`ÆÆ<¯¨0Æû/tÆ6£Æá- 6‚m5ªÆ-¬ŠOÆÆicÆÆÆ®ÆR+==:’¨0Æû5sÆ6£Æça:g¶5ÆÆ-¬³*ÆÆCcÆ9¦ÆƒƃYÆþ±+Æé/¯éÿè	é0¯
é1¯é2¯é3¯
é4¯	é5¯é6¯ÿ?ÿþü˜=ÿýƒÜ;ÿüî%F;ÿûW´9ÿúþ�k³;ÿþ×'=ÿþ×'=ÿþ×'=ÿþ×';ÿúþ�k³:ÿûW´:ÿüî%F<ÿýƒÜ<ÿþü˜+ÿ?&éÿ̯%éÿ̯$éÿ̯#éÿ̯"éÿ̯!éÿ̯ éÿ̯éÿ̯éÿ̯éÿÌ ¯éÿÌ!¯éÿÌ"¯éÿÌ#¯éÿÌ$¯éÿÌ%¯éÿÌ&¯éÿÌ'¯éþ¯Û
éø¬ßé̯¯—‘$¯éþ?À
éý\Ñ̯þhW¯þšG¯éþ?ÀéÒëé×Ãéç¿éÂÆ¯¯‘�hW¯¯£€¦�¯ø”˜¯¯|!‰˜¯û4èé?ÀéÞ¤[épè¹Mé.˜¯A7A#W¯r%R3¯ª4HFC¯_]~¯ûH¶é?ÀéàÝ"é:R¾{‹Ì(˜ $¯¯9W¯+˜¯y¯wU¯¯5š•+¯û8ˆé?Àéá5¼?š�>¯¯(˜zF¯¯]W¯¯¯š¯H&663�•+¯û>‡é?ÀéçrEzÖ>¯¯(˜ž%¯¯;W¯2“¯t¯tO¯þœ&¯ÿÿéÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯ÿÿÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯þ®¢¯ÿ¡¯éÿÿÿ¯þ�"¯ÿ¯éÿÿÿ¯ÿ¬¯ý¥¯«¯ÿª¯ÿª¯éÿÿÿ¯è©K+,x¯„/+2ž&¯¥A&,&’¯¯‡-éÿÿÿ¯èwF¯¡¯‚	¥¯�"¯qN¯ž‘¯¯…éÿÿÿ¯èDy¯¯-�‚3¯¯�"¯B~¯¯ ‘¯¯…6éÿÿÿ¯èZf¯¯¢‚=¯¯�"¯]f¯­
‘¯¯…:éÿÿÿ¯è‘}Y/¯‚=¯¯�"¯” jD’¯¯…:éÿÿÿ¯éšUq«¯™v¯¯¦i¯¯£h‹¬¯_¯šuéÿÿÿ¯ûªoŠP<¯éÿÿÿ¯û­eO~­¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯þ¤h¯÷–j¯­­¯¯s�¯éÿÿÿ¯þYs¯÷˜r¯\d¯¯Iv¯éÿÿÿ¯èª¬¯f®¯h•¯‚•›y¥15]˜Ioae¥éÿÿÿ¯èŒ/¯¯3�£V¯,™„<ªHN‰¥I"rY;éÿÿÿ¯èoG¯¯a_wV,©¯„<¯\d¯¯I_¯­éÿÿÿ¯èvA¯¯�0HŒ.vF¯„<¯\d¯¯Iv¯¯éÿÿÿ¯è”(¯¯®$®Zt¯„<¯kY¯¯Iv¯¯é¯è®$£¯¯<:¯‰¡¯„<¯›/,‹Iv¯¯Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/ÆÿÆÿÿÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-ÆÿÿÿÆþÄ·Æÿ¶ÆÿÿÿÆþ²'ÆÿÆÿÿÿÆÿÂÆý»ÆÁÆÿÁÆÿÁÆÿÿÿÆè¿U12ˆÆ•519³+Æ»J+2+¥ÆƘ3ÆÿÿÿÆè‡OƶÆ”
»Æ²'ÆXƳ¥ÆÆ—ÆÿÿÿÆèM‰ÆÆ2¢”:ÆÆ²'ÆK�ÆÆ$¥ÆÆ—=ÆÿÿÿÆèftÆÆ·”EÆÆ²'ÆjtÆÄ¥ÆÆ—BÆÿÿÿÆè¥"�d5Æ”EÆÆ²'Ƨ$xM¥ÆÆ—BÆÿÿÿÆé®`€ÁÆ­†ÆÆ¼wÆÆ¹u� ÂÆlƯ„ÆÿÿÿÆûÁ~œZDÆÿÿÿÆûÄrYŽÄÆÿÿÿ-Æÿÿÿ-ÆÿÿÿÆþºuÆ÷©xÆÄÄÆÆ‚ ÆÿÿÿÆþd‚Æ÷¬�ÆhqÆÆS†ÆÿÿÿÆèÀ ÃÆtÄÆu¨Æ“¨¯‰º7:CÒ3éÜW3:2Ãéé³<¯ÿÿÿéèŸ]é×!é®ÜéÑ.é–géÒÂé鱯ÿÿÿéèZ¡éé;¾®EééÑ.éX¨éé+Âéé±G¯ÿÿÿéèxˆéé%Ø®QééÑ.é|ˆéæÂéé±N¯ÿÿÿéèÂ(¦v>é®QééÑ.éÄ*ŽZÃéé±N¯ÿÿÿééÍq—äéÌ�ééÝŒééÙŠ¹%äééΛ¯ÿÿÿéû㔸jPé¯ÿÿÿéûæ†i§æé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿéþÚŠé÷ÇŽéææé陼é¯ÿÿÿéþvšé÷˘éz„ééb�é¯ÿÿÿéèâ%åéˆçéŠÆé­ÆÏ¢ÛAF{Ëb”‚†Ü¯ÿÿÿéèº?ééD»Ùré:̯Oã`g¶Ûb.˜vO¯ÿÿÿéè”_éé�~žr:á%é¯Oéz„ééb~鿝ÿÿÿéè�Wéé¿@`º=�]é¯Oéz„ééb�éé¯ÿÿÿéèÅ6ééè0çx(šé¯Oé�wééb�éé¯éèè0ÙééOMé¶×é¯OéÏ>:¹b�ééÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿ¯.éÿíÿ¯.éÿíÿ¯.éÿí	ÿ¯.éÿí
ÿ¯.éÿíÿ¯.éÿíÿ¯.éÿí
ÿÿ¯.éÿíÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿx=¯þÌé<¯ýÌéé;¯ÿÌé:¯ÿÌé9¯ÿÌé8¯ÿÌé7¯ÿÌé6¯ÿÌé5¯ÿÌé4¯ÿÌ	é3¯ÿÌ
é2¯ÿÌé1¯ÿÌé0¯ÿÌ
é/¯ÿÌé.¯ÿÌé-¯ÿÌé,¯ÿÌé+¯ÿÌé*¯ÿÌé)¯ÿÌé(¯ÿÌé'¯ÿÌé&¯ÿÌé%¯ÿÌé$¯ÿÌé
¯þ£¬
¯þ£­¯ü©§¯Ìé
¯þ/�
¯þ(˜¯ýhWÌéþ׌
éþ¯ª¯þ©¬¯þ/�
¯ø«®¯¯¬®hféÿãéúæçééÆ9
éó-#C¯¯C1-K¯¯/�¯ÞrR¯a¯—3¯,˜¯h"#Eté¹:7Z)éèv@@Šé\H‰éó§¯+•¯¬¯¯8–¯/�¯à�"¯3+šh]¯(˜¨©Ê7téK²é‰#é³Véè7ÛÆ9
é¯õMr¯o82j¯/�¯áž(hl:‹¯(˜~CÌéwtéçéÊ#éf0HH#žÆ9
é¯õNq¤ ­¯:´ÖZ=H:éNAJDã
é6ÆÿÍî5ÆÿÍî4ÆÿÍ	î3ÆÿÍ
î2ÆÿÍî1ÆÿÍî0ÆÿÍ
î/ÆÿÍîÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿx‹Æþ¹Ã
Æþ¸ÄÆþ¿½'Æþ6£
Æþ-¬ÆþvcÆþ¶wÆÿÁÆþ¿ÂÆþ6£
ÆøÁÅÆÆÂÄvcÆÿÁÆÄÆþ¨0
Æó2(KÆÆK82UÆÆ6£ÆÞ�]Æn!Æ«9Æ2¬Æv'(CcÆ�1/M#ÆÅd66uÆN=tÆó½Æ1¨ÆÃÆÆ?ªÆ6£Æà²'Æ90¯viÆ-¬¾¿Ä/cÆ@—ÆtƘIÆÅ/º¨0ÆõW�Æ~@9xÆ6£Æá!³.u{B�Æ-¬�KÆÆecÆÄƬÆW(==†¨0ÆõY€º$ÄÆDsÆ6£ÆçTUZªEÄÆ-¬¥6ÆÆPcÆ*³Æ�ÆqlÆþ«/ÆõY€¿—ysÆ6£Æß‰�Æ?ÆÆ-¬Æ#u‚cÆq9|<ƶ¥|³Ä‰¨
Æõ�£ÆšUn”�Æ~µÆß»c¾Æƒ–ÆÆz¹Æ¼he–”ÆÆ¤tˆGÆÆ¹tNr¾Æ£n‹/Æû¡‘�2„#Æÿ@=ÿ«Æü®eb¥£ÆÿÁÆÿÁÆü\Ƴp
ÆÄ"ÆûÂÆÆÆüfÆ¥4
ÆþnjÆÐ‚Yx·Æ”gv�	i}Áx›\·rÆ¥4Šb­ÆÆtÄÆ´„½:9iªÆ³j�ÄÆ½~Wh½ÆÐtž~(Ư—¡¬›¤º	N”¼Æ¥W‰# ÆÂÆŸ9ÂVS›¹“2�iJÆf` †¹
ÆÿÅÆÒ½ŽpªÆÂÆÆÆÆº½ÆÆÆ¥
ÄÆvcÆÂÆŸ9ÆnjÆÆKu››¿\R¨ÆÿÄÆðÀ!t‘+’ÆÂÆÆÆÆºÆëÆ¥$ÆÆ�FÆÁÆ›9ÆnjÆÆ)Ziú¶Áx3/º
ÆÿÄÆð”Dƾ’Æ(¶ÆÆÁƺÆâÆ¥³ÆX~Æ=�Æb9Æ_ÆÆkoÆÆÂÆÂÆÆM‰
ÆÿÄÆðÂ.8*/’Æv1EÆk1QºÆâÆ¥#+.EºÆŒ"2G9Ƴ52™¶L4=2ÆB7?9Á
Æé.¯ÿ®ªé.¯ÿ®ªé.¯ÿ®	ªé.¯ÿ®
ªé.¯ÿ®ªé.¯ÿ®ªé.¯ÿ®
ªÿé.¯ÿ®ªÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿ8ÿx=éþ̯<éý̯¯;éÿ̯:éÿ̯9éÿ̯8éÿ̯7éÿ̯6éÿ̯5éÿ̯4éÿÌ	¯3éÿÌ
¯2éÿ̯1éÿÌ¯0éÿÌ
¯/éÿ̯.éÿ̯-éÿ̯,éÿ̯+éÿ̯*éÿ̯)éÿ̯(éÿ̯'éÿ̯&éÿ̯%éÿ̯$éÿ̯
éþÙå
éþÙæéüáÞé̯
éþ?À
éþ5Êéý‹t̯þ¡i
¯þéãéþáäéþ?À
éøäèééäç‹f¯ÿª¯ú­®¯¯•+
¯ó;/YééYB;eéé?ÀéÞ˜né‚&éÉDé:Ëé‹./EW¯‹+)D¯®Y00h¯E
6g¯óÞé:ÆéåééJÈé?ÀéàÑ.éD9΋{é5Êà%áÊ)W¯8†¯g¯‡A¯®)¥•+
¯éõf˜é”KC%�é?Àéá'Ò6Š�N¹é5ʨY̯YW¯®¯˜¯M$66w•+
¯éõh—Ú*æéO‡é?ÀéçcdjÈQ!çé5ÊÃ8¯¯GW¯%ž¯€¯d_¯þ—)
¯éõh—á±�‡é?Àéß¡¨é#Jéé5ÊÌhsW¯d2n5¯¡q‘nž®y•¯éõ©Àéµd‚®¹é”ÕéßÜtàéš°éé�¾¯¦\Y„ƒ¯¯‘gx>¯¯£fEe¨¯�a{¯éÿ̯ûŽ€-u¯éÿKHÿÉéÿ̯üšYV’¯éÿÌ%¯éÿÌ&¯
éÿãéÿãéü^¯žc
¯­¯
éûäéééûÌ[¯‘.
¯þa^¯éКi�Øé¯z‹¦{“ã�¶^¢e¯‘.zV™¯¯f®¯Ÿu§32]–¯ž^|®¯§oM\§
¯éЉ¹”/éβ½Ê¶ÁÚQƒ¦¯‘My�¯¬¯�3¬LI‰£‚-~]A¯ZU�w£
¯ÿèéÒÞ§„$ÈéäééééÚ§¯¯¯‘®¯hW¯¬¯�3¯a^¯¯Ch‰‰©QH”¯ÿæéðâ'‰«3¬éäééé鿯믑 ¯¯~>¯«¯‰3¯a^¯¯$P]ú¡ªj-)¤
¯ÿæéð®Oéà
¬é/×éé"ä̤¯â¯‘ž¯No¯6‹¯V3¯qT¯¯_b¯¯¬¯¬¯¯Dy
¯ÿæéðä6B17¬é‹:Qé~2G¤¯â¯‘&(=¥¯|-?3¯Ÿ/,‡¡C.6,¯:183ª
¯ÿÿÿéÿéÿéÿéÿéÿéÿéÿé@é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é
¯÷—•¯¯®�¦~¦é¯ö9<>3¯«3Vœé¯õ“0¯¯3Œ«˜¯¯é¯øhT¯¯Ya«¯é¯ø�2¯¯6‰«¯îÆîÆîÆîÆîÆîÆîÆîÆ@Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ-Æ
Æ÷«¨ÆÆÅ ¼Ž¼ÆÆö@DG:ÆÁ
9b±ÆÆ÷¦6ÆÆ:žÁ¬ÆÆøv_ÆÆenÁÆÆø£9ÆÆ=›Áƪ¯ª¯ª¯ª¯ª¯ª¯ª¯ª¯@¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯é¯
é÷ÉÆééè¼Ý§Ý¯éöLPSEéäDsЯéõÄ@ééEºäËéé¯éø‹oééw‚äé¯éøÀCééG·äéÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿé@
¯þ{­¯þ‚¦¯þ‘–
¯þ�™¯þ¥ƒ¯þ8ª¯þHš¯þXg
¯þf}¯þ‘.
¯ä3¯Ž®¯¯‹‘œ ¯’ª¯�¦€¦¯¯ª„‚¬¯¯Xg¯Å§•¯ª‰¯¯�­š¡¯¯Ÿƒ�.¯¯«€ �¬¯¯¡‹¯¯™‰©®/AR-§€]©�6¯¯+h¯+”®›¯a_¯¯®”W_¯Xg¯Æ#�OCf†?¯Pn¯¦¯b.¯Tn¯¡¡Ÿ.¯¯]r¯¯©jW¯¯Kh¯+”®¯¯wH¯l2M>=¯Xg¯ÆPo(˜8Xm¯Pn£¯¯†.¯1�¯¯¡q665d¯¯©“1¯¯%h¯+”®¯¯wH¯¥¯W<¯Xg¯ç33¯'(œ¯Pn¯§¯d.¯[j¯œ¡œ(¯þ¬Æ@
Æþ‹ÄÆþ“¼Æþ¥ª
Æþ¢­Æþº”Æþ?ÁÆþR®Æþct
Æþs�Æþ¤4
Æä9Æ¡ÅÆÆ�¥°µÆ¥ÀÆŸ¼‘¼ÆÆÁ–”ÃÆÆctÆÅ½¨ÆÁ›ÆÆ£Ä¯¶ÆÆ³” 4ÆÆÁ‘µ¢ÃÆÆ¶žÆÆ­›¿Å6JD%vÆ1§Ä=[¨Æq`c&¬ÆctÆÅ¸!Æ�¨ÂÆ[}Ær-Y04Æ£&\8¶ÆUF\3½‘	i¿Ÿ=ÆÆ0vÆ1§Ä¯ÆmlÆÆÅ¨clÆctÆÆ(²YKt—GÆ[}Æ!¼Æo4Æ`}ƶ¶³4ÆÆi�ÆÆ¿xcÆÆUvÆ1§ÄÆÆ†RÆ{9WGEÆctÆÆ[~.¬@c{Æ[}¹!ÆÆ—4Æ8¢ÆÆ¶==¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ=¯þÌ<¯ýÌé;¯üÌéé:¯ÿÌéÿ9¯ÿÌéÿ8¯ÿÌéÿ7¯ÿÌéÿ6¯ÿÌéÿ5¯ÿÌéÿ¯þª¯þ†¢
¯üÌéÇÄéÿ¯þA¡¯þR‘	¯ûÌéé„zéÿ¯þš‘¯â�§€¢�¬¯¯ŽŽŸ�¯•§¯�¨�¤¯¯¬‡•äéé„zéÿ¯ÜC8C-¬¯5U–"�¯8Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>ÆÿÆþ�ÀÆþ—·Æþ©¦ÆÿÆþJ¶Æþ\¤ÆþqgÆÿÆþ¯¤ÆâŸ½�·£ÂÆÆ¡¡³²Æ¨½ÆŸ¾’ºÆÆÂ˜�ÁÆÆqgÆÿÆÜK@L2ÂÆ<`©&²Æ@DJ(iÆ>šÆ@[!žÆz^e(¢ÆqgÆÿÆä³*ÆÆG‘ÆŸÆÆ&²¬1ÆÆ=iÆ>次{_Æú©m_ÆqgÆÿÆÛƒRÆÆraÆÅÆÆ&²„UÆÆbiÆ>𯯯”DÆ…7UN8ÆqgÆÿÆø¯,ÆÆJŽÆÆæ&²³+ÆÆ7iÆ>𯯯”DÆ*¯Æp7ÆqgÆÿÆùEDR,ÁÆÆæ&²ÆU69/oÆ>𯯯”DÆUG^27ÆqgÆÿÆþ¦™Æÿ™ÆæžÁÆÆ±µ?¡Æ¥»Æ™ÆÆº¦ÆÄ”޹¢Æ±¯Æÿ!ÆüamE6Æÿº³þ½!Æý³ŒªÆÿuPþ‹>Æÿ>ÆÿÆþ7¾Æþt€Æþº¡
ÆÿÆþ—ÄÆþesÆþ—@
Æé'¯é(¯é)¯é*¯é+¯é,¯é-¯é.¯@Àÿöîm77«°77hó7öín77ª±77fõ7öëp77¨³77eö7öêq77¦´77cø7êès77¥¶77bù77ûÜÏÏëìÏÏÛüÏöûÝÏÏëíÏÏÛýÏöûÝÏÏêíÏÏÚýÏöúÝÏÏêíÏÏÚþÏôúÞÏÏêîÏÏÚþÏÏ€ÿ@>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ=éþÌ<éý̯;éü̯¯:éÿ̯ÿ9éÿ̯ÿ8éÿ̯ÿ7éÿ̯ÿ6éÿ̯ÿ5éÿ̯ÿéþ©âéþ²Ø
éü̯–“¯ÿéþW×éþmÁ	éû̯¯d[¯ÿéþÎÁéâ»ßªØÀäéé½½ÓÑéÆÞé»à¬Úéé䳕«¯¯d[¯ÿéÜYKZ;äéFqÇ-ÑéKPW/{éIµéKk&ºé�aY$�¯d[¯ÿéÛÒ1ééT«é	»éé-ÑË:ééH{éIµé
Àé�oé̯–aT¯d[¯ÿéÛš`éé†réèéé-Ñ›eéés{éIµééé®PÌu1KE1¯d[¯ÿéøÏ4ééW§ééæ-ÑÒ3ééA{éIµééé®F¯%š¯c1¯d[¯ÿéùQOa4äééæ-Ñéd?D7ƒéIµééé˜<¯K>S-1¯d[¯ÿéþÄ´éÿ´éæºäééÐÕJ½éÂÜé´é̤“¯®ƒ~£�¯œš¯ÿ!éür€Q@éÿ̯ÿ¤Ÿþ§!éýÒ¤ÈéÿÌ¯ÿhGþ{)éÿ̯ÿ(éÿ̯ÿéþAàéþ‰—éÿ̯þ¤Ž
¯ÿéþ±æéûw‡ééÌ	¯þ†9
¯ÿÿéÿíÿéÿí ÿéÿí!ÿéÿí"ÿéÿí#ÿéÿí$ÿþéí%ÿÿí&ÿX&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿõ7æt77£¸77`û7öåv77¢¹77_ü7úãx77 ÿõÏùÞÏÏéîÏÏÙþÏöùÞÏÏéïÏÏÙÿÏúùßÏÏéÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ0ÿÿÿ¯ÿÌéÿÿÿÿ¯ÿÌéÿÿÿÿ¯ÿÌéÿÿÿÿ¯ÿÌéÿÿÿèÿ¯ÿÌéÿÿþ¯ÌéÿÿÿÌéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿèÿéÿÿéÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéþíÿÿÿéýíÿÿÿÿéüíÿÿÿÿÿéÿíÿÿÿÿÿéÿíÿÿÿÿÿéÿíÿÿÿÿÿéÿíÿÿÿÿÿÆÿÍîÆÿÍ îÆÿÍ!îÆÿÍ"îÆÿÍ#îÆÿÍ$îþÆÍ%îÿÍ&îX&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿõ7æt77£¸77`û7öåv77¢¹77_ü7úãx77 ÿõÏùÞÏÏéîÏÏÙþÏöùÞÏÏéïÏÏÙÿÏúùßÏÏéÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ0ÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿèÿÆÿÿÆÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿèÿÆÿÿÆÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆþÍÿÿÿÆýÍîÿÿÿÆüÍîîÿÿÿÆÿÍîÿÿÿÿÆÿÍîÿÿÿÿÆÿÍîÿÿÿÿÆÿÍîÿÿÿÿ¯ÿ®ª¯ÿ® ª¯ÿ®!ª¯ÿ®"ª¯ÿ®#ª¯ÿ®$ªþ¯®%ªÿ®&ªX&ÿ&ÿ&ÿ&ÿ&ÿ&ÿ&ÿõ7æt77£¸77`û7öåv77¢¹77_ü7úãx77 ÿõÏùÞÏÏéîÏÏÙþÏöùÞÏÏéïÏÏÙÿÏúùßÏÏéÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ0ÿÿÿéÿ̯ÿÿÿÿéÿ̯ÿÿÿÿéÿ̯ÿÿÿÿéÿ̯ÿÿÿèÿéÿ̯ÿÿþé̯ÿÿÿ̯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿèÿ¯ÿÿ¯ÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯þ®ÿÿÿ¯ý®ªÿÿÿ¯ü®ªªÿÿÿ¯ÿ®ªÿÿÿÿ¯ÿ®ªÿÿÿÿ¯ÿ®ªÿÿÿÿ¯ÿ®ªÿÿÿÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿéþ»éíÉ
éé�	R0ßéécÒéy½é÷ç=éé%'éééþ¯Þéþâ±éüÇ—©äéùѳééׯèéø¼ÈééÂÃéé.é.é.é.é.é‘.ÿ.ÿ.ÿ.ÿ.ÿ.ÿ.ÿÿÿ�ýùìÿþÆD
ÿÿþfýúôÿþ¾)
ÿÿþÍÿñ¯øö¯óÿÿÏÊÿÿÓ˜©õÿûܘ¹½)ÿúÆáÿþ°æÿþÍÿèþéÿ^\ÿÚÊÿ‡qE'ñÿ¾ZC:)ÿútrÿÏ‚ÿã`þéÿô)™0�ÿÞßÿÿD‡ÿ8†ÿÿ[)ÿú¸/ÿŒS?ÿïÀïïõþéÿÿÔKÿÿ©_÷1\þÊÿÿŸ)ÿúôëIÄÿþÍÿôþéÿÿ—)õÿ§ÇøÚþ	Çÿÿœ)ÿû>¤ûVÿþÍÿóþéÿÐÕUaÿÝÇÿøýÿ=‚ÿÿW)ÿû�,Lÿ™ÿþÍÿèþéó&žÿñ"£ÿ”Pq?˜ÿÆR;E)ÿûÄ�ÿÜÿþÀóÿü¿úèÅÿøâËÿÿá«¥Îÿûâ¦ÉïÊÿûû¿îÿÿ.ÿ.ÿ.ÿ.ÿ.ÿ@ÆþŸÆí«	ÆÆ{F(½ÆÆT³Æf¡Æ÷Ä4ÆÆ!ÆÆÆþ•½ÆþÀ—Æü©€�ÁÆù²˜ÆÆ¶•ÅÆø ªÆÆ¥¥ÆÆ.Æ.Æ.Æ.Æ.Æ‘.î.î.î.î.î.î.îîÿxwýèÜîþ¹?
îîþ_wýéwäîþ±&
îîþ¿îñ£çæ£ãîîÁ½îîÅŽžåîûÍŽ­°&îú¹Òîí¤×îþ¿îèíÙîXVî˽î~i@$áî±T?6&îúljîÁyîãZíÙîä&�-…îÏÐîî?~î4}îîU&îú¬,îƒM;îï³ßßåíÙîîÆFîîžY÷.Ví½îî”&îúäÛD·îþ¿îôíÙîî�&åøËíºîî’&îû:™êPîþ¿îóíÙîÂÇO[îκîøìî9yîîQ&îûx)Gî�îþ¿îèíÙã#“îá ˜îŠ
Ki;Žî¹
M7@&îû·…îÍîþ³ãîü²éÙ¸îøÓ½îîÒ šÀîûÓ›¼ß½îûê²Þîî.î.î.î.î.î@¯þ�¯í—¯¯l>$§¯¯Jž¯[ޝ÷®.¯¯¯¯¯þ„§¯þª…¯ü–q«¯ù�‡¯¯¡„®¯ø�–¯¯‘’¯¯.¯.¯.¯.¯.¯‘.ª.ª.ª.ª.ª.ª.ªªÿVUý¦�ªþ„-
ªªþDUý§U£ªþ
ªªþ‰ªñu¥¤u¢ªªŠ‡ªª�eq£ªû“e{~ªú„–ª©u™ªþ‰ªè©›ª?=ª‘
‡ªZK.¡ª<-'ªúMLªŠWªã

@©›ª£f _ª”•ªª-Zª%Yªª=ªú{ª]7*ªï€ŸŸ£©›ªª�2ªªq?÷!=©‡ªªjªú£�1ƒªþ‰ªô©›ªªe£ªo…ø‘©…ªªhªû)m§9ªþ‰ªó©›ª‹Ž9Aª“…ªø©ª)Wªª:ªûV3ªfªþ‰ªè©›¢iª¡mªc	5K*eª„	7'.ªûƒ_ª“ªþ€¢ªü§›ƒªø—‡ªª–rn‰ªû—o†Ÿ‡ªû§Ÿªª.ª.ª.ª.ª.ª@/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿáæ×é 	T+>Aééh30'ŸéÕ-"eW)éé\*OÉéöé¯äé鯗ÁÔ¿éôÎÃÚºé鿝’¤ÛéýÕ¯Ýééú�°¨L+çééû‘LEoØé.é.é.é‘.ÿ.ÿ.ÿ.ÿ.ÿ.ÿ.ÿÿþºPÿþi¡ÿþîÞÿÿþØ›ÿþT“ÿþ¢MÿÞÿù°þçÁÿÿþ¶šÝT“ÿÿå›­ò¯üÿÿã Ÿçÿño5¯¶ÿÞÿ¿'ÿ°7ÿÿ]o&+“ÿÞH`Aõÿ²c[ÒçL$wƒÿàÿ|kÿ°7ÿÍíÿí“ÿiYÿÿ–õü®ÿÿxSÿ¢Mÿ÷õ9®ÿ°7ÿœ4ÿì5“ÿ8šÿÿ×õÝ\__D(ÿ¢Mÿ÷«îÿ°7ÿž1ÿó2“ÿ?�ÿÿÊõÛ¸ÇüÎÿ¢Mÿç,4ÿÿ°7ÿÒéÿê“ÿ~=öþrõü“ÿüýÿ¿7ÿßwÿÿ°7ÿÿjg'0“ÿð6(`ùÿ»>tNjÿú$CfÿúÁèÿÿìÍÿôÁªçÕäÿÿüÓå«ÿûí³¡Âûÿþ×Åÿÿú÷�ËŸ,xÿÿúútKT—ýÿ.ÿ.ÿ.ÿQáĶƈG$57ÆÆY+)!‡Æµ&VJ#ÆÆN$D«ÆöÆ”ÂÆÆ¨€¤´¢Æô¯¥ºžÆÆÄ”|‹ºÆýµ•¼ÆÆú{–�@$ÄÆÆû{@:^·Æ.Æ.Æ.Æ‘.î.î.î.î.î.î.îîþ®Kîþb–îþÞÏîîþÊ‘îþN‰îþ—HîÞîè¤íØ´îîíª�ÎN‰îîÖ‘¡â£ëîîÔ•”Øîáh1£ªîÞî²$î¤3îîWh#(‰îÏCZ=åî¦\UÄØG"ozîàîtdî¤3î¿Ýî݉îbSîîŒåë¢îîpMî—Hî÷å5¢î¤3î’1îì1‰î4�îîÉåÎVYY?%î—Hî÷ Þî¤3î“.îó/‰î;„îî½å̬ºüÀî—Hîç)1îî¤3îÄÙîÚ‰îv9æíjåë‰îüìî²3îßoîî¤3îîc`$-‰îà2%Zèî¯:lIcîé"?_îú´ÙîîÜ¿îô´ŸØÇÕîîëÅÖ îûݧ–µêîþɸîîú熽”)pîîúélFN�ìî.î.î.îQá­¡¯x? /1¯¯N&$w¯ "LA¯¯E <—¯ö¯ƒ¬¯¯•q‘Ÿ�¯ôš’¤Œ¯¯­ƒn{¥¯ý „¦¯¯úl„~9 ®¯¯ûm93S¢¯.¯.¯.¯‘.ª.ª.ª.ª.ª.ª.ªªþ|5ªþFkªþŸ”ªªþ�gªþ8bªþl3ªÞª¦u©š�ªª©yg“8bªª™gs¡u¨ªª—kjšª¡J#uyªÞªªu%ªª>Jbª”0@+£ªwB=Œš3OWªàªSGªu%ª‰žªžbªF;ªªd£¨tªªP7ªl3ª÷£&tªu%ªh#ªì#bª%gªª�£“=??-ªl3ª÷rŸªu%ªi!ªó!bª*^ªª‡£’{…ü‰ªl3ªç#ªªu%ªŒ›ªœbªT)¤©L£¨bªü©ª%ªßOªªu%ªªG
E bª $@¦ª})M4Gª§-Dªú�›ªª�‰ªô�qšŽ˜ªª¨�™rªûžwk�§ªþ�ƒªªú¥`‡jPªªú§M28e©ª.ª.ª.ªQ/ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ¯õsy¯oR¯–Z¯C²ÿÿ¯õ„<¯0¬f`¯×ÿÿ¯õ«©*M‡7޹ Ýÿÿ
¯ö0aR|V¸ä!Ýÿÿ
¯ö^�§>äé!Ýÿÿ
¯ö‘-ª¯KšééSáÿÿ¯þºåéÿÿ¯þºåéÿÿ¯þ»åéÿÿ
¯þ»æéÿÿ¯þ¼æéÿÿ¯þ¼æ	éÿÿ
¯þ½æ
éÿÿ	¯þ½æéÿÿ¯þ¾æéÿÿ¯þ¾ç
éÿÿ¯þ¾çéÿÿ¯þ¿çéÿÿ¯þÀçéÿÿ¯þÀçéÿÿ¯þÁèéÿû¯¯Áèéÿü¯ÂèéÿýÂèéÿþèéÿÿéÿÿéÿÿéÿÿéÿÿéÿÿéÿÿéÿÿéÿÿéÿÿéÿÿéÿÿéÿÿéÿÿé�>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿÆõ‚‰Æ~\ƪfÆL¿ÿÿÆõ•DÆ6#ÃsmƼÿÿÆõÁ¿/W˜>¡Æ¼ÿÿ
Æö6n]ŒbÅÆ¼ÿÿ
Æöj’½BÆÆ¼ÿÿ
Æö¤3ÀÆP†ÆÆG¿ÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆÿÿÆ�>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿéõš¡é”méÈxéZØÿÿéõ¯Pé@)å‡€é ¬ÿÿéõäá8f³I½ß¦ÿÿ
éö@‚n¤s!Þ´¦ÿÿ
éö}¬ÞJ´¯¦ÿÿ
éöÁ<âéZy¯¯>©ÿÿéþÞ³¯ÿÿéþÞ³¯ÿÿéþݳ¯ÿÿ
éþݲ¯ÿÿéþܲ¯ÿÿéþܲ	¯ÿÿ
éþÛ²
¯ÿÿ	éþÛ²¯ÿÿéþÚ²¯ÿÿéþÚ±
¯ÿÿéþÚ±¯ÿÿéþÙ±¯ÿÿéþر¯ÿÿéþر¯ÿÿéþ×°¯ÿûéé×°¯ÿüéÖ°¯ÿýÖ°¯ÿþ°¯ÿÿ¯ÿÿ¯ÿÿ¯ÿÿ¯ÿÿ¯ÿÿ¯ÿÿ¯ÿÿ¯ÿÿ¯ÿÿ¯ÿÿ¯ÿÿ¯ÿÿ¯ÿÿ¯�>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>éã³HOe‡éÏe=‘iéé§O_ÂéIH—éÿÿëÞ!ÄÁ‡éH–ÜR6é³E×Ç0é²Léÿÿë¢`ééW‡äèé­6édT{{%»²LéÿÿñždééZ‡æçé¥6é]{¶ýÚ³LéÿÿêÜ#ÊLJéQ�ËI7é±CÔéÆãÔ0àéÿÿéé«=F| éÞQ„UéèŸI3Qàé„E�éÿÿéûÏäÙSš#éÿÿéû«?C�ã#éÿÿ.éþôÿ-éýôÿÿ,éüôÿÿÿ+éÿôÿÿÿ*éÿôÿÿÿ)éÿôÿÿÿ(éÿôÿÿÿ'éÿôÿÿÿ&éÿôÿÿÿ%éÿôÿÿÿ$éÿô	ÿÿÿ#éÿô
ÿÿÿ"éÿôÿÿÿ!éÿôÿÿÿ éÿô
ÿÿÿéÿôÿÿÿéÿôÿÿÿéÿôÿÿÿéÿôÿÿÿéÿôÿÿÿéÿôÿÿÿéÿôÿÿÿéÿôÿÿÿéÿôÿÿÿéÿôÿÿÿéÿôÿÿÿéÿôÿÿÿéÿôÿÿÿéÿôÿÿÿéÿôÿÿÿéÿôÿÿÿ€ÀÿéÆ˜=CUsưV4{YÆÆŽCQ¥Æ>=€Æÿÿë½§¤sÆ=»F.Ƙ;¶©)Æ—@Æÿÿë‰RÆÆJsÁÅÆ“.ÆUGii Ÿ—@Æÿÿñ†UÆÆMsÄÄÆ�.ÆOi›ýº˜@Æÿÿ껬©sÆEm¬>/Æ—9´Æ¨Á´(¾Æÿÿ鯑4+E¾Æp:yÆÿÿÆû°Á¸Gƒ#ÆÿÿÆû‘69yÁ#Æÿÿ.ÆþÚÿ-ÆýÚîÿ,ÆüÚîîÿ+ÆÿÚîÿÿ*ÆÿÚîÿÿ)ÆÿÚîÿÿ(ÆÿÚîÿÿ'ÆÿÚîÿÿ&ÆÿÚîÿÿ%ÆÿÚîÿÿ$ÆÿÚ	îÿÿ#ÆÿÚ
îÿÿ"ÆÿÚîÿÿ!ÆÿÚîÿÿ ÆÿÚ
îÿÿÆÿÚîÿÿÆÿÚîÿÿÆÿÚîÿÿÆÿÚîÿÿÆÿÚîÿÿÆÿÚîÿÿÆÿÚîÿÿÆÿÚîÿÿÆÿÚîÿÿÆÿÚîÿÿÆÿÚîÿÿÆÿÚîÿÿÆÿÚîÿÿÆÿÚîÿÿÆÿÚîÿÿÆÿÚîÿÿ€Àÿ鵇6;Kf¯œL.mO¯¯~;G‘¯76q¯ÿÿë§”‘f¯6q¥>(¯‡4¡–$¯†9¯ÿÿëyH¯¯Af«®¯‚(¯K?]]�†9¯ÿÿñwK¯¯Df­®¯|(¯F]‰ý¤‡9¯ÿÿ꥘–f¯=a˜7)¯…2Ÿ¯•ªŸ$¨¯ÿÿ鯀.5]x¯§_=d@¯®w7&=¨¯c3k¯ÿÿ¯ûœ«£>t#¯ÿÿ¯û€/2kª#¯ÿÿ.¯þ¬ÿ-¯ý¬ªÿ,¯ü¬ªªÿ+¯ÿ¬ªÿÿ*¯ÿ¬ªÿÿ)¯ÿ¬ªÿÿ(¯ÿ¬ªÿÿ'¯ÿ¬ªÿÿ&¯ÿ¬ªÿÿ%¯ÿ¬ªÿÿ$¯ÿ¬	ªÿÿ#¯ÿ¬
ªÿÿ"¯ÿ¬ªÿÿ!¯ÿ¬ªÿÿ ¯ÿ¬
ªÿÿ¯ÿ¬ªÿÿ¯ÿ¬ªÿÿ¯ÿ¬ªÿÿ¯ÿ¬ªÿÿ¯ÿ¬ªÿÿ¯ÿ¬ªÿÿ¯ÿ¬ªÿÿ¯ÿ¬ªÿÿ¯ÿ¬ªÿÿ¯ÿ¬ªÿÿ¯ÿ¬ªÿÿ¯ÿ¬ªÿÿ¯ÿ¬ªÿÿ¯ÿ¬ªÿÿ¯ÿ¬ªÿÿ¯ÿ¬ªÿÿ€Àÿ
ÿÿ¯àŸ#Q1P¯‚=¯¯�"¯ .>,–¯¯…:¯¯Nq®Y@ÿ¯õ®�𝝤“¯¯«Œ¯ï”¤¯¯‡¯¥’¯¯— ¯§w�ÿ¯û¨C^-\
¯ÿ¯ý‘{¤¯ÿ&¯ÿ&¯ÿ&¯ÿ&¯ÿ&¯ÿ&¯ÿ&¯ÿ&¯ÿ&¯ÿ&¯ÿ&¯ÿ&¯ÿ8ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ&ÿ€ÀÿÿÆà³(\7[Æ”EÆÆ²'Ƶ4G2ªÆÆ—BÆÆY€ÅeHÿÆõÅ’¯ÆÆº¦ÆÆÁžÆï§º ÆÆ™Æº¥ÆÆ«µÆ½†ŸÿÆû¾Lj2h
ÆÿÆý¤‹ºÆÿ&Æÿ&Æÿ&Æÿ&Æÿ&Æÿ&Æÿ&Æÿ&Æÿ&Æÿ&Æÿ&Æÿ&Æÿ8ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ&ÿ€ÀÿÿéàÓ/lAké®QééÑ.éÕ=S:!Èéé±Nééh—è%wUÿéõè¬ÎééÚÄééäºéïÄÚ%éé´éÛÃééÉÕéÞž»ÿéûàZ};z
éÿéýÁ¤Úéÿ&éÿ&éÿ&éÿ&éÿ&éÿ&éÿ&éÿ&éÿ&éÿ&éÿ&éÿ&éÿ8ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ-ÿ&ÿ€Àÿÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ'ÿû f¯/�¯ß„Н&Zéé5ÊéOO^)té¨0XE)éâ=[‡XÉé-g¤éûš�¯�§¯ê¬ƒ­ÌÄÏéé¼âééµ±ÑÌééäÄ¢eéøÃ–Áééç¼Æé¯Ÿü¬¯¯Ì
éû§po0¶é¯ÿIGýš¯Ìéüã±®æé
¯ÿÌ3é	¯ÿÌ4é¯ÿÌ5é¯ÿÌ6é¯ÿÌ7é¯ÿÌ8é¯ÿÌ9é¯ÿÌ:é¯ÿÌ;é¯ÿÌ<éþ¯Ì=éÿÌ>é?ÿþü˜=ÿýƒÜ;ÿüî%F;ÿûW´9ÿúþ�k³;ÿþ×'=ÿþ×'=ÿþ×'=ÿþ×';ÿúþ�k³:ÿûW´:ÿüî%F<ÿýƒÜ<ÿþü˜+ÿ€Àÿû$sÆ6£Æß–œÆ%LÆÆ-¬ÆDDP#cÆ�)K;#ÆÀ4NsK«Æ&X‹Æû®²Æ¢½ÆêÂ”ÄÆ¦°ÆÆ ÀÆÆš—²­ÆÆÁ¦‰UÆø¥¤ÆÆÄ ¨Æÿ´³ÿÃÆûŽ`^)›#ÆÿRPÿ¯ÆüÁ—”Ä Æ?ÿþü˜=ÿýƒÜ;ÿüî%F;ÿûW´9ÿúþ�k³;ÿþ×'=ÿþ×'=ÿþ×'=ÿþ×';ÿúþ�k³:ÿûW´:ÿüî%F<ÿýƒÜ<ÿþü˜+ÿ€Àÿû+‡é?Àéß°¸é&C¯¯(˜¯<¯?ÿþü˜=ÿýƒÜ;ÿüî%F;ÿûW´9ÿúþ�k³;ÿþ×'=ÿþ×'=ÿþ×'=ÿþ×';ÿúþ�k³:ÿûW´:ÿüî%F<ÿýƒÜ<ÿþü˜+ÿ€Àÿ
ÿé¯þq^¯é¯þ­‡¯é¯é¯é¯ÿ¨é¯ÿaé	¯ÿ«¯ÿ¬¯þª§¯û®¤¯¯­é¯èq6"$X¯¯o+*Q®¯A31V¯^/58¯dé¯òn©­*”«§®6…š+¯þ¬¯ÿaé¯ènB¯¯Zb�<¯¯jR¯<"[¡¯\N•¯aé¯èn/¯¯Gw—)¯¯Vh¯¯Ÿ\2¯¯¦s¬aé¯ènhs§¯$cw�“s’l<¨g�‚"­aé¯ènObTŸ¯¯©kYž¯žWCf¡¬_C[–¯ˆé¯þnQ¯é¯þ‰w¯é¯ ¯ÿÌÿ¯þÌéÿ¯ýÌééÿ¯ÿÌéÿ¯ÿÌéÿ¯ÿÌéÿ¯ÿÌéÿ¯ÿÌéÿ¯ÿÌéÿ¯ÿÌéÿ¯ÿÌ	éÿ¯ÿÌ
éÿ¯ÿÌéÿ¯ÿÌéÿ¯ÿÌ
éÿ¯ÿÌéÿ
¯ÿÌéÿ¯ÿÌéÿ¯ÿÌéÿ
¯ÿÌéÿ	¯ÿÌéÿ¯ÿÌéÿ¯ÿÌé€ÀÿÆÆþj#ÆÆþĘ#Æ-Æ-ÆÆÿ¾ÆÆÿmÆ	ÆÿÁÆÿÂÆþÁ½ÆûÄºÆÆÄÆÆè='(cÆÆ~0/\ÄÆI97aÆj5<@ÆqÆÆò}¿Ä/¨Á½Ä=—®0ÆþÂÆÿmÆÆè}KÆÆfo’DÆÆx\ÆD'g¶Æh X¨ÆmÆÆè}6ÆÆQ‡«/ÆÆauÆÆ³h9ÆÆ¼‚ÃmÆÆè}u‚½Æ)p‡²¦‚¥{D¾t£”'ÄmÆÆè}Yo`´ÆÆ¿yd³Æ³cKt¶ÂlLg©ÆšÆÆþ}\$ÆÆþ›‡$ÆÆ ÆÿÆÿÆÿÆÿÆÿÆÿÆÿÆÿÆÿÆÿÆÿÆÿÆÿÆÿÆÿÆÿÆÿÆÿÆÿÆÿÆÿÆÿÆ€Àÿ¯éþ–}é¯éþæ³é¯é¯é¯éÿà¯éÿ�¯	éÿäéÿäéþãÞéûçÚé鿝éè–H.0uéé”98lçéVDAré}>FK鄯éò“áæ8Åä#ßçG±Í9éþ!äéÿ�¯éè“Xééxƒ¬Oéé�méP.zÖéz%gÆé�¯éè“?éé_ŸÉ7éérŠééÓzCééÝšå�¯é蓊šßé0„ŸÑÄšÃ�Pà‰À®.æ�¯éè“iƒpÔééá�vÒéÒtYˆ×äZzÇ鵯éþ“lé¯éþ¶Ÿé¯é éÿÌÿéþ̯ÿéý̯¯ÿéÿ̯ÿéÿ̯ÿéÿ̯ÿéÿ̯ÿéÿ̯ÿéÿ̯ÿéÿ̯ÿéÿÌ	¯ÿéÿÌ
¯ÿéÿ̯ÿéÿÌ¯ÿéÿÌ
¯ÿéÿ̯ÿ
éÿ̯ÿéÿ̯ÿéÿ̯ÿ
éÿ̯ÿ	éÿ̯ÿéÿ̯ÿéÿ̯€Àÿÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿ¯ÿ ¯ÿÌéÿÙéþäÜéþßÖéþÖÙé
¯ÿÌ0é¯ÿÌ1é¯ÿÌ2éü§¯¤¬¯üª¦¯Ì
éþãÜéüØçæÜéü_¯3Œ¯ývIÌéûŸ`饼éû+Ôä5åéö­¯3Œ«®¯¯vV
éþçßéûŸ`éj•éÿäéüÝééäéùãé+Ôéw“éïa¯3A&&¯Šbá%ãéÛ"äéÚ¤9CDÃéŸ`É!/HÈÎZAG¸éyT1lÔ^AE¯é×S3A!ÔéÃ<éð_¯3J¯š!Ì�béb˜é‰kéÚåééªhéŸ`éj•ééi éÎEéu:æéw’é×:é‡ué½Ôéâéð_¯3}¯¯%Ñ�béÁ:é.ÈéÙÊYAC-éŸ`éj•ééFHHáu}éé*CHH×H¹ééÔééèéñ_¯3j¯Ëã�béé7–=éñ[¦é¯'éŸ`ém“éé<ÈéúuŠééI»é÷c éáÔéÚ!éñ_¯3'”lOé�béé–›éÚeq°H'éŸ`é›M­Ý”D®¹ŽéuŠéé¢<ª»Šéª3¬a	Ôé´Néñ‡¯q j�åéÄ¥ééÓäéëØ}g¼ˆéĤéä¤wÑéÄnd¤é¯¹éòÉrb�éé¼e¤ŠßéZ³é¯ÿÌéýÉ_„(éþàZéþ¯Ìéý‡„å1éÿÌ~éÿô=éþôÿ<éýôÿÿ;éÿôÿ:éÿôÿ9éÿôÿ8éÿôÿ7éÿôÿ6éÿôÿ5éÿôÿ4éÿô	ÿ3éÿô
ÿ2éÿôÿ1éÿôÿ0éÿô
ÿ€ÀÿÆÿµÆÿ¸Æþ»Æþ½¶Æþ¶¹ÍÆü½ÆºÃÆþÁ¼ÆþÁ»Æü·ÄÄ»ÆükÆ:žÆþ†SÆû‡RÆ� Æû$´Á-ÃÆöÄÆ:žÁÅÆÆ†S
ÆþĽÆû‡RÆZÆÿÂÆü¼ÆÆÂÆùÁÆ$´Æe}ÆïnÆ:I++�ƆS¿ ÁƺÂÆÚ‹099¥Æ‡R«(=ª¯M7=œÆfG*\´P7;•ƶG+7´Æ¥3ÆðkÆ:TƯ%ƆSÆS�Æt[ÆÚÃÆÆ�YƇRÆZÆÆYˆÆ¯:Æc2ÄÆe|ƶ2ÆscÆ¡´ÆÀÆðkÆ:�ÆÆ$²†SƤ2Æ'ªÆÙ¬K79&ƇRÆZÆÆ<==¿cjÆÆ$9==¶=�ÆÆ´ÆÆÅÆñkÆ:xÆÅÁ†SÆÆ/4ÆñN�Æ•!ƇRÆ\}ÆÆ3ªÆúcuÆÆ>ŸÆ÷TˆÆ¿´ÆºÆñkÆ:,�\DƆSÆÆ„ÆÚV`–=!ƇRÆ„A“¼~9”žxÆcuÆÆ‰3�ŸuÆ�+’R´Æ™BÆñ™Æ€›Z{ÃÆ¦�ÆÆ³ÂÆë·jX tƧŒÆÁ‹e²Æ§]U‹Æ•žÆò«aS†ÆÆ U‹u½ÆM˜Æý«Qp(Æþ¾MÆýspñÆÿÚ=ÆþÚî<ÆýÚîî;ÆÿÚî:ÆÿÚî9ÆÿÚî8ÆÿÚî7ÆÿÚî6ÆÿÚî5ÆÿÚî4ÆÿÚ	î3ÆÿÚ
î2ÆÿÚî1ÆÿÚî0ÆÿÚ
î€ÀÿéÿÕéÿ̯ÿ£¯þ¬¥¯þ§¡¯þ¡£¯
éÿÌ0¯éÿÌ1¯éÿÌ2¯üßéÚåéüãÝéÌ
¯þª¥¯ü¢®­¥¯ü~éEºéý�b̯ûwH¯|�¯û Ÿ«(¬¯öæéEºäèéé�V
¯þ®§¯ûwH¯Pp¯ÿ¬¯ü¦¯¯¬¯ùª¯ Ÿ¯Yn¯ï‚éEV33©éŠI©ª¯¥¬¯Ú{+23’¯wH—#6–šD16Н[?%QŸG14„¯¡>&1Ÿ¯’-¯ð~éEcéÎ,ÌvI¯Ir¯gP¯Ú¬¯¯€N¯wH¯Pp¯¯Ox¯š3¯X,­¯Yn¯¡,¯fX¯ŽŸ¯ª¯ð~éE¦éé%�vI¯‘,¯"–¯Ù˜C12"¯wH¯Pp¯¯566©X^¯¯ 266¡6‹¯¯Ÿ¯¯®¯ñ~éEŽé˪vI¯¯)q.¯ñE}¯„¯wH¯Rn¯¯-–¯úXh¯¯7�¯÷Jx¯©Ÿ¯¤¯ñ~éE4”Q<¯vI¯¯qu¯ÚLU„6¯wH¯u:‚¦o3‚‹j¯Xh¯¯y-€�h¯€&�IŸ¯‡:¯ñ´é— Pl¬¯“|¯¯Ÿ¬¯ë¢^N�f¯”|¯«{Y�¯”RK{¯„‹¯ò—VIv¯¯�K{h§¯D‡¯éÿ̯ý—Gc(¯þ¨D¯þé̯ýfc¬1¯ÿÌ~¯ÿ¬=¯þ¬ª<¯ý¬ªª;¯ÿ¬ª:¯ÿ¬ª9¯ÿ¬ª8¯ÿ¬ª7¯ÿ¬ª6¯ÿ¬ª5¯ÿ¬ª4¯ÿ¬	ª3¯ÿ¬
ª2¯ÿ¬ª1¯ÿ¬ª0¯ÿ¬
ª€Àÿ
ÿé¯ù3AC.®«¯éÿô
¯ú�Œ¯¯®‰¯éþôÿ¯éýôÿÿ¯éÿôÿ¯éÿôÿ¯éÿôÿ¯éÿôÿ¯éÿôÿ¯
éÿôÿ¯éÿôÿ¯éÿô	ÿ¯
éÿô
ÿ¯	éÿôÿ¯éÿôÿ¯éÿô
ÿ¯éÿôÿ¯éÿôÿ¯éÿôÿ¯éÿôÿ¯éÿôÿ¯éÿôÿ¯þéôÿ¯ÿôÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯€ÀÿÆÆù:JL4ÅÁÆÿÚ
Æú¢žÆÆÅ›ÆþÚî*ÆýÚîî)ÆÿÚî(ÆÿÚî'ÆÿÚî&ÆÿÚî%ÆÿÚî$ÆÿÚî#ÆÿÚî"ÆÿÚ	î!ÆÿÚ
î ÆÿÚîÆÿÚîÆÿÚ
îÆÿÚîÆÿÚîÆÿÚîÆÿÚîÆÿÚîÆÿÚîÆÿÚîÆÿÚîÆîÆîÆîÆîÆîÆîÆîÆîÆîÆîÆîÆîÆîÆîÆîÆ€Àÿ¯éùEWZ=èäé¯ÿ¬
éú¾ºééè¶é¯þ¬ªé¯ý¬ªªé¯ÿ¬ªé¯ÿ¬ªé¯ÿ¬ªé¯ÿ¬ªé¯ÿ¬ªé
¯ÿ¬ªé¯ÿ¬ªé¯ÿ¬	ªé
¯ÿ¬
ªé	¯ÿ¬ªé¯ÿ¬ªé¯ÿ¬
ªé¯ÿ¬ªé¯ÿ¬ªé¯ÿ¬ªé¯ÿ¬ªé¯ÿ¬ªé¯ÿ¬ªéþ¯¬ªéÿ¬ªéªéªéªéªéªéªéªéªéªéªéªéªéªéªéªé€Àÿÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿ橯A3/&n¯+”®¯¯wH¯?EP)<¯Xg¯Æ¨a¯I¯¯Pn¯`*T(.¯˜)A.¥¯G:dNx¯D@‰®¯¯š£-š¯Ž¨¯ˆ¯¯¡–¯­€€¢“¯š�¯ñŠ£¯�‘¯¯˜Ÿ¯¯˜z£�¯ý’�$¯ùŸq‡­¯¯–¯üP`:9¯ÿ£Ÿÿ¨¯ûœI\)i¯ý›{š¯ÿaGÿ‚¯ü®�|§˯€Àÿæ¿ÆJ96+|Æ1§ÄÆÆ†RÆGNZ/DÆctÆÆ¾mÆSÆÆ[}Æm/_-4Ƭ/J4»ÆPAqYˆÆMH›ÄÆÆ¯¸2®Æ¡¾ÆšÆÆ¶©ÆÄ�‘·¦Æ®²Æñœ¹Æ²¥ÆÆ¬´ÆÆ¬Š¸¢Æý¥²)Æù³˜ÄÆÆ©Æü[mA@Æÿ¸³ÿ¾Æû°Rh/wÆý¯‹®ÆÿmPÿ”ÆüÅ �½ËÆ€ÀÿæáéWD?3’é:Äçééž`éT\j7Péu‰éÆà�éb#éék“é€8o5=éÊ7W=Üé^M…h éZU·çééÎÙ;Íé½àéµéé×Çéæª«ØÄéÍÑéñ¸ÙéÑÂééÊÔééÊ£Ù¾éýÃÑ0éùÓ–³æééÇéük€MLéÿÙÓÿàéûÏaz7ŒéýϤÍéÿ�^ÿ®éüè¼¥ßËé€Àÿ
ÿÿf¯ÿÿÿÿ‹¯ÿÿÿ¯ÿÿèÿ¯ÿ¯ÿ¯ÿÿÿ¯ÿÿÿ¯ÿÿÿ¯ÿÿÿ¯ÿÿÿ¯ÿÿÿ¯ÿÿÿ¯ÿÿÿ¯ÿÿÿ¯ÿÿÿ¯ÿÿÿ¯ÿÿÿ¯ÿÿèÿ¯ÿ¯ÿ¯ÿÿÿ¯ÿÿÿ¯ÿÿÿ¯ÿÿÿ¯ÿÿÿ¯ÿÿÿ¯ÿÿÿ¯ÿÿÿ¯ÿÿÿ¯ÿÿÿ¯ÿÿÿ¯ÿÿÿ¯ÿúè®77iñ7¯ÿûìÏÏÛüϯÿ¯ÿ¯ÿ¯ÿ¯ÿ€ÀÿÿtÆÿÿÿÿ�ÆÿÿÿÆÿÿèÿÆÿÆÿÆÿÿÿÆÿÿÿÆÿÿÿÆÿÿÿÆÿÿÿÆÿÿÿÆÿÿÿÆÿÿÿÆÿÿÿÆÿÿÿÆÿÿÿÆÿÿÿÆÿÿèÿÆÿÆÿÆÿÿÿÆÿÿÿÆÿÿÿÆÿÿÿÆÿÿÿÆÿÿÿÆÿÿÿÆÿÿÿÆÿÿÿÆÿÿÿÆÿÿÿÆÿÿÿÆÿúè®77iñ7ÆÿûìÏÏÛüÏÆÿÆÿÆÿÆÿÆÿ€Àÿÿˆéÿÿÿÿ¹éÿÿÿéÿÿèÿéÿéÿéÿÿÿéÿÿÿéÿÿÿéÿÿÿéÿÿÿéÿÿÿéÿÿÿéÿÿÿéÿÿÿéÿÿÿéÿÿÿéÿÿÿéÿÿèÿéÿéÿéÿÿÿéÿÿÿéÿÿÿéÿÿÿéÿÿÿéÿÿÿéÿÿÿéÿÿÿéÿÿÿéÿÿÿéÿÿÿéÿÿÿéÿúè®77iñ7éÿûìÏÏÛüÏéÿéÿéÿéÿéÿ€Àÿÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿÿ¯Þsy¯oR¯–Z¯C©¯‡6;Kf¯¶e=‘iéé§O_ÂéIH—éÿ¯à„<¯0¬f`¯¦§”‘fÌH–ÜR6é³E×Ç0é²L
éÿ¯à«©*M‡7ޝ¦yH¯¯Aväèé­6édT{{%»²L
éÿ¯ç0aR|V®¯¦wK¯¯O‡æçé¥6é]{¶ýÚ³L
éÿ¯à^�§:¯¯¦¥˜®‡éQ�ËI7é±CÔéÆãÔ0à	éÿ¯ß‘-ª¯Gv¯¯>©¯€6F| éÞQ„UéèŸI3Qàé„E�éÿ¯ÿÌéûÏäÙSšéÿ¯ÿÌéû«?C�ãéÿ¯ÿÌéÿ¯ÿÌ éÿ¯ÿÌ!éÿ¯ÿÌ"éÿ¯ÿÌ#éÿ¯ÿÌ$éÿ¯ÿÌ%éÿ¯ÿÌ&éÿ¯ÿÌ&éþí¯ÿÌ&éýíÿ¯ÿÌ&éüíÿÿ¯ÿÌ&éÿíÿÿ¯ÿÌ&éÿíÿÿ¯ÿÌ&éÿíÿÿ¯ÿÌ&éÿíÿ?ÿöîm77«°77hó7öín77ª±77fõ7öëp77¨³77eö7öêq77¦´77cø7êès77¥¶77bù77ûÜÏÏëìÏÏÛüÏöûÝÏÏëíÏÏÛýÏöûÝÏÏêíÏÏÚýÏöúÝÏÏêíÏÏÚþÏôúÞÏÏêîÏÏÚþÏÏ@ÿ€ÀÿÿÆÞ‚‰Æ~\ƪfÆL¿Æ˜=CUsưV4{YÆÆŽCQ¥Æ>=€ÆÿÆà•DÆ6#ÃsmƼ½§¤sÆ=»F.Ƙ;¶©)Æ—@
ÆÿÆàÁ¿/W˜>¡Æ¼‰RÆÆJsÁÅÆ“.ÆUGii Ÿ—@
ÆÿÆç6n]ŒbÅÆ¼†UÆÆMsÄÄÆ�.ÆOi›ýº˜@
ÆÿÆàj’½BÆÆ¼»¬©sÆEm¬>/Æ—9´Æ¨Á´(¾	ÆÿÆß¤3ÀÆP†ÆÆG¿Æ‘4+E¾Æp:yÆÿ%Æû°Á¸GƒÆÿ%Æû‘69yÁÆÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ=ÆþÍ<ÆýÍî;ÆüÍîî:ÆÿÍîÿ9ÆÿÍîÿ8ÆÿÍîÿ7ÆÿÍî?ÿöîm77«°77hó7öín77ª±77fõ7öëp77¨³77eö7öêq77¦´77cø7êès77¥¶77bù77ûÜÏÏëìÏÏÛüÏöûÝÏÏëíÏÏÛýÏöûÝÏÏêíÏÏÚýÏöúÝÏÏêíÏÏÚþÏôúÞÏÏêîÏÏÚþÏÏ@ÿ€ÀÿÿéÞš¡é”méÈxéZáé³HOe‡é¶L.mO¯¯~;G‘¯76q¯ÿéà¯Pé@)净é!ÝÞ!ÄÁ‡Ì6q¥>(¯‡4¡–$¯†9
¯ÿéàäá8f³I½é!Ý¢`ééWv«®¯‚(¯K?]]�†9
¯ÿéç@‚n¤s!èé!ÝždééOf­®¯|(¯F]‰ý¤‡9
¯ÿéà}¬ÞNéé!ÝÜ#Ê®f¯=a˜7)¯…2Ÿ¯•ªŸ$¨	¯ÿéßÁ<âé^�ééSáé«65]x¯§_=d@¯®w7&=¨¯c3k¯ÿéÿ̯ûœ«£>t¯ÿéÿ̯û€/2kª¯ÿéÿ̯ÿéÿÌ ¯ÿéÿÌ!¯ÿéÿÌ"¯ÿéÿÌ#¯ÿéÿÌ$¯ÿéÿÌ%¯ÿéÿÌ&¯ÿéÿÌ&¯þ®éÿÌ&¯ý®ªéÿÌ&¯ü®ªªéÿÌ&¯ÿ®ªÿéÿÌ&¯ÿ®ªÿéÿÌ&¯ÿ®ªÿéÿÌ&¯ÿ®ª?ÿöîm77«°77hó7öín77ª±77fõ7öëp77¨³77eö7öêq77¦´77cø7êès77¥¶77bù77ûÜÏÏëìÏÏÛüÏöûÝÏÏëíÏÏÛýÏöûÝÏÏêíÏÏÚýÏöúÝÏÏêíÏÏÚþÏôúÞÏÏêîÏÏÚþÏÏ@ÿ€Àÿ
ÿéÿíÿÿÿÿÿ
éÿíÿÿÿÿÿéÿíÿÿÿÿèÿéÿí	ÿÿÿ
éÿí
ÿÿÿ	éÿíÿÿÿÿÿéÿíÿÿÿÿÿéÿí
ÿÿÿÿÿéÿíÿÿÿÿÿéÿíÿÿÿÿÿéÿíÿÿÿÿÿéÿíÿÿÿÿÿéÿíÿÿÿÿÿéÿíÿÿÿÿÿþéíÿÿÿÿÿÿíÿÿÿÿÿÿÿÿÿÿÿÿÿÿèÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ0ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿõ7æt77£¸77`û7öåv77¢¹77_ü7úãx77 èÿõÏùÞÏÏéîÏÏÙþÏöùÞÏÏéïÏÏÙÿÏûùßÏÏéÿ&ÿ&ÿ&ÿ&ÿ&ÿ™ÀÿÆÿÍîÿÿÿÿ
ÆÿÍîÿÿÿÿÆÿÍîÿÿÿèÿÆÿÍ	îÿÿ
ÆÿÍ
îÿÿ	ÆÿÍîÿÿÿÿÆÿÍîÿÿÿÿÆÿÍ
îÿÿÿÿÆÿÍîÿÿÿÿÆÿÍîÿÿÿÿÆÿÍîÿÿÿÿÆÿÍîÿÿÿÿÆÿÍîÿÿÿÿÆÿÍîÿÿÿÿþÆÍîÿÿÿÿÿÍîÿÿÿÿîÿÿÿÿîÿÿÿèÿîÿÿîÿÿîÿÿÿÿîÿÿÿÿîÿÿÿÿ0ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿõ7æt77£¸77`û7öåv77¢¹77_ü7úãx77 èÿõÏùÞÏÏéîÏÏÙþÏöùÞÏÏéïÏÏÙÿÏûùßÏÏéÿ&ÿ&ÿ&ÿ&ÿ&ÿ™Àÿ¯ÿ®ªÿÿÿÿ
¯ÿ®ªÿÿÿÿ¯ÿ®ªÿÿÿèÿ¯ÿ®	ªÿÿ
¯ÿ®
ªÿÿ	¯ÿ®ªÿÿÿÿ¯ÿ®ªÿÿÿÿ¯ÿ®
ªÿÿÿÿ¯ÿ®ªÿÿÿÿ¯ÿ®ªÿÿÿÿ¯ÿ®ªÿÿÿÿ¯ÿ®ªÿÿÿÿ¯ÿ®ªÿÿÿÿ¯ÿ®ªÿÿÿÿþ¯®ªÿÿÿÿÿ®ªÿÿÿÿªÿÿÿÿªÿÿÿèÿªÿÿªÿÿªÿÿÿÿªÿÿÿÿªÿÿÿÿ0ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿõ7æt77£¸77`û7öåv77¢¹77_ü7úãx77 èÿõÏùÞÏÏéîÏÏÙþÏöùÞÏÏéïÏÏÙÿÏûùßÏÏéÿ&ÿ&ÿ&ÿ&ÿ&ÿ™Àÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ'ÿ
@Àÿ
@Àÿ
@Àÿ
@>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿ
@>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿ
@>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿþÿÿ
ÿÿÿþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
ÿ	ÿÿÿÿÿ
ÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿÿ	ÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿýÿÿÿÿÿ	ÿÿÿ	ÿÿÿ"ÿÿÿþÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ.¯þéÿ-¯éÿÿ,¯éÿÿ+¯éÿÿ*¯éÿÿ)¯éÿÿ(¯éÿÿ'¯éÿÿ&¯éÿÿ%¯	éÿÿ$¯
éÿÿ#¯éÿÿ"¯éÿÿ!¯
éÿÿ ¯éÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿþÿÿ
ÿÿÿþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
ÿ	ÿÿÿÿÿ
ÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿÿ	ÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿýÿÿÿÿÿ	ÿÿÿ	ÿÿÿ"ÿÿÿþÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿþÿÿ
ÿÿÿþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
ÿ	ÿÿÿÿÿ
ÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿÿ	ÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿýÿÿÿÿÿ	ÿÿÿ	ÿÿÿ"ÿÿÿþÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ.éþ¯ÿ-é¯ÿÿ,é¯ÿÿ+é¯ÿÿ*é¯ÿÿ)é¯ÿÿ(é¯ÿÿ'é¯ÿÿ&é¯ÿÿ%é	¯ÿÿ$é
¯ÿÿ#é¯ÿÿ"é¯ÿÿ!é
¯ÿÿ é¯ÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ>ÿÿø\Œ·Ìª‚!ýÿÿö)ËÿÿìÕúÿü—ý@ÿ�ÿÿõ,÷ÿŸ7»àý@ÿ�ÿÿýÀÿyÿLþ
ý#ý@ÿ�ÿÿüPÿß
øræÿÿï—ñêȶöÿùއôÿùðýCÿýŠÿ~÷£ÿÕrfºÿÔñúãÒ½t™ôÿ|CšÿÄxý!ÿý±ÿZ
ö<ÿÔ¨ÿsýúÿ–ý$ÿïý@ÿ�ÿÿýÔÿ:
ý ÿXý$þØýúþøÜÿ@ÿ�ÿÿýÂÿJ
ýÆÿùåúúâø¡ÿ+@ÿ�ÿÿýœÿm
ýâÿùÌÿúÖøšÿ.@ÿ�ÿÿýuÿª
ýÈÿùãûúÖøšÿ.@ÿ�ÿÿüðþ2ú£ÿOýýÛþúÖøšÿ.:ÿ™þÿülÿÛ4øLÃDÿÄý™ÿ|þúÖøšÿ.ÿÍÿÿõ‘þúª}e‘ÇÿÛ÷°ÿÅaV«ÿÞ
þúÖòšÿ.Ñý¼†€$ÿýAÌøÿýñŠøˆùÿÿý­þúÖýšÿ.ø)³æýÿHÿû'<ü,5
ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿþ#ÿÿþ!ÿÿþýþÿÿþýÿüûüýþÿÿþüýüûùøùùúûüýþþÿÿþúýüúøööõùöøúüýþþÿóÿþþýûùöúüýýüûøüúüýþÿþûýüúøýÿüþýüüÿþüüú÷þ*ÿûþýüùø+ÿûþýû÷û+ÿûþüúöý+ÿûþüúöü+ÿûþüúöù+ÿúþýû÷õþ*ÿúþýûùôû*ÿþûüúöóþ)ÿþûýûùõó)ÿøÿþþýûøõô(ÿÿþúüûøöôü&ÿÿóþýüûù÷öøúüýþþ ÿÿþõýüûúùùúûüýþÿÿþýüÿýþÿÿþýþ ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿ<ÿ(ÿÿ)ÿÿ)ÿÿÿÿ
ÿÿÿÿÿÿÿÿÿ	ÿÿ	ÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿ
ÿÿÿÿÿÿÿþÿÿÿ
ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿ	ÿÿÿÿÿÿÿ	ÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÓÿ¯.éÿíÿ¯.éÿíÿ¯.éÿí	ÿ¯.éÿí
ÿ¯.éÿíÿ¯.éÿíÿ¯.éÿí
ÿÿ¯.éÿíÿ.éÿíÿ-éÿíÿ,éÿíÿ+éÿíÿ*éÿíÿ)éÿíÿ(éÿíÿ'éÿíÿ&éÿíÿ%éÿíÿ$éÿíÿ#éÿíÿ"éÿíÿ!éÿíÿ éÿíÿéÿíÿ¯ÿÿ<ÿ(ÿÿ)ÿÿ)ÿÿÿÿ
ÿÿÿÿÿÿÿÿÿ	ÿÿ	ÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿ
ÿÿÿÿÿÿÿþÿÿÿ
ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿ	ÿÿÿÿÿÿÿ	ÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÓÿ6ÆÿÍî5ÆÿÍî4ÆÿÍ	î3ÆÿÍ
î2ÆÿÍî1ÆÿÍî0ÆÿÍ
î/ÆÿÍî.ÆÿÍî-ÆÿÍî,ÆÿÍî+ÆÿÍî*ÆÿÍî)ÆÿÍî(ÆÿÍî'ÆÿÍî&ÆÿÍî%ÆÿÍî$ÆÿÍî#ÆÿÍî"ÆÿÍî!ÆÿÍî ÆÿÍîÆÿÍîÆÿÿ<ÿ(ÿÿ)ÿÿ)ÿÿÿÿ
ÿÿÿÿÿÿÿÿÿ	ÿÿ	ÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿ
ÿÿÿÿÿÿÿþÿÿÿ
ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿ	ÿÿÿÿÿÿÿ	ÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÓÿé.¯ÿ®ªé.¯ÿ®ªé.¯ÿ®	ªé.¯ÿ®
ªé.¯ÿ®ªé.¯ÿ®ªé.¯ÿ®
ªÿé.¯ÿ®ª.¯ÿ®ª-¯ÿ®ª,¯ÿ®ª+¯ÿ®ª*¯ÿ®ª)¯ÿ®ª(¯ÿ®ª'¯ÿ®ª&¯ÿ®ª%¯ÿ®ª$¯ÿ®ª#¯ÿ®ª"¯ÿ®ª!¯ÿ®ª ¯ÿ®ª¯ÿ®ªéÿý0"<ýÿ¶(ý
˜Šýÿ¶)ýãÿý)ý£ÿZü(þý"
þýcÿšý¨ïÿþÜNýðª÷”ð/~çÿÿÇ#ù$½÷ÿÿÛHù”ð/‚åÿÊý#ÿÚö>ê–kZƒêÿNýÿ¶öžÿ¶åƒzÛÿÖ÷;ôõŒ\sÝþYùžÿ¸ì�qyøãÿM
ýïÖýÿ¶ðžÿáÇÿKÒö+öãä
žÿé
ú£ÿY�ø«ÿ	ÿ¶ýžÿlø€ÿpLÿ‘øzÿCžÿwëcÿ˜ÌоÜèèöÿ(ÿ¶ýžÿ>øEÿ‡wÿ¤pø¡ÿlžÿGë"ÿØü6øù·“ƒ€Ëÿ2ÿ¶ýžÿ2ø>ÿŠ™ÿù÷úžÿ2øâÿKÿÊû2ø­ÿ2ÿ¶ýžÿ2ø>ÿŠ€ÿX	ýžÿ2ù¢ÿW‹þùÔ÷ßÿ2ÿ¶ýžÿ2ø>ÿŠWÿ˜	ýžÿ2øbÿ–ÊÓïû÷nÿÿ2ÿ¶ýžÿ2÷>ÿŠßû6ùžÿ2ì"ÿßü”žÿÊjN˜õÏÿ2ÿ¶ýžÿ2ý>ÿŠñDùøŒVLk¤öžÿ2
úâÿÿT°ÿöà=–ÿ2ÿ¶ýžÿ2ý>ÿŠý*Áþÿøìžžÿ2
÷¢ÿÿ=û
1; Ùÿÿ	ÿ#ÿÿÿÿ	ÿÿÿÿÿÿ	ÿÿÿÿÿ	ÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿ	ÿÿüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿÿÿÿÿÿÿÿÿ
ÿ
ÿÿÿÿÿÿÿÿ
ÿ
ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿ ÿÿÿ+ÿÿÿ6ÿ7ÿ:ÿçÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ'¯ÿ&¯ÿéÿ%¯éÿ$¯éÿ#¯éÿ"¯éÿ!¯éÿ ¯é¯ÿ/¯ÿ/¯ÿ/¯ÿ'¯ÿÿ	ÿ#ÿÿÿÿ	ÿÿÿÿÿÿ	ÿÿÿÿÿ	ÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿ	ÿÿüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿÿÿÿÿÿÿÿÿ
ÿ
ÿÿÿÿÿÿÿÿ
ÿ
ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿ ÿÿÿ+ÿÿÿ6ÿ7ÿ:ÿÏÿîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ'Æîÿ/Æÿ/Æÿ/Æÿ/Æÿ'Æÿÿ	ÿ#ÿÿÿÿ	ÿÿÿÿÿÿ	ÿÿÿÿÿ	ÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿ	ÿÿüÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿûÿÿÿÿÿÿÿÿÿ
ÿ
ÿÿÿÿÿÿÿÿ
ÿ
ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿ ÿÿÿ+ÿÿÿ6ÿ7ÿ:ÿÏÿªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ'éªÿ&éÿ¯ªÿ%鯪ÿ$鯪ÿ#鯪ÿ"鯪ÿ!鯪ÿ é¯éÿ/éÿ/éÿ/éÿ'éÿý0		þ(-#ü˜˜&ùt˜#œÿ4	þØðýüOÿÿsøóûœÿ4	þØðýfÿjüŽüè²ø6ÿÇ	þØðýfÿjûÎÌ¢ïývÿ‡	ù!Øðþ	 þýfÿjûý�dÿ2ø¶ÿG’ð0÷…ñÿ÷¼Øð÷»ûÿë�%ð—ù¥ñÿÿèiüªöÿöðîÿN&ÿqñûœÿ4÷›ÿÓphÀ׿ðöâÿ¡_tÚ½ÿ¢ñäýžajÆÿˆU®ÿ°xôýè±5ÿÇñœÿ4,þÓ
µÿðý�ÿ€üâÿ¢ý¦ÿQ÷¾ú%fÿjõ Ъîtÿ‡øœÿ4�ÿ[ý3ÿðýâô
øuÿ¢ÿÁøJÿsfÿjõ�lÿ0´ÿFøœÿ4´ÿ$ø÷ðþÒøCÿ¢Gÿ¿pø†ÿœfÿjöQ.ÿoðûøœÿ4ÏÿùßðÿÁø.ÿ¢iÿû÷ú®fÿj÷î¯3ÿÆøœÿ4¶ÿ"ùõðûØøIÿ¢Pÿˆ	ýfÿjû²ìtÿ†øœÿ4”ÿRý*ÿðý×úøŠÿ¢'ÿÈ	ý`ÿsûtÿÜÿF÷œÿ45ÿÉý¦ÿðöhÿ¤,ðÿ¢ý»ÿbù
Bÿ§ÿû6ÿÿúýœÿ4÷¨ÿÂXP«ãêïöÃÿÓ‘¨ó’ÿ ì íÿ¢]Ie”ìD
ë÷²‚€LüôÿÆýœÿ4÷•ûÿÿÔ$Øð÷‹Ôð¼`8ÿ…ý§ûÿýõ® ù;Áêþÿ"/ý
8
ýdÿdû)>%+ÿüß÷6øË¡l[‚çÿƒ7ù•êÿÿüÚ]:ý Ûÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ'ÿÿ9ÿ7ÿ7ÿÿ7ÿÿÿÿ-ÿ
ÿÿ+ÿ+ÿÿÿÿ+ÿÿÿ1ÿÿÿ-ÿÿ+ÿÿ,ÿ	ÿ+ÿÿÿÿ+ÿÿ+ÿÿ8ÿîÿ¯.éÿí¯.éþíÿ
¯.éýíÿÿ¯.éÿíÿ¯.éÿíÿ
¯.éÿíÿ	¯.éÿíÿ¯.éÿíÿ¯.éÿíÿ¯.éÿíÿ¯.éÿí	ÿ¯.éÿí
ÿ¯.éÿíÿ¯.éÿíÿ¯.éÿí
ÿÿ¯.éÿíÿ.éÿíÿ-éÿíÿ,éÿíÿ+éÿíÿ*éÿíÿ)éÿíÿ(éÿíÿ'éÿíÿ¯ÿÿ9ÿ7ÿ7ÿÿ7ÿÿÿÿ-ÿ
ÿÿ+ÿ+ÿÿÿÿ+ÿÿÿ1ÿÿÿ-ÿÿ+ÿÿ,ÿ	ÿ+ÿÿÿÿ+ÿÿ+ÿÿ8ÿîÿ>ÆÿÍ=ÆþÍî<ÆýÍîî;ÆÿÍî:ÆÿÍî9ÆÿÍî8ÆÿÍî7ÆÿÍî6ÆÿÍî5ÆÿÍî4ÆÿÍ	î3ÆÿÍ
î2ÆÿÍî1ÆÿÍî0ÆÿÍ
î/ÆÿÍî.ÆÿÍî-ÆÿÍî,ÆÿÍî+ÆÿÍî*ÆÿÍî)ÆÿÍî(ÆÿÍî'ÆÿÍîÆÿÿ9ÿ7ÿ7ÿÿ7ÿÿÿÿ-ÿ
ÿÿ+ÿ+ÿÿÿÿ+ÿÿÿ1ÿÿÿ-ÿÿ+ÿÿ,ÿ	ÿ+ÿÿÿÿ+ÿÿ+ÿÿ8ÿîÿé.¯ÿ®é.¯þ®ª
é.¯ý®ªªé.¯ÿ®ªé.¯ÿ®ª
é.¯ÿ®ª	é.¯ÿ®ªé.¯ÿ®ªé.¯ÿ®ªé.¯ÿ®ªé.¯ÿ®	ªé.¯ÿ®
ªé.¯ÿ®ªé.¯ÿ®ªé.¯ÿ®
ªÿé.¯ÿ®ª.¯ÿ®ª-¯ÿ®ª,¯ÿ®ª+¯ÿ®ª*¯ÿ®ª)¯ÿ®ª(¯ÿ®ª'¯ÿ®ªéÿü"'9ý¬ýÿþâO7øËÿ­rZu»q7þÿÀÿ7ýúÇ<ú¯ÿâŒ?9øwãÿÿïŠ
9ú?”éÿ¥<üùö6ÿýñì7øè‹[BmÂÿ�7þËüÿýôŒ8û-A+
úÿ ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ¯ÿÿîÿÆîÿÆîÿÆîÿÆîÿÆîÿÆîÿÆîÿÆîÿÆîÿÆîÿÆîÿÆîÿÆîÿÆîÿÆîÿÆîÿÆîÿÆîÿÆîÿÆîÿÆîÿÆîÿÆîÿ/Æÿ/Æÿ/Æÿ/ÆÿÆÿÿªÿéªÿéªÿéªÿéªÿéªÿéªÿéªÿéªÿéªÿéªÿéªÿéªÿéªÿéªÿéªÿéªÿéªÿéªÿéªÿéªÿéªÿéªÿéªÿ/éÿ/éÿ/éÿ/éÿéÿÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿÿ¯.éÿíÿ¯.éÿíÿ¯.éÿí
ÿÿ¯.éÿíÿÿ3ÆÿÍî1ÆÿÍî0ÆÿÍ
î/ÆÿÍîÿÿLéÿè·é.¯ÿ®ªé.¯ÿ®ªé.¯ÿ®
ªÿé.¯ÿ®ªÿÿÿ¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ¯ÿ®'¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ¯'ÿé'ÿé'ÿé'ÿéÿÿÆÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/ÆÿÆÿÅ'Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/ÆÿÆîÿÆîÿÆîÿÆîÿÆÿÿéÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿéÿç'éÿ/éÿ/éÿ/éÿ/éÿ/éÿéªÿ¯ªÿ¯ªÿ¯ªÿ¯ÿÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿÿ¯éÿÿÆÿÿé¯ÿÿÿ¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿ/éÿ¯éÿ¯éÿ¯éÿ¯ÿÿÆÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/ÆÿÆÿÿéÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ/¯ÿé¯ÿé¯ÿé¯ÿéÿÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿÿ7éÿ6éÿ5é	ÿ4é
ÿ3éÿ2éÿ1é
ÿ0éÿ/éÿ.éÿ-éÿ,éÿ+éÿ*éÿ)éÿ(éÿ'éÿ&éÿ%éÿ$éÿ#éÿ"éÿ!éÿ éÿ¯+é¯,é¯-é¯.éÿÿ7Æî6Æî5Æ	î4Æ
î3Æî2Æî1Æ
î0Æî/Æî.Æî-Æî,Æî+Æî*Æî)Æî(Æî'Æî&Æî%Æî$Æî#Æî"Æî!Æî ÆîÆÿÿ7¯ª6¯ª5¯	ª4¯
ª3¯ª2¯ª1¯
ª0¯ª/¯ª.¯ª-¯ª,¯ª+¯ª*¯ª)¯ª(¯ª'¯ª&¯ª%¯ª$¯ª#¯ª"¯ª!¯ª ¯ªé+¯é,¯é-¯é.¯ÿÿ8ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯éÿí;ÿéÿí<ÿþéí=ÿÿí?ÿÿ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿ
ÆÿÍ#îÿÆÿÍ$îÿþÆÍ%îÿÿÍ&îÿÿ'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé¯ÿ®#ªÿ¯ÿ®$ªÿþ¯®%ªÿÿ®&ªÿ'ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿÿ/ÿÿ.¯ÿÿ.¯ÿÿ.¯ÿÿ.¯ÿÿ.¯ÿÿ.¯ÿÿ.¯ÿÿ¯ûYWWn�¯þœ¯ÿÿ¯úK]C
y¯þ¥Z¯ÿÿ¯ú�¯¯[.¯ô‹fu¨¯Ÿ{®¯¯„•¯ô’�¯¯‚”¯¯~¤©z¯ÿÿ¯ë�¯§8E¯¯aG¦«$`¯|Ÿ¯ñq-¯ª8¯£§š¯¯aÿÿ¯ì(§¯¥€¯®[¯˜x�¯ñŸª�3®v(¯š¯¦ÿÿ¯í„¤�<1¯ˆª¯¯O9¯¯sT¯ò�Sg.ŒHV¯š¯Œÿÿ¯í�¯¯“ŸŠª¯¯L:¯¯G6¯òKS%•\^„¯š¯�ÿÿ¯ì�¯¯j¬§
€¯¬_¯sž d¯òy¯Š«¯š¯§
ÿÿ¯ë'1n¯¯j>%©–
Н”Œ¯ò¥A¯­
1¯¯š¯¯hÿÿ¯ü„ƒƒ–¯ù•q€¬¯—�¯þ“”¯ö‰›¯¯�—¯¯ª„¯ÿÿ.¯ÿÿ.¯ÿÿ.¯ÿÿ.¯ÿÿ.¯ÿ/ÿ/¯é.¯é-¯é,¯é+¯é*¯é)¯é(¯é'¯é&¯é%¯é$¯é#¯é"¯é!¯é ¯é¯é¯ é¯!é¯"é¯#é¯$é¯%é¯&éÿÿ.éÿÿ.éÿÿ.éÿ/ÿÿ/ÿÿ.Æÿÿ.Æÿÿ.Æÿÿ.Æÿÿ.Æÿÿ.Æÿÿ.ÆÿÿÆûdcc}²Æþ±ÆÿÿÆúUiL‰ÆþºfÆÿÿÆúŸÆÆf4Æôžt„¾Æ³‹ÅÆÆ•¨Æô¥£ÆÆ“§ÆÆ�º¿ŠÆÿÿÆëŸÆ½?NÆÆnQ#!¼Á(mÆ�´Æñ3ÆÀ@Ƹ½®ÆÆmÿÿÆì-½Æ»�ÆÅ"fƬˆ’Æñ³À’:Ć.Æ®Ƽ	ÿÿÆí•º¢D8ÆšÁÆÆY@ÆÆ‚_Æò!’^t4žRbƮƞÿÿÆíŸÆÆ¦´œÁÆÆVBÆÆP=ÆòU^*¨hj–ƮƠÿÿÆìŸÆÆxý‘ÆÂkÆ‚³$qÆò‰ Æœ"ÁÆ®ƽÿÿÆë,7}ÆÆxG*¿ªœÆ§žÆòºJÆÄ7ÆÆ®ÆÆvÿÿÆü•””©Æù¨€�ÂÆ«¢Æþ¦§Æö›¯ÆÆŸ«ÆÆÁ–Æÿÿ.Æÿÿ.Æÿÿ.Æÿÿ.Æÿÿ.Æÿ/ÿÆÿÿ.Æÿÿ.Æÿÿ.Æÿ/ÿÿ/ÿÿ.éÿÿ.éÿÿ.éÿÿ.éÿÿ.éÿÿ.éÿÿ.éÿÿéûvtt“ÑéþÐ#éÿÿéúd{Z¢éþÛxéÿÿéú»ééy=éô¹ˆ›àéÓ¤èé靯éôÃÀéé­Äéé¨Úá£éÿÿéë»éßJ\éé‚	_)'Ýä0€é¥Ôéñ–<éâKéÙÞÍéé�ÿÿéì5ßéÜ	ªéè(yéË ¬éñÓâ¬Eç�6éÍéÝ
ÿÿéí¯Ú¾OBéµ	ãééiLéé™oéò'¬o‰=º`séÍéº	ÿÿéí»ééÄÔ¸ãééeNéé^Héòeo1Æz}#°éÍé¼ÿÿéì»éé�åÞ
«éä$~éšÒ*„éò¢%é¸(	äéÍéß
ÿÿéë4A%“ééŽ	S1áȸéĺéòÛWéæAééÍéé‹ÿÿé¯ÿÇéùÆ—ªäéÉ¿éÄéö¶Ïéé»Éééã°éÿÿ.éÿÿ.éÿÿ.éÿÿ.éÿÿ.éÿ/ÿ/é¯.é¯-é¯,é¯+é¯*é¯)é¯(é¯'é¯&é¯%é¯$é¯#é¯"é¯!é¯ é¯é¯é ¯é!¯é"¯é#¯é$¯é%¯é&¯ÿÿ.¯ÿÿ.¯ÿÿ.¯ÿ/ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿ0ÿÿ.¯ÿÿÿ.¯ÿÿÿ.¯ÿÿÿ.¯ÿÿÿ.¯ÿÿÿ.¯ÿÿÿ.¯ÿÿÿ¯þeR¯þª’¯ÿÿÿ¯þ[D¯þ�¯ÿÿå‰g�[D¯¯¦po¡€£¯¯¤th”¯¯dxxª¯ÿÿåI&D¯ª"K$‡¯‘8Iw¯D	RR¥¯ÿÿ脯®D¯i¯¯ˆ‡¯1V¯¯s¯�¯ÿÿ謯¯ED¯GI¯¯¬‡¯	3AA;ª�¯ÿÿ絛¯CD¯L@¯¯§‡¯f‰ý­�¯ÿÿï�¯¬D¯w¡¯n‡¯0D¯û®¯£	­¯ÿÿåD+D¯®<#1‹¯– N=-¯¯3%<£¯ÿÿú�r–𔝖÷Ÿ¯¯ª€m~¨¯ýœ„«¯ÿÿÿ¯ûf‡x/1"¯ÿÿÿ¯ûa75Y§"¯ÿÿÿ.¯ÿÿÿ.¯ÿÿÿ.¯ÿÿ0ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿéÿíÿÿéÿíÿÿéÿí ÿÿéÿí!ÿÿéÿí"ÿÿéÿí#ÿÿéÿí$ÿÿéÿí%ÿÿéÿí&ÿÿéÿí'ÿÿéÿí(ÿÿéÿí)ÿÿéÿí*ÿÿéÿí+ÿÿéÿí,ÿÿéÿí-ÿÿéÿí.ÿÿ
éÿí/ÿÿéÿí0ÿÿéÿí1ÿÿ
éÿí2ÿÿ	éÿí3ÿÿéÿí4ÿÿéÿí5ÿÿ.éÿÿÿ.éÿÿÿ.éÿÿ0ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿ>ÿÿ>ÿÿ>ÿÿ>ÿ0ÿÿ.Æÿÿÿ.Æÿÿÿ.Æÿÿÿ.Æÿÿÿ.Æÿÿÿ.Æÿÿÿ.ÆÿÿÿÆþr\ÆþÀ¥ÆÿÿÿÆþfMÆþ£Æÿÿå›t fMÆÆ¼~¶�¸ÆÆºƒv¨ÆÆqˆˆÀÆÿÿå	S!+MÆÀ"'U)™Æ¤@S†ÆM
\\»Æÿÿè•ÆÄMÆw ÆÆš™Æ7bÆÆ‚Æ£ÆÿÿèÂÆÆNMÆQRÆÆÃ	™Æ
:JJCÀ£ÆÿÿïÁÆÆLMÆVHÆÆ½™Æ	t›ýÄ£Æÿÿï’ÆÃMƇ¶Æ|™Æ6MÆûÄÆ¸
ÄÆÿÿåM1MÆÅD(8�Æ©$YE3ÆÆ9*D¹Æÿÿú¢�ª¯¨Æôª©ª´ÆÆÀ�{�¾Æý°•ÁÆÿÿÿÆûs™ˆ58"ÆÿÿÿÆûn>ÿÿ>ÿÿ>ÿÿ>ÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿ
ÆÿÍ îÿÿÆÿÍ!îÿÿÆÿÍ"îÿÿ
ÆÿÍ#îÿÿ	ÆÿÍ$îÿÿÆÿÍ%îÿÿÆÿÍ&îÿÿ.Æÿÿÿ.Æÿÿÿ.Æÿÿ0ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿ>ÿÿ>ÿÿ>ÿÿ>ÿ0ÿÿ.éÿÿÿ.éÿÿÿ.éÿÿÿ.éÿÿÿ.éÿÿÿ.éÿÿÿ.éÿÿÿéþ†méþâÃéÿÿÿéþyZéþÀéÿÿå·‰¼yZééÝ•”תÙééÚš‹Åéé„  âéÿÿåb'2Zéâ(.d0´éÁKbžéZmmÜéÿÿè¯éç!ZéŒ%ééµ´éAsééš éÀéÿÿèäéé\Zé_aééå´éEWWOâÀéÿÿïãééZZéeUééߴ鈶ýæÀéÿÿï¬éåZéŸÖé’´é@ZéûçéÙæéÿÿå	Z%:ZéèO	/B¹éÇ!*hQ<ééD1OÙéÿÿú¿˜ÈÎÅéôÈÇÈÔéé⪑¨àéýϯäéÿÿÿéû‡´ >B"éÿÿÿéû‚IFwß"éÿÿÿ.éÿÿÿ.éÿÿÿ.éÿÿ0ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ
¯ÿ® ªÿÿ¯ÿ®!ªÿÿ¯ÿ®"ªÿÿ
¯ÿ®#ªÿÿ	¯ÿ®$ªÿÿ¯ÿ®%ªÿÿ¯ÿ®&ªÿÿ.¯ÿÿÿ.¯ÿÿÿ.¯ÿÿ0ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ0ÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ%¯ÿ¯ÿÿÿ$¯þ̯ÿÿÿ#¯ýÌé¯ÿÿÿ"¯üÌéé¯ÿÿÿ!¯ÿÌéÿ¯ÿÿÿ ¯ÿÌéÿ¯ÿÿÿ¯ÿÌéÿ¯ÿÿÿ¯ÿÌéÿ¯ÿÿÿ¯ÿÌéÿ¯ÿÿÿ
¯þ™�¯ÿy	¯ÿÌéÿ¯ÿÿÿ
¯þ}f¯ÿ¯ÿÌéþaÍéÿ¯ÿÿد¯­�¯¯‹©¯—¦¡š¯¯¦€¦¯¯®„™–¥¯Ìß²çéÕ.š¶éé¯ÿÿÿ¯Ü¨›v¯$¡nQ¯|"R3¯£$K5ƒÌ…Fs:Žh{éé¯ÿÿÿ¯àAmCH£"¯nQ¯8‹¯€¯rQ¯¯™ç.äé¤pé:Äéÿ¯ÿÿÿ¯çnQ>”&uP¯nQ¯¬¯£¯Oq¯¯9¯¾Hühé:Äéÿ¯ÿÿÿ¯æœ"&¯93~¯nQ¯6�¯‚¯yK¯Ê¯å%äéþE½éÿ¯ÿÿÿ¯ÝC¯h§¯nQ¯y"W1¯§3E:)µé€A‚r„é‚Btéé¯ÿÿÿ¯Ý‘œ¯¥Š¯¯Ÿ˜¯¯¡w¤ˆ¯¯ÌÇÝ%äééÝšªâééѯéé¯ÿÿÿ¯ûÌZBiéÿ¯ÿÿÿ¯ûÌéÅ£Ö
éÿ¯ÿÿÿ¯ÿÌéÿ¯ÿÿÿ¯ÿÌéÿ¯ÿÿÿ¯û]{¯<œ¯éÌéé¶âéé/HÔéâwY©éé�:†éé¯ÿÿÿ¯øNq¯‰©¯¯Ìéû0ÏéééôVQŸ¶éé‹téé¯ÿÿÿ¯áNq¯MŸ¯i>9¡¢@OéééÛàé{]éé‹téé¯ÿÿÿ¯áNq¯&™¡NçåÓé0ÏééééÀéé¥>éé‹téé¯ÿÿÿ¯áNq¯&²Ò"oÀéé0Ïééééº(éé¯9éé‹téé¯ÿÿÿ¯áNq¯-ÌéÞ�7é0ÏééééÓåé†Qéé‹téé¯ÿÿÿ¯ðNqÌ3ÌÍÚé¥XéS§éééô6†Ô5šéé‹téé¯ÿÿÿ¯ðg–éaÓ¯64WËé»ZAééôÍC+xèéé‹téé¯ÿÿÿ¯ÿÌ
éýWÖéú˜4téé¯ÿÿü¯¯sGÿƒéýÛÚçéúâÚâéé¯þ’ÿÿ¯ÿÌéþï¯ÿÿÿ¯ÿÌéýïÿ¯ÿÿÿ¯ÿÌéüïÿÿ¯ÿÿÿ¯ÿÌéÿïÿÿ¯ÿÿü¯¯Ìéÿïÿÿ¯ÿÿý¯Ìéÿïÿÿ¯ÿÿþÌéÿïÿÿ¯ÿÿÿéÿïÿÿ¯ÿÿ0ÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ%ÆÿÆÿÿÿ
Æþ­¢Æÿ‰ÆÿÆÿÿÿ
Æþ�sÆÿ
ÆþR®ÆÿÆÿÿØÆÆÄ¢ÆÆ�¿Æ«¼¶¯ÆÆ¼‘¼ÆÆÅ–­ª»ÆÆ½—ÄÆµ'‚›ÆÆÆÿÿÿÆÜ¾¯†Æ(¶}\Æ�'\:ƹ)U<”Æq~¯¯¯ô(eŸ(t¯¯hW¯¯éÿÿÿéð‰–¯IŸƒ('A˜¯�C1¯¯ôš2 Z®¯¯hW¯¯éÿÿÿéÿÌ
¯ý
A¡¯úr'W¯¯éÿÿüéé™^ÿƒ¯ý¥¤®¯úª¤ª¯¯éþÃÿÿéÿ̯þ­éÿÿÿéÿ̯ý­ªéÿÿÿéÿ̯ü­ªªéÿÿÿéÿ̯ÿ­ªÿéÿÿüéé̯ÿ­ªÿéÿÿýé̯ÿ­ªÿéÿÿþ̯ÿ­ªÿéÿÿÿ¯ÿ­ªÿéÿÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ/ÿ?/¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ=¯þÌ<¯ýÌé;¯üÌéé:¯ÿÌéÿ9¯ÿÌéÿ8¯ÿÌéÿ7¯ÿÌéÿ6¯ÿÌéÿ5¯ÿÌéÿ4¯ÿÌéÿ3¯ÿÌ	éÿ2¯ÿÌ
éÿ1¯ÿÌéÿ0¯ÿÌéÿ/¯ÿÌ
éÿ.¯ÿÌéÿ-¯ÿÌéÿ¯þ¬|¯þ…£¯ÿÌéÿ¯þ§<¯þ5НýÌ™•éÿ¯Þ“ª¯–�¯£š­�¯¯®„›5Н¯“‰¢š¯¯®‡¥Ìäeb¶Ù
éÿ¯ÞHw¯#A¯Xm¤¯¡%K0НF6G&M¯‹/U=�ßEB{Ç
éÿ¯àvI¡2%¯*›¤¯nV¯¬	Š¥¯¯FM¯DˆÌà3é‚|éÿ¯à£u_@Œ¯¤¯Gy¯¯*Š„<¯¯fM¯>HHCÝ‚|éÿ¯ç#3Žn3H¯¤¯lX¯­
Ѝ¯¯AM¯L®éþŒvéÿ¯ßQ®�v¯¤¯Ÿ!Q3НX+7,RÌ·7q€ZéÄ&pÅ
éÿ¯ßŸŽ¯¯‡¨¯¬Š¯¯¬y–‘¦¯¯ œG“ééçª�Óééß³Ø
éÿ ¯û^_C/ãéÿ
¯ÿ±¯ý¡}©éÿ
¯ÿÓ¯ÿÌéÿ¯þÅé¯ÿÌéÿ¯ý¹çé¯û‹N¯io¯ï˜´ééi?£éÎ�oÎééÁEJéÿ¯öd¯¯E>±ß1}¯û„;¯˜š¯ôi†ééP®ééœs%µéþÒ,éÿ¯öŠg¯¯zwÓ鎳¯ï„;¯ty¯{6)Q¹%*H¿P®éþ:µéþÒ,éÿ	¯ÿÅé¯ï„;¯\c®¥¯µéx†ééP®éþ:µéþÒ,éÿ¯þ¹çé¯ï„;¯\c¯0B‘çéx†ééP®éþ:µéþÒ,éÿ¯þ±ßé¯ï„;¯\c¯®”[8éy†ééP®éþ:µéþÒ,éÿ¯ÿÓé¯ä„;¯\c©®éß"æšcæéP®ééâÓ5¤×ééÒ,éÿ¯ÿÅé¯ì�Y¯qvÀC0G¤éÛm=»P®éé®:ûUééÒ,éÿ¯þ¹çé¯ÿÌ	éýPA©éýÄO,éÿ¯þ±ßéGÿP¯ÿÌ
éýàÚãéýåÚÞéÿ¯ÿÓé¯ÿÌ&éþí¯ÿÅ	é¯ÿÌ&éýíÿ¯þ¹ç	é¯ÿÌ&éøíÿÿ¯¯±ß
é¯ÿÌ&éÿíÿü¯¯Óé¯ÿÌ&éÿíÿý¯Åé¯ÿÌ&éÿíÿý¹çéþ¯Ì&éÿíÿþß
éÿÌ&éÿíÿÿé?/Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>ÆÿÆþÂ�Æþ—¸ÆÿÆþ½DÆþ<œÆþ‚ÆÿÆÞ¦Àƪ²Æ¸¯Ä¢ÆÆÅ•¯<œÆÆ¦›·®ÆÆÅ™»ÆÂVS›¹
ÆÿÆÞR‡Æ(IÆc{ºƶ*U6œÆO=Q+WÆž5`;{½:8i©
ÆÿÆà†S¶9*Æ/¯ºÆ|aÆÃ
œº"ÆÆOWÆMšÆ¾+ÆnjÆÿÆç¸!„lHž!ƺÆQ‰ÆÆ/œ–DÆÆtWÆ=ü9¼njÆÿÆç(:¡}:RƺÆzcÆÄœ¾ ÆÆIWÆJ”ÆþwdÆÿÆß\Ų†ÆºÆ´%\9œÆc0>2]Æ›/`mLƧ!`¨
ÆÿÆß´¡ÆÆ˜¾ÆÃœÆÆÃ‰©¤¼ÆÆµ±Q�ÆÆÄ�†³ÆÆ½˜·
Æÿ ÆûjkK.ÁÆÿ Æý¶�¤Æÿ>Æÿ>ÆÿÆû�XÆw~Æï¬¯ÆÆY6ŠÆ¯z^¯ÆÆ¤;?ÆÿÆöq#ÆÆNGÆÆ*jÆû•CƬ®ÆôfrÆÆD”ÆÆ…bšÆþ³%ÆÿÆöœtÆÆŠ†ÆÆx˜Æï•Cƃ‰Æ‹=/\³ $=¢D”Æþ2šÆþ³%ÆÿÆï•CÆhpÅ!»Æ¯ÆfrÆÆD”Æþ2šÆþ³%ÆÿÆï•CÆhpÆ6K�ÄÆfrÆÆD”Æþ2šÆþ³%ÆÿÆï•CÆhpÆÅ�N/ÆfrÆÆD”Æþ2šÆþ³%ÆÿÆä•CÆhp¿¨Æ½ăTÄÆD”ÆÆÀ³-Œ¶ÆÆ³%ÆÿÆì¢dÆ€†º9)=ŒÆº\4ŸD”ÆÆ”1ûHÆÆ³%Æÿ"ÆýD7�Æý§C%ÆÿÆPÿ[Æý¾ºÁÆýú½Æÿ=ÆþÍ<ÆýÍî;ÆüÍîî:ÆÿÍîÿ9ÆÿÍîÿ8ÆÿÍîÿ7ÆÿÍîÿ6ÆÿÍîÿÆ?/éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ=éþÌ<éý̯;éü̯¯:éÿ̯ÿ9éÿ̯ÿ8éÿ̯ÿ7éÿ̯ÿ6éÿ̯ÿ5éÿ̯ÿ4éÿ̯ÿ3éÿÌ	¯ÿ2éÿÌ
¯ÿ1éÿ̯ÿ0éÿÌ¯ÿ/éÿÌ
¯ÿ.éÿ̯ÿ-éÿ̯ÿéþä¥éþ±Ùéÿ̯ÿéþÞOéþF¸éýÌsp¯ÿéÞÄâéÈÑéÙÎæ¾ééè¯ÏF¸ééÄ·ØÍééè´Ü̬LI‰£
¯ÿéÞ`Ÿé/Véu‘Ú$éÖ1e@!¸é]G_2fé¹>q=l§31]–
¯ÿéà�b×C1é8ÏÚ$é’réå¸Û(éé]féZµÌ¨&¯a]¯ÿéàÙ&›Uº'éÚ$é_¢éé8¸°Oééˆfé>662¦a]¯ÿéç/E½“E`éÚ$é�u鿏à%ééVféL‚¯þiY¯ÿéßlèÑ�éÚ$éÔ,lD¸éu9I:n̉)U`C¯”T”
¯ÿéßÔ½éé³àéå¸ééå¢ÇÁÝééÕÐ_“¯¯®€vŸ¯¯§‡¢
¯ÿ éû}~Y/ª¯ÿ
éÿçéýצ©¯ÿ
éÿÅéÿ̯ÿéþÓ¯éÿ̯ÿéýß±¯éû¹g錔éïÊ´¯¯O/z¯šlS𝝑48¯ÿéö…)éé[Sç¹%^éû¯OéÊÍéôie¯¯<‚¯¯uVˆ¯þž!¯ÿéö¸‰é飞ůj‡éï¯Oéš¡é¤H7l¹ 6�<‚¯þ,ˆ¯þž!¯ÿ	éÿÓ¯éï¯Oéz„è'Ü鵯Ze¯¯<‚¯þ,ˆ¯þž!¯ÿéþß±¯éï¯Oéz„é@X‘®¯Ze¯¯<‚¯þ,ˆ¯þž!¯ÿéþ繯éï¯Oéz„éè”E*¯[e¯¯<‚¯þ,ˆ¯þž!¯ÿéÿůéä¯Oéz„ᮯ§­tJ­¯<‚¯¯ªŸ(|¡¯¯ž!¯ÿéÿÓ¯éì¾vé—�À2$6|¯¥R.�<‚¯¯‚+û@¯¯ž!¯ÿéþß±¯éÿÌ	¯ý<1¯ý”;!¯ÿéþ繯^ÿkéÿÌ
¯ý¨¤ª¯ý¬¤§¯ÿéÿůéÿÌ&¯þ®éÿÓ	¯éÿÌ&¯ý®ªéþß±	¯éÿÌ&¯ø®ªªééç¹
¯éÿÌ&¯ÿ®ªüééůéÿÌ&¯ÿ®ªýéÓ¯éÿÌ&¯ÿ®ªýß±¯þéÌ&¯ÿ®ªþ¹
¯ÿÌ&¯ÿ®ªÿ¯ÿÿ'¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ¯ÿy¯þ‹�¯ÿÿÿ¯ÿ3¯þEy¯þ€c¯ÿÿÿ¯ö–¥¯šš¯¦—¯�¯ö‡–Ey¯¯˜†£–¯ù‹¡¯¯Y<‰¯ÿÿÿ¯ßYg¯31¯h]¯¯ª)F4y¯T/L)<¯–/R:_¯<)]¯ÿÿÿ¯à‡9¬()«:‹¯¯~E¯¯y­®¯V<¯Tw¯®#­rM¯ÿÿÿ¯à¬ƒOP‚®¯¯Xi¯¯:y•+¯¯w<¯%0663šrM¯ÿÿÿ¯3é~38¯¯¯|G¯¯y®¬¯Q<¯Rr¯þyH¯ÿÿÿ¯àa¨©f¯¯¯©$K8y¯e'<.B¯•)Pb:¯¢S¯ÿÿÿ¯ê£Š¯¯‹¤¯¯‡¯¯®}‘•¢¯¯¤™Xn¯ù…tš¯¯ª‰¯ÿÿÿ¯ûh\H%¡¯ÿÿÿ¯ý¥€‹¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿ'¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ,¯þ¹ÿÿ¯ÿy¯þ‹�¯ý±ßÿÿ¯ÿ3¯þEy¯þ€c¯ýÓéÿÿ¯ö–¥¯šš¯¦—¯�¯ö‡–Ey¯¯˜†£–¯ù‹¡¯¯Y<‰¯üÅééÿÿ¯ßYg¯31¯h]¯¯ª)F4y¯T/L)<¯–/R:_¯<)]¯û¹çééÿÿ¯à‡9¬()«:‹¯¯~E¯¯y­®¯V<¯Tw¯®#­rM¯þ±ßéÿÿÿ¯à¬ƒOP‚®¯¯Xi¯¯:y•+¯¯w<¯%0663šrM¯ÿÓéÿÿÿ¯3é~38¯¯¯|G¯¯y®¬¯Q<¯Rr¯ûyH¯¯Åéÿÿÿ¯Þa¨©f¯¯¯©$K8y¯e'<.B¯•)Pb:¯¢S¹çéÿÿÿ¯ê£Š¯¯‹¤¯¯‡¯¯®}‘•¢¯¯¤™Xn¯ø…tš¯¯ª‰ßéÿÿÿ¯ûh\H%¡	¯éÿÿÿ¯ý¥€‹¯éÿÿÿ&¯éÿÿÿ&¯éÿÿÿ&¯éÿÿ'éÿÿÿ&¯éÿÿÿ&¯éÿÿÿ&¯éÿÿÿ&¯éÿÿÿ&¯éÿÿÿ&¯éÿÿÿ&¯éÿÿÿ&¯éÿÿÿ&¯éÿÿÿ¯þ˜�¯þ®y¯éÿÿÿ¯þ{h¯þ¬
¯þGœéÿÿÿ¯ß­�¯¯Šª¯–§¡›¯¯¥€¤¯¯®„𖦝¯§‡®¯Ÿ uéÿÿÿ¯àª˜y¯"£lS¯z"R2¯¢$K4…¯b6V,–�Péÿÿÿ¯à>‚jCK¡$¯lS¯6ޝ~¯pS¯®…­"­¯xV¯)–éÿÿÿ¯çlT<–'sR¯lS¯®¯¡¯Lt¯¯.…Œ6üP¯)–éÿÿÿ¯æš$&¯73€¯lS¯3�¯€¯vN¯­…¬¬¯þ1‘ÿ'Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-ÆÿÿÿÆÿ‰Æþž²ÆÿÿÿÆÿ9ÆþN‰Æþ�pÆÿÿÿÆöª»Æ®®Æ¼«ÆŸÆö™ªN‰ÆÆ¬—¹ªÆù�¶ÆÆeD›ÆÿÿÿÆßdtÆ98ÆviÆÆÀ/O;!‰Æ`5V/DÆ©6\AkÆD/iÆÿÿÿÆà˜@Â./ÁB�ÆÆ�NÆÆ‰ÄÄÆbDÆ`‡ÆÅ(Ä�WÆÿÿÿÆàÔY[”ÄÆÆcwÆÆB‰¨1ÆÆ†DÆ*6==9®�WÆÿÿÿÆ:éŽ�:?ÆÆÆ�QÆÆ‰ÅÃÆ\DÆ\�Æþ‰RÆÿÿÿÆàn¾¿sÆÆÆ¿)U@‰Ær,D4Kƨ/[oBÆ·^ÆÿÿÿÆê¹œÆÆ�ºÆÆ™ÆÆÅ�¤¨·ÆÆº­c|Æù—ƒ¯ÆÆÀ›ÆÿÿÿÆûuhR*¶ÆÿÿÿÆýº�žÆÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿ'Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-ÆÿÿÿÆÿ‰Æþž²ÆÿÿÿÆÿ9ÆþN‰Æþ�pÆÿÿÿÆöª»Æ®®Æ¼«ÆŸÆö™ªN‰ÆÆ¬—¹ªÆù�¶ÆÆeD›ÆÿÿÿÆßdtÆ98ÆviÆÆÀ/O;!‰Æ`5V/DÆ©6\AkÆD/iÆÿÿÿÆà˜@Â./ÁB�ÆÆ�NÆÆ‰ÄÄÆbDÆ`‡ÆÅ(Ä�WÆÿÿÿÆàÔY[”ÄÆÆcwÆÆB‰¨1ÆÆ†DÆ*6==9®�WÆÿÿÿÆ:éŽ�:?ÆÆÆ�QÆÆ‰ÅÃÆ\DÆ\�Æþ‰RÆÿÿÿÆàn¾¿sÆÆÆ¿)U@‰Ær,D4Kƨ/[oBÆ·^ÆÿÿÿÆê¹œÆÆ�ºÆÆ™ÆÆÅ�¤¨·ÆÆº­c|Æù—ƒ¯ÆÆÀ›ÆÿÿÿÆûuhR*¶ÆÿÿÿÆýº�žÆÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿ'Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-ÆÿÿÿÆþ¬¢ÆþʼnÆÿÿÿÆþ‹uÆþÃ
ÆþP°ÆÿÿÿÆßÄ¢ÆÆœÀƪ½¶¯ÆÆ»‘ºÆÆÅ•®©¼ÆÆ½˜ÄÆ´$…ÆÿÿÿÆàÀ¬‰Æ&¹z^ÆŠ'\9Æ·)U;—Æo=a2ªŸZÆÿÿÿÆàF“xKU¶)Æz^Æ=¡ÆŽÆ^ÆÅ—Ä'ÄÆˆbÆ/©ÆÿÿÿÆçz_D©,‚\Æz^ÆÄƶÆVƒÆÆ4—ž=ü[Æ/©ÆÿÿÿÆæ¯)+Æ>:‘Æz^Æ:£Æ�ƆYÆÄ—ÂÂÆþ7¤ÿ'éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿéÿ¡éþ¹ÑéÿÿÿéÿDéþ\¢éþª„éÿÿÿéöÈÜéÍÍéÝÉé»éö´È\¢éé˲ÙÈéù¹×ééwP¶éÿÿÿéßv‰éDBé‹{ééâ7]E&¢ép>e7PéÇ?mM~éP7{éÿÿÿéà³Lä67äN¹éé¨\é颿 çésPépŸéè/æ˜féÿÿÿéàå¯ik®!çééuŒééN¢Æ:ééžPé1@HHD͘féÿÿÿéEé§©EJééé¥_éé!¢è"åélPém˜éþ¢`éÿÿÿéà‚àá‡éééá0eK"¢é†4O=XéÆ7kƒNéØ!oéÿÿÿéêÙ¸éé¹Úéé´ééè¦ÁÆØééÚÌu’éù±šÎééâ¶éÿÿÿéûŠz`1ÖéÿÿÿéýÛª¹éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿ'éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ,éþßÿÿéÿ¡éþ¹Ñéýç¹ÿÿéÿDéþ\¢éþª„éýůÿÿéöÈÜéÍÍéÝÉé»éö´È\¢éé˲ÙÈéù¹×ééwP¶éüÓ¯¯ÿÿéßv‰éDBé‹{ééâ7]E&¢ép>e7PéÇ?mM~éP7{éûß±¯¯ÿÿéà³Lä67äN¹éé¨\é颿 çésPépŸéè/æ˜féþ繯ÿÿÿéàå¯ik®!çééuŒééN¢Æ:ééžPé1@HHD͘féÿůÿÿÿéEé§©EJééé¥_éé!¢è"åélPém˜éû¢`ééÓ¯ÿÿÿéÞ‚àá‡éééá0eK"¢é†4O=XéÆ7kƒNéØ!oß±¯ÿÿÿéêÙ¸éé¹Úéé´ééè¦ÁÆØééÚÌu’éø±šÎééâ¶¹¯ÿÿÿéûŠz`1Ö	é¯ÿÿÿéýÛª¹é¯ÿÿÿ&é¯ÿÿÿ&é¯ÿÿÿ&é¯ÿÿ'¯ÿÿÿ&é¯ÿÿÿ&é¯ÿÿÿ&é¯ÿÿÿ&é¯ÿÿÿ&é¯ÿÿÿ&é¯ÿÿÿ&é¯ÿÿÿ&é¯ÿÿÿ&é¯ÿÿÿéþË¿éþè¢é¯ÿÿÿéþ¤Šéþå
éþ^ϯÿÿÿéßæ¿éé¸âéÈÞÖÏééÜ«Úééè¯ÍÇÝééÞ³çéÔ+œ¯ÿÿÿéàâË¡é-Ù�oé£.mCéØ0eE#±éƒGr:È»j¯ÿÿÿéàR­ŽYdÖ0é�oéG½é§é•oéè±æ.æé sé7ǯÿÿÿéà�oPÇ4šmé�oéçé×éešéé=±ºHHGké7ǯÿÿÿéæÎ03éIE«é�oéEÀéªé�héæ±ä%äéþAÁÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ'ÿ?¯ÿÌ é¯ÿÌ!é¯ÿÌ"é¯ÿÌ#é¯ÿÌ$é¯ÿÌ%é¯ÿÌ&é¯ÿÌ'é¯û–’¯ˆ 	¯ó~†ÔééåÌèééÇ™·é¯ûeZ¯VŒ¯ïT�¯¯"�Ôé½0%ÑéÇoUéÿ ¯äeZ¯–¥¯«„{Ÿ£.g‰È'×éé;šéoeéé©UéÿޝäeZ¯=‚­"Y]f– Flà'×éæ×é¯+éé©U靸eZ¯=‚œ"¡¯ï;šéé'×éÖééÓéé©Ué¯äeZ¯=‚¯h /‡¯E¯éé'×éãßé¹%éé©Ué¯ûeZ¯=‚¯ì‡&ÌY©éé'×éé%¸é�Oéé©UéÿŒ¯äeZ¯=‚‡GfCaé•9tß'×éé›j¹éé©Uéþÿž¯÷œš¯“¤ªƒnªéðÖ¯ä'¼âéé™ÏééÝ”Uéÿÿ¨Ÿÿ£¯ÿÌéý|eÆéý±e–éÿÿ€Gÿc¯ÿÌ.éÿ
¯ÿÌ.éÿ	¯ÿÌ.éÿ¯ÿÌ.éÿ?¯ÿÌé¯ÿÌ é¯ÿÌ!é¯ÿÌ"é¯ÿÌ#é¯ÿÌ$é¯ÿÌ%é¯ÿÌ&é¯ÿÌ'é¯û–’¯ˆ 	¯ý~†ÔéÿãéýÇ™·é¯ûeZ¯VŒ¯ïT�¯¯"�ÔésÞééÇoUéÿ ¯éeZ¯–¥¯«„{Ÿ£.g‰È'×ééÒæÞéþ©UéÿޝïeZ¯=‚­"Y]f– Flà'×éþÞéþ©U靸eZ¯=‚œ"¡¯ú;šéé'×éþÞéþ©Ué¯ïeZ¯=‚¯h /‡¯E¯éé'×éþÞéþ©Ué¯ûeZ¯=‚¯÷‡&ÌY©éé'×éþÞéþ©UéþÿŒ¯äeZ¯=‚‡GfCaé•9tß'×éé¢^ZŒéé©Uéÿÿž¯÷œš¯“¤ªƒnªéøÖ¯ä'¼âé˯ûÂéÝ”Uéÿÿ¨Ÿÿ£¯ÿÌéý|eÆéý±e–éÿÿ€Gÿc¯ÿÌ-éÿ
¯ÿÌ-éÿ	¯ÿÌ-éÿ¯ÿÌ-éÿ?¯ÿÌé¯ÿÌ é¯ÿÌ!é¯ÿÌ"é¯ÿÌ#é¯ÿÌ$é¯ÿÌ%é¯ÿÌ&é¯ÿÌ'é¯ü}«¬|	¯ø¡†¥ééãÉçéý¥™Øé¯ü£§<¯î¥=¯¯š3¥é¤#D'�éé¥0¶éÿ‰¯ä£­�¯¯Ÿy†¬‰‰®°NééÕåéÔâééH¶éÿ^¯ð£¤¯|+aN”llŸ°NéùÜáééH¶é¯ð£¤¯T_®¯¯¢"éé°NéýèOzéþH¶é¯ð£¤¯�CN§½&éé°Néþd^éþH¶é¯ü£¤¯÷«B�ß#éé°NéýhZèéþH¶éÿ?Æû©¥Æšµ	ÆóŽ‚´ÆÆÃ­ÅÆÆ©‚›#ÆûrfÆbžÆï` ÆÆ!x´Æ¡( ²Æ©_HÆÿµÆärfƪ»ÆÁ–‹´¹4t›Â!¶ÆÆ2‚Æ_UÆÆ�HÆÿ¡ÆärfÆE“Ä&dis©$Oi¾!¶ÆÄ¶Æ•$ÆÆ�H#ÆørfÆE“°'¶ÆïC•ÆÆ!¶Æ¶ÆÆ³ÆÆ�H#ÆärfÆE“Æv$6™ÆC•ÆÆ!¶ÆÁ½Æž ÆÆ�H#ÆûrfÆE“Æì˜+ÆK�ÆÆ!¶ÆÆ œÆxDÆÆ�HÆÿžÆärfÆE“˜QtK^Æ0c½!¶ÆÆ„ZžÆÆ�HÆþî³Æ÷±®Æ¦ºÁ”}¥Æð¶•Â! ÀÆÆ¥‚¯ÆÆ¼~HÆîÿ¾³ÿ¹ÆýjV¨Æý—VÆîÿ‘Pÿp5Æî:Æî9Æî8Æî?FÆû©¥Æšµ	ÆýŽ‚´ÆÿÁÆý©‚›#ÆûrfÆbžÆï` ÆÆ!x´Æb½ÆÆ©_HÆÿµÆérfƪ»ÆÁ–‹´¹4t›Â!¶ÆÆ³Ä½Æþ�HÆÿ¡ÆïrfÆE“Ä&dis©$Oi¾!¶Æþ½Æþ�H#ÆørfÆE“°'¶ÆúC•ÆÆ!¶Æþ½Æþ�H#ÆïrfÆE“Æv$6™ÆC•ÆÆ!¶Æþ½Æþ�H#ÆûrfÆE“Æ÷˜+ÆK�ÆÆ!¶Æþ½Æþ�HÆþîžÆärfÆE“˜QtK^Æ0c½!¶ÆÆ‰PMwÆÆ�HÆîÿ³Æ÷±®Æ¦ºÁ”}¥Æø¶•Â! ÀƬ”û¥Æ¼~HÆîÿ¾³ÿ¹ÆýjV¨Æý—VÆîÿ‘Pÿp4Æî9Æî8Æî7Æî?GÆü�ÁÂ�	Æø¶‚�ÆÆÁ«ÄÆý�‚·#Æü ¸½DÆî»EÆÆ–+�Æ‹9!xÆÆ�(›Æÿ›Æä ¸Ä¢ÆÆ³‰—Û›©–BÆÆµÃÆ´ÀÆÆ=›ÆÿjÆð ¸ºÆŒ1mY§{i‡–BÆù»
¿ÆÆ=›#Æð ¸ºÆ_lÄÆÆ·!ÆÆ–BÆýÅDgÆþ=›#Æð ¸ºƲLX½·!ÆÆ–BÆþUPÆþ=›#Æü ¸ºÆ÷ÁK}½ÆÆ–BÆýYMÅÆþ=›Æî?éÿÌ ¯éÿÌ!¯éÿÌ"¯éÿÌ#¯éÿÌ$¯éÿÌ%¯éÿÌ&¯éÿÌ'¯éûÇÃéµÕ	é󧆟¯¯¬™®¯¯–s‰¯éû†xésºéïp¼éé"jŸ¯Ž$�¯–T@¯ÿÕéä†xéÈÜéä°¤ÔÙ=‰¶È¡¯¯-s¯TK¯¯@¯ÿ½éä†xéQ­æ-v{‡Ç*]l¨¡¯­¡¯„ ¯¯@¯éø†xéQ­Ï.×éïO𝝡¯¡¯¯Ÿ¯¯@¯éä†xéQ­é‹*?´éE„¯¯¡¯ª§¯‹¯¯@¯éû†xéQ­éì³2ÌC¯¯¡¯¯Нj<¯¯@¯ÿºéä†xéQ­³_ˆYa¯p+W§¡¯¯uP‹¯¯@¯þªÒé÷ÐÍéÄÚ㯓ª¯ð¡„¬�ª¯¯‘s›¯¯¦o@¯ªÿàÓÿÙéÿ̯ý]L•¯ý…Lq¯ªÿ«^ÿ„éÿÌ.¯ª
éÿÌ.¯ª	éÿÌ.¯ªéÿÌ.¯ª?éÿ̯éÿÌ ¯éÿÌ!¯éÿÌ"¯éÿÌ#¯éÿÌ$¯éÿÌ%¯éÿÌ&¯éÿÌ'¯éûÇÃéµÕ	éý§†Ÿ¯ÿª¯ý–s‰¯éû†xésºéïp¼éé"jŸ¯V§¯¯–T@¯ÿÕéé†xéÈÜéä°¤ÔÙ=‰¶È¡¯¯ž­
§¯þ@¯ÿ½éï†xéQ­æ-v{‡Ç*]l¨¡¯þ
§¯þ@¯éø†xéQ­Ï.×éúO𝝡¯þ
§¯þ@¯éï†xéQ­é‹*?´éE„¯¯¡¯þ
§¯þ@¯éû†xéQ­é÷³2ÌC¯¯¡¯þ
§¯þ@¯þªºéä†xéQ­³_ˆYa¯p+W§¡¯¯yGDi¯¯@¯ªÿÒé÷ÐÍéÄÚ㯓ª¯ø¡„¬�ª¯˜ƒû‘¯¦o@¯ªÿàÓÿÙéÿ̯ý]L•¯ý…Lq¯ªÿ«^ÿ„éÿÌ-¯ª
éÿÌ-¯ª	éÿÌ-¯ªéÿÌ-¯ª?éÿ̯éÿÌ ¯éÿÌ!¯éÿÌ"¯éÿÌ#¯éÿÌ$¯éÿÌ%¯éÿÌ&¯éÿÌ'¯éü¦ää¥	éøÖ†|¯¯ª—®¯ý|s¢¯éü%ÙÞOéîÜQééš&|¯{3j¯¯|$‰¯ÿ·éä%Ùæ¾ééÓ¢²å·¶®„:¯¯ ¬¯Ÿª¯¯6‰¯ÿ}éð%ÙÚ$é¤:�hÄ�lw„:¯ù¥©¯¯6‰¯éð%ÙÚ$éoçééØ"¯¯„:¯ý®<[¯þ6‰¯éð%ÙÚ$éÑZ gÞ½¯¯„:¯þKG¯þ6‰¯éü%ÙÚ$é÷äX�§¯¯„:¯ýND®¯þ6‰¯ªÿÿ
éþÿÿÿ+éÿÿÿÿ*éÿÿÿÿ)éÿÿÿÿ(éÿÿÿÿ'éÿÿÿÿ&éÿÿÿÿ%éÿÿÿÿ$éÿÿÿÿéþ´léþç:é	ÿÿÿÿéþªUéþæé
ÿÿÿÿéèªOe<–ééÓxC�äé�©ãf—éÄæÝE¾éivÞeéÿÿÿÿéæªEééZ¤Õ{{cHéÉ}M,zé)ÙéÆéé
ÿÿÿÿéøªUééažÏ$¶òÁé6®Þ|ké&ÛéÉéÿÿÿÿéçªUééažé;¥çãÆåÚÑ-kéf{âjéÿÿÿÿéç¹zé郱éÎr4:žé—5A‡‹éØl0ŠMéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿ
éþÿÿÿ¯éÿÿÿÿ¯éÿÿÿÿ¯
éÿÿÿÿ¯	éÿÿÿÿ¯éÿÿÿÿ¯éÿÿÿÿ¯éÿÿÿÿ¯éÿÿÿÿ¯é	ÿÿÿÿ¯é
ÿÿÿÿ¯éÿÿÿÿ¯éÿÿÿÿ¯ÿé
ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯þ™�
¯þŸŠ¯ÿÿÿÿ¯þnQ
¯þ~B¯ÿÿÿÿ¯ünQ”Нþ�ޝú˜‡|B«�¯ÿ�ÿÿÿÿ¯èn 8B/®¯J5F)¨¯T.I&B®Ÿ¯œÿÿÿÿ¯èn"¯¯:‡¤¯¯Gx¬®¯NB¯_\¯Z]
ÿþéÿÿ¯ènE¯¯]^}?¯¯nN�1¯¯rB¯¢­ ÿÿ¯èn$¯¯<„¢ ¯¯Iu«¯¯PB¯¯>N<¯
éþÿÿÿ¯ènçécF]7àép=b3Xè&ÔéÐ#ªÿÿÿéè“-ééM´Ú)éé_ å!èéhXézéx|
ªþ¯ÿÿéè“\éé|}¦Téé’g¿Aéé˜XéØ%æ#Õÿÿéè“0ééO°Ø+éébœä!éékXééShOé
¯þªÿÿéè“%P_7æé[Ke0ÞéiCi/Xéé³®é¯ªÿÿÿéüÔÊ»«éþȰéöÀ©ÖÅééÍ%é鯪ÿÿÿéû£: éé
¯ªÿÿÿéþ²¼é	¯ªÿÿÿ鯪ÿÿÿ鯪ÿÿÿ鯪ÿÿÿ鯪ÿÿÿé¯	ªÿÿÿé¯
ªÿÿÿ鯪ÿÿÿé¯ªÿÿÿéÿ¯
ªÿÿÿéªÿÿÿéªÿÿÿéÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿ?éþmÈéúÍËéé„›éûÎSé­ˆéþÐ<
éú„{ééÈÏéúÈ7éå0âéö´WV¶éÒgw2ÝéÑr�ééÊiG�çéa‘;oÖÚ)&H·”§éÕyAŽâé{”ERÄéÐM9fÈéÈ7één“éõÏ0ÏÐ/áÉšäçéÑE´éé0«Ö<®é+WÜ…Ué„{éér�éT‰ÝXˆéPDÕ›OéÙÑé¦QéÈ7éé“iéø‚E{{5žÉéÑ#ÐéÏ/éé–eé+´éäç„{éér�ååé»@éPžéåéç™^HéÈ7éé¯Jéþze¶ýÔÉ6éÑ:»éÌ1ééšbé+·éäå„{éér�äæé¿<éP¯éééžWÎàèÈ7ééš`éøÎ/ÍéÏÙÉ6éÑešéé/²Ù@«é+Zá�Ré¦Yåér�éQ�á]…éP¯ééézzæ„èÈ7ééy†éù­R3JÑÑcéѽMééÄ^=“æé+„2cÒéào>²�¤éÑn7„àéw½ééJéÔP1~LèÐdéé5ÕéþIÍéþ+Ôéþ¯féþãäéþIØéþãåé?À¯ÿ«>¯ÿ®ÿ¯?Æþ\ªÆú®¬ÆÆq„Æû¯GÆ“tÆþ±3
ÆúpiÆÆª°Æúª/ÆÃ(ÀÆö™JI›Æ³Xe+¼ÆÑayÆÆ¬Y=†ÄÆR{2^¶º#!=›~ŽÆµf7xÀÆi~:F§Æ±A0WªÆª/ÆÆ]}Æõ°)°±(¿«‚ÂÄÆÑ:™ÆÆ)‘¶3”Æ$J»qHÆpiÆÆaxÆGt¼KtÆD9µ„CƸ²Æ�Eƪ/ÆÆ}YÆøn:ii-†«ÆÑ±Æ°(ÆÆVÆ$™ÆÁÄpiÆÆaxÃÃÆŸ6ÆD†ÆÃÆÄ‚P=ƪ/ÆÆ•?ÆþhU›ý´«.ÆÑ2ŸÆ­*ÆÆ‚SÆ$›ÆÂÃpiÆÆaxÂÄÆ¢3ÆD”ÆÆƆJ¯¾Ū/ÆÆƒRÆø¯(®Æ°¹«.ÆÑU‚ÆÆ(—¹6‘Æ$M¿xFÆ�KÃÆaxÆEz¿OqÆD”ÆÆÆhgÄpŪ/ÆÆfrÆù“F+?²²TÆÑ¡AÆÆ§P4}ÄÆ$p+T³Æ¾_5—{‹Æ²]/p¾Æe¡ÆÆ?Æ´D*k@űUÆÆ-µÆþ>®Æþ$´Æþ•WÆþÁÂÆþ>·ÆþÁÃÆ?ÀÆÿÁ>ÆÿÅÿÆ?¯þR–¯úš˜¯¯du¯ûš>¯‚f¯þœ-
¯úc]¯¯–œ¯ú–)¯¬$ª¯ö‡AA‰¯žNY&¦¯ÑVk¯¯˜O6v®¯Im-S¡¤6‰o~¯ [1jª¯]o3>”¯œ:+M–¯–)¯¯Rn¯õœ$œœ#©—s¬®¯Ñ3‡¯¯$€¡-‚¯ A¥d@¯c]¯¯Vj¯?g¦Bf¯<3 u;¯£�¯}=¯–)¯¯nO¯øa3]](w—¯Ñœ¯œ#¯¯qL¯ ‡¯«®c]¯¯Vj¬¬¯�0¯¯}C¬¯Vj¯=l©Fd¯<ƒ¯¯¯\[­c®–)¯¯[e¯ù‚>&8��J¯ÑŽ:¯¯”G.n­¯ c&Jž¯¨T/†l{¯�R)c¨¯Yޝ¯8¯Ÿ<%_9®œK¯¯( ¯þ7š¯þ Ÿ¯þ„M¯þª¬¯þ7¢¯þª¬¯?Àéÿä>éÿèÿéÿÿéÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿéþÚ¯
éÿâéÿÿÿéþ¾@
éÿÒéÿÿÿéû¾@ϯçéýÜ´èéýجËéÿÿÿéï¾?f0Ñé‡8j0Äé”1k=éÿÿÿéï¾ãéyˆé%ßé‹té5Ðé”éÿÿÿéï¾1éé¨RÑ)éé½;æééÄéÿÿÿéï¾
äé{…é#àéŽpé2Òé—éÿÿÿéï¾Cn,Ïé�:q,Âé�2r:éÿÿÿéû߿ɡäéýÕ¦åéýРÙéÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿéÿ¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿéþÚ¯
éÿâ¯ÿÿÿéþ¾@
éÿÒ¯ÿÿÿéû¾@ϯçéýÜ´èéýج˯ÿÿÿéï¾?f0Ñé‡8j0Äé”1k=¯ÿÿÿéï¾ãéyˆé%ßé‹té5Ð锯ÿÿÿéï¾1éé¨RÑ)éé½;æééįÿÿÿéï¾
äé{…é#àéŽpé2Òé—¯ÿÿÿéï¾Cn,Ïé�:q,Âé�2r:¯ÿÿÿéû߿ɡäéýÕ¦åéýРÙ¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿ¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿéÿÆÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-ÆÿÿÿÆþº•
ÆÿÀÆÿÿÿÆþ¢6
Æÿ³ÆÿÿÿÆû¢6°”ÄÆý»™ÅÆý·’¬ÆÿÿÿÆï¢6W(²Æs/Z(§Æ~*[4ÆÿÿÿÆï¢ÁÆftƽÆvcÆ-±Æ~ÆÿÿÿÆï¢*ÆÆ�F²#ÆÆ¡2ÄÆÆ¦ÆÿÿÿÆï¢	ÁÆiqƾÆx`Æ+³Æ€ÆÿÿÿÆï¢9]%¯Æm2`%¥Æy+a2ÆÿÿÿÆû½¢«‰ÂÆýµ�ÃÆý±ˆ¹Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-ÆÿÿÿÆÿÆÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-ÆÿÿÿÆþº•
ÆÿÀÆÿÿÿÆþ¢6
Æÿ³ÆÿÿÿÆû¢6°”ÄÆý»™ÅÆý·’¬ÆÿÿÿÆï¢6W(²Æs/Z(§Æ~*[4ÆÿÿÿÆï¢ÁÆftƽÆvcÆ-±Æ~ÆÿÿÿÆï¢*ÆÆ�F²#ÆÆ¡2ÄÆÆ¦ÆÿÿÿÆï¢	ÁÆiqƾÆx`Æ+³Æ€ÆÿÿÿÆï¢9]%¯Æm2`%¥Æy+a2ÆÿÿÿÆû½¢«‰ÂÆýµ�ÃÆý±ˆ¹Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-ÆÿÿÆÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-ÆÿÿÿÆÿ¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ¯þ¤„
¯ÿª¯ÿÿÿ¯þ�0
¯ÿž¯ÿÿÿ¯û�0œƒ®¯ý¥‡®¯ý¢�˜¯ÿÿÿ¯ï�/M$�¯f*P$”¯o%P.¯ÿÿÿ¯ï�ª¯[f¯§¯hW¯(œ¯o¯ÿÿÿ¯ï�%¯¯~>�¯¯Ž-­¯¯“¯ÿÿÿ¯ï�«¯]d¯¨¯jT¯&ž¯q¯ÿÿÿ¯ï�2R!›¯a,U!‘¯k&V,¯ÿÿÿ¯û§�—y¬¯ý }¬¯ýœx£¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ¯ÿéÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯þ¤„
¯ÿªéÿÿÿ¯þ�0
¯ÿžéÿÿÿ¯û�0œƒ®¯ý¥‡®¯ý¢�˜éÿÿÿ¯ï�/M$�¯f*P$”¯o%P.éÿÿÿ¯ï�ª¯[f¯§¯hW¯(œ¯oéÿÿÿ¯ï�%¯¯~>�¯¯Ž-­¯¯“éÿÿÿ¯ï�«¯]d¯¨¯jT¯&ž¯qéÿÿÿ¯ï�2R!›¯a,U!‘¯k&V,éÿÿÿ¯û§�—y¬¯ý }¬¯ýœx£éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿéÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿ?éÿ¨éýÀ™»éùÎâééà™ž*éÿ,éóo^»éß9+¥éà™*éë,éºèééÇÚo�éé„Sè¸èéé*éë,éK®éæ×o�ééG�ééÍéé*éë,é«Oé£Qéo�éé4°éé1¹éé*éë,éè&ÖF¯éo�ééB™ééÇéé*éë,éée(ééo�éénoéÕãéé*éï,é騂ééo�ééÏd#}éÿ*éëºééäÙééo}ÙééÖš¸ééæÌ,éý¶Quéý¥ežéýÙen,éýÁ°æûé?@éÿ¨éýÀ™»éùäçééà™ž*éÿ,éóo^»é¬$–ééà™*éð,éºèééÇÚo�ééÚßZ–éÿ*éö,éK®éæ×o�éþZ–éÿ*éö,é«Oé£Qéo�éþZ–éÿ*éö,éè&ÖF¯éo�éþZ–éÿ*éö,éée(ééo�éþZ–éÿ*éï,é騂ééo�ééÍ^%=aéÿ*éóºééäÙééo}Ùéݯû°éæÌ,éý¶Quéý¥ežéýÙen,éýÁ°æûé?ÆéþÎt=éþ±}éþÎt=éþ±}éþÎt=éþ±xé?Æÿ�Æý£‚ŸÆù¯ÀÆÆ¾‚†*Æÿ%Æó_PŸÆ½0$
�ƾ‚*Æë%ÆžÅÆÆ©º_yÆÆpGÅœÅÆÆ*Æë%Æ@”ÆÄ¶_yÆÆ=zÆÆ®ÆÆ*Æë%Æ‘DÆŠEÆ_yÆÆ,–ÆÆ*žÆÆ*Æë%ÆÅ!¶<•Æ_yÆÆ8‚ÆÆ©ÆÆ*Æë%ÆÆlU"ÆÆ_yÆÆ]_ƵÁÆÆ*Æï%ÆÆ·nÆÆ_yÆÆ°UjÆÿ*ÆëžÆÆÁ¸ÆÆ_j¸ÆÆ¶ƒœÆÆÄ­,Æý›EcÆý�V†Æý¹V],Æý¤–ÄûÆ?@Æÿ�Æý£‚ŸÆùÂÄÆÆ¾‚†*Æÿ%Æó_PŸÆ’	ÆÆ¾‚*Æð%ÆžÅÆÆ©º_yÆÆº½LÆÿ*Æö%Æ@”ÆÄ¶_yÆþLÆÿ*Æö%Æ‘DÆŠEÆ_yÆþLÆÿ*Æö%ÆÅ!¶<•Æ_yÆþLÆÿ*Æö%ÆÆlU"ÆÆ_yÆþLÆÿ*Æï%ÆÆ·nÆÆ_yÆÆ®P4RÆÿ*ÆóžÆÆÁ¸ÆÆ_j¸Æ¼”û–ÆÄ­,Æý›EcÆý�V†Æý¹V],Æý¤–ÄûÆ?ÆÆþ¯c=Æþ—}Æþ¯c=Æþ—}Æþ¯c=Æþ—xÆ?¯ÿ~¯ý�s�¯ùšª¯¯¨sw*¯ÿ!¯óTG�¯§+ |¯¨s*¯ë!¯Œ®¯¯–¤Tk¯¯c>®Š®¯¯*¯ë!¯8‚¯­¡Tk¯¯6l¯¯
š¯¯*¯ë!¯€<¯z=¯Tk¯¯'„¯¯%‹¯¯*¯ë!¯®¡5„¯Tk¯¯1s¯¯–¯¯*¯ë!¯¯_K¯¯Tk¯¯RT¯ ª¯¯*¯ï!¯¯¢a¯¯Tk¯¯œK^¯ÿ*¯ëŒ¯¯«£¯¯T^£¯¯¡tН¯­™,¯ý‰=X¯ý|Lw¯ý£LR,¯ý‘„­û¯?@¯ÿ~¯ý�s�¯ù¬®¯¯¨sw*¯ÿ!¯óTG�¯�q¯¯¨s*¯ð!¯Œ®¯¯–¤Tk¯¯¤§Cq¯ÿ*¯ö!¯8‚¯­¡Tk¯þCq¯ÿ*¯ö!¯€<¯z=¯Tk¯þCq¯ÿ*¯ö!¯®¡5„¯Tk¯þCq¯ÿ*¯ö!¯¯_K¯¯Tk¯þCq¯ÿ*¯ï!¯¯¢a¯¯Tk¯¯šG.I¯ÿ*¯óŒ¯¯«£¯¯T^£¯¦ƒû„¯­™,¯ý‰=X¯ý|Lw¯ý£LR,¯ý‘„­û¯?ƯþšW=¯þ…}¯þšW=¯þ…}¯þšW=¯þ…x¯ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿ ÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿÿþ÷ÞÿþÞ÷ÿÿéÿÿúÿÿš&€ÿú€&šÿÿéÿÿúÈ;
?ú?
;ÈéÿÿúÈ;
?ú?
;Èéÿÿúÿÿš&€ÿú€&šÿÿéÿÿÿÿþ÷ÞÿþÞ÷ÿ ÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿ ÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿÿþ÷ÞÿþÞ÷ÿÿÆÿÿúÿÿš&€ÿú€&šÿÿÆÿÿúÈ;
?ú?
;ÈÆÿÿúÈ;
?ú?
;ÈÆÿÿúÿÿš&€ÿú€&šÿÿÆÿÿÿÿþ÷ÞÿþÞ÷ÿ ÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿ ÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿÿþ÷ÞÿþÞ÷ÿÿ¯ÿÿúÿÿš&€ÿú€&šÿÿ¯ÿÿúÈ;
?ú?
;ȯÿÿúÈ;
?ú?
;ȯÿÿúÿÿš&€ÿú€&šÿÿ¯ÿÿÿÿþ÷ÞÿþÞ÷ÿ ÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿ?*éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ"éü­Þé¡éýâ™™éþÞÑéý²™Ìéÿéþ­Þéü8ÇééòÔ™éé�%Oæé²F“éÿéþ8ÇéÞÁ©ßééÞ²çéé8Çéééפ¯ãÔ+ééÜÏä8¢éél“éÿéôÁ©ßééÞ²çéé8ÇéÜÑ7\v�éƒGr:Çé8Çéé¶1€m¹Ô+éé¬+ééqhéél“éÿéòÑ7\v�éƒGr:Çé8Çéþ�€éáæ.åé¡sé8Çéé‚oåééÔ+éé™Léé–Uéél“éÿéþ�€é÷æ.åé¡sé8ÇéþD¶éá»HHGjé8ÇééÙb _ØÔ+éé¦4éé|céél“éÿéþD¶é÷»HHGjé8Çéþ}ƒéýä%äéü8Çééïåh�Ô+ééÏæéO�éél“éÿéþ}ƒéýä%äéþ8ÇéèÏ0c‚˜é~B‚r…é8Çéé{t†0»Ô+é÷a0Y%Ýéél“éÿéòÏ0c‚˜é~B‚r…é8ÇéÞ¯œÚééÜš«âé½áé´éÕ ›×éÔ%Ìéé讟ÝééÕ^“éÿéô¯œÚééÜš«âé½áéýÝeeéý�e¹éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿé?*éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ"éü­Þé¡éøâ™™ééæÉäéý²™Ìéÿéþ­Þéü8ÇééòÔ™é¿#B(qéé²F“éÿéþ8ÇéÞÁ©ßééÞ²çéé8Çéééפ¯ãÔ+ééÚßéäÎéél“éÿéôÁ©ßééÞ²çéé8ÇéèÑ7\v�éƒGr:Çé8Çéé¶1€m¹Ô+éùç"Äéél“éÿéòÑ7\v�éƒGr:Çé8Çéþ�€éíæ.åé¡sé8Çéé‚oåééÔ+éþrVéþl“éÿéþ�€é÷æ.åé¡sé8ÇéþD¶éí»HHGjé8ÇééÙb _ØÔ+éý†Aãéþl“éÿéþD¶é÷»HHGjé8Çéþ}ƒéýä%äéü8Çééûåh�Ô+éýŠ?âéþl“éÿéþ}ƒéýä%äéþ8ÇéÜÏ0c‚˜é~B‚r…é8Çéé{t†0»Ô+éé³U^^¹éél“éÿéòÏ0c‚˜é~B‚r…é8Çé篜ÚééÜš«âé½áé´éÕ ›×éÔ%Ì騝ûÕéÕ^“éÿéô¯œÚééÜš«âé½áéýÝeeéý�e¹éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿé?*éÿ>éÿ>éÿ>éÿé?*Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ"Æü“½Æ‰ÆýÀ‚‚Æþ½²Æý—‚­ÆÿÆþ“½Æü/©ÆÆò´‚ÆÆyCÄÆ—<}ÆÿÆþ/©ÆÞ¤�½ÆÆ½—ÄÆÆ/©ÆÆÆ¶‹”Á´$ÆÆ»°Á/‰ÆÆ\}ÆÿÆô¤�½ÆÆ½—ÄÆÆ/©ÆÜ²/Nd{Æo=a2©Æ/©ÆÆ›*m\�´$ÆÆ’$ÆÆ`YÆÆ\}ÆÿÆò²/Nd{Æo=a2©Æ/©ÆmÆáÄ'ÃÆ‰bÆ/©ÆÆn^ÃÆÆ´$ÆÆ‚@ÆÆHÆÆ\}ÆÿÆmÆ÷Ä'ÃÆ‰bÆ/©Æþ9›ÆþŸ=æZÆ/©ÆƸSQ·´$ÆÆ�,ÆÆjTÆÆ\}ÆÿÆþ9›ÆþŸ=üZÆ/©ÆþjoÆýÂÂÆü/©ÆÆïÃYm´$ÆÆ°ÄÆDxÆÆ\}ÆÿÆþjoÆýÂÂÆþ/©Æè¯(Tn�Æk8naqÆ/©ÆÆicr)Ÿ´$Æ÷R)K ¼ÆÆ\}ÆÿÆò¯(Tn�Æk8naqÆ/©ÆÞ•…ºÆÆ»‚‘ÀÆ¡¿Æ™Æµˆ„¶Æ´ ­ÆÆÅ”‡¼ÆÆµP}ÆÿÆô•…ºÆÆ»‚‘ÀÆ¡¿Æý¼VVÆýyV�Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>ÆÿÆ?*Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ"Æü“½Æ‰ÆøÀ‚‚ÆÆÄ«ÁÆý—‚­ÆÿÆþ“½Æü/©ÆÆò´‚Æ¢8"`ÆÆ—<}ÆÿÆþ/©ÆÞ¤�½ÆÆ½—ÄÆÆ/©ÆÆÆ¶‹”Á´$ÆÆº½ÆÂ¯ÆÆ\}ÆÿÆô¤�½ÆÆ½—ÄÆÆ/©Æè²/Nd{Æo=a2©Æ/©ÆÆ›*m\�´$ÆùÄ¦ÆÆ\}ÆÿÆò²/Nd{Æo=a2©Æ/©ÆmÆíÄ'ÃÆ‰bÆ/©ÆÆn^ÃÆÆ´$ÆþaIÆþ\}ÆÿÆmÆ÷Ä'ÃÆ‰bÆ/©Æþ9›ÆþŸ=òZÆ/©ÆƸSQ·´$Æýr7ÁÆþ\}ÆÿÆþ9›ÆþŸ=üZÆ/©ÆþjoÆýÂÂÆü/©ÆÆûÃYm´$Æýu6ÀÆþ\}ÆÿÆþjoÆýÂÂÆþ/©ÆÜ¯(Tn�Æk8naqÆ/©ÆÆicr)Ÿ´$ÆÆ˜HPPžÆÆ\}ÆÿÆò¯(Tn�Æk8naqÆ/©Æç•…ºÆÆ»‚‘ÀÆ¡¿Æ™Æµˆ„¶Æ´ ­Æ·”ûµÆµP}ÆÿÆô•…ºÆÆ»‚‘ÀÆ¡¿Æý¼VVÆýyV�Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>ÆÿÆ?*Æÿ>Æÿ>Æÿ>ÆÿÆ?*¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ"¯ü‚§¯y¯ýªss¯þ§�¯ý†s™¯ÿ¯þ‚§¯ü*–¯¯òŸs¯¯k;­¯†5n¯ÿ¯þ*–¯Þ‘§¯¯§†®¯¯*–¯¯¯¡{ƒªŸ ¯¯¥œ«*y¯¯Qn¯ÿ¯ô‘§¯¯§†®¯¯*–¯Ü�)EYl¯b6V,–¯*–¯¯‰%`R‹Ÿ ¯¯� ¯¯UN¯¯Qn¯ÿ¯ò�)EYl¯b6V,–¯*–¯þa`¯á­"¬¯yV¯*–¯¯aS¬¯¯Ÿ ¯¯s9¯¯q@¯¯Qn¯ÿ¯þa`¯÷­"¬¯yV¯*–¯þ3‰¯þ�6æP¯*–¯¯£IG¢Ÿ ¯¯}'¯¯]J¯¯Qn¯ÿ¯þ3‰¯þ�6üP¯*–¯þ^b¯ý¬¬¯ü*–¯¯ï¬NaŸ ¯¯œ­¯¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ¯?*¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ"¯ü‚§¯y¯øªss¯¯­—«¯ý†s™¯ÿ¯þ‚§¯ü*–¯¯òŸs¯�1U¯¯†5n¯ÿ¯þ*–¯Þ‘§¯¯§†®¯¯*–¯¯¯¡{ƒªŸ ¯¯¤§¯¬š¯¯Qn¯ÿ¯ô‘§¯¯§†®¯¯*–¯è�)EYl¯b6V,–¯*–¯¯‰%`R‹Ÿ ¯ù®“¯¯Qn¯ÿ¯ò�)EYl¯b6V,–¯*–¯þa`¯í­"¬¯yV¯*–¯¯aS¬¯¯Ÿ ¯þVA¯þQn¯ÿ¯þa`¯÷­"¬¯yV¯*–¯þ3‰¯þ�6òP¯*–¯¯£IG¢Ÿ ¯ýe1ª¯þQn¯ÿ¯þ3‰¯þ�6üP¯*–¯þ^b¯ý¬¬¯ü*–¯¯û¬NaŸ ¯ýh/ª¯þQn¯ÿ¯þ^b¯ý¬¬¯þ*–¯Ü›$Jar¯_1aVd¯*–¯¯]We$�Ÿ ¯¯‡@GG‹¯¯Qn¯ÿ¯ò›$Jar¯_1aVd¯*–¯ç„u¤¯¯¥s€ª¯Ž©¯‡¯ xu¡¯Ÿ™¯¢ƒû ¯ Gn¯ÿ¯ô„u¤¯¯¥s€ª¯Ž©¯ý¦LL¯ýkL‹¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ¯?*¯ÿ>¯ÿ>¯ÿ>¯ÿ¯ÿ'ÿéÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯ÿ¡éýâ™™éþçäéý²™Ìéÿ
ÿÿÿÿ¯ÿéøÔ™éé?2éý²F“éÿ
ÿÿÿÿ¯÷ééפ¯ãÔ+éýϾ2éþl“éÿ
ÿÿÿÿ¯÷é¶1€m¹Ô+éþ¾2éþl“éÿ
ÿÿÿÿ¯÷é‚oåééÔ+éþ¾2éþl“éÿ
ÿÿÿÿ¯÷éÙb _ØÔ+éþ¾2éþl“éÿ
ÿÿÿÿ¯ÿéûåh�Ô+éþ¾2éþl“éÿ
ÿÿÿÿ¯÷é{t†0»Ô+é÷}M^°éél“éÿ
ÿÿÿÿ¯ó´éÕ ›×éÔ%Ìé鼯ûÑéÕ^“éÿ
ÿÿÿÿ¯éýÝeeéý�e¹éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯ÿ¡éøâ™™ééäÈÞéý²™Ìéÿ
ÿÿÿÿ¯ÿéòÔ™éÝ$F,Qéé²F“éÿÿþ÷ÞÿþÞ÷ÿÿÿÿ¯óª˜¯¯ééפ¯ãÔ+éú1·éél“éúÿÿš&€ÿú€&šÿÿÿﯯjX¯¯é¶1€m¹Ô+éøÚ·#Óéél“éúÈ;
?ú?
;Èÿï‰(+WWé‚oåééÔ+éø:qæéél“éúÈ;
?ú?
;Èÿï‰(+WWéÙb _ØÔ+éùæ=Ÿéél“éúÿÿš&€ÿú€&šÿÿÿ÷¯¯jX¯¯éûåh�Ô+éúY•éél“éÿÿþ÷ÞÿþÞ÷ÿÿÿÿ¯çª˜¯¯é{t†0»Ô+éé°UzO)äéél“éÿ
ÿÿÿÿ¯ñ´éÕ ›×éÔ%Ìéä°“ÀéýÕ^“éÿ
ÿÿÿÿ¯éýÝeeéý�e¹éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯'ÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ‰ÆýÀ‚‚ÆþÄÁÆý—‚­Æÿ
ÿÿÿÿÆÿÆø´‚ÆÆ6+Æý—<}Æÿ
ÿÿÿÿÆ÷ÆÆ¶‹”Á´$Æý°¢+Æþ\}Æÿ
ÿÿÿÿÆ÷Æ›*m\�´$Æþ¢+Æþ\}Æÿ
ÿÿÿÿÆ÷Æn^ÃÆÆ´$Æþ¢+Æþ\}Æÿ
ÿÿÿÿÆ÷ƸSQ·´$Æþ¢+Æþ\}Æÿ
ÿÿÿÿÆÿÆûÃYm´$Æþ¢+Æþ\}Æÿ
ÿÿÿÿÆ÷Æicr)Ÿ´$Æ÷jAP–ÆÆ\}Æÿ
ÿÿÿÿÆó™Æµˆ„¶Æ´ ­ÆÆ ”û²ÆµP}Æÿ
ÿÿÿÿ
Æý¼VVÆýyV�Æÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ‰ÆøÀ‚‚ÆÆÂª½Æý—‚­Æÿ
ÿÿÿÿÆÿÆò´‚Ƽ<%EÆÆ—<}Æÿÿþ÷ÞÿþÞ÷ÿÿÿÿÆóÀ¬ÆÆÆÆ¶‹”Á´$Æú*›ÆÆ\}Æúÿÿš&€ÿú€&šÿÿÿïÆÆxcÆÆÆ›*m\�´$Æøº›³ÆÆ\}ÆúÈ;
?ú?
;Èÿï›.1ccÆn^ÃÆÆ´$Æø1
`ÄÆÆ\}ÆúÈ;
?ú?
;Èÿï›.1ccƸSQ·´$ÆùÄ4‡ÆÆ\}Æúÿÿš&€ÿú€&šÿÿÿ÷ÆÆxcÆÆÆûÃYm´$ÆúKÆÆ\}Æÿÿþ÷ÞÿþÞ÷ÿÿÿÿÆçÀ¬ÆÆÆicr)Ÿ´$ÆÆ–HgC#ÁÆÆ\}Æÿ
ÿÿÿÿÆñ™Æµˆ„¶Æ´ ­ÆÂ–}£ÆýµP}Æÿ
ÿÿÿÿ
Æý¼VVÆýyV�Æÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆ'ÿ¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿéÿy¯ýªss¯þ®«¯ý†s™¯ÿ
ÿÿÿÿéÿ¯øŸs¯¯/&¯ý†5n¯ÿ
ÿÿÿÿé÷¯¯¡{ƒªŸ ¯ýœ�&¯þQn¯ÿ
ÿÿÿÿé÷¯‰%`R‹Ÿ ¯þ�&¯þQn¯ÿ
ÿÿÿÿé÷¯aS¬¯¯Ÿ ¯þ�&¯þQn¯ÿ
ÿÿÿÿé÷¯£IG¢Ÿ ¯þ�&¯þQn¯ÿ
ÿÿÿÿéÿ¯û¬NaŸ ¯þ�&¯þQn¯ÿ
ÿÿÿÿé÷¯]We$�Ÿ ¯÷^:G„¯¯Qn¯ÿ
ÿÿÿÿé󇯠xu¡¯Ÿ™¯¯�ƒû�¯ Gn¯ÿ
ÿÿÿÿé¯ý¦LL¯ýkL‹¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿéÿy¯øªss¯¯¬–§¯ý†s™¯ÿ
ÿÿÿÿéÿ¯òŸs¯¦5!=¯¯†5n¯ÿÿþ÷ÞÿþÞ÷ÿÿÿÿéóâËé鯯¡{ƒªŸ ¯ú%‰¯¯Qn¯úÿÿš&€ÿú€&šÿÿÿïéé�#ué鯉%`R‹Ÿ ¯ø¤‰Ÿ¯¯Qn¯úÈ;
?ú?
;Èÿï·6	:tt¯aS¬¯¯Ÿ ¯ø+U­¯¯Qn¯úÈ;
?ú?
;Èÿï·6	:tt¯£IG¢Ÿ ¯ù­.w¯¯Qn¯úÿÿš&€ÿú€&šÿÿÿ÷éé�#uéé¯û¬NaŸ ¯úCp¯¯Qn¯ÿÿþ÷ÞÿþÞ÷ÿÿÿÿéçâËéé¯]We$�Ÿ ¯¯„@[;«¯¯Qn¯ÿ
ÿÿÿÿéñ‡¯ xu¡¯Ÿ™¯¬„n�¯ý Gn¯ÿ
ÿÿÿÿé¯ý¦LL¯ýkL‹¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé'ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿ??¯ÿ­>¯ÿY=¯û£¯k•	¯ú®¬¯¯®¬*¯û<¯<ƒ	¯ú“-¯¯†:)¯Ù®aI¯a¯ø/�¡Y¯(œ¯Û”¯<_¯¯§hY¯¯"œ“-¯¯†:¯¯a_¯¯¡‡!¯¯*s¯�3¯ù^asW.§
¯Û”¯¯¨«¡#¯¯•/¯¯zE¯¦­‡8¯¯5‹¯˜(¯ù®$®Vw¯Ù®aI¯<:'!u¯¬I+&l¯¯C,b¯:,lªF+&p¯‡8¯¯5‹¯˜(¯ú8=¯…£¯þ<¯þ§«¯þ§®¯þ§®$¯þ£#¯ÿ’sÿ„¯ÿY#¯ÿ’sÿ„¯ÿ­H¯7¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ5¯ÿ̯ÿ4¯þÌé¯ÿ3¯ýÌéé¯ÿ2¯ÿÌé¯ÿ1¯ÿÌé¯ÿ0¯ÿÌé¯ÿ/¯ÿÌé¯ÿ.¯ÿÌé¯ÿ-¯ÿÌé¯þ˜ª¯ÿ,¯ÿÌé¯ýXj¯ÿ+¯ÿÌ	éWù+(‰¯¯þc€¯ÿÌ
éWù+(‰¯¯ñ§Æÿe=Æû¹Æy¨	ÆúÅÂÆÆÅÂ*ÆûDÆD”	Æú¦2ÆÆ—A)ÆÙÅmSÆD…f€ÄÆÆ·n…ÃÆii�Z"i—ƶmˆÄƱ‡†]žµh€ÂÆøsÅÆt«Æ‘ªÆÛ§ÆD(†]=Ʋƒf.Ɖ(›­{2›°¬ †b4Æ™
f‚I‡FnÆø6£¶eÆ.±ÆÛ§ÆDlÆÆ½ueÆÆ'°¦2ÆÆ—AÆÆnlÆÆ!¶™%ÆÆ/‚ÆŸ:Æùjn‚c4½"
ÆÛ§ÆD…ÆÆ/¦X~ÆÆ@”¦2ÆÆ—AÆÆQ…ÆÆ9›™@ÆÆ<�Ƭ-Æùž:N¢2„S
ÆÛ§ÆDNƹÆ‘GƾÁ¶(ÆÆ¨6ÆÆŠNƼÄ™@ÆÆ<�Ƭ-ÆùÄ)Åb"‡ÆÙÅmSÆDA,%…ÆÂR0+zÆÆL2oÆA2zÀO1+Æ™@ÆÆ<�Ƭ-Æú@EÆ—¹ÆþDÆþ½ÁÆþ½ÄÆþ½Å$Æþ¹#Æÿ¥‚ÿ–Æÿe#Æÿ¥‚ÿ–ÆÿÄHÆ7Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ8Æþ¬ÀÆÿ8ÆýcxÆÿ6Æcù1.›ÆÆþp�Æcù1.›ÆÆñ½De›¾ÆÁ—ÂÆÆ¯¶œ§Æø©¼Æ­¯Æ»¬ÆýcxÆÿÆð³/Di´�*](�ÆY)N:RÆø`xÆ6;ÆslÆþ¬ÀÆÿÆðX�ÆÆ>žÆ˜@ÆYJÆÆ!ºÆø•DÁ/-Ã>¡ÆÿÆðX�ÆÆÄÆÁÆYrÆÆFŽÆøÁ’]X–ÅÆÿÆð`{ÆÆ;¡Æ›=ÆYLÆÆ"·Æú6:’�:B	ÆÿÆñ’(a³‰)c'ŒÆY$T@KÆúkÀ½w	ÆÿÆóº—½Æ½‹½ÆÆY”�Æú¸�ÆÆœº	Æÿ"ÆþYÆÿóÿ´Æÿ"Æþ¡®Æÿ¯PÿRÆÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ6Æ??éÿæ>éÿw=éûÙ"é�Æ	éúèäééèä*éûOéP¯	éúÄ;éé²M)éÙè�béPœx—æé騂œåé{{©j({±éÖ€ æéПžn¹Õz—äéø‡èéˆÉé«ÈéÛÄéP/žnHéÑ#šy6é¢/¶Ì�;¶ÏÊ%�s=é´x™VŸR‚éø?ÀÖwé6ÐéÛÄéPéé!ÞŠwéé.ÏÄ;éé²Méé‚éé'Ö´,éé8šé»Eéù}‚št=Þ(
éÛÄéPœéé7Äg”ééL®Ä;éé²Méé_œééD¶´KééF¹éÊ5éùºE[¿:›b
éÛÄéP[éÙ"é«Séàä×/ééÆ?éé£[éÝæ´KééF¹éÊ5éùç0ès(ŸéÙè�béPM4,œéäa93�ééZ:ƒéM:�â]:3•é´KééF¹éÊ5éúKQé±ÙéþOéþÞäéþÞçéþÞè$éþÙ"#éÿÙÿ°éÿw#éÿÙÿ°éÿæHé7éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ5éÿÌéÿ4éþ̯éÿ3éý̯¯éÿ2éÿ̯éÿ1éÿ̯éÿ0éÿ̯éÿ/éÿ̯éÿ.éÿ̯éÿ-éÿ̯éþËâéÿ,éÿ̯éýu#�éÿ+éÿÌ	¯tù:	6·ééþ„ªéÿÌ
¯tù:	6·ééñßPw¶àéä±äééÎ׸Äéÿ̯ò–¦¯™š¯¥˜¯ééu#�éÿéîÒ7P{Ô¦1n0¨éi0[E`é̯óUj¯04¯f_¯ééËâéÿéðg˜ééIºé³LéiWéé'¿¯÷„<ª*(¬7ޝéÿéðg˜ééæéäéi†ééH~¯÷«�RN„®¯éÿéðq‘ééE½é¶HéiZéÌ¢¯ø03�|3:¯¯éÿéñ¬0rÓ¢0u.¤éi+V8C¯ø_ª§i¯¯éÿéóÚ±Þéߤßééi‚‚‹¯ø£‹¯¯Š¥¯¯éÿ"éþ\q¯ÿ¬Ÿ¯éÿ!éýÌŽš¯ÿšGÿI¯éÿ éÿ̯éÿéÿ̯éÿéÿ̯éÿéÿ̯éÿéÿ̯éÿéÿ̯éÿéÿ̯éÿéÿ̯éÿéÿ̯éÿéÿ̯éÿéÿ̯ÿ/ÿÿ.¯ÿÿÿ.¯ÿÿÿ.¯ÿÿÿ.¯ÿÿþh-¯ÿÿý—,¯ÿÿÿ0,¯ÿÿþ
|+¯ÿÿüW{+¯ÿÿþ-¯ÿÿþ-¯ÿÿþ-¯ÿÿþ-¯ÿÿüW{+¯ÿÿþ
|+¯ÿÿÿ0,¯ÿÿý—,¯ÿÿþh-¯ÿÿÿ.¯ÿÿÿ.¯ÿÿÿ.¯ÿÿÿ.¯ÿÿÿ.¯ÿÿ ¯ÿÿÿ¯ÿÌéÿ¯ÿÿÿ¯ÿÌéÿ¯ÿÿÿ¯ÿÌéÿ¯ÿÿÿ¯ÿÌéÿ¯ÿÿü¯¯Ìéÿ¯ÿÿý¯Ìéÿ¯ÿÿþÌéÿ¯ÿÿÿéÿ¯ÿÿÿéÿ¯ÿÿÿéÿ¯ÿÿÿéÿ¯ÿÿÿéÿ¯ÿÿÿéÿ¯ÿÿÿéÿ¯ÿÿÿéÿ¯ÿÿÿéÿ¯ÿÿÿéþô¯þª˜¯þ˜ª¯ÿÿÿéýôÿ¯ýjX¯úXj¯¯ÿÿéöôÿÿ¯‰(+Wú+(‰ÿÿéÿôÿù¯‰(+Wú+(‰ÿÿéÿôÿÿ¯ýjX¯úXj¯¯ÿÿéÿôÿÿ¯þª˜¯þ˜ª¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿô	ÿÿ¯ÿÿÿéÿô
ÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿô
ÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿ
éÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿ
éÿôÿÿ¯ÿÿÿ	éÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿ/ÿÿ.Æÿÿÿ.Æÿÿÿ.Æÿÿÿ.Æÿÿþv-Æÿÿý«,Æÿÿÿ6,ÆÿÿþŒ+Æÿÿüc‹+Æÿÿþ-Æÿÿþ-Æÿÿþ-Æÿÿþ-Æÿÿüc‹+ÆÿÿþŒ+Æÿÿÿ6,Æÿÿý«,Æÿÿþv-Æÿÿÿ.Æÿÿÿ.Æÿÿÿ.Æÿÿÿ.Æÿÿÿ.Æÿÿ ÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆþÚÆþÀ¬Æþ¬ÀÆÿÿÿÆýÚîÆýxcÆúcxÆÆÿÿÆöÚîîÆ›.1cú1.›ÿÿÆÿÚîùÆ›.1cú1.›ÿÿÆÿÚîÿÆýxcÆúcxÆÆÿÿÆÿÚîÿÆþÀ¬Æþ¬ÀÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚ	îÿÆÿÿÿÆÿÚ
îÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚ
îÿÆÿÿÿÆÿÚîÿÆÿÿÿ
ÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿ
ÆÿÚîÿÆÿÿÿ	ÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿ/ÿÿ.éÿÿÿ.éÿÿÿ.éÿÿÿ.éÿÿþ‹-éÿÿýÉ,éÿÿÿ@,éÿÿþ
¤+éÿÿüt¤+éÿÿþ$-éÿÿþ$-éÿÿþ$-éÿÿþ$-éÿÿüt¤+éÿÿþ
¤+éÿÿÿ@,éÿÿýÉ,éÿÿþ‹-éÿÿÿ.éÿÿÿ.éÿÿÿ.éÿÿÿ.éÿÿÿ.éÿÿ éÿÿÿéÿ̯ÿéÿÿÿéÿ̯ÿéÿÿÿéÿ̯ÿéÿÿÿéÿ̯ÿéÿÿüéé̯ÿéÿÿýé̯ÿéÿÿþ̯ÿéÿÿÿ¯ÿéÿÿÿ¯ÿéÿÿÿ¯ÿéÿÿÿ¯ÿéÿÿÿ¯ÿéÿÿÿ¯ÿéÿÿÿ¯ÿéÿÿÿ¯ÿéÿÿÿ¯ÿéÿÿÿ¯þ¬éþâËéþËâéÿÿÿ¯ý¬ªéý�#uéúu#�ééÿÿ¯ö¬ªªé·6	:tú:	6·ÿÿ¯ÿ¬ªùé·6	:tú:	6·ÿÿ¯ÿ¬ªÿéý�#uéúu#�ééÿÿ¯ÿ¬ªÿéþâËéþËâéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬	ªÿéÿÿÿ¯ÿ¬
ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬
ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ
¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ
¯ÿ¬ªÿéÿÿÿ	¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿ/ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿÿÿÿÿéÿïÿÿ¯þÌÿÿéÿïÿÿ¯ýÌéÿÿéÿï	ÿÿ¯üÌééÿÿéÿï
ÿÿ¯ÿÌéÿÿÿéÿïÿÿ¯ÿÌéÿÿÿéÿïÿü¯¯Ìéÿÿÿéÿï
ÿý¯ÌéÿÿÿéÿïÿþÌéÿÿÿéÿïÿÿéÿÿÿéÿïÿÿéÿÿÿéÿïÿÿéÿÿÿéÿïÿÿéÿÿÿéÿïÿÿéÿÿÿéÿïÿÿéÿÿÿéÿïÿÿéÿÿÿ
éÿïÿÿéÿÿÿéÿïÿÿéÿÿÿéÿïÿÿéÿÿÿ
éÿïÿÿéÿÿÿ	éÿïÿÿéÿÿÿéÿïÿÿéÿÿÿéÿïÿÿéÿÿÿéÿïÿÿéÿÿ0ÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿ0>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿÿ
ÿÿÿþÿÿÿÿÿþÿÿ
ÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿÿÆÿÑîÿÆÿÿÿÆÿÑîÿÆÿÿÿÆÿÑ	îÿÆÿÿÿÆÿÑ
îÿÆÿÿÿÆÿÑîÿÆÿÿÿÆÿÑîÿÆÿÿÿÆÿÑ
îÿÆÿÿÿÆÿÑîÿÆÿÿÿÆÿÑîÿÆÿÿÿÆÿÑîÿÆÿÿÿÆÿÑîÿÆÿÿÿÆÿÑîÿÆÿÿÿÆÿÑîÿÆÿÿÿÆÿÑîÿÆÿÿÿÆÿÑîÿÆÿÿÿ
ÆÿÑîÿÆÿÿÿÆÿÑîÿÆÿÿÿÆÿÑîÿÆÿÿÿ
ÆÿÑîÿÆÿÿÿ	ÆÿÑîÿÆÿÿÿÆÿÑîÿÆÿÿÿÆÿÑîÿÆÿÿÿÆÿÑîÿÆÿÿ0ÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿ0>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿÿ
ÿÿÿþÿÿÿÿÿþÿÿ
ÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿÿ¯ÿ­ªÿéþÌÿÿ¯ÿ­ªÿéý̯ÿÿ¯ÿ­	ªÿéü̯¯ÿÿ¯ÿ­
ªÿéÿ̯ÿÿÿ¯ÿ­ªÿéÿ̯ÿÿÿ¯ÿ­ªüéé̯ÿÿÿ¯ÿ­
ªýé̯ÿÿÿ¯ÿ­ªþ̯ÿÿÿ¯ÿ­ªÿ¯ÿÿÿ¯ÿ­ªÿ¯ÿÿÿ¯ÿ­ªÿ¯ÿÿÿ¯ÿ­ªÿ¯ÿÿÿ¯ÿ­ªÿ¯ÿÿÿ¯ÿ­ªÿ¯ÿÿÿ¯ÿ­ªÿ¯ÿÿÿ
¯ÿ­ªÿ¯ÿÿÿ¯ÿ­ªÿ¯ÿÿÿ¯ÿ­ªÿ¯ÿÿÿ
¯ÿ­ªÿ¯ÿÿÿ	¯ÿ­ªÿ¯ÿÿÿ¯ÿ­ªÿ¯ÿÿÿ¯ÿ­ªÿ¯ÿÿÿ¯ÿ­ªÿ¯ÿÿ0ÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿ0>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿÿ
ÿÿÿþÿÿÿÿÿþÿÿ
ÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>&éÿíÿÿ4éÿíÿÿ3éÿí	ÿÿ2éÿí
ÿÿ1éÿíÿÿ0éÿíÿÿ/éÿí
ÿÿ.éÿíÿÿ-éÿíÿÿ,éÿíÿÿ+éÿíÿÿ*éÿíÿÿ)éÿíÿÿ(éÿíÿÿ
éÿíéÿíÿÿéþíÿéÿíÿÿéýíÿÿéÿíÿÿ
éÿíÿéÿíÿÿ	éÿíÿéÿíÿÿéÿíÿéÿíÿÿéÿíÿéÿíÿÿéÿíÿéÿíÿÿéÿíÿéÿíÿÿéÿíÿ?/¯ÿÿ/¯ÿÿ/¯ÿÿ?ÿ<ÿ(ÿÿ)ÿÿ)ÿÿÿÿ
ÿÿÿÿÿÿÿÿÿ	ÿÿ	ÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿÿÿÿÿþÿÿÿÿÿ
ÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿ	ÿÿ&ÆÿÍîÿ4ÆÿÍîÿ3ÆÿÍ	îÿ2ÆÿÍ
îÿ1ÆÿÍîÿ0ÆÿÍîÿ/ÆÿÍ
îÿ.ÆÿÍîÿ-ÆÿÍîÿ,ÆÿÍîÿ+ÆÿÍîÿ*ÆÿÍîÿ)ÆÿÍîÿ(ÆÿÍîÿ
ÆÿÍÆÿÍîÿÆþÍîÆÿÍîÿÆýÍîîÆÿÍîÿ
ÆÿÍîÆÿÍîÿ	ÆÿÍîÆÿÍîÿÆÿÍîÆÿÍîÿÆÿÍîÆÿÍîÿÆÿÍîÆÿÍîÿÆÿÍîÆÿÍîÿÆÿÍî?/Æÿÿ/Æÿÿ/Æÿÿ?ÿ<ÿ(ÿÿ)ÿÿ)ÿÿÿÿ
ÿÿÿÿÿÿÿÿÿ	ÿÿ	ÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿÿÿÿÿþÿÿÿÿÿ
ÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿ	ÿÿ&¯ÿ®ªÿ4¯ÿ®ªÿ3¯ÿ®	ªÿ2¯ÿ®
ªÿ1¯ÿ®ªÿ0¯ÿ®ªÿ/¯ÿ®
ªÿ.¯ÿ®ªÿ-¯ÿ®ªÿ,¯ÿ®ªÿ+¯ÿ®ªÿ*¯ÿ®ªÿ)¯ÿ®ªÿ(¯ÿ®ªÿ
¯ÿ®¯ÿ®ªÿ¯þ®ª¯ÿ®ªÿ¯ý®ªª¯ÿ®ªÿ
¯ÿ®ª¯ÿ®ªÿ	¯ÿ®ª¯ÿ®ªÿ¯ÿ®ª¯ÿ®ªÿ¯ÿ®ª¯ÿ®ªÿ¯ÿ®ª¯ÿ®ªÿ¯ÿ®ª¯ÿ®ªÿ¯ÿ®ª?/éÿÿ/éÿÿ/éÿÿ?ÿ<ÿ(ÿÿ)ÿÿ)ÿÿÿÿ
ÿÿÿÿÿÿÿÿÿ	ÿÿ	ÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿÿÿÿÿÿþÿÿÿÿÿ
ÿÿÿÿÿÿÿÿ
ÿÿÿÿÿÿÿ	ÿÿÿ
éÿÿÿ¯áE¯e©¯lS¯w#W1¯¦2<+‹¯^1aUe¯_3éÿÿÿ¯ñ‘œ¯¤‹¯¯Ÿ˜¯¯¡w¤ˆ¯ó–¦¬¯¯¥s€ª¯¯œéÿÿÿ¯üB_1R	¯éÿÿÿ¯ý”z¡
¯éþôÿÿ&¯éÿÿÿ&¯éþíÿÿ&¯éýíÿÿ'éüíÿÿÿÿ&¯éÿíÿÿÿÿ&¯éÿíÿÿÿÿ&¯þéíÿÿÿÿ¯þšW¯ÿíÿÿÿÿ¯þ…¯ÿÿÿÿ&¯ÿÿÿÿ¯þšW¯ÿÿÿÿ¯þ…¯ÿÿÿÿ&¯ÿÿÿÿ¯þšW¯ÿÿÿÿ¯þ…¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿ'ÿÿÿÿ	é#ÿÿÿÿé$ÿÿÿÿéÿÿ'
ÿ	ÿ#ÿÿÿÿ	ÿÿÿÿÿÿ	ÿÿÿÿÿ	ÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿøÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿýÿÿÿÿÿÿÿÿ
ÿÿýÿÿÿÿÿÿÿÿ	ÿÿÆÿÿÿÆáNÆr¿Æz^Ɔ(c7Ƽ9D1!�Æj8n`rÆk9ÆÿÿÿÆñ¤±Æº�ÆÆ³¬ÆÆ¶†ºšÆó©¼ÃÆÆ»‚‘ÀÆÆ±ÆÿÿÿÆüKk7\ÆÿÿÿÆý§Š¶ÆþÚÿÿ-Æþîÿÿ,ÆýÍîÿÿ+ÆüÍîîÿ'ÆûÍîîîÿÿ)ÆÿÍîþîÿÿ(ÆÿÍîþîÿÿ'ÆÿÍîþîÿÿÆþ¯cÆÿÍîþîÿÿÆþ—Æîþîÿÿ&ÆîþîÿÿÆþ¯cÆîþîÿÿÆþ—Æîþîÿÿ&ÆîþîÿÿÆþ¯cÆîþîÿÿÆþ—Æîþîÿÿ&Æîþîÿÿ&Æîþîÿÿ&Æîþîÿÿ&Æÿ'ÿÿÿÿ	ÆîÿÿÿÿÆîÿÿÿÿÆîÿ'
ÿ	ÿ#ÿÿÿÿ	ÿÿÿÿÿÿ	ÿÿÿÿÿ	ÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿøÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿýÿÿÿÿÿÿÿÿ
ÿÿýÿÿÿÿÿÿÿÿ	ÿÿ¯ÿÿÿéá!\é†áé�oéž/tAéÝCO:'¹é}B‚q†é~D¯ÿÿÿéñÁÐéÚ¹ééÓËééÖžÚµéóÇÝ%åééÜš«âééЯÿÿÿéüX~Am	é¯ÿÿÿéýÄ£×
é¯þ¬ÿÿ&é¯þªÿÿ&é¯ý®ªÿÿ&é¯ü®ªªÿ'¯û®ªªªÿÿ&é¯ÿ®ªþªÿÿ&é¯ÿ®ªþªÿÿ&éþ¯®ªþªÿÿéþÎtéÿ®ªþªÿÿéþ±éªþªÿÿ&éªþªÿÿéþÎtéªþªÿÿéþ±éªþªÿÿ&éªþªÿÿéþÎtéªþªÿÿéþ±éªþªÿÿ&éªþªÿÿ&éªþªÿÿ&éªþªÿÿ&éÿ'ÿÿÿÿ	¯ªÿÿÿÿ¯ªÿÿÿÿ¯ªÿ'
ÿ	ÿ#ÿÿÿÿ	ÿÿÿÿÿÿ	ÿÿÿÿÿ	ÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿÿøÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿýÿÿÿÿÿÿÿÿ
ÿÿýÿÿÿÿÿÿÿÿ	ÿÿÿ/ÿ/ÿ.ÿÿó/ÿÿ/ÿÿ/ÿÿ/ÿÿ/ÿÿ/ÿÿ/ÿÿ/ÿÿ/ÿÿ/ÿÿ/ÿÿ/ÿÿ/ÿÿ/ÿÿ/ÿÿ/ÿÿ/ÿÿ/ÿÿ/ÿÿ/ÿ'ÿ
ÿY¯ä£¤¯TZd&Êé9eš°Néé‘\^^ÏééH¶éÿÿ„¯í‹¬¬Š¯�vŠÚééèÀ°EÐéϯûÞéÐ?¶éÿþ¯¡Ÿÿª¯ÿÌéýÊezéýzeÍéÿþ¯UGÿޝÿÌ-éÿ
¯ÿÌ-éÿ	¯ÿÌ-éÿ¯ÿÌ-éÿ?¯þ±Ú#é¯þ³Ü$é¯þ´Þ%é¯þµà&é¯þ·â'é¯þ¹ã(é¯þ»å)é¯þ½æ*é¯þÀç+é¯þÃè,é¯þÆè-é¯ÿÉ/é
¯ÿÍ0é¯ÿÏ1é¯þËáâöãääååæççèè$é?Àÿ?ÿ9ÿ7ÿ7ÿÿÿ7ÿ<ÿ9ÿ9ÿ7ÿÿdÆä ¸ºÆ`fq%¬Æ0Uƒ–BÆÆ{NPP¯ÆÆ=›Æîÿ•Æí�ÃÜƲ††ºÆÆÅ£¥–:±Æ¯”û½Æ±6›Æîþƶ³ÿÁ
Æý¬VgÆýhV®ÆîþÆ`Pÿ¡3Æî9Æî8Æî7Æî?ÀÆ?Àî?ÿ9ÿ7ÿ7ÿÿÿ7ÿ<ÿ9ÿ9ÿ7ÿÿvéä%ÙÚ$épx„&˜¯+Kt„:¯¯mEGG›¯¯6‰¯ªÿ¯éí¹åå¸éÑ�Ф¯¯®�‘„3œ¯›ƒû§¯œ/‰¯ªþé×Óÿãéÿ̯ý˜L[¯ý\Lš¯ªþéq^ÿ½éÿÌ-¯ª
éÿÌ-¯ª	éÿÌ-¯ªéÿÌ-¯ª?éþç¾#¯éþå¼$¯éþäº%¯éþã¸&¯éþá¶'¯éþßµ(¯éþݳ)¯éþÛ²*¯éþر+¯éþÕ°,¯éþÒ°-¯éÿÏ/¯
éÿË0¯éÿÉ1¯éþÍ·¶öµ´´³³²±±°°$¯?Àª?ÿ9ÿ7ÿ7ÿÿÿ7ÿ<ÿ9ÿ9ÿ7ÿÿ
ÿÿÿÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿé
ÿþéÿÿéÿÿ,éþÿÿÿ+éÿÿÿÿéý䯷 éÿÿÿÿéýBs®éþž�éÿÿÿÿéø½¶Öéä±äéóæ²àéæk[¶Øéé¹Ö
éÿÿÿÿéçŠ{À¥1n0©é¹0m2–ãI>{ÄÊ?lN{éÿÿÿÿéèééG¼é²Méb¢éÌ2é‰uéét›éè/éÿÿÿÿéèééæéãé)Ñééâ‰uéé5?HHDéÿÿÿÿéëééD¿éµIé^¤éÏ0é“oéép”éÿÿÿÿéèéé¡0u.¥éµ.t3�éË%oÂÈ7j„Lé	ÿÿÿÿéÿ´éýÞ¤àéýä¤Ùéøà´Öé鳚Íé
ÿÿÿÿ!éÿÿÿÿ éÿÿÿÿé
ÿÿÿÿéÿÿÿÿéÿÿÿÿÿ	¯ÿÌéÿÿÿÿ¯ÿÌéÿÿÿÿ¯ÿÌéÿ	ÿîÿÿîÿÿÿÆîÿÿÿÆîÿÿÿÆîÿÿÿÆîÿÿÿÆ
îþÆÿÿÆÿÿ,Æþîÿÿ+ÆîÿÿÿÆý”› ÆîÿÿÿÆý8b”Æþ†yÆîÿÿÿÆø¡›¶ÆÁ—ÂÆóÄ—¾ÆÄ[N›·ÆÆ�¶
ÆîÿÿÿÆçu	i£�*](�Æ�)\+Á>5i§¬6\BiÆîÿÿÿÆèÆÆ= Æ—AÆS‰Æ­+ÆtcÆÆc„ÆÅ(ÆîÿÿÿÆèÆÆÄÆÁÆ#²ÆÆÀtcÆÆ-6==9ÆîÿÿÿÆëÆÆ9¢Æš>ÆPŒÆ¯(Æ}^ÆÆ`~ÆîÿÿÿÆèÆÆ‰)c'�Æš'c+{Ƭ_¥ª/Zp@Æ	îÿÿÿÆÿ™Æý½‹¾ÆýÁŒ¹Æø¾™¶ÆÆ˜‚®Æ
îÿÿÿ!Æîÿÿÿ ÆîÿÿÿÆ
îÿÿÿÆîÿÿÿÆÿîÿÿÿÆîÿÿÿÆîÿÿÿÆÿ	ÿªÿÿªÿÿÿ¯ªÿÿÿ¯ªÿÿÿ¯ªÿÿÿ¯ªÿÿÿ¯
ªþ¯ÿÿ¯ÿÿ,¯þªÿÿ+¯ªÿÿÿ¯ý¬ƒ‰ ¯ªÿÿÿ¯ý1V‚¯þwk¯ªÿÿÿ¯øŽ‰¡¯«…¬¯ó­†¨¯­PE‰¢¯¯‹¡
¯ªÿÿÿ¯çh]�|%R$¯‹$R&qª7/]”˜/Q:]¯ªÿÿÿ¯è¯¯6�¯†:¯Iy¯™&¯gX¯¯Wu¯®#¯ªÿÿÿ¯è¯¯­¯ª¯�¯¯ªgX¯¯(/663¯ªÿÿÿ¯ë¯¯3�¯ˆ7¯G|¯›$¯nS¯¯To¯ªÿÿÿ¯è¯¯y$X"|¯ˆ"W&l¯˜T‘–)Pc9¯	ªÿÿÿ¯ÿ‡¯ý§{¨¯ý«|£¯ø¨‡¡¯¯‡sš¯
ªÿÿÿ!¯ªÿÿÿ ¯ªÿÿÿ¯
ªÿÿÿ¯ªÿÿÿ¯ÿªÿÿÿ	éÿ̯ªÿÿÿéÿ̯ªÿÿÿéÿ̯ÿ	ÿÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿ
?
éþ¯åéþ¯Ûéû̾é¶ÞéþÓ?
éúp¼éédÊéû“ké˜{	éü×Î̬éþi�éëÈÇééç½Ø«äéÙ=‰¶äÄáééÓ»éî¼á®×éé嶨âéé“kéä+àéü”,J…éÑ$ÙééIRQGéà>l-»Ç*]{à=ÁéhD`5ÛéKk)®éškz2²é“kééO¯éýå”0éÝãééÀDééA¾àÊéšféO¯éé=ÁÞ%ééf™é±éŸaéöÉ�aé“kééo‹éýË”eéÐÜéé‡séés…àééÊ0éO¯éé=Á­Néé™`éâé¼Bé¨?bc3é“kéévƒéþ”jéÑßé¼FééDºàÍé�déY©éé=ÁÜ&ééi•éãé¼Bé@¿é’2é“kééV§éþ”jéÑT°ééBYX?éà?s*¸é•9tß=ÁéaHg/Úéãé¼BérKs?2é“kéé.×éþÔÊéþÁOé÷¼»ééàÒžßé÷Ö¯ä¾ßééʯéï¶èéÞÀé賤ڻéÔÊé®béþ…åéþà éþ›Ïéþæ¤ìé?2éÿôÿ1éÿôÿ0éÿô
ÿ?	ÿ?
Æþ”ÃÆþ”ºÆû­¢Æ›½Æþ³6
Æú` ÆÆU¬Æû}[Æ�i	Æü¶¯­’ÆþY†Æëª©ÆÆÄ¡·‘Áƹ4t›Â¦¿ÆÆ³ŸÆî ¿”¶ÆÆÃ›�ÀÆÆ}[ÆÂ$¾Æü~%?qÆÑ¸ÆÆ>FE=ƾ5\&Ÿ©$Oi¾4¤ÆY9R-ºÆ@[#”Æ‚[g+—Æ}[ÆÆC•ÆýÃ~)ÆÝÁÆÆ£9ÆÆ7¢¾¬ÆƒWÆC•ÆÆ4¤½ ÆÆW‚Æ—ƇRÆö«xRÆ}[ÆÆ^vÆý¬~VÆÐ»ÆÆsbÆÆbq¾
ÆÆ¬)ÆC•ÆÆ4¤“BÆÆ‚RÆÀÆ 8Æ�6ST+Æ}[ÆÆdoÆþ~ZÆÑ½Æ <ÆÆ9ž¾®Æ†UÆK�ÆÆ4¤»!ÆÆYÆÁÆ 8Æ6¢Æ|+Æ}[ÆÆIŽÆþ~ZÆÑG–ÆÆ8KK6ƾ6b$œÆ0c½4¤ÆR=X(ºÆÁÆ 8Æa@b6+Æ}[ÆÆ'¶Æþ´¬Æþ¤DÆ÷ ŸÆÆ¾³†½Æ÷¶•¢½ÆÆ¬”Æï›Åƽ£ÆÅ˜ŒºŸÆ´¬Æ”SÆþqÃÆþ¾ Æþ„°ÆþÄ‹ìÆ?2ÆÿÚî1ÆÿÚî0ÆÿÚ
î?	ÿ?
¯þƒ¬¯þƒ¥¯û™�¯‰§¯þŸ/
¯úT�¯¯K˜¯ûnP¯r]	¯ü¡š™�¯þOv¯–¯ï®Ž¢€«¯£.g‰¬“©¯¯Ÿ�¯î�©‚¡¯¯¬‰~ª¯¯nP¯¬ ¨¯üo!8d¯Ñ£¯¯7>=6¯¨/Q"�– F]¨.‘¯N3H(¥¯8P‚¯sP[&†¯nP¯¯;„¯ý¬o$¯Ýª¯¯�3¯¯1�¨˜¯tM¯;„¯¯.‘§¯¯Ms¯…¯wI¯ö—jI¯nP¯¯Sh¯ý˜oL¯Ð¥¯¯fV¯¯Vd¨¯¯˜$¯;„¯¯.‘‚:¯¯sH¯ª¯�1¯~/IJ&¯nP¯¯Yb¯þoP¯Ñ§¯�5¯¯3Œ¨š¯vK¯C¯¯.‘¥¯¯Op¯ª¯�1¯0�¯n&¯nP¯¯A~¯þoP¯Ñ?„¯¯1CB/¯¨/V Нp+W§.‘¯I6N#¤¯ª¯�1¯V8V/&¯nP¯¯"¡¯þŸ˜¯þ‘<¯�¯û¨žw§¯÷¡„¬�§¯¯˜ƒ¯ï‰®¯§�¯®‡|¤�¯Ÿ˜¯‚I¯þd¬¯þ¨ ¯þuœ¯þ­{ì¯?2¯ÿ¬ª1¯ÿ¬ª0¯ÿ¬
ª?	ÿÿ
ÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿÿÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿ	ÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÿîÿÿÿÆîÿÿÿÆîÿÿÿÆÿ	ÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿÿªÿÿÿéªÿÿÿéªÿÿÿéÿ	ÿÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿ
?#ÿþü˜=ÿýƒÜ;ÿüî%F;ÿûW´9ÿúþ�k³;ÿþ×'=ÿþ×'=ÿþ×'=ÿþ×';ÿúþ�k³:ÿûW´:ÿüî%F<ÿýƒÜ<ÿþü˜ÿ?À¯?	ÿ?#ÿþü˜=ÿýƒÜ;ÿüî%F;ÿûW´9ÿúþ�k³;ÿþ×'=ÿþ×'=ÿþ×'=ÿþ×';ÿúþ�k³:ÿûW´:ÿüî%F<ÿýƒÜ<ÿþü˜ÿ?ÀÆ?	ÿ?#ÿþü˜=ÿýƒÜ;ÿüî%F;ÿûW´9ÿúþ�k³;ÿþ×'=ÿþ×'=ÿþ×'=ÿþ×';ÿúþ�k³:ÿûW´:ÿüî%F<ÿýƒÜ<ÿþü˜ÿ?Àé?	ÿÿ
ÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿ ÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿÿ	ÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿ ÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÿ	ÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿ ÿéÿÿÿÿéÿÿÿÿéÿÿÿÿÿ	ÿÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿ
*éÿ>éÿ>éÿ>éÿ"éü­Þé¡éýâ™™éùåäéé²™Ìéÿ"éü8ÇééýÔ™éøä,Aéé²F“éÿéêÁ©ßééÞ²çéé8Çéééפ¯ãÔ+éýtfAéþl“éÿéèÑ7\v�éƒGr:Çé8Çéé¶1€m¹Ô+éüÀQ¯Aéþl“éÿéþ�€éææ.åé¡sé8Çéé‚oåééÔ+ééæ>Ó¯Aéþl“éÿéþD¶éá»HHGjé8ÇééÙb _ØÔ+éé‰W§~/¶éél“éÿéþ}ƒéýä%äéü8Çééïåh�Ô+éé¨mmRˆéél“éÿéèÏ0c‚˜é~B‚r…é8Çéé{t†0»Ô+éþ¯Aéþl“éÿé÷¤=éÏééÑ
é鯜ÚééÜš«âé½áé´éÕ ›×éÔ%ÌéùÛ¿ééÕ^“éÿé÷Ù¾éã´éé³äéýÝeeéý�e¹éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿé?Àÿ?	ÿ*Æÿ>Æÿ>Æÿ>Æÿ"Æü“½Æ‰ÆýÀ‚‚ÆùÃÂÆÆ—‚­Æÿ"Æü/©ÆÆý´‚ÆøÂ%7ÆÆ—<}ÆÿÆê¤�½ÆÆ½—ÄÆÆ/©ÆÆÆ¶‹”Á´$ÆýcW7Æþ\}ÆÿÆè²/Nd{Æo=a2©Æ/©ÆÆ›*m\�´$Æü£E•7Æþ\}ÆÿÆmÆæÄ'ÃÆ‰bÆ/©ÆÆn^ÃÆÆ´$ÆÆÄ5³•7Æþ\}ÆÿÆþ9›ÆþŸ=æZÆ/©ÆƸSQ·´$ÆÆtJŽk(›ÆÆ\}ÆÿÆþjoÆýÂÂÆü/©ÆÆïÃYm´$ÆÆ�\\FtÆÆ\}ÆÿÆè¯(Tn�Æk8naqÆ/©ÆÆicr)Ÿ´$Æþ•7Æþ\}ÆÿÆ÷Œ4ưÆÆ²
Æé•…ºÆÆ»‚‘ÀÆ¡¿Æ™Æµˆ„¶Æ´ ­Æùº¢ÆÆµP}ÆÿÆ÷¸¢ÆÁ™ÆÆ˜ÁÆý¼VVÆýyV�Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>ÆÿÆ?Àÿ?	ÿ*¯ÿ>¯ÿ>¯ÿ>¯ÿ"¯ü‚§¯y¯ýªss¯¬¯ý†s™¯ÿ"¯ü*–¯¯ýŸs¯ø¬!1¯¯†5n¯ÿ¯ê‘§¯¯§†®¯¯*–¯¯¯¡{ƒªŸ ¯ýWM1¯þQn¯ÿ¯è�)EYl¯b6V,–¯*–¯¯‰%`R‹Ÿ ¯ü�=„1¯þQn¯ÿ¯þa`¯æ­"¬¯yV¯*–¯¯aS¬¯¯Ÿ ¯¯­/Ÿ„1¯þQn¯ÿ¯þ3‰¯þ�6æP¯*–¯¯£IG¢Ÿ ¯¯gA~_#‰¯¯Qn¯ÿ¯þ^b¯ý¬¬¯ü*–¯¯ï¬NaŸ ¯¯~RR>f¯¯Qn¯ÿ¯è›$Jar¯_1aVd¯*–¯¯]We$�Ÿ ¯þ„1¯þQn¯ÿ¯÷|.¯œ¯¯�
¯é„u¤¯¯¥s€ª¯Ž©¯‡¯ xu¡¯Ÿ™¯ù¥�¯¯ Gn¯ÿ¯÷£�¯ª‡¯¯‡«¯ý¦LL¯ýkL‹¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ¯?Àÿ?	ÿÿ
éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯'ÿ&ÿÿ>ÿÿ>ÿÿÿ'	ÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆ'ÿ&ÿÿ>ÿÿ>ÿÿÿ'	ÿ¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé'ÿ&ÿÿ>ÿÿ>ÿÿÿ'	ÿ'ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿÿ
¯ÿ¯ÿÌé¯ÿ¯ÿÌ é¯ÿ¯ÿÌ!é¯ÿ¯ÿÌ"é¯ÿ¯ÿÌ#é¯ÿ¯ÿÌ$é¯ÿ¯ÿÌ%é¯7~¯ÿ­>¯ÿY=¯þ£=¯þ<<¯ý®aI>¯ÿ”>¯ÿ”>¯ÿ”>¯ÿ”<¯ý®aI=¯þ<=¯þ£>¯ÿY>¯ÿ­?ÿÿ=ÿ¯=ÿÿ¯lÿÆÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æ7~ÆÿÄ>Æÿe=Æþ¹=ÆþD<ÆýÅmS>Æÿ§>Æÿ§>Æÿ§>Æÿ§<ÆýÅmS=ÆþD=Æþ¹>Æÿe>ÆÿÄ?ÿÿ=ÿÆ=ÿÿÆlÿéÿéÿ̯éÿéÿÌ ¯éÿéÿÌ!¯éÿéÿÌ"¯éÿéÿÌ#¯éÿéÿÌ$¯éÿéÿÌ%¯é7~éÿæ>éÿw=éþÙ"=éþO<éýè�b>éÿÄ>éÿÄ>éÿÄ>éÿÄ<éýè�b=éþO=éþÙ">éÿw>éÿæ?ÿÿ=ÿé=ÿÿélÿÿÿ=þ=ÿléÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿüééôÿÿ¯ÿÿýéôÿÿ¯ÿÿ ¯ÿÿÿ.¯ÿÿþh-¯ÿÿý—,¯ÿÿÿ0,¯ÿÿþ
|+¯ÿÿüW{+¯ÿÿþ-¯ÿÿþ-¯ÿÿþ-¯ÿÿþ-¯ÿÿüW{+¯ÿÿþ
|+¯ÿÿÿ0,¯ÿÿý—,¯ÿÿþh-¯ÿÿ0ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿüÆÆÚîÿÆÿÿýÆÚîÿÆÿÿ Æÿÿÿ.Æÿÿþv-Æÿÿý«,Æÿÿÿ6,ÆÿÿþŒ+Æÿÿüc‹+Æÿÿþ-Æÿÿþ-Æÿÿþ-Æÿÿþ-Æÿÿüc‹+ÆÿÿþŒ+Æÿÿÿ6,Æÿÿý«,Æÿÿþv-Æÿÿ0ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿü¯¯¬ªÿéÿÿý¯¬ªÿéÿÿ éÿÿÿ.éÿÿþ‹-éÿÿýÉ,éÿÿÿ@,éÿÿþ
¤+éÿÿüt¤+éÿÿþ$-éÿÿþ$-éÿÿþ$-éÿÿþ$-éÿÿüt¤+éÿÿþ
¤+éÿÿÿ@,éÿÿýÉ,éÿÿþ‹-éÿÿ0ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿ/ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿþÿÿÿÿÿÿþÿÿÿÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ0ÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿþÿÿÿÿÿÿþÿÿÿÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ0ÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿýÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ
ÿÿÿÿþÿÿÿÿÿÿþÿÿÿÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿ0ÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ/ÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿ
ÿ
ÿÿÿÿÿ
ÿÿÿÙÿ¯ÿ?/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿ
ÿ
ÿÿÿÿÿ
ÿÿÿÙÿÆÿ?/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿ
ÿ
ÿÿÿÿÿ
ÿÿÿÙÿéÿ?/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿÿÿÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿ	ÿÿÿÿÿÿÿÿ
ÿÿ+ÿÿÿ6ÿ7ÿ:ÿÛÿ¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ'¯ÿÿ'ÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿ	ÿÿÿÿÿÿÿÿ
ÿÿ+ÿÿÿ6ÿ7ÿ:ÿÛÿÆÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ'Æÿÿ'ÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿÿÿÿ	ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ	ÿ	ÿÿÿÿÿÿÿÿ
ÿÿ+ÿÿÿ6ÿ7ÿ:ÿÛÿéÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ'éÿÿ'ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ'ÿÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ'ÿÿ6ÿÿÿ7ÿ7ÿ8ÿúÿ¯ÿ?5éÿÌÿ4é
ÿ3éÿ2éÿ1é
ÿ0éÿ/éÿ.éÿ-éÿ,éÿ+éÿÿ6ÿÿÿ7ÿ7ÿ8ÿúÿÆÿ?6Æî4Æ
î3Æî2Æî1Æ
î0Æî/Æî.Æî-Æî,Æî+Æîÿ6ÿÿÿ7ÿ7ÿ8ÿúÿéÿ?5¯ÿ̪4¯
ª3¯ª2¯ª1¯
ª0¯ª/¯ª.¯ª-¯ª,¯ª+¯ªÿÿÿ¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯/ÿ¯/ÿ¯/ÿ¯0ÿÿÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéþ´léýç:éÿÿÿÿéþªUéýæéÿÿÿÿéçªOe<–ééÓxC�äé�¯‚f¯þœ-
¯úc]¯¯–œ¯ú–)¯¬$ª¯ö‡AA‰¯žNY&¦¯ÑVk¯¯˜O6v®¯Im-S¡¤6‰o~¯ [1jª¯]o3>”¯œ:+M–¯–)¯¯Rn¯ÿ¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ¯ÿ®'¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ/¯ÿ¯ÿÿéÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿéþÚ¯
éÿâéÿÿÿéþ¾@
éÿÒéÿÿÿéû¾@ϯçéýÜ´èéýجËÿÆÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/ÆÿÆÿÅ'Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/ÆÿÆÿÿÆÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-ÆÿÿÿÆþº•
ÆÿÀÆÿÿÿÆþ¢6
Æÿ³ÆÿÿÿÆû¢6°”ÄÆý»™ÅÆý·’¬ÿéÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿ/éÿéÿç'éÿ/éÿ/éÿ/éÿ/éÿ/éÿéÿÿ¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ¯þ¤„
¯ÿª¯ÿÿÿ¯þ�0
¯ÿž¯ÿÿÿ¯û�0œƒ®¯ý¥‡®¯ý¢�˜ÿ¯ÿ?éÿ¨éýÀ™»éùÎâééà™ž*éÿ,éóo^»éß9+¥éà™*éë,éºèééÇÚo�éé„Sè¸èéé*éÿÆÿ?Æÿ�Æý£‚ŸÆù¯ÀÆÆ¾‚†*Æÿ%Æó_PŸÆ½0$
�ƾ‚*Æë%ÆžÅÆÆ©º_yÆÆpGÅœÅÆÆ*Æÿéÿ?¯ÿ~¯ý�s�¯ùšª¯¯¨sw*¯ÿ!¯óTG�¯§+ |¯¨s*¯ë!¯Œ®¯¯–¤Tk¯¯c>®Š®¯¯*¯ÿ¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿé¯ÿéÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿÆÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/Æÿ/ÆÿÆÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿéÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯éÿ¯ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ7éÿ6éÿ5é	ÿ4é
ÿ3éÿ2éÿ1é
ÿ0éÿ/éÿ.éÿ-éÿ,éÿ+éÿ*éÿ)éÿ(éÿ'éÿ&éÿ%éÿ$éÿ#éÿ"éÿ!éÿ éÿ?*éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ"éü­Þé¡éýâ™™éþÞÑéý²™Ìéÿéþ­Þéü8ÇééòÔ™éé�%Oæé²F“éÿéý8Çéÿ7Æî6Æî5Æ	î4Æ
î3Æî2Æî1Æ
î0Æî/Æî.Æî-Æî,Æî+Æî*Æî)Æî(Æî'Æî&Æî%Æî$Æî#Æî"Æî!Æî Æîÿ?*Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ"Æü“½Æ‰ÆýÀ‚‚Æþ½²Æý—‚­ÆÿÆþ“½Æü/©ÆÆò´‚ÆÆyCÄÆ—<}ÆÿÆý/©Æÿ7¯ª6¯ª5¯	ª4¯
ª3¯ª2¯ª1¯
ª0¯ª/¯ª.¯ª-¯ª,¯ª+¯ª*¯ª)¯ª(¯ª'¯ª&¯ª%¯ª$¯ª#¯ª"¯ª!¯ª ¯ªÿ?*¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ"¯ü‚§¯y¯ýªss¯þ§�¯ý†s™¯ÿ¯þ‚§¯ü*–¯¯òŸs¯¯k;­¯†5n¯ÿ¯ý*–¯8ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯7ÿ¯ÿ'ÿéÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯ÿ¡éýâ™™éþçäéý²™Ìéÿ
ÿÿÿÿ¯ÿéøÔ™éé?2éý²F“éÿ
ÿÿÿÿ¯ÿ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆ'îÿÆÿ'ÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ‰ÆýÀ‚‚ÆþÄÁÆý—‚­Æÿ
ÿÿÿÿÆÿÆø´‚ÆÆ6+Æý—<}Æÿ
ÿÿÿÿÆÿ'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿé'ªÿéÿ'ÿ¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿéÿy¯ýªss¯þ®«¯ý†s™¯ÿ
ÿÿÿÿéÿ¯øŸs¯¯/&¯ý†5n¯ÿ
ÿÿÿÿéÿ/¯é.¯é-¯é,¯é+¯é*¯é)¯é(¯é'¯é&¯é%¯é$¯é#¯é"¯é!¯é ¯é¯é¯ é¯!é¯"é¯#é¯$é¯%é¯&éÿ??¯ÿ­¯ÿ%¯ÿY¯ÿ#¯û£¯k•	¯ú®¬¯¯®¬¯ÿ#¯û<¯<ƒ	¯ú“-¯¯†:¯ÿ"¯Ù®aI¯a¯ø/�¡Y¯(œ¯”ÿÆÿ??ÆÿÄÆî%ÆÿeÆî#Æû¹Æy¨	ÆúÅÂÆÆÅÂÆî#ÆûDÆD”	Æú¦2ÆÆ—AÆî"ÆÙÅmSÆD…f€ÄÆÆ·n…ÃÆii�Z"i—ƶîîÄÆ±‡†]žµh€ÂÆøsÅÆt«Æ‘ªÆÛ§ÆD(†]=Ʋƒf.Ɖ(›­{2›°¬ †b4Æ™
f‚I‡FnÆø6£¶eÆ.±Ƨÿ/é¯.é¯-é¯,é¯+é¯*é¯)é¯(é¯'é¯&é¯%é¯$é¯#é¯"é¯!é¯ é¯é¯é ¯é!¯é"¯é#¯é$¯é%¯é&¯ÿ??éÿæéª%éÿwéª#éûÙ"é�Æ	éúèäééèäéª#éûOéP¯	éúÄ;éé²Méª"éÙè�béPœx—æé騂œåé{{©j({±éÖªªæéПžn¹Õz—äéø‡èéˆÉé«ÈéÛÄéP/žnHéÑ#šy6é¢/¶Ì�;¶ÏÊ%�s=é´x™VŸR‚éø?ÀÖwé6ÐéÄ—þ=ý<ý<ý
<þf>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿéÿíÿÿéÿíÿÿéÿí ÿÿéÿí!ÿÿéÿí"ÿÿéÿí#ÿÿéÿí$ÿÿéÿí%ÿÿéÿí&ÿÿéÿí'ÿÿéÿí(ÿÿéÿí)ÿÿéÿí*ÿÿéÿí+ÿÿéÿí,ÿÿéÿí-ÿÿéÿí.ÿÿ
éÿí/ÿÿéÿí0ÿÿéÿí1ÿÿ
éÿí2ÿÿ	éÿí3ÿÿéÿí4ÿÿéÿí5ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿ0ÿÿ.¯ÿÿÿ.¯ÿÿÿ.¯ÿÿÿ.¯ÿÿþh-¯ÿÿý—,¯ÿÿÿ0,¯ÿÿþ
|+¯ÿÿüW{+¯ÿÿþ-¯ÿÿþ-¯ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿÆÿÍîÿÿ
ÆÿÍ îÿÿÆÿÍ!îÿÿÆÿÍ"îÿÿ
ÆÿÍ#îÿÿ	ÆÿÍ$îÿÿÆÿÍ%îÿÿÆÿÍ&îÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿ0ÿÿ.Æÿÿÿ.Æÿÿÿ.Æÿÿÿ.Æÿÿþv-Æÿÿý«,Æÿÿÿ6,ÆÿÿþŒ+Æÿÿüc‹+Æÿÿþ-Æÿÿþ-Æÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ¯ÿ®ªÿÿ
¯ÿ® ªÿÿ¯ÿ®!ªÿÿ¯ÿ®"ªÿÿ
¯ÿ®#ªÿÿ	¯ÿ®$ªÿÿ¯ÿ®%ªÿÿ¯ÿ®&ªÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿ0ÿÿ.éÿÿÿ.éÿÿÿ.éÿÿÿ.éÿÿþ‹-éÿÿýÉ,éÿÿÿ@,éÿÿþ
¤+éÿÿüt¤+éÿÿþ$-éÿÿþ$-éÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ/ÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿÿÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿ&éÿÿÿÿôÿÿ%éÿÿ
ÿÿôÿÿ$éÿÿ	ÿÿôÿÿ#é	ÿÿÿÿôÿÿ"é
ÿÿÿÿôÿÿ!éÿÿÿÿôÿÿ éÿÿÿÿôÿÿé
ÿÿÿÿô	ÿÿéÿÿÿÿô
ÿÿéÿÿÿÿôÿÿéÿüÿÿôÿÿéÿýÿô
ÿÿéÿþôÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&Æÿÿÿÿ&ÆÿÿÿÿÚÿÿ%Æÿîÿÿ
ÿþÚîÿÿ$Æîÿÿ	ÿýÚîîÿÿ#ÆîÿÿÿÿÚîÿÿ"ÆîÿÿÿÿÚîÿÿ!ÆîÿÿÿÿÚîÿÿ ÆîÿÿÿÿÚîÿÿÆîÿÿÿÿÚîÿÿÆîÿÿÿÿÚîÿÿÆîÿÿÿÿÚîÿÿÆ	îÿüÿÿÚ	îÿÿÆ
îÿýÿÚ
îÿÿÆîÿþÚîÿÿÆîÿÿîÿÿÆ
îÿÿîÿÿÆîÿÿîÿÿÆîÿÿîÿÿÆîÿÿîÿÿÆîÿÿîÿÿÆîÿÿîÿÿÆîÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ&¯ÿÿÿÿ¬ÿÿ%¯ÿªÿÿ
ÿþ¬ªÿÿ$¯ªÿÿ	ÿý¬ªªÿÿ#¯ªÿÿÿÿ¬ªÿÿ"¯ªÿÿÿÿ¬ªÿÿ!¯ªÿÿÿÿ¬ªÿÿ ¯ªÿÿÿÿ¬ªÿÿ¯ªÿÿÿÿ¬ªÿÿ¯ªÿÿÿÿ¬ªÿÿ¯ªÿÿÿÿ¬ªÿÿ¯	ªÿüÿÿ¬	ªÿÿ¯
ªÿýÿ¬
ªÿÿ¯ªÿþ¬ªÿÿ¯ªÿÿªÿÿ¯
ªÿÿªÿÿ¯ªÿÿªÿÿ¯ªÿÿªÿÿ¯ªÿÿªÿÿ¯ªÿÿªÿÿ¯ªÿÿªÿÿ¯ªÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ'ÿ*éÿ)éÿ(éÿ'éÿ&éÿ%éÿ$éÿ#éÿ"éÿ!éÿ éÿéÿé ÿé!ÿé"ÿé#ÿé$ÿé%ÿé&ÿé'ÿé(ÿé)ÿé*ÿé+ÿé,ÿé-ÿé.ÿé/ÿé0ÿ
é1ÿé2ÿé3ÿ
é4ÿ	é5ÿé6ÿé7ÿé8ÿé9ÿé:ÿé;ÿé<ÿé=ÿÿéÿ*Æî)Æî(Æî'Æî&Æî%Æî$Æî#Æî"Æî!Æî ÆîÆîÆ îÆ!îÆ"îÆ#îÆ$îÆ%îÆ&îÆ'îÆ(îÆ)îÆ*îÆ+îÆ,îÆ-îÆ.îÆ/îÆ0î
Æ1îÆ2îÆ3î
Æ4î	Æ5îÆ6îÆ7îÆ8îÆ9îÆ:îÆ;îÆ<îÆ=îÿÆî*¯ª)¯ª(¯ª'¯ª&¯ª%¯ª$¯ª#¯ª"¯ª!¯ª ¯ª¯ª¯ ª¯!ª¯"ª¯#ª¯$ª¯%ª¯&ª¯'ª¯(ª¯)ª¯*ª¯+ª¯,ª¯-ª¯.ª¯/ª¯0ª
¯1ª¯2ª¯3ª
¯4ª	¯5ª¯6ª¯7ª¯8ª¯9ª¯:ª¯;ª¯<ª¯=ªÿ¯ªÿÿÿÿÿéçª
¶Õ$Úé>©ãf—éÄæÝE¾éivÞeéÿÿÿÿéçªEééZ¤Õ{{cHéÉ}M,zé)ÙéÆéÿÿÿÿéøªUééažÏ$¶òÁé6®Þ|ké&ÛéÉéÿÿÿÿéçªUééažé;¥çãÆåÚÑ-kéf{âjéÿÿÿÿéç¹zé郱éÎr4:žé—5A‡‹éØl0ŠMéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯þ™�
¯þŸŠ¯ÿÿÿÿ¯þnQ
¯þ~B¯ÿÿÿÿ¯ünQ”Нþ�ޝú˜‡|B«�¯ÿ�ÿÿÿÿ¯èn 8B/®¯J5F)¨¯T.I&B®Ÿ¯œÿÿÿÿ¯èn"¯¯:‡¤¯¯Gx¬®¯NB¯_\¯Z]ÿÿÿÿ¯ènE¯¯]^}?¯¯nN�1¯¯rB¯¢­ ÿÿÿÿ¯èn$¯¯<„¢ ¯¯Iu«¯¯PB¯¯>N<¯ÿÿÿÿ¯ènçécF]7àép=b3Xè&ÔéÐ#ªÿÿÿéè“-ééM´Ú)éé_ å!èéhXézéx|ªÿÿÿéè“\éé|}¦Téé’g¿Aéé˜XéØ%æ#Õªÿÿÿéè“0ééO°Ø+éébœä!éékXééShOéªÿÿÿéè“%P_7æé[Ke0ÞéiCi/Xéé³®éªÿÿÿéüÔÊ»«éþȰéöÀ©ÖÅééÍ%ééªÿÿÿéû£: ééªÿÿÿéþ²¼éªÿÿÿéªÿÿÿéªÿÿÿéªÿÿÿéªÿÿÿéªÿÿÿéªÿÿÿéªÿÿÿéªÿÿÿéªÿÿÿéªÿÿÿéªÿÿÿéªÿÿªÿÿÿ¯ªÿÿÿ¯ªÿÿÿ¯ªÿÿÿ¯ªÿÿÿ¯ªÿÿÿ¯ªÿÿÿ¯ªÿ
ªÿÿ¯ªÿ
ªÿÿ¯ý¬ƒ‰¯ªÿ
ªÿÿ¯ý1V‚¯þwk¯ªÿ
ªÿÿ¯øŽ‰¡¯«…¬¯ò­†¨¯­PE‰¢¯¯‹¡¯ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/õÏ0ÏÐ/áÉšäçéÑE´éé0«Ö<®é+WÜ…Ué„{éér�éT‰ÝXˆéPDÕ›OéÙÑé¦QéÈ7éé“iéø‚E{{5žÉéÑ#ÐéÏ/éé–eé+´éäç„{éér�ååé»@éPžéåéç™^HéÈ7éé¯Jéþze¶ýÔÉ6éÑ:»éÌ1ééšbé+·éäå„{éér�äæé¿<éP¯éééžWÎàèÈ7ééš`éøÎ/ÍéÏÙÉ6éÑešéé/²Ù@«é+Zá�Ré¦Yåér�éQ�á]…éP¯ééézzæ„èÈ7ééy†éù­R3JÑÑcéѽMééÄ^=“æé+„2cÒéào>²�¤éÑn7„àéw½ééJéÔP1~LèÐdéé5ÕéþIÍéþ+Ôéþ¯féþãäéþIØéþãåé?À¯ÿ«>¯ÿ®ÿ¯?
éþ¯åéþ¯Ûéû̾é¶ÞéþÓ?
éúp¼éédÊéû“ké˜{	éü×Î̬éþi�éëÈÇééç½Ø«äéÙ=‰¶äÄáééÓ»éî¼á®×éé嶨âéé“kéä+àéõ°)°±(¿«‚ÂÄÆÑ:™ÆÆ)‘¶3”Æ$J»qHÆpiÆÆaxÆGt¼KtÆD9µ„CƸ²Æ�Eƪ/ÆÆ}YÆøn:ii-†«ÆÑ±Æ°(ÆÆVÆ$™ÆÁÄpiÆÆaxÃÃÆŸ6ÆD†ÆÃÆÄ‚P=ƪ/ÆÆ•?ÆþhU›ý´«.ÆÑ2ŸÆ­*ÆÆ‚SÆ$›ÆÂÃpiÆÆaxÂÄÆ¢3ÆD”ÆÆƆJ¯¾Ū/ÆÆƒRÆø¯(®Æ°¹«.ÆÑU‚ÆÆ(—¹6‘Æ$M¿xFÆ�KÃÆaxÆEz¿OqÆD”ÆÆÆhgÄpŪ/ÆÆfrÆù“F+?²²TÆÑ¡AÆÆ§P4}ÄÆ$p+T³Æ¾_5—{‹Æ²]/p¾Æe¡ÆÆ?Æ´D*k@űUÆÆ-µÆþ>®Æþ$´Æþ•WÆþÁÂÆþ>·ÆþÁÃÆ?ÀÆÿÁ>ÆÿÅÿÆ?
Æþ”ÃÆþ”ºÆû­¢Æ›½Æþ³6
Æú` ÆÆU¬Æû}[Æ�i	Æü¶¯­’ÆþY†Æëª©ÆÆÄ¡·‘Áƹ4t›Â¦¿ÆÆ³ŸÆî ¿”¶ÆÆÃ›�ÀÆÆ}[ÆÂ$¾Æõœ$œœ#©—s¬®¯Ñ3‡¯¯$€¡-‚¯ A¥d@¯c]¯¯Vj¯?g¦Bf¯<3 u;¯£�¯}=¯–)¯¯nO¯øa3]](w—¯Ñœ¯œ#¯¯qL¯ ‡¯«®c]¯¯Vj¬¬¯�0¯¯}C¬¯Vj¯=l©Fd¯<ƒ¯¯¯\[­c®–)¯¯[e¯ù‚>&8��J¯ÑŽ:¯¯”G.n­¯ c&Jž¯¨T/†l{¯�R)c¨¯Yޝ¯8¯Ÿ<%_9®œK¯¯( ¯þ7š¯þ Ÿ¯þ„M¯þª¬¯þ7¢¯þª¬¯?Àéÿä>éÿèÿé?
¯þƒ¬¯þƒ¥¯û™�¯‰§¯þŸ/
¯úT�¯¯K˜¯ûnP¯r]	¯ü¡š™�¯þOv¯–¯ï®Ž¢€«¯£.g‰¬“©¯¯Ÿ�¯î�©‚¡¯¯¬‰~ª¯¯nP¯¬ ¨¯éÿÿÿéï¾?f0Ñé‡8j0Äé”1k=éÿÿÿéï¾ãéyˆé%ßé‹té5Ðé”éÿÿÿéï¾1éé¨RÑ)éé½;æééÄéÿÿÿéï¾
äé{…é#àéŽpé2Òé—éÿÿÿéï¾Cn,Ïé�:q,Âé�2r:éÿÿÿéû߿ɡäéýÕ¦åéýРÙéÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿ-éÿÿÿéÿ¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿéþÚ¯
éÿâ¯ÿÿÿéþ¾@
éÿÒ¯ÿÿÿéû¾@ϯçéýÜ´èéýج˯ÿÿÿéï¾?f0Ñé‡8j0Äé”1k=¯ÿÿÿéï¾ãéyˆé%ßé‹té5Ð锯ÿÿÿéï¾1éé¨RÑ)éé½;æééįÿÿÿéï¾
äé{…é#àéŽpé2Òé—¯ÿÿÿéï¾Cn,Ïé�:q,Âé�2r:¯ÿÿÿéû߿ɡäéýÕ¦åéýРÙ¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿ¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿé¯ÿÿÿéÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿÆÿÿÿÆï¢6W(²Æs/Z(§Æ~*[4ÆÿÿÿÆï¢ÁÆftƽÆvcÆ-±Æ~ÆÿÿÿÆï¢*ÆÆ�F²#ÆÆ¡2ÄÆÆ¦ÆÿÿÿÆï¢	ÁÆiqƾÆx`Æ+³Æ€ÆÿÿÿÆï¢9]%¯Æm2`%¥Æy+a2ÆÿÿÿÆû½¢«‰ÂÆýµ�ÃÆý±ˆ¹Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-ÆÿÿÿÆÿÆÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-ÆÿÿÿÆþº•
ÆÿÀÆÿÿÿÆþ¢6
Æÿ³ÆÿÿÿÆû¢6°”ÄÆý»™ÅÆý·’¬ÆÿÿÿÆï¢6W(²Æs/Z(§Æ~*[4ÆÿÿÿÆï¢ÁÆftƽÆvcÆ-±Æ~ÆÿÿÿÆï¢*ÆÆ�F²#ÆÆ¡2ÄÆÆ¦ÆÿÿÿÆï¢	ÁÆiqƾÆx`Æ+³Æ€ÆÿÿÿÆï¢9]%¯Æm2`%¥Æy+a2ÆÿÿÿÆû½¢«‰ÂÆýµ�ÃÆý±ˆ¹Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-ÆÿÿÆÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-Æÿÿÿ-ÆÿÿÿÆÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿ¯ÿÿÿ¯ï�/M$�¯f*P$”¯o%P.¯ÿÿÿ¯ï�ª¯[f¯§¯hW¯(œ¯o¯ÿÿÿ¯ï�%¯¯~>�¯¯Ž-­¯¯“¯ÿÿÿ¯ï�«¯]d¯¨¯jT¯&ž¯q¯ÿÿÿ¯ï�2R!›¯a,U!‘¯k&V,¯ÿÿÿ¯û§�—y¬¯ý }¬¯ýœx£¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ-¯ÿÿÿ¯ÿéÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯þ¤„
¯ÿªéÿÿÿ¯þ�0
¯ÿžéÿÿÿ¯û�0œƒ®¯ý¥‡®¯ý¢�˜éÿÿÿ¯ï�/M$�¯f*P$”¯o%P.éÿÿÿ¯ï�ª¯[f¯§¯hW¯(œ¯oéÿÿÿ¯ï�%¯¯~>�¯¯Ž-­¯¯“éÿÿÿ¯ï�«¯]d¯¨¯jT¯&ž¯qéÿÿÿ¯ï�2R!›¯a,U!‘¯k&V,éÿÿÿ¯û§�—y¬¯ý }¬¯ýœx£éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿéÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯éÿÿÿ¯ÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿë,éK®éæ×o�ééG�ééÍéé*éë,é«Oé£Qéo�éé4°éé1¹éé*éë,éè&ÖF¯éo�ééB™ééÇéé*éë,éée(ééo�éénoéÕãéé*éï,é騂ééo�ééÏd#}éÿ*éëºééäÙééo}ÙééÖš¸ééæÌ,éý¶Quéý¥ežéýÙen,éýÁ°æûé?@éÿ¨éýÀ™»éùäçééà™ž*éÿ,éóo^»é¬$–ééà™*éð,éºèééÇÚo�ééÚßZ–éÿ*éö,éK®éæ×o�éþZ–éÿ*éö,é«Oé£Qéo�éþZ–éÿ*éö,éè&ÖF¯éo�éþZ–éÿ*éö,éée(ééo�éþZ–éÿ*éï,é騂ééo�ééÍ^%=aéÿ*éóºééäÙééo}Ùéݯû°éæÌ,éý¶Quéý¥ežéýÙen,éýÁ°æûé?ÆéþÎt=éþ±}éþÎt=éþ±}éþÎt=éþ±xé?#ÿþü˜=ÿýƒÜ;ÿüî%F;ÿûW´9ÿúþ�k³;ÿþ×'=ÿþ×'ÿë%Æ@”ÆÄ¶_yÆÆ=zÆÆ®ÆÆ*Æë%Æ‘DÆŠEÆ_yÆÆ,–ÆÆ*žÆÆ*Æë%ÆÅ!¶<•Æ_yÆÆ8‚ÆÆ©ÆÆ*Æë%ÆÆlU"ÆÆ_yÆÆ]_ƵÁÆÆ*Æï%ÆÆ·nÆÆ_yÆÆ°UjÆÿ*ÆëžÆÆÁ¸ÆÆ_j¸ÆÆ¶ƒœÆÆÄ­,Æý›EcÆý�V†Æý¹V],Æý¤–ÄûÆ?@Æÿ�Æý£‚ŸÆùÂÄÆÆ¾‚†*Æÿ%Æó_PŸÆ’	ÆÆ¾‚*Æð%ÆžÅÆÆ©º_yÆÆº½LÆÿ*Æö%Æ@”ÆÄ¶_yÆþLÆÿ*Æö%Æ‘DÆŠEÆ_yÆþLÆÿ*Æö%ÆÅ!¶<•Æ_yÆþLÆÿ*Æö%ÆÆlU"ÆÆ_yÆþLÆÿ*Æï%ÆÆ·nÆÆ_yÆÆ®P4RÆÿ*ÆóžÆÆÁ¸ÆÆ_j¸Æ¼”û–ÆÄ­,Æý›EcÆý�V†Æý¹V],Æý¤–ÄûÆ?ÆÆþ¯c=Æþ—}Æþ¯c=Æþ—}Æþ¯c=Æþ—xÆ?#ÿþü˜=ÿýƒÜ;ÿüî%F;ÿûW´9ÿúþ�k³;ÿþ×'=ÿþ×'ÿë!¯8‚¯­¡Tk¯¯6l¯¯
š¯¯*¯ë!¯€<¯z=¯Tk¯¯'„¯¯%‹¯¯*¯ë!¯®¡5„¯Tk¯¯1s¯¯–¯¯*¯ë!¯¯_K¯¯Tk¯¯RT¯ ª¯¯*¯ï!¯¯¢a¯¯Tk¯¯œK^¯ÿ*¯ëŒ¯¯«£¯¯T^£¯¯¡tН¯­™,¯ý‰=X¯ý|Lw¯ý£LR,¯ý‘„­û¯?@¯ÿ~¯ý�s�¯ù¬®¯¯¨sw*¯ÿ!¯óTG�¯�q¯¯¨s*¯ð!¯Œ®¯¯–¤Tk¯¯¤§Cq¯ÿ*¯ö!¯8‚¯­¡Tk¯þCq¯ÿ*¯ö!¯€<¯z=¯Tk¯þCq¯ÿ*¯ö!¯®¡5„¯Tk¯þCq¯ÿ*¯ö!¯¯_K¯¯Tk¯þCq¯ÿ*¯ï!¯¯¢a¯¯Tk¯¯šG.I¯ÿ*¯óŒ¯¯«£¯¯T^£¯¦ƒû„¯­™,¯ý‰=X¯ý|Lw¯ý£LR,¯ý‘„­û¯?ƯþšW=¯þ…}¯þšW=¯þ…}¯þšW=¯þ…x¯?#ÿþü˜=ÿýƒÜ;ÿüî%F;ÿûW´9ÿúþ�k³;ÿþ×'=ÿþ×'ÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿ ÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿÿþ÷ÞÿþÞ÷ÿÿéÿÿúÿÿš&€ÿú€&šÿÿéÿÿúÈ;
?ú?
;ÈéÿÿúÈ;
?ú?
;Èéÿÿúÿÿš&€ÿú€&šÿÿéÿÿÿÿþ÷ÞÿþÞ÷ÿ ÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿÿéÿÿÿ
ÿ ÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿ ÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿÿþ÷ÞÿþÞ÷ÿÿÆÿÿúÿÿš&€ÿú€&šÿÿÆÿÿúÈ;
?ú?
;ÈÆÿÿúÈ;
?ú?
;ÈÆÿÿúÿÿš&€ÿú€&šÿÿÆÿÿÿÿþ÷ÞÿþÞ÷ÿ ÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿÿÆÿÿÿ
ÿ ÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿ ÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿÿþ÷ÞÿþÞ÷ÿÿ¯ÿÿúÿÿš&€ÿú€&šÿÿ¯ÿÿúÈ;
?ú?
;ȯÿÿúÈ;
?ú?
;ȯÿÿúÿÿš&€ÿú€&šÿÿ¯ÿÿÿÿþ÷ÞÿþÞ÷ÿ ÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿÿ¯ÿÿÿ
ÿ ÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿéÞÁ©ßééÞ²çéé8Çéééפ¯ãÔ+ééÜÏä8¢éél“éÿéôÁ©ßééÞ²çéé8ÇéÜÑ7\v�éƒGr:Çé8Çéé¶1€m¹Ô+éé¬+ééqhéél“éÿéòÑ7\v�éƒGr:Çé8Çéþ�€éáæ.åé¡sé8Çéé‚oåééÔ+éé™Léé–Uéél“éÿéþ�€é÷æ.åé¡sé8ÇéþD¶éá»HHGjé8ÇééÙb _ØÔ+éé¦4éé|céél“éÿéþD¶é÷»HHGjé8Çéþ}ƒéýä%äéü8Çééïåh�Ô+ééÏæéO�éél“éÿéþ}ƒéýä%äéþ8ÇéèÏ0c‚˜é~B‚r…é8Çéé{t†0»Ô+é÷a0Y%Ýéél“éÿéòÏ0c‚˜é~B‚r…é8ÇéÞ¯œÚééÜš«âé½áé´éÕ ›×éÔ%Ìéé讟ÝééÕ^“éÿéô¯œÚééÜš«âé½áéýÝeeéý�e¹éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿé?*éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ"éü­Þé¡éøâ™™ééæÉäéý²™Ìéÿéþ­Þéü8ÇééòÔ™é¿#B(qéé²F“éÿéþ8ÇéÞÁ©ßééÞ²çéé8Çéééפ¯ãÔ+ééÚßéäÎéél“éÿéôÁ©ßééÞ²çéé8ÇéèÑ7\v�éƒGr:Çé8Çéé¶1€m¹Ô+éùç"Äéél“éÿéòÑ7\v�éƒGr:Çé8Çéþ�€éíæ.åé¡sé8Çéé‚oåééÔ+éþrVéþl“éÿéþ�€é÷æ.åé¡sé8ÇéþD¶éí»HHGjé8ÇééÙb _ØÔ+éý†Aãéþl“éÿéþD¶é÷»HHGjé8Çéþ}ƒéýä%äéü8Çééûåh�Ô+éýŠ?âéþl“éÿéþ}ƒéýä%äéþ8ÇéÜÏ0c‚˜é~B‚r…é8Çéé{t†0»Ô+éé³U^^¹éél“éÿéòÏ0c‚˜é~B‚r…é8Çé篜ÚééÜš«âé½áé´éÕ ›×éÔ%Ì騝ûÕéÕ^“éÿéô¯œÚééÜš«âé½áéýÝeeéý�e¹éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿé?*éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ"éü­Þé¡éýâ™™éùåäéé²™Ìéÿ"éü8ÇééýÔ™éøä,Aéé²F“éÿéêÁ©ßééÞ²çéé8Çéééפ¯ãÔ+éýtfAéþl“éÿéèÑ7\v�éƒGr:Çé8Çéé¶1€m¹Ô+éüÀQ¯Aéþl“éÿéþ�€éææ.åé¡sé8Çéé‚oåééÔ+ééæ>Ó¯Aéþl“éÿéþD¶éá»HHGjé8ÇééÙb _ØÔ+éé‰W§~/¶éél“éÿéþ}ƒéýä%äéü8Çééïåh�Ô+éé¨mmRˆéél“éÿéèÏ0c‚˜é~B‚r…é8Çéé{t†0»Ô+éþ¯Aéþl“éÿé÷¤=éÏééÑéÆÞ¤�½ÆÆ½—ÄÆÆ/©ÆÆÆ¶‹”Á´$ÆÆ»°Á/‰ÆÆ\}ÆÿÆô¤�½ÆÆ½—ÄÆÆ/©ÆÜ²/Nd{Æo=a2©Æ/©ÆÆ›*m\�´$ÆÆ’$ÆÆ`YÆÆ\}ÆÿÆò²/Nd{Æo=a2©Æ/©ÆmÆáÄ'ÃÆ‰bÆ/©ÆÆn^ÃÆÆ´$ÆÆ‚@ÆÆHÆÆ\}ÆÿÆmÆ÷Ä'ÃÆ‰bÆ/©Æþ9›ÆþŸ=æZÆ/©ÆƸSQ·´$ÆÆ�,ÆÆjTÆÆ\}ÆÿÆþ9›ÆþŸ=üZÆ/©ÆþjoÆýÂÂÆü/©ÆÆïÃYm´$ÆÆ°ÄÆDxÆÆ\}ÆÿÆþjoÆýÂÂÆþ/©Æè¯(Tn�Æk8naqÆ/©ÆÆicr)Ÿ´$Æ÷R)K ¼ÆÆ\}ÆÿÆò¯(Tn�Æk8naqÆ/©ÆÞ•…ºÆÆ»‚‘ÀÆ¡¿Æ™Æµˆ„¶Æ´ ­ÆÆÅ”‡¼ÆÆµP}ÆÿÆô•…ºÆÆ»‚‘ÀÆ¡¿Æý¼VVÆýyV�Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>ÆÿÆ?*Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ"Æü“½Æ‰ÆøÀ‚‚ÆÆÄ«ÁÆý—‚­ÆÿÆþ“½Æü/©ÆÆò´‚Æ¢8"`ÆÆ—<}ÆÿÆþ/©ÆÞ¤�½ÆÆ½—ÄÆÆ/©ÆÆÆ¶‹”Á´$ÆÆº½ÆÂ¯ÆÆ\}ÆÿÆô¤�½ÆÆ½—ÄÆÆ/©Æè²/Nd{Æo=a2©Æ/©ÆÆ›*m\�´$ÆùÄ¦ÆÆ\}ÆÿÆò²/Nd{Æo=a2©Æ/©ÆmÆíÄ'ÃÆ‰bÆ/©ÆÆn^ÃÆÆ´$ÆþaIÆþ\}ÆÿÆmÆ÷Ä'ÃÆ‰bÆ/©Æþ9›ÆþŸ=òZÆ/©ÆƸSQ·´$Æýr7ÁÆþ\}ÆÿÆþ9›ÆþŸ=üZÆ/©ÆþjoÆýÂÂÆü/©ÆÆûÃYm´$Æýu6ÀÆþ\}ÆÿÆþjoÆýÂÂÆþ/©ÆÜ¯(Tn�Æk8naqÆ/©ÆÆicr)Ÿ´$ÆÆ˜HPPžÆÆ\}ÆÿÆò¯(Tn�Æk8naqÆ/©Æç•…ºÆÆ»‚‘ÀÆ¡¿Æ™Æµˆ„¶Æ´ ­Æ·”ûµÆµP}ÆÿÆô•…ºÆÆ»‚‘ÀÆ¡¿Æý¼VVÆýyV�Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>ÆÿÆ?*Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ"Æü“½Æ‰ÆýÀ‚‚ÆùÃÂÆÆ—‚­Æÿ"Æü/©ÆÆý´‚ÆøÂ%7ÆÆ—<}ÆÿÆê¤�½ÆÆ½—ÄÆÆ/©ÆÆÆ¶‹”Á´$ÆýcW7Æþ\}ÆÿÆè²/Nd{Æo=a2©Æ/©ÆÆ›*m\�´$Æü£E•7Æþ\}ÆÿÆmÆæÄ'ÃÆ‰bÆ/©ÆÆn^ÃÆÆ´$ÆÆÄ5³•7Æþ\}ÆÿÆþ9›ÆþŸ=æZÆ/©ÆƸSQ·´$ÆÆtJŽk(›ÆÆ\}ÆÿÆþjoÆýÂÂÆü/©ÆÆïÃYm´$ÆÆ�\\FtÆÆ\}ÆÿÆè¯(Tn�Æk8naqÆ/©ÆÆicr)Ÿ´$Æþ•7Æþ\}ÆÿÆ÷Œ4ưÆÆ²Æ¯Þ‘§¯¯§†®¯¯*–¯¯¯¡{ƒªŸ ¯¯¥œ«*y¯¯Qn¯ÿ¯ô‘§¯¯§†®¯¯*–¯Ü�)EYl¯b6V,–¯*–¯¯‰%`R‹Ÿ ¯¯� ¯¯UN¯¯Qn¯ÿ¯ò�)EYl¯b6V,–¯*–¯þa`¯á­"¬¯yV¯*–¯¯aS¬¯¯Ÿ ¯¯s9¯¯q@¯¯Qn¯ÿ¯þa`¯÷­"¬¯yV¯*–¯þ3‰¯þ�6æP¯*–¯¯£IG¢Ÿ ¯¯}'¯¯]J¯¯Qn¯ÿ¯þ3‰¯þ�6üP¯*–¯þ^b¯ý¬¬¯ü*–¯¯ï¬NaŸ ¯¯œ­¯¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ¯?*¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ"¯ü‚§¯y¯øªss¯¯­—«¯ý†s™¯ÿ¯þ‚§¯ü*–¯¯òŸs¯�1U¯¯†5n¯ÿ¯þ*–¯Þ‘§¯¯§†®¯¯*–¯¯¯¡{ƒªŸ ¯¯¤§¯¬š¯¯Qn¯ÿ¯ô‘§¯¯§†®¯¯*–¯è�)EYl¯b6V,–¯*–¯¯‰%`R‹Ÿ ¯ù®“¯¯Qn¯ÿ¯ò�)EYl¯b6V,–¯*–¯þa`¯í­"¬¯yV¯*–¯¯aS¬¯¯Ÿ ¯þVA¯þQn¯ÿ¯þa`¯÷­"¬¯yV¯*–¯þ3‰¯þ�6òP¯*–¯¯£IG¢Ÿ ¯ýe1ª¯þQn¯ÿ¯þ3‰¯þ�6üP¯*–¯þ^b¯ý¬¬¯ü*–¯¯û¬NaŸ ¯ýh/ª¯þQn¯ÿ¯þ^b¯ý¬¬¯þ*–¯Ü›$Jar¯_1aVd¯*–¯¯]We$�Ÿ ¯¯‡@GG‹¯¯Qn¯ÿ¯ò›$Jar¯_1aVd¯*–¯ç„u¤¯¯¥s€ª¯Ž©¯‡¯ xu¡¯Ÿ™¯¢ƒû ¯ Gn¯ÿ¯ô„u¤¯¯¥s€ª¯Ž©¯ý¦LL¯ýkL‹¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ¯?*¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ"¯ü‚§¯y¯ýªss¯¬¯ý†s™¯ÿ"¯ü*–¯¯ýŸs¯ø¬!1¯¯†5n¯ÿ¯ê‘§¯¯§†®¯¯*–¯¯¯¡{ƒªŸ ¯ýWM1¯þQn¯ÿ¯è�)EYl¯b6V,–¯*–¯¯‰%`R‹Ÿ ¯ü�=„1¯þQn¯ÿ¯þa`¯æ­"¬¯yV¯*–¯¯aS¬¯¯Ÿ ¯¯­/Ÿ„1¯þQn¯ÿ¯þ3‰¯þ�6æP¯*–¯¯£IG¢Ÿ ¯¯gA~_#‰¯¯Qn¯ÿ¯þ^b¯ý¬¬¯ü*–¯¯ï¬NaŸ ¯¯~RR>f¯¯Qn¯ÿ¯è›$Jar¯_1aVd¯*–¯¯]We$�Ÿ ¯þ„1¯þQn¯ÿ¯÷|.¯œ¯¯�¯÷ééפ¯ãÔ+éýϾ2éþl“éÿ
ÿÿÿÿ¯÷é¶1€m¹Ô+éþ¾2éþl“éÿ
ÿÿÿÿ¯÷é‚oåééÔ+éþ¾2éþl“éÿ
ÿÿÿÿ¯÷éÙb _ØÔ+éþ¾2éþl“éÿ
ÿÿÿÿ¯ÿéûåh�Ô+éþ¾2éþl“éÿ
ÿÿÿÿ¯÷é{t†0»Ô+é÷}M^°éél“éÿ
ÿÿÿÿ¯ó´éÕ ›×éÔ%Ìé鼯ûÑéÕ^“éÿ
ÿÿÿÿ¯éýÝeeéý�e¹éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯ÿ¡éøâ™™ééäÈÞéý²™Ìéÿ
ÿÿÿÿ¯ÿéòÔ™éÝ$F,Qéé²F“éÿÿþ÷ÞÿþÞ÷ÿÿÿÿ¯óª˜¯¯ééפ¯ãÔ+éú1·éél“éúÿÿš&€ÿú€&šÿÿÿﯯjX¯¯é¶1€m¹Ô+éøÚ·#Óéél“éúÈ;
?ú?
;Èÿï‰(+WWé‚oåééÔ+éø:qæéél“éúÈ;
?ú?
;Èÿï‰(+WWéÙb _ØÔ+éùæ=Ÿéél“éúÿÿš&€ÿú€&šÿÿÿ÷¯¯jX¯¯éûåh�Ô+éúY•éél“éÿÿþ÷ÞÿþÞ÷ÿÿÿÿ¯çª˜¯¯é{t†0»Ô+éé°UzO)äéél“éÿ
ÿÿÿÿ¯ñ´éÕ ›×éÔ%Ìéä°“ÀéýÕ^“éÿ
ÿÿÿÿ¯éýÝeeéý�e¹éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯÷ÆÆ¶‹”Á´$Æý°¢+Æþ\}Æÿ
ÿÿÿÿÆ÷Æ›*m\�´$Æþ¢+Æþ\}Æÿ
ÿÿÿÿÆ÷Æn^ÃÆÆ´$Æþ¢+Æþ\}Æÿ
ÿÿÿÿÆ÷ƸSQ·´$Æþ¢+Æþ\}Æÿ
ÿÿÿÿÆÿÆûÃYm´$Æþ¢+Æþ\}Æÿ
ÿÿÿÿÆ÷Æicr)Ÿ´$Æ÷jAP–ÆÆ\}Æÿ
ÿÿÿÿÆó™Æµˆ„¶Æ´ ­ÆÆ ”û²ÆµP}Æÿ
ÿÿÿÿ
Æý¼VVÆýyV�Æÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ‰ÆøÀ‚‚ÆÆÂª½Æý—‚­Æÿ
ÿÿÿÿÆÿÆò´‚Ƽ<%EÆÆ—<}Æÿÿþ÷ÞÿþÞ÷ÿÿÿÿÆóÀ¬ÆÆÆÆ¶‹”Á´$Æú*›ÆÆ\}Æúÿÿš&€ÿú€&šÿÿÿïÆÆxcÆÆÆ›*m\�´$Æøº›³ÆÆ\}ÆúÈ;
?ú?
;Èÿï›.1ccÆn^ÃÆÆ´$Æø1
`ÄÆÆ\}ÆúÈ;
?ú?
;Èÿï›.1ccƸSQ·´$ÆùÄ4‡ÆÆ\}Æúÿÿš&€ÿú€&šÿÿÿ÷ÆÆxcÆÆÆûÃYm´$ÆúKÆÆ\}Æÿÿþ÷ÞÿþÞ÷ÿÿÿÿÆçÀ¬ÆÆÆicr)Ÿ´$ÆÆ–HgC#ÁÆÆ\}Æÿ
ÿÿÿÿÆñ™Æµˆ„¶Æ´ ­ÆÂ–}£ÆýµP}Æÿ
ÿÿÿÿ
Æý¼VVÆýyV�Æÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆ÷¯¯¡{ƒªŸ ¯ýœ�&¯þQn¯ÿ
ÿÿÿÿé÷¯‰%`R‹Ÿ ¯þ�&¯þQn¯ÿ
ÿÿÿÿé÷¯aS¬¯¯Ÿ ¯þ�&¯þQn¯ÿ
ÿÿÿÿé÷¯£IG¢Ÿ ¯þ�&¯þQn¯ÿ
ÿÿÿÿéÿ¯û¬NaŸ ¯þ�&¯þQn¯ÿ
ÿÿÿÿé÷¯]We$�Ÿ ¯÷^:G„¯¯Qn¯ÿ
ÿÿÿÿé󇯠xu¡¯Ÿ™¯¯�ƒû�¯ Gn¯ÿ
ÿÿÿÿé¯ý¦LL¯ýkL‹¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿéÿy¯øªss¯¯¬–§¯ý†s™¯ÿ
ÿÿÿÿéÿ¯òŸs¯¦5!=¯¯†5n¯ÿÿþ÷ÞÿþÞ÷ÿÿÿÿéóâËé鯯¡{ƒªŸ ¯ú%‰¯¯Qn¯úÿÿš&€ÿú€&šÿÿÿïéé�#ué鯉%`R‹Ÿ ¯ø¤‰Ÿ¯¯Qn¯úÈ;
?ú?
;Èÿï·6	:tt¯aS¬¯¯Ÿ ¯ø+U­¯¯Qn¯úÈ;
?ú?
;Èÿï·6	:tt¯£IG¢Ÿ ¯ù­.w¯¯Qn¯úÿÿš&€ÿú€&šÿÿÿ÷éé�#uéé¯û¬NaŸ ¯úCp¯¯Qn¯ÿÿþ÷ÞÿþÞ÷ÿÿÿÿéçâËéé¯]We$�Ÿ ¯¯„@[;«¯¯Qn¯ÿ
ÿÿÿÿéñ‡¯ xu¡¯Ÿ™¯¬„n�¯ý Gn¯ÿ
ÿÿÿÿé¯ý¦LL¯ýkL‹¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿéܯ<_¯¯§hY¯¯"œ“-¯¯†:¯¯a_¯¯¡‡!¯¯*s¯�3¯ù^asW.§
¯Û”¯¯¨«¡#¯¯•/¯¯zE¯¦­‡8¯¯5‹¯˜(¯ù®$®Vw¯Ö®aI¯<:'!u¯¬I+&l¯¯C,b¯:,lªF+&p¯‡8¯¯5‹¯˜(¯¯Ê¯ú8=¯…£¯þ<¯þ§«¯þ§®¯þ§®¯ÿÿ¯þ£#¯ú’sÿss„¯ÿY¯ÿÿ¯ú’sÿss„¯ÿ­¯ÿÿ¯ÿÿ9¯ÿÿ¯ÿÿ9¯ÿÿ¯ÿ=¯ÿ;¯ÿþ¯ÿ0¯
ÿþÿ(¯ÿ	¯ÿþ¯ÿ0¯ÿ	¯ÿ/¯ÿ	¯.¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ5¯ÿ̯ÿ4¯þÌé¯ÿ3¯ýÌéé¯ÿ2¯ÿÌé¯ÿ1¯ÿÌé¯ÿ0¯ÿÌé¯ÿ/¯ÿÌé¯ÿ.¯ÿÌé¯ÿ-¯ÿÌé¯þ˜ª¯ÿ,¯ÿÌé¯ýXj¯ÿ+¯ÿÌ	éWù+(‰¯¯þc€¯ÿÌ
éWù+(‰¯¯ñ§¯ÿY=¯þ£ÜÆDlÆÆ½ueÆÆ'°¦2ÆÆ—AÆÆnlÆÆ!¶™%ÆÆ/‚ÆŸ:Æùjn‚c4½"
ÆÛ§ÆD…ÆÆ/¦X~ÆÆ@”¦2ÆÆ—AÆÆQ…ÆÆ9›™@ÆÆ<�Ƭ-Æùž:N¢2„S
ÆÛ§ÆDNƹÆ‘GƾÁ¶(ÆÆ¨6ÆÆŠNƼÄ™@ÆÆ<�Ƭ-ÆùÄ)Åb"‡ÆÖÅmSÆDA,%…ÆÂR0+zÆÆL2oÆA2zÀO1+Æ™@ÆÆ<�Ƭ-ÆÆ½Æú@EÆ—¹ÆþDÆþ½ÁÆþ½ÄÆþ½ÅÆÿîÆþ¹#Æú¥‚î‚‚–ÆÿeÆÿîÆú¥‚î‚‚–ÆÿÄÆÿîÆÿî9ÆÿîÆÿî9ÆÿîÆî=Æî;ÆîþÆî0Æ
îþî(Æÿ	ÆîþÆî0Æÿ	Æî/Æÿ	Æ.Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ8Æþ¬ÀÆÿ8ÆýcxÆÿ6Æcù1.›ÆÆþp�Æcù1.›ÆÆñ½De›¾ÆÁ—ÂÆÆ¯¶œ§Æø©¼Æ­¯Æ»¬ÆýcxÆÿÆð³/Di´�*](�ÆY)N:RÆø`xÆ6;ÆslÆþ¬ÀÆÿÆðX�ÆÆ>žÆ˜@ÆYJÆÆ!ºÆø•DÁ/-Ã>¡ÆÿÆðX�ÆÆÄÆÁÆYrÆÆFŽÆøÁ’]X–ÅÆÿÆð`{ÆÆ;¡Æ›=ÆYLÆÆ"·Æú6:’�:B	ÆÿÆñ’(a³‰)c'ŒÆY$T@KÆúkÀ½w	ÆÿÆóº—½Æ½‹½ÆÆY”�Æú¸�ÆÆœº	Æÿ"ÆþYÆÿóÿ´Æÿ"Æþ¡®Æÿ¯PÿRÆÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æ7~ÆÿÄ>Æÿe=Æþ¹ÜéPéé!ÞŠwéé.ÏÄ;éé²Méé‚éé'Ö´,éé8šé»Eéù}‚št=Þ(
éÛÄéPœéé7Äg”ééL®Ä;éé²Méé_œééD¶´KééF¹éÊ5éùºE[¿:›b
éÛÄéP[éÙ"é«Séàä×/ééÆ?éé£[éÝæ´KééF¹éÊ5éùç0ès(ŸéÖè�béPM4,œéäa93�ééZ:ƒéM:�â]:3•é´KééF¹éÊ5éé‡éúKQé±ÙéþOéþÞäéþÞçéþÞèéÿªéþÙ"#éúÙª™™°éÿwéÿªéúÙª™™°éÿæéÿªéÿª9éÿªéÿª9éÿªéª=éª;éªþéª0é
ªþª(éÿ	éªþéª0éÿ	éª/éÿ	é.éÿ>éÿ>éÿ>éÿ>éÿ5éÿÌéÿ4éþ̯éÿ3éý̯¯éÿ2éÿ̯éÿ1éÿ̯éÿ0éÿ̯éÿ/éÿ̯éÿ.éÿ̯éÿ-éÿ̯éþËâéÿ,éÿ̯éýu#�éÿ+éÿÌ	¯tù:	6·ééþ„ªéÿÌ
¯tù:	6·ééñßPw¶àéä±äééÎ׸Äéÿ̯ò–¦¯™š¯¥˜¯ééu#�éÿéîÒ7P{Ô¦1n0¨éi0[E`é̯óUj¯04¯f_¯ééËâéÿéðg˜ééIºé³LéiWéé'¿¯÷„<ª*(¬7ޝéÿéðg˜ééæéäéi†ééH~¯÷«�RN„®¯éÿéðq‘ééE½é¶HéiZéÌ¢¯ø03�|3:¯¯éÿéñ¬0rÓ¢0u.¤éi+V8C¯ø_ª§i¯¯éÿéóÚ±Þéߤßééi‚‚‹¯ø£‹¯¯Š¥¯¯éÿ"éþ\q¯ÿ¬Ÿ¯éÿ!éýÌŽš¯ÿšGÿI¯éÿ éÿ̯éÿéÿ̯éÿéÿ̯éÿéÿ̯éÿéÿ̯éÿéÿ̯éÿéÿ̯éÿéÿ̯éÿéÿ̯éÿéÿ̯éÿéÿ̯éÿéÿ̯éÿéÿÌ ¯éÿéÿÌ!¯éÿéÿÌ"¯éÿéÿÌ#¯éÿéÿÌ$¯éÿéÿÌ%¯é7~éÿæ>éÿw=éþÙ"æÿ>ÿ>ÿ)ÿÿ)ÿÿ9ÿÿ9ÿþ=þ;þ;þ;ü;þ:ú'ÿ-¯ÿÿþ-¯ÿÿüW{+¯ÿÿþ
|+¯ÿÿÿ0,¯ÿÿý—,¯ÿÿþh-¯ÿÿÿ.¯ÿÿÿ.¯ÿÿÿ.¯ÿÿÿ.¯ÿÿÿ.¯ÿÿ ¯ÿÿÿ¯ÿÌéÿ¯ÿÿÿ¯ÿÌéÿ¯ÿÿÿ¯ÿÌéÿ¯ÿÿÿ¯ÿÌéÿ¯ÿÿü¯¯Ìéÿ¯ÿÿý¯Ìéÿ¯ÿÿþÌéÿ¯ÿÿÿéÿ¯ÿÿÿéÿ¯ÿÿÿéÿ¯ÿÿÿéÿ¯ÿÿÿéÿ¯ÿÿÿéÿ¯ÿÿÿéÿ¯ÿÿÿéÿ¯ÿÿÿéÿ¯ÿÿÿéþô¯þª˜¯þ˜ª¯ÿÿÿéýôÿ¯ýjX¯úXj¯¯ÿÿéöôÿÿ¯‰(+Wú+(‰ÿÿéÿôÿù¯‰(+Wú+(‰ÿÿéÿôÿÿ¯ýjX¯úXj¯¯ÿÿéÿôÿÿ¯þª˜¯þ˜ª¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿô	ÿÿ¯ÿÿÿéÿô
ÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿô
ÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿ
éÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿ
éÿôÿÿ¯ÿÿÿ	éÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿÿéÿôÿÿ¯ÿÿüééôÿÿ¯ÿÿýéôÿÿ¯ÿÿ ¯ÿÿÿ.¯ÿÿþh-¯ÿÿý—,¯ÿÿÿ0,¯ÿÿÿ-Æÿÿþ-Æÿÿüc‹+ÆÿÿþŒ+Æÿÿÿ6,Æÿÿý«,Æÿÿþv-Æÿÿÿ.Æÿÿÿ.Æÿÿÿ.Æÿÿÿ.Æÿÿÿ.Æÿÿ ÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆÿÆÿÿÿÆþÚÆþÀ¬Æþ¬ÀÆÿÿÿÆýÚîÆýxcÆúcxÆÆÿÿÆöÚîîÆ›.1cú1.›ÿÿÆÿÚîùÆ›.1cú1.›ÿÿÆÿÚîÿÆýxcÆúcxÆÆÿÿÆÿÚîÿÆþÀ¬Æþ¬ÀÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚ	îÿÆÿÿÿÆÿÚ
îÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚ
îÿÆÿÿÿÆÿÚîÿÆÿÿÿ
ÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿ
ÆÿÚîÿÆÿÿÿ	ÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿÿÆÿÚîÿÆÿÿüÆÆÚîÿÆÿÿýÆÚîÿÆÿÿ Æÿÿÿ.Æÿÿþv-Æÿÿý«,Æÿÿÿ6,Æÿÿÿ$-éÿÿþ$-éÿÿüt¤+éÿÿþ
¤+éÿÿÿ@,éÿÿýÉ,éÿÿþ‹-éÿÿÿ.éÿÿÿ.éÿÿÿ.éÿÿÿ.éÿÿÿ.éÿÿ éÿÿÿéÿ̯ÿéÿÿÿéÿ̯ÿéÿÿÿéÿ̯ÿéÿÿÿéÿ̯ÿéÿÿüéé̯ÿéÿÿýé̯ÿéÿÿþ̯ÿéÿÿÿ¯ÿéÿÿÿ¯ÿéÿÿÿ¯ÿéÿÿÿ¯ÿéÿÿÿ¯ÿéÿÿÿ¯ÿéÿÿÿ¯ÿéÿÿÿ¯ÿéÿÿÿ¯ÿéÿÿÿ¯þ¬éþâËéþËâéÿÿÿ¯ý¬ªéý�#uéúu#�ééÿÿ¯ö¬ªªé·6	:tú:	6·ÿÿ¯ÿ¬ªùé·6	:tú:	6·ÿÿ¯ÿ¬ªÿéý�#uéúu#�ééÿÿ¯ÿ¬ªÿéþâËéþËâéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬	ªÿéÿÿÿ¯ÿ¬
ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬
ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ
¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ
¯ÿ¬ªÿéÿÿÿ	¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿÿ¯ÿ¬ªÿéÿÿü¯¯¬ªÿéÿÿý¯¬ªÿéÿÿ éÿÿÿ.éÿÿþ‹-éÿÿýÉ,éÿÿÿ@,éÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿ0ÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿÿÿÿ¯ÿÿ0>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿ?ÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿ0ÿÿÿÿÆÿÿÿÿÿÆÿÿÿÿÿÆÿÿ0>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿ?ÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿ0ÿÿÿÿéÿÿÿÿÿéÿÿÿÿÿéÿÿ0>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿ?ÿÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ0ÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>?ÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ/¯ÿÿ?/¯ÿÿ/¯ÿÿ/¯ÿÿ?Àÿ?/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ/Æÿÿ?/Æÿÿ/Æÿÿ/Æÿÿ?Àÿ?/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ/éÿÿ?/éÿÿ/éÿÿ/éÿÿ?Àÿ?ÿÀ?ÿÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿ
éÿÿÿÿé ÿÿÿÿé!ÿÿÿÿ
é"ÿÿÿÿ	é#ÿÿÿÿé$ÿÿÿÿéÿÿ'ÿÿÿÿ	é#ÿÿÿÿé$ÿÿÿÿéÿÿ'Àÿ?ÿÿîÿÿÆîÿÿîÿÿÆîÿÿîÿÿÆîÿÿîÿÿÆîÿÿîÿÿ
ÆîÿÿîÿÿÆîÿÿîÿÿÆîÿÿîÿÿ
Æîÿÿîÿÿ	ÆîÿÿîÿÿÆîÿÿîÿÿÆîÿ'ÿÿÿÿ	ÆîÿÿÿÿÆîÿÿÿÿÆîÿ'Àÿ?ÿÿªÿÿ¯ªÿÿªÿÿ¯ªÿÿªÿÿ¯ªÿÿªÿÿ¯ªÿÿªÿÿ
¯ªÿÿªÿÿ¯ªÿÿªÿÿ¯ªÿÿªÿÿ
¯ªÿÿªÿÿ	¯ªÿÿªÿÿ¯ªÿÿªÿÿ¯ªÿ'ÿÿÿÿ	¯ªÿÿÿÿ¯ªÿÿÿÿ¯ªÿ'Àÿ?ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ'ÿÀ?ÿÀÿ?Àÿ?Àÿ?Àî?Àî?9ÿî8ÿî7ÿî6ÿîÀÿ?Àª?Àª?9ÿª8ÿª7ÿª6ÿªÀÿ?ÿÀ?ÿÿÿÿÿéçŠ{À¥1n0©é¹0m2–ãI>{ÄÊ?lN{ÿÿÿÿéèééG¼é²Méb¢éÌ2é‰uéét›éè/ÿÿÿÿéèééæéãé)Ñééâ‰uéé5?HHDÿÿÿÿéëééD¿éµIé^¤éÏ0é“oéép”éÿÿÿÿéèéé¡0u.¥éµ.t3�éË%oÂÈ7j„Lÿÿÿÿéÿ´éýÞ¤àéýä¤Ùéøà´Öé鳚Íÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿ>ÿÿ>ÿÿ/ÿðÿ?îÿ
îÿÿÆçu	i£�*](�Æ�)\+Á>5i§¬6\Biîÿ
îÿÿÆèÆÆ= Æ—AÆS‰Æ­+ÆtcÆÆc„ÆÅ(îÿ
îÿÿÆèÆÆÄÆÁÆ#²ÆÆÀtcÆÆ-6==9îÿ
îÿÿÆëÆÆ9¢Æš>ÆPŒÆ¯(Æ}^ÆÆ`~Æîÿ
îÿÿÆèÆÆ‰)c'�Æš'c+{Ƭ_¥ª/Zp@îÿ
îÿÿÆÿ™Æý½‹¾ÆýÁŒ¹Æø¾™¶ÆÆ˜‚®îÿ
îÿÿÆîÿ
îÿÿÆîÿ
îÿÿÆîÿ
îÿÿÆîÿ
îÿÿÆ
îÿîÿ
î!ÿîÿ
î!ÿîÿ
î!ÿ
î!ÿî!ÿî!ÿî!ÿîâÿ?ªÿ
ªÿÿ¯çh]�|%R$¯‹$R&qª7/]”˜/Q:]ªÿ
ªÿÿ¯è¯¯6�¯†:¯Iy¯™&¯gX¯¯Wu¯®#ªÿ
ªÿÿ¯è¯¯­¯ª¯�¯¯ªgX¯¯(/663ªÿ
ªÿÿ¯ë¯¯3�¯ˆ7¯G|¯›$¯nS¯¯To¯ªÿ
ªÿÿ¯è¯¯y$X"|¯ˆ"W&l¯˜T‘–)Pc9ªÿ
ªÿÿ¯ÿ‡¯ý§{¨¯ý«|£¯ø¨‡¡¯¯‡sšªÿ
ªÿÿ¯ªÿ
ªÿÿ¯ªÿ
ªÿÿ¯ªÿ
ªÿÿ¯ªÿ
ªÿÿ¯
ªÿªÿ
ª!ÿªÿ
ª!ÿªÿ
ª!ÿ
ª!ÿª!ÿª!ÿª!ÿªâÿ?ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿ/ÿð?ÿûé”,J…éÑ$ÙééIRQGéà>l-»Ç*]{à=ÁéhD`5ÛéKk)®éškz2²é“kééO¯éýå”0éÝãééÀDééA¾àÊéšféO¯éé=ÁÞ%ééf™é±éŸaéöÉ�aé“kééo‹éýË”eéÐÜéé‡séés…àééÊ0éO¯éé=Á­Néé™`éâé¼Bé¨?bc3é“kéévƒéþ”jéÑßé¼FééDºàÍé�déY©éé=ÁÜ&ééi•éãé¼Bé@¿é’2é“kééV§éþ”jéÑT°ééBYX?éà?s*¸é•9tß=ÁéaHg/Úéãé¼BérKs?2é“kéé.×éþÔÊéþÁOé÷¼»ééàÒžßé÷Ö¯ä¾ßééʯéï¶èéÞÀé賤ڻéÔÊé®béþ…åéþà éþ›Ïéþæ¤ìé?Àÿ?ûÆ~%?qÆÑ¸ÆÆ>FE=ƾ5\&Ÿ©$Oi¾4¤ÆY9R-ºÆ@[#”Æ‚[g+—Æ}[ÆÆC•ÆýÃ~)ÆÝÁÆÆ£9ÆÆ7¢¾¬ÆƒWÆC•ÆÆ4¤½ ÆÆW‚Æ—ƇRÆö«xRÆ}[ÆÆ^vÆý¬~VÆÐ»ÆÆsbÆÆbq¾
ÆÆ¬)ÆC•ÆÆ4¤“BÆÆ‚RÆÀÆ 8Æ�6ST+Æ}[ÆÆdoÆþ~ZÆÑ½Æ <ÆÆ9ž¾®Æ†UÆK�ÆÆ4¤»!ÆÆYÆÁÆ 8Æ6¢Æ|+Æ}[ÆÆIŽÆþ~ZÆÑG–ÆÆ8KK6ƾ6b$œÆ0c½4¤ÆR=X(ºÆÁÆ 8Æa@b6+Æ}[ÆÆ'¶Æþ´¬Æþ¤DÆ÷ ŸÆÆ¾³†½Æ÷¶•¢½ÆÆ¬”Æï›Åƽ£ÆÅ˜ŒºŸÆ´¬Æ”SÆþqÃÆþ¾ Æþ„°ÆþÄ‹ìÆ?Àÿ?û¯o!8d¯Ñ£¯¯7>=6¯¨/Q"�– F]¨.‘¯N3H(¥¯8P‚¯sP[&†¯nP¯¯;„¯ý¬o$¯Ýª¯¯�3¯¯1�¨˜¯tM¯;„¯¯.‘§¯¯Ms¯…¯wI¯ö—jI¯nP¯¯Sh¯ý˜oL¯Ð¥¯¯fV¯¯Vd¨¯¯˜$¯;„¯¯.‘‚:¯¯sH¯ª¯�1¯~/IJ&¯nP¯¯Yb¯þoP¯Ñ§¯�5¯¯3Œ¨š¯vK¯C¯¯.‘¥¯¯Op¯ª¯�1¯0�¯n&¯nP¯¯A~¯þoP¯Ñ?„¯¯1CB/¯¨/V Нp+W§.‘¯I6N#¤¯ª¯�1¯V8V/&¯nP¯¯"¡¯þŸ˜¯þ‘<¯�¯û¨žw§¯÷¡„¬�§¯¯˜ƒ¯ï‰®¯§�¯®‡|¤�¯Ÿ˜¯‚I¯þd¬¯þ¨ ¯þuœ¯þ­{ì¯?Àÿ?À?ÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿéÿÿÿÿÿÀÿ?ÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÆÿÿÿÿÿÀÿ?¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿ¯ÿÿÿÿÿÀÿ?À?ÿ"ÿþ×'=ÿþ×';ÿúþ�k³:ÿûW´:ÿüî%F<ÿýƒÜ<ÿþü˜ÿ?Àÿ?"ÿþ×'=ÿþ×';ÿúþ�k³:ÿûW´:ÿüî%F<ÿýƒÜ<ÿþü˜ÿ?Àÿ?"ÿþ×'=ÿþ×';ÿúþ�k³:ÿûW´:ÿüî%F<ÿýƒÜ<ÿþü˜ÿ?Àÿ?À?ÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿ ÿÀÿ?ÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿ ÿÀÿ?ÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿÿÿÿÿÿ
ÿ ÿÀÿ?À?ÿé鯜ÚééÜš«âé½áé´éÕ ›×éÔ%ÌéùÛ¿ééÕ^“éÿé÷Ù¾éã´éé³äéýÝeeéý�e¹éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿ>éÿé?Àÿ?Æé•…ºÆÆ»‚‘ÀÆ¡¿Æ™Æµˆ„¶Æ´ ­Æùº¢ÆÆµP}ÆÿÆ÷¸¢ÆÁ™ÆÆ˜ÁÆý¼VVÆýyV�Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>Æÿ>ÆÿÆ?Àÿ?¯é„u¤¯¯¥s€ª¯Ž©¯‡¯ xu¡¯Ÿ™¯ù¥�¯¯ Gn¯ÿ¯÷£�¯ª‡¯¯‡«¯ý¦LL¯ýkL‹¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ>¯ÿ¯?Àÿ?À?ÿéÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯éÿ
ÿÿÿÿ¯'ÿÀÿ?Æÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆÿ
ÿÿÿÿÆ'ÿÀÿ?¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé¯ÿ
ÿÿÿÿé'ÿÀÿ?À?ÿ=¯þ<<¯ý®aI>¯ÿ”>¯ÿ”>¯ÿ”>¯ÿ”<¯ý®aI=¯þ<=¯þ£>¯ÿY>¯ÿ­?Àÿ?=ÆþD<ÆýÅmS>Æÿ§>Æÿ§>Æÿ§>Æÿ§<ÆýÅmS=ÆþD=Æþ¹>Æÿe>ÆÿÄ?Àÿ?=éþO<éýè�b>éÿÄ>éÿÄ>éÿÄ>éÿÄ<éýè�b=éþO=éþÙ">éÿw>éÿæ?Àÿ?À?ÿý
|+¯ÿÿüW{+¯ÿÿþ-¯ÿÿþ-¯ÿÿþ-¯ÿÿþ-¯ÿÿüW{+¯ÿÿþ
|+¯ÿÿÿ0,¯ÿÿý—,¯ÿÿþh-¯ÿÿ0ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿ@ýŒ+Æÿÿüc‹+Æÿÿþ-Æÿÿþ-Æÿÿþ-Æÿÿþ-Æÿÿüc‹+ÆÿÿþŒ+Æÿÿÿ6,Æÿÿý«,Æÿÿþv-Æÿÿ0ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿ@ý
¤+éÿÿüt¤+éÿÿþ$-éÿÿþ$-éÿÿþ$-éÿÿþ$-éÿÿüt¤+éÿÿþ
¤+éÿÿÿ@,éÿÿýÉ,éÿÿþ‹-éÿÿ0ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿ@>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>ÿÿ>@ÿ ЇhC4!urwid-urwid-69907e4/docs/manual/images/widget_layout.png000066400000000000000000000456671526073734300233610ustar00rootroot00000000000000‰PNG


IHDRXwyt5¢sBIT|dˆtEXtSoftwarewww.inkscape.org›î< IDATxœìÝwXTgÚð{½÷Ž"ÅÒ[4–$šl6Å$¦›M¯›Þ{¢1½~›ÞËnš±ÅØ+¨(½÷Þ‡:íûcäÀ83€q„ûw]^áœ÷œ÷<3„áá­"‘ÉH@©hí8ˆˆˆˆÆ©ÌâÑ‚ˆˆˆh¼a‚EDDDdbL°ˆˆˆˆLŒ	‘‰1Á""""21&XDDDD&Æ‹ˆˆˆÈĘ`™,""""c‚EDDDdbL°ˆˆˆˆLLúwoÔh4hiiA[[;:::¡P( P(´•J¥�Éd°³³…½½ìììL0ÑXwÚ	V[[
‹QS[+$TC±´´„�·7`ffvÚA�KD4JEÛ�j4dg碠°P¯L"‘ÀÖÆR©Ý==�Ëåë033ì™3àáá~¦q�IR™Ý𬌌()-Õ9'‰
?H$á|kk+REWW—^=b±ñq1pvv>ã@DDD4ÖHevÃäÞÜܬ—\Ú±V~~¾:ÉØÛÛcZx˜ÁºÔj523sþF¸DDDDç†a%XÕÕµÏ+
äå,suu1Z_Kk‹ÁÖ-"""¢ñ`X	Voo¯Ñ²êšƒç¥R©^ËÖ@­­CwK�‹†•`98Ø-“IeÆËdÆË†;‘ˆˆˆè\3¬eüü|QVV�–Ö�ób±aa!ëÁ*•êoÝ×ç¹ç^†ƒe,_Ѝ¨È3ªÿlikkC]]=‚‚
–§¤AVV6®¿~µpî§Ÿ~FGG‡Þ9Wøx{áÊU×áè‘ýë[·îMtwwãÉ'9£¸;;;QVV���à3ª‡ˆˆh"V–X,FbbÂÃBáêâggNžŒ…æÁÅE6`ww7jjj¡T*M°!¯¿þ

ŠFäYgjÏÞý¸éæÛŒ–÷ö*pÿ�B­Vçžxò9<úØÓ:çâY455ÃÁÁW^q™ÑúZZ[ÐØÔtÆqgdœÀ¥ÿ\uÆõMÃ^hT"‘ 0p2'땵··£±±	MMÍhjnFgg§Iƒ4ä©§¾þÏ>ÃUW]ŽeKÏ”—WàóÏ¿‚F£Á¢Eáëëhllı´Ìœ1[·nƒ¹¹9.ºh9,,,ÇŽ¥ÃÆÆ�8pà""f">>¥¥eظq||½±â¢tâ8pàÒÒ2àáé�/X*,¤ªT*±ióVÃÑÑsçÎ�‡‡;’“£±±	¿þöÄb‘^}11³ÑÛÛ‹´´DFÎBEE%d2)BC§êœ+**Ƽ¹s`nn†ði¡:uìÛw™™YHHˆÓ{ßZZZ°aÃ&H¥R,]ºÉ)G�+¬¶ßØØˆ¿þÚ‰¶¶v,\8��“¡V«±gÏ~Èåøõ·?^°RéßÞ€ˆˆh\û[{j4Ô×7 =ý8¶l݆�»ö ãø	TTVŽHr5˜””#˜5GSÓ�–~³£æàС@vvV¯¾	W_}Š‹Kñå—ßbÞü% þwßû7¯¹¯®}å¸àÂK±víX³æÔÔÖâᇟijϽ$<ë™g_ÄêkoF]}=>üà?˜7	zzz«¯½¯½ö&äò?‘…¯¾ú½½½ÈÍÉC{»ÉÉ)HI9¢¿™™âc±{÷^ÀîÝ{‘””ˆ¤¤D�sáá¡pqqFyy%®»îáþ^x7Ý|êêêñøãÏ`ûö]BY{{;ââ`ãÆ-ÈÏ/ÄêÕ7cÕªëQ\¬]‚ãèÑcˆŽ™‹CɇQYU……ç-Ç–­Û´Kkde£««ÉÉ)HNN9ã.^""¢ñì´š ”J%ŠŠŠQ\R*$I$¸¹ºÂÅÅŽŽHN9bðº³éÉ'ŸÃ}÷Þ‰Gý7ÀËÓ=ü8öìÞhhhÄÛo¯GHH04
¢c’ðí·?ã›ñÝ·ŸÌÍÍñæ[à,--±`Á<ÜvÛ=xú©ÇPUU�uëÞDFz²ÐÊ=Ÿ~ú%n»m
¶lÙ†Û7!"b¦N|×^w5jëêðòKÏ}
óæ%aÏžý¸ï¾»°{Ï>,˜?®n®xïÝ�„sóæ%éÝ×ÐЈW^]�ô´CBL³"ú[±>þøsNÀ÷ß	@ÛbÓ_Ïý<Œ'Ÿx7Üp- &&
�>ú–¦Äm·ÞŒ#GŽ7i
;Á*++GVvŽÁ%,--2žž:K3ˆD"ÓDyIÅK/=+Ÿþyxñ¥µÂ±‡‡»0P[$aáÂù8––�ëO–0Éáa¡°´´ŽëëiéðóóºLÅb1-Zˆcié€k®¹K–®ÄÅ_ˆå˗⢗
ºlÅ@óæ%áõ7Þ�Z­ÆîÝûðÔ“�ÂÑÑ×\s£pnàkì“••ww7�˜æÎ�#”§¥Gbb¼p1SxmJ¥ÉÉGàîæ†={ººº����Æðd"""2lX	V~~²sr
–ÙÛÙ#!!vÐ%F’X,Ö®Ñht½�e}åbqOéÀqEb±Xï¸ïþSŸsj]ï¼½kn¾ü±�>ú¾ûöüðÃWÃz
11³ÑÓÓƒM›·B­VÃÇÇ8›6oEaaæ
HœŒ½ö¾˜ú˜››¡WÑŸ «Õj�‰"‘W\y|¼½„s÷Ý{ç°b&""¢~CŽÁjnn6š\ÀŒáF“«Ñ§ƒ-[¶	Çü±1ѳ…ãÚÚ:?ž)Ä÷çŸ!úo,é1k&*+«�››@Û´eËŸˆ‰Ž M^f̘†Ç{~ð6vîÒŽŸ²´´D]]àu÷�ÃzþùW�””(œOJJÀóÏ¿"Œ¿:Uxx›„˜T*vîÜ#”ÏMJĆ
›„VÈß7lÖ#“J¥HLˆCaAbc£…Ñѳ!‰`ii‰úúÆÓ~Ÿˆˆˆ&¢![°ú@bffGGGƒe½½½£²˜è‹/>ƒeË/FqI)D"6oÞŠMÊÝÜ\ñÀ� rö,$'†““#.¿üÒÓ~Ž»»ž}æq,Yº—^z1RRŽÀÃÝ×^{z{{�.X
klذ	×\s%`vä,tvvbáyËàíí�¯¾üØ`ýóæ%áég^Àm·Þ,œKJJÄk¯½…Ûo¿Åà=ŽŽŽBLW^ùO¤Mƒ³³“P¾jÕåøßÿ~Ãô1
œG8883ßzë5¬¼ør=šŠÉ�(..EgG'~ÿý'L�:..Θ“´¾¾>øô“„îE"""Ò% Q*Œo[³cçnÈårƒe8ñyË*+«p4õ˜Ñz§…‡aòä€Ó
Ö˜ƒ“1uê89i“‰††FìÝw�s“…Öž}û⦛oÃá”=ؽg$	�·@H0rsó`ii	??_ÚÖ®ÚÚ:̘1
ÐÓӃÇS1gNÿ8¦ÌÌl¤§k—i˜?/Iè",,,Bjjºººª3¶«««™™ÙèììÂܹý-TÕ×7 3+‘3…%ººº�œrÁS‚àåå	@»è±cHLìÌžšš†¼¼|ÄÆFжdõ-lªÑh�Ÿ_©T
''G¸ºù£¹©666B}ûöDuU5¼¼½0'1^H¤zzz�••ƒÖ¶6$ÍIö˜2""¢‰D*³:ÁúkûÎA—^Xºd±� ´wï4·4½oJPBC§žVÀgª/ÁÊÍIÑçŽ%ï½÷"#gA£Ñ`ýú· •J‡=6Œˆˆˆ†&•Ù
ÝEhan1h‚•��‹™3§Ç�Ç�gš\@Ee%ÐÐЈú†ÌIŒ?ëåíííuVŸ1ÖuwwcíÚ7 ±±1¸ûnã«ÊÑß3dVqq	ŽŸÈ´GG¸¸8C­V£¶®NèR´¶¶†X,F{{ûàAˆDXºdñ˜™‰HDDDôw
«kÒ$TVV£©Ùø~vÍ-Íz-V®..ˆŠŠDkk<4è3&MbrEDDDãÆ�-X€v	‚¬¬”•—ë­³t*L	
öÿ´{¦g7xoÀ¤I˜6-lT%%"""2µa
r¨··55µhkkGoo/Ôj5¤R)¤R)¬­­àìì$Ìx;UOOÊË+ —w@¥RÁÆÆÞÞ^Âì5"""¢ñà´,""""œTf7ôJîDDDDtz˜`™,""""c‚EDDDdbL°ˆˆˆˆLŒ	‘‰1Á""""21&XDDDD&Æ‹ˆˆˆÈĘ`™,""""c‚EDDDdbÒyˆÌn$CDDD4$¥¢í¬?cD,èlÏ©Gde<"Ïa!‘‰1Á""""21&XDDDD&6bc°ú´Ë;ÐÚÒ…R	…B	¥B¡ý¯R…Rûµö¼îî.˜93l¤C$""":##ž`UUÕ¢  xX×j4š³
‘鱋�ˆˆˆÈÄF¼kJÐ$Lò…R©Dcc32Žg�tDDDDgÕˆ·`‰Åb˜™É`ee	__/ØÚÚŒtDDDDgÕ¨wZX˜�vDDDD&5ê	–X<ê!™Ô¨g7"ˆF;""""“õ‹ˆˆˆh¼a‚EDDDdbL°ˆˆˆˆLlÄ×Á:Síòtvt¢§§b±æ°³³…™™l´C#"""p%Xee•((,EWW—^™H$‚³³#‚'ÁÙÙq¢#"""ê7æ,…B‰ÔcÇÑÐÐdô�Fƒ††&444ÁËÓ3f„B"‘Œ`”DDDDýÆô,¥J…”Ãiƒ&W§ªª®ErJ”JÕYŒŒˆˆˆÈ¸1Ý‚U[[/|íëãoXXX ««ÅÅ娪®5x_ssŽÏFDÄ´‘
•ˆˆˆH0¦[°úbÆŒPØÛÛÁÜÜöˆˆ˜oO£÷TU×¢ººîŒžûÒËæÌê0…Ï>ÿ©ÇNÇ
…/¾ôŽÁs��Íxý�ÿॗß5Zßü…—áDfîÇUQYƒºúÆ!¯{쉵økû>ƒe�˜6]]ÝgOQQZ[Û…ãÖÖvL
›…B1¬û;::±go2Þzû¼ûþ:e*•
+/¹	��Íg'�c>Á²³³EP ¿Á²�� A·ÚÉ/(>£g¿øò;¨©©ú³ìhêq|ñåOÂqjê	¬[ÿ¡Þ¹·ÞùvˆˆGTÔ£õUVÖ §§÷Œãzmý‡øüó½&;»lü$,W«Õ(/¯‚F£9ãxn»ã1ìÞsèo×ýÃ�pÿÏaÃÆíxï”K"‘༅‰X»îƒ3Ž“ˆˆÆ¿1ÝE“ü} ÞNÇÜÜn®.¨©5ÜÊÔÞ.Gss+íÏ8ެì|€T"Áî=ÉðÅ¢óæå[¶îBB|:в²*,:/~B¹Z­ÆöûQXTŠðÐ`$%ÅèÜ7É)ÇPQQ�o¸BçÙs“bðÊÚ÷…ã½ûRpíêK±w_ŠÎ¹„øÙ�H$ðpwƒJÝ?­³³7m‡R©Âù‹ç꽶̬<ìßAAþ˜6-ÙÙù˜77N(?‘™‹C‡Ráèh�åËÂÒÒee•()©@kk;~ß°
®ˆ‰ž¥W÷ÇŸ|‡+.»H'nhh¦Í;aggƒøøÙz÷TTÖ`×®Ðh4X¼x.<Ü]hg’ÖÔÖÃÝÍ;v€‡‡+–.™‘H„̬<466ãð‘tÀ” xxhïëîîŦÍ;ÑÜÜŠåËÂÝÍEï™pã
WàÆ®ÀÖ?wãÞûŸÕ+¿òÊ•ˆ˜½O=y/¬­­ÖADDœ-X..Nƒ–;:
ž<ÕŸÆùÁ|õõϸýŽÇðØ¯¢²ªwÝó”NkÆMkÄu7܇­[wãø‰lÄ%®Dqq §§‹—^�ÿûÏ7hkmÇO­Ã}~�ß´æA\½ú.ü¾a**«õž�”‹œœB¡;nÏÞd¬ºb%ººztÎÍMŠ|ûý¯øâmëVWW7œw~úïF•áºîCGgÿR[ÿÜ�¥ËW£¼¢
¿þ¶W]sþýàBù+¯¾‡k¯¿õõMؼeç^‚öv9êêQ[[�ŠÊ¤NC~¾áÖÂ
ÿ‚ùñÂqyyâVâPr*Ž¥ebÍ-é\¿qÓ,\tòò‹‘•]€9Iÿº3ÿÚ¾ÿºíQÜvÇ㨩©Ç˯¼‡Õ×Ý(-­D[»%H9œ†òŠ*¡Î[o‡�¤c×îCˆ‹_�öv¹áoòÜ\�áë㥓Ø2¦[°ÌÍÍaii1è56C´$´´´š,…B‰Ÿ~ø"‘s£qÿ¿ŸÃCÞ&”/Y2·Þr
 ¹¹
?ÿ²ÜþïãoagkƒŸ~øpÇí×!$|î¸ý:M,]ºwÞ~�Áçzz¸!(höíKÁŠ‹#-=³gOÇœÄháÜÁC©xò‰{ôîýêëŸack�¿×&ƒÇÒ2‘˜t‰Pþ̳¯ãµµOàŠË/<üèËØ±c? °°o¿ûާmÖ»öúûðɧßãÞ{nFll¼<ÝuÞƒ�›QQQ�ààɹ7ÞüK–ÌÃ{ïh“¸ï¾ÿMŸ¥R©p÷=OáËÏß@bb4ÀÛÛ/½ü¾ýZ;¦¬¦¦»wü;;Üuçõ	_€ä”4,_¶o½ý	V]¹+.Z|ò{ ýÞßpýX¼(	�˜t	þÚ¾—\¼Ä`ÌC	žŒŒŒl,]2ÿoÝODDØN°¬¬,‡¼Æbˆ¬»»ÇTá >n¶Ð]0ÉWo}€€þòääTÔÕ5`Í¿ÊE" ;§@H°¢}öܤXìÙ›ooO„†L�™™æ$FçD""
ÌšLKÏļ¹±ÂqĬpØÚZÐ&4'2ó©óú¬#G3 •JðÈc¯å……%°°04Ö>ryÀÎÎF8w,=·ßººÿu
ˆ­¬¬Õ5uøüËÿâó/ÿ¨«k@iY¥pͬ™áB}66Öˆ�™…´´LÄÆèwO|M}|Q]cxöépØÙÙ ©ÙtI;�Oc:Á²0úùPŠ*JS…©¬ÿí‰Dzƒ§eR™Ñò˜èYXµj¥p¼ææUœÜ?x¨×:7)¯®ý¾>^˜3GÛº3'1o¿ó)|}¼„ñW§‹ÅP«uãì;‹Å�ɤèííŸe×Û«;øÝÝÍkn^¥sÎÙix«åÛÚj¡¶6¹ÐÕ{j<šSb“H$¸ñúË!‘ö¿33³þë5ú¯e°‰ “
üß\ÿûv:ÚÚäð÷óþÛ÷ÑÄ0¦Ç`‰Å†·ë\cd|cäGRÒœXK;�¨Ù3=1ѳ5NNï#)Ù9øù—͘s²û,0Ð--møù—ÍÂø«SÍŽœŽ�;IErJ:::hߛĄ(üüËfáú_~Ý"|�Ò²J¸º8	qÇDÏB@€/ÀÒÒõƒ,Óàää??oääç¢"§cûÉ2:_OšäOO7ÔÖ5èx7¯y›6ï„‹‹òó‹àíí�7_×¾>_O\yõˆ�‰À–-»°rÅùB×èòeñüoá¯íûpÉÅK°x‘þŒÉÁdeçãÞûŸAsS+jjêpþ²«1%(@/V[×€ªêZ!Á%""2F@£T´�Õ‡HevèlÏ3Xväh†ÎŠíy{y`Ö¬ðAënk—cïÞd£åžü{+ºïÝ›‚ˆˆpØØX£¸¸
0y²vé…îî=š!Æ>xð(fÎÆ�•””C¥R#pÀ^‡�¤#?¿VV–ˆ��€§‡›Á{�ÉÌÊCGG§Îr¥¥(-«DbB”ÐExê³{zz…–¢¹I1ÈÌÊGXh�Ð…×ÕÕ�ܼ"Lò÷Á?nÀŽ�ûñÃwýËB”•UâÈÑ(J„„bæŒþœúú&–ÀÖÎáaÁz1çåáŸWÜŠ´£[„„¹¥¥
;v€““=âb#‘r8
s£…ò¦¦J>†ææLò÷All¤R)>ýìlÚ¼ï¿÷öí;G½–»ŠŠj”–VÀË˾¾^8pð(’æÄ-™ÙÙ°³·�·—‡^¬mmr¤¥gꜳ±±¸·ßù5µ
xé…‡ôî%"¢sƒ•m0F"ïõ+åpšÑn77DGÍ´îúúF¤N3Z>mZÇÌ¢  ‡’S1mZJJÊñàC/àõõO㢙ì�?¹æ'è¬öwô%Xÿýñ#E6|*•
ÿøç-øì“õ§ÕµKDDcËH%X£ÞE8Ø€ãÎë5#?9žÈ˜¡Öњ謬-‘r8
?þô\]�ñö[ÏaÙÒ&}Ƌϛ¦ÅÇÃÃ
ááSMR×é’H$øí—OFåÙDDtîõëÔnuttB¡Pž2LWs“ñ)ó®®Î°ÆR™—§;Þ~ó¹ÑcX–/[€åËL›ü�
£>‹P­V-Óh4FÇgÚn›Áf�O	8£ØˆˆˆˆþŽ1�`@aQ©ÑkJJ+ T^ç*((g¾!Ñéõkà"—€¶[Ïýäæ¾€v5ðŒŒl½$«¡¡	yyEëôôpgë�šQƒ5påp+++DÌš‰DŒcÇ2QS«Ýj¦²ª�M-ðôp…D*Ekk›Ñ®Á  L°÷ÑHÕ«»»Gh™’J¤ˆŠš!h�Œœ†¢â2äçC¥R¡»»Å%åFërvvDhHìííF$v""""cF9Áê†ùÉ}æ¦O�­�µP&‰8Ù¾>^¨¬ªAScºº»ÑÓÓ�Fss3XX˜ÃÙÙ®®Î:÷�¦Q_h”ˆˆˆh¤ŒÔB££>È�ˆˆˆh¼a‚EDDDdbL°ˆˆˆˆLŒ	‘‰1Á""""21&XDDDD&Æ‹ˆˆˆÈĘ`™,""""õÍžMÅÊ6x´C ³Fb'…ñò3È÷Šht�—�_ÆM‚õÕ¿�vDcŽ«çŠ{Ö¹þ3È÷Šht�äÏàÙÆ.B""""c‚EDDDdbL°ˆˆˆˆLl\�Á"¢ñA¥RC¡Pjÿ)UP)UP(•P)µç•*”
”*
‚§øÀÖÖj´Ã&"0Á"¢1§¸¤•þ> Àó,FCDtúØEHDDDdblÁ"¢1ÇÓÃv¶VP©ÕPô*QZV‹ŽŽîÑ‹ˆhؘ`јcmmkk�ãÃGrG1""¢ÓÃ.B"ó¬¬,†¾ˆˆha‚EDcžX,툈N,"óDb~Tѹ…ŸZD4æ‰À,":·LøAî
…
MMmhll…¼£½½J(
@$‚L*…••9ìì¬ááîËÑ—ˆˆˆÎ6ÁêììFII
**ë¡Vk^Óäò.ÔÕµ   ÎNvžê{;뎖ˆ¨_ww/êë[ÐÐØŠîî^ôö*¡V« “É`n.ƒ££-œ�íàè`;Ú¡MX.ÁÒh4(,ªBaa%4ò*''[ØÛÛ@$š›ÛÑÜ,Ê›Ú�œœ…™3‚àîî8‚Q�o�==
¨Tj˜™É “IF;¤1©µ­ùùhhh5XÞÓ£„\Þ…ÆÆ6TÂÖÖA�>ü¼"*ÁR©ÔHMÍCcS›^™¹¹3gN�“£î_|騮nŽÕj
Ò3
νψ΀R©BUUªkÑÒ"×ùƒÇÜBwWGøû{謇5Qi4äT ¸¸ZïC™LwwGXX˜C.ïBmm“pM{{Ž¥åÃ×Ç¡¡“8“hM˜K£Ñ -½À`r%³fMÑkNW©ÔhkëÔ»^­Ö ¸¤3¦žµx‰Æ³êšFää–¡§[a°¼§[�²ò:TTÖcJ�||ÜF8±C£Ñ =£55MzeööÖ˜33™p®©¹GŽäè}(¯¨‡B©Â¬™A#3M Y„yù¨¯o1Xæííbp¬B~A…Ñí9
7ÑÑàòòË‘ž^h4¹H­Ö 7¯#ÙØ”_Pa0¹’HĈŒ˜¢“\€“£-üüÜõ®¯©iBIIÍY‹“ˆtMˆ,¹¼%%ÕFË}}õ?Œ ²²Þè=½½Ê3Ž‹h¢ÉË/GQ‘áŸE‘
ò†¯�d2)ZZåÈÉ)CkkJËjG8Ò±¡¡¡Õèûåãã
ss3ƒežžÎ“©‚Â
x{»rŒÑ˜-XÅ%ñ!Í
 IDATúãú˜[ÈŒÎ
l¼‚¥¥¹)Búõߣ¦Vÿ¯Ó‘öÕ7"=½@8V(”XûÚwÏ55µá�÷þ‡uë¿7ZßÒDVvÉÇUUÕ€úÃ-�=óÜgعë˜Á²ŽÎnDDß„®®ž3ާ¸¤mmÂq[["¢o‚B1¼„» °ï¼÷?Ü}ï[xêÙO‘™U,”©T*\qÕ3h2Ð�=46O`Ú´Éœì
33D"l
{û‰;k77¯Ìh™»»“Ñ2[+ˆ||)•j”•³‹h$ŒûK¥Rl^ïcce|m+wg£e~FZ½N׺õß¡¶¶Ù$u�‰´´||ýݶþãô¼õÎOzçÞÿðWØÛ[cæŒ DFL1Z_Uuz{†îÊ›ïü„¯¿ùsÐkrs˰ek2æÍ�i°\£V£¢¢Þh’}:î½ÿìÛ\8V«5'ë^å�<ö*+
3™K–ÿÉ)Y‰D‚ysgá�·~<ó@Ç�FƒÌ¬£åNN¶ðörÑ;/‘ˆ1}Úä³ÙØU[ÛŒöö.£åvƒL²‹E�ÉwPöyHD¦3î»[[åP©ÔFË-­7±@p°/z
�Y„b±“&y`Ò$“Æ	9¹¥©D‚}û�ÃßßæGåÛþ:‚¸Ø0$§d£¢¢óçG`’jµ»v§¡¸¤!Sý‘˜0MçÞØ˜P>’ƒÊª\{Í�g'$LÃëoöÿb?pðV]¹žÐ9
‰Dw7G¨Ôýïkgg¶ü™•R…óÎÖ{mÙ9¥8t(“'{!,lrsË0'q†Pž•]‚Çs`ï`�%‹c`iiŽòò:”•Õ¢­­›6‚»»#fGNÕ«ûó/·à—̃xÀv*��mغ-v¶Vˆ‰	Õ»§ªª{ö¦C
.˜
w7í4öòò:ÔÕ7ÃÕÕ»÷¤ÃÝ݋ϋ‚H$BvN)ššÚp458ÙKhEèéQàÏm‡ÑÜ"Ç’ó£áæjxZü÷ß<
©´¿{¦²ªÿûe7bc—ýs>’nÇ#_ëq´ÁqMm:;�· úúÄncc	gg;46ŽÏ–=cjëŒ'BffR�ÿ�1–ô··w¡W¡„™‘ŒˆLcÜ·`ÉåÆÿ©Ôø‡ŒD"ÆÌA˜›4‘S5óçG xН©Ã|÷ÃvÜ{ÿ»xú¹ÏPUÝ€?ô¾NkÆíw½Ž[n[‡¿¶Á‰¬b,XtJJµÍý==
¬¸äQ|úù&´µuà¹>ÇÃ�~¨sï�k^Ŧ͇PUÕ ÷ìÄ„éÈË+ºãö8ŽË.��î®^�s‰	Ó?þw'¾ùVÛºÕÕÕƒå=ˆ_~Ý‹â’ÜrÛkèììŸð×ö£¸øÒÇPQY�
àÆ›_ÁcOüG(_ÿÆXsë:Ô7´`Û_G°héý�Ë;ÑÐЊººTU5àÈÑ\T|ß6o=„¹Iý­Wõ˜¿èn>œ�ôŒBÜy÷›:×oý3ËW<„‚ÂJää–aÑ’û„îÌ�»�á®{ß½÷¿ƒÚÚ&¼¶þ{¬ù×:@Yy-Úå�(,ªÂ‘£¹¨0FïîûÞÂÑÔ<ìÛ—�çݹ\ö)½_Šmm°µéo‰puq€�·+8qê­ç´ŠrããE"ÀÅÙaÐûûà‰ÄØZW}ÊËëŒþËÊ*�B¡2zïPŸ‹DtæÆýŸ0=½ƒwSI$Cç˜VV°¡Ö¥R‰¯¿x"‘ñqÓðèãá¾{.Ê�…›n¸ÐÚ"Çïöáî;ÿ‰Ï¾Ø[[+|ýÅ€[Ö¬@dôM¸eÍ
Nö,^…­Yað¹îNœì…Oà‚eñÈ8^ˆˆYS.œKIÉÆ#]­wï÷?l‡��%¾úüq@zF!-¹O(镯ðÒó·àÒÌ<õÌ'ص;
PT\…>ú)>‚““�6ö[×á‹/·âŽÛ/ATÔTxz8ë¼55µ¡²²S‚|„sï¾ÿ3Ÿ…×_»ðÓw
ã³T*xè}üçÃðòtÁºõßá³� íšÙºñ5ØÚZáÖ­DdôÍ8r4KÇàý~Ååÿ\€åËâ--Ú…hW_½Dj¿GKïÇÎ]i¸èƒ1÷Ù¹ë’S²ðÚ«·ëœŸäƒ™EX¼(jÐûÏjµÍ-íFË­¬,†tmï`cê°Æ4…B5èDšÞ^å ]®CÖÏI:DgݸO°D†Fz Tÿ+o4ÄD‡
1ûû»ë�—èëJÒ–{å‡�ä ¾¾wÞÓßZ#‰�—W&$Xq±áƒ>;1a:ö8o/L
öƒ™™ñqáÂ9ˆD˜9Cí¯Œã…:Ý}3g
û6ªT*de— &º¿›.6&LH°ŽˇT*ÁSÏ~*”W�u*y‡ö/q[Ûþ±tÇqóM
Çc+¯¨Gmm¾ùn¾99¾¬¾¾eåý³ÔfL‘µ±¶Dôìdd!jvˆÑ8bc|_üÜQSÛ8hÜÇŽåãÖ;^Ãÿ}ð <=uÇúÙÚZ¡¹EnäÎsO[{‡Ñí¨€áM±²?Ý¥ÃÑÓÓ{VëW(™`�mã>Á2?e�˜SõÑÂ5Ò¤ÆEˆD"½q²]LÚòþ²Ù‘Sqùe„ã®[†€IžÂ±…ùàïEBÂ4¼ñæOðñvEB¼6‹�ÇþoWaüÕ©Äb1ÔjÝqnš“¿PÅb1d2)z̲;µUÑÍÕ7\·L眣ãðöPëë^ko³ìä3EÂóèÅ&‘ˆ±úêóuºë>õ=WkÔC®€­ó}�hÐõé…XµúY¬{åv¡Õk ööÎAÇ$�k†ZïÊÌlè�!©tÜ�fÐ1Ô¤	KKsDÍÖ�8\§®�ED¦7î?µ,,o	1¶�¨1�µµÍc®å+1~:Ò3´Ýz³#§bväTDF;Q´-X¹yeø}Ã~ÄÇiÈOðBK«¿oØ/Œ¿:Õ¬YS°{OºðKáÈÑtœƒ%‰†ß7ì®ßðÇ~áëè蔕×ÁÙÙ^ˆ{väTað¾¥¥ù cQmáëã¦3�=bÖ¡…víéÿÚßÏN¨¯oÑyÞ´ðáš´ô47k»´ÚÚ:�r8³´3&­¬ÌÑ0Œe#Œ9‘YŒ+¯z/¿ø/¬¸(Ñà5¹yåãjæÜPÄHÄC‰D"ƒËŒWæCü1¤Ö¨ammñ·ÿq,¢³oÜ·`9:ÚB,í¢�Ë» Tª†œ‘Ó§¾¾ÇÒò!‹ÆÔ¦Ï×®^‚í;Žâüe`Nâ´·wbï¾lÜð*\]@ܧoVfV‰NwX|l~ý}Ö¾r›Áû®¸l¾ùv.½üI̘ˆ™Åp0fæùgoÂeW>�]»ÓÐÑÙ�Щþ¼Ÿ¯;{ä\¸ò\tA$1Ò3
pÅåqÍUçã¼³qó¿Ö¢¤¤ññá¸óöè=ù²8ìÝ—Ž„xmRxÇíÿÀ=Œ5ÿZWW�E"Þ~ãÜ~רúça8;Û¡ °^^ÎXû²öõùx»âú›^BÔìlûë.\�™'·Y²8¯¬ý;vÃE&à¼ú3&sÇ]o@©Ráó/6áó/6fG†à©'®ÔÕ7£º¦QHpÇ…!#õ0–¸Ðh4&Yfã\af&ƒ™™Ôè8¬ÞÔj
÷$ÃD4JÅÙ�þ,•Ù¡³=ï¬>ÃÊ6õÕ¿,;ššgt›˜1c2¼<õ×à1äPr&ZZ: óæÎ²…l(ûœÀÌ™�°±¶DIi
4�Э×ÝÝ‹ciùÂ`ìä”,LŸ++형Ҳ¨TjLðêK=–‡‚‚JXY™#**'—8õ^c²sJÑÙÙ­³BYy-ÊËë&tžúìžvíÑ$OL˜ŽœœR„LõƒÍÉ.¼®®äTÀßÏÿûyví9†/?{\xFyyRÓò T¨ì«Ó‚ÓÐЊ¢â*ØÚZ!4Ä_/æ‚‚
\sÝ8°÷}a©†ÖV9vïI‡££
¢£Bq45ñqáByss;RŽd£¥Y??wDG…@*•à˯·bëŸ)xóõ»pð`&œ�íôZî*+ëQV^OOgøx»"9%	ñÓ„±s¹¹e°³³Ö[Õ÷ý9uÁSGG[„…N¼ÿᯨ«kÆ3OÝ0è÷ét¸z®8ë?€ñŸÁÚÚfKË7zŸ»»£ÐBhL¯B‰;R�–ÇÄ„êmÔþwŒö{5ЉÌ"TTèÏøí“?
vvÃßp¾««YÙ¥ðõq…««Ã�ãS‰FÃHüZÙc$òžqß‚��^ƒ&XEEÕðôpò§¼¢--ÚU¼=<œÎ8¹ ³VÕÀ5­m÷f_rèp?ýµ¸"#‚¬wþÔ{�1”Àøùºë-¬zê³ÍÍeX²8F8ØVXT…dz6	{÷eàÍw~«/ýKç~__7£ãŽ\\ìáâbo4æ  ,[‡Ý{Ò…uÃìímtºàNM’muâ=•«‹ƒÑ.ª16«×Tœ�íè…ÂÂ*ƒå--Øà8B¦úÁÍÍQgÐ{OO/ŠŠªQV^+|†…„øñ�C¢2a,0“I5EÅU(.®†R©¿G¡F£í2”CẇX,Bh¨?|}˜\�.GÛA÷¬®n„�·«°š…B…'Ї¬àÖLãÍ” ¨Tj�Ù°uw÷"-½R©ÖÖ–�J$èîéÕëZžâÃÏ/¢4¡,@Ûì8Ù~¾¨¨¨Cm]Z[;ŒN—JÅpww” “j'š¨fLŸŒýN\z@£ÑÎö
	ñƒ§‡3Äb1ZZÚ‘•]ŠŽŽnH¥bØØXmÉÊ/¨@ggÄ'·¾òóuÖ
ñçŠ�©~pu±Ç‰Ì½Y¨}”J5Z[õß33)ÂB'ÁÃÃél‡IDL¸«�L&A@€'<¡P¨ÐÞÞ�Þ^%
% dR),,Í`ogÍéÌD&`nn†ÈÈ`¤¦æL²T*523K�™Y¢s¾o͹šÚ&£	–R©FiYÿvG®®ã*Á´Ý…s“f ®®åäŒævƒ­ð}¬­-àíå??÷a¯óGD¦3a¬�d2‰^×™žƒ½
âãÂqâD±Ñ±�™›K1sFœœìPSk|×D!‰àîîwwGh4Èå]èêê�B©‚J©‚X"†¹™¶¶Vlq'eL°ˆhDYZš#::uuͨªnDCC‹^KŒµµ¼¼œáçë!Ì�sws„­�$R1$1¤	¤R	Ä'¿–HÄ�H%�J$b…s‘H[[+acr"[˜`Ѩpss„››¶%F¡T¡·G‘Hss™Á.-7·±±-Ñp0Á"¢Q%‰`&“ÂLÆ�#"?†ÞÆžˆˆˆˆN,""""c‚EDDDdbL°ˆˆˆˆLŒ	‘‰1Á""""21&XDDDD&Æ‹ˆˆˆÈĸ²Ñ8äê¹b´C q¨¾ú÷ÑáœÁ‹ˆhœ*Í[?Ú!Ð8âüÀh‡pNa!‘‰1Á""""21&XDDDD&Æ‹ˆˆˆÈÄ8È�ˆˆÕÕÕ‹¦æôö*¡P¨ •Šamm'GkÈd’Ñ�hLb‚EDD54¶£°°mí]ËÅb1¼½8Ù�‰Ñ)˜`‘µZ�™•¨­kÕ+37—¡·W	�FµZ�òŠF45w`vä$˜›ñW
QŽÁ"""�Z­FjZ©^r%“I€9	S`mm®SÖÑÑ�¼¼ê‘“hÌc‚EDD‚ü‚Z47wè�õ‚££5
Šê —wë•×ÖµA©T�DˆDç&XDDhmëBYy£Þy33)\]lUU-ïÕh4èéQžÕøˆÎ%L°ˆˆ&W`cc‘HP©Ô¯‰D0ã,",""‚J¥68¨$âþ_n®¶¯qw·çLB¢˜`ZZ;¡Qk–I$"áë�©^pu±Ó)wu±CèTϳѹ†í¹DD„––N£e"q‚%“I0k¦äònt÷(`meKK³‘‘èœÂ‹ˆˆÐÛk|€º"½s66°±±8›!�ÓØEHDDP(¸Ä‘)±‹ˆhª®iAgg¯pÜn`m«�e…Euzç]œm`oouVâ#:×1Á""š€ªª[ÐÔ$Öµíí]h7°¡L*a‚Ed»‰ˆˆˆLŒ-XDDPÐd7ôø8	ÇÅÅõh3ÐJŽŽÖðóuÖ;o{¹ñõ>ÄÅbjðè.û°åÏã°²2ÃÜ9S…s¶ÁS<ôÎ-œŠìœjìØ��õ¯\i°¾»îÿ+.˜…ÅçM;£¸ššäèêVÀÛËñŒê¡³�-XDD�½½Ü\í„ææ2£×ZZ˜é\Û÷ïl,ÏðÅ×û‘›WcòzOWYy#>úx§pÜÔ$ÇÚõ›ôÎ=ÿòï°´4Ã$ÌI˜b´¾††vtóöwýº!ëßÚrÆõÐÙÇ,""“**›P_ß{ìÚ“{{+,Y4
‰¶m 9¥“&¹¢¬¬Ù¹ÕˆŠ@X¨—N‡�#+»>ÞNX0/b±î½¥¥
ÈÌ®ÄÕWÆëlõˆõom�B¡‚L&Á¡ÃEX¾töìËÓ9ççëO¨Õøû¹÷«Tjlß™…¦æÌ�¬÷ÚÊ+š°w_.\]mˆ”ÃEX´0\(/+kÄÁ”H¥,œ
GGk47w '·UUÍØºí8¬­Í1'A¿nÆU‚åê¹b´C ""Ù³/Ÿ}¹^ž˜>Í_}síÈÄ믮¼¼îXY™ÃÃÃÎN6X»~þïý�¸ç�oPQÕŒsCðÕ·ûñùWûðå'k ‰ðòº?`n.ƒ››<ÜíõöXõ†L*AZF¢gàPrâ§ ¡Q®s.>6°ÿ`>6nNǯÜ|Û§h—w#)1<ü=*«š…ºsr«qåµ`å…()kÀç_íGÊáBäg®üòûQ¼öÆfüceÚå]X÷úfüøÍ퀊Êf46uàXZ)œ�m˜`�aã&ÁêlÏ툈ÈÄššäøý¿÷ÀÒÒWü3󿌵/^©T»ïað<óÄÅs~ùí(â‚°cW6NdU`ë†C*•@£Ñ`ÙÊõع;ç‡
÷>ÿô?>W"#&j2%h“©”BÜtý\ÔֶꜻuÍB½{÷íÏCVvöíx2™׬’#~îóBùÛïoÃuW'â¾»—¾þîR:;{ðôs¿à¿ß݉à);[K¼ÿÑv¼òÂåX´0'²*ñȃšè¦³eÜ$XDD4þ„‡yc½|}œ R©ÑÔÜ7Wí~ˆÑ³„k}}��q¢p$µJ¥=þ£PÞÑÑ‹¼ü!ÁЉê¿×�¸Ø@ìÞ›‹kVu µµþ~.ˆ‹	Ä›ïnÃ5«:�›W#´`
t"«Q³„ͯ��l„d	²²«pÕåqÂñÀ×�_P‹®®^|8`¬WUU3z{¹칆	�Y}-U}D"‘ΦÔRiÿ\-±HõÉ2‘H„ @w¬^• ”¯^•Oáxà˜+CúÆaí;�‡è“ÉØŒ~8‘U�}ò„ñW§‹Ezgk4ýÇffôX9à×"‘ææ2\³*Agƒ"î÷xîa‚EDDãN\Ìdü÷çÞâkksá¼Z­ä.]}ã°>úd®¼,`n&ÅÔ)øè“][¯`æt_üß'»ÐÝ£€…¹µumÈÍ�[Ò…–´�›Ó…²à`H¤bôô(¤·…¹��Ã[ –F,""w’§bÉâiX~ñëX´0½½J>Z„ž¹Q‘ƒw
ö釵}W^_»J8ˆ·ß߆¯›kð¾Ø˜@ÄÆâÒ+ÞAÒœ©H=Vïþ5Çî¼m®¾þ#\té›°°�!Àß²“­iæ2¬}ñrÜuß×X´0¶¶–È˯Ax˜7º9âã‚ðêë›pÍ
aj°'ž|”“»Æ*�RÑvV"•Ùq:Ñ8geŒúêßG;Œ3âê¹bD>«Fâ½rõ\�Ò¼õþ>-½õ
†xy:"<ÌÛT¡
*=£ÞÞŽpq¶Em]ZZ:t=”RˆÈYþ03“âDV¼<àäd¨ohGCC;BCú—jÈ˯AVN¤1¦…û`’¿v)…Sï5¦¬¬U5-ˆ�ž‘HÛi×ÐØŽü‚ZL÷�ÍÉÅVëêÛÐÔÔ��©ÚX5
öÈG›¼qÑ“QSÛ77[¸8ÛÐn®]PXWW[dçTáÙÃ_›žÛÐØŽÔc¥h—wc’¿"fú	KL´µw¡°°"‘³fú�Ñû}:üƒ‘ÿoÏöÏ •m0F"ïa�3gô'învpw³Ó)�‹éï–›æ£SæêbW[�sÁSZŒÚÚV$&LAôìÉB}66øè½ëq(¹±1�HJºç%…`ûæ‡PRÚ té}üÁ
ðõqî¿âŸ±FZ}ùùËp4µuõm°³µÄËÏ_w7;¡|מDÌò‡Ÿ¯³Éß'û˜`јagk‰»n[„�›ÓqÕÚV,s-®™1IçKK3Ì�3Õh�¶6X|Þ4ƒe.ζpq¶Ž£"u×÷˜lªwà:X§Ú¸9Ý¿Üh9�oL°ˆˆhLY}U¢Iê±³µÄ¬~&©ëïxcÝU£öl}äNDDãRX¨“5L°ˆˆˆˆLŒ	‘‰1Á""""21&XDDDD&Æ‹ˆˆˆÈĘ`™,""""c‚EDDDdbL°ˆˆˆˆLŒ	‘‰1Á""""21&XDDDD&Æ‹ˆˆˆÈĘ`™,""""c‚EDDDdbL°ˆˆˆˆLŒ	‘‰1Á""""21&XDDDD&Æ‹ˆˆˆÈĘ`™,""""c‚EDDDdbL°ˆˆˆˆLL:ÚÑÙáüÀh‡@4a1Á""‡ê«íˆ&4v™,""""c‚EDDDdbL°ˆˆˆˆLŒ	‘‰1Á""""21&XDDDD&Æ‹ˆˆˆÈĘ`Ñÿ·w·ËM\w‡ÿ+i%Û²cƒÁ`“�)yù�—ét:½‰ÜL®£7“kh“v¦�Ihš„„°%ÛØ–_õÖ”¤Æ’›–ÂÞçùfíj}ðŒG?ÎYŸ%1��˜ÀHL`$&°X‰	,€Ä@b 1��˜ÀHL`$&°X‰	,€Ä@b•Q€ô.\þxÔCàj<útÔC81À)õÃí?Žzœ"×n|2ê!œ(–X‰Y" ‰~¿Ë+›±¶ÖŠýýNŒ�WcáÊtLNŽ�zhðÊ	,^X«µ7¿ú)¶·÷½¾´´¾¿³³S#Œ†Ààˆ~¿ÛÛû±³{�v7º½~”˥ˣ>Q‹Zí—��ÕµV|yóAt»½׉¸w¿!°(�@D<�ªFs+–>‰õõí�ÁôÌd½ssgâ왉¸ùÕÓ¸Êór”Ë¥ØÛk:·Û~8­±þd;nÝz­ç–øžÉ²§³QÏ´¶÷£u¯ñó×µZ%~÷Û7cu­ßÜztè½ç¦ë/eÌð:X÷ÓÒZÜúöÑ¡€zfqa&®-ÎÄØX­Ö^|}ëallì9/˲èõû1y:ÍV4›[q~º¿yëâËþ'ÀkG`X³¹udÆé™+óçâí—~þzrr,>úàZüùóï¢Ýî:wo¯ûûýøÃïߊ�>XŒ�݃ˆˆ˜¯¾¼ÁÃkÌ>XÕëõãŸß<züÚâì‘×ò¼ssgžpЉo¾}kãUqE¡	,€‚ZilÆÁAgà±J¥õzmà±³gƇ^³ÑØ<2»E$°
êÉÆÎÐcÕêð;HjÇë÷#ZÛ{/4.8
@Aíïž½ŠxºeÃ0•JùØëvÚ¶e�PPyeøGÀÁÁðe¾N÷ø%Àãf¿ (@AMM
¿—ªÛí
½?kgç`èûò¼SSž=  .Í��ryøÇÀ³½¬ž×hl}ÏÕ+ÓQ*e/<68é@Aåy9Þù�}®ž÷ýÍè<÷¸œå•ÍX]Ûx~½^‹7Þ¸�tŒpRY((°ùùéèt{qû»ÇGvrßÙ9ˆ¿üõNÌÏOG5/ÇÖÖ^,=\x�©É±øðƒÅ¨3#E"°
nqa&ÎO×ãν•h6·…Öîn;îÞ]úÞYP$  ®Î$¿f»Ý�/¾ü1vw‡ÏŽA,€‚Z\8‹é#«ÓéÅÝ{Ã÷΂"p@AeYo߸ssgâÁƒµX^Ù8²›ûÿ«ÑÜŠ~¿èy(3X¶¹¹®ÇÆæN²¸Šx:‹µ¿ßIwA8aÌ`Ô½û���Á¹ze:ææÎ}_·Ó‹n¯»»íøii-ööÚÏóèŠL`Ðêjk`\]¾t6Þ}gþW_çâÅ©øìó;G^ÏsûcQlþ{P@Ë+_Ÿ>WÿŸ®SŸ¨
©K—†Ï€A,~¶ºþë6}¦ÙÜŠv»{èµjµo,Φœ8 €.Ìžøúòòf|qóÇX²½Þà»Þ{½~llìÆ··Ç—ÿxpèXž—ãÃ÷cl,O>f8I܃P@.LÅõ7/ĽïGŽ5[ÑhlE©”Ež—£šW"²ˆn·�N/ÚíÎÀ¿8œ��Š÷Þ™�ZÍGø-(¨·®_Œ¹‹gâÇ«±Ò8ºÔ×ëõc¿sìv“õZÌÌLÆ•ùé¨×k/{Èpb,€›œ‹÷޽ア³{[[{±¿ßŽƒƒNtº½èu{Ñëõ£TÊ¢\.G¥RбZãÕ˜¬×¢Zõ1ƒøÍ ""&Æ«11^õ0àTp“;@b 1��˜ÀHL`$&°X‰	,€Ä@bvr8¥®ÝødÔC€ÂX§Pãѧ£š%B€Ä@b 1��˜ÀHL`$&°X‰	,€Ä@b 1��˜ÀHL`$&°X‰	,€Ä@b 1��˜ÀHL`$&°«ŒzÀéqáòÇ£‰ág§›À’ØÙº=ê!œ~VpúY"HL`$&°X‰	,€Ä@b 1��˜ÀHL`$&°X‰	,€Ä@b 1��˜ÀHL`$&°X‰	,€Ä@b ±Ê«úFS7^Õ·©WX�öæ«ø6¯K„‰	,€Ä@b 1��˜ÀHL`$&°X‰	,€Ä@b 1��˜ÀHL`$&°X‰	,€Ä@b 1��˜ÀHL`$–EDÔƒ8MþsäÙêÞTçIEND®B`‚urwid-urwid-69907e4/docs/manual/index.rst000066400000000000000000000003461526073734300203500ustar00rootroot00000000000000.. _urwid-manual:

################
  Urwid Manual
################

.. toctree::
   :maxdepth: 2

   overview
   mainloop
   displaymodules
   widgets
   userinput
   textlayout
   encodings
   displayattributes
   canvascache


urwid-urwid-69907e4/docs/manual/mainloop.rst000066400000000000000000000146431526073734300210640ustar00rootroot00000000000000.. _main-loop:

*************
  Main Loop
*************

.. currentmodule:: urwid

The :class:`MainLoop` class ties together a :ref:`display
module `, a set of widgets and an :ref:`event loop
`. It handles passing input from the display module to the
widgets, rendering the widgets and passing the rendered canvas to the display
module to be drawn.

You may filter the user's input before it is passed to the widgets with your
own code by using :meth:`MainLoop.input_filter`, or have
special code to handle input not handled by the widgets by using
:func:`MainLoop.unhandled_input`.

You may set alarms to create timed events using
:meth:`MainLoop.set_alarm_at` or
:meth:`MainLoop.set_alarm_in`. These methods automatically add
a call to :func:`MainLoop.draw_screen` after calling your
callback. :meth:`MainLoop.remove_alarm` may be used to remove
alarms.

When the main loop is running, any code that raises an
:exc:`ExitMainLoop` exception will cause the loop to
exit cleanly. If any other exception reaches the main loop code, it will shut
down the screen to avoid leaving the terminal in an unusual state then re-raise
the exception for normal handling.

Using :class:`MainLoop` is highly recommended, but if it does
not fit the needs of your application you may choose to use your own code
instead. There are no dependencies on :class:`MainLoop` in
other parts of Urwid.

Widgets Displayed
=================

The topmost widget displayed by :class:`MainLoop` must be
passed as the first parameter to the constructor. If you want to change the
topmost widget while running, you can assign a new widget to the
:class:`MainLoop` object's
:attr:`MainLoop.widget` attribute. This is useful for
applications that have a number of different modes or views.

The displayed widgets will be handling user input, so it is better to extend
the widgets that are displayed with your application-specific input handling so
that the application's behaviour changes when the widgets change. If all your
custom input handling is done from :meth:`MainLoop.unhandled_input`,
it will be difficult to extend as your application gets more complicated.


.. _event-loops:

Event Loops
===========

Urwid's event loop classes handle waiting for things for the
:class:`MainLoop`. The different event loops allow you to
integrate with Asyncio_, Twisted_, Glib_, Tornado_, ZMQ_ libraries,
or use a simple ``select``-based loop.
Event loop classes abstract the particulars of waiting for input and
calling functions as a result of timeouts.

You will typically only have a single event loop in your application, even if
you have more than one :class:`MainLoop` running.

You can add your own files to watch to your event loop, with the
:meth:`watch_file() ` method.
Using this interface gives you the special handling
of :exc:`ExitMainLoop` and other exceptions when using Glib_, Twisted_ or
Tornado_ callbacks.

.. _Asyncio: https://docs.python.org/3/library/asyncio.html
.. _Twisted: http://twistedmatrix.com/trac/
.. _Glib: http://developer.gnome.org/glib/stable/
.. _Tornado: http://www.tornadoweb.org/
.. _ZMQ: https://pyzmq.readthedocs.io/en/latest/

``SelectEventLoop``
-------------------

This event loop is based on ``select.select()``. This is the default event loop
created if none is passed to :class:`~urwid.main_loop.MainLoop`.

::

    # same as urwid.MainLoop(widget, event_loop=urwid.SelectEventLoop())
    loop = urwid.MainLoop(widget)

.. seealso::

  :class:`SelectEventLoop reference `

``AsyncioEventLoop``
--------------------

This event loop integrates with the asyncio module in Python.

::

    import asyncio
    evl = urwid.AsyncioEventLoop(loop=asyncio.get_event_loop())
    loop = urwid.MainLoop(widget, event_loop=evl)

.. note::

    In case of multithreading or multiprocessing usage required, do not use executor directly!
    Use instead method :meth:`run_in_executor` of :class:`AsyncioEventLoop`,
    which forward API of the same method from asyncio Event Loop run_in_executor_.

.. warning::

    For input handling, selectors based logic is used.
    Under Windows OS only `SelectorEventLoop`_ is supported.

.. note:: `uvloop` event loop is not supported officially.

.. _run_in_executor: https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.run_in_executor
.. _SelectorEventLoop: https://docs.python.org/3/library/asyncio-eventloop.html?highlight=selectoreventloop#asyncio.SelectorEventLoop

.. seealso::

  :class:`AsyncioEventLoop reference `

``TwistedEventLoop``
--------------------

This event loop uses Twisted's reactor. It has been set up to emulate
:class:`SelectEventLoop`'s behaviour and will start the
reactor and stop it on an error. This is not the standard way of using
Twisted's reactor, so you may need to modify this behaviour for your
application.

::

    loop = urwid.MainLoop(widget, event_loop=urwid.TwistedEventLoop())

.. note::

    In case of threading usage required, please use twisted deferToThread_ method. It supports callbacks if needed.
    Executors from `concurrent.futures` are not supported.

.. _deferToThread: https://docs.twisted.org/en/stable/core/howto/threading.html#getting-results

.. seealso::

  :class:`TwistedEventLoop reference `

``GLibEventLoop``
-----------------

.. deprecated:: 4.1.7
    This API will be removed in version 6.0.

This event loop uses GLib's event loop. This is useful if you are building an
application that depends on DBus events, but don't want to base your
application on Twisted.

::

    loop = urwid.MainLoop(widget, event_loop=urwid.GLibEventLoop())

.. seealso::

  :class:`GLibEventLoop reference `

``TornadoEventLoop``
--------------------

This event loop integrates with Tornado.

::

    from tornado.ioloop import IOLoop
    evl = urwid.TornadoEventLoop(IOLoop())
    loop = urwid.MainLoop(widget, event_loop=evl)

.. note::

    In case of multithreading or multiprocessing usage required, do not use executor directly!
    Use instead method :meth:`run_in_executor` of :class:`TornadoEventLoop`,
    which forward API of the same method from `tornado.ioloop.IOLoop` (and internally use asyncio run_in_executor_).

.. seealso::

  :class:`TornadoEventLoop reference `

``ZMQEventLoop``
----------------

This event loop integrates with 0MQ.

::

    evl = urwid.ZMQEventLoop()
    loop = urwid.MainLoop(widget, event_loop=evl)

.. seealso::

  :class:`ZMQEventLoop reference `
urwid-urwid-69907e4/docs/manual/overview.rst000066400000000000000000000060161526073734300211070ustar00rootroot00000000000000Library Overview
================

.. currentmodule:: urwid

Urwid is a console user interface library for `Python`_. Urwid offers an
alternative to using Python's curses module directly and handles many of the
difficult and tedious tasks for you.

.. _Python: http://www.python.org/

.. image:: images/introduction.png

Each Urwid component is loosely coupled and designed to be extended by the
user.

:ref:`Display modules ` are responsible for accepting
:ref:`user input ` and converting escape sequences to lists of
keystrokes and mouse events. They also draw the screen contents and convert
attributes used in the canvases rendered to the actual colors that appear on
screen.

The included widgets are simple building blocks and examples that try not to
impose a particular style of interface.
It may be helpful to think of Urwid as a console widget construction set rather
than a finished UI library like GTK or Qt. The :class:`Widget base class
` describes the widget interface and :ref:`widget layout
` describes how widgets are nested and arranged on the screen.

Text is the bulk of what will be displayed in any console user interface.
Urwid supports a number of :ref:`text encodings ` and Urwid
comes with a configurable :ref:`text layout ` that handles the
most of the common alignment and wrapping modes. If you need more flexibility
you can also write your own text layout classes.

Urwid supports a range of common :ref:`display attributes `,
including 24-bit and 256-color foreground and background settings, bold,
underline and standout settings for displaying text. Not all of these are
supported by all terminals, so Urwid helps you write applications that support
different color modes depending on what the user's terminal supports and what
they choose to enable.

:class:`ListBox` is one of Urwid's most powerful widgets,
and you may control of the :ref:`listbox contents ` by using
a built-in list walker class or by writing one yourself. This is very useful
for scrolling through lists of any significant length, or with nesting, folding
and other similar features.

When a widget renders a canvas to be drawn on screen, a weak reference to it is
stored in the :ref:`canvas cache `. This cache is used any time a
widget needs to be rendered again, reducing the amount of work required to
update the screen. Since only weak references are used, Urwid's display modules
will hold on to a reference to the canvas that they are currently displaying as
a way to keep the cache alive and populated with current data.

Urwid's :ref:`main loop ` simplifies handling of input and updating
the screen.  It also lets you use one of a number of :ref:`the event loops
`, allowing integration with Twisted_'s reactor or Glib_'s event
loop if desired.

.. _Glib: http://developer.gnome.org/glib/stable/glib-The-Main-Event-Loop.html
.. _Twisted: http://twistedmatrix.com/documents/current/core/howto/reactor-basics.html
urwid-urwid-69907e4/docs/manual/safe_combinations.py000077500000000000000000000031561526073734300225510ustar00rootroot00000000000000#!/usr/bin/env python

"""Display readable foreground colours on neutral backgrounds, for the manual."""

from __future__ import annotations

import sys

import urwid

BLACK_FGS = (
    "light gray",
    "dark cyan",
    "dark red",
    "dark green",
    "dark magenta",
    "white",
    "light blue",
    "light cyan",
    "light red",
    "light green",
    "yellow",
    "light magenta",
)
GRAY_FGS = (
    "black",
    "dark blue",
    "dark cyan",
    "dark red",
    "dark green",
    "dark magenta",
    "white",
    "light red",
    "yellow",
    "light magenta",
)
BLUE_FGS = (
    "light gray",
    "dark cyan",
    "white",
    "light cyan",
    "light red",
    "light green",
    "yellow",
    "light magenta",
)
CYAN_FGS = (
    "black",
    "light gray",
    "dark blue",
    "white",
    "light cyan",
    "light green",
    "yellow",
)

BG_FGS = [
    ("black", BLACK_FGS),
    ("light gray", GRAY_FGS),
    ("dark blue", BLUE_FGS),
    ("dark cyan", CYAN_FGS),
]

body = urwid.SimpleFocusListWalker([])

for bg, fgs in BG_FGS:
    spec = urwid.AttrSpec(fgs[0], bg)

    body.extend(
        (
            urwid.AttrMap(urwid.Divider(), spec),
            urwid.AttrMap(
                urwid.GridFlow(
                    (urwid.AttrMap(urwid.Text(f"'{fg}' on '{bg}'"), urwid.AttrSpec(fg, bg)) for fg in fgs),
                    35,
                    0,
                    0,
                    urwid.LEFT,
                ),
                spec,
            ),
            urwid.AttrMap(urwid.Divider(), spec),
        )
    )

try:
    urwid.MainLoop(urwid.ListBox(body)).run()
except KeyboardInterrupt:
    sys.exit(0)
urwid-urwid-69907e4/docs/manual/safe_combinations.py.xdotool000066400000000000000000000000521526073734300242250ustar00rootroot00000000000000windowsize --usehints $TERMWINDOWID 70 27
urwid-urwid-69907e4/docs/manual/safe_combinations1.png000066400000000000000000000474411526073734300227700ustar00rootroot00000000000000‰PNG


IHDR``‘‡ cHRMz&€„ú€èu0ê`:˜pœºQ<-PLTEåååÍÍÍÍÍÍÿÿÿ\\ÿÿÿÿÿÿÿÿÿîÿÿÿÌÓAbKGDo½0O	pHYs‡‡�åñeNaIDATxÚí�Oo¹óÿ;aÙpHg¤�ØHIÐ
	”'Rr_8Ì
io\WœrA‚cHˆc$¤…k„ÄÞ‡Cr@°0Ïå3¶«Êe·=cOÿ
[ïÏ/ÓÝv¹ìM½Ò.ÛÃï["‘H$‰D"‘H$‰D"‘H$‰D"‘H$‰D"‘H$‰D"‘H$‰D"‘H$‰D"‘H$‰D"‘H$‰D"‘H$‰DCj­,Ë
¼åXÚµ 6€ýfæZ¯ÒÊÚTµ
ÝTp×sÿ´¶QtÌZbKc—Lªß¦Zߦ»V€±þRìÒ�IõÛš0K€Éþ_	˜"†èç.ÜÿàÀ¸`OLXLDLXLD.0*ë4Åå†
’úÜ0Ån:
õ*’º…©Æ¢¢Ø5ÓüÕú·åx)éf|‹ž�Y¯¶up0ûœ?T•	غK]¯M¹½j½Íý‘{¿Þíז㥢›í‚÷?`4!ªT_êÀ@ý¼F£°õøS,˜ƒ¿ZÿèŽ�ÆÝ̾�©”Ìr"¥C­Vñ·Œ1Óá5äûj»3/°d_UU _è¶@`¨(·ý÷Ê]%a€æ?kô'ÎD\SŸs`
KhR1o+æÏï½ÄvÜ)ã5}
·ëZóÀ¬k`
�~Ú6åÛÜn[‡Ž€ûʳc~ÖW¢À¢ég=Ò®GE€Ù€š80^ÙÚB`6Â@ùÀ`ìg¾¾Åß$0ßlýófÀœ‚@PlGünÇë}`°ÍºK5ÜÞ�–&$1`¼„…Zê7AÄ»ã/RïcG5Ȧ Ä¤à	Š™™¶=;°¡|t0èwY½ŒígèLS`ÖJHvZf
s(ë~ˆß
Ëdmò�¾�)°o
æ/ÔÿjÌZi³Ú�ˆ÷`ÖÊq³^¥£²ØíÂÚ·÷ê5¦Ä|£%`Ð&ä·kQ`+Jª‚SƒömÃý…ú@
�ÁçH`³�Ù0Û©ÀàþJËÀl«À¬…§ŽæÀl2'Å�Ùv·a¶áf½ªüvݳ=ôœ´ò²šZ—€É]V£?¶¬j†–¸nãïÃÐ
…yQ“»¬FlÅ6ô2©´¡)ÌöŠÌ·Æz–Ã*ñ³ D`Jî�mÜõöe..6ð0…̶s
„ö-Sql㮳)ñ€­R0�‘£j½¶{4ÆbáYæD4=
2'Ù£�uÿh`[ßl›zcÂ?Jˆt[àzˆú‡~jý :|dÝ
fÍ¡õÆB`èð1–ÃÐ+È"Þácϲ‡�Û>0�ÃÇ‚n/fÝ&ÇᆼYÐíðqDZö�†`ý�_¾ƒü²íà`VИ�%If¬~˜
ÙñhèÄb0•Köâ#õÃl¯�Gƒo§
¦µ%¡�ÔËfèˆD"‘H$‰D"‘H$:º°—V¶Š�¯½[ÐtîZðÙ•¾ÛO+ëBûw »}¸hLnÑ]#`¬Ÿ”þÒ�IõÛ¶¾»Cw­cý¦Ø¥“ê·±ƹ÷+À	0 f‘�#ÀÜɱ`˜;9vãæÂÞ-$ÌBç{{&pxL¡\òv~`/?ªöV ?[Ž—=º¹Uð~æ»ý;:(˜uÎö÷M ¾ã1D;¨×
{Uµw¸_ÛN—Ûz·[Ž—}º¹Sðqô
ÌÞÞžd#¦C®·Çß2P^ ´«Š÷níííú£fòÉüëw``ö÷÷
 û!b¨}þ–;¬/Юûw /°ûÔþþ~ t‹À|ÇýGoÀÍtAS@õÓ-S~Ë1„ò[øº°™ÊWŅФ³·‡¨0p§Œ/Ü*êý¤y@¾ÓÀ2ýtÇ”ßqì
ˆ Ôß�7‘¹î{öN»;ñúý}D€�;eüÝ�¢6ŽÞÄß$ÌŸø-¸wy°åô³˜[á,ņûö;�ø›¦œ;pï¼0Xïü˜·LÄÿ�x½÷Go2ÝæþÎ	…™�nqÃl"Qo™[áÄË£ÀP;|ªõ;�Ì_³¹ÇÅÜ»Ød÷›@ÔÛ…]ƒ@ÜY”íߟüqô&
à–DÀ£cØ0Яf/‚Mà1bGÏ�ã°Àó‚¡^ØKfÏd©mƒýŽ˜ïö3�Ù7Ù)]ï´Žc4ÀàRšs‹vöË�	4ë]Ð}JÚ_<%uÌþ¾?%í;%±©I—G�¹å¶[ä/	˜[#æN&0wŠÚµ`[þ*‰ìy»EþVæÖ(æ¤80wÜm˜;Csg˜9	§#Ø;aÀÔöaø¥µe5öËüŽb™„Óì•0`¢û0ô̯m-«qÌÿ°û0{¸ÇÀ3/lg`ãÐnÜ£6ð8ÞÉ€̼rß^[`#ÑnÜÍIöhà‚Œ{4àØ³=þº¿b0Øì‚Û/Ä@ì]ìhà;˜ðÑ€ûÌV5ÁÀÒÑ@:RpÇGµqô&{øxËÆ?|4¢3§[Ám:|Œå0äÁ?þáãÀ²‡�w|`b‡�F˜�âYR:|Œå0ÔxrïðQtî4À÷kEçYŒ(]w†J>EçSûCíыΧ¾hÇU$‰D"‘H$‰D"ѹÔŃ´²>tpº?€»�Ç5ù゚´èïûÉÄzæj§—NtñÝu
Œí'Å.˜T¿¹úþþ´×~™ßt`Rýö¦Ž€©5\	ç.ì~ `jܬŒsçX	0ß
0X-À0Ž˜ˆ˜°˜ˆà~ñàžf™ó‡ƒ˜‹6fÔÿî9v¦�*PŸ÷tµJV;?°¡úÞE�Ëñr@7÷
>®N�1ÑœGoòÇ÷ú^e©p�tÕÔû½©Ævž_VŽ—	ݘ~ÔÏÅx�цH«ÆüFæÀ¾,Tµ%âžmWÀóÁ=ëÆ±ó‹ÍŒ}m<è�qú+
6®¾€±¤ÇìÄb;ϯ®6åØógº,ŒÁè€1šࢎt!ÒO÷Lù=nfÞ(h‡�SFæþ"½
æ¯ÞŸ.?pý“°CýÝÓˆ\Œ´kYff˜Gr¼ï‰çòG1™„*{ÛÎÕd‚¨€W¸SÆßëk¸ÝhÄß$ÍŸð=¸çoí,0‚{…“ÃÄr#´M†÷WW²ÀüÁ^7ÞåûEÀüÎR|`|_ê¥6Þ½»Â¾-”.â;¿03Ó=kV†Þ2÷Œ“T`x» 04|òÆÕ•Ôú‹€�D¥Öм-ó£ÑvFOßOÆý‚±ºÈ’xdf°[
ÌŃƒƒ6€aýÆÕ•úæ{“¼X`Æý‚¡]Þãàé¢Ýèóúí›HC½"�ÔϽú¸:S˜ïíáãÁ†´EÎahƲT‰yü£à‡�ß †úÞ¯èœJ€¥ë^?ɧè[ÑAO{ô¢oDûÙq‰D"‘H$‰D"‘H$úFôÃãÇ�ÿÄ[^�¥¾ œµk¦eþÜQõ¦ÿÂ[^�¥]úaãðØûïä‡?‹N€ù!R¶K&ÕoSýøWÑ	0?&¶4véÀ¤úm,À`¼?qÖV�‰ú˜Â¢&ûO|%`Š¢#Æ•”#Àx`"`Â`"r�QY§)~ü§
’úüÓ;é(”S;õ	wèàO·Óܶsû÷ýÔ�¹ª±‹a€QY§)>üKI}þeнtôДðÕ`_Ôû£_ïöoËñrH7AÆbh`4!ªT_êÀ<&Ã?ñÓ�v^`ÁüÕú÷ýý€~ \Ãû8à·3E€Ñ„¨R}©cªu+"ê/²/j�%{¬wû‡îœqÂL¹†÷0à·{¹«$ÐüçúçÖ�9*eü8°gòØ“?¿wÏß…õ÷ƒ¾†Ûu/w•„šÿüHâLªP…Ž€dïɼ­âõ‡‡8¦@M??FÚõ 0BM20áM¶˜¯`°
úš_³wæ/¨©­X äöžx]
0Øû™_ûß»Ór�ÁàG�ù“[z0ˆ¨ù‹ÔûÀØQ
ô‚ñ€Á%³¦šEÀÔüEê}`l?C½`ÆÌ˜Cô8Ìï¥!0?bÎÓ0Ì>ó{i?;öìS�ùáñùFe±¦º]`¸?|æ÷22`?ö§¤–N9³Õ˜ÃÃv§$îÏŒêð˜’"Þ³€ùó¼ƒû+-ó—õçóç(VIÉÀüx¶xoÌ_ãX%­°¬þÁ½®Üe5ìÙðeõ¸öaþZ¼¬æÀ¶¸¬FlY=Ø>Œ
Ma7à0úÖ6Û°rú£Ù>Œõ÷ØÙ¸+Ɔ¦0f0qkÜPc9Ç�-sÈý±�»Þ¾ÌåɬC(ie«:#úÓZÛ­üðìàOÚʽeèh LÍßc�.Ç
ô-³9Ä3€Úª'p4`ÊÿÂÖ‹¦ÿ(!T}ÈÆ�þ°{4п\`èð‘-tÿdÆöHx‹D‡�±¦æïOÈi¼ÃÇÞåC‡�‰¿\sóhÏó£MŽÃ9½Â€,ã¯~ø8
ô=ÚÑ+é
|Ùvp	0a	0!ý9T’9rý•˜d¶;”¶?n¦íŶ�6˜~jguäú1
…ÿÜF$‰D"‘H$‰D"Qª~:::z†·¼KÓœäwþYÿÍ}¶¨Ÿ���_â-¯ÀÒ¾ý±ñøím(?=+Zæ§ÄÆ.˜T¿méç—E§ÀüœèÁØ¥“êwea –“ð§Þ˜"†æÀÀ�0+ÿi¯LCu@`\	0á	0Œ'&"&,&"•}šâ£g*Xêó™)vÓÒy¥±„`_ìO¦ø©õkËñrD7ÏL7…±•mšâã—*8êó¥)vÓP¨W4-°¡¹B»¢ØŸ�[ïŽÃ–ãå˜n^šî
c10šUª/!`
XöµÀB1ø©õKÍXÿä¯0Àe½¹ÚRMˆ*Õ—:0P?¯1`=\¡]Qì1…õî8Ð-ù-0Æ s`Pî*	5ÿù‰þÔ¹~RµÏt¥!Ê”ø2o)æÇïõ{`àNÿôüÚõ'w•„�™ÿüLÚL¸ŸÕçKmtýN;Oæ­¯?>Æ‘0p§ŒÖ×p»æÔÔ�y†vø³˜gá,ņû„û£a÷î"À¼„š80/ñ™ÿP;O¼.îî�ûÛ»Ór�1å�±­Ô[æY8/®ù‰ÔûÀØÑü‚ñ€1Õq`°Ë!1q®A ^.ŠüzÀØÑõý‚0?aîTÐc¿¿_#ægÌ¥
zìù³"0�
·ÌOGß0�Óõe;Àü|,À<3þü)éè|OIs|ìOIÇçhJ*Šö€yö�ó²¨][æ¥óÌ·7Ý<Õ*i¼À¼v•”°¬æ—Ö–Õ°gÃýŽsæåÒe5ke¯m-«aφûï}†…¾ 
9Ì3Çœ€Q­
f�mÜe}'§};!§
¸—ÌKn�õeh¯-fk�mÜõý¥.³Á$¶`«¨–T@±Ùû?
¯gèh Œ=¼Â3ˆŸÐß³8ˆýɬ?0i-Øê9pŸkí<ÑÑ@{˜…g?£ß—üh ?¹ÀÐá#[ð>cÖ4Eá)á³à�6>Ærzu!xàÇ?|L.0tøÈ¸/™5;|¤æ/í5
>Ærz•!€/!§ñÔÀß§­–}£¡Çï׎KLXLHφN6Gª—K’Ëeõ߬Žß“§Ž—ìÁ/«ÿfõÓÐ;¬#ÕÏKvT—Õ‹D"‘H$‰D"‘H$¹ºtrrò7»6x‰ú»t2ôrP—§Óé{¼åX:” 6>à�u}éïPár`.ý]$ÉØ¥“ê·k]~_ô
ÌåD�Æ.˜T¿ÉŠÛ*0Q#¦°hD€YõO¹Ön%`ŠºLïò�q%ÀÔ$À˜«ãI€	K€‰HHÇËmž–êr˜K:[5Æúo×�ÉfOþ¾ä�À`9^Nè`,Æ	ŒÊ.Mñô½
†ú|oŠyÚyyªþ§ž±ìuAhGͽÀ^†jc_—-ÇË”nÞcÑцh8ÁHzÀœh�âÀ˜ú¹çù?qA¼„ý@¹riFŒŽ´*Õ—:0Ó©%â½µ/àÙoGv~`±Ù{ìØºåã£þ
Œ1h#Fý•›À¹À\2´\rª˜ ^µ½št,m௰ý]Ò×p»áå®’0óŸËô§l52o”ËÐí•q¨Ýå"<©˜·³÷G5Åvõ÷^#r9Ò®5a ÍÔ Kþ†}Mæïp–âƒm0w™_Oƹwæ=ÔD€A[ƒÀkJƒ¸m
0¼?Hw{wü…8-ÑGÁÃllæïƒ[˜¿‹m—NìÓ¥“‘¾`<`è­ñ>
n¯>R�©õ©÷�±£íöÓ?0—xRd»üï[]+ss�%À\žN§mÃúÃÇn/ólõï’Ó80:©
�ûäséäÛæòÔ²\¿Ýåi;Àðþð¹ÛßKÏÀœœøSRK§œ­k5`¦SoJŠ3�¶3%ñþÌ@¦ÝNI…‡I˜¿/µÌßß80ï9LQog_ï5˜÷}séÄKI#À¨¸N€ù{¤sÒyæ}Çs``ßÁeµ
lp9“»¬†=¾¬é2iµe5ì¡8ËjôæŠl÷í;^&ó@9�俸2€Aï—NNœ�»b¤À°E2Û€{O�ÏŒys™oØi³@;´ó•Ì”ûaw]É‹"ΕoáÿjŽG1`pdü©ÏKè÷o~406™õ&­[�`Àœ£¶JºŒGìê·#;_t4ÆnMýþÞó£�ÎDS�=Ðnj7úÑác,‡¡WIý¼/øácWé—jǨÜo4tøýÚ%À$K€Qg1>½ÏL&síϋƺm6:M3÷Üsíϋ䓨˙;¨¹ö"‘H$‰D"‘H$‰D"‘H$‰D"‘H$‰D"‘H$‰D"‘H$‰D"‘H$‰D"‘H$‰D"‘H$‰D"ѨTŠD`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY²Àkú²Qpçic5¶˜?×C¸—qjk¶¼lv
Å3¸kÚ%ùq{jÅûêR1ÜXÓwú3˜�µe^™]À¤ößT[§t×
0[‘ò ]À¤ö¿P5`ÊX(W†ù‹“ýê50e<¤YÀ0?`BƒJüïi >�ñzÆR&bö#À0;ÆíK˜ˆÝè�Ù€⥠uUq5^À6L½mOðhŠ7ðÿŸú
eIöúÿû5ÇŸ®WꦋÊÞ�Ùš�êØ`:˜ÍL¼¶XL•Õ)Üá'ÜÍfܹ7þl;¯ß»Ãæf`¯ŠÝôÇ	ãƒñbQÙ>0ôyÀØ�`Êu�CƒúbÍÿ¿\:û:0u¨QÄqàOïÀèÈ@„Nõo_‡Z�ý‘ƒYY†ëìg§³Ù,Ô¯töu`p4>SoÁm
¸+0 ¸Sï�
}-7B“Ë´+"õþ*	Á#{oJ"07Ôç�ÃÓ¯æ`lé•ð‹×O§¦üÔÚ™`–¥Œ2šE¦4mãùaO­eI€’½7%À8>6†÷nâÀ(™iˆîƒÿ·O˜µeÙŠÚÇ�YóÊ6†ï÷S¸w8ˆÏ�4Ì)�'Ãok¶È•äîôšØy�ZÃûp¼°]*05{/a¡–ú
×Úwº((�„@¿ùOƒv.0�¨Dý¦S³÷€ñj©ß„íý:³�¹FI�Akã˜-Êtù–—zŒ˜-̽zf£08•%Ýw,;­¬L-;uí»†Æ;Ï’ƒîWSø«v€YyXëÇÐÀàRšä4`7`h¼½³6`ð9v*Ú±x Væt¹ßN€Áç–ŽÏA9À¬ç¤Þ€Ùæÿ@e˜Sw•t:R`¶f-¦0–Õ°·Â—Õ‹÷aÖZ_VSëb@`œå¬ÌÒ}˜-¾Â
ú=ídYM­g=SÀçFQ8wåŠÀXäJãÏfÍõ·æS *Ã|åÊÙ0s�)#ÀÌàs7îÊÕöaøbž60§®¿So»_¹âÀØÃܳÇÜ�¦‘àœ„‹˜ø±¥ÙÙÅ=~»úB@#GÔzc@`øÑÀ–Lðh`íOéh`ñ[.Æø¡¤™ŽØÕiY˱¶:ÆpX;|ôD‡�Ñ�ºÀÐá#[À¯9þÖxÃÆöÞµìáã©LøðNçÁÀZ¿´E	©>²S¦SÇŸ3>oWÀô©e;pÁúA¶íÆ¥e;pÁú6·í˜ó¥ÿ&0kK&—Xý0‹¤ñètÉä«ou‘40E±8ô‘úa¶íF¤H¾Õm»A€ÙXúHýýã縩õí¾`ä_>Šò$Àˆ²$Àˆ²$Àˆ²$Àˆ²$Àˆ²$Àˆ²ä³YÕ-Be-j³ªª�POX:ý²—V¶ŠM­ÉÞÞíPk,X
˜Í|jë/Á®`Rû_¦_nÓ]#`¬Ÿ„þ:&µÿ$
Liш“=€s
Liш“ýÚúf�q%À¸`˜HÌ;Æíï¼³YíèØ`:¨*¯M'¦º^›r{Õí8]lúõT�Ø]Uñq€½*®œ†8NôoÇÓ60¿ìÝÖ1Âltþ°·gâö�-”kCÞÎØ/Æ�ª½ê¯ÄnŒôöªxÏiˆý`ÿ¦‹Êî€Ñ‘�íèß¾µÿ#7f:ÌQ}OwFöUå¾+"ÀÀ8À¾ÔÛñaÿäÉíeÁ/ZG"u[GA‡\‘ÿ±Cy‰vЮ0(Þ»½··êÏü€}ì‡Æeêñ§` |•úÝϯBý´cÊYÔÃ&ÚWžó»°Þ_%! dïMI0ޝ´cébïfÆ/:R%@?Ý6å·C(¿�o _ö"S™áªüeñäï“ì½)‰ÀÅþKK{70ô&�÷ûÜ;oŒ€ Ø©÷Àë²€Ù!pëþØøXÙfGÀЛÞó·áÞåÁ–ÓÏB`n/ËV<`n°áñ1ü~Ù[d#Y`((�„@¿ùwX^
å"`Ðo*05{/a¡–|ÝP€å0.«©uÕ!0βÖ¦¶Ã/C,«©õ^¯ÀÀ¦t�Ù�½TSŠö
€á‹yíßfÇõ·ãSqtÛ—³qæS†�™6Û‡á‹xÚdÀÜvýÝv�¡
ÃN¾ržqÇŽ6}`ü£�}³cê�}ï(¡.³«ð ¶êŠ
”¼ÿξ£ÇŽ~ñ�q�{{4ðW.Ƭ¿0y¶Gh= Ö¿ôŒ=|Üñ�	>– 
#!Û´Éq$¤.0›¾¿Íèá#ï¿3`ìáãmÿðшΜn7Bèð1R:|d§L·θhœ�“«¿ì»Ìu°¾ã/©e;p0»ŠsômÓÍ¢D@/ž\v¢¹R7ãZ·—L.±úNI
€éf›¬¤$9·¾³ñ­H¾“m»fÀt1ž²–ã¦Ö«/?ÇM­ïæ#ÿòQ”'F”%F”%F”%F”%F”%F”%˜_÷롲ôëþþþÝP�X:¤®¤•õ2”ƒƒû¡`iÇRÀüzŸÚÆúM°ë˜ÔþcºrŸî:Æö“`×0©ýk�˜Ò¢&{ #¦Ö0˜Ò¢&{`çWŒo~—0¥W*À€�vLÚxÆ̯ûwuL0ûœ?ìï›8ýÊc‰vP¯”öªjÁá]§«_�_[OðˆÝ‚;èìUñ¾ÓЇ©Æ¢²5`®Ü×1Àlsþpp`ârÅÆîÊ�úß}ÇδSêó¾®VIë�cçì
Tß¿â‘ásŸ¥~jþ­?37ô‹EåêÀèˆ@dîêߺ±ÿã;¬/Ñ®ûw)ânÀö¨ý}÷]úû:0Þ8~¥þɓ۶à©#‘¸hÌot�쳪¶»oÛ•ð|pߺqìü€a³û¡ñøÀ@?¾Çß7އ<åaÛW¿óùC§Ÿîšrö«'€¨þ.¼‰Ìuß³wÚÝ�×û«$ì‡ì½)©6ŽÒÂÒæÞÍŒ+:*ýtß”ßçfæ/í(et`î¯@{Ó O.æmÁìkrWIØ�ïßñwÀÆ]ZXVXŠ;ÀЛÞëwáÞy#°@a½ó¯Ëæ.\÷ÇÆÁÊ~mz“À{ü>Üó7ƒv�ÐXn´8 `þ�ñ;óç" ÊC	@é$ú�Ïÿ^¢PBbâ\CÃv©ÀÔì=`üq`KýFÌþ=ÄE~éLü%¦,6.0ô–¹oœ¤ÃÛ¥S³÷œz	µÔoÈü_G˜_)7€ ¸)Çh�ùs°Ž€¹B¹üÒ�ÃssŠ%À ¿H�ss²–€ùu?ÈFézw`hÜó,9è~Eá/üÊA0W<` õJþ"ýuõ;Ï’ƒî«.¥y xÆ
�»#`0á¿xö'`Ðn0ä/Ößýn€¡~[æn&0wËÚu`ðÙoSñ€$ãÙ£Q
˜ûƒÏÞê;Q)ÀÜu·a_½mž†Šs?¸J:7À\	MˆKU_VÃ^	&ºCÏü:À²ÚöÛ0βÕ&¼ƒvÎ4V†�	…¬ÃeµÛokÀÀÂ;ð€Q‘²×À8èÑFà]æ®ëÏ�»å¯\9c.0eÜè;¸Ow¥PÝŠü…ú»¿¾È·ƒžëï¾õ»ÊŒÄ¿qÇŽ~õ�qæ$wEÏ´•	-bb!5ë1Lžëþ¢GÔú×€áGW|`œ­
»JÂá€]-öh ¸®¥EL,¤f}†ÉtÌ¿?~ž{]i{øx×&vøH‘c�A‡�Ñ�ºÀÐá#;eºëøsÇ�ãn{øx߯Ý<­ÙÙÃÇûÎÙ
ž5ÝC‡�Ñ�ºÀ\aS!÷ï�ŸæFWØûb­ö%ðž¾ç»´Ÿ`}_ƒKÐÊ_�h¹Ÿ˜«t%Àt)FénOßä_ÖÏÝh®ÔÃàt¿§oò/ë'V¿Ò"i`ö÷û	ɲ~"õínÛ5Pxç¿ÿ~"õ+mÛ­̯=…dY?‘ú±¼`üÜs°~"õ+Ò,ÿòQ”%F”%F”%F”%F”%F”%F”¥:0“xwuÒBW'ôèú³ýŒZÿ;::zŽ·¼KÓœÓoRÀ\}ÀK–ãÚÇ¥í:&µÿ¦úßó²U`þ—Ø¢½~~›*˜7YÀ0�`²_d½SÚE—ðúX˜Vú
ûm¤EÀ€ZÆÕùÆUÀ¬ÞoØo#	0q	0	0q	00&~óàM\Õ÷*[…¤­¦ °«¦Û±
xDר\Y’=óOþt½ËÄtû°2l
ŒÊBMñÑs+õùÜ9¡�WKhöµ€ýÏ4?môkúÁþM5•cI)y@)°^À&ÆÛYE€™˜`_êç5Å«P�?ƒ£#¥Jõ%8ƒ–ƒ}-`P~š÷‹ý@ÿÿƒzüi-óÇ;�È̯ìꨀy5u`ž£þ,æù²l%­_Ìs¯ìsõÁ"` Q©yÀ#˜š½Œ—°PK5À6¶žóåƒÀØVê-ó<œŸÖü4ì×KX¨¥~õ÷Ë8À\�@²#Àdó¿£££�ÑI¨x˜ZvŠíºæêÄfÕA÷�kE` +
˜ÿÙ¬:è~5�˜	æNL0ðvé˜Wó ì¡s`ð9ˆkj0%1û�×N�ÁgoõÝPµ_6f`®N†IaÎ;0ÁEýʲÀ\…­×EËê�:\VSëɈ€IXVóËËjj}Ô-0~àÙŽ^VÛ‡™XK»È€yàú{à3AT†úÊÕ}Ir,pÏsF4&«_¬g9 ÒêW®âÀLø™\‚ã´ˆ‰…Ô
�?:@ûèѵ¾:(0fs“X»E�¡¨Ÿ%•PŒg:Á¿qZÄÄBšÙ¯= Öÿ˜«“``mCÚ¢‰„Ô†Ù)ÓÇߞØäª,üR§8:d§=Ï™5MP:|ôD‡�Ñ�æöûœç0ó†Ï;¦O-Û�Ö´mR›;amö³Í!0«H€éS–L.±ú¡I¾žôÍýeýÆê[]$
L$w^V?Ô¶]M‘vð~£¹õó6G10W—„>R?–ŒŸkަßH}ËtË¿|eI€eI€eI€eI€eI€eI€e)
̵“““7ìÚTÌϵ^ÑŠ÷îôÛt:ýoy–þÇÆ¥€¹ö&P‘̵HyЮ`Rû_U¿ýSö˜ß=v5®¤þ£À”6”­ÃüD€q‹3üv%ü.	Œ[œáÞo—Lã-0á^ÆŒ«ñÓÖ¸˜†``â`"`t¸LÌæé©®ó€¹¦³Vó`¬ß8®LýÜàšG†Ì5tMY’½*vÓk¨W}馚�³_`T–iЧÿ¨X¨ÏLñ”…î·©úŸzÆb°×e 5÷öTû¦ã²ã˜˜�L�]‰le¾aˆ
œ̉ŽtS?7@"P`NС¾Ô��úy�FñÔ3jÝþ[W$0:ªT_êÀL§6bÿXûžývdç›ýƒ7—‡z^ñ�‡<¹ý‡e÷a0êïÝ„ÔƼ
ð%T�Ô«¶×O>ð~8ѶÖÞ›’°^‘R–óŽÙûÝ»qW#ú¯Øü˜?}÷Õ¯þ~õ_òoÐí•q¨Ýoexr1ofßl\T¯H)˨_L’0¸k'¥GE&0o–e+0hæ�WvmÀà/:ÚZ`^»PºÁm³€	�‹ó�WöÛJÀ\;1!ƒD¤ô�˜Ôÿ{"aK¦fïã%,ÔÇÙŸÜÀÐ[ãŸ4`¸½úH¦ÖO³q•^ÂB-›f¥Çç˜k'�ìœ`~Üb	0`êµS` ß•�QÙç›rÍ80:I
E¬k`®�ج:è¾3­ÌoSÈBývhêµK`¨ßy–tÖùæs«sÌtêMI`Ð.Ôk—ÀP¿«ãbæÍµA€Áç–ŽÏÓµ"0ÿr˜²ÞŽèµS`ðÙ[}/æÚ‰·Š£öÓ†&Ø{—úÆ�ùmš‘€±q.«Í웺§—ÕÔúdÀ¤.«aÅYV£7˜PÈ:\V»ý®�1¿À”Û¿±W_	ÀœXK»AÈ€yãú{ãsRö#ü…b`þñó3æ9Ìo|ÃN›Ú¡�¯`rÆEõ,‡�~³¾AS†¶äi®´S	-bb!5~(©>±g
0éÅŽ¨õµA€1›ï˜´–lõƒ!wŽØ*	s„)»úíÈÎ-bb!Í? Ö¿µŒ
;õ)ép1
Ì5›4GBêC‡�ì”é�ãï
ÏaLòÄÇÙ›ÜÀüƦœàWoåvfJû1öðñŸ@»©ÝèsD‡�Ñ�æŒË9|„Öÿ4¦çí°”.ƒõŒÓWî7VýDÛ㊀™Ó…³Š¥¾o–L.o¢¹R¿ãôõOæK<×¾«qÅê³IÎiõ›þ«˜0·Î¬ï}œ¾Â;ùíÙw5®H}Ö¶�÷õ†~umIè#õC¿`ü\²uû®Æ©Ï¤Yþå£(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KÅL$Ê�#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#Ê’#ÊsZ–åÞr,íCnÏá2o÷:«Òʆè·Ú…â
ætkÖ	0§‰-­];À¤ö»Lg»t×
0Ö_›ý.&µß$03‹F˜P4ªS`fq”ÿ“À8w«õ›¤0®†I€‰K€	H€‰K€	¨ŒÊ&gæfKK}n™b7Í„zQÝÂTcѬ¸SÓüñŠ-Çv~Oõ�maÛ›‹ú„;¸ë~eÁ/ú¬ÚÕA¬rþPU&pgNŒt½6åöªõn(pg~ýjýÚöæ¢>᮪Bý6Ó`4!ª´4qó��úy�Fáêm½À�9øc;ð‡~¬ßÙŒ7ÀnkÀÐøZ¦ª*(ˆÜY¥C­Wñ¿vcFCûj78²¯ªªA¿`6«Sin¿ÍÄ7îÜUjþs
tZR OÕç<°,x¡É¼­˜¿z½ö„~ȯg_–ÛŸ2*sgÍÍt¦‡!ÑO»¦|—ÛíêÐ0`_yvÌï²ú¤~«Š£Â�Ý�y$¶¡`¶(€NKÌ–Wvº˜­@•l§ÎÌdGf…4+Aü/æ›]¸wâ�¹€ Ø�øÝ]RŸÐo˜N6ãÀ`bÀx	µÔo„@O5‘zÁ)¦R.ÖΤÖC”˜Ìx‚bfˆ]Ïl(N‹€A¿ËêSú5U.0� µýëh˜SÌAsJ¹¹
Úõ
Ì$8÷…_ì“Ðï¹æ´´YíV §T`NË�©eåíȾÞÓ€QYìîÌÚ7&¥ß 0e½mª=`p	ÜòÀÙ
õÛ30¸¤å�Ûõìà)íLI)ýžO`ð9¶d`¶V&Ôiâ�M¦¢Ù `ú�NIí«`NËÛÕ�ÙrWI[cf×]­ìÂÍYUùíÚ¦Þï(€ÉXVSër0©ËjæË—íÜòVÛÂiöLXàü}º¡ð-ÊaR—ÕËú
,«Ïø
«e9ÀðE-m„1`¶xKÚ(³9 þ[OýX`f`Jêw†×á>Li,p3˜]ØSÅ�ycߘ%ý2`*êw¶mºÝ‡1ëJéh€�Õl1óÀjæt0öh bÁŽN}`‚G§ho²l[Ñ®Øý™¸Ê³ÛÕ7»¦ÞØG§ï(aÕ~íÑÀÚcÖíŸ9´!ç;½.0tøÈN™¶˜5;|„Ö[�¡ÃLj…=|Üò�	>Âiãü#t[²‡€»~à‡�3
?ØG�8³Éq¤>©_vx	Vöð1ä·™	|Y^¬ï*™8/êã˾ÃI€i]ÿY`¶–,U·¢¹ÈÐÿQêƒÄaDZ̲2Rßã¿þ¥úù׃i0§KB©—ÌÐ#èTò/EY`DY`DY`DY`DY`DY`DYª3y„w'mõòä—A¿';‚èÓáááS¼åXÚÏ –—ÁxØx;‘æã#^²×>®�=å“Úo[úôtÖ	0Ÿ[Z»v€Iíw¡€©E»S`œ»ÕúmKø^L(šI~Óí2€™ÅQî�ó4T+À€”�—c¢4�âäÑG}¯²O¸@j
j�ûhª±«xõ�4èFõ11Àð¶=ôöˆîÂýv.•Mšâç*Xêó©)öÒÌCS>ƒ¨ûY-pŸüz¯èý(§j$Ÿô�maÛ›‹ú„;¸ë~5…€±¤Ì80ޏ‰1ÄvV`71±ýljFLÀ٬L¬ßÎF¢JMÜ|`L5ìÔƒý¬8²Çz¯èýÌo4bÚC×Í!/Cãk
-3CÌ#3�ÚG¢Ç¹<šM"S‹îdö14™|Tå
˜ Ÿ™rf?™pT,0±~û“»JÂ@Í>AkU¨BCÀ€²÷dÞVë?i`p$úé©)gö‡‡ìÓÙaî¬W˜Gìuã]>.æQ8ûào’�æ�…ÞâÀ´¹…¸Š"À<¥:ÖøÈøÄí=ñºhý!óýÉ™™ì(£À¬�fÅÅ�ùøh0�¨Ô<`PcÀ`=¹"·îÉÚ¹ÀÄúíO.0„0ÌšjSó©§|ÂTÊ}!Y;H�ÚúeôÌÇ	fEàÖ�h¾Y`>aÎÓðc�q'š¾�ÑI«x»\r”
ÌÇÉŠÀDúíO«£²XSÝ0è'˜ú2®‰ú—Ò˜G»o
\7ýp`žìzæÑÇ…À<
{HæÑŠÀDúíO«sH³A+À<]˜€ÏbÀ¨�³^�yä®’}cÀ|:Û¼¯ÌSw•ô´g`>ÂVë¢euH©ËjØ«áþ–íÃ|ä+¬á´Ú²š³(‡I]V£ÌÒ}˜O|…Õ’80~àÙŽ^ìÃL`�fŸqãn6(4Î.m„1`žrkܸ³ó�ÆÀ€Ì,Ì!õû†×Á>L-ð~&—àŽ+
D¦v4ðÑ&x4ðí-ì·7™õ%�µUOàhÀ”?ÅÖ‹¦ÿ(!Xhý8À�>¡=fÝþ™C¥óÑ>>
¸ ÃÇ0öðñ‘LøðN'“ÙÂ~û“>2$žºææ/¾½§O69ŽÔãáãS˜ðá#œ6Î?‚@7“|	|%å-'#‘³ŠQºž&.U[LF$&[‰9d·ÿÚc0	0Ùú”†Â·ù‚`Dy`DY`DY`DY`DY`DY`DY"`>½À[n�¥)r[6kç—Á8Ø8{Ñ¿ÇÇǯñ–W`iŸrG.ƒq±q·*Ìç³V�ùœØÂÚµLj¿©ú÷õ¬S`þMô`íÚ&µß ˜™E#LÂë£`fq„;ffшŠb–ÿt»`fq¤[Æ•þÅ0Œ'& &.& :0*«œ™›*hêó…)vÓÍy¥±„`_ÜgÓüðŠ¼Ÿº¿Ÿõ�maÛ›‹ú„;°ë¾±\`TiŠ�_« ©ÏצØM/¡^EÒ´À†æ
íê�û׸±õÞ8Ð/vþÿÕ?¶…mo.êîð?ÀuŸ§ÀhBT鑉_ƒ–ƒ}-pP~¸ƒ¼ŸÏèüjX�\7Glœ�«'`4!ªôØÄËêç5¬‡+´«î�Âzoà÷_ôþ5¼ÇõîkÀÐ8C!�_8¼' `óŸÏH·égUûBWÂáóQdj1\Í>‡&l÷G@~]û£#ŽŠûïNî*	4ÿùçXSÿUŸ¯µÑ1¼œvžÌ[ka½öø/Žˆü»öÇÇößP	À¼ @ºM?ÓB?�yÎ>x;ÝÅggf²£‹³ú†aš"À¼¦À9Ö˜×øÌì1«·{½¤þ˜�ä_gf²£�Ó Ý²ŠƒÁXŒm¥þÊ_„Wó©'—Ÿ1…r_HÖÎûïN.0øË�ƒõX‰‰s
Qó©§‘ü‹)•ûB²v.0ØC�˜Ï�¤0îûJ€!¿˜#Á¨Ü‰füÀ@6ܘÏG+SËÆÛVC` +¦ëëfÀü{¼"0µì|%�\Js`^ìþëÀàRšó:Ôÿh�ya[7æÅŠÀ|¶«¦$ïÚ˜×+ð¹‚ÆÌw•ôB€	óÚ]%½˜„e5¿4^VÃ^
÷»læsÒvµò²šµ²×¦ËjØ«áþ—íÃüËWX
å÷´!Æ€yá4µo‚¹AÀàÆ1H 0¦ÿîäì~Ø
¸×ÌknMdö
07´×ÆÀÀ2L 0¦ÿ†âÀ˜u	%‘t4ÀÎl^8ææŠ«»w"Ó;øì<øŒö/Ü#‰®dÖ!”4ÒÑ;£yÍ̽U=×Úy¢£�õ°Jú×&x4ð/Ú¿¦£�v�™¹ÀÐá#;ezÁ¬i. 3 Áé�#ÀØÃÇ>0áÃǹ}¹ÀÐá#;ezͬÙá#5m¯Q èð1Z�‡�¯}`‡�xÚy{5-þx÷ùÁùÔ²| ¥|aŒ`V‘Ò‹>–¬çP¯—,Q—ÕŸo-¦ë\ò¼jYîØJn9Z-æs¿ÿèÜèß%¹ã²úó-ù—�¢,	0¢,	0¢,	0¢,	0¢,	0¢,	0¢,E�ùrrròŽ][Ñ—“åeÐ[«ý6Ð×étúoy–;¸åe0NößÑH
˜/ï	À|‰”ÇíÚ&µß¦úúaÖ+0_=Z»v€IíW+
ÌŒB7˜úm*üE.&µ$÷~»N�™Å`Z’Œ+¦&Æ\&&.& FÇÍožnê:˜/:5Æú�ãÊÔÏ
¾x‘þ‚íßéf˜ÍÎTG_ô�maÛ›‹íÇ50*k4ÅÓ*(êóƒ)æéäשúŸzÆb°×³@;jîî+Tûú¸Ð¯íg¦:øªlÛÞ\Ô'Üá�ë~±ì†s‚õ€9Ñ‹cêçXÔÛþÄ,`ý¥S®x0›Õ€¡q
ŒŽ´*�šøøÀL§–ˆÖ~Ï~;²ó‡Í>`ÇÞ¸À/µW Îo4ÌS×Í”ýw0±~ËîÃ``Ôß·‰˜ÌC˧Š	êUÛ/¡Éä‹*WÀ`sýôΔ3{K¥ŒWßrWI�ùÏW7Vçú/ÿ+´D{ej÷už,Ì[ŒÙ×ë¿NY?æéƒ)göÓ)GŃãÉ÷ådæQ‘	Ì»pöÁß$0ÿ¡_‡ƒ80á¨{E€ù@�âÆÚZ`^»X.‚¶Q X?ˆ	ÌLvôQ`VJ¿,0ú/}†¯ú˜ñÀ›ù” ƒë¡/zP3Ó» �Œ3¤^åCo�iÀp{õ‘
L­ŸH=µÇ7ÖÔ}!Y;O¦æ$)Œ;Ñ|3À|Å\g	0_§Ói`°=ãzë•­¾›AÒF'µ¡©!˜/'+Cãê[«óuêY®ßîë´0Ô>/[OTÿÀàRšó.`wÞ�™N½))ÌtÚlJ¢ö˜»€9ñÞú1`Þ}iÌ»�	øìG+ó!�ÃÌêíx.â÷ṧ�	øLæË‰·Š£vÖZæ�»Jz'ÀûYPïóÁ]%}èûö.«Í†í»º§Ôe5ìÕ0`–îÃ|±
¡Õ–Õ°‡âN³00¡&uYMí	˜¥û0_c�&ˆ�
Á˜_àˆÊ	$þ–ð•là1`f`p_pãn6Ì>_¼Ú
¸ø̘ç0_ù†�6´C;_ÉÀ`{Ì,Ì”úû`Ç“©:0voþ‹{e‹›àô@Gêa•ôÅ&x4ðíY6¾jЛȬ;(Id«:“ùÀŒÙ*	Ïz¦ìê·#;_t4™>ØÑÀW˜àÑÀW´ÿàYä)�ÚSH vô#@|±Is¤ßùÀ„gª=|ùí^.0_Ù”óñ�lí>Ì”öcìáã‡@»©ÝèsD‡�`¾ÖúÁDñôs=UN3H4ΣrçÿU¿1B	0«H€Q&A8ú�¹$͵·øiõ»¡s>”›+®”[ŽVÎ×D)úš™+æÚ�[ò/EY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY`DY**‘(CŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KŒ(K˜b]_¶‹îç�­lC“›y[1õzí	ý�_Ï^=ì°~¨ßˆßFi»°¹Ì$50ëá쇿I`¾YùÃ~8 Ü÷pгIt¬0›^ÙÎB`6U²ì83“c§GËúá¾[’»Ók‚æcƒþCÇv±ÀZ¿žéðĵîÂÝÒÖsÿh@··«´qLIf=Râ@mõ= Ö;€ñ·ôƒõ%óÃ�ñ�6m?hßaC3‡†µÃGOÛ6I
Ö>®ûÀ+vø¹>`*:|d¡ÙdÖìðZo.fÇ&©A{ø¸é8|´ý”þ8[Ñ¿X{¾ì»ì«Áú¾¾„Ûm?#ŽÓíàši„Á)F8&®Í%¯øÍh.ÒÏøºíg|Á)NX–ª,‡$R_¶˜H,î¾Ó~ÆÌÐ#X¢�%!‰Ô/˜#µQ–Q–Q–Q–Q–Q–\`vgu¿lvÅ3¸kA)ý­ÉC¼»>iËgÈS{Þ;‘f÷ŸÚf7R·k˜Ô~Suý!ZŒkŸâ·`RûmE+ãÜy-"åq»s	L-²L°L€ÁjF€q$À$ÀÄ%ÀÄ€Ù��é a6;˜ÍLàvÊêîðîf&Æ^àv�?ÛŽ*²úUgÀ··~;Æp2�âäáu}}2™Àeb*MA-p×M5¶óüªú‡t£ú˜`ܺþºéÔÚ[¿C3›ÍL  »3jœûk³ªÌŒ"~ætö³³ÙÌ}iäõký˜´·~»Æ’Rq`/pcˆí<¿¦~Žj�1…Ó€ü˜´·~{Æh ]8Šy:3å,ªÔ¢ÂxVÍ"S‹¶ñüäö«vÏXÕb¿-ÉÌóÍ£s�èq.«IdjÑ��T×kÓ™©¿®�©€JýôД?äv5LµØoÇr€¡€Ã{ÿî½?ô0ñ\m"À$õ‹¾9 ÜwG²ÀÁ¬ܺ\
°
0»³�™Í‚§©À¤ô7˜êôŒN.ÝÀ{ÀØå’×.
˜ë“4`Ô@VÖ~4Àà’–è
˜”~áe½LI�ƒKiÌCÏò°²öãælE`ÎÂ=$“Я¹™Ñì×0¯/æa¬]"0S�™Ðì7r`ÎÜUÒY_ÀÔû…ŠYŠß¶4Q;d½óÐ݆Á¶×'“¿«¾¬†½
¸¥û0»|¥ã+uY½¬_¶4"`úÈa®Ã–ê¢eu¸]Ú²öT¸?o†n˜Qä08ØHc�«"ÀÌàs7îªû0ËúÅ�;Öûn?Àø�g;vxi°3�
<`hãKÑ~p`øý®¸àÑÀ.ÚcêïýÛÕ‚À&ök�pcÏØ÷
Ì„Ÿ	À%´Óëoéëa•tÝÆ?ÐíP?	ïU{xæ.|ø§�ó:|<t±k“ÔH}R¿pøX‘°˜ëöðña°!%©‘z<||è8|¬ÈØÌyÑØ¾ìÛ—Æñe_æÜH€YQ³ÿ(0f%µø¯[Ε&$,�G`†Á0Çæ#TŒ(KŒ(KŒ(KŒ(KŒ(KŒ(KÌ‡Oð–[`ir{Ž—õ¨ÇÇǯð–W`i¿ƒI+ëP
˜ßŸT�ó{bKk×0©ý.Ó�WU§ÀÜHô`íÚ&µß ˜Ê¢&;rß0•E#ÌÊâß0®˜ð»D€`<	0	0q	0Õ�QÙoenž¨`©Ï'¦øÐEêДWðÕ`_Õ÷»_OOxèG9U#ù]ÿ/[­x[úÁzX%Ýð�ñ�̸^áè;š’*:|d¡yâš›G¼øöž~·Ij¤ŸøÀÓûm.:|d!á�`‡�Ôü•½FwÃ&©‘z<||å8|¬È˱?ÞFZü%ð¤¯õ¥Û¿ì»ì+=϶ÏA0«H€	éIâ+þp À
Õï«%¯öeõ}é¸o`“BrØQâ0Ö~19]¹¾·a¾êÂí`~OÉíãç¸Ùõ=©#jå_>в$Àˆ²$Àˆ²$Àˆ²$Àˆ²$Àˆ²$Àˆ²ææÉÉÉ[vmE7OÒÊD£•ææÛ@E07#åq»v€IíWÔ�¢ÀÌ… 0"’#Ê’#Ê’#Ê£ãf‚7Osu�ÌM�ýšcýÖqeêç7=Ânbû·ºfÑóÕÑMýÃ[èz;ž߯3 ì†h8ÁˆzÀœh�âÀ˜ú¹‡ºiÛŸ˜e¬¿4bʕӀü˜´·~˜e÷al€ÞB]`nZn:ULP¯ÚÞMb7U¹›ë§·¦ü-·{«‘ ñT‹ýŠzró¤ò¨Èæm8ëáo˜oЯó‚Áœ†Â}‹•Fÿ¥W8-Ñ]&õÿÞ†"‡°ÅK‰Iå$LzfzëÙññ ½3

ÌMHR7å©sÓ&OÌàbgIó¬òm¹i�|†–©ÀÜŒ;´`F!þ*c~�#('�ø_»¯d``�S¹À¼e{ÏÖ^€…êÀà¾]¿À•-n‚�ó¶ôƒõ°Jºéã
¼µãA{f
C§ƒöH�en$p7m’©ÇÃÇ·>0�ÃÇŠüV€�œfèÁ¸ÛxDJŒ(KÎ>ÌЃq5¶ñˆ”øiõÛ¡ãhlãi9_o•Æ6‘–üËGQ–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–Q–þMQ“‚®þòIEND®B`‚urwid-urwid-69907e4/docs/manual/textlayout.rst000066400000000000000000000133101526073734300214560ustar00rootroot00000000000000.. _text-layout:

***************
  Text Layout
***************

.. currentmodule:: urwid

Mapping a text string to screen coordinates within a widget is called text
layout. The :class:`Text` widget's default layout class supports
aligning text to the left, center or right, and can wrap text on space
characters, at any location, or clip text that is off the edge, optionally
inserting an ellipsis character.

::

    Text("Showing some different alignment modes", align=...)

    align='left' (default)
    +----------------+   +------------------------+
    |Showing some    |   |Showing some different  |
    |different       |   |alignment modes         |
    |alignment modes |   +------------------------+
    +----------------+

    align='center'
    +----------------+   +------------------------+
    |  Showing some  |   | Showing some different |
    |   different    |   |    alignment modes     |
    |alignment modes |   +------------------------+
    +----------------+

    align='right'
    +----------------+   +------------------------+
    |    Showing some|   |  Showing some different|
    |       different|   |         alignment modes|
    | alignment modes|   +------------------------+
    +----------------+

::

    Text("Showing some different wrapping modes\nnewline", wrap=...)

    wrap='space' (default)
    +----------------+   +------------------------+
    |Showing some    |   |Showing some different  |
    |different       |   |wrapping modes          |
    |wrapping modes  |   |newline                 |
    |newline         |   +------------------------+
    +----------------+

    wrap='any'
    +----------------+   +------------------------+
    |Showing some dif|   |Showing some different w|
    |ferent wrapping |   |rapping modes           |
    |modes           |   |newline                 |
    |newline         |   +------------------------+
    +----------------+

    wrap='clip'
    +----------------+   +------------------------+
    |Showing some dif|   |Showing some different w|
    |newline         |   |newline                 |
    +----------------+   +------------------------+

    wrap='ellipsis'
    +----------------+   +------------------------+
    |Showing some di…|   |Showing some different …|
    |newline         |   |newline                 |
    +----------------+   +------------------------+

If this is good enough for your application feel free to skip the rest of this
section.

.. seealso::
   :class:`Text widget reference `


Custom Text Layouts
===================

The :class:`StandardTextLayout` is set as the class variable
:attr:`Text.layout`. Individual :class:`Text`
widgets may use a different layout class, or you can change the default by
setting the :attr:`Text.layout` class variable itself.

A custom text layout class should extend the
:class:`TextLayout` base class and return text layout
structures from its ``layout()`` method.

.. seealso::
   :class:`TextLayout reference `


Tab Characters
==============

:class:`StandardTextLayout` moves a tab character to the next tab stop, as a word processor does.
Tab stops count rendered screen columns from the start of each displayed line, so a wide character counts
as two columns. The explicit ``tab_stops`` come first, then stops repeat every ``tab_stop_every`` columns
(8 by default). With ``tab_stop_every=0`` a tab past the last explicit stop takes no columns::

    layout = urwid.StandardTextLayout(tab_stops=(12, 20), tab_stop_every=4)
    text = urwid.Text("name\tvalue\tcomment", layout=layout)

A tab stays one character of the text, so the cursor of an :class:`Edit` widget steps over it in one move.
A tab that would reach past the line end is cut there, and in the ``space`` wrap mode a tab is a break
opportunity, like a space.


Text Layout Structures
======================

::

    "This is how a string of text might be displayed"
    0----5---10---15---20---25---30---35---40---45--

    0----5---10---15---+   right_aligned_text_layout = [
    |     This is how a|     [(5, 0), (13, 0, 13)],
    |    string of text|     [(4, 13), (14, 14, 28)],
    |might be displayed|     [(18, 29, 47)]
    +------------------+   ]

The mapping from a text string to where that text will be displayed in the
widget is expressed as a text layout structure.

Text layout structures are used both for rendering :class:`Text`
widgets and for mapping ``(x, y)`` positions within a widget back to the
corresponding offsets in the text. The latter is used when moving the cursor in
:class:`Edit` widgets up and down or by clicking with the mouse.

A text layout structure is a list of one or more line layouts. Each line layout
corresponds to a row of text in the widget, starting from its top.

A line layout is a list zero or more of the following tuples, each expressing
text to be displayed from left to right:

A. (*column width*, *starting text offset*, *ending text offset*)
B. (*column width of space characters to insert*, *text offset* or ``None``)
C. (*column width*, *text offset*, *new text to insert*)``

Tuple A displays a segment of text from the :class:`Text` widget.
Column width is explicitly specified because some characters within the text
may be zero width or double width.

Tuple B inserts any number of space characters, and if those characters
correspond to an offset within the text, that may be specified.

Tuple C allows insertion of arbitrary text. This could be used for hyphenating
split words or any other effect not covered by A or B. The
:class:`StandardTextLayout` uses it for the ellipsis of the ``ellipsis`` wrap mode
and for tab characters, which it displays as spaces up to the next tab stop.

.. seealso::
   :class:`TextLayout reference `,
   :class:`StandardTextLayout reference `
urwid-urwid-69907e4/docs/manual/userinput.rst000066400000000000000000000077561526073734300213130ustar00rootroot00000000000000.. vim: set fileencoding=utf-8:

.. _user-input:

**************
  User Input
**************

.. currentmodule:: urwid

All input from the user is parsed by a display module, and returned from either
the :meth:`get_input() ` or
:meth:`get_input_nonblocking() ` methods as a list.
Window resize events are also included in the user input.

The :class:`MainLoop` class will take this input and pass each
item to the widget methods :meth:`keypress() ` or
:meth:`mouse_event() `. You may
filter input (possibly removing or altering it) before it is passed to the
widgets, or can catch unhandled input by passing functions into the
:class:`MainLoop` constructor. If the window was resized
:class:`MainLoop` will query the new display size and update
the screen.

There may be more than one keystroke or mouse event processed at a time, and
each is sent as a separate item in the list.

.. _keyboard-input:

Keyboard Input
==============

Not all keystrokes are sent by a user's terminal to the program, and which keys
are sent varies from terminal to terminal, but Urwid will report any keys that
are sent.

============= =================
Key pressed   Input returned
============= =================
H             ``'h'``
SHIFT+H       ``'H'``
SPACE         ``' '``
ENTER         ``'enter'``
UP            ``'up'``
PAGE DOWN     ``'page down'``
F5            ``'f5'``
SHIFT+F5      ``'shift f5'``
CTRL+SHIFT+F5 ``'shift ctrl f5'``
ALT+J         ``'meta j'``
============= =================

With Unicode :ref:`text encoding ` you will also receive
Unicode strings for any non-ASCII characters:

=========== ==============
Key pressed Input returned
=========== ==============
é           ``u'é'``
Ж           ``u'Ж'``
ã‚«          ``u'ã‚«'``
=========== ==============

With non-Unicode :ref:`text encoding ` characters will be sent
as-is in the original encoding.

=========== =========================================
Key pressed Input returned (each in its own encoding)
=========== =========================================
é           ``'é'``
Ж           ``'Ж'``
ã‚«          ``'ã‚«'`` (two bytes)
=========== =========================================

Urwid does not try to convert this text to Unicode to avoid losing any
information. If you want the input converted to Unicode in all cases you may
create an input filter to do so.


.. _mouse-input:

Mouse Input
===========

Mouse input is sent as a (*event*, *button*, *x*, *y*) tuple. *event* is a string
describing the event. If the *SHIFT*, *ALT* or *CTRL* keys are held when a mouse
event is sent then *event* may be prefixed by ``'shift '``, ``'meta '`` or
``'ctrl'``. *button* is a number from 1 to 5. *x* and *y* are character
coordinates starting from ``(0, 0)`` at the top-left of the screen.

Support for the right-mouse button and use of modifier keys is poor in many
terminals and some users don't have a middle mouse button, so these shouldn't
be relied on.

``'mouse press'`` Events
------------------------

A mouse button was pressed.

=============== ======================
`button` number Mouse button
=============== ======================
1               Left button
2               Middle button
3               Right button
4               Scroll wheel up [#first]_
5               Scroll wheel down [#first]_
=============== ======================

.. [#first] typically no corresponding release event is sent

``'mouse release'`` Events
--------------------------

Mouse release events will often not have information about which button was
released. In this case *button* will be set to 0.

``'mouse drag'`` Events
-----------------------

In the rare event that your user is using a terminal that can send these events
you can use them to track their mouse dragging from one character cell to the
next across the screen. Be aware that you might see *x* and/or *y* coordinates
one position off the screen if the user drags their mouse to the edge.
urwid-urwid-69907e4/docs/manual/wanat.py000066400000000000000000000013621526073734300201720ustar00rootroot00000000000000from __future__ import annotations

import urwid


class Pudding(urwid.Widget):
    _sizing = frozenset((urwid.FLOW,))

    def rows(self, size: tuple[int], focus: bool = False) -> int:
        return 1

    def render(self, size: tuple[int], focus: bool = False) -> urwid.TextCanvas:
        (maxcol,) = size
        num_pudding = maxcol // len("Pudding")
        return urwid.TextCanvas([b"Pudding" * num_pudding], maxcol=maxcol)


class BoxPudding(urwid.Widget):
    _sizing = frozenset((urwid.BOX,))

    def render(self, size: tuple[int, int], focus: bool = False) -> urwid.TextCanvas:
        (maxcol, maxrow) = size
        num_pudding = maxcol // len("Pudding")
        return urwid.TextCanvas([b"Pudding" * num_pudding] * maxrow, maxcol=maxcol)
urwid-urwid-69907e4/docs/manual/wanat_multi.py000066400000000000000000000011021526073734300213740ustar00rootroot00000000000000from __future__ import annotations

import urwid


class MultiPudding(urwid.Widget):
    _sizing = frozenset((urwid.FLOW, urwid.BOX))

    def rows(self, size: tuple[int], focus: bool = False) -> int:
        return 1

    def render(self, size: tuple[int] | tuple[int, int], focus: bool = False) -> urwid.TextCanvas:
        if len(size) == 1:
            (maxcol,) = size
            maxrow = 1
        else:
            (maxcol, maxrow) = size
        num_pudding = maxcol // len("Pudding")
        return urwid.TextCanvas([b"Pudding" * num_pudding] * maxrow, maxcol=maxcol)
urwid-urwid-69907e4/docs/manual/wcur1.py000066400000000000000000000022321526073734300201160ustar00rootroot00000000000000from __future__ import annotations

import urwid


class CursorPudding(urwid.Widget):
    _sizing = frozenset(["flow"])
    _selectable = True

    def __init__(self) -> None:
        super().__init__()
        self.cursor_col = 0

    def rows(self, size: tuple[int], focus: bool = False) -> int:
        return 1

    def render(self, size: tuple[int], focus: bool = False) -> urwid.TextCanvas:
        (maxcol,) = size
        num_pudding = maxcol // len("Pudding")
        cursor = None
        if focus:
            cursor = self.get_cursor_coords(size)
        return urwid.TextCanvas([b"Pudding" * num_pudding], [], cursor=cursor, maxcol=maxcol)

    def get_cursor_coords(self, size: tuple[int]) -> tuple[int, int]:
        (maxcol,) = size
        col = min(self.cursor_col, maxcol - 1)
        return col, 0

    def keypress(self, size: tuple[int], key: str) -> str | None:
        (maxcol,) = size
        if key == "left":
            col = self.cursor_col - 1
        elif key == "right":
            col = self.cursor_col + 1
        else:
            return key
        self.cursor_x = max(0, min(maxcol - 1, col))
        self._invalidate()
        return None
urwid-urwid-69907e4/docs/manual/wcur2.py000066400000000000000000000005711526073734300201230ustar00rootroot00000000000000from __future__ import annotations

import urwid


class Widget(urwid.ListBox):
    def get_pref_col(self, size: tuple[int, int]) -> int:
        return self.cursor_x

    def move_cursor_to_coords(self, size: tuple[int, int], col: int, row: int) -> bool:
        assert row == 0  # noqa: S101  # in examples we can use `assert`
        self.cursor_x = col
        return True
urwid-urwid-69907e4/docs/manual/widgets.rst000066400000000000000000001020751526073734300207110ustar00rootroot00000000000000***********
  Widgets
***********

.. currentmodule:: urwid

.. _widget-layout:

Widget Layout
=============

Urwid uses widgets to divide up the available screen space. This makes it easy
to create a fluid interface that moves and changes with the user's terminal and
font size.

.. image:: images/widget_layout.png

The result of rendering a widget is a canvas suitable for displaying on the
screen. When we render the topmost widget:

1. The topmost widget *(a)* is rendered the full size of the screen
2. *(a)* renders *(b)* any size up to the full size of the screen
3. *(b)* renders *(c)*, *(d)* and *(e)* dividing its available screen columns
   between them
4. *(e)* renders *(f)* and *(g)* dividing its available screen rows between
   them
5. *(e)* combines the canvases from *(f)* and *(g)* and returns them
6. *(b)* combines the canvases from *(c)*, *(d)* and *(e)* and returns them
7. *(a)* possibly modifies the canvas from *(b)* and returns it

Widgets *(a)*, *(b)* and *(e)* are called container widgets because they
contain other widgets. Container widgets choose the size and position of their
contained widgets.

Container widgets must also keep track of which one of their contained widgets
is in focus. The focus is used when handling keyboard input. If in the above
example *(b)* 's focus widget is *(e)* and *(e)* 's focus widget is
*(f)* then keyboard input will be handled this way:

1. The keypress is passed to the topmost widget *(a)*
2. *(a)* passes the keypress to *(b)*
3. *(b)* passes the keypress to *(e)*, its focus widget
4. *(e)* passes the keypress to *(f)*, its focus widget
5. *(f)* either handles the keypress or returns it
6. *(e)* has an opportunity to handle the keypress if it was returned from
   *(f)*
7. *(b)* has an opportunity to handle the keypress if it was returned from
   *(e)*
8. *(a)* has an opportunity to handle the keypress if it was returned from
   *(b)*

Box, Flow and Fixed Widgets
===========================

The size of a widget is measured in screen columns and rows. Widgets that are
given an exact number of screen columns and rows are called box widgets. The
topmost widget is always a box widget.

Much of the information displayed in a console user interface is text and the
best way to display text is to have it flow from one screen row to the next.
Widgets like this that require a variable number of screen rows are called flow
widgets. Flow widgets are given a number of screen columns and can calculate
how many screen rows they need.

Occasionally it is also useful to have a widget that knows how many screen
columns and rows it requires, regardless of the space available. This is
called a fixed widget.

.. list-table:: How a Widget's Size is Determined
   :widths: 20 40 40
   :header-rows: 1

   * - sizing mode
     - width
     - height
   * - ``'box'``
     - container decides
     - container decides
   * - ``'flow'``
     - container decides
     - widget's :meth:`rows() ` method
   * - ``'fixed'``
     - widget's :meth:`pack() ` method
     - widget's :meth:`pack() ` method

It is an Urwid convention to use the variables :attr:`maxcol` and
:attr:`maxrow` to store a widget's size. Box widgets require both of ``(maxcol,
maxrow)`` to be specified.

Flow widgets expect a single-element tuple ``(maxcol,)`` instead because they
calculate their :attr:`maxrow` based on the :attr:`maxcol` value.

Fixed widgets expect the value ``()`` to be passed in to functions that take
a size because they know their :attr:`maxcol` and :attr:`maxrow` values.

.. _basic-grafic-widgets:

Included Widgets
================

:ref:`Widget class reference `

.. image:: images/urwid_widgets_1.png

Basic and graphic widgets are the content with which users interact. They may
also be used as part of custom widgets you create.

.. image:: images/urwid_widgets_2.png

.. _decoration-widgets:

Decoration Widgets
==================

Decoration widgets alter the appearance or position of a single other widget.
The widget they wrap is available as the
:attr:`original_widget ` property.
If you might be using more than one decoration widget you may use the
:attr:`base_widget ` property to access the
"most" original_widget.
:attr:`Widget.base_widget` points to ``self`` on all non-decoration widgets, so
it is safe to use in any situation.

.. _container-widgets:

Container Widgets
=================

Container widgets divide their available space between their child widgets.
This is how widget layouts are defined. When handling selectable widgets
container widgets also keep track of which of their child widgets is in focus.
Container widgets may be nested, so the actual widget in focus may be many
levels below the topmost widget.

Urwid's container widgets have a common API you can use, regardless of the
container type.  Backwards compatibility is still maintained for the old
container-specific ways of accessing and modifying contents, but this API
is now the preferred way of modifying and traversing containers.

::

  container.focus

is a read-only property that returns the widget in focus for this container.
Empty containers and non-container widgets (that inherit from Widget)
return ``None``.

::

  container.focus_position

is a read/write property that provides access to the position of the
container's widget in focus.  This will often be an integer value but may be
any object.
:class:`Columns`, :class:`Pile`, :class:`GridFlow`, :class:`Overlay` and
:class:`ListBox` with a :class:`SimpleListWalker` or :class:`SimpleFocusListWalker`
as its body use integer positions.  :class:`Frame` uses ``'body'``, ``'header'``
and ``'footer'``;  :class:`ListBox` with a custom list walker will use the
positions the list walker returns.

Reading this value on an empty container or on any non-container widgets
(that inherit from Widget) raises an IndexError.  Writing to this property with
an invalid position will also raise an IndexError.  Writing a new value
automatically marks this widget to be redrawn and will be reflected in
``container.focus``.

::

  container.contents

is a read-only property (read/write in some cases) that provides access to a
mapping- or list-like object that contains the child widgets and the options
used for displaying those widgets in this container.  The mapping- or list-like
object always allows reading from positions with the usual ``__getitem__()``
method and may support assignment and deletion with ``__setitem__()`` and
``__delitem__()`` methods.  The values are ``(child widget, option)`` tuples.
When this object or its contents are modified the widget is automatically
flagged to be redrawn.

:class:`Columns`, :class:`Pile` and :class:`GridFlow` allow assigning an
iterable to ``container.contents`` to overwrite the values in
with the ones provided.

:class:`Columns`, :class:`Pile`, :class:`GridFlow`, :class:`Overlay` and
:class:`Frame` support ``container.contents`` item assignment and deletion.

::

  container.options(...)

is a method that returns options objects for use in items added to
``container.contents``.  The arguments are specific to the container type,
and generally match the ``__init__()`` arguments for the container.
The objects returned are currently tuples of strings and integers or ``None``
for containers without child widget options.  This method exists to allow
future versions of Urwid to add new options to existing containers.  Code
that expects the option tuples to remain the same size will fail when new
options are added, so defensive programming with options tuples is strongly
encouraged.

::

  container.__getitem__(x)
  # a.k.a.
  container[x]

is a short-cut method behaving identically to:
``container.contents[x][0].base_widget``.
Which means roughly "give me the child widget at position *x* and skip all
the decoration widgets wrapping it".  Decoration widgets include
:class:`Padding`, :class:`Filler`, :class:`AttrMap` etc.

::

  container.get_focus_path()

is a method that returns the focus position for this container *and* all child
containers along the path defined by their focus settings.  This list of
positions is the closest thing we have to the singular widget-in-focus in
other UI frameworks, because the ultimate widget in focus in Urwid depends
on the focus setting of all its parent container widgets.

::

  container.set_focus_path(p)

is a method that assigns to the focus_position property of each container
along the path given by the list of positions *p*.  It may be used to restore
focus to a widget as returned by a previous call to ``container.get_focus_path()``.

::

  container.get_focus_widgets()

is a method that returns the ``.focus`` values starting from this container
and proceeding along each child widget until reaching a leaf
(non-container) widget.

Note that the list does not contain the topmost container widget
(i.e., on which this method is called), but does include the
lowest leaf widget.

::

  container.__iter__()
  # typically
  for x in container: ...

  container.__reversed__()
  # a.k.a
  reversed(container)

are methods that allow iteration over the *positions* of this container.
Normally the order of the positions generated by __reversed__() will be the
opposite of __iter__().  The exception is the case of :class:`ListBox` with
certain custom list walkers, and the reason goes back to the original way list
walker interface was defined.  Note that a custom list walker might also generate
an unbounded number of positions, so care should be used with this interface and
:class:`ListBox`.


Pile Widgets
------------

:class:`Pile` widgets are used to combine multiple widgets by stacking them vertically.
A Pile can manage selectable widgets by keeping track
of which widget is in focus and it can handle moving the focus between widgets
when the user presses the *UP* and *DOWN* keys.
A Pile will also work well when used within a :class:`ListBox`.

A Pile is selectable only if its focus widget is selectable. If you create a
Pile containing one Text widget and one Edit widget the Pile will choose the
Edit widget as its default focus widget.


Columns Widgets
---------------

:class:`Columns` widgets may be used to arrange either flow widgets,
box widgets or fixed widgets horizontally into columns.
Columns widgets will manage selectable widgets by keeping track of which column is in focus and it can
handle moving the focus between columns when the user presses the *LEFT* and *RIGHT* keys.
Columns widgets also work well when used within a :class:`ListBox`.

Columns widgets are selectable only if the column in focus is selectable.
If a focus column is not specified the first selectable widget will be chosen as the
focus column.


GridFlow Widgets
----------------

The :class:`GridFlow` widget is a fixed/flow widget designed for use
with :class:`Button`, :class:`CheckBox` and :class:`RadioButton` widgets.
It renders all the widgets it contains the same width
and it arranges them from left to right and top to bottom.

The GridFlow widget uses Pile, Columns, Padding and Divider widgets to build a
display widget that will handle the keyboard input and rendering. When the
GridFlow widget is resized it regenerates the display widget to accommodate the
new space.


Overlay Widgets
---------------

The :class:`Overlay` widget is a box/flow/fixed widget that contains two other widgets.
The bottom widget is BOX rendered the full size of the Overlay widget
and the top widget is placed on top, obscuring an area of the bottom widget.
Widget sizing depends on the top widget sizing and sizing options.
This widget can be used to create effects such as overlapping "windows" or pop-up menus.

The Overlay widget always treats the top widget as the one in focus.
All keyboard input will be passed to the top widget.

If you want to use a flow widget for the bottom widget,
first wrap the flow widget with a :class:`Filler` widget.


Scrollable Widgets
==================
Scrollable widgets can scroll long content which normally not fit into the screen resolution.
Scrolling is normally supported using keyboard positioning keys and mouse wheel.

For scrolling is expected to be used :class:`ScrollBar` which accept any BOX widget supporting scrollable api.

ScrollBar Widget
----------------
The :class:`ScrollBar` widget draw optional scrollbar on the right or left side of widget if scrolling is required
(widget not fit in the desired amount of rows).
The widget is always rendered BOX and wrapped widget is also should be BOX.
In case of other widget types, they should be wrapped to support BOX sizing.

* :class:`Scrollable` make any FLOW or FIXED widget compatible with scrollable api.
* :class:`ListBox` can be used with :class:`ScrollBar` directly,
  all input is always handled by :class:`ListBox` and it's widgets.

Keyboard positioning keys and mouse wheel is used for scrolling if not handled by the wrapped widget.
Scrolling is done on per-line basis.

Scrollable Widget
-----------------
The :class:`Scrollable` widget is used to make  any FLOW or FIXED widget compatible with scrollable api by changing
target widget render and scrolling along full-size / columns defined sized widget.

Keyboard positioning keys and mouse wheel is used for scrolling if not handled by the wrapped widget.

.. note::
    :class:`Scrollable` do not recognize if selectable wrapped widget part visible or not
    if wrapped widget do not draw cursor.
    This means, keyboard input maybe forwarded to the wrapped selectable widget even if it not visible.
    **Mouse input is not affected by this limitation** due to known column and raw of mouse event.


Comparing to the :class:`ListBox`,
:class:`Scrollable` handles fixed and flow widgets directly instead of using list of small widgets.
:class:`ListBox` should be used to scroll between widgets, which can be multiline by itself.

.. _scrollable-api:

Scrollable API
--------------
Widget pretending to be scrolled via :class:`ScrollBar` should subclass :class:`Widget`
and implement positioning API:

.. currentmodule:: Scrollable

.. py:method:: get_scrollpos(size=None, focus=False)

    Get scrolling position

    :param size: widget render size. If `size` is not given, the currently rendered number of rows is returned.
    :type size: tuple[int, int] | None
    :param focus: widget is focused
    :type focus: bool
    :return: the index of the first visible row.
    :rtype: int

.. py:method:: set_scrollpos(position: SupportsInt)

    **Optional** method for setting scrolling position.

    Method is called on mouse wheel scroll if it implemented and mouse event was not handled by widget.

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

.. py:method:: rows_max(size=None, focus=False)

    Get the total number of rows `widget` can render.

    :param size: widget render size. If `size` is not given, the currently rendered number of rows is returned.
    :type size: tuple[int, int] | None
    :param focus: widget is focused
    :type focus: bool
    :return: the number of rows for `size`
    :rtype: int

.. currentmodule:: urwid


.. _listbox-contents:

ListBox Contents
================

:class:`ListBox` is a box widget that contains flow widgets.
Its contents are displayed stacked vertically, and the
:class:`ListBox` allows the user to scroll through its content.
One of the flow widgets displayed in the :class:`ListBox` is its
focus widget.

ListBox Focus and Scrolling
---------------------------

The :class:`ListBox` is a box widget that contains flow widgets.
Its contents are displayed stacked vertically, and the
:class:`ListBox` allows the user to scroll through its content.
One of the flow widgets displayed in the :class:`ListBox` is the
focus widget. The :class:`ListBox` passes key presses to the
focus widget to allow the user to interact with it. If the focus widget does
not handle a keypress then the :class:`ListBox` may handle the
keypress by scrolling and/or selecting another widget to become the focus
widget.

The :class:`ListBox` tries to do the most sensible thing when
scrolling and changing focus. When the widgets displayed are all
:class:`Text` widgets or other unselectable widgets then the
:class:`ListBox` will behave like a web browser does when the
user presses *UP*, *DOWN*, *PAGE UP* and *PAGE DOWN*: new text is immediately
scrolled in from the top or bottom. The :class:`ListBox` chooses
one of the visible widgets as its focus widget when scrolling. When scrolling
up the :class:`ListBox` chooses the topmost widget as the focus,
and when scrolling down the :class:`ListBox` chooses the
bottommost widget as the focus.

The :class:`ListBox` remembers the location of the widget in
focus as either an "offset" or an "inset". An offset is the number of rows
between the top of the :class:`ListBox` and the beginning of the
focus widget. An offset of zero corresponds to a widget with its top aligned
with the top of the :class:`ListBox`. An inset is the fraction
of rows of the focus widget that are "above" the top of the
:class:`ListBox` and not visible. The
:class:`ListBox` uses this method of remembering the focus
widget location so that when the :class:`ListBox` is resized the
text displayed will stay roughly aligned with the top of the
:class:`ListBox`.

When there are selectable widgets in the :class:`ListBox` the
focus will move between the selectable widgets, skipping the unselectable
widgets. The :class:`ListBox` will try to scroll all the rows of
a selectable widget into view so that the user can see the new focus widget in
its entirety. This behavior can be used to bring more than a single widget into
view by using composite widgets to combine a selectable widget with other
widgets that should be displayed at the same time.


Dynamic ListBox with ListWalker
-------------------------------

While the :class:`ListBox` stores the location of its focus
widget, it does not directly store the actual focus widget or other contents of
the :class:`ListBox`. The storage of a
:class:`ListBox`'s content is delegated to a "List Walker"
object. If a list of widgets is passed to the :class:`ListBox`
constructor then it creates a :class:`SimpleListWalker` object
to manage the list.

When the :class:`ListBox` is `rendering a canvas`_ or `handling
input`_ it will:

.. _rendering a canvas: :meth:`ListBox.render`
.. _handling input: :meth:`ListBox.keypress`

1. Call the :meth:`get_focus` method of its list walker object. This method
   will return the focus widget and a position object.
2. Optionally call the :meth:`get_prev` method of its List Walker object one or
   more times, initially passing the focus position and then passing the new
   position returned on each successive call. This method will return the
   widget and position object "above" the position passed.
3. Optionally call the :meth:`get_next` method of its List Walker object one or
   more times, similarly, to collect widgets and position objects "below" the
   focus position.
4. Optionally call the :meth:`set_focus` method passing one of the position
   objects returned in the previous steps.

This is the only way the :class:`ListBox` accesses its contents,
and it will not store copies of any of the widgets or position objects beyond
the current rendering or input handling operation.

The :class:`SimpleListWalker` stores a list of widgets, and uses
integer indexes into this list as its position objects. It stores the focus
position as an integer, so if you insert a widget into the list above the focus
position then you need to remember to increment the focus position in the
:class:`SimpleListWalker` object or the contents of the
:class:`ListBox` will shift.

A custom List Walker object may be passed to the
:class:`ListBox` constructor instead of a plain list of widgets.
List Walker objects must implement the :ref:`list-walker-interface`.

The fib.py_ example program demonstrates a custom list walker that doesn't
store any widgets. It uses a tuple of two successive Fibonacci numbers as its
position objects and it generates Text widgets to display the numbers on the
fly. The result is a :class:`ListBox` that can scroll through an
unending list of widgets.

The edit.py_ example program demonstrates a custom list walker that loads lines
from a text file only as the user scrolls them into view. This allows even
huge files to be opened almost instantly.

The browse.py_ example program demonstrates a custom list walker that uses a
tuple of strings as position objects, one for the parent directory and one for
the file selected. The widgets are cached in a separate class that is accessed
using a dictionary indexed by parent directory names. This allows the
directories to be read only as required. The custom list walker also allows
directories to be hidden from view when they are "collapsed".

.. _fib.py: https://github.com/urwid/urwid/blob/master/examples/fib.py
.. _edit.py: https://github.com/urwid/urwid/blob/master/examples/edit.py
.. _browse.py: https://github.com/urwid/urwid/blob/master/examples/browse.py


Setting the Focus
-----------------

The easiest way to change the current :class:`ListBox` focus is
to call the :meth:`ListBox.set_focus` method. This method doesn't
require that you know the :class:`ListBox`'s current dimensions
``(maxcol, maxrow)``. It will wait until the next call to either keypress or
render to complete setting the offset and inset values using the dimensions
passed to that method.

The position object passed to :meth:`set_focus` must be compatible with the
List Walker object that the :class:`ListBox` is using. For
:class:`SimpleListWalker` the position is the integer index of
the widget within the list.

The *coming_from* parameter should be set if you know that the old position is
"above" or "below" the previous position. When the
:class:`ListBox` completes setting the offset and inset values
it tries to find the old widget among the visible widgets. If the old widget is
still visible, if will try to avoid causing the :class:`ListBox`
contents to scroll up or down from its previous position. If the widget is not
visible, then the :class:`ListBox` will:

* Display the new focus at the bottom of the :class:`ListBox` if
  *coming_from* is "above".
* Display the new focus at the top of the :class:`ListBox` if
  *coming_from* is "below".
* Display the new focus in the middle of the :class:`ListBox` if
  coming_from is ``None``.

If you know exactly where you want to display the new focus widget within the
:class:`ListBox` you may call
:meth:`ListBox.set_focus_valign`.  This method lets you specify
the *top*, *bottom*, *middle*, a relative position or the exact number of rows
from the top or bottom of the :class:`ListBox`.

List Walkers
------------

:class:`ListBox` does not manage the widgets it displays
directly, instead it passes that task to a class called a "list walker". List
walkers keep track of the widget in focus and provide an opaque position object
that the :class:`ListBox` may use to iterate through widgets
above and below the focus widget.

A :class:`SimpleFocusListWalker`
is a list walker that behaves like a normal Python list. It may be used any
time you will be displaying a moderate number of widgets.

If you need to display a large number of widgets you should implement your own
list walker that manages creating widgets as they are requested and destroying
them later to avoid excessive memory use.

List walkers may also be used to display tree or other structures within a
:class:`ListBox`. A number of the `example programs
`_ demonstrate the use of custom list
walker classes.

.. seealso:: :class:`ListWalker base class reference `


.. _list-walker-interface:

List Walker Interface
---------------------

List Walker API Version 1
~~~~~~~~~~~~~~~~~~~~~~~~~

This API will remain available and is still the least restrictive option fo the programmer.
Your class should subclass :class:`ListWalker`.
Whenever the focus or content changes you are responsible for calling :meth:`ListWalker._modified`.

.. currentmodule:: MyV1ListWalker

.. method:: get_focus()

   return a ``(widget, position)`` tuple or ``(None, None)`` if empty

.. method:: set_focus(position)

   set the focus and call ``self._modified()`` or raise an :exc:`IndexError`.

.. method:: get_next(position)

   return the ``(widget, position)`` tuple below *position* passed
   or ``(None, None)`` if there is none.

.. method:: get_prev(position)

   return the ``(widget, position)`` tuple above *position* passed
   or ``(None, None)`` if there is none.

List Walker API Version 2
~~~~~~~~~~~~~~~~~~~~~~~~~

This API is an attempt to remove some of the duplicate code that V1 requires for
many users.  List walker API V1 will be implemented automatically by
subclassing :class:`ListWalker` and implementing the V2 methods.
Whenever the focus or content changes you are responsible for
calling :meth:`ListWalker._modified`.

.. currentmodule:: MyV2ListWalker

.. method:: __getitem__(position)

   return widget at *position* or raise an :exc:`IndexError` or :exc:`KeyError`

.. method:: next_position(position)

   return the position below passed *position* or raise an :exc:`IndexError` or :exc:`KeyError`

.. method:: prev_position(position)

   return the position above passed *position* or raise an :exc:`IndexError` or :exc:`KeyError`

.. method:: set_focus(position)

   set the focus and call ``self._modified()`` or raise an :exc:`IndexError`.

.. attribute:: focus

   attribute or property containing the focus position, or define
   :meth:`MyV1ListWalker.get_focus` as above

List Walker Iteration
~~~~~~~~~~~~~~~~~~~~~

There is an optional iteration helper method that may be defined in any list walker.
When this is defined it will be used by :meth:`ListBox.__iter__` and
:meth:`ListBox.__reversed__`:

.. method:: positions(reverse=False)

   return a forward or reverse iterable of positions

.. currentmodule:: urwid

Custom Widgets
==============

Widgets in Urwid are easiest to create by extending other widgets. If you are
making a new type of widget that can use other widgets to display its content,
like a new type of button or control, then you should start by extending
:class:`WidgetWrap` and passing the display widget to its constructor.

The :class:`Widget` interface is described in detail in the
:class:`Widget base class reference ` and is useful if you're looking to modify
the behavior of an existing widget,
build a new widget class from scratch or just want a better understanding of
the library.

One Urwid design choice that stands out is that widgets typically have no
size. Widgets don't store their size on screen, and instead are
passed that information when they need it.

This choice has some advantages:

* widgets may be reused in different locations
* reused widgets only need to be rendered once per size displayed
* widgets don't need to know their parents
* less data to store and update
* no worrying about widgets that haven't received their size yet
* same widgets could be displayed at different sizes to different users
  simultaneously

It also has disadvantages:

* difficult to determine a widget's size on screen
* more parameters to parse
* duplicated size calculations across methods

For determining a widget's size on screen it is possible to look up the size(s)
it was rendered at in the :class:`CanvasCache`. There are plans
to address some of the duplicated size handling code in the container widgets
in a future Urwid release.

The same holds true for a widget's focus state, so that too is passed in to
functions that need it.


Modifying Existing Widgets
--------------------------

The easiest way to create a custom widget is to modify an existing widget.
This can be done by either subclassing the original widget or by wrapping it.
Subclassing is appropriate when you need to interact at a very low level with
the original widget, such as if you are creating a custom edit widget with
different behavior than the usual Edit widgets. If you are creating a custom
widget that doesn't need tight coupling with the original widget then
wrapping is more appropriate.

The :class:`WidgetWrap` class simplifies wrapping existing
widgets. You can create a custom widget simply by creating a subclass of
WidgetWrap and passing a widget into WidgetWrap's constructor.


This is an example of a custom widget that uses WidgetWrap:

.. literalinclude:: wmod.py
   :linenos:

The above code creates a group of RadioButtons and provides a method to
query the state of the buttons.


Widgets from Scratch
--------------------

Widgets must inherit from :class:`Widget`.
Box widgets must implement :meth:`Widget.selectable` and :meth:`Widget.render`
methods, and flow widgets must implement :meth:`Widget.selectable`,
:meth:`Widget.render` and :meth:`Widget.rows` methods.

The default :meth:`Widget.sizing` method returns a set of sizing modes supported
from ``self._sizing``, so we define ``_sizing`` attributes for our flow and
box widgets below.

.. literalinclude:: wanat.py
   :linenos:

The above code implements two widget classes. Pudding is a flow widget and
BoxPudding is a box widget. Pudding will render as much "Pudding" as will fit
in a single row, and BoxPudding will render as much "Pudding" as will fit into
the entire area given.

Note that the rows and render methods' focus parameter must have a default
value of False. Also note that for flow widgets the number of rows returned by
the rows method must match the number of rows rendered by the render method.

To improve the efficiency of your Urwid application you should be careful of
how long your ``rows()`` methods take to execute. The ``rows()`` methods may be called many
times as part of input handling and rendering operations. If you are using a
display widget that is time consuming to create you should consider caching it
to reduce its impact on performance.

It is possible to create a widget that will behave as either a flow widget or
box widget depending on what is required:

.. literalinclude:: wanat_multi.py
   :linenos:

MultiPudding will work in place of either Pudding or BoxPudding above. The
number of elements in the size tuple determines whether the containing widget
is expecting a flow widget or a box widget.


Selectable Widgets
------------------

Selectable widgets such as Edit and Button widgets allow the user to interact
with the application. A widget is selectable if its selectable method returns
True. Selectable widgets must implement the :meth:`Widget.keypress` method to
handle keyboard input.

.. literalinclude:: wsel.py

The SelectablePudding widget will display its contents in uppercase when it is
in focus, and it allows the user to "eat" the pudding by pressing each of the
letters *P*, *U*, *D*, *D*, *I*, *N* and *G* on the keyboard. When the user has
"eaten" all the pudding the widget will reset to its initial state.

Note that keys that are unhandled in the keypress method are returned so that
another widget may be able to handle them. This is a good convention to follow
unless you have a very good reason not to. In this case the *UP* and *DOWN*
keys are returned so that if this widget is in a
:class:`ListBox` the :class:`ListBox` will behave
as the user expects and change the focus or scroll the
:class:`ListBox`.


Widget Displaying the Cursor
----------------------------

Widgets that display the cursor must implement the
:meth:`Widget.get_cursor_coords` method.
Similar to the rows method for flow widgets, this method lets other widgets
make layout decisions without rendering the entire widget. The
:class:`ListBox` widget in particular uses get_cursor_coords to
make sure that the cursor is visible within its focus widget.

.. literalinclude:: wcur1.py
   :linenos:

CursorPudding will let the user move the cursor through the widget by pressing
*LEFT* and *RIGHT*. The cursor must only be added to the canvas when the widget
is in focus. The get_cursor_coords method must always return the same cursor
coordinates that render does.

A widget displaying a cursor may choose to implement :meth:`Widget.get_pref_col`.
This method
returns the preferred column for the cursor, and is called when the focus is
moving up or down off this widget.

Another optional method is :meth:`Widget.move_cursor_to_coords`. This method allows other
widgets to try to position the cursor within this widget. The
:class:`ListBox` widget uses :meth:`Widget.move_cursor_to_coords` when
changing focus and when the user pressed *PAGE UP* or *PAGE DOWN*. This method
must return ``True`` on success and ``False`` on failure. If the cursor may be
placed at any position within the row specified (not only at the exact column
specified) then this method must move the cursor to that position and return
``True``.

.. literalinclude:: wcur2.py
   :linenos:
   :lines: 7-


Widget Metaclass
================

The :class:`Widget` base class has a metaclass defined that
creates a ``__super`` attribute for calling your superclass:
``self.__super`` is the same as the usual ``super(MyClassName, self)``.
This shortcut is of little use with Python 3's new ``super()`` syntax, but
will likely be retained for backwards compatibility in future versions.

This metaclass also uses :class:`MetaSignal`
to allow signals to be defined as a list of signal names
in a ``signals`` class attribute.  This is equivalent to calling
:func:`register_signal` with the class name and list of signals and all those
defined in superclasses after the class definition.

.. seealso::

    :class:`Widget metaclass WidgetMeta `


urwid-urwid-69907e4/docs/manual/wmod.py000066400000000000000000000011011526073734300200150ustar00rootroot00000000000000from __future__ import annotations

import urwid


class QuestionnaireItem(urwid.WidgetWrap[urwid.GridFlow]):
    def __init__(self) -> None:
        self.options: list[urwid.RadioButton] = []
        unsure = urwid.RadioButton(self.options, "Unsure")
        yes = urwid.RadioButton(self.options, "Yes")
        no = urwid.RadioButton(self.options, "No")
        display_widget = urwid.GridFlow([unsure, yes, no], 15, 3, 1, "left")
        super().__init__(display_widget)

    def get_state(self) -> str:
        return next(o.label for o in self.options if o.state is True)
urwid-urwid-69907e4/docs/manual/wsel.py000066400000000000000000000021431526073734300200300ustar00rootroot00000000000000from __future__ import annotations

import urwid


class SelectablePudding(urwid.Widget):
    _sizing = frozenset((urwid.FLOW,))
    _selectable = True

    def __init__(self) -> None:
        super().__init__()
        self.pudding = "pudding"

    def rows(self, size: tuple[int], focus: bool = False) -> int:
        return 1

    def render(self, size: tuple[int], focus: bool = False) -> urwid.TextCanvas:
        (maxcol,) = size
        num_pudding = maxcol // len(self.pudding)
        pudding = self.pudding
        if focus:
            pudding = pudding.upper()
        return urwid.TextCanvas([pudding.encode("utf-8") * num_pudding], maxcol=maxcol)

    def keypress(self, size: tuple[int], key: str) -> str | None:
        if len(key) > 1:
            return key

        if key.lower() in self.pudding:
            # remove letter from pudding
            n = self.pudding.index(key.lower())
            self.pudding = self.pudding[:n] + self.pudding[n + 1 :]
            if not self.pudding:
                self.pudding = "pudding"
            self._invalidate()
            return None

        return key
urwid-urwid-69907e4/docs/reference/000077500000000000000000000000001526073734300171655ustar00rootroot00000000000000urwid-urwid-69907e4/docs/reference/attrspec.rst000066400000000000000000000001411526073734300215400ustar00rootroot00000000000000Raw Display Attributes
======================

.. currentmodule:: urwid

.. autoclass:: AttrSpec
urwid-urwid-69907e4/docs/reference/canvas.rst000066400000000000000000000007051526073734300211740ustar00rootroot00000000000000Canvas Classes and Functions
============================

.. currentmodule:: urwid

Canvas Classes
--------------

.. autoclass:: Canvas

.. autoclass:: TextCanvas

.. autoclass:: BlankCanvas

.. autoclass:: SolidCanvas

.. autoclass:: CompositeCanvas

CompositeCanvas Builders
------------------------

.. autofunction:: CanvasCombine

.. autofunction:: CanvasJoin

.. autofunction:: CanvasOverlay

CanvasCache
-----------

.. autoclass:: CanvasCache
urwid-urwid-69907e4/docs/reference/command_map.rst000066400000000000000000000001151526073734300221670ustar00rootroot00000000000000Command Map
-----------

.. currentmodule:: urwid

.. autoclass:: CommandMap
urwid-urwid-69907e4/docs/reference/constants.rst000066400000000000000000000164061526073734300217420ustar00rootroot00000000000000Constants
=========

.. currentmodule:: urwid

.. note::

   These constants may be used, but using the string values themselves in
   your program is equally supported.  These constants are used internally
   by urwid just to avoid possible misspelling, but the example programs
   and tutorials tend to use the string values.

Widget Sizing Methods
---------------------

One or more of these values returned by :meth:`Widget.sizing` to indicate
supported sizing methods.

.. data:: FLOW
   :annotation: = 'flow'

   Widget that is given a number of columns by its parent widget and
   calculates the number of rows it requires for rendering
   e.g. :class:`Text`

.. data:: BOX
   :annotation: = 'box'

   Widget that is given a number of columns and rows by its parent
   widget and must render that size
   e.g. :class:`ListBox`

.. data:: FIXED
   :annotation: = 'fixed'

   Widget that knows the number of columns and rows it requires and will
   only render at that exact size
   e.g. :class:`BigText`


Horizontal Alignment
--------------------

Used to horizontally align text in :class:`Text` widgets and child widgets
of :class:`Padding` and :class:`Overlay`.

.. data:: LEFT
   :annotation: = 'left'

.. data:: CENTER
   :annotation: = 'center'

.. data:: RIGHT
   :annotation: = 'right'

Veritcal Alignment
------------------

Used to vertically align child widgets of :class:`Filler` and
:class:`Overlay`.

.. data:: TOP
   :annotation: = 'top'

.. data:: MIDDLE
   :annotation: = 'middle'

.. data:: BOTTOM
   :annotation: = 'bottom'

Width and Height Settings
-------------------------

Used to distribute or set widths and heights of child widgets of
:class:`Padding`, :class:`Filler`, :class:`Columns`,
:class:`Pile` and :class:`Overlay`.

.. data:: PACK
   :annotation: = 'pack'

   Ask the child widget to calculate the number of columns or rows it needs

.. data:: GIVEN
   :annotation: = 'given'

   A set number of columns or rows, e.g. ('given', 10) will have exactly
   10 columns or rows given to the child widget

.. data:: RELATIVE
   :annotation: = 'relative'

   A percentage of the total space, e.g. ('relative', 50) will give half
   of the total columns or rows to the child widget

.. data:: RELATIVE_100
   :annotation: = ('relative', 100)

.. data:: WEIGHT
   :annotation: = 'weight'

   A weight value for distributing columns or rows, e.g. ('weight', 3)
   will give 3 times as many columns or rows as another widget in the same
   container with ('weight', 1).


Text Wrapping Modes
-------------------

.. data:: SPACE
   :annotation: = 'space'

   wrap text on space characters or at the boundaries of wide characters

.. data:: ANY
   :annotation: = 'any'

   wrap before any wide or narrow character that would exceed the available
   screen columns

.. data:: CLIP
   :annotation: = 'clip'

   clip before any wide or narrow character that would exceed the available
   screen columns and don't display the remaining text on the line

.. data:: ELLIPSIS
   :annotation: = 'ellipsis'

   clip if text would exceed the available screen columns, add an ellipsis
   character at the end


Foreground and Background Colors
--------------------------------

Standard background and foreground colors
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. data:: BLACK
   :annotation: = 'black'

.. data:: DARK_RED
   :annotation: = 'dark red'

.. data:: DARK_GREEN
   :annotation: = 'dark green'

.. data:: BROWN
   :annotation: = 'brown'

.. data:: DARK_BLUE
   :annotation: = 'dark blue'

.. data:: DARK_MAGENTA
   :annotation: = 'dark magenta'

.. data:: DARK_CYAN
   :annotation: = 'dark cyan'

.. data:: LIGHT_GRAY
   :annotation: = 'light gray'

Standard foreground colors (not safe to use as background)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. data:: DARK_GRAY
   :annotation: = 'dark gray'

.. data:: LIGHT_RED
   :annotation: = 'light red'

.. data:: LIGHT_GREEN
   :annotation: = 'light green'

.. data:: YELLOW
   :annotation: = 'yellow'

.. data:: LIGHT_BLUE
   :annotation: = 'light blue'

.. data:: LIGHT_MAGENTA
   :annotation: = 'light magenta'

.. data:: LIGHT_CYAN
   :annotation: = 'light cyan'

.. data:: WHITE
   :annotation: = 'white'

User's terminal configuration default foreground or background
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. note::

   There is no way to tell if the user's terminal has a light
   or dark color as their default foreground or background, so
   it is highly recommended to use this setting for both foreground
   and background when you do use it.

.. data:: DEFAULT
   :annotation: = 'default'


256 and 88 Color Foregrounds and Backgrounds
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Constants are not defined for these colors.

.. seealso::

   :ref:`high-colors`
 


Signal Names
------------

.. data:: UPDATE_PALETTE_ENTRY
   :annotation: = 'update palette entry'

   sent by :class:`BaseScreen` (and subclasses like 
   :class:`raw_display.Screen`) when a palette entry is changed.
   :class:`MainLoop` handles this signal by redrawing the whole
   screen.

.. data:: INPUT_DESCRIPTORS_CHANGED
   :annotation: = 'input descriptors changed'

   sent by :class:`BaseScreen` (and subclasses like 
   :class:`raw_display.Screen`) when the list of input file descriptors
   has changed.  :class:`MainLoop` handles this signal by updating
   the file descriptors being watched by its event loop.


Command Names
-------------

Command names are used as values in :class:`CommandMap` instances.
Widgets look up the command associated with keypresses in their
:meth:`Widget.keypress` methods.

You may define any new command names as you wish and look for them in
your own widget code.  These are the standard ones expected by code
included in Urwid.

.. data:: REDRAW_SCREEN
   :annotation: = 'redraw screen'

   Default associated keypress: 'ctrl l'

   :meth:`MainLoop.process_input` looks for this command to force
   a screen refresh. This is useful in case the screen becomes
   corrupted.

.. data:: ACTIVATE
   :annotation: = 'activate'

   Default associated keypresses: ' ' (space), 'enter'

   Activate a widget such as a :class:`Button`, :class:`CheckBox`
   or :class:`RadioButton`.

.. data:: CURSOR_UP
   :annotation: = 'cursor up'

   Default associated keypress: 'up'

   Move the cursor or selection up one row.

.. data:: CURSOR_DOWN
   :annotation: = 'cursor down'

   Default associated keypress: 'down'

   Move the cursor or selection down one row.

.. data:: CURSOR_LEFT
   :annotation: = 'cursor left'

   Default associated keypress: 'left'

   Move the cursor or selection left one column.

.. data:: CURSOR_RIGHT
   :annotation: = 'cursor right'

   Default associated keypress: 'right'

   Move the cursor or selection right one column.

.. data:: CURSOR_PAGE_UP
   :annotation: = 'cursor page up'

   Default associated keypress: 'page up'

   Move the cursor or selection up one page.

.. data:: CURSOR_PAGE_DOWN
   :annotation: = 'cursor page down'

   Default associated keypress: 'page down'

   Move the cursor or selection down one page.

.. data:: CURSOR_MAX_LEFT
   :annotation: = 'cursor max left'

   Default associated keypress: 'home'

   Move the cursor or selection to the leftmost column.

.. data:: CURSOR_MAX_RIGHT
   :annotation: = 'cursor max right'

   Default associated keypress: 'end'

   Move the cursor or selection to the rightmost column.

urwid-urwid-69907e4/docs/reference/deprecated.rst000066400000000000000000000001631526073734300220170ustar00rootroot00000000000000Deprecated Classes
------------------

.. currentmodule:: urwid

.. autoclass:: AttrWrap

.. autoclass:: MetaSuper
urwid-urwid-69907e4/docs/reference/display_modules.rst000066400000000000000000000012451526073734300231160ustar00rootroot00000000000000Display Modules
===============

.. currentmodule:: urwid

.. autoclass:: BaseScreen

RAW display
-----------

.. module:: urwid.display.raw

.. autoclass:: Screen

Curses display
--------------

.. module:: urwid.display.curses

.. autoclass:: Screen

WEB display
-----------

.. module:: urwid.display.web

.. autoclass:: Screen

HTML fragment
-------------

.. module:: urwid.display.html_fragment

.. autoclass:: HtmlGenerator

.. autofunction:: screenshot_init

.. autofunction:: screenshot_collect

LCD display
-----------

.. module:: urwid.display.lcd

.. autoclass:: LCDScreen

.. autoclass:: CFLCDScreen

.. autoclass:: CF635Screen

.. autoclass:: KeyRepeatSimulator
urwid-urwid-69907e4/docs/reference/exceptions.rst000066400000000000000000000006241526073734300221020ustar00rootroot00000000000000Exceptions
==========

.. currentmodule:: urwid

.. autoexception:: ExitMainLoop

.. autoexception:: WidgetError

.. autoexception:: ListBoxError

.. autoexception:: ColumnsError

.. autoexception:: PileError

.. autoexception:: GridFlowError

.. autoexception:: BoxAdapterError

.. autoexception:: OverlayError

.. autoexception:: TextError

.. autoexception:: EditError

.. autoexception:: CanvasError
urwid-urwid-69907e4/docs/reference/global_settings.rst000066400000000000000000000002441526073734300230770ustar00rootroot00000000000000Global Settings
===============

.. currentmodule:: urwid

.. autofunction:: set_encoding

.. autofunction:: get_encoding_mode

.. autofunction:: supports_unicode

urwid-urwid-69907e4/docs/reference/index.rst000066400000000000000000000004221526073734300210240ustar00rootroot00000000000000.. _urwid-reference:

###############
Urwid Reference
###############

.. toctree::

   main_loop
   widget
   display_modules
   list_walkers
   signals
   global_settings
   attrspec
   canvas
   text_layout
   command_map
   constants
   exceptions
   meta
   deprecated
urwid-urwid-69907e4/docs/reference/list_walkers.rst000066400000000000000000000007361526073734300224300ustar00rootroot00000000000000List Walker Classes
===================

.. currentmodule:: urwid

ListWalker
----------

.. autoclass:: ListWalker

List-like List Walkers
----------------------

.. autoclass:: SimpleFocusListWalker

.. autoclass:: SimpleListWalker

Deque-like List Walkers
-----------------------

.. autoclass:: SimpleFocusDequeWalker

.. autoclass:: SimpleDequeWalker

TreeWalker and Nodes
--------------------

.. autoclass:: TreeWalker

.. autoclass:: TreeNode

.. autoclass:: ParentNode
urwid-urwid-69907e4/docs/reference/main_loop.rst000066400000000000000000000010661526073734300216770ustar00rootroot00000000000000MainLoop and Event Loops
========================

.. currentmodule:: urwid

MainLoop
--------

.. autoclass:: MainLoop

SelectEventLoop
---------------

.. autoclass:: SelectEventLoop

AsyncioEventLoop
----------------

.. autoclass:: AsyncioEventLoop

TrioEventLoop
----------------

.. autoclass:: TrioEventLoop

GLibEventLoop
-------------

.. autoclass:: GLibEventLoop

TwistedEventLoop
----------------

.. autoclass:: TwistedEventLoop

TornadoEventLoop
----------------

.. autoclass:: TornadoEventLoop

ZMQEventLoop
------------

.. autoclass:: ZMQEventLoop
urwid-urwid-69907e4/docs/reference/meta.rst000066400000000000000000000002031526073734300206400ustar00rootroot00000000000000Metaclasses
===========

.. currentmodule:: urwid

.. autoclass:: WidgetMeta

.. autoclass:: MetaSuper

.. autoclass:: MetaSignals
urwid-urwid-69907e4/docs/reference/signals.rst000066400000000000000000000014011526073734300213530ustar00rootroot00000000000000Signal Functions
================

.. currentmodule:: urwid

The :func:`urwid.\*_signal` functions use a shared Signals object instance
for tracking registered and connected signals.  There is no reason to
instantiate your own Signals object.

.. function:: connect_signal(obj, name, callback, user_arg=None, weak_args=None, user_args=None)

.. automethod:: Signals.connect

.. function:: disconnect_signal_by_key(obj, name, key)

.. automethod:: Signals.disconnect_by_key

.. function:: disconnect_signal(obj, name, callback, user_arg=None, weak_args=None, user_args=None)

.. automethod:: Signals.disconnect

.. function:: register_signal(sig_cls, signals)

.. automethod:: Signals.register

.. function:: emit_signal(obj, name, \*args)

.. automethod:: Signals.emit
urwid-urwid-69907e4/docs/reference/text_layout.rst000066400000000000000000000002001526073734300222700ustar00rootroot00000000000000Text Layout Classes
===================

.. currentmodule:: urwid

.. autoclass:: TextLayout

.. autoclass:: StandardTextLayout
urwid-urwid-69907e4/docs/reference/widget.rst000066400000000000000000000047551526073734300212150ustar00rootroot00000000000000.. _widget-classes:

Widget Classes
==============

.. currentmodule:: urwid

Widget Base Classes
-------------------

Widget
~~~~~~

.. autoclass:: Widget
   :private-members: _invalidate, _emit

WidgetWrap
~~~~~~~~~~

.. autoclass:: WidgetWrap

WidgetDecoration
~~~~~~~~~~~~~~~~

.. autoclass:: WidgetDecoration

WidgetContainerMixin
~~~~~~~~~~~~~~~~~~~~

.. autoclass:: WidgetContainerMixin


Basic Widget Classes
--------------------

Text
~~~~

.. autoclass:: Text

Edit
~~~~

.. autoclass:: Edit

IntEdit
~~~~~~~

.. autoclass:: IntEdit

Button
~~~~~~

.. autoclass:: Button

CheckBox
~~~~~~~~

.. autoclass:: CheckBox

RadioButton
~~~~~~~~~~~

.. autoclass:: RadioButton

TreeWidget
~~~~~~~~~~

.. autoclass:: TreeWidget


.. currentmodule:: urwid.numedit

Extended Numerical Editing Classes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

IntegerEdit
~~~~~~~~~~~

.. autoclass:: IntegerEdit

FloatEdit
~~~~~~~~~~~

.. autoclass:: FloatEdit


Special cases text widgets
--------------------------

.. currentmodule:: urwid

SelectableIcon
~~~~~~~~~~~~~~

.. autoclass:: SelectableIcon

ANSIText
~~~~~~~~

.. autoclass:: ANSIText


Decoration Widget Classes
-------------------------

AttrMap
~~~~~~~

.. autoclass:: AttrMap

Padding
~~~~~~~

.. autoclass:: Padding

Filler
~~~~~~

.. autoclass:: Filler

Divider
~~~~~~~

.. autoclass:: Divider

LineBox
~~~~~~~

.. autoclass:: LineBox

SolidFill
~~~~~~~~~

.. autoclass:: SolidFill

PopUpLauncher
~~~~~~~~~~~~~

.. autoclass:: PopUpLauncher

PopUpTarget
~~~~~~~~~~~

.. autoclass:: PopUpTarget

WidgetPlaceholder
~~~~~~~~~~~~~~~~~

.. autoclass:: WidgetPlaceholder

WidgetDisable
~~~~~~~~~~~~~

.. autoclass:: WidgetDisable

Container Widget Classes
------------------------

Frame
~~~~~

.. autoclass:: Frame

ListBox
~~~~~~~

.. autoclass:: ListBox

TreeListBox
~~~~~~~~~~~

.. autoclass:: TreeListBox

Columns
~~~~~~~

.. autoclass:: Columns

Pile
~~~~

.. autoclass:: Pile

GridFlow
~~~~~~~~

.. autoclass:: GridFlow

BoxAdapter
~~~~~~~~~~

.. autoclass:: BoxAdapter

Overlay
~~~~~~~

.. autoclass:: Overlay

Graphic Widget Classes
----------------------

BarGraph
~~~~~~~~

.. autoclass:: BarGraph

GraphVScale
~~~~~~~~~~~

.. autoclass:: GraphVScale

ProgressBar
~~~~~~~~~~~

.. autoclass:: ProgressBar

BigText
~~~~~~~

.. autoclass:: BigText

.. autofunction:: get_all_fonts

Terminal
~~~~~~~~

.. autoclass:: Terminal


Scroll Widget Classes
---------------------

Scrollable
~~~~~~~~~~

.. autoclass:: Scrollable

ScrollBar
~~~~~~~~~

.. autoclass:: ScrollBar

.. currentmodule:: urwid
urwid-urwid-69907e4/docs/tools/000077500000000000000000000000001526073734300163675ustar00rootroot00000000000000urwid-urwid-69907e4/docs/tools/compile_pngs.sh000077500000000000000000000011401526073734300214010ustar00rootroot00000000000000#!/bin/bash -e

# args: scripts to capture

DISPLAYNUM=5
SCREENSHOTS="$(dirname "$0")"/screenshots.sh

XVFB=$(which Xvfb)
if [ -n "$XVFB" ]; then
	# 1600x1200 to leave headroom above the largest example window
	# (79x34 characters) rendered at the screenshot tool's 2x pixel density.
	Xvfb :$DISPLAYNUM -screen 0 1600x1200x24 &
	XVFBPID=$!
	trap 'kill "$XVFBPID"' EXIT
	export DISPLAY=:$DISPLAYNUM
	until [ -S "/tmp/.X11-unix/X${DISPLAYNUM}" ]; do sleep 0.1; done
fi

for script in "$@"; do
	echo "doing $script"
	if [ -f "${script}.xdotool" ]; then
		"$SCREENSHOTS" "$script" < "${script}.xdotool"
	fi
done
urwid-urwid-69907e4/docs/tools/screenshots.sh000077500000000000000000000051321526073734300212670ustar00rootroot00000000000000#!/bin/bash

# $1: python script to run
# xterm, xdotool and import are required to run this script

CLASSNAME=$(head -c 6 /dev/urandom | base64 | tr -cd '[:alnum:]')
PYTHON=${PYTHON:-python}

# 2x size for HiDPI; docs' ``:scale: 50%`` shows them at logical size.
#
# Font            | Rejected because
# ----------------|--------------------------------------------------
# DejaVu 1x       | uneven/"dancing" glyphs (e.g. "8"); fixed by 2x above
# Terminus 2x     | breaks Unicode fallback (tofu for Georgian/math/CJK)
# Cascadia Mono   | shade blocks (U+2591-2593) render as a diagonal weave
# Liberation Mono | rounded box corners (U+256D-2570) don't meet the lines
# DejaVu 2x       | chosen; only con is 1/l/I and 0/O are hard to tell apart
#
# Terminal | Rejected because
# ---------|----------------------------------------------------
# urxvt    | no color-glyph support -> emoji always render as tofu
# xterm    | chosen; needs allowSendEvents below or it silently
#          | discards the synthetic keys xdotool sends us
#
# -tn: xterm's own TERM default is plain "xterm", not 256-color.
xterm -bg gray90 -fg black -b 0 +sb -tn xterm-256color \
	-fa 'DejaVu Sans Mono:pixelsize=26:antialias=false' \
	-xrm 'XTerm*allowSendEvents: true' \
	-name "$CLASSNAME" -e "$PYTHON" "$1" &
TERMPID=$!
trap 'kill "$TERMPID" 2>/dev/null' EXIT

TERMWINDOWID=$(xdotool search --sync --classname "$CLASSNAME")
export TERMWINDOWID
image=${1%.py}

# More reliable than a fixed sleep: some examples (e.g. the directory
# browser) take a variable amount of time to settle.
wait_example_settled() {
	local -i idle_polls=0
	local -i poll=0
	while (( poll < 200 )); do
		local -i running=0
		local pid
		for pid in "$TERMPID" $(pgrep -P "$TERMPID") $(pgrep -g "$TERMPID" 2>/dev/null); do
			[ -r "/proc/$pid/stat" ] || continue
			read -r _ _ state _ < "/proc/$pid/stat"
			[ "$state" = "R" ] && running=1
		done
		if [ "$running" -eq 1 ]; then
			idle_polls=0
		else
			(( ++idle_polls ))
			(( idle_polls >= 3 )) && return
		fi
		sleep 0.05
		(( ++poll ))
	done
}

# let xterm finish its startup redraw before the first keystroke
wait_example_settled

counter=1
while read -r line; do
	# the echo trick is needed to expand TERMWINDOWID variable
	echo $line | xdotool -
	echo "sending $line"
	wait_example_settled
	import -window "$TERMWINDOWID" "${image}$counter.png"
	# 192 DPI (2x baseline); exclude-chunk drops mogrify's own tIME/date
	# stamps, which would otherwise make reruns produce different bytes.
	mogrify -units PixelsPerInch -density 192 -define png:exclude-chunk=date,time "${image}$counter.png"
	(( counter++ ))
done

kill "$TERMPID" 2>/dev/null
urwid-urwid-69907e4/docs/tools/static/000077500000000000000000000000001526073734300176565ustar00rootroot00000000000000urwid-urwid-69907e4/docs/tools/static/.placeholder000066400000000000000000000000001526073734300221270ustar00rootroot00000000000000urwid-urwid-69907e4/docs/tools/static/urwid_theme.css000066400000000000000000000050661526073734300227130ustar00rootroot00000000000000/*
 * Fixes for the "classic" Sphinx theme as customized via html_theme_options
 * in conf.py. Loaded after the theme's own classic.css, so rules here win.
 */

/* --- Fix the sidebar/table-of-contents overlapping the page text --------
 *
 * classic.css positions div.sphinxsidebar with `float: left` and
 * `margin-left: -100%`, relying on it sitting inside a plain block-level
 * div.document so the negative margin pulls it back over to the left of
 * div.documentwrapper. Sphinx's classic theme now sets
 * `div.document { display: flex; }` (for the sidebar background color to
 * reach the full height of the page), which turns the sidebar and the
 * document wrapper into flex items instead. Floats no longer apply and the
 * negative margin just shifts the sidebar left by its own width, so it
 * overlaps the body text. Making both explicit flex items with a fixed
 * order restores the intended side-by-side layout.
 */
div.documentwrapper {
    float: none;
    width: auto;
    min-width: 0;
    flex: 1 1 auto;
    order: 2;
}

div.sphinxsidebar {
    float: none;
    width: 230px;
    margin-left: 0;
    flex: 0 0 230px;
    order: 1;
}

div.bodywrapper {
    margin-left: 0;
}

/* --- Let the body text use the available width on wide screens ---------
 *
 * The basic theme caps div.body at 800px so a line of text isn't
 * uncomfortably long. Combined with our sidebar being pinned to the left,
 * that just leaves a large blank gutter on wide windows instead of using
 * the space, and it only gets worse as the window gets wider. Drop the
 * cap entirely so the text area always fills the room next to the
 * sidebar.
 */
div.body {
    max-width: none;
}

/* --- Highlight ".. deprecated::" notices --------------------------------
 *
 * classic.css styles div.note, div.warning and div.seealso but never
 * added a rule for div.deprecated (the block Sphinx generates for
 * ".. deprecated::"), so deprecation notices render as plain paragraphs
 * and are easy to miss.
 */
div.deprecated {
    background-color: #ffe4e4;
    border: 1px solid #f66;
    padding: 6px 10px;
    margin-bottom: 10px;
}

div.deprecated p {
    margin-bottom: 5px;
}

/* --- Fill the viewport even when a page's content is short --------------
 *
 * div.document only grows to the height of its content, so on short pages
 * the sidebar/body background stops partway down and the rest of the
 * window shows the browser's default white background below the footer.
 */
html {
    height: 100%;
}

body {
    min-height: 100%;
    display: flex;
    flex-direction: column;
}

div.document {
    flex: 1 0 auto;
}
urwid-urwid-69907e4/docs/tools/templates/000077500000000000000000000000001526073734300203655ustar00rootroot00000000000000urwid-urwid-69907e4/docs/tools/templates/indexcontent.html000066400000000000000000000165641526073734300237710ustar00rootroot00000000000000{% extends "layout.html" %}
{% block body %}

Urwid{% if 'dev' in release %} development version{% endif %}

Console user interface library for Python

git clone https://github.com/urwid/urwid.git

Wiki:

Requirements

Python Versions

  • CPython 3.9, 3.10, 3.11, 3.12, 3.13, 3.14, 3.15 - Full support for all extras
  • CPython 3.13+ with free-threading (PEP 703 --disable-gil) - Pure-Python extras only (tornado, trio, twisted, serial, lcd)
  • PyPy 3.x - Compatible extras (tornado, trio, serial, lcd)
  • GraalPy 3.11, 3.12 (experimental) - Compatible extras (tornado, serial, lcd)

Operating System

  • Unix-like: Linux, macOS, Cygwin (full support)
  • Windows 10+: Limited support
    • Curses display module requires additional setup (windows-curses package)
    • ANSI support required
    • Not supported: Terminal widget, file descriptor operations (except sockets), ZMQ event loop
    • ZMQ: Not available on Windows (platform limitation)

Optional Dependencies by Feature

Feature CPython Free-threaded PyPy GraalPy Package
GLib event loop (deprecated) ✓ ✗ ✗ ✗ python-gi (pygobject)
Twisted event loop ✓ ✓ (24.10.0+) ⚠ Limited ✗ twisted>=24.10.0
Tornado event loop ✓ ✓ ✓ ✓ tornado>=6.5.8
Trio event loop ✓ ✓ ✓ ✗ trio>=0.31.0
ZMQ event loop ✓ (Linux/Unix) ✗ ✗ ✗ pyzmq>=26.0.0 (not available on Windows)
Serial/LCD display ✓ ✓ ✓ ✓ pyserial
Curses display (Windows) ✓ ✓ ⚠ Limited ✗ windows-curses
Terminal widget (urwid.vterm) ✓ ✓ ✓ ✗ fcntl, pty (stdlib; not supported on Windows either)
{% endblock %} urwid-urwid-69907e4/docs/tools/templates/indexsidebar.html000066400000000000000000000007061526073734300237170ustar00rootroot00000000000000

Documentation

urwid-urwid-69907e4/docs/tools/templates/localtoc.html000066400000000000000000000007221526073734300230540ustar00rootroot00000000000000{%- if pagename.startswith('manual/') -%}

Table of contents

  • Manual
  • {%- if display_toc -%} {{ toc }} {%- endif %}
{%- elif pagename.startswith('reference/') -%}

Table of contents

  • Reference
  • {%- if display_toc -%} {{ toc }} {%- endif %}
{%- elif display_toc -%}

Table of contents

{{ toc }} {% endif %} urwid-urwid-69907e4/docs/tutorial/000077500000000000000000000000001526073734300170725ustar00rootroot00000000000000urwid-urwid-69907e4/docs/tutorial/adventure.py000066400000000000000000000062001526073734300214370ustar00rootroot00000000000000from __future__ import annotations import typing import urwid if typing.TYPE_CHECKING: from collections.abc import Callable, Hashable, MutableSequence class ActionButton(urwid.Button): def __init__( self, caption: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]], callback: Callable[[ActionButton], typing.Any], ) -> None: super().__init__("", on_press=callback) self._w = urwid.AttrMap(urwid.SelectableIcon(caption, 1), None, focus_map="reversed") class Place(urwid.WidgetWrap[ActionButton]): def __init__(self, name: str, choices: MutableSequence[urwid.Widget]) -> None: super().__init__(ActionButton([" > go to ", name], self.enter_place)) self.heading = urwid.Text(["\nLocation: ", name, "\n"]) self.choices = choices # create links back to ourself for child in choices: getattr(child, "choices", []).insert(0, self) def enter_place(self, button: ActionButton) -> None: game.update_place(self) class Thing(urwid.WidgetWrap[ActionButton]): def __init__(self, name: str) -> None: super().__init__(ActionButton([" * take ", name], self.take_thing)) self.name = name def take_thing(self, button: ActionButton) -> None: self._w = urwid.Text(f" - {self.name} (taken)") game.take_thing(self) def exit_program(button: ActionButton) -> typing.NoReturn: raise urwid.ExitMainLoop() map_top = Place( "porch", [ Place( "kitchen", [ Place("refrigerator", []), Place("cupboard", [Thing("jug")]), ], ), Place( "garden", [ Place( "tree", [ Thing("lemon"), Thing("bird"), ], ), ], ), Place( "street", [ Place("store", [Thing("sugar")]), Place( "lake", [Place("beach", [])], ), ], ), ], ) class AdventureGame: def __init__(self) -> None: self.log = urwid.SimpleFocusListWalker([]) self.top = urwid.ListBox(self.log) self.inventory: set[str] = set() self.update_place(map_top) def update_place(self, place: Place) -> None: if self.log: # disable interaction with previous place self.log[-1] = urwid.WidgetDisable(self.log[-1]) self.log.append(urwid.Pile([place.heading, *place.choices])) self.top.focus_position = len(self.log) - 1 self.place = place def take_thing(self, thing: Thing) -> None: self.inventory.add(thing.name) if self.inventory >= {"sugar", "lemon", "jug"}: response = urwid.Text("You can make lemonade!\n") done = ActionButton(" - Joy", exit_program) self.log[:] = [response, done] else: self.update_place(self.place) game = AdventureGame() urwid.MainLoop(game.top, palette=[("reversed", "standout", "")]).run() urwid-urwid-69907e4/docs/tutorial/adventure.py.xdotool000066400000000000000000000002371526073734300231320ustar00rootroot00000000000000windowsize --usehints $TERMWINDOWID 23 16 key --window $TERMWINDOWID Return Down Down key --window $TERMWINDOWID Return Down key --window $TERMWINDOWID Return urwid-urwid-69907e4/docs/tutorial/adventure1.png000066400000000000000000000047571526073734300216730ustar00rootroot00000000000000‰PNG  IHDRp¾¸h¿ cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñe gIDATxÚíÝ[–¢JQꮞEÎh9�ûQ>È� G+öO7 FÓˆšR?yé¿«7à¯18Ìà0ƒÃ 38Ìà0ƒÃ 38Ìà0ƒÃ 38Ìà0ƒÃ 38Ìà0ƒÃ 38Ìà0ƒÃ 38Ìà0ƒÃ 38Ìà0ƒÃ 38Ìà0ƒÃþ�´Þ4MQ¾®hS¦iúl{¾Þ{öpÿц�uH±ÿŸ4a‡õ)iš¦â°�Šéß»oÓ©™£¸»]_š¦<¥iø°Ó™¿Þ¾f–r{o7„8Ìu÷ðTü1M)Ó©¼y(Y±¾Û_çÓëëJåüíúÊUVÛÛùy×ùÉíÆí–4ßco¸MçùlÕ_;·Të»­®ØçÖNsž?~ʽõM³:åv{{?ï:ƒÇð†?àØ Ÿ¿sx;_iÂ:Á×ÏžF�(#g;[/|æ?ok}/œ ÷ðßGX¼ÔK�9Î6|(«¶7��Í{> œÊ7+r*Ïxo“9µ7Í×öxùW¯ï€z{V×Wmo,Ý=¼|EùxÉV¾ÒÌ�%ä‘™VWP¼ŒÜ\_µ½¡üDܨRìWŽ{y–38Ìà0ƒÃ>àI󻸇à 38Ìà0ƒÃ 38Ìà0ƒÃ 38Ìà0ƒÃ 38Ìà0ƒÃ 38Ìà0ƒÃ 38Ìà0ƒÃ 38Ìà0ƒÃ 38Ìà0ƒÃ~êêoD„ý†é·È«“z•‡˜Áa‡�u±ßWô.x²ã"(_q½½ ³‡¿{§‹½¯ŠxHéùÔ¾ )°6¸/uNÕ9¤  ×.î]I¼3ïT]kmþú§Ì¯;¾´¾ö•yê\™öq¥¬TßÏéRòÀ5¬fóì½¦ÕØüãMrs®êYÍÌÕüœöÍ«Tì/¹ó�gãNåôRŸæBÈ«óçù%Â×Ö0«÷»Â4›|._ÝÐÜ�ê_!?MÓ”¯½(Ú¾[×U¬nè\w‘²p–’nÍRìºRæÊòym¦Ñ$[Ï9�ûC\nù´ðÒëZ^öœvºå>×ï ïè±,;o;WþÊ]¼üë.Çè±´Á#ìÜ¿¿æ+wñþ“æêfª^7ìý-&#ó§¿vgëä#ÄÉÉCï×Ê œŸì»—ØØˆÜNå¼1w„Ë-·‡”íÚ)W¿ç,o‡îK¤Áù�æûù왥Yßê¿çÖý¨CoÏÖןïþn´v‰óßïÎ+÷ͦŠ_ÁÖ™;u‹aǸ”‹_ü&?€€ þx74âÑàŽ}¦Y~$ Ž?iÚû� ¿mðïXzi¯“x…|˜§…0ƒÃ 38ìSsŽˆõÆ÷÷p³‡ľÇp‡Ö÷}’SuŽáÜÇXÕâ}Ú¼8ïý�2Fóìç±ÔNîZÁÆpÚ &Q|Ax*ö­õ%ª!rÛƒ‘çO–9ÍW�—篦g_­p <>˜s×wíso*¯Îý\^Ÿ»³=ÅþX^tö.c—|3?€€½0zö¼0ÐWãßî¥Ñ³§õ8{ýzáçõ8{ýWrô,Ì'M˜Áa‡fp˜Áa‡öãï^þ�ÜÃa‡9>Ö¾y52ÊôÀøðbŽ‘ñá½ úÃÓÊ gÓ½å/w|øÖúæãëÒÞ þüÝY¾3>¼Þ�~–rýøðÑñÊó�çsug”Ä¥€ãÃoólhLJßWèøð#Þ+âøð|ÿÔ~8ùè0ÄBŽ¿ÿçJ_¸“¾óð›7¦‰;>ü+¯r|ø^õÿ†�''LJ§Þ/;›//ÏOk£üDþX}^X~ßøðÎòŠ8>ü~÷Ò׌ÛòÂ’ylyRäñá_É `!LJ³˜ãÿXÈñáßÌñá0Ÿ4a‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜Áa‡fp˜ÁaÿfÚÎÞI¹"IEND®B`‚urwid-urwid-69907e4/docs/tutorial/adventure2.png000066400000000000000000000071071526073734300216640ustar00rootroot00000000000000‰PNG  IHDRp¾¸h¿ cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñe ¿IDATxÚí�Qš£*F�ûÍ.ØÿÒØÇ}èLE!hô·´Ïy˜nD=\Œ¦‡ÝébwâU")wõÛÀ�"¦6yÑ3p†”‰ò'¿Õ§àZ ª6¿ø2™«H«\÷êæ /7¸5ù±øCÊ šÍ™ææó­?,?ïd”/<5¼.Ÿ¸›¾£é/Í×ešn'Ÿwg�fý¼RºÉ‰ŸWê[Ín>¸6Ÿ¸ŸÕ-/ÃÜ�g³ÍÇ(_¸ùàÂ|â�³”ürÞQQeµÛš|ªüÖwDÌ|"egÿ´ðÒ¬AÞpý‰Ïõ�á8íKGø·©K� =²‹»Âg)÷#о´Â#tîWÂÞ«qþAsõ€™Í¼aëÛPfÊçrŠ9YßÞïµx Û'ÎO¶ïÁæ5�ð^S•¥#$ß•(²H^-çÑõå�^y3“­,åµúª‰os©gYêî5Õw]ž5óº‰üàÙ[ìîÚ bêä à ðM*Ôßw¹n@ˆÙ%ü}5”¾™¯Þņïíì?hâ{äÃAS ÂÅ \ ÂÅ \ ÂÅÜ%˜s†[L}éábî~cèáb.†øp11ãÃMýÍÕ`âÝDÆÅ‰·„ŒwÞ1Þ¶ªÕhÚ(Ü7>¼¨?ÛJÞ LŸŠ>�n ^6çý#ÙOsSê³#Ü0><µ2½Û,äêE⓵6NK¾æñႸ:�ãÃ˶¼†·çñá[۩톨ñá�%`|xîÌSÒ¸¾j9&AãÃS»¸©‚8ñà–ˆñá¶þ+W×nr¿|¨xpKÈøpç¹âqíe{Ń["LJÇè’à 1!ãß3>üÁñç!ãßN|¸šb.áb.áb.áb.æñáW¯ ôp1C|¸˜öâÕL”éŽøðªÄL|x{ƒÒ„¦• ‹eoý ‰>ª¯Œ7+¤­ 4>üüí¬ïćWàm@xô³”ëãÃgã•ËÛóÙ|EqMÀøðW™NòüüŽ">|3��c‰žÿݵŸV>†€�ñáÿþ¸ž˜^9h|øÆá7–7><¾ `|øVì_CÈ““7ãÓ·ÂÄ´³yx¹<­�ò1>¼N->ÜYÿB"Ƈÿûº÷˜q»BçÖW9>ü‘pBLÈøð'3>üÁ„Œ2ć‹á )áb.áb.áb.áb.áb.áb.áb.áb.áb.áb.áb.áb.áb.áb.áb.áb.áb.áb.ábÎÊ®|äcÊ^]ê�õÄôß`íy<Çôð«ÿÓ®ÞþÎRÎÞÿ»ømà )ábü!¥I¥Wg2¬²w¤¦D›Ü£ª¯Iþ=Ë;ëòbªë´0;ë: ÚÝž]¸=ܦßNuž5›¦{ÊÔR%V³ïQŸIå<ï$5yáêíµùÅÝ÷ºŸÂŸÜám~ïOÏz-粘ùÕùÄÔצ÷œf¤¶¼Ÿ‘¼°W´·ÙžùÀkÏyLŽáU·Ï�½}bGzïm>œ÷†gû}î.ž‰#Üæ÷>ê%:_eÌÜà{ÔÞkó‹•óê8æÞòñö�’Ut?îÁáÕË-„<#›ø¾!åî»~aûï2Ó,&<÷ÆKgmÎNzóÙýŸ9ÛM|ò㣓�kó‹OöpónªÔîýéû0R»´òŸØæWñ·jìëü4Õ?rªöçßbñ΀÷Gemïë¶¾ý¤²Ÿ×KªíÛö6Œ¾?ÿMUÕ�÷”­žifg�Þβ6¨˜üäu{ÛÒÙ]M©PÅÜå,å1 \ ÂÅ \ ÂÅ \ ÂÅ \ ÂÅ \ ÂÅ \ ÂÅ \ ÂÅ \ ÂÅ \ ÂÅ \ ÂÅ \ ÂÅ \ ÂÅ \ ÂÅ \ ÂÅ \ ÂÅ \ ÂÅ \ ÂÅ \ ÂÅ \ ÂÅ \ ÂÅ \ ÂÅœ•ì÷Bb§Z¦‡‹ÑôðØ�N =\ ÂŴ‘;-.Î>1àþä—ú\Kiåå÷^M�åäó¶å›"£|áqð‡”Aº3›3­Í¾Œ×_+ŸL¾o¯|Qd”/<nΫ¦¿4_y·Û|ãÝ5šõs§p“¿Ü”70åj…n~ò8¸§…yIñOåòû»ðNÞ‘ÎYJÎyé�,ÉìßÖüàåÝüåuù^²üèôO ó�öâFôgš1ÿ"oOGøA©K¡Á>8K�/h…Ó¹OÅ?h®0³™Wl}ÊDùMõ_›|+^Âö‰ó“í{¸ËÉôßÙ(_xveW.‡WËy´Fýú´õ‹/‹™X–åS{áäS ™Ÿ±ØuyÖ¼j"?xö–úås=s•å �Ä7ùÃOêB1{æQpBÌ.áï«¡î‰gñÕ»Øð½�ýM|ï‚—nˆá )áb.áb.ábn>¸Ov|×~‡Kß.7�‡UØþÛ)s¯ðŒÇ]‡”ÛB|¸˜öâÕÌð¸#>¼*1 Åðã¿ÍÔ¢Šº|{ñض×6Àl¯ª`¦ýNH{�˜ñáŸ�TÍI“å‡íõêKM%] £xõ5Ƈ— (ã¿M�ÏÝò¶€Ó^SŸÙ^¶UŒÚßn¿‹ÿÖïŸXåkGó&þ;o(?Syî­ÿ¹¿²ÅÀäöcƇ/Í9\aøÙvת¾ùóÈQ¼ú:ýóðü‹ÏWNÜó{ćÝ–‰ÃõYîî{yzy,÷ˆ?º-£úNÜwâÃÅÄŒwËï¥×Þi\ߎn;>ܬ½f�ÜåÕ©dZ¯a/ã×~ü÷ÊL3;¥—æè—ýúš™ä²ü@;Y]ˆÜ “Hk‹vÿë9Üð)Jr–ÖK›Gìssìsy÷êÿ·ñêÚ�÷}Ø;>ÛŸµOÞRâîÛ1-&~4>ñ>á²>W›æ»ß¢oK:tˆº½Öþž£s �Û¶m1&ûAÌo8ø˜4-oï=û›Ä<�éö,2Þf{òxb@wËÜ<üòÑбB3#¸‹“ϪL¾Ÿ}(üs Ÿï.‡+M0†àõÙÀAïˆÒ3Ûi-|Òýµâ�˜ èìá¿G˜-õ‚Qânº‡2Ñ^GüìÍ;(†übE ùŒ÷³ƒþ(�¶/ÿd¼ d{ªñD{}aöð|E¹/Ùò•f4jˆ=…ª²ed3žh¯+þylTŽï•ã(œ¥€¡à`(8 f�“æ»`CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁÜeö{ëáú�î}?›ÏÓïît¾;q�CŠÅªú*8¤€Ñ‚»5/zÆ�Ò1@þú[kTÚ_|ë´ÀJl•“âV¼¦_xºÃQÿòk±‡”†šôL3ý¼[õ›åû5iù…µÃçüÄMûÕ_ÔשM·áç]¨¡êÇJiå‰+ñª~áâƒgýÄmW·¸5½ïfL�–_¸øàA?ñÂ,%~4/H‘¹Ú�úƒw•}GDÏ÷.,;ËÓÂG]ÿœ¼?àÊ Ÿç;Ãu8:–‚àg­K¯ ¼²‹›‚7f)ëáèX´à:÷ǰ÷éFÜ�}Ò¬ž0£X7Œ¾ ¥§|L—˜�ñf¿Çb¶wÌOÆ�`¸F£ÖkªB£´Cñ)£ÈÄ8<ÛŽ­ùå�Ry±’ÍTеxÙÂW]êÙ¶|…ûÌEõ©Ë³b]×á­­ÆáÖ¯á¾áà¤pÆ uáû.ÏÁ`¦߯†²ƒsê]lÔ{œù“&õž‚þá`xÒCÁÁPp0 CÁÁ¬’ÌÙÃK_öp0kæ‡/ {8 †ùá`|懋øêj°LñV™q~òÁ%.óÃ�wŒëV%©ª=ŽòÁ%~óÓøQÙŽ@G£]åƒKÜæ‡§ÍÙÿ òÓ¨J>òÁ%óÃÓja³=ÎR®>”>ÑQký´ä4+ä‡òêp8ÎOÛ²Ÿ —g�üðѶxj»Àk~økq˜ ë”ÐŽ—mûÄi~xЛCüäƒK<æ‡Ëø•³k7±\ÞU>¸Äe~¸ñ\q;zÚGùàÏùá>ºäÅð—ùáoÎ?÷™þâüs—ùáoÎ?g~8ž4ÁPp0 CÁÁPp0 ³F~øÓõ/„= Ãüp0úâUO–éD~xV¢'?\ß 郡0Ù¶ê?ˆßüðV¼4?\T£ â4?üHþ6êùáY¸N÷>Ky>?¼7_9½=Å—^$Îq˜þ)ÓÀ0Ï�{ÆóÇq4\‹Çüðøw×¾[òÞ4D¸Ìÿûãz£½²ÓüðÁá76¶á7?<:Nà0?|ù×àrr²ã1?:ÇðÌÇíøo¶OLØ{�éa¼7<ÊïcqóN Á¥¿÷U/Ñ9å˜9 w«½Ïú‹Ÿò¼ºŽ¾·|¼�Á½¸ŠÎóàLž½ÜÈ;ÜÄ熔ÕýÁö¯²ÒLúÛ�¡1 ½Uù%jq?Yc?óÓ�_üˆ!;ž¿Íä�ûGi´ýz†Œ7OHûyr½$Û¿l¯¢õý­ØoªÊþÛ—lùJ35JGØS¨ImPþäy{uéhVƒ@+T0«ÌR^CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁÜeöû ¾­–ÙÃÁ`z¸ïN…= £…wš_Œ1¼cÀýõ—: Ö,­,殺áç-Ë«"-¿p?ØCJÃîLz¦ið­]¿V>¿o«|R¤åî ÓóJõõuâ»­ýÆ‹5TýX(¬üËEù½�!fŠþä~0§…q þ§rqÿGnìæ})ÌRbŒ[id âøFýÁ;Ê›þåyù’Y¾wÊÓ¸ÐQ,Dy¥éó/ry ‚_d]J¦à�Y 9�œ�ûVì“fõ„źbôm(å‡â?ë>ŠeØÞ1??Â)MºÿÎZ~á~˜rWNŒÃ³íت‘¿>­~ñe Ë´|ÐNŽAü˳â=PþàÑÚ*—�ùʱU¾åî‰3þá7u!Ÿ=ó*xÌ”àûÕÐ÷Ä»8õ.6ê=ÎüI“zOÁ—n€áI CÁÁPp0 ³l~xã>‰Ûeðª=|…Kß&‹æ‡ƒÓª.lÿ²CJß+<ý±ê�²,Ì£/^õ �ùáY‰V*†�ÿ-î &!òòúâ±l¯l€Ø_ §ýFJ{ ŸùáÇN²æ„ÎòÍöZñ‚ RT¡•¯^Ãa~xÚ€4ÿ[ôøX,/ íñÄþ¢ Ñj¿Þû­ß¿¹ÊÏŽæ*ÿ;”ï Kõ�û+# tîßg~ø¦Èf…æ¤î®Y¼þyd+_½Ny¿x¾r㑯‘~u[:N×wÁüpóØÓé嵬‘~u[Zñn˜Uæs?:¤<šÞq‘õ³íÙüsŒâß‘îèHÖÈ?‡«#ù‚üð{ó½GY#?ü,ŽŽ…ùá`æ‡fAþôv¬ÅïÈ÷à4?\¬ý*Zu‡s1‚;Íßã§á÷tíf¶·Œ¯ó½›ùç÷á2?\ø�ëe‰ðoø�ùÞõ㹓óÃ×¾}Ä`¾"?Üß‘È÷óÃÁ𤠆‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¹Ë]ùÁÇ”}û‰ÿð n,×ôpþÒº¹kH¡þxÒCÁÁØCвÒË� 3÷í®Í=²x¦ÿwƒ÷ø‰›=\ú{ ?oÛ�»%—é{wì¡ëM~âV—þÛÊÏ[uä|–’™‹ñ¶-|LÊûzÑ«üÄ;ÇðÌ÷Ìöã!nƒ²ïñ7o9\Ž2ê/^ª_a!?ñSžWמ¶?èÒ9#¸SÑimÿ„à¾ü¸WkÿÜÂgõ…ûƒíçJŒegÝyvïý‹ì™íÔ>ïòïìᆟw0JÜÆ{üÄÿE5ù×þÞ¦ŸwêÇ�]�ÈŽ$êxòZˆüÙÖGíQ?qûMUbo–Ÿ·0ô®îä¬uê›üÄ_`…ºÖí&ÎRÀPp0 ó‚“æZ°‡ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`î2û=ƒõðüÀõ¾Ÿ½gƒéáWw:ß�¸ŠÇ!ÅbU}RÀhÁu#\cHé ½ƒp‘ŠÅ²ÿå&T}{‘vßV<±•Ô5v8ã_~ö�ÒðΖþÚÚ|k×o–ï×Dû‹ ÿpµÃ}ò˜öª¿¨¯ÿ:^Ì·c½†ª+¥•Ç}¬ÄûŒÉæQ_| ¾‡b»ºýÚ£?:š�é!|Ø”?¹ø`Ø¿ü: ³”øÑ¼ EæR7êÞU~@ïÖ9'$ÆyÏÏÊÓ›|ûúxìœv;å…Ïó�á:KAð¾yÛ½t¾’c1LÁ³”õpt,Zp�û׉÷•]Ü>iVO˜Ò_{ôm(=å£zP;Þì÷X,ÃöŽùÉø ×h4"è­¥Ÿ]Yü2eYr쎭r]8]Éj?òR<ãmF{-ÿrS—g¥¿vÛ—Ì}f^ÊÂBwþìñ|�HÁΟ8X8¸s–¢žà'¿OÝ•™^kc?T%~h­Þ!ÐÕ\yÒÐ=÷Þ,Ïšìâ×\-lIKéwîºþžyÜÅ\(xþ pö˜ääädËê¿ã÷7-xí1ÈmÛÌ,ãîãbóƒe™Lu‹É¹rßЪĬÀXð#Ü`}ßLöð 7‚Uæ3¨«Z=¸ºtàáñ…;x*/¥ãA—žgaÜûö`¼^½ÖíèQùÓxð¼ê¾4ð½Ýô𪚣8KÁdÎ{ÑC %¾•ÿ£—«áТIEND®B`‚urwid-urwid-69907e4/docs/tutorial/adventure4.png000066400000000000000000000105001526073734300216550ustar00rootroot00000000000000‰PNG  IHDRp¾¸h¿ cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñe¸IDATxÚí�[š¤*„íóÍ.ØÿÒØÇy¨* 2¹(abÅÿ0Ó*"Fg#hþÅ� ùïîü CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁü›ToضË^ײêê¨ÿʦœçŸ³ö<žk"üî_ÚÝÇï`V—2ûüWÑWÁ‹& ÆîR¶mÙŸmÈ–_›ßËA•È6ëú¶Å-lýÝÂgŸ¼~}|µ”ìk4h¸=C˜²ÿ¶-„l9ä«Û”Jë{ÿ˜.7ÔÕ®‰h¯:^P ’í™Ç_ÔWø°Ò7b_+ÞË1-&~4ÖˆúÞÕeú3‚./vPKi{ÕñÄ «=óhìÃã¶m[ŒÉÂÞÈbÿÉõ鑵×8žX1ОQ Á?á¿i§#ë­¤µµöîÛO5h”±qøåí¬i »¦MgDp|X\Í�g0 ømàf‡G„øX—²ú©ßØþUfšÉ„gm Áåè¤t1o=ÿ–ÑNmâ;¯ >nœì4FøK±�.£ÄL*Ý@ÐK¿Ä÷Tôáÿe�}�OC~ó#†ì|>‹1èUimûý Yß8!�óä~Iv|Ù^EmûTÌÏg”û”-ŸiFc�Ò¶ªrÔ©äU‹öêÒÑÜ ÂßêŽÕXe”ò(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 f–Ùï�ø¶Zf„ƒÁD¸ï ƒÂCÁÁhÁá�æ£oèp_þRß‚G–V–¿÷¡–áç-Ë«"5¿p?Ø]JÅîLz¦ið­¾ÿQù ü¾­òI‘š_¸'LÏ+/jsâ»­ýÆ‹{¨ýc¡°ò/å÷†˜íPô'÷ƒ9,Œ[ð?”‹û?ra7ïóHa”cÜJ=Kç×ëÞPÞô/ÏË—Ìò½SÆ…Îb!Ê3MŸ‘ËSü"ëR¢0¯ŒRÈ ´à î©ØÍà fóŠÞ¯¡4”ïªÿ^?ð^,Ãö†ñIÿiÒüwVó ÷Ã�»rbž-ÇÚùçÓŽo¾lbb™–úÆÉ·@ÿûbèö¬øTƒ?x´–Êåc>s¬•¯ù…{âŒø¤ò™WÁ`†ßï†>8gqê[lÔ»Ÿñ‹&õ‚ÝË& †‚ƒ¡à`(8 fÙüðÊs·ÓàU#|…[ß&‹æ‡ƒÓª.lÿ²]JÛ'<ý±j—²,Ì£o^µt�ùáY‰Z*†�ÿ-ž &UäåõÍcÙ^Ùq¼¬‚–ö)í%|懒5'4–¯¶×ª/¨JŠ*ÔòÕ�p˜ž6 Íÿ‹åe£½¢>q¼(«¨µ_¿ˆýÕïW®ò½½¹ÊÿŽå[*�¥ý¿ÏWzh<¾ÏüðMu�Õª�:\³úÚÇ‘µ|õcÊãðøÃ㕉g¾F~øÕmi¸\Ï‚ùáæ¹§ÃËkY#?üê¶Ôê›xîÌã3?Ü,?J©½5B½¾�°ô�.öŸsùp*ŽkÅa~øVÎÿ>˜iF£ô¦®~Ñ®OÍ$·íõf­ÈP/µ¿#Ø}æ‡Ë½ãÖ¹G¥½ÇõE½í\ûSæ‡OÃE{ùÌOä‡{jïoä‡;jïOä‡{j/óÃÁ𢠆‚ƒ¡à`(8 †‚ƒY7™sç²$d*ʳh~øº0ÂÁPp0ÌãÐ?\––Vf2ö¡ß¸º;+«¾2ä2?\û�›BiYÔoç{§ùç ç|:Â_i¡-?<_6³£u*øqùļœm-÷Ï–UýúU†Ï & & ®Ôi~¸|Î~¾2%ªÌç¾µK¹5?¼á"÷Ï–GóÏ1ŠÿF~¸£3Y#?ü®ÎäòÃçæ{÷²F~øY� óÃÁ8Ì/Œ.‚ü9èåxTC¾7§ùábîw Usu.zp§ùá{ýiõ{ºv5Û[Ö¯ó½«ùçóp™.üÀõ´Dø‹WüÆ�|ïãó™É‚ùák?>â0?‘î‰ßÈwÄOä‡{‚ùá`xÑCÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁÌrW¾ñ5eß~âÿø7–k"œ¿´ffu)Ô¿/š`(8»KQVz¹“aæÞ¡ýÁµ¹GVŸéÿ]á9~âf„Koáçmûq×ä2}ï¾G8ªëI~âV„Kÿmåç­9¥dæâF}ÛÞ&åmQô(?ñÆ><ó=³ý¸{ˆ[çŸìsüÄ Ák—½ôú‹—ö?`!?ñSžW×¶ßèÒ9"¸SÑanmÿ€à¾ü¸WkÿØÄgõ‰û�íçLŒegÝxuoý‹líM|žå'Þᆟw0JLã9~âQ þµ¿·éç�úqgw'²3‰º>y/Dþl룎¿¨Ÿ¸ý¥*q4ËÏ[zä¬uê“üÄ`…ºÖã&ŽRÀPp0 󀋿Z0ÂÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0³Ì~Ï`½<ßñB½ïwïá`0~uÐùâC=„›ò'+ºý˯£0J‰oÍ Rd.u½þàMå;ô®]sBbœwÿˆ <,œäÛׯm×´é”'>÷Ãu8:—‚àmã¶¹4~’c1LÁ+£”õpt.ZpÁýrâ}dˆÛÍà ¦ô×îýJKù¨>T¯ot;˰½a|ÒÝ{TôR•Ò÷Î,^ E–»cm�‚?¹.œÎdµy©>ãmF{-ÿrC·g¥¿vÝfò½ÝŠ?ᕜ‘P-aTÅ*ó;î2ßû“*òÅïJx¼h~Ÿoˆwò<ùÞ½L\æ�_U©ú½¯”ªF|L|$ÁZ±HW°¿÷!q=l× ì‡ž¾¬•­DˆÛ‚ß}jµãßݾx¥XtŒ3Ó]üq�ài kí½Ó¦ñE>�ù*Þ½³+®Ô¥T'þõ—¬~—ñ.¥þ õ6`~8^4ÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0³,˜nôK¾ìÐSÎáß/YI{àšç/­™Y] õ/À‹& æÈ™3Š1ßœ› µ&êñûrù¿¬/_ç£(«˜è¼jF¸´ç![.ø{WäR^ÉÙòa]âø¥¡´,Žo·?Ù¥ê~+Âwÿï�ùuo!dF(Iœä£iê(ë+ø}—å”Ç׈úUûóí–?yø6;Ì4nnìÃ3Ÿ6Óß»‹n¿ï~Ÿ¸ZeªýÒ›×¥$þß!]EÖ7ºÿ²~³ýÇÇú6KñSßD¾ð°ýFGÏÁ4 õÓþÁKþÞ«poûÇ&>«OÜol?gš` Á[¯î­‘-£�£‰OáøAþlÌ«G[¥#6øŸŸ 1ÂÅÜËð÷žÛ!Ç·èø$ê]W ¿¨ÿÚÊ4äÛ?SÌ JèkQÔõßnküVXvüÔñä R¶_|ê-¤k¢u¼ �½áÒÊô=ÅËgš†åw‰³Ö¨Öñ£.KËâørÑޫ浂X¡®õ¸‰£0 CÁÁ<ࢹŒp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0r…|#bQˈuˆ‡‹ä,ìRÀPp0Ì,³ß3X)–pó<¯€áW�ï >Äc—b±ª¾ v)`´àœêLÅèR:È—q̽® {¥þGÊüªá(ʪʨO›m ‡çÌ,+³RFcw)±ÁÃ*)ÓëiÕV¾]“¨\¬„ç•:àD7ß úæUn–�Sމ¥w¾\³”û‡ƒÒ¹Þf ‰z¯ C²øÝ_¬PÛ¡ØùaÛ¶8ÉÖ¬‘>=j¾ŠbÅ�¾……QJxk^�"s®ìõo*ß¡wíš³owaÿVNúB·]Ó¦SžøÜ ×áè\ ‚·�Ûæâ¦à+Û±›8:-¸‡à~}qæ‘!n_4�?c$æ ½_1i):>U|¸œìXŸ•iŸôŸA÷•FD½c¥ô½3‹ºK©«¢°Û޵~è³Gh,¿ë”ÆyreQõþ>kÛ¡ Ýž•ß*ÓßZ3÷è(ÿÙ¶%K!ýÏ(ÌÝ|Б—róäMøÌ�àûÝP�¢Î{¦)>ÎAÚ¿hRï!œ|mðw(MíÉ$è�†ÃB0 CÁÁ 'sž¾Ëz>ÆØy¨öºáÜáS5‰~g >Ò•}=”™Êp„Ÿ¼7;ù�Þoˆó¢ æ&Áñç4ćúð&>’`­X$„+Ø‚† r¿îiØ8Œð=šåUó!n ~÷©ÕŽwûNàq”b1ðÒ�ÏßÊu‚§o«yˆÖ¢¯£Žùk— ÷òת+úª°Þ£Œºô­/Ú rÁE3†×Ø8ˆµ}tïñûgA×bcý›×å’WÓœÚû…*ò…h•yw*ÑÚm †�n™ûÂïCÍUF)�aTðJôV‹Ì}æ7À‡º”êÄ?÷¡!)ã]J¬n¢ÞÌžË&˜ÿ†í<‡!y¢IEND®B`‚urwid-urwid-69907e4/docs/tutorial/cmenu.py000066400000000000000000000062051526073734300205560ustar00rootroot00000000000000from __future__ import annotations import typing import urwid if typing.TYPE_CHECKING: from collections.abc import Callable, Hashable, Iterable def menu_button( caption: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]], callback: Callable[[urwid.Button], typing.Any], ) -> urwid.AttrMap: button = urwid.Button(caption, on_press=callback) return urwid.AttrMap(button, None, focus_map="reversed") def sub_menu( caption: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]], choices: Iterable[urwid.Widget], ) -> urwid.Widget: contents = menu(caption, choices) def open_menu(button: urwid.Button) -> None: return top.open_box(contents) return menu_button([caption, "..."], open_menu) def menu( title: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]], choices: Iterable[urwid.Widget], ) -> urwid.ListBox: body = [urwid.Text(title), urwid.Divider(), *choices] return urwid.ListBox(urwid.SimpleFocusListWalker(body)) def item_chosen(button: urwid.Button) -> None: response = urwid.Text(["You chose ", button.label, "\n"]) done = menu_button("Ok", exit_program) top.open_box(urwid.Filler(urwid.Pile([response, done]))) def exit_program(button: urwid.Button) -> typing.NoReturn: raise urwid.ExitMainLoop() menu_top = menu( "Main Menu", [ sub_menu( "Applications", [ sub_menu( "Accessories", [ menu_button("Text Editor", item_chosen), menu_button("Terminal", item_chosen), ], ), ], ), sub_menu( "System", [ sub_menu( "Preferences", [menu_button("Appearance", item_chosen)], ), menu_button("Lock Screen", item_chosen), ], ), ], ) class CascadingBoxes(urwid.WidgetPlaceholder): max_box_levels = 4 def __init__(self, box: urwid.Widget) -> None: super().__init__(urwid.SolidFill("/")) self.box_level = 0 self.open_box(box) def open_box(self, box: urwid.Widget) -> None: self.original_widget = urwid.Overlay( urwid.LineBox(box), self.original_widget, align=urwid.CENTER, width=(urwid.RELATIVE, 80), valign=urwid.MIDDLE, height=(urwid.RELATIVE, 80), min_width=24, min_height=8, left=self.box_level * 3, right=(self.max_box_levels - self.box_level - 1) * 3, top=self.box_level * 2, bottom=(self.max_box_levels - self.box_level - 1) * 2, ) self.box_level += 1 def keypress(self, size: tuple[int, int], key: str) -> str | None: if key == "esc" and self.box_level > 1: self.original_widget = self.original_widget[0] self.box_level -= 1 return None return super().keypress(size, key) top = CascadingBoxes(menu_top) urwid.MainLoop(top, palette=[("reversed", "standout", "")]).run() urwid-urwid-69907e4/docs/tutorial/cmenu.py.xdotool000066400000000000000000000002251526073734300222410ustar00rootroot00000000000000windowsize --usehints $TERMWINDOWID 33 14 key --window $TERMWINDOWID Return key --window $TERMWINDOWID Return Down key --window $TERMWINDOWID Return urwid-urwid-69907e4/docs/tutorial/cmenu1.png000066400000000000000000000056531526073734300210010ustar00rootroot00000000000000‰PNG  IHDRÀÖ½ç cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñe #IDATxÚíÝYvâÊPÝZošÿÐrïƒö ,;Øû£ª¢Lcà)$þkÜüëý P‹@‚ � ü¯÷/ÀÕ8 ­{­C”Q"Aˆ*j4� DE„@�¢†* B H—Scï_äëÔh“Z‡ Üv]{§Ì_*Û‘uˆ ê¬Aˆ 5� D•„@�¢»R B HÑ[­1ü×.ÿmOå_*{ë� A ÿݶ¡“K‡Ë ÿLeïoKA AxuÀèM˜Ÿiˆq8?Ö×üÌÑ—÷¹JÕ°d”Q" �‡­7Š·öÌ“}ûò(B‡¨¢FƒÒ쫌6Þþ†k·o‹e[ßb|¼¼ËE´Ùó�·�Ð/Ú-Ò –:Ä8L½ÕÝ­ãýé¶.ïzAãîóóWv퇸>“oe>xc†ñô×çnpw†çÎÿ‰ª4ˆ…цûÓ·ë_KÏá<ýŽŸ?üÇÖùù3K•G·ï\�N~³J\”i;w]{ܾƞ@+k!÷ý$F¼Èª:+ÆÎ%ãèÇS>ʲ§²€B â™@µðï|qa;£‹}�8ühåéÂw}~W*5ˆ=Ûmìp> xá¸\àú�Ûw¥¢Š=¢Ýýyˆë{)æ/³|uÅ•j3¢MÿÕæþšœlá!œ¹¼þ×8{‚69Åôüíþ?f®¿M®ðíÛ*Ò –;D{&Ùíák²ë]gŸ|×Òî_aúÝOyý ß õwžº3ûZûªç'žQ§¯ƒ¾½îx¬gNœW6ÃÐò{ÿZœz\=ÿÃõµSÞo-lrž¿sþU6îÍ* b¶C´Ö†ÕñÜmÆÉEþô^»]ãí¯W¯`ò=£ã0t|e2žâþmbˆsžº÷.÷w[¨WµÓríúz?ã9‹?¯Ò æ—Œî/ò�üúïÚ*{À;ÍnTn4·víÉ¿s�î­—W}�÷X1f:ÄxÚZBýæ�»¾b<ìJ.OÎ~ß½ñº·ÎŠ1»dìÛ.þM‡_ó¯u³]ž}ÍÖÓÂËε6qÿ ~�6xºI�~×;Å 5ˆåS«/3Ƈù¸ê©ýRKÏÞgÏÿÌvããkÀ­ú‡Ê~ÃÖÔʱŒ'_zÃô™ì�^rzDc×b›9ÅkÚO/àg6ïÇJ bãð÷ÒŠ±ðŸËÆ6Ýax]YÇ\{®§m³gŸ=ÿz_žÓÖyŸæ0 ˇ¿Ç‡¿Ï-ñîñŸì\_CÇû¿& ÒÖÙ‡qýD—}hJ5ˆÞ “óÒ“o\>ÛÜã2ÎU �7Î>‰ëÌ9¶~Ý¢ÙùÞ Ó[­áKغkµj‚ „– dŸäù@¼ÑÞjž÷ŸÇ—{—mˆª{m>UÙûÛF%A AÂKû!âƒ4¥ÄQ�Wq=¹;}ßÒø&Jäá•@ŒwïMáÓ<¿ q –çü¡j4ˆ�@¬7‰ØãÍéÊ’±ó–Ü’ÿÞX™½¹;Áxþ°v¿Ïgÿ²}PEÄkoÃ�]ßýþ˜‡•*=œàÍžDÏ+»£zjñƒ:ÃZ®×_`\žë1�¡M~>^ßX݆qÏ|œó-ÝèUÄÒGù†å8´™ÏÞ�mÚ6Ve»^Ð7h§H¼ÅŠ8]26§[´û÷7óîƒ+Wyÿ2å[2±5ƒ¥Lƒ˜_26’|[–½Éóáõüðô†l>˜í~`Æv—€6n]Ê[Ü5{“Û< ÄÞoeàòdøÄmÄ×lý©³bÌvˆ}ÛÅs·øá"}YPŽò»Ç>³]Üænñ÷¼ˆX·ãÐp¡±¼§rqèÇë@Û±¿áÙ›TÜý®+Ç2–^zž?Ú}ÿ˜ä<‡™OoÏœ÷�vàýÀæð‘J bãð÷Â-Ùñš3ÏÞÆûÑ—ê;¼Qw†W_ó¾ü6ýy[¸ˆÞ7¿‚R â¥wLMƇëh›ç™�GÕ–/a«æ(ÞdÛ[­qý(ß�ï~ì[~OÙAÇG}ú»èÍãY-òð)èÂðIlṪ ˜ÑGÙûÛ’A‚@‚`´q%Žjèe”ȃ@�¢Š B HQD‘!$�¨¡Jƒ’@”P¦AI QA�C HQ@¡!$�è¯Rƒ’@tWªAI z«Õ Ž™ÁÓÊÞß:A A‚ùó!ú({[2A‚@̇¨£ÄQ ¢Œy’@TQ£AI Š(Ò ‚$5TiAˆÊ4� A *¨³bI (Ô ‚$ýUjAˆîJ5� Doµ„ù�”½¿u‚@‚ ó!æCôQöþ¶d‚ � ˜QG‰£:D%ò $�¨¢Fƒ’@Q¤AI j¨Ò ‚tÛ1UõðÛW(Ó tÒ¥CxŸ Ã0è5ÔY1‚$jAˆþ*5� Dw¥„@�¢·Z ÂGùH:A A‚@‚ � A ÷ó!ŠíUWwžQâ÷Qw®-„[ jTݹÖ!×@ ¨ºs­C®³®‹TݹÖ!ç@” ¨ºs­C‚p D�Ž¥î\ë„ÃP* êεAø7Ô ¨ºs­Cþ ¨ºs­Cþ ¨ºsm>Á’A‚@‚ � A A‚ùê¨Í‡PGmÉ ˜¡ŽZ‡ ˜¡ŽZ‡ ˜¡ŽZ‡ ˜¡ŽZ‡ Á|uÔ:Á|uÔ:Á|uÔ:Á|uÔ:Á|uÔæC,� A A‚@‚ � ˜¡ŽÚ|uÔ– ‚ùê¨u‚ùê¨u‚ùê¨u‚ùê¨u‚@̇PG­ĊPG­ĊPG­ĊPG­ĊPGm>Á’A‚@‚ � A A‚ùê¨Í‡PGmÉ ˜¡ŽZ‡ ˜¡ŽZ‡ ˜¡ŽZ‡ ˜¡ŽZ‡ Á|uÔ:Á|uÔ:Á|uÔ:Á|uÔ:Á|uÔæC,� A A‚@‚ � ˜¡ŽÚ|uÔ– ‚ùê¨u‚ùê¨u‚ùê¨u‚ùê¨u‚@̇PG­ĊPG­ĊPG­ĊPG­ĊPGm>Á’A‚@‚ � A A‚ùê¨Í‡PGmÉ ˜¡ŽZ‡ ˜¡ŽZ‡ ˜¡ŽZ‡ ˜¡ŽZ‡ Á|uÔ:Á|uÔ:Á|uÔ:Á|uÔ:Á|uÔæC,� A ÿâÈF ð,\IEND®B`‚urwid-urwid-69907e4/docs/tutorial/cmenu2.png000066400000000000000000000057501526073734300210000ustar00rootroot00000000000000‰PNG  IHDRÀÖ½ç cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñe `IDATxÚíÝ]š¢ÊPNw9ÿ¡1�û`ù³’L]ë¡«£J)K·‚ÿ�\ý;úÐ� A AøßÑ7€‹2 ãáµÑŒ&ò $�hE B HшF„@�¢ ­4� �wL•£oÈÏi£!<Ô:áºëÚ;ejj¶#ë-hg�!$�h@C B Hq¼–„@�âpM5� ÄÑÚjÃãùÛöTÖÔìý­C‚ � ü×è¶ 9wˆÒìúïÔìým•A‚@ÞP¼ ó;=¢ �õå?ŸÙzyß«‰£VÍh"OQ†WoÇ5Oö×Ë£:D+ÚhAš|•1–ë¿ÃpéöãÓrœßb¼_ÞyãäõËu#ô‡¶AiÏ:D½ÙÝ­åör¯–wYPY|}jY´âòL¾–ùà•qÊéËÝàæ ë®ÿ�ZiO:Ä8Ü^~¼|yöÎË/øùÝ7^]ŸjžmTnݾs tzðGk‰³fÄÂ]×·Ÿ±$ÛÇAÀšõ$·ý$F¼É¬vÖ W[?žòÑ,{*ÐPƒXˆ­Vü _\ØÎ8IJ@l~´ò´°§»>+ -5ˆ%ÛcùÛ°¼Ûá|:@ñÆp^àü�ÇßJE+–tˆñæßM\ÞK1½Ìqã_׸¦ÄD‡ÿ7N}y¸Ø“‡pbyyÙñÉ'-ñ*ãhm5ó!Òìý­C‚ � ˜A0‚`•A‚@‚ðîÀŽg>·j·àg D¯Ì‡ �èTåùü*�èSåùü¬&ÙRU¥wÚê„÷w]{gMMÕ:²Ñ£?›!�èP#ó!øÑŸ]?Ý)�èξÿ‚@ôfçùÞdÛ›qßZ‡ A A0‚ðî|ó$êªv[e‚ „ú{*Ï+Ãñþ»mÞþÖi€w´U < õírTc£@”añyy?=ŸÛÔŸò“ö9ÊÕÊÁ­O_ù؈�Ê^2ÂÞ§Ÿ3³ÊX‘†Ó)²þ6"Ê0Œ×³¿>ÖÏ~ÑýÉc¯ßˆ ”‡K<^¿Ü~câ÷—‡_Ø�êó!>ÚWzºryZ/ºzžímÍMº;[Üù¿åùÏ9{Ò!Þ>ãÞŸq†róºã¾ž¸pþòq†r¾üC#ÈxO\ÿî÷•Ó¹B¯-ìá:½©:¢”2Üœ:oá­[x~ÖeN'æ{8ùÛ»¿`îþþ;ñß ��øKÃG÷Öùþ.OêY[í´œû}=7‡Û¿qûzr•ÑÜSgûÇÏ®­'&7*WnP–KOÞ牷õ{ί>zo»˜:‘kYÿÚó¾½¼bÜì—Œçs�÷Ú$vülÆÔ*ão“¼¡žºù-Ù­›uïÉËÎ5mâöÜÑs®ã“Í2bÕËŒû“ußï‡Zµ_êÙ³wíõm7¾cæXƇ/=‡aþcc³�ïé�]‹eâï)Ÿ.à`»~ºsþð÷Ò5ÆMu»WñùÁƒ±<î0¼ì=ÿðþ²eòê“ן?X1ý¦-†a£ÃßãÝ׿õÍÍãÿ°gpö±økM¹§òqOã«Û3¾øq§yØ÷ãߟ¿Afúa~õ™ã™‹>~o⇹ëO†gþ7râ 2½1‚`>5 a‡UÆËíI¦Ċ ˜Ñó!8†@‚ »®ûµËQ ¢[ûå‚@ôÊ|jØf>Õíõ>™§�‡ß´Ó|v¶Ûé¦ñwx×c 4‡üú|ªÙb>_d£ùTõóó!¨fƒùÔÖþ|¾ÆÌ®kcwµë§;çné?Çáïîìûño� Dȯ ˜AMA‚@>žñjgæ¸òò¤Úûϯ2ÖÛ\~ùL€}Ÿï©vìÙ*ƒ � üÊžJÇòªÈözžñp&U>¶ÏQ®_YehO Ù¨ìÕ!ó!Ö²VøÜÁ÷ᆫŒqöGeŒ“f CžWkª¾~ã®\R—ëù¼Îgò/ÿP˶¿÷zŸÌfbÍÛöÏçÔzQߟéúóõµš¾Q¯–¸?±Q‡Xu*æóSwË\=žOÄwùùýõ¯õ©£ÇÄiûÆÇßÿtùÇú»e/îͪó!ÖzÙpÇa(¹Îxu¦×2äY‚ß83ììE^-ÿXå‰CnÐ竌q†¡¬øÀøý_ú´.Mýö2—ÏÐ?¿=K–´Óýù|ÍQy>Ä꛿ÿ=ô*E[/ÿx}Øz“@¼Xá�—½ôìyW”ó6g™®ß¹ÁsËoÃAMëó@,Zá}ö×�Ÿ,ç<Ý×›ü .ïs/6Ë*χXkÙvñÊ%®»¶^þ±V½fÛØF/;çÚÄí¢îõ2Î×ÿ �$bÁN²CæC¬5û2£Üü»úæ¾üÍ×�P‹žUK–?Þm࿪7và�¯ �e¬|鯪&ë܃ùðÃOnëìòöò~ÜõÓ�Ûþ~¶Æ¸»ÔݺãU})Êt]ÆÙ>ûÊ’åêàQåðw¹ÿZn¿<­ïW5w‹¹>‘J.þõí™_~ãöýø÷þo�)ñÿ‰#�¯ëW—ž¿ÀÔã<¿üµ·ï›xƒLȯ ˜AMAÂþÛÍìða‰�çCð]ΫŒµ{ezÙ‹ó-ªÝß¶!A‚@ìºîWÏó!ØÞ>G¹‚ ½êb>݈N5?‚ï }Úí�tAˆ.í÷N[� A z´ãg3‚ êb>ßA ú³ë§;‚ ÝÙ÷ãßAˆÞ˜A0‚š‚ � ˜A0¢æCp � A v]÷Ë|n™AÑ+ó!¨A :e>UDŸÌ‡ ŽëŽ©…‡Ó.o�ùTrîžø àCôÉ|jˆ™A5Ñó!¨G ºc> DovžñîGùŠ}›UU»¿u‚@‚ ó!ïvó!ê2‚cA A¸�±v/8ÇÚ{>„<ôÅ|*¸Bƒè‹ùÔp „Ñó!¨â2ëZƒèŠùÔñ ¢/æCP‰@N�°Æè‹ùÔòo4ˆÞ˜A5ÿ ¢7æCPÏ? ¢7æCPÑ? ¢3æC0˜ÁQ‚ � ˜A0¢æCp � A æCôË|n™AæCôÊ|j0¢SæCP…ù}2‚:þvL½Ñ î<�ùTò¿7¯gƒâK™Ñ#ó!¨Å|ˆ™A5æCôÇ|ê1¢;æCP‘ù½it>_Ê*ƒ � A A‚@‚ „OæC¨¿°þ`>„úk« Âûó!Ô_Yë„·çC¨¿³Ö!ï·Pi­CþÑL@Õ×:A ï͇Pm­CÞš¡þÞZ‡ ¼3BýŵAxc>„ú›k‚°~>„ú«kó!V� A A‚@‚ � ˜¡ŽÚ|uÔVó!ÔQëó!ÔQëó!ÔQëó!ÔQë� ˜¡ŽZ‡ ˜¡ŽZ‡ ˜¡ŽZ‡ ˜¡ŽZ‡ ˜¡ŽÚ|‚UA A‚@‚ � A æC¨£6BµUÁ|uÔ:Á|uÔ:Á|uÔ:Á|uÔ:A æC¨£Ö!æC¨£Ö!æC¨£Ö!æC¨£Ö!æC¨£6‚`•A‚@‚ � A A‚ùê¨Í‡PGm•A0BµA0BµA0BµA0BµA‚ùê¨u‚ùê¨u‚ùê¨u‚ùê¨u‚ùê¨Í‡ Xe‚ � üÙìº CúãšIEND®B`‚urwid-urwid-69907e4/docs/tutorial/cmenu3.png000066400000000000000000000073031526073734300207750ustar00rootroot00000000000000‰PNG  IHDRÀÖ½ç cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñe;IDATxÚí�[z´*D=ùþY0ÿ¡1�óà¥-ED®k=$Ù¤[©Þ jñŸ�~ü•þP‚A€ð¯ô€ 3M¶xL†¨†*ô€ @AµPG‚@  ˆJ¨$A PDÔ’ (ëÄ”)ýA†£Ž„pŠÉ ü¦®¹S&'Õfd2D ÔÓc PDT” (¢<5% ‚(NU A€‚ JSW‚˜þ³ëËÌTæ¤ÚãM†A€€ @@ üWéØ ±fSíú>©öxÓe€€ @@ ÄÚnÂ쓳 Ì´´õöÇ;R××/U\ՠ˨†*ôp!3=Ý(nmÈ—ý¹>¨2D-Ô‘ (γ k~?§iËöö2´÷#Æc}kÖ¹½ù BƒV’ ®2„™N­w;ÝjöåžêÛ*2ÞÛC.¼æ!¶oò/ÔÆ3všÌü˃%ì6Û¾GjIÂNûòvûuõÖòÿ?¼ð´=dãjP™:}k47¾¥—X©&AxN]ÓnÃà#ˆôr@`Õr!ˆ}>‘‹‘0@¸¥žóËHÝžè£Z˜©¬€ŠDˆ Ruüž'Œ3Šà'ˆäW+çÊ.§>ÇCM Âg aÍ2°›� @@ R‰?Œ‚h�OŸîD ˆæøöño‚h��ý!¸É¶5ì·1‚A€€?±þøIä%Ûñ¦ËA€€ @È?S¹v†öøê«áíXËH*AÐ ùùäªF"A˜É{]Þ·ë¹¹veH¾¹ÊUËÅ­·í‹>Á ²UŠøC0û47]F€æ%²–A„™&û[ýõ_½ÑqñØß RÀœJœ·7ûïoNoØÙý!^Í•Î›ËØks]í-ä#V‹[ÿ4×ÿ‡•‹ ½âÞ‚�¦ÉìÎ;ޱ£°¾¹�¦É¬åO‰@åíØþð~f^+ô—ÂNÛ´FVcÌ´[:ÏóÓy®ÏêǼ0ßiñ·Ø7°Óaÿ[ø/€³ 5¼:Zëñ6ñ-©&-ïÞ¯åä°ßÇô±³Ë¨î«“¾ý˜ÚºÀ9¨ Pš-'óÅK}/ÀzöÑzšø×B®&ü ôå±ÝΓ½‰]×o5I|øl†«ËX†äåÔäŸä³lÖ<§�!ibÿ nè;×ðbóEü!‚N3Ž‹u硂楮¾½¡Û3nŒáæZÆËSÏiºlì¶}ç•©Eã(‡y[Aa>}ºóþò·o�±‹ö³Š×¬9On³£ë?�e�ssçö÷+Ü7mÁ4%ºüm¿—þf×þ§™ÁÛ¶XR“ÎTžgŸ>�}øw£zøöñï÷7ȸ›ùé™ã›¢ç× l§»í�â¹G˜á™ÖÀü! '„ºŒÇñ$T ü!@Àº S×ãàuUƒ 1 ~W¹bð‡€ÒøC@õøÞ'óï¦��ü! 2¼o¤ûçÞxBƒrÄ7É�(7]ûC@6œ‚øäÁoT–“茜Ȫ¦;ÈF¨�üþÐ 7SרîtBÐÓ�÷·ÈÃÁåïî {üA€€ zð‡€7 ü!@Èå�ŸD^¢�7]¨3•ÜüsA)AÐ ù©Ù±" |ŒßU®Q» ÒÓ *G!‰?DÄ›BÛ$ì2Ô`æ…±krÙé>þ½ð´¤—#¶§EºÎ+Â]Öß¾÷É$Ëoæ¦Ía�¼‹X—[Ûÿÿ!þEîùTÿH$ʯߖçÛ-é爷ub]Ëùâ94’c¶"û²>õ÷À+ˆˆ7›‚ä`¦ÉjŸq\µÏï—â}*ïà¶ÈSýãð¾Ë0fz·è«·xí)©ïËœÊëƒFןϧþÖyéJ�¯ÒÓ¦®’"ðAqó[€×s;ýÂÚuÌiÝqÌÜÕ?)r�xPü]J¶ëòÝÖ'Ù¥„õç¥?D(ßûI¸Ö6wqêúÇ!Ñ<„�O½â2µ/Ïì¾^ü¯—ì0%ò‡%è4C–wú¶‡~[×ò¾ÝظÙÀEÂk/O=e²Óë æéŸo>ûmý­ôtgÚËß>Š8L(ÛyT¿õ)Þ뎭¹�+“¹ëSÿH¹üm�¿íþ×e|ìjÕü”ÕêŸ?Ï}ý�öøwþd¬ëï°G”Ç“·\í|_ÿHpƒLïàþð‚A€€?øCô þ�‚A€ÀÔõ8Ôìùñ»Ê… @@£PÄšA Bvè1Þ7Ò!Äøßi‹ @@ ˆx6A€€  ˆ?ô‚蟠§;¢{ÂÿF ˆÞÁü!à ‚ð‡èü! ÂÈ3•ûIÛa];RŸ £¡×Í$ -”Þ¹¢x]Õ¨O)vµ¶úªÀï*W}‚Ø–wN×$5õ×þ‘0¨…&ü!˜­zOâcX°ËÙ“ýZfùkùmv%|¿‘²K V"©ï¸„—Y{­¡§ñë]«÷‡™[5ûFZÅð¢=Žkþy•?-w~© / Ê1ßg³®½leÕ¼EÚ0‡µ:uEÙs}f}ÑÈ‹/¢M…ߺÊÞ7Ò•ÄÝŠ×{ýÓÁ¯Ž 7<®Åö²175–'jùõ ~‚ð<>i&ŠBjÙöÔMñ†&ÿò»Q‡vŸæ6Møßi[¤Ëx}0mØ¢®Æû›¦ÿZ‘õ˜=uxŽ Ž{\E÷$ΔXÈ5A‡· 0aš¦ÇaYÀ³%º ¿qña‹óþ£ˆ•àaÙ …N;SŽ‹Çc€Ý„?„ÿRëöÔ?:'Šüß9pRiW¾Vý&\·¾àµ °sã-rf=ÑÞïØ�Ç1èéβ—¿ýa�´ÏrÊiw˵ÄE}ÛÆ¿ù Ç�”ØÕ·•¯2A$þPM\þÞ¾»kZ:ãvT¬TzYâU©‡gÂÿ®ï™{º' ­oM»×îÚÙÞÖѨ4|h"CÀ ð‡x‚A€€ @Èîñ4h˃ò¶=cÏ2BïGð/¯ èøŒÿS¢ï¡ËA€€ @èu¦ÒzGõé¬áÙ "pÖû#F~E¨?DzÈNÊ«¦Uˆí^”W-EÉoP9h‚†&ü!PØ{íËÈŸ Bªû9C¬áöôµ�o­4¿;êÔèA O®�ãwBU½?DÈ�ãÖu³ÛîéëçªäQm{¨¶}îÉš!2'»Ôé]ØlwZo/þn‹6“�Ÿö´Û‡{"uóŠï¡gÙµ‡£éݾ%2Äü A�뉙ÜOëø?Ñ¿+ú“E/š˜ŸTI²7K†È˜ ‚’òÁò€ŽÝ¥ˆN‰xJ•ûCoÌN2ƒ�È ¡�aH $Ò6`�rHüôwÎ!¥™“[¹4±��v$ŒÃiù‘Êý!üÆÅ‡-R‚ž–Ýðo©+ó¤Tñ4ÑÉaùgýþþg�_�vÔc$4.ý›Š]Ö2»±ÌH¦<õíæÞÇãØ�?„Ÿ"Œ j.™¡6×Õ‰/­Ý 6Û"ñGþkáº÷öH³óf÷S_ë’>ü!ŒwhôǹmÏjrìXAü5� à Áþ�þÐøCÀ„úÎ2³%»?ÔMl—:{žp¶<ˆ>ÞŒ!@@ õùCÀWxµï_`yh¿ö¥ËˆQhªc ¢oª÷‡€:YA‚蛪ý! bâý!�b¡$þíK†!öŽ)¾ø�2g†”}оt üM ¢wšð‡€:)è™j_2MøCÀÂÚ— þ½ؾ¹åKd¼ ž°²/¤A€€ @@ àB® �?D^ð‡€4 „7þ\i‹¯ý!ÐC[àÄûC� Úˆ!Ú‚ÑøC@±þ$ˆ¶ÀâXA‚èü! Bœ?=F[à±DùC� Úˆ&Æ‚ÑøC@<þ$ˆ¶À^îA‚h ü!`ÂR� @@ ð‡ˆ>ÁÒ€ @@ ð‡ü!`þþ£€?Ä€?Ä àQà1øC@ËÄT†Áå΂à‘ü{_… (ˆÀbÁbð‡€hð‡èü! ü!ºxþ½S©?4]‚A€€ @@ „œþÄ Äý!ˆ[ˆé2@ÈçAÜDL†!›?q1„\þÄ�ÄdAT#PâÂ1Bâfb2Yü!ˆÛ‰É äð‡ n(&C€�Á‚¸¥˜ Â÷þÄMÅøC€@—‚A€€ @@ ‚b‰ñ‡ –˜.ü!ˆ%&C€€?±Ädð‡ –˜ þÄ“!@@ àA,1ü!ˆ%&C€€?±Ädð‡ –˜ þÄã]‚áÚ¾å^„¤ËIEND®B`‚urwid-urwid-69907e4/docs/tutorial/cmenu4.png000066400000000000000000000072341526073734300210010ustar00rootroot00000000000000‰PNG  IHDRÀÖ½ç cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñeIDATxÚíÝm–«(…aú®šóóè~Ä­¨ }ŸµºëZeLbvŽFãá¿à€Ÿ­l!‚@@ˆ¿Ö3ï\h>M…0ÃD�°ÂF� PÂ#‚@@¬5˜ò­ÈçØ(›i*ÄïÐ5ß”©ÉlE¦BX`g‹A  „† �€"íY*Š@4gª@(Ñš­áþ Ó¯9RY“ÙõM…€ ‚@@ügtß�L›=AÿNf×7› A ®¶ð| ó�¶�ðn|­çÜSzyïe⬛ 3Läa'Þ�}Q<„œ7ûùò` ‚@@E?eÿû¿ssµ»“áx�q½¼i!z{ÿÛ ýÐ>¨‘±W!¼Û¼z‡‡[ýr¾³åÍ òÉ·G-IÇ!æwòoR_<œóÃ�c5XÜ ïöod¥@ìTˆà–ó‡ùÇÞ{XçOøûêg·G5{;•¥Ë·n�†?°•˜˜)‰‡®yÝ>#%åã@ÀÌÚ Ä²žÈɈ‹ØA8dg‹‘¸É(ýz’³8Ri€¡‘ˆRþÄìg4‘ˆâg+‡…íúüV,ˆ”}ˆàÇËÕçáÅ…0-ðøÏá[©°"¥B„Åÿ‹˜¿K_f(|wÆ™*‘ ¶ÿ ±›Ùv^ÂÈòtÞ°3ãw"a Ÿ2Z³U èшÙõM…€ ‚@@Ђþl2 �€ W† =úC`©f|�èý!P�èTåþø*ѧÊý!ðY7:Ù¢ªJß´¥B@\?tÍ7kjªV‘©=zðÚ A :d¤?¾€@ôçÑ«; �èγ—Ñ›‡ûCð%ÛÞ„g§©‚@@úC@\íA?‰ºª­o6‚@@Ô?R9m Ãú··vo¿5 ðƒJ‚¤¾GÎj „wÉãòÞÏ-öT>陳\VNnÝ}}ÉG!ìTöªIŽ>}ÎÁ&## ÃYãN„w.üFÝNïÝÑzðØß/d¿™c{{¿üEäþýæ»S½?Ä­c¥Ã�ýîtÒÍu´·œ‡´-nú§ßÿ;&;âòˆ{£àœó‹ÏëéÈÌzçÁ9ç§ù7…@ã¹ýêþü0V评mnÓ›ªý!¼÷n1t^â£KŸ5Í00ßfð·«wÜêùlà¿ Û@Œi¸µ¶¦õíw¦•:hyt=‡ås,?Ýd˜{ë”ý8´µ#ºS™¹Céçšü̯ôw¦O½—‰GÄrõùo '×íü‰±Ø�„iìñ^‹Äƒ×fÄ6ã.¹¡šZü‘zóèí�OVÄ¿´ç �þ«ŸãöfñúoŽ ¾ciÒ#•Û#�g�'œü¹Ó<<{ù÷ý/ÈÄ_æ³kŽfÝþ.òwtûhxŽï¾ ÓúC@Ð5ˆ6§û“0Œ AúC@°É€ ‚@@pèú;’ÎjP!>#í,�€ _A\Q¦?ÌKýžÌßÁðAõ‡€1É_¤û‹ß؇�Úâ™âÀ¨¶W÷‡@5Ñ@Ï·Ò/2=�Îo5=Lz©1ó,ËyS–ÿ·úC\¸3—ï\ÐmÆzÔ¾Øôr(Þ³ù#g9[þwÜßdxïî úšÞ°)êËy6óë…Fû�/eù½»Ù"Wƒ·ÒÙ3,½üÏ(ˆÌ ÅýoÞÄÛé6Lûœ!>}å -ÿSJ äzáBñ{%9LÃw‡øt‘§TpyÍÝì‘ëù~±q�ýÑtéåG¡ãaø˜˜´Ç�Ðpó“ÝÓƒÿ½¥:¸Bý!re}Ì�áÝÏÞí¹ïÖiþÔÍØw«ALÁs7?zÊÁÎè´ÁÜüñÎc?\~ï²®î,{ú;%«ÊaØ«_þölzžñéà�•ɱ†�²ü/irú;¬†å�Ýéõ¦fµ˜_� ºøóÇs¼üÎå]þ]ÿ 2!öï¼K”O÷'gˆ½ÎÇËÿ¾ óvô‡€ ?î ‚@@Ђþl2 �€ ‚@@A �€ Âêw*99ZÛø=*„Õ ñí‹#ê›+2‚@@A �€ ‚@@A ý! ¬ö‡ ŸD]óúf“A �€ ‚@@ˆ¶�àè“9 ¯Ü" 5 q°©Q æñ`L‹@�¡ˆÆ~�H¬á·_GŠƒiM6ÄÁ®)U_#O$Ìj1�«O¨µµØd7d‚HÔèc'eªf‡®ß2ÔúÛ4¿Z죦 Q¹@ä_®¯fpÎùq_3é¦Á9ççùýxñXˆ.ož_oÿ»AÚýéü~±È0Ü×t‡ËûŠ=>-$kQ!¼÷.ïJ�°øÿ%ÊËÉp>¿NçÜ}dþõ"Óoþ‹E…LŒ�¨X Æ4\_œÿ­Ü¤töØcËó±Û'Þa|þ^ö‡;éjİõ‘¦â˜&�0~áw˜výÖ»ùO°¤ªäþ¦»¬¶KÙÁ…ßaú4`â1Ο@«ƒþq9™{óÒ§–ε8(†��6ÏNŒ±õh£ÃþÓ»wS6æò’owþTõŽC4:­uç£çÊéº^ñÄU9œ8þ*ýØhæü ‹ó‘ß= íéïk‰^Î?Ÿ Ø�ÿpõ­–·ùRÒÙísïoEŽP‡ýÅ-Ÿàü»K+ðÈ?Ãç½wÉÝ„c€c)Ú­H±åÉ�ÍõÉ”³Ç—9ÒÓ ‘ß=à¿p-¹Ÿžž÷Ìëû_‡úÛ¼ÚgÓx5®Ë€ ‚@@ЂþpŽþØC �€°ßU-*y› ¬tÐ5Q! æ@P à+S (pÎQ!°2˜ºP OGRWúB…€¸zåoü—*»”±É€hÔVQ! ö‡€ETˆ.ûCà9TúC@ЂM�€ Â|ˆ³ïá\üÞFëõW«“ís}*5ÖómÕ¢O%°@ DƒÁç©3žáKÝéaç,H½!©^oˆnó0�n@$î{É&ƒS²¥\ïa©@øš÷÷n}}Êà¿ïdΛ û"ìþzvUÇÝ Ž=͈ãaÝìC\/Ü‘‡ûßS Œˆ Ÿ+ÉCÜðÊìF¢‡ ¦'’{#òç‡HD×ÏÅþ ĵƒNäáÐ~™è¡B\xeÉé�/tŽA>`§‚�0¼Å8Üà?cR´k·¬‡ q²_Œ\G»e—úCÔ?(å½s.DïžïLªÈ3#¥¿Á°w‡°êæÐuÞ \žD9X™®‡q¶Øà·»ž8Ä¿³º©ûÏekB‰ˆ8©´úC˜9ï=ß¡_þ�gN"rõð)ãœ�Nñ©ä‚üþæ JÊïA?‰Wëk§�#¢ÿOì³e¾?ꪵÉÈ;‘??î™×7û‚@@ˆwô‡@1‹ AÀ&+/è�’¨s (pŽ �•)8ç¨X©×"ñô%u¥-*D­oLñÆïÄP!إĈMD'ý!P ⟣@`� ñ†þ(ˆ A«—ò厌{Ùq‚@@«;•hÄj… ?D]ô‡@�€ ¢f΂t bòÐ6õúCP º@…€¨Ö‚Ñ*D­þˆNP! Æ@´.+¨¢N¶Ý B@TéA�è¢F DG¨úCP zB…€x¾?¢+V/å£?D]ô‡@�€ ÂêN%±Z!èQý!G �€ ?ý! Ød@Ђ A*ý! ¨ã�©Ö!R 8ÝÙâïþ"ÁE#ô‡€`“A*ý! ¨ô‡€ B@ЂKù Ød@A þ`üè2s–IEND®B`‚urwid-urwid-69907e4/docs/tutorial/highcolors.py000066400000000000000000000017721526073734300216140ustar00rootroot00000000000000from __future__ import annotations import urwid def exit_on_q(key: str) -> None: if key in {"q", "Q"}: raise urwid.ExitMainLoop() palette = [ ("banner", "", "", "", "#ffa", "#60d"), ("streak", "", "", "", "g50", "#60a"), ("inside", "", "", "", "g38", "#808"), ("outside", "", "", "", "g27", "#a06"), ("bg", "", "", "", "g7", "#d06"), ] placeholder = urwid.SolidFill() loop = urwid.MainLoop(placeholder, palette, unhandled_input=exit_on_q) loop.screen.set_terminal_properties(colors=256) loop.widget = urwid.AttrMap(placeholder, "bg") loop.widget.original_widget = urwid.Filler(urwid.Pile([])) div = urwid.Divider() outside = urwid.AttrMap(div, "outside") inside = urwid.AttrMap(div, "inside") txt = urwid.Text(("banner", " Hello World "), align="center") streak = urwid.AttrMap(txt, "streak") pile = loop.widget.base_widget # .base_widget skips the decorations for item in (outside, inside, streak, inside, outside): pile.contents.append((item, pile.options())) loop.run() urwid-urwid-69907e4/docs/tutorial/highcolors.py.xdotool000066400000000000000000000000511526073734300232700ustar00rootroot00000000000000windowsize --usehints $TERMWINDOWID 26 9 urwid-urwid-69907e4/docs/tutorial/highcolors1.png000066400000000000000000000012431526073734300220220ustar00rootroot00000000000000‰PNG  IHDR  ¨º² cHRMz&€„ú€èu0ê`:˜pœºQ<PLTE×_¯_‡‡_¯_×ÿÿ¯ÿÿÿNÞè‹bKGDaf¸} pHYs‡‡�åñeûIDATxÚíÜ‹Më0†QV`…»+ÔÄû¯BÚæa—WApýΑP…ã MUêÃpX�#(� t‚Ò J'(� t‚Ò J'(� t‚Ò J'(� t‚Ò/èßÁJ'(� t‚Ò J'(� t‚Ò J'(� t‚Ò J'(� tÇ zú¢ÓòÙu $(,¨ÔZ§æñ^ÝüRÇ]þv³ô�‚®ƒÍŠÓƒšß>ÔÍ40èºÖwZý\Ð|Y¿Tº»Ã<¿L—Cëø None: super().__init__("", on_press=callback) self._w = urwid.AttrMap( urwid.SelectableIcon([" \N{BULLET} ", caption], 2), None, "selected", ) class SubMenu(urwid.WidgetWrap[MenuButton]): def __init__( self, caption: str | tuple[Hashable, str], choices: Iterable[urwid.Widget], ) -> None: super().__init__(MenuButton([caption, "\N{HORIZONTAL ELLIPSIS}"], self.open_menu)) line = urwid.Divider("\N{LOWER ONE QUARTER BLOCK}") listbox = urwid.ListBox( urwid.SimpleFocusListWalker( [ urwid.AttrMap(urwid.Text(["\n ", caption]), "heading"), urwid.AttrMap(line, "line"), urwid.Divider(), *choices, urwid.Divider(), ] ) ) self.menu = urwid.AttrMap(listbox, "options") def open_menu(self, button: MenuButton) -> None: top.open_box(self.menu) class Choice(urwid.WidgetWrap[MenuButton]): def __init__( self, caption: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]], ) -> None: super().__init__(MenuButton(caption, self.item_chosen)) self.caption = caption def item_chosen(self, button: MenuButton) -> None: response = urwid.Text([" You chose ", self.caption, "\n"]) done = MenuButton("Ok", exit_program) response_box = urwid.Filler(urwid.Pile([response, done])) top.open_box(urwid.AttrMap(response_box, "options")) def exit_program(key: MenuButton) -> typing.NoReturn: raise urwid.ExitMainLoop() menu_top = SubMenu( "Main Menu", [ SubMenu( "Applications", [ SubMenu( "Accessories", [ Choice("Text Editor"), Choice("Terminal"), ], ) ], ), SubMenu( "System", [ SubMenu("Preferences", [Choice("Appearance")]), Choice("Lock Screen"), ], ), ], ) palette = [ (None, "light gray", "black"), ("heading", "black", "light gray"), ("line", "black", "light gray"), ("options", "dark gray", "black"), ("focus heading", "white", "dark red"), ("focus line", "black", "dark red"), ("focus options", "black", "light gray"), ("selected", "white", "dark blue"), ] focus_map = {"heading": "focus heading", "options": "focus options", "line": "focus line"} class HorizontalBoxes(urwid.Columns): def __init__(self) -> None: super().__init__([], dividechars=1) def open_box(self, box: urwid.Widget) -> None: if self.contents: del self.contents[self.focus_position + 1 :] self.contents.append( ( urwid.AttrMap(box, "options", focus_map), self.options(urwid.GIVEN, 24), ) ) self.focus_position = len(self.contents) - 1 top = HorizontalBoxes() top.open_box(menu_top.menu) urwid.MainLoop(urwid.Filler(top, "middle", 10), palette).run() urwid-urwid-69907e4/docs/tutorial/hmenu.py.xdotool000066400000000000000000000002251526073734300222460ustar00rootroot00000000000000windowsize --usehints $TERMWINDOWID 80 12 key --window $TERMWINDOWID Return key --window $TERMWINDOWID Return Down key --window $TERMWINDOWID Return urwid-urwid-69907e4/docs/tutorial/hmenu1.png000066400000000000000000000046461526073734300210070ustar00rootroot00000000000000‰PNG  IHDR€5Ò cHRMz&€„ú€èu0ê`:˜pœºQ<PLTEÍÿÿÿåååîÿÿÿ‰+£»bKGDøoéÇ pHYs‡‡�åñe IDATxÚíÜinâÌFa$îèPbH½�O(û_Ó�‡2Ûm3¤O:ï9-B{ ?\�<¢ìvfffffffffffffffffffffffffffffffffffffff†wøÇ£·Ÿ½ H€áÑ€ H€áÑ€ H€áÑ€ H€áÑ€ H€áÑ€ H€áÑ€ H€áÑ€ H€áÑ€ H€áÑ€ H€áÑ€ H€áÑ€ H€áÑ€ H€áÑ€ H€áÑ€ H€áÑ€ H€áÑ€ H€áÑ€ H€áÑ€Þ†1þUheü¹Mµ‡Rngͪœ¬'@{<ÚVi_km]O€öÇhh4´)¬_‡_íY_)Íi^£¬eVŽíißt½C·B]Þž+zûÙ‹Í<–V¯�tXÆ;¼a½C¿^].@{¤e€‡ ©îçã]àê±½®Ëh�4x8ë�—î½x kX¯¾Æøh[›X6ìÖ«'~´gZØž о¶E€­¿-Kw"@{¦€ý9]Y;×õhÏ· ð¸`?O€öL4´?,Ö+æ¾_´­-žõ®lŒÚö6WÁs÷ëú«åºüósGÚÖ~Â:nx¨Ï‚ë=Áâ9 mî¯I ÍE`x4 †G`x4 †G`x4 †G`x4 †G`x4 †G`x4 †G`x4 †G`x4 †G`x4 †G`x4 †G`x4 †G`x4 †G`x4 †G`x4 †G`x4 š™™™™™™™™™ýð~ÿãÑÛÏ^Œ$Àðh@ �$Àðh@ �$Àðh@ �$Àðh@ �$Àðh@ �$Àðh@ �$Àðh@ �$Àðh@ �$Àðh@ �$Àðh@ �$Àðh@ �$Àðh@ �$Àðh@ �$Àðh@ �$Àðh@ �$Àð>îZšÿ]£·Ÿ½Øý€þ¯{û�†%À�hh44Ú³/¥”SýïÚª´¥6œ$@{KÒîZ8Ì -÷€ }©;þ”hh4´u€—ÒR*§K+®¾W€ÍÕÈ Ôîꤙ=ü§ÿ=·Ÿ¿”ÁLh`f`é$Õ÷ À~þ¥ƒ7X?êg ¿G€ÉM¶{¦9€¹ú~Üì×. ¸ºäî*ørýü¥ƒyj×`vS€ížiðÊÞ2À±Ù?ì_o:E¤·Ÿ½Øv€§+¢Ï-Àñ ÝÀñçëP€ö~€—ádpà;Nø¯· °A3†·ðR¶ô"Äš¦ï.B*¨­KÙv®÷k˜ÝàÝm˜‡ž¶|Ëi ½ýìÅf6]v{´ÓW\{–"À€Öžºí^pôZº Soլ݆é×ýœÝ<`j�<ÝÜ,·ÀºÑí�æò1:À.Üì?"ÀìÖö€ËÌ£¸îFJ©ÏÜF�Þ.õYÛénùÇÇÍm˜îzE€É­ìN¿Œðq ðúe„SýÐ ÀáË×Û0Œo àÐ/Þ½ýìÅhh44š m3Ào½ýìÅ–”†%À�è?0é¨ �$Àðh@ �$Àðh@ �$Àð6 1­L€?·Õ>/­uþ.éíg/¶>ÂûÏ—í‹Zà–ÙY€öEmøàÚæ¶ìö„Ý�æ¨|n?½ï 4ÓÍ¿½í±fÇô`�ª=Ü÷C>ؽ7Ó´‡›éy7v4LU€�²s·dúùk>§v{à¨Lo?{±ù!�™l‘íG§~Àö%@{ªU€ç~ºC6:&÷ÚºÃð^€öT«[‚7w×]ŸíÅ6ìwz»ýpÑ»l =×tHÏ»ÉÅì ÀfÌ÷´÷4;¦“9û«»ßu�8>ÿö"ÄžkÛ(wàvwÓ´—[àóÀë[ ðoC¤·Ÿ½Ø*ÀÝpLîöuçë!¸›î¾.#@{ªu€ÃZÜ0}}'@{²Õ>+]/‚‡%û¢í©¶õ÷øú•X4´G†šÆ&ÀØ#½§± ðöÀHÓÖø£ 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ã 0<�ÍÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌÌØþÉ&ôíÈßÍ`IEND®B`‚urwid-urwid-69907e4/docs/tutorial/hmenu2.png000066400000000000000000000060441526073734300210020ustar00rootroot00000000000000‰PNG  IHDR€5Ò cHRMz&€„ú€èu0ê`:˜pœºQ<PLTEåååÍÿÿÿîÿÿÿÉåÍbKGDaf¸} pHYs‡‡�åñe |IDATxÚíÝ]n£ÊEa,Þ“)Ä30À‘âøÅóŸÊ1T¿&87À¦î^KJ“@9êî|ÂPؤ(ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆHÞgæ¥ÇWæ©€ž©€YÀÌS ³€™§@f3O €Ì:fž˜uÌ<5 0ë˜yj@`Ö0óÔ€À¬`æ©€YÀÌS ³€™§@f3O €Ì:fž˜uÌ<5 0ë˜yj@`Ö0óÔ€À¬`æ©€YÀÌS —ú8²ÒßÀ´½96<}îUúw¼‚ÔPª.çF\Z&€õÙÈj8;©W·ŸÄï3|ü¹Z Àßô੨aÕðjHE8ÝŽûŒãÒö삤´œŒëÏÞ`zü%®h¿×hàg‚”ösŸý]`ê©9NÛ�0�KËñYp½_;×àÒ–ÑYð¹{ü9À¼4c¸ÀÏÓ)M¼„e8„ÕŽK}|‡ø„Ò‡7°oö€ñc£CDµƒ#,ÞÆ¥¿Ã¼tˆž!ÀþÝ+€ýǧ= ÷ø‘6›<·ƒ ·8àÖG»y`ÓñÖhúð–ž«÷r²n/Æcºbé)8�«;"À å]€UõÞSpš¯à:Í<½ 0®Ëàå]€›ªpm€a�vÙàÒµ®°x`:cîáû8ÀKø£Ù ö>æ¦aÒTÍÒ4 ÷Ø^ë]Xoè,ð2˜¬†ÀÂDt3Ñ\}õž`gæy ÞàS_ñê$c<î3~›´ýù¸ÓÆ ÀÕ‹Kqa"¥J×Üz—ÞÎéZÛe´ýëk0 ÃYðzÍ|þhOoŒ¯Zh·ÅŽÛÝ×ôÅ_C€Ý‹.éA€í‹ºi˜õý0׿¶mxÜ@0÷Ô€À¬`æ©€Y·0“Ô@ÏÔ€À¬`æ©€YÀÌS ³€™§@f3O €Ì:fž˜uÌ<5 0ë˜yj@`Ö0óÔ€À¬`æ©€YÀÌS ³€™§@f‰ˆˆˆˆˆˆˆˆˆh—ÔâkM¨S¦©Ð<5 š§@óÔ€hžÍS yj@4O €æ©Ð<5 š§@óÔ€hžÍS yj@4O €æ©Ð<5 š§@óÔ€hžÍS yj@4O €æ©Ð<5 š§@óÔ€hžÍS yj@4ï6jnýQSÿÿÑÿ@ÿ ‹Õ°hIIÍ*�Ç=}º4t‡¿Ï¿+Jz (C€@Q­´ñú¸zX»ú(«ê À[8Ó.îÚv¸i{þ_å( €TV>J�{ÙˆK˰> #ÔpvR¯n?‰ßgøøò± øW-ü®âA\‚Ø-«°=|=]ÞêßqØd<×î1Üu%€� )-'ãú2À›L�¿Çí÷Y¥k'/VPy�YŸ÷Ð�àÊ�÷L-À@.-ÇgÁõ~­|®)Ó–ÑYpÙ=¾ 0ïÍØ•šlØL~ט®ÃeØÃ=¿¾õ—Oc×°§»Ý®·t<ÀµPúðæv)Å<Àø±ò!â`³‡ê¬á\»É”þ2}Œ×G€×þ–ñx®ÝÀ{‡(àìï6_ì?>íWìm€‰Ó`Ù<Å^oi—ÖÇ]f» }5€k÷W€e{0XŒ·®y¸ÀöÜ¢…ö3À5Ø5X£éÃ[X>Þ¸ùIÈ`zÚœ¬âYÅèäcàx<Wnt’@½ ðñxï)8Í׬Ôàè$ä» bfÞâ".¶ã¸rÃÓ¯Þ߸êaààhfàµ7äÚ-| Žà–…=Ú} €K×R~Ó €uÀ4uòÀÁº7Þ¸CñòE˜~é}ÌM䩚¥i˜8ö¹º¾6òç¿æ2Àë-ž<ô9 §Sú{Ó0€£ñõu’ë S2êóq¼æ#›áDt3Ñü(zO°3ó€ñ!ûœNDßê½cü´]ÎŒàÆÅ À�—âÂDÊ#]së]z+Óµ¶ûh{1œ† ç+»üŽç ÃKii¯õ†gÜÉ¥¸[ú²=ûíÆpãÚÝ×ôÅÅ`÷b„{zÐ`ûb„n¦Ø`ûç‹#DˆéËøˆöÅ€ÃñÜ« NÖl àÑSÿÿ?P( €”@Ò|$is?P5,š¤¾Á$7¨4O €æ©Ð<5 š§@óÔ€¸Yë¾{m«Ô€¸UåÜ»×ö½ùÐRj@ܪæõ{¯6€;Þ_4»å0©p£æ™€;TößyÿÜÿ/ž¥Ô÷ÐZùv/¿J €ë5º3Ç=®½n©QöÞ ’î@®ÐðÖíWý»Z•aKpoÅ\çÍEÿkj@\­Ñ´Kú²»±dÜÐl>À•šÜ«}ÿdùâýæEû^Mp•&Ïåà£ÛõàÚM¯|¤w¤·'½ý›ÁÄ7ž€+õâ×' ¦ûú€Û4½ô›¦[îáËrð×€[Á=F_wŸ€ÕLùý°[4UÕ€¸Qe{?Êô›‘º§àðux¹ ¸IíMïû¿š«=ý¸é†�à6õ~á=,º;£öîv@nܱ^~5N €›@J;ö{“Ô€¸u«þzéõSàÖ{@Ò¦@óÔ€hžÍS yj@4O €æ©Ð<5 š§@óÔ€hžÍS yj@4O €æ©Ð<5 š§@óÔ€hžÍS yj@4O €æ©Ð<5 š§@óÔ€hžÍS yj@4O €æ©Ð<5 š§@óÔ€hžÍS yj@4O €æ©Ð<5 š§@óÔ€hžÍS yj@4O €æ©Ð<5 š§@óÔ€hžÍS yj@4O €æ©Ð<5 š§@óÔ€hžÍS yj@4O €æ©Ð<5 š§@óÔ€hžÍS yj@4O €æ©Ð<5 š§@óÔ€hžÍS yj@4O €æ©Ð<5 š§@óÔ€hžÍS yj@4O €æ©Ð<5 š§@óÔ€hžÍS yj@4O €æ©Ð<5 š§@óÔ€hžÍS yj@4O €æ©Ð<5 š§@óÔ€hžÍS yj@4O €æ©Ð<5 š§@óÔ€hžÍS yj@4O €æ©Ð<5 š§@óÔ€hžÍS yj@4O €æ©Ð<5 š§@óÔ€HDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD¹öyÌ#§¿3IEND®B`‚urwid-urwid-69907e4/docs/tutorial/hmenu3.png000066400000000000000000000101561526073734300210020ustar00rootroot00000000000000‰PNG  IHDR€5Ò cHRMz&€„ú€èu0ê`:˜pœºQ<PLTEåååÍÿÿÿîÿÿÿ^:qobKGDaf¸} pHYs‡‡�åñeÆIDATxÚíÝ[R㺆a»*ûÞL�ž©¾ÏE°«(æ?•…£ƒ%£–ýÙüï» ‰Ã’ÐÓIì@h"""""""""""""""""""""""""""""""""""""""’÷vò~Û<ÞOOOÚo›‡ N=ŽZóP`á©ÇQkj@,\8õ8jÍC €… §G­y¨°páÔã¨55 .œzµæ¡ÀÂ…S�£Ö<Ô€X¸pêqÔš‡ N=ŽZóP`á©ÇQkj@,\8õ8jÍC €… §G­y¨°páÔã¨55 .œzµæ¡ÀÂ…S�£Ö<Ô€X¸pêqÔš‡ N=ŽZóP`á©ÇQkj@,\8õ8jÍC €… §G­y¨°páÔã¨55 .œzµæ¡ÀÂ…S�£Ö<Ô€X¸pêqÔš‡ N=ŽZóP`á©ÇQkj@,\8õ8jÍC €… §G­y¨°páÔã¨55 .œzµæ¡ÀÂ…S�£Ö<Ô€X¸pêqÔš‡ N=ŽZóP`á©ÇQkj@GØyu °ûÞ¶]7¯]ç½!À?×3ìúåÉÏž]ˆ™í$=¸0îÓüW@Ücáf/jç¯Ò>™WröQ^¯¦6�·W­Ýn¯…›½¨�¿Jûì Šqlpט^ÔÎ_¥}öÀ’…›½¨�¿Jûì P °{ëkиìüFMÛ¥+7l÷æ6—?¶o{µ ð{„ù¸ÂøÝI¯ä¡ºùv�¯Ö&—wM~ý®ÙbzËÿ\ýƒ¸q8½ºËÝçÓÓ÷~ƒ?~³ÉöÇØ>V®-DÓf[ÇíÞüváòÃô—[�úóý|§ÃõÃüno7xõ€ÒÓ�…óÃõ¾7ˆÇÛ`²Tî;ô–Þ&PÛÇÞp¸üxý˜š0®|/¸t×&óíwÃõ;³}l»7À?=¦�üÔÝÂ}þžž~ûp·tïïïa/x¼ýq¾µmXƒa &°âvá-Åw€ý?Œv ÀlòËýÛF—öp>†ƒ)éixŸï~¤—Œ·?0Àf%@·]xàwX€í€h˜I>¯dR€éõÃ-à†óX8e§�»Ø�÷p Î÷7™ï)ÀñöÇØ…‹� ó} p“}ÿ÷-"´ç·|X`/~ðÑñ¦ ÖŒó}pï��p·9xõ{£��E€ãí� ÐÛWwÁa»£tg¯çû `8>µÍ×û `A²HͶÙqÀÑA&w :η›ÍŽî|œ>ˆ~ïoý‡ñt`¾ýáö�¡æv2ÆÛ…縒Ãíú'€›™§â:7^?¯ðTb˜oØÉÈ.ËÃì¸üÇï7äO¥…Sÿ\¯»Ç�<÷>�{¿Éö‡Ø?G¿ `X°py¿èGèßOá-8ü0B®”ìÂ# ‡a6œÇ`|?óÃbøÔ_#þ0Bp¼ýAž­—óØðqÛóØÐÆ.p²A›Ÿ4ž4+N6hóÓƒÀ“ÀcÀ“À“öÛæ¡ÀÂ…S�£Ö<Ô€X¸pêqÔš‡ N=ŽZóP`á©ÇQkj@,\8õ8jÍC €… §G­y¨°páÔã¨55 .œzµæ¡ÀÂ…S�£Ö<Ô€X¸pêqÔš‡ N=ŽZóP`á©ÇQkj@,\8õ8jÍC €… §G­y¨°páÔã¨55 .œzµæ¡ÀÂ…S�£Ö<Ô€X¸pêqÔš‡ N=ŽZóP`á©ÇQkj@,\8õ8jÍC €… §G­y¨°páÔã¨55 .œzµæ¡´;@""""""""""Ú7õñZÔ餩Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§t€—ûý~ ¾ÚT·žu§¼Ãÿd]3¼?!ÀÙoü þë8°ûÑví¯¥�Ï¿=]˜xöQ.Ícm˜ûrÛÍÜÖáž ÷b=�âîå<ÕŠ5^4¸êî( €çx¹?(Ýo—džӰßÉØí�ôgÇü×ɯÙ„ŸÇq8½§ã™ž6ýé<³íëö€nëþÝ÷[÷¸v;ìkÚþ“Îm>l×ùÍ¿/¬¸ó]ã{pÏoºÀ»ûÆ‡Ó @þÅÁ› ׿ù3â×ÙªùyÄÿozz[8?\ÏO¨™Û¾n³k: °ßºõ¾Ú'ý—íšÀðëš.½¢àø_vèÈ…Óñ^p»vù>ç.í_†ë_ÌÛcÛÍZ˜GgrênáÜx†Ó8�ï�† �·¯ÜÜ ÞˆGاmMw·xÜ=n“³’“pKøöv$€ãÙý7ü¶`ºÖÍ2@ÿ¶ùCÄ…y„ÿozÞÆç{€“y¦ÛWn~Igú�Âås‡a´ç«éÛàmX_–-Lzs30½~¸Ü´'óHÇë˜Æñ¤�p—æ‘n_¹ÕÝÝh›=–›è6™Œç×Ý)ÙváV¼Äƒ“…Ënþð‡ã¾E6Þe€›ŒÿçÃcÄ…»àÓ w7k^îën¾òdéxïaoi´ó±p¼}ÝÖ|ÔìÚßpôà=€Z ð~_wŽ×lÖ“yäãMvk“q½¸Íø§KÚÍÍÛ·ßw 8:|ñc€·µ·~¸<�ñxã&ɸžÞçÛ×lvMÇ"  èï8]ÆK²Yà«çR6›Çx¼q›ä¼·ÿŠ5¶ðæÞ=n=’·¥Ã0áPÍ«Ã0~Ûþy†ÛÏÚ ;é¨òÃ)ãDKÇ3“ík�5ÀAX3&ùàÅa˜¼edz% všÃßÑÂeÇýUö8=ÝÜãx†ÓE€ÙöB€ßzšøLÈøxÞ3€í™ú'€ï3OŹÙ^áÍ�®ß¦{�éa·¿² À8�ì©´pzO�MžŠk§Ä6ÿj€á)¶&kò§âšfxÊ­ÉwZFOÅ ?ýa„&8ü0Â-\)a8 Óì 0¾Ÿùa„a<ÃiòÃþì|ïw›ñ¯è uþùà·Ñîr÷øjîì¶Ëv“F82À˜bÇÁ`51(Úî;À]R ñÔ€h<5 O è¸i—Ô€h<5 ïkÔÒùGMýý£l¼ ÿs'ÿWà‘HÒHÒHÒ*ü¼Îžy½þ='À­{�Ò^ì!õÙ>nz�z±~c[üî¤ýOà½øثΠÐý^Îâ%T9ŽZfÀ-à¨ì—Çü»ìõÿü^JÿÚS›¿Ü‹�~Ð?üþôûƒÏkÿß_÷ ÑqûtââÞÇiŽ^ÑâæÏ½e/©q¹ç¿Üã~é¦^¿Ó7°5ðêeõðzm×1@÷ÁgØî4ó—´ˆŸ¥¯juq—ø nîW1wúõÊ_Þ`Ïk`¯-ûúúÛßþ —8Ÿ_d¤w€£Ã.Ão)¿/û(|¼°NS€�ƒ.sýGŸþvp ðï@n8ç„“W[‹¯0ê/p›ß÷»š¬Ñj€Nî>÷ëWlâ Ä}�᦯`åêü¼ÆG…'^Uàwzg^1€uZðê÷.^ü¼ž àÌŸȆWàM.섬x½ží.¸™>õ;z=ÆKðæÏ`�¦ÃDN¯þ=Àiß+È>>`­föÙø8ä÷ àpòÄk5Àø„Ü €çz ˜w‰¯Gþ2Òpì>w?.À:ý`/kÀ«;Tžà̟抻·&¼^ë´àWØ» ;'é^pØd‚õ¿#ÃßÃv‚ã%7Æ €•Zð+>üà×5Ü2~�` U{´à¡Ûð[À=àbünÒp©Sü½ñóÀ¥¸Ü%’4’´¥Uà‘Ô/0É TO €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€h<5 O €ÆS ñÔ€HDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDd¯ÿO­j‹å*IEND®B`‚urwid-urwid-69907e4/docs/tutorial/hmenu4.png000066400000000000000000000071211526073734300210010ustar00rootroot00000000000000‰PNG  IHDR€5Ò cHRMz&€„ú€èu0ê`:˜pœºQ<PLTEåååîÿÿÿÿÿÿxl§ÁbKGDøoéÇ pHYs‡‡�åñe ¬IDATxÚíÝ r£8EaáÙÞ•LÕô:û_ÓØFI€!1p‘Þ9UiwlóÂ7þÁŽÇ9"""""""""""""""""""""""""""""""""""""""’w/<æ¸V,´Úæ`aÕ6 «¶9XXµÍÀªmVms°°j›€…UÛ,¬Úæ`aÕ6 «¶9XXµÍÀªmVms°°j›€…UÛ,¬Úæ`aÕ6 «¶9XXµÍÀªmVms°°j›€…UÛ,¬Úæ`aÕ6 «¶9XXµÍÀªmVms°°j›€…UÛ,¬Úæ`aÕ6 «¶9XXµÍ±;Àö±nþº¶ª`à.º–9Nm€íüÂâ~ºW™£¨ö8HË—ûÅ ;fX|•·4ÇÖ®2GQ�p¡S÷×dàÌqÉ_F3ÙÍ»kàQs×ç¸djÝ‹’kÚ× §`;œÉï`÷:m_—ÖDßo]zþö«sŒÇSoÏôôþ\!ž3Yÿ˜8úïŽkeýò¶‡7ÎßøÇüx׺ìúývÎ.çóÝçÖ?vÇw ç´'Àž\8ÍŸ?o×ÚÇ’6|'{ÜŽço{˜Ík݃^œcØÎè´¿…ë·g<æxüm(_ÿàwÜ5œÓŸ?ðf Àñ,~Oßóï¿Îï¿zˆ¸:G¸Þø4|åË=ÀÉœñúÀñ"š͸Ó? šŸ?Ü8ðÛ9âíõL‡í‰á.ͯP/â‡[1«ùé~pxn‘lï2ÀCu0în¶lÃŬ<ûIHøíùöëDË6þ¯š?Ø.\éÕ<¬ ûþ`€[¯Aýó£›Ù£ÛþWíÛï‚Û_ÞÏÐB³\¶� íÑì~ß²ëH{4·Kû{àÖ S a¹¿£öë §÷üR@¿üñÏÇ_Z×ö×ÓúÅÃå¹árh ¹¬^Œÿîà°ÜŸÛÍœ6éÞãåÏËjs9Àþ/íp~Zh ‡0â‹)<մͽ¿kÆõ^Ëý÷Ó‹mÝ=^Þ>ÿlž·ƒMøN/É_Þ@€šÝ§Í[€÷á28~¿�èÿîàs€ÑõŒKX{‹c\1…øÿl#lãGn‰�ˆÓ(€ôjfŸŽ»ÿg] í·[_+o7€Ã““W3Ãò€í°Y´Ð~ýñ“þbÐ9î‚-µ#Àþaà”MÆi `@Kí ð@úi‹#|É{–ÃÄh—.o+@Úi ËúÑÀöL?¿1wy~ùúcÀ6¼¤Àú[žÕ¶�g“—â"€ÑzãKqnáòfö›Ñ$Ï‚Ã*[ÞKe· ° /ɺ…7#ŒÇíüz㛚™Ë ËWÞ]¸e¼;Ö߆{¹K§þùч©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4žP�o_ê-ølËOÝ~5 +üt\àí«oÛz]¼åöÇ/»8Àtó–ž3†P�¯Ò‚ à¦–� €”À2Þ¾ü�>@OýC¤þßÓS÷\á6<ðÊÖ?½[²Ï?_Éc>3»}u·qûÇõ²ùvN è ³kÂǧÝÂòp¾Ç À|ýÃ˯æ–lÇËW÷ Î2˜Í·sj@˜ÿX=À×ÞêÒÓ~=þíâÓÇ>êúËq®sãsÉtýÛðH·Ãõ7p‹wÁ·¯~‚üYðt¾]SºÀüê¹ÃºqÏħá+_îwP—îøtýÛÜÒ¦ÛÝ’¥+}…›¼pº°;`«Õ€® 0ü¸“Ó×}XçwL7,¿…=í¹|ýÃ[¶#~,—¬äÏ7þG—œÎ·kj@%ž[ ;ø=ÀÓ¾#.Üð"Ã]Ö`xTž=ùX˜¯tKǧ³]´8]ÉP0{ðð,ÒûêVëÜ“�x;¢ÅéJ€:€Ùá‹E€]´J7ž¾½ N×?¸ùÃ0Ãu/= çî(8Ý�7ÿ80Þ�1¢xÙ€î¬ç!i,`çü“‡tÅ?ÿ`t˜"Ù§ÙúÏ׺sß'“mÇÒa˜è/+‡aŽØH5 +œˆvÏ[Gÿ×át`²¾ ßŽþ•�üÁ;€Ý°<™o×Ô€.ð6<^�_Š §þ¥ªáµ*ç&·,ɳßq}À°_°ì¥¸l¾É“§ù[νRº ÀáÏ™7#øþva—ïïl:%À~;ü­umpØúáMÝ-x›¼á€Ô€.�ÎM €ÆS ñÔ€h<5 O €ÆS ñÔ€h¼ÿ²––_5õÏ�>,ß¡ÿô'ÿªaÐH$i$i´Ûì» n'ýAh3À?ßjküI>°€¾¿X^¿èNþ‚U€/�,®ß¾�ïj{{,®:~ÿí¿X\ ´ôóýçû=?ûË¿=?|´È-þ={w1€ÃÝoÿõW €Ë½ÿ|@ÿù~OwÀè—¬º~ɉ=ðùœcà÷õnÕ;ýB­}>`ÿù~Ñ/G…�î~ÍÔ]àãiïßy€¼àØÚç†_‰Ëv#¹ég:Þàë°Ë,À ÞplýóýÂR^ñ€cŸÓ¾;q«ðŠ7€[û|À€ÃG¥€¿jíóW~}]ä.xñIÈ7wÁ×nõóýŸ »‹\< ÀÂ*à³¹WBþ^ñ>X½“¯\öù€^â1à"@ŽVúù€ÞÂÇ·�¸‘ë£wð¨ÒÏ̆''ñ³à°Ê‰WÆï¼à}°z'_¹äóWº¯áó /0~GôïƒÕ;™>làµSÿüèÃHÒHÒHÒHÒHÒ–v¨�¤þ€I> Òxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@ã©Ðxj@4ž�§@"""""""""""""""""""""""""""""""""""""""{ýÖÔèùDÛIEND®B`‚urwid-urwid-69907e4/docs/tutorial/index.rst000066400000000000000000000405721526073734300207430ustar00rootroot00000000000000.. _urwid-tutorial: ****************** Urwid Tutorial ****************** .. currentmodule:: urwid Minimal Application ------------------- .. image:: minimal1.png :scale: 50% This program displays the string ``Hello World`` in the top left corner of the screen and will run until interrupted with *CTRL+C* (*^C*). .. literalinclude:: minimal.py :linenos: * The *txt* :class:`Text` widget handles formatting blocks of text, wrapping to the next line when necessary. Widgets like this are called "flow widgets" because their sizing can have a number of columns given, in this case the full screen width, then they will flow to fill as many rows as necessary. * The *fill* :class:`Filler` widget fills in blank lines above or below flow widgets so that they can be displayed in a fixed number of rows. This Filler will align our Text to the top of the screen, filling all the rows below with blank lines. Widgets which are given both the number of columns and number of rows they must be displayed in are called "box widgets". * The :class:`MainLoop` class handles displaying our widgets as well as accepting input from the user. The widget passed to :class:`MainLoop` is called the "topmost" widget. The topmost widget is used to render the whole screen and so it must be a box widget. In this case our widgets can't handle any user input so we need to interrupt the program to exit with *^C*. Global Input ------------ .. image:: input1.png :scale: 50% .. image:: input2.png :scale: 50% .. image:: input3.png :scale: 50% .. image:: input4.png :scale: 50% .. image:: input5.png :scale: 50% This program initially displays the string ``Hello World``, then it displays each key pressed, exiting when the user presses *Q*. .. literalinclude:: input.py :linenos: * The :class:`MainLoop` class has an optional function parameter *unhandled_input*. This function will be called once for each keypress that is not handled by the widgets being displayed. Since none of the widgets being displayed here handle input, every key the user presses will be passed to the *show_or_exit* function. * The :exc:`ExitMainLoop` exception is used to exit cleanly from the :meth:`MainLoop.run` function when the user presses *Q*. All other input is displayed by replacing the current Text widget's content. Display Attributes ------------------ .. image:: urwid_attr1.png :scale: 50% .. image:: urwid_attr2.png :scale: 50% .. image:: urwid_attr3.png :scale: 50% .. image:: urwid_attr4.png :scale: 50% This program displays the string ``Hello World`` in the center of the screen. It uses different attributes for the text, the space on either side of the text and the space above and below the text. It waits for a keypress before exiting. The screenshots above show how these widgets react to being resized. .. literalinclude:: urwid_attr.py :linenos: * Display attributes are defined as part of a palette. Valid foreground, background and setting values are documented in :ref:`foreground-background` A palette is a list of tuples containing: 1. Name of the display attribute, typically a string 2. Foreground color and settings for 16-color (normal) mode 3. Background color for normal mode 4. Settings for monochrome mode (optional) 5. Foreground color and settings for 88 and 256-color modes (optional, see next example) 6. Background color for 88 and 256-color modes (optional) * A :class:`Text` widget is created containing the string ``" Hello World "`` with display attribute ``'banner'``. The attributes of text in a Text widget is set by using a (*attribute*, *text*) tuple instead of a simple text string. Display attributes will flow with the text, and multiple display attributes may be specified by combining tuples into a list. This format is called :ref:`text-markup`. * An :class:`AttrMap` widget is created to wrap the text widget with display attribute ``'streak'``. :class:`AttrMap` widgets allow you to map any display attribute to any other display attribute, but by default they will set the display attribute of everything that does not already have a display attribute. In this case the text has an attribute, so only the areas around the text used for alignment will have the new attribute. * A second :class:`AttrMap` widget is created to wrap the :class:`Filler` widget with attribute ``'bg'``. When this program is run you can now clearly see the separation of the text, the alignment around the text, and the filler above and below the text. .. seealso:: :ref:`using-display-attributes` High Color Modes ---------------- .. image:: highcolors1.png :scale: 50% This program displays the string ``Hello World`` in the center of the screen. It uses a number of 256-color-mode colors to decorate the text, and will work in any terminal that supports 256-color mode. It will exit when *Q* is pressed. .. literalinclude:: highcolors.py :linenos: This palette only defines values for the high color foreground and backgrounds, because only the high colors will be used. A real application should define values for all the modes in their palette. Valid foreground, background and setting values are documented in :ref:`foreground-background`. * Behind the scenes our :class:`MainLoop` class has created a :class:`raw_display.Screen` object for drawing the screen. The program is put into 256-color mode by using the screen object's :meth:`set_terminal_properties() ` method. This example also demonstrates how you can build the widgets to display in a top-down order instead of the usual bottom-up order. In some places we need to use a *placeholder* widget because we must provide a widget before the correct one has been created. * We change the topmost widget used by the :class:`MainLoop` by assigning to its :attr:`MainLoop.widget` property. * :ref:`decoration-widgets` like :class:`AttrMap` have an ``original_widget`` property that we can assign to change the widget they wrap. * :class:`Divider` widgets are used to create blank lines, colored with :class:`AttrMap`. * :ref:`container-widgets` like :class:`Pile` have a ``contents`` property that we can treat like a list of (*widget*, *options*) tuples. :attr:`Pile.contents` supports normal list operations including ``append()`` to add child widgets. :meth:`Pile.options` is used to generate the default options for the new child widgets. Question and Answer ------------------- .. image:: qa1.png :scale: 50% .. image:: qa2.png :scale: 50% .. image:: qa3.png :scale: 50% This program asks for your name then responds ``Nice to meet you, (your name).`` .. literalinclude:: qa.py :linenos: The :class:`Edit` widget is based on the :class:`Text` widget but it accepts keyboard input for entering text, making corrections and moving the cursor around with the *HOME*, *END* and arrow keys. Here we are customizing the :class:`Filler` decoration widget that is holding our :class:`Edit` widget by subclassing it and defining a new ``keypress()`` method. Customizing decoration or container widgets to handle input this way is a common pattern in Urwid applications. This pattern is easier to maintain and extend than handling all special input in an *unhandled_input* function. * In *QuestionBox.keypress()* all keypresses except *ENTER* are passed along to the default :meth:`Filler.keypress` which sends them to the child :meth:`Edit.keypress` method. * Note that names containing *Q* can be entered into the :class:`Edit` widget without causing the program to exit because :meth:`Edit.keypress` indicates that it has handled the key by returning ``None``. See :meth:`Widget.keypress` for more information. * When *ENTER* is pressed the child widget ``original_widget`` is changed to a :class:`Text` widget. * :class:`Text` widgets don't handle any keyboard input so all input ends up in the *unhandled_input* function *exit_on_q*, allowing the user to exit the program. Signal Handlers --------------- .. image:: sig1.png :scale: 50% .. image:: sig2.png :scale: 50% .. image:: sig3.png :scale: 50% .. image:: sig4.png :scale: 50% This program asks for your name and responds ``Nice to meet you, (your name)`` *while* you type your name. Press *DOWN* then *SPACE* or *ENTER* to exit. .. literalinclude:: sig.py :linenos: * An :class:`Edit` widget and a :class:`Text` reply widget are created, like in the previous example. * The :func:`connect_signal` function is used to attach our *on_ask_change()* function to our :class:`Edit` widget's ``'change'`` signal. Now any time the content of the :class:`Edit` widget changes *on_ask_change()* will be called and passed the new content. * Finally we attach our *on_exit_clicked()* function to our exit :class:`Button`'s ``'click'`` signal. * *on_ask_change()* updates the reply text as the user enters their name and *on_exit_click()* exits. Multiple Questions ------------------ .. image:: multiple1.png :scale: 50% .. image:: multiple2.png :scale: 50% .. image:: multiple3.png :scale: 50% .. image:: multiple4.png :scale: 50% This program asks for your name and responds ``Nice to meet you, (your name).`` It then asks again, and again. Old values may be changed and the responses will be updated when you press *ENTER*. *ENTER* on a blank line exits. .. literalinclude:: multiple.py :linenos: :class:`ListBox` widgets let you scroll through a number of flow widgets vertically. It handles *UP*, *DOWN*, *PAGE UP* and *PAGE DOWN* keystrokes and changing the focus for you. :ref:`listbox-contents` are managed by a "list walker", one of the list walkers that is easiest to use is :class:`SimpleFocusListWalker`. :class:`SimpleFocusListWalker` is like a normal python list of widgets, but any time you insert or remove widgets the focus position is updated automatically. Here we are customizing our :class:`ListBox`'s keypress handling by overriding it in a subclass. * The *question()* function is used to build widgets to communicate with the user. Here we return a :class:`Pile` widget with a single :class:`Edit` widget to start. * We retrieve the name entered with :attr:`ListBox.focus` to get the :class:`Pile` in focus, the standard :ref:`container widget ` method ``[0]`` to get the first child of the pile and :attr:`Edit.edit_text` to get the user-entered text. * For the response we use the fact that we can treat :attr:`Pile.contents` like a list of (*widget*, *options*) tuples to create or replace any existing response by assigning a one-tuple list to *contents[1:]*. We create the default options using :meth:`Pile.options`. * To add another question after the current one we treat our :class:`SimpleFocusListWalker` stored as :attr:`ListBox.body` like a normal list of widgets by calling *insert()*, then update the focus position to the widget we just created. Simple Menu ----------- .. image:: smenu1.png :scale: 50% .. image:: smenu2.png :scale: 50% .. image:: smenu3.png :scale: 50% We can create a very simple menu using a list of :class:`Button` widgets. This program lets you choose an option then repeats what you chose. .. literalinclude:: smenu.py :linenos: * *menu()* builds a :class:`ListBox` with a *title* and a sequence of :class:`Button` widgets. Each button has its ``'click'`` signal attached to *item_chosen*, with item name is passed as data. The buttons are decorated with an :class:`AttrMap` that applies a display attribute when a button is in focus. * *item_chosen()* replaces the menu displayed with text indicating the users' choice. * *exit_program()* causes the program to exit on any keystroke. * The menu is created and decorated with an :class:`Overlay` using a :class:`SolidFill` as the background. The :class:`Overlay` is given a miniumum width and height but is allowed to expand to 60% of the available space if the user's terminal window is large enough. Cascading Menu -------------- .. image:: cmenu1.png :scale: 50% .. image:: cmenu2.png :scale: 50% .. image:: cmenu3.png :scale: 50% .. image:: cmenu4.png :scale: 50% A nested menu effect can be created by having some buttons open new menus. This program lets you choose an option from a nested menu that cascades across the screen. You may return to previous menus by pressing *ESC*. .. literalinclude:: cmenu.py :linenos: * *menu_button()* returns an :class:`AttrMap`-decorated :class:`Button` and attaches a *callback* to its ``'click'`` signal. This function is used for both sub-menus and final selection buttons. * *sub_menu()* creates a menu button and a closure that will open the menu when that button is clicked. Notice that :ref:`text markup ` is used to add ``'...'`` to the end of the *caption* passed to *menu_button()*. * *menu()* builds a :class:`ListBox` with a *title* and a sequence of widgets. * *item_chosen()* displays the users' choice similar to the previous example. * *menu_top* is the top level menu with all of its child menus and options built using the functions above. This example introduces :class:`WidgetPlaceholder`. :class:`WidgetPlaceholder` is a :ref:`decoration widget ` that does nothing to the widget it decorates. It is useful if you need a simple way to replace a widget that doesn't involve knowing its position in a :ref:`container `, or in this case as a base class for a widget that will be replacing its own contents regularly. * *CascadingBoxes* is a new widget that extends :class:`WidgetPlaceholder`. It provides an *open_box()* method that displays a box widget *box* "on top of" all the previous content with an :class:`Overlay` and a :class:`LineBox`. The position of each successive box is shifted right and down from the previous one. * *CascadingBoxes.keypress()* intercepts *ESC* keys to cause the current box to be removed and the previous one to be shown. This allows the user to return to a previous menu level. Horizontal Menu --------------- .. image:: hmenu1.png :scale: 50% .. image:: hmenu2.png :scale: 50% .. image:: hmenu3.png :scale: 50% .. image:: hmenu4.png :scale: 50% This example is like the previous but new menus appear on the right and push old menus off the left side of the screen. The look of buttons and other menu elements are heavily customized and new widget classes are used instead of factory functions. .. literalinclude:: hmenu.py :linenos: * *MenuButton* is a customized :class:`Button` widget. :class:`Button` uses :class:`WidgetWrap` to create its appearance and this class replaces the display widget created by :class:`Button` by the wrapped widget in *self._w*. * *SubMenu* is implemented with a *MenuButton* but uses :class:`WidgetWrap` to hide the implementation instead of inheriting from *MenuButton*. The constructor builds a widget for the menu that this button will open and stores it in *self.menu*. * *Choice* is like *SubMenu* but displays the item chosen instead of another menu. The *palette* used in this example includes an entry with the special name ``None``. The foreground and background specified in this entry are used as a default when no other display attribute is specified. * *HorizontalBoxes* arranges the menus displayed similar to the previous example. There is no special handling required for going to previous menus here because :class:`Columns` already handles switching focus when *LEFT* or *RIGHT* is pressed. :class:`AttrMap` with the *focus_map* dict is used to change the appearance of a number of the display attributes when a menu is in focus. Adventure Game -------------- .. image:: adventure1.png :scale: 50% .. image:: adventure2.png :scale: 50% .. image:: adventure3.png :scale: 50% .. image:: adventure4.png :scale: 50% We can use the same sort of code to build a simple adventure game. Instead of menus we have "places" and instead of submenus and parent menus we just have "exits". This example scrolls previous places off the top of the screen, allowing you to scroll back to view but not interact with previous places. .. literalinclude:: adventure.py :linenos: This example starts to show some separation between the application logic and the widgets that have been created. The *AdventureGame* class is responsible for all the changes that happen through the game and manages the topmost widget, but isn't a widget itself. This is a good pattern to follow as your application grows larger. urwid-urwid-69907e4/docs/tutorial/input.py000066400000000000000000000004671526073734300206120ustar00rootroot00000000000000from __future__ import annotations import urwid def show_or_exit(key: str) -> None: if key in {"q", "Q"}: raise urwid.ExitMainLoop() txt.set_text(repr(key)) txt = urwid.Text("Hello World") fill = urwid.Filler(txt, "top") loop = urwid.MainLoop(fill, unhandled_input=show_or_exit) loop.run() urwid-urwid-69907e4/docs/tutorial/input.py.xdotool000066400000000000000000000002421526073734300222700ustar00rootroot00000000000000windowsize --usehints $TERMWINDOWID 12 3 key --window $TERMWINDOWID Return key --window $TERMWINDOWID N key --window $TERMWINDOWID O key --window $TERMWINDOWID P urwid-urwid-69907e4/docs/tutorial/input1.png000066400000000000000000000010161526073734300210160ustar00rootroot00000000000000‰PNG  IHDRÀ`n«"5 cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñe†IDATxÚí—Qr„ D1Å-æþG›{äc�(6Ël²“.’~I)½#ÊÍËÚ|°; vþ½@�&lÿïƒëw3®oèfm·-¥S—ª �˜Ï€Ÿ‡\¿›ë/ÿ¤ øi83éò30xƒ®yÜ.­}ÞJñbãOšà ˜�Ÿíÿ³¸]ZaÞÆ%ìÜ©Ž:È?–®…æ`_ÚŒã¼í÷†ùÁðTìí¥OÜ+~ü´ãî€%tì[DÖRÆy�äµ5% ¸Û×;šrÔã](Å%g€þè9°Ðwæ/Í€§ ˆî&}>úŽþ háËág0ëÀwÖß|дßzW_O>ÒŽ“·¯ï]âÞý~ƒ¿?΀û-ßÿÒÁÅd¨ý•ü½¾ãø¶Ð†Yþ� °‘ °‘ °‘ °‘ °‘ °‘ °‘ °‘ °‘ °‘ °‘ °‘ °‘ °‘ °‘ °‘ °Y^à„ÀV<#áíõIEND®B`‚urwid-urwid-69907e4/docs/tutorial/input2.png000066400000000000000000000006711526073734300210250ustar00rootroot00000000000000‰PNG  IHDRÀ`n«"5 cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñe1IDATxÚí—[rÂ0 �NoáûM÷èG1MÁnƒº8³ûCÌ( •,–(sóA' �€t¯¨5÷{Ò>ÿó\:¥¼Ëñ{™-4-Ó fà{ä¢]G©ëåý%zñÏ·dÒí@]'ùs5,o1Œ/«öÔô f ʯ–›ñí-f ÝËû¶@?þ¿~'.z �½üYrÑÝÀàƒÑôèì<|Nǧ ”Ò2úë!¾ŒÏ¢7Q·jÚ"bg|"ÝDÊápÆÁø<–wùgu–+žBs¡�4 Ð(@£�4 Ð(@£�4 Ð(@£�4 Ð(@£�4 Ð(@£�4 Ð(@£�4 Ð(@£�4 Ð(@£�4_Z1Of©­IEND®B`‚urwid-urwid-69907e4/docs/tutorial/input3.png000066400000000000000000000005321526073734300210220ustar00rootroot00000000000000‰PNG  IHDRÀ`n«"5 cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñeÒIDATxÚíÕ1ƒ0Ñå¾ÿÑ| HÒÄ Åø[3 ”~ìÊl½²Ûé  €>ÀÓ^׳Uÿ¼ÕùÞª¾WT«I¯«ø ü4úXOA­ èA;?�÷5úï’œôM h‡âWhU@έ:�œF€˜Zv1 ÓþzïRÚR¾ô¨ø  @'€N�:tèÐ  @'€N�:tèÐ  @'€N�:tèÐ  @'€N�:tèÐ  @8[» WâIEND®B`‚urwid-urwid-69907e4/docs/tutorial/input4.png000066400000000000000000000005531526073734300210260ustar00rootroot00000000000000‰PNG  IHDRÀ`n«"5 cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñeãIDATxÚí×;Â@ Q/Ê-|ÿ£å�ˆÏ†cG3 Rhü´x�±FïnôèÐüÛò|Íx½Ž2"Þ|¼_¨eò,‹Ï| ˆˆugTo²ùøô41´_âo@nëÛãÚŸÀ¶ÄgNÙ ©ý  @wAÀþý•…/Ï_€fÍ] ÆdÈýPWÀìYÙF�1�»è7J�:tèÐ  @'€N�:tèÐ  @'€N�:tèÐ  @'€N�:tèÐ  @'€N�º;å†Y�íJxIEND®B`‚urwid-urwid-69907e4/docs/tutorial/input5.png000066400000000000000000000005401526073734300210230ustar00rootroot00000000000000‰PNG  IHDRÀ`n«"5 cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñeØIDATxÚíÕA Â0áTz‹Üÿh¹‡ S±�¦B“æÛT\”7†<·V²½è  0€੽?kißOÝi?ղ躚œ@ýÿ- }ôeûÌ_ýÑO®O eœAü%6€v\â»ûºì}¾>�ºðÔ?†k´oŸ„ùgw bþÉYˆø-dÍš´-iç�ÄŸ€4hÐ  @3€fÍš4hÐ  @3€fÍš4hÐ  @3€fÍš4hÐ  @3€fÍš4hñoQÓBekJÎIEND®B`‚urwid-urwid-69907e4/docs/tutorial/menu25.png000066400000000000000000000005561526073734300207210ustar00rootroot00000000000000‰PNG  IHDRÏxp%ùNbKGDÿ‡�Ì¿ pHYs  ÒÝ~üIDATxÚíÕQ‚0Àwÿ{ö&*–UDBN¿Ú- »,)×áááááááááááááááááááááááááááááááááááááéxò�ë+~ãã®7ñ§òüžþ-ž{PÊu<5GÏêÈü¨º4TPú“ù�N~N’òŠ e~Î6OSrYòÔ¥Œ÷,O‡W—׿IHÚ»æOé3ÊZOézÊ9<µ––ºY&�.Íg›NS‹ðxÏ,Q½ü”už–Ÿ¬˜ìäÙ#?m§š€šzÉÖþ6öL˵¹ÒÞâ¸ÄéÏ{nêÛÅ­–h÷@IEND®B`‚urwid-urwid-69907e4/docs/tutorial/minimal.py000066400000000000000000000002311526073734300210660ustar00rootroot00000000000000from __future__ import annotations import urwid txt = urwid.Text("Hello World") fill = urwid.Filler(txt, "top") loop = urwid.MainLoop(fill) loop.run() urwid-urwid-69907e4/docs/tutorial/minimal.py.xdotool000066400000000000000000000000511526073734300225550ustar00rootroot00000000000000windowsize --usehints $TERMWINDOWID 21 7 urwid-urwid-69907e4/docs/tutorial/minimal1.png000066400000000000000000000016361526073734300213150ustar00rootroot00000000000000‰PNG  IHDRPàp>aÄ cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñeIDATxÚíØQVâ0@Ñ:Ç]dÿKË>æC�Vp¼8VßýÐSkúLKái.‘þüï ü4Å Š{¾9b¾Ï�mmûøãqÔy>Mó1ó=;ê.’|Îí:%¶¶µGÿÁº‡b:ß,·_pÅ·Bµ�{èË¢º{9Ý>Îö^ŽË2—±}ˆwv}7«+tŒ·ç}Ûíáãlïêø±}ˆñvRßÜÚ =¬‡±,ã®eq¸5Ž;‡¯�‡ŸmŽßGÏ— ×s�˲̯=…yü²±EW.ùãkñ�ËbœbÜuÎëãç=ãwàù4Õ­€9^_s~ÃSÓÆ«üCÚîã’ý¬žC±õúó¯Ì‡ù¢:Í?i%èGŸùæé¡ü®hŸ¿+ôeògoNÆê�û‚ü‹}ô»ò4W>`>�Ë1ÆxoûƇøÇwF—ÇŸ#Îçúø{¸o¬®Ðù:ïÓü/Ïd®lÜ8Ûù‘ñ×ÇŸß?æ²,ËÓ>¦¹=‡bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VP¬ XA±‚bÅ Š+(VPì/†W<�8»¶IEND®B`‚urwid-urwid-69907e4/docs/tutorial/multiple.py000066400000000000000000000020441526073734300212770ustar00rootroot00000000000000from __future__ import annotations import urwid def question() -> urwid.Pile: return urwid.Pile([urwid.Edit(("I say", "What is your name?\n"))]) def answer(name: str) -> urwid.Text: return urwid.Text(("I say", f"Nice to meet you, {name}\n")) class ConversationListBox(urwid.ListBox): def __init__(self) -> None: body = urwid.SimpleFocusListWalker([question()]) super().__init__(body) def keypress(self, size: tuple[int, int], key: str) -> str | None: key = super().keypress(size, key) if key != "enter": return key name = self.focus[0].edit_text if not name: raise urwid.ExitMainLoop() # replace or add response self.focus.contents[1:] = [(answer(name), self.focus.options())] pos = self.focus_position # add a new question self.body.insert(pos + 1, question()) self.focus_position = pos + 1 return None palette = [("I say", "default,bold", "default")] urwid.MainLoop(ConversationListBox(), palette).run() urwid-urwid-69907e4/docs/tutorial/multiple.py.xdotool000066400000000000000000000002631526073734300227670ustar00rootroot00000000000000windowsize --usehints $TERMWINDOWID 23 13 key --window $TERMWINDOWID A b e Return B o b key --window $TERMWINDOWID Return C a r l Return key --window $TERMWINDOWID D a v e Return urwid-urwid-69907e4/docs/tutorial/multiple1.png000066400000000000000000000030221526073734300215110ustar00rootroot00000000000000‰PNG  IHDRp –LcÌ cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñeŠIDATxÚíÛ[v¢J@Q’•Y0ÿ¡1�û‘èRâ‹ÓÞ¸÷OŒU+ I÷Ç4Púü×x7‚Ç�}ísØqW‡% ÁÅÚÓ×xZw”z¾—ܰSŠûN)û‡`<~Ýá9~ Ã0Ž;>r¤3äÕŸÿ |íƒÏx2äxL|çÆÙ×sÓ¯o~ÆÙn\?äÖüg{-Ž·ºžtñ“æs^ä+Žro¼jÏkæ¶¹ÿìEgïqjY»hN§£Çéû›‹o Ìï»åõ›®Øziœ+æxp¾Å¸tüç;Yá§#žö´kÈ´~÷x~s6zŽ6]zðºù/Ž·Ó |8¬ð�ã?>üaoä–U°_¾ÛO)Óê¬�îϑכOû¹öÒøü9}­/˜Ç'sÍbwûmÁ+-ïá×EóÒ9ñþ™öÞá“ò�3¸eû}œ_ŸÑÆ¿tÞøyl¿g¹uäp!\ÿŸ»øWÃqøõÖóá3ÃÚNÿÉ;òñÞOy[¾Ž£^Z‚ãÚ}Ó…m�lOýר[ßßhs÷îD¿½Â§Ù×ùíLW=:Íîyðç}iþ7ÌêÙ®8¥,=í[&9m>ååÎ÷»?ËæËvóññ¿�}Ÿ¿í½Æ?“x±÷Ê{z�¿ø¼QíáUVøð6g”— þ.½_å¢ù>^e…¿��ùVü¾¾f…ßë-Ã?à”<&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1ÿ\9æ@ÄœRb‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLð˜à1Ác‚Ç� <&xLðØئŽ­ÍZÅIEND®B`‚urwid-urwid-69907e4/docs/tutorial/multiple2.png000066400000000000000000000051351526073734300215210ustar00rootroot00000000000000‰PNG  IHDRp –LcÌ cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñe ÕIDATxÚí�]z« ¹ù² ö¿4öq�?¨ 0µÉÌK›D''€‘ÿ¥ $�ßnÀ·¡p…Ã<ûCŽ92•ՓgœÚ�ªá'äB'纔ÏþÄño‡c|†âPn¼RÒŒy7bûãè4hþqR-ÃoÊcñ~Hh^"õ”ðŠðU§<{b6ˆÄ铹¦ ¤�Ý?·yÊüÒ|ƒI‹~þÝHb!í¶?ÅŹcéà['>qReŸlC\ü�“V^õT·‹‡ÒÒÞ‹eíÏÖשC„”½ú‚SØÊJj¢ Ÿ¾zÆ.%m¶êjs_%o;O}…”Ž! �W÷µ0×SŒ±Û·w ï°4÷úÄó-öîp¦\Ù‚šíû0¾Ý¢õïõ¯×úåQÉ` ”_ñ9à'FÃVSÏË=ÃV“àŸÌÈãÙ³¼#žc­{!·žK;ÛŽ7}U÷ÑãJwç:úãO‹¿Ëÿ+HE¯¦Å3?ï¹öW´ª5]Jî°k™9¿Ã|¿óZß¶ê^áß=¾�ýžk{÷X&q³¹rOnpÅç‹l‡»Dxøšå6¿Å÷]Íïá.þ5(Fá0 ‡Q8ŒÂaòëÃ�+vc®eˆW—2\b‘ÞØ‡Ã(Fá0ãR·gËF²×ÌV¯“�ð8_ÇWËä,W|~V7ÄÙ/âo.�ú!¼&áéh镲[�ïR†… CÏ2®4°S¹Š³˜gCx1O™b`7cò³A!([—âxÙŒçäÇyÀÏ ¹\ï8›�89iIá,¥è·ÁRÀs½““Ë1äS4Æ›ñ!³f=¥ÌÃààÙ—ºÁx¦ £p…Ã(Fá0 ‡Q8ŒÂa£p˜� Ÿ�Í÷7¹Á/‘ÿׂñùÎkbToÒ0mdŸïýÞýáØØYê&-¯¾8K)£Ù…ÝA¸—Ð 6îr¼[Y¬ßDrùŶ?Ìç½*k§ÌùÈϰNP^VXÑ 9ã,e{ñ=)·�ûÛ×Õ7Ë—ê÷?<ü÷sg|<–¥û¥ýPE>ðl>ïÚú–åœØ¿ô˜¶ó�o2ä-\dÛ^ï:vg ÉæßsŽóy§yOWÝ\óÒ2µâ¢Àòò f)ïjü€–ä÷žî~eûÚýÏæ#¯bR`eù…ÓÂ?8/o&·m—ô4kJë=‰¼– µC>òví‡ÿƒ!¼¯à®gÿži7þ?Iá—W‹uã½C'<>Ú½úw¼Ÿ‡£›´TÔô8Þç]Û2AuÕ¤¡ô3¾,¿¶¾Âú¯ë¤ÀÊö=³eÍ7SXé�,ÎÞǰ`ÔÜ;�nœ�<­ Ý<<¢¤¯¿?^Ê?(ûäô'eçøõ×õOù@K“?uå šiZ~Ñrñ”Ù½øLËòÓ–ººòZå#_•Qµ|þCׇ×f”â2P}æ´ðê�Ñ‘¼jÎõIÿ@á?Ô~™Hñ™]Ê=ÏêC;hÞ—O�ðÛ¢p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0�~§y×|<¿ÏFfNeõâ9ûY?÷ÿ[3IÂלs]ÊgÞ)=;ã3LZµKªð!ïF‡[DlrƒK%»À£>¿ÒÇSœºè ¯_uÊëôس\Üa'É×:ÿøî t"ß÷Ï¿[I ‹lä«öæ+ŸFc6N|¦yšÇ{}Ÿ½é÷:¯ô”uB²Xt�ñ¸]äQû—)Ùrõ5¼±ÉŒÝ3Í6oriʽº|ߥ‰š�«ÌÖ×Ë÷æ ¹ÌæýÓ†8IŒ¸Ùš\^çÚQ¨$÷nþèíö/îæ+ïÕšI„¯Rùÿ·ª9m?}"ß÷n2¼Šögëë—uìYRþõê Na++©‰‚;ÝÂeìR¶“n_mnoåPš¸´-¥cÃãÕ}mÌõÆ”c¿|ßw ï°4÷úÄó-ÿÚiUmûû5t*|»E êßë7êó}×V~ñõ†”_ñ9à'FÃæù¾w ÈæÿŽñìYÞÃ}|öC0n=—v¶]¦[ß¡q¾ï¢ö·,¿‚ãÏåj>ùLE¯^Ë÷]ÔþŠVµ¦ KÉvM#«ÒkOvh•ïûðm«nàn’ õ{®íÝc™ÄÍæÊ=¹ÁŸ/²îáákz”Ûÿßw4¿‡»Dø×P=hf¯åH1õ³”¹áïšb4À.Fá0 ‡)èÿç{‚gfeštäÝ¥ôºÄ!3ž!\½D,5Lsg)0ùYÊzÙÂgÿª$á™UóÇrš1Â'³í8 £ï•RËÇr–g¹Dz‘éRâ{aÂì¤ÈyL Æyxˆ]~* Þ®m„Y—b�ÑO|`“>ÌFæ3s¿oÃäëY}LN|FR\� yjÔŠI—’f'òa¶2þâmyf5®(»ÎR`£p…Ã(¦~õ¬ßp]Â_@ÀØ¥À(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂa£p…Ã(Fá0 ‡Q8ŒÂaóÜýK�ÞIIEND®B`‚urwid-urwid-69907e4/docs/tutorial/multiple3.png000066400000000000000000000075201526073734300215220ustar00rootroot00000000000000‰PNG  IHDRp –LcÌ cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñeÈIDATxÚí�i²¤ „™oÁý�Æ=æG» kK6Oó‹˜7½Èb6 ˜þs† ùüºoƒ‚ƒ¡à`(8˜-þÈÞt‡àTÄRìþ�̆1 CÁÁ|;MkŒ³Æzݦ5·^ôßÙ­ö“náÖãõ¢vÅnµŸ-ý±3ÆœM~oíÖÜŽÒÄǘ}îJ-Ø”1¤CŠÛÿ‘ÅÿÒG)`6s6á`œâÆ=Œ½Ó¤¢(6ÑVì/‡±ysŽû|;Mçý5&�pp2á(嫸¥ðÝlAëõ&—g“w–m|c®XrÆço ;Ïü£†X8ÓCÁÁPp0 CÁÁPp0 CÁÁd.@ë€È0dW|ˆG_cܯøWت“ `N ŸýÛýá¶QXêFF^}á(Eư »‡à¼„"}›1æV‚õá¥NÄ&¶©lïöç&áöQ^…<¯|î¯n™÷�nÉ�ª¨Ó<÷Ñ¿Âl¬´¾Õímy{]yÎÏÍéÓWwÿú¬E�O˜«X#�C²ííý�·½¶¼0Ÿ†ôÒ}JÖ·ÌÞÂýfá’IïŸú�cC|X¨�û.:²ùíÝ=Ò©ÃAoŸçîN”¡<Á(å Pçz¬Ï—Ò»½6½¿¡kI/-cÏP™¿pXøÇåÃÄ’®NS“ÛìA¤­¼/ãÆWÏv}ís´ð?؄ˬ:ûçL³†Í¼nDxò*X7>»é¸ÊûZrõ}¼•ãÁÎKê[àSOs•vŽW47ʆ£ éö­å Ëïk¬^†ÊúmɼÈL�u4�ÖîP¥¼ÒtÚÕÒ+{á8Óìd°†$†»âÛj §IiDñD6ÇÏ”¯‹O醿¼ÿtù‰:Mçç/Z.îÉ�´faþ.'�.¿{6�Ý�Ÿ‡jùüCׇk¥pTÏövyàUû6õPGúÿ¢=™ˆâ™!eÍY½1æ±�æº<µ…/ CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0̤û4Wõãù=gNŠ5‹ív[?îÿ¥ñLø†ÓRž}\–žöq3ž¡Õ8S…‡üýXsƒ.j¦b|ôþJ�Gl]ÔÂÞ£ Ûcß¼¸MÁä+ö/vB ~ßß—¹ŽÄšÀ�<ªÕ¯Üß�Ád&>¾Oóù¬ïÖ‡~ǾÒ>±!™=cÜæ³¬Õ?´dK•7ðÁ&7Š3Í1?²ÔrOç÷-5j®™,o–ÞÙN3tóþÖÁzƈÙÚ¤|�µ½�Ä�»è�¯ÿñeѯüJ5¯…GVþÇëQ%»üÇ ~ßE3Í ¼¡7î÷]È éÿmmë,¯ÆñŸr´¹Ï\aÛÐn½À`¿oQýG毠ÞÂS^Í�‡ }Ûç÷-ª¿¢V£„”Ônk*©²×öŒòû®þlê ö°ˆê{®í­±Lb±±òL¸âó"µÍ*-ܼ&¢,#ø[ô^¥Ó|«´ð×@ÁÁPp0 CÁÁfšï9Ï�`K¬L#¹BʬKäÆfLï%b¢Á&25ÀQ ˜ô(%^¶ðì»z€$[xbÕÀý=iæláÞhÛÝèµR*|OZÙŒIE2‹DH±×„ۤˆã˜œãpc§ÜJ®Nµ!ÜB #Æ|8ñCÁÁ$?F#÷‘9Ï‹�Á;=K=üóׇ6÷›ëùƒŒÀ )î6‘7·•ñ�K´É —º�á( CÁÁPp0 CÁÁPp0 “Y®Ìu?³XàN俎®qn—¯ [ušóôôqæ´ðÙ¿ÝoÚÆ�u$ÁÙÒ“ XëÇ.bÜ- ǰ�KR@džrbL¼<<4äÎ^)ñÊöU?ï(/S69Sú‘«I”-ÍNRÎ}¼]U÷!Õímy{]y7‡8§Oß¶?ÖÓ§È'ÈE®‡‘Æ!…xÒÏ[[^˜OCú8C{yµ¦ò‹naÈrø‹%â”÷O K—“þâå¥Îu?ï`é´: Yjín/mø�¬Á¹”+`�?°ÄßÛOÞ³½6}«¹ fç�å'/@8,üƒãò¡ã®]Ì¡ãpMg"ûœP'ù‘»!Š!å6á"óüÈ;áùp0ÂJ³›Ž«¼¯%Wß–‡;ü{íK{�l“~Û¥½7I?ðêö­å Ë×çe ùÉwpËåÎ_mz£JýZŸc(è5K§ †û‘‡§;lnƒ*’®>žK¿‘´‚x"›ãgÊï .ÿJ3µ/qÏOà·íÉÅ` ów9étùõû‘ß3ºí�8χ®×Þ?ƒ»ßæ™ÃÂÞ.c"¼âÓ¦êH à_´'Q<3¤¬9«7Æ<¶Ó\—§¶ðe¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ™tŸæª~<¿'ãÌI±f±Ýnëç35�17+šá´…”g—¥ç„}ÜŒgh5ÎTá!¿ÆŸÂ�ÇštxÌŽä£÷Wz;!_Ê´³+5ouš¥˜Ø^óÓ�øgÓ*mýçUÔ<_£å—â†Þï[[xç÷‘_ñinà ½á¿oa5’þäûs{&p<ǧÜmî3WØ6´[/0Üï[›èë-<å]Ýx:Ñ·ƒý¾«ÞÛØŽERR»­©¤Àn<™ ßï;_ÿ¾ ö°ˆê{®í­±Lb±±òL¸âó"µÍ*-ܼ&¢,#ø[ô^¥Ó|«´ð×@ÁÁPp0 CÁÁègšñÂ*¢À[êF ü‹Ï.â=g÷F³™ëbÖ»Î"ýˆ�wñ�mÀw”"Â,éd3&:ZÏ}®}äOÒË'¹ÓëÁ‹ë±‰”ä8ü~«ƒÙß±_Aj¦yEôh3CJ/â©=¥CzjÏÈ1�”à”{"%s ?�t gÀžÆ'Œ�¹|[x´>:~GÆðñNÞï­éÍߣ‰ÔùðãTÊuÒ;>ý=ÓÃåÑ|Œ¹Hßþ“­ž'J¸Ô ¯Úƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`2Ë•¹èg ܉ü0*ëç·Ëׄ­ºÀ°ôsZøìßÝ6ÎÅý…fgK÷Q]ܪ¬d ¯£ð’ªs y…²Ì°••™…@aöAŸQòNù‹W¶¯úyGyòlð#/¹GtË~æUÁ Þ.2‹ŸIQÝÞ–·×•ç¢%¿Úú¦2MÕÖÆ×{ÕO”�T#�C ?𤟷¶¼0Ÿ†ô Hó>| ƒã&N~ÿ´°\+é/^^ÞU÷óv÷H§Ž]^ú‘—t œK¹ày€Jü½ýä=ÛkÓ·ú‘‹Éø‘ »Uá°ðŽË'Æ�TÖR…¶b.?Ø™dþºò~ïG^à)° YÖ�œ3Máp¯½C 8ûxp•÷µäês{¥ãÁ‹P‡ ûŽRòy\“‡ð¤‚jÐ =ÆÃüµå ËWæßܸœ{ËoxŸ;«ëGÓiõþ•Ë+�.ëGî¢åòµä.ßÂ$×4õÏÇsé7²#QOdsüLùúx�Ü�ư"ºˆ®¯—éÉÅ` ów9étùuø‘ßè[>ÿÐõáZÇ"œÃÑ3—Iôvyà8¼M=Ô‘þ@Á¿hO&¢xfHYøTÅC;Íuyj _ †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8˜I÷iÒ©9Gé½dÛí¶~Ü=þK3óÙ•m!åÙÁeé9a7ãZ�3Uxȯ1À =䱿]L|ÊüGï¯ôxÄÖE-ì-< Ê·n�HÍ¿;á?^ì„ü¾Ë~ÝÖnäQý«~åþn &3ññ]ãì)qcb_iŸØÁk/§RœÍgY«èÏ�*¯d×Cq¦9æG–Zîéü¾¥FÍõ"“åÍÒ;Ûi†nÞæôË®úw§lBµ½�Äï»hGš¯ÿñeѯüJ5¯…GVþÇëQ%»üÇ ~ßE3Í ¼¡7ì÷û–W#éOnmë,¯ÆñŸr´¹Ï\a[Å3ãÆú}Ëê?2õžò®n<eÐý~ßÕú+j5AHIí¶¦’»ñd‚¿ïjýû*ØÃ"V¨ï¹¶·Æ2‰ÅÆÊ3YàŠÏ‹Ô6«´p󚈲ŒàoÑ{•Nó=¬ÒÂ_ÿð¶ø¹l�Âï2ü†0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0\® æ?¨î‰I–WIEND®B`‚urwid-urwid-69907e4/docs/tutorial/multiple4.png000066400000000000000000000075451526073734300215320ustar00rootroot00000000000000‰PNG  IHDRp –LcÌ cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñeÝIDATxÚí�k²¬(„™wÁþ—Æ>æÇi•7U<²¹š_ÄÌ=v+`vY búŸ3Éç× x CÁÁíU¬1ìY§qûý‹¢B¸SеÅ)‡1ÁM¹1\î¨9ŽRÀäG)±î¢OØ­N á6øÇœ*Sή÷FÛöìFíÓñ2éå0&—AÈ*2)åŒuoÜ/“n®q¸±–1àŽpª !H)Ìëበ&#ø9 Gæ¼.>ïò,õDðŸózÊ  ¯e›|OúñRŠ Nä�¯¯sá2éæ?ªˆ…£0 CÁÁPp0 CÁÁPp0Laº2çý¬B0?œÔÑçñ½üêÕ%®ËÓSÄYá«»ßÄÆ”y$ÁéY&Ìõc1ï‘…sXÈ)) 2ž{Ê*šîiãï3›ûë4Öwßÿ›ª.*Ó•Ë�ëw™¿‚ÂÇ™º¥Å‰Rʵ�Á]eqÒ\ßÖ××ÕçßÿΚT¶·g¬§O•OTŠ\#ÍC²õmø?o}m}q9Û§Z{…p®¼ä†"ß�'K¤[†ŸV¦.Ÿ+ºèÃJcÜߤ[^?š:­NS¦Z»àO'«@p-åNX×쌼ã]_»½¿¢ëپܲëÀIÊ“W þƒãò©ã®¯˜SÇáš®DÚÆr·¦ynŠâgJùC¸ŠU§y¼F˜Ã£Ciuè¸Ærksõcy¸ãÁÖ¾¶÷÷ÈÖëŒUƒé>ÅåkëÖ¯/ËVÊ“ïàQ*7>µù•ísòõ«åÛÂ*…t­í{ÛSÜq�’Í-œfK#Ê'²süBý£©Ã•ÿÒœÚW«Ësé®Tù®ï¤{—ïJÒéÊ ‹é‘=,(Øq™�®}~÷¼Í3‡…£]ÆBxǧO=Ô‘þ@ÁÿÐ^LDñÌ”²çY½1æ±�æ¾<5·…‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 fÑsš»úñüž‚3'ÅZÅ<ÖÏwjc+šéô¥”g·¥ç‚}<Œgh5ÏTá!¿ÆŸÂ˜Çš yÌÎä£÷WzÝÞÑNñû6#ëOþ}oÏÎ÷øÔCЖ>s•uc»õ Óý¾µ›ã};ÂsÞÕ�‡ };Ùï»é½�íX)%·ÛšF ìÆ³Œû}—Û?ÖÀ6±B}Ͻ½=¦Il6V^Éw|^¤¶Ù%ÂÍk2Ê6‚¿Eï]:Í÷°K„¿ †‚ƒ¡à`(8 F¦™N¬" ¼©nÔÁÉëÙE¼çêÞlsßÌz×U¤ññn2fü�Räo˜%ƒƤB'󹯹�üIFùd2w~>xu>6‘’‡‡�:˜ïûÕäÎ4Ì`fJE|jO©ç�?µgæXFNpʽ�š¹…_@>‡3a/ãFÈZþ"<™�.‘9|¼ …á¢-½ù{t‘»~^J¹/z§—¿Wz¸<š�1÷éàÙìy¢„SÝÀð®= †‚ƒ¡à`(8 †‚ƒ¡à`(8 ¦0]™“~V±Á“È£1þ¸}MÕ¦M _á«;tl\“ÿÆ+-ÎH÷QÝÜjÌdo£ð’js y‡²Î´™•…‰@qñQŸQóÎù‹7Öoúy'eUÊìð#¯“¸'7të~æMÁ+7™Åï¤h®oëëëêsÉ”_m{s…æZkÓ�Û½ê')Fª‡‘æ!…xÖÏ[[_\NÇöHË>} £ã&Ý<ü´2]+ë/^ŸÞÕöóva¦S çN/‹ýÈkºÅ®¥Ü ð:@%þÞþæ#ëk·ïõ#Sð#v«Âaá?8._˜?rEK:ª¥ü`g²åëêû½y…3¥üƒ!\e[?ržiŠˆ‡{ý‚Pð¨ÂÕǃk,·6W_Û«^†:eØÿk”R.ã>yˆ/*¨ Òc<._[Ÿ°~eYécÁ�Ó¹�òŠá¹³z²~r:­Þ¿z}µËsýÈ]2]¾µ¹+G˜äž¦þýx.¿ ;ùDvŽ_¨_Ÿ²»Ó™VD7‘ãùãí:]fsñ ˜âò]I:]y~ä�cÓç:?\ëX„s8zæ4‰Ñ.c!‡÷©‡:Ò(øÚ‹‰(ž™R6¾TñÐNs_žáÛBÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 CÁÁPp0 ³è9M:5—¨½¢—,àëÇ=ã¿5+ß]Ù—Rž}Ü–ž öñ0ž¡Õ¾ôõnïÞ—|§¾Ò>©!™½SÜ–‹lµ?öçÎÕW3�¡z¦9çG–Zîéü¾¥FÍí*³õ­Ò»ØiÆnÞæòËnúwç|œµ½�Äï»êO]nÿùeÕ¯üÞj6^„'Vþçß³jvå�;ü¾«fxŠögë[ç:vHʯ^p «¬D;½ÂåJ)eÓíÑæºëURãÒ¹Hû Ÿoú*Ìxc$Á¸­ß÷l¢N³–û[~ÿì´JÛþu õ/·hBýµ¼¡÷ûÖV>øýDäw|º¼£7œá÷-mFÖïÛÚÞ³¼ç{|ê!hKŸ¹Êº±Ýz…é~ßÚÍq‰¾á9oæÎCЉ¾�ç÷]l¿¢U³¤”Ünk)°Ïn0ê÷]kÿXGØÄ õ=÷öö˜&±ÙXy%Üñy‘Úf—7¯É(Ûþ½wé4ßÃ.þþ‹?`įåˆ~×�á0¥€¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒ¡à`(8 †‚ƒáte0ÿ÷ÏkgjÍIEND®B`‚urwid-urwid-69907e4/docs/tutorial/qa.py000066400000000000000000000011521526073734300200440ustar00rootroot00000000000000from __future__ import annotations import urwid def exit_on_q(key: str) -> None: if key in {"q", "Q"}: raise urwid.ExitMainLoop() class QuestionBox(urwid.Filler): def keypress(self, size: tuple[int, int] | tuple[()], key: str) -> str | None: if key != "enter": return super().keypress(size, key) self.original_widget = urwid.Text( f"Nice to meet you,\n{edit.edit_text}.\n\nPress Q to exit.", ) return None edit = urwid.Edit("What is your name?\n") fill = QuestionBox(edit) loop = urwid.MainLoop(fill, unhandled_input=exit_on_q) loop.run() urwid-urwid-69907e4/docs/tutorial/qa.py.xdotool000066400000000000000000000002051526073734300215310ustar00rootroot00000000000000windowsize --usehints $TERMWINDOWID 21 7 type --window $TERMWINDOWID 'Arthur, King of the Britons' key --window $TERMWINDOWID Return urwid-urwid-69907e4/docs/tutorial/qa1.png000066400000000000000000000021601526073734300202610ustar00rootroot00000000000000‰PNG  IHDRPàp>aÄ cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñeèIDATxÚíÜkrÚ0@aÁ°‹»ÿ¥Ý}ôG°±�ü’�ÐóÍ´é�僑1Éô’E¤ë»'ðm 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì °ÛóMQJÿÿt� òÝf{þßfXˆRJcж—|D¼{Ÿ_)f_ιµ<耿| GÉRJ” {qÍÉ3á+þGŽ�çYþY×A5¼äc1LLŽé†¦ L}Ó³»OnY½wVþUžnž¼~¢”œOq9àæüóþgy�\ Óâ:>z6@L‡¿¶ž‰6ùqÍYöÜ٥غËöü—�ŠÊמ®=õ“RälQžšYöÛ\™ÛÑç#†»o�bkÈ­ù—Ÿ8rÜÀ}~³³PkÏ•54gÃ=¦Õüfig™�r~ì­!OÏ?+cµ-¥¿tRÚªµÜóxÄŠ#�æ–Ã�k‹Âq·’1,ãë/äþ·ñï}GÖ2ÿÆéNÖÐÕ­öæÌᤓ«ß�|añýùƒ×}®”Æõ¹c½'}ËAÚ?ÿ3Ž]zv‡Ø=¡½vg÷æ>Ñ×qÄW=¹÷­OºêÍl�͉³üo}À”�÷åÓ½êÞüÚ=oŸ]•÷‰'滨½ lÜØÑùW§øø˜¨ùªp¾†Vw*ï×¥Ï7míýãj?¶†¯npzm}ßX¶îbeþõ; &/ùÕÝ­,q§V˜áJåðƒfWŒ}Ÿ¦­Ìq‡Ù–ú\>âTðEÄúy(ì#‚~Ó¨^õ3¥6>ªþƒ>âý¢žrRú"r„~�Ëò�Ø>·ÊO;ÕÁ—<Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì_g„ùÌ0_ò0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ 3(Ì 0ƒÂ ûx[˜M-…3¨IEND®B`‚urwid-urwid-69907e4/docs/tutorial/qa2.png000066400000000000000000000032411526073734300202630ustar00rootroot00000000000000‰PNG  IHDRPàp>aÄ cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñeIDATxÚíœk–£ éwÁþ—Æ>úG, êWçÌôLyTx¹ñÏAòúuÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂH(Œ„ÂLë—¬1ûŸt�drE¦ å�Õa�1�*ú†¼µö×m>ühc깨‚+wtkœ1Æc;ZñrÞ;¡ÿÆÍµ£U~Í®Nõò6ÊÆú[¿ ‚I$÷^ɦv‰™ÕËÞø±Æ¸°Šq†Åú»ÏŸ8…‹²éá5_d`ý,矽+Qq’ŸçœØçF“l)I¹þñU6Q`îíÚ"½(YLÊA×tfk²u™ºÕ¾ö›¼´(ØR–¥ú›w¶n.àS¿`êõ™™C]�ÝR­î›¥�iÞšö¼KY6×ß%òê›JOZ”J¶â–ÛE–­¹ ¨›ûf™›Ꙍ³ßi :ê3¥ GÕdˆz#Ÿƒ,J7b�z$FBa$FBa$¦ãÆþ”�7^H}„â¾¢'$—�-uëPÊ9LKy…�ÐÎÑ}°Ïb Vò{ùd«x³' Àq¥›œ‰#ô&>?CÞ~Â7‚Ö-AiŸ—¢¯7ôÆo&ñ}Fñ¥‰ÌÞÿÛ JGº\ÚBûVÉ÷ÐúøËî$iŸÛ™½ÿgË©w­ìQy f¥9þ2Å_¯¯¬¿—Õú°dJ™{è*DrYªÂ5kO¹~Þg2¾òAB“#rÏg™ŸØ3&³kćÖÄc>¢sc¼ûPcù¨÷Ëc¶K=®½ßÕÿÝ4òGµØ…w\·æ¤ûçìÃÎÛ)=c %„Òñ˜×fÚè}~è�ðnìs$â5ÃxÊÍxÌ'�aûÏ펧Ü�i~~è•9=' ‘÷›�è<FBa$FBa$æüè»›ßà+‚FCFBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$FBa$æÌfeSQ(�ÚIEND®B`‚urwid-urwid-69907e4/docs/tutorial/qa3.png000066400000000000000000000036471526073734300202760ustar00rootroot00000000000000‰PNG  IHDRPàp>aÄ cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñeIDATxÚí�k–³*Eé,gÁü‡Æ<ú‡/(^…9 ѳ׺ßm�î S)ÿœ!H>w7àiP( C¡`( …‚¡P0 †BÁP( C¡`( …‚¡P0 †BÁP( C¡`( …‚¡P0 †BÁP( C¡`( …‚¡P0 †BÁP( C¡`–Ìzk Îp…ÅX³¹£CË÷E$èýÞLüÞ¯BmÔ¼YÛ;=”ÀP(˜Åcœ Îy»þ/Zåä&Éë‚MìQØÜXcÜöß¹…ÜÁ[N•/ËKüe¢Õ}.Īj­¯êX>WÔö¯l._�å7Õçöí¤Ous¿"5Ê;QùÞwö~l�f[—Ý;ìÎëJ›én}mß@–.ÇåרÖ�äsQgŒ1ÎË=['Ëo¬ÏšAý1FqÊËîp,;]«÷í•›Gå·ÖçÄ_­õGy0Û5TŒó5z¿Ó¶²\ÆYcÝ]“)öP0׿ò½ß{WY®sÓ�töÐê8ÿK8ïßÑ(Ny9:¶Ž–ûöÚ«Ú·õ)ëÊ´ÕŸ”×еE6\6¥V|×@Y~\_±üÚÇÖŽgãß1ú7šÃ¦�k©\¡Õ6¹}nîï¼RåÇõÙB�G¡‘¿pfïoƒûL ê¡Ûé˜)�d÷H-©›,Ë�ëk;zQ¿k/AÏß#ï$7w8 ÌÏ¡Æ@¯©�zÁìD}à)Ÿ½õ<„GöÐ;¿c|`½—‡öÐû P0 †BÁP( sáŽý�okà•”‚W�U/�R¾<4Ú_Ù~¯¨xǰ>kÊe‹xÐJ@iýøÊñ°‚ÏÑ‚D|A¶šp͆ï«Ú{‡£VëÓ7 {ÊÛþÖýõç§­¼YèxжøÔ9FùGŒï+‹9Þ7&=�»±n =}iÞ£_«'6ªg–©§Ó�¡wµN¿éF‰ÝwLn†“Àþ³šóß«…èâ/uN]eÒÐ�ª­O‡z”·—_Ô·[_¾;+¦}FÓ+–Äœ$q°¶pKq{ñësÖ‰ ^6¤foM¡ ýÜ»~|Õú>~Åù›Œ·TŸôÁïú2­mhRyÓz<ìÉ P\üePè„3‚13¥‡ŒàFM=¡]Iþ|{&~2ú®ø#ˆ›™ãæH#íƒÎ8~²‡ÎÌOöЙ¡P0 †BÁP( £½c?ç´dB–©§?ÈÙC[n±¿fY“ šê !úSr Jcòn=Žò`Ò£|’™L ’Ïš‰ç|Éeãb|hbÓ`ãD<çK.)G�s0CU8fiòƒ&ã+_$4yF~ó]æ{†)ì·Ä‡jâ1_Ñ9�1ÞçPc»|Õ»óš©ÀÙCûï>ú¿¢›§|¯#vá'®G3(>ô=ó°q3¥w\BB¯ÄcâòuÎÆ)´qnb[wøe¼ö9ñšaz}¦ä*ËÞú™lŠF!tô$¼–�4nOûÄ¢_þRe fL2h=?¨5ÕÚv€Q{\mf1ªOü¹Ô�Šuá´#º%]»E­¸…�œ~Wó‘†søä„Í¥ÊïyùQõPñ|u—\›Ïº½PÊÚ”�4¬ùøZê£ã.ÞÃò‡¾cH™�užÉLW†¦]�O¦ Fï@ÀP( C¡`( …‚¡P0 †BÁP( C¡`( …‚¡P0 †BÁP( C¡`( …‚¡P0 †BÁP( C¡`( …‚¡P0 †BÁP( ó×2êaôØùéIEND®B`‚urwid-urwid-69907e4/docs/tutorial/sig.py000066400000000000000000000013311526073734300202240ustar00rootroot00000000000000from __future__ import annotations import typing import urwid palette = [("I say", "default,bold", "default", "bold")] ask = urwid.Edit(("I say", "What is your name?\n")) reply = urwid.Text("") button_inst = urwid.Button("Exit") div = urwid.Divider() pile = urwid.Pile([ask, div, reply, div, button_inst]) top = urwid.Filler(pile, valign="top") def on_ask_change(_edit: urwid.Edit, new_edit_text: str) -> None: reply.set_text(("I say", f"Nice to meet you, {new_edit_text}")) def on_exit_clicked(_button: urwid.Button) -> typing.NoReturn: raise urwid.ExitMainLoop() urwid.connect_signal(ask, "change", on_ask_change) urwid.connect_signal(button_inst, "click", on_exit_clicked) urwid.MainLoop(top, palette).run() urwid-urwid-69907e4/docs/tutorial/sig.py.xdotool000066400000000000000000000002301526073734300217100ustar00rootroot00000000000000windowsize --usehints $TERMWINDOWID 21 7 type --window $TERMWINDOWID 'Tim t' type --window $TERMWINDOWID 'he Enchanter' key --window $TERMWINDOWID Down urwid-urwid-69907e4/docs/tutorial/sig1.png000066400000000000000000000024531526073734300204470ustar00rootroot00000000000000‰PNG  IHDRPàp>aÄ cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñe£IDATxÚíšY–« IwÁþ—Æ>ÞGŒA@¬tçiÕ�QËË Ý�„äç¯p5 £P˜á3ÅÆÂ=gç¡÷•A0ÄÜ^TeÏ� �*ú†üµƒ8NÇŽ>!„˜Š’ÎsÛñx?>´(ýç¤zÃë§x ­S¤3Æ]Lг ³I:æת~%Ha5ÿZò´ò+¤y‚¬EÏŸ[y !¤Ýö§X¤XëKçü·±±�Y‘qRØYG,ŽsÒâd¬§R]Ü.²Öþ"×j}›ñRa÷M‰Y¤J™É‰M9[ÚVÍ_<”XÜîú[‹RÊKŸÆJLÏ“ÝmÓ+AyíÈóI ©×êihÿëf™"®•œ,BóóÂNÍÑ -ÌÄäk×׫´´w³­ý«õõ�Í¡%ÿù¡ßð v°’#O™Û_×™†|Ú¬õlsÆ’·�¦Ïv¸ugø§�í~¾²–`Š{›ýÍð ‹EioNêoÙ+wÇ›ÄÑö÷74º]# bo\�÷>§»V2XsûûîíXmbXlÍN�Ü­²àÍv¤±÷-f˜JÝ ¡¸u-í¤�Ô›¶¨»v~�jvn¢­Gh*Žå臘»©¸rr<®µÿ@«ŽÒ0ä׺u¤©Ú¥õ ó|ýÝ®>–à Üãñßë®ó·—ïø3ò/ï?É|±¿�Íð-.3â¿FèU|~Ë¢t¾%B/ã¼`Äžc( ^kÉýò0 …Q(ŒBa £P…Â(F¡0 …Q(ŒBa £P…Â(F¡0 …Q(ŒBa £P…Â(F¡0 …Q(ŒBa ã¿3ÂøÌ0y…Â(F¡0 …Q(ŒBa £P…Â(F¡0 …Q(ŒBa £P…Â(F¡0 …Q(ŒBa £P…Â(F¡0 …Q(ŒBa £P…Â(F¡0 …Q(ŒBa £P…Â(F¡0 …Q(ŒBa £P…Â(F¡0 …Q(ŒBa £P…Â(F¡0 …Q(ŒBa £P…Â(F¡0 …Q(ŒBa £P…Â(F¡0 …Q(ŒBa £P…Â(F¡0 …Q(ŒBa £P…Â(F¡0 …Q(ŒBa £P…Â(F¡0TN !ýu_¾‚ºÐ˜ýÖY*BçÜ8^›„6ȹ«¿.J0s¡±³”;S86ïì–�²ã+cš³K×f¾`L›£s”9ù”\Þ¡‡â)½²§÷á]âxv×i ¯±ÃÃ$cÇ@!0¿$!„4ŽØBz&»T}wã¦ÃxZK^\p]�ž¡ù‘é�[Ÿ£Äa6´ç!jôÍ{Âb�jͯÑù24n›ºÂTBX쑦�}:à2Î’ËË^=S«¢ç_™Fï3!Ì;ú¨ö{ñ�¹|—Ÿ=…ù ým¤¾9þùnÚ‚.dfÜÐäH=Bå~`†Q(ŒBa £P…Â(F¡0 …Q(ŒBa £P…Â(F¡0 …Q(ŒBa £P…Â(F¡0 …Q(ŒBa £P…Â(F¡0 …Q(ŒBa £P…Âüaaήß?NIEND®B`‚urwid-urwid-69907e4/docs/tutorial/sig2.png000066400000000000000000000033671526073734300204550ustar00rootroot00000000000000‰PNG  IHDRPàp>aÄ cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñeoIDATxÚí�[–« Eé,gÁü‡Vó¸¾xS…ǃgtwŒnKÛ¤þÄ$Ÿo7`6( …‚Yî)Ö;çÞÙ;„¾W‚ŇöY£´/ƒý_åÂÞEúCá`>ù#Ù‹­žNu¾^d¯ýÉVÅúªñÒ¡9S RŠR"9^µ¥¦mÝ퓃⓷GNýÚ $aéǹâe}ѼlÚWH—YŽ�(Ö.Õ£hÿþfº†/•o'ˆÐ°Ä°°K}´¢…�˜ð`5}í¥IëM]û‹õ�Ÿ›‹fûë§¾b f¬Är”q××}ŽS^ªµ^mÎVrݩܻÃÚ>Ãgë>êüzeš`ò·ÍfÿgxºlPjõIã-Û·˜É�°¶¼¡¡Ðz�­óz{ï>ݽ’�5ëïØèÀhã]vivùÌ­opEêGg1ËQj+„|m™4Ö=Vè7-«»÷ÚHws\GÛ�PI~§Õ»’,¹x>–Úoh•Å)_Ú-K#¤»Kå âíÆw»{XÌ lñ÷ŒûuóüïåÿFþÏ׊wò€;öÙtO‰P7Íÿ¡³ø|Ê 4O‰Ði€JÅ{õ/?ÊÇçÂð”C¡`(L­�grýŸYÒaƒ¯�åÏKðU¸œ~á †BÁ”úÐøqÎÒó”…µ‡W› U„¾nþx�R„fÿúç�ß~ÔÖfp®°C¡`( …‚¡P0 †BÁP(˜PΦV*ßèpüÖM€ÄÏþz5 ¥Éø£8Étÿ{üø”ÀP( C¡`( þé»—_áó f0<åÁP( C¡`( …‚¡P0 †BÁP( C¡`( …‚¡P0 †BÁP( C¡`( …‚¡P0 †BÁP( C¡`( …‚¡P0 †BÁP( SùÐ? Û¦î‡ f9?ÆÍ¨,L¨+èž\ w›ûŠÐ‡¶ö1Ôý< [ÍÃ9²ë‚l”^þ�B�ï‘lYœ°½rÄJ_YÒY¿›ï=+˵“ óÕÛHü$íW�òÇ>HøÒkÛÓ]ß·×·Õe`ûö„5~J Uûë´ý„!_|1ß»µ¾´œ�íØ«Üóz&ÙØóªã¥�üïÅüóí|ñý|ïŸiæÓU“¯ÞPVvqz¶_1S:; hÀÓ¦¾º¾uûÑ|õ ”Sϼ.ýÒuË9(Ypwc¯eò½9_}›=B0›Ü—¯¾ï6�QN=“o\»ûÐKçuoss¾z\<Z/î¼8Nœ›]m›Óò­õ)ë¿«ŸÍ#4Ï_h€¢9ÙtÏ‚bTjMgïÌW_ñ³£éC¥ù²»…X¶tªó]7­Ôo¥”öì¿ìuÉæ¢Ý“´|©©±•‡ÊWßfÒ/²&'Á%3™ó²éj—}� ïØ�ÙA�© ]±ÞŒB1ç)ÿÅ©ô¤ƒÒ÷˜5B¿…‚¡P0 †BÁP( C¡`֩烟·ü5¡`( …‚ù“ô¡,r F( óg|&Ÿt`„‚¡P0 †BÁP( C¡`’-ðbô*k„Ò#Œí”§Q| %0 †BÁP( C¡`( …‚¡P0 †BÁP( C¡`( …‚¡P0 †BÁP( C¡`( …‚¡P0 †BÁP( C¡`( …‚¡P0 †BÁP( C¡`( …‚¡P0 †BÁ à’iü8}¡Y"WÒâ¶üòou¯jMIõf8(�‰…2˼�ÄÙR}§YB”ñ9HDìòŒ‡Óg:tutn2ù�Ž'�¼-BMñt|)n�û/Ê”þªQ^¶ðÚvx9d4 $ÃEÄm©å· ZF:;{ªÜÙ9ÃtÙ^öVO°Í�54?ÎY¤skúLØ$.Q ß:KèýnÎ>pqÙ8¥ÝžFwÂah»l Sâ\v�tæ—7¦·w¼t: äSOÑ*Z‡øN7úž!ÞÑþ·3f7˜Ó¹|)™H<½öÛwÇ%h&3à…&7øý¡`xƒ …‚¡P0 †BÁP(˜¨Îy,ˆ}®RIEND®B`‚urwid-urwid-69907e4/docs/tutorial/sig3.png000066400000000000000000000042461526073734300204530ustar00rootroot00000000000000‰PNG  IHDRPàp>aÄ cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñeIDATxÚí�Y¶¤ DéwÜû_ûè�r€”!B‹Ò¸ýº”! ÉÁ$ß6àiPP0ÌrM²Þ9÷ÎÖ9#è{Å@°øX=O)�û)×)E_•v!öûߎk\œs>ˆ”ÆyˆÚÞ~uJ?Np®·xý‰{ð�¢5Da-¡§F19�4Ò>>˜Ëz \6~.xÈüÏ…4@dÑç¿¥†Ü;çBÕþàEˆÜµt¶…�½�’ô»„�yxñ7%œ~¬ù4²óå$[ö‹XÙüŠå¥Au¦„餩$âxUL�mÍøâ¦xqº§ê—:¥§¾×>?ªÃ¦-€s5gM¹¬i¸ö‚µm8†¿µù(ßðñÌ4…É_6›½³xºS§Tk“ú-ÛbwÌä@Xíï74´œ#@ˆZ½^Ï]'w+e`Îú'öÝ´£·ñî44®¹¥¢Â�H}ï,fÙS­!_:*a÷mÓNy·~iFÇ5´íÄ_ùAu6ˆ#ƒõ1g¿Á*+Š*Ÿ»,‹¡yIùi¼þËnÞ³�5þÍñ¼î9ï^æx�|óXñJ&xbÿ 5Ý,%Ô=¦ÆO#èSôœ¥Sz³”ÐÇ0A§4Ùw (¨$UÐ<a•CAÁPP0¥6t`r}õ¼|îyÿ"›ÝÛm-½½þQ0½üÜ…æVû–h U|üV}¦¾FØ)�¡ `rmhºœ3·ž2Z¼³Ë¾³ÔUí}!—ŒÒ@…õŸ2~Ö>aO.ËNT%Ôþ8]'è½5�l|_8ŸY¿Z]<š·§EM®„ž^ýç�_ÿ)…N×?‡xõà±Y»t$ßùãëŠ4¹tiµ8¶/k�\³qMº]Hù@ªßJ=~´.1=ÈêKñ›¿[ݸ¡SÚ‹‡fm-þ¥ö ²˜âñÝðÅLôžïA¿¾ô>{¦(¡cìÃ�)Êé=‚^=·´¦¡=œ)�¹AÐÎÎÝ_›É¨=u`‚*>{ëþo›ÆT¿«0}É2jO™ÂŽû_]k|íc°ílûb»âï™ûv�=#€Jhc)íàL¹`4›ysf„³§—âÄãó‡+ìåÁPP0 CAÁ<àá˜Á*‡M`XåÁPP0 CAÁPP0 CAÁPP0 CAÁPP0 CAÁPP0 CAÁPP0 CAÁPP0 CAÁPP0 CAÁPP0Lᣅg{“§¬K(˜åøŒ›¥2Kú™MS k¾SºúÞL|ï ‚Njí4”õá—t-öŒt…l딦Øóè Tö §céö›…;–Û²¤¾éïý”V%Íõ6„>Â~U•߯AlqªÝ¼²Þ×ÃÛòKör öøÄ9þ媮×iÛ ƒ¿ø¬¿wk~2�ŽølYn~=…7ösÖéÑÊfRYÿóõͧÚþÞÅÞ æêŠÜüê¬Ol¿b¦t4@I‡§Ýv4¼5~¯¿zÊ©çŽK¿4n9:%‹W;æÉ÷bõu¶úƒE°ÊuþêðiåÔSìXv÷«Æüìþêqåy/¡åäŽÁ±tpnêtµ6Ëô­ù)ó¿ª�=—гøŒ sNÓ= Š^©6�½Ò_}AŸ Mª?›1‚%¦SÕwÝ´�ÿµí•ªSÚöÛ éÏV3!¢í•ÈôCI[z(õuº–Õ;ΛÃ3‡M£Mö|bߧΣ|�\�õaŠgVù/N¥Ú)}�§–ЯAAÁPP0 CAÁV޼téÀLéaâù:{¡™Ý¾¶¡`@G~¤øÜK(˜¥°Þ2mµ îé"ÿp5ÿòuÿðq|ÅzÓOà9ÚB M×Ú} ¶üËÇç½Èç_&žµä~”j•ׯϔþÌ“ðUï‘íüËG½!­»X ë-Cj¡y}e�áÅjB¹ôsöX rœW¯7½i½bƒB Í´v&{OëóÅ«Ûø|°½x/ÙséËa=7 ›êçåêûR~Ð ziMêHüû5»È÷Ç¡‹ÓÃ$¯‘çèPèn q­éÏÒ`fhWùÑõ™r}i ƒ{ø1{®E|´ üÖ­x>4Ct%²oF)?ü5th�Ô «îA›ÂÄõÝíU¾®è¨;v±¾TžU¤�[o:§´\ŠæûãЇAAÁPP0 CAÁPP0 CAÁPP0 CAÁPP0 CAÁPP0 CAÁPP0 CAÁPP0 CAÁPP0 CAÁPP0 CAÁPP0 CAÁPP0¨pÎ4~œ¶ ÜÔÄeþåߪ½JP«Kª7ÃN L*è¼[yÌ‹Ðl)ž©¦�ìðwÞþÓ5ö„yi‡q¸C7mèj£éˉÄ[K¨©<±©ÐÉSú«zù°¯õ‚—]ŒÆ“"[Ûˆ°ÖØàö=˜måÉî*÷éÅtY¶‚‹l@O¬{òþ9gÛ®z‹M=«ˆKÒèc77\|G¸¸S?¥�OE7ânh6uSâÜiŒtø—7º·w:DâESOõ^õ»§ˆZ¥Oƒ�^h{wÆÓf9——¢H¯õÀþøn‚Ö6¢~¡’+Ü? 0ƒ¡ `(( †‚‚¡ `þj]"1�¥`�IEND®B`‚urwid-urwid-69907e4/docs/tutorial/sig4.png000066400000000000000000000042341526073734300204510ustar00rootroot00000000000000‰PNG  IHDRPàp>aÄ cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñeIDATxÚí�[š« „9ó¹ ö¿4öq¢†;ÝPBꙉri+ HDúŸ3Éß§ Ø †‚‚9æk�1¿Ù;gý]1ÖWÏRJc^.e:¥èkò{;±½ÿv\ãaŒ±.*iœMÔ¶úë˜4(}9Θ^÷ú‹¾ƒM\k7"Âé¡I§:iëÌU}%p&›?—Üeþ3.LàYôú·Ô‘[cŒ«Úïl”"w-�ý_áÆÞzEÚ[ÂÎ:lô7Ä%ÎzÕÙr‘-û£\ÙúŠþÒ :S R‚Rq¬(§Ä¶fþèK±Ñ鞦_”œ_úÝV¬{}¨Þ6] âcšïÇ RçêØ�ŒSØ\ùz<õKô ê£zÂø_VU¯«4W;)³?[_Û<$ùÇ›¾` ¦¬Dó-ãî¯ÛÜMÞk5ç,¹¬©›{ÁÒ>ÃßÙ}”¿ðñÊ$Îd§ÍfŸtO“ Jµ>©ß²+wÇL„Öþ~C}AË5„¨µëóÜ<¹[%k–ÿbßí £�5É­ÙpË-à9¯wGj{g1Ç]jÍ…l阫¤½´MKên}VÒÌŽëhÛꢿñÿ œè¬‹Ž ¶Çœý «´š|î²4F¸æ%å3„ùú/»ùµ¨ ¬ño�ßëöyö²Æcä‡ïg²À/ö©iVñP³M‹_FÐ]ô\ePÚ‡U|Æ�9ÉtOƒ`TªMggÆ«/ès!éC]õc3‡Óä4¢ö.›ƒêŸÛ_‰¥k¿M~luQv'½’¸|W’FW*^}�M÷ÕFgÀEsØó¶i´Ë`Ã_ìûÔÙ*È ´?F¡Ø³Ép*½é ô9võÐ�AAÁPP0 CAÁVŽüèÒ9€™Òfây ººÓ¬nŸûP0 G¾Ä}€ æ(¬· {-Ex:/>\-¾|=>¼Ÿ_°Þô•x�޶à¡áz@}ŒÁV|yÿ¼�êIóÇ…gíŇ¥Úäåë3ãxæAújôÈvýå£VQÖS…õ–.´P½¾ÒÅé£Õ„ñjÐ×Ù÷•÷yñzÓ‡Ö+6(xh¦·SÙ›¬Ï�Ýúç�îÁ{Éž©‡å±oE)_ü5th�” kîNZÂÂíÝÜM¾®èh8öh}i|VPvn½éšÒr)˜Ï߇nCAÁPP0 CAÁPP0 CAÁPP0 CAÁPP0 CAÁPP0 CAÁPP0 CAÁPP0 CAÁPP0 CAÁPP0 Ó|×3~g�¯áÔi¿<*¸î;�‹À&†‚‚¡ `P;:à‚i|9mA¹=¨ŠiñåU{‘ Ú�T¿ %0¡ ¼m×ivÏTKvøK·ÿ4�=a¶"0ÞáÐUÚ¯Úhz:žx§‡ªüÉE› %‘Òj”w§{�|Üb4v˜Œ’°·õpg‹uæÞƒYçOúP¹»óvÓãüØJ`špîÉûgŒn»ê+7õŒ8E<‚@ž»¹áâoñSÒüTô†ÎÛ¦.7%Æ$÷HïøòÊðö†·No<ñ¼©§x¯ú;RD­ÑÿN‡^hswÆ(>€uÉ\ÞfJ§÷¿„þç»û´¶õ*yÒöPWûHbø3 †‚‚¡ `((ÝBÒä?—$rÁ°P7IEND®B`‚urwid-urwid-69907e4/docs/tutorial/smenu.py000066400000000000000000000025671526073734300206050ustar00rootroot00000000000000from __future__ import annotations import typing import urwid if typing.TYPE_CHECKING: from collections.abc import Iterable choices = ["Chapman", "Cleese", "Gilliam", "Idle", "Jones", "Palin"] def menu(title: str, choices_: Iterable[str]) -> urwid.ListBox: body = [urwid.Text(title), urwid.Divider()] for c in choices_: button = urwid.Button(c) urwid.connect_signal(button, "click", item_chosen, c) body.append(urwid.AttrMap(button, None, focus_map="reversed")) return urwid.ListBox(urwid.SimpleFocusListWalker(body)) def item_chosen(button: urwid.Button, choice: str) -> None: response = urwid.Text(["You chose ", choice, "\n"]) done = urwid.Button("Ok") urwid.connect_signal(done, "click", exit_program) main.original_widget = urwid.Filler( urwid.Pile( [ response, urwid.AttrMap(done, None, focus_map="reversed"), ] ) ) def exit_program(button: urwid.Button) -> None: raise urwid.ExitMainLoop() main = urwid.Padding(menu("Pythons", choices), left=2, right=2) top = urwid.Overlay( main, urwid.SolidFill("\N{MEDIUM SHADE}"), align=urwid.CENTER, width=(urwid.RELATIVE, 60), valign=urwid.MIDDLE, height=(urwid.RELATIVE, 60), min_width=20, min_height=9, ) urwid.MainLoop(top, palette=[("reversed", "standout", "")]).run() urwid-urwid-69907e4/docs/tutorial/smenu.py.xdotool000066400000000000000000000001661526073734300222650ustar00rootroot00000000000000windowsize --usehints $TERMWINDOWID 24 11 key --window $TERMWINDOWID Down Down Down key --window $TERMWINDOWID Return urwid-urwid-69907e4/docs/tutorial/smenu1.png000066400000000000000000000053121526073734300210110ustar00rootroot00000000000000‰PNG  IHDR€`—èñá cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñe BIDATxÚí�mÖ¤* „™{fÙÿÒØÇýÑ~¢€Œ ¤žs¦ßQi[‰`¡)ýAò'„ƒl‡ÿrºÇôè#`v0bö�Àé¾Ól`0TATAsÀ¡ ¢ š UUÐÜ0`¨‚¨‚æ†CD47 ª ª ¹aÀPQÍ †*ˆ*hn0"z &‡-Lu@ÞÁq€s0OÆäÅúý‹ØFuç}dýkõ_ø[;K?Gò¥'>èkõ«ÒÅß/ɬ•ü=½yÌ_ª [jõ«¯‚ö É±YȲ5{©dêüµÂòo¡?Ûê¼Ô|dû8üGž/ÿŠ*(C ×XÈ³¶¬…¶³øeùº–ñèw-èXÉR©®ÊÁ½,�!° Ê#"²×ÿ^OÏjLöú•B™�¢¨‚LÐaõX”TM” Q±ÆâzR3e†¸w�ú9¬T�r]�;̆Ü‹‡Oå•~]PÜÇ]­ä6ׂnNÁ/.•ÊrµuHú_ ºÝ I¯þ$KË{ÖºÿbéÔÅ'Kceñëß(/hÎîüæ9Ç$ƒž/!ä%·^ÂÀúŠ� eå·@ª 0ôÃq€s0ôØCŸ°g0TAÆP9‡CdU�g0TAÆP9‡CdU�g0TAÆP9‡CdU�g0TAÆP9‡CdU�g0ô€yí�©òbcùÑx{Û?3î\ãÓ58ŽœÃ€á“s�ññ´”ë&‘…NØŸÀJÎØ« °¯&N�.侮>v˜§“:¨ í±ÏÿutUP®�ùÒãÏûÑŽ*¨ø ™»Âéëä¼Éûƒädùw(‘/oŽ®ŠS낲Ot‹Ù™1)±¯"žŸ–ï×xøÉÎOWW||�’ j|°äòXËxꚎ?ö;gïËcˆ²Ï¸”OÖgÊòSO÷¶Çµ cAöþü~�= �ŸwŸ”¯½¯@‘m¯_óþ`ñTÕŸhŠ~�Í €Š z½­Ç’‡ÅçéwzÀ«ŒXýj[¡ñ.ÉøNšm òá.µ–·@ïÚ–† zß!¶~|jh9íuºThžÏ¥ÿ‚ò…]µ�XQ…VžÎ}Yúh\ì�Z$wÅkA¹Â²‘ò«Ê½òGËwÁíµ ì¶I,V׺XÒ&PYèí¹ÊÝi—›ò²�]ó»$—/ävUJ+�×þ€jb–”&o¿1TåÖªˆyAÎaÀ0/ȘZý²€$fÕè ¬ÀkÓÓßAƒú„�á8À9 ú„íqæXe#Bè€kêùÑE¥I 3õmüëݰ¯�ÁTè ‚¶»‘“ómè^ ÍÝ%|tS~ï„v¼'œïaꛜž¤“éË9yR×=Ù§¥<èùÓ{Æ—édM•{Ìö(¦¥(© Bªàíj$=Üãá\‘™Þº¼;-‰‘=T�BÃù~ýÝtš¹…Ë·‹AÔz¢÷]Ðk·Â¶'qsZœ¦—2ÙòÚ=)9TTP‡ŽÀßÈía2rôЫPZAizs†Å\yJÛ aQÒëK?`Ɉ7èÉ` Àï[ošA¬L§Ý=8ÞTP(6ƒ(:G®“�òÐÞ¨½ot¦ÛLÅLMÜ8ÈU4jëÞ»?Ô÷«¡1�už�óÀk›>Ͻ,ïK{sîxGì~Û¢\ŽÙuV¼[~)ŸÜD»Y_G”cÞå¦üÚb/ŒdùVÃçétî¥üÈ0/Èæ9‡ü {èð †éé`ØÀp` ÇÎaÀp`�3ÀºQkhS¿À½ೇý`º¼?`Ëî¹[_<–¸™¯þ€ÖÐ~¶ñôñ$ã&;}ü ŸˆÀür;·ºâx?C–gøuK–Ödj�æ¯JR%Û Åt %Ô’+ÖlØ»”ß³SÖi ²|˜Ó˜8ˆ? µ BÐK¹û— ÐK‡Õxxwx²)ßнO‡]ÅpÆÃ8m4€bF§‡êŸËpÝsôõНùZ¡?àZ#æþ/(« µ�˜Æ…þ¥& hŽ„õ®åN³›@Z¦ôAš4Ü^ Ên[”Ôç»ÿMRþc¸N§å·e˜ íÈo|Õ¦wYö‘ó ó‚Œa^�s0Ì ²‡þÏ0`èÆãc8p†ã{\ú~v¼„=i¤{eG/LZ˺øŠ ï¯ãÁÏ&Y¨‚Àtõ|‚ý·Ec­€×Nk4@®¬T–Ç jpÀYÿ߯lɶs�ÏøŠY%±VÊ�þ€Ó¶Ç…––{NjœÝà�Ñü�+ôϼþ|Ýà<˜cù¾†ò±ÏûÎ%+#1dê¹úÎVéàxÈIq®¯ ðØ¢Ü^ Ên›ìÖ@©âû|1¢?àvÎÍø<ïæ~æü ç0`˜dýžaÀІ- ÇÆpà Çöpà™.�É=Eðþ€�ãÁðuÆ+xC@+Ÿñl±];Äc¬“ÜWvÝs£Z$ÍõŒØ9OE’×ò`Zv¶‚Òµ ¹br>ä÷»`)�ÎØŸè¤ ¸ËŒ{ßA\ìéŒøöTÑÌ|߉„bC®˜ìõ)¥åÇ~zŸ¿ýÕè‰FñøNäXú"g‰Š3ùÖƒÍÍYx>€¯Œio*(ôiN"0œ?àRoÀíw|ím;ÛNÀšëßrÔœüÛ |àöZPvÛnÌÅv|ñ¸º¡3œ?`Ëõ�é¬ì®$þ€xYÁ‡`^�1Ì r†yAö0/È3 úÀ°€á8ÀŽœÃ€á8À·ïX7ïGLçÎzûl–ç¨ÐV:ú2·Ô}ÝmQÇ“? %©ašæÒO p«ý­|Á Û¶=Y|ç@2¾?@þñ+~z0Eâý@vñy¡,ÉwÚ@c ÀÏõÏs]tñ`îý•>@ôÒa;ú.¼±ÉÓâ_`\ÀÉ|7(£û?Ìéð‚²?@m$üðœtÙ“!Q<Î0ï�›².FY÷³­4Ü�ZÒÿOFlÙOÎ'o°¿¨ çÈíB¼™7!Ì 2†yAÎaÀ0/Èú<À¡? [ŽŒá8À9 Žìá8À3 ª c¨‚œÃ€¡ ²‡*È3 ª c¨‚œÃ€¡ ²‡*È3 ª c¨‚œÃ€¡ ²‡*È3 ª c¨‚œÃ€¡ ²‡*È3 úÀ°€©Ž8m;͆ódÀiÃi¶0 ª ª ¹aÀPQÍ †*ˆ*hn0TATAsÀ¡ ¢ š UUÐÜ0`¨‚¨‚æ†CD47 úÀü¸osX�ÈIEND®B`‚urwid-urwid-69907e4/docs/tutorial/smenu2.png000066400000000000000000000053231526073734300210140ustar00rootroot00000000000000‰PNG  IHDR€`—èñá cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñe KIDATxÚí�[š¬* …éóíYdþCË<ÎCy)ñ‚” +Èúª[E ‰à‚JàOAò—RÒ$ËÇ׿Ün±ýú ÌŸ¦$iýHÜn»Í†CD464ª ª ±¡ÀPQ� †*ˆ*hlh0TATAcC€¡ ¢  UUÐØÐ`¨‚¨‚Ɔó§è k˜b?€<ƒý€àÐ`îôÈ3.Ë÷"?´êÊsÈüKõþ•ÞÒ÷‘óÔ?ô¥ò5i‚ôóM2j!?ÁNéWkDnó�*È—RùÚ« Õhò]-dÊÍš*ÛÚžvqü]ØwÄ–2¿ª>²||ý#÷�¿Ct‚¦½-äóEÓ[[æDË[|w|¾J´ ú.d)—¦kãîŽkJl‚ÎYË-§{%&kùÊEš—b¨‚\ÐnõX”�J5,1�_ê}šàš.†£u¨¯ÃK—U¿ÝlÈ/búõi|Ñ×  ÒµßõÒB¾Âg,èàü`¨T¦ÑÖ.i?t˜ ÉG²£×ƒ=sÙ¿±‚4j‚ôÎQ-~cù;ù�ÙœB¿ à¸ Ó÷%¿ ì§‚ˆ`ùßÅA†²ðk`|ª 0Œp†ý€àÐ`àã„#C€¡ r†*(84ª ¨‚"C€¡ r†*(84ª ¨‚"C€¡ r†*(84ª ¨‚"C€¡ r†*(84ª ¨‚"C€a|Ö0ì8Ã~@ph0ìø`¶”,?´êJ#¼xîá‡4QAÛ5ƪMÖ(ç¡ß©‡½i ‚–©²zîïc;gÜÙÄp…)±º¢rò»†sÆ�·0zœX³3�×�i@Ù¤8OïŽí;̬ ’³ò—Ó�’¥X/!‡ë¤ÓôÛëù#v_e¤‚.&iÚ]fzX¿žñÍ2ßïìåÏ&CÇéÛÖ�ÛßÕb,HDRݤ†ÛõÖ7ĽkŸ¾´�1ªË]?Æh!7�iŠ5ðÂXÓ ÷m¢‚g£öYŠÐ£Ð{¹h¢‚> =¿Y.ž@?•ñw,oÏ›·T›Þ»±-‹õž7ˆµg‚_ –*Ȩ#vQ "¿KÁx`׬#v©Beóç:Ñòo©…¶@†óˆÛ®¢tX&*Sßé¼Äää\­Lß„°cA§y›W8;i^\ ¯…Ë)ÆÆÍi“åç{9Vª»®×xÉ{% _л~1£_Pph0\QÛ™Rù²€8f•«Î1> › 0ûï‚ý€àÐ`àO°ø€oWÂø4€ø€iÏ Ö`|@p†ñµ¼$>àð\=Û޽Ñ/õWÄŸ+gÛÙ•òãÍé;>`·3wð?Ù^*û€€ûw{|§Î*È =дu39Úþþ†üx[»Ã~ñ¼ š¢3~ÏÌr'ÓS�oOiNÓ#Ð)Fãù•bÄ”‰×s{Q|ÀþAÞnOm�äªË{ân~Eª–àŽ¼=>@ÈÕñ¶DSA©I|Nôly_|@.fJâéš~çfë°ýEìøQ_GC5ßµÝ�mÇkÙÞîÝoK}uްøøߥÏ~D+Ä8ó’ø=:¾”ðv;ß»Kß3ô r†~AÁ¡ÀÐ/ÈÆD†Ãø0¬`Øp†ý€àÐ`Øð'X|Àœ©Ù´y¼ÀqüÀkÆ€±øM¸ÈâÝst=ýNq°Ý9QãjMûÚÊÓ&>àz¦¡i0ñ ¼ >@÷/¬Ç;$íÖðÅV�™@ª<þ*É•l3 Ý1ŒTP�¯XuÀÞ.ýê�2oK’éÃ�J?ÀNâj‹ %;—»_2`çÛlý€wuÅž»ÃÎô°%B?­·øC�ÎÅ?V|ÀþÎÑão‹¨…ñûq�ˆ‚±š0ëˆÝ|'™—Ëž°ÝXÐÙköfÈÓ\�Ãõªò¦’Çù®3—Mûí<=רÏóÎÿx[W·©ÅA¿ gè ý‚üa|@dh0Œó¸#Vê ieúÞxúû{ÆesÿÀJ ûÁ¡Àp1gÚŽ†þ�·]\À`´0ÀW%´”/hpI•S]ŠàÓU[efCO*ﱡ ª0UÑ?:÷Ž„mkjè–R™V ÇSà eèƒc¤‚ª\KmÛT?4¦¦œÝ½Û*è·öâÒWHJ  ÈÇ&ÕÀÂ97U¼HJéjÅŸÏåDCV�é~kªÁ&*(h)ùrÓ¹‰ 2÷ ÛøØçÑ"DÉ®Aì;™l¡‚ìį+F¦æµ×h,h j`ÜGô_ÈmŸ²ÐëàW Ã…Ü Ç‚ÜFqê4H±F… :Í›®æÀÎYE·Ç×ãg2ý)fÙíZ6ähï;yPtÌ’«Íþ)ý‚‚C€¡_�3¥òe C€y@÷ôg0@ ã„�a? 84Æ ûÃ~@dšhÌÒ“µi>>àåDˆ¨÷ݾÀ4]?àô¸~ÀIR]6t3a\ž ØÄÓ¶ïF€[Êb*ÉöH~-9MÆÐ-Åh,èF¨£lùuÇW ßêôô½c™>1H¨ ìl¡‚ž7šr¿ lG)¦¦-–Óe?o‚DRJª÷3óå'WÇ¿Äi}>|î×¢%êeý€x½ÂÞÖ¨¼àæÄç[,óø3]¬0?laÞ†º8æúz°#Ðk š JmªA w Í:b§2hWnºö»ωõ´×Ýl= Ö�¹7U‘ëùÝ\ aÇ‚Nó–ݲžÀY °äîê?ݺÆ™ðŽ^&~Ð|×é­,Iu“ –7ÄÈ’·;‡�â�ô ?ž§³œ¶ÒFÕøŠç>ÕÝA™œï"Ô�J?À8ñ«=ç?÷uÑ.~‹¥ ÅäÝéFpL9QWعÃ6ŒØÅàõMOñš'ýÆl‚ï:¥÷ø€ÎßcÆDÁ8>Àp!·Ú¥ãÈð9kp€¤ Ñ˺yŸu©áñy¢Éýˆ-ëËyÏ*ÝŜ݂ìú9C¿ àÐ`èäã"C€a|Ö0ì8Ã~@ph0ìøÃ~@dh0TAÎP‡CäUPdh0TAÎP‡CäUPdh0TAÎP‡CäUPdh0TAÎP‡CäUPdh0ŒÃ¦Øà¶ï6kÌ�~··YÀÐ`¨‚¨‚ƆCD464ª ª ±¡ÀPQ� †*ˆ*hlh0TATAcC€¡ ¢  UUÐØÐ`æiÛhdÁtéäIEND®B`‚urwid-urwid-69907e4/docs/tutorial/smenu3.png000066400000000000000000000034311526073734300210130ustar00rootroot00000000000000‰PNG  IHDR€`—èñá cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿ‡�Ì¿ pHYs‡‡�åñe‘IDATxÚí�Yv«0•w² íiÚÇûðcFM7QÕGbù0š¡.6ø«pòUJi¥Þÿ<¼¤}DûŸ{ 0óÕJ©åçO¡}l›=À ˜Á‚° ±!3X46` ‚ƆÌ`AXÐØ€, 0ƒaAcCf° ,hlÀ „� ˜ùjî {€™Å:öA˜YSÀ>¨’!3X�,(0ƒéÁ‚’!3X�,(0ƒéÁ‚’!3X�,(0ƒéÁ‚’!3X�,(0ƒéÁ‚’!3Ü`†=À u€ê€pÀ u€ê€dÀ $ ‡Ì`Az° dÀ $ ‡Ì`Az° dÀ $ ‡Ì`Az° dÀ $ ‡Ì`Az° dÀ ÷˜a0C †: 0C ‡: 0ƒ‰Á‚Â!3X�,(0ƒ‰Á‚Â!3X�,(0ƒ‰Á‚Â!3X�,(0ƒ‰Á‚Â!3X�,(0ÃýfØÌPˆ¡‡ÌPèy»~¿Ý½›ëïA©ßÊ¥Û–¶?€úÓù‡—IÔŸ—y½û^:K/Òê¤å^ Í´R&ufiýö9UñR죕à³Y j¿¼†5|w° VKE™~aiýö² :9ÔÛ¼¯�öøvô‡áïo´ùéÍ´_ßXèíÒÀ§·ƒ.…X{ú_'ÿV¬�ú<|�´Ÿ§÷ÒÞ8Ãú8Òïý9䤶߂¦+åúòrÞ«ëÆ¿m�õ>üõˆvm?Oï>ütüŸ–ç×Þ­Ý­Ó[ž™üZP{øûùZ{l¶åá§í�³_œþ1'4Áµ ú³~VíÃK{ÚÜôêÜø+gX_ó›ïÏÚØE *—jìP:�reA‡.Óíœ}� ÷†z`IyÊËÑ·“Ejé½…nt,O‡çÞýì8½¿û‰XìÆ± A·­óiªKÃoœÞêñßLï£þô¥“-°XÐÔKaµ²p{½öz{kGo'}ì0½+ jur-¥ÕËœë›áßnsOÓ{ùkiü‡ m2½×.´5ÓëŠäÐ&ÿÞ—=¥Ü­¦µ Ó›ÎÏ£Öôo:·icãôöÀ÷‚Äü] ú#€¾¤‡û’!3Ü`†=À u€ê€pÀ u€ê€dÀ $ ‡Ì`Az° dÀ $ ‡Ì`Az° dÀ $ ‡Ì`Az° dÀ $ ‡Ì`Az° dÀ ÷˜Ù}£öÒÍmãðgcï¬ZÊt�·ÃQ„CfŽyZÊÀ$>3îØò„sL¯ø�+G<3îþ´™#8æ÷Öuf8À‚®‡|2˜¥¯µ2—ñ@ j¿¾}}Òm›ù½™&ÖW⺰ýðrÒõéz·Žw² <ó¬ëÿ²ZVGp„µÛœ¶ŽtÆõYÔÖíH´?€ÏЬ3¯ÿ²y7xC ÚÞ�“¯ÿËB¯:@bACÖ¸�Šš¿%ùáñÌEÃG§½YzXP¿âIØrÚ;è±ZK)möáÏó¿‰sRöé¸,eíµµu»´­GõÄ l^Ø·t¼ôfàV_O~$ÍCßkA}¯†ÖßÞœ?:�rè|¦;âCùÛOE>uýÔ¡nó+Ju¶5„5ñ½ pÀ ß s¬ÁfÀÌîûøzú>¸Aà ÷ ‹¡‡ÌpŸ°ê€dÀ $ ‡Ì`Az° dÀ $ ‡Ì`Az° dÀ $ ‡Ì`Az° dÀ $ ‡Ì`Az° dÀ _O7Ã`†:@ u@8`†:@u@2` ƒ…Cf° =XP2` ƒ…Cf° =XP2` ƒ…Cf° =XP2` ƒ…Cf° =XP2`†ṵ̂˜¡C˜¡ÐC� ˜Á‚Ä`Aá€,H” ˜Á‚Ä`Aá€,H” ˜Á‚Ä`Aá€,H” ˜Á‚Ä`Aá€,H” ˜áþ3ìfëÚÚ6{€0³¦ -l³˜!3X46` ‚ƆÌ`AXÐØ€, 0ƒaAcCf° ,hlÀ „� ˜Á‚° ±!3Ü`æ?6ƒ° ÝýIEND®B`‚urwid-urwid-69907e4/docs/tutorial/urwid_attr.py000066400000000000000000000007701526073734300216340ustar00rootroot00000000000000from __future__ import annotations import urwid def exit_on_q(key: str) -> None: if key in {"q", "Q"}: raise urwid.ExitMainLoop() palette = [ ("banner", "black", "light gray"), ("streak", "black", "dark red"), ("bg", "black", "dark blue"), ] txt = urwid.Text(("banner", " Hello World "), align="center") map1 = urwid.AttrMap(txt, "streak") fill = urwid.Filler(map1) map2 = urwid.AttrMap(fill, "bg") loop = urwid.MainLoop(map2, palette, unhandled_input=exit_on_q) loop.run() urwid-urwid-69907e4/docs/tutorial/urwid_attr.py.xdotool000066400000000000000000000002441526073734300233170ustar00rootroot00000000000000windowsize --usehints $TERMWINDOWID 21 7 windowsize --usehints $TERMWINDOWID 10 9 windowsize --usehints $TERMWINDOWID 30 3 windowsize --usehints $TERMWINDOWID 15 2 urwid-urwid-69907e4/docs/tutorial/urwid_attr1.png000066400000000000000000000007461526073734300220540ustar00rootroot00000000000000‰PNG  IHDRPà§{#+ cHRMz&€„ú€èu0ê`:˜pœºQ<PLTEîÍåååÿÿÿ?Í�‰bKGD�hÙQ pHYs‡‡�åñeDIDATxÚíÖ n‚0€a¯°+Ðô»ÿ݆Ji‹Nq±šèó$ Yé䥎ŸÃ¾ÀÏÓ‹Üë*ôcCSDäf»W7?ÅóCOŸÙ$Ý =6%ï m~{(´›/ô�ÐsÃ�sZCçËáZhꮪy~ʧ]e|‰èæç±¡Q®ÛMh”˜jÖqWO±$7óG†Î ѯLsÝt·¯%tOÇ¿Êíü4&tYÉã�ÓÿB/ç� ­ß^àrÛÐu|Ýyuþó¿ú�¡sfNe5ßZ–vGhÝûÕ7ü€Ðåç]¡õÃoÞžÖÐæö4]Ì}ߢ¤Ôí64ý/ ­�¾ÔoûGh ¬Ó×=ëüá/%yYšh¶ýKÉù –›ñæf™?ä¥d4¡B… Ý„Àøô\b” –0IEND®B`‚urwid-urwid-69907e4/docs/tutorial/urwid_attr2.png000066400000000000000000000007461526073734300220550ustar00rootroot00000000000000‰PNG  IHDR  Á&er cHRMz&€„ú€èu0ê`:˜pœºQ<PLTEîÍåååÿÿÿ?Í�‰bKGD�hÙQ pHYs‡‡�åñeDIDATxÚíÖmr‚0€a¯Ð+do�Üÿn†|,t¦lš*ŽïóCZǼ‚ÈíÀ ?Áé8žà ƒ’RŠfëJÊ/fèò ù� ?˜Oó ”³3Lý¼‚©|‘?(ù¥îâÜ>�ÿrÛžmee°v|¶/óòú`ßÕëu_5X§ÌŠic&v¨ó°¥ëv:XÏJŸšf;{é•‘zõš­óæpA‚?+8,ãö{äïÁýîùÏÁ`o ޫދ%¶§“þ°úÊO-g0ïK†�aå -g°=&Ëž†°·Cøk0Úß°¼?ÔUƒþ`Ãät0ç¢ô½[L߬gqyÐL›!è�6:àI0Íc°—žÞt]è J»äâÔu¡ëæœ~eÀé±;@”mò£ÑIEND®B`‚urwid-urwid-69907e4/docs/tutorial/urwid_attr3.png000066400000000000000000000007421526073734300220520ustar00rootroot00000000000000‰PNG  IHDRà`7=¼Ê cHRMz&€„ú€èu0ê`:˜pœºQ<PLTEîÍåååÿÿÿ?Í�‰bKGD�hÙQ pHYs‡‡�åñe@IDATxÚíØ nƒ0 €á^aWÀ7ˆï·H›vŒ=·ÿ'Mh!•øÉÊc·à%¼ý’åŸüô8 &˜`‚OƒEU‹Û^æ‹Î¼›Kû,xtEÙƒÝo_ ó N¼·œœ ‚ç ®—�GÁ®^u¾”mW¯#ªa~ɬý:{Ö~"–‘·îêã¢-ÝÍÏ\&®”»>…ÛV ¶qY?Uü|™;¸­ì ß ¾ŸŸ!xüUÆ€~2ŽÁ6n;Ο0¸äÅàš[¤¯nîà¾Ô‚•àŒÁva"øÜ~²�ƒ<½-Y°»--wós¯ËÜ“Æö,kªàñH(q-Gè˜n{l~ŠàÅþgaoºGûÑöDVܸû.´ùS¿<ü5‚ &˜`‚Ÿ:ðÞ*øû”!öÑòIEND®B`‚urwid-urwid-69907e4/docs/tutorial/urwid_attr4.png000066400000000000000000000006631526073734300220550ustar00rootroot00000000000000‰PNG  IHDRð@Ö±çÔ cHRMz&€„ú€èu0ê`:˜pœºQ<PLTEÍåååîÿÿÿVý³vbKGD�hÙQ pHYs‡‡�åñeIDATXÃíÕ�ƒ PW $+tÿÝŠJ A«àÙÖÚÏ]�køå¡E†Ô‡ÚP7Àÿ “ˆ°é[›Ë“ìÃSÆ{ð\43Ÿ›o]°Ëß žçÜXÓ/Áq{¬ÁävYÌOCZ�—ç>Xt_V°è‚BaÆ!­“¤%˜|êWnö‘{Üœë4þŠmžÚàt¥ãDt ^æ{àr·üDº¨Îõ<¸šß¿Õ�pd™ôjÏ�õÒ`¹œ7Ðeáô9 .áÍÇ)Ãæq ‹|ïD§.} ÓkXÁå¨#ßû#³€%žGr¾û%Áiébzÿ’˜O86uó¥|ÓKâÝ 0àÃ�/5À€ 0`À€ øBð�kú£?»çŽIEND®B`‚urwid-urwid-69907e4/docs/urwid-logo.png000066400000000000000000000050271526073734300200310ustar00rootroot00000000000000‰PNG  IHDR–2Fé…Û pHYsNN±Í3 tEXtComment‰*� ´IDATxœí R90ÿˆml0£‘ñµÿYnd K áaÃrp$ت}ã>c»¥±¬Ñ­ñ˜#¡ª‹²Ç’Z­¾[=õo[ÏO§w�ƒà'öw>pHºß²™$ý ¾ÓÚŠ>WœüG÷®Òä`óßÿr8ùwø+~àŸÅs寴ÛüPý¡¾ô3±¥lc�M}·@iµœPñ³-<·T™‡Ó³±D!*7€Ì|Ò§q–Љ5œÐ¿kå¸bü�Ì|Vž˜fö®~ÒÆ†Î’áL…£ ɧÉá=q%Ýo´þR~BîG)Ó«€%œ?ŠìëÔŽ2…´¿pΪ(KòšbÔÃ$@Ÿ$‰M˜Q(k�Šåà/|ÎôqâÀ¿ZQ:ŽTÒú(3"m¾!ôoà¥x¢|€z˜£4§s^Ž(qyiÂ'� ÃÁÞ”ƒ�V—å¯pÎpÚIû̃¯õ|6c~ý™[^À)J‡Ž©£—»¨‹yi¢õõá€þB�=øð@˜v“%Ì �þ´ù–û2óEèUšìð)Æ8±øÝÜ¦ŽŠ¾*ÃzžF|ºâ'š}ÑññÕÅsBŽ"èsàÖD|øòÏgóøú7ú9ÈîN9+±(!ïsS˜è8±yEÌæ„°²‚@®ÔNDFëÇçÒÂÞ•¼:±Ž!ä ^�ÐЊ†}õ®ÝÒ¨Kêæ~€Qååà®ñÝ=< …üÑê’þ+­.ÚÄ. ¨E­ ÿè&LN¯…éä¦þ+­­u.ÑÍN½¦µe+ÙÕE/…1äMíÄÒnKlbœ´¹Ãc>ÝOp’ZƒpP<ÔÇCæ–Ù«�ísJ)óÖ)Cv5"ú‘{kLm§“[$ý ‚NúH9ÌYM“w0W¸+…îØ]ùj@N< …Åà‰Â;Œ3«‹X?3èß°püÁf�àô…྅I3ì>—ùê|‡´¿DSG¥®U[…#Ä0rŸ?ˆ§pËEaç2ÎoIW@f Á§E®¸JÈÑ(d§ÉÁ�B}@ÿF¢ÿ=@¡6øFÛ–<ÂD»ûÚê(<„LBDHÆ.ú|ý„Ø-ˆBYfü´Aܨݧy s1œ>±èÜÉaŽBIRô)ƒaÃx0:†l²û}ŽAð¡¯l tÁº‚"éñp†x_ñÝAašìû)D€Ô&=ÆìfbÌпM'_[£ùé=t �¤Kì‚ÄÏ‘ÓÚ2Pds IëÄC¡M`ôTÈ� G­ã–RBOô‘JÒ(@.¹–@¡c[ú)8ÖMðAçÁ�{M9þ€ÂÓGL!¡ÇŽ)…Òå{iRZŒBP6ìkè\€ RÈM%­¯Cìa§ðs4…:Ç S˜6÷Œú£‘!\ü•ñ'ÙÒHã±Ì+wƒÜ…ù�9Š4¢ÏÆøSÞ–®¤�W� óXE®Mkšü©Aºß†Ÿ}Ášñ\°Â A³´K”øÆ1RŒáÑŸ™‡´²`ó¿ÆÒ“£•66eMPô‡Ï’-ž í3C×þʽ¥ë@Y=MN52 ÓéÝØ@4( c½%*…ƒ’&Köv�`$ó•\J¡G¡¼°�U<§È˜-�ãf'†žuB•R¼Xwœ5v“ k˜zo9Íê’æßAu ¦{é¡Ç¼‰9Ûš@^Ô@R ~/¯áð¼å¬µIêþ0à\± DÃz—ç":qt¾¡ ³ÄÝÓtÎ1Î1c- ˜Ðz6y&fs’¦<”:� Á�ÍB…Eáç§ð§‡'>zxb¡� %@‡| ›&ŠO׃µ^ïc­¦ÑŒ¢,ìœCÄL¯Xxµ�»¡ÇFtÎKl�»€$Ótîs!ý6¼‘½„ð¾¶á#Ö�›oÓä0ë…ì\”ÃBý¶&fÙþ^¢6ßòž´N0PÁ0ñÞ¢Ý�¬ËXO™‚¶×»r\£”Ã]­c>Ž…ò«¢b¸~ýY.ð°;#l:­o¨s�1`± Ñi�ðV5l²ðI€ÞÇÇBÑü8>H§^�7kë~Ç »¶ Ž@e ü“±2*åÉ;ƒ›F¡è]+½¥z�*Ò�vÎEChq «Ìg—…Ó{xAÖ>»3… ´"F¦S;Ø2�ö—¤GÌñ_è;6É¢¬…2„…üµIÖt{^$œÅ7ê;DbH$k Ä›;mú÷c í;?µ¿ Š#øuÖØLÈá€ä[Zñ£H›o ,ÔØ#Úd9z¢ÇÂ(óâõ“׈û}¢Õe`€i¬!EAIC4^^¸o|Çæ—faÒ,L/ÌE³°ù†9é:ñ"ƒ|Ši¼ßy(,,rRa(Šl¾õ±ØÆlŠeÔž¢W=c¡|= Fmcì,4©ÑH,$‡zà‚ükž…†›Ø_†…Ý+}°Í÷˜#Rnå&Öh}¸úËìî_µ¸r³0Å䮉u�­49`�)—rüelI+Ÿ…ú˜{g¡-Õ‹<_W¬ÁMåXQ´ò=YXŸÊba1\z‰ƒ°q‡õÑ«eáb Iƒ$M°Ðظka!¶¯ ß9Å„¯¶ŒØ�Ɔ;hæÿÂ8ÆqdØ0T]d–j»™ ¹Ä ï6´b9ßÓÏËÂVþ_#f¡­s¸DŠ—eƒµ µ%´ÒŒf‡oBx»uøAä+Ÿøÿ[Šõm³ÿ"0T‹Žµ·FŽD°jXØÆ°××"|¡¼¿´2—•~’w˜Å^³�hMòÐð IØF{@ÿF%€È¥+.¤ gò»�#vÆáƒu"RZµ¾°?Fö€ ­¯ã;ä=¦>£½×`ùœ’¤jžV—l-=¢6æhÚ Ýù`©°¼°sé½tއY¬k#‡QïïN°càÎÿ]Ãô.ÖrÓ#vY}‚§Ó>CÈzv3êåeÿ†ßê³Ø Êìà„oC»#R{|Êþk‡\a¡Ç Ðq¼_nl²¶×ÌÄ�E‡Àf‡îhyö.2óI¹i*¡ÀöAпEá®.9‚�ቅåZæðv©‘ቅ�þè�µ °ç¾IEND®B`‚urwid-urwid-69907e4/examples/000077500000000000000000000000001526073734300161155ustar00rootroot00000000000000urwid-urwid-69907e4/examples/asyncio_socket_server.py000077500000000000000000000203341526073734300230770ustar00rootroot00000000000000#!/usr/bin/env python """Demo of using urwid with Python asyncio.""" from __future__ import annotations import asyncio import logging import sys import typing import weakref from datetime import datetime import urwid from urwid.display.raw import Screen if typing.TYPE_CHECKING: from collections.abc import Callable # Mirrors the private urwid.display._raw_display_base.Screen._DecodedInput alias, which isn't exported. _DecodedInput = list[str | tuple[str, int, int, int] | tuple[typing.Literal["cursor position"], int, int]] logging.basicConfig() loop = asyncio.get_event_loop() # ----------------------------------------------------------------------------- # General-purpose setup code def build_widgets() -> urwid.Filler[urwid.Pile]: """Build the demo widget tree, with a self-updating clock and three text inputs. :returns: the top-level widget for the demo. """ input1 = urwid.Edit("What is your name? ") input2 = urwid.Edit("What is your quest? ") input3 = urwid.Edit("What is the capital of Assyria? ") inputs = [input1, input2, input3] def update_clock(widget_ref: weakref.ReferenceType[urwid.Text]) -> None: widget = widget_ref() if not widget: # widget is dead; the main loop must've been destroyed return widget.set_text(datetime.now().isoformat()) # noqa: DTZ005 # use naive time # Schedule us to update the clock again in one second loop.call_later(1, update_clock, widget_ref) clock = urwid.Text("") update_clock(weakref.ref(clock)) return urwid.Filler(urwid.Pile([clock, *inputs]), urwid.TOP) def unhandled(key: str | tuple[str, int, int, int]) -> None: """Exit the main loop when the user presses :kbd:`ctrl c`. :param key: the unhandled key or mouse event passed by :class:`urwid.MainLoop`. :raises urwid.ExitMainLoop: always, when ``key`` is :kbd:`ctrl c`. """ if key == "ctrl c": raise urwid.ExitMainLoop # ----------------------------------------------------------------------------- # Demo 1 def demo1() -> None: """Plain old urwid app. Just happens to be run atop asyncio as the event loop. Note that the clock is updated using the asyncio loop directly, not via any of urwid's facilities. """ main_widget = build_widgets() urwid_loop = urwid.MainLoop( main_widget, event_loop=urwid.AsyncioEventLoop(loop=loop), unhandled_input=unhandled, ) urwid_loop.run() # ----------------------------------------------------------------------------- # Demo 2 class AsyncScreen(Screen): """An urwid screen that speaks to an asyncio stream, rather than polling file descriptors. This is fairly limited; it can't, for example, determine the size of the remote screen. Fixing that depends on the nature of the stream. """ _pending_task: asyncio.Task[bytes] | None def __init__(self, reader: asyncio.StreamReader, writer: asyncio.WriteTransport, encoding: str = "utf-8") -> None: """Initialize with the asyncio stream reader and writer to speak the screen protocol over.""" self.reader = reader self.writer = writer self.encoding = encoding # AsyncScreen overrides every method that would touch the underlying input/output files, so the defaults # (sys.stdin/sys.stdout) used by the base Screen are never actually read from or written to. super().__init__() def write(self, data: str) -> None: """Encode ``data`` and write it to the asyncio stream writer.""" self.writer.write(data.encode(self.encoding)) def flush(self) -> None: """Do nothing; the underlying asyncio writer has no separate flush step.""" def hook_event_loop( self, event_loop: urwid.EventLoop, callback: Callable[[_DecodedInput, list[int]], typing.Any], ) -> None: """Pump input from the asyncio stream reader into the urwid event loop. :param event_loop: must be an :class:`urwid.AsyncioEventLoop`, since it needs asyncio's ``create_task``. :raises TypeError: if ``event_loop`` is not an :class:`urwid.AsyncioEventLoop`. """ if not isinstance(event_loop, urwid.AsyncioEventLoop): msg = "AsyncScreen requires an AsyncioEventLoop" raise TypeError(msg) # Wait on the reader's read coro, and when there's data to read, call # the callback and then wait again def pump_reader(fut: asyncio.Future[bytes] | None = None) -> None: if fut is None: # First call, do nothing pass elif fut.cancelled(): # This is in response to an earlier .read() call, so don't # schedule another one! return elif fut.exception(): pass else: try: self.parse_input(event_loop, callback, list(fut.result())) except urwid.ExitMainLoop: # This will immediately close the transport and thus the # connection, which in turn calls connection_lost, which # stops the screen and the loop self.writer.abort() # create_task() schedules a coroutine without using `yield from` or # `await`, which are syntax errors in Pythons before 3.5 self._pending_task = event_loop._loop.create_task(self.reader.read(1024)) self._pending_task.add_done_callback(pump_reader) pump_reader() def unhook_event_loop(self, event_loop: urwid.EventLoop) -> None: """Cancel the pending read task started by :meth:`hook_event_loop`.""" if self._pending_task: self._pending_task.cancel() self._pending_task = None class UrwidProtocol(asyncio.Protocol): """An asyncio protocol that drives an urwid UI over the connection.""" def connection_made(self, transport: asyncio.BaseTransport) -> None: """Start the urwid UI once a client connects. :param transport: the transport for the new connection; must support writing, since :class:`AsyncScreen` writes urwid's output straight back to it. :raises TypeError: if ``transport`` doesn't support writing. """ if not isinstance(transport, asyncio.WriteTransport): msg = "UrwidProtocol requires a write-capable transport" raise TypeError(msg) print("Got a client!") self.transport = transport # StreamReader is super convenient here; it has a regular method on our # end (feed_data), and a coroutine on the other end that will # faux-block until there's data to be read. We could also just call a # method directly on the screen, but this keeps the screen somewhat # separate from the protocol. self.reader = asyncio.StreamReader(loop=loop) screen = AsyncScreen(self.reader, transport) main_widget = build_widgets() self.urwid_loop = urwid.MainLoop( main_widget, event_loop=urwid.AsyncioEventLoop(loop=loop), screen=screen, unhandled_input=unhandled, ) self.urwid_loop.start() def data_received(self, data: bytes) -> None: """Feed received bytes to the stream reader driving :class:`AsyncScreen`.""" self.reader.feed_data(data) def connection_lost(self, exc: Exception | None) -> None: """Tear down the urwid UI once the client disconnects.""" print("Lost a client...") self.reader.feed_eof() self.urwid_loop.stop() def demo2() -> None: """Urwid app served over the network to multiple clients at once, using an asyncio Protocol.""" coro = loop.create_server(UrwidProtocol, port=12345) loop.run_until_complete(coro) print("OK, good to go! Try this in another terminal (or two):") print() print(" socat TCP:127.0.0.1:12345 STDIN,rawer") print() loop.run_forever() if __name__ == "__main__": if len(sys.argv) == 2: which: str | None = sys.argv[1] else: which = None if which == "1": demo1() elif which == "2": demo2() else: print("Please run me with an argument of either 1 or 2.") sys.exit(1) urwid-urwid-69907e4/examples/bigtext.py000077500000000000000000000222321526073734300201410ustar00rootroot00000000000000#!/usr/bin/env python # # Urwid BigText example program # Copyright (C) 2004-2009 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """Urwid example demonstrating use of the BigText widget.""" from __future__ import annotations import typing import urwid if typing.TYPE_CHECKING: from collections.abc import Callable class SwitchingPadding(urwid.Padding[urwid.BigText]): """Padding that switches its alignment based on the available width.""" def padding_values( self, size: tuple[int], # type: ignore[override] focus: bool, ) -> tuple[int, int]: """Return the left and right padding, aligning left or right depending on the available width. :param size: render size passed by the parent widget :param focus: whether the wrapped widget is in focus :returns: left and right padding widths """ maxcol = size[0] width, _height = self.original_widget.pack((), focus=focus) # urwid.BigText is FIXED size widget if maxcol > width: self.align = urwid.LEFT else: self.align = urwid.RIGHT return super().padding_values(size, focus) class BigTextDisplay: """Interactive demo application for the :class:`urwid.BigText` widget.""" palette: typing.ClassVar[list[tuple[str, str, str] | tuple[str, str, str, str]]] = [ ("body", "black", "light gray", "standout"), ("header", "white", "dark red", "bold"), ("button normal", "light gray", "dark blue", "standout"), ("button select", "white", "dark green"), ("button disabled", "dark gray", "dark blue"), ("edit", "light gray", "dark blue"), ("bigtext", "white", "black"), ("chars", "light gray", "black"), ("exit", "white", "dark cyan"), ] def create_radio_button( self, g: list[urwid.RadioButton], name: str, font: urwid.Font, fn: Callable[[urwid.RadioButton, bool], typing.Any], ) -> urwid.AttrMap[urwid.RadioButton]: """Create a radio button for selecting a font. :param g: radio button group to add the new button to :param name: label for the radio button :param font: font this button selects :param fn: callback connected to the button's ``change`` signal :returns: the radio button wrapped in an :class:`urwid.AttrMap` """ w = urwid.RadioButton(g, name, False, on_state_change=fn) w.font = font w = urwid.AttrMap(w, "button normal", "button select") return w def create_disabled_radio_button(self, name: str) -> urwid.AttrMap[urwid.Text]: """Create a disabled placeholder shown for fonts unavailable in the current encoding mode. :param name: name of the unavailable font :returns: the placeholder text wrapped in an :class:`urwid.AttrMap` """ w = urwid.Text(f" {name} (UTF-8 mode required)") w = urwid.AttrMap(w, "button disabled") return w def create_edit( self, label: str, text: str, fn: Callable[[urwid.Edit, str], typing.Any], ) -> urwid.AttrMap[urwid.Edit]: """Create an edit widget and connect it to a change callback. :param label: caption shown before the editable text :param text: initial contents of the edit widget :param fn: callback connected to the widget's ``change`` signal, also invoked immediately :returns: the edit widget wrapped in an :class:`urwid.AttrMap` """ w = urwid.Edit(label, text) urwid.connect_signal(w, "change", fn) fn(w, text) w = urwid.AttrMap(w, "edit") return w def set_font_event(self, w: urwid.RadioButton, state: bool) -> None: """Switch the displayed :class:`urwid.BigText` to the selected font. :param w: radio button whose state changed, carrying the ``font`` it selects :param state: new state of the radio button """ if state: self.bigtext.set_font(w.font) self.chars_avail.set_text(w.font.characters()) def edit_change_event(self, widget: urwid.Edit, text: str) -> None: """Update the :class:`urwid.BigText` display with the edited text. :param widget: edit widget that changed :param text: new contents of the edit widget """ self.bigtext.set_text(text) def setup_view( self, ) -> tuple[ urwid.Frame[urwid.AttrMap[urwid.ListBox[int]], urwid.AttrMap[urwid.Text], None], urwid.Overlay[urwid.BigText, urwid.Frame[urwid.AttrMap[urwid.ListBox[int]], urwid.AttrMap[urwid.Text], None]], ]: """Build the main view and the exit-confirmation overlay. :returns: the main frame, and an overlay of the exit prompt on top of that frame """ fonts = urwid.get_all_fonts() # setup mode radio buttons self.font_buttons: list[urwid.AttrMap[urwid.RadioButton] | urwid.AttrMap[urwid.Text]] = [] group: list[urwid.RadioButton] = [] utf8 = urwid.get_encoding_mode() == "utf8" for name, fontcls in fonts: font = fontcls() if font.utf8_required and not utf8: rb = self.create_disabled_radio_button(name) else: rb = self.create_radio_button(group, name, font, self.set_font_event) if fontcls == urwid.Thin6x6Font: chosen_font_rb = rb exit_font = font self.font_buttons.append(rb) # Create BigText self.bigtext = urwid.BigText("", None) # type: ignore[arg-type] # font assigned before render bt = urwid.BoxAdapter( urwid.Filler( urwid.AttrMap( SwitchingPadding(self.bigtext, urwid.LEFT, None), "bigtext", ), urwid.BOTTOM, None, 7, ), 7, ) # Create chars_avail cah = urwid.Text("Characters Available:") self.chars_avail = urwid.Text("", wrap=urwid.ANY) ca = urwid.AttrMap(self.chars_avail, "chars") chosen_font_rb.original_widget.set_state(True) # causes set_font_event call # Create Edit widget edit = self.create_edit("", "Urwid BigText example", self.edit_change_event) # ListBox chars = urwid.Pile([cah, ca]) fonts_pile = urwid.Pile([urwid.Text("Fonts:"), *self.font_buttons], focus_item=1) col = urwid.Columns([(16, chars), fonts_pile], 3, focus_column=1) bt_pile = urwid.Pile([bt, edit], focus_item=1) lines: list[urwid.widget.AbstractFlowWidget] = [bt_pile, urwid.Divider(), col] listbox = urwid.ListBox(urwid.SimpleListWalker(lines)) # Frame w: urwid.Frame[ urwid.AttrMap[urwid.ListBox[int]], urwid.AttrMap[urwid.Text], None, ] = urwid.Frame( body=urwid.AttrMap(listbox, "body"), header=urwid.AttrMap(urwid.Text("Urwid BigText example program - F8 exits."), "header"), ) # Exit message exit_w = urwid.Overlay( urwid.BigText(("exit", " Quit? "), exit_font), w, urwid.CENTER, None, urwid.MIDDLE, None, ) return w, exit_w def main(self) -> None: """Build the view and run the main loop until the user exits.""" self.view, self.exit_view = self.setup_view() self.loop = urwid.MainLoop(self.view, self.palette, unhandled_input=self.unhandled_input) self.loop.run() def unhandled_input(self, key: str | tuple[str, int, int, int]) -> bool | None: """Handle keys not consumed by the widgets: :kbd:`f8` to confirm exit, :kbd:`y`/:kbd:`n` to answer it. :param key: unhandled key or mouse event :returns: ``True`` if the key was handled, ``None`` otherwise :raises urwid.ExitMainLoop: when the user confirms the exit prompt with :kbd:`y` """ if key == "f8": self.loop.widget = self.exit_view return True if self.loop.widget != self.exit_view: return None if key in {"y", "Y"}: raise urwid.ExitMainLoop() if key in {"n", "N"}: self.loop.widget = self.view return True return None def main() -> None: """Run the BigText example program.""" BigTextDisplay().main() if __name__ == "__main__": main() urwid-urwid-69907e4/examples/browse.py000077500000000000000000000364521526073734300200050ustar00rootroot00000000000000#!/usr/bin/env python # # Urwid example lazy directory browser / tree view # Copyright (C) 2004-2011 Ian Ward # Copyright (C) 2010 Kirk McDonald # Copyright (C) 2010 Rob Lanphier # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """ Urwid example lazy directory browser / tree view. Features: - custom selectable widgets for files and directories - custom message widgets to identify access errors and empty directories - custom list walker for displaying widgets in a tree fashion - outputs a quoted list of files and directories "selected" on exit """ from __future__ import annotations import itertools import os import re import typing import urwid if typing.TYPE_CHECKING: from collections.abc import Hashable _TreeNode = typing.TypeVar("_TreeNode", bound="urwid.TreeNode[typing.Any]") class FlagFileWidget(urwid.TreeWidget[_TreeNode]): """Tree widget that can be flagged, with a highlighted appearance while flagged.""" # apply an attribute to the expand/unexpand icons unexpanded_icon = urwid.AttrMap(urwid.TreeWidget.unexpanded_icon, "dirmark") expanded_icon = urwid.AttrMap(urwid.TreeWidget.expanded_icon, "dirmark") def __init__(self, node: _TreeNode) -> None: """Initialize unflagged, wrapping the tree widget's display in an extra `AttrMap`.""" super().__init__(node) # insert an extra AttrWrap for our own use self._w = urwid.AttrMap(self._w, None) self.flagged = False self.update_w() def selectable(self) -> bool: """Return whether this widget can take focus.""" return True def keypress(self, size: tuple[int] | tuple[()], key: str) -> str | None: """Handle a keypress, passing any unhandled key on to :meth:`unhandled_keys`. :param size: render size passed by the parent widget :param key: key that was pressed :returns: the key if it was not handled, ``None`` otherwise """ if (unhandled := super().keypress(size, key)) is not None: unhandled = self.unhandled_keys(size, unhandled) else: return None return unhandled def unhandled_keys(self, size: tuple[int] | tuple[()], key: str) -> str | None: """Override this method to intercept keystrokes in subclasses. Default behavior: toggle flagged on space, ignore other keys. :param size: render size passed by the parent widget :param key: key that was not handled by the base widget :returns: the key if it was not handled, ``None`` otherwise """ if key == " ": self.flagged = not self.flagged self.update_w() return None return key def update_w(self) -> None: """Update the attributes of self.widget based on self.flagged.""" if self.flagged: self._w.attr_map = {None: "flagged"} # type: ignore[attr-defined] self._w.focus_map = {None: "flagged focus"} # type: ignore[attr-defined] else: self._w.attr_map = {None: "body"} # type: ignore[attr-defined] self._w.focus_map = {None: "focus"} # type: ignore[attr-defined] class FileTreeWidget(FlagFileWidget["FileNode"]): """Widget for individual files.""" def __init__(self, node: FileNode) -> None: """Initialize and register this widget under its file's path.""" super().__init__(node) path = node.get_value() add_widget(path, self) def get_display_text(self) -> str: """Return the file name to display.""" return typing.cast("str", self.get_node().get_key()) class EmptyWidget(urwid.TreeWidget["EmptyNode"]): """A marker for expanded directories with no contents.""" def get_display_text(self) -> str | tuple[Hashable, str] | list[str | tuple[Hashable, str]]: """Return the placeholder text shown for an empty directory.""" return ("flag", "(empty directory)") class ErrorWidget(urwid.TreeWidget["ErrorNode"]): """A marker for errors reading directories.""" def get_display_text(self) -> str | tuple[Hashable, str] | list[str | tuple[Hashable, str]]: """Return the placeholder text shown for a directory that could not be read.""" return ("error", "(error/permission denied)") class DirectoryWidget(FlagFileWidget["DirectoryNode"]): """Widget for a directory.""" def __init__(self, node: DirectoryNode) -> None: """Initialize, register this widget under its directory's path, and set its initial expanded state.""" super().__init__(node) path = node.get_value() add_widget(path, self) self.expanded = starts_expanded(path) self.update_expanded_icon() def get_display_text(self) -> str: """Return the directory name to display, or ``/`` for the root.""" node = self.get_node() if node.get_depth() == 0: return "/" return typing.cast("str", node.get_key()) class FileNode(urwid.TreeNode[str]): """Metadata storage for individual files.""" def __init__(self, path: str, parent: DirectoryNode | None = None) -> None: """Initialize with the key and depth derived from `path`.""" depth = path.count(dir_sep()) key = os.path.basename(path) super().__init__(path, key=key, parent=parent, depth=depth) def load_parent(self) -> DirectoryNode: """Return the (freshly loaded) parent directory node.""" parentname, _myname = os.path.split(self.get_value()) parent = DirectoryNode(parentname) parent.set_child_node(self.get_key(), self) return parent def load_widget(self) -> FileTreeWidget: """Return a new widget for this file.""" return FileTreeWidget(self) class EmptyNode(urwid.TreeNode[None]): """Placeholder child of a directory with no contents.""" def load_widget(self) -> EmptyWidget: """Return a new widget for this placeholder.""" return EmptyWidget(self) class ErrorNode(urwid.TreeNode[None]): """Placeholder child of a directory that could not be read.""" def load_widget(self) -> ErrorWidget: """Return a new widget for this placeholder.""" return ErrorWidget(self) class DirectoryNode(urwid.ParentNode[str]): """Metadata storage for directories.""" def __init__(self, path: str, parent: DirectoryNode | None = None) -> None: """Initialize with the key and depth derived from `path`, with no children read yet.""" if path == dir_sep(): depth = 0 key = None else: depth = path.count(dir_sep()) key = os.path.basename(path) super().__init__(path, key=key, parent=parent, depth=depth) self.dir_count = 0 self.read_error = False def load_parent(self) -> DirectoryNode: """Return the (freshly loaded) parent directory node.""" parentname, _myname = os.path.split(self.get_value()) parent = DirectoryNode(parentname) parent.set_child_node(self.get_key(), self) return parent def load_child_keys(self) -> list[str] | list[None]: """Return the sorted directory-entry keys, or ``[None]`` for an empty or unreadable directory.""" dirs: list[str] = [] files: list[str] = [] try: path = self.get_value() # separate dirs and files for a in os.listdir(path): if os.path.isdir(os.path.join(path, a)): dirs.append(a) else: files.append(a) except OSError: # the placeholder child itself is built by load_child_node self.read_error = True self.dir_count = 0 return [None] # sort dirs and files dirs.sort(key=alphabetize) files.sort(key=alphabetize) # store where the first file starts self.read_error = False self.dir_count = len(dirs) # collect dirs and files together again keys: list[str] | list[None] = dirs + files if len(keys) == 0: keys = [None] return keys def load_child_node( self, key: str | None, # type: ignore[override] # We have explicit type ) -> EmptyNode | ErrorNode | DirectoryNode | FileNode: """Return the child node for a directory entry. :param key: entry name, or ``None`` for the placeholder child of an empty or unreadable directory :returns: a placeholder node, a :class:`FileNode`, or a :class:`DirectoryNode` """ if key is None: depth = self.get_depth() + 1 if self.read_error: return ErrorNode(None, parent=self, key=None, depth=depth) return EmptyNode(None, parent=self, key=None, depth=depth) path = os.path.join(self.get_value(), key) if self.get_child_index(key) < self.dir_count: return DirectoryNode(path, parent=self) return FileNode(path, parent=self) def load_widget(self) -> DirectoryWidget: """Return a new widget for this directory.""" return DirectoryWidget(self) class DirectoryBrowser: """Interactive lazy directory tree browser.""" palette: typing.ClassVar[list[tuple[str, str, str] | tuple[str, str, str, str]]] = [ ("body", "black", "light gray"), ("flagged", "black", "dark green", "bold,underline"), ("focus", "light gray", "dark blue", "standout"), ("flagged focus", "yellow", "dark cyan", "bold,standout,underline"), ("head", "yellow", "black", "standout"), ("foot", "light gray", "black"), ("key", "light cyan", "black", "underline"), ("title", "white", "black", "bold"), ("dirmark", "black", "dark cyan", "bold"), ("flag", "dark gray", "light gray"), ("error", "dark red", "light gray"), ] footer_text: typing.ClassVar[list[tuple[str, str] | str]] = [ ("title", "Directory Browser"), " ", ("key", "UP"), ",", ("key", "DOWN"), ",", ("key", "PAGE UP"), ",", ("key", "PAGE DOWN"), " ", ("key", "SPACE"), " ", ("key", "+"), ",", ("key", "-"), " ", ("key", "LEFT"), " ", ("key", "HOME"), " ", ("key", "END"), " ", ("key", "Q"), ] def __init__(self) -> None: """Initialize the tree view rooted at the current working directory.""" cwd = os.getcwd() store_initial_cwd(cwd) self.header = urwid.Text("") self.listbox = urwid.TreeListBox(urwid.TreeWalker(DirectoryNode(cwd))) self.listbox.offset_rows = 1 self.footer = urwid.AttrMap(urwid.Text(self.footer_text), "foot") self.view = urwid.Frame( urwid.AttrMap( urwid.ScrollBar( self.listbox, thumb_char=urwid.ScrollBar.Symbols.FULL_BLOCK, trough_char=urwid.ScrollBar.Symbols.LITE_SHADE, ), "body", ), header=urwid.AttrMap(self.header, "head"), footer=self.footer, ) def main(self) -> None: """Run the program.""" self.loop = urwid.MainLoop(self.view, self.palette, unhandled_input=self.unhandled_input) self.loop.run() # on exit, write the flagged filenames to the console names = [escape_filename_sh(x) for x in get_flagged_names()] print(" ".join(names)) def unhandled_input(self, k: str | tuple[str, int, int, int]) -> None: """Handle keys not consumed by the widgets: :kbd:`q` to exit. :param k: unhandled key or mouse event :raises urwid.ExitMainLoop: when the user presses :kbd:`q` """ # update display of focus directory if k in {"q", "Q"}: raise urwid.ExitMainLoop() def main() -> None: """Run the directory browser example program.""" DirectoryBrowser().main() ####### # global cache of widgets _widget_cache: dict[str, DirectoryWidget | FileTreeWidget] = {} def add_widget(path: str, widget: DirectoryWidget | FileTreeWidget) -> None: """Add the widget for a given path.""" _widget_cache[path] = widget def get_flagged_names() -> list[str]: """Return a list of all filenames marked as flagged.""" names = [w.get_node().get_value() for w in _widget_cache.values() if w.flagged] return names ###### # store path components of initial current working directory _initial_cwd: list[str] = [] def store_initial_cwd(name: str) -> None: """Store the initial current working directory path components.""" _initial_cwd.clear() _initial_cwd.extend(name.split(dir_sep())) def starts_expanded(name: str) -> bool: """Return True if directory is a parent of initial cwd.""" if name == "/": return True path_elements = name.split(dir_sep()) if len(path_elements) > len(_initial_cwd): return False return path_elements == _initial_cwd[: len(path_elements)] def escape_filename_sh(name: str) -> str: """Return a hopefully safe shell-escaped version of a filename.""" # check whether we have unprintable characters for ch in name: if ord(ch) < 32: # found one so use the ansi-c escaping return escape_filename_sh_ansic(name) # all printable characters, so return a double-quoted version name = name.replace("\\", "\\\\").replace('"', '\\"').replace("`", "\\`").replace("$", "\\$") return f'"{name}"' def escape_filename_sh_ansic(name: str) -> str: """Return an ansi-c shell-escaped version of a filename.""" out = [] # gather the escaped characters into a list for ch in name: if ord(ch) < 32: out.append(f"\\x{ord(ch):02x}") elif ch == "\\": out.append("\\\\") else: out.append(ch) # slap them back together in an ansi-c quote $'...' return f"$'{''.join(out)}'" SPLIT_RE = re.compile(r"[a-zA-Z]+|\d+") def alphabetize(s: str) -> list[tuple[str, int]]: """Return a sort key that orders embedded numbers numerically rather than lexically. :param s: string to build a sort key for :returns: list of (lower-cased text, number) pairs usable as a sort key """ listing: list[tuple[str, int]] = [] for isdigit, group in itertools.groupby(SPLIT_RE.findall(s), key=str.isdigit): if isdigit: listing.extend(("", int(n)) for n in group) else: listing.append(("".join(group).lower(), 0)) return listing def dir_sep() -> str: """Return the separator used in this os.""" return getattr(os.path, "sep", "/") if __name__ == "__main__": main() urwid-urwid-69907e4/examples/calc.py000077500000000000000000001050601526073734300173760ustar00rootroot00000000000000#!/usr/bin/env python # # Urwid advanced example column calculator application # Copyright (C) 2004-2009 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """ Urwid advanced example column calculator application. Features: - multiple separate list boxes within columns - custom edit widget for editing calculator cells - custom parent widget for links to other columns - custom list walker to show and hide cell results as required - custom wrap and align modes for editing right-1 aligned numbers - outputs commands that may be used to recreate expression on exit """ from __future__ import annotations import operator import string import typing import urwid if typing.TYPE_CHECKING: from collections.abc import Hashable, Iterable from typing_extensions import Literal # use appropriate Screen class if urwid.display.web.is_web_request(): Screen = urwid.display.web.Screen else: Screen = urwid.display.raw.Screen def div_or_none(a: float, b: float) -> float | None: """Divide a by b. Return result or None on divide by zero.""" if b == 0: return None return a / b # operators supported and the functions used to calculate a result OPERATORS: dict[str, typing.Callable[[int | float, int | float], int | float | None]] = { "+": operator.add, "-": operator.sub, "*": operator.mul, "/": div_or_none, } # the uppercase versions of keys used to switch columns COLUMN_KEYS = list("?ABCDEF") # these lists are used to determine when to display errors EDIT_KEYS = list(OPERATORS.keys()) + COLUMN_KEYS + ["backspace", "delete"] MOVEMENT_KEYS = ["up", "down", "left", "right", "page up", "page down"] # Event text E_no_such_column = "Column %s does not exist." E_no_more_columns = "Maxumum number of columns reached." E_new_col_cell_not_empty = "Column must be started from an empty cell." E_invalid_key = "Invalid key '%s'." E_no_parent_column = "There is no parent column to return to." E_cant_combine = "Cannot combine cells with sub-expressions." E_invalid_in_parent_cell = "Cannot enter numbers into parent cell." E_invalid_in_help_col: list[str | tuple[Hashable, str]] = [ "Help Column is in focus. Press ", ("key", COLUMN_KEYS[1]), "-", ("key", COLUMN_KEYS[-1]), " to select another column.", ] # Shared layout object CALC_LAYOUT: CalcNumLayout | None = None class CalcEvent(Exception): """Events triggered by user input.""" attr = "event" def __init__(self, message: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]]) -> None: """Initialize with the message to display for this event.""" self.message = message def widget(self) -> urwid.AttrMap[urwid.Text]: """Return a widget containing event information.""" text = urwid.Text(self.message, urwid.CENTER) return urwid.AttrMap(text, self.attr) class ColumnDeleteEvent(CalcEvent): """Sent when user wants to delete a column.""" attr = "confirm" def __init__(self, letter: str, from_parent: int = 0) -> None: """Initialize with the letter of the column pending removal.""" super().__init__(["Press ", ("key", "BACKSPACE"), " again to confirm column removal."]) self.letter = letter class UpdateParentEvent(Exception): """Sent when parent columns may need to be updated.""" class Cell: """A single number, or sub-expression, within a calculator column.""" def __init__(self, op: str | None) -> None: """Initialize an empty cell for the given operator, or the top cell when `op` is None.""" self.op = op self.is_top = op is None self.child: CellColumn | None = None self.setup_edit() self.result = urwid.Text("", layout=CALC_LAYOUT) def show_result(self, next_cell: Cell | None) -> bool: """Return whether this widget should display its result. next_cell -- the cell following self or None """ if self.is_top: return False if next_cell is None: return True return not (self.op == next_cell.op == "+") def setup_edit(self) -> None: """Create the standard edit widget for this cell.""" self.edit = urwid.IntEdit() if not self.is_top: self.edit.set_caption(f"{self.op} ") self.edit.set_layout(self.edit.align, self.edit.wrap, CALC_LAYOUT) def get_value(self) -> int | None: """Return the numeric value of the cell.""" if self.child is not None: return self.child.get_result() return int(f"0{self.edit.edit_text}") def get_result(self) -> int | None: """Return the numeric result of this cell's operation.""" if self.is_top: return self.get_value() if not self.result.text: return None return int(self.result.text) def set_result(self, result: float | None) -> None: """Set the numeric result for this cell.""" if result is None: self.result.set_text("") else: self.result.set_text(f"{result:d}") def become_parent(self, column: CellColumn, letter: str) -> None: """Change the edit widget to a parent cell widget.""" self.child = column self.edit = ParentEdit(self.op, letter) def remove_child(self) -> None: """Change the edit widget back to a standard edit widget.""" self.child = None self.setup_edit() def is_empty(self) -> bool: """Return True if the cell is "empty".""" return self.child is None and not self.result.text class ParentEdit(urwid.Edit): """Edit widget modified to link to a child column.""" def __init__(self, op: str | None, letter: str) -> None: """Use the operator and letter of the child column as caption. op -- operator or None letter -- letter of child column remove_fn -- function to call when user wants to remove child function takes no parameters """ super().__init__(layout=CALC_LAYOUT) self.op = op self.set_letter(letter) def set_letter(self, letter: str) -> None: """Set the letter of the child column for display.""" self.letter = letter caption = f"({letter})" if self.op is not None: caption = f"{self.op} {caption}" self.set_caption(caption) def keypress(self, size: tuple[int], key: str) -> str | None: """Disable usual editing, allow only removing of child. :raises ColumnDeleteEvent: when `key` is :kbd:`backspace`, to remove this column. :raises CalcEvent: when `key` is a digit, since numbers cannot be entered into a parent cell. """ if key == "backspace": raise ColumnDeleteEvent(self.letter, from_parent=True) if key in string.digits: raise CalcEvent(E_invalid_in_parent_cell) return key class CellWalker(urwid.ListWalker[tuple[int, int], urwid.Divider | urwid.AttrMap[urwid.IntEdit] | urwid.Text]): """ListWalker-compatible class for browsing the cells, dividers and results within a column. Positions are ``(cell index, sub-position)`` tuples, where sub-position is 0 for the cell's edit widget, 1 for the divider below it, and 2 for its result. """ def __init__(self, content: Iterable[Cell]) -> None: """Initialize walking over `content`, focused on the first cell's edit widget.""" self.content = urwid.MonitoredList(content) self.content.set_modified_callback(self._modified) self.focus = (0, 0) # everyone can share the same divider widget self.div = urwid.Divider("-") def get_cell(self, i: int) -> Cell | None: """Return the cell at index `i`, or None if `i` is out of range.""" if i < 0 or i >= len(self.content): return None return self.content[i] def _get_at_pos( self, pos: tuple[int, int], ) -> tuple[urwid.Divider | urwid.AttrMap[urwid.IntEdit] | urwid.Text, tuple[int, int]] | tuple[None, None]: """Return the widget and position passed, or (None, None) if `pos` is out of range.""" i, sub = pos assert sub in {0, 1, 2} # noqa: S101 # for examples "assert" is acceptable if i < 0 or i >= len(self.content): return None, None if sub == 0: edit = self.content[i].edit return urwid.AttrMap(edit, "edit", "editfocus"), pos if sub == 1: return self.div, pos return self.content[i].result, pos def get_focus( self, ) -> tuple[urwid.Divider | urwid.AttrMap[urwid.IntEdit] | urwid.Text, tuple[int, int]] | tuple[None, None]: """Return the widget and position for the current focus.""" return self._get_at_pos(self.focus) def set_focus(self, focus: tuple[int, int]) -> None: """Set the focus position.""" self.focus = focus def get_next( self, position: tuple[int, int], ) -> tuple[urwid.Divider | urwid.AttrMap[urwid.IntEdit] | urwid.Text, tuple[int, int]] | tuple[None, None]: """Return the widget and position after the given position.""" i, sub = position assert sub in {0, 1, 2} # noqa: S101 # for examples "assert" is acceptable if sub == 0: show_result = self.content[i].show_result(self.get_cell(i + 1)) if show_result: return self._get_at_pos((i, 1)) return self._get_at_pos((i + 1, 0)) if sub == 1: return self._get_at_pos((i, 2)) return self._get_at_pos((i + 1, 0)) def get_prev( self, position: tuple[int, int], ) -> tuple[urwid.Divider | urwid.AttrMap[urwid.IntEdit] | urwid.Text, tuple[int, int]] | tuple[None, None]: """Return the widget and position before the given position.""" i, sub = position assert sub in {0, 1, 2} # noqa: S101 # for examples "assert" is acceptable if sub == 0: if i == 0: return None, None show_result = self.content[i - 1].show_result(self.content[i]) if show_result: return self._get_at_pos((i - 1, 2)) return self._get_at_pos((i - 1, 0)) if sub == 1: return self._get_at_pos((i, 0)) return self._get_at_pos((i, 1)) class CellColumn( urwid.WidgetWrap[urwid.Frame[urwid.ListBox[tuple[int, int]], urwid.AttrMap[urwid.Text], None]], ): """A single lettered column of calculator cells.""" def __init__(self, letter: str) -> None: """Initialize with a single empty top cell, headed by `letter`.""" self.walker = CellWalker([Cell(None)]) self.content = self.walker.content self.listbox = urwid.ListBox(self.walker) self.set_letter(letter) super().__init__(self.frame) def set_letter(self, letter: str) -> None: """Set the column header with letter.""" self.letter = letter header = urwid.AttrMap(urwid.Text(["Column ", ("key", letter)], layout=CALC_LAYOUT), "colhead") self.frame: urwid.Frame[urwid.ListBox[tuple[int, int]], urwid.AttrMap[urwid.Text], None] = urwid.Frame( self.listbox, header, ) def _focus_pos(self) -> tuple[int, int]: """Return the current focus position. :raises AssertionError: if the walker has no focus, which never happens once a column has its first cell. """ _widget, pos = self.walker.get_focus() assert pos is not None # noqa: S101 # for examples "assert" is acceptable return pos def _cell(self, index: int) -> Cell: """Return the cell at the given position. :raises AssertionError: if `index` is out of range, which the callers below never pass. """ cell = self.walker.get_cell(index) assert cell is not None # noqa: S101 # for examples "assert" is acceptable return cell def keypress(self, size: tuple[int, int], key: str) -> str | None: """Handle a keystroke within this column, splitting, combining or removing cells as needed. :raises UpdateParentEvent: when a cell's result changed, so any parent column can be refreshed. :raises ColumnDeleteEvent: on :kbd:`backspace` at the first cell, asking the caller to remove this column. :raises CalcEvent: on an operation that would combine cells across a sub-expression boundary. """ frame_key = self.frame.keypress(size, key) if frame_key is None: changed = self.update_results() if changed: raise UpdateParentEvent() return None key = frame_key i, sub = self._focus_pos() if sub != 0: # f is not an edit widget return key if key in OPERATORS: # move trailing text to new cell below edit = self._cell(i).edit cursor_pos = edit.edit_pos tail = edit.edit_text[cursor_pos:] edit.set_edit_text(edit.edit_text[:cursor_pos]) new_cell = Cell(key) new_cell.edit.edit_text = tail self.content[i + 1 : i + 1] = [new_cell] changed = self.update_results() self.move_focus_next(size) self.content[i + 1].edit.set_edit_pos(0) if changed: raise UpdateParentEvent() return None if key == "backspace": # unhandled backspace, we're at beginning of number # append current number to cell above, removing operator above = self.walker.get_cell(i - 1) if above is None: # we're the first cell raise ColumnDeleteEvent(self.letter, from_parent=False) edit = self._cell(i).edit # check that we can combine if above.child is not None: # cell above is parent if edit.edit_text: # ..and current not empty, no good raise CalcEvent(E_cant_combine) above_pos = 0 else: # above is normal number cell above_pos = len(above.edit.edit_text) above.edit.set_edit_text(above.edit.edit_text + edit.edit_text) self.move_focus_prev(size) self.content[i - 1].edit.set_edit_pos(above_pos) del self.content[i] changed = self.update_results() if changed: raise UpdateParentEvent() return None if key == "delete": # pull text from next cell into current cell = self._cell(i) below = self.walker.get_cell(i + 1) if cell.child is not None: # this cell is a parent raise CalcEvent(E_cant_combine) if below is None: # nothing below return key if below.child is not None: # cell below is a parent raise CalcEvent(E_cant_combine) edit = self._cell(i).edit edit.set_edit_text(edit.edit_text + below.edit.edit_text) del self.content[i + 1] changed = self.update_results() if changed: raise UpdateParentEvent() return None return key def move_focus_next(self, size: tuple[int, int]) -> None: """Move the focus to the next edit widget, skipping over dividers and results.""" i, _sub = self._focus_pos() assert i < len(self.content) - 1 # noqa: S101 # for examples "assert" is acceptable ni = i while ni == i: self.frame.keypress(size, "down") ni, _nsub = self._focus_pos() def move_focus_prev(self, size: tuple[int, int]) -> None: """Move the focus to the previous edit widget, skipping over dividers and results.""" i, _sub = self._focus_pos() assert i > 0 # noqa: S101 # for examples "assert" is acceptable ni = i while ni == i: self.frame.keypress(size, "up") ni, _nsub = self._focus_pos() def update_results(self, start_from: Cell | None = None) -> bool: """Update column. Return True if final result changed. start_from -- Cell to start updating from or None to start from the current focus (default None) """ if start_from is None: i, _sub = self._focus_pos() else: i = self.content.index(start_from) focus_cell = self._cell(i) x: int | float | None if focus_cell.is_top: x = focus_cell.get_value() else: last_cell = self._cell(i - 1) x = last_cell.get_result() if x is not None and focus_cell.op is not None: value = focus_cell.get_value() assert value is not None # noqa: S101 # for examples "assert" is acceptable x = OPERATORS[focus_cell.op](x, value) focus_cell.set_result(x) for cell in self.content[i + 1 :]: if cell.op is None: x = None elif x is not None: value = cell.get_value() assert value is not None # noqa: S101 # for examples "assert" is acceptable x = OPERATORS[cell.op](x, value) if cell.get_result() == x: return False cell.set_result(x) return True def create_child(self, letter: str) -> tuple[Cell, CellColumn] | tuple[None, None]: """Return (parent cell,child column) or None,None on failure.""" i, sub = self._focus_pos() if sub != 0: # f is not an edit widget return None, None cell = self._cell(i) if cell.child is not None: raise CalcEvent(E_new_col_cell_not_empty) if cell.edit.edit_text: raise CalcEvent(E_new_col_cell_not_empty) child = CellColumn(letter) cell.become_parent(child, letter) return cell, child def is_empty(self) -> bool: """Return True if this column is empty.""" return len(self.content) == 1 and self.content[0].is_empty() def get_expression(self) -> str: """Return the expression as a printable string.""" lines = [] for c in self.content: if c.op is not None: # only applies to first cell lines.append(c.op) if c.child is not None: lines.append(f"({c.child.get_expression()})") else: lines.append(f"{c.get_value():d}") return "".join(lines) def get_result(self) -> int | None: """Return the result of the last cell in the column.""" return self.content[-1].get_result() class HelpColumn(urwid.Widget): """Column displaying the calculator's key bindings and usage notes.""" _selectable = True _sizing = frozenset((urwid.BOX,)) help_text: list[str | tuple[str, str] | list[str | tuple[str, str]]] = [ # noqa: RUF012 # typing is too complex ("title", "Column Calculator"), "", ["Numbers: ", ("key", "0"), "-", ("key", "9")], "", ["Operators: ", ("key", "+"), ", ", ("key", "-"), ", ", ("key", "*"), " and ", ("key", "/")], "", ["Editing: ", ("key", "BACKSPACE"), " and ", ("key", "DELETE")], "", [ "Movement: ", ("key", "UP"), ", ", ("key", "DOWN"), ", ", ("key", "LEFT"), ", ", ("key", "RIGHT"), ", ", ("key", "PAGE UP"), " and ", ("key", "PAGE DOWN"), ], "", ["Sub-expressions: ", ("key", "("), " and ", ("key", ")")], "", ["Columns: ", ("key", COLUMN_KEYS[0]), " and ", ("key", COLUMN_KEYS[1]), "-", ("key", COLUMN_KEYS[-1])], "", ["Exit: ", ("key", "Q")], "", "", [ "Column Calculator does operations in the order they are ", "typed, not by following usual precedence rules. ", "If you want to calculate ", ("key", "12 - 2 * 3"), " with the multiplication happening before the ", "subtraction you must type ", ("key", "12 - (2 * 3)"), " instead.", ], ] def __init__(self) -> None: """Initialize the scrollable help text box widget.""" super().__init__() self.head = urwid.AttrMap(urwid.Text(["Help Column ", ("key", "?")], layout=CALC_LAYOUT), "help") self.foot = urwid.AttrMap(urwid.Text(["[text continues.. press ", ("key", "?"), " then scroll]"]), "helpnote") self.items = [urwid.Text(x) for x in self.help_text] self.listbox = urwid.ListBox(urwid.SimpleListWalker(self.items)) self.body = urwid.AttrMap(self.listbox, "help") self.frame: urwid.Frame[ urwid.AttrMap[urwid.ListBox[int]], urwid.AttrMap[urwid.Text], urwid.AttrMap[urwid.Text] | None ] = urwid.Frame( self.body, header=self.head, ) def render(self, size: tuple[()] | tuple[int] | tuple[int, int], focus: bool = False) -> urwid.Canvas: """Render the help column, showing the scroll hint footer only while more text is available below. :raises ValueError: if `size` is not the two-dimensional size a box widget is always rendered with. """ if len(size) != 2: # box widgets are always rendered with a 2-tuple size raise ValueError(f"HelpColumn is a box widget and requires a (maxcol, maxrow) size, got {size!r}") maxcol, maxrow = size head_rows = self.head.rows((maxcol,)) if "bottom" in self.listbox.ends_visible((maxcol, maxrow - head_rows)): self.frame.footer = None else: self.frame.footer = self.foot return self.frame.render((maxcol, maxrow), focus) def keypress(self, size: tuple[()] | tuple[int] | tuple[int, int], key: str) -> str | None: """Pass the keystroke through to the wrapped frame. :raises ValueError: if `size` is not the two-dimensional size a box widget is always rendered with. """ if len(size) != 2: # box widgets are always rendered with a 2-tuple size raise ValueError(f"HelpColumn is a box widget and requires a (maxcol, maxrow) size, got {size!r}") return self.frame.keypress(size, key) class CalcDisplay: """Top-level application: the columns of cells, plus the loop and key handling that drives them.""" palette: typing.ClassVar[list[tuple[str, str, str] | tuple[str, str, str, str]]] = [ ("body", "white", "dark blue"), ("edit", "yellow", "dark blue"), ("editfocus", "yellow", "dark cyan", "bold"), ("key", "dark cyan", "light gray", "standout,underline"), ("title", "white", "light gray", "bold,standout"), ("help", "black", "light gray", "standout"), ("helpnote", "dark green", "light gray"), ("colhead", "black", "light gray", "standout"), ("event", "light red", "black", "standout"), ("confirm", "yellow", "black", "bold"), ] def __init__(self) -> None: """Initialize with the help column and a single lettered column "A".""" self.columns = urwid.Columns([HelpColumn(), CellColumn("A")], 1) self.columns.focus_position = 1 view = urwid.AttrMap(self.columns, "body") self.view: urwid.Frame[urwid.AttrMap[urwid.Columns], None, urwid.AttrMap[urwid.Text]] = urwid.Frame( view ) # for showing messages self.col_link: dict[CellColumn, tuple[Cell, CellColumn]] = {} def main(self) -> None: """Run the Column Calculator application.""" self.loop = urwid.MainLoop(self.view, self.palette, screen=Screen(), input_filter=self.input_filter) self.loop.run() # on exit write the formula and the result to the console expression, result = self.get_expression_result() print("Paste this expression into a new Column Calculator session to continue editing:") print(expression) print("Result:", result) def input_filter( self, data: list[str | tuple[str, int, int, int]], raw_input: list[int], ) -> list[str | tuple[str, int, int, int]]: """Handle every keystroke immediately and consume all input, leaving nothing for MainLoop to process.""" if "q" in data or "Q" in data: raise urwid.ExitMainLoop() # handle other keystrokes for k in data: try: self.wrap_keypress(k) self.event = None self.view.footer = None except CalcEvent as e: # noqa: PERF203 # display any message self.event = e self.view.footer = e.widget() # remove all input from further processing by MainLoop return [] def wrap_keypress(self, key: str | tuple[str, int, int, int]) -> None: """Handle confirmation and throw event on bad input. :raises ColumnDeleteEvent: if a second BACKSPACE confirms deleting a non-empty column. :raises CalcEvent: if `key` is not a valid movement or editing key for the current focus. """ # falls back to the raw key if self.keypress() raises before returning processed_key: str | tuple[str, int, int, int] | None = key try: processed_key = self.keypress(key) except ColumnDeleteEvent as e: if e.letter == COLUMN_KEYS[1]: # cannot delete the first column, ignore key return if not self.column_empty(e.letter) and not isinstance(self.event, ColumnDeleteEvent): # need to get two in a row, so check last event ask for confirmation raise self.delete_column(e.letter) except UpdateParentEvent: self.update_parent_columns() return if processed_key is None: return if self.columns.focus_position == 0 and processed_key not in {"up", "down", "page up", "page down"}: raise CalcEvent(E_invalid_in_help_col) if processed_key not in EDIT_KEYS and processed_key not in MOVEMENT_KEYS: key_display = processed_key.upper() if isinstance(processed_key, str) else processed_key raise CalcEvent(E_invalid_key % key_display) def keypress(self, key: str | tuple[str, int, int, int]) -> str | None: """Handle a keystroke. :raises CalcEvent: on an invalid column switch, or an attempt to open too many or unparented columns. """ self.loop.process_input([key]) if isinstance(key, tuple): # ignore mouse events return None if key.upper() in COLUMN_KEYS: # column switch i = COLUMN_KEYS.index(key.upper()) if i >= len(self.columns): raise CalcEvent(E_no_such_column % key.upper()) self.columns.focus_position = i return None if key == "(": # open a new column if len(self.columns) >= len(COLUMN_KEYS): raise CalcEvent(E_no_more_columns) i = self.columns.focus_position if i == 0: # makes no sense in help column return key col = self._column_at(i) new_letter = COLUMN_KEYS[len(self.columns)] parent, child = col.create_child(new_letter) if child is None or parent is None: # something invalid in focus return key self.columns.contents.append((child, (urwid.WEIGHT, 1, False))) self.set_link(parent, col, child) self.columns.focus_position = len(self.columns) - 1 return None if key == ")": i = self.columns.focus_position if i == 0: # makes no sense in help column return key col = self._column_at(i) parent, pcol = self.get_parent(col) if parent is None or pcol is None: # column has no parent raise CalcEvent(E_no_parent_column) new_i = next(iter(idx for idx, (w, _) in enumerate(self.columns.contents) if w == pcol)) self.columns.focus_position = new_i return None return key def _column_at(self, index: int) -> CellColumn: """Return the CellColumn at the given position in `self.columns`. :raises AssertionError: if the widget at that position is not a CellColumn, which happens only at position 0, the fixed help column, and callers of this method never pass 0. """ col = self.columns.contents[index][0] assert isinstance(col, CellColumn) # noqa: S101 # for examples "assert" is acceptable return col def set_link(self, parent: Cell, pcol: CellColumn, child: CellColumn) -> None: """Store the link between a parent cell and child column. parent -- parent Cell object pcol -- CellColumn where parent resides child -- child CellColumn object """ self.col_link[child] = parent, pcol def get_parent(self, child: CellColumn) -> tuple[Cell, CellColumn] | tuple[None, None]: """Return the parent and parent column for a given column.""" return self.col_link.get(child, (None, None)) def column_empty(self, letter: str) -> bool: """Return True if the column passed is empty.""" return self._column_at(COLUMN_KEYS.index(letter)).is_empty() def delete_column(self, letter: str) -> None: """Delete the column with the given letter.""" i = COLUMN_KEYS.index(letter) col = self._column_at(i) parent, pcol = self.get_parent(col) assert parent is not None # noqa: S101 # for examples "assert" is acceptable assert pcol is not None # noqa: S101 # for examples "assert" is acceptable f = self.columns.focus_position if f == i: # move focus to the parent column f = next(iter(idx for idx, (w, _) in enumerate(self.columns.contents) if w == pcol)) self.columns.focus_position = f parent.remove_child() pcol.update_results(parent) del self.columns.contents[i] # delete children of this column keep_right_cols: list[CellColumn] = [] remove_cols = [col] for rcol in self.columns.contents[i:]: assert isinstance(rcol[0], CellColumn) # noqa: S101 # for examples "assert" is acceptable _rparent, rpcol = self.get_parent(rcol[0]) if rpcol in remove_cols: remove_cols.append(rcol[0]) else: keep_right_cols.append(rcol[0]) for rc in remove_cols: # remove the links del self.col_link[rc] # keep only the non-children self.columns.contents[i:] = [(w, (urwid.WEIGHT, 1, False)) for w in keep_right_cols] # fix the letter assignments for j in range(i, len(self.columns)): col = self._column_at(j) # fix the column heading col.set_letter(COLUMN_KEYS[j]) parent, pcol = self.get_parent(col) assert parent is not None # noqa: S101 # for examples "assert" is acceptable # fix the parent cell parent.edit.set_letter(COLUMN_KEYS[j]) def update_parent_columns(self) -> None: """Update the parent columns of the current focus column.""" f = self.columns.focus_position col = self._column_at(f) while 1: parent, pcol = self.get_parent(col) if pcol is None: return changed = pcol.update_results(start_from=parent) if not changed: return col = pcol def get_expression_result(self) -> tuple[str, str]: """Return (expression, result) as strings.""" col = self._column_at(1) return col.get_expression(), f"{col.get_result():d}" class CalcNumLayout(urwid.TextLayout): """TextLayout class for bottom-right aligned numbers with a space on the last line for the cursor.""" def layout( self, text: str | bytes, width: int, align: Literal["left", "center", "right"] | urwid.Align, wrap: Literal["any", "space", "clip", "ellipsis"] | urwid.WrapMode, ) -> list[list[tuple[int, int, int | bytes] | tuple[int, int | None]]]: """Return layout structure for calculator number display.""" lt = len(text) + 1 # extra space for cursor remaining = lt % width # remaining segment not full width wide linestarts = range(remaining, lt, width) layout: list[list[tuple[int, int, int | bytes] | tuple[int, int | None]]] = [] if linestarts: if remaining: # right-align the remaining segment on 1st line layout.append([(width - remaining, None), (remaining, 0, remaining)]) # fill all but the last line for x in linestarts[:-1]: layout.append([(width, x, x + width)]) # noqa: PERF401 s = linestarts[-1] # add the last line with a cursor hint layout.append([(width - 1, s, lt - 1), (0, lt - 1)]) elif lt - 1: # all fits on one line, so right align the text # with a cursor hint at the end layout.append([(width - lt, None), (lt - 1, 0, lt - 1), (0, lt - 1)]) else: # nothing on the line, right align a cursor hint layout.append([(width - 1, None), (0, 0)]) return layout def main() -> None: """Launch Column Calculator.""" global CALC_LAYOUT # noqa: PLW0603 # pylint: disable=global-statement CALC_LAYOUT = CalcNumLayout() urwid.display.web.set_preferences("Column Calculator") # try to handle short web requests quickly if urwid.display.web.handle_short_request(): return CalcDisplay().main() if __name__ == "__main__" or urwid.display.web.is_web_request(): main() urwid-urwid-69907e4/examples/dialog.py000077500000000000000000000373271526073734300177450ustar00rootroot00000000000000#!/usr/bin/env python # # Urwid example similar to dialog(1) program # Copyright (C) 2004-2009 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """Urwid example similar to dialog(1) program.""" from __future__ import annotations import sys import typing import urwid if typing.TYPE_CHECKING: from collections.abc import Hashable, Sequence from urwid.util import _TagMarkup _Dimension: typing.TypeAlias = int | tuple[typing.Literal[urwid.WHSettings.RELATIVE], int] _ListItem: typing.TypeAlias = "urwid.CheckBox | MenuItem" class DialogExit(Exception): """Raised by a dialog button or key handler to end the main loop with an exit code.""" class ExitButton(urwid.Button): """Button that carries the exit code `DialogDisplay.button_press` raises `DialogExit` with.""" def __init__( self, label: _TagMarkup, on_press: typing.Callable[[ExitButton], typing.NoReturn], exitcode: int, ) -> None: """Initialize with the exit code raised by `DialogDisplay.button_press` on press.""" super().__init__(label, on_press) self.exitcode = exitcode class DialogDisplay: """Base dialog widget: a bordered, shadowed frame with an optional header and a button footer.""" palette: typing.ClassVar[list[tuple[str, str, str] | tuple[str, str, str, str]]] = [ ("body", "black", "light gray", "standout"), ("border", "black", "dark blue"), ("shadow", "white", "black"), ("selectable", "black", "dark cyan"), ("focus", "white", "dark blue", "bold"), ("focustext", "light gray", "dark blue"), ] def __init__( self, text: _TagMarkup | None, height: int | str, width: int | str, body: urwid.Widget | None = None, ) -> None: """Initialize the bordered, shadowed dialog frame around `body`, with an optional header from `text`.""" raw_width = int(width) parsed_width: _Dimension = raw_width if raw_width > 0 else (urwid.RELATIVE, 80) raw_height = int(height) parsed_height: _Dimension = raw_height if raw_height > 0 else (urwid.RELATIVE, 80) self.body = body if body is None: # fill space with nothing body = urwid.Filler(urwid.Divider(), urwid.TOP) self.frame: urwid.Frame[urwid.Widget, urwid.Pile, urwid.Pile] = urwid.Frame(body, focus_part="footer") if text is not None: self.frame.header = urwid.Pile([urwid.Text(text), urwid.Divider()]) w: urwid.Widget = self.frame # pad area around listbox w = urwid.Padding(w, urwid.LEFT, left=2, right=2) w = urwid.Filler(w, urwid.TOP, urwid.RELATIVE_100, top=1, bottom=1) w = urwid.AttrMap(w, "body") # "shadow" effect w = urwid.Columns([w, (2, urwid.AttrMap(urwid.Filler(urwid.Text(("border", " ")), urwid.TOP), "shadow"))]) w = urwid.Frame(w, footer=urwid.AttrMap(urwid.Text(("border", " ")), "shadow")) # outermost border area w = urwid.Padding(w, urwid.CENTER, parsed_width) w = urwid.Filler(w, urwid.MIDDLE, parsed_height) w = urwid.AttrMap(w, "border") self.view: urwid.Widget = w def add_buttons(self, buttons: Sequence[tuple[str, int]]) -> None: """Add a row of buttons to the dialog's footer, each raising `DialogExit` with its exit code.""" lines = [] for name, exitcode in buttons: b = ExitButton(name, self.button_press, exitcode) b = urwid.AttrMap(b, "selectable", "focus") lines.append(b) self.buttons = urwid.GridFlow(lines, 10, 3, 1, urwid.CENTER) self.frame.footer = urwid.Pile([urwid.Divider(), self.buttons], focus_item=1) def button_press(self, button: ExitButton) -> typing.NoReturn: """Raise `DialogExit` with the pressed button's exit code.""" raise DialogExit(button.exitcode) def main(self) -> tuple[int, str]: """Run the dialog's main loop and return its exit code and message.""" self.loop: urwid.MainLoop = urwid.MainLoop(self.view, self.palette) try: self.loop.run() except DialogExit as e: return self.on_exit(e.args[0]) raise AssertionError("unreachable: MainLoop.run() returned without raising DialogExit") def on_exit(self, exitcode: int) -> tuple[int, str]: """Return the exit code together with an empty message.""" return exitcode, "" def _decode_label(label: str | bytes) -> str: """Return a widget label as `str`, decoding it if urwid returned raw bytes.""" return label if isinstance(label, str) else label.decode() class InputDialogDisplay(DialogDisplay): """Dialog with a single-line text entry field.""" def __init__(self, text: _TagMarkup | None, height: int | str, width: int | str) -> None: """Initialize with an empty single-line text entry field, focused.""" self.edit = urwid.Edit() body = urwid.ListBox(urwid.SimpleListWalker([self.edit])) body = urwid.AttrMap(body, "selectable", "focustext") super().__init__(text, height, width, body) self.frame.focus_position = "body" def unhandled_key(self, size: tuple[int, int], k: str) -> None: """Move focus between the entry field and the buttons.""" if k in {"up", "page up"}: self.frame.focus_position = "body" if k in {"down", "page down"}: self.frame.focus_position = "footer" if k == "enter": # pass enter to the "ok" button self.frame.focus_position = "footer" self.view.keypress(size, k) def on_exit(self, exitcode: int) -> tuple[int, str]: """Return the exit code together with the entered text.""" return exitcode, self.edit.get_edit_text() class TextDialogDisplay(DialogDisplay): """Dialog that displays the contents of a file in a scrollable list box.""" def __init__(self, file: str, height: int | str, width: int | str) -> None: """Initialize with the contents of `file` loaded into the scrollable list box.""" with open(file, encoding="utf-8") as f: lines = [urwid.Text(line.rstrip()) for line in f] # read the whole file (being slow, not lazy this time) body = urwid.ListBox(urwid.SimpleListWalker(lines)) body = urwid.AttrMap(body, "selectable", "focustext") super().__init__(None, height, width, body) def unhandled_key(self, size: tuple[int, int], k: str) -> None: """Scroll the file's contents while keeping focus on the footer's buttons.""" if k in {"up", "page up", "down", "page down"}: self.frame.focus_position = "body" self.view.keypress(size, k) self.frame.focus_position = "footer" class ListDialogDisplay(DialogDisplay): """Dialog listing selectable items, built by `constr` for each `(tag, item, default)` triple.""" def __init__( self, text: _TagMarkup | None, height: int | str, width: int | str, constr: typing.Callable[[str, bool], _ListItem], items: tuple[str, ...], has_default: bool, ) -> None: """Initialize with one item widget per `(tag, item, default)` triple in `items`, built via `constr`.""" j: list[tuple[str, ...]] = [] k: int tail: tuple[str, ...] if has_default: k, tail = 3, () else: k, tail = 2, ("no",) while items: j.append(items[:k] + tail) items = items[k:] lines = [] self.items: list[_ListItem] = [] for tag, item, default in j: item_widget = constr(tag, default == "on") self.items.append(item_widget) w: urwid.Widget = urwid.Columns([(12, item_widget), urwid.Text(item)], 2) w = urwid.AttrMap(w, "selectable", "focus") lines.append(w) lb = urwid.ListBox(urwid.SimpleListWalker(lines)) lb = urwid.AttrMap(lb, "selectable") super().__init__(text, height, width, lb) self.frame.focus_position = "body" def unhandled_key(self, size: tuple[int, int], k: str) -> None: """Move focus between the item list and the buttons.""" if k in {"up", "page up"}: self.frame.focus_position = "body" if k in {"down", "page down"}: self.frame.focus_position = "footer" if k == "enter": # pass enter to the "ok" button self.frame.focus_position = "footer" self.buttons.focus_position = 0 self.view.keypress(size, k) def on_exit(self, exitcode: int) -> tuple[int, str]: """Print the tag of the item selected.""" if exitcode != 0: return exitcode, "" s = "" for i in self.items: if i.get_state(): s = _decode_label(i.get_label()) break return exitcode, s class CheckListDialogDisplay(ListDialogDisplay): """Dialog listing checkbox items, mimicking dialog(1)'s ``--checklist``.""" def on_exit(self, exitcode: int) -> tuple[int, str]: """Put each checked item in double quotes with a trailing space.""" if exitcode != 0: return exitcode, "" labels = [_decode_label(i.get_label()) for i in self.items if i.get_state()] return exitcode, "".join(f'"{tag}" ' for tag in labels) class MenuItem(urwid.Text): """A custom widget for the --menu option.""" def __init__(self, label: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]]) -> None: """Initialize unselected, with `label` as its text.""" super().__init__(label) self.state = False def selectable(self) -> bool: """Return True: a menu item can always receive the focus.""" return True def keypress(self, size: tuple[()] | tuple[int] | tuple[int, int], key: str) -> str | None: """Select this item and end the dialog on :kbd:`enter`, otherwise return the key unhandled. :raises DialogExit: when `key` is :kbd:`enter`, to end the dialog with this item selected. """ if key == "enter": self.state = True raise DialogExit(0) return key def mouse_event( self, size: tuple[()] | tuple[int] | tuple[int, int], event: str, button: int, col: int, row: int, focus: bool, ) -> bool | None: """Select this item and end the dialog on a mouse release. :raises DialogExit: when `event` is a mouse release, to end the dialog with this item selected. """ if event == "mouse release": self.state = True raise DialogExit(0) return False def get_state(self) -> bool: """Return whether this item has been selected.""" return self.state def get_label(self) -> str: """Just alias to text.""" return _decode_label(self.text) def do_checklist( text: str, height: str, width: str, list_height: str, # ignored, kept for a uniform do_* signature *items: str, ) -> CheckListDialogDisplay: """Build a checklist dialog for the ``--checklist`` command-line mode.""" def constr(tag: str, state: bool) -> urwid.CheckBox: return urwid.CheckBox(tag, state) d = CheckListDialogDisplay(text, height, width, constr, items, True) d.add_buttons([("OK", 0), ("Cancel", 1)]) return d def do_inputbox(text: str, height: str, width: str) -> InputDialogDisplay: """Build an input dialog for the ``--inputbox`` command-line mode.""" d = InputDialogDisplay(text, height, width) d.add_buttons([("Exit", 0)]) return d def do_menu( text: str, height: str, width: str, menu_height: str, # ignored, kept for a uniform do_* signature *items: str, ) -> ListDialogDisplay: """Build a menu dialog for the ``--menu`` command-line mode.""" def constr(tag: str, state: bool) -> MenuItem: # state unused, kept for a uniform constr shape return MenuItem(tag) d = ListDialogDisplay(text, height, width, constr, items, False) d.add_buttons([("OK", 0), ("Cancel", 1)]) return d def do_msgbox(text: str, height: str, width: str) -> DialogDisplay: """Build a plain message dialog for the ``--msgbox`` command-line mode.""" d = DialogDisplay(text, height, width) d.add_buttons([("OK", 0)]) return d def do_radiolist( text: str, height: str, width: str, list_height: str, # ignored, kept for a uniform do_* signature *items: str, ) -> ListDialogDisplay: """Build a radio-button list dialog for the ``--radiolist`` command-line mode.""" radiolist: list[urwid.RadioButton] = [] def constr( # pylint: disable=dangerous-default-value tag: str, state: bool, radiolist: list[urwid.RadioButton] = radiolist, ) -> urwid.RadioButton: return urwid.RadioButton(radiolist, tag, state) d = ListDialogDisplay(text, height, width, constr, items, True) d.add_buttons([("OK", 0), ("Cancel", 1)]) return d def do_textbox(file: str, height: str, width: str) -> TextDialogDisplay: """Build a dialog showing the contents of `file` for the ``--textbox`` command-line mode.""" d = TextDialogDisplay(file, height, width) d.add_buttons([("Exit", 0)]) return d def do_yesno(text: str, height: str, width: str) -> DialogDisplay: """Build a yes/no dialog for the ``--yesno`` command-line mode.""" d = DialogDisplay(text, height, width) d.add_buttons([("Yes", 0), ("No", 1)]) return d # pylint: disable-next=consider-using-namedtuple-or-dataclass # made before argparse in stdlib MODES: dict[str, tuple[typing.Callable[..., DialogDisplay], str]] = { "--checklist": (do_checklist, "text height width list-height [ tag item status ] ..."), "--inputbox": (do_inputbox, "text height width"), "--menu": (do_menu, "text height width menu-height [ tag item ] ..."), "--msgbox": (do_msgbox, "text height width"), "--radiolist": (do_radiolist, "text height width list-height [ tag item status ] ..."), "--textbox": (do_textbox, "file height width"), "--yesno": (do_yesno, "text height width"), } def show_usage() -> None: """Display a helpful usage message.""" modelist = sorted((mode, help_mode) for (mode, (fn, help_mode)) in MODES.items()) sys.stdout.write( (__doc__ or "") + "\n".join([f"{mode:<15} {help_mode}" for (mode, help_mode) in modelist]) + """ height and width may be set to 0 to auto-size. list-height and menu-height are currently ignored. status may be either on or off. """ ) def main() -> None: """Parse the command line, run the requested dialog mode, and exit with its exit code.""" if len(sys.argv) < 2 or sys.argv[1] not in MODES: show_usage() return # Create a DialogDisplay instance fn, _help_mode = MODES[sys.argv[1]] d = fn(*sys.argv[2:]) # Run it exitcode, exitstring = d.main() # Exit if exitstring: sys.stderr.write(f"{exitstring}\n") sys.exit(exitcode) if __name__ == "__main__": main() urwid-urwid-69907e4/examples/edit.py000077500000000000000000000207201526073734300174200ustar00rootroot00000000000000#!/usr/bin/env python # # Urwid example lazy text editor suitable for tabbed and format=flowed text # Copyright (C) 2004-2009 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """ Urwid example lazy text editor suitable for tabbed and flowing text. Features: - custom list walker for lazily loading text file Usage: edit.py """ from __future__ import annotations import sys import typing import urwid class LineWalker(urwid.ListWalker[int, urwid.Edit]): """ListWalker-compatible class for lazily reading file contents.""" def __init__(self, name: str) -> None: """Initialize with `name` opened for lazy line-by-line reading, no lines loaded yet.""" # do not overcomplicate example self.file: typing.TextIO | None = open( # noqa: SIM115 # pylint: disable=consider-using-with name, encoding="utf-8", ) self.lines: list[urwid.Edit] = [] self.focus = 0 def __del__(self) -> None: """Close the open file, if any.""" if self.file is not None: self.file.close() def get_focus(self) -> tuple[urwid.Edit, int] | tuple[None, None]: """Return the widget and position for the current focus.""" return self._get_at_pos(self.focus) def set_focus(self, focus: int) -> None: """Set the focus position.""" self.focus = focus self._modified() def get_next(self, position: int) -> tuple[urwid.Edit, int] | tuple[None, None]: """Return the widget and position after the given position.""" return self._get_at_pos(position + 1) def get_prev(self, position: int) -> tuple[urwid.Edit, int] | tuple[None, None]: """Return the widget and position before the given position.""" return self._get_at_pos(position - 1) def read_next_line(self) -> str: """Read another line from the file. :raises AssertionError: if called after the file has already been closed. """ assert self.file is not None, "read past end of file" # noqa: S101 # "assert" is OK in examples next_line = self.file.readline() if not next_line or next_line[-1:] != "\n": # no newline on last line of file self.file = None else: # trim newline characters next_line = next_line[:-1] edit = urwid.Edit("", next_line, allow_tab=True, expand_tabs=True) edit.edit_pos = 0 self.lines.append(edit) return next_line def _get_at_pos(self, pos: int) -> tuple[urwid.Edit, int] | tuple[None, None]: """Return a widget for the line number passed.""" if pos < 0: # line 0 is the start of the file, no more above return None, None if len(self.lines) > pos: # we have that line so return it return self.lines[pos], pos if self.file is None: # file is closed, so there are no more lines return None, None assert pos == len(self.lines), "out of order request?" # noqa: S101 # "assert" is OK in examples self.read_next_line() return self.lines[-1], pos def split_focus(self) -> None: """Divide the focus edit widget at the cursor location.""" focus = self.lines[self.focus] pos = focus.edit_pos edit = urwid.Edit("", focus.edit_text[pos:], allow_tab=True, expand_tabs=True) focus.set_edit_text(focus.edit_text[:pos]) edit.edit_pos = 0 self.lines.insert(self.focus + 1, edit) def combine_focus_with_prev(self) -> None: """Combine the focus edit widget with the one above.""" above, _ = self.get_prev(self.focus) if above is None: # already at the top return focus = self.lines[self.focus] above.set_edit_pos(len(above.edit_text)) above.set_edit_text(above.edit_text + focus.edit_text) del self.lines[self.focus] self.focus -= 1 def combine_focus_with_next(self) -> None: """Combine the focus edit widget with the one below.""" below, _ = self.get_next(self.focus) if below is None: # already at bottom return focus = self.lines[self.focus] focus.set_edit_text(focus.edit_text + below.edit_text) del self.lines[self.focus + 1] class EditDisplay: """Text editor application, tying the file walker to the urwid widget tree.""" palette: typing.ClassVar[list[tuple[str, str, str] | tuple[str, str, str, str]]] = [ ("body", "default", "default"), ("foot", "dark cyan", "dark blue", "bold"), ("key", "light cyan", "dark blue", "underline"), ] footer_text: typing.ClassVar[tuple[str, list[str | tuple[str, str]]]] = ( "foot", [ "Text Editor ", ("key", "F5"), " save ", ("key", "F8"), " quit", ], ) def __init__(self, name: str) -> None: """Initialize with the file at `name` loaded for editing.""" self.save_name = name self.walker = LineWalker(name) self.listbox: urwid.ListBox[int] = urwid.ListBox(self.walker) self.footer = urwid.AttrMap(urwid.Text(self.footer_text), "foot") self.view: urwid.Frame[ urwid.AttrMap[urwid.ListBox[int]], None, urwid.AttrMap[urwid.Text], ] = urwid.Frame(urwid.AttrMap(self.listbox, "body"), footer=self.footer) def main(self) -> None: """Run the text editor application.""" self.loop = urwid.MainLoop(self.view, self.palette, unhandled_input=self.unhandled_keypress) self.loop.run() def unhandled_keypress(self, k: str | tuple[str, int, int, int]) -> bool | None: """Last resort for keypresses.""" if k == "f5": self.save_file() elif k == "f8": raise urwid.ExitMainLoop() elif k == "delete": # delete at end of line self.walker.combine_focus_with_next() elif k == "backspace": # backspace at beginning of line self.walker.combine_focus_with_prev() elif k == "enter": # start new line self.walker.split_focus() # move the cursor to the new line and reset pref_col self.loop.process_input(["down", "home"]) elif k == "right": _w, pos = self.walker.get_next(self.walker.focus) if pos is not None: self.listbox.set_focus(pos, "above") self.loop.process_input(["home"]) elif k == "left": _w, pos = self.walker.get_prev(self.walker.focus) if pos is not None: self.listbox.set_focus(pos, "below") self.loop.process_input(["end"]) else: return None return True def save_file(self) -> None: """Write the file out to disk.""" walk = self.walker # collect the text already stored in edit widgets lines = [edit.edit_text for edit in walk.lines] # then the rest while walk.file is not None: lines.append(walk.read_next_line()) # write back to disk with open(self.save_name, "w", encoding="utf-8") as outfile: prefix = "" for line in lines: outfile.write(prefix + line) prefix = "\n" def main() -> None: """Open the file named on the command line and run the editor on it.""" try: name = sys.argv[1] # do not overcomplicate example assert open(name, "ab") # noqa: SIM115,S101 # pylint: disable=consider-using-with except OSError: sys.stderr.write(__doc__) return EditDisplay(name).main() if __name__ == "__main__": main() urwid-urwid-69907e4/examples/fib.py000077500000000000000000000115711526073734300172370ustar00rootroot00000000000000#!/usr/bin/env python # # Urwid example fibonacci sequence viewer / unbounded data demo # Copyright (C) 2004-2007 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """ Urwid example fibonacci sequence viewer / unbounded data demo. Features: - custom list walker class for browsing infinite set - custom wrap mode "numeric" for wrapping numbers to right and bottom """ from __future__ import annotations import typing import urwid if typing.TYPE_CHECKING: from typing_extensions import Literal class FibonacciWalker(urwid.ListWalker[tuple[int, int], urwid.Text]): """ListWalker-compatible class for browsing fibonacci set. positions returned are (value at position-1, value at position) tuples. """ def __init__(self) -> None: """Initialize focused at the start of the fibonacci sequence.""" self.focus = (0, 1) self.numeric_layout = NumericLayout() def _get_at_pos(self, pos: tuple[int, int]) -> tuple[urwid.Text, tuple[int, int]]: """Return a widget and the position passed.""" return urwid.Text(f"{pos[1]:d}", layout=self.numeric_layout), pos def get_focus(self) -> tuple[urwid.Text, tuple[int, int]]: """Return the widget and position for the current focus.""" return self._get_at_pos(self.focus) def set_focus(self, focus: tuple[int, int]) -> None: """Set the focus position.""" self.focus = focus self._modified() def get_next(self, position: tuple[int, int]) -> tuple[urwid.Text, tuple[int, int]]: """Return the widget and position after the given position.""" a, b = position focus = b, a + b return self._get_at_pos(focus) def get_prev(self, position: tuple[int, int]) -> tuple[urwid.Text, tuple[int, int]]: """Return the widget and position before the given position.""" a, b = position focus = b - a, a return self._get_at_pos(focus) def main() -> None: """Run the fibonacci sequence viewer application.""" palette: list[tuple[str, str, str] | tuple[str, str, str, str]] = [ ("body", "black", "dark cyan", "standout"), ("foot", "light gray", "black"), ("key", "light cyan", "black", "underline"), ( "title", "white", "black", ), ] footer_text: list[tuple[str, str] | str] = [ ("title", "Fibonacci Set Viewer"), " ", ("key", "UP"), ", ", ("key", "DOWN"), ", ", ("key", "PAGE UP"), " and ", ("key", "PAGE DOWN"), " move view ", ("key", "Q"), " exits", ] def exit_on_q(key: str | tuple[str, int, int, int]) -> None: if key in {"q", "Q"}: raise urwid.ExitMainLoop() listbox: urwid.ListBox[tuple[int, int]] = urwid.ListBox(FibonacciWalker()) footer = urwid.AttrMap(urwid.Text(footer_text), "foot") view: urwid.Frame[ urwid.AttrMap[urwid.ListBox[tuple[int, int]]], None, urwid.AttrMap[urwid.Text], ] = urwid.Frame(urwid.AttrMap(listbox, "body"), footer=footer) loop = urwid.MainLoop(view, palette, unhandled_input=exit_on_q) loop.run() class NumericLayout(urwid.TextLayout): """TextLayout class for bottom-right aligned numbers.""" def layout( self, text: str | bytes, width: int, align: Literal["left", "center", "right"] | urwid.Align, wrap: Literal["any", "space", "clip", "ellipsis"] | urwid.WrapMode, ) -> list[list[tuple[int, int, int | bytes] | tuple[int, int | None]]]: """Return layout structure for right justified numbers.""" lt = len(text) r = lt % width # remaining segment not full width wide if r: layout: list[list[tuple[int, int, int | bytes] | tuple[int, int | None]]] = [ [(width - r, None), (r, 0, r)], # right-align the remaining segment on 1st line ] layout.extend([(width, x, x + width)] for x in range(r, lt, width)) # fill the rest of the lines return layout return [[(width, x, x + width)] for x in range(0, lt, width)] if __name__ == "__main__": main() urwid-urwid-69907e4/examples/graph.py000077500000000000000000000407351526073734300176040ustar00rootroot00000000000000#!/usr/bin/env python # # Urwid graphics example program # Copyright (C) 2004-2011 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """Urwid example demonstrating use of the BarGraph widget and creating a floating-window appearance. Also shows use of alarms to create timed animation. """ from __future__ import annotations import math import time import typing import urwid if typing.TYPE_CHECKING: from collections.abc import Callable, Sequence UPDATE_INTERVAL = 0.2 def sin100(x: int) -> float: """Return a sine value scaled to the range 0-100 that repeats every 100 steps. :param x: input step :returns: sine wave value in the range 0-100 """ return 50 + 50 * math.sin(x * math.pi / 50) class GraphModel: """Store the data that will be displayed on the graph, and keep track of which mode is enabled.""" data_max_value: typing.ClassVar[int] = 100 def __init__(self) -> None: """Initialize with the built-in sample data sets, defaulting to the first mode.""" data: list[tuple[str, Sequence[float | int]]] = [ ("Saw", list(range(0, 100, 2)) * 2), ("Square", [0] * 30 + [100] * 30), ("Sine 1", [sin100(x) for x in range(100)]), ("Sine 2", [(sin100(x) + sin100(x * 2)) / 2 for x in range(100)]), ("Sine 3", [(sin100(x) + sin100(x * 3)) / 2 for x in range(100)]), ] self.modes: list[str] = [] self.data: dict[str, Sequence[float | int]] = {} for m, d in data: self.modes.append(m) self.data[m] = d self.current_mode = self.modes[0] def get_modes(self) -> list[str]: """Return the available mode names.""" return self.modes def set_mode(self, m: str) -> None: """Set the current mode. :param m: name of the mode to switch to """ self.current_mode = m def get_data(self, offset: int, r: int) -> tuple[list[float | int], int, int]: """Return a slice of the current mode's data, its maximum value, and the length it repeats at. :param offset: start offset into the data, wrapped to the data length :param r: number of data points to return :returns: the requested data points, the maximum value among all items, and the length the data repeats at """ lines: list[float | int] = [] d = self.data[self.current_mode] while r: offset %= len(d) segment = d[offset : offset + r] r -= len(segment) offset += len(segment) lines += segment return lines, self.data_max_value, len(d) class GraphView(urwid.WidgetWrap[urwid.Widget]): """Provide the application's interface and graph display.""" palette: typing.ClassVar[list[tuple[str, str, str] | tuple[str, str, str, str]]] = [ ("body", "black", "light gray", "standout"), ("header", "white", "dark red", "bold"), ("screen edge", "light blue", "dark cyan"), ("main shadow", "dark gray", "black"), ("line", "black", "light gray", "standout"), ("bg background", "light gray", "black"), ("bg 1", "black", "dark blue", "standout"), ("bg 1 smooth", "dark blue", "black"), ("bg 2", "black", "dark cyan", "standout"), ("bg 2 smooth", "dark cyan", "black"), ("button normal", "light gray", "dark blue", "standout"), ("button select", "white", "dark green"), ("line", "black", "light gray", "standout"), ("pg normal", "white", "black", "standout"), ("pg complete", "white", "dark magenta"), ("pg smooth", "dark magenta", "black"), ] graph_samples_per_bar: typing.ClassVar[int] = 10 graph_num_bars: typing.ClassVar[int] = 5 graph_offset_per_second: typing.ClassVar[int] = 5 def __init__(self, controller: GraphController) -> None: """Initialize the graph display, stopped and not yet timed, wrapping `controller`.""" self.controller = controller self.started = True self.start_time: float | None = None self.offset = 0 self.last_offset: int | None = None super().__init__(self.main_window()) def get_offset_now(self) -> int: """Return the current graph offset, accounting for elapsed animation time. :returns: graph offset """ if self.start_time is None: return 0 if not self.started: return self.offset tdelta = time.time() - self.start_time return int(self.offset + (tdelta * self.graph_offset_per_second)) def update_graph(self, force_update: bool = False) -> bool: """Recompute the graph and progress bar for the current offset. :param force_update: redraw even if the offset has not changed since the last update :returns: whether the graph was actually redrawn """ o = self.get_offset_now() if o == self.last_offset and not force_update: return False self.last_offset = o gspb = self.graph_samples_per_bar r = gspb * self.graph_num_bars d, max_value, repeat = self.controller.get_data(o, r) lines: list[list[float | int]] = [] for n in range(self.graph_num_bars): value = sum(d[n * gspb : (n + 1) * gspb]) / gspb # toggle between two bar types if n & 1: lines.append([0, value]) else: lines.append([value, 0]) self.graph.set_data(lines, max_value) # also update progress prog: float if (o // repeat) & 1: # show 100% for first half, 0 for second half if o % repeat > repeat // 2: prog = 0 else: prog = 1 else: prog = float(o % repeat) / repeat self.animate_progress.current = prog return True def on_animate_button(self, button: urwid.Button) -> None: """Toggle started state and button text. :param button: button that was pressed """ if self.started: # stop animation button.set_label("Start") self.offset = self.get_offset_now() self.started = False self.controller.stop_animation() else: button.set_label("Stop") self.started = True self.start_time = time.time() self.controller.animate_graph() def on_reset_button(self, w: urwid.Button) -> None: """Reset the animation offset and restart the clock. :param w: button that was pressed """ self.offset = 0 self.start_time = time.time() self.update_graph(True) def on_mode_button(self, button: urwid.RadioButton, state: bool) -> None: """Notify the controller of a new mode setting. :param button: radio button whose state changed, carrying the mode name as its label :param state: new state of the radio button """ if state: # The new mode is the label of the button self.controller.set_mode(typing.cast("str", button.get_label())) self.last_offset = None def on_mode_change(self, m: str) -> None: """Handle external mode change by updating radio buttons. :param m: name of the mode that is now selected """ for rb in self.mode_buttons: if rb.original_widget.label == m: rb.original_widget.set_state(True, do_callback=False) break self.last_offset = None def on_unicode_checkbox(self, w: urwid.CheckBox, state: bool) -> None: """Switch the graph and progress bar between Unicode and ASCII rendering. :param w: checkbox that changed :param state: new state of the checkbox """ self.graph = self.bar_graph(state) self.graph_wrap._w = self.graph self.animate_progress = self.progress_bar(state) self.animate_progress_wrap._w = self.animate_progress self.update_graph(True) def main_shadow(self, w: urwid.Widget) -> urwid.Widget: """Wrap a shadow and background around a widget. :param w: widget to wrap :returns: the wrapped widget """ bg: urwid.Widget = urwid.AttrMap(urwid.SolidFill("â–’"), "screen edge") shadow = urwid.AttrMap(urwid.SolidFill(" "), "main shadow") bg = urwid.Overlay( shadow, bg, align=urwid.LEFT, width=urwid.RELATIVE_100, valign=urwid.TOP, height=urwid.RELATIVE_100, left=3, right=1, top=2, bottom=1, ) w = urwid.Overlay( w, bg, align=urwid.LEFT, width=urwid.RELATIVE_100, valign=urwid.TOP, height=urwid.RELATIVE_100, left=2, right=3, top=1, bottom=2, ) return w def bar_graph(self, smooth: bool = False) -> urwid.BarGraph: """Create the bar graph widget. :param smooth: use smoothed (sub-character resolution) bars :returns: the bar graph widget """ satt = None if smooth: satt = {(1, 0): "bg 1 smooth", (2, 0): "bg 2 smooth"} w = urwid.BarGraph(["bg background", "bg 1", "bg 2"], satt=satt) return w def button( self, t: str, fn: Callable[[urwid.Button], None], ) -> urwid.AttrMap[urwid.Button]: """Create a styled push button. :param t: button label :param fn: callback connected to the button's ``click`` signal :returns: the button wrapped in an :class:`urwid.AttrMap` """ w = urwid.Button(t, fn) w = urwid.AttrMap(w, "button normal", "button select") return w def radio_button( self, g: list[urwid.RadioButton], label: str, fn: Callable[[urwid.RadioButton, bool], None], ) -> urwid.AttrMap[urwid.RadioButton]: """Create a styled radio button for selecting a mode. :param g: radio button group to add the new button to :param label: button label, also used as the mode name :param fn: callback connected to the button's ``change`` signal :returns: the radio button wrapped in an :class:`urwid.AttrMap` """ w = urwid.RadioButton(g, label, False, on_state_change=fn) w = urwid.AttrMap(w, "button normal", "button select") return w def progress_bar(self, smooth: bool = False) -> urwid.ProgressBar: """Create the animation progress bar widget. :param smooth: use smoothed (sub-character resolution) rendering :returns: the progress bar widget """ return urwid.ProgressBar( "pg normal", "pg complete", 0, 1, "pg smooth" if smooth else None, ) def exit_program(self, w: urwid.Button) -> typing.NoReturn: """Exit the main loop. :param w: button that was pressed :raises urwid.ExitMainLoop: always """ raise urwid.ExitMainLoop() def graph_controls(self) -> urwid.ListBox[int]: """Build the list of mode, animation and quit controls shown beside the graph. :returns: the controls list box """ modes = self.controller.get_modes() # setup mode radio buttons self.mode_buttons: list[urwid.AttrMap[urwid.RadioButton]] = [] group: list[urwid.RadioButton] = [] for m in modes: rb = self.radio_button(group, m, self.on_mode_button) self.mode_buttons.append(rb) # setup animate button self.animate_button = self.button("", self.on_animate_button) self.on_animate_button(self.animate_button.original_widget) self.offset = 0 self.animate_progress = self.progress_bar() animate_controls = urwid.GridFlow( [ self.animate_button, self.button("Reset", self.on_reset_button), ], 9, 2, 0, urwid.CENTER, ) unicode_checkbox: urwid.Widget if urwid.get_encoding_mode() == "utf8": unicode_checkbox = urwid.CheckBox("Enable Unicode Graphics", on_state_change=self.on_unicode_checkbox) else: unicode_checkbox = urwid.Text("UTF-8 encoding not detected") self.animate_progress_wrap = urwid.WidgetWrap(self.animate_progress) lines: list[urwid.Widget] = [ urwid.Text("Mode", align=urwid.CENTER), *self.mode_buttons, urwid.Divider(), urwid.Text("Animation", align=urwid.CENTER), animate_controls, self.animate_progress_wrap, urwid.Divider(), urwid.LineBox(unicode_checkbox), urwid.Divider(), self.button("Quit", self.exit_program), ] w = urwid.ListBox(urwid.SimpleListWalker(lines)) # type: ignore[arg-type] # all lines are flow widgets return w def main_window(self) -> urwid.Widget: """Build the complete application window. :returns: the top-level widget """ self.graph = self.bar_graph() self.graph_wrap = urwid.WidgetWrap(self.graph) vline = urwid.AttrMap(urwid.SolidFill("│"), "line") c = self.graph_controls() w: urwid.Widget = urwid.Columns( [(urwid.WEIGHT, 2, self.graph_wrap), (1, vline), c], dividechars=1, focus_column=2, ) w = urwid.Padding(w, urwid.LEFT, left=1) w = urwid.AttrMap(w, "body") w = urwid.LineBox(w) w = urwid.AttrMap(w, "line") w = self.main_shadow(w) return w class GraphController: """Set up the model and view and run the application.""" def __init__(self) -> None: """Initialize the model and view and switch to the first available mode.""" self.animate_alarm: typing.Any = None self.model = GraphModel() self.view = GraphView(self) # use the first mode as the default mode = self.get_modes()[0] self.model.set_mode(mode) # update the view self.view.on_mode_change(mode) self.view.update_graph(True) def get_modes(self) -> list[str]: """Allow our view access to the list of modes.""" return self.model.get_modes() def set_mode(self, m: str) -> None: """Allow our view to set the mode.""" self.model.set_mode(m) self.view.update_graph(True) def get_data(self, offset: int, data_range: int) -> tuple[list[float | int], int, int]: """Provide data to our view for the graph. :param offset: start offset into the data :param data_range: number of data points to return :returns: the requested data points, their maximum value, and the length the data repeats at """ return self.model.get_data(offset, data_range) def main(self) -> None: """Run the main loop.""" self.loop: urwid.MainLoop = urwid.MainLoop(self.view, self.view.palette) self.loop.run() def animate_graph(self, loop: urwid.MainLoop | None = None, user_data: typing.Any = None) -> None: """Update the graph and schedule the next update. :param loop: main loop that triggered this alarm, unused :param user_data: alarm user data, unused """ self.view.update_graph() self.animate_alarm = self.loop.set_alarm_in(UPDATE_INTERVAL, self.animate_graph) def stop_animation(self) -> None: """Stop animating the graph.""" if self.animate_alarm: self.loop.remove_alarm(self.animate_alarm) self.animate_alarm = None def main() -> None: """Run the graph example program.""" GraphController().main() if __name__ == "__main__": main() urwid-urwid-69907e4/examples/input_test.py000077500000000000000000000130331526073734300206700ustar00rootroot00000000000000#!/usr/bin/env python # # Urwid keyboard input test app # Copyright (C) 2004-2009 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """Keyboard test application.""" from __future__ import annotations import argparse import logging import urwid loop_cls: type[urwid.EventLoop] | None if urwid.display.web.is_web_request(): Screen = urwid.display.web.Screen loop_cls = urwid.SelectEventLoop else: event_loops: dict[str, type[urwid.EventLoop] | None] = { "none": None, "select": urwid.SelectEventLoop, "asyncio": urwid.AsyncioEventLoop, } if hasattr(urwid, "TornadoEventLoop"): event_loops["tornado"] = urwid.TornadoEventLoop if hasattr(urwid, "GLibEventLoop"): event_loops["glib"] = urwid.GLibEventLoop if hasattr(urwid, "TwistedEventLoop"): event_loops["twisted"] = urwid.TwistedEventLoop if hasattr(urwid, "TrioEventLoop"): event_loops["trio"] = urwid.TrioEventLoop if hasattr(urwid, "ZMQEventLoop"): event_loops["zmq"] = urwid.ZMQEventLoop displays: dict[str, type[urwid.display.BaseScreen]] = { "raw": urwid.display.raw.Screen, } try: from urwid.display import curses except ImportError: pass else: displays["curses"] = curses.Screen parser = argparse.ArgumentParser(description="Input test") parser.add_argument( "--display", choices=displays, default="raw", help="Display module to use (default is 'raw' as most advanced)", ) group = parser.add_argument_group("Advanced Options") group.add_argument( "--event-loop", choices=event_loops, default="none", help="Event loop to use ('none' = use the default)", ) group.add_argument( "--debug-log", action="store_true", help="Enable debug logging", ) args = parser.parse_args() Screen = displays[args.display] loop_cls = event_loops[args.event_loop] if args.debug_log: logging.basicConfig( level=logging.DEBUG, filename="debug.log", format=( "%(levelname)1.1s %(asctime)s | %(threadName)s | %(name)s \n" "\t%(message)s\n" "-------------------------------------------------------------------------------" ), datefmt="%d-%b-%Y %H:%M:%S", force=True, ) logging.captureWarnings(True) def key_test() -> None: """Run the main loop that echoes each keypress and its raw bytes to the screen.""" screen = Screen() header = urwid.AttrMap( urwid.Text("Values from get_input(). Q exits."), "header", ) lw: urwid.SimpleListWalker[urwid.Columns] = urwid.SimpleListWalker([]) listbox = urwid.AttrMap( urwid.ListBox(lw), "listbox", ) top: urwid.Frame[urwid.AttrMap[urwid.ListBox[urwid.Columns]], urwid.AttrMap[urwid.Text], None] = urwid.Frame( listbox, header ) def input_filter( keys: list[str | tuple[str, int, int, int]], raw: list[int] ) -> list[str | tuple[str, int, int, int]]: """Append a display row for *keys* and *raw* to the list box, then return *keys* unchanged. :raises urwid.ExitMainLoop: :kbd:`q` or :kbd:`Q` is among *keys*. """ if "q" in keys or "Q" in keys: raise urwid.ExitMainLoop t: list[str | tuple[str, str]] = [] for k in keys: if isinstance(k, tuple): out: list[str | tuple[str, str]] = [] for v in k: if out: out += [", "] out += [("key", repr(v))] t += ["(", *out, ")"] else: t += ["'", ("key", k), "' "] rawt = urwid.Text(", ".join(f"{r:d}" for r in raw)) if t: lw.append(urwid.Columns([(urwid.WEIGHT, 2, urwid.Text(t)), rawt])) listbox.original_widget.set_focus(len(lw) - 1, "above") return keys loop = urwid.MainLoop( top, [ ("header", "black", "dark cyan", "standout"), ("key", "yellow", "dark blue", "bold"), ("listbox", "light gray", "black"), ], screen, input_filter=input_filter, event_loop=loop_cls() if loop_cls is not None else None, ) old = () try: old = screen.tty_signal_keys("undefined", "undefined", "undefined", "undefined", "undefined") loop.run() finally: if old: screen.tty_signal_keys(*old) def main() -> None: """Run the keyboard input test application.""" urwid.display.web.set_preferences("Input Test") if urwid.display.web.handle_short_request(): return key_test() if __name__ == "__main__" or urwid.display.web.is_web_request(): main() urwid-urwid-69907e4/examples/lcd_cf635.py000077500000000000000000000267461526073734300201610ustar00rootroot00000000000000#!/usr/bin/env python """ The crystalfontz 635 has these characters in ROM. ....X. ...... ...... ...XX. .XXXXX ..XXX. ..XXX. .XXXXX .XXXXX .XXXX. .XXXXX .XXXXX ..XXX. .XXXXX .XXXXX ...XX. .XXXXX ..XXX. ....X. ...... ...... ...... ...... ...... 0x11 0xd0 0xbb By adding the characters in CGRAM below we can use them as part of a horizontal slider control, selected check box and selected radio button respectively. """ from __future__ import annotations import sys import typing import urwid if typing.TYPE_CHECKING: from collections.abc import Callable from typing_extensions import Literal, TypeAlias from urwid.display.lcd import CF635Screen MenuWidget: TypeAlias = "MenuOption | urwid.Text | urwid.Columns | LCDRadioButton" MenuStructureItem: TypeAlias = "tuple[str, list[MenuStructureItem]] | MenuWidget" CGRAM = """ ...... ...... ...... ...... ..X... ...... ...... ...... XXXXXX XXXXXX XXXXXX XXXXXX X.XX.. .XXXXX ..XXX. .....X ...... XX.... XXXX.. XXXXXX X.XXX. .X...X .X...X ....XX ...... XX.... XXXX.. XXXXXX X.XXXX .X...X .X...X .X.XX. ...... XX.... XXXX.. XXXXXX X.XXX. .X...X .X...X .XXX.. XXXXXX XXXXXX XXXXXX XXXXXX X.XX.. .XXXXX ..XXX. ..X... ...... ...... ...... ...... ..X... ...... ...... ...... ...... ...... ...... ...... ...... ...... ...... ...... """ def program_cgram(screen_inst: CF635Screen) -> None: """Load the character data.""" # convert .'s and X's above into integer data cbuf: list[list[int]] = [[] for x in range(8)] for row in CGRAM.strip().split("\n"): rowsegments = row.strip().split() for num, r in enumerate(rowsegments): accum = 0 for c in r: accum = (accum << 1) + (c == "X") cbuf[num].append(accum) for num, cdata in enumerate(cbuf): screen_inst.program_cgram(num, cdata) class LCDCheckBox(urwid.CheckBox): """A check box+label that uses only one character for the check box, including custom CGRAM character.""" states: typing.ClassVar[dict[bool | Literal["mixed"], urwid.SelectableIcon]] = { True: urwid.SelectableIcon("\xd0"), False: urwid.SelectableIcon("\x05"), } reserve_columns = 1 class LCDRadioButton(urwid.RadioButton): """A radio button+label that uses only one character for the radio button, including custom CGRAM character.""" states: typing.ClassVar[dict[bool | Literal["mixed"], urwid.SelectableIcon]] = { True: urwid.SelectableIcon("\xbb"), False: urwid.SelectableIcon("\x06"), } reserve_columns = 1 class LCDProgressBar(urwid.Widget): """The "progress bar" used by the horizontal slider for this device, using custom CGRAM characters.""" segments = "\x00\x01\x02\x03" _sizing = frozenset([urwid.Sizing.FLOW]) def __init__(self, data_range: int, value: int) -> None: """Initialize with the value's range and its initial position within it.""" super().__init__() self.range = data_range self.value = value def rows(self, size: tuple[int], focus: bool = False) -> int: """Return the number of rows the bar occupies, always 1.""" return 1 def render(self, size: tuple[int], focus: bool = False) -> urwid.Canvas: # type: ignore[override] """Draw the bar with ``self.segments`` where index 0 is empty and index -1 is completely full.""" (maxcol,) = size steps = self.get_steps(size) filled = urwid.int_scale(self.value, self.range, steps) full_segments = int(filled / (len(self.segments) - 1)) last_char = filled % (len(self.segments) - 1) + 1 s = ( self.segments[-1] * full_segments + self.segments[last_char] + self.segments[0] * (maxcol - full_segments - 1) ) return urwid.Text(s).render(size) def move_position(self, size: tuple[int], direction: int) -> int: """Update and return the value one step +ve or -ve, based on the size of the displayed bar. :param direction: 1 for +ve, 0 for -ve """ steps = self.get_steps(size) filled = urwid.int_scale(self.value, self.range, steps) filled += 2 * direction - 1 value = urwid.int_scale(filled, steps, self.range) value = max(0, min(self.range - 1, value)) if value != self.value: self.value = value self._invalidate() return value def get_steps(self, size: tuple[int]) -> int: """Return the number of steps available given size for rendering the bar and number of segments we can draw.""" (maxcol,) = size return maxcol * (len(self.segments) - 1) class LCDHorizontalSlider(urwid.WidgetWrap[urwid.Columns]): """A slider control using custom CGRAM characters.""" def __init__(self, data_range: int, value: int, callback: Callable[[int], None]) -> None: """Initialize with a progress bar over `data_range`, and `callback` to invoke on value changes.""" self.bar = LCDProgressBar(data_range, value) cols = urwid.Columns( [ (1, urwid.SelectableIcon("\x11")), self.bar, (1, urwid.SelectableIcon("\x04")), ] ) super().__init__(cols) self.callback = callback def keypress(self, size: tuple[int], key: str) -> str | None: """Move the slider based on which arrow is focused, or delegate other keys to the wrapped columns.""" # move the slider based on which arrow is focused if key == "enter": # use the correct size for adjusting the bar self.bar.move_position((self._w.column_widths(size)[1],), self._w.focus_position != 0) self.callback(self.bar.value) return None return typing.cast("str | None", super().keypress(size, key)) class MenuOption(urwid.Button): """A menu option, indicated with a single arrow character.""" def __init__(self, label: str, submenu: Menu) -> None: """Initialize with `label` and the `submenu` opened when this option is chosen.""" super().__init__("") # use a Text widget for label, we want the cursor # on the arrow not the label. Button types self._label as SelectableIcon; a Text works # equally well here since only its set_text()/text interface is used. self._label = urwid.Text("") # type: ignore[assignment] self.set_label(label) self._w = urwid.Columns([(1, urwid.SelectableIcon("\xdf")), self._label]) urwid.connect_signal(self, "click", lambda option: show_menu(submenu)) def keypress(self, size: tuple[int], key: str) -> str | None: """Treat :kbd:`right` as :kbd:`enter` to open the submenu, then delegate to the button.""" if key == "right": key = "enter" return super().keypress(size, key) class Menu(urwid.ListBox[int]): """A submenu of :class:`MenuOption` widgets that can return to its parent menu.""" def __init__(self, widgets: list[MenuWidget]) -> None: """Initialize with `widgets` as its options and no parent menu set.""" self.menu_parent: Menu | None = None super().__init__(urwid.SimpleListWalker(widgets)) def keypress(self, size: tuple[int, int], key: str) -> str | None: # type: ignore[override] """Go back to the previous menu when :kbd:`left` or :kbd:`esc` is pressed.""" parsed_key = super().keypress(size, key) if parsed_key in {"left", "esc"} and self.menu_parent: show_menu(self.menu_parent) return None return parsed_key def build_menus() -> Menu: """Build and return the top-level settings menu for the LCD display.""" cursor_option_group: list[urwid.RadioButton] = [] def cursor_option(label: str, style: Literal[1, 2, 3, 4]) -> LCDRadioButton: """Build a radio button that sets the cursor style when selected.""" def on_change(b: urwid.RadioButton, state: bool) -> None: if state: screen.set_cursor_style(style) b = LCDRadioButton(cursor_option_group, label, screen.cursor_style == style) urwid.connect_signal(b, "change", on_change) return b def display_setting(label: str, data_range: int, fn: Callable[[int], None]) -> urwid.Columns: slider = LCDHorizontalSlider(data_range, data_range // 2, fn) return urwid.Columns( [ urwid.Text(label), (10, slider), ] ) def led_custom(index: Literal[0, 1, 2, 3]) -> urwid.Columns: def exp_scale_led(rg: Literal[0, 1]) -> Callable[[int], None]: """Apply an exponential transformation so apparent brightness increases in a natural way.""" return lambda value: screen.set_led_pin( index, rg, [0, 1, 2, 3, 4, 5, 6, 8, 11, 14, 18, 23, 29, 38, 48, 61, 79, 100][value], ) return urwid.Columns( [ (2, urwid.Text(f"{index:d}R")), LCDHorizontalSlider(18, 0, exp_scale_led(0)), (2, urwid.Text(" G")), LCDHorizontalSlider(18, 0, exp_scale_led(1)), ] ) menu_structure: list[MenuStructureItem] = [ ( "Display Settings", [ display_setting("Brightness", 101, screen.set_backlight), display_setting("Contrast", 76, lambda x: screen.set_lcd_contrast(x + 75)), ], ), ( "Cursor Settings", [ cursor_option("Block", screen.CURSOR_BLINKING_BLOCK), cursor_option("Underscore", screen.CURSOR_UNDERSCORE), cursor_option("Block + Underscore", screen.CURSOR_BLINKING_BLOCK_UNDERSCORE), cursor_option("Inverting Block", screen.CURSOR_INVERTING_BLINKING_BLOCK), ], ), ( "LEDs", [ led_custom(0), led_custom(1), led_custom(2), led_custom(3), ], ), ( "About this Demo", [ urwid.Text( "This is a demo of Urwid's CF635Display " "module. If you need an interface for a limited " "character display device this should serve as a " "good example for implementing your own display " "module and menu-driven application." ), ], ), ] def build_submenu(ms: list[MenuStructureItem]) -> Menu: """Recursively build a :class:`Menu` from a list of menu-structure items.""" options: list[MenuWidget] = [] submenus: list[Menu] = [] for opt in ms: # shortform for MenuOptions if isinstance(opt, tuple): name, sub = opt submenu = build_submenu(sub) opt = MenuOption(name, submenu) # noqa: PLW2901 submenus.append(submenu) options.append(opt) menu = Menu(options) for s in submenus: s.menu_parent = menu return menu return build_submenu(menu_structure) screen = urwid.display.lcd.CF635Screen(sys.argv[1]) # set up our font program_cgram(screen) loop = urwid.MainLoop(build_menus(), screen=screen) # FIXME: want screen to know it is in narrow mode, or better yet, # do the unicode conversion for us urwid.set_encoding("narrow") def show_menu(menu: Menu) -> None: """Replace the main loop's displayed widget with *menu*.""" loop.widget = menu loop.run() urwid-urwid-69907e4/examples/palette_test.py000077500000000000000000000541611526073734300211760ustar00rootroot00000000000000#!/usr/bin/env python # # Urwid Palette Test. Showing off highcolor support # Copyright (C) 2004-2009 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """Palette test. Shows the available foreground and background settings in monochrome, 16 color, 88 color, 256 color, and 24-bit (true) color modes. """ from __future__ import annotations import re import typing import urwid if typing.TYPE_CHECKING: from collections.abc import Callable from typing_extensions import Literal CHART_TRUE = """ #e50000#e51000#e52000#e53000#e54000#e55000#e56000#e57000#e58000#e59000\ #e5a000#e5b100#e5c100#e5d100#e5e100#d9e500#c9e500#b9e500#a9e500#99e500\ #89e500#79e500#68e500#58e500#48e500#38e500#28e500#18e500#08e500#00e507\ #00e517#00e527#00e538#00e548#00e558#00e568#00e578#00e588#00e598#00e5a8\ #00e5b8#00e5c8#00e5d8#00e1e5#00d1e5#00c1e5#00b1e5#00a1e5#0091e5#0081e5\ #0071e5#0061e5#0051e5#0040e5#0030e5#0020e5#0010e5#0000e5#0f00e5#1f00e5\ #2f00e5#3f00e5#4f00e5#5f00e5#7000e5#8000e5#9000e5#a000e5#b000e5#c000e5\ #d000e5#e000e5#e500da#e500ca#e500b9#e500a9#e50099#e50089 #da0000#da0f00#da1e00#da2e00#da3d00#da4c00#da5c00#da6b00#da7a00#da8a00\ #da9900#daa800#dab800#dac700#dad600#cfda00#c0da00#b0da00#a1da00#92da00\ #82da00#73da00#64da00#54da00#45da00#35da00#26da00#17da00#07da00#00da07\ #00da16#00da26#00da35#00da44#00da54#00da63#00da72#00da82#00da91#00daa0\ #00dab0#00dabf#00dace#00d7da#00c8da#00b8da#00a9da#0099da#008ada#007bda\ #006bda#005cda#004dda#003dda#002eda#001fda#000fda#0000da#0e00da#1e00da\ #2d00da#3c00da#4c00da#5b00da#6a00da#7a00da#8900da#9800da#a800da#b700da\ #c600da#d600da#da00cf#da00c0#da00b1#da00a1#da0092#da0083 #d00000#d00e00#d01d00#d02b00#d03a00#d04800#d05700#d06600#d07400#d08300\ #d09100#d0a000#d0af00#d0bd00#d0cc00#c5d000#b6d000#a8d000#99d000#8ad000\ #7cd000#6dd000#5fd000#50d000#41d000#33d000#24d000#16d000#07d000#00d007\ #00d015#00d024#00d032#00d041#00d04f#00d05e#00d06d#00d07b#00d08a#00d098\ #00d0a7#00d0b6#00d0c4#00ccd0#00bed0#00afd0#00a1d0#0092d0#0083d0#0075d0\ #0066d0#0058d0#0049d0#003ad0#002cd0#001dd0#000fd0#0000d0#0e00d0#1c00d0\ #2b00d0#3900d0#4800d0#5600d0#6500d0#7400d0#8200d0#9100d0#9f00d0#ae00d0\ #bd00d0#cb00d0#d000c5#d000b7#d000a8#d00099#d0008b#d0007c #c50000#c50d00#c51b00#c52900#c53700#c54500#c55300#c56000#c56e00#c57c00\ #c58a00#c59800#c5a600#c5b300#c5c100#bbc500#adc500#9fc500#91c500#83c500\ #75c500#68c500#5ac500#4cc500#3ec500#30c500#22c500#15c500#07c500#00c506\ #00c514#00c522#00c530#00c53e#00c54b#00c559#00c567#00c575#00c583#00c591\ #00c59e#00c5ac#00c5ba#00c2c5#00b4c5#00a6c5#0098c5#008ac5#007dc5#006fc5\ #0061c5#0053c5#0045c5#0037c5#002ac5#001cc5#000ec5#0000c5#0d00c5#1b00c5\ #2800c5#3600c5#4400c5#5200c5#6000c5#6e00c5#7c00c5#8900c5#9700c5#a500c5\ #b300c5#c100c5#c500bb#c500ad#c5009f#c50092#c50084#c50076 #ba0000#ba0d00#ba1a00#ba2700#ba3400#ba4100#ba4e00#ba5b00#ba6800#ba7500\ #ba8200#ba8f00#ba9c00#baaa00#bab700#b0ba00#a3ba00#96ba00#89ba00#7cba00\ #6fba00#62ba00#55ba00#48ba00#3bba00#2eba00#20ba00#13ba00#06ba00#00ba06\ #00ba13#00ba20#00ba2d#00ba3a#00ba47#00ba54#00ba61#00ba6e#00ba7c#00ba89\ #00ba96#00baa3#00bab0#00b7ba#00aaba#009dba#0090ba#0083ba#0076ba#0069ba\ #005cba#004eba#0041ba#0034ba#0027ba#001aba#000dba#0000ba#0c00ba#1900ba\ #2600ba#3300ba#4000ba#4e00ba#5b00ba#6800ba#7500ba#8200ba#8f00ba#9c00ba\ #a900ba#b600ba#ba00b1#ba00a4#ba0097#ba008a#ba007d#ba006f #af0000#af0c00#af1800#af2400#af3100#af3d00#af4900#af5600#af6200#af6e00\ #af7b00#af8700#af9300#afa000#afac00#a6af00#9aaf00#8eaf00#81af00#75af00\ #69af00#5caf00#50af00#44af00#37af00#2baf00#1faf00#12af00#06af00#00af05\ #00af12#00af1e#00af2a#00af37#00af43#00af4f#00af5c#00af68#00af74#00af81\ #00af8d#00af99#00afa6#00adaf#00a0af#0094af#0088af#007baf#006faf#0063af\ #0056af#004aaf#003eaf#0031af#0025af#0019af#000caf#0000af#0b00af#1800af\ #2400af#3000af#3d00af#4900af#5500af#6200af#6e00af#7a00af#8700af#9300af\ #9f00af#ac00af#af00a7#af009a#af008e#af0082#af0075#af0069 #a50000#a50b00#a51700#a52200#a52e00#a53900#a54500#a55100#a55c00#a56800\ #a57300#a57f00#a58a00#a59600#a5a200#9ca500#90a500#85a500#79a500#6ea500\ #62a500#57a500#4ba500#3fa500#34a500#28a500#1da500#11a500#06a500#00a505\ #00a511#00a51c#00a528#00a533#00a53f#00a54b#00a556#00a562#00a56d#00a579\ #00a584#00a590#00a59c#00a2a5#0096a5#008ba5#007fa5#0074a5#0068a5#005da5\ #0051a5#0045a5#003aa5#002ea5#0023a5#0017a5#000ca5#0000a5#0b00a5#1600a5\ #2200a5#2d00a5#3900a5#4500a5#5000a5#5c00a5#6700a5#7300a5#7e00a5#8a00a5\ #9600a5#a100a5#a5009c#a50091#a50085#a5007a#a5006e#a50063 #9a0000#9a0a00#9a1500#9a2000#9a2b00#9a3600#9a4000#9a4b00#9a5600#9a6100\ #9a6c00#9a7700#9a8100#9a8c00#9a9700#929a00#879a00#7c9a00#719a00#679a00\ #5c9a00#519a00#469a00#3b9a00#309a00#269a00#1b9a00#109a00#059a00#009a05\ #009a10#009a1a#009a25#009a30#009a3b#009a46#009a50#009a5b#009a66#009a71\ #009a7c#009a87#009a91#00979a#008d9a#00829a#00779a#006c9a#00619a#00569a\ #004c9a#00419a#00369a#002b9a#00209a#00169a#000b9a#00009a#0a009a#15009a\ #20009a#2a009a#35009a#40009a#4b009a#56009a#61009a#6b009a#76009a#81009a\ #8c009a#97009a#9a0092#9a0087#9a007d#9a0072#9a0067#9a005c #8f0000#8f0a00#8f1400#8f1e00#8f2800#8f3200#8f3c00#8f4600#8f5000#8f5a00\ #8f6400#8f6e00#8f7800#8f8200#8f8c00#888f00#7e8f00#748f00#698f00#5f8f00\ #558f00#4b8f00#418f00#378f00#2d8f00#238f00#198f00#0f8f00#058f00#008f04\ #008f0e#008f18#008f23#008f2d#008f37#008f41#008f4b#008f55#008f5f#008f69\ #008f73#008f7d#008f87#008d8f#00838f#00798f#006f8f#00658f#005b8f#00508f\ #00468f#003c8f#00328f#00288f#001e8f#00148f#000a8f#00008f#09008f#13008f\ #1d008f#27008f#31008f#3c008f#46008f#50008f#5a008f#64008f#6e008f#78008f\ #82008f#8c008f#8f0088#8f007e#8f0074#8f006a#8f0060#8f0056 #840000#840900#841200#841b00#842500#842e00#843700#844100#844a00#845300\ #845d00#846600#846f00#847900#848200#7d8400#748400#6b8400#628400#588400\ #4f8400#468400#3c8400#338400#2a8400#208400#178400#0e8400#048400#008404\ #00840d#008417#008420#008429#008433#00843c#008445#00844f#008458#008461\ #00846a#008474#00847d#008284#007984#007084#006684#005d84#005484#004a84\ #004184#003884#002e84#002584#001c84#001284#000984#000084#080084#120084\ #1b0084#240084#2e0084#370084#400084#4a0084#530084#5c0084#660084#6f0084\ #780084#820084#84007e#840074#84006b#840062#840059#84004f #7a0000#7a0800#7a1100#7a1900#7a2200#7a2a00#7a3300#7a3b00#7a4400#7a4d00\ #7a5500#7a5e00#7a6600#7a6f00#7a7700#737a00#6b7a00#627a00#5a7a00#517a00\ #487a00#407a00#377a00#2f7a00#267a00#1e7a00#157a00#0d7a00#047a00#007a04\ #007a0c#007a15#007a1d#007a26#007a2e#007a37#007a40#007a48#007a51#007a59\ #007a62#007a6a#007a73#00787a#006f7a#00677a#005e7a#00557a#004d7a#00447a\ #003c7a#00337a#002b7a#00227a#001a7a#00117a#00087a#00007a#08007a#10007a\ #19007a#21007a#2a007a#33007a#3b007a#44007a#4c007a#55007a#5d007a#66007a\ #6f007a#77007a#7a0074#7a006b#7a0062#7a005a#7a0051#7a0049 #6f0000#6f0700#6f0f00#6f1700#6f1f00#6f2700#6f2e00#6f3600#6f3e00#6f4600\ #6f4e00#6f5500#6f5d00#6f6500#6f6d00#696f00#616f00#596f00#526f00#4a6f00\ #426f00#3a6f00#326f00#2b6f00#236f00#1b6f00#136f00#0b6f00#046f00#006f03\ #006f0b#006f13#006f1b#006f23#006f2a#006f32#006f3a#006f42#006f4a#006f51\ #006f59#006f61#006f69#006d6f#00656f#005e6f#00566f#004e6f#00466f#003e6f\ #00366f#002f6f#00276f#001f6f#00176f#000f6f#00086f#00006f#07006f#0f006f\ #17006f#1e006f#26006f#2e006f#36006f#3e006f#46006f#4d006f#55006f#5d006f\ #65006f#6d006f#6f0069#6f0062#6f005a#6f0052#6f004a#6f0042 #640000#640700#640e00#641500#641c00#642300#642a00#643100#643800#643f00\ #644600#644d00#645400#645b00#646200#5f6400#586400#516400#4a6400#436400\ #3c6400#356400#2e6400#266400#1f6400#186400#116400#0a6400#036400#006403\ #00640a#006411#006418#00641f#006426#00642d#006434#00643b#006442#006449\ #006451#006458#00645f#006364#005c64#005464#004d64#004664#003f64#003864\ #003164#002a64#002364#001c64#001564#000e64#000764#000064#060064#0d0064\ #140064#1b0064#230064#2a0064#310064#380064#3f0064#460064#4d0064#540064\ #5b0064#620064#64005f#640058#640051#64004a#640043#64003c #590000#590600#590c00#591200#591900#591f00#592500#592c00#593200#593800\ #593f00#594500#594b00#595100#595800#555900#4e5900#485900#425900#3c5900\ #355900#2f5900#295900#225900#1c5900#165900#0f5900#095900#035900#005903\ #005909#00590f#005915#00591c#005922#005928#00592f#005935#00593b#005942\ #005948#00594e#005955#005859#005259#004b59#004559#003f59#003859#003259\ #002c59#002659#001f59#001959#001359#000c59#000659#000059#060059#0c0059\ #120059#180059#1f0059#250059#2b0059#320059#380059#3e0059#450059#4b0059\ #510059#580059#590055#59004f#590048#590042#59003c#590035 #4f0000#4f0500#4f0b00#4f1000#4f1600#4f1b00#4f2100#4f2600#4f2c00#4f3100\ #4f3700#4f3d00#4f4200#4f4800#4f4d00#4b4f00#454f00#3f4f00#3a4f00#344f00\ #2f4f00#294f00#244f00#1e4f00#194f00#134f00#0d4f00#084f00#024f00#004f02\ #004f08#004f0d#004f13#004f18#004f1e#004f23#004f29#004f2f#004f34#004f3a\ #004f3f#004f45#004f4a#004d4f#00484f#00424f#003d4f#00374f#00324f#002c4f\ #00274f#00214f#001b4f#00164f#00104f#000b4f#00054f#00004f#05004f#0a004f\ #10004f#16004f#1b004f#21004f#26004f#2c004f#31004f#37004f#3c004f#42004f\ #47004f#4d004f#4f004b#4f0045#4f0040#4f003a#4f0035#4f002f #440000#440400#440900#440e00#441300#441800#441c00#442100#442600#442b00\ #443000#443400#443900#443e00#444300#404400#3c4400#374400#324400#2d4400\ #284400#244400#1f4400#1a4400#154400#104400#0c4400#074400#024400#004402\ #004407#00440b#004410#004415#00441a#00441f#004423#004428#00442d#004432\ #004437#00443b#004440#004344#003e44#003944#003444#003044#002b44#002644\ #002144#001c44#001844#001344#000e44#000944#000444#000044#040044#090044\ #0e0044#130044#170044#1c0044#210044#260044#2b0044#2f0044#340044#390044\ #3e0044#430044#440041#44003c#440037#440032#44002d#440029 #390000#390400#390800#390c00#391000#391400#391800#391c00#392000#392400\ #392800#392c00#393000#393400#393800#363900#323900#2e3900#2a3900#263900\ #223900#1e3900#1a3900#163900#123900#0e3900#0a3900#063900#023900#003901\ #003905#00390a#00390e#003912#003916#00391a#00391e#003922#003926#00392a\ #00392e#003932#003936#003839#003439#003039#002c39#002839#002439#002039\ #001c39#001839#001439#001039#000c39#000839#000439#000039#030039#070039\ #0b0039#100039#140039#180039#1c0039#200039#240039#280039#2c0039#300039\ #340039#380039#390036#390032#39002e#39002a#390026#390022 #2e0000#2e0300#2e0600#2e0900#2e0d00#2e1000#2e1300#2e1700#2e1a00#2e1d00\ #2e2000#2e2400#2e2700#2e2a00#2e2e00#2c2e00#292e00#252e00#222e00#1f2e00\ #1c2e00#182e00#152e00#122e00#0e2e00#0b2e00#082e00#052e00#012e00#002e01\ #002e04#002e08#002e0b#002e0e#002e12#002e15#002e18#002e1b#002e1f#002e22\ #002e25#002e29#002e2c#002e2e#002a2e#00272e#00242e#00212e#001d2e#001a2e\ #00172e#00132e#00102e#000d2e#000a2e#00062e#00032e#00002e#03002e#06002e\ #09002e#0d002e#10002e#13002e#16002e#1a002e#1d002e#20002e#24002e#27002e\ #2a002e#2d002e#2e002c#2e0029#2e0026#2e0022#2e001f#2e001c #240000#240200#240500#240700#240a00#240c00#240f00#241100#241400#241600\ #241900#241b00#241e00#242100#242300#222400#1f2400#1d2400#1a2400#182400\ #152400#132400#102400#0e2400#0b2400#082400#062400#032400#012400#002401\ #002403#002406#002408#00240b#00240d#002410#002413#002415#002418#00241a\ #00241d#00241f#002422#002324#002124#001e24#001c24#001924#001624#001424\ #001124#000f24#000c24#000a24#000724#000524#000224#000024#020024#040024\ #070024#0a0024#0c0024#0f0024#110024#140024#160024#190024#1b0024#1e0024\ #200024#230024#240022#24001f#24001d#24001a#240018#240015 #190000#190100#190300#190500#190700#190800#190a00#190c00#190e00#191000\ #191100#191300#191500#191700#191900#181900#161900#141900#121900#111900\ #0f1900#0d1900#0b1900#091900#081900#061900#041900#021900#001900#001900\ #001902#001904#001906#001908#001909#00190b#00190d#00190f#001910#001912\ #001914#001916#001918#001919#001719#001519#001319#001119#001019#000e19\ #000c19#000a19#000919#000719#000519#000319#000119#000019#010019#030019\ #050019#070019#080019#0a0019#0c0019#0e0019#100019#110019#130019#150019\ #170019#180019#190018#190016#190014#190012#190011#19000f """ # raise Exception(CHART_TRUE) CHART_256 = """ brown__ dark_red_ dark_magenta_ dark_blue_ dark_cyan_ dark_green_ yellow_ light_red light_magenta light_blue light_cyan light_green #00f#06f#08f#0af#0df#0ff black_______ dark_gray___ #60f#00d#06d#08d#0ad#0dd#0fd light_gray__ white_______ #80f#60d#00a#06a#08a#0aa#0da#0fa #a0f#80d#60a#008#068#088#0a8#0d8#0f8 #d0f#a0d#80d#608#006#066#086#0a6#0d6#0f6 #f0f#d0d#a0a#808#606#000#060#080#0a0#0d0#0f0#0f6#0f8#0fa#0fd#0ff #f0d#d0a#a08#806#600#660#680#6a0#6d0#6f0#6f6#6f8#6fa#6fd#6ff#0df #f0a#d08#a06#800#860#880#8a0#8d0#8f0#8f6#8f8#8fa#8fd#8ff#6df#0af #f08#d06#a00#a60#a80#aa0#ad0#af0#af6#af8#afa#afd#aff#8df#6af#08f #f06#d00#d60#d80#da0#dd0#df0#df6#df8#dfa#dfd#dff#adf#8af#68f#06f #f00#f60#f80#fa0#fd0#ff0#ff6#ff8#ffa#ffd#fff#ddf#aaf#88f#66f#00f #fd0#fd6#fd8#fda#fdd#fdf#daf#a8f#86f#60f #66d#68d#6ad#6dd #fa0#fa6#fa8#faa#fad#faf#d8f#a6f#80f #86d#66a#68a#6aa#6da #f80#f86#f88#f8a#f8d#f8f#d6f#a0f #a6d#86a#668#688#6a8#6d8 #f60#f66#f68#f6a#f6d#f6f#d0f #d6d#a6a#868#666#686#6a6#6d6#6d8#6da#6dd #f00#f06#f08#f0a#f0d#f0f #d6a#a68#866#886#8a6#8d6#8d8#8da#8dd#6ad #d68#a66#a86#aa6#ad6#ad8#ada#add#8ad#68d #d66#d86#da6#dd6#dd8#dda#ddd#aad#88d#66d g78_g82_g85_g89_g93_g100 #da6#da8#daa#dad#a8d#86d g52_g58_g62_g66_g70_g74_ #88a#8aa #d86#d88#d8a#d8d#a6d g27_g31_g35_g38_g42_g46_g50_ #a8a#888#8a8#8aa #d66#d68#d6a#d6d g0__g3__g7__g11_g15_g19_g23_ #a88#aa8#aaa#88a #a88#a8a """ CHART_88 = """ brown__ dark_red_ dark_magenta_ dark_blue_ dark_cyan_ dark_green_ yellow_ light_red light_magenta light_blue light_cyan light_green #00f#08f#0cf#0ff black_______ dark_gray___ #80f#00c#08c#0cc#0fc light_gray__ white_______ #c0f#80c#008#088#0c8#0f8 #f0f#c0c#808#000#080#0c0#0f0#0f8#0fc#0ff #88c#8cc #f0c#c08#800#880#8c0#8f0#8f8#8fc#8ff#0cf #c8c#888#8c8#8cc #f08#c00#c80#cc0#cf0#cf8#cfc#cff#8cf#08f #c88#cc8#ccc#88c #f00#f80#fc0#ff0#ff8#ffc#fff#ccf#88f#00f #c88#c8c #fc0#fc8#fcc#fcf#c8f#80f #f80#f88#f8c#f8f#c0f g62_g74_g82_g89_g100 #f00#f08#f0c#f0f g0__g19_g35_g46_g52 """ CHART_16 = """ brown__ dark_red_ dark_magenta_ dark_blue_ dark_cyan_ dark_green_ yellow_ light_red light_magenta light_blue light_cyan light_green black_______ dark_gray___ light_gray__ white_______ """ ATTR_RE = re.compile(r"(?P[ \n]*)(?P(?:#[0-9A-Fa-f]{6})|(?:#[0-9A-Fa-f]{3})|(?:[^ \n]+))") LONG_ATTR = 7 SHORT_ATTR = 4 # length of short high-colour descriptions which may # be packed one after the next def parse_chart( chart: str, convert: Callable[[str], tuple[urwid.AttrSpec, str] | None], ) -> list[str | tuple[urwid.AttrSpec, str]]: """Convert string chart into text markup with the correct attributes. :param chart: palette chart as a string :param convert: function that converts a single palette entry to an (attr, text) tuple, or ``None`` if no match is found :returns: text markup for the chart """ out = [] for match in ATTR_RE.finditer(chart): if match.group("whitespace"): out.append(match.group("whitespace")) entry = match.group("entry") entry = entry.replace("_", " ") while entry: if chart == CHART_TRUE and len(entry) == LONG_ATTR: attrtext = convert(entry[:LONG_ATTR]) elen = LONG_ATTR else: elen = SHORT_ATTR attrtext = convert(entry[:SHORT_ATTR]) # try the first four characters if attrtext: entry = entry[elen:].strip() else: # try the whole thing attrtext = convert(entry.strip()) assert attrtext, f"Invalid palette entry: {entry!r}" # noqa: S101 # "assert" is OK for examples elen = len(entry) entry = "" attr, text = attrtext if chart == CHART_TRUE: out.append((attr, "â–„")) else: out.append((attr, text.ljust(elen))) return out def foreground_chart( chart: str, background: str, colors: Literal[1, 16, 88, 256, 16777216], ) -> list[str | tuple[urwid.AttrSpec, str]]: """Create text markup for a foreground colour chart. :param chart: palette chart as string :param background: colour to use for background of chart :param colors: number of colors :returns: text markup for the chart """ def convert_foreground(entry: str) -> tuple[urwid.AttrSpec, str] | None: try: attr = urwid.AttrSpec(entry, background, colors) except urwid.AttrSpecError: return None return attr, entry return parse_chart(chart, convert_foreground) def background_chart( chart: str, foreground: str, colors: Literal[1, 16, 88, 256, 16777216], ) -> list[str | tuple[urwid.AttrSpec, str]]: """Create text markup for a background colour chart. Remaps 8 <= colour < 16 to high-colour versions in the hopes of greater compatibility. :param chart: palette chart as string :param foreground: colour to use for foreground of chart :param colors: number of colors :returns: text markup for the chart """ def convert_background(entry: str) -> tuple[urwid.AttrSpec, str] | None: try: attr = urwid.AttrSpec(foreground, entry, colors) except urwid.AttrSpecError: return None # fix 8 <= colour < 16 if colors > 16 and attr.background_basic and attr.background_number >= 8: # use high-colour with same number entry = f"h{attr.background_number:d}" attr = urwid.AttrSpec(foreground, entry, colors) return attr, entry return parse_chart(chart, convert_background) def main() -> None: """Run the palette test example program.""" palette = [ ("header", "black,underline", "light gray", "standout,underline", "black,underline", "#88a"), ("panel", "light gray", "dark blue", "", "#ffd", "#00a"), ("focus", "light gray", "dark cyan", "standout", "#ff8", "#806"), ] screen = urwid.display.raw.Screen() screen.register_palette(palette) lb: urwid.SimpleListWalker[urwid.widget.AbstractFlowWidget] = urwid.SimpleListWalker([]) chart_offset = 0 # offset of chart in lb list mode_radio_buttons: list[urwid.RadioButton] = [] chart_radio_buttons: list[urwid.RadioButton] = [] def fcs(widget: urwid.AbstractWidget) -> urwid.AttrMap[urwid.AbstractWidget]: # wrap widgets that can take focus return urwid.AttrMap(widget, None, "focus") def set_mode(colors: Literal[1, 16, 88, 256, 16777216], is_foreground_chart: bool) -> None: # set terminal mode and redraw chart screen.set_terminal_properties(colors) screen.reset_default_terminal_palette() chart_fn = (background_chart, foreground_chart)[is_foreground_chart] if colors == 1: lb[chart_offset] = urwid.Divider() else: chart: str = {16: CHART_16, 88: CHART_88, 256: CHART_256, 2**24: CHART_TRUE}[colors] txt = chart_fn(chart, "default", colors) lb[chart_offset] = urwid.Text(txt, wrap=urwid.CLIP) def on_mode_change(colors: Literal[1, 16, 88, 256, 16777216], rb: urwid.RadioButton, state: bool) -> None: # if this radio button is checked if state: is_foreground_chart = chart_radio_buttons[0].state set_mode(colors, typing.cast("bool", is_foreground_chart)) def mode_rb(text: str, colors: int, state: bool = False) -> urwid.AttrMap[urwid.RadioButton]: # mode radio buttons rb = urwid.RadioButton(mode_radio_buttons, text, state) urwid.connect_signal(rb, "change", on_mode_change, user_args=(colors,)) return fcs(rb) def on_chart_change(rb: urwid.RadioButton, state: bool) -> None: # handle foreground check box state change set_mode(screen.colors, state) def click_exit(button: urwid.Button) -> typing.NoReturn: raise urwid.ExitMainLoop() lb.extend( [ urwid.AttrMap(urwid.Text("Urwid Palette Test"), "header"), urwid.AttrMap( urwid.Columns( [ urwid.Pile( [ mode_rb("Monochrome", 1), mode_rb("16-Color", 16, True), mode_rb("88-Color", 88), mode_rb("256-Color", 256), mode_rb("24-bit Color", 2**24), ] ), urwid.Pile( [ fcs(urwid.RadioButton(chart_radio_buttons, "Foreground Colors", True, on_chart_change)), fcs(urwid.RadioButton(chart_radio_buttons, "Background Colors")), urwid.Divider(), fcs(urwid.Button("Exit", click_exit)), ] ), ] ), "panel", ), ] ) chart_offset = len(lb) lb.extend([urwid.Divider()]) # placeholder for the chart set_mode(16, True) # displays the chart def unhandled_input(key: str | tuple[str, int, int, int]) -> None: if key in {"Q", "q", "esc"}: raise urwid.ExitMainLoop() urwid.MainLoop(urwid.ListBox(lb), screen=screen, unhandled_input=unhandled_input).run() if __name__ == "__main__": main() urwid-urwid-69907e4/examples/pop_up.py000077500000000000000000000045641526073734300200050ustar00rootroot00000000000000#!/usr/bin/env python """Example: a button that opens a pop-up dialog attached to it.""" from __future__ import annotations import typing import urwid if typing.TYPE_CHECKING: from urwid.widget.popup import PopUpParametersModel class PopUpDialog(urwid.WidgetWrap[urwid.AttrMap[urwid.Filler[urwid.Pile]]]): """A dialog that appears with nothing but a close button.""" signals: typing.ClassVar[list[str]] = ["close"] def __init__(self) -> None: """Initialize with the message text and a button that emits the "close" signal.""" close_button = urwid.Button("that's pretty cool") urwid.connect_signal(close_button, "click", lambda button: self._emit("close")) pile = urwid.Pile( [ urwid.Text("^^ I'm attached to the widget that opened me. Try resizing the window!\n"), close_button, ] ) super().__init__(urwid.AttrMap(urwid.Filler(pile), "popbg")) class ThingWithAPopUp(urwid.PopUpLauncher[urwid.Button]): """A button that opens a :class:`PopUpDialog` when clicked.""" def __init__(self) -> None: """Initialize wrapping a "click-me" button that opens the pop-up when clicked.""" super().__init__(urwid.Button("click-me")) urwid.connect_signal(self.original_widget, "click", lambda button: self.open_pop_up()) def create_pop_up(self) -> PopUpDialog: """Return a new pop-up dialog wired to close itself on click.""" pop_up = PopUpDialog() urwid.connect_signal(pop_up, "close", lambda button: self.close_pop_up()) return pop_up def get_pop_up_parameters(self) -> PopUpParametersModel: """Return the position and size of the pop-up relative to this widget.""" return {"left": 0, "top": 1, "overlay_width": 32, "overlay_height": 7} def keypress(self, size: tuple[()] | tuple[int] | tuple[int, int], key: str) -> str | None: """Handle a keypress, exiting the program on :kbd:`q` or :kbd:`Q`. :raises urwid.ExitMainLoop: when the pressed key is :kbd:`q` or :kbd:`Q`. """ parsed = super().keypress(size, key) if parsed in {"q", "Q"}: raise urwid.ExitMainLoop("Done") return parsed fill = urwid.Filler(urwid.Padding(ThingWithAPopUp(), urwid.CENTER, 15)) loop = urwid.MainLoop(fill, [("popbg", "white", "dark blue")], pop_ups=True) loop.run() urwid-urwid-69907e4/examples/subproc.py000077500000000000000000000031421526073734300201470ustar00rootroot00000000000000#!/usr/bin/env python """Example: monitor and display the output of a subprocess while accepting input.""" from __future__ import annotations import os import subprocess import sys import typing import urwid factor_me = 362923067964327863989661926737477737673859044111968554257667 run_me = os.path.join(os.path.dirname(sys.argv[0]), "subproc2.py") output_widget = urwid.Text(f"Factors of {factor_me:d}:\n") edit_widget = urwid.Edit("Type anything or press enter to exit:") frame_widget = urwid.Frame[urwid.Filler[urwid.Text], urwid.Edit, None]( header=edit_widget, body=urwid.Filler(output_widget, valign=urwid.BOTTOM), focus_part="header", ) def exit_on_enter(key: str | tuple[str, int, int, int]) -> None: """Handle keys not consumed by the widgets: :kbd:`enter` to exit. :param key: unhandled key or mouse event :raises urwid.ExitMainLoop: when the user presses :kbd:`enter` """ if key == "enter": raise urwid.ExitMainLoop() loop = urwid.MainLoop(frame_widget, unhandled_input=exit_on_enter) def received_output(data: bytes) -> bool: """Append data received from the subprocess to the output widget. :param data: bytes read from the subprocess's standard output :returns: ``True`` to keep the pipe watch registered """ output_widget.set_text(typing.cast("str", output_widget.text) + data.decode("utf8")) return True write_fd = loop.watch_pipe(received_output) with subprocess.Popen( # noqa: S603 ["python", "-u", run_me, str(factor_me)], # noqa: S607 # Example can be insecure stdout=write_fd, close_fds=True, ) as proc: loop.run() urwid-urwid-69907e4/examples/subproc2.py000066400000000000000000000004031526073734300202230ustar00rootroot00000000000000"""Print the factors of the number given as the argument. The subprocess run by the subproc.py example. """ from __future__ import annotations import sys num = int(sys.argv[1]) for c in range(1, 10000000): if num % c == 0: print("factor:", c) urwid-urwid-69907e4/examples/terminal.py000077500000000000000000000057301526073734300203120ustar00rootroot00000000000000#!/usr/bin/env python # # Urwid terminal emulation widget example app # Copyright (C) 2010 aszlig # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """Example: a terminal emulator widget running a shell inside an Urwid application.""" from __future__ import annotations import typing from contextlib import suppress import urwid def main() -> None: """Run the terminal emulation widget demo.""" urwid.set_encoding("utf8") shared_event_loop = urwid.SelectEventLoop() term = urwid.Terminal(None, encoding="utf-8", main_loop=shared_event_loop) size_widget = urwid.Text("") mainframe = urwid.LineBox( urwid.Pile( [ (urwid.WEIGHT, 70, term), (1, urwid.Filler(size_widget)), (1, urwid.Filler(urwid.Edit("focus test edit: "))), ] ), ) def set_title(widget: urwid.Terminal, title: str) -> None: mainframe.set_title(title) def execute_quit(*args: typing.Any, **kwargs: typing.Any) -> typing.NoReturn: raise urwid.ExitMainLoop() def handle_key(key: str | tuple[str, int, int, int]) -> None: if key in {"q", "Q"}: execute_quit() def handle_resize(widget: urwid.Terminal, size: tuple[int, int]) -> None: size_widget.set_text(f"Terminal size: [{size[0]}, {size[1]}]") urwid.connect_signal(term, "title", set_title) urwid.connect_signal(term, "closed", execute_quit) with suppress(NameError): urwid.connect_signal(term, "resize", handle_resize) # if using a version of Urwid library where vterm doesn't support # resize, don't register the signal handler. try: # create Screen with bracketed paste mode support enabled bpm_screen = urwid.display.raw.Screen(bracketed_paste_mode=True) # pylint: disable=unexpected-keyword-arg except TypeError: # if using a version of Urwid library that doesn't support # bracketed paste mode, do without it. bpm_screen = urwid.display.raw.Screen() loop = urwid.MainLoop( mainframe, handle_mouse=False, screen=bpm_screen, unhandled_input=handle_key, event_loop=shared_event_loop, ) loop.run() if __name__ == "__main__": main() urwid-urwid-69907e4/examples/tour.py000077500000000000000000000364171526073734300174760ustar00rootroot00000000000000#!/usr/bin/env python # # Urwid tour. It slices, it dices.. # Copyright (C) 2004-2011 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """Urwid tour. Shows many of the standard widget types and features. """ from __future__ import annotations import urwid def main() -> None: """Build and run the tour of standard widget types and features.""" text_header = "Welcome to the urwid tour! UP / DOWN / PAGE UP / PAGE DOWN scroll. F8 exits." text_intro: list[str | tuple[str, str]] = [ ("important", "Text"), ( " widgets are the most common in " "any urwid program. This Text widget was created " "without setting the wrap or align mode, so it " "defaults to left alignment with wrapping on space " "characters. " ), ("important", "Change the window width"), " to see how the widgets on this page react. This Text widget is wrapped with a ", ("important", "Padding"), " widget to keep it indented on the left and right.", ] text_right = "This Text widget is right aligned. Wrapped words stay to the right as well. " text_center = "This one is center aligned." text_clip = ( "Text widgets may be clipped instead of wrapped.\n" "Extra text is discarded instead of wrapped to the next line. " "65-> 70-> 75-> 80-> 85-> 90-> 95-> 100>\n" "Newlines embedded in the string are still respected." ) text_right_clip = ( "This is a right aligned and clipped Text widget.\n" "<100 <-95 <-90 <-85 <-80 <-75 <-70 <-65 " "Text will be cut off at the left of this widget." ) text_center_clip = "Center aligned and clipped widgets will have text cut off both sides." text_ellipsis = ( "Text can be clipped using the ellipsis character (…)\n" "Extra text is discarded and a … mark is shown." "50-> 55-> 60-> 65-> 70-> 75-> 80-> 85-> 90-> 95-> 100>\n" ) text_any = ( "The 'any' wrap mode will wrap on any character. This " "mode will not collapse space characters at the end of the " "line but it still honors embedded newline characters.\n" "Like this one." ) text_padding = ( "Padding widgets have many options. This " "is a standard Text widget wrapped with a Padding widget " "with the alignment set to relative 20% and with its width " "fixed at 40." ) text_divider: list[str | tuple[str, str]] = [ "The ", ("important", "Divider"), " widget repeats the same character across the whole line. It can also add blank lines above and below.", ] text_edit: list[str | tuple[str, str]] = [ "The ", ("important", "Edit"), ( " widget is a simple text editing widget. It supports cursor " "movement and tries to maintain the current column when focus " "moves to another edit widget. It wraps and aligns the same " "way as Text widgets." ), ] text_edit_cap1 = ("editcp", "This is a caption. Edit here: ") text_edit_text1 = "editable stuff" text_edit_cap2 = ("editcp", "This one supports newlines: ") text_edit_text2 = ( "line one starts them all\n" "== line 2 == with some more text to edit.. words.. whee..\n" "LINE III, the line to end lines one and two, unless you " "change something." ) text_edit_cap3 = ("editcp", "This one is clipped, try editing past the edge: ") text_edit_text3 = "add some text here -> -> -> ...." text_edit_alignments = "Different Alignments:" text_edit_left = "left aligned (default)" text_edit_center = "center aligned" text_edit_right = "right aligned" text_intedit: tuple[str, list[str | tuple[str, str]]] = ( "editcp", [("important", "IntEdit"), " allows only numbers: "], ) text_edit_padding = ("editcp", "Edit widget within a Padding widget ") text_columns1: list[str | tuple[str, str]] = [ ("important", "Columns"), ( " are used to share horizontal screen space. " "This one splits the space into two parts with " "three characters between each column. The " "contents of each column is a single widget." ), ] text_columns2: list[str | tuple[str, str]] = [ "When you need to put more than one widget into a column you can use a ", ("important", "Pile"), " to combine two or more widgets.", ] text_col_columns = "Columns may be placed inside other columns." text_col_21 = "Col 2.1" text_col_22 = "Col 2.2" text_col_23 = "Col 2.3" text_column_widths = ( "Columns may also have uneven relative " "weights or fixed widths. Use a minimum width so that " "columns don't become too small." ) text_weight = "Weight %d" text_fixed_9 = "" # should be 9 columns wide text_fixed_14 = "<--Fixed 14-->" # should be 14 columns wide text_edit_col_cap1 = ("editcp", "Edit widget within Columns") text_edit_col_text1 = "here's\nsome\ninfo" text_edit_col_cap2 = ("editcp", "and within Pile ") text_edit_col_text2 = "more" text_edit_col_cap3 = ("editcp", "another ") text_edit_col_text3 = "still more" text_gridflow: list[str | tuple[str, str]] = [ "A ", ("important", "GridFlow"), ( " widget " "may be used to display a list of flow widgets with equal " "widths. Widgets that don't fit on the first line will " "flow to the next. This is useful for small widgets that " "you want to keep together such as " ), ("important", "Button"), ", ", ("important", "CheckBox"), " and ", ("important", "RadioButton"), " widgets.", ] text_button_list = ["Yes", "No", "Perhaps", "Certainly", "Partially", "Tuesdays Only", "Help"] text_cb_list = ["Wax", "Wash", "Buff", "Clear Coat", "Dry", "Racing Stripe"] text_rb_list = ["Morning", "Afternoon", "Evening", "Weekend"] text_listbox: list[str | tuple[str, str]] = [ "All these widgets have been displayed with the help of a ", ("important", "ListBox"), " widget. ListBox widgets handle scrolling and changing focus. A ", ("important", "Frame"), " widget is used to keep the instructions at the top of the screen.", ] def button_press(button: urwid.Button) -> None: frame.footer = urwid.AttrMap(urwid.Text(["Pressed: ", button.get_label()]), "header") radio_button_group: list[urwid.RadioButton] = [] blank = urwid.Divider() listbox_content: list[urwid.widget.AbstractFlowWidget] = [ blank, urwid.Padding(urwid.Text(text_intro), left=2, right=2, min_width=20), blank, urwid.Text(text_right, align=urwid.RIGHT), blank, urwid.Text(text_center, align=urwid.CENTER), blank, urwid.Text(text_clip, wrap=urwid.CLIP), blank, urwid.Text(text_right_clip, align=urwid.RIGHT, wrap=urwid.CLIP), blank, urwid.Text(text_center_clip, align=urwid.CENTER, wrap=urwid.CLIP), blank, urwid.Text(text_ellipsis, wrap=urwid.ELLIPSIS), blank, urwid.Text(text_any, wrap=urwid.ANY), blank, urwid.Padding(urwid.Text(text_padding), (urwid.RELATIVE, 20), 40), blank, urwid.AttrMap(urwid.Divider("=", 1), "bright"), urwid.Padding(urwid.Text(text_divider), left=2, right=2, min_width=20), urwid.AttrMap(urwid.Divider("-", 0, 1), "bright"), blank, urwid.Padding(urwid.Text(text_edit), left=2, right=2, min_width=20), blank, urwid.AttrMap(urwid.Edit(text_edit_cap1, text_edit_text1), "editbx", "editfc"), blank, urwid.AttrMap(urwid.Edit(text_edit_cap2, text_edit_text2, multiline=True), "editbx", "editfc"), blank, urwid.AttrMap(urwid.Edit(text_edit_cap3, text_edit_text3, wrap=urwid.CLIP), "editbx", "editfc"), blank, urwid.Text(text_edit_alignments), urwid.AttrMap(urwid.Edit("", text_edit_left, align=urwid.LEFT), "editbx", "editfc"), urwid.AttrMap(urwid.Edit("", text_edit_center, align=urwid.CENTER), "editbx", "editfc"), urwid.AttrMap(urwid.Edit("", text_edit_right, align=urwid.RIGHT), "editbx", "editfc"), blank, urwid.AttrMap(urwid.IntEdit(text_intedit, 123), "editbx", "editfc"), blank, urwid.Padding(urwid.AttrMap(urwid.Edit(text_edit_padding, ""), "editbx", "editfc"), left=10, width=50), blank, blank, urwid.AttrMap( urwid.Columns( [ urwid.Divider("."), urwid.Divider(","), urwid.Divider("."), ] ), "bright", ), blank, urwid.Columns( [ urwid.Padding(urwid.Text(text_columns1), left=2, right=0, min_width=20), urwid.Pile([urwid.Divider("~"), urwid.Text(text_columns2), urwid.Divider("_")]), ], 3, ), blank, blank, urwid.Columns( [ urwid.Text(text_col_columns), urwid.Columns( [ urwid.Text(text_col_21), urwid.Text(text_col_22), urwid.Text(text_col_23), ], 1, ), ], 2, ), blank, urwid.Padding(urwid.Text(text_column_widths), left=2, right=2, min_width=20), blank, urwid.Columns( [ urwid.AttrMap(urwid.Text(text_weight % 1), "reverse"), (urwid.WEIGHT, 2, urwid.Text(text_weight % 2)), (urwid.WEIGHT, 3, urwid.AttrMap(urwid.Text(text_weight % 3), "reverse")), (urwid.WEIGHT, 4, urwid.Text(text_weight % 4)), (urwid.WEIGHT, 5, urwid.AttrMap(urwid.Text(text_weight % 5), "reverse")), (urwid.WEIGHT, 6, urwid.Text(text_weight % 6)), ], 0, min_width=8, ), blank, urwid.Columns( [ (urwid.WEIGHT, 2, urwid.AttrMap(urwid.Text(text_weight % 2), "reverse")), (9, urwid.Text(text_fixed_9)), (urwid.WEIGHT, 3, urwid.AttrMap(urwid.Text(text_weight % 3), "reverse")), (14, urwid.Text(text_fixed_14)), ], 0, min_width=8, ), blank, urwid.Columns( [ urwid.AttrMap(urwid.Edit(text_edit_col_cap1, text_edit_col_text1, multiline=True), "editbx", "editfc"), urwid.Pile( [ urwid.AttrMap(urwid.Edit(text_edit_col_cap2, text_edit_col_text2), "editbx", "editfc"), blank, urwid.AttrMap(urwid.Edit(text_edit_col_cap3, text_edit_col_text3), "editbx", "editfc"), ] ), ], 1, ), blank, urwid.AttrMap( urwid.Columns( [ urwid.Divider("'"), urwid.Divider('"'), urwid.Divider("~"), urwid.Divider('"'), urwid.Divider("'"), ] ), "bright", ), blank, blank, urwid.Padding(urwid.Text(text_gridflow), left=2, right=2, min_width=20), blank, urwid.Padding( urwid.GridFlow( [urwid.AttrMap(urwid.Button(txt, button_press), "buttn", "buttnf") for txt in text_button_list], 13, 3, 1, urwid.LEFT, ), left=4, right=3, min_width=13, ), blank, urwid.Padding( urwid.GridFlow( [urwid.AttrMap(urwid.CheckBox(txt), "buttn", "buttnf") for txt in text_cb_list], 10, 3, 1, urwid.LEFT, ), left=4, right=3, min_width=10, ), blank, urwid.Padding( urwid.GridFlow( [urwid.AttrMap(urwid.RadioButton(radio_button_group, txt), "buttn", "buttnf") for txt in text_rb_list], 13, 3, 1, urwid.LEFT, ), left=4, right=3, min_width=13, ), blank, blank, urwid.Padding(urwid.Text(text_listbox), left=2, right=2, min_width=20), blank, blank, ] header = urwid.AttrMap(urwid.Text(text_header), "header") listbox = urwid.ListBox(urwid.SimpleListWalker(listbox_content)) scrollable = urwid.ScrollBar( listbox, trough_char=urwid.ScrollBar.Symbols.LITE_SHADE, ) frame: urwid.Frame[ urwid.AttrMap[urwid.ScrollBar[urwid.ListBox[int]]], urwid.AttrMap[urwid.Text] | None, urwid.AttrMap[urwid.Text] | None, ] = urwid.Frame( urwid.AttrMap(scrollable, "body"), header=header, ) palette: list[tuple[str, str, str] | tuple[str, str, str, str]] = [ ("body", "black", "light gray", "standout"), ("reverse", "light gray", "black"), ("header", "white", "dark red", "bold"), ("important", "dark blue", "light gray", "standout,underline"), ("editfc", "white", "dark blue", "bold"), ("editbx", "light gray", "dark blue"), ("editcp", "black", "light gray", "standout"), ("bright", "dark gray", "light gray", "bold,standout"), ("buttn", "black", "dark cyan"), ("buttnf", "white", "dark blue", "bold"), ] # use appropriate Screen class if urwid.display.web.is_web_request(): screen = urwid.display.web.Screen() else: screen = urwid.display.raw.Screen() def unhandled(key: str | tuple[str, int, int, int]) -> None: if key == "f8": raise urwid.ExitMainLoop() urwid.MainLoop(frame, palette, screen, unhandled_input=unhandled).run() def setup() -> None: """Run the tour as a web request when applicable, otherwise as a standalone program.""" urwid.display.web.set_preferences("Urwid Tour") # try to handle short web requests quickly if urwid.display.web.handle_short_request(): return main() if __name__ == "__main__" or urwid.display.web.is_web_request(): setup() urwid-urwid-69907e4/examples/treesample.py000077500000000000000000000140421526073734300206340ustar00rootroot00000000000000#!/usr/bin/env python # # Trivial data browser # This version: # Copyright (C) 2010 Rob Lanphier # Derived from browse.py in urwid distribution # Copyright (C) 2004-2007 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """ Urwid example lazy directory browser / tree view. Features: - custom selectable widgets for files and directories - custom message widgets to identify access errors and empty directories - custom list walker for displaying widgets in a tree fashion """ from __future__ import annotations import typing import urwid if typing.TYPE_CHECKING: from collections.abc import Hashable from typing_extensions import NotRequired class SampleTree(typing.TypedDict): """Data for a single node of the example tree: a name and, for a parent node, its children.""" name: str children: NotRequired[list[SampleTree]] class ExampleTreeWidget(urwid.TreeWidget["ExampleNode | ExampleParentNode"]): """Display widget for tree nodes.""" def get_display_text(self) -> str | tuple[Hashable, str] | list[str | tuple[Hashable, str]]: """Return the node's name to display.""" return self.get_node().get_value()["name"] class ExampleNode(urwid.TreeNode[SampleTree]): """Data storage object for leaf nodes.""" def load_widget(self) -> ExampleTreeWidget: """Return a new widget for this node.""" return ExampleTreeWidget(self) class ExampleParentNode(urwid.ParentNode[SampleTree]): """Data storage object for interior/parent nodes.""" def load_widget(self) -> ExampleTreeWidget: """Return a new widget for this node.""" return ExampleTreeWidget(self) def load_child_keys(self) -> tuple[int, ...]: """Return the indices of this node's children.""" data = self.get_value() return tuple(range(len(data["children"]))) def load_child_node( self, key: int, # type: ignore[override] # We have explicit type ) -> ExampleParentNode | ExampleNode: """Return the child node at a given index. :param key: index of the child within this node's children :returns: an :class:`ExampleParentNode` if the child has children of its own, an :class:`ExampleNode` otherwise """ childdata = self.get_value()["children"][key] childdepth = self.get_depth() + 1 if "children" in childdata: return ExampleParentNode(childdata, parent=self, key=key, depth=childdepth) return ExampleNode(childdata, parent=self, key=key, depth=childdepth) class ExampleTreeBrowser: """Interactive browser for the example tree.""" palette: typing.ClassVar[list[tuple[str, str, str] | tuple[str, str, str, str]]] = [ ("body", "black", "light gray"), ("focus", "light gray", "dark blue", "standout"), ("head", "yellow", "black", "standout"), ("foot", "light gray", "black"), ("key", "light cyan", "black", "underline"), ("title", "white", "black", "bold"), ("flag", "dark gray", "light gray"), ("error", "dark red", "light gray"), ] footer_text: typing.ClassVar[list[tuple[str, str] | str]] = [ ("title", "Example Data Browser"), " ", ("key", "UP"), ",", ("key", "DOWN"), ",", ("key", "PAGE UP"), ",", ("key", "PAGE DOWN"), " ", ("key", "+"), ",", ("key", "-"), " ", ("key", "LEFT"), " ", ("key", "HOME"), " ", ("key", "END"), " ", ("key", "Q"), ] def __init__(self, data: SampleTree) -> None: """Initialize the tree view rooted at `data`.""" self.topnode = ExampleParentNode(data) self.listbox = urwid.TreeListBox(urwid.TreeWalker(self.topnode)) self.listbox.offset_rows = 1 self.header = urwid.Text("") self.footer = urwid.AttrMap(urwid.Text(self.footer_text), "foot") self.view = urwid.Frame( urwid.AttrMap(self.listbox, "body"), header=urwid.AttrMap(self.header, "head"), footer=self.footer, ) def main(self) -> None: """Run the program.""" self.loop = urwid.MainLoop(self.view, self.palette, unhandled_input=self.unhandled_input) self.loop.run() def unhandled_input(self, k: str | tuple[str, int, int, int]) -> None: """Handle keys not consumed by the widgets: :kbd:`q` to exit. :param k: unhandled key or mouse event :raises urwid.ExitMainLoop: when the user presses :kbd:`q` """ if k in {"q", "Q"}: raise urwid.ExitMainLoop() def get_example_tree() -> SampleTree: """Generate a quick 100 leaf tree for demo purposes. :returns: a parent node with 10 children, each having 10 children of their own """ retval: SampleTree = {"name": "parent", "children": []} for i in range(10): retval["children"].append({"name": f"child {i!s}"}) retval["children"][i]["children"] = [] for j in range(10): retval["children"][i]["children"].append({"name": "grandchild " + str(i) + "." + str(j)}) return retval def main() -> None: """Run the tree sample example program.""" sample = get_example_tree() ExampleTreeBrowser(sample).main() if __name__ == "__main__": main() urwid-urwid-69907e4/examples/twisted_serve_ssh.py000066400000000000000000000501111526073734300222310ustar00rootroot00000000000000""" Twisted integration for Urwid. This module allows you to serve Urwid applications remotely over ssh. The idea is that the server listens as an SSH server, and each connection is routed by Twisted to urwid, and the urwid UI is routed back to the console. The concept was a bit of a head-bender for me, but really we are just sending escape codes and the what-not back to the console over the shell that ssh has created. This is the same service as provided by the UI components in twisted.conch.insults.window, except urwid has more features, and seems more mature. This module is not highly configurable, and the API is not great, so don't worry about just using it as an example and copy-pasting. Process ------- TODO: - better gpm tracking: there is no place for os.Popen in a Twisted app I think. Copyright: 2010, Ali Afshar License: MIT Portions Copyright: 2010, Ian Ward Licence: LGPL """ from __future__ import annotations import typing from twisted.application.internet import TCPServer from twisted.application.service import Application from twisted.conch.insults.insults import ServerProtocol, TerminalProtocol from twisted.conch.interfaces import IConchUser, ISession from twisted.conch.manhole_ssh import ( ConchFactory, TerminalRealm, TerminalSession, TerminalSessionTransport, TerminalUser, ) from twisted.cred.portal import Portal from twisted.python.components import Adapter, Componentized from zope.interface import Attribute, Interface, implementer # type: ignore[import-untyped] import urwid from urwid.display.raw import Screen if typing.TYPE_CHECKING: from collections.abc import Callable, Hashable from twisted.cred.checkers import ICredentialsChecker from twisted.internet.protocol import ProcessProtocol # Mirrors the private urwid.display._raw_display_base.Screen._DecodedInput alias, which isn't exported. _DecodedInput = list[str | tuple[str, int, int, int] | tuple[typing.Literal["cursor position"], int, int]] # Matches the (unexported) palette parameter type of urwid.MainLoop.__init__. _Palette = typing.Iterable[ tuple[str, str] | tuple[str, str, str] | tuple[str, str, str, str] | tuple[str, str, str, str, str, str] ] class IUrwidUi(Interface): """Toplevel urwid widget.""" toplevel = Attribute("Urwid Toplevel Widget") palette = Attribute("Urwid Palette") screen = Attribute("Urwid Screen") loop = Attribute("Urwid Main Loop") # zope.interface methods declare no `self`; mypy doesn't understand the Interface metaclass and would otherwise # demand an annotated `self`, which would misrepresent this as a real, callable method. def create_urwid_toplevel(): # type: ignore[no-untyped-def] """Create a toplevel widget.""" def create_urwid_mainloop(): # type: ignore[no-untyped-def] """Create the urwid main loop.""" class IUrwidMind(Interface): """Interface for the adapter that drives an urwid UI over a Twisted terminal protocol.""" ui = Attribute("") terminalProtocol = Attribute("") terminal = Attribute("") checkers = Attribute("") avatar = Attribute("The avatar") def push(data): # type: ignore[no-untyped-def] """Push data.""" def draw(): # type: ignore[no-untyped-def] """Refresh the UI.""" class UrwidUi: """Base class building the toplevel widget, palette, screen and main loop for a :class:`UrwidMind`.""" def __init__(self, urwid_mind: UrwidMind) -> None: """Build and run the toplevel widget, palette, screen and main loop for `urwid_mind`.""" self.mind = urwid_mind self.toplevel: urwid.Widget = self.create_urwid_toplevel() self.palette: _Palette = self.create_urwid_palette() self.screen: TwistedScreen = TwistedScreen(self.mind.terminalProtocol) self.loop: urwid.MainLoop = self.create_urwid_mainloop() def create_urwid_toplevel(self) -> typing.NoReturn: """Build the toplevel widget for the UI. :raises NotImplementedError: always; subclasses must override this. """ raise NotImplementedError def create_urwid_palette(self) -> _Palette: """Return the palette for the UI; subclasses may override this.""" return () def create_urwid_mainloop(self) -> urwid.MainLoop: """Create, run and return the urwid main loop driving the toplevel widget.""" evl = urwid.TwistedEventLoop(manage_reactor=False) loop = urwid.MainLoop( self.toplevel, screen=self.screen, event_loop=evl, unhandled_input=self.mind.unhandled_key, palette=self.palette, ) self.screen.loop = loop loop.run() return loop class UnhandledKeyHandler: """Dispatch an unhandled urwid key to a ``key_`` method, if one exists.""" def __init__(self, mind: UrwidMind) -> None: """Initialize dispatching unhandled keys to `mind`.""" self.mind = mind def push(self, key: str | tuple[str, int, int, int]) -> None: """Dispatch ``key`` to its ``key_`` handler method, if any.""" if isinstance(key, tuple): return None f = getattr(self, f"key_{key.replace(' ', '_')}", None) if f is None: return None return f(key) # type: ignore[no-any-return] def key_ctrl_c(self, key: str) -> None: """Close the connection when the operator presses Ctrl-C.""" self.mind.terminal.loseConnection() @implementer(IUrwidMind) class UrwidMind(Adapter): """Adapt a Twisted conch avatar to drive an urwid UI over its terminal protocol.""" cred_checkers: typing.ClassVar[list[ICredentialsChecker]] = [] ui: UrwidUi | None = None ui_factory: type[UrwidUi] | None = None unhandled_key_factory = UnhandledKeyHandler @property def avatar(self) -> IConchUser: """Return the adapted object as an :class:`IConchUser`.""" return IConchUser(self.original) def set_terminalProtocol(self, terminalProtocol: UrwidTerminalProtocol) -> None: """Attach the terminal protocol driving this mind, and build its UI.""" self.terminalProtocol = terminalProtocol self.terminal = terminalProtocol.terminal self.unhandled_key_handler = self.unhandled_key_factory(self) self.unhandled_key = self.unhandled_key_handler.push if self.ui_factory is None: msg = "ui_factory must be set by a subclass" raise RuntimeError(msg) self.ui = self.ui_factory(self) def push(self, data: bytes) -> None: """Push data received from the terminal protocol into the UI screen.""" if self.ui is None: msg = "the UI is only available after set_terminalProtocol() has run" raise RuntimeError(msg) self.ui.screen.push(data) def draw(self) -> None: """Refresh the UI.""" if self.ui is None: msg = "the UI is only available after set_terminalProtocol() has run" raise RuntimeError(msg) self.ui.loop.draw_screen() class TwistedScreen(Screen): """A Urwid screen which knows about the Twisted terminal protocol that is driving it. A Urwid screen is responsible for: 1. Input 2. Output Input is achieved in normal urwid by passing a list of available readable file descriptors to the event loop for polling/selecting etc. In the Twisted situation, this is not necessary because Twisted polls the input descriptors itself. Urwid allows this by being driven using the main loop instance's `process_input` method which is triggered on Twisted protocol's standard `dataReceived` method. """ loop: urwid.MainLoop def __init__(self, terminalProtocol: UrwidTerminalProtocol) -> None: """Initialize driven by `terminalProtocol`, already started since Twisted supplies its own input.""" # We will need these later self.terminalProtocol = terminalProtocol self.terminal = terminalProtocol.terminal self._data: list[int] = [] super().__init__() self.colors = 16 self._pal_escape = {} self.bright_is_bold = True self.register_palette_entry(None, "black", "white") urwid.signals.connect_signal(self, urwid.UPDATE_PALETTE_ENTRY, self._on_update_palette_entry) # Don't need to wait for anything to start self._started = True # Urwid Screen API def get_cols_rows(self) -> tuple[int, int]: """Get the size of the terminal as (cols, rows).""" return self.terminalProtocol.width, self.terminalProtocol.height def draw_screen(self, size: tuple[int, int], canvas: urwid.Canvas) -> None: """Render a canvas to the terminal. The canvas contains all the information required to render the Urwid UI. The content method returns a list of rows as (attr, cs, text) tuples. This very simple implementation iterates each row and simply writes it out. """ (_maxcol, _maxrow) = size # self.terminal.eraseDisplay() lasta = None for i, row in enumerate(canvas.content()): self.terminal.cursorPosition(0, i) for attr, _cs, raw_text in row: text = raw_text.decode(urwid.util.get_encoding()) if attr != lasta: text = f"{self._attr_to_escape(attr)}{text}" lasta = attr # if cs or attr: # print(cs, attr) self.write(text) cursor = canvas.get_cursor() if cursor is not None: self.terminal.cursorPosition(*cursor) # XXX from base screen def set_mouse_tracking(self, enable: bool = True) -> None: """Enable (or disable) mouse tracking. After calling this function get_input will include mouse click events along with keystrokes. :param enable: whether to enable or disable mouse tracking. """ if enable: self.write(urwid.escape.MOUSE_TRACKING_ON) else: self.write(urwid.escape.MOUSE_TRACKING_OFF) # twisted handles polling, so we don't need the loop to do it, we just # push what we get to the loop from dataReceived. def hook_event_loop( self, event_loop: urwid.EventLoop, callback: Callable[[_DecodedInput, list[int]], typing.Any], ) -> None: """Record the event loop and callback; Twisted delivers input through :meth:`push` instead of polling.""" self._urwid_callback = callback self._evl = event_loop def unhook_event_loop(self, event_loop: urwid.EventLoop) -> None: """Do nothing; Twisted's own polling means there are no hooks to remove.""" def get_input(self, raw_keys: bool = False) -> _DecodedInput: # type: ignore[override] """Return no input; it isn't clear whether or when the base class actually calls this method. The base class overloads this method to return a ``(keys, raw)`` tuple when ``raw_keys`` is True; Twisted never uses that path here, so only the ``raw_keys=False`` shape is implemented. :param raw_keys: unused; see above. """ return [] def get_available_raw_input(self) -> list[int]: """Return and clear the raw input codes buffered by :meth:`push`.""" data = self._data self._data = [] return data # Twisted driven def push(self, data: bytes) -> None: """Receive data from Twisted and push it into the urwid main loop. We must here: 1. filter the input data against urwid's input filter. 2. Calculate escapes and other clever things using urwid's `escape.process_keyqueue`. 3. Pass the calculated keys as a list to the Urwid main loop. 4. Redraw the screen """ self._data = list(data) self.parse_input(self._evl, self._urwid_callback, self.get_available_raw_input()) self.loop.draw_screen() # Convenience def write(self, data: str) -> None: """Write *data* to the terminal.""" self.terminal.write(data) # Private def _on_update_palette_entry(self, name: Hashable, *attrspecs: urwid.AttrSpec) -> None: # copy the attribute to a dictionary containing the escape sequences self._pal_escape[name] = self._attrspec_to_escape(attrspecs[{16: 0, 1: 1, 88: 2, 256: 3}[self.colors]]) def _attrspec_to_escape(self, a: urwid.AttrSpec) -> str: r"""Convert AttrSpec instance a to an escape sequence for the terminal. >>> s = Screen() >>> s.set_terminal_properties(colors=256) >>> a2e = s._attrspec_to_escape >>> a2e(s.AttrSpec("brown", "dark green")) '\\x1b[0;33;42m' >>> a2e(s.AttrSpec("#fea,underline", "#d0d")) '\\x1b[0;38;5;229;4;48;5;164m' """ if a.foreground_high: fg = f"38;5;{a.foreground_number:d}" elif a.foreground_basic: if a.foreground_number > 7: if self.bright_is_bold: fg = f"1;{a.foreground_number - 8 + 30:d}" else: fg = f"{a.foreground_number - 8 + 90:d}" else: fg = f"{a.foreground_number + 30:d}" else: fg = "39" st = "1;" * a.bold + "4;" * a.underline + "7;" * a.standout if a.background_high: bg = f"48;5;{a.background_number:d}" elif a.background_basic: if a.background_number > 7: # this doesn't work on most terminals bg = f"{a.background_number - 8 + 100:d}" else: bg = f"{a.background_number + 40:d}" else: bg = "49" return f"{urwid.escape.ESC}[0;{fg};{st}{bg}m" class UrwidTerminalProtocol(TerminalProtocol): """A terminal protocol that knows to proxy input and receive output from Urwid. This integrates with the TwistedScreen in a 1:1. """ def __init__(self, urwid_mind: UrwidMind) -> None: """Initialize with `urwid_mind` and a default 80x24 terminal size.""" self.urwid_mind = urwid_mind self.width = 80 self.height = 24 def connectionMade(self) -> None: """Attach this protocol to its mind and size the terminal once the connection opens.""" self.urwid_mind.set_terminalProtocol(self) self.terminalSize(self.width, self.height) def terminalSize(self, width: int, height: int) -> None: """Resize the terminal.""" self.width = width self.height = height if self.urwid_mind.ui is None: msg = "the UI is only available after set_terminalProtocol() has run" raise RuntimeError(msg) self.urwid_mind.ui.loop.screen_size = None self.terminal.eraseDisplay() self.urwid_mind.draw() def dataReceived(self, data: bytes) -> None: """Received data from the connection. This overrides the default implementation which parses and passes to the keyReceived method. We don't do that here, and must not do that so that Urwid can get the right juice (which includes things like mouse tracking). Instead we just pass the data to the screen instance's dataReceived, which handles the proxying to Urwid. """ self.urwid_mind.push(data) def _unhandled_input(self, data: str) -> None: # evil proceed = True if hasattr(self.urwid_toplevel, "app"): # type: ignore[attr-defined] # dead code kept as-is; never called proceed = self.urwid_toplevel.app.unhandled_input(self, data) # type: ignore[attr-defined] if not proceed: return if data == "ctrl c": self.terminal.loseConnection() class UrwidServerProtocol(ServerProtocol): """A conch server protocol that proxies input straight to the terminal protocol.""" def dataReceived(self, data: bytes) -> None: """Forward received *data* straight to the connected terminal protocol.""" if self.terminalProtocol is None: msg = "dataReceived needs a connected terminal protocol" raise RuntimeError(msg) self.terminalProtocol.dataReceived(data) class UrwidUser(TerminalUser): """A terminal user that remembers its avatarId. The default implementation doesn't. """ def __init__(self, original: Componentized, avatarId: bytes) -> None: """Initialize remembering `avatarId`, unlike the base implementation.""" super().__init__(original, avatarId) self.avatarId = avatarId class UrwidTerminalSession(TerminalSession): """A terminal session that remembers the avatar and chained protocol for later use. Also implements a missing method for changed window size. Note: This implementation assumes that each SSH connection will only request a single shell, which is not an entirely safe assumption, but is by far the most common case. """ def openShell(self, proto: ProcessProtocol) -> None: """Open a shell.""" self.chained_protocol = UrwidServerProtocol(UrwidTerminalProtocol, IUrwidMind(self.original)) TerminalSessionTransport(proto, self.chained_protocol, IConchUser(self.original), self.height, self.width) def windowChanged(self, dimensions: tuple[int, int, int, int]) -> None: """Handle the window size changing.""" (h, w, _x, _y) = dimensions if self.chained_protocol.terminalProtocol is None: msg = "terminalSize needs a connected terminal protocol" raise RuntimeError(msg) self.chained_protocol.terminalProtocol.terminalSize(w, h) class UrwidRealm(TerminalRealm): """Custom terminal realm class-configured to use our custom Terminal User Terminal Session.""" def __init__(self, mind_factory: type[UrwidMind]) -> None: """Initialize with `mind_factory` used to build the `UrwidMind` for each avatar.""" super().__init__() self.mind_factory = mind_factory def _getAvatar(self, avatarId: bytes) -> UrwidUser: comp = Componentized() user = UrwidUser(comp, avatarId) comp.setComponent(IConchUser, user) sess = UrwidTerminalSession(comp) comp.setComponent(ISession, sess) mind = self.mind_factory(comp) comp.setComponent(IUrwidMind, mind) return user def requestAvatar( self, avatarId: bytes, mind: object, *interfaces: type ) -> tuple[type, UrwidUser, Callable[[], None]]: """Build the avatar for ``avatarId``, implementing ``portal.IRealm``. :param interfaces: the interfaces the avatar is requested to support; only :class:`IConchUser` is offered. :raises NotImplementedError: if :class:`IConchUser` isn't among ``interfaces``. """ for i in interfaces: if i is IConchUser: return (IConchUser, self._getAvatar(avatarId), lambda: None) raise NotImplementedError() def create_server_factory(urwid_mind_factory: type[UrwidMind]) -> ConchFactory: """Create a server factory with a portal that uses a realm. The realm serves a given urwid widget against the checkers provided. """ rlm = UrwidRealm(urwid_mind_factory) # zope.interface lacks type stubs, so @implementer-declared interfaces (here, portal.IRealm on TerminalRealm) # aren't visible to mypy; UrwidRealm genuinely implements IRealm at runtime. ptl = Portal(rlm, urwid_mind_factory.cred_checkers) # type: ignore[arg-type] return ConchFactory(ptl) def create_service(urwid_mind_factory: type[UrwidMind], port: int, *args: typing.Any, **kw: typing.Any) -> TCPServer: """Create a service for use in tac-ish situations.""" f = create_server_factory(urwid_mind_factory) return TCPServer(port, f, *args, **kw) def create_application( application_name: str, urwid_mind_factory: type[UrwidMind], port: int, *args: typing.Any, **kw: typing.Any ) -> Componentized: """Create an application suitable for tac file.""" application: Componentized = Application(application_name) svc = create_service(urwid_mind_factory, 6022) svc.setServiceParent(application) return application urwid-urwid-69907e4/examples/twisted_serve_ssh.tac000066400000000000000000000016621526073734300223570ustar00rootroot00000000000000# encoding: utf-8 """ Example application for integrating serving a Urwid application remotely. Run this application with:: twistd -ny twisted_serve_ssh.tac Then in another terminal run:: ssh -p 6022 user@localhost (The password is 'pw' without the quotes.) Note: To use this in real life, you must use some real checker. """ from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse import urwid from twisted_serve_ssh import UrwidMind, UrwidUi, create_application class HelloUi(UrwidUi): def create_urwid_toplevel(self): txt = urwid.Edit('Hello World?\n ') txt2 = urwid.Edit('Hello World?\n ') fill = urwid.Filler(urwid.Pile([txt, txt2]), 'top') return fill class HelloMind(UrwidMind): ui_factory = HelloUi cred_checkers = [InMemoryUsernamePasswordDatabaseDontUse(user='pw')] application = create_application('TXUrwid Demo', HelloMind, 6022) # vim: ft=python urwid-urwid-69907e4/mypy-requirements.txt000066400000000000000000000000141526073734300205520ustar00rootroot00000000000000mypy==2.4.0 urwid-urwid-69907e4/pylint-requirements.txt000066400000000000000000000000161526073734300210750ustar00rootroot00000000000000pylint==4.1.1 urwid-urwid-69907e4/pyproject.toml000066400000000000000000000262261526073734300172230ustar00rootroot00000000000000[build-system] build-backend = "setuptools.build_meta" # Minimum requirements for the build system to execute. # PEP 508 specifications for PEP 518. requires = [ "setuptools>=80.8.0", "setuptools-scm[toml]>=8.1.0", ] [project] name = "urwid" description = "A full-featured console (xterm et al.) user interface library" readme = { file = "README.rst", content-type = "text/x-rst" } keywords = [ "console", "curses", "listbox", "ncurses", "scroll", "text layout", "ui", "user interface", "widget" ] license = "LGPL-2.1-only" # Use SPDX classifier license-files = [ "COPYING" ] requires-python = ">=3.9.0" dynamic = [ "classifiers", "dependencies", "version" ] [[project.authors]] name = "Ian Ward" email = "ian@excess.org" [project.optional-dependencies] curses = [ "windows-curses; sys_platform=='win32'" ] glib = [ "pygobject" ] # Floors below are the oldest releases without known CVEs (only the IOLoop/reactor APIs are used by urwid, # but the extras pull the whole distribution into the user environment). tornado = [ "tornado>=6.5.8" ] trio = [ "exceptiongroup; python_version<'3.11'", "trio>=0.31.0" ] # 26.4.0 requires CPython 3.9.12+, so the unconditional floor stays at 24.7.0. twisted = [ "twisted>=24.7.0", "twisted>=26.4.0; python_full_version>='3.9.12'" ] # `zmq` on PyPI is an abandoned 2015 placeholder ("You are probably looking for pyzmq") with no dependencies: # it does not provide the `zmq` module that urwid.event_loop.zmq_loop imports. zmq = [ "pyzmq>=26.0.0" ] # for lcd_display serial = [ "pyserial" ] lcd = [ "pyserial" ] [project.urls] Homepage = "https://urwid.org/" Documentation = "https://urwid.org/manual/index.html" Repository = "https://github.com/urwid/urwid" "Bug Tracker" = "https://github.com/urwid/urwid/issues" [dependency-groups] dev = [ { include-group = "build" }, { include-group = "documentation" }, { include-group = "lint" }, { include-group = "mypy" }, { include-group = "pre-commit" }, { include-group = "pylint" }, { include-group = "pytest" } ] build = [ "build", "twine", ] documentation = [ "sphinx", ] lint = [ "pyproject-fmt", "yamllint", { include-group = "ruff" }, ] mypy = [ "mypy[faster-cache,reports]", ] pre-commit = [ "prek", ] pylint = [ "pylint", ] pytest = [ "pytest", "pytest-asyncio", "pytest-cov", ] ruff = [ "ruff", ] [tool.setuptools] platforms = [ "unix-like" ] zip-safe = false [tool.setuptools.dynamic] classifiers = { file = [ "classifiers.txt" ] } dependencies = { file = [ "requirements.txt" ] } [tool.setuptools.package-data] urwid = [ "py.typed" ] [tool.setuptools.packages.find] include = [ "urwid", "urwid.*", ] namespaces = false [tool.distutils.bdist_wheel] universal = 0 [tool.setuptools_scm] write_to = "urwid/version.py" [tool.cibuildwheel] # Disable building PyPy wheels on all platforms # Disable musllinux as not popular platform skip = [ "pp*", "*-musllinux_*" ] [tool.ruff] target-version = "py39" line-length = 120 extend-exclude = [ "tests" ] output-format = "full" [tool.ruff.format] docstring-code-format = true [tool.ruff.lint] extend-select = [ "E", "W", # also pycodestyle warnings "F", # pyflakes "PYI", # flake8-pyi "ASYNC", # flake8-async "FA", # from __future__ import annotations "SLOT", # flake8-slots "TC", # flake8-type-checking "S", # flake8-bandit "A", # flake8-builtins "B", "T10", "EXE", # flake8-bugbear, flake8-debugger, flake8-executable "ISC", # flake8-implicit-str-concat "RET", "SIM", "C4", # flake8-return, flake8-simplify, flake8-comprehensions "ICN", "PGH", # flake8-import-conventions, pygrep-hooks "Q", # quotes "FLY", # Flynt "FURB", # Refurb "PIE", # flake8-pie, "TRY", "UP", "I", "PL", "PERF", "RUF", # tryceratops, pyupgrade, isort, pylint + perflint, Ruff-specific "D", # pydocstyle ] extend-ignore = [ "E221", "E222", # handled by formatter "PLR6301", "E203", # refactor rules (too many statements/arguments/branches) "PLR0904", "PLR0911", "PLR0912", "PLR0913", "PLR0914", "PLR0915", "PLR0917", "PLR2004", "PLC0415", # We have a lot of imports outside top-level "RET504", # Unnecessary variable assignment before a return statement "SIM108", # Use ternary operator, "TRY003", # long messages prepare outside, ... "PYI063", # MonitoredList is a list subclass, this expected to change "ISC001", # conflicts with the formatter, which can produce single-line implicit concatenation # suspicious-xml-*-usage: recommend defusedxml, which is not a dependency "S313", "S314", "S315", "S316", "S317", "S318", "S319", ] [tool.ruff.lint.isort] known-third-party = [] [tool.ruff.lint.per-file-ignores] "__init__.py" = [ "F401", "SIM105" ] "urwid/display/curses.py" = [ "A005" ] # Shown verbatim in the docs as numbered listings, wcur2.py from a fixed line (`:lines: 7-`). "docs/tutorial/*.py" = [ "D100", "D101", "D102", "D103", "D107" ] "docs/manual/*.py" = [ "D100", "D101", "D102", "D103", "D107" ] [tool.ruff.lint.pydocstyle] convention = "pep257" [tool.ruff.lint.pylint] allow-dunder-method-names = [ "__rich_repr__", # class generation "_contents__getitem__", "_contents__setitem__", "_contents__delitem__", ] [tool.pylint] disable = [ "arguments-differ", "broad-exception-caught", "broad-exception-raised", "consider-using-assignment-expr", "consider-using-enumerate", # iterable modification during iteration "cyclic-import", "file-ignored", "fixme", "import-outside-toplevel", "invalid-name", "invalid-overridden-method", # Magic part "locally-disabled", "logging-format-interpolation", "logging-fstring-interpolation", "similarities", "superfluous-parens", "suppressed-message", "too-few-public-methods", "too-many-ancestors", "too-many-arguments", "too-many-branches", "too-many-instance-attributes", "too-many-lines", "too-many-locals", "too-many-nested-blocks", "too-many-positional-arguments", "too-many-public-methods", "too-many-return-statements", "too-many-statements", "unused-argument", "unused-private-member", "use-implicit-booleaness-not-comparison-to-string", # too complex union "use-implicit-booleaness-not-comparison-to-zero", ] enable = "all" ignore = [ "CVS", "version.py" ] # gi (PyGObject) is only installed with the "glib" extra; static checks also run without it. ignored-modules = [ "gi" ] jobs = 0 load-plugins = [ "pylint.extensions.check_elif", "pylint.extensions.code_style", "pylint.extensions.dict_init_mutate", "pylint.extensions.dunder", "pylint.extensions.for_any_all", "pylint.extensions.overlapping_exceptions", "pylint.extensions.typing", ] max-line-length = 120 py-version = "3.9" [tool.pylint.basic] good-names = [ "i", "j", "k", "ex", "Run", "_", "bar", # we draw bars ] [tool.pylint.classes] exclude-protected = [ "_asdict", "_fields", "_replace", "_source", "_make", "os._exit", # API "_original_widget", "_invalidate", "_repr_attrs", "_repr_words", "_w", ] [tool.pylint.dunder] good-dunder-names = [ "__rich_repr__", # class generation "_contents__getitem__", "_contents__setitem__", "_contents__delitem__", ] [tool.pylint.format] ignore-long-lines = "^(\\s*(# )??)|(.+# type: ignore\\[[\\w\\-\\,]+\\])$" [tool.pyproject-fmt] # After how many columns split arrays/dicts into multiple lines and wrap long strings; # use a trailing comma in arrays to force multiline format instead of lowering this value column_width = 120 # Keep full version numbers (e.g., 1.0.0 instead of 1.0) in dependency specifiers keep_full_version = true generate_python_version_classifiers = false # Table format: "short" collapses sub-tables to dotted keys, "long" expands to [table.subtable] headers table_format = "long" [tool.mypy] strict = true show_error_context = true show_column_numbers = true pretty = true [[tool.mypy.overrides]] module = "urwid.display.curses" disable_error_code = [ "union-attr", # chacking for started state before each operation if too expensive ] [[tool.mypy.overrides]] module = "urwid.display._win32" disable_error_code = [ # `ctypes.windll` and friends exist only on Windows in typeshed, while this module is Windows-only by design: # checking it from a non-Windows host produces platform-specific noise instead of real issues. "attr-defined", ] [[tool.mypy.overrides]] module = "urwid.widget.constants" disable_error_code = [ # False positive: plain-string `Literal`s (e.g. "relative") are reported as overlapping # sibling `str`-based enums (`Align`/`VAlign`) in the size/alignment overloads, even though # the underlying values are disjoint. The public API must accept both string and enum forms. "overload-overlap", ] [[tool.mypy.overrides]] module = [ "gi.*", "serial.*", "trio.*" ] ignore_missing_imports = true [[tool.mypy.overrides]] module = "twisted_serve_ssh" disable_error_code = [ "misc", # twisted and zope.interface ship no type stubs, so every call into them is "untyped" to mypy, # and zope.interface.Interface's metaclass magic isn't something mypy's plugin-free checker understands. "no-untyped-call", ] [tool.pytest.ini_options] minversion = "6.0" addopts = "-vvvv --doctest-modules -s --cov --cov-report=term" doctest_optionflags = [ "DONT_ACCEPT_TRUE_FOR_1", "ELLIPSIS", "IGNORE_EXCEPTION_DETAIL" ] testpaths = [ "tests", "urwid" ] [tool.coverage.json] pretty_print = true [tool.coverage.report] exclude_lines = [ # Have to re-enable the standard pragma "pragma: no cover", # Don't complain about missing debug-only code: "def __repr__", "def __rich_repr__", # own repr construct magic "def _repr_words", "def _repr_attrs", # Don't complain if tests don't hit defensive assertion code: "raise NotImplementedError", "raise AssertionError", "assert False", "typing\\.assert_never", "assert_never\\(", # Exclude methods marked as abstract / overload stubs "@abstractmethod", "@abc.abstractmethod", "@typing\\.overload", "@overload", # Exclude import statements "^from\\b", "^import\\b", # Exclude variable declarations that are executed when file is loaded "^[a-zA-Z_]+\\b\\s=", # Code for static analysis / type narrowing is never covered at runtime: "if TYPE_CHECKING:", "if typing\\.TYPE_CHECKING:", "typing\\.cast\\(", "cast\\(", "^\\s*\\.\\.\\.\\s*$", # Protocols "@runtime_checkable", "@typing.runtime_checkable", "class \\w*\\(Protocol\\):", "class \\w*\\(typing.Protocol\\):", # Fallback code with no installed deps is almost impossible to cover properly "except ImportError:", # Don't complain if non-runnable code isn't run: "if __name__ == .__main__.:", # OS Specific "if platform.system()", "if IS_WINDOWS", "if os.name", "if sys.platform", # Manual test code "class \\w*test:", "def \\wtest():" ] [tool.coverage.run] source_pkgs = [ "urwid" ] omit = [ "tests/*", ".tox/*", "setup.py", "urwid/version.py" ] branch = true [tool.refurb] python_version = "3.9" ignore = [ "FURB104", "FURB144", "FURB146" ] [[tool.refurb.amend]] path = "urwid/__init__.py" ignore = [ "FURB107" ] [[tool.refurb.amend]] path = "urwid/display/__init__.py" ignore = [ "FURB107" ] [[tool.refurb.amend]] path = "urwid/event_loop/__init__.py" ignore = [ "FURB107" ] urwid-urwid-69907e4/pytest_requirements.txt000066400000000000000000000003001526073734300211640ustar00rootroot00000000000000pytest==8.4.2; python_version < '3.10' pytest==9.1.1; python_version >= '3.10' pytest-asyncio==1.2.0; python_version < '3.10' pytest-asyncio==1.4.0; python_version >= '3.10' pytest-cov==7.1.0 urwid-urwid-69907e4/requirements.txt000066400000000000000000000000571526073734300175650ustar00rootroot00000000000000wcwidth>=0.4,!=0.9.0,!=0.9.1 typing-extensions urwid-urwid-69907e4/tests/000077500000000000000000000000001526073734300154415ustar00rootroot00000000000000urwid-urwid-69907e4/tests/__init__.py000066400000000000000000000000001526073734300175400ustar00rootroot00000000000000urwid-urwid-69907e4/tests/display/000077500000000000000000000000001526073734300171065ustar00rootroot00000000000000urwid-urwid-69907e4/tests/display/__init__.py000066400000000000000000000000001526073734300212050ustar00rootroot00000000000000urwid-urwid-69907e4/tests/display/test_common.py000066400000000000000000000042701526073734300220120ustar00rootroot00000000000000from __future__ import annotations import unittest from urwid.display.common import AttrSpec, attr_spec_to_css class AttrSpecToCssTest(unittest.TestCase): """Tests for common.attr_spec_to_css, which translates an AttrSpec into the inline CSS shared by the web and html_fragment display modules -- their equivalent of raw_display's _attrspec_to_escape. """ def test_default_colours_fall_back_to_black_on_light_gray(self) -> None: fg, bg, extra = attr_spec_to_css(AttrSpec("default", "default")) self.assertEqual("#000000", fg) self.assertEqual("#e5e5e5", bg) self.assertEqual("", extra) def test_named_colours_render_as_hex(self) -> None: fg, bg, extra = attr_spec_to_css(AttrSpec("white", "black")) self.assertEqual("#ffffff", fg) self.assertEqual("#000000", bg) self.assertEqual("", extra) def test_high_colour_renders_exact_rgb(self) -> None: fg, bg, _extra = attr_spec_to_css(AttrSpec("#76b900", "#000000", colors=16777216)) self.assertEqual("#76b900", fg) self.assertEqual("#000000", bg) def test_standout_swaps_foreground_and_background(self) -> None: fg, bg, _extra = attr_spec_to_css(AttrSpec("white,standout", "black")) self.assertEqual("#000000", fg) self.assertEqual("#ffffff", bg) def test_all_attributes_combine_into_one_style(self) -> None: aspec = AttrSpec("white,bold,italics,underline,blink,strikethrough,faint", "black") _fg, _bg, extra = attr_spec_to_css(aspec) self.assertIn(";text-decoration:underline line-through", extra) self.assertIn(";font-weight:bold", extra) self.assertIn(";font-style:italic", extra) self.assertIn(";animation:urwid-blink 1s step-start infinite", extra) self.assertIn(";opacity:0.5", extra) def test_underline_alone_has_no_line_through(self) -> None: _fg, _bg, extra = attr_spec_to_css(AttrSpec("white,underline", "black")) self.assertEqual(";text-decoration:underline", extra) def test_no_attributes_gives_empty_extra(self) -> None: _fg, _bg, extra = attr_spec_to_css(AttrSpec("white", "black")) self.assertEqual("", extra) urwid-urwid-69907e4/tests/display/test_html_fragment.py000066400000000000000000000175251526073734300233600ustar00rootroot00000000000000from __future__ import annotations import unittest import urwid from urwid.canvas import TextCanvas from urwid.display import html_fragment, raw from urwid.display.common import AttrSpec from urwid.display.html_fragment import HtmlGenerator, HtmlGeneratorSimulationError, html_span from urwid.event_loop import ExitMainLoop try: from urwid.display import curses except ImportError: # no stdlib curses on this platform, e.g. Windows and GraalPy curses = None # type: ignore[assignment] class HtmlSpanTest(unittest.TestCase): """Tests for html_span, the html_fragment equivalent of web.code_span: both wrap common.attr_spec_to_css's inline CSS around the run of text, html_span into a ```` rather than the wire format web.code_span produces. The colour/attribute-to-CSS logic itself is covered by tests/display/test_common.py, since the two modules share it. """ def test_wraps_text_in_a_styled_span(self) -> None: self.assertEqual( 'hi', html_span("hi", AttrSpec("white", "black")), ) def test_escapes_html_special_characters(self) -> None: self.assertEqual( 'a <b> & c', html_span("a & c", AttrSpec("white", "black")), ) def test_empty_string_produces_no_span(self) -> None: self.assertEqual("", html_span("", AttrSpec("white", "black"))) def test_combined_attributes_render_in_one_style(self) -> None: aspec = AttrSpec("white,bold,italics,underline,blink,strikethrough,faint", "black") span = html_span("hi", aspec) self.assertEqual( 'hi', span, ) def test_cursor_splits_into_three_pieces_with_swapped_colours(self) -> None: span = html_span("abc", AttrSpec("white", "black"), cursor=1) self.assertEqual( 'a' 'b' 'c', span, ) def test_cursor_at_last_column_leaves_a_trailing_empty_span(self) -> None: span = html_span("ab", AttrSpec("white", "black"), cursor=1) self.assertEqual( 'a' 'b' '', span, ) class HtmlGeneratorTest(unittest.TestCase): def setUp(self) -> None: self.screen = HtmlGenerator() # HtmlGenerator keeps its lists as class variables, shared across instances; # reset them so one test cannot see fixtures left behind by another. HtmlGenerator.fragments = [] HtmlGenerator.sizes = [] HtmlGenerator.keys = [] def test_default_palette_entry_is_registered_on_construction(self) -> None: self.assertIn(None, self.screen._palette) def test_set_terminal_properties_keeps_unset_values(self) -> None: self.screen.set_terminal_properties(colors=256, has_underline=False) self.assertEqual(256, self.screen.colors) self.assertFalse(self.screen.bright_is_bold) self.assertFalse(self.screen.has_underline) def test_get_cols_rows_pops_the_next_size(self) -> None: HtmlGenerator.sizes = [(80, 25), (20, 10)] self.assertEqual((80, 25), self.screen.get_cols_rows()) self.assertEqual((20, 10), self.screen.get_cols_rows()) def test_get_cols_rows_raises_once_sizes_are_exhausted(self) -> None: HtmlGenerator.sizes = [] with self.assertRaises(HtmlGeneratorSimulationError): self.screen.get_cols_rows() def test_get_input_pops_the_next_keys(self) -> None: HtmlGenerator.keys = [["down"], ["a", "b"]] self.assertEqual(["down"], self.screen.get_input()) self.assertEqual(["a", "b"], self.screen.get_input()) def test_get_input_raises_exit_main_loop_once_keys_are_exhausted(self) -> None: HtmlGenerator.keys = [] with self.assertRaises(ExitMainLoop): self.screen.get_input() def test_get_input_raw_keys_returns_empty_raw_list(self) -> None: HtmlGenerator.keys = [["Q"]] self.assertEqual((["Q"], []), self.screen.get_input(raw_keys=True)) def test_draw_screen_appends_one_pre_fragment(self) -> None: self.screen.register_palette_entry("focus", "light red,bold,underline,standout", "dark blue") canvas = urwid.AttrMap(urwid.Text("hi"), "focus").render((5,)) self.screen.draw_screen((5, canvas.rows()), canvas) self.assertEqual( [ '
hi   \n
' ], HtmlGenerator.fragments, ) def test_draw_screen_rejects_a_canvas_with_a_different_row_count(self) -> None: canvas = urwid.Text("hi").render((5,)) with self.assertRaises(ValueError): self.screen.draw_screen((5, canvas.rows() + 1), canvas) def test_draw_screen_highlights_the_cursor_column(self) -> None: self.screen.register_palette_entry("focus", "white", "black") canvas = TextCanvas(text=[b"hi"], attr=[[("focus", 2)]], cs=[[(None, 2)]], cursor=(1, 0)) self.screen.draw_screen((2, canvas.rows()), canvas) self.assertEqual( [ "
"
                'h'
                'i'
                ''
                "\n
" ], HtmlGenerator.fragments, ) class ScreenshotInitTest(unittest.TestCase): def setUp(self) -> None: if curses is not None: self._orig_curses_screen = curses.Screen self._orig_raw_screen = raw.Screen def tearDown(self) -> None: HtmlGenerator.sizes = [] HtmlGenerator.keys = [] if curses is not None: curses.Screen = self._orig_curses_screen raw.Screen = self._orig_raw_screen def test_rejects_a_non_positive_size(self) -> None: with self.assertRaises(ValueError): html_fragment.screenshot_init([(80, 0)], []) def test_rejects_a_non_integer_size(self) -> None: with self.assertRaises(TypeError): html_fragment.screenshot_init([(80.0, 25)], []) def test_rejects_keys_that_are_not_a_list_of_lists(self) -> None: with self.assertRaises(TypeError): html_fragment.screenshot_init([(80, 25)], ["down"]) def test_rejects_a_non_string_key(self) -> None: with self.assertRaises(TypeError): html_fragment.screenshot_init([(80, 25)], [["down", 1]]) def test_replaces_curses_and_raw_screen_with_html_generator(self) -> None: html_fragment.screenshot_init([(80, 25)], [["Q"]]) if curses is not None: self.assertIs(curses.Screen, HtmlGenerator) self.assertIs(raw.Screen, HtmlGenerator) self.assertEqual([(80, 25)], HtmlGenerator.sizes) self.assertEqual([["Q"]], HtmlGenerator.keys) class ScreenshotCollectTest(unittest.TestCase): def test_drains_the_fragments_list(self) -> None: HtmlGenerator.fragments = ["
one
", "
two
"] collected = html_fragment.screenshot_collect() self.assertEqual(["
one
", "
two
"], collected) self.assertEqual([], HtmlGenerator.fragments) urwid-urwid-69907e4/tests/display/test_posix_raw_display.py000066400000000000000000000705361526073734300242720ustar00rootroot00000000000000from __future__ import annotations import os import signal import struct import subprocess import sys import unittest from unittest import mock from urwid import escape, signals from urwid.display.common import INPUT_DESCRIPTORS_CHANGED # `_posix_raw_display` (and `fcntl`/`termios`) is not importable on Windows at all, so importing # it unconditionally at module level would crash collection there under both pytest and plain # `python -m unittest` -- the latter has no `allow_module_level` skip mechanism and lets a # module-level `unittest.SkipTest` propagate as an import error instead of a skip. Every test # class below is decorated with `@unittest.skipIf(IS_WINDOWS, ...)`, which both runners honour # without needing the module to import successfully first. IS_WINDOWS = sys.platform == "win32" if not IS_WINDOWS: import termios from urwid.display._posix_raw_display import _GPM_B_LEFT, _GPM_MOD_SHIFT, Screen def _make_screen(input_fd: int | None = None) -> Screen: """Build a Screen over a pipe (or /dev/null) so tests never touch a real tty.""" if input_fd is None: input_fd, write_fd = os.pipe() os.close(write_fd) return Screen(input=os.fdopen(input_fd, "rb", buffering=0), output=open(os.devnull, "w")) @unittest.skipIf(IS_WINDOWS, "_posix_raw_display is not importable on Windows (no fcntl/termios/tty)") class TestStartStop(unittest.TestCase): def test_start_stop_not_a_tty(self): """termios is left untouched when the input fd is not a tty (a pipe here).""" read_fd, write_fd = os.pipe() self.addCleanup(os.close, write_fd) s = Screen( input=os.fdopen(read_fd, "rb", buffering=0), output=open(os.devnull, "w"), # noqa: SIM115 bracketed_paste_mode=True, focus_reporting=True, ) written: list[str] = [] s.write = written.append s.flush = lambda: None s.start() self.addCleanup(s.stop) output = "".join(written) self.assertIn(escape.PrivateMode.ALTERNATE_SCREEN_BUFFER.enable_seq, output) self.assertIn(escape.PrivateMode.BRACKETED_PASTE.enable_seq, output) self.assertIn(escape.PrivateMode.FOCUS_REPORTING.enable_seq, output) self.assertIsNone(s._old_termios_settings) written.clear() s.stop() output = "".join(written) self.assertIn(escape.PrivateMode.BRACKETED_PASTE.disable_seq, output) self.assertIn(escape.DISABLE_FOCUS_REPORTING, output) @mock.patch("termios.tcsetattr") @mock.patch("termios.tcgetattr") @mock.patch("tty.setcbreak") @mock.patch("os.isatty", return_value=True) def test_start_stop_saves_and_restores_termios_for_a_tty( self, mock_isatty, mock_setcbreak, mock_tcgetattr, mock_tcsetattr, ): # A minimally realistic termios attribute list: [iflag, oflag, cflag, lflag, ispeed, # ospeed, cc], with `cc` large enough to index by any of the VINTR/VQUIT/... constants # that tty_signal_keys() reads out of it. fake_attrs = [0, 0, 0, 0, 0, 0, [0] * 32] mock_tcgetattr.return_value = fake_attrs s = _make_screen() written: list[str] = [] s.write = written.append s.flush = lambda: None s.start() self.addCleanup(s.stop) fd = s._input_fileno() mock_tcgetattr.assert_any_call(fd) mock_setcbreak.assert_called_once_with(fd) self.assertEqual(fake_attrs, s._old_termios_settings) s.stop() mock_tcsetattr.assert_any_call(fd, termios.TCSAFLUSH, fake_attrs) def test_alternate_buffer_flag_is_set_before_later_start_steps_can_fail(self): """`stop()` still calls `_stop()` after `_start()` raises (`_started` is set first), so `_alternate_buffer` has to already be correct by the time the write happens, not after later steps -- here `signal_init()` -- that could still fail.""" s = _make_screen() s.write = lambda *_a: None s.flush = lambda: None s.signal_init = mock.Mock(side_effect=RuntimeError("boom")) with self.assertRaises(RuntimeError): s.start() self.assertTrue(s._alternate_buffer) s.stop() # must not raise: signal_restore() only reads what __init__ already set def test_start_installs_signal_handlers_and_stop_restores_them(self): s = _make_screen() s.write = lambda *_a: None s.flush = lambda: None previous_winch = signal.getsignal(signal.SIGWINCH) previous_tstp = signal.getsignal(signal.SIGTSTP) previous_cont = signal.signal(signal.SIGCONT, signal.SIG_IGN) self.addCleanup(signal.signal, signal.SIGCONT, previous_cont) s.start() self.assertEqual(s._sigwinch_handler, signal.getsignal(signal.SIGWINCH)) self.assertEqual(s._sigtstp_handler, signal.getsignal(signal.SIGTSTP)) s.stop() self.assertEqual(previous_winch, signal.getsignal(signal.SIGWINCH)) self.assertEqual(previous_tstp, signal.getsignal(signal.SIGTSTP)) # SIGCONT belongs to the application: neither start() nor stop() replaces it. self.assertEqual(signal.SIG_IGN, signal.getsignal(signal.SIGCONT)) @unittest.skipIf(IS_WINDOWS, "_posix_raw_display is not importable on Windows (no fcntl/termios/tty)") class TestSignalHandlers(unittest.TestCase): def test_sigwinch_handler_chains_to_previous(self): s = _make_screen() calls = [] s._prev_sigwinch_handler = lambda signum, frame: calls.append((signum, frame)) s._resized = False s._sigwinch_handler(signal.SIGWINCH, None) self.assertTrue(s._resized) self.assertEqual([(signal.SIGWINCH, None)], calls) def _started_screen(self) -> Screen: """Start a screen over a pipe whose write end stays open, so reading input finds nothing rather than EOF.""" read_fd, write_fd = os.pipe() self.addCleanup(os.close, write_fd) s = _make_screen(read_fd) s.write = lambda *_a: None s.flush = lambda: None s.start() self.addCleanup(s.stop) return s @mock.patch("os.kill") def test_sigtstp_handler_only_requests_a_suspend(self, mock_kill): """The handler leaves the terminal alone and wakes the input loop instead.""" s = self._started_screen() signal.getsignal(signal.SIGTSTP)(signal.SIGTSTP, None) self.assertTrue(s.started) mock_kill.assert_not_called() self.assertEqual(b"R", s.get_input_descriptors()[0].recv(16)) def test_requested_suspend_runs_in_the_input_path(self): """The screen is stopped, with the previous SIGTSTP disposition back, before the process suspends.""" previous_tstp = signal.getsignal(signal.SIGTSTP) s = self._started_screen() handler = signal.getsignal(signal.SIGTSTP) state_at_kill = [] def fake_kill(pid: int, signum: int) -> None: state_at_kill.append((pid, signum, s.started, signal.getsignal(signal.SIGTSTP))) handler(signal.SIGTSTP, None) with mock.patch("os.kill", side_effect=fake_kill): keys, _raw = s.parse_input(None, None, s.get_available_raw_input()) self.assertEqual([(os.getpid(), signal.SIGTSTP, False, previous_tstp)], state_at_kill) self.assertTrue(s.started) self.assertEqual(handler, signal.getsignal(signal.SIGTSTP)) self.assertEqual(["window resize"], keys) @mock.patch("os.kill") def test_suspend_requested_while_reading_input_is_not_lost(self, mock_kill): """A request whose wake-up byte is drained by the same pass is still carried out in that pass.""" s = self._started_screen() handler = signal.getsignal(signal.SIGTSTP) def read_while_signalled() -> list[int]: handler(signal.SIGTSTP, None) return [] with mock.patch.object(s, "_get_input_codes", side_effect=read_while_signalled): s.get_available_raw_input() mock_kill.assert_called_once_with(os.getpid(), signal.SIGTSTP) @mock.patch("os.kill") def test_stop_discards_a_pending_suspend(self, mock_kill): """A suspend requested before stop() does not stop the process after the next start().""" s = self._started_screen() signal.getsignal(signal.SIGTSTP)(signal.SIGTSTP, None) s.stop() s.start() s.get_available_raw_input() mock_kill.assert_not_called() @unittest.skipIf(IS_WINDOWS, "_posix_raw_display is not importable on Windows (no fcntl/termios/tty)") class TestGpmTracking(unittest.TestCase): @mock.patch("os.path.isfile", return_value=False) def test_start_gpm_tracking_skips_when_mev_missing(self, mock_isfile): s = _make_screen() s._start_gpm_tracking() self.assertIsNone(s.gpm_mev) mock_isfile.assert_called_once_with("/usr/bin/mev") @mock.patch.dict(os.environ, {"TERM": "xterm"}) @mock.patch("os.path.isfile", return_value=True) def test_start_gpm_tracking_skips_when_term_not_linux(self, mock_isfile): s = _make_screen() s._start_gpm_tracking() self.assertIsNone(s.gpm_mev) @mock.patch.dict(os.environ, {"TERM": "linux"}) @mock.patch("os.set_blocking") @mock.patch("urwid.display._posix_raw_display.Popen") @mock.patch("os.path.isfile", return_value=True) def test_start_gpm_tracking_success_sets_nonblocking(self, mock_isfile, mock_popen_cls, mock_set_blocking): mock_proc = mock.MagicMock() mock_proc.stdout.fileno.return_value = 99 mock_popen_cls.return_value = mock_proc s = _make_screen() s._start_gpm_tracking() mock_popen_cls.assert_called_once_with( ["/usr/bin/mev", "-e", "158"], stdin=mock.ANY, stdout=mock.ANY, close_fds=True, encoding="ascii", ) mock_set_blocking.assert_called_once_with(99, False) self.assertIs(mock_proc, s.gpm_mev) @mock.patch.dict(os.environ, {"TERM": "linux"}) @mock.patch("urwid.display._posix_raw_display.Popen") @mock.patch("os.path.isfile", return_value=True) def test_start_gpm_tracking_raises_when_stdout_missing(self, mock_isfile, mock_popen_cls): mock_proc = mock.MagicMock() mock_proc.stdout = None mock_popen_cls.return_value = mock_proc s = _make_screen() with self.assertRaises(RuntimeError): s._start_gpm_tracking() mock_proc.kill.assert_called_once() mock_proc.__exit__.assert_called_once() @mock.patch("os.kill") # guard: the helper must be stopped through its Popen object only def test_stop_gpm_tracking(self, mock_kill): """The helper is interrupted, its pipes closed and reaped, and the event loop told to stop watching it.""" s = _make_screen() fake_proc = mock.MagicMock() s.gpm_mev = fake_proc seen = [] signals.connect_signal(s, INPUT_DESCRIPTORS_CHANGED, lambda: seen.append(s.gpm_mev)) s._stop_gpm_tracking() fake_proc.send_signal.assert_called_once_with(signal.SIGINT) fake_proc.wait.assert_called_once_with(1.0) fake_proc.kill.assert_not_called() fake_proc.__exit__.assert_called_once() mock_kill.assert_not_called() self.assertIsNone(s.gpm_mev) self.assertEqual([None], seen) @mock.patch.dict(os.environ, {"TERM": "linux"}) @mock.patch("os.kill") # guard: the helper must be stopped through its Popen object only @mock.patch("os.set_blocking") @mock.patch("urwid.display._posix_raw_display.Popen") @mock.patch("os.path.isfile", return_value=True) def test_toggling_mouse_tracking_updates_watched_descriptors( self, mock_isfile, mock_popen_cls, mock_set_blocking, mock_kill ): """Turning mouse tracking on and off at run time adds and removes the helper's output from the watch.""" fake_proc = mock_popen_cls.return_value = mock.MagicMock() s = _make_screen() seen = [] signals.connect_signal(s, INPUT_DESCRIPTORS_CHANGED, lambda: seen.append(s.gpm_mev)) s.set_mouse_tracking(True) s.set_mouse_tracking(False) self.assertEqual([fake_proc, None], seen) fake_proc.__exit__.assert_called_once() mock_kill.assert_not_called() @mock.patch.dict(os.environ, {"TERM": "linux"}) @mock.patch("os.kill") # guard: the helper must be stopped through its Popen object only @mock.patch("os.set_blocking") @mock.patch("urwid.display._posix_raw_display.Popen") @mock.patch("os.path.isfile", return_value=True) def test_helper_that_does_not_exit_is_killed(self, mock_isfile, mock_popen_cls, mock_set_blocking, mock_kill): """A helper still running after the timeout is killed rather than waited for without limit.""" fake_proc = mock_popen_cls.return_value = mock.MagicMock() fake_proc.wait.side_effect = subprocess.TimeoutExpired("mev", 1.0) s = _make_screen() s.set_mouse_tracking(True) s.set_mouse_tracking(False) fake_proc.kill.assert_called_once_with() fake_proc.__exit__.assert_called_once() def test_stop_gpm_tracking_noop_when_not_tracking(self): s = _make_screen() s._stop_gpm_tracking() # must not raise self.assertIsNone(s.gpm_mev) @unittest.skipIf(IS_WINDOWS, "_posix_raw_display is not importable on Windows (no fcntl/termios/tty)") class TestEncodeGpmEvent(unittest.TestCase): @staticmethod def _gpm_line(ev_hex: str, x: int, y: int, buttons: int, modifiers_hex: str) -> str: # Field layout mirrors what _encode_gpm_event's parser expects: # - event type: hex digits after the last "x" # - x: last whitespace-separated token # - y: first whitespace-separated token (after stripping leading space) # - a 4th field that is ignored # - buttons: last whitespace-separated token, parsed as decimal # - modifiers: hex digits after the last "x" return f"Ax{ev_hex}, Ax {x}, {y} y, dx 0x0, buttons {buttons}, modifiers 0x{modifiers_hex}\n" def _screen_with_gpm_line(self, line: str) -> Screen: s = _make_screen() gpm_mev = mock.MagicMock() gpm_mev.stdout.readline.return_value = line s.gpm_mev = gpm_mev return s def test_press_left_button(self): s = self._screen_with_gpm_line(self._gpm_line("14", 7, 10, _GPM_B_LEFT, "0")) # ev=20 s.last_bstate = 0 result = s._encode_gpm_event() self.assertEqual([27, ord("["), ord("M"), 32, 7 + 32, 10 + 32], result) self.assertEqual(1, s.last_bstate) self.assertFalse(s.gpm_event_pending) def test_drag_middle_button(self): s = self._screen_with_gpm_line(self._gpm_line("92", 3, 4, 2, "0")) # ev=146, buttons=GPM_B_MIDDLE s.last_bstate = 0 result = s._encode_gpm_event() expected_button = 1 + escape.MOUSE_DRAG_FLAG + 32 self.assertEqual([27, ord("["), ord("M"), expected_button, 3 + 32, 4 + 32], result) # drag does not change the tracked button state self.assertEqual(0, s.last_bstate) def test_release_left_button(self): s = self._screen_with_gpm_line(self._gpm_line("8", 1, 2, _GPM_B_LEFT, "0")) # ev=8 (plain "up") s.last_bstate = 1 # left button was down result = s._encode_gpm_event() expected_button = escape.MOUSE_RELEASE_FLAG + 32 self.assertEqual([27, ord("["), ord("M"), expected_button, 1 + 32, 2 + 32], result) self.assertEqual(0, s.last_bstate) def test_double_click_release_emits_release_and_multi_click(self): s = self._screen_with_gpm_line(self._gpm_line("28", 5, 6, _GPM_B_LEFT, "0")) # ev=40 s.last_bstate = 1 # left button was down result = s._encode_gpm_event() release_button = escape.MOUSE_RELEASE_FLAG + 32 double_button = escape.MOUSE_MULTIPLE_CLICK_FLAG + 32 self.assertEqual( [ 27, ord("["), ord("M"), release_button, 5 + 32, 6 + 32, 27, ord("["), ord("M"), double_button, 5 + 32, 6 + 32, ], result, ) self.assertEqual(0, s.last_bstate) def test_triple_click_press(self): s = self._screen_with_gpm_line(self._gpm_line("34", 8, 9, _GPM_B_LEFT, "0")) # ev=52 s.last_bstate = 1 # left button already tracked as down from the preceding double click result = s._encode_gpm_event() triple_button = escape.MOUSE_MULTIPLE_CLICK_FLAG * 2 + 32 self.assertEqual([27, ord("["), ord("M"), triple_button, 8 + 32, 9 + 32], result) self.assertEqual(1, s.last_bstate) def test_shift_modifier_is_applied(self): s = self._screen_with_gpm_line(self._gpm_line("14", 0, 0, _GPM_B_LEFT, "1")) # ev=20, mod=GPM_MOD_SHIFT s.last_bstate = 0 result = s._encode_gpm_event() # shift maps to xterm's mod bit 4, ORed into the button byte self.assertEqual(4 + 32, result[3]) self.assertEqual(_GPM_MOD_SHIFT, 1) # sanity: the constant used to build the fixture @mock.patch("os.kill") # guard: the helper must be stopped through its Popen object only def test_malformed_line_stops_tracking(self, mock_kill): s = self._screen_with_gpm_line("not,enough,fields\n") gpm_mev = s.gpm_mev seen = [] signals.connect_signal(s, INPUT_DESCRIPTORS_CHANGED, lambda: seen.append(True)) result = s._encode_gpm_event() self.assertEqual([], result) self.assertIsNone(s.gpm_mev) self.assertEqual([True], seen) gpm_mev.send_signal.assert_called_once_with(signal.SIGINT) mock_kill.assert_not_called() @unittest.skipIf(IS_WINDOWS, "_posix_raw_display is not importable on Windows (no fcntl/termios/tty)") class TestReadRawInput(unittest.TestCase): def test_reads_bytes_of_several_writes_in_one_call(self): read_fd, write_fd = os.pipe() self.addCleanup(os.close, write_fd) s = Screen(input=os.fdopen(read_fd, "rb", buffering=0), output=open(os.devnull, "w")) # noqa: SIM115 s.write = lambda *_a: None s.flush = lambda: None s.start() self.addCleanup(s.stop) os.write(write_fd, b"abc") os.write(write_fd, b"def") result = s._read_raw_input(1) self.assertEqual(bytearray(b"abcdef"), result) @mock.patch("urwid.display._posix_raw_display._INPUT_READ_LIMIT", 4) def test_input_beyond_the_read_limit_is_left_for_the_next_pass(self): """One call reads at most the limit, so a producer that keeps writing cannot keep it reading.""" read_fd, write_fd = os.pipe() self.addCleanup(os.close, write_fd) s = _make_screen(read_fd) s.write = lambda *_a: None s.flush = lambda: None s.start() self.addCleanup(s.stop) os.write(write_fd, b"abcdef") self.assertEqual(list(b"abcd"), s.get_available_raw_input()) self.assertEqual(list(b"ef"), s.get_available_raw_input()) def test_returns_empty_on_timeout_with_nothing_available(self): read_fd, write_fd = os.pipe() self.addCleanup(os.close, write_fd) s = Screen(input=os.fdopen(read_fd, "rb", buffering=0), output=open(os.devnull, "w")) # noqa: SIM115 s.write = lambda *_a: None s.flush = lambda: None s.start() self.addCleanup(s.stop) result = s._read_raw_input(0) self.assertEqual(bytearray(), result) def test_raises_runtime_error_on_eof(self): read_fd, write_fd = os.pipe() s = Screen(input=os.fdopen(read_fd, "rb", buffering=0), output=open(os.devnull, "w")) # noqa: SIM115 s.write = lambda *_a: None s.flush = lambda: None s.start() self.addCleanup(s.stop) os.close(write_fd) # EOF with nothing ever written with self.assertRaises(RuntimeError): s._read_raw_input(1) @unittest.skipIf(IS_WINDOWS, "_posix_raw_display is not importable on Windows (no fcntl/termios/tty)") class TestDetectTerminalModes(unittest.TestCase): """_detect_terminal_modes() only writes the DECRQM queries; it does not wait for a reply.""" def _screen_over_pipe(self, *, bracketed_paste_mode=None, focus_reporting=None): read_fd, write_fd = os.pipe() self.addCleanup(os.close, write_fd) s = Screen( input=os.fdopen(read_fd, "rb", buffering=0), output=open(os.devnull, "w"), # noqa: SIM115 bracketed_paste_mode=bracketed_paste_mode, focus_reporting=focus_reporting, ) written: list[str] = [] s.write = written.append s.flush = lambda: None return s, written @mock.patch("os.isatty", return_value=True) def test_queries_every_mode_left_at_its_sentinel(self, mock_isatty): s, written = self._screen_over_pipe() s._detect_terminal_modes() output = "".join(written) self.assertIn(escape.PrivateMode.SYNCHRONIZED_OUTPUT.query, output) self.assertIn(escape.PrivateMode.GRAPHEME_CLUSTERING.query, output) self.assertIn(escape.PrivateMode.ALTERNATE_SCREEN_BUFFER.query, output) self.assertIn(escape.PrivateMode.MOUSE_REPORTING.query, output) self.assertIn(escape.PrivateMode.MOUSE_SGR_MODE.query, output) self.assertIn(escape.PrivateMode.BRACKETED_PASTE.query, output) self.assertIn(escape.PrivateMode.FOCUS_REPORTING.query, output) @mock.patch("os.isatty", return_value=True) def test_skips_modes_given_an_explicit_preference(self, mock_isatty): s, written = self._screen_over_pipe(bracketed_paste_mode=True, focus_reporting=False) s._detect_terminal_modes() output = "".join(written) # synchronized_output/grapheme_clustering have no sentinel to skip: always queried. self.assertIn(escape.PrivateMode.SYNCHRONIZED_OUTPUT.query, output) self.assertIn(escape.PrivateMode.GRAPHEME_CLUSTERING.query, output) self.assertNotIn(escape.PrivateMode.BRACKETED_PASTE.query, output) self.assertNotIn(escape.PrivateMode.FOCUS_REPORTING.query, output) def test_not_a_tty_writes_nothing(self): # os.isatty is left unmocked here: a plain pipe is never a tty. s, written = self._screen_over_pipe() s._detect_terminal_modes() self.assertEqual([], written) @unittest.skipIf(IS_WINDOWS, "_posix_raw_display is not importable on Windows (no fcntl/termios/tty)") class TestApplyPrivateModeReports(unittest.TestCase): """parse_input() recognizes a DECRPM reply, applies it to self.modes, and strips it out.""" @staticmethod def _screen(*, bracketed_paste: bool | None = None): s = _make_screen() s.modes.bracketed_paste = bracketed_paste written: list[str] = [] s.write = written.append s.flush = lambda: None return s, written def test_recognized_reply_resolves_the_sentinel_and_is_not_returned(self): s, _written = self._screen() keys, _raw = s.parse_input(None, None, list(b"\x1b[?2026;1$y")) self.assertEqual([], keys) self.assertTrue(s.modes.synchronized_output) def test_grapheme_clustering_reply_is_recorded_without_acting_on_it(self): """Queried (see TestDetectTerminalModes above), but nothing enables or reads it back.""" s, written = self._screen() keys, _raw = s.parse_input(None, None, list(b"\x1b[?2027;1$y")) self.assertEqual([], keys) self.assertTrue(s.modes.grapheme_clustering) self.assertEqual([], written) def test_bracketed_paste_confirmed_from_the_sentinel_is_enabled_immediately(self): s, written = self._screen(bracketed_paste=None) s.parse_input(None, None, list(b"\x1b[?2004;1$y")) self.assertTrue(s.modes.bracketed_paste) self.assertIn(escape.ENABLE_BRACKETED_PASTE_MODE, "".join(written)) def test_unrecognized_reply_resolves_the_sentinel_to_false_without_enabling(self): s, written = self._screen(bracketed_paste=None) s.parse_input(None, None, list(b"\x1b[?2004;0$y")) self.assertFalse(s.modes.bracketed_paste) self.assertEqual([], written) def test_reply_for_an_already_forced_mode_does_not_write_enable_again(self): """A mode that was never at its sentinel must not get ENABLE written a second time.""" s, written = self._screen(bracketed_paste=True) s.parse_input(None, None, list(b"\x1b[?2004;1$y")) self.assertTrue(s.modes.bracketed_paste) self.assertEqual([], written) def test_reply_for_a_mode_urwid_does_not_track_is_dropped_without_error(self): s, _written = self._screen() keys, _raw = s.parse_input(None, None, list(b"\x1b[?9999;1$y")) self.assertEqual([], keys) def test_ordinary_input_around_a_reply_still_comes_through(self): s, _written = self._screen() keys, _raw = s.parse_input(None, None, [ord("a"), *b"\x1b[?2026;1$y", ord("b")]) self.assertEqual(["a", "b"], keys) self.assertTrue(s.modes.synchronized_output) @unittest.skipIf(IS_WINDOWS, "_posix_raw_display is not importable on Windows (no fcntl/termios/tty)") class TestMouseTracking(unittest.TestCase): def test_enable_writes_the_sequence_when_support_is_unresolved(self): s = _make_screen() written: list[str] = [] s.write = written.append s._mouse_tracking(True) self.assertIn(escape.MOUSE_TRACKING_ON, written) self.assertTrue(s._mouse_tracking_enabled) def test_enable_is_a_noop_once_confirmed_unsupported(self): s = _make_screen() s.modes.mouse_reporting = False written: list[str] = [] s.write = written.append s._mouse_tracking(True) self.assertEqual([], written) self.assertFalse(s._mouse_tracking_enabled) def test_becoming_confirmed_unsupported_resets_a_previously_enabled_state(self): s = _make_screen() s.write = lambda *_a: None s._mouse_tracking(True) self.assertTrue(s._mouse_tracking_enabled) s.modes.mouse_reporting = False s._mouse_tracking(True) self.assertFalse(s._mouse_tracking_enabled) def test_disable_always_writes_the_sequence(self): s = _make_screen() s.modes.mouse_reporting = False written: list[str] = [] s.write = written.append s._mouse_tracking(False) self.assertIn(escape.MOUSE_TRACKING_OFF, written) self.assertFalse(s._mouse_tracking_enabled) def test_set_mouse_tracking_is_a_noop_when_already_at_the_requested_state(self): s = _make_screen() written: list[str] = [] s.write = written.append s.set_mouse_tracking(False) # already disabled by default self.assertEqual([], written) @unittest.skipIf(IS_WINDOWS, "_posix_raw_display is not importable on Windows (no fcntl/termios/tty)") class TestGetColsRows(unittest.TestCase): @mock.patch("fcntl.ioctl") def test_uses_ioctl_result(self, mock_ioctl): mock_ioctl.return_value = struct.pack("hhhh", 40, 100, 0, 0) s = _make_screen() cols, rows = s.get_cols_rows() self.assertEqual((100, 40), (cols, rows)) self.assertEqual(40, s.maxrow) @mock.patch("fcntl.ioctl") def test_falls_back_to_80x24_for_ansi_vt100_when_ioctl_gives_non_positive(self, mock_ioctl): mock_ioctl.return_value = struct.pack("hhhh", 0, 0, 0, 0) s = _make_screen() s.term = "ansi" cols, rows = s.get_cols_rows() self.assertEqual((80, 24), (cols, rows)) @unittest.skipIf(IS_WINDOWS, "_posix_raw_display is not importable on Windows (no fcntl/termios/tty)") class TestGetInputDescriptors(unittest.TestCase): def test_empty_when_not_started(self): s = _make_screen() self.assertEqual([], s.get_input_descriptors()) @mock.patch("os.waitpid") @mock.patch("os.kill") def test_includes_gpm_stdout_when_tracking_and_started(self, mock_kill, mock_waitpid): s = _make_screen() s.write = lambda *_a: None s.flush = lambda: None s.start() gpm_mev = mock.MagicMock() gpm_mev.pid = 4343 s.gpm_mev = gpm_mev descriptors = s.get_input_descriptors() self.assertIn(gpm_mev.stdout, descriptors) # stop() while the os.kill/os.waitpid mocks are still active, since _stop_gpm_tracking() # would otherwise try to signal a fake pid for real. s.stop() urwid-urwid-69907e4/tests/display/test_raw_display.py000066400000000000000000000470031526073734300230410ustar00rootroot00000000000000from __future__ import annotations import errno import gc import os import sys import unittest import weakref from unittest import mock import urwid from urwid import escape, signals from urwid.display._raw_display_base import TermModes, detect_terminal_properties from urwid.display.common import INPUT_DESCRIPTORS_CHANGED, AttrSpec from urwid.util import set_temporary_encoding class TestRawDisplay(unittest.TestCase): def test_attrspec_to_escape(self): s = urwid.display.raw.Screen() s.set_terminal_properties(colors=256) a2e = s._attrspec_to_escape self.assertEqual("\x1b[0;33;42m", a2e(s.AttrSpec("brown", "dark green"))) self.assertEqual("\x1b[0;38;5;229;4;48;5;164m", a2e(s.AttrSpec("#fea,underline", "#d0d"))) self.assertEqual("\x1b[0;33;2;42m", a2e(s.AttrSpec("brown,faint", "dark green"))) @unittest.skipUnless( sys.implementation.name == "cpython", "relies on reference counting freeing the screen as soon as it is dropped", ) def test_dropped_screen_is_freed_without_the_cyclic_collector(self): """Nothing in a new screen refers back to it, so dropping it releases its sockets at once.""" gc.disable() self.addCleanup(gc.enable) s = urwid.display.raw.Screen() screen_ref = weakref.ref(s) del s self.assertIsNone(screen_ref()) def test_attrspec_faint(self): a = urwid.AttrSpec("dark red,faint", "") self.assertTrue(a.faint) self.assertEqual("dark red,faint", a.foreground) # faint can be combined with other settings and is kept on round-trip self.assertEqual( "AttrSpec('yellow,bold,faint', 'dark blue')", repr(urwid.AttrSpec("yellow,faint,bold", "dark blue")), ) self.assertFalse(urwid.AttrSpec("dark red", "").faint) def test_last_row_without_preceding_segment(self): """A last row holding a single grapheme has no character to slide back.""" s = urwid.display.raw.Screen() row = [(None, None, "日".encode())] self.assertEqual((row, 0, None), s._last_row(row)) def test_attrspec_truecolor_to_escape(self): s = urwid.display.raw.Screen() s.set_terminal_properties(colors=16777216) a2e = s._attrspec_to_escape self.assertEqual( "\x1b[0;38;2;118;185;0;48;2;0;0;0m", a2e(s.AttrSpec("#76b900", "#000000")), ) def test_named_palette_attr_to_escape(self): # Bright foreground colors render either as bold or as a 90-series color depending on the # terminal, so bright_is_bold is pinned instead of inherited from the ambient TERM. for bright_is_bold, expected in ((True, "\x1b[0;1;32;1;44m"), (False, "\x1b[0;92;1;44m")): with self.subTest(bright_is_bold=bright_is_bold): s = urwid.display.raw.Screen() s.set_terminal_properties(colors=256, bright_is_bold=bright_is_bold) s.register_palette_entry("focus", "light green,bold", "dark blue") self.assertEqual(expected, s._attr_to_escape("focus")) def test_draw_screen_two_columns_wide_characters(self): """Drawing double width text on a two column screen used to raise IndexError.""" s = urwid.display.raw.Screen() written: list[str] = [] s.write = written.append s.flush = lambda: None s._started = True with set_temporary_encoding("utf-8"): canvas = urwid.Text("日本語").render((2,)) s.draw_screen((2, canvas.rows()), canvas) self.assertEqual(3, canvas.rows()) self.assertIn("語", "".join(written)) def test_draw_screen_shows_cursor_when_set(self): s = urwid.display.raw.Screen() written: list[str] = [] s.write = written.append s.flush = lambda: None s._started = True canvas = urwid.Edit("x").render((10,), focus=True) s.draw_screen((10, canvas.rows()), canvas) output = "".join(written) self.assertEqual((1, 0), canvas.cursor) self.assertIn(escape.SHOW_CURSOR, output) self.assertIn(escape.HIDE_CURSOR, output) def test_draw_screen_replaces_control_characters(self): """Displayed text must not reach the terminal as C0 or C1 control sequences (e.g. OSC 52, CSI).""" for encoding in ("utf-8", "latin-1"): with self.subTest(encoding=encoding), set_temporary_encoding(encoding): s = urwid.display.raw.Screen() written: list[str] = [] s.write = written.append s.flush = lambda: None # a canvas built directly, as Text leaves control characters out of the canvas it renders text = "a\x1b]0;t\x07 \x9d52;c;SGVsbG8=\x9c \x9b2J \x85é".encode(encoding) canvas = urwid.TextCanvas([text], maxcol=40) with mock.patch.object(s, "_started", True): s.draw_screen((40, canvas.rows()), canvas) output = "".join(written) self.assertIn("a?]0;t? ?52;c;SGVsbG8=? ?2J ?é", output) self.assertFalse(any("\x80" <= char <= "\x9f" for char in output)) def test_restart_after_stop_reconnects_input(self): """stop() followed by start() must leave the screen's input descriptors watchable again (regression test for urwid/urwid#285). MainLoop reacts to INPUT_DESCRIPTORS_CHANGED by re-hooking the event loop with whatever ``get_input_descriptors()`` currently returns, so that list must be non-empty again once ``start()`` finishes -- otherwise stopping the screen to run an external program and then restarting it silently stops delivering keypresses. """ read_fd, write_fd = os.pipe() self.addCleanup(os.close, write_fd) s = urwid.display.raw.Screen(input=os.fdopen(read_fd, "rb", buffering=0), output=open(os.devnull, "w")) watched = {} class FakeEventLoop: def watch_file(self, fd, callback): fd = fd if isinstance(fd, int) else fd.fileno() watched[fd] = callback return fd def remove_watch_file(self, handle): watched.pop(handle, None) return True event_loop = FakeEventLoop() def reset_input_descriptors(): s.unhook_event_loop(event_loop) s.hook_event_loop(event_loop, lambda keys, raw: None) s.start() signals.connect_signal(s, INPUT_DESCRIPTORS_CHANGED, reset_input_descriptors) reset_input_descriptors() self.assertTrue(watched, "expected watched descriptors after the initial start()") s.stop() s.start() self.assertTrue(watched, "no descriptors are watched after restart -- regression of urwid/urwid#285") s.stop() def test_modify_terminal_palette_restored_on_stop(self): """Palette entries modified with modify_terminal_palette() must be reset when the screen stops, so urwid doesn't leave the user's terminal with a custom palette after the process exits (urwid/urwid#458). """ s = urwid.display.raw.Screen() written: list[str] = [] s.write = written.append s.flush = lambda: None s._started = True s.modify_terminal_palette([(1, 255, 0, 0), (2, 0, 255, 0)]) self.assertEqual({1, 2}, s._modified_palette_entries) s._stop_restore_palette() self.assertIn("\x1b]104;1;2\x1b\\", "".join(written)) self.assertEqual(set(), s._modified_palette_entries) def test_stop_restore_palette_noop_when_untouched(self): """No reset escape should be sent if the palette was never modified.""" s = urwid.display.raw.Screen() written: list[str] = [] s.write = written.append s.flush = lambda: None s._started = True s._stop_restore_palette() self.assertEqual([], written) def test_draw_screen_ignores_eintr(self): """An EINTR OSError from write() during draw_screen() is swallowed (interrupted syscall).""" s = urwid.display.raw.Screen() s._setup_G1_done = True # skip the G1-setup write, so the error comes from the frame write under test s.write = mock.Mock(side_effect=OSError(errno.EINTR, "interrupted system call")) s.flush = lambda: None s._started = True canvas = urwid.Text("x").render((1,)) s.draw_screen((1, canvas.rows()), canvas) # must not raise def test_setup_g1_propagates_oserror(self): """A terminal that cannot be written to raises instead of retrying, and the setup is tried again later.""" s = urwid.display.raw.Screen() s.write = mock.Mock(side_effect=OSError(errno.EIO, "I/O error")) s.flush = lambda: None canvas = urwid.Text("x").render((1,)) with mock.patch.object(s, "_started", True): for _ in range(2): with self.assertRaises(OSError): s.draw_screen((1, canvas.rows()), canvas) self.assertEqual([mock.call(escape.DESIGNATE_G1_SPECIAL)] * 2, s.write.call_args_list) def test_draw_screen_reraises_other_oserror(self): """An OSError with an errno other than EINTR during draw_screen() must propagate.""" s = urwid.display.raw.Screen() s._setup_G1_done = True # skip the G1-setup write, so the error comes from the frame write under test s.write = mock.Mock(side_effect=OSError(errno.EIO, "I/O error")) s.flush = lambda: None s._started = True canvas = urwid.Text("x").render((1,)) with self.assertRaises(OSError): s.draw_screen((1, canvas.rows()), canvas) def test_get_available_raw_input_drains_resize_pipe(self): """Every pending write on the resize pipe is drained in one call, however many arrived.""" s = urwid.display.raw.Screen() # Only the resize-pipe drain is under test here: route around the keyboard-input side # (_wait_for_input_ready()'s select() over get_input_descriptors(), which is platform- and # terminal-state-sensitive -- e.g. it needs a started screen with a real selectable input, # and an empty descriptor list raises WSAEINVAL when select()ed on Windows) entirely. s._get_input_codes = list s._resize_pipe_wr.send(b"R") s._resize_pipe_wr.send(b"R") s._resize_pipe_wr.send(b"R") codes = s.get_available_raw_input() self.assertEqual([], codes) # The pipe is now empty: a second call must not find anything left to drain either. self.assertEqual([], s.get_available_raw_input()) def test_resize_drops_screen_buffer_in_parse_input(self): """The resize handler leaves the screen buffer to the drawing thread, which drops it with the report.""" s = urwid.display.raw.Screen() s.screen_buf = [[]] s._sigwinch_handler() self.assertTrue(s._resized) self.assertEqual([[]], s.screen_buf) keys, _raw = s.parse_input(None, None, []) self.assertEqual(["window resize"], keys) self.assertFalse(s._resized) self.assertIsNone(s.screen_buf) def test_resize_wake_up_does_not_block_on_a_full_socket(self): """A wake-up already pending in a full socket is enough, so the handler returns instead of blocking.""" s = urwid.display.raw.Screen() with mock.patch.object(s, "_resize_pipe_wr") as wake_socket: wake_socket.send.side_effect = BlockingIOError s._sigwinch_handler() wake_socket.send.assert_called_once_with(b"R") self.assertTrue(s._resized) def test_attrspec_hash_is_stable_and_matches_equal_instances(self): """AttrSpec caches its hash at construction time; equal instances must still hash equal, and the cached value must match what a fresh hash computation would give. """ a = AttrSpec("yellow", "dark blue") b = AttrSpec("yellow", "dark blue") self.assertEqual(a, b) self.assertEqual(hash(a), hash(b)) self.assertEqual(hash((a.__class__, a._value)), hash(a)) def test_attr_to_escape_uses_attrspec_as_dict_key(self): """_pal_escape.get(a) is looked up with an AttrSpec instance directly (not just by palette name), which requires AttrSpec to hash and compare consistently. """ s = urwid.display.raw.Screen() s.set_terminal_properties(colors=256) a = s.AttrSpec("brown", "dark green") s._pal_escape[a] = "sentinel escape" self.assertEqual("sentinel escape", s._attr_to_escape(a)) self.assertEqual("sentinel escape", s._attr_to_escape(s.AttrSpec("brown", "dark green"))) class TestTerminalProperties(unittest.TestCase): def test_term_families(self) -> None: cases = ( ("", 16, True, True, False, True), ("xterm", 16, True, False, False, True), ("xterm-256color", 256, True, False, False, True), ("xterm-direct", 16777216, True, False, False, True), ("linux", 16, True, True, True, True), ("linux-16color", 16, True, True, True, True), ("gnome-256color", 256, True, False, False, True), ("konsole", 16, True, False, False, True), ("konsole-direct", 16777216, True, False, False, True), ("rxvt-unicode-256color", 256, True, False, False, True), ("vte-256color", 256, True, False, False, True), ("screen", 16, True, True, False, False), ("screen-256color", 256, True, True, False, False), ("screen-256color-bce", 256, True, True, False, True), ("screen.xterm-256color", 256, True, False, False, False), ("tmux-256color", 256, True, False, False, True), ("alacritty", 16777216, True, False, False, True), ("xterm-kitty", 16777216, True, False, False, True), ("dumb", 1, False, True, False, False), ("xterm-88color", 88, True, False, False, True), ) for term, colors, underline, bright_bold, blink, bce in cases: with self.subTest(term=term): props = detect_terminal_properties(term, {}, windows_version=(0, 0, 0)) self.assertEqual(colors, props.colors) self.assertEqual(underline, props.has_underline) self.assertEqual(bright_bold, props.fg_bright_is_bold) self.assertEqual(blink, props.bg_bright_is_blink) self.assertEqual(bce, props.back_color_erase) def test_color_environment_variables(self) -> None: xterm = "xterm-256color" def colors(term: str, environ: dict[str, str]) -> int: return detect_terminal_properties(term, environ, windows_version=(0, 0, 0)).colors self.assertEqual(1, colors(xterm, {"NO_COLOR": "1"})) self.assertEqual(1, colors(xterm, {"CLICOLOR": "0"})) self.assertEqual(16, colors(xterm, {"FORCE_COLOR": "1"})) self.assertEqual(256, colors("xterm", {"FORCE_COLOR": "2"})) self.assertEqual(16777216, colors("linux", {"FORCE_COLOR": "3"})) self.assertEqual(256, colors("xterm", {"CLICOLOR_FORCE": "2"})) self.assertEqual(16777216, colors(xterm, {"COLORTERM": "truecolor"})) self.assertEqual(16777216, colors("xterm", {"COLORTERM": "24bit"})) self.assertEqual(256, colors("xterm", {"COLORTERM": "gnome-terminal"})) # NO_COLOR wins over COLORTERM / FORCE_COLOR self.assertEqual(1, colors(xterm, {"NO_COLOR": "1", "COLORTERM": "truecolor"})) self.assertEqual(1, colors(xterm, {"NO_COLOR": "1", "FORCE_COLOR": "3"})) # Empty NO_COLOR is unset per the spec self.assertEqual(256, colors(xterm, {"NO_COLOR": ""})) def test_windows_console_vt(self) -> None: win7 = (6, 1, 7601) win10_rtm = (10, 0, 10240) win10_1511 = (10, 0, 10586) win10_1703 = (10, 0, 15063) win11 = (10, 0, 22621) props = detect_terminal_properties("", {}, windows_version=win7) self.assertEqual(16, props.colors) self.assertTrue(props.fg_bright_is_bold) props = detect_terminal_properties("", {}, windows_version=win10_rtm) self.assertEqual(16, props.colors) self.assertTrue(props.fg_bright_is_bold) props = detect_terminal_properties("", {}, windows_version=win10_1511) self.assertEqual(256, props.colors) self.assertFalse(props.fg_bright_is_bold) self.assertTrue(props.has_underline) self.assertTrue(props.back_color_erase) self.assertFalse(props.bg_bright_is_blink) for version in (win10_1703, win11): with self.subTest(windows_version=version): props = detect_terminal_properties("", {}, windows_version=version) self.assertEqual(16777216, props.colors) self.assertFalse(props.fg_bright_is_bold) self.assertTrue(props.has_underline) self.assertTrue(props.back_color_erase) self.assertFalse(props.bg_bright_is_blink) self.assertEqual(1, detect_terminal_properties("", {"NO_COLOR": "1"}, windows_version=win11).colors) self.assertEqual(1, detect_terminal_properties("dumb", {}, windows_version=win11).colors) def test_windows_terminal_hosts(self) -> None: # Windows Terminal / ConEmu / VS Code report xterm-256color but speak 24-bit SGR. for environ in ( {"WT_SESSION": "deadbeef-0000-0000-0000-000000000000"}, {"WT_PROFILE_ID": "{guid}"}, {"ConEmuANSI": "ON"}, {"ConEmuPID": "1234"}, {"TERM_PROGRAM": "vscode"}, ): with self.subTest(environ=environ): props = detect_terminal_properties("xterm-256color", environ, windows_version=(6, 1, 7601)) self.assertEqual(16777216, props.colors) self.assertFalse(props.fg_bright_is_bold) self.assertTrue(props.has_underline) class TestTermModesSetFromCodes(unittest.TestCase): def test_recognized_reply_resolves_sentinel(self) -> None: modes = TermModes() newly_supported = modes.set_from_codes((escape.PrivateMode.BRACKETED_PASTE, 1)) self.assertTrue(modes.bracketed_paste) self.assertEqual({"bracketed_paste"}, newly_supported) def test_unrecognized_reply_resolves_sentinel_to_false(self) -> None: modes = TermModes() newly_supported = modes.set_from_codes((escape.PrivateMode.BRACKETED_PASTE, 0)) self.assertFalse(modes.bracketed_paste) self.assertEqual(frozenset(), newly_supported) def test_reply_for_an_already_resolved_field_is_not_reported_as_newly_supported(self) -> None: modes = TermModes(bracketed_paste=True) newly_supported = modes.set_from_codes((escape.PrivateMode.BRACKETED_PASTE, 1)) self.assertTrue(modes.bracketed_paste) self.assertEqual(frozenset(), newly_supported) def test_unmapped_mode_is_ignored(self) -> None: modes = TermModes() newly_supported = modes.set_from_codes(("9999", 1)) self.assertEqual(frozenset(), newly_supported) def test_multiple_reports_in_one_call(self) -> None: modes = TermModes() newly_supported = modes.set_from_codes( (escape.PrivateMode.BRACKETED_PASTE, 1), (escape.PrivateMode.FOCUS_REPORTING, 0), (escape.PrivateMode.SYNCHRONIZED_OUTPUT, 2), ) self.assertTrue(modes.bracketed_paste) self.assertFalse(modes.focus_reporting) self.assertTrue(modes.synchronized_output) self.assertEqual({"bracketed_paste", "synchronized_output"}, newly_supported) urwid-urwid-69907e4/tests/display/test_web.py000066400000000000000000000474221526073734300213050ustar00rootroot00000000000000from __future__ import annotations import io import os import sys import unittest from unittest import mock import urwid from urwid.display import web from urwid.display.common import AttrSpec, ScreenError IS_WINDOWS = sys.platform == "win32" IS_GRAALPY = sys.implementation.name == "graalpy" class HandleShortRequestTest(unittest.TestCase): def setUp(self) -> None: self.environ = { "REQUEST_METHOD": "POST", "HTTP_X_URWID_ID": "valid-id", } def test_keyboard_input_uses_request_charset(self) -> None: stdin = io.StringIO("é\n") stdout = io.StringIO() environ = {**self.environ, "CONTENT_TYPE": 'text/plain; charset="iso-8859-1"'} with ( mock.patch.dict(os.environ, environ, clear=True), mock.patch.object(web.sys, "stdin", stdin), mock.patch.object(web.sys, "stdout", stdout), mock.patch.object(web.os, "open", return_value=42), mock.patch.object(web.os, "write") as write, mock.patch.object(web.os, "close") as close, ): self.assertTrue(web.handle_short_request()) write.assert_called_once_with(42, b"\xe9\n") close.assert_called_once_with(42) self.assertEqual("Content-type: text/plain\r\n\r\n", stdout.getvalue()) def test_keyboard_encoding_error_closes_pipe(self) -> None: stdin = io.StringIO("é\n") stdout = io.StringIO() environ = {**self.environ, "CONTENT_TYPE": "text/plain; charset=ascii"} with ( mock.patch.dict(os.environ, environ, clear=True), mock.patch.object(web.sys, "stdin", stdin), mock.patch.object(web.sys, "stdout", stdout), mock.patch.object(web.os, "open", return_value=42), mock.patch.object(web.os, "write") as write, mock.patch.object(web.os, "close") as close, ): self.assertTrue(web.handle_short_request()) write.assert_not_called() close.assert_called_once_with(42) self.assertEqual("Status: 400 Bad Request\r\n\r\n", stdout.getvalue()) @unittest.skipIf(IS_WINDOWS, "The polling update channel is a POSIX-only socket.AF_UNIX socket") def test_polling_decodes_after_receiving_complete_utf8_payload(self) -> None: stdout = io.StringIO() environ = {**self.environ, "HTTP_X_URWID_METHOD": "polling"} sock = mock.MagicMock() sock.__enter__.return_value = sock sock.recv.side_effect = [b"\xc3", b"\xa9", b""] with ( mock.patch.dict(os.environ, environ, clear=True), mock.patch.object(web.sys, "stdout", stdout), mock.patch.object(web.socket, "socket", return_value=sock), ): self.assertTrue(web.handle_short_request()) sock.__exit__.assert_called_once() self.assertEqual("Content-type: text/plain; charset=utf-8\r\n\r\né", stdout.getvalue()) @unittest.skipIf(IS_WINDOWS, "The polling update channel is a POSIX-only socket.AF_UNIX socket") def test_polling_invalid_utf8_closes_socket(self) -> None: stdout = io.StringIO() environ = {**self.environ, "HTTP_X_URWID_METHOD": "polling"} sock = mock.MagicMock() sock.__enter__.return_value = sock sock.recv.side_effect = [b"\xff", b""] with ( mock.patch.dict(os.environ, environ, clear=True), mock.patch.object(web.sys, "stdout", stdout), mock.patch.object(web.socket, "socket", return_value=sock), ): self.assertTrue(web.handle_short_request()) sock.__exit__.assert_called_once() self.assertEqual("Status: 502 Bad Gateway\r\n\r\n", stdout.getvalue()) @unittest.skipIf(IS_WINDOWS, "Reading the client pipe requires the POSIX-only os.O_NONBLOCK flag") class ScreenGetInputTest(unittest.TestCase): """Tests for Screen.get_input method with improved validation.""" def setUp(self) -> None: self.screen = web.Screen() self.screen.input_fd = 10 self.screen.pipe_name = "/tmp/test_pipe" self.screen.update_method = "multipart" def _make_selector_mock(self, has_input: bool): """Create a mock selector that optionally returns file descriptor events.""" selector = mock.MagicMock() if has_input: event = mock.MagicMock() event.fd = 10 selector.__enter__.return_value = selector selector.__exit__.return_value = None selector.select.return_value = [(event, None)] else: selector.__enter__.return_value = selector selector.__exit__.return_value = None selector.select.return_value = [] return selector def test_get_input_no_input_available_raw_false(self) -> None: """When no input available and raw_keys=False, returns empty list.""" selector_mock = self._make_selector_mock(has_input=False) with mock.patch("urwid.display.web.selectors.DefaultSelector", return_value=selector_mock): result = self.screen.get_input(raw_keys=False) self.assertEqual(result, []) def test_get_input_no_input_available_raw_true(self) -> None: """When no input available and raw_keys=True, returns tuple of empty lists.""" selector_mock = self._make_selector_mock(has_input=False) with mock.patch("urwid.display.web.selectors.DefaultSelector", return_value=selector_mock): result = self.screen.get_input(raw_keys=True) self.assertEqual(result, ([], [])) def test_get_input_regular_keys(self) -> None: """Returns regular keyboard input.""" selector_mock = self._make_selector_mock(has_input=True) keydata = "a\nb\nc\n" with ( mock.patch("urwid.display.web.selectors.DefaultSelector", return_value=selector_mock), mock.patch("urwid.display.web.os.read", return_value=keydata.encode("utf-8")), mock.patch("urwid.display.web.os.close"), mock.patch("urwid.display.web.os.open", return_value=10), ): result = self.screen.get_input(raw_keys=False) self.assertEqual(result, ["a", "b", "c"]) def test_get_input_valid_window_resize(self) -> None: """Handles valid window resize commands correctly.""" selector_mock = self._make_selector_mock(has_input=True) keydata = "window resize 80 24\n" with ( mock.patch("urwid.display.web.selectors.DefaultSelector", return_value=selector_mock), mock.patch("urwid.display.web.os.read", return_value=keydata.encode("utf-8")), mock.patch("urwid.display.web.os.close"), mock.patch("urwid.display.web.os.open", return_value=10), ): result = self.screen.get_input(raw_keys=False) self.assertEqual(result, ["window resize"]) self.assertEqual(self.screen.screen_size, (80, 24)) def test_get_input_window_resize_with_regular_keys(self) -> None: """Handles window resize mixed with regular input.""" selector_mock = self._make_selector_mock(has_input=True) keydata = "key1\nwindow resize 120 40\nkey2\n" with ( mock.patch("urwid.display.web.selectors.DefaultSelector", return_value=selector_mock), mock.patch("urwid.display.web.os.read", return_value=keydata.encode("utf-8")), mock.patch("urwid.display.web.os.close"), mock.patch("urwid.display.web.os.open", return_value=10), ): result = self.screen.get_input(raw_keys=False) self.assertEqual(result, ["key1", "key2", "window resize"]) self.assertEqual(self.screen.screen_size, (120, 40)) def test_get_input_invalid_resize_wrong_param_count_too_few(self) -> None: """Rejects window resize with too few parameters.""" selector_mock = self._make_selector_mock(has_input=True) keydata = "window resize 80\n" with ( mock.patch("urwid.display.web.selectors.DefaultSelector", return_value=selector_mock), mock.patch("urwid.display.web.os.read", return_value=keydata.encode("utf-8")), mock.patch("urwid.display.web.os.close"), mock.patch("urwid.display.web.os.open", return_value=10), ): result = self.screen.get_input(raw_keys=False) # Invalid resize should be treated as regular input, not as resize command self.assertEqual(result, ["window resize 80"]) def test_get_input_invalid_resize_wrong_param_count_too_many(self) -> None: """Rejects window resize with too many parameters.""" selector_mock = self._make_selector_mock(has_input=True) keydata = "window resize 80 24 extra\n" with ( mock.patch("urwid.display.web.selectors.DefaultSelector", return_value=selector_mock), mock.patch("urwid.display.web.os.read", return_value=keydata.encode("utf-8")), mock.patch("urwid.display.web.os.close"), mock.patch("urwid.display.web.os.open", return_value=10), ): result = self.screen.get_input(raw_keys=False) self.assertEqual(result, ["window resize 80 24 extra"]) def test_get_input_invalid_resize_non_decimal_negative(self) -> None: """Rejects window resize with negative dimensions.""" selector_mock = self._make_selector_mock(has_input=True) keydata = "window resize -80 24\n" with ( mock.patch("urwid.display.web.selectors.DefaultSelector", return_value=selector_mock), mock.patch("urwid.display.web.os.read", return_value=keydata.encode("utf-8")), mock.patch("urwid.display.web.os.close"), mock.patch("urwid.display.web.os.open", return_value=10), ): result = self.screen.get_input(raw_keys=False) self.assertEqual(result, ["window resize -80 24"]) def test_get_input_invalid_resize_non_decimal_float(self) -> None: """Rejects window resize with floating point dimensions.""" selector_mock = self._make_selector_mock(has_input=True) keydata = "window resize 80.5 24\n" with ( mock.patch("urwid.display.web.selectors.DefaultSelector", return_value=selector_mock), mock.patch("urwid.display.web.os.read", return_value=keydata.encode("utf-8")), mock.patch("urwid.display.web.os.close"), mock.patch("urwid.display.web.os.open", return_value=10), ): result = self.screen.get_input(raw_keys=False) self.assertEqual(result, ["window resize 80.5 24"]) def test_get_input_invalid_resize_non_decimal_alpha(self) -> None: """Rejects window resize with non-numeric dimensions.""" selector_mock = self._make_selector_mock(has_input=True) keydata = "window resize abc 24\n" with ( mock.patch("urwid.display.web.selectors.DefaultSelector", return_value=selector_mock), mock.patch("urwid.display.web.os.read", return_value=keydata.encode("utf-8")), mock.patch("urwid.display.web.os.close"), mock.patch("urwid.display.web.os.open", return_value=10), ): result = self.screen.get_input(raw_keys=False) self.assertEqual(result, ["window resize abc 24"]) def test_get_input_input_tail_buffering(self) -> None: """Properly buffers incomplete lines at the end of input.""" selector_mock = self._make_selector_mock(has_input=True) self.screen.input_tail = "incom" keydata = "plete\nkey2\n" with ( mock.patch("urwid.display.web.selectors.DefaultSelector", return_value=selector_mock), mock.patch("urwid.display.web.os.read", return_value=keydata.encode("utf-8")), mock.patch("urwid.display.web.os.close"), mock.patch("urwid.display.web.os.open", return_value=10), ): result = self.screen.get_input(raw_keys=False) self.assertEqual(result, ["incomplete", "key2"]) self.assertEqual(self.screen.input_tail, "") def test_get_input_input_tail_carried_forward(self) -> None: """Incomplete last line is carried to next read.""" selector_mock = self._make_selector_mock(has_input=True) keydata = "key1\nincompl" with ( mock.patch("urwid.display.web.selectors.DefaultSelector", return_value=selector_mock), mock.patch("urwid.display.web.os.read", return_value=keydata.encode("utf-8")), mock.patch("urwid.display.web.os.close"), mock.patch("urwid.display.web.os.open", return_value=10), ): result = self.screen.get_input(raw_keys=False) self.assertEqual(result, ["key1"]) self.assertEqual(self.screen.input_tail, "incompl") class ScreenStartTest(unittest.TestCase): """Tests for the request validation in Screen.start.""" def setUp(self) -> None: self.screen = web.Screen() def test_start_rejects_invalid_resize_request(self) -> None: for client_init in ( "", "hello\n", "window resize\n", "window resize 80\n", "window resize 80 24 extra\n", "window resize -80 24\n", "window resize 80.5 24\n", "window resize abc 24\n", ): stdout = io.StringIO() with ( self.subTest(client_init=client_init), mock.patch.dict(os.environ, {"HTTP_X_URWID_METHOD": "multipart"}, clear=True), mock.patch.object(web.sys, "stdin", io.StringIO(client_init)), mock.patch.object(web.sys, "stdout", stdout), ): with self.assertRaises(SystemExit) as ctx: self.screen.start() self.assertEqual(ctx.exception.code, 0) self.assertEqual("Status: 400 Bad Request\r\n\r\n", stdout.getvalue()) self.assertFalse(self.screen.started) def test_start_rejects_not_set_update_method(self) -> None: stdout = io.StringIO() stdin = io.StringIO("window resize 80 24\n") with ( mock.patch.dict(os.environ, {}, clear=True), mock.patch.object(web.sys, "stdin", stdin), mock.patch.object(web.sys, "stdout", stdout), ): with self.assertRaises(RuntimeError) as ctx: self.screen.start() self.assertEqual("'HTTP_X_URWID_METHOD' environment vairable is not set", str(ctx.exception)) def test_start_rejects_unsupported_update_method(self) -> None: environ = {"HTTP_X_URWID_METHOD": "polling child"} stdout = io.StringIO() stdin = io.StringIO("window resize 80 24\n") with ( mock.patch.dict(os.environ, environ, clear=True), mock.patch.object(web.sys, "stdin", stdin), mock.patch.object(web.sys, "stdout", stdout), ): with self.assertRaises(SystemExit) as ctx: self.screen.start() self.assertEqual(ctx.exception.code, 0) self.assertEqual("Status: 400 Bad Request\r\n\r\n", stdout.getvalue()) # the request body is left untouched: validation happens before reading it self.assertEqual(0, stdin.tell()) @unittest.skipIf(IS_WINDOWS, "Creating the client pipe requires the POSIX-only os.mkfifo and signal.alarm") @unittest.skipIf(IS_GRAALPY, "os.mkfifo is missing on GraalPy") def test_start_accepts_valid_resize_request(self) -> None: with ( mock.patch.object(web.sys, "stdin", io.StringIO("window resize 80 24\n")), mock.patch.dict(os.environ, {"HTTP_X_URWID_METHOD": "multipart"}, clear=True), mock.patch.object(web.glob, "glob", return_value=[]), mock.patch.object(web.os, "mkfifo"), mock.patch.object(web.os, "open", return_value=42), mock.patch.object(web.signal, "signal"), mock.patch.object(web.signal, "alarm"), ): self.screen.start() self.assertTrue(self.screen.started) self.assertEqual(self.screen.screen_size, (80, 24)) self.assertEqual(self.screen.last_screen, {}) self.assertEqual(self.screen.last_screen_width, 0) class SpanStyleTest(unittest.TestCase): """Tests for web.code_span, which wraps common.attr_spec_to_css's inline CSS into the wire format sent to the browser. The colour/attribute-to-CSS logic itself is covered by tests/display/test_common.py, since web and html_fragment now share it. """ def test_code_span_wraps_style_and_text(self) -> None: span = web.code_span("hi", AttrSpec("white", "black")) self.assertEqual("color:#ffffff;background-color:#000000\x01hi\n", span) def test_code_span_cursor_splits_into_three_pieces_with_swapped_colours(self) -> None: span = web.code_span("abc", AttrSpec("white", "black"), cursor=1) self.assertEqual( "color:#ffffff;background-color:#000000\x01a\n" "color:#000000;background-color:#ffffff\x01b\n" "color:#ffffff;background-color:#000000\x01c\n", span, ) class ScreenPaletteTest(unittest.TestCase): """Tests for Screen palette registration and draw_screen's attribute/colour resolution, which must honour the full AttrSpec feature set the way raw_display does. """ def setUp(self) -> None: self.screen = web.Screen() self.screen.update_method = "multipart" def _draw(self, canvas: urwid.Canvas) -> str: stdout = io.StringIO() with ( mock.patch.object(web.sys, "stdout", stdout), # signal.alarm() doesn't exist on Windows; create=True lets draw_screen's # unconditional signal.alarm(...) calls hit the mock there too mock.patch.object(web.signal, "alarm", create=True), ): self.screen.draw_screen((canvas.cols(), canvas.rows()), canvas) return stdout.getvalue() def test_default_palette_entry_is_registered_on_construction(self) -> None: self.assertIn(None, self.screen._palette) def test_named_palette_entry_renders_registered_attributes(self) -> None: self.screen.register_palette_entry("focus", "light red,bold,underline,standout", "dark blue") canvas = urwid.AttrMap(urwid.Text("hi"), "focus").render((5,)) output = self._draw(canvas) # standout swaps light red (#ff0000) and dark blue (#0000ee) self.assertIn("color:#0000ee;background-color:#ff0000;text-decoration:underline;font-weight:bold\x01hi", output) def test_attrspec_on_canvas_bypasses_the_palette(self) -> None: canvas = urwid.AttrMap(urwid.Text("hi"), AttrSpec("white", "black")).render((5,)) output = self._draw(canvas) self.assertIn("color:#ffffff;background-color:#000000\x01hi", output) def test_register_palette_copies_an_existing_entry(self) -> None: self.screen.register_palette_entry("focus", "white", "black") self.screen.register_palette([("alias", "focus")]) self.assertEqual(self.screen._palette["focus"], self.screen._palette["alias"]) def test_register_palette_rejects_unknown_alias_target(self) -> None: with self.assertRaises(ScreenError): self.screen.register_palette([("alias", "missing")]) def test_set_terminal_properties_switches_to_mono_rendering(self) -> None: self.screen.register_palette_entry("focus", "white,bold", "black", mono="underline") self.screen.set_terminal_properties(colors=1) canvas = urwid.AttrMap(urwid.Text("hi"), "focus").render((5,)) output = self._draw(canvas) # mono mode drops the colour choice (falls back to the page default) but keeps 'underline' self.assertIn("color:#000000;background-color:#e5e5e5;text-decoration:underline\x01hi", output) def test_set_terminal_properties_rejects_unsupported_colour_count(self) -> None: with self.assertRaises(KeyError): self.screen.set_terminal_properties(colors=42) urwid-urwid-69907e4/tests/display/test_win32_raw_display.py000066400000000000000000000123441526073734300240630ustar00rootroot00000000000000"""Tests for the Windows console input of the raw display.""" from __future__ import annotations import socket import sys import typing import unittest from unittest import mock from urwid import signals from urwid.display.common import INPUT_DESCRIPTORS_CHANGED IS_WINDOWS = sys.platform == "win32" try: from urwid.display import _win32, _win32_raw_display except ImportError: # the console API is bound through ctypes.windll, which exists only on Windows _win32 = _win32_raw_display = None def _key_record(char: str) -> typing.Any: """Build a key-down console input record for one UTF-16 code unit.""" record = _win32.INPUT_RECORD() record.EventType = _win32.EventType.KEY_EVENT record.Event.KeyEvent.bKeyDown = 1 record.Event.KeyEvent.uChar.UnicodeChar = char return record @unittest.skipUnless(IS_WINDOWS, "_win32_raw_display needs the Windows console API") class TestReadInputThread(unittest.TestCase): """The background thread that turns console key events into input bytes.""" def _run_reads(self, *batches: list[str], read_succeeds: bool = True) -> list[bytes]: """Run the reader over console reads returning *batches* of key characters, and return what it sent.""" sent: list[bytes] = [] thread = _win32_raw_display.ReadInputThread(mock.Mock(sendall=sent.append), mock.Mock()) pending = list(batches) def wait(_handle: int, _timeout: int) -> int: if not pending: thread.should_exit = True return _win32.WAIT_TIMEOUT return _win32.WAIT_OBJECT_0 def read_console(_handle: int, records: typing.Any, _size: int, read_count: typing.Any) -> bool: batch = pending.pop(0) for index, char in enumerate(batch): records[index] = _key_record(char) read_count.value = len(batch) return read_succeeds with ( mock.patch.object(_win32_raw_display, "byref", side_effect=lambda value: value), mock.patch.object(_win32, "GetStdHandle", return_value=1), mock.patch.object(_win32, "WaitForSingleObject", side_effect=wait), mock.patch.object(_win32, "ReadConsoleInputW", side_effect=read_console), ): thread.run() return sent def test_character_split_across_reads_is_joined(self): """A character outside the BMP whose two UTF-16 halves arrive in separate reads is sent whole.""" self.assertEqual([b"a", "\U0001f600b".encode()], self._run_reads(["a", "\ud83d"], ["\ude00", "b"])) def test_lone_surrogate_and_phantom_nul_do_not_stop_the_reader(self): """A half of a pair with no other half is replaced, and modifier-key NUL input is dropped.""" self.assertEqual(["�x".encode(), b"y"], self._run_reads(["\ude00", "\x00", "x"], ["y"])) def test_failed_read_ends_the_reader(self): """A failing console read ends the thread instead of looping over stale records.""" self.assertEqual([], self._run_reads(["a"], ["b"], read_succeeds=False)) @unittest.skipUnless(IS_WINDOWS, "_win32_raw_display needs the Windows console API") class TestScreenInput(unittest.TestCase): """The Windows screen's handling of its input thread and input socket.""" def test_stop_ends_the_console_reader_and_unhook_keeps_it(self): """The reader ends with the screen, not with each re-hook of the event loop.""" s = _win32_raw_display.Screen() s.write = lambda *_a: None s.flush = lambda: None reader = mock.Mock() with mock.patch.object(s, "_input_thread", reader), mock.patch.object(s, "_started", True): s.unhook_event_loop(mock.Mock()) reader.join.assert_not_called() s.stop() self.assertTrue(reader.should_exit) reader.join.assert_called_once() def test_no_reader_outlives_a_stop_that_re_hooks_the_screen(self): """A main loop re-hooking the screen while it stops does not leave a reader running.""" readers: list[mock.Mock] = [] s = _win32_raw_display.Screen() s.write = lambda *_a: None s.flush = lambda: None event_loop = mock.Mock() signals.connect_signal( s, INPUT_DESCRIPTORS_CHANGED, lambda: (s.unhook_event_loop(event_loop), s.hook_event_loop(event_loop, mock.Mock())), ) with ( mock.patch.object( _win32_raw_display, "ReadInputThread", side_effect=lambda *_a: readers.append(mock.Mock(should_exit=False)) or readers[-1], ), mock.patch.object(s, "_started", True), ): s.hook_event_loop(event_loop, mock.Mock()) s.stop() self.assertTrue(readers) self.assertTrue(all(reader.should_exit for reader in readers)) def test_closed_input_socket_raises(self): """Input from a caller's socket whose other end closed raises instead of spinning.""" input_socket, peer = socket.socketpair() self.addCleanup(input_socket.close) s = _win32_raw_display.Screen(input=input_socket) peer.close() with mock.patch.object(s, "_started", True), self.assertRaises(RuntimeError): s.get_available_raw_input() urwid-urwid-69907e4/tests/test_ansi_parser.py000066400000000000000000000727241526073734300213740ustar00rootroot00000000000000from __future__ import annotations import unittest from urwid.ansi_parser import ( LOGGER, AnsiParser, ParsedLine, SkippedOp, parse_ansi_line, parse_ansi_text, sgi_params_to_attrspec, ) from urwid.display import AttrSpec from urwid.util import rle_len class LineSplittingTest(unittest.TestCase): def test_basic_split(self) -> None: lines = parse_ansi_line("one\ntwo\nthree") self.assertEqual(["one", "two", "three"], [line.text for line in lines]) self.assertEqual(3, len(lines)) def test_crlf_collapses_to_one_split(self) -> None: lines = parse_ansi_line("one\r\ntwo\n\rthree") self.assertEqual(["one", "two", "three"], [line.text for line in lines]) def test_lone_cr_and_lf_are_independent_splits(self) -> None: # two identical line-end characters in a row are two separate # splits, not a collapsed pair (only \r\n / \n\r collapse) lines = parse_ansi_line("a\n\nb") self.assertEqual(["a", "", "b"], [line.text for line in lines]) def test_trailing_unterminated_fragment(self) -> None: lines = parse_ansi_line("a\nb") self.assertEqual(["a", "b"], [line.text for line in lines]) def test_trailing_line_end_yields_final_empty_fragment(self) -> None: # a line end is a structural split point: content after the final # split (even if empty) is still a real, returned line lines = parse_ansi_line("a\n") self.assertEqual(["a", ""], [line.text for line in lines]) def test_empty_input(self) -> None: lines = parse_ansi_line("") self.assertEqual(1, len(lines)) self.assertEqual("", lines[0].text) def test_no_content_lost(self) -> None: original = "abc\ndef\r\nghi\rjkl" lines = parse_ansi_line(original) # every printable character from the input appears somewhere in the # returned lines (only the line-end bytes themselves are consumed # as structural splits, never any other content) self.assertEqual("abcdefghijkl", "".join(line.text for line in lines)) class SgrContinuationTest(unittest.TestCase): def test_continuation_across_calls(self) -> None: first = parse_ansi_line("\x1b[31mred") second = parse_ansi_line("still red", previous_attr=first[-1].last_attr) self.assertIsNotNone(first[-1].last_attr) self.assertEqual(first[-1].last_attr, second[0].attrib[0][0]) def test_continuation_within_single_call(self) -> None: lines = parse_ansi_line("\x1b[31mred\nstill red") self.assertEqual(2, len(lines)) self.assertEqual(lines[0].last_attr, lines[1].last_attr) self.assertEqual(lines[0].last_attr, lines[1].attrib[0][0]) self.assertIsNotNone(lines[0].last_attr) class StreamingFeedTest(unittest.TestCase): def test_chunked_feed_matches_single_call(self) -> None: parser = AnsiParser(one_line=True) parser.feed("ab") parser.feed("c\nd") streamed = parser.finalize() single = parse_ansi_line("abc\nd") self.assertEqual([line.text for line in single], [line.text for line in streamed]) self.assertEqual([line.attrib for line in single], [line.attrib for line in streamed]) def test_line_end_split_across_feed_calls(self) -> None: parser = AnsiParser(one_line=True) parser.feed("one\r") parser.feed("\ntwo") lines = parser.finalize() self.assertEqual(["one", "two"], [line.text for line in lines]) def test_escape_sequence_split_across_feed_calls(self) -> None: parser = AnsiParser(one_line=True) parser.feed("\x1b[3") parser.feed("1mred") lines = parser.finalize() self.assertEqual("red", lines[0].text) self.assertIsNotNone(lines[0].last_attr) class HorizontalMovementTest(unittest.TestCase): def test_right_then_overwrite(self) -> None: # move right past "abc", write "Z" at position 5 (padding gap with # None-attr spaces), leaving "abc" untouched lines = parse_ansi_line("abc\x1b[2CZ") self.assertEqual("abc Z", lines[0].text) def test_left_overwrite(self) -> None: lines = parse_ansi_line("abc\x1b[2DZY") self.assertEqual("aZY", lines[0].text) def test_absolute_column_pads_with_none_attr(self) -> None: lines = parse_ansi_line("\x1b[31mX\x1b[5GY") line = lines[0] self.assertEqual("X Y", line.text) # the gap between the written "X" and the absolute-column "Y" was # never actually printed to, so it carries a None attr, distinct # from a space printed in the current (red) colour -- "Y" itself, # having been actually written, carries the current (red) attrspec expanded: list = [] for attr, run in line.attrib: expanded.extend([attr] * run) self.assertIsNone(expanded[1]) # first gap cell self.assertIsNone(expanded[3]) # last gap cell, just before "Y" self.assertIsNotNone(expanded[4]) # "Y" itself: written, carries current attrspec def test_backspace_is_non_destructive(self) -> None: lines = parse_ansi_line("ab\bC") self.assertEqual("aC", lines[0].text) def test_tab_fills_with_current_attrspec(self) -> None: lines = parse_ansi_line("\x1b[31mA\tB") line = lines[0] self.assertEqual("A B", line.text) expanded: list = [] for attr, run in line.attrib: expanded.extend([attr] * run) # every cell up to and including the tab-filled spaces uses the # current (red) attrspec, unlike a bare cursor move self.assertTrue(all(a is not None for a in expanded[:9])) class StripAndLogTest(unittest.TestCase): def _assert_skip(self, text: str, kind: str, level: str) -> SkippedOp: with self.assertLogs(LOGGER, level="DEBUG") as captured: lines = parse_ansi_line(text) line = lines[0] self.assertTrue(line.skipped, f"expected a SkippedOp for {text!r}") op = line.skipped[0] self.assertEqual(kind, op.kind) self.assertEqual({level}, {record.levelname for record in captured.records}) return op def test_vertical_move_up_down(self) -> None: self._assert_skip("\x1b[2A", "vertical-move", "DEBUG") self._assert_skip("\x1b[2B", "vertical-move", "DEBUG") def test_vertical_move_next_prev_line(self) -> None: self._assert_skip("\x1b[1E", "vertical-move", "DEBUG") self._assert_skip("\x1b[1F", "vertical-move", "DEBUG") def test_vertical_move_absolute_row(self) -> None: self._assert_skip("\x1b[5d", "vertical-move", "DEBUG") def test_vertical_move_row_col(self) -> None: self._assert_skip("\x1b[3;4H", "vertical-move", "DEBUG") self._assert_skip("\x1b[3;4f", "vertical-move", "DEBUG") def test_terminal_setting_modes(self) -> None: self._assert_skip("\x1b[?25h", "terminal-setting", "DEBUG") self._assert_skip("\x1b[?25l", "terminal-setting", "DEBUG") def test_terminal_setting_scroll_region(self) -> None: self._assert_skip("\x1b[1;10r", "terminal-setting", "DEBUG") def test_terminal_setting_erase(self) -> None: self._assert_skip("\x1b[2J", "terminal-setting", "DEBUG") self._assert_skip("\x1b[K", "terminal-setting", "DEBUG") def test_terminal_setting_save_restore_cursor(self) -> None: self._assert_skip("\x1b[s", "terminal-setting", "DEBUG") self._assert_skip("\x1b[u", "terminal-setting", "DEBUG") self._assert_skip("\x1b7", "terminal-setting", "DEBUG") self._assert_skip("\x1b8", "terminal-setting", "DEBUG") def test_terminal_setting_charset(self) -> None: self._assert_skip("\x1b(B", "terminal-setting", "DEBUG") self._assert_skip("\x1b%G", "terminal-setting", "DEBUG") def test_terminal_setting_reset(self) -> None: self._assert_skip("\x1bc", "terminal-setting", "DEBUG") def test_terminal_setting_insert_delete(self) -> None: for final in "@LMPX": self._assert_skip(f"\x1b[1{final}", "terminal-setting", "DEBUG") def test_terminal_setting_device_queries(self) -> None: self._assert_skip("\x1b[c", "terminal-setting", "DEBUG") self._assert_skip("\x1b[5n", "terminal-setting", "DEBUG") def test_terminal_setting_tabstops(self) -> None: self._assert_skip("\x1bH", "terminal-setting", "DEBUG") self._assert_skip("\x1b[g", "terminal-setting", "DEBUG") def test_terminal_setting_osc_palette(self) -> None: self._assert_skip("\x1b]Pnrrggbbx", "terminal-setting", "DEBUG") self._assert_skip("\x1b]R", "terminal-setting", "DEBUG") def test_terminal_setting_unrecognised_osc(self) -> None: self._assert_skip("\x1b]666parsed right?\x1b\\", "terminal-setting", "DEBUG") def test_unknown_csi_logs_debug(self) -> None: op = self._assert_skip("\x1b[9~", "unknown", "DEBUG") self.assertEqual("unknown", op.kind) def test_unknown_escape_logs_debug(self) -> None: self._assert_skip("\x1b|", "unknown", "DEBUG") def test_never_raises(self) -> None: # a grab-bag of recognised and unrecognised sequences must never # raise, regardless of classification text = "\x1b[2A\x1b[?25h\x1b[31m\x1b[2C\x1b]666bad\x1b\\\x1b[9~\x1b|ok" lines = parse_ansi_line(text) self.assertEqual("ok", lines[0].text[-2:]) class SideChannelTest(unittest.TestCase): def test_bel_counted_and_excluded_from_text(self) -> None: lines = parse_ansi_line("a\x07\x07b") self.assertEqual("ab", lines[0].text) self.assertEqual(2, lines[0].bel) def test_bel_resets_per_line(self) -> None: lines = parse_ansi_line("a\x07\nb") self.assertEqual(1, lines[0].bel) self.assertEqual(0, lines[1].bel) def test_title_captured_and_excluded_from_text(self) -> None: lines = parse_ansi_line("\x1b]0;my title\x07rest") self.assertEqual("rest", lines[0].text) self.assertEqual("my title", lines[0].title) def test_title_prefixes(self) -> None: for prefix in (";", "0;", "2;"): lines = parse_ansi_line(f"\x1b]{prefix}hello\x07") self.assertEqual("hello", lines[0].title) def test_title_resets_per_line(self) -> None: lines = parse_ansi_line("\x1b]0;t1\x07a\nb") self.assertEqual("t1", lines[0].title) self.assertIsNone(lines[1].title) def test_title_terminated_by_st(self) -> None: lines = parse_ansi_line("\x1b];stupid title\x1b\\rest") self.assertEqual("stupid title", lines[0].title) self.assertEqual("rest", lines[0].text) def test_leds_captured_and_excluded_from_text(self) -> None: lines = parse_ansi_line("\x1b[3qtest") self.assertEqual("test", lines[0].text) self.assertEqual("caps_lock", lines[0].leds) def test_leds_mode_mapping(self) -> None: mapping = {0: "clear", 1: "scroll_lock", 2: "num_lock", 3: "caps_lock"} for mode, expected in mapping.items(): lines = parse_ansi_line(f"\x1b[{mode}q") self.assertEqual(expected, lines[0].leds) def test_leds_resets_per_line(self) -> None: lines = parse_ansi_line("\x1b[3qa\nb") self.assertEqual("caps_lock", lines[0].leds) self.assertIsNone(lines[1].leds) class SgiParamsToAttrspecTest(unittest.TestCase): def test_basic_foreground(self) -> None: self.assertEqual(AttrSpec("dark red", "default"), sgi_params_to_attrspec([31], None)) def test_basic_background(self) -> None: self.assertEqual(AttrSpec("default", "dark blue"), sgi_params_to_attrspec([44], None)) def test_bright_aixterm_foreground(self) -> None: result = sgi_params_to_attrspec([91], None) self.assertEqual(AttrSpec("light red", "default", colors=16), result) def test_256_colour(self) -> None: result = sgi_params_to_attrspec([38, 5, 200], None) self.assertEqual(AttrSpec("#f0d", "default", colors=256), result) def test_truecolour(self) -> None: result = sgi_params_to_attrspec([38, 2, 10, 20, 30], None) expected = AttrSpec("#0a141e", "default", colors=2**24) self.assertEqual(expected, result) def test_bold_set_and_unset(self) -> None: bold = sgi_params_to_attrspec([1], None) self.assertTrue(bold.bold) unbold = sgi_params_to_attrspec([22], bold) self.assertFalse(unbold.bold if unbold else False) def test_underline_blink_standout(self) -> None: result = sgi_params_to_attrspec([4, 5, 7], None) self.assertTrue(result.underline) self.assertTrue(result.blink) self.assertTrue(result.standout) unset = sgi_params_to_attrspec([24, 25, 27], result) self.assertFalse(unset.underline if unset else False) self.assertFalse(unset.blink if unset else False) self.assertFalse(unset.standout if unset else False) def test_reset(self) -> None: coloured = sgi_params_to_attrspec([31, 1], None) self.assertIsNotNone(coloured) reset = sgi_params_to_attrspec([0], coloured) self.assertIsNone(reset) def test_default_default_is_none(self) -> None: self.assertIsNone(sgi_params_to_attrspec([39, 49], None)) def test_never_raises_on_charset_codes(self) -> None: # SGR 10/11/12 toggle vterm-only charset/display-control state; # sgi_params_to_attrspec must not raise and must simply ignore them result = sgi_params_to_attrspec([10], None) self.assertIsNone(result) result = sgi_params_to_attrspec([31, 11], None) self.assertEqual(AttrSpec("dark red", "default"), result) class ParsedLineDataclassTest(unittest.TestCase): def test_fields_have_expected_defaults(self) -> None: line = ParsedLine(text="", attrib=[], last_attr=None) self.assertEqual(0, line.bel) self.assertIsNone(line.title) self.assertIsNone(line.leds) self.assertEqual([], line.skipped) def test_rle_len_matches_text_length(self) -> None: lines = parse_ansi_line("\x1b[31mred\x1b[0m and plain") for line in lines: self.assertEqual(len(line.text), rle_len(line.attrib)) class AnsiParserScreenModeTest(unittest.TestCase): def test_plain_multiline_roundtrips(self) -> None: result = parse_ansi_text("one\ntwo\nthree") self.assertEqual("one\ntwo\nthree", result.text) def test_empty_input(self) -> None: result = parse_ansi_text("") self.assertEqual("", result.text) def test_bare_cr_overwrites_current_row(self) -> None: # progress-bar-style in-place redraw: a bare \r (not part of a # \r\n / \n\r pair) resolves onto a single row, not two result = parse_ansi_text("progress: 10%\rprogress: 20%") self.assertEqual("progress: 20%", result.text) self.assertNotIn("\n", result.text) def test_crlf_and_lfcr_are_genuine_newlines(self) -> None: result = parse_ansi_text("one\r\ntwo\n\rthree") self.assertEqual("one\ntwo\nthree", result.text) def test_cr_split_across_feed_calls_stays_bare(self) -> None: parser = AnsiParser(one_line=False) parser.feed("progress: 10%\r") parser.feed("progress: 20%") result = parser.finalize()[0] self.assertEqual("progress: 20%", result.text) def test_crlf_split_across_feed_calls_is_still_one_newline(self) -> None: parser = AnsiParser(one_line=False) parser.feed("one\r") parser.feed("\ntwo") result = parser.finalize()[0] self.assertEqual("one\ntwo", result.text) def test_lfcr_split_across_feed_calls_is_still_one_newline(self) -> None: parser = AnsiParser(one_line=False) parser.feed("one\n") parser.feed("\rtwo") result = parser.finalize()[0] self.assertEqual("one\ntwo", result.text) def test_cursor_up_and_down(self) -> None: # write "AB" on row 0, drop to row 1, move back up onto row 0 -- the # column is untouched by CSI A/B, so the write lands right after "AB" result = parse_ansi_text("AB\n12\x1b[1AX") self.assertEqual("ABX\n12", result.text) def test_cursor_up_clamps_at_row_zero(self) -> None: result = parse_ansi_text("\x1b[5AX") self.assertEqual("X", result.text) def test_cursor_down_extends_with_blank_rows(self) -> None: # column untouched by CSI B: "A" leaves the column at 1, so the "B" # lands at column 1 on the extended row, padded with one space result = parse_ansi_text("A\x1b[3BB") self.assertEqual("A\n\n\n B", result.text) def test_cursor_next_line(self) -> None: result = parse_ansi_text("AB\x1b[1EX") self.assertEqual("AB\nX", result.text) def test_cursor_prev_line(self) -> None: result = parse_ansi_text("AB\nCD\x1b[1FX") self.assertEqual("XB\nCD", result.text) def test_cursor_absolute_row(self) -> None: # column untouched by CSI d, same padding reasoning as CSI B above result = parse_ansi_text("A\x1b[3dB") self.assertEqual("A\n\n B", result.text) def test_cursor_position_row_and_column(self) -> None: result = parse_ansi_text("\x1b[2;3HX") self.assertEqual("\n X", result.text) def test_sgr_continuation_across_rows(self) -> None: result = parse_ansi_text("\x1b[31mred\nstill red") self.assertIsNotNone(result.last_attr) expanded: list = [] for attr, run in result.attrib: expanded.extend([attr] * run) # "red" (positions 0-2) and "still red" (after the \n separator) # both carry the same non-None SGR attrspec self.assertEqual(result.last_attr, expanded[0]) self.assertEqual(result.last_attr, expanded[-1]) def test_bel_title_leds_aggregate_across_rows(self) -> None: # the leading \x07 and the one straight after "c" are real BEL # characters; the two used to terminate the OSC title sequences are # not counted as BEL result = parse_ansi_text("\x07a\n\x1b]0;first\x07b\n\x1b[3qc\x07\x1b]0;second\x07d") self.assertEqual(2, result.bel) self.assertEqual("second", result.title) self.assertEqual("caps_lock", result.leds) def test_terminal_setting_still_stripped_and_logged(self) -> None: with self.assertLogs(LOGGER, level="DEBUG"): result = parse_ansi_text("a\x1b[2Jb") self.assertTrue(result.skipped) self.assertEqual("terminal-setting", result.skipped[0].kind) self.assertEqual("ab", result.text) def test_terminal_setting_save_restore_cursor_still_stripped(self) -> None: with self.assertLogs(LOGGER, level="DEBUG"): result = parse_ansi_text("a\x1b[sb") self.assertTrue(result.skipped) self.assertEqual("terminal-setting", result.skipped[0].kind) def test_never_raises(self) -> None: text = "\x1b[2A\x1b[?25h\x1b[31m\x1b[2C\x1b]666bad\x1b\\\x1b[9~\x1b|ok" result = parse_ansi_text(text) self.assertTrue(result.text.endswith("ok")) def test_rle_len_matches_text_length(self) -> None: from urwid.util import rle_len result = parse_ansi_text("\x1b[31mred\nplain\rover\x1b[1Bnext") self.assertEqual(len(result.text), rle_len(result.attrib)) class UntrustedInputBoundsTest(unittest.TestCase): """Escape sequences from untrusted input cannot make the parser allocate without bound or raise. Each input is large enough to exceed the parser's limits, yet small enough to stay cheap if a limit regresses. """ def test_cursor_right_and_absolute_column_are_bounded(self) -> None: """Cap the CSI C step and the CSI G column.""" for text in ("\x1b[100000Cx", "\x1b[100000Gx", "ab\x1b[100000Gx"): with self.subTest(text=text): line = parse_ansi_line(text)[0] self.assertLessEqual(len(line.text), 1025) self.assertTrue(line.text.endswith(" x")) def test_cursor_right_step_is_capped_on_long_lines(self) -> None: """Advance a cursor past a long line by the full step, capped per sequence.""" line = parse_ansi_line("a" * 1500 + "\x1b[3Cy")[0] self.assertTrue(line.text.endswith("a y")) line = parse_ansi_line("a" * 1500 + "\x1b[100000Cy")[0] self.assertEqual(1500 + 1024 + 1, len(line.text)) def test_cursor_down_and_absolute_row_are_bounded(self) -> None: """Cap the CSI B and E steps and the CSI d and H rows and columns.""" for text in ("\x1b[100000Bx", "\x1b[100000Ex", "\x1b[100000dx", "\x1b[100000;100000Hx"): with self.subTest(text=text): rows = parse_ansi_text(text).text.split("\n") self.assertLessEqual(len(rows), 1025) self.assertLessEqual(len(rows[-1]), 1024) self.assertTrue(rows[-1].endswith("x")) def test_cursor_down_step_is_capped_below_many_rows(self) -> None: """Advance a cursor below many newline rows by the full step, capped per sequence.""" result = parse_ansi_text("\n" * 2000 + "\x1b[5Bx") self.assertEqual(2006, len(result.text.split("\n"))) result = parse_ansi_text("\n" * 2000 + "\x1b[100000Bx") self.assertEqual(2000 + 1024 + 1, len(result.text.split("\n"))) def test_overlong_csi_parameter_is_skipped(self) -> None: """Skip a CSI parameter longer than the default 4300-digit limit of int().""" result = parse_ansi_text("a\x1b[" + "9" * 5000 + "mb") self.assertEqual("ab", result.text) self.assertIsNone(result.last_attr) self.assertEqual(["unknown"], [op.kind for op in result.skipped]) def test_unterminated_csi_buffer_is_bounded(self) -> None: """Stop buffering CSI parameters at the length bound.""" parser = AnsiParser() parser.feed("\x1b[" + "1;" * 10000) self.assertLessEqual(len(parser._escbuf), 640) def test_unterminated_osc_buffer_is_bounded(self) -> None: """Stop buffering an OSC string at the length bound.""" parser = AnsiParser() parser.feed("\x1b]0;" + "A" * 100000) self.assertLessEqual(len(parser._escbuf), 10000) def test_overlong_osc_is_skipped_up_to_its_terminator(self) -> None: """Skip an over-long OSC string, discarding it up to either terminator.""" for terminator in ("\x07", "\x1b\\"): with self.subTest(terminator=terminator): result = parse_ansi_text("a\x1b]0;" + "A" * 10000 + terminator + "b") self.assertEqual(["unknown"], [op.kind for op in result.skipped]) self.assertEqual("ab", result.text) self.assertIsNone(result.title) def test_out_of_range_extended_colours_are_ignored(self) -> None: """Ignore a 256-colour index or truecolour component outside 0..255.""" for params in ([38, 5, 99999999], [48, 5, 256], [38, 2, 999, 999, 999], [48, 2, 0, 0, 256]): with self.subTest(params=params): self.assertIsNone(sgi_params_to_attrspec(params, None)) # the colour's own parameters are consumed, the rest still applies self.assertEqual(AttrSpec("dark red", "default"), sgi_params_to_attrspec([*params, 31], None)) def test_out_of_range_colour_through_parser(self) -> None: """Ignore out-of-range extended colours in parsed text.""" result = parse_ansi_line("\x1b[38;5;99999999mX\x1b[38;2;999;999;999mY")[0] self.assertEqual("XY", result.text) self.assertIsNone(result.last_attr) def test_skipped_list_is_capped(self) -> None: """Record only the first 65536 skipped sequences, in both modes.""" text = "\x1b[2J" + "\x1b[K" * 70000 for one_line in (True, False): with self.subTest(one_line=one_line): parser = AnsiParser(one_line=one_line) parser.feed(text) skipped = parser.finalize()[0].skipped self.assertEqual(65536, len(skipped)) self.assertEqual(["\x1b[2J", "\x1b[K"], [op.raw for op in skipped[:2]]) def test_skipped_cap_is_per_line_in_one_line_mode(self) -> None: """Start each line with an empty skipped list, so a full line does not cap the next one.""" parser = AnsiParser(one_line=True) parser.feed("\x1b[K" * 70000 + "\n\x1b[K") lines = parser.finalize() self.assertEqual([65536, 1], [len(line.skipped) for line in lines]) def test_column_padding_is_bounded(self) -> None: """Limit the cells padded to reach moved columns to 1024 * 1024 plus 8 per input character.""" for unit in ("\n\x1b[1024Gx", "\x1b[1024Cx"): text = unit * 3000 for one_line in (True, False): with self.subTest(unit=unit, one_line=one_line): parser = AnsiParser(one_line=one_line) parser.feed(text) lines = parser.finalize() cells = sum(len(line.text) - line.text.count("\n") for line in lines) self.assertLessEqual(cells, 1024 * 1024 + 8 * len(text) + 3000) skipped = [op for line in lines for op in line.skipped if op.raw == unit[-8:-1]] self.assertTrue(skipped) def test_row_padding_is_bounded(self) -> None: """Limit the empty rows vertical moves insert, each counted as 8 cells.""" text = "\x1b[1024Bx" * 500 result = parse_ansi_text(text) rows = result.text.split("\n") self.assertLessEqual(8 * len(rows) + sum(map(len, rows)), 1024 * 1024 + 8 * len(text) + 500) self.assertEqual(500, "".join(rows).count("x")) self.assertIn("\x1b[1024B", [op.raw for op in result.skipped]) def test_padding_cut_is_recorded_against_the_column_move(self) -> None: """Record a cut against the column move, not a vertical move one_line mode skips.""" line = parse_ansi_line("\x1b[1024C" * 1100 + "\x1b[5Ax")[0] cut = [op.raw for op in line.skipped if op.reason.startswith("padding limit")] self.assertEqual(["\x1b[1024C"], cut) def test_padding_budget_spans_feed_calls(self) -> None: """Grow the padding budget with every feed call, up to the same limit as one call with all the text.""" text = "\n\x1b[1024Gx" * 3000 parser = AnsiParser() for start in range(0, len(text), 7): parser.feed(text[start : start + 7]) result = parser.finalize()[0].text cells = len(result) - result.count("\n") self.assertLessEqual(cells, 1024 * 1024 + 8 * len(text) + 3000) self.assertGreater(cells, 1024 * 1024 + 3000) def test_sparse_full_screen_drawing_is_kept(self) -> None: """Keep the padding of a sparse drawing far below the padding limit.""" result = parse_ansi_text("\x1b[1;1Htop\x1b[60;200Hmid\x1b[1000;1000Hend") expected = ["top"] + [""] * 58 + [" " * 199 + "mid"] + [""] * 939 + [" " * 999 + "end"] self.assertEqual(expected, result.text.split("\n")) self.assertEqual([], result.skipped) class OscTerminationTest(unittest.TestCase): """An ESC inside an OSC string either starts the ST terminator or ends the string and starts a new sequence.""" def _parse_chunked(self, chunks: list[str], one_line: bool) -> ParsedLine: parser = AnsiParser(one_line=one_line) for chunk in chunks: parser.feed(chunk) return parser.finalize()[0] def test_unterminated_osc_ends_at_escape(self) -> None: """End an OSC string at an ESC not followed by a backslash, unapplied, and parse what follows.""" text = "a\x1b]0;title\x1b[31mred text\x07b" red = sgi_params_to_attrspec([31], None) for one_line in (True, False): for split in range(len(text) + 1): with self.subTest(one_line=one_line, split=split): line = self._parse_chunked([text[:split], text[split:]], one_line) self.assertEqual("ared textb", line.text) self.assertEqual([(None, 1), (red, 9)], line.attrib) self.assertIsNone(line.title) self.assertEqual(1, line.bel) self.assertEqual([("unknown", "\x1b]0;title")], [(op.kind, op.raw) for op in line.skipped]) def test_osc_terminated_by_st_across_feed_calls(self) -> None: """Apply an OSC title whose ESC backslash terminator is split across two feed calls.""" for one_line in (True, False): with self.subTest(one_line=one_line): line = self._parse_chunked(["\x1b]0;title\x1b", "\\rest"], one_line) self.assertEqual("title", line.title) self.assertEqual("rest", line.text) def test_title_never_contains_escape(self) -> None: """Take the title from the OSC string an ESC starts, not from the unterminated one before it.""" for one_line in (True, False): with self.subTest(one_line=one_line): line = self._parse_chunked(["\x1b]0;ti\x1b", "]0;tle\x07x"], one_line) self.assertEqual("tle", line.title) self.assertEqual("x", line.text) def test_overlong_osc_discard_ends_at_escape(self) -> None: """End the discarded remainder of an over-long OSC string at an ESC not followed by a backslash.""" red = sgi_params_to_attrspec([31], None) for one_line in (True, False): with self.subTest(one_line=one_line): line = self._parse_chunked(["a\x1b]0;" + "A" * 5000 + "\x1b", "[31mb"], one_line) self.assertEqual("ab", line.text) self.assertEqual([(None, 1), (red, 1)], line.attrib) self.assertIsNone(line.title) if __name__ == "__main__": unittest.main() urwid-urwid-69907e4/tests/test_canvas.py000066400000000000000000001455421526073734300203400ustar00rootroot00000000000000from __future__ import annotations import gc import sys import traceback import unittest import weakref from unittest import mock import urwid from tests.util import collect_and_count_alive from urwid import canvas from urwid.util import get_encoding class CanvasCacheTest(unittest.TestCase): def setUp(self): # purge the cache urwid.CanvasCache._widgets.clear() def cct(self, widget, size, focus, expected): with self.subTest(widget=widget, size=size, focus=focus, expected=expected): got = urwid.CanvasCache.fetch(widget, urwid.Widget, size, focus) self.assertEqual(expected, got, f"got: {got} expected: {expected}") def test1(self): a = urwid.Text("") b = urwid.Text("") blah = urwid.TextCanvas() blah.finalize(a, (10, 1), False) blah2 = urwid.TextCanvas() blah2.finalize(a, (15, 1), False) bloo = urwid.TextCanvas() bloo.finalize(b, (20, 2), True) urwid.CanvasCache.store(urwid.Widget, blah) urwid.CanvasCache.store(urwid.Widget, blah2) urwid.CanvasCache.store(urwid.Widget, bloo) self.cct(a, (10, 1), False, blah) self.cct(a, (15, 1), False, blah2) self.cct(a, (15, 1), True, None) self.cct(a, (10, 2), False, None) self.cct(b, (20, 2), True, bloo) self.cct(b, (21, 2), True, None) urwid.CanvasCache.invalidate(a) self.cct(a, (10, 1), False, None) self.cct(a, (15, 1), False, None) self.cct(b, (20, 2), True, bloo) def test_deps_does_not_grow_unbounded_on_repeated_store(self): # A widget that is re-rendered many times (e.g. one screen refresh # per store() call) used to add itself to CanvasCache._deps[w] every # single time, so the list kept growing for as long as the program # ran, even though it only ever depends on `w` once. dependency = urwid.Text("") dependent = urwid.Text("") dependency_canv = urwid.TextCanvas() dependency_canv.finalize(dependency, (10, 1), False) urwid.CanvasCache.store(urwid.Widget, dependency_canv) for _ in range(50): canv = urwid.TextCanvas() canv.finalize(dependent, (10, 1), False) canv.depends_on = [dependency] urwid.CanvasCache.store(urwid.Widget, canv) self.assertEqual({dependent}, urwid.CanvasCache._deps[dependency]) def test_store_without_widget_info_raises(self): """A canvas has to be finalized (carry widget_info) before it can be cached.""" unfinalized = urwid.TextCanvas() with self.assertRaises(TypeError): urwid.CanvasCache.store(urwid.Widget, unfinalized) @unittest.skipUnless( sys.implementation.name == "cpython", "relies on reference counting freeing the canvas as soon as its last reference is dropped", ) def test_fetch_of_a_dead_weakref_returns_none_without_counting_a_hit(self): """A cache entry whose canvas is already gone is a miss, not a hit.""" widget = urwid.Text("") key = (urwid.Widget, (10, 1), False) # A weakref with no callback: unlike store()'s, it will not clean up # after itself, so the dead entry stays in place for fetch() to find. placeholder = urwid.TextCanvas() urwid.CanvasCache._widgets[widget] = {key: weakref.ref(placeholder)} del placeholder hits_before = urwid.CanvasCache.hits fetches_before = urwid.CanvasCache.fetches result = urwid.CanvasCache.fetch(widget, urwid.Widget, (10, 1), False) self.assertIsNone(result) self.assertEqual(hits_before, urwid.CanvasCache.hits) self.assertEqual(fetches_before + 1, urwid.CanvasCache.fetches) def test_cleanup_ignores_a_ref_already_removed_by_invalidate(self): """invalidate() can drop a ref before its canvas is actually collected; cleanup() must not raise.""" widget = urwid.Text("") canv = urwid.TextCanvas() canv.finalize(widget, (10, 1), False) urwid.CanvasCache.store(urwid.Widget, canv) ref = urwid.CanvasCache._widgets[widget][(urwid.Widget, (10, 1), False)] urwid.CanvasCache.invalidate(widget) self.assertNotIn(ref, urwid.CanvasCache._refs) cleanups_before = urwid.CanvasCache.cleanups urwid.CanvasCache.cleanup(ref) # must be a no-op, not a KeyError self.assertEqual(cleanups_before + 1, urwid.CanvasCache.cleanups) def test_cleanup_ignores_a_ref_whose_widget_is_already_gone(self): """A ref can outlive its widget's entry in _widgets; cleanup() must tolerate that too.""" widget = urwid.Text("") canv = urwid.TextCanvas() canv.finalize(widget, (10, 1), False) ref = weakref.ref(canv, urwid.CanvasCache.cleanup) urwid.CanvasCache._refs[ref] = (widget, urwid.Widget, (10, 1), False) # deliberately not registering `widget` in _widgets cleanups_before = urwid.CanvasCache.cleanups urwid.CanvasCache.cleanup(ref) self.assertEqual(cleanups_before + 1, urwid.CanvasCache.cleanups) self.assertNotIn(ref, urwid.CanvasCache._refs) def test_clear_empties_all_bookkeeping(self): widget = urwid.Text("") canv = urwid.TextCanvas() canv.finalize(widget, (10, 1), False) urwid.CanvasCache.store(urwid.Widget, canv) self.assertTrue(urwid.CanvasCache._widgets) self.assertTrue(urwid.CanvasCache._refs) urwid.CanvasCache.clear() self.assertEqual({}, urwid.CanvasCache._widgets) self.assertEqual({}, urwid.CanvasCache._refs) self.assertEqual({}, urwid.CanvasCache._deps) self.assertEqual({}, urwid.CanvasCache._children) class CanvasCacheDependencyLeakTest(unittest.TestCase): """CanvasCache must not keep a dependent widget alive once none of its canvases is cached.""" def setUp(self): """Start from an empty cache and leave one behind.""" urwid.CanvasCache.clear() self.addCleanup(urwid.CanvasCache.clear) self.alive = [] # canvases a screen would still hold; the cache only keeps weak references def store(self, widget, size=(10, 1), depends_on=None): """Cache a canvas for *widget* and keep it alive for the rest of the test.""" canv = urwid.TextCanvas() self.alive.append(canv) canv.finalize(widget, size, False) if depends_on is not None: canv.depends_on = depends_on urwid.CanvasCache.store(urwid.Widget, canv) return canv def test_rebuilt_wrappers_around_a_persistent_child_are_released(self): """Wrappers rebuilt on every refresh around one cached child must not pile up in its dependants.""" child = urwid.Text("static") paddings = [] last_canvas = None # the screen keeps only the last canvas it drew for _ in range(200): padding = urwid.Padding(child) last_canvas = urwid.AttrMap(padding, None).render((20,)) paddings.append(weakref.ref(padding)) del padding self.assertEqual(1, collect_and_count_alive(paddings)) self.assertEqual({paddings[-1]()}, urwid.CanvasCache._deps[child]) self.assertLessEqual(len(urwid.CanvasCache._children), 2) # the last Padding and AttrMap self.assertIsNotNone(last_canvas) def test_gridflow_focus_changes_release_old_display_widgets(self): """GridFlow rebuilds its display widgets on a focus change: the old ones must be released.""" grid = urwid.GridFlow([urwid.Button(str(i)) for i in range(9)], 8, 1, 0, "left") size = (30,) canv = grid.render(size, focus=True) gc.collect() links_after_first_render = sum(map(len, urwid.CanvasCache._deps.values())) displays = [] for i in range(100): grid.keypress(size, "right" if i % 2 == 0 else "left") canv = grid.render(size, focus=True) displays.append(weakref.ref(grid._w)) self.assertEqual(1, collect_and_count_alive(displays)) self.assertEqual(links_after_first_render, sum(map(len, urwid.CanvasCache._deps.values()))) self.assertIsNotNone(canv) def test_invalidate_removes_the_widget_from_its_children_deps(self): """An invalidated widget must not stay among the dependants of the widgets it depended on.""" child = urwid.Text("") parent = urwid.Text("") child_canv = self.store(child) self.store(parent, depends_on=[child]) self.assertEqual({parent}, urwid.CanvasCache._deps[child]) urwid.CanvasCache.invalidate(parent) self.assertNotIn(child, urwid.CanvasCache._deps) self.assertEqual({}, urwid.CanvasCache._children) self.assertIs(child_canv, urwid.CanvasCache.fetch(child, urwid.Widget, (10, 1), False)) def test_cleanup_of_a_replaced_ref_keeps_the_live_entry(self): """A canvas stored again under the same key must stay cached when the canvas it replaced dies.""" child = urwid.Text("") parent = urwid.Text("") self.store(child) self.store(parent, depends_on=[child]) old_ref = urwid.CanvasCache._widgets[parent][urwid.Widget, (10, 1), False] new_canv = self.store(parent, depends_on=[child]) urwid.CanvasCache.cleanup(old_ref) self.assertIs(new_canv, urwid.CanvasCache.fetch(parent, urwid.Widget, (10, 1), False)) self.assertEqual({parent}, urwid.CanvasCache._deps[child]) def test_cleanup_between_dependency_lookup_and_add_in_store(self): """A cleanup that drops a now-empty dependants set while store() holds it must not lose the new link.""" child = urwid.Text("") old_parent = urwid.Text("") parent = urwid.Text("") self.store(child) self.store(old_parent, depends_on=[child]) old_ref = urwid.CanvasCache._widgets[old_parent][urwid.Widget, (10, 1), False] class CleanupAfterSetdefault(dict): """Run a cleanup right after every setdefault() lookup.""" def setdefault(self, key, default=None): found = super().setdefault(key, default) urwid.CanvasCache.cleanup(old_ref) # as if the old parent's canvas died right here return found urwid.CanvasCache._deps = CleanupAfterSetdefault(urwid.CanvasCache._deps) self.store(parent, depends_on=[child]) self.assertEqual({parent}, urwid.CanvasCache._deps[child]) urwid.CanvasCache.invalidate(child) self.assertIsNone(urwid.CanvasCache.fetch(parent, urwid.Widget, (10, 1), False)) def test_cleanup_emptying_the_sizes_store_is_adding_to(self): """A canvas that store() could not register must not leave its widget in the dependency indexes.""" child = urwid.Text("") parent = urwid.Text("") self.store(child) self.store(parent, size=(5, 1), depends_on=[child]) old_ref = urwid.CanvasCache._widgets[parent][urwid.Widget, (5, 1), False] lookups = [] class CleanupAfterFirstGet(dict): """Run a cleanup right after the first get() lookup of the parent.""" def get(self, key, default=None): found = super().get(key, default) if key is parent and not lookups: lookups.append(key) urwid.CanvasCache.cleanup(old_ref) # empties and drops the sizes just returned return found urwid.CanvasCache._widgets = CleanupAfterFirstGet(urwid.CanvasCache._widgets) canv = self.store(parent, depends_on=[child]) self.assertNotIn(parent, urwid.CanvasCache._widgets) self.assertNotIn(parent, urwid.CanvasCache._children) self.assertNotIn(child, urwid.CanvasCache._deps) self.assertIsNotNone(canv) def test_collected_child_is_released_by_a_dependant_cached_under_another_size(self): """A child whose last canvas is gone must not stay linked from a dependant that is still cached.""" parent = urwid.Text("") kept_child = urwid.Text("") self.store(kept_child) self.store(parent, depends_on=[kept_child]) # the canvas the screen keeps children = [] for _ in range(100): child = urwid.Text("") children.append(weakref.ref(child)) child_canv = urwid.TextCanvas() child_canv.finalize(child, (20, 1), False) urwid.CanvasCache.store(urwid.Widget, child_canv) parent_canv = urwid.TextCanvas() parent_canv.finalize(parent, (20, 1), False) parent_canv.depends_on = [child] urwid.CanvasCache.store(urwid.Widget, parent_canv) # a measuring render, discarded right away del child, child_canv, parent_canv self.assertEqual(0, collect_and_count_alive(children)) self.assertEqual({kept_child}, urwid.CanvasCache._children[parent]) def test_cleanup_during_invalidate_recursion(self): """A cleanup running while invalidate() walks the dependants must not change a set being iterated.""" child = urwid.Text("") parents = [urwid.Text(str(i)) for i in range(5)] self.store(child) for parent in parents: self.store(parent, depends_on=[child]) bystander = urwid.Text("") self.store(bystander, depends_on=[child, parents[0]]) bystander_ref = urwid.CanvasCache._widgets[bystander][urwid.Widget, (10, 1), False] invalidate = urwid.CanvasCache.invalidate.__func__ calls = [] def invalidate_with_cleanup(cls, widget): calls.append(widget) if len(calls) == 2: cls.cleanup(bystander_ref) # as if the bystander's canvas died mid-walk invalidate(cls, widget) with mock.patch.object(urwid.CanvasCache, "invalidate", classmethod(invalidate_with_cleanup)): urwid.CanvasCache.invalidate(child) self.assertEqual({}, urwid.CanvasCache._widgets) self.assertEqual({}, urwid.CanvasCache._deps) self.assertEqual({}, urwid.CanvasCache._children) class CanvasTest(unittest.TestCase): def test_basic_info(self): """Test str and repr methods for debugging purposes.""" string = "Hello World!" rendered = urwid.Text(string).render(()) self.assertEqual(string, str(rendered)) self.assertEqual( f"", repr(rendered), ) def test_composite_basic_info(self): """Composite canvas contain info about canvas inside. Use canvas caching feature for test. """ string = "Hello World!" widget = urwid.Text(string) rendered_widget = widget.render(()) disabled = urwid.WidgetDisable(widget) rendered = disabled.render(()) self.assertEqual( f"", repr(rendered), ) def test_set_pop_up_argument_validation(self): """Out of contract pop-up parameters are refused instead of mis-placing the pop-up.""" widget = urwid.SolidFill("*") for description, kwargs, message in ( ("negative left", {"left": -1, "top": 0}, "Pop-up position must not be negative"), ("negative top", {"left": 0, "top": -1}, "Pop-up position must not be negative"), ("zero width", {"overlay_width": 0}, "Pop-up size must be positive"), ("zero height", {"overlay_height": 0}, "Pop-up size must be positive"), ("negative width", {"overlay_width": -3}, "Pop-up size must be positive"), ("negative height", {"overlay_height": -3}, "Pop-up size must be positive"), ): with self.subTest(description): params = {"left": 0, "top": 0, "overlay_width": 4, "overlay_height": 2, **kwargs} canvas = urwid.CompositeCanvas(urwid.SolidCanvas(" ", 10, 5)) with self.assertRaises(urwid.CanvasError) as ctx: canvas.set_pop_up(widget, **params) self.assertIn(message, str(ctx.exception)) with self.subTest("in contract parameters are accepted"): canvas = urwid.CompositeCanvas(urwid.SolidCanvas(" ", 10, 5)) canvas.set_pop_up(widget, 0, 0, 4, 2) self.assertEqual((0, 0, (widget, 4, 2)), canvas.get_pop_up()) def test_finalize_twice_raises(self): widget = urwid.Text("") canv = urwid.TextCanvas() canv.finalize(widget, (10, 1), False) with self.assertRaises(urwid.CanvasError): canv.finalize(widget, (10, 1), False) def test_set_cursor_on_finalized_cacheable_canvas_raises(self): widget = urwid.Text("") canv = urwid.TextCanvas() canv.finalize(widget, (10, 1), False) with self.assertRaises(urwid.CanvasError): canv.cursor = (0, 0) def test_set_pop_up_on_finalized_cacheable_canvas_raises(self): widget = urwid.SolidFill("*") canv = urwid.CompositeCanvas(urwid.SolidCanvas(" ", 10, 5)) canv.finalize(widget, (10, 5), False) with self.assertRaises(urwid.CanvasError): canv.set_pop_up(widget, 0, 0, 4, 2) def ct(self, text, attr, exp_content): with self.subTest(text=text, attr=attr, exp_content=exp_content): c = urwid.TextCanvas([t.encode("iso8859-1") for t in text], attr) content = list(c.content()) self.assertEqual(content, exp_content, f"got: {content!r} expected: {exp_content!r}") def ct2(self, text, attr, left, top, cols, rows, def_attr, exp_content): c = urwid.TextCanvas([t.encode("iso8859-1") for t in text], attr) content = list(c.content(left, top, cols, rows, def_attr)) self.assertEqual(content, exp_content, f"got: {content!r} expected: {exp_content!r}") def test1(self): self.ct(["Hello world"], None, [[(None, None, b"Hello world")]]) self.ct(["Hello world"], [[("a", 5)]], [[("a", None, b"Hello"), (None, None, b" world")]]) self.ct(["Hi", "There"], None, [[(None, None, b"Hi ")], [(None, None, b"There")]]) def test2(self): self.ct2( ["Hello"], None, 0, 0, 5, 1, None, [[(None, None, b"Hello")]], ) self.ct2( ["Hello"], None, 1, 0, 4, 1, None, [[(None, None, b"ello")]], ) self.ct2( ["Hello"], None, 0, 0, 4, 1, None, [[(None, None, b"Hell")]], ) self.ct2( ["Hi", "There"], None, 1, 0, 3, 2, None, [[(None, None, b"i ")], [(None, None, b"her")]], ) self.ct2( ["Hi", "There"], None, 0, 0, 5, 1, None, [[(None, None, b"Hi ")]], ) self.ct2( ["Hi", "There"], None, 0, 1, 5, 1, None, [[(None, None, b"There")]], ) class ZeroColumnTextCanvasTest(unittest.TestCase): def test_empty_text_renders_without_size(self): self.assertEqual([b""], urwid.Text("").render(()).text) def test_content_of_zero_column_canvas(self): canvas = urwid.Text("").render((0,)) self.assertEqual([[]], list(canvas.content())) self.assertEqual([[]], list(urwid.CompositeCanvas(canvas).content())) def test_trim_outside_zero_column_canvas_raises(self): canvas = urwid.Text("").render((0,)) with self.assertRaises(ValueError): list(canvas.content(trim_left=1)) class FinalizedCanvasErrorTest(unittest.TestCase): """Errors raised on misuse of a finalized canvas.""" def test_repeated_misuse_does_not_keep_the_canvas_alive(self): """Each misuse of a finalized canvas raises its own error, so no traceback outlives its handler.""" canv = urwid.TextCanvas([b"hi"]) canv.finalize(urwid.Text("hi"), (2,), False) tracebacks = [] for _ in range(5): # Not assertRaises: it strips the traceback, which hides the accumulation. try: canv.finalize(urwid.Text("hi"), (2,), False) except urwid.CanvasError as exc: # noqa: PERF203 # the repeated raise is what is under test tracebacks.append(len(traceback.extract_tb(exc.__traceback__))) canv_ref = weakref.ref(canv) del canv gc.collect() self.assertEqual([tracebacks[0]] * 5, tracebacks) self.assertIsNone(canv_ref()) class TextCanvasErrorTest(unittest.TestCase): def test_text_must_be_bytes(self): with self.assertRaises(urwid.CanvasError): urwid.TextCanvas(["not bytes"]) def test_check_width_false_requires_int_maxcol(self): with self.assertRaises(TypeError): urwid.TextCanvas([b"hi"], check_width=False, maxcol=None) def test_text_wider_than_maxcol_raises(self): with self.assertRaises(urwid.CanvasError): urwid.TextCanvas([b"hello"], maxcol=3) def test_attribute_extending_beyond_text_raises(self): with self.assertRaises(urwid.CanvasError): urwid.TextCanvas([b"hi"], attr=[[(None, 5)]]) def test_character_set_extending_beyond_text_raises(self): with self.assertRaises(urwid.CanvasError): urwid.TextCanvas([b"hi"], cs=[[(None, 5)]]) def test_translated_coords_with_and_without_cursor(self): with_cursor = urwid.TextCanvas([b"hi"], cursor=(1, 0)) self.assertEqual((4, 3), with_cursor.translated_coords(3, 3)) without_cursor = urwid.TextCanvas([b"hi"]) self.assertIsNone(without_cursor.translated_coords(3, 3)) def test_content_trim_top_out_of_range_raises(self): canv = urwid.TextCanvas([b"a", b"b"]) with self.assertRaises(ValueError): list(canv.content(trim_top=5)) class BlankCanvasTest(unittest.TestCase): def test_cols_and_rows_are_unknown(self): blank = urwid.BlankCanvas() with self.assertRaises(NotImplementedError): blank.cols() with self.assertRaises(NotImplementedError): blank.rows() def test_content_uses_the_default_attribute_when_given(self): blank = urwid.BlankCanvas() self.assertEqual([[(None, None, b" ")]], list(blank.content(cols=3, rows=1))) self.assertEqual([[("a", None, b" ")]], list(blank.content(cols=3, rows=1, attr={None: "a"}))) def test_content_with_zero_rows_yields_nothing(self): blank = urwid.BlankCanvas() self.assertEqual([], list(blank.content(cols=3, rows=0))) class SolidCanvasErrorTest(unittest.TestCase): def test_fill_char_not_exactly_one_column_wide_raises(self): with self.subTest("empty"): with self.assertRaises(ValueError): urwid.SolidCanvas("", 3, 1) with self.subTest("double-width"): with self.assertRaises(ValueError): urwid.SolidCanvas("\N{HIRAGANA LETTER A}", 3, 1) class ShardBodyTest(unittest.TestCase): def sbt(self, shards, shard_tail, expected): result = canvas.shard_body(shards, shard_tail, False) assert result == expected, f"got: {result!r} expected: {expected!r}" def sbttail(self, num_rows, sbody, expected): result = canvas.shard_body_tail(num_rows, sbody) assert result == expected, f"got: {result!r} expected: {expected!r}" def sbtrow(self, sbody, expected): result = list(canvas.shard_body_row(sbody)) assert result == expected, f"got: {result!r} expected: {expected!r}" def test1(self): cviews = [(0, 0, 10, 5, None, "foo"), (0, 0, 5, 5, None, "bar")] self.sbt( cviews, [], [(0, None, (0, 0, 10, 5, None, "foo")), (0, None, (0, 0, 5, 5, None, "bar"))], ) self.sbt( cviews, [(0, 3, None, (0, 0, 5, 8, None, "baz"))], [ (3, None, (0, 0, 5, 8, None, "baz")), (0, None, (0, 0, 10, 5, None, "foo")), (0, None, (0, 0, 5, 5, None, "bar")), ], ) self.sbt( cviews, [(10, 3, None, (0, 0, 5, 8, None, "baz"))], [ (0, None, (0, 0, 10, 5, None, "foo")), (3, None, (0, 0, 5, 8, None, "baz")), (0, None, (0, 0, 5, 5, None, "bar")), ], ) self.sbt( cviews, [(15, 3, None, (0, 0, 5, 8, None, "baz"))], [ (0, None, (0, 0, 10, 5, None, "foo")), (0, None, (0, 0, 5, 5, None, "bar")), (3, None, (0, 0, 5, 8, None, "baz")), ], ) def test_underfilled_gap_is_padded(self): # cviews are short 4 columns of what the shard tail's gap expects: rather than # raising or silently truncating the row, the missing width is padded with a # blank filler cview so the resulting shard stays rectangular. # Regression test for https://github.com/urwid/urwid/issues/340 cviews = [(0, 0, 6, 5, None, "foo")] result = canvas.shard_body( cviews, [(10, 3, None, (0, 0, 5, 8, None, "baz"))], False, num_rows=5, ) assert result == [ (0, None, (0, 0, 6, 5, None, "foo")), (0, None, (0, 0, 4, 5, None, canvas.blank_canvas)), (3, None, (0, 0, 5, 8, None, "baz")), ] def test2(self): sbody = [ (0, None, (0, 0, 10, 5, None, "foo")), (0, None, (0, 0, 5, 5, None, "bar")), (3, None, (0, 0, 5, 8, None, "baz")), ] self.sbttail(5, sbody, []) self.sbttail( 3, sbody, [ (0, 3, None, (0, 0, 10, 5, None, "foo")), (0, 3, None, (0, 0, 5, 5, None, "bar")), (0, 6, None, (0, 0, 5, 8, None, "baz")), ], ) sbody = [ (0, None, (0, 0, 10, 3, None, "foo")), (0, None, (0, 0, 5, 5, None, "bar")), (3, None, (0, 0, 5, 9, None, "baz")), ] self.sbttail( 3, sbody, [(10, 3, None, (0, 0, 5, 5, None, "bar")), (0, 6, None, (0, 0, 5, 9, None, "baz"))], ) def test3(self): self.sbtrow( [ (0, iter("foo"), (0, 0, 10, 5, None, "foo")), (0, iter("bar"), (0, 0, 5, 5, None, "bar")), (3, iter("zzz"), (0, 0, 5, 8, None, "baz")), ], ["f", "b", "z"], ) def test_row_without_a_content_iterator_raises(self): with self.assertRaises(ValueError): list(canvas.shard_body_row([(0, None, (0, 0, 5, 5, None, "foo"))])) def test_cviews_wider_than_the_shard_tail_gap_raises(self): # the cview is 10 columns wide but the shard tail only leaves a 5 column gap for it cviews = [(0, 0, 10, 5, None, "foo")] shard_tail = [(5, 0, None, (0, 0, 5, 5, None, "bar"))] with self.assertRaises(canvas.CanvasError): canvas.shard_body(cviews, shard_tail, False) class ShardsTrimTest(unittest.TestCase): def sttop(self, shards, top, expected): result = canvas.shards_trim_top(shards, top) assert result == expected, f"got: {result!r} expected: {expected!r}" def strows(self, shards, rows, expected): result = canvas.shards_trim_rows(shards, rows) assert result == expected, f"got: {result!r} expected: {expected!r}" def stsides(self, shards, left, cols, expected): result = canvas.shards_trim_sides(shards, left, cols) assert result == expected, f"got: {result!r} expected: {expected!r}" def test1(self): shards = [(5, [(0, 0, 10, 5, None, "foo"), (0, 0, 5, 5, None, "bar")])] self.sttop(shards, 2, [(3, [(0, 2, 10, 3, None, "foo"), (0, 2, 5, 3, None, "bar")])]) self.strows(shards, 2, [(2, [(0, 0, 10, 2, None, "foo"), (0, 0, 5, 2, None, "bar")])]) shards = [(5, [(0, 0, 10, 5, None, "foo")]), (3, [(0, 0, 10, 3, None, "bar")])] self.sttop(shards, 2, [(3, [(0, 2, 10, 3, None, "foo")]), (3, [(0, 0, 10, 3, None, "bar")])]) self.sttop(shards, 5, [(3, [(0, 0, 10, 3, None, "bar")])]) self.sttop(shards, 7, [(1, [(0, 2, 10, 1, None, "bar")])]) self.strows(shards, 7, [(5, [(0, 0, 10, 5, None, "foo")]), (2, [(0, 0, 10, 2, None, "bar")])]) self.strows(shards, 5, [(5, [(0, 0, 10, 5, None, "foo")])]) self.strows(shards, 4, [(4, [(0, 0, 10, 4, None, "foo")])]) shards = [(5, [(0, 0, 10, 5, None, "foo"), (0, 0, 5, 8, None, "baz")]), (3, [(0, 0, 10, 3, None, "bar")])] self.sttop( shards, 2, [ (3, [(0, 2, 10, 3, None, "foo"), (0, 2, 5, 6, None, "baz")]), (3, [(0, 0, 10, 3, None, "bar")]), ], ) self.sttop(shards, 5, [(3, [(0, 0, 10, 3, None, "bar"), (0, 5, 5, 3, None, "baz")])]) self.sttop(shards, 7, [(1, [(0, 2, 10, 1, None, "bar"), (0, 7, 5, 1, None, "baz")])]) self.strows( shards, 7, [ (5, [(0, 0, 10, 5, None, "foo"), (0, 0, 5, 7, None, "baz")]), (2, [(0, 0, 10, 2, None, "bar")]), ], ) self.strows(shards, 5, [(5, [(0, 0, 10, 5, None, "foo"), (0, 0, 5, 5, None, "baz")])]) self.strows(shards, 4, [(4, [(0, 0, 10, 4, None, "foo"), (0, 0, 5, 4, None, "baz")])]) def test2(self): shards = [(5, [(0, 0, 10, 5, None, "foo"), (0, 0, 5, 5, None, "bar")])] self.stsides(shards, 0, 15, [(5, [(0, 0, 10, 5, None, "foo"), (0, 0, 5, 5, None, "bar")])]) self.stsides(shards, 6, 9, [(5, [(6, 0, 4, 5, None, "foo"), (0, 0, 5, 5, None, "bar")])]) self.stsides(shards, 6, 6, [(5, [(6, 0, 4, 5, None, "foo"), (0, 0, 2, 5, None, "bar")])]) self.stsides(shards, 0, 10, [(5, [(0, 0, 10, 5, None, "foo")])]) self.stsides(shards, 10, 5, [(5, [(0, 0, 5, 5, None, "bar")])]) self.stsides(shards, 1, 7, [(5, [(1, 0, 7, 5, None, "foo")])]) shards = [(5, [(0, 0, 10, 5, None, "foo"), (0, 0, 5, 8, None, "baz")]), (3, [(0, 0, 10, 3, None, "bar")])] self.stsides( shards, 0, 15, [(5, [(0, 0, 10, 5, None, "foo"), (0, 0, 5, 8, None, "baz")]), (3, [(0, 0, 10, 3, None, "bar")])], ) self.stsides( shards, 2, 13, [(5, [(2, 0, 8, 5, None, "foo"), (0, 0, 5, 8, None, "baz")]), (3, [(2, 0, 8, 3, None, "bar")])], ) self.stsides( shards, 2, 10, [(5, [(2, 0, 8, 5, None, "foo"), (0, 0, 2, 8, None, "baz")]), (3, [(2, 0, 8, 3, None, "bar")])], ) self.stsides( shards, 2, 8, [(5, [(2, 0, 8, 5, None, "foo")]), (3, [(2, 0, 8, 3, None, "bar")])], ) self.stsides( shards, 2, 6, [(5, [(2, 0, 6, 5, None, "foo")]), (3, [(2, 0, 6, 3, None, "bar")])], ) self.stsides(shards, 10, 5, [(8, [(0, 0, 5, 8, None, "baz")])]) self.stsides(shards, 11, 3, [(8, [(1, 0, 3, 8, None, "baz")])]) def test_argument_validation(self): shards = [(5, [(0, 0, 10, 5, None, "foo")])] with self.subTest("trim_top requires a positive amount"): with self.assertRaises(ValueError): canvas.shards_trim_top(shards, 0) with self.subTest("trim_top cannot remove every shard"): with self.assertRaises(canvas.CanvasError): canvas.shards_trim_top(shards, 5) with self.subTest("trim_rows rejects a negative row count"): with self.assertRaises(ValueError): canvas.shards_trim_rows(shards, -1) with self.subTest("trim_sides rejects a negative left"): with self.assertRaises(ValueError): canvas.shards_trim_sides(shards, -1, 5) with self.subTest("trim_sides rejects a non-positive width"): with self.assertRaises(ValueError): canvas.shards_trim_sides(shards, 0, 0) class ShardsJoinTest(unittest.TestCase): def sjt(self, shard_lists, expected): result = canvas.shards_join(shard_lists) assert result == expected, f"got: {result!r} expected: {expected!r}" def test(self): shards1 = [(5, [(0, 0, 10, 5, None, "foo"), (0, 0, 5, 8, None, "baz")]), (3, [(0, 0, 10, 3, None, "bar")])] shards2 = [(3, [(0, 0, 10, 3, None, "aaa")]), (5, [(0, 0, 10, 5, None, "bbb")])] shards3 = [ (3, [(0, 0, 10, 3, None, "111")]), (2, [(0, 0, 10, 3, None, "222")]), (3, [(0, 0, 10, 3, None, "333")]), ] self.sjt([shards1], shards1) self.sjt( [shards1, shards2], [ (3, [(0, 0, 10, 5, None, "foo"), (0, 0, 5, 8, None, "baz"), (0, 0, 10, 3, None, "aaa")]), (2, [(0, 0, 10, 5, None, "bbb")]), (3, [(0, 0, 10, 3, None, "bar")]), ], ) self.sjt( [shards1, shards3], [ (3, [(0, 0, 10, 5, None, "foo"), (0, 0, 5, 8, None, "baz"), (0, 0, 10, 3, None, "111")]), (2, [(0, 0, 10, 3, None, "222")]), (3, [(0, 0, 10, 3, None, "bar"), (0, 0, 10, 3, None, "333")]), ], ) self.sjt( [shards1, shards2, shards3], [ ( 3, [ (0, 0, 10, 5, None, "foo"), (0, 0, 5, 8, None, "baz"), (0, 0, 10, 3, None, "aaa"), (0, 0, 10, 3, None, "111"), ], ), (2, [(0, 0, 10, 5, None, "bbb"), (0, 0, 10, 3, None, "222")]), (3, [(0, 0, 10, 3, None, "bar"), (0, 0, 10, 3, None, "333")]), ], ) class CanvasJoinTest(unittest.TestCase): def cjtest(self, desc, l, expected): l = [(c, None, False, n) for c, n in l] result = list(urwid.CanvasJoin(l).content()) assert result == expected, f"{desc} expected {expected!r}, got {result!r}" def test(self): C = urwid.TextCanvas hello = C([b"hello"]) there = C([b"there"], [[("a", 5)]]) a = C([b"a"]) hi = C([b"hi"]) how = C([b"how"], [[("a", 1)]]) dy = C([b"dy"]) how_you = C([b"how", b"you"]) self.cjtest("one", [(hello, 5)], [[(None, None, b"hello")]]) self.cjtest( "two", [(hello, 5), (there, 5)], [[(None, None, b"hello"), ("a", None, b"there")]], ) self.cjtest( "two space", [(hello, 7), (there, 5)], [[(None, None, b"hello"), (None, None, b" "), ("a", None, b"there")]], ) self.cjtest( "three space", [(hi, 4), (how, 3), (dy, 2)], [ [(None, None, b"hi"), (None, None, b" "), ("a", None, b"h"), (None, None, b"ow"), (None, None, b"dy")], ], ) self.cjtest( "four space", [(a, 2), (hi, 3), (dy, 3), (a, 1)], [ [ (None, None, b"a"), (None, None, b" "), (None, None, b"hi"), (None, None, b" "), (None, None, b"dy"), (None, None, b" "), (None, None, b"a"), ] ], ) self.cjtest( "pile 2", [(how_you, 4), (hi, 2)], [ [(None, None, b"how"), (None, None, b" "), (None, None, b"hi")], [(None, None, b"you"), (None, None, b" "), (None, None, b" ")], ], ) self.cjtest( "pile 2r", [(hi, 4), (how_you, 3)], [ [(None, None, b"hi"), (None, None, b" "), (None, None, b"how")], [(None, None, b" "), (None, None, b"you")], ], ) def test_shortcuts_are_attributed_to_the_joined_canvas_position(self) -> None: left = urwid.TextCanvas([b"hi"]) left.shortcuts["k"] = "original" right = urwid.TextCanvas([b"there"]) joined = urwid.CanvasJoin([(left, "left-pos", False, 2), (right, None, False, 5)]) self.assertEqual({"k": "left-pos"}, joined.shortcuts) class CanvasOverlayTest(unittest.TestCase): def setUp(self) -> None: self.old_encoding = get_encoding() def tearDown(self) -> None: urwid.set_encoding(self.old_encoding) def cotest(self, desc, bgt, bga, fgt, fga, l, r, et): with self.subTest(desc): bgt = bgt.encode("iso8859-1") fgt = fgt.encode("iso8859-1") bg = urwid.CompositeCanvas(urwid.TextCanvas([bgt], [bga])) fg = urwid.CompositeCanvas(urwid.TextCanvas([fgt], [fga])) bg.overlay(fg, l, 0) result = list(bg.content()) assert result == et, f"{desc} expected {et!r}, got {result!r}" def test1(self): self.cotest( "left", "qxqxqxqx", [], "HI", [], 0, 6, [[(None, None, b"HI"), (None, None, b"qxqxqx")]], ) self.cotest( "right", "qxqxqxqx", [], "HI", [], 6, 0, [[(None, None, b"qxqxqx"), (None, None, b"HI")]], ) self.cotest( "center", "qxqxqxqx", [], "HI", [], 3, 3, [[(None, None, b"qxq"), (None, None, b"HI"), (None, None, b"xqx")]], ) self.cotest( "center2", "qxqxqxqx", [], "HI ", [], 2, 2, [[(None, None, b"qx"), (None, None, b"HI "), (None, None, b"qx")]], ) self.cotest( "full", "rz", [], "HI", [], 0, 0, [[(None, None, b"HI")]], ) def test2(self): self.cotest( "same", "asdfghjkl", [("a", 9)], "HI", [("a", 2)], 4, 3, [[("a", None, b"asdf"), ("a", None, b"HI"), ("a", None, b"jkl")]], ) self.cotest( "diff", "asdfghjkl", [("a", 9)], "HI", [("b", 2)], 4, 3, [[("a", None, b"asdf"), ("b", None, b"HI"), ("a", None, b"jkl")]], ) self.cotest( "None end", "asdfghjkl", [("a", 9)], "HI ", [("a", 2)], 2, 3, [[("a", None, b"as"), ("a", None, b"HI"), (None, None, b" "), ("a", None, b"jkl")]], ) self.cotest( "float end", "asdfghjkl", [("a", 3)], "HI", [("a", 2)], 4, 3, [[("a", None, b"asd"), (None, None, b"f"), ("a", None, b"HI"), (None, None, b"jkl")]], ) self.cotest( "cover 2", "asdfghjkl", [("a", 5), ("c", 4)], "HI", [("b", 2)], 4, 3, [[("a", None, b"asdf"), ("b", None, b"HI"), ("c", None, b"jkl")]], ) self.cotest( "cover 2-2", "asdfghjkl", [("a", 4), ("d", 1), ("e", 1), ("c", 3)], "HI", [("b", 2)], 4, 3, [[("a", None, b"asdf"), ("b", None, b"HI"), ("c", None, b"jkl")]], ) def test3(self): urwid.set_encoding("euc-jp") self.cotest( "db0", "\xa1\xa1\xa1\xa1\xa1\xa1", [], "HI", [], 2, 2, [[(None, None, b"\xa1\xa1"), (None, None, b"HI"), (None, None, b"\xa1\xa1")]], ) self.cotest( "db1", "\xa1\xa1\xa1\xa1\xa1\xa1", [], "OHI", [], 1, 2, [[(None, None, b" "), (None, None, b"OHI"), (None, None, b"\xa1\xa1")]], ) self.cotest( "db2", "\xa1\xa1\xa1\xa1\xa1\xa1", [], "OHI", [], 2, 1, [[(None, None, b"\xa1\xa1"), (None, None, b"OHI"), (None, None, b" ")]], ) self.cotest( "db3", "\xa1\xa1\xa1\xa1\xa1\xa1", [], "OHIO", [], 1, 1, [[(None, None, b" "), (None, None, b"OHIO"), (None, None, b" ")]], ) class CompositeCanvasWrapTest(unittest.TestCase): def test_wrap_preserves_dimensions_text_and_cursor(self) -> None: """Wrapping a rendered canvas keeps its size and allows cursor updates.""" base = urwid.Edit("x").render((10,), focus=True) wrapped = urwid.CompositeCanvas(canv=base) self.assertEqual((10, 1), (wrapped.cols(), wrapped.rows())) self.assertEqual([b"x "], wrapped.text) self.assertEqual((1, 0), wrapped.cursor) wrapped.cursor = (3, 0) self.assertEqual((3, 0), wrapped.cursor) def test_wrap_accepts_pop_up_metadata(self) -> None: """Pop-up placement is recorded on a wrapped canvas.""" inner = urwid.SolidFill(" ").render((20, 10)) popup = urwid.Text("hi") wrapped = urwid.CompositeCanvas(canv=inner) wrapped.set_pop_up(popup, 5, 3, 8, 4) self.assertEqual((20, 10), (wrapped.cols(), wrapped.rows())) self.assertEqual((5, 3, (popup, 8, 4)), wrapped.get_pop_up()) def test_wrap_inherits_shortcuts_as_wrap(self) -> None: inner = urwid.SolidCanvas(" ", 3, 1) inner.shortcuts["x"] = "original" wrapped = urwid.CompositeCanvas(canv=inner) self.assertEqual({"x": "wrap"}, wrapped.shortcuts) class CompositeCanvasDimensionsTest(unittest.TestCase): def test_rows_rejects_a_non_integer_row_count(self) -> None: canv = urwid.CompositeCanvas(urwid.SolidCanvas(" ", 3, 1)) canv.shards = [(1.5, canv.shards[0][1])] with self.assertRaises(TypeError): canv.rows() def test_cols_rejects_a_non_integer_column_count(self) -> None: canv = urwid.CompositeCanvas(urwid.SolidCanvas(" ", 3, 1)) num_rows, cviews = canv.shards[0] (trim_left, trim_top, _cols, rows, attr_map, cv) = cviews[0] canv.shards = [(num_rows, [(trim_left, trim_top, 1.5, rows, attr_map, cv)])] with self.assertRaises(TypeError): canv.cols() class CompositeCanvasTrimTest(unittest.TestCase): def test_trim_argument_validation(self) -> None: canv = urwid.CompositeCanvas(urwid.SolidCanvas(" ", 3, 5)) with self.subTest("negative top"): with self.assertRaises(ValueError): canv.trim(-1) with self.subTest("top at or beyond the row count"): with self.assertRaises(ValueError): canv.trim(5) def test_trim_end_argument_validation(self) -> None: canv = urwid.CompositeCanvas(urwid.SolidCanvas(" ", 3, 5)) with self.subTest("non-positive end"): with self.assertRaises(ValueError): canv.trim_end(0) with self.subTest("end beyond the row count"): with self.assertRaises(ValueError): canv.trim_end(6) def test_mutation_on_finalized_canvas_raises(self) -> None: widget = urwid.SolidFill(" ") for description, mutate in ( ("trim", lambda c: c.trim(1)), ("trim_end", lambda c: c.trim_end(1)), ("pad_trim_left_right", lambda c: c.pad_trim_left_right(1, 0)), ("pad_trim_top_bottom", lambda c: c.pad_trim_top_bottom(1, 0)), ("overlay", lambda c: c.overlay(urwid.CompositeCanvas(urwid.SolidCanvas(" ", 1, 1)), 0, 0)), ("fill_attr_apply", lambda c: c.fill_attr_apply({})), ("set_depends", lambda c: c.set_depends([])), ): with self.subTest(description): canv = urwid.CompositeCanvas(urwid.SolidCanvas(" ", 3, 5)) canv.finalize(widget, (3, 5), False) with self.assertRaises(urwid.CanvasError): mutate(canv) class CompositeCanvasOverlayValidationTest(unittest.TestCase): def test_overlay_rejects_a_mismatched_size(self) -> None: base = urwid.CompositeCanvas(urwid.SolidCanvas(" ", 5, 5)) with self.subTest("too wide"): with self.assertRaises(ValueError): base.overlay(urwid.CompositeCanvas(urwid.SolidCanvas(" ", 3, 2)), 3, 0) with self.subTest("too tall"): with self.assertRaises(ValueError): base.overlay(urwid.CompositeCanvas(urwid.SolidCanvas(" ", 2, 3)), 0, 3) def test_overlay_on_a_rowless_canvas_yields_no_middle_shards(self) -> None: base = urwid.CompositeCanvas(urwid.SolidCanvas(" ", 5, 5)) base.trim(0, 0) # empties the canvas: no rows left to overlay onto self.assertEqual(0, base.rows()) self.assertEqual(0, base.cols()) # trimming to no rows also leaves no shards to measure columns from base.overlay(urwid.CompositeCanvas(), 0, 0) # equally empty, so sizes still match self.assertEqual([], base.shards) class CompositeCanvasFillAttrTest(unittest.TestCase): def test_fill_attr_sets_the_default_attribute(self) -> None: canv = urwid.CompositeCanvas(urwid.TextCanvas([b"hi"])) canv.fill_attr("a") self.assertEqual([[("a", None, b"hi")]], list(canv.content())) def test_fill_attr_apply_combines_with_an_existing_mapping(self) -> None: canv = urwid.CompositeCanvas(urwid.TextCanvas([b"hi"], [[("a", 2)]])) canv.fill_attr_apply({"a": "b"}) canv.fill_attr_apply({"b": "c", "z": "z"}) self.assertEqual([[("c", None, b"hi")]], list(canv.content())) class CanvasPadTrimTopBottomTest(unittest.TestCase): def test_pad_top_and_bottom(self) -> None: canvas = urwid.CompositeCanvas(urwid.SolidCanvas(" ", 3, 1)) canvas.pad_trim_top_bottom(1, 2) self.assertEqual(4, canvas.rows()) self.assertEqual(3, canvas.cols()) self.assertEqual([b" ", b" ", b" ", b" "], canvas.text) def test_trim_bottom(self) -> None: canvas = urwid.CompositeCanvas(urwid.TextCanvas([b"a", b"b", b"c", b"d", b"e"])) canvas.pad_trim_top_bottom(0, -2) self.assertEqual(3, canvas.rows()) self.assertEqual([b"a", b"b", b"c"], canvas.text) def test_trim_top(self) -> None: canvas = urwid.CompositeCanvas(urwid.TextCanvas([b"a", b"b", b"c"])) canvas.pad_trim_top_bottom(-1, 0) self.assertEqual(2, canvas.rows()) self.assertEqual([b"b", b"c"], canvas.text) class CanvasCombineTest(unittest.TestCase): def test_stacks_canvases_vertically(self) -> None: top = urwid.Text("top").render(()) middle = urwid.Text("mid").render(()) bottom = urwid.Text("bot").render(()) combined = urwid.CanvasCombine( [ (top, None, False), (middle, None, True), (bottom, None, False), ] ) self.assertEqual(3, combined.rows()) self.assertEqual(3, combined.cols()) self.assertEqual([b"top", b"mid", b"bot"], combined.text) def test_shortcuts_are_attributed_to_the_stacked_canvas_position(self) -> None: top = urwid.Text("top").render(()) top.shortcuts["k"] = "original" bottom = urwid.Text("bot").render(()) combined = urwid.CanvasCombine([(top, "top-pos", False), (bottom, None, False)]) self.assertEqual({"k": "top-pos"}, combined.shortcuts) class CanvasOverlayFunctionTest(unittest.TestCase): def test_shortcuts_from_the_top_canvas_are_marked_fg(self) -> None: top = urwid.CompositeCanvas(urwid.SolidCanvas(" ", 2, 2)) top.shortcuts["k"] = "original" bottom = urwid.SolidCanvas(" ", 5, 5) overlayed = canvas.CanvasOverlay(top, bottom, 1, 1) self.assertEqual({"k": "fg"}, overlayed.shortcuts) self.assertEqual((5, 5), (overlayed.cols(), overlayed.rows())) class CanvasPadTrimTest(unittest.TestCase): def cptest(self, desc, ct, ca, l, r, et): with self.subTest(desc): ct = ct.encode("iso8859-1") c = urwid.CompositeCanvas(urwid.TextCanvas([ct], [ca])) c.pad_trim_left_right(l, r) result = list(c.content()) self.assertEqual(result, et, f"{desc} expected {et!r}, got {result!r}") def test1(self): self.cptest("none", "asdf", [], 0, 0, [[(None, None, b"asdf")]]) self.cptest("left pad", "asdf", [], 2, 0, [[(None, None, b" "), (None, None, b"asdf")]]) self.cptest("right pad", "asdf", [], 0, 2, [[(None, None, b"asdf"), (None, None, b" ")]]) def test2(self): self.cptest("left trim", "asdf", [], -2, 0, [[(None, None, b"df")]]) self.cptest("right trim", "asdf", [], 0, -2, [[(None, None, b"as")]]) class ApplyTextLayoutTest(unittest.TestCase): def setUp(self) -> None: self.old_encoding = get_encoding() def tearDown(self) -> None: urwid.set_encoding(self.old_encoding) def test_repeated_offsets_reset_the_attribute_walk(self) -> None: # Two lines that both cover text offsets 0-4: the first line leaves # the attribute walk sitting at offset 2 (the start of the "b" run), # so the second line's request for offset 0 is behind it and arange() # has to rewind instead of assuming offsets only increase. text = b"abcdefgh" attr = [("a", 2), ("b", 2), ("c", 4)] ls = [[(4, 0, 4)], [(4, 0, 4)]] result = list(canvas.apply_text_layout(text, attr, ls, 4).content()) expected_line = [("a", None, b"ab"), ("b", None, b"cd")] self.assertEqual([expected_line, expected_line], result) def test_attribute_change_within_a_double_width_segment(self) -> None: # A single word-wrap segment can still contain more than one markup # attribute; when the encoding also changes the segment's byte width # (double-width euc-jp characters here), each attribute chunk has to # be re-encoded on its own to work out how many columns it used. urwid.set_encoding("euc-jp") text = "��" # two double-width hiragana characters attr = [("a", 1), ("b", 1)] ls = [[(4, 0, 2)]] result = list(canvas.apply_text_layout(text, attr, ls, 4).content()) self.assertEqual([[("a", None, b"\xa4\xa2"), ("b", None, b"\xa4\xa2")]], result) urwid-urwid-69907e4/tests/test_escapes.py000066400000000000000000000662231526073734300205060ustar00rootroot00000000000000#!/usr/bin/python """Tests covering escape sequences processing""" from __future__ import annotations import unittest import urwid from urwid import util from urwid.display import escape class EscapeModifierTest(unittest.TestCase): def test_all_combinations(self): expected = { "1": "", "2": "shift ", "3": "meta ", "4": "shift meta ", "5": "ctrl ", "6": "shift ctrl ", "7": "meta ctrl ", "8": "shift meta ctrl ", } for digit, prefix in expected.items(): self.assertEqual(prefix, escape.escape_modifier(digit)) class InputEscapeSequenceParserTest(unittest.TestCase): """Tests for parser of input escape sequences""" def test_bare_escape(self): codes = [27] expected = ["esc"] actual, rest = escape.process_keyqueue(codes, more_available=False) self.assertListEqual(expected, actual) self.assertListEqual([], rest) def test_meta(self): codes = [27, ord("4"), ord("2")] expected = ["meta 4"] actual, rest = escape.process_keyqueue(codes, more_available=False) self.assertListEqual(expected, actual) self.assertListEqual([ord("2")], rest) def test_shift_arrows(self): codes = [27, ord("["), ord("a")] expected = ["shift up"] actual, rest = escape.process_keyqueue(codes, more_available=False) self.assertListEqual(expected, actual) self.assertListEqual([], rest) def test_ctrl_pgup(self): codes = [27, 91, 53, 59, 53, 126] expected = ["ctrl page up"] actual, rest = escape.process_keyqueue(codes, more_available=False) self.assertListEqual(expected, actual) self.assertListEqual([], rest) def test_esc_meta_1(self): codes = [27, 27, 49] expected = ["esc", "meta 1"] actual, rest = escape.process_keyqueue(codes, more_available=False) self.assertListEqual(expected, actual) self.assertListEqual([], rest) def test_midsequence(self): # '[11~' is F1, '[12~' is F2, etc codes = [27, ord("["), ord("1")] with self.assertRaises(escape.MoreInputRequired): escape.process_keyqueue(codes, more_available=True) actual, rest = escape.process_keyqueue(codes, more_available=False) self.assertListEqual(["meta ["], actual) self.assertListEqual([ord("1")], rest) def test_mouse_press(self): codes = [27, 91, 77, 32, 41, 48] expected = [("mouse press", 1.0, 8, 15)] actual, rest = escape.process_keyqueue(codes, more_available=False) self.assertListEqual(expected, actual) self.assertListEqual([], rest) def test_bug_104(self): """GH #104: click-Esc & Esc-click crashes urwid apps""" codes = [27, 27, 91, 77, 32, 127, 59] expected = ["esc", ("mouse press", 1.0, 94, 26)] actual, rest = escape.process_keyqueue(codes, more_available=False) self.assertListEqual(expected, actual) self.assertListEqual([], rest) codes = [27, 27, 91, 77, 35, 120, 59] expected = ["esc", ("mouse release", 0, 87, 26)] actual, rest = escape.process_keyqueue(codes, more_available=False) self.assertListEqual(expected, actual) self.assertListEqual([], rest) def test_functional_keys(self): """Test for functional keys F1-F4, F5-F12.""" def check_key(expected: str, codes: list[int]) -> None: actual, rest = escape.process_keyqueue(codes, more_available=False) self.assertEqual( [expected], actual, f"Codes {codes!r} ({[chr(code) for code in codes]}) was not decoded to {expected!r}", ) # F1-F4 for num in range(1, 5): codes = [27, ord("O"), 79 + num] check_key(f"f{num}", codes) # F5-F8 for num, offset in enumerate((0, 2, 3, 4), start=5): codes = [27, ord("["), ord("1"), 53 + offset, ord("~")] check_key(f"f{num}", codes) # F9-F12 for num, offset in enumerate((0, 1, 3, 4), start=9): codes = [27, ord("["), ord("2"), 48 + offset, ord("~")] check_key(f"f{num}", codes) def test_functional_keys_mods_f1_f4(self): """Test for modifiers handling for functional keys F1-F4.""" prefixes = ("O", "[1;") masks = "12345678" letters = "PQRS" for prefix in prefixes: encoded_prefix = tuple(ord(element) for element in prefix) for num, letter in enumerate(letters, start=1): for mask in masks: codes = [27, *encoded_prefix, ord(mask), ord(letter)] actual, rest = escape.process_keyqueue(codes, more_available=False) expected = escape.escape_modifier(mask) + f"f{num}" self.assertEqual( [expected], actual, f"Codes {codes!r} ({[chr(code) for code in codes]}) was not decoded to {expected!r}", ) def test_functional_keys_mods_simple_f1_20(self): """Test for modifiers handling for functional keys F1-F20.""" masks = "12345678" numbers = (11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 23, 24, 25, 26, 28, 29, 31, 32, 33, 34) for num, number in enumerate(numbers, start=1): encoded_number = tuple(ord(element) for element in str(number)) for mask in masks: codes = [27, ord("["), *encoded_number, ord(";"), ord(mask), ord("~")] actual, rest = escape.process_keyqueue(codes, more_available=False) expected = escape.escape_modifier(mask) + f"f{num}" self.assertEqual( [expected], actual, f"Codes {codes!r} ({[chr(code) for code in codes]}) was not decoded to {expected!r}", ) def test_sgrmouse(self): prefix = (27, ord("["), ord("<")) x = 4 y = 8 coord = (ord(f"{x + 1}"), ord(";"), ord(f"{y + 1}")) action = ord("M") modifiers = ((0, ""), (8, "meta "), (16, "ctrl "), (24, "meta ctrl ")) for key, code in enumerate((0b0, 0b1, 0b10, 0b1000000, 0b1000001), start=1): for mod_code, mod in modifiers: key_code = tuple(ord(element) for element in str(code | mod_code)) codes = [*prefix, *key_code, ord(";"), *coord, action] actual, rest = escape.process_keyqueue(codes, more_available=False) expected = [(mod + "mouse press", key, x, y)] self.assertEqual( expected, actual, f"Codes {codes!r} ({[chr(code) for code in codes]}) was not decoded to {expected!r}", ) def test_sgrmouse_shift_modifier(self): prefix = (27, ord("["), ord("<")) x = 4 y = 8 coord = (ord(f"{x + 1}"), ord(";"), ord(f"{y + 1}")) key_code = tuple(ord(element) for element in str(0 | 4)) codes = [*prefix, *key_code, ord(";"), *coord, ord("M")] actual, rest = escape.process_keyqueue(codes, more_available=False) self.assertEqual([("shift mouse press", 1, x, y)], actual) self.assertListEqual([], rest) def test_sgrmouse_drag(self): prefix = (27, ord("["), ord("<")) x = 4 y = 8 coord = (ord(f"{x + 1}"), ord(";"), ord(f"{y + 1}")) key_code = tuple(ord(element) for element in str(0 | escape.MOUSE_DRAG_FLAG)) codes = [*prefix, *key_code, ord(";"), *coord, ord("M")] actual, rest = escape.process_keyqueue(codes, more_available=False) self.assertEqual([("mouse drag", 1, x, y)], actual) self.assertListEqual([], rest) def test_sgrmouse_release(self): prefix = (27, ord("["), ord("<")) x = 4 y = 8 coord = (ord(f"{x + 1}"), ord(";"), ord(f"{y + 1}")) key_code = tuple(ord(element) for element in str(0)) codes = [*prefix, *key_code, ord(";"), *coord, ord("m")] actual, rest = escape.process_keyqueue(codes, more_available=False) self.assertEqual([("mouse release", 1, x, y)], actual) self.assertListEqual([], rest) def test_sgrmouse_truncated(self): codes = [27, ord("["), ord("<")] with self.assertRaises(escape.MoreInputRequired): escape.process_keyqueue(codes, more_available=True) actual, rest = escape.process_keyqueue(codes, more_available=False) self.assertListEqual(["meta ["], actual) self.assertListEqual([ord("<")], rest) def test_sgrmouse_missing_terminator(self): codes = [ord("0"), ord(";"), ord("5"), ord(";"), ord("8")] with self.assertRaises(escape.MoreInputRequired): escape.input_trie.read_sgrmouse_info(codes, more_available=True) self.assertIsNone(escape.input_trie.read_sgrmouse_info(codes, more_available=False)) def test_sgrmouse_empty_keys(self): with self.assertRaises(escape.MoreInputRequired): escape.input_trie.read_sgrmouse_info([], more_available=True) self.assertIsNone(escape.input_trie.read_sgrmouse_info([], more_available=False)) def test_sgrmouse_malformed_is_not_a_report(self): """Input after ``ESC [ <`` that is not ``Pb;Px;Py`` decodes as keys instead of raising.""" for body in (b"hello M", b"1;2M", b";;M", b"1;2;3;4M", b"1;x;3M"): with self.subTest(body=body): self.assertIsNone(escape.input_trie.read_sgrmouse_info(list(body), more_available=True)) actual, rest = escape.process_keyqueue([27, *b"[<", *body], more_available=False) self.assertListEqual(["meta ["], actual) self.assertListEqual(list(b"<" + body), rest) def test_sgrmouse_overlong_body_is_not_buffered(self): """A digit run far longer than any report is rejected rather than held waiting for a terminator.""" codes = [ord("1")] * 100 self.assertIsNone(escape.input_trie.read_sgrmouse_info(codes, more_available=True)) def test_sgrmouse_body_length_limit(self): """A body up to the length limit is still awaited, one past it is not a report.""" with self.assertRaises(escape.MoreInputRequired): escape.input_trie.read_sgrmouse_info([ord("1")] * 32, more_available=True) self.assertIsNone(escape.input_trie.read_sgrmouse_info([ord("1")] * 33, more_available=True)) def test_report_after_escape(self): """A terminal report following ESC keeps the ESC as a separate key instead of raising.""" actual, rest = escape.process_keyqueue([27, 27, *b"[?1000;1$y"], more_available=False) self.assertListEqual(["esc", ("private mode report", "1000", 1)], actual) self.assertListEqual([], rest) def test_escape_run_awaiting_more_input(self): """The first of three ESCs is decoded at once while the rest wait for more input.""" actual, rest = escape.process_keyqueue([27, 27, 27], more_available=True) self.assertListEqual(["esc"], actual) self.assertListEqual([27, 27], rest) @staticmethod def _decode_all(codes: list[int]) -> list[str]: """Decode every code, as parse_input does once no more input is expected.""" decoded = [] while codes: run, codes = escape.process_keyqueue(codes, more_available=False) decoded.extend(run) return decoded def test_long_escape_run(self): """A long run of ESC bytes decodes without exhausting the recursion limit.""" self.assertListEqual(["esc"] * 5000, self._decode_all([27] * 5000)) def test_escape_run_before_meta_key(self): """Only the ESC directly before a key sequence turns it into a meta key.""" self.assertListEqual(["esc", "esc", "meta up"], self._decode_all([27, 27, 27, 27, ord("["), ord("A")])) def test_sgrmouse_wheel(self): prefix = (27, ord("["), ord("<")) x = 4 y = 8 coord = (ord(f"{x + 1}"), ord(";"), ord(f"{y + 1}")) # code 64/65 (0b1000000/0b1000001) sets the "high button" bit, reporting wheel buttons 4/5 for code, expected_button in ((0b1000000, 4), (0b1000001, 5)): key_code = tuple(ord(element) for element in str(code)) codes = [*prefix, *key_code, ord(";"), *coord, ord("M")] actual, rest = escape.process_keyqueue(codes, more_available=False) self.assertEqual([("mouse press", expected_button, x, y)], actual) self.assertListEqual([], rest) def test_sgrmouse_extra_buttons(self): prefix = (27, ord("["), ord("<")) x = 4 y = 8 coord = (ord(f"{x + 1}"), ord(";"), ord(f"{y + 1}")) # code 128-131 (0b10000000-0b10000011) sets the "extra button" bit, reporting buttons 8-11 for code, expected_button in ((0b10000000, 8), (0b10000001, 9), (0b10000010, 10), (0b10000011, 11)): key_code = tuple(ord(element) for element in str(code)) codes = [*prefix, *key_code, ord(";"), *coord, ord("M")] actual, rest = escape.process_keyqueue(codes, more_available=False) self.assertEqual([("mouse press", expected_button, x, y)], actual) self.assertListEqual([], rest) def test_mouse_x10_modifiers(self): x, y = 8, 15 coord = (x + 33, y + 33) for bit, prefix in ((4, "shift "), (8, "meta "), (16, "ctrl "), (4 | 8 | 16, "shift meta ctrl ")): codes = [27, 91, 77, 32 + bit, *coord] actual, rest = escape.process_keyqueue(codes, more_available=False) self.assertEqual([(f"{prefix}mouse press", 1, x, y)], actual) self.assertListEqual([], rest) def test_mouse_x10_drag(self): x, y = 8, 15 codes = [27, 91, 77, 32 + escape.MOUSE_DRAG_FLAG, x + 33, y + 33] actual, rest = escape.process_keyqueue(codes, more_available=False) self.assertEqual([("mouse drag", 1, x, y)], actual) self.assertListEqual([], rest) def test_mouse_x10_truncated(self): codes = [27, 91, 77, 32] with self.assertRaises(escape.MoreInputRequired): escape.process_keyqueue(codes, more_available=True) self.assertIsNone(escape.input_trie.read_mouse_info([32], more_available=False)) def test_mouse_x10_release(self): x, y = 8, 15 # button bits 0b11 (3) always mean "release", regardless of the MOUSE_RELEASE_FLAG bit keys = [32 + 3, x + 33, y + 33] result = escape.input_trie.read_mouse_info(keys, more_available=False) self.assertEqual((("mouse release", 0, x, y), []), result) def test_mouse_x10_extra_buttons(self): x, y = 8, 15 # code 128-131 sets the "extra button" bit, reporting buttons 8-11; bits 0b11 (button 11) # does not mean "release" here, unlike the plain 0b11 case in test_mouse_x10_release for code, expected_button in ((128, 8), (129, 9), (130, 10), (131, 11)): keys = [32 + code, x + 33, y + 33] result = escape.input_trie.read_mouse_info(keys, more_available=False) self.assertEqual((("mouse press", expected_button, x, y), []), result) def test_mouse_x10_wheel_button_7_is_not_release(self): x, y = 8, 15 # bits 0b11 combined with the "high button" flag (0b1000011 = 64 + 3) is wheel button 7, not a release keys = [32 + 0b1000011, x + 33, y + 33] result = escape.input_trie.read_mouse_info(keys, more_available=False) self.assertEqual((("mouse press", 7, x, y), []), result) def test_mouse_x10_double_click(self): x, y = 8, 15 keys = [32 + escape.MOUSE_MULTIPLE_CLICK_FLAG, x + 33, y + 33] result = escape.input_trie.read_mouse_info(keys, more_available=False) self.assertEqual((("double mouse click", 1, x, y), []), result) def test_mouse_x10_triple_click(self): x, y = 8, 15 keys = [32 + escape.MOUSE_MULTIPLE_CLICK_FLAG * 2, x + 33, y + 33] result = escape.input_trie.read_mouse_info(keys, more_available=False) self.assertEqual((("triple mouse click", 1, x, y), []), result) def test_mouse_x10_double_click_release(self): x, y = 8, 15 keys = [32 + escape.MOUSE_MULTIPLE_CLICK_FLAG + escape.MOUSE_RELEASE_FLAG, x + 33, y + 33] result = escape.input_trie.read_mouse_info(keys, more_available=False) self.assertEqual((("double mouse release", 1, x, y), []), result) def test_mouse_x10_leftover_keys_after_report(self): x, y = 8, 15 keys = [32, x + 33, y + 33, ord("z")] result = escape.input_trie.read_mouse_info(keys, more_available=False) self.assertEqual((("mouse press", 1, x, y), [ord("z")]), result) def test_utf8_multibyte_decoding(self): old_encoding = util.get_encoding() try: urwid.set_encoding("utf-8") for text, codes in ( ("\xe9", list(b"\xc3\xa9")), # 2-byte ("€", list(b"\xe2\x82\xac")), # 3-byte ("\U0001f600", list(b"\xf0\x9f\x98\x80")), # 4-byte ): with self.subTest(text=text): actual, rest = escape.process_keyqueue(codes, more_available=False) self.assertEqual([text], actual) self.assertListEqual([], rest) # invalid continuation byte falls back to a placeholder actual, rest = escape.process_keyqueue([0xC3, ord("A")], more_available=False) self.assertEqual(["<195>"], actual) self.assertListEqual([ord("A")], rest) # a standalone byte that is not a valid utf-8 lead byte (a bare continuation byte) actual, rest = escape.process_keyqueue([0x80], more_available=False) self.assertEqual(["<128>"], actual) self.assertListEqual([], rest) # overlong 2-byte encoding of NUL is structurally valid but fails to decode actual, rest = escape.process_keyqueue([0xC0, 0x80], more_available=False) self.assertEqual(["<192>"], actual) self.assertListEqual([0x80], rest) # truncated multi-byte sequence with self.assertRaises(escape.MoreInputRequired): escape.process_keyqueue([0xC3], more_available=True) actual, rest = escape.process_keyqueue([0xC3], more_available=False) self.assertEqual(["<195>"], actual) self.assertListEqual([], rest) finally: urwid.set_encoding(old_encoding) def test_nul_byte_fallback(self): old_encoding = util.get_encoding() try: urwid.set_encoding("utf-8") actual, rest = escape.process_keyqueue([0], more_available=False) self.assertEqual(["<0>"], actual) self.assertListEqual([], rest) finally: urwid.set_encoding(old_encoding) def test_ctrl_lowercase_range(self): # codes 8, 9, 10, 13 are intercepted earlier by _keyconv (backspace/tab/enter) for code in set(range(1, 27)) - {8, 9, 10, 13}: with self.subTest(code=code): actual, rest = escape.process_keyqueue([code], more_available=False) self.assertEqual([f"ctrl {chr(ord('a') + code - 1)}"], actual) self.assertListEqual([], rest) def test_ctrl_uppercase_range(self): for code in range(28, 32): with self.subTest(code=code): actual, rest = escape.process_keyqueue([code], more_available=False) self.assertEqual([f"ctrl {chr(ord('A') + code - 1)}"], actual) self.assertListEqual([], rest) def test_keyconv_lookup(self): for code, key in ((8, "backspace"), (9, "tab"), (10, "enter"), (13, "enter"), (127, "backspace")): with self.subTest(code=code): actual, rest = escape.process_keyqueue([code], more_available=False) self.assertEqual([key], actual) self.assertListEqual([], rest) class KeyqueueTrieTest(unittest.TestCase): def test_init_rejects_dict_result(self): self.assertRaises(TypeError, escape.KeyqueueTrie, [("a", {})]) def test_add_rejects_empty_sequence(self): trie = escape.KeyqueueTrie([]) self.assertRaises(RuntimeError, trie.add, trie.data, "", "x") def test_add_prefix_of_existing_sequence_conflicts(self): # "ab" is added first, so "a" collides with the branch node it created trie = escape.KeyqueueTrie([("ab", "X")]) self.assertRaises(RuntimeError, trie.add, trie.data, "a", "Y") def test_add_extension_of_existing_sequence_conflicts(self): # "a" is added first as a leaf, so "ab" collides with that leaf trie = escape.KeyqueueTrie([("a", "X")]) self.assertRaises(ValueError, trie.add, trie.data, "ab", "Y") def test_read_cursor_position_empty_keys(self): with self.assertRaises(escape.MoreInputRequired): escape.input_trie.read_cursor_position([], more_available=True) self.assertIsNone(escape.input_trie.read_cursor_position([], more_available=False)) def test_read_cursor_position_wrong_prefix(self): self.assertIsNone(escape.input_trie.read_cursor_position([ord("x")], more_available=False)) def test_read_cursor_position_missing_y(self): # semicolon before any digit codes = [ord("["), ord(";"), ord("5"), ord("R")] self.assertIsNone(escape.input_trie.read_cursor_position(codes, more_available=False)) def test_read_cursor_position_non_digit_y(self): codes = [ord("["), ord("x")] self.assertIsNone(escape.input_trie.read_cursor_position(codes, more_available=False)) def test_read_cursor_position_leading_zero_y(self): codes = [ord("["), ord("0"), ord("1"), ord(";"), ord("5"), ord("R")] self.assertIsNone(escape.input_trie.read_cursor_position(codes, more_available=False)) def test_read_cursor_position_truncated_after_y(self): codes = [ord("["), ord("5"), ord(";")] with self.assertRaises(escape.MoreInputRequired): escape.input_trie.read_cursor_position(codes, more_available=True) self.assertIsNone(escape.input_trie.read_cursor_position(codes, more_available=False)) def test_read_cursor_position_non_digit_x(self): codes = [ord("["), ord("5"), ord(";"), ord("y")] self.assertIsNone(escape.input_trie.read_cursor_position(codes, more_available=False)) def test_read_cursor_position_leading_zero_x(self): codes = [ord("["), ord("5"), ord(";"), ord("0"), ord("1"), ord("R")] self.assertIsNone(escape.input_trie.read_cursor_position(codes, more_available=False)) def test_read_cursor_position_missing_x(self): # 'R' immediately after the semicolon, with no digit for x codes = [ord("["), ord("5"), ord(";"), ord("R")] self.assertIsNone(escape.input_trie.read_cursor_position(codes, more_available=False)) def test_read_cursor_position_truncated_after_x_digits(self): codes = [ord("["), ord("5"), ord(";"), ord("3")] with self.assertRaises(escape.MoreInputRequired): escape.input_trie.read_cursor_position(codes, more_available=True) self.assertIsNone(escape.input_trie.read_cursor_position(codes, more_available=False)) def test_read_cursor_position_success(self): codes = [ord("["), ord("5"), ord(";"), ord("3"), ord("R"), ord("z")] result = escape.input_trie.read_cursor_position(codes, more_available=False) self.assertEqual((("cursor position", 2, 4), [ord("z")]), result) class OutputSequenceTest(unittest.TestCase): def test_set_cursor_position(self): self.assertEqual(f"{escape.ESC}[6;11H", escape.set_cursor_position(10, 5)) self.assertRaises(TypeError, escape.set_cursor_position, "10", 5) self.assertRaises(TypeError, escape.set_cursor_position, 10, "5") def test_move_cursor_right(self): self.assertEqual("", escape.move_cursor_right(0)) self.assertEqual(f"{escape.ESC}[3C", escape.move_cursor_right(3)) def test_move_cursor_up(self): self.assertEqual("", escape.move_cursor_up(0)) self.assertEqual(f"{escape.ESC}[3A", escape.move_cursor_up(3)) def test_move_cursor_down(self): self.assertEqual("", escape.move_cursor_down(0)) self.assertEqual(f"{escape.ESC}[3B", escape.move_cursor_down(3)) class PrivateModeTest(unittest.TestCase): def test_members_are_plain_strings(self): # str(member) has to render the bare mode number (not "PrivateMode.BRACKETED_PASTE"), # since query_private_mode() interpolates it directly into an escape sequence. self.assertEqual("1004", str(escape.PrivateMode.FOCUS_REPORTING)) self.assertEqual("2004", str(escape.PrivateMode.BRACKETED_PASTE)) self.assertEqual("2026", str(escape.PrivateMode.SYNCHRONIZED_OUTPUT)) self.assertEqual("2027", str(escape.PrivateMode.GRAPHEME_CLUSTERING)) class QueryPrivateModeTest(unittest.TestCase): def test_query_private_mode(self): self.assertEqual(f"{escape.ESC}[?1004$p", escape.PrivateMode.FOCUS_REPORTING.query) self.assertEqual(f"{escape.ESC}[?2004$p", escape.PrivateMode.BRACKETED_PASTE.query) self.assertEqual(f"{escape.ESC}[?2026$p", escape.PrivateMode.SYNCHRONIZED_OUTPUT.query) class ReadPrivateModeReportTest(unittest.TestCase): def test_recognized_reply(self): result = escape.input_trie.read_private_mode_report(list(b"[?2004;1$y"), False) self.assertEqual((("private mode report", "2004", 1), []), result) def test_unrecognized_reply(self): result = escape.input_trie.read_private_mode_report(list(b"[?2026;0$y"), False) self.assertEqual((("private mode report", "2026", 0), []), result) def test_trailing_bytes_are_left_unconsumed(self): result = escape.input_trie.read_private_mode_report(list(b"[?2026;1$yab"), False) self.assertEqual((("private mode report", "2026", 1), [ord("a"), ord("b")]), result) def test_not_a_private_mode_report_returns_none(self): self.assertIsNone(escape.input_trie.read_private_mode_report(list(b"[6;5R"), False)) self.assertIsNone(escape.input_trie.read_private_mode_report([], False)) def test_incomplete_reply_raises_more_input_required_when_more_available(self): for prefix in (b"[", b"[?", b"[?2004", b"[?2004;", b"[?2004;1", b"[?2004;1$"): with self.subTest(prefix=prefix), self.assertRaises(escape.MoreInputRequired): escape.input_trie.read_private_mode_report(list(prefix), True) def test_incomplete_reply_returns_none_when_no_more_available(self): for prefix in (b"[", b"[?", b"[?2004", b"[?2004;", b"[?2004;1", b"[?2004;1$"): with self.subTest(prefix=prefix): self.assertIsNone(escape.input_trie.read_private_mode_report(list(prefix), False)) def test_recognized_through_process_keyqueue(self): codes = [27, *b"[?2004;1$y"] actual, rest = escape.process_keyqueue(codes, more_available=False) self.assertEqual([("private mode report", "2004", 1)], actual) self.assertEqual([], rest) def test_decrqm_recognized_values(self): for value in (1, 2, 3, 4): self.assertIn(value, escape.DECRQM_RECOGNIZED_VALUES) self.assertNotIn(0, escape.DECRQM_RECOGNIZED_VALUES) urwid-urwid-69907e4/tests/test_event_loops.py000066400000000000000000001121711526073734300214120ustar00rootroot00000000000000from __future__ import annotations import asyncio import concurrent.futures import socket import sys import time import typing import unittest import urwid if typing.TYPE_CHECKING: from collections.abc import Callable from concurrent.futures import Future from types import TracebackType try: import gi.repository except ImportError: GLIB_AVAILABLE = False else: GLIB_AVAILABLE = True try: import tornado except ImportError: TORNADO_AVAILABLE = False else: TORNADO_AVAILABLE = True try: import twisted except ImportError: TWISTED_AVAILABLE = False else: TWISTED_AVAILABLE = True try: import trio except ImportError: TRIO_AVAILABLE = False else: TRIO_AVAILABLE = True if sys.version_info < (3, 11): from exceptiongroup import BaseExceptionGroup # pylint: disable=redefined-builtin # backport try: import zmq except ImportError: ZMQ_AVAILABLE = False else: ZMQ_AVAILABLE = True IS_WINDOWS = sys.platform == "win32" class ClosingSocketPair(typing.ContextManager[tuple[socket.socket, socket.socket]]): __slots__ = ("rd_s", "wr_s") def __init__(self) -> None: self.rd_s: socket.socket | None = None self.wr_s: socket.socket | None = None def __enter__(self) -> tuple[socket.socket, socket.socket]: self.rd_s, self.wr_s = socket.socketpair() return self.rd_s, self.wr_s def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: if self.rd_s is not None: self.rd_s.close() if self.wr_s is not None: self.wr_s.close() class EventLoopTestMixin: evl: urwid.EventLoop def _exit_once_seen(self, out: list[str], exit_clean: Callable[[], typing.NoReturn], *expected: str) -> None: """Call *exit_clean* from an alarm once every string in *expected* is in *out*. Work the test waits for can finish after any fixed delay on a loaded machine, and alarms that are due together run in one pass, so a loop started late can stop before it was ever idle. The check repeats on a short alarm and calls *exit_clean* anyway after a generous deadline, so work that never happens fails the test instead of hanging it. """ deadline = time.monotonic() + 10 def exit_once_seen() -> None: if not set(expected).issubset(out) and time.monotonic() < deadline: self.evl.alarm(0.005, exit_once_seen) return exit_clean() self.evl.alarm(0.01, exit_once_seen) def test_event_loop(self): evl: urwid.EventLoop = self.evl out = [] rd: socket.socket wr: socket.socket def step1(): out.append("writing") wr.send(b"hi") def step2(): out.append(rd.recv(2).decode("ascii")) raise urwid.ExitMainLoop with ClosingSocketPair() as (rd, wr): _handle = evl.alarm(0, step1) _handle = evl.watch_file(rd.fileno(), step2) evl.run() self.assertEqual(out, ["writing", "hi"]) def test_remove_alarm(self): evl: urwid.EventLoop = self.evl handle = evl.alarm(50, lambda: None) def step1(): self.assertTrue(evl.remove_alarm(handle)) self.assertFalse(evl.remove_alarm(handle)) raise urwid.ExitMainLoop evl.alarm(0, step1) evl.run() def test_remove_watch_file(self): evl: urwid.EventLoop = self.evl with ClosingSocketPair() as (rd, _wr): handle = evl.watch_file(rd.fileno(), lambda: None) def step1(): self.assertTrue(evl.remove_watch_file(handle)) self.assertFalse(evl.remove_watch_file(handle)) raise urwid.ExitMainLoop evl.alarm(0, step1) evl.run() def test_remove_enter_idle(self): evl: urwid.EventLoop = self.evl out: list[str] = [] handle = evl.enter_idle(lambda: out.append("idle")) self.assertTrue(evl.remove_enter_idle(handle)) self.assertFalse(evl.remove_enter_idle(handle)) def exit_clean() -> typing.NoReturn: raise urwid.ExitMainLoop evl.alarm(0.01, exit_clean) evl.run() self.assertEqual([], out) def test_alarm_reschedule(self): """Reschedule a short alarm until work completes, then exit the loop.""" evl: urwid.EventLoop = self.evl ticks: list[int] = [] remaining = 3 def tick() -> None: nonlocal remaining remaining -= 1 ticks.append(remaining) if remaining: evl.alarm(0.01, tick) else: raise urwid.ExitMainLoop evl.alarm(0.01, tick) evl.run() self.assertEqual([2, 1, 0], ticks) _expected_idle_handle = 1 def test_run(self): evl: urwid.EventLoop = self.evl out = [] wr: socket.socket rd: socket.socket def say_hello(): out.append("hello") def say_waiting(): out.append("waiting") def exit_clean() -> typing.NoReturn: out.append("clean exit") raise urwid.ExitMainLoop def exit_error() -> typing.NoReturn: 1 / 0 with ClosingSocketPair() as (rd, wr): self.assertEqual(wr.send(b"data"), 4) self._exit_once_seen(out, exit_clean, "waiting", "hello") _handle = evl.alarm(0.005, say_hello) idle_handle = evl.enter_idle(say_waiting) if self._expected_idle_handle is not None: self.assertEqual(idle_handle, 1) evl.run() self.assertTrue("waiting" in out, out) self.assertTrue("hello" in out, out) self.assertTrue("clean exit" in out, out) handle = evl.watch_file(rd.fileno(), exit_clean) del out[:] evl.run() self.assertEqual(["clean exit"], out) self.assertTrue(evl.remove_watch_file(handle)) _handle = evl.alarm(0, exit_error) self.assertRaises(ZeroDivisionError, evl.run) _handle = evl.watch_file(rd.fileno(), exit_error) self.assertRaises(ZeroDivisionError, evl.run) def test_run_in_executor(self): out: list[str] = [] with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: def start(): out.append("started") self.evl.run_in_executor(executor, func).add_done_callback(callback) def func() -> bool: out.append("future called") return True def callback(fut: Future) -> None: out.append(f"callback called with future outcome: {fut.result()}") def exit_clean() -> typing.NoReturn: out.append("clean exit") raise urwid.ExitMainLoop self._exit_once_seen(out, exit_clean, "callback called with future outcome: True") _handle = self.evl.alarm(0.005, start) self.evl.run() self.assertEqual( [ "started", "future called", "callback called with future outcome: True", "clean exit", ], out, ) class StaleReadyWatchTestMixin: """Descriptors reported ready together, where an earlier callback changes the watch of a later one.""" def _poll_once(self) -> None: """Run one iteration of the loop.""" raise NotImplementedError def test_watch_removed_by_earlier_callback_is_skipped(self): """A descriptor that was ready but lost its watch earlier in the same pass is not called back.""" evl = self.evl called = [] with ClosingSocketPair() as (rd_1, wr_1), ClosingSocketPair() as (rd_2, wr_2): def first() -> None: called.append("first") evl.remove_watch_file(rd_2.fileno()) def second() -> None: called.append("second") evl.remove_watch_file(rd_1.fileno()) evl.watch_file(rd_1.fileno(), first) evl.watch_file(rd_2.fileno(), second) wr_1.send(b"x") wr_2.send(b"x") self._poll_once() # Both descriptors were ready; whichever callback ran first removed the other one's watch. self.assertEqual(1, len(called)) def test_watch_replaced_by_earlier_callback_is_skipped(self): """A watch replaced earlier in the same pass is not called with the readiness seen for the old one.""" evl = self.evl called = [] with ClosingSocketPair() as (rd_1, wr_1), ClosingSocketPair() as (rd_2, wr_2): def replacement() -> None: called.append("replacement") def first() -> None: called.append("first") evl.remove_watch_file(rd_2.fileno()) evl.watch_file(rd_2.fileno(), replacement) def second() -> None: called.append("second") evl.remove_watch_file(rd_1.fileno()) evl.watch_file(rd_1.fileno(), replacement) evl.watch_file(rd_1.fileno(), first) evl.watch_file(rd_2.fileno(), second) wr_1.send(b"x") wr_2.send(b"x") self._poll_once() self.assertEqual(1, len(called)) self.assertNotIn("replacement", called) class SelectEventLoopTest(unittest.TestCase, EventLoopTestMixin, StaleReadyWatchTestMixin): def setUp(self): self.evl = urwid.SelectEventLoop() def _poll_once(self) -> None: """Run one iteration of the loop.""" self.evl._loop() def test_watch_registered_with_a_file_object_is_called(self): """A watch registered with an object that has fileno() is found again when its descriptor is ready.""" called = [] with ClosingSocketPair() as (rd, wr): self.evl.watch_file(rd, lambda: called.append("ready")) wr.send(b"x") self._poll_once() self.assertEqual(["ready"], called) @unittest.skipIf(IS_WINDOWS, "Windows is temporary not supported by AsyncioEventLoop.") class AsyncioEventLoopTest(unittest.TestCase, EventLoopTestMixin): def setUp(self): if sys.version_info[:2] < (3, 11): self.loop = asyncio.get_event_loop_policy().get_event_loop() self.evl_runner = None else: self.evl_runner = asyncio.Runner(loop_factory=asyncio.SelectorEventLoop) self.loop = self.evl_runner.get_loop() self.evl = urwid.AsyncioEventLoop(loop=self.loop) def tearDown(self): if self.evl_runner is not None: self.evl_runner.close() _expected_idle_handle = None def test_error(self): evl = self.evl evl.alarm(0, lambda: 1 / 0) # Simulate error in event loop self.assertRaises(ZeroDivisionError, evl.run) def test_run_restores_the_previous_exception_handler(self): """After run() returns or raises, the loop's own exception handler is back in place.""" def previous_handler(_loop: asyncio.AbstractEventLoop, _context: dict[str, typing.Any]) -> None: """Stand in for an exception handler the application installed.""" def stop() -> typing.NoReturn: raise urwid.ExitMainLoop self.loop.set_exception_handler(previous_handler) self.addCleanup(self.loop.set_exception_handler, None) self.evl.alarm(0, stop) self.evl.run() self.assertIs(previous_handler, self.loop.get_exception_handler()) self.evl.alarm(0, lambda: 1 / 0) with self.assertRaises(ZeroDivisionError): self.evl.run() self.assertIs(previous_handler, self.loop.get_exception_handler()) @unittest.skipIf( sys.implementation.name in ("pypy", "graalpy"), "Relies on CPython's refcounting GC to promptly finalize the task and trigger " "asyncio's 'Task exception was never retrieved' handler; PyPy and GraalPy use a " "tracing GC, so that finalizer may never run before the process exits and evl.run() " "hangs forever.", ) def test_coroutine_error(self): evl = self.evl async def error_coro(): result = 1 / 0 # Simulate error in coroutine return result asyncio.ensure_future(error_coro(), loop=self.loop) self.assertRaises(ZeroDivisionError, evl.run) def test_async_alarm_callback(self): evl = self.evl out: list[str] = [] async def step1() -> typing.NoReturn: out.append("async alarm") raise urwid.ExitMainLoop evl.alarm(0, step1) evl.run() self.assertEqual(["async alarm"], out) # test_event_loop() and test_run() (EventLoopTestMixin) watch a pipe and close it as part of # the test body; on GraalPy, tearDown()'s evl_runner.close() -> shutdown_asyncgens() then # tries to select() on that already-closed fd and raises "OSError: [Errno 9] Bad file # descriptor" -- after the test body itself has already passed. This is GraalPy's asyncio # SelectorEventLoop failing to deregister a closed fd during Runner shutdown, not an urwid # issue, so the test as a whole is skipped rather than weakening the assertions to work around # a teardown-only failure. _skip_graalpy_runner_close = unittest.skipIf( sys.implementation.name == "graalpy", "GraalPy's asyncio.Runner.close() raises OSError: Bad file descriptor tearing down a closed watched pipe", ) @_skip_graalpy_runner_close def test_event_loop(self): super().test_event_loop() @_skip_graalpy_runner_close def test_run(self): super().test_run() def test_async_watch_file_callback(self): evl = self.evl out: list[str] = [] with ClosingSocketPair() as (rd, wr): # Non-blocking: the fd stays readable for one extra event loop # tick after the async callback is scheduled, so it may fire # again with no data left to read. rd.setblocking(False) async def step2() -> None: try: data = rd.recv(2) except BlockingIOError: return out.append(data.decode("ascii")) raise urwid.ExitMainLoop evl.watch_file(rd.fileno(), step2) wr.send(b"hi") evl.run() # De-register before the fd is closed and its number possibly reused by a later test: # AsyncioEventLoopTest reuses the process-wide default loop on Python < 3.11. evl.remove_watch_file(rd.fileno()) self.assertEqual(["hi"], out) def test_async_enter_idle_callback(self): evl = self.evl out: list[str] = [] async def say_waiting() -> None: out.append("waiting") def say_hello() -> None: out.append("hello") def exit_clean() -> typing.NoReturn: raise urwid.ExitMainLoop evl.enter_idle(say_waiting) evl.alarm(0.005, say_hello) self._exit_once_seen(out, exit_clean, "waiting") evl.run() self.assertIn("waiting", out) @unittest.skipIf( sys.implementation.name == "pypy", "Well known dead wait (lock?) on pypy.", ) def test_async_alarm_callback_error(self): evl = self.evl async def error_coro() -> typing.NoReturn: 1 / 0 # Simulate error in coroutine evl.alarm(0, error_coro) self.assertRaises(ZeroDivisionError, evl.run) @unittest.skip("Unstable test: race conditions happens") @unittest.skipUnless(GLIB_AVAILABLE, "GLIB unavailable") class GLibEventLoopTest(unittest.TestCase, EventLoopTestMixin): def setUp(self): self.evl = urwid.GLibEventLoop() def test_error(self): evl = self.evl evl.alarm(0, lambda: 1 / 0) # Simulate error in event loop self.assertRaises(ZeroDivisionError, evl.run) @unittest.skipUnless(TORNADO_AVAILABLE, "Tornado not available") @unittest.skipIf( IS_WINDOWS, "Windows is temporary not supported by TornadoEventLoop due to race conditions.", ) class TornadoEventLoopTest(unittest.TestCase, EventLoopTestMixin): def setUp(self): from tornado.ioloop import IOLoop self.evl = urwid.TornadoEventLoop(IOLoop()) _expected_idle_handle = None def test_error(self): evl = self.evl evl.alarm(0, lambda: 1 / 0) # Simulate error in event loop self.assertRaises(ZeroDivisionError, evl.run) def test_async_alarm_callback(self): evl = self.evl out: list[str] = [] async def step1() -> typing.NoReturn: out.append("async alarm") raise urwid.ExitMainLoop evl.alarm(0, step1) evl.run() self.assertEqual(["async alarm"], out) def test_async_watch_file_callback(self): evl = self.evl out: list[str] = [] with ClosingSocketPair() as (rd, wr): # Non-blocking: the fd stays readable for one extra event loop # tick after the async callback is scheduled, so it may fire # again with no data left to read. rd.setblocking(False) async def step2() -> None: try: data = rd.recv(2) except BlockingIOError: return out.append(data.decode("ascii")) raise urwid.ExitMainLoop evl.watch_file(rd.fileno(), step2) wr.send(b"hi") evl.run() evl.remove_watch_file(rd.fileno()) self.assertEqual(["hi"], out) def test_async_enter_idle_callback(self): evl = self.evl out: list[str] = [] async def say_waiting() -> None: out.append("waiting") def say_hello() -> None: out.append("hello") def exit_clean() -> typing.NoReturn: raise urwid.ExitMainLoop evl.enter_idle(say_waiting) evl.alarm(0.005, say_hello) self._exit_once_seen(out, exit_clean, "waiting") evl.run() self.assertIn("waiting", out) def test_async_alarm_callback_error(self): evl = self.evl async def error_coro() -> typing.NoReturn: 1 / 0 # Simulate error in coroutine evl.alarm(0, error_coro) self.assertRaises(ZeroDivisionError, evl.run) @unittest.skipUnless(TWISTED_AVAILABLE, "Twisted is not available") @unittest.skipIf( IS_WINDOWS, "Windows is temporary not supported by TwistedEventLoop due to race conditions.", ) class TwistedEventLoopTest(unittest.TestCase, EventLoopTestMixin): def setUp(self): self.evl = urwid.TwistedEventLoop() # can't restart twisted reactor, so use shortened tests def test_event_loop(self): pass def test_remove_alarm(self): pass def test_remove_watch_file(self): pass def test_remove_enter_idle(self): pass def test_alarm_reschedule(self): pass def test_run(self): from twisted.internet import threads evl = self.evl out = [] wr: socket.socket rd: socket.socket def step2(): out.append(rd.recv(2).decode("ascii")) def say_hello(): out.append("hello") def say_waiting(): out.append("waiting") def test_remove_alarm(): handle = evl.alarm(50, lambda: None) self.assertTrue(evl.remove_alarm(handle)) self.assertFalse(evl.remove_alarm(handle)) out.append("remove_alarm ok") def test_remove_watch_file(): with ClosingSocketPair() as (rd_, _wr): handle = evl.watch_file(rd_.fileno(), lambda: None) self.assertTrue(evl.remove_watch_file(handle)) self.assertFalse(evl.remove_watch_file(handle)) out.append("remove_watch_file ok") def test_threaded(): out.append("schedule to thread") threads.deferToThread(func).addCallback(callback) def func() -> bool: out.append("future called") return True def callback(result: bool) -> None: out.append(f"callback called with future outcome: {result}") def exit_clean() -> typing.NoReturn: out.append("clean exit") raise urwid.ExitMainLoop def exit_error() -> typing.NoReturn: 1 / 0 with ClosingSocketPair() as (rd, wr): self.assertEqual(wr.send(b"data"), 4) _handle = evl.watch_file(rd.fileno(), step2) self._exit_once_seen(out, exit_clean, "callback called with future outcome: True") _handle = evl.alarm(0.05, say_hello) _handle = evl.alarm(0.06, test_remove_alarm) _handle = evl.alarm(0.07, test_remove_watch_file) _handle = evl.alarm(0.08, test_threaded) self.assertEqual(evl.enter_idle(say_waiting), 1) evl.run() self.assertIn("da", out) self.assertIn("ta", out) self.assertIn("hello", out) self.assertIn("remove_alarm ok", out) self.assertIn("clean exit", out) self.assertIn("schedule to thread", out) self.assertIn("future called", out) self.assertIn("callback called with future outcome: True", out) def test_error(self): evl = self.evl evl.alarm(0, lambda: 1 / 0) # Simulate error in event loop self.assertRaises(ZeroDivisionError, evl.run) @unittest.skip("not implemented for twisted event loop") def test_run_in_executor(self): """Not implemented for twisted event loop.""" @unittest.skipUnless(TRIO_AVAILABLE, "Trio not available") @unittest.skipIf( IS_WINDOWS, "Windows is temporary not supported by TrioEventLoop due to race conditions.", ) class TrioEventLoopTest(unittest.TestCase, EventLoopTestMixin): def setUp(self): self.evl = urwid.TrioEventLoop() _expected_idle_handle = None def test_error(self): evl = self.evl evl.alarm(0, lambda: 1 / 0) # Simulate error in event loop self.assertRaises(ZeroDivisionError, evl.run) @unittest.skip("not implemented for trio event loop") def test_run_in_executor(self): """Not implemented for trio event loop.""" def test_async_alarm_callback(self): evl = self.evl out: list[str] = [] async def step1() -> typing.NoReturn: out.append("async alarm") raise urwid.ExitMainLoop evl.alarm(0, step1) evl.run() self.assertEqual(["async alarm"], out) def test_async_watch_file_callback(self): evl = self.evl out: list[str] = [] with ClosingSocketPair() as (rd, wr): # Non-blocking: the fd stays readable for one extra event loop # tick after the async callback is scheduled, so it may fire # again with no data left to read. rd.setblocking(False) async def step2() -> None: try: data = rd.recv(2) except BlockingIOError: return out.append(data.decode("ascii")) raise urwid.ExitMainLoop handle = evl.watch_file(rd.fileno(), step2) wr.send(b"hi") evl.run() evl.remove_watch_file(handle) self.assertEqual(["hi"], out) def test_async_enter_idle_callback(self): evl = self.evl out: list[str] = [] async def say_waiting() -> None: out.append("waiting") def say_hello() -> None: out.append("hello") def exit_clean() -> typing.NoReturn: raise urwid.ExitMainLoop evl.enter_idle(say_waiting) evl.alarm(0.005, say_hello) self._exit_once_seen(out, exit_clean, "waiting") evl.run() self.assertIn("waiting", out) def test_idle_callbacks_run_only_while_the_nursery_is_open(self) -> None: """Idle callbacks, which need the nursery when async, are not run while Trio polls after it closed.""" evl = typing.cast("urwid.TrioEventLoop", self.evl) out: list[str] = [] async def say_waiting() -> None: out.append("waiting") def exit_clean() -> typing.NoReturn: raise urwid.ExitMainLoop evl.enter_idle(lambda: out.append("idle" if evl._nursery else "idle after exit")) evl.enter_idle(say_waiting) self._exit_once_seen(out, exit_clean, "waiting") evl.run() self.assertIn("waiting", out) self.assertNotIn("idle after exit", out) def test_idle_callback_exit_main_loop_stops_the_loop(self) -> None: """ExitMainLoop raised by an idle callback stops the loop cleanly, as from an alarm callback.""" evl = self.evl out: list[str] = [] def exit_clean() -> typing.NoReturn: out.append("clean exit") raise urwid.ExitMainLoop def guard_exit() -> typing.NoReturn: out.append("guard exit") raise urwid.ExitMainLoop evl.enter_idle(exit_clean) self._exit_once_seen(out, guard_exit, "clean exit") evl.run() self.assertEqual(["clean exit"], out) def test_idle_callback_may_change_the_idle_callbacks(self) -> None: """An idle callback that removes itself and adds another does not break the loop.""" evl = self.evl out: list[str] = [] handles: list[int] = [] def once() -> None: out.append("once") evl.remove_enter_idle(handles[0]) handles.append(evl.enter_idle(lambda: out.append("waiting"))) def exit_clean() -> typing.NoReturn: out.append("clean exit") raise urwid.ExitMainLoop handles.append(evl.enter_idle(once)) self._exit_once_seen(out, exit_clean, "waiting") evl.run() self.assertEqual("once", out[0]) self.assertIn("waiting", out) self.assertEqual("clean exit", out[-1]) def test_idle_callback_error_is_raised_from_run(self) -> None: """An exception raised by an idle callback stops the loop and is raised from run().""" evl = self.evl out: list[str] = [] def exit_error() -> typing.NoReturn: out.append("error") raise ZeroDivisionError def guard_exit() -> typing.NoReturn: out.append("guard exit") raise urwid.ExitMainLoop evl.enter_idle(exit_error) self._exit_once_seen(out, guard_exit, "error") self.assertRaises(ZeroDivisionError, evl.run) self.assertEqual(["error"], out) def test_async_alarm_callback_error(self): evl = self.evl async def error_coro() -> typing.NoReturn: 1 / 0 # Simulate error in coroutine evl.alarm(0, error_coro) self.assertRaises(ZeroDivisionError, evl.run) def test_watch_file_callback_self_cancel_exits_cleanly(self): # A callback that cancels its own watch (rather than raising ExitMainLoop) # has to leave the watching task's loop, not have it call wait_readable # again on an fd it just gave up watching. evl = self.evl out: list[str] = [] with ClosingSocketPair() as (rd, wr): def step() -> None: out.append(rd.recv(2).decode("ascii")) evl.remove_watch_file(handle) handle = evl.watch_file(rd.fileno(), step) wr.send(b"hi") def exit_clean() -> typing.NoReturn: raise urwid.ExitMainLoop self._exit_once_seen(out, exit_clean, "hi") evl.run() self.assertEqual(["hi"], out) def test_run_async_embeds_in_an_existing_trio_run(self): evl = self.evl out: list[str] = [] async def body() -> None: async with trio.open_nursery() as nursery: def exit_clean() -> typing.NoReturn: out.append("clean exit") raise urwid.ExitMainLoop evl.alarm(0.01, exit_clean) await evl.run_async() nursery.cancel_scope.cancel() trio.run(body) self.assertEqual(["clean exit"], out) def test_run_async_reraises_a_callback_error(self): # No outer nursery here (unlike the embedding example above): wrapping the # await in one more nursery would let *its* __aexit__ re-wrap the already # unwrapped error in a fresh single-exception ExceptionGroup of its own. evl = self.evl evl.alarm(0, lambda: 1 / 0) async def body() -> None: await evl.run_async() self.assertRaises(ZeroDivisionError, trio.run, body) def test_multi_exception_group_is_not_unwrapped(self): # A single failing task's exception is unwrapped from its ExceptionGroup # (see test_error above), but a group of more than one exception has to # be raised as-is so neither exception is silently dropped. Triggering # two *simultaneous* task failures through evl.run() is inherently racy # (trio cancels not-yet-started sibling tasks before they can raise # their own exception), so the group is built directly instead. evl = self.evl group = BaseExceptionGroup("multiple failures", [ZeroDivisionError(), RuntimeError("boom")]) with self.assertRaises(BaseExceptionGroup) as ctx: evl._handle_main_loop_exception(group) self.assertIs(group, ctx.exception) @unittest.skipUnless(ZMQ_AVAILABLE, "ZMQ is not available") @unittest.skipIf(IS_WINDOWS, "ZMQEventLoop is not supported under windows") class ZMQEventLoopTest(unittest.TestCase, EventLoopTestMixin, StaleReadyWatchTestMixin): def setUp(self): self.evl = urwid.ZMQEventLoop() def _poll_once(self) -> None: """Run one iteration of the loop.""" asyncio.run(self.evl._loop()) def test_watch_queue_and_remove_watch_queue(self): evl = self.evl out: list[bytes] = [] ctx = zmq.Context.instance() pull = ctx.socket(zmq.PULL) push = ctx.socket(zmq.PUSH) try: address = f"inproc://{self.id()}" pull.bind(address) push.connect(address) def on_ready() -> typing.NoReturn: out.append(pull.recv()) raise urwid.ExitMainLoop handle = evl.watch_queue(pull, on_ready) push.send(b"hi") evl.run() self.assertEqual([b"hi"], out) self.assertTrue(evl.remove_watch_queue(handle)) self.assertFalse(evl.remove_watch_queue(handle)) finally: pull.close() push.close() def test_watch_file_with_a_zmq_socket(self): """A zmq socket given to watch_file() is called back when it has a message.""" evl = self.evl out: list[bytes] = [] ctx = zmq.Context.instance() pull = ctx.socket(zmq.PULL) push = ctx.socket(zmq.PUSH) try: address = f"inproc://{self.id()}" pull.bind(address) push.connect(address) def on_ready() -> typing.NoReturn: out.append(pull.recv()) raise urwid.ExitMainLoop def give_up() -> typing.NoReturn: raise urwid.ExitMainLoop evl.watch_file(pull, on_ready) evl.alarm(5, give_up) # a missed event fails the assertion below instead of hanging the run push.send(b"hi") evl.run() self.assertEqual([b"hi"], out) self.assertTrue(evl.remove_watch_file(pull)) finally: pull.close() push.close() def test_zmq_socket_has_one_watch_for_file_and_queue(self): """watch_file() and watch_queue() set the same watch on a zmq socket, and either remove call clears it.""" evl = self.evl pull = zmq.Context.instance().socket(zmq.PULL) try: evl.watch_file(pull, lambda: None) with self.assertRaises(ValueError): evl.watch_queue(pull, lambda: None) self.assertTrue(evl.remove_watch_queue(pull)) self.assertFalse(evl.remove_watch_file(pull)) evl.watch_queue(pull, lambda: None) self.assertTrue(evl.remove_watch_file(pull)) self.assertFalse(evl.remove_watch_queue(pull)) finally: pull.close() def test_remove_watch_file_by_object_or_descriptor(self): """A watch is removed from the poller whether it is named by the object or by its descriptor number.""" evl = self.evl called = [] with ClosingSocketPair() as (rd_1, wr_1), ClosingSocketPair() as (rd_2, wr_2): evl.watch_file(rd_1, lambda: called.append("by object")) evl.watch_file(rd_2.fileno(), lambda: called.append("by descriptor")) self.assertTrue(evl.remove_watch_file(rd_1.fileno())) self.assertTrue(evl.remove_watch_file(rd_2)) self.assertFalse(evl.remove_watch_file(rd_1)) wr_1.send(b"x") wr_2.send(b"x") def stop() -> typing.NoReturn: raise urwid.ExitMainLoop evl.alarm(0, stop) evl.run() self.assertEqual([], called) def test_watch_queue_rejects_a_queue_already_being_watched(self): evl = self.evl pull = zmq.Context.instance().socket(zmq.PULL) try: evl.watch_queue(pull, lambda: None) with self.assertRaises(ValueError): evl.watch_queue(pull, lambda: None) finally: evl.remove_watch_queue(pull) pull.close() def test_remove_watch_queue_missing_handle(self): evl = self.evl pull = zmq.Context.instance().socket(zmq.PULL) try: self.assertFalse(evl.remove_watch_queue(pull)) finally: pull.close() def test_async_alarm_callback(self): evl = self.evl out: list[str] = [] async def step1() -> typing.NoReturn: out.append("async alarm") raise urwid.ExitMainLoop evl.alarm(0, step1) evl.run() self.assertEqual(["async alarm"], out) def test_async_watch_file_callback(self): evl = self.evl out: list[str] = [] with ClosingSocketPair() as (rd, wr): # Non-blocking: the fd stays readable for one extra event loop # tick after the async callback is scheduled, so it may fire # again with no data left to read. rd.setblocking(False) async def step2() -> None: try: data = rd.recv(2) except BlockingIOError: return out.append(data.decode("ascii")) raise urwid.ExitMainLoop handle = evl.watch_file(rd.fileno(), step2) wr.send(b"hi") evl.run() evl.remove_watch_file(handle) self.assertEqual(["hi"], out) def test_async_enter_idle_callback(self): evl = self.evl out: list[str] = [] async def say_waiting() -> None: out.append("waiting") def say_hello() -> None: out.append("hello") def exit_clean() -> typing.NoReturn: raise urwid.ExitMainLoop evl.enter_idle(say_waiting) evl.alarm(0.005, say_hello) self._exit_once_seen(out, exit_clean, "waiting") evl.run() self.assertIn("waiting", out) def test_async_alarm_callback_error(self): evl = self.evl async def error_coro() -> typing.NoReturn: 1 / 0 # Simulate error in coroutine evl.alarm(0, error_coro) self.assertRaises(ZeroDivisionError, evl.run) def test_async_callback_runs_concurrently_as_a_background_task(self): # ZMQEventLoop runs on top of an asyncio loop (via zmq.asyncio.Poller), so an # async def callback is scheduled as a background task there just like on # AsyncioEventLoop/TornadoEventLoop: the rest of the loop keeps running rather # than waiting for the callback's own internal awaits to resolve. evl = self.evl out: list[str] = [] async def slow() -> None: out.append("start") await asyncio.sleep(10) # long enough to still be pending when the loop exits out.append("end") def exit_clean() -> typing.NoReturn: out.append("exit") raise urwid.ExitMainLoop evl.alarm(0, slow) evl.alarm(0, exit_clean) evl.run() self.assertEqual(["start", "exit"], out) def test_exception_handler_is_a_no_op_once_the_main_task_is_gone(self): # _run_async() clears _main_task in its finally block once the loop stops, so a # background task's exception that only surfaces during interpreter/loop shutdown # (after _run_async() itself has already returned) must not crash trying to # cancel a task that no longer exists. evl = self.evl self.assertIsNone(evl._main_task) evl._exception_handler(typing.cast("typing.Any", None), {"exception": RuntimeError("boom")}) self.assertIsInstance(evl._exc, RuntimeError) urwid-urwid-69907e4/tests/test_font.py000066400000000000000000000016111526073734300200170ustar00rootroot00000000000000from __future__ import annotations import unittest import urwid from urwid.util import get_encoding class TestFontRender(unittest.TestCase): def setUp(self) -> None: self.old_encoding = get_encoding() urwid.set_encoding("utf-8") def tearDown(self) -> None: urwid.set_encoding(self.old_encoding) def test_001_basic(self): font = urwid.Thin3x3Font() rendered = b"\n".join(font.render("1").text).decode() expected = " � \n │ \n ┴ " self.assertEqual(expected, rendered) def test_002_non_rect(self): """Test non rect symbol, which causes spaces based padding. Lines as bytes should be not equal length. """ font = urwid.Thin3x3Font() rendered = b"\n".join(font.render("2").text).decode() expected = "┌─�\n┌─┘\n└─ " self.assertEqual(expected, rendered) urwid-urwid-69907e4/tests/test_grapheme_clusters.py000066400000000000000000000151321526073734300225700ustar00rootroot00000000000000from __future__ import annotations import unittest from urwid import str_util # 🇨🇦 regional indicator Canada flag FLAG_CA = "\U0001F1E8\U0001F1E6" # 👨�👩�👧 family (man + ZWJ + woman + ZWJ + girl) FAMILY = "\U0001F468\u200D\U0001F469\u200D\U0001F467" # 👩��💻 woman technologist with light skin tone TECHNOLOGIST = "\U0001F469\U0001F3FB\u200D\U0001F4BB" # 👋� waving hand with light skin tone WAVE_SKIN = "\U0001F44B\U0001F3FB" # café with combining acute accent on e CAFE = "cafe\u0301" # �� red heart with VS16 (emoji presentation) HEART_VS16 = "\u2764\ufe0f" # � red heart (text presentation) HEART = "\u2764" # 中文 Chinese characters CJK = "\u4e2d\u6587" class CalcWidthGraphemeTest(unittest.TestCase): def wtest(self, text, expected): result = str_util.calc_width(text, 0, len(text)) assert result == expected, f"got:{result!r} expected:{expected!r}" def test_basic(self): self.wtest("hello", 5) self.wtest("", 0) self.wtest(CJK, 4) def test_flags(self): self.wtest(FLAG_CA, 2) self.wtest(FLAG_CA + FLAG_CA, 4) def test_zwj_sequences(self): self.wtest(FAMILY, 2) self.wtest(TECHNOLOGIST, 2) def test_skin_tones(self): self.wtest(WAVE_SKIN, 2) def test_combining(self): self.wtest(CAFE, 4) self.wtest("e\u0301", 1) def test_variation_selectors(self): self.wtest(HEART_VS16, 2) self.wtest(HEART, 1) class CalcTextPosGraphemeTest(unittest.TestCase): def ptest(self, text, pref_col, expected_pos): pos, _ = str_util.calc_text_pos(text, 0, len(text), pref_col) assert pos == expected_pos, f"got:{pos!r} expected:{expected_pos!r}" def test_basic(self): self.ptest("hello", 3, 3) def test_flag_boundaries(self): text = f"A{FLAG_CA}B" self.ptest(text, 1, 1) self.ptest(text, 2, 1) self.ptest(text, 3, 3) def test_zwj_boundaries(self): text = f"A{FAMILY}B" self.ptest(text, 2, 1) self.ptest(text, 3, 6) class MoveNextCharGraphemeTest(unittest.TestCase): def test_move_through_family_emoji(self): text = f"A{FAMILY}B" pos = str_util.move_next_char(text, 1, len(text)) assert pos == 6 def test_move_through_flag(self): text = f"A{FLAG_CA}B" pos = str_util.move_next_char(text, 1, len(text)) assert pos == 3 def test_move_through_combining(self): pos = str_util.move_next_char(CAFE, 3, len(CAFE)) assert pos == 5 def test_move_through_skin_tone(self): text = f"A{WAVE_SKIN}B" pos = str_util.move_next_char(text, 1, len(text)) assert pos == 3 def test_basic_ascii(self): pos = str_util.move_next_char("hello", 0, 5) assert pos == 1 class MovePrevCharGraphemeTest(unittest.TestCase): def test_move_back_through_family_emoji(self): text = f"A{FAMILY}B" pos = str_util.move_prev_char(text, 0, 6) assert pos == 1 def test_move_back_through_flag(self): text = f"A{FLAG_CA}B" pos = str_util.move_prev_char(text, 0, 3) assert pos == 1 def test_move_back_through_combining(self): pos = str_util.move_prev_char(CAFE, 0, 5) assert pos == 3 def test_move_back_through_skin_tone(self): text = f"A{WAVE_SKIN}B" pos = str_util.move_prev_char(text, 0, 3) assert pos == 1 def test_basic_ascii(self): pos = str_util.move_prev_char("hello", 0, 5) assert pos == 4 class IsWideCharGraphemeTest(unittest.TestCase): def test_family_emoji_is_wide(self): assert str_util.is_wide_char(FAMILY, 0) def test_flag_is_wide(self): assert str_util.is_wide_char(FLAG_CA, 0) def test_cjk_is_wide(self): assert str_util.is_wide_char(CJK, 0) def test_ascii_not_wide(self): assert not str_util.is_wide_char("hello", 0) def test_skin_tone_is_wide(self): assert str_util.is_wide_char(WAVE_SKIN, 0) # UTF-8 bytes test constants FLAG_CA_BYTES = FLAG_CA.encode("utf-8") # 8 bytes FAMILY_BYTES = FAMILY.encode("utf-8") # 18 bytes class Utf8BytesGraphemeTest(unittest.TestCase): """Tests for grapheme clustering with UTF-8 bytes.""" def setUp(self): self._orig_encoding = str_util.get_byte_encoding() str_util.set_byte_encoding("utf8") def tearDown(self): str_util.set_byte_encoding(self._orig_encoding) def test_calc_width_flag_bytes(self): result = str_util.calc_width(FLAG_CA_BYTES, 0, len(FLAG_CA_BYTES)) assert result == 2 def test_calc_width_family_emoji_bytes(self): result = str_util.calc_width(FAMILY_BYTES, 0, len(FAMILY_BYTES)) assert result == 2 def test_move_next_through_family_emoji_bytes(self): # After 'A' (1 byte), should skip entire family emoji text = f"A{FAMILY}B".encode("utf-8") pos = str_util.move_next_char(text, 1, len(text)) assert pos == 1 + len(FAMILY_BYTES) def test_move_next_through_flag_bytes(self): # After 'A' (1 byte), should skip entire flag text = f"A{FLAG_CA}B".encode("utf-8") pos = str_util.move_next_char(text, 1, len(text)) assert pos == 1 + len(FLAG_CA_BYTES) def test_move_prev_through_family_emoji_bytes(self): # From position after family emoji, should go back to after 'A' text = f"A{FAMILY}B".encode("utf-8") end_pos = 1 + len(FAMILY_BYTES) pos = str_util.move_prev_char(text, 0, end_pos) assert pos == 1 def test_move_prev_through_flag_bytes(self): # From position after flag, should go back to after 'A' text = f"A{FLAG_CA}B".encode("utf-8") end_pos = 1 + len(FLAG_CA_BYTES) pos = str_util.move_prev_char(text, 0, end_pos) assert pos == 1 def test_is_wide_family_emoji_bytes(self): assert str_util.is_wide_char(FAMILY_BYTES, 0) def test_is_wide_flag_bytes(self): assert str_util.is_wide_char(FLAG_CA_BYTES, 0) def test_calc_text_pos_flag_boundaries_bytes(self): text = f"A{FLAG_CA}B".encode("utf-8") # Column 1 should be at position 1 (after 'A') pos, col = str_util.calc_text_pos(text, 0, len(text), 1) assert pos == 1 assert col == 1 # Column 2 should still be at position 1 (inside flag grapheme) pos, col = str_util.calc_text_pos(text, 0, len(text), 2) assert pos == 1 assert col == 1 # Column 3 should be at position after flag pos, col = str_util.calc_text_pos(text, 0, len(text), 3) assert pos == 1 + len(FLAG_CA_BYTES) assert col == 3 urwid-urwid-69907e4/tests/test_main_loop.py000066400000000000000000000707371526073734300210450ustar00rootroot00000000000000from __future__ import annotations import asyncio import concurrent.futures import contextlib import doctest import io import os import socket import sys import threading import time import typing import unittest.mock import urwid from urwid.display.common import BaseScreen from urwid.event_loop import main_loop as main_loop_module if typing.TYPE_CHECKING: from types import TracebackType IS_WINDOWS = sys.platform == "win32" IS_GRAALPY = sys.implementation.name == "graalpy" class ClosingScreenIO(typing.ContextManager[tuple[socket.socket, typing.TextIO]]): """Socket backed input/output for a raw `Screen` detached from any real terminal. The raw display reads bytes from its input socket and writes text to its output stream, so the input end is handed over as a socket and the output end as a text stream. Both ends are sockets since a socket descriptor can not be wrapped by `os.fdopen` on Windows. """ __slots__ = ("_closing",) def __init__(self) -> None: self._closing: list[typing.IO[typing.Any] | socket.socket] = [] def __enter__(self) -> tuple[socket.socket, typing.TextIO]: screen_input, feed_input = socket.socketpair() collect_output, screen_output = socket.socketpair() output_stream = screen_output.makefile("w", encoding="utf-8") self._closing = [output_stream, screen_input, feed_input, collect_output, screen_output] return screen_input, output_stream def __exit__( self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: """Close everything explicit without waiting for garbage collected.""" for closing in self._closing: with contextlib.suppress(OSError): closing.close() self._closing = [] def stop_screen_cb(*_args, **_kwargs) -> typing.NoReturn: raise urwid.ExitMainLoop @contextlib.contextmanager def dummy_raw_main_loop( widget: urwid.Widget | None = None, **kwargs: typing.Any, ) -> typing.Iterator[urwid.MainLoop]: """MainLoop bound to socket-backed raw Screen IO (no real TTY).""" with ClosingScreenIO() as (screen_input, screen_output): yield urwid.MainLoop( widget if widget is not None else urwid.SolidFill(), screen=urwid.display.raw.Screen(input=screen_input, output=screen_output), handle_mouse=False, **kwargs, ) class NonExternalLoopScreen(BaseScreen): """Screen with no ``hook_event_loop`` support, forcing :meth:`MainLoop._run_screen_event_loop`.""" def __init__(self, cols_rows: tuple[int, int] = (10, 5)) -> None: super().__init__() self._cols_rows = cols_rows self.input_calls = 0 def get_cols_rows(self) -> tuple[int, int]: return self._cols_rows def draw_screen(self, size: tuple[int, int], canvas: typing.Any) -> None: pass def get_input(self, raw_keys: bool = False) -> tuple[list[typing.Any], list[int]]: self.input_calls += 1 return [], [] def set_input_timeouts(self, max_wait: float | None = None) -> None: pass class ScriptedInputScreen(NonExternalLoopScreen): """Screen returning a scripted sequence of get_input() results, then empty.""" def __init__( self, responses: list[tuple[list[typing.Any], list[int]]], cols_rows: tuple[int, int] = (10, 5), ) -> None: super().__init__(cols_rows) self._responses = list(responses) def get_input(self, raw_keys: bool = False) -> tuple[list[typing.Any], list[int]]: self.input_calls += 1 if self._responses: return self._responses.pop(0) return [], [] class RecordingWidget: """Duck-typed widget recording keypress/mouse calls with a scripted response.""" def __init__( self, selectable: bool = True, keypress_return: str | None = None, mouse_return: bool = True, ) -> None: self._selectable = selectable self._keypress_return = keypress_return self._mouse_return = mouse_return self.keypress_calls: list[str] = [] self.mouse_calls: list[tuple[str, int, int, int]] = [] def selectable(self) -> bool: return self._selectable def keypress(self, size: tuple[int, int], key: str) -> str | None: self.keypress_calls.append(key) return self._keypress_return def mouse_event(self, size: tuple[int, int], event: str, button: int, col: int, row: int, focus: bool) -> bool: self.mouse_calls.append((event, button, col, row)) return self._mouse_return class TestMainLoop(unittest.TestCase): def test_run_stops_the_screen_when_start_fails(self): """A failure in start() after the screen started leaves the terminal restored.""" screen = unittest.mock.Mock(spec=urwid.display.raw.Screen) screen.set_mouse_tracking.side_effect = OSError event_loop = unittest.mock.Mock(spec=urwid.SelectEventLoop) loop = urwid.MainLoop(urwid.SolidFill(), screen=screen, event_loop=event_loop) with self.assertRaises(OSError): loop.run() screen.start.assert_called_once_with() screen.stop.assert_called_once_with() event_loop.run.assert_not_called() @unittest.skipIf(IS_WINDOWS, "selectors for pipe are not supported on Windows") @unittest.skipIf(IS_GRAALPY, "fcntl.fcntl is missing on GraalPy") def test_watch_pipe_removed_at_end_of_file(self): """Once every write end is closed, the callback gets b"" once and the watch is gone.""" loop = urwid.MainLoop( urwid.SolidFill(), screen=unittest.mock.Mock(spec=urwid.display.raw.Screen), event_loop=urwid.SelectEventLoop(), ) received: list[bytes] = [] def stop() -> typing.NoReturn: raise urwid.ExitMainLoop def on_data(data: bytes) -> None: received.append(data) if len(received) > 2: stop() # a watch left on the closed pipe keeps it ready and starves the alarm below pipe_wr = loop.watch_pipe(on_data) os.write(pipe_wr, b"data") os.close(pipe_wr) loop.event_loop.alarm(0.1, stop) loop.event_loop.run() self.assertEqual([b"data", b""], received) self.assertFalse(loop.remove_watch_pipe(pipe_wr)) @unittest.skipIf(IS_WINDOWS, "AsyncioEventLoop is not supported on Windows here") @unittest.skipIf(IS_GRAALPY, "fcntl.fcntl is missing on GraalPy") def test_watch_pipe_async_callback_reads_each_write_once(self): """Wake-ups queued before the first task read the pipe do not fail on the drained descriptor.""" asyncio_loop = asyncio.new_event_loop() self.addCleanup(asyncio_loop.close) loop = urwid.MainLoop( urwid.SolidFill(), screen=unittest.mock.Mock(spec=urwid.display.raw.Screen), event_loop=urwid.AsyncioEventLoop(loop=asyncio_loop), ) received: list[bytes] = [] async def on_data(data: bytes) -> bool: received.append(data) return data != b"last" def stop() -> typing.NoReturn: raise urwid.ExitMainLoop pipe_wr = loop.watch_pipe(on_data) self.addCleanup(os.close, pipe_wr) os.write(pipe_wr, b"first") loop.event_loop.alarm(0.1, lambda: os.write(pipe_wr, b"last")) loop.event_loop.alarm(0.2, stop) loop.event_loop.run() self.assertEqual([b"first", b"last"], received) self.assertFalse(loop.remove_watch_pipe(pipe_wr)) @unittest.skipIf(IS_WINDOWS, "AsyncioEventLoop is not supported on Windows here") @unittest.skipIf(IS_GRAALPY, "fcntl.fcntl is missing on GraalPy") def test_watch_pipe_async_callback_gets_end_of_file_once(self): """An async callback still running at the end of the pipe does not get b"" again on later passes.""" asyncio_loop = asyncio.new_event_loop() self.addCleanup(asyncio_loop.close) loop = urwid.MainLoop( urwid.SolidFill(), screen=unittest.mock.Mock(spec=urwid.display.raw.Screen), event_loop=urwid.AsyncioEventLoop(loop=asyncio_loop), ) received: list[bytes] = [] async def on_data(data: bytes) -> None: received.append(data) await asyncio.sleep(0.05) def stop() -> typing.NoReturn: raise urwid.ExitMainLoop pipe_wr = loop.watch_pipe(on_data) os.close(pipe_wr) loop.event_loop.alarm(0.2, stop) loop.event_loop.run() self.assertEqual([b""], received) self.assertFalse(loop.remove_watch_pipe(pipe_wr)) @unittest.skipIf(IS_WINDOWS, "selectors for pipe are not supported on Windows") @unittest.skipIf(IS_GRAALPY, "fcntl.fcntl is missing on GraalPy") def test_watch_pipe(self): """Test watching pipe is stopped on explicit False only.""" evt = threading.Event() # We need thread synchronization outcome: list[bytes] = [] def pipe_cb(data: bytes) -> typing.Any: outcome.append(data) if not evt.is_set(): evt.set() if data == b"false": return False if data == b"true": return True if data == b"null": return None return object() def pipe_writer(fd: int) -> None: os.write(fd, b"something") if evt.wait(0.1): evt.clear() os.write(fd, b"true") if evt.wait(0.1): evt.clear() os.write(fd, b"null") if evt.wait(0.1): evt.clear() os.write(fd, b"false") with ( ClosingScreenIO() as (screen_input, screen_output), concurrent.futures.ThreadPoolExecutor( max_workers=1, ) as executor, unittest.mock.patch( "subprocess.Popen", # we want to be sure that nothing outside is called autospec=True, ), ): evl = urwid.MainLoop( urwid.SolidFill(), # We need screen which support mocked IO screen=urwid.display.raw.Screen(input=screen_input, output=screen_output), handle_mouse=False, # Less external calls - better ) evl.set_alarm_in(1, stop_screen_cb) pipe_fd = evl.watch_pipe(pipe_cb) executor.submit(pipe_writer, pipe_fd) evl.run() self.assertEqual([b"something", b"true", b"null", b"false"], outcome) not_removed = evl.remove_watch_pipe(pipe_fd) self.assertFalse(not_removed) def test_set_alarm_in(self): """Loop_customizations schedules work with set_alarm_in and ExitMainLoop.""" seen: list[tuple[urwid.MainLoop, object]] = [] def on_alarm(loop: urwid.MainLoop, user_data: object) -> typing.NoReturn: seen.append((loop, user_data)) raise urwid.ExitMainLoop with dummy_raw_main_loop() as evl: evl.set_alarm_in(0.01, on_alarm, user_data="token") evl.run() self.assertEqual(1, len(seen)) self.assertIs(seen[0][0], evl) self.assertEqual("token", seen[0][1]) def test_set_alarm_in_async_callback(self): """An async def callback is awaited, not silently dropped, on a loop that supports it.""" seen: list[tuple[urwid.MainLoop, object]] = [] async def on_alarm(loop: urwid.MainLoop, user_data: object) -> typing.NoReturn: seen.append((loop, user_data)) raise urwid.ExitMainLoop with dummy_raw_main_loop(event_loop=urwid.AsyncioEventLoop()) as evl: evl.set_alarm_in(0.01, on_alarm, user_data="token") evl.run() self.assertEqual(1, len(seen)) self.assertIs(seen[0][0], evl) self.assertEqual("token", seen[0][1]) def test_set_alarm_in_reschedule(self): """Drain reschedules set_alarm_in until finished.""" ticks: list[int] = [] remaining = 3 def drain(loop: urwid.MainLoop, _user_data: object) -> None: nonlocal remaining remaining -= 1 ticks.append(remaining) if remaining: loop.set_alarm_in(0.01, drain) else: raise urwid.ExitMainLoop with dummy_raw_main_loop() as evl: evl.set_alarm_in(0.01, drain) evl.run() self.assertEqual([2, 1, 0], ticks) def test_draw_screen_from_alarm(self): """Update widgets then call draw_screen.""" text = urwid.Text("wait") widget = urwid.Filler(text, valign="top") draws = 0 original_draw: typing.Callable[..., None] | None = None def counting_draw(*args: typing.Any, **kwargs: typing.Any) -> None: nonlocal draws draws += 1 typing.cast("typing.Callable[..., None]", original_draw)(*args, **kwargs) def on_alarm(loop: urwid.MainLoop, _user_data: object) -> typing.NoReturn: text.set_text("done") loop.draw_screen() raise urwid.ExitMainLoop with dummy_raw_main_loop(widget) as evl: original_draw = evl.screen.draw_screen evl.screen.draw_screen = counting_draw # type: ignore[method-assign] evl.set_alarm_in(0.01, on_alarm) evl.run() self.assertEqual("done", text.text) self.assertGreaterEqual(draws, 1) def test_widget_replace_and_draw_screen(self): """Replaces the top widget then redraw.""" first = urwid.SolidFill(".") second = urwid.SolidFill("#") def on_alarm(loop: urwid.MainLoop, _user_data: object) -> typing.NoReturn: loop.widget = second loop.draw_screen() raise urwid.ExitMainLoop with dummy_raw_main_loop(first) as evl: evl.set_alarm_in(0.01, on_alarm) evl.run() self.assertIs(second, evl.widget) def test_remove_alarm(self): fired = False def should_not_run(_loop: urwid.MainLoop, _user_data: object) -> None: nonlocal fired fired = True with dummy_raw_main_loop() as evl: handle = evl.set_alarm_in(50, should_not_run) self.assertTrue(evl.remove_alarm(handle)) self.assertFalse(evl.remove_alarm(handle)) evl.set_alarm_in(0.01, stop_screen_cb) evl.run() self.assertFalse(fired) def test_unhandled_input(self): """Pass unhandled_key as unhandled_input.""" seen: list[str | tuple[str, int, int, int]] = [] def unhandled(key: str | tuple[str, int, int, int]) -> bool: seen.append(key) return True with dummy_raw_main_loop(unhandled_input=unhandled) as evl: evl.screen_size = (80, 24) self.assertTrue(evl.process_input(["esc"])) self.assertEqual(["esc"], seen) def test_pop_ups(self): """Construct MainLoop with pop_ups enabled.""" with dummy_raw_main_loop(pop_ups=True) as evl: self.assertTrue(evl.pop_ups) evl.set_alarm_in(0.01, stop_screen_cb) evl.run() def test_default_screen_created(self): """No screen given: MainLoop constructs its own raw display Screen.""" evl = urwid.MainLoop(urwid.SolidFill()) self.assertIsInstance(evl.screen, urwid.display.raw.Screen) def test_register_palette_via_constructor(self): """A non-empty palette passed to the constructor is registered on the screen.""" with ClosingScreenIO() as (screen_input, screen_output): screen = urwid.display.raw.Screen(input=screen_input, output=screen_output) urwid.MainLoop(urwid.SolidFill(), palette=[("body", "black", "white")], screen=screen) self.assertIn("body", screen._palette) def test_explicit_event_loop_used_directly(self): """An explicit event_loop is stored as-is, without constructing a default SelectEventLoop.""" with ClosingScreenIO() as (screen_input, screen_output): screen = urwid.display.raw.Screen(input=screen_input, output=screen_output) loop = urwid.SelectEventLoop() evl = urwid.MainLoop(urwid.SolidFill(), screen=screen, event_loop=loop) self.assertIs(loop, evl.event_loop) def test_widget_setter_updates_pop_up_target(self): """Replacing widget while pop_ups is enabled updates the PopUpTarget's original_widget.""" first = urwid.SolidFill(".") second = urwid.SolidFill("#") with dummy_raw_main_loop(first, pop_ups=True) as evl: self.assertIs(first, evl._topmost_widget.original_widget) evl.widget = second self.assertIs(second, evl._topmost_widget.original_widget) def test_set_alarm_at(self): """set_alarm_at schedules a callback at an absolute time.""" seen: list[object] = [] def on_alarm(loop: urwid.MainLoop, user_data: object) -> typing.NoReturn: seen.append(user_data) raise urwid.ExitMainLoop with dummy_raw_main_loop() as evl: evl.set_alarm_at(time.time() + 0.01, on_alarm, user_data="at") evl.run() self.assertEqual(["at"], seen) def test_set_alarm_at_async_callback(self): """An async def callback is awaited, not silently dropped, on a loop that supports it.""" seen: list[object] = [] async def on_alarm(loop: urwid.MainLoop, user_data: object) -> typing.NoReturn: seen.append(user_data) raise urwid.ExitMainLoop with dummy_raw_main_loop(event_loop=urwid.AsyncioEventLoop()) as evl: evl.set_alarm_at(time.time() + 0.01, on_alarm, user_data="at") evl.run() self.assertEqual(["at"], seen) @unittest.skipIf(IS_WINDOWS, "selectors for pipe are not supported on Windows") @unittest.skipIf(IS_GRAALPY, "fcntl.fcntl is missing on GraalPy") def test_watch_pipe_async_callback(self): """An async def watch_pipe callback is awaited, and its return value still controls removal.""" outcome: list[bytes] = [] async def pipe_cb(data: bytes) -> bool | None: outcome.append(data) return False with dummy_raw_main_loop(event_loop=urwid.AsyncioEventLoop()) as evl: pipe_fd = evl.watch_pipe(pipe_cb) os.write(pipe_fd, b"hi") evl.set_alarm_in(0.05, stop_screen_cb) evl.run() self.assertEqual([b"hi"], outcome) # already removed by the callback returning False, so a second removal fails self.assertFalse(evl.remove_watch_pipe(pipe_fd)) @unittest.skipIf(IS_WINDOWS, "selectors for pipe are not supported on Windows") @unittest.skipIf(IS_GRAALPY, "fcntl.fcntl is missing on GraalPy") def test_remove_watch_pipe_missing_fd(self): """Removing a watch pipe that was never created returns False.""" with dummy_raw_main_loop() as evl: self.assertFalse(evl.remove_watch_pipe(999999)) @unittest.skipIf(IS_WINDOWS, "selectors for pipe are not supported on Windows") @unittest.skipIf(IS_GRAALPY, "fcntl.fcntl is missing on GraalPy") def test_remove_watch_pipe_active(self): """Removing a still-active watch pipe succeeds once and fails on a second attempt.""" with dummy_raw_main_loop() as evl: pipe_fd = evl.watch_pipe(lambda data: None) self.assertTrue(evl.remove_watch_pipe(pipe_fd)) self.assertFalse(evl.remove_watch_pipe(pipe_fd)) def test_watch_file_and_remove_watch_file(self): """watch_file and remove_watch_file delegate directly to the event loop.""" with dummy_raw_main_loop() as evl: pipe_rd, pipe_wr = os.pipe() try: handle = evl.watch_file(pipe_rd, lambda: None) self.assertTrue(evl.remove_watch_file(handle)) self.assertFalse(evl.remove_watch_file(handle)) finally: os.close(pipe_rd) os.close(pipe_wr) def test_start_sets_mouse_tracking_when_handled(self): """start() enables mouse tracking on the screen when handle_mouse is True.""" with ClosingScreenIO() as (screen_input, screen_output): screen = urwid.display.raw.Screen(input=screen_input, output=screen_output) calls: list[bool] = [] screen.set_mouse_tracking = lambda enable=True: calls.append(enable) evl = urwid.MainLoop(urwid.SolidFill(), screen=screen, handle_mouse=True) with evl.start(): pass self.assertEqual([True], calls) def test_start_raises_when_screen_lacks_external_loop_support(self): """start() raises CantUseExternalLoop for a screen without hook_event_loop.""" evl = urwid.MainLoop(urwid.SolidFill(), screen=NonExternalLoopScreen()) with self.assertRaises(main_loop_module.CantUseExternalLoop): evl.start() def test_run_uses_screen_event_loop_when_unsupported(self): """run() falls back to _run_screen_event_loop for a screen without external loop support.""" screen = NonExternalLoopScreen() evl = urwid.MainLoop(urwid.SolidFill(), screen=screen, handle_mouse=False) evl.set_alarm_in(0, stop_screen_cb) evl.run() self.assertGreaterEqual(screen.input_calls, 1) self.assertFalse(screen.started) def test_run_screen_event_loop_processes_keys_without_alarms(self): """_run_screen_event_loop handles real keys and a resize with no alarms scheduled.""" screen = ScriptedInputScreen([([], []), (["window resize"], []), (["quit"], [])]) def unhandled(data: str) -> bool | None: if data == "quit": raise urwid.ExitMainLoop return False evl = urwid.MainLoop(urwid.SolidFill(), screen=screen, handle_mouse=False, unhandled_input=unhandled) evl.run() self.assertGreaterEqual(screen.input_calls, 3) def test_run_screen_event_loop_processes_multiple_alarms(self): """_run_screen_event_loop pops and fires alarms one at a time across outer iterations.""" order: list[str] = [] def cb_a(loop: urwid.MainLoop, data: object) -> None: order.append("a") def cb_b(loop: urwid.MainLoop, data: object) -> typing.NoReturn: order.append("b") raise urwid.ExitMainLoop screen = NonExternalLoopScreen() evl = urwid.MainLoop(urwid.SolidFill(), screen=screen, handle_mouse=False) evl.set_alarm_in(0, cb_a) evl.set_alarm_in(0.03, cb_b) evl.run() self.assertEqual(["a", "b"], order) def test_run_screen_event_loop_clears_next_alarm_when_queue_empty(self): """_run_screen_event_loop resets next_alarm to None once the alarm queue drains.""" order: list[str] = [] def cb_a(loop: urwid.MainLoop, data: object) -> None: order.append("a") screen = ScriptedInputScreen([([], []), (["quit"], [])]) def unhandled(data: str) -> bool | None: if data == "quit": raise urwid.ExitMainLoop return False evl = urwid.MainLoop(urwid.SolidFill(), screen=screen, handle_mouse=False, unhandled_input=unhandled) evl.set_alarm_in(0, cb_a) evl.run() self.assertEqual(["a"], order) def test_run_reraises_unexpected_event_loop_error(self): """run() removes its event loop hooks, stops the screen and lets an unexpected event loop error propagate.""" with dummy_raw_main_loop() as evl: with unittest.mock.patch.object(evl.event_loop, "run", side_effect=RuntimeError("boom")): self.assertRaises(RuntimeError, evl.run) self.assertFalse(evl.screen.started) self.assertIsNone(evl.idle_handle) def test_update_processes_keys_and_resets_screen_size_on_resize(self): """_update runs process_input and clears screen_size on a window resize event.""" with dummy_raw_main_loop() as evl: evl.screen_size = (10, 5) evl._update(["window resize"], []) self.assertIsNone(evl.screen_size) def test_process_input_computes_screen_size(self): """process_input queries the screen size when none is cached yet.""" with dummy_raw_main_loop() as evl: self.assertIsNone(evl.screen_size) evl.process_input([]) self.assertIsNotNone(evl.screen_size) def test_process_input_window_resize(self): """A 'window resize' key is skipped without being treated as handled.""" with dummy_raw_main_loop() as evl: evl.screen_size = (10, 5) self.assertFalse(evl.process_input(["window resize"])) def test_process_input_keypress_handled_by_widget(self): """A key fully consumed by the widget's keypress marks input as handled.""" widget = RecordingWidget(selectable=True, keypress_return=None) with dummy_raw_main_loop(widget) as evl: evl.screen_size = (10, 5) self.assertTrue(evl.process_input(["a"])) self.assertEqual(["a"], widget.keypress_calls) def test_process_input_redraw_screen_command(self): """An unhandled key mapped to REDRAW_SCREEN clears the screen.""" widget = RecordingWidget(selectable=True, keypress_return="ctrl l") cleared: list[bool] = [] with dummy_raw_main_loop(widget) as evl: evl.screen_size = (10, 5) evl.screen.clear = lambda: cleared.append(True) self.assertTrue(evl.process_input(["ctrl l"])) self.assertEqual([True], cleared) def test_process_input_mouse_event(self): """A mouse event handled by the widget marks input as handled.""" widget = RecordingWidget(selectable=False, mouse_return=True) with dummy_raw_main_loop(widget) as evl: evl.screen_size = (10, 5) self.assertTrue(evl.process_input([("mouse press", 1, 5, 4)])) self.assertEqual([("mouse press", 1, 5, 4)], widget.mouse_calls) def test_process_input_invalid_key_raises_type_error(self): """A key that is neither a string nor a mouse event tuple raises TypeError.""" with dummy_raw_main_loop() as evl: evl.screen_size = (10, 5) with self.assertRaises(TypeError): evl.process_input([123]) def test_process_input_empty_key(self): """A falsy key value is still counted as handled.""" with dummy_raw_main_loop() as evl: evl.screen_size = (10, 5) self.assertTrue(evl.process_input([""])) def test_input_filter_uses_constructor_callback(self): """input_filter delegates to the function passed to the constructor.""" calls: list[tuple[list[str], list[int]]] = [] def custom_filter(keys: list[str], raw: list[int]) -> list[str]: calls.append((keys, raw)) return keys[:1] with dummy_raw_main_loop(input_filter=custom_filter) as evl: result = evl.input_filter(["a", "b"], [1, 2]) self.assertEqual(["a"], result) self.assertEqual([(["a", "b"], [1, 2])], calls) def test_unhandled_input_default_returns_false(self): """unhandled_input returns False when no callback was passed to the constructor.""" with dummy_raw_main_loop() as evl: self.assertFalse(evl.unhandled_input("x")) def test_entering_idle_skips_draw_when_screen_not_started(self): """entering_idle does not draw the screen before it has been started.""" with dummy_raw_main_loop() as evl: self.assertFalse(evl.screen.started) evl.entering_idle() self.assertIsNone(evl.screen_size) def test_refl_loop_exit_raises_exit_main_loop(self): """_refl(..., loop_exit=True) raises ExitMainLoop when called.""" reflected = main_loop_module._refl("scr", loop_exit=True) buf = io.StringIO() with contextlib.redirect_stdout(buf), self.assertRaises(urwid.ExitMainLoop): reflected() def test_module_doctests_pass(self): """The module's own doctests (covering the _refl test helper) run cleanly.""" buf = io.StringIO() with contextlib.redirect_stdout(buf): results = doctest.testmod(main_loop_module, verbose=False) self.assertEqual(0, results.failed) def test_test_function_runs_doctests(self): """main_loop._test() runs the module doctests via doctest.testmod().""" buf = io.StringIO() with contextlib.redirect_stdout(buf): main_loop_module._test() urwid-urwid-69907e4/tests/test_signals.py000066400000000000000000000156741526073734300205270ustar00rootroot00000000000000from __future__ import annotations import gc import sys import sysconfig import typing import unittest import weakref from unittest.mock import Mock from urwid import ( Edit, MetaSignals, Signals, SimpleListWalker, Widget, connect_signal, disconnect_signal, disconnect_signal_by_key, emit_signal, register_signal, ) if typing.TYPE_CHECKING: from collections.abc import Callable class SiglnalsTest(unittest.TestCase): @classmethod def setUpClass(cls): cls.EmClass = type("EmClass", (object,), {}) register_signal(cls.EmClass, ["change", "test"]) def test_connect(self): obj = Mock() handler = Mock() edit = Edit("") key = connect_signal(edit, "change", handler, user_args=[obj]) self.assertIsNotNone(key) edit.set_edit_text("long test text") handler.assert_called_once_with(obj, edit, "long test text") handler.reset_mock() disconnect_signal(edit, "change", handler, user_args=[obj]) edit.set_edit_text("another text") handler.assert_not_called() def test_handler_mutation_during_emit(self): emitter = self.EmClass() calls = [] def third(): calls.append("third") def first(): calls.append("first") disconnect_signal(emitter, "test", first) connect_signal(emitter, "test", third) def second(): calls.append("second") connect_signal(emitter, "test", first) connect_signal(emitter, "test", second) emit_signal(emitter, "test") self.assertEqual(calls, ["first", "second"]) emit_signal(emitter, "test") self.assertEqual(calls, ["first", "second", "second", "third"]) @unittest.skipIf( sys.implementation.name in {"pypy", "graalpy"}, "WeakRef works differently on PyPy/GraalPy's tracing GC", ) def test_weak_del(self): emitter = SiglnalsTest.EmClass() w1 = Mock(name="w1") w2 = Mock(name="w2") w3 = Mock(name="w3") handler1 = Mock(name="handler1") handler2 = Mock(name="handler2") k1 = connect_signal(emitter, "test", handler1, weak_args=[w1], user_args=[42, "abc"]) k2 = connect_signal(emitter, "test", handler2, weak_args=[w2, w3], user_args=[8]) self.assertIsNotNone(k2) emit_signal(emitter, "test", "Foo") handler1.assert_called_once_with(w1, 42, "abc", "Foo") handler2.assert_called_once_with(w2, w3, 8, "Foo") handler1.reset_mock() handler2.reset_mock() del w1 self.assertEqual( len(getattr(emitter, Signals._signal_attr)["test"]), 1, getattr(emitter, Signals._signal_attr)["test"], ) emit_signal(emitter, "test", "Bar") handler1.assert_not_called() handler2.assert_called_once_with(w2, w3, 8, "Bar") handler2.reset_mock() del w3 emit_signal(emitter, "test", "Baz") handler1.assert_not_called() handler2.assert_not_called() self.assertEqual(len(getattr(emitter, Signals._signal_attr)["test"]), 0) del w2 class RegistryLeakTest(unittest.TestCase): """The signal registry does not keep the classes it knows alive.""" @unittest.skipIf( sys.version_info[:2] == (3, 13) and sysconfig.get_config_var("Py_GIL_DISABLED"), "the free-threaded CPython 3.13 build makes every class immortal once a thread has been started", ) def test_runtime_classes_are_collected(self) -> None: """Classes created at run time are freed once nothing else refers to them.""" # Not a Widget subclass: GraalPy itself can keep a runtime Widget subclass alive. refs = [weakref.ref(MetaSignals("Runtime", (), {"signals": ["ping"]})) for _ in range(20)] gc.collect() self.assertEqual([], [ref for ref in refs if ref() is not None]) def test_live_classes_and_subclasses_keep_their_signals(self) -> None: """A live class created at run time, and its subclass, still connect and emit their signals.""" base = type("Base", (Widget,), {"signals": ["ping"]}) sub = type("Sub", (base,), {}) gc.collect() for cls in (base, sub): with self.subTest(cls=cls): obj = cls() handler = Mock() connect_signal(obj, "ping", handler) emit_signal(obj, "ping", 1) handler.assert_called_once_with(1) with self.assertRaises(NameError): connect_signal(obj, "change", handler) class _GetHook(dict): """Handler storage that calls ``hook`` once, right after the next lookup of a signal's handlers.""" def __init__(self) -> None: """Start with no hook set.""" super().__init__() self.hook: Callable[[], object] | None = None def get(self, *args: typing.Any) -> typing.Any: result = super().get(*args) hook, self.hook = self.hook, None if hook is not None: hook() return result @unittest.skipUnless(sys.implementation.name == "cpython", "relies on reference counting to free the weak argument") class WeakCallbackRaceTest(unittest.TestCase): """A handler removed by its weak argument's callback stays removed when the callback interrupts an update.""" def setUp(self) -> None: """Connect a handler whose weak argument is held only by ``self.holder``.""" self.emitter = SiglnalsTest.EmClass() self.signals = _GetHook() setattr(self.emitter, Signals._signal_attr, self.signals) self.holder = [Mock(name="weak")] connect_signal(self.emitter, "test", Mock(name="doomed"), weak_args=self.holder) self.survivor = Mock(name="survivor") def test_connect(self) -> None: """Connecting a handler does not restore the one removed while the handlers were being read.""" self.signals.hook = self.holder.clear connect_signal(self.emitter, "test", self.survivor) self.assertEqual([self.survivor], [h[1] for h in self.signals["test"]]) def test_disconnect_by_key(self) -> None: """Disconnecting a handler does not restore the one removed while the handlers were being read.""" connect_signal(self.emitter, "test", self.survivor) key = connect_signal(self.emitter, "test", Mock(name="removed")) self.signals.hook = self.holder.clear disconnect_signal_by_key(self.emitter, "test", key) self.assertEqual([self.survivor], [h[1] for h in self.signals["test"]]) def test_falsy_emitter(self) -> None: """A handler on an emitter that is false in a boolean context is removed as soon as its weak argument dies.""" emitter = SimpleListWalker([]) holder = [Mock(name="weak")] connect_signal(emitter, "modified", Mock(name="doomed"), weak_args=holder) holder.clear() self.assertEqual((), getattr(emitter, Signals._signal_attr)["modified"]) urwid-urwid-69907e4/tests/test_str_util.py000066400000000000000000000071501526073734300207220ustar00rootroot00000000000000from __future__ import annotations import unittest from urwid.display.escape import str_util class DecodeOneTest(unittest.TestCase): def gwt(self, ch, exp_ord, exp_pos): ch = ch.encode("iso8859-1") o, pos = str_util.decode_one(ch, 0) assert o == exp_ord, f" got:{o!r} expected:{exp_ord!r}" assert pos == exp_pos, f" got:{pos!r} expected:{exp_pos!r}" def test1byte(self): self.gwt("ab", ord("a"), 1) self.gwt("\xc0a", ord("?"), 1) # error def test2byte(self): self.gwt("\xc2", ord("?"), 1) # error self.gwt("\xc0\x80", ord("?"), 1) # error self.gwt("\xc2\x80", 0x80, 2) self.gwt("\xdf\xbf", 0x7FF, 2) def test3byte(self): self.gwt("\xe0", ord("?"), 1) # error self.gwt("\xe0\xa0", ord("?"), 1) # error self.gwt("\xe0\x90\x80", ord("?"), 1) # error self.gwt("\xe0\xa0\x80", 0x800, 3) self.gwt("\xef\xbf\xbf", 0xFFFF, 3) def test4byte(self): self.gwt("\xf0", ord("?"), 1) # error self.gwt("\xf0\x90", ord("?"), 1) # error self.gwt("\xf0\x90\x80", ord("?"), 1) # error self.gwt("\xf0\x80\x80\x80", ord("?"), 1) # error self.gwt("\xf0\x90\x80\x80", 0x10000, 4) self.gwt("\xf3\xbf\xbf\xbf", 0xFFFFF, 4) def test_str_input(self): """decode_one's str branch treats each character as one raw byte value (via ord()), the same way gwt()'s bytes are built from a str via .encode("iso8859-1"); passing the str literal directly (instead of encoding it first) exercises that branch with the same byte-equivalent inputs used in test1byte..test4byte. """ self.assertEqual((ord("a"), 1), str_util.decode_one("ab", 0)) self.assertEqual((0x7FF, 2), str_util.decode_one("\xdf\xbf", 0)) self.assertEqual((0x800, 3), str_util.decode_one("\xe0\xa0\x80", 0)) self.assertEqual((0x10000, 4), str_util.decode_one("\xf0\x90\x80\x80", 0)) def test_invalid_continuation_byte(self): """A malformed (non-0x80-0xBF) continuation byte is an error, distinct from a short sequence or an overlong encoding -- regression coverage for the lazily-read b2/b3/b4 refactor, since each continuation check now lives in its own guarded block. """ self.gwt("\xc2A", ord("?"), 1) # 2-byte: bad b2 self.gwt("\xe0\xa0A", ord("?"), 1) # 3-byte: bad b3 (b2 valid) self.gwt("\xe0AA", ord("?"), 1) # 3-byte: bad b2 self.gwt("\xf0\x90\x80A", ord("?"), 1) # 4-byte: bad b4 (b2, b3 valid) self.gwt("\xf0\x90AA", ord("?"), 1) # 4-byte: bad b3 self.gwt("\xf0AAA", ord("?"), 1) # 4-byte: bad b2 def test_invalid_leading_byte(self): """A leading byte matching none of the 1/2/3/4-byte patterns (e.g. a stray continuation byte, or 0xF8-0xFF) falls through to the final error case. """ self.gwt("\xff\x80\x80\x80", ord("?"), 1) self.gwt("\xf8\x80\x80\x80", ord("?"), 1) def test_out_of_range_pos_raises_value_error(self): with self.assertRaises(ValueError): str_util.decode_one(b"ab", 5) with self.assertRaises(ValueError): str_util.decode_one("ab", 5) class MovePrevCharTest(unittest.TestCase): """Moving back by one grapheme cluster within a range of a string.""" def test_cluster_starting_before_start_offs(self): """A space and a combining mark form one cluster: moving back from inside the range stops at start_offs.""" self.assertEqual(1, str_util.move_prev_char(" \u0301ab", 1, 2)) self.assertEqual(0, str_util.move_prev_char(" \u0301ab", 0, 2)) urwid-urwid-69907e4/tests/test_text_layout.py000066400000000000000000001400221526073734300214320ustar00rootroot00000000000000from __future__ import annotations import sys import threading import typing import unittest import unittest.mock import wcwidth import urwid from urwid import canvas, text_layout from urwid.util import get_encoding, set_temporary_encoding if typing.TYPE_CHECKING: from collections.abc import Callable from typing_extensions import Literal class CalcBreaksTest(unittest.TestCase): def cbtest(self, width, exp, mode, text): result = text_layout.default_layout.calculate_text_segments(text, width, mode) self.assertEqual(len(exp), len(result), f"Expected: {exp!r}, got {result!r}") for l, e in zip(result, exp): end = l[-1][-1] self.assertEqual(e, end, f"Expected: {exp!r}, got {result!r}") def validate(self, mode, text, do): for width, exp in do: self.cbtest(width, exp, mode, text) def test_calc_breaks_char(self): self.validate( mode="any", text=b"abfghsdjf askhtrvs\naltjhgsdf ljahtshgf", do=[ (100, [18, 38]), (6, [6, 12, 18, 25, 31, 37, 38]), (10, [10, 18, 29, 38]), ], ) def test_calc_reaks_db_char(self): with urwid.util.set_temporary_encoding("euc-jp"): self.validate( mode="any", text=b"abfgh\xa1\xa1j\xa1\xa1xskhtrvs\naltjhgsdf\xa1\xa1jahtshgf", do=[ (10, [10, 18, 28, 38]), (6, [5, 11, 17, 18, 25, 31, 37, 38]), (100, [18, 38]), ], ) def test_calc_breaks_word(self): self.validate( mode="space", text=b"hello world\nout there. blah", do=[ (10, [5, 11, 22, 27]), (5, [5, 11, 17, 22, 27]), (100, [11, 27]), ], ) def test_calc_breaks_word_2(self): self.validate( mode="space", text=b"A simple set of words, really....", do=[ (10, [8, 15, 22, 33]), (17, [15, 33]), (13, [12, 22, 33]), ], ) def test_calc_breaks_db_word(self): with urwid.util.set_temporary_encoding("euc-jp"): self.validate( mode="space", text=b"hel\xa1\xa1 world\nout-\xa1\xa1tre blah", # tests do=[ (10, [5, 11, 21, 26]), (5, [5, 11, 16, 21, 26]), (100, [11, 26]), ], ) def test_calc_breaks_utf8(self): with urwid.util.set_temporary_encoding("utf-8"): self.validate( mode="space", # As text: "替洼渎�潺" text=b"\xe6\x9b\xbf\xe6\xb4\xbc\xe6\xb8\x8e\xe6\xba\x8f\xe6\xbd\xba", do=[ (4, [6, 12, 15]), (10, [15]), (5, [6, 12, 15]), ], ) class CalcBreaksCantDisplayTest(unittest.TestCase): def test(self): with set_temporary_encoding("euc-jp"): self.assertRaises( text_layout.CanNotDisplayText, text_layout.default_layout.calculate_text_segments, b"\xa1\xa1", 1, "space", ) with set_temporary_encoding("utf-8"): self.assertRaises( text_layout.CanNotDisplayText, text_layout.default_layout.calculate_text_segments, "颖", 1, "space", ) class SubsegTest(unittest.TestCase): def setUp(self): self.old_encoding = get_encoding() urwid.set_encoding("euc-jp") def tearDown(self) -> None: urwid.set_encoding(self.old_encoding) def st(self, seg, text: bytes, start: int, end: int, exp): s = urwid.LayoutSegment(seg) result = s.subseg(text, start, end) self.assertEqual(exp, result, f"Expected {exp!r}, got {result!r}") def test1_padding(self): self.st((10, None), b"", 0, 8, [(8, None)]) self.st((10, None), b"", 2, 10, [(8, None)]) self.st((10, 0), b"", 3, 7, [(4, 0)]) self.st((10, 0), b"", 0, 20, [(10, 0)]) def test2_text(self): self.st((10, 0, b"1234567890"), b"", 0, 8, [(8, 0, b"12345678")]) self.st((10, 0, b"1234567890"), b"", 2, 10, [(8, 0, b"34567890")]) self.st((10, 0, b"12\xa1\xa156\xa1\xa190"), b"", 2, 8, [(6, 0, b"\xa1\xa156\xa1\xa1")]) self.st((10, 0, b"12\xa1\xa156\xa1\xa190"), b"", 3, 8, [(5, 0, b" 56\xa1\xa1")]) self.st((10, 0, b"12\xa1\xa156\xa1\xa190"), b"", 2, 7, [(5, 0, b"\xa1\xa156 ")]) self.st((10, 0, b"12\xa1\xa156\xa1\xa190"), b"", 3, 7, [(4, 0, b" 56 ")]) self.st((10, 0, b"12\xa1\xa156\xa1\xa190"), b"", 0, 20, [(10, 0, b"12\xa1\xa156\xa1\xa190")]) def test3_range(self): t = b"1234567890" self.st((10, 0, 10), t, 0, 8, [(8, 0, 8)]) self.st((10, 0, 10), t, 2, 10, [(8, 2, 10)]) self.st((6, 2, 8), t, 1, 6, [(5, 3, 8)]) self.st((6, 2, 8), t, 0, 5, [(5, 2, 7)]) self.st((6, 2, 8), t, 1, 5, [(4, 3, 7)]) t = b"12\xa1\xa156\xa1\xa190" self.st((10, 0, 10), t, 0, 8, [(8, 0, 8)]) self.st((10, 0, 10), t, 2, 10, [(8, 2, 10)]) self.st((6, 2, 8), t, 1, 6, [(1, 2), (4, 4, 8)]) self.st((6, 2, 8), t, 0, 5, [(4, 2, 6), (1, 6)]) self.st((6, 2, 8), t, 1, 5, [(1, 2), (2, 4, 6), (1, 6)]) def test4_range_inside_a_wide_character(self): """A window that lands inside a wide character is padding pointing at the start of that character.""" t = b"12\xa1\xa156\xa1\xa190" self.st((10, 0, 10), t, 2, 3, [(1, 2)]) self.st((10, 0, 10), t, 3, 4, [(1, 2)]) self.st((10, 0, 10), t, 7, 8, [(1, 6)]) self.st((6, 2, 8), t, 1, 2, [(1, 2)]) class NarrowWideCharacterRenderTest(unittest.TestCase): """Rendering wide characters into an area too narrow to hold them.""" CJK = "你好世界" def render(self, widget, size, focus: bool = False) -> list[str]: return [line.decode("utf-8") for line in widget.render(size, focus).text] def test_clipped_on_both_edges(self): with set_temporary_encoding("utf-8"): self.assertEqual([" "], self.render(urwid.Text(self.CJK, wrap="clip"), (1,))) self.assertEqual(["你 "], self.render(urwid.Text(self.CJK, wrap="clip"), (3,))) self.assertEqual([" "], self.render(urwid.Text(self.CJK, align="center", wrap="clip"), (2,))) self.assertEqual([" "], self.render(urwid.Text(self.CJK, align="right", wrap="clip"), (1,))) def test_inside_a_line_box(self): with set_temporary_encoding("utf-8"): self.assertEqual( ["┌─�", "│ │", "└─┘"], self.render(urwid.LineBox(urwid.Text(self.CJK, wrap="clip")), (3,)), ) def test_shifted_left_by_padding(self): with set_temporary_encoding("utf-8"): self.assertEqual( [" "], self.render(urwid.Padding(urwid.Text(self.CJK, wrap="clip"), left=-1), (1,)), ) def test_every_narrow_width_renders(self): with set_temporary_encoding("utf-8"): for align in (urwid.Align.LEFT, urwid.Align.CENTER, urwid.Align.RIGHT): for width in range(1, 10): with self.subTest(align=align, width=width): canvas = urwid.Text(self.CJK, align=align, wrap=urwid.WrapMode.CLIP).render((width,)) self.assertEqual(width, canvas.cols()) class CalcTranslateTest(unittest.TestCase): def setUp(self) -> None: self.old_encoding = get_encoding() urwid.set_encoding("utf-8") def tearDown(self) -> None: urwid.set_encoding(self.old_encoding) def check(self, text, mode, width, result_left, result_center, result_right) -> None: with self.subTest("left"): result = urwid.default_layout.layout(text, width, "left", mode) self.assertEqual( result_left, result, f"For {text=} {width=} {mode=} got {result!r} instead of {result_left!r}", ) with self.subTest("center"): result = urwid.default_layout.layout(text, width, "center", mode) self.assertEqual( result_center, result, f"For {text=} {width=} {mode=} got {result!r} instead of {result_center!r}", ) with self.subTest("right"): result = urwid.default_layout.layout(text, width, "right", mode) self.assertEqual( result_right, result, f"For {text=} {width=} {mode=} got {result!r} instead of {result_right!r}", ) def test_calc_translate_char(self): self.check( text="It's out of control!\nYou've got to", mode="any", width=15, result_left=[[(15, 0, 15)], [(5, 15, 20), (0, 20)], [(13, 21, 34), (0, 34)]], result_center=[[(15, 0, 15)], [(5, None), (5, 15, 20), (0, 20)], [(1, None), (13, 21, 34), (0, 34)]], result_right=[[(15, 0, 15)], [(10, None), (5, 15, 20), (0, 20)], [(2, None), (13, 21, 34), (0, 34)]], ) def test_calc_translate_word(self): self.check( text="It's out of control!\nYou've got to", mode="space", width=14, result_left=[ [(11, 0, 11), (0, 11)], [(8, 12, 20), (0, 20)], [(13, 21, 34), (0, 34)], ], result_center=[ [(2, None), (11, 0, 11), (0, 11)], [(3, None), (8, 12, 20), (0, 20)], [(1, None), (13, 21, 34), (0, 34)], ], result_right=[ [(3, None), (11, 0, 11), (0, 11)], [(6, None), (8, 12, 20), (0, 20)], [(1, None), (13, 21, 34), (0, 34)], ], ) def test_calc_translate(self): self.check( text="It's out of control!\nYou've got to ", mode="space", width=14, result_left=[ [(11, 0, 11), (0, 11)], [(8, 12, 20), (0, 20)], [(14, 21, 35), (0, 35)], ], result_center=[ [(2, None), (11, 0, 11), (0, 11)], [(3, None), (8, 12, 20), (0, 20)], [(14, 21, 35), (0, 35)], ], result_right=[ [(3, None), (11, 0, 11), (0, 11)], [(6, None), (8, 12, 20), (0, 20)], [(14, 21, 35), (0, 35)], ], ) def test_calc_translate_word_2(self): self.check( text="It's out of control!\nYou've got to ", mode="space", width=14, result_left=[[(11, 0, 11), (0, 11)], [(8, 12, 20), (0, 20)], [(14, 21, 35), (0, 35)]], result_center=[ [(2, None), (11, 0, 11), (0, 11)], [(3, None), (8, 12, 20), (0, 20)], [(14, 21, 35), (0, 35)], ], result_right=[ [(3, None), (11, 0, 11), (0, 11)], [(6, None), (8, 12, 20), (0, 20)], [(14, 21, 35), (0, 35)], ], ) def test_calc_translate_word_3(self): # As bytes: b'\xe6\x9b\xbf\xe6\xb4\xbc\n\xe6\xb8\x8e\xe6\xba\x8f\xe6\xbd\xba' # Decoded as UTF-8: "替洼\n渎�潺" self.check( text=b"\xe6\x9b\xbf\xe6\xb4\xbc\n\xe6\xb8\x8e\xe6\xba\x8f\xe6\xbd\xba", width=10, mode="space", result_left=[[(4, 0, 6), (0, 6)], [(6, 7, 16), (0, 16)]], result_center=[[(3, None), (4, 0, 6), (0, 6)], [(2, None), (6, 7, 16), (0, 16)]], result_right=[[(6, None), (4, 0, 6), (0, 6)], [(4, None), (6, 7, 16), (0, 16)]], ) def test_calc_translate_word_3_decoded(self): # As bytes: b'\xe6\x9b\xbf\xe6\xb4\xbc\n\xe6\xb8\x8e\xe6\xba\x8f\xe6\xbd\xba' # Decoded as UTF-8: "替洼\n渎�潺" self.check( text="替洼\n渎�潺", width=10, mode="space", result_left=[[(4, 0, 2), (0, 2)], [(6, 3, 6), (0, 6)]], result_center=[[(3, None), (4, 0, 2), (0, 2)], [(2, None), (6, 3, 6), (0, 6)]], result_right=[[(6, None), (4, 0, 2), (0, 2)], [(4, None), (6, 3, 6), (0, 6)]], ) def test_calc_translate_word_4(self): self.check( text=" Die Gedank", width=3, mode="space", result_left=[[(0, 0)], [(3, 1, 4), (0, 4)], [(3, 5, 8)], [(3, 8, 11), (0, 11)]], result_center=[[(2, None), (0, 0)], [(3, 1, 4), (0, 4)], [(3, 5, 8)], [(3, 8, 11), (0, 11)]], result_right=[[(3, None), (0, 0)], [(3, 1, 4), (0, 4)], [(3, 5, 8)], [(3, 8, 11), (0, 11)]], ) def test_calc_translate_word_5(self): self.check( text=" Word.", width=3, mode="space", result_left=[[(3, 0, 3)], [(3, 3, 6), (0, 6)]], result_center=[[(3, 0, 3)], [(3, 3, 6), (0, 6)]], result_right=[[(3, 0, 3)], [(3, 3, 6), (0, 6)]], ) def test_calc_translate_clip(self): self.check( text="It's out of control!\nYou've got to\n\nturn it off!!!", mode="clip", width=14, result_left=[ [(20, 0, 20), (0, 20)], [(13, 21, 34), (0, 34)], [(0, 35)], [(14, 36, 50), (0, 50)], ], result_center=[ [(-3, None), (20, 0, 20), (0, 20)], [(1, None), (13, 21, 34), (0, 34)], [(7, None), (0, 35)], [(14, 36, 50), (0, 50)], ], result_right=[ [(-6, None), (20, 0, 20), (0, 20)], [(1, None), (13, 21, 34), (0, 34)], [(14, None), (0, 35)], [(14, 36, 50), (0, 50)], ], ) def test_calc_translate_clip_2(self): self.check( text="Hello!\nto\nWorld!", mode="clip", width=5, # line width (of first and last lines) minus one result_left=[ [(6, 0, 6), (0, 6)], [(2, 7, 9), (0, 9)], [(6, 10, 16), (0, 16)], ], result_center=[ [(6, 0, 6), (0, 6)], [(2, None), (2, 7, 9), (0, 9)], [(6, 10, 16), (0, 16)], ], result_right=[ [(-1, None), (6, 0, 6), (0, 6)], [(3, None), (2, 7, 9), (0, 9)], [(-1, None), (6, 10, 16), (0, 16)], ], ) def test_calc_translate_cant_display(self): self.check( text="Hello颖", mode="space", width=1, result_left=[[]], result_center=[[]], result_right=[[]], ) class CalcPosTest(unittest.TestCase): def setUp(self): self.text = "A" * 27 self.trans = [[(2, None), (7, 0, 7), (0, 7)], [(13, 8, 21), (0, 21)], [(3, None), (5, 22, 27), (0, 27)]] self.mytests = [ (1, 0, 0), (2, 0, 0), (11, 0, 7), (-3, 1, 8), (-2, 1, 8), (1, 1, 9), (31, 1, 21), (1, 2, 22), (11, 2, 27), ] def tests(self): for x, y, expected in self.mytests: got = text_layout.calc_pos(self.text, self.trans, x, y) self.assertEqual(expected, got, f"{x, y!r} got:{got!r} expected:{expected!r}") class Pos2CoordsTest(unittest.TestCase): pos_list = [5, 9, 20, 26] text = "1234567890" * 3 mytests = [ ([[(15, 0, 15)], [(15, 15, 30), (0, 30)]], [(5, 0), (9, 0), (5, 1), (11, 1)]), ([[(9, 0, 9)], [(12, 9, 21)], [(9, 21, 30), (0, 30)]], [(5, 0), (0, 1), (11, 1), (5, 2)]), ([[(2, None), (15, 0, 15)], [(2, None), (15, 15, 30), (0, 30)]], [(7, 0), (11, 0), (7, 1), (13, 1)]), ([[(3, 6, 9), (0, 9)], [(5, 20, 25), (0, 25)]], [(0, 0), (3, 0), (0, 1), (5, 1)]), ([[(10, 0, 10), (0, 10)]], [(5, 0), (9, 0), (10, 0), (10, 0)]), ] def test(self): for t, answer in self.mytests: for pos, a in zip(self.pos_list, answer): r = text_layout.calc_coords(self.text, t, pos) self.assertEqual(a, r, f"{t!r} got: {r!r} expected: {a!r}") class TestEllipsis(unittest.TestCase): def test_ellipsis_encoding_support(self): widget = urwid.Text("Test label", wrap=urwid.WrapMode.ELLIPSIS) with self.subTest("Unicode"), set_temporary_encoding("utf-8"): widget._invalidate() canvas = widget.render((5,)) self.assertEqual("Test…", str(canvas)) with self.subTest("ascii"), set_temporary_encoding("ascii"): widget._invalidate() canvas = widget.render((5,)) self.assertEqual("Te...", str(canvas)) with self.subTest("ascii not fit"), set_temporary_encoding("ascii"): widget._invalidate() canvas = widget.render((3,)) self.assertEqual("T..", str(canvas)) with self.subTest("ascii nothing fit"), set_temporary_encoding("ascii"): widget._invalidate() canvas = widget.render((1,)) self.assertEqual("T", str(canvas)) def test_ellipsis_multichar_alignment(self): """A multi-character ellipsis has to be counted at its real width. The trimmed text plus the ellipsis fills all the available columns, so `align` has nothing left to distribute and every alignment renders alike. """ for encoding, expected in (("utf-8", "Test…"), ("ascii", "Te...")): for align in (urwid.Align.LEFT, urwid.Align.CENTER, urwid.Align.RIGHT): with self.subTest(encoding=encoding, align=align), set_temporary_encoding(encoding): widget = urwid.Text("Test label", align=align, wrap=urwid.WrapMode.ELLIPSIS) self.assertEqual(expected, str(widget.render((5,)))) def test_ellipsis_line_declares_available_columns(self): """A trimmed line and its ellipsis together declare exactly `width` columns.""" layout = urwid.StandardTextLayout() for encoding in ("utf-8", "ascii"): for width in range(3, 10): with self.subTest(encoding=encoding, width=width), set_temporary_encoding(encoding): segments = layout.layout("Test label", width, urwid.Align.LEFT, urwid.WrapMode.ELLIPSIS) self.assertEqual(width, sum(segment[0] for segment in segments[0])) class NumericLayout(urwid.TextLayout): """ TextLayout class for bottom-right aligned numbers """ def layout( self, text: str | bytes, width: int, align: Literal["left", "center", "right"] | urwid.Align, wrap: Literal["any", "space", "clip", "ellipsis"] | urwid.WrapMode, ) -> list[list[tuple[int, int, int | bytes] | tuple[int, int | None]]]: """ Return layout structure for right justified numbers. """ lt = len(text) r = lt % width # remaining segment not full width wide if r: return [ [(width - r, None), (r, 0, r)], # right-align the remaining segment on 1st line *([(width, x, x + width)] for x in range(r, lt, width)), # fill the rest of the lines ] return [[(width, x, x + width)] for x in range(0, lt, width)] class TestTextLayoutNoPack(unittest.TestCase): def test(self): """Text widget pack should work also with layout not supporting `pack` method.""" widget = urwid.Text("123", layout=NumericLayout()) self.assertEqual((3, 1), widget.pack((3,))) class SupportsModeTest(unittest.TestCase): def test_base_class_supports_everything(self): base = text_layout.TextLayout() self.assertTrue(base.supports_align_mode("bogus")) self.assertTrue(base.supports_wrap_mode("bogus")) def test_base_class_layout_not_implemented(self): base = text_layout.TextLayout() self.assertRaises(NotImplementedError, base.layout, "text", 10, "left", "space") def test_standard_layout_align_modes(self): layout = text_layout.default_layout for align in ("left", "center", "right"): self.assertTrue(layout.supports_align_mode(align)) self.assertFalse(layout.supports_align_mode("bogus")) def test_standard_layout_wrap_modes(self): layout = text_layout.default_layout for wrap in ("any", "space", "clip", "ellipsis"): self.assertTrue(layout.supports_wrap_mode(wrap)) self.assertFalse(layout.supports_wrap_mode("bogus")) class PackTest(unittest.TestCase): def test_empty_layout_raises(self): self.assertRaises(ValueError, text_layout.default_layout.pack, 10, []) def test_maxcol_returned_when_line_reaches_it(self): segs = [[(5, 0, 5)], [(20, 0, 20)]] self.assertEqual(10, text_layout.default_layout.pack(10, segs)) def test_max_line_width_returned_otherwise(self): segs = [[(3, 0, 3)], [(7, 0, 7)]] self.assertEqual(7, text_layout.default_layout.pack(10, segs)) class AlignLayoutTest(unittest.TestCase): def test_invalid_align_raises(self): self.assertRaises( ValueError, text_layout.default_layout.align_layout, "abcde", 10, [[(5, 0, 5)]], "space", "bogus", ) def test_invalid_wrap_mode_raises(self): self.assertRaises( ValueError, text_layout.default_layout.calculate_text_segments, "abcdefghij", 3, "bogus", ) class WideCharWordWrapTest(unittest.TestCase): """Word-wrap (mode "space") breaking right after a wide character.""" def test_wrap_after_wide_char(self): with set_temporary_encoding("euc-jp"): for text in ( b"\xa1\xa1\xa1\xa1\xa1\xa1abcdefgh", b"ab\xa1\xa1\xa1\xa1cdefgh", b"\xa1\xa1bcdefghij", ): for width in range(2, 8): with self.subTest(text=text, width=width): # must not raise, just exercise the wide-char break path text_layout.default_layout.calculate_text_segments(text, width, "space") class ZwjSequenceTest(unittest.TestCase): """Emoji ZWJ sequences are one grapheme: no layout offset may point inside one.""" def test_wrap_never_splits_sequence(self): for wrap in ("any", "space"): with self.subTest(wrap=wrap): self.assertEqual( ["👩�💻👩�💻 ", "👩�💻 "], [row.decode() for row in urwid.Text("👩�💻👩�💻👩�💻", wrap=wrap).render((5,)).text], ) def test_trim_inside_sequence_pads_at_its_start(self): text = "ab 👨�👩�👧👨�👩�👧" line = text_layout.default_layout.layout(text, 100, "left", "clip")[0] # column 4 is the right half of the first family emoji, which starts at offset 3 self.assertEqual([(1, 3), (2, 8, 13)], text_layout.trim_line(line, text, 4, 7)) def test_trim_inside_sequence_renders_space(self): text = "👨�👩�👧👨�👩�👧" line = text_layout.default_layout.layout(text, 100, "left", "clip")[0] trimmed = text_layout.trim_line(line, text, 1, 4) self.assertEqual(" 👨�👩�👧", canvas.apply_text_layout(text, [], [trimmed], 3).text[0].decode()) class LineWidthTest(unittest.TestCase): def test_ignores_leading_shift(self): self.assertEqual(5, text_layout.line_width([(3, None), (5, 0, 5)])) self.assertEqual(5, text_layout.line_width([(5, 0, 5)])) class ShiftLineTest(unittest.TestCase): def test_shift_no_existing_shift(self): self.assertEqual([(3, None), (5, 0, 5)], text_layout.shift_line([(5, 0, 5)], 3)) def test_shift_combines_with_existing_shift(self): self.assertEqual([(5, None), (5, 0, 5)], text_layout.shift_line([(3, None), (5, 0, 5)], 2)) def test_shift_removes_existing_shift_when_zero(self): self.assertEqual([(5, 0, 5)], text_layout.shift_line([(3, None), (5, 0, 5)], -3)) def test_shift_by_zero_is_noop(self): self.assertEqual([(5, 0, 5)], text_layout.shift_line([(5, 0, 5)], 0)) def test_non_int_amount_raises(self): self.assertRaises(TypeError, text_layout.shift_line, [(5, 0, 5)], "x") class LayoutSegmentInitTest(unittest.TestCase): def test_not_a_tuple_raises_type_error(self): self.assertRaises(TypeError, text_layout.LayoutSegment, 0) def test_three_tuple_sc_not_int_raises_type_error(self): self.assertRaises(TypeError, text_layout.LayoutSegment, ("x", 0, 5)) def test_three_tuple_offs_not_int_raises_type_error(self): self.assertRaises(TypeError, text_layout.LayoutSegment, (5, "a", 5)) def test_three_tuple_sc_not_positive_raises_value_error(self): self.assertRaises(ValueError, text_layout.LayoutSegment, (0, 0, 5)) def test_three_tuple_text_wrong_type_raises_type_error(self): self.assertRaises(TypeError, text_layout.LayoutSegment, (5, 0, 3.5)) def test_two_tuple_sc_not_int_raises_type_error(self): self.assertRaises(TypeError, text_layout.LayoutSegment, ("x", 0)) def test_two_tuple_negative_sc_with_offs_raises_value_error(self): self.assertRaises(ValueError, text_layout.LayoutSegment, (-1, 0)) def test_two_tuple_offs_not_int_raises_type_error(self): self.assertRaises(TypeError, text_layout.LayoutSegment, (5, "x")) def test_wrong_length_raises_value_error(self): self.assertRaises(ValueError, text_layout.LayoutSegment, (1, 2, 3, 4)) class LayoutSegmentSubsegTest(unittest.TestCase): def test_start_past_end_returns_empty(self): seg = text_layout.LayoutSegment((5, 0, 5)) self.assertEqual([], seg.subseg("abcde", 5, 3)) self.assertEqual([], seg.subseg("abcde", 5, 5)) def test_offs_none_with_end_raises_value_error(self): # Defensive guard: normal construction cannot leave offs None while end is set, # so exercise it directly via the __slots__ attribute. seg = text_layout.LayoutSegment((5, 0, 5)) seg.offs = None self.assertRaises(ValueError, seg.subseg, "abcde", 0, 3) class CalcLiteralLinePosTest(unittest.TestCase): text = "A" * 27 line = [(2, None), (7, 0, 7), (0, 7)] def test_left(self): self.assertEqual(0, text_layout._calc_literal_line_pos(self.text, self.line, "left")) def test_left_no_offset_found(self): self.assertIsNone(text_layout._calc_literal_line_pos(self.text, [(5, None)], "left")) def test_right(self): self.assertEqual(7, text_layout._calc_literal_line_pos(self.text, self.line, "right")) def test_right_no_offset_found(self): self.assertIsNone(text_layout._calc_literal_line_pos(self.text, [(5, None)], "right")) def test_right_without_end(self): self.assertEqual(2, text_layout._calc_literal_line_pos(self.text, [(3, 2)], "right")) def test_right_with_end(self): self.assertEqual(2, text_layout._calc_literal_line_pos(self.text, [(3, 0, 3)], "right")) def test_invalid_pref_col_raises(self): self.assertRaises(ValueError, text_layout._calc_literal_line_pos, self.text, self.line, "bogus") class CalcLinePosTest(unittest.TestCase): def test_left_dispatches_to_literal(self): line = [(2, None), (7, 0, 7), (0, 7)] self.assertEqual(0, text_layout.calc_line_pos("A" * 27, line, "left")) def test_invalid_pref_col_raises_type_error(self): line = [(2, None), (7, 0, 7), (0, 7)] self.assertRaises(TypeError, text_layout.calc_line_pos, "A" * 27, line, "bogus") def test_numeric_beyond_line_returns_last_int_position(self): self.assertEqual(21, text_layout.calc_line_pos("A" * 27, [(13, 8, 21), (0, 21)], 100)) def test_numeric_beyond_single_segment_returns_last_segment_position(self): self.assertEqual(4, text_layout.calc_line_pos("abcde", [(5, 0, 5)], 100)) class CalcPosTest2(unittest.TestCase): def test_row_out_of_range_raises(self): text = "A" * 27 trans = [[(2, None), (7, 0, 7), (0, 7)], [(13, 8, 21), (0, 21)]] self.assertRaises(ValueError, text_layout.calc_pos, text, trans, "left", 10) def test_falls_back_to_neighboring_rows(self): text = "A" * 27 layout = [[(5, None)], [(7, 0, 7), (0, 7)]] self.assertEqual(0, text_layout.calc_pos(text, layout, "left", 0)) def test_falls_back_searching_both_directions(self): text = "A" * 27 layout = [[(5, None)], [(5, None)], [(7, 0, 7), (0, 7)]] self.assertEqual(0, text_layout.calc_pos(text, layout, "left", 1)) def test_no_match_in_any_row_returns_zero(self): text = "A" * 27 layout = [[(5, None)]] * 6 self.assertEqual(0, text_layout.calc_pos(text, layout, "left", 2)) def test_falls_back_with_unequal_row_counts(self): text = "A" * 40 layout = [[(5, None)]] * 5 + [[(7, 0, 7), (0, 7)]] self.assertEqual(0, text_layout.calc_pos(text, layout, "left", 2)) def test_falls_back_matching_row_above(self): text = "A" * 27 pad = [(5, None)] row_with_offs = [(7, 0, 7), (0, 7)] layout = [pad, pad, row_with_offs, pad, pad] self.assertEqual(0, text_layout.calc_pos(text, layout, "left", 3)) class TrimLineTest(unittest.TestCase): def test_multi_segment_line_not_widened(self): # regression test: trim_line must stop appending whole segments once # their cumulative width reaches `end`, even across more than one # full segment (previously `x` was not advanced for segments taken # as-is, so more segments than requested were appended). segs = [(3, 0, 3), (3, 3, 6), (4, 6, 10)] self.assertEqual([(3, 0, 3)], text_layout.trim_line(segs, "abcdefghij", 0, 3)) class CalcCoordsTest(unittest.TestCase): def test_empty_layout_returns_origin(self): self.assertEqual((0, 0), text_layout.calc_coords("A" * 27, [], 5)) def test_no_exact_match_falls_back_to_closest(self): self.assertEqual((0, 0), text_layout.calc_coords("A" * 27, [[(5, None)]], 3)) def test_distance_based_closest_match(self): self.assertEqual((2, 0), text_layout.calc_coords("A" * 27, [[(2, None), (3, 5, 8)]], 2)) class TestTextTranslationCacheThreads(unittest.TestCase): """The translation cache of one Text widget read and refilled from several threads at once. Buttons share the class-level ``button_left``/``button_right`` Text widgets, so two threads laying out buttons hit the same cache; each reader has to get the translation for the width it asked for. """ @unittest.skipIf( sys.implementation.name == "graalpy", "under coverage tracing GraalPy needs longer than the join timeout for the forced thread switching", ) def test_concurrent_widths(self): # The race window is a few bytecodes wide; the default 5 ms switch interval lets a whole run finish # inside one interval, so the threads have to be forced to interleave. self.addCleanup(sys.setswitchinterval, sys.getswitchinterval()) sys.setswitchinterval(1e-6) widget = urwid.Text("The quick brown fox jumps over the lazy dog, again and again and again.") widths = (12, 12, 20, 20, 33, 33) expected = { width: text_layout.default_layout.layout(widget.text, width, widget.align, widget.wrap) for width in widths } errors: list[BaseException] = [] start = threading.Barrier(len(widths), timeout=30) def worker(width: int) -> None: start.wait() for _ in range(20_000): try: translation = widget.get_line_translation(width) except Exception as exc: # noqa: BLE001 # collected for the assertion on the test thread errors.append(exc) return if translation != expected[width]: errors.append(AssertionError(f"translation for width {width} is not the one laid out for it")) return threads = [threading.Thread(target=worker, args=(width,)) for width in widths] for thread in threads: thread.start() for thread in threads: thread.join(60) self.assertFalse(thread.is_alive(), "worker thread did not finish") self.assertFalse(errors, errors) class TabStopTest(unittest.TestCase): """Tab characters advance to word-processor style tab stops counted in rendered screen columns.""" def render(self, text: str, width: int, **kwargs) -> list[str]: return [row.decode() for row in urwid.Text(text, **kwargs).render((width,)).text] def test_default_stops_every_8_columns(self): self.assertEqual(["1 2 3 "], self.render("1\t2\t3", 20)) def test_stop_counts_rendered_width_of_wide_characters(self): # "中文" is 2 characters, but 4 screen columns self.assertEqual(["中文 x "], self.render("中文\tx", 11)) def test_stop_counts_zwj_sequence_as_one_wide_grapheme(self): # 3 code points joined by ZWJ, 2 screen columns self.assertEqual(["👩�💻 x "], self.render("👩�💻\tx", 12)) def test_zwj_sequence_is_not_split_by_wrap_after_tab(self): self.assertEqual( [[(1, 0, 1), (7, 1, b" "), (2, 2, 7)], [(3, 7, 13), (0, 13)]], text_layout.default_layout.layout("a\t👨�👩�👧👨�👩�👧b", 10, "left", "space"), ) self.assertEqual(["a 👨�👩�👧 ", "👨�👩�👧b "], self.render("a\t👨�👩�👧👨�👩�👧b", 11, wrap="any")) def test_ellipsis_after_tab_keeps_zwj_sequence_whole(self): self.assertEqual(["a 👩�💻…"], self.render("a\t👩�💻👩�💻", 11, wrap="ellipsis")) def test_tab_on_stop_advances_to_the_next_one(self): self.assertEqual(["12345678 y"], self.render("12345678\ty", 17)) def test_explicit_stops_then_interval(self): layout = text_layout.StandardTextLayout(tab_stops=(10, 3), tab_stop_every=4) self.assertEqual( ["a b c d "], self.render("a\tb\tc\td", 18, layout=layout), ) def test_next_tab_stop(self): layout = text_layout.StandardTextLayout(tab_stops=(4, 10)) self.assertEqual([4, 4, 10, 16, 16], [layout.next_tab_stop(col) for col in (0, 3, 4, 10, 15)]) def test_zero_tab_width(self): layout = text_layout.StandardTextLayout(tab_stop_every=0) self.assertEqual( [[(1, 0, 1), (0, 1), (1, 2, 3), (0, 3)]], layout.layout("a\tb", 10, "left", "space"), ) self.assertEqual(["ab "], self.render("a\tb", 10, layout=layout)) self.assertEqual((2, 1), urwid.Text("a\tb", layout=layout).pack()) def test_zero_tab_width_after_last_explicit_stop(self): layout = text_layout.StandardTextLayout(tab_stops=(4,), tab_stop_every=0) self.assertEqual([4, 4], [layout.next_tab_stop(col) for col in (0, 4)]) self.assertEqual(["a bc "], self.render("a\tb\tc", 10, layout=layout)) self.assertEqual((6, 1), urwid.Text("a\tb\tc", layout=layout).pack()) def test_zero_tab_width_at_line_end(self): layout = text_layout.StandardTextLayout(tab_stop_every=0) for wrap in ("any", "space"): with self.subTest(wrap=wrap): self.assertEqual( [[(4, 0, 4), (0, 4)], [(1, 5, 6), (0, 6)]], layout.layout("abcd\te", 4, "left", wrap), ) self.assertEqual([[(0, 0), (0, 1)]], layout.layout("\t", 0, "left", "any")) def test_zero_tab_width_trimmed(self): layout = text_layout.StandardTextLayout(tab_stop_every=0) self.assertEqual(["abc…"], self.render("a\tbcdef", 4, wrap="ellipsis", layout=layout)) self.assertEqual(["ab "], self.render("a\tb", 4, wrap="clip", layout=layout)) def test_invalid_stops_raise(self): self.assertRaises(ValueError, text_layout.StandardTextLayout, tab_stop_every=-1) self.assertRaises(ValueError, text_layout.StandardTextLayout, tab_stops=(0, 4)) def test_tab_is_one_segment_rendered_as_spaces(self): self.assertEqual( [[(2, 0, 2), (6, 2, b" "), (1, 3, 4), (0, 4)]], text_layout.default_layout.layout("ab\tc", 20, "left", "space"), ) def test_bytes_text(self): self.assertEqual( [[(2, 0, 2), (6, 2, b" "), (1, 3, 4), (0, 4)]], text_layout.default_layout.layout(b"ab\tc", 20, "left", "any"), ) def test_space_wrap_moves_word_after_tab_to_next_line(self): self.assertEqual( ["aaa ", "bbbbbbb ccc ", "ddd "], self.render("aaa\tbbbbbbb\tccc ddd", 12, wrap="space"), ) def test_stops_restart_on_each_display_line(self): self.assertEqual(["aaaaa ", "bbb ", "c "], self.render("aaaaa bbb\tc", 8)) def test_space_wrap_drops_tab_at_line_end(self): self.assertEqual( [[(8, 0, 8), (0, 8)], [(1, 9, 10), (0, 10)]], text_layout.default_layout.layout("12345678\tx", 8, "left", "space"), ) def test_tab_is_cut_at_line_end(self): self.assertEqual( [[(3, 0, 3), (3, 3, b" ")], [(1, 4, 5), (0, 5)]], text_layout.default_layout.layout("abc\tx", 6, "left", "space"), ) def test_any_wrap(self): self.assertEqual( ["aaa bbbb", "bbb ccc ", "ddd "], self.render("aaa\tbbbbbbb\tccc ddd", 12, wrap="any"), ) def test_any_wrap_moves_tab_at_line_end_to_next_line(self): self.assertEqual( [[(8, 0, 8)], [(8, 8, b" ")], [(1, 9, 10), (0, 10)]], text_layout.default_layout.layout("12345678\tx", 8, "left", "any"), ) def test_word_not_fitting_after_tab_moves_to_next_line(self): layout = text_layout.StandardTextLayout(tab_stop_every=4) self.assertEqual([" ", "abcdefgh"], self.render("\tabcdefgh", 8, layout=layout)) def test_word_longer_than_line_is_char_wrapped(self): self.assertEqual(["x ", "abcdefghijk", "lmnop "], self.render("x\tabcdefghijklmnop", 11)) def test_clip(self): self.assertEqual(["aaa "], self.render("aaa\tbbb", 6, wrap="clip")) def test_ellipsis_cuts_text_after_tab(self): self.assertEqual(["aaa bbb…"], self.render("aaa\tbbbbbbbbbbb\tccc", 12, wrap="ellipsis")) def test_ellipsis_cuts_tab(self): self.assertEqual(["abc …"], self.render("abc\tdef", 7, wrap="ellipsis")) def test_ellipsis_not_needed(self): self.assertEqual(["abc d "], self.render("abc\td", 11, wrap="ellipsis")) def test_ellipsis_at_tab_stop(self): self.assertEqual(["abcdef…"], self.render("abcdef\tx", 7, wrap="ellipsis")) def test_ellipsis_with_wide_char_at_limit(self): self.assertEqual(["a … "], self.render("a\t中中", 10, wrap="ellipsis")) def test_zero_width_chunk_between_tabs(self): self.assertEqual(["a b"], self.render("a\t\u200b\tb", 17)) self.assertEqual(["a b"], self.render("a\t\u200b\tb", 17, wrap="clip")) def test_space_wrap_at_space_after_tab(self): self.assertEqual(["aaa bb", "cc "], self.render("aaa\tbb cc", 10)) def test_space_wrap_right_after_tab_drops_space(self): self.assertEqual(["a ", "bbb "], self.render("a\t bbb", 8)) def test_space_wrap_before_wide_char(self): self.assertEqual(["a b ", "中 "], self.render("a\tb中", 10)) def test_space_wrap_after_wide_char(self): self.assertEqual(["a 中 ", "bc "], self.render("a\t中bc", 11)) def test_right_alignment_shifts_the_whole_line(self): self.assertEqual([" a b"], self.render("a\tb", 12, align="right")) def test_zero_width_raises_can_not_display(self): self.assertEqual([[]], text_layout.default_layout.layout("\ta", 0, "left", "any")) def test_wide_char_does_not_fit_raises_can_not_display(self): self.assertEqual([[]], text_layout.default_layout.layout("\t中", 1, "left", "space")) def test_unsupported_wrap_raises(self): self.assertRaises(ValueError, text_layout.default_layout._wrap_tabbed_line, "\t", 0, 1, 8, "clip") def test_pack_counts_tabs(self): text = urwid.Text("ab\tc") self.assertEqual((9, 1), text.pack()) self.assertEqual(["ab c"], [row.decode() for row in text.render(()).text]) def test_pack_counts_tabs_with_alignment(self): for align in ("left", "center", "right"): with self.subTest(align=align): self.assertEqual((9, 1), urwid.Text("ab\tc", align=align).pack()) def test_pack_reaches_far_tab_stop(self): layout = text_layout.StandardTextLayout(tab_stops=(40,)) self.assertEqual((41, 1), urwid.Text("x\ty", layout=layout).pack()) def test_pack_multiline_with_emoji_and_tab(self): self.assertEqual((9, 2), urwid.Text("👩�💻\tx\n中文").pack()) def test_pack_layout_without_pack_method(self): class NoPackLayout(urwid.TextLayout): def layout(self, text, width, align, wrap): return [[(len(text), 0, len(text))]] self.assertEqual((4, 1), urwid.Text("abcd", layout=NoPackLayout()).pack()) def test_pack_layout_wider_than_text(self): """A custom layout wider than the text makes pack() retry at a wider maxcol.""" class DoubleWidthLayout(urwid.TextLayout): """Lay out the text on one line of twice its length.""" def layout(self, text, width, align, wrap): return [[(2 * len(text), 0, len(text))]] def pack(self, maxcol, layout): """Return the line width, capped at maxcol.""" return min(maxcol, text_layout.line_width(layout[0])) self.assertEqual((8, 1), urwid.Text("abcd", layout=DoubleWidthLayout()).pack()) def test_edit_cursor_steps_over_tab(self): edit = urwid.Edit("", "ab\tcd") coords = [] for pos in range(6): edit.set_edit_pos(pos) coords.append(edit.get_cursor_coords((20,))) self.assertEqual([(0, 0), (1, 0), (2, 0), (8, 0), (9, 0), (10, 0)], coords) def test_edit_cursor_after_zwj_sequence_and_tab(self): edit = urwid.Edit("", "👩�💻\tx") edit.set_edit_pos(len("👩�💻\t")) self.assertEqual((8, 0), edit.get_cursor_coords((20,))) edit.set_edit_pos(len("👩�💻")) self.assertEqual((2, 0), edit.get_cursor_coords((20,))) def test_edit_click_inside_tab(self): edit = urwid.Edit("", "ab\tcd") edit.move_cursor_to_coords((20,), 4, 0) self.assertEqual(2, edit.edit_pos) edit.move_cursor_to_coords((20,), 7, 0) self.assertEqual(3, edit.edit_pos) class _CountingText(str): """A text that adds the length of every range its ``find`` scans to ``scanned``.""" __slots__ = () scanned = 0 def find(self, sub, start=0, end=None): """Count the scanned range, then search it.""" _CountingText.scanned += (len(self) if end is None else end) - start return super().find(sub, start, end) class _CountingBytes(bytes): """A byte string that adds the length of every range its ``find`` scans to ``_CountingText.scanned``.""" __slots__ = () def find(self, sub, start=0, end=None): """Count the scanned range, then search it.""" _CountingText.scanned += (len(self) if end is None else end) - start return super().find(sub, start, end) class LongParagraphWorkTest(unittest.TestCase): """Laying out a paragraph scans each character a bounded number of times, whatever its length.""" def count_processed(self, text: str | bytes, wrap: Literal["any", "space"]) -> int: """Return how many characters the layout of text hands to the scanning and measuring functions.""" real_iter_graphemes = wcwidth.iter_graphemes real_width = wcwidth.width def iter_graphemes(unistr, start=0, end=None): if start == 0 and end is None: # the C segmenter prepares the whole string before it yields the first cluster _CountingText.scanned += len(unistr) for grapheme in real_iter_graphemes(unistr, start, end): _CountingText.scanned += len(grapheme) yield grapheme def width(unistr, **kwargs): _CountingText.scanned += len(unistr) return real_width(unistr, **kwargs) _CountingText.scanned = 0 counting = _CountingText(text) if isinstance(text, str) else _CountingBytes(text) with unittest.mock.patch.multiple(wcwidth, iter_graphemes=iter_graphemes, width=width): text_layout.default_layout.calculate_text_segments(counting, 20, wrap) return _CountingText.scanned def assert_linear(self, make_text: Callable[[int], str | bytes]) -> None: """Assert that four times the text costs at most five times the work, for both wrap modes.""" for wrap in ("any", "space"): with self.subTest(text=make_text(1), wrap=wrap): self.assertLess( self.count_processed(make_text(4000), wrap), 5 * self.count_processed(make_text(1000), wrap) ) def test_unbroken_paragraph(self): """A paragraph without spaces, also one holding a tab, costs work linear in its length.""" self.assert_linear(lambda n: "x" * n) self.assert_linear(lambda n: "\u00e9" * n) self.assert_linear(lambda n: "x" * n + "\t" + "x" * n) def test_unbroken_utf8_paragraph(self): """A UTF-8 byte string paragraph without spaces costs work linear in its length.""" with set_temporary_encoding("utf-8"): self.assert_linear(lambda n: b"x" * n) self.assert_linear(lambda n: "\u00e9".encode() * n) class ClusterStartingBeforeLineTest(unittest.TestCase): """A grapheme cluster made of a space and a combining character is broken at the space.""" def capped_segments(self, text: str | bytes, width: int) -> list: """Return the space-wrapped segments of text, raising once the layout stops advancing.""" calls = 0 real_calc_text_pos = text_layout.calc_text_pos def calc_text_pos(*args): nonlocal calls calls += 1 if calls > 100: raise RuntimeError("the layout does not advance") return real_calc_text_pos(*args) with unittest.mock.patch.object(text_layout, "calc_text_pos", calc_text_pos): return text_layout.default_layout.calculate_text_segments(text, width, "space") def test_combining_mark_after_space(self): """Wrapping after the space does not measure a range that ends before it starts.""" text = " \u0301hhgb\u00e9abbd" segments = text_layout.default_layout.calculate_text_segments(text, 8, "space") self.assertEqual(len(text), segments[-1][-1][-1]) def test_emoji_modifier_after_space_before_tab(self): """Wrapping a line holding a tab advances past the cluster instead of breaking at its space again.""" text = " \U0001f3fbxx\t" self.assertEqual(len(text), self.capped_segments(text, 3)[-1][-1][-1]) def test_vowel_sign_after_space_in_long_word(self): """Joining a broken word with the line before it happens only when the break moves past the space.""" for text, width in ( ("abc \u093fdefgh ijk", 2), ("abc \u093fdefgh ijk", 4), ("\u0917\u0bb7 \u093f\u0939\u0926\u0939\u0928", 3), ): for encoded in (text, text.encode()): with self.subTest(text=encoded, width=width), set_temporary_encoding("utf-8"): self.assertEqual(len(encoded), self.capped_segments(encoded, width)[-1][-1][-1]) urwid-urwid-69907e4/tests/test_util.py000066400000000000000000000324751526073734300200420ustar00rootroot00000000000000from __future__ import annotations import locale import unittest import urwid from urwid import str_util, util class CalcWidthTest(unittest.TestCase): def setUp(self) -> None: self.old_encoding = util.get_encoding() def tearDown(self) -> None: urwid.set_encoding(self.old_encoding) def wtest(self, desc, s, exp): s = s.encode("iso8859-1") result = str_util.calc_width(s, 0, len(s)) assert result == exp, f"{desc} got:{result!r} expected:{exp!r}" def test1(self): util.set_encoding("utf-8") self.wtest("narrow", "hello", 5) self.wtest("wide char", "\xe6\x9b\xbf", 2) self.wtest("invalid", "\xe6", 1) self.wtest("zero width", "\xcc\x80", 0) self.wtest("mixed", "hello\xe6\x9b\xbf\xe6\x9b\xbf", 9) def test2(self): util.set_encoding("euc-jp") self.wtest("narrow", "hello", 5) self.wtest("wide", "\xa1\xa1\xa1\xa1", 4) self.wtest("invalid", "\xa1", 1) class ConvertDecSpecialTest(unittest.TestCase): def setUp(self) -> None: self.old_encoding = util.get_encoding() def tearDown(self) -> None: urwid.set_encoding(self.old_encoding) def ctest(self, desc, s, exp, expcs): exp = exp.encode("iso8859-1") util.set_encoding("ascii") c = urwid.Text(s).render((5,)) result = c._text[0] assert result == exp, f"{desc} got:{result!r} expected:{exp!r}" resultcs = c._cs[0] assert resultcs == expcs, f"{desc} got:{resultcs!r} expected:{expcs!r}" def test1(self): self.ctest("no conversion", "hello", "hello", [(None, 5)]) self.ctest("only special", "£££££", "}}}}}", [("0", 5)]) self.ctest("mix left", "££abc", "}}abc", [("0", 2), (None, 3)]) self.ctest("mix right", "abc££", "abc}}", [(None, 3), ("0", 2)]) self.ctest("mix inner", "a££bc", "a}}bc", [(None, 1), ("0", 2), (None, 2)]) self.ctest("mix well", "£a£b£", "}a}b}", [("0", 1), (None, 1), ("0", 1), (None, 1), ("0", 1)]) class WithinDoubleByteTest(unittest.TestCase): def setUp(self): self.old_encoding = util.get_encoding() urwid.set_encoding("euc-jp") def tearDown(self) -> None: urwid.set_encoding(self.old_encoding) def wtest(self, s, ls, pos, expected, desc): result = str_util.within_double_byte(s.encode("iso8859-1"), ls, pos) assert result == expected, f"{desc} got:{result!r} expected: {expected!r}" def test1(self): self.wtest("mnopqr", 0, 2, 0, "simple no high bytes") self.wtest("mn\xa1\xa1qr", 0, 2, 1, "simple 1st half") self.wtest("mn\xa1\xa1qr", 0, 3, 2, "simple 2nd half") self.wtest("m\xa1\xa1\xa1\xa1r", 0, 3, 1, "subsequent 1st half") self.wtest("m\xa1\xa1\xa1\xa1r", 0, 4, 2, "subsequent 2nd half") self.wtest("mn\xa1@qr", 0, 3, 2, "simple 2nd half lo") self.wtest("mn\xa1\xa1@r", 0, 4, 0, "subsequent not 2nd half lo") self.wtest("m\xa1\xa1\xa1@r", 0, 4, 2, "subsequent 2nd half lo") def test2(self): self.wtest("\xa1\xa1qr", 0, 0, 1, "begin 1st half") self.wtest("\xa1\xa1qr", 0, 1, 2, "begin 2nd half") self.wtest("\xa1@qr", 0, 1, 2, "begin 2nd half lo") self.wtest("\xa1\xa1\xa1\xa1r", 0, 2, 1, "begin subs. 1st half") self.wtest("\xa1\xa1\xa1\xa1r", 0, 3, 2, "begin subs. 2nd half") self.wtest("\xa1\xa1\xa1@r", 0, 3, 2, "begin subs. 2nd half lo") self.wtest("\xa1@\xa1@r", 0, 3, 2, "begin subs. 2nd half lo lo") self.wtest("@\xa1\xa1@r", 0, 3, 0, "begin subs. not 2nd half lo") def test3(self): self.wtest("abc \xa1\xa1qr", 4, 4, 1, "newline 1st half") self.wtest("abc \xa1\xa1qr", 4, 5, 2, "newline 2nd half") self.wtest("abc \xa1@qr", 4, 5, 2, "newline 2nd half lo") self.wtest("abc \xa1\xa1\xa1\xa1r", 4, 6, 1, "newl subs. 1st half") self.wtest("abc \xa1\xa1\xa1\xa1r", 4, 7, 2, "newl subs. 2nd half") self.wtest("abc \xa1\xa1\xa1@r", 4, 7, 2, "newl subs. 2nd half lo") self.wtest("abc \xa1@\xa1@r", 4, 7, 2, "newl subs. 2nd half lo lo") self.wtest("abc @\xa1\xa1@r", 4, 7, 0, "newl subs. not 2nd half lo") class CalcTextPosTest(unittest.TestCase): def setUp(self) -> None: self.old_encoding = util.get_encoding() def tearDown(self) -> None: urwid.set_encoding(self.old_encoding) def ctptest(self, text, tests): text = text.encode("iso8859-1") for s, e, p, expected in tests: got = str_util.calc_text_pos(text, s, e, p) assert got == expected, f"{s, e, p!r} got:{got!r} expected:{expected!r}" def test1(self): text = "hello world out there" tests = [ (0, 21, 0, (0, 0)), (0, 21, 5, (5, 5)), (0, 21, 21, (21, 21)), (0, 21, 50, (21, 21)), (2, 15, 50, (15, 13)), (6, 21, 0, (6, 0)), (6, 21, 3, (9, 3)), ] self.ctptest(text, tests) def test2_wide(self): util.set_encoding("euc-jp") text = "hel\xa1\xa1 world out there" tests = [ (0, 21, 0, (0, 0)), (0, 21, 4, (3, 3)), (2, 21, 2, (3, 1)), (2, 21, 3, (5, 3)), (6, 21, 0, (6, 0)), ] self.ctptest(text, tests) def test3_utf8(self): util.set_encoding("utf-8") text = "hel\xc4\x83 world \xe2\x81\x81 there" tests = [ (0, 21, 0, (0, 0)), (0, 21, 4, (5, 4)), (2, 21, 1, (3, 1)), (2, 21, 2, (5, 2)), (2, 21, 3, (6, 3)), (6, 21, 7, (15, 7)), (6, 21, 8, (16, 8)), ] self.ctptest(text, tests) def test4_utf8(self): util.set_encoding("utf-8") text = "he\xcc\x80llo \xe6\x9b\xbf world" tests = [ (0, 15, 0, (0, 0)), (0, 15, 1, (1, 1)), (0, 15, 2, (4, 2)), (0, 15, 4, (6, 4)), (8, 15, 0, (8, 0)), (8, 15, 1, (8, 0)), (8, 15, 2, (11, 2)), (8, 15, 5, (14, 5)), ] self.ctptest(text, tests) class TagMarkupTest(unittest.TestCase): mytests = [ ("simple one", "simple one", []), (("blue", "john"), "john", [("blue", 4)]), (["a ", "litt", "le list"], "a little list", []), ( ["mix", [" it", ("high", [" up", ("ital", " a")])], " little"], "mix it up a little", [(None, 6), ("high", 3), ("ital", 2)], ), (["££", "x££"], "££x££", []), ([b"\xc2\x80", b"\xc2\x80"], b"\xc2\x80\xc2\x80", []), ] def test(self): for input, text, attr in self.mytests: restext, resattr = urwid.decompose_tagmarkup(input) assert restext == text, f"got: {restext!r} expected: {text!r}" assert resattr == attr, f"got: {resattr!r} expected: {attr!r}" def test_bad_tuple(self): self.assertRaises(urwid.TagMarkupException, lambda: urwid.decompose_tagmarkup((1, 2, 3))) def test_mixed_text_types_count_converted_lengths(self): """Attribute runs count the characters of the joined text, whatever type each element had.""" with util.set_temporary_encoding("utf-8"): self.assertEqual( ("a\u00e9b", [(None, 2), ("x", 1)]), urwid.decompose_tagmarkup(["a", b"\xc3\xa9", ("x", "b")]) ) self.assertEqual( (b"a\xc3\xa9b", [(None, 3), ("x", 1)]), urwid.decompose_tagmarkup([b"a", "\u00e9", ("x", b"b")]), ) self.assertEqual( ("\u00e9z", [("a", 0), ("b", 1), (None, 0), ("c", 1)]), urwid.decompose_tagmarkup([("a", ""), ("b", b"\xc3\xa9"), "", ("c", "z")]), ) def test_mixed_text_types_replace_unconvertible(self): """An element the target encoding cannot convert is replaced instead of raising.""" with util.set_temporary_encoding("utf-8"): self.assertEqual(("name: \ufffd\ufffd", []), urwid.decompose_tagmarkup(["name: ", b"\xff\xfe"])) with util.set_temporary_encoding("ascii"): self.assertEqual((b"raw:?", []), urwid.decompose_tagmarkup([b"raw:", "\u20ac"])) def test_bad_type(self): self.assertRaises(urwid.TagMarkupException, lambda: urwid.decompose_tagmarkup(5)) class RleTest(unittest.TestCase): def test_rle_prepend(self): rle0 = [("A", 10), ("B", 15)] # the rle functions are mutating, so make a few copies of rle0 rle1, rle2 = rle0[:], rle0[:] util.rle_prepend_modify(rle1, ("A", 3)) util.rle_prepend_modify(rle2, ("X", 2)) self.assertListEqual(rle1, [("A", 13), ("B", 15)]) self.assertListEqual(rle2, [("X", 2), ("A", 10), ("B", 15)]) def test_rle_append(self): rle0 = [("A", 10), ("B", 15)] rle3, rle4 = rle0[:], rle0[:] util.rle_append_modify(rle3, ("B", 5)) util.rle_append_modify(rle4, ("K", 1)) self.assertListEqual(rle3, [("A", 10), ("B", 20)]) self.assertListEqual(rle4, [("A", 10), ("B", 15), ("K", 1)]) def test_rle_prepend_empty(self): rle = [] util.rle_prepend_modify(rle, ("A", 3)) self.assertListEqual(rle, [("A", 3)]) def test_rle_append_empty(self): rle = [] util.rle_append_modify(rle, ("A", 3)) self.assertListEqual(rle, [("A", 3)]) def test_rle_get_at(self): rle = [("A", 10), ("B", 15)] self.assertEqual("A", util.rle_get_at(rle, 0)) self.assertEqual("A", util.rle_get_at(rle, 9)) self.assertEqual("B", util.rle_get_at(rle, 10)) self.assertEqual("B", util.rle_get_at(rle, 24)) self.assertIsNone(util.rle_get_at(rle, 25)) self.assertIsNone(util.rle_get_at(rle, -1)) def test_rle_subseg(self): rle = [("A", 10), ("B", 15)] self.assertListEqual([("A", 10), ("B", 15)], util.rle_subseg(rle, 0, 25)) self.assertListEqual([("A", 5)], util.rle_subseg(rle, 0, 5)) self.assertListEqual([("A", 5), ("B", 5)], util.rle_subseg(rle, 5, 15)) self.assertListEqual([("B", 5)], util.rle_subseg(rle, 20, 30)) self.assertListEqual([], util.rle_subseg(rle, 25, 30)) self.assertListEqual([], util.rle_subseg(rle, 5, 5)) def test_rle_len(self): self.assertEqual(0, util.rle_len([])) self.assertEqual(25, util.rle_len([("A", 10), ("B", 15)])) self.assertRaises(TypeError, util.rle_len, [("A", 10), "not a tuple"]) def test_rle_join_modify(self): rle = [("A", 10), ("B", 15)] util.rle_join_modify(rle, []) self.assertListEqual(rle, [("A", 10), ("B", 15)]) util.rle_join_modify(rle, [("B", 5), ("C", 1)]) self.assertListEqual(rle, [("A", 10), ("B", 20), ("C", 1)]) util.rle_join_modify(rle, [("D", 2)]) self.assertListEqual(rle, [("A", 10), ("B", 20), ("C", 1), ("D", 2)]) def test_rle_product(self): rle1 = [("a", 10), ("b", 5)] rle2 = [("Q", 5), ("P", 10)] self.assertListEqual( [(("a", "Q"), 5), (("a", "P"), 5), (("b", "P"), 5)], util.rle_product(rle1, rle2), ) self.assertListEqual([], util.rle_product([], rle2)) self.assertListEqual([], util.rle_product(rle1, [])) def test_rle_product_skips_zero_length_runs(self): rle1 = [("a", 0), ("b", 5)] rle2 = [("Q", 0), ("P", 5)] self.assertListEqual([(("b", "P"), 5)], util.rle_product(rle1, rle2)) def test_trim_text_attr_cs(self): old_encoding = util.get_encoding() try: util.set_encoding("euc-jp") text = "hel\xa1\xa1lo".encode("iso8859-1") attr = [("A", 7)] cs = [(None, 7)] self.assertEqual( (b"hel\xa1\xa1lo", [("A", 7)], [(None, 7)]), util.trim_text_attr_cs(text, attr, cs, 0, 7), ) self.assertEqual((b"hel ", [("A", 4)], [(None, 4)]), util.trim_text_attr_cs(text, attr, cs, 0, 4)) self.assertEqual((b" lo", [("A", 3)], [(None, 3)]), util.trim_text_attr_cs(text, attr, cs, 4, 7)) finally: urwid.set_encoding(old_encoding) class PortabilityTest(unittest.TestCase): def test_locale(self): initial = locale.getlocale() locale.setlocale(locale.LC_ALL, (None, None)) util.detect_encoding() self.assertEqual(locale.getlocale(), (None, None)) try: locale.setlocale(locale.LC_ALL, ("en_US", "UTF-8")) except locale.Error as exc: if "unsupported locale setting" not in str(exc): raise print( f"Locale change impossible, probably locale not supported by system (libc ignores this error).\n" f"{exc}" ) return util.detect_encoding() self.assertEqual(locale.getlocale(), ("en_US", "UTF-8")) try: locale.setlocale(locale.LC_ALL, initial) except locale.Error as exc: if "unsupported locale setting" not in str(exc): raise print( f"Locale restore impossible, probably locale not supported by system (libc ignores this error).\n" f"{exc}" ) class TestEmptyMarkup(unittest.TestCase): def test_001_empty(self): text = urwid.Text("") text.set_text(text.get_text()) self.assertEqual("", text.text) urwid-urwid-69907e4/tests/test_vterm.py000066400000000000000000000531471526073734300202210ustar00rootroot00000000000000# Urwid terminal emulation widget unit tests # Copyright (C) 2010 aszlig # Copyright (C) 2011 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ from __future__ import annotations import errno import os import sys import typing import unittest from itertools import dropwhile from unittest import mock import urwid import urwid.vterm from urwid.util import set_temporary_encoding IS_WINDOWS = sys.platform == "win32" IS_GRAALPY = sys.implementation.name == "graalpy" # Sentinel values for the mocked PTY. # They never collide with real fds/pids because every os.* call in vterm.py is intercepted while the test is running. _FAKE_MASTER_FD = 0xC0FFEE _FAKE_PID = 0xBEEF def _pty_echo(data: bytes) -> bytes: """Mimic the Linux PTY cooked-mode echo of bytes written to the master. Control characters become caret notation (e.g. ESC -> ``^[``), matching what a real PTY driver would push back when ECHO/ECHOCTL is enabled - which is how the `keypress` writes to show up on the canvas. """ out = bytearray() for byte in data: if byte < 0x20: out.append(0x5E) # '^' out.append(byte + 0x40) elif byte == 0x7F: out.extend(b"^?") else: out.append(byte) return bytes(out) class _FakePTY: """In-memory replacement for the spawned subprocess + PTY pair. ``to_widget`` is the byte stream the widget will read via ``os.read`` - i.e. what a real subprocess would have written to its stdout. ``from_widget`` captures everything the widget writes to the PTY master. """ def __init__(self) -> None: self.to_widget = bytearray() self.from_widget = bytearray() self.closed = False self.signals_sent: list[int] = [] class _FakeRealTerminal: """Drop-in for ``urwid.display.RealTerminal`` that never touches a real tty.""" def tty_signal_keys(self, *args: typing.Any, **kwargs: typing.Any) -> None: return None @unittest.skipIf(IS_WINDOWS, "Terminal is not supported under windows") @unittest.skipIf(IS_GRAALPY, "Terminal is not supported under GraalPy") class TermTest(unittest.TestCase): def setUp(self) -> None: self.pty = _FakePTY() self._install_patches() # The command is never actually executed: # pty.fork is mocked to take the parent branch and os.execvpe is a no-op for safety. self.term = urwid.Terminal(["/bin/false"]) self.resize(80, 24) def tearDown(self) -> None: if not self.term.terminated: self.term.terminate() # ------------------------------------------------------------------ # Patching # ------------------------------------------------------------------ def _install_patches(self) -> None: pty_state = self.pty orig_os_read = os.read orig_os_write = os.write orig_os_close = os.close def fake_os_read(fd: int, n: int) -> bytes: if fd != _FAKE_MASTER_FD: return orig_os_read(fd, n) if pty_state.closed: raise OSError(errno.EIO, os.strerror(errno.EIO)) if not pty_state.to_widget: raise OSError(errno.EWOULDBLOCK, os.strerror(errno.EWOULDBLOCK)) chunk = bytes(pty_state.to_widget[:n]) del pty_state.to_widget[:n] return chunk def fake_os_write(fd: int, data: bytes) -> int: if fd != _FAKE_MASTER_FD: return orig_os_write(fd, data) pty_state.from_widget.extend(data) # Cooked-mode PTY echo: # bytes written to the master come back on the read side so the canvas sees them as input. pty_state.to_widget.extend(_pty_echo(data)) return len(data) def fake_os_close(fd: int) -> None: if fd == _FAKE_MASTER_FD: pty_state.closed = True return orig_os_close(fd) def fake_os_kill(pid: int, sig: int) -> None: if pid == _FAKE_PID: pty_state.signals_sent.append(sig) pty_state.closed = True return os.kill(pid, sig) # pragma: no cover - defensive only def fake_os_waitpid(pid: int, options: int) -> tuple[int, int]: # Pretend the child is already gone so terminate() exits the loop immediately # instead of sleeping for every SIG* candidate. if pid == _FAKE_PID: return 0, 0 return os.waitpid(pid, options) # pragma: no cover def fake_wait_and_feed(self_: urwid.Terminal, timeout: float = 1.0) -> None: # No real fd to select on - data is yet in pty_state.to_widget. self_.feed() patches = [ mock.patch("urwid.vterm.pty.fork", return_value=(_FAKE_PID, _FAKE_MASTER_FD)), mock.patch("urwid.vterm.os.read", side_effect=fake_os_read), mock.patch("urwid.vterm.os.write", side_effect=fake_os_write), mock.patch("urwid.vterm.os.close", side_effect=fake_os_close), mock.patch("urwid.vterm.os.kill", side_effect=fake_os_kill), mock.patch("urwid.vterm.os.waitpid", side_effect=fake_os_waitpid), mock.patch("urwid.vterm.os.execvpe"), mock.patch("urwid.vterm.fcntl.fcntl", return_value=0), mock.patch("urwid.vterm.fcntl.ioctl", return_value=0), mock.patch("urwid.vterm.atexit.register"), mock.patch("urwid.vterm.RealTerminal", _FakeRealTerminal), mock.patch.object(urwid.vterm.Terminal, "wait_and_feed", fake_wait_and_feed), ] for patch in patches: patch.start() self.addCleanup(patch.stop) # ------------------------------------------------------------------ # Signal helpers # ------------------------------------------------------------------ def connect_signal(self, signal: str): self._sig_response = None def _set_signal_response(widget: urwid.Widget, *args, **kwargs) -> None: self._sig_response = (args, kwargs) self._set_signal_response = _set_signal_response urwid.connect_signal(self.term, signal, self._set_signal_response) def expect_signal(self, *args, **kwargs): self.assertEqual(self._sig_response, (args, kwargs)) def disconnect_signal(self, signal: str) -> None: urwid.disconnect_signal(self.term, signal, self._set_signal_response) def caught_beep(self, obj): self.beeped = True # ------------------------------------------------------------------ # Drive the widget # ------------------------------------------------------------------ def resize(self, width: int, height: int, soft: bool = False) -> None: self.termsize = (width, height) if not soft: self.term.render(self.termsize, focus=False) def write(self, data: str) -> None: """Push bytes into the widget's input stream. Equivalent to the spawned subprocess writing to its stdout. PTY cooked-mode ONLCR is applied as a bare ``\\n`` becomes ``\\r\\n`` on the master read side - matching what the kernel would do for a real child process. """ encoded = data.encode("iso8859-1").replace(rb"\e", b"\x1b") encoded = encoded.replace(b"\n", b"\r\n") self.pty.to_widget.extend(encoded) def flush(self) -> None: self.write(chr(0x7F)) @typing.overload def read(self, raw: bool = False, focus: bool = ...) -> bytes: ... @typing.overload def read(self, raw: bool = True, focus: bool = ...) -> list[list[bytes, typing.Any, typing.Any]]: ... def read(self, raw: bool = False, focus: bool = False) -> bytes | list[list[bytes, typing.Any, typing.Any]]: self.term.wait_and_feed() rendered = self.term.render(self.termsize, focus=focus) if raw: is_empty = lambda c: c == (None, None, b" ") content = list(rendered.content()) lines = (tuple(dropwhile(is_empty, reversed(line))) for line in content) return [list(reversed(line)) for line in lines if line] else: content = rendered.text lines = (line.rstrip() for line in content) return b"\n".join(lines).rstrip() def expect( self, what: str | list[tuple[typing.Any, str | None, bytes]], desc: str | None = None, raw: bool = False, focus: bool = False, ) -> None: if not isinstance(what, list): what = what.encode("iso8859-1") got = self.read(raw=raw, focus=focus) if desc is None: desc = "" else: desc += "\n" desc += f"Expected:\n{what!r}\nGot:\n{got!r}" self.assertEqual(got, what, desc) # ------------------------------------------------------------------ # Tests # ------------------------------------------------------------------ def test_simplestring(self): self.write("hello world") self.expect("hello world") def test_linefeed(self): self.write("hello\x0aworld") self.expect("hello\nworld") def test_linefeed2(self): self.write("aa\b\b\\eDbb") self.expect("aa\nbb") def test_carriage_return(self): self.write("hello\x0dworld") self.expect("world") def test_insertlines(self): self.write("\\e[0;0flast\\e[0;0f\\e[10L\\e[0;0ffirst\nsecond\n\\e[11D") self.expect("first\nsecond\n\n\n\n\n\n\n\n\nlast") def test_deletelines(self): self.write("1\n2\n3\n4\\e[2;1f\\e[2M") self.expect("1\n4") def test_nul(self): self.write("a\0b") self.expect("ab") def test_movement(self): self.write(r"\e[10;20H11\e[10;0f\e[20C\e[K") self.expect("\n" * 9 + " " * 19 + "1") self.write("\\e[A\\e[B\\e[C\\e[D\b\\e[K") self.expect("") self.write(r"\e[50A2") self.expect(" " * 19 + "2") self.write("\b\\e[K\\e[50B3") self.expect("\n" * 23 + " " * 19 + "3") self.write("\b\\e[K" + r"\eM" * 30 + r"\e[100C4") self.expect(" " * 79 + "4") self.write(r"\e[100D\e[K5") self.expect("5") def edgewall(self): edgewall = "1-\\e[1;%(x)df-2\\e[%(y)d;1f3-\\e[%(y)d;%(x)df-4\x0d" self.write(edgewall % {"x": self.termsize[0] - 1, "y": self.termsize[1] - 1}) def test_horizontal_resize(self): self.resize(80, 24) self.edgewall() self.expect("1-" + " " * 76 + "-2" + "\n" * 22 + "3-" + " " * 76 + "-4") self.resize(78, 24, soft=True) self.flush() self.expect("1-" + "\n" * 22 + "3-") self.resize(80, 24, soft=True) self.flush() self.expect("1-" + "\n" * 22 + "3-") def test_vertical_resize(self): self.resize(80, 24) self.edgewall() self.expect("1-" + " " * 76 + "-2" + "\n" * 22 + "3-" + " " * 76 + "-4") for y in range(23, 1, -1): self.resize(80, y, soft=True) self.write(r"\e[%df\e[J3-\e[%d;%df-4" % (y, y, 79)) desc = "try to rescale to 80x%d." % y self.expect("\n" * (y - 2) + "3-" + " " * 76 + "-4", desc) self.resize(80, 24, soft=True) self.flush() self.expect("1-" + " " * 76 + "-2" + "\n" * 22 + "3-" + " " * 76 + "-4") def write_movements(self, arg): fmt = "XXX\n\\e[faaa\\e[Bccc\\e[Addd\\e[Bfff\\e[Cbbb\\e[A\\e[Deee" self.write(fmt.replace(r"\e[", r"\e[" + arg)) def test_defargs(self): self.write_movements("") self.expect("aaa ddd eee\n ccc fff bbb") def test_nullargs(self): self.write_movements("0") self.expect("aaa ddd eee\n ccc fff bbb") def test_erase_line(self): self.write("1234567890\\e[5D\\e[K\n1234567890\\e[5D\\e[1K\naaaaaaaaaaaaaaa\\e[2Ka") self.expect("12345\n 7890\n a") def test_erase_display(self): self.write(r"1234567890\e[5D\e[Ja") self.expect("12345a") self.write(r"98765\e[8D\e[1Jx") self.expect(" x5a98765") def test_scrolling_region_simple(self): # TODO(Aleksei): Issue #544 self.write("\\e[10;20r\\e[10f1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\\e[faa") self.expect("aa" + "\n" * 9 + "2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12") def test_scrolling_region_reverse(self): self.write("\\e[2J\\e[1;2r\\e[5Baaa\r\\eM\\eM\\eMbbb\nXXX") self.expect("\n\nbbb\nXXX\n\naaa") def test_scrolling_region_move(self): self.write("\\e[10;20r\\e[2J\\e[10Bfoo\rbar\rblah\rmooh\r\\e[10Aone\r\\eM\\eMtwo\r\\eM\\eMthree\r\\eM\\eMa") self.expect("ahree\n\n\n\n\n\n\n\n\n\nmooh") def test_scrolling_twice(self): self.write(r"\e[?6h\e[10;20r\e[2;5rtest") self.expect("\ntest") def test_cursor_scrolling_region(self): # TODO(Aleksei): Issue #544 self.write("\\e[?6h\\e[10;20r\\e[10f1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\\e[faa") self.expect("\n" * 9 + "aa\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12") def test_scrolling_region_simple_with_focus(self): # TODO(Aleksei): Issue #544 self.write("\\e[10;20r\\e[10f1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\\e[faa") self.expect("aa" + "\n" * 9 + "2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12", focus=True) def test_scrolling_region_reverse_with_focus(self): self.write("\\e[2J\\e[1;2r\\e[5Baaa\r\\eM\\eM\\eMbbb\nXXX") self.expect("\n\nbbb\nXXX\n\naaa", focus=True) def test_scrolling_region_move_with_focus(self): self.write("\\e[10;20r\\e[2J\\e[10Bfoo\rbar\rblah\rmooh\r\\e[10Aone\r\\eM\\eMtwo\r\\eM\\eMthree\r\\eM\\eMa") self.expect("ahree\n\n\n\n\n\n\n\n\n\nmooh", focus=True) def test_scrolling_twice_with_focus(self): self.write(r"\e[?6h\e[10;20r\e[2;5rtest") self.expect("\ntest", focus=True) def test_cursor_scrolling_region_with_focus(self): # TODO(Aleksei): Issue #544 self.write("\\e[?6h\\e[10;20r\\e[10f1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\\e[faa") self.expect("\n" * 9 + "aa\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12", focus=True) def test_relative_region_jump(self): self.write(r"\e[21H---\e[10;20r\e[?6h\e[18Htest") self.expect("\n" * 19 + "test\n---") def test_set_multiple_modes(self): self.write(r"\e[?6;5htest") self.expect("test") self.assertTrue(self.term.term_modes.constrain_scrolling) self.assertTrue(self.term.term_modes.reverse_video) self.write(r"\e[?6;5l") self.expect("test") self.assertFalse(self.term.term_modes.constrain_scrolling) self.assertFalse(self.term.term_modes.reverse_video) def test_wrap_simple(self): self.write(r"\e[?7h\e[1;%dHtt" % self.term.width) self.expect(" " * (self.term.width - 1) + "t\nt") def test_wrap_backspace_tab(self): self.write("\\e[?7h\\e[1;%dHt\b\b\t\ta" % self.term.width) self.expect(" " * (self.term.width - 1) + "a") def test_cursor_visibility(self): self.write(r"\e[?25linvisible") self.expect("invisible", focus=True) self.assertEqual(self.term.term.cursor, None) self.write("\rvisible\\e[?25h\\e[K") self.expect("visible", focus=True) self.assertNotEqual(self.term.term.cursor, None) def test_get_utf8_len(self): length = self.term.term.get_utf8_len(int("11110000", 2)) self.assertEqual(length, 3) length = self.term.term.get_utf8_len(int("11000000", 2)) self.assertEqual(length, 1) length = self.term.term.get_utf8_len(int("11111101", 2)) self.assertEqual(length, 5) def test_encoding_unicode(self): with set_temporary_encoding("utf-8"): self.write("\\e%G\xe2\x80\x94") self.expect("\xe2\x80\x94") def test_encoding_unicode_ascii(self): with set_temporary_encoding("ascii"): self.write("\\e%G\xe2\x80\x94") self.expect("?") def test_encoding_wrong_unicode(self): with set_temporary_encoding("utf-8"): self.write("\\e%G\xc0\x99") self.expect("") def test_encoding_vt100_graphics(self): with set_temporary_encoding("ascii"): self.write("\\e)0\\e(0\x0fg\x0eg\\e)Bn\\e)0g\\e)B\\e(B\x0fn") self.expect( [[(None, "0", b"g"), (None, "0", b"g"), (None, None, b"n"), (None, "0", b"g"), (None, None, b"n")]], raw=True, ) def test_ibmpc_mapping(self): with set_temporary_encoding("ascii"): self.write("\\e[11m\x18\\e[10m\x18") self.expect([[(None, "U", b"\x18")]], raw=True) self.write("\\ec\\e)U\x0e\x18\x0f\\e[3h\x18\\e[3l\x18") self.expect([[(None, None, b"\x18")]], raw=True) self.write("\\ec\\e[11m\xdb\x18\\e[10m\xdb") self.expect( [[(None, "U", b"\xdb"), (None, "U", b"\x18"), (None, None, b"\xdb")]], raw=True, ) def test_set_title(self): self._the_title = None def _change_title(widget, title): self._the_title = title self.connect_signal("title") self.write("\\e]666parsed right?\\e\\te\\e]0;test title\007st1") self.expect("test1") self.expect_signal("test title") self.write("\\e];stupid title\\e\\\\e[0G\\e[2Ktest2") self.expect("test2") self.expect_signal("stupid title") self.disconnect_signal("title") def test_unterminated_osc_ends_at_escape(self): """End an OSC string unapplied at an ESC not followed by a backslash, and parse what follows.""" self.connect_signal("title") for chunks in (["a\\e]0;title\\e[31mred"], ["a\\e]0;title\\e", "[31mred"]): with self.subTest(chunks=chunks): for chunk in chunks: self.write(chunk) self.read() self.expect( [ [ (None, None, b"a"), *[(urwid.AttrSpec("dark red", "default"), None, c) for c in (b"r", b"e", b"d")], ] ], raw=True, ) self.assertIsNone(self._sig_response) self.write("\\e[0m\\e[H\\e[2J") self.disconnect_signal("title") def test_osc_terminated_by_st_across_chunks(self): """Apply an OSC title whose ESC backslash terminator is split across two reads.""" self.connect_signal("title") self.write("\\e]0;title\\e") self.read() self.write("\\rest") self.expect("rest") self.expect_signal("title") self.disconnect_signal("title") def test_title_never_contains_escape(self): """Take the title from the OSC string an ESC starts, not from the unterminated one before it.""" self.connect_signal("title") self.write("\\e]0;ti\\e") self.read() self.write("]0;tle\007x") self.expect("x") self.expect_signal("tle") self.disconnect_signal("title") def test_set_leds(self): self.connect_signal("leds") self.write(r"\e[0qtest1") self.expect("test1") self.expect_signal("clear") self.write(r"\e[3q\e[H\e[Ktest2") self.expect("test2") self.expect_signal("caps_lock") self.disconnect_signal("leds") def test_in_listbox(self): listbox = urwid.ListBox([urwid.BoxAdapter(self.term, 80)]) listbox.render((80, 24)) def test_bracketed_paste_mode_on(self): self.write(r"\e[?2004htest") self.expect("test") self.assertTrue(self.term.term_modes.bracketed_paste) self.term.keypress(None, "begin paste") self.term.keypress(None, "A") self.term.keypress(None, "end paste") self.expect(r"test^[[200~A^[[201~") def test_bracketed_paste_mode_off(self): self.write(r"\e[?2004ltest") self.expect("test") self.assertFalse(self.term.term_modes.bracketed_paste) self.term.keypress(None, "begin paste") self.term.keypress(None, "B") self.term.keypress(None, "end paste") self.expect(r"testB") def test_synchronized_output_mode_on(self): self.write(r"\e[?2026htest") self.expect("test") self.assertTrue(self.term.term_modes.synchronized_output) def test_synchronized_output_mode_off(self): self.write(r"\e[?2026ltest") self.expect("test") self.assertFalse(self.term.term_modes.synchronized_output) def test_synchronized_output_block_spanning_chunks_drains_in_one_feed(self): """A synchronized-output block bigger than one os.read() chunk (4096 bytes) is fully drained within a single feed() call, so a render() in between never lands mid-block.""" self.write(r"\e[?2026h" + "a" * 5000 + r"\e[?2026l" + "B") got = self.read() self.assertFalse(self.term.term_modes.synchronized_output) self.assertTrue(got.endswith(b"B")) if __name__ == "__main__": unittest.main() urwid-urwid-69907e4/tests/util.py000066400000000000000000000020451526073734300167710ustar00rootroot00000000000000from __future__ import annotations import gc import typing import urwid if typing.TYPE_CHECKING: import weakref from collections.abc import Collection class SelectableText(urwid.Text): def selectable(self): return True def keypress(self, size, key): return key def collect_and_count_alive(refs: Collection[weakref.ReferenceType[typing.Any]]) -> int: """Collect garbage until a pass frees none of the objects behind *refs*, and return how many are alive. A tracing collector (PyPy, GraalPy) frees an object released by a weakref callback only in a pass after the one that ran the callback, and GraalPy, once a thread has been started, can defer a pass's callbacks to the next one. Stopping after an unchanged pass takes at least two passes, so the callbacks of the last freeing pass have run. """ before = None for _ in range(10): gc.collect() alive = sum(ref() is not None for ref in refs) if alive == before: break before = alive return alive urwid-urwid-69907e4/tests/widgets/000077500000000000000000000000001526073734300171075ustar00rootroot00000000000000urwid-urwid-69907e4/tests/widgets/__init__.py000066400000000000000000000000001526073734300212060ustar00rootroot00000000000000urwid-urwid-69907e4/tests/widgets/test_ansi_text.py000066400000000000000000000303711526073734300225220ustar00rootroot00000000000000from __future__ import annotations import unittest import urwid from urwid.ansi_parser import AnsiParser from urwid.util import get_encoding, rle_len class ANSITextTest(unittest.TestCase): """Tests covering the default, multi-line (``one_line=False``) behaviour of :class:`~urwid.ANSIText`.""" def setUp(self) -> None: self.old_encoding = get_encoding() urwid.set_encoding("utf-8") def tearDown(self) -> None: urwid.set_encoding(self.old_encoding) def test_sizing(self) -> None: widget = urwid.ANSIText("hello") self.assertEqual(frozenset((urwid.FIXED, urwid.FLOW)), widget.sizing()) def test_pack_and_render_fixed(self) -> None: ansi = urwid.ANSIText("\x1b[31mhello\nworld\x1b[0m") plain = urwid.Text("hello\nworld") self.assertEqual(plain.pack(), ansi.pack()) self.assertEqual(plain.render(()).text, ansi.render(()).text) def test_pack_and_render_flow(self) -> None: ansi = urwid.ANSIText("\x1b[32mimportant\nthings\x1b[0m") plain = urwid.Text("important\nthings") self.assertEqual(plain.pack((11,)), ansi.pack((11,))) self.assertEqual(plain.render((11,)).text, ansi.render((11,)).text) self.assertEqual(plain.rows((11,)), ansi.rows((11,))) def test_multirow_canvas_matches_plain_text(self) -> None: ansi = urwid.ANSIText("row one\nrow two\nrow three") plain = urwid.Text("row one\nrow two\nrow three") self.assertEqual(plain.render((20,)).text, ansi.render((20,)).text) self.assertEqual(3, len(ansi.render((20,)).text)) def test_difference_from_one_line_multiline_input(self) -> None: # the key behavioural difference between the two modes: one_line=True # only ever surfaces the first physical line, while the default # (one_line=False) resolves the whole block into real multi-row output raw = "first\nsecond\nthird" line_widget = urwid.ANSIText(raw, one_line=True) text_widget = urwid.ANSIText(raw) self.assertEqual("first", line_widget.text) self.assertEqual("first\nsecond\nthird", text_widget.text) def test_progress_bar_redraw_produces_single_row(self) -> None: widget = urwid.ANSIText("progress: 10%\rprogress: 20%\rprogress: 100%") self.assertEqual("progress: 100%", widget.text) self.assertNotIn("\n", widget.text) self.assertEqual(1, len(widget.render((30,)).text)) def test_vertical_cursor_movement_resolves_across_rows(self) -> None: widget = urwid.ANSIText("AB\n12\x1b[1EX") # CSI 1E: cursor next line + column reset -- so "X" lands on a new # row at column 0, not overwriting "12" self.assertEqual("AB\n12\nX", widget.text) def test_bel_title_leds_properties(self) -> None: widget = urwid.ANSIText("\x1b]0;my title\x07a\n\x1b[3qb\x07c") self.assertEqual("a\nbc", widget.text) self.assertEqual("my title", widget.title) self.assertEqual("caps_lock", widget.leds) self.assertEqual(1, widget.bel) def test_last_attr_property(self) -> None: widget = urwid.ANSIText("\x1b[31mred\nstill red") self.assertIsNotNone(widget.last_attr) continued = urwid.ANSIText("more red", previous_attr=widget.last_attr) self.assertEqual(widget.last_attr, continued.attrib[0][0]) def test_attrib_rle_contract(self) -> None: widget = urwid.ANSIText("\x1b[31mred\x1b[0m and \n\x1b[32mgreen\x1b[0m") self.assertEqual(len(widget.text), rle_len(widget.attrib)) def test_plain_text_regression_against_text(self) -> None: for sample in ("", "hello world", "line one\nline two", "trailing space \nnext"): ansi = urwid.ANSIText(sample) plain = urwid.Text(sample) self.assertEqual(plain.get_text(), ansi.get_text()) self.assertEqual(plain.pack(), ansi.pack()) self.assertEqual(plain.render((10,)).text, ansi.render((10,)).text) def test_set_ansi_text_reparses_and_invalidates(self) -> None: widget = urwid.ANSIText("first\nline") self.assertEqual("first\nline", widget.text) widget.set_ansi_text("\x1b[31msecond\nblock") self.assertEqual("second\nblock", widget.text) self.assertIsNotNone(widget.last_attr) def test_align_and_wrap_forwarded(self) -> None: widget = urwid.ANSIText("hi", align=urwid.RIGHT, wrap=urwid.CLIP) self.assertEqual([b" hi"], widget.render((10,)).text) def test_from_lines_multiline_yields_single_widget(self) -> None: # the "normal multiline" from_lines path: several chunks (including # one containing an internal newline, and a chunk boundary that # splits a line) are fed in, and since one_line defaults to False, # exactly one ANSIText is yielded, representing the fully # reassembled multi-line block chunks = ["\x1b[31mred\nsti", "ll red\n\x1b[0mpla", "in\nlast li", "ne"] widgets = list(urwid.ANSIText.from_lines(chunks)) self.assertEqual(1, len(widgets)) self.assertEqual("red\nstill red\nplain\nlast line", widgets[0].text) def test_from_lines_multiline_matches_single_call(self) -> None: raw = "one\ntwo\nthree\x1b[31m\nfour" whole = list(urwid.ANSIText.from_lines([raw])) chunks = [raw[:3], raw[3:10], raw[10:15], raw[15:]] self.assertEqual("".join(chunks), raw) chunked = list(urwid.ANSIText.from_lines(chunks)) self.assertEqual(1, len(whole)) self.assertEqual(1, len(chunked)) self.assertEqual(whole[0].text, chunked[0].text) self.assertEqual(whole[0].attrib, chunked[0].attrib) self.assertEqual(whole[0].last_attr, chunked[0].last_attr) def test_from_lines_multiline_preserves_mode_on_set_ansi_text(self) -> None: # a widget produced by from_lines(one_line=False) must remember its # mode, so a later set_ansi_text() call re-parses in the same mode (widget,) = list(urwid.ANSIText.from_lines(["a\nb"])) self.assertEqual("a\nb", widget.text) widget.set_ansi_text("c\nd") self.assertEqual("c\nd", widget.text) class ANSITextOneLineTest(unittest.TestCase): """Tests covering ``ANSIText`` constructed with ``one_line=True``.""" def setUp(self) -> None: self.old_encoding = get_encoding() urwid.set_encoding("utf-8") def tearDown(self) -> None: urwid.set_encoding(self.old_encoding) def test_sizing(self) -> None: widget = urwid.ANSIText("hello", one_line=True) self.assertEqual(frozenset((urwid.FIXED, urwid.FLOW)), widget.sizing()) def test_pack_and_render_fixed(self) -> None: ansi = urwid.ANSIText("\x1b[31mhello\x1b[0m", one_line=True) plain = urwid.Text("hello") self.assertEqual(plain.pack(), ansi.pack()) self.assertEqual(plain.render(()).text, ansi.render(()).text) def test_pack_and_render_flow(self) -> None: ansi = urwid.ANSIText("\x1b[32mimportant things\x1b[0m", one_line=True) plain = urwid.Text("important things") self.assertEqual(plain.pack((11,)), ansi.pack((11,))) self.assertEqual(plain.render((11,)).text, ansi.render((11,)).text) self.assertEqual(plain.rows((11,)), ansi.rows((11,))) def test_embedded_line_end_uses_first_line_only(self) -> None: widget = urwid.ANSIText("a\nb", one_line=True) self.assertEqual("a", widget.text) def test_from_lines_pre_split(self) -> None: lines = list( urwid.ANSIText.from_lines( [ "\x1b[31mred line\n", "still red (no new colour)\n", "\x1b[0mplain again\n", ], one_line=True, ) ) # each trailing "\n" is a structural split point, so the final one # also flushes a trailing empty fragment as its own ANSIText self.assertEqual(4, len(lines)) self.assertEqual("red line", lines[0].text) self.assertEqual("still red (no new colour)", lines[1].text) self.assertEqual("plain again", lines[2].text) self.assertEqual("", lines[3].text) # colour set on line 1 persists (via last_attr chaining) into the # unstyled line 2 self.assertIsNotNone(lines[0].last_attr) self.assertEqual(lines[0].last_attr, lines[1].attrib[0][0]) # explicit reset on line 3 clears it self.assertIsNone(lines[2].last_attr) def test_from_lines_unsplit_single_chunk_vs_chunked(self) -> None: raw = "\x1b[31mred\nstill red\n\x1b[0mplain\n" whole = list(urwid.ANSIText.from_lines([raw], one_line=True)) # split at arbitrary offsets, including mid-escape-sequence and # mid-line splits chunks = [raw[:4], raw[4:9], raw[9:15], raw[15:22], raw[22:]] self.assertEqual("".join(chunks), raw) chunked = list(urwid.ANSIText.from_lines(chunks, one_line=True)) self.assertEqual(len(whole), len(chunked)) for a, b in zip(whole, chunked): self.assertEqual(a.text, b.text) self.assertEqual(a.attrib, b.attrib) self.assertEqual(a.last_attr, b.last_attr) def test_from_lines_flushes_final_unterminated_fragment(self) -> None: lines = list(urwid.ANSIText.from_lines(["no trailing newline"], one_line=True)) self.assertEqual(1, len(lines)) self.assertEqual("no trailing newline", lines[0].text) def test_from_lines_accumulates_single_parser_across_chunks(self) -> None: # confirm the underlying AnsiParser sees the same result whether # driven directly or through from_lines parser = AnsiParser(one_line=True) chunks = ["\x1b[3", "1mred\n", "plain"] for chunk in chunks: parser.feed(chunk) expected = parser.finalize() widgets = list(urwid.ANSIText.from_lines(chunks, one_line=True)) self.assertEqual(len(expected), len(widgets)) for parsed, widget in zip(expected, widgets): self.assertEqual(parsed.text, widget.text) def test_attrib_rle_contract(self) -> None: widget = urwid.ANSIText("\x1b[31mred\x1b[0m and \x1b[32mgreen\x1b[0m", one_line=True) self.assertEqual(len(widget.text), rle_len(widget.attrib)) def test_bel_title_leds_properties_excluded_from_text(self) -> None: widget = urwid.ANSIText("\x1b]0;my title\x07\x1b[3q\x07hello", one_line=True) self.assertEqual("hello", widget.text) self.assertEqual("my title", widget.title) self.assertEqual("caps_lock", widget.leds) self.assertEqual(1, widget.bel) def test_last_attr_property(self) -> None: widget = urwid.ANSIText("\x1b[31mred", one_line=True) self.assertIsNotNone(widget.last_attr) continued = urwid.ANSIText("still red", previous_attr=widget.last_attr, one_line=True) self.assertEqual(widget.last_attr, continued.attrib[0][0]) def test_plain_text_regression_against_text(self) -> None: for sample in ("", "hello world", "a b c", "line with trailing space "): ansi = urwid.ANSIText(sample, one_line=True) plain = urwid.Text(sample) self.assertEqual(plain.get_text(), ansi.get_text()) self.assertEqual(plain.pack(), ansi.pack()) self.assertEqual(plain.render((10,)).text, ansi.render((10,)).text) def test_set_ansi_text_reparses_and_invalidates(self) -> None: widget = urwid.ANSIText("first", one_line=True) self.assertEqual("first", widget.text) widget.set_ansi_text("\x1b[31msecond") self.assertEqual("second", widget.text) self.assertIsNotNone(widget.last_attr) def test_set_ansi_text_preserves_mode_from_from_lines(self) -> None: # a widget produced by from_lines(one_line=True) must remember its # mode, so a later set_ansi_text() call splits on the first line # again rather than switching to multi-row behaviour (widget,) = list(urwid.ANSIText.from_lines(["only line"], one_line=True)) widget.set_ansi_text("a\nb") self.assertEqual("a", widget.text) def test_align_and_wrap_forwarded(self) -> None: widget = urwid.ANSIText("hi", align=urwid.RIGHT, wrap=urwid.CLIP, one_line=True) self.assertEqual([b" hi"], widget.render((10,)).text) if __name__ == "__main__": unittest.main() urwid-urwid-69907e4/tests/widgets/test_attr_map.py000066400000000000000000000053051526073734300223320ustar00rootroot00000000000000from __future__ import annotations import types import unittest import urwid class AttrMapTest(unittest.TestCase): def test_keyword_maps_and_original_widget(self) -> None: text = urwid.Text("hi") mapped = urwid.AttrMap(text, attr_map="idle", focus_map="focus") self.assertIs(text, mapped.original_widget) self.assertIs(text, mapped.base_widget) self.assertEqual({None: "idle"}, mapped.attr_map) self.assertEqual({None: "focus"}, mapped.focus_map) self.assertEqual(text.sizing(), mapped.sizing()) def test_dict_maps_and_assignment(self) -> None: text = urwid.Text(("word", "hi")) mapped = urwid.AttrMap(text, attr_map={"word": "greeting", None: "bg"}, focus_map={"word": "hot"}) mapped.attr_map = {None: "idle"} mapped.focus_map = {None: "focus"} self.assertEqual({None: "idle"}, mapped.attr_map) self.assertEqual({None: "focus"}, mapped.focus_map) mapped.focus_map = {None: None} self.assertEqual({None: None}, mapped.focus_map) def test_bare_attribute_assignment(self) -> None: mapped = urwid.AttrMap(urwid.Text("hi"), "idle") mapped.attr_map = "busy" mapped.focus_map = "focus" self.assertEqual({None: "busy"}, mapped.attr_map) self.assertEqual({None: "focus"}, mapped.focus_map) mapped.attr_map = types.MappingProxyType({"word": "hot"}) self.assertEqual({"word": "hot"}, mapped.attr_map) def test_render_applies_focus_map(self) -> None: mapped = urwid.AttrMap(urwid.Text("hi"), "greeting", "fgreet") size = (5,) self.assertEqual([("greeting", None, b"hi ")], next(mapped.render(size, focus=False).content())) self.assertEqual([("fgreet", None, b"hi ")], next(mapped.render(size, focus=True).content())) def test_wraps_listbox_original_widget(self) -> None: items = (urwid.Text("one"), urwid.Text("two"), urwid.Text("three")) listbox = urwid.ListBox(urwid.SimpleListWalker(items)) mapped = urwid.AttrMap(listbox, attr_map="body") self.assertIs(listbox, mapped.original_widget) self.assertEqual(0, mapped.original_widget.focus_position) mapped.original_widget.focus_position = 2 self.assertEqual(2, listbox.focus_position) self.assertIs(items[2], mapped.original_widget.focus) self.assertEqual([items[2]], mapped.original_widget.get_focus_widgets()) def test_none_attr_map_passthrough(self) -> None: text = urwid.Text("x") mapped = urwid.AttrMap(text, None) self.assertEqual({None: None}, mapped.attr_map) self.assertIsNone(mapped.focus_map) self.assertEqual([b"x"], mapped.render(()).text) urwid-urwid-69907e4/tests/widgets/test_bar_graph.py000066400000000000000000000557601526073734300224620ustar00rootroot00000000000000from __future__ import annotations import math import unittest import urwid from urwid.util import get_encoding from urwid.widget import bar_graph class SegmentAttributesTest(unittest.TestCase): def test_requires_at_least_two_entries(self) -> None: with self.assertRaises(bar_graph.BarGraphError): urwid.BarGraph(["only-bg"]) def test_plain_attributes_default_to_space_char(self) -> None: g = urwid.BarGraph(["bg", "fg"]) self.assertEqual(g.attr, ["bg", "fg"]) self.assertEqual(g.char, [" ", " "]) def test_tuple_attributes_use_given_char(self) -> None: g = urwid.BarGraph(["bg", ("fg", "#")]) self.assertEqual(g.attr, ["bg", "fg"]) self.assertEqual(g.char, [" ", "#"]) def test_hatt_defaults_to_background_attribute(self) -> None: g = urwid.BarGraph(["bg", "fg"]) self.assertEqual(g.hatt, ["bg"]) def test_hatt_accepts_a_list(self) -> None: g = urwid.BarGraph(["bg", "fg"], hatt=["hbg", "hfg"]) self.assertEqual(g.hatt, ["hbg", "hfg"]) def test_hatt_accepts_a_single_value(self) -> None: g = urwid.BarGraph(["bg", "fg"], hatt="honly") # type: ignore[arg-type] self.assertEqual(g.hatt, ["honly"]) def test_satt_invalid_key_shape_raises(self) -> None: with self.assertRaises(bar_graph.BarGraphError): urwid.BarGraph(["bg", "fg", "fg2"], satt={(1, 2, 3): "attr"}) # type: ignore[dict-item] def test_satt_fg_not_int_raises(self) -> None: with self.assertRaises(bar_graph.BarGraphError): urwid.BarGraph(["bg", "fg"], satt={("x", 0): "attr"}) # type: ignore[dict-item] def test_satt_bg_not_int_raises(self) -> None: with self.assertRaises(bar_graph.BarGraphError): urwid.BarGraph(["bg", "fg"], satt={(1, "x"): "attr"}) # type: ignore[dict-item] def test_satt_fg_not_greater_than_bg_raises(self) -> None: with self.assertRaises(bar_graph.BarGraphError): urwid.BarGraph(["bg", "fg"], satt={(0, 1): "attr"}) def test_satt_valid(self) -> None: g = urwid.BarGraph(["bg", "fg"], satt={(1, 0): "attr"}) self.assertEqual(g.satt, {(1, 0): "attr"}) class SetDataTest(unittest.TestCase): def test_hlines_are_sorted_descending(self) -> None: g = urwid.BarGraph(["bg", "fg"]) g.set_data([[1]], 10, [2, 8, 5]) _bardata, _top, hlines = g.data self.assertEqual(hlines, [8, 5, 2]) def test_hlines_none_kept_as_none(self) -> None: g = urwid.BarGraph(["bg", "fg"]) g.set_data([[1]], 10) _bardata, _top, hlines = g.data self.assertIsNone(hlines) class BarWidthTest(unittest.TestCase): def test_set_bar_width_rejects_non_positive(self) -> None: g = urwid.BarGraph(["bg", "fg"]) with self.assertRaises(ValueError): g.set_bar_width(0) def test_set_bar_width_accepts_positive(self) -> None: g = urwid.BarGraph(["bg", "fg"]) g.set_bar_width(3) self.assertEqual(g.bar_width, 3) def test_calculate_bar_widths_fixed_width(self) -> None: g = urwid.BarGraph(["bg", "fg"]) g.set_bar_width(3) self.assertEqual(g.calculate_bar_widths((10, 5), [[1], [2], [3]]), [3, 3, 3]) def test_calculate_bar_widths_more_bars_than_columns(self) -> None: g = urwid.BarGraph(["bg", "fg"]) self.assertEqual(g.calculate_bar_widths((2, 5), [[1], [2], [3]]), [1, 1]) def test_get_data_truncates_bars_that_do_not_fit(self) -> None: g = urwid.BarGraph(["bg", "fg"]) g.set_bar_width(5) g.set_data([[1], [2], [3]], 5) # Only floor(10 / 5) == 2 bars fit into 10 columns. bardata, _top, _hlines = g._get_data((10, 3)) self.assertEqual(bardata, [[1], [2]]) class SelectableTest(unittest.TestCase): def test_bar_graph_not_selectable(self) -> None: self.assertFalse(urwid.BarGraph(["bg", "fg"]).selectable()) def test_graph_vscale_not_selectable(self) -> None: self.assertFalse(urwid.GraphVScale([(1, "1")], 2).selectable()) class HLinesDisplayTest(unittest.TestCase): def test_inserts_hline_row_at_correct_position(self) -> None: g = urwid.BarGraph(["bg", "fg"], hatt=["hbg", "hfg"]) disp = [(5, [(1, 3)])] # top=5, hline=2, maxrow=5 -> row index 2 (of 5), non-smoothed underscore (chnum 0). result = g.hlines_display(disp, top=5, hlines=[2], maxrow=5) self.assertEqual( result, [(2, [(1, 3)]), (1, [((1, 0), 3)]), (2, [(1, 3)])], ) def test_out_of_range_hline_is_dropped(self) -> None: g = urwid.BarGraph(["bg", "fg"], hatt=["hbg", "hfg"]) disp = [(5, [(1, 3)])] # hline above top: rh comes out negative and is filtered. result = g.hlines_display(disp, top=5, hlines=[6], maxrow=5) self.assertEqual(result, disp) def test_infinite_hline_is_dropped(self) -> None: """Drop an infinite hline instead of overflowing.""" g = urwid.BarGraph(["bg", "fg"], hatt=["hbg", "hfg"]) disp = [(5, [(1, 3)])] result = g.hlines_display(disp, top=5, hlines=[-math.inf], maxrow=5) self.assertEqual(result, disp) def test_hline_overflowing_the_scale_is_dropped(self) -> None: """Drop an hline whose scaled row overflows.""" g = urwid.BarGraph(["bg", "fg"], hatt=["hbg", "hfg"]) disp = [(5, [(1, 3)])] # (top - h) * maxrow overflows to infinity. result = g.hlines_display(disp, top=5, hlines=[-1e308], maxrow=5) self.assertEqual(result, disp) def test_hline_too_large_for_float_is_dropped(self) -> None: """Drop an hline that does not fit in a float.""" g = urwid.BarGraph(["bg", "fg"], hatt=["hbg", "hfg"]) disp = [(5, [(1, 3)])] result = g.hlines_display(disp, top=5, hlines=[-(10**400)], maxrow=5) self.assertEqual(result, disp) def test_zero_top_drops_hlines(self) -> None: """Drop every hline when top is zero.""" g = urwid.BarGraph(["bg", "fg"], hatt=["hbg", "hfg"]) disp = [(5, [(1, 3)])] result = g.hlines_display(disp, top=0, hlines=[1], maxrow=5) self.assertEqual(result, disp) def test_nan_hline_does_not_disturb_the_other_hlines(self) -> None: """Keep the other hlines in place next to a NaN one.""" g = urwid.BarGraph(["bg", "fg"], hatt=["hbg", "hfg"]) g.set_data([[0]], 10, [3, math.nan, 7, 1]) result = g.calculate_display((3, 5)) self.assertEqual( result, [(1, [(0, 3)]), (1, [((0, 0), 3)]), (1, [(0, 3)]), (1, [((0, 0), 3)]), (1, [((0, 0), 3)])], ) def test_hline_on_segment_without_hatt_entry_is_left_unstyled(self) -> None: # hatt has only one entry (index 0), so bar_type 1 has no matching hatt: fill_row leaves it as-is. g = urwid.BarGraph(["bg", "fg"]) disp = [(5, [(1, 3)])] result = g.hlines_display(disp, top=5, hlines=[2], maxrow=5) self.assertEqual(result, [(2, [(1, 3)]), (1, [(1, 3)]), (2, [(1, 3)])]) def test_smoothed_hline_uses_fractional_character(self) -> None: old_encoding = get_encoding() urwid.set_encoding("utf-8") try: g = urwid.BarGraph(["bg", "fg"], hatt=["hbg", "hfg"], satt={(1, 0): "smooth"}) disp = [(5, [(1, 3)])] # top=10, hline=6.9, maxrow=5: rh = (10-6.9)*5/10 - 0.0 = 1.55 -> row 1, f=0.55 -> chnum 3. result = g.hlines_display(disp, top=10, hlines=[6.9], maxrow=5) self.assertEqual( result, [(1, [(1, 3)]), (1, [((1, 3), 3)]), (3, [(1, 3)])], ) finally: urwid.set_encoding(old_encoding) def test_calculate_display_applies_hlines(self) -> None: g = urwid.BarGraph(["bg", "fg"], hatt=["hbg", "hfg"]) g.set_data([[3]], 5, [2]) result = g.calculate_display((1, 5)) self.assertTrue(any(isinstance(bar_type, tuple) for _y, row in result for bar_type, _w in row)) def test_two_hlines_landing_on_the_same_row_are_deduplicated(self) -> None: g = urwid.BarGraph(["bg", "fg"], hatt=["hbg", "hfg"]) disp = [(5, [(1, 3)])] result = g.hlines_display(disp, top=10, hlines=[5, 5.05], maxrow=10) self.assertEqual(result, [(4, [(1, 3)]), (1, [((1, 0), 3)])]) def test_hline_on_the_last_row_of_a_block_needs_no_trailing_row(self) -> None: g = urwid.BarGraph(["bg", "fg"], hatt=["hbg", "hfg"]) disp = [(5, [(1, 3)])] result = g.hlines_display(disp, top=5, hlines=[0.5], maxrow=5) self.assertEqual(result, [(4, [(1, 3)]), (1, [((1, 0), 3)])]) class CalculateBargraphDisplayTest(unittest.TestCase): def test_mismatched_lengths_raise(self) -> None: with self.assertRaises(bar_graph.BarGraphError): bar_graph.calculate_bargraph_display([[1], [2]], 5, [1], 5) def test_zero_maxrow_returns_no_rows(self) -> None: self.assertEqual(bar_graph.calculate_bargraph_display([[1]], 5, [1], 0), []) def test_zero_width_bar_does_not_desync_following_bars(self) -> None: # Regression test: a zero-width bar must not shift which bar_widths entry later, # non-zero-width bars read. result = bar_graph.calculate_bargraph_display([[1], [2], [3]], 5, [0, 1, 1], 5) self.assertEqual( result, [(2, [(0, 2)]), (1, [(0, 1), (1, 1)]), (2, [(1, 2)])], ) def test_row_realignment_across_several_bars(self) -> None: # A row whose segment boundaries do not line up with the previous row's run # boundaries: the rowsets builder has to skip over several old runs to catch up. result = bar_graph.calculate_bargraph_display( [[7, 1, 2], [7, 8, 8], [8, 8, 0], [4, 2, 3]], 6, [3, 2, 0, 3], 3, ) self.assertEqual( result, [(1, [(1, 3), (3, 2), (0, 3)]), (1, [(1, 3), (3, 5)]), (1, [(3, 8)])], ) def test_row_realignment_reaching_the_last_run(self) -> None: # Same as above, but the skip lands exactly on the final run of "last". result = bar_graph.calculate_bargraph_display( [[3, 7, 1], [6, 4, 8], [7, 0, 5], [6, 4, 0]], 3, [1, 2, 1, 2], 4, ) self.assertEqual( result, [(3, [(2, 1), (3, 3), (2, 2)]), (1, [(3, 4), (2, 2)])], ) class SmoothDisplayTest(unittest.TestCase): def setUp(self) -> None: self.old_encoding = get_encoding() urwid.set_encoding("utf-8") def tearDown(self) -> None: urwid.set_encoding(self.old_encoding) def test_row_shorter_than_merge_target_raises(self) -> None: g = urwid.BarGraph(["bg", "fg"], satt={(1, 0): "smooth"}) with self.assertRaises(bar_graph.BarGraphError): # 2 columns merged into a row describing only 1: the merge cannot consume it all. g.smooth_display([(4, [(0, 2)]), (4, [(0, 1)])]) def test_large_block_is_grouped_without_remainder(self) -> None: g = urwid.BarGraph(["bg", "fg"], satt={(1, 0): "smooth"}) # 16 disp-rows (2 actual rows) with no partial remainder exercises the "copy whole blocks" path. result = g.smooth_display([(16, [(0, 2)])]) self.assertEqual(result, [(2, [(0, 2)])]) def test_merging_two_already_smoothed_segments(self) -> None: # Three adjacent bars whose eighth-row boundaries fall close enough together that the # merge combines two segments that are themselves already a smoothed (fg, bg, k) tuple. g = urwid.BarGraph(["a", "b", "c"], satt={(1, 0): "x", (2, 1): "y"}) g.set_data([[3], [6], [4]], 10) result = g.calculate_display((6, 1)) self.assertEqual( result, [(1, [((1, 0, 2), 2), ((1, 0, 5), 2), ((1, 0, 3), 2)])], ) def test_merge_falls_back_to_plain_attribute_when_pair_has_no_satt_entry(self) -> None: # The transition landed on by this bar's boundary has no (fg, bg) entry in satt, so the # merge falls back to whichever plain attribute dominates instead of a smoothed character. g = urwid.BarGraph(["a", "b", "c"], satt={(2, 0): "z"}) g.set_data([[8, 2, 5]], 10) result = g.calculate_display((2, 1)) self.assertEqual(result, [(1, [(1, 2)])]) def test_merge_combines_consecutive_runs_of_the_same_type(self) -> None: g = urwid.BarGraph(["a", "b", "c"], satt={(2, 0): "z"}) g.set_data([[9, 4], [5, 4], [3, 5]], 7) result = g.calculate_display((5, 1)) self.assertEqual(result, [(1, [(2, 4), ((2, 0, 6), 1)])]) class RenderTest(unittest.TestCase): def test_render_plain_bars(self) -> None: g = urwid.BarGraph(["bg", ("fg", "#")]) g.set_data([[3]], 5) canv = g.render((1, 5)) self.assertEqual(list(canv.text), [b" ", b" ", b"#", b"#", b"#"]) def test_render_raises_on_multirow_character(self) -> None: g = urwid.BarGraph(["bg", ("fg", "\n")]) g.set_data([[1]], 1) with self.assertRaises(bar_graph.BarGraphError): g.render((1, 1)) def test_render_horizontal_line_segment(self) -> None: g = urwid.BarGraph(["bg", ("fg", "#")], hatt=["hbg", "hfg"]) g.set_data([[3]], 5, [2]) canv = g.render((1, 5)) # Row 2 (0-indexed) carries the hline character instead of the plain fill. self.assertEqual(canv.text[2], g.hlines[0].encode()) def test_render_nan_value_draws_no_bar(self) -> None: """Render a NaN value as an empty bar.""" g = urwid.BarGraph(["bg", ("fg", "#")]) g.set_data([[math.nan]], 5) canv = g.render((1, 3)) self.assertEqual(list(canv.text), [b" ", b" ", b" "]) def test_render_infinite_value_draws_full_bar(self) -> None: """Render an infinite value as a full bar.""" g = urwid.BarGraph(["bg", ("fg", "#")]) g.set_data([[math.inf]], 5) canv = g.render((1, 3)) self.assertEqual(list(canv.text), [b"#", b"#", b"#"]) def test_render_negative_infinite_value_draws_no_bar(self) -> None: """Render a negative infinite value as an empty bar.""" g = urwid.BarGraph(["bg", ("fg", "#")]) g.set_data([[-math.inf]], 5) canv = g.render((1, 3)) self.assertEqual(list(canv.text), [b" ", b" ", b" "]) def test_render_negative_value_draws_no_bar(self) -> None: """Render a negative value as an empty bar.""" g = urwid.BarGraph(["bg", ("fg", "#")]) g.set_data([[-3]], 5) canv = g.render((1, 3)) self.assertEqual(list(canv.text), [b" ", b" ", b" "]) def test_render_value_overflowing_the_scale_draws_full_bar(self) -> None: """Render a value whose scaled row overflows as a full bar.""" g = urwid.BarGraph(["bg", ("fg", "#")]) # value * maxrow overflows to infinity. g.set_data([[1e308]], 5) canv = g.render((1, 3)) self.assertEqual(list(canv.text), [b"#", b"#", b"#"]) def test_render_value_too_large_for_float_draws_full_bar(self) -> None: """Render a value that does not fit in a float as a full bar.""" g = urwid.BarGraph(["bg", ("fg", "#")]) g.set_data([[10**400]], 5) canv = g.render((1, 3)) self.assertEqual(list(canv.text), [b"#", b"#", b"#"]) def test_render_negative_value_too_large_for_float_draws_no_bar(self) -> None: """Render a negative value that does not fit in a float as an empty bar.""" g = urwid.BarGraph(["bg", ("fg", "#")]) g.set_data([[-(10**400)]], 5) canv = g.render((1, 3)) self.assertEqual(list(canv.text), [b" ", b" ", b" "]) def test_render_zero_top_draws_nothing(self) -> None: """Render no bars and no hlines when top is zero.""" g = urwid.BarGraph(["bg", ("fg", "#")], hatt=["hbg", "hfg"]) g.set_data([[1]], 0, [1]) canv = g.render((1, 3)) self.assertEqual(list(canv.text), [b" ", b" ", b" "]) def test_render_nan_top_draws_nothing(self) -> None: """Render no bars and no hlines when top is NaN.""" g = urwid.BarGraph(["bg", ("fg", "#")], hatt=["hbg", "hfg"]) g.set_data([[1]], math.nan, [1]) canv = g.render((1, 3)) self.assertEqual(list(canv.text), [b" ", b" ", b" "]) def test_render_infinite_top_draws_nothing(self) -> None: """Render no bars and no hlines when top is infinite.""" g = urwid.BarGraph(["bg", ("fg", "#")], hatt=["hbg", "hfg"]) g.set_data([[1]], math.inf, [1]) canv = g.render((1, 3)) self.assertEqual(list(canv.text), [b" ", b" ", b" "]) def test_render_top_too_large_for_float_draws_nothing(self) -> None: """Render no bars and no hlines when top does not fit in a float.""" g = urwid.BarGraph(["bg", ("fg", "#")], hatt=["hbg", "hfg"]) g.set_data([[1]], 10**400, [1]) canv = g.render((1, 3)) self.assertEqual(list(canv.text), [b" ", b" ", b" "]) def test_render_negative_top_draws_inverted_bar(self) -> None: """Render a negative top as a graph growing from the other end.""" g = urwid.BarGraph(["bg", ("fg", "#")]) g.set_data([[-3]], -5) canv = g.render((1, 3)) self.assertEqual(list(canv.text), [b" ", b"#", b"#"]) def test_render_smoothed_vertical_eighth(self) -> None: old_encoding = get_encoding() urwid.set_encoding("utf-8") try: g = urwid.BarGraph(["black", "red"], satt={(1, 0): "red/black"}) # A value that does not land on an eighth-row boundary forces a smoothed character. g.set_data([[1]], 3) canv = g.render((1, 5)) rendered = b"".join(canv.text).decode() self.assertTrue(any(ch in g.eighths for ch in rendered)) finally: urwid.set_encoding(old_encoding) class GraphVScaleTest(unittest.TestCase): def test_set_scale_sorts_labels_descending(self) -> None: scale = urwid.GraphVScale([(1, "one"), (5, "five"), (3, "three")], 10) self.assertEqual(scale.pos, [5, 3, 1]) def test_render_empty_scale_returns_solid_canvas(self) -> None: scale = urwid.GraphVScale([], 10) canv = scale.render((5, 3)) self.assertIsInstance(canv, urwid.SolidCanvas) def test_render_places_labels_by_position(self) -> None: scale = urwid.GraphVScale([(8, "top"), (2, "bot")], 10) canv = scale.render((5, 10)) text = b"\n".join(canv.text).decode() self.assertIn("top", text) self.assertIn("bot", text) def test_render_skips_overlapping_labels(self) -> None: # Two labels landing on the same row: sorted descending puts "zzz" first (p < rows branch # then skips "aaa"). scale = urwid.GraphVScale([(5, "aaa"), (5, "zzz")], 10) canv = scale.render((5, 10)) text = b"\n".join(canv.text).decode() self.assertIn("zzz", text) self.assertNotIn("aaa", text) def test_render_stops_at_labels_beyond_maxrow(self) -> None: # A position far below the scale's own range (outside the documented 0 < position < top) # maps to a row past maxrow, so rendering stops before placing it. scale = urwid.GraphVScale([(1, "b"), (-5, "a")], 10) canv = scale.render((5, 3)) text = b"\n".join(canv.text).decode() self.assertNotIn("a", text) self.assertIn("b", text) def test_render_zero_top_draws_no_labels(self) -> None: """Render no labels when top is zero.""" scale = urwid.GraphVScale([(1, "x")], 0) canv = scale.render((3, 3)) self.assertEqual(list(canv.text), [b" ", b" ", b" "]) def test_render_nan_top_draws_no_labels(self) -> None: """Render no labels when top is NaN.""" scale = urwid.GraphVScale([(1, "x")], math.nan) canv = scale.render((3, 3)) self.assertEqual(list(canv.text), [b" ", b" ", b" "]) def test_render_nan_label_is_dropped(self) -> None: """Drop a label at a NaN position.""" scale = urwid.GraphVScale([(math.nan, "x")], 10) canv = scale.render((3, 3)) self.assertEqual(list(canv.text), [b" ", b" ", b" "]) def test_render_nan_label_does_not_disturb_the_other_labels(self) -> None: """Keep the other labels in place next to a NaN one.""" scale = urwid.GraphVScale([(8, "a"), (math.nan, "x"), (5, "b"), (2, "c")], 10) canv = scale.render((1, 10)) self.assertEqual(b"".join(canv.text), b" a b c ") def test_render_label_too_large_for_float_is_skipped(self) -> None: """Skip a label at a position that does not fit in a float.""" scale = urwid.GraphVScale([(10**400, "a"), (5, "b")], 10) canv = scale.render((3, 4)) self.assertEqual(list(canv.text), [b" ", b"b ", b" ", b" "]) def test_render_infinite_label_is_skipped(self) -> None: """Skip a label at infinity.""" scale = urwid.GraphVScale([(math.inf, "a"), (5, "b")], 10) canv = scale.render((3, 4)) self.assertEqual(list(canv.text), [b" ", b"b ", b" ", b" "]) def test_render_negative_infinite_label_is_skipped(self) -> None: """Skip a label at negative infinity.""" scale = urwid.GraphVScale([(5, "b"), (-math.inf, "a")], 10) canv = scale.render((3, 4)) self.assertEqual(list(canv.text), [b" ", b"b ", b" ", b" "]) def test_render_exact_fit_needs_no_trailing_padding(self) -> None: scale = urwid.GraphVScale([(1, "x")], 10) canv = scale.render((5, 1)) self.assertEqual(list(canv.text), [b"x "]) class ScaleBarValuesTest(unittest.TestCase): """Scaling bar values to rows.""" def test_finite_values(self) -> None: """Scale finite values in range.""" self.assertEqual(bar_graph.scale_bar_values([0, 5, 10], 10, 4), [4, 2, 0]) def test_out_of_range_values_stay_out_of_range(self) -> None: """Keep out-of-range values just outside the rows.""" rows = bar_graph.scale_bar_values([-1e308, -math.inf, 1e308, math.inf], 10, 4) self.assertEqual(rows, [8, 8, -1, -1]) def test_values_too_large_for_float_stay_out_of_range(self) -> None: """Place values that do not fit in a float outside the rows.""" self.assertEqual(bar_graph.scale_bar_values([10**400, -(10**400)], 10, 4), [-1, 8]) def test_unusable_top_puts_every_value_below_the_rows(self) -> None: """Place every value past maxrow when top is zero, NaN, infinite or too large for a float.""" for top in (0, math.nan, math.inf, 10**400): with self.subTest(top=top): self.assertEqual(bar_graph.scale_bar_values([1, 2], top, 4), [5, 5]) def test_non_number_top_is_rejected(self) -> None: """Raise TypeError for a top that is not a number, even one float() would parse.""" for top in ("nan", "inf", b"nan"): with self.subTest(top=top), self.assertRaises(TypeError): bar_graph.scale_bar_values([1, 2], top, 4) # type: ignore[arg-type] def test_nan_counts_as_zero(self) -> None: """Scale NaN as zero.""" self.assertEqual(bar_graph.scale_bar_values([math.nan], 10, 4), [4]) if __name__ == "__main__": unittest.main() urwid-urwid-69907e4/tests/widgets/test_big_text.py000066400000000000000000000050371526073734300223320ustar00rootroot00000000000000from __future__ import annotations import unittest import urwid class BigTextTest(unittest.TestCase): def test_get_text(self) -> None: widget = urwid.BigText("AB", urwid.HalfBlock5x4Font()) text, attrib = widget.get_text() self.assertEqual(text, "AB") self.assertEqual(attrib, []) def test_get_text_with_attributes(self) -> None: widget = urwid.BigText([("bold", "AB")], urwid.HalfBlock5x4Font()) text, attrib = widget.get_text() self.assertEqual(text, "AB") self.assertEqual(attrib, [("bold", 2)]) def test_set_font(self) -> None: font_a = urwid.HalfBlock5x4Font() font_b = urwid.HalfBlock6x5Font() widget = urwid.BigText("A", font_a) self.assertIs(widget.font, font_a) widget.set_font(font_b) self.assertIs(widget.font, font_b) def test_render_attributes_applied(self) -> None: widget = urwid.BigText([("bold", "A")], urwid.HalfBlock5x4Font()) canv = widget.render(()) self.assertEqual(canv.rows(), 4) # The single attribute run should be applied across the glyph's width. for row in canv.content(): for attr, _cs, _text in row: self.assertEqual(attr, "bold") def test_render_char_width_zero_is_skipped(self) -> None: # '\n' has zero width in HalfBlock5x4Font, so the render loop skips it # while still consuming the character's attribute run. widget = urwid.BigText([("bold", "A"), (None, "\n"), ("bold", "B")], urwid.HalfBlock5x4Font()) canv_with_newline = widget.render(()) canv_without = urwid.BigText([("bold", "AB")], urwid.HalfBlock5x4Font()).render(()) self.assertEqual(list(canv_with_newline.text), list(canv_without.text)) def test_pack(self) -> None: widget = urwid.BigText("AB", urwid.HalfBlock5x4Font()) font = widget.font expected_cols = font.char_width("A") + font.char_width("B") self.assertEqual(widget.pack(), (expected_cols, font.height)) def test_render_without_attributes(self) -> None: widget = urwid.BigText("A", urwid.HalfBlock5x4Font()) canv = widget.render(()) for row in canv.content(): for attr, _cs, _text in row: self.assertIsNone(attr) def test_render_empty_text(self) -> None: widget = urwid.BigText("", urwid.HalfBlock5x4Font()) canv = widget.render(()) self.assertEqual(canv.rows(), 4) self.assertEqual(canv.cols(), 0) if __name__ == "__main__": unittest.main() urwid-urwid-69907e4/tests/widgets/test_box_adapter.py000066400000000000000000000066661526073734300230260ustar00rootroot00000000000000from __future__ import annotations import unittest import urwid class StubCursorBox(urwid.SolidFill): """A box widget with cursor-related methods that record the size passed to them.""" def get_cursor_coords(self, size: tuple[int, int]) -> tuple[int, int]: self.last_size = size return 1, 1 def move_cursor_to_coords(self, size: tuple[int, int], col: int, row: int) -> bool: self.last_size = size return True def get_pref_col(self, size: tuple[int, int]) -> int: self.last_size = size return 7 class BoxAdapterTest(unittest.TestCase): def test_not_a_box_widget(self) -> None: with self.assertRaises(urwid.widget.box_adapter.BoxAdapterError): urwid.BoxAdapter(urwid.Text("flow widget"), 5) def test_repr(self) -> None: adapter = urwid.BoxAdapter(urwid.SolidFill("x"), 5) self.assertIn("height=5", repr(adapter)) def test_rows(self) -> None: adapter = urwid.BoxAdapter(urwid.SolidFill("x"), 5) self.assertEqual(adapter.rows((20,)), 5) def test_render(self) -> None: adapter = urwid.BoxAdapter(urwid.SolidFill("x"), 3) canv = adapter.render((4,)) self.assertEqual(list(canv.text), [b"xxxx"] * 3) def test_get_cursor_coords_supported(self) -> None: box = StubCursorBox("x") adapter = urwid.BoxAdapter(box, 3) self.assertEqual(adapter.get_cursor_coords((10,)), (1, 1)) self.assertEqual(box.last_size, (10, 3)) def test_get_cursor_coords_unsupported(self) -> None: adapter = urwid.BoxAdapter(urwid.SolidFill("x"), 3) self.assertIsNone(adapter.get_cursor_coords((10,))) def test_move_cursor_to_coords_supported(self) -> None: box = StubCursorBox("x") adapter = urwid.BoxAdapter(box, 3) self.assertTrue(adapter.move_cursor_to_coords((10,), 0, 0)) self.assertEqual(box.last_size, (10, 3)) def test_move_cursor_to_coords_unsupported(self) -> None: adapter = urwid.BoxAdapter(urwid.SolidFill("x"), 3) self.assertTrue(adapter.move_cursor_to_coords((10,), 0, 0)) def test_get_pref_col_supported(self) -> None: box = StubCursorBox("x") adapter = urwid.BoxAdapter(box, 3) self.assertEqual(adapter.get_pref_col((10,)), 7) self.assertEqual(box.last_size, (10, 3)) def test_get_pref_col_unsupported(self) -> None: adapter = urwid.BoxAdapter(urwid.SolidFill("x"), 3) self.assertIsNone(adapter.get_pref_col((10,))) def test_keypress(self) -> None: listbox = urwid.ListBox(urwid.SimpleFocusListWalker([urwid.Edit("", "hi")])) adapter = urwid.BoxAdapter(listbox, 3) self.assertIsNone(adapter.keypress((10,), "x")) self.assertEqual(listbox.focus.edit_text, "hix") def test_mouse_event_supported(self) -> None: listbox = urwid.ListBox(urwid.SimpleFocusListWalker([urwid.Edit("", "hi")])) adapter = urwid.BoxAdapter(listbox, 3) result = adapter.mouse_event((10,), "mouse press", 1, 0, 0, True) self.assertTrue(result) def test_mouse_event_unhandled(self) -> None: adapter = urwid.BoxAdapter(urwid.SolidFill("x"), 3) self.assertFalse(adapter.mouse_event((10,), "mouse press", 1, 0, 0, True)) def test_getattr_passthrough(self) -> None: adapter = urwid.BoxAdapter(urwid.SolidFill("y"), 3) self.assertEqual(adapter.fill_char, "y") if __name__ == "__main__": unittest.main() urwid-urwid-69907e4/tests/widgets/test_columns.py000066400000000000000000001301061526073734300222010ustar00rootroot00000000000000from __future__ import annotations import math import typing import unittest import warnings import urwid from tests.util import SelectableText if typing.TYPE_CHECKING: from collections.abc import Callable, Collection from typing_extensions import Literal class NotAWidget: __slots__ = ("name", "symbol") def __init__(self, name: str, symbol: bytes) -> None: self.name = name self.symbol = symbol def __repr__(self) -> str: return f"{self.__class__.__name__}(name={self.name!r}, symbol={self.symbol!r})" def selectable(self) -> bool: return False def rows(self, max_col_row: tuple[int], focus: bool = False) -> int: return 1 def render(self, max_col_row: tuple[int, int] | tuple[int], focus: bool = False) -> urwid.Canvas: maxcol = max_col_row[0] line = self.symbol * maxcol if len(max_col_row) == 1: return urwid.TextCanvas((line,), maxcol=maxcol) return urwid.TextCanvas((line,) * max_col_row[1], maxcol=maxcol) class ImplementWidget: __slots__ = ("name", "symbol") def sizing(self) -> frozenset[urwid.Sizing]: return frozenset((urwid.BOX, urwid.FLOW)) @property def base_widget(self) -> urwid.AbstractWidget: return self def __init__(self, name: str, symbol: bytes) -> None: self.name = name self.symbol = symbol def __repr__(self) -> str: return f"{self.__class__.__name__}(name={self.name!r}, symbol={self.symbol!r})" def selectable(self) -> bool: return False @property def focus(self) -> None: return None def _invalidate(self) -> None: pass def rows(self, max_col_row: tuple[int], focus: bool = False) -> int: return 1 def pack(self, size: typing.Any, focus: bool = False) -> tuple[int, int]: return self.rows(size, focus) def render(self, max_col_row: tuple[int, int] | tuple[int], focus: bool = False) -> urwid.Canvas: maxcol = max_col_row[0] line = self.symbol * maxcol if len(max_col_row) == 1: return urwid.TextCanvas((line,), maxcol=maxcol) return urwid.TextCanvas((line,) * max_col_row[1], maxcol=maxcol) def keypress(self, size: tuple[int, int] | tuple[int], key: str) -> str | None: return key def mouse_event( self, size: tuple[int, int] | tuple[int], event: str, button: int, col: int, row: int, focus: bool, ) -> bool | None: return False class ParentMutatingText(SelectableText): """Selectable text that runs *callback* before returning a key unhandled or accepting a cursor move.""" def __init__(self, markup: str, callback: Callable[[], object]) -> None: """Run *callback* on every keypress and cursor move.""" super().__init__(markup) self.callback = callback def keypress(self, size: tuple[int], key: str) -> str: """Run the callback, then return *key* unhandled.""" self.callback() return super().keypress(size, key) def move_cursor_to_coords(self, _size: tuple[int], _col: int, _row: int) -> bool: """Run the callback, then accept the cursor move.""" self.callback() return True class ColumnsTest(unittest.TestCase): def test_basic_sizing(self) -> None: box_only = urwid.SolidFill("#") flow_only = urwid.ProgressBar(None, None) fixed_only = urwid.BigText("0", urwid.Thin3x3Font()) flow_fixed = urwid.Text("text") with self.subTest("No sizing calculation possible"), self.assertWarns(urwid.widget.ColumnsWarning) as ctx: widget = urwid.Columns(((urwid.PACK, fixed_only), box_only), box_columns=(1,)) self.assertEqual(frozenset((urwid.BOX, urwid.FLOW)), widget.sizing()) for description, kwargs in ( ("BOX-only widget", {"widget_list": (box_only,)}), ('BOX-only widget with "get height from max"', {"widget_list": (box_only,), "box_columns": (0,)}), ("No FLOW - BOX only", {"widget_list": (box_only, urwid.SolidFill(" ")), "box_columns": (0, 1)}), ("GIVEN BOX only -> BOX only", {"widget_list": ((5, box_only),), "box_columns": (0,)}), ): with self.subTest(description): widget = urwid.Columns(**kwargs) self.assertEqual(frozenset((urwid.BOX,)), widget.sizing()) cols, rows = 2, 2 self.assertEqual((cols, rows), widget.pack((cols, rows))) canvas = widget.render((cols, rows)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) with self.subTest("FLOW-only"): widget = urwid.Columns((flow_only,)) self.assertEqual(frozenset((urwid.FLOW,)), widget.sizing()) cols, rows = 2, 1 self.assertEqual((cols, rows), widget.pack((cols,))) canvas = widget.render((cols,)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) with self.subTest('BOX/FLOW by "box_columns": can be rendered as box only as fallback'): widget = urwid.Columns((flow_only, box_only), box_columns=(1,)) self.assertEqual(frozenset((urwid.FLOW,)), widget.sizing()) cols, rows = 2, 1 self.assertEqual((cols, rows), widget.pack((cols,))) canvas = widget.render((cols,)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual((cols, rows), widget.pack((cols, rows))) canvas = widget.render((cols, rows)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) with self.subTest("FLOW/FIXED"): widget = urwid.Columns((flow_fixed,)) self.assertEqual(frozenset((urwid.FLOW, urwid.FIXED)), widget.sizing()) cols, rows = 4, 1 self.assertEqual((cols, rows), widget.pack(())) canvas = widget.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) cols, rows = 2, 2 self.assertEqual((cols, rows), widget.pack((cols,))) canvas = widget.render((cols,)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) with self.subTest("FIXED only"): widget = urwid.Columns(((urwid.PACK, fixed_only),)) self.assertEqual(frozenset((urwid.FLOW, urwid.FIXED)), widget.sizing()) cols, rows = 3, 3 self.assertEqual((cols, rows), widget.pack(())) canvas = widget.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) with self.subTest("FIXED only use FLOW methods - drop & replace by solid"): widget = urwid.Columns(((urwid.PACK, fixed_only),)) self.assertEqual(frozenset((urwid.FLOW, urwid.FIXED)), widget.sizing()) cols, rows = 2, 1 self.assertEqual((cols, rows), widget.pack((cols,))) canvas = widget.render((cols,)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual(" ", str(canvas)) with self.subTest("FIXED only use FLOW methods - add empty space"): widget = urwid.Columns(((urwid.PACK, fixed_only),)) self.assertEqual(frozenset((urwid.FLOW, urwid.FIXED)), widget.sizing()) cols, rows = 5, 3 self.assertEqual((cols, rows), widget.pack((cols,))) canvas = widget.render((cols,)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( ( "┌─� ", "│ │ ", "└─┘ ", ), canvas.decoded_text, ) with self.subTest("GIVEN BOX + FLOW/FIXED, but other widgets do not support box"): widget = urwid.Columns((flow_fixed, (3, box_only)), box_columns=(1,)) self.assertEqual(frozenset((urwid.FLOW, urwid.FIXED)), widget.sizing()) cols, rows = 7, 1 self.assertEqual((cols, rows), widget.pack(())) canvas = widget.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) cols, rows = 4, 4 self.assertEqual((cols, rows), widget.pack((cols,))) canvas = widget.render((cols,)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual([b"t###", b"e###", b"x###", b"t###"], canvas.text) cols, rows = 2, 2 self.assertEqual((cols, rows), widget.pack((cols, rows))) canvas = widget.render((cols, rows)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual([b"te", b"xt"], canvas.text) with self.subTest("Wrong sizing -> fallback to historic hardcoded"): with self.assertWarns(urwid.widget.ColumnsWarning) as ctx: widget = urwid.Columns(((urwid.PACK, box_only),)) self.assertEqual(frozenset((urwid.BOX, urwid.FLOW)), widget.sizing()) self.assertEqual( f"Sizing combination of widget {box_only} (position=0) not supported: PACK box=False", str(ctx.warnings[0].message), ) with self.subTest("Wrong sizing -> fallback to historic hardcoded 2"): with self.assertWarns(urwid.widget.ColumnsWarning) as ctx: widget = urwid.Columns(((urwid.WEIGHT, 1, fixed_only),)) self.assertEqual(frozenset((urwid.BOX, urwid.FLOW)), widget.sizing()) self.assertEqual( f"Sizing combination of widget {fixed_only} (position=0) not supported: WEIGHT box=False", str(ctx.warnings[0].message), ) with ( self.subTest('BOX not added to "box_columns" but widget handled as FLOW'), self.assertWarns(urwid.widget.ColumnsWarning) as ctx, ): self.maxDiff = None contents = ( (urwid.WEIGHT, 1, urwid.SolidFill()), (urwid.FIXED, 10, urwid.Button("OK", align=urwid.CENTER)), (urwid.WEIGHT, 1, urwid.SolidFill()), (urwid.FIXED, 10, urwid.Button("Cancel", align=urwid.CENTER)), (urwid.WEIGHT, 1, urwid.SolidFill()), ) widget = urwid.Columns(contents) self.assertEqual(frozenset((urwid.BOX, urwid.FLOW)), widget.sizing()) self.assertEqual( "Columns widget contents flags not allow to determine supported render kind:\n" "BOX WEIGHT, FIXED|FLOW GIVEN\n" "Using fallback hardcoded BOX|FLOW sizing kind.", str(ctx.warnings[0].message), ) cols, rows = 30, 1 self.assertEqual((cols, rows), widget.pack((cols,))) canvas = widget.render((cols,)) self.assertEqual(rows, canvas.rows()) self.assertEqual(cols, canvas.cols()) self.assertEqual([b" < OK > < Cancel > "], canvas.text) self.assertEqual( "Widgets in columns [0, 2, 4] " f"({[elem[-1] for elem in contents[:6:2]]}) " f'are BOX widgets not marked "box_columns" ' f"while FLOW render is requested (size=(30,))", str(ctx.warnings[2].message), ) self.assertEqual("OK", widget.focus.label) self.assertIsNone(widget.keypress((cols,), "right")) self.assertEqual( "Widgets in columns [0, 2, 4] " f"({[elem[-1] for elem in contents[:6:2]]}) " f'are BOX widgets not marked "box_columns" ' f"while FLOW render is requested (size=(30,))", str(ctx.warnings[3].message), ) self.assertEqual("Cancel", widget.focus.label) def test_pack_render_fixed(self) -> None: """Cover weighted FIXED/FLOW widgets pack. One of the popular use-cases: rows of buttons/radiobuttons/checkboxes in pop-up windows. """ widget = urwid.Columns( (urwid.Button(label, align=urwid.CENTER) for label in ("OK", "Cancel", "Help")), dividechars=1, ) self.assertEqual((32, 1), widget.pack(())) self.assertEqual([b"< OK > < Cancel > < Help >"], widget.render(()).text) def test_render_fixed_consistency(self) -> None: """Widgets supporting FIXED should be rendered with PACK side the same way as not FIXED.""" widget = urwid.Columns(((urwid.PACK, urwid.Text("Prefix:")), (urwid.PACK, urwid.Button("Btn"))), dividechars=1) cols, _ = widget.pack(()) self.assertEqual(((7, 7), (1, 1), ((), ())), widget.get_column_sizes(())) self.assertEqual(((7, 7), (1, 1), ((7,), (7,))), widget.get_column_sizes((cols,))) self.assertEqual(("Prefix: < Btn >",), widget.render(()).decoded_text) self.assertEqual(("Prefix: < Btn >",), widget.render((cols,)).decoded_text) def test_render_pack_item_not_fit(self): items = urwid.Text("123"), urwid.Text("456") widget = urwid.Columns((urwid.PACK, item) for item in items) # Make width < widget fixed pack width = items[0].pack(())[0] - 1 height = items[0].rows((width,)) self.assertEqual((width, height), widget.pack((width,))) canvas = widget.render((width,)) # Rendered should be not empty self.assertEqual(items[0].render((width,)).decoded_text, canvas.decoded_text) self.assertEqual(width, canvas.cols()) def test_pack_render_broken_sizing(self) -> None: use_sizing = frozenset((urwid.BOX,)) class BrokenSizing(urwid.Text): def sizing(self) -> frozenset[urwid.Sizing]: return use_sizing with self.assertWarns(urwid.widget.ColumnsWarning) as ctx: element = BrokenSizing("Test") widget = urwid.Columns(((urwid.PACK, element),)) self.assertEqual(frozenset((urwid.BOX, urwid.FLOW)), widget.sizing()) self.assertEqual( f"Sizing combination of widget {element} (position=0) not supported: PACK box=False", str(ctx.warnings[0].message), ) with self.assertWarns(urwid.widget.ColumnsWarning) as ctx: canvas = widget.render((10,)) self.assertEqual([b"Test "], canvas.text) self.assertEqual( f"Unusual widget {element} sizing for {urwid.PACK} (box=False). " f"Assuming wrong sizing and using FLOW for width calculation", str(ctx.warnings[0].message), ) def test_pack_render_skip_widget(self): widget = urwid.Columns(((0, urwid.Text("Ignore\nme")), (urwid.Text("Count")))) cols, rows = 5, 1 self.assertEqual((cols, rows), widget.pack(())) self.assertEqual((cols, rows), widget.pack((cols,))) self.assertEqual("Count", str(widget.render(()))) self.assertEqual("Count", str(widget.render((cols,)))) def test_pack_flow_with_fixed_item(self): widget = urwid.Columns( ( (urwid.PACK, urwid.BigText("3", urwid.Thin3x3Font())), (urwid.Text("Multi\nline\ntext\n???")), ), dividechars=1, ) with self.subTest("Skip"): self.assertEqual(3, widget.rows((3,))) self.assertEqual((3, 3), widget.pack((3,))) self.assertEqual( ( "┌─�", " ─┤", "└─┘", ), widget.render((3,)).decoded_text, ) with self.subTest("Fit all"): self.assertEqual(4, widget.rows((9,))) self.assertEqual((9, 4), widget.pack((9,))) self.assertEqual( ( "┌─� Multi", " ─┤ line ", "└─┘ text ", " ??? ", ), widget.render((9,)).decoded_text, ) with self.subTest("Use SolidCanvas instead"): self.assertEqual(1, widget.rows((2,))) self.assertEqual((2, 1), widget.pack((2,))) self.assertEqual((" ",), widget.render((2,)).decoded_text) def test_pack_render_empty_widget(self): widget = urwid.Columns(()) self.assertEqual(frozenset((urwid.FLOW, urwid.BOX)), widget.sizing()) cols, rows = 10, 1 self.assertEqual((cols, rows), widget.pack((cols,))) canvas = widget.render((cols,)) self.assertEqual(rows, canvas.rows()) self.assertEqual((" ",), canvas.decoded_text) def test_not_a_widget(self): with self.subTest("Box"), self.assertWarns(urwid.widget.ColumnsWarning) as ctx: items = (NotAWidget("First", b"*"), NotAWidget("Second", b"^")) widget = urwid.Columns(items) self.assertEqual(("**^^", "**^^"), widget.render((4, 2)).decoded_text) self.assertEqual(f"{items[0]!r} is not a Widget", str(ctx.warnings[0].message)) self.assertEqual(f"{items[1]!r} is not a Widget", str(ctx.warnings[1].message)) with self.subTest("Flow"), self.assertWarns(urwid.widget.ColumnsWarning) as ctx: items = (NotAWidget("First", b"*"), NotAWidget("Second", b"^")) widget = urwid.Columns(items) self.assertEqual(("***^^^",), widget.render((6,)).decoded_text) self.assertEqual(f"{items[0]!r} is not a Widget", str(ctx.warnings[0].message)) self.assertEqual(f"{items[1]!r} is not a Widget", str(ctx.warnings[1].message)) def test_implement_widget_interface(self): with ( warnings.catch_warnings(record=True) as collected_w, self.subTest("Box"), ): items = (ImplementWidget("First", b"*"), ImplementWidget("Second", b"^")) widget = urwid.Columns(items) self.assertEqual(("**^^", "**^^"), widget.render((4, 2)).decoded_text) columns_warnings = tuple( warning.message for warning in collected_w if warning.category == urwid.widget.ColumnsWarning ) self.assertTrue(len(columns_warnings) == 0, "No warnings expected") with ( warnings.catch_warnings(record=True) as collected_w, self.subTest("Flow"), ): items = (ImplementWidget("First", b"*"), ImplementWidget("Second", b"^")) widget = urwid.Columns(items) self.assertEqual(("***^^^",), widget.render((6,)).decoded_text) columns_warnings = tuple( warning.message for warning in collected_w if warning.category == urwid.widget.ColumnsWarning ) self.assertTrue(len(columns_warnings) == 0, "No warnings expected") def test_zero_width_column(self): elem_1 = urwid.BoxAdapter(urwid.SolidFill("#"), 2) elem_2 = urwid.BoxAdapter(urwid.SolidFill("*"), 4) widget = urwid.Columns((elem_1, (0, elem_2))) self.assertEqual((3, 2), widget.pack((3,))) canvas = widget.render((3,)) self.assertEqual(2, canvas.rows()) self.assertEqual(3, canvas.cols()) self.assertEqual([b"###", b"###"], canvas.text) def test_focus_column(self): button_1 = urwid.Button("Selectable") button_2 = urwid.Button("Other selectable") widget_list = [ (urwid.PACK, urwid.Text("Non selectable")), (urwid.PACK, button_1), (urwid.PACK, button_2), (urwid.PACK, urwid.Text("The end")), ] with self.subTest("Not provided -> select first selectable"): widget = urwid.Columns(widget_list) self.assertEqual(1, widget.focus_position) widget.keypress((), "left") self.assertEqual(1, widget.focus_position) with self.subTest("Exact index"): widget = urwid.Columns(widget_list, focus_column=2) self.assertEqual(2, widget.focus_position) widget.keypress((), "left") self.assertEqual(1, widget.focus_position) with self.subTest("Exact widget"): widget = urwid.Columns(widget_list, focus_column=button_2) self.assertEqual(2, widget.focus_position) self.assertEqual(button_2, widget.focus) widget.keypress((), "left") self.assertEqual(1, widget.focus_position) def test_pack_not_enough_info(self): """Test special case for not official fixed pack and render support.""" widget = urwid.Columns( ( (urwid.WEIGHT, 16, urwid.SolidFill(urwid.SolidFill.Symbols.LITE_SHADE)), (10, urwid.Button("First")), urwid.SolidFill(urwid.SolidFill.Symbols.LITE_SHADE), (10, urwid.Button("Second")), (urwid.WEIGHT, 16, urwid.SolidFill(urwid.SolidFill.Symbols.LITE_SHADE)), ), box_columns=(0, 2, 4), ) cols, rows = 53, 1 self.assertEqual(frozenset((urwid.FLOW,)), widget.sizing()) self.assertEqual((cols, rows), widget.pack(())) self.assertEqual( ("░░░░░░░░░░░░░░░░< First >░< Second >░░░░░░░░░░░░░░░░",), widget.render(()).decoded_text, ) def test_no_height(self): widget = urwid.Columns( ( (urwid.WEIGHT, 16, urwid.SolidFill(urwid.SolidFill.Symbols.LITE_SHADE)), urwid.SolidFill(urwid.SolidFill.Symbols.LITE_SHADE), (urwid.WEIGHT, 16, urwid.SolidFill(urwid.SolidFill.Symbols.LITE_SHADE)), ), box_columns=(0, 1, 2), ) self.assertEqual(frozenset((urwid.BOX,)), widget.sizing()) with self.assertRaises(urwid.widget.ColumnsError): widget.pack(()) def assert_column_widths( self, expected: Collection[int], widget: urwid.Columns, size: tuple[int, int] | tuple[int] | tuple[()], description: str, ) -> None: column_widths, _, _ = widget.get_column_sizes(size) self.assertEqual(expected, column_widths, f"{description} expected {expected}, got {column_widths}") def test_widths(self): x = urwid.Text("") # sample "column" self.assert_column_widths((20,), urwid.Columns([x]), (20,), "simple 1") self.assert_column_widths((10, 10), urwid.Columns([x, x]), (20,), "simple 2") self.assert_column_widths((10, 9), urwid.Columns([x, x], 1), (20,), "simple 2+1") self.assert_column_widths((6, 6, 6), urwid.Columns([x, x, x], 1), (20,), "simple 3+1") self.assert_column_widths((5, 6, 5), urwid.Columns([x, x, x], 2), (20,), "simple 3+2") self.assert_column_widths((6, 6, 5), urwid.Columns([x, x, x], 2), (21,), "simple 3+2") simple_4 = urwid.Columns([x, x, x, x], 1) for expected, cols, description in ( ((6, 5, 6, 5), 25, "simple 4+1"), ((1, 1, 1, 1), 7, "squish 4+1"), ((1, 2, 1), 6, "squish 4+1"), ((2, 1), 4, "squish 4+1"), ): with self.subTest(description): self.assert_column_widths(expected, simple_4, (cols,), description) fixed = urwid.Columns([("fixed", 4, x), ("fixed", 6, x), ("fixed", 2, x)], 1) for expected, size, description in ( ((4, 6, 2), (), "FIXED"), ((4, 6, 2), (25,), "fixed 3"), ((4, 6), (13,), "fixed 3 cut"), ((4,), (10,), "fixed 3 cut2"), ): with self.subTest(description): self.assert_column_widths(expected, fixed, size, description) mixed = urwid.Columns([("weight", 2, x), ("fixed", 5, x), x, ("weight", 3, x)], 1) for expected, cols, description in ( ((2, 5, 1, 3), 14, "mixed 4"), ((1, 5, 1, 2), 12, "mixed 4 a"), ((2, 5, 1), 10, "mixed 4 b"), ((4, 5, 2, 6), 20, "mixed 4 c"), ): with self.subTest(description): self.assert_column_widths(expected, mixed, (cols,), description) def test_widths_focus_end(self): x = urwid.Text("") # sample "column" self.assert_column_widths( (10, 10), urwid.Columns([x, x], focus_column=1), (20,), "end simple 2", ) self.assert_column_widths( (10, 9), urwid.Columns([x, x], 1, 1), (20,), "end simple 2+1", ) self.assert_column_widths( (6, 6, 6), urwid.Columns([x, x, x], 1, 2), (20,), "end simple 3+1", ) self.assert_column_widths( (5, 6, 5), urwid.Columns([x, x, x], 2, 2), (20,), "end simple 3+2", ) self.assert_column_widths( (6, 6, 5), urwid.Columns([x, x, x], 2, 2), (21,), "end simple 3+2", ) simple_4 = urwid.Columns([x, x, x, x], 1, 3) for expected, cols, description in ( ((6, 5, 6, 5), 25, "end simple 4+1"), ((1, 1, 1, 1), 7, "end squish 4+1"), ((0, 1, 2, 1), 6, "end squish 4+1"), ((0, 0, 2, 1), 4, "end squish 4+1"), ): with self.subTest(description): self.assert_column_widths(expected, simple_4, (cols,), description) fixed = urwid.Columns([("fixed", 4, x), ("fixed", 6, x), ("fixed", 2, x)], 1, 2) for expected, size, description in ( ((4, 6, 2), (), "FIXED"), ((4, 6, 2), (25,), "end fixed 3"), ((0, 6, 2), (13,), "end fixed 3 cut"), ((0, 0, 2), (8,), "end fixed 3 cut2"), ): with self.subTest(description): self.assert_column_widths(expected, fixed, size, description) mixed = urwid.Columns([("weight", 2, x), ("fixed", 5, x), x, ("weight", 3, x)], 1, 3) for expected, cols, description in ( ((2, 5, 1, 3), 14, "end mixed 4"), ((1, 5, 1, 2), 12, "end mixed 4 a"), ((0, 5, 1, 2), 10, "end mixed 4 b"), ((4, 5, 2, 6), 20, "end mixed 4 c"), ): with self.subTest(description): self.assert_column_widths(expected, mixed, (cols,), description) def check_move_cursor( self, expected: bool, widget: urwid.Columns, size: tuple[int, int] | tuple[int] | tuple[()], col: int | Literal["left", "right"], row: int, focus_position: int, pref_col: int | Literal["left", "right"] | None, description: str, ) -> None: moved = widget.move_cursor_to_coords(size, col, row) self.assertEqual(expected, moved, f"{description} expected {expected!r}, got {moved!r}") self.assertEqual( focus_position, widget.focus_position, f"{description} expected focus_position {focus_position!r}, got {widget.focus_position!r}", ) w_pref_col = widget.get_pref_col(size) self.assertEqual( pref_col, w_pref_col, f"{description} expected pref_col {pref_col!r}, got {w_pref_col!r}", ) def test_move_cursor(self): e, s, x = urwid.Edit("", ""), SelectableText(""), urwid.Text("") self.check_move_cursor( False, urwid.Columns([x, x, x], 1), (20,), 9, 0, 0, None, "nothing selectable", ) mid = urwid.Columns([x, s, x], 1) self.check_move_cursor( True, mid, (20,), 9, 0, 1, 9, "dead on", ) self.check_move_cursor( True, mid, (20,), 6, 0, 1, 6, "l edge", ) self.check_move_cursor( True, mid, (20,), 13, 0, 1, 13, "r edge", ) self.check_move_cursor( True, mid, (20,), 2, 0, 1, 2, "l off", ) self.check_move_cursor( True, mid, (20,), 17, 0, 1, 17, "r off", ) self.check_move_cursor( True, urwid.Columns([x, x, s], 1), (20,), 2, 0, 2, 2, "l off 2", ) self.check_move_cursor( True, urwid.Columns([s, x, x], 1), (20,), 17, 0, 0, 17, "r off 2", ) self.check_move_cursor( True, urwid.Columns([s, s, x], 1), (20,), 6, 0, 0, 6, "l between", ) self.check_move_cursor( True, urwid.Columns([x, s, s], 1), (20,), 13, 0, 1, 13, "r between", ) l_2 = urwid.Columns([s, s, x], 2) self.check_move_cursor( True, l_2, (22,), 6, 0, 0, 6, "l between 2l", ) self.check_move_cursor( True, l_2, (22,), 7, 0, 1, 7, "l between 2r", ) r_2 = urwid.Columns([x, s, s], 2) self.check_move_cursor( True, r_2, (22,), 14, 0, 1, 14, "r between 2l", ) self.check_move_cursor( True, r_2, (22,), 15, 0, 2, 15, "r between 2r", ) # unfortunate pref_col shifting edge = urwid.Columns([x, e, x], 1) self.check_move_cursor( True, edge, (20,), 6, 0, 1, 7, "l e edge", ) self.check_move_cursor( True, edge, (20,), 13, 0, 1, 12, "r e edge", ) # 'left'/'right' special cases full = urwid.Columns([e, e, e]) self.check_move_cursor( True, full, (12,), "right", 0, 2, "right", "l e edge", ) self.check_move_cursor( True, full, (12,), "left", 0, 0, "left", "r e edge", ) def test_init_with_a_generator(self): urwid.Columns(urwid.Text(c) for c in "ABC") def test_old_attributes(self): c = urwid.Columns([urwid.Text("a"), urwid.SolidFill("x")], box_columns=[1]) with self.assertWarns(DeprecationWarning): self.assertEqual(c.box_columns, [1]) with self.assertWarns(DeprecationWarning): c.box_columns = [] self.assertEqual(c.box_columns, []) def test_box_column(self): c = urwid.Columns([urwid.Filler(urwid.Edit()), urwid.Text("")], box_columns=[0]) c.keypress((10,), "x") c.get_cursor_coords((10,)) c.move_cursor_to_coords((10,), 0, 0) c.mouse_event((10,), "foo", 1, 0, 0, True) c.get_pref_col((10,)) def test_length(self): columns = urwid.Columns(urwid.Text(c) for c in "ABC") self.assertEqual(3, len(columns)) self.assertEqual(3, len(columns.contents)) def test_common(self): t1 = urwid.Text("one") t2 = urwid.Text("two") t3 = urwid.Text("three") sf = urwid.SolidFill("x") c = urwid.Columns([]) with self.subTest("Focus"): self.assertEqual(c.focus, None) self.assertRaises(IndexError, lambda: getattr(c, "focus_position")) self.assertRaises(IndexError, lambda: setattr(c, "focus_position", None)) self.assertRaises(IndexError, lambda: setattr(c, "focus_position", 0)) with self.subTest("Contents change"): c.contents = [ (t1, ("pack", None, False)), (t2, ("weight", 1, False)), (sf, ("weight", 2, True)), (t3, ("given", 10, False)), ] c.focus_position = 1 del c.contents[0] self.assertEqual(c.focus_position, 0) c.contents[0:0] = [(t3, ("given", 10, False)), (t2, ("weight", 1, False))] c.contents.insert(3, (t1, ("pack", None, False))) self.assertEqual(c.focus_position, 2) with self.subTest("Contents change validation"): c.contents.clear() self.assertRaises(urwid.ColumnsError, lambda: c.contents.append(t1)) self.assertRaises(urwid.ColumnsError, lambda: c.contents.append((t1, None))) self.assertRaises(urwid.ColumnsError, lambda: c.contents.append((t1, "given"))) self.assertRaises(urwid.ColumnsError, lambda: c.contents.append((t1, ("given", None)))) # Incorrect kind self.assertRaises(urwid.ColumnsError, lambda: c.contents.append((t1, ("what", 1, False)))) # Incorrect box field self.assertRaises(urwid.ColumnsError, lambda: c.contents.append((t1, ("given", 1, None)))) # Incorrect size type self.assertRaises(urwid.ColumnsError, lambda: c.contents.append((t1, ("given", (), False)))) # Incorrect size self.assertRaises(urwid.ColumnsError, lambda: c.contents.append((t1, ("given", -1, False)))) # Float and int weight accepted c.contents.append((t1, ("weight", 1, False))) c.contents.append((t2, ("weight", 0.5, False))) self.assertEqual(("one two",), c.render(()).decoded_text) def test_focus_position(self): t1 = urwid.Text("one") t2 = urwid.Text("two") c = urwid.Columns([t1, t2]) self.assertEqual(c.focus, t1) self.assertEqual(c.focus_position, 0) c.focus_position = 1 self.assertEqual(c.focus, t2) self.assertEqual(c.focus_position, 1) c.focus_position = 0 self.assertRaises(IndexError, lambda: setattr(c, "focus_position", -1)) self.assertRaises(IndexError, lambda: setattr(c, "focus_position", 2)) def test_deprecated(self): t1 = urwid.Text("one") t2 = urwid.Text("two") sf = urwid.SolidFill("x") # old methods: c = urwid.Columns([t1, ("weight", 3, t2), sf], box_columns=[2]) with self.subTest("Focus"): c.set_focus(0) self.assertRaises(IndexError, lambda: c.set_focus(-1)) self.assertRaises(IndexError, lambda: c.set_focus(3)) c.set_focus(t2) self.assertEqual(c.focus_position, 1) self.assertRaises(ValueError, lambda: c.set_focus("nonexistant")) with self.subTest("Contents"): self.assertEqual(c.widget_list, [t1, t2, sf]) self.assertEqual(c.column_types, [("weight", 1), ("weight", 3), ("weight", 1)]) self.assertEqual(c.box_columns, [2]) with self.subTest("Contents change"): c.widget_list = [t2, t1, sf] self.assertEqual(c.widget_list, [t2, t1, sf]) self.assertEqual(c.box_columns, [2]) self.assertEqual( c.contents, [(t2, ("weight", 1, False)), (t1, ("weight", 3, False)), (sf, ("weight", 1, True))], ) self.assertEqual(c.focus_position, 1) # focus unchanged c.column_types = [("flow", None), ("weight", 2), ("fixed", 5)] # use the old name self.assertEqual(c.column_types, [("flow", None), ("weight", 2), ("fixed", 5)]) self.assertEqual( c.contents, [(t2, ("pack", None, False)), (t1, ("weight", 2, False)), (sf, ("given", 5, True))], ) self.assertEqual(c.focus_position, 1) # focus unchanged with self.subTest("Contents change 2"): c.widget_list = [t1] self.assertEqual(len(c.contents), 1) self.assertEqual(c.focus_position, 0) c.widget_list.extend([t2, t1]) self.assertEqual(len(c.contents), 3) self.assertEqual(c.column_types, [("flow", None), ("weight", 1), ("weight", 1)]) c.column_types[:] = [("weight", 2)] self.assertEqual(len(c.contents), 1) def test_regression_columns_different_height(self): size = (20, 5) box_w = urwid.SolidFill("#") f_f_widget = urwid.Text("Fixed/Flow") box_flow = urwid.LineBox(urwid.Filler(f_f_widget, valign=urwid.TOP)) self.assertIn(urwid.BOX, box_w.sizing()) self.assertEqual(frozenset((urwid.BOX, urwid.FLOW)), box_flow.sizing()) with self.subTest("BoxFlow weight"): widget = urwid.Columns(((1, box_w), box_flow)) self.assertEqual( ( "#┌─────────────────�", "#│Fixed/Flow │", "#│ │", "#│ │", "#└─────────────────┘", ), widget.render(size, False).decoded_text, ) with self.subTest("BoxFlow GIVEN"): widget = urwid.Columns((box_w, (12, box_flow))) self.assertEqual( ( "########┌──────────�", "########│Fixed/Flow│", "########│ │", "########│ │", "########└──────────┘", ), widget.render(size, False).decoded_text, ) def test_focus_changed_callback_replacement(self) -> None: """Replace the contents focus callback and read the new index before it is applied.""" first = urwid.Button("first") second = urwid.Button("second") columns = urwid.Columns((first, second)) seen: list[tuple[int, int, urwid.Widget]] = [] def on_focus_change(new_focus: int) -> None: seen.append((new_focus, columns.focus_position, columns.contents[new_focus][0])) columns._invalidate() columns.contents.set_focus_changed_callback(on_focus_change) columns.focus_position = 1 self.assertEqual([(1, 0, second)], seen) self.assertIs(second, columns.focus) self.assertEqual(1, columns.focus_position) def test_selectable_follows_child_change(self) -> None: """A child changing its selectability after it was added is seen by the columns and its keypress.""" placeholder = urwid.WidgetPlaceholder(urwid.Text("wait")) columns = urwid.Columns([placeholder]) self.assertFalse(columns.selectable()) placeholder.original_widget = urwid.Edit() self.assertTrue(columns.selectable()) self.assertIsNone(columns.keypress((10,), "a")) placeholder.original_widget = urwid.Text("done") self.assertFalse(columns.selectable()) def test_pack_given_and_weight_footer_row(self) -> None: """Footer-style row: weighted SolidFill spacers around given-width buttons.""" ok_button = urwid.Button("OK", align=urwid.CENTER) cancel_button = urwid.Button("Cancel", align=urwid.CENTER) columns = urwid.Columns( ( (urwid.WHSettings.WEIGHT, 16, urwid.SolidFill()), (10, ok_button), urwid.SolidFill(), (10, cancel_button), (urwid.WHSettings.WEIGHT, 16, urwid.SolidFill()), ), box_columns=(0, 2, 4), focus_column=ok_button, ) self.assertIs(ok_button, columns.focus) self.assertEqual(1, columns.focus_position) canvas = columns.render((53, 1)) self.assertEqual(53, canvas.cols()) self.assertEqual(1, canvas.rows()) self.assertIn(b"< OK >", canvas.text[0]) self.assertIn(b"< Cancel >", canvas.text[0]) def test_non_finite_width_amount_rejected(self) -> None: """Infinite, NaN, and beyond-float-range amounts are rejected on every way into the contents.""" for amount in (math.inf, math.nan, 10**400): for width_type in (urwid.WEIGHT, urwid.GIVEN): with self.subTest(amount=amount, width_type=width_type): item = (width_type, amount, urwid.Text("a")) with self.assertRaises(urwid.ColumnsError): urwid.Columns([item]) columns = urwid.Columns([urwid.Text("b")]) options = (width_type, amount, False) with self.assertRaises(urwid.ColumnsError): columns.contents.append((urwid.Text("a"), options)) with self.assertRaises(urwid.ColumnsError): columns.contents = [(urwid.Text("a"), options)] def test_huge_finite_weights(self) -> None: """Weights whose sum exceeds the float range still divide the width between them.""" columns = urwid.Columns( [urwid.Text("a"), (urwid.WEIGHT, 1e308, urwid.Text("b")), (urwid.WEIGHT, 1e308, urwid.Text("c"))] ) self.assertEqual([1, 10, 9], columns.column_widths((20,))) self.assertEqual(20, columns.render((20,)).cols()) def test_all_zero_weights(self) -> None: """Columns whose weights are all zero keep their minimum width instead of dividing by a zero total.""" columns = urwid.Columns([(urwid.WEIGHT, 0, urwid.Text("a")), (urwid.WEIGHT, 0, urwid.Text("b"))]) self.assertEqual([1, 1], columns.column_widths((20,))) self.assertEqual(20, columns.render((20,)).cols()) def test_fixed_column_sizes_weights_too_far_apart(self) -> None: """Raise ColumnsError when the weight ratio scales a fixed width past what a float can hold.""" columns = urwid.Columns([(urwid.WEIGHT, 5e-324, urwid.Text("a")), (urwid.WEIGHT, 1, urwid.Text("b"))]) with self.assertRaisesRegex(urwid.ColumnsError, "too far apart"): columns.pack(()) with self.assertRaisesRegex(urwid.ColumnsError, "too far apart"): columns.render(()) def test_keypress_child_changes_contents(self) -> None: """An unhandled key moves the focus from the contents the focused child left behind.""" with self.subTest("Items before the focus removed"): edits = [urwid.Edit("", text) for text in "abc"] columns = urwid.Columns( [*edits, ParentMutatingText("d", lambda: columns.contents.__delitem__(slice(0, 2)))] ) columns.focus_position = 3 self.assertIsNone(columns.keypress((40,), "left")) self.assertIs(edits[2], columns.focus) with self.subTest("All items removed"): columns = urwid.Columns( [urwid.Edit("", "a"), ParentMutatingText("b", lambda: columns.contents.__delitem__(slice(None)))] ) columns.focus_position = 1 self.assertEqual("left", columns.keypress((40,), "left")) def test_move_cursor_to_coords_child_changes_contents(self) -> None: """The focus does not move to a position the child's contents change made stale.""" columns = urwid.Columns([urwid.Edit("", "a"), ParentMutatingText("b", lambda: columns.contents.__delitem__(0))]) self.assertFalse(columns.move_cursor_to_coords((40,), 30, 0)) self.assertEqual(0, columns.focus_position) urwid-urwid-69907e4/tests/widgets/test_container.py000066400000000000000000000117461526073734300225130ustar00rootroot00000000000000from __future__ import annotations import unittest import urwid class WidgetSquishTest(unittest.TestCase): def wstest(self, w): c = w.render((80, 0), focus=False) assert c.rows() == 0 c = w.render((80, 0), focus=True) assert c.rows() == 0 c = w.render((80, 1), focus=False) assert c.rows() == 1 c = w.render((0, 25), focus=False) c = w.render((1, 25), focus=False) def fwstest(self, w): def t(cols: int, focus: bool): wrows = w.rows((cols,), focus) c = w.render((cols,), focus) self.assertEqual(c.rows(), wrows, f"Canvas rows {c.rows()} != widget rows {wrows}") if focus and hasattr(w, "get_cursor_coords"): gcc = w.get_cursor_coords((cols,)) self.assertEqual(c.cursor, gcc, f"Canvas cursor {c.cursor} != widget cursor {gcc}") for cols, focus in ((0, False), (1, False), (0, True), (1, True)): with self.subTest(f"{w.__class__.__name__} cols={cols} and focus={focus}"): t(cols, focus) def test_listbox(self): self.wstest(urwid.ListBox(urwid.SimpleListWalker([]))) self.wstest(urwid.ListBox(urwid.SimpleListWalker([urwid.Text("hello")]))) def test_bargraph(self): self.wstest(urwid.BarGraph(["foo", "bar"])) def test_graphvscale(self): self.wstest(urwid.GraphVScale([(0, "hello")], 1)) self.wstest(urwid.GraphVScale([(5, "hello")], 1)) def test_solidfill(self): self.wstest(urwid.SolidFill()) def test_filler(self): self.wstest(urwid.Filler(urwid.Text("hello"))) def test_overlay(self): self.wstest( urwid.Overlay( urwid.BigText("hello", urwid.Thin6x6Font()), urwid.SolidFill(), "center", None, "middle", None ) ) self.wstest(urwid.Overlay(urwid.Text("hello"), urwid.SolidFill(), "center", ("relative", 100), "middle", None)) def test_frame(self): self.wstest(urwid.Frame(urwid.SolidFill())) self.wstest(urwid.Frame(urwid.SolidFill(), header=urwid.Text("hello"))) self.wstest(urwid.Frame(urwid.SolidFill(), header=urwid.Text("hello"), footer=urwid.Text("hello"))) def test_pile(self): self.wstest(urwid.Pile([urwid.SolidFill()])) self.wstest(urwid.Pile([("flow", urwid.Text("hello"))])) self.wstest(urwid.Pile([])) def test_columns(self): self.wstest(urwid.Columns([urwid.SolidFill()])) self.wstest(urwid.Columns([(4, urwid.SolidFill())])) def test_buttons(self): self.fwstest(urwid.Button("hello")) self.fwstest(urwid.RadioButton([], "hello")) def testFocus(self): expect_focused = urwid.Button("Focused") pile = urwid.Pile((urwid.Button("First"), expect_focused, urwid.Button("Last")), focus_item=expect_focused) self.assertEqual(1, pile.focus_position) self.assertEqual(expect_focused, pile.focus) class CommonContainerTest(unittest.TestCase): def test_list_box(self): lb = urwid.ListBox(urwid.SimpleFocusListWalker([])) self.assertEqual(lb.focus, None) self.assertRaises(IndexError, lambda: getattr(lb, "focus_position")) self.assertRaises(IndexError, lambda: setattr(lb, "focus_position", None)) self.assertRaises(IndexError, lambda: setattr(lb, "focus_position", 0)) t1 = urwid.Text("one") t2 = urwid.Text("two") lb = urwid.ListBox(urwid.SimpleListWalker([t1, t2])) self.assertEqual(lb.focus, t1) self.assertEqual(lb.focus_position, 0) lb.focus_position = 1 self.assertEqual(lb.focus, t2) self.assertEqual(lb.focus_position, 1) lb.focus_position = 0 self.assertRaises(IndexError, lambda: setattr(lb, "focus_position", -1)) self.assertRaises(IndexError, lambda: setattr(lb, "focus_position", 2)) def test_focus_path(self): # big tree of containers t = urwid.Text("x") e = urwid.Edit("?") c = urwid.Columns([t, e, t, t]) p = urwid.Pile([t, t, c, t]) a = urwid.AttrMap(p, "gets ignored") s = urwid.SolidFill("/") o = urwid.Overlay(e, s, "center", "pack", "middle", "pack") lb = urwid.ListBox(urwid.SimpleFocusListWalker([t, a, o, t])) lb.focus_position = 1 g = urwid.GridFlow([t, t, t, t, e, t], 10, 0, 0, "left") g.focus_position = 4 f = urwid.Frame(lb, header=t, footer=g) self.assertEqual(f.get_focus_path(), ["body", 1, 2, 1]) f.set_focus_path(["footer"]) # same as f.focus_position = 'footer' self.assertEqual(f.get_focus_path(), ["footer", 4]) f.set_focus_path(["body", 1, 2, 2]) self.assertEqual(f.get_focus_path(), ["body", 1, 2, 2]) self.assertRaises(IndexError, lambda: f.set_focus_path([0, 1, 2])) self.assertRaises(IndexError, lambda: f.set_focus_path(["body", 2, 2])) f.set_focus_path(["body", 2]) # focus the overlay self.assertEqual(f.get_focus_path(), ["body", 2, 1]) urwid-urwid-69907e4/tests/widgets/test_deque_walker.py000066400000000000000000000371361526073734300232020ustar00rootroot00000000000000from __future__ import annotations import collections import decimal import math import unittest import warnings import urwid class MonitoredDequeConstructionTest(unittest.TestCase): def test_from_generator(self) -> None: walker = urwid.SimpleDequeWalker(str(num) for num in range(5)) self.assertEqual(5, len(walker)) def test_from_generator_focus(self) -> None: walker = urwid.SimpleFocusDequeWalker(str(num) for num in range(5)) self.assertEqual(5, len(walker)) def test_non_iterable_raises(self) -> None: with self.assertRaises(urwid.ListWalkerError): urwid.SimpleDequeWalker(123) with self.assertRaises(urwid.ListWalkerError): urwid.SimpleFocusDequeWalker(123) class MonitoredDequeMaxlenParityTest(unittest.TestCase): """Verify a bounded MonitoredDeque matches a plain collections.deque with the same maxlen. Runs the same operation sequence against both, asserting equal contents after each step. """ def test_parity(self) -> None: maxlen = 4 monitored = urwid.MonitoredDeque(maxlen=maxlen) reference: collections.deque = collections.deque(maxlen=maxlen) operations = [ ("append", 1), ("append", 2), ("append", 3), ("appendleft", 0), ("append", 4), # triggers eviction of head ("appendleft", -1), # triggers eviction of tail ("extend", [10, 11, 12]), ("extendleft", [20, 21]), ("rotate", 1), ("rotate", -2), ("reverse", None), ("pop", None), ("popleft", None), ] for name, arg in operations: with self.subTest(name=name, arg=arg): if arg is None: # pylint incorrectly infers a fixed signature for these dynamically-dispatched # calls, treating `name` as though it always resolved to an argument-taking # method such as ``append``. getattr(monitored, name)() # pylint: disable=no-value-for-parameter getattr(reference, name)() # pylint: disable=no-value-for-parameter else: getattr(monitored, name)(arg) getattr(reference, name)(arg) self.assertEqual(list(reference), list(monitored)) class MonitoredDequeModifiedCallbackTest(unittest.TestCase): def test_modified_fires_once(self) -> None: counter = {"count": 0} def bump() -> None: counter["count"] += 1 md = urwid.MonitoredDeque([1, 2, 3], maxlen=3) md.set_modified_callback(bump) md.append(4) # evicts head self.assertEqual(1, counter["count"]) md.appendleft(0) # evicts tail self.assertEqual(2, counter["count"]) md.extend([5, 6]) # evicts multiple self.assertEqual(3, counter["count"]) md.extendleft([7, 8]) self.assertEqual(4, counter["count"]) md.pop() self.assertEqual(5, counter["count"]) md.popleft() self.assertEqual(6, counter["count"]) md.rotate(1) self.assertEqual(7, counter["count"]) md.clear() self.assertEqual(8, counter["count"]) class MonitoredFocusDequeModifiedCallbackTest(unittest.TestCase): """The modified callback runs once, after both the contents and the focus are updated.""" def test_callback_sees_final_focus(self) -> None: """Every mutating operation notifies once, with the contents and the focus already final.""" operations = { "del item": lambda md: md.__delitem__(0), "set item": lambda md: md.__setitem__(2, 7), "append": lambda md: md.append(6), "appendleft": lambda md: md.appendleft(-1), "extend": lambda md: md.extend([6, 7]), "iadd": lambda md: md.__iadd__([6, 7]), "extendleft": lambda md: md.extendleft([-1, -2]), "insert": lambda md: md.insert(0, -1), "pop": lambda md: md.pop(), "popleft": lambda md: md.popleft(), "remove": lambda md: md.remove(0), "reverse": lambda md: md.reverse(), "rotate": lambda md: md.rotate(2), "clear": lambda md: md.clear(), } for maxlen in (None, 6): for name, operation in operations.items(): if maxlen is not None and name == "insert": continue # deque.insert raises on a full bounded deque with self.subTest(name, maxlen=maxlen): md = urwid.MonitoredFocusDeque(range(6), maxlen, focus=2) seen = [] md.set_modified_callback(lambda md=md, seen=seen: seen.append((list(md), md.focus))) operation(md) self.assertEqual([(list(md), md.focus)], seen) def test_iadd_keeps_focused_item_like_extend(self) -> None: """In-place addition evicts from a bounded deque and keeps the focused item, the same as extend.""" for maxlen in (None, 4): with self.subTest(maxlen=maxlen): extended = urwid.MonitoredFocusDeque("abc", maxlen, focus=2) extended.extend("xyz") added = urwid.MonitoredFocusDeque("abc", maxlen, focus=2) added += "xyz" self.assertIsInstance(added, urwid.MonitoredFocusDeque) self.assertEqual((list(extended), extended.focus), (list(added), added.focus)) self.assertEqual("c", added[added.focus]) def test_focus_set_by_callback_is_kept(self) -> None: """A focus set by the modified callback is not overwritten afterwards.""" md = urwid.MonitoredFocusDeque(range(6), focus=2) md.set_modified_callback(lambda: setattr(md, "focus", 0) if md.focus else None) md.appendleft(-1) self.assertEqual(0, md.focus) def test_insert_by_callback_keeps_focus_item(self) -> None: """An insertion made by the modified callback keeps the focus on the same item.""" md = urwid.MonitoredFocusDeque(["a", "b", "c", "d"], focus=2) md.set_modified_callback(lambda: md.appendleft("x") if len(md) == 3 else None) md.popleft() self.assertEqual(["x", "b", "c", "d"], list(md)) self.assertEqual("c", md[md.focus]) def test_walker_signal_sees_final_focus(self) -> None: """A ListBox rendered from the walker's modified signal shows the new focus item.""" walker = urwid.SimpleFocusDequeWalker([urwid.Text(str(num)) for num in range(3)]) walker.focus = 2 listbox = urwid.ListBox(walker) seen = [] def on_modified() -> None: seen.append((walker.get_focus()[1], listbox.render((5, 1), focus=True).text)) urwid.connect_signal(walker, "modified", on_modified) walker.pop() self.assertEqual([(1, [b"1 "])], seen) class MonitoredFocusDequeFocusAdjustmentTest(unittest.TestCase): def test_focus_evicted_by_append(self) -> None: mfd = urwid.MonitoredFocusDeque([1, 2, 3], maxlen=3, focus=0) mfd.append(4) # evicts head (the focus item) self.assertEqual([2, 3, 4], list(mfd)) self.assertEqual(0, mfd.focus) def test_focus_evicted_by_appendleft(self) -> None: mfd = urwid.MonitoredFocusDeque([1, 2, 3], maxlen=3, focus=2) mfd.appendleft(0) # evicts tail (the focus item) self.assertEqual([0, 1, 2], list(mfd)) self.assertEqual(2, mfd.focus) def test_focus_shift_without_eviction(self) -> None: mfd = urwid.MonitoredFocusDeque([1, 2, 3], focus=1) mfd.appendleft(0) self.assertEqual([0, 1, 2, 3], list(mfd)) self.assertEqual(2, mfd.focus) def test_rotate_positive(self) -> None: mfd = urwid.MonitoredFocusDeque([0, 1, 2, 3, 4], focus=0) mfd.rotate(1) self.assertEqual([4, 0, 1, 2, 3], list(mfd)) self.assertEqual(1, mfd.focus) def test_rotate_negative(self) -> None: mfd = urwid.MonitoredFocusDeque([0, 1, 2, 3, 4], focus=0) mfd.rotate(-1) self.assertEqual([1, 2, 3, 4, 0], list(mfd)) self.assertEqual(4, mfd.focus) def test_clear_focus_is_none(self) -> None: mfd = urwid.MonitoredFocusDeque([1, 2, 3], focus=1) mfd.clear() self.assertIsNone(mfd.focus) def test_maxlen_zero(self) -> None: mfd = urwid.MonitoredFocusDeque([1, 2, 3], maxlen=0) self.assertIsNone(mfd.focus) mfd.append(1) self.assertEqual(0, len(mfd)) self.assertIsNone(mfd.focus) class _Index: """Integer-like object that supports only ``__index__``, as ``deque.insert()`` requires.""" def __index__(self) -> int: return 1 class MonitoredDequeInsertIndexTest(unittest.TestCase): """Check ``insert()`` index conversion against ``collections.deque.insert()``.""" classes = (urwid.MonitoredDeque, urwid.MonitoredFocusDeque) def test_integer_index_does_not_warn(self) -> None: """Insert at an ``int``, a ``bool`` and an ``__index__`` object like ``deque`` does, with no warning.""" for cls in self.classes: for index in (1, True, _Index()): with self.subTest(cls=cls.__name__, index=index), warnings.catch_warnings(): warnings.simplefilter("error") reference = collections.deque([0, 1, 2]) reference.insert(index, 9) deq = cls([0, 1, 2]) deq.insert(index, 9) self.assertEqual(list(reference), list(deq)) def test_non_integer_index_warns(self) -> None: """Insert at an index without ``__index__``, converted by ``int()``, with one deprecation warning.""" for cls in self.classes: for index in (1.5, decimal.Decimal(1), "1"): with self.subTest(cls=cls.__name__, index=index): deq = cls([0, 1, 2]) with warnings.catch_warnings(record=True) as caught: warnings.simplefilter("always") deq.insert(index, 9) message = ( f"{cls.__name__}.insert() with an index of type {type(index).__name__} is deprecated: " "pass an int or an object with __index__. " "Support will be removed in version 5.0, after which it raises TypeError like deque.insert()." ) self.assertEqual( [(DeprecationWarning, message, __file__)], [(item.category, str(item.message), item.filename) for item in caught], ) self.assertEqual([0, 9, 1, 2], list(deq)) def test_rejected_index_raises_as_before(self) -> None: """Raise what ``int()`` raises for an index it rejects, unchained, with no warning and no change.""" for cls in self.classes: for index, error in ((None, TypeError), ("abc", ValueError), (math.inf, OverflowError)): with self.subTest(cls=cls.__name__, index=index), warnings.catch_warnings(): warnings.simplefilter("error") deq = cls([0, 1, 2]) with self.assertRaises(error) as raised: deq.insert(index, 9) self.assertIsNone(raised.exception.__context__) self.assertEqual([0, 1, 2], list(deq)) class WrapAroundTest(unittest.TestCase): def test_simple_deque_walker_wrap_true(self) -> None: walker = urwid.SimpleDequeWalker([1, 2, 3], wrap_around=True) self.assertEqual(0, walker.next_position(2)) self.assertEqual(2, walker.prev_position(0)) def test_simple_deque_walker_wrap_false(self) -> None: walker = urwid.SimpleDequeWalker([1, 2, 3], wrap_around=False) with self.assertRaises(IndexError): walker.next_position(2) with self.assertRaises(IndexError): walker.prev_position(0) def test_simple_focus_deque_walker_wrap_true(self) -> None: walker = urwid.SimpleFocusDequeWalker([1, 2, 3], wrap_around=True) self.assertEqual(0, walker.next_position(2)) self.assertEqual(2, walker.prev_position(0)) def test_simple_focus_deque_walker_wrap_false(self) -> None: walker = urwid.SimpleFocusDequeWalker([1, 2, 3], wrap_around=False) with self.assertRaises(IndexError): walker.next_position(2) with self.assertRaises(IndexError): walker.prev_position(0) class SimpleDequeWalkerFocusTest(unittest.TestCase): def test_contents_returns_self(self) -> None: walker = urwid.SimpleDequeWalker([1, 2, 3]) self.assertIs(walker, walker.contents) def test_set_focus(self) -> None: walker = urwid.SimpleDequeWalker([1, 2, 3]) walker.set_focus(2) self.assertEqual(2, walker.focus) def test_set_focus_out_of_range_raises(self) -> None: walker = urwid.SimpleDequeWalker([1, 2, 3]) with self.assertRaises(IndexError): walker.set_focus(3) with self.assertRaises(IndexError): walker.set_focus(-1) def test_modified_clamps_focus_on_shrink(self) -> None: walker = urwid.SimpleDequeWalker([1, 2, 3]) walker.focus = 2 walker.pop() walker.pop() self.assertEqual(0, walker.focus) def test_next_prev_position(self) -> None: walker = urwid.SimpleDequeWalker([1, 2, 3]) self.assertEqual(1, walker.next_position(0)) self.assertEqual(0, walker.prev_position(1)) def test_positions(self) -> None: walker = urwid.SimpleDequeWalker([1, 2, 3]) self.assertEqual([0, 1, 2], list(walker.positions())) self.assertEqual([2, 1, 0], list(walker.positions(reverse=True))) class SimpleFocusDequeWalkerFocusTest(unittest.TestCase): def test_set_focus(self) -> None: walker = urwid.SimpleFocusDequeWalker([1, 2, 3]) walker.set_focus(2) self.assertEqual(2, walker.focus) def test_next_prev_position(self) -> None: walker = urwid.SimpleFocusDequeWalker([1, 2, 3]) self.assertEqual(1, walker.next_position(0)) self.assertEqual(0, walker.prev_position(1)) def test_positions(self) -> None: walker = urwid.SimpleFocusDequeWalker([1, 2, 3]) self.assertEqual([0, 1, 2], list(walker.positions())) self.assertEqual([2, 1, 0], list(walker.positions(reverse=True))) class ListBoxIntegrationTest(unittest.TestCase): def test_listbox_with_simple_focus_deque_walker(self) -> None: texts = [urwid.Text(str(num)) for num in range(5)] walker = urwid.SimpleFocusDequeWalker(texts, maxlen=3) lb = urwid.ListBox(walker) # Only the last 3 items should have survived construction eviction. self.assertEqual(3, len(lb.body)) lb.body.append(urwid.Text("new")) self.assertEqual(3, len(lb.body)) self.assertTrue(0 <= lb.focus_position < len(lb.body)) def test_signal_connected(self) -> None: lb = urwid.ListBox([]) lb.body = urwid.SimpleDequeWalker([]) self.assertEqual( lb.body._urwid_signals["modified"][0][1], urwid.ListBox._invalidate, "outdated canvas cache reuse after ListWalker's contents modified", ) def test_signal_connected_focus(self) -> None: lb = urwid.ListBox([]) lb.body = urwid.SimpleFocusDequeWalker([]) self.assertEqual( lb.body._urwid_signals["modified"][0][1], urwid.ListBox._invalidate, "outdated canvas cache reuse after ListWalker's contents modified", ) if __name__ == "__main__": unittest.main() urwid-urwid-69907e4/tests/widgets/test_divider.py000066400000000000000000000021261526073734300221470ustar00rootroot00000000000000from __future__ import annotations import unittest import urwid from urwid.util import get_encoding class DividerTest(unittest.TestCase): def setUp(self) -> None: self.old_encoding = get_encoding() urwid.set_encoding("utf-8") def tearDown(self) -> None: urwid.set_encoding(self.old_encoding) def test_default_and_div_char(self) -> None: blank = urwid.Divider() dashed = urwid.Divider("-") self.assertEqual(frozenset((urwid.FLOW,)), blank.sizing()) self.assertEqual(1, blank.rows((10,))) self.assertEqual([b" "], blank.render((10,)).text) self.assertEqual([b"-----"], dashed.render((5,)).text) def test_symbols_and_vertical_padding(self) -> None: divider = urwid.Divider(div_char=urwid.Divider.Symbols.LIGHT_HL, top=1, bottom=1) self.assertEqual("─", urwid.Divider.Symbols.LIGHT_HL) self.assertEqual(3, divider.rows((4,))) self.assertEqual( [" ", "────", " "], [line.decode("utf-8") for line in divider.render((4,)).text], ) urwid-urwid-69907e4/tests/widgets/test_edit.py000066400000000000000000000405501526073734300214510ustar00rootroot00000000000000from __future__ import annotations import sys import types import unittest from unittest import mock import urwid from urwid.util import set_temporary_encoding class EditTest(unittest.TestCase): def test_selectable(self) -> None: self.assertTrue(urwid.Edit().selectable()) def test_sizing(self) -> None: self.assertEqual(urwid.Edit().sizing(), frozenset([urwid.FLOW])) def test_get_text_masked(self) -> None: e = urwid.Edit("pw:", "secret", mask="*") self.assertEqual(e.get_text(), ("pw:******", [])) def test_repr_masked_hides_text(self) -> None: """repr() of a masked Edit shows the mask, never the text.""" e = urwid.Edit("pw: ", "hunter2", mask="*") self.assertEqual(repr(e), "") def test_insert_text_result_error_masked_hides_text(self) -> None: """The ValueError raised for a masked Edit does not carry the text.""" e = urwid.Edit("", "hunter2", mask="*") e._edit_text = b"hunter2" with self.assertRaises(ValueError) as ctx: e.insert_text_result("x") self.assertNotIn("hunter2", str(ctx.exception)) def test_valid_char_rejects_control_characters(self) -> None: """Control characters, which render as '?' but count as zero columns, are refused.""" e = urwid.Edit() for ch in ("\x00", "\x1b", "\x7f", "\x80", "\x9b", "\x9f"): with self.subTest(ch=ch): self.assertFalse(e.valid_char(ch)) self.assertEqual(e.keypress((20,), ch), ch) self.assertEqual(e.edit_text, "") for ch in (" ", "a", "~", "\xa0", "\xe9", "\u044f"): with self.subTest(ch=ch): self.assertTrue(e.valid_char(ch)) def test_valid_char_bytes(self) -> None: """A byte is refused only when it is a C0 control or DEL: 0x80-0xff are letters in KOI8-R.""" e = urwid.Edit(b"", b"") with set_temporary_encoding("koi8-r"): for ch in (b"\x00", b"\x1f", b"\x7f"): with self.subTest(ch=ch): self.assertFalse(e.valid_char(ch)) for ch in (b" ", b"a", b"\x80", b"\x9f", b"\xbf", b"\xff"): with self.subTest(ch=ch): self.assertTrue(e.valid_char(ch)) def test_text_property(self) -> None: e = urwid.Edit("Y/n? ", "yes") self.assertEqual(e.text, "Y/n? yes") def test_attrib_property(self) -> None: e = urwid.Edit(("bold", "cap "), "text") self.assertEqual(e.attrib, [("bold", 4)]) def test_set_align_mode(self) -> None: e = urwid.Edit() e.set_align_mode(urwid.RIGHT) self.assertEqual(e.align, urwid.RIGHT) def test_set_wrap_mode(self) -> None: e = urwid.Edit() e.set_wrap_mode(urwid.CLIP) self.assertEqual(e.wrap, urwid.CLIP) def test_set_layout(self) -> None: e = urwid.Edit("", "hello world", wrap="space") e.set_layout(urwid.CENTER, urwid.CLIP) self.assertEqual(e.align, urwid.CENTER) self.assertEqual(e.wrap, urwid.CLIP) def test_layout_property(self) -> None: e = urwid.Edit() self.assertIs(e.layout, e._w.layout) def test_set_caption(self) -> None: e = urwid.Edit("") e.set_caption("cap1") self.assertEqual(e.caption, "cap1") e.set_caption(("bold", "cap2")) self.assertEqual(e.caption, "cap2") self.assertEqual(e.attrib, [("bold", 4)]) def test_normalize_to_caption_bytes_caption(self) -> None: e = urwid.Edit(b"cap: ", b"") e.insert_text("hi") self.assertEqual(e.edit_text, b"hi") def test_normalize_to_caption_str_caption(self) -> None: e = urwid.Edit("cap: ", "") e.insert_text(b"hi") self.assertEqual(e.edit_text, "hi") def test_get_pref_col_cached(self) -> None: size = (10,) e = urwid.Edit("", "word") e.keypress(size, "left") self.assertEqual(e.get_pref_col(size), 3) # Same maxcol as last time: hits the cached pref_col_maxcol branch. self.assertEqual(e.get_pref_col(size), 3) def test_insert_text_result_with_highlight(self) -> None: e = urwid.Edit("", "hello world") e.highlight = (0, 5) result_text, result_pos = e.insert_text_result("HI") self.assertEqual(result_text, "HI world") self.assertEqual(result_pos, 2) def test_insert_text_result_raises_value_error(self) -> None: e = urwid.Edit("", "hi") # Force a type mismatch between edit_text and the inserted text so the # concatenation in insert_text_result() raises, which is re-raised as ValueError. e._edit_text = b"hi" with self.assertRaises(ValueError): e.insert_text_result("x") def test_keypress_encodes_str_key_for_bytes_caption(self) -> None: e = urwid.Edit(b"cap: ", b"") e.keypress((20,), "x") self.assertEqual(e.edit_text, b"x") def test_keypress_left(self) -> None: e = urwid.Edit("", "word") e.keypress((20,), "left") self.assertEqual(e.edit_pos, 3) def test_keypress_left_at_start(self) -> None: e = urwid.Edit("", "word", edit_pos=0) self.assertEqual(e.keypress((20,), "left"), "left") def test_keypress_right(self) -> None: e = urwid.Edit("", "word", edit_pos=0) e.keypress((20,), "right") self.assertEqual(e.edit_pos, 1) def test_keypress_right_at_end(self) -> None: e = urwid.Edit("", "word") self.assertEqual(e.keypress((20,), "right"), "right") def test_keypress_down(self) -> None: e = urwid.Edit("", "one\ntwo", multiline=True, edit_pos=1) e.keypress((20,), "down") self.assertEqual(e.edit_pos, 5) def test_keypress_up(self) -> None: e = urwid.Edit("", "one\ntwo", multiline=True, edit_pos=5) e.keypress((20,), "up") self.assertEqual(e.edit_pos, 1) def test_keypress_up_out_of_bounds_returns_key(self) -> None: e = urwid.Edit("", "word") self.assertEqual(e.keypress((20,), "up"), "up") def test_keypress_tab_allowed(self) -> None: e = urwid.Edit("", "", allow_tab=True) e.keypress((20,), "tab") self.assertEqual(e.edit_text, " " * 8) def test_keypress_tab_expanded_to_next_stop(self) -> None: e = urwid.Edit("", "abc", allow_tab=True) e.keypress((20,), "tab") self.assertEqual(e.edit_text, "abc ") def test_keypress_tab_expanded_by_rendered_width(self) -> None: # "中文" is 2 characters, but 4 screen columns e = urwid.Edit("", "中文", allow_tab=True) e.keypress((20,), "tab") self.assertEqual(e.edit_text, "中文 ") def test_keypress_tab_expanded_after_zwj_sequence(self) -> None: # 3 code points joined by ZWJ, 2 screen columns e = urwid.Edit("", "👩�💻", allow_tab=True) e.keypress((20,), "tab") self.assertEqual(e.edit_text, "👩�💻 ") def test_keypress_tab_expanded_from_line_start(self) -> None: e = urwid.Edit("", "abc\nd", multiline=True, allow_tab=True) e.keypress((20,), "tab") self.assertEqual(e.edit_text, "abc\nd ") def test_keypress_tab_expanded_after_tab_character(self) -> None: e = urwid.Edit("", "a\tbc", allow_tab=True) e.keypress((20,), "tab") self.assertEqual(e.edit_text, "a\tbc ") def test_keypress_tab_expanded_with_layout_tab_stops(self) -> None: e = urwid.Edit("", "a", allow_tab=True, layout=urwid.StandardTextLayout(tab_stops=(3,))) e.keypress((20,), "tab") self.assertEqual(e.edit_text, "a ") def test_keypress_tab_expanded_with_custom_layout(self) -> None: class CustomLayout(urwid.TextLayout): def layout(self, text: str, width: int, align: str, wrap: str) -> list[list[tuple[int, int, int]]]: return [[(len(text), 0, len(text))]] if text else [[]] e = urwid.Edit("", "ab", allow_tab=True, layout=CustomLayout()) e.keypress((20,), "tab") self.assertEqual(e.edit_text, "ab ") def test_keypress_tab_expanded_to_zero_width(self) -> None: e = urwid.Edit("", "ab", allow_tab=True, layout=urwid.StandardTextLayout(tab_stop_every=0)) e.keypress((20,), "tab") self.assertEqual(e.edit_text, "ab") def test_keypress_tab_expanded_bytes(self) -> None: e = urwid.Edit(b"", b"ab", allow_tab=True) e.keypress((20,), "tab") self.assertEqual(e.edit_text, b"ab ") def test_keypress_tab_character(self) -> None: e = urwid.Edit("", "ab", allow_tab=True, edit_pos=1, expand_tabs=False) e.keypress((20,), "tab") self.assertEqual(e.edit_text, "a\tb") self.assertEqual(e.edit_pos, 2) self.assertEqual(e.get_cursor_coords((20,)), (8, 0)) def test_keypress_tab_character_bytes(self) -> None: e = urwid.Edit(b"", b"", allow_tab=True, expand_tabs=False) e.keypress((20,), "tab") self.assertEqual(e.edit_text, b"\t") def test_keypress_tab_not_allowed(self) -> None: e = urwid.Edit("", "") self.assertEqual(e.keypress((20,), "tab"), "tab") def test_keypress_enter_multiline(self) -> None: e = urwid.Edit("", "ab", multiline=True, edit_pos=1) e.keypress((20,), "enter") self.assertEqual(e.edit_text, "a\nb") def test_keypress_enter_not_multiline(self) -> None: e = urwid.Edit("", "ab") self.assertEqual(e.keypress((20,), "enter"), "enter") def test_keypress_up_down_raises_when_pref_col_none(self) -> None: e = urwid.Edit("", "hello") def broken_get_pref_col(size: tuple[int]) -> None: return None e.get_pref_col = broken_get_pref_col # type: ignore[method-assign] with self.assertRaises(ValueError): e.keypress((20,), "up") def test_keypress_backspace_with_highlight(self) -> None: e = urwid.Edit("", "hello world") e.highlight = (0, 6) e.keypress((20,), "backspace") self.assertEqual(e.edit_text, "world") self.assertIsNone(e.highlight) def test_keypress_backspace_at_start(self) -> None: e = urwid.Edit("", "hi", edit_pos=0) self.assertEqual(e.keypress((20,), "backspace"), "backspace") def test_keypress_backspace_removes_char(self) -> None: e = urwid.Edit("", "hi") e.keypress((20,), "backspace") self.assertEqual(e.edit_text, "h") def test_keypress_delete_with_highlight(self) -> None: e = urwid.Edit("", "hello world") e.highlight = (0, 6) e.keypress((20,), "delete") self.assertEqual(e.edit_text, "world") self.assertIsNone(e.highlight) def test_keypress_delete_at_end(self) -> None: e = urwid.Edit("", "hi", edit_pos=2) self.assertEqual(e.keypress((20,), "delete"), "delete") def test_keypress_delete_removes_char(self) -> None: e = urwid.Edit("", "hi", edit_pos=0) e.keypress((20,), "delete") self.assertEqual(e.edit_text, "i") def test_keypress_max_right(self) -> None: e = urwid.Edit("", "hello", edit_pos=0) e.keypress((20,), "end") self.assertEqual(e.edit_pos, 5) def test_mouse_event_button1(self) -> None: e = urwid.Edit("", "words here") self.assertTrue(e.mouse_event((20,), "mouse press", 1, 2, 0, True)) self.assertEqual(e.edit_pos, 2) def test_mouse_event_other_button(self) -> None: e = urwid.Edit("", "words here") self.assertFalse(e.mouse_event((20,), "mouse press", 2, 2, 0, True)) def test_render(self) -> None: e = urwid.Edit("? ", "yes") canv = e.render((10,), focus=True) self.assertEqual(list(canv.text), [b"? yes "]) self.assertEqual(canv.cursor, (5, 0)) def test_render_no_focus(self) -> None: e = urwid.Edit("? ", "yes") canv = e.render((10,), focus=False) self.assertIsNone(canv.cursor) def test_get_line_translation_shifts_view_left(self) -> None: e = urwid.Edit("", "a very long line of text", wrap=urwid.CLIP) e.set_edit_pos(len(e.edit_text)) canv = e.render((5,), focus=True) self.assertEqual(canv.cursor[0], 4) def test_get_line_translation_shifts_view_right(self) -> None: e = urwid.Edit("", "a very long line of text", wrap=urwid.CLIP) e.set_edit_pos(len(e.edit_text)) e.render((5,), focus=True) e.set_edit_pos(0) canv = e.render((5,), focus=True) self.assertEqual(canv.cursor[0], 0) def test_cursor_after_control_characters(self) -> None: """The cursor sits where its character is drawn, with the control characters left out of the canvas.""" e = urwid.Edit("\x07> ", "a\x1b[1mb") canv = e.render((10,), focus=True) self.assertEqual([b"> ab "], canv.text) self.assertEqual((4, 0), canv.cursor) e.set_edit_pos(1) self.assertEqual((3, 0), e.render((10,), focus=True).cursor) self.assertEqual("a\x1b[1mb", e.edit_text) self.assertEqual(("\x07> a\x1b[1mb", []), e.get_text()) def test_delete_highlighted_no_highlight(self) -> None: e = urwid.Edit("", "hi") self.assertFalse(e._delete_highlighted()) def test_delete_highlighted_with_highlight(self) -> None: e = urwid.Edit("", "hello world") e.highlight = (0, 6) self.assertTrue(e._delete_highlighted()) self.assertEqual(e.edit_text, "world") self.assertEqual(e.edit_pos, 0) self.assertIsNone(e.highlight) class IntEditTest(unittest.TestCase): def test_valid_char(self) -> None: e = urwid.IntEdit() self.assertTrue(e.valid_char("5")) self.assertFalse(e.valid_char("a")) def test_default_none(self) -> None: e = urwid.IntEdit() self.assertEqual(e.edit_text, "") def test_keypress_trims_leading_zeros(self) -> None: e = urwid.IntEdit("", 5002) e.keypress((10,), "home") e.keypress((10,), "delete") self.assertEqual(e.edit_text, "002") e.keypress((10,), "end") self.assertEqual(e.edit_text, "2") def test_keypress_unhandled_passthrough(self) -> None: e = urwid.IntEdit() self.assertEqual(e.keypress((10,), "shift f1"), "shift f1") def test_value(self) -> None: e = urwid.IntEdit() e.keypress((10,), "5") e.keypress((10,), "1") self.assertEqual(e.value(), 51) @unittest.skipUnless(hasattr(sys, "get_int_max_str_digits"), "the interpreter has no int digit limit") def test_keypress_refuses_digits_beyond_int_limit(self) -> None: """Typing stops at the int digit limit, so value() still converts the text.""" e = urwid.IntEdit("", "1" * 9) with mock.patch.object(sys, "get_int_max_str_digits", return_value=10): self.assertIsNone(e.keypress((20,), "2")) self.assertEqual(e.keypress((20,), "3"), "3") self.assertEqual(e.edit_text, "1" * 9 + "2") self.assertEqual(e.value(), 1111111112) def test_keypress_without_int_limit(self) -> None: """A limit of 0, or an interpreter without the limit, accepts digits without bound.""" with self.subTest("limit 0"): e = urwid.IntEdit("", "1" * 9) with mock.patch.object(sys, "get_int_max_str_digits", return_value=0, create=True): self.assertIsNone(e.keypress((20,), "2")) self.assertEqual(e.edit_text, "1" * 9 + "2") with self.subTest("no limit function"): e = urwid.IntEdit("", "1" * 9) with mock.patch.object(urwid.widget.edit, "sys", types.SimpleNamespace()): self.assertIsNone(e.keypress((20,), "2")) self.assertEqual(e.edit_text, "1" * 9 + "2") @unittest.skipUnless(hasattr(sys, "get_int_max_str_digits"), "the interpreter has no int digit limit") def test_keypress_replaces_highlight_at_int_limit(self) -> None: """A digit replacing a highlighted range is accepted when the result stays within the limit.""" e = urwid.IntEdit("", "1" * 10) e.highlight = (0, 2) with mock.patch.object(sys, "get_int_max_str_digits", return_value=10): self.assertIsNone(e.keypress((20,), "2")) self.assertEqual(e.edit_text, "2" + "1" * 8) def test_value_empty(self) -> None: e = urwid.IntEdit() self.assertEqual(e.value(), 0) if __name__ == "__main__": unittest.main() urwid-urwid-69907e4/tests/widgets/test_filler.py000066400000000000000000000415161526073734300220040ustar00rootroot00000000000000from __future__ import annotations import unittest import warnings import urwid class CursorWidget(urwid.Widget): """Minimal selectable widget with a scripted cursor, used to drive Filler's cursor-related methods.""" def __init__( self, sizing: tuple[urwid.Sizing, ...], num_rows: int = 1, cursor: tuple[int, int] | None = (0, 0), move_result: bool = True, mouse_result: bool = True, ) -> None: super().__init__() self._sizing_set = frozenset(sizing) self.num_rows = num_rows self.cursor_pos = cursor self.move_result = move_result self.mouse_result = mouse_result def sizing(self) -> frozenset[urwid.Sizing]: return self._sizing_set def selectable(self) -> bool: return True def rows(self, size: tuple[int], focus: bool = False) -> int: return self.num_rows def render(self, size: tuple[int, int] | tuple[int], focus: bool = False) -> urwid.TextCanvas: maxcol = size[0] maxrow = size[1] if len(size) == 2 else self.num_rows return urwid.TextCanvas([b" " * maxcol for _ in range(maxrow)], maxcol=maxcol) def keypress(self, size: tuple[int, int] | tuple[int], key: str) -> str | None: return None def get_cursor_coords(self, size: tuple[int, int] | tuple[int]) -> tuple[int, int] | None: return self.cursor_pos def get_pref_col(self, size: tuple[int, int] | tuple[int]) -> int: return self.cursor_pos[0] if self.cursor_pos else 0 def move_cursor_to_coords(self, size: tuple[int, int] | tuple[int], col: int, row: int) -> bool: return self.move_result def mouse_event( self, size: tuple[int, int] | tuple[int], event: str, button: int, col: int, row: int, focus: bool, ) -> bool: return self.mouse_result class PlainBoxWidget: """A box widget that implements only the bare minimum, lacking cursor-related methods entirely. Unlike a :class:`urwid.Widget` subclass (which inherits default ``mouse_event``/``keypress`` implementations), this class has no ``get_cursor_coords``, ``get_pref_col``, ``move_cursor_to_coords``, or ``mouse_event`` attribute at all, so ``hasattr`` checks on it are ``False``. """ def sizing(self) -> frozenset[urwid.Sizing]: return frozenset((urwid.BOX,)) def selectable(self) -> bool: return False def pack(self, size: tuple[int, int], focus: bool = False) -> tuple[int, int]: return size def render(self, size: tuple[int, int], focus: bool = False) -> urwid.TextCanvas: maxcol, maxrow = size return urwid.TextCanvas([b" " * maxcol for _ in range(maxrow)], maxcol=maxcol) class TrimWidget(urwid.Widget): """Flow widget rendering a fixed number of rows with an optional scripted cursor, for render() trim tests.""" _sizing = frozenset([urwid.FLOW]) def __init__(self, num_rows: int, cursor: tuple[int, int] | None = None) -> None: super().__init__() self.num_rows = num_rows self.cursor_pos = cursor def selectable(self) -> bool: return True def rows(self, size: tuple[int], focus: bool = False) -> int: return self.num_rows def render(self, size: tuple[int], focus: bool = False) -> urwid.TextCanvas: maxcol = size[0] cursor = self.cursor_pos if focus else None return urwid.TextCanvas([b" " * maxcol for _ in range(self.num_rows)], maxcol=maxcol, cursor=cursor) def keypress(self, size: tuple[int], key: str) -> str | None: return None class FillerTest(unittest.TestCase): def ftest( self, desc: str, valign, height, maxrow: int, top: int, bottom: int, min_height: int | None = None, ) -> None: with self.subTest(desc): f = urwid.Filler(None, valign, height, min_height) t, b = f.filler_values((20, maxrow), False) self.assertEqual( (t, b), (top, bottom), f"{desc} expected {top, bottom} but got {t, b}", ) def fetest(self, desc: str, valign, height) -> None: with self.subTest(desc): self.assertRaises(urwid.FillerError, urwid.Filler, None, valign, height) def test_create(self): self.fetest("invalid pad", 6, 5) self.fetest("invalid pad type", ("bad", 2), 5) self.fetest("invalid width", "middle", "42") self.fetest("invalid width type", "middle", ("gouranga", 4)) self.fetest("invalid combination", ("relative", 20), ("fixed bottom", 4)) self.fetest("invalid combination 2", ("relative", 20), ("fixed top", 4)) def test_values(self): self.ftest("top align 5 7", "top", 5, 7, 0, 2) self.ftest("top align 7 7", "top", 7, 7, 0, 0) self.ftest("top align 9 7", "top", 9, 7, 0, 0) self.ftest("bottom align 5 7", "bottom", 5, 7, 2, 0) self.ftest("middle align 5 7", "middle", 5, 7, 1, 1) self.ftest("fixed top", ("fixed top", 3), 5, 10, 3, 2) self.ftest("fixed top reduce", ("fixed top", 3), 8, 10, 2, 0) self.ftest("fixed top shrink", ("fixed top", 3), 18, 10, 0, 0) self.ftest( "fixed top, bottom", ("fixed top", 3), ("fixed bottom", 4), 17, 3, 4, ) self.ftest( "fixed top, bottom, min_width", ("fixed top", 3), ("fixed bottom", 4), 10, 3, 2, 5, ) self.ftest( "fixed top, bottom, min_width 2", ("fixed top", 3), ("fixed bottom", 4), 10, 2, 0, 8, ) self.ftest("fixed bottom", ("fixed bottom", 3), 5, 10, 2, 3) self.ftest("fixed bottom reduce", ("fixed bottom", 3), 8, 10, 0, 2) self.ftest("fixed bottom shrink", ("fixed bottom", 3), 18, 10, 0, 0) self.ftest( "fixed bottom, top", ("fixed bottom", 3), ("fixed top", 4), 17, 4, 3, ) self.ftest( "fixed bottom, top, min_height", ("fixed bottom", 3), ("fixed top", 4), 10, 2, 3, 5, ) self.ftest( "fixed bottom, top, min_height 2", ("fixed bottom", 3), ("fixed top", 4), 10, 0, 2, 8, ) self.ftest("relative 30", ("relative", 30), 5, 10, 1, 4) self.ftest("relative 50", ("relative", 50), 5, 10, 2, 3) self.ftest("relative 130 edge", ("relative", 130), 5, 10, 5, 0) self.ftest("relative -10 edge", ("relative", -10), 4, 10, 0, 6) self.ftest("middle relative 70", "middle", ("relative", 70), 10, 1, 2) self.ftest( "middle relative 70 grow 8", "middle", ("relative", 70), 10, 1, 1, 8, ) def test_repr(self): self.assertEqual( ">", repr(urwid.Filler(urwid.Text("hai"))), ) def test_sizing(self): with self.subTest("Flow supported for PACK height (flow widget)"): widget = urwid.Filler(urwid.Text("Some text")) self.assertEqual(frozenset((urwid.BOX, urwid.FLOW)), widget.sizing()) cols, rows = 10, 1 self.assertEqual((cols, rows), widget.pack((cols,))) # top and bottom are 0 self.assertEqual((cols, rows), widget.pack((cols, rows))) canvas = widget.render((cols,)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) canvas = widget.render((cols, rows)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) with self.subTest("Flow supported for GIVEN height (box widget)"): widget = urwid.Filler(urwid.SolidFill("#"), height=3) self.assertEqual(frozenset((urwid.BOX, urwid.FLOW)), widget.sizing()) cols, rows = 5, 3 self.assertEqual((cols, rows), widget.pack((cols,))) # top and bottom are 0 self.assertEqual((cols, rows), widget.pack((cols, rows))) canvas = widget.render((cols,)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) canvas = widget.render((cols, rows)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) with self.subTest("Flow is not supported for RELATIVE scenarios"): widget = urwid.Filler(urwid.SolidFill(""), height=(urwid.RELATIVE, 10)) cols = 10 with self.assertRaises(urwid.widget.WidgetError) as ctx: widget.pack((cols,)) self.assertEqual( f"Cannot pack (maxcol,) size, this is not a flow widget: {widget!r}", str(ctx.exception), ) with self.assertRaises(urwid.widget.WidgetError) as ctx: widget.render((cols,)) self.assertEqual( f"Cannot pack (maxcol,) size, this is not a flow widget: {widget!r}", str(ctx.exception), ) def test_sizing_pack_height_requires_flow_body(self): """A PACK height is only meaningful for a FLOW body: warn instead of failing obscurely later. With a BOX-only body and the default PACK height the filler declares BOX and FLOW support, yet every render raises ``AttributeError`` from the missing ``rows`` method, so the mismatch has to be announced while the sizing is calculated. """ widget = urwid.Filler(urwid.SolidFill("#")) with self.assertWarns(urwid.widget.FillerWarning) as ctx: sizing = widget.sizing() self.assertEqual( f"WHSettings.PACK height expects a FLOW widget to be used, but received {widget.original_widget!r}", str(ctx.warning), ) # The declared sizing is deliberately left untouched: containers branch on it while laying out. self.assertEqual(frozenset((urwid.BOX, urwid.FLOW)), sizing) def test_sizing_pack_height_flow_body_does_not_warn(self): """A FLOW body with a PACK height is the supported combination and must stay silent.""" widget = urwid.Filler(urwid.Text("Some text")) with warnings.catch_warnings(): warnings.simplefilter("error") self.assertEqual(frozenset((urwid.BOX, urwid.FLOW)), widget.sizing()) def test_render_focused_not_fit(self): """Test that a focused widget will be shown and top trimmed if not enough height.""" widget = urwid.Filler( urwid.Pile( ( urwid.Text("First"), urwid.Text("Second"), urwid.Text("Third"), urwid.Button("Selectable"), urwid.Text("Last"), ), ) ) canvas = widget.render((14, 3), focus=True) self.assertEqual( [ b"Second ", b"Third ", b"< Selectable >", ], canvas.text, ) def test_valign_top_around_text(self) -> None: """Body filler used by dialog windows.""" filler = urwid.Filler(urwid.Text("body"), valign=urwid.TOP) self.assertEqual((4, 3), filler.pack((4, 3))) self.assertEqual([b"body", b" ", b" "], filler.render((4, 3)).text) def test_render_trim_no_cursor(self) -> None: """When the body has no cursor, an oversized canvas is trimmed straight to maxrow.""" widget = urwid.Filler(TrimWidget(5)) canvas = widget.render((10, 3), focus=False) self.assertEqual(3, canvas.rows()) self.assertIsNone(canvas.cursor) def test_render_trim_cursor_within_maxrow(self) -> None: """A cursor above maxrow does not trigger the cursor-driven trim, but the oversized canvas is still cut.""" widget = urwid.Filler(TrimWidget(5, cursor=(0, 1))) canvas = widget.render((10, 3), focus=True) self.assertEqual(3, canvas.rows()) self.assertEqual((0, 1), canvas.cursor) def test_render_trim_cursor_beyond_maxrow(self) -> None: """A cursor beyond maxrow triggers the cursor-driven top trim, keeping the cursor row visible.""" widget = urwid.Filler(TrimWidget(5, cursor=(0, 4))) canvas = widget.render((10, 3), focus=True) self.assertEqual(3, canvas.rows()) self.assertEqual((0, 2), canvas.cursor) def test_keypress_pack_height(self) -> None: body = CursorWidget([urwid.FLOW], num_rows=1) widget = urwid.Filler(body) self.assertIsNone(widget.keypress((10,), "enter")) def test_keypress_given_height(self) -> None: body = CursorWidget([urwid.BOX]) widget = urwid.Filler(body, height=3) self.assertIsNone(widget.keypress((10, 5), "enter")) def test_get_cursor_coords_no_cursor_api(self) -> None: widget = urwid.Filler(PlainBoxWidget(), height=3) self.assertIsNone(widget.get_cursor_coords((10, 5))) def test_get_cursor_coords_falsy_coords(self) -> None: body = CursorWidget([urwid.BOX], cursor=None) widget = urwid.Filler(body, height=3) self.assertIsNone(widget.get_cursor_coords((10, 5))) def test_get_cursor_coords_pack_height(self) -> None: body = CursorWidget([urwid.FLOW], num_rows=1, cursor=(2, 0)) widget = urwid.Filler(body) self.assertEqual((2, 1), widget.get_cursor_coords((10, 3))) def test_get_cursor_coords_given_height(self) -> None: body = CursorWidget([urwid.BOX], cursor=(2, 1)) widget = urwid.Filler(body, height=3) self.assertEqual((2, 2), widget.get_cursor_coords((10, 5))) def test_get_cursor_coords_clamps_to_maxrow(self) -> None: """The body may report a cursor row beyond maxrow; Filler clamps it to the last visible row.""" body = CursorWidget([urwid.BOX], cursor=(0, 10)) widget = urwid.Filler(body, height=3) self.assertEqual((0, 5), widget.get_cursor_coords((10, 5))) def test_get_pref_col_no_cursor_api(self) -> None: widget = urwid.Filler(PlainBoxWidget(), height=3) self.assertIsNone(widget.get_pref_col((10, 5))) def test_get_pref_col_pack_height(self) -> None: body = CursorWidget([urwid.FLOW], num_rows=1, cursor=(2, 0)) widget = urwid.Filler(body) self.assertEqual(2, widget.get_pref_col((10, 3))) def test_get_pref_col_given_height(self) -> None: body = CursorWidget([urwid.BOX], cursor=(2, 0)) widget = urwid.Filler(body, height=3) self.assertEqual(2, widget.get_pref_col((10, 5))) def test_move_cursor_to_coords_no_cursor_api(self) -> None: widget = urwid.Filler(PlainBoxWidget(), height=3) self.assertTrue(widget.move_cursor_to_coords((10, 5), 0, 0)) def test_move_cursor_to_coords_out_of_range_row(self) -> None: body = CursorWidget([urwid.BOX]) widget = urwid.Filler(body, height=3) self.assertFalse(widget.move_cursor_to_coords((10, 5), 0, 0)) def test_move_cursor_to_coords_pack_height(self) -> None: body = CursorWidget([urwid.FLOW], num_rows=1) widget = urwid.Filler(body) self.assertTrue(widget.move_cursor_to_coords((10, 3), 2, 1)) def test_move_cursor_to_coords_given_height(self) -> None: body = CursorWidget([urwid.BOX], move_result=False) widget = urwid.Filler(body, height=3) self.assertFalse(widget.move_cursor_to_coords((10, 5), 2, 2)) def test_mouse_event_no_cursor_api(self) -> None: widget = urwid.Filler(PlainBoxWidget(), height=3) self.assertFalse(widget.mouse_event((10, 5), "mouse press", 1, 0, 0, False)) def test_mouse_event_out_of_range_row(self) -> None: body = CursorWidget([urwid.BOX]) widget = urwid.Filler(body, height=3) self.assertFalse(widget.mouse_event((10, 5), "mouse press", 1, 0, 0, True)) def test_mouse_event_pack_height(self) -> None: body = CursorWidget([urwid.FLOW], num_rows=1) widget = urwid.Filler(body) self.assertTrue(widget.mouse_event((10, 3), "mouse press", 1, 2, 1, True)) def test_mouse_event_given_height(self) -> None: body = CursorWidget([urwid.BOX], mouse_result=False) widget = urwid.Filler(body, height=3) self.assertFalse(widget.mouse_event((10, 5), "mouse press", 1, 2, 2, True)) def test_calculate_top_bottom_filler_relative_valign_requires_amount(self) -> None: with self.assertRaises(TypeError): urwid.widget.calculate_top_bottom_filler(10, urwid.RELATIVE, None, urwid.GIVEN, 5, None, 0, 0) urwid-urwid-69907e4/tests/widgets/test_floatedit.py000066400000000000000000000026551526073734300225030ustar00rootroot00000000000000import unittest from decimal import Decimal from itertools import product from urwid.numedit import FloatEdit class FloatEditNoPreservePrecicionTest(unittest.TestCase): def test(self): for v in ("1.01", "2.5", "300", "4.100", "5.001"): f = FloatEdit(preserveSignificance=False) f.set_edit_text(v) print(f.value(), Decimal(v)) self.assertEqual(Decimal(v), f.value()) def test_int(self): corner_value = "1" # just int f = FloatEdit(preserveSignificance=False) f.set_edit_text(corner_value) self.assertEqual(Decimal(corner_value), f.value()) def test_no_post_dot(self): corner_value = "1." # forced float 1.0 with trimmed end f = FloatEdit(preserveSignificance=False) f.set_edit_text(corner_value) self.assertEqual(Decimal(corner_value), f.value()) class FloatEditPreservePrecicionTest(unittest.TestCase): def test(self): for v, sig, sep in product(("1.010", "2.500", "300.000", "4.100", "5.001"), (0, 1, 2, 3), (".", ",")): precision_template = "0." + "0" * sig expected = Decimal(v).quantize(Decimal(precision_template)) f = FloatEdit(preserveSignificance=True, default=precision_template, decimalSeparator=sep) f.set_edit_text(v.replace(".", sep)) self.assertEqual(expected, f.value(), f"{f.value()} != {expected} (significance={sig!r})") urwid-urwid-69907e4/tests/widgets/test_frame.py000066400000000000000000000434231526073734300216200ustar00rootroot00000000000000from __future__ import annotations import unittest import urwid from tests.util import SelectableText from urwid.canvas import TextCanvas from urwid.widget import Widget from urwid.widget.constants import Sizing class NoMouseEventWidget: """A widget-like object with no ``mouse_event`` method.""" def __init__(self, rows: int = 1, selectable: bool = False) -> None: self._rows = rows self._selectable = selectable def selectable(self) -> bool: return self._selectable def rows(self, size, focus: bool = False) -> int: return self._rows def render(self, size, focus: bool = False): maxcol = size[0] return TextCanvas([b" " * maxcol] * self._rows, maxcol=maxcol) def keypress(self, size, key): return key class NoMouseEventBoxWidget: """A box widget-like object with no ``mouse_event`` method.""" def selectable(self) -> bool: return False def render(self, size, focus: bool = False): maxcol, maxrow = size return TextCanvas([b" " * maxcol] * maxrow, maxcol=maxcol) class LiarHeader(Widget): """Flow widget whose rendered canvas has fewer rows than it reports.""" _sizing = frozenset([Sizing.FLOW]) _selectable = False def rows(self, size, focus: bool = False) -> int: return 3 def render(self, size, focus: bool = False): maxcol = size[0] return TextCanvas([b"a" * maxcol, b"b" * maxcol], maxcol=maxcol) class LiarFooter(Widget): """Flow widget whose rendered canvas has fewer rows than it reports.""" _sizing = frozenset([Sizing.FLOW]) _selectable = False def rows(self, size, focus: bool = False) -> int: return 3 def render(self, size, focus: bool = False): maxcol = size[0] return TextCanvas([b"a" * maxcol, b"b" * maxcol], maxcol=maxcol) class FrameTest(unittest.TestCase): def ftbtest(self, desc: str, focus_part, header_rows, footer_rows, size, focus, top, bottom): class FakeWidget: def __init__(self, rows, want_focus): self.ret_rows = rows self.want_focus = want_focus def rows(self, size, focus=False): assert self.want_focus == focus return self.ret_rows with self.subTest(desc): header = footer = None if header_rows: header = FakeWidget(header_rows, focus and focus_part == "header") if footer_rows: footer = FakeWidget(footer_rows, focus and focus_part == "footer") f = urwid.Frame(urwid.SolidFill(), header, footer, focus_part) rval = f.frame_top_bottom(size, focus) exp = (top, bottom), (header_rows, footer_rows) self.assertEqual(exp, rval) def test(self): self.ftbtest("simple", "body", 0, 0, (9, 10), True, 0, 0) self.ftbtest("simple h", "body", 3, 0, (9, 10), True, 3, 0) self.ftbtest("simple f", "body", 0, 3, (9, 10), True, 0, 3) self.ftbtest("simple hf", "body", 3, 3, (9, 10), True, 3, 3) self.ftbtest("almost full hf", "body", 4, 5, (9, 10), True, 4, 5) self.ftbtest("full hf", "body", 5, 5, (9, 10), True, 4, 5) self.ftbtest("x full h+1f", "body", 6, 5, (9, 10), False, 4, 5) self.ftbtest("full h+1f", "body", 6, 5, (9, 10), True, 4, 5) self.ftbtest("full hf+1", "body", 5, 6, (9, 10), True, 3, 6) self.ftbtest("F full h+1f", "footer", 6, 5, (9, 10), True, 5, 5) self.ftbtest("F full hf+1", "footer", 5, 6, (9, 10), True, 4, 6) self.ftbtest("F full hf+5", "footer", 5, 11, (9, 10), True, 0, 10) self.ftbtest("full hf+5", "body", 5, 11, (9, 10), True, 0, 9) self.ftbtest("H full hf+1", "header", 5, 6, (9, 10), True, 5, 5) self.ftbtest("H full h+1f", "header", 6, 5, (9, 10), True, 6, 4) self.ftbtest("H full h+5f", "header", 11, 5, (9, 10), True, 10, 0) def test_common(self): s1 = urwid.SolidFill("1") f = urwid.Frame(s1) self.assertEqual(f.focus, s1) self.assertEqual(f.focus_position, "body") self.assertRaises(IndexError, lambda: setattr(f, "focus_position", None)) self.assertRaises(IndexError, lambda: setattr(f, "focus_position", "header")) t1 = urwid.Text("one") t2 = urwid.Text("two") t3 = urwid.Text("three") f = urwid.Frame(s1, t1, t2, "header") self.assertEqual(f.focus, t1) self.assertEqual(f.focus_position, "header") f.focus_position = "footer" self.assertEqual(f.focus, t2) self.assertEqual(f.focus_position, "footer") self.assertRaises(IndexError, lambda: setattr(f, "focus_position", -1)) self.assertRaises(IndexError, lambda: setattr(f, "focus_position", 2)) del f.contents["footer"] self.assertEqual(f.footer, None) self.assertEqual(f.focus_position, "body") f.contents.update(footer=(t3, None), header=(t2, None)) self.assertEqual(f.header, t2) self.assertEqual(f.footer, t3) def set1(): f.contents["body"] = t1 self.assertRaises(urwid.FrameError, set1) def set2(): f.contents["body"] = (t1, "given") self.assertRaises(urwid.FrameError, set2) def test_focus(self): header = urwid.Text("header") body = urwid.ListBox((urwid.Text("first"), urwid.Text("second"))) footer = urwid.Text("footer") with self.subTest("default"): widget = urwid.Frame(body, header, footer) self.assertEqual(body, widget.focus) self.assertEqual("body", widget.focus_part) with self.subTest("body"): widget = urwid.Frame(body, header, footer, focus_part=body) self.assertEqual(body, widget.focus) self.assertEqual("body", widget.focus_part) with self.subTest("header"): widget = urwid.Frame(body, header, footer, focus_part=header) self.assertEqual(header, widget.focus) self.assertEqual("header", widget.focus_part) with self.subTest("footer"): widget = urwid.Frame(body, header, footer, focus_part=footer) self.assertEqual(footer, widget.focus) self.assertEqual("footer", widget.focus_part) def test_init_invalid_focus_part(self): self.assertRaises(ValueError, urwid.Frame, urwid.SolidFill(), focus_part="not a part") def test_header_setter_resets_focus(self): header = urwid.Text("header") f = urwid.Frame(urwid.SolidFill(), header, focus_part="header") self.assertEqual("header", f.focus_position) f.header = None self.assertEqual("body", f.focus_position) def test_footer_setter_resets_focus(self): footer = urwid.Text("footer") f = urwid.Frame(urwid.SolidFill(), footer=footer, focus_part="footer") self.assertEqual("footer", f.focus_position) f.footer = None self.assertEqual("body", f.focus_position) def test_sizing(self): f = urwid.Frame(urwid.SolidFill()) self.assertEqual(frozenset([urwid.Sizing.BOX]), f.sizing()) class FrameContentsTest(unittest.TestCase): def test_getitem_missing_header_footer(self): f = urwid.Frame(urwid.SolidFill()) self.assertRaises(KeyError, lambda: f.contents["header"]) self.assertRaises(KeyError, lambda: f.contents["footer"]) self.assertEqual((f.body, None), f.contents["body"]) def test_getitem_existing_header_footer(self): header = urwid.Text("header") footer = urwid.Text("footer") f = urwid.Frame(urwid.SolidFill(), header, footer) self.assertEqual((header, None), f.contents["header"]) self.assertEqual((footer, None), f.contents["footer"]) def test_setitem_invalid_key(self): f = urwid.Frame(urwid.SolidFill()) def set_invalid(): f.contents["invalid"] = (urwid.Text("x"), None) self.assertRaises(KeyError, set_invalid) def test_setitem_header_footer(self): f = urwid.Frame(urwid.SolidFill()) header = urwid.Text("header") footer = urwid.Text("footer") f.contents["header"] = (header, None) f.contents["footer"] = (footer, None) self.assertEqual(header, f.header) self.assertEqual(footer, f.footer) def test_setitem_body(self): f = urwid.Frame(urwid.SolidFill("x")) new_body = urwid.SolidFill("y") f.contents["body"] = (new_body, None) self.assertEqual(new_body, f.body) def test_delitem_invalid_key(self): f = urwid.Frame(urwid.SolidFill()) self.assertRaises(KeyError, lambda: f.contents.__delitem__("body")) def test_delitem_missing_header(self): f = urwid.Frame(urwid.SolidFill()) self.assertRaises(KeyError, lambda: f.contents.__delitem__("header")) def test_delitem_header(self): header = urwid.Text("header") f = urwid.Frame(urwid.SolidFill(), header) del f.contents["header"] self.assertIsNone(f.header) def test_contents_keys(self): f = urwid.Frame(urwid.SolidFill()) self.assertEqual(["body"], f._contents_keys()) header = urwid.Text("header") footer = urwid.Text("footer") f = urwid.Frame(urwid.SolidFill(), header, footer) self.assertEqual(["body", "header", "footer"], f._contents_keys()) def test_options(self): f = urwid.Frame(urwid.SolidFill()) self.assertIsNone(f.options()) def test_contents_iter_len_dict(self): # regression test: FrameContents.__iter__/__len__ used to recurse # into themselves via the inherited MutableMapping.keys(). header = urwid.Text("header") f = urwid.Frame(urwid.SolidFill(), header) self.assertEqual(["body", "header"], list(f.contents)) self.assertEqual(["body", "header"], list(f.contents.keys())) self.assertEqual(2, len(f.contents)) self.assertEqual({"body": (f.body, None), "header": (header, None)}, dict(f.contents)) def test_iter_reversed(self): f = urwid.Frame(urwid.SolidFill()) self.assertEqual(["body"], list(f)) self.assertEqual(["body"], list(reversed(f))) header = urwid.Text("header") footer = urwid.Text("footer") f = urwid.Frame(urwid.SolidFill(), header, footer) self.assertEqual(["header", "body", "footer"], list(f)) self.assertEqual(["footer", "body", "header"], list(reversed(f))) class FrameRenderTest(unittest.TestCase): def test_render_no_header_footer(self): f = urwid.Frame(urwid.SolidFill("x")) canvas = f.render((5, 3)) self.assertEqual([b"xxxxx"] * 3, canvas.text) def test_render_with_header_and_footer(self): header = urwid.Text("head") footer = urwid.Text("foot") f = urwid.Frame(urwid.SolidFill("x"), header, footer) canvas = f.render((5, 4)) self.assertEqual([b"head ", b"xxxxx", b"xxxxx", b"foot "], canvas.text) def test_render_header_trimmed(self): header = urwid.Text("one\ntwo\nthree") f = urwid.Frame(urwid.SolidFill("x"), header) canvas = f.render((5, 2)) self.assertEqual([b"one ", b"xxxxx"], canvas.text) def test_render_footer_trimmed(self): footer = urwid.Text("a\nb\nc") f = urwid.Frame(urwid.SolidFill("x"), None, footer) canvas = f.render((5, 2)) self.assertEqual([b"xxxxx", b"a "], canvas.text) def test_render_zero_rows(self): f = urwid.Frame(urwid.SolidFill("x")) canvas = f.render((5, 0)) self.assertEqual([], canvas.text) def test_render_header_row_mismatch_raises(self): f = urwid.Frame(urwid.SolidFill("x"), LiarHeader()) self.assertRaises(RuntimeError, f.render, (5, 5)) def test_render_footer_row_mismatch_raises(self): f = urwid.Frame(urwid.SolidFill("x"), None, LiarFooter()) self.assertRaises(RuntimeError, f.render, (5, 5)) class FrameKeypressTest(unittest.TestCase): def test_keypress_to_body(self): edit = urwid.Edit("", "") f = urwid.Frame(urwid.Filler(edit)) f.render((10, 3), focus=True) self.assertIsNone(f.keypress((10, 3), "x")) self.assertEqual("x", edit.edit_text) def test_keypress_to_header_selectable(self): header = urwid.Edit("", "") f = urwid.Frame(urwid.SolidFill(), header, focus_part="header") f.render((10, 3), focus=True) self.assertIsNone(f.keypress((10, 3), "x")) self.assertEqual("x", header.edit_text) def test_keypress_to_header_not_selectable(self): header = urwid.Text("header") f = urwid.Frame(urwid.SolidFill(), header, focus_part="header") self.assertEqual("x", f.keypress((10, 3), "x")) def test_keypress_to_footer_selectable(self): footer = urwid.Edit("", "") f = urwid.Frame(urwid.SolidFill(), None, footer, focus_part="footer") f.render((10, 3), focus=True) self.assertIsNone(f.keypress((10, 3), "x")) self.assertEqual("x", footer.edit_text) def test_keypress_to_footer_not_selectable(self): footer = urwid.Text("footer") f = urwid.Frame(urwid.SolidFill(), None, footer, focus_part="footer") self.assertEqual("x", f.keypress((10, 3), "x")) def test_keypress_focus_part_widget_missing(self): f = urwid.Frame(urwid.SolidFill(), focus_part="header") self.assertEqual("x", f.keypress((10, 3), "x")) def test_keypress_body_not_selectable(self): f = urwid.Frame(urwid.SolidFill()) self.assertEqual("x", f.keypress((10, 3), "x")) def test_keypress_body_no_remaining_rows(self): edit = urwid.Filler(urwid.Edit("", "")) header = urwid.Text("one\ntwo\nthree") footer = urwid.Text("a\nb\nc") f = urwid.Frame(edit, header, footer) self.assertEqual("x", f.keypress((10, 4), "x")) class FrameMouseEventTest(unittest.TestCase): def test_mouse_event_header_changes_focus(self): header = urwid.Edit("", "") f = urwid.Frame(urwid.SolidFill(), header) self.assertEqual("body", f.focus_position) f.render((10, 5), focus=True) result = f.mouse_event((10, 5), "mouse press", 1, 0, 0, True) self.assertEqual("header", f.focus_position) self.assertIsNotNone(result) def test_mouse_event_header_not_selectable_no_focus_change(self): header = urwid.Text("header") f = urwid.Frame(urwid.SolidFill(), header) f.render((10, 5), focus=True) f.mouse_event((10, 5), "mouse press", 1, 0, 0, True) self.assertEqual("body", f.focus_position) def test_mouse_event_header_no_mouse_event_attr(self): header = NoMouseEventWidget(rows=1) f = urwid.Frame(urwid.SolidFill(), header) f.render((10, 5), focus=True) result = f.mouse_event((10, 5), "mouse press", 1, 0, 0, True) self.assertFalse(result) def test_mouse_event_footer_changes_focus(self): footer = urwid.Edit("", "") f = urwid.Frame(urwid.SolidFill(), None, footer) f.render((10, 5), focus=True) result = f.mouse_event((10, 5), "mouse press", 1, 0, 4, True) self.assertEqual("footer", f.focus_position) self.assertIsNotNone(result) def test_mouse_event_footer_no_mouse_event_attr(self): footer = NoMouseEventWidget(rows=1) f = urwid.Frame(urwid.SolidFill(), None, footer) f.render((10, 5), focus=True) result = f.mouse_event((10, 5), "mouse press", 1, 0, 4, True) self.assertFalse(result) def test_mouse_event_body_changes_focus(self): edit = urwid.Filler(urwid.Edit("", "")) header = urwid.Text("header") f = urwid.Frame(edit, header, focus_part="header") f.render((10, 5), focus=True) self.assertEqual("header", f.focus_position) result = f.mouse_event((10, 5), "mouse press", 1, 0, 3, True) self.assertEqual("body", f.focus_position) self.assertIsNotNone(result) def test_mouse_event_body_no_mouse_event_attr(self): f = urwid.Frame(NoMouseEventBoxWidget()) f.render((10, 5), focus=True) result = f.mouse_event((10, 5), "mouse press", 1, 0, 0, True) self.assertFalse(result) class FrameCursorCoordsTest(unittest.TestCase): def test_get_cursor_coords_not_selectable(self): f = urwid.Frame(urwid.SolidFill()) self.assertIsNone(f.get_cursor_coords((10, 5))) def test_get_cursor_coords_no_support(self): header = urwid.Text("header") f = urwid.Frame(urwid.SolidFill(), header, focus_part="header") self.assertIsNone(f.get_cursor_coords((10, 5))) def test_get_cursor_coords_body(self): edit = urwid.Filler(urwid.Edit("", "hi"), valign="top") f = urwid.Frame(edit) self.assertEqual((2, 0), f.get_cursor_coords((10, 5))) def test_get_cursor_coords_header(self): header = urwid.Edit("", "hi") f = urwid.Frame(urwid.SolidFill(), header, focus_part="header") self.assertEqual((2, 0), f.get_cursor_coords((10, 5))) def test_get_cursor_coords_footer(self): footer = urwid.Edit("", "hi") f = urwid.Frame(urwid.SolidFill(), None, footer, focus_part="footer") self.assertEqual((2, 4), f.get_cursor_coords((10, 5))) def test_get_cursor_coords_selectable_no_get_cursor_coords_method(self): header = SelectableText("header") f = urwid.Frame(urwid.SolidFill(), header, focus_part="header") self.assertIsNone(f.get_cursor_coords((10, 5))) def test_get_cursor_coords_coords_none(self): header = urwid.Edit("", "") f = urwid.Frame(urwid.SolidFill(), header, focus_part="header") header.set_edit_text("") original = header.get_cursor_coords header.get_cursor_coords = lambda size: None try: self.assertIsNone(f.get_cursor_coords((10, 5))) finally: header.get_cursor_coords = original urwid-urwid-69907e4/tests/widgets/test_graphics.py000066400000000000000000000106331526073734300223230ustar00rootroot00000000000000from __future__ import annotations import unittest import urwid from urwid.util import get_encoding from urwid.widget import bar_graph class BarGraphTest(unittest.TestCase): def bgtest(self, desc, data, top, widths, maxrow, exp): rval = bar_graph.calculate_bargraph_display(data, top, widths, maxrow) assert rval == exp, f"{desc} expected {exp!r}, got {rval!r}" def test1(self): self.bgtest("simplest", [[0]], 5, [1], 1, [(1, [(0, 1)])]) self.bgtest( "simpler", [[0], [0]], 5, [1, 2], 5, [(5, [(0, 3)])], ) self.bgtest( "simple", [[5]], 5, [1], 1, [(1, [(1, 1)])], ) self.bgtest( "2col-1", [[2], [0]], 5, [1, 2], 5, [(3, [(0, 3)]), (2, [(1, 1), (0, 2)])], ) self.bgtest( "2col-2", [[0], [2]], 5, [1, 2], 5, [(3, [(0, 3)]), (2, [(0, 1), (1, 2)])], ) self.bgtest( "2col-3", [[2], [3]], 5, [1, 2], 5, [(2, [(0, 3)]), (1, [(0, 1), (1, 2)]), (2, [(1, 3)])], ) self.bgtest( "3col-1", [[5], [3], [0]], 5, [2, 1, 1], 5, [(2, [(1, 2), (0, 2)]), (3, [(1, 3), (0, 1)])], ) self.bgtest( "3col-2", [[4], [4], [4]], 5, [2, 1, 1], 5, [(1, [(0, 4)]), (4, [(1, 4)])], ) self.bgtest( "3col-3", [[1], [2], [3]], 5, [2, 1, 1], 5, [(2, [(0, 4)]), (1, [(0, 3), (1, 1)]), (1, [(0, 2), (1, 2)]), (1, [(1, 4)])], ) self.bgtest( "3col-4", [[4], [2], [4]], 5, [1, 2, 1], 5, [(1, [(0, 4)]), (2, [(1, 1), (0, 2), (1, 1)]), (2, [(1, 4)])], ) def test2(self): self.bgtest( "simple1a", [[2, 0], [2, 1]], 2, [1, 1], 2, [(1, [(1, 2)]), (1, [(1, 1), (2, 1)])], ) self.bgtest( "simple1b", [[2, 1], [2, 0]], 2, [1, 1], 2, [(1, [(1, 2)]), (1, [(2, 1), (1, 1)])], ) self.bgtest("cross1a", [[2, 2], [1, 2]], 2, [1, 1], 2, [(2, [(2, 2)])]) self.bgtest("cross1b", [[1, 2], [2, 2]], 2, [1, 1], 2, [(2, [(2, 2)])]) self.bgtest( "mix1a", [[3, 2, 1], [2, 2, 2], [1, 2, 3]], 3, [1, 1, 1], 3, [(1, [(1, 1), (0, 1), (3, 1)]), (1, [(2, 1), (3, 2)]), (1, [(3, 3)])], ) self.bgtest( "mix1b", [[1, 2, 3], [2, 2, 2], [3, 2, 1]], 3, [1, 1, 1], 3, [(1, [(3, 1), (0, 1), (1, 1)]), (1, [(3, 2), (2, 1)]), (1, [(3, 3)])], ) class SmoothBarGraphTest(unittest.TestCase): def setUp(self) -> None: self.old_encoding = get_encoding() urwid.set_encoding("utf-8") def tearDown(self) -> None: urwid.set_encoding(self.old_encoding) def sbgtest(self, desc, data, top, exp): g = urwid.BarGraph(["black", "red", "blue"], None, {(1, 0): "red/black", (2, 1): "blue/red"}) g.set_data(data, top) rval = g.calculate_display((5, 3)) assert rval == exp, f"{desc} expected {exp!r}, got {rval!r}" def test1(self): self.sbgtest( "simple", [[3]], 5, [(1, [(0, 5)]), (1, [((1, 0, 6), 5)]), (1, [(1, 5)])], ) self.sbgtest( "boring", [[4, 2]], 6, [(1, [(0, 5)]), (1, [(1, 5)]), (1, [(2, 5)])], ) self.sbgtest( "two", [[4], [2]], 6, [(1, [(0, 5)]), (1, [(1, 3), (0, 2)]), (1, [(1, 5)])], ) self.sbgtest( "twos", [[3], [4]], 6, [(1, [(0, 5)]), (1, [((1, 0, 4), 3), (1, 2)]), (1, [(1, 5)])], ) self.sbgtest( "twof", [[4], [3]], 6, [(1, [(0, 5)]), (1, [(1, 3), ((1, 0, 4), 2)]), (1, [(1, 5)])], ) urwid-urwid-69907e4/tests/widgets/test_grid_flow.py000066400000000000000000000400771526073734300225040ustar00rootroot00000000000000from __future__ import annotations import unittest from unittest import mock import urwid class GridFlowTest(unittest.TestCase): def test_fixed(self): grid = urwid.GridFlow( (urwid.Button(tag, align=urwid.CENTER) for tag in ("OK", "Cancel", "Help")), cell_width=10, h_sep=1, v_sep=1, align=urwid.CENTER, ) self.assertEqual(frozenset((urwid.FIXED, urwid.FLOW)), grid.sizing()) cols, rows = 32, 1 self.assertEqual((cols, rows), grid.pack(())) canvas = grid.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual([b"< OK > < Cancel > < Help >"], canvas.text) def test_default_focus(self): with self.subTest("Simple"): grid = urwid.GridFlow( (urwid.Button("btn"), urwid.Button("btn")), cell_width=10, h_sep=1, v_sep=1, align=urwid.CENTER, ) self.assertTrue(grid.selectable()) self.assertEqual(0, grid.focus_position) with self.subTest("Simple not selectable"): grid = urwid.GridFlow( (urwid.Text("btn"), urwid.Text("btn")), cell_width=10, h_sep=1, v_sep=1, align=urwid.CENTER, ) self.assertFalse(grid.selectable()) self.assertEqual(0, grid.focus_position) with self.subTest("Explicit index"): grid = urwid.GridFlow( (urwid.Button("btn"), urwid.Button("btn")), cell_width=10, h_sep=1, v_sep=1, align=urwid.CENTER, focus=1, ) self.assertEqual(1, grid.focus_position) with self.subTest("Explicit widget"): btn2 = urwid.Button("btn 2") grid = urwid.GridFlow( (urwid.Button("btn"), btn2), cell_width=10, h_sep=1, v_sep=1, align=urwid.CENTER, focus=btn2, ) self.assertEqual(1, grid.focus_position) with self.subTest("Selectable not first"): grid = urwid.GridFlow( (urwid.Text("text"), urwid.Button("btn")), cell_width=10, h_sep=1, v_sep=1, align=urwid.CENTER, ) self.assertEqual(1, grid.focus_position) def test_cell_width(self): gf = urwid.GridFlow([], 5, 0, 0, "left") self.assertEqual(gf.cell_width, 5) def test_not_fit(self): """Test scenario with maxcol < cell_width (special case, warning will be produced).""" widget = urwid.GridFlow( [urwid.Button("first"), urwid.Button("second")], 5, 0, 0, "left", ) size = (3,) with self.assertWarns(urwid.widget.GridFlowWarning): canvas = widget.render(size) self.assertEqual( ("< f", " i", " r", " s", " t", "< s", " e", " c", " o", " n", " d"), canvas.decoded_text, ) def test_multiline(self): """Test widgets fit only with multiple lines""" widget = urwid.GridFlow( [urwid.Button("first"), urwid.Button("second")], 10, 0, 0, "left", ) size = (10,) canvas = widget.render(size) self.assertEqual( ("< first >", "< second >"), canvas.decoded_text, ) def test_multiline_2(self): """Test widgets fit only with multiple lines""" widget = urwid.GridFlow( [urwid.Button("first"), urwid.Button("second"), urwid.Button("third"), urwid.Button("forth")], 10, 0, 0, "left", ) size = (20,) canvas = widget.render(size) self.assertEqual( ( "< first >< second >", "< third >< forth >", ), canvas.decoded_text, ) def test_basics(self): repr(urwid.GridFlow([], 5, 0, 0, "left")) # should not fail def test_v_sep(self): gf = urwid.GridFlow([urwid.Text("test")], 10, 3, 1, "center") self.assertEqual(gf.rows((40,), False), 1) def test_keypress_v_sep_0(self): """ Ensure proper keypress handling when v_sep is 0. https://github.com/urwid/urwid/issues/387 """ call_back = mock.Mock() button = urwid.Button("test", call_back) gf = urwid.GridFlow([button], 10, 3, v_sep=0, align="center") self.assertEqual(gf.keypress((20,), "enter"), None) call_back.assert_called_with(button) def test_keypress_callback_can_set_focus_position(self): """ A callback fired by the keypress may set the focus itself. https://github.com/urwid/urwid/issues/194 """ gf = urwid.GridFlow( [urwid.Button(f"b{idx}", lambda _btn: setattr(gf, "focus_position", 1)) for idx in range(4)], 10, 1, 0, "left", ) gf.focus_position = 2 self.assertEqual(gf.keypress((80,), "enter"), None) self.assertEqual(1, gf.focus_position) def test_keypress_without_callback_still_follows_the_display_widget(self): gf = urwid.GridFlow([urwid.Button(f"b{idx}") for idx in range(4)], 10, 1, 0, "left") gf.focus_position = 0 self.assertEqual(gf.keypress((80,), "right"), None) self.assertEqual(1, gf.focus_position) def test_mouse_event_callback_can_set_focus_position(self): """ Same as the keypress case, for a callback fired by a click. """ gf = urwid.GridFlow( [urwid.Button(f"b{idx}", lambda _btn: setattr(gf, "focus_position", 1)) for idx in range(4)], 10, 1, 0, "left", ) gf.focus_position = 0 gf.render((80,)) # column 35 is inside the fourth cell self.assertTrue(gf.mouse_event((80,), "mouse press", 1, 35, 0, True)) self.assertEqual(1, gf.focus_position) def test_mouse_event_without_callback_still_follows_the_display_widget(self): gf = urwid.GridFlow([urwid.Button(f"b{idx}") for idx in range(4)], 10, 1, 0, "left") gf.focus_position = 0 gf.render((80,)) self.assertTrue(gf.mouse_event((80,), "mouse press", 1, 35, 0, True)) self.assertEqual(3, gf.focus_position) def test_mouse_event_callback_can_shrink_contents(self): """A click whose callback replaces the contents with fewer cells keeps the new focus.""" new_cells = [urwid.Button(f"n{idx}") for idx in range(3)] gf = urwid.GridFlow( [urwid.Button(f"b{idx}", lambda _btn: gf.contents.__setitem__(slice(None), new_items)) for idx in range(6)], 10, 1, 0, "left", ) new_items = [(cell, gf.options()) for cell in new_cells] gf.focus_position = 0 gf.render((80,)) # column 60 is inside the sixth cell self.assertTrue(gf.mouse_event((80,), "mouse press", 1, 60, 0, True)) self.assertEqual(new_items, list(gf.contents)) self.assertEqual(0, gf.focus_position) def test_mouse_event_callback_can_replace_contents_same_length(self): """A click whose callback replaces the contents does not move the focus to the clicked index.""" new_cells = [urwid.Button(f"n{idx}") for idx in range(6)] gf = urwid.GridFlow( [urwid.Button(f"b{idx}", lambda _btn: gf.contents.__setitem__(slice(None), new_items)) for idx in range(6)], 10, 1, 0, "left", ) new_items = [(cell, gf.options()) for cell in new_cells] gf.focus_position = 0 gf.render((80,)) self.assertTrue(gf.mouse_event((80,), "mouse press", 1, 60, 0, True)) self.assertEqual(new_items, list(gf.contents)) self.assertEqual(0, gf.focus_position) def _replacing_grid(self, new_count: int) -> tuple[urwid.GridFlow, list[tuple[urwid.Widget, tuple]]]: """Build a GridFlow whose first cell replaces the contents and leaves the key unhandled.""" class Replacer(urwid.SelectableIcon): """Selectable cell that replaces the GridFlow contents on any key.""" def keypress(self, size: tuple[int], key: str) -> str: gf.contents[:] = new_items return key gf = urwid.GridFlow([Replacer("r"), *(urwid.Button(f"b{idx}") for idx in range(5))], 10, 1, 0, "left") new_items = [(urwid.Button(f"n{idx}"), gf.options()) for idx in range(new_count)] gf.focus_position = 0 return gf, new_items def test_keypress_handler_can_shrink_contents(self): """A key whose handler replaces the contents with fewer cells keeps the new focus.""" gf, new_items = self._replacing_grid(1) self.assertEqual(gf.keypress((80,), "right"), None) self.assertEqual(new_items, list(gf.contents)) self.assertEqual(0, gf.focus_position) def test_keypress_handler_can_replace_contents_same_length(self): """A key whose handler replaces the contents does not move the focus by the stale display widget.""" gf, new_items = self._replacing_grid(6) self.assertEqual(gf.keypress((80,), "right"), None) self.assertEqual(new_items, list(gf.contents)) self.assertEqual(0, gf.focus_position) def test_length(self): grid = urwid.GridFlow((urwid.Text(c) for c in "ABC"), 1, 0, 0, "left") self.assertEqual(3, len(grid)) self.assertEqual(3, len(grid.contents)) def test_common(self): gf = urwid.GridFlow([], 5, 1, 0, "left") with self.subTest("Focus"): self.assertEqual(gf.focus, None) self.assertEqual(gf.contents, []) self.assertRaises(IndexError, lambda: getattr(gf, "focus_position")) self.assertRaises(IndexError, lambda: setattr(gf, "focus_position", None)) self.assertRaises(IndexError, lambda: setattr(gf, "focus_position", 0)) with self.subTest("Contents options"): self.assertEqual(gf.options(), ("given", 5)) self.assertEqual(gf.options(width_amount=9), ("given", 9)) self.assertRaises(urwid.GridFlowError, lambda: gf.options("pack", None)) def test_focus_position(self): t1 = urwid.Text("one") t2 = urwid.Text("two") gf = urwid.GridFlow([t1, t2], 5, 1, 0, "left") self.assertEqual(gf.focus, t1) self.assertEqual(gf.focus_position, 0) self.assertEqual(gf.contents, [(t1, ("given", 5)), (t2, ("given", 5))]) gf.focus_position = 1 self.assertEqual(gf.focus, t2) self.assertEqual(gf.focus_position, 1) gf.contents.insert(0, (t2, ("given", 5))) self.assertEqual(gf.focus_position, 2) self.assertRaises(urwid.GridFlowError, lambda: gf.contents.append(())) self.assertRaises(urwid.GridFlowError, lambda: gf.contents.insert(1, (t1, ("pack", None)))) gf.focus_position = 0 self.assertRaises(IndexError, lambda: setattr(gf, "focus_position", -1)) self.assertRaises(IndexError, lambda: setattr(gf, "focus_position", 3)) def test_deprecated(self): t1 = urwid.Text("one") t2 = urwid.Text("two") gf = urwid.GridFlow([t1, t2], 5, 1, 0, "left") # old methods: gf.set_focus(0) self.assertRaises(IndexError, lambda: gf.set_focus(-1)) self.assertRaises(IndexError, lambda: gf.set_focus(3)) gf.set_focus(t1) self.assertEqual(gf.focus_position, 0) self.assertRaises(ValueError, lambda: gf.set_focus("nonexistant")) def test_empty(self): """Test behaviour of empty widget.""" grid = urwid.GridFlow( (), cell_width=10, h_sep=1, v_sep=1, align=urwid.CENTER, ) self.assertEqual(frozenset((urwid.FLOW,)), grid.sizing(), "Empty grid can not be handled as FIXED") rows = 1 with self.subTest("Flow"): maxcol = 1 self.assertEqual((maxcol, rows), grid.pack((maxcol,))) rendered = grid.render((maxcol,)) self.assertEqual(maxcol, rendered.cols()) self.assertEqual(rows, rendered.rows()) with self.subTest("Fixed"): maxcol = 0 self.assertEqual((maxcol, rows), grid.pack(())) with self.assertRaises(ValueError): grid.render(()) def test_cell_width_setter(self): t1 = urwid.Text("one") t2 = urwid.Text("two") gf = urwid.GridFlow([t1, t2], 5, 1, 0, "left") gf.focus_position = 1 gf.cell_width = 8 self.assertEqual(gf.cell_width, 8) self.assertEqual(gf.contents, [(t1, ("given", 8)), (t2, ("given", 8))]) self.assertEqual(gf.focus_position, 1) def test_contents_setter(self): t1 = urwid.Text("one") t2 = urwid.Text("two") gf = urwid.GridFlow([t1, t2], 5, 1, 0, "left") gf.contents = [(t2, gf.options())] self.assertEqual(gf.contents, [(t2, ("given", 5))]) self.assertEqual(len(gf), 1) def test_v_sep_gt_1(self): """Divider height grows with v_sep when wrapping to multiple rows.""" widget = urwid.GridFlow([urwid.Text("first"), urwid.Text("second")], 10, 0, 2, "left") canvas = widget.render((10,)) self.assertEqual(4, canvas.rows()) self.assertEqual( ("first ", " ", " ", "second "), canvas.decoded_text, ) def test_keypress_unhandled_key_passed_through(self): gf = urwid.GridFlow([urwid.Text("a"), urwid.Text("b")], 10, 0, 0, "left") self.assertEqual("x", gf.keypress((10,), "x")) def test_get_cursor_coords_and_pref_col(self): e1 = urwid.Edit("", "hello") e2 = urwid.Edit("", "world") gf = urwid.GridFlow([e1, e2], 10, 1, 0, "left") gf.focus_position = 0 e1.set_edit_pos(3) self.assertEqual((3, 0), gf.get_cursor_coords((30,))) self.assertEqual(3, gf.get_pref_col((30,))) def test_move_cursor_to_coords(self): e1 = urwid.Edit("", "hello") e2 = urwid.Edit("", "world") gf = urwid.GridFlow([e1, e2], 10, 1, 0, "left") gf.focus_position = 0 self.assertTrue(gf.move_cursor_to_coords((30,), 15, 0)) self.assertEqual(1, gf.focus_position) def test_set_focus_from_display_widget_no_pile_focus(self): """When the Pile display widget has no focus, the helper is a no-op.""" gf = urwid.GridFlow([urwid.Button("a")], 10, 0, 0, "left") gf._w = mock.Mock(focus=None) gf._set_focus_from_display_widget() self.assertEqual(0, gf.focus_position) def test_set_focus_from_display_widget_column_no_focus(self): """When the focused column's widget itself is falsy, column focus position defaults to 0.""" inner = urwid.GridFlow([], 5, 0, 0, "left") # empty GridFlow is falsy (len 0) gf = urwid.GridFlow([inner], 10, 0, 0, "left") gf.render((10,)) gf._set_focus_from_display_widget() self.assertEqual(0, gf.focus_position) def test_focus_changed_callback_replacement(self) -> None: """Replace the contents focus callback and read the new index before it is applied.""" first = urwid.Button("first") second = urwid.Button("second") grid = urwid.GridFlow((first, second), cell_width=10, h_sep=1, v_sep=0, align=urwid.LEFT) seen: list[tuple[int, int, urwid.Widget]] = [] def on_focus_change(new_focus: int) -> None: seen.append((new_focus, grid.focus_position, grid.contents[new_focus][0])) grid._invalidate() grid.contents.set_focus_changed_callback(on_focus_change) grid.focus_position = 1 self.assertEqual([(1, 0, second)], seen) self.assertIs(second, grid.focus) self.assertEqual(1, grid.focus_position) urwid-urwid-69907e4/tests/widgets/test_line_box.py000066400000000000000000000205671526073734300223310ustar00rootroot00000000000000from __future__ import annotations import unittest import urwid from urwid.util import get_encoding class LineBoxTest(unittest.TestCase): def setUp(self) -> None: self.old_encoding = get_encoding() urwid.set_encoding("utf-8") def tearDown(self) -> None: urwid.set_encoding(self.old_encoding) def test_linebox_pack(self): # Bug #346 'pack' Padding does not run with LineBox t = urwid.Text("AAA\nCCC\nDDD") size = t.pack() l = urwid.LineBox(t) self.assertEqual(l.pack()[0], size[0] + 2) self.assertEqual(l.pack()[1], size[1] + 2) def test_border(self): wrapped = urwid.Text("Text\non\nfour\nlines", align=urwid.CENTER) l = urwid.LineBox(wrapped, tlcorner="╭", trcorner="╮", blcorner="╰", brcorner="╯") cols, rows = 7, 6 self.assertEqual((cols, rows), l.pack(())) canvas = l.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [ "╭─────╮", "│ Text│", "│ on │", "│ four│", "│lines│", "╰─────╯", ], [line.decode("utf-8") for line in canvas.text], ) def test_header(self): wrapped = urwid.Text("Some text") l = urwid.LineBox(wrapped, title="Title", tlcorner="╭", trcorner="╮", blcorner="╰", brcorner="╯") cols, rows = 11, 3 self.assertEqual((cols, rows), l.pack(())) canvas = l.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [ "╭─ Title ─╮", "│Some text│", "╰─────────╯", ], [line.decode("utf-8") for line in canvas.text], ) l.set_title("New") self.assertEqual( [ "╭── New ──╮", "│Some text│", "╰─────────╯", ], [line.decode("utf-8") for line in l.render(()).text], ) def test_negative(self): wrapped = urwid.Text("") with self.assertRaises(ValueError) as ctx: l = urwid.LineBox(wrapped, title="Title", tline="") self.assertEqual("Cannot have a title when tline is empty string", str(ctx.exception)) l = urwid.LineBox(wrapped, tline="") with self.assertRaises(ValueError) as ctx: l.set_title("Fail") self.assertEqual("Cannot set title when tline is unset", str(ctx.exception)) def test_partial_contour(self): def mark_pressed(btn: urwid.Button) -> None: nonlocal pressed pressed = True pressed = False wrapped = urwid.Button("Wrapped", on_press=mark_pressed) with self.subTest("No top line -> no top"): l = urwid.LineBox(wrapped, tline="") cols, rows = 13, 2 self.assertEqual((cols, rows), l.pack(())) canvas = l.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [ "│< Wrapped >│", "└───────────┘", ], [line.decode("utf-8") for line in canvas.text], ) self.assertIsNone(l.keypress((), "enter")) self.assertTrue(pressed) pressed = False with self.subTest("No right side elements -> no side"): l = urwid.LineBox(wrapped, rline="") cols, rows = 12, 3 self.assertEqual((cols, rows), l.pack(())) canvas = l.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [ "┌───────────", "│< Wrapped >", "└───────────", ], [line.decode("utf-8") for line in canvas.text], ) self.assertIsNone(l.keypress((), "enter")) self.assertTrue(pressed) pressed = False with self.subTest("No left side elements -> no side"): l = urwid.LineBox(wrapped, lline="") cols, rows = 12, 3 self.assertEqual((cols, rows), l.pack(())) canvas = l.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [ "───────────�", "< Wrapped >│", "───────────┘", ], [line.decode("utf-8") for line in canvas.text], ) self.assertIsNone(l.keypress((), "enter")) self.assertTrue(pressed) pressed = False with self.subTest("No bottom line -> no bottom"): l = urwid.LineBox(wrapped, bline="") cols, rows = 13, 2 self.assertEqual((cols, rows), l.pack(())) canvas = l.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [ "┌───────────�", "│< Wrapped >│", ], [line.decode("utf-8") for line in canvas.text], ) self.assertIsNone(l.keypress((), "enter")) self.assertTrue(pressed) pressed = False def test_columns_of_lineboxes(self): # BUG #748 # Using PACK: width of widget 1 is 4, width of widget 2 is 5. # With equal weight widget 1 will be rendered also 5. columns = urwid.Columns( [ (urwid.PACK, urwid.LineBox(urwid.Text("lol"), rline="", trcorner="", brcorner="")), (urwid.PACK, urwid.LineBox(urwid.Text("wtf"), tlcorner="┬", blcorner="┴")), ] ) canvas = columns.render(()) self.assertEqual( [ "┌───┬───�", "│lol│wtf│", "└───┴───┘", ], [line.decode("utf-8") for line in canvas.text], ) def test_replace_widget(self): old_widget = urwid.Text("some text") lb_widget = urwid.LineBox(old_widget) canvas = lb_widget.render(()) self.assertEqual( [ "┌─────────�", "│some text│", "└─────────┘", ], [line.decode("utf-8") for line in canvas.text], ) self.assertIs(old_widget, lb_widget.original_widget) new_widget = urwid.widget.Button("now it's a button") lb_widget.original_widget = new_widget canvas = lb_widget.render(()) self.assertEqual( [ "┌─────────────────────�", "│< now it's a button >│", "└─────────────────────┘", ], [line.decode("utf-8") for line in canvas.text], ) self.assertIs(new_widget, lb_widget.original_widget) def test_title_attr_and_unicode_corners(self) -> None: box = urwid.LineBox( urwid.Text("body"), title="Title", title_align=urwid.CENTER, title_attr="title", tlcorner="┌", tline="─", lline="│", trcorner="�", blcorner="└", rline="│", bline="─", brcorner="┘", ) self.assertEqual((6, 3), box.pack(())) self.assertEqual( ["┌────�", "│body│", "└────┘"], [line.decode("utf-8") for line in box.render(()).text], ) self.assertEqual( ["┌─ Title ─�", "│body │", "└─────────┘"], [line.decode("utf-8") for line in box.render((11,)).text], ) urwid-urwid-69907e4/tests/widgets/test_list_walker.py000066400000000000000000000314141526073734300230430ustar00rootroot00000000000000from __future__ import annotations import operator import unittest import urwid class ListWalkerBaseTest(unittest.TestCase): """Exercise the default get_focus/get_next/get_prev implementations on the base class.""" class Container(urwid.ListWalker): def __init__(self, items: list[str], focus: int = 0) -> None: self.items = items self.focus = focus def __getitem__(self, position: int) -> str: return self.items[position] def next_position(self, position: int) -> int: if position + 1 >= len(self.items): raise IndexError return position + 1 def prev_position(self, position: int) -> int: if position <= 0: raise IndexError return position - 1 def test_get_focus(self) -> None: walker = self.Container(["a", "b", "c"], focus=1) self.assertEqual(("b", 1), walker.get_focus()) def test_get_focus_invalid_position(self) -> None: walker = self.Container(["a", "b", "c"], focus=99) self.assertEqual((None, None), walker.get_focus()) def test_get_next(self) -> None: walker = self.Container(["a", "b", "c"]) self.assertEqual(("b", 1), walker.get_next(0)) def test_get_next_out_of_range(self) -> None: walker = self.Container(["a", "b", "c"]) self.assertEqual((None, None), walker.get_next(2)) def test_get_prev(self) -> None: walker = self.Container(["a", "b", "c"]) self.assertEqual(("a", 0), walker.get_prev(1)) def test_get_prev_out_of_range(self) -> None: walker = self.Container(["a", "b", "c"]) self.assertEqual((None, None), walker.get_prev(0)) class SimpleListWalkerConstructionTest(unittest.TestCase): def test_from_generator(self) -> None: walker = urwid.SimpleListWalker(str(num) for num in range(5)) self.assertEqual(5, len(walker)) self.assertEqual(0, walker.focus) def test_non_iterable_raises(self) -> None: with self.assertRaises(urwid.ListWalkerError): urwid.SimpleListWalker(123) def test_contents_property_returns_self(self) -> None: walker = urwid.SimpleListWalker([1, 2, 3]) self.assertIs(walker, walker.contents) def test_set_modified_callback_not_implemented(self) -> None: walker = urwid.SimpleListWalker([1, 2, 3]) with self.assertRaises(NotImplementedError): walker.set_modified_callback(lambda: None) class SimpleListWalkerMutationTest(unittest.TestCase): def setUp(self) -> None: self.walker = urwid.SimpleListWalker([0, 1, 2, 3, 4]) self.modified_count = 0 urwid.connect_signal(self.walker, "modified", self._on_modified) def _on_modified(self) -> None: self.modified_count += 1 def test_insert_triggers_modified(self) -> None: self.walker.insert(0, -1) self.assertEqual(1, self.modified_count) self.assertEqual([-1, 0, 1, 2, 3, 4], list(self.walker)) def test_append_triggers_modified(self) -> None: self.walker.append(5) self.assertEqual(1, self.modified_count) self.assertEqual([0, 1, 2, 3, 4, 5], list(self.walker)) def test_extend_triggers_modified(self) -> None: self.walker.extend([5, 6]) self.assertEqual(1, self.modified_count) self.assertEqual([0, 1, 2, 3, 4, 5, 6], list(self.walker)) def test_pop_triggers_modified(self) -> None: self.walker.pop() self.assertEqual(1, self.modified_count) self.assertEqual([0, 1, 2, 3], list(self.walker)) def test_remove_triggers_modified(self) -> None: self.walker.remove(2) self.assertEqual(1, self.modified_count) self.assertEqual([0, 1, 3, 4], list(self.walker)) def test_sort_triggers_modified(self) -> None: self.walker[:] = [3, 1, 2, 0, 4] self.modified_count = 0 self.walker.sort() self.assertEqual(1, self.modified_count) self.assertEqual([0, 1, 2, 3, 4], list(self.walker)) def test_del_slice_triggers_modified(self) -> None: del self.walker[1:3] self.assertEqual(1, self.modified_count) self.assertEqual([0, 3, 4], list(self.walker)) def test_operators_returning_a_new_list_do_not_trigger_modified(self) -> None: """Do not notify for ``+`` and ``n * walker``, which return a new list and leave the walker unchanged.""" for name, operation, expected in ( ("add", lambda walker: operator.add(walker, [5]), [0, 1, 2, 3, 4, 5]), ("rmul", lambda walker: operator.mul(2, walker), [0, 1, 2, 3, 4] * 2), ): with self.subTest(name): self.modified_count = 0 self.assertEqual(expected, operation(self.walker)) self.assertEqual(0, self.modified_count) self.assertEqual([0, 1, 2, 3, 4], list(self.walker)) def test_negative_index_getitem(self) -> None: self.assertEqual(4, self.walker[-1]) def test_negative_index_setitem(self) -> None: self.walker[-1] = 40 self.assertEqual([0, 1, 2, 3, 40], list(self.walker)) def test_slicing(self) -> None: self.assertEqual([1, 2, 3], self.walker[1:4]) def test_focus_adjusted_when_shrunk_past_focus(self) -> None: self.walker.focus = 4 del self.walker[1:] self.assertEqual(0, self.walker.focus) def test_focus_not_adjusted_when_still_valid(self) -> None: self.walker.focus = 1 self.walker.append(5) self.assertEqual(1, self.walker.focus) def test_focus_adjusted_on_empty_list(self) -> None: self.walker.focus = 4 del self.walker[:] self.assertEqual(0, self.walker.focus) class SimpleListWalkerFocusApiTest(unittest.TestCase): def setUp(self) -> None: self.walker = urwid.SimpleListWalker([0, 1, 2]) def test_set_focus(self) -> None: self.walker.set_focus(2) self.assertEqual(2, self.walker.focus) def test_set_focus_out_of_range(self) -> None: with self.assertRaises(IndexError): self.walker.set_focus(5) def test_set_focus_negative(self) -> None: with self.assertRaises(IndexError): self.walker.set_focus(-1) def test_next_position(self) -> None: self.assertEqual(1, self.walker.next_position(0)) def test_next_position_at_end_no_wrap(self) -> None: with self.assertRaises(IndexError): self.walker.next_position(2) def test_next_position_at_end_wrap_around(self) -> None: walker = urwid.SimpleListWalker([0, 1, 2], wrap_around=True) self.assertEqual(0, walker.next_position(2)) def test_prev_position(self) -> None: self.assertEqual(0, self.walker.prev_position(1)) def test_prev_position_at_start_no_wrap(self) -> None: with self.assertRaises(IndexError): self.walker.prev_position(0) def test_prev_position_at_start_wrap_around(self) -> None: walker = urwid.SimpleListWalker([0, 1, 2], wrap_around=True) self.assertEqual(2, walker.prev_position(0)) def test_positions(self) -> None: self.assertEqual([0, 1, 2], list(self.walker.positions())) def test_positions_reverse(self) -> None: self.assertEqual([2, 1, 0], list(self.walker.positions(reverse=True))) class SimpleFocusListWalkerConstructionTest(unittest.TestCase): def test_from_generator(self) -> None: walker = urwid.SimpleFocusListWalker(str(num) for num in range(5)) self.assertEqual(5, len(walker)) def test_non_iterable_raises(self) -> None: with self.assertRaises(urwid.ListWalkerError): urwid.SimpleFocusListWalker(123) def test_set_modified_callback_not_implemented(self) -> None: walker = urwid.SimpleFocusListWalker([1, 2, 3]) with self.assertRaises(NotImplementedError): walker.set_modified_callback(lambda: None) class SimpleFocusListWalkerFocusTest(unittest.TestCase): def setUp(self) -> None: self.walker = urwid.SimpleFocusListWalker([0, 1, 2, 3, 4]) self.modified_count = 0 urwid.connect_signal(self.walker, "modified", self._on_modified) def _on_modified(self) -> None: self.modified_count += 1 def test_set_focus(self) -> None: self.walker.set_focus(3) self.assertEqual(3, self.walker.focus) self.assertEqual(1, self.modified_count) def test_focus_tracks_insert_before_focus(self) -> None: self.walker.set_focus(2) self.walker.insert(0, -1) self.assertEqual(3, self.walker.focus) def test_focus_tracks_removal_before_focus(self) -> None: self.walker.set_focus(3) del self.walker[0] self.assertEqual(2, self.walker.focus) def test_next_position(self) -> None: self.assertEqual(1, self.walker.next_position(0)) def test_next_position_at_end_no_wrap(self) -> None: with self.assertRaises(IndexError): self.walker.next_position(4) def test_next_position_at_end_wrap_around(self) -> None: walker = urwid.SimpleFocusListWalker([0, 1, 2], wrap_around=True) self.assertEqual(0, walker.next_position(2)) def test_prev_position(self) -> None: self.assertEqual(1, self.walker.prev_position(2)) def test_prev_position_at_start_no_wrap(self) -> None: with self.assertRaises(IndexError): self.walker.prev_position(0) def test_prev_position_at_start_wrap_around(self) -> None: walker = urwid.SimpleFocusListWalker([0, 1, 2], wrap_around=True) self.assertEqual(2, walker.prev_position(0)) def test_positions(self) -> None: self.assertEqual([0, 1, 2, 3, 4], list(self.walker.positions())) def test_positions_reverse(self) -> None: self.assertEqual([4, 3, 2, 1, 0], list(self.walker.positions(reverse=True))) def test_next_prev_position_on_empty_list(self) -> None: walker = urwid.SimpleFocusListWalker([]) with self.assertRaises(IndexError): walker.next_position(0) with self.assertRaises(IndexError): walker.prev_position(0) class MonitoredFocusListModifiedCallbackTest(unittest.TestCase): """The modified callback runs once, after both the contents and the focus are updated.""" def test_callback_sees_final_focus(self) -> None: """Every mutating operation notifies once, with the contents and the focus already final.""" operations = { "del item": lambda ml: ml.__delitem__(0), "del slice": lambda ml: ml.__delitem__(slice(0, 2)), "set item": lambda ml: ml.__setitem__(2, 7), "set slice": lambda ml: ml.__setitem__(slice(0, 1), [8, 9]), "imul": lambda ml: ml.__imul__(0), "append": lambda ml: ml.append(6), "extend": lambda ml: ml.extend([6, 7]), "insert": lambda ml: ml.insert(0, -1), "pop": lambda ml: ml.pop(0), "remove": lambda ml: ml.remove(0), "reverse": lambda ml: ml.reverse(), "sort": lambda ml: ml.sort(reverse=True), "clear": lambda ml: ml.clear(), } for name, operation in operations.items(): with self.subTest(name): ml = urwid.MonitoredFocusList(range(6), focus=2) seen = [] ml.set_modified_callback(lambda ml=ml, seen=seen: seen.append((list(ml), ml.focus))) operation(ml) self.assertEqual([(list(ml), ml.focus)], seen) def test_focus_set_by_callback_is_kept(self) -> None: """A focus set by the modified callback is not overwritten afterwards.""" ml = urwid.MonitoredFocusList(range(6), focus=2) ml.set_modified_callback(lambda: setattr(ml, "focus", 0) if ml.focus else None) ml.insert(0, -1) self.assertEqual(0, ml.focus) def test_insert_by_callback_keeps_focus_item(self) -> None: """An insertion made by the modified callback keeps the focus on the same item.""" ml = urwid.MonitoredFocusList(["a", "b", "c", "d"], focus=2) ml.set_modified_callback(lambda: ml.insert(0, "x") if len(ml) == 3 else None) del ml[0] self.assertEqual(["x", "b", "c", "d"], list(ml)) self.assertEqual("c", ml[ml.focus]) def test_walker_signal_sees_final_focus(self) -> None: """A ListBox rendered from the walker's modified signal shows the new focus item.""" walker = urwid.SimpleFocusListWalker([urwid.Text(str(num)) for num in range(3)]) walker.focus = 2 listbox = urwid.ListBox(walker) seen = [] def on_modified() -> None: seen.append((walker.get_focus()[1], listbox.render((5, 1), focus=True).text)) urwid.connect_signal(walker, "modified", on_modified) del walker[2] self.assertEqual([(1, [b"1 "])], seen) if __name__ == "__main__": unittest.main() urwid-urwid-69907e4/tests/widgets/test_listbox.py000066400000000000000000002752121526073734300222150ustar00rootroot00000000000000from __future__ import annotations import dataclasses import typing import unittest import weakref import urwid from tests.util import SelectableText, collect_and_count_alive class ListBoxCalculateVisibleTest(unittest.TestCase): def cvtest( self, desc: str, body, focus: int, offset_rows: int, inset_fraction, exp_offset_inset: int, exp_cur: tuple[int, int] | None, ) -> None: with self.subTest(desc): lbox = urwid.ListBox(urwid.SimpleListWalker(body)) lbox.body.set_focus(focus) lbox.offset_rows = offset_rows lbox.inset_fraction = inset_fraction middle, top, bottom = lbox.calculate_visible((4, 5), focus=1) offset_inset, focus_widget, focus_pos, _ign, cursor = middle if cursor is not None: x, y = cursor y += offset_inset cursor = x, y self.assertEqual(exp_offset_inset, offset_inset) self.assertEqual(exp_cur, cursor) def test1_simple(self): T = urwid.Text l = [T(""), T(""), T("\n"), T("\n\n"), T("\n"), T(""), T("")] self.cvtest( "simple top position", l, 3, 0, (0, 1), 0, None, ) self.cvtest( "simple middle position", l, 3, 1, (0, 1), 1, None, ) self.cvtest( "simple bottom position", l, 3, 2, (0, 1), 2, None, ) self.cvtest( "straddle top edge", l, 3, 0, (1, 2), -1, None, ) self.cvtest( "straddle bottom edge", l, 3, 4, (0, 1), 4, None, ) self.cvtest( "off bottom edge", l, 3, 5, (0, 1), 4, None, ) self.cvtest( "way off bottom edge", l, 3, 100, (0, 1), 4, None, ) self.cvtest( "gap at top", l, 0, 2, (0, 1), 0, None, ) self.cvtest( "gap at top and off bottom edge", l, 2, 5, (0, 1), 2, None, ) self.cvtest( "gap at bottom", l, 6, 1, (0, 1), 4, None, ) self.cvtest( "gap at bottom and straddling top edge", l, 4, 0, (1, 2), 1, None, ) self.cvtest( "gap at bottom cannot completely fill", [T(""), T(""), T("")], 1, 0, (0, 1), 1, None, ) self.cvtest( "gap at top and bottom", [T(""), T(""), T("")], 1, 2, (0, 1), 1, None, ) def test2_cursor(self): T, E = urwid.Text, urwid.Edit l1 = [T(""), T(""), T("\n"), E("", "\n\nX"), T("\n"), T(""), T("")] l2 = [T(""), T(""), T("\n"), E("", "YY\n\n"), T("\n"), T(""), T("")] l2[3].set_edit_pos(2) self.cvtest( "plain cursor in view", l1, 3, 1, (0, 1), 1, (1, 3), ) self.cvtest( "cursor off top", l2, 3, 0, (1, 3), 0, (2, 0), ) self.cvtest( "cursor further off top", l2, 3, 0, (2, 3), 0, (2, 0), ) self.cvtest( "cursor off bottom", l1, 3, 3, (0, 1), 2, (1, 4), ) self.cvtest( "cursor way off bottom", l1, 3, 100, (0, 1), 2, (1, 4), ) def test_sized(self): lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text(str(num)) for num in range(5)])) self.assertEqual(5, len(lbox)) def test_not_sized(self): class TestWalker(urwid.ListWalker): @property def contents(self): return self @staticmethod def next_position(position: int) -> tuple[urwid.Text, int]: return urwid.Text(str(position)), position @staticmethod def prev_position(position: int) -> tuple[urwid.Text, int]: return urwid.Text(str(position)), position lbox = urwid.ListBox(TestWalker()) with self.assertRaises(AttributeError) as exc: len(lbox) self.assertEqual(f"{TestWalker.__name__} is not Sized", str(exc.exception)) class ListBoxChangeFocusTest(unittest.TestCase): def cftest( self, desc: str, body, pos: int, offset_inset: int, coming_from, cursor: tuple[int, int] | None, snap_rows, exp_offset_rows: int, exp_inset_fraction, exp_cur: tuple[int, int] | None, ): with self.subTest(desc): lbox = urwid.ListBox(urwid.SimpleListWalker(body)) lbox.change_focus((4, 5), pos, offset_inset, coming_from, cursor, snap_rows) exp = exp_offset_rows, exp_inset_fraction act = lbox.offset_rows, lbox.inset_fraction cursor = None focus_widget, focus_pos = lbox.body.get_focus() if focus_widget.selectable(): if hasattr(focus_widget, "get_cursor_coords"): cursor = focus_widget.get_cursor_coords((4,)) self.assertEqual(exp, act) self.assertEqual(exp_cur, cursor) def test1unselectable(self): T = urwid.Text l = [T("\n"), T("\n\n"), T("\n\n"), T("\n\n"), T("\n")] self.cftest( "simple unselectable", l, 2, 0, None, None, None, 0, (0, 1), None, ) self.cftest( "unselectable", l, 2, 1, None, None, None, 1, (0, 1), None, ) self.cftest( "unselectable off top", l, 2, -2, None, None, None, 0, (2, 3), None, ) self.cftest( "unselectable off bottom", l, 3, 2, None, None, None, 2, (0, 1), None, ) def test2selectable(self): T, S = urwid.Text, SelectableText l = [T("\n"), T("\n\n"), S("\n\n"), T("\n\n"), T("\n")] self.cftest( "simple selectable", l, 2, 0, None, None, None, 0, (0, 1), None, ) self.cftest( "selectable", l, 2, 1, None, None, None, 1, (0, 1), None, ) self.cftest( "selectable at top", l, 2, 0, "below", None, None, 0, (0, 1), None, ) self.cftest( "selectable at bottom", l, 2, 2, "above", None, None, 2, (0, 1), None, ) self.cftest( "selectable off top snap", l, 2, -1, "below", None, None, 0, (0, 1), None, ) self.cftest( "selectable off bottom snap", l, 2, 3, "above", None, None, 2, (0, 1), None, ) self.cftest( "selectable off top no snap", l, 2, -1, "above", None, None, 0, (1, 3), None, ) self.cftest( "selectable off bottom no snap", l, 2, 3, "below", None, None, 3, (0, 1), None, ) def test3large_selectable(self): T, S = urwid.Text, SelectableText l = [T("\n"), S("\n\n\n\n\n\n"), T("\n")] self.cftest( "large selectable no snap", l, 1, -1, None, None, None, 0, (1, 7), None, ) self.cftest( "large selectable snap up", l, 1, -2, "below", None, None, 0, (0, 1), None, ) self.cftest( "large selectable snap up2", l, 1, -2, "below", None, 2, 0, (0, 1), None, ) self.cftest( "large selectable almost snap up", l, 1, -2, "below", None, 1, 0, (2, 7), None, ) self.cftest( "large selectable snap down", l, 1, 0, "above", None, None, 0, (2, 7), None, ) self.cftest( "large selectable snap down2", l, 1, 0, "above", None, 2, 0, (2, 7), None, ) self.cftest( "large selectable almost snap down", l, 1, 0, "above", None, 1, 0, (0, 1), None, ) m = [T("\n\n\n\n"), S("\n\n\n\n\n"), T("\n\n\n\n")] self.cftest( "large selectable outside view down", m, 1, 4, "above", None, None, 0, (0, 1), None, ) self.cftest( "large selectable outside view up", m, 1, -5, "below", None, None, 0, (1, 6), None, ) def test4cursor(self): T, E = urwid.Text, urwid.Edit # ... def test5set_focus_valign(self): T, E = urwid.Text, urwid.Edit lbox = urwid.ListBox(urwid.SimpleFocusListWalker([T(""), T("")])) lbox.set_focus_valign("middle") # TODO: actually test the result class ListBoxRenderTest(unittest.TestCase): def ltest( self, desc: str, body, focus: int, offset_inset_rows: int, exp_text: list[bytes], exp_cur: tuple[int, int] | None, ) -> None: with self.subTest(desc): exp_text = [t.encode("iso8859-1") for t in exp_text] lbox = urwid.ListBox(urwid.SimpleListWalker(body)) lbox.body.set_focus(focus) lbox.shift_focus((4, 10), offset_inset_rows) canvas = lbox.render((4, 5), focus=1) self.assertEqual(exp_text, canvas.text) self.assertEqual(exp_cur, canvas.cursor) def test1_simple(self): T = urwid.Text self.ltest( "simple one text item render", [T("1\n2")], 0, 0, ["1 ", "2 ", " ", " ", " "], None, ) self.ltest( "simple multi text item render off bottom", [T("1"), T("2"), T("3\n4"), T("5"), T("6")], 2, 2, ["1 ", "2 ", "3 ", "4 ", "5 "], None, ) self.ltest( "simple multi text item render off top", [T("1"), T("2"), T("3\n4"), T("5"), T("6")], 2, 1, ["2 ", "3 ", "4 ", "5 ", "6 "], None, ) def test2_trim(self): T = urwid.Text self.ltest( "trim unfocused bottom", [T("1\n2"), T("3\n4"), T("5\n6")], 1, 2, ["1 ", "2 ", "3 ", "4 ", "5 "], None, ) self.ltest( "trim unfocused top", [T("1\n2"), T("3\n4"), T("5\n6")], 1, 1, ["2 ", "3 ", "4 ", "5 ", "6 "], None, ) self.ltest( "trim none full focus", [T("1\n2\n3\n4\n5")], 0, 0, ["1 ", "2 ", "3 ", "4 ", "5 "], None, ) self.ltest( "trim focus bottom", [T("1\n2\n3\n4\n5\n6")], 0, 0, ["1 ", "2 ", "3 ", "4 ", "5 "], None, ) self.ltest( "trim focus top", [T("1\n2\n3\n4\n5\n6")], 0, -1, ["2 ", "3 ", "4 ", "5 ", "6 "], None, ) self.ltest( "trim focus top and bottom", [T("1\n2\n3\n4\n5\n6\n7")], 0, -1, ["2 ", "3 ", "4 ", "5 ", "6 "], None, ) def test3_shift(self): T, E = urwid.Text, urwid.Edit self.ltest( "shift up one fit", [T("1\n2"), T("3"), T("4"), T("5"), T("6")], 4, 5, ["2 ", "3 ", "4 ", "5 ", "6 "], None, ) e = E("", "ab\nc", 1) e.set_edit_pos(2) self.ltest( "shift down one cursor over edge", [e, T("3"), T("4"), T("5\n6")], 0, -1, ["ab ", "c ", "3 ", "4 ", "5 "], (2, 0), ) self.ltest( "shift up one cursor over edge", [T("1\n2"), T("3"), T("4"), E("", "d\ne")], 3, 4, ["2 ", "3 ", "4 ", "d ", "e "], (1, 4), ) self.ltest( "shift none cursor top focus over edge", [E("", "ab\n"), T("3"), T("4"), T("5\n6")], 0, -1, [" ", "3 ", "4 ", "5 ", "6 "], (0, 0), ) e = E("", "abc\nd") e.set_edit_pos(3) self.ltest( "shift none cursor bottom focus over edge", [T("1\n2"), T("3"), T("4"), e], 3, 4, ["1 ", "2 ", "3 ", "4 ", "abc "], (3, 4), ) def test4_really_large_contents(self): T, E = urwid.Text, urwid.Edit self.ltest( "really large edit", [T("hello" * 100)], 0, 0, ["hell", "ohel", "lohe", "lloh", "ello"], None, ) self.ltest( "really large edit", [E("", "hello" * 100)], 0, 0, ["hell", "ohel", "lohe", "lloh", "ello"], (3, 4), ) class ListBoxKeypressTest(unittest.TestCase): def ktest( self, desc: str, key, body, focus: int, offset_inset: int, exp_focus: int, exp_offset_inset: int, exp_cur: tuple[int, int] | None, lbox=None, ) -> tuple[str | None, urwid.ListBox]: if lbox is None: lbox = urwid.ListBox(urwid.SimpleListWalker(body)) lbox.body.set_focus(focus) lbox.shift_focus((4, 10), offset_inset) ret_key = lbox.keypress((4, 5), key) middle, top, bottom = lbox.calculate_visible((4, 5), focus=1) offset_inset, focus_widget, focus_pos, _ign, cursor = middle if cursor is not None: x, y = cursor y += offset_inset cursor = x, y exp = exp_focus, exp_offset_inset act = focus_pos, offset_inset self.assertEqual(exp, act) self.assertEqual(exp_cur, cursor) return ret_key, lbox def test1_up(self): T, S, E = urwid.Text, SelectableText, urwid.Edit self.ktest( "direct selectable both visible", "up", [S(""), S("")], 1, 1, 0, 0, None, ) self.ktest( "selectable skip one all visible", "up", [S(""), T(""), S("")], 2, 2, 0, 0, None, ) key, lbox = self.ktest( "nothing above no scroll", "up", [S("")], 0, 0, 0, 0, None, ) self.assertEqual("up", key) key, lbox = self.ktest( "unselectable above no scroll", "up", [T(""), T(""), S("")], 2, 2, 2, 2, None, ) self.assertEqual("up", key) self.ktest( "unselectable above scroll 1", "up", [T(""), S(""), T("\n\n\n")], 1, 0, 1, 1, None, ) self.ktest( "selectable above scroll 1", "up", [S(""), S(""), T("\n\n\n")], 1, 0, 0, 0, None, ) self.ktest( "selectable above too far", "up", [S(""), T(""), S(""), T("\n\n\n")], 2, 0, 2, 1, None, ) self.ktest( "selectable above skip 1 scroll 1", "up", [S(""), T(""), S(""), T("\n\n\n")], 2, 1, 0, 0, None, ) self.ktest( "tall selectable above scroll 2", "up", [S(""), S("\n"), S(""), T("\n\n\n")], 2, 0, 1, 0, None, ) self.ktest( "very tall selectable above scroll 5", "up", [S(""), S("\n\n\n\n"), S(""), T("\n\n\n\n")], 2, 0, 1, 0, None, ) self.ktest( "very tall selected scroll within 1", "up", [S(""), S("\n\n\n\n\n")], 1, -1, 1, 0, None, ) self.ktest( "edit above pass cursor", "up", [E("", "abc"), E("", "de")], 1, 1, 0, 0, (2, 0), ) key, lbox = self.ktest( "edit too far above pass cursor A", "up", [E("", "abc"), T("\n\n\n\n"), E("", "de")], 2, 4, 1, 0, None, ) self.ktest( "edit too far above pass cursor B", "up", None, None, None, 0, 0, (2, 0), lbox, ) self.ktest( "within focus cursor made not visible", "up", [T("\n\n\n"), E("hi\n", "ab")], 1, 3, 0, 0, None, ) self.ktest( "within focus cursor made not visible (2)", "up", [T("\n\n\n\n"), E("hi\n", "ab")], 1, 3, 0, -1, None, ) self.ktest( "force focus unselectable", "up", [T("\n\n\n\n"), S("")], 1, 4, 0, 0, None, ) self.ktest( "pathological cursor widget", "up", [T("\n"), E("\n\n\n\n\n", "a")], 1, 4, 0, -1, None, ) self.ktest( "unselectable to unselectable", "up", [T(""), T(""), T(""), T(""), T(""), T(""), T("")], 2, 0, 1, 0, None, ) self.ktest( "unselectable over edge to same", "up", [T(""), T("12\n34"), T(""), T(""), T(""), T("")], 1, -1, 1, 0, None, ) key, lbox = self.ktest( "edit short between pass cursor A", "up", [E("", "abcd"), E("", "a"), E("", "def")], 2, 2, 1, 1, (1, 1), ) self.ktest( "edit short between pass cursor B", "up", None, None, None, 0, 0, (3, 0), lbox, ) e = E("", "\n\n\n\n\n") e.set_edit_pos(1) key, lbox = self.ktest( "edit cursor force scroll", "up", [e], 0, -1, 0, 0, (0, 0), ) self.assertEqual(0, lbox.inset_fraction[0]) def test2_down(self): T, S, E = urwid.Text, SelectableText, urwid.Edit self.ktest( "direct selectable both visible", "down", [S(""), S("")], 0, 0, 1, 1, None, ) self.ktest( "selectable skip one all visible", "down", [S(""), T(""), S("")], 0, 0, 2, 2, None, ) key, lbox = self.ktest( "nothing below no scroll", "down", [S("")], 0, 0, 0, 0, None, ) self.assertEqual("down", key) key, lbox = self.ktest( "unselectable below no scroll", "down", [S(""), T(""), T("")], 0, 0, 0, 0, None, ) self.assertEqual("down", key) self.ktest( "unselectable below scroll 1", "down", [T("\n\n\n"), S(""), T("")], 1, 4, 1, 3, None, ) self.ktest( "selectable below scroll 1", "down", [T("\n\n\n"), S(""), S("")], 1, 4, 2, 4, None, ) self.ktest( "selectable below too far", "down", [T("\n\n\n"), S(""), T(""), S("")], 1, 4, 1, 3, None, ) self.ktest( "selectable below skip 1 scroll 1", "down", [T("\n\n\n"), S(""), T(""), S("")], 1, 3, 3, 4, None, ) self.ktest( "tall selectable below scroll 2", "down", [T("\n\n\n"), S(""), S("\n"), S("")], 1, 4, 2, 3, None, ) self.ktest( "very tall selectable below scroll 5", "down", [T("\n\n\n\n"), S(""), S("\n\n\n\n"), S("")], 1, 4, 2, 0, None, ) self.ktest( "very tall selected scroll within 1", "down", [S("\n\n\n\n\n"), S("")], 0, 0, 0, -1, None, ) self.ktest( "edit below pass cursor", "down", [E("", "de"), E("", "abc")], 0, 0, 1, 1, (2, 1), ) key, lbox = self.ktest( "edit too far below pass cursor A", "down", [E("", "de"), T("\n\n\n\n"), E("", "abc")], 0, 0, 1, 0, None, ) self.ktest( "edit too far below pass cursor B", "down", None, None, None, 2, 4, (2, 4), lbox, ) odd_e = E("", "hi\nab") odd_e.set_edit_pos(2) # disble cursor movement in odd_e object odd_e.move_cursor_to_coords = lambda s, c, xy: 0 self.ktest( "within focus cursor made not visible", "down", [odd_e, T("\n\n\n\n")], 0, 0, 1, 1, None, ) self.ktest( "within focus cursor made not visible (2)", "down", [ odd_e, T("\n\n\n\n"), ], 0, 0, 1, 1, None, ) self.ktest( "force focus unselectable", "down", [S(""), T("\n\n\n\n")], 0, 0, 1, 0, None, ) odd_e.set_edit_text("hi\n\n\n\n\n") self.ktest( "pathological cursor widget", "down", [odd_e, T("\n")], 0, 0, 1, 4, None, ) self.ktest( "unselectable to unselectable", "down", [T(""), T(""), T(""), T(""), T(""), T(""), T("")], 4, 4, 5, 4, None, ) self.ktest( "unselectable over edge to same", "down", [T(""), T(""), T(""), T(""), T("12\n34"), T("")], 4, 4, 4, 3, None, ) key, lbox = self.ktest( "edit short between pass cursor A", "down", [E("", "abc"), E("", "a"), E("", "defg")], 0, 0, 1, 1, (1, 1), ) self.ktest( "edit short between pass cursor B", "down", None, None, None, 2, 2, (3, 2), lbox, ) e = E("", "\n\n\n\n\n") e.set_edit_pos(4) key, lbox = self.ktest( "edit cursor force scroll", "down", [e], 0, 0, 0, -1, (0, 4), ) self.assertEqual(1, lbox.inset_fraction[0]) def test3_page_up(self): T, S, E = urwid.Text, SelectableText, urwid.Edit self.ktest( "unselectable aligned to aligned", "page up", [T(""), T("\n"), T("\n\n"), T(""), T("\n"), T("\n\n")], 3, 0, 1, 0, None, ) self.ktest( "unselectable unaligned to aligned", "page up", [T(""), T("\n"), T("\n"), T("\n"), T("\n"), T("\n\n")], 3, -1, 1, 0, None, ) self.ktest( "selectable to unselectable", "page up", [T(""), T("\n"), T("\n"), T("\n"), S("\n"), T("\n\n")], 4, 1, 1, -1, None, ) self.ktest( "selectable to cut off selectable", "page up", [S("\n\n"), T("\n"), T("\n"), S("\n"), T("\n\n")], 3, 1, 0, -1, None, ) self.ktest( "seletable to selectable", "page up", [T("\n\n"), S("\n"), T("\n"), S("\n"), T("\n\n")], 3, 1, 1, 1, None, ) self.ktest( "within very long selectable", "page up", [S(""), S("\n\n\n\n\n\n\n\n"), T("\n")], 1, -6, 1, -1, None, ) e = E("", "\n\nab\n\n\n\n\ncd\n") e.set_edit_pos(11) self.ktest( "within very long cursor widget", "page up", [S(""), e, T("\n")], 1, -6, 1, -2, (2, 0), ) self.ktest( "pathological cursor widget", "page up", [T(""), E("\n\n\n\n\n\n\n\n", "ab"), T("")], 1, -5, 0, 0, None, ) e = E("", "\nab\n\n\n\n\ncd\n") e.set_edit_pos(10) self.ktest( "very long cursor widget snap", "page up", [T(""), e, T("\n")], 1, -5, 1, 0, (2, 1), ) self.ktest( "slight scroll selectable", "page up", [T("\n"), S("\n"), T(""), S(""), T("\n\n\n"), S("")], 5, 4, 3, 0, None, ) self.ktest( "scroll into snap region", "page up", [T("\n"), S("\n"), T(""), T(""), T("\n\n\n"), S("")], 5, 4, 1, 0, None, ) self.ktest( "mid scroll short", "page up", [T("\n"), T(""), T(""), S(""), T(""), T("\n"), S(""), T("\n")], 6, 2, 3, 1, None, ) self.ktest( "mid scroll long", "page up", [T("\n"), S(""), T(""), S(""), T(""), T("\n"), S(""), T("\n")], 6, 2, 1, 0, None, ) self.ktest( "mid scroll perfect", "page up", [T("\n"), S(""), S(""), S(""), T(""), T("\n"), S(""), T("\n")], 6, 2, 2, 0, None, ) self.ktest( "cursor move up fail short", "page up", [T("\n"), T("\n"), E("", "\nab"), T(""), T("")], 2, 1, 2, 4, (0, 4), ) self.ktest( "cursor force fail short", "page up", [T("\n"), T("\n"), E("\n", "ab"), T(""), T("")], 2, 1, 0, 0, None, ) odd_e = E("", "hi\nab") odd_e.set_edit_pos(2) # disble cursor movement in odd_e object odd_e.move_cursor_to_coords = lambda s, c, xy: 0 self.ktest( "cursor force fail long", "page up", [odd_e, T("\n"), T("\n"), T("\n"), S(""), T("\n")], 4, 2, 1, -1, None, ) self.ktest( "prefer not cut off", "page up", [S("\n"), T("\n"), S(""), T("\n\n"), S(""), T("\n")], 4, 2, 2, 1, None, ) self.ktest( "allow cut off", "page up", [S("\n"), T("\n"), T(""), T("\n\n"), S(""), T("\n")], 4, 2, 0, -1, None, ) self.ktest( "at top fail", "page up", [T("\n\n"), T("\n"), T("\n\n\n")], 0, 0, 0, 0, None, ) self.ktest( "all visible fail", "page up", [T("a"), T("\n")], 0, 0, 0, 0, None, ) self.ktest( "current ok fail", "page up", [T("\n\n"), S("hi")], 1, 3, 1, 3, None, ) self.ktest( "all visible choose top selectable", "page up", [T(""), S("a"), S("b"), S("c")], 3, 3, 1, 1, None, ) self.ktest( "bring in edge choose top", "page up", [S("b"), T("-"), S("-"), T("c"), S("d"), T("-")], 4, 3, 0, 0, None, ) self.ktest( "bring in edge choose top selectable", "page up", [T("b"), S("-"), S("-"), T("c"), S("d"), T("-")], 4, 3, 1, 1, None, ) def test4_page_down(self): T, S, E = urwid.Text, SelectableText, urwid.Edit self.ktest( "unselectable aligned to aligned", "page down", [T("\n\n"), T("\n"), T(""), T("\n\n"), T("\n"), T("")], 2, 4, 4, 3, None, ) self.ktest( "unselectable unaligned to aligned", "page down", [T("\n\n"), T("\n"), T("\n"), T("\n"), T("\n"), T("")], 2, 4, 4, 3, None, ) self.ktest( "selectable to unselectable", "page down", [T("\n\n"), S("\n"), T("\n"), T("\n"), T("\n"), T("")], 1, 2, 4, 4, None, ) self.ktest( "selectable to cut off selectable", "page down", [T("\n\n"), S("\n"), T("\n"), T("\n"), S("\n\n")], 1, 2, 4, 3, None, ) self.ktest( "seletable to selectable", "page down", [T("\n\n"), S("\n"), T("\n"), S("\n"), T("\n\n")], 1, 1, 3, 2, None, ) self.ktest( "within very long selectable", "page down", [T("\n"), S("\n\n\n\n\n\n\n\n"), S("")], 1, 2, 1, -3, None, ) e = E("", "\nab\n\n\n\n\ncd\n\n") e.set_edit_pos(2) self.ktest( "within very long cursor widget", "page down", [T("\n"), e, S("")], 1, 2, 1, -2, (1, 4), ) odd_e = E("", "ab\n\n\n\n\n\n\n\n\n") odd_e.set_edit_pos(1) # disble cursor movement in odd_e object odd_e.move_cursor_to_coords = lambda s, c, xy: 0 self.ktest( "pathological cursor widget", "page down", [T(""), odd_e, T("")], 1, 1, 2, 4, None, ) e = E("", "\nab\n\n\n\n\ncd\n") e.set_edit_pos(2) self.ktest( "very long cursor widget snap", "page down", [T("\n"), e, T("")], 1, 2, 1, -3, (1, 3), ) self.ktest( "slight scroll selectable", "page down", [S(""), T("\n\n\n"), S(""), T(""), S("\n"), T("\n")], 0, 0, 2, 4, None, ) self.ktest( "scroll into snap region", "page down", [S(""), T("\n\n\n"), T(""), T(""), S("\n"), T("\n")], 0, 0, 4, 3, None, ) self.ktest( "mid scroll short", "page down", [T("\n"), S(""), T("\n"), T(""), S(""), T(""), T(""), T("\n")], 1, 2, 4, 3, None, ) self.ktest( "mid scroll long", "page down", [T("\n"), S(""), T("\n"), T(""), S(""), T(""), S(""), T("\n")], 1, 2, 6, 4, None, ) self.ktest( "mid scroll perfect", "page down", [T("\n"), S(""), T("\n"), T(""), S(""), S(""), S(""), T("\n")], 1, 2, 5, 4, None, ) e = E("", "hi\nab") e.set_edit_pos(1) self.ktest( "cursor move up fail short", "page down", [T(""), T(""), e, T("\n"), T("\n")], 2, 1, 2, -1, (1, 0), ) odd_e = E("", "hi\nab") odd_e.set_edit_pos(1) # disble cursor movement in odd_e object odd_e.move_cursor_to_coords = lambda s, c, xy: 0 self.ktest( "cursor force fail short", "page down", [T(""), T(""), odd_e, T("\n"), T("\n")], 2, 2, 4, 3, None, ) self.ktest( "cursor force fail long", "page down", [T("\n"), S(""), T("\n"), T("\n"), T("\n"), E("hi\n", "ab")], 1, 2, 4, 4, None, ) self.ktest( "prefer not cut off", "page down", [T("\n"), S(""), T("\n\n"), S(""), T("\n"), S("\n")], 1, 2, 3, 3, None, ) self.ktest( "allow cut off", "page down", [T("\n"), S(""), T("\n\n"), T(""), T("\n"), S("\n")], 1, 2, 5, 4, None, ) self.ktest( "at bottom fail", "page down", [T("\n\n"), T("\n"), T("\n\n\n")], 2, 1, 2, 1, None, ) self.ktest( "all visible fail", "page down", [T("a"), T("\n")], 1, 1, 1, 1, None, ) self.ktest( "current ok fail", "page down", [S("hi"), T("\n\n")], 0, 0, 0, 0, None, ) self.ktest( "all visible choose last selectable", "page down", [S("a"), S("b"), S("c"), T("")], 0, 0, 2, 2, None, ) self.ktest( "bring in edge choose last", "page down", [T("-"), S("d"), T("c"), S("-"), T("-"), S("b")], 1, 1, 5, 4, None, ) self.ktest( "bring in edge choose last selectable", "page down", [T("-"), S("d"), T("c"), S("-"), S("-"), T("b")], 1, 1, 4, 3, None, ) class ZeroHeightContentsTest(unittest.TestCase): def test_listbox_pile(self): lb = urwid.ListBox(urwid.SimpleListWalker([urwid.Pile([])])) size = (2, 5) canvas = lb.render(size, focus=True) self.assertEqual([b" ", b" ", b" ", b" ", b" "], canvas.text) def test_listbox_text_pile_page_down(self): lb = urwid.ListBox(urwid.SimpleListWalker([urwid.Text("above"), urwid.Pile([])])) size = (7, 5) lb.keypress(size, "page down") self.assertEqual(lb.focus_position, 0) lb.keypress(size, "page down") # second one caused ListBox failure self.assertEqual(lb.focus_position, 0) canvas = lb.render(size, focus=True) self.assertEqual([b"above ", b" ", b" ", b" ", b" "], canvas.text) def test_listbox_text_pile_page_up(self): lb = urwid.ListBox(urwid.SimpleListWalker([urwid.Pile([]), urwid.Text("below")])) size = (7, 5) lb.set_focus(1) lb.keypress(size, "page up") self.assertEqual(lb.focus_position, 1) lb.keypress(size, "page up") # second one caused pile failure self.assertEqual(lb.focus_position, 1) canvas = lb.render(size, focus=True) self.assertEqual([b"below ", b" ", b" ", b" ", b" "], canvas.text) def test_listbox_text_pile_down(self): sp = urwid.Pile([]) sp.selectable = lambda: True # abuse our Pile lb = urwid.ListBox(urwid.SimpleListWalker([urwid.Text("above"), sp])) size = (7, 5) lb.keypress(size, "down") self.assertEqual(lb.focus_position, 0) lb.keypress(size, "down") self.assertEqual(lb.focus_position, 0) canvas = lb.render(size, focus=True) self.assertEqual([b"above ", b" ", b" ", b" ", b" "], canvas.text) def test_listbox_text_pile_up(self): sp = urwid.Pile([]) sp.selectable = lambda: True # abuse our Pile lb = urwid.ListBox(urwid.SimpleListWalker([sp, urwid.Text("below")])) size = (7, 5) lb.set_focus(1) lb.keypress(size, "up") self.assertEqual(lb.focus_position, 1) lb.keypress(size, "up") self.assertEqual(lb.focus_position, 1) canvas = lb.render(size, focus=True) self.assertEqual([b"below ", b" ", b" ", b" ", b" "], canvas.text) def test_listbox_all_zero_rows_keys(self) -> None: """Keys over a body with no visible rows leave the ListBox in a state it can render.""" size = (2, 5) for key, expected_focus in (("page up", 2), ("page down", 2), ("up", 2), ("down", 2), ("home", 0), ("end", 4)): with self.subTest(key=key): lb = urwid.ListBox(urwid.SimpleFocusListWalker([urwid.Pile([]) for _ in range(5)])) lb.focus_position = 2 lb.keypress(size, key) self.assertEqual([b" "] * 5, lb.render(size, focus=True).text) self.assertEqual(expected_focus, lb.focus_position) class PageDownAboveTopTest(unittest.TestCase): def test_page_down_does_not_raise(self): # Two-row rows so the four-item list does not fit the viewport at once. lb = urwid.ListBox( urwid.SimpleListWalker( [ urwid.Text("a0\nb0"), SelectableText("a1\nb1"), urwid.Text("a2\nb2"), urwid.Text("a3\nb3"), ] ) ) size = (7, 4) lb.set_focus(0) lb.render(size, focus=True) lb.keypress(size, "page down") # used to raise ListBoxError self.assertEqual(lb.focus_position, 3) canvas = lb.render(size, focus=True) self.assertEqual([b"a2 ", b"b2 ", b"a3 ", b"b3 "], canvas.text) # a second page down at the bottom is a no-op, not a crash lb.keypress(size, "page down") self.assertEqual(lb.focus_position, 3) class ListBoxSetBodyTest(unittest.TestCase): def test_signal_connected(self): lb = urwid.ListBox([]) lb.body = urwid.SimpleListWalker([]) self.assertEqual( lb.body._urwid_signals["modified"][0][1], urwid.ListBox._invalidate, "outdated canvas cache reuse after ListWalker's contents modified", ) class TestListWalkerFromIterable(unittest.TestCase): def test_01_simple_list_walker(self): walker = urwid.SimpleListWalker(str(num) for num in range(5)) self.assertEqual(5, len(walker)) def test_02_simple_focus_list_walker(self): walker = urwid.SimpleFocusListWalker(str(num) for num in range(5)) self.assertEqual(5, len(walker)) class ListBoxConsumerApiTest(unittest.TestCase): def test_simple_list_walker_clear_and_append(self) -> None: walker = urwid.SimpleListWalker([urwid.Text("old")]) listbox = urwid.ListBox(walker) walker.clear() walker.append(urwid.AttrMap(urwid.Text("new"), None)) self.assertEqual(1, len(listbox)) self.assertEqual(0, listbox.focus_position) self.assertEqual("new", listbox.focus.original_widget.text) def test_set_focus_and_len(self) -> None: items = [urwid.Text(str(idx)) for idx in range(3)] listbox = urwid.ListBox(urwid.SimpleListWalker(items)) listbox.set_focus(2) self.assertEqual(2, listbox.focus_position) self.assertIs(items[2], listbox.focus) self.assertEqual(3, len(listbox)) self.assertEqual([items[2]], listbox.get_focus_widgets()) class DuckTypedWalker: """Minimal duck-typed ListWalker-like object (not a ListWalker subclass).""" def __init__(self, widgets): self._widgets = widgets self._focus = 0 def get_focus(self): if not self._widgets: return None, None return self._widgets[self._focus], self._focus def get_next(self, position): pos = position + 1 if pos >= len(self._widgets): return None, None return self._widgets[pos], pos def get_prev(self, position): pos = position - 1 if pos < 0: return None, None return self._widgets[pos], pos def set_focus(self, position): self._focus = position class V1ProtocolWalker(urwid.ListWalker): """ListWalker subclass without a ``__getitem__()``, to exercise the v1 contents protocol.""" def __init__(self, widgets): self._widgets = widgets self.focus = 0 def get_focus(self): if not self._widgets: return None, None return self._widgets[self.focus], self.focus def get_next(self, position): pos = position + 1 if pos >= len(self._widgets): return None, None return self._widgets[pos], pos def get_prev(self, position): pos = position - 1 if pos < 0: return None, None return self._widgets[pos], pos def set_focus(self, position): self.focus = position self._modified() class _LeafNode(urwid.TreeNode): pass class _RootNode(urwid.ParentNode): """Minimal tree used to build a real urwid.TreeWalker, which (like a few other ListWalker implementations) has no positions() method -- unlike inventing a throwaway stand-in, this exercises ListBox's "no positions()" fallback paths with an existing ListWalker subclass from the codebase. """ def __init__(self, n_children): self._n_children = n_children super().__init__("root", key="root", depth=0) def load_child_keys(self): return list(range(self._n_children)) def load_child_node(self, key): return _LeafNode(f"item{key}", parent=self, key=key, depth=1) def make_tree_walker(n_children=4, focus_key=0): root = _RootNode(n_children) walker = urwid.TreeWalker(root.get_child_node(focus_key)) return walker, root class EmptyNoPositionsWalker(urwid.ListWalker): """ListWalker subclass without positions() that can also be empty. urwid.TreeWalker always has a focus node and cannot represent "no widgets", so it cannot be used for this specific empty-body case. """ def get_focus(self): return None, None class ListBoxInitAndBodySetterTest(unittest.TestCase): def test_init_duck_typed_body_warns(self): widgets = [urwid.Text("a"), urwid.Text("b")] walker = DuckTypedWalker(widgets) with self.assertWarns(DeprecationWarning): lbox = urwid.ListBox(walker) self.assertIs(lbox.body, walker) # non-ListWalker body has no "modified" signal, so caching is disabled canvas = lbox.render((4, 2), focus=True) self.assertEqual([b"a ", b"b "], canvas.text) def test_body_setter_duck_typed_warns(self): lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text("a")])) walker = DuckTypedWalker([urwid.Text("x")]) with self.assertWarns(DeprecationWarning): lbox.body = walker self.assertIs(lbox.body, walker) def test_body_setter_plain_iterable(self): lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text("a")])) lbox.body = [urwid.Text("x"), urwid.Text("y")] self.assertIsInstance(lbox.body, urwid.SimpleListWalker) self.assertEqual(2, len(lbox.body)) def test_length_hint_sized(self): lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text(str(i)) for i in range(3)])) self.assertEqual(3, lbox.__length_hint__()) def test_length_hint_not_sized(self): class TestWalker(urwid.ListWalker): @property def contents(self): return self @staticmethod def next_position(position: int) -> tuple[urwid.Text, int]: return urwid.Text(str(position)), position @staticmethod def prev_position(position: int) -> tuple[urwid.Text, int]: return urwid.Text(str(position)), position lbox = urwid.ListBox(TestWalker()) with self.assertRaises(AttributeError): lbox.__length_hint__ class ListBoxScrollProtocolTest(unittest.TestCase): def test_check_support_scrolling_missing_methods(self): class NoGetPrevWalker: def __init__(self, widgets): self._widgets = widgets self._focus = 0 def get_focus(self): return self._widgets[self._focus], self._focus def get_next(self, position): return None, None def set_focus(self, position): self._focus = position with self.assertWarns(DeprecationWarning): lbox = urwid.ListBox(NoGetPrevWalker([urwid.Text("a")])) with self.assertRaises(urwid.ListBoxError): lbox.rows_max() def test_check_support_scrolling_not_sized(self): with self.assertWarns(DeprecationWarning): lbox = urwid.ListBox(DuckTypedWalker([urwid.Text("a")])) with self.assertRaises(urwid.ListBoxError): lbox.get_scrollpos() def test_check_support_scrolling_wrap_around(self): lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text("a"), urwid.Text("b")], wrap_around=True)) with self.assertRaises(urwid.ListBoxError): lbox.get_first_visible_pos((4, 5)) def test_scroll_protocol_empty_body(self): lbox = urwid.ListBox(urwid.SimpleListWalker([])) self.assertEqual(0, lbox.get_scrollpos((4, 5))) self.assertEqual(0, lbox.get_first_visible_pos((4, 5))) self.assertEqual(1, lbox.get_visible_amount((4, 5))) def test_scroll_protocol_focus_at_top(self): # focus at position 0 means nothing is above it, so top.fill is empty # and get_scrollpos()/get_first_visible_pos() fall back to focus_pos. items = [urwid.Text(str(i)) for i in range(20)] lbox = urwid.ListBox(urwid.SimpleListWalker(items)) size = (4, 5) self.assertEqual(0, lbox.get_scrollpos(size)) self.assertEqual(0, lbox.get_first_visible_pos(size)) def test_rows_max_no_focus(self): lbox = urwid.ListBox(urwid.SimpleListWalker([])) self.assertEqual(0, lbox.rows_max((4, 5))) def test_scroll_protocol(self): items = [urwid.Text(str(i)) for i in range(20)] lbox = urwid.ListBox(urwid.SimpleListWalker(items)) lbox.set_focus(10) size = (4, 5) pos = lbox.get_scrollpos(size) self.assertIsInstance(pos, int) rows = lbox.rows_max(size) self.assertIsInstance(rows, int) # second call without size hits the cached branch self.assertEqual(rows, lbox.rows_max()) first_visible = lbox.get_first_visible_pos(size) self.assertIsInstance(first_visible, int) visible_amount = lbox.get_visible_amount(size) self.assertGreaterEqual(visible_amount, 1) self.assertFalse(lbox.require_relative_scroll((4, 10))) big_lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text(str(i)) for i in range(100)])) self.assertTrue(big_lbox.require_relative_scroll((4, 2))) class ListBoxRenderEmptyTest(unittest.TestCase): def test_render_empty_listbox(self): lbox = urwid.ListBox(urwid.SimpleListWalker([])) canvas = lbox.render((4, 5)) self.assertEqual([b" "] * 5, canvas.text) def test_render_row_count_mismatch(self): class BadRowsText(urwid.Text): def rows(self, size, focus=False): return super().rows(size, focus) + 1 lbox = urwid.ListBox(urwid.SimpleListWalker([BadRowsText("a"), urwid.Text("b")])) with self.assertRaises(urwid.ListBoxError): lbox.render((4, 5), focus=True) def test_render_fill_above_row_count_mismatch(self): class BadRowsText(urwid.Text): def rows(self, size, focus=False): return super().rows(size, focus) + 1 lbox = urwid.ListBox(urwid.SimpleListWalker([BadRowsText("a"), urwid.Text("b")])) lbox.set_focus(1) lbox.shift_focus((4, 10), 1) with self.assertRaises(urwid.ListBoxError): lbox.render((4, 5), focus=True) def test_render_fill_below_row_count_mismatch(self): class BadRowsText(urwid.Text): def rows(self, size, focus=False): return super().rows(size, focus) + 1 lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text("a"), BadRowsText("b")])) with self.assertRaises(urwid.ListBoxError): lbox.render((4, 5), focus=True) def test_render_cursor_mismatch(self): class LyingCursorEdit(urwid.Edit): def render(self, size, focus=False): canvas = super().render(size, focus) if focus: canvas = urwid.CompositeCanvas(canvas) canvas.cursor = (0, 0) return canvas e = LyingCursorEdit("", "ab") lbox = urwid.ListBox(urwid.SimpleListWalker([e])) with self.assertRaises(urwid.ListBoxError): lbox.render((4, 5), focus=True) def test_render_too_short_extra_widget(self): class FlakyNextWalker(urwid.ListWalker): """A ListWalker that claims there is nothing below the focus while calculate_visible() is filling the view. It then reveals a real, non-empty widget when render() double-checks afterwards -- this simulates a buggy/inconsistent ListWalker implementation. """ def __init__(self, widgets): self._widgets = widgets self.focus = 0 self._calls = 0 def get_focus(self): return self._widgets[self.focus], self.focus def get_next(self, position): self._calls += 1 if self._calls <= 2: return None, None pos = position + 1 if pos >= len(self._widgets): return None, None return self._widgets[pos], pos def get_prev(self, position): if position <= 0: return None, None return self._widgets[position - 1], position - 1 def set_focus(self, position): self.focus = position lbox = urwid.ListBox(FlakyNextWalker([urwid.Text("a"), urwid.Text("b")])) with self.assertRaises(urwid.ListBoxError): lbox.render((4, 5), focus=True) def test_render_infinite_next_position_guard(self): class LoopingWalker(urwid.ListWalker): """Like FlakyNextWalker above, it claims there is nothing below the focus while calculate_visible() fills the view. On every subsequent call it keeps reporting the very same zero-row widget/position pair -- simulating a buggy next_position() that never advances. """ def __init__(self, widgets): self._widgets = widgets self.focus = 0 self._calls = 0 def get_focus(self): return self._widgets[self.focus], self.focus def get_next(self, position): self._calls += 1 if self._calls <= 2: return None, None return self._widgets[-1], len(self._widgets) - 1 def get_prev(self, position): if position <= 0: return None, None return self._widgets[position - 1], position - 1 def set_focus(self, position): self.focus = position zero_rows = urwid.Pile([]) zero_rows.selectable = lambda: False lbox = urwid.ListBox(LoopingWalker([urwid.Text("a"), zero_rows])) with self.assertRaises(urwid.ListBoxError): lbox.render((4, 5), focus=True) class ListBoxCalculateVisibleEdgeTest(unittest.TestCase): def test_calculate_visible_empty(self): lbox = urwid.ListBox(urwid.SimpleListWalker([])) self.assertEqual((None, None, None), lbox.calculate_visible((4, 5))) def test_calculate_visible_skips_zero_height_above(self): body = urwid.SimpleListWalker([urwid.Pile([]), urwid.Text("a")]) lbox = urwid.ListBox(body) lbox.body.set_focus(1) lbox.offset_rows = 1 lbox.inset_fraction = (0, 1) middle, top, bottom = lbox.calculate_visible((4, 5)) self.assertIsNotNone(middle) def test_calculate_visible_fill_from_top_partial(self): text = "\n".join(str(i) for i in range(8)) lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text(text)])) lbox.set_focus_pending = None lbox.shift_focus((4, 10), -6) canvas = lbox.render((4, 5), focus=True) self.assertEqual(5, canvas.rows()) class ListBoxGetCursorCoordsTest(unittest.TestCase): def test_empty(self): lbox = urwid.ListBox(urwid.SimpleListWalker([])) self.assertIsNone(lbox.get_cursor_coords((4, 5))) def test_no_cursor(self): lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text("a")])) self.assertIsNone(lbox.get_cursor_coords((4, 5))) def test_with_cursor(self): e = urwid.Edit("", "ab") lbox = urwid.ListBox(urwid.SimpleListWalker([e])) self.assertEqual((2, 0), lbox.get_cursor_coords((4, 5))) def test_cursor_kept_in_view_by_offset_adjustment(self): # calculate_visible() actively re-adjusts the offset/inset so a visible # cursor always lands within the rendered rows; get_cursor_coords()'s own # out-of-range guard (`y < 0 or y >= maxrow`) is therefore not reachable # through this path and is left uncovered as a defensive check. e = urwid.Edit("", "ab") lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text("\n\n\n\n\n"), e])) lbox.set_focus(1) lbox.shift_focus((4, 20), 10) x, y = lbox.get_cursor_coords((4, 5)) self.assertTrue(0 <= y < 5) class ListBoxSetFocusErrorsTest(unittest.TestCase): def test_invalid_coming_from(self): lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text("a")])) with self.assertRaises(urwid.ListBoxError): lbox.set_focus(0, "sideways") def test_no_set_focus_method(self): class NoSetFocusWalker(urwid.ListWalker): def get_focus(self): return urwid.Text("a"), 0 lbox = urwid.ListBox(NoSetFocusWalker()) with self.assertRaises(TypeError): lbox.set_focus(0) def test_empty_listbox(self): class EmptyWalker(urwid.ListWalker): def get_focus(self): return None, None def set_focus(self, position): pass lbox = urwid.ListBox(EmptyWalker()) with self.assertRaises(IndexError): lbox.set_focus(0) def test_focus_position_empty(self): lbox = urwid.ListBox(urwid.SimpleListWalker([])) with self.assertRaises(IndexError): _ = lbox.focus_position class ListBoxContentsTest(unittest.TestCase): def test_getitem_len_repr(self): items = [urwid.Text(str(i)) for i in range(3)] lbox = urwid.ListBox(urwid.SimpleListWalker(items)) contents = lbox.contents self.assertEqual(3, len(contents)) widget, options = contents[1] self.assertIs(items[1], widget) self.assertIsNone(options) self.assertIn("ListBoxContents", repr(contents)) with self.assertRaises(KeyError): contents[100] def test_getitem_v1_protocol(self): widgets = [urwid.Text(str(i)) for i in range(3)] lbox = urwid.ListBox(V1ProtocolWalker(widgets)) contents = lbox.contents widget = contents[1] self.assertIs(widgets[1], widget) self.assertEqual(0, lbox.body.focus) with self.assertRaises(KeyError): contents[100] def test_getitem_no_set_focus(self): class NoGetItemNoSetFocus(urwid.ListWalker): def get_focus(self): return urwid.Text("a"), 0 lbox = urwid.ListBox(NoGetItemNoSetFocus()) with self.assertRaises(TypeError): _ = lbox.contents[0] def test_options(self): lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text("a")])) self.assertIsNone(lbox.options()) class ListBoxSetFocusValignTest(unittest.TestCase): def test_set_focus_valign_middle(self): lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text(str(i)) for i in range(5)])) lbox.set_focus_valign("middle") lbox.render((4, 5), focus=True) self.assertIsInstance(lbox.offset_rows, int) def test_set_focus_valign_empty(self): walker = urwid.SimpleListWalker([urwid.Text("a")]) lbox = urwid.ListBox(walker) lbox.render((4, 5), focus=True) # resolve the initial "first selectable" pending walker.clear() lbox.set_focus_valign("middle") lbox.render((4, 5), focus=True) # must not raise; body is now empty class ListBoxFirstSelectableTest(unittest.TestCase): def test_focus_already_selectable(self): lbox = urwid.ListBox(urwid.SimpleListWalker([SelectableText("a"), urwid.Text("b")])) lbox.render((4, 5), focus=True) self.assertEqual(0, lbox.focus_position) def test_picks_later_selectable_widget(self): lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text("a"), SelectableText("b"), urwid.Text("c")])) lbox.render((4, 5), focus=True) self.assertEqual(1, lbox.focus_position) def test_no_set_focus_method(self): class NoSetFocusWalker(urwid.ListWalker): def __init__(self, widgets): self._widgets = widgets def get_focus(self): return self._widgets[0], 0 def get_next(self, position): pos = position + 1 if pos >= len(self._widgets): return None, None return self._widgets[pos], pos def get_prev(self, position): return None, None lbox = urwid.ListBox(NoSetFocusWalker([urwid.Text("a"), urwid.Text("b")])) with self.assertRaises(TypeError): lbox.render((4, 5), focus=True) class ListBoxSetFocusCompleteTest(unittest.TestCase): def test_type_error_when_body_loses_set_focus(self): class FlakyWalker(urwid.ListWalker): def __init__(self, widgets): self._widgets = widgets self.focus = 0 self.set_focus = self._do_set_focus def get_focus(self): return self._widgets[self.focus], self.focus def _do_set_focus(self, position): self.focus = position self._modified() walker = FlakyWalker([urwid.Text("a"), urwid.Text("b"), urwid.Text("c")]) lbox = urwid.ListBox(walker) lbox.set_focus(1) del walker.set_focus with self.assertRaises(TypeError): lbox.render((4, 5), focus=True) def test_middle_none_after_restore(self): class ShrinkingWalker(urwid.ListWalker): def __init__(self, widgets): self._widgets = widgets self.focus = 0 def get_focus(self): if 0 <= self.focus < len(self._widgets): return self._widgets[self.focus], self.focus return None, None def set_focus(self, position): self.focus = position self._modified() walker = ShrinkingWalker([urwid.Text("a"), urwid.Text("b"), urwid.Text("c")]) lbox = urwid.ListBox(walker) lbox.set_focus(2) walker._widgets = [] lbox.render((4, 5), focus=True) # must not raise def test_body_emptied_before_render(self) -> None: """A focus change pending on a body emptied before the render leaves a blank canvas.""" walker = urwid.SimpleListWalker([SelectableText("a"), SelectableText("b")]) lbox = urwid.ListBox(walker) lbox.render((4, 5), focus=True) lbox.set_focus(1) walker.clear() canvas = lbox.render((4, 5), focus=True) self.assertEqual([b" "] * 5, canvas.text) def test_body_truncated_before_render(self) -> None: """A focus change pending on a body truncated past the old focus places the new focus by ``coming_from``. The result is the one the same change gives on a body that was not truncated. """ for walker_class in (urwid.SimpleListWalker, urwid.SimpleFocusListWalker): with self.subTest(walker=walker_class.__name__): walker = walker_class([SelectableText(str(i)) for i in range(20)]) lbox = urwid.ListBox(walker) lbox.set_focus(15, coming_from="above") lbox.render((4, 5), focus=True) lbox.set_focus(5, coming_from="below") del walker[10:] canvas = lbox.render((4, 5), focus=True) self.assertEqual(5, lbox.focus_position) self.assertEqual([b"5 ", b"6 ", b"7 ", b"8 ", b"9 "], canvas.text) def test_modified_callback_error_while_restoring_propagates(self) -> None: """An error a "modified" signal callback raises while the old focus is restored is not swallowed.""" walker = urwid.SimpleListWalker([SelectableText(str(i)) for i in range(3)]) lbox = urwid.ListBox(walker) lbox.render((4, 5), focus=True) lbox.set_focus(2) def reject_first_row() -> None: if walker.focus == 0: raise KeyError(0) urwid.connect_signal(walker, "modified", reject_first_row) with self.assertRaises(KeyError): lbox.render((4, 5), focus=True) def test_coming_from_scroll_down_then_up(self): items = [urwid.Text(str(i)) for i in range(20)] lbox = urwid.ListBox(urwid.SimpleListWalker(items)) lbox.set_focus(10) lbox.render((4, 5), focus=True) lbox.set_focus(8, "above") lbox.render((4, 5), focus=True) self.assertEqual(8, lbox.focus_position) def test_coming_from_scroll_visible_below(self): items = [urwid.Text(str(i)) for i in range(20)] lbox = urwid.ListBox(urwid.SimpleListWalker(items)) lbox.render((4, 5), focus=True) lbox.set_focus(3, "below") lbox.render((4, 5), focus=True) self.assertEqual(3, lbox.focus_position) def test_far_jump_no_coming_from(self): items = [urwid.Text(str(i)) for i in range(50)] lbox = urwid.ListBox(urwid.SimpleListWalker(items)) lbox.render((4, 5), focus=True) lbox.set_focus(40) lbox.render((4, 5), focus=True) self.assertEqual(40, lbox.focus_position) def test_far_jump_coming_from_above(self): items = [urwid.Text(str(i)) for i in range(50)] lbox = urwid.ListBox(urwid.SimpleListWalker(items)) lbox.render((4, 5), focus=True) lbox.set_focus(40, "above") lbox.render((4, 5), focus=True) self.assertEqual(40, lbox.focus_position) def test_far_jump_coming_from_below(self): items = [urwid.Text(str(i)) for i in range(50)] lbox = urwid.ListBox(urwid.SimpleListWalker(items)) lbox.render((4, 5), focus=True) lbox.set_focus(40, "below") lbox.render((4, 5), focus=True) self.assertEqual(40, lbox.focus_position) class ListBoxShiftFocusErrorsTest(unittest.TestCase): def test_offset_too_large(self): lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text("a")])) with self.assertRaises(urwid.ListBoxError): lbox.shift_focus((4, 5), 5) def test_negative_offset_too_large(self): lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text("a")])) with self.assertRaises(urwid.ListBoxError): lbox.shift_focus((4, 5), -1) class ListBoxUpdatePrefColTest(unittest.TestCase): def test_empty_listbox(self): lbox = urwid.ListBox(urwid.SimpleListWalker([])) lbox.update_pref_col_from_focus((4, 5)) self.assertEqual("left", lbox.pref_col) def test_from_cursor_coords_only(self): class CoordsOnlyWidget(urwid.Text): def get_cursor_coords(self, size): return (2, 0) lbox = urwid.ListBox(urwid.SimpleListWalker([CoordsOnlyWidget("hi")])) lbox.update_pref_col_from_focus((4, 5)) self.assertEqual(2, lbox.pref_col) class ListBoxChangeFocusErrorsTest(unittest.TestCase): def test_no_set_focus(self): class NoSetFocus(urwid.ListWalker): def get_focus(self): return urwid.Text("a"), 0 lbox = urwid.ListBox(NoSetFocus()) with self.assertRaises(TypeError): lbox.change_focus((4, 5), 0) def test_invalid_offset_inset(self): lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text("a"), urwid.Text("b")])) with self.assertRaises(urwid.ListBoxError): lbox.change_focus((4, 5), 1, offset_inset=-5) def test_cursor_row_unspecified_no_coming_from(self): e = urwid.Edit("", "hello") lbox = urwid.ListBox(urwid.SimpleListWalker([e])) with self.assertRaises(ValueError): lbox.change_focus((4, 5), 0, cursor_coords=(2,)) def test_cursor_row_out_of_range(self): e = urwid.Edit("", "hello") lbox = urwid.ListBox(urwid.SimpleListWalker([e])) with self.assertRaises(urwid.ListBoxError): lbox.change_focus((4, 5), 0, cursor_coords=(2, 99)) def test_cursor_coords_no_coming_from(self): e = urwid.Edit("", "hello") lbox = urwid.ListBox(urwid.SimpleListWalker([e])) lbox.change_focus((4, 5), 0, cursor_coords=(2, 0)) self.assertEqual((2, 0), e.get_cursor_coords((4,))) class ListBoxGetFocusOffsetInsetErrorsTest(unittest.TestCase): def test_invalid_inset_fraction(self): lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text("a")])) lbox.offset_rows = 0 lbox.inset_fraction = (5, 3) with self.assertRaises(urwid.ListBoxError): lbox.get_focus_offset_inset((4, 5)) class ListBoxMakeCursorVisibleTest(unittest.TestCase): def test_empty(self): lbox = urwid.ListBox(urwid.SimpleListWalker([])) lbox.make_cursor_visible((4, 5)) # must not raise def test_not_selectable(self): lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text("a")])) lbox.make_cursor_visible((4, 5)) # must not raise def test_no_get_cursor_coords(self): lbox = urwid.ListBox(urwid.SimpleListWalker([SelectableText("a")])) lbox.make_cursor_visible((4, 5)) # must not raise def test_cursor_none(self): class CursorNoneWidget(SelectableText): def get_cursor_coords(self, size): return None lbox = urwid.ListBox(urwid.SimpleListWalker([CursorNoneWidget("a")])) lbox.make_cursor_visible((4, 5)) # must not raise def test_already_visible(self): e = urwid.Edit("", "ab") lbox = urwid.ListBox(urwid.SimpleListWalker([e])) lbox.make_cursor_visible((4, 5)) self.assertEqual(0, lbox.offset_rows) class ListBoxKeypressEdgeTest(unittest.TestCase): def test_keypress_empty_listbox(self): lbox = urwid.ListBox(urwid.SimpleListWalker([])) self.assertEqual("up", lbox.keypress((4, 5), "up")) def test_keypress_unhandled_key(self): lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text("a")])) self.assertEqual("x", lbox.keypress((4, 5), "x")) def test_keypress_page_up_middle_none(self): class FlakyEmptyWalker(urwid.ListWalker): def __init__(self, widget): self._widget = widget self._calls = 0 def get_focus(self): self._calls += 1 if self._calls <= 3: return self._widget, 0 return None, None def get_next(self, position): return None, None def get_prev(self, position): return None, None def set_focus(self, position): pass lbox = urwid.ListBox(FlakyEmptyWalker(urwid.Text("a"))) self.assertEqual("page up", lbox.keypress((4, 5), "page up")) def test_keypress_page_down_middle_none(self): class FlakyEmptyWalker(urwid.ListWalker): def __init__(self, widget): self._widget = widget self._calls = 0 def get_focus(self): self._calls += 1 if self._calls <= 3: return self._widget, 0 return None, None def get_next(self, position): return None, None def get_prev(self, position): return None, None def set_focus(self, position): pass lbox = urwid.ListBox(FlakyEmptyWalker(urwid.Text("a"))) self.assertEqual("page down", lbox.keypress((4, 5), "page down")) def test_page_up_all_visible_selectable_at_top(self): body = [SelectableText("a"), SelectableText("b"), SelectableText("c")] lbox = urwid.ListBox(urwid.SimpleListWalker(body)) lbox.set_focus(2) lbox.keypress((4, 5), "page up") # must not raise def test_page_down_all_visible_selectable_at_bottom(self): body = [SelectableText("a"), SelectableText("b"), SelectableText("c")] lbox = urwid.ListBox(urwid.SimpleListWalker(body)) lbox.keypress((4, 5), "page down") # must not raise def test_page_up_zero_row_selectable(self): zero = urwid.Pile([]) zero.selectable = lambda: True body = [zero, urwid.Text("\n"), urwid.Text("\n"), SelectableText("\n\n")] lbox = urwid.ListBox(urwid.SimpleListWalker(body)) lbox.set_focus(3) lbox.shift_focus((4, 10), 3) lbox.keypress((4, 5), "page up") # must not raise def test_page_down_zero_row_selectable(self): zero = urwid.Pile([]) zero.selectable = lambda: True body = [SelectableText("\n\n"), urwid.Text("\n"), urwid.Text("\n"), zero] lbox = urwid.ListBox(urwid.SimpleListWalker(body)) lbox.shift_focus((4, 10), 0) lbox.keypress((4, 5), "page down") # must not raise def test_keypress_home_end(self): items = [urwid.Text(str(i)) for i in range(10)] lbox = urwid.ListBox(urwid.SimpleListWalker(items)) lbox.set_focus(5) lbox.keypress((4, 5), "home") self.assertEqual(0, lbox.focus_position) lbox.keypress((4, 5), "end") self.assertEqual(9, lbox.focus_position) def test_keypress_max_left_no_positions(self): walker, _root = make_tree_walker(3) lbox = urwid.ListBox(walker) with self.assertRaises(TypeError): lbox.keypress((20, 5), "home") def test_keypress_max_right_no_positions(self): walker, _root = make_tree_walker(3) lbox = urwid.ListBox(walker) with self.assertRaises(TypeError): lbox.keypress((20, 5), "end") def test_keypress_up_middle_none(self): class FlakyEmptyWalker(urwid.ListWalker): def __init__(self, widget): self._widget = widget self._calls = 0 def get_focus(self): self._calls += 1 if self._calls <= 3: return self._widget, 0 return None, None def get_next(self, position): return None, None def get_prev(self, position): return None, None def set_focus(self, position): pass lbox = urwid.ListBox(FlakyEmptyWalker(urwid.Text("a"))) self.assertEqual("up", lbox.keypress((4, 5), "up")) def test_keypress_down_middle_none(self): class FlakyEmptyWalker(urwid.ListWalker): def __init__(self, widget): self._widget = widget self._calls = 0 def get_focus(self): self._calls += 1 if self._calls <= 3: return self._widget, 0 return None, None def get_next(self, position): return None, None def get_prev(self, position): return None, None def set_focus(self, position): pass lbox = urwid.ListBox(FlakyEmptyWalker(urwid.Text("a"))) self.assertEqual("down", lbox.keypress((4, 5), "down")) class NoMouseEventWidget: """A minimal flow widget that does not implement Widget's mouse_event API.""" _sizing = frozenset([urwid.Sizing.FLOW]) def __init__(self, text="x"): self._text = text def selectable(self): return False def rows(self, size, focus=False): return 1 def render(self, size, focus=False): return urwid.Text(self._text).render(size, focus) class ListBoxMouseEventTest(unittest.TestCase): def test_mouse_event_empty(self): lbox = urwid.ListBox(urwid.SimpleListWalker([])) self.assertFalse(lbox.mouse_event((4, 5), "mouse press", 1, 0, 0, True)) def test_mouse_event_click_changes_focus(self): items = [SelectableText(str(i)) for i in range(5)] lbox = urwid.ListBox(urwid.SimpleListWalker(items)) lbox.mouse_event((4, 5), "mouse press", 1, 0, 2, True) self.assertEqual(2, lbox.focus_position) def test_mouse_event_no_row_match(self): items = [urwid.Text("a")] lbox = urwid.ListBox(urwid.SimpleListWalker(items)) result = lbox.mouse_event((4, 5), "mouse press", 1, 0, 4, True) self.assertFalse(result) def test_mouse_event_widget_without_mouse_event(self): w = NoMouseEventWidget() lbox = urwid.ListBox(urwid.SimpleListWalker([w])) with self.assertWarns(DeprecationWarning): result = lbox.mouse_event((4, 5), "mouse press", 1, 0, 0, True) self.assertFalse(result) def test_mouse_event_scroll_up(self): items = [urwid.Text(str(i)) for i in range(20)] lbox = urwid.ListBox(urwid.SimpleListWalker(items)) lbox.set_focus(10) lbox.render((4, 5), focus=True) handled = lbox.mouse_event((4, 5), "mouse press", 4, 0, 0, True) self.assertTrue(handled) def test_mouse_event_scroll_down(self): items = [urwid.Text(str(i)) for i in range(20)] lbox = urwid.ListBox(urwid.SimpleListWalker(items)) lbox.render((4, 5), focus=True) handled = lbox.mouse_event((4, 5), "mouse press", 5, 0, 0, True) self.assertTrue(handled) def test_mouse_event_release_no_scroll(self): items = [urwid.Text(str(i)) for i in range(5)] lbox = urwid.ListBox(urwid.SimpleListWalker(items)) result = lbox.mouse_event((4, 5), "mouse release", 1, 0, 0, True) self.assertFalse(result) def test_mouse_event_widget_handles_click(self): cb = urwid.CheckBox("opt") lbox = urwid.ListBox(urwid.SimpleListWalker([cb])) handled = lbox.mouse_event((10, 5), "mouse press", 1, 2, 0, True) self.assertTrue(handled) self.assertTrue(cb.state) class ListBoxEndsVisibleTest(unittest.TestCase): def test_ends_visible_empty(self): lbox = urwid.ListBox(urwid.SimpleListWalker([])) self.assertEqual(["top", "bottom"], lbox.ends_visible((4, 5))) def test_ends_visible_both(self): items = [urwid.Text(str(i)) for i in range(3)] lbox = urwid.ListBox(urwid.SimpleListWalker(items)) result = lbox.ends_visible((4, 5), focus=True) self.assertIn("top", result) self.assertIn("bottom", result) def test_ends_visible_top_only(self): items = [urwid.Text(str(i)) for i in range(20)] lbox = urwid.ListBox(urwid.SimpleListWalker(items)) result = lbox.ends_visible((4, 5), focus=True) self.assertIn("top", result) self.assertNotIn("bottom", result) def test_ends_visible_bottom_only(self): items = [urwid.Text(str(i)) for i in range(20)] lbox = urwid.ListBox(urwid.SimpleListWalker(items)) lbox.set_focus(19) lbox.render((4, 5), focus=True) result = lbox.ends_visible((4, 5), focus=True) self.assertIn("bottom", result) self.assertNotIn("top", result) def test_ends_visible_neither(self): items = [urwid.Text(str(i)) for i in range(20)] lbox = urwid.ListBox(urwid.SimpleListWalker(items)) lbox.set_focus(10) lbox.render((4, 5), focus=True) result = lbox.ends_visible((4, 5), focus=True) self.assertEqual([], result) def test_ends_visible_trim_bottom_nonzero(self): lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text("1\n2\n3\n4\n5\n6")])) result = lbox.ends_visible((4, 5), focus=True) self.assertNotIn("bottom", result) def test_ends_visible_trim_top_nonzero(self): lbox = urwid.ListBox(urwid.SimpleListWalker([urwid.Text("1\n2\n3\n4\n5\n6")])) lbox.shift_focus((4, 10), -1) result = lbox.ends_visible((4, 5), focus=True) self.assertNotIn("top", result) class ListBoxIterTest(unittest.TestCase): def test_iter_with_positions(self): items = [urwid.Text(str(i)) for i in range(5)] lbox = urwid.ListBox(urwid.SimpleListWalker(items)) self.assertEqual(list(range(5)), list(iter(lbox))) def test_reversed_with_positions(self): items = [urwid.Text(str(i)) for i in range(5)] lbox = urwid.ListBox(urwid.SimpleListWalker(items)) self.assertEqual(list(range(4, -1, -1)), list(reversed(lbox))) def test_iter_without_positions(self): walker, root = make_tree_walker(4, focus_key=1) lbox = urwid.ListBox(walker) expected = [ root.get_child_node(1), root.get_child_node(2), root.get_child_node(3), root.get_child_node(0), root, ] self.assertEqual(expected, list(iter(lbox))) def test_reversed_without_positions(self): walker, root = make_tree_walker(4, focus_key=1) lbox = urwid.ListBox(walker) expected = [ root.get_child_node(0), root, root.get_child_node(1), root.get_child_node(2), root.get_child_node(3), ] self.assertEqual(expected, list(reversed(lbox))) def test_iter_empty_without_positions(self): lbox = urwid.ListBox(EmptyNoPositionsWalker()) self.assertEqual([], list(iter(lbox))) self.assertEqual([], list(reversed(lbox))) class ListBoxWrapAroundRenderTest(unittest.TestCase): """A wrapping walker must not repeat its contents to fill a tall listbox (#464).""" @staticmethod def _rows(walker, size: tuple[int, int]) -> list[str]: canvas = urwid.ListBox(walker).render(size, focus=True) return [line.decode().rstrip() for line in canvas.text] def test_short_list_is_shown_once(self) -> None: walker = urwid.SimpleListWalker([urwid.Text(t) for t in ("a", "b", "c")], wrap_around=True) self.assertEqual(["a", "b", "c", "", ""], self._rows(walker, (4, 5))) def test_short_focus_list_is_shown_once(self) -> None: walker = urwid.SimpleFocusListWalker([urwid.Text(t) for t in ("a", "b", "c")], wrap_around=True) self.assertEqual(["a", "b", "c", "", ""], self._rows(walker, (4, 5))) def test_focus_in_middle_is_shown_once(self) -> None: walker = urwid.SimpleListWalker([urwid.Text(t) for t in ("a", "b", "c")], wrap_around=True) walker.set_focus(1) self.assertEqual(["b", "c", "a", "", ""], self._rows(walker, (4, 5))) def test_long_list_still_fills_the_screen(self) -> None: walker = urwid.SimpleListWalker([urwid.Text(t) for t in "abcdefgh"], wrap_around=True) self.assertEqual(["a", "b", "c"], self._rows(walker, (4, 3))) def test_keyboard_focus_still_wraps(self) -> None: walker = urwid.SimpleFocusListWalker([urwid.Button(t) for t in ("a", "b", "c")], wrap_around=True) listbox = urwid.ListBox(walker) listbox.render((8, 5), focus=True) listbox.keypress((8, 5), "up") self.assertEqual(2, walker.focus) self.assertEqual(3, sum(bool(line.strip()) for line in listbox.render((8, 5), focus=True).text)) listbox.keypress((8, 5), "down") self.assertEqual(0, walker.focus) def test_zero_height_items_do_not_loop(self) -> None: walker = urwid.SimpleListWalker([urwid.Pile([]), urwid.Text("a")], wrap_around=True) self.assertEqual(["a", "", "", "", ""], self._rows(walker, (4, 5))) def test_collecting_above_focus_does_not_repeat_items(self) -> None: walker = urwid.SimpleListWalker([urwid.Text(t) for t in ("a", "b", "c")], wrap_around=True) listbox = urwid.ListBox(walker) listbox.render((4, 5), focus=True) listbox.shift_focus((4, 5), 4) rows = [line.decode().rstrip() for line in listbox.render((4, 5), focus=True).text] self.assertEqual(["b", "c", "a", "", ""], rows) class CountingPosition: """Walker position that counts equality comparisons made against it.""" eq_calls: typing.ClassVar[int] = 0 def __init__(self, index: int) -> None: """Create the position at *index* in the walker.""" self.index = index def __hash__(self) -> int: """Hash by index, so distinct positions do not collide in a set.""" return hash(self.index) def __eq__(self, other: object) -> bool: """Compare by index and count the call.""" CountingPosition.eq_calls += 1 return isinstance(other, CountingPosition) and self.index == other.index class CappedWrappingWalker(urwid.ListWalker): """Wrapping ListWalker without positions() that raises instead of letting a caller walk it forever.""" def __init__( self, widgets: list[urwid.Widget], positions: list[object] | None = None, max_steps: int = 1000, ) -> None: """Wrap *widgets*, keyed by *positions* (indexes by default), allowing *max_steps* moves in total. Positions are looked up by identity, so they need not be hashable and are never compared here. """ self._widgets: list[urwid.Widget] = widgets self._positions: list[object] = positions if positions is not None else list(range(len(widgets))) self._index: dict[int, int] = {id(pos): i for i, pos in enumerate(self._positions)} self._focus: int = 0 self._steps_left: int = max_steps def _step(self, position: object, delta: int) -> tuple[urwid.Widget, object]: self._steps_left -= 1 if self._steps_left < 0: raise AssertionError("ListBox walked the wrapping body without end") i = (self._index[id(position)] + delta) % len(self._widgets) return self._widgets[i], self._positions[i] def get_focus(self) -> tuple[urwid.Widget, object]: """Return the focus widget and its position.""" return self._widgets[self._focus], self._positions[self._focus] def get_next(self, position: object) -> tuple[urwid.Widget, object]: """Return the widget after *position*, wrapping to the first.""" return self._step(position, 1) def get_prev(self, position: object) -> tuple[urwid.Widget, object]: """Return the widget before *position*, wrapping to the last.""" return self._step(position, -1) def set_focus(self, position: object) -> None: """Move the focus to *position*.""" self._focus = self._index[id(position)] self._modified() @dataclasses.dataclass class UnhashablePosition: """Walker position that supports ``==`` only, as a non-frozen dataclass does.""" index: int def make_positions(count: int, kind: str) -> list[object]: """Return *count* walker positions of *kind*: ``hashable``, ``unhashable``, or ``mixed`` (hashable first).""" if kind == "hashable": return list(range(count)) if kind == "unhashable": return [UnhashablePosition(i) for i in range(count)] return [UnhashablePosition(i) if i % 2 else i for i in range(count)] class ListBoxWrapAroundZeroRowsTest(unittest.TestCase): """Walking a wrapping body of 0-height widgets ends.""" size = (4, 5) kinds = ("hashable", "unhashable") def _listbox(self, kind: str) -> urwid.ListBox: return urwid.ListBox(CappedWrappingWalker([urwid.Pile([]) for _ in range(5)], make_positions(5, kind))) def test_keys_do_not_loop(self) -> None: """Scrolling keys stop once a cycle of the body adds no rows, and keep the focus.""" for kind in self.kinds: for key, expected in (("up", "up"), ("down", "down"), ("page up", None), ("page down", None)): with self.subTest(kind=kind, key=key): listbox = self._listbox(kind) focus = listbox.focus_position self.assertEqual(expected, listbox.keypress(self.size, key)) self.assertEqual(focus, listbox.focus_position) self.assertEqual([b" "] * 5, listbox.render(self.size, focus=True).text) def test_page_keys_keep_the_only_row(self) -> None: """Page keys keep the focus on the one widget with rows instead of moving it to a 0-height one.""" for kind in self.kinds: for shape, focus_index in (("TZZ", 0), ("ZZT", 2)): for key in ("page up", "page down"): with self.subTest(kind=kind, shape=shape, key=key): widgets = [urwid.Text("t") if c == "T" else urwid.Pile([]) for c in shape] positions = make_positions(len(widgets), kind) walker = CappedWrappingWalker(widgets, positions) walker.set_focus(positions[focus_index]) listbox = urwid.ListBox(walker) self.assertIsNone(listbox.keypress(self.size, key)) self.assertEqual(positions[focus_index], listbox.focus_position) self.assertEqual([b"t ", *[b" "] * 4], listbox.render(self.size, focus=True).text) def test_calculate_visible_is_linear(self) -> None: """Checking for repeated positions does not compare each one against every earlier one.""" count = 300 positions = [CountingPosition(i) for i in range(count + 1)] walker = CappedWrappingWalker( [*(urwid.Pile([]) for _ in range(count)), urwid.Text("a")], positions, max_steps=10 * count, ) walker.set_focus(positions[-1]) listbox = urwid.ListBox(walker) listbox.shift_focus(self.size, 4) CountingPosition.eq_calls = 0 listbox.calculate_visible(self.size) self.assertLess(CountingPosition.eq_calls, count) class ListBoxUnhashablePositionsTest(unittest.TestCase): """Positions that only support ``==`` work wherever hashable ones do.""" size = (4, 5) keys = ("down", "down", "page down", "up", "page up", "up", "up", "page up", "page down") def _trace(self, texts: str, kind: str) -> list[list[bytes]]: walker = CappedWrappingWalker([SelectableText(t) for t in texts], make_positions(len(texts), kind)) listbox = urwid.ListBox(walker) trace = [listbox.render(self.size, focus=True).text] for key in self.keys: listbox.keypress(self.size, key) trace.append(listbox.render(self.size, focus=True).text) return trace def test_same_as_integer_positions(self) -> None: """Rendering and scrolling give the same rows as with integer positions, for tall and short bodies.""" for texts in ("abcdefgh", "ab"): expected = self._trace(texts, "hashable") for kind in ("unhashable", "mixed"): with self.subTest(texts=texts, kind=kind): self.assertEqual(expected, self._trace(texts, kind)) class ListBoxBodySignalTest(unittest.TestCase): """A ListBox does not keep itself alive through its body's "modified" signal.""" def test_dropped_listboxes_are_collected(self) -> None: """A ListBox over a long-lived walker is collected, and its handler removed with it.""" walker = urwid.SimpleFocusListWalker([urwid.Text("a")]) refs = [weakref.ref(urwid.ListBox(walker)) for _ in range(10)] self.assertEqual(0, collect_and_count_alive(refs)) self.assertEqual((), vars(walker)["_urwid_signals"]["modified"]) def test_body_replacement_disconnects(self) -> None: """Replacing the body disconnects the handler from the old one.""" old = urwid.SimpleFocusListWalker([urwid.Text("a")]) new = urwid.SimpleFocusListWalker([urwid.Text("b")]) listbox = urwid.ListBox(old) listbox.body = new self.assertEqual((), vars(old)["_urwid_signals"]["modified"]) self.assertEqual(1, len(vars(new)["_urwid_signals"]["modified"])) def test_live_listbox_is_invalidated(self) -> None: """A change to the walker invalidates the cached canvas of a live ListBox.""" walker = urwid.SimpleFocusListWalker([urwid.Text("a")]) listbox = urwid.ListBox(walker) self.assertEqual([b"a ", b" "], listbox.render((4, 2)).text) walker.append(urwid.Text("b")) self.assertEqual([b"a ", b"b "], listbox.render((4, 2)).text) urwid-urwid-69907e4/tests/widgets/test_overlay.py000066400000000000000000000732241526073734300222110ustar00rootroot00000000000000from __future__ import annotations import unittest import warnings import urwid from urwid.widget import overlay as overlay_mod class NotAWidget: """A top widget deliberately missing the :class:`urwid.AbstractWidget` protocol methods. See :meth:`Pile._check_widget_subclass` (`tests/widgets/test_pile.py`) for the same pattern. """ class FixedCursorSolidFill(urwid.SolidFill): """A box widget reporting a cursor row far beyond any size it will ever be rendered at.""" def get_cursor_coords(self, size: tuple[int, int]) -> tuple[int, int]: return 0, 100 class ZeroHeightFixed(urwid.Widget): """A FIXED widget whose `pack()` reports zero rows.""" _sizing = frozenset((urwid.FIXED,)) def pack(self, size: tuple[()] = (), focus: bool = False) -> tuple[int, int]: return 5, 0 def render(self, size: tuple[()] | tuple[int, int], focus: bool = False) -> urwid.Canvas: maxcol, maxrow = size or (5, 0) return urwid.SolidCanvas(" ", maxcol, maxrow) class OverlayTest(unittest.TestCase): def test_sizing_flow_fixed(self) -> None: top_w = urwid.Text("Flow and Fixed widget") bottom_w = urwid.SolidFill("#") widgets = {"top_w": top_w, "bottom_w": bottom_w} for description, kwargs, sizing in ( ( "Fixed render no size", { **widgets, "align": urwid.CENTER, "width": None, "valign": urwid.MIDDLE, "height": None, }, frozenset((urwid.BOX, urwid.FIXED)), ), ( "Fixed render + corners", { **widgets, "align": urwid.CENTER, "width": None, "valign": urwid.MIDDLE, "height": None, "left": 1, "right": 1, "top": 1, "bottom": 1, }, frozenset((urwid.BOX, urwid.FIXED)), ), ( "Fixed render from FLOW", { **widgets, "align": urwid.CENTER, "width": 10, "valign": urwid.MIDDLE, "height": None, }, frozenset((urwid.BOX, urwid.FLOW, urwid.FIXED)), ), ( "Fixed render from FLOW + corners", { **widgets, "align": urwid.CENTER, "width": 10, "valign": urwid.MIDDLE, "height": None, "left": 1, "right": 1, "top": 1, "bottom": 1, }, frozenset((urwid.BOX, urwid.FLOW, urwid.FIXED)), ), ): with self.subTest(description): widget = urwid.Overlay(**kwargs) self.assertEqual(sizing, widget.sizing()) def_cols, def_rows = top_w.pack() if kwargs["width"] is None: args_cols = def_cols else: args_cols = kwargs["width"] cols = args_cols + kwargs.get("left", 0) + kwargs.get("right", 0) rows = top_w.rows((args_cols,)) + kwargs.get("top", 0) + kwargs.get("bottom", 0) self.assertEqual((cols, rows), widget.pack(())) canvas = widget.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) with self.subTest("Fixed Relative"): min_width = 23 relative_width = 90 widget = urwid.Overlay( top_w, bottom_w, align=urwid.CENTER, width=(urwid.RELATIVE, relative_width), valign=urwid.MIDDLE, height=None, min_width=23, ) self.assertEqual(frozenset((urwid.BOX, urwid.FLOW, urwid.FIXED)), widget.sizing()) cols = int(min_width * 100 / relative_width + 0.5) rows = top_w.rows((min_width,)) self.assertEqual((cols, rows), widget.pack(())) canvas = widget.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual("#Flow and Fixed widget ##", str(canvas)) with self.subTest("Flow Relative"): cols = 25 min_width = 23 relative_width = 90 widget = urwid.Overlay( top_w, bottom_w, align=urwid.CENTER, width=(urwid.RELATIVE, relative_width), valign=urwid.MIDDLE, height=None, min_width=23, ) self.assertEqual(frozenset((urwid.BOX, urwid.FLOW, urwid.FIXED)), widget.sizing()) rows = top_w.rows((min_width,)) self.assertEqual((cols, rows), widget.pack((cols,))) canvas = widget.render((cols,)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual("#Flow and Fixed widget #", str(canvas)) with self.subTest("Fixed Relative + corners"): min_width = 23 relative_width = 90 widget = urwid.Overlay( top_w, bottom_w, align=urwid.CENTER, width=(urwid.RELATIVE, relative_width), valign=urwid.MIDDLE, height=None, min_width=23, top=1, bottom=1, ) self.assertEqual(frozenset((urwid.BOX, urwid.FLOW, urwid.FIXED)), widget.sizing()) cols = int(min_width * 100 / relative_width + 0.5) rows = top_w.rows((min_width,)) + 2 self.assertEqual((cols, rows), widget.pack(())) canvas = widget.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( "##########################\n#Flow and Fixed widget ##\n##########################", str(canvas), ) with self.subTest("Fixed Relative + corners"): cols = 25 min_width = 23 relative_width = 90 widget = urwid.Overlay( top_w, bottom_w, align=urwid.CENTER, width=(urwid.RELATIVE, relative_width), valign=urwid.MIDDLE, height=None, min_width=23, top=1, bottom=1, ) self.assertEqual(frozenset((urwid.BOX, urwid.FLOW, urwid.FIXED)), widget.sizing()) rows = top_w.rows((min_width,)) + 2 self.assertEqual((cols, rows), widget.pack((cols,))) canvas = widget.render((cols,)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( "#########################\n#Flow and Fixed widget #\n#########################", str(canvas), ) def test_sizing_box_fixed_given(self): top_w = urwid.SolidFill("*") bottom_w = urwid.SolidFill("#") min_width = 5 min_height = 3 widget = urwid.Overlay( top_w, bottom_w, align=urwid.CENTER, width=min_width, valign=urwid.MIDDLE, height=min_height, top=1, bottom=1, left=2, right=2, ) self.assertEqual(frozenset((urwid.BOX, urwid.FLOW, urwid.FIXED)), widget.sizing()) cols = min_width + 4 rows = min_height + 2 for description, call_args in ( ("All GIVEN FIXED", ()), ("ALL GIVEN FLOW", (cols,)), ("ALL GIVEN BOX", (cols, rows)), ): with self.subTest(description): self.assertEqual((cols, rows), widget.pack(call_args)) canvas = widget.render(call_args) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [ "#########", "##*****##", "##*****##", "##*****##", "#########", ], [line.decode("utf-8") for line in canvas.text], ) def test_sizing_box_fixed_relative(self): top_w = urwid.SolidFill("*") bottom_w = urwid.SolidFill("#") relative_width = 50 relative_height = 50 min_width = 4 min_height = 2 widget = urwid.Overlay( top_w, bottom_w, align=urwid.CENTER, width=(urwid.RELATIVE, relative_width), valign=urwid.MIDDLE, height=(urwid.RELATIVE, relative_height), min_width=min_width, min_height=min_height, top=1, bottom=1, left=2, right=2, ) cols = int(min_width * 100 / relative_width + 0.5) rows = int(min_height * 100 / relative_height + 0.5) for description, call_args in ( ("All GIVEN FIXED", ()), ("ALL GIVEN FLOW", (cols,)), ("ALL GIVEN BOX", (cols, rows)), ): with self.subTest(description): self.assertEqual(frozenset((urwid.BOX, urwid.FLOW, urwid.FIXED)), widget.sizing()) self.assertEqual((cols, rows), widget.pack(call_args)) canvas = widget.render(call_args) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [ "########", "##****##", "##****##", "########", ], [line.decode("utf-8") for line in canvas.text], ) def test_relative(self): ovl = urwid.Overlay( urwid.Text("aaa"), urwid.SolidFill(urwid.SolidFill.Symbols.LITE_SHADE), width=urwid.PACK, height=urwid.PACK, align=(urwid.RELATIVE, 30), valign=(urwid.RELATIVE, 70), ) self.assertEqual( ovl.contents[1][1], (urwid.RELATIVE, 30, urwid.PACK, None, None, 0, 0, urwid.RELATIVE, 70, urwid.PACK, None, None, 0, 0), ) self.assertEqual( ( "░░░░░░░░░░░░░░░░░░░░", "░░░░░░░░░░░░░░░░░░░░", "░░░░░░░░░░░░░░░░░░░░", "░░░░░░░░░░░░░░░░░░░░", "░░░░░░░░░░░░░░░░░░░░", "░░░░░░░░░░░░░░░░░░░░", "░░░░░aaa░░░░░░░░░░░░", "░░░░░░░░░░░░░░░░░░░░", "░░░░░░░░░░░░░░░░░░░░", "░░░░░░░░░░░░░░░░░░░░", ), ovl.render((20, 10)).decoded_text, ) def test_pack_width_not_fitting_is_clipped(self): """An oversized PACK width is clipped into the available columns instead of raising. Regression test: negative padding used to be passed to the canvas overlay as a position, producing a canvas wider than the size requested. """ top_w = urwid.Text("Some long text") self.assertEqual((14, 1), top_w.pack(())) ovl = urwid.Overlay( top_w, urwid.SolidFill("#"), urwid.CENTER, urwid.PACK, urwid.MIDDLE, urwid.PACK, ) self.assertEqual((-2, -2, 1, 1), ovl.calculate_padding_filler((10, 3), False)) canvas = ovl.render((10, 3)) self.assertEqual(10, canvas.cols()) self.assertEqual(3, canvas.rows()) self.assertEqual(("##########", "me long te", "##########"), canvas.decoded_text) with self.subTest("clipped on both axes"): top_w = urwid.LineBox(urwid.Text("Some long text")) self.assertEqual((16, 3), top_w.pack(())) ovl = urwid.Overlay( top_w, urwid.SolidFill("#"), urwid.CENTER, urwid.PACK, urwid.MIDDLE, urwid.PACK, ) self.assertEqual((-3, -3, 0, -1), ovl.calculate_padding_filler((10, 2), False)) canvas = ovl.render((10, 2)) self.assertEqual(10, canvas.cols()) self.assertEqual(2, canvas.rows()) self.assertEqual(("──────────", "me long te"), canvas.decoded_text) def test_pack_height_measured_at_top_widget_width(self): """A PACK height is measured at the width top_w is given, not at the full width. Regression test for https://github.com/urwid/urwid/issues/471: the row count was measured at the full width of the overlay, so a wrapping flow widget was placed too low and mouse events below the wrapped text were dropped. """ check_box = urwid.CheckBox("x") top_w = urwid.Pile([urwid.Text("one two three"), check_box]) self.assertEqual(4, top_w.rows((5,))) self.assertEqual(2, top_w.rows((20,))) ovl = urwid.Overlay(top_w, urwid.SolidFill("#"), urwid.CENTER, 5, urwid.MIDDLE, urwid.PACK) self.assertEqual((7, 8, 3, 3), ovl.calculate_padding_filler((20, 10), False)) self.assertEqual( ( "####################", "####################", "####################", "#######one ########", "#######two ########", "#######three########", "#######[ ] x########", "####################", "####################", "####################", ), ovl.render((20, 10)).decoded_text, ) # the check box is rendered on the last row of top_w, so a click there must reach it self.assertTrue(ovl.mouse_event((20, 10), "mouse press", 1, 8, 6, True)) self.assertTrue(check_box.state) def test_old_params(self): o1 = urwid.Overlay( urwid.SolidFill("X"), urwid.SolidFill("O"), ("fixed left", 5), ("fixed right", 4), ("fixed top", 3), ("fixed bottom", 2), ) self.assertEqual( o1.contents[1][1], ("left", None, "relative", 100, None, 5, 4, "top", None, "relative", 100, None, 3, 2), ) o2 = urwid.Overlay( urwid.SolidFill("X"), urwid.SolidFill("O"), ("fixed right", 5), ("fixed left", 4), ("fixed bottom", 3), ("fixed top", 2), ) self.assertEqual( o2.contents[1][1], ("right", None, "relative", 100, None, 4, 5, "bottom", None, "relative", 100, None, 2, 3), ) def test_get_cursor_coords(self): self.assertEqual( urwid.Overlay( urwid.Filler(urwid.Edit()), urwid.SolidFill("B"), "right", 1, "bottom", 1, ).get_cursor_coords((2, 2)), (1, 1), ) def test_replaced_top_w_is_rendered(self): """Replacing the top widget invalidates the canvas rendered from the previous one. Regression test: `top_w` used to be a plain attribute, so the canvas cached for the overlay survived the replacement and the old widget kept being rendered until something else invalidated the overlay. The first canvas is kept referenced on purpose: :class:`urwid.CanvasCache` holds weak references, so a canvas dropped by the test takes the stale cache entry with it and the replacement would render correctly either way. """ ovl = urwid.Overlay( urwid.SolidFill("X"), urwid.SolidFill("."), urwid.CENTER, 4, urwid.MIDDLE, 2, ) cached = ovl.render((8, 4)) self.assertEqual(("........", "..XXXX..", "..XXXX..", "........"), cached.decoded_text) ovl.top_w = urwid.SolidFill("O") self.assertEqual(("........", "..OOOO..", "..OOOO..", "........"), ovl.render((8, 4)).decoded_text) def test_replaced_bottom_w_is_rendered(self): """Replacing the bottom widget invalidates the canvas rendered from the previous one. The counterpart of `test_replaced_top_w_is_rendered`, keeping the first canvas referenced for the same reason. """ ovl = urwid.Overlay( urwid.SolidFill("X"), urwid.SolidFill("."), urwid.CENTER, 4, urwid.MIDDLE, 2, ) cached = ovl.render((8, 4)) self.assertEqual(("........", "..XXXX..", "..XXXX..", "........"), cached.decoded_text) ovl.bottom_w = urwid.SolidFill(",") self.assertEqual((",,,,,,,,", ",,XXXX,,", ",,XXXX,,", ",,,,,,,,"), ovl.render((8, 4)).decoded_text) def test_length(self): ovl = urwid.Overlay( urwid.SolidFill("X"), urwid.SolidFill("O"), "center", ("relative", 20), "middle", ("relative", 20), ) self.assertEqual(2, len(ovl)) self.assertEqual(2, len(ovl.contents)) def test_common(self): s1 = urwid.SolidFill("1") s2 = urwid.SolidFill("2") o = urwid.Overlay(s1, s2, "center", ("relative", 50), "middle", ("relative", 50)) self.assertEqual(o.focus, s1) self.assertEqual(o.focus_position, 1) self.assertRaises(IndexError, lambda: setattr(o, "focus_position", None)) self.assertRaises(IndexError, lambda: setattr(o, "focus_position", 2)) self.assertEqual(o.contents[0], (s2, urwid.Overlay._DEFAULT_BOTTOM_OPTIONS)) self.assertEqual( o.contents[1], (s1, ("center", None, "relative", 50, None, 0, 0, "middle", None, "relative", 50, None, 0, 0)), ) def test_not_a_widget_warns_on_construction(self) -> None: """A top widget missing the Widget API triggers the same warning as `Frame`/`Pile`/`Columns`.""" with self.assertWarns(DeprecationWarning) as ctx: urwid.Overlay( NotAWidget(), urwid.SolidFill("#"), urwid.CENTER, (urwid.RELATIVE, 100), urwid.MIDDLE, (urwid.RELATIVE, 100), ) self.assertIn("is not implementing Widget API", str(ctx.warning)) def test_sizing_warning_pack_width_without_fixed_support(self) -> None: ovl = urwid.Overlay(urwid.SolidFill("*"), urwid.SolidFill("#"), urwid.CENTER, None, urwid.MIDDLE, None) with self.assertWarns(overlay_mod.OverlayWarning): self.assertEqual(frozenset((urwid.BOX,)), ovl.sizing()) def test_sizing_warning_pack_height_without_flow_support(self) -> None: ovl = urwid.Overlay(urwid.SolidFill("*"), urwid.SolidFill("#"), urwid.CENTER, 10, urwid.MIDDLE, None) with self.assertWarns(overlay_mod.OverlayWarning): self.assertEqual(frozenset((urwid.BOX,)), ovl.sizing()) def test_sizing_pack_height_flow_supported_without_fixed_amount(self) -> None: """PACK height with a FLOW-capable top widget adds FLOW, but not FIXED without a width amount.""" ovl = urwid.Overlay( urwid.Text("hi"), urwid.SolidFill("#"), urwid.CENTER, (urwid.RELATIVE, 50), urwid.MIDDLE, None, ) with warnings.catch_warnings(): warnings.simplefilter("error") self.assertEqual(frozenset((urwid.BOX, urwid.FLOW)), ovl.sizing()) def test_sizing_given_height_without_given_or_min_skips_flow(self) -> None: """A GIVEN height with no matching GIVEN width and no min_height resolves to BOX only.""" ovl = urwid.Overlay( urwid.SolidFill("*"), urwid.SolidFill("#"), urwid.CENTER, 10, urwid.MIDDLE, (urwid.RELATIVE, 50), ) with warnings.catch_warnings(): warnings.simplefilter("error") self.assertEqual(frozenset((urwid.BOX,)), ovl.sizing()) def test_sizing_box_height_amount_flow_without_fixed_amount(self) -> None: """A BOX-capable top widget with height amount adds FLOW, but not FIXED without a width amount.""" ovl = urwid.Overlay( urwid.SolidFill("*"), urwid.SolidFill("#"), urwid.CENTER, (urwid.RELATIVE, 50), urwid.MIDDLE, (urwid.RELATIVE, 50), min_height=5, ) with warnings.catch_warnings(): warnings.simplefilter("error") self.assertEqual(frozenset((urwid.BOX, urwid.FLOW)), ovl.sizing()) def test_sizing_warning_height_amount_without_box_support(self) -> None: ovl = urwid.Overlay( urwid.Text("hi"), urwid.SolidFill("#"), urwid.CENTER, 10, urwid.MIDDLE, (urwid.RELATIVE, 50), min_height=5, ) with self.assertWarns(overlay_mod.OverlayWarning): self.assertEqual(frozenset((urwid.BOX,)), ovl.sizing()) def test_pack_errors(self) -> None: top_w = urwid.SolidFill("*") bottom_w = urwid.SolidFill("#") for description, kwargs in ( ("width_amount falsy", {"width": 0, "height": 5}), ("relative width without min_width", {"width": (urwid.RELATIVE, 50), "height": 5}), ("height_amount falsy", {"width": 10, "height": 0}), ("relative height without min_height", {"width": 10, "height": (urwid.RELATIVE, 50)}), ): with self.subTest(description): ovl = urwid.Overlay(top_w, bottom_w, urwid.CENTER, kwargs["width"], urwid.MIDDLE, kwargs["height"]) self.assertRaises(overlay_mod.OverlayError, ovl.pack, ()) def test_rows_given_width_pack_height(self) -> None: top_w = urwid.Text("hi") ovl = urwid.Overlay(top_w, urwid.SolidFill("#"), urwid.CENTER, 10, urwid.MIDDLE, None) self.assertEqual(top_w.rows((10,)), ovl.rows((999,))) def test_rows_error_pack_width_and_height(self) -> None: ovl = urwid.Overlay(urwid.Text("hi"), urwid.SolidFill("#"), urwid.CENTER, None, urwid.MIDDLE, None) self.assertRaises(overlay_mod.OverlayError, ovl.rows, (10,)) def test_align_width_valign_height_properties(self) -> None: ovl = urwid.Overlay( urwid.SolidFill("*"), urwid.SolidFill("#"), align=urwid.CENTER, width=10, valign=urwid.MIDDLE, height=5, min_width=3, min_height=2, ) self.assertEqual(urwid.CENTER, ovl.align) self.assertEqual(10, ovl.width) self.assertEqual(urwid.MIDDLE, ovl.valign) self.assertEqual(5, ovl.height) def test_options_invalid_align_type(self) -> None: with self.assertRaises(ValueError): urwid.Overlay.options("bogus", None, "given", 5, "top", None, "given", 5) def test_options_invalid_valign_type(self) -> None: with self.assertRaises(ValueError): urwid.Overlay.options("left", None, "given", 5, "bogus", None, "given", 5) def test_set_overlay_parameters_invalid_valign(self) -> None: with self.assertRaises(overlay_mod.OverlayError): urwid.Overlay(urwid.SolidFill("*"), urwid.SolidFill("#"), urwid.CENTER, 10, 123, 5) def test_selectable_delegates_to_top_w(self) -> None: ovl = urwid.Overlay(urwid.Edit(), urwid.SolidFill("#"), urwid.CENTER, 10, urwid.MIDDLE, 1) self.assertTrue(ovl.selectable()) ovl = urwid.Overlay(urwid.SolidFill("*"), urwid.SolidFill("#"), urwid.CENTER, 10, urwid.MIDDLE, 1) self.assertFalse(ovl.selectable()) def test_keypress_delegates_to_top_w(self) -> None: edit = urwid.Edit() ovl = urwid.Overlay(edit, urwid.SolidFill("#"), urwid.CENTER, 10, urwid.MIDDLE, 1) self.assertIsNone(ovl.keypress((20, 5), "x")) self.assertEqual("x", edit.edit_text) def test_focus_position_set_to_one_is_noop(self) -> None: ovl = urwid.Overlay(urwid.SolidFill("*"), urwid.SolidFill("#"), urwid.CENTER, 10, urwid.MIDDLE, 5) ovl.focus_position = 1 self.assertEqual(1, ovl.focus_position) def test_contents_is_fixed_size(self) -> None: ovl = urwid.Overlay(urwid.SolidFill("*"), urwid.SolidFill("#"), urwid.CENTER, 10, urwid.MIDDLE, 5) with self.assertRaises(TypeError): del ovl.contents[0] with self.assertRaises(TypeError): ovl.contents.insert(0, (urwid.SolidFill("x"), ovl.contents[1][1])) self.assertEqual([ovl.contents[0], ovl.contents[1]], list(ovl.contents)) def test_contents_setter_replaces_bottom_and_top_options(self) -> None: top_w = urwid.SolidFill("1") ovl = urwid.Overlay(top_w, urwid.SolidFill("2"), urwid.CENTER, 10, urwid.MIDDLE, 5) new_bottom = urwid.SolidFill("4") new_top = urwid.SolidFill("5") new_options = ovl.options( urwid.LEFT, None, urwid.WHSettings.GIVEN, 4, urwid.TOP, None, urwid.WHSettings.GIVEN, 2 ) ovl.contents = [(new_bottom, ovl._DEFAULT_BOTTOM_OPTIONS), (new_top, new_options)] self.assertIs(ovl.bottom_w, new_bottom) self.assertIs(ovl.top_w, new_top) self.assertEqual((4, 2), (ovl.width, ovl.height)) with self.assertRaises(ValueError): ovl.contents = [(new_bottom, ovl._DEFAULT_BOTTOM_OPTIONS)] def test_contents_getitem_invalid_index(self) -> None: ovl = urwid.Overlay(urwid.SolidFill("*"), urwid.SolidFill("#"), urwid.CENTER, 10, urwid.MIDDLE, 5) with self.assertRaises(IndexError): ovl.contents[2] # accessing for the side effect of raising def test_contents_setitem_errors(self) -> None: ovl = urwid.Overlay(urwid.SolidFill("*"), urwid.SolidFill("#"), urwid.CENTER, 10, urwid.MIDDLE, 5) with self.subTest("value is not a (widget, options) pair"), self.assertRaises(overlay_mod.OverlayError): ovl.contents[0] = "not-a-pair" with self.subTest("bottom options mismatch"), self.assertRaises(overlay_mod.OverlayError): ovl.contents[0] = (urwid.SolidFill("x"), ovl.contents[1][1]) with self.subTest("top options malformed"), self.assertRaises(overlay_mod.OverlayError): ovl.contents[1] = (urwid.SolidFill("y"), (1, 2, 3)) with self.subTest("invalid index"), self.assertRaises(IndexError): ovl.contents[5] = (urwid.SolidFill("z"), ovl.contents[1][1]) def test_get_cursor_coords_none_without_support(self) -> None: ovl = urwid.Overlay(urwid.SolidFill("*"), urwid.SolidFill("#"), urwid.CENTER, 10, urwid.MIDDLE, 5) self.assertIsNone(ovl.get_cursor_coords((20, 10))) def test_get_cursor_coords_clamped_to_last_row(self) -> None: ovl = urwid.Overlay(FixedCursorSolidFill("*"), urwid.SolidFill("#"), urwid.CENTER, 5, urwid.MIDDLE, 5) self.assertEqual((0, 4), ovl.get_cursor_coords((5, 5))) def test_calculate_padding_filler_raises_for_zero_height_fixed(self) -> None: ovl = urwid.Overlay(ZeroHeightFixed(), urwid.SolidFill("#"), urwid.CENTER, None, urwid.MIDDLE, None) with self.assertRaises(overlay_mod.OverlayError): ovl.calculate_padding_filler((10, 5), False) def test_calculate_padding_filler_shrinks_bottom_for_oversized_flow(self) -> None: """A PACK-height flow widget taller than maxrow has its bottom filler go negative instead of raising.""" top_w = urwid.Text("one two three four five six seven eight nine ten") ovl = urwid.Overlay(top_w, urwid.SolidFill("#"), urwid.CENTER, 5, urwid.TOP, None) height = top_w.rows((5,)) self.assertGreater(height, 3) self.assertEqual((0, 0, 0, 3 - height), ovl.calculate_padding_filler((5, 3), False)) def test_render_returns_bottom_only_when_bottom_canvas_is_empty(self) -> None: ovl = urwid.Overlay(urwid.SolidFill("X"), urwid.SolidFill("."), urwid.CENTER, 4, urwid.MIDDLE, 2) canvas = ovl.render((0, 5)) self.assertIsInstance(canvas, urwid.CompositeCanvas) self.assertEqual(0, canvas.cols()) self.assertEqual(5, canvas.rows()) def test_mouse_event_without_support(self) -> None: with self.assertWarns(DeprecationWarning): ovl = urwid.Overlay( NotAWidget(), urwid.SolidFill("#"), urwid.CENTER, (urwid.RELATIVE, 100), urwid.MIDDLE, (urwid.RELATIVE, 100), ) self.assertFalse(ovl.mouse_event((10, 5), "mouse press", 1, 0, 0, True)) def test_mouse_event_outside_top_w_area(self) -> None: ovl = urwid.Overlay(urwid.SolidFill("X"), urwid.SolidFill("."), urwid.CENTER, 4, urwid.MIDDLE, 2) self.assertEqual((2, 2, 1, 1), ovl.calculate_padding_filler((8, 4), True)) self.assertFalse(ovl.mouse_event((8, 4), "mouse press", 1, 0, 0, True)) urwid-urwid-69907e4/tests/widgets/test_padding.py000066400000000000000000000466121526073734300221370ustar00rootroot00000000000000from __future__ import annotations import unittest import urwid class PaddingTest(unittest.TestCase): def test_sizing(self) -> None: fixed_only = urwid.BigText("3", urwid.HalfBlock5x4Font()) fixed_flow = urwid.Text("Some text", align=urwid.CENTER) flow_only = urwid.ProgressBar(None, None) with self.subTest("Fixed only"): widget = urwid.Padding(fixed_only) self.assertEqual(frozenset((urwid.FIXED,)), widget.sizing()) cols, rows = 5, 4 self.assertEqual((cols, rows), widget.pack(())) canvas = widget.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [ "▄▀▀▄ ", " ▄▀ ", "▄ █ ", " ▀▀ ", ], [line.decode("utf-8") for line in canvas.text], ) with self.subTest("Fixed only + relative size & default align"): widget = urwid.Padding(fixed_only, width=(urwid.RELATIVE, 50)) self.assertEqual(frozenset((urwid.FIXED,)), widget.sizing()) cols, rows = 10, 4 self.assertEqual((cols, rows), widget.pack(())) canvas = widget.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [ "▄▀▀▄ ", " ▄▀ ", "▄ █ ", " ▀▀ ", ], [line.decode("utf-8") for line in canvas.text], ) with self.subTest("FIXED/FLOW"): widget = urwid.Padding(fixed_flow) self.assertEqual(frozenset((urwid.FIXED, urwid.FLOW)), widget.sizing()) cols, rows = 9, 1 self.assertEqual((cols, rows), widget.pack(())) canvas = widget.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( ["Some text"], [line.decode("utf-8") for line in canvas.text], ) with self.subTest("FIXED/FLOW + relative size & right align"): widget = urwid.Padding(fixed_flow, width=(urwid.RELATIVE, 65), align=urwid.RIGHT) self.assertEqual(frozenset((urwid.FIXED, urwid.FLOW)), widget.sizing()) cols, rows = 14, 1 self.assertEqual((cols, rows), widget.pack(())) canvas = widget.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [" Some text"], [line.decode("utf-8") for line in canvas.text], ) with self.subTest("GIVEN FLOW make FIXED"): widget = urwid.Padding(flow_only, width=5, align=urwid.RIGHT, right=1) self.assertEqual(frozenset((urwid.FIXED, urwid.FLOW)), widget.sizing()) cols, rows = 6, 1 self.assertEqual((cols, rows), widget.pack(())) canvas = widget.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [" 0 % "], [line.decode("utf-8") for line in canvas.text], ) with self.subTest("GIVEN FIXED = error"): widget = urwid.Padding(fixed_only, width=5) with self.assertWarns(urwid.widget.padding.PaddingWarning) as ctx, self.assertRaises(AttributeError): widget.sizing() self.assertEqual( f"WHSettings.GIVEN expect BOX or FLOW widget to be used, but received {fixed_only}", str(ctx.warnings[0].message), ) widget.pack(()) self.assertEqual( f"WHSettings.GIVEN expect BOX or FLOW widget to be used, but received {fixed_only}", str(ctx.warnings[1].message), ) with self.assertRaises(ValueError) as err_ctx: widget.render(()) self.assertEqual( "FixedWidget takes only () for size.passed: (5,)", str(err_ctx.exception), ) def test_fixed(self) -> None: """Test real-world like scenario with padded contents.""" col_list = [ urwid.SolidFill(), urwid.Button("OK", align=urwid.CENTER), urwid.SolidFill(), urwid.Button("Cancel", align=urwid.CENTER), urwid.SolidFill(), ] body = urwid.Pile( ( (urwid.Text("Window content text here and it should not touch line", align=urwid.CENTER)), (urwid.PACK, urwid.Columns(col_list, dividechars=1, box_columns=(0, 2, 4))), ) ) widget = urwid.LineBox( urwid.Pile( ( urwid.Text("Modal window", align=urwid.CENTER), urwid.Divider("─"), urwid.Padding(body, width=urwid.PACK, left=1, right=1), ) ) ) self.assertEqual(frozenset((urwid.FIXED, urwid.FLOW)), widget.sizing()) cols, rows = 57, 6 self.assertEqual((cols, rows), widget.pack(())) canvas = widget.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [ "┌───────────────────────────────────────────────────────�", "│ Modal window │", "│───────────────────────────────────────────────────────│", "│ Window content text here and it should not touch line │", "│ < OK > < Cancel > │", "└───────────────────────────────────────────────────────┘", ], [line.decode("utf-8") for line in canvas.text], ) # Forward keypress self.assertEqual("OK", body.focus.focus.label) widget.keypress((), "right") self.assertEqual("Cancel", body.focus.focus.label) def test_insufficient_space(self): width = 10 widget = urwid.Padding(urwid.Text("Some text"), width=width) with self.assertWarns(urwid.widget.PaddingWarning) as ctx: canvas = widget.render((width - 1,)) self.assertEqual("Some text", str(canvas)) self.assertEqual(width - 1, canvas.cols()) def ptest(self, desc, align, width, maxcol, left, right, min_width=None): p = urwid.Padding(None, align, width, min_width) l, r = p.padding_values((maxcol,), False) assert (l, r) == (left, right), f"{desc} expected {left, right} but got {l, r}" def petest(self, desc, align, width): self.assertRaises(urwid.PaddingError, lambda: urwid.Padding(None, align, width)) def test_create(self): self.petest("invalid pad", 6, 5) self.petest("invalid pad type", ("bad", 2), 5) self.petest("invalid width", "center", "42") self.petest("invalid width type", "center", ("gouranga", 4)) def test_values(self): self.ptest("left align 5 7", "left", 5, 7, 0, 2) self.ptest("left align 7 7", "left", 7, 7, 0, 0) self.ptest("left align 9 7", "left", 9, 7, 0, 0) self.ptest("right align 5 7", "right", 5, 7, 2, 0) self.ptest("center align 5 7", "center", 5, 7, 1, 1) self.ptest("fixed left", ("fixed left", 3), 5, 10, 3, 2) self.ptest("fixed left reduce", ("fixed left", 3), 8, 10, 2, 0) self.ptest("fixed left shrink", ("fixed left", 3), 18, 10, 0, 0) self.ptest("fixed left, right", ("fixed left", 3), ("fixed right", 4), 17, 3, 4) self.ptest("fixed left, right, min_width", ("fixed left", 3), ("fixed right", 4), 10, 3, 2, 5) self.ptest("fixed left, right, min_width 2", ("fixed left", 3), ("fixed right", 4), 10, 2, 0, 8) self.ptest("fixed right", ("fixed right", 3), 5, 10, 2, 3) self.ptest("fixed right reduce", ("fixed right", 3), 8, 10, 0, 2) self.ptest("fixed right shrink", ("fixed right", 3), 18, 10, 0, 0) self.ptest("fixed right, left", ("fixed right", 3), ("fixed left", 4), 17, 4, 3) self.ptest("fixed right, left, min_width", ("fixed right", 3), ("fixed left", 4), 10, 2, 3, 5) self.ptest("fixed right, left, min_width 2", ("fixed right", 3), ("fixed left", 4), 10, 0, 2, 8) self.ptest("relative 30", ("relative", 30), 5, 10, 1, 4) self.ptest("relative 50", ("relative", 50), 5, 10, 2, 3) self.ptest("relative 130 edge", ("relative", 130), 5, 10, 5, 0) self.ptest("relative -10 edge", ("relative", -10), 4, 10, 0, 6) self.ptest("center relative 70", "center", ("relative", 70), 10, 1, 2) self.ptest("center relative 70 grow 8", "center", ("relative", 70), 10, 1, 1, 8) def mctest(self, desc, left, right, size, cx, innercx): class Inner: def __init__(self, desc, innercx): self.desc = desc self.innercx = innercx def move_cursor_to_coords(self, size, cx, cy): assert cx == self.innercx, desc i = Inner(desc, innercx) p = urwid.Padding(i, ("fixed left", left), ("fixed right", right)) p.move_cursor_to_coords(size, cx, 0) def test_cursor(self): self.mctest("cursor left edge", 2, 2, (10, 2), 2, 0) self.mctest("cursor left edge-1", 2, 2, (10, 2), 1, 0) self.mctest("cursor right edge", 2, 2, (10, 2), 7, 5) self.mctest("cursor right edge+1", 2, 2, (10, 2), 8, 5) def test_reduced_padding_cursor(self): # FIXME: This is at least consistent now, but I don't like it. # pack() on an Edit should leave room for the cursor # fixing this gets deep into things like Edit._shift_view_to_cursor # though, so this might not get fixed for a while p = urwid.Padding(urwid.Edit("", ""), width="pack", left=4) self.assertEqual(p.render((10,), True).cursor, None) self.assertEqual(p.get_cursor_coords((10,)), None) self.assertEqual(p.render((4,), True).cursor, None) self.assertEqual(p.get_cursor_coords((4,)), None) p = urwid.Padding(urwid.Edit("", ""), width=("relative", 100), left=4) self.assertEqual(p.render((10,), True).cursor, (4, 0)) self.assertEqual(p.get_cursor_coords((10,)), (4, 0)) self.assertEqual(p.render((4,), True).cursor, None) self.assertEqual(p.get_cursor_coords((4,)), None) def test_left_right_padding_around_text(self) -> None: """Inner padding.""" padded = urwid.Padding(urwid.Text("OK"), left=1, right=1) self.assertEqual((4, 1), padded.pack(())) self.assertEqual([b" OK "], padded.render(()).text) self.assertEqual([b" OK "], padded.render((6,)).text) def test_width_none_becomes_clip(self) -> None: widget = urwid.Padding(urwid.Text("hi"), width=None) self.assertEqual(urwid.CLIP, widget.width) def test_sizing_clip(self) -> None: widget = urwid.Padding(urwid.Text("hi"), width="clip") self.assertEqual(frozenset((urwid.FLOW,)), widget.sizing()) def test_sizing_given_box_only_widget(self) -> None: """GIVEN width with a BOX-only widget should not warn (BOX in sizing).""" widget = urwid.Padding(urwid.SolidFill(), width=5) self.assertEqual(frozenset((urwid.BOX,)), widget.sizing()) def test_align_setter(self) -> None: widget = urwid.Padding(urwid.Text("hi")) widget.align = urwid.RIGHT self.assertEqual(urwid.RIGHT, widget.align) def test_width_setter(self) -> None: widget = urwid.Padding(urwid.Text("hi")) widget.width = 7 self.assertEqual(7, widget.width) def test_pack_with_size(self) -> None: widget = urwid.Padding(urwid.Text("hi")) self.assertEqual((10, 1), widget.pack((10,), False)) def test_pack_clip_raises(self) -> None: widget = urwid.Padding(urwid.Text("hi"), width="clip") with self.assertRaises(urwid.PaddingError): widget.pack(()) def test_padding_values_clip_no_size_raises(self) -> None: widget = urwid.Padding(urwid.Text("hi"), width="clip") with self.assertRaises(urwid.PaddingError): widget.padding_values((), False) def test_pack_fixed_not_supported_warns(self) -> None: class FlowOnlyButPackable(urwid.Text): def sizing(self): return frozenset((urwid.FLOW,)) def pack(self, size=(), focus=False): return (5, 1) widget = urwid.Padding(FlowOnlyButPackable("hi"), width=urwid.PACK) with self.assertWarns(urwid.widget.padding.PaddingWarning): self.assertEqual((5, 1), widget.pack(())) def test_pack_unexpected_width_type(self) -> None: widget = urwid.Padding(urwid.Text("hi"), width=urwid.PACK) widget._width_type = "bogus" with self.assertRaises(urwid.PaddingError): widget.pack(()) def test_render_clip(self) -> None: widget = urwid.Padding(urwid.BigText("1", urwid.HalfBlock5x4Font()), width="clip") canvas = widget.render((10,)) self.assertEqual(10, canvas.cols()) def test_render_empty_widget_no_size_raises(self) -> None: class EmptyFixed(urwid.Widget): _sizing = frozenset((urwid.FIXED,)) def sizing(self): return frozenset((urwid.FIXED,)) def pack(self, size=(), focus=False): return (0, 1) def render(self, size, focus=False): return urwid.TextCanvas([b""], maxcol=0) widget = urwid.Padding(EmptyFixed(), width=urwid.PACK) with self.assertRaises(ValueError): widget.render(()) def test_padding_values_clip(self) -> None: widget = urwid.Padding(urwid.Text("hi"), width="clip") self.assertEqual((0, 8), widget.padding_values((10,), False)) def test_padding_values_pack_no_size(self) -> None: widget = urwid.Padding(urwid.Text("hi"), width=urwid.PACK) self.assertEqual((0, 0), widget.padding_values((), False)) def test_rows_clip(self) -> None: widget = urwid.Padding(urwid.Text("hi"), width="clip") self.assertEqual(1, widget.rows((10,), False)) def test_keypress_no_size(self) -> None: widget = urwid.Padding(urwid.Edit("", "hi"), width=urwid.PACK) self.assertEqual("right", widget.keypress((), "right")) def test_get_cursor_coords_no_attribute(self) -> None: widget = urwid.Padding(urwid.Text("hi")) self.assertIsNone(widget.get_cursor_coords((10,))) def test_get_cursor_coords_zero_width(self) -> None: widget = urwid.Padding(urwid.Edit("", "hi"), left=5, right=5) self.assertIsNone(widget.get_cursor_coords((10,))) def test_get_cursor_coords_no_size(self) -> None: class Stub(urwid.Text): def pack(self, size=(), focus=False): return (5, 1) def sizing(self): return frozenset((urwid.FIXED, urwid.FLOW)) def get_cursor_coords(self, size): return (1, 0) widget = urwid.Padding(Stub("hi"), width=urwid.PACK) self.assertEqual((1, 0), widget.get_cursor_coords(())) def test_get_cursor_coords_none_from_original(self) -> None: class Stub(urwid.Text): def get_cursor_coords(self, size): return None widget = urwid.Padding(Stub("hi")) self.assertIsNone(widget.get_cursor_coords((10,))) def test_move_cursor_to_coords_no_attribute(self) -> None: widget = urwid.Padding(urwid.Text("hi")) self.assertTrue(widget.move_cursor_to_coords((10,), 3, 0)) def test_move_cursor_to_coords_no_size(self) -> None: class Stub(urwid.Text): def move_cursor_to_coords(self, size, x, y): return True def pack(self, size=(), focus=False): return (5, 1) def sizing(self): return frozenset((urwid.FIXED, urwid.FLOW)) widget = urwid.Padding(Stub("hi"), width=urwid.PACK) self.assertTrue(widget.move_cursor_to_coords((), 1, 0)) def test_mouse_event_no_attribute(self) -> None: class NoMouse: """Plain object without a mouse_event method (unlike urwid.Widget).""" widget = urwid.Padding(NoMouse()) self.assertFalse(widget.mouse_event((10,), "mouse press", 1, 0, 0, True)) def test_mouse_event_outside_padding(self) -> None: widget = urwid.Padding(urwid.Button("OK"), left=3, right=3) self.assertFalse(widget.mouse_event((10,), "mouse press", 1, 0, 0, True)) def test_mouse_event_inside_padding(self) -> None: widget = urwid.Padding(urwid.Button("OK"), left=3, right=3) self.assertTrue(widget.mouse_event((10,), "mouse press", 1, 4, 0, True)) def test_mouse_event_no_size(self) -> None: class Stub(urwid.Text): def pack(self, size=(), focus=False): return (5, 1) def sizing(self): return frozenset((urwid.FIXED, urwid.FLOW)) def mouse_event(self, size, event, button, col, row, focus): return True widget = urwid.Padding(Stub("hi"), width=urwid.PACK) self.assertTrue(widget.mouse_event((), "mouse press", 1, 1, 0, True)) def test_get_pref_col_no_attribute(self) -> None: widget = urwid.Padding(urwid.Text("hi")) self.assertIsNone(widget.get_pref_col((10,))) def test_get_pref_col_with_size(self) -> None: widget = urwid.Padding(urwid.Edit("", "hi"), left=2, right=2) self.assertEqual(4, widget.get_pref_col((10,))) def test_get_pref_col_no_size(self) -> None: class Stub(urwid.Text): def pack(self, size=(), focus=False): return (5, 1) def sizing(self): return frozenset((urwid.FIXED, urwid.FLOW)) def get_pref_col(self, size): return 2 widget = urwid.Padding(Stub("hi"), left=2, right=2, width=urwid.PACK) self.assertEqual(4, widget.get_pref_col(())) def test_get_pref_col_non_int(self) -> None: class Stub(urwid.Text): def pack(self, size=(), focus=False): return (5, 1) def sizing(self): return frozenset((urwid.FIXED, urwid.FLOW)) def get_pref_col(self, size): return None widget = urwid.Padding(Stub("hi"), width=urwid.PACK) self.assertIsNone(widget.get_pref_col((10,))) def test_calculate_left_right_padding_relative_align_requires_amount(self) -> None: with self.assertRaises(TypeError): urwid.widget.padding.calculate_left_right_padding( 10, urwid.widget.padding.WHSettings.RELATIVE, None, urwid.widget.padding.WHSettings.GIVEN, 5, None, 0, 0, ) urwid-urwid-69907e4/tests/widgets/test_pile.py000066400000000000000000001313461526073734300214610ustar00rootroot00000000000000from __future__ import annotations import math import typing import unittest import warnings import urwid from tests.util import SelectableText if typing.TYPE_CHECKING: from collections.abc import Callable class NotAWidget: __slots__ = ("name", "symbol") def __init__(self, name: str, symbol: bytes) -> None: self.name = name self.symbol = symbol def __repr__(self) -> str: return f"{self.__class__.__name__}(name={self.name!r}, symbol={self.symbol!r})" def selectable(self) -> bool: return False def pack(self, max_col_row: tuple[int, int] | tuple[int], focus: bool = False) -> int: if len(max_col_row) == 2: return max_col_row return max_col_row[0], self.rows(max_col_row) def rows(self, max_col_row: tuple[int], focus=False) -> int: return 1 def render(self, max_col_row: tuple[int, int] | tuple[int], focus: bool = False) -> urwid.Canvas: maxcol = max_col_row[0] line = self.symbol * maxcol if len(max_col_row) == 1: return urwid.TextCanvas((line,), maxcol=maxcol) return urwid.TextCanvas((line,) * max_col_row[1], maxcol=maxcol) class ImplementWidget: __slots__ = ("name", "symbol") def sizing(self) -> frozenset[urwid.Sizing]: return frozenset((urwid.BOX, urwid.FLOW)) @property def base_widget(self) -> urwid.AbstractWidget: return self def __init__(self, name: str, symbol: bytes) -> None: self.name = name self.symbol = symbol def __repr__(self) -> str: return f"{self.__class__.__name__}(name={self.name!r}, symbol={self.symbol!r})" def selectable(self) -> bool: return False @property def focus(self) -> None: return None def _invalidate(self) -> None: pass def rows(self, max_col_row: tuple[int], focus: bool = False) -> int: return 1 def pack(self, size: typing.Any, focus: bool = False) -> tuple[int, int]: return self.rows(size, focus) def render(self, max_col_row: tuple[int, int] | tuple[int], focus: bool = False) -> urwid.Canvas: maxcol = max_col_row[0] line = self.symbol * maxcol if len(max_col_row) == 1: return urwid.TextCanvas((line,), maxcol=maxcol) return urwid.TextCanvas((line,) * max_col_row[1], maxcol=maxcol) def keypress(self, size: tuple[int, int] | tuple[int], key: str) -> str | None: return key def mouse_event( self, size: tuple[int, int] | tuple[int], event: str, button: int, col: int, row: int, focus: bool, ) -> bool | None: return False class BoxWithRows(urwid.Widget): """A proper Widget that only supports BOX sizing but still has a rows() method.""" _sizing = frozenset((urwid.BOX,)) def selectable(self) -> bool: return False def rows(self, size: tuple[int], focus: bool = False) -> int: return 2 def pack(self, size: tuple[int, int] | tuple[int] | tuple[()], focus: bool = False) -> tuple[int, int]: return (size[0] if size else 3, 2) def render(self, size: tuple[int, int] | tuple[int], focus: bool = False) -> urwid.Canvas: maxcol = size[0] rows = size[1] if len(size) > 1 else 2 return urwid.SolidFill("#").render((maxcol, rows)) class FixedBox(urwid.Widget): """A proper Widget supporting both FIXED and BOX sizing.""" _sizing = frozenset((urwid.FIXED, urwid.BOX)) def __init__(self, width: int, height: int, symbol: str) -> None: super().__init__() self.width = width self.height = height self.symbol = symbol def selectable(self) -> bool: return False def pack(self, size: tuple[int, int] | tuple[int] | tuple[()], focus: bool = False) -> tuple[int, int]: return (self.width, self.height) def render(self, size: tuple[int, int] | tuple[int], focus: bool = False) -> urwid.Canvas: maxcol = size[0] rows = size[1] if len(size) > 1 else self.height return urwid.SolidFill(self.symbol).render((maxcol, rows)) class NoPrefColText(SelectableText): def get_pref_col(self, size: tuple[int]) -> None: return None class RejectCursorText(SelectableText): def move_cursor_to_coords(self, size: tuple[int], col: int, row: int) -> bool: return False class NoCoordsText(SelectableText): def get_cursor_coords(self, size: tuple[int]) -> None: return None class ZeroRowsText(SelectableText): def rows(self, size: tuple[int], focus: bool = False) -> int: return 0 def move_cursor_to_coords(self, size: tuple[int], col: int, row: int) -> bool: return True class MultiRowCursorText(SelectableText): def rows(self, size: tuple[int], focus: bool = False) -> int: return 3 def move_cursor_to_coords(self, size: tuple[int], col: int, row: int) -> bool: return row == 1 class ParentMutatingText(SelectableText): """Selectable text that runs *callback* before returning a key unhandled or answering a cursor move.""" def __init__(self, markup: str, callback: Callable[[], object], *, accept_cursor: bool = True) -> None: """Run *callback* on every keypress and cursor move, and answer cursor moves with *accept_cursor*.""" super().__init__(markup) self.callback = callback self.accept_cursor = accept_cursor def keypress(self, size: tuple[int], key: str) -> str: """Run the callback, then return *key* unhandled.""" self.callback() return super().keypress(size, key) def move_cursor_to_coords(self, _size: tuple[int], _col: int, _row: int) -> bool: """Run the callback, then report whether the cursor move is accepted.""" self.callback() return self.accept_cursor class PileTest(unittest.TestCase): def test_basic_sizing(self) -> None: box_only = urwid.SolidFill("#") flow_only = urwid.ProgressBar(None, None) fixed_only = urwid.BigText("0", urwid.Thin3x3Font()) flow_fixed = urwid.Text("text") with self.subTest("BOX-only widget"): widget = urwid.Pile((box_only,)) self.assertEqual(frozenset((urwid.BOX,)), widget.sizing()) cols, rows = 2, 2 self.assertEqual((cols, rows), widget.pack((cols, rows))) canvas = widget.render((cols, rows)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) with self.subTest("GIVEN BOX -> BOX/FLOW"): widget = urwid.Pile(((2, box_only),)) self.assertEqual(frozenset((urwid.BOX, urwid.FLOW)), widget.sizing()) cols, rows = 2, 5 self.assertEqual((cols, rows), widget.pack((cols, rows))) canvas = widget.render((cols, rows)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) cols, rows = 5, 2 self.assertEqual((cols, rows), widget.pack((cols,))) canvas = widget.render((cols,)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) with self.subTest("FLOW-only"): widget = urwid.Pile((flow_only,)) self.assertEqual(frozenset((urwid.FLOW,)), widget.sizing()) cols, rows = 5, 1 self.assertEqual((cols, rows), widget.pack((cols,))) canvas = widget.render((cols,)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) with self.subTest("FIXED -> FIXED"): widget = urwid.Pile(((urwid.PACK, fixed_only),)) self.assertEqual(frozenset((urwid.FIXED,)), widget.sizing()) cols, rows = 3, 3 self.assertEqual((cols, rows), widget.pack(())) canvas = widget.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [ "┌─�", "│ │", "└─┘", ], [line.decode("utf-8") for line in canvas.text], ) with self.subTest("FLOW/FIXED -> FLOW/FIXED"): widget = urwid.Pile(((urwid.PACK, flow_fixed),)) self.assertEqual(frozenset((urwid.FLOW, urwid.FIXED)), widget.sizing()) cols, rows = 4, 1 self.assertEqual((cols, rows), widget.pack(())) canvas = widget.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( ["text"], [line.decode("utf-8") for line in widget.render(()).text], ) cols, rows = 2, 2 self.assertEqual((cols, rows), widget.pack((cols,))) canvas = widget.render((cols,)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [ "te", "xt", ], [line.decode("utf-8") for line in canvas.text], ) with self.subTest("FLOW + FLOW/FIXED -> FLOW/FIXED"): widget = urwid.Pile((flow_only, (urwid.PACK, flow_fixed))) self.assertEqual(frozenset((urwid.FLOW, urwid.FIXED)), widget.sizing()) cols, rows = 4, 2 self.assertEqual((cols, rows), widget.pack(())) canvas = widget.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [ " 0 %", "text", ], [line.decode("utf-8") for line in canvas.text], ) cols, rows = 2, 3 self.assertEqual((cols, rows), widget.pack((cols,))) canvas = widget.render((cols,)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [ "0 ", "te", "xt", ], [line.decode("utf-8") for line in canvas.text], ) with self.subTest("FLOW + FIXED widgets -> FLOW/FIXED"): widget = urwid.Pile((flow_only, (urwid.PACK, fixed_only))) self.assertEqual(frozenset((urwid.FLOW, urwid.FIXED)), widget.sizing()) cols, rows = 3, 4 self.assertEqual((cols, rows), widget.pack(())) canvas = widget.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [ "0 %", "┌─�", "│ │", "└─┘", ], [line.decode("utf-8") for line in canvas.text], ) cols, rows = 10, 4 self.assertEqual((cols, rows), widget.pack((cols,))) canvas = widget.render((cols,)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [ " 0 % ", "┌─�", "│ │", "└─┘", ], [line.decode("utf-8") for line in canvas.text], ) with self.subTest("GIVEN BOX + FIXED widgets -> BOX/FLOW/FIXED"): widget = urwid.Pile(((1, box_only), (urwid.PACK, fixed_only), (1, box_only))) self.assertEqual(frozenset((urwid.BOX, urwid.FLOW, urwid.FIXED)), widget.sizing()) cols, rows = 3, 5 self.assertEqual((cols, rows), widget.pack(())) canvas = widget.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [ "###", "┌─�", "│ │", "└─┘", "###", ], [line.decode("utf-8") for line in canvas.text], ) cols, rows = 5, 5 self.assertEqual((cols, rows), widget.pack((cols,))) canvas = widget.render((cols,)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [ "#####", "┌─�", "│ │", "└─┘", "#####", ], [line.decode("utf-8") for line in canvas.text], ) cols, rows = 5, 6 self.assertEqual((cols, rows), widget.pack((cols, rows))) canvas = widget.render((cols, rows)) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [ "#####", "┌─�", "│ │", "└─┘", "#####", " ", ], [line.decode("utf-8") for line in canvas.text], ) def test_pack_render_fixed(self) -> None: """Potential real world case""" widget = urwid.LineBox( urwid.Pile( ( urwid.Text("Modal window", align=urwid.CENTER), urwid.Divider("─"), urwid.Columns( (urwid.Button(label, align=urwid.CENTER) for label in ("OK", "Cancel", "Help")), dividechars=1, ), ) ) ) cols, rows = 34, 5 self.assertEqual((cols, rows), widget.pack(())) canvas = widget.render(()) self.assertEqual(cols, canvas.cols()) self.assertEqual(rows, canvas.rows()) self.assertEqual( [ "┌────────────────────────────────�", "│ Modal window │", "│────────────────────────────────│", "│< OK > < Cancel > < Help >│", "└────────────────────────────────┘", ], [line.decode("utf-8") for line in canvas.text], ) self.assertEqual("OK", widget.focus.focus.label) widget.keypress((), "right") self.assertEqual("Cancel", widget.focus.focus.label) def test_not_a_widget(self): with self.subTest("Box"), self.assertWarns(urwid.widget.PileWarning) as ctx: items = (NotAWidget("First", b"*"), NotAWidget("Second", b"^")) widget = urwid.Pile(items) self.assertEqual(("****", "^^^^"), widget.render((4, 2)).decoded_text) self.assertEqual(f"{items[0]!r} is not a Widget", str(ctx.warnings[0].message)) self.assertEqual(f"{items[1]!r} is not a Widget", str(ctx.warnings[1].message)) with self.subTest("Flow"), self.assertWarns(urwid.widget.PileWarning) as ctx: items = (NotAWidget("First", b"*"), NotAWidget("Second", b"^")) widget = urwid.Pile(items) self.assertEqual(("******", "^^^^^^"), widget.render((6,)).decoded_text) self.assertEqual(f"{items[0]!r} is not a Widget", str(ctx.warnings[0].message)) self.assertEqual(f"{items[1]!r} is not a Widget", str(ctx.warnings[1].message)) def test_implement_widget_interface(self): with ( warnings.catch_warnings(record=True) as collected_w, self.subTest("Box"), ): items = (ImplementWidget("First", b"*"), ImplementWidget("Second", b"^")) widget = urwid.Pile(items) self.assertEqual(("****", "^^^^"), widget.render((4, 2)).decoded_text) pile_warnings = tuple( warning.message for warning in collected_w if warning.category == urwid.widget.PileWarning ) self.assertTrue(len(pile_warnings) == 0, "No warnings expected") with ( warnings.catch_warnings(record=True) as collected_w, self.subTest("Flow"), ): items = (ImplementWidget("First", b"*"), ImplementWidget("Second", b"^")) widget = urwid.Pile(items) self.assertEqual(("******", "^^^^^^"), widget.render((6,)).decoded_text) pile_warnings = tuple( warning.message for warning in collected_w if warning.category == urwid.widget.PileWarning ) self.assertTrue(len(pile_warnings) == 0, "No warnings expected") def ktest(self, desc, contents, focus_item, key, rkey, rfocus, rpref_col): p = urwid.Pile(contents, focus_item) rval = p.keypress((20,), key) assert rkey == rval, f"{desc} key expected {rkey!r} but got {rval!r}" new_focus = contents.index(p.focus) assert new_focus == rfocus, f"{desc} focus expected {rfocus!r} but got {new_focus!r}" new_pref = p.get_pref_col((20,)) assert new_pref == rpref_col, f"{desc} pref_col expected {rpref_col!r} but got {new_pref!r}" def test_select_change(self): self.ktest("simple up", [SelectableText("")], 0, "up", "up", 0, 0) self.ktest("simple down", [SelectableText("")], 0, "down", "down", 0, 0) self.ktest("ignore up", [urwid.Text(""), SelectableText("")], 1, "up", "up", 1, 0) self.ktest("ignore down", [SelectableText(""), urwid.Text("")], 0, "down", "down", 0, 0) self.ktest("step up", [SelectableText(""), SelectableText("")], 1, "up", None, 0, 0) self.ktest("step down", [SelectableText(""), SelectableText("")], 0, "down", None, 1, 0) self.ktest("skip step up", [SelectableText(""), urwid.Text(""), SelectableText("")], 2, "up", None, 0, 0) self.ktest("skip step down", [SelectableText(""), urwid.Text(""), SelectableText("")], 0, "down", None, 2, 0) self.ktest( "pad skip step up", [urwid.Text(""), SelectableText(""), urwid.Text(""), SelectableText("")], 3, "up", None, 1, 0, ) self.ktest( "pad skip step down", [SelectableText(""), urwid.Text(""), SelectableText(""), urwid.Text("")], 0, "down", None, 2, 0, ) self.ktest( "padi skip step up", [SelectableText(""), urwid.Text(""), SelectableText(""), urwid.Text(""), SelectableText("")], 4, "up", None, 2, 0, ) self.ktest( "padi skip step down", [SelectableText(""), urwid.Text(""), SelectableText(""), urwid.Text(""), SelectableText("")], 0, "down", None, 2, 0, ) e = urwid.Edit("", "abcd", edit_pos=1) e.keypress((20,), "right") # set a pref_col self.ktest("pref step up", [SelectableText(""), urwid.Text(""), e], 2, "up", None, 0, 2) self.ktest("pref step down", [e, urwid.Text(""), SelectableText("")], 0, "down", None, 2, 2) z = urwid.Edit("", "1234") self.ktest("prefx step up", [z, urwid.Text(""), e], 2, "up", None, 0, 2) assert z.get_pref_col((20,)) == 2 z = urwid.Edit("", "1234") self.ktest("prefx step down", [e, urwid.Text(""), z], 0, "down", None, 2, 2) assert z.get_pref_col((20,)) == 2 def test_init_with_a_generator(self): urwid.Pile(urwid.Text(c) for c in "ABC") def test_change_focus_with_mouse(self): p = urwid.Pile([urwid.Edit(), urwid.Edit()]) self.assertEqual(p.focus_position, 0) p.mouse_event((10,), "button press", 1, 1, 1, True) self.assertEqual(p.focus_position, 1) def test_zero_weight(self): p = urwid.Pile( [ urwid.SolidFill("a"), ("weight", 0, urwid.SolidFill("d")), ] ) p.render((5, 4)) def test_non_finite_size_rejected(self) -> None: """Reject a size that is infinite, NaN or too large for a float, as it cannot be converted to rows.""" for kind, amount in ( ("weight", math.inf), ("weight", math.nan), ("weight", 10**400), # too large for a float ("given", math.inf), ): with self.subTest(kind=kind, amount=amount): with self.assertRaises(urwid.PileError): urwid.Pile([(kind, amount, urwid.SolidFill())]) pile = urwid.Pile([]) with self.assertRaises(urwid.PileError): pile.contents.append((urwid.SolidFill(), (kind, amount))) self.assertEqual(0, len(pile.contents)) def test_huge_finite_weights_render(self) -> None: """Split rows by weight ratio even when the weights, their sum or rows * weight overflow a float.""" for weights, expected in ( ((1e308, 1e308), (25, 25)), # float sum overflows ((10**308, 10**308), (25, 25)), # int sum is too large for a float ((1e307, 1), (50, 0)), # rows * weight overflows ): with self.subTest(weights=weights): pile = urwid.Pile([("weight", weight, urwid.SolidFill()) for weight in weights]) self.assertEqual(expected, pile.get_rows_sizes((10, 50))[1]) self.assertEqual(50, pile.render((10, 50)).rows()) def test_mouse_event_in_empty_pile(self): p = urwid.Pile([]) p.mouse_event((5,), "button press", 1, 1, 1, False) p.mouse_event((5,), "button press", 1, 1, 1, True) def test_length(self): pile = urwid.Pile(urwid.Text(c) for c in "ABC") self.assertEqual(3, len(pile)) self.assertEqual(3, len(pile.contents)) def test_common(self): t1 = urwid.Text("one") t2 = urwid.Text("two") t3 = urwid.Text("three") sf = urwid.SolidFill("x") p = urwid.Pile([]) with self.subTest("Focus"): self.assertEqual(p.focus, None) self.assertRaises(IndexError, lambda: getattr(p, "focus_position")) self.assertRaises(IndexError, lambda: setattr(p, "focus_position", None)) self.assertRaises(IndexError, lambda: setattr(p, "focus_position", 0)) with self.subTest("Contents change"): p.contents = [(t1, ("pack", None)), (t2, ("pack", None)), (sf, ("given", 3)), (t3, ("pack", None))] p.focus_position = 1 del p.contents[0] self.assertEqual(p.focus_position, 0) p.contents[0:0] = [(t3, ("pack", None)), (t2, ("pack", None))] p.contents.insert(3, (t1, ("pack", None))) self.assertEqual(p.focus_position, 2) with self.subTest("Contents change validation"): p.contents.clear() self.assertRaises(urwid.PileError, lambda: p.contents.append(t1)) self.assertRaises(urwid.PileError, lambda: p.contents.append((t1, None))) self.assertRaises(urwid.PileError, lambda: p.contents.append((t1, "given"))) self.assertRaises(urwid.PileError, lambda: p.contents.append((t1, ("given",)))) # Incorrect kind self.assertRaises(urwid.PileError, lambda: p.contents.append((t1, ("what", 0)))) # incorrect size type self.assertRaises(urwid.PileError, lambda: p.contents.append((t1, ("given", ())))) # incorrect size self.assertRaises(urwid.PileError, lambda: p.contents.append((t1, ("given", -1)))) # Float and int weight accepted p.contents.append((t1, ("weight", 1))) p.contents.append((t2, ("weight", 0.5))) self.assertEqual(("one", "two"), p.render((3,)).decoded_text) def test_delete_contents_with_negative_step(self) -> None: widgets = [urwid.Text(str(index)) for index in range(5)] for focus, expected_focus in enumerate((1, 1, 3, 3, 3)): with self.subTest(focus=focus): pile = urwid.Pile(widgets, focus_item=focus) del pile.contents[::-2] self.assertEqual([widgets[1], widgets[3]], [item[0] for item in pile.contents]) self.assertIs(widgets[expected_focus], pile.focus) def test_focus_position(self): t1 = urwid.Text("one") t2 = urwid.Text("two") p = urwid.Pile([t1, t2]) self.assertEqual(p.focus, t1) self.assertEqual(p.focus_position, 0) p.focus_position = 1 self.assertEqual(p.focus, t2) self.assertEqual(p.focus_position, 1) p.focus_position = 0 self.assertRaises(IndexError, lambda: setattr(p, "focus_position", -1)) self.assertRaises(IndexError, lambda: setattr(p, "focus_position", 2)) def test_deprecated(self): t1 = urwid.Text("one") t2 = urwid.Text("two") p = urwid.Pile([t1, t2]) # old methods: with self.subTest("Focus"): p.set_focus(0) self.assertRaises(IndexError, lambda: p.set_focus(-1)) self.assertRaises(IndexError, lambda: p.set_focus(2)) p.set_focus(t2) self.assertEqual(p.focus_position, 1) self.assertRaises(ValueError, lambda: p.set_focus("nonexistant")) with self.subTest("Contents"): self.assertEqual(p.widget_list, [t1, t2]) self.assertEqual(p.item_types, [("weight", 1), ("weight", 1)]) with self.subTest("Contents change"): p.widget_list = [t2, t1] self.assertEqual(p.widget_list, [t2, t1]) self.assertEqual(p.contents, [(t2, ("weight", 1)), (t1, ("weight", 1))]) self.assertEqual(p.focus_position, 1) # focus unchanged p.item_types = [("flow", None), ("weight", 2)] self.assertEqual(p.item_types, [("flow", None), ("weight", 2)]) self.assertEqual(p.contents, [(t2, ("pack", None)), (t1, ("weight", 2))]) self.assertEqual(p.focus_position, 1) # focus unchanged with self.subTest("Contents change 2"): p.widget_list = [t1] self.assertEqual(len(p.contents), 1) self.assertEqual(p.focus_position, 0) p.widget_list.extend([t2, t1]) self.assertEqual(len(p.contents), 3) self.assertEqual(p.item_types, [("flow", None), ("weight", 1), ("weight", 1)]) p.item_types[:] = [("weight", 2)] self.assertEqual(len(p.contents), 1) def test_focused_not_fit(self): """Pile not fit in size and focused widget is out of default display window""" widget = urwid.Pile( ( (urwid.PACK, urwid.Text("top 0")), (urwid.PACK, urwid.Text("top 1")), urwid.ListBox((urwid.CheckBox("cb 0"),)), (urwid.PACK, urwid.Text("btm -2")), (urwid.PACK, urwid.Text("btm -1")), ) ) with self.subTest("Fit selectable only"): canvas = widget.render((8, 1), True) self.assertEqual(("[ ] cb 0",), canvas.decoded_text) with self.subTest("Fit selectable and some items"): canvas = widget.render((8, 2), True) self.assertEqual( ( "top 1 ", "[ ] cb 0", ), canvas.decoded_text, ) with self.subTest("Fit selectable and symmetric items"): canvas = widget.render((8, 3), True) self.assertEqual( ( "top 1 ", "[ ] cb 0", "btm -2 ", ), canvas.decoded_text, ) with self.subTest("Not symmetric top"): canvas = urwid.Pile( ( (urwid.PACK, urwid.Text("top 0")), (urwid.PACK, urwid.Text("top 1")), urwid.ListBox((urwid.CheckBox("cb 0"),)), (urwid.PACK, urwid.Text("btm -1")), ) ).render((8, 3), True) self.assertEqual( ( "top 1 ", "[ ] cb 0", "btm -1 ", ), canvas.decoded_text, ) with self.subTest("Not symmetric bottom"): canvas = urwid.Pile( ( (urwid.PACK, urwid.Text("top 0")), urwid.ListBox((urwid.CheckBox("cb 0"),)), (urwid.PACK, urwid.Text("btm -2")), (urwid.PACK, urwid.Text("btm -1")), ) ).render((8, 3), True) self.assertEqual( ( "top 0 ", "[ ] cb 0", "btm -2 ", ), canvas.decoded_text, ) with self.subTest("Non-linear sizes should not break rendering"): canvas = urwid.Pile( ( (urwid.PACK, urwid.Text("multi\nline")), (urwid.PACK, urwid.Text("top 1")), urwid.ListBox((urwid.CheckBox("cb 0"),)), (urwid.PACK, urwid.Text("also\nlines")), ) ).render((8, 3), True) self.assertEqual( ( "top 1 ", "[ ] cb 0", " ", ), canvas.decoded_text, ) with self.subTest("In multiple weighted need to choose correct to show"): canvas = urwid.Pile( ( (urwid.PACK, urwid.Text("top 0")), urwid.ListBox((urwid.CheckBox("cb 0"),)), (urwid.PACK, urwid.Text("top 1")), urwid.ListBox((urwid.CheckBox("cb 1"),)), (urwid.PACK, urwid.Text("btm -2")), urwid.ListBox((urwid.CheckBox("cb 1"),)), (urwid.PACK, urwid.Text("btm -1")), ), focus_item=3, ).render((8, 3), True) self.assertEqual( ( "top 1 ", "[ ] cb 1", "btm -2 ", ), canvas.decoded_text, ) def test_focus_changed_callback_replacement(self) -> None: """Replace the contents focus callback and read the new index before it is applied.""" first = urwid.Button("first") second = urwid.Button("second") pile = urwid.Pile((first, second)) seen: list[tuple[int, int, urwid.Widget]] = [] def on_focus_change(new_focus: int) -> None: seen.append((new_focus, pile.focus_position, pile.contents[new_focus][0])) pile._invalidate() pile.contents.set_focus_changed_callback(on_focus_change) pile.focus_position = 1 self.assertEqual([(1, 0, second)], seen) self.assertIs(second, pile.focus) self.assertEqual(1, pile.focus_position) def test_pack_rows_with_weight_body(self) -> None: """Dialog-style pile: PACK description, weighted body, PACK footer.""" pile = urwid.Pile( ( (urwid.WHSettings.PACK, urwid.Text("Describe")), urwid.SolidFill("."), (urwid.WHSettings.PACK, urwid.Button("OK")), ) ) self.assertEqual((8, 5), pile.pack((8, 5))) self.assertEqual( ( "Describe", "........", "........", "........", "< OK >", ), pile.render((8, 5)).decoded_text, ) def test_sizing_empty(self) -> None: self.assertEqual(frozenset((urwid.BOX, urwid.FLOW)), urwid.Pile([]).sizing()) def test_sizing_given_non_box_widget_falls_back(self) -> None: pile = urwid.Pile(((5, urwid.ProgressBar(None, None)),)) with self.assertWarns(urwid.widget.PileWarning) as ctx: self.assertEqual(frozenset((urwid.BOX, urwid.FLOW)), pile.sizing()) self.assertIn("not supported", str(ctx.warnings[0].message)) def test_sizing_pack_flow_only_widget(self) -> None: pile = urwid.Pile(((urwid.PACK, urwid.ProgressBar(None, None)),)) self.assertEqual(frozenset((urwid.FLOW,)), pile.sizing()) def test_init_invalid_items(self) -> None: with self.assertRaises(urwid.PileError): urwid.Pile([("bogus", 1, urwid.Text("x"))]) with self.assertRaises(urwid.PileError): urwid.Pile([(1, 2, 3, 4)]) def test_options(self) -> None: self.assertEqual((urwid.PACK, None), urwid.Pile.options(urwid.PACK)) self.assertEqual((urwid.GIVEN, 5), urwid.Pile.options(urwid.GIVEN, 5)) self.assertEqual((urwid.WEIGHT, 2), urwid.Pile.options(urwid.WEIGHT, 2)) with self.assertRaises(urwid.PileError): urwid.Pile.options(urwid.GIVEN, None) def test_focus_setter_widget_not_found(self) -> None: pile = urwid.Pile([urwid.Text("one")]) with self.assertRaises(ValueError): pile.focus = urwid.Text("other") def test_get_pref_col_not_selectable(self) -> None: pile = urwid.Pile([urwid.Text("one")]) self.assertIsNone(pile.get_pref_col((10,))) def test_get_pref_col_empty_contents_but_selectable(self) -> None: """Defensive branch: contents is empty even though `selectable` was overridden to return True.""" pile = urwid.Pile([]) pile.selectable = lambda: True # abuse our Pile self.assertIsNone(pile.get_pref_col((10,))) def test_selectable_follows_child_change(self) -> None: """A child changing its selectability after it was added is seen by the pile and its keypress.""" placeholder = urwid.WidgetPlaceholder(urwid.Text("wait")) pile = urwid.Pile([placeholder]) self.assertFalse(pile.selectable()) placeholder.original_widget = urwid.Edit() self.assertTrue(pile.selectable()) self.assertIsNone(pile.keypress((10,), "a")) placeholder.original_widget = urwid.Text("done") self.assertFalse(pile.selectable()) def test_get_rows_sizes_empty_contents(self) -> None: pile = urwid.Pile([]) self.assertEqual(((), (), ()), pile.get_rows_sizes(())) self.assertEqual(((5,), (3,), ()), pile.get_rows_sizes((5, 3))) def test_fixed_rows_sizes_errors(self) -> None: with self.subTest("PACK with BOX-only widget"), self.assertRaises(urwid.PileError): urwid.Pile(((urwid.PACK, urwid.SolidFill("#")),)).get_rows_sizes(()) with self.subTest("GIVEN with non-BOX widget"), self.assertRaises(urwid.PileError): urwid.Pile(((5, urwid.ProgressBar(None, None)),)).get_rows_sizes(()) with self.subTest("WEIGHT with BOX-only widget"), self.assertRaises(urwid.PileError): urwid.Pile((urwid.SolidFill("#"),)).get_rows_sizes(()) with self.subTest("Only GIVEN BOX items: no width information"), self.assertRaises(urwid.PileError): urwid.Pile(((5, urwid.SolidFill("#")),)).get_rows_sizes(()) def test_fixed_rows_sizes_weighted_fixed_box_widgets(self) -> None: """FIXED+BOX widgets used with the default WEIGHT option get scaled to a common height.""" pile = urwid.Pile((FixedBox(4, 2, "a"), FixedBox(6, 4, "b"))) self.assertEqual(frozenset((urwid.FIXED, urwid.BOX)), pile.sizing()) self.assertEqual((6, 8), pile.pack(())) self.assertEqual( ( "aaaaaa", "aaaaaa", "aaaaaa", "aaaaaa", "bbbbbb", "bbbbbb", "bbbbbb", "bbbbbb", ), pile.render(()).decoded_text, ) def test_fixed_rows_sizes_weights_too_far_apart(self) -> None: """Raise PileError when the weight ratio scales a fixed height past what a float can hold.""" pile = urwid.Pile((("weight", 5e-324, FixedBox(4, 2, "a")), ("weight", 1, FixedBox(6, 4, "b")))) with self.assertRaisesRegex(urwid.PileError, "too far apart"): pile.pack(()) with self.assertRaisesRegex(urwid.PileError, "too far apart"): pile.render(()) def test_flow_rows_sizes_unusual_sizing_warning(self) -> None: pile = urwid.Pile((BoxWithRows(),)) with self.assertWarns(urwid.widget.PileWarning) as ctx: canvas = pile.render((5,)) self.assertEqual(("#####", "#####"), canvas.decoded_text) self.assertIn("Unusual widget", str(ctx.warnings[0].message)) def test_get_rows_sizes_box_unusual_sizing_warning(self) -> None: pile = urwid.Pile(((urwid.PACK, BoxWithRows()),)) with self.assertWarns(urwid.widget.PileWarning) as ctx: canvas = pile.render((5, 4)) self.assertIn("Unusual widget", str(ctx.warnings[0].message)) self.assertEqual(4, canvas.rows()) def test_get_rows_sizes_hide_loop_exhausted(self) -> None: """When there is zero available space, every hideable item stays hidden (no break taken).""" pile = urwid.Pile( ( (urwid.PACK, urwid.Text("a")), (urwid.PACK, urwid.Text("b")), urwid.ListBox((urwid.CheckBox("cb0"),)), (urwid.PACK, urwid.Text("c")), (urwid.PACK, urwid.Text("d")), ) ) _widths, heights, _args = pile.get_rows_sizes((8, 0), focus=True) self.assertEqual((0, 0, 0, 0, 0), heights) def test_get_rows_sizes_zero_weight_ignored_by_hide_logic(self) -> None: pile = urwid.Pile( ( (urwid.PACK, urwid.Text("top 0")), ("weight", 0, urwid.SolidFill("z")), urwid.ListBox((urwid.CheckBox("cb 0"),)), (urwid.PACK, urwid.Text("btm -1")), ) ) self.assertEqual(("top 0 ", "[ ] cb 0"), pile.render((8, 2), True).decoded_text) def test_get_item_rows(self) -> None: pile = urwid.Pile([urwid.Text("a"), urwid.Text("b")]) self.assertEqual([1, 1], pile.get_item_rows((5, 2), False)) def test_render_empty_pile(self) -> None: pile = urwid.Pile([]) self.assertEqual((" ", " "), pile.render((5, 2)).decoded_text) with self.assertRaises(ValueError): pile.render(()) def test_keypress_empty_contents(self) -> None: pile = urwid.Pile([]) self.assertEqual("up", pile.keypress((5,), "up")) def test_keypress_unhandled_non_updown_key(self) -> None: """When the focused widget leaves a non-up/down key unhandled, Pile returns it as-is.""" pile = urwid.Pile([SelectableText("x")]) self.assertEqual("left", pile.keypress((10,), "left")) def test_keypress_pref_col_none_and_no_move_cursor(self) -> None: a = NoPrefColText("a") b = SelectableText("b") pile = urwid.Pile([a, b]) pile.focus_position = 0 self.assertIsNone(pile.keypress((10,), "down")) self.assertEqual(1, pile.focus_position) def test_get_cursor_coords(self) -> None: with self.subTest("Not selectable"): pile = urwid.Pile([urwid.Text("one")]) self.assertIsNone(pile.get_cursor_coords((10,))) with self.subTest("Focus widget without get_cursor_coords"): pile = urwid.Pile([SelectableText("x")]) self.assertIsNone(pile.get_cursor_coords((10,))) with self.subTest("Focus at position 0"): pile = urwid.Pile([urwid.Edit("", "abc", edit_pos=1)]) self.assertEqual((1, 0), pile.get_cursor_coords((10,))) with self.subTest("Focus below other items"): pile = urwid.Pile([urwid.Text("one"), urwid.Edit("", "abc", edit_pos=1)]) pile.focus_position = 1 self.assertEqual((1, 1), pile.get_cursor_coords((10,))) with self.subTest("Focus widget's get_cursor_coords returns None"): pile = urwid.Pile([NoCoordsText("x")]) self.assertIsNone(pile.get_cursor_coords((10,))) def test_keypress_not_selectable_falls_through_to_candidates(self) -> None: """When no child is selectable, up/down still scans candidates before returning the key unchanged.""" pile = urwid.Pile([urwid.Text("a"), urwid.Text("b")]) self.assertFalse(pile.selectable()) self.assertEqual("down", pile.keypress((10,), "down")) def test_keypress_zero_row_candidate_skips_move_loop(self) -> None: pile = urwid.Pile([SelectableText("above"), ZeroRowsText("below")]) self.assertIsNone(pile.keypress((10,), "down")) self.assertEqual(1, pile.focus_position) def test_keypress_move_cursor_retries_rows(self) -> None: """The row-by-row cursor placement loop retries until move_cursor_to_coords succeeds.""" pile = urwid.Pile([SelectableText("above"), urwid.Text("mid"), MultiRowCursorText("below")]) self.assertIsNone(pile.keypress((10,), "down")) self.assertEqual(2, pile.focus_position) def test_move_cursor_to_coords(self) -> None: with self.subTest("Success"): pile = urwid.Pile([urwid.Edit("", "abc"), urwid.Edit("", "def")]) self.assertTrue(pile.move_cursor_to_coords((10,), 1, 1)) self.assertEqual(1, pile.focus_position) with self.subTest("Row beyond all items"): pile = urwid.Pile([urwid.Edit("", "abc"), urwid.Edit("", "def")]) self.assertFalse(pile.move_cursor_to_coords((10,), 1, 100)) with self.subTest("Widget at row not selectable"): pile = urwid.Pile([urwid.Text("a"), urwid.Edit("", "b")]) self.assertFalse(pile.move_cursor_to_coords((10,), 0, 0)) with self.subTest("Widget rejects cursor move"): pile = urwid.Pile([RejectCursorText("x")]) self.assertFalse(pile.move_cursor_to_coords((10,), 0, 0)) self.assertEqual(0, pile.focus_position) with self.subTest("Selectable widget without move_cursor_to_coords"): pile = urwid.Pile([SelectableText("y")]) self.assertTrue(pile.move_cursor_to_coords((10,), 0, 0)) def test_mouse_event_row_beyond_items(self) -> None: pile = urwid.Pile([urwid.Text("a")]) self.assertFalse(pile.mouse_event((10,), "mouse press", 1, 0, 50, True)) def test_mouse_event_child_without_mouse_event(self) -> None: """A child widget missing mouse_event() triggers the same "not implementing Widget API" warning. See :meth:`Frame._check_widget_subclass` for the same pattern elsewhere in the codebase. """ item = NotAWidget("n", b"*") with self.assertWarns(urwid.widget.PileWarning): pile = urwid.Pile([item]) with self.assertWarns(DeprecationWarning) as ctx: result = pile.mouse_event((4,), "mouse press", 1, 0, 0, True) self.assertFalse(result) self.assertIn("is not implementing Widget API", str(ctx.warning)) def test_keypress_child_changes_contents(self) -> None: """An unhandled key moves the focus from the contents the focused child left behind.""" with self.subTest("Items before the focus removed"): edits = [urwid.Edit("", text) for text in "abc"] pile = urwid.Pile([*edits, ParentMutatingText("d", lambda: pile.contents.__delitem__(slice(0, 2)))]) pile.focus_position = 3 self.assertIsNone(pile.keypress((10,), "up")) self.assertIs(edits[2], pile.focus) with self.subTest("All items removed"): pile = urwid.Pile( [urwid.Edit("", "a"), ParentMutatingText("b", lambda: pile.contents.__delitem__(slice(None)))] ) pile.focus_position = 1 self.assertEqual("up", pile.keypress((10,), "up")) def test_keypress_new_focus_changes_contents_while_placing_cursor(self) -> None: """The cursor placement keeps addressing the newly focused child after it changes the contents.""" target = ParentMutatingText( "b\nc\nd", lambda: pile.contents.insert(0, (urwid.Text(""), pile.options())), accept_cursor=False, ) pile = urwid.Pile([urwid.Edit("", "a"), target]) self.assertIsNone(pile.keypress((10,), "down")) self.assertIs(target, pile.focus) def test_move_cursor_to_coords_child_changes_contents(self) -> None: """The focus does not move to a position the child's contents change made stale.""" pile = urwid.Pile([urwid.Edit("", "a"), ParentMutatingText("b", lambda: pile.contents.__delitem__(0))]) self.assertFalse(pile.move_cursor_to_coords((10,), 0, 1)) self.assertEqual(0, pile.focus_position) urwid-urwid-69907e4/tests/widgets/test_popup.py000066400000000000000000000175111526073734300216700ustar00rootroot00000000000000from __future__ import annotations import unittest import urwid class ThingWithAPopUp(urwid.PopUpLauncher[urwid.SolidFill]): def __init__(self) -> None: super().__init__(urwid.SolidFill("x")) def create_pop_up(self) -> urwid.Filler[urwid.Edit]: return urwid.Filler(urwid.Edit("", "pop up contents")) def get_pop_up_parameters(self) -> dict: return {"left": 1, "top": 1, "overlay_width": 10, "overlay_height": 3} class PopUpLauncherTest(unittest.TestCase): def test_open_close(self) -> None: launcher = ThingWithAPopUp() self.assertIsNone(launcher._pop_up_widget) launcher.open_pop_up() self.assertIsInstance(launcher._pop_up_widget, urwid.Filler) launcher.close_pop_up() self.assertIsNone(launcher._pop_up_widget) def test_render_without_pop_up(self) -> None: launcher = ThingWithAPopUp() canv = launcher.render((4, 2)) self.assertEqual(canv.get_pop_up(), None) def test_render_with_pop_up(self) -> None: launcher = ThingWithAPopUp() launcher.open_pop_up() canv = launcher.render((4, 2)) pop_up = canv.get_pop_up() self.assertIsNotNone(pop_up) left, top, (widget, overlay_width, overlay_height) = pop_up self.assertEqual((left, top), (1, 1)) self.assertEqual((overlay_width, overlay_height), (10, 3)) self.assertIs(widget, launcher._pop_up_widget) class PopUpTargetTest(unittest.TestCase): def test_render_without_pop_up(self) -> None: launcher = ThingWithAPopUp() target = urwid.PopUpTarget(launcher) canv = target.render((10, 5)) self.assertEqual(b"".join(canv.text), b"x" * 10 * 5) self.assertIs(target._current_widget, launcher) self.assertEqual(target._pop_up_levels, []) def test_render_with_pop_up_creates_overlay(self) -> None: launcher = ThingWithAPopUp() target = urwid.PopUpTarget(launcher) launcher.open_pop_up() target.render((10, 5)) self.assertIsInstance(target._current_widget, urwid.Overlay) self.assertEqual([w for w, _overlay in target._pop_up_levels], [launcher._pop_up_widget]) # Render again with the same pop-up widget open: hits the # set_overlay_parameters() branch instead of creating a new Overlay. overlay = target._current_widget target.render((10, 5)) self.assertIs(target._current_widget, overlay) def test_render_pop_up_closed_reverts_to_original(self) -> None: launcher = ThingWithAPopUp() target = urwid.PopUpTarget(launcher) launcher.open_pop_up() target.render((10, 5)) self.assertIsInstance(target._current_widget, urwid.Overlay) launcher.close_pop_up() target.render((10, 5)) self.assertIs(target._current_widget, launcher) self.assertEqual(target._pop_up_levels, []) def test_get_cursor_coords_no_support(self) -> None: launcher = ThingWithAPopUp() target = urwid.PopUpTarget(launcher) with self.assertRaises(TypeError): target.get_cursor_coords((10, 5)) def test_get_cursor_coords_with_pop_up(self) -> None: launcher = ThingWithAPopUp() target = urwid.PopUpTarget(launcher) launcher.open_pop_up() # Overlay delegates get_cursor_coords to its top widget when it has focus. self.assertIsInstance(target.get_cursor_coords((10, 5)), tuple) def test_get_pref_col_no_support(self) -> None: launcher = ThingWithAPopUp() target = urwid.PopUpTarget(launcher) with self.assertRaises(TypeError): target.get_pref_col((10, 5)) def test_get_pref_col_with_pop_up_still_unsupported(self) -> None: # Overlay itself has no get_pref_col method, so opening a pop-up # does not change the outcome. launcher = ThingWithAPopUp() target = urwid.PopUpTarget(launcher) launcher.open_pop_up() with self.assertRaises(TypeError): target.get_pref_col((10, 5)) def test_move_cursor_to_coords_no_support(self) -> None: launcher = ThingWithAPopUp() target = urwid.PopUpTarget(launcher) with self.assertRaises(TypeError): target.move_cursor_to_coords((10, 5), 0, 0) def test_move_cursor_to_coords_with_pop_up_still_unsupported(self) -> None: # Overlay itself has no move_cursor_to_coords method, so opening a # pop-up does not change the outcome. launcher = ThingWithAPopUp() target = urwid.PopUpTarget(launcher) launcher.open_pop_up() with self.assertRaises(TypeError): target.move_cursor_to_coords((10, 5), 0, 0) def test_keypress(self) -> None: launcher = ThingWithAPopUp() target = urwid.PopUpTarget(launcher) self.assertEqual(target.keypress((10, 5), "q"), "q") def test_mouse_event(self) -> None: launcher = ThingWithAPopUp() target = urwid.PopUpTarget(launcher) result = target.mouse_event((10, 5), "mouse press", 1, 0, 0, True) self.assertFalse(result) def test_pack(self) -> None: launcher = ThingWithAPopUp() target = urwid.PopUpTarget(launcher) self.assertEqual(target.pack((10, 5)), (10, 5)) class NestedDialog(urwid.PopUpLauncher[urwid.Filler[urwid.Edit]]): """A pop-up launcher whose own pop-up is another :class:`NestedDialog`. Used to reproduce https://github.com/urwid/urwid/issues/469: a pop-up launcher opened from inside an already-open pop-up. """ def __init__(self, level: int) -> None: self.level = level super().__init__(urwid.Filler(urwid.Edit("", f"level {level}"))) def create_pop_up(self) -> NestedDialog: return NestedDialog(self.level + 1) def get_pop_up_parameters(self) -> dict: return {"left": self.level * 12, "top": 0, "overlay_width": 10, "overlay_height": 3} class NestedPopUpTest(unittest.TestCase): def test_nested_pop_up_is_rendered(self) -> None: outer = NestedDialog(0) target = urwid.PopUpTarget(outer) outer.open_pop_up() inner = outer._pop_up_widget inner.open_pop_up() # nest a second pop-up inside the first one canv = target.render((25, 10)) text = b"\n".join(canv.text).decode() self.assertIn("level 1", text) self.assertIn("level 2", text) self.assertEqual(len(target._pop_up_levels), 2) def test_closing_inner_pop_up_reverts_to_outer(self) -> None: outer = NestedDialog(0) target = urwid.PopUpTarget(outer) outer.open_pop_up() inner = outer._pop_up_widget inner.open_pop_up() target.render((25, 10)) inner.close_pop_up() canv = target.render((25, 10)) text = b"\n".join(canv.text).decode() self.assertIn("level 1", text) self.assertNotIn("level 2", text) self.assertEqual(len(target._pop_up_levels), 1) def test_closing_outer_pop_up_closes_everything(self) -> None: outer = NestedDialog(0) target = urwid.PopUpTarget(outer) outer.open_pop_up() inner = outer._pop_up_widget inner.open_pop_up() target.render((25, 10)) outer.close_pop_up() canv = target.render((25, 10)) text = b"\n".join(canv.text).decode() self.assertNotIn("level 1", text) self.assertNotIn("level 2", text) self.assertEqual(target._pop_up_levels, []) def test_keypress_reaches_innermost_pop_up(self) -> None: outer = NestedDialog(0) target = urwid.PopUpTarget(outer) outer.open_pop_up() inner = outer._pop_up_widget inner.open_pop_up() innermost = inner._pop_up_widget target.keypress((20, 10), "x") self.assertEqual(innermost.original_widget.original_widget.edit_text, "level 2x") if __name__ == "__main__": unittest.main() urwid-urwid-69907e4/tests/widgets/test_progress_bar.py000066400000000000000000000047571526073734300232250ustar00rootroot00000000000000"""Tests for :class:`urwid.ProgressBar`.""" from __future__ import annotations import math import unittest import urwid EMPTY = [("normal", 10)] FULL = [("complete", 10)] class ProgressBarOutOfRangeTest(unittest.TestCase): """Check that a progress bar renders and labels values outside ``[0, done]`` without raising.""" def assertBar(self, current: float, done: float, attrs: list[tuple[str, int]], text: str) -> None: """Assert the label and the attribute runs of a 10-column bar, with and without smoothing.""" for satt in (None, "smooth"): with self.subTest(satt=satt): pb = urwid.ProgressBar("normal", "complete", current, done, satt) # type: ignore[arg-type] self.assertEqual(text, pb.get_text()) (row,) = pb.render((10,)).content() self.assertEqual(attrs, [(attr, len(seg)) for attr, _cs, seg in row]) def test_in_range(self) -> None: """Draw half of the bar complete for half of `done`.""" self.assertBar(50, 100, [("complete", 5), ("normal", 5)], "50 %") def test_done_zero(self) -> None: """Draw an empty bar for a zero `done`.""" self.assertBar(50, 0, EMPTY, "0 %") def test_done_negative(self) -> None: """Draw an empty bar for a negative `done`.""" self.assertBar(50, -100, EMPTY, "0 %") def test_nan(self) -> None: """Draw an empty bar when `current` or `done` is NaN.""" self.assertBar(math.nan, 100, EMPTY, "0 %") self.assertBar(50, math.nan, EMPTY, "0 %") def test_infinite(self) -> None: """Clamp an infinite `current` like any out-of-range value; draw an empty bar for an infinite `done`.""" self.assertBar(math.inf, 100, FULL, "100 %") self.assertBar(-math.inf, 100, EMPTY, "0 %") self.assertBar(50, math.inf, EMPTY, "0 %") def test_negative_current(self) -> None: """Draw an empty bar for a negative `current`.""" self.assertBar(-50, 100, EMPTY, "0 %") def test_current_above_done(self) -> None: """Draw a full bar when `current` exceeds `done`.""" self.assertBar(150, 100, FULL, "100 %") def test_current_too_large_for_float(self) -> None: """Draw a full or empty bar when ``current / done`` does not fit in a float.""" self.assertBar(10**400, 1, FULL, "100 %") self.assertBar(-(10**400), 1, EMPTY, "0 %") self.assertBar(1.0, 10**400, EMPTY, "0 %") self.assertBar(math.inf, 10**400, FULL, "100 %") urwid-urwid-69907e4/tests/widgets/test_scrollable.py000066400000000000000000001203641526073734300226500ustar00rootroot00000000000000from __future__ import annotations import string import typing import unittest import urwid from urwid.widget.scrollable import ScrollableError if typing.TYPE_CHECKING: from collections.abc import Iterable LGPL_HEADER = """ Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA """ class TestScrollable(unittest.TestCase): def test_basic(self): """Test basic init and scroll.""" long_content = urwid.Text(LGPL_HEADER) reduced_size = (80, 5) content_size = long_content.pack() widget = urwid.Scrollable(long_content) self.assertEqual(frozenset((urwid.BOX,)), widget.sizing()) long_content_canvas = long_content.render((reduced_size[0],)) cropped_content_canvas = urwid.CompositeCanvas(long_content_canvas) cropped_content_canvas.trim_end(content_size[1] - reduced_size[1]) top_decoded = cropped_content_canvas.decoded_text self.assertEqual( top_decoded, widget.render(reduced_size).decoded_text, ) for key_1, key_2 in (("down", "up"), ("page down", "page up"), ("end", "home")): widget.keypress(reduced_size, key_1) self.assertNotEqual(top_decoded, widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, key_2) self.assertEqual(top_decoded, widget.render(reduced_size).decoded_text) def test_negative(self): with self.assertRaises(ValueError): urwid.Scrollable(urwid.SolidFill(" ")) def test_selectable(self): widget = urwid.Scrollable(urwid.Text("hi")) self.assertTrue(widget.selectable()) def test_render_pads_height_and_trims_width(self): """A fixed widget wider than the viewport but short enough to fit vertically is padded to the full height, has its scroll position reset, and is trimmed horizontally. """ widget = urwid.Scrollable(urwid.BigText("12", urwid.HalfBlockHeavy6x5Font())) widget.set_scrollpos(3) size = (8, 10) canv = widget.render(size) self.assertEqual(0, widget.get_scrollpos()) self.assertEqual(10, canv.rows()) self.assertEqual(8, canv.cols()) def test_set_scrollpos_negative_counts_from_bottom(self): content = urwid.Text("\n".join(string.ascii_letters)) size = (5, 5) widget = urwid.Scrollable(content) widget.set_scrollpos(-3) canv = widget.render(size) reference = urwid.Scrollable(urwid.Text("\n".join(string.ascii_letters))) reference.set_scrollpos(45) reference_canv = reference.render(size) self.assertEqual(45, widget.get_scrollpos()) self.assertEqual(reference_canv.decoded_text, canv.decoded_text) def test_cursor_hidden_when_scrolled_out_of_view(self): """Scrolling the cursor's row out of the visible canvas hides it (unrelated to automove_cursor).""" edit = urwid.Edit("", "\n".join(f"line{index}" for index in range(10)), multiline=True) edit.set_edit_pos(0) widget = urwid.Scrollable(edit) size = (10, 3) canv = widget.render(size, True) self.assertIsNotNone(canv.cursor) widget.set_scrollpos(5) canv = widget.render(size, True) self.assertIsNone(canv.cursor) def test_keypress_unhandled_returns_key(self): widget = urwid.Scrollable(urwid.Text("hello")) size = (10, 3) widget.render(size) self.assertEqual("x", widget.keypress(size, "x")) def test_keypress_forwarded_follows_cursor_out_of_view(self): """Keys are forwarded to a widget whose cursor is visible; the cursor-position-preservation logic in render()/keypress() (separate from automove_cursor) then scrolls to keep it in view. """ edit = urwid.Edit("", "\n".join(f"line{index}" for index in range(20)), multiline=True) edit.set_edit_pos(0) widget = urwid.Scrollable(edit) size = (10, 3) canv = widget.render(size, True) self.assertEqual((0, 0), canv.cursor) for _ in range(3): self.assertIsNone(widget.keypress(size, "down")) widget.render(size, True) self.assertGreater(widget.get_scrollpos(), 0) self.assertEqual((0, 3), edit.get_cursor_coords((10,))) # Now move the cursor back up past the top of the current viewport: trim_top follows it up too. for _ in range(3): self.assertIsNone(widget.keypress(size, "up")) widget.render(size, True) self.assertEqual(0, widget.get_scrollpos()) self.assertEqual((0, 0), edit.get_cursor_coords((10,))) def test_keypress_forwarded_without_get_cursor_coords(self): """force_forward_keypress works with a widget that has no get_cursor_coords method, and an unhandled forwarded key falls through to Scrollable's own scroll handling. """ class NoCursorCoordsWidget: def sizing(self): return frozenset((urwid.FLOW,)) def selectable(self): return True def rows(self, size, focus=False): return 20 def render(self, size, focus=False): return urwid.SolidCanvas(" ", size[0], 20) def keypress(self, size, key): return key # never handles anything widget = urwid.Scrollable(NoCursorCoordsWidget(), force_forward_keypress=True) size = (10, 3) widget.render(size, True) self.assertEqual(0, widget.get_scrollpos()) widget.keypress(size, "down") widget.render(size, True) self.assertGreater(widget.get_scrollpos(), 0) def test_mouse_event_without_ow_support(self): class NoMouseWidget: def sizing(self): return frozenset((urwid.FLOW,)) def selectable(self): return False def render(self, size, focus=False): return urwid.SolidCanvas(" ", size[0], 1) def keypress(self, size, key): return key widget = urwid.Scrollable(NoMouseWidget()) self.assertFalse(widget.mouse_event((10, 3), "mouse press", 1, 0, 0, False)) def test_get_original_widget_size_raises_for_unsupported_sizing(self): class MutableSizingText(urwid.Text): forced_sizing: frozenset | None = None def sizing(self): if self.forced_sizing is not None: return self.forced_sizing return super().sizing() inner = MutableSizingText("hi") widget = urwid.Scrollable(inner) inner.forced_sizing = frozenset() with self.assertRaises(ScrollableError): widget.render((10, 3)) def test_rows_max_raises_for_unsupported_sizing(self): # sizing() is queried twice inside rows_max(): once (indirectly, via _get_original_widget_size()) # to compute ow_size, and again to pick the FIXED/FLOW branch. Reporting FLOW for the first two # calls (construction, then the ow_size lookup) and nothing for the third exercises the `else` # branch inside rows_max() itself, rather than the one in _get_original_widget_size(). class TwoStageSizingText(urwid.Text): def __init__(self, markup): super().__init__(markup) self.calls = 0 def sizing(self): self.calls += 1 if self.calls <= 2: return frozenset((urwid.FLOW,)) return frozenset() inner = TwoStageSizingText("hi") widget = urwid.Scrollable(inner) with self.assertRaises(ScrollableError): widget.rows_max((10, 3)) def test_rows_max_flow_widget(self): # Edit is FLOW-only (unlike Text, which is also FIXED), so this exercises the # `Sizing.FLOW in sizing` branch of rows_max() rather than the `Sizing.FIXED` one. widget = urwid.Scrollable(urwid.Edit("", "a\nb\nc", multiline=True)) self.assertEqual(3, widget.rows_max((10, 2))) self.assertEqual(3, widget.rows_max()) # cached value, no size given class TestScrollBarScrollable(unittest.TestCase): def test_basic(self): """Test basic init and scroll. Unlike `Scrollable`, `ScrollBar` can be also scrolled with a mouse wheel. """ long_content = urwid.Text(LGPL_HEADER) reduced_size = (40, 5) widget = urwid.ScrollBar(urwid.Scrollable(long_content)) self.assertEqual(frozenset((urwid.BOX,)), widget.sizing()) top_position_rendered = ( " █", "Copyright (C) ", " ", "This library is free software; you can ", "redistribute it and/or ", ) pos_1_down_rendered = ( "Copyright (C) ", " █", "This library is free software; you can ", "redistribute it and/or ", "modify it under the terms of the GNU ", ) self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "down") self.assertEqual(pos_1_down_rendered, widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "up") self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "page down") self.assertEqual( ( "redistribute it and/or ", "modify it under the terms of the GNU █", "Lesser General Public ", "License as published by the Free ", "Software Foundation; either ", ), widget.render(reduced_size).decoded_text, ) widget.keypress(reduced_size, "page up") self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "end") self.assertEqual( ( "not, write to the Free Software ", "Foundation, Inc., 51 Franklin Street, ", "Fifth Floor, Boston, MA 02110-1301 ", "USA ", " █", ), widget.render(reduced_size).decoded_text, ) widget.keypress(reduced_size, "home") self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 5, 1, 1, False)) self.assertEqual(pos_1_down_rendered, widget.render(reduced_size).decoded_text) self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 4, 1, 1, False)) self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) def test_mouse_left_click_scrollbar(self): """Left click on the rendered scrollbar jumps to the clicked position.""" content = urwid.Text("\n".join(string.ascii_letters)) # 52 single-char lines reduced_size = (3, 5) widget = urwid.ScrollBar(urwid.Scrollable(content)) scrollable = widget.original_widget scrollbar_col = reduced_size[0] - 1 # right side scrollbar self.assertEqual(0, scrollable.get_scrollpos()) # posmax == 52 - 5 == 47, thumb_height == 1, thumb travel == 5 - 1 == 4. # Clicking the bottom row moves past the end and is clamped to posmax. self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 1, scrollbar_col, 4, False)) self.assertEqual(47, scrollable.get_scrollpos()) # Clicking the top row scrolls back to the top. self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 1, scrollbar_col, 0, False)) self.assertEqual(0, scrollable.get_scrollpos()) # Intermediate rows map proportionally: round(row * posmax / travel). self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 1, scrollbar_col, 1, False)) self.assertEqual(12, scrollable.get_scrollpos()) self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 1, scrollbar_col, 3, False)) self.assertEqual(35, scrollable.get_scrollpos()) def test_mouse_left_click_content_ignored(self): """Left click outside of the scrollbar columns does not scroll.""" content = urwid.Text("\n".join(string.ascii_letters)) reduced_size = (3, 5) widget = urwid.ScrollBar(urwid.Scrollable(content)) scrollable = widget.original_widget self.assertEqual(0, scrollable.get_scrollpos()) # Column 0 belongs to the wrapped content, not the scrollbar. self.assertFalse(widget.mouse_event(reduced_size, "mouse press", 1, 0, 4, False)) self.assertEqual(0, scrollable.get_scrollpos()) def test_mouse_left_click_left_side_scrollbar(self): """Left click detection honours a left-aligned scrollbar.""" content = urwid.Text("\n".join(string.ascii_letters)) reduced_size = (3, 5) widget = urwid.ScrollBar(urwid.Scrollable(content), side="left") scrollable = widget.original_widget self.assertEqual(0, scrollable.get_scrollpos()) # The scrollbar occupies column 0 when aligned to the left. self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 1, 0, 4, False)) self.assertEqual(47, scrollable.get_scrollpos()) # A click on the content columns is ignored. self.assertFalse(widget.mouse_event(reduced_size, "mouse press", 1, 1, 0, False)) self.assertEqual(47, scrollable.get_scrollpos()) def test_mouse_left_click_without_scrollbar(self): """Without a rendered scrollbar a left click is not handled.""" content = urwid.Text("a\nb") # fits into the available height reduced_size = (3, 5) widget = urwid.ScrollBar(urwid.Scrollable(content)) scrollable = widget.original_widget self.assertFalse(widget.mouse_event(reduced_size, "mouse press", 1, 2, 2, False)) self.assertEqual(0, scrollable.get_scrollpos()) def test_mouse_left_click_scrollbar_over_selectable_content(self): """The scrollbar keeps its columns even when the wrapped widget consumes every click.""" clicked: list[int] = [] buttons = [] for index in range(20): button = urwid.Button(f"b{index}") urwid.connect_signal(button, "click", lambda _button, idx=index: clicked.append(idx)) buttons.append(button) reduced_size = (14, 5) widget = urwid.ScrollBar(urwid.Scrollable(urwid.Pile(buttons))) scrollable = widget.original_widget scrollbar_col = reduced_size[0] - 1 widget.render(reduced_size, True) self.assertEqual(0, scrollable.get_scrollpos()) # Button.mouse_event answers any left press without looking at the column, # so delegating first would turn a click on the scrollbar into a button press. self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 1, scrollbar_col, 4, True)) self.assertEqual([], clicked) self.assertEqual(15, scrollable.get_scrollpos()) # A click on the content columns still reaches the buttons. self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 1, 2, 0, True)) self.assertEqual([15], clicked) def test_mouse_left_click_left_side_scrollbar_over_selectable_content(self): """The same protection applies to a left-aligned scrollbar.""" clicked: list[int] = [] buttons = [] for index in range(20): button = urwid.Button(f"b{index}") urwid.connect_signal(button, "click", lambda _button, idx=index: clicked.append(idx)) buttons.append(button) reduced_size = (14, 5) widget = urwid.ScrollBar(urwid.Scrollable(urwid.Pile(buttons)), side="left") scrollable = widget.original_widget widget.render(reduced_size, True) self.assertEqual(0, scrollable.get_scrollpos()) self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 1, 0, 4, True)) self.assertEqual([], clicked) self.assertEqual(15, scrollable.get_scrollpos()) def test_mouse_event_translates_column_for_left_side_scrollbar(self): """A left-aligned scrollbar shifts the wrapped widget, so the column has to be shifted back.""" class ColumnRecorder(urwid.Text): def __init__(self, markup) -> None: super().__init__(markup) self.seen: list[tuple[int, int]] = [] def selectable(self) -> bool: return True def mouse_event(self, size, event, button, col, row, focus) -> bool: self.seen.append((col, row)) return False content = ColumnRecorder("\n".join(f"line{index}" for index in range(20))) reduced_size = (10, 5) widget = urwid.ScrollBar(urwid.Scrollable(content), side="left") sb_width = widget.scrollbar_width widget.render(reduced_size, True) # Screen column ``sb_width`` is the first column the wrapped widget draws into. widget.mouse_event(reduced_size, "mouse press", 1, sb_width, 0, True) self.assertEqual([(0, 0)], content.seen) # The last screen column maps to the last column the wrapped widget owns. content.seen.clear() widget.mouse_event(reduced_size, "mouse press", 1, reduced_size[0] - 1, 0, True) self.assertEqual([(reduced_size[0] - 1 - sb_width, 0)], content.seen) def test_alt_symbols(self): long_content = urwid.Text(LGPL_HEADER) reduced_size = (40, 5) widget = urwid.ScrollBar( urwid.Scrollable(long_content), trough_char=urwid.ScrollBar.Symbols.LITE_SHADE, ) self.assertEqual( ( " █", "Copyright (C) ░", " ░", "This library is free software; you can ░", "redistribute it and/or ░", ), widget.render(reduced_size).decoded_text, ) def test_fixed(self): """Test with fixed wrapped widget.""" widget = urwid.ScrollBar( urwid.Scrollable(urwid.BigText("1", urwid.HalfBlockHeavy6x5Font())), trough_char=urwid.ScrollBar.Symbols.LITE_SHADE, thumb_char=urwid.ScrollBar.Symbols.DARK_SHADE, ) reduced_size = (8, 3) self.assertEqual( ( " �█▌ ▓", " ▀█▌ ▓", " █▌ ░", ), widget.render(reduced_size).decoded_text, ) widget.keypress(reduced_size, "page down") self.assertEqual( ( " █▌ ░", " █▌ ▓", " ███▌ ▓", ), widget.render(reduced_size).decoded_text, ) def test_negative(self): with self.assertRaises(ValueError): urwid.ScrollBar(urwid.Text(" ")) with self.assertRaises(TypeError): urwid.ScrollBar(urwid.SolidFill(" ")) def test_no_scrollbar(self): """If widget fit without scroll - no scrollbar needed""" widget = urwid.ScrollBar( urwid.Scrollable(urwid.BigText("1", urwid.HalfBlockHeavy6x5Font())), trough_char=urwid.ScrollBar.Symbols.LITE_SHADE, thumb_char=urwid.ScrollBar.Symbols.DARK_SHADE, ) reduced_size = (8, 5) self.assertEqual( ( " �█▌ ", " ▀█▌ ", " █▌ ", " █▌ ", " ███▌ ", ), widget.render(reduced_size).decoded_text, ) def test_selectable(self): widget = urwid.ScrollBar(urwid.Scrollable(urwid.Text("hi"))) self.assertTrue(widget.selectable()) def test_scrollbar_side_property(self): widget = urwid.ScrollBar(urwid.Scrollable(urwid.Text("hi"))) self.assertEqual("right", widget.scrollbar_side) widget.scrollbar_side = "left" self.assertEqual("left", widget.scrollbar_side) with self.assertRaises(ValueError): widget.scrollbar_side = "top" def test_scrolling_base_widget_missing(self): widget = urwid.ScrollBar(urwid.Scrollable(urwid.Text("hi"))) widget.original_widget = urwid.Text("hi") # no longer wraps anything SupportsScroll-compatible with self.assertRaises(ScrollableError): _ = widget.scrolling_base_widget def test_mouse_click_scrollbar_zero_thumb_travel(self): """When the thumb fills the whole trough, any click maps to position 0.""" content = urwid.Text("a\nb\nc") widget = urwid.ScrollBar(urwid.Scrollable(content)) scrollable = widget.original_widget size = (5, 1) scrollable.set_scrollpos(1) widget.render(size) layout = widget._scrollbar_layout(size, False) self.assertEqual(0, size[1] - layout.thumb_height) self.assertTrue(widget.mouse_event(size, "mouse press", 1, size[0] - 1, 0, False)) self.assertEqual(0, scrollable.get_scrollpos()) def test_mouse_wheel_on_scrollbar_column_still_scrolls(self): """A wheel event landing on the scrollbar's own column still scrolls the content.""" content = urwid.Text("\n".join(string.ascii_letters)) widget = urwid.ScrollBar(urwid.Scrollable(content)) scrollable = widget.original_widget size = (5, 5) scrollbar_col = size[0] - 1 scrollable.set_scrollpos(5) widget.render(size) self.assertTrue(widget.mouse_event(size, "mouse press", 4, scrollbar_col, 0, False)) self.assertEqual(4, scrollable.get_scrollpos()) self.assertTrue(widget.mouse_event(size, "mouse press", 5, scrollbar_col, 0, False)) self.assertEqual(5, scrollable.get_scrollpos()) def test_scrollbar_layout_relative_scroll_all_visible_falls_back_to_absolute(self): """When relative scrolling reports every row visible, the absolute geometry is still computed.""" class RelativeScrollFake(urwid.Widget): _sizing = frozenset((urwid.BOX,)) def __init__(self, length, visible_amount, pos, rows_max_value): self._length = length self._visible_amount = visible_amount self._pos = pos self._rows_max_value = rows_max_value def sizing(self): return self._sizing def selectable(self): return False def render(self, size, focus=False): return urwid.SolidCanvas(" ", size[0], size[1]) def __len__(self): return self._length def require_relative_scroll(self, size, focus=False): return True def get_first_visible_pos(self, size, focus=False): return self._pos def get_visible_amount(self, size, focus=False): return self._visible_amount def get_scrollpos(self, size=None, focus=False): return self._pos def set_scrollpos(self, position): self._pos = position def rows_max(self, size=None, focus=False): return self._rows_max_value fake = RelativeScrollFake(length=5, visible_amount=5, pos=0, rows_max_value=10) widget = urwid.ScrollBar(fake) layout = widget._scrollbar_layout((10, 5), False) self.assertIsNotNone(layout) self.assertEqual(5, layout.posmax) self.assertEqual(2, layout.thumb_height) class TestScrollBarListBox(unittest.TestCase): def test_relative_non_selectable(self): widget = urwid.ScrollBar( urwid.ListBox(urwid.SimpleListWalker(urwid.Text(line) for line in LGPL_HEADER.splitlines())) ) reduced_size = (40, 5) top_position_rendered = ( " █", "Copyright (C) ", " ", "This library is free software; you can ", "redistribute it and/or ", ) pos_1_down_rendered = ( "Copyright (C) ", " █", "This library is free software; you can ", "redistribute it and/or ", "modify it under the terms of the GNU ", ) self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "down") self.assertEqual(pos_1_down_rendered, widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "up") self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "page down") self.assertEqual( ( "modify it under the terms of the GNU ", "Lesser General Public █", "License as published by the Free ", "Software Foundation; either ", "version 2.1 of the License, or (at your ", ), widget.render(reduced_size).decoded_text, ) widget.keypress(reduced_size, "page up") self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "end") self.assertEqual( ( "License along with this library; if ", "not, write to the Free Software ", "Foundation, Inc., 51 Franklin Street, ", "Fifth Floor, Boston, MA 02110-1301 ", "USA █", ), widget.render(reduced_size).decoded_text, ) widget.keypress(reduced_size, "home") self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 5, 1, 1, False)) self.assertEqual(pos_1_down_rendered, widget.render(reduced_size).decoded_text) self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 4, 1, 1, False)) self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) def test_empty(self): """Empty widget should be correctly rendered.""" widget = urwid.ScrollBar(urwid.ListBox(urwid.SimpleListWalker(()))) reduced_size = (10, 5) self.assertEqual( ( " ", " ", " ", " ", " ", ), widget.render(reduced_size).decoded_text, ) def test_minimal_height(self): """If we have only 1 line render height and thumb position in the middle - do not render top.""" widget = urwid.ScrollBar( urwid.ListBox( ( urwid.CheckBox("A"), urwid.CheckBox("B"), urwid.CheckBox("C"), ) ) ) reduced_size = (7, 1) self.assertEqual(("[ ] A █",), widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "down") self.assertEqual(("[ ] B █",), widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "down") self.assertEqual(("[ ] C █",), widget.render(reduced_size).decoded_text) def test_shade_symbols_around_listbox(self) -> None: """Thumb/trough characters used by list windows.""" widget = urwid.ScrollBar( urwid.ListBox(urwid.SimpleListWalker([urwid.Text(f"item {idx}") for idx in range(6)])), thumb_char=urwid.ScrollBar.Symbols.DARK_SHADE, trough_char=urwid.ScrollBar.Symbols.LITE_SHADE, ) self.assertEqual("▓", urwid.ScrollBar.Symbols.DARK_SHADE) self.assertEqual("░", urwid.ScrollBar.Symbols.LITE_SHADE) self.assertEqual( ( "item 0 ▓", "item 1 ▓", "item 2 ░", ), widget.render((8, 3)).decoded_text, ) def trivial_AttrMap(widget): return urwid.AttrMap(widget, {}) class TestScrollableAttrMap(unittest.TestCase): def test_basic(self): """Test basic init and scroll.""" long_content = urwid.Text(LGPL_HEADER) reduced_size = (80, 5) content_size = long_content.pack() widget = urwid.Scrollable(trivial_AttrMap(long_content)) self.assertEqual(frozenset((urwid.BOX,)), widget.sizing()) cropped_content_canvas = urwid.CompositeCanvas(long_content.render((reduced_size[0],))) cropped_content_canvas.trim_end(content_size[1] - reduced_size[1]) top_decoded = cropped_content_canvas.decoded_text self.assertEqual( top_decoded, widget.render(reduced_size).decoded_text, ) for key_1, key_2 in (("down", "up"), ("page down", "page up"), ("end", "home")): widget.keypress(reduced_size, key_1) self.assertNotEqual(top_decoded, widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, key_2) self.assertEqual(top_decoded, widget.render(reduced_size).decoded_text) def test_negative(self): with self.assertRaises(ValueError): urwid.Scrollable(trivial_AttrMap(urwid.SolidFill(" "))) class TestScrollBarAttrMap(unittest.TestCase): def test_basic(self): """Test basic init and scroll. Unlike `Scrollable`, `ScrollBar` can be also scrolled with a mouse wheel. """ long_content = urwid.Text(LGPL_HEADER) reduced_size = (40, 5) widget = urwid.ScrollBar(urwid.Scrollable(trivial_AttrMap(long_content))) self.assertEqual(frozenset((urwid.BOX,)), widget.sizing()) top_position_rendered = ( " █", "Copyright (C) ", " ", "This library is free software; you can ", "redistribute it and/or ", ) pos_1_down_rendered = ( "Copyright (C) ", " █", "This library is free software; you can ", "redistribute it and/or ", "modify it under the terms of the GNU ", ) self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "down") self.assertEqual(pos_1_down_rendered, widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "up") self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "page down") self.assertEqual( ( "redistribute it and/or ", "modify it under the terms of the GNU █", "Lesser General Public ", "License as published by the Free ", "Software Foundation; either ", ), widget.render(reduced_size).decoded_text, ) widget.keypress(reduced_size, "page up") self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "end") self.assertEqual( ( "not, write to the Free Software ", "Foundation, Inc., 51 Franklin Street, ", "Fifth Floor, Boston, MA 02110-1301 ", "USA ", " █", ), widget.render(reduced_size).decoded_text, ) widget.keypress(reduced_size, "home") self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 5, 1, 1, False)) self.assertEqual(pos_1_down_rendered, widget.render(reduced_size).decoded_text) self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 4, 1, 1, False)) self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) class TestScrollBarListBoxAttrMap(unittest.TestCase): def test_relative_non_selectable(self): widget = urwid.ScrollBar( trivial_AttrMap( urwid.ListBox(urwid.SimpleListWalker(urwid.Text(line) for line in LGPL_HEADER.splitlines())) ) ) reduced_size = (40, 5) top_position_rendered = ( " █", "Copyright (C) ", " ", "This library is free software; you can ", "redistribute it and/or ", ) pos_1_down_rendered = ( "Copyright (C) ", " █", "This library is free software; you can ", "redistribute it and/or ", "modify it under the terms of the GNU ", ) self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "down") self.assertEqual(pos_1_down_rendered, widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "up") self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "page down") self.assertEqual( ( "modify it under the terms of the GNU ", "Lesser General Public █", "License as published by the Free ", "Software Foundation; either ", "version 2.1 of the License, or (at your ", ), widget.render(reduced_size).decoded_text, ) widget.keypress(reduced_size, "page up") self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "end") self.assertEqual( ( "License along with this library; if ", "not, write to the Free Software ", "Foundation, Inc., 51 Franklin Street, ", "Fifth Floor, Boston, MA 02110-1301 ", "USA █", ), widget.render(reduced_size).decoded_text, ) widget.keypress(reduced_size, "home") self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 5, 1, 1, False)) self.assertEqual(pos_1_down_rendered, widget.render(reduced_size).decoded_text) self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 4, 1, 1, False)) self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) def test_large_non_selectable(self): top = urwid.Text("\n".join(string.ascii_letters)) bottom = urwid.Text("\n".join(string.digits)) widget = urwid.ScrollBar(urwid.ListBox(urwid.SimpleListWalker((top, bottom)))) reduced_size = (3, 5) top_position_rendered = ("a █", "b ", "c ", "d ", "e ") pos_1_down_rendered = ("b ", "c █", "d ", "e ", "f ") self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "down") self.assertEqual(pos_1_down_rendered, widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "up") self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "page down") self.assertEqual( ("f ", "g █", "h ", "i ", "j "), widget.render(reduced_size).decoded_text, ) widget.keypress(reduced_size, "page up") self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 5, 1, 1, False)) self.assertEqual(pos_1_down_rendered, widget.render(reduced_size).decoded_text) self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 4, 1, 1, False)) self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text) def test_large_selectable(self): """Input is handled by LineBox and wrapped widgets.""" top = urwid.Edit("\n".join(string.ascii_letters)) bottom = urwid.IntEdit("\n".join(string.digits)) widget = urwid.ScrollBar(urwid.ListBox(urwid.SimpleListWalker((top, bottom)))) reduced_size = (3, 10) top_position_rendered = ("a █", "b █", "c ", "d ", "e ", "f ", "g ", "h ", "i ", "j ") pos_1_down_rendered = ("0 ", "1 ", "2 ", "3 ", "4 ", "5 ", "6 ", "7 ", "8 █", "9 █") self.assertEqual((top_position_rendered), widget.render(reduced_size).decoded_text) widget.keypress(reduced_size, "down") self.assertEqual(pos_1_down_rendered, widget.render(reduced_size).decoded_text) def test_hinted_len(self): class HintedWalker(urwid.ListWalker): def __init__(self, items: Iterable[str]) -> None: self.items: tuple[str] = tuple(items) self.focus = 0 self.requested_numbers: set[int] = set() def __length_hint__(self) -> int: return len(self.items) def __getitem__(self, item: int) -> urwid.Text: self.requested_numbers.add(item) return urwid.Text(self.items[item]) def set_focus(self, item: int) -> None: self.focus = item def next_position(self, position: int) -> int: if position + 1 < len(self.items): return position + 1 raise IndexError def prev_position(self, position: int) -> int: if position - 1 >= 0: return position - 1 raise IndexError widget = urwid.ScrollBar(urwid.ListBox(HintedWalker((f"Line {idx:02}") for idx in range(1, 51)))) size = (10, 10) widget.original_widget.focus_position = 19 self.assertEqual( ( "Line 16 ", "Line 17 ", "Line 18 ", "Line 19 █", "Line 20 █", "Line 21 ", "Line 22 ", "Line 23 ", "Line 24 ", "Line 25 ", ), widget.render(size).decoded_text, ) self.assertNotIn( 30, widget.original_widget.body.requested_numbers, "Requested index out of range [0, last shown]. This means not relative scroll bar built.", ) urwid-urwid-69907e4/tests/widgets/test_tree.py000066400000000000000000000774051526073734300214740ustar00rootroot00000000000000from __future__ import annotations import sys import typing import unittest import urwid from urwid import TreeNode if typing.TYPE_CHECKING: from collections.abc import Hashable, Iterable class SelfRegisteringParent(urwid.ParentNode): def __init__( self, value: str, parent: SelfRegisteringParent | None = None, key: Hashable = None, depth: int | None = None, children: Iterable[SelfRegisteringChild | SelfRegisteringParent] = (), ) -> None: super().__init__(value, parent, key, depth) if parent: parent.set_child_node(key, self) self._child_keys = [] for child in children: key = child.get_key() self._children[key] = child self._child_keys.append(key) child.set_parent(self) def load_child_keys(self) -> list[Hashable]: return list(self._children) def set_child_node(self, key: Hashable, node: TreeNode) -> None: super().set_child_node(key, node) self._child_keys = self.load_child_keys() def set_parent(self, parent: SelfRegisteringParent) -> None: self._parent = parent class SelfRegisteringChild(urwid.TreeNode): def __init__( self, value: str, parent: SelfRegisteringParent | None = None, key: Hashable | None = None, depth: int | None = None, ) -> None: super().__init__(value, parent, key, depth) if parent: parent.set_child_node(key, self) def set_parent(self, parent: SelfRegisteringParent) -> None: self._parent = parent class ComparisonCountingKey(str): """String key counting the equality comparisons made against it.""" __slots__ = () comparisons = 0 def __eq__(self, other: object) -> bool: """Count the comparison, then compare as a string.""" ComparisonCountingKey.comparisons += 1 return super().__eq__(other) __hash__ = str.__hash__ class ReloadingParent(urwid.ParentNode): """Parent whose child keys change on every reload.""" def __init__(self, value: str, *, child_count: int) -> None: """Create a parent with ``child_count`` changing child keys and one key present in every generation.""" super().__init__(value, key=value) self.child_count = child_count self.generation = 0 def load_child_keys(self) -> list[Hashable]: """Return the keys of the next generation.""" self.generation += 1 return [*(f"{self.generation}-{idx}" for idx in range(self.child_count)), "kept"] def load_child_node(self, key: Hashable) -> TreeNode: """Create a leaf node for ``key``.""" return TreeNode(key, parent=self, key=key) class CyclicParent(urwid.ParentNode): """Parent whose only child is ``self.child``, assigned after construction to close a cycle.""" def __init__(self, value: str, *, parent: CyclicParent | None = None) -> None: """Create the node with no child yet and no ``has_children`` calls counted.""" super().__init__(value, parent=parent, key=value) self.child: CyclicParent | None = None self.has_children_calls = 0 def load_child_keys(self) -> list[Hashable]: """Return the key of the only child.""" return [self.child.get_key()] def load_child_node(self, key: Hashable) -> TreeNode: """Return the only child.""" return self.child def has_children(self) -> bool: """Count the call, failing once a descent has clearly stopped making progress.""" self.has_children_calls += 1 if self.has_children_calls > 100: raise RuntimeError("cycle guard: the descent did not stop") return super().has_children() class CountingParentLink(urwid.TreeNode): """Leaf whose parent link counts its reads, failing once a walk has clearly stopped making progress.""" def __init__(self, value: str) -> None: """Create the node with no reads of its parent link counted.""" self.parent_reads = 0 self.link: urwid.TreeNode | None = None super().__init__(value, key=value) @property def _parent(self) -> urwid.TreeNode | None: """Return the parent link, counting the read.""" self.parent_reads += 1 if self.parent_reads > 100: raise RuntimeError("cycle guard: the walk did not stop") return self.link @_parent.setter def _parent(self, value: urwid.TreeNode | None) -> None: self.link = value class FixedDepthParent(SelfRegisteringParent): """Parent that reports a fixed depth through an overridden ``get_depth``.""" def get_depth(self) -> int: """Return a depth no parent link implies.""" return 10 class NoDescendantWidget(urwid.TreeWidget): """Widget that reports no last descendant through an overridden ``last_child``.""" def last_child(self) -> None: """Hide the children of this widget from a descent.""" class NoDescendantParent(SelfRegisteringParent): """Parent rendered with a :class:`NoDescendantWidget`.""" def load_widget(self) -> NoDescendantWidget: """Create the overriding widget.""" return NoDescendantWidget(self) def build_chain(depth: int) -> SelfRegisteringParent: """Build a single-branch tree ``depth`` levels deep bottom-up, so no node knows its depth yet.""" node: SelfRegisteringParent | SelfRegisteringChild = SelfRegisteringChild("leaf", key="leaf") for level in range(depth, 0, -1): node = SelfRegisteringParent(f"level_{level}", key=f"{level}/", children=(node,)) return node class TestTree(unittest.TestCase): def test_basic(self): root = SelfRegisteringParent( "root", key="/", children=(SelfRegisteringChild(f"child_{idx}", key=str(idx)) for idx in range(1, 4)), ) widget = urwid.TreeListBox(urwid.TreeWalker(root)) size = (15, 5) expanded = ( "- /: root ", " 1: child_1 ", " 2: child_2 ", " 3: child_3 ", " ", ) collapsed = ( "+ /: root ", *(" " * size[0] for _ in range(size[1] - 1)), ) self.assertEqual(expanded, widget.render(size).decoded_text) widget.keypress(size, "-") self.assertEqual(collapsed, widget.render(size).decoded_text) widget.keypress(size, "+") self.assertEqual(expanded, widget.render(size).decoded_text) widget.keypress(size, "down") self.assertIs(root, widget.focus_position) widget.keypress(size, "-") self.assertEqual(collapsed, widget.render(size).decoded_text) widget.keypress(size, "right") self.assertEqual(expanded, widget.render(size).decoded_text) def test_nested_behavior(self): root = SelfRegisteringParent( "root", key="/", children=( SelfRegisteringParent( f"nested_{idx}", key=f"{idx}/", children=(SelfRegisteringChild(f"child_{idx}{cidx}", key=str(cidx)) for cidx in range(1, 4)), ) for idx in range(1, 4) ), ) widget = urwid.TreeListBox(urwid.TreeWalker(root)) size = (18, 13) expanded = ( "- /: root ", " - 1/: nested_1 ", " 1: child_11 ", " 2: child_12 ", " 3: child_13 ", " - 2/: nested_2 ", " 1: child_21 ", " 2: child_22 ", " 3: child_23 ", " - 3/: nested_3 ", " 1: child_31 ", " 2: child_32 ", " 3: child_33 ", ) collapsed = ( "+ /: root ", *(" " * size[0] for _ in range(size[1] - 1)), ) self.assertEqual(expanded, widget.render(size).decoded_text) widget.keypress(size, "-") self.assertEqual(collapsed, widget.render(size).decoded_text) widget.keypress(size, "+") self.assertEqual(expanded, widget.render(size).decoded_text) widget.keypress(size, "down") widget.keypress(size, "-") self.assertEqual( ( "- /: root ", " + 1/: nested_1 ", " - 2/: nested_2 ", " 1: child_21 ", " 2: child_22 ", " 3: child_23 ", " - 3/: nested_3 ", " 1: child_31 ", " 2: child_32 ", " 3: child_33 ", " ", " ", " ", ), widget.render(size).decoded_text, ) widget.keypress(size, "right") self.assertEqual(expanded, widget.render(size).decoded_text) widget.keypress(size, "left") widget.keypress(size, "-") self.assertEqual(collapsed, widget.render(size).decoded_text) widget.keypress(size, "+") widget.keypress(size, "page down") widget.keypress(size, "-") self.assertEqual( ( "- /: root ", " - 1/: nested_1 ", " 1: child_11 ", " 2: child_12 ", " 3: child_13 ", " - 2/: nested_2 ", " 1: child_21 ", " 2: child_22 ", " 3: child_23 ", " + 3/: nested_3 ", " ", " ", " ", ), widget.render(size).decoded_text, ) def test_deep_nested_collapse_expand(self): root = SelfRegisteringParent( "root", key="/", children=( SelfRegisteringParent( f"nested_{top_idx}", key=f"{top_idx}/", children=( SelfRegisteringParent( f"nested_{top_idx}{first_idx}", key=f"{first_idx}/", children=( SelfRegisteringChild(f"child_{top_idx}{first_idx}{last_idx}", key=str(last_idx)) for last_idx in range(1, 3) ), ) for first_idx in range(1, 3) ), ) for top_idx in range(1, 3) ), ) widget = urwid.TreeListBox(urwid.TreeWalker(root)) size = (21, 15) expanded = ( "- /: root ", " - 1/: nested_1 ", " - 1/: nested_11", " 1: child_111", " 2: child_112", " - 2/: nested_12", " 1: child_121", " 2: child_122", " - 2/: nested_2 ", " - 1/: nested_21", " 1: child_211", " 2: child_212", " - 2/: nested_22", " 1: child_221", " 2: child_222", ) collapsed = ( "+ /: root ", *(" " * size[0] for _ in range(size[1] - 1)), ) collapsed_last = ( "- /: root ", " - 1/: nested_1 ", " - 1/: nested_11", " 1: child_111", " 2: child_112", " - 2/: nested_12", " 1: child_121", " 2: child_122", " - 2/: nested_2 ", " - 1/: nested_21", " 1: child_211", " 2: child_212", " + 2/: nested_22", " ", " ", ) self.assertEqual(expanded, widget.render(size).decoded_text) widget.keypress(size, "page down") widget.keypress(size, "-") self.assertEqual(collapsed_last, widget.render(size).decoded_text) widget.keypress(size, "left") widget.keypress(size, "-") self.assertEqual( ( "- /: root ", " - 1/: nested_1 ", " - 1/: nested_11", " 1: child_111", " 2: child_112", " - 2/: nested_12", " 1: child_121", " 2: child_122", " + 2/: nested_2 ", " ", " ", " ", " ", " ", " ", ), widget.render(size).decoded_text, ) widget.keypress(size, "right") self.assertEqual(collapsed_last, widget.render(size).decoded_text) widget.keypress(size, "home") widget.keypress(size, "-") self.assertEqual(collapsed, widget.render(size).decoded_text) class TestTreeNode(unittest.TestCase): def setUp(self) -> None: self.root = SelfRegisteringParent( "root", key="/", children=(SelfRegisteringChild(f"child_{idx}", key=str(idx)) for idx in range(1, 4)), ) def test_root_properties(self): self.assertTrue(self.root.is_root()) self.assertEqual(0, self.root.get_depth()) self.assertEqual("/", self.root.get_key()) self.assertEqual("root", self.root.get_value()) self.assertIsNone(self.root.get_index()) self.assertIsNone(self.root.get_parent()) self.assertIs(self.root, self.root.get_root()) self.assertIsNone(self.root.next_sibling()) self.assertIsNone(self.root.prev_sibling()) def test_child_properties(self): child = self.root.get_child_node("2") self.assertFalse(child.is_root()) self.assertEqual(1, child.get_depth()) self.assertEqual(1, child.get_index()) self.assertIs(self.root, child.get_parent()) self.assertIs(self.root, child.get_root()) def test_siblings(self): first, second, last = (self.root.get_child_node(key) for key in ("1", "2", "3")) self.assertIs(second, first.next_sibling()) self.assertIs(last, second.next_sibling()) self.assertIsNone(last.next_sibling()) self.assertIs(second, last.prev_sibling()) self.assertIs(first, second.prev_sibling()) self.assertIsNone(first.prev_sibling()) def test_set_key(self): child = self.root.get_child_node("2") child.set_key("two") self.assertEqual("two", child.get_key()) def test_change_key(self): child = self.root.get_child_node("2") child.change_key("two") self.assertEqual("two", child.get_key()) self.assertIs(child, self.root.get_child_node("two")) self.assertEqual(["1", "3", "two"], list(self.root.get_child_keys(reload=True))) def test_change_key_in_use(self): child = self.root.get_child_node("2") with self.assertRaises(urwid.TreeWidgetError) as ctx: child.change_key("3") self.assertEqual("3 is already in use", str(ctx.exception)) self.assertEqual("2", child.get_key()) def test_get_child_index_unknown_key(self): with self.assertRaises(urwid.TreeWidgetError) as ctx: self.root.get_child_index("no_such_key") self.assertIn("Can't find key no_such_key in ParentNode /", str(ctx.exception)) def test_get_child_widget(self): child = self.root.get_child_node("1") widget = self.root.get_child_widget("1") self.assertIs(child.get_widget(), widget) self.assertIs(child, widget.get_node()) def test_get_widget_reload(self): child = self.root.get_child_node("1") widget = child.get_widget() self.assertIs(widget, child.get_widget()) self.assertIsNot(widget, child.get_widget(reload=True)) def test_set_child_node(self): extra = SelfRegisteringChild("child_4", key="4") self.root.set_child_node("4", extra) self.assertIs(extra, self.root.get_child_node("4")) self.assertIs(self.root.get_last_child(), extra) def test_first_and_last_child(self): self.assertIs(self.root.get_child_node("1"), self.root.get_first_child()) self.assertIs(self.root.get_child_node("3"), self.root.get_last_child()) self.assertTrue(self.root.has_children()) def test_no_children(self): empty = SelfRegisteringParent("empty", key="empty") self.assertFalse(empty.has_children()) self.assertEqual([], list(empty.get_child_keys())) def test_get_depth_deeper_than_recursion_limit(self): """A depth computed bottom-up does not recurse once per level.""" depth = sys.getrecursionlimit() * 2 node = build_chain(depth) while isinstance(node, urwid.ParentNode): node = node.get_last_child() self.assertEqual(depth, node.get_depth()) self.assertEqual(depth - 1, node.get_parent().get_depth()) def test_get_depth_parent_cycle(self): """A cycle in the parent links raises instead of walking it forever.""" first, second = CountingParentLink("first"), CountingParentLink("second") first._parent, second._parent = second, first with self.assertRaises(urwid.TreeWidgetError): first.get_depth() def test_get_depth_uses_overriding_ancestor(self): """The walk asks an ancestor that overrides get_depth for its depth.""" parent = FixedDepthParent("parent", key="parent") child = SelfRegisteringChild("child", parent=parent, key="child") self.assertEqual(11, child.get_depth()) def test_get_child_index_does_not_scan_keys(self): """Walking all siblings looks each key up instead of scanning the key list.""" count = 2000 root = SelfRegisteringParent( "root", key="/", children=(SelfRegisteringChild(f"child_{idx}", key=ComparisonCountingKey(idx)) for idx in range(count)), ) ComparisonCountingKey.comparisons = 0 node = root.get_first_child() visited = 1 while (node := node.next_sibling()) is not None: visited += 1 self.assertEqual(count, visited) # Scanning the key list for each sibling makes about count**2 / 2 comparisons. self.assertLess(ComparisonCountingKey.comparisons, count * 4) def test_get_child_index_after_reload(self): """Indexes follow the reloaded key list.""" parent = ReloadingParent("parent", child_count=3) self.assertEqual(1, parent.get_child_index("1-1")) parent.get_child_keys(reload=True) self.assertEqual(2, parent.get_child_index("2-2")) self.assertEqual(3, parent.get_child_index("kept")) with self.assertRaises(urwid.TreeWidgetError): parent.get_child_index("1-1") def test_reload_drops_nodes_of_removed_keys(self): """A reload keeps only the cached nodes whose keys are still listed.""" parent = ReloadingParent("parent", child_count=100) kept = parent.get_child_node("kept") for _ in range(50): for key in parent.get_child_keys(reload=True): parent.get_child_node(key) self.assertEqual(set(parent.get_child_keys()), set(parent._children)) self.assertIs(kept, parent.get_child_node("kept")) def test_load_parent_not_implemented(self): orphan = TreeNode("orphan", key="orphan", depth=1) with self.assertRaises(urwid.TreeWidgetError) as ctx: orphan.get_parent() self.assertEqual("virtual function. Implement in subclass", str(ctx.exception)) def test_load_child_keys_not_implemented(self): node = urwid.ParentNode("node", key="node") with self.assertRaises(urwid.TreeWidgetError) as ctx: node.get_child_keys() self.assertEqual("virtual function. Implement in subclass", str(ctx.exception)) def test_load_child_node_not_implemented(self): node = urwid.ParentNode("node", key="node") with self.assertRaises(urwid.TreeWidgetError) as ctx: node.get_child_node("child") self.assertEqual("virtual function. Implement in subclass", str(ctx.exception)) class TestTreeWidget(unittest.TestCase): def setUp(self) -> None: self.root = SelfRegisteringParent( "root", key="/", children=( SelfRegisteringParent( f"nested_{idx}", key=f"{idx}/", children=(SelfRegisteringChild(f"child_{idx}{cidx}", key=str(cidx)) for cidx in range(1, 3)), ) for idx in range(1, 3) ), ) def test_leaf_widget(self): widget = self.root.get_child_node("1/").get_child_node("1").get_widget() self.assertTrue(widget.is_leaf) self.assertFalse(widget.selectable()) self.assertIsNone(widget.first_child()) self.assertIsNone(widget.last_child()) self.assertEqual(6, widget.get_indent_cols()) self.assertEqual((" 1: child_11 ",), widget.render((20,)).decoded_text) def test_leaf_ignores_input(self): widget = self.root.get_child_node("1/").get_child_node("1").get_widget() self.assertEqual("+", widget.keypress((20,), "+")) self.assertEqual("-", widget.keypress((20,), "-")) self.assertFalse(widget.mouse_event((20,), "mouse press", 1, 6, 0, True)) def test_parent_widget(self): widget = self.root.get_widget() self.assertFalse(widget.is_leaf) self.assertTrue(widget.selectable()) self.assertTrue(widget.expanded) self.assertEqual(0, widget.get_indent_cols()) self.assertEqual("/: root", widget.get_display_text()) def test_inner_widget_is_cached(self): widget = self.root.get_widget() inner = widget.get_inner_widget() self.assertIs(inner, widget.get_inner_widget()) self.assertEqual("/: root", inner.text) def test_parent_without_children(self): widget = SelfRegisteringParent("empty", key="empty").get_widget() self.assertFalse(widget.is_leaf) self.assertTrue(widget.expanded) self.assertIsNone(widget.first_child()) self.assertIsNone(widget.last_child()) def test_children_of_collapsed_widget(self): widget = self.root.get_widget() widget.expanded = False widget.update_expanded_icon() self.assertIsNone(widget.first_child()) self.assertIsNone(widget.last_child()) self.assertEqual(("+ /: root ",), widget.render((20,)).decoded_text) def test_first_and_last_child(self): widget = self.root.get_widget() nested_1 = self.root.get_child_node("1/") deepest = self.root.get_child_node("2/").get_child_node("2") self.assertIs(nested_1.get_widget(), widget.first_child()) self.assertIs(deepest.get_widget(), widget.last_child()) def test_mouse_event_toggles_expanded(self): widget = self.root.get_child_node("1/").get_widget() self.assertTrue(widget.mouse_event((20,), "mouse press", 1, 3, 0, True)) self.assertFalse(widget.expanded) self.assertEqual((" + 1/: nested_1 ",), widget.render((20,)).decoded_text) self.assertTrue(widget.mouse_event((20,), "mouse press", 1, 3, 0, True)) self.assertTrue(widget.expanded) self.assertEqual((" - 1/: nested_1 ",), widget.render((20,)).decoded_text) def test_mouse_event_ignored(self): widget = self.root.get_child_node("1/").get_widget() self.assertFalse(widget.mouse_event((20,), "mouse release", 1, 3, 0, True)) self.assertFalse(widget.mouse_event((20,), "mouse press", 3, 3, 0, True)) self.assertFalse(widget.mouse_event((20,), "mouse press", 1, 10, 0, True)) self.assertTrue(widget.expanded) def test_inorder_traversal(self): keys = [] widget = self.root.get_widget() while widget is not None: keys.append(widget.get_node().get_key()) widget = widget.next_inorder() self.assertEqual(["/", "1/", "1", "2", "2/", "1", "2"], keys) backwards = [] widget = self.root.get_child_node("2/").get_child_node("2").get_widget() while widget is not None: backwards.append(widget.get_node().get_key()) widget = widget.prev_inorder() self.assertEqual(["2", "1", "2/", "2", "1", "1/", "/"], backwards) def test_next_inorder_skips_collapsed_children(self): widget = self.root.get_child_node("1/").get_widget() widget.expanded = False next_widget = widget.next_inorder() self.assertIsNotNone(next_widget) self.assertEqual("2/", next_widget.get_node().get_key()) def test_next_inorder_inconsistent_depth(self): broken = SelfRegisteringChild("broken", parent=self.root, key="broken", depth=5) with self.assertRaises(ValueError): broken.get_widget().next_inorder() class TestLastChild(unittest.TestCase): """Descent to the last visible descendant.""" def test_cycle(self): """A node that is its own descendant raises instead of descending forever.""" first = CyclicParent("first") second = CyclicParent("second", parent=first) first.child, second.child = second, first with self.assertRaises(urwid.TreeWidgetError): first.get_widget().last_child() def test_overriding_descendant_widget(self): """A descendant widget that overrides last_child decides its own last descendant.""" hiding = NoDescendantParent("hiding", key="h/", children=(SelfRegisteringChild("hidden", key="x"),)) root = SelfRegisteringParent("root", key="/", children=(hiding,)) self.assertIs(hiding.get_widget(), root.get_widget().last_child()) class TestDeepTree(unittest.TestCase): """A subtree deeper than the recursion limit, built bottom-up.""" def setUp(self) -> None: """Put the deep subtree before a leaf under the root.""" self.depth = sys.getrecursionlimit() * 2 self.deep = build_chain(self.depth) self.root = SelfRegisteringParent("root", key="/", children=(self.deep, SelfRegisteringChild("tail", key="t"))) self.widget = urwid.TreeListBox(urwid.TreeWalker(self.root)) self.size = (20, 3) def test_last_child_deeper_than_recursion_limit(self): """The last descendant is found without recursing once per level.""" last = self.deep.get_widget().last_child() self.assertEqual("leaf", last.get_node().get_key()) self.assertEqual(self.depth + 1, last.get_node().get_depth()) def test_render_below_deep_subtree(self): """Rendering with the deep subtree above the focus fills the rows above it.""" tail = self.root.get_child_node("t") self.widget.change_focus(self.size, tail, 2) self.widget.render(self.size, focus=True) self.assertIs(tail, self.widget.focus_position) self.assertEqual("leaf", self.widget.body.get_prev(tail)[1].get_key()) def test_focus_end_inside_deep_subtree(self): """The End key moves the focus to the deepest last node.""" deep = build_chain(self.depth) root = SelfRegisteringParent("root", key="/", children=(SelfRegisteringChild("head", key="h"), deep)) widget = urwid.TreeListBox(urwid.TreeWalker(root)) widget.keypress(self.size, "end") self.assertEqual("leaf", widget.focus_position.get_key()) class TestTreeWalker(unittest.TestCase): def setUp(self) -> None: self.root = SelfRegisteringParent( "root", key="/", children=(SelfRegisteringChild(f"child_{idx}", key=str(idx)) for idx in range(1, 4)), ) self.walker = urwid.TreeWalker(self.root) def test_get_focus(self): widget, position = self.walker.get_focus() self.assertIs(self.root, position) self.assertIs(self.root.get_widget(), widget) def test_set_focus(self): modified = [] urwid.connect_signal(self.walker, "modified", lambda: modified.append(True)) child = self.root.get_child_node("2") self.walker.set_focus(child) self.assertIs(child, self.walker.focus) self.assertEqual([True], modified) def test_get_next(self): widget, position = self.walker.get_next(self.root) self.assertIs(self.root.get_child_node("1"), position) self.assertIs(position.get_widget(), widget) def test_get_next_at_end(self): self.assertEqual((None, None), self.walker.get_next(self.root.get_child_node("3"))) def test_get_prev(self): widget, position = self.walker.get_prev(self.root.get_child_node("2")) self.assertIs(self.root.get_child_node("1"), position) self.assertIs(position.get_widget(), widget) def test_get_prev_at_start(self): self.assertEqual((None, None), self.walker.get_prev(self.root)) class TestTreeListBox(unittest.TestCase): def setUp(self) -> None: self.root = SelfRegisteringParent( "root", key="/", children=( SelfRegisteringParent( f"nested_{idx}", key=f"{idx}/", children=(SelfRegisteringChild(f"child_{idx}{cidx}", key=str(cidx)) for cidx in range(1, 3)), ) for idx in range(1, 3) ), ) self.widget = urwid.TreeListBox(urwid.TreeWalker(self.root)) self.size = (21, 7) def test_collapse_focus_parent_from_leaf(self): self.widget.set_focus(self.root.get_child_node("1/").get_child_node("1")) self.widget.keypress(self.size, "-") self.assertIs(self.root.get_child_node("1/"), self.widget.focus_position) self.assertEqual( ( "- /: root ", " + 1/: nested_1 ", " - 2/: nested_2 ", " 1: child_21 ", " 2: child_22 ", " ", " ", ), self.widget.render(self.size).decoded_text, ) def test_collapse_focus_parent_keeps_root(self): self.widget.collapse_focus_parent(self.size) self.assertIs(self.root, self.widget.focus_position) self.assertTrue(self.root.get_widget().expanded) def test_left_on_root_keeps_focus(self): self.assertIsNone(self.widget.keypress(self.size, "left")) self.assertIs(self.root, self.widget.focus_position) def test_unhandled_input_passes_other_keys_through(self): self.assertEqual("f1", self.widget.unhandled_input(self.size, "f1")) def test_focus_end(self): self.widget.keypress(self.size, "end") self.assertIs(self.root.get_child_node("2/").get_child_node("2"), self.widget.focus_position) def test_focus_home(self): self.widget.keypress(self.size, "end") self.widget.keypress(self.size, "home") self.assertIs(self.root, self.widget.focus_position) def test_focus_end_without_children(self): widget = urwid.TreeListBox(urwid.TreeWalker(SelfRegisteringParent("empty", key="empty"))) widget.keypress(self.size, "end") self.assertEqual("empty", widget.focus_position.get_key()) def test_move_focus_to_parent_from_offscreen_child(self): size = (21, 3) self.widget.keypress(size, "end") self.assertEqual("2", self.widget.focus_position.get_key()) self.widget.keypress(size, "left") self.assertIs(self.root.get_child_node("2/"), self.widget.focus_position) urwid-urwid-69907e4/tests/widgets/test_widget.py000066400000000000000000000323621526073734300220110ustar00rootroot00000000000000from __future__ import annotations import unittest import warnings import urwid from urwid.util import set_temporary_encoding class TextTest(unittest.TestCase): def setUp(self): self.t = urwid.Text("I walk the\ncity in the night") def test1_wrap(self): expected = [t.encode("iso8859-1") for t in ("I walk the", "city in ", "the night ")] got = self.t.render((10,))._text assert got == expected, f"got: {got!r} expected: {expected!r}" def test2_left(self): self.t.set_align_mode("left") expected = [t.encode("iso8859-1") for t in ("I walk the ", "city in the night ")] got = self.t.render((18,))._text assert got == expected, f"got: {got!r} expected: {expected!r}" def test3_right(self): self.t.set_align_mode("right") expected = [t.encode("iso8859-1") for t in (" I walk the", " city in the night")] got = self.t.render((18,))._text assert got == expected, f"got: {got!r} expected: {expected!r}" def test4_center(self): self.t.set_align_mode("center") expected = [t.encode("iso8859-1") for t in (" I walk the ", " city in the night")] got = self.t.render((18,))._text assert got == expected, f"got: {got!r} expected: {expected!r}" def test5_encode_error(self): with set_temporary_encoding("ascii"): expected = [b"? "] got = urwid.Text("û").render((3,))._text assert got == expected, f"got: {got!r} expected: {expected!r}" def test_empty_markup_segment_middle(self) -> None: """An empty markup segment in the middle must not truncate the rendered row.""" widget = urwid.Text([("a", "X" * 10), ("b", ""), ("c", " END")]) self.assertEqual(("XXXXXXXXXX END", [("a", 10), ("b", 0), ("c", 4)]), widget.get_text()) canvas = widget.render(()) self.assertEqual([b"XXXXXXXXXX END"], canvas.text) self.assertEqual(("XXXXXXXXXX END",), canvas.decoded_text) def test_empty_markup_segment_start(self) -> None: """An empty markup segment at the start must keep the whole rendered row.""" widget = urwid.Text([("b", ""), ("a", "X" * 10), ("c", " END")]) self.assertEqual(("XXXXXXXXXX END", [("b", 0), ("a", 10), ("c", 4)]), widget.get_text()) canvas = widget.render(()) self.assertEqual([b"XXXXXXXXXX END"], canvas.text) self.assertEqual(("XXXXXXXXXX END",), canvas.decoded_text) def test_empty_markup_segment_end(self) -> None: """An empty markup segment at the end must keep the whole rendered row.""" widget = urwid.Text([("a", "X" * 10), ("c", " END"), ("b", "")]) self.assertEqual(("XXXXXXXXXX END", [("a", 10), ("c", 4), ("b", 0)]), widget.get_text()) canvas = widget.render(()) self.assertEqual([b"XXXXXXXXXX END"], canvas.text) self.assertEqual(("XXXXXXXXXX END",), canvas.decoded_text) def test_multiple_empty_markup_segments(self) -> None: """Several consecutive empty markup segments must not drop following content.""" widget = urwid.Text([("a", "AB"), ("b", ""), ("c", ""), ("d", "CD")]) self.assertEqual(("ABCD", [("a", 2), ("b", 0), ("c", 0), ("d", 2)]), widget.get_text()) canvas = widget.render(()) self.assertEqual([b"ABCD"], canvas.text) self.assertEqual(("ABCD",), canvas.decoded_text) def test_empty_markup_segment_render_content(self) -> None: """The canvas content rows must expose the display attribute of each segment.""" widget = urwid.Text([("a", "X" * 10), ("b", ""), ("c", " END")]) content = list(widget.render(()).content()) attrs = [attr for row in content for attr, _cs, _text in row] rendered = b"".join(text for row in content for _attr, _cs, text in row) self.assertEqual(b"XXXXXXXXXX END", rendered) self.assertIn("a", attrs) self.assertIn("c", attrs) def test_markup_tuple_and_set_text(self) -> None: widget = urwid.Text(("title", "Hello")) self.assertEqual(("Hello", [("title", 5)]), widget.get_text()) self.assertEqual("Hello", widget.text) self.assertEqual((5, 1), widget.pack(())) widget.set_text([("a", "AB"), ("b", "CD")]) self.assertEqual(("ABCD", [("a", 2), ("b", 2)]), widget.get_text()) def test_pack_does_not_wrap_after_tab(self) -> None: """A line holding a tab packs to its unwrapped width, not to a layout wrapped after the tab stop.""" for widget, expected in ( (urwid.Text("Tab\there:"), (13, 1)), (urwid.Text(b"Tab\there:"), (13, 1)), (urwid.Text("a\tb\tcccccccc"), (24, 1)), (urwid.Text("abcdef\t中中", wrap="any"), (12, 1)), (urwid.Text("a\tbbbbbbbbbbbb", layout=urwid.StandardTextLayout(tab_stops=(12,))), (24, 1)), ): with self.subTest(text=widget.text, wrap=widget.wrap): self.assertEqual(expected, widget.pack(())) def test_control_characters_are_drawn_in_the_columns_they_are_measured_as(self) -> None: """A control character or escape sequence measured as zero columns is left out of the canvas. The raw display then draws the text as wide as the layout measured it, and the neighbour stays in place. """ for encoding in ("utf-8", "latin-1"): for text, drawn in ( ("\x1b[31mred", "red"), ("a\rb", "ab"), ("a\x07b", "ab"), ("\x9b31m", "31m"), ("a\x1b]0;title\x07b", "ab"), ("a\x7fb", "ab"), ): with self.subTest(encoding=encoding, text=text), set_temporary_encoding(encoding): widget = urwid.Text(text) columns = urwid.Columns([(urwid.PACK, widget), urwid.Text("XYZ")]) screen = urwid.display.raw.Screen() written: list[str] = [] screen.write = written.append screen.flush = lambda: None screen._started = True canvas = columns.render((20,)) screen.draw_screen((20, canvas.rows()), canvas) self.assertEqual((len(drawn), 1), widget.pack()) self.assertEqual([f"{drawn}XYZ".ljust(20).encode()], canvas.text) self.assertIn(f"{drawn}XYZ\x1b[K", "".join(written)) self.assertEqual(text, widget.text) def test_utf8_bytes_leave_out_control_characters(self) -> None: """Bytes in UTF-8 are measured as decoded text, so an escape sequence in them takes no column either.""" self.assertEqual([b"ab"], urwid.Text(b"a\x1b[1mb").render(()).text) def test_narrow_bytes_keep_control_characters(self) -> None: """Bytes in a narrow encoding measure one column per byte, which the display's ``?`` takes as well.""" with set_temporary_encoding("latin-1"): self.assertEqual([b"a\x1b[1mb"], urwid.Text(b"a\x1b[1mb").render(()).text) def test_line_cut_inside_escape_sequence_draws_its_measured_columns(self) -> None: """A line cut by screen column counts ESC as zero columns and the rest of the sequence as text, and draws so.""" self.assertEqual([b"ab[31", b"mcd ", b"ef "], urwid.Text("ab\x1b[31mcd ef").render((5,)).text) def test_control_characters_keep_attributes_aligned(self) -> None: """Each attribute run covers the characters drawn for it, without the control characters left out.""" canvas = urwid.Text([("x", "a\x1b[1m"), ("y", "bc\x07"), "d"]).render((6,)) self.assertEqual([[("x", None, b"a"), ("y", None, b"bc"), (None, None, b"d ")]], list(canvas.content())) # the text OSC 66 displays ends the sequence, and its parameters may hold the same characters canvas = urwid.Text([("A", "\x1b]66;s=2;"), ("B", "2\x07x")]).render(()) self.assertEqual([[("B", None, b"2x")]], list(canvas.content())) def test_segment_neither_measure_fits_is_drawn_unchanged(self) -> None: """A segment no drop makes as wide as laid out is drawn as given, rather than raising or overflowing.""" for widget, width, expected in ( (urwid.Text(b"a\xc2\x9bb\xff"), 3, [b"a\xc2\x9bb\xff"]), # bytes that are not valid UTF-8 (urwid.Text("\u200da\x07\u4e2d", wrap="clip"), 1, [b"\xe2\x80\x8da\x07 "]), ): with self.subTest(text=widget.text), warnings.catch_warnings(): warnings.simplefilter("ignore", UnicodeWarning) canvas = widget.render((width,)) layout_widths = [ urwid.text_layout.line_width(urwid.text_layout.trim_line(line, widget.text, 0, width)) for line in widget.get_line_translation(width) ] self.assertEqual(expected, canvas.text) self.assertEqual([width], layout_widths) self.assertEqual(width, canvas.cols()) def test_layout_and_line_drawing_controls_unchanged(self) -> None: """LF and TAB are laid out, and SO and SI select the DEC line-drawing set, rather than being left out.""" self.assertEqual([b"a ", b"b c"], urwid.Text("a\nb\tc").render(()).text) with set_temporary_encoding("latin-1"): canvas = urwid.Text("a\x0eq\x0fb").render(()) self.assertEqual([b"aqb"], canvas.text) self.assertEqual([[None, "0", None]], [[cs for _, cs, _ in row] for row in canvas.content()]) class EditTest(unittest.TestCase): def setUp(self): self.t1 = urwid.Edit(b"", "blah blah") self.t2 = urwid.Edit(b"stuff:", "blah blah") self.t3 = urwid.Edit(b"junk:\n", "blah blah\n\nbloo", 1) self.t4 = urwid.Edit("better:") def ktest(self, e, key, expected, pos, desc): got = e.keypress((12,), key) assert got == expected, f"{desc}. got: {got!r} expected:{expected!r}" assert e.edit_pos == pos, f"{desc}. pos: {e.edit_pos!r} expected pos: {pos!r}" def test1_left(self): self.t1.set_edit_pos(0) self.ktest(self.t1, "left", "left", 0, "left at left edge") self.ktest(self.t2, "left", None, 8, "left within text") self.t3.set_edit_pos(10) self.ktest(self.t3, "left", None, 9, "left after newline") def test2_right(self): self.ktest(self.t1, "right", "right", 9, "right at right edge") self.t2.set_edit_pos(8) self.ktest(self.t2, "right", None, 9, "right at right edge-1") self.t3.set_edit_pos(0) self.t3.keypress((12,), "right") assert self.t3.get_pref_col((12,)) == 1 def test3_up(self): self.ktest(self.t1, "up", "up", 9, "up at top") self.t2.set_edit_pos(2) self.t2.keypress((12,), "left") assert self.t2.get_pref_col((12,)) == 7 self.ktest(self.t2, "up", "up", 1, "up at top again") assert self.t2.get_pref_col((12,)) == 7 self.t3.set_edit_pos(10) self.ktest(self.t3, "up", None, 0, "up at top+1") def test4_down(self): self.ktest(self.t1, "down", "down", 9, "down single line") self.t3.set_edit_pos(5) self.ktest(self.t3, "down", None, 10, "down line 1 to 2") self.ktest(self.t3, "down", None, 15, "down line 2 to 3") self.ktest(self.t3, "down", "down", 15, "down at bottom") def test_utf8_input(self): with set_temporary_encoding("utf-8"): self.t1.set_edit_text("") self.t1.keypress((12,), "û") self.assertEqual(self.t1.edit_text, "û".encode()) self.t4.keypress((12,), "û") self.assertEqual(self.t4.edit_text, "û") class EditRenderTest(unittest.TestCase): def rtest(self, w, expected_text, expected_cursor): expected_text = [t.encode("iso8859-1") for t in expected_text] get_cursor = w.get_cursor_coords((4,)) assert get_cursor == expected_cursor, f"got: {get_cursor!r} expected: {expected_cursor!r}" r = w.render((4,), focus=1) text = [t for a, cs, t in [ln[0] for ln in r.content()]] assert text == expected_text, f"got: {text!r} expected: {expected_text!r}" assert r.cursor == expected_cursor, f"got: {r.cursor!r} expected: {expected_cursor!r}" def test1_SpaceWrap(self): w = urwid.Edit("", "blah blah") w.set_edit_pos(0) self.rtest(w, ["blah", "blah"], (0, 0)) w.set_edit_pos(4) self.rtest(w, ["blah", "blah"], (3, 0)) w.set_edit_pos(5) self.rtest(w, ["blah", "blah"], (0, 1)) w.set_edit_pos(9) self.rtest(w, ["blah", "blah"], (3, 1)) def test2_ClipWrap(self): w = urwid.Edit("", "blah\nblargh", 1) w.set_wrap_mode("clip") w.set_edit_pos(0) self.rtest(w, ["blah", "blar"], (0, 0)) w.set_edit_pos(10) self.rtest(w, ["blah", "argh"], (3, 1)) w.set_align_mode("right") w.set_edit_pos(6) self.rtest(w, ["blah", "larg"], (0, 1)) def test3_AnyWrap(self): w = urwid.Edit("", "blah blah") w.set_wrap_mode("any") self.rtest(w, ["blah", " bla", "h "], (1, 2)) def test4_CursorNudge(self): w = urwid.Edit("", "hi", align="right") w.keypress((4,), "end") self.rtest(w, [" hi"], (3, 0)) w.keypress((4,), "left") self.rtest(w, [" hi"], (3, 0)) urwid-urwid-69907e4/tests/widgets/test_widget_decoration.py000066400000000000000000000035371526073734300242220ustar00rootroot00000000000000from __future__ import annotations import unittest import urwid class WidgetWrapTest(unittest.TestCase): def test_delegates_sizing_pack_and_render(self) -> None: wrapped = urwid.WidgetWrap(urwid.Text("hello")) self.assertEqual(frozenset((urwid.FLOW, urwid.FIXED)), wrapped.sizing()) self.assertFalse(wrapped.selectable()) self.assertEqual((5, 1), wrapped.pack(())) self.assertEqual([b"hello"], wrapped.render(()).text) def test_replace_wrapped_widget(self) -> None: wrap = urwid.WidgetWrap(urwid.Button("edit me")) self.assertTrue(wrap.selectable()) self.assertEqual("left", wrap.keypress((12,), "left")) wrap._w = urwid.Text("gone") self.assertFalse(wrap.selectable()) self.assertEqual([b"gone"], wrap.render(()).text) class WidgetPlaceholderTest(unittest.TestCase): def test_swap_original_widget(self) -> None: first = urwid.Text("first") second = urwid.Button("second") placeholder = urwid.WidgetPlaceholder(first) self.assertIs(first, placeholder.original_widget) self.assertIs(first, placeholder.base_widget) self.assertEqual([b"first"], placeholder.render(()).text) placeholder.original_widget = second self.assertIs(second, placeholder.original_widget) self.assertTrue(placeholder.selectable()) self.assertEqual([b"< second >"], placeholder.render(()).text) class WidgetDisableTest(unittest.TestCase): def test_blocks_selection_and_focus_render(self) -> None: button = urwid.Button("Ok") disabled = urwid.WidgetDisable(button) self.assertFalse(disabled.selectable()) self.assertEqual(button.sizing(), disabled.sizing()) self.assertEqual(button.pack(()), disabled.pack(())) self.assertIsNone(disabled.render((10,), focus=True).cursor) urwid-urwid-69907e4/tests/widgets/test_wimp.py000066400000000000000000000255021526073734300215000ustar00rootroot00000000000000from __future__ import annotations import unittest import urwid class SelectableIconTest(unittest.TestCase): def test_cursor_and_render(self) -> None: icon = urwid.SelectableIcon("[!]", cursor_position=1) self.assertTrue(icon.selectable()) self.assertEqual(frozenset((urwid.FLOW, urwid.FIXED)), icon.sizing()) self.assertEqual((3, 1), icon.pack(())) self.assertIsNone(icon.render((4,)).cursor) self.assertEqual((1, 0), icon.render((4,), focus=True).cursor) self.assertEqual([b"[!] "], icon.render((4,), focus=True).text) self.assertEqual("left", icon.keypress((4,), "left")) class ButtonTest(unittest.TestCase): def test_keyword_construction_and_pack(self) -> None: button = urwid.Button(label="label", align=urwid.CENTER) self.assertTrue(button.selectable()) self.assertEqual(frozenset((urwid.FLOW, urwid.FIXED)), button.sizing()) self.assertEqual("label", button.label) self.assertEqual((9, 1), button.pack()) self.assertEqual((9, 1), button.pack(())) self.assertEqual([b"< label >"], button.render(()).text) def test_flow_align(self) -> None: centered = urwid.Button("label", align=urwid.CENTER) right = urwid.Button("label", align=urwid.RIGHT) self.assertEqual(["< label >"], [line.decode() for line in centered.render((11,)).text]) self.assertEqual(["< label >"], [line.decode() for line in right.render((11,)).text]) def test_set_label(self) -> None: button = urwid.Button("Ok") button.set_label(("bright", "Yup")) self.assertEqual("Yup", button.label) self.assertEqual("Yup", button.get_label()) self.assertEqual([b"< Yup >"], button.render(()).text) def test_on_press_user_data(self) -> None: received: list[tuple[str, object]] = [] def on_press(btn: urwid.Button, user_data: object) -> None: received.append((btn.label, user_data)) button = urwid.Button("Go", on_press=on_press, user_data="payload") self.assertIsNone(button.keypress((10,), "enter")) self.assertIsNone(button.keypress((10,), " ")) self.assertEqual("left", button.keypress((10,), "left")) self.assertTrue(button.mouse_event((10,), "mouse press", 1, 1, 0, True)) self.assertFalse(button.mouse_event((10,), "mouse press", 2, 1, 0, True)) self.assertEqual([("Go", "payload"), ("Go", "payload"), ("Go", "payload")], received) def test_connect_click_signal(self) -> None: clicks: list[str] = [] button = urwid.Button("Ok") urwid.connect_signal(button, "click", lambda btn: clicks.append(btn.label)) button.keypress((8,), "enter") self.assertEqual(["Ok"], clicks) class CheckBoxTest(unittest.TestCase): def test_keyword_construction(self) -> None: checkbox = urwid.CheckBox(label="Confirm", state=True) self.assertTrue(checkbox.selectable()) self.assertEqual(frozenset((urwid.FLOW, urwid.FIXED)), checkbox.sizing()) self.assertEqual("Confirm", checkbox.label) self.assertIs(True, checkbox.state) self.assertEqual((11, 1), checkbox.pack()) self.assertEqual([b"[X] Confirm"], checkbox.render(()).text) def test_toggle_and_mixed(self) -> None: checkbox = urwid.CheckBox("3-state", has_mixed=True) self.assertIs(False, checkbox.state) checkbox.toggle_state() self.assertIs(True, checkbox.state) checkbox.toggle_state() self.assertEqual("mixed", checkbox.state) checkbox.toggle_state() self.assertIs(False, checkbox.state) def test_on_state_change_and_postchange(self) -> None: changes: list[tuple[str, object, object]] = [] def on_change(cb: urwid.CheckBox, new_state: bool, user_data: object) -> None: changes.append(("change", new_state, user_data)) def on_post(cb: urwid.CheckBox, old_state: bool) -> None: changes.append(("postchange", old_state, cb.state)) checkbox = urwid.CheckBox("box", state=False, on_state_change=on_change, user_data="ud") urwid.connect_signal(checkbox, "postchange", on_post) checkbox.state = True checkbox.set_state(False, do_callback=False) self.assertIs(False, checkbox.state) self.assertEqual([("change", True, "ud"), ("postchange", False, True)], changes) def test_checked_symbol_is_per_instance(self) -> None: marked = urwid.CheckBox("ok", state=True, checked_symbol="*") default = urwid.CheckBox("ok", state=True) self.assertIn(b"[*]", marked.render((10,)).text[0]) self.assertIn(b"[X]", default.render((10,)).text[0]) self.assertIs(urwid.CheckBox.states[True], default.states[True]) def test_keypress_and_mouse(self) -> None: checkbox = urwid.CheckBox("press me") self.assertIsNone(checkbox.keypress((12,), " ")) self.assertIs(True, checkbox.state) self.assertEqual("left", checkbox.keypress((12,), "left")) self.assertTrue(checkbox.mouse_event((12,), "mouse press", 1, 0, 0, True)) self.assertIs(False, checkbox.state) def test_set_label(self) -> None: checkbox = urwid.CheckBox("foo") checkbox.set_label(("bright", "bar")) self.assertEqual("bar", checkbox.label) self.assertEqual("bar", checkbox.get_label()) class RadioButtonTest(unittest.TestCase): def test_group_and_first_true(self) -> None: group: list[urwid.RadioButton] = [] first = urwid.RadioButton(group=group, label="Agree") second = urwid.RadioButton(group=group, label="Disagree", state=False) self.assertEqual([first, second], group) self.assertIs(True, first.state) self.assertIs(False, second.state) self.assertEqual([b"(X) Agree"], first.render(()).text) self.assertEqual([b"( ) Disagree"], second.render(()).text) def test_exclusive_selection(self) -> None: group: list[urwid.RadioButton] = [] first = urwid.RadioButton(group, "A") second = urwid.RadioButton(group, "B") third = urwid.RadioButton(group, "C") second.toggle_state() self.assertEqual((False, True, False), (first.state, second.state, third.state)) second.toggle_state() self.assertEqual((False, True, False), (first.state, second.state, third.state)) third.state = True self.assertEqual((False, False, True), (first.state, second.state, third.state)) def test_on_state_change(self) -> None: seen: list[tuple[str, bool]] = [] group: list[urwid.RadioButton] = [] urwid.RadioButton(group, "A") second = urwid.RadioButton( group, "B", on_state_change=lambda rb, state: seen.append((rb.label, state)), ) second.set_state(True) self.assertEqual([("B", True)], seen) def test_signal_sequence(self) -> None: """Each button emits change and postchange once per transition, the selected one first.""" events: list[tuple[str, str, object]] = [] group: list[urwid.RadioButton] = [] buttons = [urwid.RadioButton(group, label) for label in "ABC"] for button in buttons: urwid.connect_signal(button, "change", lambda rb, state: events.append(("change", rb.label, state))) urwid.connect_signal(button, "postchange", lambda rb, old: events.append(("post", rb.label, old))) buttons[1].set_state(True) buttons[2].state = True buttons[2].set_state(False) buttons[0].toggle_state() buttons[1].set_state(True, do_callback=False) self.assertEqual([False, True, False], [button.state for button in buttons]) self.assertEqual( [ ("change", "B", True), ("post", "B", False), ("change", "A", False), ("post", "A", True), ("change", "C", True), ("post", "C", False), ("change", "B", False), ("post", "B", True), ("change", "C", False), ("post", "C", True), ("change", "A", True), ("post", "A", False), ("change", "A", False), ("post", "A", True), ], events, ) def test_reentrant_selection_leaves_one_selected(self) -> None: """A signal handler that moves the selection leaves exactly one button selected.""" def select_on( signal: str, source: urwid.RadioButton, target: urwid.RadioButton, when: bool, ) -> None: # Fire only on the transition that `when` names, so a chain cannot loop. if signal == "change": urwid.connect_signal(source, signal, lambda rb, new: target.set_state(True) if new is when else None) else: urwid.connect_signal( source, signal, lambda rb, old: target.set_state(True) if rb.state is when else None ) # (description, handlers as (signal, source, target, fire when source becomes), expected states) cases = ( ("postchange of the selected moves on", [("postchange", 1, 2, True)], [False, False, True, False]), ("change of the selected moves on", [("change", 1, 2, True)], [False, True, False, False]), ("postchange of a cleared one moves on", [("postchange", 0, 2, False)], [False, False, True, False]), ( "postchange chain across three buttons", [("postchange", 1, 2, True), ("postchange", 2, 3, True)], [False, False, False, True], ), ( "postchange of a cleared one starts a chain", [("postchange", 0, 2, False), ("postchange", 2, 3, True)], [False, False, False, True], ), ) for description, handlers, expected in cases: with self.subTest(description): group: list[urwid.RadioButton] = [] buttons = [urwid.RadioButton(group, label) for label in "ABCD"] for signal, source, target, when in handlers: select_on(signal, buttons[source], buttons[target], when) buttons[1].set_state(True) self.assertEqual(expected, [button.state for button in buttons]) def test_postchange_handler_unselecting_itself(self) -> None: """A button its own postchange handler unselects does not clear the previous selection.""" group: list[urwid.RadioButton] = [] first = urwid.RadioButton(group, "A") second = urwid.RadioButton(group, "B") urwid.connect_signal(second, "postchange", lambda rb, old: rb.set_state(False)) second.set_state(True) self.assertEqual((True, False), (first.state, second.state)) urwid-urwid-69907e4/tox.ini000066400000000000000000000041201526073734300156070ustar00rootroot00000000000000[tox] requires = tox>=4.57 tox-uv>=1 envlist = py3{9,10,11,12,13,14,15} py31{3,4,5}t pypy3 graalpy31{1,2} ruff pylint mypy refurb skip_missing_interpreters = True [testenv] usedevelop = true dependency_groups = pytest # Default extras for CPython: include all for maximum coverage extras = curses serial tornado trio twisted zmq commands = pytest [testenv:py31{3,4,5}t] # Freethreaded Python (PEP 703): --disable-gil build # Tox native 't' factor for freethreaded Python (automatically uses python*-nogil) # Twisted 24.10.0+ supports free-threading. Excludes glib, zmq (no free-threading wheels) usedevelop = true dependency_groups = pytest extras = curses serial tornado trio twisted commands = pytest setenv = PYTHONWARNDEFAULTENCODING=1 [testenv:pypy3] # PyPy has issues with some C extensions, skip problematic ones usedevelop = true dependency_groups = pytest # PyPy doesn't properly support pygobject (glib) or has issues with pyzmq extras = serial tornado trio commands = pytest setenv = PYTHONWARNDEFAULTENCODING=1 [testenv:graalpy31{1,2}] # GraalPy (Oracle's Python implementation on GraalVM) - experimental support # GraalPy has limited C extension support, skip problematic ones usedevelop = true dependency_groups = pytest # GraalPy doesn't support pygobject (glib), pyzmq, or twisted well extras = serial tornado commands = pytest setenv = PYTHONWARNDEFAULTENCODING=1 [testenv:readme] dependency_groups = build extras = commands = python -m build twine check {toxinidir}/dist/* [testenv:ruff] skip_install = true depends = dependency_groups = ruff extras = commands = ruff format --check . ruff check . [testenv:docs] dependency_groups = documentation extras = {[testenv]extras} glib commands = sphinx-build docs/ build/documentation [testenv:pylint] depends = ruff dependency_groups = pylint extras = {[testenv]extras} glib commands = pylint urwid [testenv:refurb] depends = ruff deps = refurb extras = commands = refurb urwid examples [testenv:mypy] depends = ruff dependency_groups = mypy commands = mypy urwid examples urwid-urwid-69907e4/urwid/000077500000000000000000000000001526073734300154315ustar00rootroot00000000000000urwid-urwid-69907e4/urwid/__init__.py000066400000000000000000000310441526073734300175440ustar00rootroot00000000000000# Urwid __init__.py - all the stuff you're likely to care about # # Copyright (C) 2004-2012 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """Urwid, a console user interface library: the public API re-exported from its submodules.""" from __future__ import annotations import importlib.util import sys import types import typing import warnings from urwid.canvas import ( BlankCanvas, Canvas, CanvasCache, CanvasCombine, CanvasError, CanvasJoin, CanvasOverlay, CompositeCanvas, SolidCanvas, TextCanvas, ) from urwid.command_map import ( ACTIVATE, CURSOR_DOWN, CURSOR_LEFT, CURSOR_MAX_LEFT, CURSOR_MAX_RIGHT, CURSOR_PAGE_DOWN, CURSOR_PAGE_UP, CURSOR_RIGHT, CURSOR_UP, REDRAW_SCREEN, CommandMap, command_map, ) from urwid.font import ( Font, FontRegistry, HalfBlock5x4Font, HalfBlock6x5Font, HalfBlock7x7Font, HalfBlockHeavy6x5Font, Sextant2x2Font, Sextant3x3Font, Thin3x3Font, Thin4x3Font, Thin6x6Font, get_all_fonts, ) from urwid.signals import ( MetaSignals, Signals, connect_signal, disconnect_signal, disconnect_signal_by_key, emit_signal, register_signal, ) from urwid.str_util import calc_text_pos, calc_width, is_wide_char, move_next_char, move_prev_char, within_double_byte from urwid.text_layout import LayoutSegment, StandardTextLayout, TextLayout, default_layout from urwid.util import ( MetaSuper, TagMarkupException, apply_target_encoding, calc_trim_text, decompose_tagmarkup, detected_encoding, get_encoding_mode, int_scale, is_mouse_event, set_encoding, supports_unicode, ) from urwid.version import version as __version__ from urwid.version import version_tuple as __version_tuple__ from . import display, event_loop, widget from .display import ( BLACK, BROWN, DARK_BLUE, DARK_CYAN, DARK_GRAY, DARK_GREEN, DARK_MAGENTA, DARK_RED, DEFAULT, LIGHT_BLUE, LIGHT_CYAN, LIGHT_GRAY, LIGHT_GREEN, LIGHT_MAGENTA, LIGHT_RED, UPDATE_PALETTE_ENTRY, WHITE, YELLOW, AttrSpec, AttrSpecError, BaseScreen, RealTerminal, ScreenError, ) from .event_loop import AsyncioEventLoop, EventLoop, ExitMainLoop, MainLoop, SelectEventLoop from .widget import ( ANY, BOTTOM, BOX, CENTER, CLIP, ELLIPSIS, FIXED, FLOW, GIVEN, LEFT, MIDDLE, PACK, RELATIVE, RELATIVE_100, RIGHT, SPACE, TOP, WEIGHT, AbstractWidget, Align, ANSIText, AttrMap, AttrMapError, AttrWrap, BarGraph, BarGraphError, BarGraphMeta, BigText, BoxAdapter, BoxAdapterError, Button, CheckBox, CheckBoxError, Columns, ColumnsError, Divider, Edit, EditError, Filler, FillerError, Frame, FrameError, GraphVScale, GridFlow, GridFlowError, IntEdit, LineBox, ListBox, ListBoxError, ListWalker, ListWalkerError, MonitoredDeque, MonitoredFocusDeque, MonitoredFocusList, MonitoredList, Overlay, OverlayError, Padding, PaddingError, ParentNode, Pile, PileError, PopUpLauncher, PopUpTarget, ProgressBar, RadioButton, Scrollable, ScrollBar, SelectableIcon, SimpleDequeWalker, SimpleFocusDequeWalker, SimpleFocusListWalker, SimpleListWalker, Sizing, SolidFill, Text, TextError, TreeListBox, TreeNode, TreeWalker, TreeWidget, TreeWidgetError, VAlign, WHSettings, Widget, WidgetContainerMixin, WidgetDecoration, WidgetDisable, WidgetError, WidgetMeta, WidgetPlaceholder, WidgetWrap, WidgetWrapError, WrapMode, delegate_to_widget_mixin, fixed_size, scale_bar_values, ) __all__: tuple[str, ...] = ( "ACTIVATE", "ANY", "BLACK", "BOTTOM", "BOX", "BROWN", "CENTER", "CLIP", "CURSOR_DOWN", "CURSOR_LEFT", "CURSOR_MAX_LEFT", "CURSOR_MAX_RIGHT", "CURSOR_PAGE_DOWN", "CURSOR_PAGE_UP", "CURSOR_RIGHT", "CURSOR_UP", "DARK_BLUE", "DARK_CYAN", "DARK_GRAY", "DARK_GREEN", "DARK_MAGENTA", "DARK_RED", "DEFAULT", "ELLIPSIS", "FIXED", "FLOW", "GIVEN", "LEFT", "LIGHT_BLUE", "LIGHT_CYAN", "LIGHT_GRAY", "LIGHT_GREEN", "LIGHT_MAGENTA", "LIGHT_RED", "MIDDLE", "PACK", "REDRAW_SCREEN", "RELATIVE", "RELATIVE_100", "RIGHT", "SPACE", "TOP", "UPDATE_PALETTE_ENTRY", "WEIGHT", "WHITE", "YELLOW", "ANSIText", "AbstractWidget", "Align", "AsyncioEventLoop", "AttrMap", "AttrMapError", "AttrSpec", "AttrSpecError", "AttrWrap", "BarGraph", "BarGraphError", "BarGraphMeta", "BaseScreen", "BigText", "BlankCanvas", "BoxAdapter", "BoxAdapterError", "Button", "Canvas", "CanvasCache", "CanvasCombine", "CanvasError", "CanvasJoin", "CanvasOverlay", "CheckBox", "CheckBoxError", "Columns", "ColumnsError", "CommandMap", "CompositeCanvas", "Divider", "Edit", "EditError", "EventLoop", "ExitMainLoop", "Filler", "FillerError", "Font", "FontRegistry", "Frame", "FrameError", "GraphVScale", "GridFlow", "GridFlowError", "HalfBlock5x4Font", "HalfBlock6x5Font", "HalfBlock7x7Font", "HalfBlockHeavy6x5Font", "IntEdit", "LayoutSegment", "LineBox", "ListBox", "ListBoxError", "ListWalker", "ListWalkerError", "MainLoop", "MetaSignals", "MetaSuper", "MonitoredDeque", "MonitoredFocusDeque", "MonitoredFocusList", "MonitoredList", "Overlay", "OverlayError", "Padding", "PaddingError", "ParentNode", "Pile", "PileError", "PopUpLauncher", "PopUpTarget", "ProgressBar", "RadioButton", "RealTerminal", "ScreenError", "ScrollBar", "Scrollable", "SelectEventLoop", "SelectableIcon", "Sextant2x2Font", "Sextant3x3Font", "Signals", "SimpleDequeWalker", "SimpleFocusDequeWalker", "SimpleFocusListWalker", "SimpleListWalker", "Sizing", "SolidCanvas", "SolidFill", "StandardTextLayout", "TagMarkupException", "Text", "TextCanvas", "TextError", "TextLayout", "Thin3x3Font", "Thin4x3Font", "Thin6x6Font", "TreeListBox", "TreeNode", "TreeWalker", "TreeWidget", "TreeWidgetError", "VAlign", "WHSettings", "Widget", "WidgetContainerMixin", "WidgetDecoration", "WidgetDisable", "WidgetError", "WidgetMeta", "WidgetPlaceholder", "WidgetWrap", "WidgetWrapError", "WrapMode", "__version__", "__version_tuple__", "apply_target_encoding", "calc_text_pos", "calc_trim_text", "calc_width", "command_map", "connect_signal", "decompose_tagmarkup", "default_layout", "delegate_to_widget_mixin", "detected_encoding", "disconnect_signal", "disconnect_signal_by_key", "display", "emit_signal", "event_loop", "fixed_size", "get_all_fonts", "get_encoding_mode", "int_scale", "is_mouse_event", "is_wide_char", "move_next_char", "move_prev_char", "register_signal", "scale_bar_values", "set_encoding", "supports_unicode", "widget", "within_double_byte", ) # Optional event loops with external dependencies try: from .event_loop import TornadoEventLoop except ImportError: pass else: __all__ += ("TornadoEventLoop",) try: from .event_loop import GLibEventLoop except ImportError: pass else: __all__ += ("GLibEventLoop",) try: from .event_loop import TwistedEventLoop except ImportError: pass else: __all__ += ("TwistedEventLoop",) try: from .event_loop import TrioEventLoop except ImportError: pass else: __all__ += ("TrioEventLoop",) # OS Specific if sys.platform != "win32": from .vterm import TermCanvas, TermCharset, Terminal, TermModes __all__ += ("TermCanvas", "TermCharset", "TermModes", "Terminal") # ZMQEventLoop cause interpreter crash on Windows try: from .event_loop import ZMQEventLoop except ImportError: pass else: __all__ += ("ZMQEventLoop",) # Backward compatibility VERSION = __version_tuple__ # Moved modules handling __locals: dict[str, typing.Any] = locals() # use mutable access for pure lazy loading # Backward compatible lazy load with deprecation warnings _moved_warn: dict[str, str] = { "display_common": "urwid.display.common", "raw_display": "urwid.display.raw", "curses_display": "urwid.display.curses", "escape": "urwid.display.escape", } # Before DeprecationWarning need to start PendingDeprecationWarning process. def lazy_import(name: str, package: str | None = None) -> types.ModuleType: """Lazy import implementation from Python documentation. Useful for cases where no warnings expected for moved modules. :raises ImportError: *name* does not resolve to a loadable module. """ spec = importlib.util.find_spec(name, package) if not spec: raise ImportError(f"No module named {name!r}") if not spec.loader: raise ImportError(f"Module named {name!r} is invalid") loader = importlib.util.LazyLoader(spec.loader) spec.loader = loader module = importlib.util.module_from_spec(spec) if not package: sys.modules[name] = module else: sys.modules[f"{package.rstrip('.')}.{name.lstrip('.')}"] = module loader.exec_module(module) return module class _MovedModule(types.ModuleType): """Special class to handle moved modules. PEP-0562 handles moved modules attributes, but unfortunately not handle nested modules access like "from xxx.yyy import zzz" """ __slots__ = ("_moved_from", "_moved_to") def __init__(self, moved_from: str, moved_to: str) -> None: super().__init__(moved_from.join(".")[-1]) self._moved_from = moved_from self._moved_to = moved_to def __getattr__(self, name: str) -> typing.Any: """Resolve `name` by importing the module this one moved to. :raises AttributeError: *name* is not found, including when *self._moved_to* itself cannot be imported. """ try: real_module = importlib.import_module(self._moved_to) except ImportError as exc: raise AttributeError(name) from exc sys.modules[self._moved_from] = real_module return getattr(real_module, name) class _MovedModuleWarn(_MovedModule): """Special class to handle moved modules. Produce DeprecationWarning messages for imports. """ __slots__ = () def __getattr__(self, name: str) -> typing.Any: warnings.warn( f"{self._moved_from} is moved to {self._moved_to} since urwid 2.4.0. API will be removed in version 6.0.", DeprecationWarning, stacklevel=2, ) return super().__getattr__(name) for _name, _module in _moved_warn.items(): _module_path = f"{__name__}.{_name}" sys.modules[_module_path] = _MovedModuleWarn(_module_path, _module) def __getattr__(name: str) -> typing.Any: """Get attributes lazy. :return: attribute by name :raises AttributeError: attribute is not defined for lazy load """ if name in _moved_warn: warnings.warn( f"{name} is moved to {_moved_warn[name]} since urwid 2.4.0. API will be removed in version 6.0.", DeprecationWarning, stacklevel=2, ) try: mod = importlib.import_module(_moved_warn[name]) except ImportError as exc: raise AttributeError(name) from exc __locals[name] = mod return mod raise AttributeError(f"{name} not found in {__package__}") urwid-urwid-69907e4/urwid/ansi_parser.py000066400000000000000000001153251526073734300203200ustar00rootroot00000000000000# Urwid standalone ANSI/VT100 escape-sequence line parser # Copyright (C) 2026 urwid contributors # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """Standalone ANSI/VT100 escape-sequence parsing, decoupled from any canvas. This module extracts the platform-independent portion of the escape-sequence grammar historically embedded inside :mod:`urwid.vterm`'s ``TermCanvas`` -- SGR colour/attribute interpretation, horizontal cursor movement, BEL, OSC title and keyboard-LED recognition -- into a form that can be used to interpret a single string (or a stream of chunks) of ANSI-coloured text without instantiating a pty-backed :class:`~urwid.Terminal` widget. Sequences that only make sense against a genuine two-dimensional screen buffer (vertical cursor movement, scroll regions, mode toggles, and so forth) are recognised, stripped from the resolved text and logged (see :class:`SkippedOp`) rather than raising or being silently discarded without trace. """ from __future__ import annotations import dataclasses import logging import typing from urwid.display import AttrSpec from urwid.display.common import _BASIC_COLORS as BASIC_COLORS from urwid.display.common import _color_desc_256 as color_desc_256 from urwid.display.common import _color_desc_true as color_desc_true from urwid.util import rle_append_modify if typing.TYPE_CHECKING: from collections.abc import Hashable, Sequence __all__ = ( "AnsiParser", "ParsedLine", "SkippedOp", "led_state", "parse_ansi_line", "parse_ansi_text", "resolve_osc_title", "sgi_params_to_attrspec", ) LOGGER = logging.getLogger(__name__) ESC = "\x1b" BEL = "\x07" #: Recognised, ready-to-render values of :attr:`ParsedLine.leds`. LedsLiteral = "Literal['clear', 'scroll_lock', 'num_lock', 'caps_lock']" _LED_STATES: dict[int, str] = { 0: "clear", 1: "scroll_lock", 2: "num_lock", 3: "caps_lock", } _SGR_ATTR_NAMES: tuple[str, ...] = ("bold", "faint", "underline", "blink", "standout") #: CSI final bytes that move the cursor vertically -- meaningless outside a #: two-dimensional screen buffer, so the whole operation is stripped and #: logged rather than reinterpreted. _VERTICAL_MOVE_CSI = frozenset("ABEFdHf") #: CSI final bytes that mutate terminal-wide state (modes, scroll regions, #: erase, insert/delete, device queries, tabstops, ...) -- again meaningless #: for a single, canvas-less line of text. _TERMINAL_SETTING_CSI = frozenset("hlrJKsu@LMPXcng") #: Line-end byte pairs that collapse into a single structural split point. _LINE_END_PAIR: dict[str, str] = {"\r": "\n", "\n": "\r"} #: The furthest a relative cursor move advances, and the furthest 1-based row or column an absolute move reaches. #: The input is untrusted, and a few bytes such as ``CSI 300000000 C`` would otherwise pad the grid with that many #: cells. 1024 exceeds the width and height of practical terminals, and caps what one movement sequence adds to the #: grid at 1024 cells. _MAX_CURSOR_POSITION = 1024 #: CSI parameter strings of this length or longer are skipped unparsed. Real sequences are far shorter (a truecolour #: foreground and background SGR is under 50 characters). Staying below 640, the lowest limit #: :func:`sys.set_int_max_str_digits` accepts, guarantees ``int()`` never rejects a parameter. _MAX_CSI_LENGTH = 256 #: OSC strings of this length or longer are skipped, and the rest of the string up to its terminator, or up to an ESC #: starting a new sequence, is discarded unbuffered, so an unterminated OSC costs neither memory nor quadratic string #: growth. _MAX_OSC_LENGTH = 4096 #: The most entries a ``skipped`` list holds, covering one 64 KiB raw display read of 2-byte sequences. _MAX_SKIPPED = 65536 #: Padding cells a parser may insert regardless of input length, one full 1024 x 1024 screen. _PADDING_BUDGET = _MAX_CURSOR_POSITION * _MAX_CURSOR_POSITION #: Padding cells each fed character adds to the budget, as many as a tab writes. _PADDING_PER_CHARACTER = 8 #: Padding cells an empty row inserted by a vertical move costs, about what the row allocates. _PADDING_PER_ROW = 8 @dataclasses.dataclass(frozen=True) class SkippedOp: """A single escape-sequence (or control character) that was recognised but stripped. Such a sequence is stripped from the resolved text rather than interpreted, because it has no meaning outside a full two-dimensional terminal canvas. :ivar kind: one of ``"vertical-move"``, ``"terminal-setting"`` or ``"unknown"``. :ivar raw: the raw sequence text as encountered (including its ``ESC``/``CSI`` framing), truncated for a sequence skipped for its length. :ivar reason: a human-readable explanation, used verbatim in the log message emitted alongside this entry. """ kind: str raw: str reason: str @dataclasses.dataclass(frozen=True) class ParsedLine: r"""The result of parsing one physical line of ANSI-coloured text. :ivar text: the resolved, escape-free text of this line. When produced by :class:`AnsiParser` constructed with ``one_line=True`` (or :func:`parse_ansi_line`), this is guaranteed never to contain a ``\n`` -- line ends are structural split points that produce *separate* :class:`ParsedLine` instances. That invariant is deliberately **not** upheld when :class:`AnsiParser` is constructed with ``one_line=False`` (or by :func:`parse_ansi_text`): since that mode resolves a whole multi-row block into a single :class:`ParsedLine`, its ``text`` legitimately contains embedded ``\n`` characters, one per row boundary, by design. :ivar attrib: run-length encoded display attributes for :attr:`text`, the same shape as :attr:`urwid.Text.attrib`. ``None`` is a valid RLE tag (meaning "no attribute"), matching :func:`urwid.util.decompose_tagmarkup`'s convention. :ivar last_attr: the SGR/colour state in effect at the end of this line; feed this back in as ``previous_attr`` to continue the same colour state into a subsequently parsed line. :ivar bel: the number of BEL (``\a``) characters seen on this line (``0`` if none were seen). :ivar title: the last OSC window-title string seen on this line, or ``None`` if none was seen. :ivar leds: the last keyboard-LED state requested on this line (``CSI n q``), or ``None`` if none was seen. :ivar skipped: the :class:`SkippedOp` entries stripped while parsing this line, in encounter order, for programmatic inspection independent of the log. It holds at most the first 65536 of them. """ text: str attrib: list[tuple[Hashable, int]] last_attr: AttrSpec | None bel: int = 0 title: str | None = None leds: str | None = None skipped: list[SkippedOp] = dataclasses.field(default_factory=list) class AnsiParser: r"""Incremental ANSI parser that resolves escape sequences against a 2-D cell grid, in one of two modes. Feed text in with :meth:`feed` -- any number of times, e.g. as chunks arrive from a subprocess -- and call :meth:`finalize` once to retrieve the result. :meth:`finalize` is a one-shot, terminal operation: construct a fresh :class:`AnsiParser` per logical "session" of feeding. Internally, a :class:`AnsiParser` always maintains a two-dimensional grid of rows (one cell buffer per screen row) plus a cursor (``row``, ``col``). The two constructor modes below share the *overwhelming majority* of their behaviour -- printable characters, horizontal cursor movement (``CSI n C/D/G``), backspace, tab, SGR colour/attribute handling (``CSI ... m``), BEL, OSC window-title recognition, keyboard-LED requests (``CSI n q``) and terminal-wide setting sequences (stripped and logged, see :class:`SkippedOp`) are interpreted identically regardless of mode. They differ only in how a **line end** and **vertical cursor movement** are treated, and in whether the side channels (``bel``, ``title``, ``leds``, ``skipped``) are reset per line or aggregated over the whole input: * ``one_line=True``: a line-end sequence (``\n``, ``\r``, ``\r\n`` or ``\n\r``, the latter two pairs collapsing into a single event) is a **structural split point**, not discarded data -- the moment one is seen, the current row is completed into a :class:`ParsedLine` and pushed onto an internal completed-lines queue, and a fresh row is started for what follows, carrying the SGR/colour state (:attr:`ParsedLine.last_attr`) forward but resetting the per-line side channels. Vertical cursor movement (``CSI n A/B/E/F/d`` and ``CSI r;c H``/``f``) is meaningless outside a single line, so it is stripped and logged as a :class:`SkippedOp` rather than reinterpreted. * ``one_line=False`` (the default): a full newline (``\n``, or a collapsed ``\r\n``/``\n\r`` pair) genuinely moves to a fresh row, resetting the column, while everything keeps accumulating in the same persistent grid -- nothing is split off. A **bare** ``\r`` (not part of a ``\r\n``/``\n\r`` pair) is carriage-return-only: it resets the column without changing row, letting progress-bar-style in-place redraw sequences (``"progress: 10%\rprogress: 20%"``) resolve correctly onto a single row, mirroring real terminal behaviour. Vertical cursor movement genuinely repositions the cursor within the grid instead of being stripped. The side channels are aggregated over the *entire* input rather than reset per row: ``bel`` is the total BEL count, ``title``/``leds`` are the last value seen anywhere, and ``skipped`` lists the :class:`SkippedOp` entries in encounter order. Deliberately out of scope in both modes (stripped and logged as :class:`SkippedOp` with ``kind="terminal-setting"`` or ``kind="unknown"``): erase (``CSI J``/``K``), scroll regions, save/restore cursor, mode toggles, insert/delete, device queries, tabstops, and the non-CSI vertical-movement escapes (``ESC D``/``M``/``E``) -- a genuine two-dimensional buffer would technically allow implementing erase and save/restore cursor, but that is a candidate follow-up, kept out of scope here. The padding inserted to reach a moved cursor -- blank cells before a written character and empty rows below the last one -- is limited to ``1024 * 1024 + 8 * n`` cells over the parser's lifetime, ``n`` being the number of characters fed so far, and an empty row counts as 8 cells. Once the limit is reached, a character is written at the end of its row instead, a vertical move past the last row stays on the last row, and each such cut is recorded as a :class:`SkippedOp` with ``kind="unknown"``. :meth:`finalize` returns a uniform ``tuple[ParsedLine, ...]`` in both modes: in ``one_line=True`` mode, every line completed since construction (queue plus a final flush of whatever remains, possibly empty or unterminated); in ``one_line=False`` mode, always exactly one :class:`ParsedLine` -- built from the whole grid, rows joined with ``"\n"`` -- wrapped in a 1-tuple. :param previous_attr: SGR/colour state to seed the parser with, as if it were carried over from previously parsed text. :param one_line: if true, treat line ends as structural split points that yield multiple :class:`ParsedLine` results; if false (the default), resolve the whole input into a single multi-row :class:`ParsedLine`. >>> parser = AnsiParser(one_line=True) >>> parser.feed("hello\nworld") >>> [line.text for line in parser.finalize()] ['hello', 'world'] >>> parser = AnsiParser() >>> parser.feed("hello\nworld") >>> parser.finalize()[0].text 'hello\nworld' """ def __init__(self, previous_attr: AttrSpec | None = None, *, one_line: bool = False) -> None: """Create a parser, as described in the class docstring.""" self._one_line = one_line self._attrspec: AttrSpec | None = previous_attr # one cell buffer per screen row, plus a 2-D cursor -- in one_line # mode there is conceptually only ever one row: it is snapshotted # and reset at each structural split, see _complete_line() self._rows: list[list[tuple[str, Hashable | None]]] = [[]] self._row = 0 self._col = 0 # padding cells still allowed, and the last cursor move a cut padding is recorded against self._padding_budget = _PADDING_BUDGET self._last_move = "" # side channels -- reset per completed line in one_line mode (see # _complete_line()), aggregated over the entire input otherwise self._bel_count = 0 self._title: str | None = None self._leds: str | None = None self._skipped: list[SkippedOp] = [] # completed-lines queue, used only in one_line mode self._completed: list[ParsedLine] = [] # escape-framing state: 0 none / 1 CSI / 2 OSC / 3 two-char intermediate / # 4 the discarded remainder of an over-long OSC string self._within_escape = False self._parsestate = 0 self._escbuf = "" # a line-end byte seen at the very end of the previous feed() call, # kept around in case the next feed() call opens with its pairing # complement (completing a \r\n / \n\r pair split across chunks) self._pending_line_end: str | None = None def feed(self, text: str) -> None: """Feed another chunk of decoded text into the parser. Chunks need not be aligned on line, row or escape-sequence boundaries; state (including a line end split across two calls, or an escape sequence split across two calls) is carried over correctly. :param text: the next chunk of decoded text to parse. """ if not text: return self._padding_budget += _PADDING_PER_CHARACTER * len(text) if self._pending_line_end is not None: pending = self._pending_line_end self._pending_line_end = None if text[:1] == _LINE_END_PAIR.get(pending): text = text[1:] if not self._one_line and pending == "\r": # \r\n split across a feed() call boundary: the bare CR # half was already applied (column reset) when it was # first seen, so only the row advance is still owed self._newline() # in one_line mode the split point was already handled in # full when the first half was seen; a pending "\n" in # screen mode needs no further action either, for the same # reason i = 0 n = len(text) while i < n: ch = text[i] if ch in _LINE_END_PAIR: if self._one_line: self._handle_line_end() if i + 1 < n and text[i + 1] == _LINE_END_PAIR[ch]: i += 2 else: if i + 1 == n: self._pending_line_end = ch i += 1 continue if i + 1 < n and text[i + 1] == _LINE_END_PAIR[ch]: self._newline() i += 2 continue if ch == "\n": self._newline() else: self._col = 0 if i + 1 == n: self._pending_line_end = ch i += 1 continue self._step(ch) i += 1 def finalize(self) -> tuple[ParsedLine, ...]: r"""Retrieve the parsed result, in a uniform ``tuple[ParsedLine, ...]`` shape regardless of mode. In ``one_line=True`` mode, whatever remains in the current (possibly empty, possibly unterminated) row is flushed as one final :class:`ParsedLine`, and every line completed since construction is returned, in order. In ``one_line=False`` mode, the whole accumulated grid is resolved into a single :class:`ParsedLine` (its ``text`` joining every resolved row with ``"\n"``) and returned as a 1-tuple. This is a terminal operation: construct a new :class:`AnsiParser` to parse a fresh "session" of input. :returns: a uniform ``tuple[ParsedLine, ...]``, shaped as described above. """ if self._one_line: self._complete_line() return tuple(self._completed) text_parts: list[str] = [] attrib: list[tuple[Hashable, int]] = [] for index, row in enumerate(self._rows): if index: text_parts.append("\n") rle_append_modify(attrib, (None, 1)) text_parts.append("".join(char for char, _attr in row)) for _char, attr in row: rle_append_modify(attrib, (attr, 1)) parsed = ParsedLine( text="".join(text_parts), attrib=attrib, last_attr=self._attrspec, bel=self._bel_count, title=self._title, leds=self._leds, skipped=list(self._skipped), ) return (parsed,) # -- internal: line/row completion and cursor management ------------- def _handle_line_end(self) -> None: r"""Complete the current line, aborting any in-flight escape sequence. Only called in one_line mode: a line end always aborts any in-flight escape sequence, since real escape sequences never legitimately contain a raw \r or \n. In screen mode a newline is instead handled directly in :meth:`feed` without touching escape-framing state. """ self._within_escape = False self._parsestate = 0 self._escbuf = "" self._complete_line() def _complete_line(self) -> None: row = self._rows[0] text = "".join(char for char, _attr in row) attrib: list[tuple[Hashable, int]] = [] for _char, attr in row: rle_append_modify(attrib, (attr, 1)) self._completed.append( ParsedLine( text=text, attrib=attrib, last_attr=self._attrspec, bel=self._bel_count, title=self._title, leds=self._leds, skipped=self._skipped, ) ) self._rows = [[]] self._col = 0 self._bel_count = 0 self._title = None self._leds = None self._skipped = [] def _ensure_row(self, row: int) -> None: while len(self._rows) <= row: self._rows.append([]) def _move_to_row(self, row: int, raw: str) -> None: """Move the cursor to ``row``, inserting the missing empty rows up to it within the padding budget. Without enough budget the cursor moves to the last existing row instead, and ``raw`` is recorded as skipped. """ cost = (row + 1 - len(self._rows)) * _PADDING_PER_ROW if cost > self._padding_budget: self._skip("unknown", raw, "padding limit reached, cursor kept on the last row") row = min(row, len(self._rows) - 1) elif cost > 0: self._padding_budget -= cost self._ensure_row(row) self._row = row def _newline(self) -> None: self._row += 1 self._ensure_row(self._row) self._col = 0 # -- internal: char-by-char state machine --------------------------- def _step(self, ch: str) -> None: if self._parsestate in {2, 4}: self._handle_osc_char(ch) return if ch == BEL: self._bel_count += 1 return if ch == "\t": self._handle_tab() return if ch == "\b": self._col = max(0, self._col - 1) return if ch in ("\x00", "\x7f"): # NUL/DEL -- ignored, matching vterm return if ch in ("\x18", "\x1a"): # CAN/SUB -- abort any escape in progress self._within_escape = False self._parsestate = 0 self._escbuf = "" return if ch == ESC: self._within_escape = True return if self._within_escape: self._parse_escape(ch) return self._write_char(ch, self._attrspec) def _handle_tab(self, tabstop: int = 8) -> None: target = ((self._col // tabstop) + 1) * tabstop while self._col < target: self._write_char(" ", self._attrspec) def _write_char(self, ch: str, attr: Hashable | None) -> None: row = self._rows[self._row] if self._col < len(row): row[self._col] = (ch, attr) elif self._col == len(row): row.append((ch, attr)) else: gap = self._col - len(row) if gap > self._padding_budget: self._skip("unknown", self._last_move, "padding limit reached, written at the end of the row") self._col = len(row) else: self._padding_budget -= gap row.extend([(" ", None)] * gap) row.append((ch, attr)) self._col += 1 # -- internal: escape/CSI/OSC framing -------------------------------- def _leave_escape(self) -> None: self._within_escape = False self._parsestate = 0 self._escbuf = "" def _skip(self, kind: str, raw: str, reason: str) -> None: """Record a stripped sequence and log it at debug level. Entries past the first :data:`_MAX_SKIPPED` are not recorded. Untrusted input can repeat a sequence without limit. A warning would reach stderr, over the UI, through Python's last-resort handler. """ if len(self._skipped) < _MAX_SKIPPED: self._skipped.append(SkippedOp(kind=kind, raw=raw, reason=reason)) LOGGER.debug("%s: %r", reason, raw) def _parse_escape(self, ch: str) -> None: if self._parsestate == 1: # within CSI if ch in "0123456789;" or (not self._escbuf and ch == "?"): # past the limit the parameters are dropped; _dispatch_csi skips the sequence by its length if len(self._escbuf) < _MAX_CSI_LENGTH: self._escbuf += ch return self._dispatch_csi(ch, self._escbuf) self._leave_escape() return if self._parsestate == 0 and ch == "]": self._escbuf = "" self._parsestate = 2 return if self._parsestate == 0 and ch == "[": self._escbuf = "" self._parsestate = 1 return if self._parsestate == 0 and ch in "%#()": self._escbuf = ch self._parsestate = 3 return if self._parsestate == 3: self._dispatch_noncsi(ch, self._escbuf) self._leave_escape() return if ch in "cDEHMZ78>=": self._dispatch_noncsi(ch, "") else: self._skip("unknown", f"{ESC}{ch}", f"unrecognised escape sequence ESC {ch!r}") self._leave_escape() def _dispatch_csi(self, final: str, escbuf: str) -> None: raw = f"{ESC}[{escbuf}{final}" if len(escbuf) >= _MAX_CSI_LENGTH: self._skip("unknown", raw, f"CSI parameters of {_MAX_CSI_LENGTH} characters or more, truncated") return qmark = escbuf.startswith("?") body = escbuf[1:] if qmark else escbuf params: list[int | None] = [int(p) if p else None for p in body.split(";")] if body else [] if final == "m": ints = [p if p is not None else 0 for p in params] or [0] self._attrspec = sgi_params_to_attrspec(ints, self._attrspec) elif final == "C": n = params[0] if params and params[0] else 1 self._col += min(n, _MAX_CURSOR_POSITION) self._last_move = raw elif final == "D": n = params[0] if params and params[0] else 1 self._col = max(0, self._col - n) elif final == "G": n = params[0] if params and params[0] else 1 self._col = min(n, _MAX_CURSOR_POSITION) - 1 self._last_move = raw elif final == "q": mode = params[0] if params and params[0] else 0 self._leds = led_state(mode) elif final in _VERTICAL_MOVE_CSI: self._dispatch_vertical_move_csi(final, params, raw) elif final in _TERMINAL_SETTING_CSI: self._skip("terminal-setting", raw, f"CSI {final!r} terminal-wide setting is out of scope for this pass") else: self._skip("unknown", raw, f"unrecognised CSI final byte {final!r}") def _dispatch_vertical_move_csi(self, final: str, params: list[int | None], raw: str) -> None: if self._one_line: # meaningless outside a full two-dimensional screen buffer, so # the whole operation is stripped and logged rather than # reinterpreted self._skip("vertical-move", raw, f"CSI {final!r} vertical cursor movement is meaningless for a single line") return self._last_move = raw # these genuinely reposition the cursor within the grid if final == "A": n = params[0] if params and params[0] else 1 self._row = max(0, self._row - n) elif final == "B": n = params[0] if params and params[0] else 1 self._move_to_row(self._row + min(n, _MAX_CURSOR_POSITION), raw) elif final == "E": n = params[0] if params and params[0] else 1 self._move_to_row(self._row + min(n, _MAX_CURSOR_POSITION), raw) self._col = 0 elif final == "F": n = params[0] if params and params[0] else 1 self._row = max(0, self._row - n) self._col = 0 elif final == "d": n = params[0] if params and params[0] else 1 self._move_to_row(min(n, _MAX_CURSOR_POSITION) - 1, raw) elif final in ("H", "f"): r = params[0] if params and params[0] else 1 c = params[1] if len(params) > 1 and params[1] else 1 self._move_to_row(min(r, _MAX_CURSOR_POSITION) - 1, raw) self._col = min(c, _MAX_CURSOR_POSITION) - 1 def _dispatch_noncsi(self, ch: str, mod: str) -> None: raw = f"{ESC}{mod}{ch}" if mod == "#" and ch == "8": self._skip("terminal-setting", raw, "DECALN screen alignment test") elif mod == "%": self._skip("terminal-setting", raw, "character set selection") elif mod in ("(", ")"): self._skip("terminal-setting", raw, "charset designation") elif ch == "M": self._skip("vertical-move", raw, "reverse line feed") elif ch == "D": self._skip("vertical-move", raw, "line feed") elif ch == "E": self._skip("vertical-move", raw, "newline") elif ch == "H": self._skip("terminal-setting", raw, "set tabstop") elif ch == "c": self._skip("terminal-setting", raw, "terminal reset") elif ch in ("7", "8"): self._skip("terminal-setting", raw, "save/restore cursor") elif ch in (">", "=", "Z"): self._skip("terminal-setting", raw, "keypad mode / device attributes") else: self._skip("unknown", raw, f"unrecognised escape sequence {raw!r}") def _handle_osc_char(self, ch: str) -> None: if self._escbuf[-1:] == ESC and ch != "\\": # ECMA-48 allows no ESC inside a command string other than in ST, so any other ESC ends the string # unapplied and starts a new escape sequence if self._parsestate == 2: self._skip("unknown", f"{ESC}]{self._escbuf[:-1]}", "unterminated OSC string") self._leave_escape() self._within_escape = True self._step(ch) return if self._parsestate == 4: # only the last character is kept, which is enough to recognise either terminator if ch == BEL or (self._escbuf == ESC and ch == "\\"): self._leave_escape() else: self._escbuf = ch return if ch == BEL: self._finish_osc(self._escbuf) return if self._escbuf[-1:] == ESC and ch == "\\": self._finish_osc(self._escbuf[:-1]) return if len(self._escbuf) >= _MAX_OSC_LENGTH: self._skip( "unknown", f"{ESC}]{self._escbuf}", f"OSC string of {_MAX_OSC_LENGTH} characters or more, truncated" ) self._parsestate = 4 self._escbuf = ch return if self._escbuf.startswith("P") and len(self._escbuf) == 8: self._finish_osc_skip("OSC palette set") return if not self._escbuf and ch == "R": self._finish_osc_skip("OSC palette reset") return self._escbuf += ch def _finish_osc(self, buf: str) -> None: raw = f"{ESC}]{buf}" self._leave_escape() title = resolve_osc_title(buf) if title is not None: self._title = title else: self._skip("terminal-setting", raw, "unrecognised OSC prefix") def _finish_osc_skip(self, reason: str) -> None: raw = f"{ESC}]{self._escbuf}" self._skip("terminal-setting", raw, reason) self._leave_escape() def parse_ansi_line(text: str, previous_attr: AttrSpec | None = None) -> tuple[ParsedLine, ...]: r"""Feed ``text`` through a fresh :class:`AnsiParser` (``one_line=True``) and return its finalised result. Returns one :class:`ParsedLine` per line found in ``text`` (split on ``\n``/``\r``/``\r\n``/``\n\r``), plus a trailing entry for a final unterminated fragment, if any -- never fewer than one element, even for empty input, and every character of the original data is preserved across the returned lines (line-end characters are structural split points, not stripped content). :param text: the text to parse. :param previous_attr: SGR/colour state to seed the parser with, as if it were carried over from a previously parsed line. :returns: every :class:`ParsedLine` found in ``text``, in order. >>> [line.text for line in parse_ansi_line("one\ntwo\nthree")] ['one', 'two', 'three'] >>> [line.text for line in parse_ansi_line("")] [''] >>> [line.text for line in parse_ansi_line("a\nb")] ['a', 'b'] """ parser = AnsiParser(previous_attr, one_line=True) parser.feed(text) return parser.finalize() def parse_ansi_text(text: str, previous_attr: AttrSpec | None = None) -> ParsedLine: r"""Feed ``text`` through a fresh :class:`AnsiParser` (``one_line=False``) and return its sole resolved line. Unlike :func:`parse_ansi_line`, vertical cursor movement and newlines are resolved into real multi-row output rather than stripped/split: the returned single :class:`ParsedLine`'s ``text`` joins every resolved row with ``"\n"``. :param text: the text to parse. :param previous_attr: SGR/colour state to seed the parser with, as if it were carried over from previously parsed text. :returns: a single :class:`ParsedLine` representing the whole resolved block. >>> parse_ansi_text("one\ntwo\nthree").text 'one\ntwo\nthree' >>> parse_ansi_text("").text '' >>> parse_ansi_text("progress: 10%\rprogress: 20%").text 'progress: 20%' """ parser = AnsiParser(previous_attr, one_line=False) parser.feed(text) return parser.finalize()[0] def led_state(mode: int) -> str | None: """Map a keyboard-LED ``CSI n q`` mode number to its urwid-facing state name. Shared by :class:`AnsiParser` and :mod:`urwid.vterm`'s ``TermCanvas.csi_set_keyboard_leds`` so the LED code mapping is maintained in exactly one place. :param mode: the ``CSI n q`` mode number to resolve. :returns: one of ``"clear"``, ``"scroll_lock"``, ``"num_lock"`` or ``"caps_lock"``, or ``None`` if ``mode`` is not recognised. >>> led_state(0) 'clear' >>> led_state(3) 'caps_lock' >>> led_state(99) is None True """ return _LED_STATES.get(mode) def resolve_osc_title(buf: str) -> str | None: """Resolve the window-title text encoded in an OSC command body, if it matches a recognised title prefix. Applies the same prefix rules xterm and :mod:`urwid.vterm` use to recognise an OSC window-title request -- a body starting (after stripping leading zeros) with ``;``, ``0;`` or ``2;`` -- and returns the text following the first ``;``. Returns ``None`` for any other OSC body (e.g. an unrecognised or palette-setting OSC), rather than raising. :param buf: the OSC command body, excluding the ``ESC ]`` framing and terminator. :returns: the resolved window-title text, or ``None`` if ``buf`` does not match a recognised title prefix. >>> resolve_osc_title(";hello") 'hello' >>> resolve_osc_title("2;hello") 'hello' >>> resolve_osc_title("666not a title") is None True """ stripped = buf.lstrip("0") if stripped.startswith((";", "0;", "2;")): return stripped.partition(";")[2] return None def sgi_params_to_attrspec(params: Sequence[int], previous: AttrSpec | None) -> AttrSpec | None: """Resolve a sequence of SGR (Select Graphic Rendition) numeric parameters against a previous attribute spec. This is a pure extraction of the SGR-number-walking logic historically duplicated inside ``vterm.TermCanvas.sgi_to_attrspec``. SGR 10/11/12 (which toggle vterm's charset/display-control state rather than colour/attributes) are deliberately ignored here -- a caller that also needs that side effect applies it itself; this function never raises for an unrecognised numeric code, it simply has no effect on the result. An extended colour (``38``/``48``) whose index or component is outside ``0..255`` is consumed and ignored the same way. :param params: the SGR numeric parameters to apply, in encounter order. :param previous: the :class:`~urwid.AttrSpec` in effect before ``params`` is applied, or ``None`` if none is in effect yet. :returns: the resulting :class:`~urwid.AttrSpec`, or ``None`` for "no attributes". >>> sgi_params_to_attrspec([31], None) AttrSpec('dark red', 'default') >>> sgi_params_to_attrspec([0], AttrSpec("dark red", "default")) """ if params and params[-1] == 0: previous = None attributes: set[str] = set() if previous is None: fg: int | None = None bg: int | None = None colors: int = 1 else: fg = None if "default" in previous.foreground else previous.foreground_number if fg is not None and fg >= 8 and previous.colors == 16: fg -= 8 bg = None if "default" in previous.background else previous.background_number if bg is not None and bg >= 8 and previous.colors == 16: bg -= 8 for name in _SGR_ATTR_NAMES: if getattr(previous, name): attributes.add(name) colors = previous.colors params = list(params) idx = 0 while idx < len(params): attr = params[idx] if 30 <= attr <= 37: fg = attr - 30 colors = max(16, colors) elif 40 <= attr <= 47: bg = attr - 40 colors = max(16, colors) elif 90 <= attr <= 97: fg = attr - 90 + 8 colors = max(16, colors) elif 100 <= attr <= 107: bg = attr - 100 + 8 colors = max(16, colors) elif attr in (38, 48): if idx + 2 < len(params) and params[idx + 1] == 5: color = params[idx + 2] if 0 <= color <= 255: colors = max(256, colors) if attr == 38: fg = color else: bg = color idx += 2 elif idx + 4 < len(params) and params[idx + 1] == 2: if all(0 <= component <= 255 for component in params[idx + 2 : idx + 5]): color = (params[idx + 2] << 16) + (params[idx + 3] << 8) + params[idx + 4] colors = 16777216 # 2 ** 24 if attr == 38: fg = color else: bg = color idx += 4 elif attr == 39: fg = None elif attr == 49: bg = None elif attr in (10, 11, 12): # vterm-only charset/display-control side effects; left to the caller pass elif attr == 1: attributes.add("bold") elif attr == 2: attributes.add("faint") elif attr == 4: attributes.add("underline") elif attr == 5: attributes.add("blink") elif attr == 7: attributes.add("standout") elif attr == 22: attributes.discard("bold") attributes.discard("faint") elif attr == 24: attributes.discard("underline") elif attr == 25: attributes.discard("blink") elif attr == 27: attributes.discard("standout") elif attr == 0: fg = bg = None attributes.clear() idx += 1 if "bold" in attributes and colors == 16 and fg is not None and fg < 8: fg += 8 def _defaulter(req_color: int | None) -> str: if req_color is None: return "default" # Note: 88-colour mode cannot be distinguished from 256-colour mode here if req_color > 255 or colors == 2**24: return color_desc_true(req_color) if req_color > 15 or colors == 256: return color_desc_256(req_color) return BASIC_COLORS[req_color] decoded_fg = _defaulter(fg) decoded_bg = _defaulter(bg) if attributes: decoded_fg = ",".join((decoded_fg, *attributes)) if decoded_fg == decoded_bg == "default": return None if colors: # `colors` is only ever assigned one of AttrSpec's accepted literals above (1, 16, 88 -- via # `previous.colors` --, 256 or 16777216), but the running total is tracked as a plain `int` # through the `max()` calls, so the literal type is lost to mypy; the cast restores it. return AttrSpec(decoded_fg, decoded_bg, colors=typing.cast("typing.Literal[1, 16, 88, 256, 16777216]", colors)) return AttrSpec(decoded_fg, decoded_bg) urwid-urwid-69907e4/urwid/canvas.py000066400000000000000000002013521526073734300172610ustar00rootroot00000000000000# Urwid canvas class and functions # Copyright (C) 2004-2011 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """Canvases: the rendered, cacheable output of widgets, and the functions that combine them.""" from __future__ import annotations import contextlib import dataclasses import itertools import re import typing import warnings import weakref import wcwidth from urwid.str_util import calc_text_pos, calc_width, get_byte_encoding from urwid.text_layout import LayoutSegment, trim_line from urwid.util import ( apply_target_encoding, get_encoding, rle_append_modify, rle_join_modify, rle_len, rle_product, trim_text_attr_cs, ) if typing.TYPE_CHECKING: from collections.abc import Hashable, Iterable, Iterator, Mapping, Sequence from typing_extensions import Literal, NotRequired from .display.common import AttrSpec from .widget import AbstractWidget _ContentLine = list[tuple[typing.Union[AttrSpec, str, None], typing.Union[Literal["0", "U"], None], bytes]] _CView = tuple[int, int, int, int, typing.Union[dict[Hashable, Hashable], None], "Canvas"] _CanvasCoords = typing.TypedDict( "_CanvasCoords", { "pop up": NotRequired[tuple[int, int, tuple[AbstractWidget, int, int]]], "cursor": NotRequired[tuple[int, int, None]], }, ) def _walk_depends(canv: Canvas) -> list[AbstractWidget]: """Collect all child widgets of *canv* for determining who a cached canvas depends on.""" # FIXME: is this recursion necessary? The cache invalidating might work with only one level. depends = [] for _x, _y, c, _pos in canv.children: # type: ignore[attr-defined] # we made `hasattr` check if c.widget_info: depends.append(c.widget_info[0]) elif hasattr(c, "children"): depends.extend(_walk_depends(c)) return depends class CanvasCache: """Cache for rendered canvases. Automatically populated and accessed by Widget render() MetaClass magic, cleared by Widget._invalidate(). Stores weakrefs to the canvas objects, so an external class must maintain a reference for this cache to be effective. At present the Screen classes store the last topmost canvas after redrawing the screen, keeping the canvases from being garbage collected. _widgets[widget] = {(wcls, size, focus): weakref.ref(canvas), ...} _refs[weakref.ref(canvas)] = (widget, wcls, size, focus) _deps[widget] = {dependent_widget, ...} _children[dependent_widget] = {widget, ...} _children is the reverse of _deps. It removes a dependent widget from _deps once that widget has no cached canvas left. Widgets are not guaranteed to be weak-referenceable, so neither index can use weak references. """ _widgets: typing.ClassVar[ dict[ AbstractWidget, dict[ tuple[type[AbstractWidget], tuple[int, int] | tuple[int] | tuple[()], bool], weakref.ReferenceType[Canvas], ], ] ] = {} _refs: typing.ClassVar[ dict[ weakref.ReferenceType[Canvas], tuple[AbstractWidget, type[AbstractWidget], tuple[int, int] | tuple[int] | tuple[()], bool], ] ] = {} _deps: typing.ClassVar[dict[AbstractWidget, set[AbstractWidget]]] = {} _children: typing.ClassVar[dict[AbstractWidget, set[AbstractWidget]]] = {} hits = 0 fetches = 0 cleanups = 0 @classmethod def store(cls, wcls: type[AbstractWidget], canvas: Canvas) -> None: """ Store a weakref to canvas in the cache. :param wcls: widget class that contains render() function :param canvas: rendered canvas with widget_info (widget, size, focus) :raises TypeError: *canvas* has not been finalized, so it carries no widget_info. """ if not canvas.cacheable: return info = canvas.widget_info if not info: raise TypeError("Can't store canvas without widget_info") widget, size, focus = info # use explicit depends_on if available from the canvas depends_on = getattr(canvas, "depends_on", None) if depends_on is None and hasattr(canvas, "children"): depends_on = _walk_depends(canvas) widgets = cls._widgets if depends_on: for w in depends_on: if w not in widgets: return # cleanup() is a weakref callback and can run between any two lines below. # It drops a widget's dependencies once its sizes are empty, # so they are added only after the live canvas keeps the sizes non-empty. key = (wcls, size, focus) ref = weakref.ref(canvas, cls.cleanup) cls._refs[ref] = (widget, wcls, size, focus) sizes = widgets.get(widget) if sizes is None: sizes = widgets[widget] = {key: ref} else: sizes[key] = ref if not depends_on or widgets.get(widget) is not sizes: # A cleanup dropped the emptied sizes before ref was added to them: the canvas is not cached. return cls._children.setdefault(widget, set()).update(depends_on) deps = cls._deps for w in depends_on: deps.setdefault(w, set()).add(widget) if widget not in deps.get(w, ()): # A cleanup emptied and dropped the set between the lookup and the add. deps[w] = {widget} @classmethod def fetch( cls, widget: AbstractWidget, wcls: type[AbstractWidget], size: tuple[int, int] | tuple[int] | tuple[()], focus: bool, ) -> Canvas | None: """ Return the cached canvas or None. :param widget: widget object requested :param wcls: widget class that contains render() function :param size: size parameter passed to the widget's render method :param focus: focus parameter passed to the widget's render method """ cls.fetches += 1 # collect stats sizes = cls._widgets.get(widget, None) if not sizes: return None ref = sizes.get((wcls, size, focus), None) if not ref: return None canv = ref() if canv: cls.hits += 1 # more stats return canv @classmethod def invalidate(cls, widget: AbstractWidget) -> None: """Remove all canvases cached for widget.""" sizes = cls._widgets.pop(widget, None) if sizes: refs = cls._refs for ref in sizes.values(): refs.pop(ref, None) cls._forget(widget) # The popped set is out of reach of every other frame, so the recursion cannot change it. dependants = cls._deps.pop(widget, None) if not dependants: return for w in dependants: cls.invalidate(w) @classmethod def cleanup(cls, ref: weakref.ReferenceType[Canvas]) -> None: """Drop the cache entry for a canvas weakref once the canvas it points to has been garbage collected.""" cls.cleanups += 1 # collect stats w = cls._refs.pop(ref, None) if not w: return widget, wcls, size, focus = w key = (wcls, size, focus) sizes = cls._widgets.get(widget, None) # A later store() for the same key replaced ref: that entry belongs to a live canvas. if not sizes or sizes.get(key) is not ref: return sizes.pop(key, None) if not sizes: cls._widgets.pop(widget, None) # Dependants still cached under another key must not keep this widget alive through _children. children = cls._children for dependant in cls._deps.pop(widget, ()): siblings = children.get(dependant) if siblings is not None: siblings.discard(widget) cls._forget(widget) @classmethod def _forget(cls, widget: AbstractWidget) -> None: """Remove *widget* from the dependants of every widget its cached canvases depended on.""" # Iterate the popped set: no other frame can reach it, so a nested cleanup() cannot change it. children = cls._children.pop(widget, None) if not children: return deps = cls._deps for child in children: dependants = deps.get(child) if dependants is not None: dependants.discard(widget) if not dependants: deps.pop(child, None) @classmethod def clear(cls) -> None: """Empty the cache.""" cls._widgets = {} cls._refs = {} cls._deps = {} cls._children = {} class CanvasError(Exception): """Raised for errors in canvas content or canvas cache management.""" class Canvas: """base class for canvases.""" __slots__ = ("__dict__", "__weakref__", "_widget_info", "coords", "shortcuts") cacheable = True # A message rather than a shared exception: re-raising one instance chains every traceback onto it, # and those frames keep the canvases and widgets of every failed call alive. _finalized_message: typing.ClassVar[str] = ( "This canvas has been finalized. Use CompositeCanvas to wrap this canvas if you need to make changes." ) def __init__(self) -> None: """Initialize the base canvas state shared by all canvas subclasses.""" self._widget_info: tuple[AbstractWidget, tuple[()] | tuple[int] | tuple[int, int], bool] | None = None self.coords: _CanvasCoords = {} self.shortcuts: dict[str, str] = {} def finalize( self, widget: AbstractWidget, size: tuple[()] | tuple[int] | tuple[int, int], focus: bool, ) -> None: """Mark this canvas as finalized (should not be any future changes to its content). This is required before caching the canvas. This happens automatically after a widget's 'render call returns the canvas thanks to some metaclass magic. :param widget: widget that rendered this canvas :param size: size parameter passed to widget's render method :param focus: focus parameter passed to widget's render method :raises CanvasError: this canvas has already been finalized and can no longer be modified. """ if self.widget_info: raise CanvasError(self._finalized_message) self._widget_info = widget, size, focus @property def widget_info(self) -> tuple[AbstractWidget, tuple[()] | tuple[int] | tuple[int, int], bool] | None: """Return the ``(widget, size, focus)`` this canvas was finalized with, or ``None`` if not yet finalized.""" return self._widget_info @property def text(self) -> list[bytes]: """Return the text content of the canvas as a list of strings, one for each row.""" return [b"".join(text for (attr, cs, text) in row) for row in self.content()] @property def decoded_text(self) -> Sequence[str]: """Decoded text content of the canvas as a sequence of strings, one for each row.""" encoding = get_encoding() return tuple(line.decode(encoding) for line in self.text) def content( self, trim_left: int = 0, trim_top: int = 0, cols: int = 0, rows: int = 0, attr: Mapping[Hashable, AttrSpec | str | None] | None = None, ) -> Iterator[_ContentLine]: """ Return the canvas content as a list of rows of ``(attr, cs, text)`` tuples. :raises NotImplementedError: the subclass does not implement the canvas content protocol. """ raise NotImplementedError() def cols(self) -> int: """ Return the screen column width of this canvas. :raises NotImplementedError: the subclass does not report its own width. """ raise NotImplementedError() def rows(self) -> int: """ Return the screen row height of this canvas. :raises NotImplementedError: the subclass does not report its own height. """ raise NotImplementedError() def content_delta(self, other: Canvas) -> list[int] | Iterator[_ContentLine]: """Delta between two canvases. :returns: a list of row deltas if other is None, otherwise an iterator of row deltas. :raises NotImplementedError: the subclass does not implement the canvas content protocol. .. deprecated:: 4.0.3 Not used by the code base; there is no replacement. It will be removed in version 5.0. """ warnings.warn( "content_delta is not used by the code base and will be removed in version 5.0", DeprecationWarning, stacklevel=2, ) raise NotImplementedError() def get_cursor(self) -> tuple[int, int] | None: """Return the cursor position as ``(x, y)``, or ``None`` if no cursor is set.""" if c := self.coords.get("cursor", None): return c[:2] # trim off data part return None def set_cursor(self, c: tuple[int, int] | None) -> None: """ Set the cursor position to ``(x, y)``, or remove it when *c* is ``None``. :raises CanvasError: this canvas has already been finalized and can no longer be modified. """ if self.widget_info and self.cacheable: raise CanvasError(self._finalized_message) if c is None: self.coords.pop("cursor", None) return self.coords["cursor"] = (*c, None) # data part cursor = property(get_cursor, set_cursor) def get_pop_up(self) -> tuple[int, int, tuple[AbstractWidget, int, int]] | None: """Return the pop-up info set on this canvas, or ``None`` if none was set.""" return self.coords.get("pop up", None) def set_pop_up( self, w: AbstractWidget, left: int, top: int, overlay_width: int, overlay_height: int, ) -> None: """Add pop-up information to the canvas. This information is intercepted by a PopUpTarget widget higher in the chain to display a pop-up at the given (left, top) position relative to the current canvas. :param w: widget to use for the pop-up :param left: x position for left edge of pop-up >= 0 :param top: y position for top edge of pop-up >= 0 :param overlay_width: width of overlay in screen columns > 0 :param overlay_height: height of overlay in screen rows > 0 :raises CanvasError: the canvas is already finalised, the position is negative or the overlay size is not positive """ if self.widget_info and self.cacheable: raise CanvasError(self._finalized_message) if left < 0 or top < 0: raise CanvasError(f"Pop-up position must not be negative, got left={left!r} and top={top!r}") if overlay_width <= 0 or overlay_height <= 0: raise CanvasError( f"Pop-up size must be positive, " f"got overlay_width={overlay_width!r} and overlay_height={overlay_height!r}" ) self.coords["pop up"] = (left, top, (w, overlay_width, overlay_height)) def translate_coords(self, dx: int, dy: int) -> _CanvasCoords: """Return coords shifted by (dx, dy).""" d: _CanvasCoords = {} for name, (x, y, data) in self.coords.items(): # type: ignore[misc] # MyPy issue with expansion of TypedDict d[name] = (x + dx, y + dy, data) # type: ignore[has-type, literal-required] return d def __repr__(self) -> str: """Return a debug representation including the canvas size, cursor, and finalized state.""" extra = [""] with contextlib.suppress(BaseException): extra.append(f"cols={self.cols()}") with contextlib.suppress(BaseException): extra.append(f"rows={self.rows()}") if self.cursor: extra.append(f"cursor={self.cursor}") return f"<{self.__class__.__name__} finalized={bool(self.widget_info)}{' '.join(extra)} at 0x{id(self):X}>" def __str__(self) -> str: """Return the canvas content decoded to text, one line per row.""" with contextlib.suppress(BaseException): return "\n".join(self.decoded_text) return repr(self) class TextCanvas(Canvas): """class for storing rendered text and attributes.""" def __init__( self, text: list[bytes] | None = None, attr: list[list[tuple[Hashable, int]]] | None = None, cs: list[list[tuple[Literal["0", "U"] | None, int]]] | None = None, cursor: tuple[int, int] | None = None, maxcol: int | None = None, check_width: bool = True, ) -> None: """Build a text canvas from the given lines and attributes. :param text: list of strings, one for each line :param attr: list of run length encoded attributes for text :param cs: list of run length encoded character set for text :param cursor: (x,y) of cursor or None :param maxcol: screen columns taken by this canvas :param check_width: check and fix width of all lines in text :raises CanvasError: a line of *text* is not a plain string in the screen encoding, is wider than *maxcol*, or has an attribute or character set run extending beyond its text. :raises TypeError: *maxcol* is not an integer. """ super().__init__() if text is None: text = [] if check_width: widths = [] for t in text: if not isinstance(t, bytes): raise CanvasError( "Canvas text must be plain strings encoded in the screen's encoding", repr(text), ) widths.append(calc_width(t, 0, len(t))) else: if not isinstance(maxcol, int): raise TypeError(maxcol) widths = [maxcol] * len(text) if maxcol is None: if widths: # find maxcol ourselves maxcol = max(widths) else: maxcol = 0 if attr is None: attr = [[] for _ in range(len(text))] if cs is None: cs = [[] for _ in range(len(text))] # pad text and attr to maxcol for i, (w, a_row, cs_row) in enumerate(zip(widths, attr, cs)): if w > maxcol: raise CanvasError( f"Canvas text is wider than the maxcol specified:\n" f"maxcol={maxcol!r}\n" f"widths={widths!r}\n" f"text={text!r}\n" f"urwid target encoding={get_encoding()}" ) t = text[i] if w < maxcol: t += b"".rjust(maxcol - w) text[i] = t a_gap = len(t) - rle_len(a_row) if a_gap < 0: raise CanvasError(f"Attribute extends beyond text \n{t!r}\n{a_row!r}") if a_gap: rle_append_modify(a_row, (None, a_gap)) cs_gap = len(t) - rle_len(cs_row) if cs_gap < 0: raise CanvasError(f"Character Set extends beyond text \n{t!r}\n{cs_row!r}") if cs_gap: rle_append_modify(cs_row, (None, cs_gap)) # type: ignore[arg-type] # str|None is Hashable self._attr = attr self._cs = cs self.cursor = cursor self._text = text self._maxcol = maxcol def rows(self) -> int: """Return the number of rows in this canvas.""" return len(self._text) def cols(self) -> int: """Return the screen column width of this canvas.""" return self._maxcol def translated_coords(self, dx: int, dy: int) -> tuple[int, int] | None: """Return cursor coords shifted by (dx, dy), or None if there is no cursor.""" if self.cursor: x, y = self.cursor return x + dx, y + dy return None def content( self, trim_left: int = 0, trim_top: int = 0, cols: int = 0, rows: int = 0, attr: Mapping[object, AttrSpec | str | None] | None = None, ) -> Iterator[_ContentLine]: """Return the canvas content as a list of rows where each row is a list of (attr, cs, text) tuples. trim_left, trim_top, cols, rows may be set by CompositeCanvas when rendering a partially obscured canvas. :raises ValueError: *trim_left* or *trim_top*, together with *cols* or *rows*, selects a region outside this canvas. """ maxcol, maxrow = self.cols(), self.rows() if not cols: cols = maxcol - trim_left if not rows: rows = maxrow - trim_top # a canvas without columns (empty text) has one region only: all of it, zero columns wide if not ( (0 <= trim_left < maxcol and cols > 0 and trim_left + cols <= maxcol) or maxcol == trim_left == cols == 0 ): raise ValueError(trim_left) if not ((0 <= trim_top < maxrow) and (rows > 0 and trim_top + rows <= maxrow)): raise ValueError(trim_top) if trim_top or rows < maxrow: text_attr_cs = zip( self._text[trim_top : trim_top + rows], self._attr[trim_top : trim_top + rows], self._cs[trim_top : trim_top + rows], ) else: text_attr_cs = zip(self._text, self._attr, self._cs) for text, a_row, cs_row in text_attr_cs: if trim_left or cols < self._maxcol: text, a_row, cs_row = trim_text_attr_cs( # type: ignore[assignment] # noqa: PLW2901 text, a_row, cs_row, # type: ignore[arg-type] # str|None is Hashable trim_left, trim_left + cols, ) attr_cs = typing.cast( "list[tuple[tuple[Hashable, Literal['0', 'U'] | None], int]]", rle_product(a_row, cs_row), # type: ignore[arg-type] # str|None is Hashable ) i = 0 row = [] for (a, cs), run in attr_cs: if attr: a = attr.get(a, a) # noqa: PLW2901 # single lookup instead of `in` + `[]` row.append((typing.cast("AttrSpec | str | None", a), cs, text[i : i + run])) i += run yield row def content_delta(self, other: Canvas) -> list[int] | Iterator[_ContentLine]: """ Return the differences between other and this canvas. If other is the same object as self this will return no differences, otherwise this is the same as calling content(). .. deprecated:: 4.0.3 Not used by the code base; there is no replacement. It will be removed in version 5.0. """ warnings.warn( "content_delta is not used by the code base and will be removed in version 5.0", DeprecationWarning, stacklevel=2, ) if other is self: return [self.cols()] * self.rows() return self.content() class BlankCanvas(Canvas): """A canvas with nothing on it. Only works as part of a composite canvas since it doesn't know its own size. """ __slots__ = () def content( self, trim_left: int = 0, trim_top: int = 0, cols: int = 0, rows: int = 0, attr: Mapping[Hashable, AttrSpec | str | None] | None = None, ) -> Iterator[_ContentLine]: """Return (cols, rows) of spaces with default attributes.""" def_attr = attr.get(None) if attr else None line = [(def_attr, None, b"".rjust(cols))] for _ in range(rows): yield line # type: ignore[misc] # Yes, list is invariant, but we return it def cols(self) -> typing.NoReturn: """ Raise :exc:`NotImplementedError`: a BlankCanvas does not know its own size. :raises NotImplementedError: a BlankCanvas does not know its own size. """ raise NotImplementedError("BlankCanvas doesn't know its own size!") def rows(self) -> typing.NoReturn: """ Raise :exc:`NotImplementedError`: a BlankCanvas does not know its own size. :raises NotImplementedError: a BlankCanvas does not know its own size. """ raise NotImplementedError("BlankCanvas doesn't know its own size!") def content_delta(self, other: Canvas) -> typing.NoReturn: """ Raise :exc:`NotImplementedError`: a BlankCanvas does not know its own size. .. deprecated:: 4.0.3 Not used by the code base; there is no replacement. It will be removed in version 5.0. :raises NotImplementedError: a BlankCanvas does not know its own size. """ warnings.warn( "content_delta is not used by the code base and will be removed in version 5.0", DeprecationWarning, stacklevel=2, ) raise NotImplementedError("BlankCanvas doesn't know its own size!") blank_canvas = BlankCanvas() class SolidCanvas(Canvas): """A canvas filled completely with a single character.""" def __init__(self, fill_char: str | bytes, cols: int, rows: int) -> None: """ Build a canvas of *cols* by *rows* screen cells, every one holding *fill_char*. :raises ValueError: *fill_char* is not exactly one screen column wide. """ super().__init__() end, col = calc_text_pos(fill_char, 0, len(fill_char), 1) if col != 1: raise ValueError(f"Invalid fill_char: {fill_char!r}") self._text, cs = apply_target_encoding(fill_char[:end]) self._cs = cs[0][0] self.size = cols, rows self.cursor = None def cols(self) -> int: """Return the screen column width of this canvas.""" return self.size[0] def rows(self) -> int: """Return the screen row height of this canvas.""" return self.size[1] def content( self, trim_left: int = 0, trim_top: int = 0, cols: int | None = None, rows: int | None = None, attr: Mapping[Hashable, AttrSpec | str | None] | None = None, ) -> Iterator[_ContentLine]: """Return the canvas content as rows of ``(attr, cs, text)`` tuples, each row filled with *fill_char*.""" if cols is None: cols = self.size[0] if rows is None: rows = self.size[1] def_attr = attr.get(None) if attr else None line = [(def_attr, self._cs, self._text * cols)] for _ in range(rows): yield line def content_delta(self, other: Canvas) -> list[int] | Iterator[_ContentLine]: """ Return the differences between other and this canvas. .. deprecated:: 4.0.3 Not used by the code base; there is no replacement. It will be removed in version 5.0. """ warnings.warn( "content_delta is not used by the code base and will be removed in version 5.0", DeprecationWarning, stacklevel=2, ) if other is self: return [self.cols()] * self.rows() return self.content() class CompositeCanvas(Canvas): """class for storing a combination of canvases.""" def __init__(self, canv: Canvas | None = None) -> None: """ :param canv: a Canvas object to wrap this CompositeCanvas around. if canv is a CompositeCanvas, make a copy of its contents """ # a "shard" is a (num_rows, list of cviews) tuple, # one for each cview starting in this shard # a "cview" is a tuple that defines a view of a canvas: # (trim_left, trim_top, cols, rows, attr_map, canv) # a "shard tail" is a list of tuples: # (col_gap, done_rows, content_iter, cview) # tuples that define the unfinished cviews that are part of shards following the first shard. super().__init__() self.depends_on: Sequence[AbstractWidget] | None = None if canv is None: self.shards: list[tuple[int, list[_CView]]] = [] self.children: list[tuple[int, int, Canvas, typing.Any]] = [] else: shards = getattr(canv, "shards", None) if shards is not None: self.shards = shards else: rows = canv.rows() self.shards = [(rows, [(0, 0, canv.cols(), rows, None, canv)])] self.children = [(0, 0, canv, None)] self.coords.update(canv.coords) for shortcut in canv.shortcuts: self.shortcuts[shortcut] = "wrap" def __repr__(self) -> str: """Return a debug representation including the canvas size, cursor, finalized state, and children.""" extra = [""] with contextlib.suppress(BaseException): extra.append(f"cols={self.cols()}") with contextlib.suppress(BaseException): extra.append(f"rows={self.rows()}") if self.cursor: extra.append(f"cursor={self.cursor}") if self.children: extra.append(f"children=({', '.join(repr(canv) for _, _, canv, _ in self.children)})") return f"<{self.__class__.__name__} finalized={bool(self.widget_info)}{' '.join(extra)} at 0x{id(self):X}>" def rows(self) -> int: """ Return the screen row height of this canvas. :raises TypeError: a shard carries a non-integer row count. """ rows = 0 for r, cv in self.shards: if not isinstance(r, int): raise TypeError(r, cv) rows += r return rows def cols(self) -> int: """ Return the screen column width of this canvas. :raises TypeError: the shards add up to a non-integer column count. """ if not self.shards: return 0 cols = 0 for cv in self.shards[0][1]: cols += cv[2] if not isinstance(cols, int): raise TypeError(cols) return cols def content( self, trim_left: int = 0, trim_top: int = 0, cols: int = 0, rows: int = 0, attr: Mapping[Hashable, AttrSpec | str | None] | None = None, ) -> Iterator[_ContentLine]: """ Return the canvas content as a list of rows where each row is a list of (attr, cs, text) tuples. All parameters are ignored. """ shard_tail: list[tuple[int, int, Iterator[_ContentLine] | None, _CView]] = [] for num_rows, cviews in self.shards: # combine shard and shard tail sbody = shard_body(cviews, shard_tail, num_rows=num_rows) # output rows for _ in range(num_rows): yield shard_body_row(sbody) # prepare next shard tail shard_tail = shard_body_tail(num_rows, sbody) def content_delta(self, other: Canvas) -> Iterator[_ContentLine]: """ Return the differences between other and this canvas. .. deprecated:: 4.0.3 Not used by the code base; there is no replacement. It will be removed in version 5.0. """ warnings.warn( "content_delta is not used by the code base and will be removed in version 5.0", DeprecationWarning, stacklevel=2, ) if not hasattr(other, "shards"): yield from self.content() else: shard_tail: list[tuple[int, int, Iterator[_ContentLine] | None, _CView]] = [] for num_rows, cviews in shards_delta(self.shards, other.shards): # combine shard and shard tail # broken contract, content_delta is deprecated and not used sbody = shard_body(cviews, shard_tail) # type: ignore[arg-type] # output rows row: _ContentLine = [] for _ in range(num_rows): # if whole shard is unchanged, don't keep # calling shard_body_row if len(row) != 1 or not isinstance(row[0], int): row = shard_body_row(sbody) yield row # prepare next shard tail shard_tail = shard_body_tail(num_rows, sbody) def trim(self, top: int, count: int | None = None) -> None: """Trim lines from the top and/or bottom of canvas. :param top: number of lines to remove from top :param count: number of lines to keep, or None for all the rest :raises ValueError: *top* is negative, or is at least the number of rows in this canvas. :raises CanvasError: this canvas has already been finalized and can no longer be modified. """ if top < 0: raise ValueError(f"invalid trim amount {top:d}!") if top >= self.rows(): raise ValueError(f"cannot trim {top:d} lines from {self.rows():d}!") if self.widget_info: raise CanvasError(self._finalized_message) if top: self.shards = shards_trim_top(self.shards, top) if count == 0: self.shards = [] elif count is not None: self.shards = shards_trim_rows(self.shards, count) self.coords = self.translate_coords(0, -top) def trim_end(self, end: int) -> None: """Trim lines from the bottom of the canvas. :param end: number of lines to remove from the end :raises ValueError: *end* is not positive, or is greater than the number of rows in this canvas. :raises CanvasError: this canvas has already been finalized and can no longer be modified. """ if end <= 0: raise ValueError(f"invalid trim amount {end:d}!") if end > self.rows(): raise ValueError(f"cannot trim {end:d} lines from {self.rows():d}!") if self.widget_info: raise CanvasError(self._finalized_message) self.shards = shards_trim_rows(self.shards, self.rows() - end) def pad_trim_left_right(self, left: int, right: int) -> None: """ Pad or trim this canvas on the left and right. values > 0 indicate screen columns to pad values < 0 indicate screen columns to trim :raises CanvasError: this canvas has already been finalized and can no longer be modified. """ if self.widget_info: raise CanvasError(self._finalized_message) shards = self.shards if left < 0 or right < 0: trim_left = max(0, -left) cols = self.cols() - trim_left - max(0, -right) shards = shards_trim_sides(shards, trim_left, cols) rows = self.rows() if left > 0 or right > 0: top_rows, top_cviews = shards[0] if left > 0: new_top_cviews = [(0, 0, left, rows, None, blank_canvas), *top_cviews] else: new_top_cviews = top_cviews.copy() if right > 0: new_top_cviews.append((0, 0, right, rows, None, blank_canvas)) shards = [(top_rows, new_top_cviews), *shards[1:]] self.coords = self.translate_coords(left, 0) self.shards = shards def pad_trim_top_bottom(self, top: int, bottom: int) -> None: """ Pad or trim this canvas on the top and bottom. :raises CanvasError: this canvas has already been finalized and can no longer be modified. """ if self.widget_info: raise CanvasError(self._finalized_message) orig_shards = self.shards if top < 0 or bottom < 0: trim_top = max(0, -top) rows = self.rows() - trim_top - max(0, -bottom) self.trim(trim_top, rows) cols = self.cols() if top > 0: self.shards = [(top, [(0, 0, cols, top, None, blank_canvas)]), *self.shards] self.coords = self.translate_coords(0, top) if bottom > 0: if orig_shards is self.shards: self.shards = self.shards.copy() self.shards.append((bottom, [(0, 0, cols, bottom, None, blank_canvas)])) def overlay(self, other: CompositeCanvas, left: int, top: int) -> None: """Overlay other onto this canvas. :raises CanvasError: this canvas has already been finalized and can no longer be modified. :raises ValueError: *other* does not fit within this canvas at the given *left* and *top* offsets. """ if self.widget_info: raise CanvasError(self._finalized_message) width = other.cols() height = other.rows() right = self.cols() - left - width bottom = self.rows() - top - height if right < 0: raise ValueError(f"top canvas of overlay not the size expected!{(other.cols(), left, right, width)!r}") if bottom < 0: raise ValueError(f"top canvas of overlay not the size expected!{(other.rows(), top, bottom, height)!r}") shards = self.shards top_shards = [] side_shards = self.shards bottom_shards = [] if top: side_shards = shards_trim_top(shards, top) top_shards = shards_trim_rows(shards, top) if bottom: bottom_shards = shards_trim_top(side_shards, height) side_shards = shards_trim_rows(side_shards, height) left_shards = [] right_shards = [] if left > 0: left_shards = [shards_trim_sides(side_shards, 0, left)] if right > 0: right_shards = [shards_trim_sides(side_shards, max(0, left + width), right)] if not self.rows(): middle_shards = [] elif left or right: middle_shards = shards_join((*left_shards, other.shards, *right_shards)) else: middle_shards = other.shards self.shards = top_shards + middle_shards + bottom_shards self.coords.update(other.translate_coords(left, top)) def fill_attr(self, a: Hashable) -> None: """Apply attribute a to all areas of this canvas with default attribute currently set to None. Other attributes are left intact. """ self.fill_attr_apply({None: a}) def fill_attr_apply(self, mapping: dict[Hashable, Hashable]) -> None: """ Apply an attribute-mapping dictionary to the canvas. :param mapping: dictionary of original-attribute:new-attribute items :raises CanvasError: this canvas has already been finalized and can no longer be modified. """ if self.widget_info: raise CanvasError(self._finalized_message) shards: list[tuple[int, list[_CView]]] = [] for num_rows, original_cviews in self.shards: new_cviews: list[_CView] = [] for cv in original_cviews: # cv[4] == attr_map if cv[4] is None: new_cviews.append((*cv[:4], mapping, *cv[5:])) else: combined = mapping.copy() combined.update((k, mapping.get(v, v)) for k, v in cv[4].items()) new_cviews.append((*cv[:4], combined, *cv[5:])) shards.append((num_rows, new_cviews)) self.shards = shards def set_depends(self, widget_list: Sequence[AbstractWidget]) -> None: """Explicitly specify the list of widgets that this canvas depends on. If any of these widgets change this canvas will have to be updated. :raises CanvasError: this canvas has already been finalized and can no longer be modified. """ if self.widget_info: raise CanvasError(self._finalized_message) self.depends_on = widget_list def shard_body_row(sbody: list[tuple[int, Iterator[_ContentLine] | None, _CView]]) -> _ContentLine: """ Return one row, advancing the iterators in sbody. ** MODIFIES sbody by calling next() on its iterators ** :raises ValueError: a shard body entry has no content iterator. """ row = [] for _done_rows, content_iter, _cview in sbody: if content_iter: row.extend(next(content_iter)) else: msg = f"Invalid canvas state: content_iter is falsy: {content_iter!r}. Impossible to display." raise ValueError(msg) return row def shard_body_tail( num_rows: int, sbody: list[tuple[int, Iterator[_ContentLine] | None, _CView]], ) -> list[tuple[int, int, Iterator[_ContentLine] | None, _CView]]: """Return a new shard tail that follows this shard body.""" shard_tail = [] col_gap = 0 for done_rows, content_iter, cview in sbody: cols, rows = cview[2:4] done_rows += num_rows # noqa: PLW2901 if done_rows == rows: col_gap += cols continue shard_tail.append((col_gap, done_rows, content_iter, cview)) col_gap = 0 return shard_tail def shards_delta( shards: list[tuple[int, list[_CView]]], other_shards: list[tuple[int, list[_CView]]], ) -> Iterator[tuple[int, Iterable[_CView | tuple[int, int, int, int, dict[Hashable, Hashable] | None, None]]]]: """ Yield shards1 with cviews that are the same as shards2 having canv = None. .. deprecated:: 4.0.3 Not used by the code base; there is no replacement. It will be removed in version 5.0. """ warnings.warn( "shards_delta is not used by the code base and will be removed in version 5.0", DeprecationWarning, stacklevel=2, ) # pylint: disable=stop-iteration-return other_shards_iter = iter(other_shards) other_num_rows = other_cviews = None done = other_done = 0 for num_rows, cviews in shards: if other_num_rows is None: other_num_rows, other_cviews = next(other_shards_iter) while other_done < done: other_done += other_num_rows other_num_rows, other_cviews = next(other_shards_iter) if other_done > done: yield (num_rows, cviews) done += num_rows continue # top-aligned shards, compare each cview yield (num_rows, shard_cviews_delta(cviews, other_cviews)) # type: ignore[arg-type] # `list` is `Iterable` other_done += other_num_rows other_num_rows = None done += num_rows def shard_cviews_delta( cviews: Iterable[_CView], other_cviews: Iterable[_CView], ) -> Iterator[_CView | tuple[int, int, int, int, dict[Hashable, Hashable] | None, None]]: """Return iterator of cviews with differences between shards. If Canvas and shard tail are equal between shards, return None instead of canvas. .. deprecated:: 4.0.3 Not used by the code base; there is no replacement. It will be removed in version 5.0. """ warnings.warn( "shard_cviews_delta is not used by the code base and will be removed in version 5.0", DeprecationWarning, stacklevel=2, ) # pylint: disable=stop-iteration-return other_cviews_iter = iter(other_cviews) other_cv = None cols = other_cols = 0 for cv in cviews: if other_cv is None: other_cv = next(other_cviews_iter) while other_cols < cols: other_cols += other_cv[2] other_cv = next(other_cviews_iter) if other_cols > cols: yield cv cols += cv[2] continue # top-left-aligned cviews, compare them if cv[5] is other_cv[5] and cv[:5] == other_cv[:5]: yield (*cv[:5], None, *cv[6:]) else: yield cv other_cols += other_cv[2] other_cv = None cols += cv[2] def shard_body( cviews: Iterable[_CView], shard_tail: list[tuple[int, int, Iterator[_ContentLine] | None, _CView]], create_iter: bool = True, iter_default: Iterator[_ContentLine] | None = None, num_rows: int | None = None, ) -> list[tuple[int, Iterator[_ContentLine] | None, _CView]]: """ Return a list of (done_rows, content_iter, cview) tuples for this shard and shard tail. If a canvas in cviews is None (eg. when unchanged from shard_cviews_delta()) or if create_iter is False then no iterator is created for content_iter. iter_default is the value used for content_iter when no iterator is created. num_rows is the row count of the shard being processed. It is used to bound a blank filler cview if cviews run out before a shard_tail gap is filled (see below); pass it whenever the caller knows how many rows this shard spans. :raises CanvasError: the cviews overflow the gaps left by *shard_tail*. """ col = 0 body: list[tuple[int, Iterator[_ContentLine] | None, _CView]] = [] # build the next shard tail cviews_iter = iter(cviews) new_iter: Iterator[_ContentLine] | None for col_gap, done_rows, content_iter, tail_cview in shard_tail: while col_gap: try: cview = next(cviews_iter) except StopIteration: # The shard calculation is inconsistent: this shard's cviews don't add up # to the width the previous shard's tail expects to continue into. This is # a known open issue (https://github.com/urwid/urwid/issues/340) that we # haven't been able to root-cause; rather than raising (crashing the whole # app) or silently leaving the row short (a corrupted-looking canvas), pad # the unmet gap with blank filler so the canvas stays rectangular. fill_rows = num_rows if num_rows is not None else done_rows + 1 filler_cview = (0, 0, col_gap, fill_rows, None, blank_canvas) new_iter = blank_canvas.content(0, 0, col_gap, fill_rows, None) if create_iter else iter_default body.append((0, new_iter, filler_cview)) col += col_gap col_gap = 0 # noqa: PLW2901 break (trim_left, trim_top, cols, rows, attr_map, canv) = cview[:6] col += cols col_gap -= cols # noqa: PLW2901 if col_gap < 0: raise CanvasError("cviews overflow gaps in shard_tail!") if create_iter and canv: new_iter = canv.content(trim_left, trim_top, cols, rows, attr_map) # type: ignore[arg-type] else: new_iter = iter_default body.append((0, new_iter, cview)) body.append((done_rows, content_iter, tail_cview)) for cview in cviews_iter: (trim_left, trim_top, cols, rows, attr_map, canv) = cview[:6] if create_iter and canv: new_iter = canv.content(trim_left, trim_top, cols, rows, attr_map) # type: ignore[arg-type] else: new_iter = iter_default body.append((0, new_iter, cview)) return body def shards_trim_top( shards: Iterable[tuple[int, list[_CView]]], top: int, ) -> list[tuple[int, list[_CView]]]: """ Return shards with top rows removed. :raises ValueError: *top* is not positive. :raises CanvasError: *top* is at least the number of rows in *shards*, so nothing would be left. """ if top <= 0: raise ValueError(top) shard_iter = iter(shards) shard_tail: list[tuple[int, int, Iterator[_ContentLine] | None, _CView]] = [] # skip over shards that are completely removed for num_rows, cviews in shard_iter: if top < num_rows: break sbody = shard_body(cviews, shard_tail, False, num_rows=num_rows) shard_tail = shard_body_tail(num_rows, sbody) top -= num_rows else: raise CanvasError("tried to trim shards out of existence") sbody = shard_body(cviews, shard_tail, False, num_rows=num_rows) shard_tail = shard_body_tail(num_rows, sbody) # trim the top of this shard new_sbody = [(0, content_iter, cview_trim_top(cv, done_rows + top)) for done_rows, content_iter, cv in sbody] sbody = new_sbody new_shards = [(num_rows - top, [cv for done_rows, content_iter, cv in sbody])] # write out the rest of the shards new_shards.extend(shard_iter) return new_shards def shards_trim_rows( shards: list[tuple[int, list[_CView]]], keep_rows: int, ) -> list[tuple[int, list[_CView]]]: """ Return the topmost keep_rows rows from shards. :raises ValueError: *keep_rows* is negative. """ if keep_rows < 0: raise ValueError(keep_rows) new_shards = [] done_rows = 0 for num_rows, cviews in shards: if done_rows >= keep_rows: break new_cviews = [] for cv in cviews: if cv[3] + done_rows > keep_rows: new_cviews.append(cview_trim_rows(cv, keep_rows - done_rows)) else: new_cviews.append(cv) if num_rows + done_rows > keep_rows: new_shards.append((keep_rows - done_rows, new_cviews)) else: new_shards.append((num_rows, new_cviews)) done_rows += num_rows return new_shards def shards_trim_sides( shards: list[tuple[int, list[_CView]]], left: int, cols: int, ) -> list[tuple[int, list[_CView]]]: """ Return shards with starting from column left and cols total width. :raises ValueError: *left* is negative, or *cols* is not positive. """ if left < 0: raise ValueError(left) if cols <= 0: raise ValueError(cols) shard_tail: list[tuple[int, int, Iterator[_ContentLine] | None, _CView]] = [] new_shards: list[tuple[int, list[_CView]]] = [] right = left + cols for num_rows, cviews in shards: sbody = shard_body(cviews, shard_tail, False, num_rows=num_rows) shard_tail = shard_body_tail(num_rows, sbody) new_cviews = [] col = 0 for done_rows, _content_iter, cv in sbody: cv_cols = cv[2] next_col = col + cv_cols if done_rows or next_col <= left or col >= right: col = next_col continue if col < left: cv = cview_trim_left(cv, left - col) # noqa: PLW2901 col = left if next_col > right: cv = cview_trim_cols(cv, right - col) # noqa: PLW2901 new_cviews.append(cv) col = next_col if not new_cviews: prev_num_rows, prev_cviews = new_shards[-1] new_shards[-1] = (prev_num_rows + num_rows, prev_cviews) else: new_shards.append((num_rows, new_cviews)) return new_shards def shards_join(shard_lists: Iterable[list[tuple[int, list[_CView]]]]) -> list[tuple[int, list[_CView]]]: """Return the result of joining shard lists horizontally. All shards lists must have the same number of rows. """ shards_iters: list[Iterator[tuple[int, list[_CView]]]] = [iter(sl) for sl in shard_lists] shards_current: list[tuple[int, list[_CView] | None]] = [next(i) for i in shards_iters] new_shards = [] while True: new_cviews = [] num_rows = min(r for r, _cv in shards_current) shards_next: list[tuple[int, list[_CView] | None]] = [] for rows, cviews in shards_current: if cviews: new_cviews.extend(cviews) shards_next.append((rows - num_rows, None)) shards_current = shards_next new_shards.append((num_rows, new_cviews)) # advance to next shards try: for i in range(len(shards_current)): if shards_current[i][0] > 0: continue shards_current[i] = next(shards_iters[i]) except StopIteration: break return new_shards def cview_trim_rows(cv: _CView, rows: int) -> _CView: """Return a copy of *cv* with its row count replaced by *rows*.""" return (*cv[:3], rows, *cv[4:]) def cview_trim_top(cv: _CView, trim: int) -> _CView: """Return a copy of *cv* with *trim* rows removed from the top.""" return (cv[0], trim + cv[1], cv[2], cv[3] - trim, *cv[4:]) def cview_trim_left(cv: _CView, trim: int) -> _CView: """Return a copy of *cv* with *trim* columns removed from the left.""" return (cv[0] + trim, cv[1], cv[2] - trim, *cv[3:]) def cview_trim_cols(cv: _CView, cols: int) -> _CView: """Return a copy of *cv* with its column count replaced by *cols*.""" return (*cv[:2], cols, *cv[3:]) def CanvasCombine(canvas_info: Iterable[tuple[Canvas, typing.Any, bool]]) -> CompositeCanvas: """Stack canvases in l vertically and return resulting canvas. :param canvas_info: list of (canvas, position, focus) tuples: position a value that widget.set_focus will accept or None if not allowed focus True if this canvas is the one that would be in focus if the whole widget is in focus """ clist = [(CompositeCanvas(c), p, f) for c, p, f in canvas_info] combined_canvas = CompositeCanvas() shards = [] children = [] row = 0 focus_index = 0 for n, (canv, pos, focus) in enumerate(clist): if focus: focus_index = n children.append((0, row, canv, pos)) shards.extend(canv.shards) combined_canvas.coords.update(canv.translate_coords(0, row)) for shortcut in canv.shortcuts: combined_canvas.shortcuts[shortcut] = pos row += canv.rows() if focus_index: children = [children[focus_index], *children[:focus_index], *children[focus_index + 1 :]] combined_canvas.shards = shards combined_canvas.children = children # type: ignore[assignment] # `CompositeCanvas` is subtype of `Canvas` return combined_canvas def CanvasOverlay(top_c: CompositeCanvas, bottom_c: Canvas, left: int, top: int) -> CompositeCanvas: """Overlay canvas top_c onto bottom_c at position (left, top).""" overlayed_canvas = CompositeCanvas(bottom_c) overlayed_canvas.overlay(top_c, left, top) overlayed_canvas.children = [(left, top, top_c, None), (0, 0, bottom_c, None)] overlayed_canvas.shortcuts = {} # disable background shortcuts for shortcut in top_c.shortcuts: overlayed_canvas.shortcuts[shortcut] = "fg" return overlayed_canvas def CanvasJoin(canvas_info: Iterable[tuple[Canvas, typing.Any, bool, int]]) -> CompositeCanvas: """ Join canvases in l horizontally. Return result. :param canvas_info: list of (canvas, position, focus, cols) tuples: position value that widget.set_focus will accept or None if not allowed focus True if this canvas is the one that would be in focus if the whole widget is in focus cols is the number of screen columns that this widget will require, if larger than the actual canvas.cols() value then this widget will be padded on the right. """ l2 = [] focus_item = 0 maxrow = 0 for n, (canv, pos, focus, cols) in enumerate(canvas_info): rows = canv.rows() pad_right = cols - canv.cols() if focus: focus_item = n maxrow = max(maxrow, rows) l2.append((canv, pos, pad_right, rows)) shard_lists = [] children = [] joined_canvas = CompositeCanvas() col = 0 for canv, pos, pad_right, rows in l2: composite_canvas = CompositeCanvas(canv) if pad_right: composite_canvas.pad_trim_left_right(0, pad_right) if rows < maxrow: composite_canvas.pad_trim_top_bottom(0, maxrow - rows) joined_canvas.coords.update(composite_canvas.translate_coords(col, 0)) for shortcut in composite_canvas.shortcuts: joined_canvas.shortcuts[shortcut] = pos shard_lists.append(composite_canvas.shards) children.append((col, 0, composite_canvas, pos)) col += composite_canvas.cols() if focus_item: children = [children[focus_item], *children[:focus_item], *children[focus_item + 1 :]] joined_canvas.shards = shards_join(shard_lists) joined_canvas.children = children # type: ignore[assignment] # `CompositeCanvas` is subtype of `Canvas` return joined_canvas # C0 and C1 control characters and DEL, other than TAB and LF, which the layout places, # and SO and SI, which select the DEC line-drawing character set _CONTROL_CHARS = "\x00-\x08\x0b-\x0d\x10-\x1f\x7f-\x9f" _CONTROL_RE = re.compile(f"[{_CONTROL_CHARS}]") _CONTROL_UTF8_RE = re.compile(rb"[\x00-\x08\x0b-\x0d\x10-\x1f\x7f]|\xc2[\x80-\x9f]") _DRAWN_RUN_RE = re.compile(f"[^{_CONTROL_CHARS}]+") def _drawn_ranges(text: str | bytes, start: int, end: int, sc: int) -> list[tuple[int, int]] | None: """Return the ranges of text[start:end] to draw so that they take the *sc* screen columns the layout measured. The layout measures a segment either with :func:`calc_width`, which takes every escape sequence and control character as zero columns, or with :func:`calc_text_pos`, which takes the control characters as zero columns and the rest of an escape sequence, such as ``[31m`` after ESC, as printable text. Leaving out what the measure that gives *sc* takes as zero columns draws exactly *sc* columns, and nothing reaches the display for it to draw as ``?`` or to run as a terminal command. :returns: ``None`` to draw the segment unchanged: it has no control character, it is a byte string in an encoding that measures every byte as one column, or neither measure gives *sc*. """ if isinstance(text, bytes): if get_byte_encoding() != "utf8" or not _CONTROL_UTF8_RE.search(text, start, end): return None try: decoded = text[start:end].decode("utf-8") except UnicodeDecodeError: return None offsets: Sequence[int] = list( itertools.accumulate((len(char.encode("utf-8")) for char in decoded), initial=start) ) else: if not _CONTROL_RE.search(text, start, end): return None decoded = text[start:end] offsets = range(start, end + 1) whole: list[tuple[int, int]] = [] pos = 0 for part, is_sequence in wcwidth.iter_sequences(decoded): # calc_width keeps the text a sequence displays, such as that of OSC 66 text sizing kept = wcwidth.strip_sequences(part) if is_sequence else part at = pos + part.rfind(kept) whole.extend((at + found.start(), at + found.end()) for found in _DRAWN_RUN_RE.finditer(kept)) pos += len(part) controls_only = [found.span() for found in _DRAWN_RUN_RE.finditer(decoded)] for ranges in (whole, controls_only): drawn = "".join(decoded[s:e] for s, e in ranges) if calc_width(drawn, 0, len(drawn)) == sc: return [(offsets[s], offsets[e]) for s, e in ranges] return None @dataclasses.dataclass class _AttrWalk: counter: int = 0 # counter for moving through elements of a offset: int = 0 # current offset into text of attr[ak] def apply_text_layout( text: str | bytes, attr: list[tuple[Hashable, int]], ls: list[list[tuple[int, int, int | bytes] | tuple[int, int | None]]], maxcol: int, ) -> TextCanvas: """Build a :class:`TextCanvas` by encoding *text* and *attr* according to the line layout *ls*. Control characters and escape sequences that the layout measured as zero columns are left out of the canvas, so that each line is drawn in the screen columns the layout measured. """ t: list[bytes] = [] a: list[list[tuple[Hashable, int]]] = [] c: list[list[tuple[Literal["0", "U"] | None, int]]] = [] aw = _AttrWalk() def arange(start_offs: int, end_offs: int) -> list[tuple[Hashable, int]]: """Return an attribute list for the range of text specified.""" if start_offs < aw.offset: aw.counter = 0 aw.offset = 0 o: list[tuple[Hashable, int]] = [] # the loop should run at least once, the '=' part ensures that while aw.offset <= end_offs: if len(attr) <= aw.counter: # run out of attributes o.append((None, end_offs - max(start_offs, aw.offset))) break at, run = attr[aw.counter] if aw.offset + run <= start_offs: # move forward through attr to find start_offs aw.counter += 1 aw.offset += run continue if end_offs <= aw.offset + run: o.append((at, end_offs - max(start_offs, aw.offset))) break o.append((at, aw.offset + run - max(start_offs, aw.offset))) aw.counter += 1 aw.offset += run return o # one search of the whole text spares the search of each segment for text without control characters has_control = bool(_CONTROL_UTF8_RE.search(text) if isinstance(text, bytes) else _CONTROL_RE.search(text)) for line_layout in ls: # trim the line to fit within maxcol line_layout = trim_line(line_layout, text, 0, maxcol) # noqa: PLW2901 line = [] linea: list[tuple[Hashable, int]] = [] linec: list[tuple[Literal["0", "U"] | None, int]] = [] def attrrange( start_offs: int, end_offs: int, destw: int, *, src: str | bytes = text, runs: list[tuple[Hashable, int]] | None = None, ) -> None: """Add attributes based on attributes between start_offs and end_offs. :param src: the text *start_offs* and *end_offs* index, which encodes to *destw* bytes :param runs: the attributes of src[start_offs:end_offs], ``None`` to look them up in *attr* """ # pylint: disable=cell-var-from-loop if start_offs == end_offs: [(at, run)] = arange(start_offs, end_offs) # pylint: disable=unbalanced-tuple-unpacking rle_append_modify(linea, (at, destw)) # noqa: B023 return if runs is None: runs = arange(start_offs, end_offs) if destw == end_offs - start_offs: for at, run in runs: rle_append_modify(linea, (at, run)) # noqa: B023 return # encoded version has different width o = start_offs for at, run in runs: if o + run == end_offs: rle_append_modify(linea, (at, destw)) # noqa: B023 return tseg = src[o : o + run] tseg, cs = apply_target_encoding(tseg) segw = rle_len(cs) rle_append_modify(linea, (at, segw)) # noqa: B023 o += run destw -= segw for seg in line_layout: # if seg is None: assert 0, ls s = LayoutSegment(seg) if s.end: offs = typing.cast("int", s.offs) # s.end is set if not has_control or (drawn := _drawn_ranges(text, offs, s.end, s.sc)) is None: tseg, cs = apply_target_encoding(text[offs : s.end]) attrrange(offs, s.end, rle_len(cs)) else: seg_text = typing.cast("str", text[:0]).join( typing.cast("str", text[start:end]) for start, end in drawn ) tseg, cs = apply_target_encoding(seg_text) runs: list[tuple[Hashable, int]] = [] for start, end in drawn: for run in arange(start, end): rle_append_modify(runs, run) attrrange(0, len(seg_text), rle_len(cs), src=seg_text, runs=runs) line.append(tseg) rle_join_modify(linec, cs) # type: ignore[arg-type] elif s.text: tseg, cs = apply_target_encoding(s.text) line.append(tseg) attrrange(typing.cast("int", s.offs), typing.cast("int", s.offs), len(tseg)) # s.text is set rle_join_modify(linec, cs) # type: ignore[arg-type] elif s.offs: if s.sc: line.append(b"".rjust(s.sc)) attrrange(s.offs, s.offs, s.sc) else: line.append(b"".rjust(s.sc)) linea.append((None, s.sc)) linec.append((None, s.sc)) t.append(b"".join(line)) a.append(linea) c.append(linec) return TextCanvas(t, a, c, maxcol=maxcol) urwid-urwid-69907e4/urwid/command_map.py000066400000000000000000000127621526073734300202660ustar00rootroot00000000000000# Urwid CommandMap class # Copyright (C) 2004-2011 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """Mapping of keystrokes to the abstract commands that widgets act on.""" from __future__ import annotations import enum import typing from collections.abc import MutableMapping if typing.TYPE_CHECKING: from collections.abc import Iterator from typing_extensions import Self class Command(str, enum.Enum): """Abstract commands that a :class:`CommandMap` maps keystrokes to.""" REDRAW_SCREEN = "redraw screen" UP = "cursor up" DOWN = "cursor down" LEFT = "cursor left" RIGHT = "cursor right" PAGE_UP = "cursor page up" PAGE_DOWN = "cursor page down" MAX_LEFT = "cursor max left" MAX_RIGHT = "cursor max right" ACTIVATE = "activate" MENU = "menu" SELECT_NEXT = "next selectable" SELECT_PREVIOUS = "prev selectable" REDRAW_SCREEN: typing.Literal[Command.REDRAW_SCREEN] = Command.REDRAW_SCREEN CURSOR_UP: typing.Literal[Command.UP] = Command.UP CURSOR_DOWN: typing.Literal[Command.DOWN] = Command.DOWN CURSOR_LEFT: typing.Literal[Command.LEFT] = Command.LEFT CURSOR_RIGHT: typing.Literal[Command.RIGHT] = Command.RIGHT CURSOR_PAGE_UP: typing.Literal[Command.PAGE_UP] = Command.PAGE_UP CURSOR_PAGE_DOWN: typing.Literal[Command.PAGE_DOWN] = Command.PAGE_DOWN CURSOR_MAX_LEFT: typing.Literal[Command.MAX_LEFT] = Command.MAX_LEFT CURSOR_MAX_RIGHT: typing.Literal[Command.MAX_RIGHT] = Command.MAX_RIGHT ACTIVATE: typing.Literal[Command.ACTIVATE] = Command.ACTIVATE class CommandMap(MutableMapping[str, typing.Union[str, Command, None]]): """ dict-like object for looking up commands from keystrokes. Default values: :kbd:`tab`, :kbd:`ctrl n` ``'next selectable'`` :kbd:`shift tab`, :kbd:`ctrl p` ``'prev selectable'`` :kbd:`ctrl l` ``'redraw screen'`` :kbd:`esc` ``'menu'`` :kbd:`up`, :kbd:`down`, :kbd:`left`, :kbd:`right` ``'cursor up'``, ``'cursor down'``, ``'cursor left'``, ``'cursor right'`` :kbd:`page up`, :kbd:`page down` ``'cursor page up'``, ``'cursor page down'`` :kbd:`home`, :kbd:`end` ``'cursor max left'``, ``'cursor max right'`` :kbd:`space`, :kbd:`enter` ``'activate'`` """ def __iter__(self) -> Iterator[str]: """Iterate over the keystrokes currently mapped to a command.""" return iter(self._command) def __len__(self) -> int: """Return the number of keystrokes currently mapped to a command.""" return len(self._command) _command_defaults: typing.ClassVar[dict[str, str | Command]] = { "tab": Command.SELECT_NEXT, "ctrl n": Command.SELECT_NEXT, "shift tab": Command.SELECT_PREVIOUS, "ctrl p": Command.SELECT_PREVIOUS, "ctrl l": Command.REDRAW_SCREEN, "esc": Command.MENU, "up": Command.UP, "down": Command.DOWN, "left": Command.LEFT, "right": Command.RIGHT, "page up": Command.PAGE_UP, "page down": Command.PAGE_DOWN, "home": Command.MAX_LEFT, "end": Command.MAX_RIGHT, " ": Command.ACTIVATE, "enter": Command.ACTIVATE, } def __init__(self) -> None: """Initialize the command map with the class's default key-to-command bindings.""" self._command = self._command_defaults.copy() def restore_defaults(self) -> None: """Reset the command map to the class's default key-to-command bindings.""" self._command = self._command_defaults.copy() def __getitem__(self, key: str) -> str | Command | None: """Return the command mapped to `key`, or None if unmapped.""" return self._command.get(key, None) def __setitem__(self, key: str, command: str | Command | None) -> None: """Set command in the command map. :param key: keystroke :param command: command aliad. If the alias is `None`, the command is removed from the map. """ if command is None: if key in self._command: del self._command[key] return self._command[key] = command def __delitem__(self, key: str) -> None: """Remove the command mapping for `key`.""" del self._command[key] def clear_command(self, command: str | Command) -> None: """Remove every keystroke currently mapped to ``command``.""" dk = [k for k, v in self._command.items() if v == command] for k in dk: del self._command[k] def copy(self) -> Self: """Return a new copy of this CommandMap, likely so we can modify it separate from a shared one.""" c = self.__class__() c._command = dict(self._command) # pylint: disable=protected-access return c command_map = CommandMap() # shared command mappings urwid-urwid-69907e4/urwid/display/000077500000000000000000000000001526073734300170765ustar00rootroot00000000000000urwid-urwid-69907e4/urwid/display/__init__.py000066400000000000000000000051741526073734300212160ustar00rootroot00000000000000"""Package with Display implementations for urwid.""" from __future__ import annotations import importlib.util import sys import typing from . import raw from .common import ( BLACK, BROWN, DARK_BLUE, DARK_CYAN, DARK_GRAY, DARK_GREEN, DARK_MAGENTA, DARK_RED, DEFAULT, LIGHT_BLUE, LIGHT_CYAN, LIGHT_GRAY, LIGHT_GREEN, LIGHT_MAGENTA, LIGHT_RED, UPDATE_PALETTE_ENTRY, WHITE, YELLOW, AttrSpec, AttrSpecError, BaseScreen, RealTerminal, ScreenError, ) if typing.TYPE_CHECKING: import types __all__: tuple[str, ...] = ( "BLACK", "BROWN", "DARK_BLUE", "DARK_CYAN", "DARK_GRAY", "DARK_GREEN", "DARK_MAGENTA", "DARK_RED", "DEFAULT", "LIGHT_BLUE", "LIGHT_CYAN", "LIGHT_GRAY", "LIGHT_GREEN", "LIGHT_MAGENTA", "LIGHT_RED", "UPDATE_PALETTE_ENTRY", "WHITE", "YELLOW", "AttrSpec", "AttrSpecError", "BaseScreen", "RealTerminal", "ScreenError", # Lazy imported "curses", "html_fragment", "lcd", "raw", "web", ) def lazy_import(name: str, package: str | None = None) -> types.ModuleType: """Lazy import implementation from Python documentation. Useful for cases where no warnings expected for moved modules. :raises ImportError: *name* does not resolve to a loadable module. """ spec = importlib.util.find_spec(name, package) if not spec: raise ImportError(f"No module named {name!r}") if not spec.loader: raise ImportError(f"Module named {name!r} is invalid") loader = importlib.util.LazyLoader(spec.loader) spec.loader = loader module = importlib.util.module_from_spec(spec) if not package: sys.modules[name] = module else: sys.modules[f"{package.rstrip('.')}.{name.lstrip('.')}"] = module loader.exec_module(module) return module html_fragment = lazy_import(".html_fragment", "urwid.display") lcd = lazy_import(".lcd", "urwid.display") web = lazy_import(".web", "urwid.display") def __getattr__(name: str) -> types.ModuleType: """Lazily import `curses`, the optional stdlib-backed display submodule. "curses" module may be present without "_curses" private part, so lazy_import will pass and real usage fail. :raises AttributeError: *name* is not ``"curses"``, or the stdlib `curses` module is unavailable. """ if name != "curses": raise AttributeError(name) try: module = importlib.import_module(".curses", __name__) except ImportError as exc: raise AttributeError(name) from exc globals()["curses"] = module return module urwid-urwid-69907e4/urwid/display/_posix_raw_display.py000066400000000000000000000525421526073734300233570ustar00rootroot00000000000000# Urwid raw display module # Copyright (C) 2004-2009 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """Direct terminal UI implementation.""" from __future__ import annotations import contextlib import fcntl import functools import os import signal import struct import subprocess import sys import termios import tty import typing import warnings from subprocess import PIPE, Popen from urwid import signals from . import _raw_display_base, escape from .common import INPUT_DESCRIPTORS_CHANGED if typing.TYPE_CHECKING: from collections.abc import Callable from types import FrameType from urwid.event_loop import EventLoop SignalHandler = typing.Union[Callable[[int, typing.Union[FrameType, None]], typing.Any], int, None] _MouseInput = tuple[str, int, int, int] _CursorPosition = tuple[typing.Literal["cursor position"], int, int] _DecodedInput = list[typing.Union[str, _MouseInput, _CursorPosition]] # GPM event-type bits, as reported by the `mev` helper on its `Ax` field. # See GPM's public gpm.h, Gpm_Event.type. _GPM_MOVE = 1 _GPM_DRAG = 2 _GPM_DOWN = 4 _GPM_UP = 8 _GPM_SINGLE = 16 _GPM_DOUBLE = 32 _GPM_TRIPLE = 64 _GPM_MFLAG = 128 _GPM_HARD = 256 # Event-type combinations actually emitted by `mev -e 158`, decoded against the bitmask above. _GPM_EV_DOWN_SINGLE = _GPM_DOWN | _GPM_SINGLE # 20: first click of a press _GPM_EV_DOWN_DOUBLE = _GPM_DOWN | _GPM_DOUBLE # 36: second click of a press _GPM_EV_DOWN_TRIPLE = _GPM_DOWN | _GPM_SINGLE | _GPM_DOUBLE # 52: third click of a press _GPM_EV_DRAG = _GPM_DRAG | _GPM_SINGLE | _GPM_MFLAG # 146 _GPM_EV_UP_DOUBLE = _GPM_UP | _GPM_DOUBLE # 40: release ending a double click # GPM Gpm_Event.buttons bits, from gpm.h. _GPM_B_LEFT = 4 _GPM_B_MIDDLE = 2 _GPM_B_RIGHT = 1 # GPM Gpm_Event.modifiers bits. GPM documents this field as the Linux console "get shift state" # byte (TIOCLINUX subcode 6): bit 0 is shift, bit 1 is AltGr, bit 2 is control, bit 3 is (left) alt. _GPM_MOD_SHIFT = 1 _GPM_MOD_ALTGR = 2 _GPM_MOD_CTRL = 4 _GPM_MOD_ALT = 8 # most bytes of terminal input read per wake-up _INPUT_READ_LIMIT = 65536 # seconds the gpm helper is given to exit on SIGINT before it is killed _GPM_STOP_TIMEOUT = 1.0 class Screen(_raw_display_base.Screen): """Raw screen backend that drives a real POSIX terminal.""" def __init__( self, input: _raw_display_base.SupportsFileno = sys.stdin, # noqa: A002 # pylint: disable=redefined-builtin output: _raw_display_base.TextWriter = sys.stdout, bracketed_paste_mode: bool | None = None, focus_reporting: bool | None = None, ) -> None: """Initialize a screen that directly prints escape codes to an output terminal. :param bracketed_paste_mode: enable bracketed paste (`begin`/`end paste` keystrokes). None (default) auto-detects via DECRQM and enables it once confirmed supported; pass True/False to force it without probing. :param focus_reporting: enable focus reporting (`focus in`/`focus out` keystrokes). None (default) auto-detects via DECRQM and enables it once confirmed supported; pass True/False to force it without probing. .. note:: on terminal-generated signals: putting the terminal into cbreak mode (see `start()`) does not clear the tty's ``ISIG`` flag, so the line discipline still turns Ctrl+C and Ctrl+Z into ``SIGINT`` and ``SIGTSTP`` and delivers them to this process (urwid/urwid#1268). ``SIGINT`` is left for the application or its event loop to handle as it sees fit. ``SIGTSTP`` is handled by this class: `signal_init()` installs a handler that wakes the input loop, which restores the terminal before actually suspending the process, then puts the terminal back into cbreak/alternate-buffer mode and forces a redraw on resume, so a stray Ctrl+Z does not leave a stale, unresponsive frame painted on screen. The suspend therefore waits until the application next reads input, through `get_input()` or the callback `hook_event_loop()` registers. Applications that install their own ``SIGTSTP`` handler on top of this one should chain to the previous handler (as this class does) rather than replacing it outright, and multithreaded applications must call `signal_init()` and `signal_restore()` from the main thread, since only the main thread can receive process signals. """ super().__init__(input, output, bracketed_paste_mode=bracketed_paste_mode, focus_reporting=focus_reporting) self.gpm_mev: Popen[str] | None = None self.gpm_event_pending: bool = False self._old_termios_settings: list[typing.Any] | None = None # These store the previous signal handlers after setting ours self._prev_sigtstp_handler: SignalHandler = None self._prev_sigwinch_handler: SignalHandler = None # Set by the SIGTSTP handler, carried out by get_available_raw_input(). self._suspend_requested = False def __repr__(self) -> str: return ( f"<{self.__class__.__name__}(" f"input={self._term_input_file}, " f"output={self._term_output_file}, " f"bracketed_paste_mode={self.modes.bracketed_paste}, " f"focus_reporting={self.modes.focus_reporting})>" ) def _sigwinch_handler(self, signum: int = signal.SIGWINCH, frame: FrameType | None = None) -> None: """:param frame: will always be None when the GLib event loop is being used.""" super()._sigwinch_handler(signum, frame) if callable(self._prev_sigwinch_handler): self._prev_sigwinch_handler(signum, frame) def _sigtstp_handler(self, signum: int, frame: FrameType | None = None) -> None: """Request a suspend, carried out by :meth:`get_available_raw_input` in the thread reading input. Restoring the terminal writes to it, changes the termios settings and re-hooks the event loop. None of that is safe at the arbitrary point of the program a signal interrupts, so the handler only sets a flag and wakes the input loop. """ self._suspend_requested = True self._wake_input_loop() def _sigcont_handler(self, signum: int, frame: FrameType | None = None) -> None: """Restore the signal handlers and restart the screen after a suspend. .. deprecated:: 4.2.5 No longer installed: :meth:`get_available_raw_input` restarts the screen after a suspend. It does not chain to a previous ``SIGCONT`` handler, since none is recorded any more. This API will be removed in version 5.0. :param frame: will always be None when the GLib event loop is being used. """ warnings.warn( "_sigcont_handler is no longer installed, the screen is restarted after a suspend by " "get_available_raw_input. API will be removed in version 5.0.", DeprecationWarning, stacklevel=2, ) self.signal_restore() self.start() self._sigwinch_handler(signal.SIGWINCH, None) def signal_init(self) -> None: """Set the SIGWINCH and SIGTSTP signal handlers, called in the startup of run wrapper. Override this function to call from main thread in threaded applications. """ self._prev_sigwinch_handler = self.signal_handler_setter(signal.SIGWINCH, self._sigwinch_handler) self._prev_sigtstp_handler = self.signal_handler_setter(signal.SIGTSTP, self._sigtstp_handler) def signal_restore(self) -> None: """Restore the SIGTSTP and SIGWINCH signal handlers, called in the finally block of run wrapper. Override this function to call from main thread in threaded applications. """ self.signal_handler_setter(signal.SIGTSTP, self._prev_sigtstp_handler or signal.SIG_DFL) self.signal_handler_setter(signal.SIGWINCH, self._prev_sigwinch_handler or signal.SIG_DFL) def _mouse_tracking(self, enable: bool) -> None: super()._mouse_tracking(enable) if enable: self._start_gpm_tracking() else: self._stop_gpm_tracking() def _start_gpm_tracking(self) -> None: """ Start the gpm helper that reports mouse events on the Linux console. :raises RuntimeError: the gpm helper process provides no standard output. """ if not os.path.isfile("/usr/bin/mev"): return if not os.environ.get("TERM", "").lower().startswith("linux"): return m = Popen( ["/usr/bin/mev", "-e", "158"], stdin=PIPE, stdout=PIPE, close_fds=True, encoding="ascii", ) if m.stdout is None: with m: # closes the pipes and reaps the process on the way out m.kill() raise RuntimeError("gpm mouse tracking stdout was not created") os.set_blocking(m.stdout.fileno(), False) self.gpm_mev = m signals.emit_signal(self, INPUT_DESCRIPTORS_CHANGED) def _stop_gpm_tracking(self) -> None: """Stop the gpm helper, close its pipes and stop watching its output.""" gpm_mev, self.gpm_mev = self.gpm_mev, None if gpm_mev is None: return try: with gpm_mev: # closes both pipes and reaps the process on the way out gpm_mev.send_signal(signal.SIGINT) try: gpm_mev.wait(_GPM_STOP_TIMEOUT) except subprocess.TimeoutExpired: gpm_mev.kill() finally: signals.emit_signal(self, INPUT_DESCRIPTORS_CHANGED) def _start( # pylint: disable=keyword-arg-before-vararg self, alternate_buffer: bool = True, *args: typing.Any, **kwargs: typing.Any, ) -> None: """ Initialize the screen and input mode. :param alternate_buffer: use an alternate screen buffer :raises TypeError: unexpected positional or keyword arguments were given. """ if args or kwargs: raise TypeError(f"start() got unexpected arguments: {args=!r}, {kwargs=!r}") if alternate_buffer: self.write(escape.PrivateMode.ALTERNATE_SCREEN_BUFFER.enable_seq) self._rows_used = None else: self._rows_used = 0 # Set as soon as the buffer is actually switched, not after: _stop() (e.g. from cleanup # after a later exception in this method) has to know to restore the normal buffer. self._alternate_buffer = alternate_buffer fd = self._input_fileno() if fd is not None and os.isatty(fd): self._old_termios_settings = termios.tcgetattr(fd) tty.setcbreak(fd) self._detect_terminal_modes() if self.modes.bracketed_paste: self.write(escape.PrivateMode.BRACKETED_PASTE.enable_seq) if self.modes.focus_reporting: self.write(escape.PrivateMode.FOCUS_REPORTING.enable_seq) self.signal_init() self._next_timeout = self.max_wait if not self._signal_keys_set: self._old_signal_keys = self.tty_signal_keys(fileno=fd) signals.emit_signal(self, INPUT_DESCRIPTORS_CHANGED) # restore mouse tracking to previous state self._mouse_tracking(self._mouse_tracking_enabled) super()._start(*args, **kwargs) # type: ignore[safe-super] def _stop(self) -> None: """Restore the screen.""" self.clear() if self.modes.bracketed_paste: self.write(escape.PrivateMode.BRACKETED_PASTE.disable_seq) if self.modes.focus_reporting: self.write(escape.PrivateMode.FOCUS_REPORTING.disable_seq) signals.emit_signal(self, INPUT_DESCRIPTORS_CHANGED) self.signal_restore() self._stop_mouse_restore_buffer() self._stop_restore_palette() fd = self._input_fileno() if fd is not None and os.isatty(fd) and self._old_termios_settings is not None: termios.tcsetattr(fd, termios.TCSAFLUSH, self._old_termios_settings) if self._old_signal_keys: self.tty_signal_keys(*self._old_signal_keys, fd) # A suspend requested while the screen ran must not stop the process after a later restart. self._suspend_requested = False super()._stop() # type: ignore[safe-super] def get_input_descriptors(self) -> list[_raw_display_base.SupportsFileno | int]: """Return a list of integer file descriptors that should be polled in external event loops. Used to check for user input. Use this method if you are implementing your own event loop. This method is only called by `hook_event_loop`, so if you override that, you can safely ignore this. """ if not self._started: return [] fd_list = super().get_input_descriptors() if self.gpm_mev is not None and self.gpm_mev.stdout is not None: fd_list.append(self.gpm_mev.stdout) return fd_list def unhook_event_loop(self, event_loop: EventLoop) -> None: """Remove any hooks added by hook_event_loop.""" for handle in self._current_event_loop_handles: event_loop.remove_watch_file(handle) if self._input_timeout: event_loop.remove_alarm(self._input_timeout) self._input_timeout = None def hook_event_loop( self, event_loop: EventLoop, callback: Callable[[_DecodedInput, list[int]], typing.Any], ) -> None: """Register the given callback with the event loop, to be called with new input whenever it's available. The callback should be passed a list of processed keys and a list of unprocessed keycodes. Subclasses may wish to use parse_input to wrap the callback. """ if hasattr(self, "get_input_nonblocking"): wrapper = self._make_legacy_input_wrapper(event_loop, callback) else: @functools.wraps(callback) def wrapper() -> tuple[_DecodedInput, list[int]] | None: self.logger.debug('Calling callback for "watch file"') return self.parse_input(event_loop, callback, self.get_available_raw_input()) fds = self.get_input_descriptors() handles = [event_loop.watch_file(fd if isinstance(fd, int) else fd.fileno(), wrapper) for fd in fds] self._current_event_loop_handles = handles def get_available_raw_input(self) -> list[int]: """Return any currently available input, then carry out a suspend requested by ``SIGTSTP``. The screen is stopped before the process suspends itself, and restarted and fully redrawn on resume. The flag is checked after the wake-up socket is drained, so a later request keeps its wake-up pending. """ codes = super().get_available_raw_input() if self._suspend_requested and self._started: self._suspend_requested = False self.stop() # restores the previous SIGTSTP disposition, which the kill below runs os.kill(os.getpid(), signal.SIGTSTP) self.start() self._resized = True return codes def _get_input_codes(self) -> list[int]: return super()._get_input_codes() + self._get_gpm_codes() def _get_gpm_codes(self) -> list[int]: codes: list[int] = [] gpm_mev = self.gpm_mev try: while gpm_mev is not None and self.gpm_event_pending: if gpm_mev.stdout is None: return codes codes.extend(self._encode_gpm_event()) except OSError as e: if e.args[0] != 11: raise return codes def _read_raw_input(self, timeout: float) -> bytearray: """ Read at most `_INPUT_READ_LIMIT` bytes of the raw input available, waiting at most *timeout* seconds. :raises RuntimeError: the input file has been closed. """ ready = self._wait_for_input_ready(timeout) gpm_stdout = self.gpm_mev.stdout if self.gpm_mev is not None else None if gpm_stdout is not None and gpm_stdout.fileno() in ready: self.gpm_event_pending = True fd = self._input_fileno() chars = bytearray() if fd is None or fd not in ready: return chars # `fd` was just reported ready, so this read does not block. Input beyond the limit is read on the next pass. chunk = os.read(fd, _INPUT_READ_LIMIT) if not chunk: raise RuntimeError("stdin has been closed") chars.extend(chunk) return chars def _encode_gpm_event(self) -> list[int]: self.gpm_event_pending = False if self.gpm_mev is None or self.gpm_mev.stdout is None: return [] s = self.gpm_mev.stdout.readline() event_result = s.split(",") if len(event_result) != 6: # unexpected output, stop tracking self._stop_gpm_tracking() return [] ev_, x_, y_, _ign, b_, m_ = event_result ev = int(ev_.rsplit("x", 1)[-1], 16) x = int(x_.rsplit(" ", 1)[-1]) y = int(y_.lstrip().split(" ", 1)[0]) b = int(b_.rsplit(" ", 1)[-1]) m = int(m_.rsplit("x", 1)[-1].rstrip(), 16) # convert to xterm-like escape sequence last_state = next_state = self.last_bstate result: list[int] = [] mod = 0 if m & _GPM_MOD_SHIFT: mod |= 4 # shift if m & (_GPM_MOD_ALTGR | _GPM_MOD_ALT): mod |= 8 # alt if m & _GPM_MOD_CTRL: mod |= 16 # ctrl def append_button(b: int) -> None: b |= mod result.extend([27, ord("["), ord("M"), b + 32, x + 32, y + 32]) if ev in {_GPM_EV_DOWN_SINGLE, _GPM_EV_DOWN_DOUBLE, _GPM_EV_DOWN_TRIPLE}: # press if b & _GPM_B_LEFT and last_state & 1 == 0: append_button(0) next_state |= 1 if b & _GPM_B_MIDDLE and last_state & 2 == 0: append_button(1) next_state |= 2 if b & _GPM_B_RIGHT and last_state & 4 == 0: append_button(2) next_state |= 4 elif ev == _GPM_EV_DRAG: # drag if b & _GPM_B_LEFT: append_button(0 + escape.MOUSE_DRAG_FLAG) elif b & _GPM_B_MIDDLE: append_button(1 + escape.MOUSE_DRAG_FLAG) elif b & _GPM_B_RIGHT: append_button(2 + escape.MOUSE_DRAG_FLAG) else: # release if b & _GPM_B_LEFT and last_state & 1: append_button(0 + escape.MOUSE_RELEASE_FLAG) next_state &= ~1 if b & _GPM_B_MIDDLE and last_state & 2: append_button(1 + escape.MOUSE_RELEASE_FLAG) next_state &= ~2 if b & _GPM_B_RIGHT and last_state & 4: append_button(2 + escape.MOUSE_RELEASE_FLAG) next_state &= ~4 if ev == _GPM_EV_UP_DOUBLE: # double click (release) if b & _GPM_B_LEFT and last_state & 1: append_button(0 + escape.MOUSE_MULTIPLE_CLICK_FLAG) if b & _GPM_B_MIDDLE and last_state & 2: append_button(1 + escape.MOUSE_MULTIPLE_CLICK_FLAG) if b & _GPM_B_RIGHT and last_state & 4: append_button(2 + escape.MOUSE_MULTIPLE_CLICK_FLAG) elif ev == _GPM_EV_DOWN_TRIPLE: # triple click (press) if b & _GPM_B_LEFT and last_state & 1: append_button(0 + escape.MOUSE_MULTIPLE_CLICK_FLAG * 2) if b & _GPM_B_MIDDLE and last_state & 2: append_button(1 + escape.MOUSE_MULTIPLE_CLICK_FLAG * 2) if b & _GPM_B_RIGHT and last_state & 4: append_button(2 + escape.MOUSE_MULTIPLE_CLICK_FLAG * 2) self.last_bstate = next_state return result def get_cols_rows(self) -> tuple[int, int]: """Return the terminal dimensions (num columns, num rows).""" y, x = super().get_cols_rows() with contextlib.suppress(OSError): # Term size could not be determined if isinstance(self._term_output_file, _raw_display_base.SupportsFileno): buf = fcntl.ioctl(self._term_output_file.fileno(), termios.TIOCGWINSZ, b" " * 8) y, x, _, _ = struct.unpack("hhhh", buf) # Provide some lightweight fallbacks in case the TIOCWINSZ doesn't # give sane answers if (x <= 0 or y <= 0) and self.term in {"ansi", "vt100"}: y, x = 24, 80 self.maxrow = y return x, y def _test() -> None: import doctest doctest.testmod() if __name__ == "__main__": _test() urwid-urwid-69907e4/urwid/display/_raw_display_base.py000066400000000000000000001436471526073734300231360ustar00rootroot00000000000000# Urwid raw display module # Copyright (C) 2004-2009 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """Direct terminal UI implementation.""" from __future__ import annotations import abc import contextlib import dataclasses import errno import functools import os import platform import selectors import signal import socket import sys import typing from urwid import signals, str_util, util from . import escape from .common import ( UNPRINTABLE_C1_TRANS_TABLE, UNPRINTABLE_TRANS_TABLE, UPDATE_PALETTE_ENTRY, AttrSpec, BaseScreen, RealTerminal, ) if typing.TYPE_CHECKING: from collections.abc import Callable, Hashable, Iterable, Mapping, Sequence from types import FrameType from typing_extensions import Literal from urwid import Canvas, EventLoop _MouseInput = tuple[str, int, int, int] _CursorPosition = tuple[typing.Literal["cursor position"], int, int] _PrivateModeReport = tuple[typing.Literal["private mode report"], str, int] _DecodedInput = list[typing.Union[str, _MouseInput, _CursorPosition]] _DecodedInputWithReports = list[typing.Union[str, _MouseInput, _CursorPosition, _PrivateModeReport]] IS_WINDOWS = sys.platform == "win32" IS_WSL = (sys.platform == "linux") and ("wsl" in platform.platform().lower()) # signal has no SIGWINCH on Windows; _win32_raw_display passes its own signum explicitly, so this # default is only ever actually used on POSIX. _SIGWINCH = getattr(signal, "SIGWINCH", 28) _ColorCount = typing.Literal[1, 16, 88, 256, 16777216] # Terminals that implement SGR 90-97 / 100-107 bright colors without needing bold/blink. _INDEPENDENT_BRIGHT_FAMILIES = frozenset( { "alacritty", "cygwin", "eterm", "foot", "ghostty", "gnome", "iterm", "iterm2", "kitty", "konsole", "mintty", "mlterm", "putty", "rxvt", "st", "terminator", "tmux", "vte", "wezterm", "xfce", "xterm", } ) _TRUECOLOR_FAMILIES = frozenset( { "alacritty", "contour", "foot", "ghostty", "iterm", "iterm2", "kitty", "wezterm", } ) _NO_UNDERLINE_FAMILIES = frozenset({"dumb", "unknown", "vt52"}) # Windows 10 TH2 (1511) gained 256-color VT; 1703-era build 14931 gained 24-bit color. _WINDOWS_256COLOR_BUILD = 10586 _WINDOWS_TRUECOLOR_BUILD = 14931 _TRUECOLOR_TERM_PROGRAMS = frozenset({"hyper", "tabby", "vscode", "vscode-insiders"}) # ANSI SGR base codes (ECMA-48 / ISO 6429): 30-37 selects a standard foreground color, 90-97 a # bright one without needing bold; 40-47 selects a standard background color, 100-107 a bright # one. AttrSpec numbers its 16 basic colors 0-15, with 8-15 being "the bright half", so # _SGR_BRIGHT_OFFSET is subtracted from the AttrSpec number before adding the bright SGR base. _SGR_FG_BASE = 30 _SGR_FG_BRIGHT_BASE = 90 _SGR_BG_BASE = 40 _SGR_BG_BRIGHT_BASE = 100 _SGR_BRIGHT_OFFSET = 8 # Index into the (basic, mono, high_88, high_256, high_true) tuple that # BaseScreen.register_palette_entry() emits via UPDATE_PALETTE_ENTRY, keyed by this screen's # current color count. _ATTRSPEC_INDEX_BY_COLORS: Mapping[_ColorCount, int] = {16: 0, 1: 1, 88: 2, 256: 3, 16777216: 4} @dataclasses.dataclass(frozen=True) class TerminalProperties: """Capabilities inferred from TERM and standard color environment variables.""" colors: _ColorCount has_underline: bool fg_bright_is_bold: bool bg_bright_is_blink: bool back_color_erase: bool @dataclasses.dataclass class TermModes: """Terminal modes negotiated with the real host terminal (counterpart to `vterm.TermModes`). None: sentinel, resolved by a DECRPM reply (see escape.PrivateMode) or an explicit Screen.__init__ arg. """ alternate_buffer: bool | None = None mouse_reporting: bool | None = None mouse_sgr_mode: bool | None = None focus_reporting: bool | None = None bracketed_paste: bool | None = None synchronized_output: bool | None = None grapheme_clustering: bool | None = None # escape.PrivateMode member -> the field it resolves. _FIELDS: typing.ClassVar[Mapping[str, str]] = { escape.PrivateMode.ALTERNATE_SCREEN_BUFFER: "alternate_buffer", escape.PrivateMode.MOUSE_REPORTING: "mouse_reporting", escape.PrivateMode.MOUSE_SGR_MODE: "mouse_sgr_mode", escape.PrivateMode.FOCUS_REPORTING: "focus_reporting", escape.PrivateMode.BRACKETED_PASTE: "bracketed_paste", escape.PrivateMode.SYNCHRONIZED_OUTPUT: "synchronized_output", escape.PrivateMode.GRAPHEME_CLUSTERING: "grapheme_clustering", } def set_from_codes(self, *response: tuple[str, int]) -> frozenset[str]: """Apply DECRPM (mode, value) pairs; return the field names newly resolved to supported.""" newly_supported = set() for mode, value in response: field = self._FIELDS.get(mode) if field is None: continue was_unresolved = getattr(self, field) is None supported = value in escape.DECRQM_RECOGNIZED_VALUES setattr(self, field, supported) if was_unresolved and supported: newly_supported.add(field) return frozenset(newly_supported) def _term_families(term: str) -> frozenset[str]: """Name tokens from a TERM value. ``screen.xterm-256color`` yields ``screen``, ``xterm`` and ``256color``. ``rxvt-unicode-256color`` yields ``rxvt``, ``unicode`` and ``256color``. ``xterm-kitty`` yields ``xterm`` and ``kitty``. """ families: set[str] = set() for part in term.lower().split("."): if part: families.update(token for token in part.split("-") if token) return frozenset(families) def _parse_force_color(value: str) -> _ColorCount: """Map FORCE_COLOR / CLICOLOR_FORCE to a color count (chalk / supports-color convention).""" normalized = value.strip().lower() if normalized in {"0", "false", "no"}: return 1 if normalized in {"", "1", "true", "yes"}: return 16 if normalized == "2": return 256 if normalized == "3": return 16777216 return 16 def _windows_version() -> tuple[int, int, int] | None: if sys.platform != "win32": return None version = sys.getwindowsversion() # pylint: disable=no-member return (version.major, version.minor, version.build) def _windows_console_colors(version: tuple[int, int, int] | None) -> _ColorCount | None: """Color depth of the Windows console VT host, if any.""" if version is None or version < (10, 0, 0): return None if version >= (10, 0, _WINDOWS_TRUECOLOR_BUILD): return 16777216 if version >= (10, 0, _WINDOWS_256COLOR_BUILD): return 256 return 16 def _truecolor_host(environ: Mapping[str, str]) -> bool: """Hosts that speak 24-bit SGR even when TERM still says xterm-256color.""" if (colorterm := environ.get("COLORTERM")) and colorterm.lower() in {"truecolor", "24bit"}: return True if environ.get("WT_SESSION") or environ.get("WT_PROFILE_ID"): return True if environ.get("ConEmuANSI", "").lower() in {"on", "1"} or environ.get("ConEmuPID"): return True return environ.get("TERM_PROGRAM", "").lower() in _TRUECOLOR_TERM_PROGRAMS def _colors_from_term(term: str, families: frozenset[str]) -> _ColorCount: term_l = term.lower() if not term_l: return 16 if families & _NO_UNDERLINE_FAMILIES or term_l in _NO_UNDERLINE_FAMILIES: return 1 if "direct" in term_l or families & _TRUECOLOR_FAMILIES: return 16777216 if "256color" in term_l: return 256 if "88color" in term_l: return 88 return 16 def detect_terminal_properties( term: str | None = None, environ: Mapping[str, str] | None = None, *, windows_version: tuple[int, int, int] | None = None, ) -> TerminalProperties: """Detect raw-display capabilities from TERM and standard color environment variables. Environment handling follows the NO_COLOR spec and common COLORTERM / FORCE_COLOR / CLICOLOR_FORCE / CLICOLOR conventions. TERM names are matched by family so ``gnome-256color``, ``konsole-direct`` and ``rxvt-unicode-256color`` are recognized. On Windows 10 and later the console VT host is used when TERM is empty or generic: 256 colors from build 10586, 24-bit color from build 14931. Windows Terminal (``WT_SESSION``), ConEmu and VS Code's terminal are treated as 24-bit hosts even when TERM still reports ``xterm-256color``. """ env = os.environ if environ is None else environ term_value = env.get("TERM", "") if term is None else term families = _term_families(term_value) term_l = term_value.lower() term_colors = _colors_from_term(term_value, families) if windows_version is None: windows_version = _windows_version() win_colors = _windows_console_colors(windows_version) vt_truecolor = _truecolor_host(env) or win_colors == 16777216 vt_host = vt_truecolor or win_colors in {256, 16777216} if env.get("NO_COLOR"): colors: _ColorCount = 1 elif force := env.get("FORCE_COLOR") or env.get("CLICOLOR_FORCE"): colors = _parse_force_color(force) elif env.get("CLICOLOR") == "0" or term_colors == 1: colors = 1 elif vt_truecolor: colors = 16777216 elif env.get("COLORTERM") and term_colors < 256: colors = 256 elif win_colors is not None and term_colors < win_colors: colors = win_colors else: colors = term_colors has_underline = not bool(families & _NO_UNDERLINE_FAMILIES) linux_console = bool(term_value) and term_value.lower().split(".", 1)[0].split("-", 1)[0] == "linux" fg_bright_is_bold = not bool(families & _INDEPENDENT_BRIGHT_FAMILIES) if vt_host and not linux_console: fg_bright_is_bold = False has_underline = True bg_bright_is_blink = linux_console back_color_erase = "bce" in term_l or not families & {"screen", "dumb", "unknown"} return TerminalProperties( colors=colors, has_underline=has_underline, fg_bright_is_bold=fg_bright_is_bold, bg_bright_is_blink=bg_bright_is_blink, back_color_erase=back_color_erase, ) @typing.runtime_checkable class SupportsFileno(typing.Protocol): """Object that can be used to obtain an OS-level file descriptor.""" def fileno(self) -> int: """Return the underlying OS-level file descriptor.""" class TextWriter(typing.Protocol): """Text output stream used by the raw display.""" def write(self, data: str, /) -> object: """Write `data` to the stream.""" def flush(self) -> object: """Flush buffered output to the stream.""" class Screen(BaseScreen, RealTerminal): """Shared base for the raw POSIX and Windows screen backends that talk directly to the terminal.""" _term_input_file: SupportsFileno _term_output_file: TextWriter def __init__( self, input: SupportsFileno, # noqa: A002 # pylint: disable=redefined-builtin output: TextWriter, bracketed_paste_mode: bool | None = None, focus_reporting: bool | None = None, ) -> None: """Initialize a screen that directly prints escape codes to an output terminal. :param bracketed_paste_mode: enable bracketed paste (`begin`/`end paste` keystrokes). None (default) auto-detects via DECRQM and enables it once confirmed supported; pass True/False to force it without probing. :param focus_reporting: enable focus reporting (`focus in`/`focus out` keystrokes). None (default) auto-detects via DECRQM and enables it once confirmed supported; pass True/False to force it without probing. """ super().__init__() self._partial_codes: list[int] = [] self.modes = TermModes(bracketed_paste=bracketed_paste_mode, focus_reporting=focus_reporting) self._pal_escape: dict[Hashable, str] = {} self._pal_attrspec: dict[Hashable, AttrSpec] = {} self._modified_palette_entries: set[int] = set() # Connected through a weak reference: a bound method would be a reference cycle back to this screen, # keeping its sockets open until the cyclic garbage collector happened to run. signals.connect_signal(self, UPDATE_PALETTE_ENTRY, type(self)._on_update_palette_entry, weak_args=(self,)) self.term = os.environ.get("TERM", "") properties = detect_terminal_properties(self.term, os.environ) self.colors = properties.colors self.has_underline = properties.has_underline self.fg_bright_is_bold = properties.fg_bright_is_bold self.bg_bright_is_blink = properties.bg_bright_is_blink self.back_color_erase = properties.back_color_erase self.prev_input_resize = 0 self.set_input_timeouts() self.screen_buf: list[list[tuple[AttrSpec | str | None, Literal["0", "U"] | None, bytes]]] | None = None self._screen_buf_canvas: Canvas | None = None self._resized = False self.maxrow: int | None = None self._alternate_buffer = False self._mouse_tracking_enabled = False self.last_bstate = 0 self._setup_G1_done = False self._rows_used: int | None = None self._cy = 0 self.register_palette_entry(None, "default", "default") self._next_timeout: float | None = None self.signal_handler_setter = signal.signal # Our connections to the world self._term_output_file = output self._term_input_file = input # pipe for signalling external event loops about resize events self._resize_pipe_rd, self._resize_pipe_wr = socket.socketpair() self._resize_pipe_rd.setblocking(False) self._resize_pipe_wr.setblocking(False) def __del__(self) -> None: self._resize_pipe_rd.close() self._resize_pipe_wr.close() def __repr__(self) -> str: return f"<{self.__class__.__name__}(input={self._term_input_file}, output={self._term_output_file})>" def _sigwinch_handler(self, signum: int = _SIGWINCH, frame: FrameType | None = None) -> None: """Record a terminal resize and wake the input loop. This runs at an arbitrary point of the program (a signal handler, or the console reader thread on Windows), so it only sets a flag and writes to the wake-up socket. :meth:`parse_input` reports the resize and drops the screen buffer in the thread that draws the screen. :param frame: will always be None when the GLib event loop is being used. """ self._resized = True self._wake_input_loop() def _wake_input_loop(self) -> None: """Make the input descriptors readable, without blocking when a wake-up is already pending.""" with contextlib.suppress(BlockingIOError): self._resize_pipe_wr.send(b"R") @property def _term_input_io(self) -> SupportsFileno | None: if isinstance(self._term_input_file, SupportsFileno): return self._term_input_file return None def _input_fileno(self) -> int | None: """Return the fileno of the input stream, or None if it doesn't have one. A stream without a fileno can't participate in whatever. """ if isinstance(self._term_input_file, SupportsFileno): return self._term_input_file.fileno() return None def _on_update_palette_entry(self, name: Hashable, *attrspecs: AttrSpec) -> None: # copy the attribute to a dictionary containing the escape seqences a: AttrSpec = attrspecs[_ATTRSPEC_INDEX_BY_COLORS[self.colors]] self._pal_attrspec[name] = a self._pal_escape[name] = self._attrspec_to_escape(a) def set_input_timeouts( self, max_wait: float | None = None, complete_wait: float = 0.125, resize_wait: float = 0.125, ) -> None: """Set the get_input timeout values. All values are in floating point numbers of seconds. :param max_wait: amount of time in seconds to wait for input when there is no input pending, wait forever if None :param complete_wait: amount of time in seconds to wait when get_input detects an incomplete escape sequence at the end of the available input :param resize_wait: amount of time in seconds to wait for more input after receiving two screen resize requests in a row to stop Urwid from consuming 100% cpu during a gradual window resize operation """ self.max_wait = max_wait if max_wait is not None: if self._next_timeout is None: self._next_timeout = max_wait else: self._next_timeout = min(self._next_timeout, max_wait) self.complete_wait = complete_wait self.resize_wait = resize_wait def set_mouse_tracking(self, enable: bool = True) -> None: """ Enable (or disable) mouse tracking. After calling this function get_input will include mouse click events along with keystrokes. """ enable = bool(enable) if enable == self._mouse_tracking_enabled: return self._mouse_tracking(enable) def _mouse_tracking(self, enable: bool) -> None: """Write the mouse tracking escape sequences and track `_mouse_tracking_enabled`. A no-op when *enable* is True but mouse reporting was detected unsupported -- there is nothing to enable, and the tracked state has to agree. """ if enable and self.modes.mouse_reporting is False: self._mouse_tracking_enabled = False return self.write(escape.MOUSE_TRACKING_ON if enable else escape.MOUSE_TRACKING_OFF) self._mouse_tracking_enabled = enable def _is_real_terminal(self) -> bool: """Whether input/output are a real terminal that could answer a DECRQM probe. Overridden on Windows, whose input is a socket, not a descriptor `os.isatty` can inspect. """ fd = self._input_fileno() return fd is not None and os.isatty(fd) # TermModes field -> the sequence that enables it, once confirmed supported from a sentinel. _PRIVATE_MODE_ENABLE_SEQUENCE: typing.ClassVar[Mapping[str, str]] = { "bracketed_paste": escape.PrivateMode.BRACKETED_PASTE.enable_seq, "focus_reporting": escape.PrivateMode.FOCUS_REPORTING.enable_seq, } def _detect_terminal_modes(self) -> None: """Send DECRQM queries for the private modes still at their sentinel; does not wait for a reply. Replies are recognized later, during normal `parse_input()` (see `escape.KeyqueueTrie.read_private_mode_report`, `_apply_private_mode_reports()`). """ if not self._is_real_terminal(): return modes = [ escape.PrivateMode.SYNCHRONIZED_OUTPUT, escape.PrivateMode.GRAPHEME_CLUSTERING, escape.PrivateMode.ALTERNATE_SCREEN_BUFFER, escape.PrivateMode.MOUSE_REPORTING, escape.PrivateMode.MOUSE_SGR_MODE, ] if self.modes.bracketed_paste is None: modes.append(escape.PrivateMode.BRACKETED_PASTE) if self.modes.focus_reporting is None: modes.append(escape.PrivateMode.FOCUS_REPORTING) self.write("".join(mode.query for mode in modes)) self.flush() def _apply_private_mode_reports( self, decoded_codes: _DecodedInputWithReports, ) -> _DecodedInput: """Apply any DECRPM replies among *decoded_codes* to `self.modes` and strip them out.""" filtered: _DecodedInput = [] reports: list[tuple[str, int]] = [] for item in decoded_codes: if isinstance(item, tuple) and item[:1] == ("private mode report",): reports.append(typing.cast("tuple[str, int]", item[1:])) else: filtered.append(typing.cast("str | _MouseInput | _CursorPosition", item)) if reports: enable = [ code for field in self.modes.set_from_codes(*reports) if (code := self._PRIVATE_MODE_ENABLE_SEQUENCE.get(field, "")) ] if enable: self.write("".join(enable)) self.flush() return filtered @abc.abstractmethod def _start( # pylint: disable=keyword-arg-before-vararg self, alternate_buffer: bool = True, *args: typing.Any, **kwargs: typing.Any, ) -> None: """ Initialize the screen and input mode. :param alternate_buffer: use alternate screen buffer """ def _stop_mouse_restore_buffer(self) -> None: """Stop mouse tracking and restore the screen.""" self._mouse_tracking(False) move_cursor = "" if self._alternate_buffer: move_cursor = escape.PrivateMode.ALTERNATE_SCREEN_BUFFER.disable_seq elif self.maxrow is not None: move_cursor = escape.set_cursor_position(0, self.maxrow) self.write(self._attrspec_to_escape(AttrSpec("", "")) + escape.SI + move_cursor + escape.SHOW_CURSOR) self.flush() def _stop_restore_palette(self) -> None: """Reset (OSC 104) any palette entries modify_terminal_palette() changed this session.""" if not self._modified_palette_entries: return if self.term == "fbterm": # fbterm's palette-modification escape (used in modify_terminal_palette) has no # known reset counterpart, so there is nothing safe to send here. self._modified_palette_entries.clear() return indexes = ";".join(str(index) for index in sorted(self._modified_palette_entries)) self.write(f"\x1b]104;{indexes}\x1b\\") self.flush() self._modified_palette_entries.clear() @abc.abstractmethod def _stop(self) -> None: """Restore the screen.""" def write(self, data: str) -> None: """Write some data to the terminal. You may wish to override this if you're using something other than regular files for input and output. """ self._term_output_file.write(data) def flush(self) -> None: """Flush the output buffer. You may wish to override this if you're using something other than regular files for input and output. """ self._term_output_file.flush() @typing.overload def get_input(self, raw_keys: Literal[False]) -> _DecodedInput: ... @typing.overload def get_input(self, raw_keys: Literal[True]) -> tuple[_DecodedInput, list[int]]: ... def get_input(self, raw_keys: bool = False) -> _DecodedInput | tuple[_DecodedInput, list[int]]: r"""Return pending input as a list. :param raw_keys: return raw keycodes as well as translated versions :raises RuntimeError: the screen has not been started. This function will immediately return all the input since the last time it was called. If there is no input pending it will wait before returning an empty list. The wait time may be configured with the set_input_timeouts function. If raw_keys is False (default) this function will return a list of keys pressed. If raw_keys is True this function will return a ( keys pressed, raw keycodes ) tuple instead. Examples of keys returned: * ASCII printable characters: :kbd:`space`, :kbd:`a`, :kbd:`0`, :kbd:`A`, :kbd:`-`, :kbd:`/` * ASCII control characters: :kbd:`tab`, :kbd:`enter` * Escape sequences: :kbd:`up`, :kbd:`page up`, :kbd:`home`, :kbd:`insert`, :kbd:`f1` * Key combinations: :kbd:`shift f1`, :kbd:`meta a`, :kbd:`ctrl b` * Window events: ``"window resize"`` When a narrow encoding is not enabled: * "Extended ASCII" characters: "\xa1", "\xb2", "\xfe" When a wide encoding is enabled: * Double-byte characters: "\xa1\xea", "\xb2\xd4" When utf8 encoding is enabled: * Unicode characters: u"\u00a5", u'\u253c" Examples of mouse events returned: * Mouse button press: ('mouse press', 1, 15, 13), ('meta mouse press', 2, 17, 23) * Mouse drag: ('mouse drag', 1, 16, 13), ('mouse drag', 1, 17, 13), ('ctrl mouse drag', 1, 18, 13) * Mouse button release: ('mouse release', 0, 18, 13), ('ctrl mouse release', 0, 17, 23) """ logger = self.logger.getChild("get_input") if not self._started: raise RuntimeError self._wait_for_input_ready(self._next_timeout) keys, raw = self.parse_input(None, None, self.get_available_raw_input()) # Avoid pegging CPU at 100% when slowly resizing if keys == ["window resize"] and self.prev_input_resize: logger.debug('get_input: got "window resize" > 1 times. Enable throttling for resize.') for _ in range(2): self._wait_for_input_ready(self.resize_wait) new_keys, new_raw = self.parse_input(None, None, self.get_available_raw_input()) raw += new_raw if new_keys and new_keys != ["window resize"]: if "window resize" in new_keys: keys = new_keys else: keys.extend(new_keys) break if keys == ["window resize"]: self.prev_input_resize = 2 elif self.prev_input_resize == 2 and not keys: self.prev_input_resize = 1 else: self.prev_input_resize = 0 if raw_keys: return keys, raw return keys def get_input_descriptors(self) -> list[SupportsFileno | int]: """Return a list of integer file descriptors that should be polled in external event loops. Used to check for user input. Use this method if you are implementing your own event loop. This method is only called by `hook_event_loop`, so if you override that, you can safely ignore this. """ if not self._started: return [] fd_list: list[SupportsFileno | int] = [self._resize_pipe_rd] if (input_io := self._term_input_io) is not None: fd_list.append(input_io) return fd_list _current_event_loop_handles: Sequence[typing.Any] = () @abc.abstractmethod def unhook_event_loop(self, event_loop: EventLoop) -> None: """Remove any hooks added by hook_event_loop.""" @abc.abstractmethod def hook_event_loop( self, event_loop: EventLoop, callback: Callable[[_DecodedInput, list[int]], typing.Any], ) -> None: """Register the given callback with the event loop, to be called with new input whenever it's available. The callback should be passed a list of processed keys and a list of unprocessed keycodes. Subclasses may wish to use parse_input to wrap the callback. """ _input_timeout: typing.Any = None def _make_legacy_input_wrapper( self, event_loop: EventLoop, callback: Callable[[_DecodedInput, list[int]], typing.Any], ) -> Callable[[], None]: """Support old Screen classes that still have a get_input_nonblocking and expect it to work.""" @functools.wraps(callback) def wrapper() -> None: if self._input_timeout: event_loop.remove_alarm(self._input_timeout) self._input_timeout = None # should we deprecate? timeout, keys, raw = self.get_input_nonblocking() # type: ignore[attr-defined] # pylint: disable=no-member if timeout is not None: self._input_timeout = event_loop.alarm(timeout, wrapper) callback(keys, raw) return wrapper def _get_input_codes(self) -> list[int]: return list(self._get_keyboard_codes()) def get_available_raw_input(self) -> list[int]: """ Return any currently available input. Does not block. This method is only used by the default `hook_event_loop` implementation; you can safely ignore it if you implement your own. """ logger = self.logger.getChild("get_available_raw_input") codes = [*self._partial_codes, *self._get_input_codes()] self._partial_codes = [] # clean out the pipe used to signal external event loops # that a resize has occurred with selectors.DefaultSelector() as selector: selector.register(self._resize_pipe_rd, selectors.EVENT_READ) present_resize_flag = selector.select(0) # nonblocking while present_resize_flag: logger.debug("Resize signal received. Cleaning socket.") # Argument "size" is maximum buffer size to read. Since we're emptying, set it reasonably big. self._resize_pipe_rd.recv(128) present_resize_flag = selector.select(0) return codes @typing.overload def parse_input( self, event_loop: EventLoop | None, callback: None, codes: list[int], wait_for_more: bool = ..., ) -> tuple[_DecodedInput, list[int]]: ... @typing.overload def parse_input( self, event_loop: EventLoop | None, callback: Callable[[_DecodedInput, list[int]], typing.Any], codes: list[int], wait_for_more: bool = ..., ) -> None: ... def parse_input( self, event_loop: EventLoop | None, callback: Callable[[_DecodedInput, list[int]], typing.Any] | None, codes: list[int], wait_for_more: bool = True, ) -> tuple[_DecodedInput, list[int]] | None: """ Read any available input from get_available_raw_input, parses it into keys, and calls the given callback. The current implementation tries to avoid any assumptions about what the screen or event loop look like; it only deals with parsing keycodes and setting a timeout when an incomplete one is detected. `codes` should be a sequence of keycodes, i.e. bytes. A bytearray is appropriate, but beware of using bytes, which only iterates as integers on Python 3. """ logger = self.logger.getChild("parse_input") # Note: event_loop may be None for 100% synchronous support, only used # by get_input. Not documented because you shouldn't be doing it. if self._input_timeout and event_loop: event_loop.remove_alarm(self._input_timeout) self._input_timeout = None original_codes = codes decoded_codes: _DecodedInputWithReports = [] try: while codes: run, remaining_codes = escape.process_keyqueue(codes, wait_for_more) codes = remaining_codes.copy() decoded_codes.extend(run) except escape.MoreInputRequired: # Set a timer to wait for the rest of the input; if it goes off # without any new input having come in, use the partial input k = len(original_codes) - len(codes) raw_codes = original_codes[:k] self._partial_codes = codes def _parse_incomplete_input() -> None: self._input_timeout = None self._partial_codes = [] self.parse_input(event_loop, callback, codes, wait_for_more=False) if event_loop: self._input_timeout = event_loop.alarm(self.complete_wait, _parse_incomplete_input) else: raw_codes = original_codes self._partial_codes = [] output_codes = self._apply_private_mode_reports(decoded_codes) logger.debug(f"Decoded codes: {output_codes!r}, raw codes: {raw_codes!r}") if self._resized: self._resized = False self.screen_buf = None output_codes.append("window resize") logger.debug('Added "window resize" to the codes') if callback: callback(output_codes, raw_codes) return None # For get_input return output_codes, raw_codes def _wait_for_input_ready(self, timeout: float | None) -> list[int]: logger = self.logger.getChild("wait_for_input_ready") fd_list = self.get_input_descriptors() logger.debug(f"Waiting for input: descriptors={fd_list!r}, timeout={timeout!r}") with selectors.DefaultSelector() as selector: for fd in fd_list: selector.register(fd, selectors.EVENT_READ) ready = selector.select(timeout) logger.debug(f"Input ready: {ready}") return [event.fd for event, _ in ready] @abc.abstractmethod def _read_raw_input(self, timeout: float) -> Iterable[int]: ... def _get_keyboard_codes(self) -> Iterable[int]: return self._read_raw_input(0) def _setup_G1(self) -> None: """Initialize the G1 character set to graphics mode if required. :raises OSError: the terminal could not be written to. The setup is tried again on the next draw. """ if self._setup_G1_done: return self.write(escape.DESIGNATE_G1_SPECIAL) self.flush() self._setup_G1_done = True def draw_screen(self, size: tuple[int, int], canvas: Canvas) -> None: """Paint screen with rendered canvas. :raises RuntimeError: the screen has not been started. :raises ValueError: *canvas* does not have the number of rows given by *size*. :raises OSError: the terminal could not be written to, for a reason other than an interrupted system call. """ def set_cursor_position(x: int, y: int) -> str: if self._rows_used is None: return escape.set_cursor_position(x, y) if cy > y: return "\b" + escape.CURSOR_HOME_COL + escape.move_cursor_up(cy - y) + escape.move_cursor_right(x) return "\b" + escape.CURSOR_HOME_COL + escape.move_cursor_down(y - cy) + escape.move_cursor_right(x) def handle_row( attr: AttrSpec | str | None, charset: Literal["0", "U"] | None, run: bytes, last: bool, ) -> None: """Append the escape sequences and text of one canvas run to the output. :raises TypeError: *run* is not a byte string. :raises ValueError: *charset* is not a known character set flag. """ nonlocal last_charset_flag, last_attributes, first # type: ignore[misc] if not isinstance(run, bytes): # canvases render with bytes raise TypeError(run) if charset != "U": run = run.translate(UNPRINTABLE_TRANS_TABLE) if last_attributes != attr: output.append(self._attr_to_escape(attr)) last_attributes = attr if last: output.append("\x08" * back) if encoding != "utf-8" and (first or last_charset_flag != charset): if charset not in {None, "0", "U"}: raise ValueError(charset) if last_charset_flag == "U": output.append(escape.IBMPC_OFF) if charset is None: icss = escape.SI elif charset == "U": icss = escape.IBMPC_ON else: icss = escape.SO output.append(icss) last_charset_flag = charset line_text = run.decode(encoding, "replace") if charset != "U" and not line_text.isascii(): line_text = line_text.translate(UNPRINTABLE_C1_TRANS_TABLE) if not last: output.append(line_text) else: if not IS_WINDOWS: output.extend((escape.INSERT_ON, line_text, escape.INSERT_OFF)) else: output.extend((f"{escape.ESC}[{str_util.calc_width(line_text, 0, len(line_text))}@", line_text)) if encoding != "utf-8" and charset == "U": output.append(escape.IBMPC_OFF) first = False encoding = util.get_encoding() logger = self.logger.getChild("draw_screen") (maxcol, maxrow) = size if not self._started: raise RuntimeError if maxrow != canvas.rows(): raise ValueError(maxrow) # quick return if nothing has changed if self.screen_buf and canvas is self._screen_buf_canvas: return self._setup_G1() if self._resized: # handle resize before trying to draw screen logger.debug("Not drawing screen: screen resized and resize was not handled") return logger.debug(f"Drawing screen with size {size!r}") last_attributes: AttrSpec | str | None = None # Default = empty output: list[str] = [escape.HIDE_CURSOR, self._attr_to_escape(last_attributes)] if self._rows_used is None: output.append(escape.CURSOR_HOME) osb: list[list[tuple[AttrSpec | str | None, Literal["0", "U"] | None, bytes]]] if self.screen_buf: osb = self.screen_buf else: osb = [] sb: list[list[tuple[AttrSpec | str | None, Literal["0", "U"] | None, bytes]]] = [] cy = self._cy y = -1 ins = None if self._rows_used is None: output.append(escape.set_cursor_position(0, 0)) else: output.append(escape.CURSOR_HOME_COL + escape.move_cursor_up(cy)) cy = 0 first = True last_charset_flag: Literal["0", "U"] | None = None for row in canvas.content(): y += 1 if osb and y < len(osb) and osb[y] == row: # this row of the screen buffer matches what is # currently displayed, so we can skip this line sb.append(osb[y]) continue sb.append(row) # leave blank lines off display when we are using # the default screen buffer (allows partial screen) if self._rows_used is not None and y > self._rows_used: if len(row) == 1 and not row[0][2].strip(): continue self._rows_used = y if y or self._rows_used is not None: output.append(set_cursor_position(0, y)) # after updating the line we will be just over the # edge, but terminals still treat this as being # on the same line cy = y whitespace_at_end = False back = 0 if row: a, cs, run = row[-1] if ( run[-1:] == b" " and self.back_color_erase and not ( isinstance(pal_a := self._pal_attrspec.get(a, a), AttrSpec) and (pal_a.standout or pal_a.underline) ) ): whitespace_at_end = True row = [*row[:-1], (a, cs, run.rstrip(b" "))] # noqa: PLW2901 elif y == maxrow - 1 and maxcol > 1: row, back, ins = self._last_row(row) # noqa: PLW2901 else: continue for a, cs, run in row: handle_row(a, cs, run, last=False) if ins: (a, cs, run) = ins handle_row(a, cs, run, last=True) if whitespace_at_end: output.append(escape.ERASE_IN_LINE_RIGHT) if canvas.cursor is not None: x, y = canvas.cursor output += [set_cursor_position(x, y), escape.SHOW_CURSOR] self._cy = y if self._resized: # handle resize before trying to draw screen return try: # A single write() call, rather than one per output fragment, avoids paying the # per-call overhead (and, for a real file, a separate write syscall) once per row. frame = "".join(line.decode(encoding, "replace") if isinstance(line, bytes) else line for line in output) if self.modes.synchronized_output: # Confirmed supported; the terminal defers painting until END_SYNCHRONIZED_UPDATE. frame = ( f"{escape.PrivateMode.SYNCHRONIZED_OUTPUT.enable_seq}" f"{frame}" f"{escape.PrivateMode.SYNCHRONIZED_OUTPUT.disable_seq}" ) self.write(frame) self.flush() except OSError as e: # ignore interrupted syscall if e.args[0] != errno.EINTR: raise self.screen_buf = sb self._screen_buf_canvas = canvas def _last_row( self, row: list[tuple[AttrSpec | str | None, Literal["0", "U"] | None, bytes]], ) -> tuple[ list[tuple[AttrSpec | str | None, Literal["0", "U"] | None, bytes]], int, tuple[AttrSpec | str | None, Literal["0", "U"] | None, bytes] | None, ]: """On the last row we need to slide the bottom right character into place. Calculate the new line, attr and an insert sequence to do that. eg. last row: XXXXXXXXXXXXXXXXXXXXYZ Y will be drawn after Z, shifting Z into position. When the whole row is a single grapheme, as happens with a double width character on a two column screen, there is no Y to draw after Z. The row is then returned untouched and no insert sequence is produced. """ new_row: list[tuple[AttrSpec | str | None, Literal["0", "U"] | None, bytes]] = row[:-1] z_attr, z_cs, last_text = row[-1] last_cols = str_util.calc_width(last_text, 0, len(last_text)) last_offs, z_col = str_util.calc_text_pos(last_text, 0, len(last_text), last_cols - 1) if last_offs == 0: if not new_row: return row, 0, None z_text = last_text del new_row[-1] # we need another segment y_attr, y_cs, nlast_text = row[-2] nlast_cols = str_util.calc_width(nlast_text, 0, len(nlast_text)) z_col += nlast_cols nlast_offs, y_col = str_util.calc_text_pos(nlast_text, 0, len(nlast_text), nlast_cols - 1) y_text = nlast_text[nlast_offs:] if nlast_offs: new_row.append((y_attr, y_cs, nlast_text[:nlast_offs])) else: z_text = last_text[last_offs:] y_attr, y_cs = z_attr, z_cs nlast_cols = str_util.calc_width(last_text, 0, last_offs) nlast_offs, y_col = str_util.calc_text_pos(last_text, 0, last_offs, nlast_cols - 1) y_text = last_text[nlast_offs:last_offs] if nlast_offs: new_row.append((y_attr, y_cs, last_text[:nlast_offs])) new_row.append((z_attr, z_cs, z_text)) return new_row, z_col - y_col, (y_attr, y_cs, y_text) def clear(self) -> None: """Force the screen to be completely repainted on the next call to draw_screen().""" self.screen_buf = None def _attr_to_escape(self, a: AttrSpec | str | None) -> str: """Convert attribute instance a to an escape sequence for the terminal.""" if found := self._pal_escape.get(a): return found if isinstance(a, AttrSpec): return self._attrspec_to_escape(a) if a is None: return self._attrspec_to_escape(AttrSpec("default", "default")) # undefined attributes use default/default self.logger.debug(f"Undefined attribute: {a!r}") return self._attrspec_to_escape(AttrSpec("default", "default")) def _attrspec_to_escape(self, a: AttrSpec) -> str: r"""Convert AttrSpec instance a to an escape sequence for the terminal. >>> from urwid.display.raw import Screen # this class is abstract >>> s = Screen() >>> s.set_terminal_properties(colors=256) >>> a2e = s._attrspec_to_escape >>> a2e(s.AttrSpec("brown", "dark green")) '\x1b[0;33;42m' >>> a2e(s.AttrSpec("#fea,underline", "#d0d")) '\x1b[0;38;5;229;4;48;5;164m' """ if self.term == "fbterm": fg = escape.ESC + f"[1;{a.foreground_number:d}}}" bg = escape.ESC + f"[2;{a.background_number:d}}}" return fg + bg if a.foreground_true: fg = f"38;2;{';'.join(str(part) for part in a.get_rgb_values()[0:3])}" elif a.foreground_high: fg = f"38;5;{a.foreground_number:d}" elif a.foreground_basic: if a.foreground_number > 7: if self.fg_bright_is_bold: fg = f"1;{a.foreground_number - _SGR_BRIGHT_OFFSET + _SGR_FG_BASE:d}" else: fg = f"{a.foreground_number - _SGR_BRIGHT_OFFSET + _SGR_FG_BRIGHT_BASE:d}" else: fg = f"{a.foreground_number + _SGR_FG_BASE:d}" else: fg = "39" st = ( "1;" * a.bold + "2;" * a.faint + "3;" * a.italics + "4;" * a.underline + "5;" * a.blink + "7;" * a.standout + "9;" * a.strikethrough ) if a.background_true: bg = f"48;2;{';'.join(str(part) for part in a.get_rgb_values()[3:6])}" elif a.background_high: bg = f"48;5;{a.background_number:d}" elif a.background_basic: if a.background_number > 7: if self.bg_bright_is_blink: bg = f"5;{a.background_number - _SGR_BRIGHT_OFFSET + _SGR_BG_BASE:d}" else: # this doesn't work on most terminals bg = f"{a.background_number - _SGR_BRIGHT_OFFSET + _SGR_BG_BRIGHT_BASE:d}" else: bg = f"{a.background_number + _SGR_BG_BASE:d}" else: bg = "49" return f"{escape.ESC}[0;{fg};{st}{bg}m" def set_terminal_properties( self, colors: Literal[1, 16, 88, 256, 16777216] | None = None, bright_is_bold: bool | None = None, has_underline: bool | None = None, ) -> None: """Set the terminal's color and attribute-rendering properties. :param colors: number of colors terminal supports (1, 16, 88, 256, or 2**24) or None to leave unchanged :param bright_is_bold: set to True if this terminal uses the bold setting to create bright colors (numbers 8-15), set to False if this Terminal can create bright colors without bold or None to leave unchanged :param has_underline: set to True if this terminal can use the underline setting, False if it cannot or None to leave unchanged """ if colors is None: colors = self.colors if bright_is_bold is None: bright_is_bold = self.fg_bright_is_bold if has_underline is None: has_underline = self.has_underline if colors == self.colors and bright_is_bold == self.fg_bright_is_bold and has_underline == self.has_underline: return self.colors = colors self.fg_bright_is_bold = bright_is_bold self.has_underline = has_underline self.clear() self._pal_escape = {} for p, v in self._palette.items(): self._on_update_palette_entry(p, *v) def reset_default_terminal_palette(self) -> None: """Attempt to set the terminal palette to default values as taken from xterm. Uses a number of colors from the current set_terminal_properties() screen setting. """ if self.colors == 1: return if self.colors == 2**24: colors = 256 else: colors = self.colors def rgb_values(n: int) -> tuple[int | None, int | None, int | None]: if colors == 16: aspec = AttrSpec(f"h{n:d}", "", 256) else: aspec = AttrSpec(f"h{n:d}", "", typing.cast("Literal[1, 16, 88, 256, 16777216]", colors)) return aspec.get_rgb_values()[:3] entries = [(n, *rgb_values(n)) for n in range(min(colors, 256))] self.modify_terminal_palette(entries) def modify_terminal_palette(self, entries: list[tuple[int, int | None, int | None, int | None]]) -> None: """Attempt to set part of the terminal palette; this does not work on all terminals. The changes are sent as a single escape sequence so they should all take effect at the same time. 0 <= index < 256 (some terminals will only have 16 or 88 colors) 0 <= red, green, blue < 256 :param entries: list of (index, red, green, blue) tuples. """ if self.term == "fbterm": modify = [f"{index:d};{red:d};{green:d};{blue:d}" for index, red, green, blue in entries] self.write(f"\x1b[3;{';'.join(modify)}}}") else: modify = [f"{index:d};rgb:{red:02x}/{green:02x}/{blue:02x}" for index, red, green, blue in entries] self.write(f"\x1b]4;{';'.join(modify)}\x1b\\") self.flush() self._modified_palette_entries.update(index for index, _red, _green, _blue in entries) # shortcut for creating an AttrSpec with this screen object's # number of colors def AttrSpec(self, fg: str, bg: str) -> AttrSpec: """Create an :class:`AttrSpec` using this screen's number of colors.""" return AttrSpec(fg, bg, self.colors) urwid-urwid-69907e4/urwid/display/_web.css000066400000000000000000000006271526073734300205310ustar00rootroot00000000000000body { margin: 8px 8px 8px 8px; border: 0; color: black; background-color: silver; font-family: fixed; overflow: hidden; } form { margin: 0 0 8px 0; } #text { position: relative; background-color: silver; width: 100%; height: 100%; margin: 3px 0 0 0; border: 1px solid #999; } #page { position: relative; width: 100%;height: 100%;} @keyframes urwid-blink { 50% { opacity: 0; } } urwid-urwid-69907e4/urwid/display/_web.js000066400000000000000000000307041526073734300203540ustar00rootroot00000000000000// Urwid web (CGI/Asynchronous Javascript) display module // Copyright (C) 2004-2005 Ian Ward // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library 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 // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // // Urwid web site: https://urwid.org/ "use strict"; // Separates a span's inline CSS from its text content within a wire-format line // (see urwid.display.web._STYLE_SEP). Safe because urwid.display.web._trans_table // has already replaced any control character in the text with '?'. const style_separator = ""; // KeyboardEvent.key values that urwid knows under a different name. const key_names = { "Backspace": "backspace", "Tab": "tab", "Enter": "enter", "Escape": "esc", "PageUp": "page up", "PageDown": "page down", "End": "end", "Home": "home", "ArrowLeft": "left", "ArrowUp": "up", "ArrowRight": "right", "ArrowDown": "down", "Insert": "insert", "Delete": "delete" }; const function_key = /^F([1-9]|1[0-2])$/; // Boundary used by the multipart/x-mixed-replace stream sent by web.py. const boundary = "ZZ"; const check_font_delay = 1000; const poll_again_delay = 500; const send_queue_max = 32; const request_url = document.URL; let update_method = null; let urwid_id = null; let screen_x = null; let screen_y = null; let send_queue = []; let sending = false; function load_web_display() { if (typeof fetch !== "function") { set_status("Connection Failed"); alert("This browser is too old to run the urwid web display."); return; } document.addEventListener("keydown", body_keydown); window.addEventListener("resize", body_resize); update_method = supports_streaming() ? "multipart" : "polling"; body_resize(); send_queue = []; // don't queue the first resize, it is sent with the request set_status("Connecting"); void start_connection(); setInterval(body_resize, check_font_delay); } // The multipart update method needs the response body as a stream: browsers // only demultiplex multipart/x-mixed-replace for images and navigations. function supports_streaming() { return typeof ReadableStream === "function" && typeof TextDecoder === "function" && typeof Response === "function" && "body" in Response.prototype; } async function start_connection() { try { const response = await fetch(request_url, { method: "POST", headers: { "X-Urwid-Method": update_method, "Content-Type": "text/plain; charset=utf-8" }, body: "window resize " + screen_x + " " + screen_y + "\n" }); if (!check_response(response)) { return; } urwid_id = response.headers.get("X-Urwid-ID"); set_status(update_method === "polling" ? "Polling" : "Connected"); void flush_send_queue(); if (update_method === "polling") { if (handle_update(await response.text())) { poll_again(); } } else { await read_updates(response.body); } } catch (e) { connection_failed(e); } } async function do_poll() { if (update_method !== "polling" || urwid_id === null) { return; } try { const response = await fetch(request_url, { method: "POST", headers: { "X-Urwid-Method": "polling", "X-Urwid-ID": urwid_id, "Content-Type": "text/plain; charset=utf-8" }, body: "" }); if (!check_response(response)) { return; } if (handle_update(await response.text())) { poll_again(); } } catch (e) { connection_failed(e); } } function poll_again() { if (update_method === "polling") { setTimeout(do_poll, poll_again_delay); } } // Read screen updates from the multipart/x-mixed-replace body stream. async function read_updates(body) { const reader = body.getReader(); const decoder = new TextDecoder("utf-8"); const parser = new MultipartParser(boundary); try { for (;;) { const chunk = await reader.read(); if (chunk.done) { break; } let open = true; for (const part of parser.push(decoder.decode(chunk.value, {stream: true}))) { open = handle_update(part); if (!open) { break; } } if (!open || parser.finished) { return; } } set_status("Connection Closed"); update_method = null; } finally { await reader.cancel(); } } // Split a multipart/x-mixed-replace body into the payload of each part. class MultipartParser { constructor(boundary_id) { this.delimiter = "\r\n--" + boundary_id; this.buffer = ""; this.preamble = true; this.finished = false; } push(text) { this.buffer += text; const parts = []; while (!this.finished) { const index = this.buffer.indexOf(this.delimiter); if (index < 0) { break; } // "--" marks the closing delimiter, "\r\n" ends the boundary line. const tail = index + this.delimiter.length; if (this.buffer.length < tail + 2) { break; } const part = this.buffer.slice(0, index); this.finished = this.buffer.startsWith("--", tail); this.buffer = this.buffer.slice(tail + 2); if (this.preamble) { this.preamble = false; } else { // an empty header block separates the part headers from the payload parts.push(part.startsWith("\r\n") ? part.slice(2) : part); } } return parts; } } // Returns false once the server has closed the connection. function handle_update(payload) { let text = payload; if (text.startsWith("\r\n")) { text = text.slice(2); // padding after the CGI headers } if (text.endsWith("\n")) { text = text.slice(0, -1); // terminator of the last row } if (text === "") { return true; // keepalive } if (text === "Z") { set_status("Connection Closed"); update_method = null; return false; } render_update(text); return true; } function render_update(text) { const container = document.getElementById('text'); const last_screen = Array.from(container.childNodes); let line = document.createElement('span'); let row = 0; for (const frag of text.split("\n")) { if (frag === "") { line.appendChild(document.createElement('br')); place_line(container, line, row); row += 1; line = document.createElement('span'); } else if (frag.startsWith("<")) { // the row is unchanged from the last screen const source = Number.parseInt(frag.slice(1), 10); if (source !== row) { const previous = last_screen[source]; place_line(container, previous ? previous.cloneNode(true) : document.createElement('span'), row); } row += 1; } else { const sep = frag.indexOf(style_separator); line.appendChild(make_span(frag.slice(sep + 1), frag.slice(0, sep))); } } while (container.childNodes.length > row) { container.removeChild(container.lastChild); } } function place_line(container, line, row) { if (container.childNodes.length > row) { container.replaceChild(line, container.childNodes[row]); } else { container.appendChild(line); } } function make_span(s, style) { const d = document.createElement('span'); d.style.cssText = style; d.appendChild(document.createTextNode(s)); return d; } function set_status(status) { document.getElementById('status').textContent = status; } function connection_failed(reason) { if (!update_method) { return; // the connection was closed on purpose } set_status("Connection Failed"); alert("Error talking to server: " + reason); } // Returns false if the response was an error that has been reported already. function check_response(response) { if (response.ok) { return true; } if (response.status === 404 && urwid_id !== null) { set_status("Connection Closed"); update_method = null; return false; } if (response.status === 403 && update_method === "polling") { set_status("Server Refused Connection"); alert("This server does not allow polling clients.\n\n" + "Please use a web browser that can read streaming " + "responses, such as Mozilla Firefox"); return false; } if (response.status === 503) { set_status("Connection Failed"); alert("The server has reached its maximum number of " + "connections.\n\nPlease try again later."); return false; } set_status("Connection Failed"); alert("Error from server: " + response.statusText); return false; } function body_keydown(e) { if (!update_method) { return; } const name = key_name(e); if (name === null) { return; } let mod = ""; if (e.ctrlKey) { mod = "ctrl " + mod; } if (e.altKey || e.metaKey) { mod = "meta " + mod; } // printable keys already carry the effect of the shift key if (e.shiftKey && name !== e.key) { mod = "shift " + mod; } e.stopPropagation(); e.preventDefault(); send_key(mod + name); } // Map a KeyboardEvent to an urwid key name, or null if urwid ignores it. function key_name(e) { if (e.isComposing || !e.key || e.key === "Unidentified") { return null; } if (Object.prototype.hasOwnProperty.call(key_names, e.key)) { return key_names[e.key]; } if (function_key.test(e.key)) { return e.key.toLowerCase(); } if (Array.from(e.key).length === 1) { return e.key; // a printable character } return null; // modifier or a key urwid has no name for } function send_key(key) { if (send_queue.length >= send_queue_max) { return; // buffer overrun } send_queue.push(key); void flush_send_queue(); } async function flush_send_queue() { if (sending || urwid_id === null || !update_method) { return; } sending = true; try { while (send_queue.length > 0 && update_method) { const keys = send_queue; send_queue = []; const response = await fetch(request_url, { method: "POST", headers: { "X-Urwid-ID": urwid_id, "Content-Type": "text/plain; charset=utf-8" }, body: keys.join("\n") + "\n" }); if (!check_response(response)) { return; } } } catch (e) { connection_failed(e); } finally { sending = false; } } function body_resize() { const t = document.getElementById('testchar'); const t2 = document.getElementById('testchar2'); const text = document.getElementById('text'); const char_width = t.offsetLeft / 44; const avail_width = window.innerWidth - 18; const x_size = (avail_width - (avail_width % char_width)) / char_width; const char_height = t2.offsetTop - t.offsetTop; const avail_height = window.innerHeight - text.offsetTop - 10; const y_size = (avail_height - (avail_height % char_height)) / char_height; text.style.width = x_size * char_width + "px"; text.style.height = y_size * char_height + "px"; if (screen_x !== x_size || screen_y !== y_size) { send_key("window resize " + x_size + " " + y_size); } screen_x = x_size; screen_y = y_size; } urwid-urwid-69907e4/urwid/display/_win32.py000066400000000000000000000144271526073734300205610ustar00rootroot00000000000000"""ctypes bindings for the Windows console API used by the Windows raw display.""" from __future__ import annotations import enum import typing from ctypes import POINTER, Structure, Union, windll from ctypes.wintypes import BOOL, CHAR, DWORD, HANDLE, LPDWORD, SHORT, UINT, WCHAR, WORD # https://docs.microsoft.com/de-de/windows/console/getstdhandle STD_INPUT_HANDLE = -10 STD_OUTPUT_HANDLE = -11 # https://docs.microsoft.com/de-de/windows/console/setconsolemode ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 DISABLE_NEWLINE_AUTO_RETURN = 0x0008 ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200 ENABLE_WINDOW_INPUT = 0x0008 class COORD(Structure): """https://docs.microsoft.com/en-us/windows/console/coord-str.""" _fields_: typing.ClassVar[list[tuple[str, type[SHORT]]]] = [ ("X", SHORT), ("Y", SHORT), ] class SMALL_RECT(Structure): """https://docs.microsoft.com/en-us/windows/console/small-rect-str.""" _fields_: typing.ClassVar[list[tuple[str, type[SHORT]]]] = [ ("Left", SHORT), ("Top", SHORT), ("Right", SHORT), ("Bottom", SHORT), ] class CONSOLE_SCREEN_BUFFER_INFO(Structure): """https://docs.microsoft.com/en-us/windows/console/console-screen-buffer-info-str.""" _fields_: typing.ClassVar[list[tuple[str, type[COORD]] | tuple[str, type[WORD]] | tuple[str, type[SMALL_RECT]]]] = [ ("dwSize", COORD), ("dwCursorPosition", COORD), ("wAttributes", WORD), ("srWindow", SMALL_RECT), ("dwMaximumWindowSize", COORD), ] class uChar(Union): """https://docs.microsoft.com/en-us/windows/console/key-event-record-str.""" _fields_: typing.ClassVar[list[tuple[str, type[CHAR]] | tuple[str, type[WCHAR]]]] = [ ("AsciiChar", CHAR), ("UnicodeChar", WCHAR), ] class KEY_EVENT_RECORD(Structure): """https://docs.microsoft.com/en-us/windows/console/key-event-record-str.""" _fields_: typing.ClassVar[ list[tuple[str, type[BOOL]] | tuple[str, type[WORD]] | tuple[str, type[uChar]] | tuple[str, type[DWORD]]] ] = [ ("bKeyDown", BOOL), ("wRepeatCount", WORD), ("wVirtualKeyCode", WORD), ("wVirtualScanCode", WORD), ("uChar", uChar), ("dwControlKeyState", DWORD), ] class MOUSE_EVENT_RECORD(Structure): """https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str.""" _fields_: typing.ClassVar[list[tuple[str, type[COORD]] | tuple[str, type[DWORD]]]] = [ ("dwMousePosition", COORD), ("dwButtonState", DWORD), ("dwControlKeyState", DWORD), ("dwEventFlags", DWORD), ] class MouseButtonState(enum.IntFlag): """https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str.""" FROM_LEFT_1ST_BUTTON_PRESSED = 0x0001 RIGHTMOST_BUTTON_PRESSED = 0x0002 FROM_LEFT_2ND_BUTTON_PRESSED = 0x0004 FROM_LEFT_3RD_BUTTON_PRESSED = 0x0008 FROM_LEFT_4TH_BUTTON_PRESSED = 0x0010 class MouseEventFlags(enum.IntFlag): """https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str.""" BUTTON_PRESSED = 0x0000 # Default action, used in examples, but not in official enum MOUSE_MOVED = 0x0001 DOUBLE_CLICK = 0x0002 MOUSE_WHEELED = 0x0004 MOUSE_HWHEELED = 0x0008 class WINDOW_BUFFER_SIZE_RECORD(Structure): """https://docs.microsoft.com/en-us/windows/console/window-buffer-size-record-str.""" _fields_: typing.ClassVar[list[tuple[str, type[COORD]]]] = [("dwSize", COORD)] class MENU_EVENT_RECORD(Structure): """https://docs.microsoft.com/en-us/windows/console/menu-event-record-str.""" _fields_: typing.ClassVar[list[tuple[str, type[UINT]]]] = [("dwCommandId", UINT)] class FOCUS_EVENT_RECORD(Structure): """https://docs.microsoft.com/en-us/windows/console/focus-event-record-str.""" _fields_: typing.ClassVar[list[tuple[str, type[BOOL]]]] = [("bSetFocus", BOOL)] class Event(Union): """https://docs.microsoft.com/en-us/windows/console/input-record-str.""" _fields_: typing.ClassVar[ list[ tuple[str, type[KEY_EVENT_RECORD]] | tuple[str, type[MOUSE_EVENT_RECORD]] | tuple[str, type[WINDOW_BUFFER_SIZE_RECORD]] | tuple[str, type[MENU_EVENT_RECORD]] | tuple[str, type[FOCUS_EVENT_RECORD]] ] ] = [ ("KeyEvent", KEY_EVENT_RECORD), ("MouseEvent", MOUSE_EVENT_RECORD), ("WindowBufferSizeEvent", WINDOW_BUFFER_SIZE_RECORD), ("MenuEvent", MENU_EVENT_RECORD), ("FocusEvent", FOCUS_EVENT_RECORD), ] class INPUT_RECORD(Structure): """https://docs.microsoft.com/en-us/windows/console/input-record-str.""" _fields_: typing.ClassVar[list[tuple[str, type[WORD]] | tuple[str, type[Event]]]] = [ ("EventType", WORD), ("Event", Event), ] class EventType(enum.IntFlag): """Windows console `INPUT_RECORD` event type flags.""" KEY_EVENT = 0x0001 MOUSE_EVENT = 0x0002 WINDOW_BUFFER_SIZE_EVENT = 0x0004 MENU_EVENT = 0x0008 FOCUS_EVENT = 0x0010 # https://docs.microsoft.com/de-de/windows/console/getstdhandle GetStdHandle = windll.kernel32.GetStdHandle GetStdHandle.argtypes = [DWORD] GetStdHandle.restype = HANDLE # https://docs.microsoft.com/de-de/windows/console/getconsolemode GetConsoleMode = windll.kernel32.GetConsoleMode GetConsoleMode.argtypes = [HANDLE, LPDWORD] GetConsoleMode.restype = BOOL # https://docs.microsoft.com/de-de/windows/console/setconsolemode SetConsoleMode = windll.kernel32.SetConsoleMode SetConsoleMode.argtypes = [HANDLE, DWORD] SetConsoleMode.restype = BOOL # https://docs.microsoft.com/de-de/windows/console/readconsoleinput ReadConsoleInputW = windll.kernel32.ReadConsoleInputW ReadConsoleInputW.argtypes = [HANDLE, POINTER(INPUT_RECORD), DWORD, LPDWORD] ReadConsoleInputW.restype = BOOL # https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitforsingleobject WAIT_OBJECT_0 = 0x00000000 WAIT_TIMEOUT = 0x00000102 WaitForSingleObject = windll.kernel32.WaitForSingleObject WaitForSingleObject.argtypes = [HANDLE, DWORD] WaitForSingleObject.restype = DWORD # https://docs.microsoft.com/en-us/windows/console/getconsolescreenbufferinfo GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo GetConsoleScreenBufferInfo.argtypes = [HANDLE, POINTER(CONSOLE_SCREEN_BUFFER_INFO)] GetConsoleScreenBufferInfo.restype = BOOL urwid-urwid-69907e4/urwid/display/_win32_raw_display.py000066400000000000000000000345161526073734300231600ustar00rootroot00000000000000# Urwid raw display module # Copyright (C) 2004-2009 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """Direct terminal UI implementation.""" from __future__ import annotations import contextlib import functools import logging import selectors import socket import sys import threading import typing from ctypes import byref from ctypes.wintypes import DWORD from urwid import signals from . import _raw_display_base, _win32, escape from .common import INPUT_DESCRIPTORS_CHANGED # milliseconds the console reader waits for input before checking whether it has to stop _INPUT_POLL_MS = 100 if typing.TYPE_CHECKING: from collections.abc import Callable from urwid.event_loop import EventLoop _MouseInput = tuple[str, int, int, int] _CursorPosition = tuple[typing.Literal["cursor position"], int, int] _DecodedInput = list[typing.Union[str, _MouseInput, _CursorPosition]] class Screen(_raw_display_base.Screen): """Raw screen backend that drives a real Windows console.""" _term_input_file: socket.socket # Only set for the socket pair created here: an input given by the caller is fed by the caller. _send_input: socket.socket | None = None def __init__( self, input: socket.socket | None = None, # noqa: A002 # pylint: disable=redefined-builtin output: _raw_display_base.TextWriter = sys.stdout, bracketed_paste_mode: bool | None = None, focus_reporting: bool | None = None, ) -> None: """Initialize a screen that directly prints escape codes to an output terminal. :param bracketed_paste_mode: enable bracketed paste (`begin`/`end paste` keystrokes). None (default) auto-detects via DECRQM and enables it once confirmed supported; pass True/False to force it without probing. :param focus_reporting: enable focus reporting (`focus in`/`focus out` keystrokes). None (default) auto-detects via DECRQM and enables it once confirmed supported; pass True/False to force it without probing. """ if input is None: input, self._send_input = socket.socketpair() # noqa: A001 super().__init__(input, output, bracketed_paste_mode=bracketed_paste_mode, focus_reporting=focus_reporting) _dwOriginalOutMode: DWORD | None = None _dwOriginalInMode: DWORD | None = None _console_mode_active: bool = False def _is_real_terminal(self) -> bool: """Whether stdin/stdout are a real console with VT mode negotiated in `_start()`. The base implementation checks `os.isatty` on the input file descriptor, which is wrong here: `_input_fileno()` is a socket from `socket.socketpair()`, fed by `ReadInputThread` reading the real console, not the console handle itself. """ return self._console_mode_active def _start( # pylint: disable=keyword-arg-before-vararg self, alternate_buffer: bool = True, *args: typing.Any, **kwargs: typing.Any, ) -> None: """ Initialize the screen and input mode. :param alternate_buffer: use alternate screen buffer :raises TypeError: unexpected positional or keyword arguments were given. :raises RuntimeError: the console mode could not be set. """ if args or kwargs: raise TypeError(f"start() got unexpected arguments: {args=!r}, {kwargs=!r}") if alternate_buffer: self.write(escape.PrivateMode.ALTERNATE_SCREEN_BUFFER.enable_seq) self._rows_used = None else: self._rows_used = 0 # Set as soon as the buffer is actually switched, not after: _stop() (e.g. from cleanup # after a later exception in this method) has to know to restore the normal buffer. self._alternate_buffer = alternate_buffer handle_out = _win32.GetStdHandle(_win32.STD_OUTPUT_HANDLE) handle_in = _win32.GetStdHandle(_win32.STD_INPUT_HANDLE) original_out_mode = DWORD(0) original_in_mode = DWORD(0) # The standard handles are not consoles when the process runs detached or with redirected streams. # In that case there is no console mode to negotiate, same as a POSIX terminal which is not a tty. if _win32.GetConsoleMode(handle_out, byref(original_out_mode)) and _win32.GetConsoleMode( handle_in, byref(original_in_mode) ): dword_out_mode = DWORD( original_out_mode.value | _win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING | _win32.DISABLE_NEWLINE_AUTO_RETURN ) dword_in_mode = DWORD( original_in_mode.value | _win32.ENABLE_WINDOW_INPUT | _win32.ENABLE_VIRTUAL_TERMINAL_INPUT ) if not (ok := _win32.SetConsoleMode(handle_out, dword_out_mode)): raise RuntimeError(f"ConsoleMode set failed for output. Err: {ok!r}") if not (ok := _win32.SetConsoleMode(handle_in, dword_in_mode)): _win32.SetConsoleMode(handle_out, original_out_mode) raise RuntimeError(f"ConsoleMode set failed for input. Err: {ok!r}") self._dwOriginalOutMode = original_out_mode self._dwOriginalInMode = original_in_mode self._console_mode_active = True self._start_input_thread() self._detect_terminal_modes() if self.modes.bracketed_paste: self.write(escape.PrivateMode.BRACKETED_PASTE.enable_seq) if self.modes.focus_reporting: self.write(escape.PrivateMode.FOCUS_REPORTING.enable_seq) self._next_timeout = self.max_wait signals.emit_signal(self, INPUT_DESCRIPTORS_CHANGED) # restore mouse tracking to previous state self._mouse_tracking(self._mouse_tracking_enabled) super()._start() # type: ignore[safe-super] def __del__(self) -> None: if self._send_input is not None: # the socket pair was created here, so it is closed here self._send_input.close() self._term_input_file.close() super().__del__() def _stop(self) -> None: """ Restore the screen. :raises RuntimeError: the original console mode could not be restored. """ self.clear() if self.modes.bracketed_paste: self.write(escape.PrivateMode.BRACKETED_PASTE.disable_seq) if self.modes.focus_reporting: self.write(escape.PrivateMode.FOCUS_REPORTING.disable_seq) signals.emit_signal(self, INPUT_DESCRIPTORS_CHANGED) # After the signal: a main loop still connected to it re-hooks the screen, which starts a reader. self._stop_input_thread() self._stop_mouse_restore_buffer() self._stop_restore_palette() if self._dwOriginalOutMode is not None and self._dwOriginalInMode is not None: handle_out = _win32.GetStdHandle(_win32.STD_OUTPUT_HANDLE) handle_in = _win32.GetStdHandle(_win32.STD_INPUT_HANDLE) if not (ok := _win32.SetConsoleMode(handle_out, self._dwOriginalOutMode)): raise RuntimeError(f"ConsoleMode set failed for output. Err: {ok!r}") if not (ok := _win32.SetConsoleMode(handle_in, self._dwOriginalInMode)): raise RuntimeError(f"ConsoleMode set failed for input. Err: {ok!r}") self._dwOriginalOutMode = None self._dwOriginalInMode = None self._console_mode_active = False super()._stop() # type: ignore[safe-super] def unhook_event_loop(self, event_loop: EventLoop) -> None: """Remove any hooks added by hook_event_loop.""" for handle in self._current_event_loop_handles: event_loop.remove_watch_file(handle) if self._input_timeout: event_loop.remove_alarm(self._input_timeout) self._input_timeout = None def hook_event_loop( self, event_loop: EventLoop, callback: Callable[[_DecodedInput, list[int]], typing.Any], ) -> None: """Register the given callback with the event loop, to be called with new input whenever it's available. The callback should be passed a list of processed keys and a list of unprocessed keycodes. Subclasses may wish to use parse_input to wrap the callback. """ self._start_input_thread() if hasattr(self, "get_input_nonblocking"): wrapper = self._make_legacy_input_wrapper(event_loop, callback) else: @functools.wraps(callback) def wrapper() -> tuple[list[str], typing.Any] | None: return self.parse_input(event_loop, callback, self.get_available_raw_input()) fds = self.get_input_descriptors() handles = [event_loop.watch_file(fd if isinstance(fd, int) else fd.fileno(), wrapper) for fd in fds] self._current_event_loop_handles = handles _input_thread: ReadInputThread | None = None def _start_input_thread(self) -> None: """Start the background console reader, if it isn't already running.""" if self._input_thread is not None or self._send_input is None: return # Console events are only translated for the input socket owned by this screen. signum is # left at its default (_raw_display_base._SIGWINCH): this class runs on Windows only, # which has no real SIGWINCH to report here anyway. self._input_thread = ReadInputThread(self._send_input, self._sigwinch_handler) self._input_thread.start() def _stop_input_thread(self) -> None: """Stop the background console reader, so it reads no input meant for whatever uses the console next.""" if self._input_thread is None: return self._input_thread.should_exit = True with contextlib.suppress(RuntimeError): self._input_thread.join(5) self._input_thread = None def _read_raw_input(self, timeout: float) -> bytearray: ready = self._wait_for_input_ready(timeout) fd = self._input_fileno() chars = bytearray() if fd is None or fd not in ready: return chars with selectors.DefaultSelector() as selector: selector.register(fd, selectors.EVENT_READ) while selector.select(0): chunk = self._term_input_file.recv(1024) if not chunk: raise RuntimeError("stdin has been closed") chars.extend(chunk) return chars def get_cols_rows(self) -> tuple[int, int]: """Return the terminal dimensions (num columns, num rows).""" y, x = super().get_cols_rows() # The console buffer describes the standard output only: any other stream keeps the fallback size. if self._term_output_file is sys.stdout: handle = _win32.GetStdHandle(_win32.STD_OUTPUT_HANDLE) info = _win32.CONSOLE_SCREEN_BUFFER_INFO() if _win32.GetConsoleScreenBufferInfo(handle, byref(info)): # Fallback will be used in case of term size could not be determined y, x = info.dwSize.Y, info.dwSize.X self.maxrow = y return x, y class ReadInputThread(threading.Thread): """Background thread that reads console input events and forwards them over a socket.""" name = "urwid Windows input reader" daemon = True should_exit: bool = False def __init__( self, input_socket: socket.socket, resize: Callable[[], typing.Any], ) -> None: self._input = input_socket self._resize = resize self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__) super().__init__() def run(self) -> None: hIn = _win32.GetStdHandle(_win32.STD_INPUT_HANDLE) MAX = 2048 read = DWORD(0) arrtype = _win32.INPUT_RECORD * MAX input_records = arrtype() send_input = self._input.sendall resize = self._resize key_event = _win32.EventType.KEY_EVENT resize_event = _win32.EventType.WINDOW_BUFFER_SIZE_EVENT # A character outside the Basic Multilingual Plane arrives as two UTF-16 halves in two key events, # possibly in two reads: the first half waits here for the second one. high_surrogate = "" while not self.should_exit: # The timeout keeps should_exit checked while no input arrives. wait_result = _win32.WaitForSingleObject(hIn, _INPUT_POLL_MS) if wait_result == _win32.WAIT_TIMEOUT: continue if wait_result != _win32.WAIT_OBJECT_0 or not _win32.ReadConsoleInputW( hIn, input_records, MAX, byref(read) ): self.logger.warning("Reading console input failed, no further input is received") return chars = [high_surrogate] for inp in input_records[: read.value]: if inp.EventType == key_event: if inp.Event.KeyEvent.bKeyDown: chars.append(inp.Event.KeyEvent.uChar.UnicodeChar) elif inp.EventType == resize_event: resize() else: pass # TODO: handle mouse events # On Windows atomic press/release of modifier keys produce phantom input with code NULL. text = "".join(chars).replace("\x00", "") high_surrogate = text[-1:] if "\ud800" <= text[-1:] <= "\udbff" else "" if high_surrogate: text = text[:-1] if text: send_input(text.encode("utf-16-le", "surrogatepass").decode("utf-16-le", "replace").encode("utf-8")) def _test() -> None: import doctest doctest.testmod() if __name__ == "__main__": _test() urwid-urwid-69907e4/urwid/display/common.py000066400000000000000000001326161526073734300207510ustar00rootroot00000000000000# Urwid common display code # Copyright (C) 2004-2011 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """Colour and attribute handling and the base classes shared by all display modules.""" from __future__ import annotations import abc import functools import logging import os import sys import typing import warnings from urwid import signals from urwid.util import StoppingContext, int_scale if typing.TYPE_CHECKING: from collections.abc import Callable, Hashable, Iterable, Sequence from typing_extensions import Literal, Self from urwid import Canvas # One register_palette() item: # (name, like_other_name) # or the 3, 4 or 6 positional arguments of register_palette_entry(). # A name is any hashable value, None being the default attribute. # typing.Union rather than "|": the alias is evaluated by the checkers as Python 3.9 code. PaletteEntry = typing.Union[ tuple[Hashable, Hashable], tuple[Hashable, str, str], tuple[Hashable, str, str, typing.Union[str, tuple[str, ...], None]], tuple[ Hashable, str, str, typing.Union[str, tuple[str, ...], None], typing.Optional[str], typing.Optional[str], ], ] IS_WINDOWS = sys.platform == "win32" # for replacing unprintable bytes with '?' UNPRINTABLE_TRANS_TABLE = bytes.maketrans(bytes(range(32)), b"?" * 32) # for replacing decoded C1 control characters (U+0080-U+009F) with '?' UNPRINTABLE_C1_TRANS_TABLE = str.maketrans(dict.fromkeys(range(0x80, 0xA0), "?")) # signals sent by BaseScreen UPDATE_PALETTE_ENTRY = "update palette entry" INPUT_DESCRIPTORS_CHANGED = "input descriptors changed" # AttrSpec internal values _BASIC_START = 0 # first index of basic color aliases _CUBE_START = 16 # first index of color cube _CUBE_SIZE_256 = 6 # one side of the color cube _GRAY_SIZE_256 = 24 _GRAY_START_256 = _CUBE_SIZE_256**3 + _CUBE_START _CUBE_WHITE_256 = _GRAY_START_256 - 1 _CUBE_SIZE_88 = 4 _GRAY_SIZE_88 = 8 _GRAY_START_88 = _CUBE_SIZE_88**3 + _CUBE_START _CUBE_WHITE_88 = _GRAY_START_88 - 1 _CUBE_BLACK = _CUBE_START # values copied from xterm 256colres.h: _CUBE_STEPS_256 = [0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF] _GRAY_STEPS_256 = [ 0x08, 0x12, 0x1C, 0x26, 0x30, 0x3A, 0x44, 0x4E, 0x58, 0x62, 0x6C, 0x76, 0x80, 0x84, 0x94, 0x9E, 0xA8, 0xB2, 0xBC, 0xC6, 0xD0, 0xDA, 0xE4, 0xEE, ] # values copied from xterm 88colres.h: _CUBE_STEPS_88 = [0x00, 0x8B, 0xCD, 0xFF] _GRAY_STEPS_88 = [0x2E, 0x5C, 0x73, 0x8B, 0xA2, 0xB9, 0xD0, 0xE7] # values copied from X11/rgb.txt and XTerm-col.ad: _BASIC_COLOR_VALUES = ( (0, 0, 0), (205, 0, 0), (0, 205, 0), (205, 205, 0), (0, 0, 238), (205, 0, 205), (0, 205, 205), (229, 229, 229), (127, 127, 127), (255, 0, 0), (0, 255, 0), (255, 255, 0), (0x5C, 0x5C, 0xFF), (255, 0, 255), (0, 255, 255), (255, 255, 255), ) _COLOR_VALUES_256: tuple[tuple[int, int, int], ...] = ( *_BASIC_COLOR_VALUES, *[(r, g, b) for r in _CUBE_STEPS_256 for g in _CUBE_STEPS_256 for b in _CUBE_STEPS_256], *[(gr, gr, gr) for gr in _GRAY_STEPS_256], ) _COLOR_VALUES_88: tuple[tuple[int, int, int], ...] = ( *_BASIC_COLOR_VALUES, *[(r, g, b) for r in _CUBE_STEPS_88 for g in _CUBE_STEPS_88 for b in _CUBE_STEPS_88], *[(gr, gr, gr) for gr in _GRAY_STEPS_88], ) if len(_COLOR_VALUES_256) != 256: raise RuntimeError(_COLOR_VALUES_256) if len(_COLOR_VALUES_88) != 88: raise RuntimeError(_COLOR_VALUES_88) # fmt: off _FG_COLOR_MASK = 0x000000ffffff _BG_COLOR_MASK = 0xffffff000000 _FG_BASIC_COLOR = 0x1000000000000 _FG_HIGH_COLOR = 0x2000000000000 _FG_TRUE_COLOR = 0x4000000000000 _BG_BASIC_COLOR = 0x8000000000000 _BG_HIGH_COLOR = 0x10000000000000 _BG_TRUE_COLOR = 0x20000000000000 _BG_SHIFT = 24 _HIGH_88_COLOR = 0x40000000000000 _HIGH_TRUE_COLOR = 0x80000000000000 _STANDOUT = 0x100000000000000 _UNDERLINE = 0x200000000000000 _BOLD = 0x400000000000000 _BLINK = 0x800000000000000 _ITALICS = 0x1000000000000000 _STRIKETHROUGH = 0x2000000000000000 _FAINT = 0x4000000000000000 # fmt: on _FG_MASK = ( _FG_COLOR_MASK | _FG_BASIC_COLOR | _FG_HIGH_COLOR | _STANDOUT | _UNDERLINE | _BLINK | _BOLD | _ITALICS | _STRIKETHROUGH | _FAINT ) _BG_MASK = _BG_COLOR_MASK | _BG_BASIC_COLOR | _BG_HIGH_COLOR DEFAULT = "default" BLACK = "black" DARK_RED = "dark red" DARK_GREEN = "dark green" BROWN = "brown" DARK_BLUE = "dark blue" DARK_MAGENTA = "dark magenta" DARK_CYAN = "dark cyan" LIGHT_GRAY = "light gray" DARK_GRAY = "dark gray" LIGHT_RED = "light red" LIGHT_GREEN = "light green" YELLOW = "yellow" LIGHT_BLUE = "light blue" LIGHT_MAGENTA = "light magenta" LIGHT_CYAN = "light cyan" WHITE = "white" _BASIC_COLORS = [ BLACK, DARK_RED, DARK_GREEN, BROWN, DARK_BLUE, DARK_MAGENTA, DARK_CYAN, LIGHT_GRAY, DARK_GRAY, LIGHT_RED, LIGHT_GREEN, YELLOW, LIGHT_BLUE, LIGHT_MAGENTA, LIGHT_CYAN, WHITE, ] # name -> index, for the O(1) lookup AttrSpec needs on every color it parses # (_BASIC_COLORS itself is kept for the reverse, index -> name, direction) _BASIC_COLOR_INDEX = {color: index for index, color in enumerate(_BASIC_COLORS)} _ATTRIBUTES = { "bold": _BOLD, "italics": _ITALICS, "underline": _UNDERLINE, "blink": _BLINK, "standout": _STANDOUT, "strikethrough": _STRIKETHROUGH, "faint": _FAINT, } # AttrSpec.__init__() and __set_foreground()/__set_background() are called # once per colored run of text an application constructs, so these are # hoisted to module scope rather than re-built as set literals on every call. _VALID_COLOR_COUNTS = frozenset({1, 16, 88, 256, 2**24}) _DEFAULT_COLOR_NAMES = frozenset({"", "default"}) def _value_lookup_table(values: Sequence[int], size: int) -> list[int]: """Generate a lookup table for finding the closest item in *values*. Lookup returns (index into values)+1. :param values: list of values in ascending order, all < size :param size: size of lookup table and maximum value >>> _value_lookup_table([0, 7, 9], 10) [0, 0, 0, 0, 1, 1, 1, 1, 2, 2] """ middle_values = [0] + [(values[i] + values[i + 1] + 1) // 2 for i in range(len(values) - 1)] + [size] lookup_table = [] for i in range(len(middle_values) - 1): count = middle_values[i + 1] - middle_values[i] lookup_table.extend([i] * count) return lookup_table _CUBE_256_LOOKUP = _value_lookup_table(_CUBE_STEPS_256, 256) _GRAY_256_LOOKUP = _value_lookup_table([0, *_GRAY_STEPS_256, 255], 256) _CUBE_88_LOOKUP = _value_lookup_table(_CUBE_STEPS_88, 256) _GRAY_88_LOOKUP = _value_lookup_table([0, *_GRAY_STEPS_88, 255], 256) # convert steps to values that will be used by string versions of the colors # 1 hex digit for rgb and 0..100 for grayscale _CUBE_STEPS_256_16 = [int_scale(n, 0x100, 0x10) for n in _CUBE_STEPS_256] _GRAY_STEPS_256_101 = [int_scale(n, 0x100, 101) for n in _GRAY_STEPS_256] _CUBE_STEPS_88_16 = [int_scale(n, 0x100, 0x10) for n in _CUBE_STEPS_88] _GRAY_STEPS_88_101 = [int_scale(n, 0x100, 101) for n in _GRAY_STEPS_88] # create lookup tables for 1 hex digit rgb and 0..100 for grayscale values _CUBE_256_LOOKUP_16 = [_CUBE_256_LOOKUP[int_scale(n, 16, 0x100)] for n in range(16)] _GRAY_256_LOOKUP_101 = [_GRAY_256_LOOKUP[int_scale(n, 101, 0x100)] for n in range(101)] _CUBE_88_LOOKUP_16 = [_CUBE_88_LOOKUP[int_scale(n, 16, 0x100)] for n in range(16)] _GRAY_88_LOOKUP_101 = [_GRAY_88_LOOKUP[int_scale(n, 101, 0x100)] for n in range(101)] # The functions _gray_num_256() and _gray_num_88() do not include the gray # values from the color cube so that the gray steps are an even width. # The color cube grays are available by using the rgb functions. Pure # white and black are taken from the color cube, since the gray range does # not include them, and the basic colors are more likely to have been # customized by an end-user. def _gray_num_256(gnum: int) -> int: """Return ths color number for gray number gnum. Color cube black and white are returned for 0 and 25 respectively since those values aren't included in the gray scale. """ # grays start from index 1 gnum -= 1 if gnum < 0: return _CUBE_BLACK if gnum >= _GRAY_SIZE_256: return _CUBE_WHITE_256 return _GRAY_START_256 + gnum def _gray_num_88(gnum: int) -> int: """Return ths color number for gray number gnum. Color cube black and white are returned for 0 and 9 respectively since those values aren't included in the gray scale. """ # gnums start from index 1 gnum -= 1 if gnum < 0: return _CUBE_BLACK if gnum >= _GRAY_SIZE_88: return _CUBE_WHITE_88 return _GRAY_START_88 + gnum def _color_desc_true(num: int) -> str: return f"#{num:06x}" @functools.lru_cache(maxsize=256) def _color_desc_256(num: int) -> str: """Return a string description of color number *num*. 0..15 -> 'h0'..'h15' basic colors (as high-colors) 16..231 -> '#000'..'#fff' color cube colors 232..255 -> 'g3'..'g93' grays >>> _color_desc_256(15) 'h15' >>> _color_desc_256(16) '#000' >>> _color_desc_256(17) '#006' >>> _color_desc_256(230) '#ffd' >>> _color_desc_256(233) 'g7' >>> _color_desc_256(234) 'g11' :raises ValueError: *num* is outside the 256-colour range. """ if not 0 <= num < 256: raise ValueError(num) if num < _CUBE_START: return f"h{num:d}" if num < _GRAY_START_256: num -= _CUBE_START b, num = num % _CUBE_SIZE_256, num // _CUBE_SIZE_256 g, num = num % _CUBE_SIZE_256, num // _CUBE_SIZE_256 r = num % _CUBE_SIZE_256 return f"#{_CUBE_STEPS_256_16[r]:x}{_CUBE_STEPS_256_16[g]:x}{_CUBE_STEPS_256_16[b]:x}" return f"g{_GRAY_STEPS_256_101[num - _GRAY_START_256]:d}" @functools.lru_cache(maxsize=88) def _color_desc_88(num: int) -> str: """Return a string description of color number *num*. 0..15 -> 'h0'..'h15' basic colors (as high-colors) 16..79 -> '#000'..'#fff' color cube colors 80..87 -> 'g18'..'g90' grays >>> _color_desc_88(15) 'h15' >>> _color_desc_88(16) '#000' >>> _color_desc_88(17) '#008' >>> _color_desc_88(78) '#ffc' >>> _color_desc_88(81) 'g36' >>> _color_desc_88(82) 'g45' :raises ValueError: *num* is outside the 88-colour range. """ if not 0 < num < 88: raise ValueError(num) if num < _CUBE_START: return f"h{num:d}" if num < _GRAY_START_88: num -= _CUBE_START b, num = num % _CUBE_SIZE_88, num // _CUBE_SIZE_88 g, r = num % _CUBE_SIZE_88, num // _CUBE_SIZE_88 return f"#{_CUBE_STEPS_88_16[r]:x}{_CUBE_STEPS_88_16[g]:x}{_CUBE_STEPS_88_16[b]:x}" return f"g{_GRAY_STEPS_88_101[num - _GRAY_START_88]:d}" @functools.lru_cache(maxsize=512) def _parse_color_true(desc: str) -> int | None: if (c := _parse_color_256(desc)) is not None: (r, g, b) = _COLOR_VALUES_256[c] return (r << 16) + (g << 8) + b if not desc.startswith("#"): return None if len(desc) == 7: h = desc[1:] return int(h, 16) if len(desc) == 4: h = f"0x{desc[1]}0{desc[2]}0{desc[3]}" return int(h, 16) return None @functools.lru_cache(maxsize=512) def _parse_color_256(desc: str) -> int | None: """Return a color number for the description *desc*. 'h0'..'h255' -> 0..255 actual color number '#000'..'#fff' -> 16..231 color cube colors 'g0'..'g100' -> 16, 232..255, 231 grays and color cube black/white 'g#00'..'g#ff' -> 16, 232...255, 231 gray and color cube black/white Returns None if desc is invalid. >>> _parse_color_256("h142") 142 >>> _parse_color_256("#f00") 196 >>> _parse_color_256("g100") 231 >>> _parse_color_256("g#80") 244 """ if len(desc) > 4: # keep the length within reason before parsing return None try: if desc.startswith("h"): # high-color number num = int(desc[1:], 10) if num < 0 or num > 255: return None return num if desc.startswith("#") and len(desc) == 4: # color-cube coordinates if (rgb := int(desc[1:], 16)) >= 0: b, rgb = rgb % 16, rgb // 16 g, r = rgb % 16, rgb // 16 # find the closest rgb values r = _CUBE_256_LOOKUP_16[r] g = _CUBE_256_LOOKUP_16[g] b = _CUBE_256_LOOKUP_16[b] return _CUBE_START + (r * _CUBE_SIZE_256 + g) * _CUBE_SIZE_256 + b return None # Only remaining possibility is gray value if desc.startswith("g#"): # hex value 00..ff gray = int(desc[2:], 16) # noqa: FURB166 if gray < 0 or gray > 255: return None gray = _GRAY_256_LOOKUP[gray] elif desc.startswith("g"): # decimal value 0..100 gray = int(desc[1:], 10) if gray < 0 or gray > 100: return None gray = _GRAY_256_LOOKUP_101[gray] else: return None if gray == 0: return _CUBE_BLACK gray -= 1 if gray == _GRAY_SIZE_256: return _CUBE_WHITE_256 return _GRAY_START_256 + gray except ValueError: return None @functools.lru_cache(maxsize=512) def _true_to_256(desc: str) -> str | None: if not (desc.startswith("#") and len(desc) == 7): return None c256 = _parse_color_256("#" + "".join(format(int(x, 16) // 16, "x") for x in (desc[1:3], desc[3:5], desc[5:7]))) return _color_desc_256(typing.cast("int", c256)) # we control input @functools.lru_cache(maxsize=512) def _parse_color_88(desc: str) -> int | None: """Return a color number for the description *desc*. 'h0'..'h87' -> 0..87 actual color number '#000'..'#fff' -> 16..79 color cube colors 'g0'..'g100' -> 16, 80..87, 79 grays and color cube black/white 'g#00'..'g#ff' -> 16, 80...87, 79 gray and color cube black/white Returns None if desc is invalid. >>> _parse_color_88("h142") >>> _parse_color_88("h42") 42 >>> _parse_color_88("#f00") 64 >>> _parse_color_88("g100") 79 >>> _parse_color_88("g#80") 83 """ if len(desc) == 7: desc = desc[0:2] + desc[3] + desc[5] if len(desc) > 4: # keep the length within reason before parsing return None try: if desc.startswith("h"): # high-color number num = int(desc[1:], 10) if num < 0 or num > 87: return None return num if desc.startswith("#") and len(desc) == 4: # color-cube coordinates if (rgb := int(desc[1:], 16)) >= 0: b, rgb = rgb % 16, rgb // 16 g, r = rgb % 16, rgb // 16 # find the closest rgb values r = _CUBE_88_LOOKUP_16[r] g = _CUBE_88_LOOKUP_16[g] b = _CUBE_88_LOOKUP_16[b] return _CUBE_START + (r * _CUBE_SIZE_88 + g) * _CUBE_SIZE_88 + b return None # Only remaining possibility is gray value if desc.startswith("g#"): # hex value 00..ff gray = int(desc[2:], 16) # noqa: FURB166 if gray < 0 or gray > 255: return None gray = _GRAY_88_LOOKUP[gray] elif desc.startswith("g"): # decimal value 0..100 gray = int(desc[1:], 10) if gray < 0 or gray > 100: return None gray = _GRAY_88_LOOKUP_101[gray] else: return None if gray == 0: return _CUBE_BLACK gray -= 1 if gray == _GRAY_SIZE_88: return _CUBE_WHITE_88 return _GRAY_START_88 + gray except ValueError: return None class AttrSpecError(Exception): """Raised when an :class:`AttrSpec` is constructed with an invalid color specification.""" class AttrSpec: """Text attribute specification, encoding foreground and background colors and display settings.""" __slots__ = ("__hash_value", "__value") def __init__( self, fg: str, bg: str, colors: Literal[1, 16, 88, 256, 16777216] = 256, ) -> None: """ :param fg: a string containing a comma-separated foreground color and settings Color values: 'default' (use the terminal's default foreground), 'black', 'dark red', 'dark green', 'brown', 'dark blue', 'dark magenta', 'dark cyan', 'light gray', 'dark gray', 'light red', 'light green', 'yellow', 'light blue', 'light magenta', 'light cyan', 'white' High-color example values: '#009' (0% red, 0% green, 60% red, like HTML colors) '#23facc' (RRGGBB hex color code) '#fcc' (100% red, 80% green, 80% blue) 'g40' (40% gray, decimal), 'g#cc' (80% gray, hex), '#000', 'g0', 'g#00' (black), '#fff', 'g100', 'g#ff' (white) 'h8' (color number 8), 'h255' (color number 255) Setting: 'bold', 'italics', 'underline', 'blink', 'standout', 'strikethrough', 'faint' Some terminals use 'bold' for bright colors. Most terminals ignore the 'blink' setting. If the color is not given then 'default' will be assumed. :param bg: a string containing the background color Color values: 'default' (use the terminal's default background), 'black', 'dark red', 'dark green', 'brown', 'dark blue', 'dark magenta', 'dark cyan', 'light gray' High-color exaples: see fg examples above An empty string will be treated the same as 'default'. :param colors: the maximum colors available for the specification :raises AttrSpecError: *colors* is not a supported palette size, or *fg*/*bg* need more colours than *colors*. Valid values include: 1, 16, 88, 256, and 2**24. High-color values are only usable with 88, 256, or 2**24 colors. With 1 color only the foreground settings may be used. >>> AttrSpec("dark red", "light gray", 16) AttrSpec('dark red', 'light gray') >>> AttrSpec("yellow, underline, bold", "dark blue") AttrSpec('yellow,bold,underline', 'dark blue') >>> AttrSpec("#ddb", "#004", 256) # closest colors will be found AttrSpec('#dda', '#006') >>> AttrSpec("#ddb", "#004", 88) AttrSpec('#ccc', '#000', colors=88) """ if colors not in _VALID_COLOR_COUNTS: raise AttrSpecError(f"invalid number of colors ({colors:d}).") self.__value = 0 | _HIGH_88_COLOR * (colors == 88) | _HIGH_TRUE_COLOR * (colors == 2**24) self.__set_foreground(fg) self.__set_background(bg) if self.colors > colors: raise AttrSpecError( f"foreground/background ({fg!r}/{bg!r}) require more colors than have been specified ({colors:d})." ) self.__hash_value = hash((self.__class__, self.__value)) def copy_modified( self, fg: str | None = None, bg: str | None = None, colors: Literal[1, 16, 88, 256, 16777216] | None = None, ) -> Self: """Return a new :class:`AttrSpec`, replacing *fg*, *bg*, and/or *colors* with the given values.""" if fg is None: foreground = self.foreground else: foreground = fg if bg is None: background = self.background else: background = bg if colors is None: new_colors = self.colors else: new_colors = colors return self.__class__(foreground, background, new_colors) def __hash__(self) -> int: """Instance is immutable and hashable.""" return self.__hash_value @property def _value(self) -> int: """Read-only value access.""" return self.__value @property def foreground_basic(self) -> bool: """Return whether the foreground color is one of the 16 basic colors.""" return self.__value & _FG_BASIC_COLOR != 0 @property def foreground_high(self) -> bool: """Return whether the foreground color is a 88- or 256-color palette entry.""" return self.__value & _FG_HIGH_COLOR != 0 @property def foreground_true(self) -> bool: """Return whether the foreground color is a 24-bit true color.""" return self.__value & _FG_TRUE_COLOR != 0 @property def foreground_number(self) -> int: """Return the encoded foreground color number.""" return self.__value & _FG_COLOR_MASK @property def background_basic(self) -> bool: """Return whether the background color is one of the 16 basic colors.""" return self.__value & _BG_BASIC_COLOR != 0 @property def background_high(self) -> bool: """Return whether the background color is a 88- or 256-color palette entry.""" return self.__value & _BG_HIGH_COLOR != 0 @property def background_true(self) -> bool: """Return whether the background color is a 24-bit true color.""" return self.__value & _BG_TRUE_COLOR != 0 @property def background_number(self) -> int: """Return the encoded background color number.""" return (self.__value & _BG_COLOR_MASK) >> _BG_SHIFT @property def italics(self) -> bool: """Return whether the italics setting is enabled.""" return self.__value & _ITALICS != 0 @property def bold(self) -> bool: """Return whether the bold setting is enabled.""" return self.__value & _BOLD != 0 @property def underline(self) -> bool: """Return whether the underline setting is enabled.""" return self.__value & _UNDERLINE != 0 @property def blink(self) -> bool: """Return whether the blink setting is enabled.""" return self.__value & _BLINK != 0 @property def standout(self) -> bool: """Return whether the standout setting is enabled.""" return self.__value & _STANDOUT != 0 @property def strikethrough(self) -> bool: """Return whether the strikethrough setting is enabled.""" return self.__value & _STRIKETHROUGH != 0 @property def faint(self) -> bool: """Return whether the faint setting is enabled.""" return self.__value & _FAINT != 0 @property def colors(self) -> Literal[1, 16, 88, 256, 16777216]: """ Return the maximum colors required for this object. Returns 16777216, 256, 88, 16 or 1. """ if self.__value & _HIGH_88_COLOR: return 88 if self.__value & (_BG_HIGH_COLOR | _FG_HIGH_COLOR): return 256 if self.__value & (_BG_TRUE_COLOR | _FG_TRUE_COLOR): return 16777216 # we have to return explicit 2**24 instead of inline calculation if self.__value & (_BG_BASIC_COLOR | _FG_BASIC_COLOR): return 16 return 1 def __repr__(self) -> str: """Return an executable python representation of the AttrSpec object.""" args = f"{self.foreground!r}, {self.background!r}" if self.colors == 88: # 88-color mode is the only one that is handled differently args = f"{args}, colors=88" return f"{self.__class__.__name__}({args})" def _foreground_color(self) -> str: """Return only the color component of the foreground.""" if not (self.foreground_basic or self.foreground_high or self.foreground_true): return "default" if self.foreground_basic: return _BASIC_COLORS[self.foreground_number] if self.colors == 88: return _color_desc_88(self.foreground_number) if self.colors == 2**24: return _color_desc_true(self.foreground_number) return _color_desc_256(self.foreground_number) @property def foreground(self) -> str: """Return the foreground color and settings as a comma-separated string.""" return ( self._foreground_color() + ",bold" * self.bold + ",italics" * self.italics + ",standout" * self.standout + ",blink" * self.blink + ",underline" * self.underline + ",strikethrough" * self.strikethrough + ",faint" * self.faint ) def __set_foreground(self, foreground: str) -> None: """ Parse and store the foreground colour and settings. :raises AttrSpecError: *foreground* names an unrecognised colour, repeats a setting, or gives more than one colour. """ color = None scolor: int | None flags = 0 # handle comma-separated foreground for part in foreground.split(","): part = part.strip() # noqa: PLW2901 if (attribute_flag := _ATTRIBUTES.get(part)) is not None: # parse and store "settings"/attributes in flags if flags & attribute_flag: raise AttrSpecError(f"Setting {part!r} specified more than once in foreground ({foreground!r})") flags |= attribute_flag continue # past this point we must be specifying a color if part in _DEFAULT_COLOR_NAMES: scolor = 0 elif (basic_color := _BASIC_COLOR_INDEX.get(part)) is not None: scolor = basic_color flags |= _FG_BASIC_COLOR elif self.__value & _HIGH_88_COLOR: scolor = _parse_color_88(part) flags |= _FG_HIGH_COLOR elif self.__value & _HIGH_TRUE_COLOR: scolor = _parse_color_true(part) flags |= _FG_TRUE_COLOR else: scolor = _parse_color_256(_true_to_256(part) or part) flags |= _FG_HIGH_COLOR # _parse_color_*() return None for unrecognized colors if scolor is None: raise AttrSpecError(f"Unrecognised color specification {part!r} in foreground ({foreground!r})") if color is not None: raise AttrSpecError(f"More than one color given for foreground ({foreground!r})") color = scolor if color is None: color = 0 self.__value = (self.__value & ~_FG_MASK) | color | flags @property def background(self) -> str: """Return the background color.""" if not (self.background_basic or self.background_high or self.background_true): return "default" if self.background_basic: return _BASIC_COLORS[self.background_number] if self.__value & _HIGH_88_COLOR: return _color_desc_88(self.background_number) if self.colors == 2**24: return _color_desc_true(self.background_number) return _color_desc_256(self.background_number) def __set_background(self, background: str) -> None: """ Parse and store the background colour. :raises AttrSpecError: *background* names an unrecognised colour. """ flags = 0 color: int | None if background in _DEFAULT_COLOR_NAMES: color = 0 elif (basic_color := _BASIC_COLOR_INDEX.get(background)) is not None: color = basic_color flags |= _BG_BASIC_COLOR elif self.__value & _HIGH_88_COLOR: color = _parse_color_88(background) flags |= _BG_HIGH_COLOR elif self.__value & _HIGH_TRUE_COLOR: color = _parse_color_true(background) flags |= _BG_TRUE_COLOR else: color = _parse_color_256(_true_to_256(background) or background) flags |= _BG_HIGH_COLOR if color is None: raise AttrSpecError(f"Unrecognised color specification in background ({background!r})") self.__value = (self.__value & ~_BG_MASK) | (color << _BG_SHIFT) | flags def get_rgb_values(self) -> tuple[int | None, int | None, int | None, int | None, int | None, int | None]: """Return ``(fg_red, fg_green, fg_blue, bg_red, bg_green, bg_blue)`` color components. Each component is in the range 0-255. Values are taken from the XTerm defaults and may not exactly match the user's terminal. If the foreground or background is 'default' then all their compenents will be returned as None. >>> AttrSpec("yellow", "#ccf", colors=88).get_rgb_values() (255, 255, 0, 205, 205, 255) >>> AttrSpec("default", "g92").get_rgb_values() (None, None, None, 238, 238, 238) :raises ValueError: this AttrSpec holds a colour number outside the 88-colour range. """ vals: tuple[int | None, int | None, int | None] if not (self.foreground_basic or self.foreground_high or self.foreground_true): vals = (None, None, None) elif self.colors == 88: if self.foreground_number >= 88: raise ValueError(f"Invalid AttrSpec _value: {self.foreground_number!r}") vals = _COLOR_VALUES_88[self.foreground_number] elif self.colors == 2**24: h = f"{self.foreground_number:06x}" vals = typing.cast( "tuple[int, int, int]", tuple(int(x, 16) for x in (h[0:2], h[2:4], h[4:6])), ) else: vals = _COLOR_VALUES_256[self.foreground_number] if not (self.background_basic or self.background_high or self.background_true): return (*vals, None, None, None) if self.colors == 88: if self.background_number >= 88: raise ValueError(f"Invalid AttrSpec _value: {self.background_number!r}") return (*vals, *_COLOR_VALUES_88[self.background_number]) if self.colors == 2**24: h = f"{self.background_number:06x}" return (*vals, *(int(x, 16) for x in (h[0:2], h[2:4], h[4:6]))) return (*vals, *_COLOR_VALUES_256[self.background_number]) def __eq__(self, other: object) -> bool: """Return whether `other` is an :class:`AttrSpec` with the same underlying value.""" return isinstance(other, AttrSpec) and self.__value == other._value def __ne__(self, other: object) -> bool: """Return whether `other` is not equal to this :class:`AttrSpec`.""" return not self == other #: default RGB values substituted for a palette entry's 'default' foreground/background, #: matching the black-on-light-gray terminal default used by the HTML-producing screens _default_aspec = AttrSpec("black", "light gray") _default_fg_rgb = _default_aspec.get_rgb_values()[:3] _default_bg_rgb = _default_aspec.get_rgb_values()[3:] @functools.cache def attr_spec_to_css(aspec: AttrSpec) -> tuple[str, str, str]: """Return the (foreground, background, extra CSS declarations) hex colors for *aspec*. Covers every :class:`AttrSpec` display setting a browser can render: color (falling back to the default black-on-light-gray for an unset foreground/background), ``standout`` (swaps foreground and background), ``underline``/``strikethrough`` (combined into one ``text-decoration``), ``bold``, ``italics``, ``blink`` (a step-start CSS animation, since the ``text-decoration:blink`` value browsers used to support has been dropped) and ``faint``. """ fg_r, fg_g, fg_b, bg_r, bg_g, bg_b = aspec.get_rgb_values() if fg_r is None: fg_r, fg_g, fg_b = _default_fg_rgb if bg_r is None: bg_r, bg_g, bg_b = _default_bg_rgb fg = f"#{fg_r:02x}{fg_g:02x}{fg_b:02x}" bg = f"#{bg_r:02x}{bg_g:02x}{bg_b:02x}" if aspec.standout: fg, bg = bg, fg decoration = [name for name, on in (("underline", aspec.underline), ("line-through", aspec.strikethrough)) if on] extra = "" if decoration: extra += f";text-decoration:{' '.join(decoration)}" if aspec.bold: extra += ";font-weight:bold" if aspec.italics: extra += ";font-style:italic" if aspec.blink: extra += ";animation:urwid-blink 1s step-start infinite" if aspec.faint: extra += ";opacity:0.5" return fg, bg, extra class RealTerminal: """Mixin providing access to the real terminal's signal key settings.""" def __init__(self) -> None: """Initialize with no signal keys saved yet.""" super().__init__() self._signal_keys_set = False self._old_signal_keys: tuple[int, int, int, int, int] | None = None if IS_WINDOWS: def tty_signal_keys( self, intr: Literal["undefined"] | int | None = None, quit: Literal["undefined"] | int | None = None, # noqa: A002 # pylint: disable=redefined-builtin start: Literal["undefined"] | int | None = None, stop: Literal["undefined"] | int | None = None, susp: Literal["undefined"] | int | None = None, fileno: int | None = None, ) -> tuple[int, int, int, int, int] | None: """Read and/or set the tty's signal character settings. This function returns the current settings as a tuple. Use the string 'undefined' to unmap keys from their signals. The value None is used when no change is being made. Setting signal keys is done using the integer ascii code for the key, eg. 3 for CTRL+C. If this function is called after start() has been called then the original settings will be restored when stop() is called. """ return None else: def tty_signal_keys( self, intr: Literal["undefined"] | int | None = None, quit: Literal["undefined"] | int | None = None, # noqa: A002 # pylint: disable=redefined-builtin start: Literal["undefined"] | int | None = None, stop: Literal["undefined"] | int | None = None, susp: Literal["undefined"] | int | None = None, fileno: int | None = None, ) -> tuple[int, int, int, int, int] | None: """Read and/or set the tty's signal character settings. This function returns the current settings as a tuple. Use the string 'undefined' to unmap keys from their signals. The value None is used when no change is being made. Setting signal keys is done using the integer ascii code for the key, eg. 3 for CTRL+C. If this function is called after start() has been called then the original settings will be restored when stop() is called. """ import termios if fileno is None: fileno = sys.stdin.fileno() if not os.isatty(fileno): return None tattr = termios.tcgetattr(fileno) sattr = tattr[6] skeys = ( sattr[termios.VINTR], sattr[termios.VQUIT], sattr[termios.VSTART], sattr[termios.VSTOP], sattr[termios.VSUSP], ) if intr == "undefined": intr = 0 if quit == "undefined": quit = 0 # noqa: A001 if start == "undefined": start = 0 if stop == "undefined": stop = 0 if susp == "undefined": susp = 0 if intr is not None: tattr[6][termios.VINTR] = intr if quit is not None: tattr[6][termios.VQUIT] = quit if start is not None: tattr[6][termios.VSTART] = start if stop is not None: tattr[6][termios.VSTOP] = stop if susp is not None: tattr[6][termios.VSUSP] = susp if any(item is not None for item in (intr, quit, start, stop, susp)): termios.tcsetattr(fileno, termios.TCSADRAIN, tattr) self._signal_keys_set = True return skeys class ScreenError(Exception): """Raised for errors in :class:`BaseScreen` and its subclasses.""" class BaseScreen(abc.ABC, metaclass=signals.MetaSignals): """Base class for Screen classes (raw_display.Screen, ..., etc.).""" signals: typing.ClassVar[list[str]] = [UPDATE_PALETTE_ENTRY, INPUT_DESCRIPTORS_CHANGED] def __init__(self) -> None: """Initialize an unstarted screen with an empty palette.""" super().__init__() self.logger = logging.getLogger(f"{self.__class__.__module__}.{self.__class__.__name__}") self._palette: dict[Hashable, tuple[AttrSpec, AttrSpec, AttrSpec, AttrSpec, AttrSpec]] = {} self._started: bool = False @property def started(self) -> bool: """Return whether the screen has been started and not yet stopped.""" return self._started def start(self, *args: typing.Any, **kwargs: typing.Any) -> StoppingContext: """Set up the screen. If the screen has already been started, does nothing. May be used as a context manager, in which case :meth:`stop` will automatically be called at the end of the block: with screen.start(): ... You shouldn't override this method in a subclass; instead, override :meth:`_start`. """ if not self._started: self._started = True self._start(*args, **kwargs) return StoppingContext(self) def _start(self, *args: typing.Any, **kwargs: typing.Any) -> None: """Perform the actual setup of the screen. Subclasses should override this method.""" def stop(self) -> None: """Tear down the screen if it has been started. You shouldn't override this method; override :meth:`_stop`.""" if self._started: self._stop() self._started = False def _stop(self) -> None: """Perform actual teardown of the screen. Subclasses should override this method.""" def run_wrapper( self, fn: Callable[[], typing.Any], *args: typing.Any, **kwargs: typing.Any, ) -> None: """Start the screen, call a function, then stop the screen. Extra arguments are passed to `start`. .. deprecated:: 1.3.0 Call `start` as a context manager instead. This API will be removed in version 5.0. """ warnings.warn( "run_wrapper is deprecated in favor of calling `start` as a context manager. " "API will be removed in version 5.0.", DeprecationWarning, stacklevel=3, ) with self.start(*args, **kwargs): fn() def set_mouse_tracking(self, enable: bool = True) -> None: """Enable or disable tracking of mouse movement events. A no-op here; subclasses override it.""" @abc.abstractmethod def draw_screen(self, size: tuple[int, int], canvas: Canvas) -> None: """Draw *canvas* of the given *size* to the screen. Subclasses must implement this method.""" def clear(self) -> None: """Clear the screen if possible. Force the screen to be completely repainted on the next call to draw_screen(). """ def get_cols_rows(self) -> tuple[int, int]: """Return the terminal dimensions (num columns, num rows). Default (fallback) is 80x24. """ return 80, 24 def register_palette(self, palette: Iterable[PaletteEntry]) -> None: """Register a set of palette entries. :param palette: a list of (name, like_other_name) or (name, foreground, background, mono, foreground_high, background_high) tuples :raises ScreenError: an entry is neither a 2-, 3-, 4- nor 6-tuple, or copies a name that is not registered yet. The (name, like_other_name) format will copy the settings from the palette entry like_other_name, which must appear before this tuple in the list. The mono and foreground/background_high values are optional ie. the second tuple format may have 3, 4 or 6 values. See register_palette_entry() for a description of the tuple values. """ for item in palette: if len(item) in {3, 4, 6}: self.register_palette_entry(*item) continue if len(item) != 2: raise ScreenError(f"Invalid register_palette entry: {item!r}") name, like_name = item if like_name not in self._palette: raise ScreenError(f"palette entry '{like_name}' doesn't exist") self._palette[name] = self._palette[like_name] def register_palette_entry( self, name: Hashable, foreground: str, background: str, mono: str | tuple[str, ...] | None = None, foreground_high: str | None = None, background_high: str | None = None, ) -> None: """Register a single palette entry. :param name: new entry/attribute name, any hashable value; ``None`` names the default attribute :param foreground: a string containing a comma-separated foreground color and settings Color values: 'default' (use the terminal's default foreground), 'black', 'dark red', 'dark green', 'brown', 'dark blue', 'dark magenta', 'dark cyan', 'light gray', 'dark gray', 'light red', 'light green', 'yellow', 'light blue', 'light magenta', 'light cyan', 'white' Settings: 'bold', 'underline', 'blink', 'standout', 'strikethrough' Some terminals use 'bold' for bright colors. Most terminals ignore the 'blink' setting. If the color is not given then 'default' will be assumed. :param background: a string containing the background color Background color values: 'default' (use the terminal's default background), 'black', 'dark red', 'dark green', 'brown', 'dark blue', 'dark magenta', 'dark cyan', 'light gray' :param mono: a comma-separated string containing monochrome terminal settings (see "Settings" above.) None = no terminal settings (same as 'default') :param foreground_high: a string containing a comma-separated foreground color and settings, standard foreground colors (see "Color values" above) or high-colors may be used High-color example values: '#009' (0% red, 0% green, 60% red, like HTML colors) '#fcc' (100% red, 80% green, 80% blue) 'g40' (40% gray, decimal), 'g#cc' (80% gray, hex), '#000', 'g0', 'g#00' (black), '#fff', 'g100', 'g#ff' (white) 'h8' (color number 8), 'h255' (color number 255) None = use foreground parameter value :param background_high: a string containing the background color, standard background colors (see "Background colors" above) or high-colors (see "High-color example values" above) may be used None = use background parameter value """ basic = AttrSpec(foreground, background, 16) if isinstance(mono, tuple): # old style of specifying mono attributes was to put them in a tuple. convert to comma-separated string mono = ",".join(mono) if mono is None: mono = DEFAULT mono_spec = AttrSpec(mono, DEFAULT, 1) if foreground_high is None: foreground_high = foreground if background_high is None: background_high = background high_256 = AttrSpec(foreground_high, background_high, 256) high_true = AttrSpec(foreground_high, background_high, 16777216) # 2**24 is not Literal # 'hX' where X > 15 are different in 88/256 color, use # basic colors for 88-color mode if high colors are specified # in this way (also avoids crash when X > 87) def large_h(desc: str) -> bool: if not desc.startswith("h"): return False if "," in desc: desc = desc.split(",", 1)[0] num = int(desc[1:], 10) return num > 15 if large_h(foreground_high) or large_h(background_high): high_88 = basic else: high_88 = AttrSpec(foreground_high, background_high, 88) signals.emit_signal(self, UPDATE_PALETTE_ENTRY, name, basic, mono_spec, high_88, high_256, high_true) self._palette[name] = (basic, mono_spec, high_88, high_256, high_true) def _test() -> None: import doctest doctest.testmod() if __name__ == "__main__": _test() urwid-urwid-69907e4/urwid/display/curses.py000066400000000000000000000660211526073734300207610ustar00rootroot00000000000000# Urwid curses output wrapper.. the horror.. # Copyright (C) 2004-2011 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """Curses-based UI implementation.""" from __future__ import annotations import curses import sys import typing import warnings from contextlib import suppress from urwid import util from . import escape from .common import UNPRINTABLE_TRANS_TABLE, AttrSpec, BaseScreen, RealTerminal if typing.TYPE_CHECKING: from collections.abc import Hashable from typing_extensions import Literal from urwid import Canvas _MouseInput = tuple[str, int, int, int] _CursorPosition = tuple[typing.Literal["cursor position"], int, int] _PrivateModeReport = tuple[typing.Literal["private mode report"], str, int] _DecodedInput = list[typing.Union[str, _MouseInput, _CursorPosition, _PrivateModeReport]] IS_WINDOWS = sys.platform == "win32" # curses.KEY_RESIZE (sometimes not defined) if IS_WINDOWS: KEY_MOUSE = 539 # under Windows key mouse is different KEY_RESIZE = 546 COLOR_CORRECTION: dict[int, int] = dict( enumerate( ( curses.COLOR_BLACK, curses.COLOR_RED, curses.COLOR_GREEN, curses.COLOR_YELLOW, curses.COLOR_BLUE, curses.COLOR_MAGENTA, curses.COLOR_CYAN, curses.COLOR_WHITE, ) ) ) def initscr() -> curses.window: """Initialize curses on Windows, copying the ``ACS_*``/``LINES``/``COLS`` attributes ``_curses`` misses.""" import curses # noqa: I001 # pylint: disable=redefined-outer-name,reimported # special case for monkeypatch import _curses stdscr = _curses.initscr() for key, value in _curses.__dict__.items(): if key.startswith("ACS_") or key in {"LINES", "COLS"}: setattr(curses, key, value) return stdscr curses.initscr = initscr else: KEY_MOUSE = 409 # curses.KEY_MOUSE KEY_RESIZE = 410 COLOR_CORRECTION = {} _curses_colours = { # pylint: disable=consider-using-namedtuple-or-dataclass # historic test/debug data "default": (-1, 0), "black": (curses.COLOR_BLACK, 0), "dark red": (curses.COLOR_RED, 0), "dark green": (curses.COLOR_GREEN, 0), "brown": (curses.COLOR_YELLOW, 0), "dark blue": (curses.COLOR_BLUE, 0), "dark magenta": (curses.COLOR_MAGENTA, 0), "dark cyan": (curses.COLOR_CYAN, 0), "light gray": (curses.COLOR_WHITE, 0), "dark gray": (curses.COLOR_BLACK, 1), "light red": (curses.COLOR_RED, 1), "light green": (curses.COLOR_GREEN, 1), "yellow": (curses.COLOR_YELLOW, 1), "light blue": (curses.COLOR_BLUE, 1), "light magenta": (curses.COLOR_MAGENTA, 1), "light cyan": (curses.COLOR_CYAN, 1), "white": (curses.COLOR_WHITE, 1), } class Screen(BaseScreen, RealTerminal): """Screen backend that drives the real terminal through the ``curses`` module.""" def __init__(self) -> None: """Initialize an unstarted curses screen with no color pairs allocated yet.""" super().__init__() self.curses_pairs = [(None, None)] # Can't be sure what pair 0 will default to self.has_color = False self.has_default_colors = False self.s: curses.window | None = None self.cursor_state: typing.Literal["fixed"] | int | None = None self.prev_input_resize = 0 self.set_input_timeouts() self.last_bstate = 0 self._mouse_tracking_enabled = False self._curses_attr_cache: dict[AttrSpec, int] = {} # Holds the last drawn canvas so the canvas cache keeps its entries alive. self.keep_cache_alive_link: Canvas | None = None self.register_palette_entry(None, "default", "default") def set_mouse_tracking(self, enable: bool = True) -> None: """ Enable mouse tracking. After calling this function get_input will include mouse click events along with keystrokes. :raises NotImplementedError: *enable* is false; tracking cannot be turned off. """ enable = bool(enable) if enable == self._mouse_tracking_enabled: return if enable: curses.mousemask( 0 | curses.BUTTON1_PRESSED | curses.BUTTON1_RELEASED | curses.BUTTON2_PRESSED | curses.BUTTON2_RELEASED | curses.BUTTON3_PRESSED | curses.BUTTON3_RELEASED | curses.BUTTON4_PRESSED | curses.BUTTON4_RELEASED | curses.BUTTON5_PRESSED | curses.BUTTON5_RELEASED | curses.BUTTON1_DOUBLE_CLICKED | curses.BUTTON1_TRIPLE_CLICKED | curses.BUTTON2_DOUBLE_CLICKED | curses.BUTTON2_TRIPLE_CLICKED | curses.BUTTON3_DOUBLE_CLICKED | curses.BUTTON3_TRIPLE_CLICKED | curses.BUTTON4_DOUBLE_CLICKED | curses.BUTTON4_TRIPLE_CLICKED | curses.BUTTON5_DOUBLE_CLICKED | curses.BUTTON5_TRIPLE_CLICKED | curses.BUTTON_SHIFT | curses.BUTTON_ALT | curses.BUTTON_CTRL ) else: raise NotImplementedError() self._mouse_tracking_enabled = enable def _start(self, *args: typing.Any, **kwargs: typing.Any) -> None: """Initialize the screen and input mode.""" self.s = curses.initscr() self.has_color = curses.has_colors() if self.has_color: curses.start_color() if curses.COLORS < 8: # not colourful enough self.has_color = False if self.has_color: try: curses.use_default_colors() self.has_default_colors = True except curses.error: self.has_default_colors = False self._setup_colour_pairs() self._curses_attr_cache.clear() curses.noecho() curses.meta(True) curses.halfdelay(10) # use set_input_timeouts to adjust self.s.keypad(False) if not self._signal_keys_set: self._old_signal_keys = self.tty_signal_keys() super()._start() if IS_WINDOWS: # halfdelay() seems unnecessary and causes everything to slow down a lot. curses.nocbreak() # exits halfdelay mode # keypad(1) is needed, or we get no special keys (cursor keys, etc.) self.s.keypad(True) def _stop(self) -> None: """Restore the screen.""" curses.echo() self._curs_set(1) with suppress(curses.error): curses.endwin() # don't block original error with curses error if self._old_signal_keys: self.tty_signal_keys(*self._old_signal_keys) super()._stop() def _setup_colour_pairs(self) -> None: """Initialize all 63 color pairs based on the term: bg * 8 + 7 - fg. So to get a color, we just need to use that term and get the right color pair number. """ if not self.has_color: return if IS_WINDOWS: self.has_default_colors = False for fg in range(8): for bg in range(8): # leave out white on black if fg == curses.COLOR_WHITE and bg == curses.COLOR_BLACK: continue curses.init_pair(bg * 8 + 7 - fg, COLOR_CORRECTION.get(fg, fg), COLOR_CORRECTION.get(bg, bg)) def _curs_set(self, x: int) -> None: if self.cursor_state in {"fixed", x}: return try: curses.curs_set(x) self.cursor_state = x except curses.error: self.cursor_state = "fixed" def _clear(self) -> None: """Clear the screen and redraw it. .. deprecated:: 4.1.4 Not used by the urwid code base; there is no replacement. """ warnings.warn( "_clear is not used by the urwid code base. API will be removed in version 5.0.", DeprecationWarning, stacklevel=2, ) self.s.clear() self.s.refresh() def _getch(self, wait_tenths: int | None) -> int: if wait_tenths == 0: return self._getch_nodelay() if not IS_WINDOWS: if wait_tenths is None: curses.cbreak() else: curses.halfdelay(wait_tenths) self.s.nodelay(False) return self.s.getch() def _getch_nodelay(self) -> int: self.s.nodelay(True) if not IS_WINDOWS: while True: # this call fails sometimes, but seems to work when I try again with suppress(curses.error): curses.cbreak() break return self.s.getch() def set_input_timeouts( self, max_wait: float | None = None, complete_wait: float = 0.1, resize_wait: float = 0.1, ) -> None: """Set the get_input timeout values. All values have a granularity of 0.1s, ie. any value between 0.15 and 0.05 will be treated as 0.1 and any value less than 0.05 will be treated as 0. The maximum timeout value for this module is 25.5 seconds. :param max_wait: amount of time in seconds to wait for input when there is no input pending, wait forever if None :param complete_wait: amount of time in seconds to wait when get_input detects an incomplete escape sequence at the end of the available input :param resize_wait: amount of time in seconds to wait for more input after receiving two screen resize requests in a row to stop urwid from consuming 100% cpu during a gradual window resize operation """ def convert_to_tenths(s: float | None) -> int | None: if s is None: return None return int((s + 0.05) * 10) self.max_tenths = convert_to_tenths(max_wait) self.complete_tenths = convert_to_tenths(complete_wait) self.resize_tenths = convert_to_tenths(resize_wait) @typing.overload def get_input(self, raw_keys: Literal[False]) -> _DecodedInput: ... @typing.overload def get_input(self, raw_keys: Literal[True]) -> tuple[_DecodedInput, list[int]]: ... def get_input(self, raw_keys: bool = False) -> _DecodedInput | tuple[_DecodedInput, list[int]]: r"""Return pending input as a list. :param raw_keys: return raw keycodes as well as translated versions :raises RuntimeError: the screen has not been started. This function will immediately return all the input since the last time it was called. If there is no input pending it will wait before returning an empty list. The wait time may be configured with the set_input_timeouts function. If raw_keys is False (default) this function will return a list of keys pressed. If raw_keys is True this function will return a ( keys pressed, raw keycodes ) tuple instead. Examples of keys returned: * ASCII printable characters: :kbd:`space`, :kbd:`a`, :kbd:`0`, :kbd:`A`, :kbd:`-`, :kbd:`/` * ASCII control characters: :kbd:`tab`, :kbd:`enter` * Escape sequences: :kbd:`up`, :kbd:`page up`, :kbd:`home`, :kbd:`insert`, :kbd:`f1` * Key combinations: :kbd:`shift f1`, :kbd:`meta a`, :kbd:`ctrl b` * Window events: ``"window resize"`` When a narrow encoding is not enabled: * "Extended ASCII" characters: "\xa1", "\xb2", "\xfe" When a wide encoding is enabled: * Double-byte characters: "\xa1\xea", "\xb2\xd4" When utf8 encoding is enabled: * Unicode characters: u"\u00a5", u'\u253c" Examples of mouse events returned: * Mouse button press: ('mouse press', 1, 15, 13), ('meta mouse press', 2, 17, 23) * Mouse button release: ('mouse release', 0, 18, 13), ('ctrl mouse release', 0, 17, 23) """ if not self._started: raise RuntimeError keys, raw = self._get_input(self.max_tenths) # Avoid pegging CPU at 100% when slowly resizing, and work # around a bug with some braindead curses implementations that # return "no key" between "window resize" commands if keys == ["window resize"] and self.prev_input_resize: for _ in range(2): new_keys, new_raw = self._get_input(self.resize_tenths) raw += new_raw if new_keys and new_keys != ["window resize"]: if "window resize" in new_keys: keys = new_keys else: keys.extend(new_keys) break if keys == ["window resize"]: self.prev_input_resize = 2 elif self.prev_input_resize == 2 and not keys: self.prev_input_resize = 1 else: self.prev_input_resize = 0 if raw_keys: return keys, raw return keys def _get_input(self, wait_tenths: int | None) -> tuple[_DecodedInput, list[int]]: # this works around a strange curses bug with window resizing # not being reported correctly with repeated calls to this # function without a doupdate call in between curses.doupdate() key = self._getch(wait_tenths) resize = False raw = [] keys = [] while key >= 0: raw.append(key) if key == KEY_RESIZE: resize = True elif key == KEY_MOUSE: keys += self._encode_mouse_event() else: keys.append(key) key = self._getch_nodelay() processed = [] try: while keys: run, keys = escape.process_keyqueue(keys, True) processed += run except escape.MoreInputRequired: key = self._getch(self.complete_tenths) while key >= 0: raw.append(key) if key == KEY_RESIZE: resize = True elif key == KEY_MOUSE: keys += self._encode_mouse_event() else: keys.append(key) key = self._getch_nodelay() while keys: run, keys = escape.process_keyqueue(keys, False) processed += run if resize: processed.append("window resize") return processed, raw def _encode_mouse_event(self) -> list[int]: # convert to escape sequence last_state = next_state = self.last_bstate (_id, x, y, _z, bstate) = curses.getmouse() mod = 0 if bstate & curses.BUTTON_SHIFT: mod |= 4 if bstate & curses.BUTTON_ALT: mod |= 8 if bstate & curses.BUTTON_CTRL: mod |= 16 result = [] def append_button(b: int) -> None: b |= mod result.extend([27, ord("["), ord("M"), b + 32, x + 33, y + 33]) if bstate & curses.BUTTON1_PRESSED and last_state & 1 == 0: append_button(0) next_state |= 1 if bstate & curses.BUTTON2_PRESSED and last_state & 2 == 0: append_button(1) next_state |= 2 if bstate & curses.BUTTON3_PRESSED and last_state & 4 == 0: append_button(2) next_state |= 4 if bstate & curses.BUTTON4_PRESSED and last_state & 8 == 0: append_button(64) next_state |= 8 if bstate & curses.BUTTON5_PRESSED and last_state & 16 == 0: append_button(65) next_state |= 16 if bstate & curses.BUTTON1_RELEASED and last_state & 1: append_button(0 + escape.MOUSE_RELEASE_FLAG) next_state &= ~1 if bstate & curses.BUTTON2_RELEASED and last_state & 2: append_button(1 + escape.MOUSE_RELEASE_FLAG) next_state &= ~2 if bstate & curses.BUTTON3_RELEASED and last_state & 4: append_button(2 + escape.MOUSE_RELEASE_FLAG) next_state &= ~4 if bstate & curses.BUTTON4_RELEASED and last_state & 8: append_button(64 + escape.MOUSE_RELEASE_FLAG) next_state &= ~8 if bstate & curses.BUTTON5_RELEASED and last_state & 16: append_button(65 + escape.MOUSE_RELEASE_FLAG) next_state &= ~16 if bstate & curses.BUTTON1_DOUBLE_CLICKED: append_button(0 + escape.MOUSE_MULTIPLE_CLICK_FLAG) if bstate & curses.BUTTON2_DOUBLE_CLICKED: append_button(1 + escape.MOUSE_MULTIPLE_CLICK_FLAG) if bstate & curses.BUTTON3_DOUBLE_CLICKED: append_button(2 + escape.MOUSE_MULTIPLE_CLICK_FLAG) if bstate & curses.BUTTON4_DOUBLE_CLICKED: append_button(64 + escape.MOUSE_MULTIPLE_CLICK_FLAG) if bstate & curses.BUTTON5_DOUBLE_CLICKED: append_button(65 + escape.MOUSE_MULTIPLE_CLICK_FLAG) if bstate & curses.BUTTON1_TRIPLE_CLICKED: append_button(0 + escape.MOUSE_MULTIPLE_CLICK_FLAG * 2) if bstate & curses.BUTTON2_TRIPLE_CLICKED: append_button(1 + escape.MOUSE_MULTIPLE_CLICK_FLAG * 2) if bstate & curses.BUTTON3_TRIPLE_CLICKED: append_button(2 + escape.MOUSE_MULTIPLE_CLICK_FLAG * 2) if bstate & curses.BUTTON4_TRIPLE_CLICKED: append_button(64 + escape.MOUSE_MULTIPLE_CLICK_FLAG * 2) if bstate & curses.BUTTON5_TRIPLE_CLICKED: append_button(65 + escape.MOUSE_MULTIPLE_CLICK_FLAG * 2) self.last_bstate = next_state return result def _dbg_instr(self) -> bytes: """Read a line of input with echo on (intended for debugging). .. deprecated:: 4.1.4 Not used by the urwid code base; there is no replacement. """ warnings.warn( "_dbg_instr is not used by the urwid code base. API will be removed in version 5.0.", DeprecationWarning, stacklevel=2, ) curses.echo() self.s.nodelay(False) curses.halfdelay(100) string = self.s.getstr() curses.noecho() return string def _dbg_out(self, string: str) -> None: """Write *string* at the cursor and refresh (intended for debugging). .. deprecated:: 4.1.4 Not used by the urwid code base; there is no replacement. """ warnings.warn( "_dbg_out is not used by the urwid code base. API will be removed in version 5.0.", DeprecationWarning, stacklevel=2, ) self.s.clrtoeol() self.s.addstr(string) self.s.refresh() self._curs_set(1) def _dbg_query(self, question: str) -> bytes: """Write *question* and read the reply (intended for debugging). .. deprecated:: 4.1.4 Not used by the urwid code base; there is no replacement. """ warnings.warn( "_dbg_query is not used by the urwid code base. API will be removed in version 5.0.", DeprecationWarning, stacklevel=2, ) with warnings.catch_warnings(): # _dbg_out and _dbg_instr are deprecated alongside this method, # so their warnings would only repeat the one raised above. warnings.simplefilter("ignore", DeprecationWarning) self._dbg_out(question) return self._dbg_instr() def _dbg_refresh(self) -> None: """Refresh the screen (intended for debugging). .. deprecated:: 4.1.4 Not used by the urwid code base; there is no replacement. """ warnings.warn( "_dbg_refresh is not used by the urwid code base. API will be removed in version 5.0.", DeprecationWarning, stacklevel=2, ) self.s.refresh() def get_cols_rows(self) -> tuple[int, int]: """Return the terminal dimensions (num columns, num rows).""" rows, cols = self.s.getmaxyx() return cols, rows def _setattr(self, a: AttrSpec | str | None) -> None: if a is None: self.s.attrset(0) return if not isinstance(a, AttrSpec): p = self._palette.get(a, (AttrSpec("default", "default"),)) a = p[0] try: attr = self._curses_attr_cache[a] except KeyError: attr = self._curses_attr_cache[a] = self._compute_curses_attr(a) self.s.attrset(attr) def _compute_curses_attr(self, a: AttrSpec) -> int: if self.has_color: if a.foreground_basic: if a.foreground_number >= 8: fg = a.foreground_number - 8 else: fg = a.foreground_number else: fg = 7 if a.background_basic: bg = a.background_number else: bg = 0 attr = curses.color_pair(bg * 8 + 7 - fg) else: attr = 0 if a.bold: attr |= curses.A_BOLD if a.standout: attr |= curses.A_STANDOUT if a.underline: attr |= curses.A_UNDERLINE if a.blink: attr |= curses.A_BLINK if a.faint: attr |= curses.A_DIM return attr def draw_screen(self, size: tuple[int, int], canvas: Canvas) -> None: """Paint screen with rendered canvas. :raises RuntimeError: the screen has not been started. :raises ValueError: *canvas* does not match *size*, or a run carries an unknown character set. :raises TypeError: a text run is not a byte string. """ logger = self.logger.getChild("draw_screen") if not self._started: raise RuntimeError _cols, rows = size if canvas.rows() != rows: raise ValueError("canvas size and passed size don't match") logger.debug(f"Drawing screen with size {size!r}") for y, row in enumerate(canvas.content()): try: self.s.move(y, 0) except curses.error: # terminal shrunk? # move failed so stop drawing rows, but still flush what # was already drawn instead of leaving a stale screen. break first = True lasta = None aborted = False for nr, (a, cs, seg) in enumerate(row): if cs != "U": seg = seg.translate(UNPRINTABLE_TRANS_TABLE) # noqa: PLW2901 if not isinstance(seg, bytes): raise TypeError(seg) if first or lasta != a: self._setattr(a) lasta = a try: if cs in {"0", "U"}: for segment in seg: self.s.addch(0x400000 + segment) else: if cs is not None: raise ValueError(f"cs not in ('0', 'U' ,'None'): {cs!r}") if not isinstance(seg, bytes): raise TypeError(seg) self.s.addstr(seg.decode(util.get_encoding())) except curses.error: # it's OK to get out of the # screen on the lower right if y != rows - 1 or nr != len(row) - 1: # perhaps screen size changed # quietly abort, but still flush what was already drawn. aborted = True break if aborted: break else: if canvas.cursor is not None: x, y = canvas.cursor self._curs_set(1) with suppress(curses.error): self.s.move(y, x) else: self._curs_set(0) self.s.move(0, 0) self.s.refresh() self.keep_cache_alive_link = canvas def clear(self) -> None: """Force the screen to be completely repainted on the next call to draw_screen().""" self.s.clear() class _test: def __init__(self) -> None: self.ui = Screen() self.l = sorted(_curses_colours) for c in self.l: self.ui.register_palette( [ (f"{c} on black", c, "black", "underline"), (f"{c} on dark blue", c, "dark blue", "bold"), (f"{c} on light gray", c, "light gray", "standout"), ] ) with self.ui.start(): self.run() def run(self) -> None: """Render a palette demo screen showing every registered color combination.""" from urwid.canvas import TextCanvas encoding = util.get_encoding() text = [f" has_color = {self.ui.has_color!r}", ""] attr: list[list[tuple[Hashable, int]]] = [[], []] for c in self.l: t = "" a: list[tuple[Hashable, int]] = [] for p in f"{c} on black", f"{c} on dark blue", f"{c} on light gray": a.append((p, 27)) t += (p + 27 * " ")[:27] text.append(t) attr.append(a) text += ["", "return values from get_input(): (q exits)", ""] attr += [[], [], []] cols, rows = self.ui.get_cols_rows() keys = None while keys != ["q"]: r = TextCanvas( text=([t.encode(encoding).ljust(cols) for t in text] + [b""] * rows)[:rows], attr=(attr + [[] for _ in range(rows)])[:rows], ) self.ui.draw_screen((cols, rows), r) keys, raw = self.ui.get_input(raw_keys=True) if "window resize" in keys: cols, rows = self.ui.get_cols_rows() if not keys: continue t = "" a = [] for k in keys: if isinstance(k, str): k = k.encode(encoding) # type: ignore[assignment] # noqa: PLW2901 t += f"'{k}' " a += [(None, 1), ("yellow on dark blue", len(k)), (None, 2)] text.append(f"{t}: {raw!r}") attr.append(a) text = text[-rows:] attr = attr[-rows:] if __name__ == "__main__": _test() urwid-urwid-69907e4/urwid/display/escape.py000066400000000000000000000650121526073734300207140ustar00rootroot00000000000000# Urwid escape sequences common to curses_display and raw_display # Copyright (C) 2004-2011 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """Terminal Escape Sequences for input and display.""" from __future__ import annotations import enum import functools import re import sys import typing from collections.abc import MutableMapping from urwid import str_util if typing.TYPE_CHECKING: from collections.abc import Iterable _KeyQueueData = dict[int, typing.Union[str, "_KeyQueueData"]] _MouseInput = tuple[str, int, int, int] _CursorPosition = tuple[typing.Literal["cursor position"], int, int] _PrivateModeReport = tuple[typing.Literal["private mode report"], str, int] IS_WINDOWS = sys.platform == "win32" within_double_byte = str_util.within_double_byte SO = "\x0e" SI = "\x0f" # Encoded once here, because `urwid.util.apply_target_encoding` splits on them for every rendered text segment. _SO_BYTES = SO.encode("ascii") _SI_BYTES = SI.encode("ascii") _SI_SO = SI + SO IBMPC_ON = "\x1b[11m" IBMPC_OFF = "\x1b[10m" DEC_TAG = "0" DEC_SPECIAL_CHARS = "▮◆▒����°±��┘�┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥π≠£·" ALT_DEC_SPECIAL_CHARS = "_`abcdefghijklmnopqrstuvwxyz{|}~" DEC_SPECIAL_CHARMAP = {} if len(DEC_SPECIAL_CHARS) != len(ALT_DEC_SPECIAL_CHARS): raise RuntimeError(repr((DEC_SPECIAL_CHARS, ALT_DEC_SPECIAL_CHARS))) for c, alt in zip(DEC_SPECIAL_CHARS, ALT_DEC_SPECIAL_CHARS): DEC_SPECIAL_CHARMAP[ord(c)] = SO + alt + SI SAFE_ASCII_DEC_SPECIAL_RE = re.compile(f"^[ -~{DEC_SPECIAL_CHARS}]*$") DEC_SPECIAL_RE = re.compile(f"[{DEC_SPECIAL_CHARS}]") # the most common ord() calls used for numeric decode, pre-calculate once _ORD_0 = ord("0") _ORD_9 = ord("9") # longest "Pb;Px;Py" body of an SGR mouse report accepted before its "M"/"m" terminator _SGR_MOUSE_MAX_LEN = 32 ################### # Input sequences ################### class MoreInputRequired(Exception): """Raised when the input decoded so far is a prefix of a longer escape sequence.""" def escape_modifier(digit: str) -> str: """Translate a CSI modifier digit into the "shift "/"meta "/"ctrl " prefix combination it encodes.""" mode = ord(digit) - ord("1") return "shift " * (mode & 1) + "meta " * ((mode & 2) // 2) + "ctrl " * ((mode & 4) // 4) input_sequences: list[tuple[str, str]] = [ ("[A", "up"), ("[B", "down"), ("[C", "right"), ("[D", "left"), ("[E", "5"), ("[F", "end"), ("[G", "5"), ("[H", "home"), ("[I", "focus in"), ("[O", "focus out"), ("[1~", "home"), ("[2~", "insert"), ("[3~", "delete"), ("[4~", "end"), ("[5~", "page up"), ("[6~", "page down"), ("[7~", "home"), ("[8~", "end"), ("[[A", "f1"), ("[[B", "f2"), ("[[C", "f3"), ("[[D", "f4"), ("[[E", "f5"), ("[11~", "f1"), ("[12~", "f2"), ("[13~", "f3"), ("[14~", "f4"), ("[15~", "f5"), ("[17~", "f6"), ("[18~", "f7"), ("[19~", "f8"), ("[20~", "f9"), ("[21~", "f10"), ("[23~", "f11"), ("[24~", "f12"), ("[25~", "f13"), ("[26~", "f14"), ("[28~", "f15"), ("[29~", "f16"), ("[31~", "f17"), ("[32~", "f18"), ("[33~", "f19"), ("[34~", "f20"), ("OA", "up"), ("OB", "down"), ("OC", "right"), ("OD", "left"), ("OH", "home"), ("OF", "end"), ("OP", "f1"), ("OQ", "f2"), ("OR", "f3"), ("OS", "f4"), ("Oo", "/"), ("Oj", "*"), ("Om", "-"), ("Ok", "+"), ("[Z", "shift tab"), ("On", "."), ("[200~", "begin paste"), ("[201~", "end paste"), *( (prefix + letter, modifier + key) for prefix, modifier in zip("O[", ("meta ", "shift ")) for letter, key in zip("abcd", ("up", "down", "right", "left")) ), *( (f"[{digit}{symbol}", modifier + key) for modifier, symbol in zip(("shift ", "meta "), "$^") for digit, key in zip("235678", ("insert", "delete", "page up", "page down", "home", "end")) ), *((f"O{ord('p') + n:c}", str(n)) for n in range(10)), *( # modified cursor keys + home, end, 5 -- [#X and [1;#X forms (prefix + digit + letter, escape_modifier(digit) + key) for prefix in ("[", "[1;") for digit in "12345678" for letter, key in zip("ABCDEFGH", ("up", "down", "right", "left", "5", "end", "5", "home")) ), *( # modified F1-F4 keys - O#X form and [1;#X form (prefix + digit + letter, escape_modifier(digit) + f"f{number}") for prefix in ("O", "[1;") for digit in "12345678" for number, letter in enumerate("PQRS", start=1) ), *( # modified F1-F13 keys -- [XX;#~ form (f"[{num!s};{digit}~", escape_modifier(digit) + key) for digit in "12345678" for num, key in zip( (3, 5, 6, 11, 12, 13, 14, 15, 17, 18, 19, 20, 21, 23, 24, 25, 26, 28, 29, 31, 32, 33, 34), ( "delete", "page up", "page down", *(f"f{idx}" for idx in range(1, 21)), ), ) ), # mouse reporting (special handling done in KeyqueueTrie) ("[M", "mouse"), # mouse reporting for SGR 1006 ("[<", "sgrmouse"), # report status response ("[0n", "status ok"), ] class KeyqueueTrie: """Trie mapping terminal escape sequences to the key names they decode to.""" __slots__ = ("data",) def __init__(self, sequences: Iterable[tuple[str, str]]) -> None: """ Build a trie of the given ``(escape sequence, key name)`` pairs. :raises TypeError: two sequences collide in the trie. """ self.data: _KeyQueueData = {} for s, result in sequences: if isinstance(result, dict): raise TypeError(result) self.add(self.data, s, result) def add( self, root: _KeyQueueData, s: str, result: str, ) -> None: """ Add one ``(escape sequence, key name)`` pair to the trie. :raises RuntimeError: the sequence collides with a shorter one already in the trie. :raises ValueError: the sequence collides with a longer one already in the trie. """ if not isinstance(root, MutableMapping) or not s: raise RuntimeError("trie conflict detected") seq_start = ord(s[0]) if seq_start in root: child = root[seq_start] if isinstance(child, str): raise ValueError("trie conflict detected") self.add(child, s[1:], result) return if len(s) > 1: d: _KeyQueueData = {} root[seq_start] = d self.add(d, s[1:], result) return root[seq_start] = result def get( self, keys: list[int], more_available: bool, ) -> tuple[str | _MouseInput | _CursorPosition | _PrivateModeReport, list[int]] | None: """Decode the longest key sequence matching the start of ``keys``, trying the trie then the special reports.""" result: tuple[str | _MouseInput | _CursorPosition | _PrivateModeReport, list[int]] | None if result := self.get_recurse(self.data, keys, more_available): return result if (result := self.read_private_mode_report(keys, more_available)) is not None: return result return self.read_cursor_position(keys, more_available) def get_recurse( self, root: (_KeyQueueData | str), keys: list[int], more_available: bool, ) -> tuple[str | _MouseInput, list[int]] | None: """ Walk the trie for the next key, consuming codes as it goes. :raises MoreInputRequired: the codes end in the middle of a sequence and *more_available* is set. """ if not isinstance(root, MutableMapping): if root == "mouse": return self.read_mouse_info(keys, more_available) if root == "sgrmouse": return self.read_sgrmouse_info(keys, more_available) return (root, keys) if not keys: # get more keys if more_available: raise MoreInputRequired() return None if keys[0] not in root: return None return self.get_recurse(root[keys[0]], keys[1:], more_available) def read_mouse_info( self, keys: list[int], more_available: bool, ) -> tuple[_MouseInput, list[int]] | None: """Read an X10 mouse report from the codes and return the resulting input. :raises MoreInputRequired: the codes end in the middle of a sequence and *more_available* is set. """ if len(keys) < 3: if more_available: raise MoreInputRequired() return None b = keys[0] - _MOUSE_BYTE_OFFSET # supports 0-255 x, y = (keys[1] - _MOUSE_COORD_OFFSET) % 256, (keys[2] - _MOUSE_COORD_OFFSET) % 256 prefixes = [] if b & _MOUSE_SHIFT_FLAG: prefixes.append("shift ") if b & _MOUSE_META_FLAG: prefixes.append("meta ") if b & _MOUSE_CTRL_FLAG: prefixes.append("ctrl ") multi_click = (b & MOUSE_MULTIPLE_CLICK_MASK) >> 9 if multi_click == 1: prefixes.append("double ") elif multi_click == 2: prefixes.append("triple ") prefix = "".join(prefixes) button_bits = b & _MOUSE_BUTTON_MASK if b & _MOUSE_EXTRA_BUTTON_FLAG: button = 8 + button_bits elif b & _MOUSE_HIGH_BUTTON_FLAG: button = 4 + button_bits else: button = 1 + button_bits if button_bits == _MOUSE_BUTTON_MASK and not b & (_MOUSE_HIGH_BUTTON_FLAG | _MOUSE_EXTRA_BUTTON_FLAG): action = "release" button = 0 elif b & MOUSE_RELEASE_FLAG: action = "release" elif b & MOUSE_DRAG_FLAG: action = "drag" elif multi_click: action = "click" else: action = "press" return ((f"{prefix}mouse {action}", button, x, y), keys[3:]) def read_sgrmouse_info( self, keys: list[int], more_available: bool, ) -> tuple[_MouseInput, list[int]] | None: # Helpful links: # https://stackoverflow.com/questions/5966903/how-to-get-mousemove-and-mouseclick-in-bash # http://invisible-island.net/xterm/ctlseqs/ctlseqs.pdf """Read an SGR mouse report from the codes and return the resulting input. :returns: the mouse event and the codes left after the report, or None when the codes do not start with an SGR mouse report. :raises MoreInputRequired: the codes end in the middle of a sequence and *more_available* is set. """ if not keys: if more_available: raise MoreInputRequired() return None # Anything other than "Pb;Px;Py" digits before the terminator, or a longer run, is not a report: it is # rejected here instead of being buffered while waiting for a terminator that may never come. for pos_m in range(min(len(keys), _SGR_MOUSE_MAX_LEN + 1)): k = keys[pos_m] if k in {ord("M"), ord("m")}: break if not (_ORD_0 <= k <= _ORD_9 or k == ord(";")): return None else: if more_available and len(keys) <= _SGR_MOUSE_MAX_LEN: raise MoreInputRequired() return None fields = bytes(keys[:pos_m]).split(b";") if len(fields) != 3 or not all(fields): return None b, x, y = (int(field) for field in fields) action = chr(keys[pos_m]) # Double and triple clicks are not supported. # They can be implemented by using a timer. # This timer can check if the last registered click is below a certain threshold. # This threshold is normally set in the operating system itself, # so setting one here will cause an inconsistent behaviour. prefixes = [] if b & _MOUSE_SHIFT_FLAG: prefixes.append("shift ") if b & _MOUSE_META_FLAG: prefixes.append("meta ") if b & _MOUSE_CTRL_FLAG: prefixes.append("ctrl ") prefix = "".join(prefixes) button_bits = b & _MOUSE_BUTTON_MASK if b & _MOUSE_EXTRA_BUTTON_FLAG: button = 8 + button_bits elif b & _MOUSE_HIGH_BUTTON_FLAG: button = 4 + button_bits else: button = 1 + button_bits x -= 1 y -= 1 if action == "M": if b & MOUSE_DRAG_FLAG: action = "drag" else: action = "press" else: action = "release" return ((f"{prefix}mouse {action}", button, x, y), keys[pos_m + 1 :]) def read_private_mode_report( self, keys: list[int], more_available: bool, ) -> tuple[_PrivateModeReport, list[int]] | None: """Interpret a DECRPM reply (CSI ? Pd ; Ps $ y). :returns: ``('private mode report', mode, value)``. :raises MoreInputRequired: the codes end in the middle of a sequence and *more_available* is set. """ if not keys: if more_available: raise MoreInputRequired() return None if keys[0] != ord("["): return None if len(keys) < 2: if more_available: raise MoreInputRequired() return None if keys[1] != ord("?"): return None # read the mode number mode = "" i = 2 for k in keys[i:]: i += 1 if k == ord(";"): if not mode: return None break if k < _ORD_0 or k > _ORD_9: return None mode += chr(k) else: if more_available: raise MoreInputRequired() return None if not keys[i:]: if more_available: raise MoreInputRequired() return None # Ps: "0" is a valid value here (mode not recognized), unlike the mode number above. value = 0 got_digit = False for k in keys[i:]: i += 1 if k == ord("$"): break if k < _ORD_0 or k > _ORD_9: return None value = value * 10 + k - _ORD_0 got_digit = True else: if more_available: raise MoreInputRequired() return None if not got_digit: return None if not keys[i:]: if more_available: raise MoreInputRequired() return None if keys[i] != ord("y"): return None return (("private mode report", mode, value), keys[i + 1 :]) def read_cursor_position( self, keys: list[int], more_available: bool, ) -> tuple[_CursorPosition, list[int]] | None: """ Interpret cursor position information being sent by the user's terminal. :returns: ('cursor position', x, y) where (x, y) == (0, 0) is the top left of the screen. :raises MoreInputRequired: the codes end in the middle of a sequence and *more_available* is set. """ if not keys: if more_available: raise MoreInputRequired() return None if keys[0] != ord("["): return None # read y value y = 0 i = 1 for k in keys[i:]: i += 1 if k == ord(";"): if not y: return None break if k < _ORD_0 or k > _ORD_9: return None if not y and k == _ORD_0: return None y = y * 10 + k - _ORD_0 if not keys[i:]: if more_available: raise MoreInputRequired() return None # read x value x = 0 for k in keys[i:]: i += 1 if k == ord("R"): if not x: return None return (("cursor position", x - 1, y - 1), keys[i:]) if k < _ORD_0 or k > _ORD_9: return None if not x and k == _ORD_0: return None x = x * 10 + k - _ORD_0 if not keys[i:] and more_available: raise MoreInputRequired() return None # This is added to button value to signal mouse release by curses_display # and raw_display when we know which button was released. NON-STANDARD MOUSE_RELEASE_FLAG = 2048 # This 2-bit mask is used to check if the mouse release from curses or gpm # is a double or triple release. 00 means single click, 01 double, # 10 triple. NON-STANDARD MOUSE_MULTIPLE_CLICK_MASK = 1536 # This is added to button value at mouse release to differentiate between # single, double and triple press. Double release adds this times one, # triple release adds this times two. NON-STANDARD MOUSE_MULTIPLE_CLICK_FLAG = 512 # xterm adds this to the button value to signal a mouse drag event MOUSE_DRAG_FLAG = 32 # Bits of the X10/SGR mouse report's button byte, per xterm's ctlseqs documentation of the # "normal tracking mode" and "SGR mouse mode" reports. _MOUSE_SHIFT_FLAG = 4 _MOUSE_META_FLAG = 8 _MOUSE_CTRL_FLAG = 16 _MOUSE_HIGH_BUTTON_FLAG = 64 # set to report buttons 4-7 (e.g. a wheel) _MOUSE_EXTRA_BUTTON_FLAG = 128 # set to report buttons 8-11 (e.g. side buttons) _MOUSE_BUTTON_MASK = 3 # low two bits: the button offset within its group # button_bits == 3 with neither flag set means "released" (X10 report only); with either flag # set it is the group's 4th button (7 or 11), which has no release encoding. # The X10 report encodes the button byte and the two coordinate bytes by adding an offset to # keep them in the printable ASCII range; coordinates are further offset by 1 since they are # reported 1-based. _MOUSE_BYTE_OFFSET = 32 _MOUSE_COORD_OFFSET = 33 ################################################# # Build the input trie from input_sequences list input_trie = KeyqueueTrie(input_sequences) ################################################# _keyconv = { 8: "backspace", 9: "tab", 10: "enter", 13: "enter", 127: "backspace", # curses-only keycodes follow.. (XXX: are these used anymore?) 258: "down", 259: "up", 260: "left", 261: "right", 262: "home", 263: "backspace", 265: "f1", 266: "f2", 267: "f3", 268: "f4", 269: "f5", 270: "f6", 271: "f7", 272: "f8", 273: "f9", 274: "f10", 275: "f11", 276: "f12", 277: "shift f1", 278: "shift f2", 279: "shift f3", 280: "shift f4", 281: "shift f5", 282: "shift f6", 283: "shift f7", 284: "shift f8", 285: "shift f9", 286: "shift f10", 287: "shift f11", 288: "shift f12", 330: "delete", 331: "insert", 338: "page down", 339: "page up", 343: "enter", # on numpad 350: "5", # on numpad 360: "end", } if IS_WINDOWS: _keyconv[351] = "shift tab" _keyconv[358] = "end" def process_keyqueue( codes: list[int], more_available: bool, ) -> tuple[list[str | _MouseInput | _CursorPosition | _PrivateModeReport], list[int]]: """Decode `codes` into input events, consuming as much of the queue as is currently decodable. :param codes: list of key codes :param more_available: if True then raise MoreInputRequired when in the middle of a character sequence (escape/utf8/wide) and caller will attempt to send more key codes on the next call. :returns: a ``(list of input, list of remaining key codes)`` tuple. :raises MoreInputRequired: the codes end in the middle of a sequence and *more_available* is set. """ code = codes[0] if 32 <= code <= 126: key = chr(code) return [key], codes[1:] if code in _keyconv: return [_keyconv[code]], codes[1:] if 0 < code < 27: return [f"ctrl {ord('a') + code - 1:c}"], codes[1:] if 27 < code < 32: return [f"ctrl {ord('A') + code - 1:c}"], codes[1:] em = str_util.get_byte_encoding() if em == "utf8" and 127 < code < 256: if code & 0xE0 == 0xC0: # 2-byte form need_more = 1 elif code & 0xF0 == 0xE0: # 3-byte form need_more = 2 elif code & 0xF8 == 0xF0: # 4-byte form need_more = 3 else: return [f"<{code:d}>"], codes[1:] for i in range(1, need_more + 1): if len(codes) <= i: if more_available: raise MoreInputRequired() return [f"<{code:d}>"], codes[1:] k = codes[i] if k > 256 or k & 0xC0 != 0x80: return [f"<{code:d}>"], codes[1:] s = bytes(codes[: need_more + 1]) try: return [s.decode("utf-8")], codes[need_more + 1 :] except UnicodeDecodeError: return [f"<{code:d}>"], codes[1:] if ( em == "wide" and code < 256 and str_util.within_double_byte( (start_code := code.to_bytes(1, "little")), 0, 0, ) ): if not codes[1:] and more_available: raise MoreInputRequired() if codes[1:] and codes[1] < 256: end_code = codes[1].to_bytes(1, "little") if str_util.within_double_byte(start_code + end_code, 0, 1): return [chr(code) + chr(codes[1])], codes[2:] if 127 < code < 256: key = chr(code) return [key], codes[1:] if code != 27: return [f"<{code:d}>"], codes[1:] if (result := input_trie.get(codes[1:], more_available)) is not None: decoded, remaining_codes = result return [decoded], remaining_codes if codes[1:3] == [27, 27]: # With a third ESC following, the second one cannot start a sequence the first one prefixes as meta, # so decode the first one alone rather than recursing once per ESC in an arbitrarily long run. return ["esc"], codes[1:] if codes[1:]: # Meta keys -- ESC+Key form run, remaining_codes = process_keyqueue(codes[1:], more_available) # A mouse event or a terminal report (cursor position, DECRPM) cannot take a meta prefix. if not isinstance(run[0], str) or run[0] == "esc" or "meta " in run[0]: return ["esc", *run], remaining_codes return [f"meta {run[0]}", *run[1:]], remaining_codes return ["esc"], codes[1:] #################### # Output sequences #################### ESC = "\x1b" class PrivateMode(str, enum.Enum): """DEC private modes (``CSI ? Pd h``/``l``). Not every member is queried or acted on, some members are for future development. `_raw_display_base.TermModes` carries a field for each regardless. """ ALTERNATE_SCREEN_BUFFER = "1049" MOUSE_REPORTING = "1000" MOUSE_BUTTON_TRACKING = "1002" MOUSE_SGR_MODE = "1006" FOCUS_REPORTING = "1004" BRACKETED_PASTE = "2004" SYNCHRONIZED_OUTPUT = "2026" GRAPHEME_CLUSTERING = "2027" @functools.cached_property def query(self) -> str: """Return the DECRQM query (CSI ? Pd $ p) asking the terminal to report *mode*'s state.""" return f"{ESC}[?{self}$p" @functools.cached_property def enable_seq(self) -> str: """Enable sequence.""" return f"{ESC}[?{self}h" @functools.cached_property def disable_seq(self) -> str: """Disable sequence.""" return f"{ESC}[?{self}l" # Plain value in f-strings/str(), matching enum.StrEnum (3.11+); drop in release 5 on py3.11+ migration. __str__ = str.__str__ CURSOR_HOME = f"{ESC}[H" CURSOR_HOME_COL = "\r" APP_KEYPAD_MODE = f"{ESC}=" NUM_KEYPAD_MODE = f"{ESC}>" # Only for backward compatibility, temporary... SWITCH_TO_ALTERNATE_BUFFER = PrivateMode.ALTERNATE_SCREEN_BUFFER.enable_seq RESTORE_NORMAL_BUFFER = PrivateMode.ALTERNATE_SCREEN_BUFFER.disable_seq ENABLE_BRACKETED_PASTE_MODE = PrivateMode.BRACKETED_PASTE.enable_seq DISABLE_BRACKETED_PASTE_MODE = PrivateMode.BRACKETED_PASTE.disable_seq ENABLE_FOCUS_REPORTING = PrivateMode.FOCUS_REPORTING.enable_seq DISABLE_FOCUS_REPORTING = PrivateMode.FOCUS_REPORTING.disable_seq # RESET_SCROLL_REGION = ESC+"[;r" # RESET = ESC+"c" REPORT_STATUS = f"{ESC}[5n" REPORT_CURSOR_POSITION = f"{ESC}[6n" # DECRQM (CSI ? Pd $ p) reply is DECRPM (CSI ? Pd ; Ps $ y), recognized by KeyqueueTrie.read_private_mode_report(). # Ps: 0 = not recognized, 1-4 = recognized. DECRQM_RECOGNIZED_VALUES = frozenset({1, 2, 3, 4}) INSERT_ON = f"{ESC}[4h" INSERT_OFF = f"{ESC}[4l" def set_cursor_position(x: int, y: int) -> str: """ Return the escape sequence that moves the cursor to column *x*, row *y*. :raises TypeError: *x* or *y* is not an integer. """ if not isinstance(x, int): raise TypeError(x) if not isinstance(y, int): raise TypeError(y) return ESC + f"[{y + 1:d};{x + 1:d}H" def move_cursor_right(x: int) -> str: """Return the escape sequence that moves the cursor right by *x* columns, or "" when *x* is not positive.""" if x < 1: return "" return ESC + f"[{x:d}C" def move_cursor_up(x: int) -> str: """Return the escape sequence that moves the cursor up by *x* rows, or "" when *x* is not positive.""" if x < 1: return "" return ESC + f"[{x:d}A" def move_cursor_down(x: int) -> str: """Return the escape sequence that moves the cursor down by *x* rows, or "" when *x* is not positive.""" if x < 1: return "" return ESC + f"[{x:d}B" HIDE_CURSOR = f"{ESC}[?25l" SHOW_CURSOR = f"{ESC}[?25h" _MOUSE_MODES = (PrivateMode.MOUSE_REPORTING, PrivateMode.MOUSE_BUTTON_TRACKING, PrivateMode.MOUSE_SGR_MODE) MOUSE_TRACKING_ON = "".join(mode.enable_seq for mode in _MOUSE_MODES) MOUSE_TRACKING_OFF = "".join(mode.disable_seq for mode in reversed(_MOUSE_MODES)) DESIGNATE_G1_SPECIAL = f"{ESC})0" ERASE_IN_LINE_RIGHT = f"{ESC}[K" urwid-urwid-69907e4/urwid/display/html_fragment.py000066400000000000000000000235271526073734300223100ustar00rootroot00000000000000# Urwid html fragment output wrapper for "screen shots" # Copyright (C) 2004-2007 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """HTML PRE-based UI implementation.""" from __future__ import annotations import html import typing import warnings from urwid import str_util from urwid.event_loop import ExitMainLoop from urwid.util import get_encoding from .common import AttrSpec, BaseScreen, attr_spec_to_css if typing.TYPE_CHECKING: from typing_extensions import Literal from urwid import Canvas _MouseInput = tuple[str, int, int, int] _CursorPosition = tuple[typing.Literal["cursor position"], int, int] _DecodedInput = list[typing.Union[str, _MouseInput, _CursorPosition]] # replace control characters with ?'s _trans_table = str.maketrans(dict.fromkeys(range(32), "?")) _default_foreground = "black" _default_background = "light gray" class HtmlGeneratorSimulationError(Exception): """Raised when :class:`HtmlGenerator` is used in a way its simulated screen does not support.""" class HtmlGenerator(BaseScreen): """Screen backend that renders each displayed frame as an HTML fragment instead of a real terminal.""" # class variables fragments: typing.ClassVar[list[str]] = [] sizes: typing.ClassVar[list[tuple[int, int]]] = [] keys: typing.ClassVar[list[_DecodedInput]] = [] started = True def __init__(self) -> None: """Initialize the simulated screen with a 16-color palette.""" super().__init__() self.colors = 16 self.bright_is_bold = False # ignored self.has_underline = True # ignored self.register_palette_entry(None, _default_foreground, _default_background) def set_terminal_properties( self, colors: int | None = None, bright_is_bold: bool | None = None, has_underline: bool | None = None, ) -> None: """Store the given terminal capabilities, keeping each one unchanged where it is left as ``None``.""" if colors is None: colors = self.colors if bright_is_bold is None: bright_is_bold = self.bright_is_bold if has_underline is None: has_underline = self.has_underline self.colors = colors self.bright_is_bold = bright_is_bold self.has_underline = has_underline def set_input_timeouts(self, *args: typing.Any) -> None: """Do nothing; this simulated screen has no real input source to configure timeouts on.""" def reset_default_terminal_palette(self, *args: typing.Any) -> None: """Do nothing; this simulated screen has no real terminal palette to reset.""" def draw_screen(self, size: tuple[int, int], canvas: Canvas) -> None: """Create an html fragment from the render object. Append it to HtmlGenerator.fragments list. :raises ValueError: *canvas* does not have the number of rows given by *size*. """ # collect output in l lines = [] _cols, rows = size if canvas.rows() != rows: raise ValueError(rows) if canvas.cursor is not None: cx, cy = canvas.cursor else: cx = cy = None for y, row in enumerate(canvas.content()): col = 0 for a, _cs, run in row: t_run = run.decode(get_encoding()).translate(_trans_table) if isinstance(a, AttrSpec): aspec = a else: aspec = self._palette[a][{1: 1, 16: 0, 88: 2, 256: 3}[self.colors]] if y == cy and col <= cx: run_width = str_util.calc_width(t_run, 0, len(t_run)) if col + run_width > cx: lines.append(html_span(t_run, aspec, cx - col)) else: lines.append(html_span(t_run, aspec)) col += run_width else: lines.append(html_span(t_run, aspec)) lines.append("\n") # add the fragment to the list self.fragments.append(f"
{''.join(lines)}
") def get_cols_rows(self) -> tuple[int, int]: """Return the next screen size in HtmlGenerator.sizes. :raises HtmlGeneratorSimulationError: the list of simulated sizes is exhausted. """ if not self.sizes: raise HtmlGeneratorSimulationError("Ran out of screen sizes to return!") return self.sizes.pop(0) @typing.overload def get_input(self, raw_keys: Literal[False]) -> _DecodedInput: ... @typing.overload def get_input(self, raw_keys: Literal[True]) -> tuple[_DecodedInput, list[int]]: ... def get_input(self, raw_keys: bool = False) -> _DecodedInput | tuple[_DecodedInput, list[int]]: """Return the next list of keypresses in HtmlGenerator.keys. :raises ExitMainLoop: the list of simulated key presses is exhausted. """ if not self.keys: raise ExitMainLoop() if raw_keys: return (self.keys.pop(0), []) return self.keys.pop(0) def html_span(s: str, aspec: AttrSpec, cursor: int = -1) -> str: """Wrap *s* in an HTML ```` styled from *aspec*, splitting it around *cursor* when given a valid index.""" html_fg, html_bg, extra = attr_spec_to_css(aspec) def _span(fg: str, bg: str, string: str) -> str: if not s: return "" return f'{html.escape(string)}' if cursor >= 0: c_off, _ign = str_util.calc_text_pos(s, 0, len(s), cursor) c2_off = str_util.move_next_char(s, c_off, len(s)) return ( _span(html_fg, html_bg, s[:c_off]) + _span(html_bg, html_fg, s[c_off:c2_off]) + _span(html_fg, html_bg, s[c2_off:]) ) return _span(html_fg, html_bg, s) def screenshot_init( sizes: list[tuple[int, int]], keys: list[_DecodedInput], ) -> None: """Replace curses_display.Screen and raw_display.Screen class with HtmlGenerator. Call this function before executing an application that uses curses_display.Screen to have that code use HtmlGenerator instead. :param sizes: list of ( columns, rows ) tuples to be returned by each call to HtmlGenerator.get_cols_rows() :param keys: list of lists of keys to be returned by each call to HtmlGenerator.get_input() :raises TypeError: *sizes* is not a list of integer pairs, or *keys* is not a list of lists of strings. :raises ValueError: a size has a non-positive number of columns or rows. Lists of keys may include "window resize" to force the application to call get_cols_rows and read a new screen size. For example, the following call will prepare an application to: 1. start in 80x25 with its first call to get_cols_rows() 2. take a screenshot when it calls draw_screen(..) 3. simulate 5 :kbd:`down` keys from get_input() 4. take a screenshot when it calls draw_screen(..) 5. simulate keys :kbd:`a`, :kbd:`b`, :kbd:`c` and a ``"window resize"`` event 6. resize to 20x10 on its second call to get_cols_rows() 7. take a screenshot when it calls draw_screen(..) 8. simulate a "Q" keypress to quit the application screenshot_init( [ (80,25), (20,10) ], [ ["down"]*5, ["a","b","c","window resize"], ["Q"] ] ) .. deprecated:: 4.1.8 Patching ``curses_display.Screen``/``raw_display.Screen`` in place is deprecated and will stop working in version 6.0. Assign :class:`HtmlGenerator` to your application's screen manually instead. """ warnings.warn( "screenshot_init patches curses.Screen and raw.Screen in place; this will stop working in version 6.0. " "Assign HtmlGenerator to your application's screen manually instead.", DeprecationWarning, stacklevel=2, ) for row, col in sizes: if not isinstance(row, int): raise TypeError(f"sizes must be list[tuple[int, int]], with values >0 : {row!r}") if row <= 0: raise ValueError(f"sizes must be list[tuple[int, int]], with values >0 : {row!r}") if not isinstance(col, int): raise TypeError(f"sizes must be list[tuple[int, int]], with values >0 : {col!r}") if col <= 0: raise ValueError(f"sizes must be list[tuple[int, int]], with values >0 : {col!r}") for line in keys: if not isinstance(line, list): raise TypeError(f"keys must be list[list[str]]: {line!r}") for k in line: if not isinstance(k, str): raise TypeError(f"keys must be list[list[str]]: {k!r}") from . import raw try: from . import curses except (ImportError, AttributeError): pass # the stdlib "curses" module is not available on this platform (e.g. Windows) else: curses.Screen = HtmlGenerator # type: ignore[assignment,misc] raw.Screen = HtmlGenerator # type: ignore[assignment,misc] HtmlGenerator.sizes = sizes HtmlGenerator.keys = keys def screenshot_collect() -> list[str]: """Return screenshots as a list of HTML fragments.""" fragments, HtmlGenerator.fragments = HtmlGenerator.fragments, [] return fragments urwid-urwid-69907e4/urwid/display/lcd.py000066400000000000000000000462501526073734300202210ustar00rootroot00000000000000# Urwid LCD display module # Copyright (C) 2010 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """Display modules for character LCD panels, including the Crystalfontz CF635.""" from __future__ import annotations import abc import time import typing from .common import BaseScreen if typing.TYPE_CHECKING: from collections.abc import Collection, Iterable, Sequence from typing_extensions import Literal, SupportsIndex from urwid import Canvas class LCDScreen(BaseScreen, abc.ABC): """Base class for LCD-based screens.""" DISPLAY_SIZE: tuple[int, int] def set_terminal_properties( self, colors: Literal[1, 16, 88, 256, 16777216] | None = None, bright_is_bold: bool | None = None, has_underline: bool | None = None, ) -> None: """Set terminal attributes. A no-op here; an LCD screen has no colors, bold, or underline to configure.""" def set_input_timeouts(self, *args: typing.Any) -> None: """Set the maximum time in seconds to wait for input characters. A no-op here.""" def reset_default_terminal_palette(self, *args: typing.Any) -> None: """Reset the terminal's default palette. A no-op here; an LCD screen has no palette to reset.""" def get_cols_rows(self) -> tuple[int, int]: """Return the size of the LCD display as ``(cols, rows)``.""" return self.DISPLAY_SIZE class CFLCDScreen(LCDScreen, abc.ABC): """Common methods for Crystal Fonts LCD displays.""" KEYS: typing.ClassVar[list[str | None]] = [ None, # no key with code 0 "up_press", "down_press", "left_press", "right_press", "enter_press", "exit_press", "up_release", "down_release", "left_release", "right_release", "enter_release", "exit_release", "ul_press", "ur_press", "ll_press", "lr_press", "ul_release", "ur_release", "ll_release", "lr_release", ] CMD_PING = 0 CMD_VERSION = 1 CMD_CLEAR = 6 CMD_CGRAM = 9 CMD_CURSOR_POSITION = 11 # data = [col, row] CMD_CURSOR_STYLE = 12 # data = [style (0-4)] CMD_LCD_CONTRAST = 13 # data = [contrast (0-255)] CMD_BACKLIGHT = 14 # data = [power (0-100)] CMD_LCD_DATA = 31 # data = [col, row] + text CMD_GPO = 34 # data = [pin(0-12), value(0-100)] # sent from device CMD_KEY_ACTIVITY = 0x80 CMD_ACK = 0x40 # in high two bits ie. & 0xc0 CURSOR_NONE = 0 CURSOR_BLINKING_BLOCK = 1 CURSOR_UNDERSCORE = 2 CURSOR_BLINKING_BLOCK_UNDERSCORE = 3 CURSOR_INVERTING_BLINKING_BLOCK = 4 MAX_PACKET_DATA_LENGTH = 22 colors = 1 has_underline = False def __init__(self, device_path: str, baud: int) -> None: """Open the serial connection. :param device_path: serial device to talk to, e.g. :file:`/dev/ttyUSB0` :param baud: baud rate """ super().__init__() self.device_path = device_path from serial import Serial self._device = Serial(device_path, baud, timeout=0) self._unprocessed = bytearray() @classmethod def get_crc(cls, buf: Iterable[int]) -> bytes: """Return the 16-bit CRC of *buf*, as the two bytes sent in a packet's checksum field.""" # This seed makes the output of this shift-based algorithm match # the table-based algorithm. The center 16 bits of the 32-bit # "newCRC" are used for the CRC. The MSB of the lower byte is used # to see what bit was shifted out of the center 16 bit CRC # accumulator ("carry flag analog"); new_crc = 0x00F32100 for byte in buf: # Push this byte's bits through a software # implementation of a hardware shift & xor. for bit_count in range(8): # Shift the CRC accumulator new_crc >>= 1 # The new MSB of the CRC accumulator comes # from the LSB of the current data byte. if byte & (0x01 << bit_count): new_crc |= 0x00800000 # If the low bit of the current CRC accumulator was set # before the shift, then we need to XOR the accumulator # with the polynomial (center 16 bits of 0x00840800) if new_crc & 0x00000080: new_crc ^= 0x00840800 # All the data has been done. Do 16 more bits of 0 data. for _bit_count in range(16): # Shift the CRC accumulator new_crc >>= 1 # If the low bit of the current CRC accumulator was set # before the shift we need to XOR the accumulator with # 0x00840800. if new_crc & 0x00000080: new_crc ^= 0x00840800 # Return the center 16 bits, making this CRC match the one's # complement that is sent in the packet. return (((~new_crc) >> 8) & 0xFFFF).to_bytes(2, "little") def _send_packet(self, command: int, data: Collection[SupportsIndex]) -> None: """Send a low-level packet. Following the protocol requires waiting for ack packet between sending each packet to the device. """ buf = bytearray([command, len(data)]) buf.extend(data) buf.extend(self.get_crc(buf)) self._device.write(buf) def _read_packet(self) -> tuple[int, bytearray] | None: """Read a low-level packet. Returns ``(command/report code, data)`` or ``None``. This method stores data read and tries to resync when bad data is received. """ # pull in any new data available self._unprocessed += self._device.read() while True: try: command, data, unprocessed = self._parse_data(self._unprocessed) self._unprocessed = unprocessed except self.MoreDataRequired: # noqa: PERF203 return None except self.InvalidPacket: # throw out a byte and try to parse again self._unprocessed = self._unprocessed[1:] else: return command, data class InvalidPacket(Exception): """Raised when a packet is longer than the protocol allows, or its CRC does not match.""" class MoreDataRequired(Exception): """Raised when the buffered data does not yet hold a complete packet.""" @classmethod def _parse_data(cls, data: bytearray) -> tuple[int, bytearray, bytearray]: """Try to read a packet from the start of *data*. Returns ``(command/report code, packet_data, remaining_data)``. :raises MoreDataRequired: *data* does not yet hold a complete packet. :raises InvalidPacket: the packet is longer than the protocol allows, or its CRC does not match. """ if len(data) < 2: raise cls.MoreDataRequired command: int = data[0] packet_len: int = data[1] if packet_len > cls.MAX_PACKET_DATA_LENGTH: raise cls.InvalidPacket("length value too large") if len(data) < packet_len + 4: raise cls.MoreDataRequired data_end = 2 + packet_len if (crc := cls.get_crc(data[:data_end])) != (packet_crc := data[data_end : data_end + 2]): raise cls.InvalidPacket(f"CRC doesn't match ({crc=}, {packet_crc=})") return command, data[2:data_end], data[data_end + 2 :] class KeyRepeatSimulator: """Provide simulated repeat key events when given press and release events. If two or more keys are pressed, disable repeating until all keys are released. """ def __init__(self, repeat_delay: float, repeat_next: float) -> None: """Set up the repeat timing. :param repeat_delay: seconds to wait before starting to repeat keys :param repeat_next: time between each repeated key """ self.repeat_delay = repeat_delay self.repeat_next = repeat_next self.pressed: dict[str, float] = {} self.multiple_pressed = False def press(self, key: str) -> None: """Record that *key* was pressed, disabling repeat if another key is already held down.""" if self.pressed: self.multiple_pressed = True self.pressed[key] = time.time() def release(self, key: str) -> None: """Record that *key* was released, re-enabling repeat once no key is held down.""" if key not in self.pressed: return # ignore extra release events del self.pressed[key] if not self.pressed: self.multiple_pressed = False def next_event(self) -> tuple[float, str] | None: """Return ``(remaining, key)``, or ``None`` if no events are pending. *remaining* is the number of seconds (float) until the key repeat event should be sent. """ if len(self.pressed) != 1 or self.multiple_pressed: return None for key, val in self.pressed.items(): return max(0.0, val + self.repeat_delay - time.time()), key return None def sent_event(self) -> None: """Call this method when a key repeat event was sent. The timer will be reset for the next event. """ if len(self.pressed) != 1: return # ignore event that shouldn't have been sent for key in self.pressed: self.pressed[key] = time.time() - self.repeat_delay + self.repeat_next return class CF635Screen(CFLCDScreen): """ Crystal Fontz 635 display. 20x4 character display + cursor no foreground/background colors or settings supported see CGROM for a list of close Unicode matches to characters available 6 button input up, down, left, right, enter (check mark), exit (cross) """ DISPLAY_SIZE = (20, 4) # ① through ⑧ are programmable CGRAM (chars 0-7, repeated at 8-15) # double arrows (⇑⇓) appear as double arrowheads (chars 18, 19) # ⑴ resembles a bell # ⑵ resembles a filled-in "Y" # ⑶ is the letters "Pt" together # partial blocks (▇▆▄▃�) are actually shorter versions of (▉▋▌��) # both groups are intended to draw horizontal bars with pixel # precision, use ▇*[▆▄▃�]? for a thin bar or ▉*[▋▌��]? for a thick bar CGROM = ( "①②③④⑤⑥⑦⑧①②③④⑤⑥⑦⑧" "►◄⇑⇓«»↖↗↙↘▲▼↲^ˇ█" " !\"#¤%&'()*+,-./" "0123456789:;<=>?" "¡ABCDEFGHIJKLMNO" "PQRSTUVWXYZÄÖÑܧ" "¿abcdefghijklmno" "pqrstuvwxyzäöñüà" "�¹²³������½¼±≥≤μ" "♪♫⑴♥♦⑵⌜⌟“�()αɛδ∞" "@£$¥èéùìòÇᴾØøʳÅå" "⌂¢ΦτλΩπΨΣθΞ♈ÆæßÉ" "ΓΛΠϒ_ÈÊêçğŞşİι~◊" "▇▆▄▃�ƒ▉▋▌��⑶◽▪↑→" "↓���ÓÚ�áíóúýÔôŮů" r"ČĔŘŠŽ�ĕřšž[\]{|}" ) cursor_style = CFLCDScreen.CURSOR_INVERTING_BLINKING_BLOCK def __init__( self, device_path: str, baud: int = 115200, repeat_delay: float = 0.5, repeat_next: float = 0.125, key_map: Iterable[str] = ("up", "down", "left", "right", "enter", "esc"), ): """Open the serial connection and set up key repeat handling. :param device_path: serial device to talk to, e.g. :file:`/dev/ttyUSB0` :param baud: baud rate :param repeat_delay: seconds to wait before starting to repeat keys :param repeat_next: time between each repeated key :param key_map: the keys to send for this device's buttons """ super().__init__(device_path, baud) self.repeat_delay = repeat_delay self.repeat_next = repeat_next self.key_repeat = KeyRepeatSimulator(repeat_delay, repeat_next) self.key_map = tuple(key_map) self._last_command: int | None = None self._last_command_time = 0.0 self._command_queue: list[tuple[int, bytearray]] = [] self._screen_buf: list[list[bytes]] = [] self._previous_canvas: Canvas | None = None self._update_cursor = False def get_input_descriptors(self) -> list[int]: """Return the fd from our serial device so we get called on input and responses.""" return [self._device.fd] def get_input_nonblocking(self) -> tuple[float | None, list[str], list[int]]: """Return a ``(next_input_timeout, keys_pressed, raw_keycodes)`` tuple. The protocol for our device requires waiting for acks between each command, so this method responds to those as well as key press and release events. Key repeat events are simulated here as the device doesn't send any for us. raw_keycodes are the bytes of messages we received, which might not seem to have any correspondence to keys_pressed. """ data_input: list[str] = [] raw_data_input: list[int] = [] timeout = None packet = self._read_packet() while packet: command, data = packet if command == self.CMD_KEY_ACTIVITY and data: d0 = data[0] if 1 <= d0 <= 12: release = d0 > 6 keycode = d0 - (release * 6) - 1 key = self.key_map[keycode] if release: self.key_repeat.release(key) else: data_input.append(key) self.key_repeat.press(key) raw_data_input.append(d0) elif command & 0xC0 == 0x40 and command & 0x3F == self._last_command: # "ACK" self._send_next_command() packet = self._read_packet() if next_repeat := self.key_repeat.next_event(): timeout, key = next_repeat if not timeout: data_input.append(key) self.key_repeat.sent_event() timeout = None return timeout, data_input, raw_data_input def _send_next_command(self) -> None: """Send out the next command in the queue.""" if not self._command_queue: self._last_command = None return command, data = self._command_queue.pop(0) self._send_packet(command, data) self._last_command = command # record command for ACK self._last_command_time = time.time() def queue_command(self, command: int, data: bytearray) -> None: """Queue *command* with *data* for sending, sending it immediately if no command is awaiting an ACK.""" self._command_queue.append((command, data)) # not waiting? send away! if self._last_command is None: self._send_next_command() def draw_screen(self, size: tuple[int, int], canvas: Canvas) -> None: """ Send *canvas* to the display. :raises ValueError: *size* is not the fixed size of this display. """ if size != self.DISPLAY_SIZE: raise ValueError(size) if self._screen_buf: osb = self._screen_buf else: osb = [] sb: list[list[bytes]] = [] for y, row in enumerate(canvas.content()): text: list[bytes] = [run for _a, _cs, run in row] if not osb or osb[y] != text: data = bytearray([0, y]) for elem in text: data.extend(elem) self.queue_command(self.CMD_LCD_DATA, data) sb.append(text) if ( self._previous_canvas and self._previous_canvas.cursor == canvas.cursor and (not self._update_cursor or not canvas.cursor) ): pass elif canvas.cursor is None: self.queue_command(self.CMD_CURSOR_STYLE, bytearray([self.CURSOR_NONE])) else: x, y = canvas.cursor self.queue_command(self.CMD_CURSOR_POSITION, bytearray([x, y])) self.queue_command(self.CMD_CURSOR_STYLE, bytearray([self.cursor_style])) self._update_cursor = False self._screen_buf = sb self._previous_canvas = canvas def program_cgram(self, index: int, data: Sequence[int]) -> None: """ Program character data. Characters available as chr(0) through chr(7), and repeated as chr(8) through chr(15). :param index: 0 to 7 index of character to program :param data: list of 8, 6-bit integer values top to bottom with MSB on the left side of the character. :raises ValueError: *index* is outside 0-7, or *data* is not 8 rows. """ if not 0 <= index <= 7: raise ValueError(index) if len(data) != 8: raise ValueError(data) self.queue_command(self.CMD_CGRAM, bytearray([index]) + bytearray(data)) def set_cursor_style(self, style: Literal[1, 2, 3, 4]) -> None: """Set the cursor style. :param style: CURSOR_BLINKING_BLOCK, CURSOR_UNDERSCORE, CURSOR_BLINKING_BLOCK_UNDERSCORE or CURSOR_INVERTING_BLINKING_BLOCK :raises ValueError: *style* is not one of the four cursor styles. """ if not 1 <= style <= 4: raise ValueError(style) self.cursor_style = style self._update_cursor = True def set_backlight(self, value: int) -> None: """ Set backlight brightness. :param value: 0 to 100 :raises ValueError: *value* is outside 0-100. """ if not 0 <= value <= 100: raise ValueError(value) self.queue_command(self.CMD_BACKLIGHT, bytearray([value])) def set_lcd_contrast(self, value: int) -> None: """Set the LCD contrast. :param value: 0 to 255 :raises ValueError: *value* is outside 0-255. """ if not 0 <= value <= 255: raise ValueError(value) self.queue_command(self.CMD_LCD_CONTRAST, bytearray([value])) def set_led_pin(self, led: Literal[0, 1, 2, 3], rg: Literal[0, 1], value: int) -> None: """Set an LED pin's brightness. :param led: 0 to 3 :param rg: 0 for red, 1 for green :param value: 0 to 100 :raises ValueError: *led* is outside 0-3, *rg* is not ``0`` or ``1``, or *value* is outside 0-100. """ if not 0 <= led <= 3: raise ValueError(led) if rg not in {0, 1}: raise ValueError(rg) if not 0 <= value <= 100: raise ValueError(value) self.queue_command(self.CMD_GPO, bytearray([12 - 2 * led - rg, value])) urwid-urwid-69907e4/urwid/display/raw.py000066400000000000000000000021131526073734300202360ustar00rootroot00000000000000# Urwid raw display module # Copyright (C) 2004-2009 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """Direct terminal UI implementation.""" from __future__ import annotations import sys __all__ = ("Screen",) if sys.platform == "win32": from ._win32_raw_display import Screen else: from ._posix_raw_display import Screen urwid-urwid-69907e4/urwid/display/web.py000066400000000000000000000545001526073734300202310ustar00rootroot00000000000000# Urwid web (CGI/Asynchronous Javascript) display module # Copyright (C) 2004-2007 Ian Ward # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Urwid web site: https://urwid.org/ """Urwid web application display module.""" from __future__ import annotations import dataclasses import glob import html import os import pathlib import secrets import selectors import signal import socket import string import sys import tempfile import typing from contextlib import suppress from email.message import Message from urwid.str_util import calc_text_pos, calc_width, move_next_char from urwid.util import StoppingContext, get_encoding from .common import AttrSpec, BaseScreen, attr_spec_to_css if typing.TYPE_CHECKING: from types import FrameType from typing_extensions import Literal from urwid.canvas import Canvas TEMP_DIR = tempfile.gettempdir() CURRENT_DIR = pathlib.Path(__file__).parent _js_code = CURRENT_DIR.joinpath("_web.js").read_text("utf-8") ALARM_DELAY = 60 POLL_CONNECT = 3 MAX_COLS = 200 MAX_ROWS = 100 MAX_READ = 4096 BUF_SZ = 16384 # Characters that may appear in an id produced by secrets.token_urlsafe(): # the URL-safe base64 alphabet. Used to validate client-supplied ids before # they are interpolated into pipe file names. _URWID_ID_CHARS = frozenset(string.ascii_letters + string.digits + "-_") _URWID_ID_MAX_LEN = 43 # len(secrets.token_urlsafe(32)); generous upper bound # replace control characters with ?'s _trans_table = str.maketrans(dict.fromkeys(range(32), "?")) _css_style = CURRENT_DIR.joinpath("_web.css").read_text("utf-8") # HTML Initial Page _html_page = [ """ Urwid Web Display - """, """

The quick brown fox jumps over the lazy dog.X
Y
Urwid Web Display - """, """ - Status: Set up



""",
]


#: index into the (basic, mono, high_88, high_256, high_true) tuple stored per palette entry,
#: selected by the ``colors`` value passed to :meth:`Screen.set_terminal_properties`
_COLOUR_INDEX = {1: 1, 16: 0, 88: 2, 256: 3, 2**24: 4}

_default_foreground = "black"
_default_background = "light gray"


class Screen(BaseScreen):
    """Screen backend that renders to a browser client over the web server's polling/streaming connection."""

    def __init__(self) -> None:
        """Initialize an unstarted screen defaulting to true color."""
        super().__init__()
        self.has_color = True
        self._started = False
        self.colors = 2**24  # browsers render arbitrary RGB, so default to true color
        self.bright_is_bold = False  # ignored: the browser renders bold as requested
        self.has_underline = True  # ignored: the browser renders underline as requested
        self._colour_index = _COLOUR_INDEX[self.colors]
        self.update_method = ""
        self.last_screen: dict[tuple[tuple[AttrSpec | str | None, str] | int | None, ...], list[int]] = {}
        self.last_screen_width = 0
        self.pipe_name = ""
        self.input_fd: int | None = None
        self.input_tail = ""
        self.content_head = ""
        self.screen_size: tuple[int, int] | None = None
        self.server_socket: socket.socket | None = None
        self.register_palette_entry(None, _default_foreground, _default_background)

    @property
    def started(self) -> bool:
        """Return whether this screen has been started."""
        return self._started

    def set_terminal_properties(
        self,
        colors: int | None = None,
        bright_is_bold: bool | None = None,
        has_underline: bool | None = None,
    ) -> None:
        """Set the number of colors used to resolve named palette entries.

        :param colors: one of 1, 16, 88, 256 or 2**24 (2**24, true color, is the default,
            since a browser can render any RGB color directly)
        :param bright_is_bold: ignored, kept for API parity with the other display modules
        :param has_underline: ignored, kept for API parity with the other display modules
        :raises KeyError: *colors* is not one of the supported palette sizes.
        """
        if colors is not None:
            self._colour_index = _COLOUR_INDEX[colors]
            self.colors = colors
        if bright_is_bold is not None:
            self.bright_is_bold = bright_is_bold
        if has_underline is not None:
            self.has_underline = has_underline

    def _handle_resize_request(self, request: str) -> bool:
        """Apply the screen size from a "window resize  " request.

        :param request: single input line without the trailing newline.
        :returns: True if the request was a valid resize command and the screen size was updated.
        """
        if not request.startswith("window resize "):
            return False

        input_resize = request.removeprefix("window resize ").split(" ")
        if len(input_resize) != 2:
            self.logger.debug("Invalid resize input format: %r", request)
            return False

        x, y = input_resize
        if not x.isdecimal() or not y.isdecimal():
            self.logger.debug("Invalid resize input format: %r", request)
            return False

        self._set_screen_size(int(x), int(y))
        return True

    def set_mouse_tracking(self, enable: bool = True) -> None:
        """Not yet implemented."""

    def tty_signal_keys(self, *args: typing.Any, **vargs: typing.Any) -> None:
        """Do nothing."""

    def start(self, *args: typing.Any, **kwargs: typing.Any) -> StoppingContext:
        """Read the initial screen size, generate a unique id and handle cleanup when fn exits.

        web_display.set_preferences(..) must be called before calling this function for the preferences to take effect

        :raises RuntimeError: the ``HTTP_X_URWID_METHOD`` environment variable is not set.
        """
        if self._started:
            return StoppingContext(self)

        self.update_method = os.environ.get("HTTP_X_URWID_METHOD", "")
        if not self.update_method:
            raise RuntimeError("'HTTP_X_URWID_METHOD' environment vairable is not set")

        if self.update_method not in {"multipart", "polling"}:
            self.logger.debug("Unsupported update method requested: %r", self.update_method)
            sys.stdout.write("Status: 400 Bad Request\r\n\r\n")
            sys.exit(0)

        if self.update_method == "polling" and not _prefs.allow_polling:
            sys.stdout.write("Status: 403 Forbidden\r\n\r\n")
            sys.exit(0)

        client_init = sys.stdin.read(50)
        if not self._handle_resize_request(client_init.split("\n", 1)[0].strip()):
            self.logger.debug("Invalid initial client request: %r", client_init)
            sys.stdout.write("Status: 400 Bad Request\r\n\r\n")
            sys.exit(0)

        self.last_screen = {}
        self.last_screen_width = 0

        clients = glob.glob(os.path.join(_prefs.pipe_dir, "urwid*.in"))
        if len(clients) >= _prefs.max_clients:
            sys.stdout.write("Status: 503 Sever Busy\r\n\r\n")
            sys.exit(0)

        urwid_id = secrets.token_urlsafe(16)
        self.pipe_name = os.path.join(_prefs.pipe_dir, f"urwid_{urwid_id}")
        os.mkfifo(f"{self.pipe_name}.in", 0o600)
        signal.signal(signal.SIGTERM, self._cleanup_pipe)

        self.input_fd = os.open(f"{self.pipe_name}.in", os.O_NONBLOCK | os.O_RDONLY)
        self.input_tail = ""
        self.content_head = (
            f"Content-type: multipart/x-mixed-replace;boundary=ZZ\r\nX-Urwid-ID: {urwid_id}\r\n\r\n\r\n--ZZ\r\n"
        )
        if self.update_method == "polling":
            self.content_head = f"Content-type: text/plain\r\nX-Urwid-ID: {urwid_id}\r\n\r\n\r\n"

        signal.signal(signal.SIGALRM, self._handle_alarm)
        signal.alarm(ALARM_DELAY)
        self._started = True

        return StoppingContext(self)

    def stop(self) -> None:
        """Restore settings and clean up."""
        if not self._started:
            return

        # XXX which exceptions does this actually raise? EnvironmentError?
        with suppress(Exception):
            self._close_connection()
        signal.signal(signal.SIGTERM, signal.SIG_DFL)
        if self.input_fd is not None:
            with suppress(Exception):
                os.close(self.input_fd)
        self._cleanup_pipe()
        self._started = False

    def set_input_timeouts(self, *args: typing.Any) -> None:
        """Not supported for web display."""

    def _close_connection(self) -> None:
        if self.update_method == "polling child" and self.server_socket is not None:
            self.server_socket.settimeout(0)
            sock, _addr = self.server_socket.accept()
            sock.sendall(b"Z")
            sock.close()

        if self.update_method == "multipart":
            sys.stdout.write("\r\nZ\r\n--ZZ--\r\n")
            sys.stdout.flush()

    def _cleanup_pipe(self, *args: typing.Any) -> None:
        if not self.pipe_name:
            return
        # XXX which exceptions does this actually raise? EnvironmentError?
        with suppress(Exception):
            os.remove(f"{self.pipe_name}.in")
            os.remove(f"{self.pipe_name}.update")

    def _set_screen_size(self, cols: int, rows: int) -> None:
        """Set the screen size (within max size)."""
        cols = min(cols, MAX_COLS)
        rows = min(rows, MAX_ROWS)
        self.screen_size = cols, rows

    def draw_screen(self, size: tuple[int, int], canvas: Canvas) -> None:
        """Send a screen update to the client.

        :raises ValueError: *canvas* does not have the number of rows given by *size*.
        """
        (cols, rows) = size
        encoding = get_encoding()

        if cols != self.last_screen_width:
            self.last_screen = {}

        sendq = [self.content_head]

        if self.update_method == "polling":
            send = sendq.append
        elif self.update_method == "polling child":
            signal.alarm(0)
            try:
                s, _addr = typing.cast("socket.socket", self.server_socket).accept()
            except socket.timeout:
                sys.exit(0)
            send = s.sendall  # type: ignore[assignment]  # use default flags
        else:
            signal.alarm(0)
            send = sendq.append
            send("\r\n")
            self.content_head = ""

        if canvas.rows() != rows:
            raise ValueError(rows)

        if canvas.cursor is not None:
            cx, cy = canvas.cursor
        else:
            cx = cy = None

        new_screen: dict[tuple[tuple[AttrSpec | str | None, str] | int | None, ...], list[int]] = {}

        y = -1
        for row in canvas.content():
            y += 1
            l_row = tuple((attr_, line.decode(encoding)) for attr_, _, line in row)

            line = []

            sig: tuple[tuple[AttrSpec | str | None, str] | int | None, ...] = l_row
            if y == cy:
                sig = (*sig, cx)
            new_screen.setdefault(sig, []).append(y)

            if (old_line_numbers := self.last_screen.get(sig, None)) is not None:
                if y in old_line_numbers:
                    old_line = y
                else:
                    old_line = old_line_numbers[0]
                send(f"<{old_line:d}\n")
                continue

            col = 0
            for a, run in l_row:
                t_run = run.translate(_trans_table)
                if isinstance(a, AttrSpec):
                    aspec = a
                else:
                    aspec = self._palette[a][self._colour_index]
                if y == cy and col <= cx:
                    run_width = calc_width(t_run, 0, len(t_run))
                    if col + run_width > cx:
                        line.append(code_span(t_run, aspec, cx - col))
                    else:
                        line.append(code_span(t_run, aspec))
                    col += run_width
                else:
                    line.append(code_span(t_run, aspec))

            send(f"{''.join(line)}\n")
        self.last_screen = new_screen
        self.last_screen_width = cols

        if self.update_method == "polling":
            sys.stdout.write("".join(sendq))
            sys.stdout.flush()
            sys.stdout.close()
            self._fork_child()
        elif self.update_method == "polling child":
            s.close()
        else:  # update_method == "multipart"
            send("\r\n--ZZ\r\n")
            sys.stdout.write("".join(sendq))
            sys.stdout.flush()

        signal.alarm(ALARM_DELAY)

    def clear(self) -> None:
        """Force the screen to be completely repainted on the next call to draw_screen().

        (does nothing for web_display)
        """

    def _fork_child(self) -> None:
        """Fork a child to run CGI disconnected for polling update method.

        Force parent process to exit.
        """
        daemonize(f"{self.pipe_name}.err")
        self.input_fd = os.open(f"{self.pipe_name}.in", os.O_NONBLOCK | os.O_RDONLY)
        self.update_method = "polling child"
        s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        s.bind(f"{self.pipe_name}.update")
        s.listen(1)
        s.settimeout(POLL_CONNECT)
        self.server_socket = s

    def _handle_alarm(self, sig: int, frame: FrameType | None) -> None:
        """
        Handle the periodic alarm that keeps the browser connection alive.

        :raises ValueError: the update method is neither multipart nor a polling child.
        """
        if self.update_method not in {"multipart", "polling child"}:
            raise ValueError(self.update_method)
        if self.update_method == "polling child":
            # send empty update
            try:
                s, _addr = typing.cast("socket.socket", self.server_socket).accept()
                s.close()
            except socket.timeout:
                sys.exit(0)
        else:
            # send empty update
            sys.stdout.write("\r\n\r\n--ZZ\r\n")
            sys.stdout.flush()
        signal.alarm(ALARM_DELAY)

    def get_cols_rows(self) -> tuple[int, int]:
        """Return the screen size."""
        return typing.cast("tuple[int, int]", self.screen_size)

    @typing.overload
    def get_input(self, raw_keys: Literal[False]) -> list[str]: ...

    @typing.overload
    def get_input(self, raw_keys: Literal[True]) -> tuple[list[str], list[int]]: ...

    def get_input(self, raw_keys: bool = False) -> list[str] | tuple[list[str], list[int]]:
        """Return pending input as a list."""
        pending_input = []
        resized = False
        input_fd = typing.cast("int", self.input_fd)
        with selectors.DefaultSelector() as selector:
            selector.register(input_fd, selectors.EVENT_READ)

            iready = [event.fd for event, _ in selector.select(0.5)]

        if not iready:
            if raw_keys:
                return [], []
            return []

        keydata = os.read(input_fd, MAX_READ).decode(get_encoding())
        os.close(input_fd)
        self.input_fd = os.open(f"{self.pipe_name}.in", os.O_NONBLOCK | os.O_RDONLY)
        # sys.stderr.write( repr((keydata,self.input_tail))+"\n" )
        keys = keydata.split("\n")
        keys[0] = self.input_tail + keys[0]
        self.input_tail = keys[-1]

        for k in keys[:-1]:
            if self._handle_resize_request(k):
                resized = True
            else:
                pending_input.append(k)
        if resized:
            pending_input.append("window resize")

        if raw_keys:
            return pending_input, []
        return pending_input


# the separator between a span's inline CSS and its text content in the wire format;
# safe because control characters in the text have already been replaced by _trans_table
_STYLE_SEP = "\x01"


def code_span(s: str, aspec: AttrSpec, cursor: int = -1) -> str:
    """Return `s` wrapped in an HTML ```` span styled per `aspec`, with the cursor column highlighted."""
    fg, bg, extra = attr_spec_to_css(aspec)

    def _piece(fg_: str, bg_: str, text: str) -> str:
        return f"color:{fg_};background-color:{bg_}{extra}{_STYLE_SEP}{text}\n"

    if cursor >= 0:
        c_off, _ign = calc_text_pos(s, 0, len(s), cursor)
        c2_off = move_next_char(s, c_off, len(s))

        return _piece(fg, bg, s[:c_off]) + _piece(bg, fg, s[c_off:c2_off]) + _piece(fg, bg, s[c2_off:])

    return _piece(fg, bg, s)


def is_web_request() -> bool:
    """Return True if this is a CGI web request."""
    return "REQUEST_METHOD" in os.environ


def _request_charset() -> str:
    content_type = Message()
    content_type["content-type"] = os.environ.get("CONTENT_TYPE", "")
    return content_type.get_content_charset() or "utf-8"


def handle_short_request() -> bool:
    """Handle short requests such as passing keystrokes to the application or sending the initial HTML page.

    If returns True, then this function recognized and handled a short request, and the calling
    script should immediately exit.

    web_display.set_preferences(..) should be called before calling this
    function for the preferences to take effect.
    """
    if not is_web_request():
        return False

    if os.environ["REQUEST_METHOD"] == "GET":
        # Initial request, send the HTML and javascript.
        sys.stdout.write("Content-type: text/html\r\n\r\n" + html.escape(_prefs.app_name).join(_html_page))
        return True

    if os.environ["REQUEST_METHOD"] != "POST":
        # Don't know what to do with head requests etc.
        return False

    if "HTTP_X_URWID_ID" not in os.environ:
        # If no urwid id, then the application should be started.
        return False

    urwid_id = os.environ["HTTP_X_URWID_ID"]
    if len(urwid_id) > _URWID_ID_MAX_LEN:
        # invalid. handle by ignoring
        # assert 0, "urwid id too long!"
        sys.stdout.write("Status: 414 URI Too Long\r\n\r\n")
        return True
    if any(c not in _URWID_ID_CHARS for c in urwid_id):
        # invalid. handle by ignoring
        # assert 0, "invalid chars in id!"
        sys.stdout.write("Status: 403 Forbidden\r\n\r\n")
        return True

    if os.environ.get("HTTP_X_URWID_METHOD", None) == "polling":
        # this is a screen update request
        try:
            with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
                s.connect(os.path.join(_prefs.pipe_dir, f"urwid_{urwid_id}.update"))
                chunks = []
                while data := s.recv(BUF_SZ):
                    chunks.append(data)
        except OSError:
            sys.stdout.write("Status: 404 Not Found\r\n\r\n")
            return True

        try:
            decoded = b"".join(chunks).decode("utf-8")
        except UnicodeDecodeError:
            sys.stdout.write("Status: 502 Bad Gateway\r\n\r\n")
            return True

        sys.stdout.write(f"Content-type: text/plain; charset=utf-8\r\n\r\n{decoded}")
        return True

    # this is a keyboard input request
    try:
        fd = os.open((os.path.join(_prefs.pipe_dir, f"urwid_{urwid_id}.in")), os.O_WRONLY)
    except OSError:
        sys.stdout.write("Status: 404 Not Found\r\n\r\n")
        return True

    try:
        try:
            keydata = sys.stdin.read(MAX_READ)
            encoded_keydata = keydata.encode(_request_charset())
        except (LookupError, UnicodeError):
            sys.stdout.write("Status: 400 Bad Request\r\n\r\n")
            return True
        os.write(fd, encoded_keydata)
    finally:
        with suppress(OSError):
            os.close(fd)
    sys.stdout.write("Content-type: text/plain\r\n\r\n")

    return True


@dataclasses.dataclass
class _Preferences:
    app_name: str = "Unnamed Application"
    pipe_dir: str = TEMP_DIR
    allow_polling: bool = True
    max_clients: int = 20


_prefs = _Preferences()


def set_preferences(
    app_name: str,
    pipe_dir: str = TEMP_DIR,
    allow_polling: bool = True,
    max_clients: int = 20,
) -> None:
    """
    Set web_display preferences.

    :param app_name: application name to appear in html interface
    :param pipe_dir: directory for input pipes, daemon update sockets and daemon error logs
    :param allow_polling: allow creation of daemon processes for browsers without multipart support
    :param max_clients: maximum concurrent client connections. This pool is shared by all urwid applications using the
        same pipe_dir
    """
    _prefs.app_name = app_name
    _prefs.pipe_dir = pipe_dir
    _prefs.allow_polling = allow_polling
    _prefs.max_clients = max_clients


class ErrorLog:
    """File-like object that appends written text to the given error log file."""

    def __init__(self, errfile: str | pathlib.PurePath) -> None:
        """Wrap the error log file at `errfile`."""
        self.errfile = errfile

    def write(self, err: str) -> None:
        """Append `err` to the error log file."""
        with open(self.errfile, "a", encoding="utf-8") as f:
            f.write(err)


def daemonize(errfile: str) -> None:
    """Detach process and become a daemon."""
    if os.fork():
        os._exit(0)

    os.setsid()
    signal.signal(signal.SIGHUP, signal.SIG_IGN)
    os.umask(0)

    if os.fork():
        os._exit(0)

    os.chdir("/")
    for fd in range(20):
        with suppress(OSError):
            os.close(fd)

    sys.stdin = open("/dev/null", encoding="utf-8")  # noqa: SIM115  # pylint: disable=consider-using-with
    sys.stdout = open("/dev/null", "w", encoding="utf-8")  # noqa: SIM115  # pylint: disable=consider-using-with
    sys.stderr = ErrorLog(errfile)
urwid-urwid-69907e4/urwid/event_loop/000077500000000000000000000000001526073734300176035ustar00rootroot00000000000000urwid-urwid-69907e4/urwid/event_loop/__init__.py000066400000000000000000000022571526073734300217220ustar00rootroot00000000000000"""Package with EventLoop implementations for urwid."""

from __future__ import annotations

import sys

from .abstract_loop import EventLoop, ExitMainLoop
from .asyncio_loop import AsyncioEventLoop
from .main_loop import MainLoop
from .select_loop import SelectEventLoop

__all__ = (
    "AsyncioEventLoop",
    "EventLoop",
    "ExitMainLoop",
    "MainLoop",
    "SelectEventLoop",
)

try:
    from .twisted_loop import TwistedEventLoop

    __all__ += ("TwistedEventLoop",)  # type: ignore[assignment]
except ImportError:
    pass

try:
    from .tornado_loop import TornadoEventLoop

    __all__ += ("TornadoEventLoop",)  # type: ignore[assignment]
except ImportError:
    pass

try:
    from .glib_loop import GLibEventLoop

    __all__ += ("GLibEventLoop",)  # type: ignore[assignment]
except ImportError:
    pass

try:
    from .trio_loop import TrioEventLoop

    __all__ += ("TrioEventLoop",)  # type: ignore[assignment]
except ImportError:
    pass

if sys.platform != "win32":
    # ZMQEventLoop cause interpreter crash on windows
    try:
        from .zmq_loop import ZMQEventLoop

        __all__ += ("ZMQEventLoop",)  # type: ignore[assignment]
    except ImportError:
        pass
urwid-urwid-69907e4/urwid/event_loop/abstract_loop.py000066400000000000000000000131571526073734300230200ustar00rootroot00000000000000# Urwid main loop code
#    Copyright (C) 2004-2012  Ian Ward
#    Copyright (C) 2008 Walter Mundt
#    Copyright (C) 2009 Andrew Psaltis
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/

"""Abstract shared code for urwid EventLoop implementation."""

from __future__ import annotations

import abc
import logging
import signal
import typing

if typing.TYPE_CHECKING:
    import asyncio
    from collections.abc import Callable
    from concurrent.futures import Executor, Future
    from types import FrameType

    from typing_extensions import ParamSpec

    _T = typing.TypeVar("_T")
    _Spec = ParamSpec("_Spec")

__all__ = ("EventLoop", "ExitMainLoop", "SupportsFileno")


@typing.runtime_checkable
class SupportsFileno(typing.Protocol):
    """Object that can provide an OS-level file descriptor."""

    def fileno(self) -> int:
        """Return the OS-level file descriptor for this object."""


class ExitMainLoop(Exception):
    """When this exception is raised within a main loop, the main loop will exit cleanly."""


class EventLoop(abc.ABC):
    """Abstract class representing an event loop to be used by :class:`MainLoop`."""

    __slots__ = ("logger",)

    def __init__(self) -> None:
        """Initialize a logger scoped to the concrete event loop subclass."""
        self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__)

    def run_in_executor(
        self,
        executor: Executor,
        func: Callable[_Spec, _T],
        *args: _Spec.args,
        **kwargs: _Spec.kwargs,
    ) -> Future[_T] | asyncio.Future[_T]:
        """Run callable in executor if supported.

        :param executor: Executor to use for running the function
        :param func: function to call
        :param args: arguments to function (positional only)
        :param kwargs: keyword arguments to function (keyword only)
        :return: future object for the function call outcome.
                 (exact future type depends on the event loop type)
        :raises NotImplementedError: this event loop cannot run callables in an executor.
        """
        raise NotImplementedError

    @abc.abstractmethod
    def alarm(self, seconds: float, callback: Callable[[], typing.Any]) -> typing.Any:
        """Call callback() a given time from now.

        No parameters are passed to callback. This method has no default implementation.

        Returns a handle that may be passed to remove_alarm().

        :param seconds: floating point time to wait before calling callback
        :param callback: function to call from event loop
        """

    @abc.abstractmethod
    def enter_idle(self, callback: typing.Any) -> typing.Any:
        """
        Add a callback for entering idle.

        This method has no default implementation.

        Returns a handle that may be passed to remove_idle()
        """

    @abc.abstractmethod
    def remove_alarm(self, handle: typing.Any) -> bool:
        """
        Remove an alarm.

        This method has no default implementation.

        Returns True if the alarm exists, False otherwise
        """

    @abc.abstractmethod
    def remove_enter_idle(self, handle: typing.Any) -> bool:
        """
        Remove an idle callback.

        This method has no default implementation.

        Returns True if the handle was removed.
        """

    @abc.abstractmethod
    def remove_watch_file(self, handle: typing.Any) -> bool:
        """
        Remove an input file.

        This method has no default implementation.

        Returns True if the input file exists, False otherwise
        """

    @abc.abstractmethod
    def run(self) -> None:
        """Start the event loop.

        Exit the loop when any callback raises an exception. If ExitMainLoop is raised, exit cleanly.
        This method has no default implementation.
        """

    @abc.abstractmethod
    def watch_file(self, fd: int, callback: Callable[[], typing.Any]) -> typing.Any:
        """Call callback() when fd has some data to read.

        No parameters are passed to callback. This method has no default implementation.

        Returns a handle that may be passed to remove_watch_file().

        :param fd: file descriptor to watch for input
        :param callback: function to call when input is available
        """

    def set_signal_handler(
        self,
        signum: int,
        handler: Callable[[int, FrameType | None], typing.Any] | int | signal.Handlers,
    ) -> Callable[[int, FrameType | None], typing.Any] | int | signal.Handlers | None:
        """Set the signal handler for signal signum.

        The default implementation of :meth:`set_signal_handler`
        is simply a proxy function that calls :func:`signal.signal()`
        and returns the resulting value.

        :param signum: signal number
        :param handler: function (taking signum as its single argument),
            or `signal.SIG_IGN`, or `signal.SIG_DFL`
        """
        return signal.signal(signum, handler)
urwid-urwid-69907e4/urwid/event_loop/asyncio_loop.py000066400000000000000000000301041526073734300226510ustar00rootroot00000000000000# Urwid main loop code
#    Copyright (C) 2004-2012  Ian Ward
#    Copyright (C) 2008 Walter Mundt
#    Copyright (C) 2009 Andrew Psaltis
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/

"""Asyncio based urwid EventLoop implementation."""

from __future__ import annotations

import asyncio
import functools
import inspect
import logging
import sys
import typing

from .abstract_loop import EventLoop, ExitMainLoop

if typing.TYPE_CHECKING:
    from collections.abc import Callable
    from concurrent.futures import Executor

    from typing_extensions import ParamSpec

    _Spec = ParamSpec("_Spec")
    _T = typing.TypeVar("_T")

__all__ = ("AsyncioEventLoop",)


class AsyncioEventLoop(EventLoop):
    """
    Event loop based on the standard library ``asyncio`` module.

    .. warning::
        Under Windows, AsyncioEventLoop globally enforces WindowsSelectorEventLoopPolicy
        as a side-effect of creating a class instance.
        Original event loop policy is restored in destructor method.

    .. note::
        If you make any changes to the urwid state outside of it
        handling input or responding to alarms (for example, from asyncio.Task
        running in background), and wish the screen to be
        redrawn, you must call :meth:`MainLoop.draw_screen` method of the
        main loop manually.

        A good way to do this:
            asyncio.get_event_loop().call_soon(main_loop.draw_screen)

    .. note::
        :meth:`alarm`, :meth:`watch_file` and :meth:`enter_idle` accept an ``async def``
        callback in addition to a plain callable. A coroutine function is scheduled as
        an ``asyncio.Task`` instead of being called directly.
    """

    def __init__(self, *, loop: asyncio.AbstractEventLoop | None = None, **kwargs: typing.Any) -> None:
        """Wrap `loop`, or the current asyncio event loop when none is given."""
        super().__init__()
        self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__)

        if sys.version_info[:2] < (3, 11):
            self._event_loop_policy_altered: bool = False
            self._original_event_loop_policy: asyncio.AbstractEventLoopPolicy | None = None

            if loop:
                self._loop: asyncio.AbstractEventLoop = loop
            else:
                self._original_event_loop_policy = asyncio.get_event_loop_policy()
                if sys.platform == "win32" and not isinstance(
                    self._original_event_loop_policy, asyncio.WindowsSelectorEventLoopPolicy
                ):
                    self.logger.debug("Set WindowsSelectorEventLoopPolicy as asyncio event loop policy")
                    asyncio.set_event_loop_policy(
                        asyncio.WindowsSelectorEventLoopPolicy()  # pylint: disable=deprecated-class
                    )
                    self._event_loop_policy_altered = True
                else:
                    self._event_loop_policy_altered = False

                try:
                    self._loop = asyncio.get_event_loop()
                except RuntimeError:
                    # get_event_loop() only auto-creates a loop the first time it is ever called in
                    # this thread (CPython's BaseDefaultEventLoopPolicy._set_called guard): once
                    # something else has called set_event_loop() - e.g. a test fixture resetting it
                    # to None during teardown - a later get_event_loop() call raises instead of
                    # creating a replacement, even though nothing is currently running one.
                    self._loop = asyncio.new_event_loop()
                    asyncio.set_event_loop(self._loop)

        else:
            self._runner: asyncio.Runner | None = None

            if loop:
                self._loop: asyncio.AbstractEventLoop = loop
            else:
                try:
                    self._loop = asyncio.get_running_loop()
                except RuntimeError:
                    self._runner = asyncio.Runner(loop_factory=asyncio.SelectorEventLoop)
                    self._loop = self._runner.get_loop()

        self._exc: BaseException | None = None

        self._idle_asyncio_handle: asyncio.TimerHandle | None = None
        self._idle_handle: int = 0
        self._idle_callbacks: dict[int, Callable[[], typing.Any]] = {}
        self._background_tasks: set[asyncio.Task[typing.Any]] = set()

    def __del__(self) -> None:
        """Restore the original event loop policy or close the runner, as appropriate for this Python version."""
        if sys.version_info[:2] < (3, 11):
            if self._event_loop_policy_altered:
                asyncio.set_event_loop_policy(self._original_event_loop_policy)  # Restore default event loop policy
        elif self._runner is not None:
            self._runner.close()

    def _also_call_idle(self, callback: Callable[_Spec, _T]) -> Callable[_Spec, _T | None]:
        """Wrap the callback to also call _entering_idle."""

        @functools.wraps(callback)
        def wrapper(*args: _Spec.args, **kwargs: _Spec.kwargs) -> _T | None:
            if not self._idle_asyncio_handle:
                self._idle_asyncio_handle = self._loop.call_later(0, self._entering_idle)
            return self._run_callback(callback, *args, **kwargs)

        return wrapper

    def _run_callback(self, callback: Callable[_Spec, _T], *args: _Spec.args, **kwargs: _Spec.kwargs) -> _T | None:
        """Call callback, scheduling it as a task instead if it is a coroutine function.

        :param callback: function or coroutine function to call
        :param args: positional arguments to pass to callback
        :param kwargs: keyword arguments to pass to callback
        :return: callback return value, or None if it was scheduled as a background task
        """
        if inspect.iscoroutinefunction(callback):
            task = self._loop.create_task(callback(*args, **kwargs))
            self._background_tasks.add(task)
            task.add_done_callback(self._background_tasks.discard)
            task.add_done_callback(self._background_task_done)
            return None
        return callback(*args, **kwargs)

    def _background_task_done(self, task: asyncio.Task[typing.Any]) -> None:
        """Report a background task's exception directly rather than relying on GC.

        A background task's exception would otherwise only surface through asyncio's
        "Task exception was never retrieved" mechanism, which CPython triggers from
        ``Task.__del__`` as soon as the task is refcounted to zero. PyPy and GraalPy use
        tracing garbage collectors instead, so that finalizer runs at an unpredictable
        time (if at all before the process exits), leaving :meth:`run` blocked forever
        on an exception - including a plain :exc:`ExitMainLoop` - that already happened.
        """
        if task.cancelled():
            return
        if exc := task.exception():
            self._exception_handler(self._loop, {"exception": exc})

    def _entering_idle(self) -> None:
        """Call all the registered idle callbacks."""
        try:
            for callback in self._idle_callbacks.values():
                self._run_callback(callback)
        finally:
            self._idle_asyncio_handle = None

    def run_in_executor(
        self,
        executor: Executor | None,
        func: Callable[_Spec, _T],
        *args: _Spec.args,
        **kwargs: _Spec.kwargs,
    ) -> asyncio.Future[_T]:
        """Run callable in executor.

        :param executor: Executor to use for running the function. Default asyncio executor is used if None.
        :param func: function to call
        :param args: arguments to function (positional only)
        :param kwargs: keyword arguments to function (keyword only)
        :return: future object for the function call outcome.
        """
        return self._loop.run_in_executor(executor, functools.partial(func, *args, **kwargs))

    def alarm(self, seconds: float, callback: Callable[[], typing.Any]) -> asyncio.TimerHandle:
        """Call callback() a given time from now.

        No parameters are passed to callback. Returns a handle that may be passed to remove_alarm().

        :param seconds: time in seconds to wait before calling callback
        :param callback: function to call from event loop
        """
        return self._loop.call_later(seconds, self._also_call_idle(callback))

    def remove_alarm(self, handle: asyncio.TimerHandle) -> bool:
        """
        Remove an alarm.

        Returns True if the alarm exists, False otherwise
        """
        existed = not handle.cancelled()
        handle.cancel()
        return existed

    def watch_file(self, fd: int, callback: Callable[[], typing.Any]) -> int:
        """Call callback() when fd has some data to read.

        No parameters are passed to callback. Returns a handle that may be passed to remove_watch_file().

        :param fd: file descriptor to watch for input
        :param callback: function to call when input is available
        """
        self._loop.add_reader(fd, self._also_call_idle(callback))
        return fd

    def remove_watch_file(self, handle: int) -> bool:
        """
        Remove an input file.

        Returns True if the input file exists, False otherwise
        """
        return self._loop.remove_reader(handle)

    def enter_idle(self, callback: Callable[[], typing.Any]) -> int:
        """
        Add a callback for entering idle.

        Returns a handle that may be passed to remove_enter_idle()
        """
        # XXX there's no such thing as "idle" in most event loops; this fakes
        # it by adding extra callback to the timer and file watch callbacks.
        self._idle_handle += 1
        self._idle_callbacks[self._idle_handle] = callback
        return self._idle_handle

    def remove_enter_idle(self, handle: int) -> bool:
        """
        Remove an idle callback.

        Returns True if the handle was removed.
        """
        try:
            del self._idle_callbacks[handle]
        except KeyError:
            return False
        return True

    def _exception_handler(self, loop: asyncio.AbstractEventLoop, context: dict[str, typing.Any]) -> None:
        if exc := context.get("exception"):
            loop.stop()

            if self._idle_asyncio_handle:
                # clean it up to prevent old callbacks
                # from messing things up if loop is restarted
                self._idle_asyncio_handle.cancel()
                self._idle_asyncio_handle = None

            if not isinstance(exc, ExitMainLoop):
                # Store the exc_info so we can re-raise after the loop stops
                self._exc = exc
        else:
            loop.default_exception_handler(context)

    def run(self) -> None:
        """Start the event loop.

        Exit the loop when any callback raises an exception.
        If ExitMainLoop is raised, exit cleanly.

        :raises BaseException: the exception that stopped the loop, once the loop has been left.
        """
        # The handler stops the loop on any exception, so it must not stay on a loop shared with other code.
        previous_handler = self._loop.get_exception_handler()
        self._loop.set_exception_handler(self._exception_handler)
        try:
            self._loop.run_forever()
        finally:
            self._loop.set_exception_handler(previous_handler)
        if self._exc:
            exc = self._exc
            self._exc = None
            raise exc.with_traceback(exc.__traceback__)
urwid-urwid-69907e4/urwid/event_loop/glib_loop.py000066400000000000000000000227531526073734300221340ustar00rootroot00000000000000# Urwid main loop code
#    Copyright (C) 2004-2012  Ian Ward
#    Copyright (C) 2008 Walter Mundt
#    Copyright (C) 2009 Andrew Psaltis
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/

"""GLib based urwid EventLoop implementation.

PyGObject library is required.
"""

from __future__ import annotations

import functools
import logging
import signal
import typing
import warnings

from gi.repository import GLib

from .abstract_loop import EventLoop, ExitMainLoop

if typing.TYPE_CHECKING:
    from collections.abc import Callable
    from concurrent.futures import Executor, Future
    from types import FrameType

    from typing_extensions import Literal, ParamSpec

    _Spec = ParamSpec("_Spec")
    _T = typing.TypeVar("_T")

__all__ = ("GLibEventLoop",)


def _ignore_handler(_sig: int, _frame: FrameType | None = None) -> None:
    return None


class GLibEventLoop(EventLoop):
    """Event loop based on GLib.MainLoop.

    .. deprecated:: 4.1.7
        This API will be removed in version 6.0.
    """

    def __init__(self) -> None:
        """Initialize a fresh GLib.MainLoop with no alarms or watched files yet."""
        warnings.warn(
            "GLibEventLoop is deprecated. API will be removed in version 6.0.",
            PendingDeprecationWarning,
            stacklevel=2,
        )
        super().__init__()
        self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__)
        self._alarms: list[int] = []
        self._watch_files: dict[int, int] = {}
        self._idle_handle: int = 0
        self._glib_idle_enabled = False  # have we called glib.idle_add?
        self._idle_callbacks: dict[int, Callable[[], typing.Any]] = {}
        self._loop = GLib.MainLoop()
        self._exc: BaseException | None = None
        self._enable_glib_idle()
        self._signal_handlers: dict[int, int] = {}

    def run_in_executor(
        self,
        executor: Executor,
        func: Callable[_Spec, _T],
        *args: _Spec.args,
        **kwargs: _Spec.kwargs,
    ) -> Future[_T]:
        """Run callable in executor.

        :param executor: Executor to use for running the function
        :param func: function to call
        :param args: positional arguments to function
        :param kwargs: keyword arguments to function
        :return: future object for the function call outcome.
        """
        return executor.submit(func, *args, **kwargs)

    def alarm(
        self,
        seconds: float,
        callback: Callable[[], typing.Any],
    ) -> tuple[int, Callable[[], typing.Any]]:
        """Call callback() a given time from now.

        No parameters are passed to callback. Returns a handle that may be passed to remove_alarm().

        :param seconds: floating point time to wait before calling callback
        :param callback: function to call from event loop
        """

        @self.handle_exit
        def ret_false() -> Literal[False]:
            callback()
            self._enable_glib_idle()
            return False

        fd = GLib.timeout_add(int(seconds * 1000), ret_false)
        self._alarms.append(fd)
        return (fd, callback)

    def set_signal_handler(
        self,
        signum: int,
        handler: Callable[[int, FrameType | None], typing.Any] | int | signal.Handlers,
    ) -> None:
        """Set the signal handler for signal signum.

        .. WARNING::
            Because this method uses the `GLib`-specific `unix_signal_add`
            function, its behaviour is different than `signal.signal().`

            If `signum` is not `SIGHUP`, `SIGINT`, `SIGTERM`, `SIGUSR1`,
            `SIGUSR2` or `SIGWINCH`, this method performs no actions and
            immediately returns None.

            Returns None in all cases (unlike :func:`signal.signal()`).
        ..

        :param signum: signal number
        :param handler: function (taking signum as its single argument),
            or `signal.SIG_IGN`, or `signal.SIG_DFL`
        """
        glib_signals = [
            signal.SIGHUP,
            signal.SIGINT,
            signal.SIGTERM,
            signal.SIGUSR1,
            signal.SIGUSR2,
        ]

        # GLib supports SIGWINCH as of version 2.54.
        if not GLib.check_version(2, 54, 0):
            glib_signals.append(signal.SIGWINCH)

        if signum not in glib_signals:
            # The GLib event loop supports only the signals listed above
            return

        if signum in self._signal_handlers:
            GLib.source_remove(self._signal_handlers.pop(signum))

        if handler == signal.Handlers.SIG_IGN:
            handler = _ignore_handler
        elif handler == signal.Handlers.SIG_DFL:
            return

        def final_handler(signal_number: int) -> bool:
            # MyPy False-negative: signal.Handlers casted
            handler(signal_number, None)  # type: ignore[operator]
            return GLib.SOURCE_CONTINUE  # type: ignore[no-any-return]

        source = GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signum, final_handler, signum)
        self._signal_handlers[signum] = source

    def remove_alarm(self, handle: tuple[int, Callable[[], typing.Any]]) -> bool:
        """
        Remove an alarm.

        Returns True if the alarm exists, False otherwise
        """
        try:
            self._alarms.remove(handle[0])
            GLib.source_remove(handle[0])

        except ValueError:
            return False

        return True

    def watch_file(self, fd: int, callback: Callable[[], typing.Any]) -> int:
        """Call callback() when fd has some data to read.

        No parameters are passed to callback. Returns a handle that may be passed to remove_watch_file().

        :param fd: file descriptor to watch for input
        :param callback: function to call when input is available
        """

        @self.handle_exit
        def io_callback(source: GLib.IOChannel, cb_condition: GLib.IOCondition) -> Literal[True]:
            callback()
            self._enable_glib_idle()
            return True

        self._watch_files[fd] = GLib.io_add_watch(fd, GLib.IO_IN, io_callback)
        return fd

    def remove_watch_file(self, handle: int) -> bool:
        """
        Remove an input file.

        Returns True if the input file exists, False otherwise
        """
        if handle in self._watch_files:
            GLib.source_remove(self._watch_files[handle])
            del self._watch_files[handle]
            return True
        return False

    def enter_idle(self, callback: Callable[[], typing.Any]) -> int:
        """
        Add a callback for entering idle.

        Returns a handle that may be passed to remove_enter_idle()
        """
        self._idle_handle += 1
        self._idle_callbacks[self._idle_handle] = callback
        return self._idle_handle

    def _enable_glib_idle(self) -> None:
        if self._glib_idle_enabled:
            return
        GLib.idle_add(self._glib_idle_callback)
        self._glib_idle_enabled = True

    def _glib_idle_callback(self) -> bool:
        for callback in self._idle_callbacks.values():
            callback()
        self._glib_idle_enabled = False
        return False  # ask glib not to call again (or we would be called

    def remove_enter_idle(self, handle: int) -> bool:
        """
        Remove an idle callback.

        Returns True if the handle was removed.
        """
        try:
            del self._idle_callbacks[handle]
        except KeyError:
            return False
        return True

    def run(self) -> None:
        """Start the event loop.

        Exit the loop when any callback raises an exception. If ExitMainLoop is raised, exit cleanly.

        :raises BaseException: the exception that stopped the loop, once the loop has been left.
        """
        try:
            self._loop.run()
        finally:
            if self._loop.is_running():
                self._loop.quit()
        if self._exc:
            # An exception caused us to exit, raise it now
            exc = self._exc
            self._exc = None
            raise exc.with_traceback(exc.__traceback__)

    def handle_exit(self, f: Callable[_Spec, _T]) -> Callable[_Spec, _T | Literal[False]]:
        """Wrap *f* so that :exc:`ExitMainLoop` raised inside it exits the :class:`GLibEventLoop` cleanly.

        Store the exception info if some other exception occurs, it will be reraised after the loop quits.

        *f* -- function to be wrapped
        """

        @functools.wraps(f)
        def wrapper(*args: _Spec.args, **kwargs: _Spec.kwargs) -> _T | Literal[False]:
            try:
                return f(*args, **kwargs)
            except ExitMainLoop:
                self._loop.quit()
            except BaseException as exc:  # noqa: BLE001  # special case
                self._exc = exc
                if self._loop.is_running():
                    self._loop.quit()
            return False

        return wrapper
urwid-urwid-69907e4/urwid/event_loop/main_loop.py000066400000000000000000001007371526073734300221420ustar00rootroot00000000000000# Urwid main loop code
#    Copyright (C) 2004-2012  Ian Ward
#    Copyright (C) 2008 Walter Mundt
#    Copyright (C) 2009 Andrew Psaltis
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/


"""The main loop that connects a widget tree, a display module and an event loop."""

from __future__ import annotations

import heapq
import inspect
import logging
import os
import sys
import time
import typing
from contextlib import suppress

from urwid import display, signals
from urwid.command_map import Command, command_map
from urwid.display.common import INPUT_DESCRIPTORS_CHANGED
from urwid.util import StoppingContext, is_mouse_event
from urwid.widget import PopUpTarget

from .abstract_loop import ExitMainLoop
from .select_loop import SelectEventLoop

if typing.TYPE_CHECKING:
    from collections.abc import Callable, Iterable

    from typing_extensions import Literal, Self

    from urwid.display import BaseScreen
    from urwid.display.common import PaletteEntry
    from urwid.widget import AbstractWidget

    from .abstract_loop import EventLoop

    _T = typing.TypeVar("_T")

    class _ExternalLoopScreen(typing.Protocol):
        """Screen able to drive an external event loop (e.g. raw_display / curses screens).

        These members are not part of :class:`BaseScreen`;
        :class:`MainLoop` only uses them after guarding with ``hasattr``
        (or after :meth:`MainLoop.start` raised :exc:`CantUseExternalLoop`),
        so accessing them through this protocol via ``cast`` is safe.
        """

        signal_handler_setter: Callable[..., typing.Any]

        def hook_event_loop(
            self,
            event_loop: EventLoop,
            callback: Callable[[list[str | tuple[str, int, int, int]], list[int]], typing.Any],
        ) -> None:
            """Register `callback` with `event_loop` to be called when input is available."""

        def unhook_event_loop(self, event_loop: EventLoop) -> None:
            """Remove the input callback previously registered with `event_loop`."""

        def get_input(self, raw_keys: Literal[True]) -> tuple[list[str | tuple[str, int, int, int]], list[int]]:
            """Return the input received since the last call, alongside its raw key codes."""

        def set_input_timeouts(self, max_wait: float | None = ...) -> None:
            """Set the maximum time to wait for input before returning, in seconds."""

    class _EventLoopWithAlarmsStore(typing.Protocol):
        """Event loop that supports alarms storage (like :class:`SelectEventLoop`).

        Not all event loops support alarms storage.
        """

        _alarms: list[tuple[float, int, Callable[[], typing.Any]]]


IS_WINDOWS = sys.platform == "win32"
PIPE_BUFFER_READ_SIZE = 4096  # can expect this much on Linux, so try for that

__all__ = ("CantUseExternalLoop", "MainLoop")


class CantUseExternalLoop(Exception):
    """Raised when an event loop given to :class:`MainLoop` cannot be reused as an external loop."""


class MainLoop:
    """This is the standard main loop implementation for a single interactive session.

    :param widget: the topmost widget used for painting the screen, stored as
                   :attr:`widget` and may be modified. Must be a box widget.

    :param palette: initial palette for screen

    :param screen: screen to use, default is a new :class:`raw_display.Screen`
                   instance; stored as :attr:`screen`

    :param handle_mouse: ``True`` to ask :attr:`.screen` to process mouse events

    :param input_filter: a function to filter input before sending it to
                   :attr:`.widget`, called from :meth:`.input_filter`

    :param unhandled_input: a function called when input is not handled by
                            :attr:`.widget`, called from :meth:`.unhandled_input`

    :param event_loop: if :attr:`.screen` supports external an event loop it may be
                       given here, default is a new :class:`SelectEventLoop` instance;
                       stored as :attr:`.event_loop`

    :param pop_ups: `True` to wrap :attr:`.widget` with a :class:`PopUpTarget`
                    instance to allow any widget to open a pop-up anywhere on the screen


    .. attribute:: screen

        The screen object this main loop uses for screen updates and reading input

    .. attribute:: event_loop

        The event loop object this main loop uses for waiting on alarms and IO

    .. note::
        Some :attr:`event_loop` implementations accept an ``async def`` callback in addition to a
        plain callable - see the specific implementation's own documentation for whether it does,
        and how it runs one. :meth:`set_alarm_in`, :meth:`set_alarm_at`, :meth:`watch_file` and
        :meth:`watch_pipe` all detect an ``async def`` callback and pass that detection through to
        :attr:`event_loop`; whether it actually does anything still depends on :attr:`event_loop`.
    """

    __slots__ = (
        "__dict__",
        "__weakref__",
        "_input_filter",
        "_pop_ups",
        "_topmost_widget",
        "_unhandled_input",
        "_watch_pipes",
        "_widget",
        "event_loop",
        "handle_mouse",
        "idle_handle",
        "logger",
        "screen",
        "screen_size",
    )

    def __init__(
        self,
        widget: AbstractWidget,
        palette: Iterable[PaletteEntry] = (),
        screen: BaseScreen | None = None,
        handle_mouse: bool = True,
        input_filter: (
            Callable[[list[str | tuple[str, int, int, int]], list[int]], list[str | tuple[str, int, int, int]]] | None
        ) = None,
        unhandled_input: Callable[[str | tuple[str, int, int, int]], bool | None] | None = None,
        event_loop: EventLoop | None = None,
        pop_ups: bool = False,
    ):
        """
        Set up the main loop around a top-level widget, a screen and an event loop.

        :raises NotImplementedError: an *event_loop* is given but *screen* does not support external event loops.
        """
        self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__)
        self._widget = widget
        self._topmost_widget = widget
        self.handle_mouse = handle_mouse
        self._pop_ups = False  # only initialize placeholder
        self.pop_ups = pop_ups  # triggers property setting side-effect

        if not screen:
            screen = display.raw.Screen()

        if palette:
            screen.register_palette(palette)

        self.screen: BaseScreen = screen
        self.screen_size: tuple[int, int] | None = None

        self._unhandled_input = unhandled_input
        self._input_filter = input_filter

        if not hasattr(screen, "hook_event_loop") and event_loop is not None:
            raise NotImplementedError(f"screen object passed {screen!r} does not support external event loops")
        if event_loop is None:
            event_loop = SelectEventLoop()
        self.event_loop: EventLoop = event_loop
        self.idle_handle: typing.Any = None

        if hasattr(self.screen, "signal_handler_setter"):
            # Tell the screen what function it must use to set
            # signal handlers
            typing.cast("_ExternalLoopScreen", self.screen).signal_handler_setter = self.event_loop.set_signal_handler

        self._watch_pipes: dict[int, tuple[Callable[[], typing.Any], int]] = {}

    @property
    def widget(self) -> AbstractWidget:
        """Property for the topmost widget used to draw the screen.

        This must be a box widget.
        """
        return self._widget

    @widget.setter
    def widget(self, widget: AbstractWidget) -> None:
        self._widget = widget
        if self.pop_ups and hasattr(self._topmost_widget, "original_widget"):
            self._topmost_widget.original_widget = self._widget
        else:
            self._topmost_widget = self._widget

    @property
    def pop_ups(self) -> bool:
        """Return whether pop-up widgets opened via :class:`PopUpLauncher` are shown automatically."""
        return self._pop_ups

    @pop_ups.setter
    def pop_ups(self, pop_ups: bool) -> None:
        self._pop_ups = pop_ups
        if pop_ups:
            self._topmost_widget = PopUpTarget(self._widget)
        else:
            self._topmost_widget = self._widget

    def _make_alarm_callback(
        self,
        callback: Callable[[Self, _T | None], typing.Any],
        user_data: _T | None,
    ) -> Callable[[], typing.Any]:
        """Wrap *callback* for :attr:`event_loop`, preserving ``async def`` support.

        An event loop implementation that supports scheduling an ``async def`` callback
        detects it by checking the callback it is directly given, so the wrapper returned
        here has to be an ``async def`` function itself whenever *callback* is one.

        :param callback: function to call with this main loop object and *user_data*
        :param user_data: user data to pass to *callback*
        :return: a zero-argument callback suitable for :meth:`EventLoop.alarm`
        """
        if inspect.iscoroutinefunction(callback):

            async def cb() -> None:
                await callback(self, user_data)

        else:

            def cb() -> None:  # type: ignore[misc]  # intentionally the sync variant of the branch above
                callback(self, user_data)

        return cb

    def set_alarm_in(
        self,
        sec: float,
        callback: Callable[[Self, _T | None], typing.Any],
        user_data: _T | None = None,
    ) -> typing.Any:
        """Schedule an alarm in *sec* seconds that will call *callback* from the within the :meth:`run` method.

        :param sec: seconds until alarm
        :param callback: function to call with two parameters: this main loop
                         object and *user_data*
        :param user_data: optional user data to pass to the callback

        .. note::
            *callback* may be an ``async def`` function on an :attr:`event_loop`
            implementation that supports one; see the note on :class:`MainLoop`.
        """
        self.logger.debug(f"Setting alarm in {sec!r} seconds with callback {callback!r}")
        return self.event_loop.alarm(sec, self._make_alarm_callback(callback, user_data))

    def set_alarm_at(
        self,
        tm: float,
        callback: Callable[[Self, _T | None], typing.Any],
        user_data: _T | None = None,
    ) -> typing.Any:
        """Schedule an alarm at *tm* time that will call *callback* from the within the :meth:`run` function.

        Returns a handle that may be passed to :meth:`remove_alarm`.

        :param tm: time to call callback e.g. ``time.time() + 5``
        :param callback: function to call with two parameters: this main loop
                         object and *user_data*
        :param user_data: optional user data to pass to the callback

        .. note::
            *callback* may be an ``async def`` function on an :attr:`event_loop`
            implementation that supports one; see the note on :class:`MainLoop`.
        """
        sec = tm - time.time()
        self.logger.debug(f"Setting alarm in {sec!r} seconds with callback {callback!r}")
        return self.event_loop.alarm(sec, self._make_alarm_callback(callback, user_data))

    def remove_alarm(self, handle: typing.Any) -> bool:
        """Remove an alarm. Return ``True`` if *handle* was found, ``False`` otherwise."""
        return self.event_loop.remove_alarm(handle)

    if not IS_WINDOWS and sys.implementation.name != "graalpy":

        def watch_pipe(self, callback: Callable[[bytes], bool | None]) -> int:
            """Create a pipe used by another thread or subprocess to trigger *callback* in the main loop.

            :param callback: function taking one parameter to call from within the process/thread running the main loop

            This method returns a file descriptor attached to the write end of a pipe.
            The read end of the pipe is added to the list of files :attr:`event_loop` is watching.
            When data is written to the pipe the callback function will be called
            and passed a single value containing data read from the pipe.

            This method may be used any time you want to update widgets from another thread or subprocess.

            Data may be written to the returned file descriptor with ``os.write(fd, data)``.
            Ensure that data is less than 512 bytes (or 4K on Linux)
            so that the callback will be triggered just once with the complete value of data passed in.

            If the callback returns ``False`` then the watch will be removed from :attr:`event_loop`
            and the read end of the pipe will be closed.
            The same happens once every write end is closed: the callback then receives ``b""`` one last time.
            You are responsible for closing the write end of the pipe with ``os.close(fd)``.

            .. note::
                *callback* may be an ``async def`` function on an :attr:`event_loop`
                implementation that supports one; see the note on :class:`MainLoop`.
            """
            import fcntl

            pipe_rd, pipe_wr = os.pipe()
            fcntl.fcntl(pipe_rd, fcntl.F_SETFL, os.O_NONBLOCK)
            entry: tuple[typing.Any, int] | None = None

            # A callback acts only while its own entry is registered: once remove_watch_pipe() closed the read end,
            # its descriptor number may already belong to another file.
            if inspect.iscoroutinefunction(callback):

                async def cb() -> None:
                    if self._watch_pipes.get(pipe_wr) is not entry:
                        return
                    try:
                        data = os.read(pipe_rd, PIPE_BUFFER_READ_SIZE)
                    except BlockingIOError:
                        return  # a task started by an earlier wake-up already read the data
                    if not data:
                        # Removed before awaiting: the pipe stays readable at its end, starting a task per pass.
                        self.remove_watch_pipe(pipe_wr)
                        await callback(data)
                    elif await callback(data) is False and self._watch_pipes.get(pipe_wr) is entry:
                        self.remove_watch_pipe(pipe_wr)

            else:

                def cb() -> None:  # type: ignore[misc]  # intentionally the sync variant of the branch above
                    if self._watch_pipes.get(pipe_wr) is not entry:
                        return
                    data = os.read(pipe_rd, PIPE_BUFFER_READ_SIZE)
                    if (callback(data) is False or not data) and self._watch_pipes.get(pipe_wr) is entry:
                        self.remove_watch_pipe(pipe_wr)

            # An entry left under this number belongs to a write end the caller has closed since.
            self.remove_watch_pipe(pipe_wr)
            entry = (self.event_loop.watch_file(pipe_rd, cb), pipe_rd)
            self._watch_pipes[pipe_wr] = entry
            return pipe_wr

        def remove_watch_pipe(self, write_fd: int) -> bool:
            """
            Close the read end of the pipe and remove the watch created by :meth:`watch_pipe`.

            ..note:: You are responsible for closing the write end of the pipe.

            Returns ``True`` if the watch pipe exists, ``False`` otherwise
            """
            try:
                watch_handle, pipe_rd = self._watch_pipes.pop(write_fd)
            except KeyError:
                return False

            self.event_loop.remove_watch_file(watch_handle)
            os.close(pipe_rd)
            return True

    def watch_file(self, fd: int, callback: Callable[[], typing.Any]) -> typing.Any:
        """Call *callback* when *fd* has some data to read. No parameters are passed to callback.

        Returns a handle that may be passed to :meth:`remove_watch_file`.

        .. note::
            *callback* is passed to :attr:`event_loop` unwrapped, so it may be an ``async def``
            function on an :attr:`event_loop` implementation that supports one; see the note on
            :class:`MainLoop`.
        """
        self.logger.debug(f"Setting watch file descriptor {fd!r} with {callback!r}")
        return self.event_loop.watch_file(fd, callback)

    def remove_watch_file(self, handle: typing.Any) -> bool:
        """Remove a watch file. Returns ``True`` if the watch file exists, ``False`` otherwise."""
        return self.event_loop.remove_watch_file(handle)

    def run(self) -> None:
        """Start the main loop handling input events and updating the screen.

        The loop will continue until an :exc:`ExitMainLoop` exception is raised.

        If you would prefer to manage the event loop yourself, don't use this
        method.  Instead, call :meth:`start` before starting the event loop,
        and :meth:`stop` once it's finished.
        """
        with suppress(ExitMainLoop):
            self._run()

    def _test_run(self) -> None:
        """Doctest for :meth:`run`.

        >>> w = _refl("widget")  # _refl prints out function calls
        >>> w.render_rval = "fake canvas"  # *_rval is used for return values
        >>> scr = _refl("screen")
        >>> scr.get_input_descriptors_rval = [42]
        >>> scr.get_cols_rows_rval = (20, 10)
        >>> scr.started = True
        >>> scr._urwid_signals = {}
        >>> evl = _refl("event_loop")
        >>> evl.enter_idle_rval = 1
        >>> evl.watch_file_rval = 2
        >>> ml = MainLoop(w, [], scr, event_loop=evl)
        >>> ml.run()  # doctest:+ELLIPSIS
        screen.start()
        screen.set_mouse_tracking()
        screen.unhook_event_loop(...)
        screen.hook_event_loop(...)
        event_loop.enter_idle()
        event_loop.run()
        event_loop.remove_enter_idle(1)
        screen.unhook_event_loop(...)
        screen.stop()
        >>> ml.draw_screen()  # doctest:+ELLIPSIS
        screen.get_cols_rows()
        widget.render((20, 10), focus=True)
        screen.draw_screen((20, 10), 'fake canvas')
        """

    def start(self) -> StoppingContext:
        """Set up the main loop, hooking into the event loop where necessary.

        Starts the :attr:`screen` if it hasn't already been started.

        If you want to control starting and stopping the event loop yourself,
        you should call this method before starting, and call `stop` once the
        loop has finished.  You may also use this method as a context manager,
        which will stop the loop automatically at the end of the block:

            with main_loop.start():
                ...

        Note that some event loop implementations don't handle exceptions
        specially if you manage the event loop yourself.  In particular, the
        Twisted and asyncio loops won't stop automatically when
        :exc:`ExitMainLoop` (or anything else) is raised.

        :raises CantUseExternalLoop: the screen does not support external event loops.
        """
        self.logger.debug(f"Starting event loop {self.event_loop.__class__.__name__!r} to manage display.")

        self.screen.start()

        if self.handle_mouse:
            self.screen.set_mouse_tracking()

        if not hasattr(self.screen, "hook_event_loop"):
            raise CantUseExternalLoop(f"Screen {self.screen!r} doesn't support external event loops")

        with suppress(NameError):
            signals.connect_signal(self.screen, INPUT_DESCRIPTORS_CHANGED, self._reset_input_descriptors)

        # watch our input descriptors
        self._reset_input_descriptors()
        self.idle_handle = self.event_loop.enter_idle(self.entering_idle)

        # the screen is redrawn automatically after input and alarms,
        # however, there can be none of those at the start,
        # so draw the initial screen here unconditionally
        self.event_loop.alarm(0, self.entering_idle)

        return StoppingContext(self)

    def stop(self) -> None:
        """Clean up any hooks added to the event loop.

        Only call this if you're managing the event loop yourself, after the loop stops.
        """
        self.event_loop.remove_enter_idle(self.idle_handle)
        self.idle_handle = None
        signals.disconnect_signal(self.screen, INPUT_DESCRIPTORS_CHANGED, self._reset_input_descriptors)
        typing.cast("_ExternalLoopScreen", self.screen).unhook_event_loop(self.event_loop)

        self.screen.stop()

    def _reset_input_descriptors(self) -> None:
        screen = typing.cast("_ExternalLoopScreen", self.screen)
        screen.unhook_event_loop(self.event_loop)
        screen.hook_event_loop(self.event_loop, self._update)

    def _run(self) -> None:
        try:
            self.start()
        except CantUseExternalLoop:
            try:
                self._run_screen_event_loop()
                return
            finally:
                self.screen.stop()
        except BaseException:
            # start() may have failed after starting the screen or hooking the event loop.
            self.stop()
            raise

        try:
            self.event_loop.run()
        finally:
            self.stop()

    def _update(self, keys: list[str | tuple[str, int, int, int]], raw: list[int]) -> None:
        """Filter *keys* and pass them to :meth:`process_input`, resetting :attr:`screen_size` on a window resize.

        >>> w = _refl("widget")
        >>> w.selectable_rval = True
        >>> w.mouse_event_rval = True
        >>> scr = _refl("screen")
        >>> scr.get_cols_rows_rval = (15, 5)
        >>> evl = _refl("event_loop")
        >>> ml = MainLoop(w, [], scr, event_loop=evl)
        >>> ml._input_timeout = "old timeout"
        >>> ml._update(["y"], [121])  # doctest:+ELLIPSIS
        screen.get_cols_rows()
        widget.selectable()
        widget.keypress((15, 5), 'y')
        >>> ml._update([("mouse press", 1, 5, 4)], [])
        widget.mouse_event((15, 5), 'mouse press', 1, 5, 4, focus=True)
        >>> ml._update([], [])
        """
        if keys := self.input_filter(keys, raw):
            self.process_input(keys)
            if "window resize" in keys:
                self.screen_size = None

    def _run_screen_event_loop(self) -> None:
        """Run the screen event loop, used when the screen does not support using external event loops.

        The alarms stored in the SelectEventLoop in :attr:`event_loop` are modified by this method.
        """
        # pylint: disable=protected-access  # special case for alarms handling
        self.logger.debug(f"Starting screen {self.screen!r} event loop")

        screen = typing.cast("_ExternalLoopScreen", self.screen)
        event_loop = typing.cast("_EventLoopWithAlarmsStore", self.event_loop)

        next_alarm: tuple[float, int, Callable[[], typing.Any]] | None = None

        while True:
            self.draw_screen()

            if not next_alarm and event_loop._alarms:
                next_alarm = heapq.heappop(event_loop._alarms)

            keys: list[str | tuple[str, int, int, int]] = []
            raw: list[int] = []
            while not keys:
                if next_alarm:
                    sec = max(0.0, next_alarm[0] - time.time())
                    screen.set_input_timeouts(sec)
                else:
                    screen.set_input_timeouts(None)
                keys, raw = screen.get_input(True)
                if not keys and next_alarm and next_alarm[0] - time.time() <= 0:
                    break

            if keys := self.input_filter(keys, raw):
                self.process_input(keys)

            while next_alarm:
                if (next_alarm[0] - time.time()) > 0:
                    break
                _tm, _tie_break, callback = next_alarm
                callback()

                if event_loop._alarms:
                    next_alarm = heapq.heappop(event_loop._alarms)
                else:
                    next_alarm = None

            if "window resize" in keys:
                self.screen_size = None

    def _test_run_screen_event_loop(self) -> None:
        """Doctest for :meth:`_run_screen_event_loop`.

        >>> w = _refl("widget")
        >>> scr = _refl("screen")
        >>> scr.get_cols_rows_rval = (10, 5)
        >>> scr.get_input_rval = [], []
        >>> ml = MainLoop(w, screen=scr)
        >>> def stop_now(loop, data):
        ...     raise ExitMainLoop()
        >>> handle = ml.set_alarm_in(0, stop_now)
        >>> try:
        ...     ml._run_screen_event_loop()
        ... except ExitMainLoop:
        ...     pass
        screen.get_cols_rows()
        widget.render((10, 5), focus=True)
        screen.draw_screen((10, 5), None)
        screen.set_input_timeouts(0.0)
        screen.get_input(True)
        """

    def process_input(self, keys: Iterable[str | tuple[str, int, int, int]]) -> bool:
        """Pass keyboard input and mouse events to :attr:`widget`.

        This method is called automatically from the :meth:`run` method when
        there is input, but may also be called to simulate input from the user.

        *keys* is a list of input returned from :attr:`screen`'s get_input()
        or get_input_nonblocking() methods.

        Returns ``True`` if any key was handled by a widget or the
        :meth:`unhandled_input` method.

        :raises TypeError: an item of *keys* is neither a key name nor a mouse event tuple.
        """
        self.logger.debug(f"Processing input: keys={keys!r}")
        if not self.screen_size:
            self.screen_size = self.screen.get_cols_rows()

        something_handled = False

        for key in keys:
            if key == "window resize":
                continue

            if isinstance(key, str):
                if self._topmost_widget.selectable():
                    if handled_key := self._topmost_widget.keypress(self.screen_size, key):
                        key = handled_key  # noqa: PLW2901

                    else:
                        something_handled = True
                        continue

            elif is_mouse_event(key):
                event, button, col, row = key
                if hasattr(self._topmost_widget, "mouse_event") and self._topmost_widget.mouse_event(
                    self.screen_size,
                    event,
                    button,
                    col,
                    row,
                    focus=True,
                ):
                    something_handled = True
                    continue

            else:
                raise TypeError(f"{key!r} is not str | tuple[str, int, int, int]")

            if key:
                if command_map[key] == Command.REDRAW_SCREEN:  # type: ignore[index]  # not `str` is mouse event
                    self.screen.clear()
                    something_handled = True
                else:
                    something_handled |= bool(self.unhandled_input(key))
            else:
                something_handled = True

        return something_handled

    def _test_process_input(self) -> None:
        """Doctest for :meth:`process_input`.

        >>> w = _refl("widget")
        >>> w.selectable_rval = True
        >>> scr = _refl("screen")
        >>> scr.get_cols_rows_rval = (10, 5)
        >>> ml = MainLoop(w, [], scr)
        >>> ml.process_input(["enter", ("mouse drag", 1, 14, 20)])
        screen.get_cols_rows()
        widget.selectable()
        widget.keypress((10, 5), 'enter')
        widget.mouse_event((10, 5), 'mouse drag', 1, 14, 20, focus=True)
        True
        """

    def input_filter(
        self,
        keys: list[str | tuple[str, int, int, int]],
        raw: list[int],
    ) -> list[str | tuple[str, int, int, int]]:
        """Pass each of the input events and raw keystroke values through *input_filter*.

        These values are passed to the *input_filter* function passed to the constructor. That function must
        return a list of keys to be passed to the widgets to handle. If no *input_filter* was defined this
        implementation will return all the input events.
        """
        if self._input_filter:
            return self._input_filter(keys, raw)
        return keys

    def unhandled_input(self, data: str | tuple[str, int, int, int]) -> bool | None:
        """Call the *unhandled_input* function passed to the constructor with any input not handled by the widgets.

        If no *unhandled_input* was defined then the input will be ignored.

        *input* is the keyboard or mouse input.

        The *unhandled_input* function should return ``True`` if it handled
        the input.
        """
        if self._unhandled_input:
            return self._unhandled_input(data)
        return False

    def entering_idle(self) -> None:
        """Call :meth:`draw_screen` to update the screen when anything has changed.

        This method is called whenever the event loop is about to enter the idle state.
        """
        if self.screen.started:
            self.draw_screen()
        else:
            self.logger.debug(f"No redrawing screen: {self.screen!r} is not started.")

    def draw_screen(self) -> None:
        """Render the widgets and paint the screen.

        This method is called automatically from :meth:`entering_idle`, which runs whenever the
        event loop is about to go idle -- including after handling input
        and after an alarm callback fires. So screen updates made from
        input handlers or alarm callbacks are redrawn automatically.

        If you modify the widgets displayed from a callback that does not go through the event loop's idle handling,
        you will need to call this method yourself to repaint the screen.

        .. important::
            Call it only from the thread running the event loop, like every other widget and screen change.
            Another thread hands the work over instead: it writes to a :meth:`watch_pipe` descriptor,
            or uses the event loop's own thread-safe scheduling, such as ``loop.call_soon_threadsafe()`` in asyncio.
        """
        if not self.screen_size:
            self.screen_size = self.screen.get_cols_rows()
            self.logger.debug(f"Screen size recalculated: {self.screen_size!r}")

        canvas = self._topmost_widget.render(self.screen_size, focus=True)
        self.screen.draw_screen(self.screen_size, canvas)


def _refl(name: str, rval: _T | None = None, loop_exit: bool = False) -> Callable[..., _T | typing.Any]:
    """Build a reflection helper used to test the main loop classes.

    >>> scr = _refl("screen")
    >>> scr.function("argument")
    screen.function('argument')
    >>> scr.callme(when="now")
    screen.callme(when='now')
    >>> scr.want_something_rval = 42
    >>> x = scr.want_something()
    screen.want_something()
    >>> x
    42

    """

    class Reflect:
        """Callable stand-in that records its calls and returns a fixed value."""

        def __init__(self, name: str, rval: _T | None = None) -> None:
            self._name = name
            self._rval = rval

        def __call__(self, *argl: typing.Any, **argd: typing.Any) -> _T | None:
            """
            Record the call and return the configured return value.

            :raises ExitMainLoop: this call was set up to leave the main loop.
            """
            args = ", ".join([repr(a) for a in argl])
            if args and argd:
                args = f"{args}, "
            args += ", ".join([f"{k}={v!r}" for k, v in argd.items()])
            print(f"{self._name}({args})")
            if loop_exit:
                raise ExitMainLoop()
            return self._rval

        def __getattr__(self, attr: str) -> Reflect:  # pylint: disable=undefined-variable
            """
            Return a nested :class:`Reflect` that records access to *attr*.

            :raises AttributeError: *attr* names a return-value slot rather than a call.
            """
            if attr.endswith("_rval"):
                raise AttributeError()
            # print(self._name+"."+attr)
            if hasattr(self, f"{attr}_rval"):
                return Reflect(f"{self._name}.{attr}", getattr(self, f"{attr}_rval"))
            return Reflect(f"{self._name}.{attr}")

    return Reflect(name)


def _test() -> None:
    import doctest

    doctest.testmod()


if __name__ == "__main__":
    _test()
urwid-urwid-69907e4/urwid/event_loop/select_loop.py000066400000000000000000000176121526073734300224740ustar00rootroot00000000000000# Urwid main loop code
#    Copyright (C) 2004-2012  Ian Ward
#    Copyright (C) 2008 Walter Mundt
#    Copyright (C) 2009 Andrew Psaltis
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/

"""Select based urwid EventLoop implementation."""

from __future__ import annotations

import contextlib
import heapq
import logging
import selectors
import time
import typing
from contextlib import suppress
from itertools import count

from .abstract_loop import EventLoop, ExitMainLoop

if typing.TYPE_CHECKING:
    from collections.abc import Callable, Iterator
    from concurrent.futures import Executor, Future

    from typing_extensions import Literal, ParamSpec

    _T = typing.TypeVar("_T")
    _Spec = ParamSpec("_Spec")

__all__ = ("SelectEventLoop",)


class SelectEventLoop(EventLoop):
    """Event loop based on :func:`selectors.DefaultSelector.select`."""

    def __init__(self) -> None:
        """Initialize with no alarms or watched files yet."""
        super().__init__()
        self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__)
        self._alarms: list[tuple[float, int, Callable[[], typing.Any]]] = []
        self._watch_files: dict[int, Callable[[], typing.Any]] = {}
        self._idle_handle: int = 0
        self._idle_callbacks: dict[int, Callable[[], typing.Any]] = {}
        self._tie_break: Iterator[int] = count()
        self._did_something: bool = False

    def run_in_executor(
        self,
        executor: Executor,
        func: Callable[_Spec, _T],
        *args: _Spec.args,
        **kwargs: _Spec.kwargs,
    ) -> Future[_T]:
        """Run callable in executor.

        :param executor: Executor to use for running the function
        :param func: function to call
        :param args: positional arguments to function
        :param kwargs: keyword arguments to function
        :return: future object for the function call outcome.
        """
        return executor.submit(func, *args, **kwargs)

    def alarm(
        self,
        seconds: float,
        callback: Callable[[], typing.Any],
    ) -> tuple[float, int, Callable[[], typing.Any]]:
        """Call callback() a given time from now.

        No parameters are passed to callback. Returns a handle that may be passed to remove_alarm().

        :param seconds: floating point time to wait before calling callback
        :param callback: function to call from event loop
        """
        tm = time.time() + seconds
        handle = (tm, next(self._tie_break), callback)
        heapq.heappush(self._alarms, handle)
        return handle

    def remove_alarm(self, handle: tuple[float, int, Callable[[], typing.Any]]) -> bool:
        """
        Remove an alarm.

        Returns True if the alarm exists, False otherwise
        """
        try:
            self._alarms.remove(handle)
            heapq.heapify(self._alarms)

        except ValueError:
            return False

        return True

    def watch_file(self, fd: int, callback: Callable[[], typing.Any]) -> int:
        """Call callback() when fd has some data to read.

        No parameters are passed to callback. Returns a handle that may be passed to remove_watch_file().

        :param fd: file descriptor to watch for input
        :param callback: function to call when input is available
        """
        self._watch_files[fd] = callback
        return fd

    def remove_watch_file(self, handle: int) -> bool:
        """
        Remove an input file.

        Returns True if the input file exists, False otherwise
        """
        if handle in self._watch_files:
            del self._watch_files[handle]
            return True
        return False

    def enter_idle(self, callback: Callable[[], typing.Any]) -> int:
        """
        Add a callback for entering idle.

        Returns a handle that may be passed to remove_idle()
        """
        self._idle_handle += 1
        self._idle_callbacks[self._idle_handle] = callback
        return self._idle_handle

    def remove_enter_idle(self, handle: int) -> bool:
        """
        Remove an idle callback.

        Returns True if the handle was removed.
        """
        try:
            del self._idle_callbacks[handle]
        except KeyError:
            return False
        return True

    def _entering_idle(self) -> None:
        """Call all the registered idle callbacks."""
        for callback in self._idle_callbacks.values():
            callback()

    def run(self) -> None:
        """Start the event loop.

        Exit the loop when any callback raises an exception. If ExitMainLoop is raised, exit cleanly.
        """
        with contextlib.suppress(ExitMainLoop):
            self._did_something = True
            while True:
                with suppress(InterruptedError):
                    self._loop()

    def _loop(self) -> None:
        """Run a single iteration of the event loop."""
        tm: float | Literal["idle"] | None = None

        with selectors.DefaultSelector() as selector:
            for fd, callback in self._watch_files.items():
                selector.register(fd, selectors.EVENT_READ, callback)

            if self._alarms or self._did_something:
                timeout = 0.0

                if self._alarms:
                    timeout_ = self._alarms[0][0]
                    tm = timeout_
                    timeout = max(timeout, timeout_ - time.time())

                if self._did_something and (not self._alarms or (self._alarms and timeout > 0)):
                    timeout = 0.0
                    tm = "idle"

                self.logger.debug(f"Waiting for input: timeout={timeout!r}")
                if self._watch_files:
                    ready = [event for event, _ in selector.select(timeout)]
                else:
                    # Windows `select()` requires at least one socket and fails with WSAEINVAL on an empty set,
                    # while on POSIX it is just a sleep.
                    time.sleep(timeout)
                    ready = []

            elif self._watch_files:
                self.logger.debug("Waiting for input: timeout")
                ready = [event for event, _ in selector.select()]
            else:
                ready = []

        if not ready:
            if tm == "idle":
                self.logger.debug("No input, entering IDLE")
                self._entering_idle()
                self._did_something = False
            elif tm is not None:
                # must have been a timeout
                tm, _tie_break, alarm_callback = heapq.heappop(self._alarms)
                self.logger.debug(f"No input in timeout, calling scheduled {alarm_callback!r}")
                alarm_callback()
                self._did_something = True

        self.logger.debug("Processing input")
        for record in ready:
            # An earlier callback in this pass may have removed or replaced the watch and closed its descriptor.
            # Looked up by the registered object rather than record.fd, so a watch given a file object is found.
            watch_callback = self._watch_files.get(typing.cast("int", record.fileobj))
            if watch_callback is None or watch_callback is not record.data:
                continue
            watch_callback()
            self._did_something = True
urwid-urwid-69907e4/urwid/event_loop/tornado_loop.py000066400000000000000000000233621526073734300226620ustar00rootroot00000000000000# Urwid main loop code
#    Copyright (C) 2004-2012  Ian Ward
#    Copyright (C) 2008 Walter Mundt
#    Copyright (C) 2009 Andrew Psaltis
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/

"""Tornado IOLoop based urwid EventLoop implementation.

Tornado library is required.
"""

from __future__ import annotations

import asyncio
import functools
import inspect
import logging
import typing
from contextlib import suppress

from tornado import ioloop

from .abstract_loop import EventLoop, ExitMainLoop

if typing.TYPE_CHECKING:
    from collections.abc import Callable
    from concurrent.futures import Executor

    from typing_extensions import Literal, ParamSpec

    _Spec = ParamSpec("_Spec")
    _T = typing.TypeVar("_T")

__all__ = ("TornadoEventLoop",)


class TornadoEventLoop(EventLoop):
    """This is an Urwid-specific event loop to plug into its MainLoop.

    It acts as an adaptor for Tornado's IOLoop which does all heavy lifting except idle-callbacks.

    .. note::
        :meth:`alarm`, :meth:`watch_file` and :meth:`enter_idle` accept an ``async def``
        callback in addition to a plain callable. A coroutine function is scheduled as
        an ``asyncio.Task`` on the IOLoop's underlying asyncio loop instead of being
        called directly.
    """

    def __init__(self, loop: ioloop.IOLoop | None = None) -> None:
        """Wrap `loop`, or Tornado's current IOLoop when none is given."""
        super().__init__()
        self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__)
        if loop:
            self._loop: ioloop.IOLoop = loop
        else:
            try:
                asyncio.get_running_loop()
            except RuntimeError:
                asyncio.set_event_loop(asyncio.new_event_loop())

            self._loop = ioloop.IOLoop.current()

        self._pending_alarms: dict[object, int] = {}
        self._watch_handles: dict[int, int] = {}  # { : }
        self._max_watch_handle: int = 0
        self._exc: BaseException | None = None

        self._idle_asyncio_handle: object | None = None
        self._idle_handle: int = 0
        self._idle_callbacks: dict[int, Callable[[], typing.Any]] = {}
        self._background_tasks: set[asyncio.Task[typing.Any]] = set()

    def _also_call_idle(self, callback: Callable[_Spec, _T]) -> Callable[_Spec, _T | None]:
        """Wrap the callback to also call _entering_idle."""

        @functools.wraps(callback)
        def wrapper(*args: _Spec.args, **kwargs: _Spec.kwargs) -> _T | None:
            if not self._idle_asyncio_handle:
                self._idle_asyncio_handle = self._loop.call_later(0, self._entering_idle)
            return self._run_callback(callback, *args, **kwargs)

        return wrapper

    def _run_callback(self, callback: Callable[_Spec, _T], *args: _Spec.args, **kwargs: _Spec.kwargs) -> _T | None:
        """Call callback, scheduling it as a task instead if it is a coroutine function.

        :param callback: function or coroutine function to call
        :param args: positional arguments to pass to callback
        :param kwargs: keyword arguments to pass to callback
        :return: callback return value, or None if it was scheduled as a background task
        """
        if inspect.iscoroutinefunction(callback):
            task = asyncio.get_running_loop().create_task(callback(*args, **kwargs))
            self._background_tasks.add(task)
            task.add_done_callback(self._background_tasks.discard)
            task.add_done_callback(self._handle_task_exception)
            return None
        return callback(*args, **kwargs)

    def _entering_idle(self) -> None:
        """Call all the registered idle callbacks."""
        try:
            for callback in self._idle_callbacks.values():
                self._run_callback(callback)
        finally:
            self._idle_asyncio_handle = None

    def run_in_executor(
        self,
        executor: Executor,
        func: Callable[_Spec, _T],
        *args: _Spec.args,
        **kwargs: _Spec.kwargs,
    ) -> asyncio.Future[_T]:
        """Run callable in executor.

        :param executor: Executor to use for running the function
        :param func: function to call
        :param args: arguments to function (positional only)
        :param kwargs: keyword arguments to function (keyword only)
        :return: future object for the function call outcome.
        """
        return typing.cast(
            "asyncio.Future[_T]",
            self._loop.run_in_executor(executor, functools.partial(func, *args, **kwargs)),
        )

    def alarm(self, seconds: float, callback: Callable[[], typing.Any]) -> object:
        """Schedule *callback* to run after *seconds* and return a handle for :meth:`remove_alarm`."""

        @self._also_call_idle
        @functools.wraps(callback)
        def wrapped() -> None:
            with suppress(KeyError):
                del self._pending_alarms[handle]

            self.handle_exit(callback)()

        handle = self._loop.add_timeout(self._loop.time() + seconds, wrapped)
        self._pending_alarms[handle] = 1
        return handle

    def remove_alarm(self, handle: object) -> bool:
        """Cancel an alarm scheduled by :meth:`alarm`, returning whether it was still pending."""
        self._loop.remove_timeout(handle)
        try:
            del self._pending_alarms[handle]
        except KeyError:
            return False

        return True

    def watch_file(self, fd: int, callback: Callable[[], _T]) -> int:
        """Call *callback* whenever *fd* is readable and return a handle for :meth:`remove_watch_file`."""

        @self._also_call_idle
        def handler(_fd: int, _events: int) -> None:
            self.handle_exit(callback)()

        self._loop.add_handler(fd, handler, ioloop.IOLoop.READ)
        self._max_watch_handle += 1
        handle = self._max_watch_handle
        self._watch_handles[handle] = fd
        return handle

    def remove_watch_file(self, handle: int) -> bool:
        """Stop watching a file descriptor registered by :meth:`watch_file`, returning whether it was watched."""
        if (fd := self._watch_handles.pop(handle, None)) is not None:
            self._loop.remove_handler(fd)
            return True

        return False

    def enter_idle(self, callback: Callable[[], typing.Any]) -> int:
        """
        Add a callback for entering idle.

        Returns a handle that may be passed to remove_idle()
        """
        # XXX there's no such thing as "idle" in most event loops; this fakes
        # it by adding extra callback to the timer and file watch callbacks.
        self._idle_handle += 1
        self._idle_callbacks[self._idle_handle] = callback
        return self._idle_handle

    def remove_enter_idle(self, handle: int) -> bool:
        """
        Remove an idle callback.

        Returns True if the handle was removed.
        """
        try:
            del self._idle_callbacks[handle]
        except KeyError:
            return False
        return True

    def _stop_after_error(self, exc: BaseException | None) -> None:
        """Stop the IOLoop after a callback failed, the same way a synchronous callback's would.

        :param exc: exception the callback raised; recorded unless it is an :exc:`ExitMainLoop`,
            or ``None`` for a clean :exc:`ExitMainLoop`
        """
        if exc is not None and not isinstance(exc, ExitMainLoop):
            self._exc = exc

        if self._idle_asyncio_handle:
            # clean it up to prevent old callbacks
            # from messing things up if loop is restarted
            self._loop.remove_timeout(self._idle_asyncio_handle)
            self._idle_asyncio_handle = None

        self._loop.stop()

    def _handle_task_exception(self, task: asyncio.Task[typing.Any]) -> None:
        """Stop the loop if a background task scheduled from an ``async def`` callback failed.

        :param task: finished background task scheduled by :meth:`_run_callback`
        """
        if task.cancelled():
            return
        if (exc := task.exception()) is not None:
            self._stop_after_error(exc)

    def handle_exit(self, f: Callable[_Spec, _T]) -> Callable[_Spec, _T | Literal[False] | None]:
        """Wrap *f* so that a raised exception stops the loop instead of propagating."""

        @functools.wraps(f)
        def wrapper(*args: _Spec.args, **kwargs: _Spec.kwargs) -> _T | Literal[False] | None:
            try:
                return self._run_callback(f, *args, **kwargs)
            except ExitMainLoop as exc:
                self._stop_after_error(exc)
            except Exception as exc:  # noqa: BLE001  # special case
                self._stop_after_error(exc)

            return False

        return wrapper

    def run(self) -> None:
        """
        Start the event loop and run it until :exc:`ExitMainLoop` is raised.

        :raises BaseException: the exception that stopped the loop, once the loop has been left.
        """
        self._loop.start()
        if self._exc:
            exc, self._exc = self._exc, None
            raise exc.with_traceback(exc.__traceback__)
urwid-urwid-69907e4/urwid/event_loop/trio_loop.py000066400000000000000000000326701526073734300221730ustar00rootroot00000000000000# Urwid main loop code using Python-3.5 features (Trio, Curio, etc)
#    Copyright (C) 2018 Toshio Kuratomi
#    Copyright (C) 2019 Tamas Nepusz
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/

"""Trio Runner based urwid EventLoop implementation.

Trio library is required.
"""

from __future__ import annotations

import functools
import inspect
import logging
import sys
import typing

import trio

from .abstract_loop import EventLoop, ExitMainLoop, SupportsFileno

if sys.version_info < (3, 11):
    from exceptiongroup import BaseExceptionGroup  # pylint: disable=redefined-builtin  # backport

if typing.TYPE_CHECKING:
    import asyncio
    from collections.abc import Awaitable, Callable
    from concurrent.futures import Executor, Future

    from typing_extensions import ParamSpec

    _Spec = ParamSpec("_Spec")
    _T = typing.TypeVar("_T")

__all__ = ("TrioEventLoop",)


async def _reraise(exc: BaseException) -> typing.NoReturn:
    """Raise *exc* from a nursery task, so it stops the loop like a failing alarm or watch callback."""
    raise exc


class _TrioIdleCallbackInstrument(trio.abc.Instrument):
    """IDLE callbacks emulation helper."""

    __slots__ = ("_event_loop",)

    def __init__(self, event_loop: TrioEventLoop) -> None:
        self._event_loop = event_loop

    def before_io_wait(self, timeout: float) -> None:
        # pylint: disable=protected-access  # cooperating class
        # Trio keeps polling while the main task's nursery is cancelled and after it has closed: the loop is exiting.
        nursery = self._event_loop._nursery
        if timeout > 0 and nursery is not None and not nursery.cancel_scope.cancel_called:
            try:
                for idle_callback in list(self._event_loop._idle_callbacks.values()):
                    self._event_loop._run_callback(idle_callback)
            # Trio logs an exception raised by an instrument hook and then disables the instrument.
            except BaseException as exc:  # noqa: BLE001  # re-raised in the nursery
                nursery.start_soon(_reraise, exc)
                # The wait this hook precedes would otherwise run its full timeout before the new task.
                trio.lowlevel.current_trio_token().run_sync_soon(lambda: None)


class TrioEventLoop(EventLoop):
    """
    Event loop based on the ``trio`` module.

    ``trio`` is an async library for Python 3.5 and later.

    .. note::
        :meth:`alarm`, :meth:`watch_file` and :meth:`enter_idle` accept an ``async def``
        callback in addition to a plain callable. A coroutine function is scheduled as
        a task in the main loop's nursery instead of being called directly.
    """

    def __init__(self) -> None:
        """Initialize the Trio event loop."""
        super().__init__()
        self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__)

        self._idle_handle = 0
        self._idle_callbacks: dict[int, Callable[[], typing.Any]] = {}
        self._pending_tasks: list[
            tuple[
                Callable[..., Awaitable[typing.Any]],
                trio.CancelScope,
                tuple[typing.Any, ...],
            ]
        ] = []

        self._nursery: trio.Nursery | None = None

        self._sleep = trio.sleep
        self._wait_readable = trio.lowlevel.wait_readable

    def run_in_executor(
        self,
        executor: Executor,
        func: Callable[_Spec, _T],
        *args: _Spec.args,
        **kwargs: _Spec.kwargs,
    ) -> Future[_T] | asyncio.Future[_T]:
        """Raise :exc:`NotImplementedError`: use Trio's own thread API.

        :raises NotImplementedError: Trio runs blocking calls in threads itself; use ``trio.to_thread.run_sync``.
        """
        raise NotImplementedError(
            "Trio implements its own worker threads. Please use native API for call:\n"
            "'await trio.to_thread.run_sync(Callable[..., T], *args)'"
        )

    def alarm(
        self,
        seconds: float,
        callback: Callable[[], typing.Any],
    ) -> trio.CancelScope:
        """Call `callback()` a given time from now.

        :param seconds: time in seconds to wait before calling the callback
        :param callback: function to call from the event loop
        :return: a handle that may be passed to `remove_alarm()`

        No parameters are passed to the callback.
        """
        return self._start_task(self._alarm_task, seconds, callback)

    def enter_idle(self, callback: Callable[[], typing.Any]) -> int:
        """Call `callback()` when the event loop enters the idle state.

        There is no such thing as being idle in a Trio event loop so we
        simulate it by repeatedly calling `callback()` with a short delay.
        """
        self._idle_handle += 1
        self._idle_callbacks[self._idle_handle] = callback
        return self._idle_handle

    def remove_alarm(self, handle: trio.CancelScope) -> bool:
        """Remove an alarm.

        :param handle: the handle of the alarm to remove
        """
        return self._cancel_scope(handle)

    def remove_enter_idle(self, handle: int) -> bool:
        """Remove an idle callback.

        :param handle: the handle of the idle callback to remove
        """
        try:
            del self._idle_callbacks[handle]
        except KeyError:
            return False
        return True

    def remove_watch_file(self, handle: trio.CancelScope) -> bool:
        """Remove a file descriptor being watched for input.

        :param handle: the handle of the file descriptor callback to remove
        :returns: True if the file descriptor was watched, False otherwise
        """
        return self._cancel_scope(handle)

    def _cancel_scope(self, scope: trio.CancelScope) -> bool:
        """Cancel the given Trio cancellation scope.

        :param scope: the Trio cancellation scope to cancel
        :returns: True if the scope was cancelled, False if it was cancelled already before invoking this function
        """
        existed = not scope.cancel_called
        scope.cancel()
        return existed

    def _run_callback(self, callback: Callable[_Spec, _T], *args: _Spec.args, **kwargs: _Spec.kwargs) -> _T | None:
        """Call callback, scheduling it as a nursery task instead if it is a coroutine function.

        A coroutine function's exceptions propagate through the nursery like any other task's,
        so unlike the asyncio/Tornado event loops there is no separate bookkeeping to fail the
        main loop on error.

        :param callback: function or coroutine function to call
        :param args: positional arguments to pass to callback
        :param kwargs: keyword arguments to pass to callback
        :return: callback return value, or None if it was scheduled as a nursery task
        """
        if inspect.iscoroutinefunction(callback):
            # Callers run only while the nursery is open: its tasks, and the idle instrument, which checks.
            nursery = typing.cast("trio.Nursery", self._nursery)
            fn = functools.partial(callback, *args, **kwargs) if kwargs else callback
            nursery.start_soon(fn, *(() if kwargs else args))
            return None
        return callback(*args, **kwargs)

    def run(self) -> None:
        """Start the event loop.

        Exit the loop when any callback raises an exception. If ExitMainLoop is raised, exit cleanly.
        """
        emulate_idle_callbacks = _TrioIdleCallbackInstrument(self)

        try:
            trio.run(self._main_task, instruments=[emulate_idle_callbacks])
        except BaseException as exc:  # noqa: BLE001  # would be handled
            self._handle_main_loop_exception(exc)

    async def run_async(self) -> None:
        """Start the main loop and block asynchronously until the main loop exits.

        This allows one to embed an urwid app in a Trio app even if the Trio event loop is already running.
        Example::

            with trio.open_nursery() as nursery:
                event_loop = urwid.TrioEventLoop()

                # [...launch other async tasks in the nursery...]

                loop = urwid.MainLoop(widget, event_loop=event_loop)
                with loop.start():
                    await event_loop.run_async()

                nursery.cancel_scope.cancel()
        """
        emulate_idle_callbacks = _TrioIdleCallbackInstrument(self)

        try:
            trio.lowlevel.add_instrument(emulate_idle_callbacks)
            try:
                await self._main_task()
            finally:
                trio.lowlevel.remove_instrument(emulate_idle_callbacks)
        except BaseException as exc:  # noqa: BLE001  # would be handled
            self._handle_main_loop_exception(exc)

    def watch_file(
        self,
        fd: int | SupportsFileno,
        callback: Callable[[], typing.Any],
    ) -> trio.CancelScope:
        """Call `callback()` when the given file descriptor has some data to read.

        No parameters are passed to the callback.

        :param fd: file descriptor to watch for input
        :param callback: function to call when some input is available
        :returns: a handle that may be passed to `remove_watch_file()`
        """
        return self._start_task(self._watch_task, fd, callback)

    async def _alarm_task(
        self,
        scope: trio.CancelScope,
        seconds: float,
        callback: Callable[[], typing.Any],
    ) -> None:
        """Asynchronous task that sleeps for a given number of seconds and then calls the given callback.

        :param scope: the cancellation scope that can be used to cancel the task
        :param seconds: the number of seconds to wait
        :param callback: the callback to call
        """
        with scope:
            await self._sleep(seconds)
            self._run_callback(callback)

    def _handle_main_loop_exception(self, exc: BaseException) -> None:
        """Handle exceptions raised from the main loop, catching ExitMainLoop instead of letting it propagate.

        Note that since Trio may collect multiple exceptions from tasks into an ExceptionGroup, we cannot simply
        use a try..catch clause, we need a helper function like this.

        :raises BaseException: *exc* itself, unless it is :exc:`ExitMainLoop`.
        """
        self._idle_callbacks.clear()
        if isinstance(exc, BaseExceptionGroup) and len(exc.exceptions) == 1:
            exc = exc.exceptions[0]

        if isinstance(exc, ExitMainLoop):
            return

        raise exc.with_traceback(exc.__traceback__) from None

    async def _main_task(self) -> None:
        """Open a nursery and sleep until the user exits the app by raising ExitMainLoop."""
        try:
            async with trio.open_nursery() as self._nursery:
                self._schedule_pending_tasks()
                await trio.sleep_forever()
        finally:
            self._nursery = None

    def _schedule_pending_tasks(self) -> None:
        """Schedule pending tasks created before the nursery opened to run on it soon.

        Tasks queued via :meth:`_start_task` before the nursery existed are started here once it is open.
        """
        if self._nursery is None:
            return
        for task, scope, args in self._pending_tasks:
            self._nursery.start_soon(task, scope, *args)
        self._pending_tasks.clear()

    def _start_task(
        self,
        task: Callable[..., Awaitable[typing.Any]],
        *args: typing.Any,
    ) -> trio.CancelScope:
        """Start an asynchronous task in the Trio nursery managed by the main loop.

        If the nursery has not started yet, store a reference to the task and the arguments so we can start the
        task when the nursery is open.

        :param task: a Trio task to run
        :param args: extra positional arguments passed to the task after its cancellation scope
        :returns: a cancellation scope for the Trio task
        """
        scope = trio.CancelScope()
        if self._nursery:
            self._nursery.start_soon(task, scope, *args)
        else:
            self._pending_tasks.append((task, scope, args))
        return scope

    async def _watch_task(
        self,
        scope: trio.CancelScope,
        fd: int | SupportsFileno,
        callback: Callable[[], typing.Any],
    ) -> None:
        """Watch *fd* and call *callback* whenever it becomes readable.

        :param scope: the cancellation scope that can be used to cancel the task
        :param fd: the file descriptor to watch
        :param callback: the callback to call
        """
        with scope:
            # We check for the scope being cancelled before calling
            # wait_readable because if callback cancels the scope, fd might be
            # closed and calling wait_readable with a closed fd does not work.
            while not scope.cancel_called:
                await self._wait_readable(fd)
                self._run_callback(callback)
urwid-urwid-69907e4/urwid/event_loop/twisted_loop.py000066400000000000000000000237341526073734300227020ustar00rootroot00000000000000# Urwid main loop code
#    Copyright (C) 2004-2012  Ian Ward
#    Copyright (C) 2008 Walter Mundt
#    Copyright (C) 2009 Andrew Psaltis
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/

"""Twisted Reactor based urwid EventLoop implementation.

Twisted library is required.
"""

from __future__ import annotations

import functools
import logging
import sys
import typing

from twisted.internet.abstract import FileDescriptor
from twisted.internet.error import AlreadyCalled, AlreadyCancelled

from .abstract_loop import EventLoop, ExitMainLoop

if typing.TYPE_CHECKING:
    import asyncio
    from collections.abc import Callable
    from concurrent.futures import Executor, Future

    from twisted.internet.base import DelayedCall, ReactorBase
    from twisted.internet.interfaces import IReactorFDSet, IReadDescriptor
    from typing_extensions import ParamSpec

    _Spec = ParamSpec("_Spec")

__all__ = ("TwistedEventLoop",)


_T = typing.TypeVar("_T")


class _TwistedInputDescriptor(FileDescriptor, typing.Generic[_T]):
    def __init__(
        self,
        reactor: ReactorBase,
        fd: int,
        cb: Callable[[], _T],
    ) -> None:
        self._fileno = fd
        self.cb = cb
        super().__init__(typing.cast("IReactorFDSet", reactor))

    def fileno(self) -> int:
        return self._fileno

    def doRead(self) -> _T:
        return self.cb()

    def getHost(self) -> typing.NoReturn:
        """
        Raise :exc:`NotImplementedError`: this descriptor is not a network transport.

        :raises NotImplementedError: this descriptor never takes part in network operations.
        """
        raise NotImplementedError("No network operation expected")

    def getPeer(self) -> typing.NoReturn:
        """
        Raise :exc:`NotImplementedError`: this descriptor is not a network transport.

        :raises NotImplementedError: this descriptor never takes part in network operations.
        """
        raise NotImplementedError("No network operation expected")

    def writeSomeData(self, data: bytes) -> int | BaseException:
        """
        Raise :exc:`NotImplementedError`: this descriptor only reads.

        :raises NotImplementedError: this descriptor is read-only.
        """
        raise NotImplementedError("Reduced functionality: read-only")


class TwistedEventLoop(EventLoop):
    """Event loop based on Twisted_."""

    _idle_emulation_delay = 1.0 / 256  # a short time (in seconds)

    def __init__(self, reactor: ReactorBase | None = None, manage_reactor: bool = True) -> None:
        """Initialize the event loop, wrapping *reactor* or Twisted's default reactor.

        :param reactor: reactor to use
        :param: manage_reactor: `True` if you want this event loop to run
                                and stop the reactor.

        .. WARNING::
           Twisted's reactor doesn't like to be stopped and run again.  If you
           need to stop and run your :class:`MainLoop`, consider setting
           ``manage_reactor=False`` and take care of running/stopping the reactor
           at the beginning/ending of your program yourself.

           You can also forego using :class:`MainLoop`'s run() entirely, and
           instead call start() and stop() before and after starting the
           reactor.

        .. _Twisted: https://twisted.org/
        """
        super().__init__()
        self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__)
        if reactor is None:
            import twisted.internet.reactor

            reactor = twisted.internet.reactor
        self.reactor: ReactorBase = reactor
        self._watch_files: dict[int, _TwistedInputDescriptor[typing.Any]] = {}
        self._idle_handle: int = 0
        self._twisted_idle_enabled = False
        self._idle_callbacks: dict[int, Callable[[], typing.Any]] = {}
        self._exc: BaseException | None = None
        self.manage_reactor = manage_reactor
        self._enable_twisted_idle()

    def run_in_executor(
        self,
        executor: Executor,
        func: Callable[_Spec, _T],
        *args: _Spec.args,
        **kwargs: _Spec.kwargs,
    ) -> Future[_T] | asyncio.Future[_T]:
        """
        Raise :exc:`NotImplementedError`: use Twisted's own thread pool API.

        :raises NotImplementedError: Twisted has its own thread pool; use ``threads.deferToThread`` instead.
        """
        raise NotImplementedError(
            "Twisted implement it's own ThreadPool executor. Please use native API for call:\n"
            "'threads.deferToThread(Callable[..., Any], *args, **kwargs)'\n"
            "And use 'addCallback' api for callbacks:\n"
            "'threads.deferToThread(Callable[..., T], *args, **kwargs).addCallback(Callable[[T], None])'"
        )

    def alarm(self, seconds: float, callback: Callable[[], typing.Any]) -> DelayedCall:
        """Call callback() a given time from now.

        No parameters are passed to callback. Returns a handle that may be passed to remove_alarm().

        :param seconds: floating point time to wait before calling callback
        :param callback: function to call from event loop
        """
        handle = self.reactor.callLater(seconds, self.handle_exit(callback))
        return handle

    def remove_alarm(self, handle: DelayedCall) -> bool:
        """
        Remove an alarm.

        Returns True if the alarm exists, False otherwise
        """
        try:
            handle.cancel()

        except (AlreadyCancelled, AlreadyCalled):
            return False

        return True

    def watch_file(self, fd: int, callback: Callable[[], _T]) -> int:
        """Call callback() when fd has some data to read.

        No parameters are passed to callback. Returns a handle that may be passed to remove_watch_file().

        :param fd: file descriptor to watch for input
        :param callback: function to call when input is available
        """
        ind = _TwistedInputDescriptor(self.reactor, fd, self.handle_exit(callback))
        self._watch_files[fd] = ind
        self.reactor.addReader(typing.cast("IReadDescriptor", ind))
        return fd

    def remove_watch_file(self, handle: int) -> bool:
        """
        Remove an input file.

        Returns True if the input file exists, False otherwise
        """
        if handle in self._watch_files:
            self.reactor.removeReader(typing.cast("IReadDescriptor", self._watch_files[handle]))
            del self._watch_files[handle]
            return True
        return False

    def enter_idle(self, callback: Callable[[], typing.Any]) -> int:
        """
        Add a callback for entering idle.

        Returns a handle that may be passed to remove_enter_idle()
        """
        self._idle_handle += 1
        self._idle_callbacks[self._idle_handle] = callback
        return self._idle_handle

    def _enable_twisted_idle(self) -> None:
        """Approximate an enter-idle callback with a very short timer, since Twisted's reactor has none.

        .. WARNING::
           This will perform worse than the other event loops until we can find a
           fix or workaround
        """
        if self._twisted_idle_enabled:
            return
        self.reactor.callLater(
            self._idle_emulation_delay,
            self.handle_exit(self._twisted_idle_callback, enable_idle=False),
        )
        self._twisted_idle_enabled = True

    def _twisted_idle_callback(self) -> None:
        for callback in self._idle_callbacks.values():
            callback()
        self._twisted_idle_enabled = False

    def remove_enter_idle(self, handle: int) -> bool:
        """
        Remove an idle callback.

        Returns True if the handle was removed.
        """
        try:
            del self._idle_callbacks[handle]
        except KeyError:
            return False
        return True

    def run(self) -> None:
        """Start the event loop.

        Exit the loop when any callback raises an exception. If ExitMainLoop is raised, exit cleanly.

        :raises BaseException: the exception that stopped the loop, once the loop has been left.
        """
        if not self.manage_reactor:
            return
        self.reactor.run()
        if self._exc:
            # An exception caused us to exit, raise it now
            exc = self._exc
            self._exc = None
            raise exc.with_traceback(exc.__traceback__)

    def handle_exit(self, f: Callable[_Spec, _T], enable_idle: bool = True) -> Callable[_Spec, _T | None]:
        """Wrap *f* so that :exc:`ExitMainLoop` raised inside it exits the :class:`TwistedEventLoop` cleanly.

        Store the exception info if some other exception occurs, it will be reraised after the loop quits.

        *f* -- function to be wrapped
        """

        @functools.wraps(f)
        def wrapper(*args: _Spec.args, **kwargs: _Spec.kwargs) -> _T | None:
            rval = None
            try:
                rval = f(*args, **kwargs)
            except ExitMainLoop:
                if self.manage_reactor:
                    self.reactor.stop()
            except BaseException as exc:  # noqa: BLE001  # we will crash if needed
                print(sys.exc_info())
                self._exc = exc
                if self.manage_reactor:
                    self.reactor.crash()
            if enable_idle:
                self._enable_twisted_idle()
            return rval

        return wrapper
urwid-urwid-69907e4/urwid/event_loop/zmq_loop.py000066400000000000000000000331271526073734300220230ustar00rootroot00000000000000# Urwid main loop code using ZeroMQ queues
#    Copyright (C) 2019 Dave Jones
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/

"""ZeroMQ based urwid EventLoop implementation.

`ZeroMQ `_ library is required.
"""

from __future__ import annotations

import asyncio
import errno
import heapq
import inspect
import logging
import time
import typing
from itertools import count

import zmq
import zmq.asyncio

from .abstract_loop import EventLoop, ExitMainLoop, SupportsFileno

if typing.TYPE_CHECKING:
    from collections.abc import Callable
    from concurrent.futures import Executor, Future

    from typing_extensions import ParamSpec

    ZMQAlarmHandle = tuple[float, int, Callable[[], typing.Any]]
    _T = typing.TypeVar("_T")
    _Spec = ParamSpec("_Spec")


class ZMQEventLoop(EventLoop):
    """This class is an urwid event loop for `ZeroMQ`_ applications.

    It is very similar to :class:`SelectEventLoop`, supporting the usual :meth:`alarm`
    events and file watching (:meth:`watch_file`) capabilities, but also incorporates
    the ability to watch zmq queues for events (:meth:`watch_queue`).

    .. _ZeroMQ: https://zeromq.org/

    .. note::
        :meth:`alarm`, :meth:`watch_file`, :meth:`watch_queue` and :meth:`enter_idle` accept an
        ``async def`` callback in addition to a plain callable. :class:`ZMQEventLoop` runs on top
        of an :mod:`asyncio` loop (via :class:`zmq.asyncio.Poller`), so a coroutine function is
        scheduled as a background task there instead of being called directly, the same way it
        would be on any other :mod:`asyncio`-backed event loop.
    """

    _alarm_break = count()

    def __init__(self) -> None:
        """Initialize with a fresh zmq poller and no alarms or watched queues yet."""
        super().__init__()
        self.logger = logging.getLogger(__name__).getChild(self.__class__.__name__)
        self._did_something = True
        self._alarms: list[tuple[float, int, Callable[[], typing.Any]]] = []
        self._poller = zmq.asyncio.Poller()
        self._queue_callbacks: dict[int | zmq.Socket[typing.Any], Callable[[], typing.Any]] = {}
        self._idle_handle = 0
        self._idle_callbacks: dict[int, Callable[[], typing.Any]] = {}
        self._background_tasks: set[asyncio.Task[typing.Any]] = set()
        self._main_task: asyncio.Task[None] | None = None
        self._exc: BaseException | None = None

    def _run_callback(self, callback: Callable[_Spec, _T], *args: _Spec.args, **kwargs: _Spec.kwargs) -> _T | None:
        """Call callback, scheduling it as a background task instead if it is a coroutine function.

        :param callback: function or coroutine function to call
        :param args: positional arguments to pass to callback
        :param kwargs: keyword arguments to pass to callback
        :return: callback return value, or None if it was scheduled as a background task
        """
        if inspect.iscoroutinefunction(callback):
            task = asyncio.get_running_loop().create_task(callback(*args, **kwargs))
            self._background_tasks.add(task)
            task.add_done_callback(self._background_tasks.discard)
            return None
        return callback(*args, **kwargs)

    def run_in_executor(
        self,
        executor: Executor,
        func: Callable[_Spec, _T],
        *args: _Spec.args,
        **kwargs: _Spec.kwargs,
    ) -> Future[_T]:
        """Run callable in executor.

        :param executor: Executor to use for running the function
        :param func: function to call
        :param args: positional arguments to function
        :param kwargs: keyword arguments to function
        :return: future object for the function call outcome.
        """
        return executor.submit(func, *args, **kwargs)

    def alarm(self, seconds: float, callback: Callable[[], typing.Any]) -> ZMQAlarmHandle:
        """Call *callback* a given time from now.

        No parameters are passed to callback. Returns a handle that may be passed to :meth:`remove_alarm`.

        :param float seconds:
            floating point time to wait before calling callback.

        :param callback:
            function to call from event loop.
        """
        handle = (time.time() + seconds, next(self._alarm_break), callback)
        heapq.heappush(self._alarms, handle)
        return handle

    def remove_alarm(self, handle: ZMQAlarmHandle) -> bool:
        """Remove an alarm.

        Returns ``True`` if the alarm exists, ``False`` otherwise.
        """
        try:
            self._alarms.remove(handle)
            heapq.heapify(self._alarms)

        except ValueError:
            return False

        return True

    def watch_queue(
        self,
        queue: zmq.Socket[typing.Any],
        callback: Callable[[], typing.Any],
        flags: int = zmq.POLLIN,
    ) -> zmq.Socket[typing.Any]:
        """Call *callback* when zmq *queue* becomes ready to read or write.

        *flags* controls the condition: ``POLLIN`` (the default) watches for data to read, ``POLLOUT`` watches for
        availability to write. No parameters are passed to the callback. Returns a handle that may be passed to
        :meth:`remove_watch_queue`.

        :param queue:
            The zmq queue to poll.

        :param callback:
            The function to call when the poll is successful.

        :param int flags:
            The condition to monitor on the queue (defaults to ``POLLIN``).
        :raises ValueError: *queue* is already being watched, by this method or by :meth:`watch_file`.
        """
        if queue in self._queue_callbacks:
            raise ValueError(f"already watching {queue!r}")
        self._poller.register(queue, flags)
        self._queue_callbacks[queue] = callback
        return queue

    def watch_file(
        self,
        fd: int | SupportsFileno,
        callback: Callable[[], typing.Any],
        flags: int = zmq.POLLIN,
    ) -> int | SupportsFileno:
        """Call *callback* when *fd* has some data to read.

        No parameters are passed to the callback. The *flags* are as for :meth:`watch_queue`.
        Returns a handle that may be passed to :meth:`remove_watch_file`.

        .. note::
            For a zmq socket, this method and :meth:`watch_queue` set the same watch.
            Calling this method on a watched socket replaces its callback and flags,
            while :meth:`watch_queue` refuses a watched socket.
            Either :meth:`remove_watch_file` or :meth:`remove_watch_queue` removes it.

        :param fd:
            The file-like object, or fileno to monitor.
            A zmq socket is watched for its 0MQ events, as by :meth:`watch_queue`.
            Its ``fileno()`` is the socket's notification descriptor and does not name the same watch.

        :param callback:
            The function to call when the file has data available.

        :param int flags:
            The condition to monitor on the file (defaults to ``POLLIN``).
        """
        # The key the poller reports a ready entry by: the socket for a zmq socket, the descriptor number otherwise.
        key = fd if isinstance(fd, (int, zmq.Socket)) else fd.fileno()
        self._poller.register(key, flags)
        self._queue_callbacks[key] = callback
        return fd

    def remove_watch_queue(self, handle: zmq.Socket[typing.Any]) -> bool:
        """Remove a queue from background polling.

        Returns ``True`` if the queue was being monitored, ``False`` otherwise.
        """
        try:
            try:
                self._poller.unregister(handle)
            finally:
                self._queue_callbacks.pop(handle, None)

        except KeyError:
            return False

        return True

    def remove_watch_file(self, handle: int | SupportsFileno) -> bool:
        """Remove a file from background polling.

        *handle* may be the object given to :meth:`watch_file` or, for an ordinary file, its descriptor number.
        A zmq socket is named by the socket object itself.
        Returns ``True`` if the file was being monitored, ``False`` otherwise.
        """
        key = handle if isinstance(handle, (int, zmq.Socket)) else handle.fileno()
        try:
            try:
                self._poller.unregister(key)
            finally:
                self._queue_callbacks.pop(key, None)

        except KeyError:
            return False

        return True

    def enter_idle(self, callback: Callable[[], typing.Any]) -> int:
        """Add a *callback* to be executed when the event loop detects it is idle.

        Returns a handle that may be passed to :meth:`remove_enter_idle`.
        """
        self._idle_handle += 1
        self._idle_callbacks[self._idle_handle] = callback
        return self._idle_handle

    def remove_enter_idle(self, handle: int) -> bool:
        """Remove an idle callback.

        Returns ``True`` if *handle* was removed, ``False`` otherwise.
        """
        try:
            del self._idle_callbacks[handle]
        except KeyError:
            return False

        return True

    def _entering_idle(self) -> None:
        for callback in list(self._idle_callbacks.values()):
            self._run_callback(callback)

    def _exception_handler(self, loop: asyncio.AbstractEventLoop, context: dict[str, typing.Any]) -> None:
        """Handle an exception from a background task scheduled by :meth:`_run_callback`.

        A background task is never awaited directly, so its exception would otherwise only
        surface through this handler once the task is garbage collected - store it and cancel
        :attr:`_main_task` to stop the loop right away instead of waiting on that.

        :param loop: the loop the exception occurred on
        :param context: exception context, as passed by :mod:`asyncio` to an exception handler
        """
        if exc := context.get("exception"):
            if not isinstance(exc, ExitMainLoop):
                self._exc = exc
            if self._main_task is not None:
                self._main_task.cancel()
        else:
            loop.default_exception_handler(context)

    def run(self) -> None:
        """Start the event loop.

        Exit the loop when any callback raises an exception. If :exc:`ExitMainLoop` is raised, exit cleanly.

        :raises BaseException: the exception that stopped the loop, once the loop has been left.
        """
        asyncio.run(self._run_async())
        if self._exc:
            exc, self._exc = self._exc, None
            raise exc.with_traceback(exc.__traceback__)

    async def _run_async(self) -> None:
        """Drive :meth:`_loop` until :exc:`ExitMainLoop` is raised, directly or from a background task."""
        asyncio.get_running_loop().set_exception_handler(self._exception_handler)
        self._main_task = typing.cast("asyncio.Task[None]", asyncio.current_task())
        try:
            while True:
                try:
                    await self._loop()
                except zmq.error.ZMQError as exc:  # noqa: PERF203
                    if exc.errno != errno.EINTR:
                        raise
        except (ExitMainLoop, asyncio.CancelledError):
            pass
        finally:
            self._main_task = None

    async def _loop(self) -> None:
        """Run a single iteration of the event loop."""
        state = "wait"  # default state not expecting any action
        polled_callbacks = dict(self._queue_callbacks)
        if self._alarms or self._did_something:
            timeout = 0.0
            if self._alarms:
                state = "alarm"
                timeout = max(0.0, self._alarms[0][0] - time.time())
            if self._did_something and (not self._alarms or (self._alarms and timeout > 0)):
                state = "idle"
                timeout = 0
            ready = dict(await self._poller.poll(int(timeout * 1000)))
        else:
            ready = dict(await self._poller.poll())

        if not ready:
            if state == "idle":
                self._entering_idle()
                self._did_something = False
            elif state == "alarm":
                _due, _tie_break, callback = heapq.heappop(self._alarms)
                self._run_callback(callback)
                self._did_something = True

        for queue in ready:
            # A callback earlier in this pass, or another task during the poll, may have removed or replaced the
            # watch and closed its descriptor.
            if queue in polled_callbacks and self._queue_callbacks.get(queue) is not polled_callbacks[queue]:
                continue
            self._run_callback(self._queue_callbacks[queue])
            self._did_something = True

        # zmq.asyncio.Poller.poll() resolves synchronously (no checkpoint) whenever
        # something is already ready, so a busy queue or alarm would otherwise never
        # let any other task on this loop - including a background task just scheduled
        # by _run_callback() above - get a turn to run. Yield once per iteration to
        # guarantee one.
        await asyncio.sleep(0)
urwid-urwid-69907e4/urwid/font.py000066400000000000000000001034161526073734300167560ustar00rootroot00000000000000# Urwid BigText fonts
#    Copyright (C) 2004-2006  Ian Ward
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/


"""Fonts for the BigText widget, and the registry they are looked up in."""

from __future__ import annotations

import typing
import warnings
from pprint import pformat

import wcwidth

from urwid.canvas import CanvasError, TextCanvas
from urwid.display.escape import SAFE_ASCII_DEC_SPECIAL_RE
from urwid.util import apply_target_encoding

if typing.TYPE_CHECKING:
    from collections.abc import Iterator, Sequence


def separate_glyphs(gdata: str, height: int) -> tuple[dict[str, tuple[int, list[str]]], bool]:
    """Return (dictionary of glyphs, utf8 required).

    :raises ValueError: *gdata* contains tabs, does not have *height* plus one lines per glyph, or has a glyph whose
        columns do not line up.
    """
    gl: list[str] = gdata.split("\n")[1:-1]

    if any("\t" in elem for elem in gl):
        raise ValueError(f"Incorrect glyphs data:\n{gdata!r}")

    if len(gl) != height + 1:
        raise ValueError(f"Incorrect glyphs height (expected: {height}):\n{gdata}")

    key_line: str = gl[0]

    character: str | None = None  # current character
    key_index = 0  # index into character key line
    end_col = 0  # column position at end of glyph
    start_col = 0  # column position at start of glyph
    jl: list[int] = [0] * height  # indexes into lines of gdata (gl)
    result: dict[str, tuple[int, list[str]]] = {}
    utf8_required = False
    while True:
        if character is None:
            if key_index >= len(key_line):
                break
            character = key_line[key_index]

        if key_index < len(key_line) and key_line[key_index] == character:
            end_col += wcwidth.width(character, control_codes="ignore")
            key_index += 1
            continue

        out: list[str] = []
        y = 0
        fill = 0

        for k, line in enumerate(gl[1:]):
            j: int = jl[k]
            y = 0
            fill = 0
            while y < end_col - start_col:
                if j >= len(line):
                    fill = end_col - start_col - y
                    break
                y += wcwidth.width(line[j], control_codes="ignore")
                j += 1
            if y + fill != end_col - start_col:
                raise ValueError(repr((y, fill, end_col)))

            segment = line[jl[k] : j]
            if not SAFE_ASCII_DEC_SPECIAL_RE.match(segment):
                utf8_required = True

            out.append(segment + " " * fill)
            jl[k] = j

        start_col = end_col
        result[character] = (y + fill, out)
        character = None
    return result, utf8_required


def add_font(name: str, cls: FontRegistry) -> None:
    """
    Register a font class under the given name.

    :param name: name to register the font class under
    :param cls: the font class to register

    .. deprecated:: 2.2.0
        Set the ``name`` attribute on the font class, pass the ``font_name`` metaclass keyword argument,
        or call ``Font.register()`` instead. This API will be removed in version 5.0.
    """
    warnings.warn(
        "`add_font` is deprecated, please set 'name' attribute to the font class,"
        " use metaclass keyword argument 'font_name'"
        " or use `Font.register()`. "
        "API will be removed in version 5.0.",
        DeprecationWarning,
        stacklevel=2,
    )
    cls.register(name)


class FontRegistryWarning(UserWarning):
    """FontRegistry warning."""


class FontRegistry(type):
    """Font registry.

    Store all registered fonts, register during class creation if possible.
    """

    __slots__ = ()

    __registered: typing.ClassVar[dict[str, FontRegistry]] = {}

    def __iter__(cls) -> Iterator[str]:
        """Iterate over registered font names."""
        return iter(cls.__registered)

    def __getitem__(cls, item: str) -> FontRegistry | None:
        """Get font by name if registered."""
        return cls.__registered.get(item)

    def __class_getitem__(mcs, item: str) -> FontRegistry | None:
        r"""Get font by name if registered.

        This method is needed to get access to font from registry class.
        >>> from urwid.util import set_temporary_encoding
        >>> repr(FontRegistry["a"])
        'None'
        >>> font = FontRegistry["Thin 3x3"]()
        >>> font.height
        3
        >>> with set_temporary_encoding("utf-8"):
        ...     canvas: TextCanvas = font.render("+")
        >>> b"\n".join(canvas.text).decode("utf-8")
        '  \n ┼\n  '
        """
        return mcs.__registered.get(item)

    @property
    def registered(cls) -> Sequence[str]:
        """Registered font names in alphabetical order."""
        return tuple(sorted(cls.__registered))

    @classmethod
    def as_list(mcs) -> list[tuple[str, FontRegistry]]:
        """List of (font name, font class) tuples."""
        return list(mcs.__registered.items())

    def __new__(
        mcs: type[FontRegistry],
        name: str,
        bases: tuple[type, ...],
        namespace: dict[str, typing.Any],
        **kwds: typing.Any,
    ) -> FontRegistry:
        """Create the new font class and register it under its ``name``, if any."""
        font_name: str = namespace.setdefault("name", kwds.get("font_name", ""))
        font_class = super().__new__(mcs, name, bases, namespace)
        if font_name:
            if font_name not in mcs.__registered:
                mcs.__registered[font_name] = font_class
            if mcs.__registered[font_name] != font_class:
                warnings.warn(
                    f"{font_name!r} is already registered, please override explicit if required or change name",
                    FontRegistryWarning,
                    stacklevel=2,
                )
        return font_class

    def register(cls, font_name: str) -> None:
        """Register font explicit.

        :param font_name: Font name to use in registration.
        :raises ValueError: *font_name* is empty.
        """
        if not font_name:
            raise ValueError('"font_name" is not set.')
        cls.__registered[font_name] = cls


get_all_fonts = FontRegistry.as_list


class Font(metaclass=FontRegistry):
    """Font base class."""

    __slots__ = ("canvas", "char", "utf8_required")

    height: int  # pylint: disable=declare-non-slot
    data: Sequence[str]  # pylint: disable=declare-non-slot
    name: str  # pylint: disable=declare-non-slot

    def __init__(self) -> None:
        """
        Decode the glyph data of this font class.

        :raises ValueError: the font class sets no height or no glyph data.
        """
        if not self.height:
            raise ValueError(f'"height" is invalid: {self.height!r}')
        if not self.data:
            raise ValueError(f'"data" is empty: {self.data!r}')

        self.char: dict[str, tuple[int, list[str]]] = {}
        self.canvas: dict[str, TextCanvas] = {}
        self.utf8_required = False
        if isinstance(self.data, str):
            self.add_glyphs(self.data)

        else:
            for gdata in self.data:
                self.add_glyphs(gdata)

    def __repr__(self) -> str:
        """Return the class name as a no-argument constructor call."""
        return f"{self.__class__.__name__}()"

    def __str__(self) -> str:
        """Font description."""
        return f"{self.__class__.__name__}():\n  {self.height!r}\n  {pformat(self.data, indent=4)}"

    def add_glyphs(self, gdata: str) -> None:
        """Parse *gdata* into glyphs and add them to this font's character set."""
        d, utf8_required = separate_glyphs(gdata, self.height)
        self.char.update(d)
        self.utf8_required |= utf8_required

    def characters(self) -> str:
        """Return the characters this font has glyphs for, in sorted order."""
        return "".join(sorted(self.char))

    def char_width(self, character: str) -> int:
        """Return the glyph width of *character*, or 0 if this font has no glyph for it."""
        if character in self.char:
            return self.char[character][0]
        return 0

    def char_data(self, character: str) -> list[str]:
        """Return the raw glyph data rows for *character*."""
        return self.char[character][1]

    def render(self, character: str) -> TextCanvas:
        """
        Return a canvas with *character* drawn in this font.

        :raises CanvasError: the glyph data for *character* cannot be rendered.
        """
        if character in self.canvas:
            return self.canvas[character]
        width, line = self.char[character]
        byte_lines = []
        character_set_lines = []
        for d in line:
            t, cs = apply_target_encoding(d)
            byte_lines.append(t)
            character_set_lines.append(cs)

        try:
            canv = TextCanvas(byte_lines, None, character_set_lines, maxcol=width, check_width=False)
        except CanvasError as exc:
            raise CanvasError(f"Failed render of {character!r} from line {line!r}:\n{self}\n:{exc}").with_traceback(
                exc.__traceback__
            ) from exc

        self.canvas[character] = canv
        return canv


# safe_palette = u"┘�┌└┼─├┤┴┬│"
# more_palette = u"�║╒╓╔╕╖╗╘╙╚╛╜�╞╟╠╡╢╣╤╥╦╧╨╩╪╫╬○"
# block_palette = u"▄#█#▀#▌#�#▖#▗#▘#▙#▚#▛#▜#�#▞#▟"


class Thin3x3Font(Font):
    """3x3 glyph font drawn with thin box-drawing lines, registered as ``"Thin 3x3"``."""

    name = "Thin 3x3"
    height = 3
    data = (
        """
000111222333444555666777888999  !
┌─� � ┌─�┌─�  �┌─ ┌─ ┌─�┌─�┌─�  │
│ │ │ ┌─┘ ─┤└─┼└─�├─�  ┼├─┤└─┤  │
└─┘ ┴ └─ └─┘  ┴ ─┘└─┘  ┴└─┘ ─┘  .
""",
        r"""
"###$$$%%%'*++,--.///:;==???[[\\\]]^__`
" ┼┼┌┼�O /'         /.. _┌─�┌ \   �^  `
  ┼┼└┼� /  * ┼  ─  / ., _ ┌┘│  \  │
    └┼┘/ O    ,  ./       . └   \ ┘ ──
""",
    )


class Thin4x3Font(Font):
    """4x3 glyph font drawn with thin box-drawing lines, registered as ``"Thin 4x3"``."""

    name = "Thin 4x3"
    height = 3
    data = (
        *Thin3x3Font.data,
        """
0000111122223333444455556666777788889999  ####$$$$
┌──�  � ┌──�┌──�   �┌── ┌── ┌──�┌──�┌──�   ┼─┼┌┼┼�
│  │  │ ┌──┘  ─┤└──┼└──�├──�   ┼├──┤└──┤   ┼─┼└┼┼�
└──┘  ┴ └── └──┘   ┴ ──┘└──┘   ┴└──┘ ──┘      └┼┼┘
""",
        r"""
AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHIJJJJKKKKLLLLMMMMNNNNOOOOPPPPQQQQRRRRSSSSTTTUUUUVVVVWWWWXXXXYYYYZZZZ
┌──�┬──�┌──�┬──�┌───┌───┌── ┬  ┬┬   �┬ ┌─┬   ┌�┌�┬  ┬╭──╮┌──�┌──�┌──�┌── ───┬  ┬┬  ┬┬  ┬\  /\  /────
├──┤├──┤│   │  │├── ├── │ ─�├──┤│   │├─┘ │   │└┘││\ ││  │├──┘│  │├──┘└──� │ │  │ \/ │┌�│ \/  \/  /
┴  ┴┴──┘└──┘┴──┘└───┴   └──┘┴  ┴┴└──┘┴ └─└───┴  ┴┴  ┴╰──╯┴   └─\┘┴ └─ ──┘ ┴ └──┘  ┴ └┘└┘ /\   ┴ ────
""",
    )


class Sextant3x3Font(Font):
    """3x3 glyph font drawn with Unicode sextant block characters, registered as ``"Sextant 3x3"``."""

    name = "Sextant 3x3"
    height = 3
    data = (
        """
   !!!###$$$%%%&&&'''((()))***+++,,,---...///
    � 🬞🬲🬲🬞�🬋🬉🬄🬖🬦🬧  🬉 🬞🬅 �🬢 🬞🬦🬞 🬦            🬖
    🬉 🬇🬛🬛🬞🬰🬗🬞🬅🬭🬦🬈🬖   🬉�  🬘 🬇🬨🬈�🬨🬂 🬭 �🬂🬂 🬭 🬞🬅
    �  🬀🬀 �   🬂 🬂�    � �         🬅     🬂
""",
        """
000111222333444555666777888999
🬦🬂🬧🬞🬫 🬇🬂🬧�🬂🬧 🬞🬫�🬂🬂🬞🬅🬀�🬂🬙🬦🬂🬧🬦🬂🬧
��� � 🬞🬅🬀 🬂🬧🬇🬌🬫�🬂🬧�🬂🬧 🬔 🬦🬂🬧 🬂🬙
 🬂🬀 � �🬂🬂�🬂🬀  ��🬂🬀 🬂🬀 🬀  🬂🬀 🬂
""",
        """
\"\"\"
 🬄🬄


""",
        """
:::;;;<<<===>>>???@@@
 🬭  🬭  🬖🬀   �🬢 🬇🬂🬧🬦🬂🬧
 🬰  🬰 �🬢 🬠🬰🬰 🬖🬀 🬇🬀�🬉🬅
 🬂  🬅   🬀   �   �  🬂🬀
""",
        """
AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQ
🬞🬅🬢�🬂🬧🬦🬂🬈�🬂🬧�🬂🬂�🬂🬂🬦🬂🬈� � 🬨🬀  ��🬞🬅�  �🬢🬫�🬢�🬦🬂🬧�🬂🬧🬦🬂🬧
�🬋🬫�🬂🬧� 🬞� ��🬂 �🬂 � 🬨�🬂🬨 � 🬞 ��🬈��  ���� 🬨� ��🬂🬀�🬇🬘
� ��🬂🬀 🬂🬀�🬂🬀�🬂🬂�   🬂🬂� � 🬂🬀 🬂🬀� ��🬂🬂� �� � 🬂🬀�   🬂�
""",
        """
RRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[]]]^^^___```
�🬂🬧🬦🬂🬈�🬨🬂� �� �� �🬉�🬘� ��🬂🬙 🬕🬀 🬂▌🬞🬅🬢    🬈�
�🬊�🬞🬂🬧 � � �🬉�🬘�🬖🬷🬞🬅🬢 🬧🬀🬞🬅  ▌   ▌
� � 🬂🬀 �  🬂🬀 � � �� � � �🬂🬂 🬂🬀 🬂🬀   🬂🬂🬂
""",
        """
\\\\\\
🬇�
 �🬢

""",
    )


class Sextant2x2Font(Font):
    """2x2 glyph font drawn with Unicode sextant block characters, registered as ``"Sextant 2x2"``."""

    name = "Sextant 2x2"
    height = 2
    data = """
..,,%%00112233445566778899
    �🬖�🬨🬇▌�🬗🬠🬸🬦��🬒�🬭�🬙�🬸�🬸
🬇 🬇🬀�🬇🬉� 🬄🬉🬋🬇��🬊🬇🬅🬉� 🬄🬉� 🬉
"""


class HalfBlock5x4Font(Font):
    """5x4 glyph font drawn with half-block characters, registered as ``"Half Block 5x4"``."""

    name = "Half Block 5x4"
    height = 4
    data = (
        """
00000111112222233333444445555566666777778888899999  !!
▄▀▀▄  ▄█  ▄▀▀▄ ▄▀▀▄ ▄  █ █▀▀▀ ▄▀▀  ▀▀▀█ ▄▀▀▄ ▄▀▀▄   █
█  █   █    ▄▀   ▄▀ █▄▄█ █▄▄  █▄▄    �▌ ▀▄▄▀ ▀▄▄█   █
â–ˆ  â–ˆ   â–ˆ  â–„â–€   â–„  â–ˆ    â–ˆ    â–ˆ â–ˆ  â–ˆ   â–ˆ  â–ˆ  â–ˆ    â–ˆ   â–€
 ▀▀   ▀▀▀ ▀▀▀▀  ▀▀     ▀ ▀▀▀   ▀▀    ▀   ▀▀   ▀▀    ▀
""",
        '''
"""######$$$$$$%%%%%&&&&&((()))******++++++,,,-----..////::;;;
█�▌ █ █  ▄▀█▀▄ �▌�▌ ▄▀▄   █ █   ▄ ▄    ▄              �▌
   ▀█▀█▀ ▀▄█▄    █  ▀▄▀  �▌ �▌ ▄▄█▄▄ ▄▄█▄▄    ▄▄▄▄    █  ▀  ▀
   ▀█▀█▀ ▄ █ █  �▌▄ █ ▀▄▌�▌ �▌  ▄▀▄    █             �▌  ▀ ▄▀
    ▀ ▀   ▀▀▀   ▀ ▀  ▀▀   ▀ ▀              ▄▀      ▀ ▀
''',
        r"""
<<<<<=====>>>>>?????@@@@@@[[[[\\\\]]]]^^^^____```{{{{||}}}}~~~~''```
  ▄▀      ▀▄   ▄▀▀▄ ▄▀▀▀▄ █▀▀ �▌  ▀▀█ ▄▀▄     ▀▄  ▄▀ █ ▀▄   ▄  █ ▄▀
▄▀   ▀▀▀▀   ▀▄   ▄▀ █ █▀█ █    █    █            ▄▀  █  ▀▄ ��▌▌
 ▀▄  ▀▀▀▀  ▄▀    ▀  █ ▀▀▀ █    �▌   █             █  █  █    ▀
   ▀      ▀      ▀   ▀▀▀  ▀▀▀   ▀ ▀▀▀     ▀▀▀▀     ▀ ▀ ▀
""",
        """
AAAAABBBBBCCCCCDDDDDEEEEEFFFFFGGGGGHHHHHIIJJJJJKKKKK
▄▀▀▄ █▀▀▄ ▄▀▀▄ █▀▀▄ █▀▀▀ █▀▀▀ ▄▀▀▄ █  █ █    █ █  █
█▄▄█ █▄▄▀ █    █  █ █▄▄  █▄▄  █    █▄▄█ █    █ █▄▀
█  █ █  █ █  ▄ █  █ █    █    █ ▀█ █  █ █ ▄  █ █ ▀▄
▀  ▀ ▀▀▀   ▀▀  ▀▀▀  ▀▀▀▀ ▀     ▀▀  ▀  ▀ ▀  ▀▀  ▀  ▀
""",
        """
LLLLLMMMMMMNNNNNOOOOOPPPPPQQQQQRRRRRSSSSSTTTTT
█    █▄ ▄█ ██ █ ▄▀▀▄ █▀▀▄ ▄▀▀▄ █▀▀▄ ▄▀▀▄ ▀▀█▀▀
█    █ ▀ █ █�▌█ █  █ █▄▄▀ █  █ █▄▄▀ ▀▄▄    █
█    █   █ █ ██ █  █ █    █ ▌█ █  █ ▄  █   █
▀▀▀▀ ▀   ▀ ▀  ▀  ▀▀  ▀     ▀▀▌ ▀  ▀  ▀▀    ▀
""",
        """
UUUUUVVVVVVWWWWWWXXXXXXYYYYYYZZZZZ
█  █ █   █ █   █ █   █ █   █ ▀▀▀█
█  █ �▌ �▌ █ ▄ █  ▀▄▀   ▀▄▀   ▄▀
█  █  █ █  �▌█�▌ ▄▀ ▀▄   █   █
 ▀▀    ▀    ▀ ▀  ▀   ▀   ▀   ▀▀▀▀
""",
        """
aaaaabbbbbcccccdddddeeeeeffffggggghhhhhiijjjjkkkkk
     █            █       ▄▀▀     █    ▄   ▄ █
 ▀▀▄ █▀▀▄ ▄▀▀▄ ▄▀▀█ ▄▀▀▄ ▀█▀ ▄▀▀▄ █▀▀▄ ▄   ▄ █ ▄▀
▄▀▀█ █  █ █  ▄ █  █ █▀▀   █  ▀▄▄█ █  █ █   █ █▀▄
 ▀▀▀ ▀▀▀   ▀▀   ▀▀▀  ▀▀   ▀   ▄▄▀ ▀  ▀ ▀ ▄▄▀ ▀  ▀
""",
        """
llmmmmmmnnnnnooooopppppqqqqqrrrrssssstttt
â–ˆ                                     â–ˆ
█ █▀▄▀▄ █▀▀▄ ▄▀▀▄ █▀▀▄ ▄▀▀█ █▀▀ ▄▀▀▀ ▀█▀
█ █ █ █ █  █ █  █ █  █ █  █ █    ▀▀▄  █
▀ ▀   ▀ ▀  ▀  ▀▀  █▀▀   ▀▀█ ▀   ▀▀▀    ▀
""",
        """
uuuuuvvvvvwwwwwwxxxxxxyyyyyzzzzz

█  █ █  █ █ ▄ █ ▀▄ ▄▀ █  █ ▀▀█▀
█  █ �▌�▌ �▌█�▌  ▄▀▄  ▀▄▄█ ▄▀
 ▀▀   ▀▀   ▀ ▀  ▀   ▀  ▄▄▀ ▀▀▀▀
""",
    )


class HalfBlock6x5Font(Font):
    """6x5 glyph font drawn with half-block characters, registered as ``"Half Block 6x5"``."""

    name = "Half Block 6x5"
    height = 5
    data = """
000000111111222222333333444444555555666666777777888888999999  ..::////
▄▀▀▀▄  ▄█   ▄▀▀▀▄ ▄▀▀▀▄ ▄  █  █▀▀▀▀ ▄▀▀▀  ▀▀▀▀█ ▄▀▀▀▄ ▄▀▀▀▄         █
█   █   █       █     █ █  █  █     █        �▌ █   █ █   █     ▀  �▌
█   █   █     ▄▀    ▀▀▄ ▀▀▀█▀ ▀▀▀▀▄ █▀▀▀▄    █  ▄▀▀▀▄  ▀▀▀█     ▄  █
█   █   █   ▄▀    ▄   █    █      █ █   █   �▌  █   █     █       �▌
 ▀▀▀   ▀▀▀  ▀▀▀▀▀  ▀▀▀     ▀  ▀▀▀▀   ▀▀▀    ▀    ▀▀▀   ▀▀▀    ▀   ▀
"""


class HalfBlockHeavy6x5Font(Font):
    """6x5 glyph font drawn with heavy half-block characters, registered as ``"Half Block Heavy 6x5"``."""

    name = "Half Block Heavy 6x5"
    height = 5
    data = """
000000111111222222333333444444555555666666777777888888999999  ..::////
▄███▄  �█▌  ▄███▄ ▄███▄    █▌ █████ ▄███▄ █████ ▄███▄ ▄███▄         █▌
█▌ �█  ▀█▌  ▀  �█ ▀  �█ █▌ █▌ █▌    █▌       █▌ █▌ �█ █▌ �█     █▌ �█
█▌ �█   █▌    ▄█▀   ██▌ █████ ████▄ ████▄   �█  �███▌ ▀████        █▌
█▌ �█   █▌  ▄█▀   ▄  �█    █▌    �█ █▌ �█   █▌  █▌ �█    �█     █▌�█
▀███▀  ███▌ █████ ▀███▀    █▌ ████▀ ▀███▀  �█   ▀███▀ ▀███▀   █▌  █▌
"""


class Thin6x6Font(Font):
    """6x6 glyph font drawn with thin box-drawing lines, registered as ``"Thin 6x6"``."""

    name = "Thin 6x6"
    height = 6
    data = (
        """
000000111111222222333333444444555555666666777777888888999999''
┌───�   �   ┌───� ┌───�    �  ┌───  ┌───  ┌───� ┌───� ┌───� │
│   │   │       │     │ ┌  │  │     │         │ │   │ │   │
│ / │   │   ┌───┘    ─┤ └──┼─ └───� ├───�     ┼ ├───┤ └───┤
│   │   │   │         │    │      │ │   │     │ │   │     │
└───┘   ┴   └───  └───┘    ┴   ───┘ └───┘     ┴ └───┘  ───┘

""",
        r'''
!!   """######$$$$$$%%%%%%&&&&&&((()))******++++++
│    ││  ┌ ┌  ┌─┼─� ┌�  /  ┌─�   / \
│       ─┼─┼─ │ │   └┘ /   │ │  │   │  \ /    │
│        │ │  └─┼─�   /   ┌─\┘  │   │ ──X── ──┼──
│       ─┼─┼─   │ │  / ┌� │  \, │   │  / \    │
.        ┘ ┘  └─┼─┘ /  └┘ └───\  \ /

''',
        r"""
,,-----..//////::;;<<<<=====>>>>??????@@@@@@
             /                  ┌───� ┌───�
            /  . .   / ──── \       │ │┌──┤
  ────     /        /        \    ┌─┘ ││  │
          /    . ,  \  ────  /    │   │└──┘
,      . /           \      /     .   └───┘

""",
        r"""
[[\\\\\\]]^^^____``{{||}}~~~~~~
┌ \     � /\     \ ┌ │ �
│  \    │          │ │ │ ┌─�
│   \   │          ┤ │ ├   └─┘
│    \  │          │ │ │
└     \ ┘    ────  └ │ ┘

""",
        """
AAAAAABBBBBBCCCCCCDDDDDDEEEEEEFFFFFFGGGGGGHHHHHHIIJJJJJJ
┌───� ┬───� ┌───� ┬───� ┬───� ┬───� ┌───� ┬   ┬ ┬     ┬
│   │ │   │ │     │   │ │     │     │     │   │ │     │
├───┤ ├───┤ │     │   │ ├──   ├──   │ ──┬ ├───┤ │     │
│   │ │   │ │     │   │ │     │     │   │ │   │ │ ┬   │
┴   ┴ ┴───┘ └───┘ ┴───┘ ┴───┘ ┴     └───┘ ┴   ┴ ┴ └───┘

""",
        """
KKKKKKLLLLLLMMMMMMNNNNNNOOOOOOPPPPPPQQQQQQRRRRRRSSSSSS
┬   ┬ ┬     ┌─┬─� ┬─� ┬ ┌───� ┬───� ┌───� ┬───� ┌───�
│ ┌─┘ │     │ │ │ │ │ │ │   │ │   │ │   │ │   │ │
├─┴�  │     │ │ │ │ │ │ │   │ ├───┘ │   │ ├─┬─┘ └───�
│  └� │     │   │ │ │ │ │   │ │     │  �│ │ └─�     │
┴   ┴ ┴───┘ ┴   ┴ ┴ └─┴ └───┘ ┴     └──┼┘ ┴   ┴ └───┘
                                       â””
""",
        """
TTTTTTUUUUUUVVVVVVWWWWWWXXXXXXYYYYYYZZZZZZ
┌─┬─� ┬   ┬ ┬   ┬ ┬   ┬ ┬   ┬ ┬   ┬ ┌───�
  │   │   │ │   │ │   │ └� ┌┘ │   │   ┌─┘
  │   │   │ │   │ │ │ │  ├─┤  └─┬─┘  ┌┘
  │   │   │ └� ┌┘ │ │ │ ┌┘ └�   │   ┌┘
  ┴   └───┘  └─┘  └─┴─┘ ┴   ┴   ┴   └───┘

""",
        """
aaaaaabbbbbbccccccddddddeeeeeefffgggggghhhhhhiijjj
                              ┌─�
      │               │       │        │     .  .
┌───� ├───� ┌───� ┌───┤ ┌───� ┼  ┌───� ├───� �  �
┌───┤ │   │ │     │   │ ├───┘ │  │   │ │   │ │  │
└───┴ └───┘ └───┘ └───┘ └───┘ ┴  └───┤ ┴   ┴ ┴  │
                                 └───┘         ─┘
""",
        """
kkkkkkllmmmmmmnnnnnnooooooppppppqqqqqqrrrrrssssss

│     │
│ ┌─  │ ┬─┬─� ┬───� ┌───� ┌───� ┌───� ┬──� ┌───�
├─┴�  │ │ │ │ │   │ │   │ │   │ │   │ │    └───�
┴  └─ └ ┴   ┴ ┴   ┴ └───┘ ├───┘ └───┤ ┴    └───┘
                          │         │
""",
        """
ttttuuuuuuvvvvvvwwwwwwxxxxxxyyyyyyzzzzzz

 │
─┼─ ┬   ┬ ┬   ┬ ┬   ┬ ─� ┌─ ┬   ┬ ────┬
 │  │   │ └� ┌┘ │ │ │  ├─┤  │   │ ┌───┘
 └─ └───┴  └─┘  └─┴─┘ ─┘ └─ └───┤ ┴────
                            └───┘
""",
    )


class HalfBlock7x7Font(Font):
    """7x7 glyph font drawn with half-block characters, registered as ``"Half Block 7x7"``."""

    name = "Half Block 7x7"
    height = 7
    data = (
        """
0000000111111122222223333333444444455555556666666777777788888889999999'''
 ▄███▄   �█▌   ▄███▄  ▄███▄     █▌ �█████▌ ▄███▄ �█████▌ ▄███▄  ▄███▄ �█
�█   █▌  ▀█▌  �█   █▌�█   █▌�█  █▌ �█     �█         �█ �█   █▌�█   █▌�█
�█ � █▌   █▌       █▌   �██ �█████▌�████▄ �████▄     █▌  █████  ▀████▌
�█ ▌ █▌   █▌     ▄█▀      █▌    █▌      █▌�█   █▌   �█  �█   █▌     █▌
�█   █▌   █▌   ▄█▀   �█   █▌    █▌      █▌�█   █▌   █▌  �█   █▌     █▌
 ▀███▀   ███▌ �█████▌ ▀███▀     █▌ �████▀  ▀███▀   �█    ▀███▀  ▀███▀

""",
        '''
!!!   """""#######$$$$$$$%%%%%%%&&&&&&&(((())))*******++++++
�█    �█ █▌ �█ █▌    █    ▄  █▌   ▄█▄    █▌�█   ▄▄ ▄▄
�█    �█ █▌�█████▌ ▄███▄ �█▌�█   �█ █▌  �█  █▌  ▀█▄█▀   �█
�█          �█ █▌ �█▄█▄▄  ▀ █▌    ███   █▌  �█ �█████▌ ████▌
�█         �█████▌ ▀▀█▀█▌  �█ ▄  ███▌▄  █▌  �█  ▄█▀█▄   �█
            �█ █▌  ▀███▀   █▌�█▌�█  █▌  �█  █▌  ▀▀ ▀▀
�█                   █    �█  ▀  ▀██▀█▌  █▌�█

''',
        """
,,,------.../////:::;;;<<<<<<<======>>>>>>>???????@@@@@@@
               █▌          ▄█▌      �█▄     ▄███▄  ▄███▄
              �█ �█ �█   ▄█▀  �████▌  ▀█▄  �█   █▌�█ ▄▄█▌
   �████▌     █▌       �██              ██▌    █▌ �█�█▀█▌
             �█  �█ �█   ▀█▄  �████▌  ▄█▀     █▌  �█�█▄█▌
             █▌     ▀      ▀█▌      �█▀           �█ ▀▀▀
�█       �█ �█                                █▌   ▀███▀
â–€
""",
        r"""
[[[[\\\\\]]]]^^^^^^^_____```{{{{{|||}}}}}~~~~~~~```
�██▌�█   �██▌  �█▌       �█    █▌�█ �█           █▌
�█   █▌    █▌ �█ █▌       █▌  █▌ �█  �█   ▄▄    �█
�█   �█    █▌�█   █▌         ▄█▌ �█  �█▄ �▀▀█▄▄▌
�█    █▌   █▌                ▀█▌ �█  �█▀     ▀▀
�█    �█   █▌                 █▌ �█  �█
�██▌   █▌�██▌       █████      █▌�█ �█

""",
        """
AAAAAAABBBBBBBCCCCCCCDDDDDDDEEEEEEEFFFFFFFGGGGGGGHHHHHHHIIIIJJJJJJJ
 ▄███▄ �████▄  ▄███▄ �████▄ �█████▌�█████▌ ▄███▄ �█   █▌ ██▌     █▌
�█   █▌�█   █▌�█     �█   █▌�█     �█     �█     �█   █▌ �█      █▌
�█████▌�█████ �█     �█   █▌�████  �████  �█     �█████▌ �█      █▌
�█   █▌�█   █▌�█     �█   █▌�█     �█     �█  ██▌�█   █▌ �█      █▌
�█   █▌�█   █▌�█     �█   █▌�█     �█     �█   █▌�█   █▌ �█ �█   █▌
�█   █▌�████▀  ▀███▀ �████▀ �█████▌�█      ▀███▀ �█   █▌ ██▌ ▀███▀

""",
        """
KKKKKKKLLLLLLLMMMMMMMMNNNNNNNOOOOOOOPPPPPPPQQQQQQQRRRRRRRSSSSSSS
�█   █▌�█      ▄█▌�█▄ �██  █▌ ▄███▄ �████▄  ▄███▄ �████▄  ▄███▄
�█  █▌ �█     �█ �▌ █▌�██▌ █▌�█   █▌�█   █▌�█   █▌�█   █▌�█
�█▄█▌  �█     �█ �▌ █▌�█�█ █▌�█   █▌�████▀ �█   █▌�█████  ▀███▄
�█▀█▌  �█     �█    █▌�█ █▌█▌�█   █▌�█     �█   █▌�█   █▌     █▌
�█  █▌ �█     �█    █▌�█ �██▌�█   █▌�█     �█ █▌█▌�█   █▌     █▌
�█   █▌�█████▌�█    █▌�█  ██▌ ▀███▀ �█      ▀███▀ �█   █▌ ▀███▀
                                               ▀▀
""",
        """
TTTTTTTUUUUUUUVVVVVVVWWWWWWWWXXXXXXXYYYYYYYZZZZZZZ
 █████▌�█   █▌�█   █▌�█    █▌�█   █▌ █▌  █▌�█████▌
   █▌  �█   █▌ █▌ �█ �█    █▌ �█ █▌  �█ �█     █▌
   █▌  �█   █▌ �█ █▌ �█    █▌  �█▌    �██     █▌
   █▌  �█   █▌  ███  �█ �▌ █▌  ███     █▌    █▌
   █▌  �█   █▌  �█▌  �█ �▌ █▌ █▌ �█    █▌   █▌
   █▌   ▀███▀    █    ▀█▌�█▀ �█   █▌   █▌  �█████▌

""",
        """
aaaaaaabbbbbbbcccccccdddddddeeeeeeefffffggggggghhhhhhhiiijjjj
       �█                 █▌         ▄█▌       �█      █▌  █▌
       �█                 █▌        �█         �█
 ▄███▄ �████▄  ▄███▄  ▄████▌ ▄███▄ �███  ▄███▄ �████▄ �█▌ �█▌
  ▄▄▄█▌�█   █▌�█     �█   █▌�█▄▄▄█▌ �█  �█   █▌�█   █▌ █▌  █▌
�█▀▀▀█▌�█   █▌�█     �█   █▌�█▀▀▀   �█  �█▄▄▄█▌�█   █▌ █▌  █▌
 ▀████▌�████▀  ▀███▀  ▀████▌ ▀███▀  �█    ▀▀▀█▌�█   █▌ █▌  █▌
                                         ▀███▀           �██
""",
        """
kkkkkkkllllmmmmmmmmnnnnnnnooooooopppppppqqqqqqqrrrrrrsssssss
�█      ██
�█      �█
�█  ▄█▌ �█  ▄█▌�█▄ �████▄  ▄███▄ �████▄  ▄████▌ ▄███▌ ▄███▄
�█▄█▀   �█ �█ �▌ █▌�█   █▌�█   █▌�█   █▌�█   █▌�█    �█▄▄▄
�█▀▀█▄  �█ �█ �▌ █▌�█   █▌�█   █▌�█   █▌�█   █▌�█      ▀▀▀█▌
�█   █▌ �█▌�█    █▌�█   █▌ ▀███▀ �████▀  ▀████▌�█     ▀███▀
                                 �█          █▌
""",
        """
tttttuuuuuuuvvvvvvvwwwwwwwwxxxxxxxyyyyyyyzzzzzzz
  █▌
  █▌
 ███▌�█   █▌�█   █▌�█    █▌�█   █▌�█   █▌�█████▌
  █▌ �█   █▌ █▌ �█ �█    █▌ ▀█▄█▀ �█   █▌   ▄█▀
  █▌ �█   █▌  ███  �█ �▌ █▌ ▄█▀█▄ �█▄▄▄█▌ ▄█▀
  █▌  ▀███▀   �█▌   ▀█▌�█▀ �█   █▌  ▀▀▀█▌�█████▌
                                   ▀███▀
""",
    )


if __name__ == "__main__":
    all_ascii = frozenset(chr(x) for x in range(32, 127))
    print("Available Fonts:     (U) = UTF-8 required")
    print("----------------")
    for n, font_cls in get_all_fonts():
        f = font_cls()
        u = ""
        if f.utf8_required:
            u = "(U)"
        print(f"{n:<20} {u:>3} ", end=" ")
        chars = f.characters()
        font_chars = frozenset(chars)
        if font_chars == all_ascii:
            print("Full ASCII")
        elif font_chars & all_ascii == all_ascii:
            print(f"Full ASCII + {''.join(font_chars ^ all_ascii)!r}")
        else:
            print(f"Characters: {chars!r}")
urwid-urwid-69907e4/urwid/graphics.py000066400000000000000000000053661526073734300176150ustar00rootroot00000000000000# Urwid graphics widgets
#    Copyright (C) 2004-2011  Ian Ward
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/


"""The PythonLogo widget, and graphics widgets re-exported from :mod:`urwid.widget`."""

from __future__ import annotations

import typing

from urwid.display import AttrSpec
from urwid.widget import (
    BarGraph,
    BarGraphError,
    BarGraphMeta,
    BigText,
    GraphVScale,
    LineBox,
    ProgressBar,
    Sizing,
    Text,
    Widget,
    fixed_size,
    scale_bar_values,
)

if typing.TYPE_CHECKING:
    from urwid import canvas

__all__ = (
    "BarGraph",
    "BarGraphError",
    "BarGraphMeta",
    "BigText",
    "GraphVScale",
    "LineBox",
    "ProgressBar",
    "scale_bar_values",
)


class PythonLogo(Widget):
    """Fixed widget that draws an ASCII-art rendering of the Python logo."""

    _sizing = frozenset([Sizing.FIXED])

    def __init__(self) -> None:
        """Create canvas containing an ASCII version of the Python Logo and store it."""
        super().__init__()
        blu = AttrSpec("light blue", "default")
        yel = AttrSpec("yellow", "default")
        width = 17
        # fmt: off
        self._canvas: canvas.TextCanvas = Text(
            [
                (blu, "     ______\n"),
                (blu, "   _|_o__  |"), (yel, "__\n"),
                (blu, "  |   _____|"), (yel, "  |\n"),
                (blu, "  |__|  "), (yel, "______|\n"),
                (yel, "     |____o_|"),
            ]
        ).render((width,))
        # fmt: on

    def pack(self, size: tuple[()] | None = None, focus: bool = False) -> tuple[int, int]:  # type: ignore[override]
        """Return the size from our pre-rendered canvas."""
        return self._canvas.cols(), self._canvas.rows()

    def render(self, size: tuple[()], focus: bool = False) -> canvas.TextCanvas:  # type: ignore[override]
        """Return the pre-rendered canvas."""
        fixed_size(size)
        return self._canvas


def _test() -> None:
    import doctest

    doctest.testmod()


if __name__ == "__main__":
    _test()
urwid-urwid-69907e4/urwid/numedit.py000066400000000000000000000347531526073734300174640ustar00rootroot00000000000000#
# Urwid basic widget classes
#    Copyright (C) 2004-2012  Ian Ward
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/


"""Edit widgets that accept only integer or decimal numbers."""

from __future__ import annotations

import decimal
import re
import typing
import warnings

from urwid import Edit

if typing.TYPE_CHECKING:
    from collections.abc import Container

    from urwid.util import _TagMarkup


class NumEdit(Edit):
    """NumEdit - edit numerical types.

    based on the characters in 'allowed' different numerical types
    can be edited:
      + regular int: 0123456789
      + regular float: 0123456789.
      + regular oct: 01234567
      + regular hex: 0123456789abcdef
    """

    ALLOWED = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"

    def __init__(
        self,
        allowed: Container[str],
        caption: _TagMarkup,
        default: str,
        trimLeadingZeros: bool | None = None,
        *,
        trim_leading_zeros: bool = True,
        allow_negative: bool = False,
    ):
        """Initialize a numeric edit widget restricted to the given set of characters.

        :param allowed: characters accepted by this widget
        :param caption: caption markup
        :param default: default edit value
        :param trimLeadingZeros: legacy spelling of ``trim_leading_zeros``
        :param trim_leading_zeros: strip leading zeros from the edit value
        :param allow_negative: accept a leading minus sign

        .. deprecated:: 2.2.3
            The ``trimLeadingZeros`` argument is deprecated, use the ``trim_leading_zeros``
            keyword argument instead.
        """
        super().__init__(caption, default)
        self._allowed = allowed
        self._trim_leading_zeros = trim_leading_zeros
        self._allow_negative = allow_negative

        if trimLeadingZeros is not None:
            warnings.warn(
                "'trimLeadingZeros' argument is deprecated. Use 'trim_leading_zeros' keyword argument",
                DeprecationWarning,
                stacklevel=3,
            )
            self._trim_leading_zeros = trimLeadingZeros

    def valid_char(self, ch: str) -> bool:
        """Return true for allowed characters."""
        if len(ch) == 1:
            if ch.upper() in self._allowed:
                return True

            return self._allow_negative and ch == "-" and self.edit_pos == 0 and "-" not in self.edit_text
        return False

    def keypress(
        self,
        size: tuple[int],
        key: str,
    ) -> str | None:
        """
        Handle editing keystrokes.  Remove leading zeros.

        >>> e, size = NumEdit("0123456789", "", "5002"), (10,)
        >>> e.keypress(size, "home")
        >>> e.keypress(size, "delete")
        >>> assert e.edit_text == "002"
        >>> e.keypress(size, "end")
        >>> assert e.edit_text == "2"
        >>> # binary only
        >>> e, size = NumEdit("01", "", ""), (10,)
        >>> assert e.edit_text == ""
        >>> e.keypress(size, "1")
        >>> e.keypress(size, "0")
        >>> e.keypress(size, "1")
        >>> assert e.edit_text == "101"
        >>> e, size = NumEdit("0123456789", "", "", allow_negative=True), (10,)
        >>> e.keypress(size, "-")
        >>> e.keypress(size, "1")
        >>> e.edit_text
        '-1'
        >>> e.keypress(size, "home")
        >>> e.keypress(size, "delete")
        >>> e.edit_text
        '1'
        >>> e.keypress(size, "end")
        >>> e.keypress(size, "-")
        '-'
        >>> e.edit_text
        '1'
        """
        unhandled = super().keypress(size, key)

        if not unhandled and self._trim_leading_zeros:
            # trim leading zeros
            while self.edit_pos > 0 and self.edit_text[:1] == "0":
                self.set_edit_pos(self.edit_pos - 1)
                self.set_edit_text(self.edit_text[1:])

        return unhandled


class IntegerEdit(NumEdit):
    """Edit widget for integer values."""

    def __init__(
        self,
        caption: _TagMarkup = "",
        default: int | str | decimal.Decimal | None = None,
        base: int = 10,
        *,
        allow_negative: bool = False,
    ) -> None:
        """Initialize an edit widget restricted to integer values.

        :param caption: caption markup
        :param default: default edit value
        :raises ValueError: *default* is not an ``int``, ``str`` or integral :class:`decimal.Decimal`, or does not
            consist of digits valid for *base*.

        >>> IntegerEdit("", 42)
        
        >>> e, size = IntegerEdit("", "5002"), (10,)
        >>> e.keypress(size, "home")
        >>> e.keypress(size, "delete")
        >>> assert e.edit_text == "002"
        >>> e.keypress(size, "end")
        >>> assert e.edit_text == "2"
        >>> e.keypress(size, "9")
        >>> e.keypress(size, "0")
        >>> assert e.edit_text == "290"
        >>> e, size = IntegerEdit("", ""), (10,)
        >>> assert e.value() is None
        >>> # binary
        >>> e, size = IntegerEdit("", "1010", base=2), (10,)
        >>> e.keypress(size, "end")
        >>> e.keypress(size, "1")
        >>> assert e.edit_text == "10101"
        >>> assert e.value() == decimal.Decimal("21")
        >>> # HEX
        >>> e, size = IntegerEdit("", "10", base=16), (10,)
        >>> e.keypress(size, "end")
        >>> e.keypress(size, "F")
        >>> e.keypress(size, "f")
        >>> assert e.edit_text == "10Ff"
        >>> assert e.keypress(size, "G") == "G"  # unhandled key
        >>> assert e.edit_text == "10Ff"
        >>> # keep leading 0's when not base 10
        >>> e, size = IntegerEdit("", "10FF", base=16), (10,)
        >>> assert e.edit_text == "10FF"
        >>> assert e.value() == decimal.Decimal("4351")
        >>> e.keypress(size, "home")
        >>> e.keypress(size, "delete")
        >>> e.keypress(size, "0")
        >>> assert e.edit_text == "00FF"
        >>> # test exception on incompatible value for base
        >>> e, size = IntegerEdit("", "10FG", base=16), (10,)
        Traceback (most recent call last):
            ...
        ValueError: invalid value: 10FG for base 16
        >>> # test exception on float init value
        >>> e, size = IntegerEdit("", 10.0), (10,)
        Traceback (most recent call last):
            ...
        ValueError: default: Only 'str', 'int', 'long' or Decimal input allowed
        >>> e, size = IntegerEdit("", decimal.Decimal("10.0")), (10,)
        Traceback (most recent call last):
            ...
        ValueError: not an 'integer Decimal' instance
        """
        self.base = base
        val = ""
        allowed_chars = self.ALLOWED[: self.base]
        if default is not None:
            if not isinstance(default, (int, str, decimal.Decimal)):
                raise ValueError("default: Only 'str', 'int' or Decimal input allowed")

            # convert to a long first, this will raise a ValueError
            # in case a float is passed or some other error
            if isinstance(default, str) and len(default):
                # check if it is a valid initial value
                validation_re = f"^[{allowed_chars}]+$"
                if not re.match(validation_re, str(default), re.IGNORECASE):
                    raise ValueError(f"invalid value: {default} for base {base}")

            elif isinstance(default, decimal.Decimal) and default.as_tuple()[2] != 0:
                # a Decimal instance with no fractional part
                raise ValueError("not an 'integer Decimal' instance")

            # convert possible int, long or Decimal to str
            val = str(default)

        super().__init__(
            allowed_chars,
            caption,
            val,
            trim_leading_zeros=(self.base == 10),
            allow_negative=allow_negative,
        )

    def value(self) -> decimal.Decimal | None:
        """
        Return the numeric value of self.edit_text.

        >>> e, size = IntegerEdit(), (10,)
        >>> e.keypress(size, "5")
        >>> e.keypress(size, "1")
        >>> assert e.value() == 51
        """
        if self.edit_text:
            return decimal.Decimal(int(self.edit_text, self.base))

        return None

    def __int__(self) -> int:
        """Enforced int value return.

        >>> e, size = IntegerEdit(allow_negative=True), (10,)
        >>> assert int(e) == 0
        >>> e.keypress(size, "-")
        >>> e.keypress(size, "4")
        >>> e.keypress(size, "2")
        >>> assert int(e) == -42
        """
        if self.edit_text:
            return int(self.edit_text, self.base)
        return 0


class FloatEdit(NumEdit):
    """Edit widget for float values."""

    def __init__(
        self,
        caption: _TagMarkup = "",
        default: str | int | decimal.Decimal | None = None,
        preserveSignificance: bool | None = None,
        decimalSeparator: str | None = None,
        *,
        preserve_significance: bool = True,
        decimal_separator: str = ".",
        allow_negative: bool = False,
    ) -> None:
        """Initialize a decimal edit widget.

        :param caption: caption markup
        :param default: default edit value
        :param preserve_significance: return value has the same signif. as default
        :param decimal_separator: use '.' as separator by default, optionally a ','
        :raises ValueError: *default* is not an ``int``, ``str`` or :class:`decimal.Decimal`, or *decimal_separator* is
            neither ``'.'`` nor ``','``.

        .. deprecated:: 2.2.3
            The ``preserveSignificance`` and ``decimalSeparator`` arguments are deprecated,
            use the ``preserve_significance`` and ``decimal_separator`` keyword arguments instead.

        >>> FloatEdit("", "1.065434")
        
        >>> e, size = FloatEdit("", "1.065434"), (10,)
        >>> e.keypress(size, "home")
        >>> e.keypress(size, "delete")
        >>> assert e.edit_text == ".065434"
        >>> e.keypress(size, "end")
        >>> e.keypress(size, "backspace")
        >>> assert e.edit_text == ".06543"
        >>> e, size = FloatEdit(), (10,)
        >>> e.keypress(size, "5")
        >>> e.keypress(size, "1")
        >>> e.keypress(size, ".")
        >>> e.keypress(size, "5")
        >>> e.keypress(size, "1")
        >>> assert e.value() == decimal.Decimal("51.51"), e.value()
        >>> e, size = FloatEdit(decimal_separator=":"), (10,)
        Traceback (most recent call last):
            ...
        ValueError: invalid decimal separator: :
        >>> e, size = FloatEdit(decimal_separator=","), (10,)
        >>> e.keypress(size, "5")
        >>> e.keypress(size, "1")
        >>> e.keypress(size, ",")
        >>> e.keypress(size, "5")
        >>> e.keypress(size, "1")
        >>> assert e.edit_text == "51,51"
        >>> e, size = FloatEdit("", "3.1415", preserve_significance=True), (10,)
        >>> e.keypress(size, "end")
        >>> e.keypress(size, "backspace")
        >>> e.keypress(size, "backspace")
        >>> assert e.edit_text == "3.14"
        >>> assert e.value() == decimal.Decimal("3.1400")
        >>> e.keypress(size, "1")
        >>> e.keypress(size, "5")
        >>> e.keypress(size, "9")
        >>> assert e.value() == decimal.Decimal("3.1416"), e.value()
        >>> e, size = FloatEdit("", ""), (10,)
        >>> assert e.value() is None
        >>> e, size = FloatEdit("", 10.0), (10,)
        Traceback (most recent call last):
            ...
        ValueError: default: Only 'str', 'int', 'long' or Decimal input allowed
        """
        self.significance = None
        self._decimal_separator = decimal_separator
        if decimalSeparator is not None:
            warnings.warn(
                "'decimalSeparator' argument is deprecated. Use 'decimal_separator' keyword argument",
                DeprecationWarning,
                stacklevel=3,
            )
            self._decimal_separator = decimalSeparator

        if self._decimal_separator not in {".", ","}:
            raise ValueError(f"invalid decimal separator: {self._decimal_separator}")

        if preserveSignificance is not None:
            warnings.warn(
                "'preserveSignificance' argument is deprecated. Use 'preserve_significance' keyword argument",
                DeprecationWarning,
                stacklevel=3,
            )
            preserve_significance = preserveSignificance

        val = ""
        if default is not None and default != "":  # noqa: PLC1901,RUF100
            if not isinstance(default, (int, str, decimal.Decimal)):
                raise ValueError("default: Only 'str', 'int' or Decimal input allowed")

            if isinstance(default, str) and default:
                # check if it is a float, raises a ValueError otherwise
                float(default)
                default = decimal.Decimal(default)

            if preserve_significance and isinstance(default, decimal.Decimal):
                self.significance = default

            val = str(default)

        super().__init__(self.ALLOWED[0:10] + self._decimal_separator, caption, val, allow_negative=allow_negative)

    def value(self) -> decimal.Decimal | None:
        """Return the numeric value of self.edit_text."""
        if self.edit_text:
            normalized = decimal.Decimal(self.edit_text.replace(self._decimal_separator, "."))
            if self.significance is not None:
                return normalized.quantize(self.significance)
            return normalized

        return None

    def __float__(self) -> float:
        """Enforced float value return.

        >>> e, size = FloatEdit(allow_negative=True), (10,)
        >>> assert float(e) == 0.0
        >>> e.keypress(size, "-")
        >>> e.keypress(size, "4")
        >>> e.keypress(size, ".")
        >>> e.keypress(size, "2")
        >>> assert float(e) == -4.2
        """
        if self.edit_text:
            return float(self.edit_text.replace(self._decimal_separator, "."))
        return 0.0
urwid-urwid-69907e4/urwid/py.typed000066400000000000000000000000001526073734300171160ustar00rootroot00000000000000urwid-urwid-69907e4/urwid/signals.py000066400000000000000000000353201526073734300174460ustar00rootroot00000000000000# Urwid signal dispatching
#    Copyright (C) 2004-2012  Ian Ward
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/


"""Signal registration and delivery between widgets and other objects."""

from __future__ import annotations

import abc
import itertools
import typing
import weakref

if typing.TYPE_CHECKING:
    from collections.abc import Callable, Collection, Container, Hashable, Iterable

    _T = typing.TypeVar("_T")


class MetaSignals(abc.ABCMeta):
    """Register the list of signals in the class variable signals, including signals in superclasses."""

    def __init__(
        cls,
        name: str,
        bases: tuple[type, ...],
        d: dict[str, typing.Any],
    ) -> None:
        """Collect `signals` from the new class and its superclasses into the new class's own list."""
        signals = d.get("signals", [])
        for superclass in cls.__bases__:
            signals.extend(getattr(superclass, "signals", []))
        signals = list(dict.fromkeys(signals).keys())
        d["signals"] = signals
        register_signal(cls, signals)
        super().__init__(name, bases, d)


def setdefaultattr(obj: typing.Any, name: str, value: _T) -> _T:
    """Return the existing attribute ``name`` on ``obj``, setting it to ``value`` first if missing."""
    # like dict.setdefault() for object attributes
    if hasattr(obj, name):
        return typing.cast("_T", getattr(obj, name))
    setattr(obj, name, value)
    return value


class Key:
    """Minimal class, whose only purpose is to produce objects with a unique hash."""

    __slots__ = ()


if typing.TYPE_CHECKING:
    # ``weak_args`` (converted to weakrefs) and ``user_args`` as prepared by ``_prepare_user_args``.
    _UserArgs = tuple[Collection[weakref.ReferenceType[typing.Any]], Collection[typing.Any]]
    # A single connected handler: (key, callback, deprecated user_arg, prepared args).
    _SignalHandler = tuple[Key, Callable[..., typing.Any], typing.Any, _UserArgs]
    # Per-sender storage attached to ``obj`` under ``Signals._signal_attr``.
    # Each signal's handlers are stored as a tuple rather than a list, and replaced
    # (rather than mutated in place) on connect/disconnect, so that ``emit`` can iterate
    # its snapshot directly without copying, even if a handler connects or disconnects
    # from within a callback while that iteration is in progress.
    _SignalStore = dict[Hashable, tuple[_SignalHandler, ...]]


class Signals:
    """Registry of signal-emitting classes and the handlers connected to their signals."""

    _signal_attr = "_urwid_signals"  # attribute to attach to signal senders

    def __init__(self) -> None:
        """Initialize with no signal-emitting classes registered yet."""
        # Weak keys, so that a class created at run time is not kept alive by having signals.
        self._supported: weakref.WeakKeyDictionary[MetaSignals, Container[Hashable]] = weakref.WeakKeyDictionary()

    def register(self, sig_cls: MetaSignals, signals: Container[Hashable]) -> None:
        """Register the signals `sig_cls` objects may send.

        This function must be called for a class before connecting any
        signal callbacks or emitting any signals from that class' objects.

        :param sig_cls: the class of an object that will be sending signals
        :param signals: a list of signals that may be sent, typically each signal is represented by a string
        """
        self._supported[sig_cls] = signals

    def connect(
        self,
        obj: typing.Any,
        name: Hashable,
        callback: Callable[..., typing.Any],
        user_arg: typing.Any = None,
        *,
        weak_args: Iterable[typing.Any] = (),
        user_args: Iterable[typing.Any] = (),
    ) -> Key:
        """Connect `callback` to `obj`'s `name` signal.

        :param obj: the object sending a signal
        :param name: the signal to listen for, typically a string
        :param callback: the function to call when that signal is sent
        :param user_arg: additional argument to callback
                         (appended  after the arguments passed when the signal is emitted).
                         If None no arguments will be added.
                         Don't use this argument, use user_args instead.
        :param weak_args: additional arguments passed to the callback
                          (before any arguments passed when the signal
                          is emitted and before any user_args).

                          These arguments are stored as weak references
                          (but converted back into their original value
                          before passing them to callback) to prevent
                          any objects referenced (indirectly) from
                          weak_args from being kept alive just because
                          they are referenced by this signal handler.

                          Use this argument only as a keyword argument,
                          since user_arg might be removed in the future.
        :param user_args: additional arguments to pass to the callback,
                          (before any arguments passed when the signal
                          is emitted but after any weak_args).

                          Use this argument only as a keyword argument,
                          since user_arg might be removed in the future.
        :raises NameError: *obj* does not support a signal called *name*.

        When a matching signal is sent, callback will be called. The
        arguments it receives will be the user_args passed at connect
        time (as individual arguments) followed by all the positional
        parameters sent with the signal.

        As an example of using weak_args, consider the following snippet:

        >>> import urwid
        >>> debug = urwid.Text("")
        >>> def handler(widget, newtext):
        ...     debug.set_text("Edit widget changed to %s" % newtext)
        >>> edit = urwid.Edit("")
        >>> key = urwid.connect_signal(edit, "change", handler)

        If you now build some interface using "edit" and "debug", the
        "debug" widget will show whatever you type in the "edit" widget.
        However, if you remove all references to the "debug" widget, it
        will still be kept alive by the signal handler. This because the
        signal handler is a closure that (implicitly) references the
        "edit" widget. If you want to allow the "debug" widget to be
        garbage collected, you can create a "fake" or "weak" closure
        (it's not really a closure, since it doesn't reference any
        outside variables, so it's just a dynamic function):

        >>> debug = urwid.Text("")
        >>> def handler(weak_debug, widget, newtext):
        ...     weak_debug.set_text("Edit widget changed to %s" % newtext)
        >>> edit = urwid.Edit("")
        >>> key = urwid.connect_signal(edit, "change", handler, weak_args=[debug])

        Here the weak_debug parameter in print_debug is the value passed
        in the weak_args list to connect_signal. Note that the
        weak_debug value passed is not a weak reference anymore, the
        signals code transparently dereferences the weakref parameter
        before passing it to print_debug.

        Returns a key associated by this signal handler, which can be
        used to disconnect the signal later on using
        urwid.disconnect_signal_by_key. Alternatively, the signal
        handler can also be disconnected by calling
        urwid.disconnect_signal, which doesn't need this key.
        """
        sig_cls = obj.__class__
        if name not in self._supported.get(sig_cls, ()):
            raise NameError(f"No such signal {name!r} for object {obj!r}")

        # Just generate an arbitrary (but unique) key
        key = Key()

        signals: _SignalStore = setdefaultattr(obj, self._signal_attr, {})

        # Remove the signal handler when any of the weakref'd arguments
        # are garbage collected. Note that this can happen _at any time_,
        # including while ``emit`` is iterating another handler's tuple for
        # this same signal; that tuple is replaced rather than mutated in
        # place (see ``_SignalStore`` above), so such an in-progress
        # iteration is unaffected.
        # We let the callback keep a weakref to the object as well, to
        # prevent a circular reference between the handler and the
        # object (via the weakrefs, which keep strong references to
        # their callbacks) from existing.
        obj_weak = weakref.ref(obj)

        def weakref_callback(_ref: weakref.ReferenceType[typing.Any]) -> None:
            if (o := obj_weak()) is not None:
                self.disconnect_by_key(o, name, key)

        user_args = self._prepare_user_args(weak_args, user_args, weakref_callback)
        signals[name] = (*signals.get(name, ()), (key, callback, user_arg, user_args))
        self._drop_dead_handlers(signals, name)

        return key

    def _prepare_user_args(
        self,
        weak_args: Iterable[_T] = (),
        user_args: Iterable[typing.Any] = (),
        callback: Callable[[weakref.ReferenceType[_T]], typing.Any] | None = None,
    ) -> tuple[Collection[weakref.ReferenceType[_T]], Collection[typing.Any]]:
        # Turn weak_args into weakrefs and prepend them to user_args
        w_args = tuple(weakref.ref(w_arg, callback) for w_arg in weak_args)
        args = tuple(user_args) or ()
        return (w_args, args)

    def disconnect(
        self,
        obj: typing.Any,
        name: Hashable,
        callback: Callable[..., typing.Any],
        user_arg: typing.Any = None,
        *,
        weak_args: Iterable[typing.Any] = (),
        user_args: Iterable[typing.Any] = (),
    ) -> None:
        """Remove a callback from the list connected to a signal with connect_signal().

        The arguments passed should be exactly the same as those passed to connect_signal().

        If the callback is not connected or already disconnected, this function will simply do nothing.

        :param obj: the object to disconnect the signal from
        :param name: the signal to disconnect, typically a string
        :param callback: the callback function passed to connect_signal
        :param user_arg: the user_arg parameter passed to connect_signal
        :param weak_args: the weak_args parameter passed to connect_signal
        :param user_args: the weak_args parameter passed to connect_signal
        """
        signals: _SignalStore = setdefaultattr(obj, self._signal_attr, {})
        if name not in signals:
            return None

        handlers = signals[name]

        # Do the same processing as in connect, so we can compare the
        # resulting tuple.
        user_args = self._prepare_user_args(weak_args, user_args)

        # Remove the given handler
        for h in handlers:
            if h[1:] == (callback, user_arg, user_args):
                return self.disconnect_by_key(obj, name, h[0])
        return None

    def disconnect_by_key(self, obj: typing.Any, name: Hashable, key: Key) -> None:
        """Remove a callback from the list connected to a signal with connect_signal().

        The key passed should be the value returned by connect_signal().

        If the callback is not connected or already disconnected, this function will simply do nothing.

        :param obj: the object to disconnect the signal from
        :param name: the signal to disconnect, typically a string
        :param key: the key for this signal handler, as returned by
                    connect_signal().
        """
        signals: _SignalStore = setdefaultattr(obj, self._signal_attr, {})
        handlers = signals.get(name, ())
        signals[name] = tuple(h for h in handlers if h[0] is not key)
        self._drop_dead_handlers(signals, name)

    def _drop_dead_handlers(self, signals: _SignalStore, name: Hashable) -> None:
        """Remove the handlers of `name` that have a garbage-collected weak argument.

        A weak argument's callback can run between another update's read and write of these handlers.
        That write then restores the handler the callback removed.
        A weak argument never comes back to life, so the loop stops once no handler has a dead one.
        """
        while any(w() is None for h in signals[name] for w in h[3][0]):
            signals[name] = tuple(h for h in signals[name] if all(w() is not None for w in h[3][0]))

    def emit(self, obj: typing.Any, name: Hashable, *args: typing.Any) -> bool:
        """Call each of the callbacks connected to this signal with the args arguments as positional parameters.

        Returns True if any of the callbacks returned True.

        :param obj: the object sending a signal
        :param name: the signal to send, typically a string
        :param args: zero or more positional arguments to pass to the signal callback functions
        """
        result = False
        handlers = getattr(obj, self._signal_attr, {}).get(name, ())
        for _key, callback, user_arg, (weak_args, user_args) in handlers:
            result |= self._call_callback(callback, user_arg, weak_args, user_args, args)
        return result

    def _call_callback(
        self,
        callback: Callable[..., typing.Any],  # we cannot use type signature here due to args decomposition
        user_arg: typing.Any,
        weak_args: Iterable[weakref.ReferenceType[typing.Any]],
        user_args: Iterable[typing.Any],
        emit_args: Iterable[typing.Any],
    ) -> bool:
        args_to_pass = []
        for w_arg in weak_args:
            if (real_arg := w_arg()) is not None:
                args_to_pass.append(real_arg)
            else:
                # de-referenced
                return False

        # The deprecated user_arg argument was added to the end
        # instead of the beginning.
        args = itertools.chain(args_to_pass, user_args, emit_args, (user_arg,) if user_arg is not None else ())

        return bool(callback(*args))


_signals = Signals()
emit_signal = _signals.emit
register_signal = _signals.register
connect_signal = _signals.connect
disconnect_signal = _signals.disconnect
disconnect_signal_by_key = _signals.disconnect_by_key
urwid-urwid-69907e4/urwid/split_repr.py000066400000000000000000000102111526073734300201610ustar00rootroot00000000000000# Urwid split_repr helper functions
#    Copyright (C) 2004-2011  Ian Ward
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/


"""Helpers for building a readable ``repr`` of widgets."""

from __future__ import annotations

import typing
from inspect import getfullargspec

if typing.TYPE_CHECKING:
    from urwid.widget import Widget


def split_repr(self: Widget) -> str:
    """Return a helpful description of the object using self._repr_words() and self._repr_attrs().

    This function may be used by adding code to your class like this:

    >>> class Foo(object):
    ...     __repr__ = split_repr
    ...
    ...     def _repr_words(self):
    ...         return ["words", "here"]
    ...
    ...     def _repr_attrs(self):
    ...         return {"attrs": "appear too"}
    >>> Foo()
    
    >>> class Bar(Foo):
    ...     def _repr_words(self):
    ...         return Foo._repr_words(self) + ["too"]
    ...
    ...     def _repr_attrs(self):
    ...         return dict(Foo._repr_attrs(self), barttr=42)
    >>> Bar()
    
    """
    alist = sorted((k, normalize_repr(v)) for k, v in self._repr_attrs().items())

    words = self._repr_words()
    if not words and not alist:
        # if we're just going to print the classname fall back
        # to the previous __repr__ implementation instead
        return super(self.__class__, self).__repr__()
    if words and alist:
        words.append("")
    return f"<{self.__class__.__name__} {' '.join(words) + ' '.join([f'{k}={v}' for k, v in alist])}>"


def normalize_repr(v: object) -> str:
    """
    Return dictionary repr sorted by keys, leave others unchanged.

    >>> normalize_repr({1: 2, 3: 4, 5: 6, 7: 8})
    '{1: 2, 3: 4, 5: 6, 7: 8}'
    >>> normalize_repr("foo")
    "'foo'"
    """
    if isinstance(v, dict):
        items = sorted((repr(k), repr(v)) for k, v in v.items())

        return f"{{{', '.join(f'{k}: {v}' for k, v in items)}}}"

    return repr(v)


def remove_defaults(d: dict[str, object], fn: object) -> dict[str, object]:
    """Remove keys in d that are set to the default values from fn.

    This method is used to unclutter the _repr_attrs() return value.

    d will be modified by this function.

    Returns d.

    >>> class Foo(object):
    ...     def __init__(self, a=1, b=2):
    ...         self.values = a, b
    ...
    ...     __repr__ = split_repr
    ...
    ...     def _repr_words(self):
    ...         return ["object"]
    ...
    ...     def _repr_attrs(self):
    ...         d = dict(a=self.values[0], b=self.values[1])
    ...         return remove_defaults(d, Foo.__init__)
    >>> Foo(42, 100)
    
    >>> Foo(10, 2)
    
    >>> Foo()
    
    """
    args, varargs, varkw, defaults, _, _, _ = getfullargspec(fn)

    # ignore *varargs and **kwargs
    if varkw:
        del args[-1]
    if varargs:
        del args[-1]

    # create a dictionary of args with default values
    ddict = dict(
        zip(
            args[len(args) - len(typing.cast("tuple[object]", defaults)) :],
            typing.cast("tuple[object]", defaults),
        )
    )

    for k in list(d.keys()):
        if k in ddict and ddict[k] == d[k]:
            # remove values that match their defaults
            del d[k]

    return d


def _test() -> None:
    import doctest

    doctest.testmod()


if __name__ == "__main__":
    _test()
urwid-urwid-69907e4/urwid/str_util.py000066400000000000000000000441701526073734300176560ustar00rootroot00000000000000# Urwid unicode character processing tables
#    Copyright (C) 2004-2011  Ian Ward
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/


"""Character width calculation and byte-string decoding for screen layout."""

from __future__ import annotations

import codecs
import re
import typing
import warnings

import wcwidth

if typing.TYPE_CHECKING:
    from typing_extensions import Literal

SAFE_ASCII_RE = re.compile(r"^[ -~]*$")
SAFE_ASCII_BYTES_RE = re.compile(rb"^[ -~]*$")

_byte_encoding: Literal["utf8", "narrow", "wide"] = "narrow"


def get_char_width(char: str) -> Literal[0, 1, 2]:
    """
    Return the screen column width for a single character.

    .. deprecated:: 3.0.4
        Use :func:`wcwidth.wcwidth` instead. This API will be removed in version 6.0.
    """
    warnings.warn(
        "get_char_width is deprecated in favor of wcwidth.width. API will be removed in version 6.0.",
        DeprecationWarning,
        stacklevel=2,
    )
    if (width := wcwidth.wcwidth(char)) >= 0:
        return typing.cast("Literal[0, 1, 2]", width)

    return 0


def get_width(o: int) -> Literal[0, 1, 2]:
    """
    Return the screen column width for unicode ordinal o.

    .. deprecated:: 3.0.4
        Use :func:`wcwidth.wcwidth` instead. This API will be removed in version 6.0.
    """
    warnings.warn(
        "get_width is deprecated in favor of wcwidth.width. API will be removed in version 6.0.",
        DeprecationWarning,
        stacklevel=2,
    )
    if (width := wcwidth.wcwidth(chr(o))) >= 0:
        return typing.cast("Literal[0, 1, 2]", width)

    return 0


def _decode_grapheme_at(text: bytes, start: int, end: int) -> tuple[str, int]:
    """
    Decode bytes starting at `start` to get the first grapheme cluster.

    :param text: UTF-8 encoded bytes
    :param start: starting byte position
    :param end: ending byte position
    :returns: (grapheme_string, next_byte_position)
    :raises UnicodeDecodeError: the decoded bytes are not valid UTF-8.

    Assumes caller provides valid UTF-8 byte boundaries.
    Decodes a window that doubles until it holds a character past the first grapheme cluster,
    so the cost follows the cluster length rather than the length of the rest of the text.
    """
    window = 16
    while True:
        stop = min(end, start + window)
        # an incremental decoder keeps a character cut by the window end instead of failing on it
        decoded = codecs.getincrementaldecoder("utf-8")().decode(text[start:stop], final=stop == end)
        grapheme = next(wcwidth.iter_graphemes(decoded), "")
        if len(grapheme) < len(decoded):
            return grapheme, start + len(grapheme.encode("utf-8"))
        if stop == end:
            # the grapheme consumed the whole decoded window: its byte length is
            # already known (end - start) without re-encoding it.
            return grapheme, end
        window *= 2


def _first_grapheme(text: str, start: int, end: int) -> str:
    """
    Return the first grapheme cluster of text[start:end].

    Segments a slice that doubles while the cluster reaches its end,
    so the cost follows the cluster length rather than the length of the rest of the text.

    :raises StopIteration: *start* is not before *end*.
    """
    window = 16
    while True:
        stop = min(end, start + window)
        grapheme = next(wcwidth.iter_graphemes(text[start:stop]))
        if start + len(grapheme) < stop or stop == end:
            return grapheme
        window *= 2


def decode_one(text: bytes | str, pos: int) -> tuple[int, int]:
    """
    Return (ordinal at pos, next position) for UTF-8 encoded text.

    :raises ValueError: *pos* is not a valid position in *text*.
    """
    lt = len(text) - pos

    try:
        if isinstance(text, str):
            b1 = ord(text[pos])
        else:
            b1 = text[pos]
    except Exception as e:
        raise ValueError(f"{e}: text={text!r}, pos={pos!r}, lt={lt!r}").with_traceback(e.__traceback__) from e

    if not b1 & 0x80:
        return b1, pos + 1
    error = ord("?"), pos + 1

    if lt < 2:
        return error

    try:
        if isinstance(text, str):
            b2 = ord(text[pos + 1])
        else:
            b2 = text[pos + 1]
    except Exception as e:
        raise ValueError(f"{e}: text={text!r}, pos={pos!r}, lt={lt!r}").with_traceback(e.__traceback__) from e

    if b1 & 0xE0 == 0xC0:
        if b2 & 0xC0 != 0x80:
            return error
        if (o := ((b1 & 0x1F) << 6) | (b2 & 0x3F)) >= 0x80:
            return o, pos + 2
        return error
    if lt < 3:
        return error

    try:
        if isinstance(text, str):
            b3 = ord(text[pos + 2])
        else:
            b3 = text[pos + 2]
    except Exception as e:
        raise ValueError(f"{e}: text={text!r}, pos={pos!r}, lt={lt!r}").with_traceback(e.__traceback__) from e

    if b1 & 0xF0 == 0xE0:
        if b2 & 0xC0 != 0x80:
            return error
        if b3 & 0xC0 != 0x80:
            return error
        if (o := ((b1 & 0x0F) << 12) | ((b2 & 0x3F) << 6) | (b3 & 0x3F)) >= 0x800:
            return o, pos + 3
        return error
    if lt < 4:
        return error

    try:
        if isinstance(text, str):
            b4 = ord(text[pos + 3])
        else:
            b4 = text[pos + 3]
    except Exception as e:
        raise ValueError(f"{e}: text={text!r}, pos={pos!r}, lt={lt!r}").with_traceback(e.__traceback__) from e

    if b1 & 0xF8 == 0xF0:
        if b2 & 0xC0 != 0x80:
            return error
        if b3 & 0xC0 != 0x80:
            return error
        if b4 & 0xC0 != 0x80:
            return error
        if (o := ((b1 & 0x07) << 18) | ((b2 & 0x3F) << 12) | ((b3 & 0x3F) << 6) | (b4 & 0x3F)) >= 0x10000:
            return o, pos + 4
        return error
    return error


def decode_one_uni(text: str, i: int) -> tuple[int, int]:
    """
    decode_one implementation for unicode strings.

    .. deprecated:: 4.1.4
        Not used by the urwid code base; there is no replacement.
        This API will be removed in version 6.0.
    """
    warnings.warn(
        "decode_one_uni is not used by the urwid code base. API will be removed in version 6.0.",
        DeprecationWarning,
        stacklevel=2,
    )
    return ord(text[i]), i + 1


def decode_one_right(text: bytes, pos: int) -> tuple[int, int] | None:
    """Return (ordinal at pos, next position) for UTF-8 encoded text.

    pos is assumed to be on the trailing byte of a utf-8 sequence.

    :raises TypeError: *text* is not a byte string.

    .. deprecated:: 4.1.4
        Not used by the urwid code base; there is no replacement.
        This API will be removed in version 6.0.
    """
    warnings.warn(
        "decode_one_right is not used by the urwid code base. API will be removed in version 6.0.",
        DeprecationWarning,
        stacklevel=2,
    )
    if not isinstance(text, bytes):
        raise TypeError(text)
    error = ord("?"), pos - 1
    p = pos
    while p >= 0:
        if text[p] & 0xC0 != 0x80:
            o, _next_pos = decode_one(text, p)
            return o, p - 1
        p -= 1
        if p == p - 4:
            return error
    return None


def set_byte_encoding(enc: Literal["utf8", "narrow", "wide"]) -> None:
    """
    Select the byte encoding used to interpret byte strings.

    :raises ValueError: *enc* is not one of the supported encodings.
    """
    if enc not in {"utf8", "narrow", "wide"}:
        raise ValueError(enc)
    global _byte_encoding  # noqa: PLW0603  # pylint: disable=global-statement
    _byte_encoding = enc


def get_byte_encoding() -> Literal["utf8", "narrow", "wide"]:
    """Return the byte encoding currently selected by :func:`set_byte_encoding`."""
    return _byte_encoding


def calc_string_text_pos(text: str, start_offs: int, end_offs: int, pref_col: int) -> tuple[int, int]:
    """Calculate the closest position to the screen column pref_col in text.

    start_offs is the offset into text assumed to be screen column 0
    and end_offs is the end of the range to search.

    Iterates by grapheme clusters for emoji ZWJ sequences, flags,
    combining characters, and other multi-codepoint unicode sequences.
    Segments a window that doubles until the result is found,
    so the cost follows the length of the returned range rather than the length of the whole range.

    :param text: string
    :param start_offs: starting text position
    :param end_offs: ending text position
    :param pref_col: target column
    :returns: (position, actual_col)
    :raises ValueError: *start_offs* is past *end_offs*.
    """
    if start_offs > end_offs:
        raise ValueError((start_offs, end_offs))

    cols = 0
    pos = start_offs
    window = max(pref_col, 0) + 16
    while True:
        stop = min(end_offs, pos + window)
        # iterating a slice uses the C segmenter of wcwidth, iterating a range of the whole text does not
        for grapheme in wcwidth.iter_graphemes(text[pos:stop]):
            if pos + len(grapheme) == stop < end_offs:
                break  # the window end may cut this cluster: segment it again in a larger window
            grapheme_width = wcwidth.width(grapheme, control_codes="ignore")
            if grapheme_width + cols > pref_col:
                return pos, cols
            cols += grapheme_width
            pos += len(grapheme)
        else:
            return end_offs, cols
        window *= 2


def calc_text_pos(text: str | bytes, start_offs: int, end_offs: int, pref_col: int) -> tuple[int, int]:
    """Calculate the closest position to the screen column pref_col in text.

    start_offs is the offset into text assumed to be screen column 0
    and end_offs is the end of the range to search.

    text may be unicode or a byte string in the target _byte_encoding

    Returns (position, actual_col).

    :raises ValueError: *start_offs* is past *end_offs*.
    :raises TypeError: the byte encoding is in use and *text* is not a byte string.
    :raises UnicodeDecodeError: the byte encoding is UTF-8 and the decoded bytes are not valid UTF-8.
    """
    if start_offs > end_offs:
        raise ValueError((start_offs, end_offs))

    if isinstance(text, str):
        return calc_string_text_pos(text, start_offs, end_offs, pref_col)

    if not isinstance(text, bytes):
        raise TypeError(text)

    if _byte_encoding == "utf8":
        # decode a window that doubles until a cluster that is whole in it crosses pref_col,
        # so the cost follows the length of the returned range rather than the length of the whole range
        window = 4 * max(pref_col, 0) + 16
        while True:
            stop = min(end_offs, start_offs + window)
            decoded = codecs.getincrementaldecoder("utf-8")().decode(text[start_offs:stop], final=stop == end_offs)
            str_pos, cols = calc_string_text_pos(decoded, 0, len(decoded), pref_col)
            if stop == end_offs:
                if str_pos == len(decoded):
                    # the whole decoded window was consumed: its byte length is already
                    # known (end_offs - start_offs) without re-encoding it.
                    return end_offs, cols
                break
            if str_pos < wcwidth.grapheme_boundary_before(decoded, len(decoded)):
                break
            window *= 2
        byte_offset = len(decoded[:str_pos].encode("utf-8"))
        return start_offs + byte_offset, cols

    # "wide" and "narrow"
    i = start_offs + pref_col
    if i >= end_offs:
        return end_offs, end_offs - start_offs
    if _byte_encoding == "wide" and within_double_byte(text, start_offs, i) == 2:
        i -= 1
    return i, i - start_offs


def calc_width(text: str | bytes, start_offs: int, end_offs: int) -> int:
    """
    Return the screen column width of text between start_offs and end_offs.

    text may be unicode or a byte string in the target _byte_encoding

    Some characters are wide (take two columns) and others affect the
    previous character (take zero columns), while others are grouped
    in sequence by "grapheme boundaries" (Emoji, Skin tones, flags, etc).

    :raises ValueError: *start_offs* is past *end_offs*.
    """
    if start_offs > end_offs:
        msg = f"{start_offs=} > {end_offs=}"
        raise ValueError(msg)

    if isinstance(text, str):
        return wcwidth.width(text[start_offs:end_offs], control_codes="ignore")

    if _byte_encoding == "utf8":
        try:
            return wcwidth.width(text[start_offs:end_offs].decode("utf-8"), control_codes="ignore")
        except UnicodeDecodeError as exc:
            warnings.warn(
                "`calc_width` with text encoded to bytes can produce incorrect results "
                f"due to possible offset in the middle of character: {exc}",
                UnicodeWarning,
                stacklevel=2,
            )

        i = start_offs
        sc = 0
        while i < end_offs:
            o, i = decode_one(text, i)
            if (w := wcwidth.wcwidth(chr(o))) > 0:
                sc += w
        return sc
    # "wide", "narrow" or all printable ASCII, just return the character count
    return end_offs - start_offs


def is_wide_char(text: str | bytes, offs: int) -> bool:
    """
    Test if the grapheme cluster at offs within text is wide (2 columns).

    For Unicode strings, extracts the full grapheme cluster starting at offs
    and checks if it renders as wide. This correctly handles multi-codepoint
    graphemes like emoji ZWJ sequences and flags.

    text may be unicode or a byte string in the target _byte_encoding

    :raises TypeError: the byte encoding is in use and *text* is not a byte string.
    """
    if isinstance(text, str):
        grapheme = _first_grapheme(text, offs, len(text))
        return wcwidth.width(grapheme, control_codes="ignore") == 2
    if not isinstance(text, bytes):
        raise TypeError(text)
    if _byte_encoding == "utf8":
        grapheme, _ = _decode_grapheme_at(text, offs, len(text))
        return wcwidth.width(grapheme, control_codes="ignore") == 2
    if _byte_encoding == "wide":
        return within_double_byte(text, offs, offs) == 1
    return False


def move_prev_char(text: str | bytes, start_offs: int, end_offs: int) -> int:
    """
    Return the position of the grapheme cluster before end_offs, never before start_offs.

    For Unicode strings, handle multi-codepoint, "grapheme clusters",
    to better measure emoji ZWJ, flags, combining characters, skin tones.
    A cluster that starts before start_offs is cut at start_offs, as the byte string path does.

    :raises ValueError: *start_offs* is not before *end_offs*.
    :raises TypeError: the byte encoding is in use and *text* is not a byte string.
    """
    if start_offs >= end_offs:
        raise ValueError((start_offs, end_offs))
    if isinstance(text, str):
        return max(start_offs, wcwidth.grapheme_boundary_before(text, end_offs))
    if not isinstance(text, bytes):
        raise TypeError(text)
    if _byte_encoding == "utf8":
        decoded = text[start_offs:end_offs].decode("utf-8")
        str_pos = len(decoded)
        prev_str_pos = wcwidth.grapheme_boundary_before(decoded, str_pos)
        # Encode only the removed suffix (normally short) rather than the
        # (potentially much longer) retained prefix, since prefix length in
        # bytes is just the total minus the suffix length in bytes.
        suffix = decoded[prev_str_pos:]
        return end_offs - len(suffix.encode("utf-8"))
    if _byte_encoding == "wide" and within_double_byte(text, start_offs, end_offs - 1) == 2:
        return end_offs - 2
    return end_offs - 1


def move_next_char(text: str | bytes, start_offs: int, end_offs: int) -> int:
    """
    Return the position of the next grapheme cluster after start_offs.

    For Unicode strings, handle multi-codepoint, "grapheme clusters",
    to better measure emoji ZWJ, flags, combining characters, skin tones.

    :raises ValueError: *start_offs* is not before *end_offs*.
    :raises TypeError: the byte encoding is in use and *text* is not a byte string.
    """
    if start_offs >= end_offs:
        raise ValueError((start_offs, end_offs))
    if isinstance(text, str):
        return start_offs + len(_first_grapheme(text, start_offs, end_offs))
    if not isinstance(text, bytes):
        raise TypeError(text)
    if _byte_encoding == "utf8":
        _, next_pos = _decode_grapheme_at(text, start_offs, end_offs)
        return next_pos
    if _byte_encoding == "wide" and within_double_byte(text, start_offs, start_offs) == 1:
        return start_offs + 2
    return start_offs + 1


def within_double_byte(text: bytes, line_start: int, pos: int) -> Literal[0, 1, 2]:
    """Return whether pos is within a double-byte encoded character.

    :param text: byte string in question
    :param line_start: offset of beginning of line (< pos)
    :param pos: offset in question
    :returns: ``0`` if not within a double-byte character, or the byte encoding is not double-byte,
        ``1`` if pos is on the first half of a double-byte character,
        ``2`` if pos is on the second half of a double-byte character.
    :raises TypeError: *text* is not a byte string.
    """
    if not isinstance(text, bytes):
        raise TypeError(text)
    v = text[pos]

    if 0x40 <= v < 0x7F:
        # might be second half of big5, uhc or gbk encoding
        if pos == line_start:
            return 0

        if text[pos - 1] >= 0x81 and within_double_byte(text, line_start, pos - 1) == 1:
            return 2
        return 0

    if v < 0x80:
        return 0

    i = pos - 1
    while i >= line_start:
        if text[i] < 0x80:
            break
        i -= 1

    if (pos - i) & 1:
        return 1
    return 2
urwid-urwid-69907e4/urwid/text_layout.py000066400000000000000000001102341526073734300203650ustar00rootroot00000000000000# Urwid Text Layout classes
#    Copyright (C) 2004-2011  Ian Ward
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/


"""Text layout: wrapping and aligning text into lines of screen columns."""

from __future__ import annotations

import bisect
import functools
import typing

import wcwidth

from urwid.str_util import calc_text_pos, calc_width, is_wide_char, move_next_char, move_prev_char
from urwid.util import calc_trim_text, get_encoding

if typing.TYPE_CHECKING:
    from collections.abc import Iterable

    from typing_extensions import Literal

    from urwid.widget import Align, WrapMode

    _LayoutSegment = typing.Union[tuple[int, int, typing.Union[int | bytes]], tuple[int, typing.Union[int | None]]]
    _LayoutLine = list[_LayoutSegment]
    _LayoutFormat = list[_LayoutLine]


@functools.lru_cache(maxsize=4)
def get_ellipsis_string(encoding: str) -> str:
    """Get ellipsis character for given encoding."""
    try:
        return "…".encode(encoding).decode(encoding)
    except UnicodeEncodeError:
        return "..."


@functools.lru_cache(maxsize=4)
def _get_width(string: str) -> int:
    """Get ellipsis character width for given encoding."""
    return wcwidth.width(string, control_codes="ignore")


def _calc_fitting_text(text: str | bytes, start: int, end: int, max_cols: int) -> tuple[int, int]:
    """Return the end offset and the screen columns of the longest part of text[start:end] fitting into max_cols.

    Text of up to ``4 * max_cols + 16`` code units is measured as a whole first. Longer text is cut with
    :func:`calc_text_pos` before it is measured, so the cost follows the length of the part that fits
    rather than the length of the whole range.
    """
    if end - start <= 4 * max_cols + 16 and (cols := calc_width(text, start, end)) <= max_cols:
        return end, cols
    pos, cols = calc_text_pos(text, start, end, max_cols)
    if pos == end and (whole := calc_width(text, start, end)) <= max_cols:
        # the per-cluster measure of calc_text_pos and the whole-text measure of calc_width can disagree
        cols = whole
    return pos, cols


class TextLayout:
    """Base class for a text layout algorithm that lays text out into lines for display."""

    def supports_align_mode(self, align: Literal["left", "center", "right"] | Align) -> bool:
        """Return True if align is a supported align mode."""
        return True

    def supports_wrap_mode(self, wrap: Literal["any", "space", "clip", "ellipsis"] | WrapMode) -> bool:
        """Return True if wrap is a supported wrap mode."""
        return True

    def layout(
        self,
        text: str | bytes,
        width: int,
        align: Literal["left", "center", "right"] | Align,
        wrap: Literal["any", "space", "clip", "ellipsis"] | WrapMode,
    ) -> _LayoutFormat:
        """
        Return a layout structure for text.

        :param text: string in current encoding or unicode string
        :param width: number of screen columns available
        :param align: align mode for text
        :param wrap: wrap mode for text
        :raises NotImplementedError: the subclass does not provide a layout implementation.

        Layout structure is a list of line layouts, one per output line.
        Line layouts are lists than may contain the following tuples:

        * (column width of text segment, start offset, end offset)
        * (number of space characters to insert, offset or None)
        * (column width of insert text, offset, "insert text")

        The offset in the last two tuples is used to determine the
        attribute used for the inserted spaces or text respectively.
        The attribute used will be the same as the attribute at that
        text offset.  If the offset is None when inserting spaces
        then no attribute will be used.
        """
        raise NotImplementedError(
            "This function must be overridden by a real text layout class. (see StandardTextLayout)"
        )


class CanNotDisplayText(Exception):
    """Raised internally by a text layout when the given text cannot be laid out at all."""


class StandardTextLayout(TextLayout):
    """Default :class:`TextLayout` implementation, wrapping and aligning text by screen column."""

    def __init__(
        self,
        *,
        tab_stops: Iterable[int] = (),
        tab_stop_every: int = 8,
    ) -> None:
        """Create a layout with word-processor style tab stops.

        A tab character advances to the next tab stop, measured in rendered screen columns
        from the start of the displayed line: explicit *tab_stops* are used first,
        then stops repeat every *tab_stop_every* columns counted from column 0.
        With *tab_stop_every* ``0`` a tab past the last explicit stop takes no columns.

        :param tab_stops: screen columns of explicit tab stops.
        :param tab_stop_every: interval of the default tab stops, ``0`` for none.
        :raises ValueError: *tab_stop_every* is negative, or one of *tab_stops* is not a positive column.
        """
        self.tab_stops: tuple[int, ...] = tuple(sorted(set(tab_stops)))
        if tab_stop_every < 0 or (self.tab_stops and self.tab_stops[0] < 1):
            raise ValueError(f"Tab stops must be positive: {tab_stops=!r}, {tab_stop_every=!r}")
        self.tab_stop_every = tab_stop_every

    def next_tab_stop(self, column: int) -> int:
        """Return the screen column of the first tab stop after *column*.

        When there is no stop after *column*, return *column* itself: the tab takes no columns.

        >>> StandardTextLayout(tab_stops=(4, 10)).next_tab_stop(5)
        10
        >>> StandardTextLayout(tab_stops=(4, 10)).next_tab_stop(10)
        16
        >>> StandardTextLayout(tab_stops=(4, 10), tab_stop_every=0).next_tab_stop(10)
        10
        """
        if (idx := bisect.bisect_right(self.tab_stops, column)) < len(self.tab_stops):
            return self.tab_stops[idx]
        if not self.tab_stop_every:
            return column
        return (column // self.tab_stop_every + 1) * self.tab_stop_every

    def supports_align_mode(self, align: Literal["left", "center", "right"] | Align) -> bool:
        """Return True if align is 'left', 'center' or 'right'."""
        return align in {"left", "center", "right"}

    def supports_wrap_mode(self, wrap: Literal["any", "space", "clip", "ellipsis"] | WrapMode) -> bool:
        """Return True if wrap is 'any', 'space', 'clip' or 'ellipsis'."""
        return wrap in {"any", "space", "clip", "ellipsis"}

    def layout(
        self,
        text: str | bytes,
        width: int,
        align: Literal["left", "center", "right"] | Align,
        wrap: Literal["any", "space", "clip", "ellipsis"] | WrapMode,
    ) -> _LayoutFormat:
        """Return a layout structure for text."""
        try:
            segs = self.calculate_text_segments(text, width, wrap)
            return self.align_layout(text, width, segs, wrap, align)  # type: ignore[arg-type]  # segs is narrowed
        except CanNotDisplayText:
            return [[]]

    def pack(
        self,
        maxcol: int,
        layout: _LayoutFormat,
    ) -> int:
        """Return a minimal maxcol value that would result in the same number of lines for layout.

        layout must be a layout structure returned by self.layout().

        :raises ValueError: *layout* is empty.
        """
        maxwidth = 0
        if not layout:
            raise ValueError(f"huh? empty layout?: {layout!r}")
        for lines in layout:
            lw = line_width(lines)
            if lw >= maxcol:
                return maxcol
            maxwidth = max(maxwidth, lw)
        return maxwidth

    def align_layout(
        self,
        text: str | bytes,
        width: int,
        segs: _LayoutFormat,
        wrap: Literal["any", "space", "clip", "ellipsis"] | WrapMode,
        align: Literal["left", "center", "right"] | Align,
    ) -> _LayoutFormat:
        """Convert the layout segments to an aligned layout.

        :raises ValueError: *align* is not a supported alignment.
        """
        out = []
        for lines in segs:
            sc = line_width(lines)
            if sc == width or align == "left":
                out.append(lines)
                continue

            if align == "right":
                out.append([(width - sc, None), *lines])
                continue
            if align != "center":
                raise ValueError(align)
            pad_trim_left = (width - sc + 1) // 2
            out.append([(pad_trim_left, None), *lines] if pad_trim_left else lines)
        return out

    def _calculate_trimmed_segments(
        self,
        text: str | bytes,
        width: int,
        wrap: Literal["clip", "ellipsis", WrapMode.CLIP, WrapMode.ELLIPSIS],
    ) -> list[list[tuple[int, int, int | bytes] | tuple[int, int]]]:
        """Calculate text segments for cases of a text trimmed (wrap is clip or ellipsis).

        :raises ValueError: the computed padding or start offset contradicts a start column of ``0``.
        """
        segments = []

        nl: str | bytes = "\n" if isinstance(text, str) else b"\n"
        tab: str | bytes = "\t" if isinstance(text, str) else b"\t"
        encoding = get_encoding()
        ellipsis_string = get_ellipsis_string(encoding)
        ellipsis_width = _get_width(ellipsis_string)
        if (extra := width - ellipsis_width - 1) < 0:
            ellipsis_string = ellipsis_string[:extra]
            ellipsis_width = _get_width(ellipsis_string)

        ellipsis_char = ellipsis_string.encode(encoding)

        idx = 0

        while idx <= len(text):
            nl_pos = text.find(nl, idx)  # type: ignore[arg-type]  # We normalise types
            if nl_pos == -1:
                nl_pos = len(text)
            if text.find(tab, idx, nl_pos) != -1:  # type: ignore[arg-type]  # We normalise types
                tabbed_line, screen_columns, end_off = self._place_tabbed_line(text, idx, nl_pos)
                if wrap == "ellipsis" and screen_columns > width and ellipsis_width:
                    limit = width - ellipsis_width
                    tabbed_line, screen_columns, end_off = self._place_tabbed_line(text, idx, nl_pos, limit=limit)
                    tabbed_line += [(ellipsis_width, end_off, ellipsis_char), (limit - screen_columns, end_off)]
                else:
                    tabbed_line += [(0, nl_pos)]
                segments.append(tabbed_line)
                idx = nl_pos + 1
                continue

            screen_columns = calc_width(text, idx, nl_pos)

            # trim line to max width if needed, add ellipsis if trimmed
            if wrap == "ellipsis" and screen_columns > width and ellipsis_width:
                trimmed = True

                start_off, end_off, pad_left, pad_right = calc_trim_text(text, idx, nl_pos, 0, width - ellipsis_width)
                # pad_left should be 0, because the start_col parameter was 0 (no trimming on the left)
                # similarly spos should not be changed from p
                if pad_left != 0:
                    raise ValueError(f"Invalid padding for start column==0: {pad_left!r}")
                if start_off != idx:
                    raise ValueError(f"Invalid start offset for  start column==0 and position={idx!r}: {start_off!r}")
                screen_columns = width - ellipsis_width - pad_right

            else:
                trimmed = False
                end_off = nl_pos
                pad_right = 0

            line: list[tuple[int, int, int | bytes] | tuple[int, int]] = []
            if idx != end_off:
                line += [(screen_columns, idx, end_off)]
            if trimmed:
                line += [(ellipsis_width, end_off, ellipsis_char)]
            line += [(pad_right, end_off)]
            segments.append(line)
            idx = nl_pos + 1
        return segments

    def _place_tabbed_line(
        self,
        text: str | bytes,
        start: int,
        end: int,
        *,
        limit: int | None = None,
    ) -> tuple[list[tuple[int, int, int | bytes] | tuple[int, int]], int, int]:
        """Lay out one line holding tab characters without wrapping it.

        :param limit: screen columns to stop at, ``None`` to lay out the whole line.
        :returns: the line segments, their total screen columns,
            and the text offset the layout stopped at (*end* unless *limit* cut the line short).
        """
        tab: str | bytes = "\t" if isinstance(text, str) else b"\t"
        line: list[tuple[int, int, int | bytes] | tuple[int, int]] = []
        column = 0
        pos = start
        while pos < end:
            if text[pos : pos + 1] == tab:
                if not (tab_width := self.next_tab_stop(column) - column):
                    line.append((0, pos))
                    pos += 1
                    continue
                if limit is not None and column + tab_width > limit:
                    if limit > column:
                        line.append((limit - column, pos, b" " * (limit - column)))
                    return line, limit, pos
                line.append((tab_width, pos, b" " * tab_width))
                column += tab_width
                pos += 1
                continue

            if (chunk_end := text.find(tab, pos, end)) == -1:  # type: ignore[arg-type]  # same type as text
                chunk_end = end
            chunk_width = calc_width(text, pos, chunk_end)
            if limit is not None and column + chunk_width > limit:
                _, epos, _, pad_right = calc_trim_text(text, pos, chunk_end, 0, limit - column)
                if (fitted := limit - column - pad_right) > 0:
                    line.append((fitted, pos, epos))
                return line, limit - pad_right, epos
            if chunk_width:
                line.append((chunk_width, pos, chunk_end))
            column += chunk_width
            pos = chunk_end
        return line, column, end

    def _wrap_tabbed_line(
        self,
        text: str | bytes,
        start: int,
        end: int,
        width: int,
        wrap: Literal["any", "space", "clip", "ellipsis"] | WrapMode,
    ) -> list[list[tuple[int, int, int | bytes] | tuple[int, int]]]:
        """Wrap one line holding tab characters into display lines of *width* screen columns.

        Tab stops restart at column 0 on every display line. A tab reaching past *width* is cut at the line end,
        and in ``space`` mode it is also a break opportunity, like a space.

        :raises CanNotDisplayText: a character does not fit into an empty display line.
        :raises ValueError: *wrap* is not a supported wrapping mode.
        """
        if wrap not in {"any", "space"}:
            raise ValueError(wrap)
        tab: str | bytes = "\t" if isinstance(text, str) else b"\t"
        space: str | bytes = " " if isinstance(text, str) else b" "
        lines: list[list[tuple[int, int, int | bytes] | tuple[int, int]]] = []
        line: list[tuple[int, int, int | bytes] | tuple[int, int]] = []
        column = 0
        pos = start
        chunk_end = start - 1
        while pos < end:
            if text[pos : pos + 1] == tab:
                if self.next_tab_stop(column) == column:
                    line.append((0, pos))
                    pos += 1
                elif column < width:
                    tab_width = min(self.next_tab_stop(column), width) - column
                    line.append((tab_width, pos, b" " * tab_width))
                    column += tab_width
                    pos += 1
                elif not column:
                    raise CanNotDisplayText("Tab will not fit in 0-column width")
                elif wrap == "space":
                    # the tab at the line end is the break itself: drop it, as a space is dropped
                    lines.append([*line, (0, pos)])
                    line, column, pos = [], 0, pos + 1
                else:
                    lines.append(line)
                    line, column = [], 0
                continue

            # the tab found for an earlier display line still ends the chunk this one continues
            if chunk_end < pos and (chunk_end := text.find(tab, pos, end)) == -1:  # type: ignore[arg-type]
                chunk_end = end
            if (
                chunk_end - pos <= width - column
                and column + (chunk_width := calc_width(text, pos, chunk_end)) <= width
            ):
                brk = chunk_end
            else:
                brk, chunk_width = _calc_fitting_text(text, pos, chunk_end, width - column)
            if brk == chunk_end:
                if chunk_width:
                    line.append((chunk_width, pos, chunk_end))
                column += chunk_width
                pos = chunk_end
                continue

            cut: int | None = None
            resume = brk
            if wrap == "any":
                if brk > pos:
                    cut = brk
            elif text[brk : brk + 1] == space:
                cut, resume = brk, brk + 1
            elif brk > pos and is_wide_char(text, brk):
                cut = brk
            else:
                prev = brk
                while prev > pos:
                    prev = move_prev_char(text, pos, prev)
                    if text[prev : prev + 1] == space:
                        cut, resume = prev, prev + 1
                        break
                    if is_wide_char(text, prev):
                        cut = resume = move_next_char(text, prev, brk)
                        break

            if cut is None:
                if not column:
                    if brk == pos:
                        raise CanNotDisplayText("Wide character will not fit in 1-column width")
                    # no break opportunity at all: force a character wrap
                    cut = brk
                else:
                    # break right after the preceding tab
                    lines.append(line)
                    line, column = [], 0
                    continue

            if cut > pos:
                line.append((calc_width(text, pos, cut), pos, cut))
            if resume > cut:
                line.append((0, cut))  # removed character hint
            lines.append(line)
            line, column, pos = [], 0, resume

        lines.append([*line, (0, end)])  # removed character hint
        return lines

    def calculate_text_segments(
        self,
        text: str | bytes,
        width: int,
        wrap: Literal["any", "space", "clip", "ellipsis"] | WrapMode,
    ) -> list[list[tuple[int, int, int | bytes] | tuple[int, int]]]:
        """
        Calculate the segments of text to display given width screen columns to display them.

        text - Unicode text or byte string to display
        width - number of available screen columns
        wrap - wrapping mode used

        Returns a layout structure without an alignment applied.
        Each line holding a tab character is laid out with tab stops, see :meth:`next_tab_stop`::

            wrap is clip or ellipsis? --yes--> per line: tabs? --yes--> place tabs, then cut with ellipsis
                    |                                    +--no---> cut with ellipsis
                    no
                    |
            per line: tabs? --yes--> wrap tab by tab
                      +--no---> wrap at spaces or anywhere

        :raises CanNotDisplayText: a character does not fit into an empty display line.
        :raises ValueError: *wrap* is not a supported wrapping mode.
        """
        if wrap in {"clip", "ellipsis"}:
            return self._calculate_trimmed_segments(text, width, wrap)  # type: ignore[arg-type]  # filtered by if

        nl: str | bytes
        tab: str | bytes
        nl_o: int | str
        sp_o: int | str
        nl, tab, nl_o, sp_o = "\n", "\t", "\n", " "
        if isinstance(text, bytes):
            nl = b"\n"  # can only find bytes in python3 bytestrings
            tab = b"\t"
            nl_o = ord(nl_o)  # + an item of a bytestring is the ordinal value
            sp_o = ord(sp_o)

        segments: list[list[tuple[int, int, int | bytes] | tuple[int, int]]] = []
        idx = 0
        nl_pos = -1

        while idx <= len(text):
            if nl_pos < idx:
                # look for the next eligible line break, valid until idx passes it
                nl_pos = text.find(nl, idx)  # type: ignore[arg-type]  # We normalise types
                if nl_pos == -1:
                    nl_pos = len(text)

                if text.find(tab, idx, nl_pos) != -1:  # type: ignore[arg-type]  # We normalise types
                    segments.extend(self._wrap_tabbed_line(text, idx, nl_pos, width, wrap))
                    idx = nl_pos + 1
                    continue

            if nl_pos - idx <= width and (screen_columns := calc_width(text, idx, nl_pos)) <= width:
                pos = nl_pos
            else:
                pos, screen_columns = _calc_fitting_text(text, idx, nl_pos, width)
            if pos == nl_pos:
                if screen_columns:
                    # this segment fits
                    segments.append([(screen_columns, idx, nl_pos), (0, nl_pos)])
                else:
                    segments.append([(0, nl_pos)])
                # removed character hint
                idx = nl_pos + 1
                continue

            if pos == idx:  # pathological width=1 double-byte case
                raise CanNotDisplayText("Wide character will not fit in 1-column width")

            if wrap == "any":
                segments.append([(screen_columns, idx, pos)])
                idx = pos
                continue

            if wrap != "space":
                raise ValueError(wrap)

            if text[pos] == sp_o:
                # perfect space wrap
                segments.append([(screen_columns, idx, pos), (0, pos)])
                # removed character hint

                idx = pos + 1
                continue

            if is_wide_char(text, pos):
                # perfect next wide
                segments.append([(screen_columns, idx, pos)])
                idx = pos
                continue

            prev = pos
            while prev > idx:
                prev = move_prev_char(text, idx, prev)
                if text[prev] == sp_o:
                    screen_columns = calc_width(text, idx, prev)
                    line: list[tuple[int, int, int | bytes] | tuple[int, int]] = [(0, prev)]
                    if idx != prev:
                        line = [(screen_columns, idx, prev), *line]
                    segments.append(line)
                    idx = prev + 1
                    break

                if is_wide_char(text, prev):
                    # wrap after wide char
                    next_char = move_next_char(text, prev, pos)
                    screen_columns = calc_width(text, idx, next_char)
                    segments.append([(screen_columns, idx, next_char)])
                    idx = next_char
                    break

            else:
                # unwrap previous line space if possible to
                # fit more text (we're breaking a word anyway)
                if segments and (len(segments[-1]) == 2 or (len(segments[-1]) == 1 and len(segments[-1][0]) == 2)):
                    # look for the removed space above
                    if len(segments[-1]) == 1:
                        [(h_sc, h_off)] = segments[-1]  # type: ignore[misc]  # we filtered-out case
                        p_sc = 0
                        p_off = _p_end = h_off

                    else:
                        [(p_sc, p_off, _p_end), (h_sc, h_off)] = segments[-1]  # type: ignore[misc]

                    if (
                        p_sc < width
                        and h_sc == 0
                        and text[h_off] == sp_o
                        # a join that breaks before the removed space again would repeat this line forever
                        and (joined := calc_text_pos(text, p_off, nl_pos, width))[0] > h_off
                    ):
                        # combine with the previous line
                        del segments[-1]
                        idx = p_off
                        pos, screen_columns = joined
                        segments.append([(screen_columns, idx, pos)])
                        # check for trailing " " or "\n"
                        idx = pos
                        if idx < len(text) and (text[idx] in {sp_o, nl_o}):
                            # removed character hint
                            segments[-1].append((0, idx))
                            idx += 1
                        continue

                # force any char wrap
                segments.append([(screen_columns, idx, pos)])
                idx = pos
        return segments


######################################
# default layout object to use
default_layout = StandardTextLayout()
######################################


class LayoutSegment:
    """One segment of a line layout: a run of text, an inserted range of spaces, or inserted text."""

    __slots__ = ("end", "offs", "sc", "text")

    sc: int
    offs: int | None
    text: bytes | None
    end: int | None

    def __init__(self, seg: _LayoutSegment) -> None:
        """Create object from line layout segment structure.

        :raises TypeError: *seg* is not a tuple, or one of its members has the wrong type.
        :raises ValueError: *seg* does not have 2 or 3 members, or holds an out-of-range screen column count.
        """
        if not isinstance(seg, tuple):
            raise TypeError(seg)

        # Unpacking in one step instead of slicing and re-indexing: this runs once per layout segment
        # of every rendered line.
        if len(seg) == 3:
            self.sc, self.offs, t = seg
            if not isinstance(self.sc, int):
                raise TypeError(self.sc)
            if not isinstance(self.offs, int):
                raise TypeError(self.offs)
            if self.sc <= 0:
                raise ValueError(seg)
            if isinstance(t, bytes):
                self.text = t
                self.end = None
            else:
                if not isinstance(t, int):
                    raise TypeError(t)
                self.text = None
                self.end = t
        elif len(seg) == 2:
            self.sc, self.offs = seg
            if not isinstance(self.sc, int):
                raise TypeError(self.sc)
            if self.offs is not None:
                if self.sc < 0:
                    raise ValueError(seg)
                if not isinstance(self.offs, int):
                    raise TypeError(self.offs)
            self.text = self.end = None
        else:
            raise ValueError(seg)

    def subseg(
        self,
        text: str | bytes,
        start: int,
        end: int,
    ) -> _LayoutLine:
        """
        Return a "sub-segment" list containing segment structures that make up a portion of this segment.

        A list is returned to handle cases where wide characters
        need to be replaced with a space character at either edge
        so two or three segments will be returned.

        :raises ValueError: this segment carries text or an end offset but no text offset.
        """
        start = max(start, 0)
        end = min(end, self.sc)

        if start >= end:
            return []  # completely gone

        if self.text or self.end:
            if self.offs is None:
                raise ValueError("offs cannot be None when text or end is present")

            if self.text:
                # use text stored in segment (self.text)
                spos, epos, pad_left, pad_right = calc_trim_text(self.text, 0, len(self.text), start, end)
                return [
                    (
                        end - start,
                        self.offs,
                        b"".ljust(pad_left) + self.text[spos:epos] + b"".ljust(pad_right),
                    )
                ]
            if self.end:
                # use text passed as parameter (text)
                spos, epos, pad_left, pad_right = calc_trim_text(text, self.offs, self.end, start, end)
                lines: list[tuple[int, int, int] | tuple[int, int]] = []
                if pad_left:
                    lines.append((1, move_prev_char(text, self.offs, spos)))
                # A window that both starts and ends inside wide characters has
                # nothing left between the two padding cells, and a segment of
                # zero screen columns is not a shape LayoutSegment accepts.
                if (seg_cols := end - start - pad_left - pad_right) > 0:
                    lines.append((seg_cols, spos, epos))
                if pad_right:
                    lines.append((1, epos))
                return lines  # type: ignore[return-value]  # we're narrowing return type

        return [(end - start, self.offs)]


def line_width(segs: _LayoutLine) -> int:
    """
    Return the screen column width of one line of a text layout structure.

    This function ignores any existing shift applied to the line,
    represented by an (amount, None) tuple at the start of the line.
    """
    sc = 0
    seglist = segs
    if segs and len(segs[0]) == 2 and segs[0][1] is None:
        seglist = segs[1:]
    for s in seglist:
        sc += s[0]
    return sc


def shift_line(
    segs: _LayoutLine,
    amount: int,
) -> _LayoutLine:
    """
    Return a shifted line from a layout structure to the left or right.

    :param segs: line of a layout structure
    :param amount: screen columns to shift right (+ve) or left (-ve)
    :raises TypeError: *amount* is not an integer.
    """
    if not isinstance(amount, int):
        raise TypeError(amount)

    if segs and len(segs[0]) == 2 and segs[0][1] is None:
        # existing shift
        amount += segs[0][0]
        if amount:
            return [(amount, None), *segs[1:]]
        return segs[1:]

    if amount:
        return [(amount, None), *segs]
    return segs


def trim_line(
    segs: _LayoutLine,
    text: str | bytes,
    start: int,
    end: int,
) -> _LayoutLine:
    """
    Return a trimmed line of a text layout structure.

    :param text: text to which this layout structure applies
    :param start: starting screen column
    :param end: ending screen column
    """
    result = []
    x = 0
    for seg in segs:
        sc = seg[0]
        if start or sc < 0:
            if start >= sc:
                start -= sc
                x += sc
                continue
            s = LayoutSegment(seg)
            if x + sc >= end:
                # can all be done at once
                return s.subseg(text, start, end - x)
            result += s.subseg(text, start, sc)
            start = 0
            x += sc
            continue
        if x >= end:
            break
        if x + sc > end:
            s = LayoutSegment(seg)
            result += s.subseg(text, 0, end - x)
            break
        result.append(seg)
        x += sc
    return result


def _calc_literal_line_pos(
    text: str | bytes,
    line_layout: _LayoutLine,
    pref_col: Literal["left", "right", Align.LEFT, Align.RIGHT],
) -> int | None:
    """
    Return the text position closest to *pref_col* on a line laid out without wrapping.

    :raises ValueError: *pref_col* is neither an integer nor ``'left'``/``'right'``.
    """
    if pref_col == "left":
        for seg in line_layout:
            layout = LayoutSegment(seg)
            if layout.offs is not None:
                return layout.offs
        return None

    if pref_col == "right":
        found = None
        for seg in line_layout:
            s = LayoutSegment(seg)
            if s.offs is not None:
                found = s

        if found is None:
            return None

        if found.end is None:
            return found.offs

        return calc_text_pos(
            text,
            found.offs,  # type: ignore[arg-type]  # filtered by if in lookup cycle
            found.end,
            found.sc - 1,
        )[0]

    raise ValueError(f"Invalid pref_col value: {pref_col}")


def calc_line_pos(
    text: str | bytes,
    line_layout: _LayoutLine,
    pref_col: Literal["left", "right", Align.LEFT, Align.RIGHT] | int,
) -> int | None:
    """Calculate the closest linear position to pref_col given a line layout structure.

    Returns None if no position found.

    :raises TypeError: *pref_col* is neither an integer nor ``'left'``/``'right'``.
    """
    if pref_col in {"left", "right"}:
        return _calc_literal_line_pos(text, line_layout, pref_col)

    if not isinstance(pref_col, int):
        raise TypeError(f"Invalid pref_col value: {pref_col}")

    closest_sc = None
    closest_pos: LayoutSegment | int | None = None
    current_sc = 0

    for seg in line_layout:
        s = LayoutSegment(seg)
        if s.offs is not None:
            if s.end is not None:
                if current_sc <= pref_col < current_sc + s.sc:
                    # exact match within this segment
                    return calc_text_pos(text, s.offs, s.end, pref_col - current_sc)[0]
                if current_sc <= pref_col:
                    closest_sc = current_sc + s.sc - 1
                    closest_pos = s

            if closest_sc is None or (abs(pref_col - current_sc) < abs(pref_col - closest_sc)):
                # this screen column is closer
                closest_sc = current_sc
                closest_pos = s.offs
            if current_sc > closest_sc:
                # we're moving past
                break
        current_sc += s.sc

    if closest_pos is None or isinstance(closest_pos, int):
        return closest_pos

    # return the last positions in the segment "closest_pos"
    s = closest_pos
    return calc_text_pos(text, s.offs, s.end, s.sc - 1)[0]  # type: ignore[arg-type]  # s.offs and s.end are set


def calc_pos(
    text: str | bytes,
    layout: _LayoutFormat,
    pref_col: Literal["left", "right", Align.LEFT, Align.RIGHT] | int,
    row: int,
) -> int:
    """Calculate the closest linear position to pref_col and row given a layout structure.

    :raises ValueError: *row* is outside the rows of *layout*.
    """
    if row < 0 or row >= len(layout):
        raise ValueError("calculate_pos: out of layout row range")

    if (pos := calc_line_pos(text, layout[row], pref_col)) is not None:
        return pos

    rows_above = list(range(row - 1, -1, -1))
    rows_below = list(range(row + 1, len(layout)))
    while rows_above and rows_below:
        if rows_above:
            r = rows_above.pop(0)
            if (pos := calc_line_pos(text, layout[r], pref_col)) is not None:
                return pos

        if rows_below:
            r = rows_below.pop(0)
            if (pos := calc_line_pos(text, layout[r], pref_col)) is not None:
                return pos

    return 0


def calc_coords(
    text: str | bytes,
    layout: _LayoutFormat,
    pos: int,
    clamp: int = 1,
) -> tuple[int, int]:
    """
    Calculate the coordinates closest to position pos in text with layout.

    :param text: raw string or unicode string
    :param layout: layout structure applied to text
    :param pos: integer position into text
    :param clamp: ignored right now
    """
    closest: tuple[int, tuple[int, int]] | None = None
    y = 0
    for line_layout in layout:
        x = 0
        for seg in line_layout:
            s = LayoutSegment(seg)
            if s.offs is None:
                x += s.sc
                continue
            if s.offs == pos:
                return x, y
            if s.end is not None and s.offs <= pos < s.end:
                x += calc_width(text, s.offs, pos)
                return x, y
            distance = abs(s.offs - pos)
            if s.end is not None and s.end < pos:
                distance = pos - (s.end - 1)
            if closest is None or distance < closest[0]:  # pylint: disable=unsubscriptable-object
                closest = distance, (x, y)
            x += s.sc
        y += 1

    if closest:
        return closest[1]
    return 0, 0
urwid-urwid-69907e4/urwid/util.py000066400000000000000000000476151526073734300167750ustar00rootroot00000000000000# Urwid utility functions
#    Copyright (C) 2004-2011  Ian Ward
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/


"""Encoding detection and conversion, run-length encoding and markup helpers."""

from __future__ import annotations

import contextlib
import itertools
import sys
import typing
import warnings
from contextlib import suppress

from urwid import str_util

if typing.TYPE_CHECKING:
    from collections.abc import Generator, Hashable, Iterable, Iterator, MutableSequence
    from types import TracebackType

    from typing_extensions import Literal, Protocol, Self

    class CanBeStopped(Protocol):
        """Protocol for an object providing a no-argument ``stop()`` method."""

        def stop(self) -> None:
            """Stop whatever the implementing object is running."""

    class _TagMarkupList(Protocol):
        """List of markup parts joined together by `decompose_tagmarkup`.

        `_tagmarkup_recurse` traverses only `list` instances as containers of parts,
        while a `tuple` always means a single (display attribute, markup) pair,
        so `tuple` must not be accepted here.
        A protocol is used instead of `list["_TagMarkup"]` because `list` is invariant:
        a plain `list[str | tuple[Hashable, str]]` is not a `list["_TagMarkup"]`.
        `reverse` is required only to exclude `tuple`, elements are never modified.
        """

        def __iter__(self) -> Iterator[_TagMarkup]: ...

        def __len__(self) -> int: ...

        def reverse(self) -> None:
            """Reverse the list of tag markup elements in place."""

    _TagMarkup = typing.Union[str, bytes, tuple[Hashable, "_TagMarkup"], "_TagMarkupList"]


def __getattr__(name: str) -> typing.Any:
    """
    Resolve module attributes that moved to another module, warning about the move.

    :raises AttributeError: *name* is not defined in this module.
    """
    if hasattr(str_util, name):
        warnings.warn(
            f"Do not import {name!r} from {__package__}.{__name__}, import it from 'urwid'.",
            DeprecationWarning,
            stacklevel=3,
        )
        return getattr(str_util, name)
    raise AttributeError(f"{name} is not defined in {__package__}.{__name__}")


def detect_encoding() -> str:
    """Return the name of the encoding to use for terminal I/O, detected from the platform."""
    # Windows is a special case:
    # CMD is Unicode and non-unicode at the same time:
    # Unicode display support depends on C API usage and partially limited by font settings.
    # By default, python is distributed in "Unicode" version, and since Python version 3.8 only Unicode.
    # In case of curses, "windows-curses" is distributed in unicode-only.
    # Since Windows 10 default console font is already unicode,
    # in older versions maybe need to set TTF font manually to support more symbols
    # (this does not affect unicode IO).
    if sys.platform == "win32" and sys.getdefaultencoding() == "utf-8":
        return "utf-8"
    # Try to determine if using a supported double-byte encoding
    import locale

    no_set_locale = locale.getpreferredencoding(False)

    if no_set_locale != "ascii":
        # ascii is fallback locale in case of detect failed

        return no_set_locale

    # Use actual `getpreferredencoding` with public API only
    old_loc = locale.setlocale(locale.LC_CTYPE)  # == getlocale, but not mangle data
    try:
        with suppress(locale.Error):
            locale.setlocale(locale.LC_CTYPE, "")
        # internally call private `_get_locale_encoding`
        return locale.getpreferredencoding(False)
    finally:
        with suppress(locale.Error):
            locale.setlocale(locale.LC_CTYPE, old_loc)


if "detected_encoding" not in locals():
    detected_encoding = detect_encoding()
else:
    raise RuntimeError("Encoding detection broken")

_target_encoding = "ascii"
_use_dec_special = True


def set_encoding(encoding: str) -> None:
    """Set the byte encoding to assume for processing strings, and to use when converting unicode strings."""
    encoding = encoding.lower()

    global _target_encoding, _use_dec_special  # noqa: PLW0603  # noqa: PLW0603  # pylint: disable=global-statement

    if encoding in {"utf-8", "utf8", "utf"}:
        str_util.set_byte_encoding("utf8")

        _use_dec_special = False
    elif encoding in {
        "euc-jp",  # JISX 0208 only
        "euc-kr",
        "euc-cn",
        "euc-tw",  # CNS 11643 plain 1 only
        "gb2312",
        "gbk",
        "big5",
        "cn-gb",
        "uhc",
        # these shouldn't happen, should they?
        "eucjp",
        "euckr",
        "euccn",
        "euctw",
        "cncb",
    }:
        str_util.set_byte_encoding("wide")

        _use_dec_special = True
    else:
        str_util.set_byte_encoding("narrow")
        _use_dec_special = True

    # if encoding is valid for conversion from unicode, remember it
    _target_encoding = "ascii"
    with contextlib.suppress(LookupError):
        if encoding:
            "".encode(encoding)
            _target_encoding = encoding


def get_encoding() -> str:
    """Get target encoding."""
    return _target_encoding


@contextlib.contextmanager
def set_temporary_encoding(encoding_name: str) -> Generator[None]:
    """Temporarily set the target encoding, for encoding-specific validation in unittests/doctests.

    Not exported globally.
    """
    old_encoding = _target_encoding
    try:
        set_encoding(encoding_name)
        yield
    finally:
        set_encoding(old_encoding)


def get_encoding_mode() -> Literal["wide", "narrow", "utf8"]:
    """Get the mode Urwid is using when processing text strings.

    Returns 'narrow' for 8-bit encodings, 'wide' for CJK encodings
    or 'utf8' for UTF-8 encodings.
    """
    return str_util.get_byte_encoding()


def apply_target_encoding(s: str | bytes) -> tuple[bytes, list[tuple[Literal["U", "0"] | None, int]]]:
    """
    Return (encoded byte string, character set rle).

    :raises TypeError: *s* could not be encoded to the target encoding.
    """
    # Import locally to warranty no circular imports
    from urwid.display import escape

    if _use_dec_special and isinstance(s, str):
        # first convert drawing characters
        s = s.translate(escape.DEC_SPECIAL_CHARMAP)

    if isinstance(s, str):
        # remove redundant shifts
        s = s.replace(escape._SI_SO, "")  # pylint: disable=protected-access
        # `str.encode` over `codecs.encode`: the latter re-looks-up the codec on every call.
        s = s.encode(_target_encoding, "replace")

    if not isinstance(s, bytes):
        raise TypeError(s)
    # Pre-encoded forms of `escape.SO`/`escape.SI`. They live next to the originals so the two cannot
    # drift apart, and are read here rather than encoded per call because this function runs for every
    # rendered text segment.
    SO = escape._SO_BYTES  # pylint: disable=protected-access
    SI = escape._SI_BYTES  # pylint: disable=protected-access

    sis = s.split(SO)

    sis0 = sis[0].replace(SI, b"")
    cout: list[tuple[Literal["U", "0"] | None, int]] = []
    if sis0:
        cout.append((None, len(sis0)))

    if len(sis) == 1:
        # No DEC drawing-character shift in the output: `sout`/`outstr` are never
        # needed, so skip building the list for this (common) case.
        return sis0, cout

    sout = [sis0] if sis0 else []
    for sn in sis[1:]:
        sl = sn.split(SI, 1)
        if len(sl) == 1:
            sin = sl[0]
            sout.append(sin)
            rle_append_modify(cout, (escape.DEC_TAG, len(sin)))  # type: ignore[arg-type]
            continue

        sin, son = sl
        son = son.replace(SI, b"")
        if sin:
            sout.append(sin)
            rle_append_modify(cout, (escape.DEC_TAG, len(sin)))  # type: ignore[arg-type]

        if son:
            sout.append(son)
            rle_append_modify(cout, (None, len(son)))  # type: ignore[arg-type]

    outstr = b"".join(sout)
    return outstr, cout


######################################################################
# Try to set the encoding using the one detected by the locale module
set_encoding(detected_encoding)
######################################################################


def supports_unicode() -> bool:
    """Return True if python is able to convert non-ascii unicode strings to the current encoding."""
    return bool(_target_encoding and _target_encoding != "ascii")


def calc_trim_text(
    text: str | bytes,
    start_offs: int,
    end_offs: int,
    start_col: int,
    end_col: int,
) -> tuple[int, int, int, int]:
    """
    Calculate the result of trimming text.

    :param start_offs: offset into text to treat as screen column 0
    :param end_offs: offset into text to treat as the end of the line
    :param start_col: screen column to trim at the left
    :param end_col: screen column to trim at the right

    :returns: a ``(start, end, pad_left, pad_right)`` tuple, where ``start`` is the resulting start offset,
        ``end`` the resulting end offset, and ``pad_left``/``pad_right`` are ``0`` for no pad
        or ``1`` for one space to be added.
    """
    spos = start_offs
    pad_left = pad_right = 0
    if start_col > 0:
        spos, sc = str_util.calc_text_pos(text, spos, end_offs, start_col)
        if sc < start_col:
            pad_left = 1
            spos, sc = str_util.calc_text_pos(text, start_offs, end_offs, start_col + 1)
    run = end_col - start_col - pad_left
    pos, sc = str_util.calc_text_pos(text, spos, end_offs, run)
    if sc < run:
        pad_right = 1
    return (spos, pos, pad_left, pad_right)


def trim_text_attr_cs(
    text: bytes,
    attr: MutableSequence[tuple[Hashable, int]],
    cs: MutableSequence[tuple[Hashable, int]],
    start_col: int,
    end_col: int,
) -> tuple[bytes, list[tuple[Hashable, int]], list[tuple[Hashable, int]]]:
    """Return ( trimmed text, trimmed attr, trimmed cs )."""
    spos, epos, pad_left, pad_right = calc_trim_text(text, 0, len(text), start_col, end_col)
    attrtr = rle_subseg(attr, spos, epos)
    cstr = rle_subseg(cs, spos, epos)
    if pad_left:
        al = rle_get_at(attr, spos - 1)
        rle_prepend_modify(attrtr, (al, 1))
        rle_prepend_modify(cstr, (None, 1))
    if pad_right:
        al = rle_get_at(attr, epos)
        rle_append_modify(attrtr, (al, 1))
        rle_append_modify(cstr, (None, 1))

    return (b"".rjust(pad_left) + text[spos:epos] + b"".rjust(pad_right), attrtr, cstr)


def rle_get_at(
    rle: Iterable[tuple[Hashable, int]],
    pos: int,
) -> Hashable | None:
    """Return the attribute at offset pos."""
    x = 0
    if pos < 0:
        return None
    for a, run in rle:
        if x + run > pos:
            return a
        x += run
    return None


def rle_subseg(
    rle: Iterable[tuple[Hashable, int]],
    start: int,
    end: int,
) -> list[tuple[Hashable, int]]:
    """Return a sub segment of a rle list."""
    sub_segment = []
    x = 0
    for a, run in rle:
        if start:
            if start >= run:
                start -= run
                x += run
                continue
            x += start
            run -= start  # noqa: PLW2901
            start = 0
        if x >= end:
            break
        if x + run > end:
            run = end - x  # noqa: PLW2901
        x += run
        sub_segment.append((a, run))
    return sub_segment


def rle_len(
    rle: Iterable[tuple[Hashable, int]],
) -> int:
    """Return the number of characters covered by a run length encoded attribute list.

    :raises TypeError: an item of *rle* is not a ``(value, run length)`` tuple.
    """
    run = 0
    for v in rle:
        if not isinstance(v, tuple):
            raise TypeError(rle)
        _a, r = v
        run += r
    return run


def rle_prepend_modify(
    rle: MutableSequence[tuple[Hashable, int]],
    a_r: tuple[Hashable, int],
) -> None:
    """Append (a, r) (unpacked from *a_r*) to BEGINNING of rle.

    Merge with first run when possible.

    MODIFIES rle parameter contents. Returns None.
    """
    a, r = a_r
    if not rle:
        rle[:] = [(a, r)]
    else:
        al, run = rle[0]
        if a == al:
            rle[0] = (a, run + r)
        else:
            rle[0:0] = [(a, r)]


def rle_append_modify(
    rle: MutableSequence[tuple[Hashable, int]],
    a_r: tuple[Hashable, int],
) -> None:
    """Append (a, r) (unpacked from *a_r*) to the rle list rle.

    Merge with the last run when possible.

    MODIFIES rle parameter contents. Returns None.
    """
    a, r = a_r
    if not rle or rle[-1][0] != a:
        rle.append((a, r))
        return
    _la, lr = rle[-1]
    rle[-1] = (a, lr + r)


def rle_join_modify(
    rle: MutableSequence[tuple[Hashable, int]],
    rle2: MutableSequence[tuple[Hashable, int]],
) -> None:
    """Append attribute list rle2 to rle.

    Merge last run of rle with first run of rle2 when possible.

    MODIFIES attr parameter contents. Returns None.
    """
    if not rle2:
        return
    rle_append_modify(rle, rle2[0])
    # Extend directly from rle2 rather than through a `rle2[1:]` copy.
    rle.extend(itertools.islice(rle2, 1, None))


def rle_product(
    rle1: MutableSequence[tuple[Hashable, int]],
    rle2: MutableSequence[tuple[Hashable, int]],
) -> list[tuple[tuple[Hashable, Hashable], int]]:
    """Merge the runs of rle1 and rle2.

    eg.
    rle1 = [ ("a", 10), ("b", 5) ]
    rle2 = [ ("Q", 5), ("P", 10) ]
    rle_product: [ (("a","Q"), 5), (("a","P"), 5), (("b","P"), 5) ]

    rle1 and rle2 are assumed to cover the same total run.
    """
    i1 = i2 = 1  # rle1, rle2 indexes
    if not rle1 or not rle2:
        return []
    a1, r1 = rle1[0]
    a2, r2 = rle2[0]

    result: list[tuple[tuple[Hashable, Hashable], int]] = []
    while True:
        # Skip zero-length runs (e.g. produced by empty markup segments): they carry no
        # columns but must not stop the merge early, otherwise following runs are dropped.
        while r1 == 0 and i1 < len(rle1):
            a1, r1 = rle1[i1]
            i1 += 1
        while r2 == 0 and i2 < len(rle2):
            a2, r2 = rle2[i2]
            i2 += 1
        if r1 == 0 or r2 == 0:
            break

        r = min(r1, r2)
        rle_append_modify(result, ((a1, a2), r))  # type: ignore[arg-type]
        r1 -= r
        r2 -= r
    return result


class TagMarkupException(Exception):
    """Raised when tag markup passed to :func:`decompose_tagmarkup` is malformed."""


def decompose_tagmarkup(tm: _TagMarkup) -> tuple[str | bytes, list[tuple[Hashable, int]]]:
    """Return (text string, attribute list) for tagmarkup passed.

    Elements that are not of the type of the first one are converted with the target encoding,
    replacing what cannot be converted, as :func:`apply_target_encoding` does.
    """
    tl, al = _tagmarkup_recurse(tm, None)
    # join as str or bytes based on type of first element
    text: str | bytes
    converted: list[str] | list[bytes]
    if tl:
        encoding = get_encoding()
        if isinstance(tl[0], str):
            converted = [item if isinstance(item, str) else item.decode(encoding, "replace") for item in tl]
            text = "".join(converted)
        else:
            converted = [item if isinstance(item, bytes) else item.encode(encoding, "replace") for item in tl]
            text = b"".join(converted)
        if len(text) != sum(len(item) for item in tl):
            # the attribute runs count the elements before conversion: recount them after it
            items = zip(tl, converted)
            for i, (attr, run) in enumerate(al):
                remaining, new_run = run, 0
                while remaining > 0:
                    item, converted_item = next(items)
                    remaining -= len(item)
                    new_run += len(converted_item)
                al[i] = (attr, new_run)
    else:
        text = ""

    if al and al[-1][0] is None:
        del al[-1]

    return text, al


def _tagmarkup_recurse(
    tm: _TagMarkup,
    attr: Hashable,
) -> tuple[list[str | bytes], list[tuple[Hashable, int]]]:
    """Return (text list, attribute list) for tagmarkup passed.

    :param tm: tagmarkup
    :param attr: current attribute or None
    :raises TagMarkupException: an element is neither text nor an ``(attribute, tagmarkup)`` pair.
    """
    if isinstance(tm, list):
        # for lists recurse to process each subelement
        rtl: list[str | bytes] = []
        ral: list[tuple[Hashable, int]] = []
        for element in tm:
            tl, al = _tagmarkup_recurse(element, attr)
            if ral and al:
                # merge attributes when possible
                last_attr, last_run = ral[-1]
                top_attr, top_run = al[0]
                if last_attr == top_attr:
                    ral[-1] = (top_attr, last_run + top_run)
                    del al[0]
            rtl += tl
            ral += al
        return rtl, ral

    if isinstance(tm, tuple):
        # tuples mark a new attribute boundary
        if len(tm) != 2:
            raise TagMarkupException(f"Tuples must be in the form (attribute, tagmarkup): {tm!r}")

        attr, element = tm
        return _tagmarkup_recurse(element, attr)

    if not isinstance(tm, (str, bytes)):
        raise TagMarkupException(f"Invalid markup element: {tm!r}")

    # text
    return [tm], [(attr, len(tm))]


def is_mouse_event(ev: tuple[str, int, int, int] | typing.Any) -> bool:
    """Return whether *ev* is a 4-tuple input event whose name contains "mouse"."""
    return isinstance(ev, tuple) and len(ev) == 4 and "mouse" in ev[0]


def is_mouse_press(ev: str) -> bool:
    """Return whether *ev* is a mouse event name for a button press."""
    return "press" in ev


class MetaSuper(type):
    """Metaclass kept only so that existing class definitions keep importing.

    All logic has been removed; it is a plain :class:`type` subclass.

    .. deprecated:: 3.0.0
        Drop it from the class bases. While it is still listed, move it to the last position,
        so that future changes to the other bases are not blocked by it.
    """

    __slots__ = ()


def int_scale(val: int, val_range: int, out_range: int) -> int:
    """Scale val in the range [0, val_range-1] to an integer in the range [0, out_range-1].

    This implementation uses the "round-half-up" rounding method.

    >>> "%x" % int_scale(0x7, 0x10, 0x10000)
    '7777'
    >>> "%x" % int_scale(0x5F, 0x100, 0x10)
    '6'
    >>> int_scale(2, 6, 101)
    40
    >>> int_scale(1, 3, 4)
    2
    """
    num = int(val * (out_range - 1) * 2 + (val_range - 1))
    dem = (val_range - 1) * 2
    # if num % dem == 0 then we are exactly half-way and have rounded up.
    return num // dem


class StoppingContext(contextlib.AbstractContextManager["StoppingContext"]):
    """Context manager that calls ``stop`` on a given object on exit.

    Used to make the ``start`` method on `MainLoop` and `BaseScreen` optionally act as context managers.
    """

    __slots__ = ("_wrapped",)

    def __init__(self, wrapped: CanBeStopped) -> None:
        """Wrap `wrapped`, whose `stop` method is called on exit."""
        self._wrapped = wrapped

    def __enter__(self) -> Self:
        """Return self, for use as a context manager."""
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc_val: BaseException | None,
        exc_tb: TracebackType | None,
    ) -> None:
        """Stop the wrapped object on exiting the context."""
        self._wrapped.stop()
urwid-urwid-69907e4/urwid/vterm.py000066400000000000000000001663601526073734300171540ustar00rootroot00000000000000# Urwid terminal emulation widget
#    Copyright (C) 2010  aszlig
#    Copyright (C) 2011  Ian Ward
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/


"""Terminal emulator widget and the canvas it renders its screen on."""

from __future__ import annotations

import atexit
import copy
import errno
import fcntl
import os
import pty
import selectors
import signal
import struct
import sys
import termios
import time
import traceback
import typing
import warnings
from collections import deque
from contextlib import suppress
from dataclasses import dataclass

from urwid import event_loop, util
from urwid.ansi_parser import led_state, resolve_osc_title, sgi_params_to_attrspec
from urwid.canvas import Canvas
from urwid.display import AttrSpec, RealTerminal
from urwid.display.escape import ALT_DEC_SPECIAL_CHARS, DEC_SPECIAL_CHARS
from urwid.widget import Sizing, Widget

if typing.TYPE_CHECKING:
    from collections.abc import Callable, Hashable, Iterable, Iterator, Mapping, Sequence

    from typing_extensions import Literal

EOF = b""
ESC = chr(27)
ESC_B = b"\x1b"

KEY_TRANSLATIONS = {
    "enter": "\r",
    "backspace": chr(127),
    "tab": "\t",
    "esc": ESC,
    "up": f"{ESC}[A",
    "down": f"{ESC}[B",
    "right": f"{ESC}[C",
    "left": f"{ESC}[D",
    "home": f"{ESC}[1~",
    "insert": f"{ESC}[2~",
    "delete": f"{ESC}[3~",
    "end": f"{ESC}[4~",
    "page up": f"{ESC}[5~",
    "page down": f"{ESC}[6~",
    "begin paste": f"{ESC}[200~",
    "end paste": f"{ESC}[201~",
    "f1": f"{ESC}[[A",
    "f2": f"{ESC}[[B",
    "f3": f"{ESC}[[C",
    "f4": f"{ESC}[[D",
    "f5": f"{ESC}[[E",
    "f6": f"{ESC}[17~",
    "f7": f"{ESC}[18~",
    "f8": f"{ESC}[19~",
    "f9": f"{ESC}[20~",
    "f10": f"{ESC}[21~",
    "f11": f"{ESC}[23~",
    "f12": f"{ESC}[24~",
}

KEY_TRANSLATIONS_DECCKM = {
    "up": f"{ESC}OA",
    "down": f"{ESC}OB",
    "right": f"{ESC}OC",
    "left": f"{ESC}OD",
    "f1": f"{ESC}OP",
    "f2": f"{ESC}OQ",
    "f3": f"{ESC}OR",
    "f4": f"{ESC}OS",
    "f5": f"{ESC}[15~",
}


class CSIAlias(typing.NamedTuple):
    """Entry in :data:`CSI_COMMANDS` redirecting a CSI final byte to another command's byte."""

    alias_mark: str  # can not have constructor with default first and non-default second arg
    alias: bytes


class CSICommand(typing.NamedTuple):
    """Entry in :data:`CSI_COMMANDS` describing how to parse and dispatch one CSI sequence."""

    num_args: int
    default: int
    callback: Callable[[TermCanvas, list[int], bool], typing.Any]  # return value ignored


CSI_COMMANDS: dict[bytes, CSIAlias | CSICommand] = {
    # possible values:
    #     None -> ignore sequence
    #     (, , callback)
    #     ('alias', )
    #
    # while callback is executed as:
    #     callback(, arguments, has_question_mark)
    b"@": CSICommand(1, 1, lambda s, number, q: s.insert_chars(chars=number[0])),
    b"A": CSICommand(1, 1, lambda s, rows, q: s.move_cursor(0, -rows[0], relative=True)),
    b"B": CSICommand(1, 1, lambda s, rows, q: s.move_cursor(0, rows[0], relative=True)),
    b"C": CSICommand(1, 1, lambda s, cols, q: s.move_cursor(cols[0], 0, relative=True)),
    b"D": CSICommand(1, 1, lambda s, cols, q: s.move_cursor(-cols[0], 0, relative=True)),
    b"E": CSICommand(1, 1, lambda s, rows, q: s.move_cursor(0, rows[0], relative_y=True)),
    b"F": CSICommand(1, 1, lambda s, rows, q: s.move_cursor(0, -rows[0], relative_y=True)),
    b"G": CSICommand(1, 1, lambda s, col, q: s.move_cursor(col[0] - 1, 0, relative_y=True)),
    b"H": CSICommand(2, 1, lambda s, x_y, q: s.move_cursor(x_y[1] - 1, x_y[0] - 1)),
    b"J": CSICommand(1, 0, lambda s, mode, q: s.csi_erase_display(mode[0])),
    b"K": CSICommand(1, 0, lambda s, mode, q: s.csi_erase_line(mode[0])),
    b"L": CSICommand(1, 1, lambda s, number, q: s.insert_lines(lines=number[0])),
    b"M": CSICommand(1, 1, lambda s, number, q: s.remove_lines(lines=number[0])),
    b"P": CSICommand(1, 1, lambda s, number, q: s.remove_chars(chars=number[0])),
    b"X": CSICommand(
        1,
        1,
        lambda s, number, q: s.erase(s.term_cursor, (s.term_cursor[0] + number[0] - 1, s.term_cursor[1])),
    ),
    b"a": CSIAlias("alias", b"C"),
    b"c": CSICommand(0, 0, lambda s, none, q: s.csi_get_device_attributes(q)),
    b"d": CSICommand(1, 1, lambda s, row, q: s.move_cursor(0, row[0] - 1, relative_x=True)),
    b"e": CSIAlias("alias", b"B"),
    b"f": CSIAlias("alias", b"H"),
    b"g": CSICommand(1, 0, lambda s, mode, q: s.csi_clear_tabstop(mode[0])),
    b"h": CSICommand(1, 0, lambda s, modes, q: s.csi_set_modes(modes, q)),
    b"l": CSICommand(1, 0, lambda s, modes, q: s.csi_set_modes(modes, q, reset=True)),
    b"m": CSICommand(1, 0, lambda s, attrs, q: s.csi_set_attr(attrs)),
    b"n": CSICommand(1, 0, lambda s, mode, q: s.csi_status_report(mode[0])),
    b"q": CSICommand(1, 0, lambda s, mode, q: s.csi_set_keyboard_leds(mode[0])),
    b"r": CSICommand(2, 0, lambda s, t_b, q: s.csi_set_scroll(t_b[0], t_b[1])),
    b"s": CSICommand(0, 0, lambda s, none, q: s.save_cursor()),
    b"u": CSICommand(0, 0, lambda s, none, q: s.restore_cursor()),
    b"`": CSIAlias("alias", b"G"),
}

CHARSET_DEFAULT: Literal[1] = 1  # type annotated exclusively for buggy IDE
CHARSET_UTF8: Literal[2] = 2


@dataclass(eq=True, order=False)
class TermModes:
    """Terminal mode flags (ECMA-48 and DEC private modes) tracked for a :class:`TermCanvas`."""

    # ECMA-48
    display_ctrl: bool = False
    insert: bool = False
    lfnl: bool = False

    # DEC private modes
    keys_decckm: bool = False
    reverse_video: bool = False
    constrain_scrolling: bool = False
    autowrap: bool = True
    visible_cursor: bool = True
    bracketed_paste: bool = False
    synchronized_output: bool = False

    # charset stuff
    main_charset: Literal[1, 2] = CHARSET_DEFAULT

    def reset(self) -> None:
        """Reset every mode flag to its terminal-startup default."""
        # ECMA-48
        self.display_ctrl = False
        self.insert = False
        self.lfnl = False

        # DEC private modes
        self.keys_decckm = False
        self.reverse_video = False
        self.constrain_scrolling = False
        self.autowrap = True
        self.visible_cursor = True

        # charset stuff
        self.main_charset = CHARSET_DEFAULT


class TermCharset:
    """Tracks the terminal's G0/G1 character set slots and which one is currently active."""

    __slots__ = ("_g", "_sgr_mapping", "active", "current")

    MAPPING: typing.ClassVar[dict[str, str | None]] = {
        "default": None,
        "vt100": "0",
        "ibmpc": "U",
        "user": None,
    }

    def __init__(self) -> None:
        """Initialize with the G0/G1 slots at their default charsets, G0 active."""
        self._g = [
            "default",
            "vt100",
        ]

        self._sgr_mapping = False

        # prepare defaults
        self.active = 0
        self.current: Literal["0", "U"] | None = None

        self.activate(0)

    def define(self, g: int, charset: str) -> None:
        """Redefine G'g' with new mapping."""
        self._g[g] = charset
        self.activate(g=self.active)

    def activate(self, g: int) -> None:
        """Activate the given charset slot."""
        self.active = g
        self.current = self.MAPPING.get(self._g[g], None)  # type: ignore[assignment]

    def set_sgr_ibmpc(self) -> None:
        """Set graphics rendition mapping to IBM PC CP437."""
        self._sgr_mapping = True

    def reset_sgr_ibmpc(self) -> None:
        """Reset graphics rendition mapping to IBM PC CP437."""
        self._sgr_mapping = False
        self.activate(g=self.active)

    def apply_mapping(self, char: bytes) -> bytes:
        """Map *char* through the active IBM PC/DEC special character set, tracking which slot the result came from."""
        if self._sgr_mapping or self._g[self.active] == "ibmpc":
            if (dec_pos := DEC_SPECIAL_CHARS.find(char.decode("cp437"))) >= 0:
                self.current = "0"
                return ALT_DEC_SPECIAL_CHARS[dec_pos].encode("cp437")

            self.current = "U"
            return char

        return char


class TermCanvas(Canvas):
    """Canvas holding a :class:`Terminal` widget's screen and scrollback content."""

    cacheable = False

    def __init__(self, width: int, height: int, widget: Terminal) -> None:
        """Initialize an empty `width` by `height` canvas backing `widget`."""
        super().__init__()

        self.width, self.height = width, height
        self.widget = widget
        self.modes: TermModes = widget.term_modes
        self.has_focus = False

        self.scrollback_buffer: deque[list[tuple[AttrSpec | None, Literal["0", "U"] | None, bytes]]] = deque(
            maxlen=10000
        )
        self.scrolling_up = 0

        self.utf8_eat_bytes: int | None = None
        self.utf8_buffer = bytearray()
        self.escbuf = b""

        self.coords["cursor"] = (0, 0, None)

        self.term_cursor: tuple[int, int] = (0, 0)  # do not allow to shoot in the leg at `set_term_cursor`

        self.within_escape = False
        self.parsestate = 0

        self.attrspec: AttrSpec | None = None

        self.charset = TermCharset()

        self.saved_cursor: tuple[int, int] | None = None
        self.saved_attrs: tuple[AttrSpec | None, TermCharset] | None = None

        self.is_rotten_cursor = False

        self.scrollregion_start = 0
        self.scrollregion_end = self.height - 1

        self.tabstops: list[int] = []
        self.term: list[list[tuple[AttrSpec | None, Literal["0", "U"] | None, bytes]]] = []

        self.reset()

    def set_term_cursor(self, x: int | None = None, y: int | None = None) -> None:
        """Set terminal cursor to x/y and update canvas cursor.

        If one or both axes are omitted, use the values of the current position.
        """
        if x is None:
            x = self.term_cursor[0]
        if y is None:
            y = self.term_cursor[1]

        self.term_cursor = self.constrain_coords(x, y)

        if self.has_focus and self.modes.visible_cursor and self.scrolling_up < self.height - y:
            self.cursor = (x, y + self.scrolling_up)
        else:
            self.cursor = None

    def reset_scroll(self) -> None:
        """Reset scrolling region to full terminal size."""
        self.scrollregion_start = 0
        self.scrollregion_end = self.height - 1

    def scroll_buffer(self, up: bool = True, reset: bool = False, lines: int | None = None) -> None:
        """Scroll the scrolling buffer up (up=True) or down (up=False) the given amount of lines.

        Scrolls half the screen height when *lines* is omitted.

        If just 'reset' is True, set the scrollbuffer view to the current terminal content.
        """
        if reset:
            self.scrolling_up = 0
            self.set_term_cursor()
            return

        if lines is None:
            lines = self.height // 2

        if not up:
            lines = -lines

        maxscroll = len(self.scrollback_buffer)
        self.scrolling_up += lines

        if self.scrolling_up > maxscroll:
            self.scrolling_up = maxscroll
        elif self.scrolling_up < 0:
            self.scrolling_up = 0

        self.set_term_cursor()

    def reset(self) -> None:
        """Reset the terminal."""
        self.escbuf = b""
        self.within_escape = False
        self.parsestate = 0

        self.attrspec = None
        self.charset = TermCharset()

        self.saved_cursor = None
        self.saved_attrs = None

        self.is_rotten_cursor = False

        self.reset_scroll()

        self.init_tabstops()

        # terminal modes
        self.modes.reset()

        # initialize self.term
        self.clear()

    def init_tabstops(self, extend: bool = False) -> None:
        """Set every column to a tabstop, or, when *extend* is set, grow the existing tabstops to the new width."""
        tablen, mod = divmod(self.width, 8)
        if mod > 0:
            tablen += 1

        if extend:
            while len(self.tabstops) < tablen:
                self.tabstops.append(1 << 0)
        else:
            self.tabstops = [1 << 0] * tablen

    def set_tabstop(self, x: int | None = None, remove: bool = False, clear: bool = False) -> None:
        """Set, remove, or (with *clear*) remove every tabstop, at column *x* or the cursor's column by default."""
        if clear:
            for tab in range(len(self.tabstops)):
                self.tabstops[tab] = 0
            return

        if x is None:
            x = self.term_cursor[0]

        div, mod = divmod(x, 8)
        if remove:
            self.tabstops[div] &= ~(1 << mod)
        else:
            self.tabstops[div] |= 1 << mod

    def is_tabstop(self, x: int | None = None) -> bool:
        """Return whether column *x* (the cursor's column by default) is a tabstop."""
        if x is None:
            x = self.term_cursor[0]

        div, mod = divmod(x, 8)
        return (self.tabstops[div] & (1 << mod)) > 0

    def empty_line(self, char: bytes = b" ") -> list[tuple[AttrSpec | None, Literal["0", "U"] | None, bytes]]:
        """Return a full row of empty characters, one per column, filled with *char*."""
        return [self.empty_char(char)] * self.width

    def empty_char(self, char: bytes = b" ") -> tuple[AttrSpec | None, Literal["0", "U"] | None, bytes]:
        """Return one empty terminal cell holding *char*, with the canvas's current attributes and charset."""
        return (self.attrspec, self.charset.current, char)

    def addstr(self, data: Iterable[int]) -> None:
        """Feed a sequence of input bytes through the terminal parser, one byte at a time."""
        if self.width <= 0 or self.height <= 0:
            # not displayable, do nothing!
            return

        for byte in data:
            self.addbyte(byte)

    def resize(self, width: int, height: int) -> None:
        """Resize the terminal to the given width and height."""
        x, y = self.term_cursor

        if width > self.width:
            # grow
            for y in range(self.height):
                self.term[y] += [self.empty_char()] * (width - self.width)
        elif width < self.width:
            # shrink
            for y in range(self.height):
                self.term[y] = self.term[y][:width]

        self.width = width

        if height > self.height:
            # grow
            for _y in range(self.height, height):
                try:
                    last_line = self.scrollback_buffer.pop()
                except IndexError:
                    # nothing in scrollback buffer, append an empty line
                    self.term.append(self.empty_line())
                    self.scrollregion_end += 1
                    continue

                # adjust x axis of scrollback buffer to the current width
                if (padding := self.width - len(last_line)) > 0:
                    last_line += [self.empty_char()] * padding
                else:
                    last_line = last_line[: self.width]

                self.term.insert(0, last_line)
        elif height < self.height:
            # shrink
            for _y in range(height, self.height):
                self.scrollback_buffer.append(self.term.pop(0))

        self.height = height

        self.reset_scroll()

        x, y = self.constrain_coords(x, y)
        self.set_term_cursor(x, y)

        # extend tabs
        self.init_tabstops(extend=True)

    def set_g01(self, char: bytes, mod: bytes) -> None:
        """Set G0 or G1 according to 'char' and modifier 'mod'."""
        if self.modes.main_charset != CHARSET_DEFAULT:
            return

        if mod == b"(":
            g = 0
        else:
            g = 1

        if char == b"0":
            cset = "vt100"
        elif char == b"U":
            cset = "ibmpc"
        elif char == b"K":
            cset = "user"
        else:
            cset = "default"

        self.charset.define(g, cset)

    def parse_csi(self, char: bytes) -> None:
        """Parse ECMA-48 CSI (Control Sequence Introducer) sequences."""
        qmark = self.escbuf.startswith(b"?")

        escbuf = []
        for arg in self.escbuf[1 if qmark else 0 :].split(b";"):
            try:
                num = int(arg)
            except ValueError:
                num = None

            escbuf.append(num)

        if (cmd_ := CSI_COMMANDS[char]) is not None:
            if isinstance(cmd_, CSIAlias):
                csi_cmd: CSICommand = CSI_COMMANDS[cmd_.alias]  # type: ignore[assignment]
            elif isinstance(cmd_, CSICommand):
                csi_cmd = cmd_
            elif cmd_[0] == "alias":  # fallback, hard deprecated
                csi_cmd = CSI_COMMANDS[CSIAlias(*cmd_).alias]
            else:
                csi_cmd = CSICommand(*cmd_)  # fallback, hard deprecated

            number_of_args, default_value, cmd = csi_cmd
            while len(escbuf) < number_of_args:
                escbuf.append(default_value)
            for i in range(len(escbuf)):
                if escbuf[i] is None or escbuf[i] == 0:
                    escbuf[i] = default_value

            with suppress(ValueError):
                cmd(self, typing.cast("list[int]", escbuf), qmark)
                # ignore commands that don't match the
                # unpacked tuples in CSI_COMMANDS.

    def parse_noncsi(self, char: bytes, mod: bytes = b"") -> None:
        """Parse escape sequences which are not CSI."""
        if mod == b"#" and char == b"8":
            self.decaln()
        elif mod == b"%":  # select main character set
            if char == b"@":
                self.modes.main_charset = CHARSET_DEFAULT
            elif char in b"G8":
                # 8 is obsolete and only for backwards compatibility
                self.modes.main_charset = CHARSET_UTF8
        elif mod in {b"(", b")"}:  # define G0/G1
            self.set_g01(char, mod)
        elif char == b"M":  # reverse line feed
            self.linefeed(reverse=True)
        elif char == b"D":  # line feed
            self.linefeed()
        elif char == b"c":  # reset terminal
            self.reset()
        elif char == b"E":  # newline
            self.newline()
        elif char == b"H":  # set tabstop
            self.set_tabstop()
        elif char == b"Z":  # DECID
            self.widget.respond(f"{ESC}[?6c")
        elif char == b"7":  # save current state
            self.save_cursor(with_attrs=True)
        elif char == b"8":  # restore current state
            self.restore_cursor(with_attrs=True)

    def parse_osc(self, buf: bytes) -> None:
        """Parse operating system command.

        OSC-title-prefix recognition (``;``/``0;``/``2;``) is delegated to the shared
        :func:`urwid.ansi_parser.resolve_osc_title`, so the rule is maintained in exactly one place.

        :param buf: the OSC command body, excluding the ``ESC ]`` framing and terminator.
        """
        title = resolve_osc_title(buf.decode())
        if title is not None:
            self.widget.set_title(title)

    def parse_escape(self, char: bytes) -> None:
        """Feed *char* through the escape-sequence state machine, dispatching to CSI/OSC/non-CSI parsing as needed."""
        if self.parsestate == 1:
            # within CSI
            if char in CSI_COMMANDS:
                self.parse_csi(char)
                self.parsestate = 0
            elif char in b"0123456789;" or (not self.escbuf and char == b"?"):
                self.escbuf += char
                return
        elif self.parsestate == 0 and char == b"]":
            # start of OSC
            self.escbuf = b""
            self.parsestate = 2
            return
        elif self.parsestate == 2 and self.escbuf[-1:] == ESC_B and char != b"\\":
            # ECMA-48 allows no ESC inside a command string other than in ST, so any other ESC ends the string
            # unapplied and starts a new escape sequence
            self.leave_escape()
            self.within_escape = True
            self.process_char(char)
            return
        elif self.parsestate == 2 and char == b"\a":
            # end of OSC
            self.parse_osc(self.escbuf.lstrip(b"0"))
        elif self.parsestate == 2 and self.escbuf[-1:] + char == f"{ESC}\\".encode("iso8859-1"):
            # end of OSC
            self.parse_osc(self.escbuf[:-1].lstrip(b"0"))
        elif self.parsestate == 2 and self.escbuf.startswith(b"P") and len(self.escbuf) == 8:
            # set palette (ESC]Pnrrggbb)
            pass
        elif self.parsestate == 2 and not self.escbuf and char == b"R":
            # reset palette
            pass
        elif self.parsestate == 2:
            self.escbuf += char
            return
        elif self.parsestate == 0 and char == b"[":
            # start of CSI
            self.escbuf = b""
            self.parsestate = 1
            return
        elif self.parsestate == 0 and char in {b"%", b"#", b"(", b")"}:
            # non-CSI sequence
            self.escbuf = char
            self.parsestate = 3
            return
        elif self.parsestate == 3:
            self.parse_noncsi(char, self.escbuf)
        elif char in {b"c", b"D", b"E", b"H", b"M", b"Z", b"7", b"8", b">", b"="}:
            self.parse_noncsi(char)

        self.leave_escape()

    def leave_escape(self) -> None:
        """Reset the escape-sequence parser state, discarding any sequence being read."""
        self.within_escape = False
        self.parsestate = 0
        self.escbuf = b""

    def get_utf8_len(self, bytenum: int) -> int:
        """Process startbyte and return the number of bytes following it to get a valid UTF-8 multibyte sequence.

        :param bytenum: an integer ordinal
        """
        length = 0

        while bytenum & 0x40:
            bytenum <<= 1
            length += 1

        return length

    def addbyte(self, byte: int) -> None:
        """Parse main charset and add the processed byte(s) to the terminal state machine.

        :param byte: an integer ordinal
        """
        if self.modes.main_charset == CHARSET_UTF8 or util.get_encoding() == "utf8":
            if byte >= 0xC0:
                # start multibyte sequence
                self.utf8_eat_bytes = self.get_utf8_len(byte)
                self.utf8_buffer = bytearray([byte])
                return
            if 0x80 <= byte < 0xC0 and self.utf8_eat_bytes is not None:
                if self.utf8_eat_bytes > 1:
                    # continue multibyte sequence
                    self.utf8_eat_bytes -= 1
                    self.utf8_buffer.append(byte)
                    return

                # end multibyte sequence
                self.utf8_eat_bytes = None

                if sequence := (self.utf8_buffer + bytes([byte])).decode("utf-8", "ignore"):
                    char = sequence.encode(util.get_encoding(), "replace")

                else:
                    # invalid multibyte sequence, stop processing
                    return
            else:
                self.utf8_eat_bytes = None
                char = bytes([byte])
        else:
            char = bytes([byte])

        self.process_char(char)

    def process_char(self, char: int | bytes) -> None:
        """
        Process a single character (single- and multi-byte).

        :param char: a byte string
        """
        x, y = self.term_cursor

        if isinstance(char, int):
            char = char.to_bytes(1, "little")

        dc = self.modes.display_ctrl

        if char == ESC_B and self.parsestate != 2:  # escape
            self.within_escape = True
        elif not dc and char == b"\r":  # carriage return CR
            self.carriage_return()
        elif not dc and char == b"\x0f":  # activate G0
            self.charset.activate(0)
        elif not dc and char == b"\x0e":  # activate G1
            self.charset.activate(1)
        elif not dc and char in b"\n\v\f":  # line feed LF/VT/FF
            self.linefeed()
            if self.modes.lfnl:
                self.carriage_return()
        elif not dc and char == b"\t":  # char tab
            self.tab()
        elif not dc and char == b"\b":  # backspace BS
            if x > 0:
                self.set_term_cursor(x - 1, y)
        elif not dc and char == b"\a" and self.parsestate != 2:  # BEL
            # we need to check if we're in parsestate 2, as an OSC can be
            # terminated by the BEL character!
            self.widget.beep()
        elif not dc and char in b"\x18\x1a":  # CAN/SUB
            self.leave_escape()
        elif not dc and char in b"\x00\x7f":  # NUL/DEL
            pass  # this is ignored
        elif self.within_escape:
            self.parse_escape(char)
        elif not dc and char == b"\x9b":  # CSI (equivalent to "ESC [")
            self.within_escape = True
            self.escbuf = b""
            self.parsestate = 1
        else:
            self.push_cursor(char)

    def set_char(self, char: bytes, x: int | None = None, y: int | None = None) -> None:
        """Set character of either the current cursor position or a position given by 'x' and/or 'y' to 'char'."""
        if x is None:
            x = self.term_cursor[0]
        if y is None:
            y = self.term_cursor[1]

        x, y = self.constrain_coords(x, y)
        self.term[y][x] = (self.attrspec, self.charset.current, char)

    def constrain_coords(self, x: int, y: int, ignore_scrolling: bool = False) -> tuple[int, int]:
        """Check if x/y are within the terminal and return the corrected version.

        If 'ignore_scrolling' is set, constrain within the full size of the screen and not within scrolling region.
        """
        if x >= self.width:
            x = self.width - 1
        elif x < 0:
            x = 0

        if self.modes.constrain_scrolling and not ignore_scrolling:
            if y > self.scrollregion_end:
                y = self.scrollregion_end
            elif y < self.scrollregion_start:
                y = self.scrollregion_start
        else:  # noqa: PLR5501  # pylint: disable=else-if-used  # readability
            if y >= self.height:
                y = self.height - 1
            elif y < 0:
                y = 0

        return x, y

    def linefeed(self, reverse: bool = False) -> None:
        """Move the cursor down (or up if reverse is True) one line but don't reset horizontal position."""
        x, y = self.term_cursor

        if reverse:
            if y <= 0 < self.scrollregion_start:
                pass
            elif y == self.scrollregion_start:
                self.scroll(reverse=True)
            else:
                y -= 1
        else:  # noqa: PLR5501  # pylint: disable=else-if-used  # readability
            if y >= self.height - 1 > self.scrollregion_end:
                pass
            elif y == self.scrollregion_end:
                self.scroll()
            else:
                y += 1

        self.set_term_cursor(x, y)

    def carriage_return(self) -> None:
        """Move the cursor to column 0 of the current line."""
        self.set_term_cursor(0, self.term_cursor[1])

    def newline(self) -> None:
        """Do a carriage return followed by a line feed."""
        self.carriage_return()
        self.linefeed()

    def move_cursor(
        self,
        x: int,
        y: int,
        relative_x: bool = False,
        relative_y: bool = False,
        relative: bool = False,
    ) -> None:
        """Move cursor to position x/y while constraining terminal sizes.

        If 'relative' is True, x/y is relative to the current cursor position.
        'relative_x' and 'relative_y' is the same but just with the corresponding axis.
        """
        if relative:
            relative_y = relative_x = True

        if relative_x:
            x += self.term_cursor[0]

        if relative_y:
            y += self.term_cursor[1]
        elif self.modes.constrain_scrolling:
            y += self.scrollregion_start

        self.set_term_cursor(x, y)

    def push_char(self, char: bytes | None, x: int, y: int) -> None:
        """Push one character to current position and advance cursor to x/y."""
        if char is not None:
            char = self.charset.apply_mapping(char)
            if self.modes.insert:
                self.insert_chars(char=char)
            else:
                self.set_char(char)

        self.set_term_cursor(x, y)

    def push_cursor(self, char: bytes | None = None) -> None:
        """Move cursor one character forward wrapping lines as needed.

        If 'char' is given, put the character into the former position.
        """
        x, y = self.term_cursor

        if self.modes.autowrap:
            if x + 1 >= self.width and not self.is_rotten_cursor:
                # "rotten cursor" - this is when the cursor gets to the rightmost
                # position of the screen, the cursor position remains the same but
                # one last set_char() is allowed for that piece of sh^H^H"border".
                self.is_rotten_cursor = True
                self.push_char(char, x, y)
            else:
                x += 1

                if x >= self.width and self.is_rotten_cursor:
                    if y >= self.scrollregion_end:
                        self.scroll()
                    else:
                        y += 1

                    x = 1

                    self.set_term_cursor(0, y)

                self.push_char(char, x, y)

                self.is_rotten_cursor = False
        else:
            if x + 1 < self.width:
                x += 1

            self.is_rotten_cursor = False
            self.push_char(char, x, y)

    def save_cursor(self, with_attrs: bool = False) -> None:
        """Save the current cursor position, and its attributes/charset when *with_attrs* is set."""
        self.saved_cursor = self.term_cursor
        if with_attrs:
            self.saved_attrs = (copy.copy(self.attrspec), copy.copy(self.charset))

    def restore_cursor(self, with_attrs: bool = False) -> None:
        """Restore the cursor position saved by :meth:`save_cursor`, and its attributes/charset when *with_attrs*."""
        if self.saved_cursor is None:
            return

        x, y = self.saved_cursor
        self.set_term_cursor(x, y)

        if with_attrs and self.saved_attrs is not None:
            self.attrspec, self.charset = (copy.copy(self.saved_attrs[0]), copy.copy(self.saved_attrs[1]))

    def tab(self, tabstop: int = 8) -> None:
        """Move cursor to the next 'tabstop' filling everything in between with spaces."""
        x, y = self.term_cursor

        while x < self.width - 1:
            self.set_char(b" ")
            x += 1

            if self.is_tabstop(x):
                break

        self.is_rotten_cursor = False
        self.set_term_cursor(x, y)

    def scroll(self, reverse: bool = False) -> None:
        """Append a new line at the bottom and put the topmost line into the scrollback buffer.

        If reverse is True, do exactly the opposite, but don't save into scrollback buffer.
        """
        if reverse:
            self.term.pop(self.scrollregion_end)
            self.term.insert(self.scrollregion_start, self.empty_line())
        else:
            killed = self.term.pop(self.scrollregion_start)
            self.scrollback_buffer.append(killed)
            self.term.insert(self.scrollregion_end, self.empty_line())

    def decaln(self) -> None:
        """DEC screen alignment test: Fill screen with E's."""
        for row in range(self.height):
            self.term[row] = self.empty_line(b"E")

    def blank_line(self, row: int) -> None:
        """Blank a single line at the specified row, without modifying other lines."""
        self.term[row] = self.empty_line()

    def insert_chars(
        self,
        position: tuple[int, int] | None = None,
        chars: int = 1,
        char: bytes | None = None,
    ) -> None:
        """Insert 'chars' number of empty characters (or those given by 'char') before 'position'.

        Defaults to the current position if 'position' is not specified,
        pushing subsequent characters of the line to the right without wrapping.
        """
        if position is None:
            position = self.term_cursor

        if chars == 0:
            chars = 1

        if char is None:
            char_spec = self.empty_char()
        else:
            char_spec = (self.attrspec, self.charset.current, char)

        x, y = position

        while chars > 0:
            self.term[y].insert(x, char_spec)
            self.term[y].pop()
            chars -= 1

    def remove_chars(self, position: tuple[int, int] | None = None, chars: int = 1) -> None:
        """Remove 'chars' number of empty characters from 'position'.

        Defaults to the current position if 'position' is not specified,
        pulling subsequent characters of the line to the left without joining any subsequent lines.
        """
        if position is None:
            position = self.term_cursor

        if chars == 0:
            chars = 1

        x, y = position

        while chars > 0:
            self.term[y].pop(x)
            self.term[y].append(self.empty_char())
            chars -= 1

    def insert_lines(self, row: int | None = None, lines: int = 1) -> None:
        """Insert 'lines' of empty lines after the specified row, pushing all subsequent lines to the bottom.

        If no 'row' is specified, the current row is used.
        """
        if row is None:
            row = self.term_cursor[1]
        else:
            row = self.scrollregion_start

        if lines == 0:
            lines = 1

        while lines > 0:
            self.term.insert(row, self.empty_line())
            self.term.pop(self.scrollregion_end)
            lines -= 1

    def remove_lines(self, row: int | None = None, lines: int = 1) -> None:
        """Remove 'lines' number of lines at the specified row, pulling all subsequent lines to the top.

        If no 'row' is specified, the current row is used.
        """
        if row is None:
            row = self.term_cursor[1]
        else:
            row = self.scrollregion_start

        if lines == 0:
            lines = 1

        while lines > 0:
            self.term.pop(row)
            self.term.insert(self.scrollregion_end, self.empty_line())
            lines -= 1

    def erase(
        self,
        start: tuple[int, int] | tuple[int, int, bool],
        end: tuple[int, int] | tuple[int, int, bool],
    ) -> None:
        """Erase a region of the terminal.

        The 'start' tuple (x, y) defines the starting position of the erase, while end (x, y) the last position.

        For example if the terminal size is 4x3, start=(1, 1) and end=(1, 2)
        would erase the following region:

        ....
        .XXX
        XX..
        """
        sx, sy = self.constrain_coords(*start)
        ex, ey = self.constrain_coords(*end)

        # within a single row
        if sy == ey:
            for x in range(sx, ex + 1):
                self.term[sy][x] = self.empty_char()
            return

        # spans multiple rows
        y = sy
        while y <= ey:
            if y == sy:
                for x in range(sx, self.width):
                    self.term[y][x] = self.empty_char()
            elif y == ey:
                for x in range(ex + 1):
                    self.term[y][x] = self.empty_char()
            else:
                self.blank_line(y)

            y += 1

    def csi_set_attr(self, attrs: Sequence[int]) -> None:
        """Set graphics rendition.

        The colour/attribute-flag computation itself is delegated to the shared, platform-independent
        :func:`urwid.ansi_parser.sgi_params_to_attrspec`; SGR 10/11/12 toggle vterm's own charset/display-control
        state rather than colour/attributes, so that side effect is applied separately, here, since
        ``sgi_params_to_attrspec`` deliberately ignores it (see its docstring).

        :param attrs: the SGR numeric parameters to apply, in encounter order.
        """
        for attr in attrs:
            if attr == 10:
                self.charset.reset_sgr_ibmpc()
                self.modes.display_ctrl = False
            elif attr in (11, 12):
                self.charset.set_sgr_ibmpc()
                self.modes.display_ctrl = True

        attrspec = sgi_params_to_attrspec(attrs, self.attrspec)

        if self.modes.reverse_video:
            self.attrspec = self.reverse_attrspec(attrspec)
        else:
            self.attrspec = attrspec

    def reverse_attrspec(self, attrspec: AttrSpec | None, undo: bool = False) -> AttrSpec:
        """Put standout mode to the 'attrspec' given and remove it if 'undo' is True."""
        if attrspec is None:
            attrspec = AttrSpec("default", "default")
        attrs = [fg.strip() for fg in attrspec.foreground.split(",")]
        if "standout" in attrs and undo:
            attrs.remove("standout")
            attrspec = attrspec.copy_modified(fg=",".join(attrs))
        elif "standout" not in attrs and not undo:
            attrs.append("standout")
            attrspec = attrspec.copy_modified(fg=",".join(attrs))
        return attrspec

    def reverse_video(self, undo: bool = False) -> None:
        """Reverse video/scanmode (DECSCNM) by swapping fg and bg colors."""
        for y in range(self.height):
            for x in range(self.width):
                char = self.term[y][x]
                attrs = self.reverse_attrspec(char[0], undo=undo)
                self.term[y][x] = (attrs, *char[1:])

    def set_mode(
        self,
        mode: int,
        flag: bool,
        qmark: bool,
        reset: bool,
    ) -> None:
        """Set a single mode, as a helper for csi_set_modes."""
        if qmark:
            # DEC private mode
            if mode == 1:
                # cursor keys send an ESC O prefix, rather than ESC [
                self.modes.keys_decckm = flag
            elif mode == 3:
                # deccolm just clears the screen
                self.clear()
            elif mode == 5:
                if self.modes.reverse_video != flag:
                    self.reverse_video(undo=not flag)
                self.modes.reverse_video = flag
            elif mode == 6:
                self.modes.constrain_scrolling = flag
                self.set_term_cursor(0, 0)
            elif mode == 7:
                self.modes.autowrap = flag
            elif mode == 25:
                self.modes.visible_cursor = flag
                self.set_term_cursor()
            elif mode == 2004:
                self.modes.bracketed_paste = flag
            elif mode == 2026:
                # Synchronized output (https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036):
                # the child program is asking that its writes not be displayed until it closes the
                # block with CSI ?2026l.
                # Terminal.feed() reads that flag to keep draining the pty instead of handing a mid-update canvas
                # back to the next render() pass.
                self.modes.synchronized_output = flag
        else:  # noqa: PLR5501  # pylint: disable=else-if-used  # readability
            # ECMA-48
            if mode == 3:
                self.modes.display_ctrl = flag
            elif mode == 4:
                self.modes.insert = flag
            elif mode == 20:
                self.modes.lfnl = flag

    def csi_set_modes(
        self,
        modes: Iterable[int],
        qmark: bool,
        reset: bool = False,
    ) -> None:
        """Set (DECSET/ECMA-48) or reset modes (DECRST/ECMA-48) if reset is True."""
        flag = not reset

        for mode in modes:
            self.set_mode(mode, flag, qmark, reset)

    def csi_set_scroll(self, top: int = 0, bottom: int = 0) -> None:
        """Set scrolling region, 'top' is the line number of first line in the scrolling region.

        'bottom' is the line number of bottom line. If both are set to 0, the whole screen will be used (default).
        """
        if not top:
            top = 1
        if not bottom:
            bottom = self.height

        if top < bottom <= self.height:
            self.scrollregion_start = self.constrain_coords(0, top - 1, ignore_scrolling=True)[1]
            self.scrollregion_end = self.constrain_coords(0, bottom - 1, ignore_scrolling=True)[1]

            self.set_term_cursor(0, 0)

    def csi_clear_tabstop(self, mode: int = 0) -> None:
        """Clear tabstop at current position or if 'mode' is 3, delete all tabstops."""
        if mode == 0:
            self.set_tabstop(remove=True)
        elif mode == 3:
            self.set_tabstop(clear=True)

    def csi_get_device_attributes(self, qmark: bool) -> None:
        """Report device attributes (what are you?). In our case, we'll report ourself as a VT102 terminal."""
        if not qmark:
            self.widget.respond(f"{ESC}[?6c")

    def csi_status_report(self, mode: int) -> None:
        """Report various information about the terminal status.

        Information is queried by 'mode', where possible values are:
            5 -> device status report
            6 -> cursor position report
        """
        if mode == 5:
            # terminal OK
            self.widget.respond(f"{ESC}[0n")
        elif mode == 6:
            x, y = self.term_cursor
            self.widget.respond(ESC + f"[{y + 1:d};{x + 1:d}R")

    def csi_erase_line(self, mode: int) -> None:
        """Erase current line.

        Modes are:

            0 -> erase from cursor to end of line.
            1 -> erase from start of line to cursor.
            2 -> erase whole line.
        """
        x, y = self.term_cursor

        if mode == 0:
            self.erase(self.term_cursor, (self.width - 1, y))
        elif mode == 1:
            self.erase((0, y), (x, y))
        elif mode == 2:
            self.blank_line(y)

    def csi_erase_display(self, mode: int) -> None:
        """Erase display.

        Modes are:

            0 -> erase from cursor to end of display.
            1 -> erase from start to cursor.
            2 -> erase the whole display.
        """
        if mode == 0:
            self.erase(self.term_cursor, (self.width - 1, self.height - 1))
        if mode == 1:
            self.erase((0, 0), (self.term_cursor[0] - 1, self.term_cursor[1]))
        elif mode == 2:
            self.clear(cursor=self.term_cursor)

    def csi_set_keyboard_leds(self, mode: int = 0) -> None:
        """Set keyboard LEDs.

        This currently just emits a signal, so it can be processed by another widget or the main application.

        The mode-to-state-name mapping is delegated to the shared :func:`urwid.ansi_parser.led_state`, so it is
        maintained in exactly one place.

        :param mode: one of ``0`` (clear all LEDs), ``1`` (set scroll lock LED), ``2`` (set num lock LED) or
            ``3`` (set caps lock LED).
        """
        state = led_state(mode)

        if state is not None:
            self.widget.leds(state)  # type: ignore[arg-type]

    def clear(self, cursor: tuple[int, int] | None = None) -> None:
        """Clear the whole terminal screen and reset the cursor position to (0, 0) or to 'cursor'."""
        self.term = [self.empty_line() for _ in range(self.height)]

        if cursor is None:
            self.set_term_cursor(0, 0)
        else:
            self.set_term_cursor(*cursor)

    def cols(self) -> int:
        """Return the canvas's number of columns."""
        return self.width

    def rows(self) -> int:
        """Return the canvas's number of rows."""
        return self.height

    def content(  # type: ignore[override]
        self,
        trim_left: int = 0,
        trim_top: int = 0,
        cols: int = 0,
        rows: int = 0,
        attr: Mapping[Hashable, AttrSpec | str | None] | None = None,
    ) -> Iterator[list[tuple[AttrSpec | None, Literal["0", "U"] | None, bytes]]]:
        """Canvas content.

        All parameters are ignored.
        """
        if self.scrolling_up == 0:
            yield from self.term
        else:
            viewport_range = slice(-(self.height + self.scrolling_up), -self.scrolling_up)
            yield from (*self.scrollback_buffer, *self.term)[viewport_range]

    def content_delta(  # type: ignore[override]
        self,
        other: Canvas,
    ) -> list[int] | Iterator[list[tuple[AttrSpec | None, Literal["0", "U"] | None, bytes]]]:
        """
        Return the differences between other and this canvas.

        .. deprecated:: 4.0.3
            Not used by the code base; there is no replacement. It will be removed in a future release.
        """
        warnings.warn(
            "content_delta is not used by code base and will be removed in the future releases",
            DeprecationWarning,
            stacklevel=2,
        )
        if other is self:
            return [self.cols()] * self.rows()
        return self.content()


class Terminal(Widget):
    """Widget that runs a child process (or callable) and renders its terminal output."""

    _selectable = True
    _sizing = frozenset([Sizing.BOX])

    signals: typing.ClassVar[list[str]] = ["closed", "beep", "leds", "title", "resize"]

    # Upper bound on the bytes drained in one feed() call while term_modes.synchronized_output is set,
    # so a child that opens a synchronized-output block (CSI ?2026h) and never closes it --
    # deliberately or by being slow -- cannot stall feed() from ever returning.
    _SYNCHRONIZED_OUTPUT_DRAIN_LIMIT: typing.ClassVar[int] = 1 << 16

    def __init__(
        self,
        command: tuple[str | bytes | os.PathLike[str] | os.PathLike[bytes], ...]
        | list[str | bytes | os.PathLike[str] | os.PathLike[bytes]]
        | Callable[[], typing.Any]
        | None,
        env: Mapping[str, str] | Iterable[tuple[str, str]] | None = None,
        main_loop: event_loop.EventLoop | None = None,
        escape_sequence: str | None = None,
        encoding: str = "utf-8",
    ):
        """Initialize a terminal emulator widget.

        ``command`` is the command to execute inside the terminal,
        provided as a list of the command followed by its arguments.
        If 'command' is None, the command is the current user's shell.
        You can also provide a callable instead of a command, which will be executed in the subprocess.

        ``env`` can be used to pass custom environment variables. If omitted,
        os.environ is used.

        ``main_loop`` should be provided, because the canvas state machine needs
        to act on input from the PTY master device. This object must have
        watch_file and remove_watch_file methods.

        ``escape_sequence`` is the urwid key symbol which should be used to break
        out of the terminal widget. If it's not specified, ``ctrl a`` is used.

        ``encoding`` specifies the encoding that is being used when local
        keypresses in Unicode are encoded into raw bytes. UTF-8 is used by default.
        Set this to the encoding of your terminal if you need to transmit
        characters to the spawned process in non-UTF8 encoding.
        Applies to Python 3.x only.

        .. note::

            If you notice your Terminal instance is not printing unicode glyphs
            correctly, make sure the global encoding for urwid is set to
            ``utf8`` with ``urwid.set_encoding("utf8")``. See
            :ref:`text-encodings` for more details.
        """
        super().__init__()

        self.escape_sequence: str = escape_sequence or "ctrl a"

        self.env = dict(env or os.environ)

        self.command = command or [self.env.get("SHELL", "/bin/sh")]

        self.encoding = encoding

        self.keygrab = False
        self.last_key: str | None = None

        self.response_buffer: list[str] = []

        self.term_modes = TermModes()

        if main_loop is not None:
            self.main_loop = main_loop
        else:
            self.main_loop = event_loop.SelectEventLoop()

        self.master: int | None = None
        self.pid: int | None = None

        self.width: int | None = None
        self.height: int | None = None
        self.term: TermCanvas | None = None
        self.has_focus = False
        self.old_tios: tuple[int, int, int, int, int] | None = None
        self.terminated = False

    def get_cursor_coords(self, size: tuple[int, int]) -> tuple[int, int] | None:
        """Return the cursor coordinates for this terminal."""
        if self.term is None:
            return None

        # temporarily set width/height to figure out the new cursor position
        # given the provided width/height
        orig_width, orig_height = self.term.width, self.term.height

        self.term.width = size[0]
        self.term.height = size[1]

        x, y = self.term.constrain_coords(
            self.term.term_cursor[0],
            self.term.term_cursor[1],
        )

        self.term.width, self.term.height = orig_width, orig_height

        return (x, y)

    def spawn(self) -> None:
        """Fork the child process running :attr:`command` and connect it to a new pty."""
        env = self.env
        env["TERM"] = "linux"

        self.pid, self.master = pty.fork()

        if self.pid == 0:
            if callable(self.command):
                try:
                    # noinspection PyBroadException
                    try:
                        self.command()
                    except BaseException:  # noqa: BLE001  # special case
                        sys.stderr.write(traceback.format_exc())
                        sys.stderr.flush()
                finally:
                    os._exit(0)
            else:
                os.execvpe(self.command[0], self.command, env)  # noqa: S606

        if self.main_loop is None:
            fcntl.fcntl(self.master, fcntl.F_SETFL, os.O_NONBLOCK)

        atexit.register(self.terminate)

    def terminate(self) -> None:
        """Stop watching the pty and kill the child process, escalating through signals until it exits."""
        if self.terminated:
            return

        self.terminated = True
        self.remove_watch()
        self.change_focus(False)

        if typing.cast("int", self.pid) > 0:
            self.set_termsize(0, 0)
            for sig in (signal.SIGHUP, signal.SIGCONT, signal.SIGINT, signal.SIGTERM, signal.SIGKILL):
                try:
                    os.kill(typing.cast("int", self.pid), sig)
                    pid, _status = os.waitpid(typing.cast("int", self.pid), os.WNOHANG)
                except OSError:
                    break

                if pid == 0:
                    break
                time.sleep(0.1)
            with suppress(OSError):
                os.waitpid(typing.cast("int", self.pid), 0)

            os.close(typing.cast("int", self.master))

    def beep(self) -> None:
        """Emit a "beep" signal for anything listening on this widget."""
        self._emit("beep")

    def leds(self, which: Literal["clear", "scroll_lock", "num_lock", "caps_lock"]) -> None:
        """Emit a "leds" signal asking the display to set keyboard LED state *which*."""
        self._emit("leds", which)

    def respond(self, string: str) -> None:
        """Respond to the underlying application with 'string'."""
        self.response_buffer.append(string)

    def flush_responses(self) -> None:
        """Write every buffered response to the pty and clear the buffer."""
        for string in self.response_buffer:
            os.write(typing.cast("int", self.master), string.encode("ascii"))
        self.response_buffer = []

    def set_termsize(self, width: int, height: int) -> None:
        """Tell the pty the terminal window is now *width* by *height* characters."""
        winsize = struct.pack("HHHH", height, width, 0, 0)
        fcntl.ioctl(typing.cast("int", self.master), termios.TIOCSWINSZ, winsize)

    def touch_term(self, width: int, height: int) -> None:
        """Spawn the child process if needed, then resize the pty and terminal canvas to *width* by *height*."""
        process_opened = False

        if self.pid is None:
            self.spawn()
            process_opened = True

        if self.width == width and self.height == height:
            return

        self.set_termsize(width, height)

        if not self.term:
            self.term = TermCanvas(width, height, self)
        else:
            self.term.resize(width, height)

        self.width = width
        self.height = height

        if process_opened:
            self.add_watch()

        self._emit("resize", (width, height))

    def set_title(self, title: str) -> None:
        """Emit a "title" signal with the terminal's new *title*."""
        self._emit("title", title)

    def change_focus(self, has_focus: bool) -> None:
        """Ignore SIGINT if this widget has focus."""
        if self.terminated:
            return

        self.has_focus = has_focus

        if self.term is not None:
            self.term.has_focus = has_focus
            self.term.set_term_cursor()

        if has_focus:
            self.old_tios = RealTerminal().tty_signal_keys()
            RealTerminal().tty_signal_keys(*(["undefined"] * 5))  # type: ignore[arg-type]
        elif self.old_tios:
            RealTerminal().tty_signal_keys(*self.old_tios)

    def render(self, size: tuple[int, int], focus: bool = False) -> TermCanvas:  # type: ignore[override]
        """Resize the terminal to *size*, read any pending child output, and return the terminal canvas."""
        if not self.terminated:
            self.change_focus(focus)

            width, height = size
            self.touch_term(width, height)

            if self.main_loop is None:
                self.feed()

        return self.term  # type: ignore[return-value]

    def add_watch(self) -> None:
        """Register the pty with the main loop, when there is one, so :meth:`feed` runs on new child output."""
        if self.main_loop is None:
            return
        self.main_loop.watch_file(typing.cast("int", self.master), self.feed)

    def remove_watch(self) -> None:
        """Unregister the pty from the main loop, when there is one."""
        if self.main_loop is None:
            return
        self.main_loop.remove_watch_file(self.master)

    def wait_and_feed(self, timeout: float = 1.0) -> None:
        """Block up to *timeout* seconds for pty output, then read and process whatever has arrived."""
        with selectors.DefaultSelector() as selector:
            selector.register(typing.cast("int", self.master), selectors.EVENT_READ)

            selector.select(timeout)

        self.feed()

    def feed(self) -> None:
        """Read pending output from the pty and feed it to the terminal, terminating the child on EOF."""
        data = EOF

        try:
            data = os.read(typing.cast("int", self.master), 4096)
        except OSError as e:
            if e.errno == errno.EIO:  # EIO, child terminated
                data = EOF
            elif e.errno == errno.EWOULDBLOCK:  # empty buffer
                return
            else:
                raise

        if data == EOF:
            self.terminate()
            self._emit("closed")
            return

        self.term.addstr(data)  # type: ignore[union-attr]

        # The child just opened a synchronized-output block (CSI ?2026h) and is still writing to it:
        # draining whatever it has already queued on the pty before returning keeps the next
        # render() from ever handing out a half-painted mid-block canvas.
        # A child that never closes the block (CSI ?2026l) still gets control back
        # once _SYNCHRONIZED_OUTPUT_DRAIN_LIMIT is hit
        # or the pty simply has nothing more buffered right now (EWOULDBLOCK).
        drained = 0
        while self.term_modes.synchronized_output and drained < self._SYNCHRONIZED_OUTPUT_DRAIN_LIMIT:
            try:
                more = os.read(typing.cast("int", self.master), 4096)
            except OSError as e:
                if e.errno == errno.EWOULDBLOCK:
                    break
                if e.errno == errno.EIO:
                    self.terminate()
                    self._emit("closed")
                    return
                raise

            if not more:
                break

            self.term.addstr(more)  # type: ignore[union-attr]
            drained += len(more)

        self.flush_responses()

    def keypress(self, size: tuple[int, int], key: str) -> str | None:  # type: ignore[override]
        """Translate *key* into the byte sequence the child process expects and write it to the pty."""
        if self.terminated:
            return key

        if key in {"begin paste", "end paste"}:
            if self.term_modes.bracketed_paste:
                pass  # passthrough bracketed paste sequences
            else:  # swallow bracketed paste sequences
                self.last_key = key
                return None

        if key == "window resize":
            width, height = size
            self.touch_term(width, height)
            return None

        if self.last_key == key == self.escape_sequence:
            # escape sequence pressed twice...
            self.last_key = key
            self.keygrab = True
            # ... so pass it to the terminal
        elif self.keygrab:
            if self.escape_sequence == key:
                # stop grabbing the terminal
                self.keygrab = False
                self.last_key = key
                return None
        else:
            if key == "page up":
                self.term.scroll_buffer()  # type: ignore[union-attr]
                self.last_key = key
                self._invalidate()
                return None

            if key == "page down":
                self.term.scroll_buffer(up=False)  # type: ignore[union-attr]
                self.last_key = key
                self._invalidate()
                return None

            if self.last_key == self.escape_sequence and key != self.escape_sequence:
                # hand down keypress directly after ungrab.
                self.last_key = key
                return key

            if self.escape_sequence == key:
                # start grabbing the terminal
                self.keygrab = True
                self.last_key = key
                return None

            if self._command_map[key] is None or key == "enter":
                # printable character or escape sequence means:
                # lock in terminal...
                self.keygrab = True
                # ... and do key processing
            else:
                # hand down keypress
                self.last_key = key
                return key

        self.last_key = key

        self.term.scroll_buffer(reset=True)  # type: ignore[union-attr]

        if key.startswith("ctrl "):
            if key[-1].islower():
                key = chr(ord(key[-1]) - ord("a") + 1)
            else:
                key = chr(ord(key[-1]) - ord("A") + 1)
        else:  # noqa: PLR5501  # pylint: disable=else-if-used  # readability
            if self.term_modes.keys_decckm and key in KEY_TRANSLATIONS_DECCKM:
                key = KEY_TRANSLATIONS_DECCKM[key]
            else:
                key = KEY_TRANSLATIONS.get(key, key)

        # ENTER transmits both a carriage return and linefeed in LF/NL mode.
        if self.term_modes.lfnl and key == "\r":
            key += "\n"

        os.write(typing.cast("int", self.master), key.encode(self.encoding, "ignore"))

        return None
urwid-urwid-69907e4/urwid/widget/000077500000000000000000000000001526073734300167145ustar00rootroot00000000000000urwid-urwid-69907e4/urwid/widget/__init__.py000066400000000000000000000131671526073734300210350ustar00rootroot00000000000000"""Widget classes: the base widget API, decorations, containers and basic widgets."""

from __future__ import annotations

import typing

from .ansi_text import ANSIText
from .attr_map import AttrMap, AttrMapError
from .attr_wrap import AttrWrap
from .bar_graph import BarGraph, BarGraphError, BarGraphMeta, GraphVScale, scale_bar_values
from .big_text import BigText
from .box_adapter import BoxAdapter, BoxAdapterError
from .columns import Columns, ColumnsError, ColumnsWarning
from .constants import (
    RELATIVE_100,
    Align,
    Sizing,
    VAlign,
    WHSettings,
    WrapMode,
    normalize_align,
    normalize_height,
    normalize_valign,
    normalize_width,
    simplify_align,
    simplify_height,
    simplify_valign,
    simplify_width,
)
from .container import WidgetContainerListContentsMixin, WidgetContainerMixin
from .deque_walker import SimpleDequeWalker, SimpleFocusDequeWalker
from .divider import Divider
from .edit import Edit, EditError, IntEdit
from .filler import Filler, FillerError, FillerWarning, calculate_top_bottom_filler
from .frame import Frame, FrameError
from .grid_flow import GridFlow, GridFlowError, GridFlowWarning
from .line_box import LineBox
from .list_walker import ListWalker, ListWalkerError, SimpleFocusListWalker, SimpleListWalker
from .listbox import ListBox, ListBoxError
from .monitored_deque import MonitoredDeque, MonitoredFocusDeque
from .monitored_list import MonitoredFocusList, MonitoredList
from .overlay import Overlay, OverlayError, OverlayWarning
from .padding import Padding, PaddingError, PaddingWarning, calculate_left_right_padding
from .pile import Pile, PileError, PileWarning
from .popup import PopUpLauncher, PopUpTarget
from .progress_bar import ProgressBar
from .scrollable import Scrollable, ScrollableError, ScrollBar
from .solid_fill import SolidFill
from .text import Text, TextError
from .treetools import ParentNode, TreeListBox, TreeNode, TreeWalker, TreeWidget, TreeWidgetError
from .widget import (
    AbstractBoxWidget,
    AbstractFixedWidget,
    AbstractFlowWidget,
    AbstractWidget,
    Widget,
    WidgetError,
    WidgetMeta,
    WidgetWarning,
    WidgetWrap,
    WidgetWrapError,
    delegate_to_widget_mixin,
    fixed_size,
    nocache_widget_render,
    nocache_widget_render_instance,
)
from .widget_decoration import WidgetDecoration, WidgetDisable, WidgetPlaceholder
from .wimp import Button, CheckBox, CheckBoxError, RadioButton, SelectableIcon

__all__ = (
    "ANY",
    "BOTTOM",
    "BOX",
    "CENTER",
    "CLIP",
    "ELLIPSIS",
    "FIXED",
    "FLOW",
    "GIVEN",
    "LEFT",
    "MIDDLE",
    "PACK",
    "RELATIVE",
    "RELATIVE_100",
    "RIGHT",
    "SPACE",
    "TOP",
    "WEIGHT",
    "ANSIText",
    "AbstractBoxWidget",
    "AbstractFixedWidget",
    "AbstractFlowWidget",
    "AbstractWidget",
    "Align",
    "AttrMap",
    "AttrMapError",
    "AttrWrap",
    "BarGraph",
    "BarGraphError",
    "BarGraphMeta",
    "BigText",
    "BoxAdapter",
    "BoxAdapterError",
    "Button",
    "CheckBox",
    "CheckBoxError",
    "Columns",
    "ColumnsError",
    "ColumnsWarning",
    "Divider",
    "Edit",
    "EditError",
    "Filler",
    "FillerError",
    "FillerWarning",
    "Frame",
    "FrameError",
    "GraphVScale",
    "GridFlow",
    "GridFlowError",
    "GridFlowWarning",
    "IntEdit",
    "LineBox",
    "ListBox",
    "ListBoxError",
    "ListWalker",
    "ListWalkerError",
    "MonitoredDeque",
    "MonitoredFocusDeque",
    "MonitoredFocusList",
    "MonitoredList",
    "Overlay",
    "OverlayError",
    "OverlayWarning",
    "Padding",
    "PaddingError",
    "PaddingWarning",
    "ParentNode",
    "Pile",
    "PileError",
    "PileWarning",
    "PopUpLauncher",
    "PopUpTarget",
    "ProgressBar",
    "RadioButton",
    "ScrollBar",
    "Scrollable",
    "ScrollableError",
    "SelectableIcon",
    "SimpleDequeWalker",
    "SimpleFocusDequeWalker",
    "SimpleFocusListWalker",
    "SimpleListWalker",
    "Sizing",
    "SolidFill",
    "Text",
    "TextError",
    "TreeListBox",
    "TreeNode",
    "TreeWalker",
    "TreeWidget",
    "TreeWidgetError",
    "VAlign",
    "WHSettings",
    "Widget",
    "WidgetContainerListContentsMixin",
    "WidgetContainerMixin",
    "WidgetDecoration",
    "WidgetDisable",
    "WidgetError",
    "WidgetMeta",
    "WidgetPlaceholder",
    "WidgetWarning",
    "WidgetWrap",
    "WidgetWrapError",
    "WrapMode",
    "calculate_left_right_padding",
    "calculate_top_bottom_filler",
    "delegate_to_widget_mixin",
    "fixed_size",
    "nocache_widget_render",
    "nocache_widget_render_instance",
    "normalize_align",
    "normalize_height",
    "normalize_valign",
    "normalize_width",
    "scale_bar_values",
    "simplify_align",
    "simplify_height",
    "simplify_valign",
    "simplify_width",
)

# Backward compatibility
FLOW: typing.Literal[Sizing.FLOW] = Sizing.FLOW
BOX: typing.Literal[Sizing.BOX] = Sizing.BOX
FIXED: typing.Literal[Sizing.FIXED] = Sizing.FIXED

LEFT: typing.Literal[Align.LEFT] = Align.LEFT
RIGHT: typing.Literal[Align.RIGHT] = Align.RIGHT
CENTER: typing.Literal[Align.CENTER] = Align.CENTER

TOP: typing.Literal[VAlign.TOP] = VAlign.TOP
MIDDLE: typing.Literal[VAlign.MIDDLE] = VAlign.MIDDLE
BOTTOM: typing.Literal[VAlign.BOTTOM] = VAlign.BOTTOM

SPACE: typing.Literal[WrapMode.SPACE] = WrapMode.SPACE
ANY: typing.Literal[WrapMode.ANY] = WrapMode.ANY
CLIP: typing.Literal[WrapMode.CLIP] = WrapMode.CLIP
ELLIPSIS: typing.Literal[WrapMode.ELLIPSIS] = WrapMode.ELLIPSIS

PACK: typing.Literal[WHSettings.PACK] = WHSettings.PACK
GIVEN: typing.Literal[WHSettings.GIVEN] = WHSettings.GIVEN
RELATIVE: typing.Literal[WHSettings.RELATIVE] = WHSettings.RELATIVE
WEIGHT: typing.Literal[WHSettings.WEIGHT] = WHSettings.WEIGHT
urwid-urwid-69907e4/urwid/widget/ansi_text.py000066400000000000000000000257561526073734300213030ustar00rootroot00000000000000# Urwid ANSI-coloured text widget
#    Copyright (C) 2026  urwid contributors
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/
"""A read-only widget rendering a block of ANSI/VT100-coloured text, optionally restricted to a single line."""

from __future__ import annotations

import typing

from urwid.ansi_parser import AnsiParser, ParsedLine

from .constants import Align, WrapMode
from .text import Text
from .widget import WidgetWrap

if typing.TYPE_CHECKING:
    from collections.abc import Hashable, Iterable, Iterator

    from typing_extensions import Literal

    from urwid.display import AttrSpec

__all__ = ("ANSIText",)


class ANSIText(WidgetWrap[Text]):
    r"""A read-only widget interpreting ANSI/VT100 colour, horizontal and (optionally) vertical movement escapes.

    It acts almost exactly like a read-only :class:`~urwid.Text` widget for the resolved, escape-free content.

    Parsing is delegated to :class:`urwid.ansi_parser.AnsiParser`, and layout (wrapping, alignment, packing and
    rendering) is delegated to an internal :class:`~urwid.Text` instance that this widget wraps via
    :class:`~urwid.WidgetWrap`, so this widget's ``pack``/``render``/``rows`` behaviour is identical to
    :class:`~urwid.Text` for the same resolved characters, without needing to forward those calls by hand.

    By default (``one_line=False``), a newline genuinely starts a new row, ``CSI n A``/``B`` genuinely moves the
    cursor up/down between rows, and a bare ``\r`` genuinely overwrites the current row in place (letting
    progress-bar-style redraw sequences resolve correctly) -- ``ansi_text`` is treated as a single, complete block,
    meant to render one already-complete captured screen/block.

    Pass ``one_line=True`` to restrict this widget to a single physical line of resolved text; see
    :class:`~urwid.ansi_parser.AnsiParser`'s ``one_line`` flag for the underlying parsing semantics. In this mode,
    line ends and vertical cursor movement are treated as meaningless outside a single line (stripped/split rather
    than resolved into rows): since the underlying parser always
    returns a ``tuple[ParsedLine, ...]`` in this mode (line-end characters are structural split points, never
    silently discarded), constructing an :class:`ANSIText` directly from text containing an embedded line end only
    surfaces the *first* resulting line -- no data is lost (the full split is always computed), but only the first
    line is exposed by this widget instance. Use :meth:`from_lines` to turn arbitrary multi-line (or arbitrarily
    chunked) raw text into a complete sequence of :class:`ANSIText` instances, in either mode.

    >>> ANSIText("hello")
    
    >>> ANSIText("a\nb").text
    'a\nb'
    >>> ANSIText("a\nb", one_line=True).text
    'a'
    """

    def __init__(
        self,
        ansi_text: str,
        previous_attr: AttrSpec | None = None,
        *,
        one_line: bool = False,
        wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode = WrapMode.SPACE,
        align: Literal["left", "center", "right"] | Align = Align.LEFT,
    ) -> None:
        """Build the widget from a block of ANSI/VT100-escaped text.

        :param ansi_text: a block of text, optionally spanning several lines and containing ANSI/VT100 escape
            sequences. If ``one_line`` is true, only its first physical line is surfaced by this widget instance.
        :param previous_attr: SGR/colour state to carry in from previously parsed text (see :attr:`last_attr`).
        :param one_line: restrict this widget to a single physical line of resolved text rather than resolving the
            whole block into multi-row output.
        :param wrap: forwarded to the internal :class:`~urwid.Text`.
        :param align: forwarded to the internal :class:`~urwid.Text`.
        """
        self._one_line = one_line
        super().__init__(Text("", align=align, wrap=wrap))
        self._parsed = ParsedLine("", [], previous_attr)
        self.set_ansi_text(ansi_text, previous_attr)

    def _repr_words(self) -> list[str]:
        first = super()._repr_words()
        return [*first, repr(self.text)]

    def set_ansi_text(self, ansi_text: str, previous_attr: AttrSpec | None = None) -> None:
        """Re-parse ``ansi_text`` and invalidate this widget.

        Parsing uses the mode (``one_line``) this widget was constructed with; if ``one_line`` is true, only the
        first resulting line is used, see the class docstring.

        :param ansi_text: a block of text, optionally spanning several lines and containing ANSI/VT100 escape
            sequences.
        :param previous_attr: SGR/colour state to carry in from previously parsed text (see :attr:`last_attr`).
        """
        parser = AnsiParser(previous_attr, one_line=self._one_line)
        parser.feed(ansi_text)
        self._set_parsed(parser.finalize()[0])

    def _set_parsed(self, parsed: ParsedLine) -> None:
        self._parsed = parsed
        self._w.set_text(self._build_markup(parsed))
        self._invalidate()

    @staticmethod
    def _build_markup(parsed: ParsedLine) -> list[tuple[Hashable, str]]:
        markup: list[tuple[Hashable, str]] = []
        pos = 0
        for attr, run in parsed.attrib:
            markup.append((attr, parsed.text[pos : pos + run]))
            pos += run
        return markup

    def get_text(self) -> tuple[str | bytes, list[tuple[Hashable, int]]]:
        """Return (*text*, *display attributes*), see :meth:`urwid.Text.get_text`."""
        return self._w.get_text()

    @property
    def text(self) -> str | bytes:
        r"""Read-only property returning the resolved, escape-free text of this widget (may contain ``\n``)."""
        return self.get_text()[0]

    @property
    def attrib(self) -> list[tuple[Hashable, int]]:
        """Read-only property returning the run-length encoded display attributes of this widget's text."""
        return self.get_text()[1]

    @property
    def last_attr(self) -> AttrSpec | None:
        """The SGR/colour state in effect at the very end of this widget's text.

        Feed this in as ``previous_attr`` to a subsequently constructed :class:`ANSIText` to continue the same
        colour state.
        """
        return self._parsed.last_attr

    @property
    def bel(self) -> int:
        """The number of BEL characters seen (aggregated over the whole block unless ``one_line`` is true)."""
        return self._parsed.bel

    @property
    def title(self) -> str | None:
        """The last OSC window-title string seen, if any."""
        return self._parsed.title

    @property
    def leds(self) -> str | None:
        """The last keyboard-LED state requested, if any."""
        return self._parsed.leds

    @classmethod
    def _from_parsed(
        cls,
        parsed: ParsedLine,
        *,
        one_line: bool,
        wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode = WrapMode.SPACE,
        align: Literal["left", "center", "right"] | Align = Align.LEFT,
    ) -> ANSIText:
        """Construct an :class:`ANSIText` directly from an already-parsed :class:`~urwid.ansi_parser.ParsedLine`.

        Bypasses re-parsing. Used internally by :meth:`from_lines`.

        :param parsed: the already-parsed line to build the widget from.
        :param one_line: the mode this widget instance should remember itself as having been parsed in, so that a
            later :meth:`set_ansi_text` call re-parses consistently.
        :param wrap: forwarded to the internal :class:`~urwid.Text`.
        :param align: forwarded to the internal :class:`~urwid.Text`.
        :returns: a new :class:`ANSIText` wrapping ``parsed``.
        """
        obj = cls.__new__(cls)
        obj._one_line = one_line
        WidgetWrap.__init__(obj, Text("", align=align, wrap=wrap))
        obj._set_parsed(parsed)
        return obj

    @classmethod
    def from_lines(
        cls,
        chunks: Iterable[str],
        previous_attr: AttrSpec | None = None,
        *,
        one_line: bool = False,
        **kwargs: typing.Any,
    ) -> Iterator[ANSIText]:
        """Consume raw ANSI text chunks and yield the resulting :class:`ANSIText` widget(s).

        ``chunks`` (e.g. successive ``os.read()`` results from a subprocess) are **not** required to be pre-split
        on line boundaries, and each chunk may contain zero, one, or several line ends. Internally accumulates a
        single :class:`~urwid.ansi_parser.AnsiParser` across all chunks, so a line split across two chunk
        boundaries -- or an escape sequence split across two chunk boundaries -- is still resolved correctly, and
        calls :meth:`~urwid.ansi_parser.AnsiParser.finalize` only once the iterable is exhausted.

        If ``one_line`` is true, one :class:`ANSIText` is yielded per line the shared parser completes, in order,
        with SGR colour state carried forward continuously: each produced :class:`ANSIText`'s ``previous_attr`` is
        the previous one's :attr:`last_attr`, whether that previous line came from the same chunk or an earlier
        one.

        If ``one_line`` is false (the default), this naturally reduces to yielding exactly *one* :class:`ANSIText`
        once every chunk has been consumed, representing the entire accumulated multi-chunk block resolved into
        real multi-row output -- the "normal multiline" case, reached through the same code path as the
        ``one_line=True`` case rather than a separate one.

        :param chunks: an iterable of raw text chunks, not required to be aligned on line or escape-sequence
            boundaries.
        :param previous_attr: SGR/colour state to seed the shared parser with, as if it were carried over from
            previously parsed text.
        :param one_line: parse in single-line-splitting mode (see the class docstring) rather than resolving the
            whole accumulated input into one multi-row block.
        :param kwargs: forwarded to :meth:`_from_parsed` (i.e. ``wrap``/``align`` for the internal
            :class:`~urwid.Text`).
        :returns: an iterator yielding one or more :class:`ANSIText` instances, in order, depending on ``one_line``.
        """
        parser = AnsiParser(previous_attr, one_line=one_line)
        for chunk in chunks:
            parser.feed(chunk)
        for parsed in parser.finalize():
            yield cls._from_parsed(parsed, one_line=one_line, **kwargs)
urwid-urwid-69907e4/urwid/widget/attr_map.py000066400000000000000000000153721526073734300211050ustar00rootroot00000000000000"""AttrMap: a decoration widget that maps display attributes of its child."""

from __future__ import annotations

import typing
from collections.abc import Hashable, Mapping

from urwid.canvas import CompositeCanvas

from .widget import WidgetError, delegate_to_widget_mixin
from .widget_decoration import WidgetDecoration

if typing.TYPE_CHECKING:
    from .widget import AbstractWidget


WrappedWidget = typing.TypeVar("WrappedWidget", bound="AbstractWidget")


class AttrMapError(WidgetError):
    """AttrMap related errors."""


class AttrMap(
    delegate_to_widget_mixin("_original_widget"),  # type: ignore[misc]
    WidgetDecoration[WrappedWidget],
):
    """A decoration that maps one set of display attributes to another.

    This object will pass all function calls and variable references to the
    wrapped widget.
    """

    def __init__(
        self,
        w: WrappedWidget,
        attr_map: Hashable | Mapping[Hashable, Hashable],
        focus_map: Hashable | Mapping[Hashable, Hashable] = None,
    ) -> None:
        """
        :param w: widget to wrap (stored as self.original_widget)

        :param attr_map: attribute to apply to *w*, or dict of old display
            attribute: new display attribute mappings

        :param focus_map: attribute to apply when in focus or dict of
            old display attribute: new display attribute mappings;
            if ``None`` use *attr*

        >>> from urwid import Divider, Edit, Text
        >>> AttrMap(Divider("!"), "bright")
         attr_map={None: 'bright'}>
        >>> AttrMap(Edit(), "notfocus", "focus").attr_map
        {None: 'notfocus'}
        >>> AttrMap(Edit(), "notfocus", "focus").focus_map
        {None: 'focus'}
        >>> size = (5,)
        >>> am = AttrMap(Text("hi"), "greeting", "fgreet")
        >>> next(am.render(size, focus=False).content())  # ... = b in Python 3
        [('greeting', None, ...'hi   ')]
        >>> next(am.render(size, focus=True).content())
        [('fgreet', None, ...'hi   ')]
        >>> am2 = AttrMap(Text(("word", "hi")), {"word": "greeting", None: "bg"})
        >>> am2
         attr_map={'word': 'greeting', None: 'bg'}>
        >>> next(am2.render(size).content())
        [('greeting', None, ...'hi'), ('bg', None, ...'   ')]
        """
        super().__init__(w)
        self._attr_map: dict[Hashable, Hashable] = {}
        self._focus_map: dict[Hashable, Hashable] | None = None
        self.set_attr_map(attr_map)
        self.set_focus_map(focus_map)

    def _repr_attrs(self) -> dict[str, typing.Any]:
        # only include the focus_attr when it takes effect (not None)
        d = {**super()._repr_attrs(), "attr_map": self._attr_map}
        if self._focus_map is not None:
            d["focus_map"] = self._focus_map
        return d

    def get_attr_map(self) -> dict[Hashable, Hashable]:
        """Return a copy of the attribute mapping dictionary."""
        # make a copy so ours is not accidentally modified
        # FIXME: a dictionary that detects modifications would be better
        return dict(self._attr_map)

    def set_attr_map(self, attr_map: Hashable | Mapping[Hashable, Hashable]) -> None:
        """
        Set the attribute mapping dictionary {from_attr: to_attr, ...}.

        A single attribute is stored as ``{None: attribute}``, the way the constructor does.

        >>> from urwid import Text
        >>> w = AttrMap(Text("hi"), None)
        >>> w.set_attr_map({"a": "b"})
        >>> w
         attr_map={'a': 'b'}>
        >>> w.set_attr_map("c")
        >>> w
         attr_map={None: 'c'}>

        :raises AttrMapError: a key or value of the mapping is not hashable.
        """
        mapping: dict[Hashable, Hashable] = dict(attr_map) if isinstance(attr_map, Mapping) else {None: attr_map}
        for from_attr, to_attr in mapping.items():
            if not isinstance(from_attr, Hashable) or not isinstance(to_attr, Hashable):
                raise AttrMapError(
                    f"{from_attr!r}:{to_attr!r} attribute mapping is invalid. Attributes must be hashable"
                )

        self._attr_map = mapping
        self._invalidate()

    attr_map = property(get_attr_map, set_attr_map)

    def get_focus_map(self) -> dict[Hashable, Hashable] | None:
        """Return a copy of the focus attribute mapping dictionary, or None if unset."""
        # make a copy so ours is not accidentally modified
        # FIXME: a dictionary that detects modifications would be better
        if self._focus_map:
            return dict(self._focus_map)
        return None

    def set_focus_map(self, focus_map: Hashable | Mapping[Hashable, Hashable] | None) -> None:
        """Set the focus attribute mapping dictionary {from_attr: to_attr, ...}.

        If None this widget will use the attr mapping instead (no change
        when in focus).

        A single attribute is stored as ``{None: attribute}``, the way the constructor does.

        >>> from urwid import Text
        >>> w = AttrMap(Text("hi"), {})
        >>> w.set_focus_map({"a": "b"})
        >>> w
         attr_map={} focus_map={'a': 'b'}>
        >>> w.set_focus_map("c")
        >>> w
         attr_map={} focus_map={None: 'c'}>
        >>> w.set_focus_map(None)
        >>> w
         attr_map={}>

        :raises AttrMapError: a key or value of the mapping is not hashable.
        """
        mapping: dict[Hashable, Hashable] | None
        if focus_map is None:
            mapping = None
        else:
            mapping = dict(focus_map) if isinstance(focus_map, Mapping) else {None: focus_map}
            for from_attr, to_attr in mapping.items():
                if not isinstance(from_attr, Hashable) or not isinstance(to_attr, Hashable):
                    raise AttrMapError(
                        f"{from_attr!r}:{to_attr!r} attribute mapping is invalid. Attributes must be hashable"
                    )
        self._focus_map = mapping
        self._invalidate()

    focus_map = property(get_focus_map, set_focus_map)

    def render(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
        focus: bool = False,
    ) -> CompositeCanvas:
        """Render wrapped widget and apply attribute. Return canvas."""
        attr_map = self._attr_map
        if focus and self._focus_map is not None:
            attr_map = self._focus_map
        canv = self._original_widget.render(size, focus=focus)
        canv = CompositeCanvas(canv)
        canv.fill_attr_apply(attr_map)
        return canv
urwid-urwid-69907e4/urwid/widget/attr_wrap.py000066400000000000000000000126721526073734300213010ustar00rootroot00000000000000"""AttrWrap: the deprecated predecessor of AttrMap."""

from __future__ import annotations

import typing
import warnings

from .attr_map import AttrMap

if typing.TYPE_CHECKING:
    from collections.abc import Hashable, Mapping

    from .constants import Sizing
    from .widget import AbstractWidget

WrappedWidget = typing.TypeVar("WrappedWidget", bound="AbstractWidget")


class AttrWrap(AttrMap[WrappedWidget]):
    """A special case of the :class:`AttrMap` widget.

    Passes all function calls and variable references on to the wrapped widget.

    .. deprecated:: 0.9.9
        Maintained for backwards compatibility only, new code should use :class:`AttrMap` instead.
    """

    def __init__(
        self,
        w: WrappedWidget,
        attr: Hashable | Mapping[Hashable, Hashable],
        focus_attr: Hashable | Mapping[Hashable, Hashable] = None,
    ) -> None:
        """Build the widget from an existing widget and attribute mapping.

        :param w: widget to wrap (stored as self.original_widget)
        :param attr: attribute to apply to w
        :param focus_attr: attribute to apply when in focus, if None use attr

        .. deprecated:: 0.9.9
            Maintained for backwards compatibility only, new code should use :class:`AttrMap` instead.

        >>> from urwid import Divider, Edit, Text
        >>> AttrWrap(Divider("!"), "bright")
         attr='bright'>
        >>> AttrWrap(Edit(), "notfocus", "focus")
         attr='notfocus' focus_attr='focus'>
        >>> size = (5,)
        >>> aw = AttrWrap(Text("hi"), "greeting", "fgreet")
        >>> next(aw.render(size, focus=False).content())
        [('greeting', None, ...'hi   ')]
        >>> next(aw.render(size, focus=True).content())
        [('fgreet', None, ...'hi   ')]
        """
        warnings.warn(
            "AttrWrap is maintained for backwards compatibility only, new code should use AttrMap instead. "
            "API will be removed in version 6.0.",
            PendingDeprecationWarning,
            stacklevel=2,
        )
        super().__init__(w, attr, focus_attr)

    def _repr_attrs(self) -> dict[str, typing.Any]:
        # only include the focus_attr when it takes effect (not None)
        d = {**super()._repr_attrs(), "attr": self.attr}
        del d["attr_map"]
        d.pop("focus_map", None)
        if self.focus_attr is not None:
            d["focus_attr"] = self.focus_attr
        return d

    @property
    def w(self) -> WrappedWidget:
        """
        The wrapped widget.

        .. deprecated:: 0.9.9
            The widget used to be stored as ``w``. Use :attr:`original_widget` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "backwards compatibility, widget used to be stored as original_widget. API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.original_widget

    @w.setter
    def w(self, new_widget: WrappedWidget) -> None:
        """
        Replace the wrapped widget.

        .. deprecated:: 0.9.9
            The widget used to be stored as ``w``. Use :attr:`original_widget` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "backwards compatibility, widget used to be stored as original_widget. API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        self.original_widget = new_widget

    def get_attr(self) -> Hashable:
        """Return the attribute applied to the wrapped widget."""
        return typing.cast("Hashable", self.attr_map[None])

    def set_attr(self, attr: Hashable) -> None:
        """
        Set the attribute to apply to the wrapped widget.

        >> w = AttrWrap(Divider("-"), None)
        >> w.set_attr('new_attr')
        >> w
         attr='new_attr'>
        """
        self.set_attr_map(attr)

    attr = property(get_attr, set_attr)

    def get_focus_attr(self) -> Hashable | None:
        """Return the attribute applied to the wrapped widget when it is in focus."""
        if focus_map := self.focus_map:
            return typing.cast("Hashable", focus_map[None])
        return None

    def set_focus_attr(self, focus_attr: Hashable) -> None:
        """Set the attribute to apply to the wrapped widget when it is in focus.

        If None this widget will use the attr instead (no change when in
        focus).

        >> w = AttrWrap(Divider("-"), 'old')
        >> w.set_focus_attr('new_attr')
        >> w
         attr='old' focus_attr='new_attr'>
        >> w.set_focus_attr(None)
        >> w
         attr='old'>
        """
        self.set_focus_map(focus_attr)

    focus_attr = property(get_focus_attr, set_focus_attr)

    def __getattr__(self, name: str) -> typing.Any:
        """
        Call getattr on wrapped widget.

        This has been the longstanding behaviour of AttrWrap, but is discouraged.
        New code should be using AttrMap and .base_widget or .original_widget instead.
        """
        return getattr(self._original_widget, name)

    def sizing(self) -> frozenset[Sizing]:
        """Return the sizing modes supported by the wrapped widget."""
        return self._original_widget.sizing()
urwid-urwid-69907e4/urwid/widget/bar_graph.py000066400000000000000000000670721526073734300212270ustar00rootroot00000000000000"""Bar graph widgets and the helpers that compute their display."""

from __future__ import annotations

import math
import typing
from collections import deque

from urwid.canvas import CanvasCombine, CompositeCanvas, SolidCanvas
from urwid.util import get_encoding_mode

from .constants import BAR_SYMBOLS, Sizing
from .text import Text
from .widget import Widget, WidgetError, WidgetMeta, nocache_widget_render, nocache_widget_render_instance

if typing.TYPE_CHECKING:
    from collections.abc import Callable, Mapping, Sequence

    from typing_extensions import Literal

    from urwid.util import _TagMarkup


class BarGraphMeta(WidgetMeta):
    """
    Detect subclass get_data() method and dynamic change to get_data() method and disable caching in these cases.

    .. deprecated:: 4.0.10
        Overriding ``get_data()`` in a :class:`BarGraph` subclass is supported for backwards compatibility only.
        Call :meth:`BarGraph.set_data` instead, so that the rendered canvas can be cached.
    """

    def __init__(  # noqa: D107 -- BarGraphMeta is deprecated for backwards compatibility only
        cls,
        name: str,
        bases: tuple[type, ...],
        d: dict[str, typing.Any],
    ) -> None:
        # pylint: disable=protected-access

        super().__init__(name, bases, d)

        if "get_data" in d:
            cls.render = nocache_widget_render(cls)
            cls._get_data = cls.get_data  # type: ignore[has-type]
        cls.get_data = property(lambda self: self._get_data, nocache_bargraph_get_data)


def nocache_bargraph_get_data(
    self: BarGraph,
    get_data_fn: Callable[
        [tuple[int, int]],
        tuple[Sequence[Sequence[float | int]], float, Sequence[float | int] | None],
    ],
) -> None:
    """Disable caching on this bargraph because get_data_fn needs to be polled to get the latest data."""
    self.render = nocache_widget_render_instance(self)  # type: ignore[assignment]
    self._get_data = get_data_fn  # type: ignore[assignment]  # pylint: disable=protected-access


class BarGraphError(WidgetError):
    """BarGraph related errors."""


class BarGraph(Widget, metaclass=BarGraphMeta):
    """Box widget that renders a bar graph from a sequence of data values."""

    _sizing = frozenset([Sizing.BOX])

    ignore_focus = True

    eighths = BAR_SYMBOLS.VERTICAL[:8]  # Full height is done by style
    hlines = "_⎺⎻─⎼⎽"

    def __init__(
        self,
        attlist: Sequence[str | tuple[str, str]],
        hatt: list[str] | None = None,
        satt: Mapping[tuple[int, int], str] | None = None,
    ) -> None:
        """Create a bar graph with the passed display characteristics.

        See :meth:`set_segment_attributes` for a description of the parameters.
        """
        super().__init__()
        self.set_segment_attributes(attlist, hatt, satt)
        self.set_data([], 1, None)
        self.set_bar_width(None)

    def set_segment_attributes(
        self,
        attlist: Sequence[str | tuple[str, str]],
        hatt: list[str] | None = None,
        satt: Mapping[tuple[int, int], str] | None = None,
    ) -> None:
        """Set the display attributes to use for the bar graph's segments.

        :param attlist: list containing display attribute or
                        (display attribute, character) tuple for background,
                        first segment, and optionally following segments.
                        ie. len(attlist) == num segments+1
                        character defaults to ' ' if not specified.
        :param hatt: list containing attributes for horizontal lines. First
                     element is for lines on background, second is for lines
                     on first segment, third is for lines on second segment
                     etc.
        :param satt: dictionary containing attributes for smoothed
                     transitions of bars in UTF-8 display mode. The values
                     are in the form:
        :raises BarGraphError: *attlist* has fewer than two entries, or *satt* is not a mapping of valid ``(fg, bg)``
            index pairs with ``fg`` above ``bg``.

                       (fg,bg) : attr

                     fg and bg are integers where 0 is the graph background,
                     1 is the first segment, 2 is the second, ...
                     fg > bg in all values.  attr is an attribute with a
                     foreground corresponding to fg and a background
                     corresponding to bg.

        If satt is not None and the bar graph is being displayed in
        a terminal using the UTF-8 encoding then the character cell
        that is shared between the segments specified will be smoothed
        with using the UTF-8 vertical eighth characters.

        eg: set_segment_attributes( ['no', ('unsure',"?"), 'yes'] )
        will use the attribute 'no' for the background (the area from
        the top of the graph to the top of the bar), question marks
        with the attribute 'unsure' will be used for the topmost
        segment of the bar, and the attribute 'yes' will be used for
        the bottom segment of the bar.
        """
        self.attr = []
        self.char = []
        if len(attlist) < 2:
            raise BarGraphError(f"attlist must include at least background and seg1: {attlist!r}")
        for a in attlist:
            if not isinstance(a, tuple):
                self.attr.append(a)
                self.char.append(" ")
            else:
                attr, ch = a
                self.attr.append(attr)
                self.char.append(ch)

        self.hatt: list[str] = []
        hatt_list: list[str]
        if hatt is None:
            hatt_list = [self.attr[0]]
        elif isinstance(hatt, list):
            hatt_list = hatt
        else:
            hatt_list = [hatt]
        self.hatt = hatt_list

        if satt is None:
            satt = {}
        for i in satt.items():
            try:
                (fg, bg), attr = i
            except ValueError as exc:
                raise BarGraphError(f"satt not in (fg,bg:attr) form: {i!r}").with_traceback(exc.__traceback__) from exc
            if not isinstance(fg, int) or fg >= len(attlist):
                raise BarGraphError(f"fg not valid integer: {fg!r}")
            if not isinstance(bg, int) or bg >= len(attlist):
                raise BarGraphError(f"bg not valid integer: {fg!r}")
            if fg <= bg:
                raise BarGraphError(f"fg ({fg}) not > bg ({bg})")
        self.satt = satt

    def set_data(
        self,
        bardata: Sequence[Sequence[float | int]],
        top: float,
        hlines: Sequence[float | int] | None = None,
    ) -> None:
        """
        Store bar data, bargraph top and horizontal line positions.

        :param bardata: a list of bar values.
        :param top: maximum value for segments within bardata
        :param hlines: None or a bar value marking horizontal line positions

        bar values are [ segment1, segment2, ... ] lists where top is
        the maximal value corresponding to the top of the bar graph and
        segment1, segment2, ... are the values for the top of each segment of this bar.
        Simple bar graphs will only have one segment in each bar value.

        Eg: if top is 100 and there is a bar value of [ 80, 30 ] then
        the top of this bar will be at 80% of full height of the graph
        and it will have a second segment that starts at 30%.
        """
        if hlines is not None:
            # NaN compares false with every value, so sorting with it in place leaves the rest out of order.
            hlines = sorted((h for h in hlines if not (isinstance(h, float) and math.isnan(h))), reverse=True)

        self.data = bardata, top, hlines
        self._invalidate()

    def _get_data(
        self,
        size: tuple[int, int],
    ) -> tuple[Sequence[Sequence[float | int]], float, Sequence[float | int] | None]:
        """
        Return (bardata, top, hlines).

        This function is called by render to retrieve the data for the graph.
        It may be overloaded to create a dynamic bar graph.

        This implementation will truncate the bardata list returned if not all bars will fit within maxcol.
        """
        (maxcol, maxrow) = size
        bardata, top, hlines = self.data
        widths = self.calculate_bar_widths((maxcol, maxrow), bardata)

        if len(bardata) > len(widths):
            return bardata[: len(widths)], top, hlines

        return bardata, top, hlines

    def set_bar_width(self, width: int | None) -> None:
        """
        Set a preferred bar width for calculate_bar_widths to use.

        :param width: width of bar or None for automatic width adjustment
        :raises ValueError: *width* is not positive.
        """
        if width is not None and width <= 0:
            raise ValueError(width)
        self.bar_width = width
        self._invalidate()

    def calculate_bar_widths(
        self,
        size: tuple[int, int],
        bardata: Sequence[Sequence[float | int]],
    ) -> list[int]:
        """
        Return a list of bar widths, one for each bar in data.

        If self.bar_width is None this implementation will stretch
        the bars across the available space specified by maxcol.
        """
        (maxcol, _maxrow) = size

        if self.bar_width is not None:
            return [self.bar_width] * min(len(bardata), maxcol // self.bar_width)

        if len(bardata) >= maxcol:
            return [1] * maxcol

        widths = []
        grow = maxcol
        remain = len(bardata)
        for _row in bardata:
            w = int(float(grow) / remain + 0.5)
            widths.append(w)
            grow -= w
            remain -= 1
        return widths

    def selectable(self) -> Literal[False]:
        """Return False."""
        return False

    def use_smoothed(self) -> bool:
        """Return whether smoothed (sub-character resolution) rendering can be used."""
        return bool(self.satt and get_encoding_mode() == "utf8")

    def calculate_display(
        self,
        size: tuple[int, int],
    ) -> list[tuple[int, list[tuple[int | tuple[int, int] | tuple[int, int, int], int]]]]:
        """Calculate display data."""
        (maxcol, maxrow) = size
        bardata, top, hlines = self._get_data((maxcol, maxrow))
        widths = self.calculate_bar_widths((maxcol, maxrow), bardata)

        disp_inner: list[tuple[int, list[tuple[int | tuple[int, int] | tuple[int, int, int], int]]]]
        if self.use_smoothed():
            disp_inner = self.smooth_display(calculate_bargraph_display(bardata, top, widths, maxrow * 8))
        else:
            disp_inner = calculate_bargraph_display(bardata, top, widths, maxrow)  # type: ignore[assignment]

        if hlines:
            disp_inner = self.hlines_display(disp_inner, top, hlines, maxrow)

        return disp_inner

    def hlines_display(
        self,
        disp: list[tuple[int, list[tuple[int | tuple[int, int] | tuple[int, int, int], int]]]],
        top: float,
        hlines: Sequence[float | int],
        maxrow: int,
    ) -> list[tuple[int, list[tuple[int | tuple[int, int] | tuple[int, int, int], int]]]]:
        """Add hlines to display structure represented as bar_type tuple values.

        The tuple is ``(bg, 0-5)`` where bg is the segment that has the hline on it, and 0-5 is the hline
        graphic to use where 0 is a regular underscore and 1-5 are the UTF-8 horizontal scan line characters.
        """
        if self.use_smoothed():
            shiftr = 0.0
            r = [
                (0.2, 1),
                (0.4, 2),
                (0.6, 3),
                (0.8, 4),
                (1.0, 5),
            ]
        else:
            shiftr = 0.5
            r = [
                (1.0, 0),
            ]

        # reverse the hlines to match screen ordering
        rhl: list[float] = []
        for h in hlines:
            try:
                rh = float(top - h) * maxrow / top - shiftr
            except (OverflowError, ZeroDivisionError):
                # out of float range, or a zero top: the line has no row in the graph
                continue
            # the range check also drops NaN and infinity
            if 0 <= rh < math.inf:
                rhl.append(rh)

        # build a list of rows that will have hlines
        hrows = []
        last_i = -1
        for rh in rhl:
            i = int(rh)
            if i == last_i:
                continue
            f = rh - i
            for spl, chnum in r:
                if f < spl:
                    hrows.append((i, chnum))
                    break
            last_i = i

        # fill hlines into disp data
        def fill_row(
            row: list[tuple[int | tuple[int, int] | tuple[int, int, int], int]],
            chnum: int,
        ) -> list[tuple[int | tuple[int, int] | tuple[int, int, int], int]]:
            rout: list[tuple[int | tuple[int, int] | tuple[int, int, int], int]] = []
            for bar_type, width in row:
                if isinstance(bar_type, int) and len(self.hatt) > bar_type:
                    rout.append(((bar_type, chnum), width))
                    continue
                rout.append((bar_type, width))
            return rout

        o = []
        k = 0
        rnum = 0
        for y_count, row in disp:
            if k >= len(hrows):
                o.append((y_count, row))
                continue
            end_block = rnum + y_count
            while k < len(hrows) and hrows[k][0] < end_block:
                i, chnum = hrows[k]
                if i - rnum > 0:
                    o.append((i - rnum, row))
                o.append((1, fill_row(row, chnum)))
                rnum = i + 1
                k += 1
            if rnum < end_block:
                o.append((end_block - rnum, row))
                rnum = end_block

        return o

    def smooth_display(
        self,
        disp: list[tuple[int, list[tuple[int, int]]]],
    ) -> list[tuple[int, list[tuple[int | tuple[int, int] | tuple[int, int, int], int]]]]:
        """Smooth (col, row*8) display into (col, row) display using UTF vertical eighth characters.

        The characters are represented as bar_type tuple values ``(fg, bg, 1-7)`` where fg is the lower
        segment, bg is the upper segment and 1-7 is the vertical eighth character to use.

        :raises BarGraphError: the smoothed rows do not add up to the graph height.
        """
        o: list[tuple[int, list[tuple[int | tuple[int, int] | tuple[int, int, int], int]]]] = []
        r = 0  # row remainder

        def seg_combine(
            a: tuple[int | tuple[int, int] | tuple[int, int, int], int],
            b: tuple[int | tuple[int, int] | tuple[int, int, int], int],
        ) -> tuple[
            tuple[int | tuple[int, int] | tuple[int, int, int], int],
            tuple[int | tuple[int, int] | tuple[int, int, int], int] | None,
            tuple[int | tuple[int, int] | tuple[int, int, int], int] | None,
        ]:
            (bt1, w1), (bt2, w2) = a, b
            if (bt1, w1) == (bt2, w2):
                return (bt1, w1), None, None
            wmin = min(w1, w2)
            l1: tuple[int | tuple[int, int] | tuple[int, int, int], int] | None = None
            l2: tuple[int | tuple[int, int] | tuple[int, int, int], int] | None = None
            if w1 > w2:
                l1 = (bt1, w1 - w2)
            elif w2 > w1:
                l2 = (bt2, w2 - w1)
            if isinstance(bt1, tuple):
                return (bt1, wmin), l1, l2
            if (bt2, bt1) not in self.satt:
                if r < 4:
                    return (bt2, wmin), l1, l2
                return (bt1, wmin), l1, l2
            return ((bt2, bt1, 8 - r), wmin), l1, l2  # type: ignore[return-value]

        def row_combine_last(
            count: int,
            row: list[tuple[int | tuple[int, int] | tuple[int, int, int], int]],
        ) -> None:
            """Merge *row* into the last row of the output, joining runs that share a bar type.

            :raises BarGraphError: *row* is shorter than the row it is merged into.
            """
            o_count, o_row = o[-1]
            # deque gives O(1) popleft()/appendleft() instead of the O(n) list.pop(0) and
            # [x, *lst] prepend this loop would otherwise do on every iteration.
            row_remaining: deque[tuple[int | tuple[int, int] | tuple[int, int, int], int]] = deque(row)
            o_row_remaining: deque[tuple[int | tuple[int, int] | tuple[int, int, int], int]] = deque(o_row)
            widget_list: list[tuple[int | tuple[int, int] | tuple[int, int, int], int]] = []
            while row_remaining:
                (bt, w), l1, l2 = seg_combine(o_row_remaining.popleft(), row_remaining.popleft())
                if widget_list and widget_list[-1][0] == bt:
                    widget_list[-1] = (bt, widget_list[-1][1] + w)
                else:
                    widget_list.append((bt, w))
                if l1:
                    o_row_remaining.appendleft(l1)
                if l2:
                    row_remaining.appendleft(l2)

            if o_row_remaining:
                raise BarGraphError(list(o_row_remaining))

            o[-1] = (o_count + count, widget_list)

        # regroup into actual rows (8 disp rows == 1 actual row)
        for y_count, row in disp:
            if r:
                count = min(8 - r, y_count)
                row_combine_last(count, row)  # type: ignore[arg-type]
                y_count -= count  # noqa: PLW2901
                r += count
                r %= 8
                if not y_count:
                    continue
            if r != 0:
                raise BarGraphError
            # copy whole blocks
            if y_count > 7:
                o.append((y_count // 8 * 8, row))  # type: ignore[arg-type]
                y_count %= 8  # noqa: PLW2901
                if not y_count:
                    continue
            o.append((y_count, row))  # type: ignore[arg-type]
            r = y_count
        return [(y // 8, row) for (y, row) in o]

    def render(
        self,
        size: tuple[int, int],  # type: ignore[override]
        focus: bool = False,
    ) -> CompositeCanvas:
        """
        Render BarGraph.

        :raises BarGraphError: a graph character does not render as a single row.
        """
        (maxcol, maxrow) = size
        disp = self.calculate_display((maxcol, maxrow))

        combinelist = []
        for y_count, row in disp:
            widget_list = []
            for bar_type, width in row:
                if isinstance(bar_type, tuple):
                    if len(bar_type) == 3:
                        # vertical eighths
                        fg, bg, k = bar_type
                        a = self.satt[fg, bg]
                        t = self.eighths[k] * width
                    else:
                        # horizontal lines
                        bg, k = bar_type
                        a = self.hatt[bg]
                        t = self.hlines[k] * width
                else:
                    a = self.attr[bar_type]
                    t = self.char[bar_type] * width
                widget_list.append((a, t))
            c = Text(widget_list).render((maxcol,))
            if c.rows() != 1:
                raise BarGraphError("Invalid characters in BarGraph!")
            combinelist += [(c, None, False)] * y_count

        canv = CanvasCombine(combinelist)
        return canv


def calculate_bargraph_display(
    bardata: Sequence[Sequence[float | int]],
    top: float,
    bar_widths: list[int],
    maxrow: int,
) -> list[tuple[int, list[tuple[int, int]]]]:
    """Calculate a rendering of the bar graph described by data, bar_widths and height.

    :param bardata: bar information with same structure as BarGraph.data
    :param top: maximal value for bardata segments
    :param bar_widths: list of integer column widths for each bar
    :param maxrow: rows for display of bargraph
    :raises BarGraphError: *bardata* and *bar_widths* have different lengths.
    :raises ValueError: a bar segment falls outside the rendered columns.

    Returns a structure as follows:
      [ ( y_count, [ ( bar_type, width), ... ] ), ... ]

    The outer tuples represent a set of identical rows. y_count is
    the number of rows in this set, the list contains the data to be
    displayed in the row repeated through the set.

    The inner tuple describes a run of width characters of bar_type.
    bar_type is an integer starting from 0 for the background, 1 for
    the 1st segment, 2 for the 2nd segment etc..

    This function should complete in approximately O(n+m) time, where
    n is the number of bars displayed and m is the number of rows.
    """
    if len(bardata) != len(bar_widths):
        raise BarGraphError

    maxcol = sum(bar_widths)

    # build intermediate data structure
    rows: list[list[tuple[int, int, int]] | None] = [None] * maxrow

    def add_segment(
        seg_num: int,
        col: int,
        row: int,
        width: int,
        rows: list[list[tuple[int, int, int]] | None] = rows,
    ) -> None:
        row_data = rows[row]
        if row_data:
            last_seg, last_col, last_end = row_data[-1]
            if last_end > col:
                if last_col >= col:
                    del row_data[-1]
                else:
                    row_data[-1] = (last_seg, last_col, col)
            elif last_seg == seg_num and last_end == col:
                row_data[-1] = (last_seg, last_col, last_end + width)
                return
        elif row_data is None:
            rows[row] = []
            row_data = rows[row]

        row_data.append((seg_num, col, col + width))  # type: ignore[union-attr]  # it's initialised

    col = 0
    for barnum, bar in enumerate(bardata):
        width = bar_widths[barnum]
        if width < 1:
            continue
        # loop through in reverse order
        tallest = maxrow
        segments = scale_bar_values(bar, top, maxrow)
        for k in range(len(bar) - 1, -1, -1):
            s = segments[k]

            if s >= maxrow:
                continue
            s = max(s, 0)
            if s < tallest:
                # add only properly-overlapped bars
                tallest = s
                add_segment(k + 1, col, s, width)
        col += width

    # print(repr(rows))
    # build rowsets data structure
    rowsets = []
    y_count = 0
    last = [(0, maxcol)]

    for r in rows:
        if r is None:
            y_count += 1
            continue
        if y_count:
            rowsets.append((y_count, last))
            y_count = 0

        i = 0  # index into "last"
        la, ln = last[i]  # last attribute, last run length
        c = 0  # current column
        o = []  # output list to be added to rowsets
        for seg_num, start, end in r:
            while start > c + ln:
                o.append((la, ln))
                i += 1
                c += ln
                la, ln = last[i]

            if la == seg_num:
                # same attribute, can combine
                o.append((la, end - c))
            else:
                if start - c > 0:
                    o.append((la, start - c))
                o.append((seg_num, end - start))

            if end == maxcol:
                i = len(last)
                break

            # skip past old segments covered by new one
            while end >= c + ln:
                i += 1
                c += ln
                la, ln = last[i]

            if la != seg_num:
                ln = c + ln - end
                c = end
                continue

            # same attribute, can extend
            oa, on = o[-1]
            on += c + ln - end
            o[-1] = oa, on

            i += 1
            c += ln
            if c == maxcol:
                break
            if i >= len(last):
                raise ValueError(repr((on, maxcol)))
            la, ln = last[i]

        if i < len(last):
            o += [(la, ln), *last[i + 1 :]]
        last = o
        y_count += 1

    if y_count:
        rowsets.append((y_count, last))

    return rowsets


class GraphVScale(Widget):
    """Box widget that renders a vertical scale of labels for a :class:`BarGraph`."""

    _sizing = frozenset([Sizing.BOX])

    def __init__(
        self,
        labels: Sequence[tuple[float | int, _TagMarkup]],
        top: float,
    ) -> None:
        """
        Build a vertical scale for the BarGraph widget, which can correspond to the BarGraph's horizontal lines.

        :param labels: a sequence of ``(label position, label markup)`` pairs, where the position satisfies
            ``0 < position < top`` and gives the y position of the label, and the markup is the text markup
            for that label
        :param top: top y position
        """
        super().__init__()
        self.set_scale(labels, top)

    def set_scale(
        self,
        labels: Sequence[tuple[float | int, _TagMarkup]],
        top: float,
    ) -> None:
        """
        Replace the labels and the top y position of the scale.

        :param labels: a sequence of ``(label position, label markup)`` pairs, where the position satisfies
            ``0 < position < top`` and gives the y position of the label, and the markup is the text markup
            for that label
        :param top: top y position
        """
        # NaN compares false with every value, so sorting with it in place leaves the rest out of order.
        labels = sorted(
            (label for label in labels if not (isinstance(label[0], float) and math.isnan(label[0]))),
            reverse=True,
        )

        self.pos = []
        self.txt = []
        for y, markup in labels:
            self.pos.append(y)
            self.txt.append(Text(markup))
        self.top = top

    def selectable(self) -> Literal[False]:
        """Return False."""
        return False

    def render(
        self,
        size: tuple[int, int],  # type: ignore[override]
        focus: bool = False,
    ) -> SolidCanvas | CompositeCanvas:
        """Render GraphVScale."""
        (maxcol, maxrow) = size
        pl = scale_bar_values(self.pos, self.top, maxrow)

        combinelist = []
        rows = 0
        for p, t in zip(pl, self.txt):
            p -= 1  # noqa: PLW2901
            if p >= maxrow:
                break
            if p < rows:
                continue
            c = t.render((maxcol,))
            if p > rows:
                run = p - rows
                c_composite = CompositeCanvas(c)
                c_composite.pad_trim_top_bottom(run, 0)
                c = c_composite  # type: ignore[assignment]
            rows += c.rows()
            combinelist.append((c, None, False))
        if not combinelist:
            return SolidCanvas(" ", size[0], size[1])

        canvas = CanvasCombine(combinelist)
        if maxrow - rows:
            canvas.pad_trim_top_bottom(0, maxrow - rows)
        return canvas


def scale_bar_values(
    bar: Sequence[float | int],
    top: float,
    maxrow: int,
) -> list[int]:
    """Return a list of bar values aliased to integer values of maxrow.

    A value that is not a number counts as zero.
    A value that scales to infinity, or is too large for a float, gives a row outside the range from 0 to *maxrow*.
    A *top* that is zero, not a number, infinite or too large for a float gives ``maxrow + 1`` for every value.
    """
    try:
        valid_top = math.isfinite(top) and math.isfinite(float(top)) and top != 0
    except OverflowError:
        valid_top = False
    if not valid_top:
        return [maxrow + 1] * len(bar)

    rows = []
    for v in bar:
        try:
            value = float(v)
        except OverflowError:
            value = math.inf if v > 0 else -math.inf
        scaled = value * maxrow / top
        if math.isnan(scaled):
            scaled = 0.0
        elif math.isinf(scaled):
            scaled = math.copysign(maxrow + 1.0, scaled)
        rows.append(maxrow - int(scaled + 0.5))
    return rows
urwid-urwid-69907e4/urwid/widget/big_text.py000066400000000000000000000056501526073734300211010ustar00rootroot00000000000000"""BigText: a widget that renders text in a large font."""

from __future__ import annotations

import typing

from urwid.canvas import CanvasJoin, CompositeCanvas, TextCanvas
from urwid.util import decompose_tagmarkup

from .constants import Sizing
from .widget import Widget, fixed_size

if typing.TYPE_CHECKING:
    from collections.abc import Hashable

    from urwid import Font
    from urwid.util import _TagMarkup


class BigText(Widget):
    """Fixed widget that renders text using a large :class:`Font`."""

    _sizing = frozenset([Sizing.FIXED])

    def __init__(self, markup: _TagMarkup, font: Font) -> None:
        """Build the widget from markup rendered with a large font.

        :param markup: same as Text widget markup
        :param font: instance of a Font class
        """
        super().__init__()
        self.text: str = ""
        self.attrib: list[tuple[Hashable, int]] = []
        self.font: Font = font
        self.set_text(markup)

    def set_text(self, markup: _TagMarkup) -> None:
        """Replace the displayed text and attributes, decomposing *markup* into them."""
        self.text, self.attrib = decompose_tagmarkup(markup)  # type: ignore[assignment]
        self._invalidate()

    def get_text(self) -> tuple[str, list[tuple[Hashable, int]]]:
        """Return (text, attributes)."""
        return self.text, self.attrib

    def set_font(self, font: Font) -> None:
        """Replace the font used to render this widget's text."""
        self.font = font
        self._invalidate()

    def pack(  # type: ignore[override]
        self,
        size: tuple[()] | None = (),
        focus: bool = False,
    ) -> tuple[int, int]:
        """Return the ``(cols, rows)`` size needed to render this widget's text in its font."""
        rows = self.font.height
        cols = 0
        for c in self.text:
            cols += self.font.char_width(c)
        return cols, rows

    def render(
        self,
        size: tuple[()],  # type: ignore[override]
        focus: bool = False,
    ) -> CompositeCanvas:
        """Render this widget's text as a canvas joining each character's font glyph."""
        fixed_size(size)  # complain if parameter is wrong
        a: Hashable | None = None
        ai = ak = 0
        o = []
        rows = self.font.height
        attrib = [*self.attrib, (None, len(self.text))]
        for ch in self.text:
            if not ak:
                a, ak = attrib[ai]
                ai += 1
            ak -= 1

            if width := self.font.char_width(ch):
                c: TextCanvas | CompositeCanvas = self.font.render(ch)
                if a is not None:
                    c = CompositeCanvas(c)
                    c.fill_attr(a)
                o.append((c, None, False, width))

        if o:
            canv = CanvasJoin(o)
        else:
            canv = CompositeCanvas(TextCanvas([b""] * rows, maxcol=0, check_width=False))
        canv.set_depends([])
        return canv
urwid-urwid-69907e4/urwid/widget/box_adapter.py000066400000000000000000000132351526073734300215620ustar00rootroot00000000000000"""BoxAdapter: a decoration that lets a box widget be used as a flow widget."""

from __future__ import annotations

import typing
import warnings

from urwid.canvas import CompositeCanvas

from .constants import Sizing
from .widget_decoration import WidgetDecoration, WidgetError

if typing.TYPE_CHECKING:
    from .widget import AbstractBoxWidget


WrappedWidget = typing.TypeVar("WrappedWidget", bound="AbstractBoxWidget")


class BoxAdapterError(WidgetError):
    """BoxAdapter related errors."""


class BoxAdapter(WidgetDecoration[WrappedWidget]):
    """Adapter for using a box widget where a flow widget would usually go."""

    no_cache: typing.ClassVar[list[str]] = ["rows"]

    def __init__(self, box_widget: WrappedWidget, height: int) -> None:
        """
        Create a flow widget that contains a box widget.

        :param box_widget: box widget to wrap
        :param height: number of rows for box widget
        :raises BoxAdapterError: *box_widget* is not a BOX widget.

        >>> from urwid import SolidFill
        >>> BoxAdapter(SolidFill("x"), 5)  # 5-rows of x's
         height=5>
        """
        if hasattr(box_widget, "sizing") and Sizing.BOX not in box_widget.sizing():
            raise BoxAdapterError(f"{box_widget!r} is not a box widget")
        super().__init__(box_widget)

        self.height = height

    def _repr_attrs(self) -> dict[str, typing.Any]:
        return {**super()._repr_attrs(), "height": self.height}

    @property
    def box_widget(self) -> WrappedWidget:
        """
        The wrapped box widget.

        .. deprecated:: 0.9.9
            The widget used to be stored as ``box_widget``. Use :attr:`original_widget` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "original stored as original_widget, keep for compatibility. API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.original_widget

    @box_widget.setter
    def box_widget(self, widget: WrappedWidget) -> None:
        """
        Replace the wrapped box widget.

        .. deprecated:: 0.9.9
            The widget used to be stored as ``box_widget``. Use :attr:`original_widget` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "original stored as original_widget, keep for compatibility. API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        self.original_widget = widget

    def sizing(self) -> frozenset[Sizing]:
        """Return the sizing modes this widget supports, which is always just FLOW."""
        return frozenset((Sizing.FLOW,))

    def rows(self, size: tuple[int], focus: bool = False) -> int:
        """
        Return the predetermined height (behave like a flow widget).

        >>> from urwid import SolidFill
        >>> BoxAdapter(SolidFill("x"), 5).rows((20,))
        5
        """
        return self.height

    # The next few functions simply tack-on our height and pass through
    # to self._original_widget
    def get_cursor_coords(self, size: tuple[int]) -> tuple[int, int] | None:
        """Return the wrapped box widget's cursor coordinates for *size* at the adapter's height."""
        (maxcol,) = size
        if (get_cursor_coords := getattr(self._original_widget, "get_cursor_coords", None)) is not None:
            return typing.cast("tuple[int, int] | None", get_cursor_coords((maxcol, self.height)))
        return None

    def move_cursor_to_coords(self, size: tuple[int], col: int, row: int) -> bool:
        """Move the cursor of the wrapped box widget to ``(col, row)`` for *size* at the adapter's height."""
        (maxcol,) = size
        if (move_cursor_to_coords := getattr(self._original_widget, "move_cursor_to_coords", None)) is not None:
            return typing.cast("bool", move_cursor_to_coords((maxcol, self.height), col, row))
        return True

    def get_pref_col(self, size: tuple[int]) -> int | None:
        """Return the wrapped box widget's preferred cursor column for *size* at the adapter's height."""
        (maxcol,) = size
        if (get_pref_col := getattr(self._original_widget, "get_pref_col", None)) is not None:
            return typing.cast("int | None", get_pref_col((maxcol, self.height)))
        return None

    def keypress(
        self,
        size: tuple[int],  # type: ignore[override]
        key: str,
    ) -> str | None:
        """Forward the keypress to the wrapped box widget at *size* and the adapter's height."""
        (maxcol,) = size
        return self._original_widget.keypress((maxcol, self.height), key)

    def mouse_event(
        self,
        size: tuple[int],  # type: ignore[override]
        event: str,
        button: int,
        col: int,
        row: int,
        focus: bool,
    ) -> bool | None:
        """Forward the mouse event to the wrapped box widget at *size* and the adapter's height."""
        (maxcol,) = size
        if not hasattr(self._original_widget, "mouse_event"):
            return False
        return self._original_widget.mouse_event((maxcol, self.height), event, button, col, row, focus)

    def render(
        self,
        size: tuple[int],  # type: ignore[override]
        focus: bool = False,
    ) -> CompositeCanvas:
        """Render the wrapped box widget at *size* and the adapter's height."""
        (maxcol,) = size
        canv = CompositeCanvas(self._original_widget.render((maxcol, self.height), focus))
        return canv

    def __getattr__(self, name: str) -> typing.Any:
        """Pass calls to box widget."""
        return getattr(self.original_widget, name)
urwid-urwid-69907e4/urwid/widget/columns.py000066400000000000000000001534651526073734300207640ustar00rootroot00000000000000"""Columns: a container that places widgets side by side."""

from __future__ import annotations

import math
import sys
import typing
import warnings
from itertools import chain, repeat

from typing_extensions import Literal

import urwid
from urwid.canvas import Canvas, CanvasJoin, CompositeCanvas, SolidCanvas
from urwid.command_map import Command
from urwid.split_repr import remove_defaults
from urwid.util import is_mouse_press

from .constants import Align, Sizing, WHSettings
from .container import WidgetContainerListContentsMixin, WidgetContainerMixin, _ContainerElementSizingFlag
from .monitored_list import MonitoredFocusList, MonitoredList
from .widget import (
    AbstractBoxWidget,
    AbstractFixedWidget,
    AbstractFlowWidget,
    AbstractWidget,
    Widget,
    WidgetError,
    WidgetWarning,
)

if typing.TYPE_CHECKING:
    from collections.abc import Collection, Iterable, Iterator, Sequence


# Flag combinations tested on every element of every ``Columns.sizing()`` call, hoisted out of the loop
# so that the enum member lookups and the OR-ing happen once per import instead of once per element.
_BOX_FLOW_FIXED = _ContainerElementSizingFlag.BOX | _ContainerElementSizingFlag.FLOW | _ContainerElementSizingFlag.FIXED
_FLOW_FIXED = _ContainerElementSizingFlag.FLOW | _ContainerElementSizingFlag.FIXED
_GIVEN_BOX = _ContainerElementSizingFlag.BOX | _ContainerElementSizingFlag.WH_GIVEN
_BOX_OR_FLOW = frozenset((Sizing.BOX, Sizing.FLOW))


class ColumnsError(WidgetError):
    """Columns related errors."""


class ColumnsWarning(WidgetWarning):
    """Columns related warnings."""


class Columns(
    Widget,
    WidgetContainerMixin[int],
    WidgetContainerListContentsMixin[
        typing.Union[
            tuple[
                typing.Union[AbstractFlowWidget, AbstractFixedWidget],
                tuple[Literal[WHSettings.PACK], None, bool],
            ],
            tuple[
                typing.Union[AbstractBoxWidget, AbstractFlowWidget],
                typing.Union[
                    tuple[Literal[WHSettings.GIVEN], int, bool],
                    tuple[Literal[WHSettings.WEIGHT], typing.Union[int, float], bool],
                ],
            ],
        ]
    ],
):
    """Widgets arranged horizontally in columns from left to right."""

    def sizing(self) -> frozenset[Sizing]:
        """Sizing supported by widget.

        :return: Calculated widget sizing

        Due to the nature of container with mutable contents, this method cannot be cached.

        Rules:
        * WEIGHT BOX -> BOX
        * GIVEN BOX -> Can be included in FIXED and FLOW depends on the other columns
        * PACK BOX -> Unsupported
        * BOX-only widget without `box_columns` disallow FLOW render

        * WEIGHT FLOW -> FLOW
        * GIVEN FLOW -> FIXED (known width and widget knows its height) + FLOW (historic)
        * PACK FLOW -> FLOW (widget fit in provided size)

        * WEIGHT FIXED -> Need also FLOW or/and BOX to properly render due to width calculation
        * GIVEN FIXED -> Unsupported
        * PACK FIXED -> FIXED (widget knows its size)

        Backward compatibility rules:
        * GIVEN BOX -> Allow BOX

        BOX can be only if ALL widgets support BOX.
        FIXED can be only if no BOX without "box_columns" flag and no strict FLOW.

        >>> from urwid import BigText, Edit, SolidFill, Text, Thin3x3Font
        >>> font = Thin3x3Font()

        # BOX-only widget
        >>> Columns((SolidFill("#"),))
        

        # BOX-only widget with "get height from max"
        >>> Columns((SolidFill("#"),), box_columns=(0,))
        

        # FLOW-only
        >>> Columns((Edit(),))
        

        # FLOW allowed by "box_columns"
        >>> Columns((Edit(), SolidFill("#")), box_columns=(1,))
        

        # FLOW/FIXED
        >>> Columns((Text("T"),))
        

        # GIVEN BOX only -> BOX only
        >>> Columns(((5, SolidFill("#")),), box_columns=(0,))
        

        # No FLOW - BOX only
        >>> Columns(((5, SolidFill("#")), SolidFill("*")), box_columns=(0, 1))
        

        # FIXED only -> FIXED (and FLOW via drop/expand)
        >>> Columns(((WHSettings.PACK, BigText("1", font)),))
        

        # Invalid sizing combination -> use fallback settings (and produce warning)
        >>> Columns(((WHSettings.PACK, SolidFill("#")),))
        

        # Special case: empty columns widget sizing is impossible to calculate
        >>> Columns(())
        
        """
        if not self.contents:
            return frozenset((urwid.BOX, urwid.FLOW))

        strict_box = False
        has_flow = False

        block_fixed = False
        has_fixed = False
        supported: set[Sizing] = set()

        # This is a set of _ContainerElementSizingFlag ORed together.
        flags: set[int] = set()

        for idx, (widget, (size_kind, _size_weight, is_box)) in enumerate(self.contents):
            w_sizing = widget.sizing()

            flag: int = _ContainerElementSizingFlag.NONE

            if size_kind == WHSettings.WEIGHT:
                flag |= _ContainerElementSizingFlag.WH_WEIGHT
                if Sizing.BOX in w_sizing:
                    flag |= _ContainerElementSizingFlag.BOX
                if Sizing.FLOW in w_sizing:
                    flag |= _ContainerElementSizingFlag.FLOW
                if Sizing.FIXED in w_sizing and w_sizing & _BOX_OR_FLOW:
                    flag |= _ContainerElementSizingFlag.FIXED

            elif size_kind == WHSettings.GIVEN:
                flag |= _ContainerElementSizingFlag.WH_GIVEN
                if Sizing.BOX in w_sizing:
                    flag |= _ContainerElementSizingFlag.BOX
                if Sizing.FLOW in w_sizing:
                    flag |= _ContainerElementSizingFlag.FIXED
                    flag |= _ContainerElementSizingFlag.FLOW

            else:
                flag |= _ContainerElementSizingFlag.WH_PACK
                if Sizing.FIXED in w_sizing:
                    flag |= _ContainerElementSizingFlag.FIXED
                if Sizing.FLOW in w_sizing:
                    flag |= _ContainerElementSizingFlag.FLOW

            if not flag & _BOX_FLOW_FIXED:
                warnings.warn(
                    f"Sizing combination of widget {widget} (position={idx}) not supported: "
                    f"{size_kind.name} box={is_box}",
                    ColumnsWarning,
                    stacklevel=3,
                )
                return frozenset((Sizing.BOX, Sizing.FLOW))

            flags.add(flag)

            if flag & _ContainerElementSizingFlag.BOX and not (is_box or flag & _FLOW_FIXED):
                strict_box = True

            if flag & _ContainerElementSizingFlag.FLOW:
                has_flow = True
            if flag & _ContainerElementSizingFlag.FIXED:
                has_fixed = True
            elif flag & _GIVEN_BOX != _GIVEN_BOX:
                block_fixed = True

        if all(flag & _ContainerElementSizingFlag.BOX for flag in flags):
            # Only if ALL widgets can be rendered as BOX, widget can be rendered as BOX.
            # Hacky "BOX" render for FLOW-only is still present,
            # due to incorrected implementation can be used by downstream
            supported.add(Sizing.BOX)

        if not strict_box:
            if has_flow:
                supported.add(Sizing.FLOW)

            if has_fixed and not block_fixed:
                supported.add(Sizing.FLOW)
                supported.add(Sizing.FIXED)

        if not supported:
            warnings.warn(
                f"Columns widget contents flags not allow to determine supported render kind:\n"
                f"{', '.join(sorted(_ContainerElementSizingFlag.log_string(flag) for flag in flags))}\n"
                f"Using fallback hardcoded BOX|FLOW sizing kind.",
                ColumnsWarning,
                stacklevel=3,
            )
            return frozenset((Sizing.BOX, Sizing.FLOW))

        return frozenset(supported)

    def __init__(
        self,
        widget_list: Iterable[
            AbstractBoxWidget
            | AbstractFlowWidget
            | tuple[Literal["pack", WHSettings.PACK], AbstractFlowWidget | AbstractFixedWidget]
            | tuple[int, AbstractBoxWidget | AbstractFlowWidget]
            | tuple[Literal["given", WHSettings.GIVEN], int, AbstractBoxWidget | AbstractFlowWidget]
            | tuple[Literal["weight", WHSettings.WEIGHT], int | float, AbstractBoxWidget | AbstractFlowWidget]
        ],
        dividechars: int = 0,
        focus_column: int | AbstractWidget | None = None,
        min_width: int = 1,
        box_columns: Iterable[int] | None = None,
    ):
        """Initialize a new Columns instance.

        :param widget_list: iterable of flow or box widgets
        :param dividechars: number of blank characters between columns
        :param focus_column: index into widget_list of column in focus or focused widget instance,
            if ``None`` the first selectable widget will be chosen.
        :param min_width: minimum width for each column which is not
            calling widget.pack() in *widget_list*.
        :param box_columns: a list of column indexes containing box widgets
            whose height is set to the maximum of the rows
            required by columns not listed in *box_columns*.
        :raises ColumnsError: an item of *widget_list* is not a widget or a valid ``(width, widget)`` pair.

        *widget_list* may also contain tuples such as:

        (*given_width*, *widget*)
            make this column *given_width* screen columns wide, where *given_width* is an int
        (``'pack'``, *widget*)
            call :meth:`pack() ` to calculate the width of this column
        (``'weight'``, *weight*, *widget*)
            give this column a relative *weight* (number) to calculate its width from th screen columns remaining

        Widgets not in a tuple are the same as (``'weight'``, ``1``, *widget*)

        If the Columns widget is treated as a box widget then all children
        are treated as box widgets, and *box_columns* is ignored.

        If the Columns widget is treated as a flow widget then the rows
        are calculated as the largest rows() returned from all columns
        except the ones listed in *box_columns*.  The box widgets in
        *box_columns* will be displayed with this calculated number of rows,
        filling the full height.
        """
        self._cache_column_widths: list[int] = []
        self._cache_pack_widths: dict[int, int] = {}
        super().__init__()
        self._contents: MonitoredFocusList[
            tuple[
                AbstractFlowWidget | AbstractFixedWidget,
                tuple[Literal[WHSettings.PACK], None, bool],
            ]
            | tuple[
                AbstractBoxWidget | AbstractFlowWidget,
                tuple[Literal[WHSettings.GIVEN], int, bool] | tuple[Literal[WHSettings.WEIGHT], int | float, bool],
            ]
        ] = MonitoredFocusList()
        self._contents.set_modified_callback(self._contents_modified)
        self._contents.set_focus_changed_callback(lambda f: self._invalidate())
        self._contents.set_validate_contents_modified(self._validate_contents_modified)

        box_columns = set(box_columns or ())

        width: Literal["pack", WHSettings.PACK] | int | float | None

        for i, original in enumerate(widget_list):
            w = original
            if not isinstance(w, tuple):
                self.contents.append((w, (WHSettings.WEIGHT, 1, i in box_columns)))

            elif len(w) == 2:
                width, w = w  # type: ignore[assignment]  # we match later
                if width in {Sizing.FLOW, WHSettings.PACK}:  # 'pack' used to be called 'flow'
                    if i in box_columns:
                        warnings.warn(
                            f"Widget {w} is listed as a box column, but sizing is {width} (Counted as PACK).",
                            ColumnsWarning,
                            stacklevel=2,
                        )
                    self.contents.append(
                        (
                            typing.cast("AbstractFlowWidget | AbstractFixedWidget", w),
                            (WHSettings.PACK, None, False),
                        )
                    )
                else:
                    self.contents.append(
                        (
                            typing.cast("AbstractBoxWidget | AbstractFlowWidget", w),
                            (WHSettings.GIVEN, typing.cast("int", width), i in box_columns),
                        )
                    )

            elif w[0] in {Sizing.FIXED, WHSettings.GIVEN}:  # backwards compatibility: FIXED -> GIVEN
                width, w = w[-2:]
                self.contents.append((w, (WHSettings.GIVEN, typing.cast("int", width), i in box_columns)))

            elif w[0] == WHSettings.WEIGHT:
                _ignored, width, w = w
                self.contents.append((w, (WHSettings.WEIGHT, width, i in box_columns)))

            else:
                raise ColumnsError(f"initial widget list item invalid: {original!r}")

            if focus_column == w or (focus_column is None and w.selectable()):  # type: ignore[union-attr]
                focus_column = i

            if not isinstance(w, AbstractWidget):
                warnings.warn(f"{w!r} is not a Widget", ColumnsWarning, stacklevel=3)

        self.dividechars = dividechars

        if self.contents and focus_column is not None:
            self.focus_position = typing.cast("int", focus_column)
        self.pref_col: Literal["left", "right", Align.LEFT, Align.RIGHT] | int | None = None
        self.min_width = min_width
        self._cache_maxcol: int | None = None

    def _repr_words(self) -> list[str]:
        if len(self.contents) > 1:
            contents_string = f"({len(self.contents)} items)"
        elif self.contents:
            contents_string = "(1 item)"
        else:
            contents_string = "()"
        return [*super()._repr_words(), contents_string]

    def _repr_attrs(self) -> dict[str, typing.Any]:
        attrs = {
            **super()._repr_attrs(),
            "dividechars": self.dividechars,
            "focus_column": self.focus_position if len(self._contents) > 1 else None,
            "min_width": self.min_width,
        }
        return remove_defaults(attrs, Columns.__init__)

    def __rich_repr__(self) -> Iterator[tuple[str | None, typing.Any] | typing.Any]:
        """Yield this widget's constructor arguments as `(name, value)` pairs, for `rich`'s repr protocol."""
        # We do not care about exact typing here: used only for debug purposes
        widget_list: list[
            AbstractWidget
            | tuple[Literal[WHSettings.PACK] | int, AbstractWidget]
            | tuple[Literal[WHSettings.WEIGHT], int | float, AbstractWidget]
        ] = []
        box_columns: list[int] = []
        for idx, (w_instance, (sizing, amount, is_box)) in enumerate(self._contents):
            if sizing == WHSettings.GIVEN:
                widget_list.append((typing.cast("int", amount), w_instance))
            elif sizing == WHSettings.PACK:
                widget_list.append((WHSettings.PACK, w_instance))
            elif amount == 1:
                widget_list.append(w_instance)
            else:
                widget_list.append((WHSettings.WEIGHT, typing.cast("int | float", amount), w_instance))

            if is_box:
                box_columns.append(idx)

        yield "widget_list", widget_list
        yield "dividechars", self.dividechars
        yield "focus_column", self.focus_position if self._contents else None
        yield "min_width", self.min_width
        yield "box_columns", box_columns

    def __len__(self) -> int:
        """Return the number of columns."""
        return len(self._contents)

    def _contents_modified(self) -> None:
        """Invalidate this widget whenever the contents has been changed."""
        self._invalidate()

    def selectable(self) -> bool:
        """Return whether any child widget is selectable.

        Asked on every call rather than cached,
        since a child such as :class:`~urwid.WidgetPlaceholder` may change its selectability after it was added.
        """
        return any(w.selectable() for w, _ in self.contents)

    def _validate_contents_modified(
        self,
        slc: tuple[int, int, int],
        new_items: Collection[
            tuple[
                AbstractFlowWidget | AbstractFixedWidget,
                tuple[Literal[WHSettings.PACK], None, bool],
            ]
            | tuple[
                AbstractBoxWidget | AbstractFlowWidget,
                tuple[Literal[WHSettings.GIVEN], int, bool] | tuple[Literal[WHSettings.WEIGHT], int | float, bool],
            ]
        ],
    ) -> None:
        """
        Reject contents changes that would put an invalid item into the Columns.

        :raises ColumnsError: an added item is not a valid ``(widget, options)`` pair.
        """
        invalid_items: list[tuple[AbstractWidget, tuple[typing.Any, typing.Any, typing.Any]]] = []
        try:
            for item in new_items:
                _w, (t, n, b) = item
                if any(
                    (
                        t not in {WHSettings.PACK, WHSettings.GIVEN, WHSettings.WEIGHT},
                        (n is not None and (not isinstance(n, (int, float)) or n < 0 or not math.isfinite(float(n)))),
                        not isinstance(b, bool),
                    )
                ):
                    invalid_items.append(item)
        # OverflowError: float() of an int too large for a float
        except (TypeError, ValueError, OverflowError) as exc:
            raise ColumnsError(f"added content invalid {exc}").with_traceback(exc.__traceback__) from exc

        if invalid_items:
            raise ColumnsError(f"added content invalid: {invalid_items!r}")

    @property
    def widget_list(self) -> MonitoredList[AbstractWidget]:
        """
        A list of the widgets in this Columns.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`contents` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "only for backwards compatibility. You should use the new standard container `contents`. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        ml = MonitoredList(w for w, t in self.contents)

        def user_modified() -> None:
            self.widget_list = ml

        ml.set_modified_callback(user_modified)
        return ml

    @widget_list.setter
    def widget_list(self, widgets: MonitoredList[AbstractWidget]) -> None:
        """
        Replace the widgets in this Columns, keeping the old options where possible.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`contents` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "only for backwards compatibility. You should use the new standard container `contents`. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        focus_position = self.focus_position
        self.contents = [
            # need to grow contents list if widgets is longer
            (new, options)  # type: ignore[misc]  # deprecated API, historic code lack support of FIXED
            for (new, (w, options)) in zip(
                widgets,
                chain(self.contents, repeat((None, (WHSettings.WEIGHT, 1, False)))),
            )
        ]
        if focus_position < len(widgets):
            self.focus_position = focus_position

    @property
    def column_types(
        self,
    ) -> MonitoredList[
        tuple[Literal[Sizing.FLOW], None]
        | tuple[Literal[Sizing.FIXED], int]
        | tuple[Literal[WHSettings.WEIGHT], int | float],
    ]:
        """
        A list of the old partial options values for the widgets in this Columns.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`contents` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "for backwards compatibility only. "
            "You should use the new standard container property .contents to modify Columns contents. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        ml = MonitoredList(
            # return the old column type names
            (
                {
                    WHSettings.GIVEN: Sizing.FIXED,
                    WHSettings.PACK: Sizing.FLOW,
                }.get(t, t),
                n,
            )
            for w, (t, n, b) in self.contents
        )

        def user_modified() -> None:
            self.column_types = ml  # type: ignore[assignment]

        ml.set_modified_callback(user_modified)
        return ml  # type: ignore[return-value]

    @column_types.setter
    def column_types(
        self,
        column_types: MonitoredList[
            tuple[Literal[Sizing.FLOW, WHSettings.PACK], None]
            | tuple[Literal[Sizing.FIXED, WHSettings.GIVEN], int]
            | tuple[Literal[WHSettings.WEIGHT], int | float],
        ],
    ) -> None:
        """
        Replace the width settings of the widgets in this Columns.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`contents` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "for backwards compatibility only. "
            "You should use the new standard container property .contents to modify Columns contents. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        focus_position = self.focus_position
        self.contents = [
            (  # type: ignore[misc]
                w,
                (
                    {
                        Sizing.FIXED: WHSettings.GIVEN,
                        Sizing.FLOW: WHSettings.PACK,
                    }.get(new_t, new_t),  # type: ignore[arg-type]
                    new_n,
                    b,
                ),
            )
            for ((new_t, new_n), (w, (t, n, b))) in zip(column_types, self.contents)
        ]
        if focus_position < len(column_types):
            self.focus_position = focus_position

    @property
    def box_columns(self) -> MonitoredList[int]:
        """Return the indexes of the columns that are treated as box widgets when Columns is a flow widget.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`contents` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "only for backwards compatibility. You should use the new standard container property `contents`. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        ml = MonitoredList(i for i, (w, (t, n, b)) in enumerate(self.contents) if b)

        def user_modified() -> None:
            self.box_columns = ml

        ml.set_modified_callback(user_modified)
        return ml

    @box_columns.setter
    def box_columns(self, box_columns: MonitoredList[int]) -> None:
        """
        Mark the columns at the given indexes as box widgets.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`contents` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "only for backwards compatibility. You should use the new standard container property `contents`. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        box_columns = set(box_columns)  # type: ignore[assignment]
        self.contents = [(w, (t, n, i in box_columns)) for (i, (w, (t, n, b))) in enumerate(self.contents)]  # type: ignore[misc]

    @property
    def contents(
        self,
    ) -> MonitoredFocusList[
        tuple[
            AbstractFlowWidget | AbstractFixedWidget,
            tuple[Literal[WHSettings.PACK], None, bool],
        ]
        | tuple[
            AbstractBoxWidget | AbstractFlowWidget,
            tuple[Literal[WHSettings.GIVEN], int, bool] | tuple[Literal[WHSettings.WEIGHT], int | float, bool],
        ]
    ]:
        """Return the contents of this Columns as a list of `(widget, options)` tuples.

        This list may be modified like a normal list and the Columns widget will update automatically.

        .. seealso:: Create new options tuples with the :meth:`options` method
        """
        return self._contents

    @contents.setter
    def contents(
        self,
        c: Sequence[
            tuple[
                AbstractFlowWidget | AbstractFixedWidget,
                tuple[Literal[WHSettings.PACK], None, bool],
            ]
            | tuple[
                AbstractBoxWidget | AbstractFlowWidget,
                tuple[Literal[WHSettings.GIVEN], int, bool] | tuple[Literal[WHSettings.WEIGHT], int | float, bool],
            ]
        ],
    ) -> None:
        self._contents[:] = c

    @staticmethod
    def options(
        width_type: Literal[
            "pack", "given", "weight", WHSettings.PACK, WHSettings.GIVEN, WHSettings.WEIGHT
        ] = WHSettings.WEIGHT,
        width_amount: int | float | None = 1,  # noqa: PYI041  # provide explicit for IDEs
        box_widget: bool = False,
    ) -> (
        tuple[Literal[WHSettings.PACK], None, bool]
        | tuple[Literal[WHSettings.GIVEN], int, bool]
        | tuple[Literal[WHSettings.WEIGHT], int | float, bool]
    ):
        """
        Return a new options tuple for use in a Columns .contents list.

        This sets an entry's width type: one of the following:

        ``'pack'``
            Call the widget's :meth:`Widget.pack` method to determine how wide
            this column should be. *width_amount* is ignored.
        ``'given'``
            Make column exactly width_amount screen-columns wide.
        ``'weight'``
            Allocate the remaining space to this column by using
            *width_amount* as a weight value.

        :param width_type: ``'pack'``, ``'given'`` or ``'weight'``
        :param width_amount: ``None`` for ``'pack'``, a number of screen columns
            for ``'given'`` or a weight value (number) for ``'weight'``
        :param box_widget: set to `True` if this widget is to be treated as a box
            widget when the Columns widget itself is treated as a flow widget.
        :raises ColumnsError: *width_type* and *width_amount* are not a valid combination.
        """
        if width_type == WHSettings.PACK:
            return (WHSettings.PACK, None, box_widget)
        if width_type in {WHSettings.GIVEN, WHSettings.WEIGHT} and width_amount is not None:
            return (WHSettings(width_type), width_amount, box_widget)  # type: ignore[return-value]
        raise ColumnsError(f"invalid combination: width_type={width_type!r}, width_amount={width_amount!r}")

    def _invalidate(self) -> None:
        self._cache_maxcol = None
        super()._invalidate()

    def set_focus_column(self, num: int) -> None:
        """
        Set the column in focus by its index in :attr:`widget_list`.

        :param num: index of focus-to-be entry

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`focus_position` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "only for backwards compatibility. You may also use the new standard container property `focus_position`. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        self.focus_position = num

    def get_focus_column(self) -> int:
        """
        Return the focus column index.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`focus_position` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "only for backwards compatibility. You may also use the new standard container property `focus_position`. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.focus_position

    def set_focus(self, item: AbstractWidget | int) -> None:
        """
        Set the item in focus.

        :param item: widget or integer index
        :raises ValueError: *item* is a widget that is not in the contents.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`focus_position` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "only for backwards compatibility. "
            "You may also use the new standard container property `focus_position` to get the focus. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        if isinstance(item, int):
            self.focus_position = item
            return
        for i, (w, _options) in enumerate(self.contents):
            if item == w:
                self.focus_position = i
                return
        raise ValueError(f"Widget not found in Columns contents: {item!r}")

    @property
    def focus(self) -> AbstractWidget | None:
        """The child widget in focus or None when Columns is empty."""
        if not self.contents:
            return None
        return self.contents[self.focus_position][0]

    def get_focus(self) -> AbstractWidget | None:
        """
        Return the widget in focus.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`focus` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "only for backwards compatibility. "
            "You may also use the new standard container property `focus` to get the focus. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        if not self.contents:
            return None
        return self.contents[self.focus_position][0]

    @property
    def focus_position(self) -> int:
        """Return the index of the child widget in focus.

        Raises :exc:`IndexError` if read when Columns is empty, or when set to an invalid index.

        :raises IndexError: the Columns is empty.
        """
        if (focus := self.contents.focus) is not None:
            return focus

        raise IndexError("No focus_position, Columns is empty")

    @focus_position.setter
    def focus_position(self, position: int) -> None:
        """
        Set the widget in focus.

        :param position: index of child widget to be made focus
        :raises IndexError: *position* is not an index of a child widget.
        """
        try:
            if position < 0 or position >= len(self.contents):
                raise IndexError(f"No Columns child widget at position {position}")
        except TypeError as exc:
            raise IndexError(f"No Columns child widget at position {position}").with_traceback(
                exc.__traceback__
            ) from exc
        self.contents.focus = position

    @property
    def focus_col(self) -> int:
        """
        A property for reading and setting the index of the column in focus.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`focus_position` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "only for backwards compatibility. "
            "You may also use the new standard container property `focus_position` to get the focus. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.focus_position

    @focus_col.setter
    def focus_col(self, new_position: int) -> None:
        """
        Set the index of the column in focus.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`focus_position` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "only for backwards compatibility. "
            "You may also use the new standard container property `focus_position` to get the focus. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        self.focus_position = new_position

    def column_widths(self, size: tuple[int] | tuple[int, int], focus: bool = False) -> list[int]:
        """
        Return a list of column widths.

        0 values in the list means hide the corresponding column completely
        """
        maxcol = size[0]

        def pack_width(
            index: int,
            widget: AbstractWidget,
            size_kind: WHSettings,
            is_box: bool,
        ) -> int:
            """Calculate the width a 'pack' column takes by packing its widget."""
            if isinstance(widget, AbstractWidget):
                w_sizing = widget.sizing()
            else:
                warnings.warn(f"{widget!r} is not a Widget", ColumnsWarning, stacklevel=4)
                w_sizing = frozenset((urwid.BOX, urwid.FLOW))

            if w_sizing & frozenset((Sizing.FIXED, Sizing.FLOW)):
                candidate_size = 0

                if Sizing.FIXED in w_sizing:
                    candidate_size = widget.pack(
                        (),
                        focus and index == self.focus_position,
                    )[0]

                if Sizing.FLOW in w_sizing and (not candidate_size or candidate_size > maxcol):
                    # FIXME: should be able to pack with a different maxcol value
                    candidate_size = widget.pack(
                        (maxcol,),
                        focus and index == self.focus_position,
                    )[0]

                return candidate_size

            warnings.warn(
                f"Unusual widget {widget} sizing for {size_kind} (box={is_box}). "
                f"Assuming wrong sizing and using {Sizing.FLOW.upper()} for width calculation",
                ColumnsWarning,
                stacklevel=4,
            )
            return widget.pack((maxcol,), focus and index == self.focus_position)[0]

        # 'pack' widgets can change their packed width without invalidating the cache,
        # so recompute their widths and reuse the cached result only when nothing changed.
        pack_widths = {
            i: pack_width(i, w, t, b) for i, (w, (t, width, b)) in enumerate(self.contents) if t == WHSettings.PACK
        }
        if maxcol == self._cache_maxcol and pack_widths == self._cache_pack_widths:
            return self._cache_column_widths

        widths = []

        weighted: list[tuple[int | float, int]] = []
        shared = maxcol + self.dividechars

        static_w: int

        for i, (_w, (t, width, _b)) in enumerate(self.contents):
            if t == WHSettings.GIVEN:
                static_w = typing.cast("int", width)
            elif t == WHSettings.PACK:
                static_w = pack_widths[i]
            else:
                static_w = self.min_width

            if shared < static_w + self.dividechars and i > self.focus_position:
                break

            widths.append(static_w)
            shared -= static_w + self.dividechars
            if t not in {WHSettings.GIVEN, WHSettings.PACK}:
                weighted.append((typing.cast("int | float", width), i))

        # drop columns on the left until we fit
        for i, width_ in enumerate(widths):
            if shared >= 0:
                break
            shared += width_ + self.dividechars
            widths[i] = 0
            if weighted and weighted[0][1] == i:
                del weighted[0]

        grow = shared + len(weighted) * self.min_width
        if weighted and (max_weight := max(weight for weight, _i in weighted)) > sys.float_info.max / (
            len(weighted) * max(grow, 1)
        ):
            # grow * wtotal could overflow a float: scale weights to at most 1, keeping their ratios
            weighted = [(weight / max_weight, i) for weight, i in weighted]

        wtotal = sum(weight for weight, _i in weighted)
        if shared and wtotal:
            # divide up the remaining space between weighted cols
            for weight, i in sorted(weighted):
                width = max(int(grow * weight / wtotal + 0.5), self.min_width)

                widths[i] = width
                grow -= width
                wtotal -= weight

        self._cache_maxcol = maxcol
        self._cache_column_widths = widths
        self._cache_pack_widths = pack_widths
        return widths

    def _get_fixed_column_sizes(
        self,
        focus: bool = False,
    ) -> tuple[tuple[int, ...], tuple[int, ...], tuple[tuple[int, int] | tuple[int] | tuple[()], ...]]:
        """Get column widths, heights and render size parameters.

        :raises ColumnsError: a child widget does not support a sizing mode this Columns needs, no child can provide
            a height, or the weights are too far apart to keep their ratio at a fixed size.
        """
        widths: dict[int, int] = {}
        heights: dict[int, int] = {}
        w_h_args: dict[int, tuple[int, int] | tuple[int] | tuple[()]] = {}
        box: list[int] = []
        weighted: dict[int | float, list[tuple[AbstractWidget, int, bool, bool]]] = {}
        weights: list[int | float] = []
        weight_max_sizes: dict[int | float, int] = {}

        for i, (widget, (size_kind, size_weight, is_box)) in enumerate(self.contents):
            w_sizing = widget.sizing()
            focused = focus and i == self.focus_position

            if size_kind == WHSettings.GIVEN:
                size_weight = typing.cast("int", size_weight)
                widths[i] = size_weight
                if is_box:
                    box.append(i)
                elif Sizing.FLOW in w_sizing:
                    heights[i] = typing.cast("AbstractFlowWidget", widget).rows((size_weight,), focused)
                    w_h_args[i] = (size_weight,)
                else:
                    raise ColumnsError(f"Unsupported combination of {size_kind} box={is_box!r} for {widget}")

            elif size_kind == WHSettings.PACK and Sizing.FIXED in w_sizing and not is_box:
                width, height = typing.cast("AbstractFixedWidget", widget).pack((), focused)
                widths[i] = width
                heights[i] = height
                w_h_args[i] = ()

            elif typing.cast("int | float", size_weight) <= 0:
                widths[i] = 0
                heights[i] = 1
                if is_box:
                    box.append(i)
                else:
                    w_h_args[i] = (0,)

            elif Sizing.FLOW in w_sizing or is_box:
                if Sizing.FIXED in w_sizing:
                    width, height = typing.cast("AbstractFixedWidget", widget).pack((), focused)
                else:
                    width = self.min_width

                size_weight = typing.cast("int | float", size_weight)

                weighted.setdefault(size_weight, []).append((widget, i, is_box, focused))
                weights.append(size_weight)
                weight_max_sizes.setdefault(size_weight, width)
                weight_max_sizes[size_weight] = max(weight_max_sizes[size_weight], width)
            else:
                raise ColumnsError(f"Unsupported combination of {size_kind} box={is_box!r} for {widget}")

        if weight_max_sizes:
            max_weighted_coefficient = max(width / weight for weight, width in weight_max_sizes.items())

            for weight in weight_max_sizes:
                try:
                    width = max(int(max_weighted_coefficient * weight + 0.5), self.min_width)
                except OverflowError as exc:
                    raise ColumnsError(
                        f"Weights of {self!r} are too far apart to keep their ratio at a fixed size"
                    ) from exc
                for widget, i, is_box, focused in weighted[weight]:
                    widths[i] = width

                    if not is_box:
                        heights[i] = typing.cast("AbstractFlowWidget", widget).rows((width,), focused)
                        w_h_args[i] = (width,)
                    else:
                        box.append(i)

        if not heights:
            raise ColumnsError(f"No height information for pack {self!r} as FIXED")

        max_height = max(heights.values())
        for idx in box:
            heights[idx] = max_height
            w_h_args[idx] = (widths[idx], max_height)

        return (
            tuple(widths[idx] for idx in range(len(widths))),
            tuple(heights[idx] for idx in range(len(heights))),
            tuple(w_h_args[idx] for idx in range(len(w_h_args))),
        )

    def get_column_sizes(
        self,
        size: tuple[int, int] | tuple[int] | tuple[()],
        focus: bool = False,
    ) -> tuple[tuple[int, ...], tuple[int, ...], tuple[tuple[int, int] | tuple[int] | tuple[()], ...]]:
        """Get column widths, heights and render size parameters.

        :raises ColumnsError: *size* is ``()`` and the children cannot be sized as a fixed widget.
        """
        if not size:
            return self._get_fixed_column_sizes(focus=focus)

        widths = tuple(self.column_widths(size=size, focus=focus))
        # Every index gets a height and a render size argument before returning, either in the loop below
        # or in the box fixup after it, so plain lists are used instead of index-keyed dicts.
        heights: list[int] = [0] * len(widths)
        w_h_args: list[tuple[int, int] | tuple[int] | tuple[()]] = [()] * len(widths)
        box: list[int] = []
        box_need_height: list[int] = []
        # -1 marks "no height was calculated in the loop", which a plain 0 cannot: a zero-width column
        # stores a real height of 0.
        max_height = -1
        focus_position = self.focus_position if focus and self.contents else -1

        for i, (width, (widget, (size_kind, _size_weight, is_box))) in enumerate(zip(widths, self.contents)):
            if isinstance(widget, AbstractWidget):
                w_sizing = widget.sizing()
            else:
                warnings.warn(f"{widget!r} is not a Widget.", ColumnsWarning, stacklevel=3)
                # This branch should be fully deleted later.
                w_sizing = frozenset((Sizing.FLOW, Sizing.BOX))

            if len(size) == 2 and Sizing.BOX in w_sizing:
                height = size[1]
                w_h_args[i] = (width, height)

            elif is_box:
                box.append(i)
                continue

            elif Sizing.FLOW in w_sizing:
                if width > 0:
                    height = typing.cast("AbstractFlowWidget", widget).rows((width,), i == focus_position)
                else:
                    height = 0
                w_h_args[i] = (width,)

            elif size_kind == WHSettings.PACK:
                if width > 0:
                    height = typing.cast("AbstractFixedWidget", widget).pack((), i == focus_position)[1]
                else:
                    height = 0
                w_h_args[i] = ()

            else:
                box_need_height.append(i)
                continue

            heights[i] = height
            max_height = max(max_height, height)

        if len(size) == 2:
            max_height = size[1]
        elif max_height < 0:
            max_height = 1
        elif box_need_height:
            warnings.warn(
                f"Widgets in columns {box_need_height} "
                f"({[self.contents[i][0] for i in box_need_height]}) "
                f'are BOX widgets not marked "box_columns" while FLOW render is requested (size={size!r})',
                ColumnsWarning,
                stacklevel=3,
            )

        for idx in (*box, *box_need_height):
            heights[idx] = max_height
            w_h_args[idx] = (widths[idx], max_height)

        return (widths, tuple(heights), tuple(w_h_args))

    def pack(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int] = (),
        focus: bool = False,
    ) -> tuple[int, int]:
        """Get packed size for widget.

        :raises ColumnsError: *size* is ``()`` and the children cannot be sized as a fixed widget.
        """
        if size:
            return super().pack(size, focus)
        widths, heights, _ = self.get_column_sizes(size, focus)
        return (sum(widths) + self.dividechars * max(len(widths) - 1, 0), max(heights))

    def render(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
        focus: bool = False,
    ) -> SolidCanvas | CompositeCanvas:
        """
        Render columns and return canvas.

        :param size: see :meth:`Widget.render` for details
        :param focus: ``True`` if this widget is in focus
        :raises ColumnsError: the Columns has no visible column to render, or *size* is ``()`` and the children cannot
            be sized as a fixed widget.
        """
        widths, _, size_args = self.get_column_sizes(size, focus)

        data: list[tuple[Canvas, int, bool, int]] = []
        for i, (width, w_size, (w, _)) in enumerate(zip(widths, size_args, self.contents)):
            # if the widget has a width of 0, hide it
            if width <= 0:
                continue

            if i < len(widths) - 1:
                width += self.dividechars  # noqa: PLW2901
            data.append(
                (
                    # widget kind and size arguments matched separately
                    w.render(w_size, focus=focus and self.focus_position == i),  # type: ignore[arg-type]
                    i,
                    self.focus_position == i,
                    width,
                )
            )

        if not data:
            if size:
                return SolidCanvas(" ", size[0], (*size[1:], 1)[0])
            raise ColumnsError("No data to render")

        canvas = CanvasJoin(data)
        if size and canvas.cols() < size[0]:
            canvas.pad_trim_left_right(0, size[0] - canvas.cols())
        return canvas

    def get_cursor_coords(self, size: tuple[()] | tuple[int] | tuple[int, int]) -> tuple[int, int] | None:
        """Return the cursor coordinates from the focus widget.

        :raises ColumnsError: *size* is ``()`` and the children cannot be sized as a fixed widget.
        """
        w, _ = self.contents[self.focus_position]

        if not w.selectable():
            return None
        if not hasattr(w, "get_cursor_coords"):
            return None

        widths, _, size_args = self.get_column_sizes(size, focus=True)
        if len(widths) <= self.focus_position:
            return None

        if (coords := w.get_cursor_coords(size_args[self.focus_position])) is not None:
            x, y = coords
            x += sum(self.dividechars + wc for wc in widths[: self.focus_position] if wc > 0)
            return x, y

        return None

    def move_cursor_to_coords(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
        col: int | Literal["left", "right", Align.LEFT, Align.RIGHT],
        row: int,
    ) -> bool:
        """
        Choose a selectable column to focus based on the coords.

        see :meth:`Widget.move_cursor_coords` for details

        :raises ValueError: the column sizes cannot be computed for *size*, for example *size* is ``()`` and the
            children cannot be sized as a fixed widget.
        """
        try:
            widths, _, size_args = self.get_column_sizes(size, focus=True)
        except Exception as exc:
            raise ValueError(self.contents, size, col, row) from exc

        best: tuple[int, int, int, AbstractWidget] | None = None
        x = 0
        for i, (width, (w, _options)) in enumerate(zip(widths, self.contents)):
            end: int = x + width
            if w.selectable():
                if col != Align.RIGHT and (col == Align.LEFT or x > col) and best is None:  # type: ignore[operator]
                    # no other choice
                    best = i, x, end, w
                    break
                if col != Align.RIGHT and x > col and col - best[2] < x - col:  # type: ignore[index,operator]
                    # choose one on left
                    break
                best = i, x, end, w
                if col != Align.RIGHT and col < end:  # type: ignore[operator]
                    # choose this one
                    break
            x = end + self.dividechars

        if best is None:
            return False

        i, x, end, w = best
        if hasattr(w, "move_cursor_to_coords"):
            if isinstance(col, int):
                move_x: int | Literal["left", "right", Align.LEFT, Align.RIGHT] = min(max(0, col - x), end - x - 1)
            else:
                move_x = col

            if w.move_cursor_to_coords(size_args[i], move_x, row) is False:
                return False

            # The child may have changed this Columns' contents while moving its cursor.
            if i >= len(self.contents) or self.contents[i][0] is not w:
                return False

        self.focus_position = i
        self.pref_col = col
        return True

    def mouse_event(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
        event: str,
        button: int,
        col: int,
        row: int,
        focus: bool,
    ) -> bool | None:
        """Send event to appropriate column. May change focus on button 1 press.

        :raises ColumnsError: *size* is ``()`` and the children cannot be sized as a fixed widget.
        """
        widths, _, size_args = self.get_column_sizes(size, focus=focus)

        x = 0
        for i, (width, w_size, (w, _)) in enumerate(zip(widths, size_args, self.contents)):
            if col < x:
                return False
            w = self.contents[i][0]  # noqa: PLW2901
            end = x + width

            if col >= end:
                x = end + self.dividechars
                continue

            focus = focus and self.focus_position == i
            if is_mouse_press(event) and button == 1 and w.selectable():
                self.focus_position = i

            if not hasattr(w, "mouse_event"):
                warnings.warn(
                    f"{w.__class__.__module__}.{w.__class__.__name__} is not implementing Widget API",
                    DeprecationWarning,
                    stacklevel=2,
                )
                return False

            # widget kind and size arguments matched separately
            return w.mouse_event(w_size, event, button, col - x, row, focus)  # type: ignore[arg-type]
        return False

    def get_pref_col(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
    ) -> Literal["left", "right", Align.LEFT, Align.RIGHT] | int | None:
        """Return the pref col from the column in focus.

        :raises ColumnsError: *size* is ``()`` and the children cannot be sized as a fixed widget.
        """
        widths, _, size_args = self.get_column_sizes(size, focus=True)

        w, _ = self.contents[self.focus_position]
        if len(widths) <= self.focus_position:
            return 0
        col = None
        cwidth = widths[self.focus_position]

        if hasattr(w, "get_pref_col"):
            col = w.get_pref_col(size_args[self.focus_position])
            if isinstance(col, int):
                col += self.focus_position * self.dividechars
                col += sum(widths[: self.focus_position])
        if col is None:
            col = self.pref_col

        if col is None and w.selectable():
            col = cwidth // 2
            col += self.focus_position * self.dividechars
            col += sum(widths[: self.focus_position])
        return typing.cast("Literal['left', 'right', Align.LEFT, Align.RIGHT] | int | None", col)

    def rows(self, size: tuple[int], focus: bool = False) -> int:
        """Return the number of rows required by the columns.

        This only makes sense if :attr:`widget_list` contains flow widgets.

        see :meth:`Widget.rows` for details
        """
        _, heights, _ = self.get_column_sizes(size, focus)
        if heights:
            return max(1, *heights)
        return 1

    def keypress(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
        key: str,
    ) -> str | None:
        """
        Pass keypress to the focus column.

        :param size: Widget size correct for the supported sizing
        :param key: a single keystroke value
        :raises ColumnsError: *size* is ``()`` and the children cannot be sized as a fixed widget.
        """
        if self.focus_position is None:
            return key

        widths, _, size_args = self.get_column_sizes(size, focus=True)
        if self.focus_position >= len(widths):
            return key

        i = self.focus_position
        w, _ = self.contents[i]
        if self._command_map[key] not in {Command.UP, Command.DOWN, Command.PAGE_UP, Command.PAGE_DOWN}:
            self.pref_col = None
        if w.selectable():
            # widget kind and size arguments matched separately
            if (processed := w.keypress(size_args[i], key)) is not None:  # type: ignore[arg-type]
                key = processed
            else:
                return None

        if self._command_map[key] not in {Command.LEFT, Command.RIGHT} or not self.contents:
            return key

        # The child may have changed this Columns' contents or focus while handling the key.
        i = self.focus_position
        if self._command_map[key] == Command.LEFT:
            candidates = list(range(i - 1, -1, -1))  # count backwards to 0
        else:  # key == 'right'
            candidates = list(range(i + 1, len(self.contents)))

        for j in candidates:
            if not self.contents[j][0].selectable():
                continue

            self.focus_position = j
            return None
        return key
urwid-urwid-69907e4/urwid/widget/constants.py000066400000000000000000000406001526073734300213020ustar00rootroot00000000000000"""Constants and enumerations for widget sizing, alignment and wrapping."""

from __future__ import annotations

import dataclasses
import enum
import typing

if typing.TYPE_CHECKING:
    from typing_extensions import Literal


# define some names for these constants to avoid misspellings in the source
# and to document the constant strings we are using


class Sizing(str, enum.Enum):
    """Widget sizing methods."""

    FLOW = "flow"
    BOX = "box"
    FIXED = "fixed"


class Align(str, enum.Enum):
    """Text alignment modes."""

    LEFT = "left"
    RIGHT = "right"
    CENTER = "center"


class VAlign(str, enum.Enum):
    """Filler alignment."""

    TOP = "top"
    MIDDLE = "middle"
    BOTTOM = "bottom"


class WrapMode(str, enum.Enum):
    """Text wrapping modes."""

    SPACE = "space"
    ANY = "any"
    CLIP = "clip"
    ELLIPSIS = "ellipsis"


class WHSettings(str, enum.Enum):
    """Width and Height settings."""

    PACK = "pack"
    GIVEN = "given"
    RELATIVE = "relative"
    WEIGHT = "weight"
    CLIP = "clip"  # Used as "given" for widgets with fixed width (with clipping part of it)
    FLOW = "flow"  # Used as pack for flow widgets


RELATIVE_100: tuple[Literal[WHSettings.RELATIVE], int] = (WHSettings.RELATIVE, 100)


@typing.overload
def normalize_align(
    align: Literal["left", "center", "right"] | Align,
    err: type[BaseException],
) -> tuple[Align, None]: ...


@typing.overload
def normalize_align(
    align: tuple[Literal["relative", WHSettings.RELATIVE], int],
    err: type[BaseException],
) -> tuple[Literal[WHSettings.RELATIVE], int]: ...


def normalize_align(
    align: Literal["left", "center", "right"] | Align | tuple[Literal["relative", WHSettings.RELATIVE], int],
    err: type[BaseException],
) -> tuple[Align, None] | tuple[Literal[WHSettings.RELATIVE], int]:
    """Split align into (align_type, align_amount).

    Raise exception err if align doesn't match a valid alignment.

    :raises err: *align* is not a valid alignment; the class to raise is supplied by the caller.
    """
    if align in {Align.LEFT, Align.CENTER, Align.RIGHT}:
        return (Align(align), None)

    if isinstance(align, tuple) and len(align) == 2 and align[0] == WHSettings.RELATIVE:
        _align_type, align_amount = align
        return (WHSettings.RELATIVE, align_amount)

    raise err(
        f"align value {align!r} is not one of 'left', 'center', 'right', ('relative', percentage 0=left 100=right)"
    )


@typing.overload
def simplify_align(
    align_type: Literal["relative", WHSettings.RELATIVE],
    align_amount: int,
) -> tuple[Literal[WHSettings.RELATIVE], int]: ...


@typing.overload
def simplify_align(
    align_type: Literal["relative", WHSettings.RELATIVE],
    align_amount: None,
) -> typing.NoReturn: ...


@typing.overload
def simplify_align(
    align_type: Literal["left", "center", "right"] | Align,
    align_amount: int | None,
) -> Align: ...


def simplify_align(
    align_type: Literal["left", "center", "right", "relative", WHSettings.RELATIVE] | Align,
    align_amount: int | None,
) -> Align | tuple[Literal[WHSettings.RELATIVE], int]:
    """Recombine (align_type, align_amount) into an align value.

    Inverse of normalize_align.

    :raises TypeError: *align_amount* is not an integer.
    """
    if align_type == WHSettings.RELATIVE:
        if not isinstance(align_amount, int):
            raise TypeError(align_amount)

        return (WHSettings.RELATIVE, align_amount)
    return Align(align_type)


@typing.overload
def normalize_valign(
    valign: Literal["top", "middle", "bottom"] | VAlign,
    err: type[BaseException],
) -> tuple[VAlign, None]: ...


@typing.overload
def normalize_valign(
    valign: tuple[Literal["relative", WHSettings.RELATIVE], int],
    err: type[BaseException],
) -> tuple[Literal[WHSettings.RELATIVE], int]: ...


def normalize_valign(
    valign: Literal["top", "middle", "bottom"] | VAlign | tuple[Literal["relative", WHSettings.RELATIVE], int],
    err: type[BaseException],
) -> tuple[VAlign, None] | tuple[Literal[WHSettings.RELATIVE], int]:
    """Split align into (valign_type, valign_amount).

    Raise exception err if align doesn't match a valid alignment.

    :raises err: *valign* is not a valid vertical alignment; the class to raise is supplied by the caller.
    """
    if valign in {VAlign.TOP, VAlign.MIDDLE, VAlign.BOTTOM}:
        return (VAlign(valign), None)

    if isinstance(valign, tuple) and len(valign) == 2 and valign[0] == WHSettings.RELATIVE:
        _valign_type, valign_amount = valign
        return (WHSettings.RELATIVE, valign_amount)

    raise err(
        f"valign value {valign!r} is not one of 'top', 'middle', 'bottom', ('relative', percentage 0=left 100=right)"
    )


@typing.overload
def simplify_valign(
    valign_type: Literal["top", "middle", "bottom"] | VAlign,
    valign_amount: int | None,
) -> VAlign: ...


@typing.overload
def simplify_valign(
    valign_type: Literal["relative", WHSettings.RELATIVE],
    valign_amount: int,
) -> tuple[Literal[WHSettings.RELATIVE], int]: ...


@typing.overload
def simplify_valign(
    valign_type: Literal["relative", WHSettings.RELATIVE],
    valign_amount: None,
) -> typing.NoReturn: ...


def simplify_valign(
    valign_type: Literal["top", "middle", "bottom", "relative", WHSettings.RELATIVE] | VAlign,
    valign_amount: int | None,
) -> VAlign | tuple[Literal[WHSettings.RELATIVE], int]:
    """Recombine (valign_type, valign_amount) into an valign value.

    Inverse of normalize_valign.

    :raises TypeError: *valign_amount* is not an integer.
    """
    if valign_type == WHSettings.RELATIVE:
        if not isinstance(valign_amount, int):
            raise TypeError(valign_amount)
        return (WHSettings.RELATIVE, valign_amount)
    return VAlign(valign_type)


@typing.overload
def normalize_width(
    width: Literal["clip", "pack", WHSettings.CLIP, WHSettings.PACK],
    err: type[BaseException],
) -> tuple[Literal[WHSettings.CLIP, WHSettings.PACK], None]: ...


@typing.overload
def normalize_width(
    width: int,
    err: type[BaseException],
) -> tuple[Literal[WHSettings.GIVEN], int]: ...


@typing.overload
def normalize_width(
    width: tuple[Literal["relative", WHSettings.RELATIVE], int],
    err: type[BaseException],
) -> tuple[Literal[WHSettings.RELATIVE], int]: ...


@typing.overload
def normalize_width(
    width: tuple[Literal["weight", WHSettings.WEIGHT], int | float],
    err: type[BaseException],
) -> tuple[Literal[WHSettings.WEIGHT], int | float]: ...


def normalize_width(
    width: (
        Literal["clip", "pack", WHSettings.CLIP, WHSettings.PACK]
        | int
        | tuple[Literal["relative", WHSettings.RELATIVE], int]
        | tuple[Literal["weight", WHSettings.WEIGHT], int | float]
    ),
    err: type[BaseException],
) -> (
    tuple[Literal[WHSettings.CLIP, WHSettings.PACK], None]
    | tuple[Literal[WHSettings.GIVEN, WHSettings.RELATIVE], int]
    | tuple[Literal[WHSettings.WEIGHT], int | float]
):
    """Split width into (width_type, width_amount).

    Raise exception err if width doesn't match a valid alignment.

    :raises err: *width* is not a valid width; the class to raise is supplied by the caller.
    """
    if width in {WHSettings.CLIP, WHSettings.PACK}:
        return (typing.cast("Literal[WHSettings.CLIP, WHSettings.PACK]", WHSettings(width)), None)

    if isinstance(width, int):
        return (WHSettings.GIVEN, width)

    if isinstance(width, tuple) and len(width) == 2 and width[0] in {WHSettings.RELATIVE, WHSettings.WEIGHT}:
        width_type, width_amount = width
        return typing.cast(
            "tuple[Literal[WHSettings.RELATIVE], int] | tuple[Literal[WHSettings.WEIGHT], int | float]",
            (WHSettings(width_type), width_amount),
        )

    raise err(
        f"width value {width!r} is not one of"
        f"fixed number of columns, 'pack', ('relative', percentage of total width), 'clip'"
    )


@typing.overload
def simplify_width(
    width_type: Literal["clip", "pack", WHSettings.CLIP, WHSettings.PACK],
    width_amount: int | None,
) -> Literal[WHSettings.CLIP, WHSettings.PACK]: ...


@typing.overload
def simplify_width(
    width_type: Literal["given", WHSettings.GIVEN],
    width_amount: int,
) -> int: ...


@typing.overload
def simplify_width(
    width_type: Literal["relative", WHSettings.RELATIVE],
    width_amount: int,
) -> tuple[Literal[WHSettings.RELATIVE], int]: ...


@typing.overload
def simplify_width(
    width_type: Literal["weight", WHSettings.WEIGHT],
    width_amount: int | float,  # noqa: PYI041  # provide explicit for IDEs
) -> tuple[Literal[WHSettings.WEIGHT], int | float]: ...


@typing.overload
def simplify_width(
    width_type: Literal["given", "relative", "weight", WHSettings.GIVEN, WHSettings.RELATIVE, WHSettings.WEIGHT],
    width_amount: None,
) -> typing.NoReturn: ...


def simplify_width(
    width_type: Literal["clip", "pack", "given", "relative", "weight"] | WHSettings,
    width_amount: int | float | None,  # noqa: PYI041  # provide explicit for IDEs
) -> (
    Literal[WHSettings.CLIP, WHSettings.PACK]
    | int
    | tuple[Literal[WHSettings.RELATIVE], int]
    | tuple[Literal[WHSettings.WEIGHT], int | float]
):
    """Recombine (width_type, width_amount) into an width value.

    Inverse of normalize_width.

    :raises TypeError: *width_amount* is not an integer.
    """
    if width_type in {WHSettings.CLIP, WHSettings.PACK}:
        return typing.cast("Literal[WHSettings.CLIP, WHSettings.PACK]", WHSettings(width_type))

    if not isinstance(width_amount, int):
        raise TypeError(width_amount)

    if width_type == WHSettings.GIVEN:
        return width_amount

    return typing.cast(
        "tuple[Literal[WHSettings.RELATIVE], int] | tuple[Literal[WHSettings.WEIGHT], int | float]",
        (WHSettings(width_type), width_amount),
    )


@typing.overload
def normalize_height(
    height: int,
    err: type[BaseException],
) -> tuple[Literal[WHSettings.GIVEN], int]: ...


@typing.overload
def normalize_height(
    height: Literal["flow", "pack", WHSettings.FLOW, WHSettings.PACK],
    err: type[BaseException],
) -> tuple[Literal[WHSettings.FLOW, WHSettings.PACK], None]: ...


@typing.overload
def normalize_height(
    height: tuple[Literal["relative", WHSettings.RELATIVE], int],
    err: type[BaseException],
) -> tuple[Literal[WHSettings.RELATIVE], int]: ...


@typing.overload
def normalize_height(
    height: tuple[Literal["weight", WHSettings.WEIGHT], int | float],
    err: type[BaseException],
) -> tuple[Literal[WHSettings.WEIGHT], int | float]: ...


def normalize_height(
    height: (
        int
        | Literal["flow", "pack", WHSettings.FLOW, WHSettings.PACK]
        | tuple[Literal["relative", WHSettings.RELATIVE], int]
        | tuple[Literal["weight", WHSettings.WEIGHT], int | float]
    ),
    err: type[BaseException],
) -> (
    tuple[Literal[WHSettings.FLOW, WHSettings.PACK], None]
    | tuple[Literal[WHSettings.RELATIVE, WHSettings.GIVEN], int]
    | tuple[Literal[WHSettings.WEIGHT], int | float]
):
    """Split height into (height_type, height_amount).

    Raise exception err if height isn't valid.

    :raises err: *height* is not a valid height; the class to raise is supplied by the caller.
    """
    if height == WHSettings.FLOW:
        return (WHSettings.FLOW, None)

    if height == WHSettings.PACK:
        return (WHSettings.PACK, None)

    if isinstance(height, tuple) and len(height) == 2 and height[0] in {WHSettings.RELATIVE, WHSettings.WEIGHT}:
        return typing.cast(
            "tuple[Literal[WHSettings.RELATIVE], int] | tuple[Literal[WHSettings.WEIGHT], int | float]",
            (WHSettings(height[0]), height[1]),
        )

    if isinstance(height, int):
        return (WHSettings.GIVEN, height)

    raise err(
        f"height value {height!r} is not one of "
        f"fixed number of columns, 'pack', ('relative', percentage of total height)"
    )


@typing.overload
def simplify_height(
    height_type: Literal["flow", "pack", WHSettings.FLOW, WHSettings.PACK],
    height_amount: int | None,
) -> Literal[WHSettings.FLOW, WHSettings.PACK]: ...


@typing.overload
def simplify_height(
    height_type: Literal["given", WHSettings.GIVEN],
    height_amount: int,
) -> int: ...


@typing.overload
def simplify_height(
    height_type: Literal["relative", WHSettings.RELATIVE],
    height_amount: int | None,
) -> tuple[Literal[WHSettings.RELATIVE], int]: ...


@typing.overload
def simplify_height(
    height_type: Literal["weight", WHSettings.WEIGHT],
    height_amount: int | float | None,  # noqa: PYI041  # provide explicit for IDEs
) -> tuple[Literal[WHSettings.WEIGHT], int | float]: ...


@typing.overload
def simplify_height(
    height_type: Literal["relative", "given", "weight", WHSettings.RELATIVE, WHSettings.GIVEN, WHSettings.WEIGHT],
    height_amount: None,
) -> typing.NoReturn: ...


def simplify_height(
    height_type: Literal[
        "flow",
        "pack",
        "relative",
        "given",
        "weight",
        WHSettings.FLOW,
        WHSettings.PACK,
        WHSettings.RELATIVE,
        WHSettings.GIVEN,
        WHSettings.WEIGHT,
    ],
    height_amount: int | float | None,  # noqa: PYI041  # provide explicit for IDEs
) -> (
    int
    | Literal[WHSettings.FLOW, WHSettings.PACK]
    | tuple[Literal[WHSettings.RELATIVE], int]
    | tuple[Literal[WHSettings.WEIGHT], int | float]
):
    """Recombine (height_type, height_amount) into a height value.

    Inverse of normalize_height.

    :raises TypeError: *height_amount* is not an integer.
    """
    if height_type in {WHSettings.FLOW, WHSettings.PACK}:
        return typing.cast("Literal[WHSettings.FLOW, WHSettings.PACK]", WHSettings(height_type))

    if not isinstance(height_amount, int):
        raise TypeError(height_amount)

    if height_type == WHSettings.GIVEN:
        return height_amount

    return typing.cast(
        "tuple[Literal[WHSettings.RELATIVE], int] | tuple[Literal[WHSettings.WEIGHT], int | float]",
        (WHSettings(height_type), height_amount),
    )


@dataclasses.dataclass(frozen=True)
class _BoxSymbols:
    """Box symbols for drawing."""

    HORIZONTAL: str
    VERTICAL: str
    TOP_LEFT: str
    TOP_RIGHT: str
    BOTTOM_LEFT: str
    BOTTOM_RIGHT: str
    # Joints for tables making
    LEFT_T: str
    RIGHT_T: str
    TOP_T: str
    BOTTOM_T: str
    CROSS: str


@dataclasses.dataclass(frozen=True)
class _BoxSymbolsWithDashes(_BoxSymbols):
    """Box symbols for drawing.

    Extra dashes symbols.
    """

    HORIZONTAL_4_DASHES: str
    HORIZONTAL_3_DASHES: str
    HORIZONTAL_2_DASHES: str
    VERTICAL_2_DASH: str
    VERTICAL_3_DASH: str
    VERTICAL_4_DASH: str


@dataclasses.dataclass(frozen=True)
class _LightBoxSymbols(_BoxSymbolsWithDashes):
    """Box symbols for drawing.

    The Thin version includes extra symbols.
    Symbols are ordered as in Unicode except dashes.
    """

    TOP_LEFT_ROUNDED: str
    TOP_RIGHT_ROUNDED: str
    BOTTOM_LEFT_ROUNDED: str
    BOTTOM_RIGHT_ROUNDED: str


class _BoxSymbolsCollection(typing.NamedTuple):
    """Standard Unicode box symbols for basic tables drawing.

    .. note::
        Transitions are not included: depends on line types, different kinds of transitions are available.
        Please check Unicode table for transitions symbols if required.
    """

    # fmt: off

    LIGHT: _LightBoxSymbols = _LightBoxSymbols(
        "─", "│", "┌", "�", "└", "┘", "├", "┤", "┬", "┴", "┼", "┈", "┄", "╌", "╎", "┆", "┊", "╭", "╮", "╰", "╯"
    )
    HEAVY: _BoxSymbolsWithDashes = _BoxSymbolsWithDashes(
        "�", "┃", "�", "┓", "┗", "┛", "┣", "┫", "┳", "┻", "╋", "┉", "┅", "�", "�", "┇", "┋"
    )
    DOUBLE: _BoxSymbols = _BoxSymbols(
        "�", "║", "╔", "╗", "╚", "�", "╠", "╣", "╦", "╩", "╬"
    )


BOX_SYMBOLS = _BoxSymbolsCollection()


class BAR_SYMBOLS(str, enum.Enum):
    """Standard Unicode bar symbols excluding empty space.

    Start from space (0), then 1/8 till full block (1/1).
    Typically used only 8 from this symbol collection depends on use-case:
    * empty - 7/8 and styles for BG different on both sides (like standard `ProgressBar` and `BarGraph`)
    * 1/8 - full block and single style for BG on the right side
    """

    # fmt: off

    HORISONTAL = " �▎�▌▋▊▉█"
    VERTICAL =   " �▂▃▄▅▆▇█"


class _SHADE_SYMBOLS(typing.NamedTuple):
    """Standard shade symbols excluding empty space."""

    # fmt: off

    FULL_BLOCK: str =   "â–ˆ"
    DARK_SHADE: str =   "â–“"
    MEDIUM_SHADE: str = "â–’"
    LITE_SHADE: str =   "â–‘"


SHADE_SYMBOLS = _SHADE_SYMBOLS()
urwid-urwid-69907e4/urwid/widget/container.py000066400000000000000000000166241526073734300212610ustar00rootroot00000000000000"""Mixins that give container widgets their common API."""

from __future__ import annotations

import abc
import enum
import typing

from .constants import Sizing, WHSettings
from .widget import AbstractWidget

if typing.TYPE_CHECKING:
    from collections.abc import Iterable, Iterator, MutableSequence, Sequence

    _KT_contra = typing.TypeVar("_KT_contra", contravariant=True)

    class WidgetContainerProto(typing.Protocol[_KT_contra]):
        """Structural type for a container's ``contents`` mapping, indexable by position."""

        def __getitem__(
            self,
            index: _KT_contra,
        ) -> tuple[AbstractWidget, typing.Any]:
            """Return the `(widget, options)` entry at `index`."""

    class WidgetContainerMixinProto(AbstractWidget, typing.Protocol[_KT_contra]):
        """Structural type for a widget exposing ``contents`` and a settable ``focus_position``."""

        @property
        def contents(self) -> WidgetContainerProto[_KT_contra]:
            """Return the container's contents, indexable by position."""

        @property
        def focus_position(self) -> int | str:
            """Return the position of the container's currently focused element."""

        @focus_position.setter
        def focus_position(self, position: int | str) -> None: ...

else:
    _KT_contra = typing.TypeVar("_KT_contra", contravariant=True)

    class WidgetContainerMixinProto(typing.Protocol[_KT_contra]):
        """Generic protocol support."""


_ContentsItem = typing.TypeVar("_ContentsItem", bound=tuple[AbstractWidget, typing.Any])


class _ContainerElementSizingFlag(enum.IntEnum):
    """Bitfield describing the sizing modes and the width/height setting a container element supports.

    .. note::
        Ideally this would be an :class:`enum.IntFlag` coupled with :func:`enum.auto`, but doing many bitwise
        operations on an ``IntFlag`` - which happens when widgets are deeply nested - is orders of magnitude
        slower than doing the same operations on an :class:`enum.IntEnum`.
    """

    # fmt: off
    NONE      = 0b000000
    BOX       = 0b000001
    FLOW      = 0b000010
    FIXED     = 0b000100
    WH_WEIGHT = 0b001000
    WH_PACK   = 0b010000
    WH_GIVEN  = 0b100000
    # fmt: on

    @staticmethod
    def reverse_flag(bitfield: int) -> tuple[frozenset[Sizing], WHSettings | None]:
        """Get flag in public API format."""
        sizing: set[Sizing] = set()

        if bitfield & _ContainerElementSizingFlag.BOX:
            sizing.add(Sizing.BOX)
        if bitfield & _ContainerElementSizingFlag.FLOW:
            sizing.add(Sizing.FLOW)
        if bitfield & _ContainerElementSizingFlag.FIXED:
            sizing.add(Sizing.FIXED)

        if bitfield & _ContainerElementSizingFlag.WH_WEIGHT:
            return frozenset(sizing), WHSettings.WEIGHT
        if bitfield & _ContainerElementSizingFlag.WH_PACK:
            return frozenset(sizing), WHSettings.PACK
        if bitfield & _ContainerElementSizingFlag.WH_GIVEN:
            return frozenset(sizing), WHSettings.GIVEN
        return frozenset(sizing), None

    @staticmethod
    def log_string(bitfield: int) -> str:
        """Get desctiprion in public API format."""
        sizing, render = _ContainerElementSizingFlag.reverse_flag(bitfield)
        render_string = f" {render.upper()}" if render else ""
        return "|".join(sorted(mode.upper() for mode in sizing)) + render_string


class WidgetContainerMixin(WidgetContainerMixinProto[_KT_contra]):
    """Mixin class for widget containers implementing common container methods."""

    def __getitem__(self, position: _KT_contra) -> AbstractWidget:
        """Give the child widget at position without any widget decorations.

        This is a short-cut for self.contents[position][0].base_widget, allowing for concise traversal of nested
        container widgets such as:

            my_widget[position0][position1][position2] ...
        """
        return self.contents[position][0].base_widget

    def get_focus_path(self) -> list[int | str]:
        """Return the .focus_position values starting from this container.

        Proceeds along each child widget until reaching a leaf (non-container) widget.
        """
        out: list[int | str] = []
        w = self
        while True:
            try:
                p = w.focus_position
            except IndexError:
                return out
            out.append(p)
            w = w.focus.base_widget  # type: ignore[union-attr,assignment]

    def set_focus_path(self, positions: Iterable[int | str]) -> None:
        """Set the .focus_position property starting from this container widget.

        Proceeds along newly focused child widgets. Any failed assignment due to incompatible position types or
        invalid positions will raise an IndexError.

        This method may be used to restore a particular widget to the
        focus by passing in the value returned from an earlier call to
        get_focus_path().

        :param positions: sequence of positions
        """
        w: WidgetContainerMixin[typing.Any] | AbstractWidget = self
        for p in positions:
            if p != w.focus_position:  # type: ignore[union-attr]
                w.focus_position = p  # type: ignore[union-attr]  # modifies w.focus
            w = w.focus.base_widget  # type: ignore[union-attr]

    def get_focus_widgets(self) -> list[WidgetContainerMixin[typing.Any] | AbstractWidget]:
        """Return the .focus values starting from this container.

        Proceeds along each child widget until reaching a leaf (non-container) widget.

        Note that the list does not contain the topmost container widget
        (i.e., on which this method is called), but does include the
        lowest leaf widget.
        """
        out = []
        w = self
        while (w := w.base_widget.focus) is not None:  # type: ignore[assignment]
            out.append(w)

        return out  # type: ignore[return-value]

    @property
    @abc.abstractmethod
    def focus(self) -> AbstractWidget | None:
        """Read-only property returning the child widget in focus for container widgets.

        This default implementation always returns ``None``, indicating that this widget has no children.
        """


class WidgetContainerListContentsMixin(typing.Generic[_ContentsItem]):
    """Mixin class for widget containers whose positions are indexes into a list available as self.contents."""

    def __iter__(self) -> Iterator[int]:
        """Return an iterable of positions for this container from first to last."""
        return iter(range(len(self.contents)))

    def __reversed__(self) -> Iterator[int]:
        """Return an iterable of positions for this container from last to first."""
        return iter(range(len(self.contents) - 1, -1, -1))

    def __len__(self) -> int:
        """Return the number of items in this container."""
        return len(self.contents)

    @property
    @abc.abstractmethod
    def contents(self) -> MutableSequence[_ContentsItem]:
        """Return the contents of container as a list of (widget, options)."""

    @contents.setter
    def contents(self, new_contents: Sequence[_ContentsItem]) -> None:
        """Set the contents of container from a list of (widget, options)."""

    @property
    @abc.abstractmethod
    def focus_position(self) -> int | None:
        """Index of child widget in focus."""

    @focus_position.setter
    def focus_position(self, position: int) -> None:
        """Index of child widget in focus."""
urwid-urwid-69907e4/urwid/widget/deque_walker.py000066400000000000000000000215611526073734300217430ustar00rootroot00000000000000# Urwid DequeWalker classes
#    Copyright (C) 2026  urwid contributors
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/

"""List walkers backed by a monitored deque."""

from __future__ import annotations

import typing
from collections.abc import Iterable

from .list_walker import ListWalker, ListWalkerError
from .monitored_deque import MonitoredDeque, MonitoredFocusDeque

if typing.TYPE_CHECKING:
    from collections.abc import Callable

_T = typing.TypeVar("_T")

__all__ = ("SimpleDequeWalker", "SimpleFocusDequeWalker")


class SimpleDequeWalker(
    MonitoredDeque[_T],
    ListWalker[int, _T],
):
    """A :class:`ListWalker` backed by a :class:`MonitoredDeque`.

    Mirrors :class:`SimpleListWalker` method-for-method,
    adapted to ``deque``'s narrower API (in particular, ``maxlen``-bounded eviction).

    .. note::
        :meth:`ListWalker.get_focus`/:meth:`ListWalker.get_next`/:meth:`ListWalker.get_prev`
        (inherited, unmodified) index via ``self[position]``, and ``deque.__getitem__``
        is O(n) (linked-block structure) rather than a list's O(1)
        -- worth bearing in mind for a very large unbounded deque,
        though the bounded/``maxlen`` scrollback use case this class targets is small enough
        that it does not matter in practice. No caching layer is provided.
    """

    def __init__(self, contents: Iterable[_T], wrap_around: bool = False, maxlen: int | None = None) -> None:
        """Initialize a deque walker, which can be treated as a deque since it inherits :class:`MonitoredDeque`.

        Changes made to this object (when it is treated as a deque) are detected automatically
        and will cause ListBox objects using this list walker to be updated.

        :param contents: iterable to copy into this object
        :param wrap_around: if true, jumps to beginning/end of deque on move
        :param maxlen: if set, bounds the deque's length; the oldest items are silently evicted
            from the opposite end once full
        :raises ListWalkerError: *contents* is not iterable.
        """
        if not isinstance(contents, Iterable):
            raise ListWalkerError(f"SimpleDequeWalker expecting iterable object, got: {contents!r}")
        super().__init__(contents, maxlen)
        self.focus = 0
        self.wrap_around = wrap_around

    @property
    def contents(self) -> SimpleDequeWalker[_T]:
        """Return self.

        Provides compatibility with old SimpleListWalker class.
        """
        return self

    def _modified(self) -> None:
        if self.focus >= len(self):
            self.focus = max(0, len(self) - 1)
        ListWalker._modified(self)  # pylint: disable=protected-access

    def set_modified_callback(self, callback: Callable[[], typing.Any]) -> typing.NoReturn:
        """Raise :exc:`NotImplementedError`: this function inherited from MonitoredDeque is not implemented here.

        Use ``connect_signal(list_walker, "modified", ...)`` instead.

        :raises NotImplementedError: always
        """
        raise NotImplementedError('Use connect_signal(list_walker, "modified", ...) instead.')

    def set_focus(self, position: int) -> None:
        """Set focus position.

        :param position: position to focus
        :raises IndexError: if there is no widget at ``position``
        """
        if not 0 <= position < len(self):
            raise IndexError(f"No widget at position {position}")

        self.focus = position
        self._modified()

    def next_position(self, position: int) -> int:
        """Return position after ``position``.

        :param position: position to start from
        :raises IndexError: if there is no next position and ``wrap_around`` is false
        """
        if len(self) - 1 <= position:
            if self.wrap_around:
                return 0
            raise IndexError
        return position + 1

    def prev_position(self, position: int) -> int:
        """Return position before ``position``.

        :param position: position to start from
        :raises IndexError: if there is no previous position and ``wrap_around`` is false
        """
        if position <= 0:
            if self.wrap_around:
                return len(self) - 1
            raise IndexError
        return position - 1

    def positions(self, reverse: bool = False) -> Iterable[int]:
        """Return an iterable of positions. Optional method.

        :param reverse: if true, return positions in reverse order
        """
        if reverse:
            return range(len(self) - 1, -1, -1)
        return range(len(self))


class SimpleFocusDequeWalker(
    MonitoredFocusDeque[_T],
    ListWalker[typing.SupportsIndex, _T],
):
    """A :class:`ListWalker` backed by a :class:`MonitoredFocusDeque`.

    Mirrors :class:`SimpleFocusListWalker` method-for-method, adapted to
    ``deque``'s narrower API. See the performance caveat documented on
    :class:`SimpleDequeWalker` -- it applies equally here.
    """

    def __init__(self, contents: Iterable[_T], wrap_around: bool = False, maxlen: int | None = None) -> None:
        """Build the walker, wrapping *contents* in a focus-tracking deque.

        This class inherits :class:`MonitoredFocusDeque`, which means it can be treated as a deque.

        Changes made to this object (when it is treated as a deque) are detected automatically
        and will cause ListBox objects using this list walker to be updated.

        Also, items added or removed before the widget in focus with normal deque methods --
        including eviction caused by ``maxlen`` -- will cause the focus to be updated
        intelligently.

        :param contents: iterable to copy into this object
        :param wrap_around: if true, jumps to beginning/end of deque on move
        :param maxlen: if set, bounds the deque's length; the oldest items are silently evicted
            from the opposite end once full, and focus is adjusted to keep tracking a sensible
            item (see :class:`MonitoredFocusDeque`)
        :raises ListWalkerError: *contents* is not iterable.
        """
        if not isinstance(contents, Iterable):
            raise ListWalkerError(f"SimpleFocusDequeWalker expecting iterable object, got: {contents!r}")
        super().__init__(contents, maxlen)
        self.wrap_around = wrap_around

    def _modified(self) -> None:
        # Prefer ListWalker's signal emission over MonitoredDeque's callback hook.
        ListWalker._modified(self)  # pylint: disable=protected-access

    def set_modified_callback(self, callback: typing.Any) -> typing.NoReturn:
        """Raise :exc:`NotImplementedError`: this function inherited from MonitoredFocusDeque is not implemented here.

        Use ``connect_signal(list_walker, "modified", ...)`` instead.

        :raises NotImplementedError: always
        """
        raise NotImplementedError('Use connect_signal(list_walker, "modified", ...) instead.')

    def set_focus(self, position: int) -> None:
        """Set focus position.

        :param position: position to focus
        """
        self.focus = position
        self._modified()

    def next_position(self, position: typing.SupportsIndex) -> int:
        """Return position after ``position``.

        :param position: position to start from
        :raises IndexError: if there is no next position and ``wrap_around`` is false
        """
        pos = int(position)
        if len(self) - 1 <= pos:
            if self.wrap_around:
                return 0
            raise IndexError
        return pos + 1

    def prev_position(self, position: typing.SupportsIndex) -> int:
        """Return position before ``position``.

        :param position: position to start from
        :raises IndexError: if there is no previous position and ``wrap_around`` is false
        """
        pos = int(position)
        if pos <= 0:
            if self.wrap_around:
                return len(self) - 1
            raise IndexError
        return pos - 1

    def positions(self, reverse: bool = False) -> Iterable[int]:
        """Return an iterable of positions. Optional method.

        :param reverse: if true, return positions in reverse order
        """
        if reverse:
            return range(len(self) - 1, -1, -1)
        return range(len(self))
urwid-urwid-69907e4/urwid/widget/divider.py000066400000000000000000000061661526073734300207250ustar00rootroot00000000000000"""Divider: a horizontal line widget."""

from __future__ import annotations

import enum
import typing

from urwid.canvas import CompositeCanvas, SolidCanvas

from .constants import BOX_SYMBOLS, SHADE_SYMBOLS, Sizing
from .widget import Widget


class DividerSymbols(str, enum.Enum):
    """Common symbols for divider widgets."""

    # Lines
    LIGHT_HL = BOX_SYMBOLS.LIGHT.HORIZONTAL
    LIGHT_4_DASHES = BOX_SYMBOLS.LIGHT.HORIZONTAL_4_DASHES
    LIGHT_3_DASHES = BOX_SYMBOLS.LIGHT.HORIZONTAL_3_DASHES
    LIGHT_2_DASHES = BOX_SYMBOLS.LIGHT.HORIZONTAL_2_DASHES
    HEAVY_HL = BOX_SYMBOLS.HEAVY.HORIZONTAL
    HEAVY_4_DASHES = BOX_SYMBOLS.HEAVY.HORIZONTAL_4_DASHES
    HEAVY_3_DASHES = BOX_SYMBOLS.HEAVY.HORIZONTAL_3_DASHES
    HEAVY_2_DASHES = BOX_SYMBOLS.HEAVY.HORIZONTAL_2_DASHES
    DOUBLE_HL = BOX_SYMBOLS.DOUBLE.HORIZONTAL

    # Full block
    FULL_BLOCK = SHADE_SYMBOLS.FULL_BLOCK
    DARK_SHADE = SHADE_SYMBOLS.DARK_SHADE
    MEDIUM_SHADE = SHADE_SYMBOLS.MEDIUM_SHADE
    LITE_SHADE = SHADE_SYMBOLS.LITE_SHADE


class Divider(Widget):
    """Horizontal divider widget."""

    Symbols = DividerSymbols

    _sizing = frozenset([Sizing.FLOW])

    ignore_focus = True

    def __init__(
        self,
        div_char: str | bytes = " ",
        top: int = 0,
        bottom: int = 0,
    ) -> None:
        """
        :param div_char: character to repeat across line

        :param top: number of blank lines above

        :param bottom: number of blank lines below

        >>> Divider()
        
        >>> Divider("-")
        
        >>> Divider("x", 1, 2)
        
        """
        super().__init__()
        self.div_char = div_char
        self.top = top
        self.bottom = bottom

    def _repr_words(self) -> list[str]:
        return super()._repr_words() + [repr(self.div_char)] * (self.div_char != " ")

    def _repr_attrs(self) -> dict[str, typing.Any]:
        attrs = dict(super()._repr_attrs())
        if self.top:
            attrs["top"] = self.top
        if self.bottom:
            attrs["bottom"] = self.bottom
        return attrs

    def rows(self, size: tuple[int], focus: bool = False) -> int:
        """
        Return the number of lines that will be rendered.

        >>> Divider().rows((10,))
        1
        >>> Divider("x", 1, 2).rows((10,))
        4
        """
        (_maxcol,) = size
        return self.top + 1 + self.bottom

    def render(
        self,
        size: tuple[int],  # type: ignore[override]
        focus: bool = False,
    ) -> CompositeCanvas:
        """
        Render the divider as a canvas and return it.

        >>> Divider().render((10,)).text  # ... = b in Python 3
        [...'          ']
        >>> Divider("-", top=1).render((10,)).text
        [...'          ', ...'----------']
        >>> Divider("x", bottom=2).render((5,)).text
        [...'xxxxx', ...'     ', ...'     ']
        """
        (maxcol,) = size
        canv = CompositeCanvas(SolidCanvas(self.div_char, maxcol, 1))
        if self.top or self.bottom:
            canv.pad_trim_top_bottom(self.top, self.bottom)
        return canv
urwid-urwid-69907e4/urwid/widget/edit.py000066400000000000000000000701471526073734300202240ustar00rootroot00000000000000"""Edit: a text editing widget, and IntEdit for integers."""

from __future__ import annotations

import string
import sys
import typing
import unicodedata

from urwid import text_layout
from urwid.canvas import CompositeCanvas, apply_text_layout
from urwid.command_map import Command
from urwid.split_repr import remove_defaults
from urwid.str_util import calc_width, is_wide_char, move_next_char, move_prev_char
from urwid.util import decompose_tagmarkup

from .constants import Align, Sizing, WrapMode
from .text import Text, TextError
from .widget import WidgetWrap

if typing.TYPE_CHECKING:
    from collections.abc import Hashable

    from typing_extensions import Literal

    from urwid.canvas import TextCanvas
    from urwid.util import _TagMarkup


class EditError(TextError):
    """Edit related errors."""


class Edit(WidgetWrap[Text]):
    """Text editing widget implements cursor movement, text insertion and deletion.

    A caption may prefix the editing area.  Uses text class for text layout.

    Users of this class may listen for ``"change"`` or ``"postchange"``
    events.  See :func:``connect_signal``.

    * ``"change"`` is sent just before the value of edit_text changes.
      It receives the new text as an argument.  Note that ``"change"`` cannot
      change the text in question as edit_text changes the text afterwards.
    * ``"postchange"`` is sent after the value of edit_text changes.
      It receives the old value of the text as an argument and thus is
      appropriate for changing the text.  It is possible for a ``"postchange"``
      event handler to get into a loop of changing the text and then being
      called when the event is re-emitted.  It is up to the event
      handler to guard against this case (for instance, by not changing the
      text if it is signaled for text that it has already changed once).
    """

    _sizing = frozenset([Sizing.FLOW])
    _selectable = True
    ignore_focus = False
    # (this variable is picked up by the MetaSignals metaclass)
    signals: typing.ClassVar[list[str]] = ["change", "postchange"]

    def valid_char(self, ch: str) -> bool:
        """
        Filter for text that may be entered into this widget by the user.

        :param ch: character to be inserted

        This implementation returns True for a wide character and for any single character that is not a control
        character (Unicode category ``Cc``): the displays print a control character as ``?`` while the width
        calculation counts it as zero columns. A single byte is accepted unless it is a C0 control or DEL, because
        0x80-0xff are printable characters in a single-byte encoding.
        """
        if is_wide_char(ch, 0):
            return True
        if len(ch) != 1:
            return False
        if isinstance(ch, str):
            return unicodedata.category(ch) != "Cc"
        return 32 <= ord(ch) != 0x7F

    def __init__(
        self,
        caption: _TagMarkup = "",
        edit_text: str = "",
        multiline: bool = False,
        align: Literal["left", "center", "right"] | Align = Align.LEFT,
        wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode = WrapMode.SPACE,
        allow_tab: bool = False,
        edit_pos: int | None = None,
        layout: text_layout.TextLayout | None = None,
        mask: str | None = None,
        *,
        expand_tabs: bool = True,
    ) -> None:
        """Create the Edit widget.

        :param caption: markup for caption preceding edit_text, see :class:`Text` for description of text markup.
        :param edit_text: initial text for editing, type (bytes or str) must match the text in the caption
        :param multiline: ``True``: :kbd:`enter` inserts a newline, ``False``: return it
        :param align: typically 'left', 'center' or 'right'
        :param wrap: typically 'space', 'any' or 'clip'
        :param allow_tab: ``True``: :kbd:`tab` is inserted, ``False``: return it
        :param edit_pos: initial position for cursor, None:end of edit_text
        :param layout: defaults to a shared :class:`StandardTextLayout` instance
        :param mask: hide text entered with this character, None:disable mask
        :param expand_tabs: ``True``: an inserted tab is spaces up to the next tab stop of the line,
            ``False``: a tab character

        >>> Edit()
        
        >>> Edit("Y/n? ", "yes")
        
        >>> Edit("Name ", "Smith", edit_pos=1)
        
        >>> Edit("", "3.14", align="right")
        
        """
        super().__init__(Text("", align, wrap, layout))
        self.multiline = multiline
        self.allow_tab = allow_tab
        self.expand_tabs = expand_tabs
        self._edit_pos = 0
        self._caption, self._attrib = decompose_tagmarkup(caption)
        self._edit_text = ""
        self.highlight: tuple[int, int] | None = None
        self._mask: str | None = None
        self._shift_view_to_cursor = False
        self.pref_col_maxcol: tuple[None, None] | tuple[int | Literal[Align.LEFT, Align.RIGHT], int] = (None, None)
        self.set_edit_text(edit_text)
        if edit_pos is None:
            edit_pos = len(edit_text)
        self.set_edit_pos(edit_pos)
        self.set_mask(mask)

    def selectable(self) -> bool:
        """Return True: this widget is always selectable.

        Wrapped Text widget is not selectable while the edit widget has to be selectable.
        """
        return True

    def sizing(self) -> frozenset[Sizing]:
        """Sizing information.

        Edit widget sizing is narrower than Text: we cannot predict the final input length.
        """
        return self._sizing

    def _repr_words(self) -> list[str]:
        return [
            *super()._repr_words(),
            repr(self._edit_text if self._mask is None else self._mask * len(self._edit_text)),
            *([f"caption={self._caption!r}"] if self._caption else []),
            *(["multiline"] if self.multiline is True else []),
        ]

    def _repr_attrs(self) -> dict[str, typing.Any]:
        attrs = {
            **super()._repr_attrs(),
            "align": self._w.align,
            "wrap": self._w.wrap,
            "edit_pos": self._edit_pos,
            "mask": self._mask,
        }
        return remove_defaults(attrs, Edit.__init__)

    def _sync_wrapped(self) -> None:
        """Synchronize the wrapped Text widget with current caption+edit_text state."""
        text, attrib = self.get_text()
        self._w._text = text  # pylint: disable=protected-access
        self._w._attrib = attrib  # pylint: disable=protected-access
        self._w._invalidate()

    def get_text(self) -> tuple[str | bytes, list[tuple[Hashable, int]]]:
        """Return ``(text, display attributes)``.

        See :meth:`Text.get_text` for details.

        Text returned includes the caption and edit_text, possibly masked.

        >>> Edit("What? ", "oh, nothing.").get_text()
        ('What? oh, nothing.', [])
        >>> Edit(("bright", "user@host:~$ "), "ls").get_text()
        ('user@host:~$ ls', [('bright', 13)])
        >>> Edit("password:", "seekrit", mask="*").get_text()
        ('password:*******', [])
        """
        if self._mask is None:
            return self._caption + self._edit_text, self._attrib  # type: ignore[operator]  # type normalised

        return self._caption + (self._mask * len(self._edit_text)), self._attrib  # type: ignore[operator]

    @property
    def text(self) -> str | bytes:
        """Read-only property returning the complete bytes/unicode content of this widget.

        Includes caption plus edit_text, possibly masked.
        """
        return self.get_text()[0]

    @property
    def attrib(self) -> list[tuple[Hashable, int]]:
        """Read-only property returning the run-length encoded display attributes of this widget."""
        return self.get_text()[1]

    def set_align_mode(self, mode: Literal["left", "center", "right"] | Align) -> None:
        """Set text alignment mode. See :meth:`Text.set_align_mode` for details."""
        self._w.set_align_mode(mode)
        self._invalidate()

    def set_wrap_mode(self, mode: Literal["space", "any", "clip", "ellipsis"] | WrapMode) -> None:
        """Set text wrapping mode. See :meth:`Text.set_wrap_mode` for details."""
        self._w.set_wrap_mode(mode)
        self._invalidate()

    def set_layout(
        self,
        align: Literal["left", "center", "right"] | Align,
        wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode,
        layout: text_layout.TextLayout | None = None,
    ) -> None:
        """Set the text layout object, alignment and wrapping modes at the same time.

        See :meth:`Text.set_layout` for details.
        """
        self._w.set_layout(align, wrap, layout)
        self._invalidate()

    align = property(lambda self: self._w.align, set_align_mode)
    wrap = property(lambda self: self._w.wrap, set_wrap_mode)

    @property
    def layout(self) -> text_layout.TextLayout:
        """Return the :class:`TextLayout` instance used to lay out and wrap this widget's text."""
        return self._w.layout

    def get_pref_col(self, size: tuple[int]) -> int:
        r"""Return the preferred column for the cursor, or the current cursor x value.

        May also return ``'left'`` or ``'right'`` to indicate the leftmost or rightmost column available.

        This method is used internally and by other widgets when
        moving the cursor up or down between widgets so that the
        column selected is one that the user would expect.

        >>> size = (10,)
        >>> Edit().get_pref_col(size)
        0
        >>> e = Edit("", "word")
        >>> e.get_pref_col(size)
        4
        >>> e.keypress(size, "left")
        >>> e.get_pref_col(size)
        3
        >>> e.keypress(size, "end")
        >>> e.get_pref_col(size)
        
        >>> e = Edit("", "2\nwords")
        >>> e.keypress(size, "left")
        >>> e.keypress(size, "up")
        >>> e.get_pref_col(size)
        4
        >>> e.keypress(size, "left")
        >>> e.get_pref_col(size)
        0
        """
        (maxcol,) = size
        pref_col, then_maxcol = self.pref_col_maxcol
        if then_maxcol != maxcol:
            return self.get_cursor_coords((maxcol,))[0]

        return typing.cast("int", pref_col)

    def set_caption(self, caption: _TagMarkup) -> None:
        """
        Set the caption markup for this widget.

        :param caption: markup for caption preceding edit_text, see
                        :meth:`Text.__init__` for description of text markup.

        >>> e = Edit("")
        >>> e.set_caption("cap1")
        >>> print(e.caption)
        cap1
        >>> e.set_caption(("bold", "cap2"))
        >>> print(e.caption)
        cap2
        >>> e.attrib
        [('bold', 4)]
        >>> e.caption = "cap3"  # not supported because caption stores text but set_caption() takes markup
        Traceback (most recent call last):
        AttributeError: can't set attribute
        """
        self._caption, self._attrib = decompose_tagmarkup(caption)
        self._sync_wrapped()
        self._invalidate()

    @property
    def caption(self) -> str | bytes:
        """Read-only property returning the caption for this widget."""
        return self._caption

    def set_edit_pos(self, pos: int) -> None:
        """Set the cursor position with a self.edit_text offset.

        Clips pos to [0, len(edit_text)].

        :param pos: cursor position

        >>> e = Edit("", "word")
        >>> e.edit_pos
        4
        >>> e.set_edit_pos(2)
        >>> e.edit_pos
        2
        >>> e.edit_pos = -1  # Urwid 0.9.9 or later
        >>> e.edit_pos
        0
        >>> e.edit_pos = 20
        >>> e.edit_pos
        4
        """
        pos = min(max(pos, 0), len(self._edit_text))
        self.highlight = None
        self.pref_col_maxcol = None, None
        self._edit_pos = pos
        self._invalidate()

    edit_pos = property(
        lambda self: self._edit_pos,
        set_edit_pos,
        doc="""
        Property controlling the edit position for this widget.
        """,
    )

    def set_mask(self, mask: str | None) -> None:
        """
        Set the character for masking text away.

        :param mask: hide text entered with this character, None:disable mask
        :param expand_tabs: ``True``: an inserted tab is spaces up to the next tab stop of the line,
            ``False``: a tab character
        """
        self._mask = mask
        self._sync_wrapped()
        self._invalidate()

    def set_edit_text(self, text: str) -> None:
        """
        Set the edit text for this widget.

        :param text: text for editing, type (bytes or unicode)
                     must match the text in the caption

        >>> e = Edit()
        >>> e.set_edit_text("yes")
        >>> print(e.edit_text)
        yes
        >>> e
        
        >>> e.edit_text = "no"  # Urwid 0.9.9 or later
        >>> print(e.edit_text)
        no
        """
        text = self._normalize_to_caption(text)  # type: ignore[assignment]
        self.highlight = None
        self._emit("change", text)
        old_text = self._edit_text
        self._edit_text = text
        self._sync_wrapped()
        self.edit_pos = min(self.edit_pos, len(text))

        self._emit("postchange", old_text)
        self._invalidate()

    def get_edit_text(self) -> str:
        """
        Return the edit text for this widget.

        >>> e = Edit("What? ", "oh, nothing.")
        >>> print(e.get_edit_text())
        oh, nothing.
        >>> print(e.edit_text)
        oh, nothing.
        """
        return self._edit_text

    edit_text = property(
        get_edit_text,
        set_edit_text,
        doc="""
        Property controlling the edit text for this widget.
        """,
    )

    def insert_text(self, text: str) -> None:
        """Insert text at the cursor position and update cursor.

        This method is used by the keypress() method when inserting
        one or more characters into edit_text.

        :param text: text for inserting, type (bytes or unicode)
                     must match the text in the caption

        >>> e = Edit("", "42")
        >>> e.insert_text(".5")
        >>> e
        
        >>> e.set_edit_pos(2)
        >>> e.insert_text("a")
        >>> print(e.edit_text)
        42a.5
        """
        text = self._normalize_to_caption(text)  # type: ignore[assignment]
        result_text, result_pos = self.insert_text_result(text)
        self.set_edit_text(result_text)  # type: ignore[arg-type]  # normalisation happen
        self.set_edit_pos(result_pos)
        self.highlight = None

    def _normalize_to_caption(self, text: str | bytes) -> str | bytes:
        """Return text converted to the same type as self.caption (bytes or unicode)."""
        tu = isinstance(text, str)
        cu = isinstance(self._caption, str)
        if tu == cu:
            return text
        if tu:
            return text.encode("ascii")  # type: ignore[union-attr]  # follow python2's implicit conversion
        return text.decode("ascii")  # type: ignore[union-attr]

    def insert_text_result(self, text: str) -> tuple[str | bytes, int]:
        """Return result of insert_text(text) without actually performing the insertion.

        Handy for pre-validation.

        :param text: text for inserting, type (bytes or unicode)
                     must match the text in the caption
        :raises ValueError: *text* cannot be inserted at the current edit position. The message carries the texts
            involved unless a mask is set.
        """
        # if there's highlighted text, it'll get replaced by the new text
        text = self._normalize_to_caption(text)  # type: ignore[assignment]
        if self.highlight:
            start, stop = self.highlight
            btext, etext = self.edit_text[:start], self.edit_text[stop:]
            result_text = btext + etext
            result_pos = start
        else:
            result_text = self.edit_text
            result_pos = self.edit_pos

        try:
            result_text = result_text[:result_pos] + text + result_text[result_pos:]
        except (IndexError, TypeError) as exc:
            detail = repr((self.edit_text, result_text, text)) if self._mask is None else "masked text"
            raise ValueError(detail).with_traceback(exc.__traceback__) from exc

        result_pos += len(text)
        return (result_text, result_pos)

    def keypress(
        self,
        size: tuple[int],
        key: str,
    ) -> str | None:
        """
        Handle editing keystrokes, return others.

        >>> e, size = Edit(), (20,)
        >>> e.keypress(size, "x")
        >>> e.keypress(size, "left")
        >>> e.keypress(size, "1")
        >>> print(e.edit_text)
        1x
        >>> e.keypress(size, "backspace")
        >>> e.keypress(size, "end")
        >>> e.keypress(size, "2")
        >>> print(e.edit_text)
        x2
        >>> e.keypress(size, "shift f1")
        'shift f1'

        :raises ValueError: the preferred cursor column could not be determined.
        """
        pos = self.edit_pos
        if self.valid_char(key):
            if isinstance(key, str) and not isinstance(self._caption, str):
                key = key.encode("utf-8")  # type: ignore[assignment]
            self.insert_text(key)
            return None

        if key == "tab" and self.allow_tab:
            if not self.expand_tabs:
                self.insert_text("\t")
                return None
            layout = self._w.layout
            if not isinstance(layout, text_layout.StandardTextLayout):
                layout = text_layout.default_layout  # a custom layout has no tab stops
            text = self._edit_text
            tab, nl = ("\t", "\n") if isinstance(text, str) else (b"\t", b"\n")
            column = 0
            for idx, chunk in enumerate(text[text.rfind(nl, 0, pos) + 1 : pos].split(tab)):
                if idx:
                    column = layout.next_tab_stop(column)
                column += calc_width(chunk, 0, len(chunk))
            if spaces := layout.next_tab_stop(column) - column:
                self.insert_text(" " * spaces)
            return None

        if key == "enter" and self.multiline:
            key = "\n"
            self.insert_text(key)
            return None

        if self._command_map[key] == Command.LEFT:
            if pos == 0:
                return key
            pos = move_prev_char(self.edit_text, 0, pos)
            self.set_edit_pos(pos)
            return None

        if self._command_map[key] == Command.RIGHT:
            if pos >= len(self.edit_text):
                return key
            pos = move_next_char(self.edit_text, pos, len(self.edit_text))
            self.set_edit_pos(pos)
            return None

        if self._command_map[key] in {Command.UP, Command.DOWN}:
            self.highlight = None

            _x, y = self.get_cursor_coords(size)
            pref_col = self.get_pref_col(size)
            if pref_col is None:
                raise ValueError(pref_col)

            if self._command_map[key] == Command.UP:
                y -= 1
            else:
                y += 1

            if not self.move_cursor_to_coords(size, pref_col, y):
                return key
            return None

        if key == "backspace":
            self.pref_col_maxcol = None, None
            if not self._delete_highlighted():
                if pos == 0:
                    return key
                pos = move_prev_char(self.edit_text, 0, pos)
                self.set_edit_text(self.edit_text[:pos] + self.edit_text[self.edit_pos :])
                self.set_edit_pos(pos)
                return None
            return None

        if key == "delete":
            self.pref_col_maxcol = None, None
            if not self._delete_highlighted():
                if pos >= len(self.edit_text):
                    return key
                pos = move_next_char(self.edit_text, pos, len(self.edit_text))
                self.set_edit_text(self.edit_text[: self.edit_pos] + self.edit_text[pos:])
                return None
            return None

        if self._command_map[key] in {Command.MAX_LEFT, Command.MAX_RIGHT}:
            self.highlight = None
            self.pref_col_maxcol = None, None

            _x, y = self.get_cursor_coords(size)

            if self._command_map[key] == Command.MAX_LEFT:
                self.move_cursor_to_coords(size, Align.LEFT, y)
            else:
                self.move_cursor_to_coords(size, Align.RIGHT, y)
            return None

        # key wasn't handled
        return key

    def move_cursor_to_coords(
        self,
        size: tuple[int],
        x: int | Literal[Align.LEFT, Align.RIGHT],
        y: int,
    ) -> bool:
        r"""Set the cursor position with (x,y) coordinates.

        Returns True if move succeeded, False otherwise.

        >>> size = (10,)
        >>> e = Edit("", "edit\ntext")
        >>> e.move_cursor_to_coords(size, 5, 0)
        True
        >>> e.edit_pos
        4
        >>> e.move_cursor_to_coords(size, 5, 3)
        False
        >>> e.move_cursor_to_coords(size, 0, 1)
        True
        >>> e.edit_pos
        5
        """
        (maxcol,) = size
        trans = self.get_line_translation(maxcol)
        _top_x, top_y = self.position_coords(maxcol, 0)
        if y < top_y or y >= len(trans):
            return False

        pos = text_layout.calc_pos(self.get_text()[0], trans, x, y)
        e_pos = min(max(pos - len(self.caption), 0), len(self.edit_text))
        self.edit_pos = e_pos
        self.pref_col_maxcol = x, maxcol
        self._invalidate()
        return True

    def mouse_event(
        self,
        size: tuple[int],
        event: str,
        button: int,
        col: int,
        row: int,
        focus: bool,
    ) -> bool | None:
        """
        Move the cursor to the location clicked for button 1.

        >>> size = (20,)
        >>> e = Edit("", "words here")
        >>> e.mouse_event(size, "mouse press", 1, 2, 0, True)
        True
        >>> e.edit_pos
        2
        """
        if button == 1:
            return self.move_cursor_to_coords(size, col, row)
        return False

    def _delete_highlighted(self) -> bool:
        """Delete all highlighted text and update cursor position, if any text is highlighted."""
        if not self.highlight:
            return False
        start, stop = self.highlight
        btext, etext = self.edit_text[:start], self.edit_text[stop:]
        self.set_edit_text(btext + etext)
        self.edit_pos = start
        self.highlight = None
        return True

    def render(
        self,
        size: tuple[int],
        focus: bool = False,
    ) -> TextCanvas | CompositeCanvas:
        """Render edit widget and return canvas.

        Include cursor when in focus.

        >>> edit = Edit("? ", "yes")
        >>> c = edit.render((10,), focus=True)
        >>> c.text
        [b'? yes     ']
        >>> c.cursor
        (5, 0)
        """
        self._shift_view_to_cursor = bool(focus)

        (maxcol,) = size
        text, attr = self.get_text()
        trans = self.get_line_translation(maxcol, (text, attr))
        canv: TextCanvas | CompositeCanvas = apply_text_layout(text, attr, trans, maxcol)

        if focus:
            canv = CompositeCanvas(canv)
            canv.cursor = self.get_cursor_coords(size)

        # .. will need to FIXME if I want highlight to work again
        # if self.highlight:
        #    hstart, hstop = self.highlight_coords()
        #    d.coords['highlight'] = [ hstart, hstop ]
        return canv

    def get_line_translation(
        self,
        maxcol: int,
        ta: tuple[str | bytes, list[tuple[Hashable, int]]] | None = None,
    ) -> list[list[tuple[int, int, int | bytes] | tuple[int, int | None]]]:
        """Return the line translation for `ta`, shifted to keep the cursor visible if needed."""
        trans = self._w.get_line_translation(maxcol, ta)
        if not self._shift_view_to_cursor:
            return trans

        if self._w.wrap in (WrapMode.SPACE, WrapMode.ANY):
            # Wrapping modes grow the number of rows instead of clipping a row's width,
            # so there is never a partially hidden row to scroll horizontally into view.
            return trans

        text, _ignore = self.get_text()
        x, y = text_layout.calc_coords(text, trans, self.edit_pos + len(self.caption))
        if x < 0:
            return [
                *trans[:y],
                *[text_layout.shift_line(trans[y], -x)],
                *trans[y + 1 :],
            ]

        if x >= maxcol:
            return [
                *trans[:y],
                *[text_layout.shift_line(trans[y], -(x - maxcol + 1))],
                *trans[y + 1 :],
            ]

        return trans

    def get_cursor_coords(self, size: tuple[int]) -> tuple[int, int]:
        """
        Return the (*x*, *y*) coordinates of cursor within widget.

        >>> Edit("? ", "yes").get_cursor_coords((10,))
        (5, 0)
        """
        (maxcol,) = size

        self._shift_view_to_cursor = True
        x, y = self.position_coords(maxcol, self.edit_pos)
        if self._w.wrap in (WrapMode.SPACE, WrapMode.ANY) and x >= maxcol:
            # The cursor sits right after a row that is already full and entirely visible.
            # Clamp onto its last column instead of scrolling that row out of view for a wrap
            # that has not happened yet.
            x = maxcol - 1
        return x, y

    def position_coords(self, maxcol: int, pos: int) -> tuple[int, int]:
        """Return (*x*, *y*) coordinates for an offset into self.edit_text."""
        p = pos + len(self.caption)
        trans = self.get_line_translation(maxcol)
        x, y = text_layout.calc_coords(self.get_text()[0], trans, p)
        return x, y


class IntEdit(Edit):
    """Edit widget for integer values."""

    def valid_char(self, ch: str) -> bool:
        """Return true for decimal digits."""
        return len(ch) == 1 and ch in string.digits

    def __init__(self, caption: _TagMarkup = "", default: int | str | None = None) -> None:
        """Create the IntEdit widget.

        :param caption: caption markup
        :param default: default edit value

        >>> IntEdit("", 42)
        
        """
        if default is not None:
            val = str(default)
        else:
            val = ""
        super().__init__(caption, val)

    def keypress(
        self,
        size: tuple[int],
        key: str,
    ) -> str | None:
        """
        Handle editing keystrokes.  Remove leading zeros.

        A digit is refused when the resulting text would exceed :func:`sys.get_int_max_str_digits` digits, so that
        :meth:`value` can convert whatever the user typed.

        >>> e, size = IntEdit("", 5002), (10,)
        >>> e.keypress(size, "home")
        >>> e.keypress(size, "delete")
        >>> print(e.edit_text)
        002
        >>> e.keypress(size, "end")
        >>> print(e.edit_text)
        2
        """
        # 0 means no limit; the function is missing on Python 3.9 and 3.10 without the security backport.
        max_digits = getattr(sys, "get_int_max_str_digits", lambda: 0)()
        if max_digits and self.valid_char(key) and len(self.insert_text_result(key)[0]) > max_digits:
            return key

        if unhandled := super().keypress(size, key):
            return unhandled

        # trim leading zeros
        while self.edit_pos > 0 and self.edit_text[:1] == "0":
            self.set_edit_pos(self.edit_pos - 1)
            self.set_edit_text(self.edit_text[1:])

        return None

    def value(self) -> int:
        """
        Return the numeric value of self.edit_text.

        >>> e, size = IntEdit(), (10,)
        >>> e.keypress(size, "5")
        >>> e.keypress(size, "1")
        >>> e.value() == 51
        True

        :raises ValueError: the text was set programmatically to something that is not a decimal integer,
            or to more digits than :func:`sys.get_int_max_str_digits` allows.
        """
        if self.edit_text:
            return int(self.edit_text)

        return 0
urwid-urwid-69907e4/urwid/widget/filler.py000066400000000000000000000427631526073734300205570ustar00rootroot00000000000000"""Filler: a decoration that lets a flow widget be used as a box widget."""

from __future__ import annotations

import typing
import warnings

from urwid.canvas import CompositeCanvas
from urwid.split_repr import remove_defaults
from urwid.util import int_scale

from .constants import (
    RELATIVE_100,
    Sizing,
    VAlign,
    WHSettings,
    normalize_height,
    normalize_valign,
    simplify_height,
    simplify_valign,
)
from .widget_decoration import WidgetDecoration, WidgetError, WidgetWarning

if typing.TYPE_CHECKING:
    from typing_extensions import Literal

    from .widget import AbstractFlowWidget, AbstractWidget

WrappedWidget = typing.TypeVar("WrappedWidget", bound="AbstractWidget")


class FillerError(WidgetError):
    """Filler related errors."""


class FillerWarning(WidgetWarning):
    """Filler related warnings."""


class Filler(WidgetDecoration[WrappedWidget]):
    """Box widget decoration that vertically aligns a flow or box widget within the available space."""

    def __init__(
        self,
        body: WrappedWidget,
        valign: (
            Literal["top", "middle", "bottom"]
            | VAlign
            | tuple[Literal["relative", "fixed top", "fixed bottom", WHSettings.RELATIVE], int]
        ) = VAlign.MIDDLE,
        height: (
            int
            | Literal["pack", WHSettings.PACK]
            | tuple[Literal["relative", "fixed top", "fixed bottom", WHSettings.RELATIVE], int]
            | None
        ) = WHSettings.PACK,
        min_height: int | None = None,
        top: int = 0,
        bottom: int = 0,
    ) -> None:
        """
        :param body: a flow widget or box widget to be filled around (stored as self.original_widget)

        :param valign: one of:
            ``'top'``, ``'middle'``, ``'bottom'``,
            (``'relative'``, *percentage* 0=top 100=bottom)

        :param height: one of:

            ``'pack'``
              if body is a flow widget

            *given height*
              integer number of rows for self.original_widget

            (``'relative'``, *percentage of total height*)
              make height depend on container's height

        :param min_height: one of:

            ``None``
              if no minimum or if body is a flow widget

            *minimum height*
              integer number of rows for the widget when height not fixed

        :param top: a fixed number of rows to fill at the top
        :param bottom: a fixed number of rows to fill at the bottom
        :raises FillerError: *valign* is not a vertical alignment value, or a fixed height is combined with a valign it
            cannot be used with.

        If body is a flow widget, then height must be ``'pack'`` and *min_height* will be ignored.
        Sizing of the filler will be BOX/FLOW in this case.

        If height is integer, *min_height* will be ignored and sizing of filler will be BOX/FLOW.

        Filler widgets will try to satisfy height argument first by reducing the valign amount when necessary.
        If height still cannot be satisfied it will also be reduced.
        """
        super().__init__(body)

        # convert old parameters to the new top/bottom values
        normalized_height: int | Literal["pack", WHSettings.PACK] | tuple[Literal["relative", WHSettings.RELATIVE], int]
        if isinstance(height, tuple):
            if height[0] == "fixed top":
                if not isinstance(valign, tuple) or valign[0] != "fixed bottom":
                    raise FillerError("fixed top height may only be used with fixed bottom valign")
                top = height[1]
                normalized_height = RELATIVE_100
            elif height[0] == "fixed bottom":
                if not isinstance(valign, tuple) or valign[0] != "fixed top":
                    raise FillerError("fixed bottom height may only be used with fixed top valign")
                bottom = height[1]
                normalized_height = RELATIVE_100
            else:
                # 'fixed top'/'fixed bottom' handled above, so only the relative form remains.
                normalized_height = typing.cast("tuple[Literal['relative', WHSettings.RELATIVE], int]", height)

        # convert old flow mode parameters height=None and height='flow' to height='pack'
        elif height is None or height in {Sizing.FLOW, WHSettings.PACK}:  # 'pack' used to be called 'flow'
            normalized_height = WHSettings.PACK

        else:
            normalized_height = height

        normalized_valign: VAlign | tuple[Literal["relative", WHSettings.RELATIVE], int]
        if isinstance(valign, tuple):
            if valign[0] == "fixed top":
                top = valign[1]
                normalized_valign = VAlign.TOP
            elif valign[0] == "fixed bottom":
                bottom = valign[1]
                normalized_valign = VAlign.BOTTOM
            else:
                # 'fixed top'/'fixed bottom' handled above, so only the relative form remains.
                normalized_valign = typing.cast("tuple[Literal['relative', WHSettings.RELATIVE], int]", valign)

        elif not isinstance(valign, (VAlign, str)):
            raise FillerError(f"invalid valign: {valign!r}")

        else:
            normalized_valign = VAlign(valign)

        self.top = top
        self.bottom = bottom
        self.valign_type: Literal[WHSettings.RELATIVE] | VAlign
        self.valign_amount: int | None
        self.height_type: Literal[WHSettings.PACK, WHSettings.GIVEN, WHSettings.RELATIVE]
        self.height_amount: int | None
        self.valign_type, self.valign_amount = normalize_valign(normalized_valign, FillerError)
        # PACK is reported as FLOW|PACK by normalize_height, while FLOW is not a valid Filler height type
        self.height_type, self.height_amount = normalize_height(normalized_height, FillerError)  # type: ignore[assignment]

        if self.height_type not in {WHSettings.GIVEN, WHSettings.PACK}:
            self.min_height = min_height
        else:
            self.min_height = None

    def sizing(self) -> frozenset[Sizing]:
        """Widget sizing.

        Sizing BOX is always supported.
        Sizing FLOW is supported if: FLOW widget (a height type is PACK) or BOX widget with height GIVEN

        Rules:
        * height == PACK: the height is taken from the wrapped widget, which therefore should support FLOW
        """
        sizing: set[Sizing] = {Sizing.BOX}
        if self.height_type in {WHSettings.PACK, WHSettings.GIVEN}:
            sizing.add(Sizing.FLOW)

        if self.height_type == WHSettings.PACK:
            body = self.original_widget
            # A body without the "sizing" method is a legacy widget: it is handled by the render path as before.
            if hasattr(body, "sizing") and Sizing.FLOW not in body.sizing():
                warnings.warn(
                    f"WHSettings.PACK height expects a FLOW widget to be used, but received {body!r}",
                    FillerWarning,
                    stacklevel=3,
                )

        return frozenset(sizing)

    def rows(self, size: tuple[int], focus: bool = False) -> int:
        """Flow pack support if FLOW sizing supported.

        :raises FillerError: this Filler wraps a BOX widget, so it has no flow row count.
        """
        if self.height_type == WHSettings.PACK:
            return typing.cast("AbstractFlowWidget", self._original_widget).rows(size, focus) + self.top + self.bottom
        if self.height_type == WHSettings.GIVEN:
            return typing.cast("int", self.height_amount) + self.top + self.bottom
        raise FillerError("Method 'rows' not supported for BOX widgets")  # pragma: no cover

    def _repr_attrs(self) -> dict[str, typing.Any]:
        attrs = {
            **super()._repr_attrs(),
            "valign": simplify_valign(self.valign_type, self.valign_amount),
            "height": simplify_height(self.height_type, self.height_amount),
            "top": self.top,
            "bottom": self.bottom,
            "min_height": self.min_height,
        }
        return remove_defaults(attrs, Filler.__init__)

    @property
    def body(self) -> WrappedWidget:
        """
        The wrapped widget.

        .. deprecated:: 0.9.9
            The widget used to be stored as ``body``. Use :attr:`original_widget` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "backwards compatibility, widget used to be stored as body. API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.original_widget

    @body.setter
    def body(self, new_body: WrappedWidget) -> None:
        """
        Replace the wrapped widget.

        .. deprecated:: 0.9.9
            The widget used to be stored as ``body``. Use :attr:`original_widget` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "backwards compatibility, widget used to be stored as body. API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        self.original_widget = new_body

    def selectable(self) -> bool:
        """Return selectable from body."""
        return self._original_widget.selectable()

    def filler_values(self, size: tuple[int, int] | tuple[int], focus: bool) -> tuple[int, int]:
        """
        Return the number of rows to pad on the top and bottom.

        Override this method to define custom padding behaviour.
        """
        maxcol, maxrow = self.pack(size, focus)

        if self.height_type == WHSettings.PACK:
            height = typing.cast("AbstractFlowWidget", self._original_widget).rows((maxcol,), focus=focus)
            return calculate_top_bottom_filler(
                maxrow,
                self.valign_type,
                self.valign_amount,
                WHSettings.GIVEN,
                height,
                None,
                self.top,
                self.bottom,
            )

        return calculate_top_bottom_filler(
            maxrow,
            self.valign_type,
            self.valign_amount,
            self.height_type,
            typing.cast("int", self.height_amount),
            self.min_height,
            self.top,
            self.bottom,
        )

    def render(
        self,
        size: tuple[int, int] | tuple[int],  # type: ignore[override]
        focus: bool = False,
    ) -> CompositeCanvas:
        """Render self.original_widget with space above and/or below."""
        maxcol, maxrow = self.pack(size, focus)
        top, bottom = self.filler_values(size, focus)

        if self.height_type == WHSettings.PACK:
            canv = self._original_widget.render((maxcol,), focus)
        else:
            canv = self._original_widget.render((maxcol, maxrow - top - bottom), focus)
        canv = CompositeCanvas(canv)

        if maxrow and canv.rows() > maxrow and canv.cursor is not None:
            _cx, cy = canv.cursor
            if cy >= maxrow:
                canv.trim(cy - maxrow + 1, maxrow - top - bottom)
        if canv.rows() > maxrow:
            canv.trim(0, maxrow)
            return canv
        canv.pad_trim_top_bottom(top, bottom)
        return canv

    def keypress(
        self,
        size: tuple[int, int] | tuple[()],  # type: ignore[override]
        key: str,
    ) -> str | None:
        """Pass keypress to self.original_widget."""
        maxcol, maxrow = self.pack(size, True)
        if self.height_type == WHSettings.PACK:
            return self._original_widget.keypress((maxcol,), key)

        top, bottom = self.filler_values((maxcol, maxrow), True)
        return self._original_widget.keypress((maxcol, maxrow - top - bottom), key)

    def get_cursor_coords(self, size: tuple[int, int] | tuple[int]) -> tuple[int, int] | None:
        """Return cursor coords from self.original_widget if any."""
        maxcol, maxrow = self.pack(size, True)
        if not hasattr(self._original_widget, "get_cursor_coords"):
            return None

        top, bottom = self.filler_values(size, True)
        if self.height_type == WHSettings.PACK:
            coords = self._original_widget.get_cursor_coords((maxcol,))
        else:
            coords = self._original_widget.get_cursor_coords((maxcol, maxrow - top - bottom))
        if not coords:
            return None
        x, y = coords
        if y >= maxrow:
            y = maxrow - 1
        return x, y + top

    def get_pref_col(self, size: tuple[int, int] | tuple[int]) -> int | None:
        """Return pref_col from self.original_widget if any."""
        maxcol, maxrow = self.pack(size, True)
        if not hasattr(self._original_widget, "get_pref_col"):
            return None

        if self.height_type == WHSettings.PACK:
            x = typing.cast("int | None", self._original_widget.get_pref_col((maxcol,)))
        else:
            top, bottom = self.filler_values(size, True)
            x = typing.cast("int| None", self._original_widget.get_pref_col((maxcol, maxrow - top - bottom)))

        return x

    def move_cursor_to_coords(self, size: tuple[int, int] | tuple[int], col: int, row: int) -> bool:
        """Pass to self.original_widget."""
        maxcol, maxrow = self.pack(size, True)
        if not hasattr(self._original_widget, "move_cursor_to_coords"):
            return True

        top, bottom = self.filler_values(size, True)
        if row < top or row >= maxcol - bottom:
            return False

        if self.height_type == WHSettings.PACK:
            target_size: tuple[int] | tuple[int, int] = (maxcol,)
        else:
            target_size = (maxcol, maxrow - top - bottom)

        return typing.cast(
            "bool",
            self._original_widget.move_cursor_to_coords(target_size, col, row - top),
        )

    def mouse_event(
        self,
        size: tuple[int, int] | tuple[int],  # type: ignore[override]
        event: str,
        button: int,
        col: int,
        row: int,
        focus: bool,
    ) -> bool | None:
        """Pass to self.original_widget."""
        maxcol, maxrow = self.pack(size, focus)
        if not hasattr(self._original_widget, "mouse_event"):
            return False

        top, bottom = self.filler_values(size, True)
        if row < top or row >= maxrow - bottom:
            return False

        if self.height_type == WHSettings.PACK:
            return self._original_widget.mouse_event(
                (maxcol,),
                event,
                button,
                col,
                row - top,
                focus,
            )
        return self._original_widget.mouse_event(
            (maxcol, maxrow - top - bottom),
            event,
            button,
            col,
            row - top,
            focus,
        )


def calculate_top_bottom_filler(
    maxrow: int,
    valign_type: Literal["top", "middle", "bottom", "relative", WHSettings.RELATIVE] | VAlign,
    valign_amount: int | None,
    height_type: Literal["given", "relative", "clip", WHSettings.GIVEN, WHSettings.RELATIVE, WHSettings.CLIP],
    height_amount: int,
    min_height: int | None,
    top: int,
    bottom: int,
) -> tuple[int, int]:
    """Return the amount of filler (or clipping) on the top and bottom part of maxrow rows.

    :param valign_type: 'top', 'middle', 'bottom', 'relative'
    :param valign_amount: a percentage when align_type=='relative'
    :param height_type: 'given', 'relative', 'clip'
    :param height_amount: a percentage when width_type=='relative' otherwise equal to the height of the widget
    :param min_height: a desired minimum width for the widget or None
    :param top: a fixed number of rows to fill on the top
    :param bottom: a fixed number of rows to fill on the bottom
    :raises TypeError: *valign_type* is relative but no *valign_amount* was given.

    >>> ctbf = calculate_top_bottom_filler
    >>> ctbf(15, "top", 0, "given", 10, None, 2, 0)
    (2, 3)
    >>> ctbf(15, "relative", 0, "given", 10, None, 2, 0)
    (2, 3)
    >>> ctbf(15, "relative", 100, "given", 10, None, 2, 0)
    (5, 0)
    >>> ctbf(15, "middle", 0, "given", 4, None, 2, 0)
    (6, 5)
    >>> ctbf(15, "middle", 0, "given", 18, None, 2, 0)
    (0, 0)
    >>> ctbf(20, "top", 0, "relative", 60, None, 0, 0)
    (0, 8)
    >>> ctbf(20, "relative", 30, "relative", 60, None, 0, 0)
    (2, 6)
    >>> ctbf(20, "relative", 30, "relative", 60, 14, 0, 0)
    (2, 4)
    """
    if valign_type == WHSettings.RELATIVE and valign_amount is None:
        raise TypeError("valign_amount must be specified when valign_type is relative")

    if height_type == WHSettings.RELATIVE:
        maxheight = max(maxrow - top - bottom, 0)
        height = int_scale(height_amount, 101, maxheight + 1)
        if min_height is not None:
            height = max(height, min_height)
    else:
        height = height_amount

    valign: int = {VAlign.TOP: 0, VAlign.MIDDLE: 50, VAlign.BOTTOM: 100}.get(  # type: ignore[assignment]
        valign_type,  # type: ignore[arg-type]  # VAlign keys; relative uses valign_amount default
        valign_amount,  # type: ignore[arg-type]  # valign_amount is not None for relative
    )  # relative + None already filtered above

    # add the remainder of top/bottom to the filler
    filler = maxrow - height - top - bottom
    bottom += int_scale(100 - valign, 101, filler + 1)
    top = maxrow - height - bottom

    # reduce filler if we are clipping an edge
    if bottom < 0 < top:
        shift = min(top, -bottom)
        top -= shift
        bottom += shift
    elif top < 0 < bottom:
        shift = min(bottom, -top)
        bottom -= shift
        top += shift

    # no negative values for filler at the moment
    top = max(top, 0)
    bottom = max(bottom, 0)

    return top, bottom
urwid-urwid-69907e4/urwid/widget/frame.py000066400000000000000000000625611526073734300203720ustar00rootroot00000000000000"""Frame: a container with a body and an optional header and footer."""

from __future__ import annotations

import typing
import warnings
from collections.abc import MutableMapping

from typing_extensions import Literal

from urwid.canvas import CanvasCombine, CompositeCanvas
from urwid.split_repr import remove_defaults
from urwid.util import is_mouse_press

from .constants import Sizing, VAlign
from .container import WidgetContainerMixin
from .filler import Filler
from .widget import AbstractBoxWidget, AbstractFlowWidget, AbstractWidget, Widget, WidgetError

if typing.TYPE_CHECKING:
    from collections.abc import Iterator

    from urwid.canvas import Canvas


BodyWidget = typing.TypeVar("BodyWidget", bound=AbstractBoxWidget)
HeaderWidget = typing.TypeVar("HeaderWidget", bound=typing.Union[AbstractFlowWidget, None])
FooterWidget = typing.TypeVar("FooterWidget", bound=typing.Union[AbstractFlowWidget, None])


class FrameError(WidgetError):
    """Frame related errors."""


def _check_widget_subclass(widget: AbstractWidget | None) -> None:
    if widget is None:
        return

    if not isinstance(widget, AbstractWidget):
        obj_class_path = f"{widget.__class__.__module__}.{widget.__class__.__name__}"
        warnings.warn(
            f"{obj_class_path} is not implementing Widget API",
            DeprecationWarning,
            stacklevel=3,
        )


class Frame(
    Widget,
    WidgetContainerMixin[Literal["header", "footer", "body"]],
    typing.Generic[BodyWidget, HeaderWidget, FooterWidget],
):
    """Frame widget is a box widget with optional header and footer flow widgets placed above and below it.

    .. note:: The main difference between a Frame and a :class:`Pile` widget
        defined as: `Pile([('pack', header), body, ('pack', footer)])` is that
        the Frame will not automatically change focus up and down in response to keystrokes.
    """

    _selectable = True
    _sizing = frozenset([Sizing.BOX])

    def __init__(
        self,
        body: BodyWidget,
        header: HeaderWidget | None = None,
        footer: FooterWidget | None = None,
        focus_part: Literal["header", "footer", "body"] | AbstractWidget = "body",
    ):
        """Initialize a new Frame instance.

        :param body: a box widget for the body of the frame
        :param header: a flow widget for above the body (or None)
        :param footer: a flow widget for below the body (or None)
        :param focus_part:  'header', 'footer' or 'body'
        :raises ValueError: *focus_part* is not one of the three frame parts.
        """
        super().__init__()

        self._header = header
        self._body = body
        self._footer = footer
        if focus_part in {"header", "footer", "body"}:
            self.focus_part: Literal["header", "footer", "body"] = focus_part
        elif focus_part == header:
            self.focus_part = "header"
        elif focus_part == footer:
            self.focus_part = "footer"
        elif focus_part == body:
            self.focus_part = "body"
        else:
            raise ValueError(f"Invalid focus part {focus_part!r}")

        _check_widget_subclass(header)
        _check_widget_subclass(body)
        _check_widget_subclass(footer)

    def _repr_attrs(self) -> dict[str, typing.Any]:
        attrs = {
            **super()._repr_attrs(),
            "body": self._body,
            "header": self._header,
            "footer": self._footer,
            "focus_part": self.focus_part,
        }
        return remove_defaults(attrs, Frame.__init__)

    def __rich_repr__(self) -> Iterator[tuple[str | None, typing.Any] | typing.Any]:
        """Yield this widget's constructor arguments as `(name, value)` pairs, for `rich`'s repr protocol."""
        yield "body", self._body
        yield "header", self._header
        yield "footer", self._footer
        yield "focus_part", self.focus_part

    @property
    def header(self) -> HeaderWidget | None:
        """Return the header widget, or None if there is no header."""
        return self._header

    @header.setter
    def header(self, header: HeaderWidget | None) -> None:
        _check_widget_subclass(header)
        self._header = header
        if header is None and self.focus_part == "header":
            self.focus_part = "body"
        self._invalidate()

    def get_header(self) -> HeaderWidget | None:
        """
        Return the header widget.

        .. deprecated:: 2.2.0
            Use the standard property :attr:`header` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            f"method `{self.__class__.__name__}.get_header` is deprecated, "
            f"standard property `{self.__class__.__name__}.header` should be used instead. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.header

    def set_header(self, header: HeaderWidget | None) -> None:
        """
        Set the header widget.

        :param header: the new header widget

        .. deprecated:: 2.2.0
            Use the standard property :attr:`header` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            f"method `{self.__class__.__name__}.set_header` is deprecated, "
            f"standard property `{self.__class__.__name__}.header` should be used instead. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        self.header = header

    @property
    def body(self) -> BodyWidget:
        """Return the body widget."""
        return self._body

    @body.setter
    def body(self, body: BodyWidget) -> None:
        _check_widget_subclass(body)
        self._body = body
        self._invalidate()

    def get_body(self) -> BodyWidget:
        """
        Return the body widget.

        .. deprecated:: 2.2.0
            Use the standard property :attr:`body` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            f"method `{self.__class__.__name__}.get_body` is deprecated, "
            f"standard property {self.__class__.__name__}.body should be used instead. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.body

    def set_body(self, body: BodyWidget) -> None:
        """
        Set the body widget.

        :param body: the new body widget

        .. deprecated:: 2.2.0
            Use the standard property :attr:`body` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            f"method `{self.__class__.__name__}.set_body` is deprecated, "
            f"standard property `{self.__class__.__name__}.body` should be used instead. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        self.body = body

    @property
    def footer(self) -> FooterWidget | None:
        """Return the footer widget, or None if there is no footer."""
        return self._footer

    @footer.setter
    def footer(self, footer: FooterWidget | None) -> None:
        _check_widget_subclass(footer)
        self._footer = footer
        if footer is None and self.focus_part == "footer":
            self.focus_part = "body"
        self._invalidate()

    def get_footer(self) -> FooterWidget | None:
        """
        Return the footer widget.

        .. deprecated:: 2.2.0
            Use the standard property :attr:`footer` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            f"method `{self.__class__.__name__}.get_footer` is deprecated, "
            f"standard property `{self.__class__.__name__}.footer` should be used instead. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.footer

    def set_footer(self, footer: FooterWidget | None) -> None:
        """
        Set the footer widget.

        :param footer: the new footer widget

        .. deprecated:: 2.2.0
            Use the standard property :attr:`footer` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            f"method `{self.__class__.__name__}.set_footer` is deprecated, "
            f"standard property `{self.__class__.__name__}.footer` should be used instead. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        self.footer = footer

    @property
    def focus_position(self) -> Literal["header", "footer", "body"]:
        """Return which part of the frame is in focus: `'body', 'header'` or `'footer'`.

        :returns: one of 'header', 'footer' or 'body'.
        """
        return self.focus_part

    @focus_position.setter
    def focus_position(self, part: Literal["header", "footer", "body"]) -> None:
        """
        Determine which part of the frame is in focus.

        :param part: 'header', 'footer' or 'body'
        :raises IndexError: *part* is not one of the three frame parts, or names a part this Frame does not have.
        """
        if part not in {"header", "footer", "body"}:
            raise IndexError(f"Invalid position for Frame: {part}")
        if (part == "header" and self._header is None) or (part == "footer" and self._footer is None):
            raise IndexError(f"This Frame has no {part}")
        self.focus_part = part
        self._invalidate()

    def get_focus(self) -> Literal["header", "footer", "body"]:
        """Return which part of the frame is in focus: `'body', 'header'` or `'footer'`.

        :returns: one of 'header', 'footer' or 'body'.

        .. deprecated:: 1.1.0
            Use the container property :attr:`focus_position` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "included for backwards compatibility. "
            "You should rather use the container property `.focus_position` to get this value. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.focus_position

    def set_focus(self, part: Literal["header", "footer", "body"]) -> None:
        """
        Set the part of the frame that is in focus.

        :param part: one of 'header', 'footer' or 'body'

        .. deprecated:: 1.1.0
            Use the container property :attr:`focus_position` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "included for backwards compatibility. "
            "You should rather use the container property `.focus_position` to set this value. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        self.focus_position = part

    @property
    def focus(self) -> BodyWidget | HeaderWidget | FooterWidget:
        """Return the child :class:`Widget` in focus: the body, header or footer widget. Read-only."""
        return {  # type: ignore[return-value]
            "header": self._header,
            "footer": self._footer,
            "body": self._body,
        }[self.focus_part]

    @property
    def contents(  # type: ignore[override]
        self,
    ) -> MutableMapping[
        Literal["header", "footer", "body"],
        tuple[BodyWidget | HeaderWidget | FooterWidget, None],
    ]:
        """Return a dict-like object similar to the following.

        ::

            {
                "body": (body_widget, None),
                "header": (header_widget, None),  # if frame has a header
                "footer": (footer_widget, None),  # if frame has a footer
            }

        This object may be used to read or update the contents of the Frame.

        The values are similar to the list-like .contents objects used
        in other containers with (:class:`Widget`, options) tuples, but are
        constrained to keys for each of the three usual parts of a Frame.
        When other keys are used a :exc:`KeyError` will be raised.

        Currently, all options are `None`, but using the :meth:`options` method
        to create the options value is recommended for forwards
        compatibility.
        """

        # noinspection PyMethodParameters
        class FrameContents(
            MutableMapping[
                str,
                tuple[typing.Union[BodyWidget, HeaderWidget, FooterWidget], None],
            ]
        ):
            """Mapping view onto this Frame's `header`, `body`, and `footer` slots."""

            # pylint: disable=no-self-argument

            __slots__ = ()

            def __len__(inner_self) -> int:
                return len(self._contents_keys())

            __getitem__ = self._contents__getitem__  # type: ignore[assignment]
            __setitem__ = self._contents__setitem__  # type: ignore[assignment]
            __delitem__ = self._contents__delitem__  # type: ignore[assignment]

            def __iter__(inner_self) -> Iterator[str]:
                yield from self._contents_keys()

            def __repr__(inner_self) -> str:
                return f"<{inner_self.__class__.__name__}({dict(inner_self)}) for {self}>"  # type: ignore[misc]

            def __rich_repr__(inner_self) -> Iterator[tuple[str | None, typing.Any] | typing.Any]:
                yield from inner_self.items()

        return FrameContents()  # type: ignore[return-value]

    def _contents_keys(self) -> list[Literal["header", "footer", "body"]]:
        keys = ["body"]
        if self._header:
            keys.append("header")
        if self._footer:
            keys.append("footer")
        return keys  # type: ignore[return-value]

    @typing.overload
    def _contents__getitem__(self, key: Literal["body"]) -> tuple[BodyWidget, None]: ...

    @typing.overload
    def _contents__getitem__(self, key: Literal["header"]) -> tuple[HeaderWidget, None]: ...

    @typing.overload
    def _contents__getitem__(self, key: Literal["footer"]) -> tuple[FooterWidget, None]: ...

    def _contents__getitem__(
        self, key: Literal["body", "header", "footer"]
    ) -> tuple[BodyWidget | HeaderWidget | FooterWidget, None]:
        """
        Return the ``(widget, options)`` pair for *key*, for the container contents protocol.

        :raises KeyError: *key* is not one of the three frame parts.
        """
        if key == "body":
            return (self._body, None)
        if key == "header" and self._header:
            return (self._header, None)
        if key == "footer" and self._footer:
            return (self._footer, None)
        raise KeyError(f"Frame.contents has no key: {key!r}")

    @typing.overload
    def _contents__setitem__(self, key: Literal["body"], value: tuple[BodyWidget, None]) -> None: ...

    @typing.overload
    def _contents__setitem__(self, key: Literal["header"], value: tuple[HeaderWidget, None]) -> None: ...

    @typing.overload
    def _contents__setitem__(self, key: Literal["footer"], value: tuple[FooterWidget, None]) -> None: ...

    def _contents__setitem__(
        self,
        key: Literal["body", "header", "footer"],
        value: tuple[BodyWidget | HeaderWidget | FooterWidget, None],
    ) -> None:
        """
        Replace the ``(widget, options)`` pair for *key*, for the container contents protocol.

        :raises KeyError: *key* is not one of the three frame parts.
        :raises FrameError: *value* is not a ``(widget, None)`` pair.
        """
        if key not in {"body", "header", "footer"}:
            raise KeyError(f"Frame.contents has no key: {key!r}")
        try:
            value_w, value_options = value
            if value_options is not None:
                raise FrameError(f"added content invalid: {value!r}")
        except (ValueError, TypeError) as exc:
            raise FrameError(f"added content invalid: {value!r}").with_traceback(exc.__traceback__) from exc
        if key == "body":
            self.body = value_w  # type: ignore[assignment]
        elif key == "footer":
            self.footer = value_w  # type: ignore[assignment]
        else:
            self.header = value_w  # type: ignore[assignment]

    def _contents__delitem__(self, key: Literal["header", "footer"]) -> None:
        """
        Remove the header or footer, for the container contents protocol.

        :raises KeyError: *key* is not a removable part, or names a part this Frame does not have.
        """
        if key not in {"header", "footer"}:
            raise KeyError(f"Frame.contents can't remove key: {key!r}")
        if (key == "header" and self._header is None) or (key == "footer" and self._footer is None):
            raise KeyError(f"Frame.contents has no key: {key!r}")
        if key == "header":
            self.header = None
        else:
            self.footer = None

    def options(self) -> None:
        """
        There are currently no options for Frame contents.

        Return None as a placeholder for future options.
        """

    def frame_top_bottom(self, size: tuple[int, int], focus: bool) -> tuple[tuple[int, int], tuple[int, int]]:
        """
        Calculate the number of rows for the header and footer.

        :param size: See :meth:`Widget.render` for details
        :param focus: ``True`` if this widget is in focus
        :returns: `(head rows, foot rows),(orig head, orig foot)`
                  orig head/foot are from rows() calls.
        """
        (maxcol, maxrow) = size
        frows = hrows = 0

        if self.header:
            hrows = self.header.rows((maxcol,), self.focus_part == "header" and focus)  # type: ignore[union-attr]

        if self.footer:
            frows = self.footer.rows((maxcol,), self.focus_part == "footer" and focus)  # type: ignore[union-attr]

        remaining = maxrow

        if self.focus_part == "footer":
            if frows >= remaining:
                return (0, remaining), (hrows, frows)

            remaining -= frows
            if hrows >= remaining:
                return (remaining, frows), (hrows, frows)

        elif self.focus_part == "header":
            if hrows >= maxrow:
                return (remaining, 0), (hrows, frows)

            remaining -= hrows
            if frows >= remaining:
                return (hrows, remaining), (hrows, frows)

        elif hrows + frows >= remaining:
            # self.focus_part == 'body'
            rless1 = max(0, remaining - 1)
            if frows >= remaining - 1:
                return (0, rless1), (hrows, frows)

            remaining -= frows
            rless1 = max(0, remaining - 1)
            return (rless1, frows), (hrows, frows)

        return (hrows, frows), (hrows, frows)

    def render(
        self,
        size: tuple[int, int],  # type: ignore[override]
        focus: bool = False,
    ) -> CompositeCanvas:
        """
        Render the Frame and return the resulting canvas.

        :raises RuntimeError: the header or footer renders a different number of rows than it reported.
        """
        (maxcol, maxrow) = size
        (htrim, ftrim), (hrows, frows) = self.frame_top_bottom((maxcol, maxrow), focus)

        combinelist = []
        depends_on: list[BodyWidget | HeaderWidget | FooterWidget] = []

        head: Canvas | CompositeCanvas | None = None
        if htrim and htrim < hrows:
            head = Filler(self.header, VAlign.TOP).render(  # type: ignore[type-var]
                (maxcol, htrim),
                focus and self.focus_part == "header",
            )
        elif htrim:
            head = self.header.render(  # type: ignore[union-attr]
                (maxcol,),
                focus and self.focus_part == "header",
            )
            if head.rows() != hrows:
                raise RuntimeError("rows, render mismatch")
        if head:
            combinelist.append((head, "header", self.focus_part == "header"))
            depends_on.append(self.header)  # type: ignore[arg-type]

        if ftrim + htrim < maxrow:
            body = self.body.render(
                (maxcol, maxrow - ftrim - htrim),
                focus and self.focus_part == "body",
            )
            combinelist.append((body, "body", self.focus_part == "body"))
            depends_on.append(self.body)

        foot: Canvas | CompositeCanvas | None = None
        if ftrim and ftrim < frows:
            foot = Filler(self.footer, VAlign.BOTTOM).render(  # type: ignore[type-var]
                (maxcol, ftrim),
                focus and self.focus_part == "footer",
            )
        elif ftrim:
            foot = self.footer.render(  # type: ignore[union-attr]
                (maxcol,),
                focus and self.focus_part == "footer",
            )
            if foot.rows() != frows:
                raise RuntimeError("rows, render mismatch")
        if foot:
            combinelist.append((foot, "footer", self.focus_part == "footer"))
            depends_on.append(self.footer)  # type: ignore[arg-type]

        return CanvasCombine(combinelist)

    def keypress(
        self,
        size: tuple[int, int],  # type: ignore[override]
        key: str,
    ) -> str | None:
        """Pass keypress to widget in focus."""
        (maxcol, maxrow) = size

        if self.focus_part == "header" and self.header is not None:
            if not self.header.selectable():
                return key
            return self.header.keypress((maxcol,), key)
        if self.focus_part == "footer" and self.footer is not None:
            if not self.footer.selectable():
                return key
            return self.footer.keypress((maxcol,), key)
        if self.focus_part != "body":
            return key
        remaining = maxrow
        if self.header is not None:
            remaining -= self.header.rows((maxcol,))
        if self.footer is not None:
            remaining -= self.footer.rows((maxcol,))
        if remaining <= 0:
            return key

        if not self.body.selectable():
            return key
        return self.body.keypress((maxcol, remaining), key)

    def mouse_event(
        self,
        size: tuple[int, int],  # type: ignore[override]
        event: str,
        button: int,
        col: int,
        row: int,
        focus: bool,
    ) -> bool | None:
        """Pass mouse event to appropriate part of frame. Focus may be changed on button 1 press."""
        (maxcol, maxrow) = size
        (htrim, ftrim), (_hrows, _frows) = self.frame_top_bottom((maxcol, maxrow), focus)

        if row < htrim:  # within header
            focus = focus and self.focus_part == "header"
            if is_mouse_press(event) and button == 1 and self.header.selectable():  # type: ignore[union-attr]
                self.focus_position = "header"

            if (header_mouse_event := getattr(self.header, "mouse_event", None)) is not None:
                return typing.cast(
                    "bool | None",
                    header_mouse_event((maxcol,), event, button, col, row, focus),
                )

            return False

        if row >= maxrow - ftrim:  # within footer
            focus = focus and self.focus_part == "footer"
            if is_mouse_press(event) and button == 1 and self.footer.selectable():  # type: ignore[union-attr]
                self.focus_position = "footer"

            if (footer_mouse_event := getattr(self.footer, "mouse_event", None)) is not None:
                return typing.cast(
                    "bool | None",
                    footer_mouse_event((maxcol,), event, button, col, row - maxrow + ftrim, focus),
                )

            return False

        # within body
        focus = focus and self.focus_part == "body"
        if is_mouse_press(event) and button == 1 and self.body.selectable():
            self.focus_position = "body"

        if not hasattr(self.body, "mouse_event"):
            return False
        return self.body.mouse_event((maxcol, maxrow - htrim - ftrim), event, button, col, row - htrim, focus)

    def get_cursor_coords(self, size: tuple[int, int]) -> tuple[int, int] | None:
        """Return the cursor coordinates of the focus widget."""
        if not self.focus.selectable():  # type: ignore[union-attr]  # mypy bug on union
            return None
        if not hasattr(self.focus, "get_cursor_coords"):
            return None

        fp = self.focus_position
        (maxcol, maxrow) = size
        (hrows, frows), _ = self.frame_top_bottom(size, True)

        if fp == "header":
            row_adjust = 0
            coords = self.header.get_cursor_coords((maxcol,))  # type: ignore[union-attr]  # expect not None
        elif fp == "body":
            row_adjust = hrows
            coords = self.body.get_cursor_coords((maxcol, maxrow - hrows - frows))  # type: ignore[attr-defined]
        else:
            row_adjust = maxrow - frows
            coords = self.footer.get_cursor_coords((maxcol,))  # type: ignore[union-attr]  # expect not None

        if coords is None:
            return None

        x, y = coords
        return x, y + row_adjust

    def __iter__(self) -> Iterator[Literal["header", "body", "footer"]]:
        """Return an iterator over the positions in this Frame top to bottom."""
        if self._header:
            yield "header"
        yield "body"
        if self._footer:
            yield "footer"

    def __reversed__(self) -> Iterator[Literal["footer", "body", "header"]]:
        """Return an iterator over the positions in this Frame bottom to top."""
        if self._footer:
            yield "footer"
        yield "body"
        if self._header:
            yield "header"
urwid-urwid-69907e4/urwid/widget/grid_flow.py000066400000000000000000000541651526073734300212550ustar00rootroot00000000000000"""GridFlow: a container that flows widgets of equal width into a grid."""

from __future__ import annotations

import typing
import warnings
import weakref

from typing_extensions import Literal

from urwid.split_repr import remove_defaults

from .columns import Columns
from .constants import Align, Sizing, WHSettings
from .container import WidgetContainerListContentsMixin, WidgetContainerMixin
from .divider import Divider
from .monitored_list import MonitoredFocusList, MonitoredList
from .padding import Padding
from .pile import Pile
from .widget import AbstractFlowWidget, WidgetError, WidgetWarning, WidgetWrap

if typing.TYPE_CHECKING:
    from collections.abc import Iterable, Iterator, Sequence

    from urwid.canvas import Canvas


class GridFlowError(WidgetError):
    """GridFlow specific error."""


class GridFlowWarning(WidgetWarning):
    """GridFlow specific warning."""


GridFlowOptions = tuple[Literal[WHSettings.GIVEN], int]
GridFlowContentsItem = tuple[AbstractFlowWidget, GridFlowOptions]


class GridFlow(
    WidgetWrap[typing.Union[Pile, Divider]],
    WidgetContainerMixin[int],
    WidgetContainerListContentsMixin[GridFlowContentsItem],
):
    """Flow widget that renders all the widgets it contains the same width.

    Arranges them from left to right and top to bottom.
    """

    def sizing(self) -> frozenset[Sizing]:
        """Widget sizing.

        ..note:: Empty widget sizing is limited to the FLOW due to no data for width.
        """
        if self:
            return frozenset((Sizing.FLOW, Sizing.FIXED))
        return frozenset((Sizing.FLOW,))

    def __init__(
        self,
        cells: Iterable[AbstractFlowWidget],
        cell_width: int,
        h_sep: int,
        v_sep: int,
        align: Literal["left", "center", "right"] | Align | tuple[Literal["relative", WHSettings.RELATIVE], int],
        focus: int | AbstractFlowWidget | None = None,
    ) -> None:
        """Create the GridFlow widget.

        :param cells: iterable of flow widgets to display
        :param cell_width: column width for each cell
        :param h_sep: blank columns between each cell horizontally
        :param v_sep: blank rows between cells vertically
            (if more than one row is required to display all the cells)
        :param align: horizontal alignment of cells, one of:
            'left', 'center', 'right', ('relative', percentage 0=left 100=right)
        :param focus: widget index or widget instance to focus on
        """
        prepared_contents: list[GridFlowContentsItem] = []
        focus_position: int = -1

        for idx, widget in enumerate(cells):
            prepared_contents.append((widget, (WHSettings.GIVEN, cell_width)))
            if focus_position < 0 and (focus in {widget, idx} or (focus is None and widget.selectable())):
                focus_position = idx

        focus_position = max(focus_position, 0)

        self._contents: MonitoredFocusList[GridFlowContentsItem] = MonitoredFocusList(
            prepared_contents,
            focus=focus_position,
        )
        # Bumped on every contents change, so input handlers can tell that the display widget went stale.
        self._contents_generation = 0
        self._contents.set_modified_callback(self._contents_changed)
        self._contents.set_focus_changed_callback(lambda f: self._invalidate())
        self._contents.set_validate_contents_modified(self._contents_modified)
        self._cell_width = cell_width
        self.h_sep = h_sep
        self.v_sep = v_sep
        self.align = align
        self.first_position: weakref.WeakKeyDictionary[Padding[Columns], int] = weakref.WeakKeyDictionary()
        maxcol = self._get_maxcol(())
        self._cache_maxcol: int | None = maxcol
        super().__init__(self.generate_display_widget((maxcol,)))

    def _repr_words(self) -> list[str]:
        if len(self.contents) > 1:
            contents_string = f"({len(self.contents)} items)"
        elif self.contents:
            contents_string = "(1 item)"
        else:
            contents_string = "()"
        return [*super()._repr_words(), contents_string]

    def _repr_attrs(self) -> dict[str, typing.Any]:
        attrs = {
            **super()._repr_attrs(),
            "cell_width": self.cell_width,
            "h_sep": self.h_sep,
            "v_sep": self.v_sep,
            "align": self.align,
            "focus": self.focus_position if len(self._contents) > 1 else None,
        }
        return remove_defaults(attrs, GridFlow.__init__)

    def __rich_repr__(self) -> Iterator[tuple[str | None, typing.Any] | typing.Any]:
        """Yield this widget's constructor arguments as `(name, value)` pairs, for `rich`'s repr protocol."""
        yield "cells", [widget for widget, _ in self.contents]
        yield "cell_width", self.cell_width
        yield "h_sep", self.h_sep
        yield "v_sep", self.v_sep
        yield "align", self.align
        yield "focus", self.focus_position

    def __len__(self) -> int:
        """Return the number of cells."""
        return len(self._contents)

    def _invalidate(self) -> None:
        self._cache_maxcol = None
        super()._invalidate()  # type: ignore[safe-super]  # dynamic base

    def _contents_changed(self) -> None:
        """Record a contents change and invalidate the cached display widget."""
        self._contents_generation += 1
        self._invalidate()

    def _contents_modified(
        self,
        _slc: tuple[int, int, int],
        new_items: Iterable[GridFlowContentsItem],
    ) -> None:
        """
        Reject contents changes that would put an invalid item into the GridFlow.

        :raises GridFlowError: an added item is not a valid ``(widget, options)`` pair.
        """
        for item in new_items:
            try:
                _w, (t, _n) = item
                if t != WHSettings.GIVEN:
                    raise GridFlowError(f"added content invalid {item!r}")
            except (TypeError, ValueError) as exc:  # noqa: PERF203
                raise GridFlowError(f"added content invalid {item!r}").with_traceback(exc.__traceback__) from exc

    @property
    def cells(self) -> MonitoredList[AbstractFlowWidget]:
        """
        A list of the widgets in this GridFlow.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`contents` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "only for backwards compatibility. "
            "You should use the new standard container property `contents` to modify GridFlow. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        ml = MonitoredList(w for w, t in self.contents)

        def user_modified() -> None:
            self.cells = ml

        ml.set_modified_callback(user_modified)
        return ml

    @cells.setter
    def cells(self, widgets: MonitoredList[AbstractFlowWidget]) -> None:
        """
        Replace the widgets in this GridFlow, giving each of them the current cell width.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`contents` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "only for backwards compatibility. "
            "You should use the new standard container property `contents` to modify GridFlow. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        focus_position = self.focus_position
        self.contents = [(new, (WHSettings.GIVEN, self._cell_width)) for new in widgets]
        if focus_position < len(widgets):
            self.focus_position = focus_position

    @property
    def cell_width(self) -> int:
        """The width of each cell in the GridFlow.

        Setting this value affects all cells.
        """
        return self._cell_width

    @cell_width.setter
    def cell_width(self, width: int) -> None:
        focus_position = self.focus_position
        self.contents = [(w, (WHSettings.GIVEN, width)) for (w, options) in self.contents]
        self.focus_position = focus_position
        self._cell_width = width

    @property
    def contents(self) -> MonitoredFocusList[GridFlowContentsItem]:
        """The contents of this GridFlow as a list of (widget, options) tuples.

        options is currently a tuple in the form `('fixed', number)`.
        number is the number of screen columns to allocate to this cell.
        'fixed' is the only type accepted at this time.

        This list may be modified like a normal list and the GridFlow
        widget will update automatically.

        .. seealso:: Create new options tuples with the :meth:`options` method.
        """
        return self._contents

    @contents.setter
    def contents(self, c: Sequence[GridFlowContentsItem]) -> None:
        self._contents[:] = c

    def options(
        self,
        width_type: Literal["given", WHSettings.GIVEN] = WHSettings.GIVEN,
        width_amount: int | None = None,
    ) -> GridFlowOptions:
        """
        Return a new options tuple for use in a GridFlow's .contents list.

        :param width_type: 'given' is the only value accepted
        :param width_amount: None to use the default cell_width for this GridFlow
        :raises GridFlowError: *width_type* is not ``GIVEN``.
        """
        if width_type != WHSettings.GIVEN:
            raise GridFlowError(f"invalid width_type: {width_type!r}")
        if width_amount is None:
            width_amount = self._cell_width
        return (WHSettings.GIVEN, width_amount)

    def set_focus(self, cell: AbstractFlowWidget | int) -> None:
        """
        Set the cell in focus.

        :param cell: contained element to focus
        :raises IndexError: *cell* is an index with no child widget at it.
        :raises ValueError: *cell* is a widget that is not in the contents.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`focus_position` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "only for backwards compatibility. "
            "You may also use the new standard container property `focus_position` to set the focus. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        if isinstance(cell, int):
            try:
                if cell < 0 or cell >= len(self.contents):
                    raise IndexError(f"No GridFlow child widget at position {cell}")
            except TypeError as exc:
                raise IndexError(f"No GridFlow child widget at position {cell}").with_traceback(
                    exc.__traceback__
                ) from exc
            self.contents.focus = cell
            return

        for i, (w, _options) in enumerate(self.contents):
            if cell == w:
                self.focus_position = i
                return
        raise ValueError(f"Widget not found in GridFlow contents: {cell!r}")

    @property
    def focus(self) -> AbstractFlowWidget | None:
        """The child widget in focus or None when GridFlow is empty."""
        if not self.contents:
            return None
        return self.contents[self.focus_position][0]

    def get_focus(self) -> AbstractFlowWidget | None:
        """
        Return the widget in focus.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`focus` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "only for backwards compatibility. "
            "You may also use the new standard container property `focus` to get the focus. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        if not self.contents:
            return None
        return self.contents[self.focus_position][0]

    @property
    def focus_cell(self) -> AbstractFlowWidget | None:
        """
        The cell in focus.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`focus` to read the cell in focus,
            and :attr:`focus_position` to read or set it by index.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "only for backwards compatibility. "
            "You may also use the new standard container property "
            "`focus` to get the focus and `focus_position` to get/set the cell in focus by index. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.focus

    @focus_cell.setter
    def focus_cell(self, cell: AbstractFlowWidget) -> None:
        """
        Set the cell in focus.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`focus` to read the cell in focus,
            and :attr:`focus_position` to read or set it by index.
            This API will be removed in version 5.0.

        :raises ValueError: *cell* is a widget that is not in the contents.
        """
        warnings.warn(
            "only for backwards compatibility. "
            "You may also use the new standard container property "
            "`focus` to get the focus and `focus_position` to get/set the cell in focus by index. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        for i, (w, _options) in enumerate(self.contents):
            if cell == w:
                self.focus_position = i
                return
        raise ValueError(f"Widget not found in GridFlow contents: {cell!r}")

    @property
    def focus_position(self) -> int:
        """Index of child widget in focus.

        Raises :exc:`IndexError` if read when GridFlow is empty, or when set to an invalid index.

        :raises IndexError: the GridFlow is empty.
        """
        if (focus := self.contents.focus) is not None:
            return focus

        raise IndexError("No focus_position, GridFlow is empty")

    @focus_position.setter
    def focus_position(self, position: int) -> None:
        """
        Set the widget in focus.

        :param position: index of child widget to be made focus
        :raises IndexError: *position* is not an index of a child widget.
        """
        try:
            if position < 0 or position >= len(self.contents):
                raise IndexError(f"No GridFlow child widget at position {position}")
        except TypeError as exc:
            raise IndexError(f"No GridFlow child widget at position {position}").with_traceback(
                exc.__traceback__
            ) from exc
        self.contents.focus = position

    def _get_maxcol(self, size: tuple[int] | tuple[()]) -> int:
        if size:
            (maxcol,) = size
            if self and maxcol < self.cell_width:
                warnings.warn(
                    f"Size is smaller than cell width ({maxcol!r} < {self.cell_width!r})",
                    GridFlowWarning,
                    stacklevel=3,
                )
        elif self:
            maxcol = len(self) * self.cell_width + (len(self) - 1) * self.h_sep
        else:
            maxcol = 0
        return maxcol

    def get_display_widget(self, size: tuple[int] | tuple[()]) -> Divider | Pile:
        """Arrange the cells into columns (and possibly a pile) for display, input or to calculate rows.

        Also updates the display widget.
        """
        maxcol = self._get_maxcol(size)

        # use cache if possible
        if self._cache_maxcol == maxcol:
            return self._w

        self._cache_maxcol = maxcol
        self._w = self.generate_display_widget((maxcol,))

        return self._w

    def generate_display_widget(self, size: tuple[int] | tuple[()]) -> Divider | Pile:
        """Actually generate display widget (ignoring cache)."""
        maxcol = self._get_maxcol(size)

        divider = Divider()
        if not self.contents:
            return divider

        if self.v_sep > 1:
            # increase size of divider
            divider.top = self.v_sep - 1

        c: Columns | None = None
        p = Pile([])
        used_space = 0

        for i, (w, (_width_type, width_amount)) in enumerate(self.contents):
            if c is None or maxcol - used_space < width_amount:
                # starting a new row
                if self.v_sep:
                    p.contents.append((divider, typing.cast("tuple[Literal[WHSettings.WEIGHT], int]", p.options())))
                c = Columns([], self.h_sep)
                column_focused = False
                pad = Padding(c, self.align)
                self.first_position[pad] = i
                p.contents.append((pad, typing.cast("tuple[Literal[WHSettings.WEIGHT], int]", p.options())))

            # Use width == maxcol in case of maxcol < width amount
            # Columns will use empty widget in case of GIVEN width > maxcol
            c.contents.append(
                (
                    w,
                    typing.cast(
                        "tuple[Literal[WHSettings.GIVEN], int, Literal[False]]",
                        c.options(
                            WHSettings.GIVEN,
                            min(width_amount, maxcol),
                        ),
                    ),
                )
            )
            if (i == self.focus_position) or (not column_focused and w.selectable()):
                c.focus_position = len(c.contents) - 1
                column_focused = True
            if i == self.focus_position:
                p.focus_position = len(p.contents) - 1
            used_space = sum(typing.cast("int", x[1][1]) for x in c.contents) + self.h_sep * len(c.contents)
            pad.width = used_space - self.h_sep

        if self.v_sep:
            # remove first divider
            del p.contents[:1]

        return p

    def _set_focus_from_display_widget(self) -> None:
        """Set the focus to the item in focus in the display widget."""
        # display widget (self._w) is always built as:
        #
        # Pile([
        #     Padding(
        #         Columns([ # possibly
        #         cell, ...])),
        #     Divider(), # possibly
        #     ...])

        pile_focus = self._w.focus
        if not pile_focus:
            return

        c = typing.cast("Columns", pile_focus.base_widget)
        if c.focus:
            col_focus_position = c.focus_position
        else:
            col_focus_position = 0

        first_position = self.first_position[typing.cast("Padding[Columns]", pile_focus)]

        self.focus_position = first_position + col_focus_position

    def keypress(
        self,
        size: tuple[int] | tuple[()],
        key: str,
    ) -> str | None:
        """Pass keypress to display widget for handling.

        Captures focus changes.
        """
        self.get_display_widget(size)
        focus_before = self.contents.focus
        generation_before = self._contents_generation

        if (processed := super().keypress(size, key)) is not None:  # type: ignore[safe-super]  # dynamic base
            return processed

        # The display widget was built before the keypress was dispatched, so a callback that
        # set the focus or changed the contents is the more recent state and must not be
        # overwritten from it: after a contents change its focus indexes the old contents.
        if self.contents.focus == focus_before and self._contents_generation == generation_before:
            self._set_focus_from_display_widget()
        return None

    def pack(
        self,
        size: tuple[int] | tuple[()] = (),
        focus: bool = False,
    ) -> tuple[int, int]:
        """Return the number of screen columns and rows this widget requires."""
        if size:
            return super().pack(size, focus)  # type: ignore[safe-super]  # dynamic base
        if self:
            cols = len(self) * self.cell_width + (len(self) - 1) * self.h_sep
        else:
            cols = 0
        return cols, self.rows((cols,), focus)

    def rows(self, size: tuple[int], focus: bool = False) -> int:
        """Return the number of rows this widget requires for the given size."""
        self.get_display_widget(size)
        return typing.cast("int", super().rows(size, focus=focus))  # int or Never - depends on kind

    def render(
        self,
        size: tuple[int] | tuple[()],
        focus: bool = False,
    ) -> Canvas:
        """Render this widget's current display widget at the given size."""
        self.get_display_widget(size)
        return super().render(size, focus)  # type: ignore[safe-super]  # dynamic base

    def get_cursor_coords(self, size: tuple[int] | tuple[()]) -> tuple[int, int]:
        """Get cursor from display widget."""
        self.get_display_widget(size)
        return typing.cast("tuple[int, int]", super().get_cursor_coords(size))

    def move_cursor_to_coords(self, size: tuple[int] | tuple[()], col: int, row: int) -> bool:
        """Set the widget in focus based on the col + row."""
        self.get_display_widget(size)
        rval = typing.cast("bool", super().move_cursor_to_coords(size, col, row))
        self._set_focus_from_display_widget()
        return rval

    def mouse_event(
        self,
        size: tuple[int] | tuple[()],
        event: str,
        button: int,
        col: int,
        row: int,
        focus: bool,
    ) -> Literal[True]:
        """Handle a mouse event, updating the focus based on the resulting display widget."""
        self.get_display_widget(size)
        focus_before = self.contents.focus
        generation_before = self._contents_generation
        super().mouse_event(size, event, button, col, row, focus)  # type: ignore[safe-super]  # dynamic base
        # Same as in keypress: a callback that set the focus or changed the contents wins
        # over the display widget, which was built before the event was dispatched.
        if self.contents.focus == focus_before and self._contents_generation == generation_before:
            self._set_focus_from_display_widget()
        return True  # at a minimum we adjusted our focus

    def get_pref_col(self, size: tuple[int] | tuple[()]) -> int:
        """Return pref col from display widget."""
        self.get_display_widget(size)
        return typing.cast("int", super().get_pref_col(size))
urwid-urwid-69907e4/urwid/widget/line_box.py000066400000000000000000000205301526073734300210650ustar00rootroot00000000000000"""LineBox: a decoration that draws a line border and title around a widget."""

from __future__ import annotations

import typing

from .columns import Columns
from .constants import BOX_SYMBOLS, Align, WHSettings
from .divider import Divider
from .pile import Pile
from .solid_fill import SolidFill
from .text import Text
from .widget_decoration import WidgetDecoration, delegate_to_widget_mixin

if typing.TYPE_CHECKING:
    from collections.abc import Hashable

    from typing_extensions import Literal

    from .widget import AbstractWidget, Widget

WrappedWidget = typing.TypeVar("WrappedWidget", bound="AbstractWidget")


class LineBox(
    WidgetDecoration[WrappedWidget],
    delegate_to_widget_mixin("_wrapped_widget"),  # type: ignore[misc]
):
    """Box widget decoration that draws a line border, with an optional title, around another widget."""

    Symbols = BOX_SYMBOLS

    def __init__(
        self,
        original_widget: WrappedWidget,
        title: str = "",
        title_align: Literal["left", "center", "right"] | Align = Align.CENTER,
        title_attr: Hashable = None,
        tlcorner: str = BOX_SYMBOLS.LIGHT.TOP_LEFT,
        tline: str = BOX_SYMBOLS.LIGHT.HORIZONTAL,
        lline: str = BOX_SYMBOLS.LIGHT.VERTICAL,
        trcorner: str = BOX_SYMBOLS.LIGHT.TOP_RIGHT,
        blcorner: str = BOX_SYMBOLS.LIGHT.BOTTOM_LEFT,
        rline: str = BOX_SYMBOLS.LIGHT.VERTICAL,
        bline: str = BOX_SYMBOLS.LIGHT.HORIZONTAL,
        brcorner: str = BOX_SYMBOLS.LIGHT.BOTTOM_RIGHT,
    ) -> None:
        """
        Draw a line around original_widget.

        Use 'title' to set an initial title text with will be centered
        on top of the box.

        Use `title_attr` to apply a specific attribute to the title text.

        Use `title_align` to align the title to the 'left', 'right', or 'center'.
        The default is 'center'.

        You can also override the widgets used for the lines/corners:
            tline: top line
            bline: bottom line
            lline: left line
            rline: right line
            tlcorner: top left corner
            trcorner: top right corner
            blcorner: bottom left corner
            brcorner: bottom right corner

        If empty string is specified for one of the lines/corners, then no character will be output there.
        If no top/bottom/left/right lines - whole lines will be omitted.
        This allows for seamless use of adjoining LineBoxes.

        Class attribute `Symbols` can be used as source for standard lines:

        >>> print(LineBox(Text("Some text")).render(()))
        ┌─────────�
        │Some text│
        └─────────┘
        >>> print(
        ...     LineBox(
        ...         Text("Some text"),
        ...         tlcorner=LineBox.Symbols.LIGHT.TOP_LEFT_ROUNDED,
        ...         trcorner=LineBox.Symbols.LIGHT.TOP_RIGHT_ROUNDED,
        ...         blcorner=LineBox.Symbols.LIGHT.BOTTOM_LEFT_ROUNDED,
        ...         brcorner=LineBox.Symbols.LIGHT.BOTTOM_RIGHT_ROUNDED,
        ...     ).render(())
        ... )
        ╭─────────╮
        │Some text│
        ╰─────────╯
        >>> print(
        ...     LineBox(
        ...         Text("Some text"),
        ...         tline=LineBox.Symbols.HEAVY.HORIZONTAL,
        ...         bline=LineBox.Symbols.HEAVY.HORIZONTAL,
        ...         lline=LineBox.Symbols.HEAVY.VERTICAL,
        ...         rline=LineBox.Symbols.HEAVY.VERTICAL,
        ...         tlcorner=LineBox.Symbols.HEAVY.TOP_LEFT,
        ...         trcorner=LineBox.Symbols.HEAVY.TOP_RIGHT,
        ...         blcorner=LineBox.Symbols.HEAVY.BOTTOM_LEFT,
        ...         brcorner=LineBox.Symbols.HEAVY.BOTTOM_RIGHT,
        ...     ).render(())
        ... )
        ����������┓
        ┃Some text┃
        ┗���������┛

        To make Table constructions, some lineboxes need to be drawn without sides
        and T or CROSS symbols used for corners of cells.

        :raises ValueError: a *title* is given while *tline* is empty, or *title_align* is not a supported alignment.
        """
        w_lline = SolidFill(lline)
        w_rline = SolidFill(rline)

        w_tlcorner, w_tline, w_trcorner = Text(tlcorner), Divider(tline), Text(trcorner)
        w_blcorner, w_bline, w_brcorner = Text(blcorner), Divider(bline), Text(brcorner)

        if not tline and title:
            raise ValueError("Cannot have a title when tline is empty string")

        if title_attr:
            self.title_widget = Text((title_attr, self.format_title(title)))
        else:
            self.title_widget = Text(self.format_title(title))

        if tline:
            if title_align not in {Align.LEFT, Align.CENTER, Align.RIGHT}:
                raise ValueError('title_align must be one of "left", "right", or "center"')
            if title_align == Align.LEFT:
                tline_widgets: list[tuple[Literal[WHSettings.PACK], Widget] | Widget] = [
                    (WHSettings.PACK, self.title_widget),
                    w_tline,
                ]
            else:
                tline_widgets = [w_tline, (WHSettings.PACK, self.title_widget)]
                if title_align == Align.CENTER:
                    tline_widgets.append(w_tline)

            self.tline_widget: Columns | None = Columns(tline_widgets)
            top = Columns(
                (
                    (int(bool(tlcorner and lline)), w_tlcorner),
                    self.tline_widget,
                    (int(bool(trcorner and rline)), w_trcorner),
                )
            )

        else:
            self.tline_widget = None
            top = None

        # Note: We need to define a fixed first widget (even if it's 0 width) so that the other
        # widgets have something to anchor onto
        middle = Columns(
            ((int(bool(lline)), w_lline), original_widget, (int(bool(rline)), w_rline)),
            box_columns=[0, 2],
            focus_column=original_widget,
        )

        if bline:
            bottom = Columns(
                (
                    (int(bool(blcorner and lline)), w_blcorner),
                    w_bline,
                    (int(bool(brcorner and rline)), w_brcorner),
                )
            )
        else:
            bottom = None

        pile_widgets: list[tuple[Literal[WHSettings.PACK], Widget] | Widget] = []
        if top:
            pile_widgets.append((WHSettings.PACK, top))
        pile_widgets.append(middle)
        if bottom:
            pile_widgets.append((WHSettings.PACK, bottom))

        self._wrapped_widget = Pile(pile_widgets, focus_item=middle)

        super().__init__(original_widget)

    @property
    def original_widget(self) -> WrappedWidget:
        """Return the widget wrapped by the line box."""
        return super().original_widget

    @original_widget.setter
    def original_widget(self, original_widget: WrappedWidget) -> None:
        v_index = int(bool(self.tline_widget))  # we care only about top
        h_index = 1  # constant
        middle: Columns = typing.cast("Columns", self._wrapped_widget[v_index])
        _old_widget, options = middle.contents[h_index]
        middle.contents[h_index] = (original_widget, options)  # type: ignore[assignment]
        WidgetDecoration.original_widget.fset(self, original_widget)  # type: ignore[attr-defined]

    @property
    def _w(self) -> Pile:
        return self._wrapped_widget

    def format_title(self, text: str) -> str:
        """Return ``text`` padded with spaces for display in the box's top border, or an empty string if unset."""
        if text:
            return f" {text} "

        return ""

    def set_title(self, text: str) -> None:
        """
        Set the title shown in the top line of the box.

        :raises ValueError: this LineBox has no top line to put a title on.
        """
        if not self.tline_widget:
            raise ValueError("Cannot set title when tline is unset")
        self.title_widget.set_text(self.format_title(text))
        self.tline_widget._invalidate()

    @property
    def focus(self) -> AbstractWidget | None:
        """LineBox is partially container.

        While focus position is a bit hacky
        (formally it's not container and only position 0 available),
        focus widget is always provided by original widget.
        """
        return self._original_widget.focus
urwid-urwid-69907e4/urwid/widget/list_walker.py000066400000000000000000000306621526073734300216150ustar00rootroot00000000000000# Urwid ListWalker classes
#    Copyright (C) 2004-2012  Ian Ward
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/

"""List walkers: the objects that supply widgets to a ListBox."""

from __future__ import annotations

import typing
from collections.abc import Iterable

from urwid import signals

from .monitored_list import MonitoredFocusList, MonitoredList

if typing.TYPE_CHECKING:
    from collections.abc import Callable

    from typing_extensions import Self

    from .widget import AbstractWidget

    _T = typing.TypeVar("_T")
    _K = typing.TypeVar("_K")
    _K_contra = typing.TypeVar("_K_contra", contravariant=True)
    _V_co = typing.TypeVar("_V_co", covariant=True)

    class ListWalkerContainer(typing.Protocol[_K, _V_co]):
        """Protocol for a positionable container a :class:`ListWalker` can page through."""

        def __getitem__(self, key: _K) -> _V_co: ...

        def next_position(self, position: _K) -> _K:
            """Return the position following `position`."""

        def prev_position(self, position: _K) -> _K:
            """Return the position preceding `position`."""

    class ListBoxContentsProto(typing.Protocol[_K_contra]):
        """Read-only `(widget, options)` view over the :class:`ListBox` body."""

        def __getitem__(self, key: _K_contra) -> tuple[AbstractWidget, None]: ...

        def __len__(self) -> int: ...

else:
    _T = typing.TypeVar("_T")
    _K = typing.TypeVar("_K")
    _V_co = typing.TypeVar("_V_co", covariant=True)

__all__ = (
    "EstimatedSized",
    "ListWalker",
    "ListWalkerError",
    "ScrollSupportingBody",
    "SimpleFocusListWalker",
    "SimpleListWalker",
)


class ListWalkerError(Exception):
    """ListWalker related errors."""


@typing.runtime_checkable
class ScrollSupportingBody(typing.Protocol):
    """Protocol for ListWalkers."""

    def get_focus(self) -> tuple[AbstractWidget, _K]:
        """Return the ``(widget, position)`` currently in focus."""

    def set_focus(self, position: _K) -> None:
        """Set the focus to *position*."""

    def get_next(self, position: _K) -> tuple[AbstractWidget, _K] | tuple[None, None]:
        """Return the ``(widget, position)`` after *position*, or ``(None, None)`` if there is none."""

    def get_prev(self, position: _K) -> tuple[AbstractWidget, _K] | tuple[None, None]:
        """Return the ``(widget, position)`` before *position*, or ``(None, None)`` if there is none."""


@typing.runtime_checkable
class EstimatedSized(typing.Protocol):
    """Widget can estimate it's size.

    PEP 424 defines API for memory-efficiency.
    For the ListBox it's a sign of the limited body length.
    The main use-case is lazy-load, where real length calculation is expensive.
    """

    def __length_hint__(self) -> int:
        """Return an estimate of the number of items, per PEP 424."""


class ListWalker(
    typing.Generic[_K, _V_co],
    metaclass=signals.MetaSignals,
):
    """Base class for objects that supply widgets to a :class:`ListBox` on demand, by position."""

    # mixin not named as mixin
    signals: typing.ClassVar[list[str]] = ["modified"]

    def _modified(self) -> None:
        signals.emit_signal(self, "modified")

    def get_focus(self) -> tuple[_V_co, _K] | tuple[None, None]:
        """Return the ``(widget, position)`` currently in focus.

        This default implementation relies on a ``focus`` attribute and a ``__getitem__()``
        method defined in a subclass.

        Override and don't call this method if these are not defined.

        :returns: ``(widget, position)`` or ``(None, None)``
        """
        try:
            # pylint: disable=no-member,unsubscriptable-object
            focus = self.focus  # type: ignore[attr-defined]  # we're OK with fail
            return typing.cast("ListWalkerContainer[_K, _V_co]", self)[focus], focus
        except (IndexError, KeyError, TypeError):
            return None, None

    def get_next(
        self,
        position: _K,
    ) -> tuple[_V_co, _K] | tuple[None, None]:
        """Return the ``(widget, position)`` after ``position``.

        This default implementation relies on a ``next_position()`` method and a
        ``__getitem__()`` method defined in a subclass.

        Override and don't call this method if these are not defined.

        :param position: position to start from
        :returns: ``(widget, position)`` or ``(None, None)``
        """
        try:
            # pylint: disable=no-member,unsubscriptable-object
            position = typing.cast("ListWalkerContainer[_K, _V_co]", self).next_position(position)
            return typing.cast("ListWalkerContainer[_K, _V_co]", self)[position], position
        except (IndexError, KeyError):
            return None, None

    def get_prev(
        self,
        position: _K,
    ) -> tuple[_V_co, _K] | tuple[None, None]:
        """Return the ``(widget, position)`` before ``position``.

        This default implementation relies on a ``prev_position()`` method and a
        ``__getitem__()`` method defined in a subclass.

        Override and don't call this method if these are not defined.

        :param position: position to start from
        :returns: ``(widget, position)`` or ``(None, None)``
        """
        try:
            # pylint: disable=no-member,unsubscriptable-object
            position = typing.cast("ListWalkerContainer[_K, _V_co]", self).prev_position(position)
            return typing.cast("ListWalkerContainer[_K, _V_co]", self)[position], position
        except (IndexError, KeyError):
            return None, None


class SimpleListWalker(
    MonitoredList[_T],
    ListWalker[int, _T],
):
    """ListWalker that presents a plain list of widgets, addressed by integer position."""

    def __init__(self, contents: Iterable[_T], wrap_around: bool = False) -> None:
        """Initialize a list walker, which can be treated as a list since it inherits :class:`MonitoredList`.

        Changes made to this object (when it is treated as a list) are detected automatically
        and will cause ListBox objects using this list walker to be updated.

        :param contents: list to copy into this object
        :param wrap_around: if true, jumps to beginning/end of list on move
        :raises ListWalkerError: *contents* is not iterable.
        """
        if not isinstance(contents, Iterable):
            raise ListWalkerError(f"SimpleListWalker expecting list like object, got: {contents!r}")
        super().__init__(contents)
        self.focus = 0
        self.wrap_around = wrap_around

    @property
    def contents(self) -> Self:
        """Return self.

        Provides compatibility with old SimpleListWalker class.
        """
        return self

    def _modified(self) -> None:
        if self.focus >= len(self):
            self.focus = max(0, len(self) - 1)
        ListWalker._modified(self)  # pylint: disable=protected-access

    def set_modified_callback(self, callback: Callable[[], typing.Any]) -> typing.NoReturn:
        """Raise :exc:`NotImplementedError`: this function inherited from MonitoredList is not implemented here.

        Use ``connect_signal(list_walker, "modified", ...)`` instead.

        :raises NotImplementedError: always
        """
        raise NotImplementedError('Use connect_signal(list_walker, "modified", ...) instead.')

    def set_focus(self, position: int) -> None:
        """Set focus position.

        :param position: position to focus
        :raises IndexError: if there is no widget at ``position``
        """
        if not 0 <= position < len(self):
            raise IndexError(f"No widget at position {position}")

        self.focus = position
        self._modified()

    def next_position(self, position: int) -> int:
        """Return position after ``position``.

        :param position: position to start from
        :raises IndexError: if there is no next position and ``wrap_around`` is false
        """
        if len(self) - 1 <= position:
            if self.wrap_around:
                return 0
            raise IndexError
        return position + 1

    def prev_position(self, position: int) -> int:
        """Return position before ``position``.

        :param position: position to start from
        :raises IndexError: if there is no previous position and ``wrap_around`` is false
        """
        if position <= 0:
            if self.wrap_around:
                return len(self) - 1
            raise IndexError
        return position - 1

    def positions(self, reverse: bool = False) -> Iterable[int]:
        """Return an iterable of positions. Optional method.

        :param reverse: if true, return positions in reverse order
        """
        if reverse:
            return range(len(self) - 1, -1, -1)
        return range(len(self))


class SimpleFocusListWalker(
    MonitoredFocusList[_T],
    ListWalker[typing.SupportsIndex, _T],
):
    """ListWalker like :class:`SimpleListWalker` that also keeps focus tracking items across edits."""

    def __init__(self, contents: Iterable[_T], wrap_around: bool = False) -> None:
        """Initialize a list walker, which can be treated as a list since it inherits :class:`MonitoredList`.

        Changes made to this object (when it is treated as a list) are detected automatically
        and will cause ListBox objects using this list walker to be updated.

        Also, items added or removed before the widget in focus with normal list methods will
        cause the focus to be updated intelligently.

        :param contents: list to copy into this object
        :param wrap_around: if true, jumps to beginning/end of list on move
        :raises ListWalkerError: *contents* is not iterable.
        """
        if not isinstance(contents, Iterable):
            raise ListWalkerError(f"SimpleFocusListWalker expecting iterable object, got: {contents!r}")
        super().__init__(contents)
        self.wrap_around = wrap_around

    def _modified(self) -> None:
        # Prefer ListWalker's signal emission over MonitoredList's callback hook.
        ListWalker._modified(self)  # pylint: disable=protected-access

    def set_modified_callback(self, callback: typing.Any) -> typing.NoReturn:
        """Raise :exc:`NotImplementedError`: this function inherited from MonitoredList is not implemented here.

        Use ``connect_signal(list_walker, "modified", ...)`` instead.

        :raises NotImplementedError: always
        """
        raise NotImplementedError('Use connect_signal(list_walker, "modified", ...) instead.')

    def set_focus(self, position: int) -> None:
        """Set focus position.

        :param position: position to focus
        """
        self.focus = position
        self._modified()

    def next_position(self, position: typing.SupportsIndex) -> int:
        """Return position after ``position``.

        :param position: position to start from
        :raises IndexError: if there is no next position and ``wrap_around`` is false
        """
        pos = int(position)
        if len(self) - 1 <= pos:
            if self.wrap_around:
                return 0
            raise IndexError
        return pos + 1

    def prev_position(self, position: typing.SupportsIndex) -> int:
        """Return position before ``position``.

        :param position: position to start from
        :raises IndexError: if there is no previous position and ``wrap_around`` is false
        """
        pos = int(position)
        if pos <= 0:
            if self.wrap_around:
                return len(self) - 1
            raise IndexError
        return pos - 1

    def positions(self, reverse: bool = False) -> Iterable[int]:
        """Return an iterable of positions. Optional method.

        :param reverse: if true, return positions in reverse order
        """
        if reverse:
            return range(len(self) - 1, -1, -1)
        return range(len(self))
urwid-urwid-69907e4/urwid/widget/listbox.py000066400000000000000000002301331526073734300207540ustar00rootroot00000000000000# Urwid listbox class
#    Copyright (C) 2004-2012  Ian Ward
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/


"""ListBox: a scrolling box container of widgets supplied by a list walker."""

from __future__ import annotations

import operator
import typing
import warnings
from collections.abc import Container, Iterable, Sized
from contextlib import suppress

from urwid import signals
from urwid.canvas import CanvasCombine, SolidCanvas

from .constants import Sizing, VAlign, WHSettings, normalize_valign
from .container import WidgetContainerMixin
from .deque_walker import SimpleDequeWalker, SimpleFocusDequeWalker
from .filler import calculate_top_bottom_filler
from .list_walker import (
    EstimatedSized,
    ListWalker,
    ListWalkerError,
    ScrollSupportingBody,
    SimpleFocusListWalker,
    SimpleListWalker,
)
from .widget import Widget, nocache_widget_render_instance

if typing.TYPE_CHECKING:
    from collections.abc import Callable, Hashable, Iterator

    from typing_extensions import Literal, Self

    from urwid.canvas import Canvas, CompositeCanvas

    from .list_walker import ListBoxContentsProto
    from .widget import AbstractFlowWidget, AbstractWidget

    _K = typing.TypeVar("_K")
else:
    _K = typing.TypeVar("_K")

__all__ = (
    "ListBox",
    "ListBoxError",
    "ListWalker",
    "ListWalkerError",
    "SimpleDequeWalker",
    "SimpleFocusDequeWalker",
    "SimpleFocusListWalker",
    "SimpleListWalker",
    "VisibleInfo",
    "VisibleInfoFillItem",
    "VisibleInfoMiddle",
    "VisibleInfoTopBottom",
)


class ListBoxError(Exception):
    """ListBox related errors."""


class VisibleInfoMiddle(typing.NamedTuple):
    """Named tuple for ListBox internals."""

    offset: int
    focus_widget: AbstractFlowWidget
    focus_pos: typing.Any
    focus_rows: int
    cursor: tuple[int, int] | None


class VisibleInfoFillItem(typing.NamedTuple):
    """Named tuple for ListBox internals."""

    widget: AbstractFlowWidget
    position: typing.Any
    rows: int


class VisibleInfoTopBottom(typing.NamedTuple):
    """Named tuple for ListBox internals."""

    trim: int
    fill: list[VisibleInfoFillItem]

    @classmethod
    def from_raw_data(
        cls,
        trim: int,
        fill: Iterable[tuple[AbstractFlowWidget, typing.Any, int]],
    ) -> Self:
        """Construct from not typed data.

        Useful for overridden cases.
        """
        return cls(trim=trim, fill=[VisibleInfoFillItem(*item) for item in fill])  # pragma: no cover


class VisibleInfo(typing.NamedTuple):
    """Named tuple for ListBox internals."""

    middle: VisibleInfoMiddle
    top: VisibleInfoTopBottom
    bottom: VisibleInfoTopBottom

    @classmethod
    def from_raw_data(
        cls,
        middle: tuple[int, AbstractFlowWidget, typing.Any, int, tuple[int, int] | None],
        top: tuple[int, Iterable[tuple[AbstractFlowWidget, typing.Any, int]]],
        bottom: tuple[int, Iterable[tuple[AbstractFlowWidget, typing.Any, int]]],
    ) -> Self:
        """Construct from not typed data.

        Useful for overridden cases.
        """
        return cls(  # pragma: no cover
            middle=VisibleInfoMiddle(*middle),
            top=VisibleInfoTopBottom.from_raw_data(*top),
            bottom=VisibleInfoTopBottom.from_raw_data(*bottom),
        )


class _SeenPositions(Container[typing.Any]):
    """Collection of walker positions already visited, for loops that must stop on a repeat.

    Positions are opaque objects that only promise ``==``.
    Hashable positions are kept in a set, so a lookup does not grow with the number visited.
    Once a position cannot be hashed, all of them move to a list and are compared with ``==``.
    """

    __slots__ = ("_hashed", "_listed")

    def __init__(self) -> None:
        """Create an empty collection."""
        self._hashed: set[object] = set()
        self._listed: list[object] | None = None

    def __contains__(self, position: object) -> bool:
        """Return whether a position equal to *position* was added."""
        if self._listed is not None:
            return position in self._listed
        try:
            return position in self._hashed
        except TypeError:
            return position in list(self._hashed)

    def add(self, position: object) -> None:
        """Add *position*."""
        if self._listed is None:
            try:
                self._hashed.add(position)
            except TypeError:
                self._listed = list(self._hashed)
            else:
                return
        self._listed.append(position)


class ListBox(Widget, WidgetContainerMixin[_K]):
    """Vertically stacked list of widgets."""

    _selectable = True
    _sizing = frozenset([Sizing.BOX])

    @typing.overload
    def __init__(
        self: ListBox[_K],
        body: ListWalker[_K, AbstractFlowWidget],
    ) -> None: ...

    @typing.overload
    def __init__(
        self: ListBox[int],
        body: Iterable[AbstractFlowWidget],
    ) -> None: ...

    def __init__(
        self,
        body: ListWalker[_K, AbstractFlowWidget] | Iterable[AbstractFlowWidget],
    ) -> None:
        """Create the ListBox widget.

        :param body: a ListWalker subclass such as :class:`SimpleFocusListWalker`
            that contains widgets to be displayed inside the list box
        """
        super().__init__()
        if isinstance(body, ListWalker):
            self._body: ListWalker[typing.Any, AbstractFlowWidget] = body
        elif getattr(body, "get_focus", None):
            self._body = typing.cast("ListWalker[typing.Any, AbstractFlowWidget]", body)
            warnings.warn(
                f"ListWalker or Iterable[Widget] argument expected, got: {type(body)}",
                DeprecationWarning,
                stacklevel=2,
            )
        else:
            self._body = SimpleListWalker["AbstractFlowWidget"](body)

        self.body = self._body  # Initialization hack

        # offset_rows is the number of rows between the top of the view
        # and the top of the focused item
        self.offset_rows = 0
        # inset_fraction is used when the focused widget is off the
        # top of the view.  it is the fraction of the widget cut off
        # at the top.  (numerator, denominator)
        self.inset_fraction = (0, 1)

        # pref_col is the preferred column for the cursor when moving
        # between widgets that use the cursor (edit boxes etc.)
        self.pref_col: int | str = "left"

        # variable for delayed focus change used by set_focus
        self.set_focus_pending: (
            Literal["first selectable"] | tuple[Literal["above", "below"] | None, AbstractWidget, typing.Any] | None
        ) = "first selectable"

        # variable for delayed valign change used by set_focus_valign
        self.set_focus_valign_pending: tuple[VAlign | Literal[WHSettings.RELATIVE], int | None] | None = None

        # used for scrollable protocol
        self._rows_max_cached = 0
        self._rendered_size = 0, 0

    @property
    def body(self) -> ListWalker[_K, AbstractFlowWidget]:
        """A ListWalker subclass such as :class:`SimpleFocusListWalker` that contains widgets.

        The widgets are to be displayed inside the list box.
        """
        return typing.cast("ListWalker[_K, AbstractFlowWidget]", self._body)

    @body.setter
    def body(self, body: Iterable[AbstractFlowWidget] | ListWalker[_K, AbstractFlowWidget]) -> None:
        with suppress(AttributeError):
            signals.disconnect_signal(self._body, "modified", type(self)._invalidate, weak_args=[self])
            # _body may be not yet assigned

        if isinstance(body, ListWalker):
            self._body = body
        elif getattr(body, "get_focus", None):
            self._body = typing.cast("ListWalker[typing.Any, AbstractFlowWidget]", body)
            warnings.warn(
                f"ListWalker or Iterable[Widget] argument expected, got: {type(body)}",
                DeprecationWarning,
                stacklevel=2,
            )
        else:
            self._body = SimpleListWalker(body)
        try:
            # A weak reference lets a ListBox over a long-lived walker be collected; the handler goes with it.
            signals.connect_signal(self._body, "modified", type(self)._invalidate, weak_args=[self])
        except NameError:
            # our list walker has no modified signal,
            # so we must not cache our canvases because we don't know when our content has changed
            self.render = nocache_widget_render_instance(self)  # type: ignore[method-assign,assignment]
        self._invalidate()

    @property
    def __len__(self) -> Callable[[], int]:
        """
        Return the length of the body, when the body reports one.

        :raises AttributeError: the body is not :class:`Sized`.
        """
        if isinstance(self._body, Sized):
            return self._body.__len__
        raise AttributeError(f"{self._body.__class__.__name__} is not Sized")

    @property
    def __length_hint__(self) -> Callable[[], int]:  # pylint: disable=invalid-length-hint-returned
        """
        Return an estimated length of the body, when the body can provide one.

        :raises AttributeError: the body is neither :class:`Sized` nor implements ``__length_hint__``.
        """
        if isinstance(self._body, (Sized, EstimatedSized)):
            return lambda: operator.length_hint(self._body)
        raise AttributeError(f'{self._body.__class__.__name__} is not Sized and do not implement "__length_hint__"')

    def calculate_visible(
        self,
        size: tuple[int, int],
        focus: bool = False,
    ) -> VisibleInfo | tuple[None, None, None]:
        """Return the widgets that would be displayed in the ListBox given the current *size* and *focus*.

        see :meth:`Widget.render` for parameter details

        :returns: (*middle*, *top*, *bottom*) or (``None``, ``None``, ``None``)

        *middle*
            (*row offset*(when +ve) or *inset*(when -ve),
            *focus widget*, *focus position*, *focus rows*,
            *cursor coords* or ``None``)
        *top*
            (*# lines to trim off top*,
            list of (*widget*, *position*, *rows*) tuples above focus in order from bottom to top)
        *bottom*
            (*# lines to trim off bottom*,
            list of (*widget*, *position*, *rows*) tuples below focus in order from top to bottom)
        """
        (maxcol, maxrow) = size

        # 0. set the focus if a change is pending
        if self.set_focus_pending or self.set_focus_valign_pending:
            self._set_focus_complete((maxcol, maxrow), focus)

        # 1. start with the focus widget
        focus_widget, focus_pos = self._body.get_focus()
        if focus_widget is None:  # list box is empty?
            return None, None, None
        top_pos = focus_pos

        offset_rows, inset_rows = self.get_focus_offset_inset((maxcol, maxrow))
        #    force at least one line of focus to be visible
        if maxrow and offset_rows >= maxrow:
            offset_rows = maxrow - 1

        #    adjust position so cursor remains visible
        cursor = None
        if maxrow and focus_widget.selectable() and focus and hasattr(focus_widget, "get_cursor_coords"):
            cursor = focus_widget.get_cursor_coords((maxcol,))

        if cursor is not None:
            _cx, cy = cursor
            effective_cy = cy + offset_rows - inset_rows

            if effective_cy < 0:  # cursor above top?
                inset_rows = cy
            elif effective_cy >= maxrow:  # cursor below bottom?
                offset_rows = maxrow - cy - 1
                if offset_rows < 0:  # need to trim the top
                    inset_rows, offset_rows = -offset_rows, 0

        #    set trim_top by focus trimmimg
        trim_top = inset_rows
        focus_rows = focus_widget.rows((maxcol,), True)

        # 2. collect the widgets above the focus
        pos = focus_pos
        fill_lines = offset_rows
        fill_above = []
        top_pos = pos
        # A body that wraps around hands out the same widgets again once the list is exhausted.
        shown_pos = _SeenPositions()
        shown_pos.add(focus_pos)
        while fill_lines > 0:
            prev, pos = self._body.get_prev(pos)
            if prev is None or pos in shown_pos:  # run out of widgets above?
                offset_rows -= fill_lines
                break
            shown_pos.add(pos)
            top_pos = pos

            p_rows = prev.rows((maxcol,))
            if p_rows:  # filter out 0-height widgets
                fill_above.append(VisibleInfoFillItem(prev, pos, p_rows))
            if p_rows > fill_lines:  # crosses top edge?
                trim_top = p_rows - fill_lines
                break
            fill_lines -= p_rows

        trim_bottom = max(focus_rows + offset_rows - inset_rows - maxrow, 0)

        # 3. collect the widgets below the focus
        pos = focus_pos
        fill_lines = maxrow - focus_rows - offset_rows + inset_rows
        fill_below = []
        while fill_lines > 0:
            next_pos, pos = self._body.get_next(pos)
            if next_pos is None or pos in shown_pos:  # run out of widgets below?
                break
            shown_pos.add(pos)

            n_rows = next_pos.rows((maxcol,))
            if n_rows:  # filter out 0-height widgets
                fill_below.append(VisibleInfoFillItem(next_pos, pos, n_rows))
            if n_rows > fill_lines:  # crosses bottom edge?
                trim_bottom = n_rows - fill_lines
                fill_lines -= n_rows
                break
            fill_lines -= n_rows

        # 4. fill from top again if necessary & possible
        fill_lines = max(0, fill_lines)

        if fill_lines > 0 and trim_top > 0:
            if fill_lines <= trim_top:
                trim_top -= fill_lines
                offset_rows += fill_lines
                fill_lines = 0
            else:
                fill_lines -= trim_top
                offset_rows += trim_top
                trim_top = 0
        pos = top_pos
        while fill_lines > 0:
            prev, pos = self._body.get_prev(pos)
            if prev is None or pos in shown_pos:
                break
            shown_pos.add(pos)

            p_rows = prev.rows((maxcol,))
            fill_above.append(VisibleInfoFillItem(prev, pos, p_rows))
            if p_rows > fill_lines:  # more than required
                trim_top = p_rows - fill_lines
                offset_rows += fill_lines
                break
            fill_lines -= p_rows
            offset_rows += p_rows

        # 5. return the interesting bits
        return VisibleInfo(
            VisibleInfoMiddle(offset_rows - inset_rows, focus_widget, focus_pos, focus_rows, cursor),
            VisibleInfoTopBottom(trim_top, fill_above),
            VisibleInfoTopBottom(trim_bottom, fill_below),
        )

    def _check_support_scrolling(self) -> None:
        """
        Reject the body if it cannot support the scrolling protocol.

        :raises ListBoxError: the body does not implement the scrolling protocol, its size cannot be estimated, or it
            wraps around, which leaves the scroll position undefined.
        """
        from .treetools import TreeWalker

        if not isinstance(self._body, ScrollSupportingBody):
            raise ListBoxError(f"{self} body do not implement methods required for scrolling protocol")

        if not isinstance(self._body, (Sized, EstimatedSized, TreeWalker)):
            raise ListBoxError(
                f"{self} body is not a Sized, can not estimate it's size and not a TreeWalker."
                f"Scroll is not allowed due to risk of infinite cycle of widgets load."
            )

        if getattr(self._body, "wrap_around", False):
            raise ListBoxError("Body is wrapped around. Scroll position calculation is undefined.")

    def get_scrollpos(self, size: tuple[int, int] | None = None, focus: bool = False) -> int:
        """Return the current scrolling position."""
        self._check_support_scrolling()

        if not self._body:
            return 0

        if size is not None:
            self._rendered_size = size

        visible = typing.cast("VisibleInfo", self.calculate_visible(self._rendered_size, focus))

        start_row = visible.top.trim
        maxcol = self._rendered_size[0]

        if visible.top.fill:
            pos = visible.top.fill[-1].position
        else:
            pos = visible.middle.focus_pos

        prev, pos = self._body.get_prev(pos)
        while prev is not None:
            start_row += prev.rows((maxcol,))
            prev, pos = self._body.get_prev(pos)

        return start_row

    def rows_max(self, size: tuple[int, int] | None = None, focus: bool = False) -> int:
        """Scrollable protocol for sized iterable and not wrapped around contents."""
        self._check_support_scrolling()

        if size is not None:
            self._rendered_size = size

        if size or not self._rows_max_cached:
            cols = self._rendered_size[0]
            rows = 0

            focused_w, idx = self._body.get_focus()
            if focused_w:
                rows += focused_w.rows((cols,), focus)

                prev, pos = self._body.get_prev(idx)
                while prev is not None:
                    rows += prev.rows((cols,), False)
                    prev, pos = self._body.get_prev(pos)

                next_, pos = self._body.get_next(idx)
                while next_ is not None:
                    rows += next_.rows((cols,), True)
                    next_, pos = self._body.get_next(pos)

            self._rows_max_cached = rows

        return self._rows_max_cached

    def require_relative_scroll(self, size: tuple[int, int], focus: bool = False) -> bool:
        """Widget require relative scroll due to performance limitations of real lines count calculation."""
        return isinstance(self._body, (Sized, EstimatedSized)) and (size[1] * 3 < operator.length_hint(self.body))

    def get_first_visible_pos(self, size: tuple[int, int], focus: bool = False) -> int:
        """Return the index of the first visible item."""
        self._check_support_scrolling()

        if not self._body:
            return 0

        visible = typing.cast("VisibleInfo", self.calculate_visible(size, focus))
        if visible.top.fill:
            first_pos = visible.top.fill[-1].position
        else:
            first_pos = self.focus_position

        over = 0
        _widget, first_pos = self._body.get_prev(first_pos)
        while first_pos is not None:
            over += 1
            _widget, first_pos = self._body.get_prev(first_pos)

        return over

    def get_visible_amount(self, size: tuple[int, int], focus: bool = False) -> int:
        """Return the number of items currently visible."""
        self._check_support_scrolling()

        if not self._body:
            return 1

        visible = typing.cast("VisibleInfo", self.calculate_visible(size, focus))
        return 1 + len(visible.top.fill) + len(visible.bottom.fill)

    def render(
        self,
        size: tuple[int, int],  # type: ignore[override]
        focus: bool = False,
    ) -> CompositeCanvas | SolidCanvas:
        """
        Render ListBox and return canvas.

        see :meth:`Widget.render` for details

        :raises ListBoxError: the rendered contents do not fit the reported rows, a child widget renders a different
            number of rows or cursor coordinates than it calculated, or the list walker returns a next position pointing
            at itself.
        """
        (maxcol, maxrow) = size

        self._rendered_size = size

        middle, top, bottom = self.calculate_visible((maxcol, maxrow), focus=focus)
        if middle is None:
            return SolidCanvas(" ", maxcol, maxrow)

        _ignore, focus_widget, focus_pos, focus_rows, cursor = middle  # pylint: disable=unpacking-non-sequence
        trim_top, fill_above = typing.cast("VisibleInfoTopBottom", top)  # pylint: disable=unpacking-non-sequence
        trim_bottom, fill_below = typing.cast("VisibleInfoTopBottom", bottom)  # pylint: disable=unpacking-non-sequence

        combinelist: list[tuple[Canvas, Hashable, bool]] = []
        rows = 0
        fill_above.reverse()  # fill_above is in bottom-up order
        for widget, w_pos, w_rows in fill_above:
            canvas = widget.render((maxcol,))
            if w_rows != canvas.rows():
                raise ListBoxError(
                    f"Widget {widget!r} at position {w_pos!r} "
                    f"within listbox calculated {w_rows:d} rows "
                    f"but rendered {canvas.rows():d}!"
                )
            rows += w_rows
            combinelist.append((canvas, w_pos, False))

        focus_canvas = focus_widget.render((maxcol,), focus=focus)

        if focus_canvas.rows() != focus_rows:
            raise ListBoxError(
                f"Focus Widget {focus_widget!r} at position {focus_pos!r} "
                f"within listbox calculated {focus_rows:d} rows "
                f"but rendered {focus_canvas.rows():d}!"
            )
        c_cursor = focus_canvas.cursor
        if cursor is not None and cursor != c_cursor:
            raise ListBoxError(
                f"Focus Widget {focus_widget!r} at position {focus_pos!r} "
                f"within listbox calculated cursor coords {cursor!r} "
                f"but rendered cursor coords {c_cursor!r}!"
            )

        rows += focus_rows
        combinelist.append((focus_canvas, focus_pos, True))

        for widget, w_pos, w_rows in fill_below:
            canvas = widget.render((maxcol,))
            if w_rows != canvas.rows():
                raise ListBoxError(
                    f"Widget {widget!r} at position {w_pos!r} "
                    f"within listbox calculated {w_rows:d} "
                    f"rows but rendered {canvas.rows():d}!"
                )
            rows += w_rows
            combinelist.append((canvas, w_pos, False))

        final_canvas = CanvasCombine(combinelist)

        if trim_top:
            final_canvas.trim(trim_top)
            rows -= trim_top
        if trim_bottom:
            final_canvas.trim_end(trim_bottom)
            rows -= trim_bottom

        if rows > maxrow:
            raise ListBoxError(
                f"Listbox contents too long!\nRender top={top!r}, middle={middle!r}, bottom={bottom!r}\n"
            )

        if rows < maxrow:
            if trim_bottom != 0:
                raise ListBoxError(
                    f"Listbox contents too short!\n"
                    f"Render top={top!r}, middle={middle!r}, bottom={bottom!r}\n"
                    f"Trim bottom={trim_bottom!r}"
                )

            bottom_pos = focus_pos
            if fill_below:
                bottom_pos = fill_below[-1][1]

            rendered_positions = [idx for _, idx, _ in combinelist]
            next_widget, next_pos = self._body.get_next(bottom_pos)
            while next_widget is not None and next_pos is not None and next_pos not in rendered_positions:
                if next_widget.rows((maxcol,), False):
                    raise ListBoxError(
                        f"Listbox contents too short!\n"
                        f"Render top={top!r}, middle={middle!r}, bottom={bottom!r}\n"
                        f"Not rendered not empty widgets available "
                        f"(first is {next_widget!r} with position {next_pos!r})"
                    )

                next_widget, next_next_pos = self._body.get_next(next_pos)
                if next_pos == next_next_pos:
                    raise ListBoxError(
                        f"Next position after {next_pos!r} is invalid (points to itself)\n"
                        f"Looks like bug with {self._body!r}"
                    )
                next_pos = next_next_pos

            final_canvas.pad_trim_top_bottom(0, maxrow - rows)

        return final_canvas

    def get_cursor_coords(self, size: tuple[int, int]) -> tuple[int, int] | None:
        """See :meth:`Widget.get_cursor_coords` for details."""
        (maxcol, maxrow) = size

        middle, _top, _bottom = self.calculate_visible((maxcol, maxrow), True)
        if middle is None:
            return None

        offset_inset, _ignore1, _ignore2, _ignore3, cursor = middle  # pylint: disable=unpacking-non-sequence
        if not cursor:
            return None

        x, y = cursor
        y += offset_inset
        if y < 0 or y >= maxrow:
            return None
        return (x, y)

    def set_focus_valign(
        self,
        valign: Literal["top", "middle", "bottom"] | VAlign | tuple[Literal["relative", WHSettings.RELATIVE], int],
    ) -> None:
        """Set the focus widget's display offset and inset.

        :param valign: one of: 'top', 'middle', 'bottom' ('relative', percentage 0=top 100=bottom)
        """
        vt, va = normalize_valign(valign, ListBoxError)
        self.set_focus_valign_pending = vt, va

    def set_focus(self, position: _K, coming_from: Literal["above", "below"] | None = None) -> None:
        """
        Set the focus position and try to keep the old focus in view.

        :param position: a position compatible with :meth:`self._body.set_focus`
        :param coming_from: set to 'above' or 'below' if you know that
                            old position is above or below the new position.
        :raises ListBoxError: *coming_from* is not ``'above'``, ``'below'`` or ``None``.
        :raises TypeError: the body does not implement ``set_focus``.
        :raises IndexError: the ListBox is empty.
        """
        if coming_from not in {"above", "below", None}:
            raise ListBoxError(f"coming_from value invalid: {coming_from!r}")

        if not hasattr(self._body, "set_focus"):
            raise TypeError(f"{type(self._body)}.set_focus is not implemented.")

        focus_widget, focus_pos = self._body.get_focus()
        if focus_widget is None:
            raise IndexError("Can't set focus, ListBox is empty")

        self.set_focus_pending = coming_from, focus_widget, focus_pos
        self._body.set_focus(position)

    def get_focus(self) -> tuple[AbstractFlowWidget, _K] | tuple[None, None]:
        """
        Return a `(focus widget, focus position)` tuple.

        .. deprecated:: 2.2.0
            Use the standard container properties :attr:`focus` and :attr:`focus_position` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "only for backwards compatibility. "
            "You may also use the new standard container property `focus` to get the focus "
            "and property `focus_position` to read these values. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self._body.get_focus()

    @property
    def focus(self) -> AbstractFlowWidget | None:
        """
        The child widget in focus or None when ListBox is empty.

        Return the widget in focus according to our :obj:`list walker `.
        """
        return self._body.get_focus()[0]

    def _get_focus_position(self) -> _K:
        """Return the list walker position of the widget in focus.

        The type of value returned depends on the :obj:`list walker `.

        :raises IndexError: the ListBox is empty.
        """
        w, pos = self._body.get_focus()
        if w is None:
            raise IndexError("No focus_position, ListBox is empty")
        # the walker reports position `None` only together with the widget
        return typing.cast("_K", pos)

    focus_position = property(
        _get_focus_position,
        set_focus,
        doc="""
        the position of child widget in focus. The valid values for this
        position depend on the list walker in use.
        :exc:`IndexError` will be raised by reading this property when the
        ListBox is empty or setting this property to an invalid position.
        """,
    )

    def _contents(self) -> ListBoxContentsProto[_K]:
        # noinspection PyMethodParameters
        class ListBoxContents(Sized):
            """Mapping-like view onto this ListBox's items, indexed by position."""

            # pylint: disable=no-self-argument

            __getitem__ = self._contents__getitem__

            def __len__(inner_self) -> int:
                return len(self)

            def __repr__(inner_self) -> str:
                return f"<{inner_self.__class__.__name__} for {self!r} at 0x{id(inner_self):X}>"

            def __call__(inner_self) -> Self:
                """
                Return the contents object itself.

                .. deprecated:: 2.4.3
                    :attr:`ListBox.contents` is a property, not a method: use it without calling it.
                    This API will be removed in version 5.0.
                """
                warnings.warn(
                    "ListBox.contents is a property, not a method: use it without calling it. "
                    "API will be removed in version 5.0.",
                    DeprecationWarning,
                    stacklevel=3,
                )
                return inner_self

        return ListBoxContents()

    def _contents__getitem__(self, key: _K) -> tuple[AbstractFlowWidget, None]:
        # try list walker protocol v2 first
        """
        Return the ``(widget, options)`` pair at *key*, for the container contents protocol.

        :raises TypeError: the body does not implement ``set_focus``.
        :raises KeyError: *key* is not a position in the body.
        """
        if hasattr(self._body, "__getitem__"):
            try:
                return (self._body[key], None)
            except (IndexError, KeyError) as exc:
                raise KeyError(f"ListBox.contents key not found: {key!r}").with_traceback(exc.__traceback__) from exc
        # fall back to v1
        if not hasattr(self._body, "set_focus"):
            raise TypeError(f"{type(self._body)}.set_focus is not implemented.")

        _w, old_focus = self._body.get_focus()

        try:
            self._body.set_focus(key)
            # protocol v1 walkers produce the widget only, without the options placeholder
            return self._body.get_focus()[0]  # type: ignore[return-value]
        except (IndexError, KeyError) as exc:
            raise KeyError(f"ListBox.contents key not found: {key!r}").with_traceback(exc.__traceback__) from exc
        finally:
            self._body.set_focus(old_focus)

    @property
    def contents(self) -> ListBoxContentsProto[_K]:
        """An object that allows reading widgets from the ListBox's list walker.

        Widgets are read as a `(widget, options)` tuple. `None` is currently the only value for options.

        .. warning::

            This object may not be used to set or iterate over contents.

            You must use the list walker stored as
            :attr:`.body` to perform manipulation and iteration, if supported.
        """
        return self._contents()

    def options(self) -> None:
        """
        There are currently no options for ListBox contents.

        Return None as a placeholder for future options.
        """

    def _set_focus_valign_complete(self, size: tuple[int, int], focus: bool) -> None:
        """Finish setting the offset and inset now that we have have a maxcol & maxrow."""
        (maxcol, maxrow) = size
        pending = self.set_focus_valign_pending
        if pending is None:  # pragma: no cover  # only called with a pending valign
            return
        vt, va = pending
        self.set_focus_valign_pending = None
        self.set_focus_pending = None

        focus_widget, _focus_pos = self._body.get_focus()
        if focus_widget is None:
            return

        rows = focus_widget.rows((maxcol,), focus)
        rtop, _rbot = calculate_top_bottom_filler(
            maxrow,
            vt,
            va,
            WHSettings.GIVEN,
            rows,
            None,
            0,
            0,
        )

        # A 0-height focus aligned at or near the bottom would start below the last row: put it on the last row.
        self.shift_focus((maxcol, maxrow), min(rtop, maxrow - 1))

    def _set_focus_first_selectable(self, size: tuple[int, int], focus: bool) -> None:
        """Choose the first visible, selectable widget below the current focus as the focus widget.

        :raises TypeError: the body does not implement ``set_focus``.
        """
        (maxcol, maxrow) = size
        self.set_focus_valign_pending = None
        self.set_focus_pending = None
        middle, top, bottom = self.calculate_visible((maxcol, maxrow), focus=focus)
        if middle is None or top is None or bottom is None:
            return

        row_offset, focus_widget, _focus_pos, focus_rows, _cursor = middle  # pylint: disable=unpacking-non-sequence
        _trim_top, _fill_above = top  # pylint: disable=unpacking-non-sequence
        trim_bottom, fill_below = bottom  # pylint: disable=unpacking-non-sequence

        if focus_widget.selectable():
            return

        if not hasattr(self._body, "set_focus"):
            raise TypeError(f"{type(self._body)}.set_focus is not implemented.")

        if trim_bottom:
            fill_below = fill_below[:-1]
        new_row_offset = row_offset + focus_rows
        for widget, pos, rows in fill_below:
            if widget.selectable():
                self._body.set_focus(pos)
                self.shift_focus((maxcol, maxrow), new_row_offset)
                return
            new_row_offset += rows

    def _set_focus_complete(self, size: tuple[int, int], focus: bool) -> None:
        """Finish setting the position now that we have maxcol & maxrow.

        :raises TypeError: the body does not implement ``set_focus``.
        """
        (maxcol, maxrow) = size
        self._invalidate()
        if self.set_focus_pending == "first selectable":
            return self._set_focus_first_selectable((maxcol, maxrow), focus)
        if self.set_focus_valign_pending is not None:
            return self._set_focus_valign_complete((maxcol, maxrow), focus)
        pending = self.set_focus_pending
        if pending is None or isinstance(pending, str):  # pragma: no cover  # nothing to complete
            return None
        coming_from, _focus_widget, focus_pos = pending
        self.set_focus_pending = None

        # new position
        _new_focus_widget, position = self._body.get_focus()
        if focus_pos == position:
            # do nothing
            return None

        if not hasattr(self._body, "set_focus"):
            raise TypeError(f"{type(self._body)}.set_focus is not implemented.")

        # restore old focus temporarily
        try:
            self._body.set_focus(focus_pos)
        except (IndexError, KeyError):
            # A focus that did move means a "modified" signal callback raised, not that the position is gone.
            if self._body.get_focus()[1] != position:
                raise
            # The body lost the old focus position after ListBox.set_focus().
            # The walker still focuses the new position, and the placement below starts from it.

        middle, top, bottom = self.calculate_visible((maxcol, maxrow), focus)
        if middle is None or top is None or bottom is None:
            return None

        focus_offset, _focus_widget, focus_pos, focus_rows, _cursor = middle  # pylint: disable=unpacking-non-sequence
        _trim_top, fill_above = top  # pylint: disable=unpacking-non-sequence
        _trim_bottom, fill_below = bottom  # pylint: disable=unpacking-non-sequence

        offset = focus_offset
        for _widget, pos, rows in fill_above:
            offset -= rows
            if pos == position:
                self.change_focus((maxcol, maxrow), pos, offset, "below")
                return None

        offset = focus_offset + focus_rows
        for _widget, pos, rows in fill_below:
            if pos == position:
                self.change_focus((maxcol, maxrow), pos, offset, "above")
                return None
            offset += rows

        # failed to find widget among visible widgets
        self._body.set_focus(position)
        widget, position = self._body.get_focus()
        # focus was just set to `position`, so the walker has a focus widget
        rows = typing.cast("AbstractFlowWidget", widget).rows((maxcol,), focus)

        if coming_from == "below":
            offset = 0
        elif coming_from == "above":
            offset = maxrow - rows
        else:
            offset = (maxrow - rows) // 2
        self.shift_focus((maxcol, maxrow), offset)
        return None

    def shift_focus(self, size: tuple[int, int], offset_inset: int) -> None:
        """Move the location of the current focus relative to the top.

        This is used internally by methods that know the widget's *size*.

        See also :meth:`.set_focus_valign`.

        :param size: see :meth:`Widget.render` for details
        :param offset_inset: either the number of rows between the
            top of the listbox and the start of the focus widget (+ve
            value) or the number of lines of the focus widget hidden off
            the top edge of the listbox (-ve value) or ``0`` if the top edge
            of the focus widget is aligned with the top edge of the
            listbox.
        :raises ListBoxError: *offset_inset* falls outside the listbox rows or the rows of the focus widget.
        """
        (maxcol, maxrow) = size

        if offset_inset >= 0:
            if offset_inset >= maxrow:
                raise ListBoxError(f"Invalid offset_inset: {offset_inset!r}, only {maxrow!r} rows in list box")
            self.offset_rows = offset_inset
            self.inset_fraction = (0, 1)
        else:
            target, _ignore = self._body.get_focus()
            tgt_rows = typing.cast("AbstractFlowWidget", target).rows((maxcol,), True)
            if offset_inset + tgt_rows <= 0:
                raise ListBoxError(f"Invalid offset_inset: {offset_inset!r}, only {tgt_rows!r} rows in target!")
            self.offset_rows = 0
            self.inset_fraction = (-offset_inset, tgt_rows)
        self._invalidate()

    def update_pref_col_from_focus(self, size: tuple[int, int]) -> None:
        """Update self.pref_col from the focus widget."""
        # TODO: should this not be private?
        (maxcol, _maxrow) = size

        widget, _old_pos = self._body.get_focus()
        if widget is None:
            return

        pref_col = None
        if hasattr(widget, "get_pref_col"):
            pref_col = widget.get_pref_col((maxcol,))
        if pref_col is None and hasattr(widget, "get_cursor_coords"):
            coords = widget.get_cursor_coords((maxcol,))
            if isinstance(coords, tuple):
                pref_col, _y = coords
        if pref_col is not None:
            self.pref_col = pref_col

    def change_focus(
        self,
        size: tuple[int, int],
        position: _K,
        offset_inset: int = 0,
        coming_from: Literal["above", "below"] | None = None,
        cursor_coords: tuple[int | str, int] | tuple[int | str] | None = None,
        snap_rows: int | None = None,
    ) -> None:
        """Change the current focus widget.

        This is used internally by methods that know the widget's *size*.

        See also :meth:`.set_focus`.

        :param size: see :meth:`Widget.render` for details
        :param position: a position compatible with :meth:`self._body.set_focus`
        :param offset_inset: either the number of rows between the
            top of the listbox and the start of the focus widget (+ve
            value) or the number of lines of the focus widget hidden off
            the top edge of the listbox (-ve value) or 0 if the top edge
            of the focus widget is aligned with the top edge of the
            listbox (default if unspecified)
        :param coming_from: either 'above', 'below' or unspecified `None`
        :param cursor_coords: (x, y) tuple indicating the desired column and row for the cursor,
            a (x,) tuple indicating only the column for the cursor, or unspecified
        :param snap_rows: the maximum number of extra rows to scroll
            when trying to "snap" a selectable focus into the view
        :raises TypeError: the body does not implement ``set_focus``.
        :raises ListBoxError: *offset_inset* leaves no row of the target visible, or *cursor_coords* names a row outside
            the target widget.
        :raises ValueError: the cursor row is unspecified and *coming_from* is neither ``'above'`` nor ``'below'``.
        """
        (maxcol, maxrow) = size

        if not hasattr(self._body, "set_focus"):
            raise TypeError(f"{type(self._body)}.set_focus is not implemented.")

        # update pref_col before change
        if cursor_coords:
            self.pref_col = cursor_coords[0]
        else:
            self.update_pref_col_from_focus((maxcol, maxrow))

        self._invalidate()

        self._body.set_focus(position)
        widget, _ignore = self._body.get_focus()
        # focus was just set to `position`, so the walker has a focus widget
        target = typing.cast("AbstractFlowWidget", widget)
        tgt_rows = target.rows((maxcol,), True)
        if snap_rows is None:
            snap_rows = maxrow - 1

        # "snap" to selectable widgets
        align_top = 0
        align_bottom = maxrow - tgt_rows

        if coming_from == "above" and target.selectable() and offset_inset > align_bottom:
            if snap_rows >= offset_inset - align_bottom:
                offset_inset = align_bottom
            elif snap_rows >= offset_inset - align_top:
                offset_inset = align_top
            else:
                offset_inset -= snap_rows

        if coming_from == "below" and target.selectable() and offset_inset < align_top:
            if snap_rows >= align_top - offset_inset:
                offset_inset = align_top
            elif snap_rows >= align_bottom - offset_inset:
                offset_inset = align_bottom
            else:
                offset_inset += snap_rows

        # convert offset_inset to offset_rows or inset_fraction
        if offset_inset >= 0:
            self.offset_rows = offset_inset
            self.inset_fraction = (0, 1)
        else:
            if offset_inset + tgt_rows <= 0:
                raise ListBoxError(f"Invalid offset_inset: {offset_inset}, only {tgt_rows} rows in target!")
            self.offset_rows = 0
            self.inset_fraction = (-offset_inset, tgt_rows)

        if cursor_coords is None:
            if coming_from is None:
                return  # must either know row or coming_from
            cursor_coords = (self.pref_col,)

        if not hasattr(target, "move_cursor_to_coords"):
            return

        attempt_rows: Iterable[int] = []

        if len(cursor_coords) == 1:
            # only column (not row) specified
            # start from closest edge and move inwards
            (pref_col,) = cursor_coords
            if coming_from == "above":
                attempt_rows = range(tgt_rows)
            else:
                if coming_from != "below":
                    raise ValueError("must specify coming_from ('above' or 'below') if cursor row is not specified")
                attempt_rows = range(tgt_rows, -1, -1)
        else:
            # both column and row specified
            # start from preferred row and move back to closest edge
            (pref_col, pref_row) = cursor_coords
            if pref_row < 0 or pref_row >= tgt_rows:
                raise ListBoxError(
                    f"cursor_coords row outside valid range for target. pref_row:{pref_row!r} target_rows:{tgt_rows!r}"
                )

            if coming_from == "above":
                attempt_rows = range(pref_row, -1, -1)
            elif coming_from == "below":
                attempt_rows = range(pref_row, tgt_rows)
            else:
                attempt_rows = [pref_row]

        for row in attempt_rows:
            if target.move_cursor_to_coords((maxcol,), pref_col, row):
                break

    def get_focus_offset_inset(self, size: tuple[int, int]) -> tuple[int, int]:
        """Return (offset rows, inset rows) for focus widget.

        :raises ListBoxError: the stored inset fraction is invalid or exceeds the rows of the focus widget.
        """
        (maxcol, _maxrow) = size
        focus_widget, _pos = self._body.get_focus()
        focus_rows = typing.cast("AbstractFlowWidget", focus_widget).rows((maxcol,), True)
        offset_rows = self.offset_rows
        inset_rows = 0
        if offset_rows == 0:
            inum, iden = self.inset_fraction
            if inum < 0 or iden < 0 or inum >= iden:
                raise ListBoxError(f"Invalid inset_fraction: {self.inset_fraction!r}")
            inset_rows = focus_rows * inum // iden
            if inset_rows and inset_rows >= focus_rows:
                raise ListBoxError("urwid inset_fraction error (please report)")
        return offset_rows, inset_rows

    def make_cursor_visible(self, size: tuple[int, int]) -> None:
        """Shift the focus widget so that its cursor is visible."""
        (maxcol, maxrow) = size

        focus_widget, _pos = self._body.get_focus()
        if focus_widget is None:
            return
        if not focus_widget.selectable():
            return
        if not hasattr(focus_widget, "get_cursor_coords"):
            return
        cursor = focus_widget.get_cursor_coords((maxcol,))
        if cursor is None:
            return
        _cx, cy = cursor
        offset_rows, inset_rows = self.get_focus_offset_inset((maxcol, maxrow))

        if cy < inset_rows:
            self.shift_focus((maxcol, maxrow), -(cy))
            return

        if offset_rows - inset_rows + cy >= maxrow:
            self.shift_focus((maxcol, maxrow), maxrow - cy - 1)
            return

    def keypress(
        self,
        size: tuple[int, int],  # type: ignore[override]
        key: str,
    ) -> str | None:
        """Move selection through the list elements scrolling when necessary.

        Keystrokes are first passed to widget in focus in case that widget can handle them.

        Keystrokes handled by this widget are:

        :kbd:`up`
            up one line (or widget)
        :kbd:`down`
            down one line (or widget)
        :kbd:`page up`
            move cursor up one listbox length (or widget)
        :kbd:`page down`
            move cursor down one listbox length (or widget)
        """
        from urwid.command_map import Command

        (maxcol, maxrow) = size

        if self.set_focus_pending or self.set_focus_valign_pending:
            self._set_focus_complete((maxcol, maxrow), focus=True)

        focus_widget, _pos = self._body.get_focus()
        if focus_widget is None:  # empty listbox, can't do anything
            return key

        if focus_widget.selectable():
            if handled := focus_widget.keypress((maxcol,), key):
                key = handled
            else:
                self.make_cursor_visible((maxcol, maxrow))
                return None

        def actual_key(unhandled: str | bool | None) -> str | None:
            if unhandled:
                return key
            return None

        # pass off the heavy lifting
        if self._command_map[key] == Command.UP:
            return actual_key(self._keypress_up((maxcol, maxrow)))

        if self._command_map[key] == Command.DOWN:
            return actual_key(self._keypress_down((maxcol, maxrow)))

        if self._command_map[key] == Command.PAGE_UP:
            return actual_key(self._keypress_page_up((maxcol, maxrow)))

        if self._command_map[key] == Command.PAGE_DOWN:
            return actual_key(self._keypress_page_down((maxcol, maxrow)))

        if self._command_map[key] == Command.MAX_LEFT:
            return actual_key(self._keypress_max_left((maxcol, maxrow)))  # type: ignore[func-returns-value]

        if self._command_map[key] == Command.MAX_RIGHT:
            return actual_key(self._keypress_max_right((maxcol, maxrow)))  # type: ignore[func-returns-value]

        return key

    def _keypress_max_left(self, size: tuple[int, int]) -> None:
        """
        Move the focus to the first position of the body.

        :raises TypeError: the body does not implement ``positions``.
        """
        if not hasattr(self.body, "positions"):
            raise TypeError(f"{type(self.body)}.positions is not implemented.")

        self.focus_position = next(iter(self.body.positions()))
        self.set_focus_valign(VAlign.TOP)

    def _keypress_max_right(self, size: tuple[int, int]) -> None:
        """
        Move the focus to the last position of the body.

        :raises TypeError: the body does not implement ``positions``.
        """
        if not hasattr(self.body, "positions"):
            raise TypeError(f"{type(self.body)}.positions is not implemented.")

        self.focus_position = next(iter(self.body.positions(reverse=True)))
        self.set_focus_valign(VAlign.BOTTOM)

    def _keypress_up(self, size: tuple[int, int]) -> bool | None:
        (maxcol, maxrow) = size

        middle, top, _bottom = self.calculate_visible((maxcol, maxrow), True)
        if middle is None or top is None:
            return True

        focus_row_offset, focus_widget, focus_pos, _ignore, cursor = middle  # pylint: disable=unpacking-non-sequence
        _trim_top, fill_above = top  # pylint: disable=unpacking-non-sequence

        row_offset = focus_row_offset

        # look for selectable widget above
        pos = focus_pos
        widget = None
        for widget, pos, rows in fill_above:
            row_offset -= rows
            if rows and widget.selectable():
                # this one will do
                self.change_focus((maxcol, maxrow), pos, row_offset, "below")
                return None

        # at this point we must scroll
        row_offset += 1
        self._invalidate()

        # A wrapping body of 0-height widgets returns to a position without moving row_offset: stop there.
        scrolled = _SeenPositions()
        while row_offset > 0:
            # need to scroll in another candidate widget
            widget, pos = self._body.get_prev(pos)
            if widget is None or (pos, row_offset) in scrolled:
                # cannot scroll any further
                return True  # keypress not handled
            scrolled.add((pos, row_offset))
            rows = widget.rows((maxcol,), True)
            row_offset -= rows
            if rows and widget.selectable():
                # this one will do
                self.change_focus((maxcol, maxrow), typing.cast("_K", pos), row_offset, "below")
                return None

        if not focus_widget.selectable() or focus_row_offset + 1 >= maxrow:
            # just take top one if focus is not selectable
            # or if focus has moved out of view
            if widget is None:
                self.shift_focus((maxcol, maxrow), row_offset)
                return None
            self.change_focus((maxcol, maxrow), pos, row_offset, "below")
            return None

        # check if cursor will stop scroll from taking effect
        if cursor is not None:
            _x, y = cursor
            if y + focus_row_offset + 1 >= maxrow:
                # cursor position is a problem,
                # choose another focus
                if widget is None:
                    # try harder to get prev widget
                    widget, pos = self._body.get_prev(pos)
                    if widget is None:
                        return None  # can't do anything
                    rows = widget.rows((maxcol,), True)
                    row_offset -= rows

                if -row_offset >= rows:
                    # must scroll further than 1 line
                    row_offset = -(rows - 1)

                self.change_focus((maxcol, maxrow), pos, row_offset, "below")
                return None

        # if all else fails, just shift the current focus.
        self.shift_focus((maxcol, maxrow), focus_row_offset + 1)
        return None

    def _keypress_down(self, size: tuple[int, int]) -> bool | None:
        (maxcol, maxrow) = size

        middle, _top, bottom = self.calculate_visible((maxcol, maxrow), True)
        if middle is None:
            return True

        focus_row_offset, focus_widget, focus_pos, focus_rows, cursor = middle  # pylint: disable=unpacking-non-sequence
        _trim_bottom, fill_below = typing.cast("VisibleInfoTopBottom", bottom)  # pylint: disable=unpacking-non-sequence

        row_offset = focus_row_offset + focus_rows
        rows = focus_rows

        # look for selectable widget below
        pos = focus_pos
        widget = None
        for widget, pos, rows in fill_below:
            if rows and widget.selectable():
                # this one will do
                self.change_focus((maxcol, maxrow), pos, row_offset, "above")
                return None
            row_offset += rows

        # at this point we must scroll
        row_offset -= 1
        self._invalidate()

        # A wrapping body of 0-height widgets returns to a position without moving row_offset: stop there.
        scrolled = _SeenPositions()
        while row_offset < maxrow:
            # need to scroll in another candidate widget
            widget, pos = self._body.get_next(pos)
            if widget is None or (pos, row_offset) in scrolled:
                # cannot scroll any further
                return True  # keypress not handled
            scrolled.add((pos, row_offset))
            rows = widget.rows((maxcol,))
            if rows and widget.selectable():
                # this one will do
                self.change_focus((maxcol, maxrow), typing.cast("_K", pos), row_offset, "above")
                return None
            row_offset += rows

        if not focus_widget.selectable() or focus_row_offset + focus_rows - 1 <= 0:
            # just take bottom one if current is not selectable
            # or if focus has moved out of view
            if widget is None:
                self.shift_focus((maxcol, maxrow), row_offset - rows)
                return None
            self.change_focus((maxcol, maxrow), pos, row_offset - rows, "above")
            return None

        # check if cursor will stop scroll from taking effect
        if cursor is not None:
            _x, y = cursor
            if y + focus_row_offset - 1 < 0:
                # cursor position is a problem,
                # choose another focus
                if widget is None:
                    # try harder to get next widget
                    widget, pos = self._body.get_next(pos)
                    if widget is None:
                        return None  # can't do anything
                else:
                    row_offset -= rows

                if row_offset >= maxrow:
                    # must scroll further than 1 line
                    row_offset = maxrow - 1

                self.change_focus(
                    (maxcol, maxrow),
                    pos,
                    row_offset,
                    "above",
                )
                return None

        # if all else fails, keep the current focus.
        self.shift_focus((maxcol, maxrow), focus_row_offset - 1)
        return None

    def _keypress_page_up(self, size: tuple[int, int]) -> bool | None:
        (maxcol, maxrow) = size

        middle, top, _bottom = self.calculate_visible((maxcol, maxrow), True)
        if middle is None or top is None:
            return True

        row_offset, focus_widget, focus_pos, focus_rows, cursor = middle  # pylint: disable=unpacking-non-sequence
        _trim_top, fill_above = top  # pylint: disable=unpacking-non-sequence

        # topmost_visible is row_offset rows above top row of
        # focus (+ve) or -row_offset rows below top row of focus (-ve)
        topmost_visible = row_offset

        # scroll_from_row is (first match)
        # 1. topmost visible row if focus is not selectable
        # 2. row containing cursor if focus has a cursor
        # 3. top row of focus widget if it is visible
        # 4. topmost visible row otherwise
        if not focus_widget.selectable():
            scroll_from_row = topmost_visible
        elif cursor is not None:
            _x, y = cursor
            scroll_from_row = -y
        elif row_offset >= 0:
            scroll_from_row = 0
        else:
            scroll_from_row = topmost_visible

        # snap_rows is maximum extra rows to scroll when
        # snapping to new a focus
        snap_rows = topmost_visible - scroll_from_row

        # move row_offset to the new desired value (1 "page" up)
        row_offset = scroll_from_row + maxrow

        # not used below:
        del scroll_from_row, topmost_visible

        # gather potential target widgets and add current focus
        t = [(row_offset, focus_widget, focus_pos, focus_rows)]
        pos = focus_pos
        # include widgets from calculate_visible(..)
        for widget, pos, rows in fill_above:
            row_offset -= rows
            t.append((row_offset, widget, pos, rows))
        # add newly visible ones, including within snap_rows
        snap_region_start = len(t)
        # A wrapping body of 0-height widgets returns to a position without moving row_offset: stop there.
        scrolled = _SeenPositions()
        while row_offset > -snap_rows:
            prev_widget, pos = self._body.get_prev(pos)
            if prev_widget is None or (pos, row_offset) in scrolled:
                break
            scrolled.add((pos, row_offset))
            rows = prev_widget.rows((maxcol,))
            row_offset -= rows
            # determine if one below puts current one into snap rgn
            if row_offset > 0:
                snap_region_start += 1
            t.append((row_offset, prev_widget, pos, rows))

        # if we can't fill the top we need to adjust the row offsets
        row_offset, _w, _p, _r = t[-1]
        if row_offset > 0:
            adjust = -row_offset
            t = [(ro + adjust, w, p, r) for (ro, w, p, r) in t]

        # if focus_widget (first in t) is off edge, remove it
        row_offset, _w, _p, _r = t[0]
        if row_offset >= maxrow:
            del t[0]
            snap_region_start -= 1

        # we'll need this soon
        self.update_pref_col_from_focus((maxcol, maxrow))

        # choose the topmost selectable and (newly) visible widget
        # search within snap_rows then visible region
        search_order = list(range(snap_region_start, len(t))) + list(range(snap_region_start - 1, -1, -1))
        # assert 0, repr((t, search_order))
        bad_choices = []
        cut_off_selectable_chosen = 0
        for i in search_order:
            row_offset, widget, pos, rows = t[i]
            if not widget.selectable():
                continue

            if not rows:
                continue

            # try selecting this widget
            pref_row = max(0, -row_offset)

            # if completely within snap region, adjust row_offset
            if rows + row_offset <= 0:
                self.change_focus(
                    (maxcol, maxrow),
                    pos,
                    -(rows - 1),
                    "below",
                    (self.pref_col, rows - 1),
                    snap_rows - ((-row_offset) - (rows - 1)),
                )
            else:
                self.change_focus(
                    (maxcol, maxrow),
                    pos,
                    row_offset,
                    "below",
                    (self.pref_col, pref_row),
                    snap_rows,
                )

            # if we're as far up as we can scroll, take this one
            if fill_above and self._body.get_prev(fill_above[-1][1]) == (None, None):
                pass  # return

            # find out where that actually puts us
            middle, top, _bottom = self.calculate_visible((maxcol, maxrow), True)
            act_middle = typing.cast("VisibleInfoMiddle", middle)
            act_row_offset, _ign1, _ign2, _ign3, _ign4 = act_middle  # pylint: disable=unpacking-non-sequence

            # discard chosen widget if it will reduce scroll amount
            # because of a fixed cursor (absolute last resort)
            if act_row_offset > row_offset + snap_rows:
                bad_choices.append(i)
                continue
            if act_row_offset < row_offset:
                bad_choices.append(i)
                continue

            # also discard if off top edge (second last resort)
            if act_row_offset < 0:
                bad_choices.append(i)
                cut_off_selectable_chosen = 1
                continue

            return None

        # anything selectable is better than what follows:
        if cut_off_selectable_chosen:
            return None

        if fill_above and focus_widget.selectable() and self._body.get_prev(fill_above[-1][1]) == (None, None):
            # if we're at the top and have a selectable, return
            pass  # return

        # if still none found choose the topmost widget
        good_choices = [j for j in search_order if j not in bad_choices]
        for i in good_choices + search_order:
            row_offset, widget, pos, rows = t[i]
            if pos == focus_pos:
                continue

            if not rows:  # never focus a 0-height widget
                continue

            # if completely within snap region, adjust row_offset
            if rows + row_offset <= 0:
                snap_rows -= (-row_offset) - (rows - 1)
                row_offset = -(rows - 1)

            self.change_focus((maxcol, maxrow), pos, row_offset, "below", None, snap_rows)
            return None

        # no choices available, just shift current one
        self.shift_focus((maxcol, maxrow), min(maxrow - 1, row_offset))

        # final check for pathological case where we may fall short
        middle, top, _bottom = self.calculate_visible((maxcol, maxrow), True)
        act_middle = typing.cast("VisibleInfoMiddle", middle)
        act_row_offset, _act_w, pos, _act_rows, _act_cursor = act_middle  # pylint: disable=unpacking-non-sequence
        if act_row_offset >= row_offset:
            # no problem
            return None

        # fell short, try to select anything else above
        if not t:
            return None
        _last_offset, _last_w, pos, _last_rows = t[-1]
        prev_widget, pos = self._body.get_prev(pos)
        if prev_widget is None:
            # no dice, we're stuck here
            return None
        # bring in only one row if possible
        rows = prev_widget.rows((maxcol,), True)
        if not rows:  # never focus a 0-height widget
            return None
        self.change_focus(
            (maxcol, maxrow),
            typing.cast("_K", pos),
            -(rows - 1),
            "below",
            (self.pref_col, rows - 1),
            0,
        )
        return None

    def _keypress_page_down(self, size: tuple[int, int]) -> bool | None:
        (maxcol, maxrow) = size

        middle, _top, bottom = self.calculate_visible((maxcol, maxrow), True)
        if middle is None or bottom is None:
            return True

        row_offset, focus_widget, focus_pos, focus_rows, cursor = middle  # pylint: disable=unpacking-non-sequence
        _trim_bottom, fill_below = bottom  # pylint: disable=unpacking-non-sequence

        # bottom_edge is maxrow-focus_pos rows below top row of focus
        bottom_edge = maxrow - row_offset

        # scroll_from_row is (first match)
        # 1. bottom edge if focus is not selectable
        # 2. row containing cursor + 1 if focus has a cursor
        # 3. bottom edge of focus widget if it is visible
        # 4. bottom edge otherwise
        if not focus_widget.selectable():
            scroll_from_row = bottom_edge
        elif cursor is not None:
            _x, y = cursor
            scroll_from_row = y + 1
        elif bottom_edge >= focus_rows:
            scroll_from_row = focus_rows
        else:
            scroll_from_row = bottom_edge

        # snap_rows is maximum extra rows to scroll when
        # snapping to new a focus
        snap_rows = bottom_edge - scroll_from_row

        # move row_offset to the new desired value (1 "page" down)
        row_offset = -scroll_from_row

        # not used below:
        del scroll_from_row, bottom_edge

        # gather potential target widgets and add current focus
        t = [(row_offset, focus_widget, focus_pos, focus_rows)]
        pos = focus_pos
        row_offset += focus_rows
        # include widgets from calculate_visible(..)
        for widget, pos, rows in fill_below:
            t.append((row_offset, widget, pos, rows))
            row_offset += rows
        # add newly visible ones, including within snap_rows
        snap_region_start = len(t)
        # A wrapping body of 0-height widgets returns to a position without moving row_offset: stop there.
        scrolled = _SeenPositions()
        while row_offset < maxrow + snap_rows:
            next_widget, pos = self._body.get_next(pos)
            if next_widget is None or (pos, row_offset) in scrolled:
                break
            scrolled.add((pos, row_offset))
            rows = next_widget.rows((maxcol,))
            t.append((row_offset, next_widget, pos, rows))
            row_offset += rows
            # determine if one above puts current one into snap rgn
            if row_offset < maxrow:
                snap_region_start += 1

        # if we can't fill the bottom we need to adjust the row offsets
        row_offset, _w, _p, rows = t[-1]
        if row_offset + rows < maxrow:
            adjust = maxrow - (row_offset + rows)
            t = [(ro + adjust, w, p, r) for (ro, w, p, r) in t]

        # if focus_widget (first in t) is off edge, remove it
        row_offset, _w, _p, rows = t[0]
        if row_offset + rows <= 0:
            del t[0]
            snap_region_start -= 1

        # we'll need this soon
        self.update_pref_col_from_focus((maxcol, maxrow))

        # choose the bottommost selectable and (newly) visible widget
        # search within snap_rows then visible region
        search_order = list(range(snap_region_start, len(t))) + list(range(snap_region_start - 1, -1, -1))
        # assert 0, repr((t, search_order))
        bad_choices = []
        cut_off_selectable_chosen = 0
        for i in search_order:
            row_offset, widget, pos, rows = t[i]
            if not widget.selectable():
                continue

            if not rows:
                continue

            # try selecting this widget
            pref_row = min(maxrow - row_offset - 1, rows - 1)

            # if completely within snap region, adjust row_offset
            if row_offset >= maxrow:
                self.change_focus(
                    (maxcol, maxrow),
                    pos,
                    maxrow - 1,
                    "above",
                    (self.pref_col, 0),
                    snap_rows + maxrow - row_offset - 1,
                )
            elif row_offset + rows <= 0:
                self.change_focus(
                    (maxcol, maxrow),
                    pos,
                    -(rows - 1),
                    "above",
                    (self.pref_col, rows - 1),
                    snap_rows - ((-row_offset) - (rows - 1)),
                )
            else:
                self.change_focus(
                    (maxcol, maxrow),
                    pos,
                    row_offset,
                    "above",
                    (self.pref_col, pref_row),
                    snap_rows,
                )

            # find out where that actually puts us
            middle, _top, bottom = self.calculate_visible((maxcol, maxrow), True)
            act_middle = typing.cast("VisibleInfoMiddle", middle)
            act_row_offset, _ign1, _ign2, _ign3, _ign4 = act_middle  # pylint: disable=unpacking-non-sequence

            # discard chosen widget if it will reduce scroll amount
            # because of a fixed cursor (absolute last resort)
            if act_row_offset < row_offset - snap_rows:
                bad_choices.append(i)
                continue
            if act_row_offset > row_offset:
                bad_choices.append(i)
                continue

            # also discard if off top edge (second last resort)
            if act_row_offset + rows > maxrow:
                bad_choices.append(i)
                cut_off_selectable_chosen = 1
                continue

            return None

        # anything selectable is better than what follows:
        if cut_off_selectable_chosen:
            return None

        # if still none found choose the bottommost widget
        good_choices = [j for j in search_order if j not in bad_choices]
        for i in good_choices + search_order:
            row_offset, widget, pos, rows = t[i]
            if pos == focus_pos:
                continue

            if not rows:  # never focus a 0-height widget
                continue

            # if completely within snap region, adjust row_offset
            if row_offset >= maxrow:
                snap_rows -= snap_rows + maxrow - row_offset - 1
                row_offset = maxrow - 1
            elif row_offset + rows <= 0:
                snap_rows -= (-row_offset) - (rows - 1)
                row_offset = -(rows - 1)

            self.change_focus((maxcol, maxrow), pos, row_offset, "above", None, snap_rows)
            return None

        # no choices available, just shift current one
        # The bottom fill puts a trailing 0-height widget at row maxrow, below the last row: use the last row.
        self.shift_focus((maxcol, maxrow), min(maxrow - 1, max(1 - focus_rows, row_offset)))

        # final check for pathological case where we may fall short
        middle, _top, bottom = self.calculate_visible((maxcol, maxrow), True)
        act_middle = typing.cast("VisibleInfoMiddle", middle)
        act_row_offset, _act_w, pos, _act_rows, _act_cursor = act_middle  # pylint: disable=unpacking-non-sequence
        if act_row_offset <= row_offset:
            # no problem
            return None

        # fell short, try to select anything else below
        if not t:
            return None
        _last_offset, _last_w, pos, _last_rows = t[-1]
        next_widget, pos = self._body.get_next(pos)
        if next_widget is None:
            # no dice, we're stuck here
            return None
        # bring in only one row if possible
        rows = next_widget.rows((maxcol,), True)
        if not rows:  # never focus a 0-height widget
            return None
        self.change_focus(
            (maxcol, maxrow),
            typing.cast("_K", pos),
            maxrow - 1,
            "above",
            (self.pref_col, 0),
            0,
        )
        return None

    def mouse_event(
        self,
        size: tuple[int, int],  # type: ignore[override]
        event: str,
        button: int,
        col: int,
        row: int,
        focus: bool,
    ) -> bool | None:
        """Pass the event to the contained widgets.

        May change focus on button 1 press.
        """
        from urwid.util import is_mouse_press

        (maxcol, maxrow) = size
        middle, top, bottom = self.calculate_visible((maxcol, maxrow), focus=True)
        if middle is None or top is None or bottom is None:
            return False

        _ignore, focus_widget, focus_pos, focus_rows, _cursor = middle  # pylint: disable=unpacking-non-sequence
        trim_top, fill_above = top  # pylint: disable=unpacking-non-sequence
        _ignore, fill_below = bottom  # pylint: disable=unpacking-non-sequence

        fill_above.reverse()  # fill_above is in bottom-up order
        w_list = [*fill_above, (focus_widget, focus_pos, focus_rows), *fill_below]

        wrow = -trim_top
        for w, w_pos, w_rows in w_list:  # noqa: B007  # magic with scope
            if wrow + w_rows > row:
                break
            wrow += w_rows
        else:
            return False

        focus = focus and w == focus_widget
        if is_mouse_press(event) and button == 1 and w.selectable():
            self.change_focus((maxcol, maxrow), w_pos, wrow)

        if not hasattr(w, "mouse_event"):
            warnings.warn(
                f"{w.__class__.__module__}.{w.__class__.__name__} is  not implementing Widget API",
                DeprecationWarning,
                stacklevel=2,
            )
            return False

        if w.mouse_event((maxcol,), event, button, col, row - wrow, focus):
            return True

        if is_mouse_press(event):
            if button == 4:
                return not self._keypress_up((maxcol, maxrow))

            if button == 5:
                return not self._keypress_down((maxcol, maxrow))

        return False

    def ends_visible(self, size: tuple[int, int], focus: bool = False) -> list[Literal["top", "bottom"]]:
        """
        Return a list that may contain ``'top'`` and/or ``'bottom'``.

        i.e. this function will return one of: [], [``'top'``],
        [``'bottom'``] or [``'top'``, ``'bottom'``].

        convenience function for checking whether the top and bottom
        of the list are visible
        """
        (maxcol, maxrow) = size
        result = []
        middle, top, bottom = self.calculate_visible((maxcol, maxrow), focus=focus)
        if middle is None or top is None or bottom is None:  # empty listbox
            return ["top", "bottom"]
        trim_top, above = top  # pylint: disable=unpacking-non-sequence
        trim_bottom, below = bottom  # pylint: disable=unpacking-non-sequence

        if trim_bottom == 0:
            row_offset, _w, pos, rows, _c = middle  # pylint: disable=unpacking-non-sequence
            row_offset += rows
            for _w, pos, rows in below:  # noqa: B007  # magic with scope
                row_offset += rows
            if row_offset < maxrow or (self._body.get_next(pos) == (None, None)):
                result.append("bottom")

        if trim_top == 0:
            row_offset, _w, pos, _rows, _c = middle  # pylint: disable=unpacking-non-sequence
            for _w, pos, rows in above:  # noqa: B007  # magic with scope
                row_offset -= rows
            if self._body.get_prev(pos) == (None, None):
                result.insert(0, "top")

        return result  # type: ignore[return-value]

    def __iter__(self) -> Iterator[_K]:
        """
        Return an iterator over the positions in this ListBox.

        If self._body does not implement positions() then iterate
        from the focus widget down to the bottom, then from above
        the focus up to the top.  This is the best we can do with
        a minimal list walker implementation.
        """
        if positions_fn := getattr(self._body, "positions", None):
            yield from positions_fn()
            return

        focus_widget, focus_pos = self._body.get_focus()
        if focus_widget is None:
            return
        pos = focus_pos
        while True:
            yield typing.cast("_K", pos)
            w, pos = self._body.get_next(pos)
            if not w:
                break
        pos = focus_pos
        while True:
            w, pos = self._body.get_prev(pos)
            if not w:
                break
            yield typing.cast("_K", pos)

    def __reversed__(self) -> Iterator[_K]:
        """
        Return a reversed iterator over the positions in this ListBox.

        If :attr:`body` does not implement :meth:`positions` then iterate
        from above the focus widget up to the top, then from the focus
        widget down to the bottom.  Note that this is not actually the
        reverse of what `__iter__()` produces, but this is the best we can
        do with a minimal list walker implementation.
        """
        if positions_fn := getattr(self._body, "positions", None):
            yield from positions_fn(reverse=True)
            return

        focus_widget, focus_pos = self._body.get_focus()
        if focus_widget is None:
            return
        pos = focus_pos
        while True:
            w, pos = self._body.get_prev(pos)
            if not w:
                break
            yield typing.cast("_K", pos)
        pos = focus_pos
        while True:
            yield typing.cast("_K", pos)
            w, pos = self._body.get_next(pos)
            if not w:
                break
urwid-urwid-69907e4/urwid/widget/monitored_deque.py000066400000000000000000000711751526073734300224640ustar00rootroot00000000000000# Urwid MonitoredDeque class
#    Copyright (C) 2026  urwid contributors
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/

"""Deques that call a callback when their contents or focus change."""

from __future__ import annotations

import collections
import contextlib
import functools
import operator
import typing
import warnings

if typing.TYPE_CHECKING:
    from collections.abc import Callable, Iterable, Iterator

    from typing_extensions import Concatenate, ParamSpec

    ArgSpec = ParamSpec("ArgSpec")
    Ret = typing.TypeVar("Ret")

__all__ = ("MonitoredDeque", "MonitoredFocusDeque")

_T = typing.TypeVar("_T")


def _call_modified(
    fn: Callable[Concatenate[MonitoredDeque[typing.Any], ArgSpec], Ret],
) -> Callable[Concatenate[MonitoredDeque[typing.Any], ArgSpec], Ret]:
    @functools.wraps(fn)
    def call_modified_wrapper(
        self: MonitoredDeque[typing.Any],
        /,
        *args: ArgSpec.args,
        **kwargs: ArgSpec.kwargs,
    ) -> Ret:
        rval = fn(self, *args, **kwargs)
        self._modified()  # pylint: disable=protected-access
        return rval

    return call_modified_wrapper


def _insert_index(owner: str, index: typing.SupportsIndex, *, stacklevel: int) -> int:
    """Convert an ``insert()`` position to an integer as ``deque.insert()`` does.

    An object without ``__index__`` that ``int()`` accepts is still converted by ``int()``, with a warning.

    :param owner: class name for the warning message
    :param index: position passed to ``insert()``
    :param stacklevel: ``warnings.warn`` stack level of the caller of ``insert()``, counted from this function
    :returns: the position as an integer
    :raises TypeError: *index* has no ``__index__`` and ``int()`` rejects its type.
    :raises ValueError: ``int()`` rejects the value of *index*, such as a NaN or an unparsable string.
    :raises OverflowError: *index* is an infinite float.
    """
    with contextlib.suppress(TypeError):
        return operator.index(index)
    idx = int(index)
    warnings.warn(
        f"{owner}.insert() with an index of type {type(index).__name__} is deprecated: "
        "pass an int or an object with __index__. "
        "Support will be removed in version 5.0, after which it raises TypeError like deque.insert().",
        DeprecationWarning,
        stacklevel=stacklevel + 1,
    )
    return idx


class MonitoredDeque(collections.deque[_T], typing.Generic[_T]):
    """Trigger a callback any time its contents are changed with the usual deque operations append, extend, etc.

    It mirrors :class:`MonitoredList`, adapted to ``collections.deque``'s
    narrower API. Deliberate differences from :class:`MonitoredList`:

    * ``sort()`` is not provided -- ``deque`` itself has no such method.
    * ``__add__``/``__rmul__``/``__imul__`` are not provided -- ``deque``
      itself raises ``TypeError`` for concatenation/repetition, so there is
      nothing meaningful to wrap.
    * ``maxlen`` needs no dedicated property: it is already a read-only
      attribute inherited from ``deque``.

    **Eviction notification**: a bounded (``maxlen``-limited) deque may
    silently evict an item from the opposite end during ``append``,
    ``appendleft``, ``extend`` or ``extendleft``. As with every other
    mutation, ``_modified()`` fires exactly once per call in that case too
    -- there is deliberately no separate "eviction" signal, matching
    :class:`MonitoredList`'s existing philosophy (e.g. ``extend()`` fires
    once regardless of how many items were added). This keeps the
    ``_modified_callback`` contract (``Callable[[], Any]``, no parameters)
    unchanged and leaves room for a future, dedicated eviction signal
    without that being a breaking change. :class:`MonitoredFocusDeque`, the
    one place that needs eviction *detail* to keep focus correct, computes
    it itself from ``len(self)``/``self.maxlen`` before calling ``super()``
    -- exactly as :class:`MonitoredFocusList` already computes its own
    focus adjustments rather than relying on :class:`MonitoredList` to
    supply them.
    """

    _modified_callback: Callable[[], typing.Any] | None = None

    def __init__(self, iterable: Iterable[_T] = (), maxlen: int | None = None) -> None:
        """Build the deque from `iterable`, bounded to `maxlen` items, with no callback set yet."""
        super().__init__(iterable, maxlen)

    def _modified(self) -> None:
        if self._modified_callback is not None:
            self._modified_callback()

    def set_modified_callback(self, callback: Callable[[], typing.Any]) -> None:
        r"""Assign a callback function with no parameters that is called any time the deque is modified.

        Callback's return value is ignored.

        >>> import sys
        >>> md = MonitoredDeque([1, 2, 3])
        >>> md.set_modified_callback(lambda: sys.stdout.write("modified\n"))
        >>> md
        MonitoredDeque([1, 2, 3])
        >>> md.append(10)
        modified
        >>> len(md)
        4
        >>> md += [11, 12, 13]
        modified
        >>> md.clear()
        modified
        >>> md
        MonitoredDeque([])

        Eviction from a bounded deque still fires the callback exactly once:

        >>> bounded = MonitoredDeque([1, 2, 3], maxlen=3)
        >>> bounded.set_modified_callback(lambda: sys.stdout.write("modified\n"))
        >>> bounded.append(4)
        modified
        >>> bounded
        MonitoredDeque([2, 3, 4], maxlen=3)
        """
        self._modified_callback = callback

    def __repr__(self) -> str:
        """Return a constructor-call-like representation of the deque's contents and maxlen."""
        if self.maxlen is None:
            return f"{self.__class__.__name__}({list(self)!r})"
        return f"{self.__class__.__name__}({list(self)!r}, maxlen={self.maxlen!r})"

    # noinspection PyMethodParameters
    def __rich_repr__(self) -> Iterator[tuple[str | None, typing.Any] | typing.Any]:
        """Yield this deque's items and its maxlen, for `rich`'s repr protocol."""
        for item in self:
            yield None, item
        yield "maxlen", self.maxlen

    @_call_modified
    def __setitem__(self, __key: typing.SupportsIndex, __value: _T) -> None:  # type: ignore[override]
        """Set the item at `__key` to `__value`."""
        # deque has no slice assignment, unlike list/MutableSequence -- see the class docstring.
        super().__setitem__(__key, __value)

    @_call_modified
    def __delitem__(self, __key: typing.SupportsIndex) -> None:  # type: ignore[override]
        """Delete the item at `__key`."""
        # deque has no slice deletion, unlike list/MutableSequence -- see the class docstring.
        super().__delitem__(__key)

    @_call_modified
    def __iadd__(self, __value: Iterable[_T]) -> MonitoredDeque[_T]:
        """Extend the deque in place with the items from `__value`."""
        return super().__iadd__(__value)

    @_call_modified
    def append(self, __object: _T) -> None:
        """Append ``__object`` to the right end of the deque."""
        super().append(__object)

    @_call_modified
    def appendleft(self, __object: _T) -> None:
        """Append ``__object`` to the left end of the deque."""
        super().appendleft(__object)

    @_call_modified
    def extend(self, __iterable: Iterable[_T]) -> None:
        """Extend the right end of the deque with items from ``__iterable``."""
        super().extend(__iterable)

    @_call_modified
    def extendleft(self, __iterable: Iterable[_T]) -> None:
        """Extend the left end of the deque with items from ``__iterable``, in reverse order."""
        super().extendleft(__iterable)

    @_call_modified
    def pop(self) -> _T:  # type: ignore[override]  # deque.pop takes no argument, unlike list.pop
        """Remove and return the item at the right end of the deque."""
        return super().pop()

    @_call_modified
    def popleft(self) -> _T:
        """Remove and return the item at the left end of the deque."""
        return super().popleft()

    @_call_modified
    def insert(self, __index: typing.SupportsIndex, __object: _T) -> None:
        """Insert ``__object`` before ``__index``.

        .. deprecated:: 4.2.5
            An ``__index`` without ``__index__`` that ``int()`` accepts (a float, a ``Decimal``, a numeric string)
            is converted by ``int()`` with a :exc:`DeprecationWarning`.
            Pass an integer instead.
            From version 5.0 such an index raises :exc:`TypeError`, as ``deque.insert()`` does.

        :raises TypeError: ``__index`` has no ``__index__`` and ``int()`` rejects its type.
        :raises ValueError: ``int()`` rejects the value of ``__index``, such as a NaN or an unparsable string.
        :raises OverflowError: ``__index`` is an infinite float or does not fit a C ``ssize_t``.
        :raises IndexError: the deque is bounded and already full.
        """
        # _call_modified's wrapper sits between this method and its caller.
        super().insert(_insert_index(type(self).__name__, __index, stacklevel=3), __object)

    @_call_modified
    def remove(self, __value: _T) -> None:
        """Remove the first occurrence of ``__value``."""
        super().remove(__value)

    @_call_modified
    def reverse(self) -> None:
        """Reverse the deque in place."""
        super().reverse()

    @_call_modified
    def rotate(self, __n: int = 1) -> None:
        """Rotate the deque ``__n`` steps to the right (or left, for negative ``__n``)."""
        super().rotate(__n)

    @_call_modified
    def clear(self) -> None:
        """Remove all items from the deque."""
        super().clear()


class MonitoredFocusDeque(MonitoredDeque[_T], typing.Generic[_T]):
    """Trigger a callback any time its contents are modified, and any time the focus index is changed.

    Reuses the ``focus``/``_focus_changed``/``set_focus_changed_callback``
    machinery verbatim from :class:`MonitoredFocusList`.

    Deliberately **not** provided (an intentional simplification, unlike
    :class:`MonitoredFocusList`): ``_validate_contents_modified_callback``/
    ``set_validate_contents_modified`` and the slice-based
    ``_adjust_focus_on_contents_modified``. ``deque`` has no slice mutation,
    so there is no meaningful external-validation hook analogous to the list
    version. Instead a single-index helper, ``_adjust_focus_on_single_change``,
    is used by every mutating method below, since every ``deque`` mutation
    touches either a single item or the whole collection.

    **Edge cases:**

    * If the deque becomes empty, ``focus`` reads as ``None`` via the
      inherited getter -- no special-case code is needed for this.
    * If the focus item itself is evicted by a bounded ``append``/
      ``appendleft``, focus lands on the new boundary element -- it is
      never left dangling.
    * ``maxlen=0`` is legal (``deque(maxlen=0)`` stays permanently empty);
      ``focus`` stays ``None`` throughout, since ``not self`` is always
      true in that case:

      >>> mfd = MonitoredFocusDeque([1, 2, 3], maxlen=0)
      >>> mfd
      MonitoredFocusDeque([], maxlen=0, focus=None)
      >>> mfd.append(1)
      >>> print(mfd.focus)
      None
      >>> mfd
      MonitoredFocusDeque([], maxlen=0, focus=None)

    Replacing an item at a given index never changes which index is in
    focus (there is no dedicated ``__setitem__`` override for this reason --
    the inherited :meth:`MonitoredDeque.__setitem__` already covers it):

    >>> mfd = MonitoredFocusDeque([0, 1, 2, 3], focus=2)
    >>> mfd[0] = 9
    >>> mfd
    MonitoredFocusDeque([9, 1, 2, 3], focus=2)

    A longer example, exercising ``maxlen``, ``appendleft`` past capacity
    with a visible focus shift, ``popleft`` and ``rotate``:

    >>> mfd = MonitoredFocusDeque([1, 2, 3], maxlen=3, focus=2)
    >>> mfd
    MonitoredFocusDeque([1, 2, 3], maxlen=3, focus=2)
    >>> mfd.appendleft(0)
    >>> mfd
    MonitoredFocusDeque([0, 1, 2], maxlen=3, focus=2)
    """

    _focus_changed_callback: Callable[[int], typing.Any] | None = None

    def __init__(
        self,
        iterable: Iterable[_T] = (),
        maxlen: int | None = None,
        *,
        focus: int = 0,
    ) -> None:
        """Initialize a deque that tracks one item as the focus item.

        If items are inserted or removed -- including items silently evicted by a bounded (``maxlen``-limited)
        deque -- it will update the focus.

        >>> mfd = MonitoredFocusDeque([10, 11, 12, 13, 14], focus=3)
        >>> mfd
        MonitoredFocusDeque([10, 11, 12, 13, 14], focus=3)
        >>> del mfd[1]
        >>> mfd
        MonitoredFocusDeque([10, 12, 13, 14], focus=2)
        >>> mfd.popleft()
        10
        >>> mfd
        MonitoredFocusDeque([12, 13, 14], focus=1)
        >>> mfd.clear()
        >>> mfd
        MonitoredFocusDeque([], focus=None)
        """
        super().__init__(iterable, maxlen)
        self._focus = focus

    def __repr__(self) -> str:
        """Return a constructor-call-like representation of the deque's contents, maxlen, and focus."""
        if self.maxlen is None:
            return f"{self.__class__.__name__}({list(self)!r}, focus={self.focus!r})"
        return f"{self.__class__.__name__}({list(self)!r}, maxlen={self.maxlen!r}, focus={self.focus!r})"

    @property
    def focus(self) -> int | None:
        """Get/set the focus index.

        This value is read as None when the deque is empty, and may only be set to a value between 0 and
        len(self)-1 or an IndexError will be raised.

        Return the index of the item "in focus" or None if
        the deque is empty.

        >>> MonitoredFocusDeque([1, 2, 3], focus=2).focus
        2
        >>> MonitoredFocusDeque().focus
        """
        if not self:
            return None
        return self._focus

    @focus.setter
    def focus(self, index: int) -> None:
        """Set the focus index.

        May call ``self._focus_changed`` when the focus is modified, passing the new focus
        position to the callback just before changing the old focus setting. The callback may
        be assigned with :meth:`set_focus_changed_callback`.

        :param index: index into this deque; any index out of range raises an ``IndexError``,
            except when the deque is empty, in which case the index passed is ignored
        :raises TypeError: *index* is not an integer.
        :raises IndexError: *index* is outside the range of this sequence.

        >>> mfd = MonitoredFocusDeque([9, 10, 11])
        >>> mfd.focus = 2
        >>> mfd.focus
        2
        >>> mfd.focus = -2
        Traceback (most recent call last):
        ...
        IndexError: focus index is out of range: -2
        """
        if not self:
            self._focus = 0
            return
        if not isinstance(index, int):
            raise TypeError("index must be an integer")
        if index < 0 or index >= len(self):
            raise IndexError(f"focus index is out of range: {index}")

        if index != self._focus:
            self._focus_changed(index)
        self._focus = index

    def _focus_changed(self, new_focus: int) -> None:
        if self._focus_changed_callback is not None:
            self._focus_changed_callback(new_focus)

    def set_focus_changed_callback(self, callback: Callable[[int], typing.Any]) -> None:
        r"""Assign a callback to be called when the focus index changes for any reason.

        The callback is called as ``callback(new_focus)``.

        :param callback: called with the new focus index whenever the focus changes

        >>> import sys
        >>> mfd = MonitoredFocusDeque([1, 2, 3], focus=1)
        >>> mfd.set_focus_changed_callback(lambda f: sys.stdout.write("focus: %d\n" % (f,)))
        >>> mfd.insert(1, 11)
        focus: 2
        >>> mfd
        MonitoredFocusDeque([1, 11, 2, 3], focus=2)
        >>> del mfd[0]
        focus: 1
        >>> mfd.focus = 0
        focus: 0
        """
        self._focus_changed_callback = callback

    def _adjust_focus_on_single_change(self, index: int, num_removed: int, num_inserted: int) -> int:
        """Compute the focus position for after a change applied at ``index``.

        Follows the same "focus follows the point of change, clamped to new length" logic as
        :meth:`MonitoredFocusList._adjust_focus_on_contents_modified`, specialised to the
        always-single-item-or-whole-collection mutations a deque supports.

        :param index: position at which the change is applied
        :param num_removed: number of items removed at ``index``
        :param num_inserted: number of items inserted at ``index``
        :returns: the focus position for after the change is applied
        """
        focus = self._focus
        if index <= focus < index + num_removed:
            focus = index + num_inserted
        elif index + num_removed <= focus:
            focus += num_inserted - num_removed

        new_len = len(self) - num_removed + num_inserted
        return max(0, min(focus, new_len - 1))

    def _modify(
        self,
        focus: int | Callable[[], int],
        change: Callable[ArgSpec, Ret],
        /,
        *args: ArgSpec.args,
        **kwargs: ArgSpec.kwargs,
    ) -> Ret:
        """Apply ``change(*args, **kwargs)``, set the focus, then send the modified notification once.

        The notification comes last so that its callback sees the new focus, and a focus it sets is kept.

        :param focus: the new focus index, or a callable returning it, called after the change
        :param change: the unmonitored operation applying the change
        :returns: what ``change`` returns
        :raises Exception: whatever ``change`` raises, leaving the focus as it was and notifying nothing;
            or whatever the focus-changed or the modified callback raises.
        :raises TypeError: the new focus is not an integer; the change is applied and notified.
        :raises IndexError: the new focus is out of range; the change is applied and notified.
        """
        rval = change(*args, **kwargs)
        try:
            self.focus = focus() if callable(focus) else focus
        finally:
            self._modified()
        return rval

    # override all the deque methods that modify the deque, except __setitem__: replacing an item at a given
    # index never changes which index is in focus, so the inherited MonitoredDeque.__setitem__ needs no
    # focus-adjustment wrapper here (see the class docstring for a worked example).

    def __delitem__(self, index: typing.SupportsIndex) -> None:  # type: ignore[override]
        """Delete the item at ``index``, adjusting focus to follow the point of removal.

        :param index: position to delete

        >>> mfd = MonitoredFocusDeque([0, 1, 2, 3, 4], focus=2)
        >>> del mfd[3]
        >>> mfd
        MonitoredFocusDeque([0, 1, 2, 4], focus=2)
        >>> del mfd[-1]
        >>> mfd
        MonitoredFocusDeque([0, 1, 2], focus=2)
        >>> del mfd[0]
        >>> mfd
        MonitoredFocusDeque([1, 2], focus=1)
        >>> del mfd[1]
        >>> mfd
        MonitoredFocusDeque([1], focus=0)
        >>> del mfd[0]
        >>> mfd
        MonitoredFocusDeque([], focus=None)
        """
        idx = int(index)
        if idx < 0:
            idx += len(self)
        focus = self._adjust_focus_on_single_change(idx, 1, 0)
        self._modify(focus, super(MonitoredDeque, self).__delitem__, index)

    def append(self, item: _T) -> None:
        """Append ``item`` to the right end of the deque.

        Unless the deque was empty, focus is unaffected -- unless the deque is already at
        ``maxlen``, in which case the head item is evicted and focus follows it:
        ``focus = max(0, focus - 1)``.

        :param item: item to append

        >>> mfd = MonitoredFocusDeque([0, 1, 2], focus=2)
        >>> mfd.append(6)
        >>> mfd
        MonitoredFocusDeque([0, 1, 2, 6], focus=2)
        >>> bounded = MonitoredFocusDeque([0, 1, 2], maxlen=3, focus=2)
        >>> bounded.append(6)
        >>> bounded
        MonitoredFocusDeque([1, 2, 6], maxlen=3, focus=1)
        """
        was_full = self.maxlen is not None and len(self) == self.maxlen
        if not self:
            focus = 0
        elif was_full:
            focus = max(0, self._focus - 1)
        else:
            focus = self._focus
        self._modify(focus, super(MonitoredDeque, self).append, item)

    def appendleft(self, item: _T) -> None:
        """Append ``item`` to the left end of the deque.

        Unless the deque was empty, focus shifts right by one to keep tracking the same item --
        unless the deque is already at ``maxlen``, in which case the tail item is evicted: focus
        is clamped to the new tail, landing there if it was the evicted item.

        :param item: item to append

        >>> mfd = MonitoredFocusDeque([0, 1, 2], focus=0)
        >>> mfd.appendleft(6)
        >>> mfd
        MonitoredFocusDeque([6, 0, 1, 2], focus=1)
        >>> bounded = MonitoredFocusDeque([0, 1, 2], maxlen=3, focus=2)
        >>> bounded.appendleft(6)
        >>> bounded
        MonitoredFocusDeque([6, 0, 1], maxlen=3, focus=2)
        """
        was_full = self.maxlen is not None and len(self) == self.maxlen
        if not self:
            focus = 0
        elif was_full:
            focus = min(self._focus + 1, len(self) - 1)
        else:
            focus = self._focus + 1
        self._modify(focus, super(MonitoredDeque, self).appendleft, item)

    def extend(self, items: Iterable[_T]) -> None:
        """Extend the deque with ``items`` on the right end.

        :param items: items to append, in order

        >>> mfd = MonitoredFocusDeque([0, 1, 2], focus=2)
        >>> mfd.extend((6, 7, 8))
        >>> mfd
        MonitoredFocusDeque([0, 1, 2, 6, 7, 8], focus=2)
        >>> bounded = MonitoredFocusDeque([0, 1, 2], maxlen=4, focus=2)
        >>> bounded.extend((6, 7, 8))
        >>> bounded
        MonitoredFocusDeque([2, 6, 7, 8], maxlen=4, focus=0)
        """
        items_list = list(items)
        if self.maxlen is not None:
            evicted = max(0, len(items_list) - (self.maxlen - len(self)))
        else:
            evicted = 0
        focus = max(0, self._focus - evicted) if self else 0
        self._modify(focus, super(MonitoredDeque, self).extend, items_list)

    def __iadd__(self, items: Iterable[_T]) -> MonitoredFocusDeque[_T]:
        """Extend the deque in place with ``items``, adjusting the focus like :meth:`extend`.

        >>> bounded = MonitoredFocusDeque([0, 1, 2], maxlen=4, focus=2)
        >>> bounded += (6, 7, 8)
        >>> bounded
        MonitoredFocusDeque([2, 6, 7, 8], maxlen=4, focus=0)
        """
        self.extend(items)
        return self

    def extendleft(self, items: Iterable[_T]) -> None:
        """Extend the deque with ``items`` on the left end, each item prepended in turn.

        :param items: items to prepend, in order (so the last item ends up leftmost)

        >>> mfd = MonitoredFocusDeque([0, 1, 2], focus=0)
        >>> mfd.extendleft((6, 7, 8))
        >>> mfd
        MonitoredFocusDeque([8, 7, 6, 0, 1, 2], focus=3)
        >>> bounded = MonitoredFocusDeque([0, 1, 2], maxlen=4, focus=0)
        >>> bounded.extendleft((6, 7))
        >>> bounded
        MonitoredFocusDeque([7, 6, 0, 1], maxlen=4, focus=2)
        """
        items_list = list(items)
        focus = self._focus + len(items_list) if self else 0
        if self.maxlen is not None:
            focus = min(focus, self.maxlen - 1)
        self._modify(focus, super(MonitoredDeque, self).extendleft, items_list)

    def pop(self) -> _T:  # type: ignore[override]  # deque.pop takes no argument, unlike list.pop
        """Remove and return the tail item.

        Focus is unaffected unless it pointed at the tail item, in which case it moves to the
        new tail.

        :returns: the removed item

        >>> mfd = MonitoredFocusDeque([0, 1, 2, 3], focus=3)
        >>> mfd.pop()
        3
        >>> mfd
        MonitoredFocusDeque([0, 1, 2], focus=2)
        """
        focus = self._adjust_focus_on_single_change(len(self) - 1, 1, 0)
        return self._modify(focus, super(MonitoredDeque, self).pop)

    def popleft(self) -> _T:
        """Remove and return the head item.

        Focus moves left by one, clamped at 0.

        :returns: the removed item

        >>> mfd = MonitoredFocusDeque([0, 1, 2, 3], focus=2)
        >>> mfd.popleft()
        0
        >>> mfd
        MonitoredFocusDeque([1, 2, 3], focus=1)
        """
        focus = self._adjust_focus_on_single_change(0, 1, 0)
        return self._modify(focus, super(MonitoredDeque, self).popleft)

    def insert(self, index: typing.SupportsIndex, item: _T) -> None:
        """Insert ``item`` before ``index``.

        Note ``deque.insert`` itself raises ``IndexError`` on an already-full bounded deque,
        rather than evicting -- so there is no eviction case to handle here.

        .. deprecated:: 4.2.5
            An ``index`` without ``__index__`` that ``int()`` accepts (a float, a ``Decimal``, a numeric string)
            is converted by ``int()`` with a :exc:`DeprecationWarning`.
            Pass an integer instead.
            From version 5.0 such an index raises :exc:`TypeError`, as ``deque.insert()`` does.

        :param index: position before which to insert
        :param item: item to insert
        :raises TypeError: *index* has no ``__index__`` and ``int()`` rejects its type.
        :raises ValueError: ``int()`` rejects the value of *index*, such as a NaN or an unparsable string.
        :raises OverflowError: *index* is an infinite float or does not fit a C ``ssize_t``.
        :raises IndexError: the deque is bounded and already full.

        >>> mfd = MonitoredFocusDeque([0, 1, 2, 3], focus=2)
        >>> mfd.insert(-1, -1)
        >>> mfd
        MonitoredFocusDeque([0, 1, 2, -1, 3], focus=2)
        >>> mfd.insert(0, -2)
        >>> mfd
        MonitoredFocusDeque([-2, 0, 1, 2, -1, 3], focus=3)
        """
        position = _insert_index(type(self).__name__, index, stacklevel=2)
        if position < 0:
            idx = max(0, len(self) + position)
        else:
            idx = min(position, len(self))
        focus = self._adjust_focus_on_single_change(idx, 0, 1)
        self._modify(focus, super(MonitoredDeque, self).insert, position, item)

    def remove(self, value: _T) -> None:
        """Remove the first occurrence of ``value``, adjusting focus to follow the removal.

        :param value: value to remove
        :raises ValueError: if ``value`` is not present

        >>> mfd = MonitoredFocusDeque([-2, 0, 1, -3, 2, -1, 3], focus=4)
        >>> mfd.remove(-3)
        >>> mfd
        MonitoredFocusDeque([-2, 0, 1, 2, -1, 3], focus=3)
        """
        index = self.index(value)
        focus = self._adjust_focus_on_single_change(index, 1, 0)
        self._modify(focus, super(MonitoredDeque, self).remove, value)

    def reverse(self) -> None:
        """Reverse the deque in place, keeping focus on the same item.

        >>> mfd = MonitoredFocusDeque([0, 1, 2, 3, 4], focus=1)
        >>> mfd.reverse()
        >>> mfd
        MonitoredFocusDeque([4, 3, 2, 1, 0], focus=3)
        """
        self._modify(max(0, len(self) - self._focus - 1), super(MonitoredDeque, self).reverse)

    def rotate(self, n: int = 1) -> None:
        """Rotate the deque ``n`` steps to the right (or left, for negative ``n``).

        Carries the focus item along with the rotation -- there is no list precedent for this
        method.

        :param n: number of steps to rotate right (negative rotates left)

        >>> mfd = MonitoredFocusDeque([0, 1, 2, 3, 4], focus=0)
        >>> mfd.rotate()
        >>> mfd
        MonitoredFocusDeque([4, 0, 1, 2, 3], focus=1)
        >>> mfd.rotate(-2)
        >>> mfd
        MonitoredFocusDeque([1, 2, 3, 4, 0], focus=4)
        """
        self._modify((self._focus + n) % len(self) if self else self._focus, super(MonitoredDeque, self).rotate, n)

    def clear(self) -> None:
        """Remove all items and reset focus to ``None``.

        >>> mfd = MonitoredFocusDeque([0, 1, 2], focus=1)
        >>> mfd.clear()
        >>> mfd
        MonitoredFocusDeque([], focus=None)
        """
        self._modify(0, super(MonitoredDeque, self).clear)


def _test() -> None:
    import doctest

    doctest.testmod()


if __name__ == "__main__":
    _test()
urwid-urwid-69907e4/urwid/widget/monitored_list.py000066400000000000000000000561031526073734300223260ustar00rootroot00000000000000# Urwid MonitoredList class
#    Copyright (C) 2004-2011  Ian Ward
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/

"""Lists that call a callback when their contents or focus change."""

from __future__ import annotations

import functools
import typing

if typing.TYPE_CHECKING:
    from collections.abc import Callable, Collection, Iterable, Iterator

    from typing_extensions import Concatenate, ParamSpec, Self

    ArgSpec = ParamSpec("ArgSpec")
    Ret = typing.TypeVar("Ret")

__all__ = ("MonitoredFocusList", "MonitoredList")

_T = typing.TypeVar("_T")
_S = typing.TypeVar("_S")


def _call_modified(
    fn: Callable[Concatenate[MonitoredList[typing.Any], ArgSpec], Ret],
) -> Callable[Concatenate[MonitoredList[typing.Any], ArgSpec], Ret]:
    @functools.wraps(fn)
    def call_modified_wrapper(
        self: MonitoredList[typing.Any],
        /,
        *args: ArgSpec.args,
        **kwargs: ArgSpec.kwargs,
    ) -> Ret:
        rval = fn(self, *args, **kwargs)
        self._modified()  # pylint: disable=protected-access
        return rval

    return call_modified_wrapper


class MonitoredList(list[_T], typing.Generic[_T]):
    """Trigger a callback any time its contents are changed with the usual list operations append, extend, etc."""

    _modified_callback: Callable[[], typing.Any] | None = None

    def _modified(self) -> None:
        if self._modified_callback is not None:
            self._modified_callback()

    def set_modified_callback(self, callback: Callable[[], typing.Any]) -> None:
        r"""Assign a callback function with no parameters that is called any time the list is modified.

        Callback's return value is ignored.

        >>> import sys
        >>> ml = MonitoredList([1, 2, 3])
        >>> ml.set_modified_callback(lambda: sys.stdout.write("modified\n"))
        >>> ml
        MonitoredList([1, 2, 3])
        >>> ml.append(10)
        modified
        >>> len(ml)
        4
        >>> ml += [11, 12, 13]
        modified
        >>> ml[:] = ml[:2] + ml[-2:]
        modified
        >>> ml
        MonitoredList([1, 2, 12, 13])
        """
        self._modified_callback = callback

    def __repr__(self) -> str:
        """Return a constructor-call-like representation of the list's contents."""
        return f"{self.__class__.__name__}({list(self)!r})"

    # noinspection PyMethodParameters
    def __rich_repr__(self) -> Iterator[tuple[str | None, typing.Any] | typing.Any]:
        """Yield this list's items, for `rich`'s repr protocol."""
        for item in self:
            yield None, item

    # list.__add__/__rmul__ return a new list, not Self, and leave this list unchanged, so they do not notify.
    @typing.overload
    def __add__(self, __value: list[_T], /) -> list[_T]: ...

    @typing.overload
    def __add__(self, __value: list[_S], /) -> list[_T | _S]: ...

    def __add__(self, __value: list[typing.Any]) -> list[typing.Any]:
        """Return a new list with the items of `__value` appended."""
        return super().__add__(__value)

    @_call_modified
    def __delitem__(self, __key: typing.SupportsIndex | slice) -> None:
        """Delete the item or slice at `__key`."""
        super().__delitem__(__key)

    @_call_modified
    def __iadd__(self, __value: Iterable[_T]) -> Self:  # type: ignore[override]
        """Extend the list in place with the items from `__value`."""
        return super().__iadd__(__value)

    def __rmul__(self, __value: typing.SupportsIndex) -> list[_T]:
        """Return a new list with this list's items repeated `__value` times."""
        return super().__rmul__(__value)

    @_call_modified
    def __imul__(self, __value: typing.SupportsIndex) -> Self:
        """Repeat the list's contents `__value` times in place."""
        return super().__imul__(__value)

    @typing.overload
    def __setitem__(self, __key: typing.SupportsIndex, __value: _T) -> None: ...

    @typing.overload
    def __setitem__(self, __key: slice, __value: Iterable[_T]) -> None: ...

    @_call_modified
    def __setitem__(self, __key: typing.SupportsIndex | slice, __value: _T | Iterable[_T]) -> None:
        """Set the item or slice at `__key` to `__value`."""
        if isinstance(__key, slice):
            super().__setitem__(__key, typing.cast("Iterable[_T]", __value))
        else:
            super().__setitem__(__key, typing.cast("_T", __value))

    @_call_modified
    def append(self, __object: _T) -> None:
        """Append ``__object`` to the end of the list."""
        super().append(__object)

    @_call_modified
    def extend(self, __iterable: Iterable[_T]) -> None:
        """Extend the list with items from ``__iterable``."""
        super().extend(__iterable)

    @_call_modified
    def pop(self, __index: typing.SupportsIndex = -1) -> _T:
        """Remove and return the item at ``__index`` (default the last item)."""
        return super().pop(__index)

    @_call_modified
    def insert(self, __index: typing.SupportsIndex, __object: _T) -> None:
        """Insert ``__object`` before ``__index``."""
        super().insert(__index, __object)

    @_call_modified
    def remove(self, __value: _T) -> None:
        """Remove the first occurrence of ``__value``."""
        super().remove(__value)

    @_call_modified
    def reverse(self) -> None:
        """Reverse the list in place."""
        super().reverse()

    @_call_modified
    def sort(self, *, key: Callable[[_T], typing.Any] | None = None, reverse: bool = False) -> None:
        """Sort the list in place."""
        super().sort(key=key, reverse=reverse)

    @_call_modified
    def clear(self) -> None:
        """Remove all items from the list."""
        super().clear()


class MonitoredFocusList(MonitoredList[_T], typing.Generic[_T]):
    """Trigger a callback any time its contents are modified, before and/or after modification.

    Also triggers a callback any time the focus index is changed.
    """

    _focus_changed_callback: Callable[[int], typing.Any] | None = None
    _validate_contents_modified_callback: Callable[[tuple[int, int, int], Collection[_T]], int | None] | None = None

    def __init__(self, *args: typing.Any, focus: int = 0, **kwargs: typing.Any) -> None:
        """Initialize a list that tracks one item as the focus item.

        If items are inserted or removed it will update the focus.

        >>> ml = MonitoredFocusList([10, 11, 12, 13, 14], focus=3)
        >>> ml
        MonitoredFocusList([10, 11, 12, 13, 14], focus=3)
        >>> del ml[1]
        >>> ml
        MonitoredFocusList([10, 12, 13, 14], focus=2)
        >>> ml[:2] = [50, 51, 52, 53]
        >>> ml
        MonitoredFocusList([50, 51, 52, 53, 13, 14], focus=4)
        >>> ml[4] = 99
        >>> ml
        MonitoredFocusList([50, 51, 52, 53, 99, 14], focus=4)
        >>> ml[:] = []
        >>> ml
        MonitoredFocusList([], focus=None)
        """
        super().__init__(*args, **kwargs)

        self._focus = focus

    def __repr__(self) -> str:
        """Return a constructor-call-like representation of the list's contents and focus."""
        return f"{self.__class__.__name__}({list(self)!r}, focus={self.focus!r})"

    @property
    def focus(self) -> int | None:
        """Get/set the focus index.

        This value is read as None when the list is empty, and may only be set to a value between 0 and
        len(self)-1 or an IndexError will be raised.

        Return the index of the item "in focus" or None if
        the list is empty.

        >>> MonitoredFocusList([1, 2, 3], focus=2).focus
        2
        >>> MonitoredFocusList().focus
        """
        if not self:
            return None
        return self._focus

    @focus.setter
    def focus(self, index: int) -> None:
        """Set the focus index.

        :param index: index into this list, any index out of range will raise an IndexError, except when the list is
            empty and the index passed is ignored.
        :raises TypeError: *index* is not an integer.
        :raises IndexError: *index* is outside the range of this sequence.

        This function may call self._focus_changed when the focus
        is modified, passing the new focus position to the
        callback just before changing the old focus setting.
        The callback may be assigned with set_focus_changed_callback().

        >>> ml = MonitoredFocusList([9, 10, 11])
        >>> ml.focus = 2
        ... ml.focus
        2
        >>> ml.focus = 0
        ... ml.focus
        0
        >>> ml.focus = -2
        Traceback (most recent call last):
        ...
        IndexError: focus index is out of range: -2
        """
        if not self:
            self._focus = 0
            return
        if not isinstance(index, int):
            raise TypeError("index must be an integer")
        if index < 0 or index >= len(self):
            raise IndexError(f"focus index is out of range: {index}")

        if index != self._focus:
            self._focus_changed(index)
        self._focus = index

    def _focus_changed(self, new_focus: int) -> None:
        if self._focus_changed_callback is not None:
            self._focus_changed_callback(new_focus)

    def set_focus_changed_callback(self, callback: Callable[[int], typing.Any]) -> None:
        r"""
        Assign a callback to be called when the focus index changes for any reason.

        :param callback: a callable in the form ``callback(new_focus)``, where ``new_focus`` is the new focus index.

        >>> import sys
        >>> ml = MonitoredFocusList([1, 2, 3], focus=1)
        >>> ml.set_focus_changed_callback(lambda f: sys.stdout.write("focus: %d\n" % (f,)))
        >>> ml
        MonitoredFocusList([1, 2, 3], focus=1)
        >>> ml.append(10)
        >>> ml.insert(1, 11)
        focus: 2
        >>> ml
        MonitoredFocusList([1, 11, 2, 3, 10], focus=2)
        >>> del ml[:2]
        focus: 0
        >>> ml[:0] = [12, 13, 14]
        focus: 3
        >>> ml.focus = 5
        focus: 5
        >>> ml
        MonitoredFocusList([12, 13, 14, 2, 3, 10], focus=5)
        """
        self._focus_changed_callback = callback

    def _validate_contents_modified(
        self,
        indices: tuple[int, int, int],
        new_items: Collection[_T],
    ) -> int | None:
        if self._validate_contents_modified_callback is not None:
            return self._validate_contents_modified_callback(indices, new_items)
        return None

    def set_validate_contents_modified(
        self,
        callback: Callable[[tuple[int, int, int], Collection[_T]], int | None],
    ) -> None:
        """Assign a callback function to handle validating changes to the list.

        This may raise an exception if the change should not be performed.
        It may also return an integer position to be the new focus after the
        list is modified, or None to use the default behaviour.

        :param callback: a callable in the form ``callback(indices, new_items)``, where ``indices`` is a
            ``(start, stop, step)`` tuple whose range covers the items being modified, and ``new_items`` is an
            iterable of items replacing those at ``range(*indices)`` - empty if items are being removed, and, if
            ``step == 1``, of any length.
        """
        self._validate_contents_modified_callback = callback

    def _adjust_focus_on_contents_modified(self, slc: slice, new_items: Collection[_T] = ()) -> int:
        """Default behaviour is to move the focus to the item following any removed items.

        Unless that item was simply replaced, failing that choose the last item in the list.

        returns focus position for after change is applied
        """
        num_new_items = len(new_items)
        start, stop, step = indices = slc.indices(len(self))
        num_removed = len(list(range(*indices)))

        focus = self._validate_contents_modified(indices, new_items)
        if focus is not None:
            return focus

        focus = self._focus
        if step == 1:
            if start + num_new_items <= focus < stop:
                focus = stop
            # adjust for added/removed items
            if stop <= focus:
                focus += num_new_items - (stop - start)

        else:  # noqa: PLR5501  # pylint: disable=else-if-used  # readability
            if not num_new_items:
                # extended slice being removed
                if focus in range(start, stop, step):
                    focus += 1

                # adjust for removed items
                focus -= sum(index < focus for index in range(start, stop, step))

        return min(focus, len(self) + num_new_items - num_removed - 1)

    def _modify(
        self,
        focus: int | Callable[[], int],
        change: Callable[ArgSpec, Ret],
        /,
        *args: ArgSpec.args,
        **kwargs: ArgSpec.kwargs,
    ) -> Ret:
        """Apply ``change(*args, **kwargs)``, set the focus, then send the modified notification once.

        The notification comes last so that its callback sees the new focus, and a focus it sets is kept.

        :param focus: the new focus index, or a callable returning it, called after the change
        :param change: the unmonitored operation applying the change
        :returns: what ``change`` returns
        :raises Exception: whatever ``change`` raises, leaving the focus as it was and notifying nothing;
            or whatever the focus-changed or the modified callback raises.
        :raises TypeError: the new focus is not an integer; the change is applied and notified.
        :raises IndexError: the new focus is out of range; the change is applied and notified.
        """
        rval = change(*args, **kwargs)
        try:
            self.focus = focus() if callable(focus) else focus
        finally:
            self._modified()
        return rval

    # override all the list methods that modify the list

    def __delitem__(self, y: typing.SupportsIndex | slice) -> None:
        """Delete items by index or slice, updating focus accordingly.

        >>> ml = MonitoredFocusList([0, 1, 2, 3, 4], focus=2)
        >>> del ml[3]
        >>> ml
        MonitoredFocusList([0, 1, 2, 4], focus=2)
        >>> del ml[-1]
        >>> ml
        MonitoredFocusList([0, 1, 2], focus=2)
        >>> del ml[0]
        >>> ml
        MonitoredFocusList([1, 2], focus=1)
        >>> del ml[1]
        >>> ml
        MonitoredFocusList([1], focus=0)
        >>> del ml[0]
        >>> ml
        MonitoredFocusList([], focus=None)
        >>> ml = MonitoredFocusList([5, 4, 6, 4, 5, 4, 6, 4, 5], focus=4)
        >>> del ml[1::2]
        >>> ml
        MonitoredFocusList([5, 6, 5, 6, 5], focus=2)
        >>> del ml[::2]
        >>> ml
        MonitoredFocusList([6, 6], focus=1)
        >>> ml = MonitoredFocusList([0, 1, 2, 3, 4, 6, 7], focus=2)
        >>> del ml[-2:]
        >>> ml
        MonitoredFocusList([0, 1, 2, 3, 4], focus=2)
        >>> del ml[-4:-2]
        >>> ml
        MonitoredFocusList([0, 3, 4], focus=1)
        >>> del ml[:]
        >>> ml
        MonitoredFocusList([], focus=None)
        """
        if isinstance(y, slice):
            focus = self._adjust_focus_on_contents_modified(y)
        else:
            idx = int(y)
            focus = self._adjust_focus_on_contents_modified(slice(idx, idx + 1 or None))
        self._modify(focus, super(MonitoredList, self).__delitem__, y)

    @typing.overload
    def __setitem__(self, i: typing.SupportsIndex, y: _T) -> None: ...

    @typing.overload
    def __setitem__(self, i: slice, y: Iterable[_T]) -> None: ...

    def __setitem__(self, i: typing.SupportsIndex | slice, y: _T | Iterable[_T]) -> None:
        """Replace items by index or slice, updating focus accordingly.

        >>> def modified(indices, new_items):
        ...     print(f"range{indices!r} <- {new_items!r}")
        >>> ml = MonitoredFocusList([0, 1, 2, 3], focus=2)
        >>> ml.set_validate_contents_modified(modified)
        >>> ml[0] = 9
        range(0, 1, 1) <- [9]
        >>> ml[2] = 6
        range(2, 3, 1) <- [6]
        >>> ml.focus
        2
        >>> ml[-1] = 8
        range(3, 4, 1) <- [8]
        >>> ml
        MonitoredFocusList([9, 1, 6, 8], focus=2)
        >>> ml[1::2] = [12, 13]
        range(1, 4, 2) <- [12, 13]
        >>> ml[::2] = [10, 11]
        range(0, 4, 2) <- [10, 11]
        >>> ml[-3:-1] = [21, 22, 23]
        range(1, 3, 1) <- [21, 22, 23]
        >>> ml
        MonitoredFocusList([10, 21, 22, 23, 13], focus=2)
        >>> ml[:] = []
        range(0, 5, 1) <- []
        >>> ml
        MonitoredFocusList([], focus=None)
        """
        if isinstance(i, slice):
            new_items = list(typing.cast("Iterable[_T]", y))
            focus = self._adjust_focus_on_contents_modified(i, new_items)
            self._modify(focus, super(MonitoredList, self).__setitem__, i, new_items)
        else:
            item = typing.cast("_T", y)
            idx = int(i)
            focus = self._adjust_focus_on_contents_modified(slice(idx, idx + 1 or None), [item])
            self._modify(focus, super(MonitoredList, self).__setitem__, i, item)

    def __imul__(self, n: typing.SupportsIndex) -> Self:
        """Repeat the list's contents `n` times in place, adjusting focus accordingly.

        >>> def modified(indices, new_items):
        ...     print(f"range{indices!r} <- {list(new_items)!r}")
        >>> ml = MonitoredFocusList([0, 1, 2], focus=2)
        >>> ml.set_validate_contents_modified(modified)
        >>> ml *= 3
        range(3, 3, 1) <- [0, 1, 2, 0, 1, 2]
        >>> ml
        MonitoredFocusList([0, 1, 2, 0, 1, 2, 0, 1, 2], focus=2)
        >>> ml *= 0
        range(0, 9, 1) <- []
        >>> print(ml.focus)
        None
        """
        multiplier = int(n)
        if multiplier > 0:
            focus = self._adjust_focus_on_contents_modified(slice(len(self), len(self)), list(self) * (multiplier - 1))
        else:  # all contents are being removed
            focus = self._adjust_focus_on_contents_modified(slice(0, len(self)))
        self._modify(focus, super(MonitoredList, self).__imul__, multiplier)
        return self

    def append(self, item: _T) -> None:
        """Append `item` to the list, adjusting focus accordingly.

        >>> def modified(indices, new_items):
        ...     print(f"range{indices!r} <- {new_items!r}")
        >>> ml = MonitoredFocusList([0, 1, 2], focus=2)
        >>> ml.set_validate_contents_modified(modified)
        >>> ml.append(6)
        range(3, 3, 1) <- [6]
        """
        focus = self._adjust_focus_on_contents_modified(slice(len(self), len(self)), [item])
        self._modify(focus, super(MonitoredList, self).append, item)

    def extend(self, items: Iterable[_T]) -> None:
        """Extend the list with `items`, adjusting focus accordingly.

        >>> def modified(indices, new_items):
        ...     print(f"range{indices!r} <- {list(new_items)!r}")
        >>> ml = MonitoredFocusList([0, 1, 2], focus=2)
        >>> ml.set_validate_contents_modified(modified)
        >>> ml.extend((6, 7, 8))
        range(3, 3, 1) <- [6, 7, 8]
        """
        items_list = list(items)
        focus = self._adjust_focus_on_contents_modified(slice(len(self), len(self)), items_list)
        self._modify(focus, super(MonitoredList, self).extend, items_list)

    def insert(self, index: typing.SupportsIndex, item: _T) -> None:
        """Insert `item` before `index`, adjusting focus accordingly.

        >>> ml = MonitoredFocusList([0, 1, 2, 3], focus=2)
        >>> ml.insert(-1, -1)
        >>> ml
        MonitoredFocusList([0, 1, 2, -1, 3], focus=2)
        >>> ml.insert(0, -2)
        >>> ml
        MonitoredFocusList([-2, 0, 1, 2, -1, 3], focus=3)
        >>> ml.insert(3, -3)
        >>> ml
        MonitoredFocusList([-2, 0, 1, -3, 2, -1, 3], focus=4)
        """
        focus = self._adjust_focus_on_contents_modified(slice(index, index), [item])
        self._modify(focus, super(MonitoredList, self).insert, index, item)

    def pop(self, index: typing.SupportsIndex = -1) -> _T:
        """Remove and return the item at `index`, adjusting focus accordingly.

        >>> ml = MonitoredFocusList([-2, 0, 1, -3, 2, 3], focus=4)
        >>> ml.pop(3)
        -3
        >>> ml
        MonitoredFocusList([-2, 0, 1, 2, 3], focus=3)
        >>> ml.pop(0)
        -2
        >>> ml
        MonitoredFocusList([0, 1, 2, 3], focus=2)
        >>> ml.pop(-1)
        3
        >>> ml
        MonitoredFocusList([0, 1, 2], focus=2)
        >>> ml.pop(2)
        2
        >>> ml
        MonitoredFocusList([0, 1], focus=1)
        """
        focus = self._adjust_focus_on_contents_modified(slice(index, int(index) + 1 or None))
        return self._modify(focus, super(MonitoredList, self).pop, index)

    def remove(self, value: _T) -> None:
        """Remove the first occurrence of `value`, adjusting focus accordingly.

        >>> ml = MonitoredFocusList([-2, 0, 1, -3, 2, -1, 3], focus=4)
        >>> ml.remove(-3)
        >>> ml
        MonitoredFocusList([-2, 0, 1, 2, -1, 3], focus=3)
        >>> ml.remove(-2)
        >>> ml
        MonitoredFocusList([0, 1, 2, -1, 3], focus=2)
        >>> ml.remove(3)
        >>> ml
        MonitoredFocusList([0, 1, 2, -1], focus=2)
        """
        index = self.index(value)
        focus = self._adjust_focus_on_contents_modified(slice(index, index + 1 or None))
        self._modify(focus, super(MonitoredList, self).remove, value)

    def reverse(self) -> None:
        """Reverse the list in place, adjusting focus accordingly.

        >>> ml = MonitoredFocusList([0, 1, 2, 3, 4], focus=1)
        >>> ml.reverse()
        >>> ml
        MonitoredFocusList([4, 3, 2, 1, 0], focus=3)
        """
        self._modify(max(0, len(self) - self._focus - 1), super(MonitoredList, self).reverse)

    def sort(
        self,
        *,
        key: Callable[[_T], typing.Any] | None = None,
        reverse: bool = False,
    ) -> None:
        """Sort the list in place, adjusting focus accordingly.

        >>> ml = MonitoredFocusList([-2, 0, 1, -3, 2, -1, 3], focus=4)
        >>> ml.sort()
        >>> ml
        MonitoredFocusList([-3, -2, -1, 0, 1, 2, 3], focus=5)
        """
        if not self:
            return None
        value = self[self._focus]
        return self._modify(
            lambda: self.index(value),
            lambda: super(MonitoredList, self).sort(key=key, reverse=reverse),
        )

    if hasattr(list, "clear"):

        def clear(self) -> None:
            """Remove all items and reset focus to ``None``."""
            focus = self._adjust_focus_on_contents_modified(slice(0, 0))
            self._modify(focus, super(MonitoredList, self).clear)


def _test() -> None:
    import doctest

    doctest.testmod()


if __name__ == "__main__":
    _test()
urwid-urwid-69907e4/urwid/widget/overlay.py000066400000000000000000001147671526073734300207670ustar00rootroot00000000000000"""Overlay: a container that draws one widget on top of another."""

from __future__ import annotations

import typing
import warnings
from collections.abc import MutableSequence

from typing_extensions import Literal

from urwid.canvas import CanvasOverlay, CompositeCanvas
from urwid.split_repr import remove_defaults

from .constants import (
    RELATIVE_100,
    Align,
    Sizing,
    VAlign,
    WHSettings,
    normalize_align,
    normalize_height,
    normalize_valign,
    normalize_width,
    simplify_align,
    simplify_height,
    simplify_valign,
    simplify_width,
)
from .container import WidgetContainerListContentsMixin, WidgetContainerMixin
from .filler import calculate_top_bottom_filler
from .padding import calculate_left_right_padding
from .widget import AbstractBoxWidget, AbstractWidget, Widget, WidgetError, WidgetWarning

if typing.TYPE_CHECKING:
    from collections.abc import Iterator, Sequence

    from .widget import AbstractFlowWidget


TopWidget = typing.TypeVar("TopWidget", bound=AbstractWidget)
BottomWidget = typing.TypeVar("BottomWidget", bound=AbstractBoxWidget)


class OverlayError(WidgetError):
    """Overlay specific errors."""


class OverlayWarning(WidgetWarning):
    """Overlay specific warnings."""


def _check_widget_subclass(widget: AbstractWidget) -> None:
    if not isinstance(widget, AbstractWidget):
        obj_class_path = f"{widget.__class__.__module__}.{widget.__class__.__name__}"
        warnings.warn(
            f"{obj_class_path} is not implementing Widget API",
            DeprecationWarning,
            stacklevel=3,
        )


class OverlayOptions(typing.NamedTuple):
    """Placement and sizing options for the top widget of an :class:`Overlay`."""

    align: Align | Literal[WHSettings.RELATIVE]
    align_amount: int | None
    width_type: WHSettings
    width_amount: int | None
    min_width: int | None
    left: int
    right: int
    valign_type: VAlign | Literal[WHSettings.RELATIVE]
    valign_amount: int | None
    height_type: WHSettings
    height_amount: int | None
    min_height: int | None
    top: int
    bottom: int


OverlayContentsItem = tuple[typing.Union[TopWidget, BottomWidget], OverlayOptions]


class Overlay(
    Widget,
    WidgetContainerMixin[Literal[0, 1]],
    WidgetContainerListContentsMixin[OverlayContentsItem[TopWidget, BottomWidget]],
    typing.Generic[TopWidget, BottomWidget],
):
    """Overlay contains two widgets and renders one on top of the other.

    Top widget can be Box, Flow or Fixed.
    Bottom widget should be Box.
    """

    _selectable = True

    align_type: Align | Literal[WHSettings.RELATIVE]
    align_amount: int | None
    width_type: Literal[WHSettings.PACK, WHSettings.GIVEN, WHSettings.RELATIVE]
    width_amount: int | None
    valign_type: VAlign | Literal[WHSettings.RELATIVE]
    valign_amount: int | None
    height_type: Literal[WHSettings.PACK, WHSettings.GIVEN, WHSettings.RELATIVE]
    height_amount: int | None

    _DEFAULT_BOTTOM_OPTIONS = OverlayOptions(
        align=Align.LEFT,
        align_amount=None,
        width_type=WHSettings.RELATIVE,
        width_amount=100,
        min_width=None,
        left=0,
        right=0,
        valign_type=VAlign.TOP,
        valign_amount=None,
        height_type=WHSettings.RELATIVE,
        height_amount=100,
        min_height=None,
        top=0,
        bottom=0,
    )

    def __init__(
        self,
        top_w: TopWidget,
        bottom_w: BottomWidget,
        align: (
            Literal["left", "center", "right"]
            | Align
            | tuple[Literal["relative", "fixed left", "fixed right", WHSettings.RELATIVE], int]
        ),
        width: Literal["pack", WHSettings.PACK] | int | tuple[Literal["relative", WHSettings.RELATIVE], int] | None,
        valign: (
            Literal["top", "middle", "bottom"]
            | VAlign
            | tuple[Literal["relative", "fixed top", "fixed bottom", WHSettings.RELATIVE], int]
        ),
        height: Literal["pack", WHSettings.PACK] | int | tuple[Literal["relative", WHSettings.RELATIVE], int] | None,
        min_width: int | None = None,
        min_height: int | None = None,
        left: int = 0,
        right: int = 0,
        top: int = 0,
        bottom: int = 0,
    ) -> None:
        """Build the widget overlaying *top_w* on *bottom_w*.

        :param top_w: a flow, box or fixed widget to overlay "on top".
        :param bottom_w: a box widget to appear "below" previous widget.
        :param align: alignment, one of ``'left'``, ``'center'``, ``'right'`` or
            (``'relative'``, *percentage* 0=left 100=right)
        :param width: width type, one of:

            ``'pack'``
              if *top_w* is a fixed widget
            *given width*
              integer number of columns wide
            (``'relative'``, *percentage of total width*)
              make *top_w* width related to container width
        :param valign: alignment mode, one of ``'top'``, ``'middle'``, ``'bottom'`` or
            (``'relative'``, *percentage* 0=top 100=bottom)
        :param height: one of:

            ``'pack'``
              if *top_w* is a flow or fixed widget
            *given height*
              integer number of rows high
            (``'relative'``, *percentage of total height*)
              make *top_w* height related to container height
        :param min_width: the minimum number of columns for *top_w* when width is not fixed.
        :param min_height: minimum number of rows for *top_w* when height is not fixed.
        :param left: a fixed number of columns to add on the left.
        :param right: a fixed number of columns to add on the right.
        :param top: a fixed number of rows to add on the top.
        :param bottom: a fixed number of rows to add on the bottom.

        Overlay widgets behave similarly to :class:`Padding` and :class:`Filler`
        widgets when determining the size and position of *top_w*. *bottom_w* is
        always rendered the full size available "below" *top_w*.
        """
        super().__init__()

        self.top_w = top_w
        self.bottom_w = bottom_w
        self.min_width: int | None = None
        self.min_height: int | None = None
        self.left = 0
        self.right = 0
        self.top = 0
        self.bottom = 0

        self.set_overlay_parameters(align, width, valign, height, min_width, min_height, left, right, top, bottom)

        _check_widget_subclass(top_w)
        _check_widget_subclass(bottom_w)

    @property
    def top_w(self) -> TopWidget:
        """Widget overlaid "on top" of `bottom_w`."""
        return self._top_w

    @top_w.setter
    def top_w(self, widget: TopWidget) -> None:
        """Replace the widget overlaid "on top" and invalidate the canvas rendered from the old one.

        Without the invalidation :class:`CanvasCache` keeps serving the canvas of the previous
        widget - together with every parent canvas built from it - so the replacement stays
        invisible until something else invalidates the overlay.
        """
        self._top_w = widget
        self._invalidate()

    @property
    def bottom_w(self) -> BottomWidget:
        """Widget appearing "below" `top_w`."""
        return self._bottom_w

    @bottom_w.setter
    def bottom_w(self, widget: BottomWidget) -> None:
        """Replace the widget appearing "below" and invalidate the canvas rendered from the old one.

        The counterpart of the `top_w` setter, and invalidating for the same reason: a plain
        assignment would leave :class:`CanvasCache` serving the canvas of the previous widget.
        """
        self._bottom_w = widget
        self._invalidate()

    def sizing(self) -> frozenset[Sizing]:
        """Actual widget sizing.

        :returns: Sizing information depends on the top widget sizing and sizing parameters.

        Rules:
        * BOX sizing is always supported provided by the bottom widget
        * FLOW sizing is supported if top widget has:
        * * PACK height type and FLOW supported by the TOP widget
        * * BOX supported by TOP widget AND height amount AND height type GIVEN of min_height
        * FIXED sizing is supported if top widget has:
        * * PACK width type and FIXED supported by the TOP widget
        * * width amount and GIVEN width or min_width AND:
        * * * FLOW supported by the TOP widget AND PACK height type
        * * * BOX supported by the TOP widget AND height_amount and GIVEN height or min height
        """
        sizing = {Sizing.BOX}
        top_sizing = self.top_w.sizing()
        if self.width_type == WHSettings.PACK:
            if Sizing.FIXED in top_sizing:
                sizing.add(Sizing.FIXED)
            else:
                warnings.warn(
                    f"Top widget {self.top_w} should support sizing {Sizing.FIXED.upper()} "
                    f"for width type {WHSettings.PACK.upper()}",
                    OverlayWarning,
                    stacklevel=3,
                )

        elif self.height_type == WHSettings.PACK:
            if Sizing.FLOW in top_sizing:
                sizing.add(Sizing.FLOW)

                if self.width_amount and (self.width_type == WHSettings.GIVEN or self.min_width):
                    sizing.add(Sizing.FIXED)
            else:
                warnings.warn(
                    f"Top widget {self.top_w} should support sizing {Sizing.FLOW.upper()} "
                    f"for height type {self.height_type.upper()}",
                    OverlayWarning,
                    stacklevel=3,
                )

        elif self.height_amount and (self.height_type == WHSettings.GIVEN or self.min_height):
            if Sizing.BOX in top_sizing:
                sizing.add(Sizing.FLOW)
                if self.width_amount and (self.width_type == WHSettings.GIVEN or self.min_width):
                    sizing.add(Sizing.FIXED)
            else:
                warnings.warn(
                    f"Top widget {self.top_w} should support sizing {Sizing.BOX.upper()} "
                    f"for height type {self.height_type.upper()}",
                    OverlayWarning,
                    stacklevel=3,
                )

        return frozenset(sizing)

    def pack(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int] = (),
        focus: bool = False,
    ) -> tuple[int, int]:
        """
        Return the size the widget would render as, for FIXED sizing.

        :raises OverlayError: a FIXED render is requested but the overlay parameters do not resolve to a fixed size.
        """
        if size:
            return super().pack(size, focus)

        extra_cols = (self.left or 0) + (self.right or 0)
        extra_rows = (self.top or 0) + (self.bottom or 0)

        if self.width_type == WHSettings.PACK:
            cols, rows = self.top_w.pack((), focus)
            return cols + extra_cols, rows + extra_rows

        if not self.width_amount:
            raise OverlayError(
                f"Requested FIXED render for {self.top_w} with "
                f"width_type={self.width_type.upper()}, "
                f"width_amount={self.width_amount!r}, "
                f"height_type={self.height_type.upper()}, "
                f"height_amount={self.height_amount!r}"
                f"min_width={self.min_width!r}, "
                f"min_height={self.min_height!r}"
            )

        if self.width_type == WHSettings.GIVEN:
            w_cols = self.width_amount
            cols = w_cols + extra_cols
        elif self.width_type == WHSettings.RELATIVE and self.min_width:
            w_cols = self.min_width
            cols = int(w_cols * 100 / self.width_amount + 0.5)
        else:
            raise OverlayError(
                f"Requested FIXED render for {self.top_w} with "
                f"width_type={self.width_type.upper()}, "
                f"width_amount={self.width_amount!r}, "
                f"height_type={self.height_type.upper()}, "
                f"height_amount={self.height_amount!r}"
                f"min_width={self.min_width!r}, "
                f"min_height={self.min_height!r}"
            )

        if self.height_type == WHSettings.PACK:
            return cols, typing.cast("AbstractFlowWidget", self.top_w).rows((w_cols,), focus) + extra_rows

        if not self.height_amount:
            raise OverlayError(
                f"Requested FIXED render for {self.top_w} with "
                f"width_type={self.width_type.upper()}, "
                f"width_amount={self.width_amount!r}, "
                f"height_type={self.height_type.upper()}, "
                f"height_amount={self.height_amount!r}"
                f"min_width={self.min_width!r}, "
                f"min_height={self.min_height!r}"
            )

        if self.height_type == WHSettings.GIVEN:
            return cols, self.height_amount + extra_rows

        if self.height_type == WHSettings.RELATIVE and self.min_height:
            return cols, int(self.min_height * 100 / self.height_amount + 0.5)

        raise OverlayError(
            f"Requested FIXED render for {self.top_w} with "
            f"width_type={self.width_type.upper()}, "
            f"width_amount={self.width_amount!r}, "
            f"height_type={self.height_type.upper()}, "
            f"height_amount={self.height_amount!r}"
            f"min_width={self.min_width!r}, "
            f"min_height={self.min_height!r}"
        )

    def rows(self, size: tuple[int], focus: bool = False) -> int:
        """Widget rows amount for FLOW sizing.

        :raises OverlayError: the overlay parameters do not resolve to a row count for the given size.
        """
        extra_height = (self.top or 0) + (self.bottom or 0)
        if self.height_type == WHSettings.GIVEN:
            return typing.cast("int", self.height_amount) + extra_height
        if self.height_type == WHSettings.RELATIVE and self.min_height:
            return int(self.min_height * 100 / typing.cast("int", self.height_amount) + 0.5)

        if self.height_type == WHSettings.PACK:
            top_w = typing.cast("AbstractFlowWidget", self.top_w)
            extra_height = (self.top or 0) + (self.bottom or 0)
            if self.width_type == WHSettings.GIVEN and self.width_amount:
                return top_w.rows((self.width_amount,), focus) + extra_height
            if self.width_type == WHSettings.RELATIVE:
                width = max(int(size[0] * typing.cast("int", self.width_amount) / 100 + 0.5), (self.min_width or 0))
                return top_w.rows((width,), focus) + extra_height

        raise OverlayError(
            f"Requested rows for {self.top_w} with size {size!r}"
            f"width_type={self.width_type.upper()}, "
            f"width_amount={self.width_amount!r}, "
            f"height_type={self.height_type.upper()}, "
            f"height_amount={self.height_amount!r}"
            f"min_width={self.min_width!r}, "
            f"min_height={self.min_height!r}"
        )

    def _repr_attrs(self) -> dict[str, typing.Any]:
        attrs = {
            **super()._repr_attrs(),
            "top_w": self.top_w,
            "bottom_w": self.bottom_w,
            "align": self.align,
            "width": self.width,
            "valign": self.valign,
            "height": self.height,
            "min_width": self.min_width,
            "min_height": self.min_height,
            "left": self.left,
            "right": self.right,
            "top": self.top,
            "bottom": self.bottom,
        }
        return remove_defaults(attrs, Overlay.__init__)

    def __rich_repr__(self) -> Iterator[tuple[str | None, typing.Any] | typing.Any]:
        """Yield this widget's constructor arguments as `(name, value)` pairs, for `rich`'s repr protocol."""
        yield "top", self.top_w
        yield "bottom", self.bottom_w
        yield "align", self.align
        yield "width", self.width
        yield "valign", self.valign
        yield "height", self.height
        yield "min_width", self.min_width
        yield "min_height", self.min_height
        yield "left", self.left
        yield "right", self.right
        yield "top", self.top
        yield "bottom", self.bottom

    @property
    def align(self) -> Align | tuple[Literal[WHSettings.RELATIVE], int]:
        """Return the simplified horizontal alignment of the top widget."""
        return simplify_align(self.align_type, self.align_amount)

    @property
    def width(
        self,
    ) -> (
        Literal[WHSettings.CLIP, WHSettings.PACK]
        | int
        | tuple[Literal[WHSettings.RELATIVE], int]
        | tuple[Literal[WHSettings.WEIGHT], int | float]
    ):
        """Return the simplified width of the top widget."""
        return simplify_width(self.width_type, self.width_amount)

    @property
    def valign(self) -> VAlign | tuple[Literal[WHSettings.RELATIVE], int]:
        """Return the simplified vertical alignment of the top widget."""
        return simplify_valign(self.valign_type, self.valign_amount)

    @property
    def height(
        self,
    ) -> (
        int
        | Literal[WHSettings.FLOW, WHSettings.PACK]
        | tuple[Literal[WHSettings.RELATIVE], int]
        | tuple[Literal[WHSettings.WEIGHT], int | float]
    ):
        """Return the simplified height of the top widget."""
        return simplify_height(self.height_type, self.height_amount)

    @staticmethod
    def options(
        align_type: Literal["left", "center", "right", "relative", WHSettings.RELATIVE] | Align,
        align_amount: int | None,
        width_type: Literal["clip", "pack", "relative", "given"] | WHSettings,
        width_amount: int | None,
        valign_type: Literal["top", "middle", "bottom", "relative", WHSettings.RELATIVE] | VAlign,
        valign_amount: int | None,
        height_type: Literal["flow", "pack", "relative", "given"] | WHSettings,
        height_amount: int | None,
        min_width: int | None = None,
        min_height: int | None = None,
        left: int = 0,
        right: int = 0,
        top: int = 0,
        bottom: int = 0,
    ) -> OverlayOptions:
        """
        Return a new options tuple for use in this Overlay's .contents mapping.

        This is the common container API to create options for replacing the
        top widget of this Overlay.  It is provided for completeness
        but is not necessarily the easiest way to change the overlay parameters.
        See also :meth:`.set_overlay_parameters`

        :raises ValueError: *align* or *valign* names an unknown alignment type.
        """
        if align_type in {Align.LEFT, Align.CENTER, Align.RIGHT}:
            align: Align | Literal[WHSettings.RELATIVE] = Align(align_type)
        elif align_type == WHSettings.RELATIVE:
            align = WHSettings.RELATIVE
        else:
            raise ValueError(f"Unknown alignment type {align_type!r}")

        if valign_type in {VAlign.TOP, VAlign.MIDDLE, VAlign.BOTTOM}:
            valign: VAlign | Literal[WHSettings.RELATIVE] = VAlign(valign_type)
        elif valign_type == WHSettings.RELATIVE:
            valign = WHSettings.RELATIVE
        else:
            raise ValueError(f"Unknown vertical alignment type {valign_type!r}")

        return OverlayOptions(
            align,
            align_amount,
            WHSettings(width_type),
            width_amount,
            min_width,
            left,
            right,
            valign,
            valign_amount,
            WHSettings(height_type),
            height_amount,
            min_height,
            top,
            bottom,
        )

    def set_overlay_parameters(
        self,
        align: (
            Literal["left", "center", "right"]
            | Align
            | tuple[Literal["relative", "fixed left", "fixed right", WHSettings.RELATIVE], int]
        ),
        width: (
            Literal["pack", WHSettings.PACK]
            | int
            | tuple[Literal["relative", "fixed left", "fixed right", WHSettings.RELATIVE], int]
            | None
        ),
        valign: (
            Literal["top", "middle", "bottom"]
            | VAlign
            | tuple[Literal["relative", "fixed top", "fixed bottom", WHSettings.RELATIVE], int]
        ),
        height: (
            Literal["pack", WHSettings.PACK]
            | int
            | tuple[Literal["relative", "fixed top", "fixed bottom", WHSettings.RELATIVE], int]
            | None
        ),
        min_width: int | None = None,
        min_height: int | None = None,
        left: int = 0,
        right: int = 0,
        top: int = 0,
        bottom: int = 0,
    ) -> None:
        """
        Adjust the overlay size and position parameters.

        See :class:`__init__() ` for a description of the parameters.

        :raises OverlayError: *valign* is not a vertical alignment value.
        """
        # convert obsolete parameters 'fixed ...':
        normalized_align: Align | tuple[Literal["relative", WHSettings.RELATIVE], int]
        if isinstance(align, tuple):
            if align[0] == "fixed left":
                left = align[1]
                normalized_align = Align.LEFT
            elif align[0] == "fixed right":
                right = align[1]
                normalized_align = Align.RIGHT
            else:
                # 'fixed left'/'fixed right' handled above, so only the relative form remains.
                normalized_align = typing.cast("tuple[Literal['relative', WHSettings.RELATIVE], int]", align)
        else:
            normalized_align = Align(align)

        if isinstance(width, tuple):
            if width[0] == "fixed left":
                left = width[1]
                width = RELATIVE_100
            elif width[0] == "fixed right":
                right = width[1]
                width = RELATIVE_100

        normalized_valign: VAlign | tuple[Literal["relative", WHSettings.RELATIVE], int]
        if isinstance(valign, tuple):
            if valign[0] == "fixed top":
                top = valign[1]
                normalized_valign = VAlign.TOP
            elif valign[0] == "fixed bottom":
                bottom = valign[1]
                normalized_valign = VAlign.BOTTOM
            else:
                # 'fixed top'/'fixed bottom' handled above, so only the relative form remains.
                normalized_valign = typing.cast("tuple[Literal['relative', WHSettings.RELATIVE], int]", valign)

        elif not isinstance(valign, (VAlign, str)):
            raise OverlayError(f"invalid valign: {valign!r}")

        else:
            normalized_valign = VAlign(valign)

        if isinstance(height, tuple):
            if height[0] == "fixed bottom":
                bottom = height[1]
                height = RELATIVE_100
            elif height[0] == "fixed top":
                top = height[1]
                height = RELATIVE_100

        if width is None:  # more obsolete values accepted
            width = WHSettings.PACK
        if height is None:
            height = WHSettings.PACK

        align_type, align_amount = normalize_align(normalized_align, OverlayError)
        width_type, width_amount = normalize_width(width, OverlayError)  # type: ignore[arg-type]
        valign_type, valign_amount = normalize_valign(normalized_valign, OverlayError)
        height_type, height_amount = normalize_height(height, OverlayError)  # type: ignore[arg-type]

        if height_type in {WHSettings.GIVEN, WHSettings.PACK}:
            min_height = None

        # use container API to set the parameters
        self.contents[1] = (
            self.top_w,
            self.options(
                align_type,
                align_amount,
                width_type,
                width_amount,
                valign_type,
                valign_amount,
                height_type,
                height_amount,
                min_width,
                min_height,
                left,
                right,
                top,
                bottom,
            ),
        )

    def selectable(self) -> bool:
        """Return selectable from top_w."""
        return self.top_w.selectable()

    def keypress(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
        key: str,
    ) -> str | None:
        """Pass keypress to top_w."""
        real_size = self.pack(size, True)
        return self.top_w.keypress(
            self.top_w_size(real_size, *self.calculate_padding_filler(real_size, True)),
            key,
        )

    @property
    def focus(self) -> TopWidget:
        """Return the child widget in focus for container widgets.

        This default implementation always returns ``None``, indicating that this widget has no children.
        """
        return self.top_w

    @property
    def focus_position(self) -> Literal[1]:
        """Return the top widget position (currently always 1)."""
        return 1

    @focus_position.setter  # type: ignore[override]
    def focus_position(self, position: Literal[1]) -> None:
        """
        Set the widget in focus.  Currently only position 1 is accepted.

        :param position: index of child widget to be made focus
        :raises IndexError: *position* is not ``1``.
        """
        if position != 1:
            raise IndexError(f"Overlay widget focus_position currently must always be set to 1, not {position}")

    @property
    def contents(self) -> MutableSequence[OverlayContentsItem[TopWidget, BottomWidget]]:
        """A list-like object holding the overlay's widgets and their options.

        It is similar to::

            [(bottom_w, bottom_options)),
             (top_w, top_options)]

        This object may be used to read or update top and bottom widgets and
        top widgets's options, but no widgets may be added or removed.

        `top_options` takes the form
        `(align_type, align_amount, width_type, width_amount, min_width, left,
        right, valign_type, valign_amount, height_type, height_amount,
        min_height, top, bottom)`

        bottom_options is always
        `('left', None, 'relative', 100, None, 0, 0,
        'top', None, 'relative', 100, None, 0, 0)`
        which means that bottom widget always covers the full area of the Overlay.
        writing a different value for `bottom_options` raises an
        :exc:`OverlayError`.
        """

        # noinspection PyMethodParameters
        class OverlayContents(MutableSequence[OverlayContentsItem[TopWidget, BottomWidget]]):
            """Two-item sequence view onto this Overlay's bottom and top widget entries."""

            # pylint: disable=no-self-argument
            def __len__(inner_self) -> int:
                return 2

            __getitem__ = self._contents__getitem__  # type: ignore[assignment]
            __setitem__ = self._contents__setitem__  # type: ignore[assignment]

            def __delitem__(self, index: int | slice) -> typing.NoReturn:
                """
                Raise :exc:`TypeError`: an Overlay always holds exactly two children.

                :raises TypeError: the contents are a fixed-size sequence.
                """
                raise TypeError("OverlayContents is fixed-sized sequence")

            def insert(self, index: int | slice, value: typing.Any) -> typing.NoReturn:
                """
                Raise :exc:`TypeError`: an Overlay always holds exactly two children.

                :raises TypeError: the contents are a fixed-size sequence.
                """
                raise TypeError("OverlayContents is fixed-sized sequence")

            def __repr__(inner_self) -> str:
                return repr(f"<{inner_self.__class__.__name__}({[inner_self[0], inner_self[1]]})> for {self}")

            def __rich_repr__(inner_self) -> Iterator[tuple[str | None, typing.Any] | typing.Any]:
                for val in inner_self:
                    yield None, val

            def __iter__(inner_self) -> Iterator[OverlayContentsItem[TopWidget, BottomWidget]]:
                for idx in range(2):
                    yield inner_self[idx]  # type: ignore[arg-type]

        return OverlayContents()

    @contents.setter
    def contents(self, new_contents: Sequence[OverlayContentsItem[TopWidget, BottomWidget]]) -> None:
        """
        Replace both children of this Overlay at once.

        :raises ValueError: *new_contents* does not hold exactly two items.
        """
        if len(new_contents) != 2:
            raise ValueError("Contents length for overlay should be only 2")
        self.contents[0] = new_contents[0]
        self.contents[1] = new_contents[1]

    def _contents__getitem__(
        self,
        index: Literal[0, 1],
    ) -> tuple[TopWidget | BottomWidget, OverlayOptions]:
        """
        Return the ``(widget, options)`` pair at *index*, for the container contents protocol.

        :raises IndexError: *index* is neither ``0`` nor ``1``.
        """
        if index == 0:
            return (self.bottom_w, self._DEFAULT_BOTTOM_OPTIONS)

        if index == 1:
            return (
                self.top_w,
                OverlayOptions(
                    self.align_type,
                    self.align_amount,
                    self.width_type,
                    self.width_amount,
                    self.min_width,
                    self.left,
                    self.right,
                    self.valign_type,
                    self.valign_amount,
                    self.height_type,
                    self.height_amount,
                    self.min_height,
                    self.top,
                    self.bottom,
                ),
            )
        raise IndexError(f"Overlay.contents has no position {index!r}")

    @typing.overload
    def _contents__setitem__(
        self,
        index: Literal[0],
        value: tuple[BottomWidget, OverlayOptions],
    ) -> None: ...

    @typing.overload
    def _contents__setitem__(
        self,
        index: Literal[1],
        value: tuple[TopWidget, OverlayOptions],
    ) -> None: ...

    def _contents__setitem__(
        self,
        index: Literal[0, 1],
        value: tuple[TopWidget | BottomWidget, OverlayOptions],
    ) -> None:
        """
        Replace the ``(widget, options)`` pair at *index*, for the container contents protocol.

        :raises OverlayError: *value* is not a ``(widget, options)`` pair, or the options are not valid for that
            position.
        :raises IndexError: *index* is neither ``0`` nor ``1``.
        """
        try:
            value_w, value_options = value
        except (ValueError, TypeError) as exc:
            raise OverlayError(f"added content invalid: {value!r}").with_traceback(exc.__traceback__) from exc
        if index == 0:
            if value_options != self._DEFAULT_BOTTOM_OPTIONS:
                raise OverlayError(f"bottom_options must be set to {self._DEFAULT_BOTTOM_OPTIONS!r}")
            self.bottom_w = value_w  # type: ignore[assignment]
        elif index == 1:
            try:
                (
                    align_type,
                    align_amount,
                    width_type,
                    width_amount,
                    min_width,
                    left,
                    right,
                    valign_type,
                    valign_amount,
                    height_type,
                    height_amount,
                    min_height,
                    top,
                    bottom,
                ) = value_options
            except (ValueError, TypeError) as exc:
                raise OverlayError(f"top_options is invalid: {value_options!r}").with_traceback(
                    exc.__traceback__
                ) from exc
            # normalize first, this is where errors are raised
            align_type, align_amount = normalize_align(simplify_align(align_type, align_amount), OverlayError)
            width_type, width_amount = normalize_width(
                simplify_width(width_type, width_amount),  # type: ignore[call-overload]  # broad WHSettings
                OverlayError,
            )
            valign_type, valign_amount = normalize_valign(simplify_valign(valign_type, valign_amount), OverlayError)
            height_type, height_amount = normalize_height(
                simplify_height(height_type, height_amount),  # type: ignore[call-overload]  # broad WHSettings
                OverlayError,
            )
            self.align_type = align_type
            self.align_amount = align_amount
            # normalize_width also allows CLIP, while Overlay supports only PACK, GIVEN and RELATIVE widths
            self.width_type = typing.cast(
                "Literal[WHSettings.PACK, WHSettings.GIVEN, WHSettings.RELATIVE]",
                width_type,
            )
            self.width_amount = width_amount
            self.valign_type = valign_type
            self.valign_amount = valign_amount
            self.height_type = height_type
            self.height_amount = height_amount
            self.left = left
            self.right = right
            self.top = top
            self.bottom = bottom
            self.min_width = min_width
            self.min_height = min_height
            self.top_w = value_w  # type: ignore[assignment]
        else:
            raise IndexError(f"Overlay.contents has no position {index!r}")
        self._invalidate()

    def get_cursor_coords(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
    ) -> tuple[int, int] | None:
        """Return cursor coords from top_w, if any."""
        if not hasattr(self.top_w, "get_cursor_coords"):
            return None
        real_size = self.pack(size, True)
        (maxcol, maxrow) = real_size
        left, right, top, bottom = self.calculate_padding_filler(real_size, True)
        x, y = self.top_w.get_cursor_coords((maxcol - left - right, maxrow - top - bottom))
        if y >= maxrow:  # required??
            y = maxrow - 1
        return x + left, y + top

    def calculate_padding_filler(
        self,
        size: tuple[int, int],
        focus: bool,
    ) -> tuple[int, int, int, int]:
        """Return (padding left, right, filler top, bottom).

        :raises OverlayError: the top widget is FIXED but reports no height.
        """
        (maxcol, maxrow) = size
        height = None
        if self.width_type == WHSettings.PACK:
            width, height = self.top_w.pack((), focus=focus)
            if not height:
                raise OverlayError("fixed widget must have a height")
            left, right = calculate_left_right_padding(
                maxcol,
                self.align_type,
                self.align_amount,
                WHSettings.CLIP,
                width,
                None,
                self.left,
                self.right,
            )
        else:
            left, right = calculate_left_right_padding(
                maxcol,
                self.align_type,
                self.align_amount,
                self.width_type,
                typing.cast("int", self.width_amount),
                self.min_width,
                self.left,
                self.right,
            )

        if height:
            # top_w is a fixed widget
            top, bottom = calculate_top_bottom_filler(
                maxrow,
                self.valign_type,
                self.valign_amount,
                WHSettings.GIVEN,
                height,
                None,
                self.top,
                self.bottom,
            )
            if maxrow - top - bottom < height:
                bottom = maxrow - top - height
        elif self.height_type == WHSettings.PACK:
            # top_w is a flow widget: it will be rendered at the width left over by the horizontal padding,
            # so the row count has to be measured at that width and not at the full width of the overlay.
            height = typing.cast("AbstractFlowWidget", self.top_w).rows((maxcol - left - right,), focus=focus)
            top, bottom = calculate_top_bottom_filler(
                maxrow,
                self.valign_type,
                self.valign_amount,
                WHSettings.GIVEN,
                height,
                None,
                self.top,
                self.bottom,
            )
            if height > maxrow:  # flow widget rendered too large
                bottom = maxrow - height
        else:
            top, bottom = calculate_top_bottom_filler(
                maxrow,
                self.valign_type,
                self.valign_amount,
                self.height_type,
                typing.cast("int", self.height_amount),
                self.min_height,
                self.top,
                self.bottom,
            )
        return left, right, top, bottom

    def top_w_size(
        self,
        size: tuple[int, int],
        left: int,
        right: int,
        top: int,
        bottom: int,
    ) -> tuple[()] | tuple[int] | tuple[int, int]:
        """Return the size to pass to top_w."""
        if self.width_type == WHSettings.PACK:
            # top_w is a fixed widget
            return ()
        maxcol, maxrow = size
        if self.height_type == WHSettings.PACK:
            # top_w is a flow widget
            return (maxcol - left - right,)
        return (maxcol - left - right, maxrow - top - bottom)

    def render(self, size: tuple[()] | tuple[int] | tuple[int, int], focus: bool = False) -> CompositeCanvas:
        """Render top_w overlayed on bottom_w."""
        real_size = self.pack(size, focus)

        left, right, top, bottom = self.calculate_padding_filler(real_size, focus)
        bottom_c = self.bottom_w.render(real_size)
        if not bottom_c.cols() or not bottom_c.rows():
            return CompositeCanvas(bottom_c)

        top_c = self.top_w.render(self.top_w_size(real_size, left, right, top, bottom), focus)
        top_c = CompositeCanvas(top_c)
        if left < 0 or right < 0:
            top_c.pad_trim_left_right(min(0, left), min(0, right))
        if top < 0 or bottom < 0:
            top_c.pad_trim_top_bottom(min(0, top), min(0, bottom))

        # Negative padding clips top_w instead of shifting it: the trimming above already removed the hidden part,
        # so what is left starts at the edge of the area available for the overlay.
        return CanvasOverlay(top_c, bottom_c, max(left, 0), max(top, 0))

    def mouse_event(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
        event: str,
        button: int,
        col: int,
        row: int,
        focus: bool,
    ) -> bool | None:
        """Pass event to top_w, ignore if outside of top_w."""
        if not hasattr(self.top_w, "mouse_event"):
            return False

        real_size = self.pack(size, focus)

        left, right, top, bottom = self.calculate_padding_filler(real_size, focus)
        maxcol, maxrow = real_size
        if col < left or col >= maxcol - right or row < top or row >= maxrow - bottom:
            return False

        return self.top_w.mouse_event(
            self.top_w_size(real_size, left, right, top, bottom),
            event,
            button,
            col - left,
            row - top,
            focus,
        )
urwid-urwid-69907e4/urwid/widget/padding.py000066400000000000000000000546771526073734300207170ustar00rootroot00000000000000"""Padding: a decoration that adds blank columns to the left and right of a widget."""

from __future__ import annotations

import typing
import warnings

from urwid.canvas import CompositeCanvas, SolidCanvas
from urwid.split_repr import remove_defaults
from urwid.util import int_scale

from .constants import (
    RELATIVE_100,
    Align,
    Sizing,
    WHSettings,
    normalize_align,
    normalize_width,
    simplify_align,
    simplify_width,
)
from .widget_decoration import WidgetDecoration, WidgetError, WidgetWarning

if typing.TYPE_CHECKING:
    from collections.abc import Iterator

    from typing_extensions import Literal

    from .widget import AbstractFlowWidget, AbstractWidget

WrappedWidget = typing.TypeVar("WrappedWidget", bound="AbstractWidget")


class PaddingError(WidgetError):
    """Padding related errors."""


class PaddingWarning(WidgetWarning):
    """Padding related warnings."""


class Padding(WidgetDecoration[WrappedWidget], typing.Generic[WrappedWidget]):
    """Decoration that adds blank columns to the left and/or right of a wrapped widget."""

    def __init__(
        self,
        w: WrappedWidget,
        align: (
            Literal["left", "center", "right"]
            | Align
            | tuple[Literal["relative", WHSettings.RELATIVE, "fixed left", "fixed right"], int]
        ) = Align.LEFT,
        width: (
            int
            | Literal["pack", "clip", WHSettings.PACK, WHSettings.CLIP]
            | tuple[Literal["relative", WHSettings.RELATIVE, "fixed left", "fixed right"], int]
            | None
        ) = RELATIVE_100,
        min_width: int | None = None,
        left: int = 0,
        right: int = 0,
    ) -> None:
        r"""Build the widget, padding *w* on the left and/or right.

        :param w: a box, flow or fixed widget to pad on the left and/or right
            this widget is stored as self.original_widget

        :param align: one of: ``'left'``, ``'center'``, ``'right'``
            (``'relative'``, *percentage* 0=left 100=right)

        :param width: one of:

            *given width*
              integer number of columns for self.original_widget

            ``'pack'``
              try to pack self.original_widget to its ideal size

            (``'relative'``, *percentage of total width*)
              make width depend on the container's width

            ``'clip'``
              to enable clipping mode for a fixed widget

        :param min_width: the minimum number of columns for self.original_widget or ``None``

        :param left: a fixed number of columns to pad on the left

        :param right: a fixed number of columns to pad on the right

        Clipping Mode: (width= ``'clip'``)
        In clipping mode this padding widget will behave as a flow
        widget and self.original_widget will be treated as a fixed widget.
        self.original_widget will be clipped to fit the available number of columns.
        For example if align is ``'left'`` then self.original_widget may be clipped on the right.

        Pack Mode: (width= ``'pack'``)
        In pack mode is supported FIXED operation if it is supported by the original widget.

        >>> from urwid import Divider, Text, BigText, FontRegistry
        >>> from urwid.util import set_temporary_encoding
        >>> size = (7,)
        >>> def pr(w):
        ...     with set_temporary_encoding("utf-8"):
        ...         for t in w.render(size).text:
        ...             print(f"|{t.decode('utf-8')}|")
        >>> pr(Padding(Text("Head"), ("relative", 20), "pack"))
        | Head  |
        >>> pr(Padding(Divider("-"), left=2, right=1))
        |  ---- |
        >>> pr(Padding(Divider("*"), "center", 3))
        |  ***  |
        >>> p = Padding(Text("1234"), "left", 2, None, 1, 1)
        >>> p
         left=1 right=1 width=2>
        >>> pr(p)  # align against left
        | 12    |
        | 34    |
        >>> p.align = "right"
        >>> pr(p)  # align against right
        |    12 |
        |    34 |
        >>> pr(Padding(Text("hi\nthere"), "right", "pack"))  # pack text first
        |  hi   |
        |  there|
        >>> pr(Padding(BigText("1,2,3", FontRegistry["Thin 3x3"]()), width="clip"))
        | �  ┌─�|
        | │  ┌─┘|
        | ┴ ,└─ |
        """
        super().__init__(w)

        # convert obsolete parameters 'fixed left' and 'fixed right':
        if isinstance(align, tuple) and align[0] in {"fixed left", "fixed right"}:
            if align[0] == "fixed left":
                left = align[1]
                align = Align.LEFT
            else:
                right = align[1]
                align = Align.RIGHT
        if isinstance(width, tuple) and width[0] in {"fixed left", "fixed right"}:
            if width[0] == "fixed left":
                left = width[1]
            else:
                right = width[1]
            width = RELATIVE_100

        # convert old clipping mode width=None to width='clip'
        if width is None:
            width = WHSettings.CLIP

        self.left = left
        self.right = right
        self._align_type: Align | Literal[WHSettings.RELATIVE]
        self._align_amount: int | None
        self._width_type: Literal[WHSettings.CLIP, WHSettings.PACK, WHSettings.GIVEN, WHSettings.RELATIVE]
        self._width_amount: int | None

        self._align_type, self._align_amount = normalize_align(align, PaddingError)  # type: ignore[arg-type]
        self._width_type, self._width_amount = normalize_width(width, PaddingError)  # type: ignore[arg-type]
        self.min_width = min_width

    def sizing(self) -> frozenset[Sizing]:
        """Widget sizing.

        Rules:
        * width == CLIP: only FLOW is supported, and wrapped widget should support FIXED
        * width == GIVEN: FIXED is supported, and wrapped widget should support FLOW
        * All other cases: use sizing of target widget
        """
        if self._width_type == WHSettings.CLIP:
            return frozenset((Sizing.FLOW,))

        sizing = set(self.original_widget.sizing())
        if self._width_type == WHSettings.GIVEN:
            if Sizing.FLOW in sizing:
                sizing.add(Sizing.FIXED)

            elif Sizing.BOX not in sizing:
                warnings.warn(
                    f"WHSettings.GIVEN expect BOX or FLOW widget to be used, but received {self.original_widget}",
                    PaddingWarning,
                    stacklevel=3,
                )

        return frozenset(sizing)

    def _repr_attrs(self) -> dict[str, typing.Any]:
        attrs = {
            **super()._repr_attrs(),
            "align": self.align,
            "width": self.width,
            "left": self.left,
            "right": self.right,
            "min_width": self.min_width,
        }
        return remove_defaults(attrs, Padding.__init__)

    def __rich_repr__(self) -> Iterator[tuple[str | None, typing.Any] | typing.Any]:
        """Yield this widget's constructor arguments as `(name, value)` pairs, for `rich`'s repr protocol."""
        yield "w", self.original_widget
        yield "align", self.align
        yield "width", self.width
        yield "min_width", self.min_width
        yield "left", self.left
        yield "right", self.right

    @property
    def align(
        self,
    ) -> Literal["left", "center", "right"] | Align | tuple[Literal["relative", WHSettings.RELATIVE], int]:
        """Return the padding alignment setting."""
        return simplify_align(self._align_type, self._align_amount)

    @align.setter
    def align(
        self, align: Literal["left", "center", "right"] | Align | tuple[Literal["relative", WHSettings.RELATIVE], int]
    ) -> None:
        """Set the padding alignment."""
        self._align_type, self._align_amount = normalize_align(align, PaddingError)
        self._invalidate()

    @property
    def width(
        self,
    ) -> Literal[WHSettings.CLIP, WHSettings.PACK] | int | tuple[Literal[WHSettings.RELATIVE], int]:
        """Return the padding width."""
        return simplify_width(self._width_type, self._width_amount)

    @width.setter
    def width(
        self,
        width: (
            Literal["clip", "pack", WHSettings.CLIP, WHSettings.PACK]
            | int
            | tuple[Literal["relative", WHSettings.RELATIVE], int]
        ),
    ) -> None:
        """Set the padding width."""
        self._width_type, self._width_amount = normalize_width(width, PaddingError)
        self._invalidate()

    def pack(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int] = (),
        focus: bool = False,
    ) -> tuple[int, int]:
        """
        Return the size the widget would render as, for FIXED sizing.

        :raises PaddingError: this Padding uses ``CLIP``, which makes it FLOW-only, or its width type does not resolve
            to a fixed size.
        """
        if size:
            return super().pack(size, focus)
        if self._width_type == WHSettings.CLIP:
            raise PaddingError("WHSettings.CLIP makes Padding FLOW-only widget")

        expand = self.left + self.right
        w_sizing = self.original_widget.sizing()

        if self._width_type == WHSettings.GIVEN:
            if Sizing.FLOW not in w_sizing:
                warnings.warn(
                    f"WHSettings.GIVEN expect FLOW widget to be used for FIXED pack/render, "
                    f"but received {self.original_widget}",
                    PaddingWarning,
                    stacklevel=3,
                )

            width_amount = typing.cast("int", self._width_amount)

            return (
                max(width_amount, self.min_width or 1) + expand,
                typing.cast("AbstractFlowWidget", self.original_widget).rows((width_amount,), focus),
            )

        if Sizing.FIXED not in w_sizing:
            warnings.warn(
                f"Padded widget should support FIXED sizing for FIXED render, but received {self.original_widget}",
                PaddingWarning,
                stacklevel=3,
            )
        width, height = self.original_widget.pack(size, focus)

        if self._width_type == WHSettings.PACK:
            return max(width, self.min_width or 1) + expand, height

        if self._width_type == WHSettings.RELATIVE:
            width_amount = typing.cast("int", self._width_amount)
            return max(int(width * 100 / width_amount + 0.5), self.min_width or 1) + expand, height

        raise PaddingError(f"Unexpected width type: {self._width_type.upper()})")

    def render(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
        focus: bool = False,
    ) -> CompositeCanvas:
        """
        Render the Padding and return the resulting canvas.

        :raises ValueError: the Padding is empty and no *size* was given.
        """
        left, right = self.padding_values(size, focus)

        if self._width_type == WHSettings.CLIP:
            canv = self._original_widget.render((), focus)
        elif size:
            maxcol = size[0] - (left + right)
            if self._width_type == WHSettings.GIVEN and maxcol < self._width_amount:  # type: ignore[operator]
                warnings.warn(
                    f"{self}.render(size={size}, focus={focus}): too narrow size ({maxcol!r} < {self._width_amount!r})",
                    PaddingWarning,
                    stacklevel=3,
                )
            canv = self._original_widget.render((maxcol, *size[1:]), focus)
        elif self._width_type == WHSettings.GIVEN:
            canv = self._original_widget.render((typing.cast("int", self._width_amount), *size[1:]), focus)
        else:
            canv = self._original_widget.render((), focus)

        if canv.cols() == 0:
            if not size:
                raise ValueError(f"{self}.render(size={size}, focus={focus}): size is required for empty widget")

            canv = SolidCanvas(" ", size[0], canv.rows())
            canv = CompositeCanvas(canv)
            canv.set_depends([self._original_widget])
            return canv

        canv = CompositeCanvas(canv)
        canv.set_depends([self._original_widget])
        if left != 0 or right != 0:
            canv.pad_trim_left_right(left, right)

        return canv

    def padding_values(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
        focus: bool,
    ) -> tuple[int, int]:
        """Return the number of columns to pad on the left and right.

        Override this method to define custom padding behaviour.

        :raises PaddingError: this Padding uses ``CLIP`` and no *size* was given.
        """
        if self._width_type == WHSettings.CLIP:
            if not size:
                raise PaddingError("WHSettings.CLIP makes Padding FLOW-only widget")

            width, _ignore = self._original_widget.pack((), focus=focus)
            return calculate_left_right_padding(
                size[0],
                self._align_type,
                self._align_amount,
                WHSettings.CLIP,
                width,
                None,
                self.left,
                self.right,
            )

        if self._width_type == WHSettings.PACK:
            if size:
                maxcol = size[0]
                maxwidth = max(maxcol - self.left - self.right, self.min_width or 0)
                (width, _ignore) = self._original_widget.pack((maxwidth,), focus=focus)
            else:
                (width, _ignore) = self._original_widget.pack((), focus=focus)
                maxcol = width + self.left + self.right

            return calculate_left_right_padding(
                maxcol,
                self._align_type,
                self._align_amount,
                WHSettings.GIVEN,
                width,
                self.min_width,
                self.left,
                self.right,
            )

        if size:
            maxcol = size[0]
        elif self._width_type == WHSettings.GIVEN:
            maxcol = typing.cast("int", self._width_amount) + self.left + self.right
        else:
            maxcol = (
                max(
                    self._original_widget.pack((), focus=focus)[0] * 100 // typing.cast("int", self._width_amount),
                    self.min_width or 1,
                )
                + self.left
                + self.right
            )

        return calculate_left_right_padding(
            maxcol,
            self._align_type,
            self._align_amount,
            self._width_type,
            typing.cast("int", self._width_amount),
            self.min_width,
            self.left,
            self.right,
        )

    def rows(self, size: tuple[int], focus: bool = False) -> int:
        """Return the rows needed for self.original_widget."""
        (maxcol,) = size
        left, right = self.padding_values(size, focus)
        if self._width_type == WHSettings.PACK:
            _pcols, prows = self._original_widget.pack((maxcol - left - right,), focus)
            return prows
        if self._width_type == WHSettings.CLIP:
            _fcols, frows = self._original_widget.pack((), focus)
            return frows
        return typing.cast("AbstractFlowWidget", self.original_widget).rows(
            (maxcol - left - right,),
            focus=focus,
        )

    def keypress(self, size: tuple[()] | tuple[int] | tuple[int, int], key: str) -> str | None:
        """Pass keypress to self._original_widget."""
        left, right = self.padding_values(size, True)
        if size:
            maxvals = (size[0] - left - right, *size[1:])
            return self._original_widget.keypress(maxvals, key)
        return self._original_widget.keypress((), key)

    def get_cursor_coords(self, size: tuple[()] | tuple[int] | tuple[int, int]) -> tuple[int, int] | None:
        """Return the (x,y) coordinates of cursor within self._original_widget."""
        if not hasattr(self._original_widget, "get_cursor_coords"):
            return None

        left, right = self.padding_values(size, True)
        if size:
            maxvals = (size[0] - left - right, *size[1:])
            if maxvals[0] == 0:
                return None
        else:
            maxvals = ()

        if (coords := self._original_widget.get_cursor_coords(maxvals)) is not None:
            x, y = coords
            return x + left, y

        return None

    def move_cursor_to_coords(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
        x: int,
        y: int,
    ) -> bool:
        """Set the cursor position with (x,y) coordinates of self._original_widget.

        Returns True if move succeeded, False otherwise.
        """
        if not hasattr(self._original_widget, "move_cursor_to_coords"):
            return True

        left, right = self.padding_values(size, True)
        if size:
            maxcol = size[0]
            maxvals = (maxcol - left - right, *size[1:])
        else:
            maxcol = self.pack((), True)[0]
            maxvals = ()

        if isinstance(x, int):
            if x < left:
                x = left
            elif x >= maxcol - right:
                x = maxcol - right - 1
            x -= left

        return typing.cast("bool", self._original_widget.move_cursor_to_coords(maxvals, x, y))

    def mouse_event(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
        event: str,
        button: int,
        col: int,
        row: int,
        focus: bool,
    ) -> bool | None:
        """Send mouse event if position is within self._original_widget."""
        if not hasattr(self._original_widget, "mouse_event"):
            return False

        left, right = self.padding_values(size, focus)
        if size:
            maxcol = size[0]
            if col < left or col >= maxcol - right:
                return False
            maxvals = (maxcol - left - right, *size[1:])
        else:
            maxvals = ()

        return self._original_widget.mouse_event(maxvals, event, button, col - left, row, focus)

    def get_pref_col(self, size: tuple[()] | tuple[int] | tuple[int, int]) -> int | None:
        """Return the preferred column from self._original_widget, or None."""
        if not hasattr(self._original_widget, "get_pref_col"):
            return None

        left, right = self.padding_values(size, True)
        if size:
            maxvals = (size[0] - left - right, *size[1:])
        else:
            maxvals = ()

        x = typing.cast("int | None", self._original_widget.get_pref_col(maxvals))
        if isinstance(x, int):
            return x + left
        return x


@typing.overload
def calculate_left_right_padding(
    maxcol: int,
    align_type: Literal["relative", WHSettings.RELATIVE],
    align_amount: int,
    width_type: Literal["fixed", "relative", "clip", "given", WHSettings.RELATIVE, WHSettings.CLIP, WHSettings.GIVEN],
    width_amount: int,
    min_width: int | None,
    left: int,
    right: int,
) -> tuple[int, int]: ...


@typing.overload
def calculate_left_right_padding(
    maxcol: int,
    align_type: Literal["relative", WHSettings.RELATIVE],
    align_amount: None,
    width_type: Literal["fixed", "relative", "clip", "given", WHSettings.RELATIVE, WHSettings.CLIP, WHSettings.GIVEN],
    width_amount: int,
    min_width: int | None,
    left: int,
    right: int,
) -> typing.NoReturn: ...


@typing.overload
def calculate_left_right_padding(
    maxcol: int,
    align_type: Literal["left", "center", "right"] | Align,
    align_amount: int | None,
    width_type: Literal["fixed", "relative", "clip", "given", WHSettings.RELATIVE, WHSettings.CLIP, WHSettings.GIVEN],
    width_amount: int,
    min_width: int | None,
    left: int,
    right: int,
) -> tuple[int, int]: ...


def calculate_left_right_padding(
    maxcol: int,
    align_type: Literal["left", "center", "right", "relative", WHSettings.RELATIVE] | Align,
    align_amount: int | None,
    width_type: Literal["fixed", "relative", "clip", "given", WHSettings.RELATIVE, WHSettings.CLIP, WHSettings.GIVEN],
    width_amount: int,
    min_width: int | None,
    left: int,
    right: int,
) -> tuple[int, int]:
    """Return the amount of padding (or clipping) on the left and right part of maxcol columns.

    :param align_type: 'left', 'center', 'right', 'relative'
    :param align_amount: a percentage when align_type=='relative'
    :param width_type: 'fixed', 'relative', 'clip'
    :param width_amount: a percentage when width_type=='relative' otherwise equal to the width of the widget
    :param min_width: a desired minimum width for the widget or None
    :param left: a fixed number of columns to pad on the left
    :param right: a fixed number of columns to pad on the right
    :raises TypeError: *align_type* is relative but no *align_amount* was given.

    >>> clrp = calculate_left_right_padding
    >>> clrp(15, "left", 0, "given", 10, None, 2, 0)
    (2, 3)
    >>> clrp(15, "relative", 0, "given", 10, None, 2, 0)
    (2, 3)
    >>> clrp(15, "relative", 100, "given", 10, None, 2, 0)
    (5, 0)
    >>> clrp(15, "center", 0, "given", 4, None, 2, 0)
    (6, 5)
    >>> clrp(15, "left", 0, "clip", 18, None, 0, 0)
    (0, -3)
    >>> clrp(15, "right", 0, "clip", 18, None, 0, -1)
    (-2, -1)
    >>> clrp(15, "center", 0, "given", 18, None, 2, 0)
    (0, 0)
    >>> clrp(20, "left", 0, "relative", 60, None, 0, 0)
    (0, 8)
    >>> clrp(20, "relative", 30, "relative", 60, None, 0, 0)
    (2, 6)
    >>> clrp(20, "relative", 30, "relative", 60, 14, 0, 0)
    (2, 4)
    """
    if align_type == WHSettings.RELATIVE and align_amount is None:
        raise TypeError("align_amount must be specified when align_type is relative")

    if width_type == WHSettings.RELATIVE:
        maxwidth = max(maxcol - left - right, 0)
        width = int(maxwidth * width_amount / 100 + 0.5)
        if min_width is not None:
            width = max(width, min_width)
    else:
        width = width_amount

    align: int = {  # type: ignore[assignment]  # we filtered not allowed None
        Align.LEFT: 0,
        Align.CENTER: 50,
        Align.RIGHT: 100,
    }.get(align_type, align_amount)  # type: ignore[arg-type]  # we filtered not allowed None

    # add the remainder of left/right the padding
    padding = maxcol - width - left - right
    right += int_scale(100 - align, 101, padding + 1)
    left = maxcol - width - right

    # reduce padding if we are clipping an edge
    if right < 0 < left:
        shift = min(left, -right)
        left -= shift
        right += shift
    elif left < 0 < right:
        shift = min(right, -left)
        right -= shift
        left += shift

    # only clip if width_type == 'clip'
    if width_type != WHSettings.CLIP and (left < 0 or right < 0):
        left = max(left, 0)
        right = max(right, 0)

    return left, right
urwid-urwid-69907e4/urwid/widget/pile.py000066400000000000000000001372771526073734300202400ustar00rootroot00000000000000"""Pile: a container that stacks widgets vertically."""

from __future__ import annotations

import itertools
import math
import sys
import typing
import warnings
from itertools import chain, repeat

from typing_extensions import Literal

from urwid.canvas import CanvasCombine, CompositeCanvas, SolidCanvas
from urwid.command_map import Command
from urwid.split_repr import remove_defaults
from urwid.util import is_mouse_press

from .constants import Sizing, WHSettings
from .container import WidgetContainerListContentsMixin, WidgetContainerMixin, _ContainerElementSizingFlag
from .monitored_list import MonitoredFocusList, MonitoredList
from .widget import (
    AbstractBoxWidget,
    AbstractFixedWidget,
    AbstractFlowWidget,
    AbstractWidget,
    Widget,
    WidgetError,
    WidgetWarning,
)

if typing.TYPE_CHECKING:
    from collections.abc import Collection, Iterable, Iterator, Sequence


# Flag combinations tested on every element of every ``Pile.sizing()`` call, hoisted out of the loop
# so that the enum member lookups and the OR-ing happen once per import instead of once per element.
_BOX_FLOW_FIXED = _ContainerElementSizingFlag.BOX | _ContainerElementSizingFlag.FLOW | _ContainerElementSizingFlag.FIXED
_FLOW_FIXED = _ContainerElementSizingFlag.FLOW | _ContainerElementSizingFlag.FIXED
_BOX_OR_FLOW = frozenset((Sizing.BOX, Sizing.FLOW))


class PileError(WidgetError):
    """Pile related errors."""


class PileWarning(WidgetWarning):
    """Pile related warnings."""


class Pile(
    Widget,
    WidgetContainerMixin[int],
    WidgetContainerListContentsMixin[
        typing.Union[
            tuple[
                typing.Union[AbstractFlowWidget, AbstractFixedWidget],
                tuple[Literal[WHSettings.PACK], None],
            ],
            tuple[
                AbstractBoxWidget,
                tuple[Literal[WHSettings.GIVEN], int],
            ],
            tuple[
                typing.Union[AbstractBoxWidget, AbstractFlowWidget],
                tuple[Literal[WHSettings.WEIGHT], typing.Union[int, float]],
            ],
        ]
    ],
):
    """A pile of widgets stacked vertically from top to bottom."""

    def sizing(self) -> frozenset[Sizing]:
        """Sizing supported by widget.

        :return: Calculated widget sizing

        Due to the nature of container with mutable contents, this method cannot be cached.

        Rules:
        * WEIGHT BOX -> BOX
        * GIVEN BOX -> FLOW (height is known) & BOX (can be shrinked/padded)
        * PACK BOX -> Unsupported

        * WEIGHT FLOW -> FLOW
        * GIVEN FLOW -> Unsupported
        * PACK FLOW -> FLOW

        * WEIGHT FIXED -> Need also FLOW or/and BOX to properly render due to width calculation
        * GIVEN FIXED -> Unsupported
        * PACK FIXED -> FIXED (widget knows its size)

        >>> from urwid import BigText, ProgressBar, SolidFill, Text, Thin3x3Font
        >>> font = Thin3x3Font()

        # BOX-only widget
        >>> Pile((SolidFill("#"),))
        

        # GIVEN BOX -> BOX/FLOW
        >>> Pile(((10, SolidFill("#")),))
        

        # FLOW-only
        >>> Pile((ProgressBar(None, None),))
        

        # FIXED -> FIXED
        >>> Pile(((WHSettings.PACK, BigText("0", font)),))
        

        # FLOW/FIXED -> FLOW/FIXED
        >>> Pile(((WHSettings.PACK, Text("text")),))
        

        # FLOW + FIXED widgets -> FLOW/FIXED
        >>> Pile((ProgressBar(None, None), (WHSettings.PACK, BigText("0", font))))
        

        # GIVEN BOX + FIXED widgets -> BOX/FLOW/FIXED (GIVEN BOX allows overriding its height & allows any width)
        >>> Pile(((10, SolidFill("#")), (WHSettings.PACK, BigText("0", font))))
        

        # Invalid sizing combination -> use fallback settings (and produce warning)
        >>> Pile(((WHSettings.WEIGHT, 1, BigText("0", font)),))
        

        # Special case: empty pile widget sizing is impossible to calculate
        >>> Pile(())
        
        """
        if not self.contents:
            return frozenset((Sizing.BOX, Sizing.FLOW))
        strict_box = False
        has_flow = False

        has_fixed = False
        supported: set[Sizing] = set()

        for idx, (widget, (size_kind, _size_weight)) in enumerate(self.contents):
            w_sizing = widget.sizing()

            flag: int = _ContainerElementSizingFlag.NONE

            if size_kind == WHSettings.WEIGHT:
                flag |= _ContainerElementSizingFlag.WH_WEIGHT
                if Sizing.BOX in w_sizing:
                    flag |= _ContainerElementSizingFlag.BOX
                if Sizing.FLOW in w_sizing:
                    flag |= _ContainerElementSizingFlag.FLOW
                if Sizing.FIXED in w_sizing and w_sizing & _BOX_OR_FLOW:
                    flag |= _ContainerElementSizingFlag.FIXED

            elif size_kind == WHSettings.GIVEN:
                flag |= _ContainerElementSizingFlag.WH_GIVEN
                if Sizing.BOX in w_sizing:
                    flag |= _ContainerElementSizingFlag.BOX
                    flag |= _ContainerElementSizingFlag.FLOW

            else:
                flag = _ContainerElementSizingFlag.WH_PACK
                if Sizing.FLOW in w_sizing:
                    flag |= _ContainerElementSizingFlag.FLOW
                if Sizing.FIXED in w_sizing:
                    flag |= _ContainerElementSizingFlag.FIXED

            if not flag & _BOX_FLOW_FIXED:
                warnings.warn(
                    f"Sizing combination of widget {idx} not supported: {size_kind.name} {'|'.join(w_sizing).upper()}",
                    PileWarning,
                    stacklevel=3,
                )
                return frozenset((Sizing.BOX, Sizing.FLOW))

            if flag & _ContainerElementSizingFlag.BOX:
                supported.add(Sizing.BOX)
                if not flag & _FLOW_FIXED:
                    strict_box = True
                    break

            if flag & _ContainerElementSizingFlag.FLOW:
                has_flow = True
            if flag & _ContainerElementSizingFlag.FIXED:
                has_fixed = True

        if not strict_box:
            if has_flow:
                supported.add(Sizing.FLOW)
            if has_fixed:
                supported.add(Sizing.FIXED)

        return frozenset(supported)

    def __init__(
        self,
        widget_list: Iterable[
            AbstractWidget
            | tuple[Literal["pack", WHSettings.PACK], AbstractFlowWidget | AbstractFixedWidget]
            | tuple[int, AbstractBoxWidget]
            | tuple[Literal["given", WHSettings.GIVEN], int, AbstractBoxWidget]
            | tuple[Literal["weight", WHSettings.WEIGHT], int | float, AbstractBoxWidget | AbstractFlowWidget]
        ],
        focus_item: AbstractWidget | int | None = None,
    ) -> None:
        """Build the pile from a list of child widgets.

        :param widget_list: child widgets
        :param focus_item: child widget that gets the focus initially.
            Chooses the first selectable widget if unset.
        :raises PileError: an item of *widget_list* is not a widget or a valid ``(height, widget)`` pair.

        *widget_list* may also contain tuples such as:

        (*given_height*, *widget*)
            always treat *widget* as a box widget and give it *given_height* rows, where given_height is an int
        (``'pack'``, *widget*)
            allow *widget* to calculate its own height by calling its :meth:`rows` method,
            i.e. treat it as a flow widget.
        (``'weight'``, *weight*, *widget*)
            if the pile is treated as a box widget then treat widget as a box
            widget with a height based on its relative weight value, otherwise treat the same as (``'pack'``, *widget*).

        Widgets not in a tuple are the same as (``'weight'``, ``1``, *widget*)`

        .. note:: If the Pile is treated as a box widget there must be at least
            one ``'weight'`` tuple in :attr:`widget_list`.
        """
        super().__init__()
        self._contents: MonitoredFocusList[
            tuple[
                AbstractFlowWidget | AbstractFixedWidget,
                tuple[Literal[WHSettings.PACK], None],
            ]
            | tuple[
                AbstractBoxWidget,
                tuple[Literal[WHSettings.GIVEN], int],
            ]
            | tuple[
                AbstractBoxWidget | AbstractFlowWidget,
                tuple[Literal[WHSettings.WEIGHT], int | float],
            ]
        ] = MonitoredFocusList()
        self._contents.set_modified_callback(self._contents_modified)
        self._contents.set_focus_changed_callback(lambda f: self._invalidate())
        self._contents.set_validate_contents_modified(self._validate_contents_modified)

        for i, original in enumerate(widget_list):
            if not isinstance(original, tuple):
                w = original
                self.contents.append((w, (WHSettings.WEIGHT, 1)))
            elif len(original) == 2:
                if original[0] in {Sizing.FLOW, WHSettings.PACK}:  # 'pack' used to be called 'flow'
                    w = original[-1]
                    self.contents.append(
                        (
                            w,
                            (WHSettings.PACK, None),
                        )
                    )
                else:
                    height, w = original
                    self.contents.append(
                        (
                            typing.cast("AbstractBoxWidget", w),
                            (
                                WHSettings.GIVEN,
                                typing.cast("int", height),
                            ),
                        )
                    )
            elif len(original) == 3:
                settings, height, w = original  # type: ignore[assignment]
                if settings in {Sizing.FIXED, WHSettings.GIVEN}:  # backwards compatibility
                    self.contents.append(
                        (
                            typing.cast("AbstractBoxWidget", w),
                            (
                                WHSettings.GIVEN,
                                typing.cast("int", height),
                            ),
                        )
                    )
                elif settings == WHSettings.WEIGHT:
                    self.contents.append(
                        (
                            w,
                            (
                                WHSettings.WEIGHT,
                                typing.cast("int | float", height),
                            ),
                        )
                    )
                else:
                    raise PileError(f"initial widget list item invalid {original!r}")
            else:
                raise PileError(f"initial widget list item invalid {original!r}")
            if focus_item is None and w.selectable():
                focus_item = i

            if not isinstance(w, AbstractWidget):
                warnings.warn(f"{w!r} is not a Widget", PileWarning, stacklevel=3)

        if self.contents and focus_item is not None:
            self.focus = focus_item

        self.pref_col = 0

    def _repr_words(self) -> list[str]:
        if len(self.contents) > 1:
            contents_string = f"({len(self.contents)} items)"
        elif self.contents:
            contents_string = "(1 item)"
        else:
            contents_string = "()"
        return [*super()._repr_words(), contents_string]

    def _repr_attrs(self) -> dict[str, typing.Any]:
        attrs = {**super()._repr_attrs(), "focus_item": self.focus_position if len(self._contents) > 1 else None}
        return remove_defaults(attrs, Pile.__init__)

    def __rich_repr__(self) -> Iterator[tuple[str | None, typing.Any] | typing.Any]:
        """Yield this widget's constructor arguments as `(name, value)` pairs, for `rich`'s repr protocol."""
        widget_list: list[
            AbstractWidget
            | tuple[Literal[WHSettings.PACK] | int, AbstractWidget]
            | tuple[Literal[WHSettings.WEIGHT], int | float, AbstractWidget]
        ] = []

        for w_instance, (sizing, amount) in self._contents:
            if sizing == WHSettings.GIVEN:
                widget_list.append((typing.cast("int", amount), w_instance))
            elif sizing == WHSettings.PACK:
                widget_list.append((WHSettings.PACK, w_instance))
            elif sizing == WHSettings.WEIGHT:
                if amount == 1:
                    widget_list.append(w_instance)
                else:
                    widget_list.append((WHSettings.WEIGHT, typing.cast("int | float", amount), w_instance))

        yield "widget_list", widget_list
        yield "focus_item", self.focus_position if self._contents else None

    def __len__(self) -> int:
        """Return the number of widgets in the pile."""
        return len(self._contents)

    def _contents_modified(self) -> None:
        """Invalidate this widget whenever the contents has been changed."""
        self._invalidate()

    def selectable(self) -> bool:
        """Return whether any child widget is selectable.

        Asked on every call rather than cached,
        since a child such as :class:`~urwid.WidgetPlaceholder` may change its selectability after it was added.
        """
        return any(w.selectable() for w, _ in self.contents)

    def _validate_contents_modified(
        self,
        slc: tuple[int, int, int],
        new_items: Collection[
            tuple[
                AbstractWidget,
                tuple[Literal[WHSettings.PACK], None]
                | tuple[Literal[WHSettings.GIVEN], int]
                | tuple[Literal[WHSettings.WEIGHT], int | float],
            ]
        ],
    ) -> None:
        """
        Reject contents changes that would put an invalid item into the Pile.

        :raises PileError: an added item is not a valid ``(widget, options)`` pair.
        """
        invalid_items: list[tuple[AbstractWidget, tuple[typing.Any, typing.Any]]] = []
        try:
            for item in new_items:
                _w, (t, n) = item
                if any(
                    (
                        t not in {WHSettings.PACK, WHSettings.GIVEN, WHSettings.WEIGHT},
                        (n is not None and (not isinstance(n, (int, float)) or n < 0 or not math.isfinite(float(n)))),
                    )
                ):
                    invalid_items.append(item)

        # OverflowError: float() of an int too large for a float
        except (TypeError, ValueError, OverflowError) as exc:
            raise PileError(f"added content invalid: {exc}").with_traceback(exc.__traceback__) from exc

        if invalid_items:
            raise PileError(f"added content invalid: {invalid_items!r}")

    @property
    def widget_list(self) -> MonitoredList[AbstractWidget]:
        """
        A list of the widgets in this Pile.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`contents` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "only for backwards compatibility. You should use the new standard container property `contents`. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        ml = MonitoredList(w for w, t in self.contents)

        def user_modified() -> None:
            self.widget_list = ml

        ml.set_modified_callback(user_modified)
        return ml

    @widget_list.setter
    def widget_list(self, widgets: MonitoredList[AbstractWidget]) -> None:
        focus_position = self.focus_position
        self.contents = [
            (new, options)  # type: ignore[misc]  # deprecated API, historic code lack support of FIXED
            for (new, (w, options)) in zip(
                widgets,
                # need to grow contents list if widgets is longer
                chain(self.contents, repeat((None, (WHSettings.WEIGHT, 1)))),
            )
        ]
        if focus_position < len(widgets):
            self.focus_position = focus_position

    @property
    def item_types(
        self,
    ) -> MonitoredList[
        tuple[Literal[Sizing.FIXED], int]
        | tuple[Literal[Sizing.FLOW], None]
        | tuple[Literal[WHSettings.WEIGHT], int | float]
    ]:
        """
        A list of the options values for widgets in this Pile.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`contents` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "only for backwards compatibility. You should use the new standard container property `contents`. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        ml = MonitoredList(
            # return the old item type names
            (
                {
                    WHSettings.GIVEN: Sizing.FIXED,
                    WHSettings.PACK: Sizing.FLOW,
                }.get(f, f),
                height,
            )
            for w, (f, height) in self.contents
        )

        def user_modified() -> None:
            self.item_types = ml  # type: ignore[assignment]

        ml.set_modified_callback(user_modified)
        return ml  # type: ignore[return-value]

    @item_types.setter
    def item_types(
        self,
        item_types: MonitoredList[
            tuple[Literal[Sizing.FIXED, WHSettings.GIVEN], int]
            | tuple[Literal[Sizing.FLOW, WHSettings.PACK], None]
            | tuple[Literal[WHSettings.WEIGHT], int | float]
        ],
    ) -> None:
        """
        Replace the height settings of the widgets in this Pile.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`contents` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "only for backwards compatibility. You should use the new standard container property `contents`. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        focus_position = self.focus_position
        self.contents = [
            (  # type: ignore[misc]
                w,
                (
                    {
                        Sizing.FIXED: WHSettings.GIVEN,
                        Sizing.FLOW: WHSettings.PACK,
                    }.get(new_t, new_t),  # type: ignore[arg-type]  # normalisation from legacy
                    new_height,
                ),
            )
            for ((new_t, new_height), (w, options)) in zip(item_types, self.contents)
        ]
        if focus_position < len(item_types):
            self.focus_position = focus_position

    @property
    def contents(
        self,
    ) -> MonitoredFocusList[
        tuple[
            AbstractFlowWidget | AbstractFixedWidget,
            tuple[Literal[WHSettings.PACK], None],
        ]
        | tuple[
            AbstractBoxWidget,
            tuple[Literal[WHSettings.GIVEN], int],
        ]
        | tuple[
            AbstractBoxWidget | AbstractFlowWidget,
            tuple[Literal[WHSettings.WEIGHT], int | float],
        ]
    ]:
        """
        The contents of this Pile as a list of (widget, options) tuples.

        options currently may be one of

        (``'pack'``, ``None``)
            allow widget to calculate its own height by calling its
            :meth:`rows ` method, i.e. treat it as a flow widget.
        (``'given'``, *n*)
            Always treat widget as a box widget with a given height of *n* rows.
        (``'weight'``, *w*)
            If the Pile itself is treated as a box widget then
            the value *w* will be used as a relative weight for assigning rows
            to this box widget. If the Pile is being treated as a flow
            widget then this is the same as (``'pack'``, ``None``) and the *w*
            value is ignored.

        If the Pile itself is treated as a box widget then at least one
        widget must have a (``'weight'``, *w*) options value, or the Pile will
        not be able to grow to fill the required number of rows.

        This list may be modified like a normal list and the Pile widget
        will updated automatically.

        .. seealso:: Create new options tuples with the :meth:`options` method
        """
        return self._contents

    @contents.setter
    def contents(
        self,
        c: Sequence[
            tuple[
                AbstractFlowWidget | AbstractFixedWidget,
                tuple[Literal[WHSettings.PACK], None],
            ]
            | tuple[
                AbstractBoxWidget,
                tuple[Literal[WHSettings.GIVEN], int],
            ]
            | tuple[
                AbstractBoxWidget | AbstractFlowWidget,
                tuple[Literal[WHSettings.WEIGHT], int | float],
            ]
        ],
    ) -> None:
        self._contents[:] = c

    @staticmethod
    def options(
        height_type: Literal["pack", "given", "weight"] | WHSettings = WHSettings.WEIGHT,
        height_amount: int | float | None = 1,  # noqa: PYI041  # provide explicit for IDEs
    ) -> (
        tuple[Literal[WHSettings.PACK], None]
        | tuple[Literal[WHSettings.GIVEN], int]
        | tuple[Literal[WHSettings.WEIGHT], int | float]
    ):
        """
        Return a new options tuple for use in a Pile's :attr:`contents` list.

        :param height_type: ``'pack'``, ``'given'`` or ``'weight'``
        :param height_amount: ``None`` for ``'pack'``, a number of rows for
            ``'fixed'`` or a weight value (number) for ``'weight'``
        :raises PileError: *height_type* and *height_amount* are not a valid combination.
        """
        if height_type == WHSettings.PACK:
            return (WHSettings.PACK, None)
        if height_type in {WHSettings.GIVEN, WHSettings.WEIGHT} and height_amount is not None:
            return (height_type, height_amount)  # type: ignore[return-value]
        raise PileError(f"invalid combination: height_type={height_type!r}, height_amount={height_amount!r}")

    @property
    def focus(self) -> AbstractWidget | None:
        """The child widget in focus or None when Pile is empty."""
        if not self.contents:
            return None
        return self.contents[self.focus_position][0]

    @focus.setter
    def focus(self, item: AbstractWidget | int) -> None:
        """
        Set the item in focus, for backwards compatibility.

        .. note:: only for backwards compatibility. You should use the new
            standard container property :attr:`focus_position`.
            to set the position by integer index instead.

        :param item: element to focus
        :raises ValueError: *item* is a widget that is not in the contents.
        """
        if isinstance(item, int):
            self.focus_position = item
            return
        for i, (w, _options) in enumerate(self.contents):
            if item == w:
                self.focus_position = i
                return
        raise ValueError(f"Widget not found in Pile contents: {item!r}")

    def get_focus(self) -> AbstractWidget | None:
        """
        Return the widget in focus.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`focus` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "for backwards compatibility. "
            "You may also use the new standard container property .focus to get the child widget in focus. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        if not self.contents:
            return None
        return self.contents[self.focus_position][0]

    def set_focus(self, item: AbstractWidget | int) -> None:
        """
        Set the child widget in focus.

        :param item: widget or integer index
        :raises ValueError: *item* is a widget that is not in the contents.

        .. deprecated:: 1.1.0
            Use the standard container property :attr:`focus_position` instead.
            This API will be removed in version 5.0.
        """
        warnings.warn(
            "for backwards compatibility. "
            "You may also use the new standard container property .focus to get the child widget in focus. "
            "API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        if isinstance(item, int):
            self.focus_position = item
            return
        for i, (w, _options) in enumerate(self.contents):
            if item == w:
                self.focus_position = i
                return
        raise ValueError(f"Widget not found in Pile contents: {item!r}")

    @property
    def focus_position(self) -> int:
        """Index of child widget in focus.

        Raises :exc:`IndexError` if read when Pile is empty, or when set to an invalid index.

        :raises IndexError: the Pile is empty.
        """
        if (focus := self.contents.focus) is not None:
            return focus

        raise IndexError("No focus_position, Pile is empty")

    @focus_position.setter
    def focus_position(self, position: int) -> None:
        """
        Set the widget in focus.

        :param position: index of child widget to be made focus
        :raises IndexError: *position* is not an index of a child widget.
        """
        try:
            if position < 0 or position >= len(self.contents):
                raise IndexError(f"No Pile child widget at position {position}")
        except TypeError as exc:
            raise IndexError(f"No Pile child widget at position {position}").with_traceback(exc.__traceback__) from exc
        self.contents.focus = position

    def get_pref_col(self, size: tuple[()] | tuple[int] | tuple[int, int]) -> int | None:
        """Return the preferred column for the cursor, or None.

        :raises PileError: *size* is ``()`` and the children cannot be sized as a fixed widget.
        """
        if not self.selectable():
            return None

        if not self.contents:
            return None

        _, _, size_args = self.get_rows_sizes(size, focus=self.selectable())
        self._update_pref_col_from_focus(size_args[self.focus_position])
        return self.pref_col

    def get_item_size(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
        i: int,
        focus: bool,
        item_rows: list[int] | None = None,
    ) -> tuple[()] | tuple[int] | tuple[int, int]:
        """
        Return a size appropriate for passing to self.contents[i][0].render.

        :raises PileError: the item uses a height rule that needs size information the caller did not provide, or a
            height rule that is not supported.

        .. deprecated:: 4.1.4
            Not used by the urwid code base. Use :meth:`get_rows_sizes` instead.
            This API will be removed in version 6.0.
        """
        warnings.warn(
            "get_item_size is not used by the urwid code base and `get_rows_sizes` serves the same purpose. "
            "API will be removed in version 6.0.",
            PendingDeprecationWarning,
            stacklevel=2,
        )
        _w, (f, height) = self.contents[i]
        if f == WHSettings.PACK:
            if not size:
                return ()
            return (size[0],)

        if not size:
            raise PileError(f"Element {i} using parameters {f} and do not have full size information")

        maxcol = size[0]

        if f == WHSettings.GIVEN:
            return (maxcol, typing.cast("int", height))

        if f == WHSettings.WEIGHT:
            if len(size) == 2:
                if not item_rows:
                    item_rows = self.get_item_rows(size, focus)
                return (maxcol, item_rows[i])

            return (maxcol,)

        raise PileError(f"Unsupported item height rules: {f}")

    def _get_fixed_rows_sizes(
        self,
        focus: bool = False,
    ) -> tuple[tuple[int, ...], tuple[int, ...], tuple[tuple[int, int] | tuple[int] | tuple[()], ...]]:
        """Get rows widths, heights and render size parameters.

        Fixed case expect widget sizes calculation with several cycles for unknown height cases.

        :raises PileError: a child widget does not support a sizing mode this Pile needs, no child can provide a
            width, or the weights are too far apart to keep their ratio at a fixed size.
        """
        if not self.contents:
            return (), (), ()

        widths: dict[int, int] = {}
        heights: dict[int, int] = {}
        w_h_args: dict[int, tuple[int, int] | tuple[int] | tuple[()]] = {}

        flow: list[tuple[AbstractWidget, int, bool]] = []
        box: list[int] = []
        weighted: dict[int | float, list[int]] = {}
        weights: list[int | float] = []
        weight_max_sizes: dict[int | float, int] = {}

        for idx, (widget, (size_kind, size_weight)) in enumerate(self.contents):
            w_sizing = widget.sizing()
            focused = focus and self.focus == widget
            if size_kind == WHSettings.PACK:
                if Sizing.FIXED in w_sizing:
                    widths[idx], heights[idx] = typing.cast("AbstractFixedWidget", widget).pack((), focused)
                    w_h_args[idx] = ()
                if Sizing.FLOW in w_sizing:
                    # re-calculate height at the end
                    flow.append((widget, idx, focused))
                if not w_sizing & {Sizing.FIXED, Sizing.FLOW}:
                    raise PileError(f"Unsupported sizing {w_sizing} for {size_kind.upper()}")

            elif size_kind == WHSettings.GIVEN:
                heights[idx] = typing.cast("int", size_weight)
                if Sizing.BOX in w_sizing:
                    box.append(idx)
                else:
                    raise PileError(f"Unsupported sizing {w_sizing} for {size_kind.upper()}")

            elif typing.cast("int | float", size_weight) <= 0:
                widths[idx] = 0
                heights[idx] = 0
                if Sizing.FLOW in w_sizing:
                    w_h_args[idx] = (0,)
                else:
                    w_h_args[idx] = (0, 0)

            elif Sizing.FIXED in w_sizing and w_sizing & {Sizing.BOX, Sizing.FLOW}:
                width, height = typing.cast("AbstractFixedWidget", widget).pack((), focused)
                widths[idx] = width  # We're fitting everything in case of FIXED

                if Sizing.BOX in w_sizing:
                    size_weight = typing.cast("int | float", size_weight)
                    weighted.setdefault(size_weight, []).append(idx)
                    weights.append(size_weight)

                    weight_max_sizes.setdefault(size_weight, height)
                    weight_max_sizes[size_weight] = max(weight_max_sizes[size_weight], height)

                else:
                    # width replace is allowed
                    flow.append((widget, idx, focused))

            elif Sizing.FLOW in w_sizing:
                # FLOW WEIGHT widgets are rendered the same as PACK WEIGHT
                flow.append((widget, idx, focused))
            else:
                raise PileError(f"Unsupported combination of {size_kind}, {w_sizing}")

        if not widths:
            raise PileError("No widgets providing width information")

        max_width = max(widths.values())

        for widget, idx, focused in flow:
            widths[idx] = max_width
            heights[idx] = typing.cast("AbstractFlowWidget", widget).rows((max_width,), focused)
            w_h_args[idx] = (max_width,)

        if weight_max_sizes:
            max_weighted_coefficient = max(height / weight for weight, height in weight_max_sizes.items())

            for weight in weight_max_sizes:
                try:
                    height = max(int(max_weighted_coefficient * weight + 0.5), 1)
                except OverflowError as exc:
                    raise PileError(
                        f"Weights of {self!r} are too far apart to keep their ratio at a fixed size"
                    ) from exc
                for idx in weighted[weight]:
                    heights[idx] = height
                    w_h_args[idx] = (max_width, height)

        for idx in box:
            widths[idx] = max_width
            w_h_args[idx] = (max_width, heights[idx])

        return (
            tuple(widths[idx] for idx in range(len(widths))),
            tuple(heights[idx] for idx in range(len(heights))),
            tuple(w_h_args[idx] for idx in range(len(w_h_args))),
        )

    def _get_flow_rows_sizes(
        self,
        size: tuple[int],
        focus: bool = False,
    ) -> tuple[tuple[int, ...], tuple[int, ...], tuple[tuple[int, int] | tuple[int] | tuple[()], ...]]:
        """Get rows widths, heights and render size parameters.

        Flow case is the simplest one: minimum cycles in the logic and no widgets manipulation.
        Here we can make some shortcuts
        """
        maxcol = size[0]
        if not self.contents:
            return (maxcol,), (0,), ()

        widths: tuple[int, ...] = (maxcol,) * len(self.contents)
        heights: list[int] = []
        w_h_args: list[tuple[int, int] | tuple[int] | tuple[()]] = []
        focus_position = self.focus_position

        for i, (w, (f, height)) in enumerate(self.contents):
            if isinstance(w, AbstractWidget):
                w_sizing = w.sizing()
            else:
                warnings.warn(f"{w!r} is not a Widget", PileWarning, stacklevel=3)
                w_sizing = frozenset((Sizing.FLOW, Sizing.BOX))

            focused = focus and i == focus_position

            if f == WHSettings.GIVEN:
                height = typing.cast("int", height)
                heights.append(height)
                w_h_args.append((maxcol, height))
            elif Sizing.FLOW in w_sizing:
                heights.append(typing.cast("AbstractFlowWidget", w).rows((maxcol,), focus=focused))
                w_h_args.append((maxcol,))
            elif Sizing.FIXED in w_sizing and f == WHSettings.PACK:
                heights.append(typing.cast("AbstractFixedWidget", w).pack((), focused)[1])
                w_h_args.append(())
            else:
                warnings.warn(
                    f"Unusual widget {i} sizing {w_sizing} for {f.upper()}). "
                    f"Assuming wrong sizing and using {Sizing.FLOW.upper()} for height calculation",
                    PileWarning,
                    stacklevel=3,
                )
                heights.append(typing.cast("AbstractFlowWidget", w).rows((maxcol,), focus=focused))
                w_h_args.append((maxcol,))

        return (widths, tuple(heights), tuple(w_h_args))

    def get_rows_sizes(
        self,
        size: tuple[int, int] | tuple[int] | tuple[()],
        focus: bool = False,
    ) -> tuple[tuple[int, ...], tuple[int, ...], tuple[tuple[int, int] | tuple[int] | tuple[()], ...]]:
        """Get rows widths, heights and render size parameters.

        :raises PileError: *size* is ``()`` and the children cannot be sized as a fixed widget.
        """
        if not size:
            return self._get_fixed_rows_sizes(focus=focus)
        if len(size) == 1:
            return self._get_flow_rows_sizes(size, focus=focus)

        maxcol, maxrow = size
        if not self.contents:
            return (maxcol,), (maxrow,), ()

        remaining: int = maxrow

        widths: tuple[int, ...] = (maxcol,) * len(self.contents)
        heights: dict[int, int] = {}
        weighted: dict[int, int | float] = {}
        w_h_args: dict[int, tuple[int, int] | tuple[int] | tuple[()]] = {}
        focus_position = self.focus_position

        for i, (w, (f, height)) in enumerate(self.contents):
            if isinstance(w, AbstractWidget):
                w_sizing = w.sizing()
            else:
                warnings.warn(f"{w!r} is not a Widget", PileWarning, stacklevel=3)
                w_sizing = frozenset((Sizing.FLOW, Sizing.BOX))

            focused = focus and i == focus_position

            if f == WHSettings.GIVEN:
                height = typing.cast("int", height)
                heights[i] = height
                w_h_args[i] = (maxcol, height)
                remaining -= height
            elif f == WHSettings.PACK or len(size) == 1:
                if Sizing.FLOW in w_sizing:
                    w_h_arg: tuple[int] | tuple[()] = (maxcol,)
                elif Sizing.FIXED in w_sizing and f == WHSettings.PACK:
                    w_h_arg = ()
                else:
                    warnings.warn(
                        f"Unusual widget {i} sizing {w_sizing} for {f.upper()}). "
                        f"Assuming wrong sizing and using {Sizing.FLOW.upper()} for height calculation",
                        PileWarning,
                        stacklevel=3,
                    )
                    w_h_arg = (maxcol,)

                # widget kind and size arguments matched separately
                item_height = w.pack(w_h_arg, focused)[1]  # type: ignore[arg-type]
                heights[i] = item_height
                w_h_args[i] = w_h_arg
                remaining -= item_height
            elif height:
                weighted[i] = height
            else:
                heights[i] = 0
                w_h_args[i] = (maxcol, 0)  # zero-weighted items treated as ('given', 0)

        if remaining <= 0 and focus_position in weighted:
            # We need to hide some widgets: focused part will be not displayed by default
            # At this place we have to operate also negative "remaining" to be sure that widget really fit
            before = []
            after = []
            for idx, height in heights.items():
                if height:
                    if idx < focus_position:
                        before.append(idx)
                    else:
                        after.append(idx)

            # Try to move to the center
            offset = len(before) - len(after)
            if not offset:
                indexes: Iterable[int] = itertools.chain.from_iterable(zip(after[::-1], before))
            elif offset > 0:
                indexes = (
                    *before[:offset],
                    *itertools.chain.from_iterable(zip(after[::-1], before[offset:])),
                )
            else:
                indexes = (
                    *after[-1 : offset - 1 : -1],
                    *itertools.chain.from_iterable(zip(after[offset - 1 :: -1], before)),
                )

            for idx in indexes:
                height, heights[idx] = heights[idx], 0
                remaining += height
                if remaining > 0:
                    break

        remaining = max(remaining, 0)

        if weighted and (max_weight := max(weighted.values())) > sys.float_info.max / (
            len(weighted) * max(remaining, 1)
        ):
            # remaining * sum_weight could overflow a float: scale weights to at most 1, keeping their ratios
            weighted = {idx: weight / max_weight for idx, weight in weighted.items()}

        sum_weight = sum(weighted.values())

        for idx in sorted(weighted, key=lambda idx: (idx != focus_position, idx)):
            # We have to sort weighted items way that focused widget will gain render priority
            if remaining == 0:
                rows = 0
            else:
                rows = int(float(remaining) * weighted[idx] / sum_weight + 0.5)
                if not rows and idx == focus_position:
                    # Special case: we have to force as minimum 1 row for focused widget to prevent broken behaviour
                    rows = 1
                remaining -= rows
                sum_weight -= weighted[idx]

            heights[idx] = rows
            w_h_args[idx] = (maxcol, rows)

        return (
            widths,
            tuple(heights[idx] for idx in range(len(heights))),
            tuple(w_h_args[idx] for idx in range(len(w_h_args))),
        )

    def pack(self, size: tuple[()] | tuple[int] | tuple[int, int] = (), focus: bool = False) -> tuple[int, int]:
        """Get packed size for widget.

        :raises PileError: *size* is ``()`` and the children cannot be sized as a fixed widget.
        """
        if size:
            return super().pack(size, focus)
        widths, heights, _ = self.get_rows_sizes(size, focus)
        return (max(widths), sum(heights))

    def get_item_rows(self, size: tuple[int] | tuple[int, int], focus: bool) -> list[int]:
        """Return the number of rows used by each widget in self.contents.

        This method is a normally used only by `get_item_size` for the BOX case..
        """
        return list(self.get_rows_sizes(size, focus)[1])

    def render(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
        focus: bool = False,
    ) -> SolidCanvas | CompositeCanvas:
        """
        Render the Pile and return the resulting canvas.

        :raises ValueError: the Pile is empty and no *size* was given.
        :raises PileError: *size* is ``()`` and the children cannot be sized as a fixed widget.
        """
        _widths, heights, size_args = self.get_rows_sizes(size, focus)

        combinelist = []
        for i, (height, w_size, (w, _)) in enumerate(zip(heights, size_args, self.contents)):
            item_focus = self.focus == w
            canv = None
            if height > 0:
                # widget kind and size arguments matched separately
                canv = w.render(w_size, focus=focus and item_focus)  # type: ignore[arg-type]

            if canv:
                combinelist.append((canv, i, item_focus))

        if not combinelist:
            if not size:
                raise ValueError("size must be provided when no contents are present")

            return SolidCanvas(" ", size[0], (*size[1:], 0)[0])

        out = CanvasCombine(combinelist)
        if len(size) == 2 and size[1] != out.rows():
            # flow/fixed widgets rendered too large/small
            out = CompositeCanvas(out)
            out.pad_trim_top_bottom(0, size[1] - out.rows())
        return out

    def get_cursor_coords(self, size: tuple[()] | tuple[int] | tuple[int, int]) -> tuple[int, int] | None:
        """Return the cursor coordinates of the focus widget.

        :raises PileError: *size* is ``()`` and the children cannot be sized as a fixed widget.
        """
        if not self.selectable():
            return None

        if (get_cursor_coords := getattr(self.focus, "get_cursor_coords", None)) is not None:
            i = self.focus_position
            _widths, heights, size_args = self.get_rows_sizes(size, focus=True)

            if (coords := get_cursor_coords(size_args[i])) is not None:  # pylint: disable=not-callable
                x, y = coords
                if i > 0:
                    y += sum(heights[:i])

                return x, y

        return None

    def rows(self, size: tuple[int], focus: bool = False) -> int:
        """Return the total number of rows occupied by all of the pile's widgets."""
        return sum(self.get_rows_sizes(size, focus)[1])

    def keypress(self, size: tuple[()] | tuple[int] | tuple[int, int], key: str) -> str | None:
        """Pass the keypress to the widget in focus.

        Unhandled :kbd:`up` and :kbd:`down` keys may cause a focus change.

        :raises PileError: *size* is ``()`` and the children cannot be sized as a fixed widget.
        """
        if not self.contents:
            return key

        i = self.focus_position
        _widths, heights, size_args = self.get_rows_sizes(size, focus=self.selectable())
        if self.selectable():
            if (processed := self.focus.keypress(size_args[i], key)) is not None:  # type: ignore[union-attr]
                key = processed
            else:
                return None
            if self._command_map[key] not in {Command.UP, Command.DOWN} or not self.contents:
                return key
            # The child may have changed this Pile's contents or focus while handling the key.
            i = self.focus_position
            _widths, heights, size_args = self.get_rows_sizes(size, focus=True)

        if self._command_map[key] == Command.UP:
            candidates = tuple(range(i - 1, -1, -1))  # count backwards to 0
        else:  # self._command_map[key] == 'cursor down'
            candidates = tuple(range(i + 1, len(self.contents)))

        for j in candidates:
            w = self.contents[j][0]
            if not w.selectable():
                continue

            self._update_pref_col_from_focus(size_args[self.focus_position])
            self.focus_position = j
            if not hasattr(w, "move_cursor_to_coords"):
                return None

            rows = heights[j]
            if self._command_map[key] == Command.UP:
                rowlist = tuple(range(rows - 1, -1, -1))
            else:  # self._command_map[key] == 'cursor down'
                rowlist = tuple(range(rows))
            for row in rowlist:
                if w.move_cursor_to_coords(size_args[j], self.pref_col, row):
                    break
            return None

        # nothing to select
        return key

    def _update_pref_col_from_focus(self, w_size: tuple[()] | tuple[int] | tuple[int, int]) -> None:
        """Update self.pref_col from the focus widget."""
        if (get_pref_col := getattr(self.focus, "get_pref_col", None)) is not None:  # noqa: SIM102
            if (pref_col := get_pref_col(w_size)) is not None:  # pylint: disable=not-callable
                self.pref_col = pref_col

    def move_cursor_to_coords(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
        col: int,
        row: int,
    ) -> bool:
        """Capture pref col and set new focus.

        :raises PileError: *size* is ``()`` and the children cannot be sized as a fixed widget.
        """
        self.pref_col = col

        # FIXME guessing focus==True
        focus = True
        wrow = 0
        _widths, heights, size_args = self.get_rows_sizes(size, focus=focus)
        for i, (r, w_size, (w, _)) in enumerate(zip(heights, size_args, self.contents)):  # noqa: B007
            if wrow + r > row:
                break
            wrow += r
        else:
            return False

        if not w.selectable():
            return False

        if hasattr(w, "move_cursor_to_coords") and w.move_cursor_to_coords(w_size, col, row - wrow) is False:
            return False

        # The child may have changed this Pile's contents while moving its cursor.
        if i >= len(self.contents) or self.contents[i][0] is not w:
            return False

        self.focus_position = i
        return True

    def mouse_event(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
        event: str,
        button: int,
        col: int,
        row: int,
        focus: bool,
    ) -> bool | None:
        """Pass the event to the contained widget.

        May change focus on button 1 press.

        :raises PileError: *size* is ``()`` and the children cannot be sized as a fixed widget.
        """
        wrow = 0
        _widths, heights, size_args = self.get_rows_sizes(size, focus=focus)

        for i, (height, w_size, (w, _)) in enumerate(zip(heights, size_args, self.contents)):  # noqa: B007
            if wrow + height > row:
                target_row = row - wrow
                break
            wrow += height
        else:
            return False

        if is_mouse_press(event) and button == 1 and w.selectable():
            self.focus_position = i

        if not hasattr(w, "mouse_event"):
            warnings.warn(
                f"{w.__class__.__module__}.{w.__class__.__name__} is not implementing Widget API",
                DeprecationWarning,
                stacklevel=2,
            )
            return False

        # widget kind and size arguments matched separately
        return w.mouse_event(
            w_size,  # type: ignore[arg-type]
            event,
            button,
            col,
            target_row,
            focus and self.focus == w,
        )
urwid-urwid-69907e4/urwid/widget/popup.py000066400000000000000000000257261526073734300204450ustar00rootroot00000000000000# Urwid Window-Icon-Menu-Pointer-style widget classes
#    Copyright (C) 2004-2011  Ian Ward
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/


"""Pop-up support: a launcher widget and the target that displays its pop-up."""

from __future__ import annotations

import typing

from urwid.canvas import CompositeCanvas

from .constants import Align, Sizing, VAlign
from .overlay import Overlay
from .widget import delegate_to_widget_mixin
from .widget_decoration import WidgetDecoration

if typing.TYPE_CHECKING:
    from typing_extensions import TypedDict

    from urwid.canvas import Canvas

    from .widget import AbstractBoxWidget, AbstractWidget

    class PopUpParametersModel(TypedDict):
        """Position and box size a :class:`PopUpLauncher` requests for its pop-up."""

        left: int
        top: int
        overlay_width: int
        overlay_height: int


WrappedWidget = typing.TypeVar("WrappedWidget", bound="AbstractBoxWidget")


class PopUpLauncher(
    delegate_to_widget_mixin("_original_widget"),  # type: ignore[misc]
    WidgetDecoration[WrappedWidget],
):
    """Decoration that lets a wrapped widget open and close a pop-up widget above it."""

    def __init__(self, original_widget: WrappedWidget) -> None:
        """Wrap `original_widget`, with no pop-up open yet."""
        super().__init__(original_widget)
        self._pop_up_widget: AbstractWidget | None = None

    def create_pop_up(self) -> AbstractWidget:
        """Return the widget to be used for the pop-up.

        Subclass must override this method. It is called once each time
        the pop-up is opened.

        :class:`PopUpTarget` renders the pop-up with the box size declared by
        :meth:`get_pop_up_parameters`, so the widget returned here must accept an
        (*overlay_width*, *overlay_height*) size.  Wrap a flow or fixed widget in a
        :class:`Filler ` (or in another box container) before returning it.

        :raises NotImplementedError: the subclass does not override this method.
        """
        raise NotImplementedError("Subclass must override this method")

    def get_pop_up_parameters(self) -> PopUpParametersModel:
        """Return a dict of pop-up parameters, eg: {'left':0, 'top':1, 'overlay_width':30, 'overlay_height':4}.

        Subclass must override this method. It is called each time this widget is rendered.

        :raises NotImplementedError: the subclass does not override this method.
        """
        raise NotImplementedError("Subclass must override this method")

    def open_pop_up(self) -> None:
        """Create the pop-up widget via :meth:`create_pop_up` and invalidate this widget."""
        self._pop_up_widget = self.create_pop_up()
        self._invalidate()

    def close_pop_up(self) -> None:
        """Discard the currently open pop-up widget, if any, and invalidate this widget."""
        self._pop_up_widget = None
        self._invalidate()

    def render(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
        focus: bool = False,
    ) -> CompositeCanvas | Canvas:
        """Render the wrapped widget, attaching the open pop-up widget to the canvas, if any."""
        canv = super().render(size, focus)
        if self._pop_up_widget:
            canv = CompositeCanvas(canv)
            canv.set_pop_up(self._pop_up_widget, **self.get_pop_up_parameters())
        return canv


class PopUpTarget(WidgetDecoration[WrappedWidget]):
    """Box widget that overlays the pop-up(s) declared by a :class:`PopUpLauncher` inside it.

    Pop-ups nest: a pop-up widget may itself be a :class:`PopUpLauncher` with its own pop-up
    open, in which case it is drawn overlaid on top of the pop-up that opened it, and so on for
    as many levels as are open at once.
    """

    # FIXME: this whole class is a terrible hack and must be fixed when layout and rendering are separated
    _sizing = frozenset((Sizing.BOX,))
    _selectable = True

    def __init__(self, original_widget: WrappedWidget) -> None:
        """Wrap *original_widget*, which is rendered with any open pop-up(s) overlaid on top.

        :param original_widget: box widget to wrap; typically contains a :class:`PopUpLauncher`.
        """
        super().__init__(original_widget)
        # One (widget, Overlay) entry per currently open pop-up, outermost first.
        self._pop_up_levels: list[tuple[AbstractWidget, Overlay[AbstractWidget, AbstractWidget]]] = []
        self._current_widget: AbstractWidget = self._original_widget
        self._cache_original_canvas: Canvas | None = None

    def _update_overlay(self, size: tuple[int, int], focus: bool) -> None:
        """Rebuild :attr:`_current_widget` as a chain of Overlay widgets, one per open pop-up.

        Level 0 wraps :attr:`_original_widget`'s own pop-up, if any; level 1 wraps a further
        pop-up declared by level 0's pop-up widget, if that widget is itself a
        :class:`PopUpLauncher` with its own pop-up open; and so on. Each level's pop-up widget
        is rendered directly to discover whether it declares a further nested pop-up, rather
        than inspecting the wrapping Overlay's merged canvas, whose "pop up" coordinate can go
        stale once nothing re-declares it.

        :param size: box size to render :attr:`_original_widget` and every pop-up level at.
        :param focus: whether this widget is in focus.
        """
        canv = self._original_widget.render(size, focus=focus)
        self._cache_original_canvas = canv  # imperfect performance hack

        old_levels = self._pop_up_levels
        new_levels: list[tuple[AbstractWidget, Overlay[AbstractWidget, AbstractWidget]]] = []
        bottom_widget: AbstractWidget = self._original_widget
        # Once a level is rebuilt, every deeper level must be rebuilt too: a reused Overlay's
        # bottom_w still points at the previous (now discarded) widget at the shallower level.
        rebuilding = False

        pop_up = canv.get_pop_up()
        while pop_up:
            left, top, (w, overlay_width, overlay_height) = pop_up
            level = len(new_levels)
            if not rebuilding and level < len(old_levels) and old_levels[level][0] == w:
                overlay = old_levels[level][1]
                overlay.set_overlay_parameters(
                    align=Align.LEFT,
                    width=overlay_width,
                    valign=VAlign.TOP,
                    height=overlay_height,
                    left=left,
                    top=top,
                )
            else:
                rebuilding = True
                overlay = Overlay(
                    top_w=w,
                    bottom_w=bottom_widget,
                    align=Align.LEFT,
                    width=overlay_width,
                    valign=VAlign.TOP,
                    height=overlay_height,
                    left=left,
                    top=top,
                )
            new_levels.append((w, overlay))
            bottom_widget = overlay

            # Check w itself for a further nested pop-up; see the docstring for why.
            pop_up = w.render((overlay_width, overlay_height), focus=focus).get_pop_up()

        self._pop_up_levels = new_levels
        self._current_widget = bottom_widget

    def render(
        self,
        size: tuple[int, int],  # type: ignore[override]
        focus: bool = False,
    ) -> Canvas:
        """Rebuild the pop-up overlay chain for *size* and render the resulting widget."""
        self._update_overlay(size, focus)
        return self._current_widget.render(size, focus=focus)

    def get_cursor_coords(self, size: tuple[int, int]) -> tuple[int, int] | None:
        """
        Return the cursor coordinates of the current widget.

        :raises TypeError: the current widget has no ``get_cursor_coords`` method.
        """
        self._update_overlay(size, True)

        if not hasattr(self._current_widget, "get_cursor_coords"):
            raise TypeError(f"widget {type(self._current_widget)} has no get_cursor_coords method")

        return self._current_widget.get_cursor_coords(size)  # type: ignore[no-any-return]

    def get_pref_col(self, size: tuple[int, int]) -> int:
        """
        Return the preferred cursor column of the current widget.

        :raises TypeError: the current widget has no ``get_pref_col`` method.
        """
        self._update_overlay(size, True)

        if not hasattr(self._current_widget, "get_pref_col"):
            raise TypeError(f"widget {type(self._current_widget)} has no get_pref_col method")

        return typing.cast("int", self._current_widget.get_pref_col(size))

    def keypress(
        self,
        size: tuple[int, int],  # type: ignore[override]
        key: str,
    ) -> str | None:
        """Rebuild the pop-up overlay chain for *size* and forward the keypress to it."""
        self._update_overlay(size, True)
        return self._current_widget.keypress(size, key)

    def move_cursor_to_coords(self, size: tuple[int, int], x: int, y: int) -> bool:
        """
        Move the cursor of the current widget to ``(x, y)``.

        :raises TypeError: the current widget has no ``move_cursor_to_coords`` method.
        """
        self._update_overlay(size, True)

        if not hasattr(self._current_widget, "move_cursor_to_coords"):
            raise TypeError(f"widget {type(self._current_widget)} has no move_cursor_to_coords method")

        return typing.cast("bool", self._current_widget.move_cursor_to_coords(size, x, y))

    def mouse_event(
        self,
        size: tuple[int, int],  # type: ignore[override]
        event: str,
        button: int,
        col: int,
        row: int,
        focus: bool,
    ) -> bool | None:
        """Rebuild the pop-up overlay chain for *size* and forward the mouse event to it."""
        self._update_overlay(size, focus)
        return self._current_widget.mouse_event(size, event, button, col, row, focus)

    def pack(  # type: ignore[override]
        self,
        size: tuple[int, int] | tuple[()] = (),
        focus: bool = False,
    ) -> tuple[int, int]:
        """Rebuild the pop-up overlay chain for *size* and return the current widget's packed size."""
        self._update_overlay(size, focus)  # type: ignore[arg-type]
        return self._current_widget.pack(size)


def _test() -> None:
    import doctest

    doctest.testmod()


if __name__ == "__main__":
    _test()
urwid-urwid-69907e4/urwid/widget/progress_bar.py000066400000000000000000000147161526073734300217670ustar00rootroot00000000000000"""ProgressBar: a flow widget that displays a completion percentage."""

from __future__ import annotations

import math
import typing

from .constants import BAR_SYMBOLS, Align, Sizing, WrapMode
from .text import Text
from .widget import Widget, nocache_widget_render_instance

if typing.TYPE_CHECKING:
    from collections.abc import Callable, Hashable

    from urwid.canvas import TextCanvas


class ProgressBar(Widget):
    """Flow widget that displays a completion percentage as a bar of Unicode block characters."""

    _sizing = frozenset([Sizing.FLOW])

    eighths = BAR_SYMBOLS.HORISONTAL[:8]  # Full width line is made by style

    text_align = Align.CENTER

    def __init__(
        self,
        normal: Hashable,
        complete: Hashable,
        current: int = 0,
        done: int = 100,
        satt: Hashable = None,
    ) -> None:
        """Build the progress bar with the given display attributes and range.

        :param normal: display attribute for incomplete part of progress bar
        :param complete: display attribute for complete part of progress bar
        :param current: current progress
        :param done: progress amount at 100%
        :param satt: display attribute for smoothed part of bar where the
                     foreground of satt corresponds to the normal part and the
                     background corresponds to the complete part.
                     If satt is ``None`` then no smoothing will be done.

        >>> from urwid import LineBox
        >>> pb = ProgressBar("a", "b")
        >>> pb
        
        >>> print(pb.get_text())
        0 %
        >>> pb.set_completion(34.42)
        >>> print(pb.get_text())
        34 %
        >>> class CustomProgressBar(ProgressBar):
        ...     def get_text(self):
        ...         return "Foobar"
        >>> cpb = CustomProgressBar("a", "b")
        >>> print(cpb.get_text())
        Foobar
        >>> for x in range(101):
        ...     cpb.set_completion(x)
        ...     s = cpb.render((10,))
        >>> cpb2 = CustomProgressBar("a", "b", satt="c")
        >>> for x in range(101):
        ...     cpb2.set_completion(x)
        ...     s = cpb2.render((10,))
        >>> pb = ProgressBar("a", "b", satt="c")
        >>> pb.set_completion(34.56)
        >>> print(LineBox(pb).render((20,)))
        ┌──────────────────�
        │      �34 %       │
        └──────────────────┘
        """
        super().__init__()
        self.normal = normal
        self.complete = complete
        self._current = current
        self._done = done
        self.satt = satt
        # The label widget is built on first render rather than here, so that an unsupported
        # `text_align` keeps raising from `render` as it did when the widget was built per call.
        self._label: Text | None = None
        self._render_label: Callable[..., TextCanvas] | None = None

    def set_completion(self, current: int) -> None:
        """:param current: current progress"""
        self._current = current
        self._invalidate()

    current = property(lambda self: self._current, set_completion)

    @property
    def done(self) -> int:
        """Return the progress amount that represents 100%."""
        return self._done

    @done.setter
    def done(self, done: int) -> None:
        """:param done: progress amount at 100%"""
        self._done = done
        self._invalidate()

    def rows(self, size: tuple[int], focus: bool = False) -> int:
        """Return the number of rows the progress bar occupies, always 1."""
        return 1

    def _scaled_completion(self, scale: int) -> float:
        """Return the completed fraction of the bar multiplied by ``scale``, clamped to ``[0, scale]``.

        A ``done`` that is not positive, or a NaN result, counts as nothing completed.
        When ``current`` or ``done`` is too large for a float, the bar is complete if ``current`` exceeds ``done``.
        """
        completed: float = 0
        if self.done > 0:
            try:
                completed = self.current * scale / self.done
            except OverflowError:
                completed = scale if self.current > self.done else 0
        if math.isnan(completed):
            return 0
        return min(scale, max(0, completed))

    def get_text(self) -> str:
        """Return the progress bar percentage text.

        You can override this method to display custom text.
        """
        percent = int(self._scaled_completion(100))
        return f"{percent!s} %"

    def render(
        self,
        size: tuple[int],  # type: ignore[override]
        focus: bool = False,
    ) -> TextCanvas:
        """Render the progress bar."""
        # pylint: disable=protected-access
        (maxcol,) = size
        label, render_label = self._label, self._render_label
        if label is None or render_label is None:
            label = self._label = Text("", self.text_align, WrapMode.CLIP)
            # The label canvas is thrown away once its attributes are rewritten below, so it is
            # rendered outside `CanvasCache`: caching it only costs a weakref, a store and a cleanup.
            render_label = self._render_label = typing.cast(
                "Callable[..., TextCanvas]",
                nocache_widget_render_instance(label),
            )
        label.set_text(self.get_text())
        if label.align != self.text_align:
            label.set_align_mode(self.text_align)
        c = render_label((maxcol,))

        cf = self._scaled_completion(maxcol)
        ccol_dirty = int(cf)
        ccol = len(c._text[0][:ccol_dirty].decode("utf-8", "ignore").encode("utf-8"))
        cs = 0
        if self.satt is not None:
            cs = int((cf - ccol) * 8)
        if ccol == cs == 0:
            c._attr = [[(self.normal, maxcol)]]
        elif ccol >= maxcol:
            c._attr = [[(self.complete, maxcol)]]
        elif cs and c._text[0][ccol] == 32:
            t = c._text[0]
            cenc = self.eighths[cs].encode("utf-8")
            c._text[0] = t[:ccol] + cenc + t[ccol + 1 :]
            a = []
            if ccol > 0:
                a.append((self.complete, ccol))
            a.append((self.satt, len(cenc)))
            if maxcol - ccol - 1 > 0:
                a.append((self.normal, maxcol - ccol - 1))
            c._attr = [a]
            c._cs = [[(None, len(c._text[0]))]]
        else:
            c._attr = [[(self.complete, ccol), (self.normal, maxcol - ccol)]]
        return c
urwid-urwid-69907e4/urwid/widget/scrollable.py000066400000000000000000000733431526073734300214220ustar00rootroot00000000000000# Copyright (C) 2024 Urwid developers
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/
#
# Copyright (C) 2017-2024 rndusr (https://github.com/rndusr)
# Re-licensed from gpl-3.0 with author permission.
# Permission comment link: https://github.com/markqvist/NomadNet/pull/46#issuecomment-1892712616

"""Scrollable and ScrollBar: decorations that scroll a box widget."""

from __future__ import annotations

import contextlib
import dataclasses
import enum
import typing

from .constants import BOX_SYMBOLS, SHADE_SYMBOLS, Sizing
from .widget import AbstractBoxWidget, AbstractWidget
from .widget_decoration import WidgetDecoration, WidgetError

if typing.TYPE_CHECKING:
    from collections.abc import Iterator

    from typing_extensions import Literal

    from urwid import Canvas, CompositeCanvas

    from .container import WidgetContainerListContentsMixin
    from .widget import AbstractFixedWidget, AbstractFlowWidget


__all__ = (
    "ScrollBar",
    "Scrollable",
    "ScrollableError",
    "ScrollbarSymbols",
    "SupportsRelativeScroll",
    "SupportsScroll",
)


WrappedScrollableWidget = typing.TypeVar("WrappedScrollableWidget", bound="SupportsScroll")
WrappedScrollWidget = typing.TypeVar("WrappedScrollWidget", bound="AbstractFlowWidget | AbstractFixedWidget")


class ScrollableError(WidgetError):
    """Scrollable specific widget errors."""


# 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 ScrollbarSymbols(str, enum.Enum):
    """Common symbols suitable for scrollbar."""

    FULL_BLOCK = SHADE_SYMBOLS.FULL_BLOCK
    DARK_SHADE = SHADE_SYMBOLS.DARK_SHADE
    MEDIUM_SHADE = SHADE_SYMBOLS.MEDIUM_SHADE
    LITE_SHADE = SHADE_SYMBOLS.LITE_SHADE

    DRAWING_LIGHT = BOX_SYMBOLS.LIGHT.VERTICAL
    DRAWING_LIGHT_2_DASH = BOX_SYMBOLS.LIGHT.VERTICAL_2_DASH
    DRAWING_LIGHT_3_DASH = BOX_SYMBOLS.LIGHT.VERTICAL_3_DASH
    DRAWING_LIGHT_4_DASH = BOX_SYMBOLS.LIGHT.VERTICAL_4_DASH

    DRAWING_HEAVY = BOX_SYMBOLS.HEAVY.VERTICAL
    DRAWING_HEAVY_2_DASH = BOX_SYMBOLS.HEAVY.VERTICAL_2_DASH
    DRAWING_HEAVY_3_DASH = BOX_SYMBOLS.HEAVY.VERTICAL_3_DASH
    DRAWING_HEAVY_4_DASH = BOX_SYMBOLS.HEAVY.VERTICAL_4_DASH

    DRAWING_DOUBLE = BOX_SYMBOLS.DOUBLE.VERTICAL


@typing.runtime_checkable
class SupportsScroll(AbstractBoxWidget, typing.Protocol):
    """Scroll specific methods."""

    def get_scrollpos(self, size: tuple[int, int], focus: bool = False) -> int:
        """Return the index of the first visible row."""

    def rows_max(self, size: tuple[int, int] | None = None, focus: bool = False) -> int:
        """Return the total number of rows the widget can render."""


@typing.runtime_checkable
class SupportsRelativeScroll(AbstractBoxWidget, typing.Protocol):
    """Relative scroll-specific methods."""

    def require_relative_scroll(self, size: tuple[int, int], focus: bool = False) -> bool:
        """Return whether the widget should be scrolled using relative position rather than absolute rows."""

    def get_first_visible_pos(self, size: tuple[int, int], focus: bool = False) -> int:
        """Return the index of the first visible item."""

    def get_visible_amount(self, size: tuple[int, int], focus: bool = False) -> int:
        """Return the number of items currently visible."""


def orig_iter(w: AbstractWidget) -> Iterator[AbstractWidget]:
    """Yield `w` and each of its `original_widget` ancestors, stopping at the first cycle."""
    visited = {w}
    yield w
    while (w := getattr(w, "original_widget", w)) not in visited:
        visited.add(w)
        yield w


class Scrollable(WidgetDecoration[WrappedScrollWidget]):
    """Box widget decoration that makes a fixed or flow widget vertically scrollable."""

    def sizing(self) -> frozenset[Sizing]:
        """Return that this widget is always a box widget."""
        return frozenset((Sizing.BOX,))

    def selectable(self) -> bool:
        """Return ``True``: a scrollable widget always accepts the input focus."""
        return True

    def __init__(self, widget: WrappedScrollWidget, force_forward_keypress: bool = False) -> None:
        """Box widget that makes a fixed or flow widget vertically scrollable.

        .. note::
            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 ouside 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.

        :raises ValueError: *widget* is neither a FIXED nor a FLOW widget.
        """
        if not widget.sizing() & frozenset((Sizing.FIXED, Sizing.FLOW)):
            raise ValueError(f"Not a fixed or flow widget: {widget!r}")

        self._trim_top = 0
        self._scroll_action: str | None = None
        self._forward_keypress: bool | None = None
        self._old_cursor_coords: tuple[int, int] | None = None
        self._rows_max_cached = 0
        self.force_forward_keypress = force_forward_keypress
        super().__init__(widget)

    def render(
        self,
        size: tuple[int, int],  # type: ignore[override]
        focus: bool = False,
    ) -> CompositeCanvas:
        """Render the wrapped widget, trimmed to the current scroll position."""
        from urwid import canvas

        maxcol, maxrow = size

        def automove_cursor() -> None:
            ch = 0
            last_hidden = False
            first_visible = False
            for pwi, (w, _o) in enumerate(
                typing.cast(
                    "WidgetContainerListContentsMixin[tuple[AbstractWidget, typing.Any]]",
                    ow,
                ).contents
            ):
                wcanv = w.render((maxcol,))

                if wh := wcanv.rows():
                    ch += wh

                if not last_hidden and ch >= self._trim_top:
                    last_hidden = True

                elif last_hidden:
                    if not first_visible:
                        first_visible = True

                    if not w.selectable():
                        continue

                    typing.cast(
                        "WidgetContainerListContentsMixin[tuple[AbstractWidget, typing.Any]]",
                        ow,
                    ).focus_position = pwi

                    st = None
                    nf = ow.focus
                    if hasattr(nf, "key_timeout"):
                        st = nf
                    elif (no := getattr(nf, "original_widget", None)) is not None:
                        if hasattr(no, "original_widget"):
                            st = no.original_widget
                        elif hasattr(no, "key_timeout"):
                            st = no

                    if st and hasattr(st, "key_timeout") and callable(getattr(st, "keypress", None)):
                        st.keypress(None, None)  # type: ignore[arg-type]  # Do not break legacy API

                    break

        # Render complete original widget
        ow = self._original_widget
        ow_size = self._get_original_widget_size(size)
        canv_full = ow.render(ow_size, focus)  # type: ignore[arg-type]

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

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

        if canv_rows <= maxrow and (fill_height := maxrow - canv_rows) > 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:
            # Pylint check acts here a bit weird.
            _curscol, cursrow = canv.cursor  # pylint: disable=unpacking-non-sequence,useless-suppression
            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
        elif canv_full.cursor is not None:
            # Full canvas contains the cursor, but scrolled out of view
            self._forward_keypress = False

            # Reset cursor position on page/up down scrolling
            if getattr(ow, "automove_cursor_on_scroll", False):
                with contextlib.suppress(Exception):
                    automove_cursor()

        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.
            self._forward_keypress = ow.selectable()

        return canv

    def keypress(
        self,
        size: tuple[int, int],  # type: ignore[override]
        key: str,
    ) -> str | None:
        """Handle scrolling keys, forwarding everything else to the wrapped widget when appropriate."""
        from urwid.command_map import Command

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

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

            if (handled := ow.keypress(ow_size, key)) is not None:  # type: ignore[arg-type]
                key = handled
            else:
                return None

        # Handle up/down, page up/down, etc.
        command_map = self._command_map
        if command_map[key] == Command.UP:
            self._scroll_action = SCROLL_LINE_UP
        elif command_map[key] == Command.DOWN:
            self._scroll_action = SCROLL_LINE_DOWN

        elif command_map[key] == Command.PAGE_UP:
            self._scroll_action = SCROLL_PAGE_UP
        elif command_map[key] == Command.PAGE_DOWN:
            self._scroll_action = SCROLL_PAGE_DOWN

        elif command_map[key] == Command.MAX_LEFT:  # 'home'
            self._scroll_action = SCROLL_TO_TOP
        elif command_map[key] == Command.MAX_RIGHT:  # 'end'
            self._scroll_action = SCROLL_TO_END

        else:
            return key

        self._invalidate()
        return None

    def mouse_event(
        self,
        size: tuple[int, int],  # type: ignore[override]
        event: str,
        button: int,
        col: int,
        row: int,
        focus: bool,
    ) -> bool | None:
        """Forward a mouse event to the wrapped widget, adjusting the row for the current scroll position."""
        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,  # type: ignore[arg-type]
                event,
                button,
                col,
                row,
                focus,
            )

        return False

    def _adjust_trim_top(self, canv: Canvas, size: tuple[int, int]) -> None:
        """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

        if action == SCROLL_LINE_UP:
            self._trim_top = max(0, min(canv_rows - maxrow, trim_top - 1))
        elif action == SCROLL_LINE_DOWN:
            self._trim_top = max(0, min(canv_rows - maxrow, trim_top + 1))

        elif action == SCROLL_PAGE_UP:
            self._trim_top = max(0, min(canv_rows - maxrow, trim_top - maxrow + 1))
        elif action == SCROLL_PAGE_DOWN:
            self._trim_top = max(0, min(canv_rows - maxrow, 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 = max(0, min(canv_rows - maxrow, 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 and canv.cursor is not None:
            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: tuple[int, int],
    ) -> tuple[int] | tuple[()]:
        """
        Return the size to render the wrapped widget with.

        :raises ScrollableError: the wrapped widget supports neither FLOW nor FIXED sizing.
        """
        ow = self._original_widget
        sizing = ow.sizing()
        if Sizing.FLOW in sizing:
            return (size[0],)
        if Sizing.FIXED in sizing:
            return ()
        raise ScrollableError(f"{ow!r} sizing is not supported")

    def get_scrollpos(self, size: tuple[int, int] | None = None, focus: bool = False) -> int:
        """Return the 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: typing.SupportsInt) -> None:
        """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: tuple[int, int] | None = None, focus: bool = False) -> int:
        """Return the number of rows for `size`.

        If `size` is not given, the currently rendered number of rows is returned.

        :raises ScrollableError: the wrapped widget is neither a FLOW nor a FIXED widget.
        """
        if size is not None:
            ow = self._original_widget
            ow_size = self._get_original_widget_size(size)
            sizing = ow.sizing()
            if Sizing.FIXED in sizing:
                self._rows_max_cached = ow.pack(ow_size, focus)[1]  # type: ignore[arg-type]
            elif Sizing.FLOW in sizing:
                self._rows_max_cached = typing.cast("AbstractFlowWidget", ow).rows(
                    typing.cast("tuple[int]", ow_size),
                    focus,
                )
            else:
                raise ScrollableError(f"Not a flow/fixed widget: {self._original_widget!r}")
        return self._rows_max_cached


@dataclasses.dataclass(frozen=True)
class _ScrollbarLayout:
    """Geometry of a scrollbar rendered next to its wrapped widget.

    Produced by :meth:`ScrollBar._scrollbar_layout` and shared between rendering and mouse handling
    so the scrollbar hit-box and the thumb placement always stay in sync.
    """

    ow_size: tuple[int, int]
    sb_width: int
    pos: int
    posmax: int
    thumb_height: int
    top_height: int
    bottom_height: int


class ScrollBar(WidgetDecoration[WrappedScrollableWidget]):
    """Box widget decoration that adds a scrollbar alongside a scrollable wrapped widget."""

    Symbols = ScrollbarSymbols

    def sizing(self) -> frozenset[Sizing]:
        """Return that this widget is always a box widget."""
        return frozenset((Sizing.BOX,))

    def selectable(self) -> bool:
        """Return ``True``: a scrollbar-wrapped widget always accepts the input focus."""
        return True

    def __init__(
        self,
        widget: WrappedScrollableWidget,
        thumb_char: str = ScrollbarSymbols.FULL_BLOCK,
        trough_char: str = " ",
        side: Literal["left", "right"] = SCROLLBAR_RIGHT,  # type: ignore[assignment]  # constant
        width: int = 1,
    ) -> None:
        """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.

        :raises ValueError: *widget* is not a BOX widget.
        :raises TypeError: *widget* does not wrap anything that supports the scrolling protocol.
        """
        if Sizing.BOX not in widget.sizing():
            raise ValueError(f"Not a box widget: {widget!r}")

        if not any(isinstance(w, SupportsScroll) for w in orig_iter(widget)):
            raise TypeError(f"Not a scrollable widget: {widget!r}")

        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_original(
        self,
        size: tuple[int, int],
        focus: bool = False,
    ) -> Canvas:
        self._original_widget_size = size
        return self._original_widget.render(size, focus)

    def _scrollbar_layout(
        self,
        size: tuple[int, int],
        focus: bool = False,
    ) -> _ScrollbarLayout | None:
        """Compute the scrollbar geometry for ``size``.

        :returns: ``None`` is returned as a "not shown" flag when the wrapped widget fits without scrolling
            and therefore no scrollbar is drawn.
            Otherwise, the thumb and trough placement is returned,
            so :meth:`render` and :meth:`mouse_event` can share the same geometry without duplicating the maths.
        :raises TypeError: the wrapped widget does not support the scrolling protocol.
        """
        maxcol, maxrow = size

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

        ow_base = self.scrolling_base_widget

        # Use hasattr instead of protocol: hasattr will return False in case of getattr raise AttributeError
        # Use __length_hint__ first since it's less resource intensive
        require_absolute = True

        if (
            isinstance(ow_base, SupportsRelativeScroll)
            and callable(len_getter := getattr(ow_base, "__len__", getattr(ow_base, "__length_hint__", None)))
            and ow_base.require_relative_scroll(size, focus)
        ):
            # `operator.length_hint` is Protocol (Spec) over class base and can end false-negative on the instance
            # use length_hint-like approach with safe `AttributeError` handling
            ow_len = len_getter()
            visible_amount = ow_base.get_visible_amount(ow_size, focus)
            pos = ow_base.get_first_visible_pos(ow_size, focus)

            # in the case of estimated length, it can be smaller than real widget length
            ow_len = max(ow_len, visible_amount, pos)
            posmax = ow_len - visible_amount
            thumb_weight = min(1.0, visible_amount / max(1, ow_len))

            # Corner case possible: formally all contents indexes should be visible, but this does not mean all rows
            if ow_len != visible_amount:
                require_absolute = False

        if require_absolute:
            if isinstance(ow_base, SupportsScroll):
                if ow_base.rows_max(size, focus) > maxrow:
                    # re-calculate using wrapped size
                    ow_rows_max = ow_base.rows_max(ow_size, focus)
                    pos = ow_base.get_scrollpos(ow_size, focus)
                    posmax = ow_rows_max - maxrow
                    thumb_weight = min(1.0, maxrow / max(1, ow_rows_max))

                else:
                    # Canvas fits without scrolling - no scrollbar needed
                    return None
            else:
                raise TypeError(f"Not a scrollable widget: {ow_base!r}")

        # Thumb shrinks/grows according to the ratio of  / 
        thumb_height = max(1, round(thumb_weight * maxrow))  # pylint: disable=possibly-used-before-assignment

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

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

        return _ScrollbarLayout(
            ow_size=ow_size,
            sb_width=sb_width,
            pos=pos,
            posmax=posmax,
            thumb_height=thumb_height,
            top_height=top_height,
            bottom_height=bottom_height,
        )

    def render(
        self,
        size: tuple[int, int],  # type: ignore[override]
        focus: bool = False,
    ) -> Canvas:
        """Render the wrapped widget together with the scrollbar, when scrolling is needed."""
        from urwid import canvas

        maxcol, maxrow = size

        # Render the wrapped widget first
        # so any pending scroll action is applied before the scroll position is queried for the scrollbar geometry.
        ow_size = (max(0, maxcol - self._scrollbar_width), maxrow)
        ow_canv = self._render_original(ow_size, focus)

        layout = self._scrollbar_layout(size, focus)
        if layout is None:
            # Canvas fits without scrolling - no scrollbar needed
            return self._render_original(size, focus)

        # 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 = canvas.SolidCanvas(self._trough_char, layout.sb_width, 1)
        thumb = canvas.SolidCanvas(self._thumb_char, layout.sb_width, 1)
        bottom = canvas.SolidCanvas(self._trough_char, layout.sb_width, 1)
        sb_canv = canvas.CanvasCombine(
            (
                *((top, None, False) for _ in range(layout.top_height)),
                *((thumb, None, False) for _ in range(layout.thumb_height)),
                *((bottom, None, False) for _ in range(layout.bottom_height)),
            ),
        )

        combinelist = [
            (ow_canv, None, True, layout.ow_size[0]),
            (sb_canv, None, False, layout.sb_width),
        ]

        if self._scrollbar_side != SCROLLBAR_LEFT:
            return canvas.CanvasJoin(combinelist)

        return canvas.CanvasJoin(reversed(combinelist))

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

    @scrollbar_width.setter
    def scrollbar_width(self, width: typing.SupportsInt) -> None:
        self._scrollbar_width = max(1, int(width))
        self._invalidate()

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

    @scrollbar_side.setter
    def scrollbar_side(self, side: Literal["left", "right"]) -> None:
        """
        Set the side of the widget the scrollbar is drawn on.

        :raises ValueError: *side* is neither ``'left'`` nor ``'right'``.
        """
        if side not in {SCROLLBAR_LEFT, SCROLLBAR_RIGHT}:
            raise ValueError(f'scrollbar_side must be "left" or "right", not {side!r}')
        self._scrollbar_side = side
        self._invalidate()

    @property
    def scrolling_base_widget(self) -> SupportsScroll | SupportsRelativeScroll:
        """Nearest `original_widget` that is compatible with the scrolling API.

        :raises ScrollableError: no wrapped widget supports the scrolling protocol.
        """
        w = self

        for w in orig_iter(self):  # type: ignore[assignment]
            if isinstance(w, SupportsScroll):
                return w

        raise ScrollableError(f"Not compatible to be wrapped by ScrollBar: {w!r}")

    def keypress(
        self,
        size: tuple[int, int],  # type: ignore[override]
        key: str,
    ) -> str | None:
        """Forward a keypress to the wrapped widget."""
        return self._original_widget.keypress(self._original_widget_size, key)

    def mouse_event(
        self,
        size: tuple[int, int],  # type: ignore[override]
        event: str,
        button: int,
        col: int,
        row: int,
        focus: bool,
    ) -> bool | None:
        """Handle a mouse event, dispatching it to the scrollbar thumb/trough or to the wrapped widget."""
        ow = self._original_widget
        ow_size = self._original_widget_size
        supports_scroll = hasattr(ow, "set_scrollpos")
        on_scrollbar = False
        ow_col = col

        if supports_scroll:
            # The geometry is needed before the event is passed on: the wrapped widget cannot tell
            # that a column belongs to the scrollbar, and would answer a click that is not its own.
            layout = self._scrollbar_layout(size, focus)
            if layout is not None:
                if self._scrollbar_side == SCROLLBAR_LEFT:
                    on_scrollbar = col < layout.sb_width
                    # The wrapped widget is drawn after the scrollbar, so its own column 0 sits at
                    # screen column sb_width and the offset has to be taken back out.
                    ow_col = col - layout.sb_width
                else:
                    on_scrollbar = col >= layout.ow_size[0]

                if on_scrollbar and button == 1:
                    # Move the thumb top to the clicked row, inverting the placement done during render.
                    thumb_travel = size[1] - layout.thumb_height
                    if thumb_travel > 0:
                        newpos = round(row * layout.posmax / thumb_travel)
                    else:
                        newpos = 0
                    ow.set_scrollpos(max(0, min(layout.posmax, newpos)))  # type: ignore[attr-defined]  # gated
                    return True

        handled: bool | None = False
        if not on_scrollbar and hasattr(ow, "mouse_event"):
            handled = ow.mouse_event(ow_size, event, button, ow_col, row, focus)

        if not handled and supports_scroll:
            if button == 4:  # scroll wheel up
                pos = ow.get_scrollpos(ow_size)
                newpos = max(pos - 1, 0)
                ow.set_scrollpos(newpos)  # type: ignore[attr-defined]  # gated
                return True
            if button == 5:  # scroll wheel down
                pos = ow.get_scrollpos(ow_size)
                ow.set_scrollpos(pos + 1)  # type: ignore[attr-defined]  # gated
                return True

        return handled
urwid-urwid-69907e4/urwid/widget/solid_fill.py000066400000000000000000000023721526073734300214120ustar00rootroot00000000000000"""SolidFill: a box widget filled with a single character."""

from __future__ import annotations

from urwid.canvas import SolidCanvas

from .constants import SHADE_SYMBOLS, Sizing
from .widget import Widget


class SolidFill(Widget):
    """A box widget that fills an area with a single character."""

    _selectable = False
    ignore_focus = True
    _sizing = frozenset([Sizing.BOX])

    Symbols = SHADE_SYMBOLS

    def __init__(self, fill_char: str = " ") -> None:
        """
        :param fill_char: character to fill area with

        >>> SolidFill("8")
        
        """
        super().__init__()
        self.fill_char = fill_char

    def _repr_words(self) -> list[str]:
        return [*super()._repr_words(), repr(self.fill_char)]

    def render(
        self,
        size: tuple[int, int],  # type: ignore[override]
        focus: bool = False,
    ) -> SolidCanvas:
        """
        Render the Fill as a canvas and return it.

        >>> SolidFill().render((4, 2)).text  # ... = b in Python 3
        [...'    ', ...'    ']
        >>> SolidFill("#").render((5, 3)).text
        [...'#####', ...'#####', ...'#####']
        """
        maxcol, maxrow = size
        return SolidCanvas(self.fill_char, maxcol, maxrow)
urwid-urwid-69907e4/urwid/widget/text.py000066400000000000000000000336231526073734300202610ustar00rootroot00000000000000"""Text: a widget that displays text with display attributes."""

from __future__ import annotations

import typing

from urwid import text_layout
from urwid.canvas import apply_text_layout
from urwid.split_repr import remove_defaults
from urwid.str_util import calc_width
from urwid.util import decompose_tagmarkup

from .constants import Align, Sizing, WrapMode
from .widget import Widget, WidgetError

if typing.TYPE_CHECKING:
    from collections.abc import Hashable

    from typing_extensions import Literal

    from urwid.canvas import TextCanvas
    from urwid.util import _TagMarkup


class TextError(WidgetError):
    """Error raised for invalid :class:`Text` widget usage."""


class Text(Widget):
    """A horizontally resizeable text widget."""

    _sizing = frozenset([Sizing.FLOW, Sizing.FIXED])

    ignore_focus = True
    _repr_content_length_max = 140

    def __init__(
        self,
        markup: _TagMarkup,
        align: Literal["left", "center", "right"] | Align = Align.LEFT,
        wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode = WrapMode.SPACE,
        layout: text_layout.TextLayout | None = None,
    ) -> None:
        """
        :param markup: content of text widget, one of:

            bytes or unicode
              text to be displayed

            (*display attribute*, *text markup*)
              *text markup* with *display attribute* applied to all parts
              of *text markup* with no display attribute already applied

            [*text markup*, *text markup*, ... ]
              all *text markup* in the list joined together

        :param align: typically ``'left'``, ``'center'`` or ``'right'``
        :param wrap: typically ``'space'``, ``'any'``, ``'clip'`` or ``'ellipsis'``
        :param layout: defaults to a shared :class:`StandardTextLayout` instance

        >>> Text("Hello")
        
        >>> t = Text(("bold", "stuff"), "right", "any")
        >>> t
        
        >>> print(t.text)
        stuff
        >>> t.attrib
        [('bold', 5)]
        """
        super().__init__()
        # (maxcol, translation) as one value: a reader either sees a width together with the translation
        # laid out for it, or nothing, even while another thread is replacing the pair.
        self._cache: tuple[int, list[list[tuple[int, int, int | bytes] | tuple[int, int | None]]]] | None = None
        self._layout: text_layout.TextLayout = layout or text_layout.default_layout
        self._text: str | bytes
        self._attrib: list[tuple[Hashable, int]]
        self._align_mode: Literal["left", "center", "right"] | Align = Align.LEFT
        self._wrap_mode: Literal["space", "any", "clip", "ellipsis"] | WrapMode = WrapMode.SPACE
        self.set_text(markup)
        self.set_layout(align, wrap, layout)

    def _repr_words(self) -> list[str]:
        """Show the text in the repr and truncate if it's too long."""
        first = super()._repr_words()
        text = self.get_text()[0]
        rest = repr(text)
        if len(rest) > self._repr_content_length_max:
            rest = (
                rest[: self._repr_content_length_max * 2 // 3 - 3] + "..." + rest[-self._repr_content_length_max // 3 :]
            )
        return [*first, rest]

    def _repr_attrs(self) -> dict[str, typing.Any]:
        attrs = {
            **super()._repr_attrs(),
            "align": self._align_mode,
            "wrap": self._wrap_mode,
        }
        return remove_defaults(attrs, Text.__init__)

    def _invalidate(self) -> None:
        self._cache = None
        super()._invalidate()

    def set_text(self, markup: _TagMarkup) -> None:
        """
        Set content of text widget.

        :param markup: see :class:`Text` for description.

        >>> t = Text("foo")
        >>> print(t.text)
        foo
        >>> t.set_text("bar")
        >>> print(t.text)
        bar
        >>> t.text = "baz"  # not supported because text stores text but set_text() takes markup
        Traceback (most recent call last):
        AttributeError: can't set attribute
        """
        self._text, self._attrib = decompose_tagmarkup(markup)
        self._invalidate()

    def get_text(self) -> tuple[str | bytes, list[tuple[Hashable, int]]]:
        """Return (*text*, *display attributes*).

            *text*
              complete bytes/unicode content of text widget

            *display attributes*
              run length encoded display attributes for *text*, eg.
              ``[('attr1', 10), ('attr2', 5)]``

        >>> Text("Hello").get_text()
        ('Hello', [])
        >>> Text(("bright", "Headline")).get_text()
        ('Headline', [('bright', 8)])
        >>> Text([("a", "one"), "two", ("b", "three")]).get_text()
        ('onetwothree', [('a', 3), (None, 3), ('b', 5)])
        """
        return self._text, self._attrib

    @property
    def text(self) -> str | bytes:
        """Read-only property returning the complete bytes/unicode content of this widget."""
        return self.get_text()[0]

    @property
    def attrib(self) -> list[tuple[Hashable, int]]:
        """Read-only property returning the run-length encoded display attributes of this widget."""
        return self.get_text()[1]

    def set_align_mode(self, mode: Literal["left", "center", "right"] | Align) -> None:
        """Set text alignment mode.

        Supported modes depend on text layout object in use but defaults to a
        :class:`StandardTextLayout` instance.

        :param mode: typically ``'left'``, ``'center'`` or ``'right'``
        :raises TextError: *mode* is not supported by the layout in use.

        >>> t = Text("word")
        >>> t.set_align_mode("right")
        >>> t.align
        'right'
        >>> t.render((10,)).text  # ... = b in Python 3
        [...'      word']
        >>> t.align = "center"
        >>> t.render((10,)).text
        [...'   word   ']
        >>> t.align = "somewhere"
        Traceback (most recent call last):
        TextError: Alignment mode 'somewhere' not supported.
        """
        if not self.layout.supports_align_mode(mode):
            raise TextError(f"Alignment mode {mode!r} not supported.")
        self._align_mode = mode
        self._invalidate()

    def set_wrap_mode(self, mode: Literal["space", "any", "clip", "ellipsis"] | WrapMode) -> None:
        """Set text wrapping mode.

        Supported modes depend on text layout object in use but defaults to a
        :class:`StandardTextLayout` instance.

        :param mode: typically ``'space'``, ``'any'``, ``'clip'`` or ``'ellipsis'``
        :raises TextError: *mode* is not supported by the layout in use.

        >>> t = Text("some words")
        >>> t.render((6,)).text  # ... = b in Python 3
        [...'some  ', ...'words ']
        >>> t.set_wrap_mode("clip")
        >>> t.wrap
        'clip'
        >>> t.render((6,)).text
        [...'some w']
        >>> t.wrap = "any"  # Urwid 0.9.9 or later
        >>> t.render((6,)).text
        [...'some w', ...'ords  ']
        >>> t.wrap = "somehow"
        Traceback (most recent call last):
        TextError: Wrap mode 'somehow' not supported.
        """
        if not self.layout.supports_wrap_mode(mode):
            raise TextError(f"Wrap mode {mode!r} not supported.")
        self._wrap_mode = mode
        self._invalidate()

    def set_layout(
        self,
        align: Literal["left", "center", "right"] | Align,
        wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode,
        layout: text_layout.TextLayout | None = None,
    ) -> None:
        """Set the text layout object, alignment and wrapping modes at the same time.

        :param wrap: typically 'space', 'any', 'clip' or 'ellipsis'
        :param layout: defaults to a shared :class:`StandardTextLayout` instance

        >>> t = Text("hi")
        >>> t.set_layout("right", "clip")
        >>> t
        
        """
        if layout is None:
            layout = text_layout.default_layout
        self._layout = layout
        self.set_align_mode(align)
        self.set_wrap_mode(wrap)

    align = property(lambda self: self._align_mode, set_align_mode)
    wrap = property(lambda self: self._wrap_mode, set_wrap_mode)

    @property
    def layout(self) -> text_layout.TextLayout:
        """Return the :class:`TextLayout` instance used to lay out and wrap this widget's text."""
        return self._layout

    def render(
        self,
        size: tuple[int] | tuple[()],  # type: ignore[override]
        focus: bool = False,
    ) -> TextCanvas:
        """
        Render contents with wrapping and alignment.  Return canvas.

        See :meth:`Widget.render` for parameter details.

        >>> Text("important things").render((18,)).text
        [b'important things  ']
        >>> Text("important things").render((11,)).text
        [b'important  ', b'things     ']
        >>> Text("demo text").render(()).text
        [b'demo text']
        """
        text, attr = self.get_text()
        if size:
            (maxcol,) = size
        else:
            maxcol, _ = self.pack(focus=focus)

        trans = self.get_line_translation(maxcol, (text, attr))
        return apply_text_layout(text, attr, trans, maxcol)

    def rows(self, size: tuple[int], focus: bool = False) -> int:
        """
        Return the number of rows the rendered text requires.

        See :meth:`Widget.rows` for parameter details.

        >>> Text("important things").rows((18,))
        1
        >>> Text("important things").rows((11,))
        2
        """
        (maxcol,) = size
        return len(self.get_line_translation(maxcol))

    def get_line_translation(
        self,
        maxcol: int,
        ta: tuple[str | bytes, list[tuple[Hashable, int]]] | None = None,
    ) -> list[list[tuple[int, int, int | bytes] | tuple[int, int | None]]]:
        """Return layout structure used to map self.text to a canvas.

        This method is used internally, but may be useful for debugging custom layout classes.

        :param maxcol: columns available for display
        :param ta: ``None`` or the (*text*, *display attributes*) tuple
                   returned from :meth:`.get_text`
        """
        cache = self._cache
        if cache is not None and cache[0] == maxcol:
            return cache[1]
        return self._update_cache_translation(maxcol, ta)

    def _update_cache_translation(
        self,
        maxcol: int,
        ta: tuple[str | bytes, list[tuple[Hashable, int]]] | None,
    ) -> list[list[tuple[int, int, int | bytes] | tuple[int, int | None]]]:
        if ta:
            text, _attr = ta
        else:
            text, _attr = self.get_text()
        translation = self.layout.layout(text, maxcol, self._align_mode, self._wrap_mode)
        self._cache = (maxcol, translation)
        return translation

    def pack(  # type: ignore[override]
        self,
        size: tuple[()] | tuple[int] | None = None,
        focus: bool = False,
    ) -> tuple[int, int]:
        """Return the number of screen columns and rows required for this Text widget.

        The widget is expected to be displayed without wrapping or clipping, as a single element tuple.
        With *maxcol* the size is taken from the layout at that width.
        Without it the width is searched for::

            maxcol = text width + 1, with StandardTextLayout tabs at their widest
                |
                v
            lay out at maxcol --> layout has pack()? --no--> widest line, line count
                ^                        |
                |                       yes
                |                        v
            double maxcol <--no-- layout.pack() < maxcol? --yes--> pack() columns, line count

        :param size: ``None`` or ``()`` for unlimited screen columns (like FIXED sizing)
                     or (*maxcol*,) to specify a maximum column size
        :param focus: widget is focused on

        >>> Text("important things").pack()
        (16, 1)
        >>> Text("important things").pack((15,))
        (9, 2)
        >>> Text("important things").pack((8,))
        (8, 2)
        >>> Text("important things").pack(())
        (16, 1)
        >>> not_common_separated_text = (
        ...     "Line feed" + chr(10) + "Line Separator" + chr(0x2028) + "Paragraph Separator" + chr(0x2029)
        ... )
        >>> # U+2028 (Line Separator) and U+2029 (Paragraph Separator) are not splitted by StandardTextLayout
        >>> not_common_separated = Text(not_common_separated_text)

        >>> not_common_separated.pack()
        (33, 2)
        >>> not_common_separated_canv = not_common_separated.render(())
        >>> not_common_separated_canv.cols()
        33
        >>> not_common_separated_canv.rows()
        2
        """
        text, attr = self.get_text()

        if size:
            (maxcol,) = size
            if not hasattr(self.layout, "pack"):
                return maxcol, self.rows(size, focus)

            trans = self.get_line_translation(maxcol, (text, attr))
            cols = self.layout.pack(maxcol, trans)
            return (cols, len(trans))

        layout = self.layout
        # No line is wider than the whole text with every tab taking the widest gap between tab stops.
        # A custom layout may still widen a line.
        maxcol = calc_width(text, 0, len(text)) + 1
        tabs = text.count("\t") if isinstance(text, str) else text.count(b"\t")
        if tabs and isinstance(layout, text_layout.StandardTextLayout):
            stops = (0, *layout.tab_stops)
            widest_tab = max((layout.tab_stop_every, *(end - start for start, end in zip(stops, stops[1:]))))
            maxcol += widest_tab * tabs
        while True:
            trans = layout.layout(text, maxcol, self._align_mode, self._wrap_mode)
            if not hasattr(layout, "pack"):
                return max(text_layout.line_width(line) for line in trans), len(trans)
            if (cols := layout.pack(maxcol, trans)) < maxcol:
                return cols, len(trans)
            maxcol *= 2
urwid-urwid-69907e4/urwid/widget/treetools.py000066400000000000000000000603161526073734300213140ustar00rootroot00000000000000# Generic TreeWidget/TreeWalker class
#    Copyright (c) 2010  Rob Lanphier
#    Copyright (C) 2004-2010  Ian Ward
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/


"""
Urwid tree view.

Features:
- custom selectable widgets for trees
- custom list walker for displaying widgets in a tree fashion
"""

from __future__ import annotations

import typing
import warnings

from .columns import Columns
from .constants import WHSettings
from .listbox import ListBox, ListWalker
from .padding import Padding
from .text import Text
from .widget import WidgetWrap
from .wimp import SelectableIcon

if typing.TYPE_CHECKING:
    from collections.abc import Hashable, Sequence

    from typing_extensions import Self

    from urwid.util import _TagMarkup

    from .listbox import VisibleInfo

__all__ = ("ParentNode", "TreeListBox", "TreeNode", "TreeWalker", "TreeWidget", "TreeWidgetError")

_T = typing.TypeVar("_T")
_Node = typing.TypeVar("_Node", bound="TreeNode[typing.Any] | ParentNode[typing.Any]")


class TreeWidgetError(RuntimeError):
    """Error raised for invalid tree widget usage."""


class TreeWidget(WidgetWrap[Padding[typing.Union[Text, Columns]]], typing.Generic[_Node]):
    """A widget representing something in a nested tree display."""

    indent_cols = 3
    unexpanded_icon = SelectableIcon("+", 0)
    expanded_icon = SelectableIcon("-", 0)

    def __init__(self, node: _Node) -> None:
        """Build the widget representing `node`, expanded by default."""
        self._node = node
        self._innerwidget: Text | None = None
        if not isinstance(node, ParentNode):
            if hasattr(node, "get_first_child"):
                warnings.warn(
                    f"get_first_child is defined, but node is not a ParentNode instance: {type(node)}",
                    DeprecationWarning,
                    stacklevel=2,
                )
                self.is_leaf = False
            else:
                self.is_leaf = True
        else:
            self.is_leaf = False

        self.expanded = True
        widget = self.get_indented_widget()
        super().__init__(widget)

    def selectable(self) -> bool:
        """Allow selection of non-leaf nodes so children may be (un)expanded."""
        return not self.is_leaf

    def get_indented_widget(self) -> Padding[Text | Columns]:
        """Return this node's widget wrapped with the expand/collapse icon and indentation."""
        widget: Text | Columns = self.get_inner_widget()
        if not self.is_leaf:
            widget = Columns(
                [(1, [self.unexpanded_icon, self.expanded_icon][self.expanded]), widget],
                dividechars=1,
            )
        indent_cols = self.get_indent_cols()
        return Padding(widget, width=(WHSettings.RELATIVE, 100), left=indent_cols)

    def update_expanded_icon(self) -> None:
        """Update display widget text for parent widgets."""
        # icon is first element in columns indented widget
        icon = [self.unexpanded_icon, self.expanded_icon][self.expanded]
        self._w.base_widget.contents[0] = (icon, (WHSettings.GIVEN, 1, False))  # type: ignore[attr-defined]

    def get_indent_cols(self) -> int:
        """Return the number of columns this widget is indented by, based on its node's depth."""
        return self.indent_cols * self.get_node().get_depth()

    def get_inner_widget(self) -> Text:
        """Return the cached inner widget, creating it with :meth:`load_inner_widget` if necessary."""
        if self._innerwidget is None:
            self._innerwidget = self.load_inner_widget()
        return self._innerwidget

    def load_inner_widget(self) -> Text:
        """Create the inner widget displaying this node's text."""
        return Text(self.get_display_text())

    def get_node(self) -> _Node:
        """Return the TreeNode this widget represents."""
        return self._node

    def get_display_text(self) -> _TagMarkup:
        """Return the text to display for this node, combining its key and value."""
        return f"{self.get_node().get_key()}: {self.get_node().get_value()!s}"

    def next_inorder(self) -> TreeWidget[TreeNode[typing.Any]] | None:
        """Return the next TreeWidget depth first from this one.

        :raises ValueError: the tree walker returned a node at an unexpected depth.
        """
        # first check if there's a child widget

        if (first_child := self.first_child()) is not None:
            return first_child

        # now we need to hunt for the next sibling
        this_node: TreeNode[typing.Any] = self.get_node()
        next_node = this_node.next_sibling()
        depth = this_node.get_depth()
        while next_node is None and depth > 0:
            # keep going up the tree until we find an ancestor next sibling
            this_node = this_node.get_parent()
            next_node = this_node.next_sibling()
            depth -= 1
            if depth != this_node.get_depth():
                raise ValueError(depth)
        if next_node is None:
            # we're at the end of the tree
            return None

        return next_node.get_widget()

    def prev_inorder(self) -> TreeWidget[TreeNode[typing.Any]] | None:
        """Return the previous TreeWidget depth first from this one."""
        this_node = self._node

        if (prev_node := this_node.prev_sibling()) is not None:
            # we need to find the last child of the previous widget if its
            # expanded
            prev_widget = prev_node.get_widget()
            if (last_child := prev_widget.last_child()) is not None:
                return last_child

            return prev_widget

        # need to hunt for the parent
        if this_node.get_depth() == 0:
            return None

        return this_node.get_parent().get_widget()

    def keypress(
        self,
        size: tuple[int] | tuple[()],
        key: str,
    ) -> str | None:
        """Handle expand & collapse requests (non-leaf nodes)."""
        if self.is_leaf:
            return key

        if key in {"+", "right"}:
            self.expanded = True
            self.update_expanded_icon()
            return None
        if key == "-":
            self.expanded = False
            self.update_expanded_icon()
            return None
        if self._w.selectable():
            return typing.cast("str | None", super().keypress(size, key))

        return key

    def mouse_event(
        self,
        size: tuple[int] | tuple[()],
        event: str,
        button: int,
        col: int,
        row: int,
        focus: bool,
    ) -> bool:
        """Toggle expand/collapse when the icon is left-clicked."""
        if self.is_leaf or event != "mouse press" or button != 1:
            return False

        if row == 0 and col == self.get_indent_cols():
            self.expanded = not self.expanded
            self.update_expanded_icon()
            return True

        return False

    def first_child(self) -> TreeWidget[TreeNode[typing.Any]] | None:
        """Return first child if expanded."""
        if self.is_leaf or not self.expanded:
            return None

        if typing.cast("ParentNode[typing.Any]", self._node).has_children():
            first_node = typing.cast("ParentNode[typing.Any]", self._node).get_first_child()
            return first_node.get_widget()

        return None

    def last_child(self) -> TreeWidget[TreeNode[typing.Any]] | None:
        """Return the last visible descendant if expanded.

        The descent is a loop rather than a recursion, so the depth of the tree is not limited by the
        interpreter's recursion limit.
        A descendant widget whose class overrides this method is asked for its own last descendant.

        :raises TreeWidgetError: the descent reaches a node it already passed, so the tree has a cycle.
        """
        # Keyed by id(), holding the node so the id cannot be reused by another node during the descent.
        visited: dict[int, TreeNode[typing.Any]] = {}
        last_descendant: TreeWidget[TreeNode[typing.Any]] | None = None
        widget: TreeWidget[typing.Any] = self
        while not widget.is_leaf and widget.expanded:
            node = typing.cast("ParentNode[typing.Any]", widget.get_node())
            if id(node) in visited:
                raise TreeWidgetError(f"Cycle in the tree: node {node.get_key()} is its own descendant")
            visited[id(node)] = node
            if not node.has_children():
                break
            widget = last_descendant = node.get_last_child().get_widget()
            if type(widget).last_child is not TreeWidget.last_child:
                if (overridden := widget.last_child()) is not None:
                    return overridden
                return widget

        return last_descendant


class TreeNode(typing.Generic[_T]):
    """Store tree contents and cache TreeWidget objects.

    A TreeNode consists of the following elements:
    *  key: accessor token for parent nodes
    *  value: subclass-specific data
    *  parent: a TreeNode which contains a pointer back to this object
    *  widget: The widget used to render the object
    """

    def __init__(
        self,
        value: _T,
        parent: ParentNode[typing.Any] | None = None,
        key: Hashable = None,
        depth: int | None = None,
    ) -> None:
        """Store `value` under `key`, with `parent` and `depth` as given, or computed from `parent` when omitted."""
        self._key = key
        self._parent = parent
        self._value = value
        self._depth = depth
        self._widget: TreeWidget[Self] | None = None

    def get_widget(self, reload: bool = False) -> TreeWidget[Self]:
        """Return the widget for this node."""
        if self._widget is None or reload:
            self._widget = self.load_widget()
        return self._widget

    def load_widget(self) -> TreeWidget[Self]:
        """Create the widget for this node."""
        return TreeWidget(self)

    def get_depth(self) -> int:
        """Return this node's depth in the tree, computing and caching it if necessary.

        The ancestors are walked in a loop rather than recursively, so the depth of the tree is not limited by the
        interpreter's recursion limit.
        Every ancestor whose depth was unknown caches its own depth as well.
        The walk stops at an ancestor whose class overrides this method, and uses the depth that ancestor returns.

        :raises TreeWidgetError: the parent links form a cycle.
        """
        if self._depth is not None:
            return self._depth

        # pylint: disable=protected-access  # the depth cache and parent link of other nodes of this class
        unresolved: list[TreeNode[typing.Any]] = []
        # ids of the nodes in ``unresolved``, which keeps them alive so no id is reused during the walk.
        visited: set[int] = set()
        depth = -1
        node: TreeNode[typing.Any] | None = self
        while node is not None:
            if node is not self and type(node).get_depth is not TreeNode.get_depth:
                depth = node.get_depth()
                break
            if node._depth is not None:
                depth = node._depth
                break
            if id(node) in visited:
                raise TreeWidgetError(f"Cycle in the tree: node {node.get_key()} is its own ancestor")
            visited.add(id(node))
            unresolved.append(node)
            node = node._parent

        for ancestor in reversed(unresolved):
            depth += 1
            ancestor._depth = depth
        return depth

    def get_index(self) -> int | None:
        """Return this node's position among its parent's children, or None for the root."""
        if self.get_depth() == 0:
            return None

        return self.get_parent().get_child_index(self.get_key())

    def get_key(self) -> Hashable:
        """Return this node's key."""
        return self._key

    def set_key(self, key: Hashable) -> None:
        """Set this node's key without updating the parent's reference to it."""
        self._key = key

    def change_key(self, key: Hashable) -> None:
        """Rename this node to ``key``, updating the parent's reference to it as well."""
        self.get_parent().change_child_key(self._key, key)

    def get_parent(self) -> ParentNode[typing.Any]:
        """Return this node's parent, loading it with :meth:`load_parent` if necessary."""
        if self._parent is None and self.get_depth() > 0:
            self._parent = self.load_parent()
        return typing.cast("ParentNode[typing.Any]", self._parent)

    def load_parent(self) -> ParentNode[typing.Any]:
        """Provide TreeNode with a parent for the current node.

        This function is only required if the tree was instantiated from a child node
        (virtual function)

        :raises TreeWidgetError: the subclass does not override this method.
        """
        raise TreeWidgetError("virtual function.  Implement in subclass")

    def get_value(self) -> _T:
        """Return this node's value."""
        return self._value

    def is_root(self) -> bool:
        """Return whether this node is the root of the tree."""
        return self.get_depth() == 0

    def next_sibling(self) -> TreeNode[typing.Any] | None:
        """Return the next sibling of this node, or None if there is none."""
        if self.get_depth() > 0:
            return self.get_parent().next_child(self.get_key())

        return None

    def prev_sibling(self) -> TreeNode[typing.Any] | None:
        """Return the previous sibling of this node, or None if there is none."""
        if self.get_depth() > 0:
            return self.get_parent().prev_child(self.get_key())

        return None

    def get_root(self) -> ParentNode[typing.Any]:
        """Return the root node of the tree this node belongs to."""
        root = self
        while root.get_parent() is not None:
            root = root.get_parent()
        return typing.cast("ParentNode[typing.Any]", root)


class ParentNode(TreeNode[_T]):
    """Maintain sort order for TreeNodes."""

    def __init__(
        self,
        value: typing.Any,
        parent: ParentNode[typing.Any] | None = None,
        key: Hashable = None,
        depth: int | None = None,
    ) -> None:
        """Store `value` under `key`, with no child keys loaded yet."""
        super().__init__(value, parent=parent, key=key, depth=depth)

        self._child_keys: Sequence[Hashable] | None = None
        self._children: dict[Hashable, TreeNode[typing.Any]] = {}
        # Position of each key in ``_indexed_keys``, the key list the map was built from.
        self._child_index: dict[Hashable, int] = {}
        self._indexed_keys: Sequence[Hashable] | None = None

    def get_child_keys(self, reload: bool = False) -> Sequence[Hashable]:
        """Return a possibly ordered list of child keys.

        A reload also drops the cached child nodes whose keys are no longer in the list.
        """
        if self._child_keys is None or reload:
            self._child_keys = self.load_child_keys()
            if reload:
                current = set(self._child_keys)
                self._children = {key: node for key, node in self._children.items() if key in current}
                self._child_index = {}
                self._indexed_keys = None
        return self._child_keys

    def load_child_keys(self) -> Sequence[Hashable]:
        """Provide ParentNode with an ordered list of child keys (virtual function).

        :raises TreeWidgetError: the subclass does not override this method.
        """
        raise TreeWidgetError("virtual function.  Implement in subclass")

    def get_child_widget(self, key: Hashable) -> TreeWidget[TreeNode[typing.Any]]:
        """Return the widget for a given key.  Create if necessary."""
        return self.get_child_node(key).get_widget()

    def get_child_node(self, key: Hashable, reload: bool = False) -> TreeNode[typing.Any]:
        """Return the child node for a given key. Create if necessary."""
        if key not in self._children or reload:
            self._children[key] = self.load_child_node(key)
        return self._children[key]

    def load_child_node(self, key: Hashable) -> TreeNode[typing.Any]:
        """Load the child node for a given key (virtual function).

        :raises TreeWidgetError: the subclass does not override this method.
        """
        raise TreeWidgetError("virtual function.  Implement in subclass")

    def set_child_node(self, key: Hashable, node: TreeNode[typing.Any]) -> None:
        """Set the child node for a given key.

        Useful for bottom-up, lazy population of a tree.
        """
        self._children[key] = node

    def change_child_key(self, oldkey: Hashable, newkey: Hashable) -> None:
        """
        Rename a child, moving it from *oldkey* to *newkey*.

        :raises TreeWidgetError: *newkey* is already used by another child.
        """
        if newkey in self._children:
            raise TreeWidgetError(f"{newkey} is already in use")
        self._children[newkey] = self._children.pop(oldkey)
        self._children[newkey].set_key(newkey)

    def get_child_index(self, key: Hashable) -> int:
        """
        Return the position of the child *key* among the child keys.

        :raises TreeWidgetError: *key* is not a child of this node.
        """
        child_keys = self.get_child_keys()
        index = self._child_index.get(key) if child_keys is self._indexed_keys else None
        if index is None or index >= len(child_keys) or child_keys[index] != key:
            # The map is missing or does not match the key list: it was reloaded, replaced, or changed in place.
            child_index: dict[Hashable, int] = {}
            for position, child_key in enumerate(child_keys):
                child_index.setdefault(child_key, position)
            self._child_index, self._indexed_keys = child_index, child_keys
            if (index := child_index.get(key)) is None:
                raise TreeWidgetError(
                    f"Can't find key {key} in ParentNode {self.get_key()}\nParentNode items: {child_keys!s}"
                )
        return index

    def next_child(self, key: Hashable) -> TreeNode[typing.Any] | None:
        """Return the next child node in index order from the given key."""
        if (index := self.get_child_index(key)) is not None and (index + 1) < len(child_keys := self.get_child_keys()):
            # get the next item at same level
            return self.get_child_node(child_keys[index + 1])

        # the given node may have just been deleted
        return None

    def prev_child(self, key: Hashable) -> TreeNode[typing.Any] | None:
        """Return the previous child node in index order from the given key."""
        if (index := self.get_child_index(key)) is not None and index >= 1:
            # get the previous item at the same level
            return self.get_child_node(self.get_child_keys()[index - 1])

        return None

    def get_first_child(self) -> TreeNode[typing.Any]:
        """Return the first TreeNode in the directory."""
        child_keys = self.get_child_keys()
        return self.get_child_node(child_keys[0])

    def get_last_child(self) -> TreeNode[typing.Any]:
        """Return the last TreeNode in the directory."""
        child_keys = self.get_child_keys()
        return self.get_child_node(child_keys[-1])

    def has_children(self) -> bool:
        """Return whether this node has any children."""
        return len(self.get_child_keys()) > 0


class TreeWalker(ListWalker[TreeNode[typing.Any], TreeWidget[TreeNode[typing.Any]]]):
    """ListWalker-compatible class for displaying TreeWidgets.

    positions are TreeNodes.
    """

    def __init__(self, start_from: TreeNode[typing.Any]) -> None:
        """start_from: TreeNode with the initial focus."""
        self.focus = start_from

    def get_focus(self) -> tuple[TreeWidget[TreeNode[typing.Any]], TreeNode[typing.Any]]:
        """Return the widget and node currently in focus."""
        widget = self.focus.get_widget()
        return widget, self.focus

    def set_focus(self, focus: TreeNode[typing.Any]) -> None:
        """Set the node in focus and notify listeners that the walker was modified."""
        self.focus = focus
        self._modified()

    # pylint: disable=arguments-renamed  # its bad, but we should not change API
    def get_next(
        self,
        start_from: TreeNode[typing.Any],
    ) -> tuple[TreeWidget[TreeNode[typing.Any]], TreeNode[typing.Any]] | tuple[None, None]:
        """Return the widget and node that follow ``start_from`` depth first, or ``(None, None)``."""
        if (target := start_from.get_widget().next_inorder()) is not None:
            return target, target.get_node()

        return None, None

    def get_prev(
        self,
        start_from: TreeNode[typing.Any],
    ) -> tuple[TreeWidget[TreeNode[typing.Any]], TreeNode[typing.Any]] | tuple[None, None]:
        """Return the widget and node that precede ``start_from`` depth first, or ``(None, None)``."""
        if (target := start_from.get_widget().prev_inorder()) is not None:
            return target, target.get_node()

        return None, None

    # pylint: enable=arguments-renamed


class TreeListBox(ListBox[TreeNode[typing.Any]]):
    """A ListBox with special handling for navigation and collapsing of TreeWidgets."""

    def keypress(
        self,
        size: tuple[int, int],  # type: ignore[override]
        key: str,
    ) -> str | None:
        """Handle a keypress, passing any unhandled key to :meth:`unhandled_input`."""
        if unhandled := super().keypress(size, key):
            return self.unhandled_input(size, unhandled)
        return None

    def unhandled_input(self, size: tuple[int, int], data: str) -> str | None:
        """Handle macro-navigation keys."""
        if data == "left":
            self.move_focus_to_parent(size)
            return None
        if data == "-":
            self.collapse_focus_parent(size)
            return None

        return data

    def collapse_focus_parent(self, size: tuple[int, int]) -> None:
        """Collapse parent directory."""
        _widget, pos = self.body.get_focus()
        self.move_focus_to_parent(size)

        _pwidget, ppos = self.body.get_focus()
        if pos != ppos:
            self.keypress(size, "-")

    def move_focus_to_parent(self, size: tuple[int, int]) -> None:
        """Move focus to parent of widget in focus."""
        _widget, pos = self.body.get_focus()

        parentpos = typing.cast("TreeNode[typing.Any]", pos).get_parent()

        if parentpos is None:
            return

        visible = typing.cast("VisibleInfo", self.calculate_visible(size))

        row_offset = visible.middle.offset

        for _widget, pos, rows in visible.top.fill:
            row_offset -= rows
            if pos == parentpos:
                self.change_focus(size, pos, row_offset)
                return

        self.change_focus(size, typing.cast("TreeNode[typing.Any]", pos).get_parent())

    def _keypress_max_left(self, size: tuple[int, int]) -> None:
        self.focus_home(size)

    def _keypress_max_right(self, size: tuple[int, int]) -> None:
        self.focus_end(size)

    def focus_home(self, size: tuple[int, int]) -> None:
        """Move focus to very top."""
        _widget, pos = self.body.get_focus()
        rootnode = typing.cast("TreeNode[typing.Any]", pos).get_root()
        self.change_focus(size, rootnode)

    def focus_end(self, size: tuple[int, int]) -> None:
        """Move focus to far bottom."""
        maxrow, _maxcol = size
        _widget, pos = self.body.get_focus()

        if lastwidget := typing.cast("TreeNode[typing.Any]", pos).get_root().get_widget().last_child():
            lastnode = lastwidget.get_node()

            self.change_focus(size, lastnode, maxrow - 1)
urwid-urwid-69907e4/urwid/widget/widget.py000066400000000000000000001065461526073734300205650ustar00rootroot00000000000000# Urwid basic widget classes
#    Copyright (C) 2004-2012  Ian Ward
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/


"""The base Widget class, its metaclass, and the protocols of the widget API."""

from __future__ import annotations

import abc
import functools
import logging
import typing
import warnings
from operator import attrgetter

from urwid import signals
from urwid.canvas import Canvas, CanvasCache, CompositeCanvas
from urwid.command_map import command_map
from urwid.split_repr import split_repr
from urwid.util import MetaSuper

from .constants import Sizing

if typing.TYPE_CHECKING:
    from collections.abc import Callable, Hashable


__all__ = (
    "AbstractBoxWidget",
    "AbstractFixedWidget",
    "AbstractFlowWidget",
    "AbstractWidget",
    "Widget",
    "WidgetError",
    "WidgetMeta",
    "WidgetWarning",
    "WidgetWrap",
    "WidgetWrapError",
    "cache_widget_render",
    "cache_widget_rows",
    "delegate_to_widget_mixin",
    "fixed_size",
    "nocache_widget_render",
    "nocache_widget_render_instance",
)


WrappedWidget = typing.TypeVar("WrappedWidget", bound="AbstractWidget")
LOGGER = logging.getLogger(__name__)


class WidgetMeta(
    signals.MetaSignals,
    type(typing.Protocol),  # type: ignore[misc]  # hack: _ProtocolMeta is private, but need to subclass protocols
    MetaSuper,
):
    """
    Bases: :class:`MetaSuper`, :class:`MetaSignals`.

    Automatic caching of render and rows methods.

    Class variable *no_cache* is a list of names of methods to not cache automatically.
    Valid method names for *no_cache* are ``'render'`` and ``'rows'``.

    Class variable *ignore_focus* if defined and set to ``True`` indicates
    that the canvas this widget renders is not affected by the focus parameter, so it may be ignored when caching.
    """

    def __init__(
        cls,
        name: str,
        bases: tuple[type, ...],
        d: dict[str, typing.Any],
    ) -> None:
        """Wrap the new class's `render`/`rows` methods with caching, unless listed in `no_cache`."""
        no_cache = d.get("no_cache", [])

        super().__init__(name, bases, d)

        if "render" in d:
            if "render" not in no_cache:
                render_fn = cache_widget_render(cls)
            else:
                render_fn = nocache_widget_render(cls)
            cls.render = render_fn

        if "rows" in d and "rows" not in no_cache:
            cls.rows = cache_widget_rows(cls)
        if "no_cache" in d:
            del cls.no_cache
        if "ignore_focus" in d:
            del cls.ignore_focus


@typing.runtime_checkable
class AbstractWidget(typing.Protocol):
    """Protocol for widget.

    Act as protocol for attributes-based type checking and as abstract base class for widget implementation.
    While normal widgets are expected to subclass :class:`Widget`,
    in some special cases we may deal with "implementation from scratch".
    In this case `isinstance` will fail to compare with :class:`Widget`,
    but if all required interfaces present - we can use it as valid widget implementation.

    .. note::
        Sizing specific arguments are left as `typing.Any` to prevent static type checking errors.

    .. note::
        `focus_position` is not listed, since it raises `IndexError` on an attribute access check.
    """

    # Base widget methods (from Widget)
    @abc.abstractmethod
    def sizing(self) -> frozenset[Sizing]:
        """Return the set of sizing modes this widget supports."""

    @abc.abstractmethod
    def selectable(self) -> bool:
        """Return whether this widget can take the input focus."""

    @abc.abstractmethod
    def pack(
        self,
        size: typing.Any,
        focus: bool = False,
    ) -> tuple[int, int]:
        """Return the number of columns and rows this widget requires to render."""

    @property
    @abc.abstractmethod
    def base_widget(self) -> AbstractWidget:
        """Return the widget without decoration, unwrapping every layer of container/decoration widget."""

    @property
    @abc.abstractmethod
    def focus(self) -> AbstractWidget | None:
        """Return the widget in focus, or ``None`` if this widget has no focus."""

    @abc.abstractmethod
    def keypress(self, size: typing.Any, key: str) -> str | None:
        """Handle a keypress, returning the key if it was not handled by this widget."""

    @abc.abstractmethod
    def mouse_event(
        self,
        size: typing.Any,
        event: str,
        button: int,
        col: int,
        row: int,
        focus: bool,
    ) -> bool | None:
        """Handle a mouse event, returning ``True`` if it was handled by this widget."""

    @abc.abstractmethod
    def render(
        self,
        size: typing.Any,
        focus: bool = False,
    ) -> Canvas:
        """Render this widget into a canvas of the given size."""

    # Protected methods

    @abc.abstractmethod
    def _invalidate(self) -> None: ...


@typing.runtime_checkable
class AbstractBoxWidget(AbstractWidget, typing.Protocol):
    """Box widget protocol.

    Used internally to declare explicit required API.
    """

    @abc.abstractmethod
    def keypress(self, size: tuple[int, int], key: str) -> str | None:
        """Handle a keypress for a box-sized widget, returning the key if it was not handled."""

    @abc.abstractmethod
    def mouse_event(
        self,
        size: tuple[int, int],
        event: str,
        button: int,
        col: int,
        row: int,
        focus: bool,
    ) -> bool | None:
        """Handle a mouse event for a box-sized widget, returning ``True`` if it was handled."""

    @abc.abstractmethod
    def pack(
        self,
        size: tuple[int, int],
        focus: bool = False,
    ) -> tuple[int, int]:
        """Return the number of columns and rows this box widget requires to render."""

    @abc.abstractmethod
    def render(
        self,
        size: tuple[int, int],
        focus: bool = False,
    ) -> Canvas:
        """Render this box widget into a canvas of the given size."""


@typing.runtime_checkable
class AbstractFlowWidget(AbstractWidget, typing.Protocol):
    """Flow widget protocol.

    Used internally to declare explicit required API.
    """

    @abc.abstractmethod
    def keypress(self, size: tuple[int], key: str) -> str | None:
        """Handle a keypress for a flow-sized widget, returning the key if it was not handled."""

    @abc.abstractmethod
    def mouse_event(
        self,
        size: tuple[int],
        event: str,
        button: int,
        col: int,
        row: int,
        focus: bool,
    ) -> bool | None:
        """Handle a mouse event for a flow-sized widget, returning ``True`` if it was handled."""

    @abc.abstractmethod
    def rows(self, size: tuple[int], focus: bool = False) -> int:
        """Return the number of rows this flow widget will occupy for the given number of columns."""

    @abc.abstractmethod
    def pack(
        self,
        size: tuple[int],
        focus: bool = False,
    ) -> tuple[int, int]:
        """Return the number of columns and rows this flow widget requires to render."""

    @abc.abstractmethod
    def render(
        self,
        size: tuple[int],
        focus: bool = False,
    ) -> Canvas:
        """Render this flow widget into a canvas of the given size."""


@typing.runtime_checkable
class AbstractFixedWidget(AbstractWidget, typing.Protocol):
    """Fixed widget protocol.

    Used internally to declare explicit required API.
    """

    @abc.abstractmethod
    def keypress(self, size: tuple[()], key: str) -> str | None:
        """Handle a keypress for a fixed-sized widget, returning the key if it was not handled."""

    @abc.abstractmethod
    def mouse_event(
        self,
        size: tuple[()],
        event: str,
        button: int,
        col: int,
        row: int,
        focus: bool,
    ) -> bool | None:
        """Handle a mouse event for a fixed-sized widget, returning ``True`` if it was handled."""

    @abc.abstractmethod
    def pack(
        self,
        size: tuple[()],
        focus: bool = False,
    ) -> tuple[int, int]:
        """Return the number of columns and rows this fixed-sized widget requires to render."""

    @abc.abstractmethod
    def render(
        self,
        size: tuple[()],
        focus: bool = False,
    ) -> Canvas:
        """Render this fixed-sized widget into a canvas."""


class WidgetError(Exception):
    """Widget specific errors."""


class WidgetWarning(Warning):
    """Widget specific warnings."""


def validate_size(
    widget: AbstractWidget,
    size: tuple[()] | tuple[int] | tuple[int, int],
    canv: Canvas,
) -> None:
    """
    Raise a WidgetError if a canv does not match size.

    :raises WidgetError: *canv* does not have the size the widget was rendered with.
    """
    if (size and size[1:] != (0,) and size[0] != canv.cols()) or (len(size) > 1 and size[1] != canv.rows()):
        raise WidgetError(
            f"Widget {widget!r} rendered ({canv.cols():d} x {canv.rows():d}) canvas when passed size {size!r}!"
        )


def cache_widget_render(
    cls: WidgetMeta,
) -> Callable[[AbstractWidget, tuple[()] | tuple[int] | tuple[int, int], bool], Canvas]:
    """Return a function that wraps the cls.render() method and fetches and stores canvases with CanvasCache."""
    ignore_focus = bool(getattr(cls, "ignore_focus", False))
    fn = cls.render

    @functools.wraps(fn)
    def cached_render(
        self: AbstractWidget,
        size: tuple[()] | tuple[int] | tuple[int, int],
        focus: bool = False,
    ) -> Canvas:
        focus = focus and not ignore_focus

        if canv := CanvasCache.fetch(self, cls, size, focus):
            return canv

        canv = fn(self, size, focus=focus)  # type: ignore[call-arg]
        validate_size(self, size, canv)
        if canv.widget_info:
            canv = CompositeCanvas(canv)
        canv.finalize(self, size, focus)
        CanvasCache.store(cls, canv)
        return canv

    cached_render.original_fn = fn  # type: ignore[attr-defined]
    return cached_render


def nocache_widget_render(
    cls: WidgetMeta,
) -> Callable[[AbstractWidget, tuple[()] | tuple[int] | tuple[int, int], bool], Canvas]:
    """Return a function that wraps the cls.render() method and finalizes the canvas that it returns."""
    fn = cls.render
    if hasattr(fn, "original_fn"):
        fn = fn.original_fn

    @functools.wraps(fn)
    def finalize_render(
        self: AbstractWidget,
        size: tuple[()] | tuple[int] | tuple[int, int],
        focus: bool = False,
    ) -> Canvas:
        canv = fn(self, size, focus=focus)  # type: ignore[call-arg]
        if canv.widget_info:
            canv = CompositeCanvas(canv)
        validate_size(self, size, canv)
        canv.finalize(self, size, focus)
        return canv

    finalize_render.original_fn = fn  # type: ignore[attr-defined]  # un-cache
    return finalize_render


def nocache_widget_render_instance(
    self: AbstractWidget,
) -> Callable[[tuple[()] | tuple[int] | tuple[int, int], bool], Canvas]:
    """Return a function that wraps the cls.render() method and finalizes the canvas, without caching it."""
    fn = self.render.original_fn  # type: ignore[attr-defined]

    @functools.wraps(fn)
    def finalize_render(
        size: tuple[()] | tuple[int] | tuple[int, int],
        focus: bool = False,
    ) -> Canvas:
        canv = typing.cast("Canvas", fn(self, size, focus=focus))
        if canv.widget_info:
            canv = CompositeCanvas(canv)
        canv.finalize(self, size, focus)
        return canv

    finalize_render.original_fn = fn  # type: ignore[attr-defined]  # un-cache
    return finalize_render


def cache_widget_rows(cls: WidgetMeta) -> Callable[[AbstractWidget, tuple[int], bool], int]:
    """Return a function that wraps the cls.rows() method and returns rows from the CanvasCache if available."""
    ignore_focus = bool(getattr(cls, "ignore_focus", False))
    fn = cls.rows

    @functools.wraps(fn)
    def cached_rows(
        self: AbstractWidget,
        size: tuple[int],
        focus: bool = False,
    ) -> int:
        focus = focus and not ignore_focus

        if canv := CanvasCache.fetch(self, cls, size, focus):
            return canv.rows()

        return fn(self, size, focus)

    return cached_rows


class Widget(AbstractWidget, metaclass=WidgetMeta):
    """
    Widget base class.

    .. attribute:: _selectable
       :annotation: = False

       The default :meth:`.selectable` method returns this value.

    .. attribute:: _sizing
       :annotation: = frozenset(['flow', 'box', 'fixed'])

       The default :meth:`.sizing` method returns this value.

    .. attribute:: _command_map
       :annotation: = urwid.command_map

       A shared :class:`CommandMap` instance. May be redefined in subclasses or widget instances.


    .. method:: rows(size, focus=False)

       .. note::

          This method is not implemented in :class:`.Widget` but
          must be implemented by any flow widget.  See :meth:`.sizing`.

       See :meth:`Widget.render` for parameter details.

       :returns: The number of rows required for this widget given a number of columns in *size*

       This is the method flow widgets use to communicate their size to other
       widgets without having to render a canvas. This should be a quick
       calculation as this function may be called a number of times in normal
       operation. If your implementation may take a long time you should add
       your own caching here.

       There is some metaclass magic defined in the :class:`Widget`
       metaclass :class:`WidgetMeta` that causes the
       result of this function to be retrieved from any
       canvas cached by :class:`CanvasCache`, so if your widget
       has been rendered you may not receive calls to this function. The class
       variable :attr:`ignore_focus` may be defined and set to ``True`` if this
       widget renders the same size regardless of the value of the *focus*
       parameter.

    .. method:: get_cursor_coords(size)

       .. note::

          This method is not implemented in :class:`.Widget` but
          must be implemented by any widget that may return cursor
          coordinates as part of the canvas that :meth:`render` returns.

       :param size: See :meth:`Widget.render` for details.

       :returns: (*col*, *row*) if this widget has a cursor, ``None`` otherwise

       Return the cursor coordinates (*col*, *row*) of a cursor that will appear
       as part of the canvas rendered by this widget when in focus, or ``None``
       if no cursor is displayed.

       The :class:`ListBox` widget
       uses this method to make sure a cursor in the focus widget is not scrolled out of view.
       It is a separate method to avoid having to render the whole widget while calculating layout.

       Container widgets will typically call the :meth:`.get_cursor_coords` method on their focus widget.


    .. method:: get_pref_col(size)

       .. note::

          This method is not implemented in :class:`.Widget` but may be implemented by a subclass.

       :param size: See :meth:`Widget.render` for details.

       :returns: a column number or ``'left'`` for the leftmost available
                 column or ``'right'`` for the rightmost available column

       Return the preferred column for the cursor to be displayed in this
       widget. This value might not be the same as the column returned from
       :meth:`get_cursor_coords`.

       The :class:`ListBox` and :class:`Pile`
       widgets call this method on a widget losing focus and use the value
       returned to call :meth:`.move_cursor_to_coords` on the widget becoming
       the focus. This allows the focus to move up and down through widgets
       while keeping the cursor in approximately the same column on screen.


    .. method:: move_cursor_to_coords(size, col, row)

       .. note::

          This method is not implemented in :class:`.Widget` but may be implemented by a subclass.
          Not implementing this method is equivalent to having a method that always returns
          ``False``.

       :param size: See :meth:`Widget.render` for details.
       :param col: new column for the cursor, 0 is the left edge of this widget
       :param row: new row for the cursor, 0 it the top row of this widget

       :returns: ``True`` if the position was set successfully anywhere on *row*, ``False`` otherwise
    """

    _selectable = False
    _sizing = frozenset([Sizing.FLOW, Sizing.BOX, Sizing.FIXED])
    _command_map = command_map

    def __init__(self) -> None:
        """Initialize a logger scoped to the concrete widget subclass."""
        self.logger = logging.getLogger(f"{self.__class__.__module__}.{self.__class__.__name__}")

    def _invalidate(self) -> None:
        """Mark cached canvases rendered by this widget as dirty so that they will not be used again."""
        CanvasCache.invalidate(self)

    def _emit(self, name: Hashable, *args: typing.Any) -> None:
        """Emit signals with self as first argument."""
        signals.emit_signal(self, name, self, *args)

    def selectable(self) -> bool:
        """
        Return whether this widget is designed to take the focus.

        :returns: ``True`` if this widget contains something the user might want to interact with,
            ``False`` otherwise.

        This default implementation returns :attr:`_selectable`.
        Subclasses may leave it as is if they are not selectable, or,
        if they are always selectable, set the :attr:`_selectable` class variable to ``True``.

        If this method returns ``True`` then the :meth:`Widget.keypress` method must be implemented.

        Returning ``False`` does not guarantee that this widget will never be in
        focus, only that this widget will usually be skipped over when changing
        focus. It is still possible for non selectable widgets to have the focus
        (typically when there are no other selectable widgets visible).
        """
        return self._selectable

    def sizing(self) -> frozenset[Sizing]:
        """Return the sizing modes this widget supports.

        :returns: A frozenset including one or more of ``'box'``, ``'flow'`` and
                  ``'fixed'``.  Default implementation returns the value of
                  :attr:`._sizing`, which for this class includes all three.

        The sizing modes returned indicate the modes that may be
        supported by this widget, but is not sufficient to know
        that using that sizing mode will work.  Subclasses should
        make an effort to remove sizing modes they know will not
        work given the state of the widget, but many do not yet
        do this.

        If a sizing mode is missing from the set then the widget
        should fail when used in that mode.

        If ``'flow'`` is among the values returned then the other
        methods in this widget must be able to accept a
        single-element tuple (*maxcol*,) to their ``size``
        parameter, and the :meth:`rows` method must be defined.

        If ``'box'`` is among the values returned then the other
        methods must be able to accept a two-element tuple
        (*maxcol*, *maxrow*) to their size parameter.

        If ``'fixed'`` is among the values returned then the other
        methods must be able to accept an empty tuple () to
        their size parameter, and the :meth:`pack` method must
        be defined.
        """
        return self._sizing

    def pack(self, size: tuple[()] | tuple[int] | tuple[int, int], focus: bool = False) -> tuple[int, int]:
        """
        See :meth:`Widget.render` for parameter details.

        :returns: A "packed" size (*maxcol*, *maxrow*) for this widget
        :raises NotImplementedError: the widget supports FIXED sizing but does not override this method.
        :raises WidgetError: *size* does not match any sizing mode the widget supports.

        Calculate and return a minimum
        size where all content could still be displayed. Fixed widgets must
        implement this method and return their size when ``()`` is passed as the
        *size* parameter.

        This default implementation returns the *size* passed, or the *maxcol*
        passed and the value of :meth:`rows` as the *maxrow* when (*maxcol*,)
        is passed as the *size* parameter.

        .. note::

           This is a new method that hasn't been fully implemented across the
           standard widget types. In particular it has not yet been
           implemented for container widgets.

        :class:`Text` widgets have implemented this method.
        You can use :meth:`Text.pack` to calculate the minimum
        columns and rows required to display a text widget without wrapping,
        or call it iteratively to calculate the minimum number of columns
        required to display the text wrapped into a target number of rows.
        """
        if not size:
            if Sizing.FIXED in self.sizing():
                raise NotImplementedError(f"{self!r} must override Widget.pack()")
            raise WidgetError(f"Cannot pack () size, this is not a fixed widget: {self!r}")

        if len(size) == 1:
            if Sizing.FLOW in self.sizing():
                # can not announce abstract
                return (*size, self.rows(size, focus))  # type: ignore[attr-defined]  # pylint: disable=no-member

            raise WidgetError(f"Cannot pack (maxcol,) size, this is not a flow widget: {self!r}")

        return size

    @property
    def base_widget(self) -> AbstractWidget:
        """Read-only property that steps through decoration widgets and returns the one at the base.

        This default implementation returns self.
        """
        return self

    @property
    def focus(self) -> AbstractWidget | None:
        """
        Read-only property returning the child widget in focus for container widgets.

        This default implementation always returns ``None``, indicating that this widget has no children.
        """
        return None

    @property
    def focus_position(self) -> typing.Any:
        """Read or set the focus position for container widgets.

        This default implementation raises :exc:`IndexError`,
        making normal widgets fail the same way accessing :attr:`.focus_position` on an empty container widget would.

        :raises IndexError: this widget is not a container widget.
        """
        raise IndexError(f"No focus_position, {self!r} is not a container widget")

    @focus_position.setter
    def focus_position(self, val: typing.Any) -> None:
        """
        Reject setting a focus position: this widget is not a container widget.

        :raises IndexError: this widget is not a container widget.
        """
        raise IndexError(f"No focus_position, {self!r} is not a container widget")

    def __repr__(self) -> str:
        """Return a friendly repr for widgets.

        Designed to be extended by subclasses with _repr_words and _repr_attr methods.
        """
        return split_repr(self)

    def _repr_words(self) -> list[str]:
        words: list[str] = []
        if self.selectable():
            words = ["selectable", *words]
        if self.sizing() and self.sizing() != frozenset([Sizing.FLOW, Sizing.BOX, Sizing.FIXED]):
            words.append("/".join(sorted(self.sizing())))
        return [*words, "widget"]

    def _repr_attrs(self) -> dict[str, typing.Any]:
        return {}

    def keypress(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
        key: str,
    ) -> str | None:
        """Keyboard input handler.

        :param size: See :meth:`Widget.render` for details
        :param key: a single keystroke value; see :ref:`keyboard-input`
        :return: ``None`` if *key* was handled by *key* (the same value passed) if *key* was not handled
        """
        if not self.selectable():
            if hasattr(self, "logger"):
                self.logger.debug(f"keypress sent to non selectable widget {self!r}")
            else:
                warnings.warn(
                    f"Widget {self.__class__.__name__} did not call 'super().__init__()",
                    WidgetWarning,
                    stacklevel=3,
                )
                LOGGER.debug(f"Widget {self!r} is not selectable")
        return key

    def mouse_event(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
        event: str,
        button: int,
        col: int,
        row: int,
        focus: bool,
    ) -> bool | None:
        """Mouse event handler.

        :param size: See :meth:`Widget.render` for details.
        :param event: Values such as ``'mouse press'``, ``'ctrl mouse press'``,
                     ``'mouse release'``, ``'meta mouse release'``,
                     ``'mouse drag'``; see :ref:`mouse-input`
        :param button: 1 through 5 for press events, often 0 for release events
                      (which button was released is often not known)
        :param col: Column of the event, 0 is the left edge of this widget
        :param row: Row of the event, 0 it the top row of this widget
        :param focus: Set to ``True`` if this widget or one of its children is in focus
        :return: ``True`` if the event was handled by this widget, ``False`` otherwise
        """
        if not self.selectable():
            if hasattr(self, "logger"):
                self.logger.debug(f"Widget {self!r} is not selectable")
            else:
                warnings.warn(
                    f"Widget {self.__class__.__name__} not called 'super().__init__()",
                    WidgetWarning,
                    stacklevel=3,
                )
                LOGGER.debug(f"Widget {self!r} is not selectable")
        return False

    def render(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
        focus: bool = False,
    ) -> Canvas:
        """Render widget and produce canvas.

        :param size: One of the following, *maxcol* and *maxrow* are integers > 0:

            (*maxcol*, *maxrow*)
              for box sizing -- the parent chooses the exact
              size of this widget

            (*maxcol*,)
              for flow sizing -- the parent chooses only the
              number of columns for this widget

            ()
              for fixed sizing -- this widget is a fixed size
              which can't be adjusted by the parent
        :param focus: set to ``True`` if this widget or one of its children is in focus

        :returns: A :class:`Canvas` subclass instance containing the rendered content of this widget
        :raises NotImplementedError: the subclass does not implement rendering.

        :class:`Text` widgets return a :class:`TextCanvas` (arbitrary text and display attributes),
        :class:`SolidFill` widgets return a :class:`SolidCanvas` (a single character repeated across the whole surface)
        and container widgets return a :class:`CompositeCanvas` (one or more other canvases arranged arbitrarily).

        If *focus* is ``False``, the returned canvas may not have a cursor position set.

        There is some metaclass magic defined in the :class:`Widget` metaclass :class:`WidgetMeta`
        that causes the result of this method to be cached by :class:`CanvasCache`.
        Later calls will automatically look up the value in the cache first.

        As a small optimization the class variable :attr:`ignore_focus`
        may be defined and set to ``True`` if this widget renders the same
        canvas regardless of the value of the *focus* parameter.

        Any time the content of a widget changes it should call
        :meth:`_invalidate` to remove any cached canvases, or the widget
        may render the cached canvas instead of creating a new one.
        """
        raise NotImplementedError


def fixed_size(size: tuple[()]) -> None:
    """
    Raise ValueError if size != ().

    Used by FixedWidgets to test size parameter.

    :raises ValueError: *size* is not the empty tuple a FIXED widget expects.
    """
    if size:
        raise ValueError(f"FixedWidget takes only () for size.passed: {size!r}")


def delegate_to_widget_mixin(attribute_name: str) -> type[Widget]:
    """Return a mixin class that delegates all standard widget methods to an attribute given by attribute_name.

    This mixin is designed to be used as a superclass of another widget.
    """
    # FIXME: this is so common, let's add proper support for it
    # when layout and rendering are separated

    get_delegate = attrgetter(attribute_name)

    class DelegateToWidgetMixin(Widget):
        """Mixin that forwards widget behaviour to the wrapped widget named by `attribute_name`."""

        no_cache: typing.ClassVar[list[str]] = ["rows"]  # crufty metaclass work-around

        def render(
            self,
            size: tuple[()] | tuple[int] | tuple[int, int],
            focus: bool = False,
        ) -> CompositeCanvas:
            canv = get_delegate(self).render(size, focus=focus)
            return CompositeCanvas(canv)

        @property
        def selectable(self) -> Callable[[], bool]:
            return typing.cast("Callable[[], bool]", get_delegate(self).selectable)

        @property
        def get_cursor_coords(self) -> Callable[[tuple[()] | tuple[int] | tuple[int, int]], tuple[int, int] | None]:
            """Return the delegate widget's `get_cursor_coords` method, if it has one."""
            # TODO(Aleksei):  Get rid of property usage after getting rid of "if getattr"
            return typing.cast(
                "Callable[[tuple[()] | tuple[int] | tuple[int, int]], tuple[int, int] | None]",
                get_delegate(self).get_cursor_coords,
            )

        @property
        def get_pref_col(self) -> Callable[[tuple[()] | tuple[int] | tuple[int, int]], int | None]:
            """Return the delegate widget's `get_pref_col` method, if it has one."""
            # TODO(Aleksei):  Get rid of property usage after getting rid of "if getattr"
            return typing.cast(
                "Callable[[tuple[()] | tuple[int] | tuple[int, int]], int | None]",
                get_delegate(self).get_pref_col,
            )

        def keypress(self, size: tuple[()] | tuple[int] | tuple[int, int], key: str) -> str | None:
            return typing.cast("str | None", get_delegate(self).keypress(size, key))

        @property
        def move_cursor_to_coords(self) -> Callable[[tuple[()] | tuple[int] | tuple[int, int], int, int], bool]:
            """Return the delegate widget's `move_cursor_to_coords` method, if it has one."""
            # TODO(Aleksei):  Get rid of property usage after getting rid of "if getattr"
            return typing.cast(
                "Callable[[tuple[()] | tuple[int] | tuple[int, int], int, int], bool]",
                get_delegate(self).move_cursor_to_coords,
            )

        @property
        def rows(self) -> Callable[[tuple[int], bool], int]:
            """Return the delegate widget's `rows` method."""
            return typing.cast("Callable[[tuple[int], bool], int]", get_delegate(self).rows)

        @property
        def mouse_event(
            self,
        ) -> Callable[[tuple[()] | tuple[int] | tuple[int, int], str, int, int, int, bool], bool | None]:
            # TODO(Aleksei):  Get rid of property usage after getting rid of "if getattr"
            return typing.cast(
                "Callable[[tuple[()] | tuple[int] | tuple[int, int], str, int, int, int, bool], bool | None]",
                get_delegate(self).mouse_event,
            )

        @property
        def sizing(self) -> Callable[[], frozenset[Sizing]]:
            return typing.cast("Callable[[], frozenset[Sizing]]", get_delegate(self).sizing)

        @property
        def pack(self) -> Callable[[tuple[()] | tuple[int] | tuple[int, int], bool], tuple[int, int]]:  # type: ignore[override]
            return typing.cast(
                "Callable[[tuple[()] | tuple[int] | tuple[int, int], bool], tuple[int, int]]",
                get_delegate(self).pack,
            )

    return DelegateToWidgetMixin


class WidgetWrapError(Exception):
    """Error raised for invalid :class:`WidgetWrap` usage."""


class WidgetWrap(
    delegate_to_widget_mixin("_wrapped_widget"),  # type: ignore[misc]
    typing.Generic[WrappedWidget],
):
    """Base class for widgets that compose another widget for their display and behaviour."""

    def __init__(self, w: WrappedWidget) -> None:
        """
        :param w: widget to wrap, stored as self._w

        This object will pass the functions defined in Widget interface
        definition to self._w.

        The purpose of this widget is to provide a base class for
        widgets that compose other widgets for their display and
        behaviour.  The details of that composition should not affect
        users of the subclass.  The subclass may decide to expose some
        of the wrapped widgets by behaving like a ContainerWidget or
        WidgetDecoration, or it may hide them from outside access.
        """
        super().__init__()
        if not isinstance(w, AbstractWidget):
            obj_class_path = f"{w.__class__.__module__}.{w.__class__.__name__}"
            warnings.warn(
                f"{obj_class_path} is not implementing Widget API",
                DeprecationWarning,
                stacklevel=2,
            )
        self._wrapped_widget = w

    @property
    def _w(self) -> WrappedWidget:
        return self._wrapped_widget

    @_w.setter
    def _w(self, new_widget: WrappedWidget) -> None:
        """Change the wrapped widget.

        This is meant to be called only by subclasses.

        >>> size = (10,)
        >>> ww = WidgetWrap(Edit("hello? ", "hi"))
        >>> ww.render(size).text  # ... = b in Python 3
        [...'hello? hi ']
        >>> ww.selectable()
        True
        >>> ww._w = Text("goodbye")  # calls _set_w()
        >>> ww.render(size).text
        [...'goodbye   ']
        >>> ww.selectable()
        False
        """
        self._wrapped_widget = new_widget
        self._invalidate()

    def _set_w(self, w: WrappedWidget) -> None:
        """
        Change the wrapped widget.  This is meant to be called only by subclasses.

        .. deprecated:: 2.2.0
            Assign to the :attr:`WidgetWrap._w` property directly instead.
            This API will be removed in version 5.0.

        >>> from urwid import Edit, Text
        >>> size = (10,)
        >>> ww = WidgetWrap(Edit("hello? ", "hi"))
        >>> ww.render(size).text  # ... = b in Python 3
        [...'hello? hi ']
        >>> ww.selectable()
        True
        >>> ww._w = Text("goodbye")  # calls _set_w()
        >>> ww.render(size).text
        [...'goodbye   ']
        >>> ww.selectable()
        False
        """
        warnings.warn(
            "_set_w is deprecated. Please use 'WidgetWrap._w' property directly. API will be removed in version 5.0.",
            DeprecationWarning,
            stacklevel=2,
        )
        self._wrapped_widget = w
        self._invalidate()


def _test() -> None:
    import doctest

    doctest.testmod()


if __name__ == "__main__":
    _test()
urwid-urwid-69907e4/urwid/widget/widget_decoration.py000066400000000000000000000126561526073734300227720ustar00rootroot00000000000000"""Base classes for decoration widgets that wrap a single child widget."""

from __future__ import annotations

import typing
import warnings

from urwid.canvas import CompositeCanvas

from .widget import AbstractWidget, Widget, WidgetError, WidgetWarning, delegate_to_widget_mixin

if typing.TYPE_CHECKING:
    from typing_extensions import Literal

    from .constants import Sizing


__all__ = (
    "WidgetDecoration",
    "WidgetDisable",
    "WidgetError",
    "WidgetPlaceholder",
    "WidgetWarning",
    "delegate_to_widget_mixin",
)

WrappedWidget = typing.TypeVar("WrappedWidget", bound="AbstractWidget")


class WidgetDecoration(Widget, typing.Generic[WrappedWidget]):  # pylint: disable=abstract-method
    """
    Base class for decoration widgets: widgets that contain one or more widgets and only ever have a single focus.

    This type of widget will affect the display or behaviour of the original_widget,
    but it is not part of determining a chain of focus.

    :param original_widget: the widget being decorated

    Don't actually do this - use a WidgetDecoration subclass instead, these are not real widgets:

    >>> from urwid import Text
    >>> WidgetDecoration(Text("hi"))
    >

    .. warning::
        WidgetDecoration does not implement the ``render`` method.
        Implement it, or forward to the wrapped widget, in the subclass.
    """

    def __init__(self, original_widget: WrappedWidget) -> None:
        """Wrap `original_widget`, warning when it does not implement the Widget API."""
        super().__init__()
        if not isinstance(original_widget, AbstractWidget):
            obj_class_path = f"{original_widget.__class__.__module__}.{original_widget.__class__.__name__}"
            warnings.warn(
                f"{obj_class_path} is not implementing Widget API",
                DeprecationWarning,
                stacklevel=2,
            )
        self._original_widget = original_widget

    def _repr_words(self) -> list[str]:
        return [*super()._repr_words(), repr(self._original_widget)]

    @property
    def original_widget(self) -> WrappedWidget:
        """Return the widget this decoration wraps."""
        return self._original_widget

    @original_widget.setter
    def original_widget(self, original_widget: WrappedWidget) -> None:
        self._original_widget = original_widget
        self._invalidate()

    @property
    def base_widget(self) -> AbstractWidget:
        """
        Return the widget without decorations.

        If there is only one Decoration, then this is the same as original_widget.

        >>> from urwid import Text
        >>> t = Text("hello")
        >>> wd1 = WidgetDecoration(t)
        >>> wd2 = WidgetDecoration(wd1)
        >>> wd3 = WidgetDecoration(wd2)
        >>> wd3.original_widget is wd2
        True
        >>> wd3.base_widget is t
        True
        """
        visited = {self}
        w = self
        while (w := getattr(w, "_original_widget", w)) not in visited:
            visited.add(w)
        return w

    def selectable(self) -> bool:
        """Return whether the wrapped widget can take the input focus."""
        return self._original_widget.selectable()

    def sizing(self) -> frozenset[Sizing]:
        """Return the set of sizing modes supported by the wrapped widget."""
        return self._original_widget.sizing()


class WidgetPlaceholder(
    delegate_to_widget_mixin("_original_widget"),  # type: ignore[misc]
    WidgetDecoration[WrappedWidget],
):
    """Do-nothing decoration widget that can be used for swapping between widgets.

    Swaps happen without modifying the container of this widget.

    This can be useful for making an interface with a number of distinct
    pages or for showing and hiding menu or status bars.

    The widget displayed is stored as the self.original_widget property and
    can be changed by assigning a new widget to it.
    """


class WidgetDisable(WidgetDecoration[WrappedWidget]):
    """Decoration widget that disables interaction with the widget it wraps.

    This widget always passes focus=False to the wrapped widget, even if it somehow does become the focus.
    """

    no_cache: typing.ClassVar[list[str]] = ["rows"]
    ignore_focus = True

    def selectable(self) -> Literal[False]:
        """Return ``False``: a disabled widget never takes the input focus."""
        return False

    def rows(self, size: tuple[int], focus: bool = False) -> int:
        """Return the number of rows the wrapped widget occupies, always passing ``focus=False``."""
        # AttributeError is a valid case
        return self._original_widget.rows(size, False)  # type: ignore[attr-defined,no-any-return]

    def sizing(self) -> frozenset[Sizing]:
        """Return the set of sizing modes supported by the wrapped widget."""
        return self._original_widget.sizing()

    def pack(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
        focus: bool = False,
    ) -> tuple[int, int]:
        """Return the size required by the wrapped widget, always passing ``focus=False``."""
        return self._original_widget.pack(size, False)

    def render(
        self,
        size: tuple[()] | tuple[int] | tuple[int, int],
        focus: bool = False,
    ) -> CompositeCanvas:
        """Render the wrapped widget into a canvas, always passing ``focus=False``."""
        canv = self._original_widget.render(size, False)
        return CompositeCanvas(canv)
urwid-urwid-69907e4/urwid/widget/wimp.py000066400000000000000000000661421526073734300202530ustar00rootroot00000000000000# Urwid Window-Icon-Menu-Pointer-style widget classes
#    Copyright (C) 2004-2011  Ian Ward
#
#    This library is free software; you can redistribute it and/or
#    modify it under the terms of the GNU Lesser General Public
#    License as published by the Free Software Foundation; either
#    version 2.1 of the License, or (at your option) any later version.
#
#    This library 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
#    Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public
#    License along with this library; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
# Urwid web site: https://urwid.org/


"""Simple interactive widgets: selectable icons, check boxes, radio buttons and buttons."""

from __future__ import annotations

import typing

from urwid.canvas import CompositeCanvas
from urwid.command_map import Command
from urwid.signals import connect_signal
from urwid.text_layout import calc_coords
from urwid.util import is_mouse_press

from .columns import Columns
from .constants import Align, WrapMode
from .text import Text
from .widget import WidgetError, WidgetWrap

if typing.TYPE_CHECKING:
    from collections.abc import Callable, MutableSequence

    from typing_extensions import Literal, Self

    from urwid.canvas import TextCanvas
    from urwid.text_layout import TextLayout
    from urwid.util import _TagMarkup

    _T = typing.TypeVar("_T")


class SelectableIcon(Text):
    """Selectable text widget that shows a fixed-position cursor when focused."""

    ignore_focus = False
    _selectable = True

    def __init__(
        self,
        text: _TagMarkup,
        cursor_position: int = 0,
        align: Literal["left", "center", "right"] | Align = Align.LEFT,
        wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode = WrapMode.SPACE,
        layout: TextLayout | None = None,
    ) -> None:
        """Create a text widget that is selectable.

        A cursor is displayed at a fixed location in the text when in focus.
        This widget has no special handling of keyboard or mouse input.

        :param text: markup for this widget; see :class:`Text` for
                     description of text markup
        :param cursor_position: position the cursor will appear in the
                                text when this widget is in focus
        :param align: typically ``'left'``, ``'center'`` or ``'right'``
        :param wrap: typically ``'space'``, ``'any'``, ``'clip'`` or ``'ellipsis'``
        :param layout: defaults to a shared :class:`StandardTextLayout` instance
        """
        super().__init__(text, align=align, wrap=wrap, layout=layout)
        self._cursor_position = cursor_position

    def render(  # type: ignore[override]
        self,
        size: tuple[int] | tuple[()],  # type: ignore[override]
        focus: bool = False,
    ) -> TextCanvas | CompositeCanvas:
        """Render the text content of this widget with a cursor when in focus.

        >>> si = SelectableIcon("[!]")
        >>> si
        
        >>> si.render((4,), focus=True).cursor
        (0, 0)
        >>> si = SelectableIcon("((*))", 2)
        >>> si.render((8,), focus=True).cursor
        (2, 0)
        >>> si.render((2,), focus=True).cursor
        (0, 1)
        >>> si.render(()).cursor
        >>> si.render(()).text
        [b'((*))']
        >>> si.render((), focus=True).cursor
        (2, 0)
        """
        c: TextCanvas | CompositeCanvas = super().render(size, focus)
        if focus:
            # create a new canvas so we can add a cursor
            c = CompositeCanvas(c)
            c.cursor = self.get_cursor_coords(size)
        return c

    def get_cursor_coords(self, size: tuple[int] | tuple[()]) -> tuple[int, int] | None:
        """Return the position of the cursor if visible.

        This method is required for widgets that display a cursor.
        """
        if self._cursor_position > len(self.text):
            return None
        # find out where the cursor will be displayed based on
        # the text layout
        if size:
            (maxcol,) = size
        else:
            maxcol, _ = self.pack()
        trans = self.get_line_translation(maxcol)
        x, y = calc_coords(self.text, trans, self._cursor_position)
        if maxcol <= x:
            return None
        return x, y

    def keypress(
        self,
        size: tuple[int] | tuple[()],  # type: ignore[override]
        key: str,
    ) -> str:
        """No keys are handled by this widget.

        This method is required for selectable widgets.
        """
        return key


class CheckBoxError(WidgetError):
    """Error raised for invalid :class:`CheckBox` usage."""


class CheckBox(WidgetWrap[Columns]):
    """Selectable widget that toggles between checked, unchecked, and mixed states."""

    states: typing.ClassVar[dict[bool | Literal["mixed"], SelectableIcon]] = {
        True: SelectableIcon("[X]", 1),
        False: SelectableIcon("[ ]", 1),
        "mixed": SelectableIcon("[#]", 1),
    }
    reserve_columns = 4

    # allow users of this class to listen for change events
    # sent when the state of this widget is modified
    # (this variable is picked up by the MetaSignals metaclass)
    signals: typing.ClassVar[list[str]] = ["change", "postchange"]

    @typing.overload
    def __init__(
        self,
        label: _TagMarkup,
        state: bool = False,
        has_mixed: typing.Literal[False] = False,
        on_state_change: Callable[[Self, bool, _T], typing.Any] | None = None,
        user_data: _T = ...,
        checked_symbol: str | None = ...,
    ) -> None: ...

    @typing.overload
    def __init__(
        self,
        label: _TagMarkup,
        state: typing.Literal["mixed"] | bool = False,
        has_mixed: typing.Literal[True] = True,
        on_state_change: Callable[[Self, bool | typing.Literal["mixed"], _T], typing.Any] | None = None,
        user_data: _T = ...,
        checked_symbol: str | None = ...,
    ) -> None: ...

    @typing.overload
    def __init__(
        self,
        label: _TagMarkup,
        state: bool = False,
        has_mixed: typing.Literal[False] = False,
        on_state_change: Callable[[Self, bool], typing.Any] | None = None,
        user_data: None = None,
        checked_symbol: str | None = ...,
    ) -> None: ...

    @typing.overload
    def __init__(
        self,
        label: _TagMarkup,
        state: typing.Literal["mixed"] | bool = False,
        has_mixed: typing.Literal[True] = True,
        on_state_change: Callable[[Self, bool | typing.Literal["mixed"]], typing.Any] | None = None,
        user_data: None = None,
        checked_symbol: str | None = ...,
    ) -> None: ...

    def __init__(
        self,
        label: _TagMarkup,
        state: bool | Literal["mixed"] = False,
        has_mixed: typing.Literal[False, True] = False,  # MyPy issue: Literal[True, False] is not equal `bool`
        on_state_change: (
            Callable[[Self, bool, _T], typing.Any]
            | Callable[[Self, bool | typing.Literal["mixed"], _T], typing.Any]
            | Callable[[Self, bool], typing.Any]
            | Callable[[Self, bool | typing.Literal["mixed"]], typing.Any]
            | None
        ) = None,
        user_data: _T | None = None,
        checked_symbol: str | None = None,
    ) -> None:
        """Create the CheckBox widget.

        :param label: markup for check box label
        :param state: False, True or "mixed"
        :param has_mixed: True if "mixed" is a state to cycle through
        :param on_state_change: shorthand for connect_signal()
                                function call for a single callback
        :param user_data: user_data for on_state_change
        :raises ValueError: *state* is not one of the states of this check box.

        ..note:: `pack` method expect, that `Columns` backend widget is not modified from outside

        Signals supported: ``'change'``, ``"postchange"``

        Register signal handler with::

          urwid.connect_signal(check_box, "change", callback, user_data)

        where callback is callback(check_box, new_state [,user_data])
        Unregister signal handlers with::

          urwid.disconnect_signal(check_box, "change", callback, user_data)

        >>> CheckBox("Confirm")
        
        >>> CheckBox("Yogourt", "mixed", True)
        
        >>> cb = CheckBox("Extra onions", True)
        >>> cb
        
        >>> cb.render((20,), focus=True).text
        [b'[X] Extra onions    ']
        >>> CheckBox("Test", None)
        Traceback (most recent call last):
        ...
        ValueError: None not in (True, False, 'mixed')
        """
        if state not in self.states:
            raise ValueError(f"{state!r} not in {tuple(self.states.keys())}")

        self._label = Text(label)
        self.has_mixed = has_mixed

        self._state = state
        if checked_symbol:
            # Copy the class-level table so a custom mark does not leak into other CheckBox instances.
            self.states = {  # type: ignore[misc]
                **self.states,
                True: SelectableIcon(f"[{checked_symbol}]", 1),
            }
        # The old way of listening for a change was to pass the callback
        # in to the constructor.  Just convert it to the new way:
        if on_state_change:
            connect_signal(self, "change", on_state_change, user_data)

        # Initial create expect no callbacks call, create explicit
        super().__init__(
            Columns(
                [(self.reserve_columns, self.states[state]), self._label],
                focus_column=0,
            ),
        )

    def pack(
        self,
        size: tuple[()] | tuple[int] | None = (),
        focus: bool = False,
    ) -> tuple[int, int]:
        r"""Pack for widget.

        :param size: size data. Special case: () - get minimal widget size to fit
        :param focus: widget is focused

        >>> cb = CheckBox("test")
        >>> cb.pack((10,))
        (10, 1)
        >>> cb.pack()
        (8, 1)
        >>> ml_cb = CheckBox("Multi\nline\ncheckbox")
        >>> ml_cb.pack()
        (12, 3)
        >>> ml_cb.pack((), True)
        (12, 3)
        """
        return typing.cast("tuple[int, int]", super().pack(size or (), focus))

    def _repr_words(self) -> list[str]:
        return [*super()._repr_words(), repr(self.label)]

    def _repr_attrs(self) -> dict[str, typing.Any]:
        return {**super()._repr_attrs(), "state": self.state}

    def set_label(self, label: _TagMarkup) -> None:
        """
        Change the check box label.

        :param label: markup for label.  See Text widget for description
            of text markup.

        >>> cb = CheckBox("foo")
        >>> cb
        
        >>> cb.set_label(("bright_attr", "bar"))
        >>> cb
        
        """
        self._label.set_text(label)
        # no need to call self._invalidate(). WidgetWrap takes care of
        # that when self.w changes

    def get_label(self) -> str | bytes:
        """
        Return label text.

        >>> cb = CheckBox("Seriously")
        >>> print(cb.get_label())
        Seriously
        >>> print(cb.label)
        Seriously
        >>> cb.set_label([("bright_attr", "flashy"), " normal"])
        >>> print(cb.label)  #  only text is returned
        flashy normal
        """
        return self._label.text

    label = property(get_label)

    def set_state(
        self,
        state: bool | Literal["mixed"],
        do_callback: bool = True,
    ) -> None:
        """
        Set the CheckBox state.

        :param state: True, False or "mixed"
        :param do_callback: False to suppress signal from this change
        :raises CheckBoxError: *state* is not one of the states of this check box.

        >>> from urwid import disconnect_signal
        >>> changes = []
        >>> def callback_a(user_data, cb, state):
        ...     changes.append("A %r %r" % (state, user_data))
        >>> def callback_b(cb, state):
        ...     changes.append("B %r" % state)
        >>> cb = CheckBox("test", False, False)
        >>> key1 = connect_signal(cb, "change", callback_a, user_args=("user_a",))
        >>> key2 = connect_signal(cb, "change", callback_b)
        >>> cb.set_state(True)  # both callbacks will be triggered
        >>> cb.state
        True
        >>> disconnect_signal(cb, "change", callback_a, user_args=("user_a",))
        >>> cb.state = False
        >>> cb.state
        False
        >>> cb.set_state(True)
        >>> cb.state
        True
        >>> cb.set_state(False, False)  # don't send signal
        >>> changes
        ["A True 'user_a'", 'B True', 'B False', 'B True']
        """
        if self._state == state:
            return

        if state not in self.states:
            raise CheckBoxError(f"{self!r} Invalid state: {state!r}")

        # self._state is None is a special case when the CheckBox
        # has just been created
        old_state = self._state
        if do_callback:
            self._emit("change", state)
        self._state = state
        # rebuild the display widget with the new state
        self._w = Columns([(self.reserve_columns, self.states[state]), self._label], focus_column=0)
        if do_callback:
            self._emit("postchange", old_state)

    def get_state(self) -> bool | Literal["mixed"]:
        """Return the state of the checkbox."""
        return self._state

    state = property(get_state, set_state)

    def keypress(self, size: tuple[int], key: str) -> str | None:
        """
        Toggle state on 'activate' command.

        >>> assert CheckBox._command_map[" "] == "activate"
        >>> assert CheckBox._command_map["enter"] == "activate"
        >>> size = (10,)
        >>> cb = CheckBox("press me")
        >>> cb.state
        False
        >>> cb.keypress(size, " ")
        >>> cb.state
        True
        >>> cb.keypress(size, " ")
        >>> cb.state
        False
        """
        if self._command_map[key] != Command.ACTIVATE:
            return key

        self.toggle_state()
        return None

    def toggle_state(self) -> None:
        """
        Cycle to the next valid state.

        >>> cb = CheckBox("3-state", has_mixed=True)
        >>> cb.state
        False
        >>> cb.toggle_state()
        >>> cb.state
        True
        >>> cb.toggle_state()
        >>> cb.state
        'mixed'
        >>> cb.toggle_state()
        >>> cb.state
        False
        """
        if self.state is False:
            self.set_state(True)
        elif self.state is True:
            if self.has_mixed:
                self.set_state("mixed")
            else:
                self.set_state(False)
        elif self.state == "mixed":
            self.set_state(False)

    def mouse_event(self, size: tuple[int], event: str, button: int, x: int, y: int, focus: bool) -> bool:
        """
        Toggle state on button 1 press.

        >>> size = (20,)
        >>> cb = CheckBox("clickme")
        >>> cb.state
        False
        >>> cb.mouse_event(size, "mouse press", 1, 2, 0, True)
        True
        >>> cb.state
        True
        """
        if button != 1 or not is_mouse_press(event):
            return False
        self.toggle_state()
        return True


class RadioButton(CheckBox):
    """Checkbox-like widget that exclusively selects itself within a shared group list."""

    states: typing.ClassVar[dict[bool | Literal["mixed"], SelectableIcon]] = {
        True: SelectableIcon("(X)", 1),
        False: SelectableIcon("( )", 1),
        "mixed": SelectableIcon("(#)", 1),
    }
    reserve_columns = 4

    @typing.overload
    def __init__(
        self,
        group: MutableSequence[RadioButton],
        label: _TagMarkup,
        state: bool | Literal["first True"] = ...,
        on_state_change: Callable[[Self, bool, _T], typing.Any] | None = None,
        user_data: _T = ...,
    ) -> None: ...

    @typing.overload
    def __init__(
        self,
        group: MutableSequence[RadioButton],
        label: _TagMarkup,
        state: bool | Literal["first True"] = ...,
        on_state_change: Callable[[Self, bool], typing.Any] | None = None,
        user_data: None = None,
    ) -> None: ...

    def __init__(
        self,
        group: MutableSequence[RadioButton],
        label: _TagMarkup,
        state: bool | Literal["first True"] = "first True",
        on_state_change: Callable[[Self, bool, _T], typing.Any] | Callable[[Self, bool], typing.Any] | None = None,
        user_data: _T | None = None,
    ) -> None:
        """Create the RadioButton widget.

        :param group: list for radio buttons in same group
        :param label: markup for radio button label
        :param state: False, True, "mixed" or "first True"
        :param on_state_change: shorthand for connect_signal()
                                function call for a single 'change' callback
        :param user_data: user_data for on_state_change

        ..note:: `pack` method expect, that `Columns` backend widget is not modified from outside

        This function will append the new radio button to group.
        "first True" will set to True if group is empty.

        Signals supported: ``'change'``, ``"postchange"``

        Register signal handler with::

          urwid.connect_signal(radio_button, "change", callback, user_data)

        where callback is callback(radio_button, new_state [,user_data])
        Unregister signal handlers with::

          urwid.disconnect_signal(radio_button, "change", callback, user_data)

        >>> bgroup = []  # button group
        >>> b1 = RadioButton(bgroup, "Agree")
        >>> b2 = RadioButton(bgroup, "Disagree")
        >>> len(bgroup)
        2
        >>> b1
        
        >>> b2
        
        >>> b2.render((15,), focus=True).text  # ... = b in Python 3
        [...'( ) Disagree   ']
        """
        if state == "first True":
            state = not group

        self.group = group
        super().__init__(  # type: ignore[call-overload]
            label,
            state,
            has_mixed=False,
            on_state_change=on_state_change,
            user_data=user_data,
        )
        group.append(self)

    def set_state(self, state: bool | Literal["mixed"], do_callback: bool = True) -> None:
        """
        Set the RadioButton state.

        :param state: True, False or "mixed"

        :param do_callback: False to suppress signal from this change

        If state is True all other radio buttons in the same button
        group will be set to False.

        >>> bgroup = []  # button group
        >>> b1 = RadioButton(bgroup, "Agree")
        >>> b2 = RadioButton(bgroup, "Disagree")
        >>> b3 = RadioButton(bgroup, "Unsure")
        >>> b1.state, b2.state, b3.state
        (True, False, False)
        >>> b2.set_state(True)
        >>> b1.state, b2.state, b3.state
        (False, True, False)
        >>> def relabel_button(radio_button, new_state):
        ...     radio_button.set_label("Think Harder!")
        >>> key = connect_signal(b3, "change", relabel_button)
        >>> b3
        
        >>> b3.set_state(True)  # this will trigger the callback
        >>> b3
        
        """
        if self._state == state:
            return

        super().set_state(state, do_callback)

        # if we're clearing the state we don't have to worry about
        # other buttons in the button group
        if state is not True:
            return

        # clear the state of each other radio button, unless a signal handler has
        # since cleared this one: the selection then belongs to whoever did that
        for cb in self.group:
            if self._state is not True:
                break
            if cb is not self and cb.state:
                cb.state = False

    def toggle_state(self) -> None:
        """
        Set state to True.

        >>> bgroup = []  # button group
        >>> b1 = RadioButton(bgroup, "Agree")
        >>> b2 = RadioButton(bgroup, "Disagree")
        >>> b1.state, b2.state
        (True, False)
        >>> b2.toggle_state()
        >>> b1.state, b2.state
        (False, True)
        >>> b2.toggle_state()
        >>> b1.state, b2.state
        (False, True)
        """
        self.set_state(True)

    # Re-bind so ``radio.state = True`` uses this class's exclusive setter, not CheckBox.state.
    state = property(CheckBox.get_state, set_state)


class Button(WidgetWrap[Columns]):
    """Selectable widget that emits a ``click`` signal when activated."""

    button_left = Text("<")
    button_right = Text(">")

    signals: typing.ClassVar[list[str]] = ["click"]

    @typing.overload
    def __init__(
        self,
        label: _TagMarkup,
        on_press: Callable[[Self, _T], typing.Any] | None = None,
        user_data: _T = ...,
        *,
        align: Literal["left", "center", "right"] | Align = ...,
        wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode = ...,
        layout: TextLayout | None = ...,
    ) -> None: ...

    @typing.overload
    def __init__(
        self,
        label: _TagMarkup,
        on_press: Callable[[Self], typing.Any] | None = None,
        user_data: None = None,
        *,
        align: Literal["left", "center", "right"] | Align = ...,
        wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode = ...,
        layout: TextLayout | None = ...,
    ) -> None: ...

    def __init__(
        self,
        label: _TagMarkup,
        on_press: Callable[[Self, _T], typing.Any] | Callable[[Self], typing.Any] | None = None,
        user_data: _T | None = None,
        *,
        align: Literal["left", "center", "right"] | Align = Align.LEFT,
        wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode = WrapMode.SPACE,
        layout: TextLayout | None = None,
    ) -> None:
        """Create the Button widget.

        :param label: markup for button label
        :param on_press: shorthand for connect_signal()
                         function call for a single callback
        :param user_data: user_data for on_press
        :param align: typically ``'left'``, ``'center'`` or ``'right'``
        :param wrap: typically ``'space'``, ``'any'``, ``'clip'`` or ``'ellipsis'``
        :param layout: defaults to a shared :class:`StandardTextLayout` instance

        ..note:: `pack` method expect, that `Columns` backend widget is not modified from outside

        Signals supported: ``'click'``

        Register signal handler with::

          urwid.connect_signal(button, "click", callback, user_data)

        where callback is callback(button [,user_data])
        Unregister signal handlers with::

          urwid.disconnect_signal(button, "click", callback, user_data)

        >>> from urwid.util import set_temporary_encoding
        >>> Button("Ok")