pax_global_header00006660000000000000000000000064142027625300014513gustar00rootroot0000000000000052 comment=17979139c3c4f3dc197cb7c23370c7f807f25387 btop-1.2.3/000077500000000000000000000000001420276253000124625ustar00rootroot00000000000000btop-1.2.3/.editorconfig000066400000000000000000000001651420276253000151410ustar00rootroot00000000000000[*.{cpp,h,sh,md,cfg,sample}] indent_style = tab indent_size = 4 [*.{yml,yaml}] indent_style = space indent_size = 2 btop-1.2.3/.github/000077500000000000000000000000001420276253000140225ustar00rootroot00000000000000btop-1.2.3/.github/FUNDING.yml000066400000000000000000000012061420276253000156360ustar00rootroot00000000000000# 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'] btop-1.2.3/.github/ISSUE_TEMPLATE/000077500000000000000000000000001420276253000162055ustar00rootroot00000000000000btop-1.2.3/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000030411420276253000206750ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: "[BUG]" labels: bug assignees: aristocratos --- **Read the README.md and search for similar issues before posting a bug report!** Any bug that can be solved by just reading the [prerequisites](https://github.com/aristocratos/btop#prerequisites) section of the README will likely be ignored. **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):** - btop++ version: `btop -v` - Binary: [self compiled or static binary from release] - (If compiled) Compiler and version: - Architecture: [x86_64, aarch64, etc.] `uname -m` - Platform: [Linux, FreeBSD, OsX] - (Linux) Kernel: `uname -r` - (OSX/FreeBSD) Os release version: - Terminal used: - Font used: **Additional context** contents of `~/.config/btop/btop.log` (try running btop with `--debug` flag if btop.log is empty) **GDB Backtrace** If btop++ is crashing at start the following steps could be helpful: (Extra helpful if compiled with `make OPTFLAGS="-O0 -g"`) 1. run (linux): `gdb btop` (macos): `lldb btop` 2. `r` to run, wait for crash and press enter if prompted, CTRL+L to clear screen if needed. 3. (gdb): `thread apply all bt` (lldb): `bt all` to get backtrace for all threads 4. Copy and paste the backtrace here: btop-1.2.3/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000011571420276253000217360ustar00rootroot00000000000000--- 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. btop-1.2.3/.github/workflows/000077500000000000000000000000001420276253000160575ustar00rootroot00000000000000btop-1.2.3/.github/workflows/continuous-build-linux.yml000066400000000000000000000056611420276253000232520ustar00rootroot00000000000000name: Continuous Build Linux on: workflow_dispatch: push: branches: - main tags-ignore: - '*.*' paths: - 'src/**' - '!src/osx/**' - '!src/freebsd/**' - 'include/**' - 'Makefile' - '.github/workflows/continuous-build.yml' jobs: static-build: continue-on-error: true strategy: matrix: toolchain: - aarch64-linux-musl - aarch64_be-linux-musl - arm-linux-musleabi - arm-linux-musleabihf - armeb-linux-musleabi - armeb-linux-musleabihf - armel-linux-musleabi - armel-linux-musleabihf - armv5l-linux-musleabi - armv5l-linux-musleabihf - armv6-linux-musleabi - armv6-linux-musleabihf - armv7l-linux-musleabihf - armv7m-linux-musleabi - armv7r-linux-musleabihf - i486-linux-musl - i686-linux-musl - m68k-linux-musl - mips-linux-musl - mips-linux-musln32sf - mips-linux-muslsf - mips64-linux-musl - mips64-linux-musln32 - mips64-linux-musln32sf - mips64el-linux-musl - mips64el-linux-musln32 - mips64el-linux-musln32sf - mipsel-linux-musl - mipsel-linux-musln32 - mipsel-linux-musln32sf - mipsel-linux-muslsf - powerpc-linux-musl - powerpc-linux-muslsf - powerpc64-linux-musl - powerpc64le-linux-musl - powerpcle-linux-musl - powerpcle-linux-muslsf - riscv32-linux-musl - riscv64-linux-musl - s390x-linux-musl - x86_64-linux-musl - x86_64-linux-muslx32 # - or1k-linux-musl # - sh2-linux-musl # - sh2-linux-muslfdpic # - sh2eb-linux-musl # - sh2eb-linux-muslfdpic # - sh4-linux-musl # - sh4eb-linux-musl runs-on: ubuntu-latest container: muslcc/x86_64:${{ matrix.toolchain }} steps: - name: Install build tools run: apk add --no-cache coreutils git make tar zstd - name: Checkout source uses: actions/checkout@v2 - name: Fix - Stopping at filesystem boundary run: git init # [fix Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).] - name: Build run: make STATIC=true STRIP=true - name: Make executable run: chmod +x bin/* - name: Set up directories run: | mkdir .artifacts mkdir .package - name: Create binary atrifacts run: | TOOLCHAIN=${{ matrix.toolchain }} GIT_HASH=$(git rev-parse --short "${{ github.sha }}") FILENAME=btop-${TOOLCHAIN/linux-musl/}-$GIT_HASH cp bin/btop .artifacts/$FILENAME - name: Upload artifacts uses: actions/upload-artifact@v2 with: name: btop-${{ matrix.toolchain }} path: '.artifacts/**' btop-1.2.3/.github/workflows/continuous-build-macos.yml000066400000000000000000000012631420276253000232070ustar00rootroot00000000000000name: Continuous Build MacOS on: push: branches: - main tags-ignore: - '*.*' paths: - 'src/**' - '!src/linux/**' - '!src/freebsd/**' - 'include/**' - 'Makefile' - '.github/workflows/*' jobs: build-osx: runs-on: macos-11 steps: - uses: actions/checkout@v2 - name: Compile run: | make CXX=g++-11 ARCH=x86_64 STATIC=true STRIP=true GIT_HASH=$(git rev-parse --short "$GITHUB_SHA") mv bin/btop bin/btop-x86_64-BigSur-$GIT_HASH ls -alh bin - uses: actions/upload-artifact@v2 with: name: btop-x86_64-macos-BigSur path: 'bin/*' btop-1.2.3/.gitignore000066400000000000000000000012711420276253000144530ustar00rootroot00000000000000# gitginore template for creating Snap packages # website: https://snapcraft.io/ parts/ prime/ stage/ *.snap # Snapcraft global state tracking data(automatically generated) # https://forum.snapcraft.io/t/location-to-save-global-state/768 /snap/.snapcraft/ # Source archive packed by `snapcraft cleanbuild` before pushing to the LXD container /*_source.tar.bz2 # Prerequisites *.d # Compiled Object files *.slo *.lo *.o *.obj # Precompiled Headers *.gch *.pch # Compiled Dynamic libraries *.so *.dylib *.dll # Fortran module files *.mod *.smod # Compiled Static libraries *.lai *.la *.a *.lib # Executables *.exe *.out *.app build bin btop .*/ #do not ignore .github directory !.githubbtop-1.2.3/CHANGELOG.md000066400000000000000000000171721420276253000143030ustar00rootroot00000000000000## v1.2.3 * Changed: floating_humanizer() now show fractions when shortened and value is < 10 * Fixed: Process tree not redrawing properly * Fixed: string to wstring conversion crash when string is too big ## v1.2.2 * Changed: Reverted uncolor() back to using regex to fix delay in opening menu when compiled with musl * Added: Toggle for showing free disk space for privileged or normal users * Added: Clarification on signal screen that number can be manually entered ## v1.2.1 * Added: Arrow only after use of "f" when filtering processes, by @NavigationHazard * Fixed: Fx::uncolor not removing all escapes * Fixed: Text alignment for popup boxes * Fixed: Terminal resize warning getting stuck * Removed: Unnecessary counter for atomic_lock * Added: Percentage progress to Makefile * Fixed: Alignment of columns in proc box when wide UTF8 characters are used * Fixed: Battery meter draw fix ## v1.2.0 * Added: Support for FreeBSD, by @joske and @aristocratos * Fixed (again): Account for system rolling over net speeds in Net::collect() * Added: Theme gruvbox_material_dark, by @marcoradocchia * Added: Option for base 10 bytes/bits ## v1.1.5 * Fixed: Account for system rolling over net speeds in Net::collect() ## v1.1.4 * Fixed: Create dependency files in build directory when compiling, by @stwnt * Fixed: fix CPU temp fallback on macOS, by @joske * Changed: From rng::sort() to rng::stable_sort() for more stability * Fixed: in_avail() can always be zero, by @pg83 ## v1.1.3 * Added: New theme ayu, by @AlphaNecron * Added: New theme gruvbox_dark_v2, by @pietryszak * Fixed: Macos cpu coretemp for Intel, by @joske * Added: New theme OneDark, by @vtmx * Fixed: Fixed network graph scale int rollover * Fixed: Suspected possibility of very rare stall in Input::clear() ## v1.1.2 * Fixed: SISEGV on macos Mojave, by @mgradowski * Fixed: Small optimizations and fixes to Mem::collect() and Input::get() * Fixed: Wrong unit for net_upload and net_download in config menu * Fixed: UTF-8 detection on macos * Fixed: coretemp iteration due to missing tempX_input, by @KFilipek * Fixed: coretemp ordering ## v1.1.1 * Added: Partial static build (libgcc, libstdc++) for macos * Changed: Continuous build macos switched to OSX 11.6 (Big Sur) and partial static build * Changed: Release binaries for macos switched to OSX 12 (Monterey) and partial static build ## v1.1.0 * Added: Support for OSX, by @joske and @aristocratos ## v1.0.24 * Changed: Collection ordering * Fixed: Restore all escape seq mouse modes on exit * Fixed: SIGINT not cleaning up on exit ## v1.0.23 * Fixed: Config parser missing first value when not including version header * Fixed: Vim keys menu lists selection * Fixed: Stall when clearing input queue on exit and queue is >1 * Fixed: Inconsistent behaviour of "q" key in the menus ## v1.0.22 * Fixed: Bad values for disks and network on 32-bit ## v1.0.21 * Fixed: Removed extra spaces in cpu name * Added: / as alternative bind for filter * Fixed: Security issue when running with SUID bit set ## v1.0.20 * Added: Improved cpu sensor detection for Ryzen Mobile, by @adnanpri * Changed: Updated makefile * Changed: Regex for Fx::uncolor() changed to string search and replace * Changed: Removed all use of regex with dedicated string functions ## v1.0.19 * Fixed: Makefile now tests compiler flag compatibility ## v1.0.18 * Fixed: Makefile g++ -dumpmachine failure to get platform on some distros ## v1.0.17 * Changed: Reverted mutexes back to custom atomic bool based locks * Added: Static binaries switched to building with musl + more platforms, by @jan-guenter * Fixed: Improved battery detection, by @jan-guenter * Added: Displayed battery selectable in options menu * Fixed: Battery error if non existent battery named is entered ## v1.0.16 * Fixed: atomic_wait() and atomic_lock{} use cpu pause instructions instead of thread sleep * Fixed: Swapped from atomic bool spinlocks to mutexes to fix rare deadlock * Added: Continuous Build workflow for OSX branch, by @ShrirajHegde * Changed: Reverted thread mutex lock to atomic bool with wait and timeout * Changed: Removed unnecessary async threads in Runner thread * Added: Try to restart secondary thread in case of stall and additional error checks for ifstream in Proc::collect() * Fixed: change [k]ill to [K]ill when enabling vim keys, by @jlopezcur ## v1.0.15 * Fixed: Extra "root" partition when running in snap * Changed: Limit atomic_wait() to 1000ms to fix rare stall * Fixed: Removed unneeded lock in Runner::run() * Added: Toggle in options for enabling directional vim keys "h,j,k,l" ## v1.0.14 * Changed: Total system memory is checked at every update instead of once at start * Added: Continuous Build workflow, by @ShrirajHegde * Fixed: Uid -> User fallback to getpwuid() if failure for non static builds * Fixed: snap root disk and changed to compiler flags instead of env variables for detection * Added: Development branch for OSX, by @joske ## v1.0.13 * Changed: Graph empty symbol is now regular whitespace ## v1.0.12 * Fixed: Exception handling for faulty net download/upload speed * Fixed: Cpu percent formatting if over 10'000 ## v1.0.11 * Changed: atomic_wait to use while loop instead of wait() because of rare stall when a signal handler is triggered while waiting * Fixed: Get real / mountpoint when running inside snap * Fixed: UTF8 set LANG and LC_ALL to empty before UTF8 search and fixed empty error msg on exit before signal handler init * Changed: Init will continue with a warning if UTF-8 locale are detected and it fails to set the locale ## v1.0.10 * Added: Wait for terminal size properties to be available at start * Changed: Stop second thread before updating terminal size variables * Changed: Moved check for valid terminal dimensions to before platform init * Added: Check for empty percentage deques * Changed: Cpu temp values check for existing values * Fixed: Cpu percent cutting off above 1000 percent and added scaling with "k" prefix above 10'000 * Fixed: Crash when rapidly resizing terminal at start ## v1.0.9 * Added: ifstream check and try-catch for stod() in Tools::system_uptime() * Fixed: Freeze on cin.ignore() ## v1.0.8 * Fixed: Additional NULL checks in UTF-8 detection * Changed: Makefile: Only look for g++-11 if CXX=g++ * Fixed: Missing NULL check for ttyname * Changed: Only log tty name if known ## v1.0.7 * Fixed: Crash when opening menu at too small size * Fixed: Cores not constrained to cpu box and core numbers above 100 cut off * Fixed: Scrollbar position incorrect in small lists and selection not working when filtering ## v1.0.6 * Fixed: Check that getenv("LANG") is not NULL in UTF-8 check * Fixed: Processes not completely hidden when collapsed in tree mode * Fixed: Changed wrong filename error.log to btop.log ## v1.0.5 * Fixed: Load AVG sizing when hiding temperatures * Fixed: Sizing constraints bug on start and boxes can be toggled from size error screen * Fixed: UTF-8 check crashing if LANG was set to non existant locale ## v1.0.4 * Fixed: Use /proc/pid/statm if RSS memory from /proc/pid/stat is faulty ## v1.0.3 * Fixed: stoi 0 literal pointer to nullptr and added more clamping for gradient array access ## v1.0.2 * Fixed: ARCH detection in Makefile * Fixed: Color gradient array out of bounds, added clamp 0-100 for cpu percent values * Fixed: Menu size and preset size issues and added warnings for small terminal size * Fixed: Options menu page selection alignment ## v1.0.1 * Fixed: UTF-8 check to include UTF8 * Fixed: Added thread started check before joining in clean_quit() * Fix documentation of --utf-force in README and --help. by @purinchu ## v1.0.0 * First release for Linux btop-1.2.3/CODE_OF_CONDUCT.md000066400000000000000000000064251420276253000152700ustar00rootroot00000000000000# 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/faqbtop-1.2.3/CONTRIBUTING.md000066400000000000000000000036371420276253000147240ustar00rootroot00000000000000# 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. ## Formatting ### Follow the current syntax design * Indent type: Tabs * Tab size: 4 * Alternative operators `and`, `or` and `not`. * Opening curly braces `{` at the end of the same line as the statement/condition. ## General guidelines * Don't force a programming style. Use object oriented, functional, data oriented, etc., where it's suitable. * Use [RAII](https://en.cppreference.com/w/cpp/language/raii). * Make use of the standard algorithms library, (re)watch [C++ Seasoning](https://www.youtube.com/watch?v=W2tWOdzgXHA) and [105 STL Algorithms](https://www.youtube.com/watch?v=bFSnXNIsK4A) for inspiration. * Make use of the included [robin_hood unordered map & set](https://github.com/martinus/robin-hood-hashing) * Do not add includes if the same functionality can be achieved using the already included libraries. * Use descriptive names for variables. * Use comments if not very obvious what your code is doing. * Add comments as labels for what's currently happening in bigger sections of code for better readability. * Avoid writing to disk. * If using the logger functions, be sensible, only call it if something of importance has changed. * Benchmark your code and look for alternatives if they cause a noticeable negative impact. For questions open a new discussion thread or send a mail to jakob@qvantnet.com For proposing changes to this document create a [new issue](https://github.com/aristocratos/btop/issues/new/choose). btop-1.2.3/Img/000077500000000000000000000000001420276253000131765ustar00rootroot00000000000000btop-1.2.3/Img/alt.png000066400000000000000000004626661420276253000145100ustar00rootroot00000000000000PNG  IHDRt sBITOtEXtSoftwaremate-screenshotȖJ IDATxy<όc'KR"Jh/)_TJQ)%xURd22d1~ όsϹ}Yq$ @ }@ m@@ )<}@ zQʼnUv }J@׮?+J1v[K'~[6]q֒$KH g(R0ĂܔG(XQv%.6(ۆPGkW_Ekv}=^~ 6l ECpg8sf׭B9ؒ"u7<r9$Ts)>.ԈxߢP鮓$&X]CΈ p3JYǂR)qSS iR}0eS$ ]kobr(QǗ w-Fer6<37?RĔ//_(#UP|C-Ƽ}qs}iӖ}4c.¬mcH;|ऌ77h .uP `Iz f!7Hnˮ.PX$;v@T_,>J-.// rt,z;S>%#)69/ y#XC1)J{qmtt E1*>3jhVV, NΆ#|ԓ7;rnSS(vrm8# QМ_`\9@EyE369B}g%_iD`_ZbvXۧ, OPg2ģܜOO/Qr3\h[ k?KtkO!KYV*^qÈkVcLQ =zZ"yJ'#(Yi;wNo&WF|䅩 k$wkƒ=OƤꇒȼ$uZcpPWK5-DA7|ynh\33q(?A~ŖJMȧ%%_wlmhs y敂aY%#;<|oFEֻ$ ݲ?cO*_ݦ٢.>¨oLӀi JbiaoyBvZVyZi^ xyS(mo찡?Rt8ǬZ{D4tM|5׏>eum6"J ExRa|ZeK\(,AkUS m$tfK_W `NuŨKs']Ij:z:0kh!MWGKB6e7m=C\{f)u-9:p*RNhvSE_sNwab***F(4u4$$ı)6xoeD?8gdjYN/f彴c\(n\74[ȭ9jtaƠ E(L# RSI|)|Йjf d'Q[tU^bȉgqAqvL*Q&4t%Drω^^QS_ ];bC Z^2U_WOcƖVKU@dfFme))wNnzzZf[A͵o hDat8k~vF;?t`ΓY#.|ZVxs3fD{@@L(Y#Gr n.W( ?z~5//0M$x?љ)z㑊֗ϬR흕N۲\(}09 7/P% <|8e-1zyn~||3bM)TGk@y=7//GaxmGmOu Hׯ@Cϙhz{5<H $=uezS*9?pAR Fnr80@Q{q "c qY^G%~0+MbU;=Eٙ9_,7/ʜstɢ"5 9< hxlχ=DYc?9{蝸BKQp~N(AAlx[B1.zȇ'Cʅfd81_TX8j< {uu2/y{%x<,#-Hzy_ @}P]./Iq@}Vxp kԦڸR2lXaϻ[{%q5[dY_Z Џer }WKjWUM͢~U ZOׅIaPc£ĔXp=5> FSuVtmxyyĖ_˜O>Aj466@e޸jKo1It ۭ8ۥF;h ؛g໮9Hyyn @Ϯ\Sy$+2 j7;'dQsC=vyFn~5E/M\lTktt!gݗjR.ƄH$߯y%Ϲ_gIq32߆t+BԺ/;ǨzZ_Zޱs&)獾?PY EqwQ6ˋӋ +fHyu2՞KՕdʁNW@ͤFf[4h269-huuK[::ewݿ7.;CDq,J$_䷞.gb`0g1D":ԾRi6~1q:M`zաyTOJ/ܒ`Du>(Xס fJ-@шvkNf8 LI0nLof0|ۻA|0Qr?{Ϫ؍7vņQ%ʹ6>VAAdee( '')HZt9sVѹj0<^r{ O$0@Hhdo,kڸ0;|s[/lll(ߪR@-^$F:saК{O~ٸQ%jdCf7:tU;#Ք2D" HXV5ZY1/u# -ߩw 7ɣ]36 G/H7'm H$:Mmqqa j ‚ӦaS3W}cʢ*l4\ k~D"n@RE2v*guZTlWhhh`Hpe%ȱM$ف^C:,8*b=IC^YI26ɈF3{ I+K^ݛ$0ruD E[vd;U[.LL_6x҉Ƙ$]9;_VFmmuhwx殓>ۄy2kF #lu+ p ߭*ANl8(ꚛ V?z QqëݐVRJhh(icR}](M6_JilnjȎ y2vӝ,ϗ} ,MU%%6RǀΑ6E~^ӕHyBb3|Ʃ>,2~nav.xV z)IahBC" 9dJ1?ȡ|А5["mb %3!-6f:lwd5*9<% =OЈS[juݽ+Oc(兔On""鶬 LL[e2xT+?g|;p8xD ƒI2:8Z=Za/%ui>{nu䣄TvNfb"Cճ0N#7LL3[8$.-.w!RSvg^s.JcoϲmK5&;?0Q%7jpYfpwV^(#=w*On$!:ވ\2F13+5%/ڷeX1) $0l|s q$kpzkvk*Otvmq,ף>%18 &'oz^l]lTǜv{%[iMn&uQb;MskLS{l[@F1EG 8nyn6N! *Ey^gn;s^I`99s1Fں!ϭhɵ%֔ST# CTԻ<ą=,矝h5O>E);;8黻8巸go:?k+ l86;Ϫ}W$oY5ݾaa#;B<_Ic/8w=8Q9.9IqN&*:mOQLyqTW[< ^u,oD@ ?rJW<`=#q8ٱ@ zJcM }@ ~ey?򈿐_K>@ ◦? m,;![7w$z|mr/mZcn$G~7TFڻgm˹Jmr7{%_N]@ @ @ SX?+J1vيT.-?#){,ѡ; E:Rmn-ˎ 1yej p.SK~INcAp_[ɍ?u*$pϩ#}@rյY7s ~:G ^(;:H"7WB ٢Zw35ur8_rq%#)6CazTJv|nEPb˿rυR23е&&| ^C1)J{q+aD$:@y[`ɢZ'8)6Az翜ւȻnfAUU9"*((2,%<2 yy?7Ȼn'-b:w-Fer6<37?RE?gLTm ձSDCiV[!?9+3PEilHMLjVTmwbBXCo*9Qhq2ɡdg %ԏ{ k\PtD~B)i 5u~Ne4RU/DŽ @L2p&,?"jD^6Xhy&Um^@|5׏>eum6">p5gd7~WFt'jVW  lԜx$ '=YdxݼW{R96ͦ|/'0oKMYY~c&6YMD"K,`4,.z.]t5uQzݎYzbCWoύ/ Ζys+)omx\6YXɢ"59;vrڠ{q݊r~=)/qz}Pd},T orr~r{>oBG9L6^[T0(s[R͔яAz^KE;~=@9__yL.-FU2V ̕ϋ $_NytiBHfASq k*9?ucIJar@`0m0ܻO? O]'ٹj!9XWn6XD0Xtr4iws} g Qu>~顁 ]v&~-9| $$D"No\[#! Xl\cţvewl+s_nWR+:I @g OhC+-?-" w?oKu+TY2Ppx7ct07MMt=~DYx5yZwSǍJ6^ot5(*H]O} 3htiGJ*Ӂ@h- Y1&4c`,;@VH)OHojl7NOKt{"'++@99y|>F\atÒ+wwIV< ꬐v?TPhjj<M$F:saК{O*(ɜ<8IɶiNSfN5>O H؞֪}|xx2Sc#@V=H|#1d@ |wQ'D @XXd!`ґՏ:_TXga~эyL80YIO | }y-׸~ZDz'Yt -AscMe#@Kڎ 4`(O|.k:sdݘI1dR?!Q5]eh\t: `ٌWʯY9=n-ѣԇ VJ^AhAQV<_HӟGcP>HlH fc>XSO-悕a: gu&~fD%ZӦӲػHவSJ^&z:Gvڨv4"PX0h@l2+;Wp1㙼&9 5Y:Nq!zf.,DD _+(J)-ET>Ka8Ckʌj0 }㰡ddN3Z:; ,*1l[o(}!󐗓 {#XY&3. ϲrHJ^: & fe QȹBYg33MJ &Nk*!IMm}tLd]ؼ\}AGGQb--`>CWUV_WG OfW:[8JUEu^xbsNt?0O߰b޻o`B{htV+t.< ]3c磦1 $$12폿o_l۩++V9bN aa_ksDdt+IO\ivryN\aNFeU_u_;$:5XPsƐdyӈ8B}h^iKGGCYؚ9=(l,e4i-]:O0nrVmӜǤ|MI.LMJb ejgu`,tPjz:})␩[WYNJ7[hOnVO}4q*;@|_ret,a<5rbc2b#?XJ6G0}jjyyD6bZm\qGzY0wD ڊ5۰v凂hv~]0T/f'.bKګcj{np{Pnw 6Ѷ&{s+Y￵} fuiWI#De e2s^D@7ߊ@ zNpc_;oVk'z?/"@ ؂? @ NA౫ʍGuleŧʪ"ε~$*?f7~h#@W=GD=zJoo@ @ 8@  *ʋ? >iyivxbaqvRMEj$ч?Q([^Ly]Pnʵ%^5]>!)ybSȹ:вBl5W_}K$.%e2/>wacUuDߣz 1WKlt]3K(_r2c#]@LZv/7OaKjkDo '?dR^^gdAt{̚'xN/7h9~:)YiwgoyD|jfv)%c+ZO4|THtwh*j?NνӾN R!>+Jc) w."h~UjX6h_^Se O-uo?As*WnB}g%_iNvΙJk7?շRRZ~OL`%9~bA}=ZPQ_~-=?ULI߻&37+?rfcKf 'WZ:KH*Wyst9uLrP넧,eY{+o_7Ǚ[ADQɫ\6L&_YqԀ!j-/mͲLM' U3e*DتI;O 6ʡmF0`_uTL95s*.""JX[3+Ϣ$G)FjևF:{]mĕ׋pbCʔ׃JIIz|~&˼*AX;Fg[qL'ITU8UE!xnXaܭ_٨H-Oi{uTS3wcks@[gDv9;WqDcݤ[G})]~y%yi>:/v]O* yqv y2"瞛w欜' G?@oOe"j<9Ks]LX3,za2]\7?V&&':"wn2nwF) 8=Ρ3ҋcH2 ;ςƞ" ~@ ܶ3|A{}MeDu>"#EFTC*#`잚j/yиo.Me8 J%DbcCB&>~oة&٫bv:cCfZ莞\XQ{qÆp:fE7K/uͱ Y@䥥ˎ:.~o:WaE%sOs>ޥ-9_$'vZzdtMt:AxfќqY@ש;asssj^]=m:à'~ 0,&dQ9<Yco?&Mx:ςX^^+))( <hr:GMU'hkO:pV!4Z#=Vș9M}j<\SKw<[t:wc##+[XP"ԇLYZrl5Tp}cZVXqwTǬUʭ/c;k] _Iȿt 9w4_y$`SR Nj%T*Tݻ꿒mHBB O>$ 9AcԀuRKV,\ʺ}4eŇ²X߭:܄T`Ʒ\Bm%]u",->ZN%q8/ۻt 4s{ *=7'%)YG. #kiVƕ˫ f a^ U"/vHu?J4N6-k-/MӅy.<0iL5#!ϲ%oΞihfƤu#@ _@oOepIy6/a(0Mifyڭcu ڝnOܰ|ƹ[}b?_˺9b3`ތE;]7ejEAۢ4py^ M17Zr:*(i;$sx^"~-CRSSdy ^AQ+ˡT_ @ ?/5}FӍ^v9ԳMgTxSD"No9LшĖhss3A [ũɏ}g#))Y^^ѥXcEA^T{q5T0!$-=剛ڏuSǿx;/ǧf[W[7mܹ.mqFu~6嵒]?TPbb 淣#"70!@Iصlht:OI<:H슨,޾"""]I),yD?SyaM@3А C8D>z/(oNձ71dKsVO;XB@NnGBC6[@ _ffb$~ƺĖ7Oeѭ-nV[ÓzGANJ.heyI+kDr@)"RΆJY~n}uu`JRsWW<ߛ\^@ @|ͫgm謤;27%O>$ 9Ac['ei^r'Rqۯ| өԒN:^TjI mܔkKTu]3K(_r2c#ǵ7?q'Sb籥lF ~,#JM7TkOXSu~JQק5۽Θp!`Zyf,3\M~Č =z @BXK+GjrMiWX䘥_TgrM@ rx~b,cVy-$Q[V\\VMk;iǖ~?W훗稧,8ȋoۖFPMg ĤW`+Aǘ?SvrYݶ.{|zkj;DGmY Ɂc.wNn;[ |Ç8[0 ɲӦ mcIZ]{CN?d/}洞g6M|yG)6obr(nfCI%=LΆgG]\2l*0\#:/3%E[j* GN":I]\B$&b U!ǃ VML6} 1yԸOy $_NytiBl#%mޔ[F}[D<ϛ0l @Cϙhz{5<H vyXGmOu Hׯ$?~XzسWrI[ z,)htf7no,: jڸ9su3&pl4[=_r}nҸVt $=uezS'с>.ȍ^8JufryJUSci}m^Pa,vs~_Z54Vr-xnXaܭuxǜZL5>6c/@K&\Te`Y6p,$1Cx/v#X;0tⒼ4'q}ׅ7.1l.#_W5=X///k'BB,Th4:4kJxqq2C'?xzag]HPw?H[\kqB\^^\XIc-#;O[>>nDSu6l0BRWO{~Yʙx@΄d0g*?=zܕzg:ɿm&E񬳿G+R7:ߥdG4co잝^lޢ*jN8l^pH[>aLܪ"4zYWDZ#^Dte'[o~' Mg'_Xj@e|flԹk#TS'6z|3+I,-wVy ΟNY{8L,g-sᦽPߞe&&:n˓R." /o%K;o1ǦFrgbԵKK/R$ddoE"d/[24Eiq:vk}yyޟ9l0}-a-//Pjfˋ~(,(6bj/|D1uuJ]n'R333 Q/<某/o6hrIק\?Ɔ 7YiN[vxg̘=cWYg[ ; wINAG$Kp򁪯w!8ʽ夏lrPdU?4Y{܌buv2{q^]qק lS}5]>S'1߷V/+Kkҡ!='2L{knw:.v-P}OAv*|BKxo$bұ]>p.'*0 d{dx6% {iqڛnV~vZmŽ_}wO>斖d>ŝo%o~gÓxk)5ttFE}璒AtQU 7]2z)7n랕WfhQBiQ[Lφ%GSZv]3؞2ڇȏWHOQNuȘD 3'5@n4F^dNB*i\fbMKӦ i~~[ˆ)_^8)y-'2ϯl۸A'}XZ6T?h0:V*0:#?5Ckk7gAlW:~MӦNB.nb/G3^y6utK[ԃ]:An`o;x?̨Ro>W&ꮿxo^:!Bcj?n.Qѕ@!B!B] ^'I ))ls#,$eyB!q>蟿zL ebf$v +  NϭUAP7+B!Q;z,njDz8,?_Bxq)-|ub庫Y]'B!\CVukG!B^B!B!BB!BW8BA"*mZ>Ka9yYqN<=yՓ{5 S5s4m /f˪PB!pB!H*J]"($'8@mǪ :m zj3Ź8ߪ>eifR[1O Gͬ3BFUxfegǽŤv NIr[?A_\z:]yILR^&oMFV'kfՅ}>ŠABv7X%rky0<G˥V%1kR''(oꏛGXP|:%j'ǎ;gG)ՙ3{޿Ow92Z3OBͥ6XG(}lLw6QW)6䃐Y Kq^-=ezn3?޸'Osӌ3ܣpFW΢}vn*)e7P.NPسWHAJJAJJAJ}_50?zzܛ58n6*!K§_uoNr?bMU3?c'S-wf3IaҿAK0rŸ]h먇8ߍf3d][җIys eoTX3ՇdzZyLKl"Uds{!@xWhF*-+)(,cJ ~@;,g9߯y[{ΧV>I** ͺJC?Bⴝ'Lpypְ9Ǝn<Ҳߞlޓ vY5ʫ> x\a;Ws#SRR~WτbfW^dyk3Q9yT򂝁EM`ym\''jQ_b9yܠ踎 j;gscל=a_k!KZKO5EsReTS$U.C7VckXsb݇) ))&eZ|IT QL ""slx驥"\B`˛w.<S}Ӹp1˲">=S }mu0WuAmFzF;QRou·OY2\]qQYZZMs쥭M{qCNiqC_?x#U%c3 !b֐:ub5.td(ӱc 8FkE鱝wx(lzzϖL$G>u^ߞ +`U}#=HGwd2Y*/'Nʏ~yfֻwCMgLLS $IXm-u][qN ns&6}WPmo0tɸ4-RZs땭Bܡ5zhpqq =8P{]fٷgʠA6i^ɬp]sa"kl4?O RYQA4,{_l!ޫTw[eϫooКu#άK|Πh1WU{mx;hD1oǢ)\w|75$cۋf,牎# >}<R(e3*"ZBǎsN]|+U$zu'c222yyulvߕ8?}-1+VԫjY'!MZx^^_ۑp+j[c\y{ʩJep q\"ϭ]F`ӧgTgQF̴L}xOTTpZs{,DA`2/OgVԿUB?HKM^o(wQF Lmg ߿s5wk7; #.0:pzǎ,vp~~x݀<6; FCS]=Z@ykѻ@q:n8g-;H^/'~>ɯ yèkcLsh_<QL"?nϑpЄD~|0<:ƙ Kۧԑ{%dƼu`0> !xzdUO2#nZҜ6]H}<o b}7kbݯ=\\fiܓEum緦[5_ݗ1`젇L3(>&ܝ[r;xTuϭݑiyCbK_[̲ꏆEnO41O9~=&1=6}-_g?_IMNa1۲[Lz=xhuPrDu!؁A!C!3M^n#!5ثibP}f:̄{vK=?!搕ѡz>d҃[)lAJ~,,v~|w*!z**WGmZ}ۼI-.5:vTܒ<ՌkW B!B!ITEI+VZ>.?g椯 IDAT~ )xpylolx驥"^:L 4jD_)iiΩ)iC8ջ38jRg#Vk_?zNְX_yRᴽ!#י">=S }l2 W<B5N]X{ik^SZ_x\Ux7b Y%_?z{f圕&;i{O511JPa&op>H!C/}L <;7H\_f6r_b'Rou·OY2U [L& PI/ϬziH\\BO5&/'ơE"z BdSAk֍h#y( Y cfER'fW,BΈyy/ŵnS222yK[H@EwzFN'>vȯ|)+FnR$?CBy^^ !+Y]BYdSF]_W}y??^~ 60nڹ/_|qqqX`mV3R~k҃f˖0dedr H)<ޘ{#7,¸;?; "yyTj'6m <̎B!ѳO}ͦJ&EE)+%$8¸"*[Ux~="#ck,?y\? dJw%_+ݯ;veH #O  ˏ1U|O\3ehs&Ŕ{)&+Y ZTe+K@zf򲬿j'#V2KN'K!PWgz6,9oײ慎w=2c:^^x0fύȋ\aEC,֝>`п7`ӎUĄ%|H^'?{p~~[ˆ)_^8)y-_7Gkx(`Lͥ39jZ7za;V!a]t]QwejG!yͭ_rsiI*}r_\zتOS]c3bR*{'hѯ.$97~fFykޯk=/=5)IMvSp"*lMTZٵaa]hϮ_t/s6-))SIFJxVSj;yItڼP ֑)(g}))oZ\,Iq^-=c-ӝMU2&~(/=GᎍZoEo4USn=j5`j-(g}iކ̏7nb[k TR3whڄq+S`4:#N%e|<;K@z7ᩴĈGnWͫ:,"-.-|_&'8 >~_NKĸ&ʠߦAg0|*\$'\5-n3qNWjR=qicKoSi) #Թ$??EyyjgIC7$( OI,93^yiFUG 2od?])_HsWX>19tn<ҲߞlSSA K',̯~PvnXs;杁5~Bn_NU$\@2A\8GfǃBzԛ+H-xR{Ag0hg& io9LJ(XWS'@uћo_ٜȓdZr|f)1;h^3i$[ɮI; y[{Χ斕Y? r LtB<.0Jr+͹!;A#!ﯸыKs>^ػ7V|IRQi٭4PfaGqKM^bsNxvÊ%z|!\;{}eċ{>esm{=vNUJ[VGOlJ*#GT˨,~.vR<B5dTNsSe E-TY*M~\뇬Qgzx9{ݚ&"SjGի)Da{И};,lߤ~KOS^!DR'c42#VX{ w.<3aA%џ֙]癿8H3>pYgΥӖ} Q1X % q', A+ 32.\%״ҁ$-?>7&Ypz7":g۬),=)6$ҾzkՇYjI)鲂UN~GiuoypnI@֐M yIzXEEُ8T6}D';V0lމzգB%]oI=SOW/mow7t!,$gl@xPXH'p?~襫'ywo{y'Ә1F@.:YRN a~Y"b$>(OҋD%eq7**ALap2?m6LIQ*/8 \d2Y*/'NJ~yf˪9&o$%PA/(({+C0-7|hYo**ET'*4\Z\|/;hrb`Ljwk**eϯ|rHGHAy`x.3̐5!kQ⯖IMd3:VA1\gk@n'Ǔ6Ů97kA &go`1F{f4?T.Ev0i]=Y2~@n#v'Hz&h5r~kWr$N!͹j drx}H1f[G7f+o|w`o]kl`*L9Q^^-T=HeB Lf `m ;cΩu(2@-JX:SS{cvu%mzg! 9R&@n5Gu!s͚Q$xWg @U{fuѯEg? ҫMq?wS?}y镼~L1\z;WH rݵN\9䭱- >[&3gȢu.t6? $.VQӖ*%t::KJ_D T@3_suy΢I,_0`|?T{F&ȂK:yUW ZnęorB0{}̊ ůtpO('+2f_U-&ˆk-.Ud+G[Hr^*ߠD `<ảJOݜ? K C618L"u.l~~>LcW7 d>yw[|rM't|]s ^8L1bW|sXpAmv[# =`Vqq1M^o.N(1Mm nbtEC2"?1#%E[-5 I 1ve{N\8䂾M U3Fc@RTѽ-qNʺ -9[Lφ%GSZv]MQMOQNuȘD>Xn4F^خV葕W7>LiIst! ϭݑiyCbK_[pe_eq_Nvݵl ݯ=uhmŸoP'VEֶAGc-)=da݀G }|Ov+as6 `&ܳXghI-UA':{,.C{ioD8Oқ {݄] b@Fct.6#sB9ĻkdeǷ*O~=bu)ֱk9X90щ2 #{7&]teq!58jLggNQ{mRm5}J !+C"޶3(SӞ0½{uYfնʟ\]BmnʾEx j'm6w֢:Uui;Pku%7;pBm@!BIycKJXz'|Rj'(]ze*=PVK7]iyeA_.Jj4N<=fԤ$mg3Rxla-Omk:y÷UkUo!B!^uY^ʕ6&xC7B!}'BN}dzt`dfľ<^|);OvȺỎ.3'e[ Ԋd֜ MJKws8j!ϐꕶΎ z!6*՚x6}h׬zz`T=|=En3cx8QE[%]|۸Л+4+ ,,&k( 3\zd) ?j~UsK̝1pJCFtځԖgy{j#7؞98ZH;7_gh @‚Pxct S'n%Rǭ;:{x>=cӎ+aپ'wpx:) ;Z@cJj=~[VҲn0uFF˳%f@FLIp.mnĻk1_0@N #r_pnXrKM{/F5ni]v:`z dVoPYAgą\]^{2iX-6x琺Z~'yfLS M:,顶߄R#/@;ŧxz>ˣ3%N.oޜ_ 2]N/xeV >BүlYqͩ1ڈ= C+'T7tWtVr U֐N'?fx?~Ͱ_RG|k@A&^zJKIuxft2n|Π%FM1<;!190!P\Ȕ58LSPh7|2ړ X~Ͼ?QyyP+fFxA QFutE5rYYYPʵ+*FR yN;s:_]u{wHhDd? G1ݻwu]= z3@R3Гf?$~/QY%9IJ> –PMk@oI ʐjݻј#(I~Z q$ dV6&>+H2[z @E Ry~RfZ/%4-gj3\G1I 7rʇw5"%ɻJ N,`TV2D"U|)]* d90APuItks#KS|\؜Xc{k/۩@A= x. .Zw3WpYL"7^Y9pr6Xtu'Qg*LpLOa5`~Mw.[ ;yROVK}sݳdG*+WGIjx3hͦ \vүo!)O݃Ja^:N*F Wgп|Mj4ff)_?%39DbX ?~˘:+~b*i-nˋ~Tt?TlY.}NPBS_0a4no?sPndy|`HJJ$͍BԞ<=[c &y98V}S="cݨ-/Y{홛 )%I!3A=;@նE>cϡ1}I/ ڣMB]}_˫v%H22u~R\}%vgߘ{c Ct,,*P"D֜p`E= R?(V8 BJ֗/Q#kk#@hwKbσT5|y\'󇢤`_~}7a`~']΂a3&)ѩu5$[T{\f} ^Z&un2,6IvN[-c=|kgΝC¤GGw>O\}ng'rT=z;N۷Չf'ۓ+t/7ɫ`Ǘx -!nO¦cFzyh-^N ^W=^\xIAg6qDoW5/zRCmtsݳR<춯pw I/qeh犣aYG 4U y8:yAϷNI{qmSӞ,.K7nQ->S/.V*{sب^yG)B® 7ݎ9MVA6nnGOR"=h :RfwՁ$l|]?Wl+(j<)Bgk+xvc@+,sLGF"Zq䪽E704{o-J=OJc{jFӵP[vvK_w'o8`-@ʒ'[B)] Bp@u\9N&4ڳ}M>Uk 99;#ZބoEV(&lnJ'GH4">}d[?bn5/CA OݞOwusikE!x_{찜h, 7*-YV[ d`q9QZ<\i΍\ʢ*4i^> %rF,Qpmy5фl<n"2]؂yǟm%A~LJ&o*&6~b.O`x(wٌ3A1"OWW%Ϡʗ+S\RJӇWYeÐ􄘀K'zZyLKl"ŹD92i*z` _{um&2.3J 6+=!v|jg.ʲb</-ˉx.]M]YK>덏'9~ )Aז-G0R;G[E޷o"ezwRc"0/W_J_}>5:5%{'g3?΀ B6So󇂈7nǦ،7/?*bR^ OJUvMV^t.AffdQ#JIKsȓطj̤ M%@j4ǔgǩP$7\@^Ġ&6Y_1~+k(GWy' VckX"gZf*#^~*~WU.CI*O<>U`_݂JRM]k3 Oi=vV/ͪSLl! +%ߝ YnPc.yH涼=+'V6ep.LπUsE9&'9y6aP}sM&4^j+RJd [ 5u /eP9ݎnQ / TNo0 _Q]=}esȵEwPZZ&&&z!hqoZzA~O2;1@D" e~k_ Mk|W|uK3SMC>$W}[TRTNR+ؗ"v  >ogV5مEU;Nވ)d| K~x? 56& 5T`NLM L&D"K V^ߞ +`U}#=HG*t8U09G%=cX|gf&LU{[ 3VKdgZQzlݼ_Aɼ'|uRo?Xݙqbɟ)E}?]qQYZ* q麊N<1f΅ Mħ(-˾|)\ڀKh||L3L-K䪧W]pVU.N:pe6JW6ƢF5&e< B摔3mR7?mĘ+*Rݽ6m}]w<&>{;O_ dWd8'j>~{ %K },j"vM#X,~ yWfZoqD&հ{ug%9PJ©A_YPa;P(<gTE(`U Y]$B8$LިSSWˌO~>񺳮DW?5I[P\PP\PP\mOxӣeF2eɥxj:1C 0Tkc;?fp #DG5|)١```{E5 ţ"֔ӕ5x.[ qRFM(HaG\Qguй%#1H&g=_CElg6wwyqZVzV(8U)Z)'b^~`ūk[ͧbإ+/}o+K[n6{OqG}n6~X-"-k?o}t"?lPϮ[q6x>}')E }m}/^&B-vK(lOh|y̫Ů%}:0q.߽۶ͬ(Kl2cF>'-lWzB|VOqѷQ8~(dsEZPd. I/:~IM.m>TU6|vsaA0e~oOڞXlR=-~z+ -Ej eW4}aMwv{:ݸfX9?ȿ%Sh X^]>a{|iX݀XSc:K"i]/A"Ut 3)@@HZc_?z$>s/`0NJp߼gu7ͤ*ޮW;o50XAyH! SD@@@@@@@@@@Kfj?l3>Ԁׂ? 08@K^a2?A7"soۛz'DrHp魻%IR?8j(/~qK&KwHXj?*v{B iÓ)7݀D o!{!6 "u7Pz\33%mqQTꉧ5X%-H OǨz)kiEp_TWL%mx*\cis(=m*%W˭˓O$tŸ3V~>n桄ա'^8Xs=!Uޭ-"]`]}g`˴~)GjCB_cԤmQgMPu5GX>m!\ꨌ0ߝ>Үvs" uEE~/>Q 6npc9Q;&fk+1V@`AVbZh@U~lRm3CeqJpL'f#:<@Gk!ZGu(GKk3y13}}deӊ%KizB度iOR9G6v&iFmkKes1cz#jrs]#Aok WWyN-*(>Tes+]o=͵8PqjOC7, 0[rSli/ٙY-4ߢCD?UO>< ^<$xZY ~EHjN;&ɾn'ƳF3 h)ς3$\8Z C¦D}NI)̍ 9lioPr6ᱼFy?˼[}V\tUgʨOnH+r5"fpÕwYC]Z#ujF5{.@ eh 5N1(!'\l@xy[ B *5㜔'(PKj%jRfs13LGI!mٜ&5FJbf!.:ԴZP[Z =t _bM*4`f1PJD*.b_Rچ>aC eriBqk67BcA.>0J}˻D(U|YzǞ@6ъ.]|~ ,Gtvo謴ؠJBF"ǜm>u֬9_z$3Ǣ(b۴z8[O:hMmiŋ^Nŷ^\N$ȉA^ŷhiSԨ_޵4$4!~N*#[5?J.(xz_FioD8(ji~ji*{NԒE6556U466xE-/2W":" ףha]]6.}5} kd{oh6 \Ny',>SZםf܎ qxt&єېV- 1T+ X! JM9`gj3*/6 T &ɬZ* h83KНBlK-dtg Fk =~WWصOOJ?nʧ42 P)% }эW=_ݸS|p(4`*BI{u#{IMCgOXeygKH{d։klnhe H*aUt1X,c3ihvar9^_-?el֤V%1VS5Rhs]W `ͣ8D-/7 u΁kzx g⴦y h!>jkQ%P* p[Dx5jc8Ъs"C~)ϼ 䥦L&pIOfDMMxBnj D 7s0D%$,AT>Ϋ9wGγ:Le.kmqr@`Wr\2f\SVFYٜ4__SZtœ^+g6]K\\eMx{'[0dJM1h icy^ %rE!)1h->IXӐh%hQsa}W>8+`4%M:d31 ;ra6~Zқ4A{ؘUg\Y;F`hB\]or1ze Lkef (w7mPWQ為,n 5놢+MdsFd&[S{XWQ/E ц=Ɲ{tȋ̠cۓ/(j=g#~"N =*C/m[jjǖ<_M|9t%viz'o{¼ TuGzԹK´vq}ph۽=˜K{Xol(ٙLм@mȏt߲Bl?8_[-9wl(vph uؚwNV0O)쏧>*oN\$rmv] l-gms 27ٳ"oٖ~=~;/MN>4YZiR"$y8K-hx[>(G+J[n-n]JɅd/zkQCXjA]l4W-)dJw}G_"G]+Bsg/b 2]ToMe/) b#R\kK~CҼ:Ҡm}#+}7n=2r_vRefe5HorJgv_MZ}g޿&9("*! S_i3/.]Px 9:D%M_[T~}2bۇTg0Eh/Ͽ:/9߼?*5"ǿ|0N `ni"2}ۦw#)*黊l A;Hӑ؀dk9d #?h >)_NN0&+oC>'Fn?^)[};0kL+;Ɗ8$V+:onDe=AȺ*C\[E^#;F:Fym n_${lh `;@YPLډ'VH$-x%XyEJ}W- @K9Jm]Ʈ?/~f@5{2fj]=Pz$H?|9+TA6Dyyvov?{AS RkwpMA?dCg\@X؜ܯw %]>q©Jj ӂ0@ѳ'@TUQ1yqhzaVr:r=d뻄Œ{Mr>n \#F$ܲY #1n𤂲vpv/8> 504[$c!.G u缏W+TGOg^l.xa{WJJOĞ670bfseYҧuUUu zK]}k]u%o}л+7<y_;}ǁZZM̈́ UN=}/jorK}x+lYAt '4pX*Ғ-Bhpw~`0 gSvQ 4@C@GJ'T-[ RN7ZL| Fq]Z6ir橋T}g1eT%:`}ub[Eq@NVBml)kO_+<e7fj ml"?&)iN>^~g+aUJVwG4|4'>Y>}EV"8ækfY|^&S]~S 7^ֶd<]hU;Y1'#ۯ R)ii(R'0'(m⵶C>RZBc$SM:T^(",Q~Wqb4~{E>l*7756V665xM͊ө>aZœ ԣ(Pڴ䈝!{[QC4}Bc/(u[˳8^? PD77Lfy{IfqF5,ӚQ4Vv?FA}m '5o=.(E}=a(~|;t{ExMZ5ӞC9z B >GwtuPy|r\h 9w9Ye_tx}C\D >PUB&DsU͚ 'ZC<>K? O9'_6PAڶ16l~c$C=m :(aƛ+N:8\=N|zʁ@mj]`hF׆:V0t @PW gHL}tZ (,FEapl ;h~U-/ jcQ\Acq8y[ƌBh{Evh/h4p8\JmKI;bj Φ#fվ\k;d_cA,wsqR{egC c>AKchOm~y֕ _lFqXvR6p?g-4h EnspBݥ7AfP0(Lcݍu,Jo@ P(TR)@ k҄F.ݽcЄBcQwl_Fsv+VO髚/WTҭ援S-W8R:NCӺzv=_钫G]=ˌ˛@&Őа`=#^H8ʔ=aTZ}skK+Z GnmxFڽUa4|D@EO'J ʫj(OWnJe` Hy4!p g IoM#+~yTMd'ٰGgk_:Vfp P0XY7|nUʠ FEXPh Ʃ/kyT`4קHjn`8 y ?T]3':2îu:*ST4u1,i_}:ߘY -^5s@Rn۝!yM?Zе.W#4Eg3#9>FrHl4\'2jp엩bx+(F!x #&r֦1Sd^0,h4W|1m;|wu3vzM]_\Ǝ IDATIkYX^+4@f\E<^)MIv`|&+NTT ߤY{䖔vCs𯑨0kFV]An%pg+U#Ơa>Af_cH#N\I)b:vxxL7Z*_P"秧K͟Z&h5JUɒMp?jb XYV3/=]b­YU衹KuPq|^F;c=# NڂN̯^&^md c߬H&e9G.1wVDlwr37m诶e-jH-N)v咃B%NCExD4c$Oõ4r2YW _vz:Ț0A^|\qiv[}0Zع:րd>54X]~B!-ʭTZYsء Nuz|''Ck59viP^~㽏c4Gݶum _>_woSզ'P}l҃NGt]n?ھ/Ef6g5ˣ!u{{ڧЫid ^\zF5F{lHΑnzu1|i{vc\sDf$*)jf1ܸ/΀fB%ڀW4G;t ̘z™Y}opmBr+IT2;?F]W;*D:m:h޽>{轚+9ӝDKlpv.XgN|H 49("scc79vƖݪH:rQS&JE%؟,Nw#Ku} #@jڞ {F).L4V߱~ >{@k+ڗN}Jd[q&1g6wwyqZW˭݂'j~vN-#v$;aS8%U>;aodۍ =/Sb'$E  #L c'9^8ۿou1p-=!3qƬ~Q(ʖ 9ڋ6B߳-*#|Lϡ!=Af2' JRi?]4 闢V}/i<N o׿"'>0ڎBe0@reWr s6 jN3-kֲSF5 f,k^7O8+)C~g4BMAz:O @@@@@@@@@@@9H‹Q!!.0oo}l5?e&}~® k1%g?Mㆶ eDmƻJAl  =M|y^GE#LoOUĄG\_)^62~n9OrGrQ$kqąwkw2@ pO?t_mY35F}4/|8J0 pF("`͝Kp.3{QzB YSueq-). =d뻄Œ{M{IP}MtNiaaj#O/ҋ]$]A햍r*JU$:1Sr cNgVU֐*J*~w>$'=~lJnjd' g%\:o^c|8.7aBrb}ׄ-w-)-R4vRɅk`]zzʃgl]b~LQh\vqeuOR w?41]B4{_J)`āN/)xpEsLݯI4_M7}?^%짘rqؚN?YS.[2wFY^7+Nn/u]ZճJR"+]vXNIgth}4O{j\фUm&>0 ɌG}zP@k-:Ԃ7tD,wIy!M9B78hYdb Ivw:[5l.z4\{/=/yTtn,Kklz}jeMN +iDꠜ4-'6m}v5Tv%SX; \vQx9ޮwGMZkz܆<)%$DA"vrC.oQ?3䇈@Mz)n ?Wel;KaDnih"3 zR 2A>p1y/jwDL.tv uzAŊsN嘯rZiZ~2kX;Ph}^x{y"o^'~u(JiK/ |Lٵ#ZIG>1Mclq (k⛻glCK~u1Ì>WclDK1M@Np*,&$M}9" ;i_?c2\pyX 1}ߧ3gy3Z!!/1Q!_&;g ucB% 9Ɯ}E{ŁNo*( Da"1k XHPCQBzshI}UDԼږsJ᦭akiUq ~m_0<<75Tf4Z~wTvaVZ[V3Z~wdvI~N쩩sYn9ARWU\ʠշUWq$F=Fjid*˩@027_-808QA{gx62r] j^ԃ_KBoy50 cG@޶ ?\| iiԧV@COHNЊvMH:w-ka^GF=C3'!Cr3M>pX4yi2hzn4S3nAghNiK:#I>lRxc|= +8RM.?'!3oª L;S|kն̠W3l>@H=eWI O dPHd).҆R%N"L~O9DyZa)%cRXA=YƀyAD޻nY}b09Qrfڨ;DdӦGGFnWP]z3@%?"-{ sҋ,nqotvB""T̜wp(,"ZWKfB~QU-ĊWǙAHD5WW[+"* y/<݌=*sӳ~۳+Z9}I|sݿ~pDֆsqXl]JsƗԝPx>{@ŋ/sɳ8FI8PPkcT/BI hna0jj38N˨PS>45kDFaՋ熌Ui=gMk}ѕ j3?=rpmľiץmN{&'_ ϯ, ǐ"/ivS^]2]rZ.RXcǥ!o?״6}!0xIo龊zěZ}/ϟW?7v3c,5hhZs"[S)Qh -?4NV[Ae]?  JJjRmY4|x{::g]QP){[EԦw_۰dY;s7DtxfɻzXZDP\zӇkoX(ǿ~yyQyeIg4DWf>nj_m.u4nԨOotх_hg~eеV_acU}vlQ|kwhBcٞry2k䅤QldNǿvmӥK[irJcŶxlO's:\ş9Vp8\KT cDMzYrcWCTݸ["?R}#,[kPsd{>8!Kw[❜ ?NwzSԌyC*XmVƺS0c MViiu\" 8QHÝ?,̠`P[3j0Ƭ on\RT\W/o qfXa,%'dpC>GOc~pX,ʶLJ{8h,xJ-voUX kg"#Ty'`0h!Bug!ހ=aHa뗳.4嘡[(=߳LT3v䶽Mf|(zcS0H(9zOPB7˛9_ w. l?Nϛ*A k A4%eX:u56U@~dZwgDj$@[[DܪDD4۷5%7` O.|; :A0O5W_|XGWvJ]a)G~?J-ah~ j!ѫ>YH>^A[=Fmkec87s-r%=ͬdP벒b-3;]~LȢ;z]BTѥegx[kӋuZ 7Dw]I"M0un xAUMYzJgv&T* ahLa{թS#߆7C/#lM`Pep4Hlϯa1{DCȋx<͚j/Jpb~<Bɥl~R!(L']_TWT篮Hfy&`WA܆N{v~h3v-G[PS ;}޸ͯk;VHʨ֝~jeŲFDT1cSG).9{!"8*NR84Nt~I3&Vp*Ȕ4:L@4G̨:`s Y=gI)qQ'l~zyOMjuрVW,^"bb AQHp+4g2rsE0I$QO`4CUOkz0; dVٰU`4~D#:Nto-sbc-#B ϛmRG@;|XeqƬh$'KD jxh;93KH:uRٹ;2թ -asgh$RqyO9]hD&^--;~|Ȉ D|6\i'-nfL{6?ᖟ6~xe~,@h O4\b3L-er|x`?u{\-hjkk+ zX/UۦS\@5UE^s/`쑠+sWKagt8lj~VQZMn˓N>Rq,sva3bK8`%%[ŽrbMG3([YY-Լ5N!ũW} fZ${t_ueO? ZUMݷ+bIg m{n/c5k f,Q0@Ѝ̊mzz;%9>Wy"Z`QbT6iml5O8z1` 8S:DZ:G8&={8]~p^1I.sL>m?3Y@ãf;5{9zr!h,>#N2 __ι:v=nu=. v0D=X^o@e!EJ.bwqQ5]?wXznlPTnTDEEFE lD%~ ^]=sܹ5qfY|]1@OޢҺg^8`dos,FY><ȍ9Ma};6dwi.Z'#3Xoϯ>P9^-YbE\^@ݶvN*6;亸(9pf╇1vIKN/bLST2sPtח'&L)U"vN=0t2NS.VS#1<5z?7t~ǮC/@n5uZo-(=lz$@K}tmpfɠ'+3k_/.DQwW28.WB8KS[2v`V  !UI/x{*4ZQu:K 73>pS7"pO?a:U>,idFxCJ|Iyq-/"kn)O}l \Wbzm}jV!o3ȅ.]zW8bk汊oN󼈷Ml ts6o[?vl-C{sVZ۔x/Kgty;3m oܹrto Nї{G.GрKl֒/ç{+#fwW/_5~Y%~7$JmsF :*%yCbUY~Φp[OnjdO$z \W* lԺ~²@/Tj%3709HKBRpe{s x" ̃V8¦vO9Zh ҒB>p*8bu~TVλ׹GРID)ok 9-kFoMcC]xH$w 77WT2-%'!䗹oN~"mWSNK<]_ᛁ5:Bs89wN|~&`l,zkƠcd3js{Vg&T|mܽ:٬xr:ZJnlsok@x^3N]|~wH;?8NX8~kY7V|ӞU6GϬ|W2Ύ(@DrlzUGyfy!{x5DᓑPä5' x- FMf Hu Rtp0cg EHg:P&:a=5N;7d''F_n&lf%tp@}#|گxůf]E;`ߎ#8&kv0ByxQ.vT{[d$_O伐J5TW5y,ؤ##.>R 4Cz| O#鳼VN>9No:Q+IG#Ÿo\ q57Tta Z>X\QHRb'6p+ë!|O9\2p* 9 w|VF5=OH!DQ)R~:},&)&Zv&? D*Jhq97N/Yt[d- }%ǮWtIA(0"#vوI ÑdŐh= 2'}=ɪJNnIj*I|/3iNO8Zl~V|Vy  RӎX6b̘Mi<֛ ;YU\W\Z/v|+.M5{zbO/ݔ*TE= joOQn <8+ è؜kQWNkhBti',@4r~̷[2-kPZ}w;{|{815A+;T*te ;ˋ82:vL˽Utkys@ĩ #ـxDZY^U+_'NyYA% 9yGu̦.T)5"?̞;텂feV69s;M.aX]ǓưoͽWˋuJn{KaHT]f"E"flӒ}:AՍY^Dqgo^D&,+ڙYG!s kA81G&GͱN}  ׳Ksyj6J daK 6[ ngw7 u_0PUUp8"Z#JʪTa>H$Ǎ3d29U4-^KzڐGs'O'#( G9ʤ%T8y=+[!ErBﯺW/;j>6Njo?b77Sx$ϟL`RS?0nqy#>e'?}Fǝ\B9*/ʨ; fzӍKNwٮbh4aᦸ^aaJee%@mu5UNkr"9`6LMi\67v_b:fMaX)sʢ)5Ϛ=X`HKW~_DF# G؈hX;JΓʊ X脀D~~C ^կ/? بD3L5&:nAAy@7s9}%Ȃ CmJ}Jc! [+IVXǎ8_%g,LA '>@T]_X'9G'_ qAf( ӦJiq\4mk&+KX%e՜9e "(5xqCjr9_xyGsdU@? c"-WCቿr9sF dMLLL˿/U8dad|$:~ *Y@fм(c{Ђ!Z2DVEAaew3#>^n(M<Mb1PVפkG߮ OL3ufp^_8ƸY 8R3߽A$\&pQa=.E9\6pQ.i{pѿPȳ@*'OSE[IO#^0vz@+E 9w"/$&~X Qcbg'm"= ;(;G+~ a`rM4c=o^vbYr(Zq遫vDGvo/ 6ͷnM}ݘk?jl]RpMů-}}[iCwe Oc`````````` lJžwd,XH]~/hYBD~M<\5niBTvQЃ;wO&D ;{r-ӴvO)UǛzL{֚= Y4Խy-/71j]7 I{F|_xj\*Bf?{?Aݣ34d s T~6TE\<^-H}l"!7Ħn}Zs'6Q:{/k'* =]]K~c=,`n~~Q:no92PJ510[?XRY^X|xN4ZSO|(M ҨkzU1_^2C]Hv{62{fF7N@a Z qu2_qOΎ>Pk3ěQRQ^XVё ]o^7۔_=ҡW׿&5~̓)[WH=9K!^g˼{V$qbj&h؞q <[@ 8WOOX {u5{IE>b:;/_|!Eᗎ1yb<,B][8ܜ@_q^Y'9kH@)uy#S tۀӔ]OMVT5tHxѭ&*I&k#ymTO>q}D4;pTusrκ1HnY]uyM i.CᷩEy>3g<<\rN$|kST ݨU),v>ct )ٰ6}BNr_|@LViӱW>SUw۶7 i箽]ij@ٕqZ{wlLW 0@Q>B>*%59p}ИBwώ.3'O & ihѣ3P"YD¨cUgRd*9*WSo=VWWX:+4fZa~Ojb``>fh7O[S:|fM?*+#hGBOl0/Y"nKq>X1+{TP&EED". EkKvi(cCm-,H%ZXx+Z_:g:FO0.$7`B¬aZ 3~%56 fz!za< I >VdžHvMx^H/+hu ؚRrnM GlL_{vY:ȰMSS cX"f0`ˬi+6_I3֥fPde![[ d rrQ?5roLXY Y7owvs@nDڕӦ\v+genEfΌs] IDAT$"\:oU^ 6;g@#{K)n1ܗ-,-,.ulDqN-& yïڗ'.Ej/A= (u6D"?UuޫZ-xk?V "dY 68+r9ZMa Hu4>y(ʪbR,(WdU5t^C<;%q b3]M#8Zg uIRiv-A}}HI>3KP6ae8ʧWjZs FgQ. DP6}ŚduLJRAho/ M:2/LC1wʗ.^*nh:y' z|̙5An$M2 T}TDXlL#<(cFh`%4H6p?ՌBF"Q5Y(?MNƺ/r=V_WKdhҗ fj,1 9WZ.k! LFj2?>:ze%HҕP1%&+C)9WSk_rPSXHGp?Vه*66;VҬBGe_Ek{E] uV3TəSyïڗ'p㢣 A$Y d2Ste;c$ $ǬU@ .M̔xZԞ5&!D%)(&MrΎm2Msiã EOG~I$_\5wݤ: EDp 󄕡`n}h?ywԝ֊7t4oBlmZe~i w79)q\҂]}kW\kbF!cs^;7j|ܘ8$眞Lg1x|ݛݓO&L&/a)Y)O%g_%U#\@PwjD6 &ie؇E}OTO!1-WIܑ<Ʀzn"k<{"2a_ӸaXRMj0%r["`u 6C]w淵D"4W\"P]]Kpi%Bu@*+-#BtPJ<56r9dX "k;aHI'F2@ O`QTUU`XZZM b=lOtƍ3|k&1ϢuwT-fU +FyuJL^h)o\F_po}oTVTEHLPRȏ/^h}W @RF[`uVp Y2hLl`=US, c2iBT>wag(:6GFPPC"SL 8fb$W{U#:ѱ& BJ_AW&LLQױf@⺓Y޿I@nXee$AdUp߉9TS㺴4l 77Ko;$!)ExҦ:m \7vW/k2DSRRe֞Ȥ P$'n;`/Zw)IUj@ ?.9AhlN\T.-'K߇YF1 (DƉeeX4ПmnڄH)A(T[fgw-|fX5b E<&!NUN䥳% ۽qJ:R 4_p$;?gO O1CQ.Tx*:|k=#󑅚v8(Zh J~S &/8XE/>CE{: /YhS|lPN+`YCUARإ-9 n: @sAM/;QL_۴WxPzSW|VB=вS$)ͣW r&{@3Y"gEJzS  rkZGM];ǧ7F^ X 4.3^+ۥ-a|UYORIz Ӆ2*uny15RH,},Ѣ%=8-8 ]%? ]ΑwTrLּq-hY6АdO|m3sJ=. ߉Ͽbrk'%Bqm57K%{.9'dʬ)rk#xnǝKAĔH-G{$`ANBO^o R.8xn˻÷<f3: Dff/R7ڐo?ϏTka};sL4 Qo8} ΂0000000000YprsWZ8TWu ;iK~\jFm#:Nܼ73w~xWwFd1NmÛƫ8ϒ1HNo]yr:w][< /D~oq/#\op'f+Ց:vsۈqtoΔﴟw?]ͮ ~C@WjљP2kޭ޶S*5ը3mn]fШ;D[dm.;l91ƘѧT#,>F_ ^l+N7"zmg!Us?q/-J˺`ū"8' c +io7trkK'oO5DnjĐ+)|A+:Nn7"cLT>9?;;>B߿2J* J3>?:2A?\T7_Yi1Wmn|K)0;3u%Sv;ǹ7ZMz~XO5>ى6Jt;3rcK鋍^6Q$no^{hHbBK7T&:dL'<1EĉKJEv4ԏgse%R"gȍ&f/]µKKf]][*Dx.o6I6D @j_6. ~4N~0Iҕ[G6}v^2g{dbLmW;*|Ls )I٣hF6^}ߧ~A^.u!߁DtYA ^~Ԇfoy5ލ J][wwfc,)u<>mb۫ʦ[Mq.~tg8*_r}/6ъyj"ל}k/'),~AkkrPP Gܫ㔽)Q\h>7\tB7la]}j2Vf3vQOq×]>ўQX65Bՙ$I`}.>,J).zAIչ\TcfF]4PEF9iDXϴ< *[;krf>a#>Ł^{8Ȏ909vהZk-7+9"?Lxrak ;T\G%8$'d$=迵所~דh e SxBi&Ϗ]޻&N㓬@ 7mkV>HKϿNL/*Jue0@]iVGS ӿ]GZ N7k:T7K..:TÆ6~*聯è%v8yOK5j(広Kp"&QS]Kv#N$* J0@N$,A*e.VVN9,}@GqGrq83gkj̈.+WxE ٧m/xԚj\ie{rNr:(m/礼-.u2߁0Kۗ)%JY߳hND#J/={dNPͤsfXYzwAaD7iN3i}5[, p1l,q+V[;OJSf.][?e_ [;͸ӶeVh/=@-%)=qȼ#ϟw<A((tF~Q eU(#w((ʈT~e/OvEsp~BP3V+@J1w tiO{53d9u5)LVݐp, BZ򌺂Q4VofZB7;$! PD75ߘ~$2vGk$?dE|İW޴nj{[rFǭ+-[1^&2#:tqh|>y1iDi5x5A1q@SeգhuDebS3vIZ!sUJ CZwfs\ d9UVJXɥEWe:nQffm.$!:ȡ o:BpADB5ٟO^|Rm{[֦ŧ4@ %ɏ+"?rZOl識FE4:).FLCm#QUJ$"+IMC8\z9οl[3~$c\lw$] lK (PdDP! )*Cp8i|Yz->=.)#+Mώd0 חJc E ;RmqƊ7L!AIY#rP.(GF9l{鷀94SEݨQ󨪂"[!PGZ< j6CY]M˰T99>c<3iEYO¸k= 2%x:7[gDp^'Vu@\=>ݽh٢E-s:Z.;pι2sj M:v@'޸y#$}O/Wf@6Ԯԏҟ:[=#+sڵg'(ς/Y5MW4T*yjHxm&EO=;r=:`V Bjm)\E@2ۤkk`X.ṃm:<%9oQAEusݑkC7k5R䌗n3 o^:mó]p8B S6[h@$Lsj8%|+C".ل2'i 5~ Y[!3v/L.ꪼfW(3gk;]]TG/M8s.EM2plbF QAw} h "(/1e*? Jm}L'ٰl<44{GpG^~q_<~m0z9GJ^P4i3$D 6n ,&NL ^u~W^@{D 0)쨖^ZRoRIûYOA$rܸ 15x|h.߆gpYlTFf 3BVY򐑑  y֏E "(pp! IDATG8TOA@de@,o:JTvqAU+|~C-ujC~ʶP@QJ㕖 UN1AsE{'q:YܫP}ց.<sy4yڻ~ 5JI(GٵyV4U93@+ʹxQT3橹ZbL&Y]{wrY4m.ΐ5cǖ4I~÷w~#* /@hke!WP.5?8p$EiSK')z:`8|g'=D9O Yu_d]p߯s-XrRBNC04gЫQWNS_"x@Ũ2>ɥGilvQȢ棘+37弡נ(:JI[b;TRi_HLdo3jL/FjY8 "̪_:ONM_t6n䥖E~ʙG>P{@UYۇ_MDoՔf{ ?1q f3m_F@ {T7_f;t(ŌL.:  JJ*/^{N+z;uIkGl3đ7N6mV&;VUO F3&w_ӂ ZM<L&r3d56}nIe=>X2rxqj 񈑄0@Niic|p8dCWHJJpr?$-,Nf}?h֒K-4t6QpXbC"(obRױ[gXYϰ1|B}+;l#$ji|4yf$,,ʪ(z3LZ.$(hUPCT7"R6(7ux'd)gҳ2M8AeiNN Ѧ@!!"NH\(ȭ2`/QU*AD,N&M,2B߷j KH_fc(Nt\:@A.tf(t!33lOAV< :nî)|/\aVP-QYt''14cǩd[˖(_g[0P ɋh2ˋiAZoQ~uy^ZoJ*3']xЪ}WT$–խЄ" 8P68<-wC^`y}280ŋ/^H(0/KnC @i3!m7m-\D7Ĕ|"﵋IZTFj@uэF>3gP3{́Ku序=w kgN`G\:mfɉVx=^@7 ɾ `|škJʜpO-! y?Ԣv}DӪ fpi(..6Z3pst4t*[H[F+[B189w-6W'1yN^FO?Jh`nTtSz vg^'E85n]M.7(`T~^l}uI߄nj}~KO7rl]{$3cv?6`y(s_mw;gDҔ˫6-[gPϝN܊;;m 3O0c;{qgImowNs?Fo#cV}zݥU#cݧ|mnqbG%~ׇ%?{܅Y:} ّL5|Iao.q#-ɯKv1Μ|Vbw)6"eF8'4EQ@97tr+8 ?CGm?EQ00000000000`kU"v "th ?```````````tQxeMBDa.Wgƭ}8xrХS;=GJ7/j#6%cVNo7"A JMk]]Z{&߁Ӯ 7{' ]4DuĚ~[Ԟ8ϴ@KCt.]9ugJ_K?/?EyglZ}&+bMCfJH{VSFHR;̀Vl5oq͟ lꊒ_e{y\RTuc F.Ť奇ޱ9}(?Kd ,:xn9*~,fzgY쒬9@JQQX[; /糌`6k|ww GyTg)VmJgBȵ=Ǎ2Qh-m=Bnrdjk!Kg*K  E}5vě_~{qAe"DAι wM JX.s?3Mhq/Gjw(ixTo>=W=xعSE:~Ϗ}{nl͚^ Yt:_CQk&݌¼(@uQv}uޤ&񷏖 .:"}tA*SM97(wqP)i*볢k4hw)4wj(z9+lcƇ DQa2=KE^nHl!;t ebh+M fa7V77^Os7JMD:7GGJQ0]Y ,7j.UDg̤~G ٤D{U*x53w)`9[kO.%\on?c27Rb\BcC2ޔrIONRh h9H :Z7H_^JVU^+ +]cAg=&*=Jk4mٿI jZTKTĽl6 Srz) S%&:C̖Ng|HaW҂S~ f$+toDpN"^F 7A6D<Bڣw^5j˟w#Ɵ{R{}C8Q{MU;z_ ['DJ92uf?h~;1zjMzka=e1N6] փrYAF2ZѼ c٦B¯n?Ʌ_Ie$< /o zγuZ&Mc< *d}>^tĞݵG&BSw=J, 2mt]@g "(bYV=<0'.﷏u6 ++'[q4œ<hHB6nx#ɪ/~Oу4iF8V}v%@IΈ\P\0?>"k]H'j4M >]O#2M^ŚzMH$})>jhq FN%򟆞bۜv'|gɂvgZK7 ai)p%g lGlǦƠjmZ},`X[<d-3iк2t;?jߓЎMA𷽖/=w !1:Z@LBt& wVL2ILx2FtIfvjۊ Wj`s<4B50jȣ|~_M/c1 SnS6sX .|*vL+&NK cٮ?\ߡH\hdDpպC]%5\"ɨ+[w 9[kpDY35.zO/{>rV U;31;τWݼ4X㇣oxKvE! ?e} AvMB_e غIoPP55PRfc,~T"2BQeuR2^}>A 1f]hyШ;,B͐!J8 ͔UTĄVRV5M uj cOW CP<+1MBstÆmϔ\:Δ +d/=+D~`awvJ6 %<zxnr uI!e%DZ_B^$uy \a]b-(4pQ|I9c s>]jkL@mOABVuM; >Z |sUD췹$=ɰ\j`fb:mxIJUOWXx5i}zvKXM)BhvQ!)Zful|Uw3DZ?!:Z}k#"-BO sФ)r\$"(H rL ;"h㾻WSNKk+긺3 BzWXZRJm<쬊~O!8V/sOofӫOKũ fs&^t|}EyÔ{[' -d믅A۩xm$S "0Ct}:jZn1%@D檃PH$ : OUxn F4fʺz9#C.O~(tj#ɦP\RZAFP]ZO76fw4( TµX;6Y2d<'Nl /윟bl'uզkftҧhP"[÷T񿀬wPW%@:k;ovG%g$g͖s3eF[vi5U8&~+7y]_= ysVZBSRDbxl")%e6@H u_˞h_E3}5ѲBV99ف☣6^`vAeG0~64MFi paXGc}q[{<>?7`EIAV[WMN%ABBjhD"a_ 5|[^%˙3}a2lQEJ8L@MUUښI9]bqqלC%/^vngS(Zߵk_TPJ:t~Yiy]*Pr 9JDX)sʚ:YXUVwQ%)d5$e& H¸4'g'`U( Hg0#jZ:@؃6UMBͪnYXj!Z#j)OЫkmȎiUM9BPRĥ|l5AѮ;d$&Fh=ien\|5P5U2f6ʲJZ bG? md*v=-Tq5$ր3Gt!!iZ閗^GՎ?c{ϋཧx{~ABNQ(X:r %-e/c3TJJ'oMQвXx}0oJJu!{u2/VI_e{wWPSPPSPPχk3N={A6҄:HsܘvvXsD7I =F5@jJ0[{TtI7g[ρ ~Ŧ㣊|=xY㪥@|te;ϳoo̘}v$<3(.BEmŵḒgW8eimYJ5TĎ}c%/l:\tD7Λz,wTe L/Xϑq24GEJߏ;|UqرYة_@ZUXƜ+VkB2O2Zz0jDAy}D@Y˖ % 5 "M`SW+"7<2ٸEtsBTg-Ta?ٹɣy*Y̰-NdԓR~p+ua_ 0}^Rz6Ep{=8J=tDAO%,eR.z(vEKwO#Gx/[qZW X +mqP.ҲOD"}X>/F }tl'AIyb޼n!KG}Ǎ̶'c1^~#Cq~H=o!N"{mѹ__~[ì1FFV2j}D7_˸w^``[>+iܾRәW@ЀZ6 r }h9{9,7{sb#z?xu%ih)/nt:QUmh]a{-v JWv[~vAdɯeg I]݃@lu#>4~iQu>ߍgna?Io;u8D.jv[nZɦ9 ZmTwϝ HOl-&I|AA]!3×o tqI Sk(*t\?H~U{?Ok<g휉7sd'^s33,vI֜^Bx (6Q>svqH_g6ºHzStTS!CgV߾E6UnˮN/#̪""3L NԼ6nbf\n͸&c-gg =?8SPxNJwMC!sJEjCR- 1ړnvzm%,LЎBka{"t,GQg1|Ym=gK(%"BDQU_%rDDs2e:zA5=zGJsMAqB=s~l/5"YOd +Rtv.8U\(WqLEDr/%Zq Az#-RK{ q>0;vއ`2oBOdD`=+_exAo5L0N>66=:5)%EL^™uh?F6VWU~Q4#q>aGn\_&|% $p>@Qdf&AqÛp †Ѹchiй?K( "=Al HF]qΠ "蠨d}>^tĞݵG&BSw=J, 2mt]@g?Psu& ugPXŁ[ƨ7gZK5m=pᄉJ5O_l}mp\d,_ZlTMW@-'hQNK*9W>P2#(D #qIB&HzN^pW\D}/u4d倏>W8A ؠ,b/kstz9?PKT2cBr- UѕkY ۺ`K+ΙwJTwo:?٪lmyK]~ 3oyTcK?\gw9n9#8)w~7Istk`~ N7BNzH% 2 zy+l4DY* \|LJ5h$ݸ`6RՅd΄whO+uqF8+ "e b@P_[E!XruH H]yYTr7|# Tl K5a9'XIxu QШ٩}&9^xCQYO|d6 - Q\P%eN}]6-D3USP{Bdʠ~Yɝ2Yz g̓|v~C9GagQ"ޱmÖ[8x%Kkbvc]W*B޻D'JjurDe])FT5^WZN@R29(Jʐ5ey"$0*{W/ ybc"Ndqo;Jg}/yrI3N܊V:|@]A=z0\)uڱمEzF@Dʱ'(>. A+r3.WDoȢ^/6\oys7=>'O=/0R$a:E#< UϬf6[::TO @d?:ɲ]ɛR!@勨1^wrв7|B" \s[/#c/@g @IŖ@\nIseH(l ˖}=m١C)Pr7Z N+^S+b>$ MO Ż1"tC|?oUȞ#8V Z|O< Vfrh"&¥i(4 Q`aԔG-6tkl134r_*7 ]s-{B:_$v?t D"QaI< XIqBe<Z¦Uo&[P=7,sw>O(k qn+7?LWtm7vLc4#2=E"v$I5| ;}3}wX.*țԯ#vJg!i:I~aiE':lIYshY^?"DijwSRQ|ldw 7T-y>ODP@$Q_K 3Bnlɉ(a2ٚ@v̉#8@$AEKƮ6}ҮFh#8*lw"7m/.׃e_S#E8\Sp޲¥3ey!7 w;lA T<=%A8@F'u5H=y*gcQ]ZWRxyY)*9vKھHIY" eӨt=cHO)g()֮Ww ڴB f2l&Si5+!+ϥk))S;infm~+Lx8B7uvΌ|N_6N8~r"n%^]f\jO6'su ^}w쒴C lB| Wy14WSIGG8Tj (scIщ9Lh8̞HSJn?q JJҶ띧s8.Q|6E\͇+DB#D|' E&l뒀K!'Hs;QuWpadDɎx=5z{,p !VTj@trlhQ2,g%Z;~w?&*xnW$r/\~U#6X&% os;4ĩ}p9Vl__#d60,jJ֣ HUc =ϯ+Iq*zYjت-'R4DMyJ?VL F…vW?rJ¬ 'lUóe'ԈtSHvs0vWVܫ~NPWwtQwvZY"lY;\E7}XѲ㓛6&*p?c~ʒ'dlDM g ]rڵ.nnF jr_ *g+CϹ1`b/&M{uMee_ձ) @vPg1_[+"8ʬԆ(Yd՛ zv~h~Ls]6·l+b:'ZƗ0fL}]DAe]dPi˖ eZ[MЂ7nw=K]'N*i2DLã}Aj(\1HOEڢu{=fdͩSԋ>_OHRVD]3[̈RmL̗²G"z_, PGV'PA˶٢"( ^yKlޑoC}_#wAc+R??/!t;?}O 5CV^4 ~*"v,RַHUU(/EK;!x_ ƿoA`vVv??O)&(ʤ>j0000000~Xa>OS5gwƟ^```````~E^,:H;we9Ə(Xa?cɒu $?6?Z}.ټwׁ ]ˋDTzݱ豽>.m"S5{bGo  9`DфrS.@5g$]tv@T 㴻ǺvMeY,=`N~/H&<.<Zb'۹нf4l>: C8+J(;l,I}غ4^W?eGNp3Г->eBÅ'+4wTTVUR)ԅh?*^7>L֛hg~5iѢ A[OytvCZwCbA5/5ʝr;y/: ݽ}.jziOԽ}lq*,)(;ёIDѡM ^A{q+oT-*x}uuξn=MIMW-i:ucA(DxwlCxU#pk]s8^֡v1qsn IDAT@ M01oꔻWY F4$jE mAOS\95a#1m뙒!3Cz7-j8ƥy.6HXNY][t)i|)|*Kd ?\j& sڨ!azJ|Ʀw˞^c̵4Ǖ}1;V4k}o` Qu火<\c=I'P~@%}4h=|Qz{'x|Tj[ΔG{ d ;0q\|vgeXuaegK(vf.~Õ\<]zI}|xK 2ۮ l||lʟq׭ Xl>䉍 QUtmO1XŰQDYU\(Kޜ6i<@mM,lLX91Elc h;Wz'ўp)eI2J$t`[АqeB7CUo_*tj_@0vP~^jDKz1r4%ӊ?񟊬5;`ĕ66**kB!;E^WH=ՎڐP>lKЁ=5EYU%h܁cgINs꛹O'9ቚ/v;m2իh$:&RȒahvAiOQgv~:no4 hG1!ol:?r{B27"8 /yВ}x% {vNMa(0ʰIJuI˵Bahs Pqn*Jgl. zc{O- yNnݗݎUxvhS:D;޹[7 x'1~<򬤋YOJfZ#5ѪvmYY-Gdhaԭ[@M`iEp'j2=6W/|Tڽ>@ght`b4@Ul/c`**Ӭ ;}a&;|6Xߌ%O=)1lds)[LW ژx>r.H]/ѶǗ>r#GO*hbsqaR@V$܉oTɓnZu ic{JTɓum_sߝ>n#LBi//DT@Oo|}4& @^u'Է:w'?&K*>*)JGK?m-~Fpp|qd$ՉNVßiB_e غIouUl K5.zOTԆRNu]MJJJ4 tܳU}m*'x(4֙)(=]!2 AeP?N7SS2FL7&#= Hv_2^6|Һ7WTLۙE>6Id9Mk3Mfgujk$^?|H9.8os9q2=h%^ڿءg5;|BR~3BVAӋUUԵ>(&R\8<vrhuxRٲzÍܮ/MP֮Kq 껫JPѩK2xܚ, -9anVᔦs8\UD<.eܢ|TE]+L%ҭ̿7D-MW;LUUlA@@Y^~DaSSRnaO CזW6mz"L^tCgߟq]iID(@x .2Jd#mjin 6?^C:uVũ;rzKzmЌﹺzj`leU4$^+ZV˞݊23%3[} u i}|E$ExD-~}Ly\$c&n7g q$z,6GWfzBgzl) [8dޱo6 zC͛_ n}`hMjzbSPf㋗/i3Q{g,=p($@_i Io*I/Эw.Ӟb|WosPފ3sJOΙ?g9=Ukzo3sB7An&bDc]V>..H<3^n\ݹ%on\].X?{^ >b4If}ޟ-Tu^%Sd *SiB@@;E)8| 9| /4=I(V[E_Kd+zqgd`Qљz1U:ToZlV-%ZK9#Δx!DY"ҳ"4_egƠZnSH/RWs|-募} ,wOQw: 2v ̶ 8yx#H|6}>5j/ 0D2+ DE- mW3ͦWPo_Ywqũ &S_D-OHYWa~;M}P֨cnexW̬fwFB,@RH|X[h" \{H F{M63ߏ Bݳg=Wfs-QU/zûz3FN<9\t,R E3v*u|&\IAF~yUZQD쿘TAݨ aW3નӂs:I3SV>@e3 x07)H|RBRx~^-Rm`%b/DML;Dezwo6xB>ObLP2eFpEijBP,%~{mld&&ɘA8L>8]uf&&wc`}|r2-{9 ]u_=wmφچچچN߆U]ypȎ!GM Ave*no\~X]?Tj=JUKPvb_C\]sƟ3Wksr&UqYjP5ͬOxqu {jg~DG[̌Q/Gc_wey ?Y,xt5*yp+*_q~#+M=Ӻ5G,p!3_ݼ~Ֆlpڈg?vd#-`!/Q/v_,'!367 ɟ>PL|5gt`aFwʗ"19q3' BoϛJ Dy~ 'PIUImgOﷵ $?渣m INǑ|$$$$b~!!!!!!$$$$$$$$$$$ͅ?4w0-̙ҽ'Sk%,~vTr@"!o@g#&^UΕSEG> FW}&. c8e܈BS+[uq^?Xv'F_p)Ui9r)Ef s>mar)y`ZQwIݲfA\<ҹyVu 0B")Ƚ~[(Uii1{AΡ[)Puc݌;9$@0ÚVD_zQ.jf[ϥtI\r0^@q}L[W,+n_p|ft=5dF($ܱڪ8"7o[U䣡݀qÂ(y BZ"S{dmO4_ W3P3 H:(bt~}\t&^çL{3FS7vڇo|XhIkw8lkBDi ~=YL4< o&Q]L*5ҁ)UdoSq⌈h5biRNkFS7h|yWp _&_bMNE|ȷ#N :ӂV SGwIk7%uSapжړ7gh}aLFoNZ='$ -?Tt?qYӽYB @lXt8jsYϋ)|}]!!(( 26>,1f[ND!EV6iןqDGH 3 .D[ Gs")XWB3*\ΊU:dj h>(OKqTuؾޖ,:|SGys_͘ [&#.1P\-Q!hpErh흇\7Px"hh8Jq555~O#!~"vb[u4U\I<6rLo/ **~q .Ԅ ]N.퍍R({Y>]olPqD$(jHd U VEll4\ ERvymfaa8 YVǂ8@A5A`T*a@1?+d S{? ɻ dU$hhI~\lRI㺪r˾FM-!BDXZ$Ġל3u+rS'' vp0b:E9plf4 +ƘS7c0AFGx 9 5ScLZM_&z r"tXw >Æe۴mW'!/FY /V!m-yI=,Fy֭ݴN)Cka@[I S:@aеsb&Va:Lﰡ94Q;0ԥ;|eօMk6zYqmk2P{,/,} XO~YV*OW> v¥\͒ZEe4*rVZ +ivF;[ *oJHekf۔]WWc />8*@z@H3GNH>4:zzaEmGXQOVEؿJ*J%]CYШ-Pt=;{V== ӱojZv\ެ)+6⊬L.Л2KxL )4 Y* W0LLCSY*L,2 LdٷD2dܼQ,(BV}Xzʣ_}X P虙 Uҩ=M9@dd.ҒJ L+_^+ O͂y"\1 ae|( @ PHo"ILh=pM$ALC@x᧒k4LMIWrKT]on&L'R֍Jfowo`MU! a2ۉ IDAT MLIR=uD(f`wGJR48J/1d 363ƴdg^R& T,W.x,7jĴ JQ8F3@b^)lScM>s_J0Ef׎=bu>waa LI~]a[ExG ȪHpB$КFL!WD :#=?r=5_7x~*&,2dSE:]wm0JJAza=(1<'{:*ʤ%ū n (  8Q'a]g,Fs=ޯGDJ,FjT"9W?UDM-2^)ltM',@$AK{i)˳c޼S Ôں$-LJKћiGQ^dK߰}- T! $is_\`lhBH*R@8D%Xe(KL7(W2DTʽ+Ju隯vh-`#{Vl ϙԇ~pb>ৃXǪ+ O,'P䓃 ܪ8bh6WnvquebH@SKK(DHu ,XDyv::mߟ3|B`s8u3 RׂAP" ?&Boq.oQŕj[8s}&i wJٶ ݪ6ae*IO5p*RNVS{"OaLC.cad"t6 WI*HXEerUkO(Omé1^`^'Ph֐O.W'K6UT@ T2vly&7E`7p# [_|hB:tتwy8; j~Ҝޛ x &%"GDkDN=8tgg_e|QK,6Rٲ_ TnCGtbSin|4 ZdfbRP^].in`3Z_i90(#M`ξ΅Q ok p\XPEEzTݬ(j"0 IAQE5jx 7܃JOLT8s(LyYbm̀;}EY]!mpp|4wwc;ԥ_^axws77P@D h eY8T RM \i հ4p0*.8t ЍlM%j4k+K{QҞx'*35ܻq4蘖(Cݠa6Qߦnhʊ^sJ"t:Cakq]QB*dz$$Ng04LNi$$-n;~?&ngGw:Ņsf\Q=0Y4ﹿ{G{q*#<n9vnt77y;׳MFO  g2/oٍqY}(ϴN3dT&yUC%4.: $~~ո1륁 ;+y~kYrbQ oFčY2MAKٷ Wؒ|n2/JLەjuV^LTJxeM2/[gM<ƥ"CxN*'I]7v}3ҿfr_>K,?VQn 4QE(\v)_.2ӋZG㠊 Q zv`90B$ ͉*M?2jH+i8Wu~KVqHPMJol7wV0^pn̋yqo}eAɹv;!,YNV|tݖy~.Dğ|e]~-$*|< cw>ݎ֐|Z0}=az3" ?5u -h__$oE_Om"*V]P"\$3#hѴ֊c$ h%吼3L/|IAc/ ;'Ea3]$$$$$$$ d-"$ޑ.63}OC@?|muT!!!@ / rNi߹֎PM:`嫖Oш^4,zMWɄDt I_}fiBSe>:*1G$Üo00tt}oxO1Lӿ`64fn2`Ϝ;Θف Huk?lr:!TF̖`3T-j~<8u1&|tݖyo-[-[v sEYfx0EܚOG"^O뜤tܢ㫂j/<_qCG)yb7&2sOR rrCLu}:rhLR,'3f-5t|Gw5<;''a\԰Y}SGTԤTzt>uqPipg,T3'fm$HtIlRIzb,6+LVlE'R-(=#ݏ?3eCF%je3>J.Cl퐲|4Q~Veg.CF-# Ic4uӏƗz` ^e ec-lt4ӬQ#W\p/ r6l$̿1o{,6qkGp_'!LiXFyoNsVY}`ǃ9,UN[JM\cN߯}cq3wos9)prإ- [y~ZRX&q_жLA_o[z'͵iE]LꅚFNlLXPUᣫPRեJ)T?{kbHYʣX YNfTnXq6B;>n`K{S QQ<[ \f]}H5[s; k!:^ t11ؓ#JqT4!N `@@O9;N>+ǛaץLй&Cy8 LG\v7qD)"~~Z~QDB![SS]R4̭L%ET ُ~&~?SK,Eϒvmص!>c?[D#saV;VLg$`ԡ<~fH˲ܼI/ vߝ!&:;V=Qڕ>|(Eg%a>پwJO^굇50$MY=u]D`NR H2JaiHEe"u~|Doڴcr'bjSZh .K7{C9Mb>3:Z;u[o_F&UjYpVO,{YSތA&EȫcdUBFJBf37-U0rCE_MwE,`[|j;|[wtesk]=wo;fSE6bD՘ǭ uXq7=7'eԵ==5~@fB*92`pf/ȓxꥋ{p˿@z=/tM̠H_ MLMՄ/VԞ+xlXh=RP{vsI}²l[)6]<_}n? ĕڂĹJ T 1(JBni꽢<_HkTMFUb@bρJ|TƓKtU!f׎&u;H SX*3L]cʔ"9@JJ +}?T Phg R9Uv@KiaQ5o&!9ҫr ˙ _`l,MϠ(׌H21_/="\fyݡ1$*(ɇ? 4mc#-$}n<=o-Ax1%!˫H %9A+0enU~[6aΥ~u(b~ ۉvב-3n;žΓv`äԿHΖώ/P :WS9IN.S9ɐTSn: n(Ԅ9Hڶ @?FuUxш='l2AsvFaB!GQTƗ TP?30ȉC/Ľn & bj4$@;xHb4G 2ŚrQVx01 VJތih1ɻb~1ȑ4C/tج}ҊKsK i%b q' ̰= p(p% ;M?ߙ/>H$$-WsYPl3oo\v~ۺugSe@R -*KE*P}C L~[욽~ =7Ȟ~[\㻖 5sO~s`$l?|Ԋ <KYtH̞}yIjPqxؿCOJ ڳ28uUIx|q9~Bހ_:mDyf-o, hFվ_gCS<@uek.6akjVVTd{[p*;/ AQ ƍWTy90 \}=uuD1[77~b_(ʖg|T(q>w4aaR ZRݘ w_nqI*{31I P_O xitŢkv0Щ{TE(U829GBR@ `kѳ,>Pߢ!]i W(2v~P(X_/L1xܨ@&/1͛Y:hW:cv^UM3x[5쀟M#w5 C >|Bgc Ї} 1c9&&jxtMڽ:ou:x5{'Lh6k_Cnl:] MMR~;Bӵ7x-5 RQa)Deuw BDch -] ʶ5UVsk5)Z4 A4uM.'y N'0mK=1oؗ󌮭d#/)jq~ L@ȴQpM4(T-G'k3Ph#+.$Mp@kwS @Ѷ0T`֤(M թ&hVN64 jV?gGSӺE.xzu aVkg r1q@g⒐|dFFjEΰ="0?:J8nT9hܭ2k~tʴ id4t},rݗjjOm]]YQk\f z0i #)b;*EQT*U pB *?BIP*>a ʝu_ugcQq_fjcl{:gee]w[v؂1W)yzY;kh,%ҍazylD/9 zoioDpAh~v׉?۩DU^1o7l7ѩBBMSnp=q~7&"q3^+SLq꣊N}gja 0\f}K2݀FD`JR"fI]DEe⅌NfmL$G!*-VWc4;oث cd0-lxoAqÏ K||u%i}g $1nZ:̲UOHsBE o W܋{SXx5MODl-[9QNљKT =X=}d]n['3l[e[?XfN)>hh@t:xƳ\p[]UdYCxˬG Va,^0v-ςN2kѡ HZkr痿JnbGoT٠} OoGjo {NqwԪv,*xT $ F q҃5yZ/t2O}v;&LD-hwJ%ATNBBBBBBCTRhʓ9Hފ8O ˿j d*R.MV䦕$$$$$$$$$$$ͅ !!! v%# Is!$$$$$$$$$$$ͥmi-g}W]:Po} }ʼ|3o㺙2:Ot 5lzvĞ㡶Яz;㼯b} ֭M4hlgU:ڞSJoy-CE΋PoRy9}Ŋ9_d><=:~qp"#tVI/O Fۤ$Z!56Fv2`8F Z=P GDT끃K攷r電u-cR IDAT$yl&ھ=YsI9pKjCFyoNsVtmn7'NN]eA{kNͻӒF& ږI5}K/_Y]׀%Gǻ^|g=tVlЙ8GOf::8gvbj(*xv 2k-܏F:[4VG lj>Fzj(\bHX}.?^0Uʠ9΍.Qwk*MD{!DSs?+;c*^Q'#PLoF]]ҌʫlgJ”X]4Ewwը W^otoS}H[^$΋jic+Ȑ’'s ՛ץ&ɈΓ6CI殑x0q~+~%]א`]<7QxxF3|Ie[W-IÓS=]9{[4—eמLmoQ`XnI1<\5qi="+@ǜnKCǚjRWo$ahŘI n}+Vg7#"/Q A[lx˙]oWiFOԘjИT - ;v.^OEz}!J۳I3 ۿKP8&F@a@D$ֆJGX"ig#[Uǵv6lZSƾ:yoE 0~NyYb:/[oG @-nkz/_3Bj;tUf9*} 7q=-Z(bωurp5L&sPgGDʢBS{kewN`|RT^Xs4܎6THFb8ABa0g9{7jK"3PwGRID2ɠH$ӹA,ը0s4`{=oѺ6m(]{D>ϼ0Z. c"skF]JYF4W`a8 YV* ǂ8@A@sH &2-(~pZlMgPi)˗\1.9TsYp w֗(:៭OB^Ei~~!B>ЬR̵(щۘ &hDƆ Pd&$Uw:YfsK ]{do\0#)OP8XI/NXjbS}Qy"Mk:œya{ev&0=}^^8Jao[nkQE? ^&pCÎy"C- I3hZ0 ʞۼkߣ7ڸ^~iNZ`{ _Cjav(:ӣg>6Nh0Ҹޟ2E,u.h6}GxpaNqf}qKTxyAy/8y̌E q (^2XR͗f衁8BCN&xmۑ(z?ΤEpߤRe0ݠS;qp~˧O7XV>?d\̥j0jֆYbil 2l741^x}XIW6@zNFEN&51ѬLP4;va^Ixs',.V޸YP/Ѱ8>piv[XQjP @RbER9E_P/Udl=#Js2l,QN]N&ׇ2bhVII /=h_W̋.9xJ=΄2:5.<>8tU!SW/e" Ӂm I^Ѓ}98[kAʇ)w3ȺM k& QH?KL ~֎ly^yD(EM#~M E=H=D刡e]Ce慎eI,hؿ^4pxpI1\8DU1 xtM07)xlQBRx~^-*VUWl@}uYU3xi)K}盇'A:hښ0͛Y~7_d>5hji\Fƫ -8fҢ SYUbni tGnY9(WIjxi4Z(s: & ^9ԆAfDQ8Y,&$AAY6z&x5^(MibZ++*4mYl=נ^@/KmDFFsl;;+EyIsIu:l l-eŵq.ٙ U s+ R{Jv.L E疪k!:@h:~Sff$֧l}֧G I<RXl 4Ƞ1N0tQ0ibd_UVh9u`)(^KX\ąQg͈з݀ :0'COI4M^k>2c0\f>؏N8wɽ/mռ=IZ yFo\ =M4ߔn1:w:3XV1y(NMpua]".x[Ӱ˳,'-|io].xΨ @QT!Je2P'p\R&S}5F pT# J>R5BuDJX&i( R'pRm]]YQ:zMD<2u.Süל16O+kejcMnЈ-}EBJqդF-@b*2yݙDeqIMy}gcRaBE,~PP4yFsV5S r42N7pg0V7a©J&ewO+ [dQCr-MհD/8 "գTz hb0yay=zZF^_'ݣ~"a&hCr~w,Jfu(~,Ǿ#zRw_U=Spـ;1paq;jg2޵2rjd"AM q+ î\Z>g伨{:xMfʪ;ɕ Mql*^US}^q [%Se%⺧2^^Qsiw|TU{gI&{'!4IQAOXQlvuEQ(JEz(IdL2 3r}o;Viۻy|ͻSUMg6/[ken4vGAPl[ tU׋=5936}~ΕqOh7bi[u_Xꨗ5/zwS拞Ԏ]=k۵ (z+jfx'Ҡ9rOZȚמTwԸcojў߷<G\ge߿ga>X>}p_(wك/Z&z6`a³Z0dhṷ?_[fAg^w%O2d^;bݚ7F lPWkpv >p \9k~;^9>,[Q!c%d΋Q-T(d]|ou7ɽ= [H/)Bg-NGw׵{gĸVrL;&#hs}4'OTcn+Ke$#ȝ@ ĭFW'F_Wig>ZnEu~;v@ qC!@  @  96wF  @ @ @ 5g}4 M A 1lF5r/;I!*tbi1̯N 8Zs~_jpxr~wo}g5qV$6!*ĸA l^VzemrܧGu/ݴc|KpS+*vt@?(>t\i-.VtWs /~ ᄮXRNSc blYU+'qApx2Auzҝ̞6śKwe8/H z2}'Jj4-9 Ej3m_F't3ec5%ӝG%A~ݥ2ꞚfFt!ӽ}^ۚ_wDev]qȕKM$Pg<̺wpT3;y;KKUM|,2Zyua_~꛿5'z$i}n3_<ҷE*/N~NHN5?/|/qgFa ^<бK\]',} Pɽ2K@垰/[ 2Mz8,w^R;>3oܜ!r_6@TA/?66w|AQ{7{5F?NM)=}:{~wFJM>AW˲h50=Or~ 17mr;I!'%*va.X:/OԳ8t^Cg)գ>ٙ6ӟ[_~ਆ:- $G{T7$[2):d͗Nk[I¬rF?4r#69Z vuَ\-woHmYW](:Ri='gN A)>ҽZ}2J 3,͋RV\GuGcdeW=ÝWSQ $e M 'GԂipOISj9^V,,Zz{M`^i'd-܊5O\|=[:{txqFY.}g7g/ȎdySZ+j\3-ƍ5_0O&X֗ɺ'rث5U72*~V -~Xٯ< 9ݍǦؿ;H~UocWL#0IcRn$*$cylhTD5E޳PYl?a4]e% .Li.7ju`4IfNk埧VYt_̤JuD#[q8֬ YQO%핍> c\~ ?gTT-44}8UJ5ײ^vE:﷢EF}| aUi'|:NPsgL$F5q?!3B}+-i'G@r闦3^8-FpryixW7@|Cy iZzϜ$s+i͘5%؅I(:3/X,ݑ+ǝbost0X yuZo萴)]f`f`6::W G6I5ո+xO&O9-) S3$O''48>3Xty]ZoIJW[.Z&TDz۱A:+ki $@{@zۃeM,OS\cŇC>Ϗؾ(OrϢʟB/+~[E+IRh4  T~oeM[7Y'{O!7V( dn.'#ml{Zܠ\g[W~RVZ9m++B9o-ڹ-߶ԶIUӴyk{g1Ok]?N|vc?%mjCeC sbi|GM[ @w@dJz~3\hL-: 꺑z3(0kOX7dŘKvTZNWy8`t^I*AA j%ȯ;VM͚ݱ+WhCTWvNMU'ˑQT"=К2`pdAtuWm2l/DLG}kRghS8СmMND lL+* 35Tyj>G pM.u#@W[aFPV6dᱠn"zr4 Y]z-\Xayi$Қ@J@V~޷# =8Ÿ#A5ziK&,>6Nޥʸ鬂Cݲn{BQlF ;8$NqPm^Ex:*ؠ t 6fk=KzJWFBdWb YE5_Š^;7Tiw3 HQ5;$I qՌ*0<"f/)8Vdqr71J \#Bp'(!vOl}]i_2c簡"'r?-$yGVq6A-;?mzC?UR: RP -&=9':"IWQkpvOMgs 2:'(jNi# [K֋j W76x6X]~Nrg˟1sHCe׼ @v]TU`0$^Jp;AA@QJ 87>{|WZb펙"nd g8::hä J=ݍbCc3Y* 3-lWhBw`sx!ɓB vlЪ5:r` t[{9&H=&iF%f{pT~܅ܬ8E7ȑEx]j)*Wxv3k[zlp'-x'zay2cSE OabF ̇w3ׇѳy̜8k2;͚,B.zJ'i-d畓ճS>=FjeMYVij%ӫ4N5֫L2V$0-VjM]lJrJ[Y.8Qg}ɮt7=x: K:Ӭs7J'k-Q_pCnjE}n40߮NMYVGJΔׇGS'Ng:IݥRsެxi =},gc"iնim朧Q՜w86kcSąC5 ]h4 = iN}9G;gD<y%7uU`M׬͵?_ebJugfW\t5֝޴B@vԸcojў+!&Nﶛt']Omy)C^|\Ղ~ϖ&jnU_9/z>?_l6_̇O<߷ ym7zc=cTMg*~cbBee7\8' e0p9=![~wtHY!E36->6j|1Ǩfm{mCmY3AukVl޲{De!3o]y4oEPӟ o` ?~̫ߟYcWwɨmKK el"]u--:q0B3؝(i &07U=TLR/rr|NOA 1ZivEӼL=-j<ԝ@ m`\,E @ 4@ ~-F ] @ @ @ b%?nn$y2z$L)ialB^|jiiBwRJ +Z2$&lc2z;rAQQp&CoYś.A?Šq1x/oUᘼ*V}D-ZF;H|;~0l'ύWȔ@mPyծj Ƕ|E 7 f 3;j۝-ti%`( 2n`E%IwuVs{pwYձjޢ?$O~HF+n@'e|V-||/9[7N~l=[:{ψ;H!X L |;5F)~E] eCr= Q:=4@b٠Y= yrL*5$fH#Ԋv#`6)ɾN 4s%@בJ5_ f5c>{j[ߒuBN`iQNLB%*t%O'TYfVCf57~cSf둔 Tŗwe􈘑gRK K^9WK6rNU +YU:hs:]C9@sH E*3JNuW.)Iel=R٠֘i^TR(m+K=ýC -3Fc"bvH^.P|Dy$!t<;P=lbTu̱km4Ƅy${E@0R^map{[NNRh  ?e ʚ8㕈0v546p`s@6;ΊӪ,{B&IIW؃҃lqMɩ+؅=-scW6_| +sT ~gO{_BcgՁ헏Cnh;q 4\;64x3;>8Ђ/+ڠn9Zs5e @ JwٹӤp'qtǷm{BY|duciJ->|p۱nB7n-#k(l{zl]S 3Ch1S a8ǮfрcE.J\PFO InlN>t6iИvzf:$S{-\wt?\c?U߳mͥy@!bV*J9PTggܡ].ffVjI jCPZzh_QՎKMmPj˺6,5`*fзtuP}xl s0FO& AަUW˛d6$[6ʖ4_L-BJ$ B]`2ye-:c¢ fAzD: # NPyպ.=eXȚ165<\VmKM;ٝ)`蠖dt;h.4&Qͅsʮ BY*;;š~seln37ra}2Ur f璘ZT (A4z(8 UIX{ ?$ î_;}?+t߾ `ʓ =tЌEn6q 76 @ yB ,AMl3*;܂}g:e$.z^kQG  Iw$ ǯ8#x_0I';-s 7?g$z4q&$HZ8D̙@F3;B^;p]18-s6Ic\\Au{겋EvK9(NADo1fAo'4%4 zKYvMPc*TkR LJS %<@*qgy Dr ÍEԠ{t!Gϧ%"#d/GaB@O~[K7xiL*w_}3x,V4#8Z=zAG\$ Sp$> NYODGI@5a ' &J;l{<L!%,_8E0&cݚ쾃 &>OmgDKZ 3c!!QqfqCv'./B1N[lq h&AE[` ncKEέ-ddlrU |۔K0ہ@ b€Mc<~Nen=ئiF{;@Ld4@ Kk4t++.5@ .@ @ NtLtxG]sciZ 'y3W{¸ɮ ѭc>끤d ۳gМk' IDAT˃\Nw3/x$/~sI\=( TwJBq?i`v{Vir[nyCř -`p}ZY񂪶֖ #~$ѹoマRY=4Ǜ. Y}D,5ظc%F''01* t?&(M]tTXSUcEEhi)^G Z[G%.1:FE'٦fP7^SpZGu]%$- Gȕb"`R D #&M^y2GEu Hu^Jh^ cLnm8gUh0;95VgM顼22ll #R?/}?/~S'yO[l7O5#RKo{FMVpjy[w ^o~a'BSi;`oB}|Dl7lH]/.Vp+~}+$|Rm^g}6EY?QنiN3uzɢFV^֒W.uza0 &h .)lPHqk&͕-[; &tIu=RfH"~e?ܥN &'931V,h+o]3Y~9 )zSePcQhM@V)lSGaj IuN]ֱZ %xe1?&Ǒ=A3W QLwiq|EK: (Uee'.uhDx'rj->1ǁN%UE( ^0 H( ]({OOMaW/֘vʦLLNe0Hw8 @ĭu,Y噭mx^ї76_/ˠGSݰ6Fuo ?;ija#V}익hO_% 5$I( $P=~ˆe`':tٍXGO gDjwwa yafd}6AFx ^:Z,Ip XPIW( X?IP^쑢Eg;<x{wLVX[q8i欜&8[l0hf{wW:sLQ~aE5lO,>G>q!.{nJ/QܣgkQўgTJuք`R*O\>pf`:l\ݩ+M,貦<'./>;Hf^L.2ɾ++wނrK\0 I!?H}h~S: =uPVۏ,Vhm?m]<,k ?[V`_v l8(>RV008v,~*VV$-Yl&\"CoD%e)\o&',ߚ?#<6xU?VkBc|X7+3MUyGy* p_?d)0jj re{W~϶Ղ7ZtPVhsW}/Ҍ(E.loRU^635¾5XMJ )3WFSTll23uv6*R7ʴfF3w67Fޛ3:.VywIEyW3k@S_($2ٗPZ1nx]͝i}b m = W0Hyb0 0wj{ SGk 딘8R@ lXС(Ӫ(8 nlG*,L  %Ü3n9.N^l]uUUU%Gs+1kBZ`@[ajp0*à 8pHNcHn1zpۻa'{d0 *(=1~\Xݳ5Gr!S𯌸r'UݣЕ\{+FkgZyRH ,zO8Î<9s+=/T𣏈7rZ:Y+Fy4OUEP7F;WIECFJ!M&j1YTp ]Bp$, 0ĀHҝ?=N7, m珈u= IvtE#HXn62p$U @N#B m,q${Z p&k@aFQ>5#=6X^ (,ǩﬢÇ7yR](h0'Fgnkz pv1gtK ^RǺ5+ :n I }h>O̺4)4*d8N,ǘ 8S?ҏq?8^˘$I 3C &0h I{70( GxyRZ q.;- [M尧j$WY!K`kL`@2C;Z@ZaM@0ԃ}7hq:` \+eJ:d8'H!8X_0cc+ sb8uW|=߷M-Z?df=2vMRd}kO]Ye5O.rۜQV}|dzb uӎU뗼!+J{3MWݍ{a,:= M7lf2CFH]۩se?-fB0?RsaQ0fePm7;i|ey2,HJ3(iJ\Sf0aOnپ. \\B~kv[>k9M{n,:%%SS)sMmhhܜ'Mz`JIL3Al$:¤1 BC~$LfA6 T݄kG?<FwStt᛿}U YPBǿvBym wɧlZ_ю _5/+)nr5G>|uƦչ25/I>xsajQM^R VO;Vy}Kov xtŃO\),\ZY0X֒7XX{yZ@m>*{tgmh&qՙwfr;OK@[Q77@;eG}ֶ^Q1̾֬kM]:ZP_B/ѡ"t8FvgQgxX~A Lν{˿e̼Z 庖v_мz1rN`< b0O#{Gnjus2@ ĸ? @ rco_e(z;[ ]EOHG/;,'qwr@D? @ rÐ'`L#/XR;D%@ 05ޓ16*pVuʺ?,$^)p8a5^On*#`O :i,Iq .=6͇?8 s\-0pR<\'ueݟ8gB8qD F 3`gմUdos庫 n]x5OSv0^{ude%Z"bs/aQl,T/>?z8`4hJOVQH 'NBXt6o>1Y,-j"}, oE@ ,_y.yK_x/[&s^rMz~;ܳa<.IBs?eI@ qψ{HB mx|<݊m=V6۽ pL^ o?L g73 !=OLs~ ?Ҽ5`ox&F2f*tnsj-&kiNsCDک20^ζTRj)nVo(ɍԐ}P}:D!;?GߜStbŅ rzH@3/;f7$6NjJ77dʌVGl=Imgش&EH`8D;qpF-jm.hY1l*@Ufi!w򦊁@ aW{_ qΩ/r޳>0M?m&jrruvoK!Ap];`Nr9CDmbaLicER?N%? =aOmvm,d`<~y_[#*_5cARYT]PoaIr r@Q\T]'! ªꂢ.q,MCEh;9"䆇b7Tv5E '{ФWkذ5lg?IS bg,Hut LItykiu܄J#`CJ[ Cܣ>|MQWY֤sh'F3KT&YmɉnqXU7['ywiيK; ƿWqNiP~ 졄r9~o.@ƴ܂vl!PNegifPP=ıc3x7zةv ٻg wgNlQ[bHZT{n-XXHa@yckӹ_~=b8PǨF8ܩE&I IFa}nw34)ZFMب.ׁz)d3Gz~tI>l0E'on2ܘ&[ȵ lILE3ϣ@ Y", |;+#qqXl~&E.bY^vB8i}}Usp_/xH5Gʣm_jll8m$! hٻ︦OBa,AĭHRX۪UhZ[Z*?[QP"ZT,*".P2d@Bm~9 vvv[ BA~O/3Q7 !>j IM?vЪZڦ5 N{*[>tY!LqmB*$zÛ 555d**fLʿʈa<;xFK]?T @!ߛ640Dװ\AdQ׺ՍtD"#RX?c:q2}Q[=} UM[J`fޕYh;?fFܹKIv> >ŵYFF8:Zo=M@(;;{7odtл˵ `]vr: lpӠ L&DbڤÙ> kP=YB5 _R}oq_[R" S_dtfl}3`3+\(z z [sM>5OQJ奉.Ctٕ"UΖڙxEWUr`o^io@4ľw5 d[T!aee u@^A~qU%[ Ѿ^'P~I$ iffԚ*ݹyefy)8Ywqfu;0jCkvTS}0lIO\QZPAUHmjo0R   RMM ,E|_Y(jc*hf،_߮ ;& uQFo.-(O/Lr?,?x񣍁s^ SAP;KBHzC=SNL#eAUR@=zr@pȔ!7Wv-]'ds0x—s>멟dׯX~?^߳hxg:=گ{ٹ#7fKu0ickFdTH3S_һ7fSQ\DIIo 3xkOəA#PL0)1yQ" Yu5◼,L6L 9`bnng4%x/^>/O)lssDJUhR~u[jĴtqioF̩j*^漻(fal.z-bv2W3mUr˞JB72~yU#SR[!T6%dfaR,d[wh0*.[*F|f@ IDAT;YSfjkä K9f eJfLκCkK87{KZoodמ\]e{C<˅ ZWe@HeDɽ[VLQ|x}K;\fռgNc%ESV4@ ?Jy}uҵ' ZX6B!h}WcȆns2tWX=BԈ!a&nv{8-q;^T!D3#zlHn B5M_VzW !7^J nUZ~Q[}DɋR<uhDbpBmJ? B!ڤn!B!BB!BHY8~@!B) !B!e!B!,? B!B!pB!RB!Bʢ7h@.;SB!BH7I$5$,r<[th Kڹ%7-nٖ-H 6OK:bvcOz,|B!s\M }_s?HK㗒yeIIlJU/~ Cy3 5 6?J'G 8/iƵ ѫ?\7 {:E*_{!6'cgOFX@/!|^ܾowf6`i\;`\9f͙ ۹]&? B<s-Nݹ@JO.}5oq椟^QQus Us^FCKy{sڛ.<U){'~R_Q~2`'Ӝ *'X;}[}:̍z2Wdݽu+F023s4L8~@!yav 'L۳|a[nmH>(f=֮#?;Zy*^*~X_eM+#~,e=kh›/ @ܺ#oYRR| @KE߮Ʌ_F x׷vZU\s ,B!Zٽ5װs#g!_<0Lg\θu>KF"ɳ̚)4!B-k&7K*oN|"{*v>(Ct8sȇM8\d2 @$+(:07g)a/k?ܻoJ ~>F6)" }G( $ROsjV{zv}?->xS>R.07g߿bAׅE%C2jxV؈ѧKe78~@!j9vVIO<|ȸI];5n̾N&Щ__G5<~CI4݋;xF%@!B|'(_RZ]a 9!B!e!B!,? B!B!]*ΡON^Meٛ'B!BB!BHY8~@!ˁ"?,Mu:Ϟ 0j[N Syy;fhw4RSm/"vK`dr;Fk;#O'sgc y'23w;} 4I.&e<;:Tvq!BZB3jCWyYr5 ,|Qz4fW7|DJ|RkPd/:u;7B|_;+CCfkCqiY+vI~3z~7-}b789i$ _9UqB!%=y3 5 6?J'G `?8o3wu^w5T51~h7Sy yAQzzz %b7vl+PC1uퟮ8nr͒O8eLGY0O+h-[м{$R"xk_.l7/!Bi9bGۚ^X @k%6v-#ǂDr8xxDfKc"C C<;q&UꟍM.ȥ#`0nEmE2qU<B!t~(LvG(,,rnOX}QRʏ]83%A{SK ̇ |o-WR|u~P\ZaH$_$Uu< !BH۸XUryՙ'8~itR[K2KuQc[Nzl![Dsy{?]@,<'\L& pB!%lܜfFu0}@,?UY5[*/0g -, ;WzO[3}Ծ6=ѣp끵C^.bv[?6w{YV3UCֶۢgԾ1f39Wݿ\Te6M3˫%OGR&? Biɓ;Yflˏ „ 𲓮d;SOd6unyRIMD>R#%hA˩oM҃^jVՀ, ";GelI$stXl C=}­g VW. [_Xh੶Smń{JH$Ӟ*,ȈDl\usxrHvAE-ylR.jŶ߂Rʮʍ\:0\Y,p+D*e7D|B! 8ُRb1}!L,u0:j&~Y~84 ?vX.A—|W$v*1 MQbY,:Is.ս[&K[%~ua]h DDpB!m\YBBT pݗTPZ?1$cg;^'QG[͈r{gD/ْv>wݥ; uQ1UʕM^3@,<'\:A?D&RFOxUni0mB56`npr_ryqM=qllm cgfoI; ^vE޳'0-^3aar^f+;ǗLkzwIgb.v3^SoN\[ 2f$_^]?Ųww!j6Opgu/?/7n^x`ύ}L\ y<[5 mPcfOjy~zdP(\n]u?l9*|?A}^e<1ZN=SZq;*4ffvT>kx/`e8r9 quUJ!+&wIW < Ze~~!P+xs |knM2ֹMMӼ* n5yOJ%u%N!UQXɸL +l 6˿#p~?؇f c*oT}$"B!ӇL4}yht G tlBqv(^ǀsgW +lts}`ڡl@ddmaF$N!Ҙ8@13"@'2ՀF%D")EFQHBԕPVQX]nFd-T)p A6pJ[#2bXe>,A]*Tg\"B+.~| Gb_rUV?W VWP#) 1Hv];<_B!B) !B!e*vz>?!BH*cw;mB5#ˁ"?,Mu:߯fv}7\)<8R}$ X-^\4Շ $7v4vvMGy*"OHSk"2pw"!3?3-~C#hv]L(ywt7맑wW<BfC3jCWyYr5{bz8dC9hAkO[9>ܔ. Y  )-v* ̨T1YGV,?V^! &ud|ln )snxa4?×%ymNUa!ǫ0o_`U`=+jÄMpGfџBa MfJ l- =*UOOP8{v'9ZsoVp13 &?Ive4Ib`;HW1f4>lN!j @. ?*. 7ihXH=M;TYɟR4&J)h삂yq%g> BQ$26z2{B!l9aftʽ{;|˙h]Ƣ q{CLXk׭Êj;Se9]tҙK $d9۵:ז ⽃ a43WWo{?!j6Opgu/?/7nk;6(13'5NtwW<`Y= WndD(D@ :pR6{ > >c{M2vy-f-&v bTŽ*X[Dy B͆s~eKͦ/pr_Y#el'8Ӏ fڊ9g;} eeYD}RlHyIxpD5~2o9pU_ќ߸:*[ UOvUDݫizie?N5KEN!Ҙ{ЬM0G[s;m:_Ax#q녿lj m=ͫpf[4Tx o/rDl^#b % ;hGL:]y B;}D3[A.zN4t G tly6\R&]fzqi~f x*=TQ\\'"x)TRA wLRHb S hTBT-RiTU$y6\R UFҊ2ȈaiDTqDu2Uo󪴝B!H2nAETZQP{T,~TpI*K^ļ"!Nj>vgInCd[rxt+79WUm[gh;VftN79@ntY<0ć ``-cBqiYA &Fv|s>8ඎv15uު5j"VYe@}Lh44Wa ?fJC)`ev'l,|g'm!BJ);Ξ]ٺЦ` IDATOcw{\IA3 &?Ive4Ib`;U澇"O/vv,Hט3,V9@~TH[V @2ͦnCsx}A@HlqriLdHz wu!M<q#A.ȥ#K'1Lne8>*q)!2(CLܯKͼZ??$. O/.k%@tiuJ19EJHP|uV_]X@"*HI >.ر flbeT-4'б/7ǥXk'*>M4['77aV3"^w7C$p gg! GBQ$26z󗐮2:۰Kp0"&[ջ :t07g8̌Nwo/p>ׇ_Y.8WA"0-^3aar^f+;Ǘ,Ua߫?zږ軧c?8) *fift} /WnTSᵮ;i ΄-/U E9a.$4k3}QY(蛛b.(*[R>HШZ$Ө I9O[I+8"#QQ xCWTh;VDZ]_{v\u!͐XԄeTvJʻ %R/{*X\]yjW^$ZǮh`k/!B!B!pO.ֳ{SfnuU'! K!T@ilP`KmiώVW[[uˎ ~ZEd:gO007E3 ^9~$Ho{\#^1R7tߢcz64pw"!3?3-~C#hv]L(ywtzۑK? ԬּʻV#4H/B3j$7Nܡg-9{@?wsp4mo>v5ִguvnJeC| )V515-`rjd=7snxm`S_/Z8~PXPMHT1E۲V],5aSsK{K u=y3 wZt( G wKĥ?vhy|MT~*J({IR(vcw=ܳuPu:fL~Ɠʪ-,,h^=J) ʟŐv*5,6f*o*MޖU'B&ɄTPۣ7I޲ 7Ԅl%<5Mb*P%ZlOnS͡Lwב7qFK*l])5asas8 )%ӻOhꭲ#<j+3}Odx`v]-^3aar^f+;ǗLe߫?zږ軧c?8) *fift} /M`h-P?|CMHIa[" γY ` /{ 5m_R&,yjp1t'y jp7[kۚ UR6G'w8c޵+;7oKܦ:۳]F uNcA xff>XRGƌ1W dD(D@~}r)N4ugҪZy&WaPauAyX]Uq~JhkͿZF&A*!wo )),bKy6k;yjp7j`^4lF-V?otc-|Ŷf97T3%7]Hx{َM3vpr}KWpex .>\&wսu)uy2z?MZ~T5 sT 6G2eǮzRدY?2(K {O~|\O׋T%?%/Ƨ߂VbP78VO-\ K*_]%<5(BZuoSL;M 4Gjnܟ&`cOV[k>Q] ukHk/^.\ڽ{372* -}~| BUf9׸nd])p,ǿ˱2ղ7}لUO_H.TEYMPEIҮ<{?P(c<1N<vwtt 1A bT2bLC'm k|P1Þ-p#2bXe>,A]K8~@!j9-0~@ɔ?K!B!e!B!,Rcl&eO_->=ۂOIͰ>sX))1A bĠ2AAHG4Ż{uDӞ. b 1A% Y\qAfa^nڭa~r_]aYoeYIOpB@r LN{~cHsmtdIbn..+=u/b^_Y!y'23w;>":iŤܜgqG{.^?=}Fd6\J57a9UB-(ϩ1A bĠ2AO5׫ ה%`1#JW?//A YI.ˮ 9/vxٰ_)  )-&:ڟg6qgC.+emZ~BCSG2V\>ǯǍ0m4?>92OS5FTUIeuD5 wB2M]]FQIX"٬AҪW~7!j6Դ}  MX"5?q6݄L;| SbZ%5AhA uoHü˚cדM#RNz^5VuTAQzzz %b7vl+h:*ffg<ɮn@t|LJ}|44K*pY4Qdo )),bKy6kP!ST~QK ؞܄%R3O .CS=$#A n&tkT>b[S!5J5Sjc`XQ>#&7Z\)^w_dl#7E\R޿U d07԰w0q2f{pw[ύ}^9ѧ=Avl5#Vz^%}}%a /.p[*n;7a$2֍qf;eMTb=i*MޖU'c{M2vy-f-&v bTŽ*/L#TBM҄mY[^u (٩L-0+Ltgoyx/۱i.N?x)WRW@rqB5YXTuow zg^;e_.fM"w'L2oߕXTSO-Z);vۖ~`R%?jW>~BwHluU'! KH~?, ?ɾ56vTzkSA_`#hqygD W<˦9}#bz'P,A]?hiN7NƴӻV#4H/|v k:&ZJiz))?O2s?ֻL;M4S*3ͤ&Km ̈iFVW[[uˎ ~Z4?PfDeXlonFTRuuzSH\K4\].#2bXe>,A]ϟ0 xfHw:eGi?\m_B+*&֑p(UvyE3y"^3[ < !B!,? B!㇖c7zi;¨K\u!MXԄBCa0ӳ#f3wT/UEM/!M(¼ܴ[竒rVK0m'wc.'nvhH F&Nck:S17y@Gb:]1 ijGsbRFAnγ=LUln9هw|BywI#:u,j~ i}Ψz^4garהCwvz :dW%'mggu;7B|_;+CCf Jc?3*-YX{~7-}b789i$ _9UC,I5rTV-q!$ױ t=y3 wZt( G w5MnZuj|M/2v T===@垭"Թ_k3 &?Ive4Ib`;U>YN/gԊ0"t\ǂN0g0XrP~[uō̼ܗ//QYJ*!珃G$o8[4&2`h삂<:X,bo׏֍~.etu02tsudX &Sɪח;X!.UL23u^:! !CpN"nڧz&nAD"ovkH>s vX.A—|Wԩ{f864Fe^$FvV6Wn/.mՅu)$InV_qtUewf(u,c+jVn]ϖ<^г+N %:SeN9aV3"^w7C$pϝjwif]Terƹ:W'G]dB _BcU̺Km`ǂt 07g8̌Nwo/a3׫PLGNGO9wq{CLXk׭Êj7O9]tҙK $d9۵:ז ⽃ a43WWv@j>{{Z&nÎ'wغ 7Y̆M6쫻l^>vb"WՀ, "­kNM2G`0ȸsӫG:f^˩AbY}BB6i3ѷNXi ɟJ|eK6cd4KP(??#HW @!Zcw!q % 109óTv9lj &)Z4 ;S24@!h3j;SH"62L"@ZH-hL*.<]Tcs #iEGdİ4"*8|Y@xURAYnC!Z W\&\݄|PH"Sa%ɮk''ZK!B!e!B!,? ]e4`_v!B2<拐{v!P3(¼ܴ[>u~vQ~~zGaki%:Ǝί+6 IDAT~_P}މ̴ȣNu1) 7Y~5 N~di;B8ɍwhv~Kn0fgz8wE_vZiw>}̮|Ƈe*!KU }Lgƥe ~7٭wms㜃:^[8ص(z0ě"x̀klޱjѡT01B7Ͻ^p²-G anU,d ڻW.G達g.l]yhݧα?᠙$yk `RJFe1dJM*fق7i;B٘3,V9@~TH`2%_vb:Lrܑ2!@HlqriLdHz w[mIvӃ<:DH*7r`dbL[Q{]2q TjHW+.1Ry B-d?J}Q&!Hvc4kmQ0_@~ឞAg- ܛcϜh)_V6_ݻzrXC)$ID>? ]U65hejbB2qqv3* ݾ"+*M@%FGD11{o.ADHUR~?"Νw{;M$NIf5<8r9Rڥ} TC**ɺ#t7S}>lZ,s6I94ӽa_ E"QW]n@=ׯ읢':B.Sd(ͺ˃XS]-x{VD|?9%^P| oh҅ <>t8jLz!71q{Ġkjj T{Ɲ;PjYD3]B!e"=cަF c"m2dIv_-ESiK5}46 Hs8.\ӣe?K@bPvld9FJLƧ6'-> SUv B/ڙƂM3Fyan\s.7x(UW]d%oݐbvc#dȽp5cvU[RZq@`:-rMFoDmo^z*߾=%:B.Tg@c-}._p+~yXۨDԼj`.+9vmo* d諓AFCk%n1?^ڌ]}]W߮h8L۸t-?tQ#T$[bb?:z8xNn>,M 9ض[?!lߌ)w ]B!eGz ̐^R^qxϴ^Pvv~diyXFJ~Lp\ӠVNNQVOő3bTT~8H8zf- m@AK|^C;S YUeMiB_.+W7e(8aɖ]=2nɂb Bv͖v5eݣ9º H}T"DH$3jի~;ܤn1yNğ!*6e&e7稩m>PĮN=ygT p@!2@UU5@ýG]Ks[D;n2(Qyf8{lp, Cm9v~36bK L9($^jYz2}UUu\zQ*сEJ@=i||1D! bf?Kl57c R ؛yK@M\'?,t5~ZQmicTfM]U>mywnz_H:^FsBkSlޱ~mD@!CXKԼHvyNTk@$`><T>.̖Ⱥ#t7S)IvZ,s6I94ӽa_ E"QW]nB!0**97BfEڳ\\ևO5<^Pʣ0Jj燭}41~qïZGਪe(SP$^555tE*3V$8؁K@!usFؾ)a5p4;J܇O^tcYrYuP^sQD"p@|窞-U}6_28c#w6P`7>1G?ia? BI ڙƂM3Fyan\sHbkmʕn}_:;YE=ZF >{j˫g~ٹ~oN\ۢQ@>jI/uv!Bٸ1nA]BpѦg^G R>&Ɯ2ZFf5#3-A~r~Ӳ JôvM_+Ȟ}v{>iB/ \u U>.TE5}=e9"PU:1{ˠt)9t1C?x!BtUYꋹ<9s mIt pY\Ӳ J&+dreeE&Q{B!(:nEq8M۽y&އF~bPx%%k?tMs/!B! B!P{aB!j/B!Fcᵲ#:c&=!?8Ds=fTs竿^oYgߑ/Re֥@"(0Zqf_dM u‚+\$ӆ/ ^^4@:?I$^,g9 |TJaINf̥>ڝQ!B Ô3jɊW 1_ҧYv:G1qz̓mzŅmԛ`֕YFm wT썾[]4Y5V;Ka-?֚ZHb"IN16د] wkg2mH^!B= ndƜ_|YfIؠ_5 $8/;Wֿ0/́i@YXK$Cy@>&vem~5ʘ=twV;;iS4t^0ٜ'[]u]*#9uUn, |5ʼn#Jk_]phDI$h,+Q.dl dݹ&>":,VhbK!BgQs%F~*zl~{&^N}x]>Y`ftg%9yYGHntߜsgFgŬ)JݫwOѼַl7B!z 7Lx lOb\\lԝoFoFeO7PQlXP>CBCD"{wJo>/^}l7H$xtHR5vrSG8?2#//N`P)$C&fBp9VDBvyNAPJ:L8{x^B!$R}$2hOۺaӈ G7#xy5n90B1s*ƺ]>]WWyES\ :PFTS]-YT#YRui1ISS^U\TJHb"%5 n^R;u&x̌*==maB!ԣdxU ʙZ @&SrD]~PFM#cE sG[ihq|Sjxx>a|L|Ð!k5 {/Ɏ~tМvw |3CLh>Owq?k|1Ϝf-T`ѓ$ADQiX;sk).Hu3:?DKӱw蕘zo:9`J$ 2}䒰@!.ڤSp10/o]G !ȿ椬ԛHN_]83<]upĈ=ޭm5u\AaaǡQ_Cѱ%H>0$nيe+2I9#,O ;,Yo8^"!291u|e>/t+3koge%[YRKE?g(lO$1Ē(i7[ض1ܳ9ݫ_|zI@hygI/u¬!BHqc܂å]BI^B!BB!B@!B!B!B!^? B!ڋܽz ?J{8WGz{XD萔w֧Ўyf|]+h d#+d=J,絖H"I }} "N}q;1%N}B!>o?3 W;F㗙U1_;} ɽ]݌"ݫ:L1t\Hb"$fWlX}JwU%@ Bw|#Z5lcTc;;iu6gzKA+IOb +)4AAF>暽MIJИ>D-NӆqS9U&l5;rx \t`B!,mJf Q0k+ǹ6xĘ*TDм`Y8*ɽ[L~/Q Q  dUtH2.Hc_ ^x;B!ry Yzc^؀z,9ԧe"me6HVu8"}F ̽P$]cF} '3@A])qKPͬd2Y(t5tAD& T?\-owvj?2Ij<.Ɨ)B}RK5ى_PD"ћDbOufuT S_ގ*,kIDP؎@$rsnZAkzlT]sfj@]jv|Ik_#B[ 9Xhiׂ[\.<%-͑Z룈$PW&2ςSʛ"um, KἓG(pXM D rVTyuV$H#"HB!n0%5;%4zN^XP;dM*K&:֞t{ɒI$fS :.hj7Y+oʐU3Ґ*q:l+[ ۼ1/Db;k׵KfVzʑ ZMeu~G!B}:z)52kfRXZ8UȨE>^mɳ;QEmˏ AWfIˇ"B!~ !B!)_*pFg̃B!B!B>XdRE>:B{|`6;Bw? B! B!P{u^@49@E&D!z9"'+-?'qEQEQ?ps+I/YI%w[zšw!z .D=/I =Y&vߌ,J>S`Iꎦs&C!.9%6^D^CW ?oksB2*Gh{h4>;PvB׍G׍9翋 0~kk\x=מ?C:9kIuV >VE;15e'$=%l݌Q>Y~kaΩ]j<ˏB;9/?ϳWA'iCPzqN5W'Bumcܴ[| wXwxɼٯ&^?fl_B¡5;s%!cG4Uy޵?]B!6Urvƃ?Pbڼ ._VkrcxW2ҎMq]x3&BdraIVz5:>'Fy3QP`|̬+&Qs޲RC+_W3ϧڕg~{f;L6˕Cdd^G:ovy]Lk9YC:B䟚-(ZÜkk 7pc)&;Q2ttt@TTk[EE5$QQiVv!坜l0@r52 Z}֪u .*9_mZ]RFB彭 $o1eM '| >m_1yT0BsH@6_tԲɛu" (9'2"|%=d?g70%XF;=o|5ˬʹ'(|Ąr+~̳eU&+!&yyX60A#n!TTTJTg--4${-ћhҗh=q' CfJsG@͛Y~@T=MdrLj>wм+P{;Tw!*@&q-seJu97ˡ)~}*dJŅ//+/Mk]ve͞O _"4J)Ək/ p7ؽt7cDWJOD6멱4VìcY`&idTDNGX(=&ReF_-;]xq"@$Æ$AJJΞWowJl[K{;=B`"]wi嫓E![<#1Z *|] _7Y99={Zݒ| !Bh|֦P2X_QF"HH"QbCQ>go߳=iK"EFgRd t8 @`He=c,fc(B@*G<>qɅ@pUE8u]7Gy$U=IL!Bd|ȿm8#7)Vfu:,rD$q!+bI Nx(DB BY0۬($ɂlZ8ݹs猟dʒHĭ['kqL`tt:+w` 㬫[x `A t,&~.2` ̉W2T(J˜=3"atBUruiwt{QeY wn7e XK$_a!;$ f-izddeYfD/0xP{}CR8 )̬N3r0.&σq6 ƳDcV>~:.Y5sl2'B( c8:FjHHD""( '(o;,- AIU~w!ܴh s$fs__߁̡ a Ě,!I9*'fD&"J@84rP]Y% DJ ek1 GI(*Q2"'B(PE"s#^,'O)$Zc612H(4(G)=`Y[BP8: B&/p8եg t"( &Til"/)T#"g✜6T뷖 Pe 2oӔےtXFrԁ٫(][m&ub\~"BŃR_Yw swaܽ]".uO SaeTHD#}޲s.]=-hf|~`hQCNe:޻ij۳yEB!h};xңcֶFcQ>GO#͌SB!22Ir B!M8B!2!xn]]>xh3C|˕VK/Ŀ+zs[cgJ.!gNs!B(`B!2F!B(`B!2F!B(`B!2 !4K/]d[\Jq*ɭ'u'ZIVyaB(Y5~9~ސhDIl={~o8mWv !4bJ\)iK,l'$IZI IR:4Bh"b^/iK,l'ŤHJI󋐒3du^i%<.B!y0E!BXB!y0E!BXB!y%^'.X}>K09gZ}Xav> Za8B!2F!B(B%H[DՅww-U*⸉gnN4ؤ:u(e%yJ Ɇ}H;!d+ X,5E}M(!B ["wjqtMLl TU<֦1<8 E'ї+ohAR pC[+r{|Y"XbI*UYV_9VB@*ȩ-QO:vu{8`BD YE[y~Iei`jT22 piإE09^` i܁\dSW9::b6qD⼉i&)1SiJk3o2/_f8LƝptp-V+=:+Nxc!`c4M'E"a8[ Kd./'8 F$zw}C]l[|4)Ѩd!GinYYdac4;CNn4(ΞKN >pWW׆ 0E!H .,L?`s]'M ,>;2Ы..X#$o=<p_5 poW' ⚢F W*f$;vvJ2Ⲵz[F7%]ݣ@e媤&|UZW#dʍ&dEڦuYl w/Y/bBCTE91ZÌxzF}dXJ33SQ(dpXF&e&c 8 e9YXBtYZ\YiޞMR'bZmYcL ž_4Rid\)7G"  DHxjKi{GBwǟsX_R$T$٥"{m%%9^:wCXgXmP2cVZV'V)Ccp5g]c*'.Mb'=4iإ"Q8ޠftĘ*c61ґXRP#qܴiqVER, zyNs{[F)Dz 4"Ma-]ݣ1QN ru-cϺ`BWR\$:869%+-X-B[` XSqy@YI3edW]\FH2߈{8TrhZvkNeO1)Tl6S\첸b=Q,B! ,-.˜蘵Ѻ:ZIC;{m!٬a6/bm_b9MqLZbJ*ɜ|g]0EhYԤ +! ])LcE :h_&.@4tz1ֺ>W.y32qיf\Jb^ g\,@*\nAi͚"u8nvҜҲa*m `4aF Z˪M\9":bJ<:]/,0F|_y𽭥CCC9sUɘ0is76lcvwpBB),7Q\U]vtX2dTRgG{@x wldU~gF%!2ǖL.ʔ~A5xY ݰAwF~ߞ_$~~Ȫ:'|=RWk7g:WIJ@L<~{ l7ʀD_ZMOlON h+Gy+q@iK*r=-tFڠs-1K'UDbÌ?+͡ogF%!2ǖL.ʔ~f+ ǜz]-8.ۛuK IDAT|;̇]$vrg$c@qIZǿY*"Q8ޠj[ R#KӦ cE9r#(I6?lo(p Xcw/%agĦ0wЮQN@QY40*(A!pU'r-9l֍|ܽ?qM;b0LðvmGn|?9c֙f_ixDa,*.2jA[wHPn^S`T yd2||.Y;<8蚸i̹`+ GJL9YHOPdhPKthhKJE0ux9Dr"uuu;wLB!V/4Rk2+x@WZ#ՙ )u{~I̬G!B(c^-%i'6V2B2*uB v]zùXBP}B QS & )D.yu@*v]3ww!BhYpL$<Mo` Lt*5yu@*v]Z!С}EXB!y0]e,.׈RB!t]) ¨cpt[ :>vb+.{{B)u A=6KJJ2Ⲵz2ųohwuȸP1#,,_%N; @J MZ/.qu7z!y>x{]SBh "W޶!7UVuqmZ3V<.HN[KShD&v TU7 T~UA E'ї+oz\QF"Qf/4>Ԩ]_rڱ?fޏ^}wFgt.3N/^?g!Z,b,oIA@H[2OY2>|D#(Q@HAaP(!C(cR*ʼn5ܑBTNem|YI'|yN -Wn@|Ԭ'{*/|K+=ld +kBh9Q#eyV/K {h\U@pykr"}ތ|Ojc.ᰍN ҤD&&IT%dyBOϞpBmc.xL{ZJv+ѳz lVo[=PU)>7=/^ 6iV!r"T* M" nnSCmP_ yV 49 Zٜ x^Ʊl_| a1A  4g?4D 1Wp IF]bGB(izz{#lFM/.arbMarXꢰ:!yJYhPjTldg51]@SX+pY,f&їgLuٜ'<^` H`mɄ1ׂ(D q&>!2GX|MY9fRݗ4@ӳSҖT{Zz c>cA=0Zb|7r%%rRJ$?8N18̘}ޛi&)1jJk < ؅"J%OLRP#9Q:ݽ q4{[V_ttCryZ0yrՑ_sZ㙇 4;'Bhu c\d$^hea8q,˱8a:17"=u϶(K!  =I =H쩦.v~1 \Q6}1VN$SW_\S~ qph;m?б6Cu?Bۿ v[~79]@!2iWZRLqa$ 8Bkn͉,KM_ rY0XݐWR\$:869K<"PQudo=k& y3aE"5޷-ノ1<WĬW{unMgo,BI-Tf,fJAe+.hHޮ3O%VfQ{ˮ5p)6貴x3 BHzVp_ ji(@!Րi/Z0GH$v3s b#%DG1F]]RşsV!BI4ՐqY̔:D97(+ D9IRM!P:p5qY̔:D?\QjGq"!B0E!B'âXT\lJ"M~EX|vU wDjξs4/nM#BqQ, Y"ىIbYU%ylRm~}"՝Z!l6e sN{}g/l'!uUkmVuSIED_ykM"u&o":( LgN ʙӏFsEᩑtr=G, %G$1Vw$MZW+u9>Tw5!œB 2AkSW'|\?LW~PZ$ikj p}ٺm )\F"Q6K3`M%*>o%E=kD?,]͟o  fl;l_?W-];Fv X===?o}u^_<@Wwj鶴~o_/8Wm6Ƿto2a[?ߟtMw;knek)l}v:lQ@Ww;vY}C笕s{ݍwu6qf1B:ʲ  2Q8 hme"%U&g͟M ؠ?0@X(YJ/ʲtJmzѝo̡d\Z\uN'.8Lޜh(_Gz~ץ  ?wǩYyOT^,յ5*￟2dՑG%cJo&l^;v O?x5?Sb,@|=ޗx௶3x⪵SՙNշo馛u촧\lq_q:^wؽtM7z8wzSuS1~O!Iᡎn:fz2B=4\ r59~oFJ tYt55՛ XҤD&&IT%d{z j{ zYg+.Q*JN9u})âu:hûgQiX6']}gG9xG_:׆|Ioѭ)O>O_TGͅkg0мl80 @Osxoc8;:֪U`klref#"̌C2D:!dY`4tea8q,˱D}i߰bSn|hW(:RW^Z |X_>`TxN7hqGb *D2C]]N|1umL\MsQWyo|Hc}ty']x7$=uqphx+_jNީ:\kwl өW5O$v?;w =x²Ӯ,p}>Qzx?_]`tC+v|q-wEe*k)Q?|=F?'5`3}tQ?pGz۟tǮ?Mw|&>ߞ|n2!tJ%*Uœ, E[^ JH kLY3Me/@ ,8_#L7bC).*6i"cC=!W~AXl0^՟8!#ظEG@q !;~Qq+3炒D/r sB裏?yٹzb?ݮΜ<;Xl6]xopcK6ц-^:~믟]-hwex;K ]Oag24}[~p\^0{֙D[>Us߫-|OyHd{o>^?n>6 W^l{W\zҋ_rD.|Qė8dW+5}7sg/^}QzM  S[?*h~{rbI+Ӄk8" %y['\{+մyBN/!,4Guԕko}>/f:3~}<nʭR-Oˮ=XYpQG}.kWuQGu1AqohSN ~|tr앫NG '[r%zwN:'^w=gF|x[WxU~򝯟zsژys#1=xKN^NE\m"\#y3h;:;_;-c '+Ƨ{h\U@pykr"}<"(!G*6m^_|ILLR8JG's43Uӳe(柋Ƅ7Jb$q]:3GdWp99A QS84&UǗ((%,' fv@UNؘD;O^̐e1}+L8`MeOh}ZpUߴ/55I,NA ZvvoưxϻLZgb:R?dؔD_ZM;F-l&"I,>":EP~o-'\$Uԏj Yݷ IDAT:{4#L%k shq&jɯ<2+Q :kqiLGQ ϟp.?{?hkvi{—&v'r)7\:(qf?MSo?sN3՞Ua:M?Y+9cwݢMb^wD{wD1Xuү_/Zn4tTzN1*. ҹ:}jz`̕|#jl<{e&IJ@LF<~{7eΣ(T*X848+"I,>":E agkNՐ&\ㅆ,H? @6Ңr oڴbp@FXwҐPoJ]W~" tLP_Wc\)}{۷|uZXY3oڗЗ'm҉oN:hkWg|ݩ[ Tt2?Bk Ia_p}Ȗ_stVWzU9cxĦuvI+8m1V%~f;V?%KI6ZЙL Q?40h=B-::Vn~KY .9Ws)oΘh^J-Q8GD u9o#ZaƮ,pŞ}MbxA%OV7-h8s}da+Elgd_옂#'P cIO{8_@/⅟TͧΚ'M[=}k6+K^xnO/}H_|WמѶNk~uY'v}/ޝ{cд? `w'|=';;{~Y"o/;owǣ\fog1Ccn~?ڃ?z\z~#;~+?~/߱r?=Xmh}|ǣ^ۯzZ}QqxsgKWR\$:869{+KlDr3d]]Ν}l0^w8& Vuugɀ!Ӝ 0:p]ʼ"`j,j`ץoP:'1V _zW_3wG}$6{w9&1uHRwk}O ;GMq0(Lg@Y$t5ٽ}2ND>VF#(ht84jm3MtMto),(/+*( , }Q.rQp",E\ ]h)ݗiӤm̼eJ+-fyN2<9s25u!bwLH$o=>_q ޚ]H$$'@* |@wKw+[[{c\~".t_)#?pK=]=]c6܌ g^YʯKoF,ٴ'ĩc?o] /Ċ]ݡwDW<`Ē/Q ]>JNNNNm8Qi-)C0Nt΃=,kCM#w'8{C:/[!QXjyHH{H&JѴTzu- 0`\yhϘL4(H+7! f\V$ɦ-*NM-!:bz:3xp@ݷDZRl5Pl-ermT( _$S @$.{):x(]N!pTD8Av~R?<,٦o{=oM#5b+.P M3ϙzK^' giOˣSWM3.qv~Π?a K+fCVO˦l1{1?ar>9G|}j/9g&W@߯H9S+fJCXX"6s大6=6t/ӧ 6[h=!O6l7 6lح nEzlXtg/v0SVLX4N5"w ~b=:SwKu=XtuA20 ` 40WSM+.5~@{KLMghwB <'lؒFEUn-w 0F;2u1fCf[/"\YQJ#e8#<8GqT}[G&A{0QO96woh}GEwťI5XYpE @"֜^_, ("R{4ΐU;dO;vFgdہʨ(zT^TT&Nzxy1ca`fNm6vKŴRoo(>@^ɪSplvHݶ~#ov]TΤ ʨ(Ҟ>qY;hkc }jI_lBBw Qs'2 NKu}Yty1Sz%$ !WSrt@"ܹ'ğ)/y6n0r;`@TRin&/; ȃKCF>9В(+({~^L;@:hEx|Z)>yEm:9Uye'kw_50v˦ 曊R}u9ZqugzY{$ľW%Oxzњ}Kд*Ugr]|hq! 1wҐyI]q/ 98.=$b6J%0`\yHTVjDi,. +˶3.g]ÉYUyvI6­RJ4}zذa~}1۰1{1?ar|+D<8ƷCdpa@t#&it` [WݩRXA@t\4p\ 9)'A4jQ_tZE.,jK][vq\%eV/"\QQ]qiREM%Þ60WsU[s C=w2SW=ʋZmq  Hc{* ԝ?m)e1121c”k7q{yՋ6…jF$ijRq1ۻNv Dsd)i}Ae3fGD̔g^(0B}HGuԴ? ȹnG3%nfr<}ZOF_}c J*pBa q w k7)z{5ryNP4)I.W( QgXk\EɆ xnO#) vu*2K~я*KM_,q"Nas.ARNL44h[mX' CWt]Q3*-O,*= "4*2v@C[2tyRqq xJm7WikenF- "] -z<&wWY?rxaэޚeomX8]"DnJE\ 狛:udH|E^|Z 0ij` IDATm7۞45b#IiHabli~h6~ 1ϓ2[7:bNL"U̐6<4a 8mJ v*n`ˑ^QW_p' 9芌6/\W] .:uǪvd- _x`Dؕ['Yf:o}kcS_ڹ ^#^~mëJ/1ϯ>>;U_XOFs0g^֔{I~rxw p{#[w {IESC_| H,#kYƅ =VUƅ{ڱѦ:uX_I06PG@ô/{ޣ@_q 6.+ a@ yVPHHHHII"]/  gl]YDXyto$Y#fmLibswߘ"“yRb]Tzjvnzn ьA<*Y]խRXpf彇;pѪ_w[ё"5qD>>2P^vn͖Cc{`,An@8\.P'y ;V7fŢy ?Q(5=<tЌAAAY,  ,AA|мX"/X#1>žOnju/`xɷӦXvaNK =alJ?|.ٍ ]+,R%a(OtLf;³wr.x'QfUh\(1꒼j. eWU[55e5(&z\Z +_̘-_wmW噭l/EikB_C]"_` |e%z8AEjG(8u7/cSeߺ& TrWTQ~R3/"_s=㺕D 0Z3f ݒ(xkBw񡽿%FܩO\L5v_yh'ڰ'tpa_ K6I:q[=l,+/ڸ'?~4vם:ѻW.>|/?|)^\#VQ.>'۞t4/Ċ]ݡw{k<|C\-_)9yWbįx1uwAqQNvh]N 09G|} /9g&1j1Y9=䵑"JO,ZrSc`̞l'_\ߐ">^|5aÆhYU\$E#i̞.)f$q,Ok3a{C=3_O’3NJٺYaæ}?oذaÆYƶ; H 9*r  -gਮ0Is5T@LߺRc{KL,6+=xAF.'<xN @$Mw]VhU(E/SjKyn v5vsr؝4Ν?x pt""gO ?_u4ҝ?m)e112-g3$fY\Rҫ<4.F]ZRՒJ*kRPjO;vFgdہ<ᤇ3_u -Dz ks3^,yG1GuԴ? ȹnG3%7赔]MMȸy 0r;`@TRi[Ə%nNɍ ~yC071ghΞU͊H9 $ j8MZ ^)Iϭi$߮b^s6XW|*.x(,Ŀ)Ad^|\W M*>= ǒ_cW'KAZ#Um :lx!QAFU%QuhrUN3/\T 9F)M9:ਬ(BmͨefCY,q0 V|F؃p6ͣ/͘ gk7IM^]qQs_q 3XbE[2sT%M7ܚf. HQ~ˮA4\jy֜. HIȀo^>.mޯ:n cvoIOD7U[R΃m4fn 08!]+͊~2s|& dR.{oI,= [y]]3-G( ֐zF gUֱn4DRy8 /GfZN/y?l@dW q4̼/+({~^w뒪 vm w3ؕ['Yf:o}kcS_ڹɒi6nhVŇzN{_/']Nz*j<z B{ٕ3u]CX<6#5,[\˕MWVUƅ2`2TE U_Zq~^9ש+,l蚲bYXxA$3Tʂb~Xd .P6Cy^y+?1>GBBBJJJ~IHЅQ>d]>I2\G^豳8ݚ=.>j^n@Ƀ<)m.U*=]bi ;V7f=ImhFt]V;y`muA# 0 cunEc r r r8SرI7d,͋EAa\uDA)Xݤ-f   bAAe  H"ݗHq/?ҢƬ}iYӑ7||j5筓|K6ǒV ~|җ!%|~&mASEM~y&-[ i A?v`#Z8>1|WU߬YԷYrȀ<Ãe2*5/zM"߿T*Fp\UavҴӬQ [A*%mtoSV[]jR万DljM;MW/5KJ{D@"[5:^ց\.`\κB/k5%$"ݓ❴$K;^z0X<:u1hG}0?{iO5c{zIB:\9驱s~MzfOƥRljX͕ɸil/Z1cƇW"Laf/v0SVLX4N泝~a*0LlzT&/rNTD*1. pe㒤@#HFIRZp) \ozG4- 2ld!PWxFG\MF]v70$"HҞ;w+ϞNEEyA68s=lluΟ̶Д˲ˆv0awrH!C{JJ 7.tsgieņ)Կ>g!F2Pz-,z=.RM?􈏏2MSPwoo.3YW2 LP gK:npg$ah)xzٵڶv|Cnm-CBbK#Ij8SE$0vkh>[![ϭi$߮b:07J4 "q@;L~_& =o#E[7fʜmI"/eD^ 56\ڜZk:5PڶʃZPUi ն~bT%J@ D4MchxJmwZ{ 6 eҥQs.s8|-TW\.\.F(J\LC\mQtWA 3  ď(OIyR{(+(KxhWA$\kn S%(y יִ6Lˉ{^r>OMZ8]=D)S||@kfI~hD#/鋇^lML|oͲ6,zl?iLFL d~EfI`_oNf1IXSېC zDF s^xcH_D8O7,Ԕ6'R;Ccm7@Br8|izI-!!XyIHƅHU muakpikԕtX%Ϫj5άN苯E#uidr(emQz-BRo#0Y[Xm<8uYm5+ [HWKzpvtY'/X_I06PnSa[8_wyM)"& mVk;BbmCϿ1fu#/ #VsqIȰY[>lܝ7wtt&X^Y99uÉNa`L;\wvt "H7Suz]u-DAu  ,AA|P  t>(EAA:.ЁӴ\_F\(ӪWj,N6(~j< IPDJ!8bEdx&XKa˻XP{F<2iЫ!M25%ʍ-Z.A%Cঅ^MG{N /vKZw]\:zy[J'nIyxzwJ-+UwUn[ A= l[ }CB5Y6L ҰGN<4Ӗw5߂zFJ-ri2/W91J_zs׻ >tjd x2׷9$-m]wZ0(>gpЍk=cٶDAVQ:o@: A}@wdAi<0hװOڳA<[cN"[5:^ց\.l1B7h`_iM^hqfF'iՖdgmQcq2ʊ*D1 <`Ē5szP|(@زҶ~GOñ!8k7cvzNy׊ t2m5/VフguZ 4P2ރ4l02r/B .]JؚYqq\HGB1^4#lqR\@pGiiKJqH`quɗ[ѩE;1xf_O{z̨ۋԻHԹhElIOm3{6~ u}C] G'C-O1+ u;) G{+&,rYsk9Ax^^B&_cP-5S ٕqg$8749uBIopm\/2 z;M}]2UFEi}w=ʛi:Zm1 0#CjluoͶ`9}cdgxA4sc"mJNc΃-RfEL:$к&0'= .ݿ^UO QADDNm6/XbipQYmQ4w̡-0iIBI4aܿ?j᪺RH[݊YH "kLJ*U[;9$\E :Dz]-O6VVgs~IwRɵ?o>o#E[7fʜmIF~IHHEQ:)n}@c6Y0f>yajKcM¯[2wkUW\.\.P=S24G%'LZc0 ݓ#9;:DQi=p(pHoIYo-yD\96q+ 'M*jެeS HX[3"}SR^ _gk::{`IZ:< :ɻᛡCKn FgÄNZo.zmG Hf .VRUeqPfWG>K6;z_аRtvv(ęS{x[:mܓt?ǣI?[ۏ}ׇcCOa)C0Nt΃=Q  ,(Q (.> R?pytꪩc%nю`v |Ӟ3j"+,,,u.ZrSc`̞@Lȭ d+{x9՝R~>Ԋ3>`[7k~pb¢q/7/1 Hw[=%jb1@ښu?yDF>ο2*J{da<{:ڌΑTթ;2BS'.bbdEd3d#қN`m c캨ҩϝIArnybPV <0ATTNy*JTу$HNK];ƋvH{}[xz}:$\E :Dz]-O6VVgs~I͆\;_ŜG<ڦG#Etj)ߖi' )QJJn%i-!wFz旦p`utL킢(pŅP;n92cm!kܮjiY'LjRm낿+ w7mUP 7pp1yw[螪'.AOxgػ^~?-: 8Ј8G^n5&'j⡗?[r76u͇L|oͲ6,ꢳHA t-[2z*HJ;mU%;|6/\W] .:uǪvd- _x`Dؕ['Yf:o}kcS_ڹ|S8W$ǩw=*:å}OPW{knbPv~݅  t>(EAA:"  bAAΧQýppgUA}x{`+=]U ј/34 #ok['|;m^/niҦq40Udo2.AˆU?T\AuEiy:-sr[[Ty P߹Os/$lP nW#]Ǖh,V$+˼u]GڦNigDeIZ:|zÇs 6?__߮>g|^1|̸ y]/>!Š|wg|Z @1׃ί9neGijV; ȃQSZX6I .\,;mr܆ yXIu=ec/dUVaȿ23{Ia(*0Q["HHzCu%o|G~c1ʴfh\HʷuW$YS1|QW|vUݢ`e &H!F)mjټ ?8:?@\9pQk}bҏ# Pۯ͈ [o}1_}c|6%xt[:knӊ =R>N UeZ; cIXNB+$ u<-W+ W BB=A2,8*WIS2gZ0<<:ܛԫ4VhHbs~꥿2hEduKiWxOer?W5O,\jq[>g=_njHk$ K!KVf9?&?gù)W T07LfL&†'sqcMa_`,h(ET^r9ȕ9)))Y](ՙZ|}9._ήzb |UM+ĭl(yFai.4Meb92o!/5#;j,5 vY zva8 : H-qDsxt $ځ2*J{da<{:ڌΑTթ;2BS'.bbdw # m+)1@DӧOL`|v0O@0K@Y:aShL)0B,nU4-3qX~]Fr=á--kkL@(røll. sZ8{{{gܴYpA@KYv0ϋ㲛&$$5 tUd@lX5PӮِ$r5t( QgXk\EɆ xnO#) vuVs;Na5iL{U/?6a[ ~Ƣ.@e|P3eNfvi7Ma)?$wk%%:֟;LVqx`a,.2437ԧdi x`}R[?ζ 4a~ne^(̺CKyjjj> nVJ%Ͽzq_ɾآa7n?&yr?kJV~n[aEzEQip(pHo1ඐ}KfnW$ou3m玢/?r0 <Hl0F#OA8f_u pRNpw^ܴ&wWYYc1'.׵cm.#REDT1Km ^+ \g㛪{s7Iwo҅"ւ}]~(( ""80#"𢲨 8:hղu6MtIٗ{s-B[8OHyν<85]_TA3ztFYе_ .Dž: 0=G3Wȯ !O:9Ct?lyS[{fǜ" d2Ymmm.Er:" QB_-b(mfs\h3l7]K uSlrtuubl 'z'00:EFN kJ*u-]Z\1{E"}EEw?Z刴wNZr4:ZJ|MAl Y2܉=6iO^½0kD᩵J܈3F8tt.18s|$K{vĹ].8\@nc}T6kƤ@i5b?i\N 8J #!p{iGSC.|CE%{tdxsE녴>'r =s,)/VvLG-%B޴r#Ӟgw%/xE W֡ҤLeVEKUr ^9H"?>y)aھk jMU>!0Pݏ}pbb֟yҀB:<& "hu@ bq^e"T@ 9W1 ͼ`X,@ b䁼X@ c`eJF tAzEM%kCfꋋ/~#0&etHutm^GO9)jcBF; :]ո/)ybx+#x@u/zj7\\k}Jzھ3'}  fl𱶷g|0ؽ oӎb*@jLe-ܽen_ ܛ|Ru;/85?/_-`ʦ@]gI!&3UZt`: JVo/.512!ɸz]}I]'#dV (:3(3Ae/>Qr&c% C ̋[@XПV[x'hWv9`g}I?~d|sEIɩ>2{}0wd Rm^,<'rk2}4HN'|'Vm},%H,5לpT TQ F$ +M|S\i1r\8GxFm*cre&&]gIc|/A#ՌΚ0.%ZD䌋i99999c#$vtzfvvq_Wڌ ccD}=wwkOR\$6ټ>R_龚Z)_YvذG;_w3^3o;x?Sˌ o?~kBqv;㾝/Lec;{,Yk>ǟ&E.Gp|ҵ¾Swڟ7ϐ$|1Ak?"}Zz&M+12`chV|w~L*B$@JL~㋟#ίyA:ey~u!G 4>5=3+k0YtMx̐yx(l|z(r WbsL#鷙M%_FO uER OsY~~~~EkcK R 9,/^eѻd4;ϟEldH#)KsCǐU]6D]]+8B˒ W=8eMVM vl})=IaǓ/)ٗNwa[_){# 9WFdg dXԪոm Pr,4S?zꝋZ8/{q G z~?bP)sQyGVk r3vŷ#?;w~йą@(£N4"]BVUthaE׶C XLIS${Pmm^p8vwMk7fn&.aSx|tw|6XOL['1N00gKUO[L1rco(-l#x mFe>$f)ޚŨ 1`p8#ѴwG,1II;avs!gn : 0pB{ lc)?<)8e269{':P*8a2kWȴ9g^3`-3?, "}mZ-J)B1Deei+̓)h k>rX0[\ӲUg8e@ qPo^fsMshyexu66u&F Hyy|F1Nׯu8tUX7+U> o= *A?^3 o0|{IE]H D7j/[z@0a2#Yd X\,+u4^nm}XE5}{?cx^cξR<1X,; dQ6l&NSR~@^>Ƈc!(*]7i wʞibA}[sA0*e8AlH͐y!:02|u43Fk>ܘ~q{HD@[: a"ܦCgx @z֍sVĩ!գ3 "xj9'hxӻU T&-)ρ(E`{gIY0,V𷮑EQ8?AQrk}YAˬYq<f͢oōeS|Uxqz;3TEQ8E^2伇 c14:E;mnLdT;cJ֜g~*FFX~}ͽ3ݥ[۾|a#ouLyyyjvC)PM-br6u4ubJs5R2;V/97O6tJh$1PF^:>n Mh4t{R_O@;D9]~bVc+($vcMcmltGM˥|H3nC=x<=CCh&y"]99p%B޴r#Ӟgw%/xE WWRBGeH NR1l>g+8~u%{.iP+w6q>dJΒFcmo|y}D(\,QtF%b3.AkH됄OHSOYo[^>.i,!U])lSV  UM]l{3Ðda㫓%WZNouԔ˱j3C*ئ&'ƋHNWZlUo(p?"9U45_[LFg&Fgb=uy1c\n5sWѱ1=K<`|hVgxmD_pi|RʎK4IQ֢~'Fg{*5IU'zrta #%c#M~^d{3ɋ8lm3t|kcBl5Y)nXR5pf R;6lcb$C:F;,n ׈aS6/`kLj~>kj:#.f ݚge޺HDz,E{Gk>lu GW6R@c9KoVoWxMY)tsmzW3j6[W6er5{>ؼ`_&%{3n}>> ^ zC?\ {d6f>+LbiS'ߜ %d;uϲnj3і/T~d%&){'n~.MAQISIJ^W p2L&bք:?2PmPjU''bdoEei+̓)h'80 D|8>]& 0&kTM@Jd!~̸hpTQ>Hў#s؜O-;M]7N3&sYL6'qT)d> e0 1@9:g, ƕM~~XUl8CHQtky~+)ɉptǏI-ufA3@UD5H2е6xoZdm@c`~:a%goscBôzip IV(E4k&i/1E-uzv{7>~ھkP*1>k`o)oQD Ã;}=|> X*Eq P,a8Q~ѰHMqWD]uD|K/!~.[5 ȇUa"Շ{o{alR7 e&uSlrtuuʯtTU>MkXޑ+ey;F "?`QqR6tWEE{W新)%hi|2#q3^YŷeN?pe+#E$AprdIh/C 1pRm6{~nw*[.ER s|}lϑjb}uDmz&ɸ,MM͎EǥH0^z0!u| fC^cA4:#TiPEu>+V\hՄmm y6E!{6<19#f'\)t~e-U:6Hua 2I䣒ϦfcL4|<]ώU:迲]L Z8ee3ذnU_<,[J]Z4ctUΦ05癷_{soЭ a ]w5|%9`s^4Qy|xwܝMڽxl/Aڣfl:o ǰ}@`@ ɠxUB܅ SRbdmF ';+KDe-j^[xbE munFqQ;5sWѱ1=K=`i>ym2D 0f0b0jlqP>ZWzHH*u4Z<4j7T 04>50-155}39}oګk(/56fY[R#D' ]{cu o|?~iZـxMϽ~9%oW R{ oZypō8?m暏烻IQ`k|c^b{yyyyyg`ϫ}=yy~oe ϊ̱K|4+W>;_ݰs|a~7@ XfHG&5~L JNP<w{Uni"SF)=,h$1`Qr8JWU_ԍ&qtbrq]lT܅yGRa$ML?Wfz ac$xYrLxۊkj5.iggB2}Q߮%4au}*W rssss{:w.qVDπefTOOsOQ g_婥uWM!o@ #ep\$d JMqa-xppWUq#XM=#gR<)#cS6kN*f NϨ=[}nіovmZvwKn~G[oG!$?0]Ϳ\IJkjyTi|FD6NdB̢j͆p~wE -O[}eH)&'?x  |>atcu6 CC'<~t\ʊI>:F<<H28}zkO7XX`Xxgk[qZxaMw,4c8$GHQ"ZajM\lKLQ]Z/@u6e<~qu:z>篝:c{o@ #Ë( PH4`XRmʣ?Ye:+jEd)td =v(P$oڞz+2v)qq6ǿqARR]?} ?ZM(:x5vbQ͚EMElTy( z\ ֳ4c{w A 79%+x0@ .vR-`8Gq-V;X::HI\i3@'+ 60c|wX q͌+B7DDYF|K/p0Pݳ~wc¸ɷN(3IPSܜ*fsoLV~mil$F_/}X)i4Zhm-ac'SJRb @zƈ듥=2+ q3^Yŷ;1 ʄZ&ǰXknW4cdzPh_׬:Am]!qߗXq"GN7cL!p0hٻ3S6>'|ò`-ˁ ,_ە0kA3FWlhSK:+Ysya7v_}} `ij=T;cWP@e6n:V}_۴ldCQQ `\ilt8e\☘pڨ7X.j!P əf=90z~_~>+nd"Y^<_Ifl:o^%{>z8zQ޷K.<1-@ d';L1&,?!'n2A¸9Y PqYrvΘ(괬$y (&_fvfxpje]6k./nWJgS]ǒ'S'=NrNKruOL=lOO.Xt{.P8͇6muZZqʸokEA{FX;_%ō:I`b"Wmy|WJG]q1&SHb}À^ aH9/V5OQ`b5SU|ٲé[: Ro4m</UP[OUv]a^,){rt\{Ցm[eVej|g~9}xCs.9Hf vxlwbu-),]gIk;.Nǻ/W)"޻m{{ZțSWo+08sg& \}2}٥9WeZRWm=0饃n~lY9T@Ed|rlK/] -; ڜmdϒ?m-lx|Nޜ> ^ <ӟ `Ȫ6{nP +G~S_}ouM:&]cqt܅yGRa$M/+OϾvb}:+5o}Ի7Y١Sr3_,v)Z,pՃSdmt "Zpoևrۣv<>{x^[S펥tIm<4&̍O2S\SבYZ|_wv\t}*W rssssP_jtި+H\ȵD0n[ה7fM/}ɂ^ep?8l\C!O IDAT8p+5X9BK"X۱EzRfO,a"W_U vpI,u75}djpXWSֶo}wMzA "sr8yAxk돭mu*Wr\Oe>]h76-;U4eLRR?(KRR$Pj>WUI&&&T\qj>ky)pjBNR : c`8!NK)쾢 -V7)dt.Y}p$`.pvw[v\ <RMN2~Dk5H+]$ Áwo3~- f{tB\SZ껚3}6k6{F/q-P#Rfk*F+e.77([qU(Vw+dbهcʏ}mQaf}w\M7w[My|@«@8p1[Jc(pzcݪG)X|4b<&%dʦdw45{FQ"1F-6{K[o;{^~ajIy1 OVp`'o|=ѣә,0Lg 6' e@Z6/ɉ= N@<5>rJddfTN EOV*' .z (rR6;YGIa['O$)2rb8]PLa1%'eoLwUTР)uSlrtuubl y,WÁ#LC-d%@ƈ듥=21x|?RYW新)%h@^@-%qR> w`7TTn'㐂;W}ڭgܶPڒg& r^ rA[>n+\hwcv|o 8~NL&zÄeB!fN 2 ԟh*s|=]X=/9 Prk xgt=;N996Mu_2Gq7(bZ ζaStg)h~qkFp^$891ӭez.-{7|~jƧD●}X-޽evZo=a=OB8 G?]SB\e ( u3uca R i.CU/n?yܴo_տZ[Mx~R %5 +>moq}5߾寷?ؓ3O<9`={|7*ޝ1wgS̈́tg}y ʚb&;lX7zO)H{.f yhQKA#h˛':z4;7N"iwVL(;Ԡhc3ILXרm9`-,mr?6='kV~qg^ksrr{rQopcbi`ٕ3̀*;` ySL{~0R3{Em(}0¯b9Cߠ9j8BPHxR$e[sݻw˷nl}P+8 t0P9B 5l6^_ 4@ Aev|o".x!7Ρ=nW@ K0P9B @ b@ #"@ F;N8VqAriD{j*{v 2l0W}q'h˵`߃`?vɞ%[:yᄌj_@:/R~Xk-ּ/˟9%n.ڽ~33xvjB@̵'3_g]3_|-}֣^: g٫?o쑕s?8yqdz!b3XL$Z-$b#6EkcK R!x~~~YsߞVUK,(IPP'mUGhE$F'yB 7"" eII9:~;$lmS PqB%c$ncsKeF>Lu7#|`f~6V1(~ c޺OPnyh+ ijnU@,QO{[{UamM׽s[VQ E?mޏQz*1>GKWyHE]/E,Cn_>i5O&j8QDB1@0A-!Zh3Z,`Y;}lE/*R=:#LdΎa/x?0TIpm5Y>ޡ=!Rw|xxSwwX]ƞ5БΚGkfF_qAUlV+G(dP& mh|Jf͢oōeS|֧*,Kv)Q S_vSx|J+ ᆛS Ћ@ CIZ|.'RfCaAaJ1Gr>Π cKdb#2I8W'vS'JLdvEJZƵXv:1Y/벆b19X !R(lIRƷق>q P,^OjEz'/XZ UQ |D~T]A' 9%223XMW*'˄"'+]Tl\'|0=RŖ&CT$Ke ~:Ae54N)FK ,ngȈF QqR6tWEEmuSlrtuubl ngewSlNW7ſ?*9߼{7}//A٣P;?W_9m/s{Z:+Ysya7V`|WC/QJˁ ,_ە0?\^~"Хn7/ߋME_}&M'[bwL~e{Kz ޷;5Ÿ_םaD/7mUj_"7iu(&^K{۝.ȴ~|M$ d/WSAcjУs%Q.}u  z 4@M%׵;4@ }[w+@ "@ (E @=z4^^)V<49yȠI4dPE nlH;o,% Oxߒ$&$qm=/`غg~O&]׉>G\HfC?bo-<U_x{?SN} ԧOp_ZD߀!@64>rn@\rR˹Dt㇄gD/уY,_p-.¢%d`t+ 5\()d`DO#PDvG&/ܝU豁Gf0h <7aםQ&tW_ m'#?9p:a/.;>1[|Ň?K5l\qc(zS8ˋM]ݕK:}ZW_Vhe2[m8A& $aa[?ְD??`c;gS!mQ4DۯU|yݎ~gVc<NPBFeK4•îWa{^v_{Ҿ~6J9㴴d~{}vvȁ}.uw;w҉N* '4YMCYWg뭖#;~I#?5~ٖo?4dͻxfqSW}㑣l#tiă]G՚CdsZڑe~уk'(hCGy*([3~3RƼm_5.Dm(#_^s?|яxQ`_ZS%iIi2t[BɪeI)Iy \J^64>XOJNJQ1tՃ!|v@Vj[k1,|@bRJj1Ju/Swz^PR^"E1Jbl5IbBٵ DPl?Wr-,ѧΧ=}.E,~yiKvnM)l%r/.sibg}uʈp@FO>yO co:BsRSSSS/MH;q;çV 3 Yظfm_X$6xtɈ/t?(^!(z+~5.T_W*4BFJSѳ<@3\8I?psV@r2)=q̹Mxv~[iGRSǽwԖ-)BGy; =)8Pwʆ˯]x?ț3=IkC$| 6\:_hh4MOOϩz9l?C{&E|z &'===== EX\p .eu. OĪrΜm55z&e)5dUYC!$IblXcqgCQznP0SWmy񰰙j@ė<쨨ח%`3ZmDO`r-eź>M ?4~%OoSdKzqZ>jdZw5K l',VwuRb JNO#TR+]5o/Ě!SX]pbĽ΂KZkjE#KjO1dr<8AmrZơX5b`9%}pcľ褄3-i 2@}+Ri&/zy|b6f@;YYT_'STԒw"t) g,{ @mEDm1_u:f|M!͔7V.zGdxQ1\|pY`l\Y}~Ad"60pm\qT&&m .ƒ.!z$2d@0+!y:&ylF IZmLNv^˿Y5ô4ܼ;i7[D}c}J!1sfu }oLr}@Ưa][fC[O_~7>=öjvٱTNto}I|׫>*0\pm!gNaK}?ow.`󋽻g粭^Rb+`_ۿsƏ~[S|?~RooADhtvQҜM?ٲ}˶d@mM׭wr2u: }RC\tt†r1@Wew;iu(GcOJCW<LRdztqkb^c5 SX@ De@ b@ }βX:<@ tCw\o %DjMf/ZGf=ް 5}n2^?w?F^~qu%*@ IgYv%l=@8x=^ j 7A@ W٫nzPrr?-Hx'bjO / (ŮOlOńg8MFEL5' \T\CiPYIXuZ1N^"Hop˯]x?ț3ӞOYR4vޒ؜wfLxe'#4JPeZjO IMMMM$Z`jj7 -xSwu}1/gOxha/Hi\qNN =q̹Mxv~[0穫8I?psV@rVu(NӇx" :b=vhqq C45Vl7ru$ c\a4,tk~B-KI26XN X?IPo+i /?yK>+ҝNYR?sfeݚ? 7u\q:)Yv%6\."o:2,{.:đ Fsh91%˅8U~i:a7:Υ[$rtȭd P kV,KAq0?`\ 5<^4;CnzK_onX̠I"uTN,b1 }ğ=(ڂ^ҋceK2;<)}V=? `1Y@c6BCE!R08a)/T"W .GWr܅ K[N㖜xv˲ElP3jP=@EQ2QDLתqP)KVF,.߾bLQ5PR& g,{ t*UK\$x霧&2ۤכ=^a7IO͔7V.zv2@ܖ`\+VMOOoW $ حN3&~sqݗc~S2꟯=?2JʰWjo7Ϧ)ʶ[>* Q ?{Oe-+ky4`E> a[3>|nJھo&)v[P7Wϭ|feʌ]u Z(~2BFsH=?|bFq8ns3' 3̅~}.J!u)IUzTܳxӫ=?iV.@t[ꆂ\vrdj?B_Z퐄yffnryL4UBN o>E }[;\v6!S*< |Y ¾ Ss:"q{X,`$t{[7s@ g@Y,@ X@ D,)$ MNIJ qsQ ' zV߇?LwbLaQ ;q4MNI20ʟi"wjI4âSn7I>zx_/طq 쬄f=㻤,xʆ} 0`wǎ;gڗ34=RCL[ŏG~;]gu?ͪQ5B: /8~Hg&qذaôw: *2ۘg#ʨAM*7ij;&|IdDZRVEFY67.&^cK5[sYo7yn_Mc⨌E{)c:scz_lߞ->CtsR:W֚mCdj7oKٹ@64yFGnc¦B:Y0!M&7ƕEȅlp[+J[6زm((E*,Ӥ+)5&2m0e"<[r'Оռ~RnA_Vh )m%"KEVT``Xt^ݺ?BD+ATWU  _x~xz g5yLrz;9sU<&S m/ U˨q*c[OiSݧ4_o-Ab<5{֮ܒadGMb)o蘷~#-yJs\ι?VJd|oՎ&}PӦ@&a2^'fͧ nSd쬢h\P} V̝4=G6?o߶ݺEe;_VZp)oR;Ts!iO᳟?Os SG: T֩z7kDP~e'rJ}U.65U2LmZ /M_vgh ] ɏ^[KSw(QIS#(Q ~CUA~hB,VQfX*\% %yN00*"H M"Ej@6lYje,g+SGz2T(lh cez(lh+'$#;[˝*c&GH[Kxy el\Kk egRnFX E]IwL&KxZKbuՄD",B`):s& [&ps,faRyZ{xBr-MhgQo{ dmIk* `r; 7$Fo"V`ƿL~{\`xCqv\tLpKQ^pc8Wy.T쵯N.z[NȸGÎԌNep1O7 oJ ]}TJi_^?{崿?%D o?gysF(2-5u'o礦>U%0sɰ{h6EڃT]P /NxposPs;uYjjjjj6k5wwO`ĩtk><={*k=5ut;8OIkDj'NYqB6h%9̘NF,|i.J 6v {K3RZo*N%*?]c0R #0  dX=Wl)Q5g3*lIFӻ|z &'====9Nt:`,qP̠X˾TkˬxJ%Vsln#/,XRy./=====̥z [Sښ HJ+/f8q#>NHTش)ͥs^dg]:[˩N}yr$_2[<@M,,t-IzxySR@V5\Ü/4 C/%%%eذ!lfPҥ՗.TsTTUg$L&k("HgV]ݑ<Ẅ́&S& &~.QXcЏ۷`w۴wom7n=^ZWuLW@ B'\0:5ǏDGJ˪eaRJQ^VEc(4&Zm)+ֵ DGןwKa՝,W(>VK>Q=K9 Y';ٽMyYd:E`Y,VOԝzn1%˅8U~ilٵ@]R?sfeݚ?ަ|u%CBhN%J?)]c0XD J8FcM&5 ,f%g0&f).wwzz7`.cu*v`(X}vʮ=൙,$uZ`E20,"`JNX%9쒨AQ2_RqVIR*1Cǽ^`r,0$[<6p$I$G;XqXNfۺp_$TnHO) C?C(GJ!c8S/Ix+HaL2f\FK}tF褞ĭ;ݓ;>Tx|ͭ/xZ<b1ۄA,]ihD&_yQ2GOtӲ˧b1&>&Qæ:;i٫"Y4DOu}/mݷ<+~wդ4Dҍ2t]]K?g]5o/Ě!SX}Ou5\$B[d(jQfxŹ' 7ԺqzǛ`Ǵ~r)ʌ{֝uyla; aޖ4. l%U^,ED^4ܱR%47^#9a T ^SlU3]p>^E pW& 0 c0D2p vO!bB) IDAT=8FA;tb)88Obd!Avpy+n׺=\z gK^}~zs &۴hlgUbsFaʇ-pU= aAも8DBi_|@߅}gj-YBͨ1Cr4E+UK\$xL1,V6bYJ?. L#[&.T+!N:W_S+1_U &+oB/Q4Dҍ2t]WGFX:1E/O 1&B'%+U# sXo_1uQ~gq|= D'KQf+ߒ{ 6%(Dy,$\.!~:-n),6ˢ( u:a)̾XU/P@{:]f8`,LZ]t:("4ׄ:2"W_Tn2W D 6:Kb"Ŷ JNa_g%8jJQl 즪* +jea&]q^XKa7VsCBi.yEH V'zvnq,%HoJ.6R 9k 98ۢ/.oX帱 ].,ʐǽ8 'cl*wzYO04bBşн1\fzjx{E di,. Y9P,4ܼH7>>)h:n~a>ժ־d?#ώ)lkƇ-ܧ;ޑ=41կw6 y|i[>g%|lg锆(]duuR ݳ~#j̗!D0?5Mgzk[3`m,k_ߚ\vR7;l-2cײwhN%J?\¨D)cinIf˯}GblS*m2mbjMl(meEOPY&ye?w4t+ 6SM딙S:ph0 \u~źF z°!|kCZg$r?&RZ( v2n]G{yOކ/nv2 %~us;@XzBܴ[o09!Rv= W/@ w yZWZp)lw@Y,@ @>To@tҳYfSf_7}@ "mpyӾ]}b@ }"@ =U!))IT-\y!8p1 C > :hȗ))_$ƽ(zTB7j]V$i7g^)Oxߒ$~~wرc}9Cӹ𦢙uv>-s@;8MZW*ca2%a|F/lܚz/gXa4ء;.G-_ig86{o>meK*g6lO9o PB}o 1ce9U2LmZ /t+K_/Sg-WS?׈7u٢ɡܦ}nD9JڑDm&bm3C>Nв,Oikӿ.K4%)L<靵f{hbY~hꦴK-MSw*(D)y۳Sd=N]Mxv5y>0̔@k+^y lH)_l)pۇ>:)C$TWY^mpx# UY^IWRk"0 QP_H\PWX|f"Eyj +>g%R41%""h/2E(ʂYuEz~Zd*̮i!FUpXTcqZChVvpwa5Ov +7b'iٍ>d/ _QHhׅKz'MȌKEV~] dH3xhr򐁑Jl5IbBٵ DPl?WE"^"|q`.v^!@,W u7i!! W/*|Oń8TT zK 0}i?62jEPtnq`*Љpc8Wi@i-|9Sx_"p*>Zr6v {K3RpR4iQ0OyK%)[Av߾|ML:D%JOtGjR6GW;Xҙ)tyJ̩gJg]ihD&_yQ2GO>a\M an.+aG;XqX&t\p_͠j4bUn\pmUϪvΖE;_epoA;ς|ajHz$k\z"Uyu!+]#I{In)0(Tp8D%4<qG8DSco+m`s' Kd!/ 2J.|j`8uw9Tb, I-܄x]y^-N8}"'4y+1L77Q'SM^;oq]6:COA\.bF`\}FX3d'd oV%k]w|=(KRyi[S.ώML(N%j KK;^?%%6ANS,+LJ>5<[]A>M7sC\CG~~ x8p'jMsuUV&y/,협4\.YYT1f>|16= "rZ?;L^Pm͞%*'bVMt$V?bTDY2ȔSNךt25ML'KQݍSNKsxxHaZzg#:kÒl.a#FmCӚ)o\/+Lտ18,BqB.vY,"<N -rq/Hi 5 \qHsEn~1T߬s$|zbTB3*di~Rbpl0T&X.p:\+Bp2Q^.p)Y[TʋIaNaGc_r4\Q|X!'NҲ=OS8&Ou|ojH` V jb %Օ[/U5BCy].# `< <`{]%Ui/`_Ws.C#s~gR'WJi[7Ϟ8h+hXnQҜM?wTfZN ;ޑ=41կw6 ɡxpZ#;ħZܻ =]Iuk,^me-Z.S+2HgӔKi϶|N)C0C^sK"g ݟີ_[RW:} k?#ώ)lkƇ-WCY:e)KRyiZA)r[v͆ڡ+LM7ݷV&~zz[UtR'YZ :C(@V6k,ͭ4lH,P^$HHbHgp[Zcan[cY TH8E5vXcst3Z3C@z\&}Q٭yGMQ)7mX@zMGKv^)|>`xǾ/u0g7_yF)>{߮ G>ekfl@uY7QyxT\IuU0$v@T@2ߔwH.IfWd;^*;v5!r!#kww@Y,&a6_Yw}xVtl5C~M/xK2ujؑ so K cg0EWݮ),`߫W;ȝ/6;8XVc#j*IeB06ab[fBt5Gae90:66D^v"XMbPm&永p!ԲH @`yu]zP ̕unM,UCNWQ~8?bqsx㶞*Ξ_JfE:Q)KD/Ī|%Hw nȎWO|ugfU\#gZSgO= -hXC3w] ɏ^[#C}}p{:eo21NO$IzyB@µV3M}}9oɜcDMVȦЦmǣSdz|fӌ9y_VZp)oR dYrKp,~b SoVw @*P֝Jgf|6<'aJa|Nfʼ wV4a~lTqFE(^Y0HULap{Y<Mxm5˪mX]Y-)9,v45. 96ʳOOh^,B{$?UVv!*Ţ_ߠ媡?-4dzdK g>:m4!3Y@8f+pSq  y?KB=3e%eb~=taĩtk><={*k=56v {K3Rh ?(vTx%rV<6Y,=䶒ꯟ}@8.jid}055uWJ>n[ܧlG{sNN =q̹Mxv~[0$6xtɈ/;y 7g$ i]T:LKMI9(EQ8wgNgU.雟Oy ]ٳk,.3 B"=ȤSw2PfXQowx&FDIپB}030]Duk2M235ݴlU3i^eVVh@QA00\~}/?0(x89yyyzH$@(kf@Vtʑ%wIGd9]ޫm-l Ί'ff{Z IDATVIgsTdG&Mp:m' CBBYQAAYHLHhy~9/MJv5w[vYHgYA[.늜,2@v{W&QC$w+mh Uy:[_~*@jPVN`{iOS9n=M] ('K\x7flYI;bp*KkC/ty{-Θ% |DM|Io |O=.e`)p1S}X ( TeMu#`miq=Ny>K sIxKPTO,7k}."O2y藞S?X%eGޕ,-mIaa7hR>d>QuZ^rRAxTj&,"Ǫ+6TZ.%D]i~'~^VÉ DzVjwYwplNJ v5zXqE_L#[0ۼ= ;VxoAs{[-"ߍak֎Az7~[4v@ӬɞQwRk=j˻ܜi?L ۖ,++|H;|7J?Y|`ݚK|BJ4-pВ[sl[OW6H녋س/}1R2}->I{lϛv]>tQo)[jM2{i e8 u!C@.bY& E1>:eX^c لsj"1e5kzMǻ4fgg_~S-W ZSL:3^Ξ!@7Ɩ<0tSzԄdM=`/|55^|xo<=wo޻q.l" H$s =13OAK ?EXF.`o@/e````M`}`]>'/֟MC:~0A C}L7f````````9=swCb]]Q ƾthK7s- &Lr\FyF]#zbaa8Js=]>sT3E=\v;:ctTT9}wny>(@K1::j(ⶄSIPFbm469!RX~[b KJ P%&*%zs:̦=68l}siʱVBso$CO<p;ԩuFlHȨ p7[h[}nh|4B'"T'%Eʛ.p2:RrށU)13u4w\9{K ێbIbSlv!KE` bz#mn .vؠtaS9+᭕ʛ\EؤT_+ &.טo{׮jҵإI2bސovͺDf@%j5I *Y[=aC(Y!bqa|NU~hY5gR8|Oj]gE cz&GWo q8LF];JW…`ս{" JYKQ! 0ĕ$=;_žquQ3V=+IJpw;ΐ?>|}yn #$[ؘ7)@~e$H/rGgC=[PokYv&{Ֆ Hãxc{?ټhۇHIQȲg9P_?: ]9Si6#I̷LJx6]Ku6T5jksM[UZ_=Ga>oطCN:}jUʒ`.e{jHZi#0`[]7CAbPbR]|ec&+abuBR0w{]aA3,SGspSM;pjIY Rʋ}+\=S2 *@`z$R>X0Igo-cv[%s5ixXxSgkIՀ `E\-񘅺`tpME$+nx[k-V cSxpY_ڇaI7˹ҧFr?Ԝą' $Ñ=XTE$Fd>--0oJnHCcq% F%1 l0jOORB׬?0{FF@1ضgdz5=3rî999999\t%?>n¬MoLE99;E͜ﲇə52y;NJ3r}j8)򻹓?裢)""Sk2 4Fظfn_X2@(9~4qcZ6} H9l1>sq_4?G}N6>}j>hגJV͘poB!<>ݯ806 > nW(2c8uϟAIC$,iE|no>qkӅHDh.; "$IH@ @AJO@KO@p:M'Z@J^pe$8EI @" 9 x,kKm_ ARU "=JyLh^aP,B!)w,VU(s͹RمDө6#Xz޻7hH !\F]q\8 ORzɁ /:q6as{fm >65C9u]\j(p⇄sՠ03NT蘨 cR a1VaO%pHweK]P5:kKTOI~?"yvօ,T ZV[i5n\d?*0Ȗe;* 6oykΩ?DP-8k v/?±~^5.h(a;][б[mlp{/FY7}5">M9S2EfUɞQwRk=j˻Kݓh.9?~@u-}Yˁu~gIY/WV||rCyuk-Y5WUtun3 ;m =GWv}Q{$0خj"1e5kp")[E uCfy`RˬXVg1lJcR3(ᶶh*6D$8v/ rCx-X|w2;;g]v[1J2ޭ}Dxp\ 7@}NLk vӛ%^?qݙ!#lT4_h$P:%tv-W ZSL:3+},20mPi)q( (m+.lz6LRUWѧτYX`cw /p#`aq8<$ a뿎p' ~2> ήitIM{߼]<8mz{pvPKˢuL3Qfj $}W&gDmmpvT% h'ӟw=?;_?S6}Lio魴I?OlҪS=?0-g{Zh` H5 .\CIP]QP1ruF&Š1y@1XipH*OLԜ>[I ݘg$44˒ĦXUJiǙ؇$X2V{`61AE s - /YˮO3hגJV͘poBgLܰkNNNNN{bƎ͞мż<#evΝ8if>rwW8dDPsj&nhTLEulW*Y7#MQui(R[a;;[jfL$Vr"|ppJ \f{}HJD4{-h͘]׭XVR4涫tai/</2_ JbjdOaogkZ76;Hp[~ml(WQcƦ ]<4,iSzڪh)ݲDca-^rni&Y Cx;r& fc="R`4]̊_;mݦ%' "8|F_K`8&t5rQJ;,?}: r EHٴalIVVG|~F&$>kt9G~9MH"(;H.!fͱ]+t7h}+.hܲB: Jܪp/9A[?/ H>Ktĵ%2tVE@P$%9!pCw:Ǜ!'- "5 l8l pQ6^";;5E00u44p$뵐,FId$] ,eRv8B@Q(#,"QaYÆ=־LFD!FS1Boz%B jQ2T՘n5h `A/ ,PjiCiYO:-e֗8޻{äSǎwj'\l,|qxN]o_ژl6mU2Q^"1~+/Zig%8l 8齫߽è}D(^[W hs.ߋt(/ǞےЩJuK\["JWx7/Xm =Y)'-ޑT;8K:-&/b M"Y[yiu | KʚIDh.; "mdNCR .nf4Tbzㅿ~|[1Bo[B0V*cDaxƪ֚CQ8Yך5F.C.1_?wiAaSJoNJ_ ?HAQ$IQ{6`8x7flqژ86Kkp=a~͇yGd%S].b8I]^g=GgL[Β soKwzOnSԿm F'm|rry=$=!p9髣k|O=ɺޚ`PIP[\U|B\@[eTie 2b0fOTͭn7p8,˅.`B`=m@dzZ N[L` dr!an6yW8 z#$b[m$nԞ+Y @(ț SRR"A"i@|P"?] $bDžgO"xms~+% %R*GK(P(T xCQ!!Bd"="0͇3A(za% "=; S|hcVhB3Te \1~&}CPCҐ(!AJʖ+YZܥ@ʚpFy1j+4$K8<Ȼ҈&"2(Ǐ~;DO^`|z 36y3a‘+yc/%zhK䓀'8$bXh'!p9骣DOů]p/zfbb>p(9AXPxsk@.fX<*ii*Q( a_\QL~؀ĸ9YлXD$8HM=9[)=Mjp QQѣ@Wgb {*1C+[~ Q'ksVD>A:Yt\!]K`]`}*OG(_ c ŭMg"23+s=$H=Mþu;^h1n(K>-,ֽ?%e9t'\YLQYmic[3hҭ sۭv*LoX_͆|`ݚK|BJ4 6oykΩ? g_^be.}ƻss?hLtr"S=ΖbNxmD+m|rMFZ{ngO<o;K6Z9imut \If/a<" QG'EوR1qCf"\fŲ:*x@b CHTWwѬ-Ǧfp(QwWjqXXiƥ3m1<g6'Yx>k;2{Ξ|4SCƻ/;sDxĐ]S6{{6}- zUk?'e~ 4#GB%bғ!Uu~r潰SkO)*1pYcԻݬ8xe߈BBHCRP39.cxO <޷6[;awm 2kBBpC华j(B.]74= \fm~پԨ&}cH8h' c$@Y,>ۯMEQ$(G푏䧣8,ic8>QA<Y[at)wƼ#MSgSmbp. Y9oӖdBac|)|铛>}"D;|okIϊ&*uCCgLWqhY+Ӿ\PA\ |O=:0B2ѷAHʗ1QHZOۦ~; w-M6 >$  vw)K sIpCc9 :OO}i0+ n5bа'C\C- X,07vk͇3A(za% "=; S|:)icVhB3Te \1'4RDe~NVX%eGޕ,-mRsex8<׳mrfߓ,#J#**@_SL?g=yQ2c2@;{̨} WNV“ Gk獕uXK+4BFdKʐ;U_ gMYH{N5|TLs׬'G م,{Xz1~RE|-c#pr iTA]W!-&X&P+ ̖U<\0 b6œBW6 1.>9Ffhz(XpPergS0X[gvhc_۶U9CEHþu;^h1n(K>-,ֽ?%e9t'\YLQYPy_4XfВ[sl[OW[y_>Y?~5c+Xz_beٲm`盫|4P'~fzúۘS8k(~7J?2zuk/] )Pòw (0ۼ= ;VxZ/\Ğ}y틑rb9f乃ve:9)}ig_1'w[ZBIVA`M6(LV] ୸kWw-;U'ΖۏL|ڿW5 gFZ^Zxb=T!*/7bM!,i_zis ݜH.bYI4nx:0[DㅓcCQ.\t< wCB4ki]0M=g6g[Cjqlp[[4=&#<ޥcǎu+R) & Z`o@/egO1sO־XЃ.ܻq}ј;sO~F~U +d`:62L)B#)ɘ@Ÿ@*Kߊ0X2&vAj.kdLX:$<Ƅe`!`rEz}{Iks6,######-IzbfXW8]^n-_,MPzl ATwۡl? XWQ`Xc2ܾ Tܮ`Xۗ`Yܘc-I[ooy.v~Msیn&ju(:bʦqʩqG ,bv:g|{~e ~:|ǼZO%bϰDቩòg큼 !${jp$xFC 1$!Lx:";+>@=<^Ĭ*.B7Dڊ㓠a#Ɍ% uu7[c}uT.6<2j.j$w W7:N;_2 -1ܺ즢| P-WKMޘ44OŒĦbm469!RX~$[b/?%hA1 ?2>N%Am@.tˇ+ᶄz7=:jj%_I &_Y$3^0!>X(tM,iohTڜ{V~UqT5jk/;HMnp*<2c[LAʴ7OųTG.#^߾]ԊNb^}F 7_=j|}gX6s8t TKʩ> )Ϸw< IDATe- >[ؘ7$}&|}U-hc&oؙ C U[.\S,{ۯLL {:ZWsLJ/ϭ!Rw>G58#Zc2y;NJ3r}j4Fظfn_X>bƎ͞мż<# U띡?{ةJ\vرcNJ]`;vDy Ԝ?uԝ-3x-HRɪ?}E\(R!IY0T Ct$`\TPDbv97?~Iw3dgϘBy'pu~V՟( iXJ;,?}*Q7Vwvj`"Qahii3YI.k2Z& !HA{Z*VR4s{(Rpb1EQ$lEtUW|ժ Ȓ(he%[m3EEEg/hͼОX{Hr^喂qi. V[C{kO f;Fv)w^aK>z"-yom@+L=,f`*3`-5jkdNtNsM\|.( 1t~+/ iқNgD "R~'";DDݬw[ .E.N]qS.QEGkuP$!N5`IR82o(+u\،&~SWACo7wڌw٦YL뗫1ƈa`qe5, q#{`Ĵ>Ӭfь5$#6?/{Vlqtƴ,P8&ǡe[fxOn30h-fZZ'  ,??t^mq$E7T0 1t!渹?VR6yw)]O |O=IfmG#i->U.JĄI߾5 wapn_*pGd"՞-M6 ',L.$&' /EO}i0+ nKX,rJvI`VhBIpxʑwMlaX,c~6 "=; S|3kT3%ݝtgIHK$&2S(* ""(( <@\\`瀠( 0 ;Kg},,7thNNsuVMT*ch J,zʲW& RrFD\y澴c)_-QIO` Ho3yFʊKJ1cʞQEE(Xἠ1 w|0x^ %"!46UDDj8O5TGq1I=uxq=G9q ð2 ?@ʳX35a2#Ta6kr+M8Bmǃ&TsQȀĸUG92Z1'ը&7Q+ױG20@)8kM'n4xŚO)Ƃ^{;?? Mw~ך7v]d-ڿmvgx)|tx۹ ]wYW{jӴ_Ԥp_eσ>eC[Q)wN~wW/{tl{'{׊Oo8c505T^ƓMϣDWVA`m6 DY]j<O}_N[my~ύ3/PUw_o@4a;B۾[%vԞۻrC>kwRQi+A3~NQ0tCeo)LNr eJv&[*CGiAWZaΆݞ7".=Iz`Ȧ3Z#Pظii{s/=7.w=S?>zɼE‡ 63!A K2"=#}İH9@="3=^Čs peLO1$!D-*}@^= ހֿ C\u{';{1g## ~~ޏv+Bs-p)!VD$5η81qHBB2#SH`dr7dW$K ʬ"69!ܜW=P+I,wDYOc^]IdC8gyڳ^( fv7[:C.|}$pwYxܜ=;D|=/&XJ*3zuRj,8gXXlfz\sOvc;F9eޤ׹_<.œ?6xĨ$ hmv {0:Ob#F;#Wcֽ{>:=2z,7WגXx8wG;\oUC'gr:v/&vm<2y`6q֋]g}2C}N+Hшy\.N8].DŻA%etpJHU5;02&>Z%NscEy Bqİ"U"mI)qDPS|sNE IXr\yW2ɌL5\$@92Q)fOy=(@ܕPkkkλhG\n?-O<~xճ=>/mg>6gvVva+E+o\\= }ndƵ;ʚ2YYYYY I%9*\ǟi v'-zf ӗlIOajpI7hG|0#FL%d糲ƿ{ieVVVVV}h'#/|=pìL_[ܒx_4E?/'73V8M\rߊ=Jup S罖vz_Y\G7D߲%gHB32O]ʚ5? 8*]s!:Cwѫ\&_=+‚ J/ͽXR'DȀEhsj([!X$ 0B. +"a4YWWOW\.E>Ё@=xSs@1G\,׿% /S$$Yp9H6!ߎ_Rc$EMbꋱ786iܰQ$4UV{E0a4M&`@+_u.&)N𠰮w()]Wƃoz7( 0l 7(_L:,q—D,S ի* w!AlhTs.kF!ꯗY&Kj/nnj'-roZ[ O=bƊ'#zI'7u@fii"~£]샆鬪LN );v$)ڂj6tc]=߭9ɺH:;O5OHJU xz]o>.w|MlOOi:ԮQ@ pkRZ\FV-A ܨ7A*p9T:7#V$>MD\sH./^oE+ w%lxy@ TkhRgco-gޟ, 5M%˛x\~`Ppp:(d\)M) C ("Kr\D@5dBƦ/XXeL亍znj#fwssXy1 S xY ui$*U?R^v꦳*hT3rs_ڱה/at}m]hD2޹Ol.;ۙ1Fe$ a<>0̇sMPI8W(ebNn"㋄>fRzezńx8G3Dj.7՛]WMQȀĸUu&քxPb!Eܕ0>2#2%MwG7 ^fS ߫מliӝߵ]qos;@77vcםb}y|er*znK60Xȧ.âּ}msu7;^TqLe'?z;ﶛ`c{77=o5 W|j6nwz{iGJcn2 =']YeڀeuuޅY]+? =^iگ}j,Ζuf:wڣyP0wApCevGN˙YB_Y!OH XZfb-USC0Tu.T"lWFR7 5C9Men~chؗtsm8iutɖ2q||aЕ)(& )h0܉[}d^;l;d[O.6@ \}C icnQ "R*z@H+uydd ԙ>[T\,qw.'=4M4 uEnv[@ @(KKsXtȈU4aѕ߹&$Pv[tX7B"As +0ϥ @߂b@ tuloڣ@ ,@ ,@ ,tĉ'NnpxYFZvfEȞEC #=N\zr?؁gFe ]|"|{fCpKC iQF.ݶx.TJMH1$!DҞZ!#32҆.FLW`*1#sp_ÕE 62>D X/{.>i/j}ֿ C\uv7~dc/<ٱ[wAa:{Čݖh~sQ^D$%EOo-<_oD)1F7#SH`drן1 ҂2+MN7UlyIoclUF!㥾Q[#ǏbӜv<8p_:CK؄:CǾ??ĐZWf߽G:~dϧˮgOٓ{ԏ; 0ψ8e:˱;cvFܷdWt_,kj2zka'i۳6orAn*l=c.v`0,,7v9u:dԺf'w[}lսM)Ɓo~^,cu v78Ū]c`% fаM>eiCy#1*cDjD5]g9t9~z̉fZ{.~_xE1T qH.oŞOf%:8XWUI݄y?[،d:Y=ᇸM||b^q!nwܳͅg}P"S)7r0:<}INmAՖA \.aFa&W{On6!a0Pr_$9&p|pxrl?~t4 !AF΂Α$G t6v"U$%MRyg.QW 1RI16g?DП>2^צ5*ޛ/1Ss0%x:t=ZeE29|~\!JzeU]HHPde$EppPuUnR漖-ȑ5d#wF9_##Ulc`J]JДh l0R$Eڌ7Bb IDATI7s.u嗛%1j^4ɩ1ƪFGBUr`>q1D&qhqހ"JhM( v$q'\,Ƽw; &d6y B+NnL =xFo0%۟ &j~޵UnQZ~/Ls)1rC7vv$IN;zSeU萖~ H Q:*s{h[mlWvq!nNM7lxI:M.9}-@ >L*&G38A74VQ`5EI7κ?BQadKn0FQlSɞ"Fxȗ4s?m<4x_ A߻ E{t5s\BV_/F5LcJ9e7)RLxWԓ,؊_xx3V]>1oWWŁ j d4wx:Fpnckȳz֫*-Hʎ ~vtu~lֹ랿5'Yxyx5\.MgL $?ɖ{LV @֑gb^|Mlq?:dL4-joB ,V%|>>ru+UbtP7( ЊaR0B!a -gޟ, 5M%˛x\~`PpkBfjʵᙑAܔ6ErՐ i*"Bǡ^vORKUA574 3LPrXg ã\Q7TG1 S xYͤEMv;Zo f++.)ƌ+7Ҏ,~y4T#>S7Q\RyDO}kݲ7{egot:@ ' Zx|>apAV&Dq9Fߨdo@sgkNxZ/]<8M% wr7͛X)XkuGvT~gZƮ¸EMm_2;{.}2c棛Up3_/g?遱5oƥ>a;/dw[NO>dWq/n߿ʷ L3VTʝ{/F8]kyXͳV-hR47.>_6_eσ xz~/MV knӕU;X[fo QVWߜ΂mdZ gvGy(*ޕ`KFzZ0hxQ[ߪ ï!TeF:/_ǸC_g2,M@tt%u3gcy(.15K;-2m3W'e^y碥$$v`~l,]i]׳ xAߨdo+!333srr||9 ;}ckbgl݅6_D:\>ʄ41E)sf(QRGFJԏVir]b w?`ni͗w+rR1ۨ:2<}2p$jR#& }y˭"As +0ϥ @NSh.@ IWn]|(C@ e@ e@ eĝd'N8qdvv2zuЂ5+F/=}= S?>zaϸYŃ\C>v7gg}P*L؛c|.>Sq# K:2##}DJS( IIOK7vpIpitV\Ya#G IpXKJGd+0\98ΓJ#gdިC@z۲O ۋұ[`^[aK}aHïllǝj@xdǞz|o=&LHᑼ'XYhlֺ(0fxh~kz;& MLQ6qeLB̊+byVƒB 3&p`<"Džqƀ"FC}#+]A;ãCE ԍx}x@!ov3\h8ջ,`o\2c岙QBsEO?}%yÓ~:o@a!.nYeYjoܯ%^]KRs?o[uLAatQؐrֽI?s?x\9ūld Sov7)nΞs>ʞ?àv]}/xodY;Ni0\qOAm}ƚ5,7~m{뀖duw/#ңU 1#SBx`kxW\p0[]^^32]E cMIiP;*)e8\/0X?x\^o%(k,eJ1"Zt͑JqTXr\y_кm*]M\1 mA6@xOɤH dX.Lw^ }pk?qW=@Ҷ}c~fgn oYtfioJfm=xҢgoL0})X!Ȑ:y;~&>TaG-nHZ>ʊuO>׿i$Ɔ9*\ǟi v/!Dz{=+kgVfeeee05Mwg>s3dVʚE-4UVCBp}+|2+J&L{-aKa{u7yyVF4ro͞\¿feŸ/䜭0-%99999N0W6`dApTB3uvb}(Fé{E>B jT%KM~qSOX'7WOW\.g) @|\0";Hk``.kX^#_1ĝTH @Ӧ;p"_48 ~9/HHPQ8s)ɑlB6Ϳl/rII 8T*)f[VSSxQaN^|mK̔\+4L kvi͡ cnJ[4IL Qyg.QWM<<@by<Ko2y1#*;e.ʿ쀞f^FRr=ͯ6*0K:IWJ$/nnj'-roRn"Ȍ0&^ VN*-Hʎ ~vtkV/j{[suAݡ`rn},3@l e-G3x`h:aI\sH.$f!pJ4K@z i qj256(,d8P/-gޟ, 5M%˛x\~`PpkBfjʵᙑAܔ ã\Q7UɌS_>Sk˵c$Qٳ![ucʘ{>=Z®$8\P"A2!McSEDH\zdFzwȋz I C ("h f++.)ƌ+7aҎ,~y49ϻu0Dj8O5TGqq Ku/DO}kݲ7{egoWc1 n++\.vOLMmmbMAXk$ЦK fWB$'Ǩ:L5h݊TH烳/K͛X)XkuGvT~gZƮ¸EMUPɌ_Ԛn~;W|u ;^TqLe'?zŧN)R~mWl͛-_g[֣y֯izkIu2БO]Ey6.} ށOcH_e|͟{~bMf=#%&tطTܖl+ v^V9޵bA0~=zP6wĎs{Wng5`TkTڊkL,@Q0tCeGNuրNTw$l,]i:]%9E\z}ׁj h [::-P~"&3sʝF0p5_~}ckbgl݅6_<ԋo˘ 2;|7o(2!Mc-j"pQDJ2\%~weTD 9䑑Rg"Do̼4Kȣ]퇺(w?`ni͗w+r}Ҽ_n|X"{APԑфEW~]J8@mbeS4`ΞE -P|4tSȾ[\,@ Ar>_@{ @ "@ "@ $8c֚m{߉‡&*n~ 42iPA@X\L\!f}ey $8>.\!nAWmt^q7P1ni@; R%447V{:SNJz^?{bȭɏ7mQ8MQ`GE> OG -?[W>ތ_LW}:^ur" 2·2TR{NkbFƺ uw4#>u$)F1-7Ѐ5/2o8 <_>f>b.E14>HЪjBru tͥ-NL/m%yh~kz;& MLQ62oΨSo.+r`HUsk"UΚ [ Ml0v;Y:Sbs*bͭ= h~Ɏ=zL,6&.>X@>AjAKj?MZ:0cSB6`N$82 fL&Wk/vveʼk2MSTx .ӉcF@$QNc}L8b@Jfت5涶t6ER)|sNE I$qz&gPk&xLj0Wi$$InuvI%=YB``H(4\w"v߫+^?)l %C.|}$pwY`nω[~`atv+kɸ9{vΉ({*@UA9eޤ׹_<.œ?6̜I)*ka}pŤ?kP, 1G_c,yۺwP̾8ee3ӣ抜~tEȀfog@S>k kWM~0Hkj>+nCMiGW\p0[]^^̐rg2TGE j,*eIqDPS`K$,\!3WgV}ŊCS$)H_ͽ"ZΟ9{ .[템 56WCaNNNNHk`vs$ 0B.`/xhh&\.aFar"|DzZZ`Ჶ5eEIϫ#<޼C=bgkk6ޑ>hy5O8cëMe=P+o\\= }ny^آ@ɬ(>Vi½ &Mޠ=9*\ǟidr<w׼/O˳2:ab=ᅯ'n5+[6'-zf ӗlIO h~Z~&meeeZ'}d!V+ աr#g`D⮹{L~ݡXozwO9 jT%&pEh֨k- J2_SӞgV 6"5e@' oX\i)),(hG:F+ASV K\f ȓHx>i02#mCx\ D'K1eRr$6F^PlbAJ{P|@[q[N45]pmm:Ԕki?}h8yABͿl/rIIvymK̔\+4Lmup=ZeE `Mm`GAZG^uPPPNU6QvQ}?}^eMkUl PIo[Mx]~4(7ۮ6jsFGצW*JLf")f4IpjTODFGVIEfVڂ .UѱX]񨨨 .+ >;5s+c}`&NˍFTff [gġ/rp8dySXZm)4Ʀ+AalhJɬ 74%|ZVm1zװE&a`~%Ɏ >A<.F6y Ž[HJ|h XU~߫/)NrxƤpg6!Fy<.AtO_ӇQ/-T0Q-r3 o 2ѯ "W?=;4M86iܰQ$4UV\vK9,PnoLcJ$(J uhxYQސ*MkB鼁&ރhv4\ί떪@˰WyA ܨ7n vU\ IDAT1}+UbtwA\[i mpKޖde&9'ù~_[%vx[J]ь_0OZ(唭ߤxXٟ^1~*0Kl'1c,#zdIO;òv`rʩϿ$-E`46t&=f'f3+AP|,HgҸfjERUUU0/Sq\?0Lپ(Aֺeo8aڈ@Bhӷ̣ x|>af\YHy󿌉D;RqdVn#S|yRJlXjyv{WVJGDr1iЕ:dKmr4U],j&Vĥ'i800S dSR7 5C9MeVNTwUl߼ vVݣJQ\bj$vZefuM)#㹴fhCd@c:yk>  z9G7?ugX_˧[ƾ/<`wկȧ.âּ}msg.6wĎs{Wngw͛-_g[X|ڊJs^~HTvwkezA_eظ emS/KvnGRQi+A3~P+Zه h&&]iInT"ۮmɔH\[-3+R74.K'[cBꙙ>d/al薵/E@̜w q 'hpe󹒖x+4ܢ&E$s+o$C$:<22WLdoWbԜ8Vq]ƒb#epUPfnx#F1"5I}dOHjX+`"Z$[?ي@ zl@٦j^{'hy=X/W K0Nve@ ~M:vkUn&i&Wܲe Eׁ@ nE @bl@/h7kBXWff~/񆇢24Q 6ږ/;7,_'N8q2{׬m_=KDhEă\C>v=dg펾4paCGfdH w !)i Aw'HC>rXrA\i+‡쐳dV~W1pCB$ߔq?4vLn-lR01\Cj b[L6_0۱ޗeC˳i*/ W#dR-]TmŗeVmɎ=zL,:O66ϛ: 568ǺrVk]0$6kH"Mg P*ǭ84ѡ[0dV~IYqElrB9e3KzAY5VB! ` /thѡ)g/J!T}o(c^ J+L67:dK,k+NЎ z\2Jg/fd*y]I{r?3<c>\h8ջ,02sުW'Ƃ~ @9eޤ׹_<.œ?2kd>t-k^0Km:͕wO}}zIYbIgܜ=;D|=+f7ʛ)WO{dsۧ=+9*\ǟi4&oЎ`J'zic~_0O<oy32S'/TdԩdVj?ًzwKJ|eEr'}dTå糲ƿ{ieVVVVֳ_@eeeZxp_9[a[Jrrrrr `)l񩴂wܳͅg}CFFé{EؾTV$QXm>|;ZkL$`\LBZn zPQXR]kǔY\\.: <1^-r\|>52Md|rjj ci4pj4;n2,Ny>`09<؄F%{>'$18lhup0 fcF-$R7%]_Z ڙt ]} tYƉLHhe+E9HJRPyg.QWUڑwh2_vb-ꕇ/1Ss0%O;uWv2zwKF%&Oht-U[ #wkOS+v%F+ASV3$Hi3$Eb8Ƥ7p4IFuEfDZe̐Y84%2C~qIoaEG38A74VW()]Wƃ%抢fvM'j^eCMWw`\u*098Sj%o/5pخ<7l=:t|'M^tbaTӄqEpqREF%NGYEaFmcDKz{s*p9o:>6h(jcsC8ZgJ!T}_i.k ssMJ"8bJBZ%{.j0TsǸP*Kl?~0D.xF%{>'\zi`ܨ؅\J@T*'ǭL<@ @-2pIĘaaiżM dqM9hMV=<3R 3Sx.W( P ""|/zթl2cO..474 3LPrX.4}lRsܧGK^=uxq¾5q vmE,ba\p]٬s:ȭ4Q TO05a2#Ta/lB$'Ǩ =(W'eRS꺮D^ 40x&?{Q >~yo]PHqzKǮ{gg㛨ևdIiZ(R.\\@-\.xݸp ( p" bd+-K[/钴MҤYg:&4t|?y<3gΜc+m6RMJvY ^ PWOVi8TeR>MYoV2i5Yaܖd5ζN6NqoϞ{ ~ogIYfVj~̒=?oٳz{>*ӛ_fR/8~\U+^d|ƼS8mсZi5mq~dכۏOUDg.\@ @WvJ/k)D#~Ka )Z4!C\6:IA}s@6T "0nWQiTj|klt%gJtn@6 #R8NN}m,hqIٕfwxSNB)\YXCS6пP*+vMGsod@ BNWu_?bGד8?$1(;_>mr$4ԟtorSw,R)3KJ8#d>B [WQ @ 26h))))))2Z' ~;ߌb,p:vf#|a݅f7!sX,@ |[9~;}2Uߌ9I]IÞkv2es@ K=v }kk@ -(E @?ڕuClXr2I[:Q[vm$gm~*HzMA⓫>s],}w- myQέ#o>t=D;&}#u}O B}4*%%Z"G$b#$! CSH<p5y|Vmi{2el6A|&Tܱd/Mu9ITVϢyyrRAxb^.yHI&O\M2M父Fݤfߚa/-Q˶>>RnAco,v1 Z{VuȝѶ'm B+1wb-m )qUC,bdN8-Hj(+շɝ?&84),xfA<;fFU߃( Ȇ³ '.z ]Yq,mI~?Op7юuech`|s9/CHul=G8b@#h ;̊en* 7vC]{[p36tnhk䤜nG5[ 'X:1gηO|)u{nF[K# AB/wrj zJ[t*/j$Njnu_6q"$Myg CTr>O^.xa+Fko  8 fkK"X\H nx.J$~\epH"r~21G}40 L`Qq70VMpxN&lPpX, o{F+D9,= Uݍ3Fpp|ɖok4zP9- {xc nhkT[q2y}/=Ə4KL&#z@P, 8pC&h88}yW#Zum{O B |ʛ+yq[pJEvY-;_Lc1 +3 Z9Wf4SXp*/ MMAb6œ)Ɩ9ޕfwl,Qwt}ۤNT枮b>ޕy9B>裁RX /,EZ/T Ⱥ]3xOKɺe2m_vV5)J؞= 6<9Ck?Β3_*voĜ߿-TƻNʜFw.]5K6޼:xm4o؞ֺݏݯh*sM׈/be(??fɞ7Yox~myW#Zum{5ssЕ]Z *Hl_j,iJILJ`U.najT Ms}$jcEMQt%goPt/ ʊёI)EVn6Zldqv04==ԩS4Z,%ϟ+ wh]#dɴ'þ睃,`|(hJc^+.^v\s^o"zPpYz$&e˚}RxߧVn{YnT=RmaʐXB?w" IDATn,Wwy9B>#D?~#Snt:|u|Fۑ"nz;( AC\x~ۙ[C kaX`VXtX {={nb Wʩ!X,vJ9I]#d~@ z[y{J@ b@ ϲX/(22؟qOHddn]w(']9v̻G@ }XJ2N%Dr_!ua^Pᮜn;B#D_ecǎ;џlwPQoGtܲo~):ŽQgm~*k=5#$-<ގp! ߵI#[h)q{$b_ ~2feBrᩩ)Cbeɳ_4qc8KME-ؘ&weކ6bs9v̻G>LѦӿ4ͯLuRRۆ hx5B & n2Z`l׵d"5cbt'ohݧ%L 41ȅ"bL42!69ky66y,lvxW,8ݗmGh!whh"i_%ǎ?]BYjUT*,X+C_O ߿) n]zMu_%q[@/;r`4ZsZݳh53Rޣf>+F9l\ٿ}ϥ@'-<%3m[~ar=KF[9qXOCjάGv.}}+cۓ/j`ު x=>ڕuClXr9mh{3V Lqk[a?'Ư93+nW{/ğB04ei),IEL҈q*iq1C%ĊI۴-Ԥ)zѕU.D?_B$VyHWU˘ej$- j-dݳ,OMP2$55>i`csAr|{k>0*Ɗ̿hM8OblRBkjkD,x=;?8-Lq((-Mt^{dԴB@5i޹'-왓J*322|x^_N =2M=<~ܳ[K_x=i,@e y+>-u\7=iFLDKl=yFvJO?*]BCd}󟬠S/HDai[#D\_Ygr8)Hq YHl$Ebm2>Rnڼsl196q(cwp^jTܱd/Mu9]yI7_ۼV&V5MZM&$IU#>r\uO=_1M !-Qaa$i5҃CC+MWNϏWqF}L/17UtEQ gF>9e,@DC!Op7юyl"R2|@C 1h{WJwxtG׋2VިT}\;%->YtJW3hRռ6{,uڔc۫o9wLTD!i;[//|y}!qb1``lԚ& WJĤ),L.$ z ـapl6_ 0P"2ry"Daz؜sW!so05s$*KCܨX5-_mM|4(T &N`ᓗ,0,LFo^U\J7A;,.vhkF *Bd^lPpX, ݢ!TU"̨֯"|ʛ+?'8902cpseJSpbU\NC啂~eW;?hqx!hl' R`nxgcPu2tqͫrOWQ#d9a֒3&}yzW ~ogIYfV_+?7nחqjfDS5b*g~7vph˖~ZJݞ棟}41o&-;L$[ZnFM,dCY :2)ת`<^sizzShXAJVSee#?WД o=5HBCIr|wi7=}2ɀ@[spe9MZ{9I ,Z}|xCe#I@/kI}|v,7e(KJq%W*|Ʒ՞r|{k@ ="v] @xelSRRRRReV'z|v %0ư*>pjgH=5(gyh,@ >P|mt;}2UH]qͻ(w@ z|v Xs@ YP@ x~ApC"# ؜9Bޚ#@)pcǎ;vl׏%i.ι߸ܖ [O5S>9t{- oPܐԴԔ8'LOw% #ixjZ a#QxJzjqi郃= !w 2$6Pv5=w-h鸥&lL=6j/60;G[sDǔxLz+:f$-<'!|@oVכ/D =_Hlʅ+&^kl۲ƒXuy!Xݥs(,!>pXKEk1N"bL42!69a35,Im6H|csr|{k> 'HDUZCyeً9>XyHF|{hgRkY%[|=D+/3h̼W/uԞުyL'-'|ɻjgs'OxbѦDss:J>1"͞9c45qqәOfKMN<1i)T YcCf(U*uyJa}*"Y{JG"b~U+/;{&lQ_*Ը=+]mq%:aXVU\V%J R nɍU홳@FBhrM(×-zCN"ٜ~qJfp\^&_dP WV%Сr3՜{\8Cc4Zl߬Y])p\. %ZS#0 =7uvyg_JN<˘ҕYV^3A1,^R)*ʫq{'@|GLkgwX5-15W֙Nʤ7R\+Cj)"[:;I~{rJb>4o@guyX#S8 UcIr 'NǝN'|I87kzbu:7ǭ|/ݗCGK~ʸ-r'`,pijIVp 68չ%w, FS]{`29;s/{v^nA| qل*4{;5^Zi*m}|N/XΎhe3ciq>|jҕY_V7:2<12KZ! r:+Gĥ,Fuy4bmnE FVCYMlӛZ4E5%,IR,՝CC+_{Y#ʨR&dγ[PaPxPaD5,ҖhyHZVM"Rx|t:J^:1yÁZwY,8d-åen* ܴ1]7q36[ 'X:1gηZZLPN ǺCuMgNIO']:_%meRcEfJUU\?"s0LE n;NBi+RW|IPtlTrI 4-KlB5;lJ8\A:tQKѬ107R4cĵ7Y@Qarq8rBРwpv <8Bď$!ᧃ9fsW7okIU;c @KT&H8Qw'Z6W(8,_SY\FMǪil7~-uw8D5zP9- {xc ޱKL&#z@P B͙% Ubh[ɫSgg[2M)>2#GbG;Ɩçrk/箊Dٸa9Mpp ðqeFE8` &SŅS+3PS PDDKKzNC啂eI0Hfx xzN]ivbuWO׷DKe*i5ۅU2@}QP>Plc!Ck ^|R.8>yE}a׶]F߷qgj$,Wݍbx7 ֮s鲭YJY @kʿFxC,CF̘~јs t-:PKEJ^5ggVjQ+{62sg mβr\Rdi U ʮTe-h$ǰMXHx|[K4LMyдpVZaZ[Z&IdQFb?&nXQv pXOdCY :2)ת^FM,ή4?:uF%d5UV6_Lm[nN&HBCIrq̻i7=}2ɀy?&=;r'?Xgᤵʷ"o(,vD\~= CeL 4h!ܔz8/U*eƁ[_Vj ]98B5G ="v]|ZaoG@ 7CDp𔔔xUɠ^6_gcbs ao1C8V;CnN&( Gȼh,@ >P|mt;}2UH]vsZvax@ DB/aa@~@ ,@ (GF1rC(t4l29@ D£*T*.!QhN&#d[ہTxHʶ/%$3}R/v55)}4 ؊;/t*}ccx뮞qe>>s7ǎ;{p%#3"գ8Oyg򈴴Ԕ!BD)R ǥfBC:( i5ۄ>t}k{8+W`+ͶŃfx \XKl}qֶr1i၅ +vϝE-Ϫd~eRMgmK3Ci87xИyxJo2v]-_ƕwu IDATA{ڜ}λ*cOdZjԁ6l?Qmk ~RG]ڙǰXzԌ[g|=u2LqkZrRwz? ͐4i!xzswf??SCӟ t!Ѷ!NgEPwsdAo_}p)h f5ĊG] ̀?Y*Ώw}JOdkK\_H\đ#-yy5Vz3B `ڰB bxH8ZiJK% fka!~Jc̸XԬ押3F]"C3!.bF4q2A[zGCRS3^A;洚mB:B5=Vs~ȥCy,n7.}..lKE0.:f"VN ͵()739K87~WQun1wemF8@ns>5!E+ׯ;޺OcĸĶk˙i8<ՑW<5[қqc9?xu7fN|7NF-z>!4'D3^LdS@5i޹'EV9r~Gf[>]=wشIeӦ^u 0~->FJ6yD7$ZV\Mpk d(dXW߁u_5o?)O.[tbΜoE[+=YE Vi5gM72Cd;]+ owoN)iK'VӖY^f|,5FXtjD^U^#o9w &Qʭ܎v/' BҔwo2UDGrjծ Fko[8l6pX&c)K ICB04HI$BG'rќVMCGܷ>J"6З"2p\2|~"i@Z~gC"0^r@}rF zM"A>J\}*3 1hA#@oniD" o^!Cn?bV3 Gp;;ƵF l- , 8pC&h8 W1q2YݳNc~<ЮYU\=v=|NJVa /`dqWcjO^`°0ak4P.7j?VMKt;K=4@ 1. q Vi5gMZ:52=]L']HDtnW':;ۚhJQ)wiˬ+/WRTPX,k%~-ԼJMCG!ʓ l",zM890 +lQ\`4fI($p+ m +.CS `bdA NJhx4R{ܼhm=]ŨќV];Gܷ>aPo!UptF@EA9lCJ~]f€ƓtnKa%'<ྰ0iXS[:z(~2i}\Z0H8\иƘ}>zXעٻg_odkE7?sx?'ۊS-7bX~\'@Y9{V/~w7sKM;z{׏zsI[żu4u4ZΥ˶f)٦ӛWg5 /[i)YwVw@}(`{iLqv֔/Y?|h_YRY}Un_Ú~јs t-:PKyD[wZMF ֜7v1mt!v}o?P~̝5Lt߽;M[Uh-YVK5MU~X]ٕ*ORԤ): u%5m-jiuՠVX'bL46ccIq uePi5jJ^Fa}b: eEȤӢS_nKMf&MK}tk`KOO?uTLm2D/t7=}rg `+{r-@U9>^YV4v#7V:-5Bjdĭs cL{qYz$&eק`p&)Su7%W*|  BĭClu}Nao76jn+rNtkB(cN//)3@MXtX$ .X,@ | 7e:I[lm?O D AMd@ GAY,@ .DFv@?@ .m!Dr_!T~ AػHlKKrXA9ǒ,{]nm_M؀coz@7{]_sرcǎGQO/Z-'&F2o8kSA@Z)i-OxTdX=/H^tIyZ0+^`G}ٌYu]AџlwP@}?6wM>9o6> !%}1p>:# OIOǥr^AC:<55eHl~tus7,5`#Ϳ6 ބ%4R1sRCCY #S 򀞎/bZ<& nˢmX]o`Z׸cA{;}w&46/jy1|Lz}iӗPO970MS;B4ͯLuY?"-N沒ӒYg;'dW(/3_bb̽X}8,TfL,Я,ekyE&\ܜSmݻ,"6'PNp^ѣBӢW*5; i޼޻jބ/#r}l;XAڌ'JКNEă7f4.a*I{yu'u&$r[J _. zf<@R"NC 4fޫs:jzoμfM^;GҜw~dʿ_}YI3+w^IhÓ\E&-ƒiaS>۰DF略/x%K헍+;\Iy'4fޫ/6kƆoo(NmyV%E{^YdmξV~~^G'Ch[Vؙ?Y=xѱo7. ^Y;(PV}|>-dCӒj귟nm1Ie9W5'DP0"-r *2Y+<؈ pՆTj#Ej~x?ǡ,VNSZ(Y0X# U{>GhHLH, X&k;g jmhLParq Y2$55>Ap돞}#v-+:sy^_N O+̭ KJ(|jFƴO4{dddddƐ:4YjX)705QFoܼW/9'8^)z`SzWqcm'>2׈3Ӯ_k;K.,:~O4ޞ1Im+ i/+{ؐJJ]^F;}+@'"9=sye0M^#~I1?2KRuk{8v1wemMIp^PjRhMm-@ӶԴ?䭓';:g~zСwnǣq<CD7ϝ=[m/j\`\9wj/<5MĵaAXUdRGO:bDJrbJzaptDKOL\lNh%fq87tu7HA#N -?zRFoxUIcnzﲑ{v5iQi5Ֆb hn8Ƨk>Db 0֫1I)45$fg<*a- ՞pDs?/:KΡCfܫ<\|t>3[fow64lK$Y;m.Lv?r*&*n!NEXuɩ=NykN9>Gk͚%8u<,.VsKz;@yIM"BpJ@򡱱_Y+-m!Ѷ@E,A/RTUU'?lr A2:ZKLyyg/.iJ\/N 񷩛  w zֶ3EԮU@ViaC 7Z&2&z E;q\6Օ'ckG8,V5V0W֙p]f"d^g'$mLUyj0X|eT\JwױrT^pw:G(XN 3_TnwYU[=( C*Q_z晴{bySuX@H#TM0OE<@btpPNp?wU!9 l61;s/ߜyWŶ5p Aco,v1 v'YҢMOU9l#v/i5lh=wL'~2N HӁD7_kLb4vsG:>Yd7״N%sړ(5Qaa$i5wGgLP6;I٫w^( Ȇ³ s󞅮5)#YTy|e?3w$֖RJ[ u( ,zc>Q@tD(,X\miKH4Ms2? L&i˝?'||g Zc`fp F|"]wC 6dT-czrtTkEsEJ N!bl(7a}C ) 1 0+c}3.37cohο{VMk{Z/=u\,ٹzNbyvղi}:Rr.wpR[-N$IW =g/w\.:?ѱnDQUi3'Q\on4ACFewpw>+F G&S;_"zn|NXcy.m}Ǧ_WsvXm7}qȗK6EA5C?tz23JkI5=)}m ̭tgp9q<=K<)L9"ZLZmn/.bԳFFC= hO$h1!\1vUR܃QOjT:k EH_yu0AV;Y-_>Y(&В6s $CCO~:9qcQk%bçgp$ʈq L0SS^OӼ AB1dk;Ց(͘4VTqq [Eg`"~E-MqGߺr˪zںkUk>8tFiy8W=L[II Xmy.ϩr@;LOK,S*dBvgYIi)6iUIgK%ݹvwdEJy\I[Ξ3%~;q!j=w-:x>4ް[{ =CSS_Ҁv#Fx7ek\*m_8뗘/7)(8qy<0~49pD=)S_Lmm"mTa!Xkk]XuW%YSt( h]뱠52u F#C>u$#NF[ IDAT.zTn[TV}J?)cBJZ n3=jbZ}۸!뫷^)+I۶C̎ToV(L<^ԺޜA}vμӱwg~=W|u3(1(y{Ɯף{>yү_z5K޸_Z`=k^<ˉ,KmOxc*vEp34pnXIiׯ^ Ru5ťo~`˟^z˻*xagy͞4{_jOl{"?.u?r 7+VDlDCUVA\zEVrekҸ{ݖ0c,$FWTFpJF!Ā<Ald4,|70;$uamni&"2geeeeeezC0L0Xtwok[#%!,xazw)Wb]݋u/%(7`1c!1R6."vO(=Ald24|6D/L5֙bgaaaaaa na6 +,2U, [SFŅIq)#<Ald4,4ɐE(yyFYXXXXXXXFުXB"qg#R&"N"&ӻwu߭ YĒ1Y~:.0cI[D𗌼r}V\=/oe(@]WxСC>?'n޵oޯ?.YVWM :1@d?|ssh/!, G1>77'+-L(MN ;b]Ҙq:ˑ]1>'7gN"\:3;77'kLRILV^N³:97/=lBy9"7xƽf/>  ŧ[AP\B!Oi DpTs( +O^ݱP#4ʹ'o8ޚ&ɞu5ϿkނEB6`+f▶vC>呅OL>ԛ^SjonY/Kf=[Ꮋgvݻ4l߳>dÅ3 YjA/ɬ2ʈ(ns;Xr NWk=wފ Òb5иH&Ge:Vn^sb&LIR782L.a~b֋g,".5)T5g֚uA][& PAld4,h(`ޝg`쾫n}J hy6q\'L>7&t k/}luQ.bh] K6ZRu"<5ÅoGkUOD$5:2cm$x ,l#Y)+@E޻`186Dizou<]p5[NB#{z٣_WW㎟ehw gxBy-z5 ?~hnr_hעU[c/CH/o? ֶ^q޶Ȏ7gsl8c+N~D?ڻ};_3޹ЕV|7TI%B}K1}/@?CUuN4PwbK];67/:.|{qY7(Uݥ76;>~:5^#P͉TY*yҋ2!Wct"MQtLy0ns|.n0X\nKɮ`5t4[;f*JpIeùNtT{fSB!g2A蛣Uj>7zQ %@ѵFFCr Ц䌉w["Z7HCEi 8i\prDN%l) ӄ4HI\1+9z߶';D*j~uw+.ҨsT3Cqյ?[iW 0.a˳P.Z{k~~/>>-w/(4'M ή[z׋3SiՑ) {u洛~ONTl/? _gHu O=zn/1V!>,89rR;Rzȑ#G#G50_5Pg~+gK g4V˩?AL3D·WTaXmT GkJS%DB[kMĤ .“ǎJOt02rO(Mݝ%HNaDzNNvvVfZϸj2v H[F %:n6M620;h=}Wl~Bɡq8@D@G{vw-fĩ7HI$s+ [0Z]iSsF/š~kC6VEEXl} 8g\"c8WVK0SőV *),n㊔)h8ݩf¿__%+R=:9Nam=\tNN[euw]cF$5HYB6 :U;.EӅTWyxO9BTtbM=A ߹ew1s߼{p#Wu >woުDKl|ؗ]Ecpz&5064Z\nvZ&dgII ˴Q ƪ1mJ^ބx p;wDQy8.Vio7r4[2Rq('Ʉr8R{颢3g+M8]0FzQ`oT =Ald24HHcu1(a28dA=@HI&Ceh^V*S}*Q]rFH5`ppT$1 rS@Y6x)D|CKK,C<]Ჽ/jBXX:W?[*'pDkLv!>#%\:T'$}}9nMW^znC=t\\K'%d(*bB[abJV~I1/_x7o_seʛeϲ@StYW҇PӈkBicGU/;:%cETyLz7م#MOR gT| #D8_PM X\c0%YwƉp-Z兎w7XD"{UMB\SB6VAkl# R|(cC9{azwzcc>D`pKo#LMբ?i|(@GKjZ:]4wtz|1h6^tP0!Յ@t6tܮ*m1dնdrG>, %;W/ \YL=ϮZ6Լ].WgnaFJ\.Ui"3w sSK0:I=W@tKr%t#J9}{ts& jT{_|q @Ykbp !h2%R.Z @Cs9M>~ЦLq"؞S-T{ sJ}G#xXyۙc%2L>^wP GV(SHuCl;l0Iw <ӳ"#RŤ$`=ix\.aK=Ald4K=n R>'0;h؃:FC!\1vUR܃QOjT:k EH_yu0AV;Y-_>Y(&В6s $CCO~:9qr_GO a(#F-} i]YWNĄjP߾O1XO2.ZE8   ͸(>?~WߟF7-Y[^tX5+uU7vO(͘4VTqkrIVbD$pzⵙ: ]ޟSsȊ򸒘7=g@KOv6{C՘{[tp|`eOݓ >ĉ]?xᕥϼa%Pp x8a#fAqpQH\NB`R.lvL*&p/U L=)SeM q9*,kmK*4Gy8s%Zleb;kEzF&C/m` 3Y=`2߫'QOUV@e'$*<&q\:->>ӣ&>יAIKkx1~zn̟4Qm;Hmu!Ek͹ 4@>iԭ x0Wr6}5WꞣǠs֧- l.U;'|9r|UteQQJP7Ak4 D]zYxVS0L3Z0=/ozwpMb?Mo(*)[~iDJک~g jaXƌT(B!Ry6hwzLhT""0;h=heq[YYYYYY)*ާc~' jaXH@b\v{ ^cB)v׫$!s|s9$`ޝg`Poeaaaa %X jaXƌTQkR 62%įА y&  O,,,,,,,fL$(K' )l2VqqaRxF RhaqqJ!d w`(UD)E8F RhDJ.DL0IB}`azwzE,YnҞ >h!)gnUVN?\i׾v~1+T_=߇:Sg3ch+f 3<8 K3ݻe̒>{üI ?YXBS\1!];>/oB72d$(0;h=C}$GQ V4'**9cWyy!+Fj/G&c欸GPOydO,-@O\d,ڵd]p5M֓>.{8XXF;mB|Ƅq}oc֚uWő)$`dr}' \^]UnMսFo> v:V7J]}`azwz3!$Tvu;lrN,|nLX!A:?^l 67]^"$=Ѻ 'm8qDeyj ߎVתbI⁑i.,X]c[[̓CUuN4PwbKn]`S_zڹxuO/{R4?jl N ~>%pjW60j[R~xyۢ#; \F?\XkQ˪1kL>x$KȈ|.JD :.{sU=K]9/=5-A) ,fNŦ{+H@ {쥧oOS~kzL.LT}$wI>~hnr_Ϭ,q8>'-LeHemoַVh&IZ p݁u֝*5b9˜X%^SzR&jlnU 1mii3T[;)Wa呑Qՙf\!B.G*2A蛣UjKg~mD(rLNNQDFad'M!+TGRb[0>Dحn-ҎpH 艜KJwDS iԑbVr$mMO5qwUwV\:hQf&ok.{{r֋eBA~ה.1)Nևg{܈2ǯO$:ڛk1&N!%$m?.ompptlj)wPMu8 ۼRsȪJk#EomO IDAT4Z+?,xwݑ*JJjee8"%EALH4T~߯D%%~h5>p=vj* h9_;J;ܶa*}c8WVK0SőV=:]78siQtZw gl_OUh[D'9:9NI=\tNhO>'-2LdB|?,E{)Z.<.NcCEeVe#$ThqQnK{*ctF'&1|HjbB|3'O9[\i.Av#GQIcޒEaIcuˮT^[gPg}0ze[]LuCdmGumM $oTSq@P7p.MeiEc!j Vߝxn4Tq(G_x7o_s%\e M).WW-?C\.A].K\]\WO?U|pSn2zߎ~b\~l;hn⪥|li<ƛz)J%.'&11#=khP:A\y9p}"OZdȄ~Yo9vƊjAL\z8m3ꫪt]{G$~h*/S#`U\%4nUZ,a"bl98qp;Pn7adKBiG&.ުX&!.PZթ!J@0H2쓣I4WT=}Ճ}0/aSa>(R񷚮1N]9B#8_/&Ƌ rSF`?~ej "٪迨W{q @YkbprUx&na9]e".R*0s T?94h,ٹzNbyvղi}|N[87/cLA;uݝsOfj[*L_,${20vgs'E<Ĵ]?t;P6@(݆8|cFa IZӹFdyXyۙc% B'tO!˓Ԟ.Ejskl&.\xOs8zHvg/R $r/lFI}b<\.sJ"Ѡԃ}0 ]V@>-6|jxx6@ L{NƦ IsWc{buwYRW3 5qyQ|~? )Y[^tX5+uU7v.(͘4VTў5?yk3r>vyK_ 3}SVvDL.S8Cz-Nud+~adtDg.,d*ڹNҨ8-'^i+)iCyҢD&xᕥϼY04>V+rqpOp K#]4 %GNZGy8s%ZlyL5KFח]66\p-ahbr|Bjg o}5EGkju ;L t5^8iD#A/L߭`X\&{!*j0~RRDŽ>s;.݀Kgz:/5(Iqci /F;9:COWo3vuVSV&mw޼.dQxuM1`/[7k"'(ۇ0m }|cd.CɡO}|ػnm3kX} i w?C v}B4]˞TZ~h5/c Gj@dڻgWn{@A6fŪ-\OmOxc*vQEcP9[lxsԟl3E~G;Y [O}f7kyEw!addDgu[[_U >{V{bJ"̤HIyt]`1Gt:(pS@uo v:ZQ0Inȍ@6@ݏ}΄: Hz8qsS \m˚_Ry FQ،H!@Z^(tV*ۆwJx ZQ*Qw!VilZN%  `i5u %ۢ199)ZD]~5BhSrrDaP$RѺwu?b:;?,,,,,,#\&ԟ?~ª'$6AlxMR h;kO9rpȑ#G e @ypP҈px}qr9jJX04y$DB孄Fdggey{J*j2I;kï^(h|IwC=HuFwL`ܦf0,nBVmvZHLd8EQO,q8͍](jln$`8e/)Ηh5jRl,k}*Ԟ9YjtƳ@m{b͗gß^Hcu'w$}7dG {[gwCeaop0 np kVn7adKBu֜.R#<:pI bҍv9.Ϧ&\zhRSck=~5QƤk|I#Du&7v EQ]F ((i]x]7bp\^ƹvPC 9W5^YrD*F.莅. Re Hr1_'˥|7&Cë ֙YXXXXXF*6k'_8SdTgiR !fMmm"mTa!Xke0vKpCRc]49pD͖ 6a@QT ;bvARG 2F{G(,,,,,PWZַ%D\w4Vմ;0.)1J!HS⦀k<*!V+Q6Bⲯ@k8_/,=+FҧVB$KtPHsqanl c-**R2Tm(%iԛHJm# pjjZU3I2!?C*ud}2L o,,,,,,#g_5Rs>4PEw5ŧk.k2]:vo|Utwէ ~8Gj8ڷl*?}ۈX("g/}K&Th4*SL1I^uƄ :; CqmDew7VXF.\U zCΘYg`cgG,u`4XQ@' LՊABՙ2 o,,,,,,, ́eaaaaaaaa}xbyȸ0)7l${lm \ܟ*6ܗeޚ?+<:Mn]cY9{szۜM[ޛJO O=zn/VCiRHO~޳CdS,O9i7/񋟜,K_\ؑC[G:}CrBs$Rx"9SJ~>|h!$}rEU` .:Ysߊ6R%Pԟ?~B &B2H*8: -7J!NHKh(Ap)Ww'P6+AI#4B=M.ުXj2v H[ePvh}%dI2@C#Zue45>OKRgv['II.Q۩{ 6{٧~i|;*RE'֮כq9( p`qEJJWk՞O .ume'SQQDwvm'9UD>δ, gtr}g۝NsUE=ӥ~ h8ݩf¿__<*v<\Z-LGZ+M0.<; pN2@CUWS!uݤl7:I7#+ FѾq8NQdBEq%[ǠRq3l4VDG& hu&y@9Wr \..Bu$ A*ZG͖Soy7{sDkLv!u=Sݏ]nxDh1ܯD)w>dZ$:Ou/go'd\R<2v|ߎ~b\~l;xzrvn:vDG&PC?e{G$~h*/nz7FfJG㤡Wb3vcFk tv୊hB\ U_9Gi0{$ې> 0IhhD31γN\.zp8.[l/t9rS=~N[87/h?rM5~#Nw٭4/$IK3=#=!$UEJfn8|Md@;>uϐΒ,&gW-vjמgu^í{3rS2g~ۥ t$1l+D=m`3J~?].Fp0O5x\{ \xx<bns3H.bԳqO,KhH2$:2:-9Uh'^I6@ܐI׍_,mz}DUO<ϋ/.S8; ͸(>?~Wߟա(pzⵙ#jnhfLUDG:}Cz-NudGsa ˹&Yv A&B7Ʊj>W}-n3ʷԡNS:y,fҧn"x#gOZ s B{; tKy1+^{u Pp x8aqa4Kusa:m CM q9*,kmS&&'ƪq&@YSt( h\jp+$LL ,u?r 7+V_~o>[Qw/>Ma{_|k]y˟nnφʏ78NV>Yφ:[گ*8Vo.zf{K"[mϿZDGy[g,'?xWCƢ]EcP9[QGT磝,Z䍛_}5s`˟^z˻*xagyIؑxOsTWɅLiׯ^ Ru5r!NѩZrk+H~ǣSOV{⪫uN3vוqݛseΐ!s :cuI}bd u1cs=RݤXZgf|G<\ ccY?>aL8s  #dKe(!.#Gٌu=ﯜV%-6I20ɼ#GT z<*JI*Fc5VG W& hu&yy ,,^^b_o[0nܬw)[Ӂ5cѮEM||'عڸh!7OCzSa/qCBo(*)[~iDʎ|,̂8dIDATR6#Vj4 !Hٸ LhT"$> 0IhhD31γxy峷}sǎ_ok>x Q暅ڼԯ"Rvl.dK%nsK39>++++++3Ee&~@fjx8<ANwe:'0ARZ!dF:C<J@fj ]WV(5)[eV -dA2@C#[uexAfAkgkbYXXXXXXXXFCRҰ+2IqqaRF:keDuIUsrsƏM.p.iLVNnnDeg{>%YI@EJ.DLD)EhVgbxx2@$⇘w,18|gl‡+=/_/O0d%L$ؑ  C dC3n„ FƎ˛Mv/U/!JMPvWpurn^zCBy99YcB^W]@U3eV5=dHSSLʍWk=wފ Òb5{q!qI-B66ӇiO$mŧA7 yT31YX|$)a9bni4y&;'sɣU ?ܞ kMEAKmB|X}#SH؁2F$q*6Iix̂+R":NU|滤H.'-! /j7[0) |.n0X\nj|mb߼p8btN}T31YFXȵlڳ-[ 7Mxp4hƙ7=45siݳoطcO"v7>i:)}{Os]gm߾2U hWa~\IOT[RaWHn~/;<(~o]-~=}볳[3k}׫v\*m{>=wK?޵/ޠr_$'MD((?1Dw O+'D;-H#SfΝ9>Y3BaLM&ĥgfDIrNTd䴴.\lvB\j3H@Ru6֙$ik7JEȲ~R`ki04[`)*iD<E$GPuMჿL199)ZQZg DM; f:|.@ņռY4Ni-o3sqݾ!o{E9i=U"65\H*Zp Sn1yi[Nyh%G+5??׭}h&}BnK{街O$.xp+Sν:sMw?K''@re'W7uw S^8v昌=#;v#'dHK  8č=$H(v{?޾}K@~.;۲fS^s4_=$t ;iA~ԙtɓ)ƸD4S|毾D|ѣŴRvJgHER׳Jճ3Tx:$ YⰠk(Ak*,ݝOs [ zR]x\x3O0[4>;KA,m[ K2 QbW鷦[$IQ VQHR靊snD(9J4dh"S m(PS#ъS.g&ti$tn^ΖHbʿ 4MO>ol^|w=w_]Ǐ<%Eߐ^7|F6R$1 q9<ߜ8.5[Lu #6hx䏼D)"ֳtA #_?u*ԶB>]\aߦ*p?W2rDtA]Α >֖chR6^B@|>P"i0maM7R<,cJm4iXx!-Y(30j- HPr^hiiRti19%;mq6O?#X^_(BRkX^_՛޼j0775v%MMstn!+_P[>ă=Ouػ‹5Y(,%+_?kFS_-|ȐOˌT{[^;=|`Ih_ &ڸx2z;r9ޡQJgo}_^M <~/XЏ'uzzȃDdd7{MV٣SLx-np"e'μ3O_='}g_|{/~`_w0]<+["㥏?#q7rK~om_}s+?0`LzW*;̓7~-Ls?x;#Gd%w9O޸W}s~|;C/K =J/{d=%[$E$C$:騬E ?Q|ե\ж-MfoOr"i:8Ȟ?YZD31L|.Mvm]ldhhu)r P3bC\!>P;zX1R44Sٜ(mcyk5ke8Q9DZ-T띗|k9Z.Uͥp+:J}Cu6L{#.|w>{y;Wݳ}IzJ'YxA!օ3#T5E܋_R5}//8f"mYP,rRZŠ^͛#T߱s+G~믿^y5UtmA[HCtv3`K96雕Xe,(FA\6[er"|aq,!Ц f_ʅ5JV229E>[Uy;[:B!v|5}!BWujg|%cB!F!B[B!m-vױ@is3շu5;l:VLG}.?Q8L{{@M덻eӮ4MZRQkҿej B!HsjZol隠 !D48v9.r0[kK=]};{†FG҄n +F!R,EJi;JK-|ɹ3,FTb-J bI}S*;R;[T.'9X`u*b9S([u&qV"!*\\.QslΠ0 YӚzYVXBV_jh6ŧ5P5|Ay.uM3"nY15tS%,P(r}`0ڪj څ!jRs: J4%~;T6Eۍ$IQ Vө(QHrM5`(2:q^;9 1=|#aSN+Μ?;04qZU* XkۻF~7蚖%/޻]]z !.K^xe̹-V_;}n$`#ĥW((RyǧB!Tum6!4Idl#ΈE"m /A`M@SgOL-ݰO3{j2Y,P*QI%ŅDNKQ5u&e̗(bB!m}=u,CrjpȗDSKw!ry+c:|=nwb:Y8IŐ^*w„iֻ!l|5@fnҭjkL<8 md- J!B](s>ch`,#kJhB!eMh0SˢX,B!>8B!F!B?dEFb~IENDB`btop-1.2.3/Img/logo.png000066400000000000000000000017641420276253000146540ustar00rootroot00000000000000PNG  IHDR]bHgAMA a cHRMz&u0`:pQ<*PLTEXXXNNNDDD:::_000&&&vevtRNS@fbKGDH pHYs``kBtIME 3kIDATxؽj@p+ ^-}Po8m ˀ_.Uຨ9nvs Wffgwc0!IL2'=xyΆܓ޸ޏ|= }A/$7}χ< /uϢˡp-R_4M5ή!z o7ۨdK{\ /yM5DX^'TqWѼ΁Rk0Ys̽o :Z'Jծ뾙9C&\- ^x'nxv1nqid}̻eS^x˪Bf<[5!FW~bƃWC {6J3K{PoTb%Viw/^xLꄷT|j"}ˎ>q]o r/%tEXtdate:create2021-10-15T15:22:25+00:00O btop-1.2.3/Img/main-menu.png000066400000000000000000003451401420276253000156010ustar00rootroot00000000000000PNG  IHDRt sBITOtEXtSoftwaremate-screenshotȖJ IDATxYR[/j T\|ۀۂ_onnnn>qi6  R)eqHHF̜353#Kl6@AWF߿Y1ҿyosS. 5kXK/ާmyGh.wz,KU"IU%!?aSUv>'`i7Z_f4V{7.VUonq~ƭE;?-g=>`s7kQ[%u$S{l^tKfTuw <+pNϛext|tz2˕RM&[say^ ZLDEK>#t=ikY35-,NLAdDGÁ ;΅z>R.TWObux"kjS6O yC#F$90Tg򬏫f9ͅӲbYʩr7R]%Aq-]%<;U]eÄ́;׬ u`lEK5~p>nTڄJ8׊[^/Kdiq6dNqY8od.'jp~~1㒛_l3S'C;Eb&)B$ 7XH6684 HDB"UV"Tt `TuwIwjG/߶e=l;xOVkx\nDBpACp1{ڹM Dyelt*.v^j* c`,V[";HhI2|.NHzz<yfz?}FbzaUʅ܂eYM8!)ηaֲ֥ۇ\ #:ץV3S5*uAMwJ0SVJJ%t#`Yvzzzbb>~x7kpig5/Ǜ'e"a4߆R+Oa:rw#1^DrJ]z޹ 09gKʦ{RQR0'zuiaxwvq6wgQcW)wǂje@z;U]eA}R:7D_Nzpv|L~WL~:'˲\(TyB86t_WKn4.3$7-cFH0MPzJӴzjk~[=ƍ"/ۙ,nMʒJD$7d4rD FC"-7Hbu)NU𻱫d^Rw>3yL]c,׋ gYb .CVD$I ։)_Euft,k =%d7F,g/rhM: g'BFD։3Dx<;U]eA]lXMn-ݙ Mz!kܨE${ hE'gbٹ,NbًS%syN'b"F&~^xg@Lǒd*y=s߆yFJg'[Oc.W %_+Gx%%X$Zr7sWoZtwZ%"%8v/)_`TvOPeB3KӬٿ$I??nYYYrS4ym|_?p}ԟC ^ vg]%'ȿ_o=UW.S:O}r21[oq3yോ)٭ϱnqAwzʼnXL4Y,%Sҿ>=8?;0$dLz6v>M˜ӳvJ'X#>"zM`DHx/_q|3Ϝ3}c1t-v'.b.lo4wde5gO9 1鿿d}Rt; ׸=0fYϣ]YI!~cw 5ٸ8o ^WǛwLV׮dJ|-7Hbu)_gܦurM/J:V'kk'ϳ'eZhjzlĚ/{Lp8||||{3>>~|||g ToQS^2  W0 ,[~\@#:y3z2)gG/ _jvn@Dj$'F5rKx蝍Xr򝗗Izl?<([]=4UW7Z700vUnH-X!bˡn멃#stuHx~w*t߬GO'yFm/) u٠Tuz~4;Ǥi A{9׸7?Dw?IN.C0Z_ҡ?3TL W\l3sOVKﭧu b|qg* 7/3*~^ /@?@?@? ~HWG{t t o21 ylFFKÔ#4"M*xIYg1Z q]\v\lk2U؜tP$"<>3Y,RUXk`~~i^,/. .b.lo4wde5Q%Z|mNY*}>*wYY\| yr֙0Berz\/} gkWTlш@DNoEG8%\LyͯN80(Fn4I\)㗗0&BO'2ۈ=MH:aKvwG"vFy&U5SyFZKn YYc.Ftյ)"%~z,ǵ>s+#26C9O]S37S{kBurrM/~wO67{k~kZQTM3{nn& p$E;YAT슦i46 qn˥͢hd!FTUSo^3ܪՏGCş䧃"lȍvTu]@0(9::d$YDZSN&by]A1"vM3{_,LVVUYp{U}0Uo*ak[&K<'`o}o}RN#Iջg6D ׸h1v*ޘqM ?B^łqc{%*"pO-}Zn(J=#:z6EnDJSVI#ab""<_ְuBq~dWj]Qj5`0u]y*+WPKIUwƦ}qajhӋo?񭽂JOVxtB\z6v~u&V1,^N0i,GtU%R2붚XrMAa5cYV4hs88N,;ZƇa"wޗk`vnJ؈ R`#Z|sSvql;N72bՓ$CD g.Fiې&"u@0(NLUUa<gTUo*pI+#.3&BO'2ۈ=MȍԬ#dww$"Ï*7H|5hIDj-Q|2{?qs%2ssF 2/6LhHc,icf4e.G[`|o- ]h kee]zYLsKI4j^ xXlF& e!˩fr% RणxV z!j)ϸo0 VInmE# 1zx{T㭏?mOEb9̉eƭB#{]?<]?OHåSOCFӽSѾu.QNTN2s3xY+)7 |\,>Bprξzo`Pd& .'r٣2^N0 mb?E}iL63v+m\7瘌xb"Qh.u],'.Ui6Mu߹ 01I MUktw8LDj1{q񡧑0 1 cD/MGkX:C!K8U}2b(d0.e jyy3Q @AtDT-d`b9yKW|#ҳí3a>X&DžazfDJSfV+SSnuCt)bTR$ֺAg'I]K'FRι w&ne \tIZ4116}zu.)wt%4@IV)V"1gB|:qFn|DNŲpؒݑ ?$RkԍѺ {| O,,]v}XZ414iM#x좿q_̀2bq9˿VVVEEzu8tf,LGGBS5Jz.MsxΜX9o*$GEEB2:x!O[A_^%yB.z2, DDK֙g+^R2ZQN\W  L֓twf.}~3u!xLp-gxb. bӟ&U >瘌xb"Qht>e{ FegML|K^w_l6?u̟^e@t7 olz|WSv^` a3;ǘ_VVV>~8Cb8d[UP9gFC:=wh٢? %KSҐݴ7|y|΋Ѣ!lfpt t et@pf- SZ&f!b)yx"柛rYxY+Ulg |;c~ܫyl#:*[,S^͹OŶAr41KDmz]?<]Zv#bE̅fοX&̌}5)2r[nx>*.52{W!:.4bydy4o&5Tn|S ~{>.u],'.Ui6Mu߹ 01I MUktw8LD&VeUmV eta=Mi6.A{$U'4aa'|i>Zb YrǩAb(d0nݏHaɇSUzBfR";dj12N> YQ%Ɋsʹ1'[H}LN vGPiʬjbYl$^x\*)? ̍k59&|qO-]ܾzvFDDR`#Z|sSvqG!cӍL;Z4116}N!xvnЕxtwy^Tma8N<Θ3 !>8o#Rp7q>"7Rpؒݑ HvS7zeE'q3;;v!ɲs1s}}8FvfDF#t(7Exy$:[ohF_+}V'v"Z\ץ8bvY`H2>RTM3{nnZ䡿cv|ʐ\:,/y IDAT*Fb4MU5FhX~>/%[j u@tYq.63_x+^q!~Rq2Ç߾YZ|)fe-SSnu] R/J!aE(uOS^uWmp5ޚn|#Xηaֲ֥ۇ^,ic9UeF҈abӍLXdc5YQDZ4116}nuBCDnz(uOS^ul08m3%1,Yǽ#&r) 2/hH{f-0vorM8Xﳕ!ibniv9F[뺁gs:mF5[{ vUIwkQlf=ݣ/z|X~5P0 ,[~\D։3Dw" +VߤqfOp)j$6wb4MU5ЩW?^rTV;NĦ9^eX7nvTu]`f/+++?~5=bW_}1W%)E޾nl{%]֓עٯfYy_[e" yFa,)(XS.3ϨM< BG\|?N(Jdp"kXBc\3:,Ub?mOE"ꛍNmViD(_o= OA NNN&<ӑIRhp냤3=ӷ*xׂ:v֢#w4ֿΠ[>Xzу-? 4V̦NOϒhMլɳӳԏ}wߦz8lTˢyۃ㔝N9+։TK'Βм9;O%ϒRmZ!]|SiTJT$v/nkM~ %,DZ&/޼1xQe_+s- aoDd.yguEޭu5 k ,zۗ㦋ri<7 ԨK*g2Q AD[nL fοn&.^l-ᧃv͏)md-dMnW|<6[ݜF,ϓ,}PE67N1~ac7O-/Pl5.:zS.x,TYxэlhjDDg\PS^N`-<w#]ο;cWg-~ w,w\y^|Q-.NO:LTuBQ:omS:Ď9LDƿL1 C CD=3 jb3^xUI^{9@?YaYC{ vUIwkQvٯfvCz\=P=~-걞j杕U:5Q:l ,Brw }90DR>Yq.63_xk6=2Ç߾YZ|)fe-SSn:0DzTR$H/>|A֢~wճ__켇{%/zZc=;+k5bkӍLvs } ={2R`#Z|sSv1JvGTO6V 1;NJNoC?]x87[7OS7scA3^iK/t}-hDd))VbLN;!''ew3 wCpOY.q0bLg]> k_)\p,,]v}XrM8Xﳕ!@9dfZiEhsb śi3CLsKI4j^ \C39BploU){ʫu>*?h/jw˕tՏqy5 i gUգ͢hd!FTUSooŽsי2g[\Mq^ 0Dj'<%kݫSWݏOoM9::d$YUwA7YCĩb.k3y<ӿvۃugj'y1rDи<7k~ܠwoۋ>;vhLGnuw0DxuY{Ogs\uWmx|t;΃?0k}p`0kԼ?AWkL}z[@k8gFC16y}uǵt90x4MiH=A{,XcOoա#۽^kke>cZj~ ::v,N{SftKFTp<"b{"x߬%mNO k<6##aJPh,n9_>Wn}[2U؜tP$wqi6r|wtE k O9M?ދ%"^(25냮Ȩ'rPHMm1j>.b.lo4wde5Q%"1v*v7G̞%UK9X'Y#(7ZDD݆qM ?B^)GDS$QdSl4FxP^$ʵt;Aqb%w80 1 cD/MGkXS *VRI.kWSϛBϛ=0xޠW7(~)*2ä)XN:0>|,B[Ű]'+oBn뺀2y!=:+%Q\l3jnmug8VZo C8dBw̳7槛ML|K^go-.փn8uYF=tj歞m1]҃Q_VVV>~8Cb8d[Wzm6`P΋~0?qӥ) iHe@شm7Oi^C_z9*?.}6pֳsҢWOx::LDCR0%(Dy|f6CDȍ hd\Sa.ݲ}o'6 # vadBSUZ:]S+MdUmVΎ`Z|VU)d Fi 1FOҤ}Ŭ3SۃWj]Qj5`0u@> 0 nQ\l35`L[՝[/WL$ )OpG{Q齍41MO>Y:^׹z-܁^!i}xz=VwV~?<*jFeR[Yu;`?ȹ#7Oygkki!xT4!.05J2MpWZj^m`g&D_uA~[ң!*_21 ylFFKÔ/دMWX <>3Y,RU]\v\l4R7/_gz*l|:(E-˗+٭ϱ2ӤX^"u,ssa{}/&+Zfky3[Qgs߶-mW<K~|sr5Z7Bk7O2p[nG*]יnޯ%E_ޗ5!`K5UBYr~\I_cv8OW#|V%Y=,*Fb4MU5ЩW?[ ڒĎsי2g[\sK A!#ީpnhrR2&kݏd"RZo<gX|WZ89rEg_7JC(2mCFs\엸VtwƙGL}c#)ݶwJDw齷u3$""K s䞞V:25aꥳX,Ums1Z qO ;E-xJvsLsS. 5k-m IDATXڇU[8{p~vafUI^_gÏshtQ񜈔RQl*4Ykig}F=Շ1XOd-mWYWT[\N9gt9P >Xکx=b W"c ..G`dDCL/:g~.>(<Å1y"s3v^*Ãr 酈n$>O&OdvzbH Imse].s)~bt.QT&W҈s:m|Y#2[BS#҉]1V{r?^xj/B?˅=ָ~)Awk(h/%D|*3l*DBYD=VRUJIfQ MDl孵댼XuVֶk`q5.qȬ]]?n}p˹YtYHH*Nt4YrjnfWBԢ8n4v##c 0͢zg!dzaRY,'bJBb}rVVQ<^z!""";lJ`Hۿ۠CmD{OAV ~aŸTs6d5RQ>ZdE%*UĨx9edHoA]c&DZ*Q)Nd"kE0k*U)e5Mc6㴺ʲw=ևsٵ_j!"xgIXcw[tDP%|Kf]j-{}XǙ#0,0ۍb^!IJ a`5Mc8{`>곲J61 SvɴLDBdt Zky}uΧ,9"oW kl_DD*0J~s3zGC)*7H|5hI3r%eSxmVHl0;8OOA ! nzU'*/4M#4e`ķoEE3^y!Oy !+eϼ{H3Q)c/e\ @Ӵ*<\?K`=N"N1B!]"懻?[^*$wg# \|B!sE wƎgzvVVt5yg*Q$(Oȍrغoqv&VGqV=1>~7/z΢or)eE̱*_G6KON!s:={G36HKE΋,gfS$sby&N};ļ? Bݙ@/&VSaRuw_W$՝"6/|DVO@ʻ]8K*m/zm˥uj鍥ʉjyo%Kj+?I~\\|97];B!BB!B_8~@!B @AVmrNn_t:{4]OJHY ~ՋY>5]v3df !B!BB!B_<i(⑟V?6Q -X40jU$rE^++YOӶXgcUI$^B4S'hme ]$z1B!B_^^? ;>ΖWmzWs#]Ig;$ƇTS;!M|qa',33!wq0 "B!a("".gx8q[`("ʘNS݌.ilC 4ug4t ptlxQP(Ek6{WeRrS͖nl}uʹJ.٬jKFje!Bנ?<bsm}t7g_,d"TK/oelbf(O8j6>l4UoGou@XEu(8ӻZYfBjI:M3*i .Bo$IAe#tg Cs,AڅB[h8uњ{+YNE|ՏYHmqNn+lTR[|HTU`-6EBdr,XYHU][]ܦFÞzS?__-/DK땷ūz(KF p8Edbb˜{A X$ fl({xv!rTP~+B\@EQU)ޤ j-ДTjSwB_uٟ\?vDs;W>Wx5IMKӡPhzza BIT,www/y GaAn0њHg'mO=^Ie)XEA'.tZF.(Kg>HoU/}]K^wYֳn(.;~tZj߈2YPMn'/tT؋DYn3Wl4ٷ 't^RFlL!3To62褮4jͰoԐmggSE;ݠ/+\W[ꖳBA̧2ɇ\"[>6bRZ6h-HKE΋݇ <3󟟐.}Z;t5yg*Q,ɥ9y2Ǫ|5o%)H ԅvzU+l%VB!0In쭽;m(Xmt_ڛoOُ og=PZ;R„@*m/N.&V^"B!!:!B/@޿H[zB!B@!Bk`@}2}|lXVЯAqA}bͼv?O_8ޟ]s'k枿]wk4Q0K!B!BB!B_8~@!B !B~!B!ԯSI ]e!B!t zkP7-~pYU{f !u3LXX, <5~HӡPhzzaBN}x9_-ķ Y !$ݓ~b۸zjoaeiaaaA!4B螹}ݓ]_=0Ko !B!/? B!B!PpB!B!B/!8^|o?V+/0K7ͼ!]!B~!B!ԯH=E^>2z99|,>6uy9C3~dĨЭ ]/7Gct#/??+y#;JU[{ͧuw}Ӡ z97Ott+%t7*lc3uV?X\nqFXNnPN%t@! گÿgI)yub~+n_4sJl>k4o"k|rm4^(90ɜ IdPmk%ͩ`pcVFjٝD!DhPnQP(% 0~@+lJG6WU~=~cV2ttKSA~w%RG0Uu!,!ݘW5c^/&FjMrV2&E8@.k#8 lTR[\!eh[ՃD?XG#jb&_k˻myip Lu^l+)TJ5rB]vaЖZ5N_`|?a\ bl?1̺=J%3{7~xpy{ t@ m7{v;5%)v ԓKk,+F;beeoM̈́ uSZMz+oB!_iu5d`hݘ˞ydr* &$I ㊅IRTSk{u0f ).)_;Q> Pվ& D81(|-ቇD+xƄ/О<n=̒+Iqnp,VUUPc?KmbQo}5'(ŭNh*o1B\{0K3B"B9n UPOQՓgvS >~;6/~@w$2co @=[RƢ0<+?~sGz1(5cS{'ko*>?1Y:\zf3WVn^=]wH}Ѯ:r\愥1S=)3'`0&&&Bq^__pp |źf$D<)a]NK^R# ں1MO)*MQE N\KL3K}Mp3a4@>)!xqJpԿr mT|e𸭹Lljx2YPMnKٽ??DJk9i~?6\.GwY~/ɏV%'~׵cS{'ko[ ("T i0R +MeL}kn&H`X$xhMꩣ/f72g\NVwFʥA?~;~`Y\ TH&s!jf@7SHg\Zx_x6?ŕ̘!:npFH'*ObDM!ԗvq66vޮkb7THXcFY>6bRZ6h$s?,!?(R~o>n_>PȶG{Gomgg~jcS{'_֬T" t}*id;XY ґSf>Yi÷?if3:_o?~6P?k^[uF>BiwN1:;Vf3>qlRA8ha4~vťͯ[K|zv _DU._=C|N&_zk62i}NOpώr{ +CB!t9 W*?{1٩nuWW!DB&w .g@ߏC!B}B':m5bgjB!B!BӔweOZ 6&t?n >y\2G~O/-'q540Ў] G˅ [ ܪ%W;E l}* \1,܇ /B0#!%UYh[{5I'hh,⳱*_$My>.gx:6;e_[I/ :LiU3;\S 4ǣ!%v5>h2KФun=S3c6R,&֓UiAv#%~jkŻ?OCwһE^+:߾_<Vzi%u*4DvDMTE;[ΞkmD^Kk|2EėnwybB#JUoռr~yv Cсi1zcрlrgvzZϘ/5W77[`L=xs\(R?ٛi3<8³#ޘDU$Y5{Cn!^104:%@t+[My0l|Hx L:k[񎏳l0=\G3ō\߸җJ4<.;~scf" gƛQac(j㭣]G#_ʽa2ؼ˓dGع[ +w[iUhv0>}2jx48p# {,\r+]Y7ѮSWutnr̸7iqupt8=VCyjI{Y=𹥸"nT* 2 Yÿr,KedY;6{WeRrS މ1VkR ]+, lFa%@#Fw\{ @%B _ | .єk<|a&#v#)kTw 4ZHg+c;Nuw3^tipcVFjٝD!q PJ(;ƐCF~)\QYk0cF}X LS#I|ҩV8o.i~5{,ZdjO\Kfd@ c宑 IDATFN[{K {nFF|iE@e1CjI/,u 7g_,dwb4Ja{H[HPEDkMfO_iSkRgacq*(_ !)L wOi>_@~{5̥eE'6 Pp+wE- L=\zy'm6RHӌJGB;DAg$-gMmLM=s 423=TYp8x@PZ$ Bkm;K;;zyXg{oiIo^?o죠]U5Iy1+ޤ\ى`cqZMz9Nκ14(k0Yb=C?Qݯ>X[²`wE=&.a}te3v6s "VR\Gj䨙\Z#dY6+{֦h/_b2yS_Δc^g#as-0 T@$UU%XFQ*\(蔹\;4؆irN KiktS]יq%YpynWJ͍}?x~ˢ?ώni|/הk eh[ՃDs>e]+eVSnj O"VS  \dt*)NR-`1D+5q &jٝ"Qdt b1<oyH*챜+`ΧsM @qOɺ %UUIB(SVDk&h>w}DjU UI'q fl+<~ss^KosYWуWA?NNXXXhe3@) >1vz[-qZME!B.}z[}͋Moшvp/w{xv!rTޔV j-ДTjS3[>qzzZ&apOҹtz4 ;5|]DD5ktb3DU%\"e̎^nFy I7ԭI1n#L:5%t'ZUU-"S{屢`svE흵ŗϱW+ h~ PUJzhplLvƏWHnkAd[_uO[h/t[|/ѡjn-m&IH)Z} ~o=ڶ{TQPN rm>ןEW>DW>-m@[J$pq;z12APE$3`(rzbadD.YyqBo'y4M(4jMoI`=-ɺsS&63 (r*y<iLahV7OAvacAa bG^!U{ mo6EV;jj''ʥ9y2Ǫ|5?ݯ'H}e|_H陙bB , ):,DWJuw[uGܝʗsN1›B>1i,o=__S4bT9𹿝KdpMbt|fVy7Q8Tx~|5zh JkxOLZcB4! vKN!qM>|he㙊o8OҍE^>u{s7S85FѪ(l%“YѮ~٨oo+󚮹EAPD)Hͦzm) Kr;I?'Zhd, `juQ]J_eٯo3@/!Bv{W1!B}ww[ :m? B!Uq;B/B!BB!B_}~y>vqv"/э3#!%UYh[{5C#ѰndQHnkGUvJs,:6l5rH^^l?z<*|ryGOS-⁗ @UUhV2y27{%u4o}n}|0aBzic`iuPkC5Xr2Ze7 &wbQsusyԃz؜PN%D~2Vji9#<;1R.Եٿλ= XX^xs1rM\`حU i$ _ Nz;;V% LJ] 85HWxَl5IGd!q;>ΖWmzWs^j";/S#ԯEȤS7j3%~V?vQ?PDtn$v-Tx ptl(B-6$8bAJ}uʹJIQYV@UffqPhKbfrӬv3ceiqupt8VCyU;Z$Mt!P#֔Tz(,  uO(Ti02|&( Jz=SCV4vfT'cJxɰ4) "q,ʽ2@)S M̩fK3@ˡеB))r]ŵHU7tr5SݢvHcn}.\MGGMyW_rGn(̫/vUG @TtT)sk}HY<ᘕZ|ZVnÛ7gLV͘F| fORp˗g]DY Q8404r*iF#+*4Vnj7{hQ>gImا423wqF(rz;L45o߭M᠃RٵwKoI/{fyg>^f@XEu(8DQdYV'w9cr|B9m6YwI:~"7δRߢv> xcvCidͥ;.1CO_An^>Ki&bzs^*BHB]$N AeǴ."7E0#[5]+eVSnj nΧsM @q/Ȧ?jef$*Zl6Ry$Ing1ImqNn+,UZs[e2jϸ__)IGh W$2TZ'5~ .Ր'5Ύ{^XX; D81(|-U yVd# UJԪ>@f{m@ȖO<Wӎ Te=S-Ovȅ ` Ez]j3<+Apc)&fs|rΥУP}]z|'^ꆲgk.L5:Gbq}yU)?pU z^"=ChѧdY'wt'Q ҧ%G(uˡg'B OU6]I]z-:fk|[g[K}߱twg@BsPqYs;kI+?hA6vSxzy7lݼIe)XEA'tC;& Ի wY&.{m馼GL-j;N6V=_2uՆv3~L;k0~86o[Zk{E GR|ko1G}%а-(&r LO)*MQE 5bc?"s ~٣.;~P*mDKFl 51CtZDR!̅hBB:|<3z;|SX3h1Zxw%:O)B#NtD_x7= 7ޜwJ#ۅx1h.va{6?,7yG'J}X1:lVFpnm—sN1')B Of cFZ4{Xs3OCfS}ll<Я O_w{,3OzehK-s,dUj& rh${8^IX-vJL]61uJmXU6V濤-p;i#"F8q[wU> QTHfx{( L]@ P;c7xY?o}j+5[{wx94>z@''o7LmKE8LfWb@5)\H{62+LA4Bhg$]_,w` "wW_Z,'>BPeQ=x"!%w<+ļݩgf̛آkj"B7 !B~!B!ԯ<0~O}E.MܸDҧx353f#bb=Y"/ OڪW렬H*WHm*"C#ѰndQHnk0puwd"䶰* \rk&& [ ܪ%w;E$ڮŶҨrjf38(% e1`@}>ө4:&QYV@#)zIC\~\.&(N|LPnς"B7jPSpՈRM&rύ2ZEJ3KK#3C t pP=X}_!a*'rM6w9<ᨕTǞU0&fj$N>,2W *.W%ఛ:W@\j=^V_\ M3$40~f!B_{+YNE|Տdz)Z WJ͍8A,Cߪ$ "VS  \dt*)NR-Dϲr|)̫Ci7v*Tvӓ/^(O`#as-0 T׌y43"b27Ax.-|p9A!tl EùA0ioWRG s-h.,v>u_D蚺C!H##3Ŷ A|3O sIU)IRo, NMڧRR!AP#6<r ;xRǖ8ZߋkƄAl~_[x.]2 {gCp܅Bip,ldԣv`phWtYKJZ| {7[!IEQEJ 7s\M@l2꭯DUUSogRA=|*2i庠?ψ2]tPAڕz'BXdP0RGoJN`ev-rTN I"eu㷉|y@{k㇎J<{](m7!+eɵ,`BUEU@F*xBöl\ne$0>4EI%vu;H3"%e?b&t ě&p$`nqit<iG^Z Ȫ7ZVt ՒHV&T U~-IiToȶTKgvnvBg?(彤#}!e+'Gn;{?SL+ڶODdTζ:\31DUM: Ik\&dn))3ɇl6EV;jj''t/1rW{-Im%  BN 4w/B+k`p:B2C!\Hft^=۫y(o}_ o6?z'س+ɵJTv6rg#|1ZL{kx[b´V.|.! .pD@6 P+yP]/  !=8=! a hC!BWC;W% bAnlfB7ZB!?(t a hC!BWkB~k{/~WH!ЍB!B_s4 :wB!^3fhpbI4#|IsikhOgًsÎA|u50ūׯ^<}0d)./^~97;=QG^NC SgΓ6:ūnK].&_:Sw'4h;ׯ_ztfԮ{{g[[e峇a}=_שg7 ʠl,@1&2{+K{.5_l-n| L](HFo,Mgƌ. yբ $M$i}uW߷_@4v2%ry:`&T]7ߗǔ=-*/5W77[`L=x/c TBTj0l+Tn# ]Ig;$ƇTru2xŞq4h7K3OZ^j94ZVji9#<;1R.ԯP"wDԣwxMsKqKEFU Gdž"Բ;lC@}uʹJ#rfmV[4R+of)3.aDT 5>9RuƊ}>ɐvD%;tVTPUN Q Iͣk5 6_:*K ߟn>}/u4h=~t^{ 2yltT VZz4d3[]͡3T}g 6{WeRrS:W4bCGGMy#f-KedY;ƌ'BATkM˒_cI5;<ի/  ; >fkmȰ(O8f߼&V `LV͘ƞhF%#SO_z57t}'7˅jmmا423wױ ujܩʝtD.S4ͨ|[{ߟ^XX=vHժbw&vuJAHV-cbo. ,wSgrvֳu\ԭ;`jq(C$"˲u42R{Wh!i[ νz(8'B`vSJKRޙܮW@ 0QRvԋ ՛ El)NĈmmf^9fUV鰧׬VJ4ycA1Q}]yu(օrz|aW$*Zl6Ry$F1Nb1qR;f Gʧ[aGcAn%:Οz̡G vvsnS߼^Qwx^? o[y U)\X MIvPj?Z _ՌI㓣t.ͅΧt XOasەk6D.l~_[x.^->Bѝ/i LOsATvz[-tWqDJ/-gx YWу}^'>\|i"c A PU?X_/ tZF.߉ӭ])o{g-)k%.0sՊxn(EPvމ8P:eAA%I"h#He$v\s1E}bFW.6 nVUi\VUU83Kh*Όjc։We)XRWb^NM7䰳wÿmֽqj b *IzeX\_^h;ePbbc?"s h!3To6Qk~K=צnc=3E&*?{ж6 5 - Y}%ױeelpMeKB) $$0jW4 @:afbGd#VqXP)e簳BiHzb {öHٮfG]wG\b ?bȝ_nvpxJI>{أ^'Fuf'a[]ƪ~l&"fK"~]L>;?e4vg?9@7d/J_Z OjLz}&GVkQp@ Q&˪#2d6/E謏v$'I4ϛI5uk2)ArW/:N7#>KLym j>oN|Z ~v'sTZgvm[ ][l$ ;)*mn>S w:1巛"T|C߿1=Z,| ~1jŇϼkAMhIhlPUz_w1ykͭg9m,ȝJк:1UYI]/d,3rxK V*5$'['?S8ʵ OЬ+ : Vz|1tgtFp? 1ة8_mFLG抒kjʣUsŶFI$'A7B@y_s{C[}$OZm3{h-w$'I$ ~goӧ/J!;g?9xVF@41XFIS$'AԊ?3Gw؇}PrRE|Tx8]ÜO΅ +kdE/hZ֚ (bpoڍ?rjvDb~Q;Mt`Cŭ7˭SoXKa3b8Ǘ3<d8 kg-m8#CLWE #  ͺ 0,}N- =\-!@%اdxWFŤ@caem?P͔**lQxekZgA+/J 1Fh!ShO$"kV/n*Uo68 b=ĢA- ~hս/Up FQ&^LO1JnJ&o/Mt(DEm:mX] U,ٙTdRp)t@WٳrލXOdCo&_kNÿ3z*åv?Jm$Kۭ׾ep%?tK9:ʹfZama6u!} c,w7Nn>N[<ien3^tizKT QqQ~14(c>6qk[T2"ɹD]鋢rY^EvJ;؟! ۴3'rg7"XMmXUOƹr8|Sz)?a}k1/VX:3J]D2x& ;lsv_tn¾lɐ!hFbAIgMfzড&3qnKuցn3@YɅ Zs /~n[;31er}vA Kxv>|Q#rxs:"!g4N%2y\VE~cNĂN KaWapʔEs^Ğ>*VĺN;<'ͯ.Yɵp3rh'g[.ig (D*4 B[p"vk(Q;x@=6v~:y|ŵl2щ~.{l@P&})R& ,,-9z =Z,7_(JWkǭi]jj.gOx? M7+֓3\I,S avRDARY+Q9NnÌ)|=aBPͤWuyuiZ_TGo"tQtfq"8T/`]MV$ĉ O5a֥V kD,7#oô.wN3 9wڔzH&"9s|MUlOFAlϟijpBxd<D99q6A?ǵ/AAV6z  5r   qAd2if/}Uz`N "   EAAAQߟLה%ML8͔. |:908nu0&+gcL|jF|&_|vg(ɝJ𸥒([(t[b6SŠ `Xe5Sͥ -e؂3MHǙ(2~vlg1`Mjtq!>~)J}M(9uzWَeZxȯ`0O_/681(lJ}TkZ-oB}}0P FO.0GLz˭N)} ?!iԴg33\c{$"KdU,خw ;s\Yy7\({{=F_ăvyWF>ۗOFQB;4;hdۆ$A;v;rR@S=#3`'yF-\:k=KB&/)3 "` 8܄^ɷIס]fJ[bS%vV[lQ"A_it>ƛxhB{t-z@TEg5MM5E 06_(d"*&˴ծtm9d6 p lrrUT@6 }rJhƟ 2A |r[ø >?`k5⼉SSqH }+ELéAǟ5毨`E K ;l\]TIgX?X&}B=F1vM R5vՉŅ`htMηT͹4%?_,1agu"^AYİV O?w3 !6NSաTA}v3Lne]Ba~T vxu,bG%<ܜZ\!~e@Մas}cmt#^YA[7Lz];^_^^/M?ֿl|A:ӰD ͺ +tpS­TzzNY~a/̏ B 2 r$Y>4QkNXզ4Ğt(VV=QsFǬCYÉ䤰ȁ |>UVF^^,}N-=tҳ3XwBrqk:( TE?ɀr=?׫Z($ц)3 0T#*]@m  syZ~3/(:@ޢV}UQ<=r1sjRߓثNS˧󳰳_W33e*Sԫ1/2&W2yL|i¤KF&b@Bo|:P& B^S{mBv=A[r>WZ@7[sr w[.ߨ@W345p,ޗ*8cƑ1+o%8oՋɽ1M@Otb.߲,FM =?Wۗ{Oc"^%{L~հzNͶwۡA1Sï'[B}}ܪ#QU.)4*֭dv*X\\"ޮ1Rywk k5mxlZі7X m|^eb(O$ϯNohgmݨMwSj[eUeXMUUhp8{_(|K( ӿq kߜg\y) IiJ{u!Z] k!Q@0\x`|djpaD<Wl;[K :ǂJ=.$HʹaXױI<&I)$ E,DHUݾ0hJbͥp|]$c;UuGxMUKxna |G pIX{MTec]ׯ4E51`a<؋Y³SVKX7 NqrEl+?$ ЄSa[$lW B*Nq9D IDAT&A< .rkNٻQxzhEʺ}.WBw"ˌD$)X{^)(׉{QBN{U"G2^8ο0HWI,6h{ICPeI<Ofo?Xjҥ"tQtfq"8TP=4/{:(B;]W:(6Cы*RWV9;cꜤO3>$|"d?$*9pA@Ieta$9[bmaf-r0cJ$_O"T3^4tV#X~N5}DNHw`FƝ'V?>. #}:j.kOν`ATϿĸ>Һ*dHMK{Rr*lX Aۗ'7bݱىF#6%A<^~}gⴼE 1H$w!}fMd0yciV))uTxrYhqL,ӪVm <` 8܄^ɷ  Y.J DqIQN]YY͈ @,iWuGf'0,M ko{2Ǐ߾ (w:nX-* KiRW']a<;Ŕub͇O/%4lRauv)`BrYnK♒}z-jib5& $I2 T]4 @kZ60@Z1qj -|_%§˪H'aVzg-~I7iWli-W˧:U33e*s*jv2z }W=0'$_ysay|wtK/ oL/h .v%=3>=F[E (Λx5%tqH6Nw3qtNÿsƜpdZ{|СY; ~jQ́dܯzD¯I$O`Ɖtk"ǦumyxOƹr8F)O$ϯN/ob>(fwrjd`][_Qy^Mk[0dw0V說"F(c]IϷy?q3~k{t']2~hv+}r 87e\6mr ~D$?$8LPpraN Kfg_Y*v.Bk`W*+  @:afb_a$QyDEGzu6L {"I#4a(uDBvyve򸬊oӉ)݄O2fIv? 1ة~$OWk4K/C HB${T%?qwjQw:Q䚚h\DI(am$X"Vیz~yc~D$Aam$AkGaMTr-L?]psZ55)ϼ{!~"~el[tFOC+_]P2fn6R=l̾ J<;lp&B6$zMѶ`mo1 X9N{t[P0@RiNO8MЕY\,h@Tr̾Xbg@6 lM:^&~N 7Y]nRGD}Y{rY10arEZlz{v_kS^k/tjA2rJh}^~ѽ~pbou G<<Aq-a\I8o9`i&iU]U6rl bP4jkzDzF^~m(l;[>̦kqGC\9,ZfbNC̰LXrggܩdme< ORcwC6g~ w_RqؔhbڣjCbX|>59⧄<^M}It`c_=ۛSo̰[8B_ORX-;0j +;%M:+E5YMl|v[3>z09466m̤%?_ p?{av3yu,>!;~;&K8F1vMn.&gX|!j|ܔ[Y^^.OX"v{Ry^w9):zP;4[SUyIEҧO?j6d~Z|ǿnq//z4lxPN]YY͈ y| %@'_Wvؤ{h;`3lX>XΡ5Ol.vͦpm7bme-Uhs.; ^y}Rzp!91z1K{Xf,=aC;M-/////ݯZi?ߛ.7>w7WK~J+eAIs;Ht#~ry>\͟4ES&@Xm}>/hZ֚ (Mi\^ߪwy60'u#Q1Fi,uQ^5f:!GܬSoXKUOO| nvaxercU*<)Ex=\-!@%1ksPFp"9)lG#"(۵NJXqUz-OYfkir?[M< Ьvh{|Sk6 Uet !cz|9+-z͡`wKrXf,{59Xo<7x @m6i UmK_z6 f_; [ܧz#r/%lĵ#aC? `6j\>`)44hG-}RG2O.֪mK:OɛgK&]j7GG5?E\:# 57 kތ2lryWlJQxm<ҏEs޻eR~bhtV`䧉v; kkM a(2*aɕ+.M~eǦumY}۴3'rgizkp^h 8e2jXm%)yʯ8{w>6ֽyK Q뚔A.`ޔ'WR_%3by+l'\9UƒE0EqcFz̆|Ĝ7 s6=pij%BP?pk"`օ!&OĬ%r,Nn,c $&,ͪxf˘Uw۽( yԎMS#2LS$g,hKpͼRn3@YɅ ;$]ZSKOkYs /Gzͯ.Yɵ` '/bPh7i7GFL qY%iXEwxs:"!gѼ&LUGzl ;{_ݹ[X;fGP ڃ> t^Oqx}& m{c~2#2p;8YCbl(@Ժ><8 <3}v8׉{/6Oݓ!gkw]%goo× =z!b:6Xeyк'=wm $bZ:߾zvM슆FkiqM|tr[[띇{R93gD9PBśx)tJT6ۥj.cK$|*Ǚ'j.gOx? MJPν|P""ueF?NMIetR=whMza/t@<" u.muɕÌ)|=aBPͤ \j =Z|:4`/v3:1~QxPρex=qOȤO9I&g^}H2XlnyH}cƒ{|V?>. #}: b5a֥V rjQ=9vS98YCk6D?Uoy"tQtfq"8̇}-z2_Hm~<] F?=s/߅JN _ahآ>u<^8tMv"0Rj>-d6/l$~|D9R]Ʊ}2Ӯ_B(7>kn{WaOAAdhSyx  ɭl<:'%epw?y   qAd2nNW#O?/ +lӮ/]t?ͽ   ?AA1yIJ/f3@}3)C3rW7Td-9^@z͢t,;LA(&8|)- O p?HP~Utpcpq91yk34 [w(Pg1`MjtLAJ;t1Nrܽ˯o%ĺcKFv0OAǻn 187in{2b7A<*ttNw*hWj] 7,w*q[,PGƆ_q(3l&X΁ܩ -; t${ZfAxrYhN{t[P0@RiNO8MЕ Ӱ@؄a)'8ba2pjPa0yciz|~By ?YY]nRG㢣ĄLb8_lJCxiE@q9$A_piE\OrsSjusl搚rYOxK N4,I72zM.n,ċ{}?29*Ê!vхX=:e &g'F Exz Ьv_dĎgO$ZUvW)hw|G۹v^W4س@ZD <+!b8O4ܢ{/ te1l*TazMz1Z?Gd~*C|l?m`]4kȎc(O$ϯN/^]510|Pyrj:7*Ehp14** EYs3í]g c v_nfZama6iF[~(܅s^'i]6*ݡ<1\^N|cqr]4.:[T2"ɹD]\#W4@0u>ZBIg de(s|v"A< 3}v8׉{>qB# hFbAIgMf1%<0;2eB[p"vk(Q;B߸&ʁ*NN0.`U޹"A~oUS=4/{,"tQtfq"8TWz3v&~d.K4Z~wLӀة:?gMx5a֥V 拞b%Sj';7x0%2}7 ~Ow:Q䚚h\DxKlTa/8<Z(`k%uPL[ oxݵ3׳*A<-w$[хz|߿蕋U2.AB?ODsUYh$$ޖ8@AZkma/ڃ?H"I-  ~W@vvvs$ ϟo&{@AAĸH   q痈߉31&o >gvf5#mn;LCBĤ`JWGë0s%e56Tcw8&BkrKZ lbitYfqF_dݡ+ g:13oï^Gٵ`9_hkP˧s A#Y&zf}jeD;T/fn\U2:w<ʂ~+#K"Kg.^_>ϼ_ R_<7q+nGا31_@ pՒ,W@cXX?>4&aK}yt5 À8Ot^NU 'MEpag.#kxn.8hhp<*[+m~81yă.县| %V~ocx.FL_o_E D%yxd"⢺f"QM WK$d..Nj:tAVJWr~mmM`AN5]F1@ס]fJ[bS%vV[lQ"A_it>ƛxhB{&iU]U6rl0:`!@qޙ ot /\NruUQG6hA c;,@*+P/֦|g4OrJhg׺Xeh‡Pkm1N]Dߎ ;&$H9;ɞ騣uYqhy8?;J?Wt. CrNaLXrggܩdmecɭ7E->_(J_q(3l&ӁŏA~l3,ҙaw,nzTl܋^?PhR˕tw=:ƭb*hwX/*TzxXM)h$; (.:! ϶fuc`HBI\VW9/MI:Kڅ.M.NvV]Hkf F,,6~2V /-K&0T]e*]^Ͽʕ:hewipޟ*u4x. 3 5 e@iӄ8cr>/0pŪvvbUcVs}MX"v{d_Ik6 .W50]ttn׎ Zbm)` ׌ijդH!Ba-3JfJKڻ劽Wm㕗 &/>D9]tjg%;7NfmuIڹԩmTs_(ĴOȯua1e8,6 LUCІҵsǐpw%wV2ҫIA K޸0,{厄QeyV*#N2~(? !ñ H=I @kZ6b)ͼb']^ߪ}ܟ29*ÊD[x*f^2Wb4g; sjSm@.~J^5v0EQcٜNƒNQC?!q~;!za@֩7GQX,~O5Œ˅ 2G}w[z-^b+Vk x.dֶ4j0DnSyxoEh[TAx 'zV5i fSvPAfQ< }ߌ ̄f]Ei;۫i]XSapwxZtU%y6t6 h|41jY`>/; D%{p`TUgTUZV!3<5 46 ab~{ґ\to79v۴f!-(w8(gVv;,o*wr0lsVC&kBx1:wa>U ,**mz&ߐP;Նp١\e8@dd"F8VjHkάFAgQEC`pʕirc!5BB}@%G8!;wofS5;^ 4=AR̹ިqӵ m3'|c~qav9Z=RˤTEѼmxmZ\ѩ?PfөTH"$'(t8됅 P2 Ֆʲ is^,/5RKhmEU h\DfDPKS1 \C1h,u*m xBުj@޳k0 ]o80 +08#Y zV_"ll*ev; @ZM\v}뭒ެ ฽y6lᠣ;^@*RI&6ROMvol(Q]缣V2qA5pbm{\rxF$ذbP$ƸDӆ(*_TF2bZt v;륁/2˙O6USlB y͍\ p}|u`$ck1;+Ҥn5,٨jz#d6_l\ZZyO5Bk?DO;>YCl*cF#"]9M:2SQATv"ofv>1ԇ[+mdz=^dbmc)M*T,^ƀ,71Z)?pi]jSٚ AҩRg3: rg2MHa"W/uinIwA M4aֿ.rpOo_}K6+wlgskѺX?;J#+Lu7 ֩ m,HB [Wzd}Y3Qi_;EΠ|+m.phŝ/>#N'cHIeR?eROqluX_mH1qc̊CsayEčA{l1SZ5vGwxn+j_tWx[LɈ[95>khDHO 'B!LxƮ 35P_9KY"owP&Ob[+MﲞeuBIjkm]AD$B@ <t\b&>}Y\\\\\4rSao߆l<ܢDsc_ЈBOB,@ ^x$ ӆ&Kݢ\ B"| !@ SA< ?C^J @  ?@ Av=>^_9~ﺮأs}̱Wẛ*Z&>~K}b@&drg7!];I3͡~{2#]6 04I(eӕ?OvnAɥv."zU\\J;JZǡX2sHr|Kv:05khTX0cxi5ɧue(ɛzmehR>8=<w)]azҀoqqD;{?N~#zsu3xg8"S_g-w _gs zo,IK[ s`*vR-aɮv@,LSIkmw} DvqaTɤdqݼۙXWxr9OgJqG5ڙγq[fOR!Za@Uq͙sG&\u\;!7O7걁Xlu%<;W6 LW#"[dn.-=6ZqE,y@"3a#Z543W@+e׶ s |C*Z)}\v> (&Tc$2vЃ<wgEog&?3-:CN|uo> t9л϶ҁLxjh$ON.ώRg݋0BƓtkl} ҪǹJ5f3yt(sL(n  YD#[{ۙmh(z p㬮!44E>?@ k! ׳[ ZK:J ?0L[Ъ4 S1oUhf6J<%c`V;*|E;lP ,lhҝvjf7k52N_A9/ dħb^;cHIChqcȒNgz4Cϖ$F'9qJ-SbZLC:ldo$8#3eJd>[1+ {%X]N~C?%gR;I&WudƓQUG:lAFV/rxңDwed?P'}}f)p_'zxznbXL>xVu<2۷_?Om5b fW0jaF*@3>:NC?j+B\@)ϚO\Ͻ ]-vS^ r u|v˷o7o]G|Uu2etwN,~vX iLi0'` 賭96F@S_?qx.B<ڞ͐SpT~q6F'(Sݩ CAu[A nlmc7ovYG1,y0Q̗ Un 衄qkuׁԄ.GҁoIwtBKP4qjA:rА-VRi_l}>nr9v:yh4Ģ*J1vmwPEVӉ rjyXv-uEfU7d@8ADoukiWw}hrYQelMNQ}T~/ղzEz{%˿:K_"d -xuhMP/!*ile3}uU_Jr+l(hJHwhxpF };<;kхϾ ٩f.v))!;6a ٢#:lס3.3g^̷TzY۠xߪsےq="Ƨht:n1vܝ~cHg剏~GFhŝ"b,4l5qۊBƅ30 B͐p1O7[u%<>lۓ '5MjW_fRv"1qN[PB*2XIqc(֮V{}`v׋+*K1׍:^0! =|Ъ-satCO]aЧ7}nh&0 1~*+v۴f!d0:hkzwJp\ӱ?gKe|*bh[DZRu];տG:|}thE8}2lI IDATsVC&kBx1:g+[7\1ܬqis@}.7`YVUae`P^2jP.;eX;xd\\/p\^ 7ANWc5+$^qguC 3\*7֬ V!UH! O$͂|4ˀhND̲.(}|-FQ/2p q,uJeR"xC >is7vy3=.VBXBӳɩqyU((Zʬ.nrqެ cq;GQX8hT:?DN"?4!fܘZ;pTkݜdn=׆ Cfa YmK֛#1erFRnxKd<`)z5Pm1D>՗ nUa6v8a2Hhvm,kD]eH|3$܄rm|6{jNBB,Q/3~+[1Tꮉ]iRuy1ݾ1B%00czVMn3x4Uk`ҴJnӽ]!z0p|:mze&rm@[i퇟c@J~ t=%?hŝS5P:ZK}d4΂,4>̼nEH|3$܀r]-]X ݄Dan} zij飋-MZX+)^f>eKN-|ک\ Gnu >nxhXg֩Ek܋./ 2ZZZ"b֒y)+?nBgkS(Kd-b27 3q|:':QƧs Jq?]f+26$6g˭N?@x 7&!](cD UeA8Z=kBk=dqtؐ8_ d@ B-n/>UBr% q~Va :բmFK!ZC+$6Η?@ =14Yn.q]GS.KlA @ @  @ @}أsV/$T" K- ٹI%RڋN";kPٯ3\v=8_cy@shCD>? _ٯ z@قS 1v-w*{@&^֩;SuH|}  MJt^{w2?\1o spX&>eFx$df%YlpG>‹̮>χ8@Ḫ^ vCIț?yqͲlX2sHr|G`1j.^To w9xJTKtqw7Z+vf3yB!pJP JZajMp:(LdR gRQ>v&Cvl *gd ͂uVa,Y[ωuOW*Gs6's0rfq ǫ=@U>Fٟl3b\i玈lÓ~i@,L˗7r3<av{9#?(*]s + Y(JY5S!͌٭LL #CM9& ke&O<9鳳T?;J5+ۅۙ؈uMvf!}1bh|pb~Oob:ϷTUiHA@:[eZ8 Q6'spٴF>F,K!hg~D`C>JF05գԣq!7z(Jt6bڕ\HaM7mlBx0`ss㴃lяoLJCKcR;c>ݽzJqR;I&W{t=N,=<٤Sbg%;p-Omu1oUhf6<Va(UQoI(9K,zvo6SŴ`u22V? /~rF"R:o>Wi3 0 j|*|smc@)Ee@*5&BY{0ZZMw8!]HSv`N\ ?wu9fƨI:kW6rj 9@!TU{;![=1V `-:NXN ٷ79xB8'kkٶ\^^^^S'}}fѸ_>BnWw&b>] y;g('{6>D}&ñ Z>Ua#\]+ YGa OuYݨA@9!=y&m c[})f -@ -hV5֮ͧ!vt`qQV!toػcvh]BZy{ "]7ܱSŸedCaP3hꖹrPA@s ^(Z@k =4X)]Ax<9N5#?|a?Sk)=ڼHh@ٵIοY.1 }>@oyqlZz*~"iBqC(7K`|>B*2XIqs]f~8{qYL0:*^G}KuYݨ0-/w6(Af۞\ѱ9Z=sz6Qw_'QpPά[amrޙبqr9p12{,^Me@U4@/l)٨xPCmܨx7߽ %t\!sjWch8{~į_ܚ* )XᩙdYoe !0`%u/aMݗvaq99;'kk'=w /0[ahTi'CJZzk7'h_5%&!Fvk7*Nse MS 鶱wѽcC4  8M:k21'Blrm{1dD*I1q|`B$1c1,[$KMnK!Y]-sIb[,iqdHW˗Q |ږInꛜp5K(8,,e bw7ΡMf\^n0!{ϗ`u7-G dc( y͍fJ!rʬ.nr30.{!h?ݦfZqT>N%>.rXퟶ@׺cR߼PJ y@x$Ti(4;lٜgpl?W Fڡ"&uq&ق'[\ivo : 6kݾ; ֺ<5[_luVN~ZC;Mx7!٫w9xB'Ǟ7BHk)bLŧSWI>HԛTT̹kئ=8͡DP:Y%0BܤEJr|Si/u/,E56N>uF~9!U0XOluxmp$g?ECTDan/fhR#w7a {|lliil <[}^XK&M 泧c<·]cRg<3_@Y"oh/:$3q|:':QƧs Qϖ[_"0^Xs~31x)CBL|i#/n@~˂z?IRx @ < jq{`OcR+_FD'Z4TU~—?@ =14Yn.8—ٿD @ @  @ @5H|y`!a@;2{cɨRNe2sJ.ӵ2c\aŘ޸zHN;̔. |0SOCN IM>}+Fѵ&_zwŖ/Y( HՍ>pB#gĒcν w/=HCjK[=>P~X3~Ძ^N#2:8CR|'c/!sj<<@ <G+=5S seH'fc'd)o‹qd0<@ ´|1P?'|̡D~E>v.~X3"3a#Z543W4l:SQ73XA,1a'LW#"[dn.-eZ9 V8B(O|8hQۙL{;ӑޱM5kZ(@BfVA&D,t:mCS5ײbZY͚`D,arY[^^˶;C?l6 75׾"@>ZY.CJ:yQJT$\=pv:&@Sv`NgBdHɮ|w#~Zavа&\uJQ1_Z:770 kPم/߾-_զjYxQS)ā,cS~, mxׯ槃 yw|sd`vwrK;y@8BqxEo--g1v2M5fBܢH<ß v]0,fjUs9tkf%>sZb0L&,V\- KrZ{N1[hު z7$DdϧkkެuMP7d$h`ȕR^ /ʸal.RZ#g0%nnX?lqyI{Z)2 q6BNQ=Gյ_;9F>ٿDx`.2c3-:q?ɇԄF-ޮpgng wӰWV@MNߍ;Iqu ժqAuj|>C?0u!.=SWQϧ7 B܂WD*턤f۞\u&AuV^`H{6x]/V+'kGUUQו24**0lsVC&kBx1:^J%bq99;'kk'ӡb*V2jP.;|CeUEPB`KV9> K;V/DEyJFNW :!O|z;!.wV7>r_\ɣ(GUo.^Rj'2p q|iir]Lud^){kj@5[UM30t Eʬ.g?л϶cU1;+}}"#Y zNmr.Ck}vU# 1R>EkH4mrmqRϯu>L^%/Eɔn^R y]泼P"JsKB8 Ƶp`3O,UumX#(_ެ⁐Qc!khdҾiT𸣚kΰU.YBӳQ+;A;~gPi[kJi2 &]4y~҈?<6'|d *hݥR߼PJ q۬vYk{zsAҩ; ҶKy 94.VʎکicO!5 *>O|gAjz^/ѫ(r]-=d*Nۧ.FIa[ȟv>,CSNꓱ_gi]l~dWËp%hxoV8L[! Atg2zaZ9}`J|\ ?(ATY%rv%& aq^d6/XZZZ^^䙌U>Ѩ[ӹFDB}1B^ 1l"?|>n2G{B('1nNbL|~Ի QNQ,L%z3$ B?^D~˂z?DBbvmk{l|>[ 2T" Km/ /Ƹ5{l}f:eW ?--8pYCirGBYa`Kd@ Nvf P" V"m|;m|?=!Ky^ k%(U5|p%r턇 ip١Du[=ŸYY³qeQlLz$:k~H:>f~r``lprv4 MlWL,a,%_:NXuͼ֪nOz&;WV+;gϘ/+ҞNzgk%43NE\ISNrD+G PO1@''V* }B` F٫6ho=ncAP,VRiFlCD[Dv}K6s4F'Nmp'֗[{$d %&LwmLJ]'2:g;y eBX-Tc7J|G-G>jjFPtjNE4d1.߼3/ ϧ`4?Lx$>b7^= ?j!CM @iC)y7(ioc[})f -@ @V5֮ͧ!Exyab[5z掠ZVP&/zVpPe[󇥥J < Cʘc&]lTrǥ!SpnEk!dqڮ)*Ka|Z$\DşLL)db\u!'.;FPhgs"P'9vb־Q2P)G4jv%%) 6\oaP er;g |+|Jk'%ض+3Ź<ݖu"ᮧs0~P._%IR׃Y}sou9"@-y$q,dlۄFj ;X6{>TzN!rB-CO1;+.O}ܚn_ַ0mҷnKC?8^:8;խ-MZX+)^OSo ҤZgsw\5W[i7x$J$?~e)M*â {zsAҩ;uJA|K@)2>sӻDA@j-BTlc(Z AˣW>-M^`4 &wqs8G]osP͒ο7VKXꥺ`fGO,HAJœb`vU9ʅYw]ɹg}u J`z8߇[\ωNꓱ_gi]l=2|iu*q3Zn灛"O24;kmN}ىb[^./nꠗr;;4Tf򏥥%l!Ix`-7a(hߛϞҏv8ϻPNQ,LG1^LɈ[95>khW'3yr"@xY0^XtR| ^J>1BL|i#xrmneAX=ׅWR @xYſ4\I0:բmFtK!@ @艡RwsC'|)K@ aP@ @ ?@ A!?_cν wwx~s5eƒQCjK[:mO'fJ>s'!֤&>{o8?v+b 3Lz,`m'?R`=y>Bbvm})|ss,%!o1?!6EJN,kTxCY&>e^ɩ\b~MнÚw ]5uO22b.neƃS'@0a'T+-27vxJDPJQo碍5c,26[e YC33q~;뜥ˁ\pɬKʙʯ)_ILJ'ml *gd \Q?G|.;ԚtPk,B:C7qvy2jaomŝZY͚`D,ar&k$leEtAP. 4(t| a!TU5L.q@0L$Z:7G? u|v˷oJ61vMZ *ɺb\N Y}R˙S5f> q)OkW~edLqr8鳭T53)8Rk+]?툹_dOJB`|oXmpN;}w4?xf!!syo}aM_u#N>5q?g qUYȭ̿`SESվnN 'kkٶ\^^^^~ᓇPprUz{y>t lJJ%117^ƄXAXz۾~2 `,wt3;;#E[|cdbml|Šm*>bX,d2Y.ڐ0v 1]YJ"XH.s:mP??m,}K.W.wX^MeۢP4𨕆4 +@xrei[b#y>¤#aIS$pz!] 8i\p&̙)A#M)WެAJȠ/(&/zVx\:0MͼJw1Ҡj`|ND^B甠}:KH|`} _yܙ?r&M|j $Ir AQ*t$F®mPȥV #-"<1ǻLUY!ogÒY"³3LK TU1_VמX4/|3sژv_3> tYB|m֤pj]e4X#LSmc1S)!^nQ$%GzGlksl-{3Nj:e5,^xi,} K" ՇJ| [ӝ?(rvv6GC~*az@ZSPQv o0%bH !%z6Yip ɗӅTq8!&gkn:`v n8D,pF9ٺ,2삏\^;P]!,aM).SQ7q_ϰB$ =X&3FN4Ĭyo%dPK.$I0̻khzSm3݂߅4 +!r"&*4=൶ۚ8ǕX +~KIƒX8G|9N%܋˟$l_8C·Jit 6]>9窦LԞ\8K*k1|3tx6F s?,Jټ{[L_V ;XtauBabzfLicB4o]ssy_l12pIK3Ms(wΔ˽VIKȀ^1{OM'˕3&7~^7Gfe {ao`4Kȶ9;UyO|JЮOJ3ͣc<&[[[OmӃ{۩ [ސWrT!|+<+SSEUB)%o0yn9ܜ[f2(ϖ{๠+م_٪ΟL]kgꅣ^H2(!LY^zz4'`0000xziRxQC~g@/,R0_20000000000Ћ100000000000Ћ100000000000˔lᕄOD>N~2i[}f>]&b!95Dmd"ⲐحOΰޅĜNc[ϧO /ym4|9>o e b7O2i=z^ľą<7Ř#tk-K.>IצzA?OdG ķgev|лQQwi_Ā&a:穃ˮ ޅh0.{1KPus&]!Zf~}KyfMsw/|^ 4RN*Ktnǰr&?B7Ì?(RSCǵd";~3ѥS1khi);T`a\%.7 4 t9=aꜷb$k`vUD0W#BCydc70xw('f;+f)DF7lӌ vI ʙtWjɜO󻙱Mٴ4>yQ86LKbz &MQ^)H!{Z(C9A=v|h4hfŪ(LK\<[bnZj]LXbgqylI`Zmԯ[Ԙ|ZX(nV6ST&l0LGR:&4cTbBeIT(YV@Y4["Tc/n0e{x114(g5N!_ֹU"o.Ky4j̩#UAb; |9H_n^i#3ybTV伩5*'   @~`kj2 7{ytyT hOb9Z4#tMAsk^zD>MQ$b}eS9N-]CzIR7֕%| 6_գgmd7p"Rhcʹڈ {Gd۽gi[H^\F3 X)%^^c5C+r߂ v<\]}n{>+O[TDnH)Ku $˵Z3ṪG.'Q;kk4q2/dS)tY"`”\ q'eͱ,LKnR5b.iXN}OuTsp:p.l]7_U]%16/3;4Og+Ͳ˒kO<|BЮC4QR\tpo IDATpdC|c=D@߬G#+v$V&XmB=ۖj8kkKj/UZLR{nX<. aW3ѕ$RxZ;qcP"ig_d4E׭^T]NN~y=`,fU5]8=v6ff'& @[6m @VC?O wӃ#[|8>%-KndIkӉ1U4 -͘zx'[#"63;Fe9M[(;emTD!`8^\%N(i[6mK*۽^?xRK 0O,ǃuZ^' EvF]}2F!=?q{|ND^BK?o'mܙ?8>??Hr:E% +E Ԩ(aW9R;9e |; %GzGw jTH*HB]۠rlɪe%ȶc&<1Z^2x0}/ڬIYUZfNluWo)pڝuoZiVvUUqkHyq{h!߈|׏idSUU1~i?浝`&WU vR+?a8 K멆$UL&(a@K2mBdVcN[PcUK"j͘QMj1 a(eǷyY曙!7A:O[PuHkP鄭֫%A݂oש7,ab_ă頿MFsg077g6y~=%`tSU+ڻ F*)υ7EIA!B|LX=^ܬ4|)IwTUpgU&p&z76#'TWN1H3\JEUAٛ!NUUbE$}UwD !9{MVOTo6vʗY+GMO]agsl_Jb5_#Us0L Si@a̤-fd2-..͝X?vI ҄*B&ukLv i0p*Wl4M n*YV :DҠ.;εzOb߈MKM*nli n|8]H;׆hҫ!=Iq}!m1o;0^plv8n %WfE'g\34E[x;BXAQf$IUPZ\N$ļ^(FH79C^s]q5V9fLJ R\ ZMuFCN1lDZ]7Ca ,/3aAS3ILfǀym(o>&n#\^;P]=E?O;hVXd6u*@j](+aە"d0hשt;YY ̞VJ:޾O:?2'zy-9vi \.j+ttk=s1 F睌SdSɳrN n7ݱVp hǰw&qV♛u][1[)J8~'c8ex։9^z ]=C6s?^ַC1aLJXi}TJL&cO۩ըP7}P$&sjO.}oi&\^7iwn_ҬlT4 R&|ޘ_xuPfDt1JؚL\=t1v$!^Q'| H v݂!a tk52:+dNjh4@TNܜWD]ͥ-Jȼ&w7O@B1|3txіoL.Yz)ܽ<ׂ=--=y-`}sn#QuzqR,铒ֲKR@FM/yKMv|h0˨M`ǜ?`8d'aIMVwK!vJO ^)'vށ+|9>ygJR]%I* /gy-o7#]_FvN^#_;%U@èMTY{ n_ kq2 50000000000p<q@xx-@~EH&T4B|zfkqs񧁁a?`2qYHUbG/|^ 4RN*Ktnƭ`5{Al͌ DW}BE#ħhj&<N#u8| ޅh0.{1=.2`bbadO7%ϭ GX">̙˿>]O^-3hEe35Y흣LK\8u|>n-ģIlw۰% us-'DӍ)tvb{N{z$DeƥyಋLsLisDkoʠtD:xA\=N:2fBbk1'im2!2d@%YbApPoaߚ"Ov r]v 2)_ Z\\h fb_Om#aţXv#oVN9Lx=Q0!ϊrny/&RG@]5yR;'UZxjeܑv#ѥXJ$.ֽ~X%ٝeO*0Lx?~ѷ %-n7/-{7㰷? LkLmPx::dOw;tj%gT+Nmq:FsbGV76G`X ܝ !P)I!ş]d%mͣo?),a2M؁6k,qs{WżqcpDH&<+jД# wL|;f;YwAHG@yq$fhyK;|q#3r{ܴ-D\Z%$)E-n57 jG|wkk7 [[[[[I:şV/CbZTI;I\IYObtA~~i ?6VoVv:Md K 5ӦP,fjt윀 wCX1_~b ·.X.e MM|nc' x\)f)@4 Ld9@4IR*n]^y-\iim2!3|Lu[qߨ9%|fo'6uHqLhMͤχ`Z-lSnZ-*JZih,gi e,jhիl)WHR8M4z*MɲPoq;mn$\J9I$_ B7aK 0O,ǃ<2eDl6O f l[⬍<78ڄދdw&+ cI:şvOaR#V1)n+2fG!uFiN栝Zn2ʛUU\G㸪ms8BaW3ѕ01ӠMƑ![[rLxwEްŋ}>\\ϦZ2|ׯɄFOOLz-yq|~~}'c,ZLBBXm&Ja_ H0@ =XqE!&]92w$Zf2ElL.Ji'a9lss7@݆*_ Nj14{@9\6Sl |U0\]*2׾*42h"أj$ρOboy!6kRf5vV~/1fZh=IҼN7V Q"?;嘻séT.v9,eofN7/$?E0L~ALQTPAm IQv$iim2!=᠐N AZ 4QNO}RkDd R\"*!>=ES389snnl6-w*pݓݎyneJt+ :\{4iBH}^U*}IWdZ\\;;;N![LSUbkM1Ciq9iiW;6)n6NRgsW dU렢V0*m.ضIxbUU¯ a5IpDlg2vz=H?=噽UN1H3\JECi>ȴ94oJ6 Ѧmn4[`s:FahV@Y ]' 4 MӂH])i>K/$@N+i *:ٝ+3M{Ί*4=൶HECz v;RMXy^ʬ!Lf vyJ5UL0lpn4&"ϓ3-f bN'w]ZlGWJ Re 9ȧ9apY#qw#( oSS7C*=cڝgF4aC^VЮkj"'ê.jtq357[7^ʘϣ]l,DM{]3P\Oίru$܋1AYWUnnpoׯɄFOOLz-yieL.Yz)ܽ<ׂўLԞ\8K*N?aYَ̋aYe({Zu}sT<+z6bb2:%$y^6ȕܙ{qSm\:/NJR&#|W=|1V>7# "c |)< (I AӤ*b ׯFOMLz%y4? x````  (}CWd̑ϖ{4Y?3d R\"$*%>5E385s;"c |)<%/ziEo``````0]^nτ3wmA"+B2S45^C? tALO[q)e OףVw0Hŋ@ڽ]>@"+B>S45^C?a/&ff\L$m$n &Tn=.v11^k˦HлQ!؎׷ABUAv9{=Ҝ `;7x;wwϚ*:G3/B2[Z,78Ѹ*bqľąxw{B8Q;XI0{ImH KٻiLbI {'k:}2,oӾ&'~++a9:Mq=Ϣq#a|0g.b{h4kоy-H};{.ѹ_ d"9=yrϽ]YSGD؉w@L]N}Oq-:-ՙ3.j#=CP;^69f=z ;CJO„ݬ@C9جpؒ=?H{V^1" (*((?oA"+B2S45^C?o+8գ+kYEUosf @:l0LGR:&4Dש⯟inOfW;[UzCh}ׯzwN3.y6_ㆼݳ!z#ȒP,+(z)3I,'|Xx,ܴX?}pC.sif}1h+uScծkc>㰷?W# IDATW1뙱5sG8Z@} un}Hw!G=wtTK.xʷ'M";vpfҸ#u߿х d+,NSe&v #вEMh$pg4j)g štFb+E X!cEݝeO*0Lx?~)ݔQQhE-9õG`='OdJ4_T t}폄evΞFM2zzv> z5P8\RAPg"IES̱O!zdf.w2)_ Z\/@i/GCՍK2'ӡnr~mEn\qktʞWc[+ SkvOaB0)nO6d?ssFXxccH{?|?<78ڄH9gXhWCβ,5c P^ p]ysBDWVb!/vԠZ=rbLp0q^XTL4oڄZ-*JZih rŎ 5ڊDcԎD*2fGy9&v A?T/ r__ B#J/_0n@g@Uo(K61Ex<&w5T7D%zN :1q\UU9r k';8<l%˧ߑHqLhMͤ;t/p-1̨5-*aşX^t|[[rLxwDeLq~3D=ES38jQqD A;fq %Hyj&I jkN2iީ>JR&#|W=|y[h܌HfH 5CD$AUiRx@"+Bn!!>5E0p )e OyVw0Hŋ@ڽ]9@"+B>R4<&i 9z <XwBBUUd&-ݘN/[Zǻgwuշa\1O^_!3=uqo3ٓ/|^ 4RN*KtnGzsp1jp̯ţ|qj뮱D>|3}R'R/ym4|9>oHl"rZo1\}(35Y!ŷzl4&ؾD>N||vo1{oӴ/|zøESݳ&a:穃.21A9.NᨪM:xA\=N:/vc2Q8x-!ڍ$'N&p2w3 DWdBE#hxLڭ 9z <݂ѷ *?UZL_pĿ.ps4ǪGi ?6%?$A;Q&̅iS(`g5y:vNj&A,'AhZ=Sp>$vwrL/K%&>}V͇<.L*"%PȃPvNp2l^ׯɄF?H27B)SS/@i/]XmB-ZG44`s: VB̡&eGvK 0O,ǃD`u#RI5:{ Sax&op\ 74P0HB~7 eiG]әwʿR/+n49(ǬyS81ħ.Rރ [wam aqmrt,ԟC$\KB*{sVl1MT5ŸVIqi]'ʝbg1g847$=󢇢?6/|c~fqP*8ea Pxpl$Gf\ޅ篑$ ' j@Nnvv #]@cIş^aVO2iN)?8;AͺfCfi۩ti@A0:s~'fVM<$BX=^ܬ4z!;Ӊyms:gLfiׯɄF?HifS81ħjQ!=vM.qqUO48N㷄V}:rx2[F.w8Snw/$@N+ C߅뉄La.Sv4=o %bUI$,n2E1gsl]i*sOR+x&0s[]pwi,]?[wTM37's&jL*l?uPMI9sΜ黳3D,pzCqr6nBEPՌzAzRѲ6 I&t d,K]v-f S1I]~w8eYm(7q_0JP Ks45c"qwGf'n~MQ2EfO؇U*Jq h0x, b -,%c떡?),@'{t DWdLE#)R9\Mhy )zώJ(ےf(`'sn!ꗩ| 齸RRwb+w^}X87S9;q)\ZL@9w^X&1:;})]-߮/RqJ8V/Y(TN3Y>UtLDtQCǓD OIR;qĚX"$Y8(yó m@hbBĖ|6Kb"[Hw$PCzWs|}[?O@(ԳC ,$ʞwǃ]p/3 WZibk_fϞ!DLS!k9v:)\tM| ;unbI߭rv:Wݷ?G@E{Ę]gQ w[=7'*tcͿ]Rᘉj.WL[(\4$d R\"c*ICzYqs4 c2M׫˻5DGO 4bCpz}PpKt5y/:| `cFez= ,w~@~EHT!>"uG]hy )2=8'boWfl_)Wݛ`7sx- /o쾔4b'U.>,{؋qA ˳azL$&p(cM1d R\"c*EC$-)81'xf<8m@Sq7b#93p3qoZGk">!!S(4s4<5dRBO50000r6fg,υxw0zvG{ ")_BH!TRd```````W1H/;˦{@~E)RMCĩh*y I)"wa 2X<ԕ["^SZ?h^ng]3&o|am[ IDATkqYd@gBlxyH0{.1糙0S;d 7̛C+oT/ꈯ  ȶJIeWëk󶁀f-qqmlSu}<Ȼwү*7nꎋpן@`+]./ҹǽɰ>țϭ8cݫ2p"@`t-@~EHT.>"uHSTR4NP[ YQ bLnNHr/ &0es;?<#zAGr21Řg7U1id&:_Ob+ '5:ٝ݋Fb 3] +_H8+v̡x.Vup ̇ RhD؉[V`$.Ԛ|\?^~EAoA"+B2"u;d9CC|R>g VR9P3ybyf8}| \_ kn16'G느ۄ[̧r{2X/Q((J"/HͫbWV…V4P1UeV> dU.z 83.ddv'?{''BEf.jZا8taiv+U)x,o$\7Zw)NѨtr,0jHgt1`(:@ a*ߑѕE |Z"uL7GSqcOJl|Ԯ?j@bIk#=ln:fe#p6;[{B KYl&bl? 0-D6?zj?3 @`9r\N E 1$%߾>WgU'ۛWF9n p_UrW 0氊;;1uH_,amevjt֬͜sa,];Q". + R(Xz6 };B.zKr2(׈KtN˸Qh}#,Rwjteywd[uvEq(Dqٯ[*+Nng8I<1٭fpYH??<*bZ}^n (o"uL7GSqcOJ=y׺=,p3@{?s߾쟷&,nas/a8w 2c@|~% @Q J1Q8MSO%VZly9HX^J`~ jwl 1nM&(VoU0 TcRD)mbk8OrҦ}Ś_}yEo61guo뿯vv?1x?1hUJFҾ63֛, ;o,stZi 7nׯɘGSh*y I):ʏu;&F88Nfʳ,iwi) 0 0ILfBy.` -ZXx[@N{&0N #NaUob=8cqFy" NggmQ}X/>av;^S>;c%~GJFAryb'bR:x$ '鴳_7>iH_;g6cE8iFQu|_+˄w8SinkKha)|Z1aj:gCG'{ DW0"ui8d9CC|R³'Ƕ f>ut{X>;,[=1y98Z]fxm5IMڬ@ޟ;7Ӆʧ$)Z8{u|bM,|,e*RL$l(cJ$Θ0Ṳ< PO]}2u3Yns[rԽ1Lb߭o1pP%C+>@H( Fݰpm~Fտ׻j4e3BK*cvNLp=V"Lf +Fhn2K@2vKW bҊu?#c➅.뼍go6:Ms[{aG^/٭tMFDx%5ulL«+' 0IJXFOd3Q/{Ss>xϴm~m5,_=ibħ@n}z]:Tއg$ pP" #cz'&2PS\$͘HEHFȦ45>fCPp̑nEO߮O/*ײ 4QdXl @`f3x:'/JK& cBk R֒gF{K WBwQ ʗK{m6ߟGkRE㒳a1T)NaB=w?Qē 1Igt1`(: 4މPtG{=Ze&Zͯdtq+}PX0߿Djki~q45>fLh@u;'X30xvX;䯕U*HB 4q=[8iZ)^%w#mj+2ww ln:f5%,_޼U%q;ߏJ4O777ye|č Q s!>ywoҴi:E,Og%aVOV.tBwJ>I$F6j5U(ۘ@Mq 3b"!M/fG׌_\n2BuOnr~ғ\eYhArOq1,o$\R>8n]bUXjKr2448ϊ]Sos*] |>x0 Rgq@˷ޒ&f+ǨiE<cuFl(Ai@VDةx'b߳tk}#,R.G\?ヒ_ >b"# M%G2IӟQ)PJ&!hESpV]l*ڼQ$ V bN?Cgq$4x,t6Cٱ%Eqr\ W(ͿI]Uf][i ѥ>/ׯ __1S]h#IȘ)3PN#`p~?8-@8jL4);hYVe> ä# Ýy,p3wh C0i3cN-Ѕ;?i @Y뷝ݟǥM&_ryz Vn/#.^Ϡxn9VZNdS:1յ&>BLA).>1Dk9~; I2 *qPa}Xm$@Nz`%Lf vyR}5gYm1Sk#qvyrs0348v٢%HyrwwSC7Ci q h0E L8~'c8e xV"e}; ҥT>j緎8ç _1cG!uHסA\WRLVp%6_,k%ˇ *fs6 iWd":L .,D\fn8s1k(3I[pn.䲚׸:I7n7\\u1qfœ+l3rYv"IR~yhZj-n㞥@A=O%;x*#>ۻd FO QPK 6.R-QL;1~߯VP'g~i`L .rJ.sRɶhAꖉO/V@vPy^۽A&\-O.*O{fgbWdLhTs">BL$CڑF+(bv 0~0Pg뇿 |>zJUM=M' Y\Է "n&O+8&rΠ?7c&'\?yܿ#4V(N#Șь}4bwȘ&!őEi```< ~{Xq4$R\"#>}Xſ“3 fqԎyAӤ,BL$Mo+9NKg seO,r'E/>G!$Mo+9PI Pg#_200000000000zya#nZ)~q bHC|$EuĥNL !LG-9]pDGt N<g ϫ2~q ;bHC|$E>uĥNvL. wSKW#["͚K^N\H!neh1G|=~߽ඹ%^ 96Ko,,R6}R{6\yg=/Sh[G; Y%YƄn"j+{$[#ԅ䡗HG4:;B|vqN>Bdœt߷m,.0mEx49պM[?7_Zi\f. cVtMܻH羾1+? %z;%K=5Њ9" ;D:)i%G(hEcܘv5`rEl }H@2v~ ̇ npyq_&/Goe+ '5dwv/z劭.T*ǃc7]N;5ȼw)Kz5<-5ғ˩]vͫEn7"{bnj嗀#oT`~bJ__^Gr21Řg7Uyi6'3y<eU cp٠N/Jd>f!x$]ZxII98*)V1iH!QrҎV4fi'J\N &W+js笣oWkHZJ'ŴHL9Gj۫g;o2_~m/^H8͸Эβ՞ʋ=nw[wbyhOb)j0U_JGkRŞJV.S|rw:Knp"@4a@E֋Ŷ + 6h6k0tUpa*ǡT#`&O,9ﵑ2<K4pDH6JbǙKrb5Mɵ\JgDDBE\KVE3 0IaoEQJ{)X3a;ﴅnKzm4J.}\=Rp#C&T K(J *%jNkK3Lo.XxaX] !SHD44#v1 nL;Qvj0"S ,KlW·9o+{ּ["kVVf3M]tShlonn_`i;.R)W?vkNjN1.UT!!$I83jүqߎ!spwa%T{T>vF%DRo8aUc  -מyP. PZe|č%,_:,ak}߯7eDcI[[ϤB1]6[tQ^̡O ѐVFDZMd2)_1iH!bOC(9BiG+ƴ%i+b33v FgI !|,@6}K`Me&]bg:0?G寃g6O*`IS$:LM=Ml nw_ӄ=U_YPL^bU0k8ϊ]Sosׁ*yv׌>q\e:!$LJ M_> k\;;nZYf괩[PoIN Z+WͶx ZQga|5dn)rlK+Œd?6-fϫ]n!*z$NƋ>0uU$Xz≑6 A)Zm;XBUZfhfz$0hZGEP IDATޭF4NA??x D1zŘP"ie[2*&[k9)nnBK#0Lo ֲ-- DWdLE#)pQrԎLĝNL)1kxƙ};fg-_,;T8:{Un@q*PUO}b^=ב}vO(߾ey>:czi.EQ?'2U[A_mV((7_ɲa7Cꭌ @ռ˼ R㩐$ 0|U $ጭ.U>y @AxS,JD͌CծAaN1^-k#rm½ GfǖxqNqhCj7CvZf8MS*]w!" ;I$|QrҎV4fi'J\N ;&VĖ@_3'[/ ;u&*6c1ILvsЛWܽ<9WYEH\]XqJ9^;g0 '~^q,ACGiH_;g]^&~1L.[HsN=Mn'ól]~wX?b %bexƅcYl}q!RvBVMΐhN&La.S괯gq(!hd6ߺhTL`AaJg@$~N@:D,pzc{ݎ嵑8N<N,qv8΄flhM$H+Dz"(1 S~1;[JHѩěXfvLn1X|#>fłvׄF0>7tU)7r?~]oyD;R9{ SYNn3Pׯɘ4GRᐳ4#v1 nL;Zvj0"֏+\A,mJl*g'x:\I~L.oU7g|2ӯϙ_@n>o4]~Wx<86_PߩfjZͻDtl waKb,)QF"M3Y}n!Wk sWLeZy7{&>m2C*Om3|s7ÝE5ybK^%1];O${! &b" hi)G(t,1Dk٩Ċ@"Zrԟ!;w5vNן7n骲=i%D\!ne}& 9-6'\]uj~ɀ{,96K]_1Q ssD>h=txum6пB|vqɷ%70Ҹ,Y]ĬFcBvI_m{MO7bs Oi*acT; ^H8̸ĶtVsqN,wfͥ_ у /MƃZ.2?~'1"b)˃pw DWdLE#)RwtuSV8JPъ,1Dkک䊸fZ-U,SGDN9Lxvw\ j.܏ecB1nJ"3A9~d3Ԙ}|9ξyS| g|%+_/%W<eU ytY˳At|~S9;y4,ǰs$̡x.Vea{ʳ؁-6^4e)s~%@@yNd R\"$c*ICd-]áâN8!OVC{?wΆ?3ybyf8}k 2s|Wxݿۄ[̧rpvx]-^T:e3|rͫbWV…V(]jNkGӝJ{F{K WBW'N3.t녳E/r{L"fa[-"b[ڕ4xQ%J%EI ;l~Gfv_eEL`K:o='(_g.P7#9Qu'?{''BEf.jZ6T Pr-6f(WMB8IS SL >p{{{,Ҫfs0Mt[Jvo\y=P3~w"*ԥjT_Vۤn-,u%f%Ϛ) ZJ'ŴjoI:C$.9vXdyG70W]5DH* [ q#agM)8s;[w{UG`P 4q=[8i8J mjDOۨDn!!SH"44#-#ةɤzFXx7_-~, \`AukD%ZN :Ucs@Ҁ(cIS$:Lnn1xw+~u3|(1G,ƄHk/ pM5Mأ^! XZ%R}aJAbWh5_ 7kuItz}"F\-m@$`';?0Qjnܦ2N*8I'4$*TZl>H:<,46.Zm͹XB8]ڥw*l1{^t+ yD'$"pIbXkvɏ{eB+^0f@y5vȨ `a♁,mu8BI8x0jك람- DWdLE#)pQr};gL}/1 o>en:M6No-(JGܯ. {-^a`W<)f\%"fơDV,=F*ALRe?0L :7ٟ|z}A"2)_1iH!&B](9>Lh"u&Qr};5y޿eאLk#qvyrs=" F<ݾuYB}hp,K-l1u˄w8Si;BXAQr#וCd0h)uڊBq <̳},hjoFB=^L#8nrFS<˒vhPՌ d&2 S4 ^ ,kv"7Rّp0U9ۿ$SYNn3ׯɘ4GRᐳ4#-ةIk}g])z5P~yd h`{S<'O&qa_V\p=AA-ѕxOAN9?[ RSܿdq]D"+B2" ;d<Gɑv qǴ%vȓuF].\wo'n kDSM.mlgxxLU.>,{؋s%r蟭)LeiRX~@"+B<"-Q;D8R4k>q^H7;1@~ET!>"uVQr]C\1Dd5dxz  eY[R|KkX<>Ǖ 윿p&%ݬ*YemOn'f6LT=Lةe2 ̡Q[pG|MeEUʦO*a /$ff\b[l:{[cIp7o8"o.2?2 d<^"jJ1|3io[Rȫ8$QƄn5a}*z_>;j~ܱoZyN6L]f\h_vi 2ay;[De!eS8^]|LDu*]%mkV&]Z_s_+^e@H:1¨N.vJ82)_1iH!NYM(9Ү!_vy&Z*X8ۡuS>%& ̇ v`^6̃9>⹪S09O0ktyy[:(&L0@m6[f81(^0[]T&2s(߷ =6zy@2v1C۽=Ew7w]׋lO,R4p~)k= ɘr{Б73ͯ?0rv;i*w,0;2KvXEi<teeVb~/|}'ጯ) CveZs88#zo$qAxII9oHqHC|$E4#YdkN8!OVC{?wΆ1'Hmg wQ ʗtZn IDATv/z@c l3r\( @zon15;QKY`K(J nwؚ޿*9VgZ\#0vViwm2@sb|k R֒gǷӋJǵaC8fS ;l~:|una6h2`}GT#+#IÈ? >?U~8H_]bf\ gًjOBo>ZKGr05 ャNbCP.́4?Ywߘi|Xp4Q*Pyξ{eUqE֋Ŷ + 6h64E jr#XHX;,2N~N$O*\^?˵^r*b;]hN{^&o# 0ѷˁkd R\"$c*IC4.vȓՐ/e x]!|Qtv67wrjsssszfM8靭 !ӥ,6I16efʞ5aZm^~f*IR2,+C,"4Ϊ,4O777Гr:-vqJ!FV@lW÷cڇDRo8iHXViaL"X&X u߭gRx!rSk|s԰G\JAt[P;qɢ % .lo,#nL)SyxRa "Տv5'qck$Tjʥ9&w}ss.77777.U;QNYCɹiS(fOdVnWLd, &!,r)8s;[w{,=`3Ӛu.qY|h[;ABdrKKK/ާQڰB 8{{ 'E9::%ےFX:lW ^QV5:zf{cݥ ]lVtWSVUza+2>R!jƇS88!!WAq;0yÁO#r6rؔ5JFMp,+2?HU5jw$N_6aSAx@2r߻"DqlڣssQo`2psBb3\d Jl%_o$!_'mgcc':E麎XAzW( 'IjɂlfZIæ qMD!u:\>&ŵ.N!o)J9,)I? xOB&1A]ͥZ(tTfWǰ5(F w҅7|Y&9Ausr~7Aޮ&+gdy+NCV$@ZTڮUeէ"e}| nw^ZZhcGP KV&z2,\!>ak#-" ??-4̧jwy7N!ÎӱGi`]^# ["sH|NaēRq R˕: ƿXi?ѥcάg@7_W ɯdg6a"g R+q MC%NTpƞ$Je[;t:B<6n,<2y /:4s)fhCTlaܬuAsݛb;iQkSv6-"DoxFb=х(;Ֆ|ngr{'6||㤪er"fWb{EVf25Y1] ? ]LO|JnkDl"V|pE߇TG^z;}j[pHoqC-B.젝=5y]6uooWo{~+ Պ3|@[n(Ň扱ټoo )SA((D7(L4:S,ˈ~yhR֙(",C*#~/E"5>l!ys>#68v/㲳nWF4tf2[hJT =X3so}=1aٽD-x>Z?,B&\ʍTr?0Bŕȕ-bnKTX}ohS>%LܮE{ik|CĤMa7eH?f$7Ԟ Õmg>LM~X]: aNPKǐ irv܊B[ IV>JbJ^AKޭfw[9"&)- @ibN7KRjv@xT,u빝=cus{iKlzqS;9nBK7#ITP.%_Jì{`':)@nAr|z 1eQLǂ ZS&676B,Xht!0;*UՇspPG= uwpgXlZUPz\ rU= |>,bvaeeׯ#(uhxOW2dLN!_y:MĊKQ'q=4>l!ŻNC%B!@2x"`).K)!x)vކ|~^ޘvqqnfܸI=ݎ= hwI\!9MƇS88!!ؘ~{U豧+.!x)vbК)҇yي;87;{< )fYy6+>"C*'~Es5z Lwl]\!Zr!n0 3S܉t~/~XEϻ;mHۣVV=sr…(엕 ҇ `ǣ~q j5b &PB'GM،4jqor'ɵ"H׎`+2>S42^C.ĵ\rtG<أOɔ':l(WLz6r5cyw⋿d; Usn9Læ6[o(.<wC_^'7|\\!Zr!΅q&l?~J c"l}Uo~m6 #l6/c',Hn&VIX>7>iyyi!?=Hߒe*F`#-4GWSbo?d쬯չ^bj =<.k_Sd ʙ߿wiXnw"#oV*uI]NRiy,s~XMfꬓLhLSƦQ݇:!&9^$F^OzGkw=kxay>Z՜.ttkuC漓{ЁKk {&fzQ-z]lVtWSVUza+2>S42^C..G hqp$tTvO^}w+g*MYjB] UNgsʕæt?%# jM$baP:bmM]?h\_[_,U ơN!o)J9, Zl|Z9T=ljX1,u|$Q+NG =jMS]jVU)]Q=x"[pȭv+226jҜr:P]Ns58SL,cnKUO,'U:AW9p1 I_V2xM0~QR x:uV|66V} ֪tjFs>{.g_ dgvIvfKpc\w Z=L]ybq?y&MĊːNLz-ǖ(d2Hdnna3` dر#t,B@X4 LfΉp(R,Vi!`C:Br"oBsw ; fЮ AOyl~ѫEϧW?76_8b=Nc+΄UiOJԡe*0nֺrѯUSmW*u8:jh?1hWDpƬ)xQ+B 5|H>ސ0{r} bY3L/4КٝtzL .t*u9T;'Dc31'힚 TJ-Ḑ*Wc+>>S42^C.۞xܧ,Qi}ƈ:űwsf=F9񞿩 D[l*7<^Z/.ޖJWX{w?2YL#g񪰺ټoo \2{nʄ A\lJOaާܸz伎X<{M{XgDa?ybSc v\lba/L:6nN~7&bWeHE}Ghd&\f;G}EtMݗi RǡZnmvyyE.Co%MH{i"'2ŇgFR-Fm&2;^KPm)@4ssݖNu,s<9zB7ʉ-EB RxKz/xM{JEsJU=VIR1{6}vB5083}ȨjRFᯯgH5aQGid)D1|5;̟Z; `Х`#ÜɃ W*K|ښ[ZH~K2ُ!NxRj>oXQ+@;x(MĊːNLz-?ZVG} YۏN>uR i[;I5BŕW׳]EH nD!?.GI*[!̼jd+c#RzK/z9n%5>!bŦ**8+$}Jt]=mWI\ЂOEHě b5&vP.%]QNo+ ǝ񝖀/w]Q[>nW*q).@nAr|SSxb3N"qQ>,K9^2zI+] Jrj \_F^W^sĠO.\,A+++IJ&S̞ "Ww~&bW}HE}Ghd&\w[YΣsd\ԕ^>osӫۊ(@GP։woL0eA!-7nRObY]~3ms.6+>",C*#>:E#38By0{Ĭ4$ܛ0!C$ЌلIRuDIWD \!љJr!y4w vϖ7:"TYtx9y&0GZu27[L&61?].2 g6 73mA4؝CqR:a'}fYƌMv'&wML}hXm,)tt&_['0n:hUas|Ѹlj\h͔Ul}=zunH{Sz5H'ޅ݅ti'GE|фw-٦ѷ3*F!N:ǡkCg)oWKsfe4;;k6/C_ͻt7(dY[`F#XɠYSrj[8c0A;,Hń$< p1~}Iop^)^XuUOҩ|K3g#!'wʙqZ;׵ DQ燭*@ naZgffvru۷ono߾o w G2&P\*]M*RXcl6L\N I.iN!o)֪555flRԔ5Mj<#EҬqZTMmתu7dMOĜA7DŽ7{>2<} @E麎Xau]+wӄlO'K_ke*6f׏7\kbuC&D"sss 3~@ g(R,nԷ5UPu]4U5fCTlaܬuAB<6nph͉/ߵ+O9˚azc{ov_PS웞M^v|ro՝ӱGiPI~ڙ_=ӎ o6_8b=!\G;90z4MC%Ҵ˘|yrD"Va{Kr#I'B&3yInq ؜ь YtС@,==,3}|9ywWe.EVWg?wCpY]:|ʫ;4`592v#B ޸|l l|o#EeiX]^.'k6/:۱%16̞.>3+o_DX@ G+C$)pi`bn793Y'YMveX1G8ruo}t;m˛(uyzn3%WrMH{i>aLқG2V/A!İ,©@J ;xB>T.}DPM[#]ׄkGY~/lFm&2;^K2Pn@ ^9lI+v %4x1!Ufѥދ+y9!YtGe܇ RZdkBcu>L403ݞvvk};m3˹?g຋B&\ʍTr?0'M:GkdR)zb6qpi ǝL1v:w]#@xPRzK/z9>S.% A|v?3*5˩b~/m~t)zh X, @ 4<3ɂ @Y'޽1x0}s=şۥ' dњԇOm+? /&^<@ @~@ @ ?@ A?j2*]IENDB`btop-1.2.3/Img/normal.png000066400000000000000000005011171420276253000152010ustar00rootroot00000000000000PNG  IHDRt sBITOtEXtSoftwaremate-screenshotȖJ IDATxw\璐 Cd P,P\ nQڡvj*UT8D d/2 dma>OrOrOjdZh4ښ~c^f3p[zڝA|Bzs)_S37OVV>?bovqlLa1dL}Fmn"|jB!󈽛m>~Fe _/Ia.3Jxy_ۏeK<[= N:Bs (Tp۪[/Ϟy"Q>s">m v5>-#3mvlӎiEBmHV7Bu#^۹t΃ kŀ|+3$U$O5)t4ڠo չnLγz(7.>/wl!Jd>/m&Hl:8??OkR>} Zn5I333{u! /\:Bs (Tx<0-0 cZrgUznj:OfZA%(@$B ,z[CHGw"Ӫ]^>ܼMLRPsojP\bdAK^L(;s*mmAhmQM _t@?a!i-^0g4.x>KꩤRjbJUr!i~mƢ :VK{$T))7(ލ {dDEE*agw%Imab`sz%u}8^U&04(f_ʎ_} "Pa@\c3U@!dA׉JPީ9~BT*ʿVRk\l.'trs9YKGg^.㞾 W?Z]A!O?dѺ.0x񂾴cŗLf+@nr())0m™݇X6{ ׭kQZO T'B5;}}}[WxQGн7̚de9n/Gz3wROp|$GG/!IwX̄)^rYmDOFISeibGOx}/@,QɭxrǨ~ښ2n&ii׮'s rCR=wv~a˪@wëހĹC'8} e FO/_>kY<B&X(<1֏m >Of4dT<;+@wWEς։B3 @Ω?$I8wvqKX^ݶtM܆͢[ASP: ^YV{\\Huc]{4rf BP O!𙜬ɿ?B!j[ mtDB!P!B!E5B!@!B5!Bd2aμI}:|Xz!B꡽ߺ6mB׾x{v^Ä_]nPnwvFKzsoOJx*3[FvS%9gƄ-t}ow-WB!P/[Rjv~ϞՋt;ٜK]G"2uvgz?RɟOzM }JDq3o]ü\YiV>LYZ\\-wb }6*<@걗֜ngas~@!th߾=$m|~kB@^#ƌay};/]sDtyY^;uP,u3t[?Q'Cͪ79\ }W%C׽~ZNX{Sihh&xB!>&CCF:ZQt{ 4W㷳d` p7!AR瘝gn7 |hÚYOL I=@px<~1kjÇiAǙ`dB wiGda-#Ͳ&nſ3OK"RkI=hy篕; WD͕VHTPPЩS'B!Z2,oIH/y/OPmޤ4=㗥A]~=`цgo|i9%A_Dd۰x߂ \#g~y[l݉y\jm_uj~b ߯HXsAʘ0Y 9=D"":E}B!j;;;'(޲RH]]nr@B!~@!B) B!~@!jb}}:&RmotX? B!B!BHQX? BoOD_9Ǯ}H_]]GCz uwc߼gV|5 0 g~ tg:#y,:x@K_׽&}ѡ9c})t5OZx/+W/qJϏC!R Rc"/Wv3y){2Ǵ_m |9j5WuRt5F%`2%c9C^LNc`S/td FxL6iOc6}杼nDeFϔyZg935t[b~c4%q|XcB!"6.L(ػ5Tt;٭'"Yc/ up|.XuR 4F2P`S뇪JX'd R22R){|y(tOO0ʐ;,?<Ʊ]kgsNdnU7խNͭfP&Ϝ`)>4!B*b2 7ΉgT{mHؚ[V'Hk+TF$mr̫(*rrHp=WkKawjhkH5K].`fVd `X&Ņ=U)58B! 8)))e kooDŹ7F׀1e;dP}Ikc̦Nkc+TRhk:IB {s_̵ķWEǓK"IYB@@SPfP<*Zɣeãɷo7OY#:٨3YQasDń ]&*Eqqʽu%NX&L(=!d^D͈=tI&U!BHE8.0Ʒ. SYˑX,G(++kxݨLI5e2 L5;Gd G&O8ˣ+bզ C 5]}y>!B*-=L+T&ܽefW$LJr$_L&}D&o8_+'[*HNٞ|)t$HP(A">֡h׵i| M=I$N7e=L"<}dĄ#ŦCG&F)7AB!^A2)d^!*%O\ň7)By ui^vEKVҥ좌S Qώ(`~Ir_]{;=ν&ޛ"}g|ÑdnVܑMG:E5>mB! ;;Ugiƒ{=bUE7 rj%B!a8ދA]h!B^P;$1&qK$T#:M*█e-l VB!BTPȯ~/hK!B!EaB!R!B!E!B*8)`(n^╰=aw@z Z6PwlgpdZToNˇٱH?ODΑߺ{U`#NY2'5nsOtԔԼ'84nZ?Hu e@^b!dr2B!ax-ݱ`~˹/'oZ1Lˌ}ƌ 8&jL[$kF2b 6UP{zxm]/pЙLTl NEڑ.r.J).~=fL {L\͘irB!"6.L(޻5٭'zyyf} ^㗥VSQ;wL:WX%W&eء$6@F&SdH@noԮ4Ugr%6gSn[[Q @upK>\Z%`9}WHqB!Lr6@sEl/{]"1}סPuꋩ)&8s(бc#m9U^99$݁ǞϨ֖ *pt1ðq[L(>;w+W/:MtI+wĴ~(zznjTķ )ʺAYDP%`B!hh B!L4*hHN_>2IN%ϳf֐lw晬h"bBք?:WPmܬr/~] P58fpD/"fD.}5A"ɤUOxB!Yk{k:Ԇp8\& `<28"h;SRBi׮@3lgX\\/,Jf6?h԰nj󵺁TA\}0tF; ;á1t4_/^fG!B*Fֲ1fe:QI \Uu&e[WOޟ/\' &\<՚03;uYֹ nf^Ggm>_  D$ E@W"xC1/|߼rDt<}dĄ#ŦCG&F)WI!RLŋK^{Wj=A$}=B<] 7%hgډrT>Ðg%m9|ȹ+Tf.tx[.&\[]lmߙ-pd!wdӑdNkNos7B!ԆьWu*H٥LO>UIb/ܼ~ײeW_auuשxv;jiZ^XfkU{=E@!CaaivBMvC{XkmIp5bt뎮Zڅ B)0У|k>&A ?,C/ NK!hFI޵k3u. S`ã^wt"ьi4["iRD,k`k*vB!ޔ5Hcy-T >ljr]B~uʂ"/!B!B!BHQX? BOXw@!UB!Pc>-RթCqRQ. (ܼ+a{Is|7.kQYzƉgxq6u|'חc4#T;eNk@:p1ek$a=vF¬?l^( )'+BH1s Hl@mkI1+=~y-'RST_Qx{zf^c:^[2لs#\wY t&SUa#{̦p ޾4DOv#*0 t$mV.(}Ӻ8kY˦Ȑ _)R/!B_WVYDm3 n ;w_@wvIxIhbFM$L!I G=STRMI|[ }&nխNͭL:8%DL.Ϝdx!R 6DSӎ:VLr6@sE/@{Wg׆B*g_  D$ E@u(umEFb._0SCno2ɦMYӋ.x$uɈ ]GMM*Qj|< "B$jڞ_M! D.1{|0E/3N[솬XM; Qqvꞈ'@΍gG^}Pm}ue+cO^8Қzo΋ GYqG6IVg:E5>mB 4F޿*%cggҢC0=}=f\{T+7XK- b 6qZ+m,9C+,ZU^}#j`k F)v!b4t]xXRUBAq>ހ*?H}Wn] 6qZڅ B)oO'*^& !q29A6Kr6cx&S i<"ssSU@F&SdH@noԮ4Ugr%6gSn[[Q @upK>\Z%`9}WHB!S?~[kU  PP( Ϊ$* (BH1ǃ2Uj4 4e%fQ߆M}O-l"qUqi<Ϩ@٨3YQasDń Ui ۸Y^Μ+[sp~/4^D͈]&{$L\? BѲ{_  D$ E@W"xC1/|߼rDn<}dĄ#ŦCG&F(5>!b [$T ~u Uv0B^A2)d^!`@U0K5Wxkݐ+i!*N|ȹ+ѥ{k%܄k-A"}g|ÑdnVܑMGe5^}PTF!VUZдM7[CfggҢC-%X:IJ^sֺn_Poч/eg_AҾl> ]ܠgҴ^icɽZa֪"zQ[5B/F.^;RU9ۊPF34$_j{ˡXkơP{*Эk ]ܠji6bPjZ(.B}( C++wsEPu4 ']\ T*E&oޛjGд Ǔ,C/⊵Y: )+ܠu *NAy(͘F%&qKIJ \Pb!Px%sX? ӶggLSMύûێoOD_9Ǯ}Hݿ˱W;K @ h<&vMk~5ĝm]8k;v"C~I>!99:c})t5?E^sW^<쵕RcőϩmbMIԑwsuu5' YpGr{шçp8 ?)y|u =6ېkLkKFz9t+d1{̦p ޾4DOߍО/fYb @3eůnj zu=[b~ot?+36MUn`<BHI*xJT.EAjlϙvlo-qqfBaޭa+n=@d=tRvDTpCU6* KI=ٱ.%Y;}L$J#pߞ])o_;4M$w:0+[[Q @upK>\Z%`9}WH?@ L_4We.MK#>]WwUy .g8gC7G> 4X]mx)ieSmT|U~_ ZVy{˱FQW+'(>'3ǰ;J>Tܤ!z[UU`fVtz' 6 b O$}>!Jr]cyvj{^@SP_R![7ZCb훈o^-Uuvj.5;8:8XF6hLVTXE1!k->w7܋_Wԝs%Ix&d_zQrD/"fD{{7$*W=%RcC3^ ^hzPܖlq-ٌ7S#5XK2b 8Nbt?g;Y;8Iu@&g'u皝^ ֿ S^:S}2s%d*.CchT W_80N!̢K/S] ,4=@HInOE͸ ߱/f7!Mqǒdy`jūۂDuH@49/7ށI\jIjfoMi>p,Xg2HI" " H:~-ވ$9t?Ob=Q)ZMYK.x$iĄtrĨ>ħ%JO;ܡ&l BO H HeT|F$'ۢf\gޱ'f7M<`hx.%%]C}{u' ]=rfb5zPYw=o .+6Np[u׭ӍG;ͮ%gϜ~w4ȝsJcwRYb4\$[kR2;>i,ekHfŝػ+A@Pj:ۧ=BEӲv2[=b)Quȯ̪eId^<-T{9ْvZ'l֜C֮]ׯ_/H~?8q, e)D#NXYC|2nP6wRͻhxשһ7R>8qƑ"<{ή{z%؟ǣf[n.eu(/m`#HNLϬ~J2Hz]н;"_xY/!Zl Ҟ 5  [mim;uw6f\ji6bPjZ(.pBcE8`2"X[ɐU2 1:i^dɐUq29AvhxשZ$1&qK$T#:M*█e-l B!Ч M/!B!B!BHQK!n<{u\!>Ls7)1&mj%B-V {@֡z` ʥ8WbcNk~:H+A_)iZ6jL a>|sRuvG/X1tp67c5oAu# s:|uGQ-pܕO8{meBH͐:$vi)[Ba鱤\PGO%R 1:޻?ĶԘ{|b1~꧘nKvW%'B-bBc^lNmp¡RZ;g>pb3RGTbaUkJZ\&F~^2+~z`UϮ7m>!1^,Z^fBM@w3ӕOY/RsGąU2 LAF&SdH@niE?=||ul5L[W*;5 @upK^\*H?siNFZ8 !Ԙ[.LFSW/4=@HInOE͸ l?qÛāw,ɥ? .g8gC_-,Bubjk'Μ{fII[`urrHժxcX}u V]&& ={t#@M72;Yl%qk:- RkU)DPr_P&j5"%-?5:[4(Ƕt{Љhh 9ˏ0ǧ"};?4I;i'- -EVZ}٨3YQᏜp. Y\CqwC|;@Ds_  D$ E@Wb _XsyyÉ& y=eDI>LՇU8 !A"@&>T^Ft}λloɤ>b<=poNd^B,3fAbm8+_;~HG U;0V*l:4tmsb S.& zY?Z!gzYq'xZ-E/aBjLc>UWA }ջsuu5g͋٧*i_l0¤}Y`_іu4{So_yāhzWU$%9 +m,9C+,ZU^}#j`k? PeyHCjnkwn;j4mSn]P7x˺ZC{Xk6} &-ꅏ څ Bj`2"Q9sCf0ێ\ 3X _k3u2~W( eEд Ǔ4q LRdbHvAROK"ьi4["iRD,k`kB!ZOP? h8 !B!Ԝ~@!B) |9;Ŏn><{p!>cX? Kn9My pס8WbcNk~#Xz?_$՝Cq֭u00 g~ tg}{E/X1t$=c{/L2:a=vїbNo\<r⩟gjcQ!US˯ʹC]pS!8l] kI1+=~yy߳T^dc7q5%#M87{@g2Pթ*c6Sy' zQYQ%&ir@3eYcL o&떘X6}El7MIx!>_"6;?S)UuB*+ ,}])]ř {޻T;$:FH?YLQPT0غSL$J#pߞ]")o_;4M$w:0+[[Q @upK>\Z%`9}WH~@!rXu.`$:sxjXL& Px~9@bbb.9n h$rUomldש.F$rlQ!#@ՙ*V~g_mDS_CuknҐQ=쭪*Ut3+c: `XqײoN)58_B!#⼈@eNN|\UH)DM @Ҳ]mŨs# ud IDAT'Mg ֯Tr) 0vXzgߵ\uD&tI+ AqӕN2~̳TAAaQ3EY7~ :$B!TfFeV/\jAm,S\P]I@+/&v C\:v *M*o!QEg¢ ( YHMni۸Y^+NN3 ҋz z4#kI$T /!BѲ{\o`##"=m )a6إXX%`0*l`z9l?05쵿|naca9>' cP?Wa) ~HP[Hޫp84F }+5(BH4,t4TjCy=}WUwᣇrm\.g'plX&_y<՛F1JGu. ,K#ABڔiEUHsAP%B!Ps!B!(B!Ѝ',;UKy *! Qc>-RթCqRQ. (ܼ+a{IAàUsRk{d@y5M~ZZE?/0u|'חc:F"T;eNk.)yOp"., hXݴ~0jx,/;C '+."d e!bx[F:5PkZc s_N޴b>78| -7{KyN^zN)lgvZ`5%#w:•@g2[/*c6SP'"H w} ϔyZ3&q`n|&f4V~@!#(l?g&FvxR֓Luex\W(,O 5xE,ZBH?YLQPT0غSL$J#pߞ]iT)KlϦ$f7L[W*;52=3OWWd}2JN?sRC%BH1Lې}XGOM;ZTP[b2 7ΉgTkKLj܉.DY7÷:S5);XNyPA2Q[ I۶* ciam1ΠRuV]&&X$b~ƛI$l6ʘ̴0a,K:F8)Aw*Zpswm_2ARU)}5aՒJNٻ@?طY**ZJ_ҾhѢ~奢T+U{HZR#-"X cfR09s=3s=e@#ٶzy~kܢS!:}# ח RI@*O3O^Yaq-D*~e$BH<oAte!S( _I.Cq尌;CГ ~uKRtg`=!P4yS 8lsn:%-xXd5WnIf IfU\ڋ $Bd'!޲݆ɷt'b((*hpsr1XuKz|}Co}Ux rl_MI=o}`$6sX]FM^Ԥ1Ω3YN~B/i/ \biU/H84E]B0lڊDM'ŋ cVU6=ڥoϒ6[$2p@"q2U/GBoUg^`iVUmj\V+_LZ9nZXJ%*w!xX c@էw]b;(1xmzn u薝KR'PmtNv͗;\9lO۾{+mϡ!en"uG5JI3$Hr$؅B֬%]?;%ѣQbbbh;zv{b]^4v%m0`E{*z:ʼ4f2??Kq,i5]:2t5(˼,an? B)٦S{HwkHV`b%n84PdԔI@QGŏe~1(GO3%x! %L(rr<#l #l`Ht9 R)OHIQJ$g"5t>$/ %W|N1\~;@JoQYO.xワ яR!;}\ri븣ھd09)vyw373 Z3~ϖ2ϟnMS^GvWJֶvzh+rNԿʹq>tQձY@ee׼0ܝ0~@:IcE+g:`"'^9?o&mXsqu*h6ϊz8I%BH<naQ#Хzanٛ"`Uɗ:W3[:ζ- J~YY9 P ۬ؿgv7zK*.*3j^j<|WrURH,+O (.rkWMM5$*? B5ܿmj(*(dB<4GTTx? C wWMsזssgztsoϨ ;ٌ*YPu M@S0jfdGJH3h2+KKZ{SdM ? Bᗽz"qQXҒA9OT{t'\7Lsgۂ)CϞ-q۹:} k'JO{Lif^Y W9x-D+#1B!$ !_fK~bҪ:ҕ'S̭,?>:ay2&6/7|d?x;˵Ips2yw ^cK̛ԾJ(#8|SY >po}@p2Y(lB!ZUi…kgFf%ϞFPAY(ШR`nΞoN|&Y6jz&qN6YrB̀/uŢӪ^qhD巙 !BgE1+*{ O㫯S]5+xJ"J'.UgI-}p8\ I uu8z.Χ<?@vV|'b/V-?=r|B!P )]v,Jiv̅Ճ>qnsHDzkWbs];\9lO3¶ g`vPykSMY^ڻn^ >Uq\,$FaجJ<st{5$A=G-.S;f>!SΘkI%m˜B!PBrҍ𰠳)U%X;7[ԷYvŁ!a!W[5p\R3ˮU ?Mӷsl3.zY 𹢲(ߘ욣͊^ >ޔ@ J?-5/7ɩCOi IC/i˜pB!Ԫ內Ø }GnZЛ@=m7[g_pn +r=j+xlj5.Ә< UI}ti_6ReiIKb+u0VT2 Xԅm嫩eNw<zy 4ATm=$˒݌J/k| K!BnQ^UD{A_Y^<0sqb^fuoz]:Gz]HOԷRR^ѻřO*6YjHw4>:ZyFb H ;zgj -rK+?l(Nx4.d\):]d^K's. /I_:M;OZ^CjEB!PBQ|~<FDc h<.INUK~ـΖZ߾BTp@::Z}v_{}*lbUA.6^ʯ9Q(4jYP(pQ\Ǩ͜:Pee;nz.鐚t%BV)c4 ,V~dKN%_r --پ0w"#3xoSџkͺ=+WT}'[W%ӋxGQc*ݯRs.^B!ԒhmdcYJW of-//\=z]|߹^za㿑*BҲ˛ۉկ_,4!E<DGg3{GAst]{|SBm(H.Q+Y}cԵv=N~YҢ*I+ѽV jGM= ~d-$~kCQP2rǓFDb0pBБF!Bbddp>Zan/!B!ąB!pB!B!BB!BH\8@!B?A|PC'~\aݴŠg1 }z KoN,Vz~j2ĘB.!B:~_߼U;Et杠;,bWfcBv[ Zf(yYHX!Q8B!BIjCJi5DESSm(6dܴ>ܲrl ZWvKN\6 #1υ"su-iK']:B!B:б)"Z\CkEg@(t>H(Ur?{WȃGo+?Wj> ~(d |ql4@!aAt9:T䩐E.B!|>Fqܖh4>`?>P($4g@6!On'DUϺvRPd :j+VN)yee63R*." E ըtA BS=~T+Ȭ-]!BQ\\'6/x@N*..n0g "I)/T[^hPF4nUq<3[d2Y(Q0'܌Rd/ g\K8B!P#竩TD,+//B!D?dqlKs餇  Ҿ!eTN Qfi\.gETEJA[[WQz=#B5P(gP$4uJ{}IWZ:BQ*T5)d C@OӧB)):.kYI2kMF*YYJFj *MNHY$ PlekHaG!BW&vj\BC캰Rb2;դ4{ ߟ/Mz/ji^Z}-FN?e:-NԼDk!I'Lyio01Wk~QMA]EǛ<=CUXeM^SIGh=rVbjKGZgS9,^K!;˔֎jve8鱯mikt? '9 =d_S1rߋ+sOv v- \@N8+\2hLQz#5[4vDetSnjC^~WZ,YByMNg_Eo`Has>,ГN]d23n:6KvV 僴>.SS5ʉ~NWmӋ5o)qrՐ15gŭI$텽Go> ҽqNq㻒bl=ftT`>w(]bʃ7|Լ.T̨5(ǙL&3q Rg_?=ݘ_ލ}vqk?Qj(v|H|d& {'1NF'/ŋ k/C( EkMZ=ҫځuڞ'YL&3+3< +G'"N%uWc'?By9mJ}YirL6hiVeT|)Բswe)Y 'C鄻 |?=@D& !8j edFk$|N휤n~k~Sݹ-cLk_x6yxy9gPry77+LxM^L>jIjjLr<ČSsm֐FR۷aVɞG7Uttts?qi O#th/E.!Kf>{Fx7ne^puuu @qw;3fpslyU^'~JE삣c8Pz35'6t_q:ڄ;UP_=z[FN۶Wz-s"w!I6GKWy"Ko 3tn4zVaslHKβrb!k龉N%u[2JBdli[cW' ?tQQU-)![X'lG^$Fv25SGW= ll2fɹ_sa]Y­sO UVI,N>I_" ^f;G҄D?3ݳ\{<3,1GV{[ CoA1G~~koP)q6 |]<t)z֚yBK|ٱiz;wۼH2>?_ LtܹsOs:)Aѭl<HUQxc>]bZMϏS S @XWA@nv N wlmP۩*Aa9+8/0 @OO@.*$ MuuM)o$<D?eJ(t^ƒ;%ilөTKFb}tj#*b.XXMMMw.}v8_(A4^T3PP;kII5I$ _(lxn &uV۽dϋ}/y6ڶnF<.V{-^QMK{8` BԵk鴆BqW\[K*N'D5 d q pZIFɼvhqsy.dCZRt).59^P(@Y{qQh6t\.''U鯐\+@"@{!]T4iyrș(qHՇl` L4#L^@l .,:Ͱ4eKf#< @}`5cэGj]n-YQPP @Ѩ5z:(W(qOe ͊NRnn۷<'wRT ))aY1c&&qp8\뫇xZP\Gq7ԣ_P1÷S(IP5ѭ/1k9z ӎ ̡%-wI%q_[lmGT_#CRFU_5KܼSL޲YUVDddQyy+ixwl%Ga @h"HK>8K{,Lh`cT pU'|>FTV@F'zq|֎-f5|BDD4@ZKpXMp}۞&֎&;L_uBJV{n8%MyOC ?槬,#oid|Rł=6pRDGWWԯuBq&j.͡}yBC[75jz;S7l J22cLLl:w%͛/@w TTj7Uj@P2`y>p_K (˫yt4I"sYjBBgIdXK+%%;覭}1&Co!٠+ @ŋ-SQV;gaU3 )ZrC{Nߡy댉|ηm-Q2P<q%%:d39z,Z >aiqq,?b{DKf\|~f1XkR?W$~eL6dvVOgDw#ܩe$Sd+iukXZ)o9wwϩ$_ճH}9qfu4ro z}#2AbmεZq7lWJl6t08+We$Q?FuLTDB33`5rŶ1{WhZH‡t!$A;ZY?ҋW+tB8otvl@[p1*-Ys;K- T0!dCLY5QOYXtk>\+(L=Ԝ?6؟w #ʉ~*NGU]Ir~*[}QTyYO.u6Apaoorsl>7= ;N~.ӏChtg *KJ:h GSM#Ⱦkxו펿yUC7./U9Uvy,X=*,oY)Sz$?OJtlT!rXD]:`{W/}DV?qNm"܎=8Щ簮A󏨏7xD-rŠ[I݌MR/gW˥;`0s+~}/S Qĵq6ecn.˝ y[k6kJ7p{x̺|GsV'O)#Bz2pNKu|/jzv>M^:V?jeYI/OaTlOÈ0w+щCNΪz)ےW7VqCԝ)o:JEt#bR?$sFY:[%;o{`M$9U{ފKMKz|zq= "5Go?K!mS"^_6-2O:d2S1[whg͘)^E]v&B'1\{ 1AuiC#Lfs♉{~ܓƾY:x%G,5[dˁZ‡C^tcYw@A]!Q˦}0lzާ:oTC.I}nKۯt\{5AG׎.g}5S\6]Ygn$#(<?^ Ta$-ϿdFL%;'O-(FՈU]kPTR&ck)н" lM0ߢglZy'$ Pϕ%KB%tcLdF@?d2;m@Cb>|H|d&K{x:uw1.}q횢4c[sW~zI[._uyeNgMgʑR>}uֱuуA`o'|vĤIM @>%hmZ;׮q~e%0̝:|S%L ip pyI$텽Go> ҽqb%xwy5nwx-ےǍJ=КfӭǏ$7h_~\d8Е&bhnVm|u߹% sra2㼬8"95)i׌D闾{\jTQq}v1Dũg_Eo`H 6%uj#1|;0*Akvz-% uѓ3b2j_{@I 5b2(cH̬[}| [KV>[ǾJSF}#Sͦ8Bqw;3fpslyd C'\]]]7'hܶm+?mv;w72zeCbH`xuEw(ü?>6`eKSEE$9 >}9suu=éDck w=!{G!wNSF۶e_;{cU%"X6ﱸ_ a"pΖCϥo2Nͱ5hZCKUTT(NFy/ׁj<m۷aVɞG7Utttϙq/=>yYNqQOj||'o?nm'yާO% IW.4ooʱ;t|ull?̓jDuR!*c/  Yx8NM'mp\hEc64@-{>re%E"~.PQzfq**hu4PTTFL&kW3%EU5r4)FߏXدC-_ ~P52NfJPt#XV cיnFsBrJ'R>mSNֽvE2cvG3\:=~D%A@ ˏ4QrC}a [i{Jt4[(i߆1׎6 u=-ےR4tv*Х$)tfXJ=8gD}E[c?&W쮜0&QATe%9tY*khli}!WZ}qH፝"e,Lg'S3(vQva[w(#:kҁZMU1~KPy2w3lFBR.*N],T +ÏEv}20InK`Z4y}SU-V)Rt).\t:qNR.ˇtQaФI$|lh>#Ef젯 P5+Wfx V "E^#|~=!d]^|vIn?A \#hddTϻ?rybyReOm%r Lt6jچӷ>ɩ͍L/Y q ee? U~ )+%ꎍ~'XBjwD!W& ҵk# q-y;qu{qrLa_w2yΗK6 sR4|z.1#=Z53-a^t3{c_d8N))co@JdedFx8ћ8/Nr\Tqqwޢ^Zc}ކi[ֺըÂSﻐN"7:#<WPHns?M1pr4*P|2FGiroas!l]/1gv;^xݖ1} e3D6ɤf =dWĩYZGۗP_: ?(+3&ew]uL3癛@cnIIj_rXSF<8t\sYG umM{ӫat|AOٯ炸EGK6/} ?P(d S($JqTՒZ򗂜6P#& $-M n Q}m0g?|%Q8b{ 9Q t>E^:!wmtIsV ɩT IMؼ\Zao +N;^.788qp iU~NqP5aU cY @UT)>o؞a-MrfgS1#dT@H 8P!2g<(7~]HceBo{SծT%f#@(Yz#F 2yOmf 9r7woտ*^4:7+ M@䵷.GG/lվsV%Q"Jdͳ~HI /Z%',kɑv9V~$̙\:k\6P 2+5M;R UM5/ IDATkUSw;/μ#:U0/xZwUW+OYض3O]! >@q?kMuq/bY0llyk\uuNۊh]+2pw]*kΘi*GA֣GU:ɓl[.^qdΰ?*'+@NN-6ebQ6c~rX'a_O7sڄ #j6}{nƟg_/ ˎjla7o rѡ6OquIEރ0=+@\p| !:`)QLSX 3þj<`.Wwer]߶3jpиQ+;RE嬣Ǽ-?}qy|4zŗ"ԇ-}u9ߧl4)v ;gvz=aRZK#h ]QsX?31D! 6Θ???1`;".^hwҮVf>{:&qܪ^_=r#4?dK?BոW#lxԒ=lQu=x]?s|3Mu>@ =t})&) {zP5jljúϼH0O|Iq(Evʍ-n%3O-ԍ6Ogg?8dsXנGGQtwZurŧ vٓps\p$;_[02Npfgr񠳭F]?|ᣈ>|xlf{0׃Cn_?{E; :tډ\jKMwv/کK-qo¾7 NJKTN|E~aaV2;wnmR3~NXٽ%:_:jݖyj;k;8|QGڸoGרޘv=zP%isܓ9]xe^ZZ6+s]ohNr_a\n+ ']iq &(!ڀ 6Y4vbg/oN6ui;&GIsٳ5|"%N5=B1~Ҫe~=gwj?YJ6"\g#kT?.y:YzmQ˦՛/i2ݧo?~/,yVbd-. qۆ1zyjjj"WRR,-V(r" {Y[[/JEQuJOz_JSTT,-nl%%JJJsU-h9.lBu|cS:ʓA(~n"t9zwт wnjt8@m% f3rR&A;)=CeInFQgoA'բ"|peͼKibr"^}9f8g\Ҫ3 z{,Nzo5jvu r11:<8qkyִzV4$2M^UU}y{tUn}Pv/,#%H(9?b.@zjv~e0thâ4n eDzǖ|V~x&0Ht:ԻQfr$Rw9"$ (THCTj%ٶzy~kܢT:B:yO=mW>/aWN7N|(xp*D -p@r @I @n1YډL"LQn ?D-NJtC>pq)McG2S-S uI;GzYqJV#[ ?p o1΍UF.90&՛Ook$6Bv_z *9 R>*@~˩$?Uno@zv)Gf=IR]o'fޅ5Z asb`sC=$5Bُz|oYݽ1|BxH+'L+@(R;>Y;'D!3>/,gU"N퐣[d-V pTҧdiu0M^omIud5z-Z)]X{S⏸fNNnntgԟSoj$q~9˫^Wm2-;7(aT߷ ##*a']۾ͦ6咫</㒋鏵dJuӷХ"Qlv$&&6V{]|^/KI} jTǠ:}ue[#Ů:\/n nё䆿6|ښWat|b kTFq3%hD#S̷oe[F6,wq1u}մO$( !ѓ5YǚGTEɾ=-ImiӾ1"(RUJ1mkFZJcO}u}6:^l,ˠQT`/ YxZE 3߼v.P1"äɉSǶdgR$G<aag 8EDŨݎu c{j"X 7C8cʿrXPE&n{qMG,EzmIm~Ǯ 2n=-)7vzο=Y &+{]i攔#)GSNp1e_=?fy>15Q;vG?=s`] Qh|VAkG^/p0X+ڽizG`IsgԿ" GMR%7Ұ_b`qzEEL^^QQ@c ~!=gx#+r-d[UW%3-70l?ӈ ȷv` ȯ '$>`IxA bkn {+~z oap||xl` ~)_ n>yX j+|cQKAASK  ?   [   HoIAc[H|l ? 7C 6q wvX_AtqVԹe_Gh2K!"C$jX?7ેKbm+;!W}hp >;g8Y͇D>c"9s߈}F5<}| M`#9tAeU,-/v T:.|&k,Txu%s'HzD(23șm[ELϱ_9zޥ36d⢽Y֖5$ˑ;518.gí3QZQEHً+@W Tsz߰VJ-<-:xu:AdRzbêaK={n}Jcʛ_wΖoj1^|]1ީ=Q ;`o۠ez6wY* %fsM9͑Փκp8<&pMt2.T_q!B]K,`e 8o4R%KS{{fC[h, 6l/AlkOr0LÏ- hIKzZ// hJJi|gd>)[yp]+7 |%;ikdIw6QcDih4It^Z \sJjhFZcm-[^$ۚYEȼ{+N/[[z&2߹SOb6) ߠ|.zJEA>W7Vc /e[Q3 (i㶨p2mfdf?}xuaᙶo6]wyjmj&8d4TmIi"Qǜ!Viz_ PWV֪3^ @J"І=JooX} yƴp >;'$SrBxci_$* g=݅" Hg- VpgV)m N- ҨkL,wtzV#OX ")))v;(\Ȅd$Ȭ\xM- 0t7R/LxOj^ݡ4:w:AZ]9BP(%yoE() =I(.ͱZ֝xb=Xn_Y@ :ti|f@ZM| "%^jo8oWn3$Y|9 ,s jbO"a )OZ}vXmp'CCíj XN0 СCTAV}Qhro,]PҬEs~QoGJeyL;uaAA:{[5kSG:9͑:񦝅ⅦA`0mq+L26Ғ|IXpX)` 㓫'Xqjs @\Bm߁@c1mD%&8e_xe種_XO7hY/I򆩪p+תkj%[^LL7 L sE.m2{"_B}δr.Fk `4*J Sc3J߽(9ښjj+*L(+?lruPKdHH je9Qs fnw>fo$c c{j"X 7fNKǒdI|Ն1.Ţ1]?,Ie0~,aX,f7 ֯FD9jM}FcQRD Ǥ$X%TRě:_~: h}uFA~}qѳ% NC[2=ۯԔ :6T4u,dnU!_έ[Ɵ>% \AIy :+f$.7*8vxV$\3U<TELs뻮%_QnlJb^n~zNϻT1>禙P 9=lGl#zCh ʬ>L>=%" O!?7XnٳQ_奴wIѮkIѯpt8"Dn:UUլD۞3<_}" ?GK" +dT .6p !  ߀>,U܂gU48"Dn:t 3H_A{EA~n)^#%g@" ?W_BAA AA-ZZSR>?]FN{TCY9]VNm@CY9(A6))J3EK=-r|N +"Hʌ[t@yfQcR?))(@LmQ_ni?A~?ytmj[1"GFNj:=04ۉb?sl2Zq)QTd֓wfȵ@B"G1G: IDAToD>#cccc>ㄍĽ2[ԧWwn#A*L&Gl0%L.5ؼg_DUY'2UYPBA [/z Y.`b܈GA࿶;[Lo>erz z~$tKSGt 28NPxaΑ<1d:k˞='LX$'9XUC0\=Ch8afeR`Y΍`b,9XsͷRXDWLK{rI @5¬vSFs @$%ҶTt+um\ueƳ^ҜVqHFzy7oQu|E4ԙ86e}gpGg ~l BTy, &4}/ۃ,?9\|d%sM7ofzNjϵY \bꙛ׆:[>d-&#nץE> |gt?tZsZYj@Aw0;qC;m6>`e 8o4R%KS{{f Mu>cT/Q~c~= oLչIO^͝୯ܖ,a6LmP@qΑFY@As` @-+2yHs}JFRE-I"ưG`\[s}cfI.fZi5}+20LÏ- hIKzZ// *k _mt֍s0FG##lkf ?::oiGE{Q~$} g[+>(A9P,4]վCݫhPoA*NqV);elٷszYmB8zf dXVq|AN{M9/X ښ`wޠaK505<9zlwj@4Nwo}9`RCC7n+^xB~>=(/G;xc 3m]߸-mb+Հ[4TmIi"1J\ }qXИ sBr6=nZ5-[2@q۩Hm՝~$,[­HPVOs! m[14Nkxavn߮K@rJ!޽{?qƂ95rƞ "q;VzE\*knPYiҴ ww5ͻnϹ-KzUV:;=$m=FR;Nig9 wO@gBH=|ª3 <_(džP􈽽=`D!G fNN4xjq%e&4S(EELFD633[䕕+g62ё$UU @7{m5re CIUj[gKpIYu`9KKNx|9>qANRv'SKqω`@+:::iLS+Z]=8ٱ{{F+젡)}T*|-JB|[w5D:'CS>a5`$8@"AÓg|wOqZŲO { ,"YII 8~HL'ijaVn۔Ex'׻5Do?ټq}$ݢӥznGs4PҬA]]-Hߏ_ZFWzbj,(3odz~+`08&@A |[%NP hg { Hf˫Z@嬣^S~y)ou^k$άxq7 09œ0Ym 蘑^/V'$>`IxA uY4`d@_ >fmSs+APmjnd C폹҆ǡH_A{şó4fO (#b[A}A_}E%#%g@" ?W_BAA AA-T@ XqK֭,^ 7}SAro|No}3oD>##7ݎRwNm!W }4 7?~0"U3q_ |q]~|lllo1yD3,E&qN\߈yzd[G;K۫5{2q9;dva%ٯ_ج+ʭ3:Z@&9v #ד`#9tA-;a "GƽPiוuߝXd``)_#d\7KE,5d!&U){]zbUsZ ~t/;[L:kNcfN?N#/Z~'Y4dǙm7 /lm3(Kv{/Y0SO渞>.\zq夙z u~xyV}+]Zq &j~a<'|X˹Ax*s<-쀀W҆AFHn8~#"qxۮil {rjaNew);Fj3U1XYc?t`hT.3^ZBԚ 53~Mg'YZy /ߋ&^|}~vJc[wFJ.GJBv^^6v{>,h}13sk;'F9 Cb6O'AqvCA@PN__jH{EPhNOQ>1NLxyr2[zzz~^ oH.(m ΅2|iH㴆f~,`&;;O޼rQPP3ܻ~PPX "jLz^_k*ҘCmߪf'8z.ڳ+wšYC$FSdម.7%h8A.%$$02 I߭:zz jQHd|$* g=ӄ oQc2k=o ohtt&%3ǖo:QHyQJJF5;9̵EĘ4` n啕+g62ё$UURw\~s[`0D͑[噡fu6E1We!1Gp,E+ѡG?;vw!Ix!@cfȡ|>tju5vs̑l!C:T{?x q^C~:-^Ne)髫Kᙘ1oV>ܷ~[$=މ[[2:h.4f> xG|Мtזy}5ҧX5iiiy= _uu릳KzRՕ2Q..MWZ5:0$uT^D&3UtH02k'[v1s N - A<ϗUUM>XWaw2ŭȏՕ!9,N)9sj9,OmÝ am=ǡ*媫>ig#9HWVVA. NرZ m}] ԧTUsV |PLإv[H "MSP 򻨝#CXXIUުlq%$e:?C%%9NcY,4'ma5Zk, ʡ6޹%]G%Hj;G+&3\oսsG|!Z>7#|I/znR V%xx{*uTJjItokӒ3K50%Ng|9;i7V^.¤*-q)zn'Se|>a|>R2])9e CJNA΋__X@~iѲ/MZ*]_//]Fu;&;lM/L(,3^]$rYa1IsE }%lsz_&ܖ;[>_L L;i,!G2@\g<͆N߁@Qe%qoQ b14*`x_wB1a-7/}Iڵ|bVhr4#LUUU@@JJӗT]S ..P-/&yZ*' AWvՕeFo߼ߧMxO Ƿn_@,7Dx%GnjXטQ^Wȋ]O9)g8zhSzAXS ٍok)6Ax+[j1a/gPjۻD{ݍ k =wA a݈Ǐ/'wwQ$t gHؘ8ac ݳ }F*L&Gl0%L.5B~ɅB@c[#`=Xb7N3{/޴T%rfVSFs׳MYd뼷KBN8DQQf]Mnۆ0lմR-Ojpr^]ix;9^X[ZXs֐\/GT D:[+g*Jw)Sl),$G"tRk+@%4ItFɘ[`-8iWACa9zޥ36d⢽Y9NI3-8n?mZ-*o~])qŪx wYz"3K8+:}'qdgΒ>`]&guC#}8;)Lg((IcZ tzTAeYrE75 ѯ*KBj%_ ;=aǯnMKC]3͐ͱZ֝xbH[u9bt$E?8" + `V 3'E!I\<;*Vc֌GB3\ܱ#dXLljᷗI |>tsD' #=h_ҶcU// E/OU8ϹkXx~e.%wt'Gn9Hd 4er ZKwn%Nxn.W ׶b;W&MViIKpBp6}c1mLhCVDEO:IHqtզȗ%Tae j_~q Z͟jE4-+璴Z*]_>6Lȯ/㜭]zSt_N=k*j xFm}T8fNKǒdIpY/h]6JHtNcKQ*K< ut r>, %X$C֜3WYYZ&1i4Ij` NW#JO:WN("?LK8ulKvWO aw9a(4U}LyiXܪCv3(7TglƝ:Vr9|׊k&G){N\}׵:ʭMДSθ/y_ ९= |ņZY3|6gkA8P3CIз=/J8L}9`0}gwv)xW\+є]Զ8(w9i&BNGKY~W>eFxN_O ҆Xƪ0c齚Y>TtqwVwJŗl>*#@-y}{G 8mgg@9_cۅE;4j+H`)m)Em<G8Nt4H\]nٳQ85ƒjVVVO|/{Zg~{{l3g~/Sc[ȶ4!n),AAߔ4c}Xy1e"AA7UߠsWqHAAA-T@AAPAAAU2ړ@@=(/AAA 9Ju"ő 1r{Q_ni?ƶ D{ݍ k f{`h ~!z.O.8 7?~0"U/B"G1G񃜹oD>#cccc>ㄍL$d2L&|uyzj ߏIbCIҏ͢bHҏLD~I6AvEB>m5nN\7|찥''>kHb `sіh,v%5" +=ʕI~LhLv]JA;n.JM鱍Fyvݒ@`:;fu|̚>m{ (0;-U]˓#N5GҶkO@_؆r `02 s ?˸Do99Db`蹋|/^$NP 0rOrlZ x(۠roXžw' l=W$VXdtb'v}'ij dmUBe.! 4OkʤDC+헊ˆ\_ʹrh3k'\~Db,ȓܵYQqZ`Zq3qy~Q1?}gu <YsN9P9oqm}@c[ȶ򍫮vI8+ij? }{Z|f?kCt;H1|q)&B`j@A@?޴XД]OaIFZfo$ȣq~ 9ӆgVTtPgb JX&Š3~X,NctЙ,&FetИ,&J2XLFk3˧b2Zh? yA l[G]|l{"b[FvJSowt0j`U![8}r./ON\םMgp^| [(3+DM(?~(ܧ̈SNƕ2??-@`Nzy^s&P%Jsp~\}' Yt]SW=_LfH" Ql? o~çhG>#4ܚr,BXr,&7Ȇ_A\UyŜ*| >B=(/AAOJXK]IUԵ@@i${߃ >aYVUic VUic=QlJg@~~ALC]]',**l*k 󎧲n۵g"E(T+HSB{XEiJ."f*;+DqͲ]+|?{s~9t~ig¯@kEC?/VoJhe@kEC?/X(($PP>+6z2n(%/+1o1G[$857^ __(`l=׋ .DE>sri#7ЋV;N|Bc"om79|]57={铘k 0k xBD)By0HVg)ʭ%?yÔܷ/׍1zlⷹiO{˲kK$KD<.,)ψJwDKbԘkU^ Fc~FG璖ڌkf$wڞeNJVrIEAAe=r%W nY*vƴxv.Cc0]3W7Z:kKa@Ŏpo/}ۂ#jj݌i=%׹w5*^xqAIS(n3jc1P@PWav'p5{{g߽riAFd8n9L_ I}}k朑zֹw=v"e x ]Aooo˩TguTE9y(l2h#d|mo$jzi({۱gu]SvHKK[5DAAA9NFa*׷1Y)IjyQ?#TI󱭽MM1[qZØ Zjѫ#on4 #iSp2CCƮ>2 3nK%FFBvaQMX#rD^ Qm񉨔os_F9hH(4g_=uՆ ʷkͺr*?^XtF 0j{sPyҳ7뚀Y|ݨ̏uuLDs/JmD.!Q׮:@VFz30\d) d %'<0/颣*&R(&VG.ʏ% :(enQAV]VF$g?j31^v]Vsut|)w/t$k,l!w) F+mzBV^nj镣YnE*8hھ7yώϒW:QN5l2IWG_ŸYWt 3D (01Gd--|Y ߜ]K6ţÌ7)QA.r$v6;bDꨳcƜRW6U8yYo uQ"BX: _B+~vڵϤHMŷ> l*-MP HPriډChhxH ND?Cooj~0X{z޵`u*05QuQR'imݕn>xqT9tùֹmU%ܬcmf:[ڧ$T%JdJC.X3uG)ED5궻Lҧn5`õrkx'ɟJHcʭ80S__폈~pZ %yߝdg5a[$g=n]Vp2Eiێ:L;} M'܂?Ӑݻ-șA^#U{]*]]^:P Jα\Vr W}! J_Zk&l,3}l% bg|El2FL =q"ott*`\6gf97YjO ;b&)*7~rc)cۘ]1b~!"܇!kEAlr4%m8ZXX EZcFxAl?-aiEDHsg_ 9Ooo^iX]_)Ei5m97{a;*Ku,:&10͏_y.R!CNYT[;7U(dq1zuG"h4ATYSZJ%xfGvFOEW 6 ۠leAyl\+:^xl8<q\ 78So^PUM->{flD#3qA}gcI WOhΉ{,Ĭj{Qw\U)b1wY`-@(yT\VJf#4PxGw_7}9W6Opw|(0wc,"OA XB@aUUPd˺S\ qQ#9|dmzu;U6~D׼pe#KɼE}4p 5]0I K `pߋZݩ h {x}hMuM3@ZrZ+':7ː0asKiXP<4UQ.c aRl\YHa"޿6NGC͓V:@SN36&厉k&I?<`X,_LS#VlbGSջ5J(=2ZO=buY-o(\)dHóMK`ka1X-ѓ6ՔlX5VRVQR/ e%.k׹=ߴ,`pںa"FK֟F|ͅ yӻ`1XKYhh&v9ETT~Z?ܷ\ryRӷ5xZ]͆PW0;1k7"h4GOQNPlcwC58hffۑz->t  NA"psq. n ӝ5/ot.,ş0M| ͺZ UZ:+:J 7'.AZcFwn8R.g[z3k7X_afz\x pJy d2NgR@9;G TTPi4 H@$u Wf)?; cw.ւx_.w|j?'u &X|a2:3gyFBvq 3Y;d\aLG}OkaQM:A1GG>Kc_woŭ{HRCګ.cGǐLU4&0K1ݗؙ8  @o#IAlr`-YJd߷t`JH.4#"|Q7`0,,]|$/ ,^ty-61Rc9hX\&\dLfN!F{wg[Ht'؞OGvx嶌ؔ6k/EeĐ;S9L p+N4Tiy/,YwTUU <;~Z XUOv#i$[D* {߮ ̎ė>IF}i'y5q!+Tڐ {_u׀"N‘? {񅰵X;hpW_wãE'@2?fgTb1]+}q.e73:ao3arb1W6OpD"!778D%+9u2&q%Zv]BiT꾵) a+fGsC@u=\ҠK:ߥ>I|N7a zF/4<ҟa}(,ؕOցtLqIQ8>7 qs3*O|ű1ѕ5Du^D;.s4~ -U~{_=GL:-,'o^w:hjuᩕO,`7X+SYb2?1""ԗ 798]b׼͏6%&$ ңvE|^=*giΜ7\ħ0b|cFV5xnN,FN-"$%CBia8 ![f]&l5|_X$'M )XmٳPec &9ڌqL}[v3xrgw2`Zm>7;w3Sb̭G rqKMBzKU^,`9G˗"4T=E#98GΟ4[=brOQXQ>( $Dl+7+HDԔ kNRSӵuĒQ$(/-eynO#]5UM')f,YBfTmv^=Փ Є,^ىxfA=9tʵΦ\P*Ml84| -;.=:z1/ ;# JnYn)'4gR aëfjh*A`v6k=ff!;*NOoOm(PZd5[ >Psd#] !1E9Q<䈏Zğ"_Zk5ЀG@fȐ JF\l Ǝ ;)y6n|v]~fS!Rk+*Aͯ9j+i!9D!kI(UR}:;MU+YvS >fumk>h7مWyg73pr".!'fN][y_)WV0ߧ qtM;1mۖl g?te+t=pJaѰХSNr۪bn~z\ġ]1Ü؟q}l;[ϸ: nfk8rũW+%(Q~e v'0jބzgkȳgϞ={ٳ %b􃐋u-(!Ƈ),JӞ\:U?y_y"YP(JD(ʀ̂Wo1EJB5Jϸ頩P10ٿ_= F`D }~ﮬJRcήc{f .d^Tx~*->+n?]# 6}*sMM=׮&Vk77:o SAb,G p񡗈 "i3 Xdm``` Xy:Knap Ѣ!.b)+v"lcoye39\[EF}-_˻ޗSUَ1P@PWE9y(l2ifg(=w{hqB{J#uҕgroA? DE7ttŦTJIIGjd-j \φ&`Nt!7Z3"n׷V܉EW7m]$55E^IP$<:TFFBvaQM=]r- LFs)X#^Β[2 +|U°ysK(JYi)rٚ;Qm񉨔os_F9h{,鯂fw}^? 7߳%(8'L#Q!lO7sR&& J K}q= Vjc n俸j$ؖ䱫OF-*ȌRO6t.&5(/&It5O0҉a05 '14Tev]u6{]9u, 4h~%i&>yIU;zLpe<3L"Abv4Jh] @g8? s+*J v_ }y;D@yx5sUbwbZά:t<}0R~~FGcS#LF" J A77䎞rOvaB'@}5v_J )%_u@n>:x5Qux2ٜ! 1Lt"hTJ'npIK`efUoR,ɶu ]>H@m}WH<1:hRsC]niFLQ#8#gbiC,6unq2!d2^ʯC}wՆBs%wNi;2oSЫo2S@"߈WEW^-FvD h4T=c׷+Hs:(g\7t"J*$ 0uo)+vjj۩@ΐ5ueA'/@˗#ߝ? n19ζ$}>iTFW`0RBjjHygUI&;z < }"ĦY' \m%r Hm>qL0Dt"eۇqudΫY8֡+B/]]NgfJ7䟝dy&O="TkƐү1Kn]O0p,IIu=!NcCE1 n><@}C=SgiE͡^Yxdzb ʏ9S__E{[l[[ӞG kl, \^>9 MtvEo%㈖БsdP'@@@@\BS)D}~s0JKrm####=mmej:Z5i* ;enGAA 0'@M0҉!3O;z/3FJNzvvbkkk]O8Hi[9%coU\k+d1X: l#F((=3=o,!$U C oE1?V51 Y[kW^ZʀqSvO2yTX. 00ҩ0Y㙯?KQ rg׿2uRJې|cqx,2Ch7٭tmdH@VbB/Gͣա=Y2A} 7qHN(JMiK-Ij-ZK6Zpv>kH Ws^қZXEoKJ[9rG@s>IfQ*0.?i'WFvZ!e>{j֋tdLL_ooNUJ43Mt,aҹA<'++[AwZA2 ?;'@G&zIzV'w(FCN>doX55=9zÕrd}6$} !:i%O=fwhm)$^| !R3|˾cu!>K|fhe%I C5'^3DJ'pѵLyRC F.~;Gu$]qr' ;`m7o#SH@̓?ON:齱 vNAw ( GSn('***??^/--@.4Q!*kZ3?!(ubjpu ((((Ltv') =QU4}BEA8n2'\ IDAT/~S}lFGQ[toPw 9aA:Ơ1yھ)]Wjrp9̥?7 }+k~haŵ, $ Eqo$70%+NbiI>}Q^ӃPP V =]=we1V~ŭޞ;ɉd9:SXp1AxmQΞjӫ:I2OOaG0oOa{ $j#e}oRs)^lmr#sysJbpLpWs<αNd.8|FbE {?_y *V6p\dmw/trg~i}MyZY-L)A6$gh;櫐q+ϵ绔<`?%zЍ\1Vyo>!DA 5*~"L=z~ hmKd~.<^J v"hWc1K5qWgt6y9U\@`u 9\M <~\]|g%DnDfM>힩a}1 y@0yz%eqλ 焕~8k@(oݸp GۤS:,̦)L63D֎察7wX//(-o 2Mń 5#x>/IQNf?Ċ@lsP2mt}ySseKJ@Th4m{7)Ἔ*>Hlΰt6TmIA6O `h2ZsJqfq.7驥GYb MZ&%KXy}wz#+rJ4IE^#mOntb8'i[`qq: Q/...w..{-6uֺgF{wzNwNcq64q&Z._ПmQuañO?TlٿP  lkQshaV#^m9Ycev(6:`}}ɦIm,{U;VO/,%",RUU&Ȇzog[$*WYUi7̎ė*ʃ13"N|t"PPUVl|2$}"px:e"UÆ* (xb!x<ڥQ:Y WQ*K?,^7|΂vNc޳JbJѺ+UG#z{_2Jgdj *ҋ?㠅 _yQBGqTd]wbkyC dńoM:LzNZ_1Lˌ cim ^>:qM1%u8>r狏_@o>ЛMdi Q/l'č*0Vk t/,ԓ&YF21ʂrmr Wj羭s9/Guu4nm3"ڌ) 9orҩT`[L*Ad2TdtL&t#sдrw?Lןg,.kR XN)ee̔8%sQ\R{Mi/g.!!8r,ߜ rOQXQ7iL6'a܊LP*,z Б9}2?c#Ԅrr #1)OǍTn-}׹Cz,uqdg=JJˋFu*׮ckU4(J&D~+"1=CU~"I腅5EPSQɫ>NC,,#%%Y%qt;+͜$Bw7]m>cw4Q;y0F}3%&XcG!OzvF=NSKYZʹ}DoroǽC;˝P1 M"+0ߧ]uM ff{#"zϦGOSrMaS>G ^\]^_ymDYg{%FuVq߀ z!lyכ>1LvK휻5Ҍ׸ <_%Jj/-xzdzUv\{`](}fgI57ח/[kpT@Kf^QҔk{/ett؊V~!0a=[(6]jETqFnOV&+3Ms-ow+MlN95u/nuw p mt8ɰ2e(m>`3tm!i=N]L@*~}*6 PUݷ!)u"WpKK'/FEW5*{;/Nrp80g=b}qxD~6Z?Ss5ݹqKɑ)o]y9jm s} ~H볾|kSר!g9ZP!*B}?|wOF~Aey#J_Pd&l ]AlO 0bwƯ|.O$3 m ~*uZt$=k!_H_"be,@/! oB޹s~.#;܆"c]_yqV9vڰ!$: zIp[)tq>F+@Y(?=m}w]`LܦK9%RK" s>2! ~':&s=g|%1[K|(͉{\> :t~ mk/Ja )V]%Swu^V@Pha*/ì-+p1_$VQxYbq5Ɍ~R09jMek>k b$ph[szx`x6(>%4hTS|SlNUX?4jL 46¾ޝ>]#5m.1//-ml-Z.`f>WEZx+&n\Đ爛7Y>*V֧ b)^@6!Yq";NRg>r6=/t4c44 5x4x=%-MEa8{ k32SŸ{ +tgK~,{0yrP C yb:ёut =oފɫXy#F-_.7 Am=gA^ zs0+[x)n}I#Vb忦ʷ2A-IX U)?º[gLx؈pQ^X`v38]SyC5w1jw+nc VroϕF`#I؟LwQ[~ҳ] C3m#6+wDQ sHȹ| p/j_󙉑D(?|2ZX’9jvʼn La脵^k!9DY)9@55Cygm# oKLc1Bmx}km7a`dOa~(l-S|>jK.* &Y?ZQ B}"yR":bS*$ߩđZ*᫊Ka2gĆ_\WPȺUȍ%}f4S[L&\_Oxt&+}2K!ig'=rI*bvGbfm8sv9+9j֖<¸^5F\s?TѸڟ|<8(FFdvOb'yL{qfwh1+nvadzeFȑ}Aoc? E:؍x{չ\fjWԛnb5f dn>Rs\eko{CfʱB)gvxD._9uTT[&ETP1yb ` v>̧"b-( %0A8C|X=}ιg}2zGdgpk IDATsd'MBf`k2/=66AKi܍[ʭ[}c;R*~yySr4ܒuv;J*Þ*if@s\bRPUUMoxlÒ:7`*Rrr(APK{C}ahqWj}!TJ5Jgz*Ba9p>Ӭ93,, 7h&|j'!,@I z1^{:[`8MHR@@E?Zq-V+>M_Z?#< \onǏwQQ`"#7 Pؐtr(\]ӹLC/)rutG=ԕTNl!-åy\f./,;Pe7(P03% LioKO'o6cpkY99Vk.V,`4ĊOT^>w+8^54-9&ujIN[1՜o׿E~9׽ Vl{/;[GazzGn4=;Q{3$]w3^% &]})Aa֦mVy%x-?ޤOzud<@pٴkj`V* )HMS(RN:]%h!% bRIg)EaJMҹ*o#v@.ǖ;f&M.&lx3, C\r !Riep& {KU8fbjJF @ZYӥ (olV%Cj [GEEyեeJg_ciBqJԇ}LZ{£/0 +,̋eG@u F뫏<YO8i֡0t"`0q~zU3G>u"!7Ub'La^Q! Y1z`8ߝJ27U8%&-}~"[0Dևy*񃽽IˋSR .۸xܐ9GnG&8 .7?!vصҢtEZ(T˿R>ǝ=J`t%QO O0͸hf>B3v>f'b]3BzVÀ{5U٧:ݣӹ&_u1+?@w.Z!~b/h%Wl^dA KʾڷCT>bt*`x&wcxbqvX1]:` 9)A2lR]"ƷBf  pv!N9q8+[ΎXE Y.X^O);}K3Y-{x6T,D> Di.~zKzź3⌂w=魿6 rc; {o;6h5\v]w3.iz$3je+Lb0 OfDpk qq{*Ewij=*E6<Ԉ5G/ wI/e,u43[UzCIe4Т贉R?twf-ܻmu+^9ñԸET]{o(7Qo \@YV4`AF"dzvE6,!y@/z5haƩkT٧%:uۦճ+O~-?U?mn玾wB5*o?F[jrMm ̈-Q&C/$nj@~W 555xZJ @ *7IxW}R~:UJj;~l[d@Fdgmr2!#ז2[ Fj q2\6B *BBrt`}UdXn_ CBc? x eՅP yƛh`cfU+M!Hev#!=dsgRSXT4TT&IcccͺE o0r$ 3V;OClc盇29 sSDy8{Loh9?<'NʯjHk-yjqۯ>!6$zw<Db؊dCWB]knԳV}@yUfJe^/3P~!llYQSO-+g?Obclooa"IQx4Ga!? 7:*g#̋`3 2B6 eb.h?sMO9 ,Kw$m&w{ \Y-.7fqS6Jswp3jr"O=+૞cÇ4V~x5|>qWu^fqft}|պK`)m]VYɋmH[k7k 63H <Ґs'nqf-(Mxqh n}3C]JR"og6spT=?[`4I) 㷙_zt[[*5k6#ʖlPߴ%W|{nXU{_Qp>i5e 5̡Sc:]'uSv{izn#VY{'*N_s^h{OC:mYĬ)-.s/WZԐ'玃{ì7lts!1SvxN}VNy55u1?;Jsc~쏍[ooo%߆:: eIlaq﷤oۅnzX?8>9?SG?(?MoJO0|\c{`z#a٨iӌ:} \*&[Fa~ 槀oqԜ;ouq﫴>Di|;Pz4Ĉ*Z?~ D1iT9ivs :׀?  ,ˢ61 OS_SS`~n _߄>fS]vl5S w`ۯX8Vۇo7lW@7 Y{O_?w*7 |=a #𤰰I@kGXaa^E{X['u?0_a@ӊsL?_=n8銍|MtJVFR䫋 ?~tg[t'GN2-f;- 9_Oyo 2wlA[}7=w>Q"-Bswz]#pr-ޱrR~ aLk~*n:;/ԩ J[b[CJ,sq!WF6]6]+WA$ƽh_BT=gIHBY.˃qw88 od{zscC-țy#m Cv;%"NS~/U5u?'=BDs4PhĦYWva웪b9&cu{FsU#S̺z@jn%J|nzUa/Si7|23zAc/X 0mI٘H 0Y:&L4Xm_1k*\yϣf@d5OWJOYipH4 H\rUAVEjM`:U?%YkM5Y_?G,^,F9`Rk[GT^A 󍤠*),!tgorYiaeG(Ax١drÁ«*gzzx <4^]c:](vB7KDhOٻ@M>gELDRdL5u>fIz̩a1;jȹ|"& a JTĜ.(Ylp*2j"/?XE-O+@si[/ϳ >kk]k:2j8U24[q5\?x/Tt!' ;67)򢍙/TŚn1+-O/{|zNm]9EAP\ɹ`qpm?_u~[bW𦴙 EL AAPAC}OǗPRldT25c `<dtjT *FE7IsPVzTW2c;YTU^Fn{ulYi(?r-GG ܏Xހ(LRR3L|=^qvgSW(L}~,W̅}ǟm;݉-[\nDL|hLw;LgIhK[A#CC4VYSSg^Js@$> -5 `~9ˍRJ1Yx9?Q9j},Pgԕ5)#8Q2d2;G4~Z/ŽS18y}.1 yDd]1qr '@l\}*h.5 ]ǫZut*I- RƝ[2W@׫,%oQ12&w,*0_@)QYW@TG/"<9VMfVmӧO>-xɳdŽu:<VSLp_fӕI k-dY)\/q զk'MĿ Jkoi;FMwY;߰ x&ܨ|aԙ[U8:j7 ;)` C:x׬UtGSӯduROg}!1ps-ux fZ9jr"rZq?1e"apӓ#+o6)/4{mT՗z6kl4Hx1UfPr;lll~+ҽ!^Q?~Q_B>:Po>> 䆪^vz)%խ[e|Sd̹K6z\xI*|mM_JG'vUoގ-/o)E)54jiDH" I Kj[~eq=6Pnϓk9ޞt^po"d٘_<ȍ(9P 3QaJӤ=5WjzmjV[UAI{A+nk AyeSd~5,`ŷ L:EK,IfԔvȦ)gAXmg2Y88j?y8nI a4hf@0p~{/Hh] &45 l<8gM|&LpiKy4,\p@`…  YSM|iנ/۱{U\KG @r߅#nfիWOSV/ jH'| 9;NfvNw~e[񌧾qަQW&CfTy1$ 5cY qq]:bhK7斴nQPƔ_<^:5.ú*0)Q^^QSK˳&q|:22NuC7΍Ғ/*;DN Q[{"":3~PCGj$9rav*?(?V]A}^o☬9w(ғ"or# X,}sI H#-ZFn15PF&+WVͩ&C6e}Uf)E"t%3 '22¶_j1Cup8۸ONj+y3OzsiV/K<%=췙{{Ȍdc82jҨI#^vuCۋݮcK?QQ8{ndw1KO ъ>">֔={檒۷p*gs!Ь)-o0WΟJ=Xh%d;C/jlfw8 KsNĵ$6fT7=&z2 ^o  {{o7`ePWWG' >i-,7J&[߾R X,6٫; iWbkfJ-p8Ap븈CB=ݡ?;kmo|KbD |,ROҝadԔxu^X:H/AAAAAxf<=}|{h0Pp{<ho? |Ԑ^7_vcG }&JR9YC$-/H?5(>2 2%t2//ZRKc ӏ@n<èm9C~ %(((((((((((=PPPPPPPPPPPz e0`0,&} /c~D5AFIG.F#*}dɰn7&GA#R\r0 47ZQCxT]vO+8w^#szXK(^._PAl(I}}x {aWإUVw<]m%/-0HaFpeOm&}<^6UlSpgæwԪ!-՗{/p8ɥbr$!s%lmO''c}nZR+L8c*FPraO㗩M#b<̎'2Ta V%{yl5g,wwW@$}mxn󨻣(xrQy:+5~E %3Sޏ쇺(KcRǥ7[OǕqHƅ[C[sn8a@g~oQPD~.!pK”fBfjKfpw)56,-#n|rz.7j!ej",ٚ感[{݌;_9ο˽WtV*t˳) N$/nf'%NIf[2♸n,WQ?r|̻ >81?ET~zjz:Qmѱ-oNEё,05ikTwWZH>Y[M_y'4~@sJD? ,bqX,L[붤l &AA |S61)W|͎ 8"G{(ﴉZK/$ӅB uu͜uFx >+OGʏ8\>:&նr[Z%`HSģ'' b9MSUa\& ZNSQaey݉ uݬ|΃RBfQ^,OGRC~#9g(> 4w垘b@`BI* @@ȇ G)g77|;927/ `{rUgI/Y/\<ޚJ.Fx{.Hzz̩wka65n#$zGV,\u:T(QC%,STZSYitI²tJ@ԅK_f=eC&Zq7G((aOӝIOeBCfV 4d.GdtZ²Q_|Ƒj$G*>ܛg:+ƙ>] }.22ez$K[s6b8G\j˭D|o.wzCKĚe!$8~Tg:x5!D3̗{Vۯ;Yt*wM-='+)쭬vwC2\ͧrYbղ̏VVVVV۟uat/sRI=d>uym[Z>e;|*o?꠫XvE.3)q-?̷_8#:cf7߾d~aVmes'T r)nؼw2J|Oe&wp y]QX8 h]^M0Vga i|)94U9qY򜸿k|Z1*;yY54kHԗy=? v,(~spR66|wm4({w}/zrz2~C%7C.rx\U'@ќOc7DDj?X ǘ|~i1~N*di> @aHp=08oSG r%Ap<H]W>{i ,JzrFt869Q|IlQUItD2KB/6L|ethzQϯȱHWDҚ" Wފ.Ւ~Cy5}l>Ղ|3C|NL'/Ϡ7DNWו4  (ǂo|_gQM ibbԘ&*SER8<'E-7qk^is@rIڲI? e0xa Bt&Xz: eb: +nfG~tṞùKwxEձ5@ys-g Ȟcl;A ìK;q@k!QoN$0Jq3-0~sVlUTYLt c2[{ L(T7kJσ+*R ICEEIa)ϥ69`0_p}N{:q8%6~DjHˀrK[o[?=DE\o}ݤћ %,k2N@U $? .`GUhө啭@%l5ʢoш+hnf`?.-=eS[[C0w:T˶Qm?;rT[k(( 1#S%U蔂Ȅd)3bS,pOd62L&L&O"/3&3kex .;ƅ7]lƦ݅--O޳l[Apޢ֍>܏]`{f$s76jR_i͏>>ñԿl~fyJXÔ;jҮlY/Nhtu$ͽiiaɻtGK{[K>yk>0K>NMh~ayj|$+ėshYפߐ[fiRNZ gS!/ўQZE_us^gD H7&oߊL)hOhEf= a )))P{ QHVZ^]H%2ʲ kv#NO-, `=F\o;32d~8o9-W&Ů8}ib(o(jWPՈ)c h)^Atj0 s"`yEEq @[! @7X~m-6%lhgu… 9ỌߝAIiD5Z_ o??'Ϫ+odBT-K Xff *D @AAAyxԲ::7?/ZVGc ӏTW PP4~@AAAA% JOA2b}ʥ7;9SN}?ⴝᛞ%жܻiGx-pݺ@O(KiVp>dXۍ|ǏA#9iŸu O{9ǗL;r  [:ɮzW@-:2bKR^w*d? TH+OpݘJQXlnZYjٮC'X핏|5KO~뙊2P>x9e?iSid!@0Ew><3s;ֳaP9mSu`sN ٥$ K7W;?TNe5>ޱ߁5XFU[ٻEWr3(3紹XA@T3o3Zz^|:Pb~w1L r℔? eY%r!r.kkP9<.>[@AAi'뒍͵8X>)_獟#v, {M??WO>7RJVk,'YM/Kp K`gK uR ଵdE: rϥqI |w+: ]My?VIaRͺXyGoNS^l5~7=T}ZHitKo 6\m>ؽǪBč[h+:EL^mQCL]TFT}G85Sl /ʘqZ\4+?$4t!cJö":Pݤ~mZvTmN/TuJUS27kGƯ7wlņw5dϗ|0҆SvYG_JkZꨗ[g6(Y}'Cֽ>aUfAZKbr0Cp?Ov*wś{J12FSLӈ~r*?ݣ[z׶E#eIuYO=}>0ѷApPQ(/q7m@|qQSbz|*jlX":Ո%]Ne|?삒={^IieӆI(}S @6qL!̋>qh/׎yUU.dȽ-2'--XsԹ:$o:4)m70#m1Pg~qrpH'U's˪1OQsûG̐/gN}7@#͝ +܀, u6lF(=Y.&@m 8ccI(mbd|*7"YtM%A!jdwmC;3}aS3@fRAh~15d @? zg_|4w^q#5yx'opLthԈ?|G k ! (ɒػhGc_O٪$c(% ܎EG,KOo`fviŖ̀?HTj Wo?b^L@`Zqfby!InьXN6k>zDL/}KpYL.5ޭzZ[ !w^fnߵ;%'&]m*'GN^No@+6mmV>`kkkkKzBJuMJZHIpuV" h~69 Ia]6^۶I7+~p]4oms =W(9ZXd۶%_LJ#7{Tl\([/jpj*\2CZ)R22i&'B%G0l_y ɾrrU1whPsٲdHfFiA 6cK?,X+h藀:BhxÛI]xA ^_W$nKm`~knrFc}2>I 韚FtaOǧa<\YW!~;=b0O4shXN+{{{{!!}_\a:e@ѣcgelZ,NϏy{̹fqCeJZH`퀨?:,ʬ 睄)Q[Q1\]ŵ[NlEE[EaazBB0{xsνƍslͺ>(4SC"%}زsS&^fo0p_]lqp _ԧ$!оwD<06go'v!!?wҩJEAivj⟼-lj_Bjr -; Qx0kT\ ' 8XrΧًpY2:Կ 斃˞3}peQIrW Nh :EO^0z4MB@ ieܺk<|\bΓ, WHK2Ņ4a'N33R6u4:/WmgZ͊VD$| 7͆'XYL2N.׏ѝ1Ӷ<'SZByqƛlWN?`OcXXX&R]]EP>S(*ڔob?#, זLh6&p>ip76cP \[Vșw=.C9#+8YBmv{zz䚩Ioz;|9C6iP=,j#/$T$C"bN嗾͌ Y?i Ԡz"^6u 0( )14tFV[7R^:ݍ XxOX,-R@MҍY%' kҰ5QTɸ "nS۞i9t3%o~*} _ ht, ftyoxNP(OZ`ߗD_돜v['yn$*.!Rˎ(8ֶ?n\CUs5#$D@BoCHOLRD3]+ EY_"P;-n{H"?`J'&R]҉d́ʼnE#JJ LU;']2{VD(/LIɠOzXvROJ YBð=Ued%DNVOOÂz{I5Cp"r;XEQT 7CQTQTQT (*DbE[T5{g MM2G5fR'یY\E: HM6wԗ#{:\O0Q.))CI2vrD{TsQ:)5~P3:*9rvݵDVxB_=z8yפ/P`Yt|F-t@ΘBFTTԈvۢ]Pi`˂[)U'^o4ۏ> ZCDq~rG#y"Iɦ(/OQ^Q?D0y-zu~ S }a5+; R*+ٚ++h-2( SDA^C dI$y{gŬƯD7_ʭTR8dcGfSΡZDnonM#^zٝ`WU^(*R:@Era査*Ctw xN [3|xC&d^Ys1lozY)Eժi-rCNq\`2$ŮKC}Wo9Ȁm׳S7VwF1<"yK^٭$ ;B 0 OӿLe5IFHh_}D p$'+;1R6%7 x)Gv4p1Mf؅W\o?oA/N?6 *rc&-xx5`9y_qn_{|8e]V2=}tyî]k#"IJ?MOW=~ |(( ֥)ï7/}'8rߟ6soDay~ɛյDFӎ2fy`K?-? "I?lض??xGE!(3B~Oo}C:qK$Zpo mU.]Hn:I=FC3X֞=)g^pIOg/)lgA>jc"@&al3/jHu Œ}wP63xw4{uɅWn $?`t_lkШT1F$[A~ğlkk÷I*`/at.````````00000000000 h+XˀPF8[:g˿3K=QNɠ+;Ǻ?,-o0|nGJԁaډ Kt-9Ͳsp#G9zl023wA&;WQT7'ư'tzE,n+c֘o3詅-Wm/fV5{nPtzS@ZH<5X89weZ:G`$F@uAVĜ fDDozԮ;pZ/eDɶ'Ji,\LZ|܌(3#򩠤jhXM[?l?;FmyǷ˭'7 sS6P'sr#gMM#q,|GhMr8F Gҩcٵ\o3<-d;h e;C rr>+4O _( jkkjjI=QN6M (u7הfAٛJΝObKVX Ӎ\Xr*ֶ*n.uhdO/%^Ɇg(k~ow R IFbԫaAf |m+?ɭ6%}h;9#e0w0]KWdaX$3 ҳ rlZKLlܹ;_V}_ O'NjoV-ZUo=D='8+(Sr#0V$i4xq^ M, ^3wg;Ν;wn]'JR1SeYdٖS@Hc&Y\Mg2G6ΓȈ|&Z1cߜ~*UܼT^l m;r#d{#! /^K3p!UkQoYz#XИ++|3?Y?3r4*Z]+е[0L]!OQd{h6Zl]Zڥp0000 Ծ}uVc={hW-F:]X0g1"MzۯTV(*Hh'O+cT#QRҩ ̩d}6jN[u%e꼛|3]UQ-ɐ]hM<)va]r)6"|x!𭨺@IJvq5U݊Y%(SovΖ.{O$*I0WI}۩SbqH$@ U-[x,!|Ÿ7%Q@DU͏速Ŕ?sJmH*5FR {WpkeRu6(3qE CrdѲ)6p*R/]JEbEpk!8x)^ Ж,8NpܷND p7_%^$tX=4E"qDDZDĘV R%:R: IoR+&O H슶˩EeL1KE8/ _cy~dyѭN95/Re'hq d Ez[%jN-P54?7'$`ON银%:5<^$H$*yv`Ã+h6؝YmG4IHD2r#'ږf8nК1bZ%[ [1ċ8kѵB:mA,NA-ö3GHGIQ_2 \"GHJϥsb+.bW)2\*T+U3)HN(`[]UbubANـ*6))--$j(Q(38L&"@[҄tEY@sңD M7Q{FCeY ]@E}#t]]89}ITݝl߽oR^VjiKѣS|\ix!U+h)KBVYF@ ܼJ()(P0N j`Q}%Ncdx2JNt"AF s-bSLz*+ѤFx[9U&\}:O~ڴqwS6yRg ^{MPI)U پ-IEwإcx'J& p4vK7yr}@d?;sMΟ9p̉QQf^;fo\h$e)Oy ^bL_6Oݺ\r@hs.g]۰;ť1GCdTP9LQk5ذmh/0 l#= e~.߂`?{k?8/^*ɐtB.]WWQ.}ğ^RFׂdD6oAW;!̊|?x]D"@Q@kN੊B!ECշI*`?lHT>MO 6VPW׺om Km?```````````V:g^Vm ǫ H]40Xr3u?*[\ ?O 7=VW_5bܖ\7ш..O Xρ7vUT|waݢݡ@kQe~KWȒ:%t>"'ڎ6+#,$gMZɆ^kw.CMqu]/V)B1>o{e'"v㦌Q 9eYa7NnGsJ^+"PMLb *Kz~JH>$= 1$=hd–?]db!zb~u-Ru}=νev=oOB}pv?z"w@p%l{IPl.bbBՔ |zө@/O=9~-j5ԗՖ?~H@еw⢭)V?|:9Ovת$(\: chʝOA&bFfIynCͩ >pb;@#ʘ9lGsZwS˿+Y߫|L)5MP3a[xה!z"uC?x"on~,tWtDC6Vxw_rxyݿ^lKCl8gڶ?\:^ge]5ieߤ X><rxe>4b;Z4o^{H:IX*ZKtnWqw8e9+پJ}S6y =e8ݡ <(Iޭ[LkReћOtRw?T ϱo.h>9|${pf ]ko ܶ'_J:]Pu~?-k ŲC9ǭNY6&ZZ[ =pIn6ϔ>z0RNg0tAVMq[uvwsSN<~uUh~DPPTHKʯ'Qg%۞{p¾Ʋ)/.ߓl^ueO訄{|Xf~<]{s̖ҿ]?HmsBMiA`&oٞ5Ph.*9w2Ոdvw­f5S?+IRN 5_bƦ( WQ_|C%͸ʖ1]x?bӞ^*s+kQ>հU)i|8P*`U&x43Nض鿟IN80c?F̻)?j9qݒ- !BCU.-~#ePo-*H.H y$U6j9g ߻QS~Yl-'rl+DQrl!T7ݾZ 3x?(JSoVVr>>0Z $&JdoTiZnTihMfԻs!n ^H5pgI>u 7,GNj@C+fo雺aJj.šlI ;FfV6 OaF',/ZІ;9u8m&_yL~iqU]x>z5rEbZrTWxwaR b ex/&ɉi/]~U `rHw=gJqƱxZmjH&I< _tuBi kIZҒPVw.!1X>;ÌvZS!hbPA@N[1hǩnuh-X)@>vuuz6e+rj2~QwuS=U_/b5^J^˦^>H;=דά!jY?2 5pҭ g.]tU`j{ g2T| ˀԽ%@YYrMsq) \-ĕRu*))M OO y`P {{uGiɣΌe:f[T{ld۳ct2S7Ni:i{= Dlx2ݖoL;:vBED۰kK9,oq,C9vѹ<&4PCC~\}Su_49;<;2~YYy2{> -W;"iY>v=&{,kvuͰM/uBSLڼ;QfҨg 1rJZ.7ޒR |f=WOn~5(g8j#~:::-y3acJ4Q@%L~[)ʲ+eEG$pې|ygȺ[Xjxr BZ~wӎmTK*(,ߧ8X,:]AJdm?\Na7!J_^q6CmIP6Ԓ˖"9G}d!7X1i}^b|,l||ɯbtP3v8ຕT47`()"2V=XJe%%(dyPYŁUV=;]FeՊ=)f>+ {^օO'dr#~irߊkgv=gvny2zss`?f?GqVfHҗ "$.F6Կe=T YScɔ-9* "7E!Qn >uYY 4VT6OR])<f ulf7UeQ-H+XhN kwA5LfSY;NUS&j6W=C[.FTΞ7C +ǞT(yx)8^tRϰ|p0)7pۛ7[\Ad.~r.ݎ=rA -wo:|({/;{?m{>CSnpD:a&??xMCEM7:@b6ĥ^v.RT#OOO ,=cFZ祔vs²[2ɪ$%GFv zG֜~ AO{ Ҕ ʳ8VV%nGUwo*(׋q޸WFGDs@NYPd96I[Vj)&(fh+b쉍Yn e9RTS%Ы~}?뗝x@Ql{[W8vn*<%N'>. @4WJLDIJBk?;1E/ x &^]˔z&zr:VO(+]^EARj::lH))*4$jW&|ᢢrzzҮW79/V7R yX(j439 x<C! :t>_wIOG3q;n@g)-O$k;wӪ,wQ."xonm=sg۹'1͕IfO6X_ώHlPsjkl!71WBԵI$"S #@* ҽ1nVK@zrb`#%~A?n(jѭ679~nRէXN -͐dbkDL}`sҴgv'UgۭZj\`yhz?o&((Q5QE/*T,p@$YuғGM~y5oYyB}7 @ Lh"Qy۴ظDΆRE )_BشYޖ#8"@%E( H b␆^@ 3-.#<6 s̲ j3n-( QXGCH3Wr qDoNm]lzE wKPT6$-vrHd6I,8?PB@Rb; 좊BTN\k)kQ!.nS'8kE"DVRS㋇"tsTtI o bf۪^T1ңݎ,JJ(VU?t- OC3 u}Vү_o!8NjE nsV:OgKm-B (pE~DƎԒv]J-}Y(씗<1>dRӅX/>^Vʬ3!7i.x9E"<&X co"UTϼGfV4,=U8Įޔڒm5wyn{_PYy,pD"ˮBi޿yρDH++^fPצYyEvh wW <8<@$x< <Hm>ɖhC>jb뼽'vN~;l V WB P(_x9eMMT]F?آغ;QUUi P/_ߗbnd_SoH(fq Sz$?ZY3} tPRw*$y_ kZQ>PXX:@1hikHoiI?JZ ^,=bYDƦ@ Բ;+IVVt. ,KJbQk/j\*>iAC:+=.Ÿ %IVQpǽiSGLssAC:+.j1(+ᢠ%/VęNWVV. 4%}heI lad0+#/K<"~'$-hr>=k C6շE"Qˣ۟jTr(o@u-m:p"UoO4ɅKhPK9+P[ +63uII9' =JQ3pN$CS2Njx-%8O7ӨQvLL( c嬹k;J PV5)[v 8#)Ie ŌgKLJeD,LJjg"q*1fqZ)GAmA Thu vK3t4'_9 7&VJ6 p&io?lWsv>8Ϙ+Lnk;~}dC;Laah***d8աH}33HiqZ.idB)mÑ4mSsy!]&K@Xyq!ܖ%1ZǓdJ:+h*D@y|bia|"ep" n>N&jU+{TV-̸`ek@ D5oO3iw3P6Ci VB(*T0T#5ݦh mAr0U{hZ^CZZYFVõe$y>fYώ^vp:)x~TK>]~djX5$"KT׶}g/q=I;Ʃ ԕ!zSrF#oY׈f&Iu&xI^ەZ]ګnSujתg^2o)^,%\Yro4=XQdb&ЧQt[={7q"Fk\Xgǔ{6o|XwͶQg^%Rc|k1 .4R'T$<ܴr-YQALoqԇfν4U/v.Ҟ5YL4fM7,G {r=uGwn>znNS).M\zGivʊr2&@KkVs)r2%IRs7( _XTs7TҠe Ôˬ@i7қ9mѺakJgŽES?{gڂ5ʼn)^ x)XnTf{ ?9Ib o}WF.NK}f.'fep% Y$y7kO|#MHzzQ*-eAQeâUyI:>FTĥ:7LKĐXwì}n[kg-?/( yM%]h\,'&xp3o .s5 В4(*ה*ʗ ` ; I,AW$ԓl0V=(ddr麺"FvavּkU}m¿ ᳗T_zC+/s0f}uE^۷)Q,8{<|zc I̎V0;&cMDb@Q@ks\Nt`!*k;v)hUh-OUTU )JuȺf&lƭp#q?|h?Ea%:[A`+B~VW~?;X-}g f"J&EB!hvW߈yl嵺Uh(SZQW7O@̩[̩u_ǯW㳗T?2(-odP_d῭kjL uKm7cnt2e%}H)zy^o']jGf4l`gنCȹq6:}C ʋ7%ј=HvPk;PvC_LoWz+ԉ|o?%ΆwX7ce"'~ʺtDVoY\On* k 娳k'xDi8ěԆO[<6l4޻E\S֍w7o]tӛTz ^%AV/ķa3EOog=9wgj}7 wtN܅Nwc<ٽy']QbtomʫNOn }TH'KN$l// ڻMq֔EK>|Z"R5r@&2u`|A8G-uqMYgoKE@ձ8PN\xir=ǹj/ +Ӻԑ7b 0L$=~Zd5mgy[޿|5%1IAZ?? ~V< JPT":#<Ezmj(ט$ACe4CR Hxm~W{a'(;?do3_K+?݌rȬ%O6oJhHav nSx,\ ]Q%K4hTي=ts[Pe+yúg#TbdtMt۾xPE̶6?حJͯ+Lh SgHN_ g6~%]efo%f*cH;3HKʰc%@5W6ڭ~&Rz^ôܻTJ?pc{H9J3Np#D%NcAtPD 3MH.@h:(L i%.B:jUXU0 JئrTQנ\SuBNS1>4C\[UQ B\_!ACe4CR Dw=k@iSWn]7$W]It9|${!WAWiF:@o"3E<%'?Hs 㧂(#/~VJxT?{Pdf3a [Q$C2QBLZք xEPъjhs"/sik F0%i"DF_(যKA+Sn>ipDGйECޖ^Kh#j2.#ρ\, ј5 ı*$NIʻ'_% ],ӎ&rmI :u%%YD Vs; zq.g4D'Gt[n@C*$'h5r0ÑpJ֣ +' }i@猿hWceeEL2@%[Q?H8jBK =z&>ziP\fŮ 4VyU_ Q1 `Ն 2HrI^>=&#'(W^1N#u5CKu 2+١V%3}:4;X")>EFh]%%UHaGͷ,ahF~Rd:]ǫ 44)4 lRa׍ǎ >4C~[F$ACe4CR 5gF۸nZÞmdW8uv(;455)ӝ! 9lDs= I >y˖ q7lyXi9bö\(&੺ZB6p "K"j}Y>|,Hm֭]b N$ >. h˳RZN*<*CtjmEp`T`9rqzv@@yk`y_;v̾ۚU\c{5]sDfSx+]{`HE :Sn+;m"BU֮AQiuS#ж>XRBʈ#G]^.iѕ4X]M+M,(mC\ 0-jѹ..&X1BcNMEDT1)~SY",Ly%[,֘$ACe4CR nVqdǙPrBwM#;{`]o*=핎dgWo'7Pmzd's)I:%^)L},t%!k:o3aqӷs<ƴ'wGV9nCL zP &Ŋ*6M(FaQjԔj P&VB9*3~(b0KQyyJJA@QI&^WVLtV*`=er B}9y/cKp t϶ AV& ̒O_2?~,g HMm) GкiOU-ͪy\qP!_roVUK2~.[줸LߜWZͷ~CTc Ӟܷt^$䔌Q'DQ- .~oz#u/s7'|%VHGi!j8xhd-dkޛ{OAnVmkvj[uuuTܠ((Nd7  @P<>:箳AndP#Z͘/ YMV%Q;" ̑Bң %a(,J&X(JP(@PZK+x6!UNPQQ 0riApmd @ B$88`/K<}-ZfJ1^w5.P5`Zn]Wra]Jn^\CP*|p@IrAS Ww|[pGQ=]A@PLqVx8td=4R]:kN;Lr5T~L.sλ{[m\Mbljĉa$V Vf!oY0&$J IU} ~rSʭ.uC (n./NN?^$о> _An&7uKeUkR~yRRԝu~%8K/fhJ"ҢOJ5եW82d9%P*U[-*_Jxi73?`nc TScHEy?>8}(-jnx-Vtf˦la&K^J\ q3F=}tdHdLu JإxGȤgP.W0$ 0h/WVUĉVxZUO&٤Lp∴3;X[CAnjqdDkxvPpfy@}"Ik[_u=u-fQV: %Qi3MpH,ohF؋+qÍ(QKedpfEXݾLkJE(jJĨq5ҥ~b0$hxLXR՚:v~eu׏/nsc*g%f/C0jȦJՔ4ãT*VȾcFnj+L$5uFczcb..t1YE?a'o۟'j"1ʠS[mҮ* OMj9IQZnVȐ>uhaAQ%K</½9F # $'t;" ̑Bңniip V][u-Dvu›gFq]Q)ssNdFBG/gEfFEM&b$(֮#D2ֈL>P(l)ƙ; =c .ߩYB߬!ۦ oI,Ϙp皚zhtR 0jjLkih%͵4{VQ+ls 9G~!ھぢ0(B]Kr(k❩$sD`[ -]#)$#8I׻xjhM9XP=!! c3^K+hi @YP?ڑB&3#}8C.E:*vʘ'x23~_"|,ќwIW:ipC]/_:,#C&sdcֹ@OhJy/O :N$>[gK|< YlpͭȭVPUY@8lwߍm?x/)TMI_o}1{>euAHfB0f`;ҏi qK/9/l=-&R/8V# xu}!5'o%g>G2 zl-11̿7 lzXr%8ެY`Ď\(Izk7$&$q/}4ʛKҏu^8q/`,WP={DtP?4J_5'4[=. o_༹IZޛVݷ XN6Tw.)m8}=ozE2,LHW Jli׸Zu 1 ōcicCk]Q,fH_ATb 0WG>6U3 ]ڷγQKvhA>k,&Dk 3>:2n2G Izpp{|?^Mvԍg< xC x A 4@;E+﷤ƿ~'+w\O€Qa ~9G5c Lcbc]~^ՍwDLR7#Ӆd#3f̘y$#z޹o@ 8 PV2&dݺu>ZpAmފo% ,\;@0` ѝl*QshKEM(amgybNP>:2n2G I~f̘1ҘtZoSQt*m u|by#5*;mFnp=1}ߤo';" ̑B2ޑ3f̘1cD%m"I?(/1(:x3f̘1cƌi1̘1cƌ3f̘1c,À|d`,j?yPGT71}tdHdL8n'' 0cƌ3il$9i?+`G#C&sdw#֠Ww?(bA}sΟQV/mw"%=_Գߐէx}ijʽ~137RK2ҥK驧x-vb^e0|OBN7O{':%{k芝7/ 㡺_GDC֭pՊEc|W¦-{kG>|y1$. ѩ{߰fV  AGd܊e>~y>xaFji?lzNYha⇫fϰ{OC2g_GkW\<1Ј a?ѹXFL|BBP;a/,_0jf}|x @dŤQFN讃=;3_袷xI1ϟ04|IAٮb\^SM5;" ̑B2aˢ IڙѢ:,<^٣i'O c?SjoGLv=$)\9)GH k9N6^'j̣ZjV%(jXԤge#C&sdvd?k?+C;28xa'^i...@Mμj YW=wn7un32c*ᅰ#xĶP}aF1!a%b˩Nٸ'$ڞߘ-\fq'ډ]\ӳh:5v\K*g)F-G}IuҎ؏k&8𲶮Tx˿|{=/kGN.3?xnGqVs_)l[쬆 IDATT,V1d2qL)$o9R5@Mꎍr9w/ME:#B\>˱Jʒ%g᫩AcVXo 3[=u0K G@sC[KߘcX+Z_Ԭn4__z&n#iŔ+7[ԀopsJUM &*>FD0OU<"+-=m;`>bѾd)vƅ<%3z[Il%g/ Bf>3=̲w9e hK!cy:)OJ_<|gd K\rc9jJĢnsǽScx} owמ:MH MȔ\֙b!!Mi5y եuqvL(5Plԕ!oLCq c&qbPi$)Or.r8lj 큳>%wN8LQ_31jLaWr#."WT_Ks%s?9|qsn)\'ͩG`4`pފ/~]%>زa^ՍwDLR7#Ӆd#v fc2t Vvj1f:BE<+GuWLrL m=~fll[1aC}JwKL8r47MvՎ<0^'i"J{Rz=؂s{LvK/%;ZH^1t﷖Šӎ 52ܒ&'%Ip굯8Y8<~3#컼1I)t;҅`?,LH(Vδ3zbZߟ+6u9Tg8 WXӛ<=/J.e풂ڴ| P{n#Iuz$%7q/j}5}$/?$Tm)8}JXnްa7~9Ddy{1=6G`{6CB +)vXъj8[7lؖz 6lQyPxIN Pf:W]z 0F 06&!ݼMp_\6C'ƴ]ܶogrWiN .n?ue 0XJ%i8eޟ7|u twq+TUƠCDx{DȠgdO0 2^nY} d.8u7j:)Ш>:2n2G xGV*4ի9csc\KB(Jc ‚BѣW(gvr 6/---k?%Uh8YRd_;O爫!8UVjIlߘ/߿-՘Q4Ƨ>y5MW3rqTՙOWqi{j;0J{7'b"5#lT#wړ1+@!{.'k\MU ms[g*2 2Ymb {0+^aE:nd1`e,o)mEeM(e^Td5wHX fΠ"AOq;AdR1|MiYY$WKkuvKZ+)RQ½sVT`W֪lnn6r2rrNmkIIq@..ws/\* yTLsvܭ+{U$g'C`]=p=Cyٹw xRAAnn A5 \XV&$iB ݯ\9L>`;=λ! eC5 kq2 S8ưv*b/[)CPU)fƉF>:2n2G xG.;곟ޛX#D'WŞ"< u2ðԏr}¥;?LvՂso,۹c_X?hDDaʪN1ozjNڜ~ޖZiV(0X7>E/}ӏh|U*Dx/; 0o?7 @jkk`伌p'HeU`&P7$e6pw; OSE ;Z(59Vxq\IvViKȳOZ,'We$]&M]> o*r.nS{ĵ.Lv^.0N-8?RWŻ! 0B΁ w0}g !2D+>ΦYՀ_qR̙*:z^pm\g읡\ +e-,|H>`{RBQTVM)StS/ym?s6&w[6#C&sdw{O}0[wZ/ 13Eu:cijEkUm!B.do `pTU jW9Y|;HQcSkM/%}isV3u &zRP8.ܞyHڔM^3iP>|ԗvɢ W `Y%@UWo/iEE>^1WYQ_WVpqA;mwjBM6/>DtjLٷOݦ6M>Tf1TSK:l\= ˻!F~UJdEU:gQC{TҦʶ^ߟ8x8[Y/VΞJhQ'Cv}~wnSeodT/@wHMYX8X6 ʠu|tVD3ыjlÀ: .`nV:kԍwDLR7#Ӆd#U>?aRx-5m~wRG+w愰P@i|jٗ3g-3=;˸Tߪ}k~VQ4ۻ#KSR5#^ּ;}-I!r*}_+Spnr$g.S.v-%ۏ7DVRܑZJ96RZTl(n#'+GfMBSx L#ܥ̺=rd`J ssxSyEjp V M"hv쭨:H|A#5̑B"|fn=-6Bvr'  n+Кl|LA:*x10U(Qk k[`P/eCCW!Is$!3 KT\xi!>3ӒβeCZ9Vant2 Ki'n>5a6DZ N~϶ٶ3ʜ3oI|cbPEVrcr55%S;ܞB&½l\ gq";.T3tZ^9RrL  KεW8pcŷ77ə~IΦ;" ̑B2Q;<ɛܔ9DV%n{[qQM8LI_ow%[ox$?l?K?>Y<: ;u(ں`_lՊS_ #^zk ;LXzyۦSgi73|N30RpiHko9D9׹,ְ.g<5/cY}!H ? ;9YJxrR/Iܲ o9 yN\xuuuj4>xjԉ눔y]B/R++׍\Ϋ:'Dddkڽmqhsx̩޴O'k_,\v!\y0"kMʠk־$F|1 d3f< dI8JeШj@ì$Ny+YXJ@@F#||EX^Â&}u"?muNޛ6cs~.ʝ&cٶ׫:dy7VO:K _nǂWb-uM?/tԃZbk]'5fZB Y)O?QֹE5~Q'~8e+An䲗l p };IREXOȲԓWT Q^6BJ}Sd3xڞ;Dy9 ysB}xu NLGƛWoq{*kԜxzsRE˜\e}tX ͗w^q:jѢ;k>񍉢\u5%fZb Ip\B=fOyqv9t!Nvd]|!P@w 767W;v~ !G'F8ڐTʴ/ag7_L:e)[~QNWiKUwԻ㩗x[$>6hΡF3p~mF/E%\0eJ?gr R.BMՍwDLR# 1]HFgaˢmE-*U^.K{WfgӦWLbNYyXdL$h7dl#kӒْW~YKkR{I#}[sdj)WۄOM&5wO}иh )sw"  \w|d\[h~fD QyM(2&*lߴGZp`XU jVdm6 '1a!aqFrCqF߽,Hkm>3pF4zбBKGr<_<'5=%ŏ.+&oS9[ta3Ԗ|0^]6A$VCe =~ . '0ߑ4#C0P+R\)C^-Ul!n#ȐzofB2:gmgehs?NI{4:u+&ۨr}՞l~{˿|{=/kGN״׬Ʈݹ:V|嘨_A]HK庂zaܲ%]^.f;LcP]&(2 ԸLI$*&}X%5%BՓcehjHXy LU ):5czR t|\*r*`&4kȩTܥ{Ҏ>ㆻd.\S; Mxi `arwDZBaH=P2 cOQtX޽7`=-a-ITq` }U{U'HK>uOHlo,ΥōڵCf9̶c'򴋻Z6YÖ; ! I'b0!ql3]%u.FTM@&NLDžwΟR-`_>kͨS܋2v*5>",ƽS⠬jV`1%k%2;z8_iaӼbgLpShd_6eLwĶ7ƏgJ*/'ӄM}o2`$3l8fMZ|䴖n*(qPԴ*]&v}+b^Qave64oņ&%4}&.n_%2q\n㍗#u'.F2ՠ#CpD! c'i9/gY0ΆEf+7HyҾgz;e1w'U'IũiZmEigVu/e;$9Tڌ]GrUhTXO1w<:+&F gƮ!Q.|tedx\947MvՎ<0 xQjܛ 1^qfv0Q eF3viwy{+OD][Ig_筷]Tvl@"*}ifeOшT D81jk'IDե'tEHqK(˚ϝb[OtEQ)Jܐr0sBMuzʖ^3v }"9~+NfwɭSG}ҿT2p (Ve䄑yrEM~Cm4 ۬MݺiӦ?l't>WSf}6|J]x|ӦM6!c{ S]Oޫ#uڤOt9{3Zfh˿xM3!i5tBDKzZ4qAMj.GFЙth(]-k/ 4K]փ_ڵ~j\ߟߞ$7"q0Rp.qL4u:_ggd<u dR\]eMp% IDAT6 KݺG>-MoEqm͉e)h0Ơ$*9޶mVM.KsÀA).b.O"y>Ǜh)pL&T-JLZ;jD<嗓a@bkc+(V9쎒>lU,τuyk].~ۅ?_[K*Js}ۭo񼓲6⾟V=,( @-;)چM3;>agwxJ*-}h@x,|4nQn|i{z}{$\p-mbSG䈃@Q7o(IQnQq# V(QqE. jtstd"-8جWHp\IKFXx5E7w(}hm|f8bM 812PBYPTz^+_R74MD~[CЈ LBBDV|bJRl+QkN_ZkΝ$Щsp<.B#YA8+@GLJM?H"2yFxόt |-P[[JX Bjt-8a$:IBQ}jn0ׅBrt myf#wٸy2ܑ]/ ~\:`ח8S-,A<Ѫ΁#b' IsÀsՠVTqЊn] >:2GbϺvx?};h݆V5a+;9]Zyaح$v14M<}6) :*q]_')i}};_E nwJ~Ԛ|.X"叛M^wȧ_yWXBz+<0amNig]%4YDrZ9y[sU[>,h/*[3dyJ&0)J2S}_|IREs[ugMD .i8#A .3^j8[Γ*I c7[Em YvSQykօ=ÒK_bN;i 6M8 +Nf&*nj(&cM)d^g_/ U{(?^jIUm tcl[gM +j-|yMp.O+k;&jw2~ܲ7g'IMBIin VPY=҂ܦ|ul:j~jE[݅c|ϔ#N!4Px89b3oxs彿n\y.\;uZ}B(+ .x[DCm|K)Kc[M>~VBΥKͰ:ztW[tM$+ДWNɾP]H&j ԕ[Օm2~)w$XM4-#Cpd0C/E'=","? __}bYΣ.376-kk!oڵSnROD/c 3:b@<d8ʑsLP}tdH fHL/l]mC?G/s7?ldyDЂ JMZ(6'ѯrŊ+V:?Hxͼb?`5ȍ1li]rD%i@0`@IL T$k*V]R)2%qUnpڽ!>:2 G3RO5p// TQq 1?1cSJSjP`$R;|2ՒHN(i@ 㡏yÀW%XA2uxpb^Hё82!"3E/~1i3f̘?pXmy J.k/k z$ ?٘d1cƌ3f̘1cX3f̘1cƌ3f0``rD~ IBuGG{`k̘1cƌ3fS? HV1`&^u$qKёA^82!^jSLtCײ?~~Oyzyd3U/D P}xG:Zw7nX:2 G3_SaˢY[@[HHP*QM X9[ъ?\T \!$<8&"{[ݹ@k 3oPɻqr2@Tq&YtJm=T)ih#tk]&(.1>%BE}Uz DĒ:az>)m̸U P 8 0;xV.)GV}]#4K=\X!{98}W' ߫16G`=`ۿ14h=4$T7!}tdP fHH v {ͱ}=<0'a xWn#=bc,^Z4 |nڬ{E<߮w'U_^죿_gƮP6Է_* ޯly:xnFcHT&D"kI6"d)([' ouIs0ɋ) VqQ-.d9)I[%mid?Ҫ.ŽW:ԭ<Ç[ŊwP{*=  lU|h/wmٲH#blCGv>Vbf}[jٲe˖-(A۱+nB[w6>| h*ljl%kd5(v$k߮,}j",ajQCͣI̓U LĨێ[RߨrudZ9rVAT4mX"tkdO0? TrH*WJV!]kO׾92 G3_Sy# uPa_EܫeWPAN 7Mp#0{v~Q8RϪ3 ɫ~?xYmw|w?r%IdAڋ4ߜZ>E:{e_rHP~xzf=xPH`8݇{E:!H8cLGw7>UW0Jج&x)&]W&?{u5;3 [X;"꽷X.rwqDZN;;K\$ Q!@ewc6p޹i^H尜zE:YdhiSPRxlIcPͮ$񋏵P_jW(:!pϾW&-ċ08Zm4-R2OĆN]+kPH6Y+֯):} *AA~|OLj]msMg`4nzk=ЈBD^??X#F5u]Ĉoq"pX 1a0t5ArS05 7t {t|<dJs4 B/N:nvJYV*L6FR eNi.8<$Xk ~{/$X} 8ڽfl#{K!i::'A+c/,=O&M{v?__wfo^aU'Z{tGl6EG?'aC7cLW*FĭذM( NQCb?…9#S/ 9PB ֡'tN qqî$XsTTaDچEȾs'W͚܎^;Y?UJ9ZG 0.ZGh"j47 EPFX2lNr뗭ؼ8vDW%3h1SV8LB@Ppâ}hS(.10=Iw8/xp?$V MPfc(w|'s85(28a/:@@j6ԍni ,͗Av+lA0̝D /_RA9p0#cs9bܚǑLin_:<׿o]9 Go}+O=$==T_?=ޓ IDATw>~}'?~Ӿ0ׅ :G p <|eS^{ ſﷳ]UG^};_L!o^>_o/|[s`#H :.yF t7'6>?@c?HL[c)Ky+[/C89ƒUNcEqLe3M(%҄ kJ|hlm\48,LԞ_"\he:,Yk׭ZUMNjggfa) ,srxh5<.LVg[[ھ ǸTJr.^ '@Oߖ;9,%kߟJ1K ;X1_t&V<5Z5=8'YU#I]qG4z٘Xյ vr>fUg٩#e XkRoXƬޮ-Zsi/kW^!o弥pegñ+C:ңiNհc˛<'*kTt+fEw'=qqq ܦ8C{yqݺwY8~- DX$5t4CE0[摕I9;>~Zaq &DZNPsrG% #@H!)$FABBrgomԢTd2u73=#??)ZSv}CBBOЯԜ)@0/[-ZPjH!)$d?5d mJg1Yc8ɠ jN[H6^PiL6RH Iᰐ77T1,4~L\S `jɦcNK1[dZRH IE6H$R(&c!ewuoZ;Yх 7+]$$$$$$cຏ jdЫ2Q^}U7A I!) SHHFLAsΞ3-@M^EjeKuN4nĂs#Tܬ^wyqN6X7c Ofmښ;ݙF2D3ݐ,\ t︵K•u E; u8uŖ/@ I qHaLPpЦE'9\\.BmZ+BMk&ڴC/fP,vXJݶկ@N*~5:pOs7;C:Z{K(;&iW뒌cjUouJH!kE2N L%Y.V 0׮Yݗcg)UktΨizIrmPv JNTYԭveEfLS[sΜvpkxV3ř{;}k Ђ3yg>ٛw^&e̎®_0/!{$F_fHGֶ_݅:w4ݡGg.r: G-~W.bDK3sDx0h>Ϝ@sڨK={ٳӍzmS3 /sn87n^ޯ~僧}XZ᯾:5/-pz3\,!9?Q4H{l~>l/F5RvOܿfNMOޙm]fSnuu-.0%ŵ[0q$5IHnqڽ}Z/Ɲi2#d r6k1EKf+~ sy eF+_MV6%dž^-V_KOcAMK4faxG#HʾrZ  umz+jan[^*3Zڛ9"t2Ilt-Ve+A<t[~vJd2Ǥ[k M0˵I6 a}xPkoGW֖]5zlA\ӛᎀf-l2AHNA k,Q>*q㡷( A[5'9EAc,8yԚ OL<%Gc0wkMF a>>M0IkxЀ eőrs=(rrt&58&TIhSwD]9"N Y-Bo-c.u8, @<}m7q\ uEYnh%*(tB=ݍ*-ڹk٬z,G^bA0386KrTwtsƚ*NѨ[qb3kgck%x7Zc`ZKjuqe,^D7P4=7wdך! &?In#Dj_40jw}pːM41 #FG0'B?Ba2b)Dt.Aq f 2ɝ8* f|EѡkPr¦\p39斟D-S}8v\fP/NY5"pwF -s\#TMmZ$܈CWhokGkFsL1T7þ+MPsst}< Ly>}} DpA(P4N4}}FH.Ah¨Pa_ӥ<4j7N̠ABqQa!"[Ր$ۛ"(#,DdS9WÌ$lt^f|mǢ4^+HFa}}$yB6pBwǎ7(Ӫh圏=?[}aPba(wz' Ug>'{lG.lD#N fa)krS2-\ӜӢWg:qV&~0%Gc0wkMF a>>M2$'m#T͖ aKVݝLG,eWe~' 8Wln,-qT=D|PJVRҦ W.Z 4='\nHg]jUu7/؝՟kTS1A4>d?pdj(.Yxn6fVv\*ŋߕuݵ\Iؼ{)բ>S /X(.|cBf/MK-;E#=]y< >Q`цȗ::9rYkRoXƬޮ-KSBg`Zz4;͉ji>oTT,J4lCsg jNБ[q8rk3qƽGUUIƋhpycIn9Xc%_3`%_wtn0#gGXUkS")N[HMK$@E3BаpH {!t|dJsDBBBBBr472{{ZwٽG2.h␐.h>)(amѦՁСʭk :rk>Gn2u9"H\C?tN5d1X)N[ư4ɴ't$ABa HsG#q. S#ܤۗz7)K$$$$$$$$$$$BHHHHHHHHHHH]-ֿUybm'm})gof~ 3xxSɗs|Ó #g^z5^C/?%yi_f?w)JHx7|ث;Cwrھze3yƻ=7{~]jɁ&JY cVReaR3&J9K\ԇvjǒngSǮ:ݚcMsߌik]G],ܩ@xPtZs˅.:S%ܵ8d8J)2'؋E!˗+{,5}S"!vGn29xmMnA62gob%O_buU'w=#b_YvNO=~%=wWijxQbQby/Z]o_8s41c~}AkV|Е?sS-| ^,wMwG1TL}9zJxSp"72s~?+Yءg(/J.#<2#ӘԪ^鑒FDZ_Ji hOv ? 9cΣ"Wuf p壇WdXK/{Pּwt˅?+W;[}_<aNhh׃_I~W߾7WFa^#&:^9BOo=ܧ^=҆C3}_=P_o_?ځ;ϻڧ:իbN}R?e;ʯu/  }U?t,`h֚/\fP[6>L(~ɼKuL>Ye&z;qNɦ9Ib^M Do:m/-oae&`Zׂ9ߛFh-rnxgcϞ2J+  9;Tu=LyWl*nNwR;xa \]SZR#7;sDK^%`PFo>PT6'e{wi⯃xGlunZX(~?.Ŭj֩ ܐRCYU81AQ]}O S=I 8يMfW 怂(Nƶ7M~i<_~c=cw?-BQPfmI|bgsG#wp6 Smܽcw6ӕ$*u0'F[[oлG^rx<+%:|:ԃT`F26#uʺwO|YYYYYq;UOXOnxsO^} ~)#׬:Ycny~|oO&B]2>'oXRɃ/R oJvw<1xwC=rQAˇ_@I_6eee=CuvyXy]f5˟o买'@]E׬s `koT.LY,BΌ,k,BP\>hPe9ODl %G=TH :;-<}޽Uw˼¡ bQ(o's7oY{RSI3nԆM+ٸlے8>o-YyƕeVgF+bqE~:-E7N a0ټ#zz5aͱ@?LI$H54=w :rg>Gn29 %޻1[}J/37{?_rrT}P A˓Gfc뵥O;Dc_>>U_M=ǚp~eVTA{fdfsMJz{s撽oO`uuU_k/[;\XtmwOӭV+U3;eXht ӨGވj9r]]'|]Бw,H c9ڢh6G2m-Tb E}ã"BCyƆF5#|v6:"/gm a45?Ie ЧPL&%46IWń~àP m/A^o%.]c8U"m-F<`+jU{aBa]% @ȥVkU)VpBCgeK{kwf&LZ  too.4nU f>a8v,7bHihs5U;UҘL*UvՖt%\@Yc]8Dpw@hPprQޙIW'lt0ѡf ?xA/>_0y6Z5j);{^ydmY`~GLf3apR,fWU=/|ٝ~uf P(šb8ۅ.nQB񨞩fmE#6m,B 6Vvp7%\эFn)`JY(v,bђ0 IDAT@EFu!Pe,]Y*Ox '(:$;EQv̒a8wq#_8oÖY/x{P<| {`HTI>J[ ]CiWZƚDiZ^ѪEQx-N##.;wudlFT[t׋7Ƌdf$\ n%0;u^2RHa6Wш[`qA,%ǝNXs,Oz9Xі[ohkPĢ4ԙ~dzB[ܽhNБ;q8r0 `m>xǑtJaѴ^v~5Zٽt_b sOqYӛbbgĥ * ."#Vgʜ5KN>$zz6V>I*0vl!dG.^tqi5eFR OOU,lm߽I'?r{=P(BWy8\Ru';zɤXb)A1}M#Cgqث&#Ld[2PA BdHSzlomA"B-܌&FFQy6"#V{&8T~pU:4@/4,3}$Po|f(٩zèlR 0,ü8̻Yq0 ‘ ДSuogUɸ3NH|FkNVJ 5LM@z:zͨZ`Bt&e0Mz2I} @6h`/(P/'ğFsl~>UI1y0B|j҉BpͳQ L JSK6N\iA;͉@?8 b`$֪.6ܑC&ȝx hg+p/}Vg{o1.>?jގ?',Gڡy}~cڐd7;iCݚˬWoLc,\y,*6P~_{cƂ=t>rK\u@A iwue5JYG55 7"W>wx+͹_\ ŸC;Fr?{þLsgž߿}yZsXFIwSE 8{W&gH{ygɽ`iH0?cVVpBJ{{ .lw\Ww{~Td;*s͢^Iayr_VVv'>YVş`I W5~B m)s dnT*hS";|hҭ Ԫ^+QNinܕ E+z>V.vxJ11,\)Вue ~49ml/&j(/m2{;XE-0(1kY+6ũj J\OE[2bvh,/KZt}e嘹XJf-Xt}ŗuTК4w鲅¤_h#ѭJ3e]L]kduCKUzڧiNd";˓pV)땕eU,\eU*5\'8= .,V;2+oќ#wp6 SZ7PTo-?sǾ5gelRw>vZ|lʡx}?~;u9vl%à/zm6#4$kg\{%ZN/l ~nZ&~9H<. X]F^p&oRۦD8m#E_100 tf` Lt␹3AGm@6G7N:oⓅZ\_p{nq(O[}exw&!#pԗ8M۷mݺuKCWƴ3MK/G`l%Lp@?0Yg80u-X4bl]l1#Mi] Wo&1FP|A !!! @0BXM|65iu)rÌftl ¬7[CsܘǑLqHl~2YC;r3#?>1ig~>  !ON~yKg 9|p"\oU$“-nwı?1&?sƟAn/{gݫBQKb^ Əlݦ-[6]:7¾WJ]f[7_ רּrggٸe˦uE \?ERvXUI!㬫MI'c>|NQG:%6 `396q+֥F'31+o޲yê$G6Sd@m_tbׯ[m=^x˚%벒"y(m_g_ ?e뺹1 r;bavY+f'g39z/5mdftն!s9fܙÑۀLmn 0>^[Sb粏XwQbQbZҋ̣]q0-߶_Eyj.jx2křP\fbxǐ;?*u强ONmE:?ő*`$1Ni)]yF }| D.XbfE'3 N! ٶ&u=v{rN90$ma޳$uN%AOuw-fPzΊ\ }ݏv@zru{{`"UŅR5旘6?Zuf[ȑͪQ`%<am;}ZCsܙÑۀLmП)}jMc~:р~gJ{oBBQ}h=%/y.Kw|\9m`dݻM?J }G?:q{~:~T-x91PPD7=# :ɃO7~+^9ҫGp'g%z{04-%Eғڧ:իbN}R?;O&dXp+>J(zQ lUު4Hh1pŊAXd0@і6Wt+fh@hP7(hIq ]Wm!q!lKWuEi?ei g_kF' zK 2uk- +McQV[Zgr$lzF$fIK2)pBB @>ՐStg"jY˵+R ZQs,vs#pNfZ0$R\ڪ!@~q$T';C>XVMN`Xz4,\N?}b..;q^Qʏ^6'ZX=Ξ$u_-Ь&v`Ꜥ !+*j&'IB4ҫv9 70A"0qlWg]#7O8anjNcU՞,i9ȻPe[@<;+Q$+={Wn@(X 7@2}O&avw4uk2|5}5GzMt^Z=ӯd. 1OFAxhuk#LБ;q8rh6^۱;{읠UOS=7>/?<:04'ljʗ?߸saO<1x⻅P̮ry}`ω'maZ&3eTs| s3ġr{%/:u;ϼ|,zr97FwS}]~R77q .']:γJPߘ(^j轚Sb/ x^ ƒ2KM7nZ(=_iʋ\aur~(1Gu\ޑKp| URC9WbΓ4}ᅄ`#aEMyǎ,`Z )>bJCjټ(=8_Gߟt 4=Z a'. Ar͗x%"T!|}iҮ^S24߿p¢..P+.:|ChFiך@B_}짺ҠtSg'EO=|tZW[joqp9!S_Q֖͟Bwsqk/[dE3#0ѨʉNܰjɺ IH+?N%ƃmi 0<3%k4E?ZAI\+3^BiKِ3AGm@6G#${w2]{˞o:=0&8:Z^ap>H7T*+? 9@~ʟ &>ֽkw} m)y[iWǃljyձ.`ذ.~`r-s]:cAB98!AA~^-Z.vQ9q3Wzy/RD 3SCe-chgsyFon&B"X> ymL`pΥnӘ M`{OŊW_G29`и]ű$T60jR)D(N#i.8<Z5E# {aWt88ڽfl#{Kq`pbȟw!%mK_ClCA1A/>_0m[*P(ֻl#b_mW`꛺tZo(~ r ĭ ]=-MJ3fYcfc? ZUח[ EEqLEPb8E4mAD!#CmNnwXpu+:[xdf2M9NՎ9%i@P'f'$P1I0Z/:rX0--;sAܓkp1 zg.I5 .w_yoY9ڛC¦;}=gt.Lܸ,Z0fo]|:?뮕'UՋ[:g (U`lSC@o<7R,*\mY  TɺoDc-di4`nJe faB*5M/ 0 7*u9*ͤSE|&eiU ?@DzA( !3m :b(IL >~~|\aܠSBz[g0xIndZJfly9Qeؔ1O'w|NCP_sjR7>J%>j"rWԪTL !Ø~"\rv5 nH! \0S@mᾣ9;_kksqJQ3 ޘqOPM,TJ[_00ҋa P?vr F5_꘴n=9)g Swyi +(OF;9~iUK@@:|߇JGg{1t|M^ wsMtCŪ7_*F.]j\ Ș: ȫr [;2][K-hZ07sm FMOmyAZkWmIª^,`'>-+ʞN:jXs$ C 631j_sVkKKr0Uz֯|Z{*+I70pmO[ɵ/ 1ܗc˛< g%kW˫r>W˫rFb)KiCM&ќ#wh<..ֳdAK}E}dhy/~gߏwx8|Ӿn;ǎ?$wԐ5K$,29r !o۪{%]i K{;{:$B)o{JR(B~e}i e%PBg9qx{˲$kquَdɱL_C>:|sչ9E)hk_U>𛍥Jrs}1t5u?_خ $E\+{ 3T@$k^i^k_S]qx'$LױoYs*V|{ ->ky:kxu^z3dyiR6vwۉ(1nrd!A=ƚVSlm@mE{:#/X?pw@$ݖ.{]Vj>8w3|ӗΔ'_q>zUnƊUytAZ|Vྎhws qVyYmmU;"*?$ŋsL455vIN E|Uח3 TRP)峨$0IzaYymQIkOF 0UɦRVK屎s$KQ^)m0qUzMߢ ڪ?QOڼ?m W3yBP JI &9;!g;{| !A$~W?ҀμG׬^#yˣw$}sj^~&R\CN2?zEo[ޚчNuhw}vF (E{*bexeYrnRֽc+55LQtA&;1&?5?۲.2I2Fj;<> 8:|fx8yUN)\Vn[\M wٜm[Fik&U2>W\ m@SE؈/8<9)39f~ݿʼnm(|"ltЛFO.t dE~hRTb%9,C7\yGUH&hCdR1BE;qV%ʜ 5TQP0˱pu9@so3To5P>UH"3MX&8`/)Q{eC$쥧=f?z4*/7-H{.._[G&}>/wÛJxcƮW?z$+b'H!;u;oto\6Xqf4YM~ ÀQWqŞ=фUYķA\K;DA2h8 c_S!_%Ot )S)=1]G"8^" CA $D9@p@vXQ*,c?7[HXNow"{psel2_XdM-zO#{ %5Ychl,EuI͛ݮ W&@3n~Hhwd\1b}N ܺ,~@m/O:'9L= ^B25f"3MX&8Woglzu'䍃].saW>ɭ~@]?nxu,<_qFZ$OcOoNos\Gv~;H$"9|惯o=)=jX,C O@KVTT30Y=-3<[m!eeBna]~Vfܴ7]1/]/$YX|#M ޮ6"XU⌒^/P"^Xhm8Omïo*;x 8SD):$pY,{j| eq\_cM~lp1UiX߁=8EY2QcWUu~nG3)Fk'TI+Ρ맪+'I2 %1:l>p 4k]AA88@7/Iq&S4!#qvv Z`CبxG1p-u7D;yBb#( M9""Bl]}i.mӱszlEJJZk,`)ݽ'x ,~tg_K#`7Y$.@YM!Br ue([R(yF{LF:u͇s2 uU낣jEZ*0wR$?EQP6_&Ƙwrx ,*RM{ 7n7n<W"2MX&8ly @ jLez, Ϣ"Ɉ Iٹ)2xc7Tpr-sWw}M 0~z)1HR@Gll681QdS°h6#eoe)y'ģ=|'( d$lm`MEQ#>2Y!dL@ &O~Addvv0h?MC=2=k yiJ÷#Ay@Rjo{GRԨ <,+ 3yE%θi9ޖGEKAKEe k َR W2ZH5hH`ɤ{ʒΪV Hnt8Ga_&Cp JCW-pe92 ؒ1`IeF/i J0ęP?nx~;e E,by^$ ([Q'H1`8()<>k_r'W 9H[+Z7V\nVnC2Wttv5?kV.tx[O(@N*1+(*H-X!Yc!+]uLAZ[}-U(+bAx i\2Iq&?&8.P*ũ2R/xLGHûnM3ωNL0AFlqv:9 blJJ:&!bpM'd %θa1Y!?,Dı'4E1"$}qOY3@0\"zC;vcO;vh?r)k1kmzXAr`pӕ4\nfIqYqYjIje(K":̚3tҽ@@J?P?*$ EfB'L(q&/4=߾B#!COk޻Y3w)}/7||wxqo>7Zwgo^*+oީ l-NMB =wܐ/~>~Mrۧ!gϩ_)1sӷzfFqoo+Cݖ;qP)C 4-ZD^mZD_w'ҷ6KKWmdc!mFX^zt;Oةh1}eE+*c'ꪀK?@76 ,g!!͒Ul%#=c=nSZ8\^[8&aylj&oe::T%kPQYF4)䓊 ` 8gkT[.SmH ?m$hIImX]Ys^>8ك`yC9 *6r!5ψD3b̠ARd} y;Qo\PX/)_6Ey-̒tO^somԦ.,3ag H]p=m]ѱJ/ٴG-D 5DHc M3y„g$ c X>y_sE3ajU~/ehW. C3lR%/rSL,i&OPL_g ؝+ysVn 6lذnq>[q&Au \E!3hb$s[&I2r:XX(@fo74XBL0ę4Irzfr3 b3RT CBT#H>T l /GY R^=1rO E$4'L(q&/DCCCCCCs&C< }#bg7sjfRPPpM,4fC44444444444Bs~ qP,%a kn|7  L,i&OPL_g28-^zHNJ*۸uґ w~qN!?F6 Wrf^$ٛ/۰.sYsD L,i&OPL_(.H7M''|6Nu 0iN' Q֢5k N0řeK+V^8E%$*,_fꕋS[p5k׮XZ)I9qΪ8+eAEQ^Œ{yNIic 3^Y"H30}ἴV+V\joUYK֯̕!p4SVhqLXtvcM d3s IDAT/8Q g-]ZB'ECu@: *C&7!Ӹg_#/B%4 JI %7 {(5vl3NraauG/y%I#',o^I`slMa=XEe) |My@8e2Zn-;#aTcem GUR"C]6@qpy)Rexʜ7:{h߸+B0DPs .L]R8堼cp8lF 7cf~IkK^OHl9 5'0cHDf %ΤѪon÷m-e[_6|tޖc7U}s|kn"R׍dn׭o_>n$ec.$'_zlӄT?˫Gԣ E3~lU`ȉv.Fͺ  [!đbD.;yJ!~?&+X"OkpN* &Ln\$6zni? T3vjy7t.R9}-@"-?d}ዉDAK_ ,!or$E3K>d_1?/NPdb M3y„g$CɌmAHϻ~ώie}?sg gX\^x>x/7ԾK;Vx&d2CHE8ff S~?)(-J}~ǧd(?tiŚ+bUk+̵̓TcCU54) yn\Pm_Q.`(cw5c|znRSƟDRzc:7PUuLB-[OWyï* w!",NEN \` ƃ{g q4٩ÕVu}=ǒ++k!Bь@l dR[*ev`k2(k;lPMU{6y9JF3Ty{ӡ}kG>ӄpq?ȁI혓.!U&9:zƶH9W,[bɲyy5hb16rxN-Wr?ΊJ3Sa')ҳ3S"_$1ť+oz&ʌUq 850Lۣ%XBL0ę4IrݮT*fכGsۿeSHYߞ~{/߱⊗7,Z f`%]peii)E\~᎕IN4xrr>(FHDR(M;"(4?R y1*&o~W~_w;]Hr"conn4ߏ,#;FQ"cڦ(:ݿI& $E |]W_H3DT&EQ2G;^=ʏbs꦳0% \!Y ',&S`T(fC!gSM?L84_m;)&b"D #}tA7I}b$vQ$6_J3BS}ܗ+sk{` FjoVh &9G~5Õě=y?Xxy<\Z1_}hxyJ *>,.YV&P(2~M?z<~2AciY(rˮmj@%A|<ܫ7K\hfA_&fa "M Hڬ:E04UűٝA; +( Jw#bK&$vr>MJb#1q(2L%w鍱^ )Lc\XEP^JQY" grvdy)EyQ^ )HLp𥠌=mQ+m"F+2Gz|lY^0Lvjdv!9;%B3U?S C@}zqg@>!tZB%4 JI ~+WI}W^4 "]x#犼_?Ы%7wez_ur|GO~tilc/o{n~텺=ug{ e{륿nɹ[q˛"C3G.W+@m6 TM/Чmn,bQ~D>&]aŠt[:ZtS c[pa:9-vCT\(gѤl.b!L91;Qat&7>з Wfkm]EֆTk>Q3>Q pzFsL N$+<#A`{.0TLaAU8Jx̝M#wʋ,Ou54=էxUf觽Z DOve$+jŒd t#=!So4'R#SK~xz(kn_!Sk30w~X #NAYsd/j̉=<̷_8lF>yX֋, vw]w-|1#7^w+5$J(x,'qBD3jSי9Di޼ٯ>-EI l)*W%[o*2co kyҜ2ӿ4KR$`'JMuЌBqø=0L\DCCC3KyxmAb²;-朾sN}Qx9.,\ş+e?vΕDC39/Mb%&3yS"tω'(%KpNIXҴL9;vTIZa%K,^Ƶ SSK֔-[4WzBФe˖'Oܔn`0y2$0L*M`줡*xElWrZ~Ow/GN{RoV7X%p*ihhh -?CAocOV~{Ra_  -Nԙjé;cE-bSO0/esHgB}̘44S)~-K'>λ}UOXݴ\6dmPԘȚmWNN@4444s,;WEez:QI+~kVdq]}>}#AgnM88PՋ?Y{nH^7@֧$ë=~osZpѻ_^о(9\:?^mq6T[ڈ5\~?cQ*b*pt SLW 9 -]Fόj-\\dyd!Cӑ.;0tà.@PoS38ajj(˒]ݽ,:P䥀UoIH8 v]WI7,vԶ8"I>MS)de(l,wM~[ G"㌡H,KT x)|=dvrI?з[77/c5WHSa@JNt444441q%~˝$!UeN9ɑBV>փgK=wOw_V( }7 }(Eox|.temy Z?} kn{. 7#)E,n%LI 5uwv$@ЪCҤ,BYy5uuYwvvvvVaW<2 OR&7–%'P$YҴd<@0b!Y${Bp*)4%d. 'GF*a{ . HRD}r7vÛduWQ4 P?.Y?@JJȗIn]>{9?OK<>p`cݳ;Y`D\'0S3[==fK)&"BbтKzMN=`GG:[Yǂ2ct,x2$b>s|wYU]EuD:$bL"}?9 у`8{gG}C1@@aAD!1?#ybώɅ*`@e2B p[p74ga1B pOr&SSp6Oc94*U;ơ\>dc,WX,c:/}n  t\,?u*섩>$9qL85Uy>%(fIOB4uchI/3lůNC3,&!%I@pHWO}e`u/ݾҬC0D!b{> `+s]"L7kM).ܰbp|:ghhhh{ <}"ݲp!yϦ0To7 Fm}տVGzvpMװXL4^@<. 1& gP?V?rg^/Mє|*`zp7;H (A1QtC0aZa8;@29㣝0yd p$EtkS "E#0Q&iF6W1W}jSCud)9E)@mڶV+raI ՙ C=0NAx  u_SW.ٺ8-; _^1Oksɽ;7-~U?1Ndo Шnnm! 5|FCCCC3sq)g{#ʂ6߻!~g#ȩb|&@yޚjtA2@Lltsm% |as_'ѡ(jȉ6| G 32|P IuYA[Bq,*MVl] VLQP`J&{;<] I-VZCv4 v1YJV4M#3 FBuVcx8/gIDRAQݲK߻yv#k?t7ڧW]mP^xdz4 ͙p[;.7CmV Κcł k==x1_bjeՒyRH]y+IuH\2*v<wNPx!3L?:" /sC@"LMZG_v@,6G(`!*bפ)!@wdIF^Enx.M<}!pJFa BP\-}~'j*#MWmY./>I/y'epy%~/ݼ:_ʲ>=# t -\{@vֵVlܼn^mQk6=oMJ-T p% 7dq 7I#m.8RM/0a79/%I8uZ{ (ty}6}n/+Jb>~…2J)>^gt[B)R)A> PCf]MgdWvCD$or$Abt5)"$ \$T̈́4=K2JE\xK .~@G`ǃfPƣn,yI ny鶌Oy!6 :6Ʊs4`HsEnvh[/Rv}8@?${Ќhhhhhhhhhhh#p*JIENDB`btop-1.2.3/Img/options-menu.png000066400000000000000000003073011420276253000163450ustar00rootroot00000000000000PNG  IHDRt sBITOtEXtSoftwaremate-screenshotȖJ IDATxy|#g}0ߌFiɖm]oSBip5+\)G9 /)P( #@mSHGK(f^ۻ-˒-떬k4;%_?xY$ӮU4!NUlWYEbPL!KhVؕB1)9vyJQn+dyni^),-$W'}ffft-;:RCSHV10BNUtWYy $RAVdS E1: =mv^9uW,t9MR|%QYvQ™nӦ*\="6y&*ȍCELYåʼnBDeR4+EVb Bz%Viz2v4KV">Ke**RRya)@moD]'Dwʊ5W{a1j$l&n:7hj?٣`X/d6N:󗦒ʎfI)]i2 W_+]zbꯐFx61?^ 14g_mVrV,Ypa,ho5s!2p"JllBsЦ畱RȵpPԅ/pۮBtʲRe>ƉBk[̦fmz(T|ynA LJ$B'ɕCRsN=̌ϦVPCS;ںui)xܡkv2]S4Q%R%o6Hlkkkii䢦gϮvU2plnt>@ ,r;֍"k!opJHܲljBc^Qs3`hʗ-dsHn#Ap1BraR:{0:[?JPXUҡ _,wNUtWYy8&);|>Ei+$s'?Y^Gl'8NB1220[l ) H)`܆/O0v8%B>86P}{'B {E`|^+Q j.O1<[(L&@& DlґTNz = waW)LNUtWYy\*.<ٺ;ͩ+!+z'?jQGar"\z>Y.n9779涜ѲK%*]+HF[ @nPDaЩy|B:!SUV(JitJ)) ˝X& |>/p]T0dd2MUZzԝ]ON{"˧J(]Sn _Z a_}䤕yf.Py^M~'EpL20+{q"Ю#4WML4G84`.<' |ae\.biUWDZ~OswIEEzia&[UBމTvODQLp)D1[f aP(nr97jr(\T|8juXBDmwVD5dgn=!B!t[EFB!Ю!BJmB!@!BmB!BB!v)}78 ,v]irRøyOJthbʖQ-#X:8;pC;Z3oƗ}Stu9 E#'\r xvv1KIXpB! SS+8.'.ƈҲDg͖}c1ᡅt]l׬rKK H==/γ B/M^gLRЕY:Uɉl=;+s݇B:g@w)-oX#*MOUuY|$tJ(BD]+,S}8:OGi^>NXҜVה e@&\sN'>2~n&!BzuC䗤f%B ~GcԢʮe]kV()r+ȓ_qJUsHq9)J*ny6/_;?c|,\[nB!jFb07#C Tǎ*6gG $JSgO%yy[]L[]p[Cy|.23>$+חA0C~Ph[Γ[AjhJN'w? B$ZZZ(jaXe/"-OplО/[`o`M`V-xճ *Džφ Jiqd) s>q.ZW9ߥײK;:4᥍F,qo5B!q: bddaU!Z喂+),g"oۛ ʮ.(:˲$%!G3x۷oITF*I@`Dء@,+cKŖ9²,IL GҖN*659o$n霛ۼB<77 &4GEA5t2 s*Y/UTbHIRY,6g5 UKGECAW&eJ밫*t2dkd$)mYx ӥR[^^0dڲIx!Bhd2w/hHBVѹܦW;:\!̄W_(z>8Ut;dK S uq1sF|^Z t&Jl^*5дGu]N3L{%f~0\VBWz!>}Nqr3W•~M<F=[V~]r/ظR"2ˌR燯Y^}nB!BB!BR8@!BU @a~9kgE/N~iCCmljB!BB!BRxB!P͐ʖQ-#X:8;p8}"ca/Ymisy8ذF*;ɀg&6-YzƼɲ/_wcǜĹ6Hn:671⽮ 8@!˥}۞g 䁔]}yo"eG^>qe_V]4'Ԏ^gstr} ] &w8 tJoY9AA2c{R 4 u2 ܪgfDJt6qd꤫9uLU!BF(Jʳ)9{!tPT UM'b焑 Y{ٱ@&J{[sw)-YNkYҠJ)/" 8@{?*(-858R7hx:ܰHF28#ir:2%Llf$F*]ւb:|\z|q,f׉B!Pt3lHK͞ZHxG}Uy@&.6_]nls؈79. =I܆bp\ͼXB!:$jɤW@=Jlx!B`X`Y#BvHl8@!B,+0V&Q!xN*V6}ͶLE~?u]_J?w^6$ɬ,Wߕگ-(=dTCB!!B!T)? B!*/!B lip2 aʄ|2v|7wj)pL:䝞_b6-YzƼɆyy=ecNEtLuJnTYV\csSPU !B"t0tӃ,itu詑,8ݚ+*aTbEPzK)Usɨ$E[VGvѴxhސз:%aۢ2ez]K!B5B6i5bl.JPZ@k4B c(Z8sE u׮ia1s8ѹ`~l):aH7a,h7paR:{0:+rC{ Zg_N1Rt*# t-g r`,kS>nX:!C,KRbyDg:c] 6رz'Q5t$Ub^KDgZ2~~~)9^'ˏh[xt#$;u8@Ӡ5V)E[WӞ: gRŦIsd1Nvæ3,0TLX;8@ & #'Ջf@6K&.JXܬD=Riǽ!BxB@y,C&z5q1sF|^Z x2_c|({NKI{fe'⽮r8@!zFIϳg=+!79 lNR\%zf\hj8TwthR܆bpj33W%*XE!B[ &^I21~[@!:P`X`Y#6dbط7 ? B(W`Jì.4bbط7 ? `I$vR? e3"Q7$ɬk߷RyH{e$z~r9EjyUuBU@!BU !BJK!B5C*[;F`l0jKWg^I \20 eZy([s*f0iB3M6oQDReu[urMyb @^W9<B!T+rKKpDrt 5M&.aRsMRX~hU.ZzohQDBD\8ҴY4PUB!AA6i5bl.JPZ@.",l<ZHáAA2p]XZT.Q"x:\Jz]5UF!Bz%YNX0ddJlvJ{[s7^Ll19& X{!san̗jĮnQDȒ $Jyԓ{]PnmlMp{,EV95 ,)׶MU14_"nFb,XWHTܭxGEe䂿tz]pڎ ٯ/D>|GUyyD IDATP%:n= |l!Rt*# IAaDf'K r#hě 炓 *!MWC&~[SJɺo,˒XسCWZ0nlSu2荶eLώzY KSC[E?zhhI]]8U;]L*d+/|{ksHL*4`NP,6*EQBݒ #'Ջf@JF'AyxQO}mh^W ? BJ>8Ut;dK S w)6yv*^ "+ >e.F=8 z]5pB!T3<F= .Ne$xAnls؈79. 8M|6O*BڙbpM\ mVq0qWZT.q=^W |~+B!E(&QEDm0J(P.q<2*s? BPltj0Icae_, 7_}+/j8w(pB!tPqEf+,/.IbuH*BabBJ_m!駏j4?w\me2՟;Wy3$p7?ӥ=7F-_MoDF؇faH!? B!*B!P%B!-ZFtrqv6$ӮUmO k]K4m{3f[~>Dg@a*cc3~~.(`YVIjnI  }SZ-/SUQ1^B!T#dV/f-lYRR)/Er xB.|͵a_-qmᖩÐЎmx`%3z>MKAb07#C9`b3#1RirDjj #1[$ !бÝ !T/|ҁx]?[ruM0$ 0A)M=n)"i'nξ~/5ȍԤTF&'炓 t>Pnhc(|pkl;$ N匝-\pr.'HܼkU^nev-7s ˲$%!Gt3ЕlґTNz b*uZnicgG֥ߢ_6ƙ^'ˏh[M"C`g`p/ U%{WNB"'7%ly۫}V߲EUqn=TeiR0Y l:TжtT|%*ɗYUN֤fW2& @ C?qJR둓ңZEE*!:rgA懟YA(V[wm_q[ƹ\'*:2`饅 swIEEzia&(r UШUt. |}..N&.mw[&nC18zz]!Ё%QL&Je,-`b JdTQ;ϹY[P(V? ={ jC5 Ln槟~s*u^uPLF ,SW8L]b> 6$R9E'06mŹW[2CjK^X)B!Ҩduko+0vgmC8@-3>}K??L&sT: i/T:t!j@!BU !5 3ڪ r$"t0`wF!(MbP@[J y8x x?ٓilF??o?\]uAD?ȉSgs nFwc7sױo}Ͼˬ r$[\xNPGA;v >-]ާ~O8ykҽ_G[+nߚr'?U!LU_a=b_y "&&^yͅB J]>DLet./&٤դ#Ŵo AA2p]X {DJt6q#J6uVB 79k@պD2hWGAb/MO2yfƤo򄄐{ZE?/8 iceʬ}!:lf$jqrt*EܲV\rn^Ѻy ?Icțwòfb :o=qŋ1@4d2meV|-܋w|ens D_JMVaa$ƂlbZ0zepF8teJHTܭx!EaI^^d@H.[}M-*5yfaH'Eܪ"U*_}*ZKL3pfm 瓃,%L8Ut;dK S ZGt1焓LdetfPSb㫃yi)2qL`P(֞600pmBmt7K}nɍm/W$h.$k|ss ^;D|~+BTFIloZj5Ro2UXmox!PXXG[0ZN\D*Pj|aQ-^3!uN LD@XNr5J:\>v h{]B!BB!BR8@!BU @h={zw~SKoovdVj=Y&SjZ8Gt5l BF@QR]}Ar"֣gΜ>mQpmGXz ^J)`bi]ω6Y({ scTC}Pۚ3+E WdIJNJ6uZzG#7͟X~?%7U^HgUe{O<ׯ;ʸ,6M*&M P$ZISDmnVe<]Z;[T|ZK,w4q(Fe}g}EI⹙͕v^W ?Y&;k}}//GOz{?B>të%УGR[BJa\3W>K/-L-d >wt;-%B& 7 z՝V ϤӞ8\x()s15ɇrʻh@XG_ӏ%|ꧻp德}~ОFB g4ِ&3W/Snls؈79. *HvIVφF; th빙蹕Tmz]`!BDm0J(PUB7*\+NHp4rBX000poDެ\yڷB{pd2E~e&qTNl ;h[!uyU"^B!t\)FWI\+%mTI3+/ھ!:pB!B!BښZG,|KZ?ts7x/i4Ms|DA,}94T!5 }磽f瞙Nu. '_^X~?z}C~_u~ov㩽 Nmks+N.zE*[;F`l0NgFFt2 fZH(:7XI::,ZO<`MK1oi;l=v̩NDReu[urMyb @^W9? )/PtTy{ƛG^u,н8M˗8nq$翆j$g]$2ͦyq~^P['L_BS.">><&n{zП[eKyBu6G# ;_ sWMGf3̺D6S#YRq5}Ō߬KF ) XiRsMR(lH[0m|2%6%Qװ?,nlIgB X}S=#yWpsЧ^4.NO }'9ώ-\LYߝ_ʼnO=tWw ': S蹼7<7)ʥ@_~s3gU-zbhfaj5@~#@o2׸E."E$Mϻˣg/E$|c˭DzW Ϯ}gTvԄҩDHlX)w>qw.=o^&&^yͅB J#ŴoY61.]z/_,ǁȽ/m>z/y_z?}󋢏ZF^T7=els= nTqvݟ~*_z&kӌZ!9qgzhe!ݼj(>|ϭo}?M}gQQgW_0EIRe9~̙]zXmqx$IoˠPZ{LB ɀ-u='dk=d.̍R u(Diok\%*9I*"[(몁ǎМw.JkOfc$wBW?xW^~w} }Ҭ9!ħ|G&HW?@[fm'OIJd?'н׿Ĝd8J;6שoʳ滟f@*ks:!Q" %lR2zG8~裏>س6hߚ|`r)X}5&i7ͫv'ɿ|_ÿu߸am@YxڻT:]Ldvҿ ZS`' N7pPкMPH_g-hN8A(dS}_|%oxjI22ڥ) o}N|O[$P,Jc&S>0ݵROҥ[{Nѯx\bYſs)JȚ2L.},XV0jYn,mW{%'+}E{eY#:C{vJ8yG#b ɖ2P`2vQ/kuij(c痢zL.?zmv@3?4M$׮.낅k368m3IPl4 %_Qo/=Sܗ􁗫ݯy@ȣhߊd$% r{)]ix;o\﫞k70yy:u _( &mŶ|$sip n]Nv/_ko~rz?yDocr'~}o? n|_z>ͥ^ypz?@~~y9{˧$/򁧹K>t̓?]>?{Ǟǿz?RF1)MeO,U0/*bY-ҿ}3CwS ӏ}9͇<|"}[>8Ut;dK S `B3^eg '%0ȴ' }^sC&z1ﴪXL'S YX>/x()s15ɇ] @(k>8{.Gоm{ٓg`~g 8o~5:rs?~ۏ#vf|ל=e Ll%vP[U9CѾ{e[rcF4po ŭ%-rh.$ *B=m{4D^7"T5SxgyҋrO{zi= g7sw P] &^I2'ns ԛLFe;L"ZӶO3+B]'!(e.*:"B?{E-˥7)"4Q@(B~(Q@QI  R/Br^n?Bܑ{||ݙwv'3DkBZSM#K?AA(^1F?Lhd!{%AAA,  X AAAAP޾B.U۷zho^eJ-=*Za`9x)ںI}=<ܓUϫZ%R8.LBuYS,?  >/+OI˕I sQwt ߛQƸ!!uy2 NukTRGF7vV;yR1'U}̻S&'ou<_BA%>OV[% 3&4{WZqk>O&QNV'Ɓ00! qn1Ã]WzohH *ZW*5t:L  Kh4In_Z\] xC -ՖAˋ*͝*YPPPSS4vfy2d:baR46S:ju_-AA=& ?vNjM^, (|Q~=HqhegױiUy"nutGoa"!,78EGerhY]@LVI$ lzvznVPP\ *DžZ ƴՔ=â}4۷ɍ'[|+_@o? [Yox54~@LSڼzAUHHH^^^oG HטlTwqBb"3x+זoD>t;, hB '% ƂLk=2́ y UkuAAA^bK>ή=[2,  @rl hC?J c ? u0Tt)NLDO#= ? 8@w 24 M'*IpVz}J!  ҅Vc6?-ud=7ާK  X AA?  C;mμD? H:H$JZ 0"Qw:iĪ+"Q!/ɝOI-~,>'771v19STt3-iv]cYז~Y’yWw<4q}i4߮3z%It®ȉ;ȕAaav1$a KGx-`L;ЈN,x-E$]-=(ЅKmgA w@dl|qQVpEF pvͦ4$Ą&~|qv6A1h(;FyT_pk&ydv 5}#!]o[}W 7k{;R{T/Je}?鏨MblR]g-Q$`8q$YJ:Ir;R,'?W[B%-_rO PA2̼ͨSc\O<1NwfW0kPc}NMg- TYdH!"jH[QBG~c[ռC sQTe3mȜC7nnv#(̿ӫA4</'Z>IW o\>PP^*J^ڴ_ޏ$-tBqLC /b)}اyK+P'DOiY駋~3 !y"ŕOLꯜ[)OFߞvդsy'6,Oen?a~Vگ 1̖o_-opE"'Dy_ oiK[ٶD~'ɓT3)?Dï:VH[!rDs\رs-o  tǑO|JHTYQ!{=$<_ODe>6׮@X*gLI$*>o|-W*@k:A')/o4&􋊍 wѻ tqinn,钖n9PjzQ3 RlN2~\lٲՇnۄB!@}}=sV\.5}1iftၣ9ߧ?36n|ZI>dOsg;v${?f)ƭX虓ZLq΂1fR#l_O_sݞP\T K N R`iKm[5tw2OjZ~_lٮZڳ-[i{!qh|6=zcg)en}qȢ*;IN/-[Õ'[Ӷtˏٖo<ѧua ׭\e/5_gwa߱:sW(:z~̫ײEq@u 3kT& xRT" $DƷ%=F'7.ꊆt6Isx.>bʒKZhLh{#[2Nhtq h|<+E8k̖vf=EggU)._|ؿCÇ.u%rmK}ʁW@sMקp8vfl/*Z3lỵ.-]RlWL5.1%KOoX֍{פwM%b#*sүgɸ>7wה֨{2$EEvzfVVV)יN`^Cy+ٷ'K `̙\~yzn8P0ǂ,:@5."z1ú:9y#%mZۻ~;|).ߥ]Jx2>99іz/8ADB:Y:i5; Վ#i%EnSvݑy7.mrcef lG3O)t5UaDDV (TKb0 mW ` b0VU\"aIB);8>Ѿj  wn]@s ۖZ7VQEP+ h5e\dSTQr8 8; }sEk~`'ZG=<1&&fށ˚FN"OVtiuJJKu萫{;/)"z NHK%eY*RF[}bdSM+5jw[x@}r:@x} _4Vmu NnyAګf': iǑ>M4ٵ'Z/etn"%8w’Mg2Ӯe&- 5sB4F Go}yVFFffNyIhd{y2}Yƕ+W\/#7!4rllqh8F0tQv|fmW͓ajV Cm7UH8ꇀ (vڲy]3x*$[=aDgw0D>i.]~] DTEi^8me fzLM(̒ˈE ^Jo:Q)XVDQfnh8Q\W5]F/Dnaаy^wcߝ-}_?4~J7C$)pi JSc+$ƈdG^zP) 79ğĴR djt>[_UAR Y ,syOA'= VgB,]]Rϣ/Lm,z,  fv1 niq/]??V[o}6_8rPD0 [ԓ*%ўW#RRB>01Jbnd4P,*Eר,7ظ `e|k Q_hdOV$|wIqZ^mNabyr@W] Ս RPm][M[Zd5*|\Ş%)h4v0CTmJSNyLwE49uibXlo5~is\% HfMF qӦd9UڜC:8gfά{ҒRkHV#d>=UiAWa2a}MOVnI~UsK}y;fײb0aUZW<ǨϾ=ˎc^,Y<*5:y["k!\Ҷdnv:2U1yˌG=X;iYFeqHngƇ˿cNws-%քe&XSNJdS}2?mg<͌?X; )IBϥ!(tMY%R:YUqm!S%. D* D 抠 򺦮^&x Ƨʟ4=45L?AnLL'n֩> BփA_V,JzhwIswNI!71n7}c2MqvshI5 `)vLOhdl||lBRwDu r+&􋊍 cNd%n0deihJe ]z0%iY¥Q(L[{Ty@Pi4M NJ{b(/!M(K(/*vJd0xF:}  k?hkDv\T5UNnz ԍU2sk+J~t^+/.o6p Itl^έԂz\$ 媬*58{I QA\AS+48C 55E%lHO FթG̢>]i'>9 ;YPRِW@=‹~vdI[PU_>.B! c>n511155Ll|b|9Y"u pbirBBBz; A: #t[AAyud%ײ"AAA4tAAAKuo! ] (tAAAK   B2gn{Ou91x޲ V.|N]V{ n4K~<6_yoͅ#7_={2Dr쉛[0ÿOi@;UK/|s=V>`ߋ? , &_lE>pSMZ#մ,NY^MC"wgW -g6ly"_tN "sot8c)~zta,?ד~ }|#Lfߜao% ]*L2 @*?X5oLzb܋(j՛[>| I?4iӿIkiyHQz,ǂGt H>sϿM;"e_|}'>mV'~oWO?G}eaRUmW3q#LyGyQ/I?@qi"WGfy4>(dX{T*T J:|'Ϟ;VዷzLұ6NdskQ";sB:tܩ}_΋3,x4fKN;7oMkj0=Ν޻n#쪧F3{xLuHgV<$K-0%z\'ϞO:1V&{U <'-t[ZMfCM/- Ǭ A_'ǙLfo -@{{Uf}7妯G(~1tOh෤~_)g%e7yRTRN:5 ,jBln[`?&:)̈p\ ܀;>'04dz mU8x8 0\7ocs?z4mҶ FqXAJo»k4$iv c(ghg 2@&5v= /{r÷M"W''٭zP{pС7]5_3tСCgDZ~I'j]@Ż^:t :tW~0g' 㦺_RՔʊeXOx\xjС ~Sd۳gIҢw?zaĹ߉~0;o d'k"/[R~ЁSG2s n-#ԓg3w.Pd ǣ iސPgPs#T1^,hqi:̄/2'om3Bkk>A_Y6~ؼO<5"D'Su:I5'pToں#ǘ5gt6Wp2KJʋTfV0ù6,$Իz=j*Rƌ8\:sOyGk;_8M. )[S 6O Yv4šnS/U[)͵nuӨ 87#*xD N:0o؉.hMA]ِ$S6LJD '7嵳iڮY*3EnJ7&:92 Fb@g;9?Uqn=+j⣓&?1q@КY1y1ޮlz H32wdw(7kx'jCHܺcU:iՄbQ :S'"tS[O {N0&- 6Ҕd}+Sr/;ٵVyVĈ HwA"K'W%?zޕOj穫'M.yDcc 7?ʵuYS> .Uʲ>sPY"#&5g3bA/?ҩ?7{$cr,[ecYfmUg+"~3KY0κ7ːH ib~JpHBNܬhl(/WiXh_-&r!pdcޫ~H#2J{t )tyHP]@ mAA?   b)4~@AARh   h@ẅ 4 l g65y"HFN:mKpB3kmyϪCGϯ{p|iNJO H/b:t0yQ . 6>1n0j}c2;S.+պȐWhq3'N`ٸ GfaꄍVfժv+ ZȪEގAXԈwQvfuoED.n!Xf{]4g˩L%;Hi,.y9Mc*nvIVtu_soU0aW ug.M|-1a.tPe+z4[@'F-̸% Elo;X~FI^>N<hd5% d#}90t%a:6 j TCUĖ=E\򒂪WR (P:iWoT\yfDD`xf3l 㽊 O7qkOG=dK >mȍ_^7\`ȑG\ E<<>2IB+eW[G\=(4UP1M~i| ANT߼~-#\ʰQd%RSSsVtrѕg^URnNNԪ׮ayae^-\yV5,GWvCAVVcŤ-ǓfרDuOTmcAzѭLh FNJz}}]cr%Ť!GN?52xz 3<](q ZcK9_r*%dk'_^)8H & p7=p˟x= Ha4jWkcCT5 ol $&$2/#H#.lPꥄ'!]8R6]H"Q7q t&{ Fx\uBx I sW1 c:DBի;AR-}EhcUZ| `#V(ɣ4*MoзJ讑JWWW=p9vK/ gl1@ia0<(O+*NpE*V ãUH)YSض~NҜ-YF 'ۆ,/htQNXitԳ;r#kQGm_ AQE_+jedxzz6'3޾C'UJ_P8.n~>p񵻾|pnԯtz͞dW;&xLY?ݓnfm*00n㨵w1(PS_?4g>后Қ>NOh]ߦ:^AljϦS\šՄV(Fg0(XǏ0Jbnd4Й,**ekǥBO7jMZ__^.Uyׯ_v5TnUMaac{?0 [IH]Pư ;XSo'?@+-% fI}zY7um:p}>< 7CGtatG-i/BS^NE$8uѵWO){;A\yxxNY_Z,}@d(JӳJՠx V%iN#.EdUy@v5zqnjD'(2Djґhtr0p@@ll*V JQ~c]u~4Fź tLRY'&NiZrVppw?-4E %H;'_|-!  X AAuSHBBBBBBbbbbBBBBBm;˳ã}l:ʕe3mU8h<ρvM1G]%cyFF pma,@)s|Xc|:H$JZ 0"Qwz;(2Dv}{+bZ/??٣1嫿ro|?a/{zyWω4_jx '3SzԄ`ػ_/A G>~8?oykôߥRHRTjዷ?yˇpLKR\/3I~8} sۓϭF̅ I_Lt&@s}9/tqvOcڗwn&''''"(n#n=x|>+Z`\V8eUOXg<̈́Èw:wz߆涒's7Y=?vόb힓޿ "Bl 8asٵ! :آp : kvѣn?Ӝwooi鋘Bظ W=񱑾v-s֍md ܠJk4|>,Gz]]lQf0MVi5Z}PB-`th4W3p :IfL2 I7(&.."tՊĄ:F9s00jvɡϕd^vLeEfjjfR[jA=leEWep'?W^<&F7+UlqqёaveKhtכzAk9/M趶lW#ܼykg& +~\5ʖ3p{i\'V# y,CmϦW|v=̭/YttcGs(Ogl {GC'|,F`P8\&AhLzFf{-b́fU7]˽vu{]96ܑ MÝF fZZiIJΠHd*:rF›uhzFPTdgfeee+}Nٴ|IO>18>>>>!!“knQgD||B|LFyyU$W<|q|z%c7/* ׁ^:- )_?wPjJKJTF2SǾǫ[<h8tҔl/xy5N}zf:k@wt@ ؊?>=织iG~h:L+(P|FN^Z\e:p{]>0 g BB̝5dlpWvZQu%ⴜg`ҹ:B:30:#)Tee N)L;C*+4Dr PmB=$]#d9ZWv j]P0 ޞISx˟Rr#b n)3 +*T*:xU+uKd<=L1lW+5ҹ\1QP' BR[nL{/A\k<'$tӹ>FZNgs:rtiD5vL5M(`f2ġ @cBcG1Uv՘pi(\9I/ijzq1}}{]8G! uvOV6[);}>Y\`7mgD<ri%EnSvݑNяφ4 0xG>ǂ,:@y+ML\.iӊ_ޝK u.w iơ@sonh4^gNMthh,(т2yt\ji9:npZOUmk՜^7/˷!*^krEMƦW$]7@,,F}1:7^o,3^_{TPRJ ea|pꙫ]ZM4>0sH(TZ{Z7#.i,̪bDRK *e㟾FZpF M}Ӻ=T#gD^#Jޯ:i`-O;jv hDWDqϐ/htQL_zYT0Mq1L_FuxrIzF㉔vlqFŌ-`5JmFUKU;a!޹xiBŨ@4|'i%u.oZ76n峫2V!w6rܗ>fY(4t:s?9$L\qO37+Op?Pz=jmQ(8\:sOyGk;_8M. )[S 6O]ҩT*[וe!Y&.JH3{ۙūwvwX4eя: twgbƂD) C"ғדOWmFpXl Q^;V!Kͪ118SMb뚶'fhg9HHEƪ>5"gVWL=Kw]V~Iz]rPL=F0B>l14AgYt 0*-@gvQlgۨāg4ے,[d˶,c'Dh %4-W`Bn%@rN([ZNo._q,ǖ,_!Yu'd9ql'~=F3|̛0RT*9$HZ%&"MIBENpPJ$Igh25m@L8^N|>i[?43++W|{t uY VV'LQ$} ן?WmJ}<_@ qNXʔJW;U1_ 0k˥(J( Z>'I$ (Sf}O 14e9r-jYr0;gVfnj7^hx<5'Տp95ݴ@IEvO `w_V\ˆi%&ӾW<0 3=m z|s:nB_XS(WJ@83%4-R*nư^OR)YgM*Th]d.SL +93wG3~`If\<`niqiu. apg+eɢj 3}JCr};h1uUCkW[[@ˌmoEC0Ïҙ%ayyN\ZdTAlĴ* Ki29-Ɇ_b?4$M.P$W@D}v }]&~ͫ6u0 jcpHk7`.`uo??{ }3ޞi[?gs.d wlі_t_~ie}@(.A%*(Ϻ.,nް_ oqxk-o\O>^uZ vŜf^xm|+g;ܺ-y>mmouP<;><_] ]\2l~އ)߿1gǓVm?SĆ[?7{S;H"*+ %oU _{I~#('I]Ǯyo?rʟ}?ye򥿪EwA&i=c@G?}`V6"ba` j)Kh6c9 cdEhX0D=Bâ {]NIJw'>8@Zp Ea6#E"mGK0`v ލF#A V{FڿwO×KylΥ咞{Ӧ}{Y\V!K7AIeMv1Mc$~ |Db($kHq^Yi̎U[[~V~ѱvpDŽQ/@*b 2gb (>MA(I"$bNXI_T$D]>BSSp#"yfp] b8K̹JVpLk(Wr'5OHkKQl vRG@s!ԴcGO /-u|Lسg hwe5Bh>%X7ـlrpS UVV8:5$q_"x\%d\ɴsue`#u"cjӪB 7:ʀ&e_{4+heQQ=~:% lpN/Fc MGaK*WoXL)!SwlU9H=ɛ= nW^!4X'RR^'tyE )ҽ&FfjՑ=CF:a"y1M.)r&U:o[?>JQa6 B>/3[F ;Ki &9&a ;L!$HxQp($gJhZULy'Ҕ.*)Y\=|( L Pf H xLq 0iJ Si z&yLm6=nC^/C+$U+NU1_ 0k˥JB8P-0S.BpSxD5hOfNS#.Is&1[CyaQ] X=!xH o_eЋ^ϔRF L`]fyBA` MD= jWSD,춙,D}6em`R|C3~w",G +)sV IDAT.pvO4ܣA_c'|nPRaH%SaX^\dTAlĴ˒{x"0/QbGaʸ4T E^GYp=w}jyǫ|W+xAۮ׷g69BXb}=%e5Z`P]LF*7&?htF]⢥|.uL6I'͙bF-f"OkS8W*"QzxLJ+/%"B(N~0q-X韟kLFsd2w-B /(1B!JB!BB!B(]~@!B !Bta!4c׬;=#@#BD7+ }ѸrBA|mJv^QBGOuڍ/|퇿 - lNyiǩDm6{W?'o拋RfT'B͇ي}-) ёD|'/ TjD{}EIQ08eB3!у/q^d/ϩ= C\vZC9`m!wǭFQYZ(pAA|_JшcMX{~tQӳ>QE?ظq;#Ssmf}_>^Ҋت̞'}gx!ZfomlniiigzEjx9Y!=QskcSCG vllЩe[v_5}oB;꺻o}ZCMܟaOҷ@kV+gghcu',pq\>ۗ_nx7|ffjN*S!$ GZtW{=Qӿ~ʥ̉BX&p(ɯY 2Kd*zq_Fhb|7ޢx!01 >@4@a@ x.VU*aˡijbkӈzۍ?M߾~mh⫺[4~&'B!N~dJ&>q@LN"BPXwy%ިF !4Tn^:]n✑HtŃx[cg~~*'^eىi*uSض󞿏~VOKh폟{kW ɉBlr[n~X }[nbz,/wD:'B!>B(N~0 ]&cB̘j }‚0;;?Sxh\^NfKyydZ f^P#bTxLJ!"+ I‚0YB!Bϧ8&e'S?a"&vNJxAAR?"x6p|$,1u!B(!BB!BJ~gꊊ$3 B!T%R3$P&b$Y$9// ߶b+B-Qҕř)¬+V,]Ră *#lIk+K kˋUX%T./͞|XVTo4\rz N_")H(B"pDLĄg 8\Fnpu?9YK9D,iv%#VV&=DHtFז}GÄXSZ*jt&EZX[^Ba+ 4  8xVYQ>"h̻zKl,倛XDLD4kVO6~؆?>Qs7A ns޷ny;uG!t":aSdʐI]@|=hep& }\!/%ml?L1?'cŅ(Un90% aup \;KKzM*ݿG׿\{esYEB QnAߛn#E)-[V|ymUJDBh:Eݬ\e1O0uOCCC-8EAdl<@펅90%͍Pӎ=ASK{Hf~ig,?ǞM?+W@{.BЪB 7:ʀ&uFHhe-ҫ#QwwQi x0Rm!<婲x1w)_B;L !-(ì:嚧yo쿴tÇO~?4al]y_F I /( )3T48ZF2Ffy o2AN/Gr1^\/jt&Z]ZX 4 Hf81d"m-P<@y 8;>G:EV0!8/hp,GI0oYnݺu^SD" r@[h4ejqN'V‰(aC8hiJF1iP (+:heAa$r\I sLbn{z9Oa߾~˔{˾2Ehg7o~y,_Λߑy$;;pw-B D">El5dlpLe cY92ONA Q/qO1@mI,U%$SR&b$"B D"hAIDŽB 1q>)`%4B'.( y$5X$?b B- y$U !B!t<`!B!.l? Ƒ_TB!tj;8T;vwjX*r.9>𣴰R5IaHaV!?K'f1D93ujb昽}œduBY`(Ȗ Xo :ˊy\,sIBuڍ[wKO^}\i߯|ѲMLL'Xd9;7-kwZNBͧi? +25v4e5=3֎uS@]\wL!B+/rO3גN.6-k*rmC?mnZ*u:IN]=E{so|ḕ6e5&q+!B'D*Bw`@xًVTjB)(X\]bE||:qemeNΛD|aie9T ki36(niM̐IòQoodLLEY6 0q1?%cg#~oNqƝu`-dnnk߷oB+?fۮ{ͧxedL*^cggry$x`(Py.d:kn޽׺mhBF8z#)S$mTbq}$or}Z$PX*qwF&&fzmf}_>^Ҋ wmܲ6qƍw{@}C]h}vU~+$'B!GREo11V@e n/ڼeca7H3,&a{m,  dĒq:hd"gpܵ8&Iƽ-fHJ%omlniiigzEjAZbŊ+W.˗$8ي+W-p[Gs'wo_?oWVǿ |oh* 5 p?}KUv[ jsOVuFO՝ 'o?͇L3d*O$-|}\)$ Zq} rq4 G?|x\onKnش`kn`5,M~TڝpH*~nݺ3)֭[B"6y]fe;2oq5]õɣH(r J?˯?S('B!SdJ|1(F&ÓoXiYz-nբrhmi*54ڪᛖ!b %\+(\R_cCN jWSD,춙'͍a NX5#rz>._HC8 4BNmnd9:WmC5~|CR;;\ }{떷ZB;avX< PfG]:GJ &?ϕ+vDL>3ws8MnRҨoBfI).6{uttڏ|8N( =x҅}CT&QzeQ&|QN-z.aw.=W,ܛ6UV%*沊!T,TiZ<<^랆akrrxie3f/jkhhhhne%{FT:DA*1ж{~H:QNVjrʍƕ£Ʀ070|Y)!4BM;vY=dKK~gأ(/WeB% f6NtU#}~cc>V 6EbI5ꋲl5:F 1'JY%F%R/v2FL #%RJ`(!,,Q}#Z}}o _FhA|J-MS> P+NU1_ 0k˥ !HSRf(gM H*3`=@H@Yr6 `pPdiHH ,)E|R9yB1V(e1̫Yah_OfIY"hpl<Xs8.'b3o|޾ IDAT_;||z}WB/Ѡ/1`D*T {⠣pI} y:^)/T :B(KUkgi$ح\ή(yAU] vZp! '?Ɔy B)~g43ͭr4ߵ@ͲAe/Q:-6OiN`5~+B!cT*mwhcr/!Bi=rlk2˛D? B!҅B!PB!JW?SWT _DLDB!@(iZ)&d0%MYm]`6+rwA!Z8R@R P 4DB8 pݳ{WWNc]gݹ{]ǵLo7Uyy8N BGl,DYLyID~cSkO?oQgp{y/g/kdB!4RbA s /`"&K":JZ?矾o*f|!_?MS W/\\)קOr |/red.v!Bs.ݰLHDG/D DNɧ2/~ K#!Z0R=+* 3y P 󒈎R$xŃ1βD"MQSs>>{mC!NN?B\ud0%Mo|r sk;wo߶M*l-lӚh~NM^xNK^GYgVWvm&r!:! 8n&b$&B_.1W> 54444ߞޯt?Ar˖ASqHVRmwyY6aD1544444>^Mє)3<TVr$c$/plX |W'd @|H,{}11V+%QW\]qĹfVjQ4)$qd<bIroԝ2~yEuEr m_s.ne+t@\p8B˿Oᖯmzl꽽2;$AYYRbΖycKK>g?lYs#!4yKS9fVs|r9awr{ J?bAi Be,TK+T֑41e6hc1Wk $ʢ wW;\;yr='@,`ns4%}]ΣyNsF1ED<(GL,P'qD]=$&9#&r ˲X y8lC1`(a|@GNeY bvPj|66bj!(PV%'ϐcdڢG1.b W*+Lms|Ӝ8Eh}\u 4%j؝b Pb !t wNFagjqI~s:<Ծo"QиdW$׻iu:yrɘcC rl8lV>{ts$qġ9R lgZ #j-cM)2M+$n{:] $9:>0A%?Xf\sSY55bY\38) M%9BC- 9"Kh0y7icZ C,|M_(:۠vvy<( \rTFK=ήiSu!W?j%7×XǗJiju&E Gp$#@#  $xeeIJL!$HxQp($gJhZUL%Ҕ.*)Y\=K=N Ɔq{o<4%e }ĩq92*لqvQZJ$#HA\ 9(DD k@N''G@5J*:f{bU1_ 0k˥{ijWSD,춙,D}6em`R&sGC0ÏfwSW4 1t4j-s!sѾ̒-E؀y(@.P$W@D}v)/T @\Pm,. zgn W'r6ȭ[{Zٟg.]t}~'?_m^bBhb<--.Υ!J>@y(ثxFt9 ]ay&gnkc=ʒE"gIShv6lȄ(yAU] vZ^[fMp2M^T_[w^f{*aD="ʤPB 01].:WtlP}ٽIK_2?-X韟kLFsd2w-Bl~/( /|'J6?ݜȳ  ʢ,Wہѣ}rX?B!7:,vuX`!BZ[1G8 B!B(]~@!ZM?!^~ ͙AB!JBsT_u X! gvD]v5U%7HaV+.)HHʪk6S9)'|6ٚ76'o拋a5/l9m |/ fe;KsG. *[a*+7W\rE}brqBW-_Q_D#٬t3̼\tyg%s ζ !ҖUUζ@]\wL!B+/rJh}O[q;8 ) 'C'l~*^}Ǻ۽4ˆ#P$ c2}`fِNA@@mɞeZ:;>i Vڱcǻky&K[w>sYNE>6ulޞgl={Ëoi ȽϪ;:*dL?OjۛYO ?m-ܴJ5XM|@\'<|ek{kwVM5x4T=}m, yfjv]xri^u[kwbpT6YydÏ˷-}c бVkiKK 2Y Ɯ’RS~>f:W&,,)++)1(ۇc&#deeUYI*dR9͈匸GxB @{|G*,+Q('^Wo[\RB⬒1r{bYzl``˅1DE\F~IiIA&|1J/QݛV$JI^!'S#Ck % h:@Q|$d5b 0Vd& `gc { GxCo;W'+o㳯ݖ%&_]\V_ .oI_;y|3G8zug}A-|O_)}aL<垛Ό)<78?'-mÓ;nΉ^x˒smJp)O:}$7].Jm<[n}uou`p;5ܘW:ya uf}{[}GB+x}j,>eBxcVSQ3e4Fk2p4m_EEM '7&r5uG2!ŗD3EL3b9#,s?hU(bthݔ N4YOA W(>EY?[Qu/b_Yu`DlLTFO-P*-'5꘧z㴚X$46@e܁5+G``~k3M#ҊLJp^R:(1a$BL?s`9]rZ? |Ķ\[я\y$%Kr1G?}jHzO|>A]nҔϯhTT~D'XO {m>_ƟዋOxϗK3.x$x?s#Vny?Kˮ-Y"F?|&To;}oWJ`z+BHKAG`F R>xB.kaXۗ* %h|cJAP>1G}F栄qҌXΈ{4܏KG7v8YFɌ9!&!PPrGpHˀc'cƜ.YXv;設 ؖ^pњTs~)yʀ s{ihg@&A!Td&Q݃rCwt;(BK@(h4Q^"+|o~k' B`{%^`S /rYj_0FL^o8a$ !f <8\;`څ&D1'sXzH@ukϣA D |>5!TgIJ@L, 03|I>$K(%5/A]#Ρ zl>:-=͈匸Gǜ:l}>D@?G4cݵu6?uZnNbSmb>9.`uoe8e)s~ U`WUCqP,〕֭Jtzxes(AhY C2>Ńx?ED^W?^ǏoM:0hEή]x'y BeBrX Ę֯'_y59 z² ξ偖L8붛G獹5T3!7_~9I読s4 bWS7߯#^S킟85wgnLKO 'su g/!4_1AR.@<Ax<#1Z%"`~Hl.AɾWs IDATX zt25Q2W$T4 Fxx8ySʃ-H^[^>"Q HJ! gKR:hӲ ,C4wG+r!Kv>,m/ {՞rS/߭Û=tJݿ(7]MCm/ѓ">/Xq /?]`[/P_}O}.|_\;+| @§Dl?K׭ u/Ng΄O69~z_]/5U s/mشj͊]Ϟ }|J:Sv(5 I;yoӇ=EcB!ZB|֟} Bh1p[0E*B!BhQB!NhfhbB!BhB!.2~wQ,;9>F {-rW\WDZBHi|zZRLq12(!{׺}bŹ:jb~XiqIFʔGIo 5m>ϔ";h`)Ollwp@fd kw34N!1 M!dř}Ek6n9j/]tKCBhikSR#k|XkԌ8ZhD4zj{}L|ًv=/l_vHyU`\)+$f* .3ӨkjCȬa|wύe?|џDHr/~"wG߿o{l%_3Oo"]!暐O9v/xG\x`퇐wiSf%Dz=H4j%|"@bSCm6qQ%-c*L㷲D`]lm᥉T$%!Ppr-Iݭ~R(1~ϢmE:Co౏r)D: ,y sݗDW߲Kӕdgݾ-Mo.y)G]<~KIm;ѱ81T/^]'BݵUu>NJTU5RUg2a&뉞v;ٗ~Eqι[Vsyq@t׮-7nܸq%{ǥ!H>>?2a\zvtT ֨W[1_pAģ$V*B}j\l悤qZRq:3"32u .w4r>˰ϧC/7+NKS1- ݇/}9l>2puXlPV_a0b0 09l?-KB3KΪA'Pj+N=ǎsgĄ0%T(Pub"BHgxJ,,6L&0[ٗt"L@!v),EhY% 81o` +0"!Bh`0B!B'iB!:B!B5K2)O~N'bK%3dZM 1\5'f썕|hy*9-9N&$1G286DH*)رu( Qɕ2p BN\ u:Ɯ׭,KתVd áB1ԔhD iE%v yG흍]nq II)H"3 l3 c,6Wr3#B Ntzuǎ4F+vر݊a5߼sOe,Gu{K:{m6[oORM6W|ʅ^gM{vn2 j[cԶ7?m-ܴJ-w;7i4W/vj5=Mo{'|H~_=n9VOB-(L"Դ*+I%RO9v/Ͳ!+ E%PSqI2Saaqc,OTe䗔fKPe%j3VR %_9mC>Bn Wz]Aۆ@CXaXaX&D{seO:ڻo1/sәQ:'yY6[ղ6J5ܘW:ya 9Pl:Yw>q!U],3xt5Sv>uڪʊ^b'%jy}E)pKA& X:d=nR<_H~%K7@  r1Yq0kiM%%eyjGe$|CVrӵFOb6u ĉ8nbbHKAG`F R>xB|mA Yj.?ޞ>Xh _ziM fEq"`k펶PPN1qiE&%8c~)stg0!&9M0iɋ.9U?>ybۋ -VG[.hu|<s%9Iᘣ>o$'`>S箿i7wih4*{)R@Kb7;O3$﫯 N_D[A}ǯJ:! AP>1G19ByV>8f@P2WK`V! w)NQ2cNGc64MɎۛ:I\q~$Iǥ%- mBKKOwyN/Qclbwef]1A;qI ީ76AXH@359}Y:C { @Hn0-DV[{^;ȍTnXwm@Tk(8  D  #$+|o~k' B`{%^`S /rYj_0FLq Iw2<Br_ ywp˧lz@P]h@*F)f^F-'K(%5/A]#!<)yʀ Mp &v8'ù?,vB4`S A5iYpeeY a*`YS[]> 7wy)ևzCdBL*f9VєM87o3}sߺ%R``t]g>Y1!HβnD/Bx`+iZ9{% Jj8^7EY1AR.@͛#muo֒ `4|>"gWc}VD&y BeBrX Ę֯'_y59 zf/߰Kv}T: 4 4-5.Ss|k^/֍TE lasnܹNwZQ3B"֥ųC  pCEQhH $&zO TIJ!{M'Gm^BGX(Q+t  IJ26xϹ` Z1;^=l_[">F ZO*L+ֆ_Za3Taq@|#=VejZ~ dK̞,´U B"[!_S"ZhNogM}_$Ʌdmot.rYl>/|n?+VWGټis/z}_㆗~~ @ _>i3 UP|?Lxӄ/Qk|u_T`{iCƦUkVW<vԌBh!<^")%;Y'Bc#\̩C6k& Fz!^ߪp2\֖^j `;UYEz`C>go{ak$=5$[k>[Nq1GWy2z DM f6qZ~o6oXgr?Ꞹ]7^lMD9͍;~tGRqfԴХ@ͱE~C!ՙ%Zg RCY'DNNRvͯ0Tjs1G t1DzuX KEhG |qا=_ @ M&d*Vlv!=T(PuQmВh1% xс;9Z sL/ZB!< µG <͗ B!.l? B! !B隻(!{<)=5^.e@N!K1eQB!#*Q(6%+a{q'DCK=ȲST*t43sd5{1mNDFaN*_G}uL:pcuXVI !:aED S-*+:EC,w33|m\7A\)B/ ijc!悔62VK&j@+ ۆ YjI pAAuJf}˅MjB-B1-9NgξK{)"Um?Fb)ERQ+SIXXjS*niTeU'g[}g^)^0")I"0Y00L'd;B1#Q6iFDQ|$d5S-&Oօk*+z}~[C=Xj$˄KC'zV8!9q p @%ՔT͞m̙$9#R\8$Ip{};!QB"d,nB!tKAǐ.82KyʈR.zT׵FMԌh%M6 |^cN+neXf1,CKjikRt\otNOU0z=˘31oX%n R2A!d ;Z"5.<ůz:BD0> =Us{ihg@&AG,(a.k$&?,vBd`[iyNq>["ls(r5{v1gbt0@ Rei}nU֣7+{CK-$l! Z8zMlUZbt+0o~,멭r!04VĹ='QJװ%,4}|)qH~Q@ DZ,ǞPMbB-e~ߘ V)Dj Zmº^V'u x1X$@@~nV o>7 IDATUS׭,}?kD ڻ ab]FAdg9s~Pfky¬fISMﰵzecȀEN64ds!BKmZe9&E0}l.+QVqZV{e(Xo׷, 8&ವzTLsG9H$b6q|Z~o6o3_MMM ] [7RYuV5 28)7V[ ]FNNRvͯ0Tjs1hV[be B03 l? B!Ǘ٘0?K_B!BMB!BӅB!taGsmGeSGGwz>@6[c'-tBh1'ox2ҲₜdxdP2C^YYj2Ŧ,,)++5II̥J*3 Uc'N_m>xOD$d2 dُ}޵O_^Ewݽ.nBh.tI:˘2à\R{.(6}eAQ (OO 쫬 Jr|Iֈ=/2NLRoowŤռ9xI-Nhr;.kMZRMwl5|3刀wNLlp֋ dwo~Qbi__2ܫ^rM&d|oKJ 2 X N,ԘR$KV 4p?I".#0+^B*,+Q('^Wo-FI;jllr4*#c4ˆ]MOH1~o |HERx?U$D"p}4ˆ<\. EO~Ó *}aL<垛Τ@xCo;W'+o㳯ݖX%k7u:1X;3"Ngg|oUsb(_;y|3G8zug}A-|O!Dh%MInHwɥS uVUV{x*PV;PU8,JIT),iiZ9ퟅxGߡR#% YťtZm*Bw(쬈c=]= NT*b>.afnsn}}u_{w-_QAZI ~ث;|; %&u b'_PvZsNN#bFJ)Jqo yя\;}?jHNNHcӧ>WBKAmsLj DSt Om66 lM]눴r&/ͱ^ /PNa1#İ A. 2wMu][jԌNOU0'%1&Evm9,.+).0CD&l? 0=?v;u#O]?4iP i nk(sg)d#Ȑ54:+XyfsQNjV xVM82,z,gDh៲lUz~Q.X-Zrluv$EIo-׀Z"߰Kv}T: 4 Gܜ̢H*3ͥ O |ͧ~ 8[oX SyB-&澶_wu~qدuXk{^7_! P39eUI[G0ݝ"=E!?@~{oUAY3 l-y*ҠL+ֆXa3Taq [iY xm#eW|;J2 u<6l&B$~\^^>UNNRvͯ0Tjs1 fD`޿,|㢂m.tQ즦.Bh- ,:RCYծ9a "tXlxRF@I B#!*d2Lluf)PX9TO 8:b'>0 "\%@hY ^YM!B +<[B!BӅB!ta!B!4]B'麧~j HcYpuݯ.tBhrW%;tP2CJurq|hy<'aXKmq R^{u$X&c썕WĘST*t4ʴ*&!ͨSyLmw;Cs@-!O:ȿs7]vB!H%Ⱥ Ҧsd%MYjcZ%9}sjlHy_|uŤ'W& 9 ^$e5B4Zg_B7OݟΊȿs? j Ew}zR/g{>㗯Y"#qc,6Wr3c<w66vo{$Pgd VV  Z-(H)0.3gix1DE\F~IiIaVUzYV6cUQN<.50 [R+cBqT^ )b"43/vͲ!}$(K {l? Ɖ KD޲'-?⬤hdgݾ-Mo.yYDZHnkUyycxGߑ~ ɱ77@  r1Yq0'BݵUu>N_TU5RUᖂLt:zjvc!z1`eǿJKKJLRr*Bw8XBЁ +U*) JϟG~@۳Ǻ ː9^*evx!&Ev)A$q@*yǒd\;d&o{Ksapp ˌ9!er?Lੴ~}|𒷧 ShbR23stc2J qxl6M/wT…Oy oB!O,K3R9 @,x=&K+(Jf L Qr75Q1f``)%@Ę^SևYٚⷷ-g*sa\z~жӡ1 yQ? : IԱR5T8%鴻B1MvϖX,ul=yvl6<A!|\ڛk{|h|O##cQ6' /A˻!*AN=xrF! /J<oa@szw!b]FVZz1vOuiFO*}隋pVP4_<һt|䮯n˻ﹿL*~}&F%!X ?4)4'~F (JᙛV5 6UX"26IzjIިU>׷,<\֖^ 27669z*SK$XQb <~x;J2 u<6l~V D"ٌТ5}ynjjZR "i I9^r-7Bur[m~bVE:_B!ZXXXl2L&Sa:`Bss'Ui4J1D0OK_B!Z#.5 r4 l_c \*B!PT_fc.8~ !B!4]~@!BMB3FƗ\醍'p$B!t?BK۞xɌ?{~U*O@LR>P3 $Lu'W& 9 ,izig6_Bh(._'v^n͙r֋ A{˟U7wt4Uٹ$(ضf}lُ}R\r>F ?~/h} lokץOF;7i4W/vj!2g=?\?G!)!r^eCW@xbVkiKK 2@թ+JJ #J D5fs~B QzHafِ>˥J1L3;G5ھ!7yB?ݭmP]]qJZ =<j^/Y!OӉi+w>gιWT3JY+潺@og~JPR۸7hvݧ[wui]Bh!B֐SXhtLYg]CveDX60:$%Ae1=Ⲓ8Shb9,>+A Ê+|$М2SZ^^~J'6{;cN=xEtCGXee[n;=K-cb" aX (| x#登I-/w~&|-.bGXDS2{@kf6A/qI+BRF&k~{[#9ůLO{Nq>["h>X윋BJHOiz߼oO>1))>괂~E[H=µ}{KǷ&P(!`Yv|jN;?>{y[ydջ/_pin^.y-Bh*}|ip̓fC@8e֘*䌳׆/SN+|>X<#C^ah T9Ǔck7;-1S~>[yKua Hv]EC&y BeBrb'cSu8[~OW78"Âԋ?+朽qS)eڍ7O  ;V'K܃WmO۫_ %]&2=!<(  2$J F T2FEqw;)ץr>q|k'}i;=%.#+-=;Qŏﳀ"Ą_뷲[.Yi-mxwvvV9 _wu~qدwށqT?3۴EۤJneY͛P6JH0%$4 1-ԐÄGI!)/$T-ےlk-Y]ZU٪mS?$˲=3ΪZ9sώ̽s/ƪ~]]_=1, -ٰ|^G0i`YyZxw+\ϷOMءo?ph/O'ol]罸͑AdCi*&?@0]SXT)o7f#=i\"{ ШU/݅;K*2$L7x L6/)1onzб Ѡ?D@>8[!5}.Yq7_+r]YT8Aru8Clpz4[vԷLq  D6Lz%RqC 8,Bq2,JB"KY*e}8o9j@AAt$ɔ܂4ng[kP/NSI:xrLӓ 1P$Y;o/A&weY"]_+ҡc\Ej^UY Ѵig7~/%Yd$J[qlu@Wɻw[8_H|VΆζ+ʬ씡"& lp +llF?\X`ՑT"+۽pPeZFh97W>ow_PZL#TX sC=d 9\ʜoBdEy>K WGx޽w|ۛvAdu'U'gvJE g8XID׀GhuZBs3A ƃR_Z\i9*L D{bej95*?zЩ3)Ows c^ANFƄ=}m}> T6%@=ƺ48FNȘĨ:&S~Wk$bժaīI=.Wb#ް%%~Lj|}cwT44Q*WӐ 3K>| zަ6lŇG+oy WnvmҖ_l! ]LVgVɤR G=.Gf(5:clեʱz6vY~q&{1(?+'ե Vcu 0.6c1IͦW-)oVUFqIe IDATMōl4kSؖe[FX)Po -oI`q\? G^/qSBS)LHUJ9.b)XRYmHwk+B5f e$f4$# IN|noSolԢ;=<4ˎ", Pf-dY z:bVXr2ݻFeVIn5 4NwRОVWi^폝rCǙH(CmQfD*YHwg$"b~̜4X,?M* }BPsp{ @Lg 9(U{Oj(uRjV)gH֜]XZQ'LV۽է3@mظ#^YP"TB{m 2>GCS)PO>ӗ`M 2s8@OƽoJn%ThB(f1;J34A.3t0d/qKt4{#vA$˲\*%,ːd^f`{;㽟;] I2'''==]*埗z J K CJ{=JR"RB|nIu&!YXNHtyˊaW@K;P,P }tCpd}Y&m3͎&V7x D}ž*I3h )!>9 .  O{Uc7)X2ގvϔ~'?,:!^p{%hoc̎0 Apa8l@@ćG )))0KG'!W^s,ˆzY`!}lR( ]iL&iZ"]u%*c{B3:ReY$}yt%c3X%%4&mP͗==w~!,8 Eza#6^ 7FwXhg${ (!HKɂD@. #RevMO}}Oo{1ۑ[<$@(8&SHRQ4Ss~4S22E&BLj3 Z)Q27 JH~Rf,).(,KyZsZJ ]FZƢ4 DnP4%dvsfU\lL+\s'+ E,IA8(+њL:9I29M[$\NA,Yp.v a; U32$hM'FF9: t>A]q؞pG?`0!\l)AֱCN㒥N /)1K`cGC]"C„}Fh nTn2 Cظ}v( +l:6Du0PF]<اpX4\<}+|;~zIй[cC U._ž=#9 se#=jIG#m6 zW^gKs?A \s'{I} l=qPU_^)gCngKdylV ?ve0%ЬY_(`@5}]JJJ\{ XTm9iRi++v4^$ c@:^0mfI2=XeRMoEAAf?<[Qe?ָLj!3B6LQxB7d#AA&#s'l=6gan̰q~  Ą#c)q6Nr\8?ڞ?   2;  EAn[St#IZ 1ϳMP]4C⹴H2o! ׬?1E" "&^BG-E+kjkW.[bQɭ 2־̪&b1d<`c fH<I@M<47[[|sTouıoHszv`^ՙ$@mN?ӕݥٯ}o5x,@^'>uݱs*5#BU*ˊlx|W4;l+*kWVVTT$yUY%5,+K-eٚupfZqlаsF\cD}];+] p J% DV|p`tJ)(Pn7˕:mHT55%fSy%{uJ09$_MҜ@BYxnBb0\/Q0=QRF av(gt0t:MpNN,rV+!$I jvJRmKM9€&ݷ">7 ̜ũ$eE.}nmPcH=Ip'O]4C⹴H2o("Qh ݳu_n^KŮcw߿|dYh_'44PLsۖ[_ʼ8f6^nY_BR6tw#H*~x SdTXA9NnZ.8i V̡鰷3X 0A;aX` ((OppF WN0&7!zWdiFb9!S+H9nP;4z@N1T4<Cnii) fH<I@M<0n@TU)j7@y͹ /!T*h4:gԁ7dipwxNp`b4׮̔)κK5>qzw{4j*=:[ӝ^^j] qoBO߹x蕷EC.=㼕iK ֯3Nړ[ח-gktpVkD3 $ B"N%O}1vՙ$\N!vJV|:h5S; Httby |@9CJR3R@ a(3g 8\ (HWzΚ.ڧiZYDi*&n9Jb"nܶ<@׃q:JP]4C⹴H2o⹀sU=?^w]yKݵe+$lٲzslQc4髯7^bB?nF.wu~y!>}Un:?xM7m,k7oY n|>UK~So:ݯ>^z5OAaCҬʊccy r$ɉTeRѸPgR Gy '>IL!%H$a gH4K@K<Ǚ珋E ? ȢP0Sd4[yґAIѸTϐh.-̗x" dJƅ8AA9AAATk~~A`Ϸ G2AC|ϥEyOAA${T,Ytw{L&bu%4ħ.!\Z$7DxQ$Y;o/AOj^U~d_sN+Vfg utL.pe) i)ii9Aζɭ\f8vY ӊݻ&|ǬA-YzmX45 /CaMܺjB4?P~"^Lhsid%- 4x/̵C 72sUe!:x`V]~YF` \c#Dm%%iCD׀GhuZTn!3G JkqYidw]j+w UFQQ%#,Dc&CG#l%4ħ.!\Z$7Dfq/|@Mkm1V=zH)7~=տ-؈!rAȍ򨻻exD u6r_DOLU2bwOKW=T䔧e`5֚B1K6RR=AlV%@j5>׾1 |]; Q^ >.5Α3C!z\FaKJ g S~Wk$bժaěya?H%55˳b޿*bG2AC|ϥEyO$R*S^[n%9;E^V:."1)&rի2$/efNǓ+,9;z!fx4%}ꚆS&(mnuiuQLsfQRK2WTT.+4:r^iC* ǧ*b-JFKPuJLlv{C1g;DG2AC|ϥEyO$B{m 2>GCS)PO>ӗgGA% FB#^VHv2Sn9L RɳF3Y<~搟>z M i"1B._Z ۻoV:?$o{)MG=|g?H֜]XZQ'uRjV)"!wzx1$' fH<I@M<1M/ULyC[P M 9nXG,;!i7Vd|aߘ`{;==a1 1 ]pPP,P )ۣ,F{eݝ>]޲"}?DU?!zWdiFb9!S+yaĐ[!jksq-)ts0dE3$K$ & KJ&&)IrEQ]|s Fek횞rsnq?NK{=#q&@#CEt4i`%Uޡ.XR!a¾mo DJJv}RmfI2=½ ˜c3P' PHm mnG4I5}睌 Ip̵CR1vDgAA4vX3\|RDhu#D.T8;%'!b6#$O 3!@E 03/[3:t4/cH%4ģ>C|CA@.!AAqp&/~ AAAAAA#~ SY |kx$4ħ>CrCVf? IQ  T*ŒTfT:L I6#$E] jmKWTT./P㩩peYJ{M ӊkˬr\ {L{IC^b]9 " 7$)BzCK۞xε; HW[r;R2m޺DSZfIc^!x0@K|{p[c ]#12sUzƂD׀GhuZTn1;ر'f^D#h]:DNH&hO}Xf? IQL7>c7$Ҿutl_`,t!@Yѧz솓}'Ht::e?y];=R3kw瞃 Լ*JE IDAT}UeYM'ޔ@ 2,2EZqlаsFULuVvUmeEEUIWUK_R^]S]QlQ`(-2/YԢM_VQYX'.sh`;#u6rJGuz%FgcO a\RS<+k*G2AC|30܎hu 'v=|~4P/w]]T{=yMa ټ;#mټy'r]`e͛zr}}zseޡg\{zWܵwn\S,h|u۷oHwBѪi U!aS MH$"|OaɈv 96gfo]]pJnaR(mnuiuQqsWc1(3s]=O )J6.GӸC{ $ņn$s tX[< S!:3$):~JWx7(XY7_{ՙiB@f`{)Ș%Y4 f,;+ӸCb=!r\;e< S! Pq˲6Dm;οSnfC<|Oe\ Pr9+~x SdT1bGR @$phAAAB4/ A7Z&oLSbeMvP'qN l@uGYb6FIl+cOa?Cnii)k)\ < S! vDg`dd`4t%0uMg,Yy%?{]7H$ 0-k֬YsꚗOHפ8G.-o{3\l3GGRQBY$HTi&o$rr<9e/R#AQ[U?Et?kvHX m dr9IXgt5QqJu7`y(3g 8\ {!m9uO%4ħ>C= _rnG4Ix GT_}E@˷>|.t~: Rz<}Eo{kt9\ .gv<{Ko扇^gq7*=xΕg|}gEr]5$'  Dj^UY* {:w7Ś=iXR*%hz㠪0RΆΖޘ OC6`O̥w@_PSbθnzбVyżCAсNE)))۷sHIy&hw1f\N/{< S.;@mq܎h?34kv:;AFIIk/yB͞: @*meҎ]k`Zȩ<ARo2U1'+$jɤW x$4ħ>CR$)BA?<$ͪ(1.#$C]DC1tn<޾e|BA 3Z%_R̅V96^niIuz|%P+TX%AKG#,07dJ&hW}8%6f? P"ei@㛏'٫~xڷS7@ZOkז]/myF<AD|HͦW-QlEyfL*%pRUG:y9nnc Pc̒ T9TnAF]Α1}Q3~TWOK f 4:"Z^SSb]kl<%4ī>CSvDdhᇬU~u][/)-j~¼vs.醌2 ,{Ά{Y}Ad(3s]=> _aɈv 96fo]]pJnal2"VXVd!Ё;H hxRn7˕:m]p2kYMMuueE:nB/^1:upltG@dxg`Srj܎h u !ǐ}>1?fc^@=]% R!z۶scz)@Qܳ0A)7m #  u!h08n'8=()s{ZOyQ:&d Ĉϰ-2Ihw7c͕s3!ӸB4[{]Ӹ!<JH@dr.ɩs;9HE4CsWo9PFE v{?\w?P={}(dںiJȹO wl@@'i V̡鰷3p ĎgQXQ27Us/ GP0͝1[M1SqJ&hW}ƌSvDd)!_</=ZY<ٱo>xÛ V翺=׾>DQT+7_|l9 r ŲU։vMO}} }9O7ڸ*ƃ3@d( m.6|yJDʘÞ0{i:$T.:|%S4J&hW}\SvDdhG"$(ҖSmSla$rGpy\./ @wki=n(Xw)o}Y "&е{{GY +}ߴ$\N!eqy/7^X`DYd DmNSWKUo.s/MAXO׽n'@ T$ "B{7Old?eYk.z_ /:ZWiϞwF.㷯_;^xA" ._Qk{-ͽ9vjԔ%nzбVy1#=jIG#mf!JFBI͕?v'Tsl 鲳 饱u82 8A?(s VV*Ձj,1jx=II$@2Fl rYU%ưWC 1GoR GChl8<\,JpA%1jx?t$<>؈AAD\09 AO#    '^ 7$ Yyix5OⲎ  /j,:}6bHuy%9dm4muϷVL]כt;;7s.;e*q~wcoλ/o]֕ԁ :2CgUDiy%Uv (XKVR\eV/*1:w f i\2+r7@K#{bl+\28`fHal<CG4@LrP]T.\Rx<93u~.Juxwn1sȄۜNgWӗ~+ 'Ow7auқ?q6" >  >c7lNu|/0:U/o=@rޟNiߺv:6~Dp#Xkk  H4֥U5ˋm1fQWgSSTg+]QYU]$Hͫ*ҥ/)(0g,_YjѦ/(,BSQiiRLlY?+4ԐajZqlаsFU@НZ^SSbYe(ihj g&9i(.qsZ.ߺF^?993u~.b}N/j?ygLLxӷnƟ]ԩc.OWg5h;ߘ>*}j " tB*I!4os'LqwDi*(ηcBaz 傃 b%Yz~jy0((7vY.I-2Ihw7chI,rV+!$c%!<JH@$թ3Pa$3d>CApހ$. AEm[n})o~┛P'_{Th_)9kӶr pK\jQp’ g"$@[WJnL8;;yPzppRxi V̡鰷3XpEҧ- *7i.(@QXQ2WØ A7Zu# j4{.T_@dxg`Srjc2Fi]p ɩO^ :uMg,Yy%?{]7s(/oxaW)x$ŇiR&"(۷Yf)k^>"d#׿D ߹cWSW@_`c)’rsnq{ڇB=gPKDx xFC(Pr<,/%*٤|7'd$TN-})%4ī>CS1"N%[OON~"WW]g={idt;ׯ.Z~Ih=+nݪ5PopX}{6OR֥ׯ_ #d~slnq5lpƵmwyA_y겲U?kO\QCFQg*e$KE"Ly/.QV5tr es5)KL.' QƢ4 DnPƓv?пޝ̙ZDb##cK KH>DT1pB@ TNɩu­''դz"E(WcǽTm_]>Eݍ9WwMW$s܇okyck##d<GN=)=}@dGﻠ:+%̕'?5Wm|+V`Qx%r#/q=v- PqVY"XmfF)#c#mcqv|xbthY#X3 2 ,:[{Ⱦgչ>Uam7-ܕyFNuh'%VkNM1m pJjr+m=oPe[Q@;2]va9UΆζļX@4q%Q5An6Ю7E1fL tSՓ1"N%a3w8~@X? ȢdƢj1HJ[YcWoZlODlLI*ɤWƞ J&hW}8%6&cDօK EAA=xHUQUYYYYYYQb ;] zJa?1q hR GcE%4$S.ɩuI 0;dyY,JLyQ5[Y:7^ ˍ#$'cDօK WyL  Ob&^BA3$AA9>AAATk~~!))r-?ߚ*3 ܐaqz2Fc}F~ ^㏈ѾbAA$ T%]e &b1t#!>9,NQOƈxOċ|"W|q~駟~.wtu6۫P~di?{so_A8@.-ZQO%5n_u-hr+5oӊkˬ e\$ͼ )F"Q:H$y$4ħ>C,)uNI* /} zʍ/Y Ƴ~_x5;7K~^}ֵٱO8<@ZOw_۪8c;LG'^%W{_;彏? _YXWn_t':  CAe>UbJJuUյj++*MԼ,]bCamqymzl1 IDAT:q Q^ ѥG86th9F*4ٔCHfyVLT55%f< S!U)u>uS=xmD 7Y} /R~7n]wx ?y%~sٹ߽ nt g}_kb[/#ߛ2Yx?hϺ O~k ?^%?x>']gwUO:gqI'tIГ`0A4~/ TU@4’۹7rmv+fI,xQ:+^9+c'(TWWV-'_ftzR y ѤT!HM*.4)#\o23'o/a- \}Bn7dg`qYXSn=A?9㏈F.*ꩦO>G%zhr߽=plK\_aPՇo#ZNq'zm}ֻ^> z&?pm߿O=s.*r}^wOv+*J=C3 Eb.)3cVG{̏ :j4J @p{ @P?S'>iBv?zw@H|{o@d$@$˲\J$l!d2Sn8LAhr\;e< S!NSԓ1"|­'':ψmD LyC[P M 9M3x]9iORxEɤX Ѓ탄Ti*(^cLA}c+sh:h?=bPt{8`UGYb6FI\:8]!n9 L̅ߤ6;[j>VX 1gH\a)ts0dg`hXPNmqzRG0qY├;d!CQT+7_|Q튕~_ m$%OHAOGbLm {X m^x 8kJÞH\>NR4JES"PaiA$4Fh^ã\rsnqE9//|y9񔂊P@ Aڜy7Hr r<L$*c{B R+VjQk$_kBR17"S4:Z.L I'uD'# { n﫤%ْ|{x 6x (P[T%)@)c@x"E(xHόh|e˖mݬԭ iۖd['uNOݖQӭY]䵝!,s=^   @M|W[/|s]05`_ZӥY&~Y5pob=u|0tq~n*hc\W))ZF6t,(a ] Խ&ݑZB{G ZOpBkl/6 Jm^cu:+EA&3I'Wd8-k"9g5Cix}h 748^ 1g͌{$4ܓ1ׇgl&^(P6 ʀree[ yA640ݘ_z˫RKk0V^HBUW-ymD!5Uֳ"xt: hZ*q\BYR KvZ7!`zU_@P&Wd2ɿ>T 훗@޽+.5HIp/$IEQlRRI%S7" tvK%tӹ!ͷyD3Q@Zջ{M# 9W]!{547]R{<"I|1S8ۮ|NZE?Gي* (QET@,eks3DJ`5&[}Y,!e7gh!]OocH2?lF|}(trY56==1(a ] Խ&ݑZBioadKKKOBVuoi.>وul'_|n'SI8`6t$'4OsJ1sWk@`˲pnxg,C.+#K0?ݥ@f)FO(a ] Խ&ݑZB FÜWKn+{/?2͇_m$ aY+z_g \L(Rf WTRr{54#("ZI7l=IV(om|XMo 2Ojb6.a ] Խ&ݑZBK:&?֎c9M>={E͍ 7&ɹ=yPŶSqSdQVrz+;xfyI7~8*|rlPШ"#U.EG;qsdRj+[0g,---//!uH: 9pxz@VZ5$KKQ=ꙩh4zӭ@u-@! J4%4/tI :f/o c}rr]ev\vS):޵@kIȭ'B!!)'Lwcz Yg^l?hBy#Ų"C,@V^5$]$Н(uNs^l.t~+5" m Ե&ݕܖ|"Buɹ^J/6 }B!B!B!_8wcp7*Vw-PtGrkB!zJsx 3[>Ru2۫EhtjVǨ] Խ&ݑܚ|"Bb\q7/k79r7R_dOI3JNTSgTLOϷ kNYȗJB_-HghQ\ߪsݫ"7./-]a^z[][ήB@Cnࠃko@9܃jKbNm ՜_K%+"X(6CsYd.U 4$L_=Xz(b.Wѝ {B))=;#IRQ5[el}Eqzޮ׹!w%ىf] Խ&~]#B}f'|xs/_i@X^%|j=$)mW>p ZNJESmrE&F!`?V| )_`AznHe ,!3Ik0hJo7rNOG\]cӣꊁ4׽@=~Iȭ'B!OH#콎><f zYwvWkv2!Qܠ(bp-azTsJ1sWk@`˲p6ʤ8e3~6biiIJ-:m6y/HfkZiTׯ{z{D[OB4͜ S>l{W aY+u=J7qwe4:l\\h(ejE2rМӥookA''߰$Yl!"a;> _E2T^.5_b/SЍӲXriTׯ{z{D[OBghi*/;~`kju٣U?ڃP$8(wؤ~lb1>GrV8>@n?nG.0$saq_5KƷyV瓛gF\KQhJ8Zbmve[ _"6+ }շ Gms}Bҫ7_l6H#|񃁡 tE*"1ؐI@d!*wo=Br{o*rCZU]?@i.~[rmEvYBuхSxdf$Z!oTG!74)Q=xBOi4`Bbc8ƞqgRin$0PZOeH[ `ď+6 Jm^n[}Ngx(Hd&8*K^B %TsFs&VnlŅX 'fTܭ-{jq_[~ZI~igSQ݆ Bq )Oȯ}*(6\J.I:62xFgkzFYvSoWVe~Gsd1ݼyGoi7u7UL#'=Rvqqaaٜ|yfgM6kP쫡m`A$˶Z}4XܮA8:3q'xF\`ej|^[~͗yF~ZI~igI.C": Llf2l%ehWG bNm ՜SK%+"X(6CXd.U 4$L_=X75Uֳ"x϶U_@P&Wd2ɿ> y|BnW@(kZ)J$rf@Bv>}t4d>䨥65?{TH؛y{ToI3;L뗥^ݿFua%!Φ?J$ ڽ{6R~KK7PE2=?Gó^%ww3g]\TDH_ζ+u547z(𧓣lq J`5&{U,!e7gh!٭a(2 Yvrr3S]z@:|ao%:? IP> ($Tj[ D&Qi= G' SI/ 4HtGr)~BuJdMD7Z3/zul'{d1 IDAT߭Kh}8EuTsqRh핉,KH u(9hBsmN3Ιq' Y>q>xvhVJVZm 8 $7sMvH-6m5&X6`hiү&ݑK;B3y#!Đ" % NWZ\>/˫BY].K$cqrbZ; P<9VN}XmOFƦCVnQhwNZN%HԒfty2F+j~oAL#۶= ׷ZX/Ai&2eiү&ݑK;Bk+D`tjjn7nYr7)}˫nT\O= "f,ɹ=yPŶSˈ>%e[X<̓QVT9[Nв z=3xrgǣ 8t<~W:rQ'DNrFuaHB!B:'܅hJ Mi:"^v5gݘ-tHL/6ZX}rM-.kyUD~ZIw$!NB d~~~~~єO> ]Y7g^l}XVDbE1ARhU]?@uؤ~i'B!ucs^l.{\Evm_֪F :l]IH!Br>vcgz_\%[R{LwO{C7{}~i'n\!B!pB! cB}q]q(j2xAWzpwH++=ޙafF)Xg7be?Ò4YG̙vpدIoj ۩zuSY>ӳv"tY50XYObGOWK0ٝeLOBOf'gIwcE88 ZXB哱|uB]j 857+vͻՕw_~ 'lUk󹽘 9ɬ@e6r[q^߰T=zvu9K߁P+34moUH{xz_zw!!q{G UK]?h0xjBi0 JDuMlgoٌ#2n}OȯyفABe\1Kw96;/-R-I"hY|GOg=sQ)ГMn7v%f͜pzL?;;Иn5Z%mNbnPOH̓V}g|2fb_2ӓvSk b+mF`d6;yWl.mF]zzF7xF˕lO6b}Dtz6 ʀ-$vg"3OX`yU< vqXQ$V<ǎ+2M3 O}6V۱ts>l:BCE!411d>IY}ٳGB=ٳgw-N4 VGx`B>S@rc63-VqI(ȲX>JUoţRkqETJ, lb&,d YOZl"{㮖 tM\ȧƥ|AdZ7$Y"6Y؋շ,Ra µ߸^V%nONR?:B]B(kZM$ynә*,U_ƋѰVr勵9jD" ݻ0zD!tJL&Ko,I(,Ilql86b.)@357(IIrˮjI wՓζe{9H5]܌^Wٹ6]W$~ C\|JJ<5r)wHdQOQ8J!uT Gڜ :dOO,~!JAlXP~n@{}jIEQCĆ(@;98vΌ;Y鍃Ke}sc 5Ph*cAJ%YBg3k+%^:{KYjÔ3[oɠ3BOt[N֟n^]*zd$ , ˲a+as 鵳y&5fn]Fa(l.>3s_?yWs&ߊK 7r[M BzZFc--UKkUch! 䞘S˰&#rg4;b':^$YӪT&)HQՋ/V€?!(B! 3#u:7MR%S]= M % h])pa+CQFoN+v~|7\>/˫BY].K$cqrr@.*VL4K[/!P?Q_4"0:5bbF5K7@L㖱 MH|>]6'{-OoΣZRLcK~sRv9~V+TPos]ȇ*&;G ͭj&{q>uQg Qͧ~O'NjF0qݛ7|\JNbI'FQUOoA(_LnnSWS5:B=%e[X<̓Ӯ>iw+SPr.?ގ"—[>40W9B"ۍtN, +є@3t7 CPC;3/67r0B]'GOͿ"ۗEG7u`B! O#B!B!BH줍FIENDB`btop-1.2.3/Img/tty.png000066400000000000000000004311121420276253000145260ustar00rootroot00000000000000PNG  IHDRt sBITOtEXtSoftwaremate-screenshotȖJ IDATxw\wD aHRXԺT(Z+ԁZZ*jE (!$?~Q#& {=#si<NZ (&t9;tгi5;f4h=H~6hq}rW7K=c1:>ϭn_`1ܻ;_niwү nq]7^!90}?o˩sIOH/4fܝ]Ag<|FgKN>6#"2~ fzVw,Ҿ#zVMFSmhSbܢ_k^<1{B~r"vY'#`Û c˖ZktمR66#_v~YӲKgS%?RYhh1LJ$*)ޗ\Jk@;CNM,Opw<L./`WW [n7Vܝ]A"jcgsNrm=~~oa-m }gץ~Գ.øR!#F'eg~\-MyM] @NmN"G={Pjs}v#"*?-5'3@K1O1}θҫ?V?wGq?F?۬FRTC׀Mb" |qϬ>}ǟM~[Rުi SU3ƯT*?D5 o[06'tGl.-QmfU6y5:C=n[= *|#|M {~S%jq.5ԉb1^[DD2$ 1o8TJ)Ou5"䠔ޖCjkjDD4 O"滿NX_?x8Vq~>`F,&&KSd4QMMM'^xHU}焸̚%^]Jh-di|ѶNR|ܝ]Ađ~+BۯleG{K(%[sv]:s5)ER~G/x<^':o]s6MuL&_d4ȳFLT?\R'y-eㅽS|Urkk0x慽[=~-$|b3&*&i8s{_L0U@®R' uST"h$wxw;vُ>GgFdϬ|Wv$/mcIj]ᬫ귬۷4q54jB;Nnʈt.d-Qd>ַfF5E>Ⱥu1晰4O,wjhrzzzDN_`YL:{g1L.jDLZmj9DLK3:_o_%}3JfSMkD(vJu͇*H"wzEήN7΄_N;?92U:ijaS[g'5kXc~$~{j]a֙ +w&~C~r:)RRl4gw.$+y- T'@c0d_o7a{[[;{/IӮ$<kYu%?}Ĺf{Q@D!K:nWId޲|2wvrr]$I {IK&|=Ro;A) |aNg19W']Ǟ 5Q8P JhGMݿU;loj(?0#Ӹ9s4k /@:]׹c{sniM -oؾܠD}=73ލkFRF1GG68}pVz #ڙ@<"-;?"a[3Pf3^c3rOX8\ 4]z$""N"u]wXf}'*{\9pIosk&`>KZS_~msgkGjdhdmGϺ/'Ն3Q]ND0w _:dO\zE/*⋤DD^rth7K?q|*{_]3hX?wG:ܗqnnf.S3)^qD:uv:LrZ&czuuPЛvx @Wu/2 #~h\zϸZ"2Evm|ᇘ rxD6ceL:P"u>I1d;EqϬ>}ǟM~({ ۢ-3M8F~Նg)TږQvm#O׮DDaWCf]:&3:$Hת cVޙurTqة77 uIgOߑbxf!23t^L59S%{ 5VHKq^xHU}焸̚%iؙF}n*;-Uh+kMU[hDZʊH!Z3LmmU| ڿ7~P?4+ PVvm6=I}8/ &A$w߽/ xD@Zp7Oÿ8 ]cnoK`1h1栫Imgӈ*Sl*8(++#xk_m'.(x]2KykW_zuūG|P?xZ]G;b&l^>G}zs$I^߻ncfsg\wE3o$KAݓ˧; /TqAؘDL~(1vJu͇DDdԽ eqWuJ[s:I>%zŤB+sqS(6,2Rcρ%=4/{OrMoQZfAUR%,1L=:INjp/)Z'+c~^ԪKe)7Rj-QuZu iyw.^UvyK@Q@QTƠff޻q>xHS&mo_8vLx ۚ=(@cG dǓgG/:`2-z˰%cCVwZslZ|NEŞ } >f>jeБȸ8ZKa1@Et|{e&w̅{\= sY3ޘkub0R]tbQm0m{EycND(w)n%\TKr&33LVkfG 濹뜞/#i ]Gҙyu>>8eI}|4%F"q)5oYek >FDĿqls85Ƃ^:mF*DYԊoY?qT]K$%Xv;2b,Rb]afyn]Ҭݍ, uЙ4zku^.)#Q[:~N3ܾ\""lnZ*&8q4mVocJ+Rbw9KTZZJD{Ͼ11J{OAjђGy!I|}xdՌ][pkU1- V<{4qfp*昩Gu*U3 MNGs>_@DrEn_!J-}ODI{F|jDkT@yK޺_躦VR~Q3 OX0pq%RU8}Q2r7`f6Yþgܸe%Tg"f/nH!"b$"Dћb-ijV?!""ܘMD$sGS&{ӓ;SWjm2"bx|( ]'Bՙ*ݢ;2IKOHH, ocEe%D5˧6^eS=)*W=%) x<Q7 mӿ]G=ȉ jc`IފMxjA2?uw&HtY<YN 4[zzW/[n4O#j_]P^V8@ȠJj7 \| QlLb- %gىS7{ﭹc?!JZ"2D"yPR"ɨoѵ@FUQT.:P(%s&CR#dyU-C~uoٮ:wߗqȴjiU2Lt%L[2ZSC*)Vj|*"[=a@U0y-7Я&%ܓ̘ә[u\*0d+SJ" qJ#|wzt& o_2kй,A_ |aNib\ur ;,@QZ3PUbb 8ϗjT (((̟h7Ըw z-Zӳ_X8BB2}=73ލkF2ܞv{tȆVmTc+N^z7/10sQ-X.LKRĐ&l^ڨw(*\Mn>jeБȸ8ZKaQ?~O'K}?/ِܳ3d?M=~A돳p6J|߯5:^0?L'G;llB_6GGW]Չ*Kr&3A$***vmݬi gvZqfk""ju.c&,>=k+@E Rn~KHsа~l5|oTU> IyjiUP\^(8w\d2b,Rb0FCՙ*"0$fmy9U2ry\""ln I-'mиx\ǥDDq׭'];#YINۋ m:z[5Dy!IFU5cWLiMbZ# jatD*+ͪN8oiսMEEº)ÏOP[ޭ\úiH ʽVi^63us Y+zH\ |aNibh)7ٸw 4Na^ j[{ $;Jk6@U3P~?_P-P?P?0ahV>ZE~E~P y5#M=4ct[|!gJ{OωZliڢD 3O9܎ނB˴t-ÈI|z4W'w(*\Mn>jeБȸ8ZKaQ?~O'K}?/ِܳ9ĤH8{VRC]2h1 }DD]uU,}ʙO(rJJJ][ i gvZqfk""ju.c&,>=k+F"q)7df?ϥAI9hX?63 1=V{?HĕZ+jH&#b1I*%Pl4TRݍ, Cro֖ǫξS,A.%"RɐRHqh^J /%z\ZJD{x|ʴ3Dd4@ٽ/ ӏx,zvN(2$؈㻪f)UL c~D-0HeY}HU3 Ts>_@DrE._ǫbR~hd;|U'^bw21 <+fww(o|aNAf^4D,/ |^Fcw'x$"biOr6&xϘEg_BD FUIDB[267Ԭ~SX4FKwUO$DOOaNݐś-0lEQ=U1~Fe߹ռp(_#)ȫSgjv"%]>=aw,1LT T/ xDD\^0N5!d&[t{Oz?]I==w8Oj~V613M閞˖9>["Njַ|!AyF[m@Ƞ*$'ėOwp^҃D1N}ï.*5L=l k{TdKB̙ IF,&&Tn~;+T IDATe}_Eo"wߢkn$~kn-jspf!w+5>QUR? ߵTyrɰWTik,q?~SZ&z5Z ?># bBinj|Ks>o j!-H*ZUVy^<ׅ5d u Er18Q;QqE@K^чlƔ;acةw~s[ee'Vpt-[*1yjK5PPPOq܈bi&=ՠ/up@e .{:nf׌4eqNrKeguU'贱?jf" iƬsz~ aԞIͣ6f|6/4}ʙƃAj)DDg\LX|Z{V @E Rn~KHsа~l2o{kj^TyKeӳnHGV_< rGcՅd2b,Rb0FCeiFa!9xseZ* 2NXq%DKKr[Od>gZX ṞUGHgPWMGV[.j3d^_Ay1E$Qt|WՌ][_v0E1Bx+|f / "rʊ34_ǫbR~hd; uOnj~*j8 ZTMĶWTPDͱaFeKJNS)qe`mL1k-ξ#A O(4OfӜB٧6g@\B#DZ6Y)8e&ݐś-0lEQ=U1~Fe߹4<$CFd$k`2eR'*Rx.=HX3XCSU;o:`VzP Tu%PJLF\#xZd޲]u/" !JdÙT9RTDzNG7VKޫ, }bh~g[hfٚt6EDDٛ<+S6--ɮgqAH$28D \(s]\C ʯ^Rdo)9a狡Й=C_ 3堬2zNL/NޕCe}?P?P?Zs[WuT妖+b"#[jի}l:AU}ki?ՠo] /K߱OE?Xxo( ( ]t{7i$"1W.Dؾh4B=6Gln$0sQ*ȰX.LK2zbυޤyVxS8t_,=[ LWR;pS6<8^Sz:.>k-74h1 }DD]NmVd4dP+|hlO&">>]b?f|Rn`|T .冬q4(4 &FvLӏq٭R Tg!)T1=;ꆤLF,6bTJ,;fh4o"0$`[:~Nճ\H{/'CRK"AƉ1z{+5tk{=.-%"=Cne>ѓ{1ԸtwTY"> zC0/2$؈㻪f)0- V~ h_PPPPPP1`n^3Ҵ!7b|.B;y|ǓgG/:`2-z0""m--ͦAzbυޤyVxSPcIG2sf%DD|K"g()cpyH~yC }FGGWZ.QmJ筜/h(rt,yiAj>3;hm)D}}p~|}iJ *nrCָo\Dqc;oGGU{@"* GLF,6bTJ,;fh:S0Y4ެ-W}?Yƃ\.KD޽!B ޽T=.-%"=Cne>uSn66z[5Dy!IFU5cWL:A%0- V~ h0:~@"fU#EJTs,L4SuM|UV|J }픚|ڜ+/#类Gna+}WIPi 3r(έCIA^}%Oى(&#tІe$k`2eR'P2up={ypE~L]\SsߵcsU{@c_IVB i_wjRDO%Γ-M BSweHS-d/ehHP^V[n1;2h*? \| QlLbmj=}?e_ SmM3Caܿw*:P(%s&CR#|vw_[z\$Ϙ&r-&`8vNJiKƻ\?Yk0qHŝ#J@Eatx8,pc?ς7屺iHHX7eqJ#|w_ۻ5+~cXW# iARת~By kd_@uMB2E;9|ݧO/ܜټ$Q9>Qr҇'~qi2\Sߊ:P/QYg}렘BBjM^ zuNnL{xUNJrM-[*]Te"W>|5ت[uobrP\$p@e .{:nf׌4e+ވ]TI2:mZ٢Ha3磂qUaS\hne6OfSJܠUFdCQ_moRА\ZcwdT?~Pr2s[d$Xk}c_C~w'N)SuS*dHj5b11NK߹srɝ;I5?1@T&f`T?quT%L9FOSCi␊;wZA9%. X/zjoߣ 5RotPy|Mbч>h&J)ÏOP[ڵs>o j!-H*ZUVy^<ׅ5d u Er18Q;QqFB 涊>p#;fa u-X'ǰSnLֶyJLl'Rjoח;50`W*@PP1`n^3ҔIDȕN|))VNkm}ͺWbf?*xW˅i[5T4PTWۛ?"O|ʠ#qϟݿqwE":=,| #V2rG⇋l͇-c:uV)lL'G;llB_6GGW]I*Kr&3 {дZG;fx5wYA:1֞r~Pv}Ҡ$4[Go'*jjnq[v<;2lIR)z eӺ0Y4lUgϩz wesdHjP$8q4FwocT=.-%"=Cne>qGpkٝ`ڧGwtQ3m4(!q(`A¼Ȑ$c#漢kk+[T+F?49f&:&|*+u|J e#v6=oi N>Uuz8&o\@>FͱaFeKJR)qe`mL1k-ξ#AeG@267Ԭ~#%ک$xx[:Z1I&rY,4_>na+}WIPm3r(έ᡼ d.0"#Y%)*W=aD<3I$> N20iapOIO篻k5G[v&f)ӻz2v _!7(\ZcwdTh˧; /TqAؘ͞c<[2F?^'$6<»iT^u%PJLF\#xZd޲]u/" !JdÙT9RTDzNG7VKޫ, #;?tMORt(lՄ|~È!.x]+ "aMOP[{ӊk6XQ{geĮs3aiy>_[)70n7K!k7y. J"A²QcQuAݿDB2,@5dbY,&I23`3U EXaHN<^ugr<.z^6O EGcz6Vjh @Ex<.R"8VSS5NG@iiJ0kwd(/2$؈㻪f)"Ug1Bx+|f / "r q_ǫbbZ?aͰ#rK޺_躦VR~Q3 OX0pq%RU8}Q2r}C6&xϘEg_BD FUIDBhjV?)>ş ܝ穫SM56C Yi]a}VؓXLgnQ[MCyA"]>=aDFK &S&Uz) x<Q7 a>}ʯ%bc:e52"v[ge3ZM鼳yrV󳺥AhJ^i .(/hV4Z.>~ U}\z(6&vݺ=!Tè**[\ ddHj5b117eHٯ-PR=.ܠiKƻ\?Yk0qHŝ#J@Eatx8,pc?6֝ffUW&\g7Vn;"&F굵ò IDATs]\C ʯ^Rdo<_SW4?⼹dULur ;onZ*ek<U%&6@|F@BBi @Y-i!ZE~E~P){|7Śzߵ}M &ughK7#ҳ~=#.^8oxKHN.DĞ;aU7g~43FB@""o%\kd Woտ.y|)kGgt[|$?=vڣ?a^&%KHky4RHDz}MߑDUbM]@f1/py"ΐxO[}_ng%9J6ҞR6EK%/sҴ]˴&xVۥDDįe}&-WGzi)zMZf;p!OuA E&DM_[<ȹ'=sz߁$ ɴߵ.4!>m/&QOmވO'z&#T(*K>7J6X}ِE=YMxbǑ!ҡC;5֚c;oGG뗥²QcQuJr)O؎yʛ0'-J[嫺XPǯ8nۓA9׻_#gzu}ַ}& 5~ѬF9V|b¾> =kMD>=\yFqEOn >2v$[kd$+)}R:UD?C.D9z.͜+Ӣ>|DK-mO.\ŀnQUUCeWn>Z݀>ҵ$n{2_L0+c$f 'zvfQz\*[3JQ>>1ADDmٗ̂suz//妹A,GCVͨKsmSjgDi6vDߓ3ZBJ?~;Z_7돼fKZ555I}mm[XO^5BmLW>e[Aۏp:bطHv'!câxKDPP;WE)JI "\~?#77373;LA4d+|(v0p+ʋP,RS(oa32Az4ܬf(O3[D؅Yw|P4Cy>P(TqIq2[4kjNMZ ̴4];\@J*R|Sq66&C7 }W,*,=z<9vw:e=׏5ԅ}%FO󫚃a6ǬV?{Hg`ur3?|JWmzl+'/"Kl 4bOњ7a#upB!Ps/|bş'\I+8q3cZGfc%ꖽ^}‚7p)#KPhӈ] qj1xE!BL66tsh\ D~#h!B!$,? B!B!pB!B!BB!BHXMW~b>y"۾OJ4(+N|Ri)^J_$}V1Ů:[s^X!8B!B\Q!tZ̐gE$kg BJ:^R,O!A 8B!Bzf~(jz.LކQ."*5 A~IK-`sY@hӭ<X,,pqEO^t-]T{ I N!B!T?H#Hj6謔#p!eظN|>Kһ\ȋRLZ^_={% k'0e18r4Ɩr „*R9I r8u#B!/8-!P]%N&DR=Go@oնj鲔ן@RV4]^9[w V T1~ Lԑ'&XE.B!пLd?'>Չf~K((#ˠ_oɟ^V;M&|afAqy<2aFD"UH$2ǫmuz1C *$(w(}ʮEBUKKI  % 4)ee/5ScqBpsh, }7 %:CMRA&*Aک)1(d ]^MU&?&&+-e׈V,"KKhuj]0'7+KF]SNJvhK,HuOg`oU.xqB!ePܦQ>NRXNS6׏-櫠Y oz2A1Xvz?f{b'?SQRDR,q2cd6/[|c38UTS8B!B2a1FuE!B=j~cB!GK_xb=!B ? B!B!pB!B!BB!BHX8~@!B !B!a!B!$,? B!Uc%!B`0;!B?_B!B !B!a!B!$,? B!B!pB!B!BB!BHX*+Gug5-9.߻B T7VB叨!Z\J[ 85r !QK)gv@Rdv;fukiWL$eɸoV_5pHg[kWʳOέ 8&=qϯZhCn;Q)I} \z[dG; Ftfک]!ƶ2;F*wԵ2UdjkSr f:9c#Pji|%XW Od7Ut7 +IJIn,Z(.%PuNiuVPʺl:(T9A-{MhJsRX<z᩸[D0 &놋':tKnҺ ONƸmǗ . pOpݜ%{XD?Z3e/Oo0|Hz5Z>QsiL'7D(ΫQhywy vP(VGފ~uf{o[>2>S;DHT$k_QғL}Ʌ?[+ }"'E?~OYi:3ʬֿ G^ǥ7s?86)!be~Eg͏aW TT)!gw] PLShJWR Ζod LhESuכ.?ą\1B7²;"3Ɍ7@QYϗ߆9)vɫ a`}b]n>19qUк>)#LKOOgޘ+)OlrCޏ~kUO1A56aIo}_RT{E/=}R QG@> 1x@/e*6KU  A$8뢣'w"B@Zƀ\vkDW^WI2:k1V>rߪ W_IҒ~Yi9츛oPjQc%n@ivN=B ֙;k)3}RseJ驃]xԫ  4x1۶m ^=6(fdkז8f;Z\Լi^ Hƭ+*$~U)6L^%s smGh=I%ڞCO {˰,onjvj˜*r#0fIj5Bb4Sy\g941ҋz`5U{Ų#~{\_fr]M]:-cG,[zc]:YPjdf6I$bt16 U1q.[czm/M tŦg^)/T9$O#54qio p$J/`@?B(z\V@hN"U)*[]ERyG&kC[VAZ磫MWg#{G3Q?K|) hԪjC{G-d}6'%vԟ@WgZJ2:sqeRy9 rވʀ6~YB= 2S?|D/NrnޱQ7lD덓.t맲WXJmXq̻??V?6CTΑR)~V]_Y[E7d2F TUTR32#ҍϼ?1 $2@PD2_$qvrdP1 EU>/JK+&3M@DyIf'Zw\o\8Nב%*XyIqUBRQ!~:?\e1\+;IE f \P_\jE#/-ox<Fj[MfAU0!A(d d+KU\VI39*5\   ۣ ,^57C8cŮr&M "oYY_& PS8g{f%&J&0&}Sk9q;G1fN]i}fZ9 㒔@b:WD*yt-A{Һ+l!N2i[v3:_X찅0q俊H?^џzVRc b9x*t]SƭyMv3:s-R> |Qnڿ=td۰SIS)XT")T2Tk-._fXIڿ;qXDkW~.=B̂>K_U 21(|=V 0F:F:*$;^GJY,h?dFAj9[Nآo3j3e$6R$΃A78Ll3wezK,}br cjpNE6JEBn{եά²ʞX}stV'\"XbCDpKKuM+1-LPPi[/Pc448fg+N-?T {Hj-5?ifh4YBk~[zMD#SmjmBLU5zk;m Rrr`hr$ټ[|_eS4'SZ_~>|B!PHbyqY4pY9 Ov86x/[B:6w&j(vUco4w.|!!B!BHXyBMoGsj}GEPӨHB!pYYwe&jx aMF!a]?5jklIg_J%gsYtfvg?Qv,)|L~NzKJ |f|Uxևv89Pyᔊe|LNmU?KQ!9`t熋o߳gtŠ=#1͘:ʳOέ 8Piкs^qIQ>W̆ ^gdzc{1"_цRcvyEHI[ "TҠu矾Ȫ!*|aտHһOd7Ujh Wy&Iwb;|QBݎFT"9ә5 _34ݛ~dϯێo׈'꛳Rju>S :Mb'/ϐ~z4s y= \xsxVˣn]xPҺX G3;W)8&r{j׈,pz2/YWM/HjNb܌RG$hTn\Ł&+g f}{Jj]def&iK)|q:;ߢ{ͺ=GEYAםO;{ı8;v@QYϗ߆Bom7۷ٮk7*/_ ~գ>} D"89 7w9OS 4G\=}+XXy9999ٟ½4PQz5M9a/ 6\|aU=6ʼng ]e2S9G'Ņ^_EATo:NL Mu7mR[zkn's?wnB!CӭW;yIÆv̄c/=J u=>cG|#d2f$YYYddd4m_sv>aе;:A=~7!!FF]=)(.Gh=I%ڞCO {<z2xї-ͷ{11aJm۶~FA32ڵk+|̇kk[eS@9 g8\j2'7Ӕ $X+oziIWΆ6M$Q0\4hȭFRI$8뢣'w3fin,@A~ple4}9ˏTe5:Cz+W[XL1LڹPvՑP"v9&F[BT@lI,Wych&*(u+םHsxn_~vWxgEL138Є3gjH^^͓O|A.Q-_/Ot9V뇙Yo I"U/ax ((9׍xnY&:vI\ _m<$re{9mi?9i)^؆-3~*+##ԥK?yڷo P~JDj>kXf&c] Y_|SQpkbb F`N1[?RSA:Wlg" "Jr\.I$|g{7bR )!4!D1f/q'Yno+l`{tKU3OFL&_%[Yk,0WZDK{|O(pc!`)/UU3R\Sr6T*슽i4 >༞[zK-溮/Vj=|XB'^Ƨb~0l֥L HU@|+U8NG+f-yyypdu])w/I ^՝_) rSJ)ɕGW^iJ<{fёf.s>޴9{y Ɨ 2~iow S=< d PVVF\ARW"X[~&& $6Y ZyI˺Gz.6Z4}{Kgq=$+|DUkRJqJm{V+ڵSHOS^3fEJO$63cMo~']]xe?E`m9 rވЉh@Ҫٺ1222@Ȫ9}zbCO%$9嶳 L瘎:ѧy3OH1L~:yބ}ɹ72*q5Y2]lO.)mJ>FcY4Sk*()1х0ejd0c*Lg_ =/wX\_Pd˩'[\&hͰ3fQA&&o/d8S;:LiY13SWlV]*H pEqP8BtM ʡT`Z-2g~VIDOd6XGk'*\&&}Sni&Oil )tnL5)ۋ+j<~M'T}-C~CW7GQ{2/Xչ"859Efmz| HqIV8,;ɤmyqϊYs$#d@D\P WT7$ ||&GEԾkc5"|t `bI1/؍I ֪UFܽE` gp( 0fLp|^k~' lrfSe1JTL"%:!?1/MXo)/}~\ #2D״ +X ADcibbzscrbYtg̲f w~|tOMOkƤ͍)^V^t`x99K: 2+0\T7úM۶͸!?h›.mܽr#9vvCfgJEULHI;NZʆ'b6|Ӷ^X-ZXޭ[G.}^Ii͍DQklGUd(0Cs[s?'<ܵsB@sdC70Bq|?`}8fN3,>87igGFUb8)v҉ԢmvV)?U;fkڡ GInpc\G.iԅdhYA1{.7w{Ѱvٱq$0YN:qSW_a߿XK`i=nK;[hgn9M7'\ SS ۅ}EP̨} xj^?ӫf~3FNZǍ%O7L؝8q!#+u#_=F_O-7h0%k)#+N/r-avFߤR>ƺ%n@}mgm]Lz9I+կåhlVб3T6jx&;z]U5QZmIJ0ޤ 5D(ь$zu80 CVtȪ~+wUjpѣ(G~Onӡ$C|viѷr>]\/୻ E1-[L]ݮwȏ.\RuÓ7|dǢocfuHuU@ZP=5glNJi . -gce[86Yco^x- fo%j֣5 3,/=jP7sm_u6_2֔$&緒dtVҤA]~yq<>Otؼ]}:6 1Y}oz+NOu~zL K <"b~~>@J+%IPRj nD}|]Jl{'˳ FPʂ$ RRR 3ഏ+ͺͬs=f+-EK: X@ؔVGRٜ:h-~sQ#EX.G]1 X\eNk[T H 9zd|vQJ`D mx tg>:̥uFS^q>޴9{y =vjC3~:ir%`~gf8"d[`D@qEg/?0GB&!u*fž+ i̲l@c|. ~tz;0TFtUT0rBjẖ;{)u= y}FURi&}aws1s<]\uqJWSQR=wgV\t Y G M3@skZ>$7-Vd'xh4m>\/Ɍvq|^ ~σ=}t8Ye!rs: NNy3o=̼ͩ'a'aSN6#J}u5kIw?ݰeyr}FH lju9c}lJřNPhӈ]շT^l,=}pخ.eSy[g< HEMm1G`j5·ھuBMj ?D4@=\ z~}q%"1?:A%ZƆM?MQQSDܺBx}QZ:ϟC,GEz[e.; 6q{1h9!wl"#BwhD%BTkV9|0BH ? B^Kg=wAtA5(B!BD3qCJKk6aB}NxuOdG!B!pB!H-}B^Uʳ/EW8Hj=~`9QP }]]V[ե[Fw^Zuљoȳ'W$ AVk~3 ,Ʊ_EuS<|?kjg_u>k} qs6,bB+Uo>|1mTBCT_J>;}ޅj]7U->Ҍh;z w2,,nT &Skq|W2-}}92w'fv+yї%(Pʼn˕U"n] LYy7p^N) -~ ~k儉FCRBH4Dr!@0=h5-hI<*uv"d2at~aIAyH:dtVҤA]~yq<>Otؼ]}:6 1Y;35/UbS<߮Ws(w|sp?0'upGh1[,:,΢u46aE$ xyery@nm؜$~O*BvaVf&33!g|ΝE?d'߀̪mm:e 7g\WE̻3u-.y{?sl FzpdglEJSh "EJ"}+JQJ{Bdd [Ƴ~)#}/xsι>ssWRh'vH\ij&v}1BL6og IS"ƒ,5DaeQ(2 MڳZQS )r{(e^:?PFF|U۹b 4=('$D~U«dR(eD;41 ?~r-g%eywxH6^*;w'c Fj sO|v(?^ɅU]dL|ӿA;=nbvuU]i.۩aLdl( 8xuMǔR:KWŜ{`UCEaD?RS ҃ϯqf# 3ػQ˷c63۞9ſ Њ 9gEV0ͬdzps=t#kHCIH#轭s_/א*iD}>"H1sM`vY )8nPOH:?omԓdCb9lxn7뮸X dX~ZUF)ngٷV;crRIgx=qcN5o IODgt1&|5?Zl]'.y<=ܶT{ˬﳐcoaN'Ԏ^y~S[Dt3THhTfC%r¢/ i,-ae)H\k8SS˶"Hn Io27Q awnN^i8q$@7őǴ7̴]UjI\ծ&.@pԄa|c&H6??? ?ݧO#B.:4f?-lS`a}Y}Ƅlxuƚ=lC)Ah㲊}A_^ nˉorN[ h@Ae&EPnems1GLn0rs z|G@#踤踤]-63^ Ǝ{Ƞ K +.~3Ц^ DfF^[-b"Ѳniqu#Ly0i2S8%Js٧t =kѦ~qѡ&ǻRqC%T R_qe;gc;ܣx&oL 2'*NЖ{yJ* 6]Jt\]Ϝ ~ ى 55c,`g*EΈu6,>6^H"Ag @"F+; 'GL;D8Xc``x@Zߣ |m7|:^@M١7o݌̧:, 7'K"0X_ ? ҋFj*j*js˹@ $VrF3 H ahJY*ap;"nHIEʼntov.(l&Va 5-'|fV.픜 7U}kt*y^Tf^pfgGV^Fi¬x<@[[Ea0q^|1qb 2T=;[.VV#LLK>&4Od@nЅϼEi8iP\z!7ZpiM3e{KAནK̉R߉F2_S@VUےZξ Q[W4xNge.[ lL7XJ|rXkȷ9gHzP;&(&zDnn)=uuu$nH|o"f[U|R$1L{ geբiHWvYQ>ڨ56h UҶ6*7x<wef[S}>=! 37u=0TlZsQ ilR(T@Iy9pjE_Y+N Hg53>d2/zFt Cֶ_7p&2ee_MuKJNJߥo<|3>@!=PC )4gpX$d @$b"oLn?* }ys&~lV0YyjcI^҅fo-S|z(`e|BT6gu]QÀlэ iIʘ0ǶG` kK:Dq8)㉐:usϽjy ޖ|hMk+#Gt44%)/+XlWq쵩Y> !e:u|8Lأ" PO^h "##x&j)p 0{VB|{<%] @mQNEpSN9VTu<=bvAzիGauʎON/m[Xh|{h%N YVr,1UD+=S ~b~w~W?S.| ?QvLa^n/Ñ|_3}|t`6@`9d1N1 LjӇRޞCNט#R֧mGjx|: 8Uq8]g?6jwY]Ą6Ǥ*=]n;ZOFDJ\z^bm5˿ v~sGGa >ÍvJw9masu֊.!Q>1|c!^GQowKO3j>8סvKtnadື]b[ZT9z AzcZEi"Xe 6w._*̟y ?  3qO[t|!e_ H@Ag:gA>C"  Wl?  ](|i?   W X6m6RDO! o? ]yi9BL6o牻%=K&GSl¯"j\ O b8Ϙ}OPy;$.Gz]aodBĔ /K с#^u%B)ذ~~R;Q/r ޼Jp_?$P(N3٦lp̔G+ t,swrzk]_]&'| IDATA~{ *H%7S(ɵDyÎi]T̼bB}) ;&99RqVc|t8EenJq1X͎Jk4,3t7M}^ d'Vk Kg^m'[A.Q_M|jW& 3A^k|ӧ!tF=~ُ  /Ŕ׶;N@QA(;u@Q=o@ӝWsOà8׼n+& EEk\\vr tD3A 34j]Ie׳jzX[[ >y4 t\l TWWcEF4%z9{E=TG[Y[y'!p '\v4(M{[. ~0Sf&u-TǦ4FGRPN(`z47Q_ڿŰM^zbLII!vƎ{Ƞ K 9x~3Цo T:Zc_[h\Rt\Jy/9FE%G?5GQf9^oׅAؕ;`LsF/#v>M3vP@Xey"'+])ت80Ti\T:*ǽ2 # I-t:g{@ HB)(:X /C~"*cqLM~icvYs HiU'G7V)l+ϕy9{ Ʉ悸Va%z,u<]B,KYlU){'ߩcUzd~pGCD9[>V6vQ}y``AbJڵr'˕C?Tļ3P:TUc+蒝k?z魌j`z\_O44ZHmIN-oMdD:&3c`䰤6:1YHZu$nH|of[U|R__^9w,uڋW\bn㊋@S/aרI<أ ÿ̧oJR=7ANY7{S|T pjE_Y+N`zijke)!o֜(`-ɉ1֘LSvEAjmjAA С߳{PAzUS[ P^w .9g A[?R%ʣ/͚APZm^&ֽ}`‰P9]Ef<ޮu:9pK U4yZ'@Efq ^w;Wm҆Nfcs~ҟg[=6կS@K2rv7#Ww3͈9;6>ZI'o()WrJ)?4x701hto[K~xJ P\THÎc/1bi|zL=L\cmQNEpSNY' hwO]^ꑽkX]5BMd*}vLDq28ذ0<0ImPJw:NZ_jr2)CE%/Csީcwo8ށ.XNN[o\y A(q![b65ekl-[+^߶qp'|,6|mn6Lb/*νt@tlz/#cM剷l{n_9kVҪ^c_BI]ݦtڦq%N H_`88>MDmZ|DS|VNG ˖6E*"@z%   }^# ]fJB  AAA AA+@AAPAAAv N*L6:vaT'wFRaWTqIao=|J|K+VbՅ𛧷MAa`8/sEcclW~͑ Oߦ$a'vH\ijbB]?CZaodBĔ /K " JYH= R/](;n&؄9~ق Bқ[O2#AI`j.JUa{76jVpLufs5a;3 C) //{">cYX n7hJM{E`26BC08X:bDI[rA'eBr!=ksz q2^ڭi=,xܖ#g}\Nkzt &E;kidϔ1:vF?a˝J:9 r=_) ̲9> Y"O"!qw. H,Җxr6h'-[G-8=S}.aI3 [B1px<psMڼY% Ƞ[NqG}~5Uq@;mɌVg96 wPxaAۤB<"!m3:v7 f(i٤/8@pb=z 23@/33>O<{RK@ $VrF3 H4 pa,W_Srn>iՑT[ڍ}Ĭ+7Nf{X4A+-X|׍&`; 4Z(0TZ2SUU)o[5No"? F\]rv47@ISgBU:i887RVzz.@uV~ȓG>@CcrfD6>{}5mǬ4ʷ(^) _CE(Џo`QI?!!'+j p.:M^Z. V)uTqUC){|_3}|t`6@`9d1N1 LjӇRqrVA~cvd?lSu׏ܺO W;J8gk;.d&oXkOλk8=0 6oWsN3sׅ'\%oٸ~gO3j>- ٥@bƶr|e~/j9?_:)6U&4!/Rh,AA|v[y c#EG,*L`[Vqf{}i!_᠝fexg7|X2rcb-,2F'lS Qg6v,'sR, C#`W">z{cԌ6{Ћ@|Bal%룎 J[8/O0ͬdzp[s=t#kHCIH#*:,}NZ&szϕ!{C㗐Ǵ7̴]UjI\ծ&.@pԄa|c&H}?}p8N U`ddjkieb"C щ@@N4ZؖYkz6@1VVҰ -e.[ lL7XJ|rmO-3waIeml6Gm]q:7 $QQ7oV8f[U|R$Gc_/@~E. =t; OS"FN.@O\dYK2c0$$9&^>wo??٭;kۨmT*`y2?,ɠY ~v#Wws?mіN1e-t2I)Ɠ99(? ", < i+Lw:^= EO#2Pwj>zL=L\.(^)Vs&Lj|z~%(`8&/Z.J::Yփc0V,z?*0/!`?,  V)怊Gs.Hz5=c IDATݒ|_3}|t`6@`9d1N1 LjӇRqrVA~!JQX/!2PO3j>' A>ӵO. AvmiQlgA>)JR/!  w   H_   }sAkt;_P ?   H_|+:}զF*hnc%  ~ x x砑l?gb~ud7gDS!,`G|K S7Bbf-g%eywxH6g:R>K^F t}w/jP^C~CgL}[%:bȊYDΓ Ho 8Y1lRi,. ?_Z߇:x[NRWkj-! KC`j.JUq8mUO[ovm, 3ػg9_e,;rc4["ꬓz.UQ1kl ^vr"i䕗Mf-V?K$l[OE7 srG#cg6 >~Q0w[_gTҫGN<)fJBO:;tFx}w\-C>9`Q ORo6.-)-{{e!.ѵf\y)$ H,AW`#XݗıS_?g)?&=YZ{^i bMJ,VRޓgkKu- #2^hLp1%ԭ{kO#تr}*<o `N3lzX }kj, ˜3g `]~XL֐^1$IM$֚ _3<L OHI}pm"w4>Q1@WLy|bM`vO ) I.x*hFRȽگX{|q㨮Uh"0+5&n řO8]8JDA|osƭ;~> aiЀ{whu,Ky U5@mM-ʸ^^ ΢Ԥ_Z=!3έA#}u` bL)K?tᡇY7MeۅO={&k$7 NhWH3RY7yG,qƎ.!3QJV5xJYئJf. :x76#'~t:_{\߼ktfE֯r"'ֵauaIdt\l!x0P#FsWUf+EtdSɦrq+ly_"$v ;6Y.:(6iDM9mL>v"a^<6H,ZERG2%ZQw,):{hF? ÷PʲOh91f|>Doml踘L8f`7;*e8iӰXxYɾr1X:A'RkGFڮ[|?jֶqKwW6>߼DeӜV/}& 3A M9?Xs3N]Lk5)߷DmUEx#7`r5fTI/miH}) 4goeLQvn7[wo5ϛy}~ m8uv E™~ansGHw+k+~!5..K_y;Mq:l" %WghxF]]㠔Q%%F_\Zfّޢ4gyk:@ӣz;-BS|U{[~ܟCC#&7N \( b?:~h :OgHi5: [wk%GIbk''wI^wXGSF.?|v{3u 'o7/,/x ~p e>_Ր}bp5 $$KH,(R͕'isʪ\+[g3[Ce`yhEa õ_TPOQ]G#,*bN֕e,898*[NUYh$9u) TBRjg2l,+5#ƍw2 PX@v.hU1cZ_]]Z h4Ad)\R*y[G}m͎ձ=[h"`W6f{_wZzP˵z'wcX% 5 kS>+ى\P?7RM١7an1k,p9R<+WNPXܴt(:D?8o6^JZ0-dhFZ{[LTty=Fv]]/D~7^|zͻhVjYslQHPjհV=i<0J^j{殟e1]A |P{AhLbCD108o5jĪ.i hxs|}hN̉*!}e`yh_TqvteA·# Xm;Xߌia0Ea]2Xz;iniGe&291? {_]p15ɧb\X,'*‡)~vY&Kg $"=1F ؜Du@\H޴6b*ܗz!U,O9op*}"\YĄX3c, U%eb$VywN ucZMD"@`%4 r.. ];hoS -#AOf *>hWw!Ҥ]=A% UUԷo悸ΆaFu#R|(BqkoImKǾnA}W~bJ';N%ⱌm1gK)rp Hw|0L&t:`1W~T*P.;è4*H$ n;S!U[ԆQKi@XBrcCc5//kD/L#eH;u8@e2~[=TX9~1wYhޕCYWwao,g`"DcicvUJqpyh>Z kh.rAo":\ .E}Y:1JVq </uc+t`>Oў1Ct3鳆`077 _6ᓤ]O!P>gsv襷2!d(Fpwr\؆$i4Z->t[GZUmlKZtjy 05S&~%zmgً+W\(n &|LK*k{7TxH{%,,m9C҃1 E15ֳ&~[LV_^Xy U{YrW={׈IcRWfskK彵!ji`{eOf([O98&9+*·ZȠ-?c0ixy2N8(+1p~?Eډ$z‰P9]E%yX(Uՙ:D"HC5%yyTH+#T\q8!׫t%m~.v!=ez+ϐW!ud50r~7Ӷ 8c2GE_?Uhj-|dd.]Zb$,`9%gϔ)+-cVrR}IœS ҟEG XN 5A A$b"o&W)_¼9e0J2kt?|z󔎤C/4Zm4G?yj,Ĉ1J 5(mخ.&((@altu*b싄 tܴ٭_8$;e<.ܸ|KIɃ̗Tmh]Z@&g=ZFkM-FQNS5y\L;KӾDoI-5BZJERʋHd ?oТŒDJJ}t$ff91s9<9{66 " vuH _ev d-&&'[jb=7:'۱b'߽ɑ6 7#CzX=U9%MuE$+3c݀&Ѹv&[II{yœ y"S$JQe 7/qyӒ߀i-=8o('ƾ7]q㹌a"Laa!ɣJR9P)ߑ1&\D~gTS})??@f'dzǗQ,k</\5[Z/]]P2`%ǥUĥ=߭`/Kho<P!=^ $ӋsW)_TܹOKq.~ց#6{7@kw[Q}PVRR?D̪?T?ڵx/c璝gٱzdaV}oYdGmqYߵ7w}*on5S@ez1{V))=-jk NoD?wQ(4${Xi]3zN#n^JnbB+=GΎ~ȝC*nqíyzwLvb[c)v}mGVvp޵` X:?|i@ 0.t84UvZ? EUV)AZt/Z}2hi@ hT';4p*OWqtڍ6@ @ GCS4gJjh<@ P351cדȠG;v_mnF_tۓ{Qҭ/56aϢžGG< {屨3@+> wZ}y@皚;5Sl]v8H糲2nҌړ&%ztiǸNd񐟏!/j^'6ʴL755+#Ԧ7ITVζ&͈_Xe$D?[Om1-r$&_Z㻽Kg6}_49rӬ6=6r,-S@a8S]^1N:xoz aI)t(?vu7 OL7p P6sd;hRF8o%\R[rFkQyιtxcӣmՐ4v֭SǮ;߱qt rq03?:c1CڲuoƄWheZASt{mX3#LFo ꞒȠ5ܦ>M*9 NQ|j2 `4q+9zlYҎ@4P}\PR{q?{ m䊈?⨛'Omw޽5Am_VFVڻiIaj{n~L ڮ0+1"ݧ77#q}FV̈́n%};9C @n3~'20S{Agq23322<牑GU|JJK+@oତ#˗^:=k򛱱7k4W_Ґc>;o@d 2{J$wZx}k0T]>yIx.@ы'O:yuY#zյ:B@N=m gnLr{RN~ >$ǿ PѬ%$MLO0v'tFK)ӓ;^b62\r'k5l:^[7Op1>򊞌  <+ϕU*"з0"+)ήVQ**s`R %*M ORamXy&KQ}{J?Z`O4I$-Q soW>5 CC˫ =x}5ګ(:qW?:qms3%ACw4t:{j翑 2r?%tg/IyYNha^ ]tq"nʆkn2LF3 Sc|,( m =J۰/rҭ! U]iu> S'KBQ훡O.D 1r$5CZZ!*-R}hS iIZޮ=04$i8GFUL4y%&FZn4ҒfHT6~Sj)+T H)84=y^:PUPP/;o< 3)<1QQ%HIj:5'FZUZd4@@{78h3Y=-߳כsr[] <@qI  jjj@Yջ4nsю%&V;'..^\\\ex  (qdlHW;HK2"85p2b0@Z.]u f7^YIv=as_v(*.B`G#Ԟ {|Ecǎ;rW]+$'x ,Mt{鎙AaW#͓kϟdde1Qyy4$/w<,nl#S_n1*ȐËAjri}o]} SP]G5}ߦkۼ 6ny9[IRJWu '&*nӴy_Jѝ=Q-{(:"Ap.h!-Mt)`1\䊈?}-c4]9mMtI®>aM%GkfhH zwfeDJꛘR>a݇R3b|VI.N QEFgЀ%Uk蠁RC7DY3/]dc}@(L}: Wu[hg=YMە8,2ر'N_?iߞ}mo`zU]5yǃSJQfw,%>+$ovdS`IJyȯB]y%7^ISt5o| zm9p"oeNY'Q›k֝xGI'uլWLPe9Wwx浽͇&;p.7} }\gjܞF/e>g&X)LXXMH#Ȕi&>^휩l; (,Ŀ-Ӫ*&PkEs@Ƌ'ojEG+@ ?l?X0g)3_>ϳ45I)YHeb1Xl&5켄\Q@ 0/5Y&xZ MU̿@ @ ?Dkb6@ ~odj :r'ȬoU@ @ @ BPP;1`{̇F,)͋#c>O'Aw]u`ijO`L.m5a ԝIģ*kr^pcGȗĤuW=O9=EH:rˉ~aO?8ǴPg g,Y{gQa/4RjցIYwq&i㷞 yyt :n۹AdGԝCjO4F}%_~ߗkhK="I y;B7 |̂y2DO'|ܑ>m%NFSV” #u&&|P' {0gL nz4UpIA/!4 rN[Bלw-S,33O"yLMƱsfyaI)C$/|uF?O++h+V&|_"0!DgySd2wp,N/cZ6fp5' Z;|ܵ6Aeoi*tخ#ir4bյ>~]Lg pZ%8[8L}L5L׏v.%cSbQg FW󳼗m1 IdǬ'imRN-!2|d&[ͪ'2T|4W ߜq-[5_Ce%~c)}e.DBf ׃8 2tl' h.%*oQ5@s_UcĝS'.drh6L-ˊrLUV^KAjElGENe,Nd:Aǁ4jRUd[Sӥa]BҪ]U'gï(xAF\U2 I mz~}/]舷.7( OurO KSP7w}5}Q8 R&,7d']0쐡vӁ\J+%{Y\> `&LslgөI&)1zKhHb"A˄(HKDt" RPp6(CK+вii )maߎ!S2U.v׆"l~:в/M#\mpz>y|Z}c7-_Zo^ZbRf[{7v[}sHw[q^@EAn;vs?GiɪyCѤk^I U5_ |({}zKUЬwܷo\CajSBNtSއ}p%>C60{ qBBK{\[v>E\9KKk'1x^qqck{]#a۷Wm]fMHnШj$%ճKǞcu7T()mԗ]~$י=E2= uYbs֓wQSsPiSY d<9y#cJ .@JQϫ)y iJHWq?^^74bF!imu-?M) 0犥CAAAFa뜟y&]z{$uR(I&:at,( W )i01SBP%0]S[b? R[DuLduP@exKm=}q!CBc[f"Fw>9cW9euvYL7ٷRR,zҹYf'< bA"36vh`bW=򋩬7Tip Z<4)CQeh ,FǺ]1&2T|(}哽A؊ >#NrTSM&yflm~mH)Q9bꆐ}Xyi~hG TBMS!q͐lhCјXǰka.ݵ +ǡ<0^2WUG~ؑb.@JrV׫)vm֪|spTO>y츑7|S f41lf-/3py]s[ZϠAy>5y^gHxG$^? bx<~tE&4ħ-Y) LM-~rdm|baKb ݰdj['G}#$:zʘt9@eO4WfꖊdaPB@~,h h0zOB$+j=j^?{fL /fz\;|My+ ~E;Hlke4|̍oLIoffkbFG ngQ4]k-y%B[ɕ'NY)l彩YPQ00=Ր8@M9̆(N0si1̭ 2߶m䂥[JDx8'7RN((}[Im~]ȭ$4"_l 04 6$r@O }pNƊ N}c.[<)Ң=z/ L&Sȵ <cY`0jxJ}351Cm;dxwXX|ي[];S>y52ʆWKrue-) vo}'vج3tܠw]'<숭Pֆ$f1TpQm5tm|P<,$b:&dhm;!6dG/W8@MtߦPR%R.ц#?=9]̞i>Q 8o P(|Co `0CW"jG>xI9i\8+cO+^WFw~ @ި2F1 %ڕoy!OWG qE pKsK^k>63$AZeH6[r2 eNv9kgt Jn 46N$ۛxCjR!84k*ca/0:j87!C߉RQ11wQQR;SR}΃ۦGҮlXի:vFkAP7ٽŭ1u3jxR1m+w9 idwhFjײ=;w,@03W +'W\\ 5vX|[jXoϣ+Tb^Ē ,%%yS?K7 >v|4SC8(.)!6jjbKeA9qJ0uerKKqҒ`uud֖h &upݻ5sO6SftՊ{Jwe@a5.c\@i N*͸nߺ KDpW/ԨY#Aڈ!DTprbjcMd%R$Y,> ""gy"ȡ=>ggubIEnjWahZ >&}DN]JXDYDX}t㾬#''iDThz +)yoAndBAG@K7E!S`vڧj3~x*Ϙ &KF^AN4]jzRҰ񽤅 F%%S%J ?;Gzg9E!5CW xϓzc, >{(+ĝ?XWlgK%2|[P -BUt`*PI!I~9 Hnll߄X@]d)I!8tK6D#B ":VTW.1C\b,=ɖ!{9 er"JK ]%AG{@A]dHD|=~y?^j>ԄR .`"FOxho| P`U=|;i3܎t|c,&%KV2 Nv9ޝ#ΊTON2@؟?8|5؏vE\bVȽs* ] _L^ nEazC-(Jzxt:_b 2zO$i;췞-VSy,Ⱦhםη侽o/n*8w//.HjϷdi6iPub̉D-ZN}촨+ί댜*ja΁ܛqlǍgC5Y{jS_"2dU%v)k <|>+1~_nB())k 䗣*p`]@"E:\v 2' F5X,nz' xiPl e%>lDVHh1)L||;V4liyZUe ^6DO'-3]e6Bhcg6+I^m߁dBʣ ]=OS^ n4(#;4YY$(4I@ ‰}dG<K.`}`Q};hkx_d/Ĺ#N ;GNSb胎V /F @ B9P?@ 3rd K? @ AA@ !(?xo<2`}]z0I~ցY4zFE4]uWž<~p~i7?:>XAϢ"_< 9iijH >cԞɁ\j ~.IdУgK8qN IDATu:n۹Ad5"A|!%{]{wLɷ jSX ]Ӈ<l(EhGczZ-Mgmf(_>"\kujF3P_yת_+&8(tiDύh;m62IU|хQw/":H\R`~I;7gΙ:Ocp\̜>pDNAqv Ms֦@vq<ˆ'b:Cf']RS<#RHwZ}E>31l |p۠DVj>jƓ^_wwl;|,ՙ`PpLj;N\ PdiBY0W,zjcwNިM=9+zSi=V]9zl26YjڱۻAZm˻zaWltb _wty"v}eblo;9FPBabU&!:\ ~[fpEf>^ցYlX}5#Cd6\.p E.=~ANtSއ}TR?H߁UW²MpEUҡZ=6>x!+Qf %A $:cc &vqck{]#a۷Pj$%ճo|jRxiJ`.?|̞"0ѱn_%tuyC3 Xknơ L}6>Bz Y/k7g盡N77+X~pzzs`R %\yi~0}D9rϐOUQ|SU Kw킧q( xḨ \۵5[u0hN-?ԧLa.bbdlzNh+fV*3W7Y|l;?EGO='Ԟ{5r\s",MiB*'NQHa ݰdj['G}qkr“G0PDJ,c*j'*M Xߤ=j +O @` NTPzHB]_">7IrXD]" |G5. $űrT } ኍ۴Wx (l>'Tnmґi4SB^^v1ߴIE}ˑ>"V7ħ.^9R> V^kt~b y gbчLb(|V?nDFL56C##ʩc} @^Ā{Z&drM h},H~;el^'9^*" +'rM BtbF1^$ 14M Ac(`C"mVD-luk V@y&g_]/jvEٱ,@(Meߔi&?Z" >^rKskۨ~u(fLRHODkpoе^å+E1!%C nK7$#?=9]̞teÍΫn Z:Vjs v u5J"\ w5F-'o>!ERJNxhm@b[Pd}:A'h8@l(K]˳6} 1{9j+qe΁onѱ <%XZ%j)~&_ڤPǺlXKQԯUudrEbHH /h9j2y}7$ Fvz2 h⚣|,bVߗ[ 4*wR6^fK65 ,%%yӀ?K7|׽U{-&9hP鋫ucd4;4#yk˞͎;Gsj"l0,h#O..)!rkj|d^њ0 4URI~]^A I +Mh$GZX#P !gl@%JkDD%W'-mL T.;B VYBmQ"Ҥ1d _18Ai+8LI;n\oA . ԍ*k)CV{|9x_J$*+C++,RėbS4eӂʓ̂p'uuomRMF=tjZyŰEZT@ND굿jTр% Pb+N8A d%#JVX=vҴ$ûXfv]#D>u㿳) kvGm1. l dL@܉uO:b. ""|!}NQ9\fGG>>~])=Kwe@a5.5?\ ->UD3 1x3I ")%#{Vnɜ8iK (Ϟ]?5j5:“ "MTFV"LW;o"#(ɉ#՟1KXT`B@ǀ& P# "g"Wi-WЮr^@S!$9!4:։{NK n &%a&>>l}@NVf!we ~L}ŴA]dܒRߦ,U2iLF>$UJ^d=ջG[e2T2=)I<)>m'cΑo,YN#K^g"[2#ƞ-z\(4B ܃PNJ2($LN'n߄X>,'ThX^V<%Zo<.!o.1`AdKh-R{Eg::;39CP]Pт!qDSzxt:sy1n'ҋ*kyjkVUnܣηs%!}*v[D/gw!4ج;bw3Yz9 6xGl wE^iO['w/Ar{Ci3܎t|81Jr?hSH^T rd&$`L:=>ࡕm wzܷٚ5dZ|  +߭`/KhoӛDc }~;:K8e|&%wڥXUZ"`Y1İ}Vͅīذ3VX Y1)sj( 7s$:]g(uZN`mU"ҤB`҈w4zvy;ɷ\ 6S|c놮Ġ#WS\Z;kj5">_gy1@E:5hU<'G?oڡ;W6q5HY)>*=ϸ9;[,Ȣ-wHahhY}q?S{e~s1ڞzR_C^ &:7 D1?,j|!j*3^y\x Zf${ <Kn_E^"N6VYAҾ+l e%_B ~.L3A-Ӫ*aBP$LpU`3k4ۂ_EAhcg6+I^w^rrQaa"+nRm7@ -c7V}n4ۂ_EvۏVsy]J!~P@ _iEc:Z ğƟ@ h)h@  ibہVYЍ@ 4AsV4 A N0>.=q%DZw~OtS05w? @E QYy'++o_:"2O~jYY**Nuu46cL>a^OI{F;gfdgFeW'Cƹ2Sz'JeI=Df靉V!|n\g8zK/6>ʖ^ߦpH+K]%ƍ ãxoyBDøQAP 7r mn=*ഊCv[[Tr Z;|QK˗N/cZ7q;F!3bhV.2@u/yvOQ)i>>):pnDLNfA͌тh@~1!OPI(dc;i/RB<`ߣ=h %Q @X7:'C4J Nd,K NC}Vm:DFF)=+=Q `4q+9zlY(QNR3'QK_won8vc`j{n~L ڮW+/+#+]x4U|JJK+w ?] ~zr_ ev8wf޽@ȁeg9aOeϱO nN s*9^]^-/)r½4v (:-E67!VkդNDO'\7oP{+&G܈vGZ,c>P:nPGAxf17Cu69wQcۇVirpٴO4qaMp>|p$HZomǑ>|&X?ƃk4Q)񇟝[;6{s_Qy.>{bd'kHÃ?cg܅FUb^$𮍁(4*JBa  tsQFoKR8 D%PXuT² BQS5l ـWa\~5A @Qޑi'$$|rl(40)EE"?C;*(*B盡N77+X~pzz#.Xc&O֘%XT4&<|HiG{u}rjsN;;O~{ٯ?K۷<RXF̽~UxڥEÆL=rH=e:lت̕Cc I(h+!m)ɩ9t4 ?XCZF9sҡ| OuO )3H}DTѱn_[on㘨)@PIH&¶"Au !"ia6гXyMm\YT -.&n Z' ?eꂿlγuH&oݮ{(%GM:JYNn=֙3=v{c [2i {޾(3R5lV>w¨q'gz?hL5djF02R*C^j* J3WNc>l{u67?{mX**b.[l_+bWb41&1 @+Hޗ;) 眙;wv@ShJVw .Х#/s> M(^ZO$"gzUj@29*'1gIB&@  hBBIGM!3jzwNn;ɩR;95ԃ{ESR(JJH:2>Si Ukr[P&rQ56:lDD{/5nj7sƞEb0Jmk9:twAZjZ 5:6{i_y\z©}ͧeog#z!>uik3~{%PgOo+zQZ?%ܵ:ws#AHI퇾gW'}ujbq |KЀH$b9@M p?j;#ܙ{G.%_Uh=qq̭,s*F9M?xc1`ɦGĶl//+Y2*u"#SsΑ$BM{/N ~l H91Ivn7 z6yeʠ9 A֩P8 /0@ C p6=%Z@N( B8"(_(EB)Lm9U@cG|TЋ9-5&h[yrj4{WBˉVn) _ͧv[]lD=o^>MP(У3*[ I@2t oӐD\!Irr!esTf+QqM6xH&70$Ԥ Mx tD<&̓ UO} 7@ f?SBfY|ʭV`h 02m tlj)`pPvob[u񃿧ͼ~9L $vKn*KC]D"B$=\6o|ynFsE(oHs{{1;3XhCN[h9dfmyPFffokiެeJls{nԷv\TXP$ EDжyn,N 0^ZajwD_ۛ 4pWhq$A/W /#{rn#,\K. CA!&r<nrT { GyZu;ti~zDPp_ >[9N@crsޞt$`29 }.%.5[i {M7Jʼ"#T!mt>L/J,~ʊ:77HEv O k7AdD+RD@N){p"{K%3mI$!V-?#q,!}|,j*24ʕH_?n?SqD!˖egyA,B8zX312N2utnǎmTzz/mr/լuau^Æ m[p!I-]fiC/<:-CXH '}E牓~J61%쬬|mm؈1s'w5"w52oM%&u5sIII/Bvb,8-ˮ/B^ f ĺS7km݈޶v-mZu[>e :f]ժLbԼt* E}C#@ |3\y.]h.eQ>Yh굜FhF줷<&sSCHӢϚJ{TxqVRimF*<ퟠ{9dy@|Gj#*5F;fG*@)5( & RW]ޘ뒄\o36HEi\psn 'wXxiRs{1deH>ϯ&U$9wIn!Q+G/$#:[ܴq:YQ]qM;?zvJoOضe{XkV||1ml'O ׫w1(gHZ1Wͮ;_ONo?a}'n>n+-!P #,_#чWmnd43JZ^xjѬj*C]p67w>X;£a|HO 6on$ - 8o.P+EmvϮK(J/cKWb[*.xxf@e*Eۑ;n 쾯d Ox("ѯWA[#BG)61`0 FBsC˿đ,/N̖SPɏv/~\JϦ?9F?0l`0*l<^ `0 Q~:[ F|TjG0IԌ&eqqR)`0$l`0 ``0 `0ŅU 9o ~m5@\]G/\x 3u'^;( :c]pN*4&6>xW_個mZJT9* 4 . V>ߠ6m:w;ΧX:*m l\*ɻ?Bi)zVXދ9V$>}T&L+:6[CVhz=CC{;i.1D9g6w GQC+{"0¹[6pTat8f>l8U]]u#hjBZ /PbkjMs.b QW ?o/j2##igIoon B  xs\k]7Pv:g$ļ}mLvu4\&Hτe(f憀7{_Ȳ=.Ghar:iy/| iͼyz ucƕo #wq-0KmHsӺJ5mL^݄).M~ظxkmősWXǜ}͝Cr'x}-AqGL6A7ޏj9n`Wiή.y8G||YRO/-|׏r6k? p&8rsa]#Vwr)0u}H?okE$Q2)ɳ_/ DD]Cv6d/I J# n} npo;$j&*ysِ&1 ԠfgFUGOO/+5%w(%5EO_^utZcifOl Y!x_we|%n5b^ w JHq 'Oつ7onv-LZ&WُJ9 ľa>jh?@5ʽͽܴ޺qvZMN? < T?o[9߈F?j$$! ՂNIIL ,tm¥ڳFkw귶Z^Gkݠ֮=$#j_EVYn/R %YrZg5DZl*pTZb2Gszˆ0l1͜amc0:= uL+:>M~[nXdd 9Zv扑 GyfAq`ק j`$zBxiHS56EmȲxՌ8CRҡGb6U hP [ =4>ٝ3I~>lZ]V&~A|!GzlwM̿z.8!+#m5KYW'g<eb*ͽbS(zv8wHO !o]|cn7hQrdd*!NQJȒn]jf&EQ\6ɬ@Lu2~;7ɕ=5f ##_"hףD*4y0;>n}UsxG Jj8L]=Q6?e/ HO}μHfe;ח.J3Ĵ'Eڵ-޴LJY2*u"#SsΑpGB I =q}w+C!(', T{(W U(\ɛygSB^  w*BN y H$rPM@IQ튳-)"dЖC]kO?vGhRc֮'W >7ˉHf[n DP`3/$ ZZZqZ7 3ƂC=yëbw1n5Ll{_ -Z$<^zP[ ol/q411pGAs[q$6CR^L^A˳)ޘ& Y1$>7EiR# 9M~[[7,|ߚyqE9r c&rUe'%Fu|vq~xdy\.;0e=o=J2%,YIG xp"{Ҵ* Q2@auTg$]@%垒dkCVe?!eȺئ`KQ~M56$ '!$,KF9PH%h4ywo36HuޘH{)`DyW~s)x=Q/T}y V_J4x_?rEb0B놖B^/aS#t_>js%+N~uUWJ>X%;"MʓMyH `0 \5 dFP~`00l`0 ``0 `0ŅUfegmI.q~?ʜ&6>xW@̜ם<{kזּzOSIY^uR \wcx#(7^*wy鮓lޕo^v1өz٤S':&仼G\1o|$P4g]! 8qq_+>2߮mZ|FUd{e{{`7wUGqjZig׎ᖪl6KJID7yi>Ev6̴sh袽pC֔+Arkm%o 5PiA\y1moESR4i :lj^hkGT6ie㜝`!/wWhAycEv;dҎ_'n< ur-W=P哓dJQ~\!-(![1BFV%1O_âbOfS5^PO 9hѝ:TSOluOُmی[Pz=B}nF]҈Jk^{{{11T#*P\R*,đM&d\~ю'B3ӯM.vK^N6nf l\]>VRr# N5ǑStw>M4%GG(YB9NScFI(xl- %zj+[Hxgܦ,m|/ԖتXrU -&-1?&G~b 2(MMBZ!/@zE_j F$p/4-_!uPxp~ALXZm5Q|-h4|j%823qdJWk=yWw/Ÿ66"ofiȳqEҏ E!@B}{&,CDsCcd;&lRv; TPgbaUP2$TP*RReRӲSe%\)6.޿x'QWp멮s PYJO|u3Vh};abM.WPi.)t1zrsn\h٫R?,F…JsIPOI$קr!^S9\h<x5|.eg IF`<ϣ(&Px&>v0HNV`٭\p!+@y 'wE mSՍ`kYÇ,e5k@͘Ccl:{.hikv̾Bl~[MY^:yX5m 8 {i=~/l,4Mԏx.{(=Ae[CBFU47mP¶!];e8S9i4m%cۉ@иzԪ'-=M;yR _$|ufNkyE|vBɩHܳhBBIGM!3jD#3J>րXKfI`*q"mCCm 7V(G#QB-[W/ 4^Z2+2O/u}p8Mދ6?HVK' ˆydŨ7͜zS9ή{y oD/\>Ѱmy1 苃Qa(f= 噈! ʬ+!@^SMr ]č:Z$IEE /:$! xKMH"oW_ zvU0 d>*x|aZŰȳ^ܡ̤@^)l~[X^6Ao8g-X~0P8KƁFBYJ֛)MS.A8&GF%NQ#3:d VpV+S)(^ax-.[HSR6Aw?' !H(Auj_:N&@uZSa ׋PTha؎zIAPi>?]g0*J?֡|laA͆H$2PW9zFtP(TȕOJ~seQ޳D箇:|orbZ˓"ZoZApzm-~%c 2TI~*SҴw˝P}ζ(%#dǀ{‰â[LW!-̋DǡR\z2ʕ9l<_ I4M|Hp'|vlhJJ+86U_* 0}Q*ǩVH$I.eIɘ·)^6q~&iá)\319*CCVbR>*EENvu~9HyJH^nb`Ms{vr y24#wӜ ^!:4Pˌtdr Cf-?#qr-)$$X(+UC@][=ĸ[o=LE307e'M@XHQ6@Ro7 7nJ*Ө6Z Yyh<1[eOVFj"qN>HnۺH_?n?SqD!˖eg[)H&Q0! *eCK-Z:z! M;v1peW!/ ,MڷU> mZڵ6(}VˁB RaΘ6OuU/Ma3DPk}&1ɓm\(GZVXDžw%o"M԰^?˩WŷY" DȨSgOߕOQZ{+%<$}zbm;4jK['.1gl\YlxZ]j¦|t:Ҩ]^M WuړP$74 'IFꐤ0BKjb ZI*4sI+ xJbКDԘ#oei:6Ch4~{Z߉EPoLuIBY<FU"w/u`ɾpSj _ٲT/1Y_ @o:~r}s o71itQӈe$Ɍytqo\}òͧF #]3x_?7D8VMMZA”3!jVP J^> O= k?{Ev_H t]w, *~ p:`J/ShvZ\xЉJdبHݒ }snBۨ\R*,/aowRa3H⼹Ts9:O {H<j gx=JN޶Gg\{zwC>}PQfyȉIx=Jo1y/c;l*E)Jko${_xcO_qt;ΈL?_Z~-t#éJʿ%?UMӨyH `0 0^C`0  `0 `0 (.`0N{/.o/xkpܱZKU#n-5ןwFI?[Yަouj繦{/ 」*6BquG0G*uc}aԤMfNKmʷf1~lPV|M;mznӦVN_.SBjP-|e @v>6mLSλso΍Z?t𼱢c:dŋ6'sOX;?4CK.栩 &OoQ^T~ ԝ~nrܭ26nѦUG?}\o ,g7g1dVWaڟJq~s9z?TKLs9+4$G@1~<._rK()R¬~Bo X`0 $>s:h4ox)2Y#<LX";!(ofn[~#we,+>rtV*=ş%Ii3M )m0ﰁwOZjJGCx|4h@m>w\wȷ]۴.u)4wm h4Y<T B7O#PiS)6.7oܹnl+ :,:spMZ6/ ڔsX O8|ai;mj:ub%Ƿԃ8Zvʥ@cgRHvko۠zvn/CGXD_Gv6d$>RJWL%zlYm7n:T)!KDf"2!lFX@j79$PpdYAЭOÿp%%Zyv̼`H}}dzzzr7#9|-r%;D׸Uꓧ@қ7Y ZQ uUJ#KH556} wew͋{s<+z= {v4ƭ7՚Pwr_ͼD6dvґ'%{ 50kf+88 {i=~/VYn/j_#+)'<)Үmd\8>̒AgWw-aoН4/A>Yj=w<+%ʷy"yt}뗇-B|laAPKhS "B(o_()pd JD"Qr\$}8 8sZjLշ ^bFhisl=Y9y$ N$?x,2U kv~$2LjDsyr2a%(n z=hn. J[B QILJB-;9jsKSBn%̶ǝw DVIMZ߆<]I$cyW) WXor]$ou8?<2_T q\^n?>b t,Z[~nۓg&f)lJ/yfSrv1Rʂ/1 ٽۏl FWMnf f ĺS7km݈j̛͆}S}w]z&A4Ӄf":RcmHa3DPk}ӂDD?Džw%o"M԰^?YM,/ѧ(;ml2f267q"vN3=}ҦjK['.1=w\xHHu. jvh& Q$Fb@ #RsoowjݥeBPXN>(T7:M3)l)Gꐤޘ뒄xJ¢DAu tmfi/Fr^S9Q(nRw%`TI\pӨpRON,ZW@5^I.aS#t_>js%+N~uUWT<$izooDw߮3īZ)lZ^*ۜu Dkzlja//;g8 S2&=eOOb-fyȉIx=6.޿i'[zyg2V sh &1:3=gO_KKXҕ)▪qWnGQGq[0gO1F|Sn:hO.-^ZGĴݿ[Lθsu'Sno9DkL1v R s"hvTG"wMwBu E4*+t>`0jff[V\Sڡ5|~'rZ]X}ȗkO8^5ۻ ۳¦ {L9 ckuw+QgT&Uֈ! Ft'v<(ɫ}_ˡѹe:^9lʂ/{zl9h`l`0`||b `0 Fqa `0rK~* 9ʠ `0 Fqa`0 Q\ʦPSpM 5K5k؀4>76EFD]o+ϝ/=8yTm@v3\G9Πd6ad󟞁_xgf;}l6ᷳ׃_~_.tEا6KYIJ{}]H n֮}_Z-6oDE[eW,>s;Pҽ(46 9.v{{9V繻z\ܳzHjMg;a^8w 4ZX&noݨlPW.l/X] ~?96j繦NYټ+n#L UѴ~>-KdzYoE)bm0ﰁwZ7iAˌԠv[#*hnCi j&y||6SfL [7iM|+ThҺxu|ܬh-Ke tqʅt |VeTs1E2 l1m$ww\{85Lo.ݜ3:Wݿjh ̴W]Mnwϕ=jwͥ.T`Mgj؍\3]Il_3N[pt956vM-!Kt_|8|lۘu1zN9%at']ƃO5:ϡs97j<*M5m_lk{GۀS%,Xyl3!/lZ]Y;~o{a /N2gܰ>-_'P{5ǑSt&tyӡ%mZ5|L׮ jnD+T_Jδ?r~?BZ _Ɠ F$p1w",PMe[ʢwyvn=ȝAǦ٪nouKCo4׊cw,-iӷrc,j|Pߚ5wk~['נ䢄ٙ)2ge&%q 92E&K}DۙgUhӫncR<-U c#ŕﲑo1嵄$ i| |T3!xbEUmQ[͠׏X7m^JLu5Irf5lsQ?>= ͒Ce8% 4pKWko* Դ\t`pɝƶ{Xn+9nPnUjW.s?T8kOiQPx 2Fෲ=ǣ~」ru1و9l^rw7^ j咲lw>+}@nv3q%?DGE։IudgCN!ٯF4‘eM@>MZ$+wH4 -,Kŧ,_r7j&zmV7}f^9HQn"H:.uSQe5zoώ-(6kY >+ُ+8abb"`ܤA 4T#tZy "c6>))6 A=?:H#>;m0V[?KPI,f/g1߯?{gUs]C(kk`ݭ?w-D%Dp Kx7 *~sy瞹]Fm״k[ %\gܝ~f#G5ľ͈氵-i۲Ӕ6VQ{IWMK6GU]aA'IYx..`䦝F84v1 OzQ|Q EҔ(wܶn٦\Z٩ʡ9gumi}X54@W gIN?]>Ϧ9( F*:3.;]5:<ݨ4Aھ=w9iC,pJ~~SRLMMQ `5M%.VW65\ʥm Eq7)~fڢNxO%a7;nYֈM'2K&XEfF>M )Dl\ -q)r"^[-(Eoo:h$Dx'{'?.嗇Wu|޽&xtl (kG'5BH~%e`x=ȱ@xU#CBe[Abrr!*&Nk6ߣ+od_GާxOJ~Z<^;{!s}K*WƮҭ؃BMZ@ƹ N7D'J[JA'tYKtYbλYw@祪A=63FqW_=|nAeBi1ʨeK5>;èT-'a}(%-zaER  Y{J3Fsd77ܲ?l]XڷՀKBvKy'yC3O_ה .l#!d]>8Ym;}j"*6J.$%/}>Rdڝ+Kɷ+lI߸Beߙ,Ǣ&P;5;gAM:mڅTLMi޿@M] x\R1˓@337Ca erd\QE0X`ދ6l 7'_;)m)6`Ԛ'--T$ GߺX"x4ѫ Ļ -)z64/ J2TcCsũ>ͭ?mԞ$$M=N?RkvU)TUGέ F-Z-ڵ24KHHO@}^¸g&|(HI)B!@jJF6Ya7ŅSe̛YgQDJ"]5%(r 4Akp;`s1zt^O>~) o#|79Sиbd3i/~`mK VXUH ZYR lj|cGnJ 'qOD$"$ /\.@CW TUT)WgO:ZZRޘy>j1v9/yq3.o^yO/&$#@N cx{R+ 5[rIPmVoyxNş.p P)%_ɊgG[1@@z{ER0e*$AÑeΊnW~JmԵTH`YbKtY<̔PY ᘏ?'RD"12y?~t0]Mq%bѽ[9l6ˤP2Suv/][z,"\}^E"T@F`ld R!Zkְłe gpT*$y<@^~>9J4Y,]=ղ== @=:"SndW.ΰ̈́=3u jS6IWUi=37L~/PRCCc$Үhtt1c٘OvS-2}Q&Wo: c:]3 |߽mӳ Ԛwܛi*):p5yI#׆fݕ&Pq@\`! 4,i| ٥H2TVVU&k;Q_kj%RCK'mb5>m޽bĊyڛpY$ No^~:w%k4mu,E+y-S~cjRck>:v&%}׮9g5lmj0jtьkwĮIrI7pAdD:l|~~(] \ *U$zzش'@ 6"PfHvu~K5.K f7kMV&94;ƭg 0MQe_Iه VTx(삷uAeN4eE3/6Nۥf̠EKӞ\+F&z,fG'RnXٔމyc䝜#=vff"iiހ:9nܸVYUPg’J4ꛬO.4`羸~љO]#YraޅܐS޶gkҰS^Z:-U?Cnl{ﶾUQIەV}Y{2Ņ+v.$\T8pBJȺV.oT'U~?%wx0/I<ǜ&_jiMylU9:"i=4]ܴíD/^ N9Z[89jEA QЌvZukpY+f{/Pten5G_]ժҼS *F+7^CGƲMJRxwj뵋NJÏ/<GVL, #[(,‘t )⒋l؉^ ʗۧR1g-,޽$?| kxfk3 (|P $?^!/~T^+?c~wjѩovUfh)`4T.zi͹A|C#w]o {֯;ea%XZ7@6V,,yc kPKv!2z=;Pu{'-{o?->1q8S?v'ɹ ;U[xppMtJ5Y)ntAXMv٧_u;uL!o%-yg=.݀ ݋lN$kJ&%KltNv_lrms;HҡKK,]}mA|3h~Jk:V dn&I1ăsBRiNi:pͲ;q_VP{ݿn R(-xLaT٩ UUU ?TOW@[O8`30Af޾8^ @rOr?Rm9v[Oj&$ B87&)if yoB}Lso̗MSZN*2E߸@$ʍ J353VxXn& I:o%/+E-=7vNWrv#"SQsж@|ـRD66o -me3٦ 0nseCeT& Y|$3my1Pu~Fû~7LfR6q]{lkH;Ȩ+z8}?4>o^TiwLxnDBCBv ңIi'a}>߾y~;u ˢcVʁ{OxX ͔WwU;k W=DI('T$$Jk@IYѵ't7"2:d@!<?~]=9նO:vJK@ao :( z=QjY6 JA@ ؐU2)Q(KPF.1? J,msy"QYR]=e5=,,މc^Y˪K;ig@AQ;n%MHH###JP>$ΠcA7_H L͡~N7zzh[n`AgÏE>1e޺( q?L0a1f^g{<U8u^׌V|гJlD/?/J2H `r)O"05[h0G'OFR$We#bD"'HCG*C &4&fY $Ţ4u! IDATL*ɳ>{#V^vѯw]ƸHg^N|?w F\e`Fgƹ N7D'Jjb5 Uecth{ղ rS\5ѕ5Hݰ'f(kgFuTQ[N&PJZ*ZOi鲖lP>ZQ%XW VCLJP"|@6.Oʺf6.=[p^|=sc]qV=gǥQ*V#/<ZڷՂ<h mu>SA\w74 eͶfnٿH.ڠ W6vNC0773C}G!~+tO_3`H /m6lQ#=foW:IqN_uʟZ>^?<;j a% (P7G P>aX KbujH,ә "1<V/_| @ybLF$зy"ApuuQsQ}/^H=~^#ƾSSBa33~jy3/HIV`@vtQ͛Yg|[*L Z8wx︑S9Bm%JMhr Cə٭ țG,HI)B!@jժbk}^¸g&|㇑B@6G@cjH|:YnUU@\)AkҸh#/jcALJH< ]6(3ukݹ OS j P~䦸U?@"qb1%bxosk˞JO:D$"˕ J'mFΟTG DgW% >uA9W8\W\&/嵛6wђJM8qAzlS[/ݛ5Ȧ|ZEG6vIϥe{Ŵ&C*Ht Ll2iHD_F/QY&ؕKcAob7< k>w$1&׭'P'9"TfW/j ;9J,L{ƣp*LH$'<=s%c씤}ί?k_TRD !w73WgelU`#I:1,@%(v13b @ ~2  4,'D:(I_Xl֮X-J^܌L 9i)yʥ[kAgMp$8T@$q8P%!Fuɸ%jJ'Ѷt6Y&< _P]/Uc}gaqaBhTN-! JcGC!cd#[>+eRY^5,`o3̿ZQHI=#> JIC=HDJ񇿈Lx$ 9fHFS){DG41^Ir\{&J Ib:QQAuS.GamGO_UEWf Yԡ $I♙cf>pŪ!+i|q#J*oKյ ]RB{W)P :yqC*PBO %ybvb!- jX'\Q& e0ǒcxr /ZCKPfp˰B%-9-?7.5N$X6iʪqO -!T4hRi֬;r5u5}@k-JMO˸Qӎ}iwGwCJ~(JB٬k&I4/7uKwð ybUnzJvP/|(+SfyA@ IiE5@k :bTH_r p1¦ Ln[p@hHy9c !$o8ܜ_ݮtVzjIV}N*;bĊyڛpY$}Uu5#sҮ:Yķou6𬆭-o :iU2ң,XϮQۦ>I;bTeJP)?XSӕ2b>#Pش'@ 6"Pf*&bn႒ 6E^%[/m-Hvm\0ʶHRk}}X>mN~j?Q+;j5U/\w]i՘ߗ'3_\Xb#`Hk RC!׎kv]œ7}ದ_k"( q銓zwoN!E @bWH^4ZAIP J0@%9>E@(Ϫ'B*#N}(">ήEQIyi z՚[YBj'JΡ4ѿ3]w}~ckײ9mˣ_Zs.xhȉ{Ş<F7A\BgF]r9|7p$;5τ{&J}п30R.MztnLJ~۶ۮ7C%Lvz߶-_В=t]@&KM90:ZtA;twSa}ӏO sPπ"k|Pò]RA#eGvѸ."Rc2%jRq' RObݎL&ҐK?$CpXC_{qeRŇYQZ&*(wL/Gۛ 7/5%@ (k%%]* Qwr/ 𸴆>h(vVk]g^/ı>-[o~*ֈܷ?n拗T??fOaVic?ҝb  dc+㾷Q ??LaeF+QAC`Cc``````````h?t, z@6@uz]3(LAFۈ7Ӟ@HՌwCv+EYpVÐ^ګy?-rvIЉqfrϕ˧VMGOd谈{|IwBgˆ.r}BJOg`Wcvm*uUm{ A7l߼c#+mED/g#z_YL f-kmߏO-ʦ:KǗHq5b /mUm=jw/M_88~@i7s7:?g@ ;z}*n@Iudn@El\f'= 1VBl.?O*.0K uGQ*Rc:pͲ;q_fTr=2^g.CGIP6ͮKtyfUif *_Obkwaܽ[Uθ?^E t>~IMy*}B9(ZoS\5jͬZJQRzhzn1yIoͿum=NY$^EW<~ğc"Vc~*YݸYK5,GaWa2b碇4mrw4x0f (f%s\\`6+K? Q*``xhX/ E@r_-8C]7d[eWuV{gʟ{c}K "LhoZ2jEceQWnp1G40U47%bЇwNoDe1IhӮaTK>rCM;?4 'u>7ldNM fkVwP2KZ<~}Hc̿@ s3233233BMdZ|iim.2n4/_f0nseÄ4q%dͤ]g/;t^4xP.2I3 RD c)ex%Ach2-BvY賱!NQ}*2d*\jD]t /ηG`5BLό 4_nҹd"L]p'MB !(scŸf5r2 V3le^߯FLqKAw#"|/:H:Vlw]pT۟qQAIb() uQA{bHRkPдlx vJK@ao\{ԋށΟ[D'Yx~<וH?3mKAAA]fl'{!7\3šfL{y/<~,mǶM^)Ե?泲 ɳ>{#VrWpN3cxjtyi7#GY/$k4:n;NݺCk!ZA:@*/iiiIn>ν[GV5u3wD.z 2-$"!OQ;mژ2Pl"JPl"`6_>{c2SHW05aKk`<kwL a z,(hQ&7L>+fZvѯw]ƸYtЕ#ˁCTNMtsvlGWU WуG6 ;j0{{yz1"jnP`uanwgmlLJw'ql[|U~Tyzbλ02gx}{y=jY9X^} Ҩެ`ctI@RP;m ]֒-d P%XW VjmMQFl9 CI(iZ&0:Cp{ PyX4{&=ak'r_c 1vH[b,.DSO];Yl!ai`wh5o߭7OApuuQs}Ꙛǿϫ6:fV!t(NE|N ~d%,ylRnML1Q)m)6`Ԛ'--PALv }r 4g &8AMRt<\ VP7hpzP$c@ P.Ÿ&%$_,{*7VIY:(ҫRؤdU W~h {'f'Gyhߞ3C=ufg´JMu噤>dw'gxT*C<^vbQ))E!ZZU,oHHP x@ p~ح Mi#.֔5ăHsi! /_3K VXUH t5?00̫oqϪzzQBJӷ@\[/Q?[8Q큡:weQU{&T$ܸP9g o߾y~|W ??Nݴ}Rq]*2R)-X$. vqAPGG4?^€ȫg_ߊoʺ"ˡӇ{!ir5@STl Z6S$KO `W t~ T>)! G`8 뗾,T[`N|uLtPR TQ(tV-\r?ɱsd)W٤q8@DasTxyCn}v#PC*%n`82;Z3WZŖQ4? *)HbDiU>VT{bV@x X@k-Y{>ɻyJҳSǣx5G1!6R;@J<cldc^f>-li¢X6 H;LuR^wyݔu;* ?O]\ǎ&cDƄZd ,ʾR!0fŒ/ORSSΫ R^~>lJ?5Кo_o_ bY(ͱ%|IcE^D~ \gUq)ĺ?*e$1%YDjR`){/JWI3~hNǎ-8G|z IOE"TbI$t M{F0!4*S't5>16X%x݇RJ)t/5uzƭ&4%I{P1񐖄Ҟa(!$ӂ2t|]$k``SώN[yY`z޺bע>- LjGjMvo;<7G=%o;WϫY.ayU7P6&'-lk#+ jogfdjS>\}8 ; :D ]\Cp+yJZ s+}.ңnjj^JC%Ql A4 FƼ,7le5D-B;?@v7yiX6GF*JݶR &Q.n`ai$^qp*μ>?&\^#Jr^'5.;M-W* |8ϥusNmBe<K[1F6aۛTg<7>H^\k썭֜Z@:@*/_hƋH3X)[vM߾4KpA8fģ(|ң…5iNL&Ev d.J>r?dB1Ht4L(fֵ=mvTh[) IDAT&r)Ӑڊn}vhxI1:zhޝ{fW[pЄzPwDS~XlbQVL,e:uB]Jj-nwy=g&]=2]F10ȿ2ߕZk)}wsD]chPbCQ{_Z ㎫5@l~'T漎7ᵥ>R0m Pfľ^Ra;Ph/PZFR,xU5PT503ZmLRrY&10000004h@HjCQ`?00000000000( ```N e#CmD5jw<{oLx?QBg8 iqnv2<{T[~zp]Kguӧ},]XJ= k$tPM4*=8Oz?]=׫P Wq rpjRE',8{+aHPii.װ{wn^1#n74$پkz؉\7^)-,yc k%Ǟ2z=;PuΖsnUVL\ ;U[xppMtJ5Yvcf1鲃t^U܇L!o%-浨~n]*`592 p ݋lN]7(C!/1 _T':|+P~*-5Otew⾌\$T1ăsBRiN}S+jס-tjZ(kGN9su֨7q e\湫VOC虶o\:u9vwr7S8Fl6ATl sWyVIx9P0`X߀u,;PaܿQz|wλ3K/Tr;S3`D3iHWqo& 쬊pLajO9Q6 C^8X-p 8o]\A:ѤEs(x0fWboBVEsH}%fڭ벿:+,>a;w/eCzN|n0ȼj\z%Y Re1I*8M)}B>v=Q(Ƅ?%ue^~tS串g`xdԕ[=?-@=V4!𼊭7B;aR7UK7MY}QJvZ)u2 )K "LDMco/<־5ƒ\vր4 tLy+}1h\)&n칙s  @I祸ll7@Z`%fM@`;{݀x}dT& Y|$3my1ɴ Y;eXƾ:)QQ4;&Ibe#bD"'HCG*C &4&fƦ5I?M]so>3HiڵWgF~uj^#5*GUtf`{C~©T9:کsm!=(t4NJxzF#<~ZX7vֆ(hQa*0*`IDJ@I.9sIWJ!*&Nk6ߣ+ekaO͌Qp.2 1@iG(1ofvYn0-nRSkzµ%=4nՂglQjz{טaF5c~5}>dw'gxYvثCÇB)7Ɏګy 8꺺(]عu?뛘rKVP0:fdZVAM-?z͉N7>wvy^ :PTMA!3%9&$JG5QތMC3ArI!? 2Y C3`QI'kLR0@}Kn'BiKo ɂKKtAYjͬ("%Y[te*UF=Fg6v P,)`[aUU %@-))E!ZZU,oҫR؄ }0R(F<(9/]G6׏>E11_EP!5K5O]10@wgFRpNq8iD"e%c8 &1]Qd _ZU}qAzlUM[/ݛ5H(< Gʝ~h{Αy!rUyTZ䪞OY|BoIbLQ/[ |@qCMnhI @&?D[wW=%)oO3MS7UB$jYֺ_Z6.aZ""JK.)_tL53yhJ4դ|?̙999Bȕ֣X5PR/xj]0%;ot;x+.$ I̴٧pmNq0V꬧N6m0m`)qyW ϝ(}_`NCqBz b~ 1>,@UjF,Y/zcEynmu k0>IṂR`FS/>4#/5 dIcԳ%4x KbXrrr|A^Ggt`QZR"nqrOx)@h1Ud|m?!Fֵhwce-.[zJXWX,EHCu?1W*37NOFX(I4p@-'6rǔFUBZ[djZOr B'6~JA ОMwϤ!Zjތ{&I qqmN M:RFRmZg MkXd@vj\H2Vy:F'SjA?.1sQa <* 8 UuW&. [ c=b(L~1Fu R!;<4Y-*Eе‚MSUBgG%Ye&=9YvAd;^TGbkٍ{Hd߷nc.Ñfl,HzJj+9F=y@h2n*PFÇHϐDxB/4IYXM;~V(<2|`cM6$/hFШ8'ɰZqD+k+k+퉼SbLOFJ^& 5FCQfc^&A3׬fG~X{OZ+U Ji(IWP@P ^)h4Ly>*Ldž /l}lhE)"ZsРQՅ44JIRzooضσny뒓u^\-ӱ?7v9L۴fXZ1iڟ}nN7ŮaTY?ORCcب 6CbQÁ.>ĽV -myzkD`L:FP2vR9E׽Eds=m9YMI2@ ȘCA;߸0ߋkċFTtW^Z)mʊ_=.ծxѹVl@udHrG.˳pθ/瑛uZksrܺB:~uܡm%Boמ}t90q^p"+w8v]_:ޙUť<*Lz&ݴJ^~!#G{{(d;S0x1'#\XyF%aY}w [g(dMJmJwF>I_Nr$!?_o,ioSѼN9:!;Si3搆5m=:aZvByE_nN-,v6F~b/\}>ﷷ]\fcGZCmTi!((h ӢeǔW{Ax{h ġ);ִ0 F9)ngt;yt+X}ᅛAAWw4 "|KBGymCP}zewiF^_8{s 28OTg[Yd"'sng{[A NT>Qs;D Bk`ƀVzo_{[ҧ응f[2@ tjZfC7@ @ @ QjWlBTlB/! oz<Ͳ)~Rߙe0fka!'}ZjnE߼z~a\$^8}-VÛ&p`qu M Gݶe+ѝm4Ҁ;^ :%tmx?{mvӣ.`Q1:&އgxoPbl}=/v}0u{`K7q'mMd4 +\U۟bӑ<+vx{=]&:=&gT9K vaSK/>޿lLv^;~V(ɨSg5fW^/ы@vywy4lz~{+tsXt]]qԤguys wqˆw%O68 6O MhO rJڤ/4X鉁LA >:4/ܦz.DE&2.XgʫnmФ@ ɻYUqZR %ci ym;L_ꯅs_ܱ@ķڨYwӢ+@Ljە797QtA`2&ZVoX/fN]~c.'svZje8@lfĔGӳ `-uNxwvރb/c*>@i`+ h dF~):6;/꣢j^vDhYo-1t?}Wsﵖ89g6ظd[pEDh|ڭu,״g1= :ڸ8#S'}nE?gi[rTz@@U@*w݋gO@IbwQu Pq(G*]!?jP XE@a TCSVU7#y W0J0N yKl=:8j023ˋL̯)~|k]@ 829NAe%ͮ* Ǵqhr[cyg[t8`c>zjr(u :T}ҼG]b3'sz"(H0A9 JIEyQ۩sHim#Hg +m; jX d~y7 ұ+>-f F-'}|Ɔ_<&&JS/eT5נNȗ{" Dg+.o6bԘ X$jݕ}>tsE/ނ?|#,)p׷"8]z'c~2+FRL==Fe;b!PbS֔ 6s ;aT""y)4E;K1$et\_Ȇ=q`c8"?WǩK'zX|{G'eIHiFIuE+^!' P+YyESaQi5,v,)zzֱ M1KTbXiLQ t?'M /3ߔbGg~]9< E9hs{\nqq8I&JJJ8*l>ptur5eɽˡ^vlKN׭U1 ~>旽/r#$O:@ +P ;Z8ՄL|=6U7..TYA9_l"999@ L IDATRfv}MY(L~1FzpzFsp#;I0)HtTv%hbv՞u"ZasTP!LSPQ) "|RAYbV |''ڎ1WWе9H/#?;Gu!b)*)R?qOHE^BI6X;KK(y\4p}E)7{\Ec?'Dub,5-gO 4Lh=a} :l@ϢVғׅ&-jBh4|Hό 1'@yh6h}% `A4hT..@!F */0EGz0ғujqJ&.zS uƦ:[n ޳7L\{=CG% E.m ?»:c -myzg Id-ǣ̰Ao`c$S=m9n5F>X *9Ae* ڱƭ\ԅ^\%nw˗%Ŭ+ٯ Y&N;+:p1zqIMPYvvųN79+5rckůb|g@Y7lz3B$#{]]V+@= NdҀ襘_Qxq!,5E`s7~i* _v:Ed3. *(Be9Φt8 %Ns'߂0AKAuۮ t_Qrм"@ G/p{Imq@I: V>5i@  -bFK;Ug8[iRƎCҤVzi5S{gCeGD A@ @ @ NRXr<4b. }:yVL+L%/^[-ko\>eF.t* #A דXt ⧎֔CK1N?5NU:M\Yf»uWӎhWӃz_xaM0>t?s:D \D{#ܰ1coԐS`8qӺol>mȝ䶔{ISY=g8İνaTwbr7k)-}I4dk\]e+ X #6a$tP Ə3?^ Q(;|<^ě ;a* Vw<{\EK/* ' *Ǯ3kge/[P%?BEdEݛذUN𬴦()2>Ш8A|+ӂO^IWFZ$ţGJ[}3pzhqqeȝZӬ'I öF&p׷6H^_u,Cz Ueri3zth%f:%F%&((CU݌ ZNyN2& F*8P1"VR} a11xORf~#<"vLz,N+}O~9{Z\ɮSĺXǠ̀[@'\Z?f =KHjv$/7W[GGΌ[%V z˟>+')# 0u5R(ja22Sj2 X`` Q{d%b4^>F/gur_{ֆGX[-+xgiq L>4f/N.Zy56Nup;JOp(#f" *@~A?.1sQa 0]7qm<7i4!)1۝Uj^ƵJPQ⺿A 7MIOCJ U5P8QJ=蓣|VQ|ƶuvxt;ƚl=@3iՁvVz&\''ڎ1WWе9H_+jl#OF={l>hl0u\}JrlD(Ӗ3񠩘Քdk 9prv} ?<HqOzn6kC$7ńn{ZcnGh1:7:~uܡmSI0doP/*[ LDЖ`= eB6E(*Na>J;;՚IQʓbnœz'E_.j rKVX~̪m,CL]q^pB\BZw?],a/f .//Tӡ^]xi ܷ'1("X&]@Xש1Nuae=u,Ԡs`>vPa䞡2ԠS{6[1^y{$kM64l HAAǏN+^U]JiΈ:c4vjj+&uujZiɗ/,GciXsʮAm zݢByE_:NK jh"[Br9 v׿v}Th~>nQrм"@ |ruf ;m Kj  'G'Q B%#p%;ޣXLNPAO@ i;B9Z齕&MRq I]ziwFZBz{gD1R@ 'U$ERj-d@ @ H ?)멵m,!Cm)cofksul`К0߹t⾞>^&SI YOvlq&އgxoPvIKP=7l,K/)5%AZ-{T҄T)sj`o>S[Ihiiw 9s}~qme7Ͽy4lz~{+,Sq7 2sݷo t!@k\]e+ X #6ߡ}_}ī^ӽvd?#N2jnCvyx]k\1#k6^y o-0i7O6pd6;r'-aTVY;<1skn5&,%>,qδlv>shPK/*%Sp]iaX7eT2Kn O2 T~0eb#9o{1C`~Y g9:^Mȯ(O >yvp0/cC=gjzq{ b + %F-1ә} .1:.Q4`IدW:R]F&p׶Р GtrGa#"*⯣xB~QbCXKO ^7Sj ШHEWw)q3Iډ.xE\w=r'4"Mg) Vxћ_M,>R)6'ڣvƏ2t\u \FxDpX)V rМJ@} a11xORnXdxO8ߧ˘bԮa)㪿n rp8Qe(goܹN4'9՜ PJ{R"PSBhU/սA @db]KHAUPyec#v&Mzf@,zX .Mq+"ʥV5Wmݮ_j z͛p&Ee:竧/D3F@o00$je3y'[[X[<ݣ Gw7kyCfN僯>Н>>ÄQӖWU;lqOo68Q"֟~pĝ6QSw0u gcC]CCLI׷}*z<mcmد=A/gur_{֦'8wvZ}aInR̡(O r;7l##RSJ]>ўk8jPQX?&SQk05TBE"FB_X-ݾNsfGF@y56Nup; fyd6wce׭9䟦9YS2}_͖=&Nr;gšF+\$FTމ=32L9*)*֚?6qn=.,.י<~RD5ĸC*@i)zp`-iiJiv(F$7\ST}o!JZ{,]u[-H@rd7PܓsN>oLeM~^uWG uz4Q 0yM[9A!r?g/J"ßXXlDD&3' ?zF_C c(J^Fٲ#Xisچ)OLw/"C%>FCZy^m\ I:}{N&gj3;Mfd=ld$JW/TU=͓H{jjNWCvSAl##6ά91?d\Fe Lb>VTP PÇGP*|!rW7EPU?ľʻe2Lˈuo^>*eG&(ޏ*+cTVlT!QS]xVJUt H)8f0@viS "jZB{}>*RSD-K3E`k@;(/*z;p)M VyTҝ؝s?J$\H 0 TLĽ}l.hLvP}rv]14'[%쵧zbUp2U_]qN?+BIk',zk*2X:׵8g9_B^t!Mї6XyُwjSJun.Đ#>>^4&nHegtsZi7L(/U)M=8 ILq8kopyeb{N F(ɉEELZGq痖455>zjl##([ux]ƥ]X7* LWhba|Rk{\(`ֱ%F %D_6ݜl6Q ĦǩK?V&n_ްԆ,( L~5wx!bf`Dŋ5\ĸ:|UUTPԜ U@d`=1U G!ʱyU( Z'*~$YP`F{>$|YЃX@ˈK6 dE y<(2jݫۨm\=㉧U<xY # o 21uMcj8,N-}urCFe\Ӡ^tqˢV5~4\ZRp-h1Ud|m?^W9rLʻc3j]m9A&[\\,&#H=nޟwTcKrrr|u[GIqg)++7,-FzGr8pKEϡ+mg~xJG3@OY h! Lu{Լ%ȧDɄ@ `Dk5X,V}#QRJ@oryn?l@nddaFnuc* IuԠ q o%jAƸ3М hbvZVvxT -1=S!;<4[:*@GdEе‚MS##sG}V1wd=y?iٓpBxorFr {P դYl;=H|ڤa} jߵ"…\ zyP~v@;+=umC. Ғ?ܘ1oٶσnL{,4X//$fg+u[| !r/ IDAT=3>k:Uc4( f7583jߋ*&$/nz¶僻HS7dSl᎟U=&<ֲ9雦nhhTS-YØmwTUS{䏉V6V6V6Ny+z8Cyh 6AF*Nx< 4́+V#\4*̤''ڎ1WWе9H,+u% hhi˳X,ֻ5Q%YC]|2s1LONֵsa+L9NM\ܳvwϷ+K{#Wr3 ӇqE(-=0 Ga ʫ;BɞMŬ$[ /_6*ͭ./tޛW Es;D@ >o;Ma+뼮K:li@R̯(HOyWimS_lkh;n._{˂v%,^fv?]ka39/%W"lKSTG.wr9Xt.^7~i* _vȑ'܊'u-쳞o?C `gX0&dBlzzo=BCL]q^p"+w8v]_:ޙUEW*C~:të /-I4lUm{ggܗPM1Lن1/19"@}qfmcd]@j_]2B u|K9k_9n(i  BOpOf&#A;߸0ߋkċ]!u%qT_XWVf]20Q%u\bXc{?_Ni_+?1rs^Xu"rIqgYh͢~c`A4 =ρLA E{[8:/5̩50pP$&~V3{6[1^y{$kM6}[q)((ikj1RƎH[4Զcl-$wFg-Dz7H$KARjl,kB?Í/BxMˊA~ ԕ K jj$s=;ciXsʮAmL HFeɏU{Og$?Bp6ېj{IE7|VtF9:U{ѿMY?_;(qiM @e<<م0ХOOjNBi Z?/W %Kڊӣ-C<<=OJQg}^ԩGȦl0PϹIh1}BK T>hvNDUړu@ AlPWWJwe0ieƎe/ HzmJ{gxFB3,kGJҧ'#CN ֐6yi(4@ AZH@ =HQ_Oh+z\qA‡F] Q Qwk2jC(W?)Z,9s,CDTl?yVͫw-;ǠĐv,;b}2~(2kjk Pn m|uҭWoK2Pൈ؈>^ .Շ΅D6/GbhJcieqxCn:ZSY;D_:/Gl&ǃ,8* #Hd@zf7=jǘu דbw מJ;i-@ <=Աn>J"ޛᆍ%vlF;bS-&dE߲GI.;(_1{@g2sݷo t!M뾱 W )9KEgM5kOS=^dF+ZXφ ߞs/TfS1#k6^yUM~r[C}&=$ܛ?L9kǑ'pMoΒE,i]?Tiiw 9s}~qmB4_|@;</[rv׸ϻ8VϽFlhWX^ qyE1Rd?%, B1L(8b p\!Ld-]6W,)=d@) >gd eйѡvZ@Haa<'חXQkîъ%V_@a#"{wobfnT> =xpoIO `acT}t?23/82W($Cw?k+A'=Zj!@f{h^Qn^^n^^nʘo>\+Jkj" @yq1{BJYMM?ډ<}ϝc|HYJ;xIw,g=j.֝I;?傻~C ]Ov1iXIډn;ϗ`DHx:4F&pZ;@L#bWGm'V'sh\3zd*\) VLK[b3@\bt\i:xO$l`ziYwӢ++=vUS7kܱ7lqOo6>+O r;\:&.'n܊r$'a[ƺx߅15C ,#:vhk~a嬮7VΙK|ڈ>S^w|FM[_ tҵ8s"֞;sx$3ZϞ%sj/tZ@]]] kޯgo޼us7l 0t}u}?RWQ1נ۷~7 X ֯m֛ *0XQ, ZWx ߏL,{%'[P(cպ0rWl5HP OAX Ҍ˨PNJ=^ ^GMRO+'@ A!ԭolRbktʹU WRytiU`;K#T}ҼG]H6ٸpD?f5L(3hϑȔ[\_22ywܽ $6*IDn`c>z~Ir(u qޏ*+cTV򢢷S $ڐ 䱚mDji 05lw`K*XI hP*+<4\h_iiP@hLvb:Cnۜf +[NT`,jaҔ)Mi~iNhSkOg I]0튧W4<+cϣUxx^1{İS^('rۺg7_]AZbk:%ȗ「2OվqN?+BIk',zŔz4J$L(53:`PxքI@5Z (2BYw*JL }W:K# BǠZˋ:Ēi;rT4[.dV-uu KmǙG) ė}=N]<7q:w1_L^r2@2#O)]!P`FSmRTF`/V.C,6kG! +M6P@iQ{^}%jX,Q1999P1ѫ~D|fUk oCJKUTTU9rLʻc3jm\=㉧/Bٖt hrŒ:'[מFdRȍ+.nYtت&ֳXUb ,eeEt\wn݌i{W\jE-B FH}w7h!~}J͓I,ģ{-T8#&MT?(%ƨ"7iTƵ94L۴wd*-SΌۑ7k`hX?P1 ~>旽/r#79e>p5@gD1(/07qm<=!0 Jxo8~R!;<4CZj*@G>kQ5RyE^:BuU<@zrsuy]ɢw<ֲ9i]u`2s[7vLFBfg+I#}Fou>ƈ'8V`s'6{9( f7583jߋ*Qg]YXM;~V(<2|`cM6$/hШ8'ՇZ-zԏʫwAA+6Xc-1FM-ޢi&LPQDQz=z/Wv~PDC@7N55Qn[Kً f(e.V '/M뤦 W1PJ{zvpY9 b^8L]hzL\T}D6=Zv[]Hqg:UꆕlKC=^@Mfw^FS]> 6UWW?\Bjiw@UG5n+LlTdmb7SɌ^ P|0e3.AP/zӽCH(ۤC.DPAG x~koakO鉗 7zhv0GYs'm*'*=vm+SOZ~'|{prLU<}? 7[Ǔ·UOBV?ZdT+՚PJRCJ!_(@yV 9([EKR_EjqX2N昵^o߲tĊ#wTYr|&Fg%L؟!}yM?zKa4gvhcpI}B>f7A3"<.oR=gkPcZ)4,?RBa|mм"BK"8ʓviGU|0;ĔI}0 v|#Y뵭?tOн[&9F-!͏(K^}틇䜼F>}%-wȬֶT%\F0g#?BMmML/WněJKdH uw̻;Z  ~ -OUvd 3/׹C@ 4~O*oݍqBLCt-@ v @ @ @~ >6= {0<z7 oϛާwuK泵]AY=$$pk_V~N`XE= ]S;dkc\?wݽǖ ܺ S~5RZ']e^ yzҡu‘?:73ïy {濺gu\uDžw3>Q-ɜ51o8A;;mҮA[议2?v7albD{quw2k _?gm'? e,Qgcvw]Em긷'=_u:ǂ!p5u yуgbsIw.##4Be/=p6mի ߛ Y@ 43v61 vPveE+g;üH* }[V7.^.d4WjLY&1L.XjR3Ġ ,ALIYt'bL'bsqѶG1M?͊BZ}0/rҁ=,_9JwCti[u!~Y[=jROjz-Spˊĕ"LWˊDܮmn\J, #~_Vۭcԕ(VF_H _9w^owH CRo`PINHJŅ!hSm( ~xG߮ʼ!8%]z[^}*ז)?,DcFo/㪚յפ|x9.{bnO*{[!C|i{޷2ZtuqnN×61Օ!ɘL@@xj$D_Mٸnh dA= |d}+Iygj< sFmS?! XQ7,|QIАIEoX@D9?_b]pXzC:/| o֜nY} V V *?XW$X[,Q~ڇ<$b>6҅"PP-{ `;:lDF6d|;Jx A:G OSudՐtv n+L]4AcUﭟ3cSGmk@%vZ24pݿaZ,0p{/'4[ L8i - %`ʐΜ4iʬS-n|gF mw'o& 钘PoLȸuthJ^FPSlbRjw4dwE-yac!+*sSjͱC3%^n1)v6+l9쮠2G,(C!@RtW%0qt4"PXQ Bpem70f;LL_Pٞ^Tbߚ] ?`-!Smp%K-JV,ckmfunGGZ*H8leNȑHOqIbFu_k3^X}%\xLdgɀGWyU,swNYÕL:1T][8V*mA/gJC\= ']2kC^و?nC 0v2fK2k&9 y9SG(bzLbD2$!SPmc\)LM) Sl0Y@,>ޘ~%k[]:SzѥL1 d$_60jDɭy e~t*yzpw|3zA#uʢJlv]I:먳N>tvXh3eg_ɞ0c燠5vE(:*ys*f\E5'l=;׆c\`#v:I E]e>OU-Gጓ4a7%b d!EA`Z7#@Bsa6ar7PX=?d#o7m$ USbXIm2McW̊_M(|}UT8CWq:odp*pfoGL2Қρy<@Qq1_Je (..)pSˊ͑!Ze?ڙ -0Ij+?zD.&=.\շ 2I2fҌ9Yz%@u@qnK[P::99#7+0'#4:(1h}*DKqʯT\D,eݎ sL;dP-u{DɎ՞5~{~ge4ʐeFEm /Gռ֌W@l6L:H$/"$c>3޳>=V[ 4rŦ:zW _:~7hOxY&Yz! F*- P\Na&7$Af=ԾvJ x ^{?Sr_˜\gy xQ3vʍ"p%q eհ9< m.b_1hl961ZPQ.!@(f~|Fϰ`R7l@In|a*vk F(oK3.WL{ֽWpKo][϶: EM>>zEwtlΰEɯ2Kt_MىnXyf(9sEdiqo4n#}a.uqkkƵ%{J%B`qy~Z[ơ{prLU<}?Λ|=WW?eW]>6ë~@xwf뭫xkKRCN:C˒dOf* @$*q {KHm: 2 @iJBFJSÁ*<*)C.DPAUWW?FQ?j[{ ^;|z_.OOlP`%f8C{6A EH}| Ҟ]3Ɉ]Joo,aF8ES,Oww>(o ;p7LA%XǓ·UOV?ZdTLj/0g v hs%BՠvKE2;otJ>zVQ&(ǒ RPx)+9Pg˩Rw0ib5I-KGIeeaw۠4}8Wo _q§EyQqsw*tw7>lԒe8 ~;9joDkIaFƷ[)2@ BBm{^tND,Wb  tɋcZ(§i?о(x#ΖD3hm+5*0@ .9 v51i _Ah:mpD4@ U(fwf2@ @ 䅴@ ̭~FNZ]GKv[wf)x/i.R[^;.<;}oRr͆_HSmxᰕ_wsQf 5YbߟM>BBm[g#pZ5ӰR%<?2r cCuf=P. œb \GǵqDž^z*l/$ {kUR ΃l74SՊJYZjzz-t*_˿_ZZkBh˹ r*xl',>%w @[2tfKU <:cHבsr3D5F~wm6f|hoX@[4K1 (((ߵ %hZEw"D\*%<"= \*N{ ,@\/eO+b*?F̺u>~/ qXP FdBL6lIrsgoDu&V<|riTnYG$%<|-D'$F$43EqA/HxI@'7K^OrRܡ]]6ƥR0W("qeiUe"bEglvMw_s+6AiucmĝЮA?`7.kyO oF4tm8pv$e t`AOuԵ_'ge}o=A{/8z麷;Nzcmo;ق%;!YƿҰÎM4ڵ(OVPݛgvhĭvm:~)%7>weu!}(ԛHs{K| #t;%r [UP+YGs4?TU OQ&ƺ2Gl $łҷ/%@q5ŵJͲ5QBiM |hwr$mk7!%v_f7cώoy}ȗ#vF\{<P[SŹ9U_8<';[GWɁvY;lp7cdbkWϸ/yS?! X`0Skk`8k IX- ^MGeҚZu5zfc2V#FqoG0;/mxbˁPr͓\uߵ;t^< @yMuL8m\[ Nnrѩ]N#1kb_XtdżY 2*> `ثrpP4MCeN@eԪuvNw٪ SH[' |bMA߀1Rr dnG9OEPћuXem7j(OA~ >0`H*@AɒCOsz7rh7ւuOkV꫌f@Rxkʃg`,dE޿8{8iB&ZW_m%(u/D}LG O"pt{8%b.@Hk[lׯC׬xosܝ:j\{NCv@ M"1VTP1' Ce4pJu1>$ V1 PXY*h(vWݼ: A9 uN*af' V҈GBaEE| !0dwE-yac!+2(u;IW^zUB cGWiJHRɹ@oo#h(n]`bex=h Pdy%<>yl |>W+q)G٨[oGtIF f!=4,!3νa@*dLQb1x<M]JSP86D"sFaOhD"U 2-59]UCI:mg W"J48iwǣa!SlX  0zXbZ`aԈ[I%58R! 8N]D"e'1|p uN'l=;׆W`i\zg (]Qvۏ1]k$5d1p8<.0֣w}gu |}Vea1@!SKs܎O 3NRB NtPPțUU 4 P%\QNIt79L޳Zz3n)`9þej%7Q5gT-.ΫՀ,\4FN&< c fl6ԶN=I oǁ֌ûU~fz: #ݹ~VВ'Vfw+ 6@QuRSPw2$2%@qB|a xl5*!>q @ 8P T䂖TAoeд 2v 0{7=biz}٪srˬtgYN| {b*b|inXz0A`@UP]@S6|?&G\[B5nwc,iiYkiupfv?}[f(h峌YF\C  Zɷ) X={kHiűK}.s?Ն fn{W/cL@ND:r,/6*~ 2RաA\M2/}]]L~0ox}j[xF{.ܿϵZH8u#g f~º ~/٨V2/L|IDATVU"~%?HmY{Վļb/Xqp68О5nv?:캖'?+3N>4ynC3zSўF;9W’to鷣=#o.PJRjF!! 9(*}EKR_meI$:sWnhd )((X:bő;|o|I茴Aú&;i~儶cI{Ҙ@h=]}ǬkM;dQKnX,稽MI`~;EK#mۢCiؓZo}0@  ]nl7CB{s]+ɍ1MI`|;$=yi?mȝ w]P|e/;fѼ: LSo!w6"@ M??GEۋMulי^$}ZY5$@ A^H@ H8HAĢ{^r@ @h? %>7Ӹ6ARa_ *jEV(t9vfoՄ>Ig2lHQn=LWY5rF*1Lzs3ûGm'۸81= i-f'Xx޼0wVu:z! <:ɢ;u ]  @ޡ1 /NHK^`itUzX>+nɷU]UM75i#'I+}vu9ϒ @\,Qb|xfC~[&Gfpu:u1Tb lԛHI 4d퉣TK=q8ˮ4|Cj~kڴa oG\g*#zTρͦGh8}`ص*,νK"eԍ}ei/.z9 AyMs\:3#_rXx6L4~`Zk~kV]9`˳3Uƞ(sA-๒6jP*e)z!.OD,ՠ0Qh\F̹iiQeU\]̴U8+6::TfÞ $)rѾ W ^F?O/AaToӪG;+s<-5sqX\JiiO+x KO^ߥC7k]T&DE%JcT쬔a<7g l j=_']mTh^FaW&Y9Ž2RbmZ?(=&؋?}G@Gz\rb[uQnlDTdB7Q'3U:QP&xT@TeJȓۥWg,cw+aLJӣ#^e pQrDTdDtz)Tnɏ{Pbh\+hhYQ,mM%h`QRdﻂ}WCNQ⋘,9Lj<#KU\<{hY|"2D29QJ8ݧFN pFqA7066=v rx=v~two` '/b^&rظ+]t$@  r(~E+*2ՕJ1|9W` A.;y=]zQGbptR"?V ُ.US}G*yŧ(>B@SBX!)JIK)&|Rx<TcqqfZjJZv5 LIH|6/3m"T䦤dT(kp@,(Y;_U fB! DQ.D}'(PRR[צ.,!*G (6-)Ɉ}0TV47[H*+$luSnZ@?uEq9@Ki H~pW ^P{):tpUV׌UmQoKGh@h4++Z m I 2;=+peʽr4>?|WR`((qb1glx(8_nS鮲R7W,)-E԰ itVhVb5wIO")L( k}! جZ4M09VH%J֍Snjee!@2PMz=Y.75;u(I)n&{R^E)Ywc Ҋ'IEBe!#gE><ޗ?LDb1P,6WAwUHTsI=E>_*~XXu-b̿aj7L773\ q =L6KOrx@ zTu=fޭ}7"1@5W c$ͪDQqTSbT @q8,K ĿgOqU-~LM nlnoJع ,"0(_.DL(I02v0Sˉ|2uzLvWZwJoYKrKSR+jt3d0iݧvGKTk\8B d!@jFFѧg3ggvYp|k+ 9v.S2B'bb!yi@P؍)y箅D PS OK``.B/>4J%ѷob|^ /!-]_tpdƖ};K1D dw0r@r1'1l}#u pM+r"1t)UhdoTӡzɫ d2",-EW ^Td$fHCWWU*46beD<|%rB@q449inS[)I\R.)Kz=ƬKrF;|SME:LXb.C`Xt9F=Wf旊*)s.Ii)(i (P$sWkg.CBsFE_[C=P=HvN.LhSxsڡQz%E%[/dev/null | grep "Version =" | cut -f2 -d"\"" || echo " unknown") override TIMESTAMP := $(shell date +%s 2>/dev/null || echo "0") override DATESTAMP := $(shell date '+%Y-%m-%d %H:%M:%S' || echo "5 minutes ago") ifeq ($(shell command -v gdate >/dev/null; echo $$?),0) DATE_CMD := gdate else DATE_CMD := date endif ifneq ($(QUIET),true) override PRE := info info-quiet override QUIET := false else override PRE := info-quiet endif PREFIX ?= /usr/local #? Detect PLATFORM and ARCH from uname/gcc if not set PLATFORM ?= $(shell uname -s || echo unknown) ifneq ($(filter unknown Darwin, $(PLATFORM)),) override PLATFORM := $(shell $(CXX) -dumpmachine | awk -F"-" '{ print (NF==4) ? $$3 : $$2 }') ifeq ($(PLATFORM),apple) override PLATFORM := macos endif endif ifeq ($(shell uname -v | grep ARM64 >/dev/null 2>&1; echo $$?),0) ARCH ?= arm64 else ARCH ?= $(shell $(CXX) -dumpmachine | cut -d "-" -f 1) endif override PLATFORM_LC := $(shell echo $(PLATFORM) | tr '[:upper:]' '[:lower:]') #? Any flags added to TESTFLAGS must not contain whitespace for the testing to work override TESTFLAGS := -fexceptions -fstack-clash-protection -fcf-protection ifneq ($(PLATFORM) $(ARCH),macos arm64) override TESTFLAGS += -fstack-protector endif ifeq ($(STATIC),true) override ADDFLAGS += -static-libgcc -static-libstdc++ ifneq ($(PLATFORM),macos) override ADDFLAGS += -DSTATIC_BUILD -static -Wl,--fatal-warnings endif endif ifeq ($(STRIP),true) override ADDFLAGS += -s endif #? Compiler and Linker ifeq ($(shell command -v g++-11 >/dev/null; echo $$?),0) CXX := g++-11 else ifeq ($(shell command -v g++11 >/dev/null; echo $$?),0) CXX := g++11 else ifeq ($(shell command -v g++ >/dev/null; echo $$?),0) CXX := g++ endif override CXX_VERSION := $(shell $(CXX) -dumpfullversion -dumpversion || echo 0) #? Try to make sure we are using GCC/G++ version 11 or later if not instructed to use g++-10 ifeq ($(CXX),g++) ifeq ($(shell g++ --version | grep clang >/dev/null 2>&1; echo $$?),0) V_MAJOR := 0 else V_MAJOR := $(shell echo $(CXX_VERSION) | cut -f1 -d".") endif ifneq ($(shell test $(V_MAJOR) -ge 11; echo $$?),0) ifeq ($(shell command -v g++-11 >/dev/null; echo $$?),0) override CXX := g++-11 override CXX_VERSION := $(shell $(CXX) -dumpfullversion -dumpversion || echo 0) endif endif endif #? Pull in platform specific source files and get thread count ifeq ($(PLATFORM_LC),linux) PLATFORM_DIR := linux THREADS := $(shell getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1) SU_GROUP := root else ifeq ($(PLATFORM_LC),freebsd) PLATFORM_DIR := freebsd THREADS := $(shell getconf NPROCESSORS_ONLN 2>/dev/null || echo 1) SU_GROUP := wheel override ADDFLAGS += -lstdc++ -lm -lkvm -ldevstat -Wl,-rpath=/usr/local/lib/gcc11 export MAKE = gmake else ifeq ($(PLATFORM_LC),macos) PLATFORM_DIR := osx THREADS := $(shell sysctl -n hw.ncpu || echo 1) override ADDFLAGS += -framework IOKit -framework CoreFoundation -Wno-format-truncation SU_GROUP := wheel else $(error $(shell printf "\033[1;91mERROR: \033[97mUnsupported platform ($(PLATFORM))\033[0m")) endif #? Use all CPU cores (will only be set if using Make 4.3+) MAKEFLAGS := --jobs=$(THREADS) ifeq ($(THREADS),1) override THREADS := auto endif #? The Directories, Source, Includes, Objects and Binary SRCDIR := src INCDIR := include BUILDDIR := obj TARGETDIR := bin SRCEXT := cpp DEPEXT := d OBJEXT := o #? Filter out unsupported compiler flags override GOODFLAGS := $(foreach flag,$(TESTFLAGS),$(strip $(shell echo "int main() {}" | $(CXX) -o /dev/null $(flag) -x c++ - >/dev/null 2>&1 && echo $(flag) || true))) #? Flags, Libraries and Includes override REQFLAGS := -std=c++20 WARNFLAGS := -Wall -Wextra -pedantic OPTFLAGS := -O2 -ftree-loop-vectorize -flto=$(THREADS) LDCXXFLAGS := -pthread -D_FORTIFY_SOURCE=2 -D_GLIBCXX_ASSERTIONS $(GOODFLAGS) $(ADDFLAGS) override CXXFLAGS += $(REQFLAGS) $(LDCXXFLAGS) $(OPTFLAGS) $(WARNFLAGS) override LDFLAGS += $(LDCXXFLAGS) $(OPTFLAGS) $(WARNFLAGS) INC := -I$(INCDIR) -I$(SRCDIR) SU_USER := root ifdef DEBUG override OPTFLAGS := -O0 -g endif SOURCES := $(sort $(shell find $(SRCDIR) -maxdepth 1 -type f -name *.$(SRCEXT))) SOURCES += $(sort $(shell find $(SRCDIR)/$(PLATFORM_DIR) -type f -name *.$(SRCEXT))) #? Setup percentage progress SOURCE_COUNT := $(words $(SOURCES)) OBJECTS := $(patsubst $(SRCDIR)/%,$(BUILDDIR)/%,$(SOURCES:.$(SRCEXT)=.$(OBJEXT))) ifeq ($(shell find $(BUILDDIR) -type f -newermt "$(DATESTAMP)" -name *.o >/dev/null 2>&1; echo $$?),0) ifneq ($(wildcard $(BUILDDIR)/.*),) SKIPPED_SOURCES := $(foreach fname,$(SOURCES),$(shell find $(BUILDDIR) -type f -newer $(fname) -name *.o | grep "$(basename $(notdir $(fname))).o" 2>/dev/null)) override SOURCE_COUNT := $(shell expr $(SOURCE_COUNT) - $(words $(SKIPPED_SOURCES))) ifeq ($(SOURCE_COUNT),0) override SOURCE_COUNT = $(words $(SOURCES)) endif endif PROGRESS = expr $$(find $(BUILDDIR) -type f -newermt "$(DATESTAMP)" -name *.o | wc -l || echo 1) '*' 90 / $(SOURCE_COUNT) | cut -c1-2 else PROGRESS = expr $$(find $(BUILDDIR) -type f -name *.o | wc -l || echo 1) '*' 90 / $(SOURCE_COUNT) | cut -c1-2 endif P := %% #? Default Make all: $(PRE) directories btop info: @printf " $(BANNER)\n" @printf "\033[1;92mPLATFORM \033[1;93m?| \033[0m$(PLATFORM)\n" @printf "\033[1;96mARCH \033[1;93m?| \033[0m$(ARCH)\n" @printf "\033[1;93mCXX \033[1;93m?| \033[0m$(CXX) \033[1;93m(\033[97m$(CXX_VERSION)\033[93m)\n" @printf "\033[1;94mTHREADS \033[1;94m:| \033[0m$(THREADS)\n" @printf "\033[1;92mREQFLAGS \033[1;91m!| \033[0m$(REQFLAGS)\n" @printf "\033[1;91mWARNFLAGS \033[1;94m:| \033[0m$(WARNFLAGS)\n" @printf "\033[1;94mOPTFLAGS \033[1;94m:| \033[0m$(OPTFLAGS)\n" @printf "\033[1;93mLDCXXFLAGS \033[1;94m:| \033[0m$(LDCXXFLAGS)\n" @printf "\033[1;95mCXXFLAGS \033[1;92m+| \033[0;37m\$$(\033[92mREQFLAGS\033[37m) \$$(\033[93mLDCXXFLAGS\033[37m) \$$(\033[94mOPTFLAGS\033[37m) \$$(\033[91mWARNFLAGS\033[37m)\n" @printf "\033[1;95mLDFLAGS \033[1;92m+| \033[0;37m\$$(\033[93mLDCXXFLAGS\033[37m) \$$(\033[94mOPTFLAGS\033[37m) \$$(\033[91mWARNFLAGS\033[37m)\n" info-quiet: @sleep 0.1 2>/dev/null || true @printf "\n\033[1;92mBuilding btop++ \033[91m(\033[97mv$(BTOP_VERSION)\033[91m) \033[93m$(PLATFORM) \033[96m$(ARCH)\033[0m\n" help: @printf " $(BANNER)\n" @printf "\033[1;97mbtop++ makefile\033[0m\n" @printf "usage: make [argument]\n\n" @printf "arguments:\n" @printf " all Compile btop (default argument)\n" @printf " clean Remove built objects\n" @printf " distclean Remove built objects and binaries\n" @printf " install Install btop++ to \$$PREFIX ($(PREFIX))\n" @printf " setuid Set installed binary owner/group to \$$SU_USER/\$$SU_GROUP ($(SU_USER)/$(SU_GROUP)) and set SUID bit\n" @printf " uninstall Uninstall btop++ from \$$PREFIX\n" @printf " info Display information about Environment,compiler and linker flags\n" #? Make the Directories directories: @mkdir -p $(TARGETDIR) @mkdir -p $(BUILDDIR)/$(PLATFORM_DIR) #? Clean only Objects clean: @printf "\033[1;91mRemoving: \033[1;97mbuilt objects...\033[0m\n" @rm -rf $(BUILDDIR) #? Clean Objects and Binaries distclean: clean @printf "\033[1;91mRemoving: \033[1;97mbuilt binaries...\033[0m\n" @rm -rf $(TARGETDIR) install: @printf "\033[1;92mInstalling binary to: \033[1;97m$(DESTDIR)$(PREFIX)/bin/btop\n" @mkdir -p $(DESTDIR)$(PREFIX)/bin @cp -p $(TARGETDIR)/btop $(DESTDIR)$(PREFIX)/bin/btop @chmod 755 $(DESTDIR)$(PREFIX)/bin/btop @printf "\033[1;92mInstalling doc to: \033[1;97m$(DESTDIR)$(PREFIX)/share/btop\n" @mkdir -p $(DESTDIR)$(PREFIX)/share/btop @cp -p README.md $(DESTDIR)$(PREFIX)/share/btop @printf "\033[1;92mInstalling themes to: \033[1;97m$(DESTDIR)$(PREFIX)/share/btop/themes\033[0m\n" @cp -pr themes $(DESTDIR)$(PREFIX)/share/btop #? Set SUID bit for btop as $SU_USER in $SU_GROUP setuid: @printf "\033[1;97mFile: $(DESTDIR)$(PREFIX)/bin/btop\n" @printf "\033[1;92mSetting owner \033[1;97m$(SU_USER):$(SU_GROUP)\033[0m\n" @chown $(SU_USER):$(SU_GROUP) $(DESTDIR)$(PREFIX)/bin/btop @printf "\033[1;92mSetting SUID bit\033[0m\n" @chmod u+s $(DESTDIR)$(PREFIX)/bin/btop uninstall: @printf "\033[1;91mRemoving: \033[1;97m$(DESTDIR)$(PREFIX)/bin/btop\033[0m\n" @rm -rf $(DESTDIR)$(PREFIX)/bin/btop @printf "\033[1;91mRemoving: \033[1;97m$(DESTDIR)$(PREFIX)/share/btop\033[0m\n" @rm -rf $(DESTDIR)$(PREFIX)/share/btop #? Pull in dependency info for *existing* .o files -include $(OBJECTS:.$(OBJEXT)=.$(DEPEXT)) #? Link .ONESHELL: btop: $(OBJECTS) @sleep 0.2 2>/dev/null || true @TSTAMP=$$(date +%s 2>/dev/null || echo "0") @$(QUIET) || printf "\n\033[1;92mLinking and optimizing binary\033[37m...\033[0m\n" @$(CXX) -o $(TARGETDIR)/btop $^ $(LDFLAGS) || exit 1 @printf "\033[1;92m100$(P) -> \033[1;37m$(TARGETDIR)/btop \033[100D\033[38C\033[1;93m(\033[1;97m$$(du -ah $(TARGETDIR)/btop | cut -f1)iB\033[1;93m) \033[92m(\033[97m$$($(DATE_CMD) -d @$$(expr $$(date +%s 2>/dev/null || echo "0") - $${TSTAMP} 2>/dev/null) -u +%Mm:%Ss 2>/dev/null | sed 's/^00m://' || echo '')\033[92m)\033[0m\n" @printf "\n\033[1;92mBuild complete in \033[92m(\033[97m$$($(DATE_CMD) -d @$$(expr $$(date +%s 2>/dev/null || echo "0") - $(TIMESTAMP) 2>/dev/null) -u +%Mm:%Ss 2>/dev/null | sed 's/^00m://' || echo "unknown")\033[92m)\033[0m\n" #? Compile .ONESHELL: $(BUILDDIR)/%.$(OBJEXT): $(SRCDIR)/%.$(SRCEXT) @sleep 0.3 2>/dev/null || true @TSTAMP=$$(date +%s 2>/dev/null || echo "0") @$(QUIET) || printf "\033[1;97mCompiling $<\033[0m\n" @$(CXX) $(CXXFLAGS) $(INC) -MMD -c -o $@ $< || exit 1 @printf "\033[1;92m$$($(PROGRESS))$(P)\033[10D\033[5C-> \033[1;37m$@ \033[100D\033[38C\033[1;93m(\033[1;97m$$(du -ah $@ | cut -f1)iB\033[1;93m) \033[92m(\033[97m$$($(DATE_CMD) -d @$$(expr $$($(DATE_CMD) +%s 2>/dev/null || echo "0") - $${TSTAMP} 2>/dev/null) -u +%Mm:%Ss 2>/dev/null | sed 's/^00m://' || echo '')\033[92m)\033[0m\n" #? Non-File Targets .PHONY: all msg help pre btop-1.2.3/README.md000066400000000000000000000635751420276253000137610ustar00rootroot00000000000000# ![btop++](Img/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) ![c++20](https://img.shields.io/badge/cpp-c%2B%2B20-green) ![latest_release](https://img.shields.io/github/v/tag/aristocratos/btop?label=release) [![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) [![btop](https://snapcraft.io/btop/badge.svg)](https://snapcraft.io/btop) [![Continuous Build Linux](https://github.com/aristocratos/btop/actions/workflows/continuous-build-linux.yml/badge.svg)](https://github.com/aristocratos/btop/actions/workflows/continuous-build-linux.yml) [![Continuous Build MacOS](https://github.com/aristocratos/btop/actions/workflows/continuous-build-macos.yml/badge.svg)](https://github.com/aristocratos/btop/actions/workflows/continuous-build-macos.yml) ## 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!) * [Screenshots](#screenshots) * [Keybindings](#help-menu) * [Installation Linux/OSX](#installation) * [Compilation Linux](#compilation-linux) * [Compilation OSX](#compilation-osx) * [Compilation FreeBSD](#compilation-freebsd) * [Installing the snap](#installing-the-snap) * [Configurability](#configurability) * [License](#license) ## News ##### 16 January 2022 Release v1.2.0 with FreeBSD support. No release binaries for FreeBSD provided as of yet. Again a big thanks to [@joske](https://github.com/joske) for his porting efforts! Since compatibility with Linux, MacOS and FreeBSD are done, the focus going forward will be on new features like GPU monitoring. ##### 13 November 2021 Release v1.1.0 with OSX support. Binaries in [continuous-build-macos](https://github.com/aristocratos/btop/actions/workflows/continuous-build-macos.yml) are only x86 for now. Macos binaries + installer are included for both x86 and ARM64 (Apple Silicon) in the releases. Big thank you to [@joske](https://github.com/joske) who wrote the vast majority of the implementation!
More... ##### 30 October 2021 Work on the OSX and FreeBSD branches, both initiated and mostly worked on by [@joske](https://github.com/joske), will likely be completed in the coming weeks. The OSX branch has some memory leaks that needs to be sorted out and both have some issues with the processes cpu usage calculation and other smaller issues that needs fixing. If you want to help out, test for bugs/fix bugs or just try out the branches: **OSX** ```bash # Install and use Homebrew or MacPorts package managers for easy dependency installation brew install coreutils make gcc@11 git clone https://github.com/aristocratos/btop.git cd btop git checkout OSX gmake ``` **FreeBSD** ```bash sudo pkg install gmake gcc11 coreutils git git clone https://github.com/aristocratos/btop.git cd btop git checkout freebsd gmake ``` Note that GNU make (`gmake`) is recommended but not required for OSX but it is required on FreeBSD. ##### 6 October 2021 OsX development have been started by [@joske](https://github.com/joske), big thanks :) See branch [OSX](https://github.com/aristocratos/btop/tree/OSX) for current progress. ##### 18 September 2021 The Linux version of btop++ is complete. Released as version 1.0.0 I will be providing statically compiled binaries for a range of architectures in every release for those having problems compiling. For compilation GCC 10 is required, GCC 11 preferred. Please report any bugs to the [Issues](https://github.com/aristocratos/btop/issues/new?assignees=aristocratos&labels=bug&template=bug_report.md&title=%5BBUG%5D) page. 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... Windows support is not in the plans as of now, but if anyone else wants to take it on, I will try to help. ##### 5 May 2021 This project is gonna take some time until it has complete feature parity with bpytop, since all system information gathering will 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.
## Documents **[CHANGELOG.md](CHANGELOG.md)** **[CONTRIBUTING.md](CONTRIBUTING.md)** **[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)** ## Description Resource monitor that shows usage and stats for processor, memory, disks, network and processes. C++ version and continuation of [bashtop](https://github.com/aristocratos/bashtop) and [bpytop](https://github.com/aristocratos/bpytop). ## 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. * Easy switching between sorting options. * Tree view of processes. * Send any signal to selected process. * UI menu for changing all config file options. * Auto scaling graph for network usage. * Shows IO activity and speeds for disks * Battery meter * Selectable symbols for the graphs * Custom presets * And more... ## Themes Btop++ uses the same theme files as bpytop and bashtop (some color values missing in bashtop themes) . See [themes](https://github.com/aristocratos/btop/tree/master/themes) folder for available themes. The `make install` command places the default themes in `[$PREFIX or /usr/local]/share/btop/themes`. User created themes should be placed in `$XDG_CONFIG_HOME/btop/themes` or `$HOME/.config/btop/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 For best experience, 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" arguments. * 16 color TTY mode will be activated if a real tty device is detected. Can be forced with "-t/--tty_on" arguments. * 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 (Not needed in TTY mode or with graphs set to type: block or tty.) * 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 btop, 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. ## Screenshots #### Main UI showing details for a selected process ![Screenshot 1](Img/normal.png) #### Main UI in TTY mode ![Screenshot 2](Img/tty.png) #### Main UI with custom options ![Screenshot 3](Img/alt.png) #### Main-menu ![Screenshot 3](Img/main-menu.png) #### Options-menu ![Screenshot 4](Img/options-menu.png) #### Help-menu ![Screenshot 5](Img/help-menu.png) ## Installation **Binaries for Linux are statically compiled with musl and works on kernel 2.6.39 and newer** 1. **Download btop-(VERSION)-(ARCH)-(PLATFORM).tbz from [latest release](https://github.com/aristocratos/btop/releases/latest) and unpack to a new folder** **Notice! Use x86_64 for 64-bit x86 systems, i486 and i686 are 32-bit!** 2. **Install (from created folder)** * **Run install.sh or:** ``` bash # use "make install PREFIX=/target/dir" to set target, default: /usr/local # only use "sudo" when installing to a NON user owned directory sudo make install ``` 3. **(Optional) Set suid bit to make btop always run as root (or other user)** Enables signal sending to any process without starting with `sudo` and can prevent /proc read permissions problems on some systems. * **Run setuid.sh or:** ``` bash # run after make install and use same PREFIX if any was used at install # set SU_USER and SU_GROUP to select user and group, default is root:root sudo make setuid ``` * **Uninstall** * **Run uninstall.sh or:** ``` bash sudo make uninstall ``` * **Show help** ```bash make help ``` **Binary release (from native os repo)** * **openSUSE** * **Tumbleweed:** ```bash sudo zypper in btop ``` * For all other versions, see [openSUSE Software: btop](https://software.opensuse.org/package/btop) **Binary release on Homebrew (macOS (x86_64 & ARM64) / Linux (x86_64))** * **[Homebrew](https://formulae.brew.sh/formula/btop)** ```bash brew install btop ``` ## Compilation Linux Needs GCC 10 or higher, (GCC 11 or above strongly recommended for better CPU efficiency in the compiled binary). The makefile also needs GNU coreutils and `sed` (should already be installed on any modern distribution). For a `cmake` based build alternative see the [fork](https://github.com/jan-guenter/btop/tree/main) by @jan-guenter 1. **Install dependencies (example for Ubuntu 21.04 Hirsute)** Use gcc-10 g++-10 if gcc-11 isn't available ``` bash sudo apt install coreutils sed git build-essential gcc-11 g++-11 ``` 2. **Clone repository** ``` bash git clone https://github.com/aristocratos/btop.git cd btop ``` 3. **Compile** Append `STATIC=true` to `make` command for static compilation. Notice! If using LDAP Authentication, usernames will show as UID number for LDAP users if compiling statically with glibc. Append `QUIET=true` for less verbose output. Append `STRIP=true` to force stripping of debug symbols (adds `-s` linker flag). Append `ARCH=` to manually set the target architecture. If omitted the makefile uses the machine triple (output of `-dumpmachine` compiler parameter) to detect the target system. Use `ADDFLAGS` variable for appending flags to both compiler and linker. For example: `ADDFLAGS=-march=native` might give a performance boost if compiling only for your own system. If `g++` is linked to an older version of gcc on your system specify the correct version by appending `CXX=g++-10` or `CXX=g++-11`. ``` bash make ``` 4. **Install** Append `PREFIX=/target/dir` to set target, default: `/usr/local` Notice! Only use "sudo" when installing to a NON user owned directory. ``` bash sudo make install ``` 5. **(Optional) Set suid bit to make btop always run as root (or other user)** No need for `sudo` to enable signal sending to any process and to prevent /proc read permissions problems on some systems. Run after make install and use same PREFIX if any was used at install. Set `SU_USER` and `SU_GROUP` to select user and group, default is `root` and `root` ``` bash sudo make setuid ``` * **Uninstall** ``` bash sudo make uninstall ``` * **Remove any object files from source dir** ```bash make clean ``` * **Remove all object files, binaries and created directories in source dir** ```bash make distclean ``` * **Show help** ```bash make help ``` ## Compilation OSX Needs GCC 10 or higher, (GCC 11 or above strongly recommended for better CPU efficiency in the compiled binary). The makefile also needs GNU coreutils and `sed`. Install and use Homebrew or MacPorts package managers for easy dependency installation 1. **Install dependencies (example for Homebrew)** ``` bash brew install coreutils make gcc@11 ``` 2. **Clone repository** ``` bash git clone https://github.com/aristocratos/btop.git cd btop ``` 3. **Compile** Append `STATIC=true` to `make` command for static compilation (only libgcc and libstdc++ will be static!). Append `QUIET=true` for less verbose output. Append `STRIP=true` to force stripping of debug symbols (adds `-s` linker flag). Append `ARCH=` to manually set the target architecture. If omitted the makefile uses the machine triple (output of `-dumpmachine` compiler parameter) to detect the target system. Use `ADDFLAGS` variable for appending flags to both compiler and linker. For example: `ADDFLAGS=-march=native` might give a performance boost if compiling only for your own system. ``` bash gmake ``` 4. **Install** Append `PREFIX=/target/dir` to set target, default: `/usr/local` Notice! Only use "sudo" when installing to a NON user owned directory. ``` bash sudo gmake install ``` 5. **(Recommended) Set suid bit to make btop always run as root (or other user)** No need for `sudo` to see information for non user owned processes and to enable signal sending to any process. Run after make install and use same PREFIX if any was used at install. Set `SU_USER` and `SU_GROUP` to select user and group, default is `root` and `wheel` ``` bash sudo gmake setuid ``` * **Uninstall** ``` bash sudo gmake uninstall ``` * **Remove any object files from source dir** ```bash gmake clean ``` * **Remove all object files, binaries and created directories in source dir** ```bash gmake distclean ``` * **Show help** ```bash gmake help ``` ## Compilation FreeBSD Needs GCC 10 or higher, (GCC 11 or above strongly recommended for better CPU efficiency in the compiled binary). Note that GNU make (`gmake`) is required to compile on FreeBSD. 1. **Install dependencies** ``` bash sudo pkg install gmake gcc11 coreutils git ``` 2. **Clone repository** ``` bash git clone https://github.com/aristocratos/btop.git cd btop ``` 3. **Compile** Append `STATIC=true` to `make` command for static compilation. Append `QUIET=true` for less verbose output. Append `STRIP=true` to force stripping of debug symbols (adds `-s` linker flag). Append `ARCH=` to manually set the target architecture. If omitted the makefile uses the machine triple (output of `-dumpmachine` compiler parameter) to detect the target system. Use `ADDFLAGS` variable for appending flags to both compiler and linker. For example: `ADDFLAGS=-march=native` might give a performance boost if compiling only for your own system. ``` bash gmake ``` 4. **Install** Append `PREFIX=/target/dir` to set target, default: `/usr/local` Notice! Only use "sudo" when installing to a NON user owned directory. ``` bash sudo gmake install ``` 5. **(Recommended) Set suid bit to make btop always run as root (or other user)** No need for `sudo` to see information for non user owned processes and to enable signal sending to any process. Run after make install and use same PREFIX if any was used at install. Set `SU_USER` and `SU_GROUP` to select user and group, default is `root` and `wheel` ``` bash sudo gmake setuid ``` * **Uninstall** ``` bash sudo gmake uninstall ``` * **Remove any object files from source dir** ```bash gmake clean ``` * **Remove all object files, binaries and created directories in source dir** ```bash gmake distclean ``` * **Show help** ```bash gmake help ``` ## Installing the snap [![btop](https://snapcraft.io/btop/badge.svg)](https://snapcraft.io/btop) * **Install the snap** ```bash sudo snap install btop ``` * **Install the latest snap from the edge channel** ``` sudo snap install btop --edge ``` * **Connect the interface** ```bash sudo snap connect btop:removable-media ``` ## Configurability All options changeable from within UI. Config and log files stored in `$XDG_CONFIG_HOME/btop` or `$HOME/.config/btop` folder #### btop.conf: (auto generated if not found) ```bash #? Config file for btop v. 1.2.2 #* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes. #* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes" color_theme = "Default" #* If the theme set background should be shown, set to False if you want terminal background transparency. theme_background = True #* Sets if 24-bit truecolor should be used, will convert 24-bit colors to 256 color (6x6x6 color cube) if false. truecolor = True #* Set to true to force tty mode regardless if a real tty has been detected or not. #* Will force 16-color mode and TTY theme, set all graph symbols to "tty" and swap out other non tty friendly symbols. force_tty = False #* Define presets for the layout of the boxes. Preset 0 is always all boxes shown with default settings. Max 9 presets. #* Format: "box_name:P:G,box_name:P:G" P=(0 or 1) for alternate positions, G=graph symbol to use for box. #* Use withespace " " as separator between different presets. #* Example: "cpu:0:default,mem:0:tty,proc:1:default cpu:0:braille,proc:0:tty" presets = "cpu:1:default,proc:0:default cpu:0:default,mem:0:default,net:0:default cpu:0:block,net:0:tty" #* Set to True to enable "h,j,k,l" keys for directional control in lists. #* Conflicting keys for h:"help" and k:"kill" is accessible while holding shift. vim_keys = False #* Rounded corners on boxes, is ignored if TTY mode is ON. rounded_corners = True #* Default symbols to use for graph creation, "braille", "block" or "tty". #* "braille" offers the highest resolution but might not be included in all fonts. #* "block" has half the resolution of braille but uses more common characters. #* "tty" uses only 3 different symbols but will work with most fonts and should work in a real TTY. #* Note that "tty" only has half the horizontal resolution of the other two, so will show a shorter historical view. graph_symbol = "braille" # Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". graph_symbol_cpu = "default" # Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". graph_symbol_mem = "default" # Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". graph_symbol_net = "default" # Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty". graph_symbol_proc = "default" #* Manually set which boxes to show. Available values are "cpu mem net proc", separate values with whitespace. shown_boxes = "proc cpu mem net" #* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs. update_ms = 1500 #* Processes sorting, "pid" "program" "arguments" "threads" "user" "memory" "cpu lazy" "cpu responsive", #* "cpu lazy" sorts top process over time (easier to follow), "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 #* 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 = True #* Show process memory as bytes instead of percent. proc_mem_bytes = True #* Use /proc/[pid]/smaps for memory information in the process info box (very slow but more accurate) proc_info_smaps = False #* Show proc box on left side of screen instead of right. proc_left = False #* Sets the CPU stat shown in upper half of the CPU graph, "total" is always available. #* 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. #* 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 #* Show cpu box at bottom of screen instead of top. cpu_bottom = False #* Shows the system uptime in the CPU box. show_uptime = True #* Show cpu temperature. 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 #* Set a custom mapping between core and coretemp, can be needed on certain cpus to get correct temperature for correct core. #* Use lm-sensors or similar to see which cores are reporting temperatures on your machine. #* Format "x:y" x=core with wrong temp, y=core with correct temp, use space as separator between multiple entries. #* Example: "4:0 5:1 6:3" cpu_core_map = "" #* Which temperature scale to use, available values: "celsius", "fahrenheit", "kelvin" and "rankine". temp_scale = "celsius" #* Use base 10 for bits/bytes sizes, KB = 1000 instead of KiB = 1024. base_10_sizes = False #* Show CPU frequency. show_cpu_freq = True #* Draw a clock at top of screen, formatting according to strftime, empty string to disable. #* Special formatting: /host = hostname | /user = username | /uptime = system uptime clock_format = "%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 whitespace " ". #* 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 #* Show mem box below net box instead of above. mem_below_net = False #* 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 = False #* Set to true to show available disk space for privileged users. disk_free_priv = False #* Toggles if io activity % (disk busy time) should be shown in regular disk usage view. show_io_stat = True #* Toggles io mode for disks, showing 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 (100 by default), use format "mountpoint:speed" separate disks with whitespace " ". #* Example: "/mnt/media:100 /:20 /boot:1". io_graph_speeds = "" #* Set fixed values for network graphs in Mebibits. Is only used if net_auto is also set to False. net_download = 100 net_upload = 100 #* Use network graphs auto rescaling mode, ignores any values set above and rescales down to 10 Kibibytes at the lowest. net_auto = True #* Sync the auto scaling for download and upload to whichever currently has the highest scale. net_sync = False #* Starts with the Network Interface specified here. net_iface = "br0" #* Show battery stats in top right if battery is present. show_battery = True #* Which battery to use if multiple are present. "Auto" for auto detection. selected_battery = "Auto" #* Set loglevel for "~/.config/btop/btop.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: btop [-h] [-v] [-/+t] [-p ] [--utf-force] [--debug] optional arguments: -h, --help show this help message and exit -v, --version show version info and exit -lc, --low-color disable truecolor, converts 24-bit colors to 256-color -t, --tty_on force (ON) tty mode, max 16 colors and tty friendly graph symbols +t, --tty_off force (OFF) tty mode -p, --preset start with preset, integer value between 0-9 --utf-force force start even if no UTF-8 locale was detected --debug start in DEBUG mode: shows microsecond timer for information collect and screen draw functions and sets loglevel to DEBUG ``` ## LICENSE [Apache License 2.0](LICENSE) btop-1.2.3/include/000077500000000000000000000000001420276253000141055ustar00rootroot00000000000000btop-1.2.3/include/robin_hood.h000066400000000000000000002704611420276253000164120ustar00rootroot00000000000000// ______ _____ ______ _________ // ______________ ___ /_ ___(_)_______ ___ /_ ______ ______ ______ / // __ ___/_ __ \__ __ \__ / __ __ \ __ __ \_ __ \_ __ \_ __ / // _ / / /_/ /_ /_/ /_ / _ / / / _ / / // /_/ // /_/ // /_/ / // /_/ \____/ /_.___/ /_/ /_/ /_/ ________/_/ /_/ \____/ \____/ \__,_/ // _/_____/ // // Fast & memory efficient hashtable based on robin hood hashing for C++11/14/17/20 // https://github.com/martinus/robin-hood-hashing // // Licensed under the MIT License . // SPDX-License-Identifier: MIT // Copyright (c) 2018-2021 Martin Ankerl // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #ifndef ROBIN_HOOD_H_INCLUDED #define ROBIN_HOOD_H_INCLUDED // see https://semver.org/ #define ROBIN_HOOD_VERSION_MAJOR 3 // for incompatible API changes #define ROBIN_HOOD_VERSION_MINOR 11 // for adding functionality in a backwards-compatible manner #define ROBIN_HOOD_VERSION_PATCH 3 // for backwards-compatible bug fixes #include #include #include #include #include #include // only to support hash of smart pointers #include #include #include #include #if __cplusplus >= 201703L # include #endif // #define ROBIN_HOOD_LOG_ENABLED #ifdef ROBIN_HOOD_LOG_ENABLED # include # define ROBIN_HOOD_LOG(...) \ std::cout << __FUNCTION__ << "@" << __LINE__ << ": " << __VA_ARGS__ << std::endl; #else # define ROBIN_HOOD_LOG(x) #endif // #define ROBIN_HOOD_TRACE_ENABLED #ifdef ROBIN_HOOD_TRACE_ENABLED # include # define ROBIN_HOOD_TRACE(...) \ std::cout << __FUNCTION__ << "@" << __LINE__ << ": " << __VA_ARGS__ << std::endl; #else # define ROBIN_HOOD_TRACE(x) #endif // #define ROBIN_HOOD_COUNT_ENABLED #ifdef ROBIN_HOOD_COUNT_ENABLED # include # define ROBIN_HOOD_COUNT(x) ++counts().x; namespace robin_hood { struct Counts { uint64_t shiftUp{}; uint64_t shiftDown{}; }; inline std::ostream& operator<<(std::ostream& os, Counts const& c) { return os << c.shiftUp << " shiftUp" << std::endl << c.shiftDown << " shiftDown" << std::endl; } static Counts& counts() { static Counts counts{}; return counts; } } // namespace robin_hood #else # define ROBIN_HOOD_COUNT(x) #endif // all non-argument macros should use this facility. See // https://www.fluentcpp.com/2019/05/28/better-macros-better-flags/ #define ROBIN_HOOD(x) ROBIN_HOOD_PRIVATE_DEFINITION_##x() // mark unused members with this macro #define ROBIN_HOOD_UNUSED(identifier) // bitness #if SIZE_MAX == UINT32_MAX # define ROBIN_HOOD_PRIVATE_DEFINITION_BITNESS() 32 #elif SIZE_MAX == UINT64_MAX # define ROBIN_HOOD_PRIVATE_DEFINITION_BITNESS() 64 #else # error Unsupported bitness #endif // endianess #ifdef _MSC_VER # define ROBIN_HOOD_PRIVATE_DEFINITION_LITTLE_ENDIAN() 1 # define ROBIN_HOOD_PRIVATE_DEFINITION_BIG_ENDIAN() 0 #else # define ROBIN_HOOD_PRIVATE_DEFINITION_LITTLE_ENDIAN() \ (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) # define ROBIN_HOOD_PRIVATE_DEFINITION_BIG_ENDIAN() (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) #endif // inline #ifdef _MSC_VER # define ROBIN_HOOD_PRIVATE_DEFINITION_NOINLINE() __declspec(noinline) #else # define ROBIN_HOOD_PRIVATE_DEFINITION_NOINLINE() __attribute__((noinline)) #endif // exceptions #if !defined(__cpp_exceptions) && !defined(__EXCEPTIONS) && !defined(_CPPUNWIND) # define ROBIN_HOOD_PRIVATE_DEFINITION_HAS_EXCEPTIONS() 0 #else # define ROBIN_HOOD_PRIVATE_DEFINITION_HAS_EXCEPTIONS() 1 #endif // count leading/trailing bits #if !defined(ROBIN_HOOD_DISABLE_INTRINSICS) # ifdef _MSC_VER # if ROBIN_HOOD(BITNESS) == 32 # define ROBIN_HOOD_PRIVATE_DEFINITION_BITSCANFORWARD() _BitScanForward # else # define ROBIN_HOOD_PRIVATE_DEFINITION_BITSCANFORWARD() _BitScanForward64 # endif # include # pragma intrinsic(ROBIN_HOOD(BITSCANFORWARD)) # define ROBIN_HOOD_COUNT_TRAILING_ZEROES(x) \ [](size_t mask) noexcept -> int { \ unsigned long index; \ return ROBIN_HOOD(BITSCANFORWARD)(&index, mask) ? static_cast(index) \ : ROBIN_HOOD(BITNESS); \ }(x) # else # if ROBIN_HOOD(BITNESS) == 32 # define ROBIN_HOOD_PRIVATE_DEFINITION_CTZ() __builtin_ctzl # define ROBIN_HOOD_PRIVATE_DEFINITION_CLZ() __builtin_clzl # else # define ROBIN_HOOD_PRIVATE_DEFINITION_CTZ() __builtin_ctzll # define ROBIN_HOOD_PRIVATE_DEFINITION_CLZ() __builtin_clzll # endif # define ROBIN_HOOD_COUNT_LEADING_ZEROES(x) ((x) ? ROBIN_HOOD(CLZ)(x) : ROBIN_HOOD(BITNESS)) # define ROBIN_HOOD_COUNT_TRAILING_ZEROES(x) ((x) ? ROBIN_HOOD(CTZ)(x) : ROBIN_HOOD(BITNESS)) # endif #endif // fallthrough #ifndef __has_cpp_attribute // For backwards compatibility # define __has_cpp_attribute(x) 0 #endif #if __has_cpp_attribute(clang::fallthrough) # define ROBIN_HOOD_PRIVATE_DEFINITION_FALLTHROUGH() [[clang::fallthrough]] #elif __has_cpp_attribute(gnu::fallthrough) # define ROBIN_HOOD_PRIVATE_DEFINITION_FALLTHROUGH() [[gnu::fallthrough]] #else # define ROBIN_HOOD_PRIVATE_DEFINITION_FALLTHROUGH() #endif // likely/unlikely #ifdef _MSC_VER # define ROBIN_HOOD_LIKELY(condition) condition # define ROBIN_HOOD_UNLIKELY(condition) condition #else # define ROBIN_HOOD_LIKELY(condition) __builtin_expect(condition, 1) # define ROBIN_HOOD_UNLIKELY(condition) __builtin_expect(condition, 0) #endif // detect if native wchar_t type is availiable in MSVC #ifdef _MSC_VER # ifdef _NATIVE_WCHAR_T_DEFINED # define ROBIN_HOOD_PRIVATE_DEFINITION_HAS_NATIVE_WCHART() 1 # else # define ROBIN_HOOD_PRIVATE_DEFINITION_HAS_NATIVE_WCHART() 0 # endif #else # define ROBIN_HOOD_PRIVATE_DEFINITION_HAS_NATIVE_WCHART() 1 #endif // detect if MSVC supports the pair(std::piecewise_construct_t,...) consructor being constexpr #ifdef _MSC_VER # if _MSC_VER <= 1900 # define ROBIN_HOOD_PRIVATE_DEFINITION_BROKEN_CONSTEXPR() 1 # else # define ROBIN_HOOD_PRIVATE_DEFINITION_BROKEN_CONSTEXPR() 0 # endif #else # define ROBIN_HOOD_PRIVATE_DEFINITION_BROKEN_CONSTEXPR() 0 #endif // workaround missing "is_trivially_copyable" in g++ < 5.0 // See https://stackoverflow.com/a/31798726/48181 #if defined(__GNUC__) && __GNUC__ < 5 # define ROBIN_HOOD_IS_TRIVIALLY_COPYABLE(...) __has_trivial_copy(__VA_ARGS__) #else # define ROBIN_HOOD_IS_TRIVIALLY_COPYABLE(...) std::is_trivially_copyable<__VA_ARGS__>::value #endif // helpers for C++ versions, see https://gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html #define ROBIN_HOOD_PRIVATE_DEFINITION_CXX() __cplusplus #define ROBIN_HOOD_PRIVATE_DEFINITION_CXX98() 199711L #define ROBIN_HOOD_PRIVATE_DEFINITION_CXX11() 201103L #define ROBIN_HOOD_PRIVATE_DEFINITION_CXX14() 201402L #define ROBIN_HOOD_PRIVATE_DEFINITION_CXX17() 201703L #if ROBIN_HOOD(CXX) >= ROBIN_HOOD(CXX17) # define ROBIN_HOOD_PRIVATE_DEFINITION_NODISCARD() [[nodiscard]] #else # define ROBIN_HOOD_PRIVATE_DEFINITION_NODISCARD() #endif namespace robin_hood { #if ROBIN_HOOD(CXX) >= ROBIN_HOOD(CXX14) # define ROBIN_HOOD_STD std #else // c++11 compatibility layer namespace ROBIN_HOOD_STD { template struct alignment_of : std::integral_constant::type)> {}; template class integer_sequence { public: using value_type = T; static_assert(std::is_integral::value, "not integral type"); static constexpr std::size_t size() noexcept { return sizeof...(Ints); } }; template using index_sequence = integer_sequence; namespace detail_ { template struct IntSeqImpl { using TValue = T; static_assert(std::is_integral::value, "not integral type"); static_assert(Begin >= 0 && Begin < End, "unexpected argument (Begin<0 || Begin<=End)"); template struct IntSeqCombiner; template struct IntSeqCombiner, integer_sequence> { using TResult = integer_sequence; }; using TResult = typename IntSeqCombiner::TResult, typename IntSeqImpl::TResult>::TResult; }; template struct IntSeqImpl { using TValue = T; static_assert(std::is_integral::value, "not integral type"); static_assert(Begin >= 0, "unexpected argument (Begin<0)"); using TResult = integer_sequence; }; template struct IntSeqImpl { using TValue = T; static_assert(std::is_integral::value, "not integral type"); static_assert(Begin >= 0, "unexpected argument (Begin<0)"); using TResult = integer_sequence; }; } // namespace detail_ template using make_integer_sequence = typename detail_::IntSeqImpl::TResult; template using make_index_sequence = make_integer_sequence; template using index_sequence_for = make_index_sequence; } // namespace ROBIN_HOOD_STD #endif namespace detail { // make sure we static_cast to the correct type for hash_int #if ROBIN_HOOD(BITNESS) == 64 using SizeT = uint64_t; #else using SizeT = uint32_t; #endif template T rotr(T x, unsigned k) { return (x >> k) | (x << (8U * sizeof(T) - k)); } // This cast gets rid of warnings like "cast from 'uint8_t*' {aka 'unsigned char*'} to // 'uint64_t*' {aka 'long unsigned int*'} increases required alignment of target type". Use with // care! template inline T reinterpret_cast_no_cast_align_warning(void* ptr) noexcept { return reinterpret_cast(ptr); } template inline T reinterpret_cast_no_cast_align_warning(void const* ptr) noexcept { return reinterpret_cast(ptr); } // make sure this is not inlined as it is slow and dramatically enlarges code, thus making other // inlinings more difficult. Throws are also generally the slow path. template [[noreturn]] ROBIN_HOOD(NOINLINE) #if ROBIN_HOOD(HAS_EXCEPTIONS) void doThrow(Args&&... args) { // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay) throw E(std::forward(args)...); } #else void doThrow(Args&&... ROBIN_HOOD_UNUSED(args) /*unused*/) { abort(); } #endif template T* assertNotNull(T* t, Args&&... args) { if (ROBIN_HOOD_UNLIKELY(nullptr == t)) { doThrow(std::forward(args)...); } return t; } template inline T unaligned_load(void const* ptr) noexcept { // using memcpy so we don't get into unaligned load problems. // compiler should optimize this very well anyways. T t; std::memcpy(&t, ptr, sizeof(T)); return t; } // Allocates bulks of memory for objects of type T. This deallocates the memory in the destructor, // and keeps a linked list of the allocated memory around. Overhead per allocation is the size of a // pointer. template class BulkPoolAllocator { public: BulkPoolAllocator() noexcept = default; // does not copy anything, just creates a new allocator. BulkPoolAllocator(const BulkPoolAllocator& ROBIN_HOOD_UNUSED(o) /*unused*/) noexcept : mHead(nullptr) , mListForFree(nullptr) {} BulkPoolAllocator(BulkPoolAllocator&& o) noexcept : mHead(o.mHead) , mListForFree(o.mListForFree) { o.mListForFree = nullptr; o.mHead = nullptr; } BulkPoolAllocator& operator=(BulkPoolAllocator&& o) noexcept { reset(); mHead = o.mHead; mListForFree = o.mListForFree; o.mListForFree = nullptr; o.mHead = nullptr; return *this; } BulkPoolAllocator& // NOLINTNEXTLINE(bugprone-unhandled-self-assignment,cert-oop54-cpp) operator=(const BulkPoolAllocator& ROBIN_HOOD_UNUSED(o) /*unused*/) noexcept { // does not do anything return *this; } ~BulkPoolAllocator() noexcept { reset(); } // Deallocates all allocated memory. void reset() noexcept { while (mListForFree) { T* tmp = *mListForFree; ROBIN_HOOD_LOG("std::free") std::free(mListForFree); mListForFree = reinterpret_cast_no_cast_align_warning(tmp); } mHead = nullptr; } // allocates, but does NOT initialize. Use in-place new constructor, e.g. // T* obj = pool.allocate(); // ::new (static_cast(obj)) T(); T* allocate() { T* tmp = mHead; if (!tmp) { tmp = performAllocation(); } mHead = *reinterpret_cast_no_cast_align_warning(tmp); return tmp; } // does not actually deallocate but puts it in store. // make sure you have already called the destructor! e.g. with // obj->~T(); // pool.deallocate(obj); void deallocate(T* obj) noexcept { *reinterpret_cast_no_cast_align_warning(obj) = mHead; mHead = obj; } // Adds an already allocated block of memory to the allocator. This allocator is from now on // responsible for freeing the data (with free()). If the provided data is not large enough to // make use of, it is immediately freed. Otherwise it is reused and freed in the destructor. void addOrFree(void* ptr, const size_t numBytes) noexcept { // calculate number of available elements in ptr if (numBytes < ALIGNMENT + ALIGNED_SIZE) { // not enough data for at least one element. Free and return. ROBIN_HOOD_LOG("std::free") std::free(ptr); } else { ROBIN_HOOD_LOG("add to buffer") add(ptr, numBytes); } } void swap(BulkPoolAllocator& other) noexcept { using std::swap; swap(mHead, other.mHead); swap(mListForFree, other.mListForFree); } private: // iterates the list of allocated memory to calculate how many to alloc next. // Recalculating this each time saves us a size_t member. // This ignores the fact that memory blocks might have been added manually with addOrFree. In // practice, this should not matter much. ROBIN_HOOD(NODISCARD) size_t calcNumElementsToAlloc() const noexcept { auto tmp = mListForFree; size_t numAllocs = MinNumAllocs; while (numAllocs * 2 <= MaxNumAllocs && tmp) { auto x = reinterpret_cast(tmp); tmp = *x; numAllocs *= 2; } return numAllocs; } // WARNING: Underflow if numBytes < ALIGNMENT! This is guarded in addOrFree(). void add(void* ptr, const size_t numBytes) noexcept { const size_t numElements = (numBytes - ALIGNMENT) / ALIGNED_SIZE; auto data = reinterpret_cast(ptr); // link free list auto x = reinterpret_cast(data); *x = mListForFree; mListForFree = data; // create linked list for newly allocated data auto* const headT = reinterpret_cast_no_cast_align_warning(reinterpret_cast(ptr) + ALIGNMENT); auto* const head = reinterpret_cast(headT); // Visual Studio compiler automatically unrolls this loop, which is pretty cool for (size_t i = 0; i < numElements; ++i) { *reinterpret_cast_no_cast_align_warning(head + i * ALIGNED_SIZE) = head + (i + 1) * ALIGNED_SIZE; } // last one points to 0 *reinterpret_cast_no_cast_align_warning(head + (numElements - 1) * ALIGNED_SIZE) = mHead; mHead = headT; } // Called when no memory is available (mHead == 0). // Don't inline this slow path. ROBIN_HOOD(NOINLINE) T* performAllocation() { size_t const numElementsToAlloc = calcNumElementsToAlloc(); // alloc new memory: [prev |T, T, ... T] size_t const bytes = ALIGNMENT + ALIGNED_SIZE * numElementsToAlloc; ROBIN_HOOD_LOG("std::malloc " << bytes << " = " << ALIGNMENT << " + " << ALIGNED_SIZE << " * " << numElementsToAlloc) add(assertNotNull(std::malloc(bytes)), bytes); return mHead; } // enforce byte alignment of the T's #if ROBIN_HOOD(CXX) >= ROBIN_HOOD(CXX14) static constexpr size_t ALIGNMENT = (std::max)(std::alignment_of::value, std::alignment_of::value); #else static const size_t ALIGNMENT = (ROBIN_HOOD_STD::alignment_of::value > ROBIN_HOOD_STD::alignment_of::value) ? ROBIN_HOOD_STD::alignment_of::value : +ROBIN_HOOD_STD::alignment_of::value; // the + is for walkarround #endif static constexpr size_t ALIGNED_SIZE = ((sizeof(T) - 1) / ALIGNMENT + 1) * ALIGNMENT; static_assert(MinNumAllocs >= 1, "MinNumAllocs"); static_assert(MaxNumAllocs >= MinNumAllocs, "MaxNumAllocs"); static_assert(ALIGNED_SIZE >= sizeof(T*), "ALIGNED_SIZE"); static_assert(0 == (ALIGNED_SIZE % sizeof(T*)), "ALIGNED_SIZE mod"); static_assert(ALIGNMENT >= sizeof(T*), "ALIGNMENT"); T* mHead{nullptr}; T** mListForFree{nullptr}; }; template struct NodeAllocator; // dummy allocator that does nothing template struct NodeAllocator { // we are not using the data, so just free it. void addOrFree(void* ptr, size_t ROBIN_HOOD_UNUSED(numBytes) /*unused*/) noexcept { ROBIN_HOOD_LOG("std::free") std::free(ptr); } }; template struct NodeAllocator : public BulkPoolAllocator {}; // c++14 doesn't have is_nothrow_swappable, and clang++ 6.0.1 doesn't like it either, so I'm making // my own here. namespace swappable { #if ROBIN_HOOD(CXX) < ROBIN_HOOD(CXX17) using std::swap; template struct nothrow { static const bool value = noexcept(swap(std::declval(), std::declval())); }; #else template struct nothrow { static const bool value = std::is_nothrow_swappable::value; }; #endif } // namespace swappable } // namespace detail struct is_transparent_tag {}; // A custom pair implementation is used in the map because std::pair is not is_trivially_copyable, // which means it would not be allowed to be used in std::memcpy. This struct is copyable, which is // also tested. template struct pair { using first_type = T1; using second_type = T2; template ::value && std::is_default_constructible::value>::type> constexpr pair() noexcept(noexcept(U1()) && noexcept(U2())) : first() , second() {} // pair constructors are explicit so we don't accidentally call this ctor when we don't have to. explicit constexpr pair(std::pair const& o) noexcept( noexcept(T1(std::declval())) && noexcept(T2(std::declval()))) : first(o.first) , second(o.second) {} // pair constructors are explicit so we don't accidentally call this ctor when we don't have to. explicit constexpr pair(std::pair&& o) noexcept(noexcept( T1(std::move(std::declval()))) && noexcept(T2(std::move(std::declval())))) : first(std::move(o.first)) , second(std::move(o.second)) {} constexpr pair(T1&& a, T2&& b) noexcept(noexcept( T1(std::move(std::declval()))) && noexcept(T2(std::move(std::declval())))) : first(std::move(a)) , second(std::move(b)) {} template constexpr pair(U1&& a, U2&& b) noexcept(noexcept(T1(std::forward( std::declval()))) && noexcept(T2(std::forward(std::declval())))) : first(std::forward(a)) , second(std::forward(b)) {} template // MSVC 2015 produces error "C2476: ‘constexpr’ constructor does not initialize all members" // if this constructor is constexpr #if !ROBIN_HOOD(BROKEN_CONSTEXPR) constexpr #endif pair(std::piecewise_construct_t /*unused*/, std::tuple a, std::tuple b) noexcept(noexcept(pair(std::declval&>(), std::declval&>(), ROBIN_HOOD_STD::index_sequence_for(), ROBIN_HOOD_STD::index_sequence_for()))) : pair(a, b, ROBIN_HOOD_STD::index_sequence_for(), ROBIN_HOOD_STD::index_sequence_for()) { } // constructor called from the std::piecewise_construct_t ctor template pair(std::tuple& a, std::tuple& b, ROBIN_HOOD_STD::index_sequence /*unused*/, ROBIN_HOOD_STD::index_sequence /*unused*/) noexcept( noexcept(T1(std::forward(std::get( std::declval&>()))...)) && noexcept(T2(std:: forward(std::get( std::declval&>()))...))) : first(std::forward(std::get(a))...) , second(std::forward(std::get(b))...) { // make visual studio compiler happy about warning about unused a & b. // Visual studio's pair implementation disables warning 4100. (void)a; (void)b; } void swap(pair& o) noexcept((detail::swappable::nothrow::value) && (detail::swappable::nothrow::value)) { using std::swap; swap(first, o.first); swap(second, o.second); } T1 first; // NOLINT(misc-non-private-member-variables-in-classes) T2 second; // NOLINT(misc-non-private-member-variables-in-classes) }; template inline void swap(pair& a, pair& b) noexcept( noexcept(std::declval&>().swap(std::declval&>()))) { a.swap(b); } template inline constexpr bool operator==(pair const& x, pair const& y) { return (x.first == y.first) && (x.second == y.second); } template inline constexpr bool operator!=(pair const& x, pair const& y) { return !(x == y); } template inline constexpr bool operator<(pair const& x, pair const& y) noexcept(noexcept( std::declval() < std::declval()) && noexcept(std::declval() < std::declval())) { return x.first < y.first || (!(y.first < x.first) && x.second < y.second); } template inline constexpr bool operator>(pair const& x, pair const& y) { return y < x; } template inline constexpr bool operator<=(pair const& x, pair const& y) { return !(x > y); } template inline constexpr bool operator>=(pair const& x, pair const& y) { return !(x < y); } inline size_t hash_bytes(void const* ptr, size_t len) noexcept { static constexpr uint64_t m = UINT64_C(0xc6a4a7935bd1e995); static constexpr uint64_t seed = UINT64_C(0xe17a1465); static constexpr unsigned int r = 47; auto const* const data64 = static_cast(ptr); uint64_t h = seed ^ (len * m); size_t const n_blocks = len / 8; for (size_t i = 0; i < n_blocks; ++i) { auto k = detail::unaligned_load(data64 + i); k *= m; k ^= k >> r; k *= m; h ^= k; h *= m; } auto const* const data8 = reinterpret_cast(data64 + n_blocks); switch (len & 7U) { case 7: h ^= static_cast(data8[6]) << 48U; ROBIN_HOOD(FALLTHROUGH); // FALLTHROUGH case 6: h ^= static_cast(data8[5]) << 40U; ROBIN_HOOD(FALLTHROUGH); // FALLTHROUGH case 5: h ^= static_cast(data8[4]) << 32U; ROBIN_HOOD(FALLTHROUGH); // FALLTHROUGH case 4: h ^= static_cast(data8[3]) << 24U; ROBIN_HOOD(FALLTHROUGH); // FALLTHROUGH case 3: h ^= static_cast(data8[2]) << 16U; ROBIN_HOOD(FALLTHROUGH); // FALLTHROUGH case 2: h ^= static_cast(data8[1]) << 8U; ROBIN_HOOD(FALLTHROUGH); // FALLTHROUGH case 1: h ^= static_cast(data8[0]); h *= m; ROBIN_HOOD(FALLTHROUGH); // FALLTHROUGH default: break; } h ^= h >> r; // not doing the final step here, because this will be done by keyToIdx anyways // h *= m; // h ^= h >> r; return static_cast(h); } inline size_t hash_int(uint64_t x) noexcept { // tried lots of different hashes, let's stick with murmurhash3. It's simple, fast, well tested, // and doesn't need any special 128bit operations. x ^= x >> 33U; x *= UINT64_C(0xff51afd7ed558ccd); x ^= x >> 33U; // not doing the final step here, because this will be done by keyToIdx anyways // x *= UINT64_C(0xc4ceb9fe1a85ec53); // x ^= x >> 33U; return static_cast(x); } // A thin wrapper around std::hash, performing an additional simple mixing step of the result. template struct hash : public std::hash { size_t operator()(T const& obj) const noexcept(noexcept(std::declval>().operator()(std::declval()))) { // call base hash auto result = std::hash::operator()(obj); // return mixed of that, to be save against identity has return hash_int(static_cast(result)); } }; template struct hash> { size_t operator()(std::basic_string const& str) const noexcept { return hash_bytes(str.data(), sizeof(CharT) * str.size()); } }; #if ROBIN_HOOD(CXX) >= ROBIN_HOOD(CXX17) template struct hash> { size_t operator()(std::basic_string_view const& sv) const noexcept { return hash_bytes(sv.data(), sizeof(CharT) * sv.size()); } }; #endif template struct hash { size_t operator()(T* ptr) const noexcept { return hash_int(reinterpret_cast(ptr)); } }; template struct hash> { size_t operator()(std::unique_ptr const& ptr) const noexcept { return hash_int(reinterpret_cast(ptr.get())); } }; template struct hash> { size_t operator()(std::shared_ptr const& ptr) const noexcept { return hash_int(reinterpret_cast(ptr.get())); } }; template struct hash::value>::type> { size_t operator()(Enum e) const noexcept { using Underlying = typename std::underlying_type::type; return hash{}(static_cast(e)); } }; #define ROBIN_HOOD_HASH_INT(T) \ template <> \ struct hash { \ size_t operator()(T const& obj) const noexcept { \ return hash_int(static_cast(obj)); \ } \ } #if defined(__GNUC__) && !defined(__clang__) # pragma GCC diagnostic push # pragma GCC diagnostic ignored "-Wuseless-cast" #endif // see https://en.cppreference.com/w/cpp/utility/hash ROBIN_HOOD_HASH_INT(bool); ROBIN_HOOD_HASH_INT(char); ROBIN_HOOD_HASH_INT(signed char); ROBIN_HOOD_HASH_INT(unsigned char); ROBIN_HOOD_HASH_INT(char16_t); ROBIN_HOOD_HASH_INT(char32_t); #if ROBIN_HOOD(HAS_NATIVE_WCHART) ROBIN_HOOD_HASH_INT(wchar_t); #endif ROBIN_HOOD_HASH_INT(short); ROBIN_HOOD_HASH_INT(unsigned short); ROBIN_HOOD_HASH_INT(int); ROBIN_HOOD_HASH_INT(unsigned int); ROBIN_HOOD_HASH_INT(long); ROBIN_HOOD_HASH_INT(long long); ROBIN_HOOD_HASH_INT(unsigned long); ROBIN_HOOD_HASH_INT(unsigned long long); #if defined(__GNUC__) && !defined(__clang__) # pragma GCC diagnostic pop #endif namespace detail { template struct void_type { using type = void; }; template struct has_is_transparent : public std::false_type {}; template struct has_is_transparent::type> : public std::true_type {}; // using wrapper classes for hash and key_equal prevents the diamond problem when the same type // is used. see https://stackoverflow.com/a/28771920/48181 template struct WrapHash : public T { WrapHash() = default; explicit WrapHash(T const& o) noexcept(noexcept(T(std::declval()))) : T(o) {} }; template struct WrapKeyEqual : public T { WrapKeyEqual() = default; explicit WrapKeyEqual(T const& o) noexcept(noexcept(T(std::declval()))) : T(o) {} }; // A highly optimized hashmap implementation, using the Robin Hood algorithm. // // In most cases, this map should be usable as a drop-in replacement for std::unordered_map, but // be about 2x faster in most cases and require much less allocations. // // This implementation uses the following memory layout: // // [Node, Node, ... Node | info, info, ... infoSentinel ] // // * Node: either a DataNode that directly has the std::pair as member, // or a DataNode with a pointer to std::pair. Which DataNode representation to use // depends on how fast the swap() operation is. Heuristically, this is automatically choosen // based on sizeof(). there are always 2^n Nodes. // // * info: Each Node in the map has a corresponding info byte, so there are 2^n info bytes. // Each byte is initialized to 0, meaning the corresponding Node is empty. Set to 1 means the // corresponding node contains data. Set to 2 means the corresponding Node is filled, but it // actually belongs to the previous position and was pushed out because that place is already // taken. // // * infoSentinel: Sentinel byte set to 1, so that iterator's ++ can stop at end() without the // need for a idx variable. // // According to STL, order of templates has effect on throughput. That's why I've moved the // boolean to the front. // https://www.reddit.com/r/cpp/comments/ahp6iu/compile_time_binary_size_reductions_and_cs_future/eeguck4/ template class Table : public WrapHash, public WrapKeyEqual, detail::NodeAllocator< typename std::conditional< std::is_void::value, Key, robin_hood::pair::type, T>>::type, 4, 16384, IsFlat> { public: static constexpr bool is_flat = IsFlat; static constexpr bool is_map = !std::is_void::value; static constexpr bool is_set = !is_map; static constexpr bool is_transparent = has_is_transparent::value && has_is_transparent::value; using key_type = Key; using mapped_type = T; using value_type = typename std::conditional< is_set, Key, robin_hood::pair::type, T>>::type; using size_type = size_t; using hasher = Hash; using key_equal = KeyEqual; using Self = Table; private: static_assert(MaxLoadFactor100 > 10 && MaxLoadFactor100 < 100, "MaxLoadFactor100 needs to be >10 && < 100"); using WHash = WrapHash; using WKeyEqual = WrapKeyEqual; // configuration defaults // make sure we have 8 elements, needed to quickly rehash mInfo static constexpr size_t InitialNumElements = sizeof(uint64_t); static constexpr uint32_t InitialInfoNumBits = 5; static constexpr uint8_t InitialInfoInc = 1U << InitialInfoNumBits; static constexpr size_t InfoMask = InitialInfoInc - 1U; static constexpr uint8_t InitialInfoHashShift = 0; using DataPool = detail::NodeAllocator; // type needs to be wider than uint8_t. using InfoType = uint32_t; // DataNode //////////////////////////////////////////////////////// // Primary template for the data node. We have special implementations for small and big // objects. For large objects it is assumed that swap() is fairly slow, so we allocate these // on the heap so swap merely swaps a pointer. template class DataNode {}; // Small: just allocate on the stack. template class DataNode final { public: template explicit DataNode(M& ROBIN_HOOD_UNUSED(map) /*unused*/, Args&&... args) noexcept( noexcept(value_type(std::forward(args)...))) : mData(std::forward(args)...) {} DataNode(M& ROBIN_HOOD_UNUSED(map) /*unused*/, DataNode&& n) noexcept( std::is_nothrow_move_constructible::value) : mData(std::move(n.mData)) {} // doesn't do anything void destroy(M& ROBIN_HOOD_UNUSED(map) /*unused*/) noexcept {} void destroyDoNotDeallocate() noexcept {} value_type const* operator->() const noexcept { return &mData; } value_type* operator->() noexcept { return &mData; } const value_type& operator*() const noexcept { return mData; } value_type& operator*() noexcept { return mData; } template ROBIN_HOOD(NODISCARD) typename std::enable_if::type getFirst() noexcept { return mData.first; } template ROBIN_HOOD(NODISCARD) typename std::enable_if::type getFirst() noexcept { return mData; } template ROBIN_HOOD(NODISCARD) typename std::enable_if::type getFirst() const noexcept { return mData.first; } template ROBIN_HOOD(NODISCARD) typename std::enable_if::type getFirst() const noexcept { return mData; } template ROBIN_HOOD(NODISCARD) typename std::enable_if::type getSecond() noexcept { return mData.second; } template ROBIN_HOOD(NODISCARD) typename std::enable_if::type getSecond() const noexcept { return mData.second; } void swap(DataNode& o) noexcept( noexcept(std::declval().swap(std::declval()))) { mData.swap(o.mData); } private: value_type mData; }; // big object: allocate on heap. template class DataNode { public: template explicit DataNode(M& map, Args&&... args) : mData(map.allocate()) { ::new (static_cast(mData)) value_type(std::forward(args)...); } DataNode(M& ROBIN_HOOD_UNUSED(map) /*unused*/, DataNode&& n) noexcept : mData(std::move(n.mData)) {} void destroy(M& map) noexcept { // don't deallocate, just put it into list of datapool. mData->~value_type(); map.deallocate(mData); } void destroyDoNotDeallocate() noexcept { mData->~value_type(); } value_type const* operator->() const noexcept { return mData; } value_type* operator->() noexcept { return mData; } const value_type& operator*() const { return *mData; } value_type& operator*() { return *mData; } template ROBIN_HOOD(NODISCARD) typename std::enable_if::type getFirst() noexcept { return mData->first; } template ROBIN_HOOD(NODISCARD) typename std::enable_if::type getFirst() noexcept { return *mData; } template ROBIN_HOOD(NODISCARD) typename std::enable_if::type getFirst() const noexcept { return mData->first; } template ROBIN_HOOD(NODISCARD) typename std::enable_if::type getFirst() const noexcept { return *mData; } template ROBIN_HOOD(NODISCARD) typename std::enable_if::type getSecond() noexcept { return mData->second; } template ROBIN_HOOD(NODISCARD) typename std::enable_if::type getSecond() const noexcept { return mData->second; } void swap(DataNode& o) noexcept { using std::swap; swap(mData, o.mData); } private: value_type* mData; }; using Node = DataNode; // helpers for insertKeyPrepareEmptySpot: extract first entry (only const required) ROBIN_HOOD(NODISCARD) key_type const& getFirstConst(Node const& n) const noexcept { return n.getFirst(); } // in case we have void mapped_type, we are not using a pair, thus we just route k through. // No need to disable this because it's just not used if not applicable. ROBIN_HOOD(NODISCARD) key_type const& getFirstConst(key_type const& k) const noexcept { return k; } // in case we have non-void mapped_type, we have a standard robin_hood::pair template ROBIN_HOOD(NODISCARD) typename std::enable_if::value, key_type const&>::type getFirstConst(value_type const& vt) const noexcept { return vt.first; } // Cloner ////////////////////////////////////////////////////////// template struct Cloner; // fast path: Just copy data, without allocating anything. template struct Cloner { void operator()(M const& source, M& target) const { auto const* const src = reinterpret_cast(source.mKeyVals); auto* tgt = reinterpret_cast(target.mKeyVals); auto const numElementsWithBuffer = target.calcNumElementsWithBuffer(target.mMask + 1); std::copy(src, src + target.calcNumBytesTotal(numElementsWithBuffer), tgt); } }; template struct Cloner { void operator()(M const& s, M& t) const { auto const numElementsWithBuffer = t.calcNumElementsWithBuffer(t.mMask + 1); std::copy(s.mInfo, s.mInfo + t.calcNumBytesInfo(numElementsWithBuffer), t.mInfo); for (size_t i = 0; i < numElementsWithBuffer; ++i) { if (t.mInfo[i]) { ::new (static_cast(t.mKeyVals + i)) Node(t, *s.mKeyVals[i]); } } } }; // Destroyer /////////////////////////////////////////////////////// template struct Destroyer {}; template struct Destroyer { void nodes(M& m) const noexcept { m.mNumElements = 0; } void nodesDoNotDeallocate(M& m) const noexcept { m.mNumElements = 0; } }; template struct Destroyer { void nodes(M& m) const noexcept { m.mNumElements = 0; // clear also resets mInfo to 0, that's sometimes not necessary. auto const numElementsWithBuffer = m.calcNumElementsWithBuffer(m.mMask + 1); for (size_t idx = 0; idx < numElementsWithBuffer; ++idx) { if (0 != m.mInfo[idx]) { Node& n = m.mKeyVals[idx]; n.destroy(m); n.~Node(); } } } void nodesDoNotDeallocate(M& m) const noexcept { m.mNumElements = 0; // clear also resets mInfo to 0, that's sometimes not necessary. auto const numElementsWithBuffer = m.calcNumElementsWithBuffer(m.mMask + 1); for (size_t idx = 0; idx < numElementsWithBuffer; ++idx) { if (0 != m.mInfo[idx]) { Node& n = m.mKeyVals[idx]; n.destroyDoNotDeallocate(); n.~Node(); } } } }; // Iter //////////////////////////////////////////////////////////// struct fast_forward_tag {}; // generic iterator for both const_iterator and iterator. template // NOLINTNEXTLINE(hicpp-special-member-functions,cppcoreguidelines-special-member-functions) class Iter { private: using NodePtr = typename std::conditional::type; public: using difference_type = std::ptrdiff_t; using value_type = typename Self::value_type; using reference = typename std::conditional::type; using pointer = typename std::conditional::type; using iterator_category = std::forward_iterator_tag; // default constructed iterator can be compared to itself, but WON'T return true when // compared to end(). Iter() = default; // Rule of zero: nothing specified. The conversion constructor is only enabled for // iterator to const_iterator, so it doesn't accidentally work as a copy ctor. // Conversion constructor from iterator to const_iterator. template ::type> // NOLINTNEXTLINE(hicpp-explicit-conversions) Iter(Iter const& other) noexcept : mKeyVals(other.mKeyVals) , mInfo(other.mInfo) {} Iter(NodePtr valPtr, uint8_t const* infoPtr) noexcept : mKeyVals(valPtr) , mInfo(infoPtr) {} Iter(NodePtr valPtr, uint8_t const* infoPtr, fast_forward_tag ROBIN_HOOD_UNUSED(tag) /*unused*/) noexcept : mKeyVals(valPtr) , mInfo(infoPtr) { fastForward(); } template ::type> Iter& operator=(Iter const& other) noexcept { mKeyVals = other.mKeyVals; mInfo = other.mInfo; return *this; } // prefix increment. Undefined behavior if we are at end()! Iter& operator++() noexcept { mInfo++; mKeyVals++; fastForward(); return *this; } Iter operator++(int) noexcept { Iter tmp = *this; ++(*this); return tmp; } reference operator*() const { return **mKeyVals; } pointer operator->() const { return &**mKeyVals; } template bool operator==(Iter const& o) const noexcept { return mKeyVals == o.mKeyVals; } template bool operator!=(Iter const& o) const noexcept { return mKeyVals != o.mKeyVals; } private: // fast forward to the next non-free info byte // I've tried a few variants that don't depend on intrinsics, but unfortunately they are // quite a bit slower than this one. So I've reverted that change again. See map_benchmark. void fastForward() noexcept { size_t n = 0; while (0U == (n = detail::unaligned_load(mInfo))) { mInfo += sizeof(size_t); mKeyVals += sizeof(size_t); } #if defined(ROBIN_HOOD_DISABLE_INTRINSICS) // we know for certain that within the next 8 bytes we'll find a non-zero one. if (ROBIN_HOOD_UNLIKELY(0U == detail::unaligned_load(mInfo))) { mInfo += 4; mKeyVals += 4; } if (ROBIN_HOOD_UNLIKELY(0U == detail::unaligned_load(mInfo))) { mInfo += 2; mKeyVals += 2; } if (ROBIN_HOOD_UNLIKELY(0U == *mInfo)) { mInfo += 1; mKeyVals += 1; } #else # if ROBIN_HOOD(LITTLE_ENDIAN) auto inc = ROBIN_HOOD_COUNT_TRAILING_ZEROES(n) / 8; # else auto inc = ROBIN_HOOD_COUNT_LEADING_ZEROES(n) / 8; # endif mInfo += inc; mKeyVals += inc; #endif } friend class Table; NodePtr mKeyVals{nullptr}; uint8_t const* mInfo{nullptr}; }; //////////////////////////////////////////////////////////////////// // highly performance relevant code. // Lower bits are used for indexing into the array (2^n size) // The upper 1-5 bits need to be a reasonable good hash, to save comparisons. template void keyToIdx(HashKey&& key, size_t* idx, InfoType* info) const { // In addition to whatever hash is used, add another mul & shift so we get better hashing. // This serves as a bad hash prevention, if the given data is // badly mixed. auto h = static_cast(WHash::operator()(key)); h *= mHashMultiplier; h ^= h >> 33U; // the lower InitialInfoNumBits are reserved for info. *info = mInfoInc + static_cast((h & InfoMask) >> mInfoHashShift); *idx = (static_cast(h) >> InitialInfoNumBits) & mMask; } // forwards the index by one, wrapping around at the end void next(InfoType* info, size_t* idx) const noexcept { *idx = *idx + 1; *info += mInfoInc; } void nextWhileLess(InfoType* info, size_t* idx) const noexcept { // unrolling this by hand did not bring any speedups. while (*info < mInfo[*idx]) { next(info, idx); } } // Shift everything up by one element. Tries to move stuff around. void shiftUp(size_t startIdx, size_t const insertion_idx) noexcept(std::is_nothrow_move_assignable::value) { auto idx = startIdx; ::new (static_cast(mKeyVals + idx)) Node(std::move(mKeyVals[idx - 1])); while (--idx != insertion_idx) { mKeyVals[idx] = std::move(mKeyVals[idx - 1]); } idx = startIdx; while (idx != insertion_idx) { ROBIN_HOOD_COUNT(shiftUp) mInfo[idx] = static_cast(mInfo[idx - 1] + mInfoInc); if (ROBIN_HOOD_UNLIKELY(mInfo[idx] + mInfoInc > 0xFF)) { mMaxNumElementsAllowed = 0; } --idx; } } void shiftDown(size_t idx) noexcept(std::is_nothrow_move_assignable::value) { // until we find one that is either empty or has zero offset. // TODO(martinus) we don't need to move everything, just the last one for the same // bucket. mKeyVals[idx].destroy(*this); // until we find one that is either empty or has zero offset. while (mInfo[idx + 1] >= 2 * mInfoInc) { ROBIN_HOOD_COUNT(shiftDown) mInfo[idx] = static_cast(mInfo[idx + 1] - mInfoInc); mKeyVals[idx] = std::move(mKeyVals[idx + 1]); ++idx; } mInfo[idx] = 0; // don't destroy, we've moved it // mKeyVals[idx].destroy(*this); mKeyVals[idx].~Node(); } // copy of find(), except that it returns iterator instead of const_iterator. template ROBIN_HOOD(NODISCARD) size_t findIdx(Other const& key) const { size_t idx{}; InfoType info{}; keyToIdx(key, &idx, &info); do { // unrolling this twice gives a bit of a speedup. More unrolling did not help. if (info == mInfo[idx] && ROBIN_HOOD_LIKELY(WKeyEqual::operator()(key, mKeyVals[idx].getFirst()))) { return idx; } next(&info, &idx); if (info == mInfo[idx] && ROBIN_HOOD_LIKELY(WKeyEqual::operator()(key, mKeyVals[idx].getFirst()))) { return idx; } next(&info, &idx); } while (info <= mInfo[idx]); // nothing found! return mMask == 0 ? 0 : static_cast(std::distance( mKeyVals, reinterpret_cast_no_cast_align_warning(mInfo))); } void cloneData(const Table& o) { Cloner()(o, *this); } // inserts a keyval that is guaranteed to be new, e.g. when the hashmap is resized. // @return True on success, false if something went wrong void insert_move(Node&& keyval) { // we don't retry, fail if overflowing // don't need to check max num elements if (0 == mMaxNumElementsAllowed && !try_increase_info()) { throwOverflowError(); } size_t idx{}; InfoType info{}; keyToIdx(keyval.getFirst(), &idx, &info); // skip forward. Use <= because we are certain that the element is not there. while (info <= mInfo[idx]) { idx = idx + 1; info += mInfoInc; } // key not found, so we are now exactly where we want to insert it. auto const insertion_idx = idx; auto const insertion_info = static_cast(info); if (ROBIN_HOOD_UNLIKELY(insertion_info + mInfoInc > 0xFF)) { mMaxNumElementsAllowed = 0; } // find an empty spot while (0 != mInfo[idx]) { next(&info, &idx); } auto& l = mKeyVals[insertion_idx]; if (idx == insertion_idx) { ::new (static_cast(&l)) Node(std::move(keyval)); } else { shiftUp(idx, insertion_idx); l = std::move(keyval); } // put at empty spot mInfo[insertion_idx] = insertion_info; ++mNumElements; } public: using iterator = Iter; using const_iterator = Iter; Table() noexcept(noexcept(Hash()) && noexcept(KeyEqual())) : WHash() , WKeyEqual() { ROBIN_HOOD_TRACE(this) } // Creates an empty hash map. Nothing is allocated yet, this happens at the first insert. // This tremendously speeds up ctor & dtor of a map that never receives an element. The // penalty is payed at the first insert, and not before. Lookup of this empty map works // because everybody points to DummyInfoByte::b. parameter bucket_count is dictated by the // standard, but we can ignore it. explicit Table( size_t ROBIN_HOOD_UNUSED(bucket_count) /*unused*/, const Hash& h = Hash{}, const KeyEqual& equal = KeyEqual{}) noexcept(noexcept(Hash(h)) && noexcept(KeyEqual(equal))) : WHash(h) , WKeyEqual(equal) { ROBIN_HOOD_TRACE(this) } template Table(Iter first, Iter last, size_t ROBIN_HOOD_UNUSED(bucket_count) /*unused*/ = 0, const Hash& h = Hash{}, const KeyEqual& equal = KeyEqual{}) : WHash(h) , WKeyEqual(equal) { ROBIN_HOOD_TRACE(this) insert(first, last); } Table(std::initializer_list initlist, size_t ROBIN_HOOD_UNUSED(bucket_count) /*unused*/ = 0, const Hash& h = Hash{}, const KeyEqual& equal = KeyEqual{}) : WHash(h) , WKeyEqual(equal) { ROBIN_HOOD_TRACE(this) insert(initlist.begin(), initlist.end()); } Table(Table&& o) noexcept : WHash(std::move(static_cast(o))) , WKeyEqual(std::move(static_cast(o))) , DataPool(std::move(static_cast(o))) { ROBIN_HOOD_TRACE(this) if (o.mMask) { mHashMultiplier = std::move(o.mHashMultiplier); mKeyVals = std::move(o.mKeyVals); mInfo = std::move(o.mInfo); mNumElements = std::move(o.mNumElements); mMask = std::move(o.mMask); mMaxNumElementsAllowed = std::move(o.mMaxNumElementsAllowed); mInfoInc = std::move(o.mInfoInc); mInfoHashShift = std::move(o.mInfoHashShift); // set other's mask to 0 so its destructor won't do anything o.init(); } } Table& operator=(Table&& o) noexcept { ROBIN_HOOD_TRACE(this) if (&o != this) { if (o.mMask) { // only move stuff if the other map actually has some data destroy(); mHashMultiplier = std::move(o.mHashMultiplier); mKeyVals = std::move(o.mKeyVals); mInfo = std::move(o.mInfo); mNumElements = std::move(o.mNumElements); mMask = std::move(o.mMask); mMaxNumElementsAllowed = std::move(o.mMaxNumElementsAllowed); mInfoInc = std::move(o.mInfoInc); mInfoHashShift = std::move(o.mInfoHashShift); WHash::operator=(std::move(static_cast(o))); WKeyEqual::operator=(std::move(static_cast(o))); DataPool::operator=(std::move(static_cast(o))); o.init(); } else { // nothing in the other map => just clear us. clear(); } } return *this; } Table(const Table& o) : WHash(static_cast(o)) , WKeyEqual(static_cast(o)) , DataPool(static_cast(o)) { ROBIN_HOOD_TRACE(this) if (!o.empty()) { // not empty: create an exact copy. it is also possible to just iterate through all // elements and insert them, but copying is probably faster. auto const numElementsWithBuffer = calcNumElementsWithBuffer(o.mMask + 1); auto const numBytesTotal = calcNumBytesTotal(numElementsWithBuffer); ROBIN_HOOD_LOG("std::malloc " << numBytesTotal << " = calcNumBytesTotal(" << numElementsWithBuffer << ")") mHashMultiplier = o.mHashMultiplier; mKeyVals = static_cast( detail::assertNotNull(std::malloc(numBytesTotal))); // no need for calloc because clonData does memcpy mInfo = reinterpret_cast(mKeyVals + numElementsWithBuffer); mNumElements = o.mNumElements; mMask = o.mMask; mMaxNumElementsAllowed = o.mMaxNumElementsAllowed; mInfoInc = o.mInfoInc; mInfoHashShift = o.mInfoHashShift; cloneData(o); } } // Creates a copy of the given map. Copy constructor of each entry is used. // Not sure why clang-tidy thinks this doesn't handle self assignment, it does // NOLINTNEXTLINE(bugprone-unhandled-self-assignment,cert-oop54-cpp) Table& operator=(Table const& o) { ROBIN_HOOD_TRACE(this) if (&o == this) { // prevent assigning of itself return *this; } // we keep using the old allocator and not assign the new one, because we want to keep // the memory available. when it is the same size. if (o.empty()) { if (0 == mMask) { // nothing to do, we are empty too return *this; } // not empty: destroy what we have there // clear also resets mInfo to 0, that's sometimes not necessary. destroy(); init(); WHash::operator=(static_cast(o)); WKeyEqual::operator=(static_cast(o)); DataPool::operator=(static_cast(o)); return *this; } // clean up old stuff Destroyer::value>{}.nodes(*this); if (mMask != o.mMask) { // no luck: we don't have the same array size allocated, so we need to realloc. if (0 != mMask) { // only deallocate if we actually have data! ROBIN_HOOD_LOG("std::free") std::free(mKeyVals); } auto const numElementsWithBuffer = calcNumElementsWithBuffer(o.mMask + 1); auto const numBytesTotal = calcNumBytesTotal(numElementsWithBuffer); ROBIN_HOOD_LOG("std::malloc " << numBytesTotal << " = calcNumBytesTotal(" << numElementsWithBuffer << ")") mKeyVals = static_cast( detail::assertNotNull(std::malloc(numBytesTotal))); // no need for calloc here because cloneData performs a memcpy. mInfo = reinterpret_cast(mKeyVals + numElementsWithBuffer); // sentinel is set in cloneData } WHash::operator=(static_cast(o)); WKeyEqual::operator=(static_cast(o)); DataPool::operator=(static_cast(o)); mHashMultiplier = o.mHashMultiplier; mNumElements = o.mNumElements; mMask = o.mMask; mMaxNumElementsAllowed = o.mMaxNumElementsAllowed; mInfoInc = o.mInfoInc; mInfoHashShift = o.mInfoHashShift; cloneData(o); return *this; } // Swaps everything between the two maps. void swap(Table& o) { ROBIN_HOOD_TRACE(this) using std::swap; swap(o, *this); } // Clears all data, without resizing. void clear() { ROBIN_HOOD_TRACE(this) if (empty()) { // don't do anything! also important because we don't want to write to // DummyInfoByte::b, even though we would just write 0 to it. return; } Destroyer::value>{}.nodes(*this); auto const numElementsWithBuffer = calcNumElementsWithBuffer(mMask + 1); // clear everything, then set the sentinel again uint8_t const z = 0; std::fill(mInfo, mInfo + calcNumBytesInfo(numElementsWithBuffer), z); mInfo[numElementsWithBuffer] = 1; mInfoInc = InitialInfoInc; mInfoHashShift = InitialInfoHashShift; } // Destroys the map and all it's contents. ~Table() { ROBIN_HOOD_TRACE(this) destroy(); } // Checks if both tables contain the same entries. Order is irrelevant. bool operator==(const Table& other) const { ROBIN_HOOD_TRACE(this) if (other.size() != size()) { return false; } for (auto const& otherEntry : other) { if (!has(otherEntry)) { return false; } } return true; } bool operator!=(const Table& other) const { ROBIN_HOOD_TRACE(this) return !operator==(other); } template typename std::enable_if::value, Q&>::type operator[](const key_type& key) { ROBIN_HOOD_TRACE(this) auto idxAndState = insertKeyPrepareEmptySpot(key); switch (idxAndState.second) { case InsertionState::key_found: break; case InsertionState::new_node: ::new (static_cast(&mKeyVals[idxAndState.first])) Node(*this, std::piecewise_construct, std::forward_as_tuple(key), std::forward_as_tuple()); break; case InsertionState::overwrite_node: mKeyVals[idxAndState.first] = Node(*this, std::piecewise_construct, std::forward_as_tuple(key), std::forward_as_tuple()); break; case InsertionState::overflow_error: throwOverflowError(); } return mKeyVals[idxAndState.first].getSecond(); } template typename std::enable_if::value, Q&>::type operator[](key_type&& key) { ROBIN_HOOD_TRACE(this) auto idxAndState = insertKeyPrepareEmptySpot(key); switch (idxAndState.second) { case InsertionState::key_found: break; case InsertionState::new_node: ::new (static_cast(&mKeyVals[idxAndState.first])) Node(*this, std::piecewise_construct, std::forward_as_tuple(std::move(key)), std::forward_as_tuple()); break; case InsertionState::overwrite_node: mKeyVals[idxAndState.first] = Node(*this, std::piecewise_construct, std::forward_as_tuple(std::move(key)), std::forward_as_tuple()); break; case InsertionState::overflow_error: throwOverflowError(); } return mKeyVals[idxAndState.first].getSecond(); } template void insert(Iter first, Iter last) { for (; first != last; ++first) { // value_type ctor needed because this might be called with std::pair's insert(value_type(*first)); } } void insert(std::initializer_list ilist) { for (auto&& vt : ilist) { insert(std::move(vt)); } } template std::pair emplace(Args&&... args) { ROBIN_HOOD_TRACE(this) Node n{*this, std::forward(args)...}; auto idxAndState = insertKeyPrepareEmptySpot(getFirstConst(n)); switch (idxAndState.second) { case InsertionState::key_found: n.destroy(*this); break; case InsertionState::new_node: ::new (static_cast(&mKeyVals[idxAndState.first])) Node(*this, std::move(n)); break; case InsertionState::overwrite_node: mKeyVals[idxAndState.first] = std::move(n); break; case InsertionState::overflow_error: n.destroy(*this); throwOverflowError(); break; } return std::make_pair(iterator(mKeyVals + idxAndState.first, mInfo + idxAndState.first), InsertionState::key_found != idxAndState.second); } template std::pair try_emplace(const key_type& key, Args&&... args) { return try_emplace_impl(key, std::forward(args)...); } template std::pair try_emplace(key_type&& key, Args&&... args) { return try_emplace_impl(std::move(key), std::forward(args)...); } template std::pair try_emplace(const_iterator hint, const key_type& key, Args&&... args) { (void)hint; return try_emplace_impl(key, std::forward(args)...); } template std::pair try_emplace(const_iterator hint, key_type&& key, Args&&... args) { (void)hint; return try_emplace_impl(std::move(key), std::forward(args)...); } template std::pair insert_or_assign(const key_type& key, Mapped&& obj) { return insertOrAssignImpl(key, std::forward(obj)); } template std::pair insert_or_assign(key_type&& key, Mapped&& obj) { return insertOrAssignImpl(std::move(key), std::forward(obj)); } template std::pair insert_or_assign(const_iterator hint, const key_type& key, Mapped&& obj) { (void)hint; return insertOrAssignImpl(key, std::forward(obj)); } template std::pair insert_or_assign(const_iterator hint, key_type&& key, Mapped&& obj) { (void)hint; return insertOrAssignImpl(std::move(key), std::forward(obj)); } std::pair insert(const value_type& keyval) { ROBIN_HOOD_TRACE(this) return emplace(keyval); } std::pair insert(value_type&& keyval) { return emplace(std::move(keyval)); } // Returns 1 if key is found, 0 otherwise. size_t count(const key_type& key) const { // NOLINT(modernize-use-nodiscard) ROBIN_HOOD_TRACE(this) auto kv = mKeyVals + findIdx(key); if (kv != reinterpret_cast_no_cast_align_warning(mInfo)) { return 1; } return 0; } template // NOLINTNEXTLINE(modernize-use-nodiscard) typename std::enable_if::type count(const OtherKey& key) const { ROBIN_HOOD_TRACE(this) auto kv = mKeyVals + findIdx(key); if (kv != reinterpret_cast_no_cast_align_warning(mInfo)) { return 1; } return 0; } bool contains(const key_type& key) const { // NOLINT(modernize-use-nodiscard) return 1U == count(key); } template // NOLINTNEXTLINE(modernize-use-nodiscard) typename std::enable_if::type contains(const OtherKey& key) const { return 1U == count(key); } // Returns a reference to the value found for key. // Throws std::out_of_range if element cannot be found template // NOLINTNEXTLINE(modernize-use-nodiscard) typename std::enable_if::value, Q&>::type at(key_type const& key) { ROBIN_HOOD_TRACE(this) auto kv = mKeyVals + findIdx(key); if (kv == reinterpret_cast_no_cast_align_warning(mInfo)) { doThrow("key not found"); } return kv->getSecond(); } // Returns a reference to the value found for key. // Throws std::out_of_range if element cannot be found template // NOLINTNEXTLINE(modernize-use-nodiscard) typename std::enable_if::value, Q const&>::type at(key_type const& key) const { ROBIN_HOOD_TRACE(this) auto kv = mKeyVals + findIdx(key); if (kv == reinterpret_cast_no_cast_align_warning(mInfo)) { doThrow("key not found"); } return kv->getSecond(); } const_iterator find(const key_type& key) const { // NOLINT(modernize-use-nodiscard) ROBIN_HOOD_TRACE(this) const size_t idx = findIdx(key); return const_iterator{mKeyVals + idx, mInfo + idx}; } template const_iterator find(const OtherKey& key, is_transparent_tag /*unused*/) const { ROBIN_HOOD_TRACE(this) const size_t idx = findIdx(key); return const_iterator{mKeyVals + idx, mInfo + idx}; } template typename std::enable_if::type // NOLINT(modernize-use-nodiscard) find(const OtherKey& key) const { // NOLINT(modernize-use-nodiscard) ROBIN_HOOD_TRACE(this) const size_t idx = findIdx(key); return const_iterator{mKeyVals + idx, mInfo + idx}; } iterator find(const key_type& key) { ROBIN_HOOD_TRACE(this) const size_t idx = findIdx(key); return iterator{mKeyVals + idx, mInfo + idx}; } template iterator find(const OtherKey& key, is_transparent_tag /*unused*/) { ROBIN_HOOD_TRACE(this) const size_t idx = findIdx(key); return iterator{mKeyVals + idx, mInfo + idx}; } template typename std::enable_if::type find(const OtherKey& key) { ROBIN_HOOD_TRACE(this) const size_t idx = findIdx(key); return iterator{mKeyVals + idx, mInfo + idx}; } iterator begin() { ROBIN_HOOD_TRACE(this) if (empty()) { return end(); } return iterator(mKeyVals, mInfo, fast_forward_tag{}); } const_iterator begin() const { // NOLINT(modernize-use-nodiscard) ROBIN_HOOD_TRACE(this) return cbegin(); } const_iterator cbegin() const { // NOLINT(modernize-use-nodiscard) ROBIN_HOOD_TRACE(this) if (empty()) { return cend(); } return const_iterator(mKeyVals, mInfo, fast_forward_tag{}); } iterator end() { ROBIN_HOOD_TRACE(this) // no need to supply valid info pointer: end() must not be dereferenced, and only node // pointer is compared. return iterator{reinterpret_cast_no_cast_align_warning(mInfo), nullptr}; } const_iterator end() const { // NOLINT(modernize-use-nodiscard) ROBIN_HOOD_TRACE(this) return cend(); } const_iterator cend() const { // NOLINT(modernize-use-nodiscard) ROBIN_HOOD_TRACE(this) return const_iterator{reinterpret_cast_no_cast_align_warning(mInfo), nullptr}; } iterator erase(const_iterator pos) { ROBIN_HOOD_TRACE(this) // its safe to perform const cast here // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast) return erase(iterator{const_cast(pos.mKeyVals), const_cast(pos.mInfo)}); } // Erases element at pos, returns iterator to the next element. iterator erase(iterator pos) { ROBIN_HOOD_TRACE(this) // we assume that pos always points to a valid entry, and not end(). auto const idx = static_cast(pos.mKeyVals - mKeyVals); shiftDown(idx); --mNumElements; if (*pos.mInfo) { // we've backward shifted, return this again return pos; } // no backward shift, return next element return ++pos; } size_t erase(const key_type& key) { ROBIN_HOOD_TRACE(this) size_t idx{}; InfoType info{}; keyToIdx(key, &idx, &info); // check while info matches with the source idx do { if (info == mInfo[idx] && WKeyEqual::operator()(key, mKeyVals[idx].getFirst())) { shiftDown(idx); --mNumElements; return 1; } next(&info, &idx); } while (info <= mInfo[idx]); // nothing found to delete return 0; } // reserves space for the specified number of elements. Makes sure the old data fits. // exactly the same as reserve(c). void rehash(size_t c) { // forces a reserve reserve(c, true); } // reserves space for the specified number of elements. Makes sure the old data fits. // Exactly the same as rehash(c). Use rehash(0) to shrink to fit. void reserve(size_t c) { // reserve, but don't force rehash reserve(c, false); } // If possible reallocates the map to a smaller one. This frees the underlying table. // Does not do anything if load_factor is too large for decreasing the table's size. void compact() { ROBIN_HOOD_TRACE(this) auto newSize = InitialNumElements; while (calcMaxNumElementsAllowed(newSize) < mNumElements && newSize != 0) { newSize *= 2; } if (ROBIN_HOOD_UNLIKELY(newSize == 0)) { throwOverflowError(); } ROBIN_HOOD_LOG("newSize > mMask + 1: " << newSize << " > " << mMask << " + 1") // only actually do anything when the new size is bigger than the old one. This prevents to // continuously allocate for each reserve() call. if (newSize < mMask + 1) { rehashPowerOfTwo(newSize, true); } } size_type size() const noexcept { // NOLINT(modernize-use-nodiscard) ROBIN_HOOD_TRACE(this) return mNumElements; } size_type max_size() const noexcept { // NOLINT(modernize-use-nodiscard) ROBIN_HOOD_TRACE(this) return static_cast(-1); } ROBIN_HOOD(NODISCARD) bool empty() const noexcept { ROBIN_HOOD_TRACE(this) return 0 == mNumElements; } float max_load_factor() const noexcept { // NOLINT(modernize-use-nodiscard) ROBIN_HOOD_TRACE(this) return MaxLoadFactor100 / 100.0F; } // Average number of elements per bucket. Since we allow only 1 per bucket float load_factor() const noexcept { // NOLINT(modernize-use-nodiscard) ROBIN_HOOD_TRACE(this) return static_cast(size()) / static_cast(mMask + 1); } ROBIN_HOOD(NODISCARD) size_t mask() const noexcept { ROBIN_HOOD_TRACE(this) return mMask; } ROBIN_HOOD(NODISCARD) size_t calcMaxNumElementsAllowed(size_t maxElements) const noexcept { if (ROBIN_HOOD_LIKELY(maxElements <= (std::numeric_limits::max)() / 100)) { return maxElements * MaxLoadFactor100 / 100; } // we might be a bit inprecise, but since maxElements is quite large that doesn't matter return (maxElements / 100) * MaxLoadFactor100; } ROBIN_HOOD(NODISCARD) size_t calcNumBytesInfo(size_t numElements) const noexcept { // we add a uint64_t, which houses the sentinel (first byte) and padding so we can load // 64bit types. return numElements + sizeof(uint64_t); } ROBIN_HOOD(NODISCARD) size_t calcNumElementsWithBuffer(size_t numElements) const noexcept { auto maxNumElementsAllowed = calcMaxNumElementsAllowed(numElements); return numElements + (std::min)(maxNumElementsAllowed, (static_cast(0xFF))); } // calculation only allowed for 2^n values ROBIN_HOOD(NODISCARD) size_t calcNumBytesTotal(size_t numElements) const { #if ROBIN_HOOD(BITNESS) == 64 return numElements * sizeof(Node) + calcNumBytesInfo(numElements); #else // make sure we're doing 64bit operations, so we are at least safe against 32bit overflows. auto const ne = static_cast(numElements); auto const s = static_cast(sizeof(Node)); auto const infos = static_cast(calcNumBytesInfo(numElements)); auto const total64 = ne * s + infos; auto const total = static_cast(total64); if (ROBIN_HOOD_UNLIKELY(static_cast(total) != total64)) { throwOverflowError(); } return total; #endif } private: template ROBIN_HOOD(NODISCARD) typename std::enable_if::value, bool>::type has(const value_type& e) const { ROBIN_HOOD_TRACE(this) auto it = find(e.first); return it != end() && it->second == e.second; } template ROBIN_HOOD(NODISCARD) typename std::enable_if::value, bool>::type has(const value_type& e) const { ROBIN_HOOD_TRACE(this) return find(e) != end(); } void reserve(size_t c, bool forceRehash) { ROBIN_HOOD_TRACE(this) auto const minElementsAllowed = (std::max)(c, mNumElements); auto newSize = InitialNumElements; while (calcMaxNumElementsAllowed(newSize) < minElementsAllowed && newSize != 0) { newSize *= 2; } if (ROBIN_HOOD_UNLIKELY(newSize == 0)) { throwOverflowError(); } ROBIN_HOOD_LOG("newSize > mMask + 1: " << newSize << " > " << mMask << " + 1") // only actually do anything when the new size is bigger than the old one. This prevents to // continuously allocate for each reserve() call. if (forceRehash || newSize > mMask + 1) { rehashPowerOfTwo(newSize, false); } } // reserves space for at least the specified number of elements. // only works if numBuckets if power of two // True on success, false otherwise void rehashPowerOfTwo(size_t numBuckets, bool forceFree) { ROBIN_HOOD_TRACE(this) Node* const oldKeyVals = mKeyVals; uint8_t const* const oldInfo = mInfo; const size_t oldMaxElementsWithBuffer = calcNumElementsWithBuffer(mMask + 1); // resize operation: move stuff initData(numBuckets); if (oldMaxElementsWithBuffer > 1) { for (size_t i = 0; i < oldMaxElementsWithBuffer; ++i) { if (oldInfo[i] != 0) { // might throw an exception, which is really bad since we are in the middle of // moving stuff. insert_move(std::move(oldKeyVals[i])); // destroy the node but DON'T destroy the data. oldKeyVals[i].~Node(); } } // this check is not necessary as it's guarded by the previous if, but it helps // silence g++'s overeager "attempt to free a non-heap object 'map' // [-Werror=free-nonheap-object]" warning. if (oldKeyVals != reinterpret_cast_no_cast_align_warning(&mMask)) { // don't destroy old data: put it into the pool instead if (forceFree) { std::free(oldKeyVals); } else { DataPool::addOrFree(oldKeyVals, calcNumBytesTotal(oldMaxElementsWithBuffer)); } } } } ROBIN_HOOD(NOINLINE) void throwOverflowError() const { #if ROBIN_HOOD(HAS_EXCEPTIONS) throw std::overflow_error("robin_hood::map overflow"); #else abort(); #endif } template std::pair try_emplace_impl(OtherKey&& key, Args&&... args) { ROBIN_HOOD_TRACE(this) auto idxAndState = insertKeyPrepareEmptySpot(key); switch (idxAndState.second) { case InsertionState::key_found: break; case InsertionState::new_node: ::new (static_cast(&mKeyVals[idxAndState.first])) Node( *this, std::piecewise_construct, std::forward_as_tuple(std::forward(key)), std::forward_as_tuple(std::forward(args)...)); break; case InsertionState::overwrite_node: mKeyVals[idxAndState.first] = Node(*this, std::piecewise_construct, std::forward_as_tuple(std::forward(key)), std::forward_as_tuple(std::forward(args)...)); break; case InsertionState::overflow_error: throwOverflowError(); break; } return std::make_pair(iterator(mKeyVals + idxAndState.first, mInfo + idxAndState.first), InsertionState::key_found != idxAndState.second); } template std::pair insertOrAssignImpl(OtherKey&& key, Mapped&& obj) { ROBIN_HOOD_TRACE(this) auto idxAndState = insertKeyPrepareEmptySpot(key); switch (idxAndState.second) { case InsertionState::key_found: mKeyVals[idxAndState.first].getSecond() = std::forward(obj); break; case InsertionState::new_node: ::new (static_cast(&mKeyVals[idxAndState.first])) Node( *this, std::piecewise_construct, std::forward_as_tuple(std::forward(key)), std::forward_as_tuple(std::forward(obj))); break; case InsertionState::overwrite_node: mKeyVals[idxAndState.first] = Node(*this, std::piecewise_construct, std::forward_as_tuple(std::forward(key)), std::forward_as_tuple(std::forward(obj))); break; case InsertionState::overflow_error: throwOverflowError(); break; } return std::make_pair(iterator(mKeyVals + idxAndState.first, mInfo + idxAndState.first), InsertionState::key_found != idxAndState.second); } void initData(size_t max_elements) { mNumElements = 0; mMask = max_elements - 1; mMaxNumElementsAllowed = calcMaxNumElementsAllowed(max_elements); auto const numElementsWithBuffer = calcNumElementsWithBuffer(max_elements); // calloc also zeroes everything auto const numBytesTotal = calcNumBytesTotal(numElementsWithBuffer); ROBIN_HOOD_LOG("std::calloc " << numBytesTotal << " = calcNumBytesTotal(" << numElementsWithBuffer << ")") mKeyVals = reinterpret_cast( detail::assertNotNull(std::calloc(1, numBytesTotal))); mInfo = reinterpret_cast(mKeyVals + numElementsWithBuffer); // set sentinel mInfo[numElementsWithBuffer] = 1; mInfoInc = InitialInfoInc; mInfoHashShift = InitialInfoHashShift; } enum class InsertionState { overflow_error, key_found, new_node, overwrite_node }; // Finds key, and if not already present prepares a spot where to pot the key & value. // This potentially shifts nodes out of the way, updates mInfo and number of inserted // elements, so the only operation left to do is create/assign a new node at that spot. template std::pair insertKeyPrepareEmptySpot(OtherKey&& key) { for (int i = 0; i < 256; ++i) { size_t idx{}; InfoType info{}; keyToIdx(key, &idx, &info); nextWhileLess(&info, &idx); // while we potentially have a match while (info == mInfo[idx]) { if (WKeyEqual::operator()(key, mKeyVals[idx].getFirst())) { // key already exists, do NOT insert. // see http://en.cppreference.com/w/cpp/container/unordered_map/insert return std::make_pair(idx, InsertionState::key_found); } next(&info, &idx); } // unlikely that this evaluates to true if (ROBIN_HOOD_UNLIKELY(mNumElements >= mMaxNumElementsAllowed)) { if (!increase_size()) { return std::make_pair(size_t(0), InsertionState::overflow_error); } continue; } // key not found, so we are now exactly where we want to insert it. auto const insertion_idx = idx; auto const insertion_info = info; if (ROBIN_HOOD_UNLIKELY(insertion_info + mInfoInc > 0xFF)) { mMaxNumElementsAllowed = 0; } // find an empty spot while (0 != mInfo[idx]) { next(&info, &idx); } if (idx != insertion_idx) { shiftUp(idx, insertion_idx); } // put at empty spot mInfo[insertion_idx] = static_cast(insertion_info); ++mNumElements; return std::make_pair(insertion_idx, idx == insertion_idx ? InsertionState::new_node : InsertionState::overwrite_node); } // enough attempts failed, so finally give up. return std::make_pair(size_t(0), InsertionState::overflow_error); } bool try_increase_info() { ROBIN_HOOD_LOG("mInfoInc=" << mInfoInc << ", numElements=" << mNumElements << ", maxNumElementsAllowed=" << calcMaxNumElementsAllowed(mMask + 1)) if (mInfoInc <= 2) { // need to be > 2 so that shift works (otherwise undefined behavior!) return false; } // we got space left, try to make info smaller mInfoInc = static_cast(mInfoInc >> 1U); // remove one bit of the hash, leaving more space for the distance info. // This is extremely fast because we can operate on 8 bytes at once. ++mInfoHashShift; auto const numElementsWithBuffer = calcNumElementsWithBuffer(mMask + 1); for (size_t i = 0; i < numElementsWithBuffer; i += 8) { auto val = unaligned_load(mInfo + i); val = (val >> 1U) & UINT64_C(0x7f7f7f7f7f7f7f7f); std::memcpy(mInfo + i, &val, sizeof(val)); } // update sentinel, which might have been cleared out! mInfo[numElementsWithBuffer] = 1; mMaxNumElementsAllowed = calcMaxNumElementsAllowed(mMask + 1); return true; } // True if resize was possible, false otherwise bool increase_size() { // nothing allocated yet? just allocate InitialNumElements if (0 == mMask) { initData(InitialNumElements); return true; } auto const maxNumElementsAllowed = calcMaxNumElementsAllowed(mMask + 1); if (mNumElements < maxNumElementsAllowed && try_increase_info()) { return true; } ROBIN_HOOD_LOG("mNumElements=" << mNumElements << ", maxNumElementsAllowed=" << maxNumElementsAllowed << ", load=" << (static_cast(mNumElements) * 100.0 / (static_cast(mMask) + 1))) if (mNumElements * 2 < calcMaxNumElementsAllowed(mMask + 1)) { // we have to resize, even though there would still be plenty of space left! // Try to rehash instead. Delete freed memory so we don't steadyily increase mem in case // we have to rehash a few times nextHashMultiplier(); rehashPowerOfTwo(mMask + 1, true); } else { // we've reached the capacity of the map, so the hash seems to work nice. Keep using it. rehashPowerOfTwo((mMask + 1) * 2, false); } return true; } void nextHashMultiplier() { // adding an *even* number, so that the multiplier will always stay odd. This is necessary // so that the hash stays a mixing function (and thus doesn't have any information loss). mHashMultiplier += UINT64_C(0xc4ceb9fe1a85ec54); } void destroy() { if (0 == mMask) { // don't deallocate! return; } Destroyer::value>{} .nodesDoNotDeallocate(*this); // This protection against not deleting mMask shouldn't be needed as it's sufficiently // protected with the 0==mMask check, but I have this anyways because g++ 7 otherwise // reports a compile error: attempt to free a non-heap object 'fm' // [-Werror=free-nonheap-object] if (mKeyVals != reinterpret_cast_no_cast_align_warning(&mMask)) { ROBIN_HOOD_LOG("std::free") std::free(mKeyVals); } } void init() noexcept { mKeyVals = reinterpret_cast_no_cast_align_warning(&mMask); mInfo = reinterpret_cast(&mMask); mNumElements = 0; mMask = 0; mMaxNumElementsAllowed = 0; mInfoInc = InitialInfoInc; mInfoHashShift = InitialInfoHashShift; } // members are sorted so no padding occurs uint64_t mHashMultiplier = UINT64_C(0xc4ceb9fe1a85ec53); // 8 byte 8 Node* mKeyVals = reinterpret_cast_no_cast_align_warning(&mMask); // 8 byte 16 uint8_t* mInfo = reinterpret_cast(&mMask); // 8 byte 24 size_t mNumElements = 0; // 8 byte 32 size_t mMask = 0; // 8 byte 40 size_t mMaxNumElementsAllowed = 0; // 8 byte 48 InfoType mInfoInc = InitialInfoInc; // 4 byte 52 InfoType mInfoHashShift = InitialInfoHashShift; // 4 byte 56 // 16 byte 56 if NodeAllocator }; } // namespace detail // map template , typename KeyEqual = std::equal_to, size_t MaxLoadFactor100 = 80> using unordered_flat_map = detail::Table; template , typename KeyEqual = std::equal_to, size_t MaxLoadFactor100 = 80> using unordered_node_map = detail::Table; template , typename KeyEqual = std::equal_to, size_t MaxLoadFactor100 = 80> using unordered_map = detail::Table) <= sizeof(size_t) * 6 && std::is_nothrow_move_constructible>::value && std::is_nothrow_move_assignable>::value, MaxLoadFactor100, Key, T, Hash, KeyEqual>; // set template , typename KeyEqual = std::equal_to, size_t MaxLoadFactor100 = 80> using unordered_flat_set = detail::Table; template , typename KeyEqual = std::equal_to, size_t MaxLoadFactor100 = 80> using unordered_node_set = detail::Table; template , typename KeyEqual = std::equal_to, size_t MaxLoadFactor100 = 80> using unordered_set = detail::Table::value && std::is_nothrow_move_assignable::value, MaxLoadFactor100, Key, void, Hash, KeyEqual>; } // namespace robin_hood #endif btop-1.2.3/include/widechar_width.hpp000066400000000000000000001073611420276253000176130ustar00rootroot00000000000000/** * widechar_width.h, generated on 2022-02-11. * See https://github.com/ridiculousfish/widecharwidth/ * * SHA1 file hashes: * UnicodeData.txt: 8a5c26bfb27df8cfab23cf2c34c62d8d3075ae4d * EastAsianWidth.txt: 8ec36ccac3852bf0c2f02e37c6151551cd14db72 * emoji-data.txt: 3f0ec08c001c4bc6df0b07d01068fc73808bfb4c */ #ifndef WIDECHAR_WIDTH_H #define WIDECHAR_WIDTH_H #include #include #include #include namespace utf8 { /* Special width values */ enum { widechar_nonprint = 0, // The character is not printable. widechar_combining = 0, // The character is a zero-width combiner. widechar_ambiguous = 1, // The character is East-Asian ambiguous width. widechar_private_use = 1, // The character is for private use. widechar_unassigned = 0, // The character is unassigned. widechar_widened_in_9 = 2, // Width is 1 in Unicode 8, 2 in Unicode 9+. widechar_non_character = 0 // The character is a noncharacter. }; /* An inclusive range of characters. */ struct widechar_range { uint32_t lo; uint32_t hi; }; /* Simple ASCII characters - used a lot, so we check them first. */ static const struct widechar_range widechar_ascii_table[] = { {0x00020, 0x0007E} }; /* Private usage range. */ static const struct widechar_range widechar_private_table[] = { {0x0E000, 0x0F8FF}, {0xF0000, 0xFFFFD}, {0x100000, 0x10FFFD} }; /* Nonprinting characters. */ static const struct widechar_range widechar_nonprint_table[] = { {0x00000, 0x0001F}, {0x0007F, 0x0009F}, {0x000AD, 0x000AD}, {0x00600, 0x00605}, {0x0061C, 0x0061C}, {0x006DD, 0x006DD}, {0x0070F, 0x0070F}, {0x00890, 0x00891}, {0x008E2, 0x008E2}, {0x0180E, 0x0180E}, {0x0200B, 0x0200F}, {0x02028, 0x0202E}, {0x02060, 0x02064}, {0x02066, 0x0206F}, {0x0D800, 0x0DFFF}, {0x0FEFF, 0x0FEFF}, {0x0FFF9, 0x0FFFB}, {0x110BD, 0x110BD}, {0x110CD, 0x110CD}, {0x13430, 0x13438}, {0x1BCA0, 0x1BCA3}, {0x1D173, 0x1D17A}, {0xE0001, 0xE0001}, {0xE0020, 0xE007F} }; /* Width 0 combining marks. */ static const struct widechar_range widechar_combining_table[] = { {0x00300, 0x0036F}, {0x00483, 0x00489}, {0x00591, 0x005BD}, {0x005BF, 0x005BF}, {0x005C1, 0x005C2}, {0x005C4, 0x005C5}, {0x005C7, 0x005C7}, {0x00610, 0x0061A}, {0x0064B, 0x0065F}, {0x00670, 0x00670}, {0x006D6, 0x006DC}, {0x006DF, 0x006E4}, {0x006E7, 0x006E8}, {0x006EA, 0x006ED}, {0x00711, 0x00711}, {0x00730, 0x0074A}, {0x007A6, 0x007B0}, {0x007EB, 0x007F3}, {0x007FD, 0x007FD}, {0x00816, 0x00819}, {0x0081B, 0x00823}, {0x00825, 0x00827}, {0x00829, 0x0082D}, {0x00859, 0x0085B}, {0x00898, 0x0089F}, {0x008CA, 0x008E1}, {0x008E3, 0x00903}, {0x0093A, 0x0093C}, {0x0093E, 0x0094F}, {0x00951, 0x00957}, {0x00962, 0x00963}, {0x00981, 0x00983}, {0x009BC, 0x009BC}, {0x009BE, 0x009C4}, {0x009C7, 0x009C8}, {0x009CB, 0x009CD}, {0x009D7, 0x009D7}, {0x009E2, 0x009E3}, {0x009FE, 0x009FE}, {0x00A01, 0x00A03}, {0x00A3C, 0x00A3C}, {0x00A3E, 0x00A42}, {0x00A47, 0x00A48}, {0x00A4B, 0x00A4D}, {0x00A51, 0x00A51}, {0x00A70, 0x00A71}, {0x00A75, 0x00A75}, {0x00A81, 0x00A83}, {0x00ABC, 0x00ABC}, {0x00ABE, 0x00AC5}, {0x00AC7, 0x00AC9}, {0x00ACB, 0x00ACD}, {0x00AE2, 0x00AE3}, {0x00AFA, 0x00AFF}, {0x00B01, 0x00B03}, {0x00B3C, 0x00B3C}, {0x00B3E, 0x00B44}, {0x00B47, 0x00B48}, {0x00B4B, 0x00B4D}, {0x00B55, 0x00B57}, {0x00B62, 0x00B63}, {0x00B82, 0x00B82}, {0x00BBE, 0x00BC2}, {0x00BC6, 0x00BC8}, {0x00BCA, 0x00BCD}, {0x00BD7, 0x00BD7}, {0x00C00, 0x00C04}, {0x00C3C, 0x00C3C}, {0x00C3E, 0x00C44}, {0x00C46, 0x00C48}, {0x00C4A, 0x00C4D}, {0x00C55, 0x00C56}, {0x00C62, 0x00C63}, {0x00C81, 0x00C83}, {0x00CBC, 0x00CBC}, {0x00CBE, 0x00CC4}, {0x00CC6, 0x00CC8}, {0x00CCA, 0x00CCD}, {0x00CD5, 0x00CD6}, {0x00CE2, 0x00CE3}, {0x00D00, 0x00D03}, {0x00D3B, 0x00D3C}, {0x00D3E, 0x00D44}, {0x00D46, 0x00D48}, {0x00D4A, 0x00D4D}, {0x00D57, 0x00D57}, {0x00D62, 0x00D63}, {0x00D81, 0x00D83}, {0x00DCA, 0x00DCA}, {0x00DCF, 0x00DD4}, {0x00DD6, 0x00DD6}, {0x00DD8, 0x00DDF}, {0x00DF2, 0x00DF3}, {0x00E31, 0x00E31}, {0x00E34, 0x00E3A}, {0x00E47, 0x00E4E}, {0x00EB1, 0x00EB1}, {0x00EB4, 0x00EBC}, {0x00EC8, 0x00ECD}, {0x00F18, 0x00F19}, {0x00F35, 0x00F35}, {0x00F37, 0x00F37}, {0x00F39, 0x00F39}, {0x00F3E, 0x00F3F}, {0x00F71, 0x00F84}, {0x00F86, 0x00F87}, {0x00F8D, 0x00F97}, {0x00F99, 0x00FBC}, {0x00FC6, 0x00FC6}, {0x0102B, 0x0103E}, {0x01056, 0x01059}, {0x0105E, 0x01060}, {0x01062, 0x01064}, {0x01067, 0x0106D}, {0x01071, 0x01074}, {0x01082, 0x0108D}, {0x0108F, 0x0108F}, {0x0109A, 0x0109D}, {0x0135D, 0x0135F}, {0x01712, 0x01715}, {0x01732, 0x01734}, {0x01752, 0x01753}, {0x01772, 0x01773}, {0x017B4, 0x017D3}, {0x017DD, 0x017DD}, {0x0180B, 0x0180D}, {0x0180F, 0x0180F}, {0x01885, 0x01886}, {0x018A9, 0x018A9}, {0x01920, 0x0192B}, {0x01930, 0x0193B}, {0x01A17, 0x01A1B}, {0x01A55, 0x01A5E}, {0x01A60, 0x01A7C}, {0x01A7F, 0x01A7F}, {0x01AB0, 0x01ACE}, {0x01B00, 0x01B04}, {0x01B34, 0x01B44}, {0x01B6B, 0x01B73}, {0x01B80, 0x01B82}, {0x01BA1, 0x01BAD}, {0x01BE6, 0x01BF3}, {0x01C24, 0x01C37}, {0x01CD0, 0x01CD2}, {0x01CD4, 0x01CE8}, {0x01CED, 0x01CED}, {0x01CF4, 0x01CF4}, {0x01CF7, 0x01CF9}, {0x01DC0, 0x01DFF}, {0x020D0, 0x020F0}, {0x02CEF, 0x02CF1}, {0x02D7F, 0x02D7F}, {0x02DE0, 0x02DFF}, {0x0302A, 0x0302F}, {0x03099, 0x0309A}, {0x0A66F, 0x0A672}, {0x0A674, 0x0A67D}, {0x0A69E, 0x0A69F}, {0x0A6F0, 0x0A6F1}, {0x0A802, 0x0A802}, {0x0A806, 0x0A806}, {0x0A80B, 0x0A80B}, {0x0A823, 0x0A827}, {0x0A82C, 0x0A82C}, {0x0A880, 0x0A881}, {0x0A8B4, 0x0A8C5}, {0x0A8E0, 0x0A8F1}, {0x0A8FF, 0x0A8FF}, {0x0A926, 0x0A92D}, {0x0A947, 0x0A953}, {0x0A980, 0x0A983}, {0x0A9B3, 0x0A9C0}, {0x0A9E5, 0x0A9E5}, {0x0AA29, 0x0AA36}, {0x0AA43, 0x0AA43}, {0x0AA4C, 0x0AA4D}, {0x0AA7B, 0x0AA7D}, {0x0AAB0, 0x0AAB0}, {0x0AAB2, 0x0AAB4}, {0x0AAB7, 0x0AAB8}, {0x0AABE, 0x0AABF}, {0x0AAC1, 0x0AAC1}, {0x0AAEB, 0x0AAEF}, {0x0AAF5, 0x0AAF6}, {0x0ABE3, 0x0ABEA}, {0x0ABEC, 0x0ABED}, {0x0FB1E, 0x0FB1E}, {0x0FE00, 0x0FE0F}, {0x0FE20, 0x0FE2F}, {0x101FD, 0x101FD}, {0x102E0, 0x102E0}, {0x10376, 0x1037A}, {0x10A01, 0x10A03}, {0x10A05, 0x10A06}, {0x10A0C, 0x10A0F}, {0x10A38, 0x10A3A}, {0x10A3F, 0x10A3F}, {0x10AE5, 0x10AE6}, {0x10D24, 0x10D27}, {0x10EAB, 0x10EAC}, {0x10F46, 0x10F50}, {0x10F82, 0x10F85}, {0x11000, 0x11002}, {0x11038, 0x11046}, {0x11070, 0x11070}, {0x11073, 0x11074}, {0x1107F, 0x11082}, {0x110B0, 0x110BA}, {0x110C2, 0x110C2}, {0x11100, 0x11102}, {0x11127, 0x11134}, {0x11145, 0x11146}, {0x11173, 0x11173}, {0x11180, 0x11182}, {0x111B3, 0x111C0}, {0x111C9, 0x111CC}, {0x111CE, 0x111CF}, {0x1122C, 0x11237}, {0x1123E, 0x1123E}, {0x112DF, 0x112EA}, {0x11300, 0x11303}, {0x1133B, 0x1133C}, {0x1133E, 0x11344}, {0x11347, 0x11348}, {0x1134B, 0x1134D}, {0x11357, 0x11357}, {0x11362, 0x11363}, {0x11366, 0x1136C}, {0x11370, 0x11374}, {0x11435, 0x11446}, {0x1145E, 0x1145E}, {0x114B0, 0x114C3}, {0x115AF, 0x115B5}, {0x115B8, 0x115C0}, {0x115DC, 0x115DD}, {0x11630, 0x11640}, {0x116AB, 0x116B7}, {0x1171D, 0x1172B}, {0x1182C, 0x1183A}, {0x11930, 0x11935}, {0x11937, 0x11938}, {0x1193B, 0x1193E}, {0x11940, 0x11940}, {0x11942, 0x11943}, {0x119D1, 0x119D7}, {0x119DA, 0x119E0}, {0x119E4, 0x119E4}, {0x11A01, 0x11A0A}, {0x11A33, 0x11A39}, {0x11A3B, 0x11A3E}, {0x11A47, 0x11A47}, {0x11A51, 0x11A5B}, {0x11A8A, 0x11A99}, {0x11C2F, 0x11C36}, {0x11C38, 0x11C3F}, {0x11C92, 0x11CA7}, {0x11CA9, 0x11CB6}, {0x11D31, 0x11D36}, {0x11D3A, 0x11D3A}, {0x11D3C, 0x11D3D}, {0x11D3F, 0x11D45}, {0x11D47, 0x11D47}, {0x11D8A, 0x11D8E}, {0x11D90, 0x11D91}, {0x11D93, 0x11D97}, {0x11EF3, 0x11EF6}, {0x16AF0, 0x16AF4}, {0x16B30, 0x16B36}, {0x16F4F, 0x16F4F}, {0x16F51, 0x16F87}, {0x16F8F, 0x16F92}, {0x16FE4, 0x16FE4}, {0x16FF0, 0x16FF1}, {0x1BC9D, 0x1BC9E}, {0x1CF00, 0x1CF2D}, {0x1CF30, 0x1CF46}, {0x1D165, 0x1D169}, {0x1D16D, 0x1D172}, {0x1D17B, 0x1D182}, {0x1D185, 0x1D18B}, {0x1D1AA, 0x1D1AD}, {0x1D242, 0x1D244}, {0x1DA00, 0x1DA36}, {0x1DA3B, 0x1DA6C}, {0x1DA75, 0x1DA75}, {0x1DA84, 0x1DA84}, {0x1DA9B, 0x1DA9F}, {0x1DAA1, 0x1DAAF}, {0x1E000, 0x1E006}, {0x1E008, 0x1E018}, {0x1E01B, 0x1E021}, {0x1E023, 0x1E024}, {0x1E026, 0x1E02A}, {0x1E130, 0x1E136}, {0x1E2AE, 0x1E2AE}, {0x1E2EC, 0x1E2EF}, {0x1E8D0, 0x1E8D6}, {0x1E944, 0x1E94A}, {0xE0100, 0xE01EF} }; /* Width 0 combining letters. */ static const struct widechar_range widechar_combiningletters_table[] = { {0x01160, 0x011FF}, {0x0D7B0, 0x0D7FF} }; /* Width 2 characters. */ static const struct widechar_range widechar_doublewide_table[] = { {0x01100, 0x0115F}, {0x02329, 0x0232A}, {0x02E80, 0x02E99}, {0x02E9B, 0x02EF3}, {0x02F00, 0x02FD5}, {0x02FF0, 0x02FFB}, {0x03000, 0x0303E}, {0x03041, 0x03096}, {0x03099, 0x030FF}, {0x03105, 0x0312F}, {0x03131, 0x0318E}, {0x03190, 0x031E3}, {0x031F0, 0x0321E}, {0x03220, 0x03247}, {0x03250, 0x04DBF}, {0x04E00, 0x0A48C}, {0x0A490, 0x0A4C6}, {0x0A960, 0x0A97C}, {0x0AC00, 0x0D7A3}, {0x0F900, 0x0FAFF}, {0x0FE10, 0x0FE19}, {0x0FE30, 0x0FE52}, {0x0FE54, 0x0FE66}, {0x0FE68, 0x0FE6B}, {0x0FF01, 0x0FF60}, {0x0FFE0, 0x0FFE6}, {0x16FE0, 0x16FE4}, {0x16FF0, 0x16FF1}, {0x17000, 0x187F7}, {0x18800, 0x18CD5}, {0x18D00, 0x18D08}, {0x1AFF0, 0x1AFF3}, {0x1AFF5, 0x1AFFB}, {0x1AFFD, 0x1AFFE}, {0x1B000, 0x1B122}, {0x1B150, 0x1B152}, {0x1B164, 0x1B167}, {0x1B170, 0x1B2FB}, {0x1F200, 0x1F200}, {0x1F202, 0x1F202}, {0x1F210, 0x1F219}, {0x1F21B, 0x1F22E}, {0x1F230, 0x1F231}, {0x1F237, 0x1F237}, {0x1F23B, 0x1F23B}, {0x1F240, 0x1F248}, {0x1F260, 0x1F265}, {0x1F57A, 0x1F57A}, {0x1F5A4, 0x1F5A4}, {0x1F6D1, 0x1F6D2}, {0x1F6D5, 0x1F6D7}, {0x1F6DD, 0x1F6DF}, {0x1F6F4, 0x1F6FC}, {0x1F7E0, 0x1F7EB}, {0x1F7F0, 0x1F7F0}, {0x1F90C, 0x1F90F}, {0x1F919, 0x1F93A}, {0x1F93C, 0x1F945}, {0x1F947, 0x1F97F}, {0x1F985, 0x1F9BF}, {0x1F9C1, 0x1F9FF}, {0x1FA70, 0x1FA74}, {0x1FA78, 0x1FA7C}, {0x1FA80, 0x1FA86}, {0x1FA90, 0x1FAAC}, {0x1FAB0, 0x1FABA}, {0x1FAC0, 0x1FAC5}, {0x1FAD0, 0x1FAD9}, {0x1FAE0, 0x1FAE7}, {0x1FAF0, 0x1FAF6}, {0x20000, 0x2FFFD}, {0x30000, 0x3FFFD} }; /* Ambiguous-width characters. */ static const struct widechar_range widechar_ambiguous_table[] = { {0x000A1, 0x000A1}, {0x000A4, 0x000A4}, {0x000A7, 0x000A8}, {0x000AA, 0x000AA}, {0x000AD, 0x000AE}, {0x000B0, 0x000B4}, {0x000B6, 0x000BA}, {0x000BC, 0x000BF}, {0x000C6, 0x000C6}, {0x000D0, 0x000D0}, {0x000D7, 0x000D8}, {0x000DE, 0x000E1}, {0x000E6, 0x000E6}, {0x000E8, 0x000EA}, {0x000EC, 0x000ED}, {0x000F0, 0x000F0}, {0x000F2, 0x000F3}, {0x000F7, 0x000FA}, {0x000FC, 0x000FC}, {0x000FE, 0x000FE}, {0x00101, 0x00101}, {0x00111, 0x00111}, {0x00113, 0x00113}, {0x0011B, 0x0011B}, {0x00126, 0x00127}, {0x0012B, 0x0012B}, {0x00131, 0x00133}, {0x00138, 0x00138}, {0x0013F, 0x00142}, {0x00144, 0x00144}, {0x00148, 0x0014B}, {0x0014D, 0x0014D}, {0x00152, 0x00153}, {0x00166, 0x00167}, {0x0016B, 0x0016B}, {0x001CE, 0x001CE}, {0x001D0, 0x001D0}, {0x001D2, 0x001D2}, {0x001D4, 0x001D4}, {0x001D6, 0x001D6}, {0x001D8, 0x001D8}, {0x001DA, 0x001DA}, {0x001DC, 0x001DC}, {0x00251, 0x00251}, {0x00261, 0x00261}, {0x002C4, 0x002C4}, {0x002C7, 0x002C7}, {0x002C9, 0x002CB}, {0x002CD, 0x002CD}, {0x002D0, 0x002D0}, {0x002D8, 0x002DB}, {0x002DD, 0x002DD}, {0x002DF, 0x002DF}, {0x00300, 0x0036F}, {0x00391, 0x003A1}, {0x003A3, 0x003A9}, {0x003B1, 0x003C1}, {0x003C3, 0x003C9}, {0x00401, 0x00401}, {0x00410, 0x0044F}, {0x00451, 0x00451}, {0x02010, 0x02010}, {0x02013, 0x02016}, {0x02018, 0x02019}, {0x0201C, 0x0201D}, {0x02020, 0x02022}, {0x02024, 0x02027}, {0x02030, 0x02030}, {0x02032, 0x02033}, {0x02035, 0x02035}, {0x0203B, 0x0203B}, {0x0203E, 0x0203E}, {0x02074, 0x02074}, {0x0207F, 0x0207F}, {0x02081, 0x02084}, {0x020AC, 0x020AC}, {0x02103, 0x02103}, {0x02105, 0x02105}, {0x02109, 0x02109}, {0x02113, 0x02113}, {0x02116, 0x02116}, {0x02121, 0x02122}, {0x02126, 0x02126}, {0x0212B, 0x0212B}, {0x02153, 0x02154}, {0x0215B, 0x0215E}, {0x02160, 0x0216B}, {0x02170, 0x02179}, {0x02189, 0x02189}, {0x02190, 0x02199}, {0x021B8, 0x021B9}, {0x021D2, 0x021D2}, {0x021D4, 0x021D4}, {0x021E7, 0x021E7}, {0x02200, 0x02200}, {0x02202, 0x02203}, {0x02207, 0x02208}, {0x0220B, 0x0220B}, {0x0220F, 0x0220F}, {0x02211, 0x02211}, {0x02215, 0x02215}, {0x0221A, 0x0221A}, {0x0221D, 0x02220}, {0x02223, 0x02223}, {0x02225, 0x02225}, {0x02227, 0x0222C}, {0x0222E, 0x0222E}, {0x02234, 0x02237}, {0x0223C, 0x0223D}, {0x02248, 0x02248}, {0x0224C, 0x0224C}, {0x02252, 0x02252}, {0x02260, 0x02261}, {0x02264, 0x02267}, {0x0226A, 0x0226B}, {0x0226E, 0x0226F}, {0x02282, 0x02283}, {0x02286, 0x02287}, {0x02295, 0x02295}, {0x02299, 0x02299}, {0x022A5, 0x022A5}, {0x022BF, 0x022BF}, {0x02312, 0x02312}, {0x02460, 0x024E9}, {0x024EB, 0x0254B}, {0x02550, 0x02573}, {0x02580, 0x0258F}, {0x02592, 0x02595}, {0x025A0, 0x025A1}, {0x025A3, 0x025A9}, {0x025B2, 0x025B3}, {0x025B6, 0x025B7}, {0x025BC, 0x025BD}, {0x025C0, 0x025C1}, {0x025C6, 0x025C8}, {0x025CB, 0x025CB}, {0x025CE, 0x025D1}, {0x025E2, 0x025E5}, {0x025EF, 0x025EF}, {0x02605, 0x02606}, {0x02609, 0x02609}, {0x0260E, 0x0260F}, {0x0261C, 0x0261C}, {0x0261E, 0x0261E}, {0x02640, 0x02640}, {0x02642, 0x02642}, {0x02660, 0x02661}, {0x02663, 0x02665}, {0x02667, 0x0266A}, {0x0266C, 0x0266D}, {0x0266F, 0x0266F}, {0x0269E, 0x0269F}, {0x026BF, 0x026BF}, {0x026C6, 0x026CD}, {0x026CF, 0x026D3}, {0x026D5, 0x026E1}, {0x026E3, 0x026E3}, {0x026E8, 0x026E9}, {0x026EB, 0x026F1}, {0x026F4, 0x026F4}, {0x026F6, 0x026F9}, {0x026FB, 0x026FC}, {0x026FE, 0x026FF}, {0x0273D, 0x0273D}, {0x02776, 0x0277F}, {0x02B56, 0x02B59}, {0x03248, 0x0324F}, {0x0E000, 0x0F8FF}, {0x0FE00, 0x0FE0F}, {0x0FFFD, 0x0FFFD}, {0x1F100, 0x1F10A}, {0x1F110, 0x1F12D}, {0x1F130, 0x1F169}, {0x1F170, 0x1F18D}, {0x1F18F, 0x1F190}, {0x1F19B, 0x1F1AC}, {0xE0100, 0xE01EF}, {0xF0000, 0xFFFFD}, {0x100000, 0x10FFFD} }; /* Unassigned characters. */ static const struct widechar_range widechar_unassigned_table[] = { {0x00378, 0x00379}, {0x00380, 0x00383}, {0x0038B, 0x0038B}, {0x0038D, 0x0038D}, {0x003A2, 0x003A2}, {0x00530, 0x00530}, {0x00557, 0x00558}, {0x0058B, 0x0058C}, {0x00590, 0x00590}, {0x005C8, 0x005CF}, {0x005EB, 0x005EE}, {0x005F5, 0x005FF}, {0x0070E, 0x0070E}, {0x0074B, 0x0074C}, {0x007B2, 0x007BF}, {0x007FB, 0x007FC}, {0x0082E, 0x0082F}, {0x0083F, 0x0083F}, {0x0085C, 0x0085D}, {0x0085F, 0x0085F}, {0x0086B, 0x0086F}, {0x0088F, 0x0088F}, {0x00892, 0x00897}, {0x00984, 0x00984}, {0x0098D, 0x0098E}, {0x00991, 0x00992}, {0x009A9, 0x009A9}, {0x009B1, 0x009B1}, {0x009B3, 0x009B5}, {0x009BA, 0x009BB}, {0x009C5, 0x009C6}, {0x009C9, 0x009CA}, {0x009CF, 0x009D6}, {0x009D8, 0x009DB}, {0x009DE, 0x009DE}, {0x009E4, 0x009E5}, {0x009FF, 0x00A00}, {0x00A04, 0x00A04}, {0x00A0B, 0x00A0E}, {0x00A11, 0x00A12}, {0x00A29, 0x00A29}, {0x00A31, 0x00A31}, {0x00A34, 0x00A34}, {0x00A37, 0x00A37}, {0x00A3A, 0x00A3B}, {0x00A3D, 0x00A3D}, {0x00A43, 0x00A46}, {0x00A49, 0x00A4A}, {0x00A4E, 0x00A50}, {0x00A52, 0x00A58}, {0x00A5D, 0x00A5D}, {0x00A5F, 0x00A65}, {0x00A77, 0x00A80}, {0x00A84, 0x00A84}, {0x00A8E, 0x00A8E}, {0x00A92, 0x00A92}, {0x00AA9, 0x00AA9}, {0x00AB1, 0x00AB1}, {0x00AB4, 0x00AB4}, {0x00ABA, 0x00ABB}, {0x00AC6, 0x00AC6}, {0x00ACA, 0x00ACA}, {0x00ACE, 0x00ACF}, {0x00AD1, 0x00ADF}, {0x00AE4, 0x00AE5}, {0x00AF2, 0x00AF8}, {0x00B00, 0x00B00}, {0x00B04, 0x00B04}, {0x00B0D, 0x00B0E}, {0x00B11, 0x00B12}, {0x00B29, 0x00B29}, {0x00B31, 0x00B31}, {0x00B34, 0x00B34}, {0x00B3A, 0x00B3B}, {0x00B45, 0x00B46}, {0x00B49, 0x00B4A}, {0x00B4E, 0x00B54}, {0x00B58, 0x00B5B}, {0x00B5E, 0x00B5E}, {0x00B64, 0x00B65}, {0x00B78, 0x00B81}, {0x00B84, 0x00B84}, {0x00B8B, 0x00B8D}, {0x00B91, 0x00B91}, {0x00B96, 0x00B98}, {0x00B9B, 0x00B9B}, {0x00B9D, 0x00B9D}, {0x00BA0, 0x00BA2}, {0x00BA5, 0x00BA7}, {0x00BAB, 0x00BAD}, {0x00BBA, 0x00BBD}, {0x00BC3, 0x00BC5}, {0x00BC9, 0x00BC9}, {0x00BCE, 0x00BCF}, {0x00BD1, 0x00BD6}, {0x00BD8, 0x00BE5}, {0x00BFB, 0x00BFF}, {0x00C0D, 0x00C0D}, {0x00C11, 0x00C11}, {0x00C29, 0x00C29}, {0x00C3A, 0x00C3B}, {0x00C45, 0x00C45}, {0x00C49, 0x00C49}, {0x00C4E, 0x00C54}, {0x00C57, 0x00C57}, {0x00C5B, 0x00C5C}, {0x00C5E, 0x00C5F}, {0x00C64, 0x00C65}, {0x00C70, 0x00C76}, {0x00C8D, 0x00C8D}, {0x00C91, 0x00C91}, {0x00CA9, 0x00CA9}, {0x00CB4, 0x00CB4}, {0x00CBA, 0x00CBB}, {0x00CC5, 0x00CC5}, {0x00CC9, 0x00CC9}, {0x00CCE, 0x00CD4}, {0x00CD7, 0x00CDC}, {0x00CDF, 0x00CDF}, {0x00CE4, 0x00CE5}, {0x00CF0, 0x00CF0}, {0x00CF3, 0x00CFF}, {0x00D0D, 0x00D0D}, {0x00D11, 0x00D11}, {0x00D45, 0x00D45}, {0x00D49, 0x00D49}, {0x00D50, 0x00D53}, {0x00D64, 0x00D65}, {0x00D80, 0x00D80}, {0x00D84, 0x00D84}, {0x00D97, 0x00D99}, {0x00DB2, 0x00DB2}, {0x00DBC, 0x00DBC}, {0x00DBE, 0x00DBF}, {0x00DC7, 0x00DC9}, {0x00DCB, 0x00DCE}, {0x00DD5, 0x00DD5}, {0x00DD7, 0x00DD7}, {0x00DE0, 0x00DE5}, {0x00DF0, 0x00DF1}, {0x00DF5, 0x00E00}, {0x00E3B, 0x00E3E}, {0x00E5C, 0x00E80}, {0x00E83, 0x00E83}, {0x00E85, 0x00E85}, {0x00E8B, 0x00E8B}, {0x00EA4, 0x00EA4}, {0x00EA6, 0x00EA6}, {0x00EBE, 0x00EBF}, {0x00EC5, 0x00EC5}, {0x00EC7, 0x00EC7}, {0x00ECE, 0x00ECF}, {0x00EDA, 0x00EDB}, {0x00EE0, 0x00EFF}, {0x00F48, 0x00F48}, {0x00F6D, 0x00F70}, {0x00F98, 0x00F98}, {0x00FBD, 0x00FBD}, {0x00FCD, 0x00FCD}, {0x00FDB, 0x00FFF}, {0x010C6, 0x010C6}, {0x010C8, 0x010CC}, {0x010CE, 0x010CF}, {0x01249, 0x01249}, {0x0124E, 0x0124F}, {0x01257, 0x01257}, {0x01259, 0x01259}, {0x0125E, 0x0125F}, {0x01289, 0x01289}, {0x0128E, 0x0128F}, {0x012B1, 0x012B1}, {0x012B6, 0x012B7}, {0x012BF, 0x012BF}, {0x012C1, 0x012C1}, {0x012C6, 0x012C7}, {0x012D7, 0x012D7}, {0x01311, 0x01311}, {0x01316, 0x01317}, {0x0135B, 0x0135C}, {0x0137D, 0x0137F}, {0x0139A, 0x0139F}, {0x013F6, 0x013F7}, {0x013FE, 0x013FF}, {0x0169D, 0x0169F}, {0x016F9, 0x016FF}, {0x01716, 0x0171E}, {0x01737, 0x0173F}, {0x01754, 0x0175F}, {0x0176D, 0x0176D}, {0x01771, 0x01771}, {0x01774, 0x0177F}, {0x017DE, 0x017DF}, {0x017EA, 0x017EF}, {0x017FA, 0x017FF}, {0x0181A, 0x0181F}, {0x01879, 0x0187F}, {0x018AB, 0x018AF}, {0x018F6, 0x018FF}, {0x0191F, 0x0191F}, {0x0192C, 0x0192F}, {0x0193C, 0x0193F}, {0x01941, 0x01943}, {0x0196E, 0x0196F}, {0x01975, 0x0197F}, {0x019AC, 0x019AF}, {0x019CA, 0x019CF}, {0x019DB, 0x019DD}, {0x01A1C, 0x01A1D}, {0x01A5F, 0x01A5F}, {0x01A7D, 0x01A7E}, {0x01A8A, 0x01A8F}, {0x01A9A, 0x01A9F}, {0x01AAE, 0x01AAF}, {0x01ACF, 0x01AFF}, {0x01B4D, 0x01B4F}, {0x01B7F, 0x01B7F}, {0x01BF4, 0x01BFB}, {0x01C38, 0x01C3A}, {0x01C4A, 0x01C4C}, {0x01C89, 0x01C8F}, {0x01CBB, 0x01CBC}, {0x01CC8, 0x01CCF}, {0x01CFB, 0x01CFF}, {0x01F16, 0x01F17}, {0x01F1E, 0x01F1F}, {0x01F46, 0x01F47}, {0x01F4E, 0x01F4F}, {0x01F58, 0x01F58}, {0x01F5A, 0x01F5A}, {0x01F5C, 0x01F5C}, {0x01F5E, 0x01F5E}, {0x01F7E, 0x01F7F}, {0x01FB5, 0x01FB5}, {0x01FC5, 0x01FC5}, {0x01FD4, 0x01FD5}, {0x01FDC, 0x01FDC}, {0x01FF0, 0x01FF1}, {0x01FF5, 0x01FF5}, {0x01FFF, 0x01FFF}, {0x02065, 0x02065}, {0x02072, 0x02073}, {0x0208F, 0x0208F}, {0x0209D, 0x0209F}, {0x020C1, 0x020CF}, {0x020F1, 0x020FF}, {0x0218C, 0x0218F}, {0x02427, 0x0243F}, {0x0244B, 0x0245F}, {0x02B74, 0x02B75}, {0x02B96, 0x02B96}, {0x02CF4, 0x02CF8}, {0x02D26, 0x02D26}, {0x02D28, 0x02D2C}, {0x02D2E, 0x02D2F}, {0x02D68, 0x02D6E}, {0x02D71, 0x02D7E}, {0x02D97, 0x02D9F}, {0x02DA7, 0x02DA7}, {0x02DAF, 0x02DAF}, {0x02DB7, 0x02DB7}, {0x02DBF, 0x02DBF}, {0x02DC7, 0x02DC7}, {0x02DCF, 0x02DCF}, {0x02DD7, 0x02DD7}, {0x02DDF, 0x02DDF}, {0x02E5E, 0x02E7F}, {0x02E9A, 0x02E9A}, {0x02EF4, 0x02EFF}, {0x02FD6, 0x02FEF}, {0x02FFC, 0x02FFF}, {0x03040, 0x03040}, {0x03097, 0x03098}, {0x03100, 0x03104}, {0x03130, 0x03130}, {0x0318F, 0x0318F}, {0x031E4, 0x031EF}, {0x0321F, 0x0321F}, {0x03401, 0x04DBE}, {0x04E01, 0x09FFE}, {0x0A48D, 0x0A48F}, {0x0A4C7, 0x0A4CF}, {0x0A62C, 0x0A63F}, {0x0A6F8, 0x0A6FF}, {0x0A7CB, 0x0A7CF}, {0x0A7D2, 0x0A7D2}, {0x0A7D4, 0x0A7D4}, {0x0A7DA, 0x0A7F1}, {0x0A82D, 0x0A82F}, {0x0A83A, 0x0A83F}, {0x0A878, 0x0A87F}, {0x0A8C6, 0x0A8CD}, {0x0A8DA, 0x0A8DF}, {0x0A954, 0x0A95E}, {0x0A97D, 0x0A97F}, {0x0A9CE, 0x0A9CE}, {0x0A9DA, 0x0A9DD}, {0x0A9FF, 0x0A9FF}, {0x0AA37, 0x0AA3F}, {0x0AA4E, 0x0AA4F}, {0x0AA5A, 0x0AA5B}, {0x0AAC3, 0x0AADA}, {0x0AAF7, 0x0AB00}, {0x0AB07, 0x0AB08}, {0x0AB0F, 0x0AB10}, {0x0AB17, 0x0AB1F}, {0x0AB27, 0x0AB27}, {0x0AB2F, 0x0AB2F}, {0x0AB6C, 0x0AB6F}, {0x0ABEE, 0x0ABEF}, {0x0ABFA, 0x0ABFF}, {0x0AC01, 0x0D7A2}, {0x0D7A4, 0x0D7AF}, {0x0D7C7, 0x0D7CA}, {0x0D7FC, 0x0D7FF}, {0x0FA6E, 0x0FA6F}, {0x0FADA, 0x0FAFF}, {0x0FB07, 0x0FB12}, {0x0FB18, 0x0FB1C}, {0x0FB37, 0x0FB37}, {0x0FB3D, 0x0FB3D}, {0x0FB3F, 0x0FB3F}, {0x0FB42, 0x0FB42}, {0x0FB45, 0x0FB45}, {0x0FBC3, 0x0FBD2}, {0x0FD90, 0x0FD91}, {0x0FDC8, 0x0FDCE}, {0x0FE1A, 0x0FE1F}, {0x0FE53, 0x0FE53}, {0x0FE67, 0x0FE67}, {0x0FE6C, 0x0FE6F}, {0x0FE75, 0x0FE75}, {0x0FEFD, 0x0FEFE}, {0x0FF00, 0x0FF00}, {0x0FFBF, 0x0FFC1}, {0x0FFC8, 0x0FFC9}, {0x0FFD0, 0x0FFD1}, {0x0FFD8, 0x0FFD9}, {0x0FFDD, 0x0FFDF}, {0x0FFE7, 0x0FFE7}, {0x0FFEF, 0x0FFF8}, {0x1000C, 0x1000C}, {0x10027, 0x10027}, {0x1003B, 0x1003B}, {0x1003E, 0x1003E}, {0x1004E, 0x1004F}, {0x1005E, 0x1007F}, {0x100FB, 0x100FF}, {0x10103, 0x10106}, {0x10134, 0x10136}, {0x1018F, 0x1018F}, {0x1019D, 0x1019F}, {0x101A1, 0x101CF}, {0x101FE, 0x1027F}, {0x1029D, 0x1029F}, {0x102D1, 0x102DF}, {0x102FC, 0x102FF}, {0x10324, 0x1032C}, {0x1034B, 0x1034F}, {0x1037B, 0x1037F}, {0x1039E, 0x1039E}, {0x103C4, 0x103C7}, {0x103D6, 0x103FF}, {0x1049E, 0x1049F}, {0x104AA, 0x104AF}, {0x104D4, 0x104D7}, {0x104FC, 0x104FF}, {0x10528, 0x1052F}, {0x10564, 0x1056E}, {0x1057B, 0x1057B}, {0x1058B, 0x1058B}, {0x10593, 0x10593}, {0x10596, 0x10596}, {0x105A2, 0x105A2}, {0x105B2, 0x105B2}, {0x105BA, 0x105BA}, {0x105BD, 0x105FF}, {0x10737, 0x1073F}, {0x10756, 0x1075F}, {0x10768, 0x1077F}, {0x10786, 0x10786}, {0x107B1, 0x107B1}, {0x107BB, 0x107FF}, {0x10806, 0x10807}, {0x10809, 0x10809}, {0x10836, 0x10836}, {0x10839, 0x1083B}, {0x1083D, 0x1083E}, {0x10856, 0x10856}, {0x1089F, 0x108A6}, {0x108B0, 0x108DF}, {0x108F3, 0x108F3}, {0x108F6, 0x108FA}, {0x1091C, 0x1091E}, {0x1093A, 0x1093E}, {0x10940, 0x1097F}, {0x109B8, 0x109BB}, {0x109D0, 0x109D1}, {0x10A04, 0x10A04}, {0x10A07, 0x10A0B}, {0x10A14, 0x10A14}, {0x10A18, 0x10A18}, {0x10A36, 0x10A37}, {0x10A3B, 0x10A3E}, {0x10A49, 0x10A4F}, {0x10A59, 0x10A5F}, {0x10AA0, 0x10ABF}, {0x10AE7, 0x10AEA}, {0x10AF7, 0x10AFF}, {0x10B36, 0x10B38}, {0x10B56, 0x10B57}, {0x10B73, 0x10B77}, {0x10B92, 0x10B98}, {0x10B9D, 0x10BA8}, {0x10BB0, 0x10BFF}, {0x10C49, 0x10C7F}, {0x10CB3, 0x10CBF}, {0x10CF3, 0x10CF9}, {0x10D28, 0x10D2F}, {0x10D3A, 0x10E5F}, {0x10E7F, 0x10E7F}, {0x10EAA, 0x10EAA}, {0x10EAE, 0x10EAF}, {0x10EB2, 0x10EFF}, {0x10F28, 0x10F2F}, {0x10F5A, 0x10F6F}, {0x10F8A, 0x10FAF}, {0x10FCC, 0x10FDF}, {0x10FF7, 0x10FFF}, {0x1104E, 0x11051}, {0x11076, 0x1107E}, {0x110C3, 0x110CC}, {0x110CE, 0x110CF}, {0x110E9, 0x110EF}, {0x110FA, 0x110FF}, {0x11135, 0x11135}, {0x11148, 0x1114F}, {0x11177, 0x1117F}, {0x111E0, 0x111E0}, {0x111F5, 0x111FF}, {0x11212, 0x11212}, {0x1123F, 0x1127F}, {0x11287, 0x11287}, {0x11289, 0x11289}, {0x1128E, 0x1128E}, {0x1129E, 0x1129E}, {0x112AA, 0x112AF}, {0x112EB, 0x112EF}, {0x112FA, 0x112FF}, {0x11304, 0x11304}, {0x1130D, 0x1130E}, {0x11311, 0x11312}, {0x11329, 0x11329}, {0x11331, 0x11331}, {0x11334, 0x11334}, {0x1133A, 0x1133A}, {0x11345, 0x11346}, {0x11349, 0x1134A}, {0x1134E, 0x1134F}, {0x11351, 0x11356}, {0x11358, 0x1135C}, {0x11364, 0x11365}, {0x1136D, 0x1136F}, {0x11375, 0x113FF}, {0x1145C, 0x1145C}, {0x11462, 0x1147F}, {0x114C8, 0x114CF}, {0x114DA, 0x1157F}, {0x115B6, 0x115B7}, {0x115DE, 0x115FF}, {0x11645, 0x1164F}, {0x1165A, 0x1165F}, {0x1166D, 0x1167F}, {0x116BA, 0x116BF}, {0x116CA, 0x116FF}, {0x1171B, 0x1171C}, {0x1172C, 0x1172F}, {0x11747, 0x117FF}, {0x1183C, 0x1189F}, {0x118F3, 0x118FE}, {0x11907, 0x11908}, {0x1190A, 0x1190B}, {0x11914, 0x11914}, {0x11917, 0x11917}, {0x11936, 0x11936}, {0x11939, 0x1193A}, {0x11947, 0x1194F}, {0x1195A, 0x1199F}, {0x119A8, 0x119A9}, {0x119D8, 0x119D9}, {0x119E5, 0x119FF}, {0x11A48, 0x11A4F}, {0x11AA3, 0x11AAF}, {0x11AF9, 0x11BFF}, {0x11C09, 0x11C09}, {0x11C37, 0x11C37}, {0x11C46, 0x11C4F}, {0x11C6D, 0x11C6F}, {0x11C90, 0x11C91}, {0x11CA8, 0x11CA8}, {0x11CB7, 0x11CFF}, {0x11D07, 0x11D07}, {0x11D0A, 0x11D0A}, {0x11D37, 0x11D39}, {0x11D3B, 0x11D3B}, {0x11D3E, 0x11D3E}, {0x11D48, 0x11D4F}, {0x11D5A, 0x11D5F}, {0x11D66, 0x11D66}, {0x11D69, 0x11D69}, {0x11D8F, 0x11D8F}, {0x11D92, 0x11D92}, {0x11D99, 0x11D9F}, {0x11DAA, 0x11EDF}, {0x11EF9, 0x11FAF}, {0x11FB1, 0x11FBF}, {0x11FF2, 0x11FFE}, {0x1239A, 0x123FF}, {0x1246F, 0x1246F}, {0x12475, 0x1247F}, {0x12544, 0x12F8F}, {0x12FF3, 0x12FFF}, {0x1342F, 0x1342F}, {0x13439, 0x143FF}, {0x14647, 0x167FF}, {0x16A39, 0x16A3F}, {0x16A5F, 0x16A5F}, {0x16A6A, 0x16A6D}, {0x16ABF, 0x16ABF}, {0x16ACA, 0x16ACF}, {0x16AEE, 0x16AEF}, {0x16AF6, 0x16AFF}, {0x16B46, 0x16B4F}, {0x16B5A, 0x16B5A}, {0x16B62, 0x16B62}, {0x16B78, 0x16B7C}, {0x16B90, 0x16E3F}, {0x16E9B, 0x16EFF}, {0x16F4B, 0x16F4E}, {0x16F88, 0x16F8E}, {0x16FA0, 0x16FDF}, {0x16FE5, 0x16FEF}, {0x16FF2, 0x16FFF}, {0x17001, 0x187F6}, {0x187F8, 0x187FF}, {0x18CD6, 0x18CFF}, {0x18D01, 0x18D07}, {0x18D09, 0x1AFEF}, {0x1AFF4, 0x1AFF4}, {0x1AFFC, 0x1AFFC}, {0x1AFFF, 0x1AFFF}, {0x1B123, 0x1B14F}, {0x1B153, 0x1B163}, {0x1B168, 0x1B16F}, {0x1B2FC, 0x1BBFF}, {0x1BC6B, 0x1BC6F}, {0x1BC7D, 0x1BC7F}, {0x1BC89, 0x1BC8F}, {0x1BC9A, 0x1BC9B}, {0x1BCA4, 0x1CEFF}, {0x1CF2E, 0x1CF2F}, {0x1CF47, 0x1CF4F}, {0x1CFC4, 0x1CFFF}, {0x1D0F6, 0x1D0FF}, {0x1D127, 0x1D128}, {0x1D1EB, 0x1D1FF}, {0x1D246, 0x1D2DF}, {0x1D2F4, 0x1D2FF}, {0x1D357, 0x1D35F}, {0x1D379, 0x1D3FF}, {0x1D455, 0x1D455}, {0x1D49D, 0x1D49D}, {0x1D4A0, 0x1D4A1}, {0x1D4A3, 0x1D4A4}, {0x1D4A7, 0x1D4A8}, {0x1D4AD, 0x1D4AD}, {0x1D4BA, 0x1D4BA}, {0x1D4BC, 0x1D4BC}, {0x1D4C4, 0x1D4C4}, {0x1D506, 0x1D506}, {0x1D50B, 0x1D50C}, {0x1D515, 0x1D515}, {0x1D51D, 0x1D51D}, {0x1D53A, 0x1D53A}, {0x1D53F, 0x1D53F}, {0x1D545, 0x1D545}, {0x1D547, 0x1D549}, {0x1D551, 0x1D551}, {0x1D6A6, 0x1D6A7}, {0x1D7CC, 0x1D7CD}, {0x1DA8C, 0x1DA9A}, {0x1DAA0, 0x1DAA0}, {0x1DAB0, 0x1DEFF}, {0x1DF1F, 0x1DFFF}, {0x1E007, 0x1E007}, {0x1E019, 0x1E01A}, {0x1E022, 0x1E022}, {0x1E025, 0x1E025}, {0x1E02B, 0x1E0FF}, {0x1E12D, 0x1E12F}, {0x1E13E, 0x1E13F}, {0x1E14A, 0x1E14D}, {0x1E150, 0x1E28F}, {0x1E2AF, 0x1E2BF}, {0x1E2FA, 0x1E2FE}, {0x1E300, 0x1E7DF}, {0x1E7E7, 0x1E7E7}, {0x1E7EC, 0x1E7EC}, {0x1E7EF, 0x1E7EF}, {0x1E7FF, 0x1E7FF}, {0x1E8C5, 0x1E8C6}, {0x1E8D7, 0x1E8FF}, {0x1E94C, 0x1E94F}, {0x1E95A, 0x1E95D}, {0x1E960, 0x1EC70}, {0x1ECB5, 0x1ED00}, {0x1ED3E, 0x1EDFF}, {0x1EE04, 0x1EE04}, {0x1EE20, 0x1EE20}, {0x1EE23, 0x1EE23}, {0x1EE25, 0x1EE26}, {0x1EE28, 0x1EE28}, {0x1EE33, 0x1EE33}, {0x1EE38, 0x1EE38}, {0x1EE3A, 0x1EE3A}, {0x1EE3C, 0x1EE41}, {0x1EE43, 0x1EE46}, {0x1EE48, 0x1EE48}, {0x1EE4A, 0x1EE4A}, {0x1EE4C, 0x1EE4C}, {0x1EE50, 0x1EE50}, {0x1EE53, 0x1EE53}, {0x1EE55, 0x1EE56}, {0x1EE58, 0x1EE58}, {0x1EE5A, 0x1EE5A}, {0x1EE5C, 0x1EE5C}, {0x1EE5E, 0x1EE5E}, {0x1EE60, 0x1EE60}, {0x1EE63, 0x1EE63}, {0x1EE65, 0x1EE66}, {0x1EE6B, 0x1EE6B}, {0x1EE73, 0x1EE73}, {0x1EE78, 0x1EE78}, {0x1EE7D, 0x1EE7D}, {0x1EE7F, 0x1EE7F}, {0x1EE8A, 0x1EE8A}, {0x1EE9C, 0x1EEA0}, {0x1EEA4, 0x1EEA4}, {0x1EEAA, 0x1EEAA}, {0x1EEBC, 0x1EEEF}, {0x1EEF2, 0x1EFFF}, {0x1F02C, 0x1F02F}, {0x1F094, 0x1F09F}, {0x1F0AF, 0x1F0B0}, {0x1F0C0, 0x1F0C0}, {0x1F0D0, 0x1F0D0}, {0x1F0F6, 0x1F0FF}, {0x1F1AE, 0x1F1E5}, {0x1F203, 0x1F20F}, {0x1F23C, 0x1F23F}, {0x1F249, 0x1F24F}, {0x1F252, 0x1F25F}, {0x1F266, 0x1F2FF}, {0x1F6D8, 0x1F6DC}, {0x1F6ED, 0x1F6EF}, {0x1F6FD, 0x1F6FF}, {0x1F774, 0x1F77F}, {0x1F7D9, 0x1F7DF}, {0x1F7EC, 0x1F7EF}, {0x1F7F1, 0x1F7FF}, {0x1F80C, 0x1F80F}, {0x1F848, 0x1F84F}, {0x1F85A, 0x1F85F}, {0x1F888, 0x1F88F}, {0x1F8AE, 0x1F8AF}, {0x1F8B2, 0x1F8FF}, {0x1FA54, 0x1FA5F}, {0x1FA6E, 0x1FA6F}, {0x1FA75, 0x1FA77}, {0x1FA7D, 0x1FA7F}, {0x1FA87, 0x1FA8F}, {0x1FAAD, 0x1FAAF}, {0x1FABB, 0x1FABF}, {0x1FAC6, 0x1FACF}, {0x1FADA, 0x1FADF}, {0x1FAE8, 0x1FAEF}, {0x1FAF7, 0x1FAFF}, {0x1FB93, 0x1FB93}, {0x1FBCB, 0x1FBEF}, {0x1FBFA, 0x1FFFD}, {0x20001, 0x2A6DE}, {0x2A6E0, 0x2A6FF}, {0x2A701, 0x2B737}, {0x2B739, 0x2B73F}, {0x2B741, 0x2B81C}, {0x2B81E, 0x2B81F}, {0x2B821, 0x2CEA0}, {0x2CEA2, 0x2CEAF}, {0x2CEB1, 0x2EBDF}, {0x2EBE1, 0x2F7FF}, {0x2FA1E, 0x2FFFD}, {0x30001, 0x31349}, {0x3134B, 0x3FFFD}, {0x40000, 0x4FFFD}, {0x50000, 0x5FFFD}, {0x60000, 0x6FFFD}, {0x70000, 0x7FFFD}, {0x80000, 0x8FFFD}, {0x90000, 0x9FFFD}, {0xA0000, 0xAFFFD}, {0xB0000, 0xBFFFD}, {0xC0000, 0xCFFFD}, {0xD0000, 0xDFFFD}, {0xE0000, 0xE0000}, {0xE0002, 0xE001F}, {0xE0080, 0xE00FF}, {0xE01F0, 0xEFFFD} }; /* Non-characters. */ static const struct widechar_range widechar_nonchar_table[] = { {0x0FDD0, 0x0FDEF}, {0x0FFFE, 0x0FFFF}, {0x1FFFE, 0x1FFFF}, {0x2FFFE, 0x2FFFF}, {0x3FFFE, 0x3FFFF}, {0x4FFFE, 0x4FFFF}, {0x5FFFE, 0x5FFFF}, {0x6FFFE, 0x6FFFF}, {0x7FFFE, 0x7FFFF}, {0x8FFFE, 0x8FFFF}, {0x9FFFE, 0x9FFFF}, {0xAFFFE, 0xAFFFF}, {0xBFFFE, 0xBFFFF}, {0xCFFFE, 0xCFFFF}, {0xDFFFE, 0xDFFFF}, {0xEFFFE, 0xEFFFF}, {0xFFFFE, 0xFFFFF}, {0x10FFFE, 0x10FFFF} }; /* Characters that were widened from width 1 to 2 in Unicode 9. */ static const struct widechar_range widechar_widened_table[] = { {0x0231A, 0x0231B}, {0x023E9, 0x023EC}, {0x023F0, 0x023F0}, {0x023F3, 0x023F3}, {0x025FD, 0x025FE}, {0x02614, 0x02615}, {0x02648, 0x02653}, {0x0267F, 0x0267F}, {0x02693, 0x02693}, {0x026A1, 0x026A1}, {0x026AA, 0x026AB}, {0x026BD, 0x026BE}, {0x026C4, 0x026C5}, {0x026CE, 0x026CE}, {0x026D4, 0x026D4}, {0x026EA, 0x026EA}, {0x026F2, 0x026F3}, {0x026F5, 0x026F5}, {0x026FA, 0x026FA}, {0x026FD, 0x026FD}, {0x02705, 0x02705}, {0x0270A, 0x0270B}, {0x02728, 0x02728}, {0x0274C, 0x0274C}, {0x0274E, 0x0274E}, {0x02753, 0x02755}, {0x02757, 0x02757}, {0x02795, 0x02797}, {0x027B0, 0x027B0}, {0x027BF, 0x027BF}, {0x02B1B, 0x02B1C}, {0x02B50, 0x02B50}, {0x02B55, 0x02B55}, {0x1F004, 0x1F004}, {0x1F0CF, 0x1F0CF}, {0x1F18E, 0x1F18E}, {0x1F191, 0x1F19A}, {0x1F201, 0x1F201}, {0x1F21A, 0x1F21A}, {0x1F22F, 0x1F22F}, {0x1F232, 0x1F236}, {0x1F238, 0x1F23A}, {0x1F250, 0x1F251}, {0x1F300, 0x1F320}, {0x1F32D, 0x1F335}, {0x1F337, 0x1F37C}, {0x1F37E, 0x1F393}, {0x1F3A0, 0x1F3CA}, {0x1F3CF, 0x1F3D3}, {0x1F3E0, 0x1F3F0}, {0x1F3F4, 0x1F3F4}, {0x1F3F8, 0x1F43E}, {0x1F440, 0x1F440}, {0x1F442, 0x1F4FC}, {0x1F4FF, 0x1F53D}, {0x1F54B, 0x1F54E}, {0x1F550, 0x1F567}, {0x1F595, 0x1F596}, {0x1F5FB, 0x1F64F}, {0x1F680, 0x1F6C5}, {0x1F6CC, 0x1F6CC}, {0x1F6D0, 0x1F6D0}, {0x1F6EB, 0x1F6EC}, {0x1F910, 0x1F918}, {0x1F980, 0x1F984}, {0x1F9C0, 0x1F9C0} }; template bool widechar_in_table(const Collection &arr, uint32_t c) { auto where = std::lower_bound(std::begin(arr), std::end(arr), c, [](widechar_range p, uint32_t c) { return p.hi < c; }); return where != std::end(arr) && where->lo <= c; } /* Return the width of character c, or a special negative value. */ int wcwidth(uint32_t c) { if (widechar_in_table(widechar_ascii_table, c)) return 1; if (widechar_in_table(widechar_private_table, c)) return widechar_private_use; if (widechar_in_table(widechar_nonprint_table, c)) return widechar_nonprint; if (widechar_in_table(widechar_nonchar_table, c)) return widechar_non_character; if (widechar_in_table(widechar_combining_table, c)) return widechar_combining; if (widechar_in_table(widechar_combiningletters_table, c)) return widechar_combining; if (widechar_in_table(widechar_doublewide_table, c)) return 2; if (widechar_in_table(widechar_ambiguous_table, c)) return widechar_ambiguous; if (widechar_in_table(widechar_unassigned_table, c)) return widechar_unassigned; if (widechar_in_table(widechar_widened_table, c)) return widechar_widened_in_9; return 1; } } // namespace #endif // WIDECHAR_WIDTH_H btop-1.2.3/snap/000077500000000000000000000000001420276253000134235ustar00rootroot00000000000000btop-1.2.3/snap/snapcraft.yaml000066400000000000000000000024311420276253000162700ustar00rootroot00000000000000name: btop adopt-info: btop summary: Resource monitor that shows usage and stats description: | Resource monitor that shows usage and stats for processor, memory, disks, network and processes. C++ version and continuation of bashtop and bpytop. license: Apache-2.0 base: core20 grade: stable confinement: strict compression: lzo architectures: - build-on: amd64 - build-on: arm64 - build-on: armhf - build-on: ppc64el - build-on: s390x package-repositories: - type: apt ppa: ubuntu-toolchain-r/test apps: btop: command: usr/local/bin/btop environment: LC_ALL: C.UTF-8 LANG: C.UTF-8 plugs: - mount-observe - process-control - system-observe - hardware-observe - network - network-observe # - physical-memory-observe - home - removable-media parts: btop: source: https://github.com/aristocratos/btop source-type: git plugin: make make-parameters: - PREFIX=/usr/local - STATIC=true - ADDFLAGS="-D SNAPPED" build-packages: - coreutils - sed - git - build-essential - gcc-11 - g++-11 override-pull: | snapcraftctl pull snapcraftctl set-version "$(git describe --tags | sed 's/^v//' | cut -d "-" -f1)" btop-1.2.3/src/000077500000000000000000000000001420276253000132515ustar00rootroot00000000000000btop-1.2.3/src/btop.cpp000066400000000000000000000737161420276253000147370ustar00rootroot00000000000000/* 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. indent = tab tab-size = 4 */ #include #include #include #ifdef __FreeBSD__ #include #endif #include #include #include #include #include #include #include #include #include #include #ifdef __APPLE__ #include #endif #include #include #include #include #include #include #include using std::string, std::string_view, std::vector, std::atomic, std::endl, std::cout, std::min, std::flush, std::endl; using std::string_literals::operator""s, std::to_string; namespace fs = std::filesystem; namespace rng = std::ranges; using namespace Tools; using namespace std::chrono_literals; namespace Global { const vector> Banner_src = { {"#E62525", "██████╗ ████████╗ ██████╗ ██████╗"}, {"#CD2121", "██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗ ██╗ ██╗"}, {"#B31D1D", "██████╔╝ ██║ ██║ ██║██████╔╝ ██████╗██████╗"}, {"#9A1919", "██╔══██╗ ██║ ██║ ██║██╔═══╝ ╚═██╔═╝╚═██╔═╝"}, {"#801414", "██████╔╝ ██║ ╚██████╔╝██║ ╚═╝ ╚═╝"}, {"#000000", "╚═════╝ ╚═╝ ╚═════╝ ╚═╝"}, }; const string Version = "1.2.3"; int coreCount; string overlay; string clock; string bg_black = "\x1b[0;40m"; string fg_white = "\x1b[1;97m"; string fg_green = "\x1b[1;92m"; string fg_red = "\x1b[0;91m"; uid_t real_uid, set_uid; fs::path self_path; string exit_error_msg; atomic thread_exception (false); bool debuginit = false; bool debug = false; bool utf_force = false; uint64_t start_time; atomic resized (false); atomic quitting (false); atomic should_quit (false); atomic should_sleep (false); atomic _runner_started (false); bool arg_tty = false; bool arg_low_color = false; int arg_preset = -1; } //* A simple argument parser void argumentParser(const int& argc, char **argv) { for(int i = 1; i < argc; i++) { const string argument = argv[i]; if (is_in(argument, "-h", "--help")) { cout << "usage: btop [-h] [-v] [-/+t] [-p ] [--utf-force] [--debug]\n\n" << "optional arguments:\n" << " -h, --help show this help message and exit\n" << " -v, --version show version info and exit\n" << " -lc, --low-color disable truecolor, converts 24-bit colors to 256-color\n" << " -t, --tty_on force (ON) tty mode, max 16 colors and tty friendly graph symbols\n" << " +t, --tty_off force (OFF) tty mode\n" << " -p, --preset start with preset, integer value between 0-9\n" << " --utf-force force start even if no UTF-8 locale was detected\n" << " --debug start in DEBUG mode: shows microsecond timer for information collect\n" << " and screen draw functions and sets loglevel to DEBUG\n" << endl; exit(0); } else if (is_in(argument, "-v", "--version")) { cout << "btop version: " << Global::Version << endl; exit(0); } else if (is_in(argument, "-lc", "--low-color")) { Global::arg_low_color = true; } else if (is_in(argument, "-t", "--tty_on")) { Config::set("tty_mode", true); Global::arg_tty = true; } else if (is_in(argument, "+t", "--tty_off")) { Config::set("tty_mode", false); Global::arg_tty = true; } else if (is_in(argument, "-p", "--preset")) { if (++i >= argc) { cout << "ERROR: Preset option needs an argument." << endl; exit(1); } else if (const string val = argv[i]; isint(val) and val.size() == 1) { Global::arg_preset = std::clamp(stoi(val), 0, 9); } else { cout << "ERROR: Preset option only accepts an integer value between 0-9." << endl; exit(1); } } else if (argument == "--utf-force") Global::utf_force = true; else if (argument == "--debug") Global::debug = true; else { cout << " Unknown argument: " << argument << "\n" << " Use -h or --help for help." << endl; exit(1); } } } //* Handler for SIGWINCH and general resizing events, does nothing if terminal hasn't been resized unless force=true void term_resize(bool force) { static atomic resizing (false); if (Input::polling) { Global::resized = true; Input::interrupt = true; return; } atomic_lock lck(resizing, true); if (auto refreshed = Term::refresh(true); refreshed or force) { if (force and refreshed) force = false; } else return; static const array all_boxes = {"cpu", "mem", "net", "proc"}; Global::resized = true; if (Runner::active) Runner::stop(); Term::refresh(); Config::unlock(); auto boxes = Config::getS("shown_boxes"); auto min_size = Term::get_min_size(boxes); while (not force or (Term::width < min_size.at(0) or Term::height < min_size.at(1))) { sleep_ms(100); if (Term::width < min_size.at(0) or Term::height < min_size.at(1)) { cout << Term::clear << Global::bg_black << Global::fg_white << Mv::to((Term::height / 2) - 2, (Term::width / 2) - 11) << "Terminal size too small:" << Mv::to((Term::height / 2) - 1, (Term::width / 2) - 10) << " Width = " << (Term::width < min_size.at(1) ? Global::fg_red : Global::fg_green) << Term::width << Global::fg_white << " Height = " << (Term::height < min_size.at(0) ? Global::fg_red : Global::fg_green) << Term::height << Mv::to((Term::height / 2) + 1, (Term::width / 2) - 12) << Global::fg_white << "Needed for current config:" << Mv::to((Term::height / 2) + 2, (Term::width / 2) - 10) << "Width = " << min_size.at(0) << " Height = " << min_size.at(1) << flush; bool got_key = false; for (; not Term::refresh() and not got_key; got_key = Input::poll(10)); if (got_key) { auto key = Input::get(); if (key == "q") clean_quit(0); else if (is_in(key, "1", "2", "3", "4")) { Config::current_preset = -1; Config::toggle_box(all_boxes.at(std::stoi(key) - 1)); boxes = Config::getS("shown_boxes"); } } min_size = Term::get_min_size(boxes); } else if (not Term::refresh()) break; } Input::interrupt = true; } //* Exit handler; stops threads, restores terminal and saves config changes void clean_quit(int sig) { if (Global::quitting) return; Global::quitting = true; Runner::stop(); if (Global::_runner_started) { #ifdef __APPLE__ if (pthread_join(Runner::runner_id, NULL) != 0) { Logger::warning("Failed to join _runner thread on exit!"); pthread_cancel(Runner::runner_id); } #else struct timespec ts; ts.tv_sec = 5; if (pthread_timedjoin_np(Runner::runner_id, NULL, &ts) != 0) { Logger::warning("Failed to join _runner thread on exit!"); pthread_cancel(Runner::runner_id); } #endif } Config::write(); if (Term::initialized) { Input::clear(); Term::restore(); } if (not Global::exit_error_msg.empty()) { sig = 1; Logger::error(Global::exit_error_msg); std::cerr << Global::fg_red << "ERROR: " << Global::fg_white << Global::exit_error_msg << Fx::reset << endl; } Logger::info("Quitting! Runtime: " + sec_to_dhms(time_s() - Global::start_time)); const auto excode = (sig != -1 ? sig : 0); #ifdef __APPLE__ _Exit(excode); #else quick_exit(excode); #endif } //* Handler for SIGTSTP; stops threads, restores terminal and sends SIGSTOP void _sleep() { Runner::stop(); Term::restore(); std::raise(SIGSTOP); } //* Handler for SIGCONT; re-initialize terminal and force a resize event void _resume() { Term::init(); term_resize(true); } void _exit_handler() { clean_quit(-1); } void _signal_handler(const int sig) { switch (sig) { case SIGINT: if (Runner::active) { Global::should_quit = true; Runner::stopping = true; Input::interrupt = true; } else { clean_quit(0); } break; case SIGTSTP: if (Runner::active) { Global::should_sleep = true; Runner::stopping = true; Input::interrupt = true; } else { _sleep(); } break; case SIGCONT: _resume(); break; case SIGWINCH: term_resize(); break; } } //* Manages secondary thread for collection and drawing of boxes namespace Runner { atomic active (false); atomic stopping (false); atomic waiting (false); atomic redraw (false); //* Setup semaphore for triggering thread to do work #if __GNUC__ < 11 #include sem_t do_work; inline void thread_sem_init() { sem_init(&do_work, 0, 0); } inline void thread_wait() { sem_wait(&do_work); } inline void thread_trigger() { sem_post(&do_work); } #else #include std::binary_semaphore do_work(0); inline void thread_sem_init() { ; } inline void thread_wait() { do_work.acquire(); } inline void thread_trigger() { do_work.release(); } #endif //* RAII wrapper for pthread_mutex locking class thread_lock { pthread_mutex_t& pt_mutex; public: int status; thread_lock(pthread_mutex_t& mtx) : pt_mutex(mtx) { pthread_mutex_init(&pt_mutex, NULL); status = pthread_mutex_lock(&pt_mutex); } ~thread_lock() { if (status == 0) pthread_mutex_unlock(&pt_mutex); } }; //* Wrapper for raising priviliges when using SUID bit class gain_priv { int status = -1; public: gain_priv() { if (Global::real_uid != Global::set_uid) this->status = seteuid(Global::set_uid); } ~gain_priv() { if (status == 0) status = seteuid(Global::real_uid); } }; string output; string empty_bg; bool pause_output = false; sigset_t mask; pthread_t runner_id; pthread_mutex_t mtx; enum debug_actions { collect_begin, draw_begin, draw_done }; enum debug_array { collect, draw }; string debug_bg; unordered_flat_map> debug_times; struct runner_conf { vector boxes; bool no_update; bool force_redraw; bool background_update; string overlay; string clock; }; struct runner_conf current_conf; void debug_timer(const char* name, const int action) { switch (action) { case collect_begin: debug_times[name].at(collect) = time_micros(); return; case draw_begin: debug_times[name].at(draw) = time_micros(); debug_times[name].at(collect) = debug_times[name].at(draw) - debug_times[name].at(collect); debug_times["total"].at(collect) += debug_times[name].at(collect); return; case draw_done: debug_times[name].at(draw) = time_micros() - debug_times[name].at(draw); debug_times["total"].at(draw) += debug_times[name].at(draw); return; } } //? ------------------------------- Secondary thread: async launcher and drawing ---------------------------------- void * _runner(void * _) { (void)_; //? Block some signals in this thread to avoid deadlock from any signal handlers trying to stop this thread sigemptyset(&mask); // sigaddset(&mask, SIGINT); // sigaddset(&mask, SIGTSTP); sigaddset(&mask, SIGWINCH); sigaddset(&mask, SIGTERM); pthread_sigmask(SIG_BLOCK, &mask, NULL); //? pthread_mutex_lock to lock thread and monitor health from main thread thread_lock pt_lck(mtx); if (pt_lck.status != 0) { Global::exit_error_msg = "Exception in runner thread -> pthread_mutex_lock error id: " + to_string(pt_lck.status); Global::thread_exception = true; Input::interrupt = true; stopping = true; } //* ----------------------------------------------- THREAD LOOP ----------------------------------------------- while (not Global::quitting) { thread_wait(); atomic_wait_for(active, true, 5000); if (active) { Global::exit_error_msg = "Runner thread failed to get active lock!"; Global::thread_exception = true; Input::interrupt = true; stopping = true; } if (stopping or Global::resized) { sleep_ms(1); continue; } //? Atomic lock used for blocking non thread-safe actions in main thread atomic_lock lck(active); //? Set effective user if SUID bit is set gain_priv powers{}; auto& conf = current_conf; //! DEBUG stats if (Global::debug) { if (debug_bg.empty() or redraw) Runner::debug_bg = Draw::createBox(2, 2, 32, 8, "", true, "debug"); debug_times.clear(); debug_times["total"] = {0, 0}; } output.clear(); //* Run collection and draw functions for all boxes try { //? CPU if (v_contains(conf.boxes, "cpu")) { try { if (Global::debug) debug_timer("cpu", collect_begin); //? Start collect auto cpu = Cpu::collect(conf.no_update); if (Global::debug) debug_timer("cpu", draw_begin); //? Draw box if (not pause_output) output += Cpu::draw(cpu, conf.force_redraw, conf.no_update); if (Global::debug) debug_timer("cpu", draw_done); } catch (const std::exception& e) { throw std::runtime_error("Cpu:: -> " + (string)e.what()); } } //? MEM if (v_contains(conf.boxes, "mem")) { try { if (Global::debug) debug_timer("mem", collect_begin); //? Start collect auto mem = Mem::collect(conf.no_update); if (Global::debug) debug_timer("mem", draw_begin); //? Draw box if (not pause_output) output += Mem::draw(mem, conf.force_redraw, conf.no_update); if (Global::debug) debug_timer("mem", draw_done); } catch (const std::exception& e) { throw std::runtime_error("Mem:: -> " + (string)e.what()); } } //? NET if (v_contains(conf.boxes, "net")) { try { if (Global::debug) debug_timer("net", collect_begin); //? Start collect auto net = Net::collect(conf.no_update); if (Global::debug) debug_timer("net", draw_begin); //? Draw box if (not pause_output) output += Net::draw(net, conf.force_redraw, conf.no_update); if (Global::debug) debug_timer("net", draw_done); } catch (const std::exception& e) { throw std::runtime_error("Net:: -> " + (string)e.what()); } } //? PROC if (v_contains(conf.boxes, "proc")) { try { if (Global::debug) debug_timer("proc", collect_begin); //? Start collect auto proc = Proc::collect(conf.no_update); if (Global::debug) debug_timer("proc", draw_begin); //? Draw box if (not pause_output) output += Proc::draw(proc, conf.force_redraw, conf.no_update); if (Global::debug) debug_timer("proc", draw_done); } catch (const std::exception& e) { throw std::runtime_error("Proc:: -> " + (string)e.what()); } } } catch (const std::exception& e) { Global::exit_error_msg = "Exception in runner thread -> " + (string)e.what(); Global::thread_exception = true; Input::interrupt = true; stopping = true; } if (stopping) { continue; } if (redraw or conf.force_redraw) { empty_bg.clear(); redraw = false; } if (not pause_output) output += conf.clock; if (not conf.overlay.empty() and not conf.background_update) pause_output = true; if (output.empty() and not pause_output) { if (empty_bg.empty()) { const int x = Term::width / 2 - 10, y = Term::height / 2 - 10; output += Term::clear; empty_bg += Draw::banner_gen(y, 0, true) + Mv::to(y+6, x) + Theme::c("title") + Fx::b + "No boxes shown!" + Mv::to(y+8, x) + Theme::c("hi_fg") + "1" + Theme::c("main_fg") + " | Show CPU box" + Mv::to(y+9, x) + Theme::c("hi_fg") + "2" + Theme::c("main_fg") + " | Show MEM box" + Mv::to(y+10, x) + Theme::c("hi_fg") + "3" + Theme::c("main_fg") + " | Show NET box" + Mv::to(y+11, x) + Theme::c("hi_fg") + "4" + Theme::c("main_fg") + " | Show PROC box" + Mv::to(y+12, x-2) + Theme::c("hi_fg") + "esc" + Theme::c("main_fg") + " | Show menu" + Mv::to(y+13, x) + Theme::c("hi_fg") + "q" + Theme::c("main_fg") + " | Quit"; } output += empty_bg; } //! DEBUG stats --> if (Global::debug and not Menu::active) { output += debug_bg + Theme::c("title") + Fx::b + ljust(" Box", 9) + ljust("Collect μs", 12, true) + ljust("Draw μs", 9, true) + Theme::c("main_fg") + Fx::ub; for (const string name : {"cpu", "mem", "net", "proc", "total"}) { if (not debug_times.contains(name)) debug_times[name] = {0,0}; const auto& [time_collect, time_draw] = debug_times.at(name); if (name == "total") output += Fx::b; output += Mv::l(29) + Mv::d(1) + ljust(name, 8) + ljust(to_string(time_collect), 12) + ljust(to_string(time_draw), 9); } } //? If overlay isn't empty, print output without color and then print overlay on top cout << Term::sync_start << (conf.overlay.empty() ? output : (output.empty() ? "" : Fx::ub + Theme::c("inactive_fg") + Fx::uncolor(output)) + conf.overlay) << Term::sync_end << flush; } //* ----------------------------------------------- THREAD LOOP ----------------------------------------------- pthread_exit(NULL); } //? ------------------------------------------ Secondary thread end ----------------------------------------------- //* Runs collect and draw in a secondary thread, unlocks and locks config to update cached values void run(const string& box, const bool no_update, const bool force_redraw) { atomic_wait_for(active, true, 5000); if (active) { Logger::error("Stall in Runner thread, restarting!"); active = false; // exit(1); pthread_cancel(Runner::runner_id); if (pthread_create(&Runner::runner_id, NULL, &Runner::_runner, NULL) != 0) { Global::exit_error_msg = "Failed to re-create _runner thread!"; clean_quit(1); } } if (stopping or Global::resized) return; if (box == "overlay") { cout << Term::sync_start << Global::overlay << Term::sync_end << flush; } else if (box == "clock") { cout << Term::sync_start << Global::clock << Term::sync_end << flush; } else { Config::unlock(); Config::lock(); current_conf = { (box == "all" ? Config::current_boxes : vector{box}), no_update, force_redraw, (not Config::getB("tty_mode") and Config::getB("background_update")), Global::overlay, Global::clock }; if (Menu::active and not current_conf.background_update) Global::overlay.clear(); thread_trigger(); atomic_wait_for(active, false, 10); } } //* Stops any work being done in runner thread and checks for thread errors void stop() { stopping = true; int ret = pthread_mutex_trylock(&mtx); if (ret != EBUSY and not Global::quitting) { if (active) active = false; Global::exit_error_msg = "Runner thread died unexpectedly!"; clean_quit(1); } else if (ret == EBUSY) { atomic_wait_for(active, true, 5000); if (active) { active = false; if (Global::quitting) { return; } else { Global::exit_error_msg = "No response from Runner thread, quitting!"; clean_quit(1); } } thread_trigger(); atomic_wait_for(active, false, 100); atomic_wait_for(active, true, 100); } stopping = false; } } //* --------------------------------------------- Main starts here! --------------------------------------------------- int main(int argc, char **argv) { //? ------------------------------------------------ INIT --------------------------------------------------------- Global::start_time = time_s(); //? Save real and effective userid's and drop priviliges until needed if running with SUID bit set Global::real_uid = getuid(); Global::set_uid = geteuid(); if (Global::real_uid != Global::set_uid) { if (seteuid(Global::real_uid) != 0) { Global::real_uid = Global::set_uid; Global::exit_error_msg = "Failed to change effective user ID. Unset btop SUID bit to ensure security on this system. Quitting!"; clean_quit(1); } } //? Call argument parser if launched with arguments if (argc > 1) argumentParser(argc, argv); //? Setup paths for config, log and user themes for (const auto& env : {"XDG_CONFIG_HOME", "HOME"}) { if (std::getenv(env) != NULL and access(std::getenv(env), W_OK) != -1) { Config::conf_dir = fs::path(std::getenv(env)) / (((string)env == "HOME") ? ".config/btop" : "btop"); break; } } if (Config::conf_dir.empty()) { cout << "WARNING: Could not get path user HOME folder.\n" << "Make sure $XDG_CONFIG_HOME or $HOME environment variables is correctly set to fix this." << endl; } else { if (std::error_code ec; not fs::is_directory(Config::conf_dir) and not fs::create_directories(Config::conf_dir, ec)) { cout << "WARNING: Could not create or access btop config directory. Logging and config saving disabled.\n" << "Make sure $XDG_CONFIG_HOME or $HOME environment variables is correctly set to fix this." << endl; } else { Config::conf_file = Config::conf_dir / "btop.conf"; Logger::logfile = Config::conf_dir / "btop.log"; Theme::user_theme_dir = Config::conf_dir / "themes"; if (not fs::exists(Theme::user_theme_dir) and not fs::create_directory(Theme::user_theme_dir, ec)) Theme::user_theme_dir.clear(); } } //? Try to find global btop theme path relative to binary path #if defined(__linux__) { std::error_code ec; Global::self_path = fs::read_symlink("/proc/self/exe", ec).remove_filename(); } #endif if (std::error_code ec; not Global::self_path.empty()) { Theme::theme_dir = fs::canonical(Global::self_path / "../share/btop/themes", ec); if (ec or not fs::is_directory(Theme::theme_dir) or access(Theme::theme_dir.c_str(), R_OK) == -1) Theme::theme_dir.clear(); } //? If relative path failed, check two most common absolute paths if (Theme::theme_dir.empty()) { for (auto theme_path : {"/usr/local/share/btop/themes", "/usr/share/btop/themes"}) { if (fs::is_directory(fs::path(theme_path)) and access(theme_path, R_OK) != -1) { Theme::theme_dir = fs::path(theme_path); break; } } } //? Config init { vector load_warnings; Config::load(Config::conf_file, load_warnings); if (Config::current_boxes.empty()) Config::check_boxes(Config::getS("shown_boxes")); Config::set("lowcolor", (Global::arg_low_color ? true : not Config::getB("truecolor"))); if (Global::debug) { Logger::set("DEBUG"); Logger::debug("Starting in DEBUG mode!"); } else Logger::set(Config::getS("log_level")); Logger::info("Logger set to " + (Global::debug ? "DEBUG" : Config::getS("log_level"))); for (const auto& err_str : load_warnings) Logger::warning(err_str); } //? Try to find and set a UTF-8 locale if (std::setlocale(LC_ALL, "") != NULL and not s_contains((string)std::setlocale(LC_ALL, ""), ";") and str_to_upper(s_replace((string)std::setlocale(LC_ALL, ""), "-", "")).ends_with("UTF8")) { Logger::debug("Using locale " + (string)std::setlocale(LC_ALL, "")); } else { string found; bool set_failure = false; for (const auto loc_env : array{"LANG", "LC_ALL"}) { if (std::getenv(loc_env) != NULL and str_to_upper(s_replace((string)std::getenv(loc_env), "-", "")).ends_with("UTF8")) { found = std::getenv(loc_env); if (std::setlocale(LC_ALL, found.c_str()) == NULL) { set_failure = true; Logger::warning("Failed to set locale " + found + " continuing anyway."); } } } if (found.empty()) { if (setenv("LC_ALL", "", 1) == 0 and setenv("LANG", "", 1) == 0) { try { if (const auto loc = std::locale("").name(); not loc.empty() and loc != "*") { for (auto& l : ssplit(loc, ';')) { if (str_to_upper(s_replace(l, "-", "")).ends_with("UTF8")) { found = l.substr(l.find('=') + 1); if (std::setlocale(LC_ALL, found.c_str()) != NULL) { break; } } } } } catch (...) { found.clear(); } } } #ifdef __APPLE__ if (found.empty()) { CFLocaleRef cflocale = CFLocaleCopyCurrent(); CFStringRef id_value = (CFStringRef)CFLocaleGetValue(cflocale, kCFLocaleIdentifier); auto loc_id = CFStringGetCStringPtr(id_value, kCFStringEncodingUTF8); CFRelease(cflocale); std::string cur_locale = (loc_id != nullptr ? loc_id : ""); if (cur_locale.empty()) { Logger::warning("No UTF-8 locale detected! Some symbols might not display correctly."); } else if (std::setlocale(LC_ALL, string(cur_locale + ".UTF-8").c_str()) != NULL) { Logger::debug("Setting LC_ALL=" + cur_locale + ".UTF-8"); } else if(std::setlocale(LC_ALL, "en_US.UTF-8") != NULL) { Logger::debug("Setting LC_ALL=en_US.UTF-8"); } else { Logger::warning("Failed to set macos locale, continuing anyway."); } } #else if (found.empty() and Global::utf_force) Logger::warning("No UTF-8 locale detected! Forcing start with --utf-force argument."); else if (found.empty()) { Global::exit_error_msg = "No UTF-8 locale detected!\nUse --utf-force argument to force start if you're sure your terminal can handle it."; clean_quit(1); } #endif else if (not set_failure) Logger::debug("Setting LC_ALL=" + found); } //? Initialize terminal and set options if (not Term::init()) { Global::exit_error_msg = "No tty detected!\nbtop++ needs an interactive shell to run."; clean_quit(1); } if (Term::current_tty != "unknown") Logger::info("Running on " + Term::current_tty); if (not Global::arg_tty and Config::getB("force_tty")) { Config::set("tty_mode", true); Logger::info("Forcing tty mode: setting 16 color mode and using tty friendly graph symbols"); } #ifndef __APPLE__ else if (not Global::arg_tty and Term::current_tty.starts_with("/dev/tty")) { Config::set("tty_mode", true); Logger::info("Real tty detected: setting 16 color mode and using tty friendly graph symbols"); } #endif //? Check for valid terminal dimensions { int t_count = 0; while (Term::width <= 0 or Term::width > 10000 or Term::height <= 0 or Term::height > 10000) { sleep_ms(10); Term::refresh(); if (++t_count == 100) { Global::exit_error_msg = "Failed to get size of terminal!"; clean_quit(1); } } } //? Platform dependent init and error check try { Shared::init(); } catch (const std::exception& e) { Global::exit_error_msg = "Exception in Shared::init() -> " + (string)e.what(); clean_quit(1); } //? Update list of available themes and generate the selected theme Theme::updateThemes(); Theme::setTheme(); //? Setup signal handlers for CTRL-C, CTRL-Z, resume and terminal resize std::atexit(_exit_handler); std::signal(SIGINT, _signal_handler); std::signal(SIGTSTP, _signal_handler); std::signal(SIGCONT, _signal_handler); std::signal(SIGWINCH, _signal_handler); //? Start runner thread Runner::thread_sem_init(); if (pthread_create(&Runner::runner_id, NULL, &Runner::_runner, NULL) != 0) { Global::exit_error_msg = "Failed to create _runner thread!"; clean_quit(1); } else { Global::_runner_started = true; } //? Calculate sizes of all boxes Config::presetsValid(Config::getS("presets")); if (Global::arg_preset >= 0) { Config::current_preset = min(Global::arg_preset, (int)Config::preset_list.size() - 1); Config::apply_preset(Config::preset_list.at(Config::current_preset)); } { const auto [x, y] = Term::get_min_size(Config::getS("shown_boxes")); if (Term::height < y or Term::width < x) { term_resize(true); Global::resized = false; Input::interrupt = false; } } Draw::calcSizes(); //? Print out box outlines cout << Term::sync_start << Cpu::box << Mem::box << Net::box << Proc::box << Term::sync_end << flush; //? ------------------------------------------------ MAIN LOOP ---------------------------------------------------- uint64_t update_ms = Config::getI("update_ms"); auto future_time = time_ms(); try { while (not true not_eq not false) { //? Check for exceptions in secondary thread and exit with fail signal if true if (Global::thread_exception) clean_quit(1); else if (Global::should_quit) clean_quit(0); else if (Global::should_sleep) { Global::should_sleep = false; _sleep(); } //? Make sure terminal size hasn't changed (in case of SIGWINCH not working properly) term_resize(Global::resized); //? Trigger secondary thread to redraw if terminal has been resized if (Global::resized) { Draw::calcSizes(); Draw::update_clock(true); Global::resized = false; if (Menu::active) Menu::process(); else Runner::run("all", true, true); atomic_wait_for(Runner::active, true, 1000); } //? Update clock if needed if (Draw::update_clock() and not Menu::active) { Runner::run("clock"); } //? Start secondary collect & draw thread at the interval set by config value if (time_ms() >= future_time and not Global::resized) { Runner::run("all"); update_ms = Config::getI("update_ms"); future_time = time_ms() + update_ms; } //? Loop over input polling and input action processing for (auto current_time = time_ms(); current_time < future_time; current_time = time_ms()) { //? Check for external clock changes and for changes to the update timer if (std::cmp_not_equal(update_ms, Config::getI("update_ms"))) { update_ms = Config::getI("update_ms"); future_time = time_ms() + update_ms; } else if (future_time - current_time > update_ms) future_time = current_time; //? Poll for input and process any input detected else if (Input::poll(min((uint64_t)1000, future_time - current_time))) { if (not Runner::active) Config::unlock(); if (Menu::active) Menu::process(Input::get()); else Input::process(Input::get()); } //? Break the loop at 1000ms intervals or if input polling was interrupted else break; } } } catch (const std::exception& e) { Global::exit_error_msg = "Exception in main loop -> " + (string)e.what(); clean_quit(1); } } btop-1.2.3/src/btop_config.cpp000066400000000000000000000516311420276253000162540ustar00rootroot00000000000000/* 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. indent = tab tab-size = 4 */ #include #include #include #include #include #include #include #include using std::array, std::atomic, std::string_view, std::string_literals::operator""s; namespace fs = std::filesystem; namespace rng = std::ranges; using namespace Tools; //* Functions and variables for reading and writing the btop config file namespace Config { atomic locked (false); atomic writelock (false); bool write_new; const vector> descriptions = { {"color_theme", "#* Name of a btop++/bpytop/bashtop formatted \".theme\" file, \"Default\" and \"TTY\" for builtin themes.\n" "#* Themes should be placed in \"../share/btop/themes\" relative to binary or \"$HOME/.config/btop/themes\""}, {"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, will convert 24-bit colors to 256 color (6x6x6 color cube) if false."}, {"force_tty", "#* Set to true to force tty mode regardless if a real tty has been detected or not.\n" "#* Will force 16-color mode and TTY theme, set all graph symbols to \"tty\" and swap out other non tty friendly symbols."}, {"presets", "#* Define presets for the layout of the boxes. Preset 0 is always all boxes shown with default settings. Max 9 presets.\n" "#* Format: \"box_name:P:G,box_name:P:G\" P=(0 or 1) for alternate positions, G=graph symbol to use for box.\n" "#* Use withespace \" \" as separator between different presets.\n" "#* Example: \"cpu:0:default,mem:0:tty,proc:1:default cpu:0:braille,proc:0:tty\""}, {"vim_keys", "#* Set to True to enable \"h,j,k,l\" keys for directional control in lists.\n" "#* Conflicting keys for h:\"help\" and k:\"kill\" is accessible while holding shift."}, {"rounded_corners", "#* Rounded corners on boxes, is ignored if TTY mode is ON."}, {"graph_symbol", "#* Default symbols to use for graph creation, \"braille\", \"block\" or \"tty\".\n" "#* \"braille\" offers the highest resolution but might not be included in all fonts.\n" "#* \"block\" has half the resolution of braille but uses more common characters.\n" "#* \"tty\" uses only 3 different symbols but will work with most fonts and should work in a real TTY.\n" "#* Note that \"tty\" only has half the horizontal resolution of the other two, so will show a shorter historical view."}, {"graph_symbol_cpu", "# Graph symbol to use for graphs in cpu box, \"default\", \"braille\", \"block\" or \"tty\"."}, {"graph_symbol_mem", "# Graph symbol to use for graphs in cpu box, \"default\", \"braille\", \"block\" or \"tty\"."}, {"graph_symbol_net", "# Graph symbol to use for graphs in cpu box, \"default\", \"braille\", \"block\" or \"tty\"."}, {"graph_symbol_proc", "# Graph symbol to use for graphs in cpu box, \"default\", \"braille\", \"block\" or \"tty\"."}, {"shown_boxes", "#* Manually set which boxes to show. Available values are \"cpu mem net proc\", separate values with whitespace."}, {"update_ms", "#* Update time in milliseconds, recommended 2000 ms or above for better sample times for graphs."}, {"proc_sorting", "#* Processes sorting, \"pid\" \"program\" \"arguments\" \"threads\" \"user\" \"memory\" \"cpu lazy\" \"cpu responsive\",\n" "#* \"cpu lazy\" sorts top process over time (easier to follow), \"cpu responsive\" updates top process directly."}, {"proc_reversed", "#* Reverse sorting order, True or False."}, {"proc_tree", "#* Show processes as a tree."}, {"proc_colors", "#* Use the cpu graph colors in the process list."}, {"proc_gradient", "#* Use a darkening gradient in the process list."}, {"proc_per_core", "#* If process cpu usage should be of the core it's running on or usage of the total available cpu power."}, {"proc_mem_bytes", "#* Show process memory as bytes instead of percent."}, {"proc_info_smaps", "#* Use /proc/[pid]/smaps for memory information in the process info box (very slow but more accurate)"}, {"proc_left", "#* Show proc box on left side of screen instead of right."}, {"cpu_graph_upper", "#* Sets the CPU stat shown in upper half of the CPU graph, \"total\" is always available.\n" "#* Select from a list of detected attributes from the options menu."}, {"cpu_graph_lower", "#* Sets the CPU stat shown in lower half of the CPU graph, \"total\" is always available.\n" "#* Select from a list of detected attributes from the options menu."}, {"cpu_invert_lower", "#* Toggles if the lower CPU graph should be inverted."}, {"cpu_single_graph", "#* Set to True to completely disable the lower CPU graph."}, {"cpu_bottom", "#* Show cpu box at bottom of screen instead of top."}, {"show_uptime", "#* Shows the system uptime in the CPU box."}, {"check_temp", "#* Show cpu temperature."}, {"cpu_sensor", "#* Which sensor to use for cpu temperature, use options menu to select from list of available sensors."}, {"show_coretemp", "#* Show temperatures for cpu cores also if check_temp is True and sensors has been found."}, {"cpu_core_map", "#* Set a custom mapping between core and coretemp, can be needed on certain cpus to get correct temperature for correct core.\n" "#* Use lm-sensors or similar to see which cores are reporting temperatures on your machine.\n" "#* Format \"x:y\" x=core with wrong temp, y=core with correct temp, use space as separator between multiple entries.\n" "#* Example: \"4:0 5:1 6:3\""}, {"temp_scale", "#* Which temperature scale to use, available values: \"celsius\", \"fahrenheit\", \"kelvin\" and \"rankine\"."}, {"base_10_sizes", "#* Use base 10 for bits/bytes sizes, KB = 1000 instead of KiB = 1024."}, {"show_cpu_freq", "#* Show CPU frequency."}, {"clock_format", "#* Draw a clock at top of screen, formatting according to strftime, empty string to disable.\n" "#* Special formatting: /host = hostname | /user = username | /uptime = system uptime"}, {"background_update", "#* Update main ui in background when menus are showing, set this to false if the menus is flickering too much for comfort."}, {"custom_cpu_name", "#* Custom cpu model name, empty string to disable."}, {"disks_filter", "#* Optional filter for shown disks, should be full path of a mountpoint, separate multiple values with whitespace \" \".\n" "#* Begin line with \"exclude=\" to change to exclude filter, otherwise defaults to \"most include\" filter. Example: disks_filter=\"exclude=/boot /home/user\"."}, {"mem_graphs", "#* Show graphs instead of meters for memory values."}, {"mem_below_net", "#* Show mem box below net box instead of above."}, {"show_swap", "#* If swap memory should be shown in memory box."}, {"swap_disk", "#* Show swap as a disk, ignores show_swap value above, inserts itself after first disk."}, {"show_disks", "#* If mem box should be split to also show disks info."}, {"only_physical", "#* Filter out non physical disks. Set this to False to include network disks, RAM disks and similar."}, {"use_fstab", "#* Read disks list from /etc/fstab. This also disables only_physical."}, {"disk_free_priv", "#* Set to true to show available disk space for privileged users."}, {"show_io_stat", "#* Toggles if io activity % (disk busy time) should be shown in regular disk usage view."}, {"io_mode", "#* Toggles io mode for disks, showing big graphs for disk read/write speeds."}, {"io_graph_combined", "#* Set to True to show combined read/write io graphs in io mode."}, {"io_graph_speeds", "#* Set the top speed for the io graphs in MiB/s (100 by default), use format \"mountpoint:speed\" separate disks with whitespace \" \".\n" "#* Example: \"/mnt/media:100 /:20 /boot:1\"."}, {"net_download", "#* Set fixed values for network graphs in Mebibits. Is only used if net_auto is also set to False."}, {"net_upload", ""}, {"net_auto", "#* Use network graphs auto rescaling mode, ignores any values set above and rescales down to 10 Kibibytes at the lowest."}, {"net_sync", "#* Sync the auto scaling for download and upload to whichever currently has the highest scale."}, {"net_iface", "#* Starts with the Network Interface specified here."}, {"show_battery", "#* Show battery stats in top right if battery is present."}, {"selected_battery", "#* Which battery to use if multiple are present. \"Auto\" for auto detection."}, {"log_level", "#* Set loglevel for \"~/.config/btop/btop.log\" levels are: \"ERROR\" \"WARNING\" \"INFO\" \"DEBUG\".\n" "#* The level set includes all lower levels, i.e. \"DEBUG\" will show all logging info."} }; unordered_flat_map strings = { {"color_theme", "Default"}, {"shown_boxes", "cpu mem net proc"}, {"graph_symbol", "braille"}, {"presets", "cpu:1:default,proc:0:default cpu:0:default,mem:0:default,net:0:default cpu:0:block,net:0:tty"}, {"graph_symbol_cpu", "default"}, {"graph_symbol_mem", "default"}, {"graph_symbol_net", "default"}, {"graph_symbol_proc", "default"}, {"proc_sorting", "cpu lazy"}, {"cpu_graph_upper", "total"}, {"cpu_graph_lower", "total"}, {"cpu_sensor", "Auto"}, {"selected_battery", "Auto"}, {"cpu_core_map", ""}, {"temp_scale", "celsius"}, {"clock_format", "%X"}, {"custom_cpu_name", ""}, {"disks_filter", ""}, {"io_graph_speeds", ""}, {"net_iface", ""}, {"log_level", "WARNING"}, {"proc_filter", ""}, {"proc_command", ""}, {"selected_name", ""}, }; unordered_flat_map stringsTmp; unordered_flat_map bools = { {"theme_background", true}, {"truecolor", true}, {"rounded_corners", true}, {"proc_reversed", false}, {"proc_tree", false}, {"proc_colors", true}, {"proc_gradient", true}, {"proc_per_core", false}, {"proc_mem_bytes", true}, {"proc_info_smaps", false}, {"proc_left", false}, {"cpu_invert_lower", true}, {"cpu_single_graph", false}, {"cpu_bottom", false}, {"show_uptime", true}, {"check_temp", true}, {"show_coretemp", true}, {"show_cpu_freq", true}, {"background_update", true}, {"mem_graphs", true}, {"mem_below_net", false}, {"show_swap", true}, {"swap_disk", true}, {"show_disks", true}, {"only_physical", true}, {"use_fstab", true}, {"show_io_stat", true}, {"io_mode", false}, {"base_10_sizes", false}, {"io_graph_combined", false}, {"net_auto", true}, {"net_sync", false}, {"show_battery", true}, {"vim_keys", false}, {"tty_mode", false}, {"disk_free_priv", false}, {"force_tty", false}, {"lowcolor", false}, {"show_detailed", false}, {"proc_filtering", false}, }; unordered_flat_map boolsTmp; unordered_flat_map ints = { {"update_ms", 2000}, {"net_download", 100}, {"net_upload", 100}, {"detailed_pid", 0}, {"selected_pid", 0}, {"proc_start", 0}, {"proc_selected", 0}, {"proc_last_selected", 0}, }; unordered_flat_map intsTmp; bool _locked(const string& name) { atomic_wait(writelock, true); if (not write_new and rng::find_if(descriptions, [&name](const auto& a) { return a.at(0) == name; }) != descriptions.end()) write_new = true; return locked.load(); } fs::path conf_dir; fs::path conf_file; vector available_batteries = {"Auto"}; vector current_boxes; vector preset_list = {"cpu:0:default,mem:0:default,net:0:default,proc:0:default"}; int current_preset = -1; bool presetsValid(const string& presets) { vector new_presets = {preset_list.at(0)}; for (int x = 0; const auto& preset : ssplit(presets)) { if (++x > 9) { validError = "Too many presets entered!"; return false; } for (int y = 0; const auto& box : ssplit(preset, ',')) { if (++y > 4) { validError = "Too many boxes entered for preset!"; return false; } const auto& vals = ssplit(box, ':'); if (vals.size() != 3) { validError = "Malformatted preset in config value presets!"; return false; } if (not is_in(vals.at(0), "cpu", "mem", "net", "proc")) { validError = "Invalid box name in config value presets!"; return false; } if (not is_in(vals.at(1), "0", "1")) { validError = "Invalid position value in config value presets!"; return false; } if (not v_contains(valid_graph_symbols_def, vals.at(2))) { validError = "Invalid graph name in config value presets!"; return false; } } new_presets.push_back(preset); } preset_list = move(new_presets); return true; } //* Apply selected preset void apply_preset(const string& preset) { string boxes; for (const auto& box : ssplit(preset, ',')) { const auto& vals = ssplit(box, ':'); boxes += vals.at(0) + ' '; } if (not boxes.empty()) boxes.pop_back(); auto min_size = Term::get_min_size(boxes); if (Term::width < min_size.at(0) or Term::height < min_size.at(1)) { return; } for (const auto& box : ssplit(preset, ',')) { const auto& vals = ssplit(box, ':'); if (vals.at(0) == "cpu") set("cpu_bottom", (vals.at(1) == "0" ? false : true)); else if (vals.at(0) == "mem") set("mem_below_net", (vals.at(1) == "0" ? false : true)); else if (vals.at(0) == "proc") set("proc_left", (vals.at(1) == "0" ? false : true)); set("graph_symbol_" + vals.at(0), vals.at(2)); } if (check_boxes(boxes)) set("shown_boxes", boxes); } void lock() { atomic_wait(writelock); locked = true; } string validError; bool intValid(const string& name, const string& value) { int i_value; try { i_value = stoi(value); } catch (const std::invalid_argument&) { validError = "Invalid numerical value!"; return false; } catch (const std::out_of_range&) { validError = "Value out of range!"; return false; } catch (const std::exception& e) { validError = (string)e.what(); return false; } if (name == "update_ms" and i_value < 100) validError = "Config value update_ms set too low (<100)."; else if (name == "update_ms" and i_value > 86400000) validError = "Config value update_ms set too high (>86400000)."; else return true; return false; } bool stringValid(const string& name, const string& value) { if (name == "log_level" and not v_contains(Logger::log_levels, value)) validError = "Invalid log_level: " + value; else if (name == "graph_symbol" and not v_contains(valid_graph_symbols, value)) validError = "Invalid graph symbol identifier: " + value; else if (name.starts_with("graph_symbol_") and (value != "default" and not v_contains(valid_graph_symbols, value))) validError = "Invalid graph symbol identifier for" + name + ": " + value; else if (name == "shown_boxes" and not value.empty() and not check_boxes(value)) validError = "Invalid box name(s) in shown_boxes!"; else if (name == "presets" and not presetsValid(value)) return false; else if (name == "cpu_core_map") { const auto maps = ssplit(value); bool all_good = true; for (const auto& map : maps) { const auto map_split = ssplit(map, ':'); if (map_split.size() != 2) all_good = false; else if (not isint(map_split.at(0)) or not isint(map_split.at(1))) all_good = false; if (not all_good) { validError = "Invalid formatting of cpu_core_map!"; return false; } } return true; } else if (name == "io_graph_speeds") { const auto maps = ssplit(value); bool all_good = true; for (const auto& map : maps) { const auto map_split = ssplit(map, ':'); if (map_split.size() != 2) all_good = false; else if (map_split.at(0).empty() or not isint(map_split.at(1))) all_good = false; if (not all_good) { validError = "Invalid formatting of io_graph_speeds!"; return false; } } return true; } else return true; return false; } string getAsString(const string& name) { if (bools.contains(name)) return (bools.at(name) ? "True" : "False"); else if (ints.contains(name)) return to_string(ints.at(name)); else if (strings.contains(name)) return strings.at(name); return ""; } void flip(const string& name) { if (_locked(name)) { if (boolsTmp.contains(name)) boolsTmp.at(name) = not boolsTmp.at(name); else boolsTmp.insert_or_assign(name, (not bools.at(name))); } else bools.at(name) = not bools.at(name); } void unlock() { if (not locked) return; atomic_wait(Runner::active); atomic_lock lck(writelock, true); try { if (Proc::shown) { ints.at("selected_pid") = Proc::selected_pid; strings.at("selected_name") = Proc::selected_name; ints.at("proc_start") = Proc::start; ints.at("proc_selected") = Proc::selected; } for (auto& item : stringsTmp) { strings.at(item.first) = item.second; } stringsTmp.clear(); for (auto& item : intsTmp) { ints.at(item.first) = item.second; } intsTmp.clear(); for (auto& item : boolsTmp) { bools.at(item.first) = item.second; } boolsTmp.clear(); } catch (const std::exception& e) { Global::exit_error_msg = "Exception during Config::unlock() : " + (string)e.what(); clean_quit(1); } locked = false; } bool check_boxes(const string& boxes) { auto new_boxes = ssplit(boxes); for (auto& box : new_boxes) { if (not v_contains(valid_boxes, box)) return false; } current_boxes = move(new_boxes); return true; } void toggle_box(const string& box) { auto old_boxes = current_boxes; auto box_pos = rng::find(current_boxes, box); if (box_pos == current_boxes.end()) current_boxes.push_back(box); else current_boxes.erase(box_pos); string new_boxes; if (not current_boxes.empty()) { for (const auto& b : current_boxes) new_boxes += b + ' '; new_boxes.pop_back(); } auto min_size = Term::get_min_size(new_boxes); if (Term::width < min_size.at(0) or Term::height < min_size.at(1)) { current_boxes = old_boxes; return; } Config::set("shown_boxes", new_boxes); } void load(const fs::path& conf_file, vector& load_warnings) { if (conf_file.empty()) return; else if (not fs::exists(conf_file)) { write_new = true; return; } std::ifstream cread(conf_file); if (cread.good()) { vector valid_names; for (auto &n : descriptions) valid_names.push_back(n[0]); if (string v_string; cread.peek() != '#' or (getline(cread, v_string, '\n') and not s_contains(v_string, Global::Version))) write_new = true; while (not cread.eof()) { cread >> std::ws; if (cread.peek() == '#') { cread.ignore(SSmax, '\n'); continue; } string name, value; getline(cread, name, '='); if (name.ends_with(' ')) name = trim(name); if (not v_contains(valid_names, name)) { cread.ignore(SSmax, '\n'); continue; } cread >> std::ws; if (bools.contains(name)) { cread >> value; if (not isbool(value)) load_warnings.push_back("Got an invalid bool value for config name: " + name); else bools.at(name) = stobool(value); } else if (ints.contains(name)) { cread >> value; if (not isint(value)) load_warnings.push_back("Got an invalid integer value for config name: " + name); else if (not intValid(name, value)) { load_warnings.push_back(validError); } else ints.at(name) = stoi(value); } else if (strings.contains(name)) { if (cread.peek() == '"') { cread.ignore(1); getline(cread, value, '"'); } else cread >> value; if (not stringValid(name, value)) load_warnings.push_back(validError); else strings.at(name) = value; } cread.ignore(SSmax, '\n'); } if (not load_warnings.empty()) write_new = true; } } void write() { if (conf_file.empty() or not write_new) return; Logger::debug("Writing new config file"); if (geteuid() != Global::real_uid and seteuid(Global::real_uid) != 0) return; std::ofstream cwrite(conf_file, std::ios::trunc); if (cwrite.good()) { cwrite << "#? Config file for btop v. " << Global::Version; for (auto [name, description] : descriptions) { cwrite << "\n\n" << (description.empty() ? "" : description + "\n") << name << " = "; if (strings.contains(name)) cwrite << "\"" << strings.at(name) << "\""; else if (ints.contains(name)) cwrite << ints.at(name); else if (bools.contains(name)) cwrite << (bools.at(name) ? "True" : "False"); } } } } btop-1.2.3/src/btop_config.hpp000066400000000000000000000071011420276253000162520ustar00rootroot00000000000000/* 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. indent = tab tab-size = 4 */ #pragma once #include #include #include #include using std::string, std::vector, robin_hood::unordered_flat_map; //* Functions and variables for reading and writing the btop config file namespace Config { extern std::filesystem::path conf_dir; extern std::filesystem::path conf_file; extern unordered_flat_map strings; extern unordered_flat_map stringsTmp; extern unordered_flat_map bools; extern unordered_flat_map boolsTmp; extern unordered_flat_map ints; extern unordered_flat_map intsTmp; const vector valid_graph_symbols = { "braille", "block", "tty" }; const vector valid_graph_symbols_def = { "default", "braille", "block", "tty" }; const vector valid_boxes = { "cpu", "mem", "net", "proc" }; const vector temp_scales = { "celsius", "fahrenheit", "kelvin", "rankine" }; extern vector current_boxes; extern vector preset_list; extern vector available_batteries; extern int current_preset; //* Check if string only contains space separated valid names for boxes bool check_boxes(const string& boxes); //* Toggle box and update config string shown_boxes void toggle_box(const string& box); //* Parse and setup config value presets bool presetsValid(const string& presets); //* Apply selected preset void apply_preset(const string& preset); bool _locked(const string& name); //* Return bool for config key inline const bool& getB(const string& name) { return bools.at(name); } //* Return integer for config key inline const int& getI(const string& name) { return ints.at(name); } //* Return string for config key inline const string& getS(const string& name) { return strings.at(name); } string getAsString(const string& name); extern string validError; bool intValid(const string& name, const string& value); bool stringValid(const string& name, const string& value); //* Set config key to bool inline void set(const string& name, const bool& value) { if (_locked(name)) boolsTmp.insert_or_assign(name, value); else bools.at(name) = value; } //* Set config key to int inline void set(const string& name, const int& value) { if (_locked(name)) intsTmp.insert_or_assign(name, value); else ints.at(name) = value; } //* Set config key to string inline void set(const string& name, const string& value) { if (_locked(name)) stringsTmp.insert_or_assign(name, value); else strings.at(name) = value; } //* Flip config key bool void flip(const string& name); //* Lock config and cache changes until unlocked void lock(); //* Unlock config and write any cached values to config void unlock(); //* Load the config file from disk void load(const std::filesystem::path& conf_file, vector& load_warnings); //* Write the config file to disk void write(); } btop-1.2.3/src/btop_draw.cpp000066400000000000000000002123071420276253000157430ustar00rootroot00000000000000/* 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. indent = tab tab-size = 4 */ #include #include #include #include #include #include #include #include #include #include #include using std::round, std::views::iota, std::string_literals::operator""s, std::clamp, std::array, std::floor, std::max, std::min, std::to_string, std::cmp_equal, std::cmp_less, std::cmp_greater, std::cmp_less_equal; using namespace Tools; namespace rng = std::ranges; namespace Symbols { const string meter = "■"; const array superscript = { "⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹" }; const unordered_flat_map> graph_symbols = { { "braille_up", { " ", "⢀", "⢠", "⢰", "⢸", "⡀", "⣀", "⣠", "⣰", "⣸", "⡄", "⣄", "⣤", "⣴", "⣼", "⡆", "⣆", "⣦", "⣶", "⣾", "⡇", "⣇", "⣧", "⣷", "⣿" }}, {"braille_down", { " ", "⠈", "⠘", "⠸", "⢸", "⠁", "⠉", "⠙", "⠹", "⢹", "⠃", "⠋", "⠛", "⠻", "⢻", "⠇", "⠏", "⠟", "⠿", "⢿", "⡇", "⡏", "⡟", "⡿", "⣿" }}, {"block_up", { " ", "▗", "▗", "▐", "▐", "▖", "▄", "▄", "▟", "▟", "▖", "▄", "▄", "▟", "▟", "▌", "▙", "▙", "█", "█", "▌", "▙", "▙", "█", "█" }}, {"block_down", { " ", "▝", "▝", "▐", "▐", "▘", "▀", "▀", "▜", "▜", "▘", "▀", "▀", "▜", "▜", "▌", "▛", "▛", "█", "█", "▌", "▛", "▛", "█", "█" }}, {"tty_up", { " ", "░", "░", "▒", "▒", "░", "░", "▒", "▒", "█", "░", "▒", "▒", "▒", "█", "▒", "▒", "▒", "█", "█", "▒", "█", "█", "█", "█" }}, {"tty_down", { " ", "░", "░", "▒", "▒", "░", "░", "▒", "▒", "█", "░", "▒", "▒", "▒", "█", "▒", "▒", "▒", "█", "█", "▒", "█", "█", "█", "█" }} }; } namespace Draw { string banner_gen(int y, int x, bool centered, bool redraw) { static string banner; static size_t width = 0; if (redraw) banner.clear(); if (banner.empty()) { string b_color, bg, fg, oc, letter; auto& lowcolor = Config::getB("lowcolor"); auto& tty_mode = Config::getB("tty_mode"); for (size_t z = 0; const auto& line : Global::Banner_src) { if (const auto w = ulen(line[1]); w > width) width = w; if (tty_mode) { fg = (z > 2) ? "\x1b[31m" : "\x1b[91m"; bg = (z > 2) ? "\x1b[90m" : "\x1b[37m"; } else { fg = Theme::hex_to_color(line[0], lowcolor); int bg_i = 120 - z * 12; bg = Theme::dec_to_color(bg_i, bg_i, bg_i, lowcolor); } for (size_t i = 0; i < line[1].size(); i += 3) { if (line[1][i] == ' ') { letter = ' '; i -= 2; } else letter = line[1].substr(i, 3); b_color = (letter == "█") ? fg : bg; if (b_color != oc) banner += b_color; banner += letter; oc = b_color; } if (++z < Global::Banner_src.size()) banner += Mv::l(ulen(line[1])) + Mv::d(1); } banner += Mv::r(18 - Global::Version.size()) + Theme::c("main_fg") + Fx::b + Fx::i + "v" + Global::Version + Fx::reset; } if (redraw) return ""; return (centered ? Mv::to(y, Term::width / 2 - width / 2) : Mv::to(y, x)) + banner; } TextEdit::TextEdit() {} TextEdit::TextEdit(string text, bool numeric) : numeric(numeric), text(text) { pos = this->text.size(); upos = ulen(this->text); } bool TextEdit::command(const string& key) { if (key == "left" and upos > 0) { upos--; pos = uresize(text, upos).size(); } else if (key == "right" and pos < text.size()) { upos++; pos = uresize(text, upos).size(); } else if (key == "home" and pos > 0) { pos = upos = 0; } else if (key == "end" and pos < text.size()) { pos = text.size(); upos = ulen(text); } else if (key == "backspace" and pos > 0) { if (pos == text.size()) { text = uresize(text, --upos); pos = text.size(); } else { const string first = uresize(text, --upos); pos = first.size(); text = first + luresize(text.substr(pos), ulen(text) - upos - 1); } } else if (key == "delete" and pos < text.size()) { const string first = uresize(text, upos + 1); text = uresize(first, ulen(first) - 1) + text.substr(first.size()); } else if (key == "space" and not numeric) { text.insert(pos++, 1, ' '); upos++; } else if (ulen(key) == 1 and text.size() < text.max_size() - 20) { if (numeric and not isint(key)) return false; if (key.size() == 1) { text.insert(pos++, 1, key.at(0)); upos++; } else { const string first = uresize(text, upos) + key; text = first + text.substr(pos); upos++; pos = first.size(); } } else return false; return true; } string TextEdit::operator()(const size_t limit) { if (limit > 0 and ulen(text) + 1 > limit) { try { const size_t half = (size_t)round((double)limit / 2); string first; if (upos + half > ulen(text)) first = luresize(text.substr(0, pos), limit - (ulen(text) - upos)); else if (upos - half < 1) first = text.substr(0, pos); else first = luresize(text.substr(0, pos), half); return first + Fx::bl + "█" + Fx::ubl + uresize(text.substr(pos), limit - ulen(first)); } catch (const std::exception& e) { Logger::error("In TextEdit::operator() : " + (string)e.what()); } } return text.substr(0, pos) + Fx::bl + "█" + Fx::ubl + text.substr(pos); } void TextEdit::clear() { this->text.clear(); } string createBox(const int x, const int y, const int width, const int height, string line_color, const bool fill, const string title, const string title2, const int num) { string out; if (line_color.empty()) line_color = Theme::c("div_line"); const auto& tty_mode = Config::getB("tty_mode"); const auto& rounded = Config::getB("rounded_corners"); const string numbering = (num == 0) ? "" : Theme::c("hi_fg") + (tty_mode ? std::to_string(num) : Symbols::superscript.at(clamp(num, 0, 9))); const auto& right_up = (tty_mode or not rounded ? Symbols::right_up : Symbols::round_right_up); const auto& left_up = (tty_mode or not rounded ? Symbols::left_up : Symbols::round_left_up); const auto& right_down = (tty_mode or not rounded ? Symbols::right_down : Symbols::round_right_down); const auto& left_down = (tty_mode or not rounded ? Symbols::left_down : Symbols::round_left_down); out = Fx::reset + line_color; //? Draw horizontal lines for (const int& hpos : {y, y + height - 1}) { out += Mv::to(hpos, x) + Symbols::h_line * (width - 1); } //? Draw vertical lines and fill if enabled for (const int& hpos : iota(y + 1, y + height - 1)) { out += Mv::to(hpos, x) + Symbols::v_line + ((fill) ? string(width - 2, ' ') : Mv::r(width - 2)) + Symbols::v_line; } //? Draw corners out += Mv::to(y, x) + left_up + Mv::to(y, x + width - 1) + right_up + Mv::to(y + height - 1, x) +left_down + Mv::to(y + height - 1, x + width - 1) + right_down; //? Draw titles if defined if (not title.empty()) { out += Mv::to(y, x + 2) + Symbols::title_left + Fx::b + numbering + Theme::c("title") + title + Fx::ub + line_color + Symbols::title_right; } if (not title2.empty()) { out += Mv::to(y + height - 1, x + 2) + Symbols::title_left_down + Fx::b + numbering + Theme::c("title") + title2 + Fx::ub + line_color + Symbols::title_right_down; } return out + Fx::reset + Mv::to(y + 1, x + 1); } bool update_clock(bool force) { const auto& clock_format = Config::getS("clock_format"); if (not Cpu::shown or clock_format.empty()) { if (clock_format.empty() and not Global::clock.empty()) Global::clock.clear(); return false; } static const unordered_flat_map clock_custom_format = { {"/user", Tools::username()}, {"/host", Tools::hostname()}, {"/uptime", ""} }; static time_t c_time = 0; static size_t clock_len = 0; static string clock_str; if (auto n_time = time(NULL); not force and n_time == c_time) return false; else { c_time = n_time; const auto new_clock = Tools::strf_time(clock_format); if (not force and new_clock == clock_str) return false; clock_str = new_clock; } auto& out = Global::clock; const auto& cpu_bottom = Config::getB("cpu_bottom"); const auto& x = Cpu::x; const auto y = (cpu_bottom ? Cpu::y + Cpu::height - 1 : Cpu::y); const auto& width = Cpu::width; const auto& title_left = (cpu_bottom ? Symbols::title_left_down : Symbols::title_left); const auto& title_right = (cpu_bottom ? Symbols::title_right_down : Symbols::title_right); for (const auto& [c_format, replacement] : clock_custom_format) { if (s_contains(clock_str, c_format)) { if (c_format == "/uptime") { string upstr = sec_to_dhms(system_uptime()); if (upstr.size() > 8) upstr.resize(upstr.size() - 3); clock_str = s_replace(clock_str, c_format, upstr); } else { clock_str = s_replace(clock_str, c_format, replacement); } } } clock_str = uresize(clock_str, std::max(10, width - 66 - (Term::width >= 100 and Config::getB("show_battery") and Cpu::has_battery ? 22 : 0))); out.clear(); if (clock_str.size() != clock_len) { if (not Global::resized and clock_len > 0) out = Mv::to(y, x+(width / 2)-(clock_len / 2)) + Fx::ub + Theme::c("cpu_box") + Symbols::h_line * clock_len; clock_len = clock_str.size(); } out += Mv::to(y, x+(width / 2)-(clock_len / 2)) + Fx::ub + Theme::c("cpu_box") + title_left + Theme::c("title") + Fx::b + clock_str + Theme::c("cpu_box") + Fx::ub + title_right; return true; } //* Meter class ------------------------------------------------------------------------------------------------------------> Meter::Meter() {} Meter::Meter(const int width, const string& color_gradient, const bool invert) : width(width), color_gradient(color_gradient), invert(invert) {} string Meter::operator()(int value) { if (width < 1) return ""; value = clamp(value, 0, 100); if (not cache.at(value).empty()) return cache.at(value); auto& out = cache.at(value); for (const int& i : iota(1, width + 1)) { int y = round((double)i * 100.0 / width); if (value >= y) out += Theme::g(color_gradient).at(invert ? 100 - y : y) + Symbols::meter; else { out += Theme::c("meter_bg") + Symbols::meter * (width + 1 - i); break; } } out += Fx::reset; return out; } //* Graph class ------------------------------------------------------------------------------------------------------------> void Graph::_create(const deque& data, int data_offset) { const bool mult = (data.size() - data_offset > 1); const auto& graph_symbol = Symbols::graph_symbols.at(symbol + '_' + (invert ? "down" : "up")); array result; const float mod = (height == 1) ? 0.3 : 0.1; long long data_value = 0; if (mult and data_offset > 0) { last = data.at(data_offset - 1); if (max_value > 0) last = clamp((last + offset) * 100 / max_value, 0ll, 100ll); } //? Horizontal iteration over values in for (const int& i : iota(data_offset, (int)data.size())) { // if (tty_mode and mult and i % 2 != 0) continue; if (not tty_mode and mult) current = not current; if (i < 0) { data_value = 0; last = 0; } else { data_value = data.at(i); if (max_value > 0) data_value = clamp((data_value + offset) * 100 / max_value, 0ll, 100ll); } //? Vertical iteration over height of graph for (const int& horizon : iota(0, height)) { const int cur_high = (height > 1) ? round(100.0 * (height - horizon) / height) : 100; const int cur_low = (height > 1) ? round(100.0 * (height - (horizon + 1)) / height) : 0; //? Calculate previous + current value to fit two values in 1 braille character for (int ai = 0; const auto& value : {last, data_value}) { const int clamp_min = (no_zero and horizon == height - 1 and not (mult and i == data_offset and ai == 0)) ? 1 : 0; if (value >= cur_high) result[ai++] = 4; else if (value <= cur_low) result[ai++] = clamp_min; else { result[ai++] = clamp((int)round((float)(value - cur_low) * 4 / (cur_high - cur_low) + mod), clamp_min, 4); } } //? Generate graph symbol from 5x5 2D vector graphs.at(current).at(horizon) += (height == 1 and result.at(0) + result.at(1) == 0) ? Mv::r(1) : graph_symbol.at((result.at(0) * 5 + result.at(1))); } if (mult and i >= 0) last = data_value; } last = data_value; out.clear(); if (height == 1) { if (not color_gradient.empty()) out += (last < 1 and not color_gradient.empty() ? Theme::c("inactive_fg") : Theme::g(color_gradient).at(clamp(last, 0ll, 100ll))); out += graphs.at(current).at(0); } else { for (const int& i : iota(1, height + 1)) { if (i > 1) out += Mv::d(1) + Mv::l(width); if (not color_gradient.empty()) out += (invert) ? Theme::g(color_gradient).at(i * 100 / height) : Theme::g(color_gradient).at(100 - ((i - 1) * 100 / height)); out += (invert) ? graphs.at(current).at(height - i) : graphs.at(current).at(i-1); } } if (not color_gradient.empty()) out += Fx::reset; } Graph::Graph() {} Graph::Graph(int width, int height, const string& color_gradient, const deque& data, const string& symbol, bool invert, bool no_zero, long long max_value, long long offset) : width(width), height(height), color_gradient(color_gradient), invert(invert), no_zero(no_zero), offset(offset) { if (Config::getB("tty_mode") or symbol == "tty") this->symbol = "tty"; else if (symbol != "default") this->symbol = symbol; else this->symbol = Config::getS("graph_symbol"); if (this->symbol == "tty") tty_mode = true; if (max_value == 0 and offset > 0) max_value = 100; this->max_value = max_value; const int value_width = (tty_mode ? data.size() : ceil((double)data.size() / 2)); int data_offset = (value_width > width) ? data.size() - width * (tty_mode ? 1 : 2) : 0; if (not tty_mode and (data.size() - data_offset) % 2 != 0) { data_offset--; } //? Populate the two switching graph vectors and fill empty space if data size < width for (const int& i : iota(0, height * 2)) { if (tty_mode and i % 2 != current) continue; graphs[(i % 2 != 0)].push_back((value_width < width) ? ((height == 1) ? Mv::r(1) : " "s) * (width - value_width) : ""); } if (data.size() == 0) return; this->_create(data, data_offset); } string& Graph::operator()(const deque& data, const bool data_same) { if (data_same) return out; //? Make room for new characters on graph if (not tty_mode) current = not current; for (const int& i : iota(0, height)) { if (graphs.at(current).at(i).at(1) == '[') graphs.at(current).at(i).erase(0, 4); else if (graphs.at(current).at(i).at(0) == ' ') graphs.at(current).at(i).erase(0, 1); else graphs.at(current).at(i).erase(0, 3); } this->_create(data, (int)data.size() - 1); return out; } string& Graph::operator()() { return out; } //*-------------------------------------------------------------------------------------------------------------------------> } namespace Cpu { int width_p = 100, height_p = 32; int min_width = 60, min_height = 8; int x = 1, y = 1, width = 20, height; int b_columns, b_column_size; int b_x, b_y, b_width, b_height; int graph_up_height; bool shown = true, redraw = true, mid_line = false; string box; Draw::Graph graph_upper; Draw::Graph graph_lower; Draw::Meter cpu_meter; vector core_graphs; vector temp_graphs; string draw(const cpu_info& cpu, const bool force_redraw, const bool data_same) { if (Runner::stopping) return ""; if (force_redraw) redraw = true; const bool show_temps = (Config::getB("check_temp") and got_sensors); auto& single_graph = Config::getB("cpu_single_graph"); const bool hide_cores = show_temps and (cpu_temp_only or not Config::getB("show_coretemp")); const int extra_width = (hide_cores ? max(6, 6 * b_column_size) : 0); auto& graph_up_field = Config::getS("cpu_graph_upper"); auto& graph_lo_field = Config::getS("cpu_graph_lower"); auto& tty_mode = Config::getB("tty_mode"); auto& graph_symbol = (tty_mode ? "tty" : Config::getS("graph_symbol_cpu")); auto& graph_bg = Symbols::graph_symbols.at((graph_symbol == "default" ? Config::getS("graph_symbol") + "_up" : graph_symbol + "_up")).at(6); auto& temp_scale = Config::getS("temp_scale"); auto& cpu_bottom = Config::getB("cpu_bottom"); const string& title_left = Theme::c("cpu_box") + (cpu_bottom ? Symbols::title_left_down : Symbols::title_left); const string& title_right = Theme::c("cpu_box") + (cpu_bottom ? Symbols::title_right_down : Symbols::title_right); static int bat_pos = 0, bat_len = 0; if (cpu.cpu_percent.at("total").empty() or cpu.core_percent.at(0).empty() or (show_temps and cpu.temp.at(0).empty())) return ""; string out; out.reserve(width * height); //* Redraw elements not needed to be updated every cycle if (redraw) { mid_line = (not single_graph and graph_up_field != graph_lo_field); graph_up_height = (single_graph ? height - 2 : ceil((double)(height - 2) / 2) - (mid_line and height % 2 != 0 ? 1 : 0)); const int graph_low_height = height - 2 - graph_up_height - (mid_line ? 1 : 0); const int button_y = cpu_bottom ? y + height - 1 : y; out += box; //? Buttons on title out += Mv::to(button_y, x + 10) + title_left + Theme::c("hi_fg") + Fx::b + 'm' + Theme::c("title") + "enu" + Fx::ub + title_right; Input::mouse_mappings["m"] = {button_y, x + 11, 1, 4}; out += Mv::to(button_y, x + 16) + title_left + Theme::c("hi_fg") + Fx::b + 'p' + Theme::c("title") + "reset " + (Config::current_preset < 0 ? "*" : to_string(Config::current_preset)) + Fx::ub + title_right; Input::mouse_mappings["p"] = {button_y, x + 17, 1, 8}; const string update = to_string(Config::getI("update_ms")) + "ms"; out += Mv::to(button_y, x + width - update.size() - 8) + title_left + Fx::b + Theme::c("hi_fg") + "- " + Theme::c("title") + update + Theme::c("hi_fg") + " +" + Fx::ub + title_right; Input::mouse_mappings["-"] = {button_y, x + width - (int)update.size() - 7, 1, 2}; Input::mouse_mappings["+"] = {button_y, x + width - 5, 1, 2}; //? Graphs & meters graph_upper = Draw::Graph{x + width - b_width - 3, graph_up_height, "cpu", cpu.cpu_percent.at(graph_up_field), graph_symbol, false, true}; cpu_meter = Draw::Meter{b_width - (show_temps ? 23 - (b_column_size <= 1 and b_columns == 1 ? 6 : 0) : 11), "cpu"}; if (not single_graph) graph_lower = Draw::Graph{x + width - b_width - 3, graph_low_height, "cpu", cpu.cpu_percent.at(graph_lo_field), graph_symbol, Config::getB("cpu_invert_lower"), true}; if (mid_line) { out += Mv::to(y + graph_up_height + 1, x) + Fx::ub + Theme::c("cpu_box") + Symbols::div_left + Theme::c("div_line") + Symbols::h_line * (width - b_width - 2) + Symbols::div_right + Mv::to(y + graph_up_height + 1, x + ((width - b_width) / 2) - ((graph_up_field.size() + graph_lo_field.size()) / 2) - 4) + Theme::c("main_fg") + graph_up_field + Mv::r(1) + "▲▼" + Mv::r(1) + graph_lo_field; } if (b_column_size > 0 or extra_width > 0) { core_graphs.clear(); for (const auto& core_data : cpu.core_percent) { core_graphs.emplace_back(5 * b_column_size + extra_width, 1, "", core_data, graph_symbol); } } if (show_temps) { temp_graphs.clear(); temp_graphs.emplace_back(5, 1, "", cpu.temp.at(0), graph_symbol, false, false, cpu.temp_max, -23); if (not hide_cores and b_column_size > 1) { for (const auto& i : iota((size_t)1, cpu.temp.size())) { temp_graphs.emplace_back(5, 1, "", cpu.temp.at(i), graph_symbol, false, false, cpu.temp_max, -23); } } } } //? Draw battery if enabled and present if (Config::getB("show_battery") and has_battery) { static int old_percent = 0; static long old_seconds = 0; static string old_status; static Draw::Meter bat_meter {10, "cpu", true}; static const unordered_flat_map bat_symbols = { {"charging", "▲"}, {"discharging", "▼"}, {"full", "■"}, {"unknown", "○"} }; const auto& [percent, seconds, status] = current_bat; if (redraw or percent != old_percent or seconds != old_seconds or status != old_status) { old_percent = percent; old_seconds = seconds; old_status = status; const string str_time = (seconds > 0 ? sec_to_dhms(seconds, true, true) : ""); const string str_percent = to_string(percent) + '%'; const auto& bat_symbol = bat_symbols.at((bat_symbols.contains(status) ? status : "unknown")); const int current_len = (Term::width >= 100 ? 11 : 0) + str_time.size() + str_percent.size() + to_string(Config::getI("update_ms")).size(); const int current_pos = Term::width - current_len - 17; if ((bat_pos != current_pos or bat_len != current_len) and bat_pos > 0 and not redraw) out += Mv::to(y, bat_pos) + Fx::ub + Theme::c("cpu_box") + Symbols::h_line * (bat_len + 4); bat_pos = current_pos; bat_len = current_len; out += Mv::to(y, bat_pos) + title_left + Theme::c("title") + Fx::b + "BAT" + bat_symbol + ' ' + str_percent + (Term::width >= 100 ? Fx::ub + ' ' + bat_meter(percent) + Fx::b : "") + (not str_time.empty() ? ' ' + Theme::c("title") + str_time : " ") + Fx::ub + title_right; } } else if (bat_pos > 0) { out += Mv::to(y, bat_pos) + Fx::ub + Theme::c("cpu_box") + Symbols::h_line * (bat_len + 4); bat_pos = bat_len = 0; } try { //? Cpu graphs out += Fx::ub + Mv::to(y + 1, x + 1) + graph_upper(cpu.cpu_percent.at(graph_up_field), (data_same or redraw)); if (not single_graph) out += Mv::to( y + graph_up_height + 1 + (mid_line ? 1 : 0), x + 1) + graph_lower(cpu.cpu_percent.at(graph_lo_field), (data_same or redraw)); //? Uptime if (Config::getB("show_uptime")) { string upstr = sec_to_dhms(system_uptime()); if (upstr.size() > 8) { upstr.resize(upstr.size() - 3); upstr = trans(upstr); } out += Mv::to(y + (single_graph or not Config::getB("cpu_invert_lower") ? 1 : height - 2), x + 2) + Theme::c("graph_text") + "up" + Mv::r(1) + upstr; } //? Cpu clock and cpu meter if (Config::getB("show_cpu_freq") and not cpuHz.empty()) out += Mv::to(b_y, b_x + b_width - 10) + Fx::ub + Theme::c("div_line") + Symbols::h_line * (7 - cpuHz.size()) + Symbols::title_left + Fx::b + Theme::c("title") + cpuHz + Fx::ub + Theme::c("div_line") + Symbols::title_right; out += Mv::to(b_y + 1, b_x + 1) + Theme::c("main_fg") + Fx::b + "CPU " + cpu_meter(cpu.cpu_percent.at("total").back()) + Theme::g("cpu").at(clamp(cpu.cpu_percent.at("total").back(), 0ll, 100ll)) + rjust(to_string(cpu.cpu_percent.at("total").back()), 4) + Theme::c("main_fg") + '%'; if (show_temps) { const auto [temp, unit] = celsius_to(cpu.temp.at(0).back(), temp_scale); const auto& temp_color = Theme::g("temp").at(clamp(cpu.temp.at(0).back() * 100 / cpu.temp_max, 0ll, 100ll)); if (b_column_size > 1 or b_columns > 1) out += ' ' + Theme::c("inactive_fg") + graph_bg * 5 + Mv::l(5) + temp_color + temp_graphs.at(0)(cpu.temp.at(0), data_same or redraw); out += rjust(to_string(temp), 4) + Theme::c("main_fg") + unit; } out += Theme::c("div_line") + Symbols::v_line; } catch (const std::exception& e) { throw std::runtime_error("graphs, clock, meter : " + (string)e.what()); } //? Core text and graphs int cx = 0, cy = 1, cc = 0, core_width = (b_column_size == 0 ? 2 : 3); if (Shared::coreCount >= 100) core_width++; for (const auto& n : iota(0, Shared::coreCount)) { out += Mv::to(b_y + cy + 1, b_x + cx + 1) + Theme::c("main_fg") + (Shared::coreCount < 100 ? Fx::b + 'C' + Fx::ub : "") + ljust(to_string(n), core_width); if (b_column_size > 0 or extra_width > 0) out += Theme::c("inactive_fg") + graph_bg * (5 * b_column_size + extra_width) + Mv::l(5 * b_column_size + extra_width) + Theme::g("cpu").at(clamp(cpu.core_percent.at(n).back(), 0ll, 100ll)) + core_graphs.at(n)(cpu.core_percent.at(n), data_same or redraw); else out += Theme::g("cpu").at(clamp(cpu.core_percent.at(n).back(), 0ll, 100ll)); out += rjust(to_string(cpu.core_percent.at(n).back()), (b_column_size < 2 ? 3 : 4)) + Theme::c("main_fg") + '%'; if (show_temps and not hide_cores) { const auto [temp, unit] = celsius_to(cpu.temp.at(n+1).back(), temp_scale); const auto& temp_color = Theme::g("temp").at(clamp(cpu.temp.at(n+1).back() * 100 / cpu.temp_max, 0ll, 100ll)); if (b_column_size > 1) out += ' ' + Theme::c("inactive_fg") + graph_bg * 5 + Mv::l(5) + temp_color + temp_graphs.at(n+1)(cpu.temp.at(n+1), data_same or redraw); out += temp_color + rjust(to_string(temp), 4) + Theme::c("main_fg") + unit; } out += Theme::c("div_line") + Symbols::v_line; if ((++cy > ceil((double)Shared::coreCount / b_columns) or cy == b_height - 2) and n != Shared::coreCount - 1) { if (++cc >= b_columns) break; cy = 1; cx = (b_width / b_columns) * cc; } } //? Load average if (cy < b_height - 1 and cc <= b_columns) { string lavg_pre; int sep = 1; if (b_column_size == 2 and show_temps) { lavg_pre = "Load AVG:"; sep = 3; } else if (b_column_size == 2 or (b_column_size == 1 and show_temps)) { lavg_pre = "LAV:"; } else if (b_column_size == 1 or (b_column_size == 0 and show_temps)) { lavg_pre = "L"; } string lavg; for (const auto& val : cpu.load_avg) { lavg += string(sep, ' ') + (lavg_pre.size() < 3 ? to_string((int)round(val)) : to_string(val).substr(0, 4)); } out += Mv::to(b_y + b_height - 2, b_x + cx + 1) + Theme::c("main_fg") + lavg_pre + lavg; } redraw = false; return out + Fx::reset; } } namespace Mem { int width_p = 45, height_p = 36; int min_width = 36, min_height = 10; int x = 1, y, width = 20, height; int mem_width, disks_width, divider, item_height, mem_size, mem_meter, graph_height, disk_meter; int disks_io_h = 0; int disks_io_half = 0; bool shown = true, redraw = true; string box; unordered_flat_map mem_meters; unordered_flat_map mem_graphs; unordered_flat_map disk_meters_used; unordered_flat_map disk_meters_free; unordered_flat_map io_graphs; string draw(const mem_info& mem, const bool force_redraw, const bool data_same) { if (Runner::stopping) return ""; if (force_redraw) redraw = true; auto& show_swap = Config::getB("show_swap"); auto& swap_disk = Config::getB("swap_disk"); auto& show_disks = Config::getB("show_disks"); auto& show_io_stat = Config::getB("show_io_stat"); auto& io_mode = Config::getB("io_mode"); auto& io_graph_combined = Config::getB("io_graph_combined"); auto& use_graphs = Config::getB("mem_graphs"); auto& tty_mode = Config::getB("tty_mode"); auto& graph_symbol = (tty_mode ? "tty" : Config::getS("graph_symbol_mem")); auto& graph_bg = Symbols::graph_symbols.at((graph_symbol == "default" ? Config::getS("graph_symbol") + "_up" : graph_symbol + "_up")).at(6); auto totalMem = Mem::get_totalMem(); string out; out.reserve(height * width); //* Redraw elements not needed to be updated every cycle if (redraw) { out += box; mem_meters.clear(); mem_graphs.clear(); disk_meters_free.clear(); disk_meters_used.clear(); io_graphs.clear(); //? Mem graphs and meters for (const auto& name : mem_names) { if (use_graphs) mem_graphs[name] = Draw::Graph{mem_meter, graph_height, name, mem.percent.at(name), graph_symbol}; else mem_meters[name] = Draw::Meter{mem_meter, name}; } if (show_swap and has_swap) { for (const auto& name : swap_names) { if (use_graphs) mem_graphs[name] = Draw::Graph{mem_meter, graph_height, name.substr(5), mem.percent.at(name), graph_symbol}; else mem_meters[name] = Draw::Meter{mem_meter, name.substr(5)}; } } //? Disk meters and io graphs if (show_disks) { if (show_io_stat or io_mode) { unordered_flat_map custom_speeds; int half_height = 0; if (io_mode) { disks_io_h = max((int)floor((double)(height - 2 - (disk_ios * 2)) / max(1, disk_ios)), (io_graph_combined ? 1 : 2)); half_height = ceil((double)disks_io_h / 2); if (not Config::getS("io_graph_speeds").empty()) { auto split = ssplit(Config::getS("io_graph_speeds")); for (const auto& entry : split) { auto vals = ssplit(entry); if (vals.size() == 2 and mem.disks.contains(vals.at(0)) and isint(vals.at(1))) try { custom_speeds[vals.at(0)] = std::stoi(vals.at(1)); } catch (const std::out_of_range&) { continue; } } } } for (const auto& [name, disk] : mem.disks) { if (disk.io_read.empty()) continue; io_graphs[name + "_activity"] = Draw::Graph{disks_width - 6, 1, "", disk.io_activity, graph_symbol}; if (io_mode) { //? Create one combined graph for IO read/write if enabled long long speed = (custom_speeds.contains(name) ? custom_speeds.at(name) : 100) << 20; if (io_graph_combined) { deque combined(disk.io_read.size(), 0); rng::transform(disk.io_read, disk.io_write, combined.begin(), std::plus()); io_graphs[name] = Draw::Graph{disks_width - (io_mode ? 0 : 6), disks_io_h, "available", combined, graph_symbol, false, true, speed}; } else { io_graphs[name + "_read"] = Draw::Graph{disks_width, half_height, "free", disk.io_read, graph_symbol, false, true, speed}; io_graphs[name + "_write"] = Draw::Graph{disks_width, disks_io_h - half_height, "used", disk.io_write, graph_symbol, true, true, speed}; } } } } for (int i = 0; const auto& [name, ignored] : mem.disks) { if (i * 2 > height - 2) break; disk_meters_used[name] = Draw::Meter{disk_meter, "used"}; if (cmp_less_equal(mem.disks.size() * 3, height - 1)) disk_meters_free[name] = Draw::Meter{disk_meter, "free"}; } out += Mv::to(y, x + width - 6) + Fx::ub + Theme::c("mem_box") + Symbols::title_left + (io_mode ? Fx::b : "") + Theme::c("hi_fg") + 'i' + Theme::c("title") + 'o' + Fx::ub + Theme::c("mem_box") + Symbols::title_right; Input::mouse_mappings["i"] = {y, x + width - 5, 1, 2}; } } //? Mem and swap int cx = 1, cy = 1; string divider = (graph_height > 0 ? Mv::l(2) + Theme::c("mem_box") + Symbols::div_left + Theme::c("div_line") + Symbols::h_line * (mem_width - 1) + (show_disks ? "" : Theme::c("mem_box")) + Symbols::div_right + Mv::l(mem_width - 1) + Theme::c("main_fg") : ""); string up = (graph_height >= 2 ? Mv::l(mem_width - 2) + Mv::u(graph_height - 1) : ""); bool big_mem = mem_width > 21; out += Mv::to(y + 1, x + 2) + Theme::c("title") + Fx::b + "Total:" + rjust(floating_humanizer(totalMem), mem_width - 9) + Fx::ub + Theme::c("main_fg"); vector comb_names (mem_names.begin(), mem_names.end()); if (show_swap and has_swap and not swap_disk) comb_names.insert(comb_names.end(), swap_names.begin(), swap_names.end()); for (auto name : comb_names) { if (cy > height - 4) break; string title; if (name == "swap_used") { if (cy > height - 5) break; if (height - cy > 6) { if (graph_height > 0) out += Mv::to(y+1+cy, x+1+cx) + divider; cy += 1; } out += Mv::to(y+1+cy, x+1+cx) + Theme::c("title") + Fx::b + "Swap:" + rjust(floating_humanizer(mem.stats.at("swap_total")), mem_width - 8) + Theme::c("main_fg") + Fx::ub; cy += 1; title = "Used"; } else if (name == "swap_free") title = "Free"; if (title.empty()) title = capitalize(name); const string humanized = floating_humanizer(mem.stats.at(name)); const string graphics = (use_graphs ? mem_graphs.at(name)(mem.percent.at(name), redraw or data_same) : mem_meters.at(name)(mem.percent.at(name).back())); if (mem_size > 2) { out += Mv::to(y+1+cy, x+1+cx) + divider + ljust(title, 4, false, false, not big_mem) + ljust(":", (big_mem ? 1 : 6)) + Mv::to(y+1+cy, x+cx + mem_width - 2 - humanized.size()) + trans(humanized) + Mv::to(y+2+cy, x+cx + (graph_height >= 2 ? 0 : 1)) + graphics + up + rjust(to_string(mem.percent.at(name).back()) + "%", 4); cy += (graph_height == 0 ? 2 : graph_height + 1); } else { out += Mv::to(y+1+cy, x+1+cx) + ljust(title, (mem_size > 1 ? 5 : 1)) + (graph_height >= 2 ? "" : " ") + graphics + Theme::c("title") + rjust(humanized, (mem_size > 1 ? 9 : 7)); cy += (graph_height == 0 ? 1 : graph_height); } } if (graph_height > 0 and cy < height - 2) out += Mv::to(y+1+cy, x+1+cx) + divider; //? Disks if (show_disks) { const auto& disks = mem.disks; cx = mem_width; cy = 0; const bool big_disk = disks_width >= 25; divider = Mv::l(1) + Theme::c("div_line") + Symbols::div_left + Symbols::h_line * disks_width + Theme::c("mem_box") + Fx::ub + Symbols::div_right + Mv::l(disks_width); if (io_mode) { for (const auto& mount : mem.disks_order) { if (not disks.contains(mount)) continue; if (cy > height - 3) break; const auto& disk = disks.at(mount); if (disk.io_read.empty()) continue; const string total = floating_humanizer(disk.total, not big_disk); out += Mv::to(y+1+cy, x+1+cx) + divider + Theme::c("title") + Fx::b + uresize(disk.name, disks_width - 2) + Mv::to(y+1+cy, x+cx + disks_width - total.size()) + trans(total) + Fx::ub; if (big_disk) { const string used_percent = to_string(disk.used_percent); out += Mv::to(y+1+cy, x+1+cx + round((double)disks_width / 2) - round((double)used_percent.size() / 2)) + Theme::c("main_fg") + used_percent + '%'; } out += Mv::to(y+2+cy++, x+1+cx) + (big_disk ? " IO% " : " IO " + Mv::l(2)) + Theme::c("inactive_fg") + graph_bg * (disks_width - 6) + Theme::g("available").at(clamp(disk.io_activity.back(), 50ll, 100ll)) + Mv::l(disks_width - 6) + io_graphs.at(mount + "_activity")(disk.io_activity, redraw or data_same) + Theme::c("main_fg"); if (++cy > height - 3) break; if (io_graph_combined) { auto comb_val = disk.io_read.back() + disk.io_write.back(); const string humanized = (disk.io_write.back() > 0 ? "▼"s : ""s) + (disk.io_read.back() > 0 ? "▲"s : ""s) + (comb_val > 0 ? Mv::r(1) + floating_humanizer(comb_val, true) : "RW"); if (disks_io_h == 1) out += Mv::to(y+1+cy, x+1+cx) + string(5, ' '); out += Mv::to(y+1+cy, x+1+cx) + io_graphs.at(mount)({comb_val}, redraw or data_same) + Mv::to(y+1+cy, x+1+cx) + Theme::c("main_fg") + humanized; cy += disks_io_h; } else { const string human_read = (disk.io_read.back() > 0 ? "▲" + floating_humanizer(disk.io_read.back(), true) : "R"); const string human_write = (disk.io_write.back() > 0 ? "▼" + floating_humanizer(disk.io_write.back(), true) : "W"); if (disks_io_h <= 3) out += Mv::to(y+1+cy, x+1+cx) + string(5, ' ') + Mv::to(y+cy + disks_io_h, x+1+cx) + string(5, ' '); out += Mv::to(y+1+cy, x+1+cx) + io_graphs.at(mount + "_read")(disk.io_read, redraw or data_same) + Mv::l(disks_width) + Mv::d(1) + io_graphs.at(mount + "_write")(disk.io_write, redraw or data_same) + Mv::to(y+1+cy, x+1+cx) + human_read + Mv::to(y+cy + disks_io_h, x+1+cx) + human_write; cy += disks_io_h; } } } else { for (const auto& mount : mem.disks_order) { if (not disks.contains(mount)) continue; if (cy > height - 3) break; const auto& disk = disks.at(mount); auto comb_val = (not disk.io_read.empty() ? disk.io_read.back() + disk.io_write.back() : 0ll); const string human_io = (comb_val > 0 and big_disk ? (disk.io_write.back() > 0 ? "▼"s : ""s) + (disk.io_read.back() > 0 ? "▲"s : ""s) + floating_humanizer(comb_val, true) : ""); const string human_total = floating_humanizer(disk.total, not big_disk); const string human_used = floating_humanizer(disk.used, not big_disk); const string human_free = floating_humanizer(disk.free, not big_disk); out += Mv::to(y+1+cy, x+1+cx) + divider + Theme::c("title") + Fx::b + uresize(disk.name, disks_width - 2) + Mv::to(y+1+cy, x+cx + disks_width - human_total.size()) + trans(human_total) + Fx::ub + Theme::c("main_fg"); if (big_disk and not human_io.empty()) out += Mv::to(y+1+cy, x+1+cx + round((double)disks_width / 2) - round((double)human_io.size() / 2)) + Theme::c("main_fg") + human_io; if (++cy > height - 3) break; if (show_io_stat and io_graphs.contains(mount + "_activity")) { out += Mv::to(y+1+cy, x+1+cx) + (big_disk ? " IO% " : " IO " + Mv::l(2)) + Theme::c("inactive_fg") + graph_bg * (disks_width - 6) + Theme::g("available").at(clamp(disk.io_activity.back(), 50ll, 100ll)) + Mv::l(disks_width - 6) + io_graphs.at(mount + "_activity")(disk.io_activity, redraw or data_same) + Theme::c("main_fg"); if (not big_disk) out += Mv::to(y+1+cy, x+cx) + Theme::c("main_fg") + human_io; if (++cy > height - 3) break; } out += Mv::to(y+1+cy, x+1+cx) + (big_disk ? " Used:" + rjust(to_string(disk.used_percent) + '%', 4) : "U") + ' ' + disk_meters_used.at(mount)(disk.used_percent) + rjust(human_used, (big_disk ? 9 : 5)); if (++cy > height - 3) break; if (cmp_less_equal(disks.size() * 3 + (show_io_stat ? disk_ios : 0), height - 1)) { out += Mv::to(y+1+cy, x+1+cx) + (big_disk ? " Free:" + rjust(to_string(disk.free_percent) + '%', 4) : "F") + ' ' + disk_meters_free.at(mount)(disk.free_percent) + rjust(human_free, (big_disk ? 9 : 5)); cy++; if (cmp_less_equal(disks.size() * 4 + (show_io_stat ? disk_ios : 0), height - 1)) cy++; } } } if (cy < height - 2) out += Mv::to(y+1+cy, x+1+cx) + divider; } redraw = false; return out + Fx::reset; } } namespace Net { int width_p = 45, height_p = 32; int min_width = 36, min_height = 6; int x = 1, y, width = 20, height; int b_x, b_y, b_width, b_height, d_graph_height, u_graph_height; bool shown = true, redraw = true; string old_ip; unordered_flat_map graphs; string box; string draw(const net_info& net, const bool force_redraw, const bool data_same) { if (Runner::stopping) return ""; if (force_redraw) redraw = true; auto& net_sync = Config::getB("net_sync"); auto& net_auto = Config::getB("net_auto"); auto& tty_mode = Config::getB("tty_mode"); auto& graph_symbol = (tty_mode ? "tty" : Config::getS("graph_symbol_net")); string ip_addr = (net.ipv4.empty() ? net.ipv6 : net.ipv4); if (old_ip != ip_addr) { old_ip = ip_addr; redraw = true; } string out; out.reserve(width * height); const string title_left = Theme::c("net_box") + Fx::ub + Symbols::title_left; const string title_right = Theme::c("net_box") + Fx::ub + Symbols::title_right; const int i_size = min((int)selected_iface.size(), 10); const long long down_max = (net_auto ? graph_max.at("download") : ((long long)(Config::getI("net_download")) << 20) / 8); const long long up_max = (net_auto ? graph_max.at("upload") : ((long long)(Config::getI("net_upload")) << 20) / 8); //* Redraw elements not needed to be updated every cycle if (redraw) { out = box; //? Graphs graphs.clear(); if (net.bandwidth.at("download").empty() or net.bandwidth.at("upload").empty()) return out + Fx::reset; graphs["download"] = Draw::Graph{width - b_width - 2, u_graph_height, "download", net.bandwidth.at("download"), graph_symbol, false, true, down_max}; graphs["upload"] = Draw::Graph{width - b_width - 2, d_graph_height, "upload", net.bandwidth.at("upload"), graph_symbol, true, true, up_max}; //? Interface selector and buttons out += Mv::to(y, x+width - i_size - 10) + title_left + Fx::b + Theme::c("hi_fg") + "" + title_right + Mv::to(y, x+width - i_size - 16) + title_left + Theme::c("hi_fg") + (net.stat.at("download").offset + net.stat.at("upload").offset > 0 ? Fx::b : "") + 'z' + Theme::c("title") + "ero" + title_right; Input::mouse_mappings["b"] = {y, x+width - i_size - 9, 1, 3}; Input::mouse_mappings["n"] = {y, x+width - 6, 1, 3}; Input::mouse_mappings["z"] = {y, x+width - i_size - 15, 1, 4}; if (width - i_size - 20 > 6) { out += Mv::to(y, x+width - i_size - 21) + title_left + Theme::c("hi_fg") + (net_auto ? Fx::b : "") + 'a' + Theme::c("title") + "uto" + title_right; Input::mouse_mappings["a"] = {y, x+width - i_size - 20, 1, 4}; } if (width - i_size - 20 > 13) { out += Mv::to(y, x+width - i_size - 27) + title_left + Theme::c("title") + (net_sync ? Fx::b : "") + 's' + Theme::c("hi_fg") + 'y' + Theme::c("title") + "nc" + title_right; Input::mouse_mappings["y"] = {y, x+width - i_size - 26, 1, 4}; } } //? IP or device address if (not ip_addr.empty() and cmp_greater(width - i_size - 36, ip_addr.size())) { out += Mv::to(y, x + 8) + title_left + Theme::c("title") + Fx::b + ip_addr + title_right; } //? Graphs and stats int cy = 0; for (const string dir : {"download", "upload"}) { out += Mv::to(y+1 + (dir == "upload" ? u_graph_height : 0), x + 1) + graphs.at(dir)(net.bandwidth.at(dir), redraw or data_same or not net.connected) + Mv::to(y+1 + (dir == "upload" ? height - 3: 0), x + 1) + Fx::ub + Theme::c("graph_text") + floating_humanizer((dir == "upload" ? up_max : down_max), true); const string speed = floating_humanizer(net.stat.at(dir).speed, false, 0, false, true); const string speed_bits = (b_width >= 20 ? floating_humanizer(net.stat.at(dir).speed, false, 0, true, true) : ""); const string top = floating_humanizer(net.stat.at(dir).top, false, 0, true, true); const string total = floating_humanizer(net.stat.at(dir).total); const string symbol = (dir == "upload" ? "▲" : "▼"); out += Mv::to(b_y+1+cy, b_x+1) + Fx::ub + Theme::c("main_fg") + symbol + ' ' + ljust(speed, 10) + (b_width >= 20 ? rjust('(' + speed_bits + ')', 13) : ""); cy += (b_height == 5 ? 2 : 1); if (b_height >= 8) { out += Mv::to(b_y+1+cy, b_x+1) + symbol + ' ' + "Top: " + rjust('(' + top, (b_width >= 20 ? 17 : 9)) + ')'; cy++; } if (b_height >= 6) { out += Mv::to(b_y+1+cy, b_x+1) + symbol + ' ' + "Total: " + rjust(total, (b_width >= 20 ? 16 : 8)); cy += (b_height > 6 and b_height % 2 ? 2 : 1); } } redraw = false; return out + Fx::reset; } } namespace Proc { int width_p = 55, height_p = 68; int min_width = 44, min_height = 16; int x, y, width = 20, height; int start, selected, select_max; bool shown = true, redraw = true; int selected_pid = 0; string selected_name; unordered_flat_map p_graphs; unordered_flat_map p_counters; int counter = 0; Draw::TextEdit filter; Draw::Graph detailed_cpu_graph; Draw::Graph detailed_mem_graph; int user_size, thread_size, prog_size, cmd_size, tree_size; int dgraph_x, dgraph_width, d_width, d_x, d_y; string box; int selection(const string& cmd_key) { auto start = Config::getI("proc_start"); auto selected = Config::getI("proc_selected"); auto last_selected = Config::getI("proc_last_selected"); const int select_max = (Config::getB("show_detailed") ? Proc::select_max - 8 : Proc::select_max); auto& vim_keys = Config::getB("vim_keys"); int numpids = Proc::numpids; if ((cmd_key == "up" or (vim_keys and cmd_key == "k")) and selected > 0) { if (start > 0 and selected == 1) start--; else selected--; if (Config::getI("proc_last_selected") > 0) Config::set("proc_last_selected", 0); } else if (cmd_key == "mouse_scroll_up" and start > 0) { start = max(0, start - 3); } else if (cmd_key == "mouse_scroll_down" and start < numpids - select_max) { start = min(numpids - select_max, start + 3); } else if (cmd_key == "down" or (vim_keys and cmd_key == "j")) { if (start < numpids - select_max and selected == select_max) start++; else if (selected == 0 and last_selected > 0) { selected = last_selected; Config::set("proc_last_selected", 0); } else selected++; } else if (cmd_key == "page_up") { if (selected > 0 and start == 0) selected = 0; else start = max(0, start - select_max); } else if (cmd_key == "page_down") { if (selected > 0 and start >= numpids - select_max) selected = select_max; else start = clamp(start + select_max, 0, max(0, numpids - select_max)); } else if (cmd_key == "home") { start = 0; if (selected > 0) selected = 1; } else if (cmd_key == "end") { start = max(0, numpids - select_max); if (selected > 0) selected = select_max; } else if (cmd_key.starts_with("mousey")) { int mouse_y = std::stoi(cmd_key.substr(6)); start = clamp((int)round((double)mouse_y * (numpids - select_max - 2) / (select_max - 2)), 0, max(0, numpids - select_max)); } bool changed = false; if (start != Config::getI("proc_start")) { Config::set("proc_start", start); changed = true; } if (selected != Config::getI("proc_selected")) { Config::set("proc_selected", selected); changed = true; } return (not changed ? -1 : selected); } string draw(const vector& plist, const bool force_redraw, const bool data_same) { if (Runner::stopping) return ""; auto& proc_tree = Config::getB("proc_tree"); const bool show_detailed = (Config::getB("show_detailed") and cmp_equal(Proc::detailed.last_pid, Config::getI("detailed_pid"))); const bool proc_gradient = (Config::getB("proc_gradient") and not Config::getB("lowcolor") and Theme::gradients.contains("proc")); auto& proc_colors = Config::getB("proc_colors"); auto& tty_mode = Config::getB("tty_mode"); auto& graph_symbol = (tty_mode ? "tty" : Config::getS("graph_symbol_proc")); auto& graph_bg = Symbols::graph_symbols.at((graph_symbol == "default" ? Config::getS("graph_symbol") + "_up" : graph_symbol + "_up")).at(6); auto& mem_bytes = Config::getB("proc_mem_bytes"); auto& vim_keys = Config::getB("vim_keys"); start = Config::getI("proc_start"); selected = Config::getI("proc_selected"); const int y = show_detailed ? Proc::y + 8 : Proc::y; const int height = show_detailed ? Proc::height - 8 : Proc::height; const int select_max = show_detailed ? Proc::select_max - 8 : Proc::select_max; auto totalMem = Mem::get_totalMem(); int numpids = Proc::numpids; if (force_redraw) redraw = true; string out; out.reserve(width * height); //* Redraw elements not needed to be updated every cycle if (redraw) { out = box; const string title_left = Theme::c("proc_box") + Symbols::title_left; const string title_right = Theme::c("proc_box") + Symbols::title_right; const string title_left_down = Theme::c("proc_box") + Symbols::title_left_down; const string title_right_down = Theme::c("proc_box") + Symbols::title_right_down; for (const auto& key : {"T", "K", "S", "enter"}) if (Input::mouse_mappings.contains(key)) Input::mouse_mappings.erase(key); //? Adapt sizes of text fields user_size = (width < 75 ? 5 : 10); thread_size = (width < 75 ? - 1 : 4); prog_size = (width > 70 ? 16 : ( width > 55 ? 8 : width - user_size - thread_size - 33)); cmd_size = (width > 55 ? width - prog_size - user_size - thread_size - 33 : -1); tree_size = width - user_size - thread_size - 23; //? Detailed box if (show_detailed) { const bool alive = detailed.status != "Dead"; dgraph_x = x; dgraph_width = max(width / 3, width - 121); d_width = width - dgraph_width - 1; d_x = x + dgraph_width + 1; d_y = Proc::y; //? Create cpu and mem graphs if process is alive if (alive) { detailed_cpu_graph = Draw::Graph{dgraph_width - 1, 7, "cpu", detailed.cpu_percent, graph_symbol, false, true}; detailed_mem_graph = Draw::Graph{d_width / 3, 1, "", detailed.mem_bytes, graph_symbol, false, false, detailed.first_mem}; } //? Draw structure of details box const string pid_str = to_string(detailed.entry.pid); out += Mv::to(y, x) + Theme::c("proc_box") + Symbols::div_left + Symbols::h_line + title_left + Theme::c("hi_fg") + Fx::b + (tty_mode ? "4" : Symbols::superscript.at(4)) + Theme::c("title") + "proc" + Fx::ub + title_right + Symbols::h_line * (width - 10) + Symbols::div_right + Mv::to(d_y, dgraph_x + 2) + title_left + Fx::b + Theme::c("title") + pid_str + Fx::ub + title_right + title_left + Fx::b + Theme::c("title") + uresize(detailed.entry.name, dgraph_width - pid_str.size() - 7, true) + Fx::ub + title_right; out += Mv::to(d_y, d_x - 1) + Theme::c("proc_box") + Symbols::div_up + Mv::to(y, d_x - 1) + Symbols::div_down + Theme::c("div_line"); for (const int& i : iota(1, 8)) out += Mv::to(d_y + i, d_x - 1) + Symbols::v_line; const string& t_color = (not alive or selected > 0 ? Theme::c("inactive_fg") : Theme::c("title")); const string& hi_color = (not alive or selected > 0 ? t_color : Theme::c("hi_fg")); const string hide = (selected > 0 ? t_color + "hide " : Theme::c("title") + "hide " + Theme::c("hi_fg")); int mouse_x = d_x + 2; out += Mv::to(d_y, d_x + 1); if (width > 55) { out += Fx::ub + title_left + hi_color + Fx::b + 't' + t_color + "erminate" + Fx::ub + title_right; if (alive and selected == 0) Input::mouse_mappings["t"] = {d_y, mouse_x, 1, 9}; mouse_x += 11; } out += title_left + hi_color + Fx::b + (vim_keys ? 'K' : 'k') + t_color + "ill" + Fx::ub + title_right + title_left + hi_color + Fx::b + 's' + t_color + "ignals" + Fx::ub + title_right + Mv::to(d_y, d_x + d_width - 10) + title_left + t_color + Fx::b + hide + Symbols::enter + Fx::ub + title_right; if (alive and selected == 0) { Input::mouse_mappings["k"] = {d_y, mouse_x, 1, 4}; mouse_x += 6; Input::mouse_mappings["s"] = {d_y, mouse_x, 1, 7}; } if (selected == 0) Input::mouse_mappings["enter"] = {d_y, d_x + d_width - 9, 1, 6}; //? Labels const int item_fit = floor((double)(d_width - 2) / 10); const int item_width = floor((double)(d_width - 2) / min(item_fit, 8)); out += Mv::to(d_y + 1, d_x + 1) + Fx::b + Theme::c("title") + cjust("Status:", item_width) + cjust("Elapsed:", item_width); if (item_fit >= 3) out += cjust("IO/R:", item_width); if (item_fit >= 4) out += cjust("IO/W:", item_width); if (item_fit >= 5) out += cjust("Parent:", item_width); if (item_fit >= 6) out += cjust("User:", item_width); if (item_fit >= 7) out += cjust("Threads:", item_width); if (item_fit >= 8) out += cjust("Nice:", item_width); //? Command line for (int i = 0; const auto& l : {'C', 'M', 'D'}) out += Mv::to(d_y + 5 + i++, d_x + 1) + l; out += Theme::c("main_fg") + Fx::ub; const int cmd_size = ulen(detailed.entry.cmd, true); for (int num_lines = min(3, (int)ceil((double)cmd_size / (d_width - 5))), i = 0; i < num_lines; i++) { out += Mv::to(d_y + 5 + (num_lines == 1 ? 1 : i), d_x + 3) + cjust(luresize(detailed.entry.cmd, cmd_size - (d_width - 5) * i, true), d_width - 5, true, true); } } //? Filter auto& filtering = Config::getB("proc_filtering"); // ? filter(20) : Config::getS("proc_filter")) const auto filter_text = (filtering) ? filter(max(6, width - 58)) : uresize(Config::getS("proc_filter"), max(6, width - 58)); out += Mv::to(y, x+9) + title_left + (not filter_text.empty() ? Fx::b : "") + Theme::c("hi_fg") + 'f' + Theme::c("title") + (not filter_text.empty() ? ' ' + filter_text : "ilter") + (not filtering and not filter_text.empty() ? Theme::c("hi_fg") + " del" : "") + (filtering ? Theme::c("hi_fg") + ' ' + Symbols::enter : "") + Fx::ub + title_right; if (not filtering) { int f_len = (filter_text.empty() ? 6 : ulen(filter_text) + 2); Input::mouse_mappings["f"] = {y, x + 10, 1, f_len}; if (filter_text.empty() and Input::mouse_mappings.contains("delete")) Input::mouse_mappings.erase("delete"); else if (not filter_text.empty()) Input::mouse_mappings["delete"] = {y, x + 11 + f_len, 1, 3}; } //? per-core, reverse, tree and sorting const auto& sorting = Config::getS("proc_sorting"); const int sort_len = sorting.size(); const int sort_pos = x + width - sort_len - 8; if (width > 55 + sort_len) { out += Mv::to(y, sort_pos - 25) + title_left + (Config::getB("proc_per_core") ? Fx::b : "") + Theme::c("title") + "per-" + Theme::c("hi_fg") + 'c' + Theme::c("title") + "ore" + Fx::ub + title_right; Input::mouse_mappings["c"] = {y, sort_pos - 24, 1, 8}; } if (width > 45 + sort_len) { out += Mv::to(y, sort_pos - 15) + title_left + (Config::getB("proc_reversed") ? Fx::b : "") + Theme::c("hi_fg") + 'r' + Theme::c("title") + "everse" + Fx::ub + title_right; Input::mouse_mappings["r"] = {y, sort_pos - 14, 1, 7}; } if (width > 35 + sort_len) { out += Mv::to(y, sort_pos - 6) + title_left + (Config::getB("proc_tree") ? Fx::b : "") + Theme::c("title") + "tre" + Theme::c("hi_fg") + 'e' + Fx::ub + title_right; Input::mouse_mappings["e"] = {y, sort_pos - 5, 1, 4}; } out += Mv::to(y, sort_pos) + title_left + Fx::b + Theme::c("hi_fg") + "< " + Theme::c("title") + sorting + Theme::c("hi_fg") + " >" + Fx::ub + title_right; Input::mouse_mappings["left"] = {y, sort_pos + 1, 1, 2}; Input::mouse_mappings["right"] = {y, sort_pos + sort_len + 3, 1, 2}; //? select, info and signal buttons const string down_button = (selected == select_max and start == numpids - select_max ? Theme::c("inactive_fg") : Theme::c("hi_fg")) + Symbols::down; const string t_color = (selected == 0 ? Theme::c("inactive_fg") : Theme::c("title")); const string hi_color = (selected == 0 ? Theme::c("inactive_fg") : Theme::c("hi_fg")); int mouse_x = x + 14; out += Mv::to(y + height - 1, x + 1) + title_left_down + Fx::b + hi_color + Symbols::up + Theme::c("title") + " select " + down_button + Fx::ub + title_right_down + title_left_down + Fx::b + t_color + "info " + hi_color + Symbols::enter + Fx::ub + title_right_down; if (selected > 0) Input::mouse_mappings["enter"] = {y + height - 1, mouse_x, 1, 6}; mouse_x += 8; if (width > 60) { out += title_left_down + Fx::b + hi_color + 't' + t_color + "erminate" + Fx::ub + title_right_down; if (selected > 0) Input::mouse_mappings["t"] = {y + height - 1, mouse_x, 1, 9}; mouse_x += 11; } if (width > 55) { out += title_left_down + Fx::b + hi_color + (vim_keys ? 'K' : 'k') + t_color + "ill" + Fx::ub + title_right_down; if (selected > 0) Input::mouse_mappings["k"] = {y + height - 1, mouse_x, 1, 4}; mouse_x += 6; } out += title_left_down + Fx::b + hi_color + 's' + t_color + "ignals" + Fx::ub + title_right_down; if (selected > 0) Input::mouse_mappings["s"] = {y + height - 1, mouse_x, 1, 7}; //? Labels for fields in list if (not proc_tree) out += Mv::to(y+1, x+1) + Theme::c("title") + Fx::b + rjust("Pid:", 8) + ' ' + ljust("Program:", prog_size) + ' ' + (cmd_size > 0 ? ljust("Command:", cmd_size) : "") + ' '; else out += Mv::to(y+1, x+1) + Theme::c("title") + Fx::b + ljust("Tree:", tree_size) + ' '; out += (thread_size > 0 ? Mv::l(4) + "Threads: " : "") + ljust("User:", user_size) + ' ' + rjust((mem_bytes ? "MemB" : "Mem%"), 5) + ' ' + rjust("Cpu%", 10) + Fx::ub; } //* End of redraw block //? Draw details box if shown if (show_detailed) { const bool alive = detailed.status != "Dead"; const int item_fit = floor((double)(d_width - 2) / 10); const int item_width = floor((double)(d_width - 2) / min(item_fit, 8)); //? Graph part of box string cpu_str = (alive ? to_string(detailed.entry.cpu_p) : ""); if (alive) { cpu_str.resize((detailed.entry.cpu_p < 10 or detailed.entry.cpu_p >= 100 ? 3 : 4)); cpu_str += '%'; } out += Mv::to(d_y + 1, dgraph_x + 1) + Fx::ub + detailed_cpu_graph(detailed.cpu_percent, (redraw or data_same or not alive)) + Mv::to(d_y + 1, dgraph_x + 1) + Theme::c("title") + Fx::b + cpu_str; for (int i = 0; const auto& l : {'C', 'P', 'U'}) out += Mv::to(d_y + 3 + i++, dgraph_x + 1) + l; //? Info part of box const string stat_color = (not alive ? Theme::c("inactive_fg") : (detailed.status == "Running" ? Theme::c("proc_misc") : Theme::c("main_fg"))); out += Mv::to(d_y + 2, d_x + 1) + stat_color + Fx::ub + cjust(detailed.status, item_width) + Theme::c("main_fg") + cjust(detailed.elapsed, item_width); if (item_fit >= 3) out += cjust(detailed.io_read, item_width); if (item_fit >= 4) out += cjust(detailed.io_write, item_width); if (item_fit >= 5) out += cjust(detailed.parent, item_width, true); if (item_fit >= 6) out += cjust(detailed.entry.user, item_width, true); if (item_fit >= 7) out += cjust(to_string(detailed.entry.threads), item_width); if (item_fit >= 8) out += cjust(to_string(detailed.entry.p_nice), item_width); const double mem_p = (double)detailed.mem_bytes.back() * 100 / totalMem; string mem_str = to_string(mem_p); mem_str.resize((mem_p < 10 or mem_p >= 100 ? 3 : 4)); out += Mv::to(d_y + 4, d_x + 1) + Theme::c("title") + Fx::b + rjust((item_fit > 4 ? "Memory: " : "M:") + mem_str + "% ", (d_width / 3) - 2) + Theme::c("inactive_fg") + Fx::ub + graph_bg * (d_width / 3) + Mv::l(d_width / 3) + Theme::c("proc_misc") + detailed_mem_graph(detailed.mem_bytes, (redraw or data_same or not alive)) + ' ' + Theme::c("title") + Fx::b + detailed.memory; } //? Check bounds of current selection and view if (start > 0 and numpids <= select_max) start = 0; if (start > numpids - select_max) start = max(0, numpids - select_max); if (selected > select_max) selected = select_max; if (selected > numpids) selected = numpids; //* Iteration over processes int lc = 0; for (int n=0; auto& p : plist) { if (p.filtered or (proc_tree and p.tree_index == plist.size()) or n++ < start) continue; bool is_selected = (lc + 1 == selected); if (is_selected) { selected_pid = (int)p.pid; selected_name = p.name; } //? Update graphs for processes with above 0.0% cpu usage, delete if below 0.1% 10x times const bool has_graph = p_counters.contains(p.pid); if ((p.cpu_p > 0 and not has_graph) or (not data_same and has_graph)) { if (not has_graph) { p_graphs[p.pid] = Draw::Graph{5, 1, "", {}, graph_symbol}; p_counters[p.pid] = 0; } else if (p.cpu_p < 0.1 and ++p_counters[p.pid] >= 10) { p_graphs.erase(p.pid); p_counters.erase(p.pid); } else p_counters.at(p.pid) = 0; } out += Fx::reset; //? Set correct gradient colors if enabled string c_color, m_color, t_color, g_color, end; if (is_selected) { c_color = m_color = t_color = g_color = Fx::b; end = Fx::ub; out += Theme::c("selected_bg") + Theme::c("selected_fg") + Fx::b; } else { int calc = (selected > lc) ? selected - lc : lc - selected; if (proc_colors) { end = Theme::c("main_fg") + Fx::ub; array colors; for (int i = 0; int v : {(int)round(p.cpu_p), (int)round(p.mem * 100 / totalMem), (int)p.threads / 3}) { if (proc_gradient) { int val = (min(v, 100) + 100) - calc * 100 / select_max; if (val < 100) colors[i++] = Theme::g("proc_color").at(max(0, val)); else colors[i++] = Theme::g("process").at(clamp(val - 100, 0, 100)); } else colors[i++] = Theme::g("process").at(clamp(v, 0, 100)); } c_color = colors.at(0); m_color = colors.at(1); t_color = colors.at(2); } else { c_color = m_color = t_color = Fx::b; end = Fx::ub; } if (proc_gradient) { g_color = Theme::g("proc").at(clamp(calc * 100 / select_max, 0, 100)); } } //? Normal view line if (not proc_tree) { out += Mv::to(y+2+lc, x+1) + g_color + rjust(to_string(p.pid), 8) + ' ' + c_color + ljust(p.name, prog_size, true) + ' ' + end + (cmd_size > 0 ? g_color + ljust(p.cmd, cmd_size, true, true) + Mv::to(y+2+lc, x+11+prog_size+cmd_size) + ' ' : ""); } //? Tree view line else { const string prefix_pid = p.prefix + to_string(p.pid); int width_left = tree_size; out += Mv::to(y+2+lc, x+1) + g_color + uresize(prefix_pid, width_left) + ' '; width_left -= ulen(prefix_pid); if (width_left > 0) { out += c_color + uresize(p.name, width_left - 1) + end + ' '; width_left -= (ulen(p.name) + 1); } if (width_left > 7 and p.short_cmd != p.name) { out += g_color + '(' + uresize(p.short_cmd, width_left - 3, true) + ") "; width_left -= (ulen(p.short_cmd, true) + 3); } out += string(max(0, width_left), ' ') + Mv::to(y+2+lc, x+2+tree_size); } //? Common end of line string cpu_str = to_string(p.cpu_p); if (p.cpu_p < 10 or (p.cpu_p >= 100 and p.cpu_p < 1000)) cpu_str.resize(3); else if (p.cpu_p >= 10'000) { cpu_str = to_string(p.cpu_p / 1000); cpu_str.resize(3); if (cpu_str.ends_with('.')) cpu_str.pop_back(); cpu_str += "k"; } string mem_str = (mem_bytes ? floating_humanizer(p.mem, true) : ""); if (not mem_bytes) { double mem_p = clamp((double)p.mem * 100 / totalMem, 0.0, 100.0); mem_str = to_string(mem_p); if (mem_str.size() < 4) mem_str = "0"; else mem_str.resize((mem_p < 10 or mem_p >= 100 ? 3 : 4)); mem_str += '%'; } out += (thread_size > 0 ? t_color + rjust(to_string(min(p.threads, (size_t)9999)), thread_size) + ' ' + end : "" ) + g_color + ljust((cmp_greater(p.user.size(), user_size) ? p.user.substr(0, user_size - 1) + '+' : p.user), user_size) + ' ' + m_color + rjust(mem_str, 5) + end + ' ' + (is_selected ? "" : Theme::c("inactive_fg")) + graph_bg * 5 + (p_graphs.contains(p.pid) ? Mv::l(5) + c_color + p_graphs.at(p.pid)({(p.cpu_p >= 0.1 and p.cpu_p < 5 ? 5ll : (long long)round(p.cpu_p))}, data_same) : "") + end + ' ' + c_color + rjust(cpu_str, 4) + " " + end; if (lc++ > height - 5) break; } out += Fx::reset; while (lc++ < height - 5) out += Mv::to(y+lc+1, x+1) + string(width - 2, ' '); //? Draw scrollbar if needed if (numpids > select_max) { const int scroll_pos = clamp((int)round((double)start * select_max / (numpids - select_max)), 0, height - 5); out += Mv::to(y + 1, x + width - 2) + Fx::b + Theme::c("main_fg") + Symbols::up + Mv::to(y + height - 2, x + width - 2) + Symbols::down + Mv::to(y + 2 + scroll_pos, x + width - 2) + "█"; } //? Current selection and number of processes string location = to_string(start + selected) + '/' + to_string(numpids); string loc_clear = Symbols::h_line * max((size_t)0, 9 - location.size()); out += Mv::to(y + height - 1, x+width - 3 - max(9, (int)location.size())) + Fx::ub + Theme::c("proc_box") + loc_clear + Symbols::title_left_down + Theme::c("title") + Fx::b + location + Fx::ub + Theme::c("proc_box") + Symbols::title_right_down; //? Clear out left over graphs from dead processes at a regular interval if (not data_same and ++counter >= 100) { counter = 0; for (auto element = p_graphs.begin(); element != p_graphs.end();) { if (rng::find(plist, element->first, &proc_info::pid) == plist.end()) { element = p_graphs.erase(element); p_counters.erase(element->first); } else ++element; } p_graphs.compact(); p_counters.compact(); } if (selected == 0 and selected_pid != 0) { selected_pid = 0; selected_name.clear(); } redraw = false; return out + Fx::reset; } } namespace Draw { void calcSizes() { atomic_wait(Runner::active); Config::unlock(); auto& boxes = Config::getS("shown_boxes"); auto& cpu_bottom = Config::getB("cpu_bottom"); auto& mem_below_net = Config::getB("mem_below_net"); auto& proc_left = Config::getB("proc_left"); Cpu::box.clear(); Mem::box.clear(); Net::box.clear(); Proc::box.clear(); Global::clock.clear(); Global::overlay.clear(); Runner::pause_output = false; Runner::redraw = true; Proc::p_counters.clear(); Proc::p_graphs.clear(); if (Menu::active) Menu::redraw = true; Input::mouse_mappings.clear(); Cpu::x = Mem::x = Net::x = Proc::x = 1; Cpu::y = Mem::y = Net::y = Proc::y = 1; Cpu::width = Mem::width = Net::width = Proc::width = 0; Cpu::height = Mem::height = Net::height = Proc::height = 0; Cpu::redraw = Mem::redraw = Net::redraw = Proc::redraw = true; Cpu::shown = s_contains(boxes, "cpu"); Mem::shown = s_contains(boxes, "mem"); Net::shown = s_contains(boxes, "net"); Proc::shown = s_contains(boxes, "proc"); //* Calculate and draw cpu box outlines if (Cpu::shown) { using namespace Cpu; const bool show_temp = (Config::getB("check_temp") and got_sensors); width = round((double)Term::width * width_p / 100); height = max(8, (int)ceil((double)Term::height * (trim(boxes) == "cpu" ? 100 : height_p) / 100)); x = 1; y = cpu_bottom ? Term::height - height + 1 : 1; b_columns = max(1, (int)ceil((double)(Shared::coreCount + 1) / (height - 5))); if (b_columns * (21 + 12 * show_temp) < width - (width / 3)) { b_column_size = 2; b_width = (21 + 12 * show_temp) * b_columns - (b_columns - 1); } else if (b_columns * (15 + 6 * show_temp) < width - (width / 3)) { b_column_size = 1; b_width = (15 + 6 * show_temp) * b_columns - (b_columns - 1); } else if (b_columns * (8 + 6 * show_temp) < width - (width / 3)) { b_column_size = 0; } else { b_columns = (width - width / 3) / (8 + 6 * show_temp); b_column_size = 0; } if (b_column_size == 0) b_width = (8 + 6 * show_temp) * b_columns + 1; b_height = min(height - 2, (int)ceil((double)Shared::coreCount / b_columns) + 4); b_x = x + width - b_width - 1; b_y = y + ceil((double)(height - 2) / 2) - ceil((double)b_height / 2) + 1; box = createBox(x, y, width, height, Theme::c("cpu_box"), true, (cpu_bottom ? "" : "cpu"), (cpu_bottom ? "cpu" : ""), 1); auto& custom = Config::getS("custom_cpu_name"); const string cpu_title = uresize((custom.empty() ? Cpu::cpuName : custom) , b_width - 14); box += createBox(b_x, b_y, b_width, b_height, "", false, cpu_title); } //* Calculate and draw mem box outlines if (Mem::shown) { using namespace Mem; auto& show_disks = Config::getB("show_disks"); auto& swap_disk = Config::getB("swap_disk"); auto& mem_graphs = Config::getB("mem_graphs"); width = round((double)Term::width * (Proc::shown ? width_p : 100) / 100); height = ceil((double)Term::height * (100 - Cpu::height_p * Cpu::shown - Net::height_p * Net::shown) / 100) + 1; if (height + Cpu::height > Term::height) height = Term::height - Cpu::height; x = (proc_left and Proc::shown) ? Term::width - width + 1: 1; if (mem_below_net and Net::shown) y = Term::height - height + 1 - (cpu_bottom ? Cpu::height : 0); else y = cpu_bottom ? 1 : Cpu::height + 1; if (show_disks) { mem_width = ceil((double)(width - 3) / 2); mem_width += mem_width % 2; disks_width = width - mem_width - 2; divider = x + mem_width; } else mem_width = width - 1; item_height = has_swap and not swap_disk ? 6 : 4; if (height - (has_swap and not swap_disk ? 3 : 2) > 2 * item_height) mem_size = 3; else if (mem_width > 25) mem_size = 2; else mem_size = 1; mem_meter = max(0, mem_width - (mem_size > 2 ? 7 : 17)); if (mem_size == 1) mem_meter += 6; if (mem_graphs) { graph_height = max(1, (int)round((double)((height - (has_swap and not swap_disk ? 2 : 1)) - (mem_size == 3 ? 2 : 1) * item_height) / item_height)); if (graph_height > 1) mem_meter += 6; } else graph_height = 0; if (show_disks) { disk_meter = max(-14, width - mem_width - 23); if (disks_width < 25) disk_meter += 14; } box = createBox(x, y, width, height, Theme::c("mem_box"), true, "mem", "", 2); box += Mv::to(y, (show_disks ? divider + 2 : x + width - 9)) + Theme::c("mem_box") + Symbols::title_left + (show_disks ? Fx::b : "") + Theme::c("hi_fg") + 'd' + Theme::c("title") + "isks" + Fx::ub + Theme::c("mem_box") + Symbols::title_right; Input::mouse_mappings["d"] = {y, (show_disks ? divider + 3 : x + width - 8), 1, 5}; if (show_disks) { box += Mv::to(y, divider) + Symbols::div_up + Mv::to(y + height - 1, divider) + Symbols::div_down + Theme::c("div_line"); for (auto i : iota(1, height - 1)) box += Mv::to(y + i, divider) + Symbols::v_line; } } //* Calculate and draw net box outlines if (Net::shown) { using namespace Net; width = round((double)Term::width * (Proc::shown ? width_p : 100) / 100); height = Term::height - Cpu::height - Mem::height; x = (proc_left and Proc::shown) ? Term::width - width + 1 : 1; if (mem_below_net and Mem::shown) y = cpu_bottom ? 1 : Cpu::height + 1; else y = Term::height - height + 1 - (cpu_bottom ? Cpu::height : 0); b_width = (width > 45) ? 27 : 19; b_height = (height > 10) ? 9 : height - 2; b_x = x + width - b_width - 1; b_y = y + ((height - 2) / 2) - b_height / 2 + 1; d_graph_height = round((double)(height - 2) / 2); u_graph_height = height - 2 - d_graph_height; box = createBox(x, y, width, height, Theme::c("net_box"), true, "net", "", 3); box += createBox(b_x, b_y, b_width, b_height, "", false, "download", "upload"); } //* Calculate and draw proc box outlines if (Proc::shown) { using namespace Proc; width = Term::width - (Mem::shown ? Mem::width : (Net::shown ? Net::width : 0)); height = Term::height - Cpu::height; x = proc_left ? 1 : Term::width - width + 1; y = (cpu_bottom and Cpu::shown) ? 1 : Cpu::height + 1; select_max = height - 3; box = createBox(x, y, width, height, Theme::c("proc_box"), true, "proc", "", 4); } } }btop-1.2.3/src/btop_draw.hpp000066400000000000000000000076361420276253000157570ustar00rootroot00000000000000/* 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. indent = tab tab-size = 4 */ #pragma once #include #include #include #include #include using std::string, std::array, std::vector, robin_hood::unordered_flat_map, std::deque; namespace Symbols { const string h_line = "─"; const string v_line = "│"; const string dotted_v_line = "╎"; const string left_up = "┌"; const string right_up = "┐"; const string left_down = "└"; const string right_down = "┘"; const string round_left_up = "╭"; const string round_right_up = "╮"; const string round_left_down = "╰"; const string round_right_down = "╯"; const string title_left_down = "┘"; const string title_right_down = "└"; const string title_left = "┐"; const string title_right = "┌"; const string div_right = "┤"; const string div_left = "├"; const string div_up = "┬"; const string div_down = "┴"; const string up = "↑"; const string down = "↓"; const string left = "←"; const string right = "→"; const string enter = "↲"; } namespace Draw { //* Generate if needed and return the btop++ banner string banner_gen(int y=0, int x=0, bool centered=false, bool redraw=false); //* An editable text field class TextEdit { size_t pos = 0; size_t upos = 0; bool numeric; public: string text; TextEdit(); TextEdit(string text, bool numeric=false); bool command(const string& key); string operator()(const size_t limit=0); void clear(); }; //* Create a box and return as a string string createBox(const int x, const int y, const int width, const int height, string line_color="", const bool fill=false, const string title="", const string title2="", const int num=0); bool update_clock(bool force=false); //* Class holding a percentage meter class Meter { int width; string color_gradient; bool invert; array cache; public: Meter(); Meter(const int width, const string& color_gradient, const bool invert = false); //* Return a string representation of the meter with given value string operator()(int value); }; //* Class holding a percentage graph class Graph { int width, height; string color_gradient; string out, symbol = "default"; bool invert, no_zero; long long offset; long long last = 0, max_value = 0; bool current = true, tty_mode = false; unordered_flat_map> graphs = { {true, {}}, {false, {}}}; //* Create two representations of the graph to switch between to represent two values for each braille character void _create(const deque& data, int data_offset); public: Graph(); Graph( int width, int height, const string& color_gradient, const deque& data, const string& symbol="default", bool invert=false, bool no_zero=false, long long max_value=0, long long offset=0); //* Add last value from back of and return string representation of graph string& operator()(const deque& data, const bool data_same=false); //* Return string representation of graph string& operator()(); }; //* Calculate sizes of boxes, draw outlines and save to enabled boxes namespaces void calcSizes(); } namespace Proc { extern Draw::TextEdit filter; extern unordered_flat_map p_graphs; extern unordered_flat_map p_counters; }btop-1.2.3/src/btop_input.cpp000066400000000000000000000367271420276253000161570ustar00rootroot00000000000000/* 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. indent = tab tab-size = 4 */ #include #include #include #include #include #include #include #include #include #include #include #include using std::cin, std::vector, std::string_literals::operator""s; using namespace Tools; namespace rng = std::ranges; namespace Input { //* Map for translating key codes to readable values const unordered_flat_map Key_escapes = { {"\033", "escape"}, {"\n", "enter"}, {" ", "space"}, {"\x7f", "backspace"}, {"\x08", "backspace"}, {"[A", "up"}, {"OA", "up"}, {"[B", "down"}, {"OB", "down"}, {"[D", "left"}, {"OD", "left"}, {"[C", "right"}, {"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"} }; std::atomic interrupt (false); std::atomic polling (false); array mouse_pos; unordered_flat_map mouse_mappings; deque history(50, ""); string old_filter; struct InputThr { InputThr() : thr(run, this) { } static void run(InputThr* that) { that->runImpl(); } void runImpl() { char ch = 0; // TODO(pg83): read whole buffer while (cin.get(ch)) { std::lock_guard g(lock); current.push_back(ch); if (current.size() > 100) { current.clear(); } } } size_t avail() { std::lock_guard g(lock); return current.size(); } std::string get() { std::string res; { std::lock_guard g(lock); res.swap(current); } return res; } static InputThr& instance() { // intentional memory leak, to simplify shutdown process static InputThr* input = new InputThr(); return *input; } std::string current; // TODO(pg83): use std::conditional_variable instead of sleep std::mutex lock; std::thread thr; }; bool poll(int timeout) { atomic_lock lck(polling); if (timeout < 1) return InputThr::instance().avail() > 0; while (timeout > 0) { if (interrupt) { interrupt = false; return false; } if (InputThr::instance().avail() > 0) return true; sleep_ms(timeout < 10 ? timeout : 10); timeout -= 10; } return false; } string get() { string key = InputThr::instance().get(); if (not key.empty()) { //? Remove escape code prefix if present if (key.substr(0, 2) == Fx::e) { key.erase(0, 1); } //? Detect if input is an mouse event if (key.starts_with("[<")) { std::string_view key_view = key; string mouse_event; if (key_view.starts_with("[<0;") and key_view.find('M') != std::string_view::npos) { mouse_event = "mouse_click"; key_view.remove_prefix(4); } // else if (key_view.starts_with("[<0;") and key_view.ends_with('m')) { // mouse_event = "mouse_release"; // key_view.remove_prefix(4); // } else if (key_view.starts_with("[<64;")) { mouse_event = "mouse_scroll_up"; key_view.remove_prefix(5); } else if (key_view.starts_with("[<65;")) { mouse_event = "mouse_scroll_down"; key_view.remove_prefix(5); } else key.clear(); if (Config::getB("proc_filtering")) { if (mouse_event == "mouse_click") return mouse_event; else return ""; } //? Get column and line position of mouse and check for any actions mapped to current position if (not key.empty()) { try { const auto delim = key_view.find(';'); mouse_pos[0] = stoi((string)key_view.substr(0, delim)); mouse_pos[1] = stoi((string)key_view.substr(delim + 1, key_view.find('M', delim))); } catch (const std::invalid_argument&) { mouse_event.clear(); } catch (const std::out_of_range&) { mouse_event.clear(); } key = mouse_event; if (key == "mouse_click") { const auto& [col, line] = mouse_pos; for (const auto& [mapped_key, pos] : (Menu::active ? Menu::mouse_mappings : mouse_mappings)) { if (col >= pos.col and col < pos.col + pos.width and line >= pos.line and line < pos.line + pos.height) { key = mapped_key; break; } } } } } else if (Key_escapes.contains(key)) key = Key_escapes.at(key); else if (ulen(key) > 1) key.clear(); if (not key.empty()) { history.push_back(key); history.pop_front(); } } return key; } string wait() { while (InputThr::instance().avail() < 1) { sleep_ms(10); } return get(); } void clear() { // do not need it, actually } void process(const string& key) { if (key.empty()) return; try { auto& filtering = Config::getB("proc_filtering"); auto& vim_keys = Config::getB("vim_keys"); auto help_key = (vim_keys ? "H" : "h"); auto kill_key = (vim_keys ? "K" : "k"); //? Global input actions if (not filtering) { bool keep_going = false; if (str_to_lower(key) == "q") { clean_quit(0); } else if (is_in(key, "escape", "m")) { Menu::show(Menu::Menus::Main); return; } else if (is_in(key, "F1", help_key)) { Menu::show(Menu::Menus::Help); return; } else if (is_in(key, "F2", "o")) { Menu::show(Menu::Menus::Options); return; } else if (is_in(key, "1", "2", "3", "4")) { atomic_wait(Runner::active); Config::current_preset = -1; static const array boxes = {"cpu", "mem", "net", "proc"}; Config::toggle_box(boxes.at(std::stoi(key) - 1)); Draw::calcSizes(); Runner::run("all", false, true); return; } else if (is_in(key, "p", "P") and Config::preset_list.size() > 1) { if (key == "p") { if (++Config::current_preset >= (int)Config::preset_list.size()) Config::current_preset = 0; } else { if (--Config::current_preset < 0) Config::current_preset = Config::preset_list.size() - 1; } atomic_wait(Runner::active); Config::apply_preset(Config::preset_list.at(Config::current_preset)); Draw::calcSizes(); Runner::run("all", false, true); return; } else keep_going = true; if (not keep_going) return; } //? Input actions for proc box if (Proc::shown) { bool keep_going = false; bool no_update = true; bool redraw = true; if (filtering) { if (key == "enter" or key == "down") { Config::set("proc_filter", Proc::filter.text); Config::set("proc_filtering", false); old_filter.clear(); if(key == "down"){ process("down"); return; } } else if (key == "escape" or key == "mouse_click") { Config::set("proc_filter", old_filter); Config::set("proc_filtering", false); old_filter.clear(); } else if (Proc::filter.command(key)) { if (Config::getS("proc_filter") != Proc::filter.text) Config::set("proc_filter", Proc::filter.text); } else return; } else if (key == "left" or (vim_keys and key == "h")) { int cur_i = v_index(Proc::sort_vector, Config::getS("proc_sorting")); if (--cur_i < 0) cur_i = Proc::sort_vector.size() - 1; Config::set("proc_sorting", Proc::sort_vector.at(cur_i)); } else if (key == "right" or (vim_keys and key == "l")) { int cur_i = v_index(Proc::sort_vector, Config::getS("proc_sorting")); if (std::cmp_greater(++cur_i, Proc::sort_vector.size() - 1)) cur_i = 0; Config::set("proc_sorting", Proc::sort_vector.at(cur_i)); } else if (is_in(key, "f", "/")) { Config::flip("proc_filtering"); Proc::filter = { Config::getS("proc_filter") }; old_filter = Proc::filter.text; } else if (key == "e") { Config::flip("proc_tree"); no_update = false; } else if (key == "r") Config::flip("proc_reversed"); else if (key == "c") Config::flip("proc_per_core"); else if (key == "delete" and not Config::getS("proc_filter").empty()) Config::set("proc_filter", ""s); else if (key.starts_with("mouse_")) { redraw = false; const auto& [col, line] = mouse_pos; const int y = (Config::getB("show_detailed") ? Proc::y + 8 : Proc::y); const int height = (Config::getB("show_detailed") ? Proc::height - 8 : Proc::height); if (col >= Proc::x + 1 and col < Proc::x + Proc::width and line >= y + 1 and line < y + height - 1) { if (key == "mouse_click") { if (col < Proc::x + Proc::width - 2) { const auto& current_selection = Config::getI("proc_selected"); if (current_selection == line - y - 1) { redraw = true; goto proc_mouse_enter; } else if (current_selection == 0 or line - y - 1 == 0) redraw = true; Config::set("proc_selected", line - y - 1); } else if (line == y + 1) { if (Proc::selection("page_up") == -1) return; } else if (line == y + height - 2) { if (Proc::selection("page_down") == -1) return; } else if (Proc::selection("mousey" + to_string(line - y - 2)) == -1) return; } else goto proc_mouse_scroll; } else if (key == "mouse_click" and Config::getI("proc_selected") > 0) { Config::set("proc_selected", 0); redraw = true; } else keep_going = true; } else if (key == "enter") { proc_mouse_enter: if (Config::getI("proc_selected") == 0 and not Config::getB("show_detailed")) { return; } else if (Config::getI("proc_selected") > 0 and Config::getI("detailed_pid") != Config::getI("selected_pid")) { Config::set("detailed_pid", Config::getI("selected_pid")); Config::set("proc_last_selected", Config::getI("proc_selected")); Config::set("proc_selected", 0); Config::set("show_detailed", true); } else if (Config::getB("show_detailed")) { if (Config::getI("proc_last_selected") > 0) Config::set("proc_selected", Config::getI("proc_last_selected")); Config::set("proc_last_selected", 0); Config::set("detailed_pid", 0); Config::set("show_detailed", false); } } else if (is_in(key, "+", "-", "space") and Config::getB("proc_tree") and Config::getI("proc_selected") > 0) { atomic_wait(Runner::active); auto& pid = Config::getI("selected_pid"); if (key == "+" or key == "space") Proc::expand = pid; if (key == "-" or key == "space") Proc::collapse = pid; no_update = false; } else if (is_in(key, "t", kill_key) and (Config::getB("show_detailed") or Config::getI("selected_pid") > 0)) { atomic_wait(Runner::active); if (Config::getB("show_detailed") and Config::getI("proc_selected") == 0 and Proc::detailed.status == "Dead") return; Menu::show(Menu::Menus::SignalSend, (key == "t" ? SIGTERM : SIGKILL)); return; } else if (key == "s" and (Config::getB("show_detailed") or Config::getI("selected_pid") > 0)) { atomic_wait(Runner::active); if (Config::getB("show_detailed") and Config::getI("proc_selected") == 0 and Proc::detailed.status == "Dead") return; Menu::show(Menu::Menus::SignalChoose); return; } else if (is_in(key, "up", "down", "page_up", "page_down", "home", "end") or (vim_keys and is_in(key, "j", "k"))) { proc_mouse_scroll: redraw = false; auto old_selected = Config::getI("proc_selected"); auto new_selected = Proc::selection(key); if (new_selected == -1) return; else if (old_selected != new_selected and (old_selected == 0 or new_selected == 0)) redraw = true; } else keep_going = true; if (not keep_going) { Runner::run("proc", no_update, redraw); return; } } //? Input actions for cpu box if (Cpu::shown) { bool keep_going = false; bool no_update = true; bool redraw = true; static uint64_t last_press = 0; if (key == "+" and Config::getI("update_ms") <= 86399900) { int add = (Config::getI("update_ms") <= 86399000 and last_press >= time_ms() - 200 and rng::all_of(Input::history, [](const auto& str){ return str == "+"; }) ? 1000 : 100); Config::set("update_ms", Config::getI("update_ms") + add); last_press = time_ms(); redraw = true; } else if (key == "-" and Config::getI("update_ms") >= 200) { int sub = (Config::getI("update_ms") >= 2000 and last_press >= time_ms() - 200 and rng::all_of(Input::history, [](const auto& str){ return str == "-"; }) ? 1000 : 100); Config::set("update_ms", Config::getI("update_ms") - sub); last_press = time_ms(); redraw = true; } else keep_going = true; if (not keep_going) { Runner::run("cpu", no_update, redraw); return; } } //? Input actions for mem box if (Mem::shown) { bool keep_going = false; bool no_update = true; bool redraw = true; if (key == "i") { Config::flip("io_mode"); } else if (key == "d") { Config::flip("show_disks"); no_update = false; Draw::calcSizes(); } else keep_going = true; if (not keep_going) { Runner::run("mem", no_update, redraw); return; } } //? Input actions for net box if (Net::shown) { bool keep_going = false; bool no_update = true; bool redraw = true; if (is_in(key, "b", "n")) { atomic_wait(Runner::active); int c_index = v_index(Net::interfaces, Net::selected_iface); if (c_index != (int)Net::interfaces.size()) { if (key == "b") { if (--c_index < 0) c_index = Net::interfaces.size() - 1; } else if (key == "n") { if (++c_index == (int)Net::interfaces.size()) c_index = 0; } Net::selected_iface = Net::interfaces.at(c_index); Net::rescale = true; } } else if (key == "y") { Config::flip("net_sync"); Net::rescale = true; } else if (key == "a") { Config::flip("net_auto"); Net::rescale = true; } else if (key == "z") { atomic_wait(Runner::active); auto& ndev = Net::current_net.at(Net::selected_iface); if (ndev.stat.at("download").offset + ndev.stat.at("upload").offset > 0) { ndev.stat.at("download").offset = 0; ndev.stat.at("upload").offset = 0; } else { ndev.stat.at("download").offset = ndev.stat.at("download").last + ndev.stat.at("download").rollover; ndev.stat.at("upload").offset = ndev.stat.at("upload").last + ndev.stat.at("upload").rollover; } no_update = false; } else keep_going = true; if (not keep_going) { Runner::run("net", no_update, redraw); return; } } } catch (const std::exception& e) { throw std::runtime_error("Input::process(\"" + key + "\") : " + (string)e.what()); } } }btop-1.2.3/src/btop_input.hpp000066400000000000000000000034311420276253000161460ustar00rootroot00000000000000/* 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. indent = tab tab-size = 4 */ #pragma once #include #include #include #include #include using robin_hood::unordered_flat_map, std::array, std::string, std::atomic, std::deque; /* The input functions relies on the following std::cin options being set: cin.sync_with_stdio(false); cin.tie(NULL); These will automatically be set when running Term::init() from btop_tools.cpp */ //* Functions and variables for handling keyboard and mouse input namespace Input { struct Mouse_loc { int line, col, height, width; }; //? line, col, height, width extern unordered_flat_map mouse_mappings; extern atomic interrupt; extern atomic polling; //* Mouse column and line position extern array mouse_pos; //* Last entered key extern deque history; //* Poll keyboard & mouse input for ms and return input availabilty as a bool bool poll(int timeout=0); //* Get a key or mouse action from input string get(); //* Wait until input is available and return key string wait(); //* Clears last entered key void clear(); //* Process actions for input void process(const string& key); }btop-1.2.3/src/btop_menu.cpp000066400000000000000000001323221420276253000157500ustar00rootroot00000000000000/* 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. indent = tab tab-size = 4 */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include using std::deque, robin_hood::unordered_flat_map, std::array, std::views::iota, std::ref, std::max, std::min, std::ceil, std::clamp; using namespace Tools; namespace fs = std::filesystem; namespace rng = std::ranges; namespace Menu { atomic active (false); string bg; bool redraw = true; int currentMenu = -1; msgBox messageBox; int signalToSend = 0; int signalKillRet = 0; const array P_Signals = { "0", "SIGHUP", "SIGINT", "SIGQUIT", "SIGILL", "SIGTRAP", "SIGABRT", "SIGBUS", "SIGFPE", "SIGKILL", "SIGUSR1", "SIGSEGV", "SIGUSR2", "SIGPIPE", "SIGALRM", "SIGTERM", "16", "SIGCHLD", "SIGCONT", "SIGSTOP", "SIGTSTP", "SIGTTIN", "SIGTTOU", "SIGURG", "SIGXCPU", "SIGXFSZ", "SIGVTALRM", "SIGPROF", "SIGWINCH", "SIGIO", "SIGPWR", "SIGSYS" }; unordered_flat_map mouse_mappings; const array, 3> menu_normal = { array{ "┌─┐┌─┐┌┬┐┬┌─┐┌┐┌┌─┐", "│ │├─┘ │ ││ ││││└─┐", "└─┘┴ ┴ ┴└─┘┘└┘└─┘" }, { "┬ ┬┌─┐┬ ┌─┐", "├─┤├┤ │ ├─┘", "┴ ┴└─┘┴─┘┴ " }, { "┌─┐ ┬ ┬ ┬┌┬┐", "│─┼┐│ │ │ │ ", "└─┘└└─┘ ┴ ┴ " } }; const array, 3> menu_selected = { array{ "╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗", "║ ║╠═╝ ║ ║║ ║║║║╚═╗", "╚═╝╩ ╩ ╩╚═╝╝╚╝╚═╝" }, { "╦ ╦╔═╗╦ ╔═╗", "╠═╣║╣ ║ ╠═╝", "╩ ╩╚═╝╩═╝╩ " }, { "╔═╗ ╦ ╦ ╦╔╦╗ ", "║═╬╗║ ║ ║ ║ ", "╚═╝╚╚═╝ ╩ ╩ " } }; const array menu_width = {19, 12, 12}; const vector> help_text = { {"Mouse 1", "Clicks buttons and selects in process list."}, {"Mouse scroll", "Scrolls any scrollable list/text under cursor."}, {"Esc, m", "Toggles main menu."}, {"p", "Cycle view presets forwards."}, {"shift + p", "Cycle view presets backwards."}, {"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, h", "Shows this window."}, {"ctrl + z", "Sleep program and put in background."}, {"q, ctrl + c", "Quits program."}, {"+, -", "Add/Subtract 100ms to/from update timer."}, {"Up, Down", "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, Right", "Select previous/next sorting column."}, {"b, n", "Select previous/next network device."}, {"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, /", "To enter a process filter."}, {"delete", "Clear any entered filter."}, {"c", "Toggle per-core cpu usage of processes."}, {"r", "Reverse sorting order in processes box."}, {"e", "Toggle processes tree view."}, {"Selected +, -", "Expand/collapse the selected process in tree view."}, {"Selected t", "Terminate selected process with SIGTERM - 15."}, {"Selected k", "Kill selected process with SIGKILL - 9."}, {"Selected s", "Select or enter signal to send to process."}, {"", " "}, {"", "For bug reporting and project updates, visit:"}, {"", "https://github.com/aristocratos/btop"}, }; const vector>> categories = { { {"color_theme", "Set color theme.", "", "Choose from all theme files in (usually)", "\"/usr/[local/]share/btop/themes\" and", "\"~/.config/btop/themes\".", "", "\"Default\" for builtin default theme.", "\"TTY\" for builtin 16-color theme.", "", "For theme updates see:", "https://github.com/aristocratos/btop"}, {"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.", "", "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."}, {"force_tty", "TTY mode.", "", "Set to true to force tty mode regardless", "if a real tty has been detected or not.", "", "Will force 16-color mode and TTY theme,", "set all graph symbols to \"tty\" and swap", "out other non tty friendly symbols."}, {"vim_keys", "Enable vim keys.", "Set to True to enable \"h,j,k,l\" keys for", "directional control in lists.", "", "Conflicting keys for", "h (help) and k (kill)", "is accessible while holding shift."}, {"presets", "Define presets for the layout of the boxes.", "", "Preset 0 is always all boxes shown with", "default settings.", "Max 9 presets.", "", "Format: \"box_name:P:G,box_name:P:G\"", "P=(0 or 1) for alternate positions.", "G=graph symbol to use for box.", "", "Use withespace \" \" as separator between", "different presets.", "", "Example:", "\"mem:0:tty,proc:1:default cpu:0:braille\""}, {"shown_boxes", "Manually set which boxes to show.", "", "Available values are \"cpu mem net proc\".", "Separate values with whitespace.", "", "Toggle between presets with key \"p\"."}, {"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."}, {"rounded_corners", "Rounded corners on boxes.", "", "True or False", "", "Is always False if TTY mode is ON."}, {"graph_symbol", "Default symbols to use for graph creation.", "", "\"braille\", \"block\" or \"tty\".", "", "\"braille\" offers the highest resolution but", "might not be included in all fonts.", "", "\"block\" has half the resolution of braille", "but uses more common characters.", "", "\"tty\" uses only 3 different symbols but will", "work with most fonts.", "", "Note that \"tty\" only has half the horizontal", "resolution of the other two,", "so will show a shorter historical view."}, {"clock_format", "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"}, {"base_10_sizes", "Use base 10 for bits and bytes sizes.", "", "Uses KB = 1000 instead of KiB = 1024,", "MB = 1000KB instead of MiB = 1024KiB,", "and so on.", "", "True or False."}, {"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."}, {"selected_battery", "Select battery.", "", "Which battery to use if multiple are present.", "Can be both batteries and UPS.", "", "\"Auto\" for auto detection."}, {"log_level", "Set loglevel for error.log", "", "\"ERROR\", \"WARNING\", \"INFO\" and \"DEBUG\".", "", "The level set includes all lower levels,", "i.e. \"DEBUG\" will show all logging info."} }, { {"cpu_bottom", "Cpu box location.", "", "Show cpu box at bottom of screen instead", "of top."}, {"graph_symbol_cpu", "Graph symbol to use for graphs in cpu box.", "", "\"default\", \"braille\", \"block\" or \"tty\".", "", "\"default\" for the general default symbol.",}, {"cpu_graph_upper", "Cpu upper graph.", "", "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.", "+ more depending on kernel."}, {"cpu_graph_lower", "Cpu lower graph.", "", "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.", "+ more depending on kernel."}, {"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."}, {"cpu_core_map", "Custom mapping between core and coretemp.", "", "Can be needed on certain cpus to get correct", "temperature for correct core.", "", "Use lm-sensors or similar to see which cores", "are reporting temperatures on your machine.", "", "Format: \"X:Y\"", "X=core with wrong temp.", "Y=core with correct temp.", "Use space as separator between multiple", "entries.", "", "Example: \"4:0 5:1 6:3\""}, {"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 = abosulte 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 certain kernel versions."}, {"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_below_net", "Mem box location.", "", "Show mem box below net box instead of above."}, {"graph_symbol_mem", "Graph symbol to use for graphs in mem box.", "", "\"default\", \"braille\", \"block\" or \"tty\".", "", "\"default\" for the general default symbol.",}, {"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 IO activity graphs.", "", "Show small IO graphs that for disk activity", "(disk busy time) when not in IO mode.", "", "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.", "(100 MiB/s by default).", "", "Format: \"device:speed\" separate disks with", "whitespace \" \".", "", "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", "(Linux) Read disks list from /etc/fstab.", "", "This also disables only_physical.", "", "True or False."}, {"disk_free_priv", "(Linux) Type of available disk space.", "", "Set to true to show how much disk space is", "available for privileged users.", "", "Set to false to show available for normal", "users."}, {"disks_filter", "Optional filter for shown disks.", "", "Should be full path of a mountpoint.", "Separate multiple values with", "whitespace \" \".", "", "Begin line with \"exclude=\" to change to", "exclude filter.", "Oterwise defaults to \"most include\" filter.", "", "Example:", "\"exclude=/boot /home/user\""}, }, { {"graph_symbol_net", "Graph symbol to use for graphs in net box.", "", "\"default\", \"braille\", \"block\" or \"tty\".", "", "\"default\" for the general default symbol.",}, {"net_download", "Fixed network graph download value.", "", "Value in Mebibits, default \"100\".", "", "Can be toggled with auto button."}, {"net_upload", "Fixed network graph upload value.", "", "Value in Mebibits, default \"100\".", "", "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_iface", "Network Interface.", "", "Manually set the starting Network Interface.", "", "Will otherwise automatically choose the NIC", "with the highest total download since boot."}, }, { {"proc_left", "Proc box location.", "", "Show proc box on left side of screen", "instead of right."}, {"graph_symbol_proc", "Graph symbol to use for graphs in proc box.", "", "\"default\", \"braille\", \"block\" or \"tty\".", "", "\"default\" for the general default symbol.",}, {"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."}, {"proc_colors", "Enable colors in process view.", "", "True or False."}, {"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.", " ", "Will show percentage of total memory", "if False."}, } }; msgBox::msgBox() {} msgBox::msgBox(int width, int boxtype, vector content, string title) : width(width), boxtype(boxtype) { const auto& tty_mode = Config::getB("tty_mode"); const auto& rounded = Config::getB("rounded_corners"); const auto& right_up = (tty_mode or not rounded ? Symbols::right_up : Symbols::round_right_up); const auto& left_up = (tty_mode or not rounded ? Symbols::left_up : Symbols::round_left_up); const auto& right_down = (tty_mode or not rounded ? Symbols::right_down : Symbols::round_right_down); const auto& left_down = (tty_mode or not rounded ? Symbols::left_down : Symbols::round_left_down); height = content.size() + 7; x = Term::width / 2 - width / 2; y = Term::height/2 - height/2; if (boxtype == 2) selected = 1; button_left = left_up + Symbols::h_line * 6 + Mv::l(7) + Mv::d(2) + left_down + Symbols::h_line * 6 + Mv::l(7) + Mv::u(1) + Symbols::v_line; button_right = Symbols::v_line + Mv::l(7) + Mv::u(1) + Symbols::h_line * 6 + right_up + Mv::l(7) + Mv::d(2) + Symbols::h_line * 6 + right_down + Mv::u(2); box_contents = Draw::createBox(x, y, width, height, Theme::c("hi_fg"), true, title) + Mv::d(1); for (const auto& line : content) { box_contents += Mv::save + Mv::r(max((size_t)0, (width / 2) - (Fx::uncolor(line).size() / 2) - 1)) + line + Mv::restore + Mv::d(1); } } string msgBox::operator()() { string out; int pos = width / 2 - (boxtype == 0 ? 6 : 14); auto& first_color = (selected == 0 ? Theme::c("hi_fg") : Theme::c("div_line")); out = Mv::d(1) + Mv::r(pos) + Fx::b + first_color + button_left + (selected == 0 ? Theme::c("title") : Theme::c("main_fg") + Fx::ub) + (boxtype == 0 ? " Ok " : " Yes ") + first_color + button_right; mouse_mappings["button1"] = Input::Mouse_loc{y + height - 4, x + pos + 1, 3, 12 + (boxtype > 0 ? 1 : 0)}; if (boxtype > 0) { auto& second_color = (selected == 1 ? Theme::c("hi_fg") : Theme::c("div_line")); out += Mv::r(2) + second_color + button_left + (selected == 1 ? Theme::c("title") : Theme::c("main_fg") + Fx::ub) + " No " + second_color + button_right; mouse_mappings["button2"] = Input::Mouse_loc{y + height - 4, x + pos + 15 + (boxtype > 0 ? 1 : 0), 3, 12}; } return box_contents + out + Fx::reset; } //? Process input int msgBox::input(string key) { if (key.empty()) return Invalid; if (is_in(key, "escape", "backspace", "q") or key == "button2") { return No_Esc; } else if (key == "button1" or (boxtype == 0 and str_to_upper(key) == "O")) { return Ok_Yes; } else if (is_in(key, "enter", "space")) { return selected + 1; } else if (boxtype == 0) { return Invalid; } else if (str_to_upper(key) == "Y") { return Ok_Yes; } else if (str_to_upper(key) == "N") { return No_Esc; } else if (is_in(key, "right", "tab")) { if (++selected > 1) selected = 0; return Select; } else if (is_in(key, "left", "shift_tab")) { if (--selected < 0) selected = 1; return Select; } return Invalid; } void msgBox::clear() { box_contents.clear(); box_contents.shrink_to_fit(); button_left.clear(); button_left.shrink_to_fit(); button_right.clear(); button_right.shrink_to_fit(); if (mouse_mappings.contains("button1")) mouse_mappings.erase("button1"); if (mouse_mappings.contains("button2")) mouse_mappings.erase("button2"); } enum menuReturnCodes { NoChange, Changed, Closed, Switch }; int signalChoose(const string& key) { auto& s_pid = (Config::getB("show_detailed") and Config::getI("selected_pid") == 0 ? Config::getI("detailed_pid") : Config::getI("selected_pid")); static int x = 0, y = 0, selected_signal = -1; if (bg.empty()) selected_signal = -1; auto& out = Global::overlay; int retval = Changed; if (redraw) { x = Term::width/2 - 40; y = Term::height/2 - 9; bg = Draw::createBox(x + 2, y, 78, 19, Theme::c("hi_fg"), true, "signals"); bg += Mv::to(y+2, x+3) + Theme::c("title") + Fx::b + cjust("Send signal to PID " + to_string(s_pid) + " (" + uresize((s_pid == Config::getI("detailed_pid") ? Proc::detailed.entry.name : Config::getS("selected_name")), 30) + ")", 76); } else if (is_in(key, "escape", "q")) { return Closed; } else if (key.starts_with("button_")) { if (int new_select = stoi(key.substr(7)); new_select == selected_signal) goto ChooseEntering; else selected_signal = new_select; } else if (is_in(key, "enter", "space") and selected_signal >= 0) { ChooseEntering: signalKillRet = 0; if (s_pid < 1) { signalKillRet = ESRCH; menuMask.set(SignalReturn); } else if (kill(s_pid, selected_signal) != 0) { signalKillRet = errno; menuMask.set(SignalReturn); } return Closed; } else if (key.size() == 1 and isdigit(key.at(0)) and selected_signal < 10) { selected_signal = std::min(std::stoi((selected_signal < 1 ? key : to_string(selected_signal) + key)), 64); } else if (key == "backspace" and selected_signal != -1) { selected_signal = (selected_signal < 10 ? -1 : selected_signal / 10); } else if (is_in(key, "up", "k") and selected_signal != 16) { if (selected_signal == 1) selected_signal = 31; else if (selected_signal < 6) selected_signal += 25; else { bool offset = (selected_signal > 16); selected_signal -= 5; if (selected_signal <= 16 and offset) selected_signal--; } } else if (is_in(key, "down", "j")) { if (selected_signal == 31) selected_signal = 1; else if (selected_signal < 1 or selected_signal == 16) selected_signal = 1; else if (selected_signal > 26) selected_signal -= 25; else { bool offset = (selected_signal < 16); selected_signal += 5; if (selected_signal >= 16 and offset) selected_signal++; if (selected_signal > 31) selected_signal = 31; } } else if (is_in(key, "left", "h") and selected_signal > 0 and selected_signal != 16) { if (--selected_signal < 1) selected_signal = 31; else if (selected_signal == 16) selected_signal--; } else if (is_in(key, "right", "l") and selected_signal <= 31 and selected_signal != 16) { if (++selected_signal > 31) selected_signal = 1; else if (selected_signal == 16) selected_signal++; } else { retval = NoChange; } if (retval == Changed) { int cy = y+4, cx = x+4; out = bg + Mv::to(cy++, x+3) + Theme::c("main_fg") + Fx::ub + rjust("Enter signal number: ", 48) + Theme::c("hi_fg") + (selected_signal >= 0 ? to_string(selected_signal) : "") + Theme::c("main_fg") + Fx::bl + "█" + Fx::ubl; auto sig_str = to_string(selected_signal); for (int count = 0, i = 0; const auto& sig : P_Signals) { if (count == 0 or count == 16) { count++; continue; } if (i++ % 5 == 0) { ++cy; cx = x+4; } out += Mv::to(cy, cx); if (count == selected_signal) out += Theme::c("selected_bg") + Theme::c("selected_fg") + Fx::b + ljust(to_string(count), 3) + ljust('(' + sig + ')', 12) + Fx::reset; else out += Theme::c("hi_fg") + ljust(to_string(count), 3) + Theme::c("main_fg") + ljust('(' + sig + ')', 12); if (redraw) mouse_mappings["button_" + to_string(count)] = {cy, cx, 1, 15}; count++; cx += 15; } cy++; out += Mv::to(++cy, x+3) + Fx::b + Theme::c("hi_fg") + rjust( "↑ ↓ ← →", 33, true) + Theme::c("main_fg") + Fx::ub + " | To choose signal."; out += Mv::to(++cy, x+3) + Fx::b + Theme::c("hi_fg") + rjust("0-9", 33) + Theme::c("main_fg") + Fx::ub + " | Enter manually."; out += Mv::to(++cy, x+3) + Fx::b + Theme::c("hi_fg") + rjust("ENTER", 33) + Theme::c("main_fg") + Fx::ub + " | To send signal."; mouse_mappings["enter"] = {cy, x, 1, 73}; out += Mv::to(++cy, x+3) + Fx::b + Theme::c("hi_fg") + rjust("ESC or \"q\"", 33) + Theme::c("main_fg") + Fx::ub + " | To abort."; mouse_mappings["escape"] = {cy, x, 1, 73}; out += Fx::reset; } return (redraw ? Changed : retval); } int sizeError(const string& key) { if (redraw) { vector cont_vec; cont_vec.push_back(Fx::b + Theme::g("used")[100] + "Error:" + Theme::c("main_fg") + Fx::ub); cont_vec.push_back("Terminal size to small to" + Fx::reset); cont_vec.push_back("display menu or box!" + Fx::reset); messageBox = Menu::msgBox{45, 0, cont_vec, "error"}; Global::overlay = messageBox(); } auto ret = messageBox.input(key); if (ret == msgBox::Ok_Yes or ret == msgBox::No_Esc) { messageBox.clear(); return Closed; } else if (redraw) { return Changed; } return NoChange; } int signalSend(const string& key) { auto& s_pid = (Config::getB("show_detailed") and Config::getI("selected_pid") == 0 ? Config::getI("detailed_pid") : Config::getI("selected_pid")); if (s_pid == 0) return Closed; if (redraw) { atomic_wait(Runner::active); auto& p_name = (s_pid == Config::getI("detailed_pid") ? Proc::detailed.entry.name : Config::getS("selected_name")); vector cont_vec = { Fx::b + Theme::c("main_fg") + "Send signal: " + Fx::ub + Theme::c("hi_fg") + to_string(signalToSend) + (signalToSend > 0 and signalToSend <= 32 ? Theme::c("main_fg") + " (" + P_Signals.at(signalToSend) + ')' : ""), Fx::b + Theme::c("main_fg") + "To PID: " + Fx::ub + Theme::c("hi_fg") + to_string(s_pid) + Theme::c("main_fg") + " (" + uresize(p_name, 16) + ')' + Fx::reset, }; messageBox = Menu::msgBox{50, 1, cont_vec, (signalToSend > 1 and signalToSend <= 32 and signalToSend != 17 ? P_Signals.at(signalToSend) : "signal")}; Global::overlay = messageBox(); } auto ret = messageBox.input(key); if (ret == msgBox::Ok_Yes) { signalKillRet = 0; if (kill(s_pid, signalToSend) != 0) { signalKillRet = errno; menuMask.set(SignalReturn); } messageBox.clear(); return Closed; } else if (ret == msgBox::No_Esc) { messageBox.clear(); return Closed; } else if (ret == msgBox::Select) { Global::overlay = messageBox(); return Changed; } else if (redraw) { return Changed; } return NoChange; } int signalReturn(const string& key) { if (redraw) { vector cont_vec; cont_vec.push_back(Fx::b + Theme::g("used")[100] + "Failure:" + Theme::c("main_fg") + Fx::ub); if (signalKillRet == EINVAL) { cont_vec.push_back("Unsupported signal!" + Fx::reset); } else if (signalKillRet == EPERM) { cont_vec.push_back("Insufficient permissions to send signal!" + Fx::reset); } else if (signalKillRet == ESRCH) { cont_vec.push_back("Process not found!" + Fx::reset); } else { cont_vec.push_back("Unknown error! (errno: " + to_string(signalKillRet) + ')' + Fx::reset); } messageBox = Menu::msgBox{50, 0, cont_vec, "error"}; Global::overlay = messageBox(); } auto ret = messageBox.input(key); if (ret == msgBox::Ok_Yes or ret == msgBox::No_Esc) { messageBox.clear(); return Closed; } else if (redraw) { return Changed; } return NoChange; } int mainMenu(const string& key) { enum MenuItems { Options, Help, Quit }; static int y = 0, selected = 0; static vector colors_selected; static vector colors_normal; auto& tty_mode = Config::getB("tty_mode"); if (bg.empty()) selected = 0; int retval = Changed; if (redraw) { y = Term::height/2 - 10; bg = Draw::banner_gen(y, 0, true); if (not tty_mode) { colors_selected = { Theme::hex_to_color(Global::Banner_src.at(0).at(0)), Theme::hex_to_color(Global::Banner_src.at(2).at(0)), Theme::hex_to_color(Global::Banner_src.at(4).at(0)) }; colors_normal = { Theme::hex_to_color("#CC"), Theme::hex_to_color("#AA"), Theme::hex_to_color("#80") }; } } else if (is_in(key, "escape", "q", "m", "mouse_click")) { return Closed; } else if (key.starts_with("button_")) { if (int new_select = key.back() - '0'; new_select == selected) goto MainEntering; else selected = new_select; } else if (is_in(key, "enter", "space")) { MainEntering: switch (selected) { case Options: menuMask.set(Menus::Options); currentMenu = Menus::Options; return Switch; case Help: menuMask.set(Menus::Help); currentMenu = Menus::Help; return Switch; case Quit: clean_quit(0); } } else if (is_in(key, "down", "tab", "mouse_scroll_down", "j")) { if (++selected > 2) selected = 0; } else if (is_in(key, "up", "shift_tab", "mouse_scroll_up", "k")) { if (--selected < 0) selected = 2; } else { retval = NoChange; } if (retval == Changed) { auto& out = Global::overlay; out = bg + Fx::reset + Fx::b; auto cy = y + 7; for (const auto& i : iota(0, 3)) { if (tty_mode) out += (i == selected ? Theme::c("hi_fg") : Theme::c("main_fg")); const auto& menu = (not tty_mode and i == selected ? menu_selected[i] : menu_normal[i]); const auto& colors = (i == selected ? colors_selected : colors_normal); if (redraw) mouse_mappings["button_" + to_string(i)] = {cy, Term::width/2 - menu_width[i]/2, 3, menu_width[i]}; for (int ic = 0; const auto& line : menu) { out += Mv::to(cy++, Term::width/2 - menu_width[i]/2) + (tty_mode ? "" : colors[ic++]) + line; } } out += Fx::reset; } return (redraw ? Changed : retval); } int optionsMenu(const string& key) { enum Predispositions { isBool, isInt, isString, is2D, isBrowseable, isEditable}; static int y = 0, x = 0, height = 0, page = 0, pages = 0, selected = 0, select_max = 0, item_height = 0, selected_cat = 0, max_items = 0, last_sel = 0; static bool editing = false; static Draw::TextEdit editor; static string warnings; static bitset<8> selPred; static const unordered_flat_map>> optionsList = { {"color_theme", std::cref(Theme::themes)}, {"log_level", std::cref(Logger::log_levels)}, {"temp_scale", std::cref(Config::temp_scales)}, {"proc_sorting", std::cref(Proc::sort_vector)}, {"graph_symbol", std::cref(Config::valid_graph_symbols)}, {"graph_symbol_cpu", std::cref(Config::valid_graph_symbols_def)}, {"graph_symbol_mem", std::cref(Config::valid_graph_symbols_def)}, {"graph_symbol_net", std::cref(Config::valid_graph_symbols_def)}, {"graph_symbol_proc", std::cref(Config::valid_graph_symbols_def)}, {"cpu_graph_upper", std::cref(Cpu::available_fields)}, {"cpu_graph_lower", std::cref(Cpu::available_fields)}, {"cpu_sensor", std::cref(Cpu::available_sensors)}, {"selected_battery", std::cref(Config::available_batteries)}, }; auto& tty_mode = Config::getB("tty_mode"); auto& vim_keys = Config::getB("vim_keys"); if (max_items == 0) { for (const auto& cat : categories) { if ((int)cat.size() > max_items) max_items = cat.size(); } } if (bg.empty()) { page = selected = selected_cat = last_sel = 0; redraw = true; Theme::updateThemes(); } int retval = Changed; bool recollect = false; bool screen_redraw = false; bool theme_refresh = false; //? Draw background if needed else process input if (redraw) { mouse_mappings.clear(); selPred.reset(); y = max(1, Term::height/2 - 3 - max_items); x = Term::width/2 - 39; height = min(Term::height - 7, max_items * 2 + 4); if (height % 2 != 0) height--; bg = Draw::banner_gen(y, 0, true) + Draw::createBox(x, y + 6, 78, height, Theme::c("hi_fg"), true, "tab" + Symbols::right) + Mv::to(y+8, x) + Theme::c("hi_fg") + Symbols::div_left + Theme::c("div_line") + Symbols::h_line * 29 + Symbols::div_up + Symbols::h_line * (78 - 32) + Theme::c("hi_fg") + Symbols::div_right + Mv::to(y+6+height - 1, x+30) + Symbols::div_down + Theme::c("div_line"); for (const auto& i : iota(0, height - 4)) { bg += Mv::to(y+9 + i, x + 30) + Symbols::v_line; } } else if (not warnings.empty() and not key.empty()) { auto ret = messageBox.input(key); if (ret == msgBox::msgReturn::Ok_Yes or ret == msgBox::msgReturn::No_Esc) { warnings.clear(); messageBox.clear(); } } else if (editing and not key.empty()) { if (is_in(key, "escape", "mouse_click")) { editor.clear(); editing = false; } else if (key == "enter") { const auto& option = categories[selected_cat][item_height * page + selected][0]; if (selPred.test(isString) and Config::stringValid(option, editor.text)) { Config::set(option, editor.text); if (option == "custom_cpu_name") screen_redraw = true; else if (is_in(option, "shown_boxes", "presets")) { screen_redraw = true; Config::current_preset = -1; } else if (option == "clock_format") { Draw::update_clock(true); screen_redraw = true; } else if (option == "cpu_core_map") { atomic_wait(Runner::active); Cpu::core_mapping = Cpu::get_core_mapping(); } } else if (selPred.test(isInt) and Config::intValid(option, editor.text)) { Config::set(option, stoi(editor.text)); } else warnings = Config::validError; editor.clear(); editing = false; } else if (not editor.command(key)) retval = NoChange; } else if (key == "mouse_click") { const auto [mouse_x, mouse_y] = Input::mouse_pos; if (mouse_x < x or mouse_x > x + 80 or mouse_y < y + 6 or mouse_y > y + 6 + height) { return Closed; } else if (mouse_x < x + 30 and mouse_y > y + 8) { auto m_select = ceil((double)(mouse_y - y - 8) / 2) - 1; if (selected != m_select) selected = m_select; else if (selPred.test(isEditable)) goto mouseEnter; else retval = NoChange; } } else if (is_in(key, "enter", "e", "E") and selPred.test(isEditable)) { mouseEnter: const auto& option = categories[selected_cat][item_height * page + selected][0]; editor = Draw::TextEdit{Config::getAsString(option), selPred.test(isInt)}; editing = true; mouse_mappings.clear(); } else if (is_in(key, "escape", "q", "o", "backspace")) { return Closed; } else if (is_in(key, "down", "mouse_scroll_down") or (vim_keys and key == "j")) { if (++selected > select_max or selected >= item_height) { if (page < pages - 1) page++; else if (pages > 1) page = 0; selected = 0; } } else if (is_in(key, "up", "mouse_scroll_up") or (vim_keys and key == "k")) { if (--selected < 0) { if (page > 0) page--; else if (pages > 1) page = pages - 1; selected = item_height - 1; } } else if (pages > 1 and key == "page_down") { if (++page >= pages) page = 0; selected = 0; } else if (pages > 1 and key == "page_up") { if (--page < 0) page = pages - 1; selected = 0; } else if (key == "tab") { if (++selected_cat >= (int)categories.size()) selected_cat = 0; page = selected = 0; } else if (key == "shift_tab") { if (--selected_cat < 0) selected_cat = (int)categories.size() - 1; page = selected = 0; } else if (is_in(key, "1", "2", "3", "4", "5") or key.starts_with("select_cat_")) { selected_cat = key.back() - '0' - 1; page = selected = 0; } else if (is_in(key, "left", "right") or (vim_keys and is_in(key, "h", "l"))) { const auto& option = categories[selected_cat][item_height * page + selected][0]; if (selPred.test(isInt)) { const int mod = (option == "update_ms" ? 100 : 1); long value = Config::getI(option); if (key == "right" or (vim_keys and key == "l")) value += mod; else value -= mod; if (Config::intValid(option, to_string(value))) Config::set(option, static_cast(value)); else { warnings = Config::validError; } } else if (selPred.test(isBool)) { Config::flip(option); screen_redraw = true; if (option == "truecolor") { theme_refresh = true; Config::flip("lowcolor"); } else if (option == "force_tty") { theme_refresh = true; Config::flip("tty_mode"); } else if (is_in(option, "rounded_corners", "theme_background")) theme_refresh = true; else if (option == "background_update") { Runner::pause_output = false; } else if (option == "base_10_sizes") { recollect = true; } } else if (selPred.test(isBrowseable)) { auto& optList = optionsList.at(option).get(); int i = v_index(optList, Config::getS(option)); if ((key == "right" or (vim_keys and key == "l")) and ++i >= (int)optList.size()) i = 0; else if ((key == "left" or (vim_keys and key == "h")) and --i < 0) i = optList.size() - 1; Config::set(option, optList.at(i)); if (option == "color_theme") theme_refresh = true; else if (option == "log_level") { Logger::set(optList.at(i)); Logger::info("Logger set to " + optList.at(i)); } else if (is_in(option, "proc_sorting", "cpu_sensor") or option.starts_with("graph_symbol") or option.starts_with("cpu_graph_")) screen_redraw = true; } else retval = NoChange; } else { retval = NoChange; } //? Draw the menu if (retval == Changed) { Config::unlock(); auto& out = Global::overlay; out = bg; item_height = min((int)categories[selected_cat].size(), (int)floor((double)(height - 4) / 2)); pages = ceil((double)categories[selected_cat].size() / item_height); if (page > pages - 1) page = pages - 1; select_max = min(item_height - 1, (int)categories[selected_cat].size() - 1 - item_height * page); if (selected > select_max) { selected = select_max; } //? Get variable properties for currently selected option if (selPred.none() or last_sel != (selected_cat << 8) + selected) { selPred.reset(); last_sel = (selected_cat << 8) + selected; const auto& selOption = categories[selected_cat][item_height * page + selected][0]; if (Config::ints.contains(selOption)) selPred.set(isInt); else if (Config::bools.contains(selOption)) selPred.set(isBool); else selPred.set(isString); if (not selPred.test(isString)) selPred.set(is2D); else if (optionsList.contains(selOption)) { selPred.set(isBrowseable); } if (not selPred.test(isBrowseable) and (selPred.test(isString) or selPred.test(isInt))) selPred.set(isEditable); } //? Category buttons out += Mv::to(y+7, x+4); for (int i = 0; const auto& m : {"general", "cpu", "mem", "net", "proc"}) { out += Fx::b + (i == selected_cat ? Theme::c("hi_fg") + '[' + Theme::c("title") + m + Theme::c("hi_fg") + ']' : Theme::c("hi_fg") + to_string(i + 1) + Theme::c("title") + m + ' ') + Mv::r(10); if (string button_name = "select_cat_" + to_string(i + 1); not editing and not mouse_mappings.contains(button_name)) mouse_mappings[button_name] = {y+6, x+2 + 15*i, 3, 15}; i++; } if (pages > 1) { out += Mv::to(y+6 + height - 1, x+2) + Theme::c("hi_fg") + Symbols::title_left_down + Fx::b + Symbols::up + Theme::c("title") + " page " + to_string(page+1) + '/' + to_string(pages) + ' ' + Theme::c("hi_fg") + Symbols::down + Fx::ub + Symbols::title_right_down; } //? Option name and value auto cy = y+9; for (int c = 0, i = max(0, item_height * page); c++ < item_height and i < (int)categories[selected_cat].size(); i++) { const auto& option = categories[selected_cat][i][0]; const auto& value = (option == "color_theme" ? (string) fs::path(Config::getS("color_theme")).stem() : Config::getAsString(option)); out += Mv::to(cy++, x + 1) + (c-1 == selected ? Theme::c("selected_bg") + Theme::c("selected_fg") : Theme::c("title")) + Fx::b + cjust(capitalize(s_replace(option, "_", " ")) + (c-1 == selected and selPred.test(isBrowseable) ? ' ' + to_string(v_index(optionsList.at(option).get(), (option == "color_theme" ? Config::getS("color_theme") : value)) + 1) + '/' + to_string(optionsList.at(option).get().size()) : ""), 29); out += Mv::to(cy++, x + 1) + (c-1 == selected ? "" : Theme::c("main_fg")) + Fx::ub + " " + (c-1 == selected and editing ? cjust(editor(24), 34, true) : cjust(value, 25, true)) + " "; if (c-1 == selected) { if (not editing and (selPred.test(is2D) or selPred.test(isBrowseable))) { out += Fx::b + Mv::to(cy-1, x+2) + Symbols::left + Mv::to(cy-1, x+28) + Symbols::right; mouse_mappings["left"] = {cy-2, x, 2, 5}; mouse_mappings["right"] = {cy-2, x+25, 2, 5}; } if (selPred.test(isEditable)) { out += Fx::b + Mv::to(cy-1, x+28 - (not editing and selPred.test(isInt) ? 2 : 0)) + (tty_mode ? "E" : Symbols::enter); } //? Description of selected option out += Fx::reset + Theme::c("title") + Fx::b; for (int cyy = y+7; const auto& desc : categories[selected_cat][i]) { if (cyy++ == y+7) continue; else if (cyy == y+10) out += Theme::c("main_fg") + Fx::ub; else if (cyy > y + height + 4) break; out += Mv::to(cyy, x+32) + desc; } } } if (not warnings.empty()) { messageBox = msgBox{min(78, (int)ulen(warnings) + 10), msgBox::BoxTypes::OK, {uresize(warnings, 74)}, "warning"}; out += messageBox(); } out += Fx::reset; } if (theme_refresh) { Theme::setTheme(); Draw::banner_gen(0, 0, false, true); screen_redraw = true; redraw = true; optionsMenu(""); } if (screen_redraw) { auto overlay_bkp = move(Global::overlay); auto clock_bkp = move(Global::clock); Draw::calcSizes(); Global::overlay = move(overlay_bkp); Global::clock = move(clock_bkp); recollect = true; } if (recollect) { Runner::run("all", false, true); retval = NoChange; } return (redraw ? Changed : retval); } int helpMenu(const string& key) { static int y = 0, x = 0, height = 0, page = 0, pages = 0; if (bg.empty()) page = 0; int retval = Changed; if (redraw) { y = max(1, Term::height/2 - 4 - (int)(help_text.size() / 2)); x = Term::width/2 - 39; height = min(Term::height - 6, (int)help_text.size() + 3); pages = ceil((double)help_text.size() / (height - 3)); page = 0; bg = Draw::banner_gen(y, 0, true); bg += Draw::createBox(x, y + 6, 78, height, Theme::c("hi_fg"), true, "help"); } else if (is_in(key, "escape", "q", "h", "backspace", "space", "enter", "mouse_click")) { return Closed; } else if (pages > 1 and is_in(key, "down", "page_down", "tab", "mouse_scroll_down")) { if (++page >= pages) page = 0; } else if (pages > 1 and is_in(key, "up", "page_up", "shift_tab", "mouse_scroll_up")) { if (--page < 0) page = pages - 1; } else { retval = NoChange; } if (retval == Changed) { auto& out = Global::overlay; out = bg; if (pages > 1) { out += Mv::to(y+height+6, x + 2) + Theme::c("hi_fg") + Symbols::title_left_down + Fx::b + Symbols::up + Theme::c("title") + " page " + to_string(page+1) + '/' + to_string(pages) + ' ' + Theme::c("hi_fg") + Symbols::down + Fx::ub + Symbols::title_right_down; } auto cy = y+7; out += Mv::to(cy++, x + 1) + Theme::c("title") + Fx::b + cjust("Key:", 20) + "Description:"; for (int c = 0, i = max(0, (height - 3) * page); c++ < height - 3 and i < (int)help_text.size(); i++) { out += Mv::to(cy++, x + 1) + Theme::c("hi_fg") + Fx::b + cjust(help_text[i][0], 20) + Theme::c("main_fg") + Fx::ub + help_text[i][1]; } out += Fx::reset; } return (redraw ? Changed : retval); } //* Add menus here and update enum Menus in header const auto menuFunc = vector{ ref(sizeError), ref(signalChoose), ref(signalSend), ref(signalReturn), ref(optionsMenu), ref(helpMenu), ref(mainMenu), }; bitset<8> menuMask; void process(string key) { if (menuMask.none()) { Menu::active = false; Global::overlay.clear(); Global::overlay.shrink_to_fit(); Runner::pause_output = false; bg.clear(); bg.shrink_to_fit(); currentMenu = -1; Runner::run("all", true, true); mouse_mappings.clear(); return; } if (currentMenu < 0 or not menuMask.test(currentMenu)) { Menu::active = true; redraw = true; if (((menuMask.test(Main) or menuMask.test(Options) or menuMask.test(Help) or menuMask.test(SignalChoose)) and (Term::width < 80 or Term::height < 24)) or (Term::width < 50 or Term::height < 20)) { menuMask.reset(); menuMask.set(SizeError); } for (const auto& i : iota(0, (int)menuMask.size())) { if (menuMask.test(i)) currentMenu = i; } } auto retCode = menuFunc.at(currentMenu)(key); if (retCode == Closed) { menuMask.reset(currentMenu); mouse_mappings.clear(); bg.clear(); Runner::pause_output = false; process(); } else if (redraw) { redraw = false; Runner::run("all", true, true); } else if (retCode == Changed) Runner::run("overlay"); else if (retCode == Switch) { Runner::pause_output = false; bg.clear(); redraw = true; mouse_mappings.clear(); process(); } } void show(int menu, int signal) { menuMask.set(menu); signalToSend = signal; process(); } } btop-1.2.3/src/btop_menu.hpp000066400000000000000000000042421420276253000157540ustar00rootroot00000000000000/* 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. indent = tab tab-size = 4 */ #pragma once #include #include #include #include #include using std::string, std::atomic, std::vector, std::bitset; namespace Menu { extern atomic active; extern string output; extern int signalToSend; extern bool redraw; //? line, col, height, width extern unordered_flat_map mouse_mappings; //* Creates a message box centered on screen //? Height of box is determined by size of content vector //? Boxtypes: 0 = OK button | 1 = YES and NO with YES selected | 2 = Same as 1 but with NO selected //? Strings in content vector is not checked for box width overflow class msgBox { string box_contents, button_left, button_right; int height = 0, width = 0, boxtype = 0, selected = 0, x = 0, y = 0; public: enum BoxTypes { OK, YES_NO, NO_YES }; enum msgReturn { Invalid, Ok_Yes, No_Esc, Select }; msgBox(); msgBox(int width, int boxtype, vector content, string title); //? Draw and return box as a string string operator()(); //? Process input and returns value from enum Ret int input(string key); //? Clears content vector and private strings void clear(); }; extern bitset<8> menuMask; //* Enum for functions in vector menuFuncs enum Menus { SizeError, SignalChoose, SignalSend, SignalReturn, Options, Help, Main }; //* Handles redirection of input for menu functions and handles return codes void process(string key=""); //* Show a menu from enum Menu::Menus void show(int menu, int signal=-1); } btop-1.2.3/src/btop_shared.hpp000066400000000000000000000166421420276253000162650ustar00rootroot00000000000000/* 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. indent = tab tab-size = 4 */ #pragma once #include #include #include #include #include #include #include #include #include #include using std::string, std::vector, std::deque, robin_hood::unordered_flat_map, std::atomic, std::array, std::tuple; void term_resize(bool force=false); void banner_gen(); extern void clean_quit(int sig); namespace Global { extern const vector> Banner_src; extern const string Version; extern atomic quitting; extern string exit_error_msg; extern atomic thread_exception; extern string banner; extern atomic resized; extern string overlay; extern string clock; extern uid_t real_uid, set_uid; } namespace Runner { extern atomic active; extern atomic reading; extern atomic stopping; extern atomic redraw; extern pthread_t runner_id; extern bool pause_output; extern string debug_bg; void run(const string& box="", const bool no_update=false, const bool force_redraw=false); void stop(); } namespace Tools { //* Platform specific function for system_uptime (seconds since last restart) double system_uptime(); } namespace Shared { //* Initialize platform specific needed variables and check for errors void init(); extern long coreCount, page_size, clk_tck; } namespace Cpu { extern string box; extern int x, y, width, height, min_width, min_height; extern bool shown, redraw, got_sensors, cpu_temp_only, has_battery; extern string cpuName, cpuHz; extern vector available_fields; extern vector available_sensors; extern tuple current_bat; struct cpu_info { unordered_flat_map> cpu_percent = { {"total", {}}, {"user", {}}, {"nice", {}}, {"system", {}}, {"idle", {}}, {"iowait", {}}, {"irq", {}}, {"softirq", {}}, {"steal", {}}, {"guest", {}}, {"guest_nice", {}} }; vector> core_percent; vector> temp; long long temp_max = 0; array load_avg; }; //* Collect cpu stats and temperatures auto collect(const bool no_update=false) -> cpu_info&; //* Draw contents of cpu box using as source string draw(const cpu_info& cpu, const bool force_redraw=false, const bool data_same=false); //* Parse /proc/cpu info for mapping of core ids auto get_core_mapping() -> unordered_flat_map; extern unordered_flat_map core_mapping; //* Get battery info from /sys auto get_battery() -> tuple; } namespace Mem { extern string box; extern int x, y, width, height, min_width, min_height; extern bool has_swap, shown, redraw; const array mem_names = {"used", "available", "cached", "free"}; const array swap_names = {"swap_used", "swap_free"}; extern int disk_ios; struct disk_info { std::filesystem::path dev; string name; std::filesystem::path stat = ""; int64_t total = 0, used = 0, free = 0; int used_percent = 0, free_percent = 0; array old_io = {0, 0, 0}; deque io_read = {}; deque io_write = {}; deque io_activity = {}; }; struct mem_info { unordered_flat_map stats = {{"used", 0}, {"available", 0}, {"cached", 0}, {"free", 0}, {"swap_total", 0}, {"swap_used", 0}, {"swap_free", 0}}; unordered_flat_map> percent = {{"used", {}}, {"available", {}}, {"cached", {}}, {"free", {}}, {"swap_total", {}}, {"swap_used", {}}, {"swap_free", {}}}; unordered_flat_map disks; vector disks_order; }; //?* Get total system memory uint64_t get_totalMem(); //* Collect mem & disks stats auto collect(const bool no_update=false) -> mem_info&; //* Draw contents of mem box using as source string draw(const mem_info& mem, const bool force_redraw=false, const bool data_same=false); } namespace Net { extern string box; extern int x, y, width, height, min_width, min_height; extern bool shown, redraw; extern string selected_iface; extern vector interfaces; extern bool rescale; extern unordered_flat_map graph_max; struct net_stat { uint64_t speed = 0, top = 0, total = 0, last = 0, offset = 0, rollover = 0; }; struct net_info { unordered_flat_map> bandwidth = { {"download", {}}, {"upload", {}} }; unordered_flat_map stat = { {"download", {}}, {"upload", {}} }; string ipv4 = "", ipv6 = ""; bool connected = false; }; extern unordered_flat_map current_net; //* Collect net upload/download stats auto collect(const bool no_update=false) -> net_info&; //* Draw contents of net box using as source string draw(const net_info& net, const bool force_redraw=false, const bool data_same=false); } namespace Proc { extern atomic numpids; extern string box; extern int x, y, width, height, min_width, min_height; extern bool shown, redraw; extern int select_max; extern atomic detailed_pid; extern int selected_pid, start, selected, collapse, expand; extern string selected_name; //? Contains the valid sorting options for processes const vector sort_vector = { "pid", "name", "command", "threads", "user", "memory", "cpu direct", "cpu lazy", }; //? Translation from process state char to explanative string const unordered_flat_map proc_states = { {'R', "Running"}, {'S', "Sleeping"}, {'D', "Waiting"}, {'Z', "Zombie"}, {'T', "Stopped"}, {'t', "Tracing"}, {'X', "Dead"}, {'x', "Dead"}, {'K', "Wakekill"}, {'W', "Unknown"}, {'P', "Parked"} }; //* Container for process information struct proc_info { size_t pid = 0; string name = "", cmd = ""; string short_cmd = ""; size_t threads = 0; int name_offset = 0; string user = ""; uint64_t mem = 0; double cpu_p = 0.0, cpu_c = 0.0; char state = '0'; uint64_t p_nice = 0, ppid = 0, cpu_s = 0, cpu_t = 0; string prefix = ""; size_t depth = 0, tree_index = 0; bool collapsed = false, filtered = false; }; //* Container for process info box struct detail_container { size_t last_pid = 0; bool skip_smaps = false; proc_info entry; string elapsed, parent, status, io_read, io_write, memory; long long first_mem = -1; deque cpu_percent; deque mem_bytes; }; //? Contains all info for proc detailed box extern detail_container detailed; //* Collect and sort process information from /proc auto collect(const bool no_update=false) -> vector&; //* Update current selection and view, returns -1 if no change otherwise the current selection int selection(const string& cmd_key); //* Draw contents of proc box using as data source string draw(const vector& plist, const bool force_redraw=false, const bool data_same=false); } btop-1.2.3/src/btop_theme.cpp000066400000000000000000000336041420276253000161110ustar00rootroot00000000000000/* 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. indent = tab tab-size = 4 */ #include #include #include #include #include #include #include #include #include using std::round, std::vector, std::stoi, std::views::iota, std::clamp, std::max, std::min, std::ceil, std::to_string; using namespace Tools; namespace rng = std::ranges; namespace fs = std::filesystem; string Term::fg, Term::bg; string Fx::reset = reset_base; namespace Theme { fs::path theme_dir; fs::path user_theme_dir; vector themes; unordered_flat_map colors; unordered_flat_map> rgbs; unordered_flat_map> gradients; const unordered_flat_map Default_theme = { { "main_bg", "#00" }, { "main_fg", "#cc" }, { "title", "#ee" }, { "hi_fg", "#b54040" }, { "selected_bg", "#6a2f2f" }, { "selected_fg", "#ee" }, { "inactive_fg", "#40" }, { "graph_text", "#60" }, { "meter_bg", "#40" }, { "proc_misc", "#0de756" }, { "cpu_box", "#556d59" }, { "mem_box", "#6c6c4b" }, { "net_box", "#5c588d" }, { "proc_box", "#805252" }, { "div_line", "#30" }, { "temp_start", "#4897d4" }, { "temp_mid", "#5474e8" }, { "temp_end", "#ff40b6" }, { "cpu_start", "#77ca9b" }, { "cpu_mid", "#cbc06c" }, { "cpu_end", "#dc4c4c" }, { "free_start", "#384f21" }, { "free_mid", "#b5e685" }, { "free_end", "#dcff85" }, { "cached_start", "#163350" }, { "cached_mid", "#74e6fc" }, { "cached_end", "#26c5ff" }, { "available_start", "#4e3f0e" }, { "available_mid", "#ffd77a" }, { "available_end", "#ffb814" }, { "used_start", "#592b26" }, { "used_mid", "#d9626d" }, { "used_end", "#ff4769" }, { "download_start", "#291f75" }, { "download_mid", "#4f43a3" }, { "download_end", "#b0a9de" }, { "upload_start", "#620665" }, { "upload_mid", "#7d4180" }, { "upload_end", "#dcafde" }, { "process_start", "#80d0a3" }, { "process_mid", "#dcd179" }, { "process_end", "#d45454" } }; const unordered_flat_map TTY_theme = { { "main_bg", "\x1b[0;40m" }, { "main_fg", "\x1b[37m" }, { "title", "\x1b[97m" }, { "hi_fg", "\x1b[91m" }, { "selected_bg", "\x1b[41m" }, { "selected_fg", "\x1b[97m" }, { "inactive_fg", "\x1b[90m" }, { "graph_text", "\x1b[90m" }, { "meter_bg", "\x1b[90m" }, { "proc_misc", "\x1b[92m" }, { "cpu_box", "\x1b[32m" }, { "mem_box", "\x1b[33m" }, { "net_box", "\x1b[35m" }, { "proc_box", "\x1b[31m" }, { "div_line", "\x1b[90m" }, { "temp_start", "\x1b[94m" }, { "temp_mid", "\x1b[96m" }, { "temp_end", "\x1b[95m" }, { "cpu_start", "\x1b[92m" }, { "cpu_mid", "\x1b[93m" }, { "cpu_end", "\x1b[91m" }, { "free_start", "\x1b[32m" }, { "free_mid", "" }, { "free_end", "\x1b[92m" }, { "cached_start", "\x1b[36m" }, { "cached_mid", "" }, { "cached_end", "\x1b[96m" }, { "available_start", "\x1b[33m" }, { "available_mid", "" }, { "available_end", "\x1b[93m" }, { "used_start", "\x1b[31m" }, { "used_mid", "" }, { "used_end", "\x1b[91m" }, { "download_start", "\x1b[34m" }, { "download_mid", "" }, { "download_end", "\x1b[94m" }, { "upload_start", "\x1b[35m" }, { "upload_mid", "" }, { "upload_end", "\x1b[95m" }, { "process_start", "\x1b[32m" }, { "process_mid", "\x1b[33m" }, { "process_end", "\x1b[31m" } }; namespace { //* Convert 24-bit colors to 256 colors int truecolor_to_256(const int& r, const int& g, const int& b) { //? Use upper 232-255 greyscale values if the downscaled red, green and blue are the same value if (const int red = round((double)r / 11); red == round((double)g / 11) and red == round((double)b / 11)) { return 232 + red; } //? Else use 6x6x6 color cube to calculate approximate colors else { return round((double)r / 51) * 36 + round((double)g / 51) * 6 + round((double)b / 51) + 16; } } } string hex_to_color(string hexa, const bool& t_to_256, const string& depth) { if (hexa.size() > 1) { hexa.erase(0, 1); for (auto& c : hexa) if (not isxdigit(c)) { Logger::error("Invalid hex value: " + hexa); return ""; } string pre = Fx::e + (depth == "fg" ? "38" : "48") + ";" + (t_to_256 ? "5;" : "2;"); if (hexa.size() == 2) { int h_int = stoi(hexa, nullptr, 16); if (t_to_256) { return pre + to_string(truecolor_to_256(h_int, h_int, h_int)) + "m"; } else { string h_str = to_string(h_int); return pre + h_str + ";" + h_str + ";" + h_str + "m"; } } else if (hexa.size() == 6) { if (t_to_256) { return pre + to_string(truecolor_to_256( stoi(hexa.substr(0, 2), nullptr, 16), stoi(hexa.substr(2, 2), nullptr, 16), stoi(hexa.substr(4, 2), nullptr, 16))) + "m"; } else { return pre + to_string(stoi(hexa.substr(0, 2), nullptr, 16)) + ";" + to_string(stoi(hexa.substr(2, 2), nullptr, 16)) + ";" + to_string(stoi(hexa.substr(4, 2), nullptr, 16)) + "m"; } } else Logger::error("Invalid size of hex value: " + hexa); } else Logger::error("Hex value missing: " + hexa); return ""; } string dec_to_color(int r, int g, int b, const bool& t_to_256, const string& depth) { string pre = Fx::e + (depth == "fg" ? "38" : "48") + ";" + (t_to_256 ? "5;" : "2;"); r = std::clamp(r, 0, 255); g = std::clamp(g, 0, 255); b = std::clamp(b, 0, 255); if (t_to_256) return pre + to_string(truecolor_to_256(r, g, b)) + "m"; else return pre + to_string(r) + ";" + to_string(g) + ";" + to_string(b) + "m"; } namespace { //* Convert hex color to a array of decimals array hex_to_dec(string hexa) { if (hexa.size() > 1) { hexa.erase(0, 1); for (auto& c : hexa) if (not isxdigit(c)) return array{-1, -1, -1}; if (hexa.size() == 2) { int h_int = stoi(hexa, nullptr, 16); return array{h_int, h_int, h_int}; } else if (hexa.size() == 6) { return array{ stoi(hexa.substr(0, 2), nullptr, 16), stoi(hexa.substr(2, 2), nullptr, 16), stoi(hexa.substr(4, 2), nullptr, 16) }; } } return {-1 ,-1 ,-1}; } //* Generate colors and rgb decimal vectors for the theme void generateColors(const unordered_flat_map& source) { vector t_rgb; string depth; const bool& t_to_256 = Config::getB("lowcolor"); colors.clear(); rgbs.clear(); for (const auto& [name, color] : Default_theme) { if (name == "main_bg" and not Config::getB("theme_background")) { colors[name] = "\x1b[49m"; rgbs[name] = {-1, -1, -1}; continue; } depth = (name.ends_with("bg") and name != "meter_bg") ? "bg" : "fg"; if (source.contains(name)) { if (name == "main_bg" and source.at(name).empty()) { colors[name] = "\x1b[49m"; rgbs[name] = {-1, -1, -1}; continue; } else if (source.at(name).empty() and (name.ends_with("_mid") or name.ends_with("_end"))) { colors[name] = ""; rgbs[name] = {-1, -1, -1}; continue; } else if (source.at(name).starts_with('#')) { colors[name] = hex_to_color(source.at(name), t_to_256, depth); rgbs[name] = hex_to_dec(source.at(name)); } else if (not source.at(name).empty()) { t_rgb = ssplit(source.at(name)); if (t_rgb.size() != 3) Logger::error("Invalid RGB decimal value: \"" + source.at(name) + "\""); else { colors[name] = dec_to_color(stoi(t_rgb[0]), stoi(t_rgb[1]), stoi(t_rgb[2]), t_to_256, depth); rgbs[name] = array{stoi(t_rgb[0]), stoi(t_rgb[1]), stoi(t_rgb[2])}; } } } if (not colors.contains(name) and not is_in(name, "meter_bg", "process_start", "process_mid", "process_end", "graph_text")) { Logger::debug("Missing color value for \"" + name + "\". Using value from default."); colors[name] = hex_to_color(color, t_to_256, depth); rgbs[name] = hex_to_dec(color); } } //? Set fallback values for optional colors not defined in theme file if (not colors.contains("meter_bg")) { colors["meter_bg"] = colors.at("inactive_fg"); rgbs["meter_bg"] = rgbs.at("inactive_fg"); } if (not colors.contains("process_start")) { colors["process_start"] = colors.at("cpu_start"); colors["process_mid"] = colors.at("cpu_mid"); colors["process_end"] = colors.at("cpu_end"); rgbs["process_start"] = rgbs.at("cpu_start"); rgbs["process_mid"] = rgbs.at("cpu_mid"); rgbs["process_end"] = rgbs.at("cpu_end"); } if (not colors.contains("graph_text")) { colors["graph_text"] = colors.at("inactive_fg"); rgbs["graph_text"] = rgbs.at("inactive_fg"); } } //* Generate color gradients from two or three colors, 101 values indexed 0-100 void generateGradients() { gradients.clear(); const bool& t_to_256 = Config::getB("lowcolor"); //? Insert values for processes greyscale gradient and processes color gradient rgbs.insert({ { "proc_start", rgbs["main_fg"] }, { "proc_mid", {-1, -1, -1} }, { "proc_end", rgbs["inactive_fg"] }, { "proc_color_start", rgbs["inactive_fg"] }, { "proc_color_mid", {-1, -1, -1} }, { "proc_color_end", rgbs["process_start"] }, }); for (const auto& [name, source_arr] : rgbs) { if (not name.ends_with("_start")) continue; const string color_name = rtrim(name, "_start"); //? input_colors[start,mid,end][red,green,blue] const array, 3> input_colors = { source_arr, rgbs[color_name + "_mid"], rgbs[color_name + "_end"] }; //? output_colors[red,green,blue][0-100] array, 101> output_colors; output_colors[0][0] = -1; //? Only start iteration if gradient has an end color defined if (input_colors[2][0] >= 0) { //? Split iteration in two passes of 50 + 51 instead of one pass of 101 if gradient has start, mid and end values defined int current_range = (input_colors[1][0] >= 0) ? 50 : 100; for (const int& rgb : iota(0, 3)) { int start = 0, offset = 0; int end = (current_range == 50) ? 1 : 2; for (const int& i : iota(0, 101)) { output_colors[i][rgb] = input_colors[start][rgb] + (i - offset) * (input_colors[end][rgb] - input_colors[start][rgb]) / current_range; //? Switch source arrays from start->mid to mid->end at 50 passes if mid is defined if (i == current_range) { ++start; ++end; offset = 50; } } } } //? Generate color escape codes for the generated rgb decimals array color_gradient; if (output_colors[0][0] != -1) { for (int y = 0; const auto& [red, green, blue] : output_colors) color_gradient[y++] = dec_to_color(red, green, blue, t_to_256); } else { //? If only start was defined fill array with start color color_gradient.fill(colors[name]); } gradients[color_name] = std::move(color_gradient); } } //* Set colors and generate gradients for the TTY theme void generateTTYColors() { rgbs.clear(); gradients.clear(); colors = TTY_theme; if (not Config::getB("theme_background")) colors["main_bg"] = "\x1b[49m"; for (const auto& c : colors) { if (not c.first.ends_with("_start")) continue; const string base_name = rtrim(c.first, "_start"); string section = "_start"; int split = colors.at(base_name + "_mid").empty() ? 50 : 33; for (const int& i : iota(0, 101)) { gradients[base_name][i] = colors.at(base_name + section); if (i == split) { section = (split == 33) ? "_mid" : "_end"; split *= 2; } } } } //* Load a .theme file from disk auto loadFile(const string& filename) { unordered_flat_map theme_out; const fs::path filepath = filename; if (not fs::exists(filepath)) return Default_theme; std::ifstream themefile(filepath); if (themefile.good()) { Logger::debug("Loading theme file: " + filename); while (not themefile.bad()) { themefile.ignore(SSmax, '['); if (themefile.eof()) break; string name, value; getline(themefile, name, ']'); if (not Default_theme.contains(name)) { continue; } themefile.ignore(SSmax, '='); themefile >> std::ws; if (themefile.eof()) break; if (themefile.peek() == '"') { themefile.ignore(1); getline(themefile, value, '"'); } else getline(themefile, value, '\n'); theme_out[name] = value; } return theme_out; } return Default_theme; } } void updateThemes() { themes.clear(); themes.push_back("Default"); themes.push_back("TTY"); for (const auto& path : { user_theme_dir, theme_dir } ) { if (path.empty()) continue; for (auto& file : fs::directory_iterator(path)) { if (file.path().extension() == ".theme" and access(file.path().c_str(), R_OK) != -1 and not v_contains(themes, file.path().c_str())) { themes.push_back(file.path().c_str()); } } } } void setTheme() { const auto& theme = Config::getS("color_theme"); fs::path theme_path; for (const fs::path p : themes) { if (p == theme or p.stem() == theme or p.filename() == theme) { theme_path = p; break; } } if (theme == "TTY" or Config::getB("tty_mode")) generateTTYColors(); else { generateColors((theme == "Default" or theme_path.empty() ? Default_theme : loadFile(theme_path))); generateGradients(); } Term::fg = colors.at("main_fg"); Term::bg = colors.at("main_bg"); Fx::reset = Fx::reset_base + Term::fg + Term::bg; } } btop-1.2.3/src/btop_theme.hpp000066400000000000000000000047211420276253000161140ustar00rootroot00000000000000/* 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. indent = tab tab-size = 4 */ #pragma once #include #include #include #include using std::string, robin_hood::unordered_flat_map, std::array; namespace Theme { extern std::filesystem::path theme_dir; extern std::filesystem::path user_theme_dir; //* Contains "Default" and "TTY" at indeces 0 and 1, otherwise full paths to theme files extern vector themes; //* Generate escape sequence for 24-bit or 256 color and return as a string //* Args hexa: ["#000000"-"#ffffff"] for color, ["#00"-"#ff"] for greyscale //* t_to_256: [true|false] convert 24bit value to 256 color value //* depth: ["fg"|"bg"] for either a foreground color or a background color string hex_to_color(string hexa, const bool& t_to_256=false, const string& depth="fg"); //* Generate escape sequence for 24-bit or 256 color and return as a string //* Args r: [0-255], g: [0-255], b: [0-255] //* t_to_256: [true|false] convert 24bit value to 256 color value //* depth: ["fg"|"bg"] for either a foreground color or a background color string dec_to_color(int r, int g, int b, const bool& t_to_256=false, const string& depth="fg"); //* Update list of paths for available themes void updateThemes(); //* Set current theme from current "color_theme" value in config void setTheme(); extern unordered_flat_map colors; extern unordered_flat_map> rgbs; extern unordered_flat_map> gradients; //* Return escape code for color inline const string& c(const string& name) { return colors.at(name); } //* Return array of escape codes for color gradient inline const array& g(string name) { return gradients.at(name); } //* Return array of red, green and blue in decimal for color inline const std::array& dec(string name) { return rgbs.at(name); } }btop-1.2.3/src/btop_tools.cpp000066400000000000000000000357171420276253000161560ustar00rootroot00000000000000/* 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. indent = tab tab-size = 4 */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using std::string_view, std::max, std::floor, std::to_string, std::cin, std::cout, std::flush, robin_hood::unordered_flat_map; namespace fs = std::filesystem; namespace rng = std::ranges; //? ------------------------------------------------- NAMESPACES ------------------------------------------------------ //* Collection of escape codes and functions for terminal manipulation namespace Term { atomic initialized = false; atomic width = 0; atomic height = 0; string current_tty; namespace { struct termios initial_settings; //* Toggle terminal input echo bool echo(bool on=true) { struct termios settings; if (tcgetattr(STDIN_FILENO, &settings)) return false; if (on) settings.c_lflag |= ECHO; else settings.c_lflag &= ~(ECHO); return 0 == tcsetattr(STDIN_FILENO, TCSANOW, &settings); } //* Toggle need for return key when reading input bool linebuffered(bool on=true) { struct termios settings; if (tcgetattr(STDIN_FILENO, &settings)) return false; if (on) settings.c_lflag |= ICANON; else settings.c_lflag &= ~(ICANON); if (tcsetattr(STDIN_FILENO, TCSANOW, &settings)) return false; if (on) setlinebuf(stdin); else setbuf(stdin, NULL); return true; } } bool refresh(bool only_check) { struct winsize w; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) < 0) return false; if (width != w.ws_col or height != w.ws_row) { if (not only_check) { width = w.ws_col; height = w.ws_row; } return true; } return false; } auto get_min_size(const string& boxes) -> array { const bool cpu = boxes.find("cpu") != string::npos; const bool mem = boxes.find("mem") != string::npos; const bool net = boxes.find("net") != string::npos; const bool proc = boxes.find("proc") != string::npos; int width = 0; if (mem) width = Mem::min_width; else if (net) width = Mem::min_width; width += (proc ? Proc::min_width : 0); if (cpu and width < Cpu::min_width) width = Cpu::min_width; int height = (cpu ? Cpu::min_height : 0); if (proc) height += Proc::min_height; else height += (mem ? Mem::min_height : 0) + (net ? Net::min_height : 0); return { width, height }; } bool init() { if (not initialized) { initialized = (bool)isatty(STDIN_FILENO); if (initialized) { tcgetattr(STDIN_FILENO, &initial_settings); current_tty = (ttyname(STDIN_FILENO) != NULL ? (string)ttyname(STDIN_FILENO) : "unknown"); //? Disable stream sync cin.sync_with_stdio(false); cout.sync_with_stdio(false); //? Disable stream ties cin.tie(NULL); cout.tie(NULL); echo(false); linebuffered(false); refresh(); cout << alt_screen << hide_cursor << mouse_on << flush; Global::resized = false; } } return initialized; } void restore() { if (initialized) { tcsetattr(STDIN_FILENO, TCSANOW, &initial_settings); cout << mouse_off << clear << Fx::reset << normal_screen << show_cursor << flush; initialized = false; } } } //? --------------------------------------------------- FUNCTIONS ----------------------------------------------------- // ! Dsiabled due to issue when compiling with musl, reverted back to using regex // namespace Fx { // string uncolor(const string& s) { // string out = s; // for (size_t offset = 0, start_pos = 0, end_pos = 0;;) { // start_pos = (offset == 0) ? out.find('\x1b') : offset; // if (start_pos == string::npos) // break; // offset = start_pos + 1; // end_pos = out.find('m', offset); // if (end_pos == string::npos) // break; // else if (auto next_pos = out.find('\x1b', offset); not isdigit(out[end_pos - 1]) or end_pos > next_pos) { // offset = next_pos; // continue; // } // out.erase(start_pos, (end_pos - start_pos)+1); // offset = 0; // } // out.shrink_to_fit(); // return out; // } // } namespace Tools { size_t wide_ulen(const string& str) { unsigned int chars = 0; std::wstring_convert> conv; auto w_str = conv.from_bytes((str.size() > 10000 ? str.substr(0, 10000).c_str() : str.c_str())); for (auto c : w_str) { chars += utf8::wcwidth(c); } return chars; } size_t wide_ulen(const std::wstring& w_str) { unsigned int chars = 0; for (auto c : w_str) { chars += utf8::wcwidth(c); } return chars; } string uresize(string str, const size_t len, const bool wide) { if (len < 1 or str.empty()) return ""; if (wide) { std::wstring_convert> conv; auto w_str = conv.from_bytes((str.size() > 10000 ? str.substr(0, 10000).c_str() : str.c_str())); while (wide_ulen(w_str) > len) w_str.pop_back(); str = conv.to_bytes(w_str); } else { for (size_t x = 0, i = 0; i < str.size(); i++) { if ((static_cast(str.at(i)) & 0xC0) != 0x80) x++; if (x >= len + 1) { str.resize(i); break; } } } str.shrink_to_fit(); return str; } string luresize(string str, const size_t len, const bool wide) { if (len < 1 or str.empty()) return ""; for (size_t x = 0, last_pos = 0, i = str.size() - 1; i > 0 ; i--) { if (wide and static_cast(str.at(i)) > 0xef) { x += 2; last_pos = max((size_t)0, i - 1); } else if ((static_cast(str.at(i)) & 0xC0) != 0x80) { x++; last_pos = i; } if (x >= len) { str = str.substr(last_pos); str.shrink_to_fit(); break; } } return str; } string s_replace(const string& str, const string& from, const string& to) { string out = str; for (size_t start_pos = out.find(from); start_pos != std::string::npos; start_pos = out.find(from)) { out.replace(start_pos, from.length(), to); } return out; } string ltrim(const string& str, const string& t_str) { string_view str_v = str; while (str_v.starts_with(t_str)) str_v.remove_prefix(t_str.size()); return (string)str_v; } string rtrim(const string& str, const string& t_str) { string_view str_v = str; while (str_v.ends_with(t_str)) str_v.remove_suffix(t_str.size()); return (string)str_v; } auto ssplit(const string& str, const char& delim) -> vector { vector out; for (const auto& s : str | rng::views::split(delim) | rng::views::transform([](auto &&rng) { return string_view(&*rng.begin(), rng::distance(rng)); })) { if (not s.empty()) out.emplace_back(s); } return out; } string ljust(string str, const size_t x, const bool utf, const bool wide, const bool limit) { if (utf) { if (limit and ulen(str, wide) > x) return uresize(str, x, wide); return str + string(max((int)(x - ulen(str)), 0), ' '); } else { if (limit and str.size() > x) { str.resize(x); return str; } return str + string(max((int)(x - str.size()), 0), ' '); } } string rjust(string str, const size_t x, const bool utf, const bool wide, const bool limit) { if (utf) { if (limit and ulen(str, wide) > x) return uresize(str, x, wide); return string(max((int)(x - ulen(str)), 0), ' ') + str; } else { if (limit and str.size() > x) { str.resize(x); return str; }; return string(max((int)(x - str.size()), 0), ' ') + str; } } string cjust(string str, const size_t x, const bool utf, const bool wide, const bool limit) { if (utf) { if (limit and ulen(str, wide) > x) return uresize(str, x, wide); return string(max((int)ceil((double)(x - ulen(str)) / 2), 0), ' ') + str + string(max((int)floor((double)(x - ulen(str)) / 2), 0), ' '); } else { if (limit and str.size() > x) { str.resize(x); return str; } return string(max((int)ceil((double)(x - str.size()) / 2), 0), ' ') + str + string(max((int)floor((double)(x - str.size()) / 2), 0), ' '); } } string trans(const string& str) { string_view oldstr = str; string newstr; newstr.reserve(str.size()); for (size_t pos; (pos = oldstr.find(' ')) != string::npos;) { newstr.append(oldstr.substr(0, pos)); size_t x = 0; while (pos + x < oldstr.size() and oldstr.at(pos + x) == ' ') x++; newstr.append(Mv::r(x)); oldstr.remove_prefix(pos + x); } return (newstr.empty()) ? str : newstr + (string)oldstr; } string sec_to_dhms(size_t seconds, bool no_days, bool no_seconds) { size_t days = seconds / 86400; seconds %= 86400; size_t hours = seconds / 3600; seconds %= 3600; size_t minutes = seconds / 60; seconds %= 60; string out = (not no_days and days > 0 ? to_string(days) + "d " : "") + (hours < 10 ? "0" : "") + to_string(hours) + ':' + (minutes < 10 ? "0" : "") + to_string(minutes) + (not no_seconds ? ":" + string(std::cmp_less(seconds, 10) ? "0" : "") + to_string(seconds) : ""); return out; } string floating_humanizer(uint64_t value, const bool shorten, size_t start, const bool bit, const bool per_second) { string out; const size_t mult = (bit) ? 8 : 1; const bool mega = Config::getB("base_10_sizes"); static const array mebiUnits_bit = {"bit", "Kib", "Mib", "Gib", "Tib", "Pib", "Eib", "Zib", "Yib", "Bib", "GEb"}; static const array mebiUnits_byte = {"Byte", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "BiB", "GEB"}; static const array megaUnits_bit = {"bit", "Kb", "Mb", "Gb", "Tb", "Pb", "Eb", "Zb", "Yb", "Bb", "Gb"}; static const array megaUnits_byte = {"Byte", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "BB", "GB"}; const auto& units = (bit) ? ( mega ? megaUnits_bit : mebiUnits_bit) : ( mega ? megaUnits_byte : mebiUnits_byte); value *= 100 * mult; if (mega) { while (value >= 100000) { value /= 1000; if (value < 100) { out = to_string(value); break; } start++; } } else { while (value >= 102400) { value >>= 10; if (value < 100) { out = to_string(value); break; } start++; } } if (out.empty()) { out = to_string(value); if (not mega and out.size() == 4 and start > 0) { out.pop_back(); out.insert(2, ".");} else if (out.size() == 3 and start > 0) out.insert(1, "."); else if (out.size() >= 2) out.resize(out.size() - 2); } if (shorten) { auto f_pos = out.find('.'); if (f_pos == 1 and out.size() > 3) out = out.substr(0,2) + to_string((int)round(stof(out.substr(2)) / 10)); else if (f_pos != string::npos) out = to_string((int)round(stof(out))); if (out.size() > 3) { out = to_string((int)(out[0] - '0') + 1); start++;} out.push_back(units[start][0]); } else out += " " + units[start]; if (per_second) out += (bit) ? "ps" : "/s"; return out; } std::string operator*(const string& str, int64_t n) { if (n < 1 or str.empty()) return ""; else if(n == 1) return str; string new_str; new_str.reserve(str.size() * n); for (; n > 0; n--) new_str.append(str); return new_str; } string strf_time(const string& strf) { auto in_time_t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); std::tm bt {}; std::stringstream ss; ss << std::put_time(localtime_r(&in_time_t, &bt), strf.c_str()); return ss.str(); } void atomic_wait(const atomic& atom, const bool old) noexcept { while (atom.load(std::memory_order_relaxed) == old ) busy_wait(); } void atomic_wait_for(const atomic& atom, const bool old, const uint64_t wait_ms) noexcept { const uint64_t start_time = time_ms(); while (atom.load(std::memory_order_relaxed) == old and (time_ms() - start_time < wait_ms)) sleep_ms(1); } atomic_lock::atomic_lock(atomic& atom, bool wait) : atom(atom) { if (wait) while (not this->atom.compare_exchange_strong(this->not_true, true)); else this->atom.store(true); } atomic_lock::~atomic_lock() { this->atom.store(false); } string readfile(const std::filesystem::path& path, const string& fallback) { if (not fs::exists(path)) return fallback; string out; try { std::ifstream file(path); for (string readstr; getline(file, readstr); out += readstr); } catch (const std::exception& e) { Logger::error("readfile() : Exception when reading " + (string)path + " : " + e.what()); return fallback; } return (out.empty() ? fallback : out); } auto celsius_to(const long long& celsius, const string& scale) -> tuple { if (scale == "celsius") return {celsius, "°C"}; else if (scale == "fahrenheit") return {(long long)round((double)celsius * 1.8 + 32), "°F"}; else if (scale == "kelvin") return {(long long)round((double)celsius + 273.15), "K "}; else if (scale == "rankine") return {(long long)round((double)celsius * 1.8 + 491.67), "°R"}; return {0, ""}; } string hostname() { char host[HOST_NAME_MAX]; gethostname(host, HOST_NAME_MAX); return (string)host; } string username() { auto user = getenv("LOGNAME"); if (user == NULL or strlen(user) == 0) user = getenv("USER"); return (user != NULL ? user : ""); } } namespace Logger { using namespace Tools; std::atomic busy (false); bool first = true; const string tdf = "%Y/%m/%d (%T) | "; size_t loglevel; fs::path logfile; //* Wrapper for lowering priviliges if using SUID bit and currently isn't using real userid class lose_priv { int status = -1; public: lose_priv() { if (geteuid() != Global::real_uid) this->status = seteuid(Global::real_uid); } ~lose_priv() { if (status == 0) status = seteuid(Global::set_uid); } }; void set(const string& level) { loglevel = v_index(log_levels, level); } void log_write(const size_t level, const string& msg) { if (loglevel < level or logfile.empty()) return; atomic_lock lck(busy, true); lose_priv neutered{}; std::error_code ec; try { if (fs::exists(logfile) and fs::file_size(logfile, ec) > 1024 << 10 and not ec) { auto old_log = logfile; old_log += ".1"; if (fs::exists(old_log)) fs::remove(old_log, ec); if (not ec) fs::rename(logfile, old_log, ec); } if (not ec) { std::ofstream lwrite(logfile, std::ios::app); if (first) { first = false; lwrite << "\n" << strf_time(tdf) << "===> btop++ v." << Global::Version << "\n";} lwrite << strf_time(tdf) << log_levels.at(level) << ": " << msg << "\n"; } else logfile.clear(); } catch (const std::exception& e) { logfile.clear(); throw std::runtime_error("Exception in Logger::log_write() : " + (string)e.what()); } } } btop-1.2.3/src/btop_tools.hpp000066400000000000000000000303431420276253000161510ustar00rootroot00000000000000/* 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. indent = tab tab-size = 4 */ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #ifndef HOST_NAME_MAX #ifdef __APPLE__ #define HOST_NAME_MAX 255 #else #define HOST_NAME_MAX 64 #endif #endif using std::string, std::vector, std::atomic, std::to_string, std::tuple, std::array; //? ------------------------------------------------- NAMESPACES ------------------------------------------------------ //* Collection of escape codes for text style and formatting namespace Fx { const string e = "\x1b["; //* Escape sequence start const string b = e + "1m"; //* Bold on/off const string ub = e + "22m"; //* Bold off const string d = e + "2m"; //* Dark on const string ud = e + "22m"; //* Dark off const string i = e + "3m"; //* Italic on const string ui = e + "23m"; //* Italic off const string ul = e + "4m"; //* Underline on const string uul = e + "24m"; //* Underline off const string bl = e + "5m"; //* Blink on const string ubl = e + "25m"; //* Blink off const string s = e + "9m"; //* Strike/crossed-out on const string us = e + "29m"; //* Strike/crossed-out on/off //* Reset foreground/background color and text effects const string reset_base = e + "0m"; //* Reset text effects and restore theme foregrund and background color extern string reset; //* Regex for matching color, style and cursor move escape sequences const std::regex escape_regex("\033\\[\\d+;?\\d?;?\\d*;?\\d*;?\\d*(m|f|s|u|C|D|A|B){1}"); //* Regex for matching only color and style escape sequences const std::regex color_regex("\033\\[\\d+;?\\d?;?\\d*;?\\d*;?\\d*(m){1}"); //* Return a string with all colors and text styling removed inline string uncolor(const string& s) { return std::regex_replace(s, color_regex, ""); } // string uncolor(const string& s); } //* Collection of escape codes and functions for cursor manipulation namespace Mv { //* Move cursor to , inline string to(const int& line, const int& col) { return Fx::e + to_string(line) + ';' + to_string(col) + 'f'; } //* Move cursor right columns inline string r(const int& x) { return Fx::e + to_string(x) + 'C'; } //* Move cursor left columns inline string l(const int& x) { return Fx::e + to_string(x) + 'D'; } //* Move cursor up x lines inline string u(const int& x) { return Fx::e + to_string(x) + 'A'; } //* Move cursor down x lines inline string d(const int& x) { return Fx::e + to_string(x) + 'B'; } //* Save cursor position const string save = Fx::e + "s"; //* Restore saved cursor postion const string restore = Fx::e + "u"; } //* Collection of escape codes and functions for terminal manipulation namespace Term { extern atomic initialized; extern atomic width; extern atomic height; extern string fg, bg, current_tty; const string hide_cursor = Fx::e + "?25l"; const string show_cursor = Fx::e + "?25h"; const string alt_screen = Fx::e + "?1049h"; const string normal_screen = Fx::e + "?1049l"; const string clear = Fx::e + "2J" + Fx::e + "0;0f"; const string clear_end = Fx::e + "0J"; const string clear_begin = Fx::e + "1J"; const string mouse_on = Fx::e + "?1002h" + Fx::e + "?1015h" + Fx::e + "?1006h"; //? Enable reporting of mouse position on click and release const string mouse_off = Fx::e + "?1002l" + Fx::e + "?1015l" + Fx::e + "?1006l"; const string mouse_direct_on = Fx::e + "?1003h"; //? Enable reporting of mouse position at any movement const string mouse_direct_off = Fx::e + "?1003l"; const string sync_start = Fx::e + "?2026h"; //? Start of terminal synchronized output const string sync_end = Fx::e + "?2026l"; //? End of terminal synchronized output //* Returns true if terminal has been resized and updates width and height bool refresh(bool only_check=false); //* Returns an array with the lowest possible width, height with current box config auto get_min_size(const string& boxes) -> array; //* Check for a valid tty, save terminal options and set new options bool init(); //* Restore terminal options void restore(); } //? --------------------------------------------------- FUNCTIONS ----------------------------------------------------- namespace Tools { constexpr auto SSmax = std::numeric_limits::max(); size_t wide_ulen(const string& str); size_t wide_ulen(const std::wstring& w_str); //* Return number of UTF8 characters in a string (wide=true for column size needed on terminal) inline size_t ulen(const string& str, const bool wide=false) { return (wide ? wide_ulen(str) : std::ranges::count_if(str, [](char c) { return (static_cast(c) & 0xC0) != 0x80; })); } //* Resize a string consisting of UTF8 characters (only reduces size) string uresize(const string str, const size_t len, const bool wide=false); //* Resize a string consisting of UTF8 characters from left (only reduces size) string luresize(const string str, const size_t len, const bool wide=false); //* Replace in with and return new string string s_replace(const string& str, const string& from, const string& to); //* Capatilize inline string capitalize(string str) { str.at(0) = toupper(str.at(0)); return str; } //* Return with only uppercase characters inline string str_to_upper(string str) { std::ranges::for_each(str, [](auto& c) { c = ::toupper(c); } ); return str; } //* Return with only lowercase characters inline string str_to_lower(string str) { std::ranges::for_each(str, [](char& c) { c = ::tolower(c); } ); return str; } //* Check if vector contains value template inline bool v_contains(const vector& vec, const T2& find_val) { return std::ranges::find(vec, find_val) != vec.end(); } //* Check if string contains value template inline bool s_contains(const string& str, const T& find_val) { return str.find(find_val) != string::npos; } //* Return index of from vector , returns size of if is not present template inline size_t v_index(const vector& vec, const T& find_val) { return std::ranges::distance(vec.begin(), std::ranges::find(vec, find_val)); } //* Compare with all following values template inline bool is_in(const First& first, const T& ... t) { return ((first == t) or ...); } //* Return current time since epoch in seconds inline uint64_t time_s() { return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); } //* Return current time since epoch in milliseconds inline uint64_t time_ms() { return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); } //* Return current time since epoch in microseconds inline uint64_t time_micros() { return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); } //* Check if a string is a valid bool value inline bool isbool(const string& str) { return is_in(str, "true", "false", "True", "False"); } //* Convert string to bool, returning any value not equal to "true" or "True" as false inline bool stobool(const string& str) { return is_in(str, "true", "True"); } //* Check if a string is a valid integer value (only postive) inline bool isint(const string& str) { return all_of(str.begin(), str.end(), ::isdigit); } //* Left-trim from and return new string string ltrim(const string& str, const string& t_str = " "); //* Right-trim from and return new string string rtrim(const string& str, const string& t_str = " "); //* Left/right-trim from and return new string inline string trim(const string& str, const string& t_str = " ") { return ltrim(rtrim(str, t_str), t_str); } //* Split at all occurrences of and return as vector of strings auto ssplit(const string& str, const char& delim = ' ') -> vector; //* Put current thread to sleep for milliseconds inline void sleep_ms(const size_t& ms) { std::this_thread::sleep_for(std::chrono::milliseconds(ms)); } //* Put current thread to sleep for microseconds inline void sleep_micros(const size_t& micros) { std::this_thread::sleep_for(std::chrono::microseconds(micros)); } //* Left justify string if is greater than length, limit return size to by default string ljust(string str, const size_t x, const bool utf=false, const bool wide=false, const bool limit=true); //* Right justify string if is greater than length, limit return size to by default string rjust(string str, const size_t x, const bool utf=false, const bool wide=false, const bool limit=true); //* Center justify string if is greater than length, limit return size to by default string cjust(string str, const size_t x, const bool utf=false, const bool wide=false, const bool limit=true); //* Replace whitespaces " " with escape code for move right string trans(const string& str); //* Convert seconds to format "d ::" and return string string sec_to_dhms(size_t seconds, bool no_days=false, bool no_seconds=false); //* Scales up in steps of 1024 to highest positive value 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 string floating_humanizer(uint64_t value, const bool shorten=false, size_t start=0, const bool bit=false, const bool per_second=false); //* Add std::string operator * : Repeat string number of times std::string operator*(const string& str, int64_t n); //* Return current time in format string strf_time(const string& strf); string hostname(); string username(); static inline void busy_wait (void) { #if defined __i386__ || defined __x86_64__ __builtin_ia32_pause(); #elif defined __ia64__ __asm volatile("hint @pause" : : : "memory"); #elif defined __sparc__ && (defined __arch64__ || defined __sparc_v9__) __asm volatile("membar #LoadLoad" : : : "memory"); #else __asm volatile("" : : : "memory"); #endif } void atomic_wait(const atomic& atom, const bool old=true) noexcept; void atomic_wait_for(const atomic& atom, const bool old=true, const uint64_t wait_ms=0) noexcept; //* Sets atomic to true on construct, sets to false on destruct class atomic_lock { atomic& atom; bool not_true = false; public: atomic_lock(atomic& atom, bool wait=false); ~atomic_lock(); }; //* Read a complete file and return as a string string readfile(const std::filesystem::path& path, const string& fallback=""); //* Convert a celsius value to celsius, fahrenheit, kelvin or rankin and return tuple with new value and unit. auto celsius_to(const long long& celsius, const string& scale) -> tuple; } //* Simple logging implementation namespace Logger { const vector log_levels = { "DISABLED", "ERROR", "WARNING", "INFO", "DEBUG", }; extern std::filesystem::path logfile; //* Set log level, valid arguments: "DISABLED", "ERROR", "WARNING", "INFO" and "DEBUG" void set(const string& level); void log_write(const size_t level, const string& msg); inline void error(const string msg) { log_write(1, msg); } inline void warning(const string msg) { log_write(2, msg); } inline void info(const string msg) { log_write(3, msg); } inline void debug(const string msg) { log_write(4, msg); } } btop-1.2.3/src/freebsd/000077500000000000000000000000001420276253000146635ustar00rootroot00000000000000btop-1.2.3/src/freebsd/btop_collect.cpp000066400000000000000000001353661420276253000200560ustar00rootroot00000000000000/* 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. indent = tab tab-size = 4 */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using std::clamp, std::string_literals::operator""s, std::cmp_equal, std::cmp_less, std::cmp_greater; using std::ifstream, std::numeric_limits, std::streamsize, std::round, std::max, std::min; namespace fs = std::filesystem; namespace rng = std::ranges; using namespace Tools; //? --------------------------------------------------- FUNCTIONS ----------------------------------------------------- namespace Cpu { vector core_old_totals; vector core_old_idles; vector available_fields = {"total"}; vector available_sensors = {"Auto"}; cpu_info current_cpu; bool got_sensors = false, cpu_temp_only = false; //* Populate found_sensors map bool get_sensors(); //* Get current cpu clock speed string get_cpuHz(); //* Search /proc/cpuinfo for a cpu name string get_cpuName(); struct Sensor { fs::path path; string label; int64_t temp = 0; int64_t high = 0; int64_t crit = 0; }; string cpu_sensor; vector core_sensors; unordered_flat_map core_mapping; } // namespace Cpu namespace Mem { double old_uptime; std::vector zpools; void get_zpools(); } namespace Shared { fs::path passwd_path; uint64_t totalMem; long pageSize, clkTck, coreCount, physicalCoreCount, arg_max; int totalMem_len, kfscale; long bootTime; void init() { //? Shared global variables init int mib[2]; mib[0] = CTL_HW; mib[1] = HW_NCPU; int ncpu; size_t len = sizeof(ncpu); if (sysctl(mib, 2, &ncpu, &len, NULL, 0) == -1) { Logger::warning("Could not determine number of cores, defaulting to 1."); } else { coreCount = ncpu; } pageSize = sysconf(_SC_PAGE_SIZE); if (pageSize <= 0) { pageSize = 4096; Logger::warning("Could not get system page size. Defaulting to 4096, processes memory usage might be incorrect."); } clkTck = sysconf(_SC_CLK_TCK); if (clkTck <= 0) { clkTck = 100; Logger::warning("Could not get system clock ticks per second. Defaulting to 100, processes cpu usage might be incorrect."); } int64_t memsize = 0; size_t size = sizeof(memsize); if (sysctlbyname("hw.physmem", &memsize, &size, NULL, 0) < 0) { Logger::warning("Could not get memory size"); } totalMem = memsize; struct timeval result; size = sizeof(result); if (sysctlbyname("kern.boottime", &result, &size, NULL, 0) < 0) { Logger::warning("Could not get boot time"); } else { bootTime = result.tv_sec; } size = sizeof(kfscale); if (sysctlbyname("kern.fscale", &kfscale, &size, NULL, 0) == -1) { kfscale = 2048; } //* Get maximum length of process arguments arg_max = sysconf(_SC_ARG_MAX); //? Init for namespace Cpu Cpu::current_cpu.core_percent.insert(Cpu::current_cpu.core_percent.begin(), Shared::coreCount, {}); Cpu::current_cpu.temp.insert(Cpu::current_cpu.temp.begin(), Shared::coreCount + 1, {}); Cpu::core_old_totals.insert(Cpu::core_old_totals.begin(), Shared::coreCount, 0); Cpu::core_old_idles.insert(Cpu::core_old_idles.begin(), Shared::coreCount, 0); Cpu::collect(); for (auto &[field, vec] : Cpu::current_cpu.cpu_percent) { if (not vec.empty() and not v_contains(Cpu::available_fields, field)) Cpu::available_fields.push_back(field); } Cpu::cpuName = Cpu::get_cpuName(); Cpu::got_sensors = Cpu::get_sensors(); Cpu::core_mapping = Cpu::get_core_mapping(); //? Init for namespace Mem Mem::old_uptime = system_uptime(); Mem::collect(); Mem::get_zpools(); } } // namespace Shared namespace Cpu { string cpuName; string cpuHz; bool has_battery = true; tuple current_bat; const array time_names = {"user", "nice", "system", "idle"}; unordered_flat_map cpu_old = { {"totals", 0}, {"idles", 0}, {"user", 0}, {"nice", 0}, {"system", 0}, {"idle", 0} }; string get_cpuName() { string name; char buffer[1024]; size_t size = sizeof(buffer); if (sysctlbyname("hw.model", &buffer, &size, NULL, 0) < 0) { Logger::error("Failed to get CPU name"); return name; } name = string(buffer); auto name_vec = ssplit(name); if ((s_contains(name, "Xeon"s) or v_contains(name_vec, "Duo"s)) and v_contains(name_vec, "CPU"s)) { auto cpu_pos = v_index(name_vec, "CPU"s); if (cpu_pos < name_vec.size() - 1 and not name_vec.at(cpu_pos + 1).ends_with(')')) name = name_vec.at(cpu_pos + 1); else name.clear(); } else if (v_contains(name_vec, "Ryzen"s)) { auto ryz_pos = v_index(name_vec, "Ryzen"s); name = "Ryzen" + (ryz_pos < name_vec.size() - 1 ? ' ' + name_vec.at(ryz_pos + 1) : "") + (ryz_pos < name_vec.size() - 2 ? ' ' + name_vec.at(ryz_pos + 2) : ""); } else if (s_contains(name, "Intel"s) and v_contains(name_vec, "CPU"s)) { auto cpu_pos = v_index(name_vec, "CPU"s); if (cpu_pos < name_vec.size() - 1 and not name_vec.at(cpu_pos + 1).ends_with(')') and name_vec.at(cpu_pos + 1) != "@") name = name_vec.at(cpu_pos + 1); else name.clear(); } else name.clear(); if (name.empty() and not name_vec.empty()) { for (const auto &n : name_vec) { if (n == "@") break; name += n + ' '; } name.pop_back(); for (const auto& replace : {"Processor", "CPU", "(R)", "(TM)", "Intel", "AMD", "Core"}) { name = s_replace(name, replace, ""); name = s_replace(name, " ", " "); } name = trim(name); } return name; } bool get_sensors() { got_sensors = false; if (Config::getB("show_coretemp") and Config::getB("check_temp")) { int32_t temp; size_t size = sizeof(temp); if (sysctlbyname("dev.cpu.0.temperature", &temp, &size, NULL, 0) < 0) { Logger::warning("Could not get temp sensor - maybe you need to load the coretemp module"); } else { got_sensors = true; int temp; size_t size = sizeof(temp); sysctlbyname("dev.cpu.0.coretemp.tjmax", &temp, &size, NULL, 0); //asuming the max temp is same for all cores temp = (temp - 2732) / 10; // since it's an int, it's multiplied by 10, and offset to absolute zero... current_cpu.temp_max = temp; } } return got_sensors; } void update_sensors() { int temp; size_t size = sizeof(temp); sysctlbyname("hw.acpi.thermal.tz0.temperature", &temp, &size, NULL, 0); temp = (temp - 2732) / 10; // since it's an int, it's multiplied by 10, and offset to absolute zero... current_cpu.temp.at(0).push_back(temp); if (current_cpu.temp.at(0).size() > 20) current_cpu.temp.at(0).pop_front(); for (int i = 0; i < Shared::coreCount; i++) { string s = "dev.cpu." + std::to_string(i) + ".temperature"; if (sysctlbyname(s.c_str(), &temp, &size, NULL, 0) < 0) { Logger::warning("Could not get temp sensor - maybe you need to load the coretemp module"); } else { temp = (temp - 2732) / 10; if (cmp_less(i + 1, current_cpu.temp.size())) { current_cpu.temp.at(i + 1).push_back(temp); if (current_cpu.temp.at(i + 1).size() > 20) current_cpu.temp.at(i + 1).pop_front(); } } } } string get_cpuHz() { unsigned int freq = 1; size_t size = sizeof(freq); if (sysctlbyname("dev.cpu.0.freq", &freq, &size, NULL, 0) < 0) { return ""; } return std::to_string(freq / 1000.0 ).substr(0, 3); // seems to be in MHz } auto get_core_mapping() -> unordered_flat_map { unordered_flat_map core_map; if (cpu_temp_only) return core_map; for (long i = 0; i < Shared::coreCount; i++) { core_map[i] = i; } //? If core mapping from cpuinfo was incomplete try to guess remainder, if missing completely, map 0-0 1-1 2-2 etc. if (cmp_less(core_map.size(), Shared::coreCount)) { if (Shared::coreCount % 2 == 0 and (long) core_map.size() == Shared::coreCount / 2) { for (int i = 0, n = 0; i < Shared::coreCount / 2; i++) { if (std::cmp_greater_equal(n, core_sensors.size())) n = 0; core_map[Shared::coreCount / 2 + i] = n++; } } else { core_map.clear(); for (int i = 0, n = 0; i < Shared::coreCount; i++) { if (std::cmp_greater_equal(n, core_sensors.size())) n = 0; core_map[i] = n++; } } } //? Apply user set custom mapping if any const auto &custom_map = Config::getS("cpu_core_map"); if (not custom_map.empty()) { try { for (const auto &split : ssplit(custom_map)) { const auto vals = ssplit(split, ':'); if (vals.size() != 2) continue; int change_id = std::stoi(vals.at(0)); int new_id = std::stoi(vals.at(1)); if (not core_map.contains(change_id) or cmp_greater(new_id, core_sensors.size())) continue; core_map.at(change_id) = new_id; } } catch (...) { } } return core_map; } auto get_battery() -> tuple { if (not has_battery) return {0, 0, ""}; long seconds = -1; uint32_t percent = -1; size_t size = sizeof(percent); string status = "discharging"; if (sysctlbyname("hw.acpi.battery.life", &percent, &size, NULL, 0) < 0) { has_battery = false; } else { has_battery = true; size_t size = sizeof(seconds); if (sysctlbyname("hw.acpi.battery.time", &seconds, &size, NULL, 0) < 0) { seconds = 0; } int state; size = sizeof(state); if (sysctlbyname("hw.acpi.battery.state", &state, &size, NULL, 0) < 0) { status = "unknown"; } else { if (state == 2) { status = "charging"; } } if (percent == 100) { status = "full"; } } return {percent, seconds, status}; } auto collect(const bool no_update) -> cpu_info & { if (Runner::stopping or (no_update and not current_cpu.cpu_percent.at("total").empty())) return current_cpu; auto &cpu = current_cpu; double avg[3]; if (getloadavg(avg, sizeof(avg)) < 0) { Logger::error("failed to get load averages"); } cpu.load_avg = { (float)avg[0], (float)avg[1], (float)avg[2]}; vector> cpu_time(Shared::coreCount); size_t size = sizeof(long) * CPUSTATES * Shared::coreCount; if (sysctlbyname("kern.cp_times", &cpu_time[0], &size, NULL, 0) == -1) { Logger::error("failed to get CPU times"); } long long global_totals = 0; long long global_idles = 0; vector times_summed = {0, 0, 0, 0}; for (long i = 0; i < Shared::coreCount; i++) { vector times; //? 0=user, 1=nice, 2=system, 3=idle for (int x = 0; const unsigned int c_state : {CP_USER, CP_NICE, CP_SYS, CP_IDLE}) { auto val = cpu_time[i][c_state]; times.push_back(val); times_summed.at(x++) += val; } try { //? All values const long long totals = std::accumulate(times.begin(), times.end(), 0ll); //? Idle time const long long idles = times.at(3); global_totals += totals; global_idles += idles; //? Calculate cpu total for each core if (i > Shared::coreCount) break; const long long calc_totals = max(0ll, totals - core_old_totals.at(i)); const long long calc_idles = max(0ll, idles - core_old_idles.at(i)); core_old_totals.at(i) = totals; core_old_idles.at(i) = idles; cpu.core_percent.at(i).push_back(clamp((long long)round((double)(calc_totals - calc_idles) * 100 / calc_totals), 0ll, 100ll)); //? Reduce size if there are more values than needed for graph if (cpu.core_percent.at(i).size() > 40) cpu.core_percent.at(i).pop_front(); } catch (const std::exception &e) { Logger::error("Cpu::collect() : " + (string)e.what()); throw std::runtime_error("collect() : " + (string)e.what()); } } const long long calc_totals = max(1ll, global_totals - cpu_old.at("totals")); const long long calc_idles = max(1ll, global_idles - cpu_old.at("idles")); //? Populate cpu.cpu_percent with all fields from syscall for (int ii = 0; const auto &val : times_summed) { cpu.cpu_percent.at(time_names.at(ii)).push_back(clamp((long long)round((double)(val - cpu_old.at(time_names.at(ii))) * 100 / calc_totals), 0ll, 100ll)); cpu_old.at(time_names.at(ii)) = val; //? Reduce size if there are more values than needed for graph while (cmp_greater(cpu.cpu_percent.at(time_names.at(ii)).size(), width * 2)) cpu.cpu_percent.at(time_names.at(ii)).pop_front(); ii++; } cpu_old.at("totals") = global_totals; cpu_old.at("idles") = global_idles; //? Total usage of cpu cpu.cpu_percent.at("total").push_back(clamp((long long)round((double)(calc_totals - calc_idles) * 100 / calc_totals), 0ll, 100ll)); //? Reduce size if there are more values than needed for graph while (cmp_greater(cpu.cpu_percent.at("total").size(), width * 2)) cpu.cpu_percent.at("total").pop_front(); if (Config::getB("show_cpu_freq")) { auto hz = get_cpuHz(); if (hz != "") { cpuHz = hz; } } if (Config::getB("check_temp") and got_sensors) update_sensors(); if (Config::getB("show_battery") and has_battery) current_bat = get_battery(); return cpu; } } // namespace Cpu namespace Mem { bool has_swap = false; vector fstab; fs::file_time_type fstab_time; int disk_ios = 0; vector last_found; mem_info current_mem{}; uint64_t get_totalMem() { return Shared::totalMem; } void assign_values(struct disk_info& disk, int64_t readBytes, int64_t writeBytes) { disk_ios++; if (disk.io_read.empty()) { disk.io_read.push_back(0); } else { disk.io_read.push_back(max((int64_t)0, (readBytes - disk.old_io.at(0)))); } disk.old_io.at(0) = readBytes; while (cmp_greater(disk.io_read.size(), width * 2)) disk.io_read.pop_front(); if (disk.io_write.empty()) { disk.io_write.push_back(0); } else { disk.io_write.push_back(max((int64_t)0, (writeBytes - disk.old_io.at(1)))); } disk.old_io.at(1) = writeBytes; while (cmp_greater(disk.io_write.size(), width * 2)) disk.io_write.pop_front(); // no io times - need to push something anyway or we'll get an ABORT if (disk.io_activity.empty()) disk.io_activity.push_back(0); else disk.io_activity.push_back(clamp((long)round((double)(disk.io_write.back() + disk.io_read.back()) / (1 << 20)), 0l, 100l)); while (cmp_greater(disk.io_activity.size(), width * 2)) disk.io_activity.pop_front(); } class PipeWrapper { public: PipeWrapper(const char *file, const char *mode) {fd = popen(file, mode);} virtual ~PipeWrapper() {if (fd) pclose(fd);} auto operator()() -> FILE* { return fd;}; private: FILE *fd; }; // find all zpools in the system. Do this only at startup. void get_zpools() { PipeWrapper poolPipe = PipeWrapper("zpool list -H -o name", "r"); while (not std::feof(poolPipe())) { char poolName[512]; size_t len = 512; if (fgets(poolName, len, poolPipe())) { poolName[strcspn(poolName, "\n")] = 0; Logger::debug("zpool found: " + string(poolName)); Mem::zpools.push_back(poolName); } } } void collect_disk(unordered_flat_map &disks, unordered_flat_map &mapping) { // this bit is for 'regular' mounts static struct statinfo cur; long double etime = 0; uint64_t total_bytes_read; uint64_t total_bytes_write; static std::unique_ptr curDevInfo (reinterpret_cast(std::calloc(1, sizeof(struct devinfo))), std::free); cur.dinfo = curDevInfo.get(); if (devstat_getdevs(NULL, &cur) != -1) { for (int i = 0; i < cur.dinfo->numdevs; i++) { auto d = cur.dinfo->devices[i]; string devStatName = "/dev/" + string(d.device_name) + std::to_string(d.unit_number); for (auto& [ignored, disk] : disks) { // find matching mountpoints - could be multiple as d.device_name is only ada (and d.unit_number is the device number), while the disk.dev is like /dev/ada0s1 if (disk.dev.string().rfind(devStatName, 0) == 0) { devstat_compute_statistics(&d, NULL, etime, DSM_TOTAL_BYTES_READ, &total_bytes_read, DSM_TOTAL_BYTES_WRITE, &total_bytes_write, DSM_NONE); assign_values(disk, total_bytes_read, total_bytes_write); string mountpoint = mapping.at(disk.dev); Logger::debug("dev " + devStatName + " -> " + mountpoint + " read=" + std::to_string(total_bytes_read) + " write=" + std::to_string(total_bytes_write)); } } } Logger::debug(""); } // this code is for ZFS mounts for (string poolName : Mem::zpools) { char sysCtl[1024]; snprintf(sysCtl, sizeof(sysCtl), "sysctl kstat.zfs.%s.dataset | egrep \'dataset_name|nread|nwritten\'", poolName.c_str()); PipeWrapper f = PipeWrapper(sysCtl, "r"); if (f()) { char buf[512]; size_t len = 512; while (not std::feof(f())) { uint64_t nread = 0, nwritten = 0; if (fgets(buf, len, f())) { char *name = std::strtok(buf, ": \n"); char *value = std::strtok(NULL, ": \n"); if (string(name).find("dataset_name") != string::npos) { // create entry if datasetname matches with anything in mapping // relies on the fact that the dataset name is last value in the list // alternatively you could parse the objset-0x... when this changes, you have a new entry string datasetname = string(value);// this is the zfs volume, like 'zroot/usr/home' -> this maps onto the device we get back from getmntinfo(3) if (mapping.contains(datasetname)) { string mountpoint = mapping.at(datasetname); if (disks.contains(mountpoint)) { auto& disk = disks.at(mountpoint); assign_values(disk, nread, nwritten); } } } else if (string(name).find("nread") != string::npos) { nread = atoll(value); } else if (string(name).find("nwritten") != string::npos) { nwritten = atoll(value); } } } } } } auto collect(const bool no_update) -> mem_info & { if (Runner::stopping or (no_update and not current_mem.percent.at("used").empty())) return current_mem; auto &show_swap = Config::getB("show_swap"); auto &show_disks = Config::getB("show_disks"); auto &swap_disk = Config::getB("swap_disk"); auto &mem = current_mem; static const bool snapped = (getenv("BTOP_SNAPPED") != NULL); int mib[4]; u_int memActive, memWire, cachedMem, freeMem; size_t len; len = 4; sysctlnametomib("vm.stats.vm.v_active_count", mib, &len); len = sizeof(memActive); sysctl(mib, 4, &(memActive), &len, NULL, 0); memActive *= Shared::pageSize; len = 4; sysctlnametomib("vm.stats.vm.v_wire_count", mib, &len); len = sizeof(memWire); sysctl(mib, 4, &(memWire), &len, NULL, 0); memWire *= Shared::pageSize; mem.stats.at("used") = memWire + memActive; mem.stats.at("available") = Shared::totalMem - memActive - memWire; len = sizeof(cachedMem); len = 4; sysctlnametomib("vm.stats.vm.v_cache_count", mib, &len); sysctl(mib, 4, &(cachedMem), &len, NULL, 0); cachedMem *= Shared::pageSize; mem.stats.at("cached") = cachedMem; len = sizeof(freeMem); len = 4; sysctlnametomib("vm.stats.vm.v_free_count", mib, &len); sysctl(mib, 4, &(freeMem), &len, NULL, 0); freeMem *= Shared::pageSize; mem.stats.at("free") = freeMem; if (show_swap and mem.stats.at("swap_total") > 0) { for (const auto &name : swap_names) { mem.percent.at(name).push_back(round((double)mem.stats.at(name) * 100 / mem.stats.at("swap_total"))); while (cmp_greater(mem.percent.at(name).size(), width * 2)) mem.percent.at(name).pop_front(); } has_swap = true; } else has_swap = false; //? Calculate percentages for (const auto &name : mem_names) { mem.percent.at(name).push_back(round((double)mem.stats.at(name) * 100 / Shared::totalMem)); while (cmp_greater(mem.percent.at(name).size(), width * 2)) mem.percent.at(name).pop_front(); } if (show_disks) { unordered_flat_map mapping; // keep mapping from device -> mountpoint, since IOKit doesn't give us the mountpoint double uptime = system_uptime(); auto &disks_filter = Config::getS("disks_filter"); bool filter_exclude = false; // auto &only_physical = Config::getB("only_physical"); auto &disks = mem.disks; vector filter; if (not disks_filter.empty()) { filter = ssplit(disks_filter); if (filter.at(0).starts_with("exclude=")) { filter_exclude = true; filter.at(0) = filter.at(0).substr(8); } } struct statfs *stfs; int count = getmntinfo(&stfs, MNT_WAIT); vector found; found.reserve(last_found.size()); for (int i = 0; i < count; i++) { auto fstype = string(stfs[i].f_fstypename); if (fstype == "autofs" || fstype == "devfs" || fstype == "linprocfs" || fstype == "procfs" || fstype == "tmpfs" || fstype == "linsysfs" || fstype == "fdesckfs") { // in memory filesystems -> not useful to show continue; } std::error_code ec; string mountpoint = stfs[i].f_mntonname; string dev = stfs[i].f_mntfromname; mapping[dev] = mountpoint; //? Match filter if not empty if (not filter.empty()) { bool match = v_contains(filter, mountpoint); if ((filter_exclude and match) or (not filter_exclude and not match)) continue; } found.push_back(mountpoint); if (not disks.contains(mountpoint)) { disks[mountpoint] = disk_info{fs::canonical(dev, ec), fs::path(mountpoint).filename()}; if (disks.at(mountpoint).dev.empty()) disks.at(mountpoint).dev = dev; if (disks.at(mountpoint).name.empty()) disks.at(mountpoint).name = (mountpoint == "/" ? "root" : mountpoint); } if (not v_contains(last_found, mountpoint)) redraw = true; disks.at(mountpoint).free = stfs[i].f_bfree; disks.at(mountpoint).total = stfs[i].f_iosize; } //? Remove disks no longer mounted or filtered out if (swap_disk and has_swap) found.push_back("swap"); for (auto it = disks.begin(); it != disks.end();) { if (not v_contains(found, it->first)) it = disks.erase(it); else it++; } if (found.size() != last_found.size()) redraw = true; last_found = std::move(found); //? Get disk/partition stats for (auto &[mountpoint, disk] : disks) { if (std::error_code ec; not fs::exists(mountpoint, ec)) continue; struct statvfs vfs; if (statvfs(mountpoint.c_str(), &vfs) < 0) { Logger::warning("Failed to get disk/partition stats with statvfs() for: " + mountpoint); continue; } disk.total = vfs.f_blocks * vfs.f_frsize; disk.free = vfs.f_bfree * vfs.f_frsize; disk.used = disk.total - disk.free; disk.used_percent = round((double)disk.used * 100 / disk.total); disk.free_percent = 100 - disk.used_percent; } //? Setup disks order in UI and add swap if enabled mem.disks_order.clear(); if (snapped and disks.contains("/mnt")) mem.disks_order.push_back("/mnt"); else if (disks.contains("/")) mem.disks_order.push_back("/"); if (swap_disk and has_swap) { mem.disks_order.push_back("swap"); if (not disks.contains("swap")) disks["swap"] = {"", "swap"}; disks.at("swap").total = mem.stats.at("swap_total"); disks.at("swap").used = mem.stats.at("swap_used"); disks.at("swap").free = mem.stats.at("swap_free"); disks.at("swap").used_percent = mem.percent.at("swap_used").back(); disks.at("swap").free_percent = mem.percent.at("swap_free").back(); } for (const auto &name : last_found) if (not is_in(name, "/", "swap", "/dev")) mem.disks_order.push_back(name); disk_ios = 0; collect_disk(disks, mapping); old_uptime = uptime; } return mem; } } // namespace Mem namespace Net { unordered_flat_map current_net; net_info empty_net = {}; vector interfaces; string selected_iface; int errors = 0; unordered_flat_map graph_max = {{"download", {}}, {"upload", {}}}; unordered_flat_map> max_count = {{"download", {}}, {"upload", {}}}; bool rescale = true; uint64_t timestamp = 0; //* RAII wrapper for getifaddrs class getifaddr_wrapper { struct ifaddrs *ifaddr; public: int status; getifaddr_wrapper() { status = getifaddrs(&ifaddr); } ~getifaddr_wrapper() { freeifaddrs(ifaddr); } auto operator()() -> struct ifaddrs * { return ifaddr; } }; auto collect(const bool no_update) -> net_info & { auto &net = current_net; auto &config_iface = Config::getS("net_iface"); auto &net_sync = Config::getB("net_sync"); auto &net_auto = Config::getB("net_auto"); auto new_timestamp = time_ms(); if (not no_update and errors < 3) { //? Get interface list using getifaddrs() wrapper getifaddr_wrapper if_wrap{}; if (if_wrap.status != 0) { errors++; Logger::error("Net::collect() -> getifaddrs() failed with id " + to_string(if_wrap.status)); redraw = true; return empty_net; } int family = 0; char ip[NI_MAXHOST]; interfaces.clear(); string ipv4, ipv6; //? Iteration over all items in getifaddrs() list for (auto *ifa = if_wrap(); ifa != NULL; ifa = ifa->ifa_next) { if (ifa->ifa_addr == NULL) continue; family = ifa->ifa_addr->sa_family; const auto &iface = ifa->ifa_name; //? Get IPv4 address if (family == AF_INET) { if (getnameinfo(ifa->ifa_addr, sizeof(struct sockaddr), ip, NI_MAXHOST, NULL, 0, NI_NUMERICHOST) == 0) net[iface].ipv4 = ip; } //? Get IPv6 address // else if (family == AF_INET6) { // if (getnameinfo(ifa->ifa_addr, sizeof(struct sockaddr_in6), ip, NI_MAXHOST, NULL, 0, NI_NUMERICHOST) == 0) // net[iface].ipv6 = ip; // } //? Update available interfaces vector and get status of interface if (not v_contains(interfaces, iface)) { interfaces.push_back(iface); net[iface].connected = (ifa->ifa_flags & IFF_RUNNING); } } unordered_flat_map> ifstats; int mib[] = {CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST, 0}; size_t len; if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) { Logger::error("failed getting network interfaces"); } else { std::unique_ptr buf(new char[len]); if (sysctl(mib, 6, buf.get(), &len, NULL, 0) < 0) { Logger::error("failed getting network interfaces"); } else { char *lim = buf.get() + len; char *next = NULL; for (next = buf.get(); next < lim;) { struct if_msghdr *ifm = (struct if_msghdr *)next; next += ifm->ifm_msglen; struct if_data ifm_data = ifm->ifm_data; if (ifm->ifm_addrs & RTA_IFP) { struct sockaddr_dl *sdl = (struct sockaddr_dl *)(ifm + 1); char iface[32]; strncpy(iface, sdl->sdl_data, sdl->sdl_nlen); iface[sdl->sdl_nlen] = 0; ifstats[iface] = std::tuple(ifm_data.ifi_ibytes, ifm_data.ifi_obytes); } } } } //? Get total recieved and transmitted bytes + device address if no ip was found for (const auto &iface : interfaces) { for (const string dir : {"download", "upload"}) { auto &saved_stat = net.at(iface).stat.at(dir); auto &bandwidth = net.at(iface).bandwidth.at(dir); uint64_t val = dir == "download" ? std::get<0>(ifstats[iface]) : std::get<1>(ifstats[iface]); //? Update speed, total and top values if (val < saved_stat.last) { saved_stat.rollover += saved_stat.last; saved_stat.last = 0; } if (cmp_greater((unsigned long long)saved_stat.rollover + (unsigned long long)val, numeric_limits::max())) { saved_stat.rollover = 0; saved_stat.last = 0; } saved_stat.speed = round((double)(val - saved_stat.last) / ((double)(new_timestamp - timestamp) / 1000)); if (saved_stat.speed > saved_stat.top) saved_stat.top = saved_stat.speed; if (saved_stat.offset > val + saved_stat.rollover) saved_stat.offset = 0; saved_stat.total = (val + saved_stat.rollover) - saved_stat.offset; saved_stat.last = val; //? Add values to graph bandwidth.push_back(saved_stat.speed); while (cmp_greater(bandwidth.size(), width * 2)) bandwidth.pop_front(); //? Set counters for auto scaling if (net_auto and selected_iface == iface) { if (saved_stat.speed > graph_max[dir]) { ++max_count[dir][0]; if (max_count[dir][1] > 0) --max_count[dir][1]; } else if (graph_max[dir] > 10 << 10 and saved_stat.speed < graph_max[dir] / 10) { ++max_count[dir][1]; if (max_count[dir][0] > 0) --max_count[dir][0]; } } } } //? Clean up net map if needed if (net.size() > interfaces.size()) { for (auto it = net.begin(); it != net.end();) { if (not v_contains(interfaces, it->first)) it = net.erase(it); else it++; } net.compact(); } timestamp = new_timestamp; } //? Return empty net_info struct if no interfaces was found if (net.empty()) return empty_net; //? Find an interface to display if selected isn't set or valid if (selected_iface.empty() or not v_contains(interfaces, selected_iface)) { max_count["download"][0] = max_count["download"][1] = max_count["upload"][0] = max_count["upload"][1] = 0; redraw = true; if (net_auto) rescale = true; if (not config_iface.empty() and v_contains(interfaces, config_iface)) selected_iface = config_iface; else { //? Sort interfaces by total upload + download bytes auto sorted_interfaces = interfaces; rng::sort(sorted_interfaces, [&](const auto &a, const auto &b) { return cmp_greater(net.at(a).stat["download"].total + net.at(a).stat["upload"].total, net.at(b).stat["download"].total + net.at(b).stat["upload"].total); }); selected_iface.clear(); //? Try to set to a connected interface for (const auto &iface : sorted_interfaces) { if (net.at(iface).connected) selected_iface = iface; break; } //? If no interface is connected set to first available if (selected_iface.empty() and not sorted_interfaces.empty()) selected_iface = sorted_interfaces.at(0); else if (sorted_interfaces.empty()) return empty_net; } } //? Calculate max scale for graphs if needed if (net_auto) { bool sync = false; for (const auto &dir : {"download", "upload"}) { for (const auto &sel : {0, 1}) { if (rescale or max_count[dir][sel] >= 5) { const uint64_t avg_speed = (net[selected_iface].bandwidth[dir].size() > 5 ? std::accumulate(net.at(selected_iface).bandwidth.at(dir).rbegin(), net.at(selected_iface).bandwidth.at(dir).rbegin() + 5, 0) / 5 : net[selected_iface].stat[dir].speed); graph_max[dir] = max(uint64_t(avg_speed * (sel == 0 ? 1.3 : 3.0)), (uint64_t)10 << 10); max_count[dir][0] = max_count[dir][1] = 0; redraw = true; if (net_sync) sync = true; break; } } //? Sync download/upload graphs if enabled if (sync) { const auto other = (string(dir) == "upload" ? "download" : "upload"); graph_max[other] = graph_max[dir]; max_count[other][0] = max_count[other][1] = 0; break; } } } rescale = false; return net.at(selected_iface); } } // namespace Net namespace Proc { vector current_procs; unordered_flat_map uid_user; string current_sort; string current_filter; bool current_rev = false; fs::file_time_type passwd_time; uint64_t cputimes; int collapse = -1, expand = -1; uint64_t old_cputimes = 0; atomic numpids = 0; int filter_found = 0; detail_container detailed; //* Generate process tree list void _tree_gen(proc_info &cur_proc, vector &in_procs, vector> &out_procs, int cur_depth, const bool collapsed, const string &filter, bool found = false, const bool no_update = false, const bool should_filter = false) { auto cur_pos = out_procs.size(); bool filtering = false; //? If filtering, include children of matching processes if (not found and (should_filter or not filter.empty())) { if (not s_contains(std::to_string(cur_proc.pid), filter) and not s_contains(cur_proc.name, filter) and not s_contains(cur_proc.cmd, filter) and not s_contains(cur_proc.user, filter)) { filtering = true; cur_proc.filtered = true; filter_found++; } else { found = true; cur_depth = 0; } } else if (cur_proc.filtered) cur_proc.filtered = false; //? Set tree index position for process if not filtered out or currently in a collapsed sub-tree if (not collapsed and not filtering) { out_procs.push_back(std::ref(cur_proc)); cur_proc.tree_index = out_procs.size() - 1; //? Try to find name of the binary file and append to program name if not the same if (cur_proc.short_cmd.empty() and not cur_proc.cmd.empty()) { std::string_view cmd_view = cur_proc.cmd; cmd_view = cmd_view.substr((size_t)0, min(cmd_view.find(' '), cmd_view.size())); cmd_view = cmd_view.substr(min(cmd_view.find_last_of('/') + 1, cmd_view.size())); cur_proc.short_cmd = (string)cmd_view; } } else { cur_proc.tree_index = in_procs.size(); } //? Recursive iteration over all children int children = 0; for (auto &p : rng::equal_range(in_procs, cur_proc.pid, rng::less{}, &proc_info::ppid)) { if (not no_update and not filtering and (collapsed or cur_proc.collapsed)) { out_procs.back().get().cpu_p += p.cpu_p; out_procs.back().get().mem += p.mem; out_procs.back().get().threads += p.threads; filter_found++; } if (collapsed and not filtering) { cur_proc.filtered = true; } else children++; _tree_gen(p, in_procs, out_procs, cur_depth + 1, (collapsed ? true : cur_proc.collapsed), filter, found, no_update, should_filter); } if (collapsed or filtering) return; //? Add tree terminator symbol if it's the last child in a sub-tree if (out_procs.size() > cur_pos + 1 and not out_procs.back().get().prefix.ends_with("]─")) out_procs.back().get().prefix.replace(out_procs.back().get().prefix.size() - 8, 8, " └─ "); //? Add collapse/expand symbols if process have any children out_procs.at(cur_pos).get().prefix = " │ "s * cur_depth + (children > 0 ? (cur_proc.collapsed ? "[+]─" : "[-]─") : " ├─ "); } string get_status(char s) { if (s & SRUN) return "Running"; if (s & SSLEEP) return "Sleeping"; if (s & SIDL) return "Idle"; if (s & SSTOP) return "Stopped"; if (s & SZOMB) return "Zombie"; return "Unknown"; } //* Get detailed info for selected process void _collect_details(const size_t pid, vector &procs) { if (pid != detailed.last_pid) { detailed = {}; detailed.last_pid = pid; detailed.skip_smaps = not Config::getB("proc_info_smaps"); } //? Copy proc_info for process from proc vector auto p_info = rng::find(procs, pid, &proc_info::pid); detailed.entry = *p_info; //? Update cpu percent deque for process cpu graph if (not Config::getB("proc_per_core")) detailed.entry.cpu_p *= Shared::coreCount; detailed.cpu_percent.push_back(clamp((long long)round(detailed.entry.cpu_p), 0ll, 100ll)); while (cmp_greater(detailed.cpu_percent.size(), width)) detailed.cpu_percent.pop_front(); //? Process runtime : current time - start time (both in unix time - seconds since epoch) struct timeval currentTime; gettimeofday(¤tTime, NULL); detailed.elapsed = sec_to_dhms(currentTime.tv_sec - detailed.entry.cpu_s); // only interested in second granularity, so ignoring tc_usec if (detailed.elapsed.size() > 8) detailed.elapsed.resize(detailed.elapsed.size() - 3); //? Get parent process name if (detailed.parent.empty()) { auto p_entry = rng::find(procs, detailed.entry.ppid, &proc_info::pid); if (p_entry != procs.end()) detailed.parent = p_entry->name; } //? Expand process status from single char to explanative string detailed.status = get_status(detailed.entry.state); detailed.mem_bytes.push_back(detailed.entry.mem); detailed.memory = floating_humanizer(detailed.entry.mem); if (detailed.first_mem == -1 or detailed.first_mem < detailed.mem_bytes.back() / 2 or detailed.first_mem > detailed.mem_bytes.back() * 4) { detailed.first_mem = min((uint64_t)detailed.mem_bytes.back() * 2, Mem::get_totalMem()); redraw = true; } while (cmp_greater(detailed.mem_bytes.size(), width)) detailed.mem_bytes.pop_front(); // rusage_info_current rusage; // if (proc_pid_rusage(pid, RUSAGE_INFO_CURRENT, (void **)&rusage) == 0) { // // this fails for processes we don't own - same as in Linux // detailed.io_read = floating_humanizer(rusage.ri_diskio_bytesread); // detailed.io_write = floating_humanizer(rusage.ri_diskio_byteswritten); // } } //* RAII wrapper for kvm_openfiles class kvm_openfiles_wrapper { kvm_t* kd = NULL; public: kvm_openfiles_wrapper(const char* execf, const char* coref, const char* swapf, int flags, char* err) { this->kd = kvm_openfiles(execf, coref, swapf, flags, err); } ~kvm_openfiles_wrapper() { kvm_close(kd); } auto operator()() -> kvm_t* { return kd; } }; //* Collects and sorts process information from /proc auto collect(const bool no_update) -> vector & { const auto &sorting = Config::getS("proc_sorting"); const auto &reverse = Config::getB("proc_reversed"); const auto &filter = Config::getS("proc_filter"); const auto &per_core = Config::getB("proc_per_core"); const auto &tree = Config::getB("proc_tree"); const auto &show_detailed = Config::getB("show_detailed"); const size_t detailed_pid = Config::getI("detailed_pid"); bool should_filter = current_filter != filter; if (should_filter) current_filter = filter; const bool sorted_change = (sorting != current_sort or reverse != current_rev or should_filter); if (sorted_change) { current_sort = sorting; current_rev = reverse; } const int cmult = (per_core) ? Shared::coreCount : 1; bool got_detailed = false; vector> cpu_time(Shared::coreCount); size_t size = sizeof(long) * CPUSTATES * Shared::coreCount; if (sysctlbyname("kern.cp_times", &cpu_time[0], &size, NULL, 0) == -1) { Logger::error("failed to get CPU times"); } cputimes = 0; for (const auto core : cpu_time) { for (const unsigned int c_state : {CP_USER, CP_NICE, CP_SYS, CP_IDLE}) { cputimes += core[c_state]; } } //* Use pids from last update if only changing filter, sorting or tree options if (no_update and not current_procs.empty()) { if (show_detailed and detailed_pid != detailed.last_pid) _collect_details(detailed_pid, current_procs); } else { //* ---------------------------------------------Collection start---------------------------------------------- should_filter = true; vector found; struct timeval currentTime; gettimeofday(¤tTime, NULL); const double timeNow = currentTime.tv_sec + (currentTime.tv_usec / 1'000'000); int count = 0; char buf[_POSIX2_LINE_MAX]; kvm_openfiles_wrapper kd(NULL, _PATH_DEVNULL, NULL, O_RDONLY, buf); const struct kinfo_proc* kprocs = kvm_getprocs(kd(), KERN_PROC_PROC, 0, &count); for (int i = 0; i < count; i++) { const struct kinfo_proc* kproc = &kprocs[i]; const size_t pid = (size_t)kproc->ki_pid; if (pid < 1) continue; found.push_back(pid); //? Check if pid already exists in current_procs bool no_cache = false; auto find_old = rng::find(current_procs, pid, &proc_info::pid); if (find_old == current_procs.end()) { current_procs.push_back({pid}); find_old = current_procs.end() - 1; no_cache = true; } auto &new_proc = *find_old; //? Get program name, command, username, parent pid, nice and status if (no_cache) { if (kproc->ki_comm == NULL or kproc->ki_comm == "idle"s) { current_procs.pop_back(); continue; } new_proc.name = kproc->ki_comm; char** argv = kvm_getargv(kd(), kproc, 0); if (argv) { for (int i = 0; argv[i]; i++) { new_proc.cmd += argv[i] + " "s; } if (not new_proc.cmd.empty()) new_proc.cmd.pop_back(); } if (new_proc.cmd.empty()) new_proc.cmd = new_proc.name; new_proc.ppid = kproc->ki_ppid; new_proc.cpu_s = round(kproc->ki_start.tv_sec); struct passwd *pwd = getpwuid(kproc->ki_uid); if (pwd) new_proc.user = pwd->pw_name; } new_proc.p_nice = kproc->ki_nice; new_proc.state = kproc->ki_stat; int cpu_t = 0; cpu_t = kproc->ki_rusage.ru_utime.tv_sec * 1'000'000 + kproc->ki_rusage.ru_utime.tv_usec + kproc->ki_rusage.ru_stime.tv_sec * 1'000'000 + kproc->ki_rusage.ru_stime.tv_usec; new_proc.mem = kproc->ki_rssize * Shared::pageSize; new_proc.threads = kproc->ki_numthreads; //? Process cpu usage since last update new_proc.cpu_p = clamp((100.0 * kproc->ki_pctcpu / Shared::kfscale) * cmult, 0.0, 100.0 * Shared::coreCount); //? Process cumulative cpu usage since process start new_proc.cpu_c = (double)(cpu_t * Shared::clkTck / 1'000'000) / max(1.0, timeNow - new_proc.cpu_s); //? Update cached value with latest cpu times new_proc.cpu_t = cpu_t; if (show_detailed and not got_detailed and new_proc.pid == detailed_pid) { got_detailed = true; } // //? Clear dead processes from current_procs auto eraser = rng::remove_if(current_procs, [&](const auto &element) { return not v_contains(found, element.pid); }); current_procs.erase(eraser.begin(), eraser.end()); //? Update the details info box for process if active if (show_detailed and got_detailed) { _collect_details(detailed_pid, current_procs); } else if (show_detailed and not got_detailed and detailed.status != "Dead") { detailed.status = "Dead"; redraw = true; } old_cputimes = cputimes; } } //* ---------------------------------------------Collection done----------------------------------------------- //* Sort processes if (sorted_change or not no_update) { if (reverse) { switch (v_index(sort_vector, sorting)) { case 0: rng::stable_sort(current_procs, rng::less{}, &proc_info::pid); break; case 1: rng::stable_sort(current_procs, rng::less{}, &proc_info::name); break; case 2: rng::stable_sort(current_procs, rng::less{}, &proc_info::cmd); break; case 3: rng::stable_sort(current_procs, rng::less{}, &proc_info::threads); break; case 4: rng::stable_sort(current_procs, rng::less{}, &proc_info::user); break; case 5: rng::stable_sort(current_procs, rng::less{}, &proc_info::mem); break; case 6: rng::stable_sort(current_procs, rng::less{}, &proc_info::cpu_p); break; case 7: rng::stable_sort(current_procs, rng::less{}, &proc_info::cpu_c); break; } } else { switch (v_index(sort_vector, sorting)) { case 0: rng::stable_sort(current_procs, rng::greater{}, &proc_info::pid); break; case 1: rng::stable_sort(current_procs, rng::greater{}, &proc_info::name); break; case 2: rng::stable_sort(current_procs, rng::greater{}, &proc_info::cmd); break; case 3: rng::stable_sort(current_procs, rng::greater{}, &proc_info::threads); break; case 4: rng::stable_sort(current_procs, rng::greater{}, &proc_info::user); break; case 5: rng::stable_sort(current_procs, rng::greater{}, &proc_info::mem); break; case 6: rng::stable_sort(current_procs, rng::greater{}, &proc_info::cpu_p); break; case 7: rng::stable_sort(current_procs, rng::greater{}, &proc_info::cpu_c); break; } } //* When sorting with "cpu lazy" push processes over threshold cpu usage to the front regardless of cumulative usage if (not tree and not reverse and sorting == "cpu lazy") { double max = 10.0, target = 30.0; for (size_t i = 0, x = 0, offset = 0; i < current_procs.size(); i++) { if (i <= 5 and current_procs.at(i).cpu_p > max) max = current_procs.at(i).cpu_p; else if (i == 6) target = (max > 30.0) ? max : 10.0; if (i == offset and current_procs.at(i).cpu_p > 30.0) offset++; else if (current_procs.at(i).cpu_p > target) { rotate(current_procs.begin() + offset, current_procs.begin() + i, current_procs.begin() + i + 1); if (++x > 10) break; } } } } //* Match filter if defined if (should_filter) { filter_found = 0; for (auto &p : current_procs) { if (not tree and not filter.empty()) { if (not s_contains(to_string(p.pid), filter) and not s_contains(p.name, filter) and not s_contains(p.cmd, filter) and not s_contains(p.user, filter)) { p.filtered = true; filter_found++; } else { p.filtered = false; } } else { p.filtered = false; } } } //* Generate tree view if enabled if (tree and (not no_update or should_filter or sorted_change)) { if (auto find_pid = (collapse != -1 ? collapse : expand); find_pid != -1) { auto collapser = rng::find(current_procs, find_pid, &proc_info::pid); if (collapser != current_procs.end()) { if (collapse == expand) { collapser->collapsed = not collapser->collapsed; } else if (collapse > -1) { collapser->collapsed = true; } else if (expand > -1) { collapser->collapsed = false; } } collapse = expand = -1; } if (should_filter or not filter.empty()) filter_found = 0; vector> tree_procs; tree_procs.reserve(current_procs.size()); //? Stable sort to retain selected sorting among processes with the same parent rng::stable_sort(current_procs, rng::less{}, &proc_info::ppid); //? Start recursive iteration over processes with the lowest shared parent pids for (auto &p : rng::equal_range(current_procs, current_procs.at(0).ppid, rng::less{}, &proc_info::ppid)) { _tree_gen(p, current_procs, tree_procs, 0, false, filter, false, no_update, should_filter); } //? Final sort based on tree index rng::stable_sort(current_procs, rng::less{}, &proc_info::tree_index); } numpids = (int)current_procs.size() - filter_found; return current_procs; } } // namespace Proc namespace Tools { double system_uptime() { struct timeval ts, currTime; std::size_t len = sizeof(ts); int mib[2] = {CTL_KERN, KERN_BOOTTIME}; if (sysctl(mib, 2, &ts, &len, NULL, 0) != -1) { gettimeofday(&currTime, NULL); return currTime.tv_sec - ts.tv_sec; } return 0.0; } } // namespace Tools btop-1.2.3/src/linux/000077500000000000000000000000001420276253000144105ustar00rootroot00000000000000btop-1.2.3/src/linux/btop_collect.cpp000066400000000000000000001676371420276253000176110ustar00rootroot00000000000000/* 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. indent = tab tab-size = 4 */ #include #include #include #include #include #include #include #include #include #if !(defined(STATIC_BUILD) && defined(__GLIBC__)) #include #endif #include #include #include using std::ifstream, std::numeric_limits, std::streamsize, std::round, std::max, std::min; using std::clamp, std::string_literals::operator""s, std::cmp_equal, std::cmp_less, std::cmp_greater; namespace fs = std::filesystem; namespace rng = std::ranges; using namespace Tools; //? --------------------------------------------------- FUNCTIONS ----------------------------------------------------- namespace Cpu { vector core_old_totals; vector core_old_idles; vector available_fields; vector available_sensors = {"Auto"}; cpu_info current_cpu; fs::path freq_path = "/sys/devices/system/cpu/cpufreq/policy0/scaling_cur_freq"; bool got_sensors = false, cpu_temp_only = false; //* Populate found_sensors map bool get_sensors(); //* Get current cpu clock speed string get_cpuHz(); //* Search /proc/cpuinfo for a cpu name string get_cpuName(); struct Sensor { fs::path path; string label; int64_t temp = 0; int64_t high = 0; int64_t crit = 0; }; unordered_flat_map found_sensors; string cpu_sensor; vector core_sensors; unordered_flat_map core_mapping; } namespace Mem { double old_uptime; } namespace Shared { fs::path procPath, passwd_path; long pageSize, clkTck, coreCount; void init() { //? Shared global variables init procPath = (fs::is_directory(fs::path("/proc")) and access("/proc", R_OK) != -1) ? "/proc" : ""; if (procPath.empty()) throw std::runtime_error("Proc filesystem not found or no permission to read from it!"); passwd_path = (fs::is_regular_file(fs::path("/etc/passwd")) and access("/etc/passwd", R_OK) != -1) ? "/etc/passwd" : ""; if (passwd_path.empty()) Logger::warning("Could not read /etc/passwd, will show UID instead of username."); coreCount = sysconf(_SC_NPROCESSORS_ONLN); if (coreCount < 1) { coreCount = 1; Logger::warning("Could not determine number of cores, defaulting to 1."); } pageSize = sysconf(_SC_PAGE_SIZE); if (pageSize <= 0) { pageSize = 4096; Logger::warning("Could not get system page size. Defaulting to 4096, processes memory usage might be incorrect."); } clkTck = sysconf(_SC_CLK_TCK); if (clkTck <= 0) { clkTck = 100; Logger::warning("Could not get system clock ticks per second. Defaulting to 100, processes cpu usage might be incorrect."); } //? Init for namespace Cpu if (not fs::exists(Cpu::freq_path) or access(Cpu::freq_path.c_str(), R_OK) == -1) Cpu::freq_path.clear(); Cpu::current_cpu.core_percent.insert(Cpu::current_cpu.core_percent.begin(), Shared::coreCount, {}); Cpu::current_cpu.temp.insert(Cpu::current_cpu.temp.begin(), Shared::coreCount + 1, {}); Cpu::core_old_totals.insert(Cpu::core_old_totals.begin(), Shared::coreCount, 0); Cpu::core_old_idles.insert(Cpu::core_old_idles.begin(), Shared::coreCount, 0); Cpu::collect(); for (auto& [field, vec] : Cpu::current_cpu.cpu_percent) { if (not vec.empty()) Cpu::available_fields.push_back(field); } Cpu::cpuName = Cpu::get_cpuName(); Cpu::got_sensors = Cpu::get_sensors(); for (const auto& [sensor, ignored] : Cpu::found_sensors) { Cpu::available_sensors.push_back(sensor); } Cpu::core_mapping = Cpu::get_core_mapping(); //? Init for namespace Mem Mem::old_uptime = system_uptime(); Mem::collect(); } } namespace Cpu { string cpuName; string cpuHz; bool has_battery = true; tuple current_bat; const array time_names = {"user", "nice", "system", "idle", "iowait", "irq", "softirq", "steal", "guest", "guest_nice"}; unordered_flat_map cpu_old = { {"totals", 0}, {"idles", 0}, {"user", 0}, {"nice", 0}, {"system", 0}, {"idle", 0}, {"iowait", 0}, {"irq", 0}, {"softirq", 0}, {"steal", 0}, {"guest", 0}, {"guest_nice", 0} }; string get_cpuName() { string name; ifstream cpuinfo(Shared::procPath / "cpuinfo"); if (cpuinfo.good()) { for (string instr; getline(cpuinfo, instr, ':') and not instr.starts_with("model name");) cpuinfo.ignore(SSmax, '\n'); if (cpuinfo.bad()) return name; else if (not cpuinfo.eof()) { cpuinfo.ignore(1); getline(cpuinfo, name); } else if (fs::exists("/sys/devices")) { for (const auto& d : fs::directory_iterator("/sys/devices")) { if (string(d.path().filename()).starts_with("arm")) { name = d.path().filename(); break; } } if (not name.empty()) { auto name_vec = ssplit(name, '_'); if (name_vec.size() < 2) return capitalize(name); else return capitalize(name_vec.at(1)) + (name_vec.size() > 2 ? ' ' + capitalize(name_vec.at(2)) : ""); } } auto name_vec = ssplit(name); if ((s_contains(name, "Xeon"s) or v_contains(name_vec, "Duo"s)) and v_contains(name_vec, "CPU"s)) { auto cpu_pos = v_index(name_vec, "CPU"s); if (cpu_pos < name_vec.size() - 1 and not name_vec.at(cpu_pos + 1).ends_with(')')) name = name_vec.at(cpu_pos + 1); else name.clear(); } else if (v_contains(name_vec, "Ryzen"s)) { auto ryz_pos = v_index(name_vec, "Ryzen"s); name = "Ryzen" + (ryz_pos < name_vec.size() - 1 ? ' ' + name_vec.at(ryz_pos + 1) : "") + (ryz_pos < name_vec.size() - 2 ? ' ' + name_vec.at(ryz_pos + 2) : ""); } else if (s_contains(name, "Intel"s) and v_contains(name_vec, "CPU"s)) { auto cpu_pos = v_index(name_vec, "CPU"s); if (cpu_pos < name_vec.size() - 1 and not name_vec.at(cpu_pos + 1).ends_with(')') and name_vec.at(cpu_pos + 1) != "@") name = name_vec.at(cpu_pos + 1); else name.clear(); } else name.clear(); if (name.empty() and not name_vec.empty()) { for (const auto& n : name_vec) { if (n == "@") break; name += n + ' '; } name.pop_back(); for (const auto& replace : {"Processor", "CPU", "(R)", "(TM)", "Intel", "AMD", "Core"}) { name = s_replace(name, replace, ""); name = s_replace(name, " ", " "); } name = trim(name); } } return name; } bool get_sensors() { bool got_cpu = false, got_coretemp = false; vector search_paths; try { //? Setup up paths to search for sensors if (fs::exists(fs::path("/sys/class/hwmon")) and access("/sys/class/hwmon", R_OK) != -1) { for (const auto& dir : fs::directory_iterator(fs::path("/sys/class/hwmon"))) { fs::path add_path = fs::canonical(dir.path()); if (v_contains(search_paths, add_path) or v_contains(search_paths, add_path / "device")) continue; if (s_contains(add_path, "coretemp")) got_coretemp = true; for (const auto & file : fs::directory_iterator(add_path)) { if (string(file.path().filename()) == "device") { for (const auto & dev_file : fs::directory_iterator(file.path())) { string dev_filename = dev_file.path().filename(); if (dev_filename.starts_with("temp") and dev_filename.ends_with("_input")) { search_paths.push_back(file.path()); break; } } } string filename = file.path().filename(); if (filename.starts_with("temp") and filename.ends_with("_input")) { search_paths.push_back(add_path); break; } } } } if (not got_coretemp and fs::exists(fs::path("/sys/devices/platform/coretemp.0/hwmon"))) { for (auto& d : fs::directory_iterator(fs::path("/sys/devices/platform/coretemp.0/hwmon"))) { fs::path add_path = fs::canonical(d.path()); for (const auto & file : fs::directory_iterator(add_path)) { string filename = file.path().filename(); if (filename.starts_with("temp") and filename.ends_with("_input") and not v_contains(search_paths, add_path)) { search_paths.push_back(add_path); got_coretemp = true; break; } } } } //? Scan any found directories for temperature sensors if (not search_paths.empty()) { for (const auto& path : search_paths) { const string pname = readfile(path / "name", path.filename()); for (const auto & file : fs::directory_iterator(path)) { const string file_suffix = "input"; const int file_id = atoi(file.path().filename().c_str() + 4); // skip "temp" prefix string file_path = file.path(); if (!s_contains(file_path, file_suffix)) { continue; } const string basepath = file_path.erase(file_path.find(file_suffix), file_suffix.length()); const string label = readfile(fs::path(basepath + "label"), "temp" + to_string(file_id)); const string sensor_name = pname + "/" + label; const int64_t temp = stol(readfile(fs::path(basepath + "input"), "0")) / 1000; const int64_t high = stol(readfile(fs::path(basepath + "max"), "80000")) / 1000; const int64_t crit = stol(readfile(fs::path(basepath + "crit"), "95000")) / 1000; found_sensors[sensor_name] = {fs::path(basepath + "input"), label, temp, high, crit}; if (not got_cpu and (label.starts_with("Package id") or label.starts_with("Tdie"))) { got_cpu = true; cpu_sensor = sensor_name; } else if (label.starts_with("Core") or label.starts_with("Tccd")) { got_coretemp = true; if (not v_contains(core_sensors, sensor_name)) core_sensors.push_back(sensor_name); } } } } //? If no good candidate for cpu temp has been found scan /sys/class/thermal if (not got_cpu and fs::exists(fs::path("/sys/class/thermal"))) { const string rootpath = fs::path("/sys/class/thermal/thermal_zone"); for (int i = 0; fs::exists(fs::path(rootpath + to_string(i))); i++) { const fs::path basepath = rootpath + to_string(i); if (not fs::exists(basepath / "temp")) continue; const string label = readfile(basepath / "type", "temp" + to_string(i)); const string sensor_name = "thermal" + to_string(i) + "/" + label; const int64_t temp = stol(readfile(basepath / "temp", "0")) / 1000; int64_t high, crit; for (int ii = 0; fs::exists(basepath / string("trip_point_" + to_string(ii) + "_temp")); ii++) { const string trip_type = readfile(basepath / string("trip_point_" + to_string(ii) + "_type")); if (not is_in(trip_type, "high", "critical")) continue; auto& val = (trip_type == "high" ? high : crit); val = stol(readfile(basepath / string("trip_point_" + to_string(ii) + "_temp"), "0")) / 1000; } if (high < 1) high = 80; if (crit < 1) crit = 95; found_sensors[sensor_name] = {basepath / "temp", label, temp, high, crit}; } } } catch (...) {} if (not got_coretemp or core_sensors.empty()) { cpu_temp_only = true; } else { rng::sort(core_sensors, rng::less{}); rng::stable_sort(core_sensors, [](const auto& a, const auto& b){ return a.size() < b.size(); }); } if (cpu_sensor.empty() and not found_sensors.empty()) { for (const auto& [name, sensor] : found_sensors) { if (s_contains(str_to_lower(name), "cpu") or s_contains(str_to_lower(name), "k10temp")) { cpu_sensor = name; break; } } if (cpu_sensor.empty()) { cpu_sensor = found_sensors.begin()->first; Logger::warning("No good candidate for cpu sensor found, using random from all found sensors."); } } return not found_sensors.empty(); } void update_sensors() { if (cpu_sensor.empty()) return; const auto& cpu_sensor = (not Config::getS("cpu_sensor").empty() and found_sensors.contains(Config::getS("cpu_sensor")) ? Config::getS("cpu_sensor") : Cpu::cpu_sensor); found_sensors.at(cpu_sensor).temp = stol(readfile(found_sensors.at(cpu_sensor).path, "0")) / 1000; current_cpu.temp.at(0).push_back(found_sensors.at(cpu_sensor).temp); current_cpu.temp_max = found_sensors.at(cpu_sensor).crit; if (current_cpu.temp.at(0).size() > 20) current_cpu.temp.at(0).pop_front(); if (Config::getB("show_coretemp") and not cpu_temp_only) { vector done; for (const auto& sensor : core_sensors) { if (v_contains(done, sensor)) continue; found_sensors.at(sensor).temp = stol(readfile(found_sensors.at(sensor).path, "0")) / 1000; done.push_back(sensor); } for (const auto& [core, temp] : core_mapping) { if (cmp_less(core + 1, current_cpu.temp.size()) and cmp_less(temp, core_sensors.size())) { current_cpu.temp.at(core + 1).push_back(found_sensors.at(core_sensors.at(temp)).temp); if (current_cpu.temp.at(core + 1).size() > 20) current_cpu.temp.at(core + 1).pop_front(); } } } } string get_cpuHz() { static int failed = 0; if (failed > 4) return ""s; string cpuhz; try { double hz = 0.0; //? Try to get freq from /sys/devices/system/cpu/cpufreq/policy first (faster) if (not freq_path.empty()) { hz = stod(readfile(freq_path, "0.0")) / 1000; if (hz <= 0.0 and ++failed >= 2) freq_path.clear(); } //? If freq from /sys failed or is missing try to use /proc/cpuinfo if (hz <= 0.0) { ifstream cpufreq(Shared::procPath / "cpuinfo"); if (cpufreq.good()) { while (cpufreq.ignore(SSmax, '\n')) { if (cpufreq.peek() == 'c') { cpufreq.ignore(SSmax, ' '); if (cpufreq.peek() == 'M') { cpufreq.ignore(SSmax, ':'); cpufreq.ignore(1); cpufreq >> hz; break; } } } } } if (hz <= 1 or hz >= 1000000) throw std::runtime_error("Failed to read /sys/devices/system/cpu/cpufreq/policy and /proc/cpuinfo."); if (hz >= 1000) { if (hz >= 10000) cpuhz = to_string((int)round(hz / 1000)); // Future proof until we reach THz speeds :) else cpuhz = to_string(round(hz / 100) / 10.0).substr(0, 3); cpuhz += " GHz"; } else if (hz > 0) cpuhz = to_string((int)round(hz)) + " MHz"; } catch (const std::exception& e) { if (++failed < 5) return ""s; else { Logger::warning("get_cpuHZ() : " + (string)e.what()); return ""s; } } return cpuhz; } auto get_core_mapping() -> unordered_flat_map { unordered_flat_map core_map; if (cpu_temp_only) return core_map; //? Try to get core mapping from /proc/cpuinfo ifstream cpuinfo(Shared::procPath / "cpuinfo"); if (cpuinfo.good()) { int cpu, core, n = 0; for (string instr; cpuinfo >> instr;) { if (instr == "processor") { cpuinfo.ignore(SSmax, ':'); cpuinfo >> cpu; } else if (instr.starts_with("core")) { cpuinfo.ignore(SSmax, ':'); cpuinfo >> core; if (std::cmp_greater_equal(core, core_sensors.size())) { if (std::cmp_greater_equal(n, core_sensors.size())) n = 0; core_map[cpu] = n++; } else core_map[cpu] = core; } cpuinfo.ignore(SSmax, '\n'); } } //? If core mapping from cpuinfo was incomplete try to guess remainder, if missing completely, map 0-0 1-1 2-2 etc. if (cmp_less(core_map.size(), Shared::coreCount)) { if (Shared::coreCount % 2 == 0 and (long)core_map.size() == Shared::coreCount / 2) { for (int i = 0, n = 0; i < Shared::coreCount / 2; i++) { if (std::cmp_greater_equal(n, core_sensors.size())) n = 0; core_map[Shared::coreCount / 2 + i] = n++; } } else { core_map.clear(); for (int i = 0, n = 0; i < Shared::coreCount; i++) { if (std::cmp_greater_equal(n, core_sensors.size())) n = 0; core_map[i] = n++; } } } //? Apply user set custom mapping if any const auto& custom_map = Config::getS("cpu_core_map"); if (not custom_map.empty()) { try { for (const auto& split : ssplit(custom_map)) { const auto vals = ssplit(split, ':'); if (vals.size() != 2) continue; int change_id = std::stoi(vals.at(0)); int new_id = std::stoi(vals.at(1)); if (not core_map.contains(change_id) or cmp_greater(new_id, core_sensors.size())) continue; core_map.at(change_id) = new_id; } } catch (...) {} } return core_map; } struct battery { fs::path base_dir, energy_now, energy_full, power_now, status, online; string device_type; bool use_energy = true; }; auto get_battery() -> tuple { if (not has_battery) return {0, 0, ""}; static string auto_sel; static unordered_flat_map batteries; //? Get paths to needed files and check for valid values on first run if (batteries.empty() and has_battery) { if (fs::exists("/sys/class/power_supply")) { for (const auto& d : fs::directory_iterator("/sys/class/power_supply")) { //? Only consider online power supplies of type Battery or UPS //? see kernel docs for details on the file structure and contents //? https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-class-power battery new_bat; fs::path bat_dir; try { if (not d.is_directory() or not fs::exists(d.path() / "type") or not fs::exists(d.path() / "present") or stoi(readfile(d.path() / "present")) != 1) continue; string dev_type = readfile(d.path() / "type"); if (is_in(dev_type, "Battery", "UPS")) { bat_dir = d.path(); new_bat.base_dir = d.path(); new_bat.device_type = dev_type; } } catch (...) { //? skip power supplies not conforming to the kernel standard continue; } if (fs::exists(bat_dir / "energy_now")) new_bat.energy_now = bat_dir / "energy_now"; else if (fs::exists(bat_dir / "charge_now")) new_bat.energy_now = bat_dir / "charge_now"; else new_bat.use_energy = false; if (fs::exists(bat_dir / "energy_full")) new_bat.energy_full = bat_dir / "energy_full"; else if (fs::exists(bat_dir / "charge_full")) new_bat.energy_full = bat_dir / "charge_full"; else new_bat.use_energy = false; if (not new_bat.use_energy and not fs::exists(bat_dir / "capacity")) { continue; } if (fs::exists(bat_dir / "power_now")) new_bat.power_now = bat_dir / "power_now"; else if (fs::exists(bat_dir / "current_now")) new_bat.power_now = bat_dir / "current_now"; if (fs::exists(bat_dir / "AC0/online")) new_bat.online = bat_dir / "AC0/online"; else if (fs::exists(bat_dir / "AC/online")) new_bat.online = bat_dir / "AC/online"; batteries[bat_dir.filename()] = new_bat; Config::available_batteries.push_back(bat_dir.filename()); } } if (batteries.empty()) { has_battery = false; return {0, 0, ""}; } } auto& battery_sel = Config::getS("selected_battery"); if (auto_sel.empty()) { for (auto& [name, bat] : batteries) { if (bat.device_type == "Battery") { auto_sel = name; break; } } if (auto_sel.empty()) auto_sel = batteries.begin()->first; } auto& b = (battery_sel != "Auto" and batteries.contains(battery_sel) ? batteries.at(battery_sel) : batteries.at(auto_sel)); int percent = -1; long seconds = -1; //? Try to get battery percentage if (b.use_energy) { try { percent = round(100.0 * stoll(readfile(b.energy_now, "-1")) / stoll(readfile(b.energy_full, "1"))); } catch (const std::invalid_argument&) { } catch (const std::out_of_range&) { } } if (percent < 0) { try { percent = stoll(readfile(b.base_dir / "capacity", "-1")); } catch (const std::invalid_argument&) { } catch (const std::out_of_range&) { } } if (percent < 0) { has_battery = false; return {0, 0, ""}; } //? Get charging/discharging status string status = str_to_lower(readfile(b.base_dir / "status", "unknown")); if (status == "unknown" and not b.online.empty()) { const auto online = readfile(b.online, "0"); if (online == "1" and percent < 100) status = "charging"; else if (online == "1") status = "full"; else status = "discharging"; } //? Get seconds to empty if (not is_in(status, "charging", "full")) { if (b.use_energy and not b.power_now.empty()) { try { seconds = round((double)stoll(readfile(b.energy_now, "0")) / stoll(readfile(b.power_now, "1")) * 3600); } catch (const std::invalid_argument&) { } catch (const std::out_of_range&) { } } if (seconds < 0 and fs::exists(b.base_dir / "time_to_empty")) { try { seconds = stoll(readfile(b.base_dir / "time_to_empty", "0")) * 60; } catch (const std::invalid_argument&) { } catch (const std::out_of_range&) { } } } return {percent, seconds, status}; } auto collect(const bool no_update) -> cpu_info& { if (Runner::stopping or (no_update and not current_cpu.cpu_percent.at("total").empty())) return current_cpu; auto& cpu = current_cpu; ifstream cread; try { //? Get cpu load averages from /proc/loadavg cread.open(Shared::procPath / "loadavg"); if (cread.good()) { cread >> cpu.load_avg[0] >> cpu.load_avg[1] >> cpu.load_avg[2]; } cread.close(); //? Get cpu total times for all cores from /proc/stat cread.open(Shared::procPath / "stat"); for (int i = 0; cread.good() and cread.peek() == 'c'; i++) { cread.ignore(SSmax, ' '); //? Expected on kernel 2.6.3> : 0=user, 1=nice, 2=system, 3=idle, 4=iowait, 5=irq, 6=softirq, 7=steal, 8=guest, 9=guest_nice vector times; long long total_sum = 0; for (uint64_t val; cread >> val; total_sum += val) { times.push_back(val); } cread.clear(); if (times.size() < 4) throw std::runtime_error("Malformatted /proc/stat"); //? Subtract fields 8-9 and any future unknown fields const long long totals = max(0ll, total_sum - (times.size() > 8 ? std::accumulate(times.begin() + 8, times.end(), 0) : 0)); //? Add iowait field if present const long long idles = max(0ll, times.at(3) + (times.size() > 4 ? times.at(4) : 0)); //? Calculate values for totals from first line of stat if (i == 0) { const long long calc_totals = max(1ll, totals - cpu_old.at("totals")); const long long calc_idles = max(1ll, idles - cpu_old.at("idles")); cpu_old.at("totals") = totals; cpu_old.at("idles") = idles; //? Total usage of cpu cpu.cpu_percent.at("total").push_back(clamp((long long)round((double)(calc_totals - calc_idles) * 100 / calc_totals), 0ll, 100ll)); //? Reduce size if there are more values than needed for graph while (cmp_greater(cpu.cpu_percent.at("total").size(), width * 2)) cpu.cpu_percent.at("total").pop_front(); //? Populate cpu.cpu_percent with all fields from stat for (int ii = 0; const auto& val : times) { cpu.cpu_percent.at(time_names.at(ii)).push_back(clamp((long long)round((double)(val - cpu_old.at(time_names.at(ii))) * 100 / calc_totals), 0ll, 100ll)); cpu_old.at(time_names.at(ii)) = val; //? Reduce size if there are more values than needed for graph while (cmp_greater(cpu.cpu_percent.at(time_names.at(ii)).size(), width * 2)) cpu.cpu_percent.at(time_names.at(ii)).pop_front(); if (++ii == 10) break; } } //? Calculate cpu total for each core else { if (i > Shared::coreCount) break; const long long calc_totals = max(0ll, totals - core_old_totals.at(i-1)); const long long calc_idles = max(0ll, idles - core_old_idles.at(i-1)); core_old_totals.at(i-1) = totals; core_old_idles.at(i-1) = idles; cpu.core_percent.at(i-1).push_back(clamp((long long)round((double)(calc_totals - calc_idles) * 100 / calc_totals), 0ll, 100ll)); //? Reduce size if there are more values than needed for graph if (cpu.core_percent.at(i-1).size() > 40) cpu.core_percent.at(i-1).pop_front(); } } } catch (const std::exception& e) { Logger::debug("get_cpuHz() : " + (string)e.what()); if (cread.bad()) throw std::runtime_error("Failed to read /proc/stat"); else throw std::runtime_error("collect() : " + (string)e.what()); } if (Config::getB("show_cpu_freq")) cpuHz = get_cpuHz(); if (Config::getB("check_temp") and got_sensors) update_sensors(); if (Config::getB("show_battery") and has_battery) current_bat = get_battery(); return cpu; } } namespace Mem { bool has_swap = false; vector fstab; fs::file_time_type fstab_time; int disk_ios = 0; vector last_found; mem_info current_mem {}; uint64_t get_totalMem() { ifstream meminfo(Shared::procPath / "meminfo"); int64_t totalMem; if (meminfo.good()) { meminfo.ignore(SSmax, ':'); meminfo >> totalMem; totalMem <<= 10; } if (not meminfo.good() or totalMem == 0) throw std::runtime_error("Could not get total memory size from /proc/meminfo"); return totalMem; } auto collect(const bool no_update) -> mem_info& { if (Runner::stopping or (no_update and not current_mem.percent.at("used").empty())) return current_mem; auto& show_swap = Config::getB("show_swap"); auto& swap_disk = Config::getB("swap_disk"); auto& show_disks = Config::getB("show_disks"); auto totalMem = get_totalMem(); auto& mem = current_mem; mem.stats.at("swap_total") = 0; //? Read memory info from /proc/meminfo ifstream meminfo(Shared::procPath / "meminfo"); if (meminfo.good()) { bool got_avail = false; for (string label; meminfo.peek() != 'D' and meminfo >> label;) { if (label == "MemFree:") { meminfo >> mem.stats.at("free"); mem.stats.at("free") <<= 10; } else if (label == "MemAvailable:") { meminfo >> mem.stats.at("available"); mem.stats.at("available") <<= 10; got_avail = true; } else if (label == "Cached:") { meminfo >> mem.stats.at("cached"); mem.stats.at("cached") <<= 10; if (not show_swap and not swap_disk) break; } else if (label == "SwapTotal:") { meminfo >> mem.stats.at("swap_total"); mem.stats.at("swap_total") <<= 10; } else if (label == "SwapFree:") { meminfo >> mem.stats.at("swap_free"); mem.stats.at("swap_free") <<= 10; break; } meminfo.ignore(SSmax, '\n'); } if (not got_avail) mem.stats.at("available") = mem.stats.at("free") + mem.stats.at("cached"); mem.stats.at("used") = totalMem - mem.stats.at("available"); if (mem.stats.at("swap_total") > 0) mem.stats.at("swap_used") = mem.stats.at("swap_total") - mem.stats.at("swap_free"); } else throw std::runtime_error("Failed to read /proc/meminfo"); meminfo.close(); //? Calculate percentages for (const auto& name : mem_names) { mem.percent.at(name).push_back(round((double)mem.stats.at(name) * 100 / totalMem)); while (cmp_greater(mem.percent.at(name).size(), width * 2)) mem.percent.at(name).pop_front(); } if (show_swap and mem.stats.at("swap_total") > 0) { for (const auto& name : swap_names) { mem.percent.at(name).push_back(round((double)mem.stats.at(name) * 100 / mem.stats.at("swap_total"))); while (cmp_greater(mem.percent.at(name).size(), width * 2)) mem.percent.at(name).pop_front(); } has_swap = true; } else has_swap = false; //? Get disks stats if (show_disks) { double uptime = system_uptime(); auto free_priv = Config::getB("disk_free_priv"); try { auto& disks_filter = Config::getS("disks_filter"); bool filter_exclude = false; auto& use_fstab = Config::getB("use_fstab"); auto& only_physical = Config::getB("only_physical"); auto& disks = mem.disks; ifstream diskread; vector filter; if (not disks_filter.empty()) { filter = ssplit(disks_filter); if (filter.at(0).starts_with("exclude=")) { filter_exclude = true; filter.at(0) = filter.at(0).substr(8); } } //? Get list of "real" filesystems from /proc/filesystems vector fstypes; if (only_physical and not use_fstab) { fstypes = {"zfs", "wslfs", "drvfs"}; diskread.open(Shared::procPath / "filesystems"); if (diskread.good()) { for (string fstype; diskread >> fstype;) { if (not is_in(fstype, "nodev", "squashfs", "nullfs")) fstypes.push_back(fstype); diskread.ignore(SSmax, '\n'); } } else throw std::runtime_error("Failed to read /proc/filesystems"); diskread.close(); } //? Get disk list to use from fstab if enabled if (use_fstab and fs::last_write_time("/etc/fstab") != fstab_time) { fstab.clear(); fstab_time = fs::last_write_time("/etc/fstab"); diskread.open("/etc/fstab"); if (diskread.good()) { for (string instr; diskread >> instr;) { if (not instr.starts_with('#')) { diskread >> instr; #ifdef SNAPPED if (instr == "/") fstab.push_back("/mnt"); else if (not is_in(instr, "none", "swap")) fstab.push_back(instr); #else if (not is_in(instr, "none", "swap")) fstab.push_back(instr); #endif } diskread.ignore(SSmax, '\n'); } } else throw std::runtime_error("Failed to read /etc/fstab"); diskread.close(); } //? Get mounts from /etc/mtab or /proc/self/mounts diskread.open((fs::exists("/etc/mtab") ? fs::path("/etc/mtab") : Shared::procPath / "self/mounts")); if (diskread.good()) { vector found; found.reserve(last_found.size()); string dev, mountpoint, fstype; while (not diskread.eof()) { std::error_code ec; diskread >> dev >> mountpoint >> fstype; //? Match filter if not empty if (not filter.empty()) { bool match = v_contains(filter, mountpoint); if ((filter_exclude and match) or (not filter_exclude and not match)) continue; } if ((not use_fstab and not only_physical) or (use_fstab and v_contains(fstab, mountpoint)) or (not use_fstab and only_physical and v_contains(fstypes, fstype))) { found.push_back(mountpoint); if (not v_contains(last_found, mountpoint)) redraw = true; //? Save mountpoint, name, dev path and path to /sys/block stat file if (not disks.contains(mountpoint)) { disks[mountpoint] = disk_info{fs::canonical(dev, ec), fs::path(mountpoint).filename()}; if (disks.at(mountpoint).dev.empty()) disks.at(mountpoint).dev = dev; #ifdef SNAPPED if (mountpoint == "/mnt") disks.at(mountpoint).name = "root"; #endif if (disks.at(mountpoint).name.empty()) disks.at(mountpoint).name = (mountpoint == "/" ? "root" : mountpoint); string devname = disks.at(mountpoint).dev.filename(); while (devname.size() >= 2) { if (fs::exists("/sys/block/" + devname + "/stat", ec) and access(string("/sys/block/" + devname + "/stat").c_str(), R_OK) == 0) { disks.at(mountpoint).stat = "/sys/block/" + devname + "/stat"; break; } devname.resize(devname.size() - 1); } } } diskread.ignore(SSmax, '\n'); } //? Remove disks no longer mounted or filtered out if (swap_disk and has_swap) found.push_back("swap"); for (auto it = disks.begin(); it != disks.end();) { if (not v_contains(found, it->first)) it = disks.erase(it); else it++; } if (found.size() != last_found.size()) redraw = true; last_found = std::move(found); } else throw std::runtime_error("Failed to get mounts from /etc/mtab and /proc/self/mounts"); diskread.close(); //? Get disk/partition stats for (auto& [mountpoint, disk] : disks) { if (std::error_code ec; not fs::exists(mountpoint, ec)) continue; struct statvfs64 vfs; if (statvfs64(mountpoint.c_str(), &vfs) < 0) { Logger::warning("Failed to get disk/partition stats with statvfs() for: " + mountpoint); continue; } disk.total = vfs.f_blocks * vfs.f_frsize; disk.free = (free_priv ? vfs.f_bfree : vfs.f_bavail) * vfs.f_frsize; disk.used = disk.total - disk.free; disk.used_percent = round((double)disk.used * 100 / disk.total); disk.free_percent = 100 - disk.used_percent; } //? Setup disks order in UI and add swap if enabled mem.disks_order.clear(); #ifdef SNAPPED if (disks.contains("/mnt")) mem.disks_order.push_back("/mnt"); #else if (disks.contains("/")) mem.disks_order.push_back("/"); #endif if (swap_disk and has_swap) { mem.disks_order.push_back("swap"); if (not disks.contains("swap")) disks["swap"] = {"", "swap"}; disks.at("swap").total = mem.stats.at("swap_total"); disks.at("swap").used = mem.stats.at("swap_used"); disks.at("swap").free = mem.stats.at("swap_free"); disks.at("swap").used_percent = mem.percent.at("swap_used").back(); disks.at("swap").free_percent = mem.percent.at("swap_free").back(); } for (const auto& name : last_found) #ifdef SNAPPED if (not is_in(name, "/mnt", "swap")) mem.disks_order.push_back(name); #else if (not is_in(name, "/", "swap")) mem.disks_order.push_back(name); #endif //? Get disks IO int64_t sectors_read, sectors_write, io_ticks; disk_ios = 0; for (auto& [ignored, disk] : disks) { if (disk.stat.empty() or access(disk.stat.c_str(), R_OK) != 0) continue; diskread.open(disk.stat); if (diskread.good()) { disk_ios++; for (int i = 0; i < 2; i++) { diskread >> std::ws; diskread.ignore(SSmax, ' '); } diskread >> sectors_read; if (disk.io_read.empty()) disk.io_read.push_back(0); else disk.io_read.push_back(max((int64_t)0, (sectors_read - disk.old_io.at(0)) * 512)); disk.old_io.at(0) = sectors_read; while (cmp_greater(disk.io_read.size(), width * 2)) disk.io_read.pop_front(); for (int i = 0; i < 3; i++) { diskread >> std::ws; diskread.ignore(SSmax, ' '); } diskread >> sectors_write; if (disk.io_write.empty()) disk.io_write.push_back(0); else disk.io_write.push_back(max((int64_t)0, (sectors_write - disk.old_io.at(1)) * 512)); disk.old_io.at(1) = sectors_write; while (cmp_greater(disk.io_write.size(), width * 2)) disk.io_write.pop_front(); for (int i = 0; i < 2; i++) { diskread >> std::ws; diskread.ignore(SSmax, ' '); } diskread >> io_ticks; if (disk.io_activity.empty()) disk.io_activity.push_back(0); else disk.io_activity.push_back(clamp((long)round((double)(io_ticks - disk.old_io.at(2)) / (uptime - old_uptime) / 10), 0l, 100l)); disk.old_io.at(2) = io_ticks; while (cmp_greater(disk.io_activity.size(), width * 2)) disk.io_activity.pop_front(); } diskread.close(); } old_uptime = uptime; } catch (const std::exception& e) { Logger::warning("Error in Mem::collect() : " + (string)e.what()); } } return mem; } } namespace Net { unordered_flat_map current_net; net_info empty_net = {}; vector interfaces; string selected_iface; int errors = 0; unordered_flat_map graph_max = { {"download", {}}, {"upload", {}} }; unordered_flat_map> max_count = { {"download", {}}, {"upload", {}} }; bool rescale = true; uint64_t timestamp = 0; //* RAII wrapper for getifaddrs class getifaddr_wrapper { struct ifaddrs* ifaddr; public: int status; getifaddr_wrapper() { status = getifaddrs(&ifaddr); } ~getifaddr_wrapper() { freeifaddrs(ifaddr); } auto operator()() -> struct ifaddrs* { return ifaddr; } }; auto collect(const bool no_update) -> net_info& { auto& net = current_net; auto& config_iface = Config::getS("net_iface"); auto& net_sync = Config::getB("net_sync"); auto& net_auto = Config::getB("net_auto"); auto new_timestamp = time_ms(); if (not no_update and errors < 3) { //? Get interface list using getifaddrs() wrapper getifaddr_wrapper if_wrap {}; if (if_wrap.status != 0) { errors++; Logger::error("Net::collect() -> getifaddrs() failed with id " + to_string(if_wrap.status)); redraw = true; return empty_net; } int family = 0; char ip[NI_MAXHOST]; interfaces.clear(); string ipv4, ipv6; //? Iteration over all items in getifaddrs() list for (auto* ifa = if_wrap(); ifa != NULL; ifa = ifa->ifa_next) { if (ifa->ifa_addr == NULL) continue; family = ifa->ifa_addr->sa_family; const auto& iface = ifa->ifa_name; //? Get IPv4 address if (family == AF_INET) { if (getnameinfo(ifa->ifa_addr, sizeof(struct sockaddr_in), ip, NI_MAXHOST, NULL, 0, NI_NUMERICHOST) == 0) net[iface].ipv4 = ip; } //? Get IPv6 address else if (family == AF_INET6) { if (getnameinfo(ifa->ifa_addr, sizeof(struct sockaddr_in6), ip, NI_MAXHOST, NULL, 0, NI_NUMERICHOST) == 0) net[iface].ipv6 = ip; } //? Update available interfaces vector and get status of interface if (not v_contains(interfaces, iface)) { interfaces.push_back(iface); net[iface].connected = (ifa->ifa_flags & IFF_RUNNING); } } //? Get total recieved and transmitted bytes + device address if no ip was found for (const auto& iface : interfaces) { if (net.at(iface).ipv4.empty() and net.at(iface).ipv6.empty()) net.at(iface).ipv4 = readfile("/sys/class/net/" + iface + "/address"); for (const string dir : {"download", "upload"}) { const fs::path sys_file = "/sys/class/net/" + iface + "/statistics/" + (dir == "download" ? "rx_bytes" : "tx_bytes"); auto& saved_stat = net.at(iface).stat.at(dir); auto& bandwidth = net.at(iface).bandwidth.at(dir); uint64_t val = 0; try { val = (uint64_t)stoull(readfile(sys_file, "0")); } catch (const std::invalid_argument&) {} catch (const std::out_of_range&) {} //? Update speed, total and top values if (val < saved_stat.last) { saved_stat.rollover += saved_stat.last; saved_stat.last = 0; } if (cmp_greater((unsigned long long)saved_stat.rollover + (unsigned long long)val, numeric_limits::max())) { saved_stat.rollover = 0; saved_stat.last = 0; } saved_stat.speed = round((double)(val - saved_stat.last) / ((double)(new_timestamp - timestamp) / 1000)); if (saved_stat.speed > saved_stat.top) saved_stat.top = saved_stat.speed; if (saved_stat.offset > val + saved_stat.rollover) saved_stat.offset = 0; saved_stat.total = (val + saved_stat.rollover) - saved_stat.offset; saved_stat.last = val; //? Add values to graph bandwidth.push_back(saved_stat.speed); while (cmp_greater(bandwidth.size(), width * 2)) bandwidth.pop_front(); //? Set counters for auto scaling if (net_auto and selected_iface == iface) { if (saved_stat.speed > graph_max[dir]) { ++max_count[dir][0]; if (max_count[dir][1] > 0) --max_count[dir][1]; } else if (graph_max[dir] > 10 << 10 and saved_stat.speed < graph_max[dir] / 10) { ++max_count[dir][1]; if (max_count[dir][0] > 0) --max_count[dir][0]; } } } } //? Clean up net map if needed if (net.size() > interfaces.size()) { for (auto it = net.begin(); it != net.end();) { if (not v_contains(interfaces, it->first)) it = net.erase(it); else it++; } net.compact(); } timestamp = new_timestamp; } //? Return empty net_info struct if no interfaces was found if (net.empty()) return empty_net; //? Find an interface to display if selected isn't set or valid if (selected_iface.empty() or not v_contains(interfaces, selected_iface)) { max_count["download"][0] = max_count["download"][1] = max_count["upload"][0] = max_count["upload"][1] = 0; redraw = true; if (net_auto) rescale = true; if (not config_iface.empty() and v_contains(interfaces, config_iface)) selected_iface = config_iface; else { //? Sort interfaces by total upload + download bytes auto sorted_interfaces = interfaces; rng::sort(sorted_interfaces, [&](const auto& a, const auto& b){ return cmp_greater(net.at(a).stat["download"].total + net.at(a).stat["upload"].total, net.at(b).stat["download"].total + net.at(b).stat["upload"].total); }); selected_iface.clear(); //? Try to set to a connected interface for (const auto& iface : sorted_interfaces) { if (net.at(iface).connected) selected_iface = iface; break; } //? If no interface is connected set to first available if (selected_iface.empty() and not sorted_interfaces.empty()) selected_iface = sorted_interfaces.at(0); else if (sorted_interfaces.empty()) return empty_net; } } //? Calculate max scale for graphs if needed if (net_auto) { bool sync = false; for (const auto& dir: {"download", "upload"}) { for (const auto& sel : {0, 1}) { if (rescale or max_count[dir][sel] >= 5) { const uint64_t avg_speed = (net[selected_iface].bandwidth[dir].size() > 5 ? std::accumulate(net.at(selected_iface).bandwidth.at(dir).rbegin(), net.at(selected_iface).bandwidth.at(dir).rbegin() + 5, 0) / 5 : net[selected_iface].stat[dir].speed); graph_max[dir] = max(uint64_t(avg_speed * (sel == 0 ? 1.3 : 3.0)), (uint64_t)10 << 10); max_count[dir][0] = max_count[dir][1] = 0; redraw = true; if (net_sync) sync = true; break; } } //? Sync download/upload graphs if enabled if (sync) { const auto other = (string(dir) == "upload" ? "download" : "upload"); graph_max[other] = graph_max[dir]; max_count[other][0] = max_count[other][1] = 0; break; } } } rescale = false; return net.at(selected_iface); } } namespace Proc { vector current_procs; unordered_flat_map uid_user; string current_sort; string current_filter; bool current_rev = false; fs::file_time_type passwd_time; uint64_t cputimes; int collapse = -1, expand = -1; uint64_t old_cputimes = 0; atomic numpids = 0; int filter_found = 0; detail_container detailed; //* Generate process tree list void _tree_gen(proc_info& cur_proc, vector& in_procs, vector>& out_procs, int cur_depth, const bool collapsed, const string& filter, bool found=false, const bool no_update=false, const bool should_filter=false) { auto cur_pos = out_procs.size(); bool filtering = false; //? If filtering, include children of matching processes if (not found and (should_filter or not filter.empty())) { if (not s_contains(std::to_string(cur_proc.pid), filter) and not s_contains(cur_proc.name, filter) and not s_contains(cur_proc.cmd, filter) and not s_contains(cur_proc.user, filter)) { filtering = true; cur_proc.filtered = true; filter_found++; } else { found = true; cur_depth = 0; } } else if (cur_proc.filtered) cur_proc.filtered = false; //? Set tree index position for process if not filtered out or currently in a collapsed sub-tree if (not collapsed and not filtering) { out_procs.push_back(std::ref(cur_proc)); cur_proc.tree_index = out_procs.size() - 1; //? Try to find name of the binary file and append to program name if not the same if (cur_proc.short_cmd.empty() and not cur_proc.cmd.empty()) { std::string_view cmd_view = cur_proc.cmd; cmd_view = cmd_view.substr((size_t)0, min(cmd_view.find(' '), cmd_view.size())); cmd_view = cmd_view.substr(min(cmd_view.find_last_of('/') + 1, cmd_view.size())); cur_proc.short_cmd = (string)cmd_view; } } else { cur_proc.tree_index = in_procs.size(); } //? Recursive iteration over all children int children = 0; for (auto& p : rng::equal_range(in_procs, cur_proc.pid, rng::less{}, &proc_info::ppid)) { if (not no_update and not filtering and (collapsed or cur_proc.collapsed)) { out_procs.back().get().cpu_p += p.cpu_p; out_procs.back().get().mem += p.mem; out_procs.back().get().threads += p.threads; filter_found++; } if (collapsed and not filtering) { cur_proc.filtered = true; } else children++; _tree_gen(p, in_procs, out_procs, cur_depth + 1, (collapsed ? true : cur_proc.collapsed), filter, found, no_update, should_filter); } if (collapsed or filtering) return; //? Add tree terminator symbol if it's the last child in a sub-tree if (out_procs.size() > cur_pos + 1 and not out_procs.back().get().prefix.ends_with("]─")) out_procs.back().get().prefix.replace(out_procs.back().get().prefix.size() - 8, 8, " └─ "); //? Add collapse/expand symbols if process have any children out_procs.at(cur_pos).get().prefix = " │ "s * cur_depth + (children > 0 ? (cur_proc.collapsed ? "[+]─" : "[-]─") : " ├─ "); } //* Get detailed info for selected process void _collect_details(const size_t pid, const uint64_t uptime, vector& procs) { fs::path pid_path = Shared::procPath / std::to_string(pid); if (pid != detailed.last_pid) { detailed = {}; detailed.last_pid = pid; detailed.skip_smaps = not Config::getB("proc_info_smaps"); } //? Copy proc_info for process from proc vector auto p_info = rng::find(procs, pid, &proc_info::pid); detailed.entry = *p_info; //? Update cpu percent deque for process cpu graph if (not Config::getB("proc_per_core")) detailed.entry.cpu_p *= Shared::coreCount; detailed.cpu_percent.push_back(clamp((long long)round(detailed.entry.cpu_p), 0ll, 100ll)); while (cmp_greater(detailed.cpu_percent.size(), width)) detailed.cpu_percent.pop_front(); //? Process runtime detailed.elapsed = sec_to_dhms(uptime - (detailed.entry.cpu_s / Shared::clkTck)); if (detailed.elapsed.size() > 8) detailed.elapsed.resize(detailed.elapsed.size() - 3); //? Get parent process name if (detailed.parent.empty()) { auto p_entry = rng::find(procs, detailed.entry.ppid, &proc_info::pid); if (p_entry != procs.end()) detailed.parent = p_entry->name; } //? Expand process status from single char to explanative string detailed.status = (proc_states.contains(detailed.entry.state)) ? proc_states.at(detailed.entry.state) : "Unknown"; ifstream d_read; string short_str; //? Try to get RSS mem from proc/[pid]/smaps detailed.memory.clear(); if (not detailed.skip_smaps and fs::exists(pid_path / "smaps")) { d_read.open(pid_path / "smaps"); uint64_t rss = 0; try { while (d_read.good()) { d_read.ignore(SSmax, 'R'); if (d_read.peek() == 's') { d_read.ignore(SSmax, ':'); getline(d_read, short_str, 'k'); rss += stoull(short_str); } } if (rss == detailed.entry.mem >> 10) detailed.skip_smaps = true; else { detailed.mem_bytes.push_back(rss << 10); detailed.memory = floating_humanizer(rss, false, 1); } } catch (const std::invalid_argument&) {} catch (const std::out_of_range&) {} d_read.close(); } if (detailed.memory.empty()) { detailed.mem_bytes.push_back(detailed.entry.mem); detailed.memory = floating_humanizer(detailed.entry.mem); } if (detailed.first_mem == -1 or detailed.first_mem < detailed.mem_bytes.back() / 2 or detailed.first_mem > detailed.mem_bytes.back() * 4) { detailed.first_mem = min((uint64_t)detailed.mem_bytes.back() * 2, Mem::get_totalMem()); redraw = true; } while (cmp_greater(detailed.mem_bytes.size(), width)) detailed.mem_bytes.pop_front(); //? Get bytes read and written from proc/[pid]/io if (fs::exists(pid_path / "io")) { d_read.open(pid_path / "io"); try { string name; while (d_read.good()) { getline(d_read, name, ':'); if (name.ends_with("read_bytes")) { getline(d_read, short_str); detailed.io_read = floating_humanizer(stoull(short_str)); } else if (name.ends_with("write_bytes")) { getline(d_read, short_str); detailed.io_write = floating_humanizer(stoull(short_str)); break; } else d_read.ignore(SSmax, '\n'); } } catch (const std::invalid_argument&) {} catch (const std::out_of_range&) {} d_read.close(); } } //* Collects and sorts process information from /proc auto collect(const bool no_update) -> vector& { const auto& sorting = Config::getS("proc_sorting"); const auto& reverse = Config::getB("proc_reversed"); const auto& filter = Config::getS("proc_filter"); const auto& per_core = Config::getB("proc_per_core"); const auto& tree = Config::getB("proc_tree"); const auto& show_detailed = Config::getB("show_detailed"); const size_t detailed_pid = Config::getI("detailed_pid"); bool should_filter = current_filter != filter; if (should_filter) current_filter = filter; const bool sorted_change = (sorting != current_sort or reverse != current_rev or should_filter); if (sorted_change) { current_sort = sorting; current_rev = reverse; } ifstream pread; string long_string; string short_str; const double uptime = system_uptime(); const int cmult = (per_core) ? Shared::coreCount : 1; bool got_detailed = false; //* Use pids from last update if only changing filter, sorting or tree options if (no_update and not current_procs.empty()) { if (show_detailed and detailed_pid != detailed.last_pid) _collect_details(detailed_pid, round(uptime), current_procs); } //* ---------------------------------------------Collection start---------------------------------------------- else { should_filter = true; auto totalMem = Mem::get_totalMem(); int totalMem_len = to_string(totalMem >> 10).size(); //? Update uid_user map if /etc/passwd changed since last run if (not Shared::passwd_path.empty() and fs::last_write_time(Shared::passwd_path) != passwd_time) { string r_uid, r_user; passwd_time = fs::last_write_time(Shared::passwd_path); uid_user.clear(); pread.open(Shared::passwd_path); if (pread.good()) { while (pread.good()) { getline(pread, r_user, ':'); pread.ignore(SSmax, ':'); getline(pread, r_uid, ':'); uid_user[r_uid] = r_user; pread.ignore(SSmax, '\n'); } } else { Shared::passwd_path.clear(); } pread.close(); } //? Get cpu total times from /proc/stat cputimes = 0; pread.open(Shared::procPath / "stat"); if (pread.good()) { pread.ignore(SSmax, ' '); for (uint64_t times; pread >> times; cputimes += times); } else throw std::runtime_error("Failure to read /proc/stat"); pread.close(); //? Iterate over all pids in /proc vector found; for (const auto& d: fs::directory_iterator(Shared::procPath)) { if (Runner::stopping) return current_procs; if (pread.is_open()) pread.close(); const string pid_str = d.path().filename(); if (not isdigit(pid_str[0])) continue; const size_t pid = stoul(pid_str); found.push_back(pid); //? Check if pid already exists in current_procs auto find_old = rng::find(current_procs, pid, &proc_info::pid); bool no_cache = false; if (find_old == current_procs.end()) { current_procs.push_back({pid}); find_old = current_procs.end() - 1; no_cache = true; } auto& new_proc = *find_old; //? Get program name, command and username if (no_cache) { pread.open(d.path() / "comm"); if (not pread.good()) continue; getline(pread, new_proc.name); pread.close(); //? Check for whitespace characters in name and set offset to get correct fields from stat file new_proc.name_offset = rng::count(new_proc.name, ' '); pread.open(d.path() / "cmdline"); if (not pread.good()) continue; long_string.clear(); while(getline(pread, long_string, '\0')) { new_proc.cmd += long_string + ' '; if (new_proc.cmd.size() > 1000) { new_proc.cmd.resize(1000); break; } } pread.close(); if (not new_proc.cmd.empty()) new_proc.cmd.pop_back(); pread.open(d.path() / "status"); if (not pread.good()) continue; string uid; string line; while (pread.good()) { getline(pread, line, ':'); if (line == "Uid") { pread.ignore(); getline(pread, uid, '\t'); break; } else { pread.ignore(SSmax, '\n'); } } pread.close(); if (uid_user.contains(uid)) { new_proc.user = uid_user.at(uid); } else { #if !(defined(STATIC_BUILD) && defined(__GLIBC__)) try { struct passwd* udet; udet = getpwuid(stoi(uid)); if (udet != NULL and udet->pw_name != NULL) { new_proc.user = string(udet->pw_name); } else { new_proc.user = uid; } } catch (...) { new_proc.user = uid; } #else new_proc.user = uid; #endif } } //? Parse /proc/[pid]/stat pread.open(d.path() / "stat"); if (not pread.good()) continue; const auto& offset = new_proc.name_offset; short_str.clear(); int x = 0, next_x = 3; uint64_t cpu_t = 0; try { for (;;) { while (pread.good() and ++x < next_x + offset) pread.ignore(SSmax, ' '); if (not pread.good()) break; else getline(pread, short_str, ' '); switch (x-offset) { case 3: //? Process state new_proc.state = short_str.at(0); if (new_proc.ppid != 0) next_x = 14; continue; case 4: //? Parent pid new_proc.ppid = stoull(short_str); next_x = 14; continue; case 14: //? Process utime cpu_t = stoull(short_str); continue; case 15: //? Process stime cpu_t += stoull(short_str); next_x = 19; continue; case 19: //? Nice value new_proc.p_nice = stoull(short_str); continue; case 20: //? Number of threads new_proc.threads = stoull(short_str); if (new_proc.cpu_s == 0) { next_x = 22; new_proc.cpu_t = cpu_t; } else next_x = 24; continue; case 22: //? Get cpu seconds if missing new_proc.cpu_s = stoull(short_str); next_x = 24; continue; case 24: //? RSS memory (can be inaccurate, but parsing smaps increases total cpu usage by ~20x) if (cmp_greater(short_str.size(), totalMem_len)) new_proc.mem = totalMem; else new_proc.mem = stoull(short_str) * Shared::pageSize; } break; } } catch (const std::invalid_argument&) { continue; } catch (const std::out_of_range&) { continue; } pread.close(); if (x-offset < 24) continue; //? Get RSS memory from /proc/[pid]/statm if value from /proc/[pid]/stat looks wrong if (new_proc.mem >= totalMem) { pread.open(d.path() / "statm"); if (not pread.good()) continue; pread.ignore(SSmax, ' '); pread >> new_proc.mem; new_proc.mem *= Shared::pageSize; pread.close(); } //? Process cpu usage since last update new_proc.cpu_p = clamp(round(cmult * 1000 * (cpu_t - new_proc.cpu_t) / max((uint64_t)1, cputimes - old_cputimes)) / 10.0, 0.0, 100.0 * Shared::coreCount); //? Process cumulative cpu usage since process start new_proc.cpu_c = (double)cpu_t / max(1.0, (uptime * Shared::clkTck) - new_proc.cpu_s); //? Update cached value with latest cpu times new_proc.cpu_t = cpu_t; if (show_detailed and not got_detailed and new_proc.pid == detailed_pid) { got_detailed = true; } } //? Clear dead processes from current_procs auto eraser = rng::remove_if(current_procs, [&](const auto& element){ return not v_contains(found, element.pid); }); current_procs.erase(eraser.begin(), eraser.end()); //? Update the details info box for process if active if (show_detailed and got_detailed) { _collect_details(detailed_pid, round(uptime), current_procs); } else if (show_detailed and not got_detailed and detailed.status != "Dead") { detailed.status = "Dead"; redraw = true; } old_cputimes = cputimes; } //* ---------------------------------------------Collection done----------------------------------------------- //* Sort processes if (sorted_change or not no_update) { if (reverse) { switch (v_index(sort_vector, sorting)) { case 0: rng::stable_sort(current_procs, rng::less{}, &proc_info::pid); break; case 1: rng::stable_sort(current_procs, rng::less{}, &proc_info::name); break; case 2: rng::stable_sort(current_procs, rng::less{}, &proc_info::cmd); break; case 3: rng::stable_sort(current_procs, rng::less{}, &proc_info::threads); break; case 4: rng::stable_sort(current_procs, rng::less{}, &proc_info::user); break; case 5: rng::stable_sort(current_procs, rng::less{}, &proc_info::mem); break; case 6: rng::stable_sort(current_procs, rng::less{}, &proc_info::cpu_p); break; case 7: rng::stable_sort(current_procs, rng::less{}, &proc_info::cpu_c); break; } } else { switch (v_index(sort_vector, sorting)) { case 0: rng::stable_sort(current_procs, rng::greater{}, &proc_info::pid); break; case 1: rng::stable_sort(current_procs, rng::greater{}, &proc_info::name); break; case 2: rng::stable_sort(current_procs, rng::greater{}, &proc_info::cmd); break; case 3: rng::stable_sort(current_procs, rng::greater{}, &proc_info::threads); break; case 4: rng::stable_sort(current_procs, rng::greater{}, &proc_info::user); break; case 5: rng::stable_sort(current_procs, rng::greater{}, &proc_info::mem); break; case 6: rng::stable_sort(current_procs, rng::greater{}, &proc_info::cpu_p); break; case 7: rng::stable_sort(current_procs, rng::greater{}, &proc_info::cpu_c); break; } } //* When sorting with "cpu lazy" push processes over threshold cpu usage to the front regardless of cumulative usage if (not tree and not reverse and sorting == "cpu lazy") { double max = 10.0, target = 30.0; for (size_t i = 0, x = 0, offset = 0; i < current_procs.size(); i++) { if (i <= 5 and current_procs.at(i).cpu_p > max) max = current_procs.at(i).cpu_p; else if (i == 6) target = (max > 30.0) ? max : 10.0; if (i == offset and current_procs.at(i).cpu_p > 30.0) offset++; else if (current_procs.at(i).cpu_p > target) { rotate(current_procs.begin() + offset, current_procs.begin() + i, current_procs.begin() + i + 1); if (++x > 10) break; } } } } //* Match filter if defined if (should_filter) { filter_found = 0; for (auto& p : current_procs) { if (not tree and not filter.empty()) { if (not s_contains(to_string(p.pid), filter) and not s_contains(p.name, filter) and not s_contains(p.cmd, filter) and not s_contains(p.user, filter)) { p.filtered = true; filter_found++; } else { p.filtered = false; } } else { p.filtered = false; } } } //* Generate tree view if enabled if (tree and (not no_update or should_filter or sorted_change)) { if (auto find_pid = (collapse != -1 ? collapse : expand); find_pid != -1) { auto collapser = rng::find(current_procs, find_pid, &proc_info::pid); if (collapser != current_procs.end()) { if (collapse == expand) { collapser->collapsed = not collapser->collapsed; } else if (collapse > -1) { collapser->collapsed = true; } else if (expand > -1) { collapser->collapsed = false; } } collapse = expand = -1; } if (should_filter or not filter.empty()) filter_found = 0; vector> tree_procs; tree_procs.reserve(current_procs.size()); //? Stable sort to retain selected sorting among processes with the same parent rng::stable_sort(current_procs, rng::less{}, &proc_info::ppid); //? Start recursive iteration over processes with the lowest shared parent pids for (auto& p : rng::equal_range(current_procs, current_procs.at(0).ppid, rng::less{}, &proc_info::ppid)) { _tree_gen(p, current_procs, tree_procs, 0, false, filter, false, no_update, should_filter); } //? Final sort based on tree index rng::stable_sort(current_procs, rng::less{}, &proc_info::tree_index); } numpids = (int)current_procs.size() - filter_found; return current_procs; } } namespace Tools { double system_uptime() { string upstr; ifstream pread(Shared::procPath / "uptime"); if (pread.good()) { try { getline(pread, upstr, ' '); pread.close(); return stod(upstr); } catch (const std::invalid_argument&) {} catch (const std::out_of_range&) {} } throw std::runtime_error("Failed get uptime from from " + (string)Shared::procPath + "/uptime"); } } btop-1.2.3/src/osx/000077500000000000000000000000001420276253000140625ustar00rootroot00000000000000btop-1.2.3/src/osx/btop_collect.cpp000066400000000000000000001436471420276253000172560ustar00rootroot00000000000000/* 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. indent = tab tab-size = 4 */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "sensors.hpp" #include "smc.hpp" using std::clamp, std::string_literals::operator""s, std::cmp_equal, std::cmp_less, std::cmp_greater; using std::ifstream, std::numeric_limits, std::streamsize, std::round, std::max, std::min; namespace fs = std::filesystem; namespace rng = std::ranges; using namespace Tools; //? --------------------------------------------------- FUNCTIONS ----------------------------------------------------- namespace Cpu { vector core_old_totals; vector core_old_idles; vector available_fields = {"total"}; vector available_sensors = {"Auto"}; cpu_info current_cpu; fs::path freq_path = "/sys/devices/system/cpu/cpufreq/policy0/scaling_cur_freq"; bool got_sensors = false, cpu_temp_only = false; int core_offset = 0; //* Populate found_sensors map bool get_sensors(); //* Get current cpu clock speed string get_cpuHz(); //* Search /proc/cpuinfo for a cpu name string get_cpuName(); struct Sensor { fs::path path; string label; int64_t temp = 0; int64_t high = 0; int64_t crit = 0; }; string cpu_sensor; vector core_sensors; unordered_flat_map core_mapping; } // namespace Cpu namespace Mem { double old_uptime; } class MachProcessorInfo { public: processor_info_array_t info_array; mach_msg_type_number_t info_count; MachProcessorInfo() {} virtual ~MachProcessorInfo() {vm_deallocate(mach_task_self(), (vm_address_t)info_array, (vm_size_t)sizeof(processor_info_array_t) * info_count);} }; namespace Shared { fs::path passwd_path; uint64_t totalMem; long pageSize, coreCount, clkTck, physicalCoreCount, arg_max; double machTck; int totalMem_len; void init() { //? Shared global variables init coreCount = sysconf(_SC_NPROCESSORS_ONLN); // this returns all logical cores (threads) if (coreCount < 1) { coreCount = 1; Logger::warning("Could not determine number of cores, defaulting to 1."); } size_t physicalCoreCountSize = sizeof(physicalCoreCount); if (sysctlbyname("hw.physicalcpu", &physicalCoreCount, &physicalCoreCountSize, NULL, 0) < 0) { Logger::error("Could not get physical core count"); } pageSize = sysconf(_SC_PAGE_SIZE); if (pageSize <= 0) { pageSize = 4096; Logger::warning("Could not get system page size. Defaulting to 4096, processes memory usage might be incorrect."); } mach_timebase_info_data_t convf; if (mach_timebase_info(&convf) == KERN_SUCCESS) { machTck = convf.numer / convf.denom; } else { Logger::warning("Could not get mach clock tick conversion factor. Defaulting to 100, processes cpu usage might be incorrect."); machTck = 100; } clkTck = sysconf(_SC_CLK_TCK); if (clkTck <= 0) { clkTck = 100; Logger::warning("Could not get system clock ticks per second. Defaulting to 100, processes cpu usage might be incorrect."); } int64_t memsize = 0; size_t size = sizeof(memsize); if (sysctlbyname("hw.memsize", &memsize, &size, NULL, 0) < 0) { Logger::warning("Could not get memory size"); } totalMem = memsize; //* Get maximum length of process arguments arg_max = sysconf(_SC_ARG_MAX); //? Init for namespace Cpu if (not fs::exists(Cpu::freq_path) or access(Cpu::freq_path.c_str(), R_OK) == -1) Cpu::freq_path.clear(); Cpu::current_cpu.core_percent.insert(Cpu::current_cpu.core_percent.begin(), Shared::coreCount, {}); Cpu::current_cpu.temp.insert(Cpu::current_cpu.temp.begin(), Shared::coreCount + 1, {}); Cpu::core_old_totals.insert(Cpu::core_old_totals.begin(), Shared::coreCount, 0); Cpu::core_old_idles.insert(Cpu::core_old_idles.begin(), Shared::coreCount, 0); Cpu::collect(); for (auto &[field, vec] : Cpu::current_cpu.cpu_percent) { if (not vec.empty() and not v_contains(Cpu::available_fields, field)) Cpu::available_fields.push_back(field); } Cpu::cpuName = Cpu::get_cpuName(); Cpu::got_sensors = Cpu::get_sensors(); Cpu::core_mapping = Cpu::get_core_mapping(); //? Init for namespace Mem Mem::old_uptime = system_uptime(); Mem::collect(); } } // namespace Shared namespace Cpu { string cpuName; string cpuHz; bool has_battery = true; bool macM1 = false; tuple current_bat; const array time_names = {"user", "nice", "system", "idle"}; unordered_flat_map cpu_old = { {"totals", 0}, {"idles", 0}, {"user", 0}, {"nice", 0}, {"system", 0}, {"idle", 0} }; string get_cpuName() { string name; char buffer[1024]; size_t size = sizeof(buffer); if (sysctlbyname("machdep.cpu.brand_string", &buffer, &size, NULL, 0) < 0) { Logger::error("Failed to get CPU name"); return name; } name = string(buffer); auto name_vec = ssplit(name); if ((s_contains(name, "Xeon"s) or v_contains(name_vec, "Duo"s)) and v_contains(name_vec, "CPU"s)) { auto cpu_pos = v_index(name_vec, "CPU"s); if (cpu_pos < name_vec.size() - 1 and not name_vec.at(cpu_pos + 1).ends_with(')')) name = name_vec.at(cpu_pos + 1); else name.clear(); } else if (v_contains(name_vec, "Ryzen"s)) { auto ryz_pos = v_index(name_vec, "Ryzen"s); name = "Ryzen" + (ryz_pos < name_vec.size() - 1 ? ' ' + name_vec.at(ryz_pos + 1) : "") + (ryz_pos < name_vec.size() - 2 ? ' ' + name_vec.at(ryz_pos + 2) : ""); } else if (s_contains(name, "Intel"s) and v_contains(name_vec, "CPU"s)) { auto cpu_pos = v_index(name_vec, "CPU"s); if (cpu_pos < name_vec.size() - 1 and not name_vec.at(cpu_pos + 1).ends_with(')') and name_vec.at(cpu_pos + 1) != "@") name = name_vec.at(cpu_pos + 1); else name.clear(); } else name.clear(); if (name.empty() and not name_vec.empty()) { for (const auto &n : name_vec) { if (n == "@") break; name += n + ' '; } name.pop_back(); for (const auto& replace : {"Processor", "CPU", "(R)", "(TM)", "Intel", "AMD", "Core"}) { name = s_replace(name, replace, ""); name = s_replace(name, " ", " "); } name = trim(name); } return name; } bool get_sensors() { Logger::debug("get_sensors(): show_coretemp=" + std::to_string(Config::getB("show_coretemp")) + " check_temp=" + std::to_string(Config::getB("check_temp"))); got_sensors = false; if (Config::getB("show_coretemp") and Config::getB("check_temp")) { ThermalSensors sensors; if (sensors.getSensors() > 0) { Logger::debug("M1 sensors found"); got_sensors = true; cpu_temp_only = true; macM1 = true; } else { // try SMC (intel) Logger::debug("checking intel"); SMCConnection smcCon; try { long long t = smcCon.getTemp(-1); // check if we have package T if (t > -1) { Logger::debug("intel sensors found"); got_sensors = true; t = smcCon.getTemp(0); if (t == -1) { // for some macs the core offset is 1 - check if we get a sane value with 1 if (smcCon.getTemp(1) > -1) { Logger::debug("intel sensors with offset 1"); core_offset = 1; } } } else { Logger::debug("no intel sensors found"); got_sensors = false; } } catch (std::runtime_error &e) { // ignore, we don't have temp got_sensors = false; } } } return got_sensors; } void update_sensors() { current_cpu.temp_max = 95; // we have no idea how to get the critical temp try { if (macM1) { ThermalSensors sensors; current_cpu.temp.at(0).push_back(sensors.getSensors()); if (current_cpu.temp.at(0).size() > 20) current_cpu.temp.at(0).pop_front(); } else { SMCConnection smcCon; int threadsPerCore = Shared::coreCount / Shared::physicalCoreCount; long long packageT = smcCon.getTemp(-1); // -1 returns package T current_cpu.temp.at(0).push_back(packageT); for (int core = 0; core < Shared::coreCount; core++) { long long temp = smcCon.getTemp((core / threadsPerCore) + core_offset); // same temp for all threads of same physical core if (cmp_less(core + 1, current_cpu.temp.size())) { current_cpu.temp.at(core + 1).push_back(temp); if (current_cpu.temp.at(core + 1).size() > 20) current_cpu.temp.at(core + 1).pop_front(); } } } } catch (std::runtime_error &e) { got_sensors = false; Logger::error("failed getting CPU temp"); } } string get_cpuHz() { unsigned int freq = 1; size_t size = sizeof(freq); int mib[] = {CTL_HW, HW_CPU_FREQ}; if (sysctl(mib, 2, &freq, &size, NULL, 0) < 0) { // this fails on Apple Silicon macs. Apparently you're not allowed to know return ""; } return std::to_string(freq / 1000.0 / 1000.0 / 1000.0).substr(0, 3); } auto get_core_mapping() -> unordered_flat_map { unordered_flat_map core_map; if (cpu_temp_only) return core_map; natural_t cpu_count; natural_t i; MachProcessorInfo info {}; kern_return_t error; error = host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &cpu_count, &info.info_array, &info.info_count); if (error != KERN_SUCCESS) { Logger::error("Failed getting CPU info"); return core_map; } for (i = 0; i < cpu_count; i++) { core_map[i] = i; } //? If core mapping from cpuinfo was incomplete try to guess remainder, if missing completely, map 0-0 1-1 2-2 etc. if (cmp_less(core_map.size(), Shared::coreCount)) { if (Shared::coreCount % 2 == 0 and (long) core_map.size() == Shared::coreCount / 2) { for (int i = 0, n = 0; i < Shared::coreCount / 2; i++) { if (std::cmp_greater_equal(n, core_sensors.size())) n = 0; core_map[Shared::coreCount / 2 + i] = n++; } } else { core_map.clear(); for (int i = 0, n = 0; i < Shared::coreCount; i++) { if (std::cmp_greater_equal(n, core_sensors.size())) n = 0; core_map[i] = n++; } } } //? Apply user set custom mapping if any const auto &custom_map = Config::getS("cpu_core_map"); if (not custom_map.empty()) { try { for (const auto &split : ssplit(custom_map)) { const auto vals = ssplit(split, ':'); if (vals.size() != 2) continue; int change_id = std::stoi(vals.at(0)); int new_id = std::stoi(vals.at(1)); if (not core_map.contains(change_id) or cmp_greater(new_id, core_sensors.size())) continue; core_map.at(change_id) = new_id; } } catch (...) { } } return core_map; } class IOPSInfo_Wrap { CFTypeRef data; public: IOPSInfo_Wrap() { data = IOPSCopyPowerSourcesInfo(); } CFTypeRef& operator()() { return data; } ~IOPSInfo_Wrap() { CFRelease(data); } }; class IOPSList_Wrap { CFArrayRef data; public: IOPSList_Wrap(CFTypeRef cft_ref) { data = IOPSCopyPowerSourcesList(cft_ref); } CFArrayRef& operator()() { return data; } ~IOPSList_Wrap() { CFRelease(data); } }; auto get_battery() -> tuple { if (not has_battery) return {0, 0, ""}; uint32_t percent = -1; long seconds = -1; string status = "discharging"; IOPSInfo_Wrap ps_info{}; if (ps_info()) { IOPSList_Wrap one_ps_descriptor(ps_info()); if (one_ps_descriptor()) { if (CFArrayGetCount(one_ps_descriptor())) { CFDictionaryRef one_ps = IOPSGetPowerSourceDescription(ps_info(), CFArrayGetValueAtIndex(one_ps_descriptor(), 0)); has_battery = true; CFNumberRef remaining = (CFNumberRef)CFDictionaryGetValue(one_ps, CFSTR(kIOPSTimeToEmptyKey)); int32_t estimatedMinutesRemaining; if (remaining) { CFNumberGetValue(remaining, kCFNumberSInt32Type, &estimatedMinutesRemaining); seconds = estimatedMinutesRemaining * 60; } CFNumberRef charge = (CFNumberRef)CFDictionaryGetValue(one_ps, CFSTR(kIOPSCurrentCapacityKey)); if (charge) { CFNumberGetValue(charge, kCFNumberSInt32Type, &percent); } CFBooleanRef charging = (CFBooleanRef)CFDictionaryGetValue(one_ps, CFSTR(kIOPSIsChargingKey)); if (charging) { bool isCharging = CFBooleanGetValue(charging); if (isCharging) { status = "charging"; } } if (percent == 100) { status = "full"; } } else { has_battery = false; } } else { has_battery = false; } } return {percent, seconds, status}; } auto collect(const bool no_update) -> cpu_info & { if (Runner::stopping or (no_update and not current_cpu.cpu_percent.at("total").empty())) return current_cpu; auto &cpu = current_cpu; double avg[3]; if (getloadavg(avg, sizeof(avg)) < 0) { Logger::error("failed to get load averages"); } cpu.load_avg = { (float)avg[0], (float)avg[1], (float)avg[2]}; natural_t cpu_count; natural_t i; kern_return_t error; processor_cpu_load_info_data_t *cpu_load_info = NULL; MachProcessorInfo info{}; error = host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &cpu_count, &info.info_array, &info.info_count); if (error != KERN_SUCCESS) { Logger::error("Failed getting CPU load info"); } cpu_load_info = (processor_cpu_load_info_data_t *)info.info_array; long long global_totals = 0; long long global_idles = 0; vector times_summed = {0, 0, 0, 0}; for (i = 0; i < cpu_count; i++) { vector times; //? 0=user, 1=nice, 2=system, 3=idle for (int x = 0; const unsigned int c_state : {CPU_STATE_USER, CPU_STATE_NICE, CPU_STATE_SYSTEM, CPU_STATE_IDLE}) { auto val = cpu_load_info[i].cpu_ticks[c_state]; times.push_back(val); times_summed.at(x++) += val; } try { //? All values const long long totals = std::accumulate(times.begin(), times.end(), 0ll); //? Idle time const long long idles = times.at(3); global_totals += totals; global_idles += idles; //? Calculate cpu total for each core if (i > Shared::coreCount) break; const long long calc_totals = max(0ll, totals - core_old_totals.at(i)); const long long calc_idles = max(0ll, idles - core_old_idles.at(i)); core_old_totals.at(i) = totals; core_old_idles.at(i) = idles; cpu.core_percent.at(i).push_back(clamp((long long)round((double)(calc_totals - calc_idles) * 100 / calc_totals), 0ll, 100ll)); //? Reduce size if there are more values than needed for graph if (cpu.core_percent.at(i).size() > 40) cpu.core_percent.at(i).pop_front(); } catch (const std::exception &e) { Logger::error("Cpu::collect() : " + (string)e.what()); throw std::runtime_error("collect() : " + (string)e.what()); } } const long long calc_totals = max(1ll, global_totals - cpu_old.at("totals")); const long long calc_idles = max(1ll, global_idles - cpu_old.at("idles")); //? Populate cpu.cpu_percent with all fields from syscall for (int ii = 0; const auto &val : times_summed) { cpu.cpu_percent.at(time_names.at(ii)).push_back(clamp((long long)round((double)(val - cpu_old.at(time_names.at(ii))) * 100 / calc_totals), 0ll, 100ll)); cpu_old.at(time_names.at(ii)) = val; //? Reduce size if there are more values than needed for graph while (cmp_greater(cpu.cpu_percent.at(time_names.at(ii)).size(), width * 2)) cpu.cpu_percent.at(time_names.at(ii)).pop_front(); ii++; } cpu_old.at("totals") = global_totals; cpu_old.at("idles") = global_idles; //? Total usage of cpu cpu.cpu_percent.at("total").push_back(clamp((long long)round((double)(calc_totals - calc_idles) * 100 / calc_totals), 0ll, 100ll)); //? Reduce size if there are more values than needed for graph while (cmp_greater(cpu.cpu_percent.at("total").size(), width * 2)) cpu.cpu_percent.at("total").pop_front(); if (Config::getB("show_cpu_freq")) { auto hz = get_cpuHz(); if (hz != "") { cpuHz = hz; } } if (Config::getB("check_temp") and got_sensors) update_sensors(); if (Config::getB("show_battery") and has_battery) current_bat = get_battery(); return cpu; } } // namespace Cpu namespace Mem { bool has_swap = false; vector fstab; fs::file_time_type fstab_time; int disk_ios = 0; vector last_found; mem_info current_mem{}; uint64_t get_totalMem() { return Shared::totalMem; } int64_t getCFNumber(CFDictionaryRef dict, const void *key) { CFNumberRef ref = (CFNumberRef)CFDictionaryGetValue(dict, key); if (ref) { int64_t value; CFNumberGetValue(ref, kCFNumberSInt64Type, &value); return value; } return 0; } string getCFString(io_registry_entry_t volumeRef, CFStringRef key) { CFStringRef bsdNameRef = (CFStringRef)IORegistryEntryCreateCFProperty(volumeRef, key, kCFAllocatorDefault, 0); if (bsdNameRef) { char buf[200]; CFStringGetCString(bsdNameRef, buf, 200, kCFStringEncodingASCII); CFRelease(bsdNameRef); return string(buf); } return ""; } bool isWhole(io_registry_entry_t volumeRef) { CFBooleanRef isWhole = (CFBooleanRef)IORegistryEntryCreateCFProperty(volumeRef, CFSTR("Whole"), kCFAllocatorDefault, 0); Boolean val = CFBooleanGetValue(isWhole); CFRelease(isWhole); return bool(val); } class IOObject { public: IOObject(string name, io_object_t& obj) : name(name), object(obj) {} virtual ~IOObject() { IOObjectRelease(object); } private: string name; io_object_t &object; }; void collect_disk(unordered_flat_map &disks, unordered_flat_map &mapping) { io_registry_entry_t drive; io_iterator_t drive_list; mach_port_t libtop_master_port; if (IOMasterPort(bootstrap_port, &libtop_master_port)) { Logger::error("errot getting master port"); return; } /* Get the list of all drive objects. */ if (IOServiceGetMatchingServices(libtop_master_port, IOServiceMatching("IOMediaBSDClient"), &drive_list)) { Logger::error("Error in IOServiceGetMatchingServices()"); return; } auto d = IOObject("drive list", drive_list); // dummy var so it gets destroyed while ((drive = IOIteratorNext(drive_list)) != 0) { auto dr = IOObject("drive", drive); io_registry_entry_t volumeRef; IORegistryEntryGetParentEntry(drive, kIOServicePlane, &volumeRef); if (volumeRef) { if (!isWhole(volumeRef)) { string bsdName = getCFString(volumeRef, CFSTR("BSD Name")); string device = getCFString(volumeRef, CFSTR("VolGroupMntFromName")); if (!mapping.contains(device)) { device = "/dev/" + bsdName; // try again with BSD name - not all volumes seem to have VolGroupMntFromName property } if (device != "") { if (mapping.contains(device)) { string mountpoint = mapping.at(device); if (disks.contains(mountpoint)) { auto& disk = disks.at(mountpoint); CFDictionaryRef properties; IORegistryEntryCreateCFProperties(volumeRef, (CFMutableDictionaryRef *)&properties, kCFAllocatorDefault, 0); if (properties) { CFDictionaryRef statistics = (CFDictionaryRef)CFDictionaryGetValue(properties, CFSTR("Statistics")); if (statistics) { disk_ios++; int64_t readBytes = getCFNumber(statistics, CFSTR("Bytes read from block device")); if (disk.io_read.empty()) disk.io_read.push_back(0); else disk.io_read.push_back(max((int64_t)0, (readBytes - disk.old_io.at(0)))); disk.old_io.at(0) = readBytes; while (cmp_greater(disk.io_read.size(), width * 2)) disk.io_read.pop_front(); int64_t writeBytes = getCFNumber(statistics, CFSTR("Bytes written to block device")); if (disk.io_write.empty()) disk.io_write.push_back(0); else disk.io_write.push_back(max((int64_t)0, (writeBytes - disk.old_io.at(1)))); disk.old_io.at(1) = writeBytes; while (cmp_greater(disk.io_write.size(), width * 2)) disk.io_write.pop_front(); // IOKit does not give us IO times, (use IO read + IO write with 1 MiB being 100% to get some activity indication) if (disk.io_activity.empty()) disk.io_activity.push_back(0); else disk.io_activity.push_back(clamp((long)round((double)(disk.io_write.back() + disk.io_read.back()) / (1 << 20)), 0l, 100l)); while (cmp_greater(disk.io_activity.size(), width * 2)) disk.io_activity.pop_front(); } } CFRelease(properties); } } } } } } } auto collect(const bool no_update) -> mem_info & { if (Runner::stopping or (no_update and not current_mem.percent.at("used").empty())) return current_mem; auto &show_swap = Config::getB("show_swap"); auto &show_disks = Config::getB("show_disks"); auto &swap_disk = Config::getB("swap_disk"); auto &mem = current_mem; static const bool snapped = (getenv("BTOP_SNAPPED") != NULL); vm_statistics64 p; mach_msg_type_number_t info_size = HOST_VM_INFO64_COUNT; if (host_statistics64(mach_host_self(), HOST_VM_INFO64, (host_info64_t)&p, &info_size) == 0) { mem.stats.at("free") = p.free_count * Shared::pageSize; mem.stats.at("cached") = p.external_page_count * Shared::pageSize; mem.stats.at("used") = (p.active_count + p.inactive_count + p.wire_count) * Shared::pageSize; mem.stats.at("available") = Shared::totalMem - mem.stats.at("used"); } int mib[2] = {CTL_VM, VM_SWAPUSAGE}; struct xsw_usage swap; size_t len = sizeof(struct xsw_usage); if (sysctl(mib, 2, &swap, &len, NULL, 0) == 0) { mem.stats.at("swap_total") = swap.xsu_total; mem.stats.at("swap_free") = swap.xsu_avail; mem.stats.at("swap_used") = swap.xsu_used; } if (show_swap and mem.stats.at("swap_total") > 0) { for (const auto &name : swap_names) { mem.percent.at(name).push_back(round((double)mem.stats.at(name) * 100 / mem.stats.at("swap_total"))); while (cmp_greater(mem.percent.at(name).size(), width * 2)) mem.percent.at(name).pop_front(); } has_swap = true; } else has_swap = false; //? Calculate percentages for (const auto &name : mem_names) { mem.percent.at(name).push_back(round((double)mem.stats.at(name) * 100 / Shared::totalMem)); while (cmp_greater(mem.percent.at(name).size(), width * 2)) mem.percent.at(name).pop_front(); } if (show_disks) { unordered_flat_map mapping; // keep mapping from device -> mountpoint, since IOKit doesn't give us the mountpoint double uptime = system_uptime(); auto &disks_filter = Config::getS("disks_filter"); bool filter_exclude = false; // auto &only_physical = Config::getB("only_physical"); auto &disks = mem.disks; vector filter; if (not disks_filter.empty()) { filter = ssplit(disks_filter); if (filter.at(0).starts_with("exclude=")) { filter_exclude = true; filter.at(0) = filter.at(0).substr(8); } } struct statfs *stfs; int count = getmntinfo(&stfs, MNT_WAIT); vector found; found.reserve(last_found.size()); for (int i = 0; i < count; i++) { std::error_code ec; string mountpoint = stfs[i].f_mntonname; string dev = stfs[i].f_mntfromname; mapping[dev] = mountpoint; if (string(stfs[i].f_fstypename) == "autofs") { continue; } //? Match filter if not empty if (not filter.empty()) { bool match = v_contains(filter, mountpoint); if ((filter_exclude and match) or (not filter_exclude and not match)) continue; } found.push_back(mountpoint); if (not disks.contains(mountpoint)) { disks[mountpoint] = disk_info{fs::canonical(dev, ec), fs::path(mountpoint).filename()}; if (disks.at(mountpoint).dev.empty()) disks.at(mountpoint).dev = dev; if (disks.at(mountpoint).name.empty()) disks.at(mountpoint).name = (mountpoint == "/" ? "root" : mountpoint); } if (not v_contains(last_found, mountpoint)) redraw = true; disks.at(mountpoint).free = stfs[i].f_bfree; disks.at(mountpoint).total = stfs[i].f_iosize; } //? Remove disks no longer mounted or filtered out if (swap_disk and has_swap) found.push_back("swap"); for (auto it = disks.begin(); it != disks.end();) { if (not v_contains(found, it->first)) it = disks.erase(it); else it++; } if (found.size() != last_found.size()) redraw = true; last_found = std::move(found); //? Get disk/partition stats for (auto &[mountpoint, disk] : disks) { if (std::error_code ec; not fs::exists(mountpoint, ec)) continue; struct statvfs vfs; if (statvfs(mountpoint.c_str(), &vfs) < 0) { Logger::warning("Failed to get disk/partition stats with statvfs() for: " + mountpoint); continue; } disk.total = vfs.f_blocks * vfs.f_frsize; disk.free = vfs.f_bfree * vfs.f_frsize; disk.used = disk.total - disk.free; disk.used_percent = round((double)disk.used * 100 / disk.total); disk.free_percent = 100 - disk.used_percent; } //? Setup disks order in UI and add swap if enabled mem.disks_order.clear(); if (snapped and disks.contains("/mnt")) mem.disks_order.push_back("/mnt"); else if (disks.contains("/")) mem.disks_order.push_back("/"); if (swap_disk and has_swap) { mem.disks_order.push_back("swap"); if (not disks.contains("swap")) disks["swap"] = {"", "swap"}; disks.at("swap").total = mem.stats.at("swap_total"); disks.at("swap").used = mem.stats.at("swap_used"); disks.at("swap").free = mem.stats.at("swap_free"); disks.at("swap").used_percent = mem.percent.at("swap_used").back(); disks.at("swap").free_percent = mem.percent.at("swap_free").back(); } for (const auto &name : last_found) if (not is_in(name, "/", "swap", "/dev")) mem.disks_order.push_back(name); disk_ios = 0; collect_disk(disks, mapping); old_uptime = uptime; } return mem; } } // namespace Mem namespace Net { unordered_flat_map current_net; net_info empty_net = {}; vector interfaces; string selected_iface; int errors = 0; unordered_flat_map graph_max = {{"download", {}}, {"upload", {}}}; unordered_flat_map> max_count = {{"download", {}}, {"upload", {}}}; bool rescale = true; uint64_t timestamp = 0; //* RAII wrapper for getifaddrs class getifaddr_wrapper { struct ifaddrs *ifaddr; public: int status; getifaddr_wrapper() { status = getifaddrs(&ifaddr); } ~getifaddr_wrapper() { freeifaddrs(ifaddr); } auto operator()() -> struct ifaddrs * { return ifaddr; } }; auto collect(const bool no_update) -> net_info & { auto &net = current_net; auto &config_iface = Config::getS("net_iface"); auto &net_sync = Config::getB("net_sync"); auto &net_auto = Config::getB("net_auto"); auto new_timestamp = time_ms(); if (not no_update and errors < 3) { //? Get interface list using getifaddrs() wrapper getifaddr_wrapper if_wrap{}; if (if_wrap.status != 0) { errors++; Logger::error("Net::collect() -> getifaddrs() failed with id " + to_string(if_wrap.status)); redraw = true; return empty_net; } int family = 0; char ip[NI_MAXHOST]; interfaces.clear(); string ipv4, ipv6; //? Iteration over all items in getifaddrs() list for (auto *ifa = if_wrap(); ifa != NULL; ifa = ifa->ifa_next) { if (ifa->ifa_addr == NULL) continue; family = ifa->ifa_addr->sa_family; const auto &iface = ifa->ifa_name; //? Get IPv4 address if (family == AF_INET) { if (getnameinfo(ifa->ifa_addr, sizeof(struct sockaddr_in), ip, NI_MAXHOST, NULL, 0, NI_NUMERICHOST) == 0) net[iface].ipv4 = ip; } //? Get IPv6 address else if (family == AF_INET6) { if (getnameinfo(ifa->ifa_addr, sizeof(struct sockaddr_in6), ip, NI_MAXHOST, NULL, 0, NI_NUMERICHOST) == 0) net[iface].ipv6 = ip; } //? Update available interfaces vector and get status of interface if (not v_contains(interfaces, iface)) { interfaces.push_back(iface); net[iface].connected = (ifa->ifa_flags & IFF_RUNNING); } } unordered_flat_map> ifstats; int mib[] = {CTL_NET, PF_ROUTE, 0, 0, NET_RT_IFLIST2, 0}; size_t len; if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) { Logger::error("failed getting network interfaces"); } else { std::unique_ptr buf(new char[len]); if (sysctl(mib, 6, buf.get(), &len, NULL, 0) < 0) { Logger::error("failed getting network interfaces"); } else { char *lim = buf.get() + len; char *next = NULL; for (next = buf.get(); next < lim;) { struct if_msghdr *ifm = (struct if_msghdr *)next; next += ifm->ifm_msglen; if (ifm->ifm_type == RTM_IFINFO2) { struct if_msghdr2 *if2m = (struct if_msghdr2 *)ifm; struct sockaddr_dl *sdl = (struct sockaddr_dl *)(if2m + 1); char iface[32]; strncpy(iface, sdl->sdl_data, sdl->sdl_nlen); iface[sdl->sdl_nlen] = 0; ifstats[iface] = std::tuple(if2m->ifm_data.ifi_ibytes, if2m->ifm_data.ifi_obytes); } } } } //? Get total recieved and transmitted bytes + device address if no ip was found for (const auto &iface : interfaces) { for (const string dir : {"download", "upload"}) { auto &saved_stat = net.at(iface).stat.at(dir); auto &bandwidth = net.at(iface).bandwidth.at(dir); uint64_t val = dir == "download" ? std::get<0>(ifstats[iface]) : std::get<1>(ifstats[iface]); //? Update speed, total and top values if (val < saved_stat.last) { saved_stat.rollover += saved_stat.last; saved_stat.last = 0; } if (cmp_greater((unsigned long long)saved_stat.rollover + (unsigned long long)val, numeric_limits::max())) { saved_stat.rollover = 0; saved_stat.last = 0; } saved_stat.speed = round((double)(val - saved_stat.last) / ((double)(new_timestamp - timestamp) / 1000)); if (saved_stat.speed > saved_stat.top) saved_stat.top = saved_stat.speed; if (saved_stat.offset > val + saved_stat.rollover) saved_stat.offset = 0; saved_stat.total = (val + saved_stat.rollover) - saved_stat.offset; saved_stat.last = val; //? Add values to graph bandwidth.push_back(saved_stat.speed); while (cmp_greater(bandwidth.size(), width * 2)) bandwidth.pop_front(); //? Set counters for auto scaling if (net_auto and selected_iface == iface) { if (saved_stat.speed > graph_max[dir]) { ++max_count[dir][0]; if (max_count[dir][1] > 0) --max_count[dir][1]; } else if (graph_max[dir] > 10 << 10 and saved_stat.speed < graph_max[dir] / 10) { ++max_count[dir][1]; if (max_count[dir][0] > 0) --max_count[dir][0]; } } } } //? Clean up net map if needed if (net.size() > interfaces.size()) { for (auto it = net.begin(); it != net.end();) { if (not v_contains(interfaces, it->first)) it = net.erase(it); else it++; } net.compact(); } timestamp = new_timestamp; } //? Return empty net_info struct if no interfaces was found if (net.empty()) return empty_net; //? Find an interface to display if selected isn't set or valid if (selected_iface.empty() or not v_contains(interfaces, selected_iface)) { max_count["download"][0] = max_count["download"][1] = max_count["upload"][0] = max_count["upload"][1] = 0; redraw = true; if (net_auto) rescale = true; if (not config_iface.empty() and v_contains(interfaces, config_iface)) selected_iface = config_iface; else { //? Sort interfaces by total upload + download bytes auto sorted_interfaces = interfaces; rng::sort(sorted_interfaces, [&](const auto &a, const auto &b) { return cmp_greater(net.at(a).stat["download"].total + net.at(a).stat["upload"].total, net.at(b).stat["download"].total + net.at(b).stat["upload"].total); }); selected_iface.clear(); //? Try to set to a connected interface for (const auto &iface : sorted_interfaces) { if (net.at(iface).connected) selected_iface = iface; break; } //? If no interface is connected set to first available if (selected_iface.empty() and not sorted_interfaces.empty()) selected_iface = sorted_interfaces.at(0); else if (sorted_interfaces.empty()) return empty_net; } } //? Calculate max scale for graphs if needed if (net_auto) { bool sync = false; for (const auto &dir : {"download", "upload"}) { for (const auto &sel : {0, 1}) { if (rescale or max_count[dir][sel] >= 5) { const uint64_t avg_speed = (net[selected_iface].bandwidth[dir].size() > 5 ? std::accumulate(net.at(selected_iface).bandwidth.at(dir).rbegin(), net.at(selected_iface).bandwidth.at(dir).rbegin() + 5, 0) / 5 : net[selected_iface].stat[dir].speed); graph_max[dir] = max(uint64_t(avg_speed * (sel == 0 ? 1.3 : 3.0)), (uint64_t)10 << 10); max_count[dir][0] = max_count[dir][1] = 0; redraw = true; if (net_sync) sync = true; break; } } //? Sync download/upload graphs if enabled if (sync) { const auto other = (string(dir) == "upload" ? "download" : "upload"); graph_max[other] = graph_max[dir]; max_count[other][0] = max_count[other][1] = 0; break; } } } rescale = false; return net.at(selected_iface); } } // namespace Net namespace Proc { vector current_procs; unordered_flat_map uid_user; string current_sort; string current_filter; bool current_rev = false; fs::file_time_type passwd_time; uint64_t cputimes; int collapse = -1, expand = -1; uint64_t old_cputimes = 0; atomic numpids = 0; int filter_found = 0; detail_container detailed; //* Generate process tree list void _tree_gen(proc_info &cur_proc, vector &in_procs, vector> &out_procs, int cur_depth, const bool collapsed, const string &filter, bool found = false, const bool no_update = false, const bool should_filter = false) { auto cur_pos = out_procs.size(); bool filtering = false; //? If filtering, include children of matching processes if (not found and (should_filter or not filter.empty())) { if (not s_contains(std::to_string(cur_proc.pid), filter) and not s_contains(cur_proc.name, filter) and not s_contains(cur_proc.cmd, filter) and not s_contains(cur_proc.user, filter)) { filtering = true; cur_proc.filtered = true; filter_found++; } else { found = true; cur_depth = 0; } } else if (cur_proc.filtered) cur_proc.filtered = false; //? Set tree index position for process if not filtered out or currently in a collapsed sub-tree if (not collapsed and not filtering) { out_procs.push_back(std::ref(cur_proc)); cur_proc.tree_index = out_procs.size() - 1; //? Try to find name of the binary file and append to program name if not the same if (cur_proc.short_cmd.empty() and not cur_proc.cmd.empty()) { std::string_view cmd_view = cur_proc.cmd; cmd_view = cmd_view.substr((size_t)0, min(cmd_view.find(' '), cmd_view.size())); cmd_view = cmd_view.substr(min(cmd_view.find_last_of('/') + 1, cmd_view.size())); cur_proc.short_cmd = (string)cmd_view; } } else { cur_proc.tree_index = in_procs.size(); } //? Recursive iteration over all children int children = 0; for (auto &p : rng::equal_range(in_procs, cur_proc.pid, rng::less{}, &proc_info::ppid)) { if (not no_update and not filtering and (collapsed or cur_proc.collapsed)) { out_procs.back().get().cpu_p += p.cpu_p; out_procs.back().get().mem += p.mem; out_procs.back().get().threads += p.threads; filter_found++; } if (collapsed and not filtering) { cur_proc.filtered = true; } else children++; _tree_gen(p, in_procs, out_procs, cur_depth + 1, (collapsed ? true : cur_proc.collapsed), filter, found, no_update, should_filter); } if (collapsed or filtering) return; //? Add tree terminator symbol if it's the last child in a sub-tree if (out_procs.size() > cur_pos + 1 and not out_procs.back().get().prefix.ends_with("]─")) out_procs.back().get().prefix.replace(out_procs.back().get().prefix.size() - 8, 8, " └─ "); //? Add collapse/expand symbols if process have any children out_procs.at(cur_pos).get().prefix = " │ "s * cur_depth + (children > 0 ? (cur_proc.collapsed ? "[+]─" : "[-]─") : " ├─ "); } string get_status(char s) { if (s & SRUN) return "Running"; if (s & SSLEEP) return "Sleeping"; if (s & SIDL) return "Idle"; if (s & SSTOP) return "Stopped"; if (s & SZOMB) return "Zombie"; return "Unknown"; } //* Get detailed info for selected process void _collect_details(const size_t pid, vector &procs) { if (pid != detailed.last_pid) { detailed = {}; detailed.last_pid = pid; detailed.skip_smaps = not Config::getB("proc_info_smaps"); } //? Copy proc_info for process from proc vector auto p_info = rng::find(procs, pid, &proc_info::pid); detailed.entry = *p_info; //? Update cpu percent deque for process cpu graph if (not Config::getB("proc_per_core")) detailed.entry.cpu_p *= Shared::coreCount; detailed.cpu_percent.push_back(clamp((long long)round(detailed.entry.cpu_p), 0ll, 100ll)); while (cmp_greater(detailed.cpu_percent.size(), width)) detailed.cpu_percent.pop_front(); //? Process runtime : current time - start time (both in unix time - seconds since epoch) struct timeval currentTime; gettimeofday(¤tTime, NULL); detailed.elapsed = sec_to_dhms(currentTime.tv_sec - (detailed.entry.cpu_s / 1'000'000)); if (detailed.elapsed.size() > 8) detailed.elapsed.resize(detailed.elapsed.size() - 3); //? Get parent process name if (detailed.parent.empty()) { auto p_entry = rng::find(procs, detailed.entry.ppid, &proc_info::pid); if (p_entry != procs.end()) detailed.parent = p_entry->name; } //? Expand process status from single char to explanative string detailed.status = get_status(detailed.entry.state); detailed.mem_bytes.push_back(detailed.entry.mem); detailed.memory = floating_humanizer(detailed.entry.mem); if (detailed.first_mem == -1 or detailed.first_mem < detailed.mem_bytes.back() / 2 or detailed.first_mem > detailed.mem_bytes.back() * 4) { detailed.first_mem = min((uint64_t)detailed.mem_bytes.back() * 2, Mem::get_totalMem()); redraw = true; } while (cmp_greater(detailed.mem_bytes.size(), width)) detailed.mem_bytes.pop_front(); rusage_info_current rusage; if (proc_pid_rusage(pid, RUSAGE_INFO_CURRENT, (void **)&rusage) == 0) { // this fails for processes we don't own - same as in Linux detailed.io_read = floating_humanizer(rusage.ri_diskio_bytesread); detailed.io_write = floating_humanizer(rusage.ri_diskio_byteswritten); } } //* Collects and sorts process information from /proc auto collect(const bool no_update) -> vector & { const auto &sorting = Config::getS("proc_sorting"); const auto &reverse = Config::getB("proc_reversed"); const auto &filter = Config::getS("proc_filter"); const auto &per_core = Config::getB("proc_per_core"); const auto &tree = Config::getB("proc_tree"); const auto &show_detailed = Config::getB("show_detailed"); const size_t detailed_pid = Config::getI("detailed_pid"); bool should_filter = current_filter != filter; if (should_filter) current_filter = filter; const bool sorted_change = (sorting != current_sort or reverse != current_rev or should_filter); if (sorted_change) { current_sort = sorting; current_rev = reverse; } const int cmult = (per_core) ? Shared::coreCount : 1; bool got_detailed = false; //* Use pids from last update if only changing filter, sorting or tree options if (no_update and not current_procs.empty()) { if (show_detailed and detailed_pid != detailed.last_pid) _collect_details(detailed_pid, current_procs); } else { //* ---------------------------------------------Collection start---------------------------------------------- { //* Get CPU totals natural_t cpu_count; kern_return_t error; processor_cpu_load_info_data_t *cpu_load_info = NULL; MachProcessorInfo info{}; error = host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &cpu_count, &info.info_array, &info.info_count); if (error != KERN_SUCCESS) { Logger::error("Failed getting CPU load info"); } cpu_load_info = (processor_cpu_load_info_data_t *)info.info_array; cputimes = 0; for (natural_t i = 0; i < cpu_count; i++) { cputimes += (cpu_load_info[i].cpu_ticks[CPU_STATE_USER] + cpu_load_info[i].cpu_ticks[CPU_STATE_NICE] + cpu_load_info[i].cpu_ticks[CPU_STATE_SYSTEM] + cpu_load_info[i].cpu_ticks[CPU_STATE_IDLE]); } } should_filter = true; int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0}; vector found; size_t size = 0; const auto timeNow = time_micros(); if (sysctl(mib, 4, NULL, &size, NULL, 0) < 0 || size == 0) { Logger::error("Unable to get size of kproc_infos"); } uint64_t cpu_t = 0; std::unique_ptr processes(new kinfo_proc[size / sizeof(kinfo_proc)]); if (sysctl(mib, 4, processes.get(), &size, NULL, 0) == 0) { size_t count = size / sizeof(struct kinfo_proc); for (size_t i = 0; i < count; i++) { //* iterate over all processes in kinfo_proc struct kinfo_proc& kproc = processes.get()[i]; const size_t pid = (size_t)kproc.kp_proc.p_pid; if (pid < 1) continue; found.push_back(pid); //? Check if pid already exists in current_procs bool no_cache = false; auto find_old = rng::find(current_procs, pid, &proc_info::pid); if (find_old == current_procs.end()) { current_procs.push_back({pid}); find_old = current_procs.end() - 1; no_cache = true; } auto &new_proc = *find_old; //? Get program name, command, username, parent pid, nice and status if (no_cache) { char fullname[PROC_PIDPATHINFO_MAXSIZE]; proc_pidpath(pid, fullname, sizeof(fullname)); const string f_name = std::string(fullname); size_t lastSlash = f_name.find_last_of('/'); new_proc.name = f_name.substr(lastSlash + 1); //? Get process arguments if possible, fallback to process path in case of failure if (Shared::arg_max > 0) { std::unique_ptr proc_chars(new char[Shared::arg_max]); int mib[] = {CTL_KERN, KERN_PROCARGS2, (int)pid}; size_t argmax = Shared::arg_max; if (sysctl(mib, 3, proc_chars.get(), &argmax, NULL, 0) == 0) { int argc = 0; memcpy(&argc, &proc_chars.get()[0], sizeof(argc)); std::string_view proc_args(proc_chars.get(), argmax); if (size_t null_pos = proc_args.find('\0', sizeof(argc)); null_pos != string::npos) { if (size_t start_pos = proc_args.find_first_not_of('\0', null_pos); start_pos != string::npos) { while (argc-- > 0 and null_pos != string::npos) { null_pos = proc_args.find('\0', start_pos); new_proc.cmd += (string)proc_args.substr(start_pos, null_pos - start_pos) + ' '; start_pos = null_pos + 1; } } } if (not new_proc.cmd.empty()) new_proc.cmd.pop_back(); } } if (new_proc.cmd.empty()) new_proc.cmd = f_name; new_proc.ppid = kproc.kp_eproc.e_ppid; new_proc.cpu_s = kproc.kp_proc.p_starttime.tv_sec * 1'000'000 + kproc.kp_proc.p_starttime.tv_usec; struct passwd *pwd = getpwuid(kproc.kp_eproc.e_ucred.cr_uid); new_proc.user = pwd->pw_name; } new_proc.p_nice = kproc.kp_proc.p_nice; new_proc.state = kproc.kp_proc.p_stat; //? Get threads, mem and cpu usage struct proc_taskinfo pti; if (sizeof(pti) == proc_pidinfo(new_proc.pid, PROC_PIDTASKINFO, 0, &pti, sizeof(pti))) { new_proc.threads = pti.pti_threadnum; new_proc.mem = pti.pti_resident_size; cpu_t = pti.pti_total_user + pti.pti_total_system; if (new_proc.cpu_t == 0) new_proc.cpu_t = cpu_t; } //? Process cpu usage since last update new_proc.cpu_p = clamp(round(((cpu_t - new_proc.cpu_t) * Shared::machTck) / ((cputimes - old_cputimes) * Shared::clkTck)) * cmult / 1000.0, 0.0, 100.0 * Shared::coreCount); //? Process cumulative cpu usage since process start new_proc.cpu_c = (double)(cpu_t * Shared::machTck) / (timeNow - new_proc.cpu_s); //? Update cached value with latest cpu times new_proc.cpu_t = cpu_t; if (show_detailed and not got_detailed and new_proc.pid == detailed_pid) { got_detailed = true; } } // //? Clear dead processes from current_procs auto eraser = rng::remove_if(current_procs, [&](const auto &element) { return not v_contains(found, element.pid); }); current_procs.erase(eraser.begin(), eraser.end()); //? Update the details info box for process if active if (show_detailed and got_detailed) { _collect_details(detailed_pid, current_procs); } else if (show_detailed and not got_detailed and detailed.status != "Dead") { detailed.status = "Dead"; redraw = true; } old_cputimes = cputimes; } } //* ---------------------------------------------Collection done----------------------------------------------- //* Sort processes if (sorted_change or not no_update) { if (reverse) { switch (v_index(sort_vector, sorting)) { case 0: rng::stable_sort(current_procs, rng::less{}, &proc_info::pid); break; case 1: rng::stable_sort(current_procs, rng::less{}, &proc_info::name); break; case 2: rng::stable_sort(current_procs, rng::less{}, &proc_info::cmd); break; case 3: rng::stable_sort(current_procs, rng::less{}, &proc_info::threads); break; case 4: rng::stable_sort(current_procs, rng::less{}, &proc_info::user); break; case 5: rng::stable_sort(current_procs, rng::less{}, &proc_info::mem); break; case 6: rng::stable_sort(current_procs, rng::less{}, &proc_info::cpu_p); break; case 7: rng::stable_sort(current_procs, rng::less{}, &proc_info::cpu_c); break; } } else { switch (v_index(sort_vector, sorting)) { case 0: rng::stable_sort(current_procs, rng::greater{}, &proc_info::pid); break; case 1: rng::stable_sort(current_procs, rng::greater{}, &proc_info::name); break; case 2: rng::stable_sort(current_procs, rng::greater{}, &proc_info::cmd); break; case 3: rng::stable_sort(current_procs, rng::greater{}, &proc_info::threads); break; case 4: rng::stable_sort(current_procs, rng::greater{}, &proc_info::user); break; case 5: rng::stable_sort(current_procs, rng::greater{}, &proc_info::mem); break; case 6: rng::stable_sort(current_procs, rng::greater{}, &proc_info::cpu_p); break; case 7: rng::stable_sort(current_procs, rng::greater{}, &proc_info::cpu_c); break; } } //* When sorting with "cpu lazy" push processes over threshold cpu usage to the front regardless of cumulative usage if (not tree and not reverse and sorting == "cpu lazy") { double max = 10.0, target = 30.0; for (size_t i = 0, x = 0, offset = 0; i < current_procs.size(); i++) { if (i <= 5 and current_procs.at(i).cpu_p > max) max = current_procs.at(i).cpu_p; else if (i == 6) target = (max > 30.0) ? max : 10.0; if (i == offset and current_procs.at(i).cpu_p > 30.0) offset++; else if (current_procs.at(i).cpu_p > target) { rotate(current_procs.begin() + offset, current_procs.begin() + i, current_procs.begin() + i + 1); if (++x > 10) break; } } } } //* Match filter if defined if (should_filter) { filter_found = 0; for (auto &p : current_procs) { if (not tree and not filter.empty()) { if (not s_contains(to_string(p.pid), filter) and not s_contains(p.name, filter) and not s_contains(p.cmd, filter) and not s_contains(p.user, filter)) { p.filtered = true; filter_found++; } else { p.filtered = false; } } else { p.filtered = false; } } } //* Generate tree view if enabled if (tree and (not no_update or should_filter or sorted_change)) { if (auto find_pid = (collapse != -1 ? collapse : expand); find_pid != -1) { auto collapser = rng::find(current_procs, find_pid, &proc_info::pid); if (collapser != current_procs.end()) { if (collapse == expand) { collapser->collapsed = not collapser->collapsed; } else if (collapse > -1) { collapser->collapsed = true; } else if (expand > -1) { collapser->collapsed = false; } } collapse = expand = -1; } if (should_filter or not filter.empty()) filter_found = 0; vector> tree_procs; tree_procs.reserve(current_procs.size()); //? Stable sort to retain selected sorting among processes with the same parent rng::stable_sort(current_procs, rng::less{}, &proc_info::ppid); //? Start recursive iteration over processes with the lowest shared parent pids for (auto &p : rng::equal_range(current_procs, current_procs.at(0).ppid, rng::less{}, &proc_info::ppid)) { _tree_gen(p, current_procs, tree_procs, 0, false, filter, false, no_update, should_filter); } //? Final sort based on tree index rng::stable_sort(current_procs, rng::less{}, &proc_info::tree_index); } numpids = (int)current_procs.size() - filter_found; return current_procs; } } // namespace Proc namespace Tools { double system_uptime() { struct timeval ts, currTime; std::size_t len = sizeof(ts); int mib[2] = {CTL_KERN, KERN_BOOTTIME}; if (sysctl(mib, 2, &ts, &len, NULL, 0) != -1) { gettimeofday(&currTime, NULL); return currTime.tv_sec - ts.tv_sec; } return 0.0; } } // namespace Toolsbtop-1.2.3/src/osx/sensors.cpp000066400000000000000000000103641420276253000162660ustar00rootroot00000000000000/* 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. indent = tab tab-size = 4 */ #include "sensors.hpp" #include #include #include #include #include extern "C" { typedef struct __IOHIDEvent *IOHIDEventRef; typedef struct __IOHIDServiceClient *IOHIDServiceClientRef; #ifdef __LP64__ typedef double IOHIDFloat; #else typedef float IOHIDFloat; #endif #define IOHIDEventFieldBase(type) (type << 16) #define kIOHIDEventTypeTemperature 15 IOHIDEventSystemClientRef IOHIDEventSystemClientCreate(CFAllocatorRef allocator); int IOHIDEventSystemClientSetMatching(IOHIDEventSystemClientRef client, CFDictionaryRef match); int IOHIDEventSystemClientSetMatchingMultiple(IOHIDEventSystemClientRef client, CFArrayRef match); IOHIDEventRef IOHIDServiceClientCopyEvent(IOHIDServiceClientRef, int64_t, int32_t, int64_t); CFStringRef IOHIDServiceClientCopyProperty(IOHIDServiceClientRef service, CFStringRef property); IOHIDFloat IOHIDEventGetFloatValue(IOHIDEventRef event, int32_t field); // create a dict ref, like for temperature sensor {"PrimaryUsagePage":0xff00, "PrimaryUsage":0x5} CFDictionaryRef matching(int page, int usage) { CFNumberRef nums[2]; CFStringRef keys[2]; keys[0] = CFStringCreateWithCString(0, "PrimaryUsagePage", 0); keys[1] = CFStringCreateWithCString(0, "PrimaryUsage", 0); nums[0] = CFNumberCreate(0, kCFNumberSInt32Type, &page); nums[1] = CFNumberCreate(0, kCFNumberSInt32Type, &usage); CFDictionaryRef dict = CFDictionaryCreate(0, (const void **)keys, (const void **)nums, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); CFRelease(keys[0]); CFRelease(keys[1]); return dict; } double getValue(IOHIDServiceClientRef sc) { IOHIDEventRef event = IOHIDServiceClientCopyEvent(sc, kIOHIDEventTypeTemperature, 0, 0); // here we use ...CopyEvent IOHIDFloat temp = 0.0; if (event != 0) { temp = IOHIDEventGetFloatValue(event, IOHIDEventFieldBase(kIOHIDEventTypeTemperature)); CFRelease(event); } return temp; } } // extern C long long Cpu::ThermalSensors::getSensors() { CFDictionaryRef thermalSensors = matching(0xff00, 5); // 65280_10 = FF00_16 // thermalSensors's PrimaryUsagePage should be 0xff00 for M1 chip, instead of 0xff05 // can be checked by ioreg -lfx IOHIDEventSystemClientRef system = IOHIDEventSystemClientCreate(kCFAllocatorDefault); IOHIDEventSystemClientSetMatching(system, thermalSensors); CFArrayRef matchingsrvs = IOHIDEventSystemClientCopyServices(system); std::vector temps; if (matchingsrvs) { long count = CFArrayGetCount(matchingsrvs); for (int i = 0; i < count; i++) { IOHIDServiceClientRef sc = (IOHIDServiceClientRef)CFArrayGetValueAtIndex(matchingsrvs, i); if (sc) { CFStringRef name = IOHIDServiceClientCopyProperty(sc, CFSTR("Product")); // here we use ...CopyProperty if (name) { char buf[200]; CFStringGetCString(name, buf, 200, kCFStringEncodingASCII); std::string n(buf); // this is just a guess, nobody knows which sensors mean what // on my system PMU tdie 3 and 9 are missing... // there is also PMU tdev1-8 but it has negative values?? // there is also eACC for efficiency package but it only has 2 entries // and pACC for performance but it has 7 entries (2 - 9) WTF if (n.starts_with("eACC") or n.starts_with("pACC")) { temps.push_back(getValue(sc)); } CFRelease(name); } } } CFRelease(matchingsrvs); } CFRelease(system); CFRelease(thermalSensors); if (temps.empty()) return 0ll; return round(std::accumulate(temps.begin(), temps.end(), 0ll) / temps.size()); } btop-1.2.3/src/osx/sensors.hpp000066400000000000000000000013411420276253000162660ustar00rootroot00000000000000/* 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. indent = tab tab-size = 4 */ namespace Cpu { class ThermalSensors { public: long long getSensors(); }; } // namespace Cpu btop-1.2.3/src/osx/smc.cpp000066400000000000000000000112031420276253000153450ustar00rootroot00000000000000/* 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. indent = tab tab-size = 4 */ #include "smc.hpp" static UInt32 _strtoul(char *str, int size, int base) { UInt32 total = 0; int i; for (i = 0; i < size; i++) { if (base == 16) { total += str[i] << (size - 1 - i) * 8; } else { total += (unsigned char)(str[i] << (size - 1 - i) * 8); } } return total; } static void _ultostr(char *str, UInt32 val) { str[0] = '\0'; sprintf(str, "%c%c%c%c", (unsigned int)val >> 24, (unsigned int)val >> 16, (unsigned int)val >> 8, (unsigned int)val); } namespace Cpu { SMCConnection::SMCConnection() { IOMasterPort(kIOMasterPortDefault, &masterPort); CFMutableDictionaryRef matchingDictionary = IOServiceMatching("AppleSMC"); result = IOServiceGetMatchingServices(masterPort, matchingDictionary, &iterator); if (result != kIOReturnSuccess) { throw std::runtime_error("failed to get AppleSMC"); } device = IOIteratorNext(iterator); IOObjectRelease(iterator); if (device == 0) { throw std::runtime_error("failed to get SMC device"); } result = IOServiceOpen(device, mach_task_self(), 0, &conn); IOObjectRelease(device); if (result != kIOReturnSuccess) { throw std::runtime_error("failed to get SMC connection"); } } SMCConnection::~SMCConnection() { IOServiceClose(conn); } long long SMCConnection::getSMCTemp(char *key) { SMCVal_t val; kern_return_t result; result = SMCReadKey(key, &val); if (result == kIOReturnSuccess) { if (val.dataSize > 0) { if (strcmp(val.dataType, DATATYPE_SP78) == 0) { // convert sp78 value to temperature int intValue = val.bytes[0] * 256 + (unsigned char)val.bytes[1]; return static_cast(intValue / 256.0); } } } return -1; } // core means physical core in SMC, while in core map it's cpu threads :-/ Only an issue on hackintosh? // this means we can only get the T per physical core // another issue with the SMC API is that the key is always 4 chars -> what with systems with more than 9 physical cores? // no Mac models with more than 18 threads are released, so no problem so far // according to VirtualSMC docs (hackintosh fake SMC) the enumeration follows with alphabetic chars - not implemented yet here (nor in VirtualSMC) long long SMCConnection::getTemp(int core) { char key[] = SMC_KEY_CPU_TEMP; if (core >= 0) { snprintf(key, 5, "TC%1dc", core); } long long result = getSMCTemp(key); if (result == -1) { // try again with C snprintf(key, 5, "TC%1dC", core); result = getSMCTemp(key); } return result; } kern_return_t SMCConnection::SMCReadKey(UInt32Char_t key, SMCVal_t *val) { kern_return_t result; SMCKeyData_t inputStructure; SMCKeyData_t outputStructure; memset(&inputStructure, 0, sizeof(SMCKeyData_t)); memset(&outputStructure, 0, sizeof(SMCKeyData_t)); memset(val, 0, sizeof(SMCVal_t)); inputStructure.key = _strtoul(key, 4, 16); inputStructure.data8 = SMC_CMD_READ_KEYINFO; result = SMCCall(KERNEL_INDEX_SMC, &inputStructure, &outputStructure); if (result != kIOReturnSuccess) return result; val->dataSize = outputStructure.keyInfo.dataSize; _ultostr(val->dataType, outputStructure.keyInfo.dataType); inputStructure.keyInfo.dataSize = val->dataSize; inputStructure.data8 = SMC_CMD_READ_BYTES; result = SMCCall(KERNEL_INDEX_SMC, &inputStructure, &outputStructure); if (result != kIOReturnSuccess) return result; memcpy(val->bytes, outputStructure.bytes, sizeof(outputStructure.bytes)); return kIOReturnSuccess; } kern_return_t SMCConnection::SMCCall(int index, SMCKeyData_t *inputStructure, SMCKeyData_t *outputStructure) { size_t structureInputSize; size_t structureOutputSize; structureInputSize = sizeof(SMCKeyData_t); structureOutputSize = sizeof(SMCKeyData_t); return IOConnectCallStructMethod(conn, index, // inputStructure inputStructure, structureInputSize, // ouputStructure outputStructure, &structureOutputSize); } } // namespace Cpu btop-1.2.3/src/osx/smc.hpp000066400000000000000000000052071420276253000153610ustar00rootroot00000000000000/* 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. indent = tab tab-size = 4 */ #pragma once #include #include #include #include #include #define VERSION "0.01" #define KERNEL_INDEX_SMC 2 #define SMC_CMD_READ_BYTES 5 #define SMC_CMD_WRITE_BYTES 6 #define SMC_CMD_READ_INDEX 8 #define SMC_CMD_READ_KEYINFO 9 #define SMC_CMD_READ_PLIMIT 11 #define SMC_CMD_READ_VERS 12 #define DATATYPE_FPE2 "fpe2" #define DATATYPE_UINT8 "ui8 " #define DATATYPE_UINT16 "ui16" #define DATATYPE_UINT32 "ui32" #define DATATYPE_SP78 "sp78" // key values #define SMC_KEY_CPU_TEMP "TC0P" // proximity temp? #define SMC_KEY_CPU_DIODE_TEMP "TC0D" // diode temp? #define SMC_KEY_CPU_DIE_TEMP "TC0F" // die temp? #define SMC_KEY_CPU1_TEMP "TC1C" #define SMC_KEY_CPU2_TEMP "TC2C" // etc #define SMC_KEY_FAN0_RPM_CUR "F0Ac" typedef struct { char major; char minor; char build; char reserved[1]; UInt16 release; } SMCKeyData_vers_t; typedef struct { UInt16 version; UInt16 length; UInt32 cpuPLimit; UInt32 gpuPLimit; UInt32 memPLimit; } SMCKeyData_pLimitData_t; typedef struct { UInt32 dataSize; UInt32 dataType; char dataAttributes; } SMCKeyData_keyInfo_t; typedef char SMCBytes_t[32]; typedef struct { UInt32 key; SMCKeyData_vers_t vers; SMCKeyData_pLimitData_t pLimitData; SMCKeyData_keyInfo_t keyInfo; char result; char status; char data8; UInt32 data32; SMCBytes_t bytes; } SMCKeyData_t; typedef char UInt32Char_t[5]; typedef struct { UInt32Char_t key; UInt32 dataSize; UInt32Char_t dataType; SMCBytes_t bytes; } SMCVal_t; namespace Cpu { class SMCConnection { public: SMCConnection(); virtual ~SMCConnection(); long long getTemp(int core); private: kern_return_t SMCReadKey(UInt32Char_t key, SMCVal_t *val); long long getSMCTemp(char *key); kern_return_t SMCCall(int index, SMCKeyData_t *inputStructure, SMCKeyData_t *outputStructure); io_connect_t conn; kern_return_t result; mach_port_t masterPort; io_iterator_t iterator; io_object_t device; }; } // namespace Cpu btop-1.2.3/themes/000077500000000000000000000000001420276253000137475ustar00rootroot00000000000000btop-1.2.3/themes/adapta.theme000066400000000000000000000042471420276253000162340ustar00rootroot00000000000000#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" btop-1.2.3/themes/ayu.theme000066400000000000000000000041461420276253000155760ustar00rootroot00000000000000# Main background, empty for terminal default, need to be empty if you want transparent background theme[main_bg]="#0B0E14" # Main text color theme[main_fg]="#BFBDB6" # Title color for boxes theme[title]="#BFBDB6" # Highlight color for keyboard shortcuts theme[hi_fg]="#E6B450" # Background color of selected item in processes box theme[selected_bg]="#E6B450" # Foreground color of selected item in processes box theme[selected_fg]="#f8f8f2" # Color of inactive/disabled text theme[inactive_fg]="#565B66" # Color of text appearing on top of graphs, i.e uptime and current network graph scaling theme[graph_text]="#BFBDB6" # Background color of the percentage meters theme[meter_bg]="#565B66" # Misc colors for processes box including mini cpu graphs, details memory graph and details status text theme[proc_misc]="#DFBFFF" # Cpu box outline color theme[cpu_box]="#DFBFFF" # Memory/disks box outline color theme[mem_box]="#95E6CB" # Net up/down box outline color theme[net_box]="#F28779" # Processes box outline color theme[proc_box]="#E6B673" # Box divider line and small boxes line color theme[div_line]="#565B66" # Temperature graph colors theme[temp_start]="#DFBFFF" theme[temp_mid]="#D2A6FF" theme[temp_end]="#A37ACC" # CPU graph colors theme[cpu_start]="#DFBFFF" theme[cpu_mid]="#D2A6FF" theme[cpu_end]="#A37ACC" # Mem/Disk free meter theme[free_start]="#95E6CB" theme[free_mid]="#95E6CB" theme[free_end]="#4CBF99" # Mem/Disk cached meter theme[cached_start]="#95E6CB" theme[cached_mid]="#95E6CB" theme[cached_end]="#4CBF99" # Mem/Disk available meter theme[available_start]="#95E6CB" theme[available_mid]="#95E6CB" theme[available_end]="#4CBF99" # Mem/Disk used meter theme[used_start]="#95E6CB" theme[used_mid]="#95E6CB" theme[used_end]="#4CBF99" # Download graph colors theme[download_start]="#F28779" theme[download_mid]="#F07178" theme[download_end]="#F07171" # Upload graph colors theme[upload_start]="#73D0FF" theme[upload_mid]="#59C2FF" theme[upload_end]="#399EE6" # Process box color gradient for threads, mem and cpu usage theme[process_start]="#FFCC66" theme[process_mid]="#E6B450" theme[process_end]="#FFAA33" btop-1.2.3/themes/dracula.theme000066400000000000000000000041461420276253000164130ustar00rootroot00000000000000# 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" btop-1.2.3/themes/dusklight.theme000066400000000000000000000045431420276253000167770ustar00rootroot00000000000000#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" btop-1.2.3/themes/flat-remix-light.theme000066400000000000000000000043471420276253000201600ustar00rootroot00000000000000#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" btop-1.2.3/themes/flat-remix.theme000066400000000000000000000043341420276253000170470ustar00rootroot00000000000000#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" btop-1.2.3/themes/greyscale.theme000066400000000000000000000040361420276253000167540ustar00rootroot00000000000000#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"btop-1.2.3/themes/gruvbox_dark.theme000066400000000000000000000044451420276253000174770ustar00rootroot00000000000000#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" btop-1.2.3/themes/gruvbox_dark_v2.theme000066400000000000000000000050711420276253000201020ustar00rootroot00000000000000# 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" btop-1.2.3/themes/gruvbox_material_dark.theme000066400000000000000000000044101420276253000213450ustar00rootroot00000000000000# Btop gruvbox material dark (https://github.com/sainnhe/gruvbox-material) theme # by Marco Radocchia # 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]="#d4be98" # Title color for boxes theme[title]="#d4be98" # Higlight color for keyboard shortcuts theme[hi_fg]="#ea6962" # Background color of selected items theme[selected_bg]="#d8a657" # Foreground color of selected items theme[selected_fg]="#282828" # 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]="#665c54" # Misc colors for processes box including mini cpu graphs, details memory graph and details status text theme[proc_misc]="#a9b665" # Cpu box outline color theme[cpu_box]="#7c6f64" # Memory/disks box outline color theme[mem_box]="#7c6f64" # Net up/down box outline color theme[net_box]="#7c6f64" # Processes box outline color theme[proc_box]="#7c6f64" # Box divider line and small boxes line color theme[div_line]="#7c6f64" # Temperature graph colors theme[temp_start]="#7daea3" theme[temp_mid]="#e78a4e" theme[temp_end]="#ea6962" # CPU graph colors theme[cpu_start]="#a9b665" theme[cpu_mid]="#d8a657" theme[cpu_end]="#ea6962" # Mem/Disk free meter theme[free_start]="#89b482" theme[free_mid]="" theme[free_end]="" # Mem/Disk cached meter theme[cached_start]="#7daea3" theme[cached_mid]="" theme[cached_end]="" # Mem/Disk available meter theme[available_start]="#d8a657" theme[available_mid]="" theme[available_end]="" # Mem/Disk used meter theme[used_start]="#ea6962" theme[used_mid]="" theme[used_end]="" # Download graph colors theme[download_start]="#e78a4e" theme[download_mid]="" theme[download_end]="" # Upload graph colors theme[upload_start]="#d3869b" theme[upload_mid]="" theme[upload_end]="" btop-1.2.3/themes/kyli0x.theme000066400000000000000000000042371420276253000162210ustar00rootroot00000000000000#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" btop-1.2.3/themes/matcha-dark-sea.theme000066400000000000000000000045121420276253000177170ustar00rootroot00000000000000#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" btop-1.2.3/themes/monokai.theme000066400000000000000000000045071420276253000164360ustar00rootroot00000000000000#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" btop-1.2.3/themes/night-owl.theme000066400000000000000000000044151420276253000167070ustar00rootroot00000000000000#Bashtop theme with night-owl colors #by zkourouma # 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]="#011627" # Main text color theme[main_fg]="#d6deeb" # Title color for boxes theme[title]="#ffffff" # Higlight color for keyboard shortcuts theme[hi_fg]="#addb67" # Background color of selected items theme[selected_bg]="#000000" # Foreground color of selected items theme[selected_fg]="#ffeb95" # Color of inactive/disabled text theme[inactive_fg]="#575656" # 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]="#22da6e" # Cpu box outline color theme[cpu_box]="#ffffff" # Memory/disks box outline color theme[mem_box]="#ffffff" # Net up/down box outline color theme[net_box]="#ffffff" # Processes box outline color theme[proc_box]="#ffffff" # Box divider line and small boxes line color theme[div_line]="#ffffff" # Temperature graph colors theme[temp_start]="#82aaff" theme[temp_mid]="#c792ea" theme[temp_end]="#fb4394" # CPU graph colors theme[cpu_start]="#22da6e" theme[cpu_mid]="#addb67" theme[cpu_end]="#ef5350" # Mem/Disk free meter theme[free_start]="#4e5900" theme[free_mid]="" theme[free_end]="#22da6e" # Mem/Disk cached meter theme[cached_start]="#82aaff" theme[cached_mid]="" theme[cached_end]="#82aaff" # Mem/Disk available meter theme[available_start]="#addb67" theme[available_mid]="" theme[available_end]="#ffeb95" # Mem/Disk used meter theme[used_start]="#ef5350" theme[used_mid]="" theme[used_end]="#ef5350" # 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]="#c792ea" theme[upload_end]="#c792ea" btop-1.2.3/themes/nord.theme000066400000000000000000000044071420276253000157420ustar00rootroot00000000000000#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" btop-1.2.3/themes/onedark.theme000066400000000000000000000033341420276253000164210ustar00rootroot00000000000000# Theme: OneDark # By: Vitor Melo # Main bg theme[main_bg]="#282c34" # Main text color theme[main_fg]="#abb2bf" # Title color for boxes theme[title]="#abb2bf" # Higlight color for keyboard shortcuts theme[hi_fg]="#61afef" # Background color of selected item in processes box theme[selected_bg]="#2c313c" # Foreground color of selected item in processes box theme[selected_fg]="#abb2bf" # Color of inactive/disabled text theme[inactive_fg]="#5c6370" # Misc colors for processes box including mini cpu graphs, details memory graph and details status text theme[proc_misc]="#61afef" # Cpu box outline color theme[cpu_box]="#5c6370" # Memory/disks box outline color theme[mem_box]="#5c6370" # Net up/down box outline color theme[net_box]="#5c6370" # Processes box outline color theme[proc_box]="#5c6370" # Box divider line and small boxes line color theme[div_line]="#5c6370" # Temperature graph colors theme[temp_start]="#98c379" theme[temp_mid]="#e5c07b" theme[temp_end]="#e06c75" # CPU graph colors theme[cpu_start]="#98c379" theme[cpu_mid]="#e5c07b" theme[cpu_end]="#e06c75" # Mem/Disk free meter theme[free_start]="#98c379" theme[free_mid]="#e5c07b" theme[free_end]="#e06c75" # Mem/Disk cached meter theme[cached_start]="#98c379" theme[cached_mid]="#e5c07b" theme[cached_end]="#e06c75" # Mem/Disk available meter theme[available_start]="#98c379" theme[available_mid]="#e5c07b" theme[available_end]="#e06c75" # Mem/Disk used meter theme[used_start]="#98c379" theme[used_mid]="#e5c07b" theme[used_end]="#e06c75" # Download graph colors theme[download_start]="#98c379" theme[download_mid]="#e5c07b" theme[download_end]="#e06c75" # Upload graph colors theme[upload_start]="#98c379" theme[upload_mid]="#e5c07b" theme[upload_end]="#e06c75" btop-1.2.3/themes/solarized_dark.theme000066400000000000000000000042161420276253000177730ustar00rootroot00000000000000#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"btop-1.2.3/themes/whiteout.theme000066400000000000000000000042631420276253000166500ustar00rootroot00000000000000#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"