pax_global_header00006660000000000000000000000064141606754100014516gustar00rootroot0000000000000052 comment=930e6eac1e009fc56d94c14afa56ae5413c74857 bpytop-1.0.68/000077500000000000000000000000001416067541000131275ustar00rootroot00000000000000bpytop-1.0.68/.editorconfig000066400000000000000000000000741416067541000156050ustar00rootroot00000000000000[*.{py,sh,md,cfg,sample}] indent_style = tab indent_size = 4bpytop-1.0.68/.github/000077500000000000000000000000001416067541000144675ustar00rootroot00000000000000bpytop-1.0.68/.github/FUNDING.yml000066400000000000000000000012061416067541000163030ustar00rootroot00000000000000# These are supported funding model platforms github: aristocratos patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] bpytop-1.0.68/.github/ISSUE_TEMPLATE/000077500000000000000000000000001416067541000166525ustar00rootroot00000000000000bpytop-1.0.68/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000015741416067541000213530ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: "[BUG]" labels: bug assignees: aristocratos --- **Describe the bug** [A clear and concise description of what the bug is.] **To Reproduce** [Steps to reproduce the behavior:] **Expected behavior** [A clear and concise description of what you expected to happen.] **Screenshots** [If applicable, add screenshots to help explain your problem.] **Info (please complete the following information):** - bpytop version: `bpytop -v` - psutil version: `bpytop -v` (version 5.7.0 or above is required) - (Linux) Linux distribution and version: - (OSX/FreeBSD) Os release version: - Terminal used: - Font used: - Python version, `python3 --version` (version 3.6 or above is required): **Additional context** contents of `~/.config/bpytop/error.log` (try running bpytop with `--debug` flag if error.log is empty) bpytop-1.0.68/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011571416067541000224030ustar00rootroot00000000000000--- name: Feature request about: Suggest an idea for this project title: "[REQUEST]" labels: enhancement assignees: aristocratos --- **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. bpytop-1.0.68/.github/workflows/000077500000000000000000000000001416067541000165245ustar00rootroot00000000000000bpytop-1.0.68/.github/workflows/testing.yml000066400000000000000000000011531416067541000207240ustar00rootroot00000000000000name: Testing on: [pull_request, push] jobs: build: runs-on: ubuntu-latest if: "!contains(github.event.head_commit.message, '[skip tests]')" strategy: max-parallel: 3 matrix: python-version: [3.7, 3.8, 3.9] steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install tox tox-gh-actions poetry - name: Test with tox run: toxbpytop-1.0.68/.gitignore000066400000000000000000000002501416067541000151140ustar00rootroot00000000000000syntax: glob *.al *.bak *.egg-info *.la *.lo *.o *.orig *.pyc *.pyd *.rej *.so *.swp .failed-tests.txt .cache/ .idea/ .tox/ build/ dist/ __pycache__ .mypy_cache .vscodebpytop-1.0.68/.travis.yml000066400000000000000000000004471416067541000152450ustar00rootroot00000000000000language: python python: - "3.6" - "3.7" - "3.8" jobs: include: - name: "mypy" python: 3.8 env: - TOXENV=mypy - name: "pylint" python: 3.8 env: - TOXENV=pylint before_install: pip install poetry install: pip install tox-travis script: tox bpytop-1.0.68/CHANGELOG.md000066400000000000000000000317531416067541000147510ustar00rootroot00000000000000# Changelog ## v1.0.68 * Fixed: typos discovered by codespell, by @cclauss * Added: search processes using vim keybinds, by @jedi2610 * Fixed: Removed a simple consider-using-in pitfall case, by @NaelsonDouglas * Added: New theme gruvbox_dark_v2, by @pietryszak * Fixed: Implement strtobool over distutils strtobool, by @RCristiano ## v1.0.67 * Fixed: Removed not needed escape character replacements * Fixed: Themes missing when installing with pip3 * Fixed: Color in-range check, by @GerbenWelter ## v1.0.66 * Fixed: Program not stalling when system time is changed and regular update of current timezone * Fixed: NetBox not redrawing when network interface is removed, by @UmarJ * Fixed: Some typos ## v1.0.65 * Fixed: Removed degrees symbol from Kelvin scale, by @jrbergen * Fixed: Mouse buttons not working in netbox when changing interface * Fixed: q key not working when terminal size warning is showed * Fixed: Cleanup of unused libraries and other small fixes ## v1.0.64 * Changed: Init screen not shown by default * Fixed: Broken cleanup in ProcBox class * Fixed: cpu frequency type change in psutil 5.8.1 * Added: Option to toggle CPU frequency * Fixed: Check for config in /usr/local/etc instead of /etc on BSD ## v1.0.63 * Added: Options for choosing temperature scale and re-added support for negative celsius temps * Changed: Cpu values above 0 will always register on the graphs ## v1.0.62 * Fixed: Support cpus with non-sequential core ids, patch by @ErwinJunge * Added: New theme Adapta, by @olokelo * Changed: Net graphs will now round up any value above 0 to register on graph ## v1.0.61 * Added: Vim keys (h, j, k, l) for browsing and moved help to shift+h * Changed: Size constraints now adapts to currently shown boxes ## v1.0.60 * Added: Ignore input unicode decode errors * Fixed: Wrong letter in "io" highlighted * Fixed: Crash on missing psutil.disk_usage * Added: Toggle for IO graphs in regular disk usage mode * Added: Toggle for uptime and uptime added as a option for the clock formatting * Added: Ability choose cpu graph attributes and split up upper and lower part * Added: Ability to toggle one big CPU graph instead of two combined graphs * Added: IP address to net box ## v1.0.59 * Fixed: Crash on missing disks * Fixed: IO stats text transparency ## v1.0.58 * Added: Disks io stat graphs and a dedicated io mode for disks box * Fixed: Better detection for disk io stats including multiple disks for OsX * Changed: Terminate, Kill, Interrupt shortcuts now only uses uppercase T, K, I * Changed: Process filtering changed to non case-sensitive, patch by @UmarJ * Case-sensitive proc filtering using uppercase F * Changed: Get CPU load average from psutil module instead of os module, patch by @araczkowski * Fixed: Misc bugs ## v1.0.57 * Fixed: proc_sorting option counter not updating in menu, by @UmarJ * Added: Support for non truecolor terminals through 24-bit to 256-color conversion * Activate by setting "truecolor" variable to False or starting with "-lc/--low-color" argument ## v1.0.56 * Fixed: units_to_bytes returning 0 if input value <10 and in bits * Added: Testing for some functions and classes * Added: net_iface variable to set startup network interface, by @raTmole * Added: use_fstab variable to get the disk list from /etc/fstab, by @BrHal * Added: Categories in Options menu and only_physical option for disks ## v1.0.55 * Fixed: Disks usage and free meters not updating unless resized * Changed: All boxes are now toggeable with key 1-4, start argument -b/--boxes and config variable shown_boxes. * Changed: Moved testing from Travis CI to Github workflow ## v1.0.54 * Fixed: Added nullfs filesystem to auto exclude from disks list * Fixed: Process box not updating on window resize ## v1.0.53 * Added: Process update multiplier (only update processes every X times) to reduce cpu usage (set to 2 by default) * Changed: Patch for faster loading of config file, by @rohithill * Added: Network interface list now updates automatically, by @UmarJ * Notice: Bumped minimum python version to 3.7 because of unicode issues in 3.6 * Added: pylint disable=unsubscriptable-object because of python 3.9 issue * Changed: Default theme now has a black background * Fixed: Crash if bpytop.conf exists but don't have update_ms variable set ## v1.0.52 * Fixed: Removed "/sys/class/power_supply" check for FreeBSD and OsX ## v1.0.51 * Fixed: Text argument in subprocess not working on python 3.6 * Changed: Disks filtering now uses full mountpoint path for better accuracy * Fixed: Disable battery detection if /sys/class/power_supply is missing to avoid exception is psutil * Fixed: Catch faulty temperature readings instead of crashing * Changed: psutil update to 5.8.0 in pypi package (fixes errors on apple silicon cpus) ## v1.0.50 * Fixed: Correction for missing coretemp values * Fixed: Cpu temp calculation from cores if missing and better multi cpu temp support * Added: New theme dusklight, by @drazil100 ## v1.0.49 * Fixed: Missing default values for cpu temp high and crit ## v1.0.48 * Added: Sync clock to timer if timer = 1000ms * Fixed: Wrong coretemp mapping when missing package id 0 * Fixed: Sizing when coretemp is hidden * Added: Link to Terminess Powerline with included braille symbols in README.md ## v1.0.47 * Added: Testing, by @ErwinJunge * Added: Theme matcha-dark-sea, by @TheCynicalLiger * Fixed: New type errors for mypy v 0.790 * Added: pylint and mypy test with tox, by @ErwinJunge ## v1.0.46 * Changed: psutil update to 5.7.3 in pypi package * Fixed: Better sensor and temperature detection ## v1.0.45 * Fixed: Missing temps if high or crit is None, by @TheComputerGuy96 * Changed: Some refactoring by @dpshelio * Added: Proper mapping for correct coretemp display and added toggle for coretemp * Fixed: Cleanup of escaped characters in process argument string ## v1.0.44 * Added: Spread CPUs across columns evenly if possible, by @ErwinJunge * Added: Additional crash fixes for graph and swap toggles ## v1.0.43 * Fixed: Battery meter not clearing properly when disabled * Fixed: Correction for broken cpu high and cpu critical temps * Fixed: get_cpu_name() function for some Xeon cpus * Fixed: Additional error handling to prevent crashes from graph and swap toggles ## v1.0.42 * Fixed: Battery status not using same sensors as psutil * Added: Stripping of .local from /host clock format * Fixed: Battery clear if removed ## v1.0.41 * Skipped due to pypi - github versioning error ## v1.0.40 * Fixed: Title leading whitespace * Fixed: Battery meter crash on non Linux systems ## v1.0.39 * Fixed: Manual sensor selection screen refresh * Fixed: Rare swap toggle crash * Fixed: Clock and battery placement and sizing ## v1.0.38 * Fixed: Cpu sensor check when changing from manual sensor to Auto * Fixed: Menu collection timeout and menu background update stall * Added: Custom options for clock formatting: hostname and username ## v1.0.37 * Fixed: Swap toggle rare crash * Fixed: Cpu sensor option to trigger temp toggle if check temp is true ## v1.0.36 * Added: Rounding for floating_humanizer() short option * Fixed: Cpu temp not showing when manually selected and not auto detected * Fixed Crash during theme change ## v1.0.35 * Fixed: Decimal placement in floating_humanizer() function ## v1.0.34 * Changed: Improvement on cpu name detection * Added: Option to choose cpu temperature sensor * Fixed: Battery meter adaptation ## v1.0.33 * Changed: Improvement on osx cpu temperature collection with coretemp * Fixed: Battery stats crash and better battery status detection * README: coretemp install instructions by @hacker1024 * README: Added notice about font problems and possible solutions ## v1.0.32 * Added: Symbol for battery inactive * Fixed: Cpu model name exception for certain xeon cpus * Fixed: Exception when sending signal using uppercase T, K, I * Fixed: Battery meter placement calculation correction * Added: Support for OSX cpu core temperatures via coretemp program ## v1.0.31 * Fixed: Battery meter redraw after terminal resize * Fixed: Battery meter additional fixes * Fixed: Cpu temp color wrong on small sizes ## v1.0.30 * Changed: Argument parsing using argparse * Fixed: Hide battery time when not known ## v1.0.29 * Fixed: Battery percent converted to integer and battery time hidden at 100% level ## v1.0.28 * Fixed: Battery meter causing crash when connecting/disconnecting battery * README: Added more repositories ## v1.0.27 * Added: kyli0x theme by @kyli0x * Added: Battery meter and stats * Added: Option to change the tree view auto collapse depth ## v1.0.26 * Fixed: Cpu temp color index crash * Fixed: Start from virtualenv crash ## v1.0.25 * Added: More sizing adaptation for processes * Fixed: Clock centering ## v1.0.24 * Fixed: "view_mode" option entry format * Fixed: Help menu entries ## v1.0.23 * Added: View mode toggle with 3 presets, "full", "proc" and "stat" * Added: Rescaling of net stat box width on smaller terminal sizes * Changed: Net box height slight increase, mem/disks box height slight decrease * Fixed: Some element placement fixes by @RedBearAK * Fixed: "delete" and "filter" mouse click area misaligned * Added: Option to sync network scaling between download and upload ## v1.0.22 * Some refactoring and cleanup * README: Info for debian package * Added: Theme search path for snap install * README: Updated snap install info ## v1.0.21 * Fixed: Clean excess whitespace from CPU model name, by @RedBearAK * Changed: README.md absolute paths to work on PyPi ## v1.0.20 * Release bump to fix pypi and source version mismatch ## v1.0.19 * Changed: net_auto variable now default to True * Fixed: Sorting out negative cpu temperature values from bad sensors ## v1.0.18 * Fixed: Init screen and error log level when starting from pip installation ## v1.0.17 * Added: Option to toggle theme background color * Added: Dracula theme by @AethanFoot * Added: PyPi theme install and path detection * Added: PyPi packaging with poetry by @cjolowicz * Added: Error checking for net_download and net_upload config values * Added: psutil outdated warning message * Changed: Expanded cpu name detection ## v1.0.16 * Fixed: net_upload variable not working * Added: Ability to expand/collapse processes in the tree view ## v1.0.15 * Added: Network graph color gradient bandwidth option by @drazil100 * Added: cpu_thermal sensor detection for raspberri pi cpu temp * Fixed: Single color graphs crash ## v1.0.14 * Added: New theme values "graph_text", "meter_bg", "process_start", "process_mid" and "process_end", see default_black.theme for reference. * Updated: default_black.theme with new values * Updated: monokai.theme and gruvbox_dark.theme with "graph_text" value. ## v1.0.13 * Fixed: Cpu usage bug when showing tree and memory in percent * Fixed: Check for minimum terminal size at start when init screen is enabled ## v1.0.12 * Fixed: Cpu high and cpu crit for osx and raspberry pi ## v1.0.11 * Fixed: getsensors detection of vcgencmd * Fixed: Load AVG being drawn outside box on small sizes * Fixed: Slowdown when showing memory in percent instead of bytes * Fixed: Cpu temperature colors not converted to percent of cpu critical temp * Fixed: Crash on sorting change when lacking permissions ## v1.0.10 * Fixed: Raspberry pi cpu temps, actually fixed this time... ## v1.0.9 * Fixed: Raspberry pi cpu temp, again. ## v1.0.8 * Added: Set terminal title at start * Added: Update checker, can be toggled off in options menu * Added: Option to show memory in bytes for processes, enabled by default * Added: Options to set custom network graphs minimum scaling values and a "auto" button to toggle manual and default values. * Fixed: Failure to detect cpu temp on raspberry pi * Changed: Layout changes to cpu box ## v1.0.7 * Changed: Info box now restores last selection on close * Fixed: Crash when starting with show_disks=False ## v1.0.6 * Fixed: Cpu temps index error on uneven temp collection * Fixed: No cpu percent in info box when filtering ## v1.0.5 * Fixed: Attribute typo in detailed process collection ## v1.0.4 * Fixed: Crash when filtering and showing info box * Added: Improved cpu temperature detection * Fixed: Broken cpu box layout on high core count and change to default layout * Changed: Selection now returns to last selection when pressing down from info box ## v1.0.3 * Fixed: Crash on detailed info when showing tree * Fixed: Incorrect sorting for memory * FIxed: Removed unsupported osx psutil values * Changed: Removed shift modifiers for some keys and removed redundant toggles ## v1.0.2 * Added: IndexError catch for cpu temperature collection * Fixed: net_io_counters() not iterating over itself * Fixed: Clear mouse queue to avoid accidental character interpretation * Added: "/etc/bpytop.conf" as default seed for config file creation if it exists. * Added: Error handling for exception in psutil.cpu_freq() ## v1.0.1 * Fixed: Bad assumption of cpu model name string contents. * Added: Exception catch for psutil io_counters error caused by psutil < 5.7.0 and Linux kernel >= 5 * Added: Error handling for psutil.net_io_counters() errors. ## v1.0.0 * First release * Missing update checker bpytop-1.0.68/CODE_OF_CONDUCT.md000066400000000000000000000064251416067541000157350ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at admin@qvantnet.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faqbpytop-1.0.68/CONTRIBUTING.md000066400000000000000000000020701416067541000153570ustar00rootroot00000000000000# Contributing guidelines ## When submitting pull requests * Explain your thinking in why a change or addition is needed. * Is it a requested change or feature? * If not, open a feature request to get feedback before making a pull request. * Split up multiple unrelated changes in multiple pull requests. * If it's a fix for a unreported bug, make a bug report and link the pull request. * Purely cosmetic changes won't be accepted without a very good explanation of its value. * (Some design choices are for better configurability of syntax highlighting.) ## Formatting ### Follow the current syntax design * Indent type: Tabs * Tab size: 4 ## Optimization * Avoid writing to disk if possible. * Make sure variables/arrays are cleaned up if not reused. * Compare cpu and memory usage with and without your code and look for alternatives if they cause a noticeable negative impact. For questions contact Aristocratos at admin@qvantnet.com For proposing changes to this document create a [new issue](https://github.com/aristocratos/bashtop/issues/new/choose). bpytop-1.0.68/Imgs/000077500000000000000000000000001416067541000140265ustar00rootroot00000000000000bpytop-1.0.68/Imgs/init.png000066400000000000000000002222571416067541000155110ustar00rootroot00000000000000PNG  IHDRM@l7vsBITOtEXtSoftwaremate-screenshotȖJ IDATx{lYU'9y3Un*^@!H#Q|}"SC-AFD[ƞvGViFV[^ (nT|W@=*̈s揼yD抽V>]?0\w~ʈp8 p8ñH9p8c098p82}p8X(TUUɲZB0..%IwaT+&c0]jnm9p8cPp8ñXq8p8 ~΢}\)!C8)y zݩo 2y5pӿp8p8 Y8p8ł?p8p,ʿpF*Ml):#'#s)npɼ %:8OpAw Qf0LޅvMhk9UPA]Sr{@2N:8Š.g+Rt'!qhy0@T3YJGȻpn82~p8X,sp88p8E}NeY=S )sJ*ܛ>R{2l N xrJ.tp(:+'#yopɼ u1Jn |u4y5PΪPzACX.%l&)-ꐮĞV EaC:Teiތ9vp:xґؓow[dޅ:(8Oz۠+- %g t]UpB|A0~p/fjLӿؓ8j7.wvDl,"Ohjct&Q*ܚp8X(:p8D1h>WzG!'¬~pZȘSZ:8J|v%d:=.wԓ'm@[a ɷxB-(tTJy|.U.+l)-zDM"RC'#qg޿L=Y@!m#f)#Z6~pZȘSTDx)Y`>,up-\2 'KOzmVgm|L|'TۂRLG5j'ޚ*T'\8-l)u)%&“Nbe)lfSnyL=Y|#+1GOzD'&٢oW}XBj<9p8c࿟p8p8>p8p,\~@A5~\8-3 %R+Iޫd&ʓNbe;n,`Zhe -6@zIot ۗM)$߮ I6TG֊Aq aV,-o.62.+Yj#x)Yl7j[3Yn1b'KOzmVggNؾlvB/Pm J1Y9UUe w B DGJ-:9nId|jDw](TOx>ѕcN9 sأ0U_wB"ڵd^\u aeQ.lwn;uB/0]KuDW' !8p8s,˲l׊~m-l;Crn&!vzd q(O:%p%]ȧ"yr&T=k յq¡+ . -xc%"KZ0䅔OOy0A|nGtj Lz?#آaA!Ly&ƓN \IrG޶bX8mc6?-[pbB ]w/Y59:x,^% ,s*oMaZخw܊MBJ)0!6QtJJ0ݔБ:hJOF[NuJ յqh>ѕqGp8p,9Yy MaZ"wЂ'fr16QtJJO+t[Xڍ=""yEa ;JB g1(9LҊBJM^b3̷2L VVWl<O^~lT'0wpTXK6g7 6VBmq³Cq++E;r\PNo j\^"wЂ'fr16QtJJO+t[Xڍ=""yEa ;JBdP~ci,Rk!NZY5p8ñX֊ +nUhlT2M!bWWvAd q(O:%p%]H':K' i1XZ]NуV7+vpx%&Ñu+ )Y lskyVn-˲, {fYF'1<#V$d 1tJJOȫSu|c9K^nQXVY4H pu鰯܎a ) /pVzf Ua!T5p8ñP u8p8 s׆+=ૅ_8_64ygw}FUf&ق|&ʓN \IIﺎir>cvG6zX{7.=h{kNq2?Qa[̻,! G֭(dZ> c&N VֆE6jԦ_ A$>P0v̊R?lA>fN \I "Byw%r060sdP]G8v+cT DƐj&g gG:&ep8p8 B~֯D2<WZB&SW҅|ȧP=NxF煽̻iDZǣ'p9W"yyW<%H2WVwsV{?X /ql*{-dnֱj $[Dy)+Bz>]ב^_Pd=/dM#B;/Gi7+Ƒż+|p$yN+D4qETUY߇5kk!dy=!VAd q(O:%p%]H'| -MH{^KɼF#/T{3o[MyAHVlʣGSܬLTG2w1G-(#$?u;X4ܷ|Z9CypMaUǡ*s!dBG!dE3jSW҅|#ŖP=Nb:Єy%0d)nx>ѕqGTG2w1LC)#d@8y[s8p8p8ՕKPCz CH 2exqSfڼjՅI0vFmCt6a~7S?Z6z]zW͐l[cDsdS#MY(%kL_be8),Bm&xX9ȳskYeYvQk'ۄ4wku-6(bΨʓN \IH1B8y^hBiA).9g^˦# ºǁѓlQ8%TȒyź3"&¦5%Q/ +CU5>e@Ma'nS&ϦE'oQ#'\6ݢOzt Z ӂR\s~?M5l*AW%;PPaq-_# ֔ 2 JPGۘ+p,$|ؽZg L+ouei3&׃0CnשOERWNБ0vFmCt6a%ya0(ETׁ)Ņh3$Yj7M5d *ddӗy~,l Om)cz)gfX9BjԚ/+nûOEq)d#a$y)+Bz>Yg#BXw:ЄDӂR\sDϼMEG<B:Pu#Fya2ȣ8kda;y4 G6Aj01Ի#d"K!p8X(4+,RSX Aq!Z]8 ,BGIFy)+Bz>%/l%:Ӯo2V{ǹR:ۛA2dAn2Yؤ6*3b1tXdr(8YaAqT{=5u-6Aua5I:H2Z8:%p%]HgmàS]7;&*[j7M5d ,)vEݓͻpda>S 3Ƣ(Eh+,ԿSe]SXeU6= nSfKd#a$%/M%: Y2E).9g^˦# Z熪TN\NPMFOm 7}uJE/Y ֈp8سtf싗ŋr,<җ؛p8!8z&ϕ޷}u)O?/o"̷_g|!7>9}uO*TdMP"uZ-:%t6xZ!/M%hw];mJhS8V{3Nq@ϒbYb "oqvH2( /Cx-Lrϩ*waj)Dx7V4p,RFޢ1, µA^8Jtu,!]zNbޅ#6Z{dӁ #6 c o^ ӱsx\ӥme"Vrsxǎ@X8VD&Ķ1G, µA^8JtuVBab 1]CSSarz ]@F69w{=MF$,)`~ hDtf\"u„v2ꈑ%t6xZ!/m%8^&fmz*07ttJ%ŮĵUՈ`|e@x-ORyk6֖K[Sgե2H M0_j/n{A!]h"v%M^Dtr$sO2ꈑ%t6xZ!/m%h/0vzXxm*֝NޔPf<tJ%5z-jkL+W#F ɻƴ۴[ݻE筕'4U9mr\:&h"v%MVDtr$sD'J)\䅣͡DWg%D&B Mho,vN hoM5x-j,HqLOvVx {!QUF[y[s8p8pXnmel0_8t`}%ۺcD@M0O+&"mZΠDɋΓ^%#FQG,I dA^8 J ,% 3^aT;)OpOdvۛN ҡum ɛ^cZ1LOSQ)j+skYye ag/yB$JwPn]Ii;&/":Oz9xI2/ dA^8 Jtu^Bab -6N'oӢ؅[ej{131Q>p筕x4*7HMa5mƅ(ˆx㔄%9pDE 5ڊ[5=$[쟨w-mzB,ڛ>qa6tʼnl T|/uZxD G+5Q*~p8X( _p8p8q8p8yky1(<>)nUV]f-$eQ/vz;hDW|Qw4wzyBtXgޢ넔a'n/b6-(ټN'oS(T,xx m+0M)I?SO^>ŠXljf>J:D-!vjeyUskYe{phM ѩMP%΋ q݆%wzyMy bBx hF,16ypd4ody0q ^(8rhce|+aR!Y >Y եd82ӄqJ`ћAXb1:(^bOkkUd?A8nC߂;<x YۤCf6}it8d&p8p8ʨ*GGuUV 6Q-S&P;Z#Fш ,EFN/1O'YI  xjoW} [/<rƘڤ_9iݓ3a>^ y5KEQ;W9z𾥩 a1C•p,Jf-D m2yb) KsDy1:˼P'&%-WHS\K{'(<4#VnSSNx{G3.<&ZQ1ykfI aU67rT W@W6k!j_hΓ;]=NIXb#z[Ԉtu^BF>i6)h¥G2= D_ڻ/GItd,j3cp_҆OpGɼw $}icfM@x{d\c@uTKV-y#Y<u,AaUUe9.K|% :!nΓ;])1="&u7+s|Y8`KKɐK{'(<4+nk cu!F:$[s!Yv,j}Ny/]ë!,/<YzoyN6fYIIW(qJL(nsZKqϰ'N:$[GYy`{NUF[y9)hkk<Е ZtJpDɋNDwD:#e ϟOzyBa9fDᾔ xZދw_x n}͈&@6-l*f>N:$[GY9e>WgQCX^|x5Tr@p4#uJ:O:="z,J0!(X'#_ Kɐ'mK{'C_x@b ЅMw jS'#QB5jgp8Np;+Ҫ?Sԕ 4o[WnX- Y/mcwA7cp8I0s|p8p 7y VCo aۯ"CSO,tG`@uVtu:%#zDY0!8%c6qFp~vМ-n`Bg=""PSw=^M5Y #PIc6E>yځcǎY_Κ|c/JN0u{-d:%d8Sb)#΢ĈGYBp^bPX+l8FpX~vЌ-n`Bg=""Xkjjy ϻZ |Zy)<ꦰom^ QYb+@AﵐlL΋NDwD:#e ϟOzyBa9"+pv8,@?v;x hF70!Xb5ydz5[݂`-yk$5:c%ݟI>o?omN?obӟK[y:~ph=nU!5pf>#0u:/vFQJtG5(1Q'cvp79/ ܨ ',]px hF70!XevrWw=~`\xxYb݂@5ڣR]Cy^lo ay^Tya+9Aamryb)#΢ A3I/1K(,Q[7ΫCwƠ:(AGj%x`Rpw8Ru~_gϞ7<ӻ?%ߵsgZ' !r-|;t\r`;BvNmKٔLtuМ-nUfrz5%X`lykN\t!H +CKcAۯr/@G{{FY}'3bЯ[SMh%Y0 wZݻ8%_x@JϭeyQaݚ,ϋȲ ]z!p LpDIND/1iP s>\1sip-M-vީbk}x;sp:nxDF{&%u*joz:x FZ o洖Nq޽MQecSa6՟[;wCSk g\*d%:}"d9d8SDx%S2M5'yBѕ !/; y02ZZ(nqDGh9LM6Q JQnފN8L$ #H k'#)u\`-]ږ.nqr- 5R;P}9P fjabGm{ /wV'y"@Jp*\x {5/3t:z;;h;SLWg9+6n\#V> JL3X$tKZ YFzڡؽQG J67ڜR =M[xɈqg%D8[9QW>"G~sDΛ\ Gw}=?;677U !կ~׻޵!poRg̓ z/mCg'jKp8W{K&;-] XEvr N8L$ #H kdOzD%:yܫk#[.Զtw/`KC$]&@i뽍8@^5v2' gaxڽjauFCWTxRym;N$+qJ%"93ey"7[ G2xE`m>stGJ&JD0gx4)X0Jpzykc˳_K0_?vա{yhs(PtJ6%KO~iUJ #tX"qJz>M3X$|UB&^-N/Ynvyyc`yo%"Fw^{ ;94ujuZE5YQEDn:%E GRS i6n\#|"|f>կ檾焀g;&+N DĠz5X;1S=/sk!TSKs+MAh(YxXj VAX"0JtFx9}}dzLz#_w^BUhmBX\H޼}ɢp::GHw:, kwgvrh0Jppp, N\`9\/$ aݝMás_stu{Ƈ&|#w}Tb~GRS#l< 12Ou,)L=o`7!=G7s`;|#!#8n֔Zm{;F:#>o;l2Dw8 #Qy{NUr;Ц SRp,u4Eo:-]Wp;FzڡO1u\u"Xxo%"%@SQBՇa#ȋ`PL?)~xn:%E GRS i8bܮF|y Aϧi޼ӑ%z-ݚ5:P}>;@:,/ o,v1S(9ѫ>}JS),',Au ))d8J:z<4Eo(67-il֔Fցx yy։`ὕx}Ixcir:f; e_5 w n:%E GRSTkaJ ºF|y Aϧi޼auZ["Bw0zC_k;PN DĠĻ@v{_X@(!?pHqCԕ_WwgϞ7V &Ko!n=yINA n%p8+)Vy55>*fJ[tJz69YZ ZA׈O:Owf6UrzȊ`;8Y<&;- Eb65R;PvwߑpWۓYJ_[ԝuONBpSf/-筝>}=C]}c([NI&Qaz VAX׈O:O!4|n "}Ǯ>mczxNy- E65:PtߑpړyJ_[dԝwONB* Տ^3޼x!oMuCX/][%PtJz69 qOj vᒂ5tXWүX'f^#Ej{2@w F[nz;lM]&=PMdca5FZ%a]#z><4U2H_8޼aA^kKB`џMzC_kɼ/-l2λ''ko!Tw'9:;|~'N/}[NIf1d㩃{VjaúF|m*=5Э!TDq{ o7N>[HxtMkɼ1xC1"F E{ UbPb#skY^O!̲(U>qlr%KK S5R+1lݶF|y AϧmMpM7%CЬ|f*BE 6:s.3!=!|NwPGU'y$>9*xª pβ C)^3pRTqOj cWv)켓;vXRyWۓYJ_[4[nz1[}\=n ǏGf-` 3;&:Z30RԂuOV*"}]N>ǮށbqHq]kO(}na`j&ko!Tcp8KKKbMa>+Ma 3;&ń<:kD'|fנWSXw&Ғe[]wrA?vC7Z{2@w U3Y{ 83E>'w3g>4lk ǯ9s!n Ghy6BB< f`dW5Sz>M3k[%CkؘQW}Џ]08x֞;P&xLB*NlG7筕[ƣrjUCX[hh-,aa"l+䅘kN@J0=tBi^^MaYʭM#3 켓vXRyWۓYJ_[4[nz1[}P眻s{51t,acW2Of`n%}X׈O:O!4|n "N8~(g޵dށ6 ƫf"MUq:f0VcQo[ zf{{;G|p8p,K+Kym]j~!̊奬mm-` e\)$Qo`³*:E_׈Oq$Sœf> 50Ao Q@_xjl6'MڞLCݮw cU/i 7U阭T=wkΜ:4XX?v5gN\{[f -"Nq -H} ~jUs`pHy=>k׻MFٗLSUU rk a5tBo?L [fuLz>M3 ۅP^)`仼)/ -"Nq -H} ~U̻֞LCĵ&xl*Nlp>'8yw;0ՙCwK{ZpYDcԊH1!uwm: tJ<8kDgCϧizW'򨸃䅝><'Ft!QsO:x֞LCݮ8#iѴɠUb#RcW˳;s,/Aȧ>nrcr,acW QTlF@J;m)v.g p Sq-H} ~jU'[ج=>k׻MFٗ4H< 2N ]pԜbCk9zh0^5MSEhј#Uѥrsjm ѥ& T#fdSk>`y`T;]Âg4-(k?Sn~^li/問O=կo|Wc> g!ݷCDֈ.d8* )>'w@&x4=LazS=Y?op8Xz[?p7>oz?-n| p(!\_׼O?|yaqqy׿{N! ?gV޲_M7}O=cey߿5p.d_~m,om˯}Uǿ'mO_⥯t80wS?uw~ ;8pVG/}k[jsO]wPyaf<d]AgӍ3ȃ}:-qO<|[}|s`+ kD25X됢 pOn{ ôhd*1ѣ1k?d`0(.\4#&)=uL.Y{LdЊw2`N ]qmPU(֟z'~뽟_}~Od7>yP<'[(?+7{({cWGǾx_=iO{ -?Ճw{a[~;/XحΟyS6;~/=o|0y?|>4ԓxdK'}/|7gL^IB&̳֞6UނyVn-4Y-}:gg\n?<_y}OM'^^c?zۇ'_?5kM'{oᵯ~So޸&6?"ܟm{3V?!7?c)~Guo~ßx?y0{I~w~쓷v'oOo{,3r4$)p-l k<;}naQ/^5;x-Hjds8 TUYuc"r4PU8w'?"U7\?ȫ˻?_y=s|?L}lʵooY?_/S7Ws+ o_x:=nTq_k7NM]uVwV,6㱣6ꭠ9ka즩Kp%=lZy6[)qخ: -Y,oo%?kײ_n>qnx?_>n|3Ýkc'WS_Ɵ/BX~qMwG [;!;' _>\m\^'$|?IO:Zgj)q˧%gKL~׭ۿz+g٧\Oх Gvh#j0LMd׭P]sr4-\4#&)=uJ.Y{L6Nyf_rZgI^5.{7?peC_極^/6 ˇ޲O}˞}!z֍o~2᫆_OύBl񲷾o? Uˏ|oW?˿,>{s=7O~3>W\?x;'?R̗-zQ?m$R  uDZ\#ٝd-Gy?Wp8|ګ|&s~Y??;'>_7Mg}S'?_~_;q2??{^wӯ]|Yo}r!k‹d!էőDBKO?Sԧ>?} e4?wZ:{vD9V:G09`iyiPS.H MES БME&• f_rv0KĠV%,nB;Z눴 Y@ݣ¼`4bǜ9MՏzvs/2im{SHq\ БM`2ȃ,NJhv pBd;}xi}F$ Ӣi!u+.T48pc9vS.]8M}ƶ)~=I>MFLc7M/"#-&|vḭJhv pBd;"bg}IDhwWTHE&CV\Ho]Y9p8c0 Ot8p8^8p8EsB<.Y90 &'Äg$jmSNJHH& |vi9Nz^MZa&k, y(`qAcѴ,J:"iѴɐ캅(ep8\[;pcϜ>x08x1לX< 6ԡJq1fPy:x i䁞O v;f+%â;f2:   1hZJ:#iѴɐ*R;ش `' mR8,Za&o, y(`qmg}Iiy޵0LMd׭PR5x|^ykp8c0'zc`qsS * ϧB2 LI2l3wd{s;w&Gxz{y޹s!ё)iqa5@R4MR˵hUPhmJ] KAdtr N. eYMtgP E:KV<ʀtEں$נ(cF^HFb-υ2DP4MA$9ܚk!CЦ*Ng^ "cH',|2*;U᠕YH/yikOv15 ʀtV=HLS={;n(L xLM\?"5տ`0L`l.g F!ωLIu yK!=.)Ұ8UnS% B'f{D|F^T:HpruJQdw@1UbH[CPL.$~{iS!ޑz\Ș"+6QS0tp,0ÿZ*(4NTt8D|F^T:Hpr)bdY@].P E˯',4x*)2(6;K#ȘҎFjd8@,+`0 ` JjhpC.\ EmD:AqBܑJ>ɨPTY(˅:/1eS ҳ#XqY.X30v3\={ao DLH7ɡϹґYp׿ρũr*NgڐQy;R|G(F uJQdw\9F;>dign8u~(ol:GGa\vEIn hi;a`~(}g9}`-iKY3W ;+O en״ǧTF$\ g2_ӝЅ|{r](y3{]֑5܆4LZN!:lD6@r(% *7!{>'`xgcHJ@G=N]{y).Vt(v܂^bxyԄBGeBHiGBhHU}Kg_SHy;Z|G(F!r*2XfL ePU]^t^ڃ%KJ'Oݵ H`;dJQEi?ߘ{缴vl;~{#>;ƛ?}guYq~J3-;/J[jྗlQ>+>XP\4|5OLDw?zQC;yn*}Pu{|cޛ u5Ќ 0|(6{b y%m4ܻ XC{1 8Gܨ${w@6KBr?gORTTTTt29D&į$~io/ h(2IKx6>U02ŪŎ[höUv\"h2qr@@y;R|G(F!B aAYAlgSUm+̩îl!Q2&>fkg&Eo3:ӴlBrqQZeeKZ+_2XSkuG$`M !IEzZuqOK..JZ [a)biMxRFޭM,PvΚpW~-BQϴ6wm9ӭ!5G"9*kƜ/h锜p;O \sc@MM͇Μ= (|IPK:sk ~`0?5 `0  u`0  49ӌmlj&+G2*0uj0(& TU,N1.8 &W ]m)&LȈթ\[qg|Gcya|uj_vŀ f(KS̓V1O:ŀJT9VkWw϶(rw{֮n߶!>N5Q4}34L^hѢEYЬ(M1lțv \ }`ɲgN~Rkw,O ܿign8v'9tE"7 9F]w`#TDЊ䵉0ueGP -»8U=hS>S tJu~ѧN=0ХO;3/>DujЂy%:hbgMώp9 omO|x߮)xp#Q3{}`0tPialkƮ5ڤ3Cl-57zԬ--:N5Q4o(foܬmɏ !"b":/4jˉ GՕ].0T0Rn3ں6~frqQ*UkzZvl `~(}g9}`-iKY3W ;+O en״ǧTFE\(:TKU-ϺLJ{?>añ-57_WM~bnj߹VMmXի3h-(h \#37:?u6VۿmL^3k/[W=.tY FM]KW=v튒XӢ_D55o>i6mSGS7mUѩzߔ'J+@~?.j{rws" kz"#]SɷKxTsw(|IP8$i&AP4MӤڵ'( >GI D{[7A<tDFь)A$lƔ[&m^UU^+}%y{Bnk,)w1sM6,|5𜧿ߌ<KO_fJ_,Q>+>XP\4|ƽk6o㙁4?ґ*]ʍo̿=s^H{.mMzrc% 7ɺgGnC㔷e$2!~M$CMw}ˎ[O%.KSݡ'b@SsضK1x"wWvs6?V|(jh_82~C2#-lL(}]W*335W_IgNvdC 1$`Muu]6Km]?3)6.n곍5VGM}^ATZyѿ?6;Z" g8Id c|g'1Eƻɚm=kf.],mH]?;o;|.-N5wbtN` (F XH*]pFrgqnVHZKX^@E*2{Rrgi9euVT)^DAkLo6|>9i1V޹ I޳g-VkW/6!.DC͇f^{l.G89_Bk̃w"ɏ?޵\Б.2$z܊x5G7s0bk2skTTF~A~NR"wWنyԜo|̸QQ!<Ϛ:W$6V%/[b2BeK<˰tJ&ܜ︸?8'3ZiwZGoαqUI o |< =ɣW^DMK#b3N8tiewl}3{UH_6iqy*|IP8DZva1ya&nZHչv}bMJ 5I.mbA~h݅uք#̵%KQs\}Ks mtFz3mːq*s06K?x:%''\+<*kƜ F$p[_kmn)qaD}4d#wk\d_8“}w4G'oGs3Gի"F2HSՃV1O:ŀJP9nݱ(rwɘk+2-3Rs;HySS5LoGSUYԕug`_h^u@$_ik5nۧ];3Oޱo&џ%`s~ztR]^ZxY[v#7O(ٜk׿4oAo< [O^}/N_ٹ~ke3V}ye U} H $_5K]T sItMw7MM.|mA]LqG|_-kkbfPl(TULN1)qmV5: ]lTi+>#U1?gA ]f˘E]U#PoQ,#I|w霿ωLL_"wWtdZnnV|Fjݷc~,8@GK"x9&\j^1 %2ww(q1^֎kjjE.6ybяר:wߎ<|㋢<՜}ɨU*b$/Jm=e$nQϭa0r5 `0sk `0 ( `0 h4tPHHHk`TbQ#`0R TDJNnNfBHrrsm`0Xa : jc7(f.NJ[g<}E;sñe?@ 4SyL;{=~Aʆt]6,+;Ѣh;.x;lyW5DЊ䵉0Y`kYe7pç`05dgF=n qws+n1?ɵ!pG/|m¼e|#,~_F=Ns^-w]λLHufT3cv[NxG`09h2H gZEnjI ܸv[e`AFώ{(dt8ZtQDϞ{sL֓c]E[zy5|ll-`ڿYT~Ow޺RX9?Jٮzh|llbDGkէ`Ί9oB5-i!՟-j 7[^K+|Lrg+٦ٴNؼ`LBV?>-;):TKU-j{T#Bp4,  mU(hͭԮ3BoO %=W~r.ɵkWƀƚW5n{ES7mQSidw {>'`x.a 0?T 绯 4 (I06&PJN~rfc-1%'ICU׎bgvSiiTMG e<<:>Bp%~ESY Ucq7X7f=g^[rwb_VܷO@e.~ey5MCKfx|\sbezb+bȨ ?=ښ+O2KBR ;o{~GGN%xDېtLe4 X pd9\$ ˖kЛfڳf)?YUI- {.;`$w ?.ksk$ xpL].%'rx*2{Rrgi9euw{5GJ_/E@{Ϻ]_F58h~9z࿤9xïm}rx9b8s?d iXdt񊵫L! Ma6ȝ*֛C0jBD7V[F?f1qz{LiO 5]/U~ q1ay}䱣W;a1y&e*-H 5^֌' Ycy`;Wz`,ܴf A_#@k$}n7>xLDD\츚?[W'${]DMQ>Vs*IgHMW.sܥ]j5wzL\X2dLz#d_#`iiYm$߶2Px=-u 9+#Sr䉱>?o+$GxN@C]2tdZļM:*#obބ9cEwhqQ?`o[`0 0 `0Ls0 `0L!`f,^fi+>#5Ԭ Al(7H\.Ra'M0a\X|#]m)&LȈ3Rscya0c5ˆx[bIs;8ujE.6y{H7sG&/X4ȣF"ؐ7 <4(3JBdϖ |.F;sñe? o@kw,OOK2w |'ӇzF+C/ "?ym.LD]Uˆ[[H#<ݢP9+vޮDK&cUmپosǙ.d]K K4)vPP\g9rds+n1?ɵ!rI#[~a޲ ];VLؿoN"~̘U#Pz끢XF:qCEe'u5<@M*:#;1Rs'0Eg$v،#Άn;n6GńʮEeAT~i9:wd=^1u\<ؖZ?>,:QQӌǶ̦Mt\}j_a`~(}gGŬOs|'2kZB?[kD8 ג7_<\/} <wUX^tFAk=GGla\vEIn hi;{xDsȻ* /vܭ.|ۓBIwS tJu4EQb (i2ps[ZCS#$ϛ[Z6cJN-6/*J^alQ>+>XP\4)E; '8祽4D<9TdCh:7]syϸ "oL_IP]peU#P$ũA'b@Sϭ^j'ύMڴ/3Rs~=-cz:*.hqYmiemJ22*>6v}[tLDLL4yua˖P)2v̚_cfO}+KQC?Eq$ 9$*ws,lД~YtüÚ_ptiewl}3ώ[N%.KSݡ'b@S{e_NPbYaUJ|(jh.ogA'e$<;,)biMxRlo&]joYgM+?\{[JttFj$ܺpؖVPY3 &blÚvX8@ŧh3ң[;"S̋%xatpMK?x:%''B"g8"M|0~ȤG~>m(p8}w4G'oGs3_v*vI]RŔ<*B}NJ;b_jE6smYf('({Ʒ}34L75Uv4UUX@O]yy]w vpXKv۫Nxfm9u>-\ٹ~ke3V}ye U}|>{'8&#yaH>.sܥ]j5wQͤ9<>k9O5T7V8wo/vx1կ_5K]TiQ82xͽ Ab=a:[N?;o;|.-N5wbtNtH6HU4jE.6 ?'( >3-Z.XGoPV&FˎPA[UmwIqz*|IP8N}NdJnnnfڤ#rss\[qg|Gcy?t(,`0A1(& TULN1)q1^.;~]ll_O0#DQEs|Gcy-N5Q4oh,8 q]~[lwiq'b@S&9<7"wWcSDQEswƑ   (YȢGwE-Yoqeeeepl٩O&H暩lLOrm姮u\~ֽ_B9(RƶZ_?徿1P=1%2!יQ͌y!W_6Qx1)x)T:A=eqCENH eMcw(rwIEeBuզ5kK;(-:#5Q4o(ftd]>|hݖ=+]aQaƳ^)${Xrc[fkZ~M(ol:GGM3626UW)#Xsݛ6~ˣі&:Zg>vVy?ܮiяO lSۯ̊ڵ+Jrc@cM]b2jA ,їV/k5VÏ4߹ՏONS=ƊC;}|m-^zaD tpÚsy0ҍ#On>lvUtj7| ;˟1gtСY.|ۓBɁ4wG; i q6X'Z9[5=tWlG+.-N%Ҷr tNPO9li(rwIMvmD|F^V:8"<5E{u1 OJX,]Z6cJN-6/*B^a'lQ>+>XP\4؎/RT<}Ѻcn*}PXC{aG6ϸO_Y]֬_RkKNl˪V::7|9/m5G DgEڴ)7#/sG*og ζ g»B[+Opƀ+̩îl]wގy27mOFxЙeR+5u{3PgT5V#"HzYK]};B4$ $`/[,ZDwqEfD^1J3$5uz`h(>6.Ⲧ'N-ғժ־f~ZrqQZ%' 0Bpvo(CBrG#;å˃Hn( Ǩ3D>"!5J*%y{.;`n_5D]Zj>H%dx)T:A=8@kI8~1MM?|F=(7^Wq "'%w֛Z03]>]Vg~돔9R ߼θxÿ⟮]dڄ IXM5jߜjeDsň8fOZXhhYϳ s]<0=9,މ'?<{򁚈СrxBD7V[F?(cOP\q71/;1u3u-DeMJ {oe/9㤅#η}b)&>5Z,swFߥ_ؼƊۺ0SeKLFlsm};OFfMHpyOz/αs3>Qƥ!`kMcdE|9ӿ~hJQ?,~sSjH}Y||yνtBu\u{$p=,|Zj&Ea7sŚ,~otޥm|j>(.Vt(v\(cH1Քcaءq.<2 ܅nrϦH7 32wP:2Ŵo: AkKiAunYʏ0ޖ\::#5`n]plKs+sr1a[;,BQ^tqdM7q͍M4s7:s m-CLksh~tJNNa('g$6"0 A{Wkmn)qaD}4`ҍ o'ϝLz7yM3 wWNst\nbMIKx6>UcHRZ:;n 9W;Wr-dڌWۤR;e/ҹ) t#N1T}!"ǎ^=UT҅S[ ƽ_XA[ؑ͡/Lw.R1[cZdM-8!!Έ_[҃K6?}`Kÿm2l<{ֿDU/vx1OCɔ>2 "CbH72DyMX^}Sظ~/uh)/Df l0o8XAb=a:[Nqw3Y~Q8 vI]ƧcbIb5Kb-υ2Ĵt:V {K!AjZ篇ƩnjN8η}b Xlj`ѢEl.XG_`jb IA$m|>ȗ@sIj\P%P(_HjBωHΌk!=..ط}S6U" 8i!AgeHmu(q=.=9K3( f4@WK=N(w$mS cFKM5Kb-υ2DGddD9߷z{{Ї\ yK_mE:!D|F^V:Q'b8l_[͉>׈`ڠXqR:DE#Iw2h.7U-  gm}|n `ÿ[`0 nt:b0 `0:9 `0 &p^PZ].H3ǥVPa S(`y;Z|Gu3&4MPL.ѪAB~HƧ&_FU3QqKPJ M *"9+-!YqZHEd&ζ.FKYƩr*NJ(-:#5Q4oŨDAnnիW`A,F1OX@G'C\{)~S1#ig@BC#S2ScBxevww؆z¥o߾FmD:AqrBܑJ>B1;`C޴.XЬ(CL&_}ޒuǎ Cݾ[R;sñeeeeXJz*OS۟LV3H<~]H IBM1Y^b2P#UIpƸ`! ;yɘ(PPƐxo B>m̼V:y8V_r_aqwxhD4gG޷F?3g~VLÿΞ-Г7e^T0$ 51 QN~OoekL=+'B*2e\|fmU)4NTtЎ8by({F>\ Y:G;ʛ>dJ^tFAk=GGaz̼(M_~Ƕ&Ltg-~`~(}g9}`-iKY3W ;+O en״ǧTFOM|r%1E(w:QQӌBrFw6_B ^s擛fӦj:e|k>{]M9_z~N+ȐX֐.T%=N.gʅyNSPdF2 ;nr CNIiRBVAyqܦJ 32w)GK18%x@oUT9/e8͘tˤˡkFPM aMz =Xz]0N 2~읇u ώ&z)Xme J2.m*΋wA+2f乹sE E8?Z9U2;WPh*qAgeHS'bq|oYn?@&‚)sΦ7l͕'WS']9 _ȘX.EfqWnX:WiEiM7PIGD57v(>@xRy/NtyЙeRɋVȇԬ)$nyϚ{G萶Xme J{\6[;RϠ 3\PdF2 Tx bAxNT%\7RHx7,Tĩbn]jCAz;m((.ʳ*2{Rrgi9euV$) g?Ϯ/`)r)oH%4 \+֮^2mB\$I.5=?,$㝈ϳw-/t E|/Ɵp2*ؼƊۺ0SeKLFWcP|1jxR=˶_\l_|CkR+׿0;_,Wa.!ѓx576 D|.$*kƜh_![;,Ct[3jY΃aBׄ!&<$jBs-\>:%9.,ސ#VѽAd2;5EO/:˚2P#UI׳M (3B1hy 32MF2 ;ny.1$e坿ϱܼn^ǵio.ї@DmSB*N8DܑJ'>N/GR zzԕug`TUVib# ګNey9WjMŸ7a;3Oޱo&3/vx1EOuk[zp)f<Ϸls5fŻ>.sܥ]j5wzLb.}طV,~tqSuK ƽ_XA rxM#76G^$>xK|o75~j$2Ab=b֡Xe J-m8]@Ҽ˅yb#<p]9CZV3nBhAM NU( 8 2T:iyH.2FeM|?'` ADdS3WHbozn.W)נU2/T^/Ŏ[=0B'efee y εHJq)!wJ'3Nڻ cH) zˠU=t(,lo:˯',4xA8i7=793B1hy 32MV1;ny.1$:"93+51z{{Yf+s\ yK1vψ'jrWt8r0Ș;Re#lz(=eЪEyk9'rsu(&_OXh %8qozn.G2B1hy 32MV1;ny.1$<|n `c<`0 t:L`s` IDAT0 `0us0 `0LA40 Vnț] oƉئU(q|NRF^:ͦ$~^zu ɒb],!br.B)[P.G($5#_F^b*b-BB"(.(HK;s(}0>K!4.%>Xۻ 'jsWt8y1d]⑖) z"U :GBБ, 6}ŞA1}\1d"QL.!JpʀtEDէN=0jЂy%:hbgMώp#~`0tt֒Ƨ&DP=݃\ q z imz]Ц>k0x;RނKzDΚ 7Zz 91f ƧPMuqw5&IM*=HxnmCG4$I{PmsAZ\e I1 ^|G (NP#w6% 0i/sSBC#ǥƆA4 ȡ_u-$HygIQ‘MϹ ڔ'8$d ^T:ņ!^O5 9#''uH y<%ڼ./o>xH(l=b(8/Aޮ2Mŀ/p[P.$נEM LZoTPƐ)p>F$IZmJp*^:SeHSlx ❺$c3c50yތ w? /7nMB2, '>9}VECR̦bH{M /)MŎ[=0߃s0(!3S-B*2{Rrg}Y ?0%=LKDe'`0/qb?~DLHͥLJOԓ]##As)OpI'd4t o%=Q"^OךDHCnN!u6X!?%6('U&#Chl xsFֿmyhj\|/+3pY[S^?E5:Q6Nv LIj>2,b1j.gk8=cGAӺ׍6W'-=.a;ƾ 75ê9沓]={CRje:hjx<@J#5zؚy l u$le {>2; >0@5d ߣ Q'&~aJrzƾ 36{ƚ@a՜wsI}R~R/ŨQ L%Ñ&tS+ lgtɮ=Ͱ;; >0@5d }{:1Ks)gkG ΥxXV^"Oٻ{KR!<!ؿWmb\'^'NOћH ;8)>e?oP3%8F|s$AWRx1@5X 3{4{lszbޣbV8請#mqzXh0n.;Twʏa>[e?QN953 3%]ʼzؗċ;ADžH4{lszbޣbV8請#mqzXh0n.;Tw_~θ{۰_'>'M Ñf=vlMqSfR'J¦[ϫ=Ͱ3*g1=#Hԉ_rޣbqz%moÌ&`X5\v??B!rUl6B!Bȵs!B!}kۛUT=1q42Yp#-L%vhMqS<2}AgYG5:QRF_%ȞONwETX浃ž"^<f\N񹒜= ;&uW6Wܚ'-=.AyxX@ʥxXi0 Oq׿OL^k05>/{d8R\WcG:ӧOAPiM]#{>:|Fe^=XKcj*ɹ g'u_U6W'-nadBjlk UY{;*vDֺ/e;51o_t?U3p)twr HDItiĞϰ;c$)!Ռ)>Ws"aG0$9K \am](=cMj6ko'3m Rg 4ܬR <NO=gv31N .)nMQAӕ0Y˫ȞON'|fl.'I<f\N񹒠.3cANgq\Ikv3.)hs t\5TNwJB!B{~u^e<ij7T5K6fkgJ,/\#{>:󙱹F}ŋ )>U|fw^׉o&02ښe!B# Rg {NnN? 'ެ?ч&_O{ 5K6V EX$F|uڽ%lM\u ^<f`N񩒰.3c;NL|0ؖ6Faft3v`$v\5cI'3<=>~mQ8<=>g%ŭ8j:hf\38kVbiĞόͥ$)!Ռ)>W|fw瑮tGIџښe!ؑЅr)3D=5ko'3<Ɲ ?B!rUl69B!"s!B!}k7U^J=18qzvIqk"م5Fa%I=vv$-K1j Cj+ b>':c-줅B\2÷](aM}秳<aO,"7&[{_> Sc?~p?~ם4=5W;j:lCT38kdNv{KRy`/39S|$l U|Nc͹s QFKyGŎ](QM}秽2IvDo7}kǯ_ {'~g`&YJqQXEkdϧ]{>36bdrT3.v\I~h=$[#B!\f_?^}KԸz|z?罘]Rfæ EX$F%\u ^<^Mo xI[wuc-u'`e!cbm}秳<*N}2nժ+hXVj|6@]Rfæ lgJ,/\|:3csՍ٫):~+#̌;~QM}秳'g'O'o6wkm!R""r=pQ᱉\uoͯC9=izvIk^է5FQ%ɮQF>%/dL\vH@y$9ukXGT;d'u'e!bBRcov6>KD`<*N]={}{8vϞS#SK \p,uzQTœkOţ6Ҟόe)ϲ;ⳏJ g(uX03:V7;Ty%"lg'+'Ӈq{ !B!W]ZW}n68ݎ:}x%a5GI]dgDEx^2i.ɸTG5û.o { pb=.>R353C}Afgy~:v'7~z(W/_u'>(MN{s( . 8ZN$;#r("5ȧ=$ ˮS 3u$xNc`(v7&0ޟ=oQF"&$5fj3Dj:qvxpi7s!B!Wf@!B!B!Bȵqxfwk "F"hދY]\q%IvFQXEkSEIZ  w%!:n/so:tE]|ͥLg\kfFϰK zi=w aaj=q:3NooV o?ʯQ~xoy\h9KY# vkS EIJ6̫۫)%^8un/1;:3n.5L`?{^0>p2dĞ$05D|T؍SNdΌ7wힾ}+ON۷n=8ݎ6. .8ZL$;#r("5BD$xcpzIɧ}}Mg@:9r9~ ZI ](ѾFaftȁfg< =w aa:Ow$g'uf ݎ[#B!\skV}UWë޿%a5Gʼ=i9kUb$5 Љz>UC41N<5_t:[F n2ΞWŧf)έ{@3kNJqw'xpzMZWUWGMWU)-Q%a5Gʼ=fp('mo H(JR󙱹X/itrpb=it2jtB5 3C&m{@3kϊIr~w>j:`'}W[+(G0jִ/y{͐ȧFźŃS'K|:5ccI5a=i$u:9eiOHFb_'*vpuG kԞOmoR#,~֚,RK IDATy@ `!1>|6y~z_$71^ǯ?WTE{#I#1u ͱc%KJMo.Q1hS'Tqјz~:؆:~4:o5L`}8{^r8vT'i7[sJTT/q),}2nS3{ڦqv8Hu}oDZucǦ%Kl:'5:1|7 ԉ5C=5=?Uvǰ/Q:FL=L`:vF,wn؝OXFyo$;{{3@ 'JB!B{~^n/no G⬈a8?61qFm$6]~0B>k":I\D|K 5=a#:fP E䬑s[!/0N#$&2PD}M{Bxgl#H$iü7[sBiqKP3d8؛qB!BfB!BY29B!rm޷Y`,2A.z/>5GbPvIvGrS$F"H>%EIZ $N~G䬑s#ǕCڰa/0ND۷5Y+G;D!osHFD"a=Z5KP3d8؛7?}&Wn}ڿ:ɀ6}%yKrj_su#1ut,vt$#{D@Hȧv(Ig]}MNl#a .u ubsny˨a[xga:ἠ3f`%Vdrj2"N[`wOߺvt:5 8S'NbOK;G:'5??ۍ$[k:ub.F] Xc\K]Ny˨a[,#ckGAǸf`%V5C#yV;oB!r]l6~zٿ0>}u>N>S#1ut,vt$#{DsX#yi/1f.IiO]~G䬑sC`.Je0ط2XuosHFDƞO%`$ FmwewBKQc)}߯ƾm$6]~0vOi5GvPvIvGrS$F@>%|*đ%wx{D9w1{\9jPt:[F ItFrdl#Hvִa38WYOrZ)p75VGEQN5GvX6{f9uFX,{>%ƌ@]3w&o X#@(N#&YO`ִ'$*K娇{Skڳ=cۧ fO]Rx@Y0ͪ{P(J>I}9\$B!7o?x(G˛WëՑ u}o_1i^p$N~S!}]ݑ="{2J\Ǚ cFg9#e>C="g&Y{a#r;[F H}82X Ğ$5m5.ɞ;Ι%@CSQ`gɞ:{D~~b[^8e03`r8/h 'iguuI|:8}IiV_v3잞a8ibS?QMO#N(vt$#{DeX $CFg9!If@ ;5uC:9_(ua:F{ry%Aa*ἠ3 f\Iwz犟ЌC!B*6B!Bs!B!ƏtfxAquFEQN5GbX6Uݑ="@^`׷^b,G}gwJ# LNg9_mm{c[&0!uګp^@OXFyX3Ss&9w^Ru}k?}|_01D2M~5GbXvIvG(qgBT|K娏7 NI@bDIv%{9_mm{c[&0!$$: ,bx6{ƚ]WdϧsٗTf5z1qxz|,磩qe؎Hu}oDZubۧ%#.ĚGWB 럝O{1'Hr6PQ_^w;&ޮRi,!$gTr8/h 'ig$~N_¶ 8S'NbOKcW#,1x{DucF{>|x$ LÒ,Gu~b[^8 L. P$X >˓sֽ>9ը`$|q^p[K߯|XJwN L5!GvH$1v͑="mM%Lg5uċ <ĈўOp2$g3@s6R$;=nwDxFJ´{W$AJ\qp^Hs< 8Z3P Mrt(jld?J)<75VGEQN5GvX6vGka:K\^wLO{1#-ٿe>CS8vvQk3hStt }3!u5Yf}%Ԛ,EyִHRX>4ѳ9'4*/e<}^"> e_ǣWMW|ڿɔWL _n^$n/2J $DՈ%ƌ|6o|xg3`';;~؏f^wL|N,[&0!$Bʒ{`94,O1kb {{=5Zwa8n#E#}Mȑ]'}&Ybđ=Lg5u'>ɼhg3ljq6%hw :;{xӉD|(n %.Kr8/h(2c ˼&ɹs/8SݑF80|4!B!䪘W+j]7N꯯;HiͽӾ&HL8>.I^b5"{38KQ#Ո%ƌ@>3%^kv}Ol뎉OՉEz&D^](QYc,bI 1&uMPu@a`uAg5ըV뛛s>}vѽqOmׯ;HqͽuM&DbOKcX gd5w{1#lGq6v~þ짍juħ"Je"zK.,yA$&v:Ogwv泎jz᧏ݏ?(75nۍΑ"HqͽQ]߹&Ȯ>.Yb̑=L{3jt}:ygx{S|̀e x(q~ޣtb2 %9h' }'ǘ53_ٗ).I^b5"{38KQ#Ո%ƌ@>3%2nv}Ol뎉O4f2 {Pb1KyA$&v:O-S|q^Xzu^߷vw Scy[y)"y? 7; cۧ%KFdog3jtQݻĘgx{S|̀8ic?w{1:ƌ[&0!uJ],x;/h Y2cRU 4eޱڙ:kQy}k^k01ۧn؍Α"HqFu};cOJrtdӞ`.ޞ "ftuywSi< 2J]t传Ϗ}'ǘ5#3_ιeu|Ѱ}z ߷F!B.6͏ϭOܗ/O ͻlkHqͽ񫶾sMq:}z]jDfp8F@'սK|fg>Jg?mGPn;&>UӘq&=BرìOxM߆U VkGRJqSa8<ߑ]'}zUĠ#{gNO34tgoݹ3O{NjΣ2hD^(Awfތ\Ghl $0u-SUQ)}_rZ)85v<2R\ӾsM]';MPb̑=2T%i/1f-ޞ()gI״N].ޮ3ʻ}M{mt`ӝ {7*n㬑}M` vg`YJ]30)0vxj>RR5Pg1!B!қ?^v0?x)7"Қ{lzBAۧ%KFdo{9ktQݻĘѹ;23%^kv}Ol뎉O4f2 {Pb1vF50^a5Cy~Oؖ{jg>8 F^?gaYGqaGH}85aM1t0v$gAG0ϯ>lfit goݹ3O{NjΣ2hD^(Awfތ\Ghl $0u-SU8؍|4!B!䪘W+j;ijFSXDuIXxO(=urQ#FTn/1ft gtS}ǝXSu:'XFB/^׌ȹF1h ySee YyM`5*jus>~λO=߿])"z? 7tO0FETn^$n/=urԧv%"jDucFo|F9%awX{~5XSu:'XFB/^st{DmlG~^N,uVݧwv|tdvv7Α"HqFqX* >*YbБ=󫩏Gt~;ݑ!ޞsJ:?LuywSi< 2~qȹfuݘq#4ۆ֌|U:疩zgl*hnB!B~!B!o!B!\UߍGyz*8v")u>Q]}z]OgDմK'Yw5w{1swdځG aqzwwv:,*!mqюF5^a53_dϧZy̜&fsNټ{tx߽}{?xF+Y7~՜ڧ;#KbOKRȞ:vI8FdIݛvswdځG aqzwwv:,*!mqюF5^a53O>9YyM`5޾Yw?޷ {8lOݰ#E⚊Q>" >],1&NFӡ;ݑ!>lkAH3ą)3dq~jtf; hoӌv5rL3nf۰ٚqW$A kŸ+ҙO9j4S_FoB!rUl6||_NhWJc/1m$]n_SiH\}z]pXDH¦uO24ڳLskӱ8ÔŎ!{zҙjֶ'$6FcLΨhl $Cr̢1,_J_uϭR^N_c:H>0> >.^8,"{$aӁۧcwnooŌOd3wMwqV`Ru:v ioU<֫u5rYn7:ӕџ,ey{Q3ʻYjTJ \=w# I#ߊ{~|fS`7>}W^ƺ Ft,"q͌I5:$a$,{ntf)CskD,5 ^g.XwTN;mo$h'1&{kg)>1fG#?GM5ը߼~s=gaݝq_b:H8ݾ:fF$XDvr0G@ӱ;,ewnm@}:g ⚁uOՙjΣ}{;omǘ쭝Q#ǘ53_|(>XTM]8cJB!B{үV})ISnČuԑwq}Mu="q͌I5:hOuX/,[d%f ⚁uOՙj@ hoRQIl%akv:f5*ju^~?͇w7ݳww[ Hާޏ?W3ڧckfHEd,G3G$,{nnέ HQ,z9&ûf`Su:~LT#%cLΨQTB2cfy4̙O1fަu>ٽgԨ߼n?gv045ݶ jk#kfHEd,G3Gv|#zfڀ$t,!0Eb3* IDATk.>UgF{5'$ǘ쭝QIG#۴Χ wgnF~!B!B!BC!B6߷KgXV}A:{>H\3#v@}M,"Ng99S4 7x$!0z9&ûf`Su:Cy&vIȒ1&{kG]|0${>j۴Χ jussxû7nӷ&7cJ$S߶>H\3#v@}M,"Ng99'YudIݛvcpwnm@fa65niTf=vIȒ1&{kG]|0|b՜iOAv5^{}:om=m˸;hbӶ3*h#tH{D#kBu:̑$HQ)[d%.*v1ûf\suf;R#{B2ykxIaǯbs?o:Ii|!B!l6ϭ~u45o_-ۧ` O?nǗE}:fF$XDvr4sTOȒ7zfڀ$5K¬m2k.>UӨz5T#%cL֎f9~a6[30QĪ9o:3j>}}.w)cJZwq}Mm="q͌I5:hOmӨnέHO 󤺐 quՙZH hom'FaCa:u%d1nؚqW$Akv :f5*}ߗq8|nrԸ3QGXDYkbu:̑ @e%[]W.Sgl t`YZH{%:5r31Nhf`Sa:[ QkڙFPB̄MhS7E&$:~on{7n)ՌXDkbu:șd%uoڍ"޹I"jY/3v{ۨ gjoj.i wgN?qvgb HĻ8ݾ6fF$PDvr9r&Yst34*Csk#ӡ8<.$vQm'>CgX{dֶW50vG{t vIκ',Ne.F'ŏ+M!B.&^uG?"M}=:.Ntf^Y8,9ɪ# Kzfڀ$t,!0kܱۍl@:ڻ dvf)C|`ݣv,omsȮIzs޿yu[?w0w߽yum=`T2 yx?^hFd_3#v{5jDs&$UG@Խi7 7x$Yjfmz_6 3Egm,v]Fݣv,ǯX> 'V6:B]un7?VM0QAxF":cWp(#u΄vRQ)[d%.*vQm'>CgX{|Þvv2c8~/i0g>! lZs!B!W~!B! 9B!kcժ`jA:wqݻHt@8+0:X5Ո2RLHj',{nTx$0Uymz_6>?:[ !]Y6uuq*,h0g>iZc!;޷f]S Hާct@t͌IeΙNVYRX/,{$fYr8c}!0tjoJ3LFRa"ݡ)NW֌PFǘ亇9_>! '$~M\!B!Wfn՛כ۲K_nSBDG81c:Q50v{5jDjD@Խi7 7x$jYjuwg9Z?(!m}~:uFw&] Ho{.GPή%a'- P:BY޷VJ_Rʱt߿Z"H]Nt"Y3*Cso"ӵvݨywnm|~:u,η;0ʚq gOa#fܭK2QF'8%#F{>ܛ$ 3CRݨ6Cc s3EԦlIR`eԽ~8~*D:QKQg삎+=8Z8v߿⚊q}{0qk*Ιd$LNV$Wso"ӵv=NbpT&Na--Lqfƅc2lg84c${BmDpFWB!rUL+ݕ}FKt5ƣ{Ϟ3p:s&($#F{>ܛ$t5!]'zT7bSgT{|C S^_3BQ@2_~8~AkfA1J=! 7dd4r^O?cM}f8~ѽQ:;Y%(Uđv=wM@HF{!nTٱD9"jG6ŀ$d)2lgbtv(`n%(NFl#o9nN8va⚊q}{0qk*Ιd$LNV$Wso"ӵv=NbpT&Na--Lqfƅ27ǯ53 &ɞe2d9~!B!亘B!BH&9B!rm޷egަטQ\nؽgOf5Kӑ3v ɪN$qhmHOWu4.mfumGq W@2_~8~Gkڷjdd4r^߷vy[0wjm;(Y7pN8Y$;9c.Ii79xg*.DFk@D Q]іw֓BQ@.{ 8XӾ f%(NFl#Wauߥ05n׍5)Q:׬XfIv:r$Yi$WmIjBRN(n?*OΨ>TEGuj!{ 8XӾUcF`'#K}_5B!BUl[[onp/Ae+wѾ}4vz5ƪYd#g@mcںxg*.DFk@D Q]іw֓BQ@c.eg56vˤ>.( #}nRJWIˏ5$={z5d}3!X@TvIHўzDr;Ԅ8d3CR݈9g>j@G:uN;X$X'Fkc:īIeY]w;ל%nRטѾ={z5`kΒd:풌&-mHO4SKEI1Gө3NMduo3_Qƚޝ\ #K;dn;,V[\.Q?<z'+}pw}yno}MQ&Q:׬f}3t6M Lc3x`ODFk@D Q]7mazF Kُ13_Qƚaxz|u3~Xח#h)xϞ3p:NII, ޙQ.Ii7Y(0uXՄ4 3CR݈9g>j@G:uݑ=0Rx;3JdQƚْ],Ȯ^iB!BU1ytWJݳF`fӝc㔔{F5Ru%Tȧsz]|PҾ1Iub; \SՓ|ū#뎚](N33a(*ɰ#s##oV+sRr_G;ƮT3:đv=Us>9YQuG-.JPTcf/zglC}(>CRa9FöSxU?g<8PqL7k6t{3.Ii7Y(0uCMH$a6(pMWOv{i;jv:_~؆z>p'r΍ ?B!r]B!BC!B6?}ON"ͼ{Y 5UvIHўO|:g5! yg/5&9=0ofjL|~S%?c˙]߷]v񯯻ݮܦ7nyp̎D9o38!.Ii7iﺅ(ޞZi[Y;{ԈN'pU4/f 9{>p-716;u[ ]7Kq71(h{Y 5UvIHўO|:g5! yg/5&9=0ofjL|~S%?c:qM|oB!rUl6Vu%hbB2MQT;`|ogs^6Vy 5UvIHўO{-7S{KͼFv xXN3=kQv+:%F J] sk}nRJ7{RJV6tƮtI_[Ӡ.Ii7)ObHwR33^ Y8Țf%5s:%lG0❒ޯx*=59#n!B!WN(!B! Wn|{__w]je6}Fpƪ9o38!.Ii7iﺅ߸|fl,Uw{Dj'/Q;pURx5&(2Rws'XY߽ڬ9~l|H3gCu:cVqH$gii$ȧszM|h>L4z7P1Ɠ" gYsZ$QuB~v5 I<ۑ!Lx$3L)#yȮ9B!bL߷u]wSԸK64ŌͲf IHN$qhϧHF>󙺵g9X捽(#Np)W| xXg3{eVtJHEE/s7/?˸7uەTVwxϷsUsfJHN$qhg[BUSg֞`7#R;yWQ 0[{e2";gԽpȺfji_EdaeVtJHE46y#R9B!?B!rB!Bȵ]ʫ-Dͳ)rp&dađvcƻ}tNw '*K "e{Or~fw=e^Kyf<{eVtJHE q'_[V7w7wӷLLeusN51N0뚝ĕÙ8ʚN$q݈3N|n9V9֞`A&{nD=?3;p=Ԍ:.fD-깾k;N)ݳu~_gEͳ)rp&Y:đv#@&d=fS#;gԽpȺfjйnf5̑SSRF.XOZMg37W|!B!l6ϭ77U97ǿ C51N$ϻX+)3!q55vIHgrs=69C&{~fO{$:.&6,˾z2RwFdg\|+t챱7|z?x$|5EKGڍ3R|f9~G|k], ElȬgF{&}aui0|omz:;Մ8f }?F!B&wB !B!Wҷ7?{71ej;b<~݌k^JMH\/)kj:2F*:?di=o=cw̻ Sko%IaZ.j6 )~g ;Zu;#qDZq^guMEdiބ4ON5!,Ky{g'dIGz-gC]QZζ19jv~#>#ͳ3twk y6dֶǮ+B!BUq9RʳSk-EB4K_N L2.)ctNZ`؁zOu9YWGFmwX垟wqeTYNN>cbwtc1tGL88M%-)NVt%e\`>Ͱ,-p[fh5Y|k#3 _L9.Qfa:;JBj}}kB!b>ZW}yoRq(4_f8@υL]}5.lzioudF9ySӯu]W!^2X]h|i䋒=d s178Z`C_ޖi6YD#6HQF3]1,v-)~g2G~nB!rUB!Bz^;Bq1Jzw" ꐥL(kh)IRtFD:C`H68y=3ƗI(ڍ.ZdL q/taa.Z<όfw="}ׅc$ctD!KyQy ̻R[\v ٲ,E1i#ǿ3>8vp4'k7NS{ɒYL#-\3NGf.I9 f0-QB!BUC!BBs!B!9B!b9}?{UԸgF1ctƗDIX,$f1G wydT)XNB]qlӘ״}kόi|5%}/{Y{3bq%,tJ,'!xѸEy"Ɨ_/QoB!rUL߷"\?]$e:_Z R2@Nq$rjCAKOc^v|!B![#B!\;B!+C#4{_I2a`q#0vIҫ!B!\B!B ~!B!G~4h5&Wo|~K*b-P'g|>iƿwg$*YN&a|%xN2.P'e,C!B.6M9B!+! B!\9B!rmB!Bȵs!B!!B!\9B!rmB!Bȵs!B!!B!\9B!rmB!Bȵs!B!!B!\9B!rmB!Bȵs!B!!B!\z4: IENDB`bpytop-1.0.68/Imgs/logo.png000077500000000000000000000324521416067541000155050ustar00rootroot00000000000000PNG  IHDRjfm,~zTXtRaw profile type exifxڭir#9)8V̬nL""#[g*˓Jq|Z>u?~_}ݧυ Uusy'}}п^0VienCG?#Oy^<=c7P^DڪD2cC=ǜr%[OnXRɥ+nђe+fVYkVkm"[ijktnԹVWFqG6hOgg6l&VYjolbwٶnrēN>ةOTDn>>f_<7E@S bZ(r`YyWa>c+nOVGC .R}'UƓJ\<xrI81|A;s+^2x. hrrsƟ7uw8}FmW]{mXgk=톏=-B`L+F'f.s0nq͐+Kb9JnP >_];xݝNc\]ejmBYEV-R𵗽*odeWdޡ!ams^Ia,*rs؝ ݚϓnNf[!'ҏR[!g=c Oi5v}gLr<>sN|RYr-[3Hpg't2pfe Vhb]KQb2UVbONYgmk>Y@~rBB=$8)/;d[?%dofFzhgRo+dM/w,If\M >轲Ȗc{oj|)YiHGrlyq9Y3PDaT_7V iJgG2@Pg[u 6q햵 E,s>,w!,l\uH-4z-a5X'JI*Nx(;D#mL: ms;g p7_@rB_}K3SNJ3o̺l"MPǀm]~,D ٝ i1"ʔ7SڱDσqv^"/`,n{MB='Jd#y@-`柨aH}$D) ,i;sjsǬ=rX3)2O?L"{O%MqxZHDS+r(P҇N$3РS1SiF aO|?pR9$(з(E^BUUԐC1R"FnWഠ Q1pX.,n!2>P y+7R69N"z\QhQ]~dѐ/N#2#ctڀ%}MѮSA ,X`})8|oÃVGc-)+dQgAH*)ȊžTZA^5B G႙2")ܕ19LDVp * "䥡qТXyThNE< mEoz [=s<^Ł,/ ic:`eAlx %s$It6we5O{ &^nLN8m2Hk4 k_ί%'*[_!( jc9X3/LO?o? -7L L!p$a:|C”s.#\$<aZތ% œURq`cJL} 0J$19eJBh:ě Qr"Ɲjd\ڔߘ$9Y_a|S 5UHG$40` haDA.ZA$aJƐ9V{O` \:= 0)Gmaa1xC}zT|- Qh; /D8TlO֙ ȘBK#05L).zcZd62W0@&Ͱ"0ro$z_iOqKl 61XUQ e1.#4i*RSMdM\+*OX_ЧsH~Rwt2#sF.T[@DGĂʿ&M۞ρTݤNН*F ٺ>obb[p0hcj^ LN阜 MwsR_ګXVuj@U *=0~w-H?P)a(H/dðۓq%VS oH$U ddÃSt/eF ~@ 82 I%74Q&|7(  &y+D 4:f_ yۅdRWgO.tBd1k Y dU?.6Ey(B0hHM8YWdsw'zK m<P,#b?a .=ѡ A9`IԺ39UU7\2z@o Ymd"|a A H`V^ƣW׫/d۔ :n Aus\$6p4dղZMsѮ*^FYKkrW¦Rj8Fdi %MtZkj w#гI$&)1ɞt!8PZUԼ2-v'uxݑ&3i9|q&L0=j!sBcƌ?cX(:t@ۄ`$%h괷G҅2MTMNh֘]VtĨ lˈH%IxtSxߥIyBErzVEmGP%tkM5j [n#zBcjo!S9gzIQbYɒ&8F˹ru mS #n̪O\7ts?n5~!R(NǵYGj ]Rs:&qYI]y*!p)s_7iZ W^SQ W^ydyԐ v#e>'FT*ukS (`}8cN1~BHA פֿiVdJ#g{E30w f{I0ɓI Ҁ 'aIlbncu~c}JԤzg;DI'Xwz{? ͕ M(A{ <%Ra{Sorc(S=M:4M.TPdI]7mﱗ6=Ql+ \U[6 #5*ԙ6HEPQ%a`]^tKAAj[ \b$^  .ߴ%DH ]J_LeEAG!\Nj hS!]_&d\3DŽ˴uȚ !7/:bQ|veϣ)Wgdϸ-) ̱h#u: beyfaGzFEZb9b'5[ʤѨ="i $D4hpp! "NTE:,ms{:P"Y93[H $G砭*UD3>Ҕ'swWwRY7]!1#%!/Zk!O2Htq:R։4Dh6]=QX=VpK_ Vro`3. Hi2I Z-j|h8*J|N3ď\W<~\vYR.fS#"NBcgVg{r4G"RHC:FV O]\ `4Ȯ~wk&'p}qQ }8 \F$ѢG6pqє=rz2dSv MT @_zkQWK7!0V5wuv?rUiTXtXML:com.adobe.xmp 5qkbKGD pHYs  tIME 2'lIDATx^ݿk[WץҎc PC5) u!ThtѡCiBbuc]}99~XVdLbq@KV>/7E8|4xZ>Xօ|^\5GQq5GQq5yb̧=yƲw{>u!_XN8xZӇԳZE=rvR:_i2ORNjQ*l,B }D0(j0(j0roٰj??n'<<~T%S5IQDD^Q2L<lj.)Welnnát]"r:{|hχL ϕۭ>傫|@,y~Q`E Q`E Q`\/ˆU[Mu*e.@:[{7_\ ,M^7Ftd4aWZd͂SҞPyYЮ5888szNS7A݀B^oNp`TUq϶,ULkIJ?V݈bwBgY. Z5Cb؟΋ZU9M &Mt¼ ?HnN>iTVξm> 3^")ayD0(j0(j0r=Wބ{x'@L?]^K~EDJF/9Sk=2gYƭ[GGG!GGG"2}nqVqN;j0(j0(j0=/!R87+Y9],W׋87}NܸS1(y߅; t^E-"rxxh@E 4s?L(j0(j0(j0.wyGߝ߀cY__4,X{5>^ xڞZ5k. 888{zݐχe^J*1J=˼f)dO!CTuߕχe^J nC+'IENDB`bpytop-1.0.68/Imgs/main.png000066400000000000000000004036751416067541000154770ustar00rootroot00000000000000PNG  IHDRM@l7vsBITOtEXtSoftwaremate-screenshotȖJ IDATxw\;rD`,X(j`cbn~&b%1&AA1JTREznwM<?ss;36 tbjт֩Իuxõ薡EW@ސSld?B?4 9iy]ӶnC"6#-*%x#+qֶW> }nFTݫL<8,5flRRRRRbBlؽ?}VKc~؊9PJqΞwbۺQ6BBjiYfػV;qlkP$@uZv_a^un;eo_g}o;[Op`UPtC/ªC.ؚ""rZ˞iZO]evg$:=ko}Ӂ~eNmuOĕ踨|PvŃWKzx Ec&%>̈́"BoH@;A۾v玭9e喯^ ]y=2"2* FPmg=nɕT +7,G"|-p֧j+ KO-cHCrJ3b8ءU͋^'e8!/M{3vL\t޶2s^i?lRi~|/JN)VuMJJyDGӉ ]:{aD܋Ĥ&(:/NvC,?ؽqoe_ 6#VhYπ<8c@GhٹKk){~r}!{`UFB}>t{MU/xr1@!Ҋ^%r/ Aw!vsө$@HܺmZ;7 )*( !n7+ݪ7Ͳ()8!Jdo9eeؿ[3%nqL>Zg ǘnI>u7I^CA`kBtvQ n<g:`{RvzZ7K v;¤Vq^?5yT[˶ ᒣcK,t8C!ZcBظ* I޶20jUXS?[>"ib;29t5KÞswBgQ9YpU󗤒~:˕qfbO|~_ QX7Y2\%u}6MtF%_I(zǴɻc[CniʼnְLݜ n}jC{WBQ&F4IjЬ(Es{x%NZ.b U?6ՆظACR䟄Bn:򔕞.-y8_wyn/ߓW*/g"h獪:^SYlѰ+ΟI" #րm!d1)#(UGKW+/o\xB[׷2V*,K3t,:h'thmq6N|,ş|m>a2_&1qj']^boX}/|TfG3:ӭ&~@#T5~]0vS/|G77]ͨuX/=2'~׳Ba:+H`mQҠ]o_a҅1OL:IC_soUq|4E^>K//)q,Onq9h#aͪjd ʿq,5t߈3s-"t\Ué]RڸKWAy{?W"L i6:08S KB^fytgcI:5T`-A?/ڣmG!޻bT,Pz|rb2Kڋ?IHul~cۣMmhd%Em1굑?|R36ԆkvPx׈TӾWyhkCsM|`C,|9dxpjTɋrwFUF4E)rZ+ߟ}whf|XBJ|;鯡__+le'u!hIIWVtm!UFĘ{_Jz`өMSڕbs*5fԵh@+)֩Իuxõ薡EW@ސSsoM~hyhMjz4r~@9M~hs@Ӡ4A?4 9iM~hs@Ӡ4A?4 9iM~hs@Ӡ4A?4 9iM~hs@Ӡ4A?4 9i7x-v- Qm=M~hs@Ӡo$%$=?-C!Zc&$ʮ9 -sRqֶWFD}t䮭|@MPق%&Ćݻxwm)YoHkÃ>A-4]>ś9F\_8B.xr^srÞ$Hk] s񟏜c99GNۗtaUuV~5tOYVj3;g3ߩ렯\<øwDGkވĤ9USp)5broǁ4}az޳,?;o— {ǚh}-y4_zfUE>YrKR7O%ݧ~⤞Zs[܈5]u2"@(yn4URz/H*Y}"Ne';yTK/r<3# ?GB}tdR2i{!DkЎG۹_7y-Ae߮b-Uo[ZPgqy͚|¾_kh*wZ;#p/г!o@?Z6'yy˂wf%KjjM!Dh>k;?ٻ6' -: -j.>貪vU/5fX}/|zZ:rZH|o\ɵsZWn^uZj_,XOF{@uʃqk(áS5nWd$3*6 Z0-^:o˳RƣCwj^ZEPE!TVNN߲6 A?@2{w4 lݱ1> OfkYuV-\yٱüOBTϑ{/%+\Uj!ortZ;u0A:7&xZqkBvc\uS!$/w_./y;n'ʋwykzMϕ<;KdhߏQUOZ,KH#޴NZ4B4ۑ#v5ɣ(mm^1 h獦 @SW|~N˝Ң+jRT524A?-blzmZt@ZJz4A?4chQKlh@Bn޼pҙt0!>zh^GF5@;N -U#l8a\UcXtg#W<4cm[V\hB^%#=;TSN4M=FY{cPJ$lFY;YIi :AҨ2m];#!CUtEA^R^R&ҳ3U.K3Db;KfZu6U,Ң2~gCAљHW$|9ϕK8DP ia=:$&7twɍgI ]D?Pol[y7ͮZ0hԏQv;j(zu } BΤCUqB "t!gےhGkC^ um'dBt5N†6z=;,nǖɩ+8S}&3c}F)! !e[7h`vo17{XleJU)EW F{cg9|JSgXNglqF퓛v?(vI1[MZ' Z0= l&f+rqsZ& G/toH,eۿkk9ds `%UpZF~HPb"=ҽI6Ho7Cqo)c0,lnko<(4"c|)W/0sEelṗe [,VJ%,O rlDZ9g$AF*IU 9͟eGA *Irر|.vBSֻ(m繑+-4{{m<taӂ/W%֝29m}e񧺮KZXA%j,a׆8r/'ZȼOΝ!Ykȉ%mdG؆+ԭ[8H}si}kn(~S2%Ja_nIXJn_hשF_s@SpZy3>9m7ySq4;,?tDp=FNs4y԰wx=??m jF-+,jbOOtRUV fH~Q+*I绵|en.ʺ:9[i-4'20al Z3\nID`^ڇ5~#Tұgtk4LLL222] Kq.vYoMtȾ#al@[%~| ƭ(|nqzD~qG ئu/qkЂafk5U? @M~hs@yk93G0ٽxHJʘzEHP$Zq3:wl'2p2LՊҜ'7}Z!=>3$j?*&u.ѹ!2=]u_Ѳ7*M,F|yHȦνfoqs¸qc翭[ eV9΋xPefL]Ꞻoݍ(BM;/^_NN; ɠfXq%7VϒBihנ孅Ղ;_q]F~x~.l7`:vUPr!Ov (fZ4$;V ]9Dǰn߭7^*$!Dm’߼չk-4ezQ$$|Hؒ!;N†6z=;,nǨ2ZŸS}?5g23Y3nh 2_TK%h ~Tus!}6䥠QF|[L!AQU|9ZS7RBBClѦVrnᤩp7z'p=Rxq'W|[,>=3RcDmF;vƿxS}k NlIho&|%{ҡy,!VOĦT`#Nv3{F>p%[v'/k@<~>YcƣLg7 5&}9oeJr2ѢajhKhLv N)ɕ3w_NH !'"W2rBX݁6 ~Hv1[~)}M(—>PdaC߬ٿJqa/z4!{dIM;j mf^eۿkJ7\YPg_ީ wpo7i$2*h_ü9#KRxs7$Yjf.,Q MB"YRe幀7o޼$4K߅>;_Q1.G{evo[i:"=R~~7nǼ 2P g ̄M7B[趢J6q˓vTNgX!77 76U60 xw;9}?V9ߔ؛?u[ahYOk.gyƱ\Ebg~l9n2gҠ$rΞ'6չ9%EpP{z|-').KB̀/h9\7}MB).ͩKn&+^RɜȳO)ȍm֮9NA$9Xln~qσhi۶j@6?"7qI uvsДc FF1L[ǼCglE6I툉fbCj$RYR8@jĈڌv*L}lckm'F\9ŝ3R-DRQV\l4&,7%ܳ,{2/O`Nn&+^RɜgcB# d+/t]wvBȬ_~䏬dOkMч=agv54ǟkHSxUN9i1ߪxNY^D̵ksJү!.1BwŒ˾QyxI%s^<:Mad4GH ;x7'ЖvwwnF\RjZ_RTvxST j|jeϩed+f&K/0ZVZ̤ ;x(6$'Xʈ tV+U(= esXdКrK%Brͭ% ؒ")#[ ɬN &71oR7U\gtk Ȣ IDATQWܘ0o*YO7g8uL;xdddu ξ޸1_crLz̹9 ?;F mnGkg=Q7y!NTFBUCni'U3y{ְ1GrM'ܘ>r7Ü_xvzHeD/Pu| +-ϖFdeme%)O3iw ϖdgB$řiie-:CY>8-"$UJas#ilSպ-[V69کTב[_\-? ئu/qkЂqkF[@Ӡsju5I =zIF׸VYA<ʧ(!GK]MV^uQ.wqAwqR|7AyJfS{?bPԢU>ϼW-ٺsgǴޥR#6$wqCwSIϺ};n6nC&(O\l"4soG ZH =d&% =2ZDTRdz=w*ې S24\1c`f\3U:(ׯd:ug-FӨK.Iqe7[QDZu!5pV3$jܕMqyUAY1Ϩ4Ψ+wMspE♬;qE=+_ =/$x !D:8Ҵۣ knVx;F>9&%ҲY}FkC1_vgգ0V֨* -KnyTMh_,;4gk@rL7Yv~0o9{Zv8QA9ڭǎfӿlM (ѷss<_ps'ע#3={LURY-!}n˳f 7l (P*5È= 0P.55 ۼ{.1Y,{ک0It:m4yʐ74:;x=&c&% {jǤ)u/e\I{s3{Ry֭=jzߛaɯ-_95%k&͖=R 74"UTLnb J.I9aڬUFu7={ x?g0#໵_Kd~>UibhdΨIoξ$>^w'iIOpmlz;N†6z=;,nG8#DJ>YyKֱԍ>BxkRK&9_v}սA(N}R9_I}p%[v'/k@<~pđj%}9mùW(ꈻ3EpJ|=n (^qK5׽NSyGMuQS{3YdP|缒{Ը/W5Qfߛ"ڎk)yw@Ax뺳Bf<]:¾kx]v.',/Zȋ}rOzJpkj/?71?pո5`6SkXO!߬9b=o7_hB 2/{$Vm{A}/ɊTEB>:p(F`~=osyO_>'Mr6_׽?/;Ukt{ۺomox- E3f˕E?ߕ[RqBأ&z1[FdrSo]ԟ*p7xͯ2+oiL4j;U y(!KM9f\V X)}Zuy{ӽT eIul~ W?|n*K& ܷvo|K >|~|Mst-lW2eG{y:9J*{e!Dm2q3}gh%TSmھ= pJgkQ-*v?Y/`{x?ӤۣmozADAORIA<3LmU)R_m-cs llw^*U1{.\tg!jˋ5x:?Gf>.vBr3Y弌ȷ_Nֈyd܏^:s^kf㼟M"Z[\ə662gSKOyV_gIo:IK JYB+-(,*DAD9_bC#ۏlcwɡ3$vDg3]EIhW|/!7ҋy՜N@4#2^dckm'F\9ŝ3jqds谊9VF)}7Gn&+^Rɜ'od5-#j=ƹ 9%ɰ%۟MtҦ&˽(u5̊9̐[V3ioc#巟u|ϡb[RP$eDb+=8?AADAن -ҳ֓UeDzVziIA2rEͮG L.$/" [/<7g--n?><[RJ*W7USt,$y^U֠Y5qpcp*GwMDATwXL>GLLL222Hsf;#|q$2yL#8ZlXtO|jp\xvzHeD/Pu0V Frˠ;3q0c*Go<r~Ӽc]3CMrsľԸ\U?oke|[_FDjKDAD9kNآ< !DR"J͗蠺dT)!)JMT^lqNxjzv^!DRT-nAu:9E%,!.MT^* sk@ 9i|ؘ.,e&]M\ڰ+" ;U6d~*D~A]ʠFGm})O} " k[%4xiaxanv ! %0J. "`3{)c`f\3UBW{Byiaڳۧv_Xڒ掞\<񄐪w&Hn+=We(L,JfYO |qdڝUBXB~iwnh+$dtf O /WkL0hiukϑ=yÒrYgsz{* +=>ubeb(޶[vqKBFf%y?Ϸע{kBsbT?}?huto &[:w'qqsZ& GpH"Oe;3(gy.:sIƭI^yA_&w}AVܣTP#Jr pjAV8zmM5T7NRrIU y(!KML,\Jٿòmclϙ;hel#Mƾ;n{b"=ҽI6H߼.p3PDN*Q茔jS8[jQ(/;~M|~|MӶw ~خv0Wפ*yO(emڣUuG,UUD=WYݔ;Ȋ׳zIRxo1%lG68RMU%Cd.5Yk",058R C,Wۏ>ǖMfQ;ds8xŶ*HK2|GK۶(Nwv]7jc_tO z}h ֋vhm#ݥoh7˧)Ch#^NLVr9/h|RxvYqݿhVu:9<ϗ$^~R4s,:r^{ot?8[I\k([3FK~}ڻégcԞk)T!wZnJsM㚵$##rjd}YcϏD8 1x7V on)=iޱ.vڙ!׋&9b߁JjenixTu hfh;7&:?dߑʈ0_`s^vF_LA}x$;<5%;agE5pׯԳB=ji Ͻl@ylqNxjzv^!DR/iAu:9E%,!.MT^/6mrPց-N/NB؜aaA0n Z0n qkxP hs@Ӡ4`\m?sܑ3 T>5N3l0Ө8V7Jg[k?xiV5fH%OGz6rŃA3vڶ՗V;O0xIP e{~|lPld7k50飃C>[&V!:˫jVvxq\{jno kk`JSbǏѱjG{w5(k=fyk!!TᛍӾXU2u{ٓv7iLx9):\ȓ]sôYn*{jBu(8e1aFwk-I!DaNԷo-$jC"҄;{"#I״Ljq6ѳYaq; ?F54 Wq*ߧLfNgCؤr$dzߵLf!<қkdhA1xm:j*ǨN 7~llaHu^\?mKRFfKtD Q'vOH|S}k NlIho&|%yRy,fZMiњ6ޫm;gdWe9&/p?4ӱsoS xrl,|?RB !啌S竫YæyW4ejulO{3b2= l&f+霺/z4!=$!(gɒB{zWwu4f.,Q MB"YRe幀7o޼)B=0(++ƠDxow\xD5U2';it;Feh.#i.wvBBi[I;^r~;p)gyƱ\Ebgچqld>/|fyUXR]Qp(Nwv]7jc_tO>?OqY2t2|#w3gcolLZTIԖKr3!lthdʹu.9tfVaDڎl+<(<4Q%E.Knmmq?͚s0 &@L *kRFn?viĕ^>FK$EeŅ{Pl~.27- ǷvSMܼZrIn&@)h .VOHVkŹ IDAT:g"IwjB侴(gm )6(Eqp̮(BDl,+ M cWdO{zrŤMS?C~iV-3G?Z(_ݚ)!$AwĆ~q7*dhJ[TA;o6 booZԉZ ْ"Y %ɬ]0\^I^6GE.$_"Ty0/GCr(0A-~6IwvM$##CݵAw#H8o<*`GV ~ÿh;7&:?dߑʈ0_`ȭYxoke0n )y)B(7"E/mAu:ũRBS!,Lc"ֽ5hZ5PBǭykiM~hs@Ӡ1s]%sGt4P;䅮nV뼎k}Q.ǖL|'h!=>y:r+ Ӷ~ t;=S0ʨ]'.١p(Buwz c}PM[36L*V&Տ1޳|F@śJ.U}MX)Mq@\,lՎ{O!-vSɘ$n2g³@w񥏣)+w)rMt)‚Ā]s&Pס.l='8uryźyaژv͈9]_W ʴ}Q/l8;Գ*Sw=U+?933nh3_z(/LVLYs;U%ͨBcgS rc (͂&tٿ+C!z7r>%|%M&nJMXGjcz*Y'[s۬]&;WJMq@~h XڮC F^\U3+ f̷چ|c?2-^ 4\0GtbO1b{/S:47%;}شlĩW.>vfȜd32Yqe$p/*TP]U%e9) z[ӄ{~>߀o\U8ydYj['vWg}EǪl< OiFhrCB?[[o^Rg5`͈Ԝ?)πkQU - 9KBemVn&pY/zw*g nLa 5J&y}֜y̴M`>hˌt#^P6ߐo\=Uʏ珫I\{Du|%a# oJʃͮj%PwЄ.51EZs+ebӔlB#~k貭cf VOL(aGOK) UM:AofqS"H8'p}NC7er'2:y,?1S>#7)ׯd!PrI)[KXy>';Lt6ܩ0plĒJ8Ե 7RJͳn;`Js9{YSp?4 Sc 04rqlߵQ:6'i ;ڱ/rS\ }]_t3g$4ҜjAfr*'.S?qO~nn{+e<}gdg& |u|J˫:T3ldU_R3l?by%ά؊"lHtŶ&28Hg;xl?3WAߎjuGO2r[ﶓO#O|gΏ_")(+. 6l~YN[NokkL٤yfjyxA,qZXrYiM܍a_^Q uV}1on;˳~9uS]U^PϦ8M@SpZy3>9m7yB|Z9I½S"xEI?kMч=u!x8#N Jdrb N!|vW!<=bX?C~S`C1Cz|"\QI [3%7C3.FEP\ a5_QrD{ubZ-,lH=iw#FHޢlU˔MɕUI:XBEQ#̎49}yy:ZW2UJT#J[i2W yCh{{uעNHZ̰E E,#ҵ3dVrEhJ9J,2hp%ʃy9?w{s>!ҸSg+Be^xO3k2hd =.D/W8+)]X`i/t q~6xQQM~ v+Ruѧ !S*?VC ss*Z-XHLf1yÑqjdP LSSSKU>RzovnXEwk49_} oD_FE[k 0BX 21Ԙ޷֠*>-9$ÛCdž`95&xhCЈsk~@@?nP7 s9u#ޫ4H>@U0MM͆<P7 s9u~@԰իjQGa NB*BWoeTo+򶷯)Ri&Ha##ۢTg s9u@〵ܖ%A?4n~ f` OSғsmLRR)RZ-qPSDՐuS\rømDzs%.z*,aғg3%o__֑F| ֞^}R.Ӷ`mWJîH}M f&n% T{cYos.;znmo 8~ͨ(O;?}m-fc\uB,SɤKBu`YL8Liάp &iʹ?BmM,\}")PK{w 1ɘ2:`󪕫υh})40Qiפ \KKu"D>~"(cJFpq/W߱gH+77ScX⤋U\!f,)O-Jxi$1U">`=fGޣ+VpysʣQ)in8lﳤ;rMª7[goȨD|.ύMȣ6vf=mRGuP`JS7 ʈR1]uI,k#ܲj5$]7;95EvDk!YFǜ%mrs&6ݳµ- Аqvx003St1ujzҭ ,}-k˪傛fsw.87KE76wP%@kuӵbȸy[Ɣ1 )=ݬ9gA,e c?oߩZ'QJ:Md{F/u>pDWKiwN7}#+JOi7a~Z>>a op{sQWb{w5ceH![ٌX;C!*fW^6^mܝ6ߡ7X(|?rwfijbbb"vyӴYp4_z.f2_O[TV`?իV\rǞ4Ċ|h1 ~c\p*/2"Z8ӗndmhG? JL\b7no-xu^Lӕaܟ.=R9ז{͓jCHkg|Β6㇝pu+[W\?(0d8> ŃA FgL,G%eI xnϳx_(bxKmg(̪6kkĵ=8o 5u]kB&KpD]ХfùMUӱ{}KV파ڵnm5V7v>13=;Txd̗jEg0)[UmrHӆn! 'GfHTF\|7V'*Ϯ?JkE 21,,L+Vn̄R~A\|j[_aslL椘 %y !צʒ҈-{ P1ejc+F" ;]}_/bX>Sso7h?.88ͨ- q _=翿hQ`"]g%Jfκ=88r|w*bӚqJD~)1..NY6AU[0Ψ#8ų>6|z q"!UcTwvQ`xŲT5/MێڥoOBbjK;$[j%9t(vl&365&<veO֨]Ŕ°ik3(PeKgtk#ܽGURׄWfAoGJF 8CӼI|tLA" T= ]m8kǐxMJ.ښ x-ݪà%xqV쾍B0hg{eLJ˕"P*#LMk׋ s‚!nŶSmL\>2"̕~(fWYez@1>%E*Njb *g,)1UTXczٯnە.Ɗ _;7͔Di20udT*OcU}9MXB6#tiq p,vm_YIP/h$}ـ޽u K'v8MK?I\茔tu8:Q(i B]&}׺)ASVyȢ ,-tLt1~fzR4 %9/卥 s= A!2:,\0GԾ!Ky*9Ct9MXvme#"k3݆2ǔ%:mHsD}),nlô`'1 #]R] [ -zMntU)AEhgE A=32pԴg2ϡs4k4?'+UynS;@<ö 95V7D%^w3XGuƃs1/ѿKDt#bfЧ3.X:Y1rN8ci D/9 eBtY%?vG3,^Nzqj rs !e9Q_ߗRxw3+$Hјo`d%3^{_q|xpuƍǺjI_ K#DƟ\p۪||rxWo'fJ3¯lۺ_nCt9!K!t!Pۭ%0:z:K_on+Hߑ#2%oм IDAT.]6wYGN=lQp/ދ^l6L%bVZLv'8@a:{5Z '{7]}cj*>(M؁ym2iTZ455E(5ACb0av6Yw|NWMp=py!Wѳچ Eʨe*5Ryg\{fިf6 Mߨ j* CuiVR~ <Te7cwNMS⬈n|~o*paê 5{cB blA^ DEE5t.@K h@0>栟P7e9~ U$yxgT I[J#BߛQ) nyHHMK {9ڒ-c-XK;< svK{RӥמFYq`UAtm-s.1=WBed=4fōu`߅tUqT ͓ ꦷT"D:YE!X0G6`S^Ř}WJHO7kg_)b:5=ҎF5k@BmZaa+GdԐ)ؽ ǩ/Y33_t-{榅GPl~_e07ScX⤋1żR1멻8qt6BD{)wdjg,,MVɳ:%ˮ$l/`j1h[ed&u$xQ d6Kf\~k11Y'Bwbj;9Ű]TF||QG )/̳+"91EQbܢԲ*GU2T0ݞz3crr#QO>&Ql_d^f 25fj4ƒ{^ƶ7`{>v{;QW8t-b;˜ i{rX!wۘu6ghu~bA\tjG8mOv5˓H&'  !^`"#2 t .{c3j*7> 1o(~u>CBgIȄXzmc5x!M k=hΰ Q\cn6UOs3ùo#p8 /臨`(c/!D, !&cCHʴ3vtg؏87`"4nZL{Cѳ LƔXN?rwf)qCd/q_2ې]lFxlC jv凶LN^ i,ml&h祼|J̼-smI<ѲviCc0ym72T̮V< v2t3X#\TamHpǸȒyGz̪=,o7(eMBۃ/}gJ ~2T yz_T}~mllJNLBʈ/B~1oDEP: $**J~4o7C2'4BʾlS%3/!bLa|yf-m L(&A+X?e(Cl~Wn'˰XB5o]7cbwY1Q =wM:O)X&S ē28Txd̗jEgT  r2m&_0(J -'<$.\T(酫<3]Bխ_[$2dbXX^WWfZ ܀qMDƿ \9}6b K%3^{_+2Pi1Uk]e\+2 gv4؉.T/ϣ0V[*zNŋ9r1Q#EJKU.˶o5Hc!>eo#|q^*P1ǃBxav]M8j1r8!{s eoҙ{޶~˖XT]WGq:"%V"Al4W}j| 9OCcCߔ 30X0yWCfOz{Ii* n_;`SNws)CKo0jLn^-%#x[7xMyA0-H,mhx7ͺ"B6tagn0wf@8+C[v_/Gus}4B! $8aIZvo3ո**gL-yZS? 5g:h]anqez\LT aLkF5,fWxfb'3K# M \U[eNkwrHOˎ%F xRoIcT"45Tꍓ>p{~Vv>_4?siz^cv Q_s+OgР-#x;>okpȯe)'8ө"[DQ$`~@5mhƦ(B?n~%>bsV7.ܿsrꍼ-TC y +<Zj7TI>E)(2Kv7U9Y$;Xp;uh< Q9Y9$"MOxH4w.̚ɐA0>q|V@Œ+`ॼxZ_=gƘxEhrCތO].Bne ΞwR0S_&c0-c"!Bijհti ތL7kSk}0dW  2OC_?RCeyƧȈiNq%e مtAgJlH#MQB x"|޽u K'v8MVCWfP茔*r*LOku02jy)O=/G?e.#"3e Tj}{%'5?|>Q߱2)~]󇮸~#'#}G:hyccB#0ǰv)BnGEs@!&sǽ/ ND2Bw䈽3<7?f756R3u1SZ(Zdh*0m7Iݷr"o25߾X{LSy4 Z #G!x?Pl ϒr5Gg9*{vQ}iƿjǥnU#Z0Dޫn+v`ɖw|q>˸D\bWJx4Ѩgsv5_'R`͜.04%ѱ{ۻ_g+}\ySHd 2N*Kd;{E|QgCnsє 5NƔevoOMVOeeA 6kCͻ2 .k+Bg{\ϧa5 NRS^5BCWď<:g$ *뻆k366(av"$kXho!" >VCdCS&9gq`jjbbb05OY9׻h!ľ34ZV\j- EG 2wEכ?լc8|jҘ󈖝t%CꢓiƆ+'2pXƯ?L25k&Kt_<Ա*u;~\ӸZjlx3k[ ח̬YBz:2/F {o2K4--j_D}wބiUkޒ9yʀ -?=*2:L(8D9GnC/8)yOfߤ.:92KȒ3lMLOՑ!ұĘ{mM+QὩ2H~V΂.g\[h/.ڙw8Y$.M6ԉx8}ޥ}IOtwa9ĵE%1ASJޅ+%1{ ywsg7t!#or9C"?Ъ3;ۆ|w+eNhz&#}qzW;|q^b:t9B[`el|8\gؖ)2XĢl8G^ZύOM {Ad+\|w}I*G8q|,lSɸqz䓢θ~⍚[.V&rʱ[yoz3nނ݌}CW`-ݺٍwᖗYg@Gf@F~*(%斖!!D~ MFr.K)r"_c 02Zƣ C&c,bm(^p-bS*?났Q<L1ſ/rgɮE3z'iGh$ n"hϯvleЏOln#"Kij" xMr&^͐`0 ٽ[l rIja>Ô^%B"X& ;KH u#wa^ & 2b֎74-E" S/1J[5tz} s?TfEhGQ!DS)MS$IRDEv2eckL6g㸼_Nxl"fYalih~BM*,qetAJ6r@!a3e1IQ8Q.4azձMƣ#/{4㑒! #^>w:fP22MIrO3hG[W鸹MV?HѲؙCYa |~&6%Dr8xƊyE^2Gƽh5jrg[8O^ B)ϼ.g`c^b𛒚 ׶<+7ˊ77x=mǚeWOx.}^2ǨgK=~rv٥Km.Eo6gJzlio~Bla_mٟIX2y ~G`ld&]Vk#(؆Mp<2iԮRS&~x"8BHviJS|օZFHSA&D[Mxd?ʸv;o[q:b4+Lzk/gULaFx h1_qVc@18+2HX1]h/@wi{]{|֮<hK}yie(==wh*eJx5hv}nJBqqeaq~3q/m= !X5Zo5ɂ9!X4!9U/arHd u26IQŚNk&TV!_P3Q -ÄB'lm$%fMV[WNVyw}.;qBpZNiJٟ*WiOCdcijV~fjWjd/[oo{5 l5\vZb-9dӂӲ^cdi&lfS qkJ+\jz ff6M*N(zaYsǭU9<>ff6e=vKx=nӼ4]L6)9u~@ݨ=o?c]ڿψ""Q8oH4tmzh{C^ Ga8fna\L-TNUie,P8{]Ma֪ W21kyúi~Iy|޿"gT=66~O;"!pܞ#1Yڎ vnlwϺ}Kc*{Mp=bw%ra="*S06UlWS-X^X% mCM.M敖ښdIvQ1gRL,J+o]G2 IDAT/H/iv0R<1S>Wm(OOy\v6b4/"QWl>-4XOG1&VW }V\&Ja,2iٽkahJ~~U&{v5Tna^..N!t-W,ܥ]׉ u߃WOмl7}v,nm :9c;{^b۰nzݰf2񺾌DE:$YDsӤ&cُVF[j9zUa[Xp;>Uo5*߾3p^(ŮϲҳEE"&OG{m`匿Hg;~1rU!b?k9oG? =~*rmQb?knŠUZƔXi=\)e$j@1D]n~YL[߮HgYsgnon)Lz t9ݻ&BspmёbczV +ߺmރWz i\s-MnH?oDa՗^ 5e$"p1\yfc k!Enӊigxo ZG H~16UF{=i_YLsUL Ck \l#8x7bWmGR6E [2ޏe9uߪ8FgkBȣsOBLky~` E4 xn&x !! R*Ή\!YaYZS9NQa,#$dƲ[r F[BQe*7e۟}3=F| .V&iʱ[ocA0/{ۭˠNBJW'8 ԃJCk[ ׏+|s=y4Ba]-5Htni;,:|*Mըɽy]NKDc^b9tȄxQz>>y3[T \Y\V(\g驒EW##%^zR21w%S~U21C%c|TA}{Jc{T/]w"9MpB`K#x9Í]cT?᳂qC?ĭ._x琥q/m= |)n8xd7[GR:/#cՔ?Tf}sڕn1gmIa-3ЀRX+WQ9Gk)XF҈f%y\xMT|>4 _X^* @a}o+s@ݱj\P'[oo{5 lAK h@>P7 s9u~@@?g_BzzzzzZjBNei<"~ iWB- NIUgU 1 iɱa=G['UXFE@x'Sn-H~oR/"DSHJK =@K<{t2}h<,:ٗ蹵5:? 4gVOn8f׏GNu(9;~sÁr7b: Xhm"`k*xJHԻۿ2bL;&ܠQ\׭_Yޜa^?fT5\YvgIWV@=\`fb["Y9-E~_6_^?>nqiv=n=B"$6D*G-~eH n)lV=f{Cel[=|}}n^'sʣQ ^9fL~PXbjr۠Kt5%'^a!OS bȮ0]V@ccSC ˉjQ76Kf\~kj)t Bx6u3ֺ'2}Vld,'3s*i6#W(=/3`TZzzzz{D!hnZxDŶusj`-559lFjjk8$BbSK~B[uU*Y4$u#2IŒ9ٲ>#W^&ӯlޓ6zM&Za/w'.EX ]vBх7tͨի e~*C&EJ?/Oz"Yn5dL!|Q /臨`hc 㘁F!56fp[eIRZ? JD#}'!5NEVN `w=zu#W",')gĂDHlbMTTT1TBkB3/B4~ @5@K+o6A#FLM:S ZP)%3݌%KkaYmw%B=pkzo ["0)TyAZe1/EepS=!PyCh.<̭l v»ۗ  8Cғ^hI `1T\|iZ*ˤ^J8k5\}tf[?aZrqo7~&WD<<:Í&yzxT5}cNqUd D}̪OJ.[>XjB{&~4櫞8´:4HnM,h~nv_BS$0ps0]SVvZs..pS\] :m!V4Z x;>okpȯeZ虱ގf߳Xd;e7D'i6]ߌ)=6wyXCTF|ܸRVys6]mA'䗲Oe\B& J?x$BbSKTȷiN+]tnV6p:N4?'ܑ.LOY'dȇ Og9W1#3cF&~0;v4V࿀iۍ5>gOǫ[sO<"IS9o։#z{Si$2' QNu4Mv p|/48SjL9quH9X>n=MZ0 !ďu;)Pr|Ρ#Ojݼan܄k6m{ʰoݿabOS%ր!~ޑ#2Ύ[.9.+nYw9aߝ!/Tȶn۪};|3RP1YsL9r9@[Kfq1U]JB"$6Ēr.sZ@}i.--QecD5:MY\4."XL&39%Cϙ!8F$y|џpEQ!7)"XfZRtCV=f{Y[c~ U$yزd\"g_BZ-qPSDA(-5!?'׍v{L0Clǟȯԝ*12Y-%9ѴݻFxo)r.Zʯ~K{-!jjrZ\cxt.B{wY53"ާhEN_8/8ޥEşÆУi5EO_8o&W΍&aa MDO7dRGr1M9zrp1'T^=\`fbV5x8Xteಣ`XMrY8XW ]> Sꆒ®?O=N-̏v6%.+' !Z 99ICHxRr^ggus*!l⮜yZu=ZݼƏ[IC?~BY+/\گ%!a/viì.az]y6*1%8glZM;(4.$~ ^p=طA}9I">&Qx;#,'NEe xX"J%[8}`%lNi9ۢpVz860{S?)ꄒ 'p/HI'p qQ't3!EJ{a4wuwd- EW~ut;9ܘ˸e7PIZEL!Zv;ӕMؽ: RpG'RF n[u6g5x_t+MaP\eY[p;Z*)*8cL!|Q*Ji$u#2+•{w ƠZT^d4m(xP$YÅ?Ƣ y#Bp15x@sď¨kgn/dI{Ƞ$&o*Ա{LG(dxѤ??\b;rON(/ֻ>-L/)],KAPF\& Xsh׼l !~/:2fp,Y`'V-'Օo3"YoڲO@Fjq !'XkNMVt ꚄSN#M.mv 87+H JOIJ1#B!e[=ӷ=o[e b M^D Gh_iά'HbXm\ږMML,F,c (REv"K)nb*Pz}e`sPxH{OXASu-Yg"IQ9Ƃ9C:C_1ѹ{gΛBM]9т<e < Ldfv$AjSwyz<}r8EjND]mw*@ϻxvV+^Uq$&MB}·'&YZTvbVd ysAd8f` P$%DQ=*)J56C%Glk- 1 7,1'Ws3‚QJ}۴L0ѹ{ԱH9O?z6gbam/[MԄ ˯{\ "BU觻|rh㛠yྱ K;sJ&B٪ζJAG'/WO-7 5ςfkYGئ~pmxgT yV69F1\=NιlĀv*ik IDATm}cSdLƊh>j-r[Cf?Y;ſ8R_O9. .qA[l٭8N>_DTL?*4gۋ nto93dyCn"֖V΄E65i^W9;܈c+$vytaQWTNt5#; @`H[n8hJx7QnFZD"{[LLBDWeP0aihrt`G{_͞ңQE W};4gPk'HNKn}!7ϲz26~2^abS?=@\Wo~[xZb+9sg7X&_9/e@Mf}]vQ`Y@%d}c݉-ꢱVyJxJ~rpS>YZbv,C4$G>ϯ}B`Ml;kZ. cnmgVNș٣ϫ">S5d~ˈR>ǢbC*'c'K2׆m{'RHcChh¯wf/1zca5xwZ&v5:ٯ'?i:AWvѵKLW{޽{2QEXd?$,NTõN;+6 \}4˷xbG5`κbǑ=4---,,,ǔd#({Rh(/.>,C8\z-Đ) 5|$ V'.#%b!{3yz'rH|/%P~k"EMiaY k|6^lGeDAAAtEם\teC͠?I2Rck‰6];x49Y3 wS,y=hZ0 t(I 'iQ$GM4рyi)YE}e4l-ߊ 06=-~Vf, S+%CSr#i$fk~艨$?~ rmoV>B~Y-.XHZ+msM@362yQAJOpxt S h/r\{֣ u,fIGpE!t%0/c%Gtؘ便9^?J 3|}RZv륅'_?:Y>@(1ف2+"uRQrIg`\ݬealvŻ,a8jAܴc7Yt=QY1ΡRq[=5nܧ]d^Zw_r}2'_)nN{E.+E oʉ#t0 0 ]i.GgxzT[HhrmstD LrD]O }u2."0\lTwx"cUe?Balv'W7wc{rq+vt?)5B09}ߛlp I:!1m IǧR[P"!JEJK"uvo* VA2*z2܂􄼿 =旧^ϷY=T'Lu Tg+~$۱N<3I P:(!Lv\n(7r#r[KKS^_ꋞ* E~M&U3(WFUƀ !JIX@ ʁG !b! рAudiD-R}k@ QA@  X#:6}G\QSCwjԪMSe,;GB$_L@MM---*Q0@SEJJ`)+bG"ZT'5όkg0VΌS;e$Dz&4E!i[s'L@b[#/;3ges SE/V+*^ՁqsW^Ɓ7sЫk)&;sμ" j|o0G ]X8+Ws-I B^_Niyd! @SYڲIB  to[wFOup˨^ c]'mw*od۬ܽNѓcbϚy73s0~V`0WQ [HX]! u9.M哬 rB27%߸Q,͓̉iBYXˊʪVQOs3"~e,L 8y\Ƅ@ nX{3x'xn\WuK;g_5fٶY{tPs0noƝ/X->m! J5|smN]>e.ZHj0.Hha* {Xb`Í9C.6xA.եdߔ\ a~ӈlM ?fI#0Nw/VLuڮ@`ʕ{цom\$䥸ڞojT΍ ˯)aF O|r Z:[اP3+KlR(dlttb9/jF=G< Ꝇ?I7M Y;_?ө Z9G b[X*M5Tee`kHMXvQ)X8Zop+Wv ;lqϧ PYOj7 :!؀DھO.٣Zu0~7);S}c`IԌVy>A<㬷fdb p#2\ڹ e{*VeC`k nWq_x>`RROJ/?@;Mk:J{a4ɓKu@  q⯃kD#Tr|R>NtuF1B*Q[PlalӼ{}Jy|)91s.є1>=x#͏bТt iepӬ./ k^܏ S{׈93'v~ u8K$/i2SZB]@ bV2raM,M]_.ګ[$Dz&*"+2MҔЇxY)ⒼE@C~ Hi^>#y܂G@S\N~.R44E4@ f{>uO,\I#t?p#OL_<7C Nau<4nZjhWډ@ kh^fznJFH^fFnJ&C!!7P2fCt9'\{֣ uƔЬmoV> Bo:;Lb"Gr߶j1ר8ҜAA韎x}*-L~ы¸--!!!>:DCS2}WcT|!&SUT0p{v 4٨=ɶ6D˿Վg{* Ipqv tn{YrѵD<<m&VZ" n='^[k!q?xYơn儎@ X.ڰrÞ TeYg)FoS6qΘj1vl:O>65X~HvLy;qy:*Hs}nXbӞaejA)#;bJu+<+mraTCMNqz5<֨ɷsعw e:80jzQlpΣ:wŹ_<;GdWBKdZ(=:P7YvS?oy_K7Cykϒ@ED ;3޽zI5^v/4Qר/gP--<t/Qa?_p\0_s ֌-pi=9BÒp]fͷ%D?^Ԫ1C#3Pm wzG{o))Q+yJO~{b݊|g)`%RTl|BBBBDz+`TfJj/.s1T49ə5C`Jg~3&&!M:YLؘr]_ q~₾| 2!*a]U*/C gC -cLr?%Q^x ELj0Z,#NUDOLݔnBeI: STZBۙ(5_"LWI:Q +)yK#~@ĸ;t4 :ׯyfL2Sb #pW6oVv+1kݽ&fhvi< Cvɰ,P,5%RqQ ƟB1GT 'pGb&?d"rkÕCDTB?L[dM|0߮eAbtumhOs/fiIߥ/C.ybrM;ڄm쮄X#n)w[rl"xsDeqZ ':CIW3m=bA߉- h ~-ʴ.MhUi-廬J҆2!*NJk$Ky|e޿hҺ\#|ٴAuٔgГ~waުpu[Ok1InȑA0?~wa%Oc qpcZ΋%ƺKJ 1+*1)*tcDF=?ᆍ>gT-OLsOz,*wov 7u[W]yF$LN}tvquD_F^iXSͧ>n2ѵkm)n= rmG/1i"Xͱ:bɪճd.Z~ߔH`KÎȼwk_LuMdQ@ 0nET?4EힲrlAPvץ'8-}\vƯΖ smG!RYEPrٽN@>MO=:]P$I3&c3"K:Q9諃FzO$`Wl%Fh<2rGz{ 'o%ѓH8&\J)*62]FR<_\B1KuD7匡W ^~vg{N6x=0g+# `s$Ew:ePdԻ?vf ҪdV!tNDž gYNyحk"/IDT=i3I7-<$d]gk ynP f smy=M| Uf|kCEyNϼ u f sm[\BEç5(׷?ϩu@`Y2y6m;&Kպ\:J_I4n=| Wt>DT ui:+qNMN!n\QiMp+1>PV=.U|N~o6j\Ualzy4n` a9޽&If IDATct&XB2Tq ɻ9zhɲ55mwJ䡓bJ):/ hTƉJMN%h;жދQB1+*nʥzׯR{fjīt4EFtw}JN }p[D߷F3_(cҳ22)KYM T&}7r |{Zߣ*raGשQsS؟`ܨktO:=ɢa 1O/\Wxb_9=wSl ~&Xڻ17ck^Pb8a|" j͙ͺ;q+78|n̗hGhy$c- Qz{w"+X2V#? @TYu: }wx&޿h{앀^N_\5wU>K>IJӫyUԻVzm|_nzЙ>WN|MSrG'|g\xa,!ILApSS||ħݒ_\ε2z6q+ 1ō}C̸9rhߕ qNk1,2 KYc. "Ql)hh)*ZƎȦ\ڡ 3}42n'U WnEC^/69NkV>t*fME߷d~4Q^'.y'jͺb7k2TX='Ll-+}.} zu1sw^bgRm10999Nm9@[i.DCzn^ ͅ.ey8tt5MʨTg*Uu0*A=%ŨNQWol+{ /%J4@ `7mղ \b-#@ iSt9M8vo<*h@ "85יWZ B ԺIgAs |S9@4@ A@ @@ Q+{̀9,ANw6w|N Zz+QWqg!;}b%yc_ƶ5.!mASa;Wd@OlRpǟן.xAY)uqc`d\|LW+C @(w+>T31.2ӓ3k4~+R!S4u/?ԙ㿱1 4Z0. QW'U+a+SZ~HCMPN3i\=04EөW9 gu'-Ӄ)veS_}eEQ?&+tsS3+bE'MnU0!L۴)epaFT}Q^b\!X%6[qfbI >[d{SٚԮjpkG74=f.G6kC;pZ ~@R[_]i%ho'.^ѠMB*UGf !\{+ko% &(FoBGSX}/CX:?s(Ws5 MhOW=쿺u#ES4sϬNV+o'f#qkRێ|*U\zާ'>퇎{b]/Cŋ ƑJDujn?kp3M0͛bJg~3&&!M6:3!Ą7_X_fqkpw^r+dI9}~$ GՈJz&žA_tnb|hy+2c )Rl=N z'QP [ȄϯݿFń}q?t/Qa?_p\XxFƄ~pWyIqYuȿrQCkѿ]B< D t^_Y>m-OEN5Jc@іT#?8f`6lM΅e].++=@Tcc= XUp* (w~F`H*ҼW}Ȉ/S( < lk`iޜ +j$88p!ӆi2ZNq*U?sBi"T8 [dott?WF͇سuNW'yQq>Ѯ..2 .: )H!4EynӜBffK;0;0cud?z1}rsrrrrp \>JLfu,9vUN4uq{9ң|ޤIy1$PêEeNA5 SKGwhӬ]yB}(|Et=*U$ɣKI!M0Uŋs6n Ȁ.: )TdЗ4ahj$v'&kl(Ӵu&bjRPJ}~"N9W%2Z^>I\wGsmLKOQ?sjL1nO4P*=PjrJh^jrzVF&U-P.4S*c4RSI mRfuhX I[;W%'7~r+R>% :a79 <+8GC?]:5Ul9zH|AJ@4AUް[GM2M% 8>s]'sι6VS'\fvF<㬷z{jܸO76,ܴ.'c'\_1zՂvݸOŦίpho SX}ZQsۢ$=q?ZOka^}lmg&8\v!ͻ]'*n`ko.Opu,ocX7ߡo*a]߸#I0LYtE`JTi앀YD~Z䫓7j;]2^7j֝YSĴoRUv~^yhYuF*dzӻoG6MLNx'f6B~lLhk`d&ƕ-uAK1T.H(aJ: `ll\W Uۯ }jvW7wFOupˠ0wno3{c%g;X22ܬEtb+.NQӼ?)^7#箜?:7cI䑘,fuZze^QUIhZs{妆}uНW9sAxE)yi$JuV>\ڹ^5V^,# حM={:&A\(ӶC谺-m[wW֩ǪE.1컳F\svag>#1ی9hTھ1iŴ֔jN(2'iVKD-L[Ƽ뗔~=D gȣ6[; ze-WJ@Dz(ꎽa'B>3Z}}P?[i9b`,n>(|Ǡ[leI_}Lh](mu˲c11eڶ[~6 d t)qؽw]XUGMoqϢt^-UVdD@2[V[.%b!l8+4 S3:r7rC_sj!Ǻg&;.5m ~..)"b3ujУO.!į}cY˵ W:F=w8PY=(,&/" Qh Cgt)y rVa[7yZt%x .e~VgY\ c3W< tfMSpߌh(.ߤxNYN-) p&Pms \WoF[OP͏pޛ~. \Xh]8B2fn;6wgQ;Ĩ9h>T֛S0Q@aaX PJmT1sOsX|ULJ^T/ h&[y/sOF;rg6tEc=\%|?g 6c^y|yz2;,~cCJd̝/S@_",a \2wFϤŘ1TcH8.Pg}燾'Spxׯy,َ3WG;l-:aXZt P)vom}w+"S4M( ('E4I\|Hy!?$ 4C+pB7|@MS$I AԜPMDadYP'/sC'Vk܌$PRrs( %EDlȘ.^CGY+8]>: B]wG?JЬmoV$ Ȉ? y9Dܶ7+t_E6 Ikmbѳyccx *-|Iia&|diaG:ݿT&>&pSƑAEagG;d'q11??"Ql;ڨװ쟇R(bvyhܓ ~;[s]Y^rf`SɭϧȤ )-RFJʇQ$]CoQ[‡La\Ic 3"DB* J\U<#wQMd]3{STQł(*{`eU{˺6\eUTTzoNHLB|lw}e?B'L"Ste{97B(T8DYͽW^Nx{ۑ LvIZay$U!Xh)"~t=VJBiiA 9qxցKAn+ H u \p5DdYKȥ(?/ύ׮=?jk1oT\Z˳շU~z٪%osx:;r7&muk<JҷS=}=+nı؈BUwEqg17S1$%&iYE$`PDPQE.M"1*i\IK.7]ߦ[˺[Zpo -Z^Jos-%SpDrJҾ?;~E @γCMV;z)bWWT)7$X?)"%MDC*C?8"2R\gݟ83Lϡ& &q=1ÿ?gUFFpOGS UHdޟ8i8ffq J3XJP%L%?QX?@ݼsKʷ-&LHCL(^ILW5VKuFUOq{[Isl=)3^M잫 P7K #iTj<^de5?"PSw|!A6ShmZE b)`V[O$FlE#utf͚%,x"GukD~QThnA:_ M:gfEZ2:P(<*8W/+ f\iukj?@ (.$!@ Hss @ Hss @ HsC~h$3Orj;}}d35gU` bn'MDшuV^*Xb!#iii̤:tJuӠ|\mK`C]V~Gtpj<]vVB.ŘwZǃ㓭0)2H2ڶ=Lh_6i .4i[ϭpQNhSe2׹}M$m'i]C$x:#Qf#0xGO&4K@۴>uu6vׅmc@dJRr殺ެ0F'4;3yEyct5{v_>tOuϭO>ҘB(6_`Y'_3]I9?/gLj!U@vĎ:tJa7^F4]o %D48e u;,:8`,jut5K8UfE,S3Pf[N(+i&jB,C׶P%b6DC~tVE9hBLMX/dԷ,CdKqZzW8tx$ZjVn%di0ץz%NWZ3Tɓ)yHil ŧ[OY7nDZL=_vܫ֟ZDB\?I_C& >"|Nx )B(41S['R)9 x>##헴x唈Nq'=SN32Ԭ=[_dE"Sf~黮j'}箾NQ@<՚G/5-Ww~RLhz2OMa B@6U~bmR^~(!@3ڛTBn馻fĉ) jt~xI|33FebK~0!޲3xHV) ~8 IN 8UJ"bO5FCrZ0"n}LVo>F/Űi I{VF\\g3s8l\B'#-3|"71}s߅6@iA$T6TK%ςhyI޶' 44lg$ZO?_b=kxW=e7/Ku^yylٽDi~8TNbS-uةϜzUJ4˶QN-* )CR)T7UCG91W}j0NLiwxG;=3Z^;C$~}SoF1mst3*CvxZRʇMR -&X|jh>]r^ScFLI-.pRA~ltm(!eѼX 2ܒ_ <ɐy8Ɲ={(wgK'T!-&\l܁ZTێ?T+:<x0Qzk: j(g{/ЧwGeUc~󸋔F롓a?CrpHg^ϙU?l k|Eɐ,vs>3Ɵ,RCֈ3?iϝtaۡl5pBXk1PUݲpWEieR YȐݧ6js`g,.&xrtYU,zpE ՀUܴ1 Ev;WҽP|Kx<AZx~9D~U!BHut0ӏ)%XzaurPu-+1X1GӞl1fذCX엀g?ZS73:@։qϮ91x!ηc3?+f̩RN֏KQɛ8=cБ+ !ܙfSK59/uN~Jc\ R?ٛf)""S0K ΩMҗRI C}X#Ld&"(Ahc]Mxj$EzKzdԞӆ-.Pd<0! ;{aź;alnsguPj ^ytgXP0 "VAy|Y~3eӓ<ޭz̈́XQ)Y8 (E8MÊ6_{W&jCw~ Du=<]ư0g7MIOg ^ԕM*a<@"m&:sh!Ygh-̪@uǯ\;5xY׭O섌\ZpU,|`lnk#,vr @΋'L-8rMWw)F@D0DC'_s[1]NNtC47?G;!uc啓yi$\zft ['+KK+++Jl `T!)sI0G-iҐ()Mg}?庚kbɱU&ioj #[L5-:2[Q@dJ!P#|v sԚC)󍹇'' P D͙n Ć+VgNNZ?tQ jb;yJYḚIh/?gՂw6[m$Q!2fP~j_x%!H~:M{'^_1/@5}{'+Zה@4l2 ?cȕ@d;);=4kHaCܟ3kӃ_|;6e1I;:9.mt j.v=mY,l=5 Yzu/ ܔL< " @^5L(;.O8wWG?5qogx_Gqu&'+"K,MєP$qu8A ?j*$!%F(z̤N*!! i]E]}ro%A RdD5N:/z [na3Oc+DI{na6 zX=u֎jnJL2D"vZIopUp N%((tG&gXb {ނl) j9Vs!/'[z %khѿW/)͞u!Bx1üUN\4ؼKF2z2uG$oI$$!p!}Aq p@, 8 Ir*8$$A"m=44 EI ͺC{37ݹNe,_[֩o'櫛B:М6[(AnCP% "UjjG': EݟCp"IҎ@8kv|MO % :I\ @:;K'! ND 6S[EM/VDp}`ϧ[mwf^ k "%n$hmL\sO7\eQ4ˉ|Zޘ9U`NnaAc:5R@M8,`ӂFqh //-ڸz0Ymb -'rl1w]t⢨Q>-$¼-|Emas-,9^T :j!?jġ_b: U3-tşyL/c|86qމU:#t&Q=KCytNɮU" k;-RX W2i}*1#3 " L'Z\L]̐tnHNVAN Y'**EYG.)w6G*{(pZ chwbUaֈ@$@H6[L)@oӯJ'4O@ԺUp^Yy<)"dؐN-&")tH|CC#uPiJdžMݽꅚc^WRP&gD꘎ʬ.ڦf1cR~hUFDd 0!o{{[&wKʋO,P4{օXJ!7{6[ޒfClzn"[P.s>vJ!$^!\ݔig>N$s^8at/!)A\Xxz0Ґ(Xgh1IvQeO:@ -iP(#)3_s.67a;7(e:f;=+o: zd/s^4Dt*9|MZ/>@ "[P%2[ 3m/6n'G 0Ѹ!atGW]s% +Лf1tr9h59REpz7Qϊq..FaѵO{.fJdAvaf: 1jciGM")6PMX/d@O3kʾɀ7%hK=L^fSuP uCtb|ܔNsq{ߒOӑ[??5y'S0XOa-W~_ɖS@ ֢eZe$x븙m 4ʒèҶUje$S1m#k/;QJ 9j)Y#FsqI`ب|tkG4/sm?oGl9i1.$&BʱCF\93F.:g\R(*Aj,e[ "A@He]&ɥz{fXshRK6S`&q?<4b.7ß;}g6rLZ(jȲI=j4R(*fIta#~81tEvl%G jӓLzys~'Z&*2>qOc!Opf~8*i#jMߓPQB_6EZ9V'QO!kځT0Uo@ hڳ{=5H\Lhuq,Ń(B8PXcI- C𨅛J z,Do]c$,^Ǝ z5%CEȊp9ly= J!:C,PS_UPTtDP!uy4~|)JUY jxu^w=!/ז'vY;^^?nuoh~M}{/N"#x%oA?)CpW#qׂJ6cxCk8c2ODzCidgxh=іʪj:BsP]lj;.[qu/g?bkK3D%@ 2ٳs|ϝ2F;޳lhk5z÷w?\͆([/`j44wWX8C`ڶiw+J+˂BӪqi*JK^gʹ7TK'Izt'2eZ(*:.,5pמ./K~Uxle}fnc>P-8ɉsoKo{;]p",柟92@,-wJX0@~Nu)M}DYmҲrVeEYY )tk6y]vqT {Fjݖ-?vr=v>Y<cVbW!pm0֏lT5}{'Ii$ۃD}* 6`j-\gҰt{$`|Z[~ٗwl|b:q?(ٻmM0D (Irduo IEh+PPNtגA%vqijysY$Oɩ^zL9>rQd3L=IVޫeo6aCEi{n&[ +ǁ]I%m~[.ZYFϮ/'2hTB! 2J ~c(!ϊv6.Id8)UOlg=G+MYtق{/RޛD!*w2l@ KLi\(( kxf܄SqQ=js1D<jsNH).S}1[=d[xBPƦ"98266i3q `f;onSCSpl?\iO>2 4-lmm### D.P(PryYO]{ff97>2LQ2@LPNyeN0i~\ @ QP}Pogwk^Z4ڷ\&CM@ o Өc=oQQYRX2i98. p<׭A " p@ i~@ in~@ in~@ in~"3 {sZU=SVuJ!w)i޾SӒ-DA4gy_xB*39{ߦ-%vRC$1S?sa KWECIXmG[oIIaEn!$4'W^zҙC> X"BvnYCc?}ʕ 'v.lTm3`늾Ƙh2H 9T 6|m'] U;Bk?zO]9y_YߒW#:}V`Ά;/\ť:?xtyZkOYn/A:ޙT%"J&Ja]9@hx4"DNWw%=ER+`Y|0NNngstޟtO3r (wu6?:9UjbV^-pJ>3ao3l9K&xY"odo{<ſWw/ᒄp? S5:uqpB/.X$xD(UHo5hKusb&6w }Oxp_{_gࢭ"y / ){8U:E):!Qqo_R2E:))ͪMWwT'kj tuAcL ue w ϱρFH ` ^XB7+E10qƛ%qCA%;6Nu3V/:d=jǙ=.At)Y/!!rR9ݗ,QlЍ' <膐MLBN5qԕݗ-~㻆\tnӦnЋK_n=۫ᔻnX!)Z롋&\ު =C=Pt:tRCI"PtހNaC$$Fϙe" ^ >넢(BN*CR1@z6~{@c)u }(nƑ+3xaSW2$ k)y%%I&K1n_wδ:EmސA59hfb6^g&Zjlg䇄D5T A󗡷%#!pR7$''z$pv43ҢnCpǍč]Z=̾ʨ-LڑIms~tBi&n;;:9scT}gG5O((gFIZ#۟#KR>AXY b6#q~U'0 $})+TgmP$I$HBPH#HIRPUs-#l( CI  BPXur1Ԋ)tMt&$NKO2s=޺_aC:Q>ŔSِvJqIpƚ"\$ʴ?q,I%EDdb(xTQZ^{.2H@ RG| $JZW̓@IGMh)fDTaKX&Waz6V(Q$vTD~xxT$PSwG,)Ah׍#FGDȤ\ Qх5ZxjlqvʪxZ/'FFa 8=-Tz9W8Jb_5t֑K ^iw 4MhVVR5uۉM<%&NWYդk3Ԙ$z YWZx$)b%;w:!k%GP b V=lHqJ_\5xva%Dd˂^udz z!y28]ܐ2k~5DJ˫V:~̕4p"xt>5=: ~8>#( 9~HH]ئ/ 2K s$Zy{۷m8T>8#?A͉{W|z/BY\x-%]b<{ڜ⢋ĜtN' ȫy1EfotEWS( DgV餼n; gU&+d]U9šgՋ'n=Rc>ز^BҤ )E#[G!O3PW C ΓeSz?lHqP?G:Kr8nc[ЂkzPKʐZBOAgK*'StȆ(++fO B M[[[\l98Q?GHSB39︖ d)l&V~AjXx[chӥP9|P=2DrQ:ץFAQl02#9J!_a֋@ DA@ܕQu.<U.UZ3yCfؽ /Cd $WƂkZXݟPA4y64 quhMoʺW &4n4h.Q(~6.$a*pw[4cϺ5@Kp'D 9IIs\畹jQr'9Y=,즂xE~򏗷n$.D]lrٻyVUu;<'#T0@I IDAT$AcΟ~Yz+ȉaU ΝmKH},fkŶ?EƪU9 v K+*w$3>A)yhZ:p;N=R]k3aȼ#yBZY{ݣ}-.Z~"ZçikIO߱!qSZ)uJDAl &ZhYJyRΓwvw}:1Z Ǟ udQnέgQFU$0yj5I;5rUJ+%)94..xT.(d;lDycZ]w(â ˢԈ0h׾lv%)1 &+Y$DLLr63_Z9EPk85ZuNi)?.!}TܖJzZ+t˼.PU3 bn`@Ǵ 1vSڤ^}>IQm;z^vrDU3zWѝ4ySܮ#EɇkuDRdP!wdYgj_>6v(<}ԉhLnumМJN:5S:Ϧ isNĝ˘XG [ݣ1)3iզ¥KQ3'ݽ?eR>;.l0^̋՗nuĺ;t-]*"yHiDF jc'X~x8? 21xW'%V 1LZKsL5|C;ܙJ7}el #ĭ]إ"wb ӾKʤYh;T٭=':bҫ['7_B2c\,i ={vd\L97Kn|)fW8I3?iUWJ8/BD sFHJ(CLLܱtKoĨNtS2 {Z.UUEDۖ#ny>3ŀhox/7oUR4k؇39hĠYWNyU x]&zeqցZoי6 4F7axg:C:)C~Mc>1J󳩃g1i`Uz9IRg Տ{{g Ɖ!sg5 .]og#ܒܺ23vW P> If`{"F<0NӠNSv% d0Y{>gOx@~ 4: $A>n )RS8m5?8]ާQgh-̪lřV4j缈2DcΝ͎/~jIVtGA_٢5.iM7FJhڤ?|u|I- jԾiU#dnXwp4Ȓ}b"trgGă,}=gl#NFƓ22Xg-Uj3ӫx#s)(+`*il8 h[,laUVQ6FDdh"us<¨+P'Ȓaݦ@gV Y!恭m}~{^u/.:<y ^W"5FIWG=wm`4N]/DZgla:a{M9O1{Hn+,3C[gNKkŅܟ3kӃ_~eW~m5a~)"EDjiޮQ`H lak w-wK؅X#Gg̈7c:FJ"#I^ .$DIyv AFVƳNoJ'_AמF?FH|z5pq߸1 r)tU$s7D'(Jd^c߶(T.}opKO0Yљs `tϗb>J C!0HܧfSh:v _ ޟX!v] _ 7I35(v+-Yz`{[o`NnS?[Pwe ƝD0 %+ -B׵kxW|kEG-d}v藜V9}8p`Oh,-A |k!A$ I ]$PXrz 6,%45NKPC˽X顑"f7jɧ~E$@7UvC#X~P&C?FFŜ~*%1# O$Y.Sp4K$I[ !zyZ`|Sq@oP,{q~ 4]8G7Œ A$Na:ZwѪnN{W*Dc~V% cJެ d@j"C3Zp3}DŨfi✟qӴ IiZ 0AIocL_uQ _"6B ?s *Ac::M)`Z& BX:䕯Ym52E_pɐ'lZpkY}}C, A>sb -'rl1w]tbWYZ[Z>&}maA_(=3Oo9hS(Ut1!j_ .WPeg7,9`3dچf p|3 ̪;zގě}Y ) ?QlF"j>s=Ǻpzk @~*xfadSḶ=-Bԛ:zu(u#\D&A4MOhmsj1N;:,ZᎥó 5MYdˤuOόA4M֎1 //xnQ{.9ρ@~_,+/g~HpM&$-<O,ЪCX) m mុg^דjxyN܇ێgj-ops eo:(hm-9DeanGpז7l]nBmFutIƳBS %Lm5}]"1&Y|Nx . :]l^=T(LxwiλѢn'(#B Ĕ&qWE77o)fESrhE!԰w$Cs³" FO(M{ʪ3A=."dh-T 5*{V:x?L/BUCUYWߤ6C0 =NTe?׫`꭯_(a.dBt+SCO\ʕfQYZnFHOAtڵe_s79S*TUa5-ph,>v`?iՏA YkhUץ;}ƅ\B48e u;,:8`,J]Fodq8͠ +*Y8fDy^yN0QSeY% V-,aszԪ¤wqM]oν aQp=ljmkmve[uԪUk=qoq .d^I aCֽ !9<{ֽR/\垖2K${?iHqZR&zZrazirXH%$uRQTHd/dyL~݂QW>>,`Re# q$|TM4 dWXX0.#{wa>eqE?KZE͘ѽ`os'<O%:çICߛpõzg )@L)Ylf.HYw\1_\06::f%6W\%]d̜K\/ 7۰<bg+!-&+(&x$E0f`~}hv^㇇pK~\Y!}%}/ȉH(PcdeyXA 8ZrZKj̺ pbuY23+z G{(]'D%ӥ|? eZ/.|z\&Ƙ%G:,2 {CԄݼ3#3Y6wڼ=#Ԅۇmޚ֩Մ[oޜlo0ʪOJʯc0U? *TyWl_N! @Xzr EFG %/tR?K{$*c3˧8Jݭd +i%c+Ok߬*=s< tX䌋{Ta70^-37$l˖)$|{K۾\٥o<.kaZ8ep)AQ pï'yAPx=ep*Vv\/!ﯼO+gDZ9 QFaꗾ6E4>AQaes0`>\t{cNռMj"pF"0չ+SԈH"ۤ,Qns|/G6{9Y&r05@9 4c/_ ;w=7/E+ϩ2g\tyCrܡ߾MMM DniDypղ&ېM9f41C %ڮ8U-ve!Ǹ~)3~=gm,B:.")9"=w$Oep lx٭+9vs05QK/YʟsڙCsT`j{ZP\Es yLL {/_ǧlxmT<!-n8QYV|1-9cI?]}\ˍ_>[ҬUS﬜{3y;]oEnkOs)aȊLB , 6s]T7ًwn9E T Y3[=C2|7J& r@"^K)M(̧fZ"_am&^,&ڲ4ܹ&A߰:655(BeXt.Ay+ېMB[4l24A G Wܿ"mWz%^?|RT\[+Jĵu fF`T/mOر&r5ւ9s(^Aq4p'OH*|r\Vo{{Lw~zKJ-948s>{M'JoxQ ./?on:( mdٟ߭iשUmȟٟ݊& 2 y zjy"jtAuQGÚWt7*9-;ASJSMqj$WN(jtD,79/s/(4s615AEux|I=1t(8׉)8wK\PoX{]9"uiZ/dZNSj?4 4% ?ʛ%~(9 \"J\NдN&QRqÏLF@SI]TS+R kR$BHjk)EMSr9 ,d._N]xANsuzܶsv褨w* ȩj Z-x3qcOݭP 1+M rLw盦(i։;ш գwVhRF>Mٲi7%9} f~*yhuaCQAQt7MF~n}lt2zoC6Hl79S/d@/B!H~9[#ѷ_2DZM.ƣR}tx=i9ҽFO ._L<︞zz]3LѓzL5N[~$0I MBK*w3cf_ܰ(dnt .iv44nk?s˭#mdqfTQnB1Tmdf4O(Ukx_0}_Fk#R՞"&tCr&I9Rq^NwÁ3PKAU\NUtv;Μd_9x64[Tŀ4zEdaɷVTɓmf1d%187;oW2cgTT4Ϧ6U|U ɓ!0?.NJz.f?I:uq|;lmo&4Ok’mNk;aA]tOۏ]/ uA$-eT ;)Mn||` ȴOٍg&Ǝ=r}]zewrf.dݰlf:Xh }k (m r; }$zl'3do6H!?_p!-a$xa^`^}k o Eye&^`0 *q`0 y `0 9 `0 Eo `0jg|=7'G_˭99Ƙ%۳zY7pQ^VrA2yM ܚ2~"lىJԖCi6kN쾒U"@تY1yAV}^{ƚ)=F6I)_̏2;r>>YeR yLo[oW1gIo1QYfKfR#gu)\Rb^A@^߬.A!X\xz݌*j^!qRv7wgi}S9 QCgg{0q 0QU&([ura[coY$<ٻh 8iYZw;deto7SHic.*ޞ_WpZ}6YĚ8fTQnp(G+d~s(n{ ʕAmSמ-9.Q& u2"R, g|1rc0ӊq;&6C8hL&*w񃾦MV c Zl2h"׵R׸/C5&CSKDXҴ+gU`P,ZFkIX#!%+RŵVG9+g8=[ZQs]~z.5ʭ;ɥb,m@ägYZHTq&q ?tiA(;cߨ಄D?Y7g-Yw^yFF;~s݈~jټMP.\+kLkTˣd7Z{_ӊdO販'<ٻ(n#FtSBG-18Y4-@xfO?q׌&Q}ŭ adjK+-iUӏqʳOy=0ߝ̃ YW.,˱cf}Rwt \2! VY9 pFZ|̯b0ܼ1PLN#Ψ"̽M6֣if! 9FL{k7b)||K0Q\::xr~BiBm~Q#$ḯ'b28alMњ¶!yOsfDp/]0tJ-gg:N;tROs'#,Gت),Kd` -,k-9 a|5}ʸ?>lx"=4MIsNo} ZJknMkm'Wr&QlT0*)HFY@ ņD 5'b5ՅMv[h I;`w)r&'X^31"sjm uF4%8떛͍G*$G\Q-7["So4"&!|=ӚBa>Sq%w H[wenDJCRhj i)/{B d=-LoWrE tChT|253.n4F!UShڻ!o0+/WHi,cZ7PB[[fe%p<8ؕn4ؕclddTh+ʅy֎]9qhLOCMl6^8yWZ\Plăm&yK$L`r- x[h~TPE%xlb`dP%%Mܪvě)|Mk)uBhh΍&r_K(j.7`0CII; ̳ľ!eCJ]ċĻ֝om&iD,:VIir#Y,`0/ ; 36/zJi[0 z 'u1E+G_ O[v#&7><`0 tnH)_̏46+Cjj2޷`0/&x`Mn||ZLĖvcmb0]HwE`0LG+`0 `^48`0 hq`0 yx[3SMn|=}[a71LXasO~fU74>u_G0p҆Bo3{pM+n1oxX΄zG'e1 eS1uj]9fkklYv;1/>U_]I͜]HS@U IWOٙQF!꒎}Abv}gGTrl mۘi<'`W~hݭr˞s=܌3nZ|S7@ K0t:`ʷ{άZy"]/YHg 5 +7$Ξۦ?T oiaƹNc#.%GcI?djڭ݆K;(ݚUL8vيcYC_']DasFVT,Yeqj=2ӏmk*[YtTQb.V(STU.Qbn\)tp<8Y.HO):ei ;䖳M[V()JU!UOHM3rX״HffJ2 wČA}DEL{gLsɎxeXw^ΗZk^ӊ,RvlT}c+uZ1 4+4?$ՂS\,BȢýHqԉ5yWRJn#7;c!/WwgnZCK)-97z=!2n?nRpvVzPGl vrK;$q=EmyuIY"쑨o֩.;I}.r t-+/3 ʭIN0K[$*bb5[俍Pw\|$`'KN}3]:ܱ$Ž?w _vs9 _|hѢE|8]yiE*~&9hߛ1=y/yohWw(kFc?}x_m 45>_67uhn^tvt||_sȨ\3YSO 4g Ap_JPPl.8( hBF/͟lFHYЀвk7 e7}XLWه<ȩБit֭o'.#T|}ExP 5n cyͫ{=e)Nyg]d< USQ7ё5K32i/[rȪ b'Œ ;V}| x q}E^ۖKաGkɻp !^g;srҭW`ڕ$ HKOީSVc.o\ugu42ȖTEQt b-`;50CVwqԐv MSt |*[%4[ TAZF,fem+"KPI)iuT5ȣ7y'3c!ݯ2n";8*4}'4~{{-}EI<2i:\*2;u]RjԼӏu'g@.]-ׁ~JɰhK[$ih<#lAatÒr1;ǸYR%!-ԉ%2NL` iJ?Nw&GڂggZjQ4$IN.{q:d{#cԗ;s!S%P,y zTqhcq.hpǞ21 bD7uBD~We)0;$!)i\N ͞;Ur"&NHߟh$hZHdԓCgf@[C=c =V퓙?=sz=8%"δs~_OG޵5o; [嶡j&MN;F+lϰZWr-WM t Vwk K(!.4A6l/ﰢ)ePlﱋU/ח#uπ0=*,%~z;cFSyY^gJdBRCޛ]pL -7$g.:Ȧ AR?ؐ83rrP`a>g d0«\\yX.}36/*N;o3M)9 %Q/P?P./!b0F\cy:5OSګ Oh M$KqpHZ`*)w^?{^ٟԷ!@jb謌mԪ\Ho)a>cXY򾭇R:pp> et:.p\MI/:( 4^6o6m$p0)Úsu/}pYT?Ŕ\0)7MkbTKK :]XѪ1x^=ُ63MSjP}kӔf_#YG6Ce 0'2O׿46Ɓ7H33FٙPMS4"F˜&B+4w^$g.:=E6ۃFshgvO)BYwr ,D<]` iZ*|nfK蘖"OHLP@d !\C;I`L+:U^L<9b4-}ǽ:s?1E蜨K IDAT괣[>ˑG`a;:#ZZ&ܴdΊI /¹o7 x6iL}ǿ>Im;o?@PG 8krW 8۟U/|7˷\%ČS0<3(xDƘ\(QEױ3cd`TMΗ#7S/ؾGw*֭䖭騑ͨ ngGܞ3>ZpFS$"( ͬ4-S4PŹE}ؖFZX^[uE5rԢEGS-.(T /-ϱ1Fر iő\$Yng[/}qy f"ܾSkc>Q˦6Ͷ1 *?YsX,eը&?XR^G_ BB`2OH'{yz}kE{,36$RtLɩzͻҬ_| .(aoRvl&Sd")Ir`8;o b]Iu%Y6to1,%1eI?gGZVI/SKx_@wv>fi2UAAaY7նWtu&ӣ#RDP M(ALΒF@rV:1 ){*f\+Pi.1a718jiI 5- { 2GEFaMŢ{'V@cG 1<Jh.+ź:w_ 4G޵$g.:EXmAJ*tU(.˭2!]p@x8*̽7(&ꏘ-o4atetE6?.NJꀸ0čzu?v;tuD _0Fom[a(N^h`]m&iD,:VIirû0 1|>G(uEY/Qt:Z,n=YISg7(#`0 i3de"m1i'pZVWSÖI-48`0LQyGya LGg!4Zvc0# y/6訋t]$ t]  `0 K9 `0 Es0 `0̋ #kww{ #8;Jup|!!cPI59 p11uxM 8fmc'mn[U_]:{p'k\#$wy+"c';tOHǽ7%Ղkʧu>심\&r0K .oV(x*O`HmOl r4KE>x[T?}^MZ{<*Ķuu*:򶇀`Z'gcM|i;S-{vON5,_+ӫHɯ I{ǠQ y֩{4@z;qQn!1]pcnpBVTFvNmmXieCǓ8d;bPe9wv~-##ǞFD;Y ˀ6mT|AsMdwV k+rᢺ:6H:ݝ++gc;lPGA\$jʕ9tMe5ΜETViZ*+&mj򊪻"IiD"?ZlFo0˓=;#_5 ɒ)L&u^ph{Xio}=8ö 0.WRȾW}vdDS2r?kl)A &1b/#nR.{ZZ4Β2­|RD&Wqgs"k@/E)!NcZφM "dԘnnذju!->crŊ+V*b}{3{Y}{O_kۨi&]r.9 >|//zTfgĭ2솟WRa%ď45?&TV--@(Ny]WY}yG?1vʳi Nވ7CH_}R liQ6l6u[BBz{LRh Ivj n_AN$PEO3+eO7'㜂 %ЉlhqcW zyBS4kteYyjP,5TtDJ-je7Nٕ @eT_^4h=6m"&1)Kqעl1c{Vyr~~YHkCS*~݌iy`0 NiV϶uMF@0вL"ha0 o o `00à <`0s0 `0̋>,2Q@BXY:EB\S(,iʨ^Ftd1'׿:9Q `0p fةIS=ޘI^ZYmDM8rKՁ 3<^gG,\K `ړA(0)W(y0)_,f4Q5뽍趩O)ur::Z 5kͳ=u\:BSo!b埣NشtzEc>yt@+@pfןz~gnDW_:hrϗf-$ ǝϧ:j{鯩ޏo݆mJŝxc٫~C{sZ8if o({7pV_;U%Ff]gO [Fk٘@!60ǐ7{p8W#ޜlLl {~&S {;;f¬7~#{:=K Y2ͦ KW]21Yǐ/YtW#\?5c=Ɔ\g>uu3&_ht`" E[H$ W&(yMů5Q3ۈnZlcr}Kա:qY6]Xd@#^d/dyL~݂QW>>\j;O{foLc~[Lzм)^k?{F0 eWXHSz{OU H&1jcպkЄu]+Q|Lzm4Ws(d2ǜyo+#ǫu@z 9ԇ+lޑY*<#kqbGoJ*9wcƉN&]ȳYYd.6ωiE\޾6;uGOZ?4 a?9c{[2<صܞS|#cGƔ'{ָ!$SDZVHj ?|;#2ueU|~!4gSJHWj_O.UjёN߈jBL9&LZwпTmpG#D~iBl G_3lu'׿<[זYryo 4ZoniWgu27!_焜oa]Oq#Wo‡ooBHtdh{)?7b(O FGB|顩I?$+%Wد|Yg/ɾ;;wѼm--]v&:d91i,=705X*ܦ tnBN((AFi/OKڒWST Lf.TӖi@:@b9#(yq-P%},nRle#/J!Qv =DE c{?M(H+kݡ]&V#zq7w 1RNDy ?h2S8T[}@6ndNkn#GpNTIr84_jiу ߡsU)X8jd8dw۶U?uS×eOmB(dd2r_@}D"mo3"= Q(:֮Yǐ9@aXݼiAyAůe#ЎS^m1)u[. Z*3pdUe.T9 Bl Gq6*ֵg%*o/8㪮K8p6=g,Z<5i/@&ޣ?0?uӦ5ӫocG-9;+UmC6'L>V,(4MQtt=bEņhQ%N}5ϻ,;U4??gQ  }r-1u IDATȃD%ѕw"7v5Eԟ"ΥGK=PJR)RN"+// {&Raд8-j.~DX[9׉FXpH@UK({+\k b#;.n!.ŏTihC"ܽ6@RVJf&%zx-cƨIc2@f-@U28Iu WiGQ- (X4su>Y&Ⱥ_M7)O lg(?+ɒ,w[;8J ျ98Z W88RH'NuN\ޛ,[myeEvb%=;ݩY!]>{+-6mrN8SqcZVԶR1<CiT]mGXCE6/9(@gaAJw5i@jȂhWw7"D~װ'<;ޖ>QQ)v,_蛽 fF 4}:mB BGm[jțTo/`Cf  qBybCJ\<ҿ/p}wt<o6e[0]EvZ#JK h(zh[(Ɏ+vmP?q3gxrqsʪb͏%t\ݠ4B"Fm/Ð?{\{ӕ&5.9usOg/VK$1LYh$ SuR%X-qwKDp<-͚ JȘr8- p-җ'HVq cX>t7IOWwS>lHKC_t{h۫@p0# O]uhM]@@hcљ*OF_S Z,=]ƹpw TojڡF .CxDO,/`TCeJDArhвC2yGؙq9ie󎸃g%{NnA/{kᯞCBWқ}cT}yeB ,,q2Us["_rV#V́Ѣhygb;dmqyL2C̈Jǚߡkoz_ץߍiuAgtb8!#@v=bQZsQLKenپN?!ʋ'7<}LoSvpb@Uemŝbϸ`3:%͸@M9~ i>Vwn)N#%7Zri[q\>)Q#dJ)/QMLHBVB;D an tE:Q9`?wF%YNAKe]t۴sL')rM qaM'Fy}r't[z3# ~y0ɉO"!v?~rtImYQ?ys_*!My'ؙ% dn;.&O-6v%:Anb7jұMs^=X 5(Z]{_ٹ|{"Z(=}V~Ԝ8z{yוWc/7LQd,!VS^l^7 Zz ^ꖫv;Ỡ  㞣Ň_f5 qSO[4E3>3%m֋L@0O^T:ku+WT]Gee:]h`D!_XFE$+.!79!{(U{3&ʓ1N@*2`T3 cf7ȥ  l)l0UΔM\bS.HVZV]zdބ. E铮yxZn?]O7X.EV?\gY?9*F t[Gv-]2sLesio}JX/@kUK$ OEE/?Ňbtii:]# NLȮ*rGM9ziB.B՝E+ ׼h!2g4qo>GM8,n߾m>g n";:m?ۢ/x򽀝ڞ5UcN[>ʭ[V|BxGl>(X^U l:J~y檏,sK M=)$&V2tdhdsܹ$VhGUSl#Y)Fr }{~SbѪn}O r&MXqamw6#&cwT{{{bsdKvnTިq_.tԓ!s{ƚ.;"μ}=bkB_"U@6?1)4bbO<Fc ?8@gLδPyyqc?uT0#IK"dRD$>$܈gEƓN@/t7gv-6 J&mj%'Cdޚ4{kY7 igh<,H^ֲ{md)3ڈu p>TkZ``kԝPAhPT98HdDcvLuk0 ߄T`0 `0)`0 `0 s3B2倄.|' ɔj',Nqq@bljd֋Rqb0 `&X鿉${ʇSӞ{1iXbWը 0N$PLK[GB2Dۋ]a?i3aHK@Daz:]ok=7jc wv\t_·r;OpLDs7 n g2[qHoHu䶉{dd'eΚ'$)֝ef3ik$bfXLJjo;rM=*ꠥ:ui1avk5;$1v~ הI]B<$j?We;g}HR0{oZ^РŜK\} w>{+ 2NF3< bonضbX }:ܼ?>%Lu&U}?{oxF.\uN[TtB឴hyHޣJdm~U}N93C24+).A -2a@ W\隸и8i[ɋZ&Y^xU8N8'g%k>*حRZ>ޔǕ4~}\? $u\]LXNctS]1O\rXJFO(2i[ ee%Ug6Λ#`q=_)b;tLgs$N V\`iā2s-go,#F!7u@}Ies_;3Vl~a˺Yâ#A>b~_A[ȿwc%ҀUK]ivlrH2d΃!}ARQ+:}FW}Ζ'%OٰtF֊'RC~ʗk{n6m/ч)4O/8Qm 5(rMVw *+*FёAdm!inJڇ71zKx{n*@{?o/lĀH7#WЀp m 2Bz:ZIwWi8IB3}yΞ!~tmlh.DHߴk%CrkkZK;;~Ey3r23auHsOjh!y;AypqNzX$ScBfd%yvb@&LP:d8A4.N2N(ə0?/{LBLq2EfZjE*KjFkipi)@ѭ2'D?,=$.;7') 4!^vY r᭺֨b^ogNrDHlVC`'ia|`pS95C6Z@Ҳ>ԀD675+4 L45_և4чϼ5a lW S$;wY0ʢoSs_kB,}崺o^׷dfj Wr/{\&ߖ$>05Ep֠CCU?2a3.K4kBbkj)zw^E7ZK%OFo [̟5+//oF ",-NKp`]r#₡g!#:%IkpB +n^A~ C`0?M|싟*ʐDwdF""`tMIIub˽qM7` '(>LQ]cnx;uN*Qyd~駟~K769÷ݲ҂ٶ 8TnNtץQidM8|n_VK I@Fahi 54{w hQMyB @}ESz+laI5PZp$b2DjCe `{Q޹aKS,ToW|iwizj+ڙn[ a+nadL7jTjVQI!v|ynMĶrSX δ iu fA$"q/#QYa7{l^Jt=DQ4$ ݫq"yMٓǸ z|5|}Kn݂vboޮ^@g6="1e|#@Qf G}3dS & "EGNTrR~hNS{ĦYA3lLhaܟ3,MY|GGBpNt+M !ٍc)}ߟS|xrYeO,b -:Anb7Z<Zz ^-pET͉Sg~]yU7jΜM8Ԡlig{}"s%?:Qt#ؿ~=Gkur2hgK ٫),t>6hH|^IiQ!OOM,` e-?j40 %-?IMlBݧKySӞYOlj*>'QمM[~8"6aOʝ we}C:#*YG &M 57sδl'C*l>eA}c 9#~jY;cWs5(ouI `J`#GZUw{ڄhxJ!x,~{u!h6ym7fY)bzEO!9\'+T r\IRQl,bJږQ 9f-}#g[;zÂ?l{|~Ʊ.I˖z.4׉O Ο!pں<ɽקTds$ҪUZzr)Mf*bLOm]bE,)vU;?g C<ߵxcNw7 IDATPިq_.tZWTǞ8?.o/` E6Gs2p;f@{dFV |s`yǤ%znjRũч5hsJ(oZJaX_g}^[1hL` sFI;J3`kduk fuk[`0 z>| `0 3 `0 `0 .yNN$Pd4;qHRkX"x$`0aKoPIaiE,bNIsao `0ߚbhu~C-R4EE{<$돛rיye#O[di \>z߷ݷMp旟^is+Gd,JZpcUR;g$!V ;tp!Uj.5iPۭKBEhȠF!#$sF~:^)Jtꘉ6M|B584RGZgWB?}UtҖMC! nn,4(+*-R&ğ 7$7N(~QǕ~([I[oql&Uv iSsM }#Y 5HNDžYΧz}G ?77 l{l[ 2NN.HBVf''4Ee ^40 W:8^:H^vtp|3d':t'#A3WLj32mjh:u$ݒv*9R3MF;"|W)Ov᳗)u(Fu p? <b_<_0="pkA).-O'eP {oI-C{uѫ6NwMO@ao|P@5Sa!ڒ>aEĶFor˅ MgO>ĖgabdgUlmn!p )z⽒. NDq# f^;9 1 yGt:U-'K"-s+[l皇UИN,M2sBV#[lej.l5UtT9. F9IH.UY024sp$̏OVV=}K<BtGKk8J'Q6i@c$-A R $Y$ K"h)OZz} 6[>K_~9׭y7-\47N@SnY׎49s|[Pu V \DQ"|"nbK߆$wPSwu3ucduUdO#B8tIhV%k䆩3dʛ9SYƹ@C=0)/OyV5?*HY )a]txϾ3阼4])ޑDO&iӄc{3eP`gŒԞoqLd 1¬U?DWhϱ 1ʼnoi| I-DH;.xnڞ~wBf6Ǜ #cxxy{7t-C5u M,~\\DDFM{궂5v|dbH_f^p d!ѐ Y̓ТPswM/<ăWLٚK75ڲƆ (!,bv v|wU f@P pjO[/CJ`EH,Zt~BS4]4 g qyCCMpֆ̷oo4ՃڶM+ϙSzZ Ě67%{d:QJ'f(1u@}I?Ԙ8(o5̈́S^Hoo]hG7Պ5`-)[;вE {>W:FB<+mr5mI@hlC8hK?׃MDHVseI@YyUe[{d6Y3o Lō70dk4b}e{dsz4'i#lw9@QZJŸ0()J yGl&`/^:wƃ0h`m0($a=֦XK!;a![mТo1,_{6^#h+fW̅B? >('iV^+:vC9Y 믝ceK?TԩmKqҋ:#;rHݟuzRۭ62g t tm/k7욢hw+Ƕ p\HC|BJMNւ5ӝheOӭ2 BYHp-qB&Ԋkb7|qP:?F&$2dCBIËt,9GfoBmH.XqKPEiO*ʜ zLQ `@͉M;z.C6 -s؟C$ό,r0H.L/g9RZ:^ԙ@h{t!]₝ f2 j(^Í? 4nB i(ZJHSuh$'~5Ci;15H9BdyD*pA{ i'F.>6.:7<Ǜ}3|CL 9$.#ϟ 22*BõAP]rB80 G)us\r E)V"hhS=ldT}ymB ,,ju2x% WfstACoPQ2ig&fx 9쑰n"?oZ.ۓ?:9e"nH\b|l[HT klqSgd}a7gl +*Dr̄Y4 â<-$&MD{xmIz#l? ssGL ]cCe7Nf'; i/T'w&GckhL@r|i&XtvR:,Mbs\bB ayᡞT>읒/`ڪx7 מ^GؚM&x+38ФoޖW4[vu5쮕ԫ.Oqg5MO|^<=V׭lW_? 8گQpWFseG%#SEǔwS<}H/yаzGϡH&NŁA3"yժhH@- )Eg |Cs~l!qΛ)ZQTʴL!L)l!-G7~"fvRΞ!;yj!0NHD7c!3z[#:R0My3+6LE2Q[" @a.U|f׭[#SVE,/<C<d~)~Ý?}?`vq?>զ( !V ;?Mڱ .iSm:۞OŸ]Jg>eLhLL!ܦܶÌ!Д:Ŏ?z*8k5֮^pzn -_Yt^N @yǹ@ We7%FxEQz/`ʘvrL4 /"oI֨8 >iKc.;\6'v1ɪs5Ynj]SrrM*:;7{ͼ̅)^Xt O^;=Jl͟fFNBo g0z"Y$休 \>z߷OĕɳB־M/c{"avy_n}W6vk{֔>2/i`cۊGO#?nz1w>{+J>ty[>?.!+Bnyۄߪm>OB֠gέ7:K9.4 qsIVKX37 '0goǔ En8Ya6^"NwL)h9k\tNɛwA(F{ҸS; Ϝed&:d!/)kF^\P+m)ed 5U22 .6j8Voֱ=5t݁JlB%)E *7Owk}vNq+窶*)(n0J>d귾ns+Cb|ME3yO,ղ9'Nӱߊ^:@h. ؕ E2 ܢ彃spd''zU/:dhzeg>r~PߍF%"|Wexywy'&ez97-=\Ox^?e=qJ3=&MͰZ`lz$-̤G, w>WJsY*[yѱ +/CsWSHp~˳{HR6h3YM|p_ d;0hokˉ,,XpS?ూ _>_T7fކQ --.֪ϾV_Lw<*9,`Y]5_k_{?kV^^ޜ;9Q Tce]UB , QoVZ,ʦov$( {$azy'ڛ{jwӫ_ߒ׷=ꗣ#eiq}tZ/{\Ccز!_c]y S`|m]CB8MbwW=x [B0+Uzª7=MnaPtL779C7pGKOq˥b'n[s6}1qҽ_unTߤ 7<`Yncc ڵk׮'*u3 z)H^[ӷ6ʣ?^R pjO[V4O/ l.H:(7lj/[q]l~]`9 ZB#:ֹm(8834^ؘG־xEU=R:/ 4Z)Oȳ;3n ki@ yRSI5uhhK23AKDL!)iijHH-]\2N t$\ 1MI;'3,PYۢ7Ζ،a(F(VCL*%%Ǻ*nUvń Hx:wյ͗l ?31@ m9_kM,6#w0I z/xtŎ+߷~.ɏ?+۶UJ!F ڶ*@Y)zg~ҏ}P2D}Y?DIUxg/?xWð-֋('Kke/^OoH>VR -ts;BECEcE>xM=hVa-Y== /$>Pv }BT-fMIé[ueL FF~љ!n4L$6^ $LYe .iHOTv}8II.׮KH@A_'Oa*Z*-oYAurhqZsn~ WU,N4g7 !IPVk@d`h#P[sT +fڌz}q!d;d6~n˭Ӫ9]!Jt 6 ]݄>;9Kg.HjhugLf!M8o _ne.$܃r4Wk&r|]֪AZMVMZ XĢm8#z!Wf tpn)jF(ۺ9GFi@Z֚~YӢh2h~p5٨yB;yVkh@՗YWr蘈b`/Ya';nT/.sBS3#jK+lj:9P*h,@w^ۿk׮]wrp 4[ش\.Dy9 ˥(/νڂk$I+cX>A&:F y,kPdq!EiD@x@Zbe!\8 w14JC"}ؼ[eLH.E7b=yhQc?vq~iS1PȎ%|uw;v}ޚzE>C Vr^zE +|`09ou:QWbVbBBzj-TcSV; E˭!Qq%7;>u5MB/8FWLf<5&tBrqǕR_!66`{jݶ[V+{J 4!Ni񐲻nk$bb$N&`FKf'BVyd)Ѽs89Z .9rB_tz N$-c!;4MW9~?nIzG\9|u"̟<PPC<(E,bQEr:CBazȎt϶nҰPUo|XP-U.5L}sYO^E<x\ ᙼ ]{tս=9ġ aT) IUSuVv8!"dX6%HYC,,b֋x{{ Kv~eU7jW <]n/Ԃ `|5t=q\bHdM_s]rb!w+N9P"e>@;X=Ѷ،# SwLZB!떪&U}̮[C*OB+D4$9QZ,b66>?g십eڊDSg̔Dى s+5 [&[t̯[`0}n c7x`0c׻&1 `0 f `0 savݜ@߀L(BߕC+X"3`0 >[c/MOA( r{yE,Z- $o|Ÿg*ߚM3ۓkak XM֔by+K+2%rhX"mm|nǠ?nrω:sj^BFϲ7\Q3EB8sO/MѴ\ٹ}ۏ ]"~6=O7^7w}ۏq2+[_zv8uێaBS~ԃXϫ'oM[ҝ(j(oj'M Ҋ(uR8ޱɡ^|R+kK˚dHB:BaB /& ɞ\8>F}+j"^sc.7(wh'ᓜ)tip]&Y!a.%4i|qyD =heogyu""Ek6!uo W0z"Y$bھO!7~gse /v+/p_YΧz}G ?77 l{l[1cLr X!kGnᦗ0 ;orȾ2ՙ Vmgxb̤դ[lmr1 ݨ|̉Yg>^tƹ}Vp˜}Y-4ܐ왞-Kp*Ľ ⴳSMMx?g"k~/ 3cz\PٟS?: sUr7:6j8[XAsxxϦ!b9d=rFT|/VD07XĢբUɸ*+&W-kǏu-6K*{Ԁڡb [͊ 2A_n řyg%Zޗ$CfinWI>xϮ -4ZX2}L!ɐ9FNKE*}s^;[ԧNٰtF֊'RC~ʗk{n6m/SG6MK9'NTX19hB bi=ZR+`O$F)B*QMJ %(iU;E6˘O9$!\T5`9).P.ҦD-02;9*9:$.<8vu4QDi_fJ?!6 a$^5oڂDv[5$9+3-9!.&&&vj|zAԴ `g8/2H ҡsTGDw)ᛶpZOp\ؐvh7mav0/ ҝmI> A0.=3-ylfL2|:̿,\̴H7 RD' ]C!j됛.v݌ s*SY47#ωdd'}}CS%@'$ejZ3\}Nc!3Uܥtugf1YN,gg64%B6X"mM9bCW&Nmou41-$CJԊ!}Y =4ߔ~ x{F& w _ۀKu5\},6 Dӌ[M7vPڔŅ&FaYsxt#i:;3Vȕ斖 U#Ey3:i5>5ƅ u"q}~`~I!A&)/EQ4>7Z9씓 мqB~@X4;0;+5Dz4;Apvf&dKSmC s!Lbu!9 QѦ0>iLhG ΪM, gw|4%v' 8#_lomӄ IH+-K渲|DCFMyYFx뮼j^w[KL c !I'2EtKk}\v.FÿPW/=0iljdqPhʣ*|S-o=^4Ho-qkzOI hkh+lU6a`BZaU䑥dKKz\P!PU/0Z HprzQgͻ0U.f LrI:nMUhGF E>PNb)4Jᐞ9hmd`?[UP1Y7\hKLhڠs!zH@?^cwn$o}ef/dP5bޠNFQ hDN_Gd㓥B]`L3*Mb ;IyԎ!6D|hN fa۽*y]>硱 0&=C?4j֞ J(Kwr[2l!qOwlHpzVVYվ3G?P[S ama'5GYS433rŠʼeߙ򵂁.Ksh'wL_l 9\}4aJʔG,&2_cEAk9!|G_h}•Gچk쿧7չ˚?yȝB2StL5xLBFWBNj{WQT~ h3}LLfȐ"9!OF.cB+`.-@X 3J=QȒ<ԝK{,VKawǏn;qy?Z-S6/ htF|.9wpG(_! yՐ !A!<6|0Aڋ+%cv[Kʿ{tg-@XQ zgV9+z,2W1K,5i_ycػJ;/λl9>`V+^`RXP%e*KmK$s!1$$,yẵ22jrU=fظ %&c.LXQ [ʦ ~_J[O'_J$$9Tdn р|.Z$!5 G~n sk DϜ`0 `V#x`0 `Va!"B.ghBB.ϔE M 4,ALJa<.zN' `|`03ny[^+ {Mu{Mx<턍n@Ab,x]: s0 `'mnFq`:ƜԂy /ULE^bnÜznr*]7ε]X[o[~umL+l޳^-=x]m)y4BstF:˕o:Y~kw yP}Hԗ B^"fump% DDOsuA~zIT?4ε<8_ʣ3LKuԖ1P\j$8d$ O-Qr49dCBwQ0+=v 72uybSo_ɶtΠ֙49 [{gR=UgI5.FM;H+rT\0:O3bLE^g+9o1߄H(.u˯{=} [Py}s̄ 6ŸjP=ʳE4z;^^)圽{Qɂ3Cbz治:ʸ$9{d[HlAPCe'fdvE\X[5fd3. @!@ARYRYbC87pE)#'$' XLs Bstv5҆CcnSǾ ,.2zVm96a8܌bcVB:C"h)"Mc@nzݞ+DBB*n@_kpsb6~5.5NYMf+ [@H6ےZ./)JU0tY.m)?ϭl6@>χ#By 1L@a<.z>4Tqo#ວ?=߻{>@Ur9tsk!=jdb}Wd6'/r~aҏe{3{ >f]{:gƇ* oÙΊ[{_p >CڡGrҴEkvPq<e劓FCN;7~$+j}M'=њ,<eC PSC-va LڬkLkl=nYLkUèg=p2*6JUMc>fŨD=E[:9$EKv\U]}l~ϭ1hU5n`nKx'Ptm95sgJtηݞe|B2UTkn(Ȅds6^v~!]=⯎&9d%۵&!˿pNmBAvu[݌v:d%5ƪ>Qe]ZB6Lݖ Nqy0ĞWBHu۵>gj=DiվPG!_]NEzIG̜_{z  p}ҳtCg2:"8z+njʫ6!P~uuim !IZ3v T!C@?ѥ`FSʮ}ыѩsz5zQ3:5r7 [S97^?^.oІփsFڌ`攕pɦ/y|.p^7m DgD',ڍjV:jl|m!WSu7wPXDZ6wi MM?t2u8jDwep5leirztN.dcQ:섐8822U3&Q!K <$s':4GH@8Sb.zO8ksR{[T!v8ؙvs4@J꭭a:G)q䉒pc o]ڵWCYny/1d:X\eQq *Ep1?bמ3fٽ\>q2h&IZH_3 2xBJ5ܱ7/:E*݆o Y Lݼ&bW͎Kٺ]6=aoF> k՞꺞ɠ$udy!żeўQ Dk> %dKФ O#|ЦC}~}qjj9pzᬡϼ08P@¤hA?>(2sbko+n|IȽ˘:u:POAa6ϸ+{,/MUZwt$; 9u4J̼Cmmfqr"d $+tDO]av.SvS^(Ьݤ4W ɴ94>O :$:8iԷwySdY9T.pF;dE*9 o\8W)᳡cDhsD;lbDìw)%k*}NpWkטq4NQ$S\9gYg~/Lv&p}2dh^QmpiA4vFuj 8'zݺhA4>\2t,:AyYBUn5ԋ+ pla5*LT|yZA&vRĴN66?2" ԠaA K1dx {vQ;rd-:`0gnDS;fc\-LVݹ5 &y00 9xMK`0 3 `0 `0`0윒*`0 s0ϣ}`0 *p( ˔f*Bې: >X[075 zImm'y`^^kx 凷k~kfzs/po߸׽ +]!!i;5 O%汐a@DshIӣ!m,V=q=P?Lc. s*֩&z8w]if6OshiWto&>phi7t H>LwٜWJ MKqqxyL-˗lYB,3]odfy&`0+ }S_q]eWOuMJ[IvH+s7x ?$[ahpyybGgCd9U26^cgN՚򏘑i] KOMJYpy);)),:z̉Qػ;$liccSDp'$'l`I23DQ1+sށcWsT3]-L 8,nvD30s[g?y%g_ZϫF"o?M =!Lчn8kVruǞIAnP~ޓ?2`ץd뫭 ٽJ E6)&F9".4GzF'L?4IRe[}qE*d164IW:nHCYE^~{TuWˑ.OmiAYSEgk$2{G$[:]t~NHdn ΪG=zx]mcrXm\IT}tK>լw'Q6BB(u%26l%嬰g[6ٲLgx)sAVeGKf {w1e_#YWh޺:TU;BoѤJ LWwu[ݦ.M2]FQ{G_ݸ&k:TwMemwhi<&lqFn^:k<cee>CGHxF>v̶_H!Gb(c=Fco1 Y1dxPh(8ؑmb⡀g:d%RR-Pyd;0bs_t=z!37%Pt4Q4M(HzLtHEK5%aV R1ЇԅjzCf^@l#5D+K2cj(RDGFMBKB9 pɬ`Vv,Q>uQ4 *rOB~ugM ,"X'tFo?fJ,,PN;0ȵJjAfggz+d SN;X!N QA$b{&:^Zɾ*dHi LTަImzim717 cbi!HsO%@&TCN$i 4-è:$!Pd0+EnøKSn>$Y-yU|BusNEѭOD$D#lT1_^qR5_U^f2g?(~~Cr:9LRꈙ&2sEYA>GS^.doPh|tAёo|Ԁ Yr9rIX E:-Kasŗ$||JglBl7_GH'-lN 6[_(l"bHVjNY-%nj $8i9Eg:/:CD J*P{WOaV 9x@TCf nPӡ$9vT1[* 7l,%۸!bHٜ<8ׇ|iw"hC>Dȳ۰F/= fQwﺆC7دn{*y7w_6QO{CL?{o w>.~?/&5o^z}?O}yGw>z|},Jfb3- Ik.B1%T!0M^wwssJ7p2-"C;+nDB)E o <=@ Ւ(g'SGNq@gb`0 f(X+fskK1~{nX`!_4s;ި:sbscd!$֝Q}řo'm<9SF"`0!漽碗gfi{[_Yv?vwTu.Īc1 @aV}$i~Ffw3^~筗lѩ%B6޸ߏm{l+Ng[>zy S ^˷ɔݓM=\ȿ8X_kyo2EWzW̊*$Wפw!@H 6Ky!98iMY)ZUKB&],`h &9 [}ӰHMOq21?dl,A :MvB|8]Σ-}-mNuާJ EcRdKrK } 'l zyEUkZ|598/)2LrrrG|(z(fϧR__5 70Nr`?ɾ ڹcǎ;qݲo?ܾ O=qE7m|~8 d)(xi%|_^Н|[ˆq-&9!Nt:qL'-X$')9nRCS2uي0Z}dC%bh4)$r\ӡ*qmYzO;vq؉(s\sWMuð7-HPKzjOF#ego-Mة4nyn_03lCIDAT+xsۻznKgmQWn f&![TJcv__@@cbI9l颦B9,<ƝV)%d(UphEiÐ'x}?>tCFxO>g Gb4CHO 1p&KyeZ'|G?f1i?<T([9枚~BZ(ididpSm]UQbR ǒel];cŒWc[ 楎,'c',7^RM_uHnI#Ά*e}ʿxR.wX板!MM]Fu-y3]쟃`0y kC(ghj"ji 2@B1Dc4 Y[z>B@& 4 *v}C;@{\Iz@Gl> z}!ci\Y,x|Lx9lv@@8ÝVR_'Q> Gg9䉄,'r >R`V᠎*9N,pUNJxoZV~ڟs&f7"2rE̲5 ;pj0N3a $0쳟_O珿zi~O.a{}=>CțxWG{X?ۿ?_66AWl:ʾ×_:{ d'TtH{]&PxN[ w2$ @n^V^Q55lCҖANVd:P,&54x7#]Bu>\͹(Y@} K^QvzgLɪ(`8280[1E>Ny|cKA@מ/OP.Ct~Oܻ;ǞS<oy`0LLYp1 dsl6Z/`BDh`d~к7yWNs0 `0Curq++j`0 `0 `0 Іc+m\` `0 wtIENDB`bpytop-1.0.68/Imgs/menu.png000066400000000000000000003011341416067541000155020ustar00rootroot00000000000000PNG  IHDRM@l7vsBITOtEXtSoftwaremate-screenshotȖJ IDATx;P#Ͼ'̪IH BBc1΍؈czscc8^֌q11ܸ5ּF@TzTr !z Ǡ!;2+SUeee`p-z:[/h37{<=g+a?B? 90naWlw>r~׎VXdB-Kk޷H=sFyôo7U>SۇEj OFO#YYeInjCxY᭭7䙵MHi$QR75,ȥB,QTx9}ѷ8mE0!m$9D5yW$y5qr2G;-8!pI˗+vboԼOmzqewYL&{z'.ۄ^;- TgTH]CNR{iq՜޺E&5W2ǻߣv9SUƲuCɒ zXyOKS (fJZ"[u V݄F)."9Y='@_vw^żȅĈ8wk,{\iuJ,3te[,ړrK#-=SI[LUν%㞱Nv@]ϡƿ磼6Dz'_?~9ƩۢWԬ>~-j&:cסK~u.YW^ʹWۧP]噔ȅL4U{j4kRR'{HFn8gq} 9?lqBvk]5_{>зuPQ=Llll{Z=zUZMdZ,u&)pj]U^tVxlerc*9 ~][c|mS鲞f Zg`7J̈Xu ɲzZY,>8gR.dǧ񢢒~hs^=@THCCwɵJӬO]'S:cWG06ǨXN]){i;h'&tɶf{d&fE=/elV+ĢVD!T3]hB&ˌPASʇQ`zӄMZJ,WdB99Ѡy 1W'U^z@94%n3:G9:G.eA*#1aL;,퇞bUrg\I]DE#d= ]TH8^1Z !>"bV8]>8KձAch0_|=~@ oDZZwsɳ~9 h+OX]ob8I}Nnn" pY]!OΝ8|Ko&eȩMӠS+["KT(ַ7 P^_c}}9-tΎc4=HW`s[xbqӫRp~Ӂ90q~s`ܠ7A? 90nq~s`ܠ7A? 90nq~s`ܠ7A? 90nq~s`ܠ7A? 90nga}}}UHWnT!'90nq~a:{r_v"EF{?&"j3ë剙Ejrr18 J6}Iu،`t+ѓHVVO8܋svJ\4+L|I*yR6-w}~cZ~|,N<RZJ(%=9|N!sƹ#/LU0S!pVuQB&z|xH+6*qQq{B6nԲvեv[ 8ԕ.\n_RRmllthkZ?8zdjQ(Ƀ6亞uCo~y1WKsolyd6/ȹh[p6zae՜:Ei|u ,ZHQJcJK*UL*%FQK.| H'tL騒]us`gp3R,;;uhVȩX%FUTck( b5[u,^ʥ32zI\j}to<;ҐJ:hvӚR݃i z֚$͆C-gt OX"!wޟb8YiD%R{Z)d#'af+G|Fu^;3;"[j{Tb楜cٞBRB+A5nwė`TT!{ﮡcC~Z+q0{`/VPu:N$)bARD$>D:}lr!gM\)A! U{(QxV޹VAOMݣaOBR+Xbf%=޻90.?U83!ڋ~>qp9PQtF:?ga ej]2mvs`\0结RJϘR( g%nbb}5.^K*}Љq::Z_Η\JcLuxZ~pZd{vP^_c}}}ccE#n,Z˟<'ɓAU ʝ|]IF\. : rO[ [ϭ@e5ުHS߯'Huw1|~OO=\ͣ6a"14d/uc/.Eudf= O)-!OsQ?=Qmllllz3k#XsC~ލij#?1"D|71FxL*$2ohtM8q+fC ,MYsqF˝|٬Z̉C#P"gJMa?3 fb'Hx41h`[\/U_w)D>#Tww91o1ߎQ<\^~?@!3;4i7npY9[1;%2FlPʲ2 $wN+Jj7&"SZu7oWr4.URd# = 滈v`<m@_$&=M aP, o}jSaeOr*op,ȟE].BSjpNwaWGݪSCC_ !Tg5|Pi.V"4=2Ɣths7* 5/;DC2Ɣ"!-n% ktD8mXaO!7s:)RI"Y}}yr=cK0.X!7N&z^O 017L&`Lq\[9ql *hB&{l^7h8ےG FeL.c3]Vl^onu*Hn=g1z=C]|;0ct }KkI)sqLȄrNˊqZiv:᭭nn:_{E5ାqt|dsd흆wS Euz.cz=R1W1oll`1 G.^L.dZcrNy ngfHgaa!ƬK#pshqV=S$,nb45F~!9|;9q@?1(\!(r eIa?AO0hs2ܷ# G.}kx s``~,< EkL* z+eU[ M}Y)?\F/&z.V \j;90PTaprok~>*ꭤj:}nYJQ,Ӄuk&~E/0IL^GDFkgf[/c3 L,GG\=nz9`V`& b5Z3i̫`̷YrxFirZATK BztZgs^Y[.4d/5)Q+Ӣ#$F^ Lc n]J;wPb.Hsq{J෍B[ ^,*j'|ud_Y]'bi'BΗ,b ݏy`8ec 8Mߋ,iVPK|-s6&:|=Y~'=l6S9tݸ=g缮1;%DtrzKƢʂHk yX|ƙyc(x#E:ϛ9|sG8=-$b9z03cU+BE!? DQXćW*9{UPg'8V`3U jAxJ ~n|\n wN !}M+PզBaDJnD296n6&fzL5UFr9yB!۴9Cラ=oll`qEuS>IKJYF"P[t"곧f2_nLnr^R]"߯r'_w %e?M6S%Psv"#D;j"5G 1߇or~*ku咬 %hx͓k3F~!|^nROX[LT\O;J$>h<G.a:Ԛ- ?AxN1~NN<F" IDAT X&EQ#+H|V <=9o{p0[yk0nq90FoL*Q3(+H|>byo`di+o^,߼\t4U $ xìct(x}| /+GG8ox|T_^oϪq}CaCu{vG$I;cA0*VG)[:J!oKӴ&ɳËLs1:9iYqOV}頱Oq{7Y[9o;4+MVådcQefeAH5+Z{_Or쑜"|!Tҡb| PҚwo~ݛoؽe{ /Z< Hn=g1O?!rˏkY]8&Vg󿝓"ىira!oy0;o !16AcTmqтHj]t2!cz]TsRMz߭{k,t0g2#@)Ƀ6M+nK(nbk/Ү?Yea/ph2Tqmk H\|8gNgV/#WN J77ggKQv] 'tC&uk).OJGvtݽ5j&^z_&>j]m wD)A1CQՋLǬQ?o=M:.^K*}Љq¨7rUc)GhT!C 8I !Ǣ'q<]%5/;DC2Ɣ"!-É4]c#)Y c,I\txCjA]^_ħVlK VӮI-묞oSBua5}f _ϩzl.2pӞmɣ#\hy&rɱ6Lj(>Kz.c9Ӌ'/d01M9?1V]0?Fם#M-&-)e.gBu6oaҲb*rdfr^R瓔šEYy5l*؄j$Y[Yg?c9ȢV\. 5yJ3Q8r5Dj,^ 5'1?(x~ڪg` k~c2#gh`m#pjZ,\`? ?AxN1sY\*bYzAL.F.z>j34c4Fj,\Zy۟'?mo[#x5ìct(xaS“{ (i"aVޙX'/"z(Rr-2BL_NQxE2~ 39jq,{ĭdo+%zLIqP]|"x;wPb.Hsqg[L*K%V!D,*jBMK}euў؋I U0]WbP̼1qt+X.ry޼]uӸT]J5=FBx"15iH+\byFHxS"LY}<sCޔqBM;3BSjpNwaWGݪSCC_ !Tg5|Pi0Rî.B?ڭK1Bk%v7'7}eÄOvLoXgIWp^{1v'?.q!0~V,Yb t|:8[vq#>H)%MO,cJ:IaaX>lxZ.]iq{-PXC'i h/~Z;_Nr}*z~YG3Eʑ|S Eu A?+%ww'ɓ$%yQa*9o/weJgr$+4%3Q9rNZ5rkݜ* mᦫXKeX[LT\4hf"Op];91OL.F.W!\*bYRX."tQ^_c}}}ccch \n0hV/>ZZz=[_g,}+_.z + 7u^;3;"EvbsV)."q|f{BKͤ=#Zmͤ1d S[IJQ,Ӥ8Lޥul7{7X~pSU3&ɳË|Ofu^z=zR,g9tz&27IZknyR`-~9e ]guA #h{B`W73V!D,*jBMK}euў؋I39jq,{ĭ䐻?0TViё؍vJoxE,_bl}AA?f,XK?}S7)h(\ӥ !Dnqdf3i`&!2ڝFLǤ#mqi"~RSUj4OVJm%.V=ݪS5TɊŊƛ჋_$&=M?ד#}e͞JV+贛)q0V`TINJ[ay?>ճNl~-Dɡ()wݯQzC *V*5⃂~ۿmUQiyUM*rz(իFnR#]53EB9~q/pߣO㎗ޠQB3D0xE7or*|~vJj2Ls7J1B !J1爛 ỳ̭yC嬾JpTFpOjϊIs)}&5+H+\byFHx57RZhPQY]t81fSNctiV?zQn*SJIڹ^LnF۫'gqv.Eu61ci,}0gn# sX(t:-_(JWRGTwte[V!f~YG3Eʑ|GD{.-E|"#a/$Ti䮻ݖrL#+zTs6Y|anVڐEkx&KByNCȓ_r'MwA8&K} &m;+%w=-HJ~V$>^?ߚO6\.~5GFm ^%jZ|eճ9j&ܭnK/Gw?Jr/>L5_~Z0QB4o~v^5}H|ޱDGnx sF B" +4lD,KJ?Cn9<ھ H !~$B=۟`DQ^_c}}}cccQt_Ugp3qSo}IMO ӟ՟ZX,UݩKGkЮ{(wr>?0q~s`ܠ7x ڟNϝӍUWҺVkZ=yzt /v^u'@!?NhgfUywF[ AwSJQ .빾A@?/$wA=;{y`$^ w{9ݕ+}4»zu00д{^}6##9[g< w0e=ѓG%UOv90nq~s`ܠ7x4t+m5,B춠Y\.WI! *Go zz[!??VyGC=z,UJ#]| 90nq~AL^`<;<+.C'ܯ}`;,԰JV߿dbxk+,2}nYRGG<xVϡ%{˯I\hyUhj=v*{Rcy޶Xq_B;yж69v&uRjf)Z<2Rr'_6ksGiXhh:{*|VjDO,y&<Qiu3Fնh(\ӥ !DnqޤP<XwQhu38fI~A*.M'gidUUWBwuE}q|MR#]5nvZҫzVzh sX*)WH9~>WᣓB[saT٤.VmB;qGhoRԻQM=xZ'~cn >dr+i痝ABj塂*vNc] ,92<@Wf+d1M.xL:.2dO\ taT.'wVϻKsWbټnqEtʱPH[zNKt3^&ksZ?ĕ=^ jA,9pߚZn`b&v]wO(<PpŦwVY,䦞X+@ڞJɓIcG+Dy]ĕ "DJ~>mxC.浙Ýśz֞2?~?5ν֧ֆ 75O烫 A/9JcXìY=[.wccMU_G7sf9¥w7@ns:YG9ݟwlw<ѡX붞\zgF":`h턴;ӫs~_?xs`ܠ7A? 90n:|zϟߏu P/y՜EB!W]mAfs\'!Uů 3갠mf?m5Ke?YpJ*w) FR`-~ aUG[4=j[$~85wi0njy$c-@_maӋY :JdݮzŚcr.z!Z\z%s=Z.eNez޿ IDATξ춛t<+$ިՊ 3 D*"yVBȥBc4#B8ڿ D&ev7Ks )$R.of=c $o_Y'ۨ'?1䝟&z}o,]\|Ç:9ooKl"l}Uga`KAO/=;jwWtέe״U4y^z瓢=]1p߶j9'vh9όES8h_Y[nxj%>-kTlRJq@8!vԂ; VuZ~YZpg]EygZW0Rm_ޠyǡ-|<_V!rFʲC?ۏs?'tJ}sGi-Q_ ~C[|xZfB.lX+sˤn{ߚyަ~u:Vp›J !7j1s^޿5K5(!I,uW^=kZ9m-k\maiqjeW;澵m vJL=h !(79kɘN{}bau[__7IcOq-RNi@͓&1u$6][OWMPJ]g+Լp#4٥wlx׍}B!U)d.E:YVb-9띭Ebqn"Mx4aˋbAbVŢϩh㾩 @yA EɅͬBH{]^F)gx!sy"gsef B:#ā 괔b.'+r^,I]kqoTf+9_]BT?G?{ZN»/[h5-V4XnjVN_Y-mèV)qZiEBX03B[˪')-e(Y Im%:ګ'^(u_yU-Rҩ~˃AE5*_nJ{{D"#pٕet\_>:z.Ŀ2 x|0J)!ܤh+"{nC"D398c5jk_d[N[Jwd'sUʵ;4!Ճt8Qn]#E_ zٻD;!ڙ^Moekw }X} ;Y .(ܤݺ' y;?S5~H {ŇrJ=YhsGY{5wQsWs-fih-UIFw"^Y.sL*˂ݳ&L=gv7@H/{s'7c=Bs>MZ2Ɣth uN=7c͢˽bC=i܆۠J2ƪ'S5ʴLH?gY BB2d_ow{_}ӾKGGRȒlɮvNկ~JVeݑJ2*m|YsI#E%B^Xf+*|*+ǁL&W)2Bɲs\V:/]$JJkNxU$ GU&A{ B$ZLmcFX5z]rlq=}8 Hm4u48QFa”jS_oRBGG1ÀEb"n:O0_`}'Fp|pomۛ9J1uʪ\],7|Ш[+58Q`5xut YF6w j='NU[\_vI)wN5ý֩X_Eqp@1lz@VCC9 ñV]9UݚpTx |*8>q4CdcK=7>bNB,dR~яOpwr7\/Q/T s{zoܵ"!p1ǃ&\Rޝmcܤe..1:=3^{Hso2VwQ:>;pe-QCVb=*tZ&6=|]}9a熛`$2'νߞUB2xEa*p"pUe8ϡ?#& Đ DsHN{l!ζ;DkQgcAMk &=f9KΓ#"#7 6̭8v([|h:%@qiX~<%}/'ʀ^ג#3SJ_QL*xP-s#;TXEADQr}@ " %ҙdD0_+0]2Ea G i'MjJ}/3,Xte4{kRzDq^,tPv=6P( BP.þihXHGm BP( r\vs(MEdXڷ o]P:66BP(Mz#fpqN+7C(.ஞ*vbExUSO}_{2pW #XVF狨Yl5ST.]ʺ64ADPN+|Gg݅q s( BP(Us( BP(Us( BP(J'|NK^YImaIvujmVVdK.6vprlUk d.mҪ0{B~k[lf,ZR # ŧƉ'Õ3B'1f䏗Ogn(G^FU>v:ygtB,/rs)5)S!E5iz]'=P||ϯ9㘘̉DB.\bJat}}ڳjR")9ǭǵvNKKKoQn iÝDM:ҩ/2 +[nѡN:b3t)eHHþ}f,;mőKGq_iIJDçw3?#1(J9b 9X<Rg=9j+~B2Kn^DxgG2sHǑRB*8vmR FzEpI"3Vb̽*cnOZѶz 'Rt:% ' ˴V耱K\<% itf*ed,7c]Q3L9M #'1W%vWN#[ qbX9©xĵ}, `U7P.F7<" BqUV_G4(wr\Y?+SxIV?g`2O ,}Cx%a掗oh,h8JW9`z&^Xy+6*D]VM%y'ut<8G̊sIČ?\xv WVgofݶ7kMR*-Ωaw}i7…/ʅf PHd"\bʁSH\Is34+u>BrM:xt1VSO=%U?畳DezIS,nnqd޴p8 #H;[n';ygTU<&>~`O/-?O&fƊb/TE{g TSӘנ<'h׎ܰ&=u8XOL8zl!2f å#ez@Ksgk 󉱞![{Uf ;*}a7\ٯ[+@+ʝ<)#@cImB8%G5N>'RaА:^>(! q84ߘ.Rr/u$GW"Preãs 9i'Rr0YB`ҪQ*9@J0y|UEA'oZ|O6vH,|mOxȪwl3g bÉCOEѓ#y /uJ\|9,dyo_t#3st&1vohWϧ6,;?sy ) 8q[Y"[x$cY%I,Z\=h0Š1nI,Kb)le`.seeHmBN HOK!We oP.\I="V;!ܪY?FbnΠL.@]brVtp}Es( 8|8a]^[f(>(3h4bTUgkΪy\E/"9ldԽv"~%afj@Y[{Pw9D_\۱a1ڎ̒W>S3|r4u2:^ !._8 jrqH9X3 KouG tXNӝg{>͹$p}m/IUXTZQ0d9)Fb_ܴoM_#C:M ͳO+kIf9ҩ,$T(#cȤ-=܋jk6!_+Ulcetee֪0y%Ȑ=R{*•'vTb&j2bd2(+$U`/4&ozg\pBi~[8{[K3cL"rzE^uG'Q.5XcbN ru 8K D9妑Ct?aKQ厷@ʐ_;XH 5}~?R) HVZ%GS)Nq6jnMFivj{\"c# \"_KcgNi>dZVړt}N5"Q![.MJ% $Ez|d @i)4jYyKWm8$ gt{[Ne.vx(fp_d*h˝د#3Fʽ`?P:kg0 Rcouԣw9o/Lzɕ㉻(o^ӰO<0K6J&R w Uow9H;23A R(z5osR( 媑O < Bi9◂ESutCP(Ba-Rqсm B4ESP( 媐}BP(M$P(FI$=5󄔚C;MԻuB\ 5/{) BP( ͠ BP( rՠ BPaF_6~⍷4 O|Mem :^OE5d=7^?zF4BP.:0'BWR6|)I #gs \Ha9lfG;;'>-|y7&N'Fyf%T V K!6nӫXNbz^zePL-.qH !GDtKfe.}l81ݎ'c'\mzݾ|?7~+M:soz;ϟmNx"0/O筯ͯa5/&u ~~{C ?S{[\ڴeEenZV@$eP!J;˧YZ|rcJK)[Qgtr fNyDg5Q-ٹnsۜVuZsCգ92F_{;f|*tlmPxRyDD,JԔ}tߟBjְٷrJ.z~-U$3#"9bdV9C5%E X2?|WY'^wo_d_~+f̑G_G0xsG-CHӥ4!N$rmJ5vyVdVioFliCw޽1{Fv-Ka>=h(o$ũ7n?Xfl0:KlP7n1`ڽ'2`ّD1Lz?rZ"[VEh׿AJ-?79ӥ`@zo?f}5d'{h 钔{0c"K,Q7X!^) jU('9DYK I$1ߝ4١Wark+!KEәɿQ{\mH6 ڇ-z%Jv\a@H 2֗71S?aO|{~[^mz?y{42a7NWSgo )ft{dߧ(`/7@f7 v;Vر/?ɿׯ,n_P;c] Q&w IDAT֑|1?94Mv׷q,}1ώbK^Pp LJ}$MSr 7M"BφхQXOx̷p0`C Α̚}zh8u x JsjX]_ 5#Ui5mq>WH JD2b@ѝfIᷙz a0>1y!h A3OOH:Z`>9 M1oK_|fu/CoWDz}.l읟y`}wc_?̷[Q]b6G@;4he"4ĈI<s,P+D5,Ein Eדa$]2|vE(o#yPjƄky+u6Hba8-` 2v-~2ǿf?sV=w~?k_;gouuO|Kj&;ޯwG"Ϳic_Yշn3[?{?~;^5[~ EFFQ L27*sܹov7)&2ZX;Qu~Yquޜ0iɫ,//- !$IVOvɆ2GGzqFHؐv.Vpd+C8x)u{׈!P8o^>M=ڋ!傹w P4Urn?BZ <Q bן igjsA"izGEGjz̑xϋz''>$|?;vj~NϤW7"ƶxsC@lw7^7gUN'1{".\o]_ysw0ZkʻMejTWtjXG2}2aHtZ4*K8cG:bL۾䪭\Yg?H{[_j|˃}+_?__yTeS\ ONk5 ~?~6sͷl"Ker޴ Ҧ[S_?qʳPHxO1k_{jY]kiV?BK^ӿGO5ʬcpdbe'xN| >IaJO u)f~&$jg}"%EC2E fC VFRnJS>Ωc$8Y`gBL}v>8veәX($!yL GPR3Q$5<+SI.⼂ޣE<&SU]m9 jzׁȮ\knJs8yv+k;~5F"PCxCǟȧu޷ߎ➽O?ddO #q v6I)KR{KQ2;3>srgu,Y9JO&'1"6@qDuxw]A>kBq㶓e}#O['O;B_o|~(ѷ|õ0owN6;i--n)|: }Sǯ. }oc7 [U,$ėoWM}"re2Q6,f?+dRuYm")1ջD%"l<(RԞH5RL}ƿ8u[C`/B2̇79j0+f9 o71f&WJ} fC³XeD1 yD)Q~;wqs:]p(A;b1qb1i,>n[Y75Ҍ4>c_ GQ@!?ҝKnp̒]:H{tuսƹ̯w8Bxm,r:IғII S:Ew`e+"i.)jgpK _ng 鳏h< fGojсF?s[73ef 篖~Ǩ&(/qx7455 {H|U߿^LjWXX-%N17~?:F5_ ;cCxotB:2B$G; GtTOHKe4R$AEEiv|V-…Qo2qN,UD\%g0`1.yϕȓs$S,Fue* Qw}ثw͂^%:[hg.$5GT(9K8#8{ vD<] G5Ro,q349c05TȰF/W~UۉF嬆$](MePXBwrջ0o3ʯgNQ-yLrLObcj5s GĺY+m7BXg6xcٳw?'._>55{,J}^jLgkv Wz׷xAcK_*WL oE0b$ŝ4;7˲;#o.'ޝ~g2;?sу?NIًK|p%TG<_.b+8O2[OsQcԴu%i-ja;Q(ghbpaԙLI}zrӳ o O;T9A 8O%3+Stw;˕; !sWڮp ɫJ[&R\z7L̥7a+=zq㩾@~WG~}Է_7??kV`"޽~q|S[ؤ_O}+U S8Wf,MDXDI%O1 9۟x᤬tcOʻvܞxID%uQҪߞUB2xiH翎g~ y8zO|w|Yxl83 #y |̽_|-ΌpxʿfnΖ9A}M<ׅǾ ߾WS=PW_='[oL|7g>K@ܸz>kOϧ^ֽ}_|*b֑dbG+EĂQUM{Sbڥ"G "޻PœU,oeȥ6Dr2$T<=CPnD^MECToMԻurq {zc oQ|dr\y. u4YOd+BP( V D"mB;yb֐رCh; BP(JȽځ{.'b19ϥ[!qBP( ܭ|R;i΢";:ΡP(BS0T) })=CP( BPtCP( BPtCP( BP8SQ2?9E/y@(ಕ)6ͬSGRӖqjeTlE|qI'ҏؒ DnozRT#!DVmU,N[['&ܖOO c/x|i2ۋv:-–7&Ii+O#!fTE 0;FTf#Hl=H(XpEm3PTFdH 93:9a}årkY/q_$}R$ݑKiGMG):o }+m4)4-lQwipkQ%Bh:z ꇝ ra>=h(/2U9dgqkG=:ȡ;q8lʷ:'%4ZM»L,MU̬'c]Ka8`Y\eu"s]WܓܽНw< }T>QW3:A@W<1 "|A cKEd"{fӔRf9#1ώbK^hG]i\Awt\1IOX/qe6#r8Z(GgWθ΃{^<*XޑV7+}on_un1y.$#EH2S$%|R$S[mqRVhOI(ƺWn4jKF#H77cSs&d#/H!(K#ur\m- kcpܝw@'lQ pJ''A{w13i!zq3s6&'ReZ.RA5HigjKLyU_L2yl+sj(JϹV__UiKGz҉# u3Auf[Y'#g#RN&zYo,)eHT҂ڳtO!NՎEZ2`TeλlbDh8'Hmf{Ѐ1^cʳ7Lנݔpqpo}SvP98(aB1ű1GڃqMCsq{xrx@ ] Q(=Zӎj~H 6 `Op)UbgFgN.T6kV781nnf Y#-E/YꟗIl/:;됪עY8I3RD(\ՎEj2J%\GNH%stJtd!Y2mv}uw]{Ũriq`iڤo'^ijNj=XH;62*20ƐaH׌fa^\o;Ac!y@!2rJtBG1Kjwq'zs(R|^Ak7M u͌w3\1)"ɬ#vT곟W"5JD DM:qM#rrc.I~Q7,t=Z@Dp,U 'TuPS#*nJ"1w-FP+kKG8rRH8rrSX{f5ÔNamP;|g_lˌJ{z+3ǐ$9Sj+CaisHۧ؞3Y=JıpTe|U׈J>-<@Ss"阬#YEbJWJ!moe$S,Ea)=vt갟W%E>C =8M2M~?7wsLsWx+o- zqbP. rs*9rwy1I0g8GGC~֒XFb!?Á'{\V0:ƿ(Ek\3'X;jI9,,OJ 䱎lO:&*"c鎹/]e $3&0r;籟W&E*QȖ#LQT*U{5!T Mջ#ukH;|ǞX|\{?~+)j* \;"B)g0%L#ߞ@e BP(M}=L$BxDݎ'c'\E8D^LF';;WR2)4/MymާW8nmD;"A a3B2 4/5%w5lr(.j]^?@a1%;fSG2Y2- u OaB~̭Q3q8x}=Vo B) gGY'*.۝;YWcrr>2lY(m#ظ}ȬN1&R X ۨڣAPPÆםewo}I>ma+ CCgE2w#B s^f@HS>3pO%g瞘Q`oƨ {C&$FTsd4PZ$H$ԃSY&1no%,\ğAkbȲXs> uD>B õUQ+뜁d+C*)]X\!İrd:^+NjS5 i?_,݅e}Nʟ%RMu[ɳxGu x`tsܵp7' ¦0e8Z:튃[id,/&h+dqhma-}ϮeH6Y (SI^bBxoHsc(6#1yi҈ a2h䈰D *#]6i0js4'mB0 X#&y'^ uasd8;[{ ?Lc7 E u+yZ$D !{,nս?!JÈ'[.w,<&O47hתPP#OB.3!=դ]Wܳ] u;foZ-C^bN i|sԧw.;1"1I X[0%-:yV__.15%LmZ5}#hi' P(Jg"-}w "U굨8r}::rsb.9i&ƸC5R0I>%Synz"LRD8ܽڜ$Ec !>;X~MyDa.$ QHaJ}:3npb܂ݮ@JkFFgN.TڽzFkLyVpiғY_Hzs'zhi)>XwdNHN㣽H %G> ] ~.ePI|*ޢ&%cooPx7$b:*>\<` 9r*~Ry¬P(-LT|^[NǼDuo vt=\9-_4Z Mx"d6QpWy,M_& ͳ2ِ,&/F!=f4:ך|*Jc,TOcx/N( `-]4cᨺڭjGY68p츄0isa0H`0r(L1q,Ue.LD2bDH9D=ﳢ IDAT,9u+ٹ;#zYc:` !)O{cᨲ\!Wqғ)!zNʽqy/u ra{,1^Ʉ/vTY$b̽80w)iOR3YN7wU4ǹB$ l#&(~p@>S '1Ep)>Tr_GZ &Z:rQ c/~}SrG1)~o@)qsLmuKFTE E V7lJsQqT?14O e4*&"314Fv-$ܰ3,y*l-uu;-.pX "mq(Ni*n۸Obq*?:f!9m --X$v[Cꁩ=ťxX8Gavuѭō8ꜰ{;b[5sJm+wV?H/~Hjh#bzF''l|Dj g5"BFaa7OמߍL8FqF}7wcO!yk[9P}zFB-% =Rmr I쭱~-5):9H{47hתP0픹E0L3&u_d'}9ckYf4ZM›-SШVbl84}߶+z.R` M$[!Y?\zn?g9;i(m7n NpFb|czg,$/)`5(T*T@=^I;$j>+6cgo>"޴1O'Zt4ĈI[ 'E%P2`ο`cSjRDܙktNbw&9c.ޣES02sn08Gq~pxw{Xwkh!U=8:g(d(ّRŦZn/SDh٤BBHOB3=sNmbUnyΧ6Y̩k;53'wVb*[sγ'esLWmZAņ"ʆYKD6:ʅ'-Z $~TQo(8^MKQ?kh@vDb{KQI[%H 6r-:pFDRZ:$cN+6H72)GKᙩeoȺ=J$ݞnFs'Yç8ս4s.逴 +6Mnι#R^lZED嗡0r;VDT_aQ.v7}ꙉu[KխWz3ޙfq\CnmHkVXDX8v+ӧ4;ge(UA*؈lp"?q,U2ed1hx& N&Z{a6\)LRe e*v],v)W9Ai3XGۗ۴&1XU@RU%䫇h+#Ye-}`?e*٘M19+ 0(bx(|Ht*= 4r+)7w޽{O/#{swvӎ@ @nszOpRv}kntzV]e;rs*9BE3'vNJ2*=I+Yy:qj嶫 )XS;x#ǝ5M/o(}S/[ˇt3sRqɁ^B1(Lf x۟kf Rg+}v2穰5mՃʈ;jI9z)"* KV%brV:|rQQ.fP+ԻI{yϪ~ 1rRU;3Ly=R9tS̿)6IH{+A]i\iT * \L3X|G=G[e=LP( B풭 g3@;v牙y ;d{T˞lP( BPBۻu`>y. b\Jh㥦R:tC qeKAP8xᲅPJz˖Iws_oV8vtB#psy$s K=G B88BP( B\58BP( B\58BP( B\5xR9M.y(GaP(c/x, |S{|'ݶq{^TuKNeŁFf)xԬ@"q|ۏ6}YGϕӖ(m Qj86[`՛K"jE3 Ey ڠ@1# ]MAr2 ˍ)},ل]9qx˻a>=hUzulP7n1`ڽG9G7jRPa8?wĉDZq"X!.FޣX,uMU_c:gʋit]qv0"ÃZ p5XS=C2&Jsf>ZMV`R8IJdp6xu`㳣q:d7;?#{˕Ԗ[ĹXbDyjMA`<둬Iz-wrFĉVr֑"^眱i5r;[/R 6)AA!Ŋ Jfɬ 11єq j|.$S,I"CPnbj|R(mG+"v%8&n aV̅!YdclЩ[9'LJ HL^eθ4BqFPWN/Պ\:q|YdBVR[mSpmAr#H\ N1AJQ%9 $AsHmWxJG(ߠ]Bx^_RLگn{Szޯ 4GHq汩Ω nE{g5b41F~RyM`ls|O0Ϧ|iKKupsRͬ¬ǢL!iAH(FCږFTӛ7ԕ#]y#AL1Lb2[!R6)vAԗL5mд)9Jqgͣy˵=Cz+$5h7%Aq[; 0I}1ע{ep$L"L! /1$RW_H-B4Br/ט4k9zO3핹=:% q:]p(A;b1qA cȟ[GAcFpC+)imTy}tuսƹ̯wvp̒]$KG Pt_sG鐋c!y@y j+5>,ZzDJ{+RDWrJ`f-E*V`rv͌w3\=8cM&F$!! OM;"Dj)5զ f`br72ِ,V? yPS#5fֵwF8pq-7JG8r(bq@,Ub nېM&Z%AU'RA>(g.Y v Qj ԰Dj 0S]sVzDLH,CCi{+l8^慉RjMn`R )ԙLWq4UizvTYN7wBɝ\,ւǫ7#f Td|"v|Ω \tQmhJ,s|"s&"Z. MQ4 l (8A?!KR&},]oD&I Zʲj~ ܏p\k3ueYeH#*dd4HG\"betM6Vc`[+N62fn&J_'#\Db"㛔nKg3in3k>DL.|4rMSL\%T>~?888<׺:{ᱨIS2D(5EX;_+| uh-%Gnk,* W#WF"Z W͐WX$,b ZpIXukqo@ 7Pg@ KK@ 낌s@ [c5@FӨQ rF pul%@X)V5wn#*deOxL* Fx&a5[l-`%5pJP_z(;~M'jd4,8ㄨWʈ2 3kBڇ=[`P_':\^ғJs]"35=? ũrtH9yq6<>dnnP \L9sZ:eY|͐6nz 5u wNfFz/ykUa{Ol >vuJ6wܭ8rCw${o4,vjw0\!PJIv_\ ejKDLYӤiڼkz7'kQO^-CL5@kwG;#:t'dNƺW?7sY\WP3PݜE r|0OF׎"3 =y|onmoSL_2"\]= LjE+kbיuTTk\6\[dSyWԫtNHUenD9Pz4 TZlmAM6ox XDņٵA3j3ԸDK'/(E)dB}?4{6onZ ?v+x`p75MˬӰ&b`=;j\q j;:+#N\Hug^ y r+ fX cjn(6t.xr1{{F 黬ܹ'~o: *7?;? ۰12jELʁj.BqimLyke R2ad4$•?n`V^7UcGǿ6PbB¾WObUɪQh5r+eRc{C+2 >z ,@*e;VYFX5$0T 'OIѹzS$Ck(9T ?<[ \$8W~Ogf}k Gՙ]N ɴXq L >8[l< f1Uwع4ןJ e*$GːO?:'-7qj\2&r^T5NW v, -4= s"UJS(?Un~ݷ6TK^G2U`p~"̹|Z%,pκibؒQ?~ϒqr ď܄)PkWT.h#gGLmUSJ@!?jBjڐ1)jē'(|Z*Ik8 IDAThHNӉH%w'a~0f hڧ)d1Sȇ _\yrb4RS4Q-uo>q,v. g=(6倯T\طZVKZ|c>ߚM,giU5M5M94}9ĽA"B!|<Ф2:-boń'Gsmng(0 {{F<<\,JYLko.C^Gjj<;Ϩ~nXg"ckgU*j&PwL9@;r=$":<'7%?ɀ )}C A_%PW)be~@ծ c{LrNmze^~<Kt28f)0 QIB9;]M$v.'od][$"k$CH_2k;`GYHʾgYt8KJ4YqO~vVeh$C+#ySo`Dq§(ےiZ>4 EW"y&e $ZB^|'0C6ڌHRy;E8O^wڦo q;!A O& jl6*z,'B (Fi+Sg Ҥػ:f9BMto*6E>}AnuD4c^7Pڲ7bNXֽrBB9;^A¶Uf/b3U@Ze`5£eÐ&FxUfnuu{Q1v+EG,J&:0"`6$ i&VH uJF:pM 7 lK9cȷ\Y`Y\=׺:[*zAϵg j W\iƺՌyӟ?lh!pv&S/E,vVYfR!w*`Xj5֭ MƲ2zWJ|F ato}+yC/ˊVc@ܛ!֞@  +n@ VbR9ؐX!ߘC K7iƐ@X)8@ @ 58@ @ 58@ @ 5>o MnۦS JYD__sNE"<Pnٳ jd4:!FP_tcﻜ$ 1>^)C 6w}bJ{=B@==O֟l?u~)|U*t:9bFȇ9 `_ ^fxi0=+,;QE`qT4CȷGO=yPu(7|^}4(7۟Z ΚRljP:PU<.0O gzrCjT~ëUbm&Un~w~racd_k(raTM9lͯBiVpYƱs5rv'9cz3]^g )jgLj;:)NL0N'.ܺk/I{~9eS؎TO6?ݯ۔ӹ-g{,Meum=;x;q_!Z,ol.Sg@2]M?Bmȭ>8:mX^SgH%CqHߧĄ i}g'tV[ލ;}aP1|t/؍Z@ <ʶfW8b FJȇ ߧQʌ/`7̘f}k GY)D6:> fUuc_;;Y'F2w(/tb^'xrcb3ɞkjf<$cǐC=a{e0L k*α:Cb yw]1v2euV1(g ^w3!w|kr"3~aP75R?>?˜J:6v+̌R-yT'GU2 )CFӉP-uo>q>b }/؎szrz6$ vf{Q;9@lj%g:pD/|:gDW?Ώz\x< S&{ 'AO2:>y zt 9l|,*9#!s0TytXg"㝁+}(F )ӹv+.͟ԆI4 2 q\_Pi+a$܇S~n75x7*IxdLM稝1h Ttq7Tb07 1E T ? V$fe߸7 2֮Tֻ#X6쭻ersAON}[QԸ}vH)`Bx"0!Js@!ƩB>;EQ`]SҲ,W%Pڲ7bM/rVf|{:rYfH2ۙ,'B (Fi+Sg $\&ϨslqěD69T8l|;X;%>1EV{b ɫv+#tj |.Zi^Ͻ?*UiS0 [lkFM%: r 'e*ì轗 ٟJ@l0ۺeDU*#e$]*$ Md@CJiya-P+3^JӨ%#-e[Ld:Õ*]|!s*3Ezq;T9<.~qݽʲ9M^& <-!хJWߋJ8 O31|:Q7J" w5Pτ u$U3m_ w jYg#[Vyv ꕑqD(n@M3 ѴtNE W3ۑf2=[Nfy.}!V)bM"fyYҺ-\++NT*?v[7 uo`HJ)_IUwzƲ\bO*(*Q24jX+e5\Gt@ xsn@ (L!qBYbq@ q@ @xkq@ @xkq@ @xknXWYPnr6\hҜ_\³@*џIP?쭻wA{`爘JY⭜ 嶝={켓"P klųE݄8g(K}ֹ]3i2(4CZ>>ku2Ը(t^ |ХN(H7ObZN,/I)w/ΥG**hÍݏ㍢KTEVl-zF_DZ Q)z|3FT/aSQd&ךs 6jLSe$4Α]\>IE? K"2z3]^gjH$ER-wc[=P SFb:BĩuCXU<.E^Tvkj7ST"T;\\4Q#Ė U+(j2Wʤ,Wd}8V8m `m۶OF;ֺ^e/ދ%CW?ߎr~ֽ⊋ү[.j=Ȩ')KT[mZ $!cPJ%엟zcL* FZ_m in?$pl Ԁ"AzLՂ6M|,CX P9Ske1p|~F #iǎHW( A t-[(65 };<<<<-[}~?qtz۰&Xg%S: fY{HHYGcrGwWF WABݲ0P--e/a:E/y|ρ I;ZxW , M?*绀*̎]A ,i F6S7ˬŧ@A/ʲMȨU*MYeȠ[rEީVJ?OI"9<<@as; 0CGTD/V9<!Z*CulN)5P'J"ӻd# euV)~_hMu~+L2V1(ĩ%WqF0̹|Z%,¼a/2nx:ٮcM,<$e]GZf%V vьCfŁ[('7]vg 3T뵵t$5GO)Kkm ;>Q-yT'G)qr$e-U5M5M9[8زBikl8۰)OAZ/[S(1 {{F<<\,JYLko.CY7E b,bnF::hqP+B5K n(7?ю/~8jygu = ~m]O'+*5cBd@m%cĔʺkCm̠o۟cX_7fu*7o3 r< S q-B?t{;5KTZ|?HDs8Og<T͗N=SV[PP>Ppy &h_m2@$vl!I4d"nc F9l|,*cpEnqkwgU)OAb/s Tw+NgͥFsJCPx X>}!r+nRfjK4БYw d8ZB8!cO_?ySYd MYm˳Y7Ij4hbG0[nD:vgYw7,MU>K!8ʹjwUv.k)lf9h2hs۲+T0-=F#y)`iY_k!NB9{7W ٨lh© ϑűBPQYl&cr"tbtk;R0@uQ)Z/c}\lh.f"ErqLK!$Ci8gu*髵=j⹯$!U2ӦW 'l3J:d[tfh fh-WF=p&9JUPM:_GUJbTJmL*3GߟotceY:4 O^}{ drW!gδ ~ҏrUeuT84,T-(d&9>P~F4)9?vh n)/F!:&֌ {_f٢}i8vɏhDZYx*s Bl0ۦq';0=*7gxc_39L$oWHbE+ljGb1?3lGv<=n.\1C(fVaJ:۫ ZcOUPpmHon`37_%ߟ'.Gc>,Xz P+eLPτ u$I$hS4"YפtCɜɾ$|-,:kD1}E1\@*ul@Z`(ԬV24j@+$l oؽ P8?WZ#"Z'_nXDֽx4-VJj i[ܴ)|~5BBR!•7qo*d9#*ˆȧlƿ=u8`+̺-TZj tO+Gh57Xi[DRs5@XU:wP.H2@L XF &i{04@ -C9@ ᭱s TiԨZA[D:BN@ +rޚm;2ϧ?<&XLxWFH[#u{r 6jDRB~B__Po|7tPaڱ.S^iMLu|5FXX(7nN. !%ј"n<~x8W嶝={k9Jjd|e] rtHzZD*gn~ݠb`m  IDAT-E{z%Kpu1emoPD«w?sKv=&FX&sz"{oRH ;ɵ=3 Bֻs uVY8Lmq"?.Ǔh^i `epH ͉&ug-:OY$߀*S$"EUY$V^`r0/csxVl)X_5yd=y|onmoSL_2"\]u:86}.FN&DO8\Wflj4;՝.`7&֫\݈ rݑ2n㇝fS Vj..-rePaI/*΅I*7?;? ۰122EoJkˊLM%+(4ceE_֛ Vmhe\:SC"ӮPnt(m,ݐA  v>PoV֫nE/uc:WkVEίKH x|U%ˁ7~'͹;Tib~yaĤ_E_;N]gBR?\iN,0]LF.!}vM^Fxʻ+\ !!@mƅf,.K_w=}s⥓z. WI8S!`HCD6sJX .__xiQQF_-Pa4)z5Y;YU}C]@fʉyZ/O;?~4s%`h&L4i5/\jCBFE,r&WlWұǪURz {%#* Z鵵t$5GOc"\Ub{DMx y9]]"q֭#9_1Ivx5^-M)4Tmɐ䠝jZj+mih:הpaY6K&s59“Apj~/Cm&@)oS"(=1"!0DRHPz:YڝT4.P2~kT ? s~c]M#cٰN'jγ"e4+B!|ݟ3Τ H *7 )ӹv: B1rћ*ve>>[-PwGr@86"U))Sd !GXTIм_ۘ9pqĂM%DgX[׍EZN(G/Ҭ=/̵*RZͶ9\qO~vVeh$C+#yS2 @޾u!hWr"tbtk;R0@H(Fp'=dpl!->}!r+nRfj؁BB%G+.:,hZ>4 EWelk9Ѥ2APڲ7b*n7~Ffj l 1GLDRStR:M7"Džꘄq9jҷ,{4:hzfp2.BH(pl&S^e5}G-ѩt/ l4-fhP4﫴a*/ B/] ҡmg+}U V#<-W6!w_Mm*3W7պ:ۢ U?b+l_qPbe4gؤeCCJe2Y0nid]:{VfV-WU5cᶄtw>$kDU41E":p[yۜ s bUi΅:c?MzΡXfq2*KbUfgHKټ\^:\iª Ž{lZ9 BQ+en79B O< ֒`{*BaQL]RgcM"8+# $ ֽ'1Ç֦xm:FEȬ;{cQ&(4rWŻܸ ]!܋;^'?oM,ـrk+{pl(!L?yk87R]LS3{G^ɶ}O"bdxa9Ϟ̡p59+ 5< u) voasG^uk`B'E-O]|32U^^+| v`[~]͘>Æ* lg"@ <>y~,Cќ0SWy-BRC2d<^BKj|e 59e\@ GV-$B0iE\jF30J/h%a5֭Ľ onMJD@x۬5@X=]$s@ [s@ [s@ [cFX5;s2`wɪw_'D}ӜwtrV 1PrM\g+\*'Lݏ YX!a^`)wif'G@ȶ+6#9Q/2kMK9QL 8UHh/Y|iZ!Pj aL{26?,ٽ|5{Q`NYo rCfW和A=\V:?fWcyfݠNT  áF=Y*;4m"ޖIV)^]%%LAj"yܟC_֛ Vmhe\:SC3N|^?r󓫰 #&M7Z,rj.- ǥU¢wT-6̮ Q%"Zķuovkj:%*tv$zUA޽;2]ĭu=֪lۓf۽1©\{`d/ ^f>f.uZMwS8)s[t@iVpYqQEƱs5r^#6?1:z2T;v> V)K`:DNb%W>:|"]>]\>IEڎiqO/tfl{O6QR-jz\E 6-Cj"9)tv\*gw8JLPw~q^+ʶf!UV+<Bݲ0p܍')'bq~ 5-7mcvD4vEMґ< =`~4=m/ˏ4zqfn'bEL*J@̈́70l3n#i,-2ΪHi p*\59{2T7L[Rʈ{ذp 0qh$8 #{I&J.=}7~SC\[zjWW}8}z6zP.?8zoNԝ.&w fik#an~Eu`5=#m/&Ińl[w5yYb78ԑޠL<`A q0e8!aI:ז; pWSP9 :poJcЫ['unȍk7)35lZ-o/>Zq!EftבDጼ{KySHj64v}98l8&8-Bz!+u!śjl6*zdH&ΦLbFtNiR]XaMæȧx&;.QCDh?qBʠEOcFY'xEL܇ 5z-VǫW+lfde 7aUiX!(l;^e,6P3e ]+E[H,vFx^ZjU0SGl,½vOQ̴Ufnuu{Q1v+>gdN~8D=YҾU/+yiq \U}6MHmtZ[lѮy̭QT6!_MN>L2-Lk0!C@%FuNZ;U8PX`?MXi"\U:\|wf\?"LH27(Ӱqg"s㮝`wq'Vx#lDӽXQΰ/G\O8"z6^н΢jGbWQO-8Ze㢿0V8^ƽJ@au+X \2%wS]oƊMve y}\) gtNE ;NvD0s&w srEuY#L=+| ב\ߎ񱏴2XFjŴm ;;`g4 F6Vc`'mu7#rzPT888 coބ7 ֽnϑ(BОN^p~x5כ5Eg.VK(5|S !-W0jyDcB (yj =@ *X|Ueܟ-" 3o}24h ᄌuO_?;m'ySY'@ !:U%i<[#fPcYYo &_czC'\tB > <7VV>z'lf'OL~!Kt $b3.\D3$C9@ A9@ A9@ ᭱sܶg/'@wtrVHFJ28J. ƽGSu~!Ʈ@|1 ߕDb}bOϓ鞛O@n.u29V<hߡ~󳛿6hN{5C:sZLߵ|5ߩ7=$;s2`wwWӄ"92*n\zNjk Վn53au{>z=3wHJVPha{y`Un~/JkˊDǦei؝D猁/[F.vy3_؏@hħ3c"5 };<<<oPiNlq8R7Y[6ob }/5^[KG\szrh#ׁ;._7i#vrVj BՂK@FElLɿJ+yssҸ|.St~@śPzs[NuR:^Y)2ܶ[q!{_iB`ɿ<> ! No S=0jWㅱ T62T/?Fojn]NȰe4Q;EcrʩLn2kazap6g|9LݴSo ӹvpH?yqwȭ+_L^Gf:ۋ'3~ԓ~r0~ۻڿ-Y IDAT!$4Jބ7 qo _shYt.esZ'_ni (5|SX2>5@  OϻS,:عb@-)2֧?? >o@ @eW6ɱ,Ӭ-\8@ iKVvZi9KYF o6@ s@ [s@ [s@ [C@ C*џIP?쭻sA{`爘JYBLZ4 \!ɉ My뎅Jw7w9wO_=9LnUb`S7D)qH7iJ;s2`wj6ukRLqA;rΞtv]:X~ݠܦl)4d-Ը(t7Kb+Um:P+$DvquR 5ݏũ@9(áME;RצݝM`zYE]};Jnpk[Eb-PH7BڇO-۟P;.vawX ݧ &Ij6-||;<\0U޳ފ;B!|ܗ̎oo$ fWcŅ *΅A)c \{0@ckUOisgIȇ9 ugiTr2;L5-=*Ӕ92uLFjrvZm4 t24 v}߱Ntz\@P_l\T+z,KF*^ZB5P5rX> M|m T&*7?;? ۰12LMM0aMo*Q(;ܺk/I{~9 WJkˊLM%+(4c.QfU-ZY#Ԑȴ+]>]\>\Ʊs5r>N9OQFPp g8 *7kPGE3br~)|e-⾐Z ̊e40J 5qlSN'LLYʝzw ?ANS{\*aqWŦ,b^-S&Z>m5d0 JWngS|?o]:Z x={/_ɵέ{|!cPJeyPr QJ^ lPRR2+ !] Rʦ4i2ʶf:.u.x"ciON'cml< f1Uw%A,$Og2T `(U׍4O7qG kkD5[k0T#!RJ8i̜?QN!]@5fS&1Yٜ7>qyy漜l2fǶ`OEei-U/IŎ"Azb[^p5lV-uc}b>UTu@ ^.wյ !Yг؞v)F˟vZR+8wԛ$J3|Jعk/2#ZB)LJHh$N`]>ЊX< 0V>ԹQp%P 4b>}g\C=XeYl]eٸG[+^}$^iR:cԛ f "%eNl:E J Q`ſ'#Y&9̠r5z[\6qiL!D% 2U9>w_U&DZHc$ۓ$lR<%F19L"İpr_%4%Dj'MG:%Je9)Win:3Ѝ<BiA@+.PMd܎o֟_q|V3𥲈0g4I HZ#sCdu:P x:CQzηVMoTzō _Sa`&#Q_>49Fe5B3AbsturIPǘ:Ϧnan%\:,ZuS6,5L$u=5/ y^[lOO-!d?7/!ZiHt>*mTzc4 3yv!|wzkBrMp[JC}6.N.ٰCBh 67.0V+MR^|sx_G2fJe5y3s5eui7DQ]pѽ5k|:H~s7بU ]*.Rn$pD;OX7J8혬+#ɓLU٭(Ԙh^m0Nt^1iZa@T4-}ѣj":>AI iT<#9;KH=_my6=.(aPi6_xM=CgjC'DɋP?px@1 e}duJ&4^-K_C>!? c5Q6N)= ;zw*CИ#+u|}O8pF}k#ګliodxmԚ`17780"$w/* ˏ~#4sþ7&xo8sxo8|H/VD_x3K:ptl}1=\[!u@8]<,֎'m<~׉@W{'j9,`mМӒ:+ydž/m W-?<5=Fɨ49t>[.skmL"'ϰF~ck^VfZŇA}6>VfD`SVRA(>FMX_i,۴.ZGtgΝD CBwCyYr~\RjXc^Y[&.R8Vp&-kӓH32X r7];:GTi/cF鳒d)nM+)/ƹ퀑j:F` ޵@wBw7Fʒ.udO;P!m%yFJ٤2Qݬ ^lZX'k'OZ i௤;=l|lȧ<Xdnp'BbYp mX[#BIwԧq0ϼS[ks9N5]'s&){fS^sP+Ŀﰽ=Zb,^N? VW̼|~av#$gd-){e|߽{]Ec:JR[SROn:FԒB &$UR_fSZE=;IV5NuFE+.+jJpl!ieFw=gwwwt#Sפ?dp$GH Kv篎ƪ$3e}WK_=|jd#?myi{sA_QZbjB|Y/~% P-q+Tk!Ę}\&$=.>/zQk]RewPkl4l6H)}-:H|J ZK E@skHA>3]L9#Sd &I*rRkEjdwg]@C87FclΛW{O!Z+M)j(̨s I6z\֒Z1}LWD1c >9Z_z">GՇ/SZf8c\B*m/"3u(qb>Z6oMV҂%(^_8$[aE$sӄZk^EmeH-wyDHX MwVnʄs`nG=}2AjC0JWg49 QU#4*s{գb֐4$odY{\Q"{QͲPRmؼ)eS:zเR_٤2#s !uVT1ƨUd0Zփ}1?RǽY!cZ.^P:ͦ(9O>x _j6^=KV=.ʼI a >NݢDžb[OFZiBf5ܭ}b:V.SZ7Mb۵ ,d,JQ.lxLyݧgQܚg{eDIdwu2eG ޑ7F~EE] _fBX:<aWJSl*2/9ІŲnǜ)źD"I̛|My\O{7Q/`uruef|$;C~yyl۲u0J1%yn==WkYMP5?,⭝Ka楯'PYRJ6a/9ϞjOQkk]몧 f]z$~nML6Gɻ9k:2T^Ը6U.yw# /- 4ϦB+30T,ƅ[;:"<&#(G4B*M%1q,<Ҕ iC=o?o!R.r!_%n!hGy.!#_cx:ԕ|dI_,xW ;x^mŕ%qCP )%޷&RJàuzm|kYO$_5i2B Ƣ)^ЉLQ^0~Gr]3d䷫\"WfٶCma6&Vf$0q?vww|^ cCpsw-́}bP捷̲ R ˱ҨUdsP;2́a9LQaJΕ+~@&8#8}mm ( j F99)b.oL#P$g>5 0n`{97`{CmuC*`#x,:x'K7W9+%u>wsqqWRPxT'[_JZX-dN孄uW쾥VܣM0W$Sדj>) iOl j=7_gEnH|U)()oIV~G=z@xk3ՎaYs:uSyַ#U68+gοA14tpR8ȏ+2I>X/e" ۄcUZ6ك=cVRAUl?of +5ns} ʨ8a,42kEBFfM29Y~},9ek#eCRfc l- 8:EJJ?.#B{X-F5ZL;f=5&75" >s[\ZXJ]JZRД)fh1Uҭliop~kzƹ퀑PbRr2NE,IdꈏOuYIDc\0wtܩ2vȔZQLlL/sLIsoz ''w,oQ QgMO`ZZXxǒEZխuϫi'ׂ]ƹp̵ɵb}; h`oT)f 7(#3kׇx=8u$c,5=?8z6F.!|l(%_,nav U#{qnmQͳE6:I1ϼS[ᩃ$_9לM\^ >H>qD嵐.6xRяj1" ѻb; m*\Rg)uu22\䟓Z!}oQʈO>¦y||:2\I}G]2OszߗV'eR_\-suNѷ(_H tP!\^]vhB<.eIy.4=3,eXL#&e7UO-C8Z22{0BH:J*wT t}!=?Cf+}jҮ|_|[B5!dr+lmN2Y/~%3Iwjh[ nPH*7LNQD|kohMNZ լ9m>ODclΛW{o,? P*)h5NUee :CB5`׫ !Sw-SE9UbRyLzLB*5TMwYѳijgq 9q;{qW4+z\{En6^S X< `'6[qY!OxNC#}(SA%5O ̹\Jy)F˟vZR+銈hNLĨh$*}2.S5_LI ϊt) masĊ04BOώзqwVnʄs*/A7(#BKGDlx."/-[Hݻ\Bصn5]j.UtMQr|@-o=qzsHkElhw|E f%̠rs{o xuycx̃u|}TD25Au^LB,8H%zEȪIM;f4a!̯={ٞ|!UyD}P_\LJeΣ6s}==UZSĠN$jp;[~bf|C88Ųn5Q1!e.svP'|{%[=>!UOHz@DMSݚ!?(s(hDޱan%\_V7L%lV&u@,T@uk}݀kzwvg3]iYOiaeqZ*oM71IŮ j|+.UKN20HBi,+!~z7Ƨ}.%WMNrT6\L+,9=]K+j052ԡj"mȨ$ SģoMTRD)ZƧ.&O< GSuټd,Y+K%+#ɓLU٭(3Gҗ=.bW;Z%yMĒ/1!U#970}@1 e}d9GfE$=?ؼ5ݼQJ8zZ)R*`:`4RvIo4ٓkN)=c'l*V%Ah:gFu,p>Pc;Fem2bq,#6l йCapu{u١E;4.xy|;gaZukI!vߏꉹ{{{{{{HTv7eYc)$0iI1xOcDAwusZ )meFw=gwwwt0@Q WBz}m !F~D{0iV˹qyYF rS`.9Bff;-?) &ub*Njq 00{|I)}xO#gesT|cSkeFk7CkzI*vy>uXעysoؖ6,Dk،O:?@BR<=0&B:w8l}gFكChJra X< ӉlZ-o=qZ`\ͦJuY}LŪt3H =v!VDJ_Iy!b4Pn>Vfd` kYMP"|\s6FkpyL$[5ׁYRJeeBb5ξǪSs2lvYYf`Lmi[KDdN=`:`P!1%%/NO{w}]jSlR[*hi~V-q֎H"/J>E5B:ǔO^ q]KEҗ=.b@$4[h{A'Vg9niffZ |=#杶ZN!dʌX0q#wq1Ws{00+`PF4,jHVm Rz>طX3xGF\E3pڞ @7!<ۨ5'8#5Gܷ|`Ƴom-0CZ&A&L8{97`{uֺ?);j| ڷuz`v.%S;]<,5BXN<*/m X[`54g崤n..b';%:o-1~ #ߴ@ױڊ2=-?t$FGv }uq,OթΎTͧȞ-oHu|WwֻW7W(=>O 2Xi^0y-ޘ''ץvB\BkZˋ:O8d)_5'y3SͶW K}!1?ky۴ՙiglxD~EJ"Sa#)^"mgc۱|X11zJpmB: 7~w)ַCk q)}k̆"01+,Bx=}Xg/ra{`k\{M.&o_WB"u2LYsVEZJ  S)< z]]8ӱqUٹv\W "|RDy: ]Qyyv_۪ ])01cB,\ sqըb[hg9"ӏ3Wڞi.=sD_9>-shP/ёwVUɔ8c̨r_xO&m@X\bZ$wֱ)!jwVnʄsIDj-U/IŎ"ՖJGQO(Mv@R?턵VL_/ӕҼ s{գb7HtuSVRI JLLZL=.Xh jji\tBmvtؼ)eS:zเB%Ǧ ٜ wr/v<ijVc\X h~E M1ۧ:n!3XN>o̯WO~I=oz\ZJ޽d!i6=.tvhui dL6nspp]1Wa$KӼ êzf"(&#I2Ngks`d"N\W>dDIdR'SFyPk@g#VI7:D|aU!b4 <-/ϏM/X9|,"M(RˤUo-^Ϧf:15ZkɔJCo/Oyq}ţ8 8#Vߴ"2ΊB^']2IZ6.m4:!@O'\L*cλޖ% POJ5OֲJ(3ƷVQ|XG"]w|[3 p0ilM=UZ*o˴;}};掎HbyRMOzj5>)䓗{MuVsx,͹Ӈź𘼉jo͙7pl?JHnR#k,+!~*b-W&szLŠZ*ՇQ5w6d-OB!kkS] fϕ |9h2B ++^ЉL!Tģo, Xٔ*#V06|kÔCu6Ѝ,,$d0!^R bDA>2ג9Ol^j޼,0-a:Ѱ!Z!N6pJiPxTߧLixNy o#s:0h+_WlxL2pFqSӵ]7P@ϳZsҁ3sxS\%WWF0UI/<9=߰+tW&A&Lm#0!x% !$n'?"?oyGp'"sۺA!Q77Ya<_CVJ8J{ؼS䦹j9w'K 9Ǣ+ʁ-aa3{xt[!f޻b*| 4Z!t>[.rdн4Jh_b́12_C*!#RF]Loy G^PfS~#"EZu6| f]~_2wϛF~t1߯o6Dqđs"|'+FWp}-E'/r`K73>/SA@v|C7mHj*32)^"mɤmT)FW|k9oMU[uQB5}^ W:fn90g75XWUθmt#Q$i=;aas4uFbR<Ţ7E꜉땙s[\6q^0B&6@n>fKK~3T j#zG=1vɦژàbnm6Ux*$cU<>՚al.{,(/y1r+Bzcy\ 8Tyϳ_X4fD2g{PCS\^;.s9N5!!1" ѻbq{ʺڭŲ6kFowPXo3,eXS9kI!v}ͭlC=lZX'w2AHc:n]Vj͛&{LGLLJB$nFJJOySPBPIwEfLuʋ?'Bo_Urox !i:YA}69qy٪ea߭8ň*BSlT:AzVwV41Z FmbVLV>#gr%#"J Qk7pfq^Ӛ]fB|g^DRѡj Jj yS*~{7ekA" woŬ|_|[B5Rh6Elb1) &eyas`Z}XZzHtNj RJߵfVHMLgyLN;fMt޼PZ=&Ž\a;U%xSS !g*ZsQOtWX"JƢ|dTbՍZwG7yt{PSL.ٍi3$|cnom_\bZ$wֱ)Z˜usF;+7Ge¹:ePb"ֵm0KvCeI! 7GJ򧝰Ԋe2="u`rWҽ}VjϛvaB;OJ_%'S6S^%U/n8ԿwZB4w񖌣MOKg"js4v]HVǓ5xy, )./tۣ˲qi{sA/Z-z\lq.}#!b4 1HD1&v|sbg\Kut~i٪E !E cܙd bRh$B k_[z 'lxLdJ !b>rHz%:{ʢNȢ??Bso ؔAH>/0 lVenK)%GŤZ(z 3p5p,F^oj5ѰtzZgYɤFUk1ik .CsޕeCuŲis2@ZJ?KeRc/gOb9ގ=ger1#,몧Rd@ IDATNuYm:=^by5=;߾.[52O)>BUS9*'|̹|vrN{֡]ii-=zVPM٘#szL,}>=nQnŦX\jܿb]xLS*{.ҩ"*D2 ֦2 R4kݒon!DJ7.0VQ?^Ʌ51I7k,+!~zיb1.AS~T8c m/JG\LyiT<=kSw`^܎c-!ՖRyukįWBdl0oyjgQCx˷ W'yT!tmoY>d";YA}6Gl&`:6zca9VCj&L׸32b y楯`pb'{ ϟ 7?'Ơ^b)?2 #do 9vc&84| Xu|}y5·zw**&BˏIIyxxyW3.? !<ۨ%qxO<](E#7Mx̴#j7f3s@B3?̽ʭ4DEg%d9vc&8#5fL+0 X˙L&N٘& s@`QjLZmޯMm |$:_,0 aݸԘZ>ܧ4]<,5BXNGcvyӽjRQr\VI dD+aS`+L"CqIw}526ZLth537i{Mgd1" Z[v՛ jgO}ut݉jO!%ųSsW5]WզB BϐƂ=kL ;EXEAp9' ȚnlY],!$"h{s6GAX!$/On-ΪKv4uLS};:0>*?YDNIFhT2&=աmNqޑ EPQHhORJ=yUTm*r U.b2A292Z͕XNh \i(P9RgoLREW6sАt oy!l6MQl6x9T;- @%aDQ$ݿJhzZY)/ uE:`M "Z6oMV҂%(*D(6 ]z>)!œ۝ZR?턵VL_/ӕa! MØ!yGGdNȌeS:zเBSE7PؓVCs6_#{Yztud"ClOw %ZM=ޝMcB;Oxf{mftΛf9:[d?^`ۣ)*UP4G*,StDxN4ʋ9ϱUGiǴqA0YlTzL痖z\.miB6yHjL_ _,ک܅T4v6 _,{R2Y  HT{\m̼(U[RB[vh2Pb-CK_ve2y"Gf!P9,*gXģo,\\8}7Vxzz߇СF-,u](Ѱ!Z!bN2QcFY1t}?n~+su+ Əc 9\d skU=K5{/`[1 26u2?/7o>HFIN)= GԢu|}#ĺWi8pݘSԕbs#=P;#ͽ_aݼܔT_Aۦ9z` ~Io Pi<6jMبVxV쿩1?NM]9z*c UhONZIB[+0ݴo}w~ȸgطo8@ط}ks`{L: W*ΨאnHj kb@A{=_4iu6ZGpsƂ5?mAM}:5jk@e=g%omc~Mm˟W=0N`=gk|&oDTë@e=GyS7͵Sqۤ:Z Qc#x,:x'K6,l~qf!c;a607y˓?j[ہdG Ňt=埖:p{'\jO)RS)㈒:6$r~gEn[e*T*3bB Ô~F0LX Y9-󹛋;4C)?f9D#8ɵ~^@W%C7rE Vп?-"E:&0?sN.~O_J ,\_ ?o:f6;4?L eGrqnwg)n-Ж g|~뼿mZ*Q|U cq;&:s8 #LB}>Ta6z{ܼaaW K}4(?f9oֵ6Z֒zwz*ߔGJaS@B9466HoZ!L0՝ Njz;@adz'(jEQGKtSH{ DxIoxu|wrE:eCfwfi>¾5$˴iL*3ɤmf'{w lLSY:::,ɒ㑬X.m2-рFCTn53e\)RM3ٷ9c-܆#F-0cJ\&$l *#|I! (J(wWl|а'<?{2t"W*RT d抐6[L*Ի ;k/5T\.b psO#N;*m>N٩+FSH5xa<å|k0]‡e]&^p/ְ' ڳ"r@M7nӚ%j;qz2"5 Kןw cJsMpڪǮe`VzMF<λ┞Y޿C|8<,!~L)$Cj'D l.&p)zw-K.@mu!J7A8!rY4:ZlF)anOΣ%Mt/꟟Ur3k닾lӮSdb!aOv7U hqýeFD? Y}>&qSmu:T}ֆ  (n ^ ddvg2j #G%I+R%I`L6?D ID;N*P*H%/XހrfϴcdN;xUFΗ V3#fImf߅ 2, F#TRuP>Tdm*0nSNK!K˞kǺvziqMldM.=ZqN&bM~SqbEGy"&bszcj g\g+siѢt=gȉOvb{O1$nqRᨄF]]v ~<(y q.m],$ʆyUWD#R)3vo\,R65:8oǮz9zp~_=`pNjQP6gh')d8y\*V@pnSݴ6ʴ'!N;*BPm{}|pTV7_5siKW7AL_~?RJ/g,e#`Y,caI+Ը={u+RL`hyop:7-|(=oBP( , ,#dPD!ERBo^؏4.APr1ȹGP88zE[;&qZdpC>A)Ǽܓ=yghuia$/.T~NȈp~*6r,nʿjFn\HT&<ɝ<]F0=D=Bu|x4/rAI="UQڝ8de4)zs_> p;+o_&猚J8]-S՟TFk\"i%uy;JޑLlcd/\XZH~ڳ6 6)_;%!s`/y!wZC Uҗ2gUX;\SNvd9V\\>Kf#d=G-/m*qR2uV;]QˤIyа~8t3j8֢C8ּtrR·-FH\*Uxвٿ]ɸVwMbl ^,! OLms|hR_tN1?fG8)y}z+b+j*~&yXa9WX P<< v}ڭRX\Ǚw/}sޔ8=ξQ,oڙ|(.ˀ,^/?b̾7W!r!wFX? !={<lӓ<|^MH şuBNUyq" z<ދ+Ȉ'C֝r7 )}^[B.[NN-azN=~#"s-qqMΗ(?& +^BT"Uѿ"fC;2lqΨߒɷ8(@o\Hz}|foCɳ扐S2}"˂1".L+[;(妁 <?\hkҌ1w:ٵm~o IDAT{sXCL-u1 1%.gY̚}+3m8h ;O_@N*sw!k;$vR+ET*H!LTvĕp>gPQHqFgw/y98ǯjcK:աtq+`{zM7rl{ 92tC%YŜ𤾙4*GOZ& ,nS7keӗmZb^?ֶ&q{P؆2=1Z_"fs !ZIo8ZC!&vR^E$I`JE$1u?T2Kر+̞i7Wc!W0ySuo}=ĀLcPhc㗎#gٜQ-ut5{.Ti:gwwwwb;W7ظ}nCB^0y!.U鏳)vcw=BwmHOi<ޞnz{؋d#(8Ѥ詓lVyI96:t=gȉOab{ 2-/np)x~CJ/ lK$xX]`gk0_)|ԼhbpNju4B{i@zq\Zy-wBsDQIM<&VD t#PS,eLjyJT $~gPcSAx\*Vz>{C ]${b7ȌqeM/9-.E?`hAUztȈL&S_ VJg,e#`Y,c ߸PVhtCCMCK1X7+VĚhLguk  C`&>Ɲ'EQ ;#HLOTR?Oֳ3M!qTQB#ҚnM5!?fG8)y}zk*Z%}CM)wV-M=ϊ0%0k-f.'c=ȈKl`?JS#n vRjF~?=D7t3ry` x^a·.T̬/#WXGBeXJ&]ee~Yd~W9zT{ ή̠x=5X &ߴ3P`]~GUs`<|kjmw.~.'?O.Zyaac1쏛6K^e[2q%ws`mpzZ^2 8serP۱N/3  S:'!!-]UkqDՐ`~_l6fǩ\qZw @Da$=50xO#>ܔg9b\5*s'm$ӍwRl t3j$`1z-( P,>#b g˒N?Tgyd~h c1%T 8'3e茓ws P.+֎"bYԈ5V6gp$~UO}DcO%dppHVX5.J(k'6:zq@6gTJEQ* j enϮ3"mLh9ŖRj9y+U~ 8_܉F&VXPkF`DmE`lS2!yf5õ(Q8l~G^ uRoA8gBR{ P~a3?f<#87눵M.]lޟ'2ymy8Ņ;sMS7qslY dӾraT0 fϻD;0ƀBhӛ0K7z{Z]v8XP=E!8v^! 5!n*2/Tw`yBbK={zFBV93;G۰SRcqe^l3SFr#>0tod#/$n!e=>R@%QT'ޓZ ڡ+ajzk1;=DArKEx90{]_??o1j;6_MWCz3p 2<@%I+R%I`M/NB=޷j|AѷS… -'{Xkgˉ٬lwZ$ɿ`^>4a;r43fowd:B6ﴃgH9Bv7˶tkǒX<r'0ed%foixYV̞ۨ9L uzzHm:k\騆ZXg/wPZMRZjq3HO?<7?ԏy5^ NǮ|џM^Q#'{>>GŪ?K~婰[7яƅΐMBz0>{isByNq KX25݈DB;MX܊>yjYȻ2-/npY!dޖȌsi{֠;R'1婑f:+>kBNp:6uDҼw? 3ہ~4J_PmzHM5#+쉎ڡ?aj$$z2-( #]uC'T d2b ˛xbYVk0L bz!1KP k1Gsխ>JI԰vy(c^No !Đo(m MhA_3Șl*gcxizpLiXk,ݩjw &Iy7#ucx!bTH*ͷ^*Sϓb|r7#잱#>9 Mc$C!ߺQH8Xa{}1~gk̅ȡ Ǖ~6`$,V@EAx=$Qy*HiOɨ&8s} 2<ߑ:ЯͯCN5 edGц~FP(~:>zBP( BykqBP( Byk9cM NǨ3Y,JBӧz '4) BP&3ͭO끎ֵic֊TqX8)9po3k2\3֘BP(ABsErY$' , ,#d5a=GY؏wUmp^\{=> yghuia$/.TOEWh9b)~T;q,L3|g##v9/h"auV8 M7u zމѰ2n~r'Ze抽95nEϋ%0E #NL/)L([MUDFʺ'wv,;0 :{qjJB%}s6= @aن)v{&d3?Rb=ȹF ̢|́q|ߓd/\XZH~5=c1rg &{5b$| NC$68wA4Ĺ36lDo{5NAfߘTFSBc;A_ւ 8:[o5ỠVs̯Vn&lK6Mqַm姓D87YZtѳ& fޭP4r8S!L0՝EP&чx=Lⲥř˽#e;$|߯,sD}f)%.3 @" .،(STW92CY׷CYRm5m!>7X܂͈2V-Vvk1^̈́2d ZYQݙhd8Q1.*1n9\=?\uveEG1r6#XWhkP!_ƀ`L; ,g]!c`-=cFVaO{7Kj:rX=-3.&xה i۳5zlmLR:N`Nj]Zvn; .N-J߇bg{ shvUɑUMGj-t1#zhe:hI5clM腮=b[@Qơу?lWYXϑW 1fޖt'BÖ́2%Gbĺ%z P.i EҌ1w::%oO<5 *NcO:,_DF.*^PSQ@]^Vv P˵!"klX H{1%.g)Nfjϯ-{~HFfqx=$nK*a7Zw׍/7uڳ\(JRA5 {&2Q-bĄԔ't 9 %^KWʅ,[1z*ϔzRi'XUU~稇U˽]ReD,27823\e"u7= P(:?㘙u ޠ1D`9i'{h=pڪǮe^+>=y K< kY.# v9,wxU`w?gn*qsl}W: P1xa<{~HFbޭBq.|an$7n[ngOrtlO#N{S.̹RZTjQ?TTR'n9q+j Y l*F$F&sƿnB/t3/9ć7;+Nx?$!!|}= iܬRi{$ꋼ!P ˢlDك+~e%R=Lr!)?%dĹ] $۳#!bq CyPs(s8*_LhDNq/:sDL]#Llu03--@!oY:nfā,IReB<Y$M&a=Gxl'nbt?5}eMfobټi7slV;68-fs/#lێ>|Mes8-klEw#e|~\HIS{pPh=ʼ9NqmB#j >;7HXԚ mL%@8yO9N}Z^;Nкg[]w [Ԛ $%ԈJj+9]L?[I\bҸ4iURV{z%稗UL.n_bMVzaD&ׯ_www+e `,eyBM<ۮp#̎pz+Ը={u+Rfy~0BKFcx L8u|,5tۦͱ_t$3gtqd#2Q?(/dWq잱#I<-9fzsvsw'VbafJP(}SH e@PCwA '22hhC[P( E?m1H5e|~0[J >#?/R9qbz>l{WAo2QD'Mes9u}}SIM$ tO%t%9j!fVRƙ˽#e;4dY9qޗ<pqpV`]k!x/ ɸ7BzH Uҗ2gh9jsikuI+9g+G-pt[}~c:3鱦85>͞Sorچtw(-$$2}C9Wn-:mN9$[ߚN xMIi3qꇘ׻P5uy{ S&I8LP_R^|<@,e\HDRs6#IŃziՐX]he!I-9r4FmsEkWٳB0\;~_=dPsE }8qǬ(X'%O/bEJ%`al~/OJ5G:t[ ,z^.MsDez};di~{!6ⳋumY{=d4wd~>h32w!̷9ׁxXRB"xo}J[ uS?Ą^ T˼cfN{W:er}k!'οGlxowF3_#*FΗ V#$˜}3n(Liˠ?$BHr8swDf xw2$Ï);*wp'yW\ù}\q>;;NCSlK_%4Z:r4+3(r_?jYr'a%$IjZKyy&Xusã$v/@9WA~K(J׺R|?-TP8CLB_gD!0\]\ ATpvwך{BqxGN˫Qk/ Q nLKf挵pZ[iѝ!@9q+ʸPWeZ9b3?f{qjY@kW wZ'{Qj==DZtb8uV6}٬@Z Ӽ?gnNP3^&hDbklMR{;D1Υ\jUm)L^)uu ?\F>,sX7ƩQ+8i/ QkYQp=HaӾraT0[i џ!@9W8pn! ߗ<x9Nx9IZ)<7Tof=!C}9!N?Z6bd i1=◮ΓEpS)~qEC0}݈MEO4+_B .eBb6C$a&3X+Hsy Y}>&qkD#g}فqVj7.=iaߗ5D=9z4 9PȒ$[HTdIZԅxK ʼn c{{/B`[GL>__\ L_X׍?q<[H,2WˇBv(z:VĚ=~d}x]Yw1i`!W0f,cZcͩj T""bʴcGAR"|i^^eG]"`u|1N_bs+RewrWriA仫+\pGMT=V&剮柝K#3OW=T1 z>L3Kʫu˹N!vS&d UUjR6I$ 2cSIHm~zjm:=dV!ld2|uwwH)>`ybY,䊌XgTovyEz6ĬvQGFCR{g? fIBT40 ^92]Y 1f/ӳ'-u B姣A&%N :ނ%wk1%AeQu_EA%r'I|Y5u BP(!=O@91 ȉ˟]7 ׏J]RP( B]߂%wkQDP( Rs( BP([s(o ęV0^ Z BP(M8`][6f>ЫNP( BѤ9[gח2՝e~i/s8(&$䲌\G+θ׾{ }߯lޜ5wָ\,;n QƟʅS8\nPѻɝFg7?Viܻwh950hHa*,483Ѿܑcq;TuG\qҴaIH_\<4,!ǐfSsK[+..m`1d9ӿ o#KR׉:v|Ӄ)Js@ų_eoy^88+s̯Vn$+8ccFw)bߡ'e oL]g*h)󝠯q686)_;%!s`/yѰ T>B!&kX4;;RCBFrX"d˼n-:mk _^ߚ"2Z8駫q&BSV#K>\&Jļ"}ʃXX)[B"[QdYJLmF)b\ůt!K`Z̊ޝ}J0՝E|q.ĥRG'zs0062׶C]ɸV`!z0fm]^ѴgkB!$6y4E(MwOՏ<ֳI8ZnEEd .FTJ?s%IFcV~,~ק"VɑU2; ?״¯R~p~g)/H>C bݰ d[jN6WQkC\5A/:>W>>z,kTRO̴[},P(<ϵmB^#)W|v`rͬ,y ݿϒ\Y[pukʼn=V7b*Liˀ1L-pG{e-ZD8 #|_NmziO%wst̸ g 5eBlM283ubBjwK4v8M;mB:# ׏;Kˮ{wwuũ?_RI8cPl8aN*9Xqrf^R,}3n(hu6,q1!c`-TB`tymNKpџM!{%`1%T 80 (D~K( &dR U\^v#DZCxX,*\,IyR @L9`19рyY 'z΄QQ@]^Vv P"drxv43eB6L-uu)i#1N28\>%K6T@`=$nx9Gź@=KPȕT*T[ѰgkB! NLHMyXHgp_28̽P}Ejoa+FR% Rϑ~_*MɩOWW-ؓYF"wf_I@a!<97XoesZ Gb6<ٱT':SH}\Y2`E`(TewwWgH}#jτuӵ)(Zqן2忻9sUV,nS7keӗVhDb#i'.S燘61NUEqy> amr!8v^pׇ&dMۖٓ=S5ޔ' !sFG%s.edq2\.|/*-*5_| *M˥xBzy\t!u~8{pmՏcWr"pO_Qz=sE91?kO^&h7C2,2c<,/ʄ1@mR7 i 9pSooUBRsFMBUujB"Rߴ`3Lt/j[L{{~ȟ1 Ƹ!PHB\*Ȑwc]ށT IgGByQP 9x~E_8"a\]i|zj_u战GD+bUtt}plٰ4330JT?BдikwbGHgmr7P}|%.4sc~uPhOmL%@8ѷJ6-gL#0TLY0Ǫ/8tȦ{dzluԨݰ4ӿXn_gu|FJsM8NN RBm]ϕ}̽~C)qE]b;ӨR>]T緿X88;a秽@TJ?އc{ygLooߟ_g?~Es)@9~<~lϭJᓧz/ƮLˋ[;\j;NBߡ;N2BJk/&AW8)n4O=d{j %Ab5剫r&- .ԊqKX2&4.1B9tqUާ/xI59qɌ5N/Bu;_wu_?T[-ɦu +ed2|~pO!]f塯m. bq2zg? fn !}ocxXijX˲2\9 BOGOoNzPՔ{ tc\/tw^┞YHpBxGP(C(/\J}Rf=Bu0=qBP( BykqBP( BykqBP07WMn}R(0P( ˷5j(~?7L6ѓ׃vSw=9|`b9s!)VJGGdAt5TҸSOuĄȪ[J_X9tuqiKJd"U;]ZuojIw<^mzoi8BP(-o!a_(o+9bIzCN67W27? 8Gägթ'I120rxVğd .Eө9Who:K0%gEʽtCP(&5pRYfy\V́9OKy酱X4_kX{pk@XLb,E0BP?~\?~9Gkwwwwww[mg\i*!$k[[vɿ ٕ/8yLH3f]ݛϡP(J'X(_ȹNjG@wm{uf)}(#z,j ɸ E i1MolXOIgW=,&𺧿!HA(\7欱zKydx1 B6W xcpr9gA= c&`u!0ZB-CX.̛kST//} G,z<:op SOw2V;(O/N9{v-v ']c> R4~d2A B~>'fs >)zh9yFָ8Ue`4y*G~_e?EǽzBP( B(pHUs( BP([P( B7JݳQkALtBP( Bykn*ԶSIENDB`bpytop-1.0.68/Imgs/mini.png000066400000000000000000004060451416067541000155010ustar00rootroot00000000000000PNG  IHDRM@l7vsBITOtEXtSoftwaremate-screenshotȖJ IDATxw\gFP( Up n:жZ+]jZpPnPqY"aH#l$_n`i3p%3In 575lfO ugq]{7-)msf"o|˦}OL1EK8|vIA,rZ*'{\0?7>p=cĔ'׎8֚)٭)i Ï,Drz.?C! F9vnvo_ ^{96ë?Vry{7t`ߝîf2r^K#v호)@zUחq*0>w1 g\Ë÷"S11tõK /nƠ];f /1q;ZP5xCSڠWprz:/5Ҧ7^'&D۝t\p97=v@[5dn0m]=&>6aWڜ+VM6? Q.G={N{jtKK%\M򕳭{+uγaۿ_{i!21 ,/o#pDO;ǥ +6ژ9(FUwW9ֻ6 OKvv9x)fr"KHΧpm^?\09 bES-5'QY1Į.һ.g\-|? _ 6L}[=*247a`dJ Dg7-DgŒK|SB}pGξJl\4EU\j+e2v/\y!xemnE{{cY)~pI a#Ũ6G[qc/)nuTF{uz!.k~c[\5g&]pܵܺVu]9)Q;>ɽ[n>WRg4]% B|pH|k1ݻ3iB`>xI˺upٚsYm-f7XsR+Ao~3OJ$~keJ"ŮGN]xP)M_ sʦm?Z!Zyp8\.WѥhN]xusJ9ev!siB[XjW%%b;N`/8;ܭܷqNlKn8wo4^j‹ۧ֎b6=eWi4BgLg˟w_%&E^;^ URN0%V$ s!wZyB"TDsUwi(uPUW1#EٓyٹM.|`FjAK _6M[~bye ,ڷǸQM.v e|cSS\'s奧!&NJe^>w;{sd8qħsؙ;~{nPGt@ S_ ^Jn_nĎBLq*#12B{sދBN9F"BUחq*3yJ_ſUbbgo~(q%A=wanmZZXF 0h +5#;9" InjtsM4sTf!M Snvˍ ;{Qt>x*Nz5@>69:(V\T9YгYUiH3%~_]7d?\GmÑ{K.<:ySl9/| [(h s9|:8]I:uvYΛ-'s9e#cQO #f >Ze +f0)zߣ+N ]|>ȳ !TF!ae7^~Wt1׿0DnoL( P ''dSM쥖&v̅v 7j-bq?$eJOvx-V:-|fEIߢl;=F1T&dWVUDDʄ*.Ul5;]nv~bfǪHWTjI 9U,u;=V]PP+)aˮ*f]nAv`Ybny%Rc1j_ MSU AB݆^U^zsk![ܘF䍸k4s \;׻, XGBUKVѐ7_{µ48?:y-/9!OPDqѯDD<YشK-h[a"3h{nB!fͧdiR1œzn=Bj՜ w%j\zv˺`&sO[sc=&tsqu`W ]^eS/ 6Ngu|?:i?<=~Kd'g FIsWΠTU ![R$t=F󑐟ltM`_Zah%Wڹ9Qťܳ9WkkyyQqUx!Dt*"3?%IJխ@lѥ*rfddR5~:,:3$Mcv>XLQqlM{dD!{v-%',a5uZ`%Hn#Tw;_P n2ј! QCGtN79ěNns3rZbHW Xr:udb~Ĉq1k?*˲vw@;%F4AH5:")1JJш*9BM!DӍUh #ihlM&ԤhETà N)`J$z !Qcswà#VG\2~?Rԯ1eF (ޟr7IIΔͮopd]9Ў8˭2~A7֟ŝl]sw!] 7pxlBYKm1(yҵ#[- ]ӷlk/xr`wBX},ݒSO(ihp9 =>OT vAAsTWTmԨ-9(/*@Q-ߊCe_hjg7퍂FRܞ=*Ҹae扞(O)*Ȩu!u)Nzfl.銦9HBEfПR:+Lm@P `iTD{Xcd1hKh/.q቞vw+';?±rzF+Eme` ]T-".,lBRF0n LZ=nMoEv@@;lP65awL#*=Dᐄ-G_,LŸߙ:m% =, !ܴÜ]5>W<17ǕnؘQ\C>]ˋg(:^joo荵gr^pg~][WRDvĜ9}Ā ȑmDŽ. ?Bm9>a=9l[dF"vύF/P잠bKiWz&{Nnk~*;%BH$`ҪSܴHoدx}40ŪwUɅ%Ҧ'6p_bye U-Pťqgs"H:nCy .4o ,Fr]9Larcۭ3Y' 1DW%oM_}L4/GgB1Ean4*<8))b5R˪(R֜Wq^jL*5w`abr,|iz/IK&4IRc!De2TqYq1޽{nϰ`TO ZKePk/=֌QP1w!>~ᯜa ,YM9xZ *bpݻ|!TML s˝l93$  ZGxi>mj-r3SEHYsNXP{o]եؐ6q9ڵ%+iEs6.|Zs #Bʚ ;΋zݪOp@X/>ue@` U3{w`e6.JXv^4ID_9ER5A p$l)iQ݇́Ȅa>lSɦ'$6hhodL%p)49=}pEdľ)˱Z4nr"b(&ÊzAfSʖDGQ?]<~щl>!Mq%y^zlRҹiM6s"=?yY18OWVފ X*{ηqeȲ¼*!BW W;;-.no7MLyk3z[x# Fj&bŤK8NɂnyUմS6>VlC/i:Knx(ӕKz u:qDlQ1쮦NU48>DO)c>{j锘fυ|,=/FFv缝![ޘDQl#Yw4ٹq6i3yJmzoQY;\Gݕ}Ep8\.OxТ<{QxV@#Dtq]x ;Ͼ*nk/L"ODSfX+W#aos]"RS>4'͚?7RYQ;;;k)I^TߟSϹrc} !iy735 /@\&K?9/°Cη@8BރG%PޟSUҳ E Խ?s;%&1)h=~TĠss=Toaf==hQ?\U֜o|MedZy g"eRTH0/hɱ@apM u͔ IDATW.$XZj !8TA^EYU/.0 >AKL TN#X'Fގ'zrVOv^pˍ,w02v?ɛv~1ċgكFc)!w Oc?[>y-,wr^}<8_o<1^Z6x2GsnӨ-!MVfqsS J4BU7٥|*(XÐh_YPRTsTK:uPQ tQE6%D9\fi6AneAe B"n&+DЩړM*r%,lBӠ*Jb3 4B VvPؙ y)5J' V[ sCd@MUBi/=#7AE-Wks?i3#h A-5ykha'<2HSTA+= 0r[;?d~f :݊NjTQ[˺̏n819m) \g҄ ^B>pbx%0Dnؔ^\OHȦt]K-Me,g >Z^ ;%}g)B{B+K^yrse<Խ3QS2k>c|yL*._4 =AŖvuX0 1÷ wt"HYsz91%.(B'/q^z5JgFvvƓ󂟖%BƳkuڿ}ϰe[<9:[sGM_ϸ|0-[lUp}}ea?:[{y y>_?B!F1ؗ֟(1~%W(!DƝ͡zik5[i>bEYo{w)D Jt}uΫye{n.DWnyYZ~Ю1T&H u=~F=^L.*y/019=C7<㪜jVVo H\ P7PWa  ~PzcͮHVv^d5DŽNEԧ˰ɼgDU6Ӈi$@gL0ك.=&|u%j/tn?Y0ufgH 448 آb EreGWwd +h˜rE\Y/=} b!Bk7ut'_14dJ~y٩9MTCRS߽T1b1[^:Y1$=[V@@@TY1?9q k-XTBqzl&(JXv^̄mwa;2Ki~!brS<=Bd[ :<"B!)(|٫C{aG桯>emB+ϋ5>a5uZ`peiGY] D D9,:ԿiQuղ{WFe?{@4Cte_ʊ+k/AcݬUywjnx9{˓_^!O^T6BF*[oM_ί7h 7hL_ % JP.WtnQ]Q+V*·K&4IRc!De2TqYq1޽{nϰ`8{V5.{}nX!6}pm3c?ur?Z!Xv=R˛rʕ?6Ub zR8|=&B͗{8a;‚E=,3hݲfA<&C'|o:mgԷ\EnfJ8a]Q󸩚&V#71Ɩ]=|57gF8`~{;?NoKBʚp‚ݻ|:_.UuGTņ{ 9(1 " F8MR<Y4p w!s'0q?/^yq@|peĞL.* y(4110 ~Nw1ax frWF70!6RE&{Dtijl49QLb~ (4TUd<q4UC!]q/UVz[ZY(q{6,w3tnZse>D&Ƽ=وj<`hbL|zdg#5 ab%tNɪw2_I:N?kM s7ffFo,.tB#$}G97d駔1k de*סXKņ9ST|avWSO*Zdr\Ո NFjƻ1!eC`x9P)qΧ}D͑>[Tx4ʤѳ AV.mDgDpYw1gw<)ZhpLڐw»DZO\i o<(FfvC\w翮5鍙XNCf 9[\ihT?( a iBaVd7ȫpț/څ54i\=1-]^Ln eYL]3CT{>"Bp^C[}/>fUq8kN~gd43pv؅ zJ7_MͰϘ KO?}+g:Z*.;Ckk!:nC BGt%dmoqnmHoS*x'[2{9߫:%h}K 2n).]Wc.|TfiQR^8m~rq/!ԇmu&~~qM鿰 =zL)&''@m{;q {S]m_ifiQR&8JY$2יӷG ż-֗cuԢ) TPPbd9.A1z2->Qmbn=lM\f:uf*zSܺi^^  Ote֩1cI 5lNM@reGv2Y q_tϷrzZ_B+eixlQ5ő'7药g4zueH4IRcJ}I9ZJ~n/"M\Hvo\Z"vLvܺ_z!296b'#5 a _2axNb1`1uQ4 1k)!2Aao?!SܔIb_y>pȠ9ͬ4 3v2UYWPWVSw Wsnw uҼk0 n*}]ȼ;)76d#D?^,U˖)fZns]~ó{mu lEM"C r@psXuJYRnlJIoǜOٯdiRᔳW4XfUxTG43:f'!ԿZt[" a1L S_۽9ԩ!De_hzȌl$Hk}1{uϝ"1²pB#Be ObDyL<*R$`B|9)lSh7h~loJjf{x+Ot9_&Z=tr12U*_o|8]佼RO7;e98*H(g/&滑nH%}Σnܳn;}c[ 1T//WO.+kE]QJG7+d5$ӒDE$BM&Ux{iWO[+G"!q{$S*f=Aopd] #r]&,5 u&/M+' :C/ Y7+p`,m*PK[%܃Ņ?u|8KR+HjA*3HV ;9_[^n$562Qץc6*7s000Ut)gùiޖ86 Qwi':){'ndG8ݱ8=By?HCqУ 'O&tqVu~.<\>9Npus$3tx?MM [/8.3Dq3YY%NTԞl]T-g BdayYMEP[iP%B!Ayi\JvPؙOVdsK*DYPmTV~,W֮@F0n ƭHykev@@;lykLm [.Zl-(/%&fJ8VLݢwW:uZy=El9-MFtNrLBae}>5eR5K}$4=)ڪsW]U[Ѣ{aĖSB+<ˮ?3U(!Ԩ bX.g[@3 )_ ZΡia5 jL%(0MnFw&ijE@c6Q$aIF pIֆ-yrJ_e[i">MS%$dšKٴ9tyI P7cն/4 h1q~=/a`%RK݊/RLjFbpJpA͕Sb%/#TtJ%e,=Ú-jb/dqkU9ֽU IDAT\Pw۽׶kBUf$k!WDn1h+b R"4]c;޸5qE{ -$4O0 #T5ՙXq(|jNU;4fkpt˨dg4,ؐo_x^3ۭoW9iԖiḵ #rl2yk Uz$c_ vU@i5CY@D!v@@;lP6(h sʦ{B]+ /Vt@`@@;lP6(h s9evN@-+j"I hBaT$36}ُ${*ZEGޢWIe\U*=eLy'ܾz?%o0wVs 7cUǶqWl;#Qu}iQf98eÕ\3j v.-_el s=*g8Mc3w_I9O;klf@"a5~nW) >p+[8yh6}We\ ڻJe٭)i Ï,D8W0Le wsSb\;FaZgl=( 8֚) CE搝/߅.&ܨ^i%*-ݕol'&LG9y3yg-z|n/>Sx@ ;[5yu5~~OSXNVRSW:,aՋQ9;-+nEҒc_=3k0-/mjj݀kًLO_gg6fv=~-)ZblTh1"#a,en#!D~Gobܙ [kC^TEm0|+m&a!DZMw`O?9}mhifm]=&ɜ~  6Ǡ];ylGx5ī/=TZٟ@"n1jj @N n2m舘,{/Cvr+ !򕳭{+VM6k7b 4Hw@O<]%k=7{Djs'[?J k2%MMOKEd⭻] 8qo/[PN=UN|ޯYm.ɭ Id[;tY :z=ըEvԬ3)Ėt|QΤ>1BHL*95)R B *#1mb>5̤&s~ݚR cN=(#}߯ Ei4 9tK?+"" !2w s fd\GYThN'zd(LQNRL&]4HGTƵ\1+EWf> /,L D&=.FΦ%-qN/ľɡZr~B/>v3upLa"ݴMASPSzH="¸]7ϕדitǑB-Y)u淈rgy꯷+y_{&ޗC#Đ &F$ ע‹%^JaclՊ7 -&F5IˮȆ!Vυ?=#Uc[*ޝ0e6(wի7X(hW@b 2I0`o.S BLOIxIt'Srlx<&5_<^ui!I\x2SNa?[\2L_Šnf e1`TEUW%B R ^cHt7Ki%J_c Tp':0I(H IR9&n.)~ &.RAK^ :ٗnY£Z~Ƿn:,HOϮ5?]H^xK}xb/^tS! 5jV;BǦCkb~ZF.$ iHlK>wuy=m |0;$ F4EV[=dziү(;A=%͔tAe!]&w+ɌRzCBH0/ݽ»m{?{ݻJ6 **b7k4{4Dck,?Kw Hw7{!Gf73̮Zz2 ͢G[ ="Dvҍ42!,C3RJ,vE1\VzHr3^xj9,gy/߀5EҜ񶳟_c7"DM'ؿrӡ>]y~`Iz~C'i/wQu !󿊋u޾e}*jOit螵% T?ij6lhȒd/pMMNi]̩Nu&JO% -=cIKՕ7CZjzPб!9,i`SU/&p vO/{aq nLO:C6K'yؤ؄"zr xR <1ɩ4\q3#2!Or4ԷFDaFNFvy*({\ Uՙɣ-()tasᰂ* \GQCqg5BHKԱton,]܎zn);[(w=fPc[7Us _8+5gq')+N<d{=3{qܼCOQ#Þ|w̥Ҵl깅s_z- :^v&}`r~~qtYO-Z>Aģ~|ʱ /I? 7)>>_XxRO z~ue*l}>I $#ל&(4^ѧG@_* ΜF}vG&$DfNRuXKT Qݑg:!_V"աЫ |q]ϨI3l0rݱWd9uޘv# ?Ʊ(jHz棫Ը7[h:__qT_k]7}w &"Ң~6JU5i7쫠 :~cN#ݒ}k^ZothwOz_7HG=3 #Ws%=T:Ԝ Lc[S 9Q[ yǬԱ}vG&$ľw]T\L؛;&yI1>|ŽkϪO"ccB/(FŅ#+欻('o!hքd#} OcJHp$m7obb90NHf˸SRt\TlTU|c8Ź(Fxj퓩-ܽw{*,1'5|c=g9X\_,?Yagc[gn_6Yan66666^$UF톉?:ܥf9s-'`iʕ]#_'T3ȂP"]]! veh"9Tr^<5^xM{ޖZ W05uvXaCR 3#3~9j孔~9ɜ+:{zͳZU)E%W$Gd'dk[kяVRX 5LLMH$EFH#+ټ'3Bmk"b>zW,'D_TKaƜxTkքdbC&6bCAaGaq Z|~b@Z#T^9(Fmq~]yL:e{U.ޯF|ǟK}T=^^5q=sy[hg :ݻH߷I r2<ߟ1G!lLQMB8l( XWҢ?je($ DQB]+i}i R9N?uX6~4Q)+2d$2+9m*O# pS9en kTԯT:VOER5#fs*8WQN}W,l+FO9.Jk``r1SKj%? !]b9dd{G^Pi(m!Yp~c麲 U80{go۰^ͺmi +-=`ЦܮQ1S9MK"uapuF)zW,vP̓+[㨅g}DPH*Hͤr%mElblH: EfNu+CÝm5iMMN)"&}IG,⹚Y+JlFAk}s籔c}iE`RSiDtf2Bj52h-tn*ǹFl_'{)rO0 uc{:<]#F˺}>0G#;]{.#vfuƜ{!UaCS}GF$~Iq KaoOHV_or唑F$V$Mȷ? <7yw'ccq^1_Sh -6X-9|d!A JXJӨ.V$IlD= !6]ޛ%N[Z1]f\_$$ 7;[!}Mgғa]7wF7Ncwdr&zgg'.[x,kĦ\][ZCA{N'#Y!tZ,P߄N o͙W&dIz.b+"lgt] S_sޭ ,I^BX6UÒ"W7c̾9sOoFU m_˔_U1U h< o \x h3"W{ǬU;.o煡8\V{G a5Uw.!jhy迻[JKzaf gQi3{v㰿&o;"FmxKEH|־ym &~GǕzah"B/w9]s青~&p ^2Dv)*jC?4b^:\ƶz>-һ>r/C {QS;sc t5W4c_:mas}_lRYF qNdSuEX*9%= 7͚|Kdځ3ULZwf,F (̄@q9Eo~fb7$ᛟnΑ.݌3Zz>M4M5=Eu{d8"l\V47fÃd9-Wvlh,HkSXCX֫]kS|o?(ٓh8qN0"W} ܌8aQv;g9"aEVy޳qkʿ  EH|5=处>=eLVfхU ttIОQ0!Fcrr4SiFxֳqha4i{5&}On/͢2ODmW]p$ ,:;!"۲E~3yNh([$'G8 yێ)!zU 9r 0pXU?-'Yk䮥zAǥ<V{|ڎ^ZCќq[rMRF۠MR|e:8U:K?NilK~ d\.&tz:onP~_qၭߨU v0'M}Ϳ0F a "mVz 6z0"BHh;P?9 8qI'ؗTHJoBHk?:C:.[rϴ}K"Ųl} IDAT~:v4,w񸦺;r?v/s7e{&80'C ֱgQhD{Ϟ:A?N'm zx /ìvc%4o?pha"V:6'NeGU* y6.Jj ecW+<6'i5믬r oN5ʓm H K[D[(]-̩/ ') ",R9AaCV9 o_;PqD5xwf~5{;W!M Yw݀?Z1K%y|,IN')&R&) rfo%IB,"M& ͹}IS2X>!KwiS8aB؄aEqұ9fgfdegttfsn\MW׽^s2 sO[ DBIJ,[6rrI\fVcz,}NǜYFؗ$C6!$yuDtlN:y3Fi;>*J'.=-*5BG<mج)׼atS7{G8Ա s[ר;g$$·*[n=rkrwv4kK~g4p[#ͻutrĮbANi7vDNty|kN4 *8C.?CTslIOI&UW !j߭6!IR8#Gn]Jk0߯xָȹO]\cE%n>r.&Hi8^Kvx+B,+{ZͰ4APIpұ9$OTc768WHJ3gIT8@б5fLpf$MnɖjS5(N>!D҆gi!)@4V:.JێMhd#5TBpt"7go-hjުQ拕Ɍ -xk̮\'O f <`V"t9q!NT|XAz>gi|_ad=lZHn~P6RW{ >쟿RE246-coJ,CgF޽tdKUzmOY\.=̔ 윮 a ˰9aŦ5W~iF{<a(mDra86$AZ#,nS1@"ɼ'O&hDQ$Q'c˲aNEH@˔ؒi|O7[ZmnBde^Gé0֞=X:vN!'rBnxjiرwkAHP2#w~iFߨ$鳿tu?/u SJE B D'~g(e(] C=^k}U D =^I"Clԑ:?̱M-tx|²`40`[Z\MTFʶFVR3y/'4ps 5Z>a?yAPPZ 9O(L{ZW;CuAut z+HS ;_Y@|J!Mm,|guÂt߯unD(^[n}g$q'*!MɔDwRJ"-<>0XUs>?ȞTWOIl2+vݧ+۷&Mg31W"AJxMa__"]-j: ^gSY/8WDlh3),~A %ckDGOI}tп[/+ dۊdM~{Zyw_r0D7@/됄>*bFږX>^[zgWW \b48VjAF2=Hjd.^N ^W˳GSv⛮?:6ܠ UK-9U/ ݯo5&2$z%D\_I[)?7(H!g%AdsݝLu,3)S77!WBW%Dk)Km@:EVe C u+ϓ0KT^W΀gWT?~X9]v2"2@8"#^aFvE1Qw;H aE%Qn~c( +Ji &*" JQYvCh7kuZ[Zh;tzI{M8J@Q>PՀ8Fq0xCd9IVcW;u ;?!DwAr .޾S}ԡ"L^s۷ٷ_]] N~uP([~XWzUп.pfc#ͨ$a٬5+Zf"DjwK} @"iWs32tsLEc訓xL5}xyyyyyuEjxh(AS:ݳK~3\+]@ o:vmìFo3V .=!jCw^ɕx:p]0k{rH#Mz+ف:`ajA-͖hH? jV'/U1&S󯓳2| \ؐj$"$/7 }.u'¦c7^f!D5r$Kblf̡Ggt|0RT\윮LL"QvV8Vmyv[)> s3Jll*O-ltpEEqI?f_PHz=-O=q΍=D9ZHq:VPtfh'gR5ХGtjXc\*I:fmQO˾%ƶl.tL0cOoZʧ em %HX qh@1 7OCeIm]1KUzI^ Η#HiM)$H,A=>Hnn< ?re_xvm b~":,=2B^!ǾBqS9gOssA|FB@/an?"~WRn{񘂦&}%),wْ G%ѳm[*JORLz䴈_jJǠGssA.'`.2|d]}K.{fjDD$HAVf$Qyf;^(֠|U*7`սҋ:/9B,~"'n{ WHӆZ/o}qIǣ3ߦ[DNtuk(Y  [}^S:z`i)aiBXGqD<ePnLد'5̤tLJc'Uy{Ǝ;N#:Cu&ۊ59!Lts 8 A{4^vVCj%At#jhUNHY䤫2W,^Pj(„Jv>GP߫nю wOq}[G!ƫG MX~G8CXu_~uL~=s7ee1":/Cds8(ΛVyN`j9P$Mm/tGuP#7N`n5TScfSod;{%Wx(>Ꞧ:uJQ]k9#$}lG&?X| ]VTm~5b"g_ʾLylf+{za.ψ)=aS@t~^P\zi6k`X;iNGed4]-fOyX`+Sz?,)yတ`ܶGkSlm-bf8E:z#D.2'*7((zO\s*7ob]M)$=R:>egN; ߤ7SE2@Y>O~KD.n[<c ]##" o}ߏgswjΊGxaݚm|Y0:U:|RGΐssJG֐&d$Z\X\(T?P:|7 n<ؤsGHŚ Z̛ڜ: 2k~mNC7oN4&!$jRn'XNag;Y%bN4̛|`u>g,BDAjDu"+M$;+5{}k|@@UPs-眾zڵ4_}|HVp\ |{B~ٹM37oFPOŌ_ڕF%¤+t;#{ps1<˲*_U'em {Um3cQMz2^|Oufu[єFì+]VvL|Ք{7NʴJLur%~n`u!ʠMYשS#6;L8UyRڿ0i:"|Vw KBHz?(%j!A#(]CtwS{!iŻ=C\:ܹ$~;G.M S]_hrM@=ԻefVyz?Lhw\]kF{꘹X;'0h4jာÿ]pDpxShe_{dp>W2iqy,WR6yd{Ylk.t]LfmnMnOSF6KHuQ@5<5|;sYQ^ MbMqIbk;[ok7sg,x$dPkM=v쇏:J֞m_{UfOvAhץHvezi,lNe p!npć).enabqutijF֩,r3LȆQyƟwgtoe!$l9pof˶G":> iW\W@tԱiSz92(Lc.Ɵc8 NskǾ8s,!$h:ݫ?LҏNʩQ罯3l"hҬٰɣ7$_ݽD :)6˄O"E ұ<|ℙ뻾[bۦ[b-sŕN쵉K*RrJ$VݘD_s1;a ]2CI܃hF[xL֩RO-nk\qwݜ&EV r|pҐ%;v2ﶟf^zy료f\+]A%GuYr.Y"7Wym,,.)vTN圅:x{ 2*zvor 7=p@E#kn+pZ9zomGn]'iH5}䘥^ F5FכGοZT.i({Er4mQ9 ~n?9O'T*.HpɭRC% W9.ULxP9ˢ ɺt .a^f6%6]&6Y3[$yθVޢы ʫŕG2"Uwbq?޼fѭ1hO V+{2s9Xkh;F䗠=PMOzjP 1'H+eQ/">m-R2s44iH A}#D.9Yú^ʅ--hCʙ=s[>6!ͭ[?ԛՃSӤcY+9r:ht1+œ}p'DStŏ:`NEed^Mɷ3Waq"r3 Kc9؜!a]+jr^m}܃n+;ɿ{-*(U9/tH⪻Hq:GW-}jO T!+ub/m|m+X?j,iݼqGϞ.a.r`IKOmMzڵkݨ ¬f͍ GNe*i`:6a-vb^*IƖ__e sRL5*I^octO.1,z~-gl{!wg_TWCƜIt*^NC_.޲G:2&ԏcJ:qaKs&3x9R!m:{TĆty0?c|0uK4! &'NO|N(?;T'wupKyNJ$)j<WHqbaj4$}[g^"aSD%m/(*]ұM)V IDATi:Ύ rc!&heJub9,=qBt(ۥͳo~x~ߝm֚W);Jb5r kXY[;V*$=s8áe9Qջ^u@oƃ=w\δ݀/ƅ;TrnӾ*ݶ!f P|s |0j:j YX>>>g gdR¸U(傊l&Chkw:_D'}a=A5l$K`uuuuTF ,t =,|n#o݌,_Б{zebX\'BHX߫wRF=Bz}TUKy::0ܺSf5DHq^z*_b^.fW [:{oQ'f8*.e?m͚s:Q8&'-U|TGPRbo$u!s?1I ߌ .l1럞tnvL]\M5ˊ&~3*V:Qۮ+ZWaӓS?  f/H;*{U4QIG]TPk?99b.Ta6Gu)=խ]զ:'Ԍs3ɓ"7Y挌-֟p1QŠ(aH w_5d9Β{"#|"69i/'p`Cӌ5}d\4s1b"B q٢9iḷq=mh&h2ކ!&͓,sq$9|":?E:x{:=۷rx 6vѹ5Ǿ,6XT: ^cLLFz3)Y$ h]b8'E8ج?sɉ`<(8n^7ͼ2uBw#S7L]SEdqвzqjϡc#m;443o*>D1PqA I M#Z޳joBQ$QthZd5._NFϻlz}_L qXׁD8?ȿȠ.Kn6%()C%x9z웫ϼ6&bE}sFYt2ғi wq/3>sF6A!$ymK ʈ}7G,&SNs;Ǎ?~lˑU49DtaF)Nx.[(y!I?6bG&YSPR\$"YF=܏ Nxuoo4aXOG ]x/;j#wÓ?Ⱥ^,-9j|S:w` 4;}84I|)venBAE딤~)!([pY糩U\x,"WzaQub^~{7Y’ަd}~t幸UٛN.XLܥM;Fdd&_r"WqIȽ+R5'a^)FG=NS d>ǂrrsrň ,[G8V6j"`E]{QZwNBhطݭ؃c4IO0}.=vA ;70M鹑g.ݻ[6F>q˃%{0?B,[ ,eX@P(HN&#;A)SN$H[ tr8)W֨/!bB}i\%!]bLjb:r"AF]$.L%\hg  W4xmɎthu4pD[! \ =3EnvOjfH!5M;DtV/W|TMǖ:JmGc&?_OdH&7hڹYW1aDlMR1N'$A)?&⫚JJgND`C rcU4,$%B$Dha`fJJg޻\S<'TK%*C[{CSa٦M #aܢs;ϊE3\>հVKW_GcSs;y(/VK@E /*jHRVZWWPX O*ơ#:lhoܫ|0uG#={5 S"U VKMYH ]m,Lf쎧U7PAf*h30QhGx0K kF\HDtrꨤq 5GLuƃftUb??ص~꘍ǗQ1JI; mD$!R:*P<[@3okxNGٙDHeOpQR#zc΍%w TPQsUF;@dpnv!tu 3qw私Z+ t_fA|@9<^? AQ[&=]ђ!utl (:77!,MOL/T݅?*AfC5f=XyC 3k_ʹKy[SI[M[H|9!I:3h!qNvh +-)Jsj[PŁ89T5 5Ǵ < WBXy,}jMT,FCf|yqrky(f شF! ki&+s>X|z*vm!Bܐb>T7MCe>4Ǵ>3T=S.&w]9[_J& }K+wGUQ ^s>ڐ@Wct;MC#&&DhH*$L'q (c3s{!񻝣9CyDI>c|<4}gϼ?Ft|Ǎ4!mYǗL)kYшCt z7Z9sմ"-bҶrr ʣU?ZݐYDuΙC'f3iK@ keaPg 3j~߭W?\ׂ|;gK =2m,99$2hZy7JA*NEܮt7׭_4uýeã^.,I~ *E!nՄű1%VP_jlEEUP[1mX\q  SuHo_dvKool8L8Bǰ;9WĿ)SYP%vqN^Уu!bXЎuY~N/ǩ<\NAtbLy''Cy;y9U:ݡvkJQ4!h#tnשcT9QҤDŽU= 2-@#amUͤjwdn&nNM#59].ͨԒ>=͉/c٧AlTi}#K)Х_]}kۛu9C}ƧDV !D2nOEID k^ym=_&w:?G7r2cB\縷=1kaqKާ*&bs"u!DziZ/#@9̌{|i2)+Ky:UL+;[_L'^_6+v42kȰg曚3GTjLkc:>Z~k?O5 rߚݮ1wIO vM6'60.=mzQ+ҜHoɹ=Q%6t ˰4~@\\}*ש"qD@?s~mczZZв{M3$~/N$дr8Ajt1":xټ::S/] $E'}J"u2LᚓMQOjyT G'݈ =‰\ul{ y0.zLLƒOZvi<ɽ[q;ԂQ Yt40k >|\H_WRL뗔 9O>6yD+p OeYH?! sH Y U?=]// ,S>[kaYRkleuJ:^M-I|n.:vmBDGGyض޲`iվ{cs+,^Xjt{6wFpޜQTA|ZbVyA]e.ەN9a^J?{gݻ\=hM nZRHhK_ܝHqw%INfrwfgGV1 c]Di?W7֎}(.5 ku;bk- ݤ2j6wdE9/2 ŏL'o2c=6W-i3ʜN_v%P=/X2EҚEiΛ-ވ3nOtJ -i~ނIDxW,*Jp`|oo], x玭^6iNt?.suT8zs;ȁ/w#gUa}^xMZŢL_ 7 հ;p@0 Ky)$2btWh  5 `0 գ1V`0 `0`0 |i}`0 ~3w` $;/E^@va}%&~a| S^tj?sNRk*hj|\Xoo}1Qoժ4t$WEo'3i_LG-ƞ`P.{ѫ2^o@.?-wuub:9<)/ludó.f>,LQ42yt'ּ?t:BlLM  IDAT|k&{G,9T\Z>㾮WE=oؙ4Dj|a'3~gY+V 1/ׂNNr9!]M]s*?|4v'Zwi-̢"5mɓxk_peMӧԪM{MؖF[ JrT[+Lx?:"XNaG pd]}[^++6WEnieB Ӛ^۫2}f2ab'`0}3Wf&~~GNJ{2eb6SCQ֝7ZrDR&jB|m;Сyӝ5?V灰p&OV.X8rr#Z{͚^csć7^ aO&ӈ-Yӫv= xxŠ^Dh -xWʚR$5?)D}*fY^U}D9$ٽx5YLq= :^F@ڹ![f}[[fl;b6öyE$ɤd m`v=PEڃ#+}m O&u:4֘J8OS_ktgS(L6yn^˺"Y:Lf/ev/d"jB? |z4~1k& /34;1 42D0sz "iPNws\;*5ɚO4s[B}]5oL3l{ʾ3p⣒#G.3c~#+JIo~td]?+5$ r91c߽"9[͈, е[~v Mej&D:la84i|d%{~WP.]ELx׻luptL'vUڃL]9ᦓڛH7מ ]QI25_́fPe|oYm#d>3򮮙1faÆ/KW/V2SЩ y.+d 4".1{ewý,2~3,p춓Fl3e!e"k2N^}3]#[Ɲ+`0 Ae1oֱ z7cKSz S-TX3[Qqm~dղsot ߬gk'BųiOB0$=ydM@BZppfqY7nzLɛ}߲{Ln I4 \5w|eYd;?u{G@||}^D9Lo>,H~`j)S7gwW}HI=@j& Nrj$'e62eMJTSZwnyp5a:T?ݍpdsIt9PAfVlퟰdlujծ_7.E($&M%_=(E?Xi؉`0'. yV !-s܃KO ^y%;|sȜj]-1zՌ<5FN̻p ߬7-칒4D#'Zgnh#|{ݢsi|w?V~~lcM[V idszNQr!,r!Qd}?>(] ]Zvgj !TF `[1h f5 JPPߋMkG$Je~U(.婗,yAEyu¸Lfpf:;= dPB BJ '<9I* H&E^s@{י N `>Тμvݝ Å o1hr3 >>+2/?]]j+nݳ^El~ o >;c9D}m˯וzy>Ӟx%h;|1b\>3ӞM# fTpCG"BMPg7,ݾBtK{4aJc#FOVkne$?I(xrC4A" L[o;ZYfjiхL%.{5rխ"5I%.о[{%[C`0OűinҒ2zS-"3Nn#/(u7+z0v!ݥuN~Ô%Gx.9< Cݥuv<]s1o1$;u.҇vY4$]xޡ2#]dKQ&Ik9|}s۷en>]n 04LEˉ25)ޒA@2R7\> RHn$Xia--R N `>%TzvC`wSscoZS s\wʼnMy69/bc*Wl:Mw W?VNkF) Vk `0 |i}`0 xͦ>2TING].f4}|׆&ta˿w_.]v7^/WO4ϛgr/cQﺓ1%rܛ cRօߝL̬ )l9&hbwQ2y>2>`t! F\k6?zgYfLMFTةwx>6~rݵ54HZ.f;]=QDzR|P~eݚ+P%?*s˃QnT1`,&Ѭ6Zv.PRޟv>+=jnf& \-H1YpĂ* pF0}c9²҈!BK+ EP1#wF+wW':tbL ᥟBs$︚r5}Ω;ig M;ʽc &']Y4tN8Tx@V]|uٶOOO~;ڈ˶.wJ1YEUN-E#~[ilޣW] ϡ(|49z'xu`;pM'kym:. ,x%P^ -\tdm:Mg߅^St8鿛t'ޥD\0_\rQkկro߻b~3#.M[[ЛBҮIftʝ"`呕!?dlgWrcO :! 3NX_5H\"}s= E CڸvP- H琶~S|!-EcJ!eE&\;^7UEk 2?:.DPkqE&& niu믦\1\v,a aF襣.Ȱ]Tf_Ǧ"-?_ {sCHˀ%_Țb6b;m̔KDTqן r$%H渡baoܢ-NNXA=0L";3>4R,n'/Y7ݻKdzOqxIӆHI#ZdԿc od SSS/SZuX*<>)Wv/௥i**շY P8c Sr_9>l'*"LYR|~ƂoaօeΈ =΁i xydKvܿq  }x}n)+ۜcPiMWq\ `xw^scbD4<v a'wG)ETDlAVFpl,]:(}Ȧ80y3dv'=pyb)|hpȌ.\Nz#VA" so=FCv(I|FƸAe=#<XH$r9irO)/-.)(+-. O1ey]`>aVIu~fU7֫`ג`xSsGp$uruqo9n-ػCaY3:Rw=4[|"  :fu$!mg3n]rYBA42cDGo!$!sE"Oesdı :::HV*6B߇O=L)9Y%'l04%)wZ+; h⑒JԸzIQ7tqAMb"{+vZE4M+D{ı9KMdv"=BA@42^9;O-M$l A{o5qp*d:Sx@Txvahq^X&s'*=*u%?PM@'C^䶺Olw+v,x%B0jÐdGS;*ú,=3-RC;蝎 5i *^^-{&ijssYcq6٫As籾#G"[tl2͇ya"ƟJʑ@( H0 ŲcDnM|sIܚ NvFPI4MwRSyeYy1'}ejvi2h)S.;`<HM2hYd%MP䬝!Hh!#CKH;5:pNEG&A#=!:5ne.F(|?"]=Qv.k>]%D/xÔϩ,4mvoW"aS>K+Cv 8_u=ۥ\)c0 S|r x*Ϥu x33̫4%ci-M">,#Z656uǏT])6w퇘l!PQ.k Ṹ-0uh`>*3?ޥmg}-ddm򣬝5H/ı4تD#]uuGB3B@ejϽ}WdR2R7EX. &]~ ރ,R NǺvX`3ji~MNmI9WmP}k:H;qGlt! LG^4s&27651 MM#pTVAsQ&j|NK__2mʅ 9wtz0퍤9J&tQe'wcʖ+?tH3`LL҂IM˵QWM'\,r].a)D\: Ou`0|8N|r;i(5 B j,@cW̄:tG5"ShNE.|(>!F6t5— !8ҲLbFPe% 3Jg2x;@`22+ N ` ɍ>ӍMd<~6'^v__ϵQX] `0LFҲ*ޞ",)gRTIILFaA!@0 `0rY< IDAT_X`0 `V>`0 |i}`0  `0 5^[kk$'cG3MZPk/WJ-ܹ ]}7$ s˗Hc{w#11m:~{_U4%UؾV=djQgq]+Ik@pw7Atw(Fth3yFJP92\ P!j9֡+RoGjό {Y'->VanV7n:BUadfts$v*䢩/I4틍r%Dz ҏ*4 ¥m{'ߖM2̯nN?`mUdMѳU5dC dsr\˱YHPO ,T!wj򼃗OhfP#m5"@=t;YPՄGOI?vghf$ʅȣsN0OY&jSJ>X݅.HcݛT8bN6>^kM\txuoao˥24’_ot 4w&ӐtςEӯ /kj.N~rMu퉰 7i .aop kzN}x ,/bt ’iYKZr$bN/I'_\Zq ='=#{4>4ӂNBI-b>.gTۦ"3֭tses*O+ԴӓloXO-]Jd(B gt#KO5xTkӧE wࡁ AFG|(Tw ߆H- y/]PRУiJbnmژ?A+Xh̄| : [OEyw.a `a<靁Д<A74H>jOa䩵lǎ'|֚ʊ*kp-K`X# iؙ=mx1Z='Ô Ə?q?46mMlysSfQeႍW"U.vtsg o|,qoOTX=[>>R2Qk5 3oΞ0aݶW 䑀ivduR|^̀WǖC.WO0AR; A?+~`m'9K記!:N>R(M8[fliY,9ư ,bwL.ؚ{0ڠƺGt0,P51,5ʅ#-xdw ;N4i^D՞6UJ\I# gG~2*t!HDj΃vm|!Vќ bhMBZB o DeƒhC87E2>Ӕ8@I_љ`B<¨a tNRSIL@3T?GEl#ƾ}{{1#<\߿GO?J-3I#rx&B.\ ,ڑs2#<Ҥ9:t ȣkbi[[ rO5 ʇMI(K`X=Rl*[o_rnIad$P5+O_;&Vxdw:"^S(BDHi,ݡ:th1Ps&tCELXas&x)WíO&EzSX`@sb9\eY=ޭ~ R:I, ?p-VV$#V$G ߶v2M<"E]ٹ .'m3I:4 B:7w3`اYF)lW> htz?:HAz7(FOHv`69*3?ʱ[-9Z,al5s貼Ww x >o7R,{aqWlVDov1D1H[d:*Sל6[pL0-'>駇C4 ]!~U^Jv eEӟ{`%HoLj N#Cl =^Ur*9@ȩ6@c!>>?@* ]u[=J["cѥWŃİ`=4uRCWd | iVS2x&F};刊`0 &0Rp2РUEZ#룰g1Uh^N |i#'aiq1_XN@s0 `0"4`0 `j>`0 |i}`0  `0 }kТQ:ַ7 Sޜ?pVCS"Nk2a&W7ӗ\r5ʅoQz^?yFl1J;F@k]|IxƬ,2\VԮ%?$RxƬ陴~-~p ! _? %,C[/75JTYɄ?,zy(&G*) JsHfTM?ji4)eBGIդO|hYgC'STw$NHEj$>K$i"m.q$c ) =l&o*3C_ͷwwΥf5\X_H' i9M{MKcb N-rN/t>o`뗿cNVϦ0\h& uQ%eoqa& H^q,\Ȧ6NaGlX_Z1qƑ{f)'ch ͑'I:׮<#_=u?0uk5\<#L(h֤W׺W* 2i̅*Pl&i3d LCH):3!fcMd!Gd(gH9tZvqDKS{{\ŠSnp@si>۔NC@9*~Hs Og.fELبaR`.H}@ϡc.],ʈI< ^- :r!Yӳv#% }7֎ԃ)Bs=BѰCwJ5:-k xM&:9̮uE)K&oaQlg9}};ک"eEVƵ٫ym !g3Q((B [O[-73C~3Lg ABrRzf]zj<yMNkzRŶ/7FDjG^O2 O9AfNE #iCeX/T&"$FcngVp.+s'B&߹љ`$8C4[M-gԨ#2ͽfc% xz7aDxrb1ćR"h9YfCiRoz 9*J-ߞ+ab@2Qn" Sd%Ev"BeB\腙ï0.bIzO`8|3^}7i:tI=Qc-`Nk7]'7?о{@$i |MW?Ԥ<µc/N\KR ) h?q eWV; Nγ^_`~}8xI^)KM ƍ7yHzV?Enс4<6]o~ԯ@z/?ԄmF:W?aHSVD>;o fN:eʔi+M2L}еv\&)Bd}u[ǁM]*c%v/NVʑW6or\ַQYs[nd'&2Z–6°# 6̑=djn3fؓA9HDNSU{ӌ:fGH5=iUC&rZn}FQSQ&ԪC}cS6 EuT@+RmِJk֪9ȮqLtd{sD4 V/EZC5etS\rI6CeT);99E_nzfٴc|Bdƿp" eh8A?/ &Axۑtଚ}!ae.綞|QB0uC73wQ3׶t۝;jG"S:kph$~ظo@KT/hټW?+N*ݨR&W=G;i7(5?U5kzIMF1^\Z"Ga:DDC?  Xdi>k3iTsחFڑ7sݳDL9kI25|s4LjGzHdTT2 36 pOFĖ T ]\VD#Y騚(79-i]˲) ;";vnO=/hCM!B&5s1s W:+SD!BORUMfC&i Lˣ*X#D8wvxuڴd5A;\B%Z&iHlyγ Q \Kdd+@yiqIiEyYI |=+gͯ7i"Ѭ>qW09U!01eR;u{HnrOlX o%;̮m&F'K@ߖnUr8Q]poz.Tv>ŧ,p||MBՊˇo}H9*A %^C5 iޗ@"')mA(B}6j7ד Tsd iolM*Ҩr@*Pn s%@HEN.F&Lp K#z7(vYСh5G2'}>hH={" :2&[{w^%HEꚲD,Du1:&Zơb#{SLs UVxu(7ڰq 0婱/1$ 04Ky66έ;~6w,6@JM)s,`ρ&""*YPRNQ*):yHf`S~\l֎nkܫ@Vu# 'zd&*j~/dV<2݁JNti$qTbTK@WocM㡋T|R )QV"p8E_Qn IDAT[v6ޯ9d-TJtCFR2snѱ\9M2LcSYHt Kck߼tp}E6^^Kuwp)Bds&RFZt eH_γdd־=^gP1k Lf|e;ؘHjVĄ|s曃O&$F>j0Rd^Eo3}pc!TkNɼ.O @WDc^ADjG+S錠VGlFPYƛ:n{6#38bZ+WwJQ5 (Wt4oҾaT`JYͺ"];Av{xA Baֻ.fЊiK2?<8yqDGs>+PdV뛟dϾ^8?#˴Sࡋ8HjMgJ@U<0T6^ɀH,\23SLՑHO-UOdHlwI#Wma"LCMA #Hlgq2Nl*T]69qlʊv̆LYBBYےx_*}]Mn7BdJ}ı&vj8@ iŜe6k2hN#Cl =~4.~U^r#mla u9w P*nV~?,>VV@cϏ:1}^Zu4LfU+L!-Q >:d#0!h2~Ek?CA#@H1deBZL|dܸSl Gu0o&jC=! @qFr[L C'_^mF_I)qeaiq1_XN@a0 `0߷`0 `p>`0 |i}`0  `0 }afy]]m<mQo7i}Ҿ;:eLկ]?Qn7sRL^Ñت אs+ y>cjkB}ةF8{D?s@x ?.ɧ~$5m]݁DG>drEIu.Iff4IOU3'}W824q5=4Kdh8ݱmCz֬'йJ>Y.qg{]ƶ?93`c%mVYM}]cv]^qS5(_hrhӣS~i0O;*~ 7KP*; bR~Lݽ];'66φ2Vb-*UGd<ٹKkǗw- ׿jn=bWґ_$ <_ꚓ۪OX2u322ڠX}s =4o!к݌ͺnHQծEߑ@s)>ےAPAt ^#=:54kxBTIzQHEj,nT̙w@!ip^'{:)aV5a7#qdXf&Oڜ{y͒}nH# Y20DFIA;G+,2"0<"u2Gцpi(q8!y$ XN}$!/T& T#rJC+\*ɤro'k[^2@ge BLޛ ݲ_DDҳMbD'<Q€ӿ*]!(\Gu?uTT C'5.I`Q(EqJIK7]JuQ0QH0 @G$ { 3%e%N#C͎8d@t|,F$5* v?:W#SсWۿ9|<5H#$Αk<#M3U5t~FxuPX^F{´560$cӡj}uUa Sw4jsJsuv[hvec2RZ(0qm1T|1mko ;;qK/ 0m]|O g%C -JmyDq||f8Azi콗cfz4FMLL1&&XҌޣ&6l qz+\̼͛3[e49Up|9 [iu#;_ZQ5bJ&4%#Z*:d坻T;|h*yƧfs*5E;JB2[MHw;6Gԫ{ʵeUzDǝ`Ə.jAɃ[}}H2l&AbMB}vjOwz!f@;h%9

Hxs:QEARHݔ{Oȱ9=^Aj7"e T5; MrI?_EkO3R HC/6"^u6"PlW{[kuJ!A $"`=+޺]'%.z ]kEG)BN.dws%$8{*(f GԧHWVr-(;ٚdеhn+Xi9 H4j Ӱ mm ZW xZ >P9 םj3Ev%wxW_}79Qw/^-%qQdQA5%y C|B8b=(*RQ]_+k+A}]yL2Դsxaq .<Е{% y~hCh@Z"qgK[Ap˵-ȫdC 1vPxڨOM">Ο~-a>IDZS5pT+'gbagsY}xFX %E.X(GG0ZTZ[84™Ud|ߵ1M.ûzƵ( 3^F@ZKOyM)c;Ӑin"ȜPB튠m'$;5;E.|Kmly&Qw]E! h1s͔+[6߈MvTY~)խ x5˫;b_;|ӎn(]wQIѥy*5-|?D> !9UvQS]~I@H#$< 5;tyZLU]u̩"184<2wzEGc04$%iI[7F@mR32{>oBdo{(OCBW|#*S =QIu" TXʌ3Ls#&nQ`)8}3ownid*>ߍT.$ ~AN>aaYuT_2 5)P!b!-]ݺsd#w :V{& os[WJy9UzUB`˚:` EjvEiɰbg`8.| Z%a'W%VtI7,ʻa#j g Y(+7$rcH9 /j(!Gnn3l@QvQ^> :ʽ3Y‘rK@^avTYA3E5f_=Hp+ng&}+[7bf+߻@y_c]!iܐmm)!eB; N-}D~kȬjSITIaX3=LAYRռ ${ 92>e&uz βG%8")0}%7?ʺ KMT%>vГ)#M (淿߫p19Y*MgԌ= 3VvFG0yvvyǯ\mzzOz>*s[Jjq%={@[#'R׉ȸ!Ql 4~RvqlaɊM l)^u 3oNBdGo g﷞JuIWGjυR^Ke X;ԦhK4^ V~6Lϗ{ʻ%Ϡ:H>%>U1(>ж_]"e2Mz^hR/{P9L+ȷ2A[&' i'%J&&tL\0g+9Aߙ wޗfivp 7{gXuz9M4R'3=Jۜ|%w5G~fWߝRh;7gu@zI&!INU%zZn3vi E0tjg:eLV'MҋѡF;Y$=݀.q nf@zxW\q7.?#.nλKP 1mKW9 y2U푈vBMM2'K*U:FC4J[ު|b.3AJT,>L׎xPz Q)?3oH ^G}3PO5iyλ[V74D!}*R$nfNDGwXS2}MC09#vPFLk]^ޚ MQ{So>gOY*O}s^tso"C@ДQ}\*y;XZ]|u)ߥܜF\cDxz|$O]7|+SIxMIc" ׁ֨ܽ.lY*a*c&ާȏ,_s!Qmy{W/[Q(I j^ͷW696@;W}ZsCH gbL+6q)":Ӳ@x$ 3 M;Y&(;;a E,6Pᕐ`{6U^P?8T`e;(Jy{^PM!ˎ*(.ҬK.|wC+Uk1/IÅoož{U9Wo cIWAB121B$iC3FzN@NmkՏv|ٛc˵c^"&%`@GoFz~uVv n ϔ1QMh#ھdv٫F^} ~+;ےU#1&8h@; k.Oҷ́Ƌ7}-L*_)Oi(^74Ehfpf]Zث+JR}@VMo"߱6J as>]#_TRǘn(-jԖrU>=;5Qy_A@Z." Qv{Į9SM턨!aÌBN)eȴjn"aJ0E2]~#b=2Oٴ_LQǐXy Eژ`x!I}S*WikO?u<0.B@)+>&Ȼ5(;?4o]`'U >FOXg18$YWeɾy*IpDM+d&@S҈k9p^C+Bkxà ƛ&{#Cjqvq[#^IݮbҧfI9@S{JTsPuC@{ДJ@U%0Į }'UNhe7NoY}@gev)S \*<5kHx':g\Pl!,NiHjMEH("Hcj.F;NdcdsfZ3N=Pϴ}2m0,B2R&=C,6:<)1ͬm{ON^Bfl^$3iC$خ(d#C>ai 8zBZV|d*HW"۶μf=my i@^{i>֎/e8CoM4%fEќ*IK; r^-(pqr +pcF)cʦ+^AiU]X++.?vHclUdmV;`LI, ^~v8*+L*78Y%)T[Apj2QvêQ1Q04::h{SjyNrfk;ٯ ƈms0x O >},TE5 a*-sCLv+J>Ϧ679@up歹x^o=; n24~Z_zֽw/^[ΝnfQ*ʔdn(im:t8ЍeX8,q˃@ZdG^T+{hZ;ZXX9QP%d*K652RLdfS6/#+d0mss|sm_>TIIvK9Ĵ=; it 4O,Ÿ-E/l?TS5c!ҔnԈ83glP澜6K;̝p3:mgz JS"Rwm7/H/zpZ=Q ڑ#hr-|}@zK=b/ɰĴn>/N`HԱP;A^Rb A ڳ\y^ũ`r/! Oū^PgŨ ܭH`qs3W&5w[`0 `0^!=i`0 <}`0 y `0 9 `0 ap:\qB/h6ٗ3m^YE H_`{g?;qXaƕ@8&>h zO+ԡcz,.2O`X6{ &T {>1{q} EavZg= w;_ͱSz^jt%wz"?|3Ke0#cEX3٩:a9YeΘ3(l rƱin|$7IA\"y1?\rPMJOBºaܫu ]gO^>.a q~is3nLzg\څyI{Jæ\8pK~&ŧ|ZL|jX˱EJS%v-J=d̅s" :QȨc$̾"]mhZ!7cNh?4bzt7LAj5t"#aSugڮYzѽڑ0Iz.%Z+6WT3E]hNm=&(:Mzgk{]I~3e]Pi9ľ!;LxFG}03pI+}U_V4%:BPGC)}osV>LIJA;Rt|zW-U^X\Qn+:tD:(xoŅVTM ҶJ V'G0 [hNKFϔzo[~pk")GϜ4 TeݿLJ*{FqRe|o) J;P:?ZMWr16tm˟Cd iё?%ɹrķgIߊpVrCRhw\g"TMciadZW /cгNtT4!9ȰK^oSxxպWDC  1á=ޙL55]hKKYKߴd"}2{E8Cc{`~tn9W{fN ?pL}VM(/#6lM!BQgGZf²><i3D2۰FHps; `Mn7C~Ru.&%/ٟ aCc83s:y&  w8+҄be(XS^BuIVSY/L{Ӥc?=/Yxd/ꛮڞheOǨN\"@ki5#ъ݋uY{ 5pȨH]g{IfRQlH&'*ǼChgGcd3\|xzFL}G1NioDWF8{ ,8 &&ǸԘ`탆Θ=5/4рpp=l]]NfIov$;ū镓vtYz}*oHi K-h= W=Ei[3Fp!W@U۱z_Sq}1 5E|v߭lա䐀) P2Q A&Uqo>Ӱ&t5ڕK3U u@"-oݭXQ%Ң jNfc ȚHB 娨SBwKismRCbڠh3ĨW8@NC#ԁj XFM:ҍG,LI@"u+.] ǽ"nmoo9!4@ώ6'{eoK$u$ Qac(NLmoFz~"j"{vf:["LCl\tc;hD8}J[ sg=N &t0΋M\>Zxս?]+p}S[/yo[elAiGCOط ʗ%ZGǵ/u|!\2enM)J6h!!;XCH@栦"3t&/*!$!3㆟Qf{uGUZM뮩Dj O O}yr=B\GlԜ"HxY!Z&* `gk%Pļn@D1!3]+bh2Z=q7H6 2BM:gF7^Ej,D*{35f.#FnrB' 9-Kt0CM_4xdTqhމQ SR/"y iڷQ⤧1N dz+-v  9fK&'$.ٴEb!Gpp;g:PvQſ9z3q_zFs#@[ŇJ49#­*[wŴ2wRyj(B) A@i$ij4E$"h+l {aO<;#1gFj{M:"I&`Y0(9R58JibIB?AhF5gmAY4PdadK ߉o3"5h&iE]>n$Z^"CkK̏YPy;n8G;TBe:!I5F!tegqYeFD9%<1`nFBzݶyGկvݝ"<_'Gep ڄykNVЀ$ ASZzFՕs&|D}YY Ti^Gp+co}4?2*C4DBv6i+bI'Og㖋EBB痸 -PUq{̠hE4}sьԉuK@@yMКHQ׽t.BTH9 Xqgy=63SԜ<,7Ssmiݘu`ӓCmt V|+.8@3TG_"Y;l. EmkMMVw^sj֚Ssw弽 aӢVڻ [$Fr++e^{j'jչz;eUwFϵouq^jock͙RieٿoxZ˲@q=N(e_ЎLe[Z?;$G.*y3UT⟎_mE,&F-zD~Z pvrQiT0w:ϜR'X}=-4Z2lh*= 4 r VGYfo Mj+93ޜ5*0`nn1{Oؒjȑ*oVXAEhKDB̤N4[C:c|/aF"/h18mJkTM3; 9pL á=z7GyI՟#݊TH#:{#t7F#]o?>5QYݻv_mߧ={sBF%%oan_ . -cztL)Q._EE.TqB͏@'-u1*Ư߿sH:~%SR=4ӬA)2!gnxe=d̑mz z2 7T:6]RH.KҒVg(he zLσ>K;!F X& 9%G΅MY; sp`̛B ;"m[ǫx.b0Iы9RBR^h+iBa0 `0Gc/cY'c0 `0~>`0 5nN #,bsV(dwyd:v}0SpFql9jĐpHf":`ImcRwvK:nb0#[9{#MB_ c"'zs%%z3&Aܛٖ%E$F96|N N\;52f;˖2%ȷ6n|q|߯4͘T%V7Nv&b`_N]$s'wCҳtAҲj )*2uD47:#{Q'Ɍ`'`I݄/nk{?|J3, ӽ@+ܦ6KO I;0Oj^bMgnxγ w^N[iIN<pk:[$|J l!ٍIq% yTŴEdJ9: 7k MS{=MļL'B^˥/Z3/!훕q;<,K͗{YsgGXKrZM?~$sg_=EK֥t4Rf*积d6 VщZ=-H1y iܡQuۮO aI{?_xƛd>4AJ; 5<>d>jb0E&ӠTƐ?ԥ!i >p' `6!D|<n`H7plumҷbyǞ k ޼/t]>^_7 o_+sfZߚY{L|,0y i'*mR$ {NG 'bβ_(^]0tRqbz5ه"SM h?ל}'pl{#>qD+! /u{nˏ!)X{Ndϐ Z?/~`{ṛe1 m@g6SZBAq}n-Kar#Re)i @@"# T*B._d;Zƛd,KlklJSg^:EJk npƯb;Sn1ڐ~#g%5^+AbmBǍq{ ;856X[:g7va*N{;i {)S)3G_U %CtD)J =+>w{grYFSM*BP tsȱ2ɩy]nb-TeD`4SIøh'kvEufer ,;ˤAJ{c. 8s7TP%>u#p`0E@: ۓ_l\h]te!*KKg+aMg"vȺљc֘0Ey Y9S*@~3uѢE?[Q&@ۈLI+V:x s&UuENýUumuqTWu /8)B(j@^%K[&'ٿ2}by*2UwS5 pu>V;;; +pDsvvICII wָkQWIAS^ژEe +tpKsY=_y" M)lJyuQYE [ nB>Ϻ)79YL2 @+1*qB96TYvt+rS@3 ZȀ) rqaZ$S p2[!e<nu`c^Ֆ8"e9H2T:jk(# wb%TSw'V q]Sohp0`0 i)Шe!_A^Ro߲;:+wR1R"bN^Xnz43oS[锰[|d!`0Ο |.IKZU_ 1GaEc/cipcL]sLJnyIna'X87رp!eIv}a0O 86 -`0 x42u8`0 s0 `0`0 `0 `0 `08o{!-m/˾xb1 mYS8Y){])SA:<6'*kUR {29/byDW5@?z0uڠjM}@_ [db|Bz^ђNgdsÁ O.~ovʝIL{xB_]7y!wݚڄ>dqnvљ OSi !R+u5t3MJx2TT0ezezՁf5 6HjvE,֗;!.12rM*Cg7 Xn3t~f<ȸDm8bi=3]ve$Om-g % ,oOpٷ;,ZRm= NE|6ZO&!b#޳rw4芵sɓYV#1[@~-'7__zC)6. o.AXV@[yɂi`h|>SUڻ56Nu,ǔE]տ>Z4Ț_:)EOZu]kTa2aib'h1D@T5 9L˘2ƣʈ%=zYjnʼ_VK;m8@g_r:ȇےk@y7ȭkW1Z4B9vpp{i|qm&84ՁԴ! .{Meh2.ڞXinL ,(9LBӗC,)LY#`ϡ׳9~^V,.*kgoA= mOꕱ{f#}=nʭ z>NDN/b޿[2fSa@Gҩׂ)=%][wX K0EhSnkKuʶ,r:$y܌p|b~PywJ8yh^<Bw/%i:RL& i${UW}-i'!J&&b[.Wsǿ4x4da2l؄ypg3ߛ*lx9HD4o=$iu?voT=2!pMhUC$SnU#»zW3Nґ CT#!֑.T5 ]bzR$YlM r?׳Rq-=l)Lmػ\U9b8SWc//$CTu#mu&Y> CB/NFz^Y:pkMҫvp5%BuNw*Zyl8UtyEZjIWںFѧ|{)\`Mh`5x`s+0NsMiaG.DUQ\`ww) Xy|af-Ad|x%]wҽIpytꅈBX?0ʴ.-Q()XYmexhdssbQvy)Fx&H&mYbٲK._x˹}ͪΣSL }Y$ȗݸW٣ 29g~L `[|!.v"LdM+1IcNQ2[1Sg՜տj97k: %{ښZeK?(?}ZsCH gbLk\X(2gET5m㙒6LC̜.4,Gu0 Ƅ"rdd(pcsf~;:Ep!}Z{g1tsKdbRr˖X%-uW}jOI˖.]tkHd0]. .T83#@lGJZO= 'K8HG$'Z 3+J;i: AÍûΫ}uCe%? WwtϩmsShK1ot!^a>&'U/>Ҋ44ES=%땸CJ~Q-$}~.TɹW'Oxi6Һۧ]kJ&2ɪwm''h{0vѢIE ib&*w6! Qv{Į9SM턨!aÌBN)eϐi ) h!t}AMdąe'S1d0V^B,fr6&Gn@lD?/CAq)|uQ ~wHw#( ]:9TuQ IKu:DM`o1؉Z 87f##fߺl٧8 Νic(F5 `;{}\@RG3^s6*-s 61ɿ=XN09>"g،c콫zOה⨽o mGrP# MS^M -a21Xh;):MV-+!C4ʹi&4on.MAH0Į )P?(E!tK]/ȱ4{~[6iڕnL-hj=kz9i:Di71α1)Fpb a)vNC*D+BBQ3jd'VsVڑv"{#3D8}LiC&"~@~aLME{X"Yl?$6f"+5G$t9c;Z_\=CAx'O} %G lzH5{[iC$}QK#MH'wj n=Fs&٦ʇ)#)&g kDE4^Li% Hg=#ꄝzMQZ * ]k:M\2>q"wW'i%ٝb^Vor 3yIGeҬlϏs6{<ﺋPh;!QCCQ,fri{"c=s/x=p; L:$T#d5F{Dƺ#S<Lvj8Y{lr[ ~:WP8tH+{CTEǹSCQgd_Mh\rt |_GG玎ژ 4٧έ?t[Ls`)vZ4D P4:`0 ʝ./jh}mFƦ[o6e %Th–]uDuvl;[LM{kED '/|<κO˟ u9 `0= ɡWpgXLUc`&bqHUWxYRX W,L npv*,hku IDATcoUFЙnTUp (@U £{ِ\ǐ@V_W[Tu6o|׍['Z+n|qN\aĈ?SDή@s5>D7h;i_U*᱑Ar؜ԧ|Ϯ `g#n#~ou s"H< sNxiwm?{{|3ϻ;kcxƓ4Y(g$*I1g 2HHl1Suo[ꪮпl]Bgo_WC톮  SCcJ%yavUO N g}1boBa~6.! $ _ !=9 !+w^}\wp×ϻ9ybYؔz^o2iw/^wds>^olZCZ6(8Bmר/:%}N4%9UWmB40э4&`ӓnk3Zn_vGjiQˈbXXX[wlSR+tAY6t݋@.  VhJwБ0f\'jKެ7bHKWW1طxRiϵw4pS?^ʷZz}U!O[#ܵ|j׆nXb9lyrSMz.QF_rء.ͨ(`yl(H)[R^DvIO0.jB(7qaqCR0,$toޭ7/! 25"~GоN陂e"n:7 $+Zx_vrȥ֫Go#M1$FRb<<&m݃Ui$MH64Y uLZzKFԷ6sIA99ڛcDn<`ih3 B aN4}oTH@P`R0V :tIņ 6?BAήA7o^ulp<:U*DEAyZߍF/p:cOdTy$`Y66ӛ"V'共Rlll2I"ڽ.pBX]qKVEɤ9۾78U2|yVL[@cɦUgJ'goyxKܰ  g0G;FkK bXIh :8)YVP6pzSohDϑ* M|/ rFthm㯛TQj>9D2s$w=vŖѻw0F'.㢜uP2$|  2c0[^aiW-VX\Rbf6q1Y;AAE |SbFS85pb0J#  Tasv3ngmdxXjg:1@XAA |p      y     oJ8IzK}#@so__f׵NtĬ|łw߻\}UAܾܰWTNCs{|F͓v*$lϦ\քN[Uwa/!&}kLpQXN6W+q+pD)ufAA _k`M"R3LPPMoϾڞnbJR qc0־ X7<޲_y\іX?CZ%+q?ynͲ$LEco>׷~\ OHbAzhdD:#ᲦfDE$U_4uG*`Vi"͙ y )\è(p>utyeFy,wIn"xi*/r d4$8n&J D.\߫KJ[z?i,|Gs.0ڜf%vu۟C^*멑ǫcL|$tt&aŮlY8C)C_ GWe7o_c)z}Sۊ~hm19TAk<:m];7ۚ-~c!ޕIW~ /$]C޽Y}D@'>r/Lo̎:߇OA'D1{RbRqȣPj'lRnKB] ȼx7g& >97!qQ0:E!dNݣ-_)em~rD  `i;o&|]sxpzSX\uBPLQZ+њwgIudWF(͋aufcOar)KmVy\fq/g,rʇX:IcanM;$ sAA|Hss"F ;&6Dbda1YI! 0] 62<,Y3 5AAAAAA   y     `%+׶&$Q%g_}p]Mʥٱ Y][TNt,uw]t:XOZnvSI:g6)nNj3ݝUs -*=|cvzfn204}9:'ƫm7ɷld&mӫbn/mm oW:?N `nuMA]eAb HzlmᮊHP&.U\7k ^d|$S8&k/tY;=` w.`F_lu9O3emݯO?{eo;zAg,'gOڻHZ ;_ !չЭN<3'ΧG;2]f80j# illRPGaY+} Rco_z$+Ts~(/D釤h8N̵k=2@9[߹4P`CR`Sr~46uyEvj3?|Dcq對B{C W܎RƧ1n5R·$?6adEO.M8K&TҸ9p=^B#|zchRCn:kYxryMM!IF.{2H^ͥ={@9ES: Q08݅O:9lushlm}[JrC@d2{A)4=߯J}H@}x7! "{>'QOlg"YwVmeC;Kr%>#uT{7g붝U'$׸bՓnw3 d46&:^&)b$Z JjhH oLҖ>uLIU[A{r]P&+“ק˄XsZg*RC.n IvMO!u.]ӫ,Z.zw],Tcs0'nnў6uʐ@d1{LS7R !O> wy&,k 戀^as.eF.?1~K,e2/j[GDm로ERN/>w)U?:e=ԥKBڷd7EHDԾwc/\-o?/݃N8'n^^3:&eFDdq݅/X\Rbf6q1q>'aůIK.'{CDrԯUx綜Syo e}>`wItNyNĨzL$ wX U-/!_%aIw:͒um9H@~&V7 ,7M KmVL'`   ̼F,   y     y   V"lkB]/nmkqgm jVXVY9 IDAT Lt/ n aY5ھ"'URPev%'q6;6UҺ{yc6k f/; -} kCapɦozk2Gs:IY}1 "4(15<_3LMFս]$8 UҠvt=e)02}}&p4nze|+ͦoxjsvϙbo?&(&5NtY*$涱oܘ9A,{݄NwWﺚKDUXvk::]|4ź͹zSf"J 2`s_^#י NoiW:I^eуCo_1s(͵>9>wEt}pҭĖ/2c]f+@GfYRGwd9m+}lbޏ%,>g"86I2χ:$,2fBr@v?Wbw\;ux`|Lpڏ> ѥ; u6PJy_ݾ <; g(c+K-W51%Cmgs: Z89RN|p0t^;}j]!z4I䲞 _vA8V>+3h-a/>Y۾>ܺZYhZ[QDbKkyCqx$1 sj׶5Y|#xzCS( $sh.9~_^\fg![AAf'J1^S `;؇V ?`޶:#RkA#l|VCqn2%M!`7}'?m1_X;Ih<.*)CՓu$6cۗ$JӪ.)-P}Bn%0,ԣ,N}Eş}éw#ڦmY\sʩovgAS<-_?zaE}w&&9t:oĦ$n_i3XS9r?);z=rsc2!C 2L<,-gtjZmQuxT'Q/x*^ү[O߹t+SNC.:EJI%_}zKYćB3?>yC)7!ҬyQRp-[65g6q ǍA C N_}cyjDTWܰW6s;O dD" K";.t &d&it--z5qH|@%=uyw7}#t㘬*FA9i81ICSe.s[?NzqiQٯMr}Fчڔ>|HaBm(B:&Lf0ѩ[,Ǿ=|2љisuxO$H^ P 0nTNlwnKuP[ĝaB]}ul!pe )?q㯄S׵[-x<:B-MV9T :ĝwN޼ע]G^ЌK,!W>Os" vm!o E$:<'bTpc"B3 <AAߌ}t6q KmVL'o AAAc.s!  Wp   B9  ,4p   BH&\ۚpDFLHօ7*-Q<&3;V.:t4Sg6!J&qe2_Xn $x;Mcliy-]wDL޽_F7gr#zزX}i[uil*Io| \YK*x-0|5=ps9[ c,?9@IXRֲ`7$ڽG;٘dr05 O؄h:`W>;VIcK֮Ή 2Ok_^F%ȇ}{Du* rw~~HQFiGBzI¿3j/W^**# GB3W_YPi7{k5/%C8 m3T5 P Fq|DGU{FF;c^7,i>Ù`]IeVi7,tGF`:7k |*!y}&Ӵh,;ʢ#:>1&2|rlmlΉ>QV3FЊǔt[ŜҊ _KMK߼۬:CES>8HYzc|S(;q{H3W߷u=6m$o翺s[VG\UD{DB ׯO숊I]sK`@&$wlDA!HYzAΥz1N)%s}B᳭&F_@eBϙawI"80qEc͂RʸāEy"q}sKŃN쮆(t=AAؘ&9??t)hl3|5]٣ǎi RKJ]Sfv;[o?W]!ଂ$՝麦3aA2 'kC@脤Tc딛ijqFĢ *5QeҠlO7)nNu eg fjȾ ф,"0#L]uxDA[w?{ݪ~OәzC%IH`&II IxTDTg4qcgVov!! ZBL9Joxawӏ2vǎ4s#ٽwUGo|=ꍣ ؤ7VPfPq7\$}ӛ3w,",J4\p`Ɠ"6}PjԎXo<{yg rSoSw )&yPjqlWIEG.9cm)VXCAل+?؊Rpޞk~ f>y?ybgzp!9cXB{;Ǻbp+KYjZl&S<2BƖZ}\h>e̛VA*|w")ωU6\8>8vu Y}{6 r G KmVL'`   l'AAAdy     y   V"rmkBzgl|m˫+ ңA׫ )&]Ael쳦 eZr_ j| n+I7uG\y3/,StvIzggj;Ó6u6 7Lܾ<}sLR͓\s۴I]̺92~sw!A(#n%ݝ1gA<$:ݹ.,B3[$9'A_!/Vڦ6, fM̌+m]:ѥ,r?DtlWce/̃*;){.܉//k9il5YR]ˆVXo袊rC珜cxu {vGaOYevCwwg.om㕶\Uۜ:G5]cٌoϓ<@/(ek'pކՑgPmz[('OaF(u 2tu4!JMk0itNAd1CrF3;DH;|qِQ> LDxYuLmt EV0ۯvs9D]F4'{ܠnQLvfCE/l+l4k(U-oH?^l~M4 EgDWP9ݴIohԢ;r1+α+ $0)ݵ}IZI:A-8 ┧8eX)/Jsyj\2`؟^jmg+"!k&N}yO9=tR"NY+W'urF~ !^|AP9ݴIn[KV1 Ȣ&(vyʭ&:ie=-x;!8f( &s]aݜyi07dϡۢvnۭ6cnj.]"8}th:% ck/cJL]44鑫weVd&w1, @zp['6i9_|N8uӭA+@5 WGuRۨjFVYu!1cǔ/4ZGawx뼒BÀ|i|MZ$W渊A/LB^jxOSHRrnvG۽O9}HDQTU^5xF.1n<iuER9sIr^H=HӋ 6DDH p.VN$>Grnj7yN:8&Ď)б-ndhE"r #.w<=ͩ_]urʹ 萉<(BLRBx6)I1鮎g*"!5ϽfAUB\Mɛ(:P0 sC  .9U h!q+/B''uX2)64s\fS7g|N|(Ifh1_dvu3GkLT5 (UrHnw(dP*_Ebb %'9$f,qeQ3ڽX{gn4C$hքǗیϙ]&Ue> m^ Hfwӯ"I޹qjڨ,2$+̣R}So~ƍ[/dj/Et*MxnIj3N7EֻN6.(I!q/'o"6t9 f2h=ԗ$E_ {̿}vw'zt&9S& 恐8qQ$ڱdvss!]w=S7O<8"y}\e =[ak ]c`8uMlkaRѱ.]kK^ JQ x_ m}o95z0.dY$yfQN59nυp&݈1YL}wΝmܿģ/Myw:ߵoZ+7 ݖsg֭|`F/k|!B.!4VI*vx9YD~{Ӑ o\ ntMIQ?Hs[}GY uxu@s&MY/ꦮ HX鮝Q?;$r` IDATA"oN a@s[ L֌*^+KYjZl&S<23>P}/R AYm|~im")ωU6\L$8AAA7ctq KmVL'o AAAc.3!  .8AAAdAAA8AAAd!|9_}BHHZժ;Qcwf׾uF:k:_hwt6nț"ˎƯvsik/T}yid k~V_C컦{"ޙW $3JLXΚ'8w}ovc:~> MZx H2p֑QN su}j!fy9;;T;烈<}{#Zq] }vEUOki}%ZQu9r}9u" 7 Ζys 9܋NadJWy;߻_r<}w&h9켏KSx)__NJy}dW_&ӨL\̡K_u(xҜ#w ^ćN|&GBruzm*J<v L!奓:En18б9OVF鍓&.<~mEڍcF6hs7 $bs_MrpπDs8bil3GEZ)7uW, %}Cw}VNMUE\S` ˣ!"2]<6uz&?_قb~b;)H4 ڃa m'nF'Q؍]ءǏ|1nhL2!3!S>"Bo u.`ސȊ]$@Y@]bUyFLqykHXYJ}k/R8h NWDο(Qfώw^:q x$6`Glz**صށIy|k~.oֽwG Җ8s9$|[&aY97 _vGٝxu"%#P{!w!IOrqdiЍ)F:U83R{$z{$$D&Ę.Nry%9Y3ջ䭎a翺 %3Ί:cb>&]ݲ,l|LOc?X ?"?,tH#2SV3D4,"3?-8 @!rDn`!ك5TֆgYIhJv2UTX' 9} 6EJ[7>H98v&J.2Cͭbĩk p]U".|{]R=u#8#%6tAZ5$Txo&%6>9o`Kה2?}Ϟ={5k~e&61 A|ٙ\ ,K x$>`8}C C#U% QQo\ގaw)O>|Pa^JI"sW,^xhlR]3橗j\w$kͅw?U @G:|;ytc׫o Xʲpb0L ܧN)Oډ,77dƮqa&ؙhۨ:j٬ |c q$:W Z5$Tx8]o2)ΤHN9,uHb -MV_wg@Vyy,|*vu Sc:ھO=#PkZx+;^u4y;.nxDns~BÜUU$&6'p ]#׉z0x;@')S_;Z9"Px୸q&OgvD^$g>:l\e*Λy!IoCu[ ӳ:BqJ>X`9dz?oȁ&CDU C>QRYy7t I\]yI|:@BB_t`!s5o3 tN;Vq!dM{":BUO4qPC:Wx8M2(Q Mi5X97n~rN!%-'o@}w9Wu!# 4LXCaN]<2{t/Iq>'.;.R*#^ Rg0tR`b"Ӟ⡔Rcz'KZ>>C͕QOR\@YR{߹;!Or}G*;'=ךJnQ>6W~ӊ<ՑƻRxo[xAy]hGON\ " ;GN58pXXmf”,9'zYD><׉>IG}D]k^5ouBgv!ވ7 Ցzw:<4:.`J;+|Lcb^0 aA);4Q`GFubڡ. #_ n?{>'i+Lxea[^ s].Lg5fx?gϞ=en)?c\Ǝo)j2kKcREvf`6Qp&H3GZZz.2(ipJztކuf$4oO?4Q:ƕ+{\(g(PJ[&,2=_yC助b:dp= ޱ=r4-]gN|+sDѾ ˌ*Z<[q~ο:/)ou4YnFóHuA0z>&11zcoPY)qQ$lۺgf64!Sx3ᦀa}WKV.m0|7w3Jɛ]N^]KS- AlK"L]Z<Ց'뚯Nzly;J3r81䢓 #~\Z{ LP gSլlx. |w.lҝO绬0Y St~|م/׺@vQ+bw>X8ڎu+~n-G: ̉ﵸ.wK>;U!ytxhjcIqeL)ރG.w/,P"G.m%i {ފT*73=1n?FFlܞQOuUu9IUD{Ј ϗ2|M4d&x60‘CI*uԇ0A#D`_uk/LLJ:'gsf"T=SR6^ߟ/[Ro}mʟ=u41q&Luh oT$P=<%m-C945nD+O{T B$t$!;B}SO&998󕴵̛y*;I( B J#@@ىw)~ϡP( BP(L^F,BP( BΡP( BP(g ΡP( BP(g ΡP( BP(g Pwm񧭭#u7> _]~SP~巟]hk+ׂkH\(knC{F_NҺ/'sa?uּ5UZu׾ 7QSxastWDW~+YXtdގvB^Lɥ__V,/y=7=sv}rhZ2BP(yWVt+%%^ ح^߱@KNk)U7{MJ{ zTYkPe˙53Y^O2y4SrKb^mѵS@޷uyn;<%&0}/OOZ#kk/7OIIW?Dk&7=) B$ʮt!uͿi?mwfM}\/nAEKӍFɶG-f5[<>Љ]89FߝO ︖{ hÍARsp=sꍘHWrn-;ʫM}cfkƆߵɝIu%%/ bu-ɚ0`DCvK<^kO%er/uQT% _pRN"2d?玠ڢ (k7wN,5^hE vaOٕ_'jd{僧I dU z̓/OvK_S/ o̟W~s^ +t$HV~X ^ yYZM'_"//r'߽u}d䪝_|!;W3#pԗ- 03=]wUs/<0z=_p*/Lm3kmhUk[&Lkoͻ5`z{/7LzzjR)CP(ʙɋJoti_cJ]?seٗ`m,ӯyCgGrl&[~">856xv;b\x* =q\1J1K"1.De+]Ͷ̐~I]: @R",~Ox]]pVcb&R}[oܟ<7! Y&s3@Jܿ"̦T[n?`#~o&<LP(ل)]GD'5 ' x"SaL ޽fMx>f){t(Χ.p$\|2|҅^sD8dDzw2䵙w tTvwR2?wEn>]k7Vp^0]y]a|ZNM! `Κw틾%ι5$od T ̿_'A&Qbh3ŞP"y 7θ_/j)ǷN (o-'-J*@ew. BP }Q7Y{+blds@f'Z* Ȏ3%)t 鋫(q@R4?ԎrAwo}۟ ȤP( B9kDRGvBx󔴵 ϡ7WoL BP(3PWQ oN-ݞ}ΡP( BPN?8 Dq':BP( B9DKcבxJȟ ( BP( h BP( r֠ BP( r֠ BP( r྇)*6f_ګdw3+ptޛt6R7nOxOf 1$^y%<6JTހdo,9ZzzB\8q̥z]{pXK>];=i~?LGm%MrB(ΐ,"&fh_|ˑ3/+81?1{MawV au~P,n ҮnjQ)o.ضTn|KD0ߴ]QV&D")=Qz|405U u۵'m*5?*75OLأY_̧VS}pOcaEriZn]qāATqJO3 IDATw@i.ʅL[[YqKOGA|C!){vh=Ţ_̓k'h Oյ',E|sǽi4YyjŎެghyh#%^{)*aTaQfY.Y|zgil9:Z %|mʙbwgweIyޛ8%GbNFS '/r8d˹o#݃ 2p 6x "txS:t7Oqb)Tzݾ}GRk[}PluիE cX^u5+>%r0w\Z% 嵥2`Y@+Iʛӳ4/.DDxkbo&qUS0(H3 Ĝoڲw1!koKFAEou骴X8sfV3~a홵+R>ٴBBνa@ڦï/˿0抽=_o.t8lOROS>)pN*T1KO_֔mB.x猻 ́o~ !&w,~C'RWVI֗,ʼneO+9eZ #W!Y$,tb18r\9!T7=m RH$9'Szur}z)-dϹo#}ړ庌.hvԇY;al G@n/{RRt<84Qc' 1h+LuIR{=F-bR vzm#xC:P"[^|Rg2b@DxFi$35*V^Re~}w$#%Q&ֽ5cW-*%ryVc"直=@ZVw, 퍼cyΙ{ MzN8F& )gCjE9WVqNKyص%j5ʦ|b}`s20)P^Y|ĩ?!ڞr.ݢnډN/\,ݯ-ʡ8WKe}C%ңI^ ]u%^y`8E)Ł--Կ8k4^H>Q{5e&`{֜ kR8']wKqD")9YsW7=K,ʼnDs̬#b܁Sfz8\9pGͻz@ġAҷԎڃL>;wg/y$!@ym{G72X͙ k%\wO{TxSNd]簛@lpk~Vp<< *=ƺ` _1LAq}.EUc|}Gyt)F:u:|%'or8S@[^_\Ѧ^y2g<=]kԫJ_|p~$ d0aB83p,g>mNYgF[S-XoHb  =[QXKymSY]&E[Cwl3}wgia㥫=[CcrmOKDl מYkc)\]3eNX;~zE> KR%n'uο6EqhsyK@6QfW&CyN033E-jC|"(Os̄N.zVqO4\GxlQ&FWNG3 I3R"_n0eNC!ZydӇITI0*u}nV+紜of9s 9e.)g웾ۻ\8E:_~7|cu()'L`ǩ4 "R'p{}8-PRr;l"4V{\TW5eBTs49YQLUe}∔GNC>bp55ȶ62MYc.S]X e]?X&=5L$מK r*Ntb29hu,&i!).r,MҋsZD.sd.SQ^cHm 2Km;LbkΪʓA \˭8 dߡ'KE_ָ@ GJq 8\G}Ίt8X$y6R5s\h z:]L͋7i2.!B˸}xLȶkcMTv;hֳp{KUo{IYfRN2۔?~79$[d[~-^+D1Eé9Y. 9?#)5G]CA\#'rJ)1dʰlg|p1EۛsýKA^P6g_!v&-)%Fp$8j'pnvd0SuS9e^J3;s~qmwV,˫ fSRd(8_'$eЩrN43(qixw S+h9< ,A*3Iu :\G}Ίt8BؿY n yy ~;)DR4?:zR)h~]=c~b)|'7GMqB9&NěI B 8Ǜe:r j{/s@P(&( hcȎPS oN-<|%m-X %NDIBP(7:LP NO {BP( B9exܷFP( BP( ΡP( BP(g ΡP( BP(g ΡP( BP(g Pwm񧭭̔~ޥ xHVunƲ#gؐtyپVzֺjުl{2Nʪ+jˊߏy[_:RI+{>葯:fm}J}%D 6mO*,o=¤!ڭepT ,1ر\1|Zyb:F>AZ._X[^:,`ǃc"(*{Hlf(G3KN>F뷚:+Z e*p}ݢ\@r}.SO}I ܎,Mowwח+.c'wރҪ6g9ikcWn:tswh+ )9m%MZf{#Uuw/yt HYY%3Z#3WC6ÃYJ^n[ǒOW=n=i=A O"Uݥ*O9AHz!kJ*-O,~7Ǝ\⒯*;_̿ӿOW]!M|hr_j o%1V3[ ID}x:kϷbNf-{40b9K͙)K%2F~IS)Ӣ? ]Vs?L*{;Jr;SyQW7U\->G۞ 1퓥mw_?O'I;ky|>2kΞO|ǐ"b&ss=K e}bE0٭!>\$%F%ʡ1$yє' {-[7@gy,3j`<$6X}fou|‘s?' 3$֡{/FU"=V/Te52)E^wnY><.yIe G$H)/}ɻz{,ʘ$΂xONH(("+F\5C` M@^]U(N\DZڔu{{N Y2˵PECnꪞZB}]j|κsP`uԬR"7i/&.`~X=I9WVM- Zzi Sfʝ"H֧1 d{^Yɟ*uYxf+i29T)Gz#Y$\_ovjOi(OM_ON*εt{m?yE)oAN#:Jw99KUsj$(c_@[p2ĉ+=H8F#f؞Lι*p[DzٴOf( XS՞Et#5$DM+m'Dbֹh F xU˃Cʞw>6p[wu7(OF9}GbDxI$^WƗkoڎ>YO-3'aCԓYϩ)Q"n"WGMЇ)yukIlʞI%-Jʣ) ~f"D!voGv]\簖WU|NheJw(h*ت7 x IY/erW&BCG_{J왌=Ӣw vN '%2@=BN*.1+RH[<G4:!&b_j\gO{2!.בr&{)b%6>lyQae$62|SShu}fVDԓ8:j|pEZR9vG) ./'#춻ՕublW2V}efKyH2s',ߕI.uUCB(;4?㞪>`v dTH8|Z8Ң2ϭE$bnGC{pAYRUHTZ\:use$620%:y9ňgl2ڊ"iJ1:Kf{fX< yե?EЎ:T$P=<%m-s:dGE ?($#7}qZtZP )h#utu\}㿇` >1J>~eGE{ <QxImzhG NO o5 BP( ryQ( BP(7ΡP( BP(g ΡP( BP(g ΡP( BP(g Pwm񧭭HYVxsձds{ڛjJ9lN˻/50M&=hmk YTotA#iŷk"=E&΅&CԼ<*svvSvGSz 4ݸPX}绝x BdIKmolxǵ $+:n/)QLƿ IDAT3I7̾sDFSmA"Vttݻ߿{\c{C9\)YVG9iNZ>[>kgkdo+( B?]fNѤ̎}:4\]9󐜔?ư0F]Ӡ9 i0ֆ٤#wҦ1ځez9HQ|RZzm g2!^@SVQ] [}|KofZFb}+Em%(T7hdB{f>0ʪs74Ҕ٪VK' *hށ_=`REyL+n XL\==Tojo,A06:^IE5R1v׮DdDץ$;T) B9XO}3G.;Rfh߻PNoƙ{vRU;Ba-_sJĉ.yHNn ɾΑ4mO"P]qTE%Mj'/ɑ~hjGTQj2c{o{!TBP(/tvG`c3r٠uzpC0H.m(lqNtCNXl֘:yJIEUyKRbD$7~[=ZQ2`u 5GT'GQYuyijS\NJD ;ֹQеbf &GEط84 %30MO.,oÑe/7)bQ4\w-ۃpE)frtWE B8{#WػhNqG#p$#|>ɂf.yȉ*=IJ|sO˞ܖplV.dlke.l9loɟ9@鐈@q^2Im +;[z_)a3N{vs PjBP(yNv\ 藁u[sS m OIҦGifƐ H*&C{GG/BIk.]R=uele&_MP(mx4sZV~DRGvBW0GbygO̓d-P( %A؆Swu"ui›CK{/#@9 BP(&hгq7 NO tBP( B9ec BP( A9 BP( A9 BP( A9 BP( ! \m4uґVeHCǭMu [kR,Ht] d쀨|[]KMYAzֿ \.6_-ۂ=QW"[}O&:n6͞L#]*yFOzl$RU%Ԓ:I:;Z&GɌ֛o3{O|w{Xu[!|pQr|&m>'YBPH iI&Ɔ7>-,jrSkmPw|3֐X^WvۑIlH}fe^iJAv&Rحߏ)K;O&D$8wQ|fqNE!G(zoAƋuzˈeYK,FL5m;Jۓdfj"d!eN%-mEavtk3+bm?`jH_PR=ǭX5 M!S(Jjo6DO"~{oP^[?~C^x!ͤLN]͕FIy)NmZ52d%p$+.,`jmrwس;D,@ *1oLnP AOl)PִV4'^md8SX2UTcj|n-r 3> ԛ2:A|}8My)\,0k;Hc?bΨ =}:wܨ,z%ȺeRVv_3$߶49{8W!rO?_:n6+Q:5'Q̜DwzJ7o} 15)"$76i%8Yf-8VؘJjeFk`F.OL6(TQvS!Ӌ!t2R]L49ZH v]*sX0ڦ˭0lk(Ϸn )uX*Nu,<߮y*'m N(*3d*R,VLavj]y*u!UˡX;GkJAQn{s<>VvT$PXAe=She"9rV:9?O\Gfi~_$` CV9|L!{-0$} /eE5GhHN!S:F9[#oz^sdTJZ[uѾa E&J|fߔ<0?}b"@YV],M>d*}8BLpf-E 1E>T8݀C3+b˜d9ܞi&py? s=2 r< T< Y^evVQ#P~'jk_^(@>đ4`4xcȝL>wo^x6vT"w9vZr"뎺b]Ʒ60 AH^&p`cdaFe~SP ^ 5!Nn_3cֽ:G 3r`ĺ{KVm^mG6Vj[ |e{$s@(3ݙTZ}̞gC%Lt:s[b]ukk9r6Ox~ӧs>?W3VPim;'X$ؿ2yv|IftSBPFW~Q"l2;+'s^yYcI?;YU~'ik_$Pљb2Q}Hj+M%xmvl[:%cvShT8Vذg}\y]ݶesؿG(6n[Vk$&,:Aru֖v/4&NČ 'l{'UI929yY!DIJlN :[+urڮrv}p9*P;џ [o~+dmuH+ubLV9'@յ`Um`Ƽw ?y*OP(||NSf|"\CyAWVYu'j [< ,*+t ҔLK|6w}\٘&=ZUFTPӠwXv΅ÖP<߬j(*+1ΥҍR 6F6av- ?[`]-eγ2!zeٿK/j'g4/Hcښc%uQK(SK򇬈OG/Vy=N FSKh*eaYe̺R`f|nP(QbQk(Ͽn-Y0IDfCh#CP(GB|8js ֭a#-,/H"kuqĜ{t[Pmb{:֥V$-.SٗFًV֍ؿc h;nJp1<6*4x6V' 0 3gm d"JI&pMȺ^V 6Tyfmqy[˕c[dž  4_!eFOZ$O$)19lmB>#z>a ɞD3Z2OO3ΖƷ?VSmqipd*LwbJLn2ڠm έ8/T^lZmj^ 6ɟsw;B9=$E?{10E vE8㶙f\9\QwZbZxS<)%jo%An7'm&J? =M iN = $:^dmm2f&*hz6O) B&"H#;{yJZl|;8O(-gS&ƾV{'9 BP^'B]]G&99ǙxJx93F}Q( B)p$";N<%( BP('m-sS( BP(B9 BP( A9 BP( A!xatMWg`~&P( B9"9LQ[B zm+ӫ iQmS]FIZʵ2'&ּфطBX8YO]R-Q9G(SRԣqkE{^ q M=#}32vM?\ ^-wҟG&I:i֞OXR[ϒ e΁+iFt7d.XԣSHShFP( &<* plox"ϙ68 Vv&wŘWXdʚ4}?[C\}uh= 'wiɊ%w` myV $/)Zgqf%]"yR( B;Ab}c736՚Z7s!'m&}k8ⳬjRVj)g0nip%=s}bnND]xh[TmQ6$}Aʊ x|h͇ +EFW"◗ꕢ|$,i82m̛Dz΅'#}3TBfaQE- [UC#k~H u&|cZVEV|b(d`L0{qF]Z z<^7#A%mLy0 mm#a5}QZcߝӘM*kl6( BP&<'<8)UmmJZϙNibުѐמO$I%?]šX9Vs_ qaC{ , 9@ߘt#5kuFX /ǔ'_3x6kc?l~<ʩJAGuAݚfL9BUucCR .s;y%ݜxqEJUnĪKity':Xu \x)!^^plnQ( B!8'89 =rw|7,ZR3븟 _~O]/LD[3gEWqpIZ|U%pX[o;S)`!B^L/8Sh3?$ylzlM9wHP ߮ jS^D8LlhLlT*1*"RNq;wFBߟJݙ'З I%l#a{vIԍ5F:svwu^AP(J !!Ioqk\`}|~7QRPZ{cHkͬ~N nήF%H(D|<[̒Hz+qW-̦``Xeٍp0(kԈXخc,/^˷UYј"݈-Z֎$CU)]{eNx'Vg_wOV V p[:ubV 7u!dO])Igq_BˡNϓՠ.~Pj$ i6 BP'=8 Jt<,y"h|?'צk3t{da| )1NT IDATs}"}ɧr d+bH+b(Bhgŭ8sJw-D1޼C271xfV b\Bnͬn `ӉPvȮZK=|(;^[`J! )7¨CH"ӒÝw'\atFm;˻3#DoIO \~G< WL.WQAB.)4낛K=/]Tx &'ĨcW!'YKD(\<Ttsan_+P( PcC1ŭC J<,yhH"ӓM$HW,{}O<~"W6_OM:&/P( rB`!Sx;cJZ9z;7[D y<]J'p`պP( B9at}gٻ+|'yɼZPP( rhp*SQ':BP( B9}+S{xBuQ BP( r BP( rA9 BP( ~s( BP(Ƒ]=c~r,_<g^}k%#$6E']ꈌOotHDkQ-2Qםqy2rNykķiDځay#~'>9~7&Mitol'WϓzlLD3#m8 B@ m}'Z۠q%V0U)8pڪ{thGz"~p"MyV S BLj+0S[bw"3]n7$8ˇbԽWquT쒴\"?XPJ?zn{6,x*m~٫B.]q8w#@TeWVϜCk)oH3W!h>)39 UIԓ|M|R@{$%JpBP(0+g$NE>OOKA*925Z[R3W8$Йtѝ;1}=5[[y'z//C0sWwU BDJs"+VTY&JzKҁ3WBMbo)鷍\TzťⒸ1睹¥m߹NmͯәNImbdހR2ym' ԛ]‚t>zUlEp*XII2"}qԗoZekiok*eu,F9#D|D*dP4L3ځV1OH2=>$U:;~)>I}*, w<DP('P!q> x`Bc|(AY Zl&H$<;2dB _!}(|WuCH:ΝZhWN ~ϴGN qQSiqQ( avўJ_CJ,N%)h)%kC96Ư?2O4! By Al"l:={w%2@9 BP(MχSHDJ:@ϭQ( BP(OZBP( BouBP( Bߠ BP( rA9 BP( ~[cC6MOV(BuWͤp2s9a'V[Q%DrM$[Gl1"~Z0ga`[k8⚜$Ll'+XU9k|nf/IN +'BHc=eWNoTugs~Q"mTKE(ꚜXϼJDfjd-y B{dC B6\jjqMN$ϴD3m}?wEߎZFqY4.SKQ}-]2|蟮o̚fݬ9 0$ʉJ&1p*l,n3륛Dc`bXWz@"f5(E\t{e<_C@3wW_՛@Rp;$%&DMqL.Y]8GLl$w>Kӭ`V|e`L5*ͅw)m}sj/,Gf}=)GLy 2jSnn`%>"δ +OVuN!xݟn@d:uFa]=ۧVe^bPim:L_ R74'AxDۈ4☆ὁ8ㅬsȋ ~ y3{fH;ҧGGd*KSaV3o/;ۖ\_UtKSSG!Fhhlr,NEB RbRGN< u(ѽLTJ+ydsVښR60l nǐ{˓ȭ/f+b ='UM=:Eq]8eVǧFe 0`o>A0fN"_zTcБތHMz ]wF1Q&o#m#y.W"T{8D(hz\dr g RJ;izi:{j VVFkFbyqb)^Z82-Ηf-j!s]6uS$3'ESBVU"oG/﬈o=3Iy$PŢmThQ#NXxd6Yw**T"q7r!ѷI]iT[ٓ`5.I]#0L-)=ofLH*0,*&Q\56l8RQ.y1ER";;v#t(3|39k8J '}oCQ!!;ཛx[ZJ& 02C^]Kđ@Hl0([Vݦr1r[/oT!-X\+I,'Y6 Yu q6>9˕[H SXgA[t'ڰ:a&]|wO<[1h:,Psp쭺L{l|Hf՞,'%eq&kۦS]zKԭ M.N5 241Vv {boppirv,Wz x5|[7%kc H~k_<$@(/䷣ց@g*շ@W$ƝL@6лGaѬnYI+&i'^Ùփ7_qnkrfSY?Qad!;Vi%rbA^"Y bѠr= =zl}̖Tgxa>nH6'B(ˌxߤuNf/9 H,J>(KLw## ]23;+3-RUBH-\>z pho,FlPK;kT?ķf*AK*"jhG!BI]io-n-9`}V2y'2yDdsb^tQ9)zC|r]/-O+*\,8 $Ґ(*^$tS58ւUR`3 0W Xi6b ;[3Fg*hc]8Lq0aB('tJzώp<8ky wWXJ$vHGrf^$ vsruz+δ.x'6 +mF(oQ`t׏ 埾P1^WxiH緼e}>\:Js\:F@}ʏ%Ha_+.Vj+;f=Iugzoρ@1oĻS1)Qf5]S7"{C] 4+S;iWez:g:#ulȀ##ɧ^[ۋWЦs'nʚ;J~8[و H &ޞӣ֗W3x&ٞ3!z"Fm)q7T; sgl=K)< )Y"r$[T[qDו,JHO2l L"|@B. d~;9y{RϭTғ`eJV պb4a2ك6[}gnl3/唍C/#␬5TiOn=Ð/JۨMF6;_CN5IZ ˈ4=g/uފx$DaI[ѐ"bq@6JT'Ѣix-z8b;qm&zHߎd2@2 +`Y=uSoWg&'7`PgPJp$1{ĻKz\H Wu Pw+('!Dr\VԬRE˄B:1I\TyR.\<ۭbXx&n!8 u̪_{#__bg? E49Jk88ZN:[Q1llq* ,\^?@Q\c5g~NVIWD֬'Ck[j5@ҳں\kr᫈q*[q>+,ѳ!d:&VT2ނxfy̺H1ʞvoXf993/IJ?+ں֌'CkNJ;1z^sIb>3*\#Tt/;}=#$BHQP< )Vc;!⢕k'O,$sHs^O!EFp06X$PO) |B۰ճ*1L֓8x{]b>'Wur{Je s( BP(ǛܧK{<b0Hu [P( BP('!>;2$}5D3'Ei) BPJhmPK}&BP( BΡP( BP(\0}ݦ@7w BP(}DӂeV1tn(iD#?x_|?YLƣ_ mtl*O0Np򕱱+g;e]"ޭS/]9mNMj^/]2{\Lc5$E9樑NP( >DOde IDATp㗞_p?'k[ig+g>:atj2Ut YUBTKKԇ{FU[!;o]VW#E{_Q%nNzڵW'}+F$:^/S's,,1( BPwOC'~}/hB0" fR3U N??>J0Z5#lFrU/HS9Sp Tg+v\ _3ӿҟ7ȑ&R*˙R 䓿GNuhqw/{~ "8S'Zq޻?UʊGXZފa`tL*dBeF0"y©\ ,P&\". z7W=1> wJ0փCkӛQ j(DL*pomxzFX&dtkٹ-8gp*Q$\`uv~'j=%.VTJj{ efa' ez5v? &¾nJun]uMmHrVBP( ({ٕϼ7dB_y?5w}Hd_=naK?M{ى%w<9;>1I\F~cgz'^׼5yͣE>O3o|]_!a_Q73o>&{oo0${"vpҽxe?#uZ$m,Rr]);%nݼh-}m&!FmekA97"ˤ嶡*],DB0^X&ig9joڻb BP(,Qy90Ihg+ {>ؾ#2n|Ǐ1?/=sPxب>7r?_ܗogn4Z;\a?' пc_?"\7?zoz+Gb! azb<-Cz煏cY FnU>E

    G\HrHҘP;BP( ~_OGES׿.&v_˼do{{{{{}BL3[B1r$_/ d^q:^>?i'~E/abZs!{~ Aw1ܗ{ÿ !޿S}ȑ.ej5#.X;V8"h;\B#MSb ^4BmTpD]flE49Y 1^\3 {9W#MoZّu/dÐEQ߶Bx9BP(J?y~Q˟2; 77Dc|exCO|mWR{/oO]3NG<;2 'y m?)z6HkPe659""-wt $31IRzIeI@d"UggRcLɥVO A.0[21EڦO6$O*ӹ{6 8Ѳ :KIt7>gQ+ edܬ!ynY˅Ma!8]u"3U$7/NH+)) v?xKǾ/.))4(u+JGE/ɈѭDiDQynEiQ(*(/49u+VH3 tJ){^B~jltOV#!z6R-ȱR[Vh#{>ҍyYL.g$x.?={KK%"e{ G(R [pfKoksrVZ DNtxRZFD$xs1 ^Z~@='2 sJf](ȳ v< b[cDsbbmZxs|2y=vZ^IQt ")3 =vCaع$4kQq{pqj: 0Mlbק2T&[h7ǦpmҧO ^k~׭ms+kP÷$cK)VW(fm[و%/4 r`[ w&N[\S)ɦ=rxt5ܺ<$*O/*/6ȝ Πz™WZvtģCG@ލ\Ҝ&y}>6U11`~زIAfFem= @w%>Ϗ}ZzMڂѫՅ5N\oHo83|.KWIn9z“;enn?.-)(N簴c,]ƥ!"T*SWW9 |9EYy]:qq2u+h{G\iUH4zLAOXf?`NJs)94$Y38ßg\&COxIx`Hp@A? 9hD~$yrwx`"RĹ/[^T8 oGTVse=^x q,9ߦV)䥈se=^x q,šRĹ/[^T8 H4@A? 9hD~$T*SWWW__@< fMʅ+-ق\sr9%$&& ~_~j,Ecr)`V˼Y\z| ά s ѠH4@A? 9h+97pb'}hqƢ3 _QgNl8 O.nGz/s~ܵs1`tGfݲyB6"^8P /q,92uC"^ɉ<Yqmd9-+6bH`O'cuQZc/ nYq"s\bLd+'@H4@A? 9hD~$Gm5Ivxs)~ee]tkjat[ץK|՘2,G<׭/)) vnUYzRoО^t?hZÆ~=l[k>F9G1>] 1h c0cmXR#tyHJ3sNiRSe$8.?={KKgZ"K4ȃ~$;vtaU6& =D"0UA Q8/^"WO5wLZyR¹2&xln #" "J6VdHGL<49 Y>se k)񣮮nC Nq.@Qjɶ}^K\`ڬ}qF7dgo<&o:#iTf)X^D鶎f8Uvyi=rm9s^~$x9M.xeŅJO]10t&gTԔ/}ld7$E5 C^- ݮuE)9Ѯ:x&fcb~T,@KGJ,F?JB,.S-xa~R!MN~qԽ@! KUrq.1 @"T}vWHk4 6>"A bv-МqIYee u1PؐR ,I)cs:WqayYGZSq!hlD1`˾e p?nTid`B};j@fswp?ֶG{yg5!pl=b>bL)>3\!-Hb_ ,Prf 7 g"w"M+\!O͈d՛k*sT%(M39qE'"v:/+QB? Ǻ:L_0~[\DDWMvA;Po٦GٓNjx^s) .W_}5& Qf({e7*s ѠH4?K wuUsXLR*xcCv~eϑ(oD}lk6L^Zh=2Zkvo~&/Yώ䆊CSN䃰!.e Cn()8[mL_^ZhhCOg0uv}`ט%eUl(0}DcˈwT_Q~g"EK9CSɭtխ/,Л[^$*g ab)U:mhB39蜏9(JDc] SG:M(睖ɳlKoСЫ^svs1&zesx 2).&#S2گ)HԅI֞> &ᱼBYIuEɦTdQHMkC|M~PɜLAO՞79.•V PR7%Di2 G(r]N}?cznybG7dN:{#ٻSeW- nSYQf sgyXSp6r~9 s Av0Si MCbY]DshΞh:wD/~ӛɴYيޖn#RKՕ]om˒֕%6<+r2"[;Zy"H)emRۼ7;Ek:h 9ۜHӄ r_@B c2h/Y)JsV86k=Mzla]R-v7" ߣ/Z:e%iaNitA]MGvp`$2Y| sp.ba  HSs QN]"$ª-eiR"1aOJJKI4fKKᗓ}^cR9>!ɡɋunsKuEJcǠfq&Ff]MDŽ] T(Lf9?]U^ 9?[9 @ptv*K=>a(*\n+|5kl8qNdO>]>زo0QwL&8Lf9?UV)显Gf#;4A@~GHl]8<\ďуKDIDq$"{ořAp90uKD39hH²e+ o=*%!!jk7Ç3Z@$M+\!O͈d՛k*sT2˜_Dp*lc W~G·9@$\s;-\HE|+qcvZkyÆ~Ǻk:L_E WHcw|hxBIDg}o1x<韡(wEg8\z5#ͥe>^{n:s Ѡ1Ťd\BX@?bJ+Xi]N2[T!@,)|. |F!]17,*2ƛ N]_VUJc}tD}lk6L^Zh=2$ x:~&>秃|yhs~dH ˘C?boi,0*rCIjcBGCX"Q R\UƏƼ,)bC̬(Jv4yގG\~ߙȤ*}QEy@gMRmAE9֗W-F/EK/|gߨu ܜI^!9蜏9a˪މޗ* ÇHaT3_p5tؕ=Rɥ4ɎO40|ԩӄri<ʶ Z5wi7:њ ڱ_Tb9W5dZMd_S:$ =}L9Lcyj: M-̚dKJAic%֊9OSɠ&!jO~G>{Cˢ2$+; $RXᒬhͅ E'I1}3XcoolKtSgdPV府Ts_̃Ne%ӤpvYy/OMLHe.@T*sGgzmTVTED3YtKL.5w' N׸ 9蜏9K+vEXd#D<_IûGg@q IDATG2pVszA<x$*z}~zC)¸^h6*=+EIU[ҤDcž"HiRn\:L*8'eBcn3RK m#c s$3tns $8&J}f  Efr5c2]f4M)r~s>XJc0Wi%j9LJ:ǽDZ.,]gOelΚ\_R`(ekCb`z}si)wK:T A4ïu3+7&"5OLD9~eVMs QIdJDy>I =$0W3׈MU-iWắBT~_C9~D*=:,'6@WlPƇ-nYFEMeaYQ=Ft&Yj+Ռ25be{eKUP4qR·CU!%\s;7ړR\qQF~>C~*UL=ln VE_ZX3t˖q MXLH4¤JB"x]ɔ*D:k `Iyk-o^ JI/Y_ca[f(^*XL҂jag鿹||\~v@׸etŕ6\ۯpdtR"-9 |$iO9ӏ/Wz䌚vM`yk6^8s8&ƙG,K&4 m.;W~WV/s1ek/%/|/.}g=SK??: Ljsן2S^1䆊CS{Gri%;#ÁM"K/+TK;( ^j>4ϑgd %|^!,+;yVy;ZqU"DDRmae{L..q+SFI&>d0"XQ;r˗r~亷s[ZGN~?7<_;ʹp !K>޵?<}^me7p9ir~>͝ _̆F,nt ⧱y?OT").{%#WWs׿qU;4i)FtW5~-:WtW |moL{3Hෟp~-{E_TrF.[&HbzǼN-7{747surǧ?sli2o蚡NN ֎9,f[A:I)iהe $ckOs fu $%[,]"E,E3Z2i˝ 17("hJNO\w?Q{s>xDKn4ejfVN@vV6q=v3OֺoԨj-T{vu:6\;x grÇN➛L 4T&I%'d2 c(\ss1F0xLn()ػ?Na`{MI9P5yA D]{%뼟rK7h C[EkGUFR1a8gIVY~71<,xb$}F*Se]CZ6mRVo4zM{3YtKc®ХHMO wTRqNON;ExD>kK㘰?|7-x̓ҍuZns[[;?yj8CV&Gno@RI=LgQ" k0oӐ9XdVj4!k{gm?1csT ".Sdmx=)7㔶]:IYw=}쏉HZVq/X[﷍OJ6Zg>WhM~@_ac6u>7QXxrO\!l~O!"RUVFC07dsҼkoz-Ygd77٩hItcvk8+gm˒֕%6".o \$#"'bTR&ؖ-6颣y3;iSmmfaE1AOva _;R|@t#gYoƼ׭q)#qw靁DJ{K/;W0hwzsgv]M%Z(:ELছA n-n8M(T(5,5cK)ZWn-!qLiġϚ*K-ֻ֬MeHH_'%X;wU;B<#;辗(sd A'R Gc8Iqtwy5i3{ݗ 9-~loMxRM^4W}k!"i?~|e""b"􊭥)ޡ]㲌u&gޖ9zla6-\=b) ?n-=q)ܴQl 3y?Og_BYFgWpF)u-}*վ ySc|1ul4p{{y"M%ߚ&oKTĺ|'ΩT{7XʆnU b3.S|Bǟ\ƍ|kCˌsw^Ce|w_S'E|(flm#."6;$bO^VbfFg{4 6bMS̹!h*L@Dy^ F~ )A1yK_BQyj N]2ocڼbYC瘰'R]fg u)0U R^7fxSoMX{ܪMWa(*\n+)F.VoU #\˖q-Xb"kL%|˶;pjtr8d>xpx3N55[i,=BEmpE2n,X}-{pDky[׭lYukXׯoiiY(8qD%mBԧqJ H4@A?`翚3* tE Vl]sۆ%2C~Ă,%rs1۪2Uӟ%&v1[+r`1C&V Sg[%"Dhkl1m~yi} orsKv#/i[b:z΁}?.6?kon-;֟u۷/)<_s6s7!|z{/zUM}s?ت~=o|.n鱟8Q-NӾ҈HΓ*~&`>?sk`6) T|}&/1H&<._>iXFV ̟ƈ]m]RtZF'm0EXC}.f|ˢ&;?GB}-.<"igOL#C)'XO]̲s2µ_ ":6+ʟ^ eO$"$km߻$*sCu3OL"sfGn?w蛜}ckv&X/RDMɾTH?>-Yp猗>?gB*BB`y"X;qC1"b$.,ا,_v*:wm܋`/o (ŧ<@D$)֩ kk29A~TyyNJ3y&ɓ|{fb""F )ܸQ+ڔeko"ay`M6*=+EIU[p^ r,/Gns{d>=UMw>78S{#o? <~?oݡ6s%̈́7Ϲ/oKIvҙ b+,џ{QBJ]tUW_ h3{~q|I'}'w׃_(V-.һk6 =DnܻP'P Xcnܻe$!$K)*I~Gv>4"-0ȧ&fc)I-bV0C U3ɗn@dCa5ݦx_)o^RX8 |wzŎꋳkW~;KNIjq o~o=5}w/S_Tߡ/Ӂ@`'?iɜ.QsRV6a槿xm޹\wz~3|{mϛ7Ϲ#s$t~;|Fx.K>7{,,dcY?32N[e?=dd'R3z}Ϳtpnb=`Djrѥi+M8T ӵOKSOF-hrE<@Ja$;F;n?XWX3br]~IA#睖Al̐隂1S94ɐLCbYM΃=6$#>s-9A*CzGڍNaK }A|$X;kf9W5Ȥ<1UVNsN!IuEɦK,6Od!K XB IDAT:$\{ӆjss/GsNvm>5`}у}ɥe\]&vO&wﻟ?aI_tZj_\pgem#^wŏ]gqW_ϑ?ΏNْ{/%sVqNa.Zż{_9^Z[a[[m3 \QMv]<ԖE[2A1O \{o)w} 9kg80 ^'guIj&>הnί~Gۄ~j;Cgkm~= nnwz76鞃Î^na2juBP̚c"AԒmuuuuuUNގ"8,Ҕ9jγt9i|c!BrLYjڙL]QUo48uVoH쐘"U+h`#韋΄sƿ'a$;?>-PHoDkr}Gs7mS ï5|4:@s\ 2?8H:tM?on7u7:n?'oo !MI7=N@,Q5W3 D^d)eřZS-[FD,9drظqÆ,nw?wy_!I<"Jr]Y9}yOQٻ{QW}r[f㒋N!퍭Eg]uM;Dlh= d*mm[9*eq7>'yE^|i v f(b5ïewXEͮJJsEjaՖK {RzV\"Mgq6[goͪNdɭɃy.sFHY!s̓Ie>?i}}}}'N[%+Mcv"6U 9?tMo| &?onqo^R-ާv6Ŏ8}>YV~O}$/]/c|'WA,ξߟoT>V#+uAd'UѦ8w՟v ɧ^T#_9cSۯ8vwx:5t2m>ED#졼oS᪯='?X{A@\?{ ol6pt/{>Z^fDg ^k1Ƣ9#ʒmrrw>K-V7u1&*\n+ODnSYcI^$F5e_ `>NͳV)dHm0Hrr9tD}O{ld>D)[%D2$Z3˃)GPSWWW__kׯoii ~GA=X `>yX͗I7/{z'¯ @ؿl׃~eVM@{\}򞕎. /aA?Vz $s`{;V: Hx$s ѠH4$.Igc&#馓c ~|S}MsՓn٨,rͻǗGcq>x|[7_d O,9iAm_o(Cdin^Qﯟ='^qk~ ?U&ky6s?.x0\XI'<Ҝ}-[? L M;(7I6NI Q@C J9Pt!R ***,EB !$fmؔdfyymeʼ ]Mؿ}g?K7tO5 5 C f#Xh^@"ZUMJ'5g,B3,@X( #}ڵ~Yv/x[0:dm /p!9}FۻN]V,^d=^ҫ&.>!a|ltI걝(?n?nIx4x9l\B:9 2:v\ؑa2x3 2v^H!l'yĪ"b&oZ\ZplIݍk sE&o/G>1!LXwl4>vr_;}aaƯ9zݏQ ]~x,k4yq=,* ˼\UW4k؈ڌ u`vj1MɬCsҲiay \8`ibS Qck~M`zMP]3&Eҩg8o]+^/ڼx2e߼4ɛg.S[}D(tڔsԴG]9C9Fz0!e9,n_ «)u+4"F$8־32؅iCbu(5 !3td̸xSkhPZE}dnDsSoo;3KJKK;Vi+--θr%0ʉO;a./)U[KɾS4v]W2'߻nZaO㥬›7jv\̺~=/$vT:tăʚ%gy=#'!DNű㉇>Z?gL>[8swv؃>|3gϞm[UཛྷQptDɯ2ufº>X>oO$~vfº3?_97&}("fϜ{Lض1>{G|&S^tczk E3aO?e'ZƼkSn#pR6.TЭ~]N,S8iȽ|E'Wd\NNJJJJ:ZR;;J6nYȻWK -=~K}|;fL _ކqwO1=7]?٣ FG?un{ٱoþ.?&/˵$ Әe.Ŕ6u™sWflX+Ero1>;n yc[1sʤI'NĞl&/̄uǶ?5fٷCQ0? -|,F@;BHs{$~.}4eV7&i+V^~g|櫂.mb 9,4!F5UԔrݬT`{بlIf^VGMypBU1FgCv ʩB6QýHY~EK!?s+ɕnRUgr;:;wsPW\rz vvv^4rrd2^n>>4bs+KR*9<2n6)+oTqL }`AˡKϨGQxbyHĈ!ɫZ`ܴ-1A~cokI \R47Q3=z53[ 9,~!!{0Rw=^en߶~+gÂ=""bAs䥹AcBDDT]_XbֳNU7.W_e_HFak۫8!]yR!29.}8b1xMWfkrwM"wlN)٬+" ;yaYݝ,i #Du/d3݊eۏ)Ow>8^B-]u̪V9y|I-|mլ>2IfOSЙ "4 5B:0K&# l)HˮU1HxG1ITV{ ro4^;M7 /P& JBٕJ(E!ʆ~Ru >v#_V:6ic)@!i͆gAqzdæcU_~Ƃn2_Rk]gjtX*(eB0&״ xZ*TMM6n^bZd"i_<JdBJ>1tKc EL7׈ Dڗ$&Kg21mጛ*Y摑(TWT ]C]()-*5(D!6JЁQQfn°9׳!m٧^l/pdsҲa+ یI0VkIGM#X7{V-6Zԙ<%F{Fz5Cݷ˓#Bޣr #Z˲ L9&O׮ Ю!ʖ-򲂚1#h}k|&Qan4BhUUm1mcoFSJA;o4oNX= vewQ;Ͽʓ][@+~hy{+Y[|?_*gML1BH=woMz{aV5+ et_U*ܲoOi}E%B؛w04Ҕo9^K:j(0قnMܿV~ʚ6HooZDy/xzOQbz^~Z0KǬ$$$$%%-r٘s1DYi%FNWCG{6]-Jt }"~? )3:o_!1=D K2KsKucHCڷqC0Ԁq`3~gϞ={Gcb liB;3KJ)]9 RZ#`t`Mͩqh X*<Mn[3hi 7{ 0q0\-.)qqƲs͂ g+_90Q܍p] g;&_}3gΞ={j01+w|x" dq-_MG猐隘rIL5GO9~MSM8}ǎ'(] D_~6w}i mixml_"⑸P_ k[oݖws9 ͹V#H{GuC!d7~ڤ Uyc[1sʤI'NĞl!DQn/x^&/}W~>ذ(V~o/ٳǿNԥ4eVGMM^c̹xw GB&Q2mgf֤d۞zhN7njmhéBڅe+7 B?G2Λ2)ޗy|aι3v0Uytނ/v "))M[SK"4)LۥKuh3M\F\ }&廘0pG񣲾]un.jH-mcҏ[e,1"Das ƒ%fiڳp2â# ߺX.'3C], srm\'5sMM/.UԮH_{_@>tw5I }GHWZW)d/]R$(rBjinfc "o.[;_;}Ȼzsf)Ou~Xͧn t`0Bڌ)Tmʮ{7~6M~忇߼RfkˇֻRxϏj2anŲG^rҔ;/P!r׮L)`YV7t%,aºBm,vUD!63Wn":$' -]uLkBg'IEM72Mn cRך7g^@l/UHz-hK39 hǷW%w?wn1ܿ[ͱ-g_C3;D+mcjGRɱM6 c!Ho6< ŒK#6-/ewu4Nډ#q,Eנ?”]:S#}L7y1M3sˏt괼&uB,QAx)פ2Mv1&B<4y5ЬI9F5J!AT,Alf?5[z,Xi촿Snb t IDAT[YgN|K89Z+܈]e͘$ɸ^!6;53辇oz/Iﮛܗ7{7Mİ !&rꃣ]R~ieN9SVO^B&ui>͸K0 6ek?ٟ[)3pƂCkot<=EJcmmji~ح 3W㖭~kW|;[Lҝ2L84[z>Ӯw\T"~><˒wY9A(s?^[ok۪d&kv~wPl{9~89'߹%ɭ3qҸ5_sUB׷7]ON<'/2UfnQ3'Ӄ8.knaKݝzm0 03P޳Փ[f_kLf,~雑mQs/k|uI7yI0cu(W(^{s#g0"C} ;1^kmB!mƇ}gQ̅m<{Fgr80Dy IÌ_sh4GUu_m[`7zxz5>koAWk`ԡ3z8sj8s >>!a|W, gv畖= #&J OOݙYRZp)6`&;m3FhIܧo;+E/|k>sj0T4yъC=MɬԊl]+5g{-LGn#.BR6vL/S |؊`[XT MϪS\] .fuG "EqFF \ㆹRD+(W8xIƂ-3I&’Z;-נPW& m+i%6QX҂%"i/-mRan4⴪_D8]aI\e^A>v Rh7yN߸IG8kNʊ]ܘNJSw~>۞J2nƷ.M nbMSf^}婩~vn||5vo|4Mm۟Gj_}nb#xY#[%<.ܮ"jAAH")vXa͛k%zovljAՋ)Lڿ}۷rLXaZ B舒_wrttx%WZ*3C[asz=mNv36}lvfº3S_u :rgk? aݱ7ad=S3qgc6O)XwD IpC bgw{1R__&"9lLa*ij nT &VX &-Bt<5{QuEGw0:/PN>ڵ.f ;i;Ϡ`_;1"k׊|z9Qvc@D+-/16ȡ<"Ӵ4ՔۆdVLp0WT{%R+ #i)6!%/B $Eچa7+-6Ȳ8QW\5JI"v v4\tToykZB?0@>Myie ߱y)/5R.~~.LHRRrJV-vn!!TzjQ3m&֕+o\8)uwneanvKD}79F޽je3+=`t#_9_E޶$Q|\;(≃i|5'],طa?uu~qmĖ^g~U3/oXy#}}|||| AEМ8m:-Ft͇n*X'OtH&UX(<;J߻>y~{jeQ^=&gg?7câX({mꎅ3v2טeߞ>{-U3ʜl f:Iɴ;IL}yS1(̼cϴFI#A& "m̻uLk^|bI&N8=٬@6qprAHl\v9F9 w!R*+rF-$K,Jr9X,BiK#(뉃#D; Zsv>޸"rrRRRRbpzQv{XTU}J t57.],q tk |Uo¡,].ݨTm1kBT97sPDŖ sz|:5iS*}2$4-s!ҬPh9ECR9OM #8/|pi!V*9mQi ju4* ԍ */۶RެaMr5D%6J A}mX"ƈ(r-mVWSʹx?GgGWR)iD?DU%O4!Eh Nw-f.Fwf&X][qQ)!Ζ!8]cСx7?rYE:U02ycVIё?n>W,'!b¢# ߺX.'3C],BHˡK6N[hUͿi=j6o/tўWZ( yEf.gj_@B.WT-͌a,`R!B0)$6!jE}YVAkmPՔˤAi)ɫj&_uݚF2NE.ZB5v/CaQ4-H|攒ͺ<`w'HjȽîu4W#bQvך,$rswH틷m[[ښ_MF(EcVyIјeue9L6r] M5CNf5 Ӵ^NUGάj}gvd3݊eۏ)Ow>8^0<L=YS[1([R }N($ԶL!]iGD^Q;붟 $ub/I6dKs+DŽ8h'ʊ"nu͹ uH,pSXp3^c1+GZ*=,[E\Cޕ&\­ 8zё-v s^O)l ӱNb}4XVKuvcam/PP! 3!Ѹe﬚晵I6ՓGׯL/c RY;/+{p%u>~q!s{X]srLm޵,ߩ3yJڻ)dRWݔtw[&!̩Zҵ5[hjqrVKM6N;"ғIl\=](\蝊^jok,-U8x`c2zu{WOD"oN>E֮m([ rFݖ`-<.!a\nʦsfĶ6mDNqSqV^m(ŌP*j.mCs"7Wٺ(N+.`@+lܼyi5Z#4_x=MSi2sM 47A?X<_㭫GUAZGS8WXܺ@`hÕ\p!e$v)g1;e:E0,z1i+-#Viꜿ6 HU9E=kk,k }Gy{xLMgVI -AA5{?~ Y=9 5``96«1غ+jbێb ,Tb`-dxPFtZl'aNl.+U73mj/ymyQqU3B1$fi紪’Z`ݱfכO(jo埥)}}959O.7s Vbnŏ`Y(~2h-$Ϡ\9󯦗<:҃-,ƛH$SZJZO-edWYjgTj"IS-N\]Ō~z`94ft󸙫ϽԊPg~a2KٍwXW )y";42U7' Χ KIʴN6iqDcwz&xh E!k[oݖ?){/OAf}o 2R e)*v q]X;~@LxRy^Z{Aδ闛&-w`$.CݥFasE^Nvs҂:i "/L^Bڗn`~e_b{cZKD,a0RwRNN "%io^n+7ǩ(up&߲usHX҂%"i/-mZX@E{wLWSp9a D3ōs+/grL.̴`6}4-yeL0O=hWp-5oE4 A&/SmNS?ʱs]!]㩳"ZyEM5dd=<ʬ0O,yΔAHs\j2.!>0\Zt7H Qq6^~UۣKS Rg`7Qm,B1rRR]e_HJJJNɪKl!ʥkm]:ZfGjn^Qҥy"̸t&{2;1GWͅ(TRpWhUBD]]PrW(G7' c䬓,D>G9!1IB4^ZLwUp4dTWS rs0#f0"%i[$Xa2nmPyJh8mKcEANQCJ- 3sϢ`ý$V!ބu+Rá̼PiC=M[ ~%HFn~u…L/iu2"DTr5t+0|KfBi-dur#˵Y&::=4Gf۪rz>NUuN4)a1~:Y~#%?FK /! bI7[O9M/pyaZH Oq>SqѽdWfh7\0H]u?ba;y:Mu1q\CAjIcd7-5ѵNFe*n)Rlm F7 dk+u]$*22މ;h}?Glg'~ľc~_cfpǖ~!FDӜ#f!JFE !u~kr!0>:;R6eT?W.!2VIaB4akk`H+z8KZ^£F&Xe^C4 AXl#)F`D4-ADVm]?ݚ;3v׶:5l8̈es 9QkЗL.D%s}nRr}Mp÷{xIʥB֌B`Afa]j>MHDhx(Qiv`kP6uJ6'!Y(zTw?*K 74\ǻ4FNYQWdR^b<8ۿb)$6!Hni,*(n|[V[SKx_6x=5y|kg! QVJC"<[S8K A>#<-ro IDAT>>\3N+"dRבxb\,SD]L+1"RMx۩ɺToY敲>3\(dL,f[r 8G7lEHRW^\lWyEyzRV Uѽ###333-+-[w2;%{z?[#mvcsx* `QA_8AY_ 0ԀqC 39sXlBGzy9ЗPa:-lT;DFǎ;2[f쭰bFyAc%?[bm+-----)K? =FOݙYRZp)[ 3y҂cK,SxbՈ|8SR}=b߮,(ȽόsLfºcۦ9c$zݏRgΞ={/>Z?g̸Gk·~3.335GO=?ʠY%|vZ$#۴h̋?)n ]E_|jh8# 5103«_/fgC SS#k$<}`5Mg 7S9 a+7e5AW8K;jR.A-5Ic~u"[W29ꓫV4>} 6t4V[m)i,B!Dy=웓ZӊmEy4^~"]2p?GUx?MZR䷦:hr76,}|E@Mr׊6/^Li&ye}zgEM=ڔsf˭l8`91'ZquBx0&<=iٷFyo0ƌ vanQ-9 Ȯ&=Ķ2[eME*j*L&hzlkoR]Ѡb-U)Wں8v MH3/Et2eݷMMv^-2}w^ii?N\/.̾r0v]W2'߻nZaQ6߉9o<}ᑩ[Q]{g;`҂MByJK;stg\]T\}ħK㝰(pĹN4|Ё3fÕxTc[dܼ"m7wYM`,7IUWQ.#|,4`'QC!(oh<舒_5GO,_j`&;mGçϞۥL1__m?XWgl/?wBnvnL<ΔQ꣆INE'7.̩Wy̓G*J*&ЉgEg>7)簸#G6H(簸b>61K:{łK;.6O60_Kxr,uffbh|l+2.''%%%%O-VaYA3ߑBeJq[x t*y- /k{H\!ut"Wg1!WcϯICruqpr`5,؅O.B4a^KyӼ/o8'FGLژpƽ''G1)Koxȅ[ކFLٚ5G'㢈'68?ve!u٨ypUG Mݫ }OPw@&DfGMji ÔGZm66;$X&v vزٞƿќ8m:tW]bHX4~c}vëq36,yYaªIo#l%Ӧ|f{Ip6sHR!+TfF0rslkVў/>((]+V: =701ɛYiGJjYX6QvE^^*bǐ jOx&iFMS<N+3O!V uSuq~au 1cZ{"QND/RU޸\i@cPDՕy٥+0!sh{c#+ Q!l?ǡ*x({DPOQ\7궿ߪW[xkI~^yr܋?[i k!/>@/|{f l_h !Տ/{x Ko;cr'-]u*OlgvUDx>1} !U : }rh7pS˒Ap]4xk0aRFm/i[(D-"zΛY<1L y40=U+RY=iȽ;A41Mk{. !}! ctq6e!D1Â3*4p y)Η7!Զ7/P& -)\S\RS/|РQif7d2wouXeõ^l/3>F1}Ǣ풼; {TE9cG yioؗ{/_4skeN9Sb=2 f=.7#^:fÙXtjRo:tI tn/fMJr z_hjwM9Sb &<}n $ꪼAy2E}V'Vb`朴fڹ;߱cPgO-渉!`[^a#Yt҂h5uN&1I]ݥUÓ*Ѻz88T PaQA>#čn3R@{޹߮df^%.!$3=C̼{8VTB/~|3gF[0YRNU#DPtKm=O9sٶU[8!!fº>X>oO$~h26#fgO~1MNű2%99y0OEIj /`5GO9K)p#d#J~=d8Aݽ(ղ1i/L;ՇJBy_ocB9qʣߙy;-쵩;Μ!SH6Hys]&5yBy#!xd۞zhNOd2טe5ɛg.;X7}!LޫJsW/fLY&iz"?9y911ء[\.KߑC銚c맚qSϧwnZwt_XU]k<(L(mO^6g[IM #qo7.]{[X,͝k3>Ŗ3fTΖA&_Df5v2S>4yT[{&hu;O}Yp՞X_{n0ڐ_^43g* Ex v9JrJS8̉wﯞ8/g4KYljY`bdp;BAuYGG9Z˺XXeczJ'J*d"mnbO 3JRRFY.$ "9H{{en\,X<]nz좒r61sYu)2k9K;s|@X'fƺBbYf>|\vBZx¤~_t1l˫޹G߽-9 ώ7+u7*h@YpAS_'Bْw6 YysLjD"!M<&x܂d w_TUmS"O\Zl ~ tq,G3LZBMs6ıwAhU;ҏuNQ<4),RZ͚YDh=I iBvƏ@&{wxFM8o|wħ"eŠ8~F[xD8`µ` ?Yŧqы|T%8=g&iTk39-OL@Ð`{#=aƎ+,åOD"0BU;-OB}Gg=>dBsa&eDg 2 4x9 핌"=٬uz*,;Ȑ- #u~uϻ,lg[]@l]b`'hv4vn:QV?[6HZs8\h%ipil:gM$d h:S\lfYgJ)>=3LtwtR _Y;%?9gchJB\g@f|IǷa ~1:sĈLԁ YM1-JPfw; "IS-H.DU^U@>?6T 7YM22daHPՋi밎iۻ=L4UQgr:X@ 7ƭ7Ex\3/EBA|w8qOiS7"EZ}[oz8`SYoy|Ǟu9ZYY}W>؞O=@ )bɺ#޷0 &%(C.!1hf+TbnblKki˼A־#G fR8d=@C_m/#}k$s@ LP%!c$$e*݇lsUVi-ݍuGAFY^ _3_;I]j ' 3p#R./{M"U|}Q[a8#Q}?,Ww^k.?>5t_eLi$`21.%0zvuYANvvvvvɻ[UIU=sW_^E4c "46V؇nd"d Ιz{4K۱F*ܳ4xɫ}mNiv_U7dkNUggdy~W4ӿJqGZlz '|oZ"]v=rcØ0uA|3J^|8/l1X}>7 iK 4hN468#9E! we],q,B~xul\"S~̚䀌<(KbmPEXNPTd%YztӺ2 f B qm6&74#&u%%M -Ͽ˫N,e\-_|ρ80>S]}؈ܿ&CpJ  B*f玬}1ה˩ӷ"Dгa,qq1I䬱=W݈sJHijDJPٻZҼ{<]n <73I/*Iqw3E+䎁UDHQ1]1{z0DZ8o_{+CiZ4NWP8bDa˳&'xG%^@ gԷM`JE8S5߲g3'(U mQkAarN>:݊Ql1Bh ! J 7gG!pD>s9Nc]NE0(رX@eʲ4 mvRȨmo{!6j#B_w[rj/Z\!\T&-H;g !VAV%!=3EPQ)jSE 4I=aDU^U@N 1krv8(bqmq3=@VEr[F͓IlvFlO;9rFC#cuzO"$¨ "`zr6yO#ݭ w]商拧QU ۊ~K͗ wnv~E/;\n~ ԋ@e75_>=oh;&枌՛͗h t@ k4RQDiǎsY%63Kpa<2Yac,A5ߡ^ǔE6m6z/p!NS%qZp3y](JF PЌawqK vD!FQS/^0 q@ P H2.Io,Ejta c]a٣E%ܾ>!J/_@I쌺rIAa ')Ñ /  k= en^}΋!Ìo7;X%7֐f2. \ 䈐*M B)UK"aƞa!.^PTq[#oUȾ5 R Q"G.,"@  2!@  2!@  2!@ [bE^q*YM vuiY兕yf;4݇!^?2Z0w(I/)Wڎ¤eȥ";:-|x" @Wנ7bC)JW/J@b8΢H"xË=vy^;f6>tՋS98M|Y{ꆷ<s]oSmb_­~k{E!V @|zyv j,"3UEɖ6 (,/˳)4U/1v5sya! /Elyr,Pi>| KU-:4Qi&nu%JToQ$zۊ_P8oN ]cuA )EK"\+8qN.3䤉N`dp4H& iβrZcFVU* dž]S7M _{{O(Kd]U L%F&]g-Iq'D¤1o#sɟ6թbD8p} ^䲎 T̂$ yl|[S-=sՍZ'[YI :{]&EHAPXrԾh6#Ļn޴,ux_G=λF77Z.8{˿+W9 /8t/To]:7dùݻp]鮹;'2]dko}kfбlN%X~ 7_z"UPcxX3N O@Tv%G[RK VN-)uD-shcst!L F0@B! H!Tr:ft7CmCGAFY^ 5 yXW/ ƽ,R.^Q <p *UfiQR#9]Z$JMX_U@:ANs4F9d4w5,A-9%k.{铷Oq&ѫ/_T]f2ߵ{Zi@θ喕y+W?Uy:._^q:Dvg{?鷓jxs. /"W_dI담 rh^a%s=|~$=ϝr];7s ׯx)F8Kl|SYg^fubIA , hT/Nr^*+vlQJ!_Y}Ԅp]{(RWF>?c㢼qMl78!ջsD_6Y?迚 v_CKpS[[HK WЄw"IUH|u_I<(KbmuSQO8ʊlDœ5 F'L碎#=0oпy'|;_wVl0qNtmضoi7-ìJ]bQ]|KnZMysڜ<Ŗuu|:0y"B6l', x:Ù8u~a+WܳaU1"ăsOy^!WE ` ӯ_7S4)Gn .8eal䄋{ LSs0)ogc13I"g&w$,B*f;n:Z709 =-D 8ogU"@ǐ}$(9T1;_S O|ÍS͖5qd(9P/Άc4:;bf 90Ƿ>e'e͹ꟸQpwm8~T,9 Y!{+%t3i X ūrvq].vbwo;wULWmyu;7]軷:>}ᑧ>Ěc;- B 'A#"zΓA!Ψo㙭XF'+eD=S @MSvv7N,( L?9,Q82i;ȤUfP6yhU;ҏuNQ<3(VU]ۋ^؉߶—u `5l6Xݻ2 MS.`y(z]ka7;>#%_~C,"KzɅן{SWp+ExƏ@&{wxFMhCl@ u aEQ>##jfaǬٙbXt/@YzHFe6d&KBzf8>9nT]R :{jq"7 (ތYfND "2qRDB8 YM]vb؎ƶέR' gIK@3].͑MɐMDYO٘QYgJEf[-N%s8\ӗ)0"G/!|2=%(3ӄt,vx"oϒZedǘųhv\>}k]Tr*1iۻ׳zFZ\ZP,oxsKT~oQ Xw5Y9I<8ЅaPieJ rƴ]p'M }k|.= 9@Ew~\&a7y }7tgwA2 h=n槊4/^CFϼ 5yp#^ؾNEZ}[Ekѫ]?7ٟ=,j*_㾙 qN@G>(O%L^!pߧPYxzfuΏ<V>F=xzc&Jǩ!UhŋF6LB"0b3IRJ D R (_QĶ5/OB3RF.AZ,a(ry+6G[H[#@  9XY4W`KV\DA'k۷F @ Dqwk@.ʸ,s'VpNa.pg̵!N@j;ZX5t5&F:FC}k@ aA9@ aA9@ aA9@ aq._19(]SSSSSnͪeY2r!/¤sD,j4~Z(Z\o~l2u ͊ǬUS #:Ĥ酁VSh'w9%v,̸hD4 3ώY=^)UK3g^CՋW(hlnU +B"kŪE+-j0Y""})<Ffz @^\!#DR>"0i{_ePL*WgCm.:5AQPԒA!2JUY 9G5ºjzpW_Oϟ}՗?7Z5]M>yEb˷lp <߽K!`~t^SJO&F`_=ƗZۚ…]p?d[sRgn|/ (B7}?&쎽o~RzN|R]z'd7jԚVO~3' W0-;_XJ_?}*SѾ_{eR~ۙ>S7^|c|K?۱)ϓ ==PO,! RM*KRV/QU55e'LZի-OS2u:2Ov`bFyޱxT"޾7.OZ^b 8Ԭ^Qh՚䤉aκcf(XAκHg Uc16G$abFNhւ@:ANm\2rEF&/ԑl L.ɂ5YBFYZF9{;knVܼ9kO{A5Υ6h?GwS}]>~׫xw}W?zpՓ+]8ounvO{*2՝ڵOaO}MBF(ԉQdWcWs-oq[r=rΏן(~~wl[wn: ֯_De?zՉ*$ݼAWHθ喕ݹ mBx8wy^w^^<߻᯿^cEچu9S@۝<N>ϻәuϮ2K%؎Myl`Z"=GWB ͎REJÌk8C!xW]mw&= XWtdLDh\s$PtZaOx[({R4y9ԢSc&gi,mFEjlե%JƤ38̄QxDB"1΁TQN055T9?[SҜ]#DM D!q7">iាe>y/8`i;:]*3dn Luo"7 3!"xb*3tG*hy~Й߱tYwx:]ĈB|tE:8h6u}Jt5N.\k3[y_i٬fnX&>=o|x_|+ >yS>XyXBB :LŒk|gGG9,82B`rT ½}E>gS 2128!pe],q,˲1oNPT,D., &̄2 [" 9#HcH>̤ IDATr{en\S鍙˪KX|Y<:uKEaj3W۪/~mMCM R8iZ?h@;xgIJN l˫޹G߽-9 `x#ZK@! ?YŧlX~tg/'"z OOla'Ny|n T/yoaJE F¼EīRT+ 8S! GpͤSUݘq,ReF%(Պ핐ESk1 R CrD; ɒ.iW>@o$HbUR$+J(FI!ڲ_`8Nzɩg/K H6j#BN4Gr6&CBM#doYR*+-$-M;8@p!G/|t1|N}hl+<*usO g0>a䜍0%+϶7q ىRk]d/L\teӍ_'VOE|`fGp-!)\%yGeN}|g_HNy(3;V)Ly>4>=d5Y$i.DU^U@>?oi&HO6G[s5iq*1iۻE4uXB8b`ѶF"f!,lN}>Xyk^=r9m|cѧքJ}Xӏ\wOڝrPl˞͏a]/޼x|Be=% h=n槊4/^Y"JGwANA9='܄с)6iphF }7=i g0|arff ϽbLZUnfA7пO; g&_}kٳ,A<&7qzEX^Olae+u'7 s^L(.XV}GYEUf Q:Ū wQBOElV'B(La|N_pY UW-Q3TVٸ"bc&ڎ6P*6r f \XP#02BxȆISĻ>W>0W[He#Eئ"7Y틝<> 2m;2Òg%@)JWe7MI2.Iow,E׊ƋsO:%OϊJV8[ƙEr@SD.aD{̤2$i߼BAB/T{[ўxVHdޔX>\` V\ۣF 1A.ʸ,s'V#B"pVJ1a)o"BJH%v\/z- ٷF DoUbh@mH%@ D߈@ @ 78@ @ 7f|I%4rڬ2D10zB%@XP"P'77/Ш3R؈(aԄ:#:;==}xx8aᅁaC*!Jtf}=9qes84v0BsIPwYFeDϓ{^ R'񡃯^zhƟ?BS(B"8O,&B4WZ!jyDhGLra0} ϝ9M'6,NaN׷o=s@*&|yw̾YՋ+C Xx>`y,EIcp„ Ñc:_SI7:F9 审^HU<_fYOO0~˕g>oMϟ|0{1'?v:}>־h+C bʱ66z-Ԓ,{#(b*9Y`s>\5i 01gٲ$ m?|lgxPpm}U'l2Ə $.ͳƘ=yfQQѭN>$]|@)JWe;$KtM34s 7Yt>m3LVρ?.;@@\-{(W77Ns}Ξ;v Rtt}1O!zxf L:#3֣A<]ntaeoA;A/N6aăo*䜲R5kڅ HJPٻZҼ]G?^^UlgԋVMM&<]!LhU;ҏuNQ<3(VU]ۋ^؉߶—u$ !BB<`*9C%$*!sTUmѣ8L]f'ذ1@.&v&用ݍ> Hx!ęz%H9s8t8t:Y)=/͛ q@dϻĬXM 1, )bZHf| [rEzY?ձGVUpVP3 LE =1n HTU+ .^@VE ,ɳf&WP8{C^6=`T'mϗ[>p>sqosΏrNm+J0<) a >(U21i4ZoX7o7;X%7;f2. \ 䈐*M !j%j밎iۻi 0N s oUHa an }k@0555@ ,8@ @ 78@ @ 78@ @ 7¾o yyd u]E(=o?(V/W75k'߲1716ե5?K 0 tu z]Byae--ag=f [}{ct|<8ر/bI+-ʐKEi5wt Z8!{}{Ӻ\y?KuG: |Ü}"CQLj^q:%S%$ p.g,X 3D) sĪdk{cJ G: ڄ-/QS"(fey{%V,ow7:[\De0 U_a;es{ѽo|9U.v,Pi>| KU-:'N}51ZTjɹ0 t\Q 0E)ƶ1oe^glX s gEx'*rk$H sÝF; +wSsr ti&uYe^;Bd ܪ䤉06jϑI;򆉲Dۈΐ[(@TzW3w@.ΐ[9G5ܩ'+>>Y4x]Hҷ}={IK"Pr/O]Y؆:=>µ;Pqٝ:8L n/G3?=YLC\-{N?mw|~pf:UN ꂟy4ӄ<-~ݩPj$w{&̽GJN]2 rҒ4̛RKWeDR&rv(&؊:D l#0)VO&Υ8B=2hˋH8oz 1Z#odԪӣ-Gl|_YGę2hK_Ȃ;w + Uc}c | 2vP[{AQBE [o>gzibrH%Bƺz_dK7Uke, QWU(A:fOaե%JB>jJ)bZT+ĘfPκA Ju*eiQ@?h;֗[r欵7>y;V.ڰ9y~N {wD_Zt9'_?O'Sqƅ[58WՎ7.+? rfE%DVPxf@5(XC _Agh?2磌3J(B!}`RvIԵarsVA}[}]HB^vO;;}#0)Ei `UMͺe 0T=qفqA&NWXz}3%9RgPNONJaHTBsNT r-LLO,<~oP!udp4H&ƉDlbHc|(yx=$r95rbO!@,&*Jv23p;5ҒUb`w4aw lM0(˯ȖܦL)֨i0r 0O}giXWۦпzI٫ >ko 8mG>1ʝLo-( ΥSTR@TGAufK*@Of}n1S>opON3qMyfQQ=R_3?XTZ0̈́ d4(7sH,S{arPwȣ@B03Jw.0y#09CN)`=aŅwЄgDU صͣ3^{1͒\Pk=G`=mu,DZ, cs%'<0V),'2 Y"cc8g)V= 'WJ),ȧc:Z7$\aBT3T֦!4/dECOB5>d!sF8TsrzM0GAU_ڰk P =T\s__F)Oqs{h?v©k䜲R5kڧv`5Ū9N6O"V{P#O%!ޣޱHd,*!igx 7.(LȰy40͎ x3[޷==uf!b\3 tn!O+]HMy>!QT}SK!%!5-:7"!f0 8S!B4I;4 EQ`oٳ7Ye3m>X^02| [0>cW>*||?.,Dnl|a2Ffr2LN7Qx fp`[.ʄ.[A${I*5nd;]&m{I*+/To 3Lp<0fKJTrJlwI2T3=\{!Dl=mSvbpt*6Y0w\ %1=3ELђԂJJj$R%4%2=3ELᄴ(A&~9# "ISy2'K $CB>,3']"_ZAuC,JdbeYI\z݈_;)dT[6 s: s d:óE =1no^Bޚ!(LxRk] ` M5kZ'*3S%@QJQ+zLi*r-78̰ Fxv;LWz[ yJN>)tb0"=٬H.{Cpj1vtfmHiʵ}k/qCٷCZPsaGX 3?+Sb j++}A<#ۘ{j?&`5|gMcVʭgS;!`zr6yO#ݭ w]商拧Q\F9 v9Yp؅lKz&g?hElV\̀  _kNS̈́ ࢒1UPؖ '_Α΄eՋ<ϛ${T4uU7:e&Ytr]z'0G33lGؖKDږ ./Αusqgwfb^3JrY0NT V_QJ2j;"xJ?jjjq|%{ŭ maoR/0$K$7Dex 3ċ (_QĶ5/EC3RF.EW4|^D'0 #)0A,SDi%+VV.͕( p@ IzY=eZ0N @ Bl㾖zuXCWa6qٷF @ dC @ dC @ dC @ JKʕc:} B>E *TZYuuyjd^[*&7y"+ LYVNkzʲJذ IE9iIbN"BqjNQZ..;.WeDR&rvO zČʼD|\=_8 &;뚹yYU3> [A&A*iDA:ANd5[ʌ1-JPbBtD@EFt `kz*A&F!SWc]AQB<_7Uk;.tZa LO8SOs/fi;6(R0tzGuyEk1c#S>dC)JNYnj.`RvIԵϊ+l͇ 3+K4iHQ!7Z4k¨4Y^GLv$RFGRtpjC̓I5A]҂9 Q {s|n\rzZ =%q~*9CejmJ&2'mɂ߁(9T1_"{S{t !<_du!Rj&;A7}t (MWK ~ȅ'>8 ((LSřr$v @`IU9L}]=J>ͤ,='vl!-p9gQ8yNXKU68"Q▰C`R0@ XFt2 4/t*{Ʈ{Zdɲeٖ,'1@yJJH4 iZhxIBd)M@JBHJRJi^/eI^%Ye=?dZΕ%k矑Ϝ{s~~Lt'"-Uހk]jWxi4>{i9 ggeqig9+[tR)'VAL2IsRsC0 >mgTsӱs>'vHЗI#8!W7"@!`H&+SOPӫa__1olsK_fAJhGyuŚ(R)yV`˓-&< av@R+л3v(spO39(/*?G5%v+u&JbVJrvA?U,ƂmP*YعB`ԞK\E$J\E_e+ lx*9d"'61xE}> ~5M/òB _N%DPj{bbbՙp`r.1\ vmJy|JQ] [cg4;QѱR:;0Itx9\ЩcJ@X@7Vnooܬnƀ5tBcH\*nqܟ H UzytcS.uSQkا{5 lV*Z!( X$P(gH V`]@ 5;wPm$1R4Eh}kB!5[#5|4@  C9@ Q9#h( Œ+h(3<Hk}@ o8uG(W=x^ݜ$,|oP{@h,>*IXhhbLF(ҏyn•owmw?xb5Zp+1՝{B7|_|~V[x7>{ nV[۽LrҾS=usօzѠaUr+:m8WYsN1ݪա٬8&6fVH8r%II`ւL Т>N)ѰfPmn17:{쿟r6Q$\ |':5<|[І? F>Cwtٰ\3<_6\}?|ы .Cەoyf? ]qo|7o%]~׈-p'Ԃ(;}un{q~{#lߴ0IDM7`{+У 7"n>4Luzǔ=MxXĚvy5(h ERgPF%ķ.ˆe:CFʇuO2IDR '_P#-$0:އ˗g "ǔ|9s| %xif;*6JoK9{DBknK_1p*YyKGpw]7W/<~̑nJ{{2=/\Yq!7S;}s׈F{w2ûOw8c?o^ve9Tϼtڑw~sýϿt>Ï\7}ާ|XwW ސ4r ToJTr¡uڶl*n"4*.5lXRK3SWcP/1vꕼ6m"cb-(݁1yDI4jZ4.9 Xʓn0*"+s'gV  ,(رu'9VZ1_0m|ɉWgWSX*ksdẘydt4ˢch,VNep᪛ *Ԋ=\e\N[fsW/l0ƕ 0 H a`E]D!M2{S;N|G|ƻ}^ceB ^Xbnh;u@?2}VPSqX umEѯXϛ7^w i8ٞ`"s3mx殸K]5g)بZJn<;nzZ7CTk0t+K[ _p顳dmJOBup(bsj*fM[:CjnAϥf9Hpi`ciwW l:}$eJ) ; `uzR68aDxlӞ"p:b< &%WP-,{J$"Haɇ TO,>[g`M3QePDoS@zFd"dd2 z`wPbmmqU]uӭ42u [}Foږ7ÍwWw6ӭ/Bfh'01D,8~ӽy׊$h P9ړDZtwI]V_fǑ4e5g|۰׳wN{̯Mp(I:j F 'Fwz1(nwbZ]d# 5J6 J̢h$o0=3' F dl>l`Y|JP9x*{dgL_ibbyDc{ҔV Utl Tuӭ44?>4/;LS6WsA[ w!$ftB !)g3v(spOk0ܥbX|xs^{c?|lwu I"k?Ffv Ŵo 9>Ux>JtҌy+4@L͆ _bވ^:^ətyC8bfHfNO0``*ba\e:*]R;* dT:ƭ甓&>& mf+uX(L*lW<9B59ht6Y`x^lZ3몕~tnntn\I|G_:Эwn[U_}OkON1L_(2}~}ߚz-#'.}G{;G3siNGzz,6jsؠIO} ax9}&&V:*ҏ LZd75bI| 6f45B'(T gU\ŁbzNAXN`]euQ-$YcG⻟G)(2 4cb*]y*͖Kp48P dD+(&H\ %bQD@XS>z|ulFg?TTּ _{prTPM=' Ʌj%_4kb S {^nvmpviWWk|>tx9\ЩcJ@X@7VnooܬnMW;y~+xk XK'4ɀ˱\GU-NPM*i[/nyJ%_4kb u'@Z4Gv7J^W}k( X$P(gH i`=, D}_yk, ~6_U~(i4B($T@j ٷF Bck@ @8q@ @8hq@ @8hq@ @8h݋z#t=HPQ#\2o%; }9{&]jhO= 8rև"Vfzf…]'[3y H_d]=>,q^^p'ˋVOci8Zti|*ti6 --sA O7nc)!S ʌgt9GVʣ.G+Bڎj+̶e/"T16a@NohPdbA9f*qtb*̈́#mj6WmF"[z"E}g~Wm/P#RoG^G0E}̻B0ү|Q:Tt+q=C]ɕ"@t\mMlP{,429ǜ*Ӭ!قF$8p,hK]/%ֶq8ӕ4*1'tuF,V0DtFBAo\릹m!_6w&w㳯չ?uL#wwp>M8Ss sOI.@. -lv¾^9>{q~{/7$OqwV ێ=>ZQ=lnVO$6Hzny hS\9ml]OFW(txb+'z-%_RjA8g˳)~FBJf(э+Ah%uKl )E*H 2A ]RKiJhއm)Z]\\τi-Cy7eS 2aCz؆ռ^rB)@%I; !aܘ (h<+w{lA1my3V_WR0O;]1.\z*_˼PُYL:%gCn"c 5D@I!glT[\yd=Gz$[Kfl*)jb)3qV'}@‡9jn- "$4'594 ovz/MnޔH&+S'}@L//C+p[x=Zo{#WIwV +|VSH>K$v+SLNQLAe8lC͋&j)_~5RC\`+D!FXTPݨP%lѱg4{vH*nqMoOM"Tkv vG|59Ӻ@Z(psJ tBI];[|w(h†EhrfjIzg wʢ ;s0,"v<)#~Vp,q8cZ}ѓ=P(5k9=}fqY=qiE;.MJEv;[#Ft{TZ8A5bzNAXn%(.gUkX0>Bw,Bc#\nx] ai;Y"?A}IhQk X>.{N 3kyǏ .WfVI3zK!10BK@s\%biKD,}bߍ5|[!A@:e9#ꘛ:iɩ "d,0M|il}v6ܽ@igb^Zq@ ;U6Ӗ?B[#43( X$P(g%n9 2!"iFkA7uwb`f&xv~'ܖiw~dl @ P)dC @ dC @ dC @ 5W0tHyV8мn~ʋ@LԻ6:?gɕ=0Ǽ4f ]^Y {|z<Ј7ٴtT_Nҩ8;TѥK=q(Qo-;u+)=̯Z&!c=_{埞{;{m0kf䴩KV/*t&g< .,iE:f?Om4JB晅{CӋ*4M 5|ϒ429ǜB>>vQ,lGm4`I#`e414>ܫ^;~Ov~n^x?#ݔ|*!؅|ۉ}nZv{^w3/?ov_p/0ո4fGtB-P` .=3& ݦWqaÑ[zNE# 0#;KK&1_0m|ɉWgң !LnN4MxN!JQCԤ-w'NtѼ8u&;DK':ib+.^qNצQWֲg FEdej}Wa0\-boz^4٣qds+T2pUe~ҝ R/m%`e(d@/>uiqėyGn-;_/tЀ/Z-nSיt}#CPvV`[P9{bMW֞7_w Jqy7P+?GBj!^΁\^NL @ Ýͩ@,Jwv܎-A,(r>Ǟf)iVx *5R&B@^(Xrdrd< &SLzb#Ks٭86쵋V3칗pTްz"zZrƳ3~gst $da$lΕߖt_V3Q5p$ rYgͻKHgPE\^퉱]jިpr؇~yx gws  Jħh<9d* Asrܷ4Nk&R7wwI]VweĤ$m~v#$~A.j$Sq!%3tmPh5->m!!P),d?1iQkk X8z9(\-oFpS%. [GY'kuZiueW^V3al(f/BY`vdRg&ƾqT[ҝ9D;]&4cOKu7`Z9&o0,y= GI"k?Ffu R=`0 #^‡Kb+W =U赌sMP:erf @M!ThY9{@~*x"rlޜn@q%C}j 4T WY:bU$j^4P=U,Yz-E"9s,|([ҝYD|pϟ]%\E_e+ lxF s# ~WR(rk@fV"s输'mc:(ř({']Ӄ ɹBFFqD!Tx4GPR@E2zMʇDžZ/⡸][B(ke<{G (9Yi VΧCp*xdU`MTt10ͲQiRzu;v"+Wt^n=ˑi*dʳ[A;+l;,~V_ͲԞŝAE{ %?tdy_Wws_y;8oyd&P(}c tSI7 O͵Vf'뚋Xhn^FBW1C1*0P$ w9(Zj\ 4@Xl%~w,Bc#MT*t%9J,ZΙ&⩳l7P"s,8kqi*N\u k<^&w4F:i=ːn ='F u,Z*XplZr9tjf}qN YN3Q2P dBBQ >z|ulRߕ4M\!=~Lv7JU<|@qICrafe 4JĢ MJ^Gvp:榪)o4kAyȀt׵(eӧwa[~W@ @XvAH(MĒl8@ 6Z @ HZ4;ImS[#@  2!@  2!@  2!@  J986.9N$Z~o&M3>/>v{jk4J986ʞYҥY{edѰosmym+RO qeCTAgmq\oR:#(\=<Ј7;gk 59z\^ݟ<=sjr'Pf< 9b֤DH?bD晆\X'p=)FZc`is}rKPv6Q2YdC %o,GUZPթ_+A|:  #B̶&=roPHM/+lO]tBg+2p2ۖx^ 2H#]:@Z,yk>$د L1vQ,lGn9" fS BV CXkb<&;iKCі'Ӧ*8@ (Tkp@ ^#yp}^_/Nru'/>~ٝ*Cq$H|WVx(|ijg$bp=}"gؾilq&xC@IX|Lӄ JiW R B _-%J1p+aiTbN|k+X]!O3h|^Y^ϠSx4ܝr.`Dq/r5B2/x ,_]#@)t.oijO"rLɗ5]ba8+&D\f+r"N`"2M R =B57O%ٯVMH$Mщл*@lnЀD†! XA&O_s7w~7 Yq=op_ySs @io7=zo 1_0寜\A<QBuC׶eoHt,ɿɩ+1Q{;J^G\Y1l1 i n|R$5t-LMB},k73^EuwBiīWC:c:3םܻZHꅍlά +,h9\w Pz8oa4o-O,!*`@30y/ϭPU7W!@4  ݦWqZ¶vΖׄ6VMZa0Pe+C8Dݮ Z$nxSo9s|Q7oώҿ#q\vJ# ߻׫>x2ƳFHVַB W5:/WNN2{6%1&܎-!5ez RĬ W6vKX"c "V',nVO$6)"BX]z1?+=Wz(*m '0*\GP`ӑ_q1=:CKT ܩ\#d2L2L2M=&NQpj j\+9\@k\5@9CEyMsv,SUyswWʅ$LOS,1>_'I_}O?7G E kۉlK+$|KOQG u֜alj^9]M8 0:ӋA՝`NWL,{<@AFYن2G;/!FJӮ91s BuبbUfF dX%_T^;BI9 ʪ͝(TX'Pgȵ48n(I:j {s(i"ijɜҳ:jz.kKWdC F+Qw8 zgg/w O=_>M&7|ΖHănS]&4c$)t;SI/BZLC͋&j-0Ej56~"QBX ˜@L#(ᷛgP\IPgr+AM`!%Rnѡ@MnzN0q*xՋ BnYBy#,njyds&ZFM}|@岂 F$.v͹7HAQmʐå>ngACƣ9BFҘ*JǂB]+NPѣ_u]z%HC %Ө.W/wHGPR@63IgD!Pi'93*{tBBըBA [LiLo+Tj3>ba>u[ k"}k@ ګoCNa! AH(MĒl8@  1;BR!%_u PHc)0Zd@ @ 48@ @ 48@ @ 48@ @ 48@HC)p@Hw[ ~C1 (;'O8`zg~%w uw}_Y+qލί/]]y'^s830PwzS.UJ1puל91)npecǮA(U ]'9%d(u'\Pf=U;y\޳Qv4GO+ /Jٮ2r yzVGJao\+rZr84YJf6 _S4P?o}ڏwwjfxjA\YRʾ# g͉l[76UР>tї@qg*t'ڡ=-3#Z'GSѭ_ڄ99:, E~Ea!XP{ lVE&.+Z/2 KH!W\wqX(?h){C9Т>N)Z^m;9o^N%_m|| x7olOaĵo<^JOCwx5OǞ-.}vndr<(R~v/~ߟ}+λ+/V?zҹkDto狏Wz ]=ܟ}% Go>}xϙ;KLy?wy=oݩu؅|ۉ}nZv{ Th>ks`7 -Ti0+PݩC1P(:d<#TiL**nlkol|c`ڈltawff&8@Ǡ^JQCԤ-wS;s թ .\us|!sߖ&Z^8q7r -(ln]4i|ѕX49g_XA,uĮRo\KrZ2!E=J; 4n %RQ^6ST_F?WNѤ]6fg_͞F[:]>?`)-q@qϩ.~ %77@k$7CTk0t+K[ @e FFy (!%v w\UX 2ߨAcKg 7 ; `uzK&$$IXP`ӑf:P"T? W6iXid+dLN_, bY:luS)Nq-[5lEE=:Nxa# '`hS\9"J-&lD8gbb >(nq5rP̿j[ 7,y;Es ɩ'S Cc6t{GQnIrH0 g4b,'|Nwee_np@\Yv4 yMe8aϗT:exm64(i"i*{MBH ;]ZT4ᗱnЎqeCTق*WR@AFYNXǥ;BfMd g# 䵼>a%q\ȅkrHwKAml %PGm!XRjNJfya9BȬBw~yŕt}&gI!L ?b~}n2x7Δd~›N얿O=Ha%<,! @Q>ަ,C_c@__1o1ˑEk@dRab=_'G̲-W#3=eToxS&gZlc"}X]4(R)ٟѦ@ϔakg TRYidQV}QIo\Kr^.,EEЪ FXK/&l>R͆UAa6uؠCI9@ Fso m=N=N).ftq&aG@5{P1W} 3k z|NrK~/Rr>v{NS3*9"}'O*zNuPy= _Ѩ]㊵v~ 7H UE3ρ.q(7,멝Z@z8|y=*%v VΧCZQMVAP2*rer*V&+;[R.&ՁeuT҅l/gBnAP+ՅUWf ^JMHPՆb.U`ȶݼe>PP"s,={iMc>$w?K|կsOw`bھO?o0=w}##Go}:@Ӌˁ=_{~]߸O\}(wՏfRIz糅ny=kze50nҿ#u>%c)6#C%ci7R-Iw,Bc#\:Ŕ.1OWUBP0o]wQʁBqWO SkXƢ<.M@R= Jf8c™4q1\G)kBnU)lкJXl%Jf9a@@c||!TjaGnҟ?lUckĥ2DŽkzR43(9iH.̬5x& ,М\FX46Ikz@ cn` Ԏk76G4&栉)(OӔm{l8@ BS6p1R@H(MĒl8@ 4FkA 13[wu1RHzl3R-[F$@ jC9@ A9@ A9@ AC@ ٽ }aw㳗y4}W\ <10(;S_Jͺ.-8l6{"+:i8=U@Rv;_ ,]g޷ВQ\أ WXގrY.g3u,uM䩇F4ْ O(ff(5(B9B̶enӡ 3|`૶Vd3x̯M!MF޶Lo$8mA}RK YґO]tBg+2ixjA\YҊzF$dY@~749Ƚ)(Tk%o5_429ǜ*vQ,lGm4`Iī3QJZQD7M@QW't&OLLLLzyg;Mfɬu&;DK':ib+.^qNצQWֲg FEdej}Wa0\-eo_w'_[:nB.(4#\N[AנT Y!ƳF@+ ; `uz 6I?ߛ׎,|_~zgz_ϓo1EZ/{#Gjr/rZ__JrK$w'}c.U72@h,qNQpj@ %k3nǖ @op9rK_#bOs4+|pi`cQ !zRdrd< &SLzb#Ks٭86V3ipTްz"zZ2ֱxgst $da$lJ-ѪҜ?A2!AHCfӛm)Z]\\τiLՋ9]R̉H;ss$ rYg͙5|>6(Y6y(>_ Jҡg7B^SE͖d7!d M @y敳ȓxfhET*gX8z9(\-oJJ]*JPO("ˮhv3̹l(f/BY >=36 5|}__1oeO@ Qܽ^/\!ƿrżBAc\^8 ;{S&gp Q4M(,J'Ri (R)ٟLA6SՓ3v(spO39Y{i+LluīDI:Լhߡ{!DYR^Ij֭X,PeUZ-i|xlx`HX$6Ò!,G ,H H I` M>xaB`gf2cS-[e]![-lv}ϣz"J2OowQD'Q'?عѶoJh=w16:O@R)?g+u(H:C'N(57HV+HTpJv'9LSK+tB4rq' 5z:))h:֢EbR8 -2I`,AA%MJy)δ|F{ԙܡ^[2u!b3ԵUEZʺ.EIOHm} ȲC>s6nd^X<Of9 fϰ?c႞1M](F8fPIII Л39ݟuND?(mj#_dYmg4GBre1['}>!ʄ<; Ls#}כ$0zV)", d`x Ӝ%țw.i3d΢(V ]M5y'[4vܜPW1apW,\B>Krٛ NbogB0P*F?00Lᑍ111Y"%x*!>$[GEg6(;P^mM,ѴF ))E"&g ^`0>otXtfb+RD=k0"[`0 6[c5oMSL ma9 `0&|u(tBvӈ="[`0 `0 `0 `0 `0 `0 `0 `0 |O_{zw,OW'zw@ W@|㡷tNj _/ZU^bTHE(Hۗgg(V5iĬwl2;(7*89*{c8td : 4j##S`0kw}g}ۧ򡳙;yү!>\1ʫ3 k7bm3;2!mu`7'փ3gfu ڟ4rGs-ꐃr`༳PtHYS%'Js0} ڦg7ثzY{n{ yr4+ksRWehggӓ7cO_GbwG5o}O>oMAK`[cWSs<#_D!W=Pwۿp: r/ nsA-% X"/5{M\:RV?5zB}+&㪋GjD/jy`ur| -RR1c[7oF&%|:LQ,A>K BY KJp775r)tzR֟Gل%R3m5xs @HoM/t/ּ<^׵%c>4V?j#@i?}OvS2mJ (5rAH*4J9K4цk MJ؅#KAMKRGGNAek]y$EJ噞Q iԉ89Ώc3g ;.F/®i!|a|OF@\&aŻ Kt-j 2uYxMJ]z2` L;`ybo+YP[/k VT]s-x/C!7g7ثkuz WtW;~ObjH׊R P%q-No0uq]H_=٘rMPǭƆrX=O(u`7 ۷;#6FA. bB! B0]Dn3HȤC20JSہ :LUp[m5-9r[Šw!Bʴ͝UEӚ^2t@MBlAuuҎ} z^~z9E@=R9!csE'5/K\_De]**im!qtfӿ@(u.<ƪ(o.ۘwECQ:n56(d?~!B$NI97fu⃳ PIDYo]u9Dh6wyԃ &9U pb8ԥw=qN珯km8=_p](`=&dlx!뚵ėH IDATUdojuH'X7, oZL!zmkvT* mjM!XT!I?C%-u"̢V]):!$!ĺ'g=!聆M\!ӴHL^U[nj9̾Rf\(S5VVi5 ȘW,\jU9Ϲ?YַBխ*15gtѧ晊ѽ}Y+f"NWvRue:%qGLS!Ԕ-\'TDtydڪ I+D[B>PplrM"R׷3L]Z`auHlȨWuD<6ݔhz{Dx6#^Y.ft|_.wog|Wݚ&>Go~iS= s_}ʯtO'?[W%#+Ɵ俴۟=z6Yl#Y[Gl,FW|360. BTc,ssC^ ;̳F 40DZzSONV["%ێksTv:LU>S>:y5[%,@fR4`0|[) 0˝B -*nS,@y}!4={"G/Tφ@uk `0bW`dD=k0"[`0 6[W-ĂMSL ma9 /76881\(tBvH‹ aukٸoެ`0׊ ?`0 7p?`0 7p?`0 7r!-^d1n_u@@kB$oٹSQAȦ~i҉귿M;R%:/K)IV}#23̛kK wȤ:s,o:#U"<uuGd \gJvQB8*vFgZؖ;Ou%X~</׊y?F~s0``򍉋ƻTşo&R[>RʷŁKʳިc3g\4ֺr 6<#S VTɣ[~qiھIt;?f 7LY?6 X.!DNЛ^D ]JсS+y{KMM0 ! z7ƹe#W+b9 ־65P47"`0nx};c?燿o{GG׎* s5-ѩ{ڶfmKW"/5{mcXXV굆:h%R3&T @gO([l%7Pt^XK뺚ȒQJRnMU7<U zix/dAu`ro/e֞h/ʺ:m8ugB$R|4#&7ƇyWBs0JHK*zJb}ߋ{~hC(9L'gHN}-5$q-+=IEl1w~K03p5mbHXdShtPϞ7:UmF"6w7W5ue`cܤ5r >;9LAxhE ׮8LGNF??Y?/bpߟc_:rGW uvj[]$ej] MKՕbR$SbBdYMVJ]zRB"(5rAH*4J2pκ*gzF)+f!qxw ?8Uܐ:ݲ!IД瓍& ya|OLP&'E;Ilrq9[0Y̹@bJs7)3)`oMgE],Ϧ3PeX " DrBAT!U(K_) mB@r돟j^6GxůZ|寞XU$ֳj\Qu !3!VL],ã 0^te(mn ?2msgUgѴׇcr}6g tMPGFB#G̐3Yg)bB! B0y k@l\ 2bj/R@DVsx$xKިq%xrj&VcM3!>{ p%.Cr=s4+)`GM_E],fZ!nM`.x !(o:ߴnm>b0ŃG;?۱[Y@T]ݠڷ5!@ r]3.%w['s&{c`Jԍm /);NgR7MҺoX].zTQg:[! (u.<ƪ(o.ۘwe$B7+sk zD(.ނn J5:}q3jQMc $ [JD/%7#Y)!NO~=ܹ.I& XQrQQFHda0Nk2[I`0[@[}ǫ_jW>򮆟==TGnj|s_E>h1(4՗!qҴmM $Y"ZgABu-NάBrLӄww{BzKrdش:t!"eB3%dqΌ@PeUmZZyjm&E2KB8)L(x>;sWB8O^7>^?(\E^@+08kDWłl!ґ (-)G` )եh ؁eO>w/} ![pGd>@ooR<˓39l-WF/R]sd}n%.D Tn휿Ȋ׽p1v~Nΐqˉ4#iz$ڑk*e'_ȤE-DNXR4Ҷ뒇k,$& 1^mnP$?0%N(O>t 4.T$o ;6$Tbi-qu $[hT 3>%$N}4'xGqR$b~ͮ@UC 9@26[P$cJhϐY/!pr:b ]"Usўõreas0 [W.FK )!Em9yb|4M1PE`0 r-a3oz~6>K 'd7$l][ƚ}`1 5x`0 o~`0 o~`0 oCsC`O_EKo`p1/`ӓϜXcyo:3ҋ_/Z>RǿKzw>6H^|ÏV<t60"U:Z!k eّiL. k)k#?(jT؃Po;\_c`7g<LV2Nbwd]3B4 Fg~~;N A!IMGZ,f:sjtsi3!#%>,o#./CQq{ɃKedOt<9Eڕeg zoy]qN)νԃWS9"G֌J8:XPo˿٧ϰGo>uM-O[E;2)MCVޚ o~ljgBwUݵb@cж5{m\y6ky^%R3C?}n:D5zB}+&㪋U1GjD/jy`ur|}&NSnNtҺ&43D#nfVZ. nOZ֣u`Ie[O] 3u{~htkP6JiVRJq ٙ`o#r/\ܞ2]kO]lBM~;O,rem!b 4%G3brc<ȈE%KJpMᮯKxfi"zOm9_o9T˚jHι_| JyoQ9~[u։BYAl2Y Js[_G?]sszvVW%,ODB+˫I0Yg/M?!І{̠}SJ1g}e;<>&-9Zߟ)UB'<LBE XƮ^1GJjձZ)`'7M\[?( F(u`7 ۓ!ph PeCŪc7.O'Y\8y_?quj=LpNɥ6^ژzb 6Q'sdCSw=W~)"45$[ѕҙO(6G|n1;yP(0!))E7!,P>|`i6(+C n\JҬ"rؓl`0ō<6?uB2Řu,{0ADjn.MNMa9e^q|oT@m=eK SaD`0ϼa琇{ι9B!즑W)"``0 `0`0 `0 0SSΐ£c0 `)`(H~!G`,o8ܢ`0L!0lv9aПΐ£c8>m,\6jztɓX֥sd~ﱛMU#/@N[&f!yun.h!)QI5yqXqlmx֞z6Z[L#\g$uhO 5i^u]MhfdF7Ǵݩ[_*Ufs0 cvgH1k?߻&awʹQFrCWRvl9;0uc[k=WPbm3;2!Q!u͹!%=PIL :cS IDATSJ= +rG /#8OI{)La@iۺ~ز%'}U] f}zdѵsq%"%-\ыlp$:~>/? :y恷E+/[;CV_u#_ᒪ~pn-QJR*z,l4ԨJ$]5ZefG I1m^ @GxD_]uzksClnkRQV$u38t=u?(u\)7 bh9Vͬ%:\ܘxN\r CqE^@ɸͻgtΩ!\#D>_65Y߈ ͵r'=C)A><#-Ո^_R/*UNQ|gs0$HE$BBDQD GNS'F|̽[~ܝWӯr sn_xچ3zMBv񕒎ZԔBkX瑜V#LaXi/!yh ϧ3zxx'G% wh1Mq V3>G|"&CIZ#ٴEy bB! B9WM>9E $"p3eR6љ*]D bZo^XfāNƓ:(jT؃u5AڌE\KZ6㊔Pl(tr5ryNoq:pqO%0|M|b! JR-FM|cCq CL0dW( )Dt&ԉ#sm|Y\zc'bb(T*(B$SW;L(fY(/8Y2N =n? O >u/\^@#Qi$gRL.(I˟(שK*.D<*I[/ D&G'MD<2mU$ ]ZTr?(!E\6#D#U+$[$$w.6BVVm[o_wL8w6|NqL`˯-0SBg*a =®{ = z4i`TRwS-f36bӜegU֕-NDtQKQm; XFiScWB ԙFB(KȽ:ok1YϢkeNҕӖd;%`x#^: q䄳(,ssC^ ;̳ 'ip3 8t:$&|Ơ4'kj8NAOJ0ƪ&1zn XqS 7^*@M(J#ػRLցXB )HL8Br +Jg ;S̘0*ݸ+b CzW.C:zrt1Pu2gH\cDlbFOˆ.XAvw@y}!4=NZ!"Ga;c_ i>sR5w9\+O9+ ):ghL2SDw: &2&4)~`;$Й $w Ga׭',t "GH=RxtS&Δ!3%4~wy`2ȑ:m6۷ XzۣH0݃׭a"d0uk [`0 `0`0 `0 ?(HHс%bt GA9S 9Sٹ p;S> `0LHecgבR2tvis/EYr.!9sBv|Ĺ/ },o8ܢ`0Lsx@A$` 4z'7):@܅P)Mn ,?z|Uwe?n/Գ+L=I;] ۙ\'_xn0ó<ċJ}Ng5ӲFe "3JN7c3gfu ڏF 1w~K9ʴ:}ޑ~t"ip4 @B#bm3;2!muPncaF~=eڍg|{iO7>+X(m[WoUu5*EKǦf$eO*%y"6^eѷ]VA߸`q?g@|i3'kw}EG_{uS_sd}]q^S&&!'l} -_?8 [q"z}+O=u>5 1'ru~7h][>ڨ|s^~G^=3'D)Юd,>D;XB;^|gNP5 ъOI]usח!cyo:[43Y/cfW'zw&oE -RRRdieFU*&aD~ۺF|%:OKǺ;Z+\ %l9ޮFMV'Ǘ j jӟ#11>70;D>_]RrX,fI:_E˱jf.ѩԬŋ8urd$s°.`IY:pȥrXrr/ nsA-% ep/ N6q?'H"bI EQ|L%s D;@jHR`ٔ|TM>uݑGλw{q֨K??P={=,x/SԿpou?6%$L~#7ΞzGLW='o|}O,|λyo^^Y˟aӻO.zWG]P^;OQ׿  }!uçڨ|?9''iQd\ V7WVo(fQ!'E3JRר+ _`F#0:Z_)LM[+nW0Ȉ׷';vT YSǼNe:8=8ګ+6vVHߊkjdc6x\  Y36)r9* iT"g'BC&XQkKKj;2 m7 7Ֆ+ S3ʕԑW|ˡ^8agٱ*vx|峏MZ+z<;ۺ @fި1ȥ/R俇\_%ق\{BL(IJP&$q3zDnBCv&Jp')k5Z<ItEw[[*|cmh<. ꂔi;J=5>)`'7-[D&Hv 3!Gϑ{&y~19_UwӇ:I³6w7O呗?!vSWp)@3ˆ3^/_+ ): A G RDqx'! Zcá& KhunwyԃFgqm?G!praPI틛I7zE9$x>E8B 2^x|a(0x R7MҺlXrE,љMAƽKl~N,b]Q"2:=\404m !nBL S3L( @\ }= *?>Hg>NO!EO1l'v .뉻OTN?޷~cIm !{."iZYʪZΨ]" 8 wwDiBQi""; m:)P?OßAu9T%l3HI@xNpa(R8 /BkqrfLd0 XVӬ Ώl޼_lbAl !X& 2!'܁+$T:,u|0%zͷ,s7Y@T5WHYB?5\IO|/}/J?z_o$X{Ehc< v 9sћ_!o5=w7=xFQ88n! =bֻ:3`A29-9N֕-Ƃ E /n}X-u<4'ҦƮ^1+nrLB>o(2H+- 1h]3N7:C5voxNpaO<]lBT4o }X|/l^/&P*F?00 $@LH)E"&g6: @GGD lA>:|x2MLaHrg"]%NxrVP^mM,s.|-_ЙpaDa X+,P>|#P=K(Usўõr+!̂N,,(aLzc L -,^[;LwH3C € CQT%"[e^Yhb  J咈9@ #F<[Mt-d\0pa#ŒUa׭a0[DFa0 ^`0 `9`0 `xN,IzYqDAa!& ^`0 S"NWvii)mDJXSX 5GFcZ]j1A6 :3$1 sXr ?I0> S"Yp:y恷EG_}p>=lXYT- }d2̄ƞ:jȕv(&Ӫ;7ro'kۺtф _~J6ayӱzdȀ ҉CkR{|wc{?ĺekó :rX_$;;sR4wag1:$@YuH77۟ 1LY+״C~90q{υM,$B P: i n N3uc[k=J҅ϜX;aY)r;CĺNGы;? ӊLmɗļ Y,diC2HYS%'#|&HeZ1-MωҶuI_UWYYt c˞"RXRV9!,3$1m^ @G!hٙ'UkK!ʖjD/jy`ur|ܒ38oD[#B9Q'OxR>mgb6B"/DA[[u`r vF%*GdieFU*&aԜ۱jf.ѩԬ% EWIe[O] 8st2lmҬ4NNc'#,/㋐ ` 8kgG3brc|xW8]WhD&675r)t99c%#X A(%!$DEb!r sU$ wu ]q" !%I*qHԺ 1 ұh4 3!$:Z_)LM[+n{c`.f:>sn_abB>nܩ NɑؐM%)M#uJX2,nP$QzӃS@bcgTjy'kvdqsO "EʤBns吽Rר+ ꚤ&ɕ;/F& x\Whl&AMmQVw6q?'9+,˂s,B,^)DZh:!b $S@PUtd3 l(qC5H-%鋤 eN;#9Φ7&ƐJ5)Iwj@{ͳF4|>]< ̙D9yN5g ɅS~y|5 `0  `0 sbDLˊK$$ @ 0QH` `0X^W6mz%tm),!>o  ,o8\'voLA5 `barC(H.B,H B|iqw5~Wnos84a*~챟va¸>,N]|ɉ啕o>5rȺҬP\KiopA%})[?ɪ\rZctySwm ܅ K뺻©c@8ڬ$Wv^T \p, yPxJ*8}Pm?: f%ʢlf3׭?7  kw}g}ۧ~\O粝玓}^GKL=]]{ٶ`keUgOτ`櫺kŎ_|߭\}z"ihW2מ?,A([z4+)XdZu3P*D/ /mj*ʚC r tJ%>ۜfm Jְps]_#BD_]uaΚ:q+ebN!B y : NZPkKOBr]eX_-N/٤Mh;e{ZŻUrU8$ C`:B{-MӁadHH3& C„@2aU.ﻼo}W|Ȗ%\ʖl>?gy{YbbfBidDV>múFWBXk;:Rڝ|K`L$'h7#:U {˳ (Hitr TYSZE&B`R*gq.L=SnfQQ̅%İ,B <"*B1R'O3x?^q3G656666:~/EsM__ݷ}kB9m/6"k?z[ ܉$ͽ />xE_O/͇Q_63l0杜qKu/6C d 2*8$kjk?%m ^6RS<$Q K?xp4Hj} 5եTlF_ߠ]i2掛C8{f՝Wo9@ܛRSj4g+v]<=#yuC;ˉ@?cLɤ$%I?ǟeM1wbFr_k1c*!_[#tF}t卉ޥ1ERu66F|ptkc{][2"R Vi(kll.YԊ"B*sNd$!"*C􊜺TϿߝ|+^_X[o$ِ?X,_'<GO D۟eĸ:zi51ƀ6Z/vp8-x ŻË9͍F+ NxW+v3:{|izb'iרM0X fGr}f19{ ӔU!>8 ‘994լ-INN֟G3IS6'ހƤ> peaIRg!;u8;B2SVĩ@8NRTCiVQb5Q7E&B9٨ۍhN55 ﯄8[ActԔG _y*8MxV,*C]"]n[Ee7a]eQ\C *XAli »郔z5WfB(7۹,%EAK bzFc6RHo3 2P'r0`5Mp4"VkuXRs%Ggzgq#Gau=uh@y;tF42:y(+#g~}!!MH0gG?"TZoNcrXF=?<܌Ҭ/( ޺jq2U~_Z{Y2*skZuhXTTt^Qr Viȶ9uGF5|3 zN6{;3 Jw3C"*T"8T}ʧ[_~vf/_G}Wplo-oyhnt~? $>|]ܣ/#߅ D>Шe"{i}8S: !q29P 54wcK3[tp,34I&гqAY[*jZ:ԫ6]ϰjԴ\o>NKit%O S?K'-7\~YCC޼]_@6UCN8/]ʀR3 *sLZ"S539™XC" f2`<~Ïٽf^x՟/?<<G{;k~|OMQ=w0k7?}Vxۋ2jLl?.(_rCqY0=<+X4I]KQfHF"Y[ۿ;ll5/ޛZH#V8fwaڗ9j# I 0Q̷ TAuOK^ 08ԆC+Q pxFS|&?|ݗ-wM՛}qåS+'<ŏk3Wͥ6 r͵?pc폼 ='7[_o;!g{#3[PbbPMcgڞw{xxxxѵ\ #5F}6ɈZf156ÙqD![|`jpp0Ϸv_ٸkzZ[+YdR!DzH#ͽȌ.$ y(hk!H&{쁩 9Ѱ qbvj2`+9QH:1N6 9:@cTQuK~vfwyۙa'*ʎrL%f=.=ic!,6D2b 6Gkق13;19:f! g$ &r :8â V@rDo#_J^ߘ+M[Zɉۣ=/kʝI7v?9PwZU #wWG'qxK_&Me7_Ho3 2P'0`5{Mp TESt VkuXRq$XL*+ $#`VSsZoVs/=էޥbLvgR>G%BU,& (FOZl4lCC!rզ5H0VogֵVctt(7S6䉠9 ?SmQܳyUͿ~ڟ?MjX$6m}ɣԟ}dž#[}ﻜ0?V'i>S/.xSL 9⋳#'e懥'~|!#tu_ }=O|OvZS@J6~ ݇o|a] >ЇR_+J[ȁp\AIΫj\{LtΊhzGѵHhsyK$R ::Yސ)e@ƦVŠO8:M&M#roJZҤP( S-R9֊Hմ(-h$$0Ӣ VI Y+AP(S-J(CQҔ2}k B(ȋB:AP*K. gEBP(s( BP(y)itj HUnR<B4rQI6(~\wk"nPwcii+tP͑m%>=9xػ*O#с*)m=vHwvo<"L7jZ;Y_W1NQ rKJH<ojj3j8,D [jX. CV gk&Cҷ2;k%J]I!:$S7^72=~Fk;|؈@X/_ޫj$w&k6au[n/υڶKq`id"+[9%r<'Jeft'kʝ3:{G$A(iiBlLvڻO{FFFAY6A F^Xs[zu3{ z~͸ZĭhoxOxh%~Gw|9Oy%Nxv'Rr~sݏQsCqNQzNl#xw7K2jjE< q妎.|d*#V:QLvSzn{s,ζ݅i_l7DB}g_7) d e< clD$u 9GK q,nJj, alh*#{?K^+͙ןa"ݛ[ )Y\j] a`VGFŢåE27;F56\r%IN(nFtgQ,c宫he; yrphPD Ki*X템Mz WQvCTD )L!Ϲ xg 1ID 7݈ӯzWDz9ר`7:~hw#řEPh<i l󻌾A+Xs/xi!Vjrk]Ah<=#Y-_0viB IIJ&h&?3*bČ'#bRT&Cܿ.HpΨ14敷Hy] gҢNѭ;[TDtrwo){rˈXĥupuzyOkX1KR* Vkk .mKxVw^qo~EET P%ҶKP%3CfŚ>UCNG?5ɬa㸰_Iͨ;_ɯ=ʦ:POƬW י-1!2ƉJVjfin4]zaENxW+$ZFF&i6>3?ļi*?`@pd},ܟz@1Naj@e$''ϣ){qao@Uojc?˃N=kK+D\V=a]J,dr'c9.:eE C9ˁݮy_|'5,DJQ*Pg/MOD4-zّ\eL^gao9XECfR1ts!Q9zWB{CrN55$!ONs"|#' -L+,JIޏZcYiNQ8-jW]ę>y;~ }U LOWtwsw0a،Ar>'+#Ң,RS wӇvZjטZ-Z?BUp)Mff!ISBڮTc3@Dʅ$3'#bqCyBjȿ|pM nΏo00>x޳,*C]"]n[Ee7a]eQ\J34u: K&%`X P2B5F}|o)$H>jT`Qnpv.SKIQd᧤@HIA< !1zZя1rtaۮ]Zo 1'ٮWY;k[qO=97L=?8\Rb?yקN:ȁvjޤby}}M`ZrZs2BGgzgq#AGC;wكhhe4/RPyb9'2P ^̗crXF=?<܌ҬUR) ޺jd3ɀ/bhW}9bcfFW-Wt4ws,**:uHJ$ g`1Ò+X#֞[F9uAJR2!O]Ϲ{ w'$FbU<~?5'fg]Ǿu/}N1QQ=|؈n~?dЗ楺6g:T}bt<)B鯼/qߛo@. ˥hA_$ {p%m/|N9N{lif+==V^cyUK3}h.P&bC'X4yK3(JW0E+I$$et&"B{'F2$|{Wrb,^Zt p/.۝Wo-DOXrύ.ߺ2ñ8u"BI+X!*g4MJ97 V,&"ge*PяgFr/$pҥ3*5֫!߽y{ǃxw5G^:=m? ĐI`\֮fB!Z{rn|^)(*8S.ĎY9ʑ\ڧ(u^>pQG\"j;]Ҭ[@&FO Bяgfs/(oMm;Yc\CmM-73Q@P*ȩtLfc995p]1+؅1+J(yx4I#b,ń21zZX01L3l8cEoX.[E^gpXxU"uXs%rxq2wZP K[7[=#K"zgѩ^H;Se.~Mcgڜ&9. p88;>9MH;G]s#PѼMd,m:tlkr! -=]otXߙ,;%4Y\Ξ%zsˈ+PkzPQN֧Vs/2KErRpejCw9<;r531VFko+2ȁD&ޢ򎳝'KӑdחP $$vfx$ VBa!)C lPsr,GGXR1W I?^ZB!QÑ6˩pYk.7xRL!d`e6P( %[ mn9qȈrlhS2Rc/&E %1DD I:d/5tRTY*]d.о5 BPd({\+_fVբ 9[3/)RqLItCP( BPtCP( BPtCP( UuNk)|%BP~oBP(GaMquj~'VZ?YaSљ3[^o:֓9ft969IۗHP)ϵ`z+ k1#X?+-w Fcuv8mzV lY@)YMeWΉj2瀊oi8BP(YK-Fr'Zpx#yLIaU:Gc1ؿxwx`0olshͰxR1xAgN vZW-P*FHO~gwgblz'=yt6'9KKYUwz)ċkm8BP(+'EqI[l ̻IK \XL[Ro֫1𬯬ɍ8}nHt[j4# O׵?gljáɿ|&44ǧY!I?(  Qߖkq=("^BAN!,hpocyuWv{{t#bsg}NJ(R8M~`114R`yjf;~%IG33Rq̈YvNJa3O8Qq,QkTG׵}Y!N6N뾄Y%To׹^áC8 7iS! : f`pCP(J8>5::69 M]vޙSkyݮ/wZ$X[>IDATcMcP *D<{e8a_Pdq(/#UqڵG A`Q0 ֎/uQes6㛓 F`vIA'wƬ`s( Rbpsabojdb$뙩m%;djdrDSԳWo6 csSUJEٮ sMɆ 9! CSGUݘY }bT*,$DڶZ],J^ĺ$W<J@0Yo17\p,;:`fa>z~flR&:gת s{=p @P( Mo+~@#H)4I!H`@*Ev.-Z5p[Z8Aa_yjxX?Ć^b:ϲ SeR G6WX%'$%Ngi5ר(&qORF@4,z$ 6 ;iovHVEDrtn@5]RBP* $ 1;EMGVA|mCιgz$ 1%`izˉ \ g.:)?J9wL,LpF?ːu`}qM嬯{q?bT*!,E}=]٭)RJ^8sn9ϣ#s!ZġB)Rh}vp]38u<{;c6a;'F9VD B.&~h{yBsrSh(O 4}ל5_) %՛P( BP(c7Tz>BP( B7zBP(Je#;k-(ꂞϡP( BP(]5Jd IENDB`bpytop-1.0.68/LICENSE000066400000000000000000000261361416067541000141440ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. bpytop-1.0.68/Makefile000066400000000000000000000007511416067541000145720ustar00rootroot00000000000000PREFIX ?= /usr/local DOCDIR ?= $(PREFIX)/share/bpytop/doc all: @echo Run \'make install\' to install bpytop. install: @mkdir -p $(DESTDIR)$(PREFIX)/bin @cp -p bpytop.py $(DESTDIR)$(PREFIX)/bin/bpytop @mkdir -p $(DESTDIR)$(DOCDIR) @cp -p README.md $(DESTDIR)$(DOCDIR) @cp -pr themes $(DESTDIR)$(PREFIX)/share/bpytop @chmod 755 $(DESTDIR)$(PREFIX)/bin/bpytop uninstall: @rm -rf $(DESTDIR)$(PREFIX)/bin/bpytop @rm -rf $(DESTDIR)$(DOCDIR) @rm -rf $(DESTDIR)$(PREFIX)/share/bpytop bpytop-1.0.68/README.md000066400000000000000000000443511416067541000144150ustar00rootroot00000000000000# ![bpytop](https://github.com/aristocratos/bpytop/raw/master/Imgs/logo.png) Packaging status ![Linux](https://img.shields.io/badge/-Linux-grey?logo=linux) ![OSX](https://img.shields.io/badge/-OSX-black?logo=apple) ![FreeBSD](https://img.shields.io/badge/-FreeBSD-red?logo=freebsd) ![Usage](https://img.shields.io/badge/Usage-System%20resource%20monitor-yellow) ![Python](https://img.shields.io/badge/Python-v3.7%5E-green?logo=python) ![bpytop_version](https://img.shields.io/github/v/tag/aristocratos/bpytop?label=version) [![pypi_version](https://img.shields.io/pypi/v/bpytop?label=pypi)](https://pypi.org/project/bpytop) [![Test Status](https://img.shields.io/github/workflow/status/aristocratos/bpytop/Testing?label=tests)](https://github.com/aristocratos/bpytop/actions?query=workflow%3Atesting) [![Donate](https://img.shields.io/badge/-Donate-yellow?logo=paypal)](https://paypal.me/aristocratos) [![Sponsor](https://img.shields.io/badge/-Sponsor-red?logo=github)](https://github.com/sponsors/aristocratos) [![Coffee](https://img.shields.io/badge/-Buy%20me%20a%20Coffee-grey?logo=Ko-fi)](https://ko-fi.com/aristocratos) [![bpytop](https://img.shields.io/badge/-snapcraft.io-black)](https://snapcraft.io/bpytop)[![bpytop](https://snapcraft.io//bpytop/badge.svg)](https://snapcraft.io/bpytop) ## Index * [News](#news) * [Documents](#documents) * [Description](#description) * [Features](#features) * [Themes](#themes) * [Support and funding](#support-and-funding) * [Prerequisites](#prerequisites) (Read this if you are having issues!) * [Dependencies](#dependencies) * [Screenshots](#screenshots) * [Installation](#installation) * [Configurability](#configurability) * [License](#license) ## News ### C++ Version ##### 18 September 2021 ![btop++](https://raw.githubusercontent.com/aristocratos/btop/main/Img/logo.png) The Linux version of btop++ is complete. Released as version 1.0.0 Get it at https://github.com/aristocratos/btop The development plan right now: * 1.1.0 Mac OsX support * 1.2.0 FreeBSD support * 1.3.0 Support for GPU monitoring * 1.X.0 Other platforms and features... ##### 2 May 2021 I've started work on the third iteration of bashtop->bpytop. It's being written in C++ and will simply be called `btop`. I'm aiming at releasing a beta version around August this year and will publish the repo when I've got the core functionality and structure ready for anybody that wanna help out. This project is gonna take some time until it has complete feature parity with bpytop, since all system information gathering will likely have to be written from scratch without any external libraries. And will need some help in the form of code contributions to get complete support for BSD and OSX. If you got suggestions of C++ libraries that are multi-platform and are as extensive as [psutil](https://github.com/giampaolo/psutil) are for python, feel free to open up a new thread in Discussions, it could help speed up the development a lot. Will post any updates about this project here until the repo is made available. ## Documents #### [CHANGELOG.md](https://github.com/aristocratos/bpytop/blob/master/CHANGELOG.md) #### [CONTRIBUTING.md](https://github.com/aristocratos/bpytop/blob/master/CONTRIBUTING.md) #### [CODE_OF_CONDUCT.md](https://github.com/aristocratos/bpytop/blob/master/CODE_OF_CONDUCT.md) ## Description Resource monitor that shows usage and stats for processor, memory, disks, network and processes. Python port and continuation of [bashtop](https://github.com/aristocratos/bashtop). ## Features * Easy to use, with a game inspired menu system. * Full mouse support, all buttons with a highlighted key is clickable and mouse scroll works in process list and menu boxes. * Fast and responsive UI with UP, DOWN keys process selection. * Function for showing detailed stats for selected process. * Ability to filter processes, multiple filters can be entered. * Easy switching between sorting options. * Send SIGTERM, SIGKILL, SIGINT to selected process. * UI menu for changing all config file options. * Auto scaling graph for network usage. * Shows message in menu if new version is available * Shows current read and write speeds for disks ## Themes Bpytop uses the same theme files as bashtop so any theme made for bashtop will work. See [themes](https://github.com/aristocratos/bpytop/tree/master/themes) folder for available themes. The `make install` command places the default themes in `/usr/local/share/bpytop/themes`. If installed with `pip3` the themes will be located in a folder called `bpytop-themes` in the python3 site-packages folder. User created themes should be placed in `$HOME/.config/bpytop/themes`. Let me know if you want to contribute with new themes. ## Support and funding You can sponsor this project through github, see [my sponsors page](https://github.com/sponsors/aristocratos) for options. Or donate through [paypal](https://paypal.me/aristocratos) or [ko-fi](https://ko-fi.com/aristocratos). Any support is greatly appreciated! ## Prerequisites #### Mac Os X Will not display correctly in the standard terminal (unless truecolor is set to False)! Recommended alternative [iTerm2](https://www.iterm2.com/) Will also need to be run as superuser to display stats for processes not owned by user. OsX on Apple Silicon (arm) requires psutil version 5.8.0 to work and currently has no temperature monitoring. Upgrade psutil with `sudo pip3 install psutil --upgrade` #### Linux, Mac Os X and FreeBSD For correct display, a terminal with support for: * 24-bit truecolor ([See list of terminals with truecolor support](https://gist.github.com/XVilka/8346728)) * 256-color terminals are supported through 24-bit to 256-color conversion when setting "truecolor" to False in the options or with "-lc/--low-color" argument. * Wide characters (Are sometimes problematic in web-based terminals) Also needs a UTF8 locale and a font that covers: * Unicode Block “Braille Patterns” U+2800 - U+28FF * Unicode Block “Geometric Shapes” U+25A0 - U+25FF * Unicode Block "Box Drawing" and "Block Elements" U+2500 - U+259F #### Notice (Text rendering issues) If you are having problems with the characters in the graphs not looking like they do in the screenshots, it's likely a problem with your systems configured fallback font not having support for braille characters. See [Terminess Powerline](https://github.com/ryanoasis/nerd-fonts/tree/master/patched-fonts/Terminus/terminus-ttf-4.40.1) for an example of a font that includes the braille symbols. See comments by @sgleizes [link](https://github.com/aristocratos/bpytop/issues/100#issuecomment-684036827) and @XenHat [link](https://github.com/aristocratos/bpytop/issues/100#issuecomment-691585587) in issue #100 for possible solutions. If text are misaligned and you are using Konsole or Yakuake, turning off "Bi-Directional text rendering" is a possible fix. Characters clipping in to each other or text/border misalignments is not bugs caused by bpytop, but most likely a fontconfig or terminal problem where the braille characters making up the graphs aren't rendered correctly. Look to the creators of the terminal emulator you use to fix these issues if the previous mentioned fixes don't work for you. #### Notice (SSH) Dropbear seems to not be able to set correct locale. So if accessing bpytop over ssh, OpenSSH is recommended. ## Dependencies **[Python3](https://www.python.org/downloads/)** (v3.7 or later) **[psutil module](https://github.com/giampaolo/psutil)** (v5.7.0 or later) ## Optionals for additional stats (Optional OSX) **[coretemp](https://github.com/hacker1024/coretemp)** (recommended), or **[osx-cpu-temp](https://github.com/lavoiesl/osx-cpu-temp)** (less accurate) needed to show CPU temperatures. ## Screenshots Main UI showing details for a selected process. ![Screenshot 1](https://github.com/aristocratos/bpytop/raw/master/Imgs/main.png) Main UI in mini mode. ![Screenshot 2](https://github.com/aristocratos/bpytop/raw/master/Imgs/mini.png) Main menu. ![Screenshot 3](https://github.com/aristocratos/bpytop/raw/master/Imgs/menu.png) Options menu. ![Screenshot 4](https://github.com/aristocratos/bpytop/raw/master/Imgs/options.png) ## Installation I only maintain the PyPi package, so will not take responsibility for issues caused by any other install method! ### PyPi (will always have latest version) > Install or update to latest version ``` bash pip3 install bpytop --upgrade ``` ### Mac OsX >Install with Homebrew ```bash brew install bpytop ``` >Optional coretemp (Shows temperatures for cpu cores) ```bash brew install hacker1024/hacker1024/coretemp ``` >Alternatively install with MacPorts ```bash port install bpytop ``` OsX on Apple Silicon (arm) requires psutil version 5.8.0 to work and currently has no temperature monitoring. Upgrade psutil with `sudo pip3 install psutil --upgrade` ### Arch Linux Available in the Arch Linux [community] repository as `bpytop` >Installation ```bash sudo pacman -S bpytop ``` ### Debian based Available in [official Debian repository](https://tracker.debian.org/pkg/bpytop) since Debian 11 >Installation ```bash sudo apt install bpytop ``` Available for debian/ubuntu from [Azlux's repository](http://packages.azlux.fr/) ### FreeBSD package Available in [FreeBSD ports](https://www.freshports.org/sysutils/bpytop/) >Install pre-built package ``` bash sudo pkg install bpytop ``` ### Fedora/CentOS 8 package [Available](https://src.fedoraproject.org/rpms/bpytop) in the Fedora and [EPEL-8 repository](https://fedoraproject.org/wiki/EPEL). >Installation ``` bash sudo dnf install bpytop ``` ### Gentoo / Calculate Linux Available from [adrien-overlay](https://github.com/aaaaadrien/adrien-overlay) >Installation ``` bash sudo emerge -av sys-process/bpytop ``` ### Mageia Cauldron (Mageia 8) Available in Mageia Cauldron and then Mageia 8 when it is released. >Installation ``` bash sudo urpmi bpytop sudo dnf install bpytop ``` ### MX Linux Available in the MX Test Repo as `bpytop` Please use MX Package Installer MX Test Repo tab to install. http://mxrepo.com/mx/testrepo/pool/test/b/bpytop/ ### Void Linux Available in void repo and void-packages ports tree >Installation ``` bash sudo xbps-install bpytop ``` ### Snap package (Note! There is some issues caused by the snap sandboxing) by @kz6fittycent https://github.com/kz6fittycent/bpytop-snap >Install the package ``` bash sudo snap install bpytop ``` The config folder will be located in `~/snap/bpytop/current/.config/bpytop` ## Manual installation #### Dependencies installation Linux >Install python3 and git with a package manager of you choice >Install psutil python module (sudo might be required) ``` bash python3 -m pip install psutil ``` #### Dependencies installation OSX >Install homebrew if not already installed ``` bash /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" ``` >Install python3 if not already installed ``` bash brew install python3 git ``` >Install psutil python module ``` bash python3 -m pip install psutil ``` >Install optional dependency coretemp (recommended), or osx-cpu-temp (less accurate) ``` bash brew install hacker1024/hacker1024/coretemp ``` ``` bash brew install osx-cpu-temp ``` #### Dependencies installation FreeBSD >Install with pkg and pip ``` bash sudo pkg install git python3 py37-psutil ``` #### Manual installation Linux, OSX and FreeBSD >Clone and install ``` bash git clone https://github.com/aristocratos/bpytop.git cd bpytop sudo make install ``` >to uninstall it ``` bash sudo make uninstall ``` ## Configurability All options changeable from within UI. Config files stored in "$HOME/.config/bpytop" folder #### bpytop.cfg: (auto generated if not found) "/etc/bpytop.conf" will be used as default seed for config file creation if it exists. ("/usr/local/etc/bpytop.conf" on BSD) ```bash #? Config file for bpytop v. 1.0.64 #* Color theme, looks for a .theme file in "/usr/[local/]share/bpytop/themes" and "~/.config/bpytop/themes", "Default" for builtin default theme. #* Prefix name by a plus sign (+) for a theme located in user themes folder, i.e. color_theme="+monokai" color_theme="monokai" #* If the theme set background should be shown, set to False if you want terminal background transparency theme_background=False #* Sets if 24-bit truecolor should be used, will convert 24-bit colors to 256 color (6x6x6 color cube) if false. truecolor=True #* Manually set which boxes to show. Available values are "cpu mem net proc", separate values with whitespace. shown_boxes="cpu mem net proc" #* Update time in milliseconds, increases automatically if set below internal loops processing time, recommended 2000 ms or above for better sample times for graphs. update_ms=2000 #* Processes update multiplier, sets how often the process list is updated as a multiplier of "update_ms". #* Set to 2 or higher to greatly decrease bpytop cpu usage. (Only integers) proc_update_mult=2 #* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu responsive", #* "cpu lazy" updates top process over time, "cpu responsive" updates top process directly. proc_sorting="cpu lazy" #* Reverse sorting order, True or False. proc_reversed=False #* Show processes as a tree proc_tree=False #* Which depth the tree view should auto collapse processes at tree_depth=3 #* Use the cpu graph colors in the process list. proc_colors=True #* Use a darkening gradient in the process list. proc_gradient=True #* If process cpu usage should be of the core it's running on or usage of the total available cpu power. proc_per_core=False #* Show process memory as bytes instead of percent proc_mem_bytes=True #* Sets the CPU stat shown in upper half of the CPU graph, "total" is always available, see: #* https://psutil.readthedocs.io/en/latest/#psutil.cpu_times for attributes available on specific platforms. #* Select from a list of detected attributes from the options menu cpu_graph_upper="total" #* Sets the CPU stat shown in lower half of the CPU graph, "total" is always available, see: #* https://psutil.readthedocs.io/en/latest/#psutil.cpu_times for attributes available on specific platforms. #* Select from a list of detected attributes from the options menu cpu_graph_lower="total" #* Toggles if the lower CPU graph should be inverted. cpu_invert_lower=True #* Set to True to completely disable the lower CPU graph. cpu_single_graph=False #* Shows the system uptime in the CPU box. show_uptime=True #* Check cpu temperature, needs "osx-cpu-temp" on MacOS X. check_temp=True #* Which sensor to use for cpu temperature, use options menu to select from list of available sensors. cpu_sensor=Auto #* Show temperatures for cpu cores also if check_temp is True and sensors has been found show_coretemp=True #* Which temperature scale to use, available values: "celsius", "fahrenheit", "kelvin" and "rankine" temp_scale="celsius" #* Show CPU frequency, can cause slowdowns on certain systems with some versions of psutil show_cpu_freq=True #* Draw a clock at top of screen, formatting according to strftime, empty string to disable. draw_clock="%H:%M" #* Update main ui in background when menus are showing, set this to false if the menus is flickering too much for comfort. background_update=True #* Custom cpu model name, empty string to disable. custom_cpu_name="" #* Optional filter for shown disks, should be full path of a mountpoint, separate multiple values with a comma ",". #* Begin line with "exclude=" to change to exclude filter, otherwise defaults to "most include" filter. Example: disks_filter="exclude=/boot, /home/user" disks_filter="exclude=/boot" #* Show graphs instead of meters for memory values. mem_graphs=True #* If swap memory should be shown in memory box. show_swap=True #* Show swap as a disk, ignores show_swap value above, inserts itself after first disk. swap_disk=True #* If mem box should be split to also show disks info. show_disks=True #* Filter out non physical disks. Set this to False to include network disks, RAM disks and similar. only_physical=True #* Read disks list from /etc/fstab. This also disables only_physical. use_fstab=True #* Toggles if io stats should be shown in regular disk usage view show_io_stat=True #* Toggles io mode for disks, showing only big graphs for disk read/write speeds. io_mode=False #* Set to True to show combined read/write io graphs in io mode. io_graph_combined=False #* Set the top speed for the io graphs in MiB/s (10 by default), use format "device:speed" separate disks with a comma ",". #* Example: "/dev/sda:100, /dev/sdb:20" io_graph_speeds="" #* Set fixed values for network graphs, default "10M" = 10 Mibibytes, possible units "K", "M", "G", append with "bit" for bits instead of bytes, i.e "100mbit" net_download="100Mbit" net_upload="100Mbit" #* Start in network graphs auto rescaling mode, ignores any values set above and rescales down to 10 Kibibytes at the lowest. net_auto=True #* Sync the scaling for download and upload to whichever currently has the highest scale net_sync=False #* If the network graphs color gradient should scale to bandwidth usage or auto scale, bandwidth usage is based on "net_download" and "net_upload" values net_color_fixed=False #* Starts with the Network Interface specified here. net_iface="br0" #* Show battery stats in top right if battery is present show_battery=True #* Show init screen at startup, the init screen is purely cosmetical show_init=False #* Enable check for new version from github.com/aristocratos/bpytop at start. update_check=True #* Set loglevel for "~/.config/bpytop/error.log" levels are: "ERROR" "WARNING" "INFO" "DEBUG". #* The level set includes all lower levels, i.e. "DEBUG" will show all logging info. log_level=DEBUG ``` #### Command line options: ``` text usage: bpytop.py [-h] [-b BOXES] [-lc] [-v] [--debug] optional arguments: -h, --help show this help message and exit -b BOXES, --boxes BOXES which boxes to show at start, example: -b "cpu mem net proc" -lc, --low-color disable truecolor, converts 24-bit colors to 256-color -v, --version show version info and exit --debug start with loglevel set to DEBUG overriding value set in config ``` ## LICENSE [Apache License 2.0](https://github.com/aristocratos/bpytop/blob/master/LICENSE) bpytop-1.0.68/bpytop-themes000077700000000000000000000000001416067541000170552themesustar00rootroot00000000000000bpytop-1.0.68/bpytop.py000077500000000000000000006454061416067541000150400ustar00rootroot00000000000000#!/usr/bin/env python3 # pylint: disable=not-callable, no-member, unsubscriptable-object # indent = tab # tab-size = 4 # Copyright 2021 Aristocratos (jakob@qvantnet.com) # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import os, sys, io, threading, signal, re, subprocess, logging, logging.handlers, argparse import urllib.request from time import time, sleep, strftime, tzset from datetime import timedelta from _thread import interrupt_main from collections import defaultdict from select import select from string import Template from math import ceil, floor from random import randint from shutil import which from typing import List, Dict, Tuple, Union, Any, Iterable errors: List[str] = [] try: import fcntl, termios, tty, pwd except Exception as e: errors.append(f'{e}') try: import psutil # type: ignore except Exception as e: errors.append(f'{e}') SELF_START = time() SYSTEM: str if "linux" in sys.platform: SYSTEM = "Linux" elif "bsd" in sys.platform: SYSTEM = "BSD" elif "darwin" in sys.platform: SYSTEM = "MacOS" else: SYSTEM = "Other" if errors: print("ERROR!") print("\n".join(errors)) if SYSTEM == "Other": print("\nUnsupported platform!\n") else: print("\nInstall required modules!\n") raise SystemExit(1) VERSION: str = "1.0.68" #? Argument parser -------------------------------------------------------------------------------> args = argparse.ArgumentParser() args.add_argument("-b", "--boxes", action="store", dest="boxes", help = "which boxes to show at start, example: -b \"cpu mem net proc\"") args.add_argument("-lc", "--low-color", action="store_true", help = "disable truecolor, converts 24-bit colors to 256-color") args.add_argument("-v", "--version", action="store_true", help = "show version info and exit") args.add_argument("--debug", action="store_true", help = "start with loglevel set to DEBUG overriding value set in config") stdargs = args.parse_args() if stdargs.version: print(f'bpytop version: {VERSION}\n' f'psutil version: {".".join(str(x) for x in psutil.version_info)}') raise SystemExit(0) ARG_BOXES: str = stdargs.boxes LOW_COLOR: bool = stdargs.low_color DEBUG: bool = stdargs.debug #? Variables -------------------------------------------------------------------------------------> BANNER_SRC: List[Tuple[str, str, str]] = [ ("#ffa50a", "#0fd7ff", "██████╗ ██████╗ ██╗ ██╗████████╗ ██████╗ ██████╗"), ("#f09800", "#00bfe6", "██╔══██╗██╔══██╗╚██╗ ██╔╝╚══██╔══╝██╔═══██╗██╔══██╗"), ("#db8b00", "#00a6c7", "██████╔╝██████╔╝ ╚████╔╝ ██║ ██║ ██║██████╔╝"), ("#c27b00", "#008ca8", "██╔══██╗██╔═══╝ ╚██╔╝ ██║ ██║ ██║██╔═══╝ "), ("#a86b00", "#006e85", "██████╔╝██║ ██║ ██║ ╚██████╔╝██║"), ("#000000", "#000000", "╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝"), ] #*?This is the template used to create the config file DEFAULT_CONF: Template = Template(f'#? Config file for bpytop v. {VERSION}' + ''' #* Color theme, looks for a .theme file in "/usr/[local/]share/bpytop/themes" and "~/.config/bpytop/themes", "Default" for builtin default theme. #* Prefix name by a plus sign (+) for a theme located in user themes folder, i.e. color_theme="+monokai" color_theme="$color_theme" #* If the theme set background should be shown, set to False if you want terminal background transparency theme_background=$theme_background #* Sets if 24-bit truecolor should be used, will convert 24-bit colors to 256 color (6x6x6 color cube) if false. truecolor=$truecolor #* Manually set which boxes to show. Available values are "cpu mem net proc", separate values with whitespace. shown_boxes="$shown_boxes" #* Update time in milliseconds, increases automatically if set below internal loops processing time, recommended 2000 ms or above for better sample times for graphs. update_ms=$update_ms #* Processes update multiplier, sets how often the process list is updated as a multiplier of "update_ms". #* Set to 2 or higher to greatly decrease bpytop cpu usage. (Only integers) proc_update_mult=$proc_update_mult #* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu responsive", #* "cpu lazy" updates top process over time, "cpu responsive" updates top process directly. proc_sorting="$proc_sorting" #* Reverse sorting order, True or False. proc_reversed=$proc_reversed #* Show processes as a tree proc_tree=$proc_tree #* Which depth the tree view should auto collapse processes at tree_depth=$tree_depth #* Use the cpu graph colors in the process list. proc_colors=$proc_colors #* Use a darkening gradient in the process list. proc_gradient=$proc_gradient #* If process cpu usage should be of the core it's running on or usage of the total available cpu power. proc_per_core=$proc_per_core #* Show process memory as bytes instead of percent proc_mem_bytes=$proc_mem_bytes #* Sets the CPU stat shown in upper half of the CPU graph, "total" is always available, see: #* https://psutil.readthedocs.io/en/latest/#psutil.cpu_times for attributes available on specific platforms. #* Select from a list of detected attributes from the options menu cpu_graph_upper="$cpu_graph_upper" #* Sets the CPU stat shown in lower half of the CPU graph, "total" is always available, see: #* https://psutil.readthedocs.io/en/latest/#psutil.cpu_times for attributes available on specific platforms. #* Select from a list of detected attributes from the options menu cpu_graph_lower="$cpu_graph_lower" #* Toggles if the lower CPU graph should be inverted. cpu_invert_lower=$cpu_invert_lower #* Set to True to completely disable the lower CPU graph. cpu_single_graph=$cpu_single_graph #* Shows the system uptime in the CPU box. show_uptime=$show_uptime #* Check cpu temperature, needs "osx-cpu-temp" on MacOS X. check_temp=$check_temp #* Which sensor to use for cpu temperature, use options menu to select from list of available sensors. cpu_sensor=$cpu_sensor #* Show temperatures for cpu cores also if check_temp is True and sensors has been found show_coretemp=$show_coretemp #* Which temperature scale to use, available values: "celsius", "fahrenheit", "kelvin" and "rankine" temp_scale="$temp_scale" #* Show CPU frequency, can cause slowdowns on certain systems with some versions of psutil show_cpu_freq=$show_cpu_freq #* Draw a clock at top of screen, formatting according to strftime, empty string to disable. draw_clock="$draw_clock" #* Update main ui in background when menus are showing, set this to false if the menus is flickering too much for comfort. background_update=$background_update #* Custom cpu model name, empty string to disable. custom_cpu_name="$custom_cpu_name" #* Optional filter for shown disks, should be full path of a mountpoint, separate multiple values with a comma ",". #* Begin line with "exclude=" to change to exclude filter, otherwise defaults to "most include" filter. Example: disks_filter="exclude=/boot, /home/user" disks_filter="$disks_filter" #* Show graphs instead of meters for memory values. mem_graphs=$mem_graphs #* If swap memory should be shown in memory box. show_swap=$show_swap #* Show swap as a disk, ignores show_swap value above, inserts itself after first disk. swap_disk=$swap_disk #* If mem box should be split to also show disks info. show_disks=$show_disks #* Filter out non physical disks. Set this to False to include network disks, RAM disks and similar. only_physical=$only_physical #* Read disks list from /etc/fstab. This also disables only_physical. use_fstab=$use_fstab #* Toggles if io stats should be shown in regular disk usage view show_io_stat=$show_io_stat #* Toggles io mode for disks, showing only big graphs for disk read/write speeds. io_mode=$io_mode #* Set to True to show combined read/write io graphs in io mode. io_graph_combined=$io_graph_combined #* Set the top speed for the io graphs in MiB/s (10 by default), use format "device:speed" separate disks with a comma ",". #* Example: "/dev/sda:100, /dev/sdb:20" io_graph_speeds="$io_graph_speeds" #* Set fixed values for network graphs, default "10M" = 10 Mibibytes, possible units "K", "M", "G", append with "bit" for bits instead of bytes, i.e "100mbit" net_download="$net_download" net_upload="$net_upload" #* Start in network graphs auto rescaling mode, ignores any values set above and rescales down to 10 Kibibytes at the lowest. net_auto=$net_auto #* Sync the scaling for download and upload to whichever currently has the highest scale net_sync=$net_sync #* If the network graphs color gradient should scale to bandwidth usage or auto scale, bandwidth usage is based on "net_download" and "net_upload" values net_color_fixed=$net_color_fixed #* Starts with the Network Interface specified here. net_iface="$net_iface" #* Show battery stats in top right if battery is present show_battery=$show_battery #* Show init screen at startup, the init screen is purely cosmetical show_init=$show_init #* Enable check for new version from github.com/aristocratos/bpytop at start. update_check=$update_check #* Set loglevel for "~/.config/bpytop/error.log" levels are: "ERROR" "WARNING" "INFO" "DEBUG". #* The level set includes all lower levels, i.e. "DEBUG" will show all logging info. log_level=$log_level ''') CONFIG_DIR: str = f'{os.path.expanduser("~")}/.config/bpytop' if not os.path.isdir(CONFIG_DIR): try: os.makedirs(CONFIG_DIR) os.mkdir(f'{CONFIG_DIR}/themes') except PermissionError: print(f'ERROR!\nNo permission to write to "{CONFIG_DIR}" directory!') raise SystemExit(1) CONFIG_FILE: str = f'{CONFIG_DIR}/bpytop.conf' THEME_DIR: str = "" if os.path.isdir(f'{os.path.dirname(__file__)}/bpytop-themes'): THEME_DIR = f'{os.path.dirname(__file__)}/bpytop-themes' elif os.path.isdir(f'{os.path.dirname(__file__)}/themes'): THEME_DIR = f'{os.path.dirname(__file__)}/themes' else: for td in ["/usr/local/", "/usr/", "/snap/bpytop/current/usr/"]: if os.path.isdir(f'{td}share/bpytop/themes'): THEME_DIR = f'{td}share/bpytop/themes' break USER_THEME_DIR: str = f'{CONFIG_DIR}/themes' CORES: int = psutil.cpu_count(logical=False) or 1 THREADS: int = psutil.cpu_count(logical=True) or 1 THREAD_ERROR: int = 0 DEFAULT_THEME: Dict[str, str] = { "main_bg" : "#00", "main_fg" : "#cc", "title" : "#ee", "hi_fg" : "#969696", "selected_bg" : "#7e2626", "selected_fg" : "#ee", "inactive_fg" : "#40", "graph_text" : "#60", "meter_bg" : "#40", "proc_misc" : "#0de756", "cpu_box" : "#3d7b46", "mem_box" : "#8a882e", "net_box" : "#423ba5", "proc_box" : "#923535", "div_line" : "#30", "temp_start" : "#4897d4", "temp_mid" : "#5474e8", "temp_end" : "#ff40b6", "cpu_start" : "#50f095", "cpu_mid" : "#f2e266", "cpu_end" : "#fa1e1e", "free_start" : "#223014", "free_mid" : "#b5e685", "free_end" : "#dcff85", "cached_start" : "#0b1a29", "cached_mid" : "#74e6fc", "cached_end" : "#26c5ff", "available_start" : "#292107", "available_mid" : "#ffd77a", "available_end" : "#ffb814", "used_start" : "#3b1f1c", "used_mid" : "#d9626d", "used_end" : "#ff4769", "download_start" : "#231a63", "download_mid" : "#4f43a3", "download_end" : "#b0a9de", "upload_start" : "#510554", "upload_mid" : "#7d4180", "upload_end" : "#dcafde", "process_start" : "#80d0a3", "process_mid" : "#dcd179", "process_end" : "#d45454", } MENUS: Dict[str, Dict[str, Tuple[str, ...]]] = { "options" : { "normal" : ( "┌─┐┌─┐┌┬┐┬┌─┐┌┐┌┌─┐", "│ │├─┘ │ ││ ││││└─┐", "└─┘┴ ┴ ┴└─┘┘└┘└─┘"), "selected" : ( "╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗", "║ ║╠═╝ ║ ║║ ║║║║╚═╗", "╚═╝╩ ╩ ╩╚═╝╝╚╝╚═╝") }, "help" : { "normal" : ( "┬ ┬┌─┐┬ ┌─┐", "├─┤├┤ │ ├─┘", "┴ ┴└─┘┴─┘┴ "), "selected" : ( "╦ ╦╔═╗╦ ╔═╗", "╠═╣║╣ ║ ╠═╝", "╩ ╩╚═╝╩═╝╩ ") }, "quit" : { "normal" : ( "┌─┐ ┬ ┬ ┬┌┬┐", "│─┼┐│ │ │ │ ", "└─┘└└─┘ ┴ ┴ "), "selected" : ( "╔═╗ ╦ ╦ ╦╔╦╗ ", "║═╬╗║ ║ ║ ║ ", "╚═╝╚╚═╝ ╩ ╩ ") } } MENU_COLORS: Dict[str, Tuple[str, ...]] = { "normal" : ("#0fd7ff", "#00bfe6", "#00a6c7", "#008ca8"), "selected" : ("#ffa50a", "#f09800", "#db8b00", "#c27b00") } #? Units for floating_humanizer function UNITS: Dict[str, Tuple[str, ...]] = { "bit" : ("bit", "Kib", "Mib", "Gib", "Tib", "Pib", "Eib", "Zib", "Yib", "Bib", "GEb"), "byte" : ("Byte", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "BiB", "GEB") } SUBSCRIPT: Tuple[str, ...] = ("₀", "₁", "₂", "₃", "₄", "₅", "₆", "₇", "₈", "₉") SUPERSCRIPT: Tuple[str, ...] = ("⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹") #? Setup error logger ----------------------------------------------------------------> try: errlog = logging.getLogger("ErrorLogger") errlog.setLevel(logging.DEBUG) eh = logging.handlers.RotatingFileHandler(f'{CONFIG_DIR}/error.log', maxBytes=1048576, backupCount=4) eh.setLevel(logging.DEBUG) eh.setFormatter(logging.Formatter("%(asctime)s | %(levelname)s: %(message)s", datefmt="%d/%m/%y (%X)")) errlog.addHandler(eh) except PermissionError: print(f'ERROR!\nNo permission to write to "{CONFIG_DIR}" directory!') raise SystemExit(1) #? Timers for testing and debugging --------------------------------------------------------------> class TimeIt: timers: Dict[str, float] = {} paused: Dict[str, float] = {} @classmethod def start(cls, name): cls.timers[name] = time() @classmethod def pause(cls, name): if name in cls.timers: cls.paused[name] = time() - cls.timers[name] del cls.timers[name] @classmethod def stop(cls, name): if name in cls.timers: total: float = time() - cls.timers[name] del cls.timers[name] if name in cls.paused: total += cls.paused[name] del cls.paused[name] errlog.debug(f'{name} completed in {total:.6f} seconds') def timeit_decorator(func): def timed(*args, **kw): ts = time() out = func(*args, **kw) errlog.debug(f'{func.__name__} completed in {time() - ts:.6f} seconds') return out return timed #? Issue #364 -----------------------------------------------------------> def strtobool(val: str) -> bool: """Convert a string representation of truth to true (1) or false (0). True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if 'val' is anything else. """ try: val = val.lower() except AttributeError: raise ValueError(f"invalid type {type(val)} for truth value {val}") if val in ('y', 'yes', 't', 'true', 'on', '1'): return True elif val in ('n', 'no', 'f', 'false', 'off', '0'): return False else: raise ValueError(f"invalid truth value {val}") #? Set up config class and load config -----------------------------------------------------------> class Config: '''Holds all config variables and functions for loading from and saving to disk''' keys: List[str] = ["color_theme", "update_ms", "proc_sorting", "proc_reversed", "proc_tree", "check_temp", "draw_clock", "background_update", "custom_cpu_name", "proc_colors", "proc_gradient", "proc_per_core", "proc_mem_bytes", "disks_filter", "update_check", "log_level", "mem_graphs", "show_swap", "swap_disk", "show_disks", "use_fstab", "net_download", "net_upload", "net_auto", "net_color_fixed", "show_init", "theme_background", "net_sync", "show_battery", "tree_depth", "cpu_sensor", "show_coretemp", "proc_update_mult", "shown_boxes", "net_iface", "only_physical", "truecolor", "io_mode", "io_graph_combined", "io_graph_speeds", "show_io_stat", "cpu_graph_upper", "cpu_graph_lower", "cpu_invert_lower", "cpu_single_graph", "show_uptime", "temp_scale", "show_cpu_freq"] conf_dict: Dict[str, Union[str, int, bool]] = {} color_theme: str = "Default" theme_background: bool = True truecolor: bool = True shown_boxes: str = "cpu mem net proc" update_ms: int = 2000 proc_update_mult: int = 2 proc_sorting: str = "cpu lazy" proc_reversed: bool = False proc_tree: bool = False tree_depth: int = 3 proc_colors: bool = True proc_gradient: bool = True proc_per_core: bool = False proc_mem_bytes: bool = True cpu_graph_upper: str = "total" cpu_graph_lower: str = "total" cpu_invert_lower: bool = True cpu_single_graph: bool = False show_uptime: bool = True check_temp: bool = True cpu_sensor: str = "Auto" show_coretemp: bool = True temp_scale: str = "celsius" show_cpu_freq: bool = True draw_clock: str = "%X" background_update: bool = True custom_cpu_name: str = "" disks_filter: str = "" update_check: bool = True mem_graphs: bool = True show_swap: bool = True swap_disk: bool = True show_disks: bool = True only_physical: bool = True use_fstab: bool = False show_io_stat: bool = True io_mode: bool = False io_graph_combined: bool = False io_graph_speeds: str = "" net_download: str = "10M" net_upload: str = "10M" net_color_fixed: bool = False net_auto: bool = True net_sync: bool = False net_iface: str = "" show_battery: bool = True show_init: bool = False log_level: str = "WARNING" warnings: List[str] = [] info: List[str] = [] sorting_options: List[str] = ["pid", "program", "arguments", "threads", "user", "memory", "cpu lazy", "cpu responsive"] log_levels: List[str] = ["ERROR", "WARNING", "INFO", "DEBUG"] cpu_percent_fields: List = ["total"] cpu_percent_fields.extend(getattr(psutil.cpu_times_percent(), "_fields", [])) temp_scales: List[str] = ["celsius", "fahrenheit", "kelvin", "rankine"] cpu_sensors: List[str] = [ "Auto" ] if hasattr(psutil, "sensors_temperatures"): try: _temps = psutil.sensors_temperatures() if _temps: for _name, _entries in _temps.items(): for _num, _entry in enumerate(_entries, 1): if hasattr(_entry, "current"): cpu_sensors.append(f'{_name}:{_num if _entry.label == "" else _entry.label}') except: pass changed: bool = False recreate: bool = False config_file: str = "" _initialized: bool = False def __init__(self, path: str): self.config_file = path conf: Dict[str, Union[str, int, bool]] = self.load_config() if not "version" in conf.keys(): self.recreate = True self.info.append(f'Config file malformatted or missing, will be recreated on exit!') elif conf["version"] != VERSION: self.recreate = True self.info.append(f'Config file version and bpytop version mismatch, will be recreated on exit!') for key in self.keys: if key in conf.keys() and conf[key] != "_error_": setattr(self, key, conf[key]) else: self.recreate = True self.conf_dict[key] = getattr(self, key) self._initialized = True def __setattr__(self, name, value): if self._initialized: object.__setattr__(self, "changed", True) object.__setattr__(self, name, value) if name not in ["_initialized", "recreate", "changed"]: self.conf_dict[name] = value def load_config(self) -> Dict[str, Union[str, int, bool]]: '''Load config from file, set correct types for values and return a dict''' new_config: Dict[str,Union[str, int, bool]] = {} conf_file: str = "" if os.path.isfile(self.config_file): conf_file = self.config_file elif SYSTEM == "BSD" and os.path.isfile("/usr/local/etc/bpytop.conf"): conf_file = "/usr/local/etc/bpytop.conf" elif SYSTEM != "BSD" and os.path.isfile("/etc/bpytop.conf"): conf_file = "/etc/bpytop.conf" else: return new_config try: with open(conf_file, "r") as f: for line in f: line = line.strip() if line.startswith("#? Config"): new_config["version"] = line[line.find("v. ") + 3:] continue if not '=' in line: continue key, line = line.split('=', maxsplit=1) if not key in self.keys: continue line = line.strip('"') if type(getattr(self, key)) == int: try: new_config[key] = int(line) except ValueError: self.warnings.append(f'Config key "{key}" should be an integer!') if type(getattr(self, key)) == bool: try: new_config[key] = bool(strtobool(line)) except ValueError: self.warnings.append(f'Config key "{key}" can only be True or False!') if type(getattr(self, key)) == str: new_config[key] = str(line) except Exception as e: errlog.exception(str(e)) if "proc_sorting" in new_config and not new_config["proc_sorting"] in self.sorting_options: new_config["proc_sorting"] = "_error_" self.warnings.append(f'Config key "proc_sorted" didn\'t get an acceptable value!') if "log_level" in new_config and not new_config["log_level"] in self.log_levels: new_config["log_level"] = "_error_" self.warnings.append(f'Config key "log_level" didn\'t get an acceptable value!') if "update_ms" in new_config and int(new_config["update_ms"]) < 100: new_config["update_ms"] = 100 self.warnings.append(f'Config key "update_ms" can\'t be lower than 100!') for net_name in ["net_download", "net_upload"]: if net_name in new_config and not new_config[net_name][0].isdigit(): # type: ignore new_config[net_name] = "_error_" if "cpu_sensor" in new_config and not new_config["cpu_sensor"] in self.cpu_sensors: new_config["cpu_sensor"] = "_error_" self.warnings.append(f'Config key "cpu_sensor" does not contain an available sensor!') if "shown_boxes" in new_config and not new_config["shown_boxes"] == "": for box in new_config["shown_boxes"].split(): #type: ignore if not box in ["cpu", "mem", "net", "proc"]: new_config["shown_boxes"] = "_error_" self.warnings.append(f'Config key "shown_boxes" contains invalid box names!') break for cpu_graph in ["cpu_graph_upper", "cpu_graph_lower"]: if cpu_graph in new_config and not new_config[cpu_graph] in self.cpu_percent_fields: new_config[cpu_graph] = "_error_" self.warnings.append(f'Config key "{cpu_graph}" does not contain an available cpu stat attribute!') if "temp_scale" in new_config and not new_config["temp_scale"] in self.temp_scales: new_config["temp_scale"] = "_error_" self.warnings.append(f'Config key "temp_scale" does not contain a recognized temperature scale!') return new_config def save_config(self): '''Save current config to config file if difference in values or version, creates a new file if not found''' if not self.changed and not self.recreate: return try: with open(self.config_file, "w" if os.path.isfile(self.config_file) else "x") as f: f.write(DEFAULT_CONF.substitute(self.conf_dict)) except Exception as e: errlog.exception(str(e)) try: CONFIG: Config = Config(CONFIG_FILE) if DEBUG: errlog.setLevel(logging.DEBUG) else: errlog.setLevel(getattr(logging, CONFIG.log_level)) DEBUG = CONFIG.log_level == "DEBUG" errlog.info(f'New instance of bpytop version {VERSION} started with pid {os.getpid()}') errlog.info(f'Loglevel set to {"DEBUG" if DEBUG else CONFIG.log_level}') errlog.debug(f'Using psutil version {".".join(str(x) for x in psutil.version_info)}') errlog.debug(f'CMD: {" ".join(sys.argv)}') if CONFIG.info: for info in CONFIG.info: errlog.info(info) CONFIG.info = [] if CONFIG.warnings: for warning in CONFIG.warnings: errlog.warning(warning) CONFIG.warnings = [] except Exception as e: errlog.exception(f'{e}') raise SystemExit(1) if ARG_BOXES: _new_boxes: List = [] for _box in ARG_BOXES.split(): if _box in ["cpu", "mem", "net", "proc"]: _new_boxes.append(_box) CONFIG.shown_boxes = " ".join(_new_boxes) del _box, _new_boxes if SYSTEM == "Linux" and not os.path.isdir("/sys/class/power_supply"): CONFIG.show_battery = False if psutil.version_info[0] < 5 or (psutil.version_info[0] == 5 and psutil.version_info[1] < 7): warn = f'psutil version {".".join(str(x) for x in psutil.version_info)} detected, version 5.7.0 or later required for full functionality!' print("WARNING!", warn) errlog.warning(warn) #? Classes ---------------------------------------------------------------------------------------> class Term: """Terminal info and commands""" width: int = 0 height: int = 0 resized: bool = False _w : int = 0 _h : int = 0 fg: str = "" #* Default foreground color bg: str = "" #* Default background color hide_cursor = "\033[?25l" #* Hide terminal cursor show_cursor = "\033[?25h" #* Show terminal cursor alt_screen = "\033[?1049h" #* Switch to alternate screen normal_screen = "\033[?1049l" #* Switch to normal screen clear = "\033[2J\033[0;0f" #* Clear screen and set cursor to position 0,0 mouse_on = "\033[?1002h\033[?1015h\033[?1006h" #* Enable reporting of mouse position on click and release mouse_off = "\033[?1002l" #* Disable mouse reporting mouse_direct_on = "\033[?1003h" #* Enable reporting of mouse position at any movement mouse_direct_off = "\033[?1003l" #* Disable direct mouse reporting winch = threading.Event() old_boxes: List = [] min_width: int = 0 min_height: int = 0 @classmethod def refresh(cls, *args, force: bool = False): """Update width, height and set resized flag if terminal has been resized""" if Init.running: cls.resized = False; return if cls.resized: cls.winch.set(); return cls._w, cls._h = os.get_terminal_size() if (cls._w, cls._h) == (cls.width, cls.height) and cls.old_boxes == Box.boxes and not force: return if force: Collector.collect_interrupt = True if cls.old_boxes != Box.boxes: w_p = h_p = 0 cls.min_width = cls.min_height = 0 cls.old_boxes = Box.boxes.copy() for box_class in Box.__subclasses__(): for box_name in Box.boxes: if box_name in str(box_class).capitalize(): if not (box_name == "cpu" and "proc" in Box.boxes) and not (box_name == "net" and "mem" in Box.boxes) and w_p + box_class.width_p <= 100: w_p += box_class.width_p cls.min_width += getattr(box_class, "min_w", 0) if not (box_name in ["mem", "net"] and "proc" in Box.boxes) and h_p + box_class.height_p <= 100: h_p += box_class.height_p cls.min_height += getattr(box_class, "min_h", 0) while (cls._w, cls._h) != (cls.width, cls.height) or (cls._w < cls.min_width or cls._h < cls.min_height): if Init.running: Init.resized = True CpuBox.clock_block = True cls.resized = True Collector.collect_interrupt = True cls.width, cls.height = cls._w, cls._h Draw.now(Term.clear) box_width = min(50, cls._w - 2) Draw.now(f'{create_box(cls._w // 2 - box_width // 2, cls._h // 2 - 2, 50, 3, "resizing", line_color=Colors.green, title_color=Colors.white)}', f'{Mv.r(box_width // 4)}{Colors.default}{Colors.black_bg}{Fx.b}Width : {cls._w} Height: {cls._h}{Fx.ub}{Term.bg}{Term.fg}') if cls._w < 80 or cls._h < 24: while cls._w < cls.min_width or cls._h < cls.min_height: Draw.now(Term.clear) box_width = min(50, cls._w - 2) Draw.now(f'{create_box(cls._w // 2 - box_width // 2, cls._h // 2 - 2, box_width, 4, "warning", line_color=Colors.red, title_color=Colors.white)}', f'{Mv.r(box_width // 4)}{Colors.default}{Colors.black_bg}{Fx.b}Width: {Colors.red if cls._w < cls.min_width else Colors.green}{cls._w} ', f'{Colors.default}Height: {Colors.red if cls._h < cls.min_height else Colors.green}{cls._h}{Term.bg}{Term.fg}', f'{Mv.d(1)}{Mv.l(25)}{Colors.default}{Colors.black_bg}Current config need: {cls.min_width} x {cls.min_height}{Fx.ub}{Term.bg}{Term.fg}') cls.winch.wait(0.3) while Key.has_key(): if Key.last() == "q": clean_quit() cls.winch.clear() cls._w, cls._h = os.get_terminal_size() else: cls.winch.wait(0.3) cls.winch.clear() cls._w, cls._h = os.get_terminal_size() Key.mouse = {} Box.calc_sizes() Collector.proc_counter = 1 if Menu.active: Menu.resized = True Box.draw_bg(now=False) cls.resized = False Timer.finish() @staticmethod def echo(on: bool): """Toggle input echo""" (iflag, oflag, cflag, lflag, ispeed, ospeed, cc) = termios.tcgetattr(sys.stdin.fileno()) if on: lflag |= termios.ECHO # type: ignore else: lflag &= ~termios.ECHO # type: ignore new_attr = [iflag, oflag, cflag, lflag, ispeed, ospeed, cc] termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, new_attr) @staticmethod def title(text: str = "") -> str: out: str = f'{os.environ.get("TERMINAL_TITLE", "")}' if out and text: out += " " if text: out += f'{text}' return f'\033]0;{out}\a' class Fx: """Text effects * trans(string: str): Replace whitespace with escape move right to not overwrite background behind whitespace. * uncolor(string: str) : Removes all 24-bit color and returns string .""" start = "\033[" #* Escape sequence start sep = ";" #* Escape sequence separator end = "m" #* Escape sequence end reset = rs = "\033[0m" #* Reset foreground/background color and text effects bold = b = "\033[1m" #* Bold on unbold = ub = "\033[22m" #* Bold off dark = d = "\033[2m" #* Dark on undark = ud = "\033[22m" #* Dark off italic = i = "\033[3m" #* Italic on unitalic = ui = "\033[23m" #* Italic off underline = u = "\033[4m" #* Underline on ununderline = uu = "\033[24m" #* Underline off blink = bl = "\033[5m" #* Blink on unblink = ubl = "\033[25m" #* Blink off strike = s = "\033[9m" #* Strike / crossed-out on unstrike = us = "\033[29m" #* Strike / crossed-out off #* Precompiled regex for finding a 24-bit color escape sequence in a string color_re = re.compile(r"\033\[\d+;\d?;?\d*;?\d*;?\d*m") @staticmethod def trans(string: str): return string.replace(" ", "\033[1C") @classmethod def uncolor(cls, string: str) -> str: return f'{cls.color_re.sub("", string)}' class Raw(object): """Set raw input mode for device""" def __init__(self, stream): self.stream = stream self.fd = self.stream.fileno() def __enter__(self): self.original_stty = termios.tcgetattr(self.stream) tty.setcbreak(self.stream) def __exit__(self, type, value, traceback): termios.tcsetattr(self.stream, termios.TCSANOW, self.original_stty) class Nonblocking(object): """Set nonblocking mode for device""" def __init__(self, stream): self.stream = stream self.fd = self.stream.fileno() def __enter__(self): self.orig_fl = fcntl.fcntl(self.fd, fcntl.F_GETFL) fcntl.fcntl(self.fd, fcntl.F_SETFL, self.orig_fl | os.O_NONBLOCK) def __exit__(self, *args): fcntl.fcntl(self.fd, fcntl.F_SETFL, self.orig_fl) class Mv: """Class with collection of cursor movement functions: .t[o](line, column) | .r[ight](columns) | .l[eft](columns) | .u[p](lines) | .d[own](lines) | .save() | .restore()""" @staticmethod def to(line: int, col: int) -> str: return f'\033[{line};{col}f' #* Move cursor to line, column @staticmethod def right(x: int) -> str: #* Move cursor right x columns return f'\033[{x}C' @staticmethod def left(x: int) -> str: #* Move cursor left x columns return f'\033[{x}D' @staticmethod def up(x: int) -> str: #* Move cursor up x lines return f'\033[{x}A' @staticmethod def down(x: int) -> str: #* Move cursor down x lines return f'\033[{x}B' save: str = "\033[s" #* Save cursor position restore: str = "\033[u" #* Restore saved cursor position t = to r = right l = left u = up d = down class Key: """Handles the threaded input reader for keypresses and mouse events""" list: List[str] = [] mouse: Dict[str, List[List[int]]] = {} mouse_pos: Tuple[int, int] = (0, 0) escape: Dict[Union[str, Tuple[str, str]], str] = { "\n" : "enter", ("\x7f", "\x08") : "backspace", ("[A", "OA") : "up", ("[B", "OB") : "down", ("[D", "OD") : "left", ("[C", "OC") : "right", "[2~" : "insert", "[3~" : "delete", "[H" : "home", "[F" : "end", "[5~" : "page_up", "[6~" : "page_down", "\t" : "tab", "[Z" : "shift_tab", "OP" : "f1", "OQ" : "f2", "OR" : "f3", "OS" : "f4", "[15" : "f5", "[17" : "f6", "[18" : "f7", "[19" : "f8", "[20" : "f9", "[21" : "f10", "[23" : "f11", "[24" : "f12" } new = threading.Event() idle = threading.Event() mouse_move = threading.Event() mouse_report: bool = False idle.set() stopping: bool = False started: bool = False reader: threading.Thread @classmethod def start(cls): cls.stopping = False cls.reader = threading.Thread(target=cls._get_key) cls.reader.start() cls.started = True @classmethod def stop(cls): if cls.started and cls.reader.is_alive(): cls.stopping = True try: cls.reader.join() except: pass @classmethod def last(cls) -> str: if cls.list: return cls.list.pop() else: return "" @classmethod def get(cls) -> str: if cls.list: return cls.list.pop(0) else: return "" @classmethod def get_mouse(cls) -> Tuple[int, int]: if cls.new.is_set(): cls.new.clear() return cls.mouse_pos @classmethod def mouse_moved(cls) -> bool: if cls.mouse_move.is_set(): cls.mouse_move.clear() return True else: return False @classmethod def has_key(cls) -> bool: return bool(cls.list) @classmethod def clear(cls): cls.list = [] @classmethod def input_wait(cls, sec: float = 0.0, mouse: bool = False) -> bool: '''Returns True if key is detected else waits out timer and returns False''' if cls.list: return True if mouse: Draw.now(Term.mouse_direct_on) cls.new.wait(sec if sec > 0 else 0.0) if mouse: Draw.now(Term.mouse_direct_off, Term.mouse_on) if cls.new.is_set(): cls.new.clear() return True else: return False @classmethod def break_wait(cls): cls.list.append("_null") cls.new.set() sleep(0.01) cls.new.clear() @classmethod def _get_key(cls): """Get a key or escape sequence from stdin, convert to readable format and save to keys list. Meant to be run in it's own thread.""" input_key: str = "" clean_key: str = "" try: while not cls.stopping: with Raw(sys.stdin): if not select([sys.stdin], [], [], 0.1)[0]: #* Wait 100ms for input on stdin then restart loop to check for stop flag continue input_key += sys.stdin.read(1) #* Read 1 key safely with blocking on if input_key == "\033": #* If first character is a escape sequence keep reading cls.idle.clear() #* Report IO block in progress to prevent Draw functions from getting a IO Block error Draw.idle.wait() #* Wait for Draw function to finish if busy with Nonblocking(sys.stdin): #* Set non blocking to prevent read stall input_key += sys.stdin.read(20) if input_key.startswith("\033[<"): _ = sys.stdin.read(1000) cls.idle.set() #* Report IO blocking done #errlog.debug(f'{repr(input_key)}') if input_key == "\033": clean_key = "escape" #* Key is "escape" key if only containing \033 elif input_key.startswith(("\033[<0;", "\033[<35;", "\033[<64;", "\033[<65;")): #* Detected mouse event try: cls.mouse_pos = (int(input_key.split(";")[1]), int(input_key.split(";")[2].rstrip("mM"))) except: pass else: if input_key.startswith("\033[<35;"): #* Detected mouse move in mouse direct mode cls.mouse_move.set() cls.new.set() elif input_key.startswith("\033[<64;"): #* Detected mouse scroll up clean_key = "mouse_scroll_up" elif input_key.startswith("\033[<65;"): #* Detected mouse scroll down clean_key = "mouse_scroll_down" elif input_key.startswith("\033[<0;") and input_key.endswith("m"): #* Detected mouse click release if Menu.active: clean_key = "mouse_click" else: for key_name, positions in cls.mouse.items(): #* Check if mouse position is clickable if list(cls.mouse_pos) in positions: clean_key = key_name break else: clean_key = "mouse_click" elif input_key == "\\": clean_key = "\\" #* Clean up "\" to not return escaped else: for code in cls.escape.keys(): #* Go through dict of escape codes to get the cleaned key name if input_key.lstrip("\033").startswith(code): clean_key = cls.escape[code] break else: #* If not found in escape dict and length of key is 1, assume regular character if len(input_key) == 1: clean_key = input_key if clean_key: cls.list.append(clean_key) #* Store up to 10 keys in input queue for later processing if len(cls.list) > 10: del cls.list[0] clean_key = "" cls.new.set() #* Set threading event to interrupt main thread sleep input_key = "" except Exception as e: errlog.exception(f'Input thread failed with exception: {e}') cls.idle.set() cls.list.clear() clean_quit(1, thread=True) class Draw: '''Holds the draw buffer and manages IO blocking queue * .buffer([+]name[!], *args, append=False, now=False, z=100) : Add *args to buffer * - Adding "+" prefix to name sets append to True and appends to name's current string * - Adding "!" suffix to name sets now to True and print name's current string * .out(clear=False) : Print all strings in buffer, clear=True clear all buffers after * .now(*args) : Prints all arguments as a string * .clear(*names) : Clear named buffers, all if no argument * .last_screen() : Prints all saved buffers ''' strings: Dict[str, str] = {} z_order: Dict[str, int] = {} saved: Dict[str, str] = {} save: Dict[str, bool] = {} once: Dict[str, bool] = {} idle = threading.Event() idle.set() @classmethod def now(cls, *args): '''Wait for input reader and self to be idle then print to screen''' Key.idle.wait() cls.idle.wait() cls.idle.clear() try: print(*args, sep="", end="", flush=True) except BlockingIOError: pass Key.idle.wait() print(*args, sep="", end="", flush=True) cls.idle.set() @classmethod def buffer(cls, name: str, *args: str, append: bool = False, now: bool = False, z: int = 100, only_save: bool = False, no_save: bool = False, once: bool = False): string: str = "" if name.startswith("+"): name = name.lstrip("+") append = True if name.endswith("!"): name = name.rstrip("!") now = True cls.save[name] = not no_save cls.once[name] = once if not name in cls.z_order or z != 100: cls.z_order[name] = z if args: string = "".join(args) if only_save: if name not in cls.saved or not append: cls.saved[name] = "" cls.saved[name] += string else: if name not in cls.strings or not append: cls.strings[name] = "" cls.strings[name] += string if now: cls.out(name) @classmethod def out(cls, *names: str, clear = False): out: str = "" if not cls.strings: return if names: for name in sorted(cls.z_order, key=cls.z_order.get, reverse=True): #type: ignore if name in names and name in cls.strings: out += cls.strings[name] if cls.save[name]: cls.saved[name] = cls.strings[name] if clear or cls.once[name]: cls.clear(name) cls.now(out) else: for name in sorted(cls.z_order, key=cls.z_order.get, reverse=True): #type: ignore if name in cls.strings: out += cls.strings[name] if cls.save[name]: cls.saved[name] = cls.strings[name] if cls.once[name] and not clear: cls.clear(name) if clear: cls.clear() cls.now(out) @classmethod def saved_buffer(cls) -> str: out: str = "" for name in sorted(cls.z_order, key=cls.z_order.get, reverse=True): #type: ignore if name in cls.saved: out += cls.saved[name] return out @classmethod def clear(cls, *names, saved: bool = False): if names: for name in names: if name in cls.strings: del cls.strings[name] if name in cls.save: del cls.save[name] if name in cls.once: del cls.once[name] if saved: if name in cls.saved: del cls.saved[name] if name in cls.z_order: del cls.z_order[name] else: cls.strings = {} cls.save = {} cls.once = {} if saved: cls.saved = {} cls.z_order = {} class Color: '''Holds representations for a 24-bit color value __init__(color, depth="fg", default=False) -- color accepts 6 digit hexadecimal: string "#RRGGBB", 2 digit hexadecimal: string "#FF" or decimal RGB "255 255 255" as a string. -- depth accepts "fg" or "bg" __call__(*args) joins str arguments to a string and apply color __str__ returns escape sequence to set color __iter__ returns iteration over red, green and blue in integer values of 0-255. * Values: .hexa: str | .dec: Tuple[int, int, int] | .red: int | .green: int | .blue: int | .depth: str | .escape: str ''' hexa: str; dec: Tuple[int, int, int]; red: int; green: int; blue: int; depth: str; escape: str; default: bool def __init__(self, color: str, depth: str = "fg", default: bool = False): self.depth = depth self.default = default try: if not color: self.dec = (-1, -1, -1) self.hexa = "" self.red = self.green = self.blue = -1 self.escape = "\033[49m" if depth == "bg" and default else "" return elif color.startswith("#"): self.hexa = color if len(self.hexa) == 3: self.hexa += self.hexa[1:3] + self.hexa[1:3] c = int(self.hexa[1:3], base=16) self.dec = (c, c, c) elif len(self.hexa) == 7: self.dec = (int(self.hexa[1:3], base=16), int(self.hexa[3:5], base=16), int(self.hexa[5:7], base=16)) else: raise ValueError(f'Incorrectly formatted hexadecimal rgb string: {self.hexa}') else: c_t = tuple(map(int, color.split(" "))) if len(c_t) == 3: self.dec = c_t #type: ignore else: raise ValueError(f'RGB dec should be "0-255 0-255 0-255"') if not all(0 <= c <= 255 for c in self.dec): raise ValueError(f'One or more RGB values are out of range: {color}') except Exception as e: errlog.exception(str(e)) self.escape = "" return if self.dec and not self.hexa: self.hexa = f'{hex(self.dec[0]).lstrip("0x").zfill(2)}{hex(self.dec[1]).lstrip("0x").zfill(2)}{hex(self.dec[2]).lstrip("0x").zfill(2)}' if self.dec and self.hexa: self.red, self.green, self.blue = self.dec self.escape = f'\033[{38 if self.depth == "fg" else 48};2;{";".join(str(c) for c in self.dec)}m' if not CONFIG.truecolor or LOW_COLOR: self.escape = f'{self.truecolor_to_256(rgb=self.dec, depth=self.depth)}' def __str__(self) -> str: return self.escape def __repr__(self) -> str: return repr(self.escape) def __iter__(self) -> Iterable: for c in self.dec: yield c def __call__(self, *args: str) -> str: if len(args) < 1: return "" return f'{self.escape}{"".join(args)}{getattr(Term, self.depth)}' @staticmethod def truecolor_to_256(rgb: Tuple[int, int, int], depth: str="fg") -> str: out: str = "" pre: str = f'\033[{"38" if depth == "fg" else "48"};5;' greyscale: Tuple[int, int, int] = ( rgb[0] // 11, rgb[1] // 11, rgb[2] // 11 ) if greyscale[0] == greyscale[1] == greyscale[2]: out = f'{pre}{232 + greyscale[0]}m' else: out = f'{pre}{round(rgb[0] / 51) * 36 + round(rgb[1] / 51) * 6 + round(rgb[2] / 51) + 16}m' return out @staticmethod def escape_color(hexa: str = "", r: int = 0, g: int = 0, b: int = 0, depth: str = "fg") -> str: """Returns escape sequence to set color * accepts either 6 digit hexadecimal hexa="#RRGGBB", 2 digit hexadecimal: hexa="#FF" * or decimal RGB: r=0-255, g=0-255, b=0-255 * depth="fg" or "bg" """ dint: int = 38 if depth == "fg" else 48 color: str = "" if hexa: try: if len(hexa) == 3: c = int(hexa[1:], base=16) if CONFIG.truecolor and not LOW_COLOR: color = f'\033[{dint};2;{c};{c};{c}m' else: color = f'{Color.truecolor_to_256(rgb=(c, c, c), depth=depth)}' elif len(hexa) == 7: if CONFIG.truecolor and not LOW_COLOR: color = f'\033[{dint};2;{int(hexa[1:3], base=16)};{int(hexa[3:5], base=16)};{int(hexa[5:7], base=16)}m' else: color = f'{Color.truecolor_to_256(rgb=(int(hexa[1:3], base=16), int(hexa[3:5], base=16), int(hexa[5:7], base=16)), depth=depth)}' except ValueError as e: errlog.exception(f'{e}') else: if CONFIG.truecolor and not LOW_COLOR: color = f'\033[{dint};2;{r};{g};{b}m' else: color = f'{Color.truecolor_to_256(rgb=(r, g, b), depth=depth)}' return color @classmethod def fg(cls, *args) -> str: if len(args) > 2: return cls.escape_color(r=args[0], g=args[1], b=args[2], depth="fg") else: return cls.escape_color(hexa=args[0], depth="fg") @classmethod def bg(cls, *args) -> str: if len(args) > 2: return cls.escape_color(r=args[0], g=args[1], b=args[2], depth="bg") else: return cls.escape_color(hexa=args[0], depth="bg") class Colors: '''Standard colors for menus and dialogs''' default = Color("#cc") white = Color("#ff") red = Color("#bf3636") green = Color("#68bf36") blue = Color("#0fd7ff") yellow = Color("#db8b00") black_bg = Color("#00", depth="bg") null = Color("") class Theme: '''__init__ accepts a dict containing { "color_element" : "color" }''' themes: Dict[str, str] = {} cached: Dict[str, Dict[str, str]] = { "Default" : DEFAULT_THEME } current: str = "" main_bg = main_fg = title = hi_fg = selected_bg = selected_fg = inactive_fg = proc_misc = cpu_box = mem_box = net_box = proc_box = div_line = temp_start = temp_mid = temp_end = cpu_start = cpu_mid = cpu_end = free_start = free_mid = free_end = cached_start = cached_mid = cached_end = available_start = available_mid = available_end = used_start = used_mid = used_end = download_start = download_mid = download_end = upload_start = upload_mid = upload_end = graph_text = meter_bg = process_start = process_mid = process_end = Colors.default gradient: Dict[str, List[str]] = { "temp" : [], "cpu" : [], "free" : [], "cached" : [], "available" : [], "used" : [], "download" : [], "upload" : [], "proc" : [], "proc_color" : [], "process" : [], } def __init__(self, theme: str): self.refresh() self._load_theme(theme) def __call__(self, theme: str): for k in self.gradient.keys(): self.gradient[k] = [] self._load_theme(theme) def _load_theme(self, theme: str): tdict: Dict[str, str] if theme in self.cached: tdict = self.cached[theme] elif theme in self.themes: tdict = self._load_file(self.themes[theme]) self.cached[theme] = tdict else: errlog.warning(f'No theme named "{theme}" found!') theme = "Default" CONFIG.color_theme = theme tdict = DEFAULT_THEME self.current = theme #if CONFIG.color_theme != theme: CONFIG.color_theme = theme if not "graph_text" in tdict and "inactive_fg" in tdict: tdict["graph_text"] = tdict["inactive_fg"] if not "meter_bg" in tdict and "inactive_fg" in tdict: tdict["meter_bg"] = tdict["inactive_fg"] if not "process_start" in tdict and "cpu_start" in tdict: tdict["process_start"] = tdict["cpu_start"] tdict["process_mid"] = tdict.get("cpu_mid", "") tdict["process_end"] = tdict.get("cpu_end", "") #* Get key names from DEFAULT_THEME dict to not leave any color unset if missing from theme dict for item, value in DEFAULT_THEME.items(): default = item in ["main_fg", "main_bg"] depth = "bg" if item in ["main_bg", "selected_bg"] else "fg" if item in tdict: setattr(self, item, Color(tdict[item], depth=depth, default=default)) else: setattr(self, item, Color(value, depth=depth, default=default)) #* Create color gradients from one, two or three colors, 101 values indexed 0-100 self.proc_start, self.proc_mid, self.proc_end = self.main_fg, Colors.null, self.inactive_fg self.proc_color_start, self.proc_color_mid, self.proc_color_end = self.inactive_fg, Colors.null, self.process_start rgb: Dict[str, Tuple[int, int, int]] colors: List[List[int]] = [] for name in self.gradient: rgb = { "start" : getattr(self, f'{name}_start').dec, "mid" : getattr(self, f'{name}_mid').dec, "end" : getattr(self, f'{name}_end').dec } colors = [ list(getattr(self, f'{name}_start')) ] if rgb["end"][0] >= 0: r = 50 if rgb["mid"][0] >= 0 else 100 for first, second in ["start", "mid" if r == 50 else "end"], ["mid", "end"]: for i in range(r): colors += [[rgb[first][n] + i * (rgb[second][n] - rgb[first][n]) // r for n in range(3)]] if r == 100: break self.gradient[name] += [ Color.fg(*color) for color in colors ] else: c = Color.fg(*rgb["start"]) self.gradient[name] += [c] * 101 #* Set terminal colors Term.fg = f'{self.main_fg}' Term.bg = f'{self.main_bg}' if CONFIG.theme_background else "\033[49m" Draw.now(self.main_fg, self.main_bg) @classmethod def refresh(cls): '''Sets themes dict with names and paths to all found themes''' cls.themes = { "Default" : "Default" } try: for d in (THEME_DIR, USER_THEME_DIR): if not d: continue for f in os.listdir(d): if f.endswith(".theme"): cls.themes[f'{"" if d == THEME_DIR else "+"}{f[:-6]}'] = f'{d}/{f}' except Exception as e: errlog.exception(str(e)) @staticmethod def _load_file(path: str) -> Dict[str, str]: '''Load a bashtop formatted theme file and return a dict''' new_theme: Dict[str, str] = {} try: with open(path, "r") as f: for line in f: if not line.startswith("theme["): continue key = line[6:line.find("]")] s = line.find('"') value = line[s + 1:line.find('"', s + 1)] new_theme[key] = value except Exception as e: errlog.exception(str(e)) return new_theme class Banner: '''Holds the bpytop banner, .draw(line, [col=0], [center=False], [now=False])''' out: List[str] = [] c_color: str = "" length: int = 0 if not out: for num, (color, color2, line) in enumerate(BANNER_SRC): if len(line) > length: length = len(line) out_var = "" line_color = Color.fg(color) line_color2 = Color.fg(color2) line_dark = Color.fg(f'#{80 - num * 6}') for n, letter in enumerate(line): if letter == "█" and c_color != line_color: if 5 < n < 25: c_color = line_color2 else: c_color = line_color out_var += c_color elif letter == " ": letter = f'{Mv.r(1)}' c_color = "" elif letter != "█" and c_color != line_dark: c_color = line_dark out_var += line_dark out_var += letter out.append(out_var) @classmethod def draw(cls, line: int, col: int = 0, center: bool = False, now: bool = False): out: str = "" if center: col = Term.width // 2 - cls.length // 2 for n, o in enumerate(cls.out): out += f'{Mv.to(line + n, col)}{o}' out += f'{Term.fg}' if now: Draw.out(out) else: return out class Symbol: h_line: str = "─" v_line: str = "│" left_up: str = "┌" right_up: str = "┐" left_down: str = "└" right_down: str = "┘" title_left: str = "┤" title_right: str = "├" div_up: str = "┬" div_down: str = "┴" graph_up: Dict[float, str] = { 0.0 : " ", 0.1 : "⢀", 0.2 : "⢠", 0.3 : "⢰", 0.4 : "⢸", 1.0 : "⡀", 1.1 : "⣀", 1.2 : "⣠", 1.3 : "⣰", 1.4 : "⣸", 2.0 : "⡄", 2.1 : "⣄", 2.2 : "⣤", 2.3 : "⣴", 2.4 : "⣼", 3.0 : "⡆", 3.1 : "⣆", 3.2 : "⣦", 3.3 : "⣶", 3.4 : "⣾", 4.0 : "⡇", 4.1 : "⣇", 4.2 : "⣧", 4.3 : "⣷", 4.4 : "⣿" } graph_up_small = graph_up.copy() graph_up_small[0.0] = "\033[1C" graph_down: Dict[float, str] = { 0.0 : " ", 0.1 : "⠈", 0.2 : "⠘", 0.3 : "⠸", 0.4 : "⢸", 1.0 : "⠁", 1.1 : "⠉", 1.2 : "⠙", 1.3 : "⠹", 1.4 : "⢹", 2.0 : "⠃", 2.1 : "⠋", 2.2 : "⠛", 2.3 : "⠻", 2.4 : "⢻", 3.0 : "⠇", 3.1 : "⠏", 3.2 : "⠟", 3.3 : "⠿", 3.4 : "⢿", 4.0 : "⡇", 4.1 : "⡏", 4.2 : "⡟", 4.3 : "⡿", 4.4 : "⣿" } graph_down_small = graph_down.copy() graph_down_small[0.0] = "\033[1C" meter: str = "■" up: str = "↑" down: str = "↓" left: str = "←" right: str = "→" enter: str = "↲" ok: str = f'{Color.fg("#30ff50")}√{Color.fg("#cc")}' fail: str = f'{Color.fg("#ff3050")}!{Color.fg("#cc")}' class Graph: '''Class for creating and adding to graphs * __str__ : returns graph as a string * add(value: int) : adds a value to graph and returns it as a string * __call__ : same as add ''' out: str width: int height: int graphs: Dict[bool, List[str]] colors: List[str] invert: bool max_value: int color_max_value: int offset: int no_zero: bool round_up_low: bool current: bool last: int lowest: int = 0 symbol: Dict[float, str] def __init__(self, width: int, height: int, color: Union[List[str], Color, None], data: List[int], invert: bool = False, max_value: int = 0, offset: int = 0, color_max_value: Union[int, None] = None, no_zero: bool = False, round_up_low: bool = False): self.graphs: Dict[bool, List[str]] = {False : [], True : []} self.current: bool = True self.width = width self.height = height self.invert = invert self.offset = offset self.round_up_low = round_up_low self.no_zero = no_zero or round_up_low if not data: data = [0] if max_value: self.lowest = 1 if self.round_up_low else 0 self.max_value = max_value data = [ min_max((v + offset) * 100 // (max_value + offset), min_max(v + offset, 0, self.lowest), 100) for v in data ] #* Convert values to percentage values of max_value with max_value as ceiling else: self.max_value = 0 if color_max_value: self.color_max_value = color_max_value else: self.color_max_value = self.max_value if self.color_max_value and self.max_value: color_scale = int(100.0 * self.max_value / self.color_max_value) else: color_scale = 100 self.colors: List[str] = [] if isinstance(color, list) and height > 1: for i in range(1, height + 1): self.colors.insert(0, color[min(100, i * color_scale // height)]) #* Calculate colors of graph if invert: self.colors.reverse() elif isinstance(color, Color) and height > 1: self.colors = [ f'{color}' for _ in range(height) ] else: if isinstance(color, list): self.colors = color elif isinstance(color, Color): self.colors = [ f'{color}' for _ in range(101) ] if self.height == 1: self.symbol = Symbol.graph_down_small if invert else Symbol.graph_up_small else: self.symbol = Symbol.graph_down if invert else Symbol.graph_up value_width: int = ceil(len(data) / 2) filler: str = "" if value_width > width: #* If the size of given data set is bigger then width of graph, shrink data set data = data[-(width*2):] value_width = ceil(len(data) / 2) elif value_width < width: #* If the size of given data set is smaller then width of graph, fill graph with whitespace filler = self.symbol[0.0] * (width - value_width) if len(data) % 2: data.insert(0, 0) for _ in range(height): for b in [True, False]: self.graphs[b].append(filler) self._create(data, new=True) def _create(self, data: List[int], new: bool = False): h_high: int h_low: int value: Dict[str, int] = { "left" : 0, "right" : 0 } val: int side: str #* Create the graph for h in range(self.height): h_high = round(100 * (self.height - h) / self.height) if self.height > 1 else 100 h_low = round(100 * (self.height - (h + 1)) / self.height) if self.height > 1 else 0 for v in range(len(data)): if new: self.current = bool(v % 2) #* Switch between True and False graphs if new and v == 0: self.last = 0 for val, side in [self.last, "left"], [data[v], "right"]: # type: ignore if val >= h_high: value[side] = 4 elif val <= h_low: value[side] = 0 else: if self.height == 1: value[side] = round(val * 4 / 100 + 0.5) else: value[side] = round((val - h_low) * 4 / (h_high - h_low) + 0.1) if self.no_zero and not (new and v == 0 and side == "left") and h == self.height - 1 and value[side] < 1 and not (self.round_up_low and val == 0): value[side] = 1 if new: self.last = data[v] self.graphs[self.current][h] += self.symbol[float(value["left"] + value["right"] / 10)] if data: self.last = data[-1] self.out = "" if self.height == 1: self.out += f'{"" if not self.colors else (THEME.inactive_fg if self.last < 5 else self.colors[self.last])}{self.graphs[self.current][0]}' elif self.height > 1: for h in range(self.height): if h > 0: self.out += f'{Mv.d(1)}{Mv.l(self.width)}' self.out += f'{"" if not self.colors else self.colors[h]}{self.graphs[self.current][h if not self.invert else (self.height - 1) - h]}' if self.colors: self.out += f'{Term.fg}' def __call__(self, value: Union[int, None] = None) -> str: if not isinstance(value, int): return self.out self.current = not self.current if self.height == 1: if self.graphs[self.current][0].startswith(self.symbol[0.0]): self.graphs[self.current][0] = self.graphs[self.current][0].replace(self.symbol[0.0], "", 1) else: self.graphs[self.current][0] = self.graphs[self.current][0][1:] else: for n in range(self.height): self.graphs[self.current][n] = self.graphs[self.current][n][1:] if self.max_value: value = min_max((value + self.offset) * 100 // (self.max_value + self.offset), min_max(value + self.offset, 0, self.lowest), 100) self._create([value]) return self.out def add(self, value: Union[int, None] = None) -> str: return self.__call__(value) def __str__(self): return self.out def __repr__(self): return repr(self.out) class Graphs: '''Holds all graphs and lists of graphs for dynamically created graphs''' cpu: Dict[str, Graph] = {} cores: List[Graph] = [NotImplemented] * THREADS temps: List[Graph] = [NotImplemented] * (THREADS + 1) net: Dict[str, Graph] = {} detailed_cpu: Graph = NotImplemented detailed_mem: Graph = NotImplemented pid_cpu: Dict[int, Graph] = {} disk_io: Dict[str, Dict[str, Graph]] = {} class Meter: '''Creates a percentage meter __init__(value, width, theme, gradient_name) to create new meter __call__(value) to set value and return meter as a string __str__ returns last set meter as a string ''' out: str color_gradient: List[str] color_inactive: Color gradient_name: str width: int invert: bool saved: Dict[int, str] def __init__(self, value: int, width: int, gradient_name: str, invert: bool = False): self.gradient_name = gradient_name self.color_gradient = THEME.gradient[gradient_name] self.color_inactive = THEME.meter_bg self.width = width self.saved = {} self.invert = invert self.out = self._create(value) def __call__(self, value: Union[int, None]) -> str: if not isinstance(value, int): return self.out if value > 100: value = 100 elif value < 0: value = 100 if value in self.saved: self.out = self.saved[value] else: self.out = self._create(value) return self.out def __str__(self) -> str: return self.out def __repr__(self): return repr(self.out) def _create(self, value: int) -> str: if value > 100: value = 100 elif value < 0: value = 100 out: str = "" for i in range(1, self.width + 1): if value >= round(i * 100 / self.width): out += f'{self.color_gradient[round(i * 100 / self.width) if not self.invert else round(100 - (i * 100 / self.width))]}{Symbol.meter}' else: out += self.color_inactive(Symbol.meter * (self.width + 1 - i)) break else: out += f'{Term.fg}' if not value in self.saved: self.saved[value] = out return out class Meters: cpu: Meter battery: Meter mem: Dict[str, Union[Meter, Graph]] = {} swap: Dict[str, Union[Meter, Graph]] = {} disks_used: Dict[str, Meter] = {} disks_free: Dict[str, Meter] = {} class Box: '''Box class with all needed attributes for create_box() function''' name: str num: int = 0 boxes: List = [] view_modes: Dict[str, List] = {"full" : ["cpu", "mem", "net", "proc"], "stat" : ["cpu", "mem", "net"], "proc" : ["cpu", "proc"]} view_mode: str for view_mode in view_modes: if sorted(CONFIG.shown_boxes.split(), key=str.lower) == view_modes[view_mode]: break else: view_mode = "user" view_modes["user"] = CONFIG.shown_boxes.split() height_p: int width_p: int x: int y: int width: int height: int out: str bg: str _b_cpu_h: int _b_mem_h: int redraw_all: bool buffers: List[str] = [] c_counter: int = 0 clock_on: bool = False clock: str = "" clock_len: int = 0 resized: bool = False clock_custom_format: Dict[str, Any] = { "/host" : os.uname()[1], "/user" : os.environ.get("USER") or pwd.getpwuid(os.getuid())[0], "/uptime" : "", } if clock_custom_format["/host"].endswith(".local"): clock_custom_format["/host"] = clock_custom_format["/host"].replace(".local", "") @classmethod def calc_sizes(cls): '''Calculate sizes of boxes''' cls.boxes = CONFIG.shown_boxes.split() for sub in cls.__subclasses__(): sub._calc_size() # type: ignore sub.resized = True # type: ignore @classmethod def draw_update_ms(cls, now: bool = True): if not "cpu" in cls.boxes: return update_string: str = f'{CONFIG.update_ms}ms' xpos: int = CpuBox.x + CpuBox.width - len(update_string) - 15 if not "+" in Key.mouse: Key.mouse["+"] = [[xpos + 7 + i, CpuBox.y] for i in range(3)] Key.mouse["-"] = [[CpuBox.x + CpuBox.width - 4 + i, CpuBox.y] for i in range(3)] Draw.buffer("update_ms!" if now and not Menu.active else "update_ms", f'{Mv.to(CpuBox.y, xpos)}{THEME.cpu_box(Symbol.h_line * 7, Symbol.title_left)}{Fx.b}{THEME.hi_fg("+")} ', f'{THEME.title(update_string)} {THEME.hi_fg("-")}{Fx.ub}{THEME.cpu_box(Symbol.title_right)}', only_save=Menu.active, once=True) if now and not Menu.active: Draw.clear("update_ms") if CONFIG.show_battery and hasattr(psutil, "sensors_battery") and psutil.sensors_battery(): Draw.out("battery") @classmethod def draw_clock(cls, force: bool = False): if not "cpu" in cls.boxes or not cls.clock_on: return cls.c_counter += 1 if cls.c_counter > 3600 / (Config.update_ms / 1000): tzset() cls.c_counter = 0 out: str = "" if force: pass elif Term.resized or strftime(CONFIG.draw_clock) == cls.clock: return clock_string = cls.clock = strftime(CONFIG.draw_clock) for custom in cls.clock_custom_format: if custom in clock_string: if custom == "/uptime": cls.clock_custom_format["/uptime"] = CpuCollector.uptime clock_string = clock_string.replace(custom, cls.clock_custom_format[custom]) clock_len = len(clock_string[:(CpuBox.width-56)]) if cls.clock_len != clock_len and not CpuBox.resized: out = f'{Mv.to(CpuBox.y, ((CpuBox.width)//2)-(cls.clock_len//2))}{Fx.ub}{THEME.cpu_box}{Symbol.h_line * cls.clock_len}' cls.clock_len = clock_len now: bool = False if Menu.active else not force out += (f'{Mv.to(CpuBox.y, ((CpuBox.width)//2)-(clock_len//2))}{Fx.ub}{THEME.cpu_box}' f'{Symbol.title_left}{Fx.b}{THEME.title(clock_string[:clock_len])}{Fx.ub}{THEME.cpu_box}{Symbol.title_right}{Term.fg}') Draw.buffer("clock", out, z=1, now=now, once=not force, only_save=Menu.active) if now and not Menu.active: if CONFIG.show_battery and hasattr(psutil, "sensors_battery") and psutil.sensors_battery(): Draw.out("battery") @classmethod def empty_bg(cls) -> str: return (f'{Term.clear}' + (f'{Banner.draw(Term.height // 2 - 10, center=True)}' f'{Mv.d(1)}{Mv.l(46)}{Colors.black_bg}{Colors.default}{Fx.b}[esc] Menu' f'{Mv.r(25)}{Fx.i}Version: {VERSION}{Fx.ui}' if Term.height > 22 else "") + f'{Mv.d(1)}{Mv.l(34)}{Fx.b}All boxes hidden!' f'{Mv.d(1)}{Mv.l(17)}{Fx.b}[1] {Fx.ub}Toggle CPU box' f'{Mv.d(1)}{Mv.l(18)}{Fx.b}[2] {Fx.ub}Toggle MEM box' f'{Mv.d(1)}{Mv.l(18)}{Fx.b}[3] {Fx.ub}Toggle NET box' f'{Mv.d(1)}{Mv.l(18)}{Fx.b}[4] {Fx.ub}Toggle PROC box' f'{Mv.d(1)}{Mv.l(19)}{Fx.b}[m] {Fx.ub}Cycle presets' f'{Mv.d(1)}{Mv.l(17)}{Fx.b}[q] Quit {Fx.ub}{Term.bg}{Term.fg}') @classmethod def draw_bg(cls, now: bool = True): '''Draw all boxes outlines and titles''' out: str = "" if not cls.boxes: out = cls.empty_bg() else: out = "".join(sub._draw_bg() for sub in cls.__subclasses__()) # type: ignore Draw.buffer("bg", out, now=now, z=1000, only_save=Menu.active, once=True) cls.draw_update_ms(now=now) if CONFIG.draw_clock: cls.draw_clock(force=True) class SubBox: box_x: int = 0 box_y: int = 0 box_width: int = 0 box_height: int = 0 box_columns: int = 0 column_size: int = 0 class CpuBox(Box, SubBox): name = "cpu" num = 1 x = 1 y = 1 height_p = 32 width_p = 100 min_w: int = 60 min_h: int = 8 resized: bool = True redraw: bool = False buffer: str = "cpu" battery_percent: int = 1000 battery_secs: int = 0 battery_status: str = "Unknown" old_battery_pos = 0 old_battery_len = 0 battery_path: Union[str, None] = "" battery_clear: bool = False battery_symbols: Dict[str, str] = {"Charging": "▲", "Discharging": "▼", "Full": "■", "Not charging": "■"} clock_block: bool = True Box.buffers.append(buffer) @classmethod def _calc_size(cls): if not "cpu" in cls.boxes: Box._b_cpu_h = 0 cls.width = Term.width return cpu = CpuCollector height_p: int if cls.boxes == ["cpu"]: height_p = 100 else: height_p = cls.height_p cls.width = round(Term.width * cls.width_p / 100) cls.height = round(Term.height * height_p / 100) if cls.height < 8: cls.height = 8 Box._b_cpu_h = cls.height #THREADS = 64 cls.box_columns = ceil((THREADS + 1) / (cls.height - 5)) if cls.box_columns * (20 + 13 if cpu.got_sensors else 21) < cls.width - (cls.width // 3): cls.column_size = 2 cls.box_width = (20 + 13 if cpu.got_sensors else 21) * cls.box_columns - ((cls.box_columns - 1) * 1) elif cls.box_columns * (15 + 6 if cpu.got_sensors else 15) < cls.width - (cls.width // 3): cls.column_size = 1 cls.box_width = (15 + 6 if cpu.got_sensors else 15) * cls.box_columns - ((cls.box_columns - 1) * 1) elif cls.box_columns * (8 + 6 if cpu.got_sensors else 8) < cls.width - (cls.width // 3): cls.column_size = 0 else: cls.box_columns = (cls.width - cls.width // 3) // (8 + 6 if cpu.got_sensors else 8); cls.column_size = 0 if cls.column_size == 0: cls.box_width = (8 + 6 if cpu.got_sensors else 8) * cls.box_columns + 1 cls.box_height = ceil(THREADS / cls.box_columns) + 4 if cls.box_height > cls.height - 2: cls.box_height = cls.height - 2 cls.box_x = (cls.width - 1) - cls.box_width cls.box_y = cls.y + ceil((cls.height - 2) / 2) - ceil(cls.box_height / 2) + 1 @classmethod def _draw_bg(cls) -> str: if not "cpu" in cls.boxes: return "" if not "M" in Key.mouse: Key.mouse["M"] = [[cls.x + 10 + i, cls.y] for i in range(6)] return (f'{create_box(box=cls, line_color=THEME.cpu_box)}' f'{Mv.to(cls.y, cls.x + 10)}{THEME.cpu_box(Symbol.title_left)}{Fx.b}{THEME.hi_fg("M")}{THEME.title("enu")}{Fx.ub}{THEME.cpu_box(Symbol.title_right)}' f'{create_box(x=cls.box_x, y=cls.box_y, width=cls.box_width, height=cls.box_height, line_color=THEME.div_line, fill=False, title=CPU_NAME[:cls.box_width - 14] if not CONFIG.custom_cpu_name else CONFIG.custom_cpu_name[:cls.box_width - 14])}') @classmethod def battery_activity(cls) -> bool: if not hasattr(psutil, "sensors_battery") or psutil.sensors_battery() == None: if cls.battery_percent != 1000: cls.battery_clear = True return False if cls.battery_path == "": cls.battery_path = None if os.path.isdir("/sys/class/power_supply"): for directory in sorted(os.listdir("/sys/class/power_supply")): if directory.startswith('BAT') or 'battery' in directory.lower(): cls.battery_path = f'/sys/class/power_supply/{directory}/' break return_true: bool = False percent: int = ceil(getattr(psutil.sensors_battery(), "percent", 0)) if percent != cls.battery_percent: cls.battery_percent = percent return_true = True seconds: int = getattr(psutil.sensors_battery(), "secsleft", 0) if seconds != cls.battery_secs: cls.battery_secs = seconds return_true = True status: str = "not_set" if cls.battery_path: status = readfile(cls.battery_path + "status", default="not_set") if status == "not_set" and getattr(psutil.sensors_battery(), "power_plugged", None) == True: status = "Charging" if cls.battery_percent < 100 else "Full" elif status == "not_set" and getattr(psutil.sensors_battery(), "power_plugged", None) == False: status = "Discharging" elif status == "not_set": status = "Unknown" if status != cls.battery_status: cls.battery_status = status return_true = True return return_true or cls.resized or cls.redraw or Menu.active @classmethod def _draw_fg(cls): if not "cpu" in cls.boxes: return cpu = CpuCollector if cpu.redraw: cls.redraw = True out: str = "" out_misc: str = "" lavg: str = "" x, y, w, h = cls.x + 1, cls.y + 1, cls.width - 2, cls.height - 2 bx, by, bw, bh = cls.box_x + 1, cls.box_y + 1, cls.box_width - 2, cls.box_height - 2 hh: int = ceil(h / 2) hh2: int = h - hh mid_line: bool = False temp: int = 0 unit: str = "" if not CONFIG.cpu_single_graph and CONFIG.cpu_graph_upper != CONFIG.cpu_graph_lower: mid_line = True if h % 2: hh = floor(h / 2) else: hh2 -= 1 hide_cores: bool = (cpu.cpu_temp_only or not CONFIG.show_coretemp) and cpu.got_sensors ct_width: int = (max(6, 6 * cls.column_size)) * hide_cores if cls.resized or cls.redraw: if not "m" in Key.mouse: Key.mouse["m"] = [[cls.x + 16 + i, cls.y] for i in range(12)] out_misc += f'{Mv.to(cls.y, cls.x + 16)}{THEME.cpu_box(Symbol.title_left)}{Fx.b}{THEME.hi_fg("m")}{THEME.title}ode:{Box.view_mode}{Fx.ub}{THEME.cpu_box(Symbol.title_right)}' Graphs.cpu["up"] = Graph(w - bw - 3, (h if CONFIG.cpu_single_graph else hh), THEME.gradient["cpu"], cpu.cpu_upper, round_up_low=True) if not CONFIG.cpu_single_graph: Graphs.cpu["down"] = Graph(w - bw - 3, hh2, THEME.gradient["cpu"], cpu.cpu_lower, invert=CONFIG.cpu_invert_lower, round_up_low=True) Meters.cpu = Meter(cpu.cpu_usage[0][-1], bw - (21 if cpu.got_sensors else 9), "cpu") if cls.column_size > 0 or ct_width > 0: for n in range(THREADS): Graphs.cores[n] = Graph(5 * cls.column_size + ct_width, 1, None, cpu.cpu_usage[n + 1]) if cpu.got_sensors: Graphs.temps[0] = Graph(5, 1, None, cpu.cpu_temp[0], max_value=cpu.cpu_temp_crit, offset=-23) if cls.column_size > 1: for n in range(1, THREADS + 1): if not cpu.cpu_temp[n]: continue Graphs.temps[n] = Graph(5, 1, None, cpu.cpu_temp[n], max_value=cpu.cpu_temp_crit, offset=-23) Draw.buffer("cpu_misc", out_misc, only_save=True) if CONFIG.show_battery and cls.battery_activity(): bat_out: str = "" if cls.battery_secs > 0: battery_time: str = f' {cls.battery_secs // 3600:02}:{(cls.battery_secs % 3600) // 60:02}' else: battery_time = "" if not hasattr(Meters, "battery") or cls.resized: Meters.battery = Meter(cls.battery_percent, 10, "cpu", invert=True) battery_symbol: str = cls.battery_symbols.get(cls.battery_status, "○") battery_len: int = len(f'{CONFIG.update_ms}') + (11 if cls.width >= 100 else 0) + len(battery_time) + len(f'{cls.battery_percent}') battery_pos = cls.width - battery_len - 17 if (battery_pos != cls.old_battery_pos or battery_len != cls.old_battery_len) and cls.old_battery_pos > 0 and not cls.resized: bat_out += f'{Mv.to(y-1, cls.old_battery_pos)}{THEME.cpu_box(Symbol.h_line*(cls.old_battery_len+4))}' cls.old_battery_pos, cls.old_battery_len = battery_pos, battery_len bat_out += (f'{Mv.to(y-1, battery_pos)}{THEME.cpu_box(Symbol.title_left)}{Fx.b}{THEME.title}BAT{battery_symbol} {cls.battery_percent}%'+ ("" if cls.width < 100 else f' {Fx.ub}{Meters.battery(cls.battery_percent)}{Fx.b}') + f'{THEME.title}{battery_time}{Fx.ub}{THEME.cpu_box(Symbol.title_right)}') Draw.buffer("battery", f'{bat_out}{Term.fg}', only_save=Menu.active) elif cls.battery_clear: out += f'{Mv.to(y-1, cls.old_battery_pos)}{THEME.cpu_box(Symbol.h_line*(cls.old_battery_len+4))}' cls.battery_clear = False cls.battery_percent = 1000 cls.battery_secs = 0 cls.battery_status = "Unknown" cls.old_battery_pos = 0 cls.old_battery_len = 0 cls.battery_path = "" Draw.clear("battery", saved=True) cx = cy = cc = 0 ccw = (bw + 1) // cls.box_columns if cpu.cpu_freq: freq: str = f'{cpu.cpu_freq} Mhz' if cpu.cpu_freq < 1000 else f'{float(cpu.cpu_freq / 1000):.1f} GHz' out += f'{Mv.to(by - 1, bx + bw - 9)}{THEME.div_line(Symbol.title_left)}{Fx.b}{THEME.title(freq)}{Fx.ub}{THEME.div_line(Symbol.title_right)}' out += f'{Mv.to(y, x)}{Graphs.cpu["up"](None if cls.resized else cpu.cpu_upper[-1])}' if mid_line: out += (f'{Mv.to(y+hh, x-1)}{THEME.cpu_box(Symbol.title_right)}{THEME.div_line}{Symbol.h_line * (w - bw - 3)}{THEME.div_line(Symbol.title_left)}' f'{Mv.to(y+hh, x+((w-bw)//2)-((len(CONFIG.cpu_graph_upper)+len(CONFIG.cpu_graph_lower))//2)-4)}{THEME.main_fg}{CONFIG.cpu_graph_upper}{Mv.r(1)}▲▼{Mv.r(1)}{CONFIG.cpu_graph_lower}') if not CONFIG.cpu_single_graph and Graphs.cpu.get("down"): out += f'{Mv.to(y + hh + (1 * mid_line), x)}{Graphs.cpu["down"](None if cls.resized else cpu.cpu_lower[-1])}' out += (f'{THEME.main_fg}{Mv.to(by + cy, bx + cx)}{Fx.b}{"CPU "}{Fx.ub}{Meters.cpu(cpu.cpu_usage[0][-1])}' f'{THEME.gradient["cpu"][cpu.cpu_usage[0][-1]]}{cpu.cpu_usage[0][-1]:>4}{THEME.main_fg}%') if cpu.got_sensors: try: temp, unit = temperature(cpu.cpu_temp[0][-1], CONFIG.temp_scale) out += (f'{THEME.inactive_fg} ⡀⡀⡀⡀⡀{Mv.l(5)}{THEME.gradient["temp"][min_max(cpu.cpu_temp[0][-1], 0, cpu.cpu_temp_crit) * 100 // cpu.cpu_temp_crit]}{Graphs.temps[0](None if cls.resized else cpu.cpu_temp[0][-1])}' f'{temp:>4}{THEME.main_fg}{unit}') except: cpu.got_sensors = False cy += 1 for n in range(1, THREADS + 1): out += f'{THEME.main_fg}{Mv.to(by + cy, bx + cx)}{Fx.b + "C" + Fx.ub if THREADS < 100 else ""}{str(n):<{2 if cls.column_size == 0 else 3}}' if cls.column_size > 0 or ct_width > 0: out += f'{THEME.inactive_fg}{"⡀" * (5 * cls.column_size + ct_width)}{Mv.l(5 * cls.column_size + ct_width)}{THEME.gradient["cpu"][cpu.cpu_usage[n][-1]]}{Graphs.cores[n-1](None if cls.resized else cpu.cpu_usage[n][-1])}' else: out += f'{THEME.gradient["cpu"][cpu.cpu_usage[n][-1]]}' out += f'{cpu.cpu_usage[n][-1]:>{3 if cls.column_size < 2 else 4}}{THEME.main_fg}%' if cpu.got_sensors and cpu.cpu_temp[n] and not hide_cores: try: temp, unit = temperature(cpu.cpu_temp[n][-1], CONFIG.temp_scale) if cls.column_size > 1: out += f'{THEME.inactive_fg} ⡀⡀⡀⡀⡀{Mv.l(5)}{THEME.gradient["temp"][min_max(cpu.cpu_temp[n][-1], 0, cpu.cpu_temp_crit) * 100 // cpu.cpu_temp_crit]}{Graphs.temps[n](None if cls.resized else cpu.cpu_temp[n][-1])}' else: out += f'{THEME.gradient["temp"][min_max(temp, 0, cpu.cpu_temp_crit) * 100 // cpu.cpu_temp_crit]}' out += f'{temp:>4}{THEME.main_fg}{unit}' except: cpu.got_sensors = False elif cpu.got_sensors and not hide_cores: out += f'{Mv.r(max(6, 6 * cls.column_size))}' out += f'{THEME.div_line(Symbol.v_line)}' cy += 1 if cy > ceil(THREADS/cls.box_columns) and n != THREADS: cc += 1; cy = 1; cx = ccw * cc if cc == cls.box_columns: break if cy < bh - 1: cy = bh - 1 if cy < bh and cc < cls.box_columns: if cls.column_size == 2 and cpu.got_sensors: lavg = f' Load AVG: {" ".join(str(l) for l in cpu.load_avg):^19.19}' elif cls.column_size == 2 or (cls.column_size == 1 and cpu.got_sensors): lavg = f'LAV: {" ".join(str(l) for l in cpu.load_avg):^14.14}' elif cls.column_size == 1 or (cls.column_size == 0 and cpu.got_sensors): lavg = f'L {" ".join(str(round(l, 1)) for l in cpu.load_avg):^11.11}' else: lavg = f'{" ".join(str(round(l, 1)) for l in cpu.load_avg[:2]):^7.7}' out += f'{Mv.to(by + cy, bx + cx)}{THEME.main_fg}{lavg}{THEME.div_line(Symbol.v_line)}' if CONFIG.show_uptime: out += f'{Mv.to(y + (0 if not CONFIG.cpu_invert_lower or CONFIG.cpu_single_graph else h - 1), x + 1)}{THEME.graph_text}{Fx.trans("up " + cpu.uptime)}' Draw.buffer(cls.buffer, f'{out_misc}{out}{Term.fg}', only_save=Menu.active) cls.resized = cls.redraw = cls.clock_block = False class MemBox(Box): name = "mem" num = 2 height_p = 38 width_p = 45 min_w: int = 36 min_h: int = 10 x = 1 y = 1 mem_meter: int = 0 mem_size: int = 0 disk_meter: int = 0 divider: int = 0 mem_width: int = 0 disks_width: int = 0 disks_io_h: int = 0 disks_io_order: List[str] = [] graph_speeds: Dict[str, int] = {} graph_height: int resized: bool = True redraw: bool = False buffer: str = "mem" swap_on: bool = CONFIG.show_swap Box.buffers.append(buffer) mem_names: List[str] = ["used", "available", "cached", "free"] swap_names: List[str] = ["used", "free"] @classmethod def _calc_size(cls): if not "mem" in cls.boxes: Box._b_mem_h = 0 cls.width = Term.width return width_p: int; height_p: int if not "proc" in cls.boxes: width_p = 100 else: width_p = cls.width_p if not "cpu" in cls.boxes: height_p = 60 if "net" in cls.boxes else 98 elif not "net" in cls.boxes: height_p = 98 - CpuBox.height_p else: height_p = cls.height_p cls.width = round(Term.width * width_p / 100) cls.height = round(Term.height * height_p / 100) + 1 if cls.height + Box._b_cpu_h > Term.height: cls.height = Term.height - Box._b_cpu_h Box._b_mem_h = cls.height cls.y = Box._b_cpu_h + 1 if CONFIG.show_disks: cls.mem_width = ceil((cls.width - 3) / 2) cls.disks_width = cls.width - cls.mem_width - 3 if cls.mem_width + cls.disks_width < cls.width - 2: cls.mem_width += 1 cls.divider = cls.x + cls.mem_width else: cls.mem_width = cls.width - 1 item_height: int = 6 if cls.swap_on and not CONFIG.swap_disk else 4 if cls.height - (3 if cls.swap_on and not CONFIG.swap_disk else 2) > 2 * item_height: cls.mem_size = 3 elif cls.mem_width > 25: cls.mem_size = 2 else: cls.mem_size = 1 cls.mem_meter = cls.width - (cls.disks_width if CONFIG.show_disks else 0) - (9 if cls.mem_size > 2 else 20) if cls.mem_size == 1: cls.mem_meter += 6 if cls.mem_meter < 1: cls.mem_meter = 0 if CONFIG.mem_graphs: cls.graph_height = round(((cls.height - (2 if cls.swap_on and not CONFIG.swap_disk else 1)) - (2 if cls.mem_size == 3 else 1) * item_height) / item_height) if cls.graph_height == 0: cls.graph_height = 1 if cls.graph_height > 1: cls.mem_meter += 6 else: cls.graph_height = 0 if CONFIG.show_disks: cls.disk_meter = cls.width - cls.mem_width - 23 if cls.disks_width < 25: cls.disk_meter += 10 if cls.disk_meter < 1: cls.disk_meter = 0 @classmethod def _draw_bg(cls) -> str: if not "mem" in cls.boxes: return "" out: str = "" out += f'{create_box(box=cls, line_color=THEME.mem_box)}' if CONFIG.show_disks: out += (f'{Mv.to(cls.y, cls.divider + 2)}{THEME.mem_box(Symbol.title_left)}{Fx.b}{THEME.hi_fg("d")}{THEME.title("isks")}{Fx.ub}{THEME.mem_box(Symbol.title_right)}' f'{Mv.to(cls.y, cls.divider)}{THEME.mem_box(Symbol.div_up)}' f'{Mv.to(cls.y + cls.height - 1, cls.divider)}{THEME.mem_box(Symbol.div_down)}{THEME.div_line}' f'{"".join(f"{Mv.to(cls.y + i, cls.divider)}{Symbol.v_line}" for i in range(1, cls.height - 1))}') Key.mouse["d"] = [[cls.divider + 3 + i, cls.y] for i in range(5)] else: out += f'{Mv.to(cls.y, cls.x + cls.width - 9)}{THEME.mem_box(Symbol.title_left)}{THEME.hi_fg("d")}{THEME.title("isks")}{THEME.mem_box(Symbol.title_right)}' Key.mouse["d"] = [[cls.x + cls.width - 8 + i, cls.y] for i in range(5)] return out @classmethod def _draw_fg(cls): if not "mem" in cls.boxes: return mem = MemCollector if mem.redraw: cls.redraw = True out: str = "" out_misc: str = "" gbg: str = "" gmv: str = "" gli: str = "" x, y, w, h = cls.x + 1, cls.y + 1, cls.width - 2, cls.height - 2 if cls.resized or cls.redraw: cls.redraw = True cls._calc_size() out_misc += cls._draw_bg() Meters.mem = {} Meters.swap = {} Meters.disks_used = {} Meters.disks_free = {} if cls.mem_meter > 0: for name in cls.mem_names: if CONFIG.mem_graphs: Meters.mem[name] = Graph(cls.mem_meter, cls.graph_height, THEME.gradient[name], mem.vlist[name]) else: Meters.mem[name] = Meter(mem.percent[name], cls.mem_meter, name) if cls.swap_on: for name in cls.swap_names: if CONFIG.swap_disk and CONFIG.show_disks: break elif CONFIG.mem_graphs and not CONFIG.swap_disk: Meters.swap[name] = Graph(cls.mem_meter, cls.graph_height, THEME.gradient[name], mem.swap_vlist[name]) else: Meters.swap[name] = Meter(mem.swap_percent[name], cls.mem_meter, name) if CONFIG.show_disks and mem.disks: if CONFIG.show_io_stat or CONFIG.io_mode: d_graph: List[str] = [] d_no_graph: List[str] = [] l_vals: List[Tuple[str, int, str, bool]] = [] if CONFIG.io_mode: cls.disks_io_h = (cls.height - 2 - len(mem.disks)) // max(1, len(mem.disks_io_dict)) if cls.disks_io_h < 2: cls.disks_io_h = 1 if CONFIG.io_graph_combined else 2 else: cls.disks_io_h = 1 if CONFIG.io_graph_speeds and not cls.graph_speeds: try: cls.graph_speeds = { spds.split(":")[0] : int(spds.split(":")[1]) for spds in list(i.strip() for i in CONFIG.io_graph_speeds.split(","))} except (KeyError, ValueError): errlog.error("Wrong formatting in io_graph_speeds variable. Using defaults.") for name in mem.disks.keys(): if name in mem.disks_io_dict: d_graph.append(name) else: d_no_graph.append(name) continue if CONFIG.io_graph_combined or not CONFIG.io_mode: l_vals = [("rw", cls.disks_io_h, "available", False)] else: l_vals = [("read", cls.disks_io_h // 2, "free", False), ("write", cls.disks_io_h // 2, "used", True)] Graphs.disk_io[name] = {_name : Graph(width=cls.disks_width - (6 if not CONFIG.io_mode else 0), height=_height, color=THEME.gradient[_gradient], data=mem.disks_io_dict[name][_name], invert=_invert, max_value=cls.graph_speeds.get(name, 10), no_zero=True) for _name, _height, _gradient, _invert in l_vals} cls.disks_io_order = d_graph + d_no_graph if cls.disk_meter > 0: for n, name in enumerate(mem.disks.keys()): if n * 2 > h: break Meters.disks_used[name] = Meter(mem.disks[name]["used_percent"], cls.disk_meter, "used") if len(mem.disks) * 3 <= h + 1: Meters.disks_free[name] = Meter(mem.disks[name]["free_percent"], cls.disk_meter, "free") if not "g" in Key.mouse: Key.mouse["g"] = [[x + 8 + i, y-1] for i in range(5)] out_misc += (f'{Mv.to(y-1, x + 7)}{THEME.mem_box(Symbol.title_left)}{Fx.b if CONFIG.mem_graphs else ""}' f'{THEME.hi_fg("g")}{THEME.title("raph")}{Fx.ub}{THEME.mem_box(Symbol.title_right)}') if CONFIG.show_disks: if not "s" in Key.mouse: Key.mouse["s"] = [[x + w - 6 + i, y-1] for i in range(4)] out_misc += (f'{Mv.to(y-1, x + w - 7)}{THEME.mem_box(Symbol.title_left)}{Fx.b if CONFIG.swap_disk else ""}' f'{THEME.hi_fg("s")}{THEME.title("wap")}{Fx.ub}{THEME.mem_box(Symbol.title_right)}') if not "i" in Key.mouse: Key.mouse["i"] = [[x + w - 10 + i, y-1] for i in range(2)] out_misc += (f'{Mv.to(y-1, x + w - 11)}{THEME.mem_box(Symbol.title_left)}{Fx.b if CONFIG.io_mode else ""}' f'{THEME.hi_fg("i")}{THEME.title("o")}{Fx.ub}{THEME.mem_box(Symbol.title_right)}') if Collector.collect_interrupt: return Draw.buffer("mem_misc", out_misc, only_save=True) try: #* Mem cx = 1; cy = 1 out += f'{Mv.to(y, x+1)}{THEME.title}{Fx.b}Total:{mem.string["total"]:>{cls.mem_width - 9}}{Fx.ub}{THEME.main_fg}' if cls.graph_height > 0: gli = f'{Mv.l(2)}{THEME.mem_box(Symbol.title_right)}{THEME.div_line}{Symbol.h_line * (cls.mem_width - 1)}{"" if CONFIG.show_disks else THEME.mem_box}{Symbol.title_left}{Mv.l(cls.mem_width - 1)}{THEME.title}' if cls.graph_height >= 2: gbg = f'{Mv.l(1)}' gmv = f'{Mv.l(cls.mem_width - 2)}{Mv.u(cls.graph_height - 1)}' big_mem: bool = cls.mem_width > 21 for name in cls.mem_names: if cy > h - 1: break if Collector.collect_interrupt: return if cls.mem_size > 2: out += (f'{Mv.to(y+cy, x+cx)}{gli}{name.capitalize()[:None if big_mem else 5]+":":<{1 if big_mem else 6.6}}{Mv.to(y+cy, x+cx + cls.mem_width - 3 - (len(mem.string[name])))}{Fx.trans(mem.string[name])}' f'{Mv.to(y+cy+1, x+cx)}{gbg}{Meters.mem[name](None if cls.resized else mem.percent[name])}{gmv}{str(mem.percent[name])+"%":>4}') cy += 2 if not cls.graph_height else cls.graph_height + 1 else: out += f'{Mv.to(y+cy, x+cx)}{name.capitalize():{5.5 if cls.mem_size > 1 else 1.1}} {gbg}{Meters.mem[name](None if cls.resized else mem.percent[name])}{mem.string[name][:None if cls.mem_size > 1 else -2]:>{9 if cls.mem_size > 1 else 7}}' cy += 1 if not cls.graph_height else cls.graph_height #* Swap if cls.swap_on and CONFIG.show_swap and not CONFIG.swap_disk and mem.swap_string: if h - cy > 5: if cls.graph_height > 0: out += f'{Mv.to(y+cy, x+cx)}{gli}' cy += 1 out += f'{Mv.to(y+cy, x+cx)}{THEME.title}{Fx.b}Swap:{mem.swap_string["total"]:>{cls.mem_width - 8}}{Fx.ub}{THEME.main_fg}' cy += 1 for name in cls.swap_names: if cy > h - 1: break if Collector.collect_interrupt: return if cls.mem_size > 2: out += (f'{Mv.to(y+cy, x+cx)}{gli}{name.capitalize()[:None if big_mem else 5]+":":<{1 if big_mem else 6.6}}{Mv.to(y+cy, x+cx + cls.mem_width - 3 - (len(mem.swap_string[name])))}{Fx.trans(mem.swap_string[name])}' f'{Mv.to(y+cy+1, x+cx)}{gbg}{Meters.swap[name](None if cls.resized else mem.swap_percent[name])}{gmv}{str(mem.swap_percent[name])+"%":>4}') cy += 2 if not cls.graph_height else cls.graph_height + 1 else: out += f'{Mv.to(y+cy, x+cx)}{name.capitalize():{5.5 if cls.mem_size > 1 else 1.1}} {gbg}{Meters.swap[name](None if cls.resized else mem.swap_percent[name])}{mem.swap_string[name][:None if cls.mem_size > 1 else -2]:>{9 if cls.mem_size > 1 else 7}}'; cy += 1 if not cls.graph_height else cls.graph_height if cls.graph_height > 0 and not cy == h: out += f'{Mv.to(y+cy, x+cx)}{gli}' #* Disks if CONFIG.show_disks and mem.disks: cx = x + cls.mem_width - 1; cy = 0 big_disk: bool = cls.disks_width >= 25 gli = f'{Mv.l(2)}{THEME.div_line}{Symbol.title_right}{Symbol.h_line * cls.disks_width}{THEME.mem_box}{Symbol.title_left}{Mv.l(cls.disks_width - 1)}' if CONFIG.io_mode: for name in cls.disks_io_order: item = mem.disks[name] io_item = mem.disks_io_dict.get(name, {}) if Collector.collect_interrupt: return if cy > h - 1: break out += Fx.trans(f'{Mv.to(y+cy, x+cx)}{gli}{THEME.title}{Fx.b}{item["name"]:{cls.disks_width - 2}.12}{Mv.to(y+cy, x + cx + cls.disks_width - 11)}{item["total"][:None if big_disk else -2]:>9}') if big_disk: out += Fx.trans(f'{Mv.to(y+cy, x + cx + (cls.disks_width // 2) - (len(str(item["used_percent"])) // 2) - 2)}{Fx.ub}{THEME.main_fg}{item["used_percent"]}%') cy += 1 if io_item: if cy > h - 1: break if CONFIG.io_graph_combined: if cls.disks_io_h <= 1: out += f'{Mv.to(y+cy, x+cx-1)}{" " * 5}' out += (f'{Mv.to(y+cy, x+cx-1)}{Fx.ub}{Graphs.disk_io[name]["rw"](None if cls.redraw else mem.disks_io_dict[name]["rw"][-1])}' f'{Mv.to(y+cy, x+cx-1)}{THEME.main_fg}{item["io"] or "RW"}') cy += cls.disks_io_h else: if cls.disks_io_h <= 3: out += f'{Mv.to(y+cy, x+cx-1)}{" " * 5}{Mv.to(y+cy+1, x+cx-1)}{" " * 5}' out += (f'{Mv.to(y+cy, x+cx-1)}{Fx.ub}{Graphs.disk_io[name]["read"](None if cls.redraw else mem.disks_io_dict[name]["read"][-1])}' f'{Mv.to(y+cy, x+cx-1)}{THEME.main_fg}{item["io_r"] or "R"}') cy += cls.disks_io_h // 2 out += f'{Mv.to(y+cy, x+cx-1)}{Graphs.disk_io[name]["write"](None if cls.redraw else mem.disks_io_dict[name]["write"][-1])}' cy += cls.disks_io_h // 2 out += f'{Mv.to(y+cy-1, x+cx-1)}{THEME.main_fg}{item["io_w"] or "W"}' else: for name, item in mem.disks.items(): if Collector.collect_interrupt: return if not name in Meters.disks_used: continue if cy > h - 1: break out += Fx.trans(f'{Mv.to(y+cy, x+cx)}{gli}{THEME.title}{Fx.b}{item["name"]:{cls.disks_width - 2}.12}{Mv.to(y+cy, x + cx + cls.disks_width - 11)}{item["total"][:None if big_disk else -2]:>9}') if big_disk: out += f'{Mv.to(y+cy, x + cx + (cls.disks_width // 2) - (len(item["io"]) // 2) - 2)}{Fx.ub}{THEME.main_fg}{Fx.trans(item["io"])}' cy += 1 if cy > h - 1: break if CONFIG.show_io_stat and name in Graphs.disk_io: out += f'{Mv.to(y+cy, x+cx-1)}{THEME.main_fg}{Fx.ub}{" IO: " if big_disk else " IO " + Mv.l(2)}{Fx.ub}{Graphs.disk_io[name]["rw"](None if cls.redraw else mem.disks_io_dict[name]["rw"][-1])}' if not big_disk and item["io"]: out += f'{Mv.to(y+cy, x+cx-1)}{Fx.ub}{THEME.main_fg}{item["io"]}' cy += 1 if cy > h - 1: break out += Mv.to(y+cy, x+cx) + (f'Used:{str(item["used_percent"]) + "%":>4} ' if big_disk else "U ") out += f'{Meters.disks_used[name](None if cls.resized else mem.disks[name]["used_percent"])}{item["used"][:None if big_disk else -2]:>{9 if big_disk else 7}}' cy += 1 if len(mem.disks) * 3 + (len(mem.disks_io_dict) if CONFIG.show_io_stat else 0) <= h + 1: if cy > h - 1: break out += Mv.to(y+cy, x+cx) out += f'Free:{str(item["free_percent"]) + "%":>4} ' if big_disk else f'{"F "}' out += f'{Meters.disks_free[name](None if cls.resized else mem.disks[name]["free_percent"])}{item["free"][:None if big_disk else -2]:>{9 if big_disk else 7}}' cy += 1 if len(mem.disks) * 4 + (len(mem.disks_io_dict) if CONFIG.show_io_stat else 0) <= h + 1: cy += 1 except (KeyError, TypeError): return Draw.buffer(cls.buffer, f'{out_misc}{out}{Term.fg}', only_save=Menu.active) cls.resized = cls.redraw = False class NetBox(Box, SubBox): name = "net" num = 3 height_p = 30 width_p = 45 min_w: int = 36 min_h: int = 6 x = 1 y = 1 resized: bool = True redraw: bool = True graph_height: Dict[str, int] = {} symbols: Dict[str, str] = {"download" : "▼", "upload" : "▲"} buffer: str = "net" Box.buffers.append(buffer) @classmethod def _calc_size(cls): if not "net" in cls.boxes: cls.width = Term.width return if not "proc" in cls.boxes: width_p = 100 else: width_p = cls.width_p cls.width = round(Term.width * width_p / 100) cls.height = Term.height - Box._b_cpu_h - Box._b_mem_h cls.y = Term.height - cls.height + 1 cls.box_width = 27 if cls.width > 45 else 19 cls.box_height = 9 if cls.height > 10 else cls.height - 2 cls.box_x = cls.width - cls.box_width - 1 cls.box_y = cls.y + ((cls.height - 2) // 2) - cls.box_height // 2 + 1 cls.graph_height["download"] = round((cls.height - 2) / 2) cls.graph_height["upload"] = cls.height - 2 - cls.graph_height["download"] cls.redraw = True @classmethod def _draw_bg(cls) -> str: if not "net" in cls.boxes: return "" return f'{create_box(box=cls, line_color=THEME.net_box)}\ {create_box(x=cls.box_x, y=cls.box_y, width=cls.box_width, height=cls.box_height, line_color=THEME.div_line, fill=False, title="Download", title2="Upload")}' @classmethod def _draw_fg(cls): if not "net" in cls.boxes: return net = NetCollector if net.redraw: cls.redraw = True if not net.nic: return out: str = "" out_misc: str = "" x, y, w, h = cls.x + 1, cls.y + 1, cls.width - 2, cls.height - 2 bx, by, bw, bh = cls.box_x + 1, cls.box_y + 1, cls.box_width - 2, cls.box_height - 2 reset: bool = bool(net.stats[net.nic]["download"]["offset"]) if cls.resized or cls.redraw: out_misc += cls._draw_bg() Key.mouse["b"] = [[x+w - len(net.nic[:10]) - 9 + i, y-1] for i in range(4)] Key.mouse["n"] = [[x+w - 5 + i, y-1] for i in range(4)] Key.mouse["z"] = [[x+w - len(net.nic[:10]) - 14 + i, y-1] for i in range(4)] out_misc += (f'{Mv.to(y-1, x+w - 25)}{THEME.net_box}{Symbol.h_line * (10 - len(net.nic[:10]))}{Symbol.title_left}{Fx.b if reset else ""}{THEME.hi_fg("z")}{THEME.title("ero")}' f'{Fx.ub}{THEME.net_box(Symbol.title_right)}{Term.fg}' f'{THEME.net_box}{Symbol.title_left}{Fx.b}{THEME.hi_fg("")}{Fx.ub}{THEME.net_box(Symbol.title_right)}{Term.fg}') if w - len(net.nic[:10]) - 20 > 6: Key.mouse["a"] = [[x+w - 20 - len(net.nic[:10]) + i, y-1] for i in range(4)] out_misc += (f'{Mv.to(y-1, x+w - 21 - len(net.nic[:10]))}{THEME.net_box(Symbol.title_left)}{Fx.b if net.auto_min else ""}{THEME.hi_fg("a")}{THEME.title("uto")}' f'{Fx.ub}{THEME.net_box(Symbol.title_right)}{Term.fg}') if w - len(net.nic[:10]) - 20 > 13: Key.mouse["y"] = [[x+w - 26 - len(net.nic[:10]) + i, y-1] for i in range(4)] out_misc += (f'{Mv.to(y-1, x+w - 27 - len(net.nic[:10]))}{THEME.net_box(Symbol.title_left)}{Fx.b if CONFIG.net_sync else ""}{THEME.title("s")}{THEME.hi_fg("y")}{THEME.title("nc")}' f'{Fx.ub}{THEME.net_box(Symbol.title_right)}{Term.fg}') if net.address and w - len(net.nic[:10]) - len(net.address) - 20 > 15: out_misc += (f'{Mv.to(y-1, x+7)}{THEME.net_box(Symbol.title_left)}{Fx.b}{THEME.title(net.address)}{Fx.ub}{THEME.net_box(Symbol.title_right)}{Term.fg}') Draw.buffer("net_misc", out_misc, only_save=True) cy = 0 for direction in ["download", "upload"]: strings = net.strings[net.nic][direction] stats = net.stats[net.nic][direction] if cls.redraw: stats["redraw"] = True if stats["redraw"] or cls.resized: Graphs.net[direction] = Graph(w - bw - 3, cls.graph_height[direction], THEME.gradient[direction], stats["speed"], max_value=net.sync_top if CONFIG.net_sync else stats["graph_top"], invert=direction != "download", color_max_value=net.net_min.get(direction) if CONFIG.net_color_fixed else None, round_up_low=True) out += f'{Mv.to(y if direction == "download" else y + cls.graph_height["download"], x)}{Graphs.net[direction](None if stats["redraw"] else stats["speed"][-1])}' out += (f'{Mv.to(by+cy, bx)}{THEME.main_fg}{cls.symbols[direction]} {strings["byte_ps"]:<10.10}' + ("" if bw < 20 else f'{Mv.to(by+cy, bx+bw - 12)}{"(" + strings["bit_ps"] + ")":>12.12}')) cy += 1 if bh != 3 else 2 if bh >= 6: out += f'{Mv.to(by+cy, bx)}{cls.symbols[direction]} {"Top:"}{Mv.to(by+cy, bx+bw - 12)}{"(" + strings["top"] + ")":>12.12}' cy += 1 if bh >= 4: out += f'{Mv.to(by+cy, bx)}{cls.symbols[direction]} {"Total:"}{Mv.to(by+cy, bx+bw - 10)}{strings["total"]:>10.10}' if bh > 2 and bh % 2: cy += 2 else: cy += 1 stats["redraw"] = False out += (f'{Mv.to(y, x)}{THEME.graph_text(net.sync_string if CONFIG.net_sync else net.strings[net.nic]["download"]["graph_top"])}' f'{Mv.to(y+h-1, x)}{THEME.graph_text(net.sync_string if CONFIG.net_sync else net.strings[net.nic]["upload"]["graph_top"])}') Draw.buffer(cls.buffer, f'{out_misc}{out}{Term.fg}', only_save=Menu.active) cls.redraw = cls.resized = False class ProcBox(Box): name = "proc" num = 4 height_p = 68 width_p = 55 min_w: int = 44 min_h: int = 16 x = 1 y = 1 current_y: int = 0 current_h: int = 0 select_max: int = 0 selected: int = 0 selected_pid: int = 0 last_selection: int = 0 filtering: bool = False moved: bool = False start: int = 1 count: int = 0 s_len: int = 0 detailed: bool = False detailed_x: int = 0 detailed_y: int = 0 detailed_width: int = 0 detailed_height: int = 8 resized: bool = True redraw: bool = True buffer: str = "proc" pid_counter: Dict[int, int] = {} Box.buffers.append(buffer) @classmethod def _calc_size(cls): if not "proc" in cls.boxes: cls.width = Term.width return width_p: int; height_p: int if not "net" in cls.boxes and not "mem" in cls.boxes: width_p = 100 else: width_p = cls.width_p if not "cpu" in cls.boxes: height_p = 100 else: height_p = cls.height_p cls.width = round(Term.width * width_p / 100) cls.height = round(Term.height * height_p / 100) if cls.height + Box._b_cpu_h > Term.height: cls.height = Term.height - Box._b_cpu_h cls.x = Term.width - cls.width + 1 cls.y = Box._b_cpu_h + 1 cls.current_y = cls.y cls.current_h = cls.height cls.select_max = cls.height - 3 cls.redraw = True cls.resized = True @classmethod def _draw_bg(cls) -> str: if not "proc" in cls.boxes: return "" return create_box(box=cls, line_color=THEME.proc_box) @classmethod def selector(cls, key: str, mouse_pos: Tuple[int, int] = (0, 0)): old: Tuple[int, int] = (cls.start, cls.selected) new_sel: int if key in ["up", "k"]: if cls.selected == 1 and cls.start > 1: cls.start -= 1 elif cls.selected == 1: cls.selected = 0 elif cls.selected > 1: cls.selected -= 1 elif key in ["down", "j"]: if cls.selected == 0 and ProcCollector.detailed and cls.last_selection: cls.selected = cls.last_selection cls.last_selection = 0 if cls.selected == cls.select_max and cls.start < ProcCollector.num_procs - cls.select_max + 1: cls.start += 1 elif cls.selected < cls.select_max: cls.selected += 1 elif key == "mouse_scroll_up" and cls.start > 1: cls.start -= 5 elif key == "mouse_scroll_down" and cls.start < ProcCollector.num_procs - cls.select_max + 1: cls.start += 5 elif key == "page_up" and cls.start > 1: cls.start -= cls.select_max elif key == "page_down" and cls.start < ProcCollector.num_procs - cls.select_max + 1: cls.start += cls.select_max elif key == "home": if cls.start > 1: cls.start = 1 elif cls.selected > 0: cls.selected = 0 elif key == "end": if cls.start < ProcCollector.num_procs - cls.select_max + 1: cls.start = ProcCollector.num_procs - cls.select_max + 1 elif cls.selected < cls.select_max: cls.selected = cls.select_max elif key == "mouse_click": if mouse_pos[0] > cls.x + cls.width - 4 and cls.current_y + 1 < mouse_pos[1] < cls.current_y + 1 + cls.select_max + 1: if mouse_pos[1] == cls.current_y + 2: cls.start = 1 elif mouse_pos[1] == cls.current_y + 1 + cls.select_max: cls.start = ProcCollector.num_procs - cls.select_max + 1 else: cls.start = round((mouse_pos[1] - cls.current_y) * ((ProcCollector.num_procs - cls.select_max - 2) / (cls.select_max - 2))) else: new_sel = mouse_pos[1] - cls.current_y - 1 if mouse_pos[1] >= cls.current_y - 1 else 0 if new_sel > 0 and new_sel == cls.selected: Key.list.insert(0, "enter") return elif new_sel > 0 and new_sel != cls.selected: if cls.last_selection: cls.last_selection = 0 cls.selected = new_sel elif key == "mouse_unselect": cls.selected = 0 if cls.start > ProcCollector.num_procs - cls.select_max + 1 and ProcCollector.num_procs > cls.select_max: cls.start = ProcCollector.num_procs - cls.select_max + 1 elif cls.start > ProcCollector.num_procs: cls.start = ProcCollector.num_procs if cls.start < 1: cls.start = 1 if cls.selected > ProcCollector.num_procs and ProcCollector.num_procs < cls.select_max: cls.selected = ProcCollector.num_procs elif cls.selected > cls.select_max: cls.selected = cls.select_max if cls.selected < 0: cls.selected = 0 if old != (cls.start, cls.selected): cls.moved = True Collector.collect(ProcCollector, proc_interrupt=True, redraw=True, only_draw=True) @classmethod def _draw_fg(cls): if not "proc" in cls.boxes: return proc = ProcCollector if proc.proc_interrupt: return if proc.redraw: cls.redraw = True out: str = "" out_misc: str = "" n: int = 0 x, y, w, h = cls.x + 1, cls.current_y + 1, cls.width - 2, cls.current_h - 2 prog_len: int; arg_len: int; val: int; c_color: str; m_color: str; t_color: str; sort_pos: int; tree_len: int; is_selected: bool; calc: int dgx: int; dgw: int; dx: int; dw: int; dy: int l_count: int = 0 scroll_pos: int = 0 killed: bool = True indent: str = "" offset: int = 0 tr_show: bool = True usr_show: bool = True vals: List[str] g_color: str = "" s_len: int = 0 if proc.search_filter: s_len = len(proc.search_filter[:10]) loc_string: str = f'{cls.start + cls.selected - 1}/{proc.num_procs}' end: str = "" if proc.detailed: dgx, dgw = x, w // 3 dw = w - dgw - 1 if dw > 120: dw = 120 dgw = w - 121 dx = x + dgw + 2 dy = cls.y + 1 if w > 67: arg_len = w - 53 - (1 if proc.num_procs > cls.select_max else 0) prog_len = 15 else: arg_len = 0 prog_len = w - 38 - (1 if proc.num_procs > cls.select_max else 0) if prog_len < 15: tr_show = False prog_len += 5 if prog_len < 12: usr_show = False prog_len += 9 if CONFIG.proc_tree: tree_len = arg_len + prog_len + 6 arg_len = 0 #* Buttons and titles only redrawn if needed if cls.resized or cls.redraw: s_len += len(CONFIG.proc_sorting) if cls.resized or s_len != cls.s_len or proc.detailed: cls.s_len = s_len for k in ["e", "r", "c", "T", "K", "I", "enter", "left", " ", "f", "delete"]: if k in Key.mouse: del Key.mouse[k] if proc.detailed: killed = proc.details.get("killed", False) main = THEME.main_fg if cls.selected == 0 and not killed else THEME.inactive_fg hi = THEME.hi_fg if cls.selected == 0 and not killed else THEME.inactive_fg title = THEME.title if cls.selected == 0 and not killed else THEME.inactive_fg if cls.current_y != cls.y + 8 or cls.resized or Graphs.detailed_cpu is NotImplemented: cls.current_y = cls.y + 8 cls.current_h = cls.height - 8 for i in range(7): out_misc += f'{Mv.to(dy+i, x)}{" " * w}' out_misc += (f'{Mv.to(dy+7, x-1)}{THEME.proc_box}{Symbol.title_right}{Symbol.h_line*w}{Symbol.title_left}' f'{Mv.to(dy+7, x+1)}{THEME.proc_box(Symbol.title_left)}{Fx.b}{THEME.hi_fg(SUPERSCRIPT[cls.num])}{THEME.title(cls.name)}{Fx.ub}{THEME.proc_box(Symbol.title_right)}{THEME.div_line}') for i in range(7): out_misc += f'{Mv.to(dy + i, dgx + dgw + 1)}{Symbol.v_line}' out_misc += (f'{Mv.to(dy-1, x-1)}{THEME.proc_box}{Symbol.left_up}{Symbol.h_line*w}{Symbol.right_up}' f'{Mv.to(dy-1, dgx + dgw + 1)}{Symbol.div_up}' f'{Mv.to(dy-1, x+1)}{THEME.proc_box(Symbol.title_left)}{Fx.b}{THEME.title(str(proc.details["pid"]))}{Fx.ub}{THEME.proc_box(Symbol.title_right)}' f'{THEME.proc_box(Symbol.title_left)}{Fx.b}{THEME.title(proc.details["name"][:(dgw - 11)])}{Fx.ub}{THEME.proc_box(Symbol.title_right)}') if cls.selected == 0: Key.mouse["enter"] = [[dx+dw-10 + i, dy-1] for i in range(7)] if cls.selected == 0 and not killed: Key.mouse["T"] = [[dx+2 + i, dy-1] for i in range(9)] out_misc += (f'{Mv.to(dy-1, dx+dw - 11)}{THEME.proc_box(Symbol.title_left)}{Fx.b}{title if cls.selected > 0 else THEME.title}close{Fx.ub} {main if cls.selected > 0 else THEME.main_fg}{Symbol.enter}{THEME.proc_box(Symbol.title_right)}' f'{Mv.to(dy-1, dx+1)}{THEME.proc_box(Symbol.title_left)}{Fx.b}{hi}T{title}erminate{Fx.ub}{THEME.proc_box(Symbol.title_right)}') if dw > 28: if cls.selected == 0 and not killed and not "K" in Key.mouse: Key.mouse["K"] = [[dx + 13 + i, dy-1] for i in range(4)] out_misc += f'{THEME.proc_box(Symbol.title_left)}{Fx.b}{hi}K{title}ill{Fx.ub}{THEME.proc_box(Symbol.title_right)}' if dw > 39: if cls.selected == 0 and not killed and not "I" in Key.mouse: Key.mouse["I"] = [[dx + 19 + i, dy-1] for i in range(9)] out_misc += f'{THEME.proc_box(Symbol.title_left)}{Fx.b}{hi}I{title}nterrupt{Fx.ub}{THEME.proc_box(Symbol.title_right)}' if Graphs.detailed_cpu is NotImplemented or cls.resized: Graphs.detailed_cpu = Graph(dgw+1, 7, THEME.gradient["cpu"], proc.details_cpu) Graphs.detailed_mem = Graph(dw // 3, 1, None, proc.details_mem) cls.select_max = cls.height - 11 y = cls.y + 9 h = cls.height - 10 else: if cls.current_y != cls.y or cls.resized: cls.current_y = cls.y cls.current_h = cls.height y, h = cls.y + 1, cls.height - 2 out_misc += (f'{Mv.to(y-1, x-1)}{THEME.proc_box}{Symbol.left_up}{Symbol.h_line*w}{Symbol.right_up}' f'{Mv.to(y-1, x+1)}{THEME.proc_box(Symbol.title_left)}{Fx.b}{THEME.hi_fg(SUPERSCRIPT[cls.num])}{THEME.title(cls.name)}{Fx.ub}{THEME.proc_box(Symbol.title_right)}' f'{Mv.to(y+7, x-1)}{THEME.proc_box(Symbol.v_line)}{Mv.r(w)}{THEME.proc_box(Symbol.v_line)}') cls.select_max = cls.height - 3 sort_pos = x + w - len(CONFIG.proc_sorting) - 7 if not "left" in Key.mouse: Key.mouse["left"] = [[sort_pos + i, y-1] for i in range(3)] Key.mouse["right"] = [[sort_pos + len(CONFIG.proc_sorting) + 3 + i, y-1] for i in range(3)] out_misc += (f'{Mv.to(y-1, x + 8)}{THEME.proc_box(Symbol.h_line * (w - 9))}' + ("" if not proc.detailed else f"{Mv.to(dy+7, dgx + dgw + 1)}{THEME.proc_box(Symbol.div_down)}") + f'{Mv.to(y-1, sort_pos)}{THEME.proc_box(Symbol.title_left)}{Fx.b}{THEME.hi_fg("<")} {THEME.title(CONFIG.proc_sorting)} ' f'{THEME.hi_fg(">")}{Fx.ub}{THEME.proc_box(Symbol.title_right)}') if w > 29 + s_len: if not "e" in Key.mouse: Key.mouse["e"] = [[sort_pos - 5 + i, y-1] for i in range(4)] out_misc += (f'{Mv.to(y-1, sort_pos - 6)}{THEME.proc_box(Symbol.title_left)}{Fx.b if CONFIG.proc_tree else ""}' f'{THEME.title("tre")}{THEME.hi_fg("e")}{Fx.ub}{THEME.proc_box(Symbol.title_right)}') if w > 37 + s_len: if not "r" in Key.mouse: Key.mouse["r"] = [[sort_pos - 14 + i, y-1] for i in range(7)] out_misc += (f'{Mv.to(y-1, sort_pos - 15)}{THEME.proc_box(Symbol.title_left)}{Fx.b if CONFIG.proc_reversed else ""}' f'{THEME.hi_fg("r")}{THEME.title("everse")}{Fx.ub}{THEME.proc_box(Symbol.title_right)}') if w > 47 + s_len: if not "c" in Key.mouse: Key.mouse["c"] = [[sort_pos - 24 + i, y-1] for i in range(8)] out_misc += (f'{Mv.to(y-1, sort_pos - 25)}{THEME.proc_box(Symbol.title_left)}{Fx.b if CONFIG.proc_per_core else ""}' f'{THEME.title("per-")}{THEME.hi_fg("c")}{THEME.title("ore")}{Fx.ub}{THEME.proc_box(Symbol.title_right)}') if not "f" in Key.mouse or cls.resized: Key.mouse["f"] = [[x+6 + i, y-1] for i in range(6 if not proc.search_filter else 2 + len(proc.search_filter[-10:]))] if proc.search_filter: if not "delete" in Key.mouse: Key.mouse["delete"] = [[x+12 + len(proc.search_filter[-10:]) + i, y-1] for i in range(3)] elif "delete" in Key.mouse: del Key.mouse["delete"] out_misc += (f'{Mv.to(y-1, x + 8)}{THEME.proc_box(Symbol.title_left)}{Fx.b if cls.filtering or proc.search_filter else ""}{THEME.hi_fg("F" if cls.filtering and proc.case_sensitive else "f")}{THEME.title}' + ("ilter" if not proc.search_filter and not cls.filtering else f' {proc.search_filter[-(10 if w < 83 else w - 74):]}{(Fx.bl + "█" + Fx.ubl) if cls.filtering else THEME.hi_fg(" del")}') + f'{THEME.proc_box(Symbol.title_right)}') main = THEME.inactive_fg if cls.selected == 0 else THEME.main_fg hi = THEME.inactive_fg if cls.selected == 0 else THEME.hi_fg title = THEME.inactive_fg if cls.selected == 0 else THEME.title out_misc += (f'{Mv.to(y+h, x + 1)}{THEME.proc_box}{Symbol.h_line*(w-4)}' f'{Mv.to(y+h, x+1)}{THEME.proc_box(Symbol.title_left)}{main}{Symbol.up} {Fx.b}{THEME.main_fg("select")} {Fx.ub}' f'{THEME.inactive_fg if cls.selected == cls.select_max else THEME.main_fg}{Symbol.down}{THEME.proc_box(Symbol.title_right)}' f'{THEME.proc_box(Symbol.title_left)}{title}{Fx.b}info {Fx.ub}{main}{Symbol.enter}{THEME.proc_box(Symbol.title_right)}') if not "enter" in Key.mouse: Key.mouse["enter"] = [[x + 14 + i, y+h] for i in range(6)] if w - len(loc_string) > 34: if not "T" in Key.mouse: Key.mouse["T"] = [[x + 22 + i, y+h] for i in range(9)] out_misc += f'{THEME.proc_box(Symbol.title_left)}{Fx.b}{hi}T{title}erminate{Fx.ub}{THEME.proc_box(Symbol.title_right)}' if w - len(loc_string) > 40: if not "K" in Key.mouse: Key.mouse["K"] = [[x + 33 + i, y+h] for i in range(4)] out_misc += f'{THEME.proc_box(Symbol.title_left)}{Fx.b}{hi}K{title}ill{Fx.ub}{THEME.proc_box(Symbol.title_right)}' if w - len(loc_string) > 51: if not "I" in Key.mouse: Key.mouse["I"] = [[x + 39 + i, y+h] for i in range(9)] out_misc += f'{THEME.proc_box(Symbol.title_left)}{Fx.b}{hi}I{title}nterrupt{Fx.ub}{THEME.proc_box(Symbol.title_right)}' if CONFIG.proc_tree and w - len(loc_string) > 65: if not " " in Key.mouse: Key.mouse[" "] = [[x + 50 + i, y+h] for i in range(12)] out_misc += f'{THEME.proc_box(Symbol.title_left)}{Fx.b}{hi}spc {title}collapse{Fx.ub}{THEME.proc_box(Symbol.title_right)}' #* Processes labels selected: str = CONFIG.proc_sorting label: str if selected == "memory": selected = "mem" if selected == "threads" and not CONFIG.proc_tree and not arg_len: selected = "tr" if CONFIG.proc_tree: label = (f'{THEME.title}{Fx.b}{Mv.to(y, x)}{" Tree:":<{tree_len-2}}' + (f'{"Threads: ":<9}' if tr_show else " "*4) + (f'{"User:":<9}' if usr_show else "") + f'Mem%{"Cpu%":>11}{Fx.ub}{THEME.main_fg} ' + (" " if proc.num_procs > cls.select_max else "")) if selected in ["pid", "program", "arguments"]: selected = "tree" else: label = (f'{THEME.title}{Fx.b}{Mv.to(y, x)}{"Pid:":>7} {"Program:" if prog_len > 8 else "Prg:":<{prog_len}}' + (f'{"Arguments:":<{arg_len-4}}' if arg_len else "") + ((f'{"Threads:":<9}' if arg_len else f'{"Tr:":^5}') if tr_show else "") + (f'{"User:":<9}' if usr_show else "") + f'Mem%{"Cpu%":>11}{Fx.ub}{THEME.main_fg} ' + (" " if proc.num_procs > cls.select_max else "")) if selected == "program" and prog_len <= 8: selected = "prg" selected = selected.split(" ")[0].capitalize() if CONFIG.proc_mem_bytes: label = label.replace("Mem%", "MemB") label = label.replace(selected, f'{Fx.u}{selected}{Fx.uu}') out_misc += label Draw.buffer("proc_misc", out_misc, only_save=True) #* Detailed box draw if proc.detailed: if proc.details["status"] == psutil.STATUS_RUNNING: stat_color = Fx.b elif proc.details["status"] in [psutil.STATUS_DEAD, psutil.STATUS_STOPPED, psutil.STATUS_ZOMBIE]: stat_color = f'{THEME.inactive_fg}' else: stat_color = "" expand = proc.expand iw = (dw - 3) // (4 + expand) iw2 = iw - 1 out += (f'{Mv.to(dy, dgx)}{Graphs.detailed_cpu(None if cls.moved or proc.details["killed"] else proc.details_cpu[-1])}' f'{Mv.to(dy, dgx)}{THEME.title}{Fx.b}{0 if proc.details["killed"] else proc.details["cpu_percent"]}%{Mv.r(1)}{"" if SYSTEM == "MacOS" else (("C" if dgw < 20 else "Core") + str(proc.details["cpu_num"]))}') for i, l in enumerate(["C", "P", "U"]): out += f'{Mv.to(dy+2+i, dgx)}{l}' for i, l in enumerate(["C", "M", "D"]): out += f'{Mv.to(dy+4+i, dx+1)}{l}' out += (f'{Mv.to(dy, dx+1)} {"Status:":^{iw}.{iw2}}{"Elapsed:":^{iw}.{iw2}}' + (f'{"Parent:":^{iw}.{iw2}}' if dw > 28 else "") + (f'{"User:":^{iw}.{iw2}}' if dw > 38 else "") + (f'{"Threads:":^{iw}.{iw2}}' if expand > 0 else "") + (f'{"Nice:":^{iw}.{iw2}}' if expand > 1 else "") + (f'{"IO Read:":^{iw}.{iw2}}' if expand > 2 else "") + (f'{"IO Write:":^{iw}.{iw2}}' if expand > 3 else "") + (f'{"TTY:":^{iw}.{iw2}}' if expand > 4 else "") + f'{Mv.to(dy+1, dx+1)}{Fx.ub}{THEME.main_fg}{stat_color}{proc.details["status"]:^{iw}.{iw2}}{Fx.ub}{THEME.main_fg}{proc.details["uptime"]:^{iw}.{iw2}} ' + (f'{proc.details["parent_name"]:^{iw}.{iw2}}' if dw > 28 else "") + (f'{proc.details["username"]:^{iw}.{iw2}}' if dw > 38 else "") + (f'{proc.details["threads"]:^{iw}.{iw2}}' if expand > 0 else "") + (f'{proc.details["nice"]:^{iw}.{iw2}}' if expand > 1 else "") + (f'{proc.details["io_read"]:^{iw}.{iw2}}' if expand > 2 else "") + (f'{proc.details["io_write"]:^{iw}.{iw2}}' if expand > 3 else "") + (f'{proc.details["terminal"][-(iw2):]:^{iw}.{iw2}}' if expand > 4 else "") + f'{Mv.to(dy+3, dx)}{THEME.title}{Fx.b}{("Memory: " if dw > 42 else "M:") + str(round(proc.details["memory_percent"], 1)) + "%":>{dw//3-1}}{Fx.ub} {THEME.inactive_fg}{"⡀"*(dw//3)}' f'{Mv.l(dw//3)}{THEME.proc_misc}{Graphs.detailed_mem(None if cls.moved else proc.details_mem[-1])} ' f'{THEME.title}{Fx.b}{proc.details["memory_bytes"]:.{dw//3 - 2}}{THEME.main_fg}{Fx.ub}') cy = dy + (4 if len(proc.details["cmdline"]) > dw - 5 else 5) for i in range(ceil(len(proc.details["cmdline"]) / (dw - 5))): out += f'{Mv.to(cy+i, dx + 3)}{proc.details["cmdline"][((dw-5)*i):][:(dw-5)]:{"^" if i == 0 else "<"}{dw-5}}' if i == 2: break #* Checking for selection out of bounds if cls.start > proc.num_procs - cls.select_max + 1 and proc.num_procs > cls.select_max: cls.start = proc.num_procs - cls.select_max + 1 elif cls.start > proc.num_procs: cls.start = proc.num_procs if cls.start < 1: cls.start = 1 if cls.selected > proc.num_procs and proc.num_procs < cls.select_max: cls.selected = proc.num_procs elif cls.selected > cls.select_max: cls.selected = cls.select_max if cls.selected < 0: cls.selected = 0 #* Start iteration over all processes and info cy = 1 for n, (pid, items) in enumerate(proc.processes.items(), start=1): if n < cls.start: continue l_count += 1 if l_count == cls.selected: is_selected = True cls.selected_pid = pid else: is_selected = False indent, name, cmd, threads, username, mem, mem_b, cpu = [items.get(v, d) for v, d in [("indent", ""), ("name", ""), ("cmd", ""), ("threads", 0), ("username", "?"), ("mem", 0.0), ("mem_b", 0), ("cpu", 0.0)]] if CONFIG.proc_tree: arg_len = 0 offset = tree_len - len(f'{indent}{pid}') if offset < 1: offset = 0 indent = f'{indent:.{tree_len - len(str(pid))}}' if offset - len(name) > 12: cmd = cmd.split(" ")[0].split("/")[-1] if not cmd.startswith(name): offset = len(name) arg_len = tree_len - len(f'{indent}{pid} {name} ') + 2 cmd = f'({cmd[:(arg_len-4)]})' else: offset = prog_len - 1 if cpu > 1.0 or pid in Graphs.pid_cpu: if pid not in Graphs.pid_cpu: Graphs.pid_cpu[pid] = Graph(5, 1, None, [0]) cls.pid_counter[pid] = 0 elif cpu < 1.0: cls.pid_counter[pid] += 1 if cls.pid_counter[pid] > 10: del cls.pid_counter[pid], Graphs.pid_cpu[pid] else: cls.pid_counter[pid] = 0 end = f'{THEME.main_fg}{Fx.ub}' if CONFIG.proc_colors else Fx.ub if cls.selected > cy: calc = cls.selected - cy elif 0 < cls.selected <= cy: calc = cy - cls.selected else: calc = cy if CONFIG.proc_colors and not is_selected: vals = [] for v in [int(cpu), int(mem), int(threads // 3)]: if CONFIG.proc_gradient: val = ((v if v <= 100 else 100) + 100) - calc * 100 // cls.select_max vals += [f'{THEME.gradient["proc_color" if val < 100 else "process"][val if val < 100 else val - 100]}'] else: vals += [f'{THEME.gradient["process"][v if v <= 100 else 100]}'] c_color, m_color, t_color = vals else: c_color = m_color = t_color = Fx.b if CONFIG.proc_gradient and not is_selected: g_color = f'{THEME.gradient["proc"][calc * 100 // cls.select_max]}' if is_selected: c_color = m_color = t_color = g_color = end = "" out += f'{THEME.selected_bg}{THEME.selected_fg}{Fx.b}' #* Creates one line for a process with all gathered information out += (f'{Mv.to(y+cy, x)}{g_color}{indent}{pid:>{(1 if CONFIG.proc_tree else 7)}} ' + f'{c_color}{name:<{offset}.{offset}} {end}' + (f'{g_color}{cmd:<{arg_len}.{arg_len-1}}' if arg_len else "") + (t_color + (f'{threads:>4} ' if threads < 1000 else "999> ") + end if tr_show else "") + (g_color + (f'{username:<9.9}' if len(username) < 10 else f'{username[:8]:<8}+') if usr_show else "") + m_color + ((f'{mem:>4.1f}' if mem < 100 else f'{mem:>4.0f} ') if not CONFIG.proc_mem_bytes else f'{floating_humanizer(mem_b, short=True):>4.4}') + end + f' {THEME.inactive_fg}{"⡀"*5}{THEME.main_fg}{g_color}{c_color}' + (f' {cpu:>4.1f} ' if cpu < 100 else f'{cpu:>5.0f} ') + end + (" " if proc.num_procs > cls.select_max else "")) #* Draw small cpu graph for process if cpu usage was above 1% in the last 10 updates if pid in Graphs.pid_cpu: out += f'{Mv.to(y+cy, x + w - (12 if proc.num_procs > cls.select_max else 11))}{c_color if CONFIG.proc_colors else THEME.proc_misc}{Graphs.pid_cpu[pid](None if cls.moved else round(cpu))}{THEME.main_fg}' if is_selected: out += f'{Fx.ub}{Term.fg}{Term.bg}{Mv.to(y+cy, x + w - 1)}{" " if proc.num_procs > cls.select_max else ""}' cy += 1 if cy == h: break if cy < h: for i in range(h-cy): out += f'{Mv.to(y+cy+i, x)}{" " * w}' #* Draw scrollbar if needed if proc.num_procs > cls.select_max: if cls.resized: Key.mouse["mouse_scroll_up"] = [[x+w-2+i, y] for i in range(3)] Key.mouse["mouse_scroll_down"] = [[x+w-2+i, y+h-1] for i in range(3)] scroll_pos = round(cls.start * (cls.select_max - 2) / (proc.num_procs - (cls.select_max - 2))) if scroll_pos < 0 or cls.start == 1: scroll_pos = 0 elif scroll_pos > h - 3 or cls.start >= proc.num_procs - cls.select_max: scroll_pos = h - 3 out += (f'{Mv.to(y, x+w-1)}{Fx.b}{THEME.main_fg}↑{Mv.to(y+h-1, x+w-1)}↓{Fx.ub}' f'{Mv.to(y+1+scroll_pos, x+w-1)}█') elif "scroll_up" in Key.mouse: del Key.mouse["scroll_up"], Key.mouse["scroll_down"] #* Draw current selection and number of processes out += (f'{Mv.to(y+h, x + w - 3 - len(loc_string))}{THEME.proc_box}{Symbol.title_left}{THEME.title}' f'{Fx.b}{loc_string}{Fx.ub}{THEME.proc_box(Symbol.title_right)}') #* Clean up dead processes graphs and counters cls.count += 1 if cls.count == 100: cls.count = 0 for p in list(cls.pid_counter): if not psutil.pid_exists(p): del cls.pid_counter[p], Graphs.pid_cpu[p] Draw.buffer(cls.buffer, f'{out_misc}{out}{Term.fg}', only_save=Menu.active) cls.redraw = cls.resized = cls.moved = False class Collector: '''Data collector master class * .start(): Starts collector thread * .stop(): Stops collector thread * .collect(*collectors: Collector, draw_now: bool = True, interrupt: bool = False): queues up collectors to run''' stopping: bool = False started: bool = False draw_now: bool = False redraw: bool = False only_draw: bool = False thread: threading.Thread collect_run = threading.Event() collect_idle = threading.Event() collect_idle.set() collect_done = threading.Event() collect_queue: List = [] collect_interrupt: bool = False proc_interrupt: bool = False use_draw_list: bool = False proc_counter: int = 1 @classmethod def start(cls): cls.stopping = False cls.thread = threading.Thread(target=cls._runner, args=()) cls.thread.start() cls.started = True @classmethod def stop(cls): if cls.started and cls.thread.is_alive(): cls.stopping = True cls.started = False cls.collect_queue = [] cls.collect_idle.set() cls.collect_done.set() try: cls.thread.join() except: pass @classmethod def _runner(cls): '''This is meant to run in it's own thread, collecting and drawing when collect_run is set''' draw_buffers: List[str] = [] debugged: bool = False try: while not cls.stopping: if CONFIG.draw_clock and CONFIG.update_ms != 1000: Box.draw_clock() cls.collect_run.wait(0.1) if not cls.collect_run.is_set(): continue draw_buffers = [] cls.collect_interrupt = False cls.collect_run.clear() cls.collect_idle.clear() cls.collect_done.clear() if DEBUG and not debugged: TimeIt.start("Collect and draw") while cls.collect_queue: collector = cls.collect_queue.pop() if not cls.only_draw: collector._collect() collector._draw() if cls.use_draw_list: draw_buffers.append(collector.buffer) if cls.collect_interrupt: break if DEBUG and not debugged: TimeIt.stop("Collect and draw"); debugged = True if cls.draw_now and not Menu.active and not cls.collect_interrupt: if cls.use_draw_list: Draw.out(*draw_buffers) else: Draw.out() if CONFIG.draw_clock and CONFIG.update_ms == 1000: Box.draw_clock() cls.collect_idle.set() cls.collect_done.set() except Exception as e: errlog.exception(f'Data collection thread failed with exception: {e}') cls.collect_idle.set() cls.collect_done.set() clean_quit(1, thread=True) @classmethod def collect(cls, *collectors, draw_now: bool = True, interrupt: bool = False, proc_interrupt: bool = False, redraw: bool = False, only_draw: bool = False): '''Setup collect queue for _runner''' cls.collect_interrupt = interrupt cls.proc_interrupt = proc_interrupt cls.collect_idle.wait() cls.collect_interrupt = False cls.proc_interrupt = False cls.use_draw_list = False cls.draw_now = draw_now cls.redraw = redraw cls.only_draw = only_draw if collectors: cls.collect_queue = [*collectors] cls.use_draw_list = True if ProcCollector in cls.collect_queue: cls.proc_counter = 1 else: cls.collect_queue = list(cls.__subclasses__()) if CONFIG.proc_update_mult > 1: if cls.proc_counter > 1: cls.collect_queue.remove(ProcCollector) if cls.proc_counter == CONFIG.proc_update_mult: cls.proc_counter = 0 cls.proc_counter += 1 cls.collect_run.set() class CpuCollector(Collector): '''Collects cpu usage for cpu and cores, cpu frequency, load_avg, uptime and cpu temps''' cpu_usage: List[List[int]] = [] cpu_upper: List[int] = [] cpu_lower: List[int] = [] cpu_temp: List[List[int]] = [] cpu_temp_high: int = 0 cpu_temp_crit: int = 0 for _ in range(THREADS + 1): cpu_usage.append([]) cpu_temp.append([]) freq_error: bool = False cpu_freq: int = 0 load_avg: List[float] = [] uptime: str = "" buffer: str = CpuBox.buffer sensor_method: str = "" got_sensors: bool = False sensor_swap: bool = False cpu_temp_only: bool = False @classmethod def get_sensors(cls): '''Check if we can get cpu temps and return method of getting temps''' cls.sensor_method = "" if SYSTEM == "MacOS": try: if which("coretemp") and subprocess.check_output(["coretemp", "-p"], universal_newlines=True).strip().replace("-", "").isdigit(): cls.sensor_method = "coretemp" elif which("osx-cpu-temp") and subprocess.check_output("osx-cpu-temp", universal_newlines=True).rstrip().endswith("°C"): cls.sensor_method = "osx-cpu-temp" except: pass elif CONFIG.cpu_sensor != "Auto" and CONFIG.cpu_sensor in CONFIG.cpu_sensors: cls.sensor_method = "psutil" elif hasattr(psutil, "sensors_temperatures"): try: temps = psutil.sensors_temperatures() if temps: for name, entries in temps.items(): if name.lower().startswith("cpu"): cls.sensor_method = "psutil" break for entry in entries: if entry.label.startswith(("Package", "Core 0", "Tdie", "CPU")): cls.sensor_method = "psutil" break except: pass if not cls.sensor_method and SYSTEM == "Linux": try: if which("vcgencmd") and subprocess.check_output(["vcgencmd", "measure_temp"], universal_newlines=True).strip().endswith("'C"): cls.sensor_method = "vcgencmd" except: pass cls.got_sensors = bool(cls.sensor_method) @classmethod def _collect(cls): cls.cpu_usage[0].append(ceil(psutil.cpu_percent(percpu=False))) if len(cls.cpu_usage[0]) > Term.width * 4: del cls.cpu_usage[0][0] cpu_times_percent = psutil.cpu_times_percent() for x in ["upper", "lower"]: if getattr(CONFIG, "cpu_graph_" + x) == "total": setattr(cls, "cpu_" + x, cls.cpu_usage[0]) else: getattr(cls, "cpu_" + x).append(ceil(getattr(cpu_times_percent, getattr(CONFIG, "cpu_graph_" + x)))) if len(getattr(cls, "cpu_" + x)) > Term.width * 4: del getattr(cls, "cpu_" + x)[0] for n, thread in enumerate(psutil.cpu_percent(percpu=True), start=1): cls.cpu_usage[n].append(ceil(thread)) if len(cls.cpu_usage[n]) > Term.width * 2: del cls.cpu_usage[n][0] try: if CONFIG.show_cpu_freq and hasattr(psutil.cpu_freq(), "current"): freq: float = psutil.cpu_freq().current cls.cpu_freq = round(freq * (1 if freq > 10 else 1000)) elif cls.cpu_freq > 0: cls.cpu_freq = 0 except Exception as e: if not cls.freq_error: cls.freq_error = True errlog.error("Exception while getting cpu frequency!") errlog.exception(f'{e}') else: pass cls.load_avg = [round(lavg, 2) for lavg in psutil.getloadavg()] cls.uptime = str(timedelta(seconds=round(time()-psutil.boot_time(),0)))[:-3].replace(" days,", "d").replace(" day,", "d") if CONFIG.check_temp and cls.got_sensors: cls._collect_temps() @classmethod def _collect_temps(cls): temp: int = 1000 cores: List[int] = [] core_dict: Dict[int, int] = {} entry_int: int = 0 cpu_type: str = "" c_max: int = 0 s_name: str = "_-_" s_label: str = "_-_" if cls.sensor_method == "psutil": try: if CONFIG.cpu_sensor != "Auto": s_name, s_label = CONFIG.cpu_sensor.split(":", 1) for name, entries in psutil.sensors_temperatures().items(): for num, entry in enumerate(entries, 1): if name == s_name and (entry.label == s_label or str(num) == s_label): if entry.label.startswith("Package"): cpu_type = "intel" elif entry.label.startswith("Tdie"): cpu_type = "ryzen" else: cpu_type = "other" if getattr(entry, "high", None) != None and entry.high > 1: cls.cpu_temp_high = round(entry.high) else: cls.cpu_temp_high = 80 if getattr(entry, "critical", None) != None and entry.critical > 1: cls.cpu_temp_crit = round(entry.critical) else: cls.cpu_temp_crit = 95 temp = round(entry.current) elif entry.label.startswith(("Package", "Tdie")) and cpu_type in ["", "other"] and s_name == "_-_" and hasattr(entry, "current"): if not cls.cpu_temp_high or cls.sensor_swap or cpu_type == "other": cls.sensor_swap = False if getattr(entry, "high", None) != None and entry.high > 1: cls.cpu_temp_high = round(entry.high) else: cls.cpu_temp_high = 80 if getattr(entry, "critical", None) != None and entry.critical > 1: cls.cpu_temp_crit = round(entry.critical) else: cls.cpu_temp_crit = 95 cpu_type = "intel" if entry.label.startswith("Package") else "ryzen" temp = round(entry.current) elif (entry.label.startswith(("Core", "Tccd", "CPU")) or (name.lower().startswith("cpu") and not entry.label)) and hasattr(entry, "current"): if entry.label.startswith(("Core", "Tccd")): entry_int = int(entry.label.replace("Core", "").replace("Tccd", "")) if entry_int in core_dict and cpu_type != "ryzen": if c_max == 0: c_max = max(core_dict) + 1 if c_max < THREADS // 2 and (entry_int + c_max) not in core_dict: core_dict[(entry_int + c_max)] = round(entry.current) continue elif entry_int in core_dict: continue core_dict[entry_int] = round(entry.current) continue elif cpu_type in ["intel", "ryzen"]: continue if not cpu_type: cpu_type = "other" if not cls.cpu_temp_high or cls.sensor_swap: cls.sensor_swap = False if getattr(entry, "high", None) != None and entry.high > 1: cls.cpu_temp_high = round(entry.high) else: cls.cpu_temp_high = 60 if name == "cpu_thermal" else 80 if getattr(entry, "critical", None) != None and entry.critical > 1: cls.cpu_temp_crit = round(entry.critical) else: cls.cpu_temp_crit = 80 if name == "cpu_thermal" else 95 temp = round(entry.current) cores.append(round(entry.current)) if core_dict: if not temp or temp == 1000: temp = sum(core_dict.values()) // len(core_dict) if not cls.cpu_temp_high or not cls.cpu_temp_crit: cls.cpu_temp_high, cls.cpu_temp_crit = 80, 95 cls.cpu_temp[0].append(temp) if cpu_type == "ryzen": ccds: int = len(core_dict) cores_per_ccd: int = CORES // ccds z: int = 1 for x in range(THREADS): if x == CORES: z = 1 if CORE_MAP[x] + 1 > cores_per_ccd * z: z += 1 if z in core_dict: cls.cpu_temp[x+1].append(core_dict[z]) else: for x in range(THREADS): if CORE_MAP[x] in core_dict: cls.cpu_temp[x+1].append(core_dict[CORE_MAP[x]]) elif len(cores) == THREADS / 2: cls.cpu_temp[0].append(temp) for n, t in enumerate(cores, start=1): try: cls.cpu_temp[n].append(t) cls.cpu_temp[THREADS // 2 + n].append(t) except IndexError: break else: cls.cpu_temp[0].append(temp) if len(cores) > 1: for n, t in enumerate(cores, start=1): try: cls.cpu_temp[n].append(t) except IndexError: break except Exception as e: errlog.exception(f'{e}') cls.got_sensors = False CpuBox._calc_size() else: try: if cls.sensor_method == "coretemp": temp = max(0, int(subprocess.check_output(["coretemp", "-p"], universal_newlines=True).strip())) cores = [max(0, int(x)) for x in subprocess.check_output("coretemp", universal_newlines=True).split()] if len(cores) == THREADS / 2: cls.cpu_temp[0].append(temp) for n, t in enumerate(cores, start=1): try: cls.cpu_temp[n].append(t) cls.cpu_temp[THREADS // 2 + n].append(t) except IndexError: break else: cores.insert(0, temp) for n, t in enumerate(cores): try: cls.cpu_temp[n].append(t) except IndexError: break if not cls.cpu_temp_high: cls.cpu_temp_high = 85 cls.cpu_temp_crit = 100 elif cls.sensor_method == "osx-cpu-temp": temp = max(0, round(float(subprocess.check_output("osx-cpu-temp", universal_newlines=True).strip()[:-2]))) if not cls.cpu_temp_high: cls.cpu_temp_high = 85 cls.cpu_temp_crit = 100 elif cls.sensor_method == "vcgencmd": temp = max(0, round(float(subprocess.check_output(["vcgencmd", "measure_temp"], universal_newlines=True).strip()[5:-2]))) if not cls.cpu_temp_high: cls.cpu_temp_high = 60 cls.cpu_temp_crit = 80 except Exception as e: errlog.exception(f'{e}') cls.got_sensors = False CpuBox._calc_size() else: if not cores: cls.cpu_temp[0].append(temp) if not core_dict and len(cores) <= 1: cls.cpu_temp_only = True if len(cls.cpu_temp[0]) > 5: for n in range(len(cls.cpu_temp)): if cls.cpu_temp[n]: del cls.cpu_temp[n][0] @classmethod def _draw(cls): CpuBox._draw_fg() class MemCollector(Collector): '''Collects memory and disks information''' values: Dict[str, int] = {} vlist: Dict[str, List[int]] = {} percent: Dict[str, int] = {} string: Dict[str, str] = {} swap_values: Dict[str, int] = {} swap_vlist: Dict[str, List[int]] = {} swap_percent: Dict[str, int] = {} swap_string: Dict[str, str] = {} disks: Dict[str, Dict] disk_hist: Dict[str, Tuple] = {} timestamp: float = time() disks_io_dict: Dict[str, Dict[str, List[int]]] = {} recheck_diskutil: bool = True diskutil_map: Dict[str, str] = {} io_error: bool = False old_disks: List[str] = [] old_io_disks: List[str] = [] fstab_filter: List[str] = [] excludes: List[str] = ["squashfs", "nullfs"] if SYSTEM == "BSD": excludes += ["devfs", "tmpfs", "procfs", "linprocfs", "gvfs", "fusefs"] buffer: str = MemBox.buffer @classmethod def _collect(cls): #* Collect memory mem = psutil.virtual_memory() if hasattr(mem, "cached"): cls.values["cached"] = mem.cached else: cls.values["cached"] = mem.active cls.values["total"], cls.values["free"], cls.values["available"] = mem.total, mem.free, mem.available cls.values["used"] = cls.values["total"] - cls.values["available"] for key, value in cls.values.items(): cls.string[key] = floating_humanizer(value) if key == "total": continue cls.percent[key] = round(value * 100 / cls.values["total"]) if CONFIG.mem_graphs: if not key in cls.vlist: cls.vlist[key] = [] cls.vlist[key].append(cls.percent[key]) if len(cls.vlist[key]) > MemBox.width: del cls.vlist[key][0] #* Collect swap if CONFIG.show_swap or CONFIG.swap_disk: swap = psutil.swap_memory() cls.swap_values["total"], cls.swap_values["free"] = swap.total, swap.free cls.swap_values["used"] = cls.swap_values["total"] - cls.swap_values["free"] if swap.total: if not MemBox.swap_on: MemBox.redraw = True MemBox.swap_on = True for key, value in cls.swap_values.items(): cls.swap_string[key] = floating_humanizer(value) if key == "total": continue cls.swap_percent[key] = round(value * 100 / cls.swap_values["total"]) if CONFIG.mem_graphs: if not key in cls.swap_vlist: cls.swap_vlist[key] = [] cls.swap_vlist[key].append(cls.swap_percent[key]) if len(cls.swap_vlist[key]) > MemBox.width: del cls.swap_vlist[key][0] else: if MemBox.swap_on: MemBox.redraw = True MemBox.swap_on = False else: if MemBox.swap_on: MemBox.redraw = True MemBox.swap_on = False if not CONFIG.show_disks: return #* Collect disks usage disk_read: int = 0 disk_write: int = 0 dev_name: str disk_name: str filtering: Tuple = () filter_exclude: bool = False io_string_r: str io_string_w: str u_percent: int cls.disks = {} if CONFIG.disks_filter: if CONFIG.disks_filter.startswith("exclude="): filter_exclude = True filtering = tuple(v.strip() for v in CONFIG.disks_filter.replace("exclude=", "").strip().split(",")) else: filtering = tuple(v.strip() for v in CONFIG.disks_filter.strip().split(",")) try: io_counters = psutil.disk_io_counters(perdisk=SYSTEM != "BSD", nowrap=True) except ValueError as e: if not cls.io_error: cls.io_error = True errlog.error(f'Non fatal error during disk io collection!') if psutil.version_info[0] < 5 or (psutil.version_info[0] == 5 and psutil.version_info[1] < 7): errlog.error(f'Caused by outdated psutil version.') errlog.exception(f'{e}') io_counters = None if SYSTEM == "MacOS" and cls.recheck_diskutil: cls.recheck_diskutil = False try: dutil_out = subprocess.check_output(["diskutil", "list", "physical"], universal_newlines=True) for line in dutil_out.split("\n"): line = line.replace("\u2068", "").replace("\u2069", "") if line.startswith("/dev/"): xdisk = line.split()[0].replace("/dev/", "") elif "Container" in line: ydisk = line.split()[3] if xdisk and ydisk: cls.diskutil_map[xdisk] = ydisk xdisk = ydisk = "" except: pass if CONFIG.use_fstab and SYSTEM != "MacOS" and not cls.fstab_filter: try: with open('/etc/fstab','r') as fstab: for line in fstab: line = line.strip() if line and not line.startswith('#'): mount_data = (line.split()) if mount_data[2].lower() != "swap": cls.fstab_filter += [mount_data[1]] errlog.debug(f'new fstab_filter set : {cls.fstab_filter}') except IOError: CONFIG.use_fstab = False errlog.warning(f'Error reading fstab, use_fstab flag reset to {CONFIG.use_fstab}') if not CONFIG.use_fstab and cls.fstab_filter: cls.fstab_filter = [] errlog.debug(f'use_fstab flag has been turned to {CONFIG.use_fstab}, fstab_filter cleared') for disk in psutil.disk_partitions(all=CONFIG.use_fstab or not CONFIG.only_physical): disk_io = None io_string_r = io_string_w = "" if CONFIG.use_fstab and disk.mountpoint not in cls.fstab_filter: continue disk_name = disk.mountpoint.rsplit('/', 1)[-1] if not disk.mountpoint == "/" else "root" if cls.excludes and disk.fstype in cls.excludes: continue if filtering and ((not filter_exclude and not disk.mountpoint in filtering) or (filter_exclude and disk.mountpoint in filtering)): continue if SYSTEM == "MacOS" and disk.mountpoint == "/private/var/vm": continue try: disk_u = psutil.disk_usage(disk.mountpoint) except: pass u_percent = round(getattr(disk_u, "percent", 0)) cls.disks[disk.device] = { "name" : disk_name, "used_percent" : u_percent, "free_percent" : 100 - u_percent } for name in ["total", "used", "free"]: cls.disks[disk.device][name] = floating_humanizer(getattr(disk_u, name, 0)) #* Collect disk io if io_counters: try: if SYSTEM != "BSD": dev_name = os.path.realpath(disk.device).rsplit('/', 1)[-1] if not dev_name in io_counters: for names in io_counters: if names in dev_name: disk_io = io_counters[names] break else: if cls.diskutil_map: for names, items in cls.diskutil_map.items(): if items in dev_name and names in io_counters: disk_io = io_counters[names] else: disk_io = io_counters[dev_name] elif disk.mountpoint == "/": disk_io = io_counters else: raise Exception disk_read = round((disk_io.read_bytes - cls.disk_hist[disk.device][0]) / (time() - cls.timestamp)) #type: ignore disk_write = round((disk_io.write_bytes - cls.disk_hist[disk.device][1]) / (time() - cls.timestamp)) #type: ignore if not disk.device in cls.disks_io_dict: cls.disks_io_dict[disk.device] = {"read" : [], "write" : [], "rw" : []} cls.disks_io_dict[disk.device]["read"].append(disk_read >> 20) cls.disks_io_dict[disk.device]["write"].append(disk_write >> 20) cls.disks_io_dict[disk.device]["rw"].append((disk_read + disk_write) >> 20) if len(cls.disks_io_dict[disk.device]["read"]) > MemBox.width: del cls.disks_io_dict[disk.device]["read"][0], cls.disks_io_dict[disk.device]["write"][0], cls.disks_io_dict[disk.device]["rw"][0] except: disk_read = disk_write = 0 else: disk_read = disk_write = 0 if disk_io: cls.disk_hist[disk.device] = (disk_io.read_bytes, disk_io.write_bytes) if CONFIG.io_mode or MemBox.disks_width > 30: if disk_read > 0: io_string_r = f'▲{floating_humanizer(disk_read, short=True)}' if disk_write > 0: io_string_w = f'▼{floating_humanizer(disk_write, short=True)}' if CONFIG.io_mode: cls.disks[disk.device]["io_r"] = io_string_r cls.disks[disk.device]["io_w"] = io_string_w elif disk_read + disk_write > 0: io_string_r += f'▼▲{floating_humanizer(disk_read + disk_write, short=True)}' cls.disks[disk.device]["io"] = io_string_r + (" " if io_string_w and io_string_r else "") + io_string_w if CONFIG.swap_disk and MemBox.swap_on: cls.disks["__swap"] = { "name" : "swap", "used_percent" : cls.swap_percent["used"], "free_percent" : cls.swap_percent["free"], "io" : "" } for name in ["total", "used", "free"]: cls.disks["__swap"][name] = cls.swap_string[name] if len(cls.disks) > 2: try: new = { list(cls.disks)[0] : cls.disks.pop(list(cls.disks)[0])} new["__swap"] = cls.disks.pop("__swap") new.update(cls.disks) cls.disks = new except: pass if cls.old_disks != list(cls.disks) or cls.old_io_disks != list(cls.disks_io_dict): MemBox.redraw = True cls.recheck_diskutil = True cls.old_disks = list(cls.disks) cls.old_io_disks = list(cls.disks_io_dict) cls.timestamp = time() @classmethod def _draw(cls): MemBox._draw_fg() class NetCollector(Collector): '''Collects network stats''' buffer: str = NetBox.buffer nics: List[str] = [] nic_i: int = 0 nic: str = "" new_nic: str = "" nic_error: bool = False reset: bool = False graph_raise: Dict[str, int] = {"download" : 5, "upload" : 5} graph_lower: Dict[str, int] = {"download" : 5, "upload" : 5} #min_top: int = 10<<10 #* Stats structure = stats[netword device][download, upload][total, last, top, graph_top, offset, speed, redraw, graph_raise, graph_low] = int, List[int], bool stats: Dict[str, Dict[str, Dict[str, Any]]] = {} #* Strings structure strings[network device][download, upload][total, byte_ps, bit_ps, top, graph_top] = str strings: Dict[str, Dict[str, Dict[str, str]]] = {} switched: bool = False timestamp: float = time() net_min: Dict[str, int] = {"download" : -1, "upload" : -1} auto_min: bool = CONFIG.net_auto net_iface: str = CONFIG.net_iface sync_top: int = 0 sync_string: str = "" address: str = "" @classmethod def _get_nics(cls): '''Get a list of all network devices sorted by highest throughput''' cls.nic_i = 0 cls.nics = [] cls.nic = "" try: io_all = psutil.net_io_counters(pernic=True) except Exception as e: if not cls.nic_error: cls.nic_error = True errlog.exception(f'{e}') if not io_all: return up_stat = psutil.net_if_stats() for nic in sorted(io_all.keys(), key=lambda nic: (getattr(io_all[nic], "bytes_recv", 0) + getattr(io_all[nic], "bytes_sent", 0)), reverse=True): if nic not in up_stat or not up_stat[nic].isup: continue cls.nics.append(nic) if not cls.nics: cls.nics = [""] cls.nic = cls.nics[cls.nic_i] if cls.net_iface and cls.net_iface in cls.nics: cls.nic = cls.net_iface cls.nic_i = cls.nics.index(cls.nic) @classmethod def switch(cls, key: str): if cls.net_iface: cls.net_iface = "" if len(cls.nics) < 2 and cls.nic in cls.nics: return if cls.nic_i == -1: cls.nic_i = 0 if key == "n" else -1 else: cls.nic_i += +1 if key == "n" else -1 cls.nic_i %= len(cls.nics) cls.new_nic = cls.nics[cls.nic_i] cls.switched = True Collector.collect(NetCollector, redraw=True) @classmethod def _collect(cls): speed: int stat: Dict up_stat = psutil.net_if_stats() if sorted(cls.nics) != sorted(nic for nic in up_stat if up_stat[nic].isup): old_nic = cls.nic cls._get_nics() cls.nic = old_nic if cls.nic not in cls.nics: cls.nic_i = -1 else: cls.nic_i = cls.nics.index(cls.nic) if cls.switched: cls.nic = cls.new_nic cls.switched = False if not cls.nic or cls.nic not in up_stat: cls._get_nics() if not cls.nic: return NetBox.redraw = True try: io_all = psutil.net_io_counters(pernic=True)[cls.nic] except KeyError: pass return if not cls.nic in cls.stats: cls.stats[cls.nic] = {} cls.strings[cls.nic] = { "download" : {}, "upload" : {}} for direction, value in ["download", io_all.bytes_recv], ["upload", io_all.bytes_sent]: cls.stats[cls.nic][direction] = { "total" : value, "last" : value, "top" : 0, "graph_top" : 0, "offset" : 0, "speed" : [], "redraw" : True, "graph_raise" : 0, "graph_lower" : 7 } for v in ["total", "byte_ps", "bit_ps", "top", "graph_top"]: cls.strings[cls.nic][direction][v] = "" cls.stats[cls.nic]["download"]["total"] = io_all.bytes_recv cls.stats[cls.nic]["upload"]["total"] = io_all.bytes_sent if cls.nic in psutil.net_if_addrs(): cls.address = getattr(psutil.net_if_addrs()[cls.nic][0], "address", "") for direction in ["download", "upload"]: stat = cls.stats[cls.nic][direction] strings = cls.strings[cls.nic][direction] #* Calculate current speed stat["speed"].append(round((stat["total"] - stat["last"]) / (time() - cls.timestamp))) stat["last"] = stat["total"] speed = stat["speed"][-1] if cls.net_min[direction] == -1: cls.net_min[direction] = units_to_bytes(getattr(CONFIG, "net_" + direction)) stat["graph_top"] = cls.net_min[direction] stat["graph_lower"] = 7 if not cls.auto_min: stat["redraw"] = True strings["graph_top"] = floating_humanizer(stat["graph_top"], short=True) if stat["offset"] and stat["offset"] > stat["total"]: cls.reset = True if cls.reset: if not stat["offset"]: stat["offset"] = stat["total"] else: stat["offset"] = 0 if direction == "upload": cls.reset = False NetBox.redraw = True if len(stat["speed"]) > NetBox.width * 2: del stat["speed"][0] strings["total"] = floating_humanizer(stat["total"] - stat["offset"]) strings["byte_ps"] = floating_humanizer(stat["speed"][-1], per_second=True) strings["bit_ps"] = floating_humanizer(stat["speed"][-1], bit=True, per_second=True) if speed > stat["top"] or not stat["top"]: stat["top"] = speed strings["top"] = floating_humanizer(stat["top"], bit=True, per_second=True) if cls.auto_min: if speed > stat["graph_top"]: stat["graph_raise"] += 1 if stat["graph_lower"] > 0: stat["graph_lower"] -= 1 elif speed < stat["graph_top"] // 10: stat["graph_lower"] += 1 if stat["graph_raise"] > 0: stat["graph_raise"] -= 1 if stat["graph_raise"] >= 5 or stat["graph_lower"] >= 5: if stat["graph_raise"] >= 5: stat["graph_top"] = round(max(stat["speed"][-5:]) / 0.8) elif stat["graph_lower"] >= 5: stat["graph_top"] = max(10 << 10, max(stat["speed"][-5:]) * 3) stat["graph_raise"] = 0 stat["graph_lower"] = 0 stat["redraw"] = True strings["graph_top"] = floating_humanizer(stat["graph_top"], short=True) cls.timestamp = time() if CONFIG.net_sync: c_max: int = max(cls.stats[cls.nic]["download"]["graph_top"], cls.stats[cls.nic]["upload"]["graph_top"]) if c_max != cls.sync_top: cls.sync_top = c_max cls.sync_string = floating_humanizer(cls.sync_top, short=True) NetBox.redraw = True @classmethod def _draw(cls): NetBox._draw_fg() class ProcCollector(Collector): '''Collects process stats''' buffer: str = ProcBox.buffer search_filter: str = "" case_sensitive: bool = False processes: Dict = {} num_procs: int = 0 det_cpu: float = 0.0 detailed: bool = False detailed_pid: Union[int, None] = None details: Dict[str, Any] = {} details_cpu: List[int] = [] details_mem: List[int] = [] expand: int = 0 collapsed: Dict = {} tree_counter: int = 0 p_values: List[str] = ["pid", "name", "cmdline", "num_threads", "username", "memory_percent", "cpu_percent", "cpu_times", "create_time"] sort_expr: Dict = {} sort_expr["pid"] = compile("p.info['pid']", "str", "eval") sort_expr["program"] = compile("'' if p.info['name'] == 0.0 else p.info['name']", "str", "eval") sort_expr["arguments"] = compile("' '.join(str(p.info['cmdline'])) or ('' if p.info['name'] == 0.0 else p.info['name'])", "str", "eval") sort_expr["threads"] = compile("0 if p.info['num_threads'] == 0.0 else p.info['num_threads']", "str", "eval") sort_expr["user"] = compile("'' if p.info['username'] == 0.0 else p.info['username']", "str", "eval") sort_expr["memory"] = compile("p.info['memory_percent']", "str", "eval") sort_expr["cpu lazy"] = compile("(sum(p.info['cpu_times'][:2] if not p.info['cpu_times'] == 0.0 else [0.0, 0.0]) * 1000 / (time() - p.info['create_time']))", "str", "eval") sort_expr["cpu responsive"] = compile("(p.info['cpu_percent'] if CONFIG.proc_per_core else (p.info['cpu_percent'] / THREADS))", "str", "eval") @classmethod def _collect(cls): '''List all processes with pid, name, arguments, threads, username, memory percent and cpu percent''' if not "proc" in Box.boxes: return out: Dict = {} cls.det_cpu = 0.0 sorting: str = CONFIG.proc_sorting reverse: bool = not CONFIG.proc_reversed proc_per_cpu: bool = CONFIG.proc_per_core search: List[str] = [] if cls.search_filter: if cls.case_sensitive: search = [i.strip() for i in cls.search_filter.split(",")] else: search = [i.strip() for i in cls.search_filter.lower().split(",")] err: float = 0.0 n: int = 0 if CONFIG.proc_tree and sorting == "arguments": sorting = "program" sort_cmd = cls.sort_expr[sorting] if CONFIG.proc_tree: cls._tree(sort_cmd=sort_cmd, reverse=reverse, proc_per_cpu=proc_per_cpu, search=search) else: for p in sorted(psutil.process_iter(cls.p_values + (["memory_info"] if CONFIG.proc_mem_bytes else []), err), key=lambda p: eval(sort_cmd), reverse=reverse): if cls.collect_interrupt or cls.proc_interrupt: return if p.info["name"] == "idle" or p.info["name"] == err or p.info["pid"] == err: continue if p.info["cmdline"] == err: p.info["cmdline"] = "" if p.info["username"] == err: p.info["username"] = "" if p.info["num_threads"] == err: p.info["num_threads"] = 0 if search: if cls.detailed and p.info["pid"] == cls.detailed_pid: cls.det_cpu = p.info["cpu_percent"] for value in [ p.info["name"], " ".join(p.info["cmdline"]), str(p.info["pid"]), p.info["username"] ]: if not cls.case_sensitive: value = value.lower() for s in search: if s in value: break else: continue break else: continue cpu = p.info["cpu_percent"] if proc_per_cpu else round(p.info["cpu_percent"] / THREADS, 2) mem = p.info["memory_percent"] if CONFIG.proc_mem_bytes and hasattr(p.info["memory_info"], "rss"): mem_b = p.info["memory_info"].rss else: mem_b = 0 cmd = " ".join(p.info["cmdline"]) or "[" + p.info["name"] + "]" out[p.info["pid"]] = { "name" : p.info["name"], "cmd" : cmd, "threads" : p.info["num_threads"], "username" : p.info["username"], "mem" : mem, "mem_b" : mem_b, "cpu" : cpu } n += 1 cls.num_procs = n cls.processes = out.copy() if cls.detailed: cls.expand = ((ProcBox.width - 2) - ((ProcBox.width - 2) // 3) - 40) // 10 if cls.expand > 5: cls.expand = 5 if cls.detailed and not cls.details.get("killed", False): try: c_pid = cls.detailed_pid det = psutil.Process(c_pid) except (psutil.NoSuchProcess, psutil.ZombieProcess): cls.details["killed"] = True cls.details["status"] = psutil.STATUS_DEAD ProcBox.redraw = True else: attrs: List[str] = ["status", "memory_info", "create_time"] if not SYSTEM == "MacOS": attrs.extend(["cpu_num"]) if cls.expand: attrs.extend(["nice", "terminal"]) if not SYSTEM == "MacOS": attrs.extend(["io_counters"]) if not c_pid in cls.processes: attrs.extend(["pid", "name", "cmdline", "num_threads", "username", "memory_percent"]) cls.details = det.as_dict(attrs=attrs, ad_value="") if det.parent() != None: cls.details["parent_name"] = det.parent().name() else: cls.details["parent_name"] = "" cls.details["pid"] = c_pid if c_pid in cls.processes: cls.details["name"] = cls.processes[c_pid]["name"] cls.details["cmdline"] = cls.processes[c_pid]["cmd"] cls.details["threads"] = f'{cls.processes[c_pid]["threads"]}' cls.details["username"] = cls.processes[c_pid]["username"] cls.details["memory_percent"] = cls.processes[c_pid]["mem"] cls.details["cpu_percent"] = round(cls.processes[c_pid]["cpu"] * (1 if CONFIG.proc_per_core else THREADS)) else: cls.details["cmdline"] = " ".join(cls.details["cmdline"]) or "[" + cls.details["name"] + "]" cls.details["threads"] = f'{cls.details["num_threads"]}' cls.details["cpu_percent"] = round(cls.det_cpu) cls.details["killed"] = False if SYSTEM == "MacOS": cls.details["cpu_num"] = -1 cls.details["io_counters"] = "" if hasattr(cls.details["memory_info"], "rss"): cls.details["memory_bytes"] = floating_humanizer(cls.details["memory_info"].rss) # type: ignore else: cls.details["memory_bytes"] = "? Bytes" if isinstance(cls.details["create_time"], float): uptime = timedelta(seconds=round(time()-cls.details["create_time"],0)) if uptime.days > 0: cls.details["uptime"] = f'{uptime.days}d {str(uptime).split(",")[1][:-3].strip()}' else: cls.details["uptime"] = f'{uptime}' else: cls.details["uptime"] = "??:??:??" if cls.expand: if cls.expand > 1 : cls.details["nice"] = f'{cls.details["nice"]}' if SYSTEM == "BSD": if cls.expand > 2: if hasattr(cls.details["io_counters"], "read_count"): cls.details["io_read"] = f'{cls.details["io_counters"].read_count}' else: cls.details["io_read"] = "?" if cls.expand > 3: if hasattr(cls.details["io_counters"], "write_count"): cls.details["io_write"] = f'{cls.details["io_counters"].write_count}' else: cls.details["io_write"] = "?" else: if cls.expand > 2: if hasattr(cls.details["io_counters"], "read_bytes"): cls.details["io_read"] = floating_humanizer(cls.details["io_counters"].read_bytes) else: cls.details["io_read"] = "?" if cls.expand > 3: if hasattr(cls.details["io_counters"], "write_bytes"): cls.details["io_write"] = floating_humanizer(cls.details["io_counters"].write_bytes) else: cls.details["io_write"] = "?" if cls.expand > 4 : cls.details["terminal"] = f'{cls.details["terminal"]}'.replace("/dev/", "") cls.details_cpu.append(cls.details["cpu_percent"]) mem = cls.details["memory_percent"] if mem > 80: mem = round(mem) elif mem > 60: mem = round(mem * 1.2) elif mem > 30: mem = round(mem * 1.5) elif mem > 10: mem = round(mem * 2) elif mem > 5: mem = round(mem * 10) else: mem = round(mem * 20) cls.details_mem.append(mem) if len(cls.details_cpu) > ProcBox.width: del cls.details_cpu[0] if len(cls.details_mem) > ProcBox.width: del cls.details_mem[0] @classmethod def _tree(cls, sort_cmd, reverse: bool, proc_per_cpu: bool, search: List[str]): '''List all processes in a tree view with pid, name, threads, username, memory percent and cpu percent''' out: Dict = {} err: float = 0.0 det_cpu: float = 0.0 infolist: Dict = {} cls.tree_counter += 1 tree = defaultdict(list) n: int = 0 for p in sorted(psutil.process_iter(cls.p_values + (["memory_info"] if CONFIG.proc_mem_bytes else []), err), key=lambda p: eval(sort_cmd), reverse=reverse): if cls.collect_interrupt: return try: tree[p.ppid()].append(p.pid) except (psutil.NoSuchProcess, psutil.ZombieProcess): pass else: infolist[p.pid] = p.info n += 1 if 0 in tree and 0 in tree[0]: tree[0].remove(0) def create_tree(pid: int, tree: defaultdict, indent: str = "", inindent: str = " ", found: bool = False, depth: int = 0, collapse_to: Union[None, int] = None): nonlocal infolist, proc_per_cpu, search, out, det_cpu name: str; threads: int; username: str; mem: float; cpu: float; collapse: bool = False cont: bool = True getinfo: Dict = {} if cls.collect_interrupt: return try: name = psutil.Process(pid).name() if name == "idle": return except psutil.Error: pass cont = False name = "" if pid in infolist: getinfo = infolist[pid] if search and not found: if cls.detailed and pid == cls.detailed_pid: det_cpu = getinfo["cpu_percent"] if "username" in getinfo and isinstance(getinfo["username"], float): getinfo["username"] = "" if "cmdline" in getinfo and isinstance(getinfo["cmdline"], float): getinfo["cmdline"] = "" for value in [ name, str(pid), getinfo.get("username", ""), " ".join(getinfo.get("cmdline", "")) ]: if not cls.case_sensitive: value = value.lower() for s in search: if s in value: found = True break else: continue break else: cont = False if cont: if getinfo: if getinfo["num_threads"] == err: threads = 0 else: threads = getinfo["num_threads"] if getinfo["username"] == err: username = "" else: username = getinfo["username"] cpu = getinfo["cpu_percent"] if proc_per_cpu else round(getinfo["cpu_percent"] / THREADS, 2) mem = getinfo["memory_percent"] if getinfo["cmdline"] == err: cmd = "" else: cmd = " ".join(getinfo["cmdline"]) or "[" + getinfo["name"] + "]" if CONFIG.proc_mem_bytes and hasattr(getinfo["memory_info"], "rss"): mem_b = getinfo["memory_info"].rss else: mem_b = 0 else: threads = mem_b = 0 username = "" mem = cpu = 0.0 if pid in cls.collapsed: collapse = cls.collapsed[pid] else: collapse = depth > CONFIG.tree_depth cls.collapsed[pid] = collapse if collapse_to and not search: out[collapse_to]["threads"] += threads out[collapse_to]["mem"] += mem out[collapse_to]["mem_b"] += mem_b out[collapse_to]["cpu"] += cpu else: if pid in tree and len(tree[pid]) > 0: sign: str = "+" if collapse else "-" inindent = inindent.replace(" ├─ ", "[" + sign + "]─").replace(" └─ ", "[" + sign + "]─") out[pid] = { "indent" : inindent, "name": name, "cmd" : cmd, "threads" : threads, "username" : username, "mem" : mem, "mem_b" : mem_b, "cpu" : cpu, "depth" : depth, } if search: collapse = False elif collapse and not collapse_to: collapse_to = pid if pid not in tree: return children = tree[pid][:-1] for child in children: create_tree(child, tree, indent + " │ ", indent + " ├─ ", found=found, depth=depth+1, collapse_to=collapse_to) create_tree(tree[pid][-1], tree, indent + " ", indent + " └─ ", depth=depth+1, collapse_to=collapse_to) create_tree(min(tree), tree) cls.det_cpu = det_cpu if cls.collect_interrupt: return if cls.tree_counter >= 100: cls.tree_counter = 0 for pid in list(cls.collapsed): if not psutil.pid_exists(pid): del cls.collapsed[pid] cls.num_procs = len(out) cls.processes = out.copy() @classmethod def sorting(cls, key: str): index: int = CONFIG.sorting_options.index(CONFIG.proc_sorting) + (1 if key in ["right", "l"] else -1) if index >= len(CONFIG.sorting_options): index = 0 elif index < 0: index = len(CONFIG.sorting_options) - 1 CONFIG.proc_sorting = CONFIG.sorting_options[index] if "left" in Key.mouse: del Key.mouse["left"] Collector.collect(ProcCollector, interrupt=True, redraw=True) @classmethod def _draw(cls): ProcBox._draw_fg() class Menu: '''Holds all menus''' active: bool = False close: bool = False resized: bool = True menus: Dict[str, Dict[str, str]] = {} menu_length: Dict[str, int] = {} background: str = "" for name, menu in MENUS.items(): menu_length[name] = len(menu["normal"][0]) menus[name] = {} for sel in ["normal", "selected"]: menus[name][sel] = "" for i in range(len(menu[sel])): menus[name][sel] += Fx.trans(f'{Color.fg(MENU_COLORS[sel][i])}{menu[sel][i]}') if i < len(menu[sel]) - 1: menus[name][sel] += f'{Mv.d(1)}{Mv.l(len(menu[sel][i]))}' @classmethod def main(cls): if Term.width < 80 or Term.height < 24: errlog.warning(f'The menu system only works on a terminal size of 80x24 or above!') return out: str = "" banner: str = "" redraw: bool = True key: str = "" mx: int = 0 my: int = 0 skip: bool = False mouse_over: bool = False mouse_items: Dict[str, Dict[str, int]] = {} cls.active = True cls.resized = True menu_names: List[str] = list(cls.menus.keys()) menu_index: int = 0 menu_current: str = menu_names[0] cls.background = f'{THEME.inactive_fg}' + Fx.uncolor(f'{Draw.saved_buffer()}') + f'{Term.fg}' while not cls.close: key = "" if cls.resized: banner = (f'{Banner.draw(Term.height // 2 - 10, center=True)}{Mv.d(1)}{Mv.l(46)}{Colors.black_bg}{Colors.default}{Fx.b}← esc' f'{Mv.r(30)}{Fx.i}Version: {VERSION}{Fx.ui}{Fx.ub}{Term.bg}{Term.fg}') if UpdateChecker.version != VERSION: banner += f'{Mv.to(Term.height, 1)}{Fx.b}{THEME.title}New release {UpdateChecker.version} available at https://github.com/aristocratos/bpytop{Fx.ub}{Term.fg}' cy = 0 for name, menu in cls.menus.items(): ypos = Term.height // 2 - 2 + cy xpos = Term.width // 2 - (cls.menu_length[name] // 2) mouse_items[name] = { "x1" : xpos, "x2" : xpos + cls.menu_length[name] - 1, "y1" : ypos, "y2" : ypos + 2 } cy += 3 redraw = True cls.resized = False if redraw: out = "" for name, menu in cls.menus.items(): out += f'{Mv.to(mouse_items[name]["y1"], mouse_items[name]["x1"])}{menu["selected" if name == menu_current else "normal"]}' if skip and redraw: Draw.now(out) elif not skip: Draw.now(f'{cls.background}{banner}{out}') skip = redraw = False if Key.input_wait(Timer.left(), mouse=True): if Key.mouse_moved(): mx, my = Key.get_mouse() for name, pos in mouse_items.items(): if pos["x1"] <= mx <= pos["x2"] and pos["y1"] <= my <= pos["y2"]: mouse_over = True if name != menu_current: menu_current = name menu_index = menu_names.index(name) redraw = True break else: mouse_over = False else: key = Key.get() if key == "mouse_click" and not mouse_over: key = "M" if key == "q": clean_quit() elif key in ["escape", "M"]: cls.close = True break elif key in ["up", "mouse_scroll_up", "shift_tab"]: menu_index -= 1 if menu_index < 0: menu_index = len(menu_names) - 1 menu_current = menu_names[menu_index] redraw = True elif key in ["down", "mouse_scroll_down", "tab"]: menu_index += 1 if menu_index > len(menu_names) - 1: menu_index = 0 menu_current = menu_names[menu_index] redraw = True elif key == "enter" or (key == "mouse_click" and mouse_over): if menu_current == "quit": clean_quit() elif menu_current == "options": cls.options() cls.resized = True elif menu_current == "help": cls.help() cls.resized = True if Timer.not_zero() and not cls.resized: skip = True else: Collector.collect() Collector.collect_done.wait(2) if CONFIG.background_update: cls.background = f'{THEME.inactive_fg}' + Fx.uncolor(f'{Draw.saved_buffer()}') + f'{Term.fg}' Timer.stamp() Draw.now(f'{Draw.saved_buffer()}') cls.background = "" cls.active = False cls.close = False @classmethod def help(cls): if Term.width < 80 or Term.height < 24: errlog.warning(f'The menu system only works on a terminal size of 80x24 or above!') return out: str = "" out_misc : str = "" redraw: bool = True key: str = "" skip: bool = False main_active: bool = cls.active cls.active = True cls.resized = True if not cls.background: cls.background = f'{THEME.inactive_fg}' + Fx.uncolor(f'{Draw.saved_buffer()}') + f'{Term.fg}' help_items: Dict[str, str] = { "(Mouse 1)" : "Clicks buttons and selects in process list.", "Selected (Mouse 1)" : "Show detailed information for selected process.", "(Mouse scroll)" : "Scrolls any scrollable list/text under cursor.", "(Esc, shift+m)" : "Toggles main menu.", "(m)" : "Cycle view presets, order: full->proc->stat->user.", "(1)" : "Toggle CPU box.", "(2)" : "Toggle MEM box.", "(3)" : "Toggle NET box.", "(4)" : "Toggle PROC box.", "(d)" : "Toggle disks view in MEM box.", "(F2, o)" : "Shows options.", "(F1, shift+h)" : "Shows this window.", "(ctrl+z)" : "Sleep program and put in background.", "(ctrl+c, q)" : "Quits program.", "(+) / (-)" : "Add/Subtract 100ms to/from update timer.", "(Up, k) (Down, j)" : "Select in process list.", "(Enter)" : "Show detailed information for selected process.", "(Spacebar)" : "Expand/collapse the selected process in tree view.", "(Pg Up) (Pg Down)" : "Jump 1 page in process list.", "(Home) (End)" : "Jump to first or last page in process list.", "(Left, h) (Right, l)" : "Select previous/next sorting column.", "(b) (n)" : "Select previous/next network device.", "(s)" : "Toggle showing swap as a disk.", "(i)" : "Toggle disks io mode with big graphs.", "(z)" : "Toggle totals reset for current network device", "(a)" : "Toggle auto scaling for the network graphs.", "(y)" : "Toggle synced scaling mode for network graphs.", "(f, /)" : "Input a NON case-sensitive process filter.", "(shift+f)" : "Input a case-sensitive process filter.", "(c)" : "Toggle per-core cpu usage of processes.", "(r)" : "Reverse sorting order in processes box.", "(e)" : "Toggle processes tree view.", "(delete)" : "Clear any entered filter.", "Selected (shift+t)" : "Terminate selected process with SIGTERM - 15.", "Selected (shift+k)" : "Kill selected process with SIGKILL - 9.", "Selected (shift+i)" : "Interrupt selected process with SIGINT - 2.", "_1" : " ", "_2" : "For bug reporting and project updates, visit:", "_3" : "https://github.com/aristocratos/bpytop", } while not cls.close: key = "" if cls.resized: y = 8 if Term.height < len(help_items) + 10 else Term.height // 2 - len(help_items) // 2 + 4 out_misc = (f'{Banner.draw(y-7, center=True)}{Mv.d(1)}{Mv.l(46)}{Colors.black_bg}{Colors.default}{Fx.b}← esc' f'{Mv.r(30)}{Fx.i}Version: {VERSION}{Fx.ui}{Fx.ub}{Term.bg}{Term.fg}') x = Term.width//2-36 h, w = Term.height-2-y, 72 if len(help_items) > h: pages = ceil(len(help_items) / h) else: h = len(help_items) pages = 0 page = 1 out_misc += create_box(x, y, w, h+3, "help", line_color=THEME.div_line) redraw = True cls.resized = False if redraw: out = "" cy = 0 if pages: out += (f'{Mv.to(y, x+56)}{THEME.div_line(Symbol.title_left)}{Fx.b}{THEME.title("pg")}{Fx.ub}{THEME.main_fg(Symbol.up)} {Fx.b}{THEME.title}{page}/{pages} ' f'pg{Fx.ub}{THEME.main_fg(Symbol.down)}{THEME.div_line(Symbol.title_right)}') out += f'{Mv.to(y+1, x+1)}{THEME.title}{Fx.b}{"Keys:":^20}Description:{THEME.main_fg}' for n, (keys, desc) in enumerate(help_items.items()): if pages and n < (page - 1) * h: continue out += f'{Mv.to(y+2+cy, x+1)}{Fx.b}{("" if keys.startswith("_") else keys):^20.20}{Fx.ub}{desc:50.50}' cy += 1 if cy == h: break if cy < h: for i in range(h-cy): out += f'{Mv.to(y+2+cy+i, x+1)}{" " * (w-2)}' if skip and redraw: Draw.now(out) elif not skip: Draw.now(f'{cls.background}{out_misc}{out}') skip = redraw = False if Key.input_wait(Timer.left()): key = Key.get() if key == "mouse_click": mx, my = Key.get_mouse() if x <= mx < x + w and y <= my < y + h + 3: if pages and my == y and x + 56 < mx < x + 61: key = "up" elif pages and my == y and x + 63 < mx < x + 68: key = "down" else: key = "escape" if key == "q": clean_quit() elif key in ["escape", "M", "enter", "backspace", "H", "f1"]: cls.close = True break elif key in ["up", "mouse_scroll_up", "page_up"] and pages: page -= 1 if page < 1: page = pages redraw = True elif key in ["down", "mouse_scroll_down", "page_down"] and pages: page += 1 if page > pages: page = 1 redraw = True if Timer.not_zero() and not cls.resized: skip = True else: Collector.collect() Collector.collect_done.wait(2) if CONFIG.background_update: cls.background = f'{THEME.inactive_fg}' + Fx.uncolor(f'{Draw.saved_buffer()}') + f'{Term.fg}' Timer.stamp() if main_active: cls.close = False return Draw.now(f'{Draw.saved_buffer()}') cls.background = "" cls.active = False cls.close = False @classmethod def options(cls): if Term.width < 80 or Term.height < 24: errlog.warning(f'The menu system only works on a terminal size of 80x24 or above!') return out: str = "" out_misc : str = "" redraw: bool = True selected_cat: str = "" selected_int: int = 0 option_items: Dict[str, List[str]] = {} cat_list: List[str] = [] cat_int: int = 0 change_cat: bool = False key: str = "" skip: bool = False main_active: bool = cls.active cls.active = True cls.resized = True d_quote: str inputting: bool = False input_val: str = "" Theme.refresh() if not cls.background: cls.background = f'{THEME.inactive_fg}' + Fx.uncolor(f'{Draw.saved_buffer()}') + f'{Term.fg}' categories: Dict[str, Dict[str, List[str]]] = { "system" : { "color_theme" : [ 'Set color theme.', '', 'Choose from all theme files in', '"/usr/[local/]share/bpytop/themes" and', '"~/.config/bpytop/themes".', '', '"Default" for builtin default theme.', 'User themes are prefixed by a plus sign "+".', '', 'For theme updates see:', 'https://github.com/aristocratos/bpytop'], "theme_background" : [ 'If the theme set background should be shown.', '', 'Set to False if you want terminal background', 'transparency.'], "truecolor" : [ 'Sets if 24-bit truecolor should be used.', '(Requires restart to take effect!)', '', 'Will convert 24-bit colors to 256 color', '(6x6x6 color cube) if False.', '', 'Set to False if your terminal doesn\'t have', 'truecolor support and can\'t convert to', '256-color.'], "shown_boxes" : [ 'Manually set which boxes to show.', '', 'Available values are "cpu mem net proc".', 'Seperate values with whitespace.', '', 'Toggle between presets with mode key "m".'], "update_ms" : [ 'Update time in milliseconds.', '', 'Recommended 2000 ms or above for better sample', 'times for graphs.', '', 'Min value: 100 ms', 'Max value: 86400000 ms = 24 hours.'], "draw_clock" : [ 'Draw a clock at top of screen.', '(Only visible if cpu box is enabled!)', '', 'Formatting according to strftime, empty', 'string to disable.', '', 'Custom formatting options:', '"/host" = hostname', '"/user" = username', '"/uptime" = system uptime', '', 'Examples of strftime formats:', '"%X" = locale HH:MM:SS', '"%H" = 24h hour, "%I" = 12h hour', '"%M" = minute, "%S" = second', '"%d" = day, "%m" = month, "%y" = year'], "background_update" : [ 'Update main ui when menus are showing.', '', 'True or False.', '', 'Set this to false if the menus is flickering', 'too much for a comfortable experience.'], "show_battery" : [ 'Show battery stats.', '(Only visible if cpu box is enabled!)', '', 'Show battery stats in the top right corner', 'if a battery is present.'], "show_init" : [ 'Show init screen at startup.', '', 'The init screen is purely cosmetical and', 'slows down start to show status messages.'], "update_check" : [ 'Check for updates at start.', '', 'Checks for latest version from:', 'https://github.com/aristocratos/bpytop'], "log_level" : [ 'Set loglevel for error.log', '', 'Levels are: "ERROR" "WARNING" "INFO" "DEBUG".', 'The level set includes all lower levels,', 'i.e. "DEBUG" will show all logging info.'] }, "cpu" : { "cpu_graph_upper" : [ 'Sets the CPU stat shown in upper half of', 'the CPU graph.', '', '"total" = Total cpu usage.', '"user" = User mode cpu usage.', '"system" = Kernel mode cpu usage.', 'See:', 'https://psutil.readthedocs.io/en/latest/', '#psutil.cpu_times', 'for attributes available on specific platforms.'], "cpu_graph_lower" : [ 'Sets the CPU stat shown in lower half of', 'the CPU graph.', '', '"total" = Total cpu usage.', '"user" = User mode cpu usage.', '"system" = Kernel mode cpu usage.', 'See:', 'https://psutil.readthedocs.io/en/latest/', '#psutil.cpu_times', 'for attributes available on specific platforms.'], "cpu_invert_lower" : [ 'Toggles orientation of the lower CPU graph.', '', 'True or False.'], "cpu_single_graph" : [ 'Completely disable the lower CPU graph.', '', 'Shows only upper CPU graph and resizes it', 'to fit to box height.', '', 'True or False.'], "check_temp" : [ 'Enable cpu temperature reporting.', '', 'True or False.'], "cpu_sensor" : [ 'Cpu temperature sensor', '', 'Select the sensor that corresponds to', 'your cpu temperature.', 'Set to "Auto" for auto detection.'], "show_coretemp" : [ 'Show temperatures for cpu cores.', '', 'Only works if check_temp is True and', 'the system is reporting core temps.'], "temp_scale" : [ 'Which temperature scale to use.', '', 'Celsius, default scale.', '', 'Fahrenheit, the american one.', '', 'Kelvin, 0 = absolute zero, 1 degree change', 'equals 1 degree change in Celsius.', '', 'Rankine, 0 = absolute zero, 1 degree change', 'equals 1 degree change in Fahrenheit.'], "show_cpu_freq" : [ 'Show CPU frequency', '', 'Can cause slowdowns on systems with many', 'cores and psutil versions below 5.8.1'], "custom_cpu_name" : [ 'Custom cpu model name in cpu percentage box.', '', 'Empty string to disable.'], "show_uptime" : [ 'Shows the system uptime in the CPU box.', '', 'Can also be shown in the clock by using', '"/uptime" in the formatting.', '', 'True or False.'], }, "mem" : { "mem_graphs" : [ 'Show graphs for memory values.', '', 'True or False.'], "show_disks" : [ 'Split memory box to also show disks.', '', 'True or False.'], "show_io_stat" : [ 'Toggle small IO stat graphs.', '', 'Toggles the small IO graphs for the regular', 'disk usage view.', '', 'True or False.'], "io_mode" : [ 'Toggles io mode for disks.', '', 'Shows big graphs for disk read/write speeds', 'instead of used/free percentage meters.', '', 'True or False.'], "io_graph_combined" : [ 'Toggle combined read and write graphs.', '', 'Only has effect if "io mode" is True.', '', 'True or False.'], "io_graph_speeds" : [ 'Set top speeds for the io graphs.', '', 'Manually set which speed in MiB/s that equals', '100 percent in the io graphs.', '(10 MiB/s by default).', '', 'Format: "device:speed" separate disks with a', 'comma ",".', '', 'Example: "/dev/sda:100, /dev/sdb:20".'], "show_swap" : [ 'If swap memory should be shown in memory box.', '', 'True or False.'], "swap_disk" : [ 'Show swap as a disk.', '', 'Ignores show_swap value above.', 'Inserts itself after first disk.'], "only_physical" : [ 'Filter out non physical disks.', '', 'Set this to False to include network disks,', 'RAM disks and similar.', '', 'True or False.'], "use_fstab" : [ 'Read disks list from /etc/fstab.', '(Has no effect on macOS X)', '', 'This also disables only_physical.', '', 'True or False.'], "disks_filter" : [ 'Optional filter for shown disks.', '', 'Should be full path of a mountpoint,', '"root" replaces "/", separate multiple values', 'with a comma ",".', 'Begin line with "exclude=" to change to exclude', 'filter.', 'Otherwise defaults to "most include" filter.', '', 'Example: disks_filter="exclude=/boot, /home/user"'], }, "net" : { "net_download" : [ 'Fixed network graph download value.', '', 'Default "10M" = 10 MibiBytes.', 'Possible units:', '"K" (KiB), "M" (MiB), "G" (GiB).', '', 'Append "bit" for bits instead of bytes,', 'i.e "100Mbit"', '', 'Can be toggled with auto button.'], "net_upload" : [ 'Fixed network graph upload value.', '', 'Default "10M" = 10 MibiBytes.', 'Possible units:', '"K" (KiB), "M" (MiB), "G" (GiB).', '', 'Append "bit" for bits instead of bytes,', 'i.e "100Mbit"', '', 'Can be toggled with auto button.'], "net_auto" : [ 'Start in network graphs auto rescaling mode.', '', 'Ignores any values set above at start and', 'rescales down to 10KibiBytes at the lowest.', '', 'True or False.'], "net_sync" : [ 'Network scale sync.', '', 'Syncs the scaling for download and upload to', 'whichever currently has the highest scale.', '', 'True or False.'], "net_color_fixed" : [ 'Set network graphs color gradient to fixed.', '', 'If True the network graphs color is based', 'on the total bandwidth usage instead of', 'the current autoscaling.', '', 'The bandwidth usage is based on the', '"net_download" and "net_upload" values set', 'above.'], "net_iface" : [ 'Network Interface.', '', 'Manually set the starting Network Interface.', 'Will otherwise automatically choose the NIC', 'with the highest total download since boot.'], }, "proc" : { "proc_update_mult" : [ 'Processes update multiplier.', 'Sets how often the process list is updated as', 'a multiplier of "update_ms".', '', 'Set to 2 or higher to greatly decrease bpytop', 'cpu usage. (Only integers)'], "proc_sorting" : [ 'Processes sorting option.', '', 'Possible values: "pid", "program", "arguments",', '"threads", "user", "memory", "cpu lazy" and', '"cpu responsive".', '', '"cpu lazy" updates top process over time,', '"cpu responsive" updates top process directly.'], "proc_reversed" : [ 'Reverse processes sorting order.', '', 'True or False.'], "proc_tree" : [ 'Processes tree view.', '', 'Set true to show processes grouped by parents,', 'with lines drawn between parent and child', 'process.'], "tree_depth" : [ 'Process tree auto collapse depth.', '', 'Sets the depth where the tree view will auto', 'collapse processes at.'], "proc_colors" : [ 'Enable colors in process view.', '', 'Uses the cpu graph gradient colors.'], "proc_gradient" : [ 'Enable process view gradient fade.', '', 'Fades from top or current selection.', 'Max fade value is equal to current themes', '"inactive_fg" color value.'], "proc_per_core" : [ 'Process usage per core.', '', 'If process cpu usage should be of the core', 'it\'s running on or usage of the total', 'available cpu power.', '', 'If true and process is multithreaded', 'cpu usage can reach over 100%.'], "proc_mem_bytes" : [ 'Show memory as bytes in process list.', ' ', 'True or False.'], } } loglevel_i: int = CONFIG.log_levels.index(CONFIG.log_level) cpu_sensor_i: int = CONFIG.cpu_sensors.index(CONFIG.cpu_sensor) cpu_graph_i: Dict[str, int] = { "cpu_graph_upper" : CONFIG.cpu_percent_fields.index(CONFIG.cpu_graph_upper), "cpu_graph_lower" : CONFIG.cpu_percent_fields.index(CONFIG.cpu_graph_lower)} temp_scale_i: int = CONFIG.temp_scales.index(CONFIG.temp_scale) color_i: int max_opt_len: int = max([len(categories[x]) for x in categories]) * 2 cat_list = list(categories) while not cls.close: key = "" if cls.resized or change_cat: cls.resized = change_cat = False selected_cat = list(categories)[cat_int] option_items = categories[cat_list[cat_int]] option_len: int = len(option_items) * 2 y = 12 if Term.height < max_opt_len + 13 else Term.height // 2 - max_opt_len // 2 + 7 out_misc = (f'{Banner.draw(y-10, center=True)}{Mv.d(1)}{Mv.l(46)}{Colors.black_bg}{Colors.default}{Fx.b}← esc' f'{Mv.r(30)}{Fx.i}Version: {VERSION}{Fx.ui}{Fx.ub}{Term.bg}{Term.fg}') x = Term.width//2-38 x2 = x + 27 h, w, w2 = min(Term.height-1-y, option_len), 26, 50 h -= h % 2 color_i = list(Theme.themes).index(THEME.current) out_misc += create_box(x, y - 3, w+w2+1, 3, f'tab{Symbol.right}', line_color=THEME.div_line) out_misc += create_box(x, y, w, h+2, "options", line_color=THEME.div_line) redraw = True cat_width = floor((w+w2) / len(categories)) out_misc += f'{Fx.b}' for cx, cat in enumerate(categories): out_misc += f'{Mv.to(y-2, x + 1 + (cat_width * cx) + round(cat_width / 2 - len(cat) / 2 ))}' if cat == selected_cat: out_misc += f'{THEME.hi_fg}[{THEME.title}{Fx.u}{cat}{Fx.uu}{THEME.hi_fg}]' else: out_misc += f'{THEME.hi_fg}{SUPERSCRIPT[cx+1]}{THEME.title}{cat}' out_misc += f'{Fx.ub}' if option_len > h: pages = ceil(option_len / h) else: h = option_len pages = 0 page = pages if selected_int == -1 and pages > 0 else 1 selected_int = 0 if selected_int >= 0 else len(option_items) - 1 if redraw: out = "" cy = 0 selected = list(option_items)[selected_int] if pages: out += (f'{Mv.to(y+h+1, x+11)}{THEME.div_line(Symbol.title_left)}{Fx.b}{THEME.title("pg")}{Fx.ub}{THEME.main_fg(Symbol.up)} {Fx.b}{THEME.title}{page}/{pages} ' f'pg{Fx.ub}{THEME.main_fg(Symbol.down)}{THEME.div_line(Symbol.title_right)}') #out += f'{Mv.to(y+1, x+1)}{THEME.title}{Fx.b}{"Keys:":^20}Description:{THEME.main_fg}' for n, opt in enumerate(option_items): if pages and n < (page - 1) * ceil(h / 2): continue value = getattr(CONFIG, opt) t_color = f'{THEME.selected_bg}{THEME.selected_fg}' if opt == selected else f'{THEME.title}' v_color = "" if opt == selected else f'{THEME.title}' d_quote = '"' if isinstance(value, str) else "" if opt == "color_theme": counter = f' {color_i + 1}/{len(Theme.themes)}' elif opt == "proc_sorting": counter = f' {CONFIG.sorting_options.index(CONFIG.proc_sorting) + 1}/{len(CONFIG.sorting_options)}' elif opt == "log_level": counter = f' {loglevel_i + 1}/{len(CONFIG.log_levels)}' elif opt == "cpu_sensor": counter = f' {cpu_sensor_i + 1}/{len(CONFIG.cpu_sensors)}' elif opt in ["cpu_graph_upper", "cpu_graph_lower"]: counter = f' {cpu_graph_i[opt] + 1}/{len(CONFIG.cpu_percent_fields)}' elif opt == "temp_scale": counter = f' {temp_scale_i + 1}/{len(CONFIG.temp_scales)}' else: counter = "" out += f'{Mv.to(y+1+cy, x+1)}{t_color}{Fx.b}{opt.replace("_", " ").capitalize() + counter:^24.24}{Fx.ub}{Mv.to(y+2+cy, x+1)}{v_color}' if opt == selected: if isinstance(value, bool) or opt in ["color_theme", "proc_sorting", "log_level", "cpu_sensor", "cpu_graph_upper", "cpu_graph_lower", "temp_scale"]: out += f'{t_color} {Symbol.left}{v_color}{d_quote + str(value) + d_quote:^20.20}{t_color}{Symbol.right} ' elif inputting: out += f'{str(input_val)[-17:] + Fx.bl + "█" + Fx.ubl + "" + Symbol.enter:^33.33}' else: out += ((f'{t_color} {Symbol.left}{v_color}' if type(value) is int else " ") + f'{str(value) + " " + Symbol.enter:^20.20}' + (f'{t_color}{Symbol.right} ' if type(value) is int else " ")) else: out += f'{d_quote + str(value) + d_quote:^24.24}' out += f'{Term.bg}' if opt == selected: h2 = len(option_items[opt]) + 2 y2 = y + (selected_int * 2) - ((page-1) * h) if y2 + h2 > Term.height: y2 = Term.height - h2 out += f'{create_box(x2, y2, w2, h2, "description", line_color=THEME.div_line)}{THEME.main_fg}' for n, desc in enumerate(option_items[opt]): out += f'{Mv.to(y2+1+n, x2+2)}{desc:.48}' cy += 2 if cy >= h: break if cy < h: for i in range(h-cy): out += f'{Mv.to(y+1+cy+i, x+1)}{" " * (w-2)}' if not skip or redraw: Draw.now(f'{cls.background}{out_misc}{out}') skip = redraw = False if Key.input_wait(Timer.left()): key = Key.get() redraw = True has_sel = False if key == "mouse_click" and not inputting: mx, my = Key.get_mouse() if x < mx < x + w + w2 and y - 4 < my < y: # if my == y - 2: for cx, cat in enumerate(categories): ccx = x + (cat_width * cx) + round(cat_width / 2 - len(cat) / 2 ) if ccx - 2 < mx < ccx + 2 + len(cat): key = str(cx+1) break elif x < mx < x + w and y < my < y + h + 2: mouse_sel = ceil((my - y) / 2) - 1 + ceil((page-1) * (h / 2)) if pages and my == y+h+1 and x+11 < mx < x+16: key = "page_up" elif pages and my == y+h+1 and x+19 < mx < x+24: key = "page_down" elif my == y+h+1: pass elif mouse_sel == selected_int: if mx < x + 6: key = "left" elif mx > x + 19: key = "right" else: key = "enter" elif mouse_sel < len(option_items): selected_int = mouse_sel has_sel = True else: key = "escape" if inputting: if key in ["escape", "mouse_click"]: inputting = False elif key == "enter": inputting = False if str(getattr(CONFIG, selected)) != input_val: if selected == "update_ms": if not input_val or int(input_val) < 100: CONFIG.update_ms = 100 elif int(input_val) > 86399900: CONFIG.update_ms = 86399900 else: CONFIG.update_ms = int(input_val) elif selected == "proc_update_mult": if not input_val or int(input_val) < 1: CONFIG.proc_update_mult = 1 else: CONFIG.proc_update_mult = int(input_val) Collector.proc_counter = 1 elif selected == "tree_depth": if not input_val or int(input_val) < 0: CONFIG.tree_depth = 0 else: CONFIG.tree_depth = int(input_val) ProcCollector.collapsed = {} elif selected == "shown_boxes": new_boxes: List = [] for box in input_val.split(): if box in ["cpu", "mem", "net", "proc"]: new_boxes.append(box) CONFIG.shown_boxes = " ".join(new_boxes) Box.view_mode = "user" Box.view_modes["user"] = CONFIG.shown_boxes.split() Draw.clear(saved=True) elif isinstance(getattr(CONFIG, selected), str): setattr(CONFIG, selected, input_val) if selected.startswith("net_"): NetCollector.net_min = {"download" : -1, "upload" : -1} elif selected == "draw_clock": Box.clock_on = len(CONFIG.draw_clock) > 0 if not Box.clock_on: Draw.clear("clock", saved=True) elif selected == "io_graph_speeds": MemBox.graph_speeds = {} Term.refresh(force=True) cls.resized = False elif key == "backspace" and len(input_val): input_val = input_val[:-1] elif key == "delete": input_val = "" elif isinstance(getattr(CONFIG, selected), str) and len(key) == 1: input_val += key elif isinstance(getattr(CONFIG, selected), int) and key.isdigit(): input_val += key elif key == "q": clean_quit() elif key in ["escape", "o", "M", "f2"]: cls.close = True break elif key == "tab" or (key == "down" and selected_int == len(option_items) - 1 and page in [0, pages]): if cat_int == len(categories) - 1: cat_int = 0 else: cat_int += 1 change_cat = True elif key == "shift_tab" or (key == "up" and selected_int == 0 and page == 1): if cat_int == 0: cat_int = len(categories) - 1 else: cat_int -= 1 change_cat = True selected_int = -1 if key != "shift_tab" else 0 elif key in list(map(str, range(1, len(cat_list)+1))) and key != str(cat_int + 1): cat_int = int(key) - 1 change_cat = True elif key == "enter" and selected in ["update_ms", "disks_filter", "custom_cpu_name", "net_download", "net_upload", "draw_clock", "tree_depth", "proc_update_mult", "shown_boxes", "net_iface", "io_graph_speeds"]: inputting = True input_val = str(getattr(CONFIG, selected)) elif key == "left" and selected == "update_ms" and CONFIG.update_ms - 100 >= 100: CONFIG.update_ms -= 100 Box.draw_update_ms() elif key == "right" and selected == "update_ms" and CONFIG.update_ms + 100 <= 86399900: CONFIG.update_ms += 100 Box.draw_update_ms() elif key == "left" and selected == "proc_update_mult" and CONFIG.proc_update_mult > 1: CONFIG.proc_update_mult -= 1 Collector.proc_counter = 1 elif key == "right" and selected == "proc_update_mult": CONFIG.proc_update_mult += 1 Collector.proc_counter = 1 elif key == "left" and selected == "tree_depth" and CONFIG.tree_depth > 0: CONFIG.tree_depth -= 1 ProcCollector.collapsed = {} elif key == "right" and selected == "tree_depth": CONFIG.tree_depth += 1 ProcCollector.collapsed = {} elif key in ["left", "right"] and isinstance(getattr(CONFIG, selected), bool): setattr(CONFIG, selected, not getattr(CONFIG, selected)) if selected == "check_temp": if CONFIG.check_temp: CpuCollector.get_sensors() else: CpuCollector.sensor_method = "" CpuCollector.got_sensors = False if selected in ["net_auto", "net_color_fixed", "net_sync"]: if selected == "net_auto": NetCollector.auto_min = CONFIG.net_auto NetBox.redraw = True if selected == "theme_background": Term.bg = f'{THEME.main_bg}' if CONFIG.theme_background else "\033[49m" Draw.now(Term.bg) if selected == "show_battery": Draw.clear("battery", saved=True) Term.refresh(force=True) cls.resized = False elif key in ["left", "right"] and selected == "color_theme" and len(Theme.themes) > 1: if key == "left": color_i -= 1 if color_i < 0: color_i = len(Theme.themes) - 1 elif key == "right": color_i += 1 if color_i > len(Theme.themes) - 1: color_i = 0 Collector.collect_idle.wait() CONFIG.color_theme = list(Theme.themes)[color_i] THEME(CONFIG.color_theme) Term.refresh(force=True) Timer.finish() elif key in ["left", "right"] and selected == "proc_sorting": ProcCollector.sorting(key) elif key in ["left", "right"] and selected == "log_level": if key == "left": loglevel_i -= 1 if loglevel_i < 0: loglevel_i = len(CONFIG.log_levels) - 1 elif key == "right": loglevel_i += 1 if loglevel_i > len(CONFIG.log_levels) - 1: loglevel_i = 0 CONFIG.log_level = CONFIG.log_levels[loglevel_i] errlog.setLevel(getattr(logging, CONFIG.log_level)) errlog.info(f'Loglevel set to {CONFIG.log_level}') elif key in ["left", "right"] and selected in ["cpu_graph_upper", "cpu_graph_lower"]: if key == "left": cpu_graph_i[selected] -= 1 if cpu_graph_i[selected] < 0: cpu_graph_i[selected] = len(CONFIG.cpu_percent_fields) - 1 if key == "right": cpu_graph_i[selected] += 1 if cpu_graph_i[selected] > len(CONFIG.cpu_percent_fields) - 1: cpu_graph_i[selected] = 0 setattr(CONFIG, selected, CONFIG.cpu_percent_fields[cpu_graph_i[selected]]) setattr(CpuCollector, selected.replace("_graph", ""), []) Term.refresh(force=True) cls.resized = False elif key in ["left", "right"] and selected == "temp_scale": if key == "left": temp_scale_i -= 1 if temp_scale_i < 0: temp_scale_i = len(CONFIG.temp_scales) - 1 if key == "right": temp_scale_i += 1 if temp_scale_i > len(CONFIG.temp_scales) - 1: temp_scale_i = 0 CONFIG.temp_scale = CONFIG.temp_scales[temp_scale_i] Term.refresh(force=True) cls.resized = False elif key in ["left", "right"] and selected == "cpu_sensor" and len(CONFIG.cpu_sensors) > 1: if key == "left": cpu_sensor_i -= 1 if cpu_sensor_i < 0: cpu_sensor_i = len(CONFIG.cpu_sensors) - 1 elif key == "right": cpu_sensor_i += 1 if cpu_sensor_i > len(CONFIG.cpu_sensors) - 1: cpu_sensor_i = 0 Collector.collect_idle.wait() CpuCollector.sensor_swap = True CONFIG.cpu_sensor = CONFIG.cpu_sensors[cpu_sensor_i] if CONFIG.check_temp and (CpuCollector.sensor_method != "psutil" or CONFIG.cpu_sensor == "Auto"): CpuCollector.get_sensors() Term.refresh(force=True) cls.resized = False elif key in ["up", "mouse_scroll_up"]: selected_int -= 1 if selected_int < 0: selected_int = len(option_items) - 1 page = floor(selected_int * 2 / h) + 1 elif key in ["down", "mouse_scroll_down"]: selected_int += 1 if selected_int > len(option_items) - 1: selected_int = 0 page = floor(selected_int * 2 / h) + 1 elif key == "page_up": if not pages or page == 1: selected_int = 0 else: page -= 1 if page < 1: page = pages selected_int = (page-1) * ceil(h / 2) elif key == "page_down": if not pages or page == pages: selected_int = len(option_items) - 1 else: page += 1 if page > pages: page = 1 selected_int = (page-1) * ceil(h / 2) elif has_sel: pass else: redraw = False if Timer.not_zero() and not cls.resized: skip = True else: Collector.collect() Collector.collect_done.wait(2) if CONFIG.background_update: cls.background = f'{THEME.inactive_fg}' + Fx.uncolor(f'{Draw.saved_buffer()}') + f'{Term.fg}' Timer.stamp() if main_active: cls.close = False return Draw.now(f'{Draw.saved_buffer()}') cls.background = "" cls.active = False cls.close = False class Timer: timestamp: float return_zero = False @classmethod def stamp(cls): cls.timestamp = time() @classmethod def not_zero(cls) -> bool: if cls.return_zero: cls.return_zero = False return False return cls.timestamp + (CONFIG.update_ms / 1000) > time() @classmethod def left(cls) -> float: t_left: float = cls.timestamp + (CONFIG.update_ms / 1000) - time() if t_left > CONFIG.update_ms / 1000: cls.stamp() return CONFIG.update_ms / 1000 return t_left @classmethod def finish(cls): cls.return_zero = True cls.timestamp = time() - (CONFIG.update_ms / 1000) Key.break_wait() class UpdateChecker: version: str = VERSION thread: threading.Thread @classmethod def run(cls): cls.thread = threading.Thread(target=cls._checker) cls.thread.start() @classmethod def _checker(cls): try: with urllib.request.urlopen("https://github.com/aristocratos/bpytop/raw/master/bpytop.py", timeout=5) as source: # type: ignore for line in source: line = line.decode("utf-8") if line.startswith("VERSION: str ="): cls.version = line[(line.index("=")+1):].strip('" \n') break except Exception as e: errlog.exception(f'{e}') else: if cls.version != VERSION and which("notify-send"): try: subprocess.run(["notify-send", "-u", "normal", "BpyTop Update!", f'New version of BpyTop available!\nCurrent version: {VERSION}\nNew version: {cls.version}\nDownload at github.com/aristocratos/bpytop', "-i", "update-notifier", "-t", "10000"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except Exception as e: errlog.exception(f'{e}') class Init: running: bool = True initbg_colors: List[str] = [] initbg_data: List[int] initbg_up: Graph initbg_down: Graph resized = False @classmethod def start(cls): Draw.buffer("init", z=1) Draw.buffer("initbg", z=10) for i in range(51): for _ in range(2): cls.initbg_colors.append(Color.fg(i, i, i)) Draw.buffer("banner", (f'{Banner.draw(Term.height // 2 - 10, center=True)}{Mv.d(1)}{Mv.l(11)}{Colors.black_bg}{Colors.default}' f'{Fx.b}{Fx.i}Version: {VERSION}{Fx.ui}{Fx.ub}{Term.bg}{Term.fg}{Color.fg("#50")}'), z=2) for _i in range(7): perc = f'{str(round((_i + 1) * 14 + 2)) + "%":>5}' Draw.buffer("+banner", f'{Mv.to(Term.height // 2 - 2 + _i, Term.width // 2 - 28)}{Fx.trans(perc)}{Symbol.v_line}') Draw.out("banner") Draw.buffer("+init!", f'{Color.fg("#cc")}{Fx.b}{Mv.to(Term.height // 2 - 2, Term.width // 2 - 21)}{Mv.save}') cls.initbg_data = [randint(0, 100) for _ in range(Term.width * 2)] cls.initbg_up = Graph(Term.width, Term.height // 2, cls.initbg_colors, cls.initbg_data, invert=True) cls.initbg_down = Graph(Term.width, Term.height // 2, cls.initbg_colors, cls.initbg_data, invert=False) @classmethod def success(cls): if not CONFIG.show_init or cls.resized: return cls.draw_bg(5) Draw.buffer("+init!", f'{Mv.restore}{Symbol.ok}\n{Mv.r(Term.width // 2 - 22)}{Mv.save}') @staticmethod def fail(err): if CONFIG.show_init: Draw.buffer("+init!", f'{Mv.restore}{Symbol.fail}') sleep(2) errlog.exception(f'{err}') clean_quit(1, errmsg=f'Error during init! See {CONFIG_DIR}/error.log for more information.') @classmethod def draw_bg(cls, times: int = 5): for _ in range(times): sleep(0.05) x = randint(0, 100) Draw.buffer("initbg", f'{Fx.ub}{Mv.to(0, 0)}{cls.initbg_up(x)}{Mv.to(Term.height // 2, 0)}{cls.initbg_down(x)}') Draw.out("initbg", "banner", "init") @classmethod def done(cls): cls.running = False if not CONFIG.show_init: return if cls.resized: Draw.now(Term.clear) else: cls.draw_bg(10) Draw.clear("initbg", "banner", "init", saved=True) if cls.resized: return del cls.initbg_up, cls.initbg_down, cls.initbg_data, cls.initbg_colors #? Functions -------------------------------------------------------------------------------------> def get_cpu_name() -> str: '''Fetch a suitable CPU identifier from the CPU model name string''' name: str = "" nlist: List = [] command: str = "" cmd_out: str = "" rem_line: str = "" if SYSTEM == "Linux": command = "cat /proc/cpuinfo" rem_line = "model name" elif SYSTEM == "MacOS": command ="sysctl -n machdep.cpu.brand_string" elif SYSTEM == "BSD": command ="sysctl hw.model" rem_line = "hw.model" try: cmd_out = subprocess.check_output("LANG=C " + command, shell=True, universal_newlines=True) except: pass if rem_line: for line in cmd_out.split("\n"): if rem_line in line: name = re.sub( ".*" + rem_line + ".*:", "", line,1).lstrip() else: name = cmd_out nlist = name.split(" ") try: if "Xeon" in name and "CPU" in name: name = nlist[nlist.index("CPU")+(-1 if name.endswith(("CPU", "z")) else 1)] elif "Ryzen" in name: name = " ".join(nlist[nlist.index("Ryzen"):nlist.index("Ryzen")+3]) elif "Duo" in name and "@" in name: name = " ".join(nlist[:nlist.index("@")]) elif "CPU" in name and not nlist[0] == "CPU" and not nlist[nlist.index("CPU")-1].isdigit(): name = nlist[nlist.index("CPU")-1] except: pass name = name.replace("Processor", "").replace("CPU", "").replace("(R)", "").replace("(TM)", "").replace("Intel", "") name = re.sub(r"\d?\.?\d+[mMgG][hH][zZ]", "", name) name = " ".join(name.split()) return name def get_cpu_core_mapping() -> List[int]: mapping: List[int] = [] core_ids: List[int] = [] if SYSTEM == "Linux" and os.path.isfile("/proc/cpuinfo"): try: mapping = [0] * THREADS num = 0 with open("/proc/cpuinfo", "r") as f: for line in f: if line.startswith("processor"): num = int(line.strip()[(line.index(": ")+2):]) if num > THREADS - 1: break elif line.startswith("core id"): core_id = int(line.strip()[(line.index(": ")+2):]) if core_id not in core_ids: core_ids.append(core_id) mapping[num] = core_ids.index(core_id) if num < THREADS - 1: raise Exception except: mapping = [] if not mapping: mapping = [] for _ in range(THREADS // CORES): mapping.extend([x for x in range(CORES)]) return mapping def create_box(x: int = 0, y: int = 0, width: int = 0, height: int = 0, title: str = "", title2: str = "", line_color: Color = None, title_color: Color = None, fill: bool = True, box = None) -> str: '''Create a box from a box object or by given arguments''' out: str = f'{Term.fg}{Term.bg}' num: int = 0 if not line_color: line_color = THEME.div_line if not title_color: title_color = THEME.title #* Get values from box class if given if box: x = box.x y = box.y width = box.width height = box.height title = box.name num = box.num hlines: Tuple[int, int] = (y, y + height - 1) out += f'{line_color}' #* Draw all horizontal lines for hpos in hlines: out += f'{Mv.to(hpos, x)}{Symbol.h_line * (width - 1)}' #* Draw all vertical lines and fill if enabled for hpos in range(hlines[0]+1, hlines[1]): out += f'{Mv.to(hpos, x)}{Symbol.v_line}{" " * (width-2) if fill else Mv.r(width-2)}{Symbol.v_line}' #* Draw corners out += f'{Mv.to(y, x)}{Symbol.left_up}\ {Mv.to(y, x + width - 1)}{Symbol.right_up}\ {Mv.to(y + height - 1, x)}{Symbol.left_down}\ {Mv.to(y + height - 1, x + width - 1)}{Symbol.right_down}' #* Draw titles if enabled if title: numbered: str = "" if not num else f'{THEME.hi_fg(SUPERSCRIPT[num])}' out += f'{Mv.to(y, x + 2)}{Symbol.title_left}{Fx.b}{numbered}{title_color}{title}{Fx.ub}{line_color}{Symbol.title_right}' if title2: out += f'{Mv.to(hlines[1], x + 2)}{Symbol.title_left}{title_color}{Fx.b}{title2}{Fx.ub}{line_color}{Symbol.title_right}' return f'{out}{Term.fg}{Mv.to(y + 1, x + 1)}' def now_sleeping(signum, frame): """Reset terminal settings and stop background input read before putting to sleep""" Key.stop() Collector.stop() Draw.now(Term.clear, Term.normal_screen, Term.show_cursor, Term.mouse_off, Term.mouse_direct_off, Term.title()) Term.echo(True) os.kill(os.getpid(), signal.SIGSTOP) def now_awake(signum, frame): """Set terminal settings and restart background input read""" Draw.now(Term.alt_screen, Term.clear, Term.hide_cursor, Term.mouse_on, Term.title("BpyTOP")) Term.echo(False) Key.start() Term.refresh() Box.calc_sizes() Box.draw_bg() Collector.start() def quit_sigint(signum, frame): """SIGINT redirection to clean_quit()""" clean_quit() def clean_quit(errcode: int = 0, errmsg: str = "", thread: bool = False): """Stop background input read, save current config and reset terminal settings before quitting""" global THREAD_ERROR if thread: THREAD_ERROR = errcode interrupt_main() return if THREAD_ERROR: errcode = THREAD_ERROR Key.stop() Collector.stop() if not errcode: CONFIG.save_config() Draw.now(Term.clear, Term.normal_screen, Term.show_cursor, Term.mouse_off, Term.mouse_direct_off, Term.title()) Term.echo(True) if errcode == 0: errlog.info(f'Exiting. Runtime {timedelta(seconds=round(time() - SELF_START, 0))} \n') else: errlog.warning(f'Exiting with errorcode ({errcode}). Runtime {timedelta(seconds=round(time() - SELF_START, 0))} \n') if not errmsg: errmsg = f'Bpytop exited with errorcode ({errcode}). See {CONFIG_DIR}/error.log for more information!' if errmsg: print(errmsg) raise SystemExit(errcode) def floating_humanizer(value: Union[float, int], bit: bool = False, per_second: bool = False, start: int = 0, short: bool = False) -> str: '''Scales up in steps of 1024 to highest possible unit and returns string with unit suffixed * bit=True or defaults to bytes * start=int to set 1024 multiplier starting unit * short=True always returns 0 decimals and shortens unit to 1 character ''' out: str = "" mult: int = 8 if bit else 1 selector: int = start unit: Tuple[str, ...] = UNITS["bit"] if bit else UNITS["byte"] if isinstance(value, float): value = round(value * 100 * mult) elif value > 0: value *= 100 * mult else: value = 0 while len(f'{value}') > 5 and value >= 102400: value >>= 10 if value < 100: out = f'{value}' break selector += 1 else: if len(f'{value}') == 4 and selector > 0: out = f'{value}'[:-2] + "." + f'{value}'[-2] elif len(f'{value}') == 3 and selector > 0: out = f'{value}'[:-2] + "." + f'{value}'[-2:] elif len(f'{value}') >= 2: out = f'{value}'[:-2] else: out = f'{value}' if short: if "." in out: out = f'{round(float(out))}' if len(out) > 3: out = f'{int(out[0]) + 1}' selector += 1 out += f'{"" if short else " "}{unit[selector][0] if short else unit[selector]}' if per_second: out += "ps" if bit else "/s" return out def units_to_bytes(value: str) -> int: if not value: return 0 out: int = 0 mult: int = 0 bit: bool = False value_i: int = 0 units: Dict[str, int] = {"k" : 1, "m" : 2, "g" : 3} try: if value.lower().endswith("s"): value = value[:-1] if value.lower().endswith("bit"): bit = True value = value[:-3] elif value.lower().endswith("byte"): value = value[:-4] if value[-1].lower() in units: mult = units[value[-1].lower()] value = value[:-1] if "." in value and value.replace(".", "").isdigit(): if mult > 0: value_i = round(float(value) * 1024) mult -= 1 else: value_i = round(float(value)) elif value.isdigit(): value_i = int(value) out = int(value_i) << (10 * mult) if bit: out = round(out / 8) except ValueError: out = 0 return out def min_max(value: int, min_value: int=0, max_value: int=100) -> int: return max(min_value, min(value, max_value)) def readfile(file: str, default: str = "") -> str: out: Union[str, None] = None if os.path.isfile(file): try: with open(file, "r") as f: out = f.read().strip() except: pass return default if out is None else out def temperature(value: int, scale: str = "celsius") -> Tuple[int, str]: """Returns a tuple with integer value and string unit converted from an integer in celsius to: celsius, fahrenheit, kelvin or rankine.""" if scale == "celsius": return (value, "°C") elif scale == "fahrenheit": return (round(value * 1.8 + 32), "°F") elif scale == "kelvin": return (round(value + 273.15), "K ") elif scale == "rankine": return (round(value * 1.8 + 491.67), "°R") else: return (0, "") def process_keys(): mouse_pos: Tuple[int, int] = (0, 0) filtered: bool = False box_keys = {"1" : "cpu", "2" : "mem", "3" : "net", "4" : "proc"} while Key.has_key(): key = Key.get() found: bool = True if key in ["mouse_scroll_up", "mouse_scroll_down", "mouse_click"]: mouse_pos = Key.get_mouse() if mouse_pos[0] >= ProcBox.x and ProcBox.current_y + 1 <= mouse_pos[1] < ProcBox.current_y + ProcBox.current_h - 1: pass elif key == "mouse_click": key = "mouse_unselect" else: key = "_null" if ProcBox.filtering: if key in ["enter", "mouse_click", "mouse_unselect"]: ProcBox.filtering = False Collector.collect(ProcCollector, redraw=True, only_draw=True) continue elif key in ["escape", "delete"]: ProcCollector.search_filter = "" ProcBox.filtering = False elif len(key) == 1: ProcCollector.search_filter += key elif key == "backspace" and len(ProcCollector.search_filter) > 0: ProcCollector.search_filter = ProcCollector.search_filter[:-1] else: continue Collector.collect(ProcCollector, proc_interrupt=True, redraw=True) if filtered: Collector.collect_done.wait(0.1) filtered = True continue if key == "_null": continue elif key == "q": clean_quit() elif key == "+" and CONFIG.update_ms + 100 <= 86399900: CONFIG.update_ms += 100 Box.draw_update_ms() elif key == "-" and CONFIG.update_ms - 100 >= 100: CONFIG.update_ms -= 100 Box.draw_update_ms() elif key in ["M", "escape"]: Menu.main() elif key in ["o", "f2"]: Menu.options() elif key in ["H", "f1"]: Menu.help() elif key == "m": if list(Box.view_modes).index(Box.view_mode) + 1 > len(list(Box.view_modes)) - 1: Box.view_mode = list(Box.view_modes)[0] else: Box.view_mode = list(Box.view_modes)[(list(Box.view_modes).index(Box.view_mode) + 1)] CONFIG.shown_boxes = " ".join(Box.view_modes[Box.view_mode]) Draw.clear(saved=True) Term.refresh(force=True) elif key in box_keys: boxes = CONFIG.shown_boxes.split() if box_keys[key] in boxes: boxes.remove(box_keys[key]) else: boxes.append(box_keys[key]) CONFIG.shown_boxes = " ".join(boxes) Box.view_mode = "user" Box.view_modes["user"] = CONFIG.shown_boxes.split() Draw.clear(saved=True) Term.refresh(force=True) else: found = False if found: continue if "proc" in Box.boxes: if key in ["left", "right", "h", "l"]: ProcCollector.sorting(key) elif key == " " and CONFIG.proc_tree and ProcBox.selected > 0: if ProcBox.selected_pid in ProcCollector.collapsed: ProcCollector.collapsed[ProcBox.selected_pid] = not ProcCollector.collapsed[ProcBox.selected_pid] Collector.collect(ProcCollector, interrupt=True, redraw=True) elif key == "e": CONFIG.proc_tree = not CONFIG.proc_tree Collector.collect(ProcCollector, interrupt=True, redraw=True) elif key == "r": CONFIG.proc_reversed = not CONFIG.proc_reversed Collector.collect(ProcCollector, interrupt=True, redraw=True) elif key == "c": CONFIG.proc_per_core = not CONFIG.proc_per_core Collector.collect(ProcCollector, interrupt=True, redraw=True) elif key in ["f", "F", "/"]: ProcBox.filtering = True ProcCollector.case_sensitive = key == "F" if not ProcCollector.search_filter: ProcBox.start = 0 Collector.collect(ProcCollector, redraw=True, only_draw=True) elif key in ["T", "K", "I"] and (ProcBox.selected > 0 or ProcCollector.detailed): pid: int = ProcBox.selected_pid if ProcBox.selected > 0 else ProcCollector.detailed_pid # type: ignore if psutil.pid_exists(pid): if key == "T": sig = signal.SIGTERM elif key == "K": sig = signal.SIGKILL elif key == "I": sig = signal.SIGINT try: os.kill(pid, sig) except Exception as e: errlog.error(f'Exception when sending signal {sig} to pid {pid}') errlog.exception(f'{e}') elif key == "delete" and ProcCollector.search_filter: ProcCollector.search_filter = "" Collector.collect(ProcCollector, proc_interrupt=True, redraw=True) elif key == "enter": if ProcBox.selected > 0 and ProcCollector.detailed_pid != ProcBox.selected_pid and psutil.pid_exists(ProcBox.selected_pid): ProcCollector.detailed = True ProcBox.last_selection = ProcBox.selected ProcBox.selected = 0 ProcCollector.detailed_pid = ProcBox.selected_pid ProcBox.resized = True Collector.proc_counter = 1 elif ProcCollector.detailed: ProcBox.selected = ProcBox.last_selection ProcBox.last_selection = 0 ProcCollector.detailed = False ProcCollector.detailed_pid = None ProcBox.resized = True Collector.proc_counter = 1 else: continue ProcCollector.details = {} ProcCollector.details_cpu = [] ProcCollector.details_mem = [] Graphs.detailed_cpu = NotImplemented Graphs.detailed_mem = NotImplemented Collector.collect(ProcCollector, proc_interrupt=True, redraw=True) elif key in ["up", "down", "mouse_scroll_up", "mouse_scroll_down", "page_up", "page_down", "home", "end", "mouse_click", "mouse_unselect", "j", "k"]: ProcBox.selector(key, mouse_pos) if "net" in Box.boxes: if key in ["b", "n"]: NetCollector.switch(key) elif key == "z": NetCollector.reset = not NetCollector.reset Collector.collect(NetCollector, redraw=True) elif key == "y": CONFIG.net_sync = not CONFIG.net_sync Collector.collect(NetCollector, redraw=True) elif key == "a": NetCollector.auto_min = not NetCollector.auto_min NetCollector.net_min = {"download" : -1, "upload" : -1} Collector.collect(NetCollector, redraw=True) if "mem" in Box.boxes: if key == "g": CONFIG.mem_graphs = not CONFIG.mem_graphs Collector.collect(MemCollector, interrupt=True, redraw=True) elif key == "s": Collector.collect_idle.wait() CONFIG.swap_disk = not CONFIG.swap_disk Collector.collect(MemCollector, interrupt=True, redraw=True) elif key == "d": Collector.collect_idle.wait() CONFIG.show_disks = not CONFIG.show_disks Collector.collect(MemCollector, interrupt=True, redraw=True) elif key == "i": Collector.collect_idle.wait() CONFIG.io_mode = not CONFIG.io_mode Collector.collect(MemCollector, interrupt=True, redraw=True) #? Pre main --------------------------------------------------------------------------------------> CPU_NAME: str = get_cpu_name() CORE_MAP: List[int] = get_cpu_core_mapping() THEME: Theme def main(): global THEME Term.width = os.get_terminal_size().columns Term.height = os.get_terminal_size().lines #? Init --------------------------------------------------------------------------------------> if DEBUG: TimeIt.start("Init") #? Switch to alternate screen, clear screen, hide cursor, enable mouse reporting and disable input echo Draw.now(Term.alt_screen, Term.clear, Term.hide_cursor, Term.mouse_on, Term.title("BpyTOP")) Term.echo(False) #Term.refresh(force=True) #? Start a thread checking for updates while running init if CONFIG.update_check: UpdateChecker.run() #? Draw banner and init status if CONFIG.show_init and not Init.resized: Init.start() #? Load theme if CONFIG.show_init: Draw.buffer("+init!", f'{Mv.restore}{Fx.trans("Loading theme and creating colors... ")}{Mv.save}') try: THEME = Theme(CONFIG.color_theme) except Exception as e: Init.fail(e) else: Init.success() #? Setup boxes if CONFIG.show_init: Draw.buffer("+init!", f'{Mv.restore}{Fx.trans("Doing some maths and drawing... ")}{Mv.save}') try: if CONFIG.check_temp: CpuCollector.get_sensors() Box.calc_sizes() Box.draw_bg(now=False) except Exception as e: Init.fail(e) else: Init.success() #? Setup signal handlers for SIGSTP, SIGCONT, SIGINT and SIGWINCH if CONFIG.show_init: Draw.buffer("+init!", f'{Mv.restore}{Fx.trans("Setting up signal handlers... ")}{Mv.save}') try: signal.signal(signal.SIGTSTP, now_sleeping) #* Ctrl-Z signal.signal(signal.SIGCONT, now_awake) #* Resume signal.signal(signal.SIGINT, quit_sigint) #* Ctrl-C signal.signal(signal.SIGWINCH, Term.refresh) #* Terminal resized except Exception as e: Init.fail(e) else: Init.success() #? Start a separate thread for reading keyboard input if CONFIG.show_init: Draw.buffer("+init!", f'{Mv.restore}{Fx.trans("Starting input reader thread... ")}{Mv.save}') try: if isinstance(sys.stdin, io.TextIOWrapper) and sys.version_info >= (3, 7): sys.stdin.reconfigure(errors="ignore") # type: ignore Key.start() except Exception as e: Init.fail(e) else: Init.success() #? Start a separate thread for data collection and drawing if CONFIG.show_init: Draw.buffer("+init!", f'{Mv.restore}{Fx.trans("Starting data collection and drawer thread... ")}{Mv.save}') try: Collector.start() except Exception as e: Init.fail(e) else: Init.success() #? Collect data and draw to buffer if CONFIG.show_init: Draw.buffer("+init!", f'{Mv.restore}{Fx.trans("Collecting data and drawing... ")}{Mv.save}') try: Collector.collect(draw_now=False) pass except Exception as e: Init.fail(e) else: Init.success() #? Draw to screen if CONFIG.show_init: Draw.buffer("+init!", f'{Mv.restore}{Fx.trans("Finishing up... ")}{Mv.save}') try: Collector.collect_done.wait() except Exception as e: Init.fail(e) else: Init.success() Init.done() Term.refresh() Draw.out(clear=True) if CONFIG.draw_clock: Box.clock_on = True if DEBUG: TimeIt.stop("Init") #? Main loop -------------------------------------------------------------------------------------> def run(): while not False: Term.refresh() Timer.stamp() while Timer.not_zero(): if Key.input_wait(Timer.left()): process_keys() Collector.collect() #? Start main loop try: run() except Exception as e: errlog.exception(f'{e}') clean_quit(1) else: #? Quit cleanly even if false starts being true... clean_quit() if __name__ == "__main__": main() bpytop-1.0.68/poetry.lock000066400000000000000000001113201416067541000153210ustar00rootroot00000000000000[[package]] name = "astroid" version = "2.9.0" description = "An abstract syntax tree for Python with inference support." category = "dev" optional = false python-versions = "~=3.6" [package.dependencies] lazy-object-proxy = ">=1.4.0" typed-ast = {version = ">=1.4.0,<2.0", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} wrapt = ">=1.11,<1.14" [[package]] name = "atomicwrites" version = "1.4.0" description = "Atomic file writes." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" version = "21.2.0" description = "Classes Without Boilerplate" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [package.extras] dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] [[package]] name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "importlib-metadata" version = "4.10.0" description = "Read metadata from Python packages" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] perf = ["ipython"] testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] [[package]] name = "iniconfig" version = "1.1.1" description = "iniconfig: brain-dead simple config-ini parsing" category = "dev" optional = false python-versions = "*" [[package]] name = "isort" version = "5.10.1" description = "A Python utility / library to sort Python imports." category = "dev" optional = false python-versions = ">=3.6.1,<4.0" [package.extras] pipfile_deprecated_finder = ["pipreqs", "requirementslib"] requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] plugins = ["setuptools"] [[package]] name = "lazy-object-proxy" version = "1.7.1" description = "A fast and thorough lazy object proxy." category = "dev" optional = false python-versions = ">=3.6" [[package]] name = "mccabe" version = "0.6.1" description = "McCabe checker, plugin for flake8" category = "dev" optional = false python-versions = "*" [[package]] name = "more-itertools" version = "8.12.0" description = "More routines for operating on iterables, beyond itertools" category = "dev" optional = false python-versions = ">=3.5" [[package]] name = "mypy" version = "0.790" description = "Optional static typing for Python" category = "dev" optional = false python-versions = ">=3.5" [package.dependencies] mypy-extensions = ">=0.4.3,<0.5.0" typed-ast = ">=1.4.0,<1.5.0" typing-extensions = ">=3.7.4" [package.extras] dmypy = ["psutil (>=4.0)"] [[package]] name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." category = "dev" optional = false python-versions = "*" [[package]] name = "packaging" version = "21.3" description = "Core utilities for Python packages" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "platformdirs" version = "2.4.0" description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.6" [package.extras] docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] [[package]] name = "pluggy" version = "1.0.0" description = "plugin and hook calling mechanisms for python" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] name = "psutil" version = "5.8.0" description = "Cross-platform lib for process and system monitoring in Python." category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [package.extras] test = ["ipaddress", "mock", "unittest2", "enum34", "pywin32", "wmi"] [[package]] name = "py" version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "pylint" version = "2.12.2" description = "python code static checker" category = "dev" optional = false python-versions = ">=3.6.2" [package.dependencies] astroid = ">=2.9.0,<2.10" colorama = {version = "*", markers = "sys_platform == \"win32\""} isort = ">=4.2.5,<6" mccabe = ">=0.6,<0.7" platformdirs = ">=2.2.0" toml = ">=0.9.2" typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [[package]] name = "pyparsing" version = "3.0.6" description = "Python parsing module" category = "dev" optional = false python-versions = ">=3.6" [package.extras] diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" version = "6.2.5" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" py = ">=1.8.2" toml = "*" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] [[package]] name = "toml" version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" category = "dev" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "typed-ast" version = "1.4.3" description = "a fork of Python 2 and 3 ast modules with type comment support" category = "dev" optional = false python-versions = "*" [[package]] name = "typing-extensions" version = "4.0.1" description = "Backported and Experimental Type Hints for Python 3.6+" category = "dev" optional = false python-versions = ">=3.6" [[package]] name = "wrapt" version = "1.13.3" description = "Module for decorators, wrappers and monkey patching." category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "zipp" version = "3.6.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false python-versions = ">=3.6" [package.extras] docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [metadata] lock-version = "1.1" python-versions = "^3.7" content-hash = "b3bd3798e1494ff11c901ea6d6c21d7d42daf0c58cf688edb702430bf977233b" [metadata.files] astroid = [ {file = "astroid-2.9.0-py3-none-any.whl", hash = "sha256:776ca0b748b4ad69c00bfe0fff38fa2d21c338e12c84aa9715ee0d473c422778"}, {file = "astroid-2.9.0.tar.gz", hash = "sha256:5939cf55de24b92bda00345d4d0659d01b3c7dafb5055165c330bc7c568ba273"}, ] atomicwrites = [ {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, ] attrs = [ {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, ] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] importlib-metadata = [ {file = "importlib_metadata-4.10.0-py3-none-any.whl", hash = "sha256:b7cf7d3fef75f1e4c80a96ca660efbd51473d7e8f39b5ab9210febc7809012a4"}, {file = "importlib_metadata-4.10.0.tar.gz", hash = "sha256:92a8b58ce734b2a4494878e0ecf7d79ccd7a128b5fc6014c401e0b61f006f0f6"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] lazy-object-proxy = [ {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"}, {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"}, {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"}, {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"}, {file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"}, {file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"}, {file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"}, {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"}, {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"}, {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"}, {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"}, {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"}, {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"}, {file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"}, {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"}, {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"}, {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"}, {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"}, {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"}, {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"}, {file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"}, {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"}, {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"}, {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"}, {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"}, {file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"}, {file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"}, {file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"}, {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"}, {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"}, {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"}, {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"}, {file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"}, {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, ] mccabe = [ {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, ] more-itertools = [ {file = "more-itertools-8.12.0.tar.gz", hash = "sha256:7dc6ad46f05f545f900dd59e8dfb4e84a4827b97b3cfecb175ea0c7d247f6064"}, {file = "more_itertools-8.12.0-py3-none-any.whl", hash = "sha256:43e6dd9942dffd72661a2c4ef383ad7da1e6a3e968a927ad7a6083ab410a688b"}, ] mypy = [ {file = "mypy-0.790-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:bd03b3cf666bff8d710d633d1c56ab7facbdc204d567715cb3b9f85c6e94f669"}, {file = "mypy-0.790-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2170492030f6faa537647d29945786d297e4862765f0b4ac5930ff62e300d802"}, {file = "mypy-0.790-cp35-cp35m-win_amd64.whl", hash = "sha256:e86bdace26c5fe9cf8cb735e7cedfe7850ad92b327ac5d797c656717d2ca66de"}, {file = "mypy-0.790-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e97e9c13d67fbe524be17e4d8025d51a7dca38f90de2e462243ab8ed8a9178d1"}, {file = "mypy-0.790-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0d34d6b122597d48a36d6c59e35341f410d4abfa771d96d04ae2c468dd201abc"}, {file = "mypy-0.790-cp36-cp36m-win_amd64.whl", hash = "sha256:72060bf64f290fb629bd4a67c707a66fd88ca26e413a91384b18db3876e57ed7"}, {file = "mypy-0.790-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:eea260feb1830a627fb526d22fbb426b750d9f5a47b624e8d5e7e004359b219c"}, {file = "mypy-0.790-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c614194e01c85bb2e551c421397e49afb2872c88b5830e3554f0519f9fb1c178"}, {file = "mypy-0.790-cp37-cp37m-win_amd64.whl", hash = "sha256:0a0d102247c16ce93c97066443d11e2d36e6cc2a32d8ccc1f705268970479324"}, {file = "mypy-0.790-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cf4e7bf7f1214826cf7333627cb2547c0db7e3078723227820d0a2490f117a01"}, {file = "mypy-0.790-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:af4e9ff1834e565f1baa74ccf7ae2564ae38c8df2a85b057af1dbbc958eb6666"}, {file = "mypy-0.790-cp38-cp38-win_amd64.whl", hash = "sha256:da56dedcd7cd502ccd3c5dddc656cb36113dd793ad466e894574125945653cea"}, {file = "mypy-0.790-py3-none-any.whl", hash = "sha256:2842d4fbd1b12ab422346376aad03ff5d0805b706102e475e962370f874a5122"}, {file = "mypy-0.790.tar.gz", hash = "sha256:2b21ba45ad9ef2e2eb88ce4aeadd0112d0f5026418324176fd494a6824b74975"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] platformdirs = [ {file = "platformdirs-2.4.0-py3-none-any.whl", hash = "sha256:8868bbe3c3c80d42f20156f22e7131d2fb321f5bc86a2a345375c6481a67021d"}, {file = "platformdirs-2.4.0.tar.gz", hash = "sha256:367a5e80b3d04d2428ffa76d33f124cf11e8fff2acdaa9b43d545f5c7d661ef2"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] psutil = [ {file = "psutil-5.8.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0066a82f7b1b37d334e68697faba68e5ad5e858279fd6351c8ca6024e8d6ba64"}, {file = "psutil-5.8.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:0ae6f386d8d297177fd288be6e8d1afc05966878704dad9847719650e44fc49c"}, {file = "psutil-5.8.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:12d844996d6c2b1d3881cfa6fa201fd635971869a9da945cf6756105af73d2df"}, {file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:02b8292609b1f7fcb34173b25e48d0da8667bc85f81d7476584d889c6e0f2131"}, {file = "psutil-5.8.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6ffe81843131ee0ffa02c317186ed1e759a145267d54fdef1bc4ea5f5931ab60"}, {file = "psutil-5.8.0-cp27-none-win32.whl", hash = "sha256:ea313bb02e5e25224e518e4352af4bf5e062755160f77e4b1767dd5ccb65f876"}, {file = "psutil-5.8.0-cp27-none-win_amd64.whl", hash = "sha256:5da29e394bdedd9144c7331192e20c1f79283fb03b06e6abd3a8ae45ffecee65"}, {file = "psutil-5.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:74fb2557d1430fff18ff0d72613c5ca30c45cdbfcddd6a5773e9fc1fe9364be8"}, {file = "psutil-5.8.0-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:74f2d0be88db96ada78756cb3a3e1b107ce8ab79f65aa885f76d7664e56928f6"}, {file = "psutil-5.8.0-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:99de3e8739258b3c3e8669cb9757c9a861b2a25ad0955f8e53ac662d66de61ac"}, {file = "psutil-5.8.0-cp36-cp36m-win32.whl", hash = "sha256:36b3b6c9e2a34b7d7fbae330a85bf72c30b1c827a4366a07443fc4b6270449e2"}, {file = "psutil-5.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:52de075468cd394ac98c66f9ca33b2f54ae1d9bff1ef6b67a212ee8f639ec06d"}, {file = "psutil-5.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c6a5fd10ce6b6344e616cf01cc5b849fa8103fbb5ba507b6b2dee4c11e84c935"}, {file = "psutil-5.8.0-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:61f05864b42fedc0771d6d8e49c35f07efd209ade09a5afe6a5059e7bb7bf83d"}, {file = "psutil-5.8.0-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:0dd4465a039d343925cdc29023bb6960ccf4e74a65ad53e768403746a9207023"}, {file = "psutil-5.8.0-cp37-cp37m-win32.whl", hash = "sha256:1bff0d07e76114ec24ee32e7f7f8d0c4b0514b3fae93e3d2aaafd65d22502394"}, {file = "psutil-5.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:fcc01e900c1d7bee2a37e5d6e4f9194760a93597c97fee89c4ae51701de03563"}, {file = "psutil-5.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6223d07a1ae93f86451d0198a0c361032c4c93ebd4bf6d25e2fb3edfad9571ef"}, {file = "psutil-5.8.0-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d225cd8319aa1d3c85bf195c4e07d17d3cd68636b8fc97e6cf198f782f99af28"}, {file = "psutil-5.8.0-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:28ff7c95293ae74bf1ca1a79e8805fcde005c18a122ca983abf676ea3466362b"}, {file = "psutil-5.8.0-cp38-cp38-win32.whl", hash = "sha256:ce8b867423291cb65cfc6d9c4955ee9bfc1e21fe03bb50e177f2b957f1c2469d"}, {file = "psutil-5.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:90f31c34d25b1b3ed6c40cdd34ff122b1887a825297c017e4cbd6796dd8b672d"}, {file = "psutil-5.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6323d5d845c2785efb20aded4726636546b26d3b577aded22492908f7c1bdda7"}, {file = "psutil-5.8.0-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:245b5509968ac0bd179287d91210cd3f37add77dad385ef238b275bad35fa1c4"}, {file = "psutil-5.8.0-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:90d4091c2d30ddd0a03e0b97e6a33a48628469b99585e2ad6bf21f17423b112b"}, {file = "psutil-5.8.0-cp39-cp39-win32.whl", hash = "sha256:ea372bcc129394485824ae3e3ddabe67dc0b118d262c568b4d2602a7070afdb0"}, {file = "psutil-5.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:f4634b033faf0d968bb9220dd1c793b897ab7f1189956e1aa9eae752527127d3"}, {file = "psutil-5.8.0.tar.gz", hash = "sha256:0c9ccb99ab76025f2f0bbecf341d4656e9c1351db8cc8a03ccd62e318ab4b5c6"}, ] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] pylint = [ {file = "pylint-2.12.2-py3-none-any.whl", hash = "sha256:daabda3f7ed9d1c60f52d563b1b854632fd90035bcf01443e234d3dc794e3b74"}, {file = "pylint-2.12.2.tar.gz", hash = "sha256:9d945a73640e1fec07ee34b42f5669b770c759acd536ec7b16d7e4b87a9c9ff9"}, ] pyparsing = [ {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, ] pytest = [ {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] typed-ast = [ {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6"}, {file = "typed_ast-1.4.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075"}, {file = "typed_ast-1.4.3-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528"}, {file = "typed_ast-1.4.3-cp35-cp35m-win32.whl", hash = "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428"}, {file = "typed_ast-1.4.3-cp35-cp35m-win_amd64.whl", hash = "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3"}, {file = "typed_ast-1.4.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f"}, {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341"}, {file = "typed_ast-1.4.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace"}, {file = "typed_ast-1.4.3-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f"}, {file = "typed_ast-1.4.3-cp36-cp36m-win32.whl", hash = "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363"}, {file = "typed_ast-1.4.3-cp36-cp36m-win_amd64.whl", hash = "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7"}, {file = "typed_ast-1.4.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266"}, {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e"}, {file = "typed_ast-1.4.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04"}, {file = "typed_ast-1.4.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899"}, {file = "typed_ast-1.4.3-cp37-cp37m-win32.whl", hash = "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c"}, {file = "typed_ast-1.4.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805"}, {file = "typed_ast-1.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a"}, {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff"}, {file = "typed_ast-1.4.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41"}, {file = "typed_ast-1.4.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39"}, {file = "typed_ast-1.4.3-cp38-cp38-win32.whl", hash = "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927"}, {file = "typed_ast-1.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40"}, {file = "typed_ast-1.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3"}, {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4"}, {file = "typed_ast-1.4.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0"}, {file = "typed_ast-1.4.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3"}, {file = "typed_ast-1.4.3-cp39-cp39-win32.whl", hash = "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808"}, {file = "typed_ast-1.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c"}, {file = "typed_ast-1.4.3.tar.gz", hash = "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65"}, ] typing-extensions = [ {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, ] wrapt = [ {file = "wrapt-1.13.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a"}, {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489"}, {file = "wrapt-1.13.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909"}, {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229"}, {file = "wrapt-1.13.3-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af"}, {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de"}, {file = "wrapt-1.13.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb"}, {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80"}, {file = "wrapt-1.13.3-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca"}, {file = "wrapt-1.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44"}, {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056"}, {file = "wrapt-1.13.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785"}, {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096"}, {file = "wrapt-1.13.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33"}, {file = "wrapt-1.13.3-cp310-cp310-win32.whl", hash = "sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f"}, {file = "wrapt-1.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e"}, {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d"}, {file = "wrapt-1.13.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179"}, {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3"}, {file = "wrapt-1.13.3-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755"}, {file = "wrapt-1.13.3-cp35-cp35m-win32.whl", hash = "sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851"}, {file = "wrapt-1.13.3-cp35-cp35m-win_amd64.whl", hash = "sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13"}, {file = "wrapt-1.13.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918"}, {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade"}, {file = "wrapt-1.13.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc"}, {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf"}, {file = "wrapt-1.13.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125"}, {file = "wrapt-1.13.3-cp36-cp36m-win32.whl", hash = "sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36"}, {file = "wrapt-1.13.3-cp36-cp36m-win_amd64.whl", hash = "sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10"}, {file = "wrapt-1.13.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068"}, {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709"}, {file = "wrapt-1.13.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df"}, {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2"}, {file = "wrapt-1.13.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b"}, {file = "wrapt-1.13.3-cp37-cp37m-win32.whl", hash = "sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829"}, {file = "wrapt-1.13.3-cp37-cp37m-win_amd64.whl", hash = "sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"}, {file = "wrapt-1.13.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9"}, {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554"}, {file = "wrapt-1.13.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c"}, {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b"}, {file = "wrapt-1.13.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce"}, {file = "wrapt-1.13.3-cp38-cp38-win32.whl", hash = "sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79"}, {file = "wrapt-1.13.3-cp38-cp38-win_amd64.whl", hash = "sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb"}, {file = "wrapt-1.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb"}, {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32"}, {file = "wrapt-1.13.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7"}, {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e"}, {file = "wrapt-1.13.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640"}, {file = "wrapt-1.13.3-cp39-cp39-win32.whl", hash = "sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374"}, {file = "wrapt-1.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb"}, {file = "wrapt-1.13.3.tar.gz", hash = "sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185"}, ] zipp = [ {file = "zipp-3.6.0-py3-none-any.whl", hash = "sha256:9fe5ea21568a0a70e50f273397638d39b03353731e6cbbb3fd8502a33fec40bc"}, {file = "zipp-3.6.0.tar.gz", hash = "sha256:71c644c5369f4a6e07636f0aa966270449561fcea2e3d6747b8d23efaa9d7832"}, ] bpytop-1.0.68/pyproject.toml000066400000000000000000000012011416067541000160350ustar00rootroot00000000000000[tool.poetry] name = "bpytop" version = "1.0.68" description = "Resource monitor that shows usage and stats for processor, memory, disks, network and processes." readme = "README.md" authors = ["Aristocratos "] homepage = "https://github.com/aristocratos/bpytop" license = "Apache-2.0" include = ["bpytop-themes/*.theme"] [tool.poetry.dependencies] python = "^3.7" psutil = "^5.7.0" [tool.poetry.dev-dependencies] pytest = "^6.1.1" mypy = "^0.790" pylint = "^2.6.0" more-itertools = "^8.7.0" [tool.poetry.scripts] bpytop = "bpytop:main" [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" bpytop-1.0.68/tests/000077500000000000000000000000001416067541000142715ustar00rootroot00000000000000bpytop-1.0.68/tests/__init__.py000066400000000000000000000000001416067541000163700ustar00rootroot00000000000000bpytop-1.0.68/tests/test_classes.py000066400000000000000000000106751416067541000173500ustar00rootroot00000000000000import bpytop, pytest from bpytop import Box, SubBox, CpuBox, MemBox, NetBox, ProcBox, Term, Draw from bpytop import Graph, Fx, Meter, Color, Banner from bpytop import Collector, CpuCollector, MemCollector, NetCollector, ProcCollector bpytop.Term.width, bpytop.Term.height = 80, 25 def test_Fx_uncolor(): assert Fx.uncolor("\x1b[38;2;102;238;142mTEST\x1b[48;2;0;0;0m") == "TEST" def test_Color(): assert Color.fg("#00ff00") == "\x1b[38;2;0;255;0m" assert Color.bg("#cc00cc") == "\x1b[48;2;204;0;204m" assert Color.fg(255, 255, 255) == "\x1b[38;2;255;255;255m" def test_Theme(): bpytop.THEME = bpytop.Theme("Default") assert str(bpytop.THEME.main_fg) == "\x1b[38;2;204;204;204m" assert list(bpytop.THEME.main_fg) == [204, 204, 204] assert len(bpytop.THEME.gradient["cpu"]) == 101 def test_Box_calc_sizes(): Box.calc_sizes() assert CpuBox.width == MemBox.width + ProcBox.width == NetBox.width + ProcBox.width == 80 assert CpuBox.height + ProcBox.height == CpuBox.height + MemBox.height + NetBox.height == 25 def test_Graph(): test_graph = Graph(width=20, height=10, color=None, data=[x for x in range(20)], invert=False, max_value=0, offset=0, color_max_value=None) assert len(str(test_graph)) > 1 assert str(test_graph).endswith("⣀⣤⣴⣾⣿⣿⣿⣿⣿") assert test_graph(5).endswith("⣧") def test_Meter(): test_meter = Meter(value=100, width=20, gradient_name="cpu", invert=False) assert Fx.uncolor(str(test_meter)) == "■■■■■■■■■■■■■■■■■■■■" def test_Banner(): assert len(Banner.draw(line=1, col=1, center=False, now=False)) == 2477 def test_CpuCollector_collect(): bpytop.CONFIG.check_temp = False CpuCollector._collect() assert len(CpuCollector.cpu_usage) == bpytop.THREADS + 1 assert isinstance(CpuCollector.cpu_usage[0][0], int) assert isinstance(CpuCollector.load_avg, list) assert isinstance(CpuCollector.uptime, str) def test_CpuCollector_get_sensors(): bpytop.CONFIG.check_temp = True bpytop.CONFIG.cpu_sensor = "Auto" CpuCollector.get_sensors() if CpuCollector.got_sensors: assert CpuCollector.sensor_method != "" else: assert CpuCollector.sensor_method == "" def test_CpuCollector_collect_temps(): if not CpuCollector.got_sensors: pytest.skip("Not testing temperature collection if no sensors was detected!") CpuCollector._collect_temps() assert len(CpuCollector.cpu_temp) == bpytop.THREADS + 1 for temp_instance in CpuCollector.cpu_temp: assert temp_instance assert isinstance(temp_instance[0], int) assert isinstance(CpuCollector.cpu_temp_high, int) assert isinstance(CpuCollector.cpu_temp_crit, int) def test_MemCollector_collect(): MemBox.width = 20 bpytop.CONFIG.show_swap = True bpytop.CONFIG.show_disks = True bpytop.CONFIG.disks_filter = "" bpytop.CONFIG.swap_disk = True MemCollector._collect() assert isinstance(MemCollector.string["total"], str) and MemCollector.string["total"] != "" assert isinstance(MemCollector.values["used"], int) assert isinstance(MemCollector.percent["free"], int) if MemBox.swap_on: assert len(MemCollector.disks) > 1 assert "__swap" in MemCollector.disks else: assert len(MemCollector.disks) > 0 def test_NetCollector_get_nics(): NetCollector._get_nics() if NetCollector.nic == "": pytest.skip("No nic found, skipping tests!") assert NetCollector.nic in NetCollector.nics def test_NetCollector_collect(): if NetCollector.nic == "": pytest.skip("No nic found, skipping tests!") NetBox.width = 20 NetCollector._collect() assert isinstance(NetCollector.strings[NetCollector.nic]["download"]["total"], str) assert isinstance(NetCollector.stats[NetCollector.nic]["upload"]["total"], int) def test_ProcCollector_collect(): bpytop.CONFIG.proc_tree = False bpytop.CONFIG.proc_mem_bytes = True bpytop.Box.boxes = ["proc"] ProcCollector._collect() assert len(ProcCollector.processes) > 0 bpytop.CONFIG.proc_tree = True ProcCollector.processes = {} ProcCollector._collect() assert len(ProcCollector.processes) > 0 def test_CpuBox_draw(): Box.calc_sizes() assert len(CpuBox._draw_bg()) > 1 CpuBox._draw_fg() assert "cpu" in Draw.strings def test_MemBox_draw(): bpytop.CONFIG.show_disks = True Box.calc_sizes() assert len(MemBox._draw_bg()) > 1 MemBox._draw_fg() assert "mem" in Draw.strings def test_NetBox_draw(): Box.calc_sizes() assert len(NetBox._draw_bg()) > 1 NetBox._draw_fg() assert "net" in Draw.strings def test_ProcBox_draw(): Box.calc_sizes() assert len(ProcBox._draw_bg()) > 1 ProcBox._draw_fg() assert "proc" in Draw.strings bpytop-1.0.68/tests/test_functions.py000066400000000000000000000024111416067541000177100ustar00rootroot00000000000000from more_itertools import divide import bpytop from bpytop import (CORES, SYSTEM, THREADS, Fx, create_box, floating_humanizer, get_cpu_core_mapping, get_cpu_name, units_to_bytes) def test_get_cpu_name(): assert isinstance(get_cpu_name(), str) def test_get_cpu_core_mapping(): cpu_core_mapping = get_cpu_core_mapping() assert isinstance(cpu_core_mapping, list) # Assert cpu submappings are sequential for submapping in divide(THREADS//CORES, cpu_core_mapping): submapping = list(submapping) for a, b in zip(submapping[:-1], submapping[1:]): assert b - a == 1 def test_create_box(): assert len(create_box(x=1, y=1, width=10, height=10, title="", title2="", line_color=None, title_color=None, fill=True, box=None)) > 1 def test_floating_humanizer(): assert floating_humanizer(100) == "100 Byte" assert floating_humanizer(100<<10) == "100 KiB" assert floating_humanizer(100<<20, bit=True) == "800 Mib" assert floating_humanizer(100<<20, start=1) == "100 GiB" assert floating_humanizer(100<<40, short=True) == "100T" assert floating_humanizer(100<<50, per_second=True) == "100 PiB/s" def test_units_to_bytes(): assert units_to_bytes("10kbits") == 1280 assert units_to_bytes("100Mbytes") == 104857600 assert units_to_bytes("1gbit") == 134217728 bpytop-1.0.68/tests/test_title.py000066400000000000000000000010311416067541000170160ustar00rootroot00000000000000from bpytop import Term import os from unittest import mock def test_empty(): assert Term.title() == "\033]0;\a" def test_nonempty(): assert Term.title("BpyTOP") == "\033]0;BpyTOP\a" def test_empty_with_environ(): with mock.patch.dict("os.environ", {"TERMINAL_TITLE": "hello"}, clear=True): assert Term.title() == "\033]0;hello\a" def test_nonempty_with_environ(): with mock.patch.dict("os.environ", {"TERMINAL_TITLE": "hello"}, clear=True): assert Term.title("BpyTOP") == "\033]0;hello BpyTOP\a" bpytop-1.0.68/themes/000077500000000000000000000000001416067541000144145ustar00rootroot00000000000000bpytop-1.0.68/themes/adapta.theme000066400000000000000000000042471416067541000167010ustar00rootroot00000000000000#Bashtop Adapta theme #by olokelo # Colors should be in 6 or 2 character hexadecimal or single spaced rgb decimal: "#RRGGBB", "#BW" or "0-255 0-255 0-255" # example for white: "#ffffff", "#ff" or "255 255 255". # All graphs and meters can be gradients # For single color graphs leave "mid" and "end" variable empty. # Use "start" and "end" variables for two color gradient # Use "start", "mid" and "end" for three color gradient # Main background, empty for terminal default, need to be empty if you want transparent background theme[main_bg]="" # Main text color theme[main_fg]="#cfd8dc" # Title color for boxes theme[title]="#ff" # Higlight color for keyboard shortcuts theme[hi_fg]="#90" # Background color of selected item in processes box theme[selected_bg]="#bb0040" # Foreground color of selected item in processes box theme[selected_fg]="#ff" # Color of inactive/disabled text theme[inactive_fg]="#40" # Misc colors for processes box including mini cpu graphs, details memory graph and details status text theme[proc_misc]="#55bcea" # Cpu box outline color theme[cpu_box]="#00bcd4" # Memory/disks box outline color theme[mem_box]="#00bcd4" # Net up/down box outline color theme[net_box]="#00bcd4" # Processes box outline color theme[proc_box]="#00bcd4" # Box divider line and small boxes line color theme[div_line]="#50" # Temperature graph colors theme[temp_start]="#00bcd4" theme[temp_mid]="#d4d400" theme[temp_end]="#ff0040" # CPU graph colors theme[cpu_start]="#00bcd4" theme[cpu_mid]="#d4d400" theme[cpu_end]="#ff0040" # Mem/Disk free meter theme[free_start]="#00bcd4" theme[free_mid]="#1090a0" theme[free_end]="#206f79" # Mem/Disk cached meter theme[cached_start]="#991199" theme[cached_mid]="#770a55" theme[cached_end]="#550055" # Mem/Disk available meter theme[available_start]="#00b0ff" theme[available_mid]="#1099cc" theme[available_end]="#2070aa" # Mem/Disk used meter theme[used_start]="#ff0040" theme[used_mid]="#ff2060" theme[used_end]="#ff4080" # Download graph colors theme[download_start]="#00bcd4" theme[download_mid]="#991199" theme[download_end]="#ff0040" # Upload graph colors theme[upload_start]="#00bcd4" theme[upload_mid]="#991199" theme[upload_end]="#ff0040" bpytop-1.0.68/themes/default_black.theme000066400000000000000000000050301416067541000202160ustar00rootroot00000000000000#Bashtop theme with default colors and black background #by aristocratos # Colors should be in 6 or 2 character hexadecimal or single spaced rgb decimal: "#RRGGBB", "#BW" or "0-255 0-255 0-255" # example for white: "#FFFFFF", "#ff" or "255 255 255". # All graphs and meters can be gradients # For single color graphs leave "mid" and "end" variable empty. # Use "start" and "end" variables for two color gradient # Use "start", "mid" and "end" for three color gradient # Main background, empty for terminal default, need to be empty if you want transparent background theme[main_bg]="#00" # Main text color theme[main_fg]="#cc" # Title color for boxes theme[title]="#ee" # Highlight color for keyboard shortcuts theme[hi_fg]="#90" # Background color of selected item in processes box theme[selected_bg]="#7e2626" # Foreground color of selected item in processes box theme[selected_fg]="#ee" # Color of inactive/disabled text theme[inactive_fg]="#40" # Color of text appearing on top of graphs, i.e uptime and current network graph scaling theme[graph_text]="#60" # Background color of the percentage meters theme[meter_bg]="#40" # Misc colors for processes box including mini cpu graphs, details memory graph and details status text theme[proc_misc]="#0de756" # Cpu box outline color theme[cpu_box]="#3d7b46" # Memory/disks box outline color theme[mem_box]="#8a882e" # Net up/down box outline color theme[net_box]="#423ba5" # Processes box outline color theme[proc_box]="#923535" # Box divider line and small boxes line color theme[div_line]="#30" # Temperature graph colors theme[temp_start]="#4897d4" theme[temp_mid]="#5474e8" theme[temp_end]="#ff40b6" # CPU graph colors theme[cpu_start]="#50f095" theme[cpu_mid]="#f2e266" theme[cpu_end]="#fa1e1e" # Mem/Disk free meter theme[free_start]="#223014" theme[free_mid]="#b5e685" theme[free_end]="#dcff85" # Mem/Disk cached meter theme[cached_start]="#0b1a29" theme[cached_mid]="#74e6fc" theme[cached_end]="#26c5ff" # Mem/Disk available meter theme[available_start]="#292107" theme[available_mid]="#ffd77a" theme[available_end]="#ffb814" # Mem/Disk used meter theme[used_start]="#3b1f1c" theme[used_mid]="#d9626d" theme[used_end]="#ff4769" # Download graph colors theme[download_start]="#231a63" theme[download_mid]="#4f43a3" theme[download_end]="#b0a9de" # Upload graph colors theme[upload_start]="#510554" theme[upload_mid]="#7d4180" theme[upload_end]="#dcafde" # Process box color gradient for threads, mem and cpu usage theme[process_start]="#80d0a3" theme[process_mid]="#dcd179" theme[process_end]="#d45454"bpytop-1.0.68/themes/dracula.theme000066400000000000000000000041461416067541000170600ustar00rootroot00000000000000# Main background, empty for terminal default, need to be empty if you want transparent background theme[main_bg]="#282a36" # Main text color theme[main_fg]="#f8f8f2" # Title color for boxes theme[title]="#f8f8f2" # Highlight color for keyboard shortcuts theme[hi_fg]="#6272a4" # Background color of selected item in processes box theme[selected_bg]="#ff79c6" # Foreground color of selected item in processes box theme[selected_fg]="#f8f8f2" # Color of inactive/disabled text theme[inactive_fg]="#44475a" # Color of text appearing on top of graphs, i.e uptime and current network graph scaling theme[graph_text]="#f8f8f2" # Background color of the percentage meters theme[meter_bg]="#44475a" # Misc colors for processes box including mini cpu graphs, details memory graph and details status text theme[proc_misc]="#bd93f9" # Cpu box outline color theme[cpu_box]="#bd93f9" # Memory/disks box outline color theme[mem_box]="#50fa7b" # Net up/down box outline color theme[net_box]="#ff5555" # Processes box outline color theme[proc_box]="#8be9fd" # Box divider line and small boxes line color theme[div_line]="#44475a" # Temperature graph colors theme[temp_start]="#bd93f9" theme[temp_mid]="#ff79c6" theme[temp_end]="#ff33a8" # CPU graph colors theme[cpu_start]="#bd93f9" theme[cpu_mid]="#8be9fd" theme[cpu_end]="#50fa7b" # Mem/Disk free meter theme[free_start]="#ffa6d9" theme[free_mid]="#ff79c6" theme[free_end]="#ff33a8" # Mem/Disk cached meter theme[cached_start]="#b1f0fd" theme[cached_mid]="#8be9fd" theme[cached_end]="#26d7fd" # Mem/Disk available meter theme[available_start]="#ffd4a6" theme[available_mid]="#ffb86c" theme[available_end]="#ff9c33" # Mem/Disk used meter theme[used_start]="#96faaf" theme[used_mid]="#50fa7b" theme[used_end]="#0dfa49" # Download graph colors theme[download_start]="#bd93f9" theme[download_mid]="#50fa7b" theme[download_end]="#8be9fd" # Upload graph colors theme[upload_start]="#8c42ab" theme[upload_mid]="#ff79c6" theme[upload_end]="#ff33a8" # Process box color gradient for threads, mem and cpu usage theme[process_start]="#50fa7b" theme[process_mid]="#59b690" theme[process_end]="#6272a4" bpytop-1.0.68/themes/dusklight.theme000066400000000000000000000045431416067541000174440ustar00rootroot00000000000000#Bpytop theme comprised of blues, oranges, cyan, and yellow. #by Drazil100 # Colors should be in 6 or 2 character hexadecimal or single spaced rgb decimal: "#RRGGBB", "#BW" or "0-255 0-255 0-255" # example for white: "#FFFFFF", "#ff" or "255 255 255". # All graphs and meters can be gradients # For single color graphs leave "mid" and "end" variable empty. # Use "start" and "end" variables for two color gradient # Use "start", "mid" and "end" for three color gradient # Main background, empty for terminal default, need to be empty if you want transparent background theme[main_bg]="#04142E" # Main text color theme[main_fg]="#99DFFF" # Title color for boxes theme[title]="#99FFFF" # Higlight color for keyboard shortcuts theme[hi_fg]="#FF7F00" # Background color of selected item in processes box theme[selected_bg]="#722B01" # Foreground color of selected item in processes box theme[selected_fg]="#99FFFF" # Color of inactive/disabled text theme[inactive_fg]="#052E51" # Color of text appearing on top of graphs, i.e uptime and current network graph scaling theme[graph_text]="#79A1B4" # Background color of the percentage meters theme[meter_bg]="#052E51" # Misc colors for processes box including mini cpu graphs, details memory graph and details status text theme[proc_misc]="#B46718" # Cpu box outline color theme[cpu_box]="#00FFFF" # Memory/disks box outline color theme[mem_box]="#00FFFF" # Net up/down box outline color theme[net_box]="#00FFFF" # Processes box outline color theme[proc_box]="#00FFFF" # Box divider line and small boxes line color theme[div_line]="#A55800" # Temperature graph colors theme[temp_start]="#00ADFF" theme[temp_mid]="#00FFFF" theme[temp_end]="#FFF86B" # CPU graph colors theme[cpu_start]="#00D4FF" theme[cpu_mid]="#FFF86B" theme[cpu_end]="#FF7F00" # Mem/Disk free meter theme[free_start]="#0187CB" theme[free_mid]="" theme[free_end]="" # Mem/Disk cached meter theme[cached_start]="#B4BB63" theme[cached_mid]="" theme[cached_end]="" # Mem/Disk available meter theme[available_start]="#01C0CB" theme[available_mid]="" theme[available_end]="" # Mem/Disk used meter theme[used_start]="#B46718" theme[used_mid]="" theme[used_end]="" # Download graph colors theme[download_start]="#009EFF" theme[download_mid]="" theme[download_end]="#00FFFF" # Upload graph colors theme[upload_start]="#FF7F00" theme[upload_mid]="" theme[upload_end]="#FFF86B" bpytop-1.0.68/themes/flat-remix-light.theme000066400000000000000000000043471416067541000206250ustar00rootroot00000000000000#Bashtop theme with flat-remix colors #by Daniel Ruiz de Alegría # Colors should be in 6 or 2 character hexadecimal or single spaced rgb decimal: "#RRGGBB", "#BW" or "0-255 0-255 0-255" # example for white: "#ffffff", "#ff" or "255 255 255". # All graphs and meters can be gradients # For single color graphs leave "mid" and "end" variable empty. # Use "start" and "end" variables for two color gradient # Use "start", "mid" and "end" for three color gradient # Main background, empty for terminal default, need to be empty if you want transparent background theme[main_bg]="#e4e4e7" # Main text color theme[main_fg]="#737680" # Title color for boxes theme[title]="#272a34" # Higlight color for keyboard shortcuts theme[hi_fg]="#90" # Background color of selected item in processes box theme[selected_bg]="#b8174c" # Foreground color of selected item in processes box theme[selected_fg]="#ff" # Color of inactive/disabled text theme[inactive_fg]="#40" # Misc colors for processes box including mini cpu graphs, details memory graph and details status text theme[proc_misc]="#367bf0" # Cpu box outline color theme[cpu_box]="#367bf0" # Memory/disks box outline color theme[mem_box]="#19a187" # Net up/down box outline color theme[net_box]="#fd3535" # Processes box outline color theme[proc_box]="#4aaee6" # Box divider line and small boxes line color theme[div_line]="#50" # Temperature graph colors theme[temp_start]="#367bf0" theme[temp_mid]="#b8174c" theme[temp_end]="#d41919" # CPU graph colors theme[cpu_start]="#367bf0" theme[cpu_mid]="#4aaee6" theme[cpu_end]="#54bd8e" # Mem/Disk free meter theme[free_start]="#811035" theme[free_mid]="#b8174c" theme[free_end]="#d41919" # Mem/Disk cached meter theme[cached_start]="#2656a8" theme[cached_mid]="#4aaee6" theme[cached_end]="#23bac2" # Mem/Disk available meter theme[available_start]="#fea44c" theme[available_mid]="#fd7d00" theme[available_end]="#fe7171" # Mem/Disk used meter theme[used_start]="#12715f" theme[used_mid]="#19a187" theme[used_end]="#23bac2" # Download graph colors theme[download_start]="#367bf0" theme[download_mid]="#19a187" theme[download_end]="#4aaee6" # Upload graph colors theme[upload_start]="#8c42ab" theme[upload_mid]="#b8174c" theme[upload_end]="#d41919" bpytop-1.0.68/themes/flat-remix.theme000066400000000000000000000043341416067541000175140ustar00rootroot00000000000000#Bashtop theme with flat-remix colors #by Daniel Ruiz de Alegría # Colors should be in 6 or 2 character hexadecimal or single spaced rgb decimal: "#RRGGBB", "#BW" or "0-255 0-255 0-255" # example for white: "#ffffff", "#ff" or "255 255 255". # All graphs and meters can be gradients # For single color graphs leave "mid" and "end" variable empty. # Use "start" and "end" variables for two color gradient # Use "start", "mid" and "end" for three color gradient # Main background, empty for terminal default, need to be empty if you want transparent background theme[main_bg]="" # Main text color theme[main_fg]="#E6E6E6" # Title color for boxes theme[title]="#ff" # Higlight color for keyboard shortcuts theme[hi_fg]="#90" # Background color of selected item in processes box theme[selected_bg]="#b8174c" # Foreground color of selected item in processes box theme[selected_fg]="#ff" # Color of inactive/disabled text theme[inactive_fg]="#40" # Misc colors for processes box including mini cpu graphs, details memory graph and details status text theme[proc_misc]="#367bf0" # Cpu box outline color theme[cpu_box]="#367bf0" # Memory/disks box outline color theme[mem_box]="#19a187" # Net up/down box outline color theme[net_box]="#fd3535" # Processes box outline color theme[proc_box]="#4aaee6" # Box divider line and small boxes line color theme[div_line]="#50" # Temperature graph colors theme[temp_start]="#367bf0" theme[temp_mid]="#b8174c" theme[temp_end]="#d41919" # CPU graph colors theme[cpu_start]="#367bf0" theme[cpu_mid]="#4aaee6" theme[cpu_end]="#54bd8e" # Mem/Disk free meter theme[free_start]="#811035" theme[free_mid]="#b8174c" theme[free_end]="#d41919" # Mem/Disk cached meter theme[cached_start]="#2656a8" theme[cached_mid]="#4aaee6" theme[cached_end]="#23bac2" # Mem/Disk available meter theme[available_start]="#fea44c" theme[available_mid]="#fd7d00" theme[available_end]="#fe7171" # Mem/Disk used meter theme[used_start]="#12715f" theme[used_mid]="#19a187" theme[used_end]="#23bac2" # Download graph colors theme[download_start]="#367bf0" theme[download_mid]="#19a187" theme[download_end]="#4aaee6" # Upload graph colors theme[upload_start]="#8c42ab" theme[upload_mid]="#b8174c" theme[upload_end]="#d41919" bpytop-1.0.68/themes/greyscale.theme000066400000000000000000000040361416067541000174210ustar00rootroot00000000000000#Bashtop grayscale theme #by aristocratos # Colors should be in 6 or 2 character hexadecimal or single spaced rgb decimal: "#RRGGBB", "#BW" or "0-255 0-255 0-255" # example for white: "#FFFFFF", "#ff" or "255 255 255". # All graphs and meters can be gradients # For single color graphs leave "mid" and "end" variable empty. # Use "start" and "end" variables for two color gradient # Use "start", "mid" and "end" for three color gradient # Main background, empty for terminal default, need to be empty if you want transparent background theme[main_bg]="#00" # Main text color theme[main_fg]="#bb" # Title color for boxes theme[title]="#cc" # Higlight color for keyboard shortcuts theme[hi_fg]="#90" # Background color of selected item in processes box theme[selected_bg]="#ff" # Foreground color of selected item in processes box theme[selected_fg]="#00" # Color of inactive/disabled text theme[inactive_fg]="#30" # Misc colors for processes box including mini cpu graphs, details memory graph and details status text theme[proc_misc]="#90" # Cpu box outline color theme[cpu_box]="#90" # Memory/disks box outline color theme[mem_box]="#90" # Net up/down box outline color theme[net_box]="#90" # Processes box outline color theme[proc_box]="#90" # Box divider line and small boxes line color theme[div_line]="#30" # Temperature graph colors theme[temp_start]="#50" theme[temp_mid]="" theme[temp_end]="#ff" # CPU graph colors theme[cpu_start]="#50" theme[cpu_mid]="" theme[cpu_end]="#ff" # Mem/Disk free meter theme[free_start]="#50" theme[free_mid]="" theme[free_end]="#ff" # Mem/Disk cached meter theme[cached_start]="#50" theme[cached_mid]="" theme[cached_end]="#ff" # Mem/Disk available meter theme[available_start]="#50" theme[available_mid]="" theme[available_end]="#ff" # Mem/Disk used meter theme[used_start]="#50" theme[used_mid]="" theme[used_end]="#ff" # Download graph colors theme[download_start]="#30" theme[download_mid]="" theme[download_end]="#ff" # Upload graph colors theme[upload_start]="#30" theme[upload_mid]="" theme[upload_end]="#ff"bpytop-1.0.68/themes/gruvbox_dark.theme000066400000000000000000000044451416067541000201440ustar00rootroot00000000000000#Bashtop gruvbox (https://github.com/morhetz/gruvbox) theme #by BachoSeven # Colors should be in 6 or 2 character hexadecimal or single spaced rgb decimal: "#RRGGBB", "#BW" or "0-255 0-255 0-255" # example for white: "#FFFFFF", "#ff" or "255 255 255". # All graphs and meters can be gradients # For single color graphs leave "mid" and "end" variable empty. # Use "start" and "end" variables for two color gradient # Use "start", "mid" and "end" for three color gradient # Main background, empty for terminal default, need to be empty if you want transparent background theme[main_bg]="#1d2021" # Main text color theme[main_fg]="#a89984" # Title color for boxes theme[title]="#ebdbb2" # Higlight color for keyboard shortcuts theme[hi_fg]="#d79921" # Background color of selected items theme[selected_bg]="#282828" # Foreground color of selected items theme[selected_fg]="#fabd2f" # Color of inactive/disabled text theme[inactive_fg]="#282828" # Color of text appearing on top of graphs, i.e uptime and current network graph scaling theme[graph_text]="#585858" # Misc colors for processes box including mini cpu graphs, details memory graph and details status text theme[proc_misc]="#98971a" # Cpu box outline color theme[cpu_box]="#a89984" # Memory/disks box outline color theme[mem_box]="#a89984" # Net up/down box outline color theme[net_box]="#a89984" # Processes box outline color theme[proc_box]="#a89984" # Box divider line and small boxes line color theme[div_line]="#a89984" # Temperature graph colors theme[temp_start]="#458588" theme[temp_mid]="#d3869b" theme[temp_end]="#fb4394" # CPU graph colors theme[cpu_start]="#b8bb26" theme[cpu_mid]="#d79921" theme[cpu_end]="#fb4934" # Mem/Disk free meter theme[free_start]="#4e5900" theme[free_mid]="" theme[free_end]="#98971a" # Mem/Disk cached meter theme[cached_start]="#458588" theme[cached_mid]="" theme[cached_end]="#83a598" # Mem/Disk available meter theme[available_start]="#d79921" theme[available_mid]="" theme[available_end]="#fabd2f" # Mem/Disk used meter theme[used_start]="#cc241d" theme[used_mid]="" theme[used_end]="#fb4934" # Download graph colors theme[download_start]="#3d4070" theme[download_mid]="#6c71c4" theme[download_end]="#a3a8f7" # Upload graph colors theme[upload_start]="#701c45" theme[upload_mid]="#b16286" theme[upload_end]="#d3869b" bpytop-1.0.68/themes/gruvbox_dark_v2.theme000066400000000000000000000050711416067541000205470ustar00rootroot00000000000000# Bashtop gruvbox (https://github.com/morhetz/gruvbox) theme # First version created By BachoSeven # Adjustments to proper colors by Pietryszak (https://github.com/pietryszak/) # Colors should be in 6 or 2 character hexadecimal or single spaced rgb decimal: "#RRGGBB", "#BW" or "0-255 0-255 0-255" # example for white: "#FFFFFF", "#ff" or "255 255 255". # All graphs and meters can be gradients # For single color graphs leave "mid" and "end" variable empty. # Use "start" and "end" variables for two color gradient # Use "start", "mid" and "end" for three color gradient # Main background, empty for terminal default, need to be empty if you want transparent background theme[main_bg]="#282828" # Main text color theme[main_fg]="#EBDBB2" # Title color for boxes theme[title]="#EBDBB2" # Highlight color for keyboard shortcuts theme[hi_fg]="#CC241D" # Background color of selected items theme[selected_bg]="#32302F" # Foreground color of selected items theme[selected_fg]="#D3869B" # Color of inactive/disabled text theme[inactive_fg]="#3C3836" # Color of text appearing on top of graphs, i.e uptime and current network graph scaling theme[graph_text]="#A89984" # Misc colors for processes box including mini cpu graphs, details memory graph and details status text theme[proc_misc]="#98971A" # Cpu box outline color theme[cpu_box]="#A89984" # Memory/disks box outline color theme[mem_box]="#A89984" # Net up/down box outline color theme[net_box]="#A89984" # Processes box outline color theme[proc_box]="#A89984" # Box divider line and small boxes line color theme[div_line]="#A89984" # Temperature graph colors theme[temp_start]="#98971A" theme[temp_mid]="" theme[temp_end]="#CC241D" # CPU graph colors theme[cpu_start]="#8EC07C" theme[cpu_mid]="#D79921" theme[cpu_end]="#CC241D" # Mem/Disk free meter theme[free_start]="#CC241D" theme[free_mid]="#D79921" theme[free_end]="#8EC07C" # Mem/Disk cached meter theme[cached_start]="#458588" theme[cached_mid]="#83A598" theme[cached_end]="#8EC07C" # Mem/Disk available meter theme[available_start]="#CC241D" theme[available_mid]="#D65D0E" theme[available_end]="#FABD2F" # Mem/Disk used meter theme[used_start]="#8EC07C" theme[used_mid]="#D65D0E" theme[used_end]="#CC241D" # Download graph colors theme[download_start]="#98971A" theme[download_mid]="#689d6A" theme[download_end]="#B8BB26" # Upload graph colors theme[upload_start]="#CC241D" theme[upload_mid]="#D65d0E" theme[upload_end]="#FABF2F" # Process box color gradient for threads, mem and cpu usage theme[process_start]="#8EC07C" theme[process_mid]="#FE8019" theme[process_end]="#CC241D" bpytop-1.0.68/themes/kyli0x.theme000066400000000000000000000042371416067541000166660ustar00rootroot00000000000000#Bashtop Kyli0x Theme #by Kyli0x # Main background, empty for terminal default, need to be empty if you want transparent background theme[main_bg]="#23252e" # Main text color theme[main_fg]="#f8f8f2" # Title color for boxes theme[title]="#f8f8f2" # Highlight color for keyboard shortcuts theme[hi_fg]="#21d6c9" # Background color of selected item in processes box theme[selected_bg]="#1aaba0" # Foreground color of selected item in processes box theme[selected_fg]="#f8f8f2" # Color of inactive/disabled text theme[inactive_fg]="#497e7a" # Color of text appearing on top of graphs, i.e uptime and current network graph scaling theme[graph_text]="#21d6c9" # Background color of the percentage meters theme[meter_bg]="#80638e" # Misc colors for processes box including mini cpu graphs, details memory graph and details status text theme[proc_misc]="#21d6c9" # Cpu box outline color theme[cpu_box]="#d486d4" # Memory/disks box outline color theme[mem_box]="#d486d4" # Net up/down box outline color theme[net_box]="#d486d4" # Processes box outline color theme[proc_box]="#d486d4" # Box divider line and small boxes line color theme[div_line]="#80638e" # Temperature graph colors theme[temp_start]="#21d6c9" theme[temp_mid]="#1aaba0" theme[temp_end]="#497e7a" # CPU graph colors theme[cpu_start]="#21d6c9" theme[cpu_mid]="#1aaba0" theme[cpu_end]="#497e7a" # Mem/Disk free meter theme[free_start]="#21d6c9" theme[free_mid]="#1aaba0" theme[free_end]="#497e7a" # Mem/Disk cached meter theme[cached_start]="#21d6c9" theme[cached_mid]="#1aaba0" theme[cached_end]="#497e7a" # Mem/Disk available meter theme[available_start]="#21d6c9" theme[available_mid]="#1aaba0" theme[available_end]="#497e7a" # Mem/Disk used meter theme[used_start]="#21d6c9" theme[used_mid]="#1aaba0" theme[used_end]="#497e7a" # Download graph colors theme[download_start]="#21d6c9" theme[download_mid]="#1aaba0" theme[download_end]="#497e7a" # Upload graph colors theme[upload_start]="#ec95ec" theme[upload_mid]="#1aaba0" theme[upload_end]="#497e7a" # Process box color gradient for threads, mem and cpu usage theme[process_start]="#21d6c9" theme[process_mid]="#1aaba0" theme[process_end]="#d486d4" bpytop-1.0.68/themes/matcha-dark-sea.theme000066400000000000000000000045121416067541000203640ustar00rootroot00000000000000#Bashtop matcha-dark-sea theme #by TheCynicalTeam # Colors should be in 6 or 2 character hexadecimal or single spaced rgb decimal: "#RRGGBB", "#BW" or "0-255 0-255 0-255" # example for white: "#FFFFFF", "#ff" or "255 255 255". # All graphs and meters can be gradients # For single color graphs leave "mid" and "end" variable empty. # Use "start" and "end" variables for two color gradient # Use "start", "mid" and "end" for three color gradient # Main background, empty for terminal default, need to be empty if you want transparent background theme[main_bg]="" # Main text color theme[main_fg]="#F8F8F2" # Title color for boxes theme[title]="#F8F8F2" # Higlight color for keyboard shortcuts theme[hi_fg]="#2eb398" # Background color of selected item in processes box theme[selected_bg]="#0d493d" # Foreground color of selected item in processes box theme[selected_fg]="#F8F8F2" # Color of inactive/disabled text theme[inactive_fg]="#595647" # Color of text appearing on top of graphs, i.e uptime and current network graph scaling theme[graph_text]="#797667" # Misc colors for processes box including mini cpu graphs, details memory graph and details status text theme[proc_misc]="#33b165" # Cpu box outline color theme[cpu_box]="#75715E" # Memory/disks box outline color theme[mem_box]="#75715E" # Net up/down box outline color theme[net_box]="#75715E" # Processes box outline color theme[proc_box]="#75715E" # Box divider line and small boxes line color theme[div_line]="#595647" # Temperature graph colors theme[temp_start]="#7976B7" theme[temp_mid]="#D8B8B2" theme[temp_end]="#33b165" # CPU graph colors theme[cpu_start]="#33b165" theme[cpu_mid]="#F8F8F2" #2eb398" theme[cpu_end]="#2eb398" # Mem/Disk free meter theme[free_start]="#75715E" theme[free_mid]="#a9c474" theme[free_end]="#e2f5bc" # Mem/Disk cached meter theme[cached_start]="#75715E" theme[cached_mid]="#66D9EF" theme[cached_end]="#aae7f2" # Mem/Disk available meter theme[available_start]="#75715E" theme[available_mid]="#E6DB74" theme[available_end]="#f2ecb6" # Mem/Disk used meter theme[used_start]="#75715E" theme[used_mid]="#2eb398" theme[used_end]="#33b165" # Download graph colors theme[download_start]="#2d2042" theme[download_mid]="#2eb398" theme[download_end]="#33b165" # Upload graph colors theme[upload_start]="#0d493d" theme[upload_mid]="#2eb398" theme[upload_end]="#33b165" bpytop-1.0.68/themes/monokai.theme000066400000000000000000000045071416067541000171030ustar00rootroot00000000000000#Bashtop monokai theme #by aristocratos # Colors should be in 6 or 2 character hexadecimal or single spaced rgb decimal: "#RRGGBB", "#BW" or "0-255 0-255 0-255" # example for white: "#FFFFFF", "#ff" or "255 255 255". # All graphs and meters can be gradients # For single color graphs leave "mid" and "end" variable empty. # Use "start" and "end" variables for two color gradient # Use "start", "mid" and "end" for three color gradient # Main background, empty for terminal default, need to be empty if you want transparent background theme[main_bg]="#060604" # Main text color theme[main_fg]="#F8F8F2" # Title color for boxes theme[title]="#F8F8F2" # Higlight color for keyboard shortcuts theme[hi_fg]="#F92672" # Background color of selected item in processes box theme[selected_bg]="#7a1137" # Foreground color of selected item in processes box theme[selected_fg]="#F8F8F2" # Color of inactive/disabled text theme[inactive_fg]="#595647" # Color of text appearing on top of graphs, i.e uptime and current network graph scaling theme[graph_text]="#797667" # Misc colors for processes box including mini cpu graphs, details memory graph and details status text theme[proc_misc]="#A6E22E" # Cpu box outline color theme[cpu_box]="#75715E" # Memory/disks box outline color theme[mem_box]="#75715E" # Net up/down box outline color theme[net_box]="#75715E" # Processes box outline color theme[proc_box]="#75715E" # Box divider line and small boxes line color theme[div_line]="#595647" # Temperature graph colors theme[temp_start]="#7976B7" theme[temp_mid]="#D8B8B2" theme[temp_end]="#F92672" # CPU graph colors theme[cpu_start]="#A6E22E" theme[cpu_mid]="#F8F8F2" #b05475" theme[cpu_end]="#F92672" # Mem/Disk free meter theme[free_start]="#75715E" theme[free_mid]="#a9c474" theme[free_end]="#e2f5bc" # Mem/Disk cached meter theme[cached_start]="#75715E" theme[cached_mid]="#66D9EF" theme[cached_end]="#aae7f2" # Mem/Disk available meter theme[available_start]="#75715E" theme[available_mid]="#E6DB74" theme[available_end]="#f2ecb6" # Mem/Disk used meter theme[used_start]="#75715E" theme[used_mid]="#F92672" theme[used_end]="#ff87b2" # Download graph colors theme[download_start]="#2d2042" theme[download_mid]="#7352a8" theme[download_end]="#ccaefc" # Upload graph colors theme[upload_start]="#570d33" theme[upload_mid]="#cf277d" theme[upload_end]="#fa91c7" bpytop-1.0.68/themes/nord.theme000066400000000000000000000044071416067541000164070ustar00rootroot00000000000000#Bashtop theme with nord palette (https://www.nordtheme.com) #by Justin Zobel # Colors should be in 6 or 2 character hexadecimal or single spaced rgb decimal: "#RRGGBB", "#BW" or "0-255 0-255 0-255" # example for white: "#ffffff", "#ff" or "255 255 255". # All graphs and meters can be gradients # For single color graphs leave "mid" and "end" variable empty. # Use "start" and "end" variables for two color gradient # Use "start", "mid" and "end" for three color gradient # Main background, empty for terminal default, need to be empty if you want transparent background theme[main_bg]="#2E3440" # Main text color theme[main_fg]="#D8DEE9" # Title color for boxes theme[title]="#8FBCBB" # Higlight color for keyboard shortcuts theme[hi_fg]="#5E81AC" # Background color of selected item in processes box theme[selected_bg]="#4C566A" # Foreground color of selected item in processes box theme[selected_fg]="#ECEFF4" # Color of inactive/disabled text theme[inactive_fg]="#4C566A" # Misc colors for processes box including mini cpu graphs, details memory graph and details status text theme[proc_misc]="#5E81AC" # Cpu box outline color theme[cpu_box]="#4C566A" # Memory/disks box outline color theme[mem_box]="#4C566A" # Net up/down box outline color theme[net_box]="#4C566A" # Processes box outline color theme[proc_box]="#4C566A" # Box divider line and small boxes line color theme[div_line]="#4C566A" # Temperature graph colors theme[temp_start]="#81A1C1" theme[temp_mid]="#88C0D0" theme[temp_end]="#ECEFF4" # CPU graph colors theme[cpu_start]="#81A1C1" theme[cpu_mid]="#88C0D0" theme[cpu_end]="#ECEFF4" # Mem/Disk free meter theme[free_start]="#81A1C1" theme[free_mid]="#88C0D0" theme[free_end]="#ECEFF4" # Mem/Disk cached meter theme[cached_start]="#81A1C1" theme[cached_mid]="#88C0D0" theme[cached_end]="#ECEFF4" # Mem/Disk available meter theme[available_start]="#81A1C1" theme[available_mid]="#88C0D0" theme[available_end]="#ECEFF4" # Mem/Disk used meter theme[used_start]="#81A1C1" theme[used_mid]="#88C0D0" theme[used_end]="#ECEFF4" # Download graph colors theme[download_start]="#81A1C1" theme[download_mid]="#88C0D0" theme[download_end]="#ECEFF4" # Upload graph colors theme[upload_start]="#81A1C1" theme[upload_mid]="#88C0D0" theme[upload_end]="#ECEFF4" bpytop-1.0.68/themes/solarized_dark.theme000066400000000000000000000042161416067541000204400ustar00rootroot00000000000000#Bashtop solarized theme #by aristocratos # Colors should be in 6 or 2 character hexadecimal or single spaced rgb decimal: "#RRGGBB", "#BW" or "0-255 0-255 0-255" # example for white: "#FFFFFF", "#ff" or "255 255 255". # All graphs and meters can be gradients # For single color graphs leave "mid" and "end" variable empty. # Use "start" and "end" variables for two color gradient # Use "start", "mid" and "end" for three color gradient # Main background, empty for terminal default, need to be empty if you want transparent background theme[main_bg]="#002b36" # Main text color theme[main_fg]="#eee8d5" # Title color for boxes theme[title]="#fdf6e3" # Higlight color for keyboard shortcuts theme[hi_fg]="#b58900" # Background color of selected items theme[selected_bg]="#073642" # Foreground color of selected items theme[selected_fg]="#d6a200" # Color of inactive/disabled text theme[inactive_fg]="#073642" # Misc colors for processes box including mini cpu graphs, details memory graph and details status text theme[proc_misc]="#bad600" # Cpu box outline color theme[cpu_box]="#586e75" # Memory/disks box outline color theme[mem_box]="#586e75" # Net up/down box outline color theme[net_box]="#586e75" # Processes box outline color theme[proc_box]="#586e75" # Box divider line and small boxes line color theme[div_line]="#586e75" # Temperature graph colors theme[temp_start]="#268bd2" theme[temp_mid]="#ccb5f7" theme[temp_end]="#fc5378" # CPU graph colors theme[cpu_start]="#adc700" theme[cpu_mid]="#d6a200" theme[cpu_end]="#e65317" # Mem/Disk free meter theme[free_start]="#4e5900" theme[free_mid]="" theme[free_end]="#bad600" # Mem/Disk cached meter theme[cached_start]="#114061" theme[cached_mid]="" theme[cached_end]="#268bd2" # Mem/Disk available meter theme[available_start]="#705500" theme[available_mid]="" theme[available_end]="#edb400" # Mem/Disk used meter theme[used_start]="#6e1718" theme[used_mid]="" theme[used_end]="#e02f30" # Download graph colors theme[download_start]="#3d4070" theme[download_mid]="#6c71c4" theme[download_end]="#a3a8f7" # Upload graph colors theme[upload_start]="#701c45" theme[upload_mid]="#d33682" theme[upload_end]="#f56caf"bpytop-1.0.68/themes/whiteout.theme000066400000000000000000000042631416067541000173150ustar00rootroot00000000000000#Bashtop "whiteout" theme #by aristocratos # Colors should be in 6 or 2 character hexadecimal or single spaced rgb decimal: "#RRGGBB", "#BW" or "0-255 0-255 0-255" # example for white: "#FFFFFF", "#ff" or "255 255 255". # All graphs and meters can be gradients # For single color graphs leave "mid" and "end" variable empty. # Use "start" and "end" variables for two color gradient # Use "start", "mid" and "end" for three color gradient # Main background, empty for terminal default, need to be empty if you want transparent background theme[main_bg]="#ff" # Main text color theme[main_fg]="#30" # Title color for boxes theme[title]="#10" # Higlight color for keyboard shortcuts theme[hi_fg]="#284d75" # Background color of selected item in processes box theme[selected_bg]="#15283d" # Foreground color of selected item in processes box theme[selected_fg]="#ff" # Color of inactive/disabled text theme[inactive_fg]="#dd" # Misc colors for processes box including mini cpu graphs, details memory graph and details status text theme[proc_misc]="#03521d" # Cpu box outline color theme[cpu_box]="#1a361e" # Memory/disks box outline color theme[mem_box]="#3d3c14" # Net up/down box outline color theme[net_box]="#1a1742" # Processes box outline color theme[proc_box]="#3b1515" # Box divider line and small boxes line color theme[div_line]="#80" # Temperature graph colors theme[temp_start]="#184567" theme[temp_mid]="#122c87" theme[temp_end]="#9e0061" # CPU graph colors theme[cpu_start]="#0b8e44" theme[cpu_mid]="#a49104" theme[cpu_end]="#8d0202" # Mem/Disk free meter theme[free_start]="#b0d090" theme[free_mid]="#70ba26" theme[free_end]="#496600" # Mem/Disk cached meter theme[cached_start]="#26c5ff" theme[cached_mid]="#74e6fc" theme[cached_end]="#0b1a29" # Mem/Disk available meter theme[available_start]="#ffb814" theme[available_mid]="#ffd77a" theme[available_end]="#292107" # Mem/Disk used meter theme[used_start]="#ff4769" theme[used_mid]="#d9626d" theme[used_end]="#3b1f1c" # Download graph colors theme[download_start]="#8d82de" theme[download_mid]="#413786" theme[download_end]="#130f29" # Upload graph colors theme[upload_start]="#f590f9" theme[upload_mid]="#722e76" theme[upload_end]="#2b062d"bpytop-1.0.68/tox.ini000066400000000000000000000006671416067541000144530ustar00rootroot00000000000000[tox] isolated_build = true envlist = py37,py38,py39,mypy,pylint [gh-actions] python = 3.7: py37 3.8: py38 3.9: py39, mypy, pylint [testenv] whitelist_externals = poetry commands = poetry install -v poetry run pytest [testenv:mypy] basepython = python3.9 commands = poetry install -v poetry run mypy . [testenv:pylint] basepython = python3.9 commands = poetry install -v poetry run pylint -E bpytop